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 號進程和當前線程組以外的所有線程組發送信號。