linux内核进程管理(1)——创建,退出
linux源码阅读——进程管理(1)
- 1. 进程的基本介绍
- 1.1 linux中进程和线程的区别
- 1.2 task_struct中的基本内容
- 1.3 命名空间ns(namespace)
- 命名空间结构图
- Linux 中的命名空间类型
- 1.4 进程标识符
- 2. 创建一个进程的流程
- 2.1 CLONE宏
- 2.2 创建进程系统调用
- 1. do_fork流程——v6.9-kernel_clone
- 2. do_fork中copy_process流程
- (1). 标志冲突判断
- (2). dup_task_struct——分配task空间
- (3). 检查用户的进程数量限制
- (4). copy_creds复制或共享证书
- (5). 检查线程数量限制
- (6). sched_fork——设置调度器相关参数
- (7). copy_xxx——根据CLONE_FLAG复制或共享资源
- 3. do_fork中的wake_up_new_task流程
- 3. 进程的退出
- 3.1 退出概念简介
- 3.2 exit_group线程组退出
- 1. exit_group简介
- 2. exit_group具体流程
- 3.3 kill
注 : 图片来自《Linux内核深度解析 基于ARM64架构的Linux 4.x内核》(余华兵)
1. 进程的基本介绍
1.1 linux中进程和线程的区别
在课本教学中我们习惯将进程和线程看作两种差别很大的东西,但是在实际的linux系统中,无论是进程还是线程都由所谓PCB(process control block) 也就是我们的 task_struct表示。
进程的虚拟地址空间分为用户虚拟地址空间和内核虚拟地址空间,所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间。
- 先说结论
- 进程——独立拥有用户虚拟地址空间
- 用户线程——共享用户虚拟地址空间
- 内核线程——没有用户虚拟地址空间
那么大家就要问了,什么是用户虚拟地址空间(mm_struct),这个在讲调度的时候介绍
1.2 task_struct中的基本内容
成员 | 说明 |
---|---|
volatile long state; | 进程的状态,使用volatile关键字,确保读取都能得到最新的值 |
void *stack; | 指向内核栈, 进程在内核态下的运行“装备”,确保进程在进入内核模式时能够正确恢复上下文并安全执行内核代码 |
pid_t pid; | 全局的进程号,同一PID命名空间下,pid唯一 |
pid_t tgid; | 全局的线程组标识符,在一个多线程进程中,多个线程会有不同的 pid,但它们的 tgid 会相同 |
struct pid_link pids[PIDTYPE_MAX]; | 进程号,进程组标识符和会话标识符 |
struct task_struct __rcu *real_parent; | real_parent指向真实的父进程, rcu(read-copy-update)并发编程机制 |
struct task_struct __rcu *parent; | parent指向父进程:如果进程被另一个进程(通常是调试器)使用系统调用ptrace跟踪,那么父进程是跟踪进程,否则和real_parent相同 |
struct task_struct *group_leader; | 指向线程组的组长 |
const struct cred __rcu *real_cred; | real_cred指向主体和真实客体证书, |
const struct cred __rcu *cred; | cred指向有效客体证书。通常情况下,cred和real_cred指向相同的证书,但是cred可以被临时改变 |
char comm[TASK_COMM_LEN]; | 进程名称 |
int prio, static_prio, normal_prio; unsigned int rt_priority; unsigned int policy; | 调度策略和优先级,dl,rt,cfs |
cpumask_t cpus_allowed | 允许进程在哪些处理器上运行,处理器亲和性 |
struct mm_struct *mm,*active_mm; | 指向内存描述符进程:mm和active_mm指向同一个内存描述符内核线程:mm是空指针,当内核线程运行时,active_mm指向从进程借用的内存描述符 |
struct fs_struct *fs; | 文件系统信息,主要是进程的根目录和当前工作目录 |
struct files_struct *files; | 打开文件表 |
struct nsproxy *nsproxy; | 命名空间代理 |
struct signal_struct *signal; struct sighand_struct *sighand; sigset_t blocked, real_blocked; sigset_t saved_sigmask; struct sigpending pending; | 信号处理, signal_struct 用于存储和管理进程的信号状态。 sighand_struct 管理进程的信号处理程序。 blocked 和 real_blocked 控制进程哪些信号被阻塞,避免信号干扰进程执行。 saved_sigmask 保存和恢复进程的信号掩码状态。 sigpending 存储待处理的信号。 |
1.3 命名空间ns(namespace)
和虚拟机相比,容器是一种轻量级的虚拟化技术,直接使用宿主机的内核,使用命名空间隔离资源。
命名空间(Namespace)是 Linux 内核中的一种重要特性,它用于隔离不同进程的资源,使得多个进程能够在同一台机器上“仿佛”运行在独立的系统中。命名空间技术是实现容器(如 Docker)的核心技术之一。命名空间通过提供资源的隔离,使得不同进程或容器能够有自己独立的资源视图,如进程号(PID)、网络、挂载等。
命名空间结构图
Linux 中的命名空间类型
Linux 支持多种类型的命名空间,每种命名空间用于隔离系统的某种资源。以下是 Linux 中常见的几种命名空间类型:
-
进程号命名空间(PID Namespace)
- 进程号命名空间使得每个进程可以拥有自己的进程号(PID)。在不同的进程号命名空间中,同一个 PID 号可以表示不同的进程。
- 父进程的 PID 在子命名空间内不再是唯一的,而是局限于该命名空间中。比如,PID 1 代表的是该命名空间中的第一个进程,而不是宿主机上的第一个进程。
- 这种隔离有助于容器化应用的管理,因为它们在容器内可以拥有从 1 开始的 PID,而与宿主机的进程号不冲突。
-
挂载命名空间(Mount Namespace)
- 挂载命名空间允许不同的进程在不同的命名空间中看到不同的文件系统视图。
- 它能够实现容器内进程看到的文件系统与宿主机或其他容器内进程看到的文件系统完全不同。例如,在容器中,你可以挂载不同的目录,而这些挂载操作不会影响宿主机的文件系统。
- 挂载命名空间是实现文件系统隔离的基础。
-
网络命名空间(Network Namespace)
- 网络命名空间提供了进程之间网络资源(如 IP 地址、路由表、网络设备等)的隔离。
- 每个网络命名空间有自己的网络接口、路由表、防火墙等配置。这样,容器或进程在不同的网络命名空间中,彼此之间不能直接通信,除非通过某种方式显式地配置网络连接。
- 网络命名空间使得容器能够拥有自己的 IP 地址,甚至在不同的容器之间实现隔离的虚拟网络。
-
IPC 命名空间(IPC Namespace)
- IPC 命名空间用于隔离进程间通信(IPC)机制,如信号量、消息队列和共享内存。
- 在不同的 IPC 命名空间中,进程看到的共享内存段、消息队列等资源是隔离的。即使两个进程有相同的 PID,它们也不能相互访问对方的 IPC 资源。
-
UTS 命名空间(UTS Namespace)
- UTS 命名空间用于隔离主机名和域名系统(DNS)信息。
- 在不同的 UTS 命名空间中,进程可以拥有独立的主机名和域名,进而可以在容器中使用不同的主机名而不影响宿主机。
-
用户命名空间(User Namespace)
- 用户命名空间用于隔离进程的用户和组 ID(UID/GID)。
- 在一个用户命名空间内,进程可以有一个与宿主机完全不同的 UID 和 GID 映射。例如,容器中的进程可以运行在 UID 0(即 root 用户),而在宿主机上可能对应的是一个普通用户。这使得容器中的进程能够具有 root 权限,但在宿主机上却是以普通用户身份运行,从而提高安全性。
-
时间命名空间(Time Namespace)(Linux 5.6 及以后版本)
- 时间命名空间允许进程有自己独立的时间视图。每个时间命名空间可以有独立的系统时间(即时间和时区设置)。这使得容器能够有自己的时钟和时间管理系统,而不依赖于宿主机的时间。
-
cgroup 命名空间(Cgroup Namespace)(Linux 4.5 及以后版本)
- cgroup 命名空间用于隔离进程的控制组(cgroup)信息。cgroup 是一种内核机制,用于对进程进行资源限制、优先级调度等管理。cgroup 命名空间允许每个进程组(如容器)有自己独立的 cgroup 层次结构。
命名空间(Namespace)是 Linux 内核提供的一种资源隔离机制,它通过为进程提供独立的资源视图,确保不同进程或容器之间的隔离。命名空间的类型包括 PID、网络、挂载、IPC、UTS、用户等,这些命名空间在容器化技术、进程管理、安全性等方面起着关键作用。通过使用命名空间,Linux 能够在同一台机器上运行多个互相隔离的进程或容器,从而实现更高效和安全的资源管理。
1.4 进程标识符
- 进程标识符 pid
- 线程组标识符 tgid
- 进程组标识符 pgid
- 会话标识符 sid
sid是多个兄弟在一起(shell),pgid是爸爸带一堆儿子(fork)
会话和进程组被设计用来支持 shell 作业控制,shell 为执行单一命令或者管道的进程创建一个进程组
2. 创建一个进程的流程
2.1 CLONE宏
想要了解进程的创建流程首先需要了解,clone都具备哪些标识
标志 | 类别 | 作用 |
---|---|---|
CSIGNAL | 信号相关标志 | 子进程退出时发送给父进程的信号掩码。 |
CLONE_VM | 资源共享标志 | 父子进程共享虚拟内存空间,通常用于线程。 |
CLONE_FS | 资源共享标志 | 父子进程共享文件系统信息(如当前工作目录、根目录等)。 |
CLONE_FILES | 资源共享标志 | 父子进程共享打开的文件描述符。 |
CLONE_SIGHAND | 资源共享标志 | 父子进程共享信号处理程序和阻塞信号。 |
CLONE_PIDFD | 进程级别标志 | 在父进程中为子进程创建一个 pidfd (PID文件描述符)。 |
CLONE_PTRACE | 进程级别标志 | 允许追踪继续,父进程可以继续对其子进程进行追踪。 |
CLONE_VFORK | 进程级别标志 | 父进程希望子进程在释放其内存时唤醒父进程。 |
CLONE_PARENT | 进程级别标志 | 子进程和父进程保持相同的父进程。 |
CLONE_THREAD | 线程/进程级别标志 | 子进程和父进程属于同一个线程组,通常用于线程。 |
CLONE_NEWNS | 命名空间相关标志 | 创建一个新的挂载命名空间。 |
CLONE_SYSVSEM | 进程级别标志 | 父子进程共享 System V 信号量(SEM_UNDO)语义。 |
CLONE_SETTLS | 线程级别标志 | 为子进程创建新的 TLS(线程本地存储)。 |
CLONE_PARENT_SETTID | 线程级别标志 | 将线程标识符 (TID) 设置到父进程的 parent_tid 指向的位置。 |
CLONE_CHILD_CLEARTID | 线程级别标志 | 线程退出时清除其 TID(线程标识符)。 |
CLONE_DETACHED | 进程级别标志 | 已废弃,忽略。 |
CLONE_UNTRACED | 进程级别标志 | 如果设置了该标志,父进程不能强制对子进程进行追踪。 |
CLONE_CHILD_SETTID | 线程级别标志 | 子进程首次调度时将 TID 设置到 child_tid 指向的位置。 |
CLONE_NEWCGROUP | 命名空间相关标志 | 创建一个新的 cgroup(控制组)命名空间。 |
CLONE_NEWUTS | 命名空间相关标志 | 创建一个新的 UTS(UNIX时间共享)命名空间,通常用于主机名和域名的隔离。 |
CLONE_NEWIPC | 命名空间相关标志 | 创建一个新的 IPC(进程间通信)命名空间。 |
CLONE_NEWUSER | 命名空间相关标志 | 创建一个新的用户命名空间,通常用于进程的用户和组 ID 隔离。 |
CLONE_NEWPID | 命名空间相关标志 | 创建一个新的 PID(进程标识符)命名空间。 |
CLONE_NEWNET | 命名空间相关标志 | 创建一个新的网络命名空间。 |
CLONE_IO | 进程级别标志 | 子进程与父进程共享 I/O 上下文(I/O 调度等)。 |
2.2 创建进程系统调用
(1)fork(分叉):子进程是父进程的一个副本,采用了写时复制的技术。
(2)vfork:用于创建子进程,之后子进程立即调用 execve 以装载新程序的情况。为了避免复制物理页,父进程会睡眠等待子进程装载新程序。现在 fork 采用了写时复制的技术,vfork 失去了速度优势,已经被废弃。
(3)clone(克隆):可以精确地控制子进程和父进程共享哪些资源。这个系统调用的
主要用处是可供 pthread 库用来创建线程。
clone 是功能最齐全的函数,参数多,使用复杂,fork 是 clone 的简化函数。
那么在执行这三个系统调用,实际执行的是do_fork函数,注意在当前(2025/4)最新linux版本6.9中do_fork被更换为kernel_clone(), 具体执行流程和do_fork大体相同,主要变更在kernel_clone 扩展了 do_fork 的功能,并且增加了更多针对不同类型进程创建的支持,特别是在 clone() 调用中引入了更多控制参数和特性。
1. do_fork流程——v6.9-kernel_clone
在v6.9版本的内核中,do_fork被替换取而代之的是kernel_clone,两者核心流程类似
- 调用函数 copy_process 以创建新进程
- 关于第二个步骤中判断CLONE_PARENT_SETTID的操作——CLONE_PARENT_SETTID:这是 clone() 调用中的一个标志,表示 父进程想要在子进程创建时将子进程的 PID 设置到某个指定位置。这个标志位指示内核将子进程的 PID 写入父进程传入的 parent_tid 指针。
具体作用:在一些特定的多线程应用或线程库中,父进程或创建线程的控制者希望在用户空间中直接获得新创建线程(或进程)的 PID,便于后续的管理,比如设置线程的调度策略、处理进程间通信等。CLONE_PARENT_SETTID 可以帮助父进程或控制者直接获取子进程的 PID,避免了额外的查询操作。
- 调用函数 wake_up_new_task 以唤醒新进程。
- 如果是系统调用 vfork,那么当前进程等待子进程装载程序。
2. do_fork中copy_process流程
创建fork新进程的主要工作由函数 copy_process 实现
官方注释——只执行复制但是不启动,state设置为TASK_NEW
This creates a new process as a copy of the old one, but does not actually start it yet.
It copies the registers, and all the appropriate parts of the process environment (as per the clone flags). The actual kick-off is left to the caller.
接下来我们详细解读一下copy_process中每个流程
(1). 标志冲突判断
-
CLONE_NEWNS & CLONE_FS:
- CLONE_NEWNS 会创建一个新的挂载命名空间,意味着新的进程有一个独立的根目录(根文件系统)。而 CLONE_FS 是要求多个进程共享文件系统信息,包括根目录。如果同时设置这两个标志,会导致根目录和文件系统信息冲突,因此是无效的。
-
CLONE_NEWUSER & CLONE_FS:
- CLONE_NEWUSER 创建新的用户命名空间,使得新的进程具有独立的用户身份。而 CLONE_FS 会共享文件系统信息,这会导致新进程在文件系统方面不独立,违反了用户命名空间的隔离原则,因此这两个标志不能同时使用。
-
CLONE_THREAD & !CLONE_SIGHAND:
- 线程组的进程必须共享信号处理程序。因此,当设置 CLONE_THREAD 时,必须设置 CLONE_SIGHAND 来共享信号处理程序。如果没有设置 CLONE_SIGHAND,则无法确保线程组的一致性,导致冲突。
-
CLONE_SIGHAND & !CLONE_VM:
- 当进程共享信号处理程序(CLONE_SIGHAND)时,必须共享虚拟内存空间(CLONE_VM)。如果不共享虚拟内存,信号处理程序会因进程间内存空间不同而无法正常工作,因此此组合会产生冲突。
-
CLONE_PARENT & SIGNAL_UNKILLABLE:
- CLONE_PARENT 要求子进程的父进程为当前进程。全局 init 进程(init)是不可杀死的且没有父进程,因此不允许全局 init 进程创建其他兄弟进程。否则,会违反进程树结构,产生冲突。
-
CLONE_THREAD & (CLONE_NEWUSER | CLONE_NEWPID):
- 线程不允许跨越用户或 PID 命名空间。如果创建一个线程时,设置了 CLONE_NEWUSER 或 CLONE_NEWPID,则该线程将进入一个不同的命名空间,这会破坏线程组的共享,因此是无效的。
-
CLONE_PIDFD & CLONE_DETACHED:
- 如果设置了 CLONE_PIDFD,表示父进程会持有子进程的 PID 文件描述符,允许父进程跟踪子进程。而 CLONE_DETACHED 表示子进程是分离的,不需要父进程等待,因此不能同时设置这两个标志。分离进程不应持有 PID 文件描述符。
(2). dup_task_struct——分配task空间
函数 dup_task_struct:函数 dup_task_struct 为新进程的进程描述符分配内存,把当前进程的进程描述符复制一份,为新进程分配内核栈。
内核栈——task_struct中的stack指向内核栈
-
分配内核栈和 task_struct 所需空间
- 每个进程在内核中有一块私有栈(内核栈),和 task_struct 一起分配。
-
复制当前进程的 task_struct 数据到新结构体中
- 这是“浅拷贝”,后续会由 copy_xxx() 函数根据CLONE_FLAG做深拷贝处理(比如 copy_mm()、copy_files() 等)。
-
初始化调试字段、引用计数等
- 比如清除 task_struct->stack_canary,初始化调试状态。
(3). 检查用户的进程数量限制
对于普通用户:如果创建的进程数量超限,则失败
对于根用户:因为根用户默认拥有忽略资源限制的权限(CAP_SYS_RESOURCE)和系统管理权限(CAP_SYS_ADMIN),可以创建
(4). copy_creds复制或共享证书
什么是 cred 证书?
回答 :
cred
(全称 struct cred
)是 Linux 内核中用于描述进程安全相关信息的一种核心数据结构。它就是**“进程的身份证 + 安全通行证”,内核通过它判断当前进程能不能干某件事**。
🔐 cred
结构体包含什么?
这个结构体定义在 include/linux/cred.h
中,里面包含了以下这些字段(简化版):
字段 | 含义 |
---|---|
uid , gid | 实际用户/组 ID |
euid , egid | 有效用户/组 ID |
suid , sgid | 保存的用户/组 ID(用于切换身份) |
fsuid , fsgid | 文件系统相关的用户/组 ID(用于访问控制) |
cap_inheritable | 可继承的能力(capabilities) |
cap_permitted | 被允许的能力 |
cap_effective | 当前生效的能力(实际起作用的) |
cap_bset | bounding set,允许继承的最大能力集合 |
user | 指向 struct user_struct ,跟踪用户的资源使用(比如进程数) |
security | 安全模块使用(如 SELinux、AppArmor) |
🧠 作用是什么?
内核中,几乎所有需要安全判断的操作,比如:
- 访问文件
- 打开 socket
- 调用某些系统调用(比如
mount
、kill
、ptrace
) - 进入 namespace
- 修改资源限制
- 获取 debug 权限(如
/proc/*
)
都会通过 current->cred
来做决策。
比如:
if (capable(CAP_SYS_ADMIN)) {// 允许系统管理操作
}
这里的 capable()
内部其实就是读取当前线程的 cred->cap_effective
,看看有没有这个 capability。
🐾 谁用它?
除了内核自身判断权限外,像:
ptrace()
setuid()
,setgid()
capset()
- LSM(如 SELinux)
- 容器安全模型
都会操作或依赖 cred
。
一句话总结
cred是进程的安全身份信息,它决定了一个进程能干什么、能访问什么、有没有权限。
对于kernel_clone(do_fork)copy_cred()
如果设置了标志 CLONE_THREAD
,即新进程和当前进程属于同一个线程组,那么新进程和当前进程共享证书。
否则,复制cred。
(5). 检查线程数量限制
全局变量
nr_threads
存放当前的线程数量;max_threads
存放允许创建的线程最大数量,默认值是 MAX_THREADS。
如果线程数量达到允许的线程最大数量,那么不允许创建新进程。
(6). sched_fork——设置调度器相关参数
- 将task->state设为TASK_NEW,确保新进程不会被运行,也不会被信号唤醒,不会被加入就绪队列
rq(runqueue)
- 把新进程的调度优先级设置为当前进程的正常优先级
- 因为当前进程可能因为占有实时互斥锁而被临时提升了优先级
- unlikely检查是否设置了
SCHED_RESET_ON_FORK
标志,要求创建新进程时把新进程的调度策略和优先级设置为默认值 - 拒绝
dl(dealline)
任务fork - SMP 多核支持初始化
(7). copy_xxx——根据CLONE_FLAG复制或共享资源
Linux 内核在创建新进程时执行的一系列子系统状态复制流程,它的作用是将父进程的资源、状态等复制或共享给子进程,以确保新进程具有完整的运行环境。
✅ 步骤及作用一览表
在5-10
步骤中,只有相同线程组的线程间才会进行共享,人话:一个老爹生的
步骤 | 函数调用 | 作用 | 失败时回滚标签 |
---|---|---|---|
1 | perf_event_init_task(p, clone_flags) | 初始化 perf 事件监控(性能分析支持) | bad_fork_sched_cancel_fork |
2 | audit_alloc(p) | 初始化审计上下文,用于安全审计(audit 子系统) | bad_fork_cleanup_perf |
3 | shm_init_task(p) | 初始化与 System V 共享内存相关的结构 | 无回滚(无失败) |
4 | security_task_alloc(p, clone_flags) | 安全模块(如 SELinux)相关初始化 | bad_fork_cleanup_audit |
5 | copy_semundo(clone_flags, p) | 复制 System V 信号量 undo 状态 | bad_fork_cleanup_security |
6 | copy_files(clone_flags, p, args->no_files) | 复制或共享文件描述符表(如 open files ) | bad_fork_cleanup_semundo |
7 | copy_fs(clone_flags, p) | 复制或共享文件系统信息(如 cwd、root) | bad_fork_cleanup_files |
8 | copy_sighand(clone_flags, p) | 复制或共享信号处理器(signal handler) | bad_fork_cleanup_fs |
9 | copy_signal(clone_flags, p) | 复制或共享信号状态(阻塞信号集等) | bad_fork_cleanup_sighand |
10 | copy_mm(clone_flags, p) | 复制或共享内存描述符(内存空间) | bad_fork_cleanup_signal |
11 | copy_namespaces(clone_flags, p) | 创建或共享命名空间(如 UTS、IPC、Mount 等) | bad_fork_cleanup_mm |
12 | copy_io(clone_flags, p) | 复制或共享 IO 上下文(如 IO调度器相关) | bad_fork_cleanup_namespaces |
13 | copy_thread(p, args) | 初始化线程状态:栈、寄存器、TLS 等 | bad_fork_cleanup_io |
3. do_fork中的wake_up_new_task流程
那么在结束copy_process流程后,一个新的进程或线程就创建成功了,接下来就是要去运行它,还记得在copy_process()/sched_fork()
中将task->state
置为TASK_NEW
的作用吗?那么在wake_up_new_task
中把新进程的状态从 TASK_NEW
切换到 TASK_RUNNING
。
在 SMP 系统上,创建新进程是执行负载均衡的绝佳时机,为新进程选择一个负载最轻的处理器。
这时使用__set_task_cpu()
将任务放到负载最轻的处理器上,实现负载均衡
流程:
- 调整state为running
- __set_task_cpu(),负载均衡
- 锁rq
- 更新rq
- 释放锁
至此,一个新进程/线程的创建就正式结束了。
3. 进程的退出
3.1 退出概念简介
退出,又分为
- 主动退出
- exit,exit_group
- 被动退出
- kill,tgkill 被信号通知退出
当进程退出的时候,根据父进程是否关注子进程退出事件,处理存在如下差异。
(1)如果父进程关注子进程退出事件,那么进程退出时释放各种资源,只留下一个空的进程描述符,变成僵尸进程(即task_struct没有被完全释放)[因为系统需要保留一些关于子进程的信息,供父进程查询(例如子进程的退出状态)],发送信号 SIGCHLD(CHLD 是 child 的缩写)通知父进程,父进程在查询进程终止的原因以后回收子进程的进程描述符。
(2)如果父进程不关注子进程退出事件,那么进程退出时释放各种资源,释放进程描述符,自动消失。进程默认关注子进程退出事件,如果不想关注,可以使用系统调用 sigaction 针对信号SIGCHLD 设置标志 SA_NOCLDWAIT(CLD 是 child 的缩写),以指示子进程退出时不要变成僵尸进程,或者设置忽略信号 SIGCHLD。
3.2 exit_group线程组退出
1. exit_group简介
exit_group() 可以被看作是 信号传递式退出函数 的一种典型代表,它的语义是:终止整个线程组(即进程的所有线程)。
🧠 和普通 exit()
的区别:
函数 | 影响范围 | 特点 |
---|---|---|
exit() | 仅当前线程 | 不会终止进程中其他线程 |
exit_group() | 当前线程 + 同一进程中的所有线程 | 会向整个线程组中的所有线程发出终止信号 |
🧩 内核行为概览
exit_group()
最终调用的是do_group_exit()
- 它会设置
signal->group_exit_code
,标记线程组整体退出; - 然后会逐个 wake up 其他线程,让它们也进入退出流程;
- 所有线程会逐个进入
do_exit()
,释放资源、触发钩子等。
📦 为什么说它是“信号式”的?
尽管 exit_group()
本质是一个系统调用,但其实现通过内部机制模拟了类似“信号传播”的退出方式: 通过共享的 struct signal_struct
对线程组成员**“广播”退出状态**,这就像是“向整个线程组传递了一个退出意图”。
2. exit_group具体流程
假设一个线程组有两个线程,称为线程 1 和线程 2,线程 1 调用 exit_group 使线程组退
出,线程 1 的执行过程如下。
(1)把退出码保存在信号结构体的成员 group_exit_code 中,传递给线程 2。
(2)给线程组设置正在退出的标志。
(3)向线程 2 发送杀死信号,然后唤醒线程 2,让线程 2 处理杀死信号。
(4)线程 1 调用函数 do_exit 以退出。
线程 2 退出的执行流程如下图所示,线程 2 准备返回用户模式的时候,发现收到
了杀死信号,于是处理杀死信号,调用函数 do_group_exit,函数 do_group_exit 的执行过
程如下。
3.3 kill
系统调用 kill(源文件“kernel/signal.c”)负责向线程组或者进程组发送信号。
(1)如果参数 pid 大于 0,那么调用函数 kill_pid_info 来向线程 pid 所属的线程组发送信号。
(2)如果参数 pid 等于 0,那么向当前进程组发送信号。
(3)如果参数 pid 小于−1,那么向组长标识符为-pid 的进程组发送信号。
(4)如果参数 pid 等于−1,那么向除了 1 号进程和当前线程组以外的所有线程组发送信号。