进程结束留下的最后一句话是退出码(0 成功、非 0 失败)。死法有三种(自己退、被信号杀、崩溃),死完还要有人收尸——不 wait 的父进程会留下僵尸。
3.1 造、3.2 复制、3.3 换内容,这一节收尾:怎么退场,退完之后资源归谁。这是进程生命周期的最后一环,也是进程管理这一章唯一涉及【回收】的地方。
一、三种结束方式
一、自己体面地退出:exit() 或从 main 返回。退出码交给父进程,0 表示成功,非 0 表示失败(具体含义自定)。
二、被信号杀死:kill -9、Ctrl+C、内存越界(SIGSEGV)。进程没有机会说最后一句话,父进程拿到的是【信号编号】。
三、崩溃:严格说是第二种的特例,内核发现它干了不该干的事(段错误、除零、非法指令),于是给它发信号。可能留下核心转储。
exit() 与 _exit() 差一步:前者会先刷 stdio 缓冲、跑 atexit 回调、再进内核;后者直接进内核。fork 出来的子进程通常该用 _exit()——否则它会替父进程把缓冲区刷一遍,输出就重复了(3.2 那个坑)。
二、退出码:进程留下的最后一句话
约定只有一条,但极其重要:0 是成功,非 0 是失败。
1、2、42 之间没有标准含义——它们的意思只有写程序的人知道,所以正经程序会把退出码写进文档。
shell 靠它决定下一步:cmd && next(成功才继续)、cmd || fallback、$? 取上一个命令的退出码。
管道的退出码有个坑:a | b 的退出码默认取最后一个命令的,前面失败了也看不出来——所以 bash 有 set -o pipefail。
三、收尸:僵尸与孤儿
进程死了,但它的档案不能马上删:父进程可能还想读它的退出码。于是在【已终止但还没被读】这个状态上停一下——这就是僵尸(zombie)。
僵尸只占一条进程表记录,不占内存,所以一堆僵尸通常不致命;但进程号是有限的,攒多了就起不了新进程。
收尸的动作叫 wait:父进程调用它,读走退出码,内核才把记录删掉。
孤儿是另一回事:父进程先死了,子进程还活着——它被过继给 1 号,由 1 号负责收尸。所以孤儿不可怕,僵尸才要处理。
为什么守护进程要【双 fork】:第一次 fork 之后让父进程立刻退出,孙进程就成了孤儿被 1 号收养——这样它永远不会有僵尸父进程,也不会跟着终端一起死。
四、回收的账要算清
进程结束时内核要收回的东西:地址空间(所有页)、打开的文件(引用计数减一,归零才真关)、内存映射、信号处理、定时器、子进程关系、进程表记录。
这份清单解释了为什么【进程泄漏】比【内存泄漏】更严重:内存泄漏只吃内存,进程泄漏同时吃进程号、文件描述符、内存、CPU 时间片。
工程上的三条实践:
1. 一定要 wait(或用 SIGCHLD 设为 SIG_IGN 让内核自动收)。
2. 给子进程设超时——不设的话,一个卡住的子进程会让父进程永远等。
3. 退出码要检查——不检查等于放弃了子进程唯一的反馈渠道。
五、动手 Demo:结束进程——退出码是它留下的最后一句话
下面让子进程用三个不同的退出码结束,看父进程拿到什么。这就是 shell 里 $? 的来源。
for code in (0, 1, 42):
pr = subprocess.Popen([sys.executable, "-c",
"import sys; sys.exit(%d)" % code])
rc = pr.wait() # 父进程收尸:读走退出码
print(code, "->", rc)
实跑输出:
--------------------------------------------------------------------
结束进程——退出码是它留下的最后一句话
--------------------------------------------------------------------
① 进程怎么结束,最后留下什么
程序里写的退出码 父进程看到的 含义
-------------------------------------------------------
0 0 成功
1 1 失败(含义自定)
42 42 失败(含义自定)
**约定只有一条:0 是成功,非 0 是失败。**
具体 1 和 42 有什么差别,**只有写这个程序的人知道**——
shell 的 `$?` 拿到的就是它,`&&` 与 `||` 靠它决定下一步。
② 结束的三种方式与身后事
方式 谁发起 留下的东西
----------------------------------------------------------
正常返回 / exit 自己 退出码
被信号杀死 别人或内核 信号编号
崩溃 内核(收到异常) 核心转储(可能)
**进程死了不等于清干净了**:内核要把它占的内存、打开的文件、
信号处理、子进程关系全部收回去。**这一步做完,资源才真的回来了。**
**父进程没去收(没 wait),就会留下一个僵尸**——只剩一条记录,等着被读退出码。
📋 真 Linux 上观察:`./程序 &` 然后 `ps -o stat,comm -p $!` 看状态;
僵尸的 STAT 是 `Z`。`cat /proc/<pid>/status` 里 `State:` 那行也会写。
三个退出码原样传回来了——exit(n) 里写什么,父进程就拿到什么。
约定只有一条:0 是成功。 1 与 42 的差别只有写程序的人知道,所以正经程序必须把退出码写进文档,否则调用方只能猜。
wait() 这一步就是收尸。不调它,子进程的记录会挂在那儿——Linux 上叫僵尸,ps 里状态显示 Z。
📋 在真 Linux 上看僵尸:写个小程序 fork 之后父进程 sleep(60) 不 wait,另开一个终端 ps -eo pid,ppid,stat,comm | grep Z。你会看到那条记录一直挂着,直到父进程退出或被 kill。
30 秒实验
false; echo $?与true; echo $?——1 与 0,就是这两个命令的全部差别。ls /nope; echo $?看失败时 shell 拿到什么;再ls /nope || echo 兜底。(exit 42); echo $?——子 shell 的退出码会传出来,管道与括号都靠这个机制。
常见误区
- 【退出码只是一个数字,随便写】。它是进程唯一的结构化反馈。0 与非 0 的区别被 shell、CI、监控全都在用。
- 【僵尸会占内存】。只占一条进程表记录,不占内存。真正的风险是进程号耗尽(以及它说明父进程没在 wait)。
- 【父进程死了子进程会变成僵尸】。那是孤儿,会被 1 号收养并正常收尸。僵尸的成因是父进程活着却不 wait。
自测三问(点开对答案)
1. 进程的三种结束方式各留下什么? —— 自己退出→退出码;被信号杀→信号编号;崩溃→信号(可能还有核心转储)。 2. exit 与 _exit 的差别?fork 出来的子进程该用哪个? —— exit 先刷 stdio 缓冲、跑 atexit 回调;_exit 直接进内核。**子进程该用 _exit**,否则会替父进程刷一遍缓冲,造成输出重复。 3. 僵尸与孤儿的区别?为什么守护进程要双 fork? —— 僵尸=已终止但父进程没 wait;孤儿=父进程先死、被 1 号收养。**双 fork 让孙进程成为孤儿**,从而不会被僵尸父进程拖住,也不跟终端一起死。- ☐ 4.1 关于实验程序的设计
相关笔记
- fork()函数 — 缓冲区那个坑的出处
- execve()函数 — 换魂之后也会走到这里
- 进程的状态 — 下一章就从【僵尸也是状态】讲起
- 🐧 Linux是怎样工作的 · 索引