分类: LINUX
2015-04-10 23:26:52
ARM Linux 源码分析系列文章基于 Linux 2.6.22 讲解,
转载请标明原处!
一个进程可以使用 exit 系统调用来结束自己并进入僵死状态。他最后在内核中执行到的函数为 sys_exit() 。
他调用 do_exit() 来执行真正的操作,实际上 do_exit() 涉及到很多其他内容,所以我们只讲解部分最为关键的代码,下面分析一下 do_exit() 函数。
847 行,取出要结束的进程(也就是当前进程)的进程描述符。
874 行,in_interrupt() 函数用来判断当前是否正在执行中断服务处理程序。exit() 系统调用只能结束进程(线程),那么中断服务程序调用他是想结束谁呢?我们并不知道,因为我们并不知道中断会在什么时候发生,所以这里不允许在执行中断服务程序时调用到本函数。
876 行,进程号(PID)为 0 的进程是 idle 进程,这个进程是系统中没有其他进程在执行时执行的函数,他不能被杀死。
879~882 行,不允许杀死 init 进程。
887~889 行,如果当前进程被跟踪且他需要报告给跟踪进程自己正在执行 exit 系统调用,那么就在 888 行把退出信号存放到 ptrace_message 字段,并在 889 行调用 ptrace_notify() 来通知跟踪进程我正在执行 exit 系统调用。
893 行,如果当前进程在调用 exit 系统调用前已经正在执行删除操作了,那么就设置PF_EXITPIDONE 标志,并把当前进程的状态设为不可中断的暂停状态,这样他就不会再执行了,最后调度出去。
905 行,设置 PF_EXITING 标志说明当前进程正在执行 exit 系统调用。
914~917 行,只有线程的进程描述符的 mm 字段才为空,所以这里是:如果他是一个进程,那么就更新他的内存描述符。
918 行,信号描述符是可以被进程共享的(比如说一个线程组内的所有进程都共享一个信号描述符),其共享的进程数存放在 live 字段里,而 tsk 进程都要被终结了,自然要递减他,说明少了一个使用这个描述符的进程。
919~922 行,如果 918 行的 live 字段减为 0 了,就说明没有进程使用这个信号描述符了,那么剥离信号描述符中的所有定时器。
935~946 行,和 copy_process() 的那一堆 copy_xxx 相对应,这里就是剥离他的那些资源(还记得使用 vfork() 创建进程时的父进程吗?他就是在 exit_mm() 里被唤醒的)。
951 行,递减执行域的引用计数
952~953 行,如果当前进程有使用的可执行文件,那么递减这个文件的引用计数。
955 行,设置退出代码。
958 行,通知其他进程:已经有一个进程死了。这个函数是重中之重,因为他会使得父进程来为他来处理后事,一会我们再来详细讲解这个函数。
974 行,由于完成了进程退出操作,所以为他设置 PF_EXITPIDONE 标记。
983 行,把进程状态设为 TASK_DEAD,表示他已经死亡了。
985 行,这里被调度出去后,tsk 进程再也不会执行了。
在 do_exit() 里,调用exit_notify() 通知内核中其他相关进程已经有进程死了。这个函数非常重要,下面我们再来看看 exit_notify() 是怎么实现的。
755 行,如果 tsk 进程有信号处理,且他所处的线程组没有正在被杀死,并且他所处的线程组存在其他进程(或线程),那么我们就应该通知线程组内其他进程。
760~762 行,遍历线程组内所有进程。
761~762 行,如果遍历到的当前进程没有信号需要处理,且没有正在被杀死,那么就调用recalc_sigpending_and_wake(),recalc_sigpending_and_wake() 会再次确定是否有信号在等待,如果有就为他设置TIF_SIGPENDING 标志(表示需要处理信号)并来尝试唤醒他。
770 行,为 tsk 的所有子进程都找一个新的父进程,因为他们现在的父进程要死亡了。当然,如果子进程是僵死状态的线程,则他会被放到 ptrace_dead 链表里,在下面会释放他。
777 行~782 行,先看看 tsk 和他的实际父进程是否不在同一进程组但在同一会话,这实际上是为了快速判断这是否是一个孤儿进程组,如果他是则会在这里直接退出。当然如果他确实是那样我们也无法直接得出一个他们所属的进程组不是孤儿进程组的结论,所以我们就调用will_become_orphaned_pgrp() 遍历进程组内的进程来判断 tsk 进程被杀死后这个进程组是否会成为一个孤儿进程组,如果会,则再调用has_stopped_jobs() 查看这个孤儿进程组内是否存在一个处于 TASK_STOP 状态的进程,如果存在,那么按照 POSIX 3.2.2.2 的规定,需要发送给进程组内所有进程SIGHUP 和SIGCONT 信号,前者表示他从终端退出了(同一终端的属于同一会话),后者会让暂停的进程重新开始执行。
787~791 行,如果退出时发送给父进程的不是 SIGCHLD 信号,那么就把退出通知信号设为 SIGCLHLD。
793~897 行,如果定义了退出信号且他是线程组内最后一个进程,那么进入 794~795 的代码段。他调用 do_notify_parent() 来通知父进程我已经死亡了,一会我们来详细讲解他。
800~805 行,如果 tsk 死亡时向父进程发送一个自定义的信号且他没有被跟踪或他正在被杀死,那么就把状态设为 EXIT_DEAD,否则设为 EXIT_ZOMBIE 。
809~813 行,还记得 770 行的forget_original_parent() 吗?他把子进程里所有僵死进程都挑了出来并放到 ptrace_dead 链表里,这里就是彻底消灭这些进程。
816 行,如果 tsk 进程也被杀死了,那么就把他也彻底消灭吧。
然后我们再来看看 do_notify_parent() 是怎么实现的,这个函数极为重要。
402 行,先定义一个信号描述符,他用于描述发送给父进程的信号。
413~432 行,初始化这个信号描述符。
子进程退出的时候,会向其父进程发送某个信号作为通知(多数情况为 SIGCHLD)。然后子进程等待父进程调用 wait4() 系统调用。
434 行,取出父进程的信号处理器描述符。
436~438 行,如果我们要发送给父进程的是 SIGCHLD 信号(通知父进程我死了,需要你帮我做下一步工作)但是父进程却忽略这个信号或者父进程不会把子进程转变为僵死进程,那么就说明父进程不接受我们的信号,则进入 439~440 代码段。
439~440 行,把 tsk 进程的 exit_signal 设为 -1,然后当本函数返回到 exit_notify() 时,他就会直接把 tsk 进程彻底杀死(请看)。然后把 sig 设为 0 以防止他在 444 行进入 if 代码段。
444~446 行,如果信号没有任何问题,那么就把信号发送给父进程,然后唤醒他,这样当父进程醒来后就会处理这个子进程。
我们现在假设:父进程已经调用了 wait4() 系统调用,wait4() 系统调用在内核中对应的函数为 sys_wait4() ,我们现在再来看一下他的实现 。
sys_wait4() 调用 do_wait() 来执行真正的等待操作。645 行的 prevent_tail_call() 在 ARM 处理器上则什么都不做。下面我们再来看一下 do_wait() 是怎么实现的。
438 行,为当前进程定义一个等待对象,这是因为要等待的进程可能没死亡,所以要先让自己进入等待状态等待他死亡并唤醒自己(还记得 do_notify_parent() 的 444~446 行吗?)。
443 行,把自己加入线程组共享的 wait_chldexit 等待队列。
448 行,把自己设为等待状态。
453 行,我们能注意到的是,这里是一个 do-while 循环,这个 do-while 循环实际上是遍历当前进程所属线程组内的所有进程,每次循环 tsk 指向一个线程组内的进程。
458 行,tsk 是线程组内当前遍历到的进程,而这里的 list_for_each 又遍历了 tsk 进程的所有子进程。他和 453 行结合起来,就形成了从当前进程所属的进程组里的进程所有子进程里查找特定的进程。
459~467 行,先取出当前循环到的子进程,然后调用 eligible_child() 根据 options 来判断当前子进程是否为符合要求的进程,如果不是,则跳过本次循环。
470 行,到了这里就说明,p进程是符合要求的进程。
471 行,根据需要等待的子进程的状态执行不同操作。
472 行,被跟踪状态:把 flag 设为 1,然后调用my_ptrace_child() 查看他是否是被实际父进程跟踪的(也就是 tsk 进程)。如果不是,则跳过他,因为被跟踪的进程只能由跟踪他的进程来进行处理;否则把他看做暂停状态来进行处理。
478 行,暂停状态:先在 481 行检查是否已经设置了WUNTRACED 标志(该标志说明目标进程处于暂停状态时,退出操作),如果未设置就再继续调用my_ptrace_child() 看看他是否被实际父进程跟踪(tsk进程),如果不是,跳过他。
495~511 行,其他状态:如果 p 进程是 EXIT_DEAD 状态,那么他不需要我们来对他进行处理,跳过他;如果 p 进程是僵死状态,那很好,因为在上面我们知道:进程在调用 exit() 系统调用时,如果一切正常,那么他最后会唤醒父进程并让自己进入 EXIT_ZOMBIE 状态,那么父进程就是在这里处理他了。
501 行,如果 p 进程是一个非空线程组的领头进程,则跳到 512 行。
503 行,如果没有设置 WEXITED 就说明我们不等待不处理已经死亡的进程,就跳过当前子进程
505~508 行,处理子进程消亡,在这个函数中就会释放找到的那个子进程。如果释放成功,则他返回那个进程的 pid,然后在 508 行返回。
512 行,到了这里就说明,p 进程可能正在运行或者他是一个非空线程组的领头进程
517~520 行,这里仅仅是把 p 进程的相关信息复制到用户空间,如果复制成功,则跳到 end 处返回。
528 行,到了这里,就说明遍历完了 tsk 进程下所有子进程。
529~538 行,如果 flag 为 0 ,那么说明子进程里没有一个是暂停状态和运行状态的,那么遍历 tsk 所有被跟踪的子进程(因为被跟踪所以父进程暂时不是 tsk 进程,上面不会检查到他们),查看这些子进程是否符合我们的需要,如果符合,则把 flag 设为 1 。
539 行,因为我们最外面那层循环是遍历当前进程所属线程组内所有进程,即让线程组所有进程都等待符合要求的子进程死亡。而如果设置了 __WNOTHREAD 标志,那么就意味着不等待线程组内其他线程的子进程死亡,那么就直接跳出最外层的循环。
542 行,同一线程组的进程必须使用同一个信号描述符,所以如果 tsk 进程和当前进程的信号描述符不同,那肯定出错了。
543 行,tsk指向进程组内下一个进程,并在 543 行判断是否已经遍历完进程组,如果未完,则继续循环处理。
548 行,如果 flag 为 1,那么就说明要么有符合要求的进程正在被跟踪或正在被暂停。
561 行,到了这里,就说明完成了,那么恢复进程状态并把他从等待队列中删除,这样他就能继续被正常调度了。
564~582 行,输出一些信息到用户空间。