前面几节介绍了 linux 如何创建进程,以及如何记录进程的信息(task_struct 结构体)。但是虽然令人伤感,进程终归也是会死亡的,这时 linux 内核要为死亡的进程做好善后工作,例如处理它的遗产(释放它占用的资源),处理它的“人际关系”。
之前几节说过,linux 的进程有着明显的继承关系,所有进程都有父进程,有着相同父进程的进程称为兄弟进程,这里把这些关系比作“人际关系”了。
进程的死亡
一般来说,进程的是通过调用 exit() 函数退出。既可能是进程自己显式的调用 exit(),也有可能是隐式的从某个程序的主函数退出。事实上,C 语言编译器会在 main() 函数的返回点后面自动放置 exit() 的代码。另外,linux 中的进程要受到 linux 内核管理,当它收到不能处理也不能忽略的信号或异常时,它还可能会被操作系统终结。
不过,进程退出时一般都是靠 do_exit() 函数完成的,它位于 kernel/exit.c 文件里。
do_exit() 函数的主要工作就是为死亡的进程做善后工作——清理遗产,处理“人际关系”。
从它的源码可以看出,do_exit() 函数会调用 exit_mm() 释放它占用的地址空间,调用 exit_sem() 函数退出 IPC 信号队列,调用 exit_files() 和 exit_fs() 函数减少它占用的文件描述符、文件系统引用计数,如果减少到 0,就把它们释放。等等。这些函数就是“清理遗产”函数了。而 exit_notify() 函数则主要负责处理进程“人际关系”。
为什么要处理进程的“人际关系”
进入 exit_notify() 函数,到最后会发现这样的代码:
if (state == EXIT_DEAD)
release_task(tsk);
linux 内核只在 state 为 EXIT_DEAD 时才真正释放进程的 task_struct 结构体。这就有疑问了,其他 state 内核不释放进程的 task_struct 结构体,岂不是内存泄漏了?或者说,为什么只有 state 为 EXIT_DEAD 时,内核才释放进程的 task_struct 呢?
这是因为 linux 中的进程有着自己的“人际关系”,它可能和其他进程共享着一块内存呢,要是它突然“失踪”了,linux 内核怎么知道这块内存还有没有人用了呢?还有可能我们正在调试这个进程,要是这个进程突然消失了又不通知调试者,调试者可能会一直傻等着它再出现,陷入“死机”状态。等等。
所以,即使进程死亡了,它的 task_struct 结构体(相当于进程的身份证)也不能立刻释放,一定要等到善后工作做完。但是既然进程已经死亡,它还能管理它的 task_struct 结构体吗?当然不能了,这些工作要靠它的“父进程”。
进程死亡后,linux 内核会通知它的父进程做善后工作。父进程则一般会调用 wait() 函数将自己挂起,直到收到子进程死亡的通知,此时 wait() 会返回该子进程的 PID,也能通过指针得到子进程退出时的退出代码。当最终需要释放进程描述符时,release_task() 等函数会被调用,以释放进程所有的资源。
但是,如果某个进程没有父进程为它做善后工作,它在死亡后就会成为“僵尸进程”。僵尸进程不会再被调度运行,它占用的资源会一直残留在内存里。要避免僵尸进程的出现,就要确保进程都有父进程。因此,进程一旦死亡,它的“子进程”们也要重新找父亲。只有在该进程没有人关心的情况下(state==EXIT_DEAD),才能直接释放它的 task_struct 结构体。
forget_original_parent() 函数的英文字面意思就是“忘记原来的父母”,内核程序员也是挺幽默的。
为死亡进程的子进程们,找新的父进程
事实上,exit_notify() 函数的第一件事,就是为死亡进程的“子进程”们重新找父进程。
死亡进程的子进程主要存在于子进程链表里和它锁跟踪的链表里,因此需要遍历这两个链表,并为它们找到新的父进程。请看下面代码:
list_for_each_entry_safe(p, n, &father->children, sibling) {
int ptrace;
ptrace = p->ptrace;
/* if father isn't the real parent, then ptrace must be enabled */
BUG_ON(father != p->real_parent && !ptrace);
if (father == p->real_parent) {
/* reparent with a reaper, real father it's us */
p->real_parent = reaper;
reparent_thread(p, father, 0);
} else {
/* reparent ptraced task to its real parent */
__ptrace_unlink (p);
if (p->exit_state == EXIT_ZOMBIE && !task_detached(p) &&
thread_group_empty(p))
do_notify_parent(p, p->exit_signal);
}
if (unlikely(ptrace && p->exit_state == EXIT_ZOMBIE && task_detached(p)))
list_add(&p->ptrace_list, &ptrace_dead);
}
list_for_each_entry_safe(p, n, &father->ptrace_children, ptrace_list) {
p->real_parent = reaper;
reparent_thread(p, father, 1);
}
刚才提到,进程只有在没有人关心的情况下,死亡时,linux 内核才会直接释放它的 task_struct 结构体,那么什么时候才算进程“没有人关心”呢?请看下面的代码:
state = EXIT_ZOMBIE;
if (task_detached(tsk) && likely(!tsk->ptrace))
state = EXIT_DEAD;
tsk->exit_state = state;
答案是显而易见的,第一,该进程没有被跟踪(被调试),第二,该进程是 detached,相当于与它的“父进程”已经断绝关系了。(后面线程章节会细说)
可以看出,linux 内核尽力保证每一个需要父进程的进程都能心想事成,这样就避免了“僵尸进程”的出现。如果无法从子进程的“家族谱”里为它找到新的父进程,就把 init 进程作为它的父进程。init 进程会例行调用 wait() 函数,以清理死亡进程的遗产。
收尸者 do_exit() 的最后工作
linux 内核本质上是 C 语言程序,它不能退出,否则整个 linux 就无法继续工作了。所以 do_exit() 是永不退出的函数,查看它最后的代码,就可以明白了:
for (;;)
cpu_relax(); /* For when BUG is null */
可以看出,do_exit() 函数最终会进入一个死循环,不过看这个死循环函数的字面意思是“让 cpu 放松一下”。这里的死循环也可以认为是一项任务,在没有其他任务需要处理时,cpu 就在这里空转休息。一旦有新任务需要调度,linux 内核就从“放松”状态转为工作状态,开始工作。