一、概念
Linux系统中,应用程序以进程的方式存在,调度也以进程为单位,有关进程的概念就不多说了,参考教科书。
本文主要关注进程状态、偶然会见到的僵尸进程(Z状态)、以及很少见过的X状态进程。
每个进程都有相应的状态,如平常常见的R、S和D状态,也有在出现问题时见到的Z状态,即僵尸状态,还有极少见到的X状态,这也是本文重点分析和关注的。
首先需要简单介绍下几种基本的进程状态的相关概念:
1、R状态
R即Running状态为可运行状态,但并不代表该进程正在运行,只有处于R状态的进程才可以被调度器选中运行,也就是说R状态的进程有机会得到调度运行,其它状态进程不行,当R状态进程被调度器选中运行时,其才正在开始运行。
2、S状态
S即Sleeping状态为可中断睡眠状态,对应于内核中的INTERRUPTIBLE状态,可中断的意思时,处于S状态的进程可以处理信号,可以被信号中断和唤醒。这是系统中最常见的进程状态了。
3、D状态
D为不可中断睡眠状态,对应于内核中的UNINTERRUPTIBLE状态,不可中断的意思是,处于D状态的额进程不能处理信号,不能被信号中断和唤醒,处于该状态的进程大多在等待IO完成后将其唤醒,其它方式不能唤醒,处于D状态的进程由于不处理信号,所以无法被kill,这也是平常遇到的比较头疼的问题。在处理问题时经常见D状态进程,无法kill,也没有其它办法处理,有人想把它强制kill掉,但没有办法,及时有办法(比如内核模块),但其实这样做也不妥。首先如果进程长期处于D状态不退出的话,那此时该进程或系统肯定有问题了,D状态通常等待IO完成,完成后会自动唤醒退出,不应该长期处于D状态,如果是这样,要么是系统IO挂住了(通常scsi等层都有超时机制的,所以通常不会导致长期D),要么是内核中产生死锁了(这种可能性比较大),要么内核出其它问题了。内核中针对长期处于D状态的进程也有相应的检测手段,俗称hungtask检测机制,基本原理是定期检测处于D状态的进程,如果D状态持续时间超过120s,就打印相应的堆栈及错误信息,也可以通过配置使内核直接panic,这样可以通过kdump搜集vmcore做详细分析。好像说太多了,说来话太长,这里就不继续了。
4、Z状态(僵尸进程)
僵尸进程产生的原理为:当进程退出时,默认会向其父进程发送SIGCLD信号,同时将自己设置为Z状态(僵尸状态),父进程在收到SIGCLD信号后,标准做法需要在SIGCLD信号的处理中,调用wait(或类似接口)函数对其子进程占用的剩余资源(如进程描述符)进行回收,回收后,进程彻底退出并消失。
也就是说Z状态(僵尸状态),其实是进程退出过程中的一个正常的中间状态,正常情况下该状态持续的时间应该比较短,应该会很快被回收并退出。当发现一个进程长期处于僵尸状态时,可能的原因有:
1)父进程的SIGCLD信号处理函数中没有调用wait。
2)父进程SIGCLD信号处理函数中调用wait执行过程中阻塞,可能由于资源回收过程中发生阻塞,见过的案例有:wait过程中,需要等待进程的所有子线程退出,而子线程处于D状态不返回,导致wait无法继续。
3)内核出问题了。
那如果父进程在收到SIGCLD信号之前先退出了,是否会导致僵尸进程呢?
答案是不会。因为父进程退出后,子进程会变成孤儿进程,孤儿进程会由init进程自动接管,而init进程会定期通过wait回收其正在退出过程中处于僵尸状态的进程,所以正常情况下,是不会出现这样的情况的。
5、X状态(Dead状态)
X即Dead状态,跟Z状态是密切相关的。如前面所述,进程退出时,默认会向父进程发送SIGCLD信号,但在发送之前,会先对父进程的sighand进行检查,当父进程忽略了SIGCLD信号时,就不会发送信号了,此时会将进程的退出状态设置为EXIT_DEAD,即X状态,此时进程的资源不由父进程回收,进程也不会进入僵尸状态。这种情况下,进程的资源需要自己回收,实际上是在内核调度到下一个进程开始执行时进行回收,回收后,进程消失,X状态也随之消失。
所以,X状态其实也是退出过程中的一个正常的中间状态,正常情况下该状态持续的时间应该比较短,应该会很快被回收并退出。当发现一个进程长期处于X状态时,那应该也有问题了,但这种情况很少见。
二、实现
1、内核中定义的进程状态
在3.10内核中,定义了如下进程状态:
点击(此处)折叠或打开
/*进程状态*/
#define TASK_RUNNING 0 /*可运行状态,被调度的对象*/
#define TASK_INTERRUPTIBLE 1 /*可中断睡眠状态,即平时见的S状态*/
#define TASK_UNINTERRUPTIBLE 2 /*不可中断睡眠状态,即平时见的D状态*/
#define __TASK_STOPPED 4
#define __TASK_TRACED 8 /*调试使用状态,比如gdb attach时*/
#define TASK_DEAD 64 /*进程退出后,等待资源回收时的状态*/
#define TASK_WAKEKILL 128
#define TASK_WAKING 256
#define TASK_PARKED 512
#define TASK_STATE_MAX 1024
组合状态:
点击(此处)折叠或打开
/* Convenience macros for the sake of set_task_state */
#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
#define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED)
#define TASK_TRACED (TASK_WAKEKILL | __TASK_TRACED)
/* Convenience macros for the sake of wake_up */
#define TASK_NORMAL (TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE)
#define TASK_ALL (TASK_NORMAL | __TASK_STOPPED | __TASK_TRACED)
/* get_task_state() */
#define TASK_REPORT (TASK_RUNNING | TASK_INTERRUPTIBLE | \
TASK_UNINTERRUPTIBLE | __TASK_STOPPED | \
__TASK_TRACED)
为何没有我们熟知的ZOMBIE(僵尸)状态?不是常见(或偶见)僵尸进程么?也没有X状态?
答案:进程状态中确实没有ZOMBIE(僵尸)状态,ZOMBIE(僵尸)在内核中只是一种exit_status,即退出状态,也就是进程退出时的一种状态,具体定义如下:
/* in tsk->exit_state */
#define EXIT_ZOMBIE16
#define EXIT_DEAD32
可见,退出状态中,除了ZOMBIE,还有另一种叫DEAD的状态,该状态其实就是X状态。
2、用户看到的进程状态
那我们在ps、top或cat /proc//status中看到的Z状态()的进程是如何产生的呢?
ps、top或cat /proc//status中看到进程状态来源于内核中如下的定义
点击(此处)折叠或打开
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"Z (zombie)", /* 16 */
"X (dead)", /* 32 */
"x (dead)", /* 64 */
"K (wakekill)", /* 128 */
"W (waking)", /* 256 */
"P (parked)", /* 512 */
};
以/proc//status中显示的状态为例,看看这个状态是如何获取并显示的。
cat /proc//status示例:
点击(此处)折叠或打开
[root@A10097139 ~]# cat /proc/115/status
Name: crypto/7
State: S (sleeping)
Tgid: 115
Pid: 115
PPid: 2
TracerPid: 0
Uid: 0 0 0 0
Gid: 0 0 0 0
Utrace: 0
FDSize: 64
Groups:
Threads: 1
SigQ: 2/30472
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: ffffffffffffffff
SigCgt: 0000000000000000
CapInh: 0000000000000000
CapPrm: ffffffffffffffff
CapEff: fffffffffffffeff
CapBnd: ffffffffffffffff
Cpus_allowed: 80
Cpus_allowed_list: 7
Mems_allowed: 00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
Mems_allowed_list: 0
voluntary_ctxt_switches: 2
nonvoluntary_ctxt_switches: 0
/proc//status其中的进程状态获取是通过如下调用路径:
proc_pid_status()
->task_state()
->get_task_state()
点击(此处)折叠或打开
/*获取进程状态,实际是根据根据task_struct->state和exit_state来确认的*/
static inline const char *get_task_state(struct task_struct *tsk)
{
/*将tsk->state通过TASK_REPORT过滤后,再组合exit_state,形成最终的状态*/
unsigned int state = (tsk->state & TASK_REPORT) | tsk->exit_state;
/*获取进程状态列表中的第一种状态*/
const char * const *p = &task_state_array[0];
BUILD_BUG_ON(1 + ilog2(TASK_STATE_MAX) != ARRAY_SIZE(task_state_array));
/*取进程状态中的最低位作为返回的状态,比如如果Z(Zombie)位为1,那就不管后面的DEAD位了*/
while (state) {
p++;
state >>= 1;
}
return *p;
}
可见,/proc中反应的进程状态实际为tsk->status和exit_state的组合。那需要继续分析这两种状态的设置情况。
3、Z(僵尸)状态和X(Dead)状态的形成
Z(僵尸)状态和X(Dead)状态的形成原理前面已经描述,这里主要关注Z(Zombie)和X状态形成的相关流程。基本流程为:
进程退出必经do_exit入口,其中调用exit_notify通知父进程,如果父进程未忽略SIGCLD信号,则设置进程的退出状态(exit_state)为EXIT_ZOMBIE(即为Z状态);如果父进程忽略了SIGCLD信号,则设置进程的退出状态(exit_state)为EXIT_DEAD(即为X状态)。
do_exit():
点击(此处)折叠或打开
/*进程退出时必经入口,完成相应处理*/
void do_exit(long code)
{
struct task_struct *tsk = current;
int group_dead;
profile_task_exit(tsk);
WARN_ON(blk_needs_flush_plug(tsk));
/*不能在中断上下文中退出进程。*/
if (unlikely(in_interrupt()))
panic("Aiee, killing interrupt handler!");
/*不能kill idle(pid=0)进程*/
if (unlikely(!tsk->pid))
panic("Attempted to kill the idle task!");
...
/*退出通知,其中完成向父进程发SIGCLD信号*/
exit_notify(tsk, group_dead);
...
/* causes final put_task_struct in finish_task_switch(). */
/*设置进程状态为DEAD.Fixme:跟僵尸进程和ZOMBIE状态有何关系?*/
tsk->state = TASK_DEAD;
tsk->flags |= PF_NOFREEZE; /* tell freezer to ignore us */
schedule();
/*不应该再回来了。Fixme:task_struct会在finish_task_switch中清理?那SIGCLD信号谁来发?*/
BUG();
/* Avoid "noreturn function does return". */
for (;;)
cpu_relax(); /* For when BUG is null */
}
exit_notify():
点击(此处)折叠或打开
static void exit_notify(struct task_struct *tsk, int group_dead)
{
bool autoreap;
/*
* This does two things:
*
* A. Make init inherit all the child processes
* B. Check to see if any process groups have become orphaned
* as a result of our exiting, and if they have any stopped
* jobs, send them a SIGHUP and then a SIGCONT. (POSIX 3.2.2.2)
*/
forget_original_parent(tsk);
write_lock_irq(&tasklist_lock);
if (group_dead)
kill_orphaned_pgrp(tsk->group_leader, NULL);
if (unlikely(tsk->ptrace)) {
int sig = thread_group_leader(tsk) &&
thread_group_empty(tsk) &&
!ptrace_reparented(tsk) ?
tsk->exit_signal : SIGCHLD;
autoreap = do_notify_parent(tsk, sig);
} else if (thread_group_leader(tsk)) {
/*autoreap表示当父进程忽略了SIGCLD信号时,需要进程self-reap相应的资源*/
autoreap = thread_group_empty(tsk) &&
do_notify_parent(tsk, tsk->exit_signal); /*通过信号(通常是SIGCLD)通知父进程*/
} else {
autoreap = true;
}
/*
* 设置进程退出状态,父进程忽略了SIGCLD信号时,需要进程self-reap,
* 此时autoreap==1,则退出状态为EXIT_DEAD,否则为EXIT_ZOMBIE。
* 父进程只会负责EXIT_ZOMBIE状态的子进程的资源回收,EXIT_DEAD的进程
* 自行处理。
*/
tsk->exit_state = autoreap ? EXIT_DEAD : EXIT_ZOMBIE;
/* mt-exec, de_thread() is waiting for group leader */
if (unlikely(tsk->signal->notify_count < 0))
wake_up_process(tsk->signal->group_exit_task);
write_unlock_irq(&tasklist_lock);
/* If the process is dead, release it - nobody will wait for it */
if (autoreap)
release_task(tsk);
}
do_notify_parent():
点击(此处)折叠或打开
/*
* 通知父进程自己要退出了,其实就是向父进程发送SIGCLD信号,
* 如果父进程处理SIGCLD信号,则通常会在信号处理函数中调用wait()相关接口,
* 回收子进程最后的资源(比如task_struct?);如果父进程忽略该信号,则子进程
* 需要自行回收(self-reaping)。Fixme:可能会变僵尸进程?
*/
bool do_notify_parent(struct task_struct *tsk, int sig)
{
struct siginfo info;
unsigned long flags;
struct sighand_struct *psig;
bool autoreap = false;
cputime_t utime, stime;
BUG_ON(sig == -1);
/* do_notify_parent_cldstop should have been called instead. */
BUG_ON(task_is_stopped_or_traced(tsk));
BUG_ON(!tsk->ptrace &&
(tsk->group_leader != tsk || !thread_group_empty(tsk)));
if (sig != SIGCHLD) {
/*
* This is only possible if parent == real_parent.
* Check if it has changed security domain.
*/
if (tsk->parent_exec_id != tsk->parent->self_exec_id)
sig = SIGCHLD;
}
info.si_signo = sig;
info.si_errno = 0;
/*
* We are under tasklist_lock here so our parent is tied to
* us and cannot change.
*
* task_active_pid_ns will always return the same pid namespace
* until a task passes through release_task.
*
* write_lock() currently calls preempt_disable() which is the
* same as rcu_read_lock(), but according to Oleg, this is not
* correct to rely on this
*/
rcu_read_lock();
info.si_pid = task_pid_nr_ns(tsk, task_active_pid_ns(tsk->parent));
info.si_uid = from_kuid_munged(task_cred_xxx(tsk->parent, user_ns),
task_uid(tsk));
rcu_read_unlock();
task_cputime(tsk, &utime, &stime);
info.si_utime = cputime_to_clock_t(utime + tsk->signal->utime);
info.si_stime = cputime_to_clock_t(stime + tsk->signal->stime);
info.si_status = tsk->exit_code & 0x7f;
if (tsk->exit_code & 0x80)
info.si_code = CLD_DUMPED;
else if (tsk->exit_code & 0x7f)
info.si_code = CLD_KILLED;
else {
info.si_code = CLD_EXITED;
info.si_status = tsk->exit_code >> 8;
}
/*获取父进程的sighand*/
psig = tsk->parent->sighand;
spin_lock_irqsave(&psig->siglock, flags);
/*
* 如果发送信号为SIGCHLD且父进程忽略了SIGCHLD信号或者设置了SA_NOCLDWAIT标记,则设置autoreap,
* 即子进程自己回收资源,不由父进程通过wait来回收。
*/
if (!tsk->ptrace && sig == SIGCHLD &&
(psig->action[SIGCHLD-1].sa.sa_handler == SIG_IGN ||
(psig->action[SIGCHLD-1].sa.sa_flags & SA_NOCLDWAIT))) {
/*
* We are exiting and our parent doesn't care. POSIX.1
* defines special semantics for setting SIGCHLD to SIG_IGN
* or setting the SA_NOCLDWAIT flag: we should be reaped
* automatically and not left for our parent's wait4 call.
* Rather than having the parent do it as a magic kind of
* signal handler, we just set this to tell do_exit that we
* can be cleaned up without becoming a zombie. Note that
* we still call __wake_up_parent in this case, because a
* blocked sys_wait4 might now return -ECHILD.
*
* Whether we send SIGCHLD or not for SA_NOCLDWAIT
* is implementation-defined: we do (if you don't want
* it, just use SIG_IGN instead).
*/
autoreap = true;
if (psig->action[SIGCHLD-1].sa.sa_handler == SIG_IGN)
sig = 0;
}
if (valid_signal(sig) && sig)
/*向父进程发送信号(SIGCLD)*/
__group_send_sig_info(sig, &info, tsk->parent);
/*
* 唤醒父进程。
* Fixme:如果上面的if不成立,不发送信号,此时还唤醒父进程来干嘛?
* 答案:见上面注释:(?)
* Note that we still call __wake_up_parent in this case, because a
* blocked sys_wait4 might now return -ECHILD.
*/
__wake_up_parent(tsk, tsk->parent);
spin_unlock_irqrestore(&psig->siglock, flags);
return autoreap;
}
4、Z(僵尸)状态进程回收
如之前所说,僵尸进程的资源由父进程,在SIGCLD信号处理中,通过wait接口回收,回收代码流程如下:
sys_wait4()
->do_wait()
->do_wait_thread()
->wait_consider_task()
->wait_task_zombie()
->release_task()
5、X(Dead)状态进程回收
X状态进程资源不由父进程回收,需要自己回收(autoreap==1),其回收是在内核调度到下一个进程开始运行时进行的。
这里就涉及到进程上下文切换的问题,调度产生后必然会进行进程上下文切换,上下文切换后问题变得相对复杂一些,有关进程上下文切换相关的知识请参见另一篇blog:http://blog.chinaunix.net/uid-14528823-id-4740294.html内核调度的代码路径如下:
schedule()
->__schedule()
->context_switch()
->switch_to(宏)
实际的上下文切换发生在switch_to宏中。
这里需要分两种情况:
1)当调度时,被选中的next进程已经经历过调度时,上下文切换后其会继续从switch_to宏的“标号1”处继续执行:
点击(此处)折叠或打开
/*
* 上下文切换,在schedule中调用,current进程调度出去,当该进程被再次调度到时,重新从__switch_to后面开始执行
* prev:被替换的进程
* next:被调度的新进程
* last:当切换回原来的进程(prev)后,被替换的另外一个进程。
*/
#define switch_to(prev, next, last) \
do { \
/* \
* Context-switching clobbers all registers, so we clobber \
* them explicitly, via unused output variables. \
* (EAX and EBP is not listed because EBP is saved/restored \
* explicitly for wchan access and EAX is the return value of \
* __switch_to()) \
*/ \
unsigned long ebx, ecx, edx, esi, edi; \
\
asm volatile("pushfl\n\t" /* save flags */ /*将eflags寄存器值压栈*/\
"pushl %%ebp\n\t" /* save EBP */ /*将EBP压栈*/\
/*将当前栈指针(内核态)保存到prev进程的thread.sp中*/
"movl %%esp,%[prev_sp]\n\t" /* save ESP */ \
/*将next进程的栈指针(内核态)装载到ESP寄存器中*/
"movl %[next_sp],%%esp\n\t" /* restore ESP */ \
/*保存"标号1"的地址到prev进程的thread.ip,以便当prev进程重新被调度运行时,可以从"标号1处"重新开始执行*/
"movl $1f,%[prev_ip]\n\t" /* save EIP */ \
/*
* 将next进程的IP(通常都是"标号1"的地址,因为通常都是经历过这里的调度过程的,上一行代码中即保存了这个IP)
* 压入当前的(即next进程的)堆栈中。结合后面的jmp指令(注意:不是call指令)一起理解,当__switch_to执行完ret返回时,
* 会自动从当前的堆栈中弹出该地址作为函数的返回地址接着执行,如此即可实现新进程的运行。
*/
"pushl %[next_ip]\n\t" /* restore EIP */ \
__switch_canary \
/*
*jmp到__switch_to函数执行,当此函数返回时,自动跳转到[next_ip]开始执行,实现新进程的调度。注意不是call,jmp指令
* 不会自动将当前地址压栈,call会自动压栈
*/
"jmp __switch_to\n" /* regparm call */ \
/*当prev进程再次被调度到时,从这里开始执行*/
"1:\t" \
/*恢复EBP*/
"popl %%ebp\n\t" /* restore EBP */ \
/*恢复eflags*/
"popfl\n" /* restore flags */ \
\
/* output parameters */ \
/*输出参数*/
: [prev_sp] "=m" (prev->thread.sp), \
[prev_ip] "=m" (prev->thread.ip), \
"=a" (last), \
\
/* clobbered output registers: */ \
"=b" (ebx), "=c" (ecx), "=d" (edx), \
"=S" (esi), "=D" (edi) \
\
__switch_canary_oparam \
\
/* input parameters: */ \
/*输入参数*/
: [next_sp] "m" (next->thread.sp), \
[next_ip] "m" (next->thread.ip), \
\
/* regparm parameters for __switch_to(): */ \
/*将prev和next分别存入ecx和edx,然后作为参数传入到__switch_to函数中*/
[prev] "a" (prev), \
[next] "d" (next) \
\
__switch_canary_iparam \
\
: /* reloaded segment registers */ \
"memory"); \
} while (0)
退出switch_to宏后,会返回到context_switch函数中继续执行:
点击(此处)折叠或打开
/*
* context_switch - switch to the new MM and the new
* thread's register state.
*/
static inline void
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next)
{
...
/* Here we just switch the register state and the stack. */
/*切换到新的进程上下文*/
switch_to(prev, next, prev);
/*屏障,防止乱序*/
barrier();
/*
* this_rq must be evaluated again because prev may have moved
* CPUs since it called schedule(), thus the 'rq' on its stack
* frame will be invalid.
*/
/*
* 上下文切换后,会继续到这里执行,但这里已经是新的进程上下文了
* 在新的上下文中,清理掉上一个被调度进程prev的相关资源(比如DEAD状态的进程占用的资源)。
*/
finish_task_switch(this_rq(), prev);
/*
* Fixme:本函数执行完成后,返回到哪里? 这里已经是新的进程上下文了,
* 进程的内核栈已经切换,所以,内核栈中该函数的返回地址也已经
* 切换了,因此,不可能再返回上一个进程的上下文中的__schedule函数了。
* 但是新的进程上下文该函数的上级函数(该返回的函数)也必然是__schedule函数,
* 因为每个进程的调度都需要经历相同的过程和函数调用,所以实际上,
* 这里还是返回__schedule函数,只是在新的进程上下文中运行而已。
*/
}
X状态(EXIT_DEAD)的进程在finish_task_switch函数中被回收:
点击(此处)折叠或打开
static void finish_task_switch(struct rq *rq, struct task_struct *prev)
__releases(rq->lock)
{
struct mm_struct *mm = rq->prev_mm;
long prev_state;
rq->prev_mm = NULL;
/*
* A task struct has one reference for the use as "current".
* If a task dies, then it sets TASK_DEAD in tsk->state and calls
* schedule one last time. The schedule call will never return, and
* the scheduled task must drop that reference.
* The test for TASK_DEAD must occur while the runqueue locks are
* still held, otherwise prev could be scheduled on another cpu, die
* there before we look at prev->state, and then the reference would
* be dropped twice.
* Manfred Spraul
*/
prev_state = prev->state;
vtime_task_switch(prev);
finish_arch_switch(prev);
perf_event_task_sched_in(prev, current);
finish_lock_switch(rq, prev);
finish_arch_post_lock_switch();
fire_sched_in_preempt_notifiers(current);
if (mm)
mmdrop(mm);
/*判断DEAD状态(即X状态)的进程,如果是的话,需要对齐占用的资源(比如进程描述符)进行回收*/
if (unlikely(prev_state == TASK_DEAD)) {
task_numa_free(prev);
/*
* Remove function-return probe instances associated with this
* task and put them back on the free list.
*/
kprobe_flush_task(prev);
put_task_struct(prev);
}
tick_nohz_task_switch(current);
}
2)当进程被fork创建后首次运行
当进程被fork创建后首次运行时,在进程上下文切换后,switch_to宏中应该返回到ret_from_fork(entry_32.S汇编代码中定义)处开始执行(具体原理参见另一篇blog)
点击(此处)折叠或打开
/*fork返回,单独处理*/
ENTRY(ret_from_fork)
CFI_STARTPROC
pushl_cfi %eax
/*进行调度收尾处理,包括回收DEAD状态(X状态)的进程*/
call schedule_tail
GET_THREAD_INFO(%ebp)
popl_cfi %eax
pushl_cfi $0x0202 # Reset kernel eflags
popfl_cfi
jmp syscall_exit
CFI_ENDPROC
END(ret_from_fork)
其中,schedule_tail会调用finish_task_switch回收X状态进程。