Exit
Linux 进程的退出
linux 下进程退出的方式
正常退出
- 从 main 函数返回 return
- 调用 exit
- 调用_exit
异常退出
- 调用 abort
- 由信号终止
_exit, exit 和_Exit 的区别和联系
_exit 是 linux 系统调用,关闭所有文件描述符,然后退出进程。
exit 是 c 语言的库函数,他最终调用_exit。在此之前,先清洗标准输出的缓存,调用用 atexit 注册的函数等, 在 c 语言的 main 函数中调用 return 就等价于调用 exit。
_Exit 是 c 语言的库函数,自 c99 后加入,等价于_exit,即可以认为它直接调用_Exit。
基本来说,_Exit(或 _exit,建议使用大写版本)是为 fork 之后的子进程准备的特殊 API。功能见POSIX 标准:_Exit,讨论见 c - how to exit a child process
由 fork()函数创建的子进程分支里,正常情况下使用函数 exit()是不正确的,这是因为使用它会导致标准输入输出的缓冲区被清空两次,而且临时文件可能被意外删除。”
因为在 fork 之后,exec 之前,很多资源还是共享的(如某些文件描述符),如果使用 exit 会关闭这些资源,导致某些非预期的副作用(如删除临时文件等)。
「刷新」是对应 flush,意思是把内容从内存缓存写出到文件里,而不仅仅是清空(所以常见的对 stdin 调用 flush 的方法是耍流氓而已)。如果在 fork 的时候父进程内存有缓冲内容,则这个缓冲会带到子进程,并且两个进程会分别 flush (写出)一次,造成数据重复。参见c - How does fork() work with buffered streams like stdout?
进程退出的系统调用
_exit 和 exit_group 系统调用
_exit 系统调用
进程退出由 exit 系统调用来完成, 这使得内核有机会将该进程所使用的资源释放回系统中
进程终止时,一般是调用 exit 库函数(无论是程序员显式调用还是编译器自动地把 exit 库函数插入到 main 函数的最后一条语句之后)来释放进程所拥有的资源。
exit 系统调用的入口点是 sys_exit()函数, 需要一个错误码作为参数, 以便退出进程。
其定义是体系结构无关的, 见 kernel/exit.c
而我们用户空间的多线程应用程序, 对应内核中就有多个进程, 这些进程共享虚拟地址空间和资源, 他们有各自的进程 id(pid), 但是他们的组进程 id(tpid)是相同的, 都等于组长(领头进程)的 pid
在 linux 内核中对线程并没有做特殊的处理,还是由 task_struct 来管理。所以从内核的角度看, 用户态的线程本质上还是一个进程。对于同一个进程(用户态角度)中不同的线程其 tgid 是相同的,但是 pid 各不相同。 主线程即 group_leader(主线程会创建其他所有的子线程)。如果是单线程进程(用户态角度),它的 pid 等于 tgid。
这个信息我们已经讨论过很多次了
参见
为什么还需要 exit_group
我们如果了解 linux 的线程实现机制的话, 会知道所有的线程是属于一个线程组的, 同时即使不是线程, linux 也允许多个进程组成进程组, 多个进程组组成一个会话, 因此我们本质上了解到不管是多线程, 还是进程组起本质都是多个进程组成的一个集合, 那么我们的应用程序在退出的时候, 自然希望一次性的退出组内所有的进程。
因此 exit_group 就诞生了
group_exit 函数会杀死属于当前进程所在线程组的所有进程。它接受进程终止代号作为参数,进程终止代号可能是系统调用 exit_group(正常结束)指定的一个值,也可能是内核提供的一个错误码(异常结束)。
因此 C 语言的库函数 exit 使用系统调用 exit_group 来终止整个线程组,库函数 pthread_exit 使用系统调用_exit 来终止某一个线程
_exit 和 exit_group 这两个系统调用在 Linux 内核中的入口点函数分别为 sys_exit 和 sys_exit_group。
系统调用声明
声明见include/linux/syscalls.h, line 326
|
|
系统调用号
其系统调用号是一个体系结构相关的定义, 但是多数体系结构的定义如下, 在include/uapi/asm-generic/unistd.h, line 294文件中
|
|
只有少数体系结构, 重新定义了系统调用号
| 体系 | 定义 |
|---|---|
| xtensa | rch/xtensa/include/uapi/asm/unistd.h, line 267 |
| arm64 | rch/arm64/include/asm/unistd32.h, line 27 |
| 通用 | include/uapi/asm-generic/unistd.h, line 294 |
系统调用实现
然后系统调用的实现在kernel/exit.c 中
|
|
do_exit_group 流程
do_group_exit()函数杀死属于 current 线程组的所有进程。它接受进程终止代码作为参数,进程终止代号可能是系统调用 exit_group()指定的一个值,也可能是内核提供的一个错误代号。 该函数执行下述操作
- 检查退出进程的 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_exit 流程
进程终止所要完成的任务都是由 do_exit 函数来处理。
该函数定义在kernel/exit.c中
触发 task_exit_nb 通知链实例的处理函数
|
|
该函数会定义在触发kernel/profile.c
|
|
会触发 task_exit_notifier 通知, 从而触发对应的处理函数
其中 task_exit_notifier 被定义如下
|
|
检查进程的 blk_plug 是否为空
保证 task_struct 中的 plug 字段是空的,或者 plug 字段指向的队列是空的。plug 字段的意义是 stack plugging
|
|
其中 blk_needs_flush_plug 函数定义在include/linux/blkdev.h, 如下
|
|
OOPS 消息
中断上下文不能执行 do_exit 函数, 也不能终止 PID 为 0 的进程。
|
|
设定进程可以使用的虚拟地址的上限(用户空间)
|
|
其定义在 arch/对应体系/include/asm/uaccess.h 中
| 体系 | 定义 |
|---|---|
| arm | arch/arm/include/asm/uaccess.h, line 99 |
| arm64 | arch/arm64/include/asm/uaccess.h, line 66 |
| x86 | arch/x86/include/asm/uaccess.h, line 32 |
| 通用 | include/asm-generic/uaccess.h, line 28 |
arm64 的定义如下
|
|
检查进病设置进程程 PF_EXITING
首先是检查 PF_EXITING 标识, 此标识表示进程正在退出,
如果此标识已被设置, 则进一步设置 PF_EXITPIDONE 标识, 并将进程的状态设置为不可中断状态 TASK_UNINTERRUPTIBLE, 并进程一次进程调度
|
|
如果此标识未被设置, 则通过 exit_signals 来设置
|
|
内存屏障
|
|
同步进程的 mm 的 rss_stat
|
|
获取 current->mm->rss_stat.count[member]计数
|
|
函数的实现如下, 参见 http://lxr.free-electrons.com/source/kernel/tsacct.c?v=4.6#L156
|
|
其中 task_cputime 获取了进程的 cpu 时间 __acct_update_integr 定义如下
参照http://lxr.free-electrons.com/source/kernel/tsacct.c#L125
|
|
清除定时器
|
|
收集进程会计信息
|
|
审计
|
|
释放进程占用的资源
释放线性区描述符和页表
|
|
输出进程会计信息
|
|
释放用户空间的“信号量”
|
|
遍历 current->sysvsem.undo_list 链表,并清除进程所涉及的每个 IPC 信号量的操作痕迹
释放锁
|
|
释放文件对象相关资源
|
|
释放 struct fs_struct 结构体
|
|
脱离控制终端
|
|
释放命名空间
|
|
释放 task_struct 中的 thread_struct 结构
|
|
触发 thread_notify_head 链表中所有通知链实例的处理函数,用于处理 struct thread_info 结构体
Performance Event 功能相关资源的释放
|
|
Performance Event 功能相关资源的释放
|
|
注销断点
|
|
更新所有子进程的父进程
|
|
进程事件连接器(通过它来报告进程 fork、exec、exit 以及进程用户 ID 与组 ID 的变化)
|
|
用于 NUMA,当引用计数为 0 时,释放 struct mempolicy 结构体所占用的内存
|
|
释放 struct futex_pi_state 结构体所占用的内存
|
|
释放 struct io_context 结构体所占用的内存
|
|
释放与进程描述符 splice_pipe 字段相关的资源
|
|
检查有多少未使用的进程内核栈
|
|
调度其它进程
|
|
在设置了进程状态为 TASK_DEAD 后, 进程进入僵死状态, 进程已经无法被再次调度, 因为对应用程序或者用户空间来说此进程已经死了, 但是尽管进程已经不能再被调度,但系统还是保留了它的进程描述符,这样做是为了让系统有办法在进程终止后仍能获得它的信息。
在父进程获得已终止子进程的信息后,子进程的 task_struct 结构体才被释放(包括此进程的内核栈)。
-
No backlinks found.