[TOC]

kernel/exit.c 进程终结与资源回收(Process Termination and Resource Reclamation)

历史与背景

这项技术是为了解决什么特定问题而诞生的?

kernel/exit.c 内的代码是为了解决操作系统中最基本和最核心的问题之一:如何安全、彻底地终结一个正在运行的进程,并确保其占用的所有系统资源都被完全回收

一个进程在运行时会占用各种系统资源,包括:

  • 内存:进程地址空间(代码、数据、堆、栈)、页表。
  • 文件描述符:打开的文件、套接字、管道等。
  • CPU时间:进程作为被调度的实体。
  • 内核数据结构:如 task_struct、信号处理器、定时器等。
  • 子进程关系:作为其他进程的父进程。

如果没有一个健壮、集中的退出机制,当进程结束时:

  • 资源泄漏:内存、文件句柄等资源将无法被释放,随着时间推移会耗尽系统资源,导致系统崩溃。
  • 产生僵尸进程:父进程需要一种机制来获知其子进程的退出状态。如果子进程直接消失,父进程将无法进行后续处理。
  • 孤儿进程问题:如果一个父进程先于其子进程退出,这些子进程将成为“孤儿”,必须有一个机制来“收养”它们,否则它们将永远无法被清理。

exit.c 就是为了提供一个标准化的、强制执行的流程来解决上述所有问题,确保每个进程的生命周期都能得到一个干净的了结。

它的发展经历了哪些重要的里程碑或版本迭代?

进程退出机制是类Unix系统的基石,其基本思想(exit()/wait())从一开始就存在。Linux中的演进主要体现在对更复杂场景的支持上:

  • 线程的引入:Linux早期将线程实现为共享特定资源的轻量级进程。这使得进程退出变得复杂。一个线程退出不应该影响整个进程。因此,内核引入了**线程组(Thread Group)**的概念。exit() 系统调用只结束当前线程,而新的 exit_group() 系统调用用于结束整个线程组(即用户眼中的“进程”)。现在,C库中的 exit() 函数实际上调用的是 exit_group() 系统调用。
  • 僵尸进程与孤儿进程处理的完善:为了更好地处理孤儿进程,引入了**子进程收割者(Sub-reaper)**的概念。一个进程可以通过 prctl(PR_SET_CHILD_SUBREAPER) 将自己设置为一个局部的“init进程”,负责收养其后代中的孤儿进程。这在容器和复杂的守护进程管理中非常有用。
  • 与新内核特性的集成:随着内核功能的发展,进程退出流程需要与越来越多的子系统进行交互。例如,当一个进程退出时,必须清理其所属的控制组(cgroups)资源、解除其所处的命名空间(Namespaces)引用、审计(Audit)其退出事件等。do_exit() 函数也因此不断扩展,增加了对这些新特性的清理调用。

目前该技术的社区活跃度和主流应用情况如何?

kernel/exit.c 是Linux内核中最稳定、最核心的部分。它不是一个会频繁引入新功能的领域,但由于其中心地位,任何对内核进程模型、资源管理的修改都可能需要触及此处的代码。因此,它的维护和更新是持续的,主要是为了提升健壮性、修复边界情况的bug以及与新内核特性集成。它是所有Linux系统运行的绝对基础,每一个进程的消亡都会执行到这里的代码。

核心原理与设计

它的核心工作原理是什么?

exit.c 的核心是 do_exit()do_group_exit() 函数,它们定义了一个严格的、多阶段的进程退出流程。

核心流程:

  1. 触发:进程退出可以由多种方式触发:

    • 自愿退出:进程调用 exit()exit_group() 系统调用(例如,main函数返回)。
    • 非自愿退出:进程收到一个致命信号(如 SIGSEGV, SIGKILL),其内核处理函数的最后一步就是调用 do_exit()
  2. 进入退出状态

    • 内核首先在进程的 task_struct 中设置 PF_EXITING 标志,表明该进程正处于退出过程中,阻止其他子系统对其进行新的操作。
  3. 资源剥离与释放:这是最关键的一步,do_exit() 会像一个清单一样,调用一系列 exit_*() 辅助函数来释放与该进程关联的所有资源:

    • exit_mm():解除对进程地址空间 (mm_struct) 的使用。对于多线程进程,只有最后一个线程退出时才会真正释放地址空间。
    • exit_files():关闭所有打开的文件描述符。
    • exit_sighand():释放信号处理相关的结构。
    • exit_thread():清理TLS(线程局部存储)等线程私有资源。
    • exit_namespaces():减少对命名空间的引用计数。
    • ……以及其他几十项清理工作。
  4. 处理亲属关系

    • 遣散子进程(Reparenting):内核会遍历该进程的所有子进程,并将它们的父进程设置为当前线程组的“收割者”(通常是init进程/PID 1,或是一个sub-reaper)。这个过程称为“re-parenting”。
    • 设置退出码:将进程的退出状态码保存在 task_struct 中。
  5. 转变为僵尸(Zombie)状态

    • 所有资源都被释放后,进程本身并没有完全消失。它的 task_struct 结构和一小部分内核栈被保留下来。进程状态被设置为 EXIT_ZOMBIE
    • 目的:保留这些信息是为了让其父进程能够通过 wait() 系列系统调用来查询它的退出码和资源使用情况。
  6. 通知父进程

    • 向其(原始的)父进程发送 SIGCHLD 信号,通知父进程“你的一个孩子已经终止,可以来收集它的信息了”。
  7. 最终消亡(release_task()

    • 当父进程调用 wait()waitpid() 并成功收集到这个僵尸进程的信息后,内核的 wait() 实现会调用 release_task()。这个函数负责释放最后剩下的 task_struct 结构和内核栈,至此,该进程才算从系统中被彻底抹除。

它的主要优势体现在哪些方面?

  • 健壮性和可靠性:保证了资源100%被回收,从根本上杜绝了内核级的资源泄漏。
  • 有序性:定义了严格的清理顺序,避免了因错误的释放顺序导致的依赖问题。
  • 进程间通信:僵尸状态和wait()机制是POSIX标准中父子进程间进行状态同步和通信的基础。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 僵尸进程问题:该设计的直接后果是,如果一个父进程编写得有缺陷,从不调用 wait() 来为它的子进程“收尸”,那么这些子进程将永远停留在僵尸状态。虽然僵尸进程不占用内存等主要资源,但它们会占据进程ID(PID),如果大量积累,最终可能导致系统无法创建新进程。这不是exit.c的缺陷,而是对设计的一种权衡。
  • 无法终止D状态进程:如果一个进程陷入了 TASK_UNINTERRUPTIBLE_SLEEP(D状态),通常是等待一个不可中断的I/O操作(如等待有问题的NFS响应)。此时,它无法响应信号,也无法进入退出流程。exit.c对此无能为力,必须等待其等待的事件完成。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

它不是一个“可选”的方案,而是所有进程正常或异常终止时必须经过的唯一路径

  • 任何程序的正常结束:当你运行的bash命令(如 ls, grep)执行完毕,C库会调用 exit_group(),触发kernel/exit.c中的流程。
  • 杀死进程:当你使用 kill 命令发送信号给一个进程时,如果该信号导致进程终止,最终也会执行到 do_exit()
  • 应用崩溃:当一个程序因非法内存访问等原因崩溃时,内核会向其发送SIGSEGV信号,同样会引导其进入exit.c的清理流程。

是否有不推荐使用该技术的场景?为什么?

不存在不推荐使用该技术的场景。唯一可以讨论的是触发进程退出的方式。例如,粗暴地使用 kill -9 (SIGKILL) 是最后的手段。它虽然也会触发kernel/exit.c中完整的内核资源清理,但它会剥夺进程在用户空间进行任何最后清理工作的机会(如保存数据、删除临时文件等)。

对比分析

请将其 与 其他相似技术 进行详细对比。

这里最有意义的对比是进程的正常退出与被**强制杀死(SIGKILL之间的区别,以及内核中线程退出 (exit)进程退出 (exit_group)**的区别。

对比1:正常退出 vs. kill -9

特性 正常退出 (e.g., exit(0), SIGTERM) 强制杀死 (kill -9, SIGKILL)
触发方式 进程自愿调用exit()或响应可捕获的终止信号。 内核直接向目标进程注入一个不可捕获、不可忽略的信号。
用户空间清理 会执行。进程可以捕获SIGTERM等信号,执行自定义的清理逻辑。atexit()注册的函数和C++析构函数会被调用。 完全跳过。进程在用户空间没有任何机会执行任何代码。临时文件、共享内存段等可能不会被优雅地清理。
内核空间清理 完全执行do_exit()会释放所有内核资源(内存、文件等)。 完全执行SIGKILL的效果是强制进程进入do_exit()流程,内核自身的资源回收是同样有保障的。
安全性 安全。允许应用程序保持其状态的一致性。 对应用程序状态是不安全的。可能导致数据损坏或应用级资源泄漏。但对内核是安全的。
使用场景 默认的、推荐的进程终止方式。 作为最后手段,用于杀死无响应(非D状态)的进程。

对比2:exit() vs. exit_group() (内核系统调用)

特性 exit() 系统调用 exit_group() 系统调用
作用范围 单个线程。只终止调用该系统调用的线程。 整个线程组(即进程)。终止属于同一线程组的所有线程。
资源释放 只释放线程私有资源。如果它是进程中最后一个线程,则会触发整个进程的资源释放。 立即开始整个进程的资源释放流程。
C库 exit() C库中的exit()函数直接调用这个。 C库中的exit()函数调用这个系统调用,以确保整个进程退出。
使用场景 pthread_exit()等线程库函数间接使用,或在需要精细控制线程生命周期的场景下使用。 用户空间程序想要终止整个进程时的标准方式。

include/linux/sched/task.h

put_task_struct 删除任务结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static inline void put_task_struct(struct task_struct *t)
{
if (!refcount_dec_and_test(&t->usage))
return;

/*
* 在非实时模式下,调用 __put_task_struct() 总是安全的。
* 在实时模式下,我们只能在可抢占的上下文中调用它。
*/
if (!IS_ENABLED(CONFIG_PREEMPT_RT) || preemptible()) {
static DEFINE_WAIT_OVERRIDE_MAP(put_task_map, LD_WAIT_SLEEP);

lock_map_acquire_try(&put_task_map);
__put_task_struct(t);
lock_map_release(&put_task_map);
return;
}


/*
* 在 PREEMPT_RT 下,我们不能在原子上下文中调用 put_task_struct,
* 因为它会间接获取需要休眠的锁。
*
* call_rcu() 将调度 delayed_put_task_struct_rcu()
* 在进程上下文中被调用。
*
* 当 refcount_dec_and_test(&t->usage) 成功时,
* 会调用 __put_task_struct()。
*
* 这意味着它不会与 put_task_struct_rcu_user() 冲突,
* 后者以相同方式滥用 ->rcu;rcu_users 持有一个引用,
* 因此在 rcu_users 从 1 -> 0 转换后,task->usage 不会为零。
*
* delayed_free_task() 也使用 ->rcu,但它仅在
* 创建进程失败时被调用。因此,它不可能与 put_task_struct() 冲突。
*/
call_rcu(&t->rcu, __put_task_struct_rcu_cb);
}

kernel/exit.c

rcuwait_wake_up Rcu 等待醒来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int rcuwait_wake_up(struct rcuwait *w)
{
int ret = 0;
struct task_struct *task;

rcu_read_lock();

/*
* Order condition vs @task, such that everything prior to the load
* of @task is visible. This is the condition as to why the user called
* rcuwait_wake() in the first place. Pairs with set_current_state()
* barrier (A) in rcuwait_wait_event().
*
* WAIT WAKE
* [S] tsk = current [S] cond = true
* MB (A) MB (B)
* [L] cond [L] tsk
*/
smp_mb(); /* (B) */

task = rcu_dereference(w->task);
if (task)
ret = wake_up_process(task);
rcu_read_unlock();

return ret;
}
EXPORT_SYMBOL_GPL(rcuwait_wake_up);

delayed_put_task_struct 延迟放置任务结构体

  • 这两个函数协同工作,以一种并发安全的方式,实现对task_struct结构体的延迟释放。这套机制是专门为了解决在多核(SMP)环境下,当一个任务死亡后,如何安全地释放其核心数据结构的问题。
  • 其基本原理如下:
    • 问题的根源:task_struct是内核中最核心、最复杂的数据结构之一,它被内核的各个子系统(调度器、信号处理、性能事件、追踪等)频繁引用。当一个任务死亡时,即使它的主体代码已经停止运行,在其他CPU上可能仍然有代码正在读取它的task_struct。如果在此时立即释放task_struct的内存,就会导致其他CPU发生“use-after-free”的严重错误。
      两阶段释放模型:为了解决这个问题,内核采用了一种两阶段的释放模型。
    • 第一阶段:引用计数。内核设计了一个特殊的引用计数器 task->rcu_users。任何需要在RCU保护的读端临界区之外,临时“持有”一个task_struct引用的代码,都必须先增加这个计数器。当它使用完毕后,再减少这个计数器。
    • 第二阶段:RCU延迟销毁。只有当最后一个“临时持有者”减少引用计数,使得rcu_users变为0时,才能启动真正的销毁流程。但销毁也不能立即进行,因为可能还有其他不使用这个引用计数的、纯粹的RCU“读者”存在。因此,它会调用call_rcu,将最终的销毁工作委托给RCU子系统。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static void delayed_put_task_struct(struct rcu_head *rhp)
{
struct task_struct *tsk = container_of(rhp, struct task_struct, rcu);

kprobe_flush_task(tsk);
rethook_flush_task(tsk);
perf_event_delayed_put(tsk);
trace_sched_process_free(tsk);
/*
* 调用put_task_struct,这是对task_struct引用计数的最终减少。
* 在此路径下,这将是最后一个引用,会导致task_struct结构体
* 本身的内存被释放(通常是通过kmem_cache_free)。
*/
put_task_struct(tsk);
}

void put_task_struct_rcu_user(struct task_struct *task)
{
/*
* 原子地将task->rcu_users引用计数减1,并检查结果是否为0。
* refcount_dec_and_test提供了防止计数器下溢的保护。
*/
if (refcount_dec_and_test(&task->rcu_users))
/*
* 如果当前是最后一个引用者,那么就调用call_rcu来启动延迟释放流程。
* 它将delayed_put_task_struct函数注册为回调,
* 这个回调将在未来的一个安全时间点被RCU子系统调用。
*/
call_rcu(&task->rcu, delayed_put_task_struct);
}

coredump_task_exit

  • coredump_task_exit是do_exit流程中的一个同步函数。它只在一种非常特殊的并发场景下被调用:当一个线程正在执行do_exit退出流程时,该线程所属的线程组中的另一个线程,恰好因为致命错误而触发了核心转储(coredump)过程。
    它的核心作用是:让正在退出的线程tsk暂停下来,并将其自身“注册”到coredump的发起者那里,然后进入睡眠,直到coredump过程完成或明确表示不再需要它。 这是一种确保coredump能够获得一个一致性的、完整的进程快照的关键同步机制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/*
* 这是一个静态函数,用于处理一个正在退出的任务与一个并发的
* coredump过程之间的同步。
* @tsk: 指向当前正在退出的任务。
* @core_state: 指向由coredump发起者创建的、用于协调的全局状态结构体。
*/
static void coredump_task_exit(struct task_struct *tsk,
struct core_state *core_state)
{
/* 在当前线程的栈上创建一个临时的core_thread结构体。*/
struct core_thread self;

/* 将自己的task_struct指针存入self中。*/
self.task = tsk;
/*
* 检查自己是否是被信号杀死的(PF_SIGNALED标志)。
* coredump的发起者可能需要获取导致其死亡的信号信息。
*/
if (self.task->flags & PF_SIGNALED)
/*
* 如果是,则通过原子交换(xchg),将自己的self结构体加入到
* dumper维护的链表的头部,并获取之前的链表头。
*/
self.next = xchg(&core_state->dumper.next, &self);
else
/*
* 如果不是被信号杀死的,则coredump过程不需要关于它的详细信息,
* 将self.task置为NULL,作为一个标志。
*/
self.task = NULL;
/*
* 注释:这意味着一个内存屏障(mb()),xchg()的结果必须对
* core_state->dumper可见。
*
* 原理:atomic_dec_and_test是一个完整的内存屏障,它确保了在它之前
* 的所有内存操作(如上面的xchg)对所有CPU都可见,然后才执行
* 后续的complete()操作,这对于正确的同步至关重要。
*/
/*
* 原子地将coredump发起者正在等待的线程数减1,并检查结果是否为0。
*/
if (atomic_dec_and_test(&core_state->nr_threads))
/*
* 如果当前线程是最后一个完成同步的,则调用complete(),
* 唤醒正在等待core_state->startup这个completion对象的
* coredump发起者线程。
*/
complete(&core_state->startup);

/*
* 进入一个循环,将自己投入睡眠,直到coredump过程完成。
*/
for (;;) {
/*
* 将自己的状态设置为TASK_IDLE|TASK_FREEZABLE。TASK_IDLE是一个
* 特殊的、不可被信号唤醒的睡眠状态。
*/
set_current_state(TASK_IDLE|TASK_FREEZABLE);
/*
* 注释:参见coredump_finish()。
* coredump完成后,dumper线程会遍历它等待的线程列表,
* 并将它们的self.task指针置为NULL。
* 所以,这里检查self.task是否已变为NULL。
*/
if (!self.task)
break; /* 如果是,则跳出循环,继续执行退出流程。*/

/* 如果不是,则调用schedule(),放弃CPU,继续睡眠。*/
schedule();
}
/*
* 当跳出循环后,将自己的状态恢复为TASK_RUNNING,
* 以便能继续执行do_exit的后续部分。
*/
__set_current_state(TASK_RUNNING);
}

synchronize_group_exit 同步一个线程组的退出状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/*
* 这是一个静态函数,用于同步一个线程组的退出状态。
* @tsk: 指向当前正在退出的任务。
* @code: 当前任务的退出码。
*/
static void synchronize_group_exit(struct task_struct *tsk, long code)
{
/* sighand: 指向线程组共享的信号处理器结构体,其中的siglock用于保护。*/
struct sighand_struct *sighand = tsk->sighand;
/* signal: 指向线程组共享的信号状态结构体,包含了退出相关的标志和计数。*/
struct signal_struct *signal = tsk->signal;
/* core_state: 指向用于协调核心转储(coredump)的状态结构体。*/
struct core_state *core_state;

/* 获取保护整个线程组信号状态的自旋锁,并禁用中断。*/
spin_lock_irq(&sighand->siglock);

/* 将“快速路径”线程计数减1。这个计数代表了还未进入do_exit的线程数。*/
signal->quick_threads--;
/*
* 检查当前线程是否是最后一个到达此同步点的线程 (quick_threads == 0),
* 并且检查SIGNAL_GROUP_EXIT标志是否尚未被设置(防止重复设置)。
*/
if ((signal->quick_threads == 0) &&
!(signal->flags & SIGNAL_GROUP_EXIT)) {
/*
* 如果满足条件,则当前线程被选举为“退出发起者”。
*/
/* 设置SIGNAL_GROUP_EXIT标志,向所有兄弟线程宣告:整个组正在退出。*/
signal->flags = SIGNAL_GROUP_EXIT;
/* 记录整个线程组的退出码。*/
signal->group_exit_code = code;
/* 重置组停止计数器。*/
signal->group_stop_count = 0;
}
/*
* 与任何可能挂起的coredump进行序列化。
* 我们必须在持有siglock的情况下检查core_state并设置PF_POSTCOREDUMP。
* 引发coredump的线程会为组内每个未设置PF_POSTCOREDUMP的线程增加其等待计数。
*/
/*
* 为当前任务设置PF_POSTCOREDUMP标志。这相当于举手示意:“我已经进入退出流程,
* coredump的发起者请不要再等我了”。
*/
tsk->flags |= PF_POSTCOREDUMP;
/*
* 在锁的保护下,读取coredump状态指针。
*/
core_state = signal->core_state;

/* 释放信号锁,恢复中断。*/
spin_unlock_irq(&sighand->siglock);

/*
* unlikely()是编译器优化提示,表示这种情况不常发生。
* 如果core_state指针非空,说明在当前线程退出的同时,组内有另一个线程
* 正在发起coredump。
*/
if (unlikely(core_state))
/*
* 调用coredump_task_exit(),这是一个同步函数,它会与coredump的发起者
* 进行协调,确保本线程的退出被正确处理,之后才允许继续do_exit流程。
*/
coredump_task_exit(tsk, core_state);
}

exit_mm

  • exit_mm是do_exit函数中一个至关重要的步骤。它的核心作用是:将当前正在退出的任务,从其所关联的用户地址空间(由struct mm_struct描述)中安全地、原子性地“解绑(detach)”。
    对于一个线程组(进程)中的非最后一个线程,这个操作意味着它将变成一个“懒惰TLB(Lazy TLB)”的内核线程,不再拥有自己的地址空间。对于最后一个线程,这个操作会触发整个用户地址空间的销毁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/*
* 将我们自己转变为一个懒惰TLB进程,如果我们还不是的话..
*/
static void exit_mm(void)
{
/* mm: 获取当前任务的内存描述符结构体指针。*/
struct mm_struct *mm = current->mm;

/*
* 调用exit_mm_release,它会释放与I/O调度器相关的io_context,
* 并为OOM killer更新oom_score_adj_min。
*/
exit_mm_release(current, mm);
/* 如果当前任务没有地址空间(例如,它是一个内核线程),则直接返回。*/
if (!mm)
return;

/* 获取mm->mmap_lock的读锁,防止在操作期间,VMA(虚拟内存区域)列表被修改。*/
mmap_read_lock(mm);
/*
* 增加mm的“懒惰TLB”引用计数。这表示有一个任务正在懒惰地使用这个mm,
* 防止它被过早地完全销毁。
*/
mmgrab_lazy_tlb(mm);
/* 一个健壮性检查,确保当前任务的活动mm就是我们正在处理的mm。*/
BUG_ON(mm != current->active_mm);
/* 获取任务自身的锁。注释说,这更像一个内存屏障而不是一个真正的锁。*/
task_lock(current);
/*
* 注释:当一个线程停止在一个地址空间上操作时,membarrier_private_expedited()
* 中的循环可能没有观察到那个tsk->mm,并且membarrier_global_expedited()
* 中的循环可能没有观察到一个MEMBARRIER_STATE_GLOBAL_EXPEDITED的
* rq->membarrier_state,所以它们可能不会发出IPI。Membarrier要求
* 在访问用户空间内存之后、清除tsk->mm或rq->membarrier_state之前,
* 必须有一个内存屏障。
*/
/* 插入一个显式的、完全的内存屏障,以满足membarrier的同步要求。*/
smp_mb__after_spinlock();
/* 关闭本地中断,以确保接下来的current->mm = NULL操作是原子的。*/
local_irq_disable();
/* 核心操作:将当前任务的mm指针置为NULL,正式放弃对该地址空间的所有权。*/
current->mm = NULL;
/* 通知membarrier子系统,当前任务的mm已经改变。*/
membarrier_update_current_mm(NULL);
/*
* 调用enter_lazy_tlb,将当前任务设置为“懒惰TLB”模式。
* 这意味着它的active_mm仍然指向旧的mm,但它自己已经没有mm了。
* 这允许它在执行最后的内核代码时,仍然有一个有效的地址空间上下文。
*/
enter_lazy_tlb(mm, current);
/* 恢复本地中断。*/
local_irq_enable();
/* 释放任务锁。*/
task_unlock(current);
/* 释放mmap_lock读锁。*/
mmap_read_unlock(mm);
/* 如果这个mm现在没有持有者了,尝试为它寻找一个新的所有者(用于NUMA优化)。*/
mm_update_next_owner(mm);
/*
* 调用mmput,将当前任务对mm的引用计数减1。
* 如果这是最后一个引用,这次调用将触发整个地址空间的销毁。
*/
mmput(mm);
/* 检查TIF_MEMDIE标志,如果任务是被OOM killer杀死的,则调用特殊的退出处理。*/
if (test_thread_flag(TIF_MEMDIE))
exit_oom_victim();
}

release_task

  • 负责彻底销毁一个僵尸任务(zombie task)所剩余的所有资源的最终执行函数。当一个任务的父进程通过wait()回收了它,或者它被确认为可以被自动回收(autoreap)时,此函数就会被调用
  • 执行一系列不可逆的、最终的清理操作,包括将任务从所有内核列表中“除名”,并最终释放其task_struct结构体和内核栈的内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/*
* @p: 指向即将被彻底释放的僵尸任务。
*/
void release_task(struct task_struct *p)
{
/* @post: 一个临时结构体,用于在__exit_signal中收集需要稍后释放的PID信息。*/
struct release_task_post post;
/* @leader: 指向任务p所在线程组的领导者。*/
struct task_struct *leader;
/* @thread_pid: 临时保存任务p的PID结构体指针。*/
struct pid *thread_pid;
/* @zap_leader: 一个布尔标志,如果为true,表示需要接着清理线程组的领导者。*/
int zap_leader;
repeat: /* 这是循环回收机制的入口点。*/
/* 将post结构体清零。*/
memset(&post, 0, sizeof(post));

/*
* 注释:这里不需要获取RCU读锁 - 因为进程已死,不能再修改它自己的凭证。
* 但需要关闭RCU-lockdep的警告。
*/
/* 进入一个短暂的RCU读临界区,主要是为了满足锁依赖检测器的要求。*/
rcu_read_lock();
/* 将该任务的用户所拥有的进程数(用于RLIMIT_NPROC)减1。*/
dec_rlimit_ucounts(task_ucounts(p), UCOUNT_NPROC, 1);
rcu_read_unlock();

/* 清理与pidfd相关的资源。*/
pidfs_exit(p);
/* 释放任务在cgroup子系统中的所有关联和资源。*/
cgroup_release(p);

/* 注释:在__unhash_process()可能将PID设为NULL之前,先获取@thread_pid。*/
thread_pid = task_pid(p);

/* 获取保护全局进程树的写锁,并禁用中断。*/
write_lock_irq(&tasklist_lock);
/* 处理与ptrace调试相关的最终清理。*/
ptrace_release_task(p);
/*
* 调用__exit_signal,这是一个核心函数,它会将任务从PID哈希表和
* 各种链表(线程组,父子关系)中彻底摘除。
* 同时,它会收集需要释放的PID信息到post结构体中。
*/
__exit_signal(&post, p);

/*
* 注释:如果我们是线程组中最后一个非领导者成员,并且领导者是僵尸,
* 那么就通知组领导者的父进程。(如果它需要通知的话。)
*/
/* 将zap_leader标志清零。*/
zap_leader = 0;
/* 获取组领导者。*/
leader = p->group_leader;
/*
* 检查是否满足“子线程为僵尸领导者送终”的条件:
* 1. p不是领导者自己。
* 2. 领导者所在的线程组现在为空(thread_group_empty会检查leader是否是最后一个)。
* 3. 领导者处于僵尸状态。
*/
if (leader != p && thread_group_empty(leader)
&& leader->exit_state == EXIT_ZOMBIE) {
/* 如果组已退出,则将组退出码复制到leader的退出码中。*/
if (leader->signal->flags & SIGNAL_GROUP_EXIT)
leader->exit_code = leader->signal->group_exit_code;
/*
* 代表僵尸领导者,向“祖父”进程发送通知。
* do_notify_parent的返回值决定了leader是否也可以被自动回收。
*/
zap_leader = do_notify_parent(leader, leader->exit_signal);
/* 如果可以,则将leader的状态也置为EXIT_DEAD。*/
if (zap_leader)
leader->exit_state = EXIT_DEAD;
}

/* 释放全局进程树锁。*/
write_unlock_irq(&tasklist_lock);

/* 刷新与该任务PID相关的procfs缓存。*/
proc_flush_pid(thread_pid);
/* 将任务的运行时长贡献给系统的随机数熵池。*/
add_device_randomness(&p->se.sum_exec_runtime,
sizeof(p->se.sum_exec_runtime));
/* 释放所有在__exit_signal中收集的PID结构体。*/
free_pids(post.pids);
/* 释放与线程相关的、体系结构特定的资源。*/
release_thread(p);
/*
* 注释:这个任务已经被从进程/线程/pid列表中移除,lock_task_sighand(p)
* 不会成功。没有其他人能接触到->pending,或者在组死亡的情况下,
* 接触到signal->shared_pending。我们可以无锁地调用flush_sigqueue()。
*/
/* 清理任务自身私有的待决信号队列。*/
flush_sigqueue(&p->pending);
/* 如果p是组领导者,还需清理共享的待决信号队列。*/
if (thread_group_leader(p))
flush_sigqueue(&p->signal->shared_pending);

/*
* 调用此函数,它会减少一个特殊的RCU用户引用计数,并在计数为0时,
* 通过call_rcu()来延迟触发对task_struct本身的最终释放。
*/
put_task_struct_rcu_user(p);

/* 将p指针指向leader。*/
p = leader;
/* 如果zap_leader为真,说明我们还需要清理领导者。*/
if (unlikely(zap_leader))
/* 跳转到repeat标签,以leader为目标,重新开始整个释放流程。*/
goto repeat;
}

exit_notify

  • 向内核中的相关方(主要是父进程、ptrace调试器)宣告当前任务tsk已经死亡,并处理其子进程的“过继”以及自身的最终状态(是成为僵尸,还是被立即回收)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/*
* 注释:向我们所有最近的亲属发送信号,让他们知道要适当地为我们“哀悼”..
*
* @tsk: 指向当前正在退出的任务。
* @group_dead: 布尔值,指示这是否是其线程组中的最后一个线程。
*/
static void exit_notify(struct task_struct *tsk, int group_dead)
{
/* autoreap: 布尔标志,如果为true,表示任务可以被自动回收,无需进入僵尸状态。*/
bool autoreap;
/* p, n: 用于安全地遍历链表的任务指针。*/
struct task_struct *p, *n;
/* dead: 一个临时的链表头,用于收集所有可以被立即释放的任务。*/
LIST_HEAD(dead);

/* 获取保护全局进程树(父子、兄弟关系)的写锁,并禁用中断。*/
write_lock_irq(&tasklist_lock);
/*
* 处理tsk的所有子进程,将它们“过继”给tsk线程组内的其他线程或init进程。
* 如果发现有已经是僵尸的子进程,会将它们加入到dead链表中。
*/
forget_original_parent(tsk, &dead);

/* 如果整个线程组都死亡了。*/
if (group_dead)
/* 检查tsk所在的进程组是否已成为孤儿进程组,如果是,则向其成员发送SIGHUP和SIGCONT。*/
kill_orphaned_pgrp(tsk->group_leader, NULL);

/*
* 将任务的退出状态设置为EXIT_ZOMBIE。这是默认状态,任务将保留其task_struct
* 等待父进程的wait()调用。
*/
tsk->exit_state = EXIT_ZOMBIE;

/* 如果任务正在被ptrace调试,unlikely()是编译器优化提示。*/
if (unlikely(tsk->ptrace)) {
/*
* 计算要发送给父进程的信号。如果这是线程组中最后一个、且未被重设父进程的
* 领导者,则使用其自定义的退出信号,否则使用标准的SIGCHLD。
*/
int sig = thread_group_leader(tsk) &&
thread_group_empty(tsk) &&
!ptrace_reparented(tsk) ?
tsk->exit_signal : SIGCHLD;
/* 调用do_notify_parent发送通知,并获取是否可以自动回收的标志。*/
autoreap = do_notify_parent(tsk, sig);
/* 如果任务是线程组领导者(但未被调试)。*/
} else if (thread_group_leader(tsk)) {
/* 只有当它是组内最后一个线程时,才通知父进程,并根据返回值决定是否自动回收。*/
autoreap = thread_group_empty(tsk) &&
do_notify_parent(tsk, tsk->exit_signal);
} else {
/* 如果是一个普通的、未被追踪的子线程退出。*/
autoreap = true;
/* 通过pidfd机制通知任何正在监听此特定线程的进程。*/
do_notify_pidfd(tsk);
}

/* 如果根据上面的逻辑,确定该任务可以被自动回收。*/
if (autoreap) {
/* 将其退出状态从EXIT_ZOMBIE更新为EXIT_DEAD。*/
tsk->exit_state = EXIT_DEAD;
/* 将其加入到临时的dead链表中,准备立即释放。*/
list_add(&tsk->ptrace_entry, &dead);
}

/*
* 处理多线程exec()的同步。如果notify_count为负,说明group_exec_task
* 正在等待所有旧线程退出,这里需要唤醒它。
*/
if (unlikely(tsk->signal->notify_count < 0))
wake_up_process(tsk->signal->group_exec_task);

/* 释放全局进程树锁,恢复中断。*/
write_unlock_irq(&tasklist_lock);

/*
* 在释放了tasklist_lock之后,安全地遍历dead链表,
* 对其中所有任务(可能是tsk的僵尸子进程,也可能是tsk自己)
* 调用release_task来彻底释放它们的资源。
*/
list_for_each_entry_safe(p, n, &dead, ptrace_entry) {
list_del_init(&p->ptrace_entry);
release_task(p);
}
}

do_exit 处理进程退出

  • 释放一个任务所拥有的大部分资源,将其从一个活跃的执行单元转变为一个“僵尸(zombie)”状态,并最终调用调度器让出CPU
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
/*
* __noreturn: GCC属性,告诉编译器和静态分析工具,这个函数永远不会返回。
* @code: 任务的退出码。
*/
void __noreturn do_exit(long code)
{
/* tsk: 指向当前正在退出的任务的task_struct。*/
struct task_struct *tsk = current;
/* group_dead: 布尔标志,用于记录当前任务是否是其线程组中的最后一个。*/
int group_dead;

/* 打印内核警告,如果退出时中断是关闭的(这通常是内核逻辑错误的标志)。*/
WARN_ON(irqs_disabled());
/* 打印内核警告,如果任务的plug字段非空,表示有未完成的“拔出”操作。*/
WARN_ON(tsk->plug);

/* 通知kcov(代码覆盖率)子系统,任务正在退出。*/
// kcov_task_exit(tsk);
/* 通知kmsan(内核内存消毒剂)子系统,任务正在退出。*/
// kmsan_task_exit(tsk);

/* 同步线程组的退出状态。*/
synchronize_group_exit(tsk, code);
/* 如果任务被ptrace调试,则向调试器发送PTRACE_EVENT_EXIT事件。*/
ptrace_event(PTRACE_EVENT_EXIT, code);
/* 为用户空间事件通知机制(user_events)处理退出事件。*/
// user_events_exit(tsk);

/* 取消该任务所有挂起的io_uring异步I/O操作。*/
// io_uring_files_cancel();
/*
* 处理信号相关的退出逻辑。此函数是关键,它会设置PF_EXITING标志,
* 防止新的信号被处理。
*/
exit_signals(tsk);

/* 释放与seccomp(安全计算模式)过滤器相关的资源。*/
// seccomp_filter_release(tsk);

/* 更新任务的CPU时间积分统计。*/
// acct_update_integrals(tsk);
/* 原子地将线程组的存活线程数减1,并检查结果是否为0。*/
group_dead = atomic_dec_and_test(&tsk->signal->live);
/* 如果这是线程组中的最后一个线程... */
if (group_dead) {
/*
* 如果全局的init进程(PID 1)的最后一个线程退出了,立即触发panic
* 以获取一个可用的coredump。系统无法在没有init进程的情况下继续运行。
*/
if (unlikely(is_global_init(tsk)))
panic("Attempted to kill init! exitcode=0x%08x\n",
tsk->signal->group_exit_code ?: (int)code);

/* 如果配置了POSIX定时器。*/
// #ifdef CONFIG_POSIX_TIMERS
// /* 取消与整个线程组关联的实时定时器。*/
// hrtimer_cancel(&tsk->signal->real_timer);
// /* 清理进程间隔定时器(itimers)。*/
// exit_itimers(tsk);
// #endif
/* 如果任务有关联的地址空间,则更新线程组的最大内存占用(maxrss)统计。*/
if (tsk->mm)
setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm);
}
/* 为进程记账(BSD process accounting)收集退出信息。*/
// acct_collect(code, group_dead);
/* 如果是组长退出,则进行tty审计相关的退出处理。*/
// if (group_dead)
// tty_audit_exit();
/* 释放与audit子系统相关的资源。*/
// audit_free(tsk);

/* 将最终的退出码保存到task_struct中,供父进程通过wait()获取。*/
tsk->exit_code = code;
/* 为taskstats接口报告退出事件。*/
// taskstats_exit(tsk, group_dead);
/* 为内核追踪系统记录进程退出事件。*/
// trace_sched_process_exit(tsk, group_dead);

/* 释放与地址空间(mm_struct)的关联。如果是最后一个使用者,则销毁整个地址空间。*/
exit_mm();

/* 如果是组长退出,执行最终的进程记账。*/
// if (group_dead)
// acct_process();

/* 释放System V信号量资源。*/
// exit_sem(tsk);
/* 释放System V共享内存资源。*/
// exit_shm(tsk);
/* 释放文件描述符表。*/
exit_files(tsk);
/* 释放文件系统上下文(如根目录、当前工作目录的引用)。*/
exit_fs(tsk);
/* 如果是组长退出,脱离控制终端(ctty)。*/
if (group_dead)
disassociate_ctty(1);
/* 退出任务所属的所有命名空间。*/
exit_task_namespaces(tsk);
/* 执行所有通过task_work_add()注册的待办工作。*/
exit_task_work(tsk);
/* 最终的线程特定清理。*/
exit_thread(tsk);

/*
* 在父进程被子进程退出通知唤醒之前,将继承的性能计数器刷回给父进程。
* 因为cgroup模式的原因,必须在cgroup_exit()之前调用。
*/
// perf_event_exit_task(tsk);

// /* 执行与调度器自动分组相关的退出清理。*/
// sched_autogroup_exit_task(tsk);
// /* 执行cgroup的退出清理。*/
// cgroup_exit(tsk);

/*
* FIXME: 应该只在需要时,使用sched_exit追踪点来做这个。
* 清理ptrace设置的硬件断点。
*/
// flush_ptrace_hw_breakpoint(tsk);

/* 开始任务RCU清理的第一阶段。*/
// exit_tasks_rcu_start();
/* 发出退出通知,唤醒父进程(通过SIGCHLD)和ptrace调试器。*/
exit_notify(tsk, group_dead);
/* 通过connector接口向用户空间报告进程退出事件。*/
// proc_exit_connector(tsk);
/* 释放任务的内存策略(NUMA)。*/
// mpol_put_task_policy(tsk);
/* 如果配置了FUTEX。*/
// #ifdef CONFIG_FUTEX
// /* 释放与优先级继承相关的futex状态缓存。*/
// if (unlikely(current->pi_state_cache))
// kfree(current->pi_state_cache);
// #endif
/*
* 确保我们没有持有任何锁。这是一个调试检查。
*/
// debug_check_no_locks_held();

/* 如果任务有关联的I/O上下文,则释放它。*/
if (tsk->io_context)
exit_io_context(tsk);

/* 如果任务在使用splice()时有残留的管道信息,则释放它。*/
if (tsk->splice_pipe)
free_pipe_info(tsk->splice_pipe);

/* 如果任务使用了页片段(page fragment),则减少其页引用计数。*/
if (tsk->task_frag.page)
put_page(tsk->task_frag.page);

/* 为任务栈进行记账清理。*/
exit_task_stack_account(tsk);

/* 检查内核栈的使用情况,用于调试。*/
// check_stack_usage();
/* 禁用抢占。*/
preempt_disable();
/* 如果任务弄脏了一些页面,将这个“泄漏”的计数值加回到全局计数中。*/
if (tsk->nr_dirtied)
__this_cpu_add(dirty_throttle_leaks, tsk->nr_dirtied);
/* 执行与RCU相关的退出清理。*/
exit_rcu();
/* 完成任务RCU清理的最后阶段。*/
exit_tasks_rcu_finish();

/* 释放与锁依赖检测器(lockdep)相关的任务数据。*/
lockdep_free_task(tsk);
/* 调用do_task_dead(),它会将任务状态设置为EXIT_DEAD,并最终调用schedule()。*/
do_task_dead();
}