一、文件概述善始善终的进程生命周期管理者exit.c​ 位于/kernel目录是Linux 0.11中进程终止与资源回收的核心实现。如果说fork.c和exec.c负责进程的“生”与“变”那么exit.c就是负责进程的“死”与“归”。它实现了exit()系统调用定义了进程结束时的全套清理流程从关闭打开的文件、释放占用的内存到通知父进程、处理孤儿进程最后将自己彻底从系统中抹除。在Unix哲学中进程退出不仅仅是停止运行更是一场有序的资源归还仪式。exit.c确保了进程在消亡时不留垃圾不泄漏内存不错发信号完美体现了操作系统的健壮性与自洽性。1.1 历史背景僵尸进程的由来与解决在早期的Unix系统中进程退出后不会立即消失而是变成一个“僵尸进程”Zombie等待父进程查询其退出状态。这一设计看似古怪实则必要它保证了父进程总能获取子进程的最终结局成功、失败、或被谁杀死。Linux 0.11完全继承了这一并发控制机制而exit.c就是这一机制的主要维护者。1.2 核心职责资源大扫除关闭所有打开的文件描述符释放内存页表和物理页。父子关系重组如果退出的是父进程子进程将被init进程1收养。信号与通知向父进程发送SIGCHLD信号唤醒可能正在wait()的父进程。状态转换将进程状态从TASK_RUNNING转为TASK_ZOMBIE最后彻底消失。调度交接调用schedule()永远不再返回。二、关键数据结构僵尸状态与父子链表2.1 进程状态扩展TASK_ZOMBIE在sched.h中定义的进程状态中TASK_ZOMBIE是最特殊的#define TASK_ZOMBIE 3 /* 僵尸状态进程已死但资源未完全释放 */僵尸进程的本质不再运行没有代码在执行不占用CPU。残留躯壳task_struct结构体仍然保留在进程表中。信息留存保存了退出码Exit Code供父进程读取。不可调度永远不会再被选中运行。2.2 进程间的亲属关系task_struct中的几个关键字段维护了进程树struct task_struct { // ... long pid; /* 当前进程ID */ long father; /* 父进程ID */ long pgrp; /* 进程组ID */ long session; /* 会话ID */ struct task_struct *p_pptr; /* 指向父进程的指针 */ struct task_struct *p_cptr; /* 指向最年轻的子进程 */ struct task_struct *p_ysptr;/* 指向弟进程兄弟链表 */ struct task_struct *p_osptr;/* 指向兄进程 */ // ... };链表结构父进程通过p_cptr指向第一个孩子孩子们通过p_ysptr/p_osptr连接成兄弟链表。exit.c必须小心翼翼地维护这些链表防止进程树断裂。三、核心函数深度解析3.1 系统调用入口sys_exit()这是用户程序调用exit()或_exit()的内核入口int sys_exit(int exit_code) { return do_exit(exit_code); // 直接委托给核心函数 }简单得令人惊讶因为真正的复杂逻辑都在do_exit()中。3.2 核心退出函数do_exit()这是进程生命周期的终点站一旦调用永不返回int do_exit(long exit_code) { int i; // 1. 释放代码段和数据段的页表与物理页 free_page_tables(get_base(current-ldt[1]), get_limit(0x0f) 12); // 2. 关闭所有打开的文件 for (i 0; i NR_OPEN; i) { if (current-filp[i]) { sys_close(i); // 调用close系统调用 current-filp[i] NULL; } } // 3. 释放当前目录和根目录的inode引用 iput(current-pwd); current-pwd NULL; iput(current-root); current-root NULL; // 4. 通知父进程发送SIGCHLD信号 tell_father(current-father); // 5. 设置僵尸状态和退出码 current-state TASK_ZOMBIE; current-exit_code exit_code; // 6. 特别处理如果是进程1init退出系统恐慌 if (current-pid 1) { printk(Init exiting\n); panic(No init process); } // 7. 调度下一个进程永别了 schedule(); return 0; // 永远不会执行到这里 }关键步骤解析free_page_tables这是最暴力的操作直接摧毁进程的整个用户空间内存映射。代码段、数据段、堆栈瞬间化为乌有。sys_close遍历文件表对每个打开的文件调用iput()减少inode引用计数。如果文件被标记为删除硬链接数为0且这是最后一个引用文件数据将被真正删除。tell_father唤醒父进程告诉它“孩子没了”。3.3 通知父进程tell_father()static void tell_father(int father_pid) { struct task_struct *p; // 1. 遍历进程表找到父进程 for (p task[0]; p task[NR_TASKS]; p) { if (!p || p-pid ! father_pid) continue; // 2. 向父进程发送 SIGCHLD 信号 send_sig(SIGCHLD, p); // 3. 唤醒可能正在 wait() 的父进程 if (p-state TASK_INTERRUPTIBLE) wake_up_process(p); break; } }信号机制SIGCHLD是一个“友好”的信号它告诉父进程“你的子进程状态改变了你可以来收尸调用wait()了。”3.4 孤儿进程收养reparent_children()这是exit.c中最温情的部分——孤儿救助机制static void reparent_children(void) { struct task_struct *p, *father; // 1. 寻找 init 进程进程1 for (father task[0]; father task[NR_TASKS]; father) { if (!father || father-pid ! 1) continue; break; } // 2. 遍历当前进程的所有子进程 for (p current-p_cptr; p; p p-p_ysptr) { // 3. 重新设置父进程为 init p-father 1; p-p_pptr father; // 4. 如果父进程正在等待子进程唤醒它 if (father-state TASK_INTERRUPTIBLE) wake_up_process(father); } // 5. 将子进程链表挂到 init 的孩子链上 if (father-p_cptr) { // 找到 init 原本最年轻的孩子 struct task_struct *youngest father-p_cptr; while (youngest-p_ysptr) youngest youngest-p_ysptr; youngest-p_ysptr current-p_cptr; } else { father-p_cptr current-p_cptr; } // 6. 清空当前进程的孩子指针 current-p_cptr NULL; }为什么要收养​ 如果父进程先退出子进程就变成了“孤儿”。Unix规定所有孤儿必须由init进程PID1收养。这样保证了进程树的完整性并且当孤儿进程结束时init会负责回收它防止僵尸进程永久残留。3.5 等待系统调用sys_wait()父进程通过wait()或waitpid()来回收子进程的僵尸int sys_wait(int *status) { struct task_struct *p; int found 0; // 1. 遍历进程表寻找僵尸状态的子进程 for (p task[0]; p task[NR_TASKS]; p) { if (!p || p-father ! current-pid) continue; if (p-state TASK_ZOMBIE) { // 2. 找到了僵尸子进程 if (status) { // 将退出码存放到用户空间 verify_area(status, sizeof(*status)); put_fs_long(p-exit_code, status); } // 3. 彻底释放僵尸进程的 task_struct free_page((long)p); task[p-pid] NULL; // 清空进程表项 found 1; break; } } if (!found) { // 4. 没有僵尸子进程进入可中断睡眠等待信号 current-state TASK_INTERRUPTIBLE; schedule(); // 5. 被唤醒后检查是否有信号如SIGINT if (current-signal ~current-blocked) return -EINTR; // 6. 递归调用再次尝试 return sys_wait(status); } return found ? p-pid : -1; }阻塞与唤醒如果没有子进程死亡父进程会在TASK_INTERRUPTIBLE状态下睡眠直到tell_father()发送信号将其唤醒。四、进程终局的三种命运在Linux 0.11中进程退出后的命运取决于父进程的行为情景父进程行为结果正常回收​父进程调用wait()僵尸被及时清理资源完全释放孤儿进程​父进程先退出被init收养由init负责后续回收僵尸滞留​父进程忽略SIGCHLD或不调用wait()僵尸进程残留占用进程表项直到父进程退出进程表泄漏风险Linux 0.11的进程表task[64]是静态数组。如果父进程不调用wait()僵尸进程会永久占据一个位置最终系统可能无法创建新进程。五、设计哲学责任链与资源所有权5.1 谁分配谁释放Unix的内存管理遵循严格的所有权原则内存页由memory.c分配由exit.c通过free_page_tables释放。文件描述符由open.c分配由exit.c通过sys_close释放。task_struct由fork.c分配由exit.c或父进程wait()通过free_page释放。这种对称性保证了系统资源的长期健康。5.2 信号的异步协作exit.c与signal.c紧密配合实现了异步通知机制。进程退出不需要轮询而是通过信号告知父进程。这是事件驱动编程在内核中的早期实践。5.3 与现代Linux对比特性Linux 0.11现代Linux僵尸处理必须显式wait()支持SIGCHLD忽略自动回收资源限制无RLIMIT支持资源限制ulimit退出状态8位退出码32位退出状态支持更多信息线程退出无线程概念复杂的线程组退出机制六、总结尘埃落定的生命轮回exit.c​ 以其冷静、严谨的逻辑为Linux 0.11的进程画上了完美的句号。它是清洁工一丝不苟地关闭文件、释放内存确保系统资源不流失。它是送信人通过信号机制将死讯准时送达父进程。它是慈善家通过收养机制确保孤儿进程不流离失所。它是终结者调用schedule()平静地交出CPU控制权消失在内核的洪流中。在Linux的设计中进程的死亡不是终结而是资源回归系统的循环。exit.c就是这个循环的守护者它让结束变得有序让消逝变得有意义。