Linux-进程的管理与调度14(基于6.1内核)---Linux进程退出
一、Linux进程的退出
1.1 linux下进程退出的方式
| 出方式 | 说明 | 退出状态码 | 是否清理资源 | 示例代码 |
|---|---|---|---|---|
| 正常退出 | 程序调用 exit()、return 或正常结束 | 返回值 0 或非 0 | 清理资源 | exit(0)、return 0 |
| 异常退出 | 程序因信号(如 SIGSEGV、SIGFPE 等)或调用 abort() 强制退出 | 非零状态码 | 未必完全清理资源 | abort()、访问非法内存(如 SIGSEGV) |
1.2 _exit, exit和_Exit的区别和联系
| 特性 | exit | _exit | _Exit |
|---|---|---|---|
| 定义位置 | <stdlib.h> | <unistd.h> | <stdlib.h> |
| 执行操作 | 执行缓冲区刷新,调用 atexit() 注册的函数,执行文件清理等 | 不执行缓冲区刷新,不调用 atexit() 注册的函数,直接退出 | 不执行缓冲区刷新,不调用 atexit() 注册的函数,直接退出 |
| 返回值 | 返回退出状态码,通常由 exit() 提供给操作系统 | 返回退出状态码,直接返回给操作系统 | 返回退出状态码,直接返回给操作系统 |
| 使用场景 | 需要清理资源和执行退出处理的情况 | 在 fork() 后的子进程中直接退出,或者在多线程程序中避免资源清理 | 用于现代 C11 代码,表达清晰的终止,替代 _exit |
二、进程退出的系统调用
2.1 _exit和exit_group系统调用
_exit系统调用
进程退出由_exit系统调用来完成,这使得内核有机会将该进程所使用的资源释放回系统中
进程终止时,一般是调用exit库函数来释放进程所拥有的资源。
_exit系统调用的入口点是sys_exit()函数,需要一个错误码作为参数,以便退出进程。
其定义是体系结构无关的, 见kernel/exit.c
用户空间的多线程应用程序(用户空间线程!!!),对应内核中就有多个进程, 这些进程共享虚拟地址空间和资源,有各自的进程id(pid),但是组进程id(tpid)是相同的, 都等于组长(领头进程)的pid
为什么还需要exit_group
我们如果了解linux的线程实现机制的话,会知道所有的线程是属于一个线程组的,同时即使不是线程,linux也允许多个进程组成进程组,多个进程组组成一个会话,因此我们本质上了解到不管是多线程,还是进程组起本质都是多个进程组成的一个集合,那么我们的应用程序在退出的时候,自然希望一次性的退出组内所有的进程。
因此exit_group就诞生了
exit_group函数会杀死属于当前进程所在线程组的所有进程。它接受进程终止代号作为参数,进程终止代号可能是系统调用exit_group(正常结束)指定的一个值,也可能是内核提供的一个错误码(异常结束)。
因此C语言的库函数exit使用系统调用exit_group来终止整个线程组,库函数pthread_exit使用系统调用_exit来终止某一个线程
_exit和exit_group这两个系统调用在Linux内核中的入口点函数分别为sys_exit和sys_exit_group。
2.2 系统调用声明
include/linux/syscalls.h
asmlinkage long sys_exit(int error_code);
asmlinkage long sys_exit_group(int error_code);
asmlinkage long sys_wait4(pid_t pid, int __user *stat_addr,
int options, struct rusage __user *ru);
asmlinkage long sys_waitid(int which, pid_t pid,
struct siginfo __user *infop,
int options, struct rusage __user *ru);
asmlinkage long sys_waitpid(pid_t pid, int __user *stat_addr, int options);
2.3 系统调用号
其系统调用号是一个体系结构相关的定义, 但是多数体系结构的定义如下, 在tools/include/uapi/asm-generic/unistd.h
/* kernel/exit.c */
#define __NR_exit 93
__SYSCALL(__NR_exit, sys_exit)
#define __NR_exit_group 94
__SYSCALL(__NR_exit_group, sys_exit_group)
#define __NR_waitid 95
__SC_COMP(__NR_waitid, sys_waitid, compat_sys_waitid)
2.4 系统调用实现
kernel/exit.c
SYSCALL_DEFINE1(exit, int, error_code)
{
do_exit((error_code&0xff)<<8);
}
/*
* this kills every thread in the thread group. Note that any externally
* wait4()-ing process will get the correct exit code - even if this
* thread is not the thread group leader.
*/
SYSCALL_DEFINE1(exit_group, int, error_code)
{
do_group_exit((error_code & 0xff) << 8);
/* NOTREACHED */
return 0;
}
三、do_group_exist流程
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的进程发送SIGKILL信号,结果,所有这样的进程都将执行do_exit()函数,从而被杀死。
遍历线程所在线程组的所有线程函数while_each_thread(p, t)使用了:
static inline struct task_struct *next_thread(const struct task_struct *p)
{
return list_entry_rcu(p->thread_group.next,
struct task_struct, thread_group);
}
#define while_each_thread(g, t) \
while ((t = next_thread(t)) != g)
同一个进程组的可以, 扫描与current->pids[PIDTYPE_PGID](这是进程组组长pid结构体)对应的PIDTYPE_PGID类型的散列表(因为是进程组组长,所以其真实的pid结构体中tasks[PIDTYPE_PGID]是这个散列表的表头)中的每个PID链表
调用do_exit()函数,把进程的终止代码传递给它。正如我们将在下面看到的,do_exit()杀死进程而且不再返回。
/*
* Take down every thread in the group. This is called by fatal signals
* as well as by sys_exit_group (below).
*/
void __noreturn
do_group_exit(int exit_code)
{
struct signal_struct *sig = current->signal;
if (sig->flags & SIGNAL_GROUP_EXIT)
exit_code = sig->group_exit_code;
else if (sig->group_exec_task)
exit_code = 0;
else {
struct sighand_struct *const sighand = current->sighand;
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 if (sig->group_exec_task)
exit_code = 0;
else {
sig->group_exit_code = exit_code;
sig->flags = SIGNAL_GROUP_EXIT;
zap_other_threads(current);
}
spin_unlock_irq(&sighand->siglock);
}
do_exit(exit_code);
/* NOTREACHED */
}
四、 do_exit流程
进程终止所要完成的任务都是由do_exit函数来处理。kernel/exit.c
void __noreturn do_exit(long code)
{
......
}
EXPORT_SYMBOL_GPL(do_exit);
4.1 触发synchronize_group_exit通知链实例的处理函数
kernel/exit.c
static void synchronize_group_exit(struct task_struct *tsk, long code)
{
struct sighand_struct *sighand = tsk->sighand;
struct signal_struct *signal = tsk->signal;
spin_lock_irq(&sighand->siglock);
signal->quick_threads--;
if ((signal->quick_threads == 0) &&
!(signal->flags & SIGNAL_GROUP_EXIT)) {
signal->flags = SIGNAL_GROUP_EXIT;
signal->group_exit_code = code;
signal->group_stop_count = 0;
}
spin_unlock_irq(&sighand->siglock);
}
4.2 检查进程的blk_plug是否为空
保证task_struct中的plug字段是空的,或者plug字段指向的队列是空的。plug字段的意义是stack plugging
WARN_ON(tsk->plug);
其中__blk_flush_plug函数定义block/blk-core.c
void __blk_flush_plug(struct blk_plug *plug, bool from_schedule)
{
if (!list_empty(&plug->cb_list))
flush_plug_callbacks(plug, from_schedule);
blk_mq_flush_plug_list(plug, from_schedule);
/*
* Unconditionally flush out cached requests, even if the unplug
* event came from schedule. Since we know hold references to the
* queue for cached requests, we don't want a blocked task holding
* up a queue freeze/quiesce event.
*/
if (unlikely(!rq_list_empty(plug->cached_rq)))
blk_mq_free_plug_rqs(plug);
}
4.3 检查进程设置进程PF_EXITING
通过exit_signals来设置
exit_signals(tsk); /* sets PF_EXITING */
4.4 内存屏障
/*
* tsk->flags are checked in the futex code to protect against
* an exiting task cleaning up the robust pi futexes.
*/
/* 内存屏障,用于确保在它之后的操作开始执行之前,它之前的操作已经完成 */
smp_mb();
/* 一直等待,直到获得current->pi_lock自旋锁 */
raw_spin_unlock_wait(&tsk->pi_lock);
4.5 同步进程的mm的rss_stat
/* sync mm's RSS info before statistics gathering */
if (tsk->mm)
sync_mm_rss(tsk->mm);
4.6 获取current->mm->rss_stat.count[member]计数
/*
cct_update_integrals - update mm integral fields in task_struct
更新进程的运行时间, 获取current->mm->rss_stat.count[member]计数
http://lxr.free-electrons.com/source/kernel/tsacct.c?v=4.6#L152
*/
acct_update_integrals(tsk);
函数的实现如下kernel/tsacct.c
/**
* acct_update_integrals - update mm integral fields in task_struct
* @tsk: task_struct for accounting
*/
void acct_update_integrals(struct task_struct *tsk)
{
u64 utime, stime;
unsigned long flags;
local_irq_save(flags);
task_cputime(tsk, &utime, &stime);
__acct_update_integrals(tsk, utime, stime);
local_irq_restore(flags);
}
其中task_cputime获取了进程的cpu时间__acct_update_integr定义kernel/tsacct.c
static void __acct_update_integrals(struct task_struct *tsk,
u64 utime, u64 stime)
{
u64 time, delta;
if (!likely(tsk->mm))
return;
time = stime + utime;
delta = time - tsk->acct_timexpd;
if (delta < TICK_NSEC)
return;
tsk->acct_timexpd = time;
/*
* Divide by 1024 to avoid overflow, and to avoid division.
* The final unit reported to userspace is Mbyte-usecs,
* the rest of the math is done in xacct_add_tsk.
*/
tsk->acct_rss_mem1 += delta * get_mm_rss(tsk->mm) >> 10;
tsk->acct_vm_mem1 += delta * READ_ONCE(tsk->mm->total_vm) >> 10;
}
4.7 清除定时器
继续:kernel/exit.c
group_dead = atomic_dec_and_test(&tsk->signal->live);
if (group_dead) {
/*
* If the last thread of global init has exited, panic
* immediately to get a useable coredump.
*/
if (unlikely(is_global_init(tsk)))
panic("Attempted to kill init! exitcode=0x%08x\n",
tsk->signal->group_exit_code ?: (int)code);
#ifdef CONFIG_POSIX_TIMERS
hrtimer_cancel(&tsk->signal->real_timer);
exit_itimers(tsk);
#endif
if (tsk->mm)
setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm);
}
4.8 收集进程会计信息
继续:kernel/exit.c
acct_collect(code, group_dead);
4.9 审计
继续:kernel/exit.c
if (group_dead)
tty_audit_exit(); //记录审计事件
audit_free(tsk); // 释放struct audit_context结构体
4.10 释放进程占用的资源
释放线性区描述符和页表
继续:kernel/exit.c
/* 释放存储空间
放弃进程占用的mm,如果没有其他进程使用该mm,则释放它。
*/
exit_mm(tsk);
输出进程会计信息
if (group_dead)
acct_process();
trace_sched_process_exit(tsk);
释放用户空间的“信号量”
exit_sem(tsk); /* 释放用户空间的“信号量” */
遍历current->sysvsem.undo_list链表,并清除进程所涉及的每个IPC信号量的操作痕迹
释放锁
exit_shm(tsk); /* 释放锁 */
释放文件对象相关资源
exit_files(tsk); /* 释放已经打开的文件 */
释放struct fs_struct结构体
exit_fs(tsk); /* 释放用于表示工作目录等结构 */
脱离控制终端
if (group_dead)
disassociate_ctty(1);
释放命名空间
exit_task_namespaces(tsk); /* 释放命名空间 */
exit_task_work(tsk);
释放task_struct中的thread_struct结构
exit_thread();
触发thread_notify_head链表中所有通知链实例的处理函数,用于处理struct thread_info结构体
Performance Event功能相关资源的释放
perf_event_exit_task(tsk);
Performance Event功能相关资源的释放
cgroup_exit(tsk);
注销断点
/*
* FIXME: do that only when needed, using sched_exit tracepoint
*/
flush_ptrace_hw_breakpoint(tsk);
更新所有子进程的父进程
exit_notify(tsk, group_dead);
进程事件连接器(通过它来报告进程fork、exec、exit以及进程用户ID与组ID的变化)
proc_exit_connector(tsk);
用于NUMA,当引用计数为0时,释放struct mempolicy结构体所占用的内存
#ifdef CONFIG_NUMA
task_lock(tsk);
mpol_put(tsk->mempolicy);
tsk->mempolicy = NULL;
task_unlock(tsk);
#endif
释放struct futex_pi_state结构体所占用的内存
#ifdef CONFIG_FUTEX
if (unlikely(current->pi_state_cache))
kfree(current->pi_state_cache);
#endif
释放struct io_context结构体所占用的内存
if (tsk->io_context)
exit_io_context(tsk);
释放与进程描述符splice_pipe字段相关的资源
if (tsk->splice_pipe)
free_pipe_info(tsk->splice_pipe);
if (tsk->task_frag.page)
put_page(tsk->task_frag.page);
4.11 检查有多少未使用的进程内核栈
check_stack_usage();
4.12 抢占和任务更新
preempt_disable();
if (tsk->nr_dirtied)
__this_cpu_add(dirty_throttle_leaks, tsk->nr_dirtied);
- 禁用抢占:首先,使用
preempt_disable()禁用当前 CPU 的抢占,以确保后续代码段在执行时不被中断。 - 检查脏页:接下来,检查当前进程(
tsk)是否有脏页(即tsk->nr_dirtied是否大于零)。如果当前进程有脏页,执行下一步。 - 更新脏页统计:如果当前进程有脏页,则将该进程修改的脏页数(
tsk->nr_dirtied)累加到dirty_throttle_leaks变量中。这个变量是一个 每个 CPU 独立的变量,它用于记录所有任务中已修改的脏页数。
703

被折叠的 条评论
为什么被折叠?



