作者:熊轶翔@熊仙僧,中国科学院软件研究所智能软件研究中心
在之间的章节我们了解了进程的创建和执行,在进程执行完毕或者发生一些意料之外的事之后难免会被结束,因此Linux提供了exit()
的系统调用来完成进程结束以及释放占用资源的功能。其中有两种系统调用能用来完成结束进程或者线程的功能。
sys_exit()以及sys_exit_group()
sys_exit()
可以用来结束单个进程或线程,sys_exit_group()
可以用来结束一个线程组,均需要传入一个错误码(error_code)来执行。 声明如下:(路径:/kernel-4.19/includelinuxsyscalls.h)
asmlinkage long sys_exit(int error_code);
asmlinkage long sys_exit_group(int error_code);
实现如下:(路径:/kernel-4.19/kernel/exit.c)
SYSCALL_DEFINE1(exit, int, error_code)
{
do_exit((error_code&0xff)<<8);
}
...
SYSCALL_DEFINE1(exit_group, int, error_code)
{
do_group_exit((error_code & 0xff) << 8);
/* NOTREACHED */
return 0;
}
从以上可以看出,系统调用sys_exit()
是调用了do_exit()
函数,系统调用sys_exit_group()
是调用了do_group_exit()
函数。
接下来再分别看看do_exit()
以及do_group_exit()
函数的实现过程。
do_exit()
do_exit()
的作用是结束当前的进程,部分代码如下[1]:(路径:/kernel-4.19/kernel/exit.c)
void __noreturn do_exit(long code)
{
struct task_struct *tsk = current; //当前进程
...
/*
*检查进程的blk_plug是否为空
*保证task_struct中的plug字段是空的,或者plug字段指向的队列是空的。
*/
WARN_ON(blk_needs_flush_plug(tsk));
//中断上下文不能执行do_exit函数, 也不能终止PID为0的进程。
if (unlikely(in_interrupt()))
panic("Aiee, killing interrupt handler!");
if (unlikely(!tsk->pid))
panic("Attempted to kill the idle task!");
//设定进程可以使用的虚拟地址的上限(用户空间)
set_fs(USER_DS);
...
/*
*首先是检查PF_EXITING标识, 标识表示进程正在退出,如果此标识已被设置,则进一步设置PF_EXITPIDONE标识,并将进程的状态设置为不可中断状态TASK_UNINTERRUPTIBLE,并进行一次进程调度。
*/
if (unlikely(tsk->flags & PF_EXITING)) {
pr_alert("Fixing recursive fault but reboot is needed!n");
tsk->flags |= PF_EXITPIDONE;
set_current_state(TASK_UNINTERRUPTIBLE);
schedule();
}
//如果此PF_EXITING标识未被设置, 则通过exit_signals来设置
exit_signals(tsk); /* sets PF_EXITING */
/*内存屏障,用于确保在它之后的操作开始执行之前,它之前的操作已经完成*/
smp_mb();
/* 一直等待,直到获得current->pi_lock自旋锁 */
raw_spin_lock_irq(&tsk->pi_lock);
raw_spin_unlock_irq(&tsk->pi_lock);
...
/* 释放线性区描述符和页表 */
exit_mm();
/* 输出进程审计信息 */
if (group_dead)
acct_process();
trace_sched_process_exit(tsk);
/* 释放用户空间的“信号量” */
exit_sem(tsk);
/* 释放锁 */
exit_shm(tsk);
/* 释放文件对象相关资源 */
exit_files(tsk);
exit_fs(tsk);
/* 脱离控制终端 */
if (group_dead)
disassociate_ctty(1);
/* 释放命名空间 */
exit_task_namespaces(tsk);
exit_task_work(tsk);
/* 释放task_struct中的thread_struct结构 */
...
/* 更新所有子进程的父进程 */
exit_notify(tsk, group_dead);
/* 进程事件连接器(通过它来报告进程fork、exec、exit以及进程用户ID与组ID的变化) */
proc_exit_connector(tsk);
mpol_put_task_policy(tsk);
...
/* 释放struct io_context结构体所占用的内存 */
if (tsk->io_context)
exit_io_context(tsk);
/* 释放与进程描述符splice_pipe字段相关的资源 */
if (tsk->splice_pipe)
free_pipe_info(tsk->splice_pipe);
...
/* 检查有多少未使用的进程内核栈 */
check_stack_usage();
...
do_task_dead();
}
在最后的do_task_dead()
中调用了__schedule()
切换到了其他就绪进程,至此该进程在操作系统中的生命周期就完全结束了。
do_group_exit()
我们如果了解Linux的线程实现机制的话,会知道所有的线程是属于一个线程组的。即使不是线程, Linux也允许多个进程组成进程组,多个进程组组成一个会话,因此我们了解到不管是多线程还是进程组,其本质都是多个进程组成的一个集合,那么我们的应用程序在退出的时候,自然希望一次性的退出组内所有的进程,do_group_exit()
也就应运而生了[2]。
do_group_exit()
函数将会杀死属于当前线程组的所有线程,它具体执行的是下述操作[2]:
- 检查当前进程的
SIGNAL_GROUP_EXIT
标志是否不为0,如果不为0,说明内核已经开始为线性组执行退出的过程。在这种情况下,就把存放在current
->signal
->group_exit_code
的值当作退出码,然后跳转到第4步。 - 否则,设置进程的
SIGNAL_GROUP_EXIT
标志并把终止代号放到current
->signal
->group_exit_code
字段。 - 调用
zap_other_threads()
函数杀死current
线程组中的其它进程。为了完成这个步骤,函数扫描与current
->tgid
对应的PIDTYPE_TGID
类型的散列表中的PID
链表,向表中所有不同于current
的进程发送SIGKILL
信号,所有这样的进程都将执行do_exit()
函数,从而被结束。 - 调用
do_exit()
函数,把进程的终止代码传递给它。正如我们将在之前看到的,do_exit()
结束进程而且不再返回。
do_group_exit()
的部分代码如下:(路径:/kernel-4.19/kernel/exit.c)
void do_group_exit(int exit_code)
{
struct signal_struct *sig = current->signal;
...
/*
检查current->sig->flags的SIGNAL_GROUP_EXIT标志是否置位
或者current->sig->group_exit_task是否不为NULL
*/
if (signal_group_exit(sig))
/* group_exit_code存放的是线程组终止代码 */
exit_code = sig->group_exit_code;
/* 检查线程组链表是否不为空 */
else if (!thread_group_empty(current)) {
struct sighand_struct *const sighand = current->sighand;
spin_lock_irq(&sighand->siglock);
if (signal_group_exit(sig))
/* Another thread got here before we took the lock. */
exit_code = sig->group_exit_code;
else {
sig->group_exit_code = exit_code;
sig->flags = SIGNAL_GROUP_EXIT;
zap_other_threads(current);
}
spin_unlock_irq(&sighand->siglock);
}
/* 调用do_exit() */
do_exit(exit_code);
}
在上述的函数中,清除了当前进程所占用或者与它相关的数据结构,最后执行的一步都将是schedule()
函数,在这个函数中就将跳转到其他的进程。由于当前进程的各种相关的数据结构都不再存在,因此永远也不会再跳转到这个进程来执行,也就是进程被永远宣告了死亡。
总结
在本章我们了解了进程结束的过程,分别学习了do_exit()
和do_group_exit()
函数,简而言之,他们的作用都是清除关于进程或进程组所占据的资源并开启调度。关于进程的生命周期(创建、执行、结束)的讲解到这里就告一段落,在下一章我们将开始对进程地址空间的学习。
参考:
[1] https:// blog.csdn.net/tjcwt2011 /article/details/80272127
[2] https://www. cnblogs.com/linhaostudy /p/9662144.html
[3]《深入Linux内核架构》