busybox里的僵尸进程为何那么多

busybox里面的僵尸进程很多是有目共睹的,为什么呢?这要从僵尸进程的概念说起,所谓僵尸进程实际上就是没有人回收的进程,什么也没有了,只剩下 task_struct这个空壳子了,task_struct里面的字段都没有了,都被释放了但是task_struct本身还在,占据着 sizeof(struct task_struct)大小的空间,其空虚的task_struct仍然在全局的task_struct链表中挂着,这样遍历整个系统的进程的时候仍然 可以找到它,在用户空间ps的时候仍然可以看到僵尸进程。但是为何会有这种进程呢?这得从进程回收说起。进程在以下情况下被回收:
1.父进程调用wait系统调用等待子进程;
2.系统在父进程显式忽略SIGCHLD信号的时候进行回收。
那么在别的情况下,该进程就会成为僵尸进程,这怎么理解呢?一般情况下,当一个进程结束的时候都要向其父进程发送SIGCHLD信号,什么情况呢?就是父进程没有将SIGCHLD信号设置为SIG_IGN并且没有设置为SIG_DFL,满足以上条件的话,父进程收到信号后必须调用wait进行回收,如果没有wait,那么该子进程就会变成僵尸进程,如果父进程将信号设置为SIG_DFL,那么退出进程照样向父进程法信号,只不过父进程不处理,子进程会成为僵尸,这是情况一;情况二就是父进程将SIGCHLD信号设置为SIG_DFL,这样的话当子进程结束时不会向父进程发送SIGCHLD信号,而且内核也 不会帮着回收,这样的话该结束的子进程一定会变成僵尸进程;情况三就是父进程显式乎略了SIGCHLD信号,即设置为SIG_IGN,这样的话内核会回收 子进程,故该子进程一定不会变成僵尸进程。为何如此复杂呢?呵呵,这是posix的约定,问他们去吧。我们可以从内核源代码看个究竟,当进程exit的时候,调用就到了do_exit:

asmlinkage NORET_TYPE void do_exit(long code)

{

         struct task_struct *tsk = current;

         profile_task_exit(tsk);

  ...

         tsk->flags |= PF_EXITING;

         del_timer_sync(&tsk->real_timer);

...

         exit_notify(tsk);    //这个函数告知了僵尸进程产生的原因

         schedule();

         BUG();

         /* Avoid "noreturn function does return".  */

         for (;;) ;  //不可能到这里了,因为进程永远不会从schedule返回了

}

static void exit_notify(struct task_struct *tsk)

{

         int state;

         struct task_struct *t;

         struct list_head ptrace_dead, *_p, *_n;

         INIT_LIST_HEAD(&ptrace_dead);

         forget_original_parent(tsk, &ptrace_dead);

         BUG_ON(!list_empty(&tsk->children));

         BUG_ON(!list_empty(&tsk->ptrace_children));

         t = tsk->real_parent;

...

         if (tsk->exit_signal != -1 && thread_group_empty(tsk)) {

                 int signal = tsk->parent == tsk->real_parent ? tsk->exit_signal : SIGCHLD;

                 do_notify_parent(tsk, signal);   //告诉父进程这个进程退出了,如果可能,那么向父进程发送子进程退出信号

         } else if (tsk->ptrace) {

                 do_notify_parent(tsk, SIGCHLD);  //这个是跟踪调试相关的,暂不讨论,可以参考我前面的关于调试的文章《关于linux内核调试的实现》

         }

...

         state = TASK_ZOMBIE;        //默认情况下进程就是僵尸进程,呵呵

         if (tsk->exit_signal == -1 && tsk->ptrace == 0)

                 state = TASK_DEAD;      //如果没有父进程wait,就将进程状态转为TASK_DEAD了,内核负责回收

         tsk->state = state;

         if (state == TASK_DEAD)

                 release_task(tsk);      //内核回收了TASK_DEAD状态的进程

         preempt_disable();

         tsk->flags |= PF_DEAD; // 注意release_task并没有真正将task_struct的内存释放,因为do_exit中最后还要调用schedule,而 schedule 里还要用到该退出进程的task_struct,真正内存释放在schedule里面的finish_task_switch,该函数 将 task_struct的计数器减一,如果为0,那么释放内存。

}

我们下面看一下do_notify_parent:

void do_notify_parent(struct task_struct *tsk, int sig)

{

         struct siginfo info;

         unsigned long flags;

         struct sighand_struct *psig;

...

         info.si_signo = sig;

         info.si_errno = 0;

         info.si_pid = tsk->pid;

info.si_uid = tsk->uid;

         info.si_utime = tsk->utime + tsk->signal->utime;

         info.si_stime = tsk->stime + tsk->signal->stime;

...

         psig = tsk->parent->sighand;

         spin_lock_irqsave(&psig->siglock, flags);

         if (sig == SIGCHLD &&

             (psig->action[SIGCHLD-1].sa.sa_handler == SIG_IGN ||

              (psig->action[SIGCHLD-1].sa.sa_flags & SA_NOCLDWAIT))) {  //如果父进程SIG_IGN了SIGCHLD,那么就设置一些标志,然后由内核进行回收,见上面的函数

                 tsk->exit_signal = -1;

                 if (psig->action[SIGCHLD-1].sa.sa_handler == SIG_IGN)

                         sig = 0;

         }

         if (sig > 0 && sig <= _NSIG)                                 //如果没有SIG_IGN,那么向父进程发送信号,父进程SIG_DFL信号时也发送,只是父进程不处理,不wait,子进程当然成为了僵尸进程

                 __group_send_sig_info(sig, &info, tsk->parent);

         __wake_up_parent(tsk, tsk->parent);

         spin_unlock_irqrestore(&psig->siglock, flags);

}

上面的函数说明足以说明僵尸进程产生的原因,但是还有一个有意思的事情就是forget_original_parent函数,该函数就是把退出进程的孩 子们过继给一个选择出来的新的进程,典型的不养老不送终,父亲到死还要照顾儿子,而僵尸进程就是典型的白发送黑发的惨剧,那么过继给谁呢?一般是过继给本线程组的另外一个进程,如果没有就过继给一个全局变量child_reaper,该变量在内核初始化的时候设置为1号init进程,具体就是在 rest_init函数中设置的,而rest_init就是start_kenenl函数fork出来的1号init进程的前身,1号进程一切初始化完毕 后就会exec成/sbin/init,具体代码很清晰就不多说了,为什么说这个呢?因为init进程负责着回收大多数僵尸进程的重任,很多进程过继给了 init进程,按照道理讲,init进程必须有wait子进程的调用,也就是说必须设置SIGCHLD信号处理器,然后在该处理器里面wait子进程,要 么就是init进程SIG_IGN了SIGCHLD信号,但是如果init进程SIG_DFL了信号那就麻烦了,init进程将不会回收子进程,造成大量 僵尸进程的产生。下面我们就看看busybox的init进程是怎么做的:busybox的init进程从init_main函数开始,注意它没有 main函数,这是busybox体系决定的,在busybox中所有进程都是busybox,不同的参数决定执行不同的进程,具体研究一下就明白了,这 里就不多说了,看一下init_main:

int init_main(int argc, char **argv)

{

...//前面主要就是解析/etc/inittab然后运行初始化脚本,和system v的init没有本质区别,所以掠过

    while (1) {

...

        /* Wait for a child process to exit */

        wpid = wait(NULL);            //看到这里,你还把busybox的僵尸进程多的原因推卸给busybox的init吗?

        while (wpid > 0) {

            /* Find out who died and clean up their corpse */

            for (a = init_action_list; a; a = a->next) {

                if (a->pid == wpid) {

                    /* Set the pid to 0 so that the process gets

                     * restarted by run_actions() */

                    a->pid = 0;

                    message(LOG, "Process '%s' (pid %d) exited.  "

                            "Scheduling it for restart.",

                            a->command, wpid);

                }

            }

            /* see if anyone else is waiting to be reaped */

            wpid = waitpid(-1, NULL, WNOHANG);    //如果还不明白就看一下内核的sys_wait4调用吧,该系统调用里回收了所有状态为“僵尸”的子进程,如果系统将没有父亲的进程都过继给了init,在busybox里面是没有任何问题的,这里全部被回收了。

        }

    }

}

既然不是init惹的祸,那么是谁呢?想象一下linux里面的老大级别的除了内核,init进程还有谁?答案是shell,我们知道当你得到一个 shell,那么该shell下面的所有的进程都是该shell的子进程,如果shell不wait的话,僵尸还是会出现的,那就看看shell吧,我们 看msh.c文件,通篇查找没有找到wait(-1,...)的,倒是有wait调用,全是wait特定pid的进程的,也就是wait它的直接子进程,那么就是不管过继给它的子进程了,因此如果将一个进程过继给了msh,那么就别指望msh回收了,它不管这种事。过继给shell的可能性极大,毕竟 shell是很多进程的父进程,认祖父为父在linux里面是再正常不过的了(内核的意思是认叔叔为父,这个还比较正常)。
于是真相大白了,busybox里的僵尸进程很大部分是shell设计的问题,但是也不一定,我敢肯定的是大多是是这样的,因为我调试shell的时候事实就是如此,可能还有别的凶手,我懒得找了。
也许有些较真的看了以上文字会去看一下子进程过继的相关代码,那么我还是具体说一下好了,不就是forget_original_parent嘛:
static inline void forget_original_parent(struct task_struct * father, struct list_head *to_release)
{
         struct task_struct *p, *reaper = father;
         struct list_head *_p, *_n;
         do {
                 reaper = next_thread(reaper);    //在本线程组找新父亲,就是找一个叔叔
                 if (reaper == father) {
                         reaper = child_reaper;   //如果没有再过继给init进程
                         break;
                 }
         } while (reaper->state >= TASK_ZOMBIE);
//就此打住,再往后说就没完了,只要明白这里的reaper是所有退出进程的子进程们的新父亲就可以了。
}
那 么按照上述推理,busybox的shell就应该和它的子进程是同一线程组的了(它显然不是init进程),那么就看看msh.c文件吧,里面只要 fork新进程,通篇用vfork,所谓vfork就是和当前进程共用虚存空间,在sys_vfork里明确指示要CLONE_VM标志,这样shell 不一定和子进程在同一线程组,但是和父进程关系甚密。vfork在调用exec之前完全在父进程的空间运行,这样可以减少复制开销,直到exec才和父进程分道扬镳,但是却还是和父进程关系甚密。

原文链接: https://blog.csdn.net/dog250/article/details/5303622

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    busybox里的僵尸进程为何那么多

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/410041

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月26日 上午11:46
下一篇 2023年4月26日 上午11:47

相关推荐