当一个进程终结时,内核必须释放掉它所占有的资源并把这一终结事件告知父进程。进程的终结大部分都要靠 exit() 来完成的,最终的系统调用为 do_exit()。
当一个进程终结时,内核必须释放掉它所占有的资源并把这一终结事件告知父进程。
进程的终结大部分都要靠 exit() 来完成的,最终的系统调用为 do_exit()。
asmlinkage long sys_exit(int error_code)
{
do_exit((error_code&0xff)<<8);
}
/*当cpu进入到do_exit后,当前进程就会在中途寿终正寝,不会从这个函数中返回,当然也就不会从sys_exit
中返回,从而也就不会从系统调用exit()中返回*/
fastcall NORET_TYPE void do_exit(long code)
{
...
WARN_ON(atomic_read(&tsk->fs_excl));
/*由于中断服务程序根本不应该调用do_exit,不管是直接还是间接,所以首先通过in_interrupt进行加以检查
若发现是在某个中断服务程序中调用的,那就一定是出了问题*/
if (unlikely(in_interrupt()))
panic("Aiee, killing interrupt handler!");
...
/*current->flags的PF_EXITING标志表示进程正在被删除 */
if (unlikely(tsk->flags & PF_EXITING)) {
printk(KERN_ALERT
"Fixing recursive fault but reboot is needed!\n");
...
tsk->flags |= PF_EXITPIDONE; /* 设置进程标识为PF_EXITPIDONE*/
if (tsk->io_context)
exit_io_context();
/* 设置进程状态为不可中断的等待状态 */
set_current_state(TASK_UNINTERRUPTIBLE);
/* 调度其它进程 */
schedule();
}
tsk->flags |= PF_EXITING;
/* 内存屏障,用于确保在它以后的操做开始执行以前,它以前的操做已经完成 */
smp_mb();
spin_unlock_wait(&tsk->pi_lock);
...
//清除定时器
group_dead = atomic_dec_and_test(&tsk->signal->live);//live用来表示线程组中活动进程的数量
if (group_dead) { //当没有活动的进程时
exit_child_reaper(tsk);
//取消高精度定时器
hrtimer_cancel(&tsk->signal->real_timer);
//删除POSIX.1b类型的定时器
exit_itimers(tsk->signal);
}
//收集进程会计信息
acct_collect(code, group_dead);
...
//设置终止代码
tsk->exit_code = code;
taskstats_exit(tsk, group_dead);
//释放线性区描述符和页表
exit_mm(tsk);
if (group_dead)
acct_process();
//遍历current->sysvsem.undo_list链表,并清除进程所涉及的每个IPC信号量的操作痕迹
exit_sem(tsk);
//释放文件对象相关资源
__exit_files(tsk);
//释放struct fs_struct结构体
__exit_fs(tsk);
//检查有多少未使用的进程内核栈
check_stack_usage();
exit_thread();
cgroup_exit(tsk, 1);
exit_keys(tsk);
if (group_dead && tsk->signal->leader)
disassociate_ctty(1);
module_put(task_thread_info(tsk)->exec_domain->module);
if (tsk->binfmt)
module_put(tsk->binfmt->module);
proc_exit_connector(tsk);
//给父进程发送信号,让其知道子进程生命已经结束,来料理子进程的后事. 同时把进程状态exit_state 设置成 EXIT_ZOMBIE
exit_notify(tsk);
...
tsk->flags |= PF_EXITPIDONE;
...
preempt_disable();
/* causes final put_task_struct in finish_task_switch(). */
tsk->state = TASK_DEAD;
/*do_exit 不返回的真正原因在这里,由于进程状态设置成了EXIT_ZOMBIE,使得该进程永远不会再被选中进行调度,所以
也就不会使用schedule()调度别的进程后从schedule中返回。因此只能等父进程收到子进程发送的信号来处理子进程,并将
子进程的task_struct结构释放掉,子进程最终从系统中消失。而父进程在wait4(对应系统函数sys_wait4)中等待着。
*/
schedule();
BUG();
/* Avoid "noreturn function does return". */
for (;;)
cpu_relax(); /* For when BUG is null */
}
do_exit() 完成工作如下:
- 对该调用进行检查,比如该方法是不能在中断服务程序中调用的。
- 将 task_struct 中的标志成员设置为 PF_EXITING。
- 删除内核定时器,根据返回的结果,它确保没有定时器在排队,也没有定时器处理程序在运行。
- 把进程的退出代码 exit_code 设置为由 exit() 提供的退出代码,或者去完成任何其他由内核机制规定的退出动作。退出代码存放在这里供父进程随时检索。
- 调用 exit_mm( )释放进程占用的 mm_struct,若没有别的进程使用它们(也即是这个地址空间没有被共享),就彻底释放它们。
- 调用 exit_sem(),清除进程所涉及的每个IPC信号量的操作痕迹,使得若进程排队等候IPC信号,则离开队列。
- 调用 __exit_files、__exit_fs,分别递减文件描述符、文件系统数据的引用计数。若其中某个引用计数的数值降为零,那么就代表没有进程在使用相应的资源,此时就可以释放。
- 调用 exit_notify() 向父进程发送信号,给子进程重新找养父,养父为线程组中的其他线程或者init进程,并把进程状态(task_strcut 结构中的exit_state)设置成 EXIT_ZOMBIE。
- 调用 schedule() 切换到新的进程。由于处于 EXIT_ZOMBIE 状态的进程不会再会被调度,所以这是进程所执行的最后一段代码。do_exit 永不返回。
到此,与进程相关的所有资源该释放的都释放掉了(假设该进程是这些资源的唯一使用者)。进程不可运行(实际上它也没有地址空间可供它运行)并处于EXIT_ZOMBIE 退出状态。
该进程目前所占用的内存资源就是内核栈、thread_info 结构和 task_struct 结构。此时进程存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者通知内核它不关心那些无关的信息后,子进程的这些剩余资源才被释放归还给系统。
进程描述符的删除
从上面可以知道,进程在调用 do_exit() 后,进程处于僵死状态且不能运行。但是系统还保留它的进程描述符相关信息。之所以保留这些信息是为了让系统有办法在子进程终结后仍能获得它的信息。
当父进程获取已终结的子进程的信息后,或者通知内核它不关心那些无关的信息后,子进程的这些剩余资源才被释放归还给系统。
wait() 这一族函数都是通过唯一的一条系统调用 wait4() 来实现的。它的作用就是挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回该子进程的 PID。另外,调用该函数时提供的指针会包含子函数退出时的退出代码。
wait4() 最终会调用 sys_wait4()。
asmlinkage long sys_wait4(pid_t pid, int __user *stat_addr,
int options, struct rusage __user *ru)
{
long ret;
if (options & ~(WNOHANG|WUNTRACED|WCONTINUED|
__WNOTHREAD|__WCLONE|__WALL))
return -EINVAL;
ret = do_wait(pid, options | WEXITED, NULL, stat_addr, ru);
/* avoid REGPARM breakage on x86: */
prevent_tail_call(ret);
return ret;
}
当父进程因子进程在 exit() 中向其发送信号而被唤醒,父进程在将子进程在用户空间运行的时间和系统空间运行的时间两项统计数据合并入其自身的统计数据中,然后,在典型的条件下,就会调用 release_task() 将子进程残存的资源,就是其 task_struct 结构和系统空间堆栈,全部释放掉。
调用过程如下:
sys_wait4
--> do_wait
--> wait_task_zombie
--> release_task
release_task() 实现如下:
void release_task(struct task_struct * p)
{
struct task_struct *leader;
int zap_leader;
repeat:
...
/* 1)该函数调用_unhash_process(),后者调用detach_pid()从pidhash
• 上删除该进程,同时也要从任务列表中删除该进程
• 2)释放目前僵死进程所使用的所有剩余资源,并进行最终统计和记录
*/
__exit_signal(p);
/*
• If we are the last non-leader member of the thread
• group, and the leader is zombie, then notify the
• group leader's parent process. (if it wants notification.)
*/
zap_leader = 0;
leader = p->group_leader;
/*若该进程是线程组最后一个进程,并且领头进程已经死掉,,则通知僵死的领头进程的父进程 /
if (leader != p && thread_group_empty(leader) && leader->exit_state == EXIT_ZOMBIE) {
BUG_ON(leader->exit_signal == -1);
do_notify_parent(leader, leader->exit_signal);
/
• If we were the last child thread and the leader has
• exited already, and the leader's parent ignores SIGCHLD,
• then we are the one who should release the leader.
•
• do_notify_parent() will have marked it self-reaping in
• that case.
*/
zap_leader = (leader->exit_signal == -1);
}
write_unlock_irq(&tasklist_lock);
release_thread(p);
//调用 put_task_struct 释放进程内核栈和thread_info结构所占的页,并释放task_struct 所占的slab告诉缓存。
call_rcu(&p->rcu, delayed_put_task_struct);
p = leader;
if (unlikely(zap_leader))
goto repeat;
}
release_task 完成的工作如下:
- 调用__exit_signal(),该函数调用_unhash_process(),后者调用detach_pid() 从 pidhash 上删除该进程,同时也要从任务列表中删除该进程。
- __exit_signal() 释放目前僵死进程所使用的所有剩余资源,并进行最终统计和记录。
- 若该进程是线程组最后一个进程,并且领头进程已经死掉,则通知僵死的领头进程的父进程 。
- 调用 put_task_struct() 释放进程内核栈和 thread_info 结构所占的页,并释放 task_struct 所占的 slab 告诉缓存。
到此,进程描述符和进程所有独享的资源全部就释放掉了。