为什么busybox中的crond是linux 僵尸进程程

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 =
&&&&&&&& 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)
&&&&&&&& 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_
&&&&&&&& 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 =
&&&&&&&& 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 sighand_struct *
&&&&&&&& info.si_signo =
&&&&&&&& info.si_errno = 0;
&&&&&&&& info.si_pid = tsk-&
info.si_uid = tsk-&
&&&&&&&& info.si_utime = tsk-&utime + tsk-&signal-&
&&&&&&&& info.si_stime = tsk-&stime + tsk-&signal-&
&&&&&&&& psig = tsk-&parent-&
&&&&&&&& spin_lock_irqsave(ψg-&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, ∈fo, tsk-&parent);
&&&&&&&& __wake_up_parent(tsk, tsk-&parent);
&&&&&&&& spin_unlock_irqrestore(ψg-&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_ 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 = &&&&&&&& struct list_head *_p, *_n; &&&&&&&& do { &&&&&&&&&&&&&&&& reaper = next_thread(reaper);&&& //在本线程组找新父亲,就是找一个叔叔 &&&&&&&&&&&&&&&& if (reaper == father) { &&&&&&&&&&&&&&&&&&&&&&&& reaper = child_&& //如果没有再过继给init进程 &&&&&&&&&&&&&&&&&&&&&&&& &&&&&&&&&&&&&&&& } &&&&&&&& } while (reaper-&state &= TASK_ZOMBIE); //就此打住,再往后说就没完了,只要明白这里的reaper是所有退出进程的子进程们的新父亲就可以了。 } 那 么按照上述推理,busybox的shell就应该和它的子进程是同一线程组的了(它显然不是init进程),那么就看看msh.c文件吧,里面只要 fork新进程,通篇用vfork,所谓vfork就是和当前进程共用虚存空间,在sys_vfork里明确指示要CLONE_VM标志,这样shell 不一定和子进程在同一线程组,但是和父进程关系甚密。vfork在调用exec之前完全在父进程的空间运行,这样可以减少复制开销,直到exec才和父进程分道扬镳,但是却还是和父进程关系甚密。
原文链接:

我要回帖

更多关于 查看僵尸进程 的文章

 

随机推荐