三. 进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这称为进程切换。
3.1 硬件上下文
尽管每个进程可以拥有属于自己的地址空间,但所有进程必须共享CPU寄存器。因此,在恢复一个进程的执行之前,内核必须确保每个寄存器装入了挂起进程时的值。
进程恢复执行前必须装入寄存器的一组数据称为硬件上下文。硬件上下文是进程可执行上下文的一个子集,因为可执行上下文包含进程执行时需要的所有信息。在Linux中,(intel)进程硬件上下文存放在TSS段,而剩余部分存放在内核态堆栈中。
进程切换只发生在内核态。在执行进程切换之前,用户态进程使用的所有寄存器内容都已保存在内核态堆栈上,这也包括SS和ESP这对寄存器的内容。
3.2 任务状态段(TSS)
TSS是用来存放硬件上下文的。但是linux没有使用TSS来做硬件上下文的切换。
tss_struct结构描述TSS的格式。init_tss数组为系统上每个不同的CPU存放一个TSS。在每次进程切换时,内核都更新TSS的某些字段以便相应的CPU控制单元可以安全地检索到它需要的信息。因此,TSS反映了CPU上当前进程的特权级,但不必为没有运行的进程保留TSS。
每个TSS有一个8字节的任务状态段描述符(TSSD)。由Linux创建的TSSD存放在全局描述符标(GDT)中,GDT的基地址存放在gdtr寄存器中,tr寄存器存放TSSD选择符。
3.2.1 thread字段
linux中,被替换的进程的硬件上下文没有保存在TSS中,他们保存在task_struct的thread字段中。这个数据结构包含的字段涉及大部分CPU寄存器,但不包括诸如eax、ebx等通用寄存器,它们保留在堆栈中。
3.3 执行进程切换
进程切换可能只发生在精心定义的点:schedule()函数(后边会详细讲到)。这里,我们仅关注内核如何执行一个进程切换:
- 切换页全局目录以安装一个新的地址空间(后续详细讲解);
- 切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包含CPU寄存器。
3.3.1 switch_to宏
硬件上下文的切换,由switch_to宏开始。这个宏跟体系结构相关。这个宏的最后一个参数last用来保存被切换出去的进程的值,宏的prev和next两个参数分别表示正在执行的进程和即将执行的进程,prev和next都是task_struct指针类型。x86下switch_to步骤如下:
- 在两个通用寄存器eax、edx中保存prev和next的值;
- 保存eflags和ebp寄存器(栈底)到prev内核栈中;
- 保存esp(栈顶)的内容到prev->thread.esp;
- 将next->thread.esp装入esp。此时,内核开始在next的内核栈上操作。这条指令实际完成了从prev到next进程的切换。此时current宏的返回值变指向next;
- 将prev进程下一条要被执行的指令地址保存到prev->thread.eip(实际被保存的指令并发进程B的指令,而是switch_to宏的切换指令);
- 将next->thread.eip的值压入next的内核栈;
- 执行switch_to函数;
- 进程prev再次获得CPU,esp指向prev进程。将eflags和ebp(栈低)压栈;
- 将eax的值保存到第三个参数last中。
3.3.2 __switch_to()函数
__switch_to函数也是体系结构相关的。这个函数跟普通函数的参数传递区别是:普通函数通过栈传递参数,此函数使用switch_to宏保存的eax和edx作为prev和next的参数,实现方式是通过gcc的扩张编译器宏。
函数执行过程:
- 有选择地保存FPU、MMX及XMM寄存器的内容;
- 将next->thread.esp0装入本地CPU的TSS的esp0字段;
- 将next进程使用的线程局部存储(TLS)段装入本地CPU的全局描述符表;
- 将fs,gs寄存器的内容分别存放在prev->thread.fs和prev->thread.gs中(fs/gs/ds/es都是数据段);
- 如果fs或者gs被prev和next中任一使用(值非0),就将next->thread中存放的fs和gs装入寄存器fs和gs;
- 如果next进程正在被调试,则装入next->thread中的debugreg数组的内容到对应寄存器;
- 根据需要,更新TSS的I/O bitmap;
四 创建进程
传统的Unix操作系统以统一的方式对待所有的进程:子进程复制父进程所拥有的资源。 这种方式使进程的创建非常慢且效率低,因为子进程需要拷贝父进程的整个地址空间。而且,很多时候子进程几乎不必读或修改父进程拥有的任何资源。
现代Unix内核通过引入以下机制解决了这个问题:
- 写时拷贝技术。允许父子进程读相同的物理页。任何一个进程试图写一个物理页时,内核就把这个页的内容拷贝到一个新的物理页,并把这个物理页映射给正在写的进程。
- 轻量级进程允许父子进程共享本地CPU在内核的很多数据结构,如页表(也就是整个用户态地址空间)、打开文件表及信号处理。
- vfork()系统调用创建的进程能共享其父进程的内存地址空间。为了防止父进程重写子进程需要的数据,阻塞父进程的执行,直到子进程退出或执行新的程序。
4.1 clone()、fork()及vfork()系统调用
Linux中,轻量级进程是由clone()函数创建的。
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, void *newtls, pid_t *ctid */ );
fn:新进程执行的函数, 函数的返回值表示子进程的退出代码。
arg: fn()函数的参数
flags: 控制子进程创建或退出等的某些行为,例如结束时候发送的信号代码,子进程创建完是否立即执行,是否共享页表等。
child_stack:表示把用户态堆栈指针赋给子进程的esp寄存器。父进程应该总是为子进程分配新的堆栈。
tls: 表示线程局部存储段(TLS)数据结构的地址,该结构是为新轻量级进程定义的。设置flags标志CLONE_SETTLS有效
传统的fork() 系统调用在Linux中是用clone()实现的,其中flags参数只设置了SIGCHLD信号,child_stack参数是父进程当前的堆栈指针。当父子进程之一试图写堆栈的时候,写时拷贝机制会产生一份栈的拷贝,之后两个进程都使用各自独立的堆栈。
4.1.1 do_fork()函数
内核中的do_fork()函数负责处理clone()系统调用。
/*
* Ok, this is the main fork-routine.
*
* It copies the process, and if successful kick-starts
* it and waits for it to finish using the VM if required.
*/
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
clone_flags:即clone()系统调用的flags
stack_start:即clone()系统调用参数child_stack
regs: 指向通用寄存器值的指针,通用寄存器的值是在用户态切换内核态时保存到内核态堆栈中的。
stack_size:未使用
do_fork()利用辅助函数copy_process()来创建进程描述符以及子进程执行所需要的所有其他内核数据结构。下面是do_fork()执行的主要步骤:
- 通过查找pid_map_array位图,为子进程分配新的PID。
- 检查自己是否可被跟踪,如果可以,并且需要被跟踪,设置CLONE_PTRACE标志。
- 调用copy_process()复制进程描述符。如果所有必须的资源都是可用的,该函数返回刚创建的task_struct描述符的地址。这是创建过程的关键步骤,后文详述。
- 如果flags设置了CLONE_STOPPED,或者必须跟踪子进程;则将进程状态设置成TASK_STOPPED,并为子进程增加挂起的信号SIGSTOP信号。
- 如果flags没有设置CLONE_STOPPED标志,则调用wake_up_new_task()函数以执行下述操作:
- 调整父进程和子进程的调度参数,以后详述
- 如果子进程将和父进程运行在同一个CPU上,而且父进程和子进程不能共享同一组页表(flags标志CLONE_VM未设置),那么,就把子进程插入父进程运行队列,插入时,子进程刚好在父进程前面,这样可以使子进程先于父进程运行。如果子进程刷新其地址空间(使用新的页表),这样可以获得更好的性能。如果让父进程先运行,那么写时复制将会造成不必要的页面复制。
- 如果子进程和父进程运行在不同的CPU上,或者父子进程共享页表,就将子进程插入父进程运行队列的队尾。
- 如果CLONE_STOPPED标志被设置,就把子进程设置为TASK_STOPPED状态。
- 进程被跟踪的处理
- 如果flags设置了CLONE_VFORK标志,则把父进程插入等待队列,并挂起父进程直到子进程释放自己的内存空间(即子进程结束或执行新的程序)。
- 结束并返回子进程的PID。
4.1.2 copy_process()函数
copy_process()创建进程描述符以及子进程执行所需要的所有其他数据结构。
/*
* This creates a new process as a copy of the old one,
* but does not actually start it yet.
*
* It copies the registers, and all the appropriate
* parts of the process environment (as per the clone
* flags). The actual kick-off is left to the caller.
*/
static task_t *copy_process(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
int pid)
它的参数与do_fork()相同,外加一个子进程的pid,下面描述其重要步骤:
- 检查flags标志是否正确
- 调用dup_task_struct()为子进程分配进程描述符。同时,分配内核线程堆栈并做初始化。
- 检查当前用户及系统总的线程数量是否满足
- 设置进程状态的几个关键字段:
- lock_depth: -1 (大内核锁,以后详述)
- did_exec:0 进程发起execve()系统调用的次数
- flags: 更新从父进程拷贝过来的标志
- pid: 参数传入的新进程的pid,
- 调用copy_xx()类的函数来创建新的数据结构,用父进程对应的数据填充新的数据结构。包括:mm,namespace,fs,files,signal等
- 调用copy_thread()来初始化新的内核栈
- 调用sched_fork()完成对新进程调度相关数据的初始化,子进程共享父进程的调度时间片。
- 初始化进程中表示亲子关系的字段
- 执行SET_LINKS宏,把新进程描述符插入进程链表
- 调用attach_pid()将进程插入到pid相关的四张表中,如果父进程不是领头线程,就不插入PGID和SID表。
- 递增总线程计数
- 返回新进程描述符指针
4.2 内核线程
内核线程在以下方面跟普通进程不同:
- 内核线程只运行在内核态,而普通进程可以运行在用户态,也可以运行在内核态(系统调用);内核线程没有用户态上下文切换的开销
- 内核线程只能使用PAGE_OFFSET(i386 默认1G)大小的线性地址空间,而普通进程可以使用4GB的线性地址空间
4.2.1 创建一个内核线程
kernel_thread()函数创建一个新的内核线程,它的参数:
- fn:所要执行的内核函数的地址
- arg:fn的参数
- flags:clone的标志
/*
* Create a kernel thread
*/
int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
struct pt_regs regs;
memset(®s, 0, sizeof(regs));
regs.ebx = (unsigned long) fn;
regs.edx = (unsigned long) arg;
regs.xds = __USER_DS;
regs.xes = __USER_DS;
regs.orig_eax = -1;
regs.eip = (unsigned long) kernel_thread_helper;
regs.xcs = __KERNEL_CS;
regs.eflags = X86_EFLAGS_IF | X86_EFLAGS_SF | X86_EFLAGS_PF | 0x2;
/* Ok, create the new process.. */
return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, ®s, 0, NULL, NULL);
}
该函数最终调用do_fork()完成线程创建。CLONE_VM标志避免复制调用进程的页表:由于新内核线程无论如何都不会访问用户态地址空间,所以这种复制是没有意义的。CLONE_UNTRACED标志表示内核线程不能被跟踪,即使调用进程被跟踪。
4.2.2 进程0
所有进程的祖先叫做进程0,idle进程或者(因为历史原因)叫做swapper进程,它是Linux的初始化阶段从无到有创建的一个内核线程。这个祖先进程使用下列静态分配的结构(其它进程的数据结构都是动态分配的):
- 存放在init_task变量中的进程描述符,由INIT_TASK宏完成对它的初始化
- 存放在init_thread_union变量中的thread_info描述符和内核堆栈,由INIT_THREAD_INFO宏完成对它的初始化
- 进程描述符中的下列字段,分别由对应的宏完成初始化:
- init_mm: INIT_MM
- init_fs:INIT_FS
- init_files:INIT_FILES
- init_signals:INIT_SIGNALS
- init_sighand:INIT_SIGHAND
- 主内核页全局目录存放在swapper_pg_dir中
start_kernel()函数初始化内核需要的所有数据结构,激活中断,最后创建另一个叫进程1(init进程)的内核线程:
kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND); /* called by rest_init() */
新创建的init内核线程的PID为1,与进程0共享进程相关的内核数据结构,当调度程序选择到它时,init进程开始执行init函数。
创建init进程后,进程0执行cpu_idle()函数,该函数本质上是在开中断的情况下重复执行hlt汇编指令。只有当没有其它进程处于TASK_RUNNING状态时,调度程序才选择进程0。在多处理器系统中,每个CPU都有一个进程0,且PID都为0.
4.2.3 进程1
由进程0创建的内核线程执行init()函数,init()依次完成内核初始化。init()调用execve()系统调用装入可执行程序init。结果,init内核线程变为一个普通进程,且拥有自己的本地CPU相关的(per-process)内核数据结构。在系统关闭之前,init进程一直存活(也可以使用systemd等相同功能的程序),因为它需要创建和监控在操作系统外层执行的所有进程的活动。
五 撤销进程
进程终止的一般方式是调用exit()库函数,该函数释放C函数库所分配的资源,执行编程者所注册的每个函数,C编译器总是把exit()系统调用插入main()函数的最后一条语句之后。
内核某些情况下也会终止整个线程组:
- 当进程接收到一个不能处理或忽视的信号。
- 当进程在内核中产生了一个不可恢复的CPU异常时。
5.1 进程终止
在Linux 2.6中有两个终止用户态应用的系统调用:
- exit_group()系统调用:它终止整个线程组。C库函数exit()调用这个系统调用,内核中对应的主要函数是do_group_exit()
- exit()系统调用:它终止某一个线程。线程库函数pthread_exit()调用这个系统调用,内核中对应的主要函数是do_exit()
5.1.1 do_group_exit()函数
do_group_exit()函数杀死属于current线程组的所有进程。它接收进程终止代号作为参数,参数可能来自于系统调用exit_group()(正常结束),也可能来自于内核的错误代号(异常结束)。
/*
* Take down every thread in the group. This is called by fatal signals
* as well as by sys_exit_group (below).
*/
NORET_TYPE void
do_group_exit(int exit_code)
{
BUG_ON(exit_code & 0x80); /* core dumps don't get here */
/* 检查是否已经开始了线程组的推出过程 */
if (current->signal->flags & SIGNAL_GROUP_EXIT)
exit_code = current->signal->group_exit_code;
else if (!thread_group_empty(current)) {
struct signal_struct *const sig = current->signal;
struct sighand_struct *const sighand = current->sighand;
read_lock(&tasklist_lock);
spin_lock_irq(&sighand->siglock);
if (sig->flags & SIGNAL_GROUP_EXIT)
/* Another thread got here before we took the lock. */
exit_code = sig->group_exit_code;
else {
sig->flags = SIGNAL_GROUP_EXIT;
/* 设置线程组的退出标志 */
sig->group_exit_code = exit_code;
/* 杀死current线程组中的其它进程。通过current->tgid查对应的hash表,
* 给表中的所有成员发送SIGKILL信号实现
*/
zap_other_threads(current);
}
spin_unlock_irq(&sighand->siglock);
read_unlock(&tasklist_lock);
}
/* 杀死本进程且不再返回 */
do_exit(exit_code);
/* NOTREACHED */
}
5.1.2 do_exit()函数
所有进程的终止都是由do_exit()函数来处理的,这个函数从内核数据结构中删除对终止进程的大部分应用。
fastcall NORET_TYPE void do_exit(long code)
{
struct task_struct *tsk = current;
int group_dead;
profile_task_exit(tsk);
if (unlikely(in_interrupt()))
panic("Aiee, killing interrupt handler!");
if (unlikely(!tsk->pid))
panic("Attempted to kill the idle task!");
if (unlikely(tsk->pid == 1))
panic("Attempted to kill init!");
if (tsk->io_context)
exit_io_context();
if (unlikely(current->ptrace & PT_TRACE_EXIT)) {
current->ptrace_message = code;
ptrace_notify((PTRACE_EVENT_EXIT << 8) | SIGTRAP);
}
/* 设置标志,表示进程正在被删除 */
tsk->flags |= PF_EXITING;
/* 从动态定时器队列中删除本进程 */
del_timer_sync(&tsk->real_timer);
if (unlikely(in_atomic()))
printk(KERN_INFO "note: %s[%d] exited with preempt_count %d\n",
current->comm, current->pid,
preempt_count());
acct_update_integrals();
update_mem_hiwater();
group_dead = atomic_dec_and_test(&tsk->signal->live);
if (group_dead)
acct_process(code);
/* 下面几个exit_xx()函数分别从进程描述符中分离出分页、信号量、文件系统、
* 打开文件描述符、命名空间及IO权限位图的相关数据结构。减少这些对象的
* 引用计数,并对引用计数为0的对象进行释放。
*/
exit_mm(tsk);
exit_sem(tsk);
__exit_files(tsk);
__exit_fs(tsk);
exit_namespace(tsk);
exit_thread();
exit_keys(tsk);
if (group_dead && tsk->signal->leader)
disassociate_ctty(1);
module_put(tsk->thread_info->exec_domain->module);
if (tsk->binfmt)
module_put(tsk->binfmt->module);
/* 设置进程终止的代号 */
tsk->exit_code = code;
/* 此函数完成(后文详述):进程亲属关系的更新、设置僵死进程或者
* 正常退出并清理其余资源。
*/
exit_notify(tsk);
#ifdef CONFIG_NUMA
mpol_free(tsk->mempolicy);
tsk->mempolicy = NULL;
#endif
BUG_ON(!(current->flags & PF_DEAD));
schedule();
BUG();
/* Avoid "noreturn function does return". */
for (;;) ;
}
函数的最后调用schedule()函数选择一个新进程运行。调度程序忽略处于EXIT_ZOMBIE状态的进程,所以这种进程正好在schedule()中的宏switch_to被调用之后停止执行。
5.1.3 exit_notify()函数
/*
* Send signals to all our closest relatives so that they know
* to properly mourn us..
*/
static void exit_notify(struct task_struct *tsk)
{
int state;
struct task_struct *t;
struct list_head ptrace_dead, *_p, *_n;
if (signal_pending(tsk) && !(tsk->signal->flags & SIGNAL_GROUP_EXIT)
&& !thread_group_empty(tsk)) {
/*
* This occurs when there was a race between our exit
* syscall and a group signal choosing us as the one to
* wake up. It could be that we are the only thread
* alerted to check for pending signals, but another thread
* should be woken now to take the signal since we will not.
* Now we'll wake all the threads in the group just to make
* sure someone gets all the pending signals.
*/
read_lock(&tasklist_lock);
spin_lock_irq(&tsk->sighand->siglock);
for (t = next_thread(tsk); t != tsk; t = next_thread(t))
if (!signal_pending(t) && !(t->flags & PF_EXITING)) {
recalc_sigpending_tsk(t);
if (signal_pending(t))
signal_wake_up(t, 0);
}
spin_unlock_irq(&tsk->sighand->siglock);
read_unlock(&tasklist_lock);
}
write_lock_irq(&tasklist_lock);
/*
* 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)
*/
/* 更新相关父子进程的亲属关系。如果同一线程组中有正在运行的进程,
* 就让终止进程所创建的所有子进程变成同一线程组中另外一个进程的
* 子进程,否则让他们成为init的子进程。
* /
INIT_LIST_HEAD(&ptrace_dead);
forget_original_parent(tsk, &ptrace_dead);
BUG_ON(!list_empty(&tsk->children));
BUG_ON(!list_empty(&tsk->ptrace_children));
/*
* 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)
*
* Case i: Our father is in a different pgrp than we are
* and we were the only connection outside, so our pgrp
* is about to become orphaned.
*/
t = tsk->real_parent;
if ((process_group(t) != process_group(tsk)) &&
(t->signal->session == tsk->signal->session) &&
will_become_orphaned_pgrp(process_group(tsk), tsk) &&
has_stopped_jobs(process_group(tsk))) {
__kill_pg_info(SIGHUP, (void *)1, process_group(tsk));
__kill_pg_info(SIGCONT, (void *)1, process_group(tsk));
}
/* Let father know we died
*
* Thread signals are configurable, but you aren't going to use
* that to send signals to arbitary processes.
* That stops right now.
*
* If the parent exec id doesn't match the exec id we saved
* when we started then we know the parent has changed security
* domain.
*
* If our self_exec id doesn't match our parent_exec_id then
* we have changed execution domain as these two values started
* the same after a fork.
*
*/
if (tsk->exit_signal != SIGCHLD && tsk->exit_signal != -1 &&
( tsk->parent_exec_id != t->self_exec_id ||
tsk->self_exec_id != tsk->parent_exec_id)
&& !capable(CAP_KILL))
tsk->exit_signal = SIGCHLD;
/* If something other than our normal parent is ptracing us, then
* send it a SIGCHLD instead of honoring exit_signal. exit_signal
* only has special meaning to our real parent.
*/
/ * 如果进程的exit_signal不为-1,且是该进程组的最后一个成员;则给其
* 父进程发送SIGCHLD信号,以告知子进程死亡。 否则,在进程被跟踪的
* 情况下,对其父进程发送SIGCHLD信号。
*/
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);
}
/* 如果进程的exit_signal等于-1,且没有被跟踪。则设置进程描述符的exit_state
* 为EXIT_DEAD, 否则设置为EXIT_ZOMBIE。僵死进程暂时不会被回收。
*/
state = EXIT_ZOMBIE;
if (tsk->exit_signal == -1 &&
(likely(tsk->ptrace == 0) ||
unlikely(tsk->parent->signal->flags & SIGNAL_GROUP_EXIT)))
state = EXIT_DEAD;
tsk->exit_state = state;
/*
* Clear these here so that update_process_times() won't try to deliver
* itimer, profile or rlimit signals to this task while it is in late exit.
*/
tsk->it_virt_value = cputime_zero;
tsk->it_prof_value = cputime_zero;
write_unlock_irq(&tasklist_lock);
list_for_each_safe(_p, _n, &ptrace_dead) {
list_del_init(_p);
t = list_entry(_p,struct task_struct,ptrace_list);
release_task(t);
}
/* If the process is dead, release it - nobody will wait for it */
/* 回收进程的其它数据结构占用的内存,并递减进程描述符的引用计数。
* 正常情况下, 递减后,引用计数变成1,因此进程描述符不会被释放。
*/
if (state == EXIT_DEAD)
release_task(tsk);
/* PF_DEAD causes final put_task_struct after we schedule. */
/* 合适的时候,进程描述符会被调度器彻底释放 */
preempt_disable();
tsk->flags |= PF_DEAD;
}
5.2 进程删除
Unix允许进程查询内核来获得其父进程的PID,或者其任何子进程的执行状态。例如,进程可以创建一个子进程来执行特定的任务,然后调用诸如wait()这样的一些库函数检查子进程是否终止。如果子进程已经终止,那么,它的终止代号将高速父进程这个任务是否已经成功完成。
为了遵循这些设计选择,不允许Unix内核在进程一终止就马上丢弃包含在进程描述符字段中的数据。只有父进程发出了与被终止的进程的相关的wait()类系统调用之后,才允许销毁进程描述符。这就是引入僵死状态的原因:尽管从技术上来说进程已死,但必须保存它的描述符,直到父进程得到通知。
如果父进程在子进程结束之前结束会发生什么情况了?这种情况下,系统中会到处都是僵死进程,而且它们的进程描述符会永久的占据RAM。如前所述,必须强迫所有的孤儿进程称为init进程的子进程来解决这个问题。这也,init进程在用wait()类系统调用检查其合法的子进程终止时,就会撤销僵死的进程。
release_task()函数(?)从僵死进程的描述符中分离出最后的数据结构;对僵死进程的处理有两种可能的方式:如果父进程不需要接收来自子进程的信号,就调用do_exit();如果已经给父进程发送了一个信号,就调用wait4()或waitpid()系统调用。在后一种情况下,函数还将回收进程描述符所占用的内存空间,而在前一种情况下,内存的回收将有进程调度程序来完成。其完成步骤如下(?在哪儿实现):
- 递减终止进程拥有者的进程个数
- 如果进程正在被跟踪,则将它从调试程序的ptrace_children链表中删除,并让该紧凑重新属于初始的父进程
- 调用__exit_signal()删除所有的挂起的信号并减少signal_struct的引用计数,计数为0则销毁删除。
- 调用__exit_sighand()删除信号处理函数。
- 调用__unhash_process(), 该函数依次执行下面的操作:
- 变量nr_threads减1
- 两次调用detach_pid(),分别从PIDTYPE_PID何PIDTYPE_TGID类型的hash表中删除进程描述符。
- 如果经常是线程组的领头进程,则再调用两次detach_pid(), 从PIDTYPE_PGID和PIDTYPE_SID类型的hash表中删除进程描述符。
- 用宏REMOVE_LINKS从进程链表中删除进程
- 如果进程不是线程组的领头进程,领头进程处于僵死状态,而且进程是线程组的最后一个成员,则向领头进程的父进程发送一个信号,通知进程已经死亡。
- 调用sched_exit()函数来调整父进程的时间片
- 调用put_task_struct()递减进程描述符的使用技术,如果使用计数变成0,则函数终止所有残留的对进程的引用。
- 递减进程所有者的user_struct数据结构的使用计数器,如果计数变为0,则释放该数据结构
- 释放进程描述符以及thread_info描述符和内核态堆栈所占用的内存区域。