2015年(100)
分类: LINUX
2015-07-04 20:42:07
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才和父进程分道扬镳,但是却还是和父进程关系甚密。