这篇文章,我们不绕弯子,直接上硬菜把进程的“生与死”拆得明明白白。
进程创建:从fork函数那套双返回值的神奇机制讲起,再钻进写时拷贝(COW)的内存优化原理,看看 Linux 是怎么用“偷懒”的方式省下大把内存。最后聊聊fork的常规用法,以及它为什么偶尔会失败,失败背后的根本原因,我们一条条说清。
进程终止:进程的“死法”不止一种。我们会拆解正常退出、异常崩溃等三种典型退出场景,然后重点对比return、exit和_exit这三条退出路径:它们表面相似,底层资源回收的逻辑却大不相同。搞清楚这些,你才算真正摸透了一个进程的完整生命周期。话不多说,我们直接开讲。
目录
2.2.3 深入理解:我们一直在谈论的“缓冲区”到底是什么?
一、进程创建
1.1 fork函数初识
在Linux的世界里,fork绝对是一等一重要的函数。它的任务就一个:从现有的进程里,再“生”出一个新进程来。生出来的叫子进程,原来的那个自然就是父进程。
#include <unistd.h>
pid_t fork(void);
// 返回值:子进程中返回 0,父进程中返回子进程 id,出错返回 -1。
第一次看到这个返回值,很多人是懵的:一个函数,凭什么在父进程里返回一个值,在子进程里又返回另一个值?别急,这正是fork最神奇也最核心的地方,后面我们会专门拆开讲。先看进程调用fork之后,内核忙活了些什么。它可不是随随便便复制一份代码那么简单,而是有条不紊地干了四件事:
- 分配新家当:给子进程划出一块新的内存空间,再给它建一套全新的内核数据结构,比如属于它自己的PCB(task_struct)。从这一刻起,子进程在系统里有了独立户口。
- 复制老子的“人生档案”:把父进程的内核数据结构内容,一项一项拷贝给子进程。爹的优先级、内存映射、文件描述符,儿子都得继承一份。
- 挂上系统进程列表:把刚建好的子进程正式登记到系统的进程列表中,让它成为可以被调度器看到的合法成员。
- 返回,等待调度:fork函数返回,父子俩从同一行代码分道扬镳,接下来就全交给调度器安排,谁先跑、谁后跑,看它们各自的本事和运气。

1.1.1 fork之后的执行流表现
fork 一返回,系统里就多出了一条执行流。这话说来简单,但真到了代码层面,很多人还是会被它“反直觉”的表现打个措手不及。我们直接看一段最经典的演示:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id < 0) {
perror("fork failed");
return 1;
}
else if (id == 0) {
// 子进程走这里
printf("我是子进程, pid: %d, ppid: %d\n", getpid(), getppid());
}
else {
// 父进程走这里
printf("我是父进程, pid: %d, 子进程 id: %d\n", getpid(), id);
}
return 0;
}
运行之后,屏幕上会同时出现两行输出:一行父进程的,一行子进程的。同一个if判断,两个分支都被走了一遍,这不是代码逻辑出了问题,而是执行流真的从一条裂变成了两条。
几个必须想通的表现:
-
fork之前,只有父进程一条执行流。 那一行fork()是整个故事的分水岭。
-
fork之后,父子进程从同一行代码开始,各自带着不同的返回值,走向不同的分支。 父进程手里是子进程的PID,子进程手里是0。
-
先后顺序不确定。 调度器决定谁先上CPU。你可能会先看到“我是父进程”,也可能先看到“我是子进程”。多跑几次,输出顺序会有变化,这不是bug,是调度器在正常工作。
fork把一条执行流劈成了两条,它们共享同一份代码,却拥有各自独立的变量空间和身份。 这正是多进程编程最基本的认知起点。
1.2 写时拷贝
通常来说,父子进程的代码是共享的,两份PCB,同一份机器指令,谁也不多占一份。数据部分更有意思:在没人动手修改之前,父子俩连数据也是共享的,虚拟地址指向同一块物理内存,像两个人共用一本账本,读起来相安无事。
但“共享”总有个前提:只读不写。一旦其中一方起了改账本的念头,平衡立刻被打破。操作系统可不会允许你改我的数据、我改你的数据这种糊涂账发生。于是,写时拷贝机制登场了。
它的逻辑干脆利落:谁要写,就给谁单开一份。 当任意一方试图修改那块共享数据时,操作系统会立即介入,为要写入的那一方单独配置一份副本。把原数据拷过去,再把页表映射悄悄改掉,让它指向新副本。改的人改自己的,没改的人继续用原来的。两不相欠,互不干扰。
这就是写时拷贝(Copy-On-Write)的名字由来:写的时候才拷贝,不写就一直共享。 平时能省则省,真要改就当场分家。一份内存,两份虚拟地址,平时共享,写时分裂,把内存利用率拉满的同时,还牢牢守住了进程的独立性。

1.2.1 写时拷贝是如何实现的
写时拷贝的实现,靠的是一套精心设计的“只读陷阱”。整个过程分三步走,每一步都很精妙。
第一步:初始只读——给内存页设下“陷阱”
子进程刚被创建时,父子进程共享同一块物理内存。但注意一个细节:操作系统会故意把父子页表中对应的数据段都标记成只读。代码段本来就是只读的,这没问题;数据段也跟着变成只读,这就有点“反直觉”了。明明是可变的数据,凭什么不让写?答案是:这只是暂时的假象,为了后面那一下“捕风捉影”做准备。
第二步:尝试写入——硬件进行权限拦截
当程序真的试图去修改这些被标成只读的数据时,CPU的硬件权限检查会立刻跳出来拦住它。从硬件层面看,这是一次“访问违规”,但它不是真正的非法操作。表面上是报错,实际上是在向操作系统通风报信:有人想改共享内存了。
第三步:操作系统接管——缺页中断登场
这记“假报错”会引发一次缺页中断。操作系统一接到这个中断,立刻心领神会,不是进程出bug了,而是写时拷贝的触发条件成熟了。于是它不慌不忙地开始处理:给要写入的那一方单独分配一块新物理页,把原数据完整拷贝过去,再修改对应页表项,让它指向新页,同时把权限改回可读写。做完这一切,进程才被重新放回CPU,继续执行刚才那步被拦下的写操作。从进程的视角看,它只是“卡顿”了极短的一瞬间,根本意识不到背后发生了一次内存分裂。
整个过程可以浓缩成一句话:平时共享,只读设防;一旦写入,触发中断;拷贝副本,重新映射。 父子进程各自的独立性,就靠这套机制稳稳当当建立起来,谁也不碰谁的数据。
顺带一提,这个思路还被用在了更宏大的场景里,内核级虚拟机。当一台物理机上跑着多个虚拟机时,每个虚拟机都以为自己是独享硬件资源的,彼此之间互不干扰。其底层隔离原理,跟写时拷贝如出一辙:共享的是底层资源,分裂的是各自的运行状态。所以,写时拷贝不只是一个内存优化技巧,更是现代计算架构中“隔离”思想的基石。
1.2.2 为什么要采用写时拷贝?
因为这是操作系统对堆、数据段这些可写的虚拟内存区域,专门设计的一套“延迟复制”控制机制。说白了,内核在赌一件事:父子进程fork之后,很多时候并不会立刻去改那些共享数据。既然大概率只是读一读,那何必一出生就急吼吼地把整块数据复制两份?那不是白白浪费内存吗?于是它想了个办法:先让你们共享着,等到真有人要写的那一天,再现场分割。能拖就拖,省得浪费。写时拷贝,正是这种“懒到极致”的内存管理哲学在进程创建上的体现。
1.2.3 操作系统如何分辨数据所在的内存页?
答案就藏在内核的一个结构体里:vm_area_struct。
写时拷贝触发的缺页中断,本身是一种“有人要写只读页”的信号。但信号归信号,内核必须搞清楚:这到底是写时拷贝的合法信号,还是进程真的写了不该写的地方?
这时候,内核会顺着出错的虚拟地址,去查该地址所属的vm_area_struct。这个结构体里记录着这块区域的原始权限,它本来是可读写的,还是本来就只读的。
-
如果这块区域原本就具备可写权限,只是之前为了写时拷贝被临时改成了只读,那内核立刻就明白了:哦,是自家进程要分家,不是越界。于是触发写时拷贝流程,分配新页、修改映射、恢复可写,放行。
-
如果这块区域本来就只读(比如字符串常量所在的.rodata段),那这次写操作就是货真价实的非法访问。内核直接按规矩办事,一个Segmentation Fault 把进程拍死。
所以,vm_area_struct就像每块内存区域的“原始档案”。页表上的权限可以临时变动,但档案里记着它的本来面目。有了这份档案,内核才能在“合法的写时拷贝”和“非法的内存越界”之间,划出一道清晰的界线。权限可以变,档案说了算。
1.2.4 页表查询的几种典型情况
进程每次访问内存,都免不了要去页表里走一遭。这趟查询,通常会有三种结局:
-
正常查询:权限对得上,映射关系也在,一切顺利。CPU 拿着虚拟地址进去,换出一个物理地址,数据到手,程序继续跑。
-
查无此项或权限不符:要么页表里压根没有这个虚拟地址的条目,要么权限位不让你这么干。这种情况一旦发生,多半是程序自己出了问题,野指针、越界写、碰了不该碰的只读区。
-
缺页中断:页表里有这个虚拟地址的“户口”,但背后没有对应的物理页。这不算错误,而是操作系统在说:“稍等,我去拿货。”于是触发物理内存申请、物理页拷贝、页表重映射这一整套补票流程。
1.2.5 为什么fork要采用写时拷贝?
写时拷贝的存在,至少解决了两件大事:
-
加速子进程的创建。如果没有这套机制,每次fork都要把父进程的全部物理内存原原本本复制一份,那进程创建就会慢得像蜗牛爬。写时拷贝让父子先共享着,创建过程几乎零拷贝,速度自然上去了。
-
减少内存里的冗余。既然父子大部分数据短期内都不会被改,何必让同一份代码和数据在内存里存两遍?写时拷贝的策略就是:不改就一直共用,谁要改才给谁单开。这样一来,物理内存的利用率被拉到了极限。
写时拷贝不只是一个省内存的小技巧。它在物理层面为父子进程划出了清晰的隔离边界,你要写,就分家;你不写,我们继续搭伙。独立性有了,内存也省了,一举两得。这,才是现代操作系统心甘情愿绕这么大一圈设计这套机制的根本原因。
1.2.6 写时拷贝完成后,读写权限如何恢复?
写时拷贝触发之后,权限恢复这件事,父子双方并不是同步进行的。这里有个容易忽略的细节。
-
子进程:一旦发生写时拷贝,它就被映射到了全新的物理页上。从这一刻起,子进程对应的页表项权限立即恢复为可读写。新页只属于它自己,想怎么改就怎么改,无需再设防。
-
父进程:情况要复杂一点。完成写时拷贝后,父进程原有的数据段并不会马上变回可读写。为什么?因为可能还有别的子进程仍在跟它共享那部分物理页。内核必须等所有共享这份数据的子进程都完成了写时拷贝、彻底脱离共享之后,才敢把父进程的页表权限恢复为可读写。否则,万一还有谁在共用着,这权限一开,隔离就形同虚设了。
所以,父进程的权限恢复是最后一步,只有当共享被彻底拆干净,它才重获自由。这个过程由内核自动完成,不需要进程自己操心。
1.3 fork的常见用法
理解了fork的机制,再来看看它在实际工程里最常扮演的两个角色。
用法一:复制自己——父子进程分头干活
一个父进程想“复制”出一个自己,让父子进程同时执行不同的逻辑。最典型的场景是服务器模型:父进程作为常驻服务,一直守在门口等客户端请求。一旦有请求进来,它立刻fork出一个子进程去处理这个请求,而父进程自己则转身继续等待下一个请求。这样既保证了并发处理能力,又让主进程保持轻量,不会因为某个请求的阻塞而停下整个服务。
用法二:换壳重生——通过exec执行另一个程序
另一个经典用法,是让进程彻底“改头换面”。子进程从fork返回后,立即调用exec族函数,加载并执行一个全新的程序,把自己当前的进程空间整个替换掉。fork负责“生”,exec负责“变身”,两者配合,就能在一个进程里启动另一个完全不同的程序。Shell里你敲的每一条命令,几乎都是这个模式的产物:bash先fork一个子进程,子进程再exec你要运行的那个程序。
这两种用法,一个偏“分身协作”,一个偏“脱胎换骨”,共同构成了多进程编程最基础也最实用的两块积木。
1.4 fork调用失败的原因
fork不是万能的,它也有翻车的时候。一旦返回值是 -1,别急着怀疑代码,先看看是不是下面这几个原因在作祟。
原因一:系统进程数量达到限制
操作系统对进程总数是有上限的。当系统里已经挤满了进程,达到了允许的最大值,你再调用fork,内核就是想给你开新户口也拿不出名额了。进程表满了,新的PCB自然无从谈起,fork只能灰溜溜地返回-1。
原因二:单个用户的进程数量达到上限
除了系统全局的上限,操作系统还给每个用户单独设了一道配额。这是为了防止某个用户恶意或无意地创建海量进程,把整台机器的资源吃干抹净。如果你名下的进程数已经顶到了配额上限,再fork也会被无情的拒绝。这属于“个人额度”刷爆了,不是系统不给你面子,是规矩不让。
原因三:内存空间不足
这是最现实也最让人头疼的一种情况。fork创建子进程,虽然代码和数据可以靠写时拷贝先共享着,但有一些东西是省不掉的,PCB得新建,内核栈得分配,页表得搭起来。如果物理内存已经紧张到连这些基础结构都撑不起来,fork自然就失败了。具体来说,内存不足会在两个环节卡住进程:
-
进程PCB(task_struct)创建失败。这是最直接的,连户口本都发不出来,子进程根本无从诞生。
-
无法成功加载代码与数据。即便PCB勉强建出来了,如果内存实在挤不出地方来安放新的代码段和数据段,进程也就成了一个空壳,无法真正运行起来。
二、进程退出
前面我们聊了进程怎么“生”,现在来看看它怎么“死”。一个进程执行完毕或被外力终止后,结局无非三种。
2.1 进程退出的常见场景
2.1.1 进程退出后的三种结局
第一种:功德圆满。 代码老老实实从头跑到尾,结果也完全符合预期。这是最理想的收场,main 函数通常返回0,表示一切尽在掌握。
第二种:跑完了,但事儿没办成。 程序安全抵达终点,没有崩溃,可最终结果却跟预想差得远。这时候main函数会返回一个非0值,比如1、2、3……每个值代表不同的出错原因。这些非0值在系统层面有个正式名称进程退出码。它就像进程留给父进程的一纸“交代”,告诉对方自己是怎么失败收场的。
第三种:中途横死。 程序跑到一半,突然踩中致命错误,野指针乱飞、除以零、越界访问……这种时候,进程不是正常走到终点,而是被操作系统当场击毙。准确地说,它是收到了某个致命信号,比如SIGSEGV,被系统强制终止。代码压根没执行完,什么退出码、返回值,全都失去了意义。此时唯一有价值的线索,是那个终结它生命的信号。
那这些“遗言”究竟说给谁听?层级很清楚:子进程的退出码,是留给父进程查收的;而main函数的返回值,则是返回给调用main的那个函数,也就是我们之前提过的_start。层层上交,各找各的家长。
2.1.2 进程退出码是什么?
进程退出时留下的那个数字,从来不是随便给的。每个退出码都有它的潜台词,看懂这张表,你就能一眼识破程序是善终还是横死。
| 退出码 | 解释 |
|---|---|
| 0 | 命令成功执行,功德圆满 |
| 1 | 通用错误代码,表示“事儿没办成” |
| 2 | 命令或参数使用不当 |
| 126 | 权限被拒绝,或无法执行 |
| 127 | 命令未找到,或PATH配置有误 |
| 128+n | 被外部信号终止,n为终止信号编号 |
| 130 | 被Ctrl+C或SIGINT中断(对应128+2) |
| 143 | 被SIGTERM终止(默认的终止信号) |
| 255/* | 退出码超出0-255范围,重新计算(对256取模) |
几个高频退出码,值得单独拉出来聊聊。
退出码0,是所有程序梦寐以求的终点。它表示命令执行无误,完美收工,是理想状态中的理想状态。
退出码1,是最常见的“失败脸”。我们可以把它理解为“不被允许的操作”。举个现实的例子:你没有sudo权限却硬要跑yum装软件,系统就会甩你一个退出码1。再比如在let里写一句let a=1/0,除零错误也会返回1。它不负责解释具体错哪了,只负责告诉你:出事了,但不是什么光彩的事。
130和143,这两个属于“横死”典型案例。它们都是128+n家族的一员,n是终止信号编号。130 = 128 + 2,对应SIGINT,就是你Ctrl+C强行打断程序的场景。143 = 128 + 7,对应SIGTERM,这是kill命令默认发出的终止信号,属于“奉命处决”。
2.2 进程退出的常见方式与核心机制
一个进程要退出,摆在明面上的路主要有三条:
- 在main函数里return
- 在任意位置调用C标准库的exit()
- 直接调用Linux的系统调用_exit()
这三条路,表面看都是退出,底层机制却大不一样。当然,还有一条“横死”的野路子,异常退出,最典型的就是Ctrl+C或者kill -9进程PID,一个信号过去,进程当场被击毙,连句遗言都来不及留。
2.2.1 return退出机制与返回值编码原理
从语言层面看,main函数的返回值是整份程序执行情况的“总结报告”。但你想过没有:这个返回值,是怎么从一个函数传到另一个函数手里,又是怎么从一个进程传到它父进程手里的?这就得扒开函数调用的汇编层,看看底层到底是怎么跑的。整个过程分三步走:
-
第一步:调用。func1()调用func2(),一条call func2指令,把程序流推进去。
-
第二步:塞返回值。func2执行完,把返回值1写进eax寄存器,然后执行ret返回。
-
第三步:接收。func1从eax寄存器里把那个1取出来,挪进自己的局部变量。
看出来了吗?返回值不是从函数里“飞”出去的,而是被塞进一个叫eax的寄存器里,由调用方自己来取的。这个寄存器,就是函数之间传递返回值的秘密通道。
补充要点:关于默认返回值
函数返回值在语言层面有时候不写也不会报错。比如你定义了一个int func(),但函数体里压根没有return语句。很多编译器会好心地给你一个警告,但程序还能跑。为什么?
因为底层汇编里少了一步:没有显式往eax寄存器里写返回值。但程序不管这些,它照旧执行 mov 转移,把 eax 寄存器当前的值搬走。于是,eax里默认的0就被当成了返回值,传给了调用方。所以没写return的函数,最终返回的往往是0,不是语言规定,而是寄存器的锅。
main函数也逃不掉这套底层机制。你写不写return,它最终都从eax里拿值。C语言里默认返回值类型就是int,所以以int为返回值的函数,理论上不写return也能编译过,但返回什么,你心里得有点数。别让寄存器的默认值替你背锅。
2.2.2 exit与_exit的底层区别
除了main函数正常走到终点,程序其实在任何一个角落都可以直接结束,只要调用exit或者_exit。它俩的参数,就是最终要交给父进程的退出码。
int main()
{
exit(200); // 退出码就是 200
return 0; // 这行永远执行不到
}
这俩函数名字只差一个下划线,但对待“缓冲数据”的态度,堪称冰火两重天。先看两条退出路径:
用户调用exit() ──> 刷新库缓冲区 ──> 调用内核_exit() ──> 进程彻底终止
用户调用_exit() ───────────────────────────────────> 进程彻底终止
看明白了吗?exit比_exit多走了一步,刷新缓冲区。这一步,直接决定了数据是安全落地,还是半路蒸发。
-
走exit()退出:进程在临死前,会把滞留在缓冲区里的数据老老实实刷到屏幕或文件里。该交代的都交代完,才算真正咽气。
-
走_exit()退出:进程死得干脆利落,但那些还憋在缓冲区里没来得及输出的数据,直接跟着它一起陪葬,一个字都留不下。
空口无凭,看两组代码。第一组用exit:
int main()
{
printf("hello");
exit(0);
}
// 运行结果:
// [root@localhost linux]# ./a.out
// hello[root@localhost linux]#
hello没加\n,按行缓冲的规矩,它会先待在缓冲区里。但exit在退出前把缓冲区冲了一遍,所以hello安安稳稳地出现在屏幕上。
第二组用_exit:
int main()
{
printf("hello");
_exit(0);
}
// 运行结果:
// [root@localhost linux]# ./a.out
// [root@localhost linux]#
屏幕上一片空白。hello去哪了?它老老实实躺在缓冲区里,等着被刷出去。可惜,_exit没给这个机会,进程直接咽气,缓冲区里的数据也跟着灰飞烟灭。exit之所以能这么从容,是因为它在内部依次办了这几件事:
- 执行用户注册的清理函数。通过atexit或on_exit挂上来的那些收尾逻辑,全部按序执行一遍。
- 关闭所有打开的流。所有缓存里的数据,强制写入各自的文件或设备。
- 调用内核级_exit。一切收拾妥当,才真正完成进程终止。
所以结论很清晰:exit是优雅退场,先把屁股擦干净;_exit是提桶跑路,连门都不关。 如果你的程序里还有没刷出去的日志、没写完的文件,别用_exit走人,除非你确定那些数据本来就该丢。

2.2.3 深入理解:我们一直在谈论的“缓冲区”到底是什么?
从exit()和_exit()的行为差异出发,可以推导出一个极其重要的结论:
我们之前谈论的输出缓冲区,一定不是操作系统内核内部的缓冲区。
这个推理过程干净利落:假如这个缓冲区真的建立在内核里,那么无论是走库函数exit()退出,还是直接走系统调用_exit()退出,操作系统都应该统一刷新这个内核缓冲区才对。因为系统调用是直接跟内核对话的,内核自己的东西,它没理由不收拾干净。
但事实却恰恰相反_exit()作为系统调用,完全无视这个缓冲区的存在,任由里面的数据灰飞烟灭。这说明什么?说明那个缓冲区根本不在内核的地盘上,内核管不着它。
那么真相就浮出水面了:exit()本质上只是对_exit()的一层封装。它在调用_exit()之前,额外加了一段“刷新缓冲区”的代码。刷新的是谁家的缓冲区?
当然是库函数自己维护的用户态缓冲区。内核从头到尾都不知道这个缓冲区的存在,所以_exit()也压根不会去碰它。
至于那个缓冲区究竟藏在哪、怎么组织、何时刷新,现在还不是揭晓的时候。等我们讲到文件I/O的时候,再把这个埋下的钩子彻底拉起来。你只需要记住一句话:缓冲区不在内核,在库函数的地盘上。
2.3 进程退出码的回收与访问
2.3.1 父进程如何获取子进程退出码?
当进程通过前面说的任意一种方式退场,并且带上了那个status退出状态时,这个数字并不会凭空蒸发。它要经历两站旅程。
第一站:写进户口本。
子进程退出时,它的退出码会被内核郑重地写进它自己的task_struct里。没错,就是那个进程控制块。这个结构体里专门预留了一个退出变量,像一个小信箱,负责收下这份最后的数字。子进程的“临终遗言”,就安安静静地躺在这个信箱里,等着有人来拆封。
第二站:父进程来认领。
写完遗言之后,子进程并不会立刻烟消云散,而是进入我们前面讲过的僵尸状态(Zombie)。它继续挂在进程表里,不干活、不占内存,唯一的存在意义,就是等父进程来收尸。直到父进程通过wait或waitpid把它回收掉,那个躺在task_struct里的退出码,才会真正递到父进程手上。
拆开信封的那一刻,父进程才终于明白:自己派出去的这个任务,到底是顺顺当当办成了,还是半路出了什么岔子。退出码不是喊一嗓子就消失的,它得先存档,再等人领。
2.3.2 如何正确访问进程退出状态?
日常开发中,父进程通常是Bash。当我们运行./proc之后,它就作为Bash的子进程跑起来了。程序跑完,我们最关心的自然是:它最后干得怎么样?答案就藏在一个特殊变量里。
[abc@]$ ./proc
[abc@]$ echo $?
1
这里的$?是Shell中的一个特殊变量,专门用来存放上一个前台进程退出时的状态码。因为./proc的父进程正好是Bash,所以它的退出码自然能被Bash收走,我们再用echo $?把它亮出来。这里返回1,就代表程序是带着退出码1离开的。
不过这里藏着一个挺容易踩坑的细节:虽然status的类型是int,但真正能被父进程取走的,只有它的低8位。所以当你用_exit(-1)退出时,终端里执行echo $? 看到的返回值会是255。为什么?-1在二进制补码下是32位全1,截取低8位就是 11111111,换算成十进制正好是255。至于为什么要截取低8位,这个问题放到进程等待那一节再仔细讲,现在先记住有这么回事就够了。
另外,errno也是日常排错里的常客。它是C标准库提供的一个全局变量,定义在<errno.h>里。你可以把它简单理解成“错误码”:当某个库函数调用失败时,errno会被设置成一个非零值,告诉你具体错在哪。下面这个函数就是用它来当返回值:
int Func02()
{
FILE* fp = fopen("log.txt", "r");
if (fp == NULL) return errno;
fclose(fp);
return 0;
}
打开文件失败时,函数直接返回errno。上层拿到这个值,再一查错误码表,就能定位到失败原因。所以,退出码和errno虽然都是数字,但一个是进程的“遗言”,一个是库函数的“报错”,各司其职,别搞混了。
2.3.3 退出码如何转化为错误信息?
退出码是一串干巴巴的数字,光看数字,你永远猜不到背后发生了什么。好在Linux早就给我们配了一位“翻译官”——strerror函数。
#include <string.h>
char *strerror(int errnum); // 将错误码转化为错误描述
它接收一个错误码,返回一段描述这个错误的字符串。比如错误码2,它可能会告诉你“No such file or directory”。有了它,排查问题时就不用抱着数字去查手册了。那系统里到底有多少种错误码?我们写个小程序,把前133个错误码和它们对应的描述一次性全打印出来:
#include <cstdio>
#include <string.h>
using namespace std;
void Func01() {
for (int i = 0; i < 133; i++)
printf("%d -> %s\n", i, strerror(i));
}
int main() {
Func01();
return 0;
}
运行之后,屏幕上会哗啦啦列出一长串数字和对应的英文描述。每一个数字,都是某一类错误的“身份证”;而strerror,就是那台专门查证件的机器。下次你的程序只丢给你一个冷冰冰的错误码时,别慌,交给strerror,它会一五一十告诉你到底发生了什么。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注三连支持。你的每一个正反馈,都是我继续硬核输出的最大动力。我们下篇见。
:进程篇·八——进程创建与退出:深入剖析fork、写时拷贝与退出码》&spm=1001.2101.3001.5002&articleId=163876451&d=1&t=3&u=9afbdfa00c6c4284a6b4d0a4a12759e3)
2167

被折叠的 条评论
为什么被折叠?



