进程描述符回收

一、进程消亡
一个进程消亡之后,它在内核中的户口task_struct结构需要被回收,这个回收必须是主动的通过系统调用来回收,也就是waitpid或者wait4之类的系统调用。但是很多时候,我们并没有关心这个子进程的退出,比如我写的一些测试程序,如果这些进程没有被wait,那么它们是否真的像僵尸一样在系统中阴魂不散,永不瞑目呢?如果不是,谁将让它们rest in peace呢。
二、一个测试程序
#include<unistd.h>
int main(int argc, char * argv[])
{
    pid_t forked = fork();
    if(0 == forked)
    {
        execv(argv[1],argv+1);
    } else if (forked < 0)
    {
        return printf("forked failed\n");
    } else
    {
        sleep(10000);
    }

}

这个程序比较简单,只是简单的派生一个子进程,然后等待子进程的结束。此时有一个问题,就是当子进程退出的时候,父进程正在执行sleeep函数,根据man手册说明,这个函数是会被信号唤醒的,所以如果说子进程执行结束之后,那么父进程将会被子进程退出信号SIGCLD唤醒,从而整个进程结束。但是在真实的环境上试了一下,子进程退出之后,父进程依然无动于衷,继续睡觉。而此时子进程已经进入了僵尸状态。
测试代码
[root@Harry SIGCLD]# cat Makefile 
default:
    gcc -static SigTest.c -o SigTest.exe 
[root@Harry SIGCLD]# ./SigTest.exe /bin/ls . &
a.out  Makefile  SigTest.c  SigTest.exe
[1] 13192
[root@Harry SIGCLD]# ps aux
……
root     12980  0.0  0.0    756   128 pts/5    S+   20:07   0:00 ./SigTest.exe /bin/ls
root     12981  0.0  0.0      0     0 pts/5    Z+   20:07   0:00 [ls] <defunct>
……
可以看到子进程已经成为僵尸进程,而父进程依然在休眠,这也就是说子进程没有向父进程发送信号。这一点可以通过调试器来验证,我们以调试状态启动SigTest.exe,然后执行该命令,调试器同样没有收到子进程退出信号。但是从内核的代码里可以看到,这个信号的确是通过do_exit-->>exit_notify-->>do_notify_paren--->>>__group_send_sig_infot向父进程发送了信号。
三、进程退出通告
此时的问题就出在__group_send_sig_info中的sig_ignored判断位置,在这个函数的最后,有下面的判断:
return   handler == SIG_IGN ||
        (handler == SIG_DFL && sig_kernel_ignore(sig));这里对于测试程序,是满足这个条件的,也就是对SIGCHLD默认信号处理,并且是内核忽略信号,所以这个信号被忽略,这里返回一。
但是do_notify_parent函数中并没有判断这个返回值,而是直接调用了唤醒动作__wake_up_parent(tsk, tsk->parent);,此时父进程没有被唤醒,那应该就是用户态的C库代码对这个返回值做了判断,因为C库说明说只有当睡眠时间到或者信号到来的时候才被唤醒,所以C库判断系统时间未到,也没有信号,所以继续睡眠。遗憾是的是,我验证了一下,并不是这样,也就是说这个__wake_up_parent(tsk, tsk->parent);函数也没有执行对父进程的唤醒。测试的代码比较简单
[root@Harry SIGCLD]# ./SigTest.exe /bin/sleep 60  &
[root@Harry SIGCLD]# ps aux
root     13328  0.0  0.0    756   128 pts/1    S    20:42   0:00 ./SigTest.exe /bin/sleep 60
root     13329  0.0  0.0   3940   476 pts/1    S    20:42   0:00 /bin/sleep 60
[root@Harry SIGCLD]# cat /proc/13328/status 
voluntary_ctxt_switches:    2
nonvoluntary_ctxt_switches:    1
然后等待60之后,让子进程退出
[root@Harry SIGCLD]# ps aux
root     13328  0.0  0.0    756   128 pts/1    S    20:42   0:00 ./SigTest.exe /bin/sleep 60
root     13329  0.0  0.0      0     0 pts/1    Z    20:42   0:00 [sleep] <defunct>
[root@Harry SIGCLD]# cat /proc/13328/status 
voluntary_ctxt_switches:    2
nonvoluntary_ctxt_switches:    1
可以看到,在子进程退出前后,父进程的调度次数并没有增加,这说明父进程从来没有被唤醒过。
我们看一下
static inline void __wake_up_parent(struct task_struct *p,
                    struct task_struct *parent)
{
    wake_up_interruptible_sync(&parent->signal->wait_chldexit);
}
代码,可以看到它唤醒的队列头是parent->signal->wait_chldexit,查看源代码,这个队列头只有在父进程执行do_wait时才会通过add_wait_queue(&current->signal->wait_chldexit,&wait);将自己加到子进程的唤醒队列中,由于父进程没有执行这个操作,所以此时父进程不会被唤醒。这里也从另一个侧面说明了子进程退出时为什么既要发信号又要主动唤醒父进程的原因了,因为父进程可以忽略信号,也可能不wait子进程,或者两个只有一个,所以要面面俱到。
四、僵尸子进程的释放
不管怎么说,父进程没有处理子进程的退出,所以子进程依然保持僵尸状态,这只是一个状态,它始终需要一个解脱。此时就需要更上一级来处理了。当父进程自己也退出的时候,内核会对该进程所有的僵尸子进程做特殊处理。
exit_notify-->>forget_original_parent
list_for_each_safe(_p, _n, &father->children) {遍历所有以自己为父进程的线程,将子线程重新寄养在father自己的父进程中。
        int ptrace;
        p = list_entry(_p, struct task_struct, sibling);

        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 */
            choose_new_parent(p, reaper);
            reparent_thread(p, father, 0);
        } else {

reparent_thread(struct task_struct *p, struct task_struct *father, int traced)
    /* If we'd notified the old parent about this child's death,
     * also notify the new parent.
     */
    if (!traced && p->exit_state == EXIT_ZOMBIE &&
        p->exit_signal != -1 && thread_group_empty(p))
        do_notify_parent(p, p->exit_signal);这里就开始通知父进程,但是此时的父进程已经是之前通过forget_original_parent--->>choose_new_parent设置为reaper任务,这个任务通常是init任务,也就是大名鼎鼎的1号任务,正如它名字所示,该任务非常仁慈,它将会reap所有的僵尸任务。
这里有一个有趣的比喻:
0号任务内核是皇帝、1号任务时宰相,其它的守护任务及bash为管理。当然这个比喻比较封建,也不符合中国封建社会的传统,我们这里取这些职位的本意,或者说儒家宣传的那个模式。
五、主动忽略
从上面看,父进程此时没有对子进程做任何处理,这样并不好,因为子进程的结构并没有被释放,它依然在内核中存在。如果这种派生比较多的话,可能就会消耗大量的系统资源。所以如果真的不需要子进程的退出信息的话,可以主动的将自己的信号处理函数设置为SIG_IGN,也就是不管子进程死活。这样,当子进程退出的时候,内核会代劳回收子进程的进程控制块。这个代码比较简单,在do_notify_parent函数中
if (!tsk->ptrace && sig == SIGCHLD &&
        (psig->action[SIGCHLD-1].sa.sa_handler == SIG_IGN ||
         (psig->action[SIGCHLD-1].sa.sa_flags & SA_NOCLDWAIT))) {
……
        tsk->exit_signal = -1;
        if (psig->action[SIGCHLD-1].sa.sa_handler == SIG_IGN)
            sig = 0;
    }    if (valid_signal(sig) && sig > 0)由于if中将sig赋值为0,所以这里不会发信号。
        __group_send_sig_info(sig, &info, tsk->parent);

然后在exit_notify函数中判断exit_signal==-1,从而导致接下来的直接释放
    if (tsk->exit_signal == -1 &&
        (likely(tsk->ptrace == 0) ||
         unlikely(tsk->parent->signal->flags & SIGNAL_GROUP_EXIT)))
        state = EXIT_DEAD;
……
    /* If the process is dead, release it - nobody will wait for it */
    if (state == EXIT_DEAD)
        release_task(tsk);
六、主线程的持久性
在多线程系统中,主线程(也就是main启动的线程)是最后消亡的,这一点需要保证,至于为什么,可以看一下do_notify_parent的代码,其中有一个判断。
BUG_ON(!tsk->ptrace &&
           (tsk->group_leader != tsk || !thread_group_empty(tsk)));
也就是只有leader能够通知父进程说整个线程组全军覆没了,其它人说都是谎报军情。那么为什么这么搞呢?我想可能是父进程派生出一个子进程之后,它通过waitpid等待的子进程应该只有自己直接派生的子进程,也就是自己的第一个亲生儿子。如果子进程想复制出任意多的子进程,那是它自己的事情,父进程不想也无法知道这些东西。
当然,通常main函数退出之后,C库会调用exit系统调用,从而整个线程就退出了。但是本着贵在折腾的思想,我们假设main函数是通过pthread_exit来退出,也就是只是退出线程而不是退出线程组,那么此时thread_group_leader结构如何保持坚挺的。
有意思的是,这个并不是由do_exit本身来保证的,而是在wait的时候由收割者决定,对应的代码在
do_wait
ret = eligible_child(pid, options, p);

在eligible_child中调用
#define delay_group_leader(p) \
        (thread_group_leader(p) && !thread_group_empty(p))
是满足的,所以返回值为2,在
do_wait
    default:
            // case EXIT_DEAD:
                if (p->exit_state == EXIT_DEAD)
                    continue;
            // case EXIT_ZOMBIE:
                if (p->exit_state == EXIT_ZOMBIE) {
                    /*
                     * Eligible but we cannot release
                     * it yet:
                     */
                    if (ret == 2)这里将会放这个leader一马,从而让它一直存在,作为线程组的首领,看来这个职位还是一个终身制头衔呢。
                        goto check_continued;
此时看一下最后一个非leader子进程退出的时候,如何让leader进程还魂,并让它通知父进程的。
void release_task(struct task_struct * p)
leader = p->group_leader;
    if (leader != p && thread_group_empty(leader) && leader->exit_state == EXIT_ZOMBIE) {如果所有的子进程都退出,将会满足这个分支,所以此时主动通过do_notify_parent通知父进程,可以看到其中的参数使用的是leader而不是自己,可将有多卑微无奈。
        BUG_ON(leader->exit_signal == -1);
        do_notify_parent(leader, leader->exit_signal);
        /*
         * If we were the last child thread and the leader has
         * exited already, and the leader's parent ignores SIGCHLD,
         * then we are the one who should release the leader.
         *
         * do_notify_parent() will have marked it self-reaping in
         * that case.
         */
        zap_leader = (leader->exit_signal == -1);
    }

posted on 2019-03-06 21:02  tsecer  阅读(238)  评论(0编辑  收藏  举报

导航