内核进程实现
本文章基于 Linux 2.6.11 编写
每个进程都有生命周期,对于用户态进程,当程序从 main()
函数中返回或直接调用 exit()
,由或者进程接收到了不能处理、忽视的信号时,进程就会被终止。当这些进程“死”了后,内核必须能够回收他们的资源,比如内存、打开的文件等等。
内核提供了两种终止进程的方式,系统调用 exit_group()
终止整个线程组,由内核函数 do_exit_group()
实现,标准库函数 exit()
就是以此实现;系统调用 exit()
终止线程组中的单个进程,由内核函数 do_exit()
实现,POSIX
库函数 pthread_exit()
以此实现。下面我们分 线程组终止 和 进程终止 来讨论。
线程组的终止
线程组的终止描述的是内核对 由多个共享内存空间、文件表等资源的轻量级进程(struct task
实体)构成的线程组的回收工作。do_exit_group()
内核函数其实现的入口,下面给出注解。
NORET_TYPE void do_group_exit(int exit_code)
{
// 乐观加锁
if (current->signal->flags & SIGNAL_GROUP_EXIT)
// 已经开始执行进程组退出过程,将退出码作为本进程的退出码
exit_code = current->signal->group_exit_code;
else if (!thread_group_empty(current)) {
// 线程组不为空
// 线程组中信号处理是共享的
struct signal_struct *const sig = current->signal;
if (sig->flags & SIGNAL_GROUP_EXIT)
/*
* 加锁再次判断,确认竞争
*/
exit_code = sig->group_exit_code;
else {
// 保存退出码
sig->flags = SIGNAL_GROUP_EXIT;
sig->group_exit_code = exit_code;
// 杀死其他线程
zap_other_threads(current);
}
}
// 杀死本进程,而不返回
do_exit(exit_code);
}
通过判断线程组中的成员是否已经调用了过 do_group_exit()
,内核使用 SIGNAL_GROUP_EXIT
来进行标记,防止多次杀死同一个线程组中的其他进程,线程组一定会共享 信号处理,所以我们可以将已发起线程组退出退出标志和进程退出码保存在 struct signal
数据结构中。
在内核保存标记和退出码后,调用 zap_other_threads()
来将其线程组成员杀死。
void zap_other_threads(struct task_struct *p)
{
struct task_struct *t;
p->signal->flags = SIGNAL_GROUP_EXIT;
p->signal->group_stop_count = 0;
/*线程组为空,直接返回*/
if (thread_group_empty(p))
return;
/*遍历线程组中的其他成员*/
for (t = next_thread(p); t != p; t = next_thread(t)) {
/*
* 已经处于退出状态
*/
if (t->exit_state)
continue;
/*
* 非首领进程,在退出时不用向父进程发送信号(一般就是 SIGCHLD)
*/
if (t != p->group_leader)
t->exit_signal = -1;
/*通过KILL信号来通知被强制退出的线程*/
sigaddset(&t->pending.signal, SIGKILL);
/*移除未决的其他非 KILL 的停止信号*/
rm_from_queue(SIG_KERNEL_STOP_MASK, &t->pending);
/*唤醒,处理信号,就会退出*/
signal_wake_up(t, 1);
}
}
关于 Unix
信号处理的 内核实现需要大量的篇幅来讲解,这里你仅需要知道,内核通过向 线程组其他成员发送 SIGKILL
信号来杀死他们。 内核将一个 SIGKILL
信号标记在未决(待处理)的信号集合中,并清除其他会引起进程停止被调度的未决信号,然后调用 signal_wake_up()
函数使目标进程能立马响应 SIGKILL
信号。该函数通过发送 CPU
间中断,使正在用户态运行的目标进程强制陷入内核态,在《中断实现》中我们提及过,当进程从中断例程中退出,恢复被中断的用户态上下文时,会检查和处理未决的信号,所以内核选择使用 中断例程为空的 RESCHEDULE_VECTOR
中断来完成这样的需求。这样所有的线程组成员在用户态响应 SIGKILL
,并陷入内核执行 do_exit()
从而被杀死。
进程的终止
进程的终止描述的是内核对 struct task_struct
为单位的进程实体的回收工作,包括内存资源(页表)、IPC对象(System V
信号量)、文件表、文件系统等,要解说这些需要大量的篇幅,并可以另立主题,这里我们仅需要知道 do_exit()
会释放一次对这些资源的引用,如果没有其他路径或进程引用他们,那么就会被当即回收。我们主要讨论在进程在死亡时,对父进程和子进程产生的影响,这会使进程描述符所在的组织关系发生变化,以及进程最后一次调度和进程描述符的回收工作,在展开讲解时,我们将忽略进程跟踪的情况,以及粗略介绍退出时的信号处理,以便引入不必要的复杂性而不能把握整体的流程。
do_exit()
所有进程的退出都是由 do_exit()
实现的,下面给出主干部分的源码和注解。
fastcall NORET_TYPE void do_exit(long code)
{
struct task_struct *tsk = current;
int group_dead;
...
/*标记正在退出*/
tsk->flags |= PF_EXITING;
/*删除定时器*/
del_timer_sync(&tsk->real_timer);
...
/*解除页表和数据页*/
exit_mm(tsk);
/*关闭 System V 信号量*/
exit_sem(tsk);
/*关闭文件描述符*/
__exit_files(tsk);
/*关闭文件系统*/
__exit_fs(tsk);
...
tsk->exit_code = code;
// 通知亲戚进程,调整组织结构
exit_notify(tsk);
BUG_ON(!(current->flags & PF_DEAD));
schedule();
BUG();
...
}
当进程关闭各种资源,并通知 亲属进程后,就进行一次主动调度,这是该进程最后一次运行,对该进程描述符和内存描述符的一次引用会在下一个被调度进程的内核路径 finish_task_switch()
中完成,见《完全公平调度》中的介绍。
exit_notify()
退出的进程可能存在子进程,所以内核必须考虑他们的寄养问题,以保证进程组织结构的完整性,并按照 POSIX
的规定进程退出时,必须以