[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()
函数,它们定义了一个严格的、多阶段的进程退出流程。
核心流程:
触发 :进程退出可以由多种方式触发:
自愿退出 :进程调用 exit()
或 exit_group()
系统调用(例如,main
函数返回)。
非自愿退出 :进程收到一个致命信号(如 SIGSEGV
, SIGKILL
),其内核处理函数的最后一步就是调用 do_exit()
。
进入退出状态 :
内核首先在进程的 task_struct
中设置 PF_EXITING
标志,表明该进程正处于退出过程中,阻止其他子系统对其进行新的操作。
资源剥离与释放 :这是最关键的一步,do_exit()
会像一个清单一样,调用一系列 exit_*()
辅助函数来释放与该进程关联的所有资源:
exit_mm()
:解除对进程地址空间 (mm_struct
) 的使用。对于多线程进程,只有最后一个线程退出时才会真正释放地址空间。
exit_files()
:关闭所有打开的文件描述符。
exit_sighand()
:释放信号处理相关的结构。
exit_thread()
:清理TLS(线程局部存储)等线程私有资源。
exit_namespaces()
:减少对命名空间的引用计数。
……以及其他几十项清理工作。
处理亲属关系 :
遣散子进程(Reparenting) :内核会遍历该进程的所有子进程,并将它们的父进程设置为当前线程组的“收割者”(通常是init进程/PID 1,或是一个sub-reaper)。这个过程称为“re-parenting”。
设置退出码 :将进程的退出状态码保存在 task_struct
中。
转变为僵尸(Zombie)状态 :
所有资源都被释放后,进程本身并没有完全消失。它的 task_struct
结构和一小部分内核栈被保留下来。进程状态被设置为 EXIT_ZOMBIE
。
目的 :保留这些信息是为了让其父进程能够通过 wait()
系列系统调用来查询它的退出码和资源使用情况。
通知父进程 :
向其(原始的)父进程发送 SIGCHLD
信号,通知父进程“你的一个孩子已经终止,可以来收集它的信息了”。
最终消亡(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 ; 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 ; } 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(); smp_mb(); 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(tsk); } void put_task_struct_rcu_user (struct task_struct *task) { if (refcount_dec_and_test(&task->rcu_users)) 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 static void coredump_task_exit (struct task_struct *tsk, struct core_state *core_state) { struct core_thread self ; self.task = tsk; if (self.task->flags & PF_SIGNALED) self.next = xchg(&core_state->dumper.next, &self); else self.task = NULL ; if (atomic_dec_and_test(&core_state->nr_threads)) complete(&core_state->startup); for (;;) { set_current_state(TASK_IDLE|TASK_FREEZABLE); if (!self.task) break ; schedule(); } __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 static void synchronize_group_exit (struct task_struct *tsk, long code) { struct sighand_struct *sighand = tsk->sighand; struct signal_struct *signal = tsk->signal; struct core_state *core_state ; 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 ; } tsk->flags |= PF_POSTCOREDUMP; core_state = signal->core_state; spin_unlock_irq(&sighand->siglock); if (unlikely(core_state)) 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 static void exit_mm (void ) { struct mm_struct *mm = current->mm; exit_mm_release(current, mm); if (!mm) return ; mmap_read_lock(mm); mmgrab_lazy_tlb(mm); BUG_ON(mm != current->active_mm); task_lock(current); smp_mb__after_spinlock(); local_irq_disable(); current->mm = NULL ; membarrier_update_current_mm(NULL ); enter_lazy_tlb(mm, current); local_irq_enable(); task_unlock(current); mmap_read_unlock(mm); mm_update_next_owner(mm); mmput(mm); 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 void release_task (struct task_struct *p) { struct release_task_post post ; struct task_struct *leader ; struct pid *thread_pid ; int zap_leader; repeat: memset (&post, 0 , sizeof (post)); rcu_read_lock(); dec_rlimit_ucounts(task_ucounts(p), UCOUNT_NPROC, 1 ); rcu_read_unlock(); pidfs_exit(p); cgroup_release(p); thread_pid = task_pid(p); write_lock_irq(&tasklist_lock); ptrace_release_task(p); __exit_signal(&post, p); zap_leader = 0 ; leader = p->group_leader; if (leader != p && thread_group_empty(leader) && leader->exit_state == EXIT_ZOMBIE) { if (leader->signal->flags & SIGNAL_GROUP_EXIT) leader->exit_code = leader->signal->group_exit_code; zap_leader = do_notify_parent(leader, leader->exit_signal); if (zap_leader) leader->exit_state = EXIT_DEAD; } write_unlock_irq(&tasklist_lock); proc_flush_pid(thread_pid); add_device_randomness(&p->se.sum_exec_runtime, sizeof (p->se.sum_exec_runtime)); free_pids(post.pids); release_thread(p); flush_sigqueue(&p->pending); if (thread_group_leader(p)) flush_sigqueue(&p->signal->shared_pending); put_task_struct_rcu_user(p); p = leader; if (unlikely(zap_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 static void exit_notify (struct task_struct *tsk, int group_dead) { bool autoreap; struct task_struct *p , *n ; LIST_HEAD(dead); write_lock_irq(&tasklist_lock); forget_original_parent(tsk, &dead); if (group_dead) kill_orphaned_pgrp(tsk->group_leader, NULL ); tsk->exit_state = EXIT_ZOMBIE; if (unlikely(tsk->ptrace)) { int sig = thread_group_leader(tsk) && thread_group_empty(tsk) && !ptrace_reparented(tsk) ? tsk->exit_signal : SIGCHLD; 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 ; do_notify_pidfd(tsk); } if (autoreap) { tsk->exit_state = EXIT_DEAD; list_add(&tsk->ptrace_entry, &dead); } if (unlikely(tsk->signal->notify_count < 0 )) wake_up_process(tsk->signal->group_exec_task); write_unlock_irq(&tasklist_lock); 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 void __noreturn do_exit (long code) { struct task_struct *tsk = current; int group_dead; WARN_ON(irqs_disabled()); WARN_ON(tsk->plug); synchronize_group_exit(tsk, code); ptrace_event(PTRACE_EVENT_EXIT, code); exit_signals(tsk); group_dead = atomic_dec_and_test(&tsk->signal->live); if (group_dead) { if (unlikely(is_global_init(tsk))) panic("Attempted to kill init! exitcode=0x%08x\n" , tsk->signal->group_exit_code ?: (int )code); if (tsk->mm) setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm); } tsk->exit_code = code; exit_mm(); exit_files(tsk); exit_fs(tsk); if (group_dead) disassociate_ctty(1 ); exit_task_namespaces(tsk); exit_task_work(tsk); exit_thread(tsk); exit_notify(tsk, group_dead); if (tsk->io_context) exit_io_context(tsk); if (tsk->splice_pipe) free_pipe_info(tsk->splice_pipe); if (tsk->task_frag.page) put_page(tsk->task_frag.page); exit_task_stack_account(tsk); preempt_disable(); if (tsk->nr_dirtied) __this_cpu_add(dirty_throttle_leaks, tsk->nr_dirtied); exit_rcu(); exit_tasks_rcu_finish(); lockdep_free_task(tsk); do_task_dead(); }