# PID 0 行程之謎 > 資料整理: [jserv](https://wiki.csie.ncku.edu.tw/User/jserv) ## 管中窺豹 UNIX 從第 4 版開始,就嘗試以 C 語言撰寫,而現代相容於 UNIX 的作業系統核心也多以 C 語言開發,例如 Linux, FreeBSD 和 macOS 底層的 [XNU](https://en.wikipedia.org/wiki/XNU)。依據 C 語言的慣例,索引值從零開始,這原則也適用於 [UID](https://en.wikipedia.org/wiki/User_identifier) 和 [GID](https://en.wikipedia.org/wiki/Group_identifier),不過作為 UNIX 首個使用者層級的行程,[init](https://en.wikipedia.org/wiki/Init) (在 Linux 系統可能是 systemd) 的 [PID](https://en.wikipedia.org/wiki/Process_identifier) 則從 1 開始呢?而且對照 [Linux man pages](https://man7.org/linux/man-pages/) 後,更讓這問題變得更費解 ── [kill](https://man7.org/linux/man-pages/man2/kill.2.html) 系統呼叫提及: > If `pid` equals 0, then `sig` is sent to every process in the process group of the calling process. 這暗示著 PID 0 是相當特殊的案例,以至於 [kill](https://man7.org/linux/man-pages/man2/kill.2.html) 系統呼叫無法直接處理該執行單元,且 [fork](https://man7.org/linux/man-pages/man2/fork.2.html) 系統呼叫也提到: > On success, the PID of the child process is returned in the parent, and 0 is returned in the child. PID 0 根本直接被 [fork](https://man7.org/linux/man-pages/man2/fork.2.html) 系統呼叫跳過,`0` 這個數值被挪用在子行程判斷 fork 成功的依據。更甚者,2001 年才正式納入 POSIX 標準的 [sched_setscheduler](https://man7.org/linux/man-pages/man2/sched_setscheduler.2.html) 系統呼叫,其行為是 > ... sets both the scheduling policy and parameters for the thread whose ID is specified in `pid`. If `pid` equals zero, the scheduling policy and parameters of the calling thread will be set. 這明確說,根本無法變更 PID 0 對應的執行單元。 在 GNU/Linux 執行命令 `ps -eaf`,會得到類似以下輸出: ``` UID PID PPID C STIME TTY TIME CMD root 1 0 0 Jun09 ? 00:00:02 /sbin/init root 2 0 0 Jun09 ? 00:00:00 [kthreadd] root 3 2 0 Jun09 ? 00:00:00 [rcu_gp] root 4 2 0 Jun09 ? 00:00:00 [rcu_par_gp] ... ``` `ps` 命令的輸出中,用方括號標示的任務就隸屬於 Linux 核心執行緒,而 PPID 是 parent PID 的意思,於是我們可對比行程的 PPID,知曉其階層關係。`[kthreadd]` 是 Linux 核心執行緒的背景程式 (daemon),幾乎所有的 Linux 核心執行緒衍生自 `kthreadd` (存在例外狀況,也就是下文要探討的 PID 0),我們也可發現,kthread 和 init 皆衍生自 PID 0 這個執行單元。上述觀察讓我們知道在 UNIX 及其相容作業系統中,PID 確實從 0 開始,但 UNIX 不提供系統呼叫來讓使用者得以操作 PID 0 這個執行單元。 PID 0 到底是何方神聖? ## PID 0 的歷史淵源 [A Commentary on the UNIX Operating System](http://www.lemis.com/grog/Documentation/Lions/) 是解析 UNIX 第 6 版原始程式碼的權威著作,以下摘錄其關於 PID 0 的解說: > Process #0 executes `sched`. When it is not waiting for the completion of an input/output operation that it has initiated, it spends most of its time waiting in one of the following situations: > * A. (runout) None of the processes which are swapped out is ready to run, so that there is nothing to do. The situation may be changed by a call to "wakeup", or to "xswap" called by either "newproc" or "expand". > * B. (runin) There is at least one process swapped out and ready to run, but it hasn't been out more than 3 seconds and/or none of the processes presently in main memory is inactive or has been there more than 2 seconds. The situation may be changed by the effluxion of time as measured by "clock" or by a call to "sleep". 在早期的 UNIX 中,PID 0 被稱為 `sched`,其名稱意味著 scheduler (排程器),其行為緊密地圍繞在行程的置換 (swap),後者會置換整個行程的內容,包含核心模式的資料結構,到儲存空間 (通常為磁帶或硬碟),也會反過來將儲存空間的內容還原為行程,這在 PDP-7 和 PDP-11 主機上,是必要的機制,其硬體的記憶體管理不具備今日完整的虛擬記憶體能力。 以下是 UNIX 第 4 版對應的 [`sched` 函式](https://github.com/dspinellis/unix-history-repo/blob/Research-V4/sys/ken/slp.c#L89)程式碼: ```c sched() { static struct proc *p1, *p2; register struct proc *rp; int a; p1 = p2 = &proc[0]; /* * find user to swap in */ loop: spl6(); rp = p1; for (a = 0; a < NPROC; a++) { rp++; if (rp >= &proc[NPROC]) rp = &proc[0]; if (rp->p_stat == SRUN && (rp->p_flag&SLOAD) == 0) { p1 = rp; goto found; } } ... /* swap user out */ ... /* swap user in */ ... swaper: panic("swap error"); } ``` 這裡我們可留意到,排程器有時稱為 "swapper",之所以有這樣的用語,是因為在 UNIX 發展早期,任務的處理不僅包括經典的排程,尚有在資源有限的主記憶體和磁碟之間置換的操作,詳情可參見〈[UNIX 作業系統 fork/exec 系統呼叫的前世今生](https://hackmd.io/@sysprog/unix-fork-exec)〉。從早期 UNIX 原始程式碼來看,行程表格的第 0 個進入點初始化作業系統核心,然後進入 `sched` 函式,顯然,其排程演算法非常簡單,`sched` 大部分程式處理將行程映像在核心記憶體和磁碟之間進行交換。此外,當沒有其他行程可運行時,名為 idle 的任務就會運行,其具有最低的優先級,因此在沒有其他行程可運行時,會被執行,這簡化排程的實作方式:不必擔心特殊情況「若沒有行程可執行會發生什麼?」,因為總有一個行程可運行,亦即 idle 任務。此外,可藉此計算每個行程使用的 CPU 時間。 這種置換形式的記憶體管理,讓 UNIX 效率不彰,於是在 UNIX System V R2V5 和 4BSD (1980 年代) 就將 swapper 更換為 [demand paging](https://en.wikipedia.org/wiki/Demand_paging) 的虛擬記憶體管理機制,後者是 BSD 開發團隊向 Mach 微核心學習並改造的重大突破。 上述 UNIX 基本結構延續至今,並變得更複雜,當記憶體分頁 ([paging](https://en.wikipedia.org/wiki/Memory_paging)) 成為主流作業系統的關鍵設計考量,原本發生在 PID 0 的行程交換的機制就不再涉及記憶體管理。爾後,隨著排程演算法和 CPU idle 機制趨於複雜,原本實作於 `sched` 函式的排程和 idle 任務操作,被拆分為獨立的程式碼片段,形成延續數十年的面貌。 關於 idle 任務,過往缺乏能降頻或進入省電模式的 CPU,無論何時都是全速執行,一旦沒有行程可執行運行時,idle 任務會執行一系列 NOP 指令。如今,idle 任務的排程通常伴隨著特定的 CPU 指令來降低 CPU 的運行頻率,從而節省電力。 ## 從 Linux 核心啟動流程說起 當引導程式 (boot loader) 選定並載入作業系統核心後,它會將電腦的控制權交給已載入的核心。以 Linux 核心來說,引導程式會跳到 Linux 核心程式碼的第一條指令,用以處理 CPU 架構和相關系統整合晶片的硬體初始化。當準備工作完成後,Linux 核心會執行 `start_kernel()` 函式,後者的工作包含建立中斷處理機制、初始化記憶體管理的剩餘部分、初始化 CPU 排程器、初始化裝置及驅動程式等等。 在多核處理器系統中,引導程式或硬體會安排其中一個 CPU 核來執行 Linux 核心,稱為 bootstrap core。我們關注的是該單個執行緒,直到 Linux 核心自行啟動其他處理器核之前,這是我們唯一能夠使用的處理器。 以下程式碼列表取自 Linux v6.9 的 [init/main.c](https://elixir.bootlin.com/linux/v6.9.3/source/init/main.c#L881): ```c asmlinkage __noreturn void start_kernel(void) { char *command_line; char *after_dashes; set_task_stack_end_magic(&init_task); ... ``` `set_task_stack_end_magic` 函式會在 `init_task` 的堆疊空間頂端寫入一個特別的數值,用以檢測溢位 (overflow),而 `init_task` 定義於 [init/init_task.c](https://elixir.bootlin.com/linux/v6.9.3/source/init/init_task.c#L64),其程式碼註解提到: > Set up the first task table 那,什麼是任務 (task) 呢?解釋之前,我們需要知道,Linux 核心和使用者空間對 PID 的定義存在分歧:在 Linux 核心模式中,執行單位是 `task_struct`,它代表一個執行緒,而非整個行程。 ![image](https://hackmd.io/_uploads/HkHt6-NrR.png) 對核心來說,PID 數值所識別的是一個任務,而非一個行程,而且 `task_struct.pid` 只是該執行緒的識別碼。Linux 核心要以某種方式,來描述使用者空間中的行程,執行緒被歸類到執行緒群組 (thread group) 中,後者由執行緒群組標識別碼 (TGID) 為 Linux 核心所識別。使用者空間稱執行緒群組為行程,因此核心中的 TGID 在使用者空間中被稱為 PID。更令人困惑的是,TGID 和 PID 的數值通常是相同的。 當建立一個新的執行緒群組時 (例如執行 `fork` 系統呼叫),新的執行緒會獲得一個新的執行緒 ID,而該 ID 也成為新群組的 TGID。因此對於單執行緒的行程來說,核心的 TID 和 TGID 是相同的,當我們向 Linux 核心查詢該執行單元在核心或使用者空間的 PID 時,就會得到相同的數值。不過,一旦產生更多執行緒,前述等效性就會打破:新執行緒獲得自己的執行緒 ID (即 Linux 核心所稱的 PID),但繼承其親代執行緒群組的 ID (即使用者空間所稱的 PID)。 一般來說,TID 和 TGID 有時會如上所述相同,但也可能建構出一個新的使用者空間行程,其單個初始執行緒的 TID 與其 TGID 不匹配。若在多執行緒的行程中,從初始執行緒以外的任何執行緒中呼叫 `execve()`,Linux 核心將棄置所有其他執行緒,並使呼叫 `exec` 的執行緒成為執行緒群組的主導 (leader)。該執行緒的 TID 不會改變,於是新行程將在 TID 與 TGID 不匹配的執行緒上執行。 在 Linux 核心的起始階段,狀況很單純:`init_task` 就代表著 PID 0,後者是 ID 為 0 的執行緒 (即核心所稱的 PID),也是是 ID 為 0 的群組中唯一的執行緒 (即使用者空間所稱的 PID),並且此刻還不存在子 PID namespce 命名空間,因此 `init_task` 不會有其他的識別編號。 > 延伸閱讀: [Linux 核心設計: 不只挑選任務的排程器](https://hackmd.io/@sysprog/linux-scheduler) 不幸的是,稍後情況會變得更混亂,因為執行緒群組編號 0 會增長更多執行緒,所以從使用者空間的角度來看,我們會有一個包含多個執行緒的 PID 0 行程,其中一個執行緒具有 TID 0。 為了減少混淆,本文使用「任務」或「執行緒」來表示單個執行緒,即由 `task_struct` 描述的執行單元,使用「執行緒群組」來表示使用者空間所稱的行程。 ## 探索 `init_task` 前述 `init_task` 是我們正在尋找的 PID 0,也就是執行緒群組 ID 0 裡頭的執行緒 ID 0。那 `init_task` 如何被 Linux 核心執行呢? 首先,我們知道 Linux 核心啟動的早期階段,僅有 `init_task` 這個執行緒在執行,於是 `init_task` 就成為第一個任務,也就是第一個執行緒,使用 `init_stack` 作為其堆疊,它的任務狀態是 `TASK_RUNNING`,這意味著它要不正在執行,要不是可執行狀態並等待 CPU 時間。Linux核心的 CPU 排程器尚未初始化,因此此時不會有其他可執行的任務,`init_task` 確實是執行 `start_kernel` 的初始執行緒。 `init_task` 是 Linux 核心所有行程和執行緒的 `task_struct` 雛形。核心初始化過程中,靜態定義一個 `task_struct` 的實例 (instance),名為 `init_task`。在 Linux 核心初始化的後期,呼叫 `rest_init()` 函式來建立 `init` 任務和 `kthreadd` 核心執行緒,其中 `init` 任務最終會找到並執行 `init` 程式,成為所有使用者空間行程的祖先行程,以下用 `pstree` 命令觀察: (`systemd` 即為 init 行程) ``` systemd-+-ModemManager---2*[{ModemManager}] |-4*[agetty] |-cron |-dbus-daemon |-irqbalance---{irqbalance} ... ``` 一旦 Linux 核心在初始化階段,完成 `init` 和 `kthreadd` 核心執行緒的建立後,核心會發生 CPU 排程,此時核心將使用 `init_task` 作為其 `task_struct` 結構體描述子,`init_task` 不可能藉由 `fork`建立,而是 Linux 核心靜態建立的,也是唯一不透過 `kernel_thread` 函式建立的核心執行緒。 在一系列 Linux 核心早期初始化尾聲,[`sched_init` 函式](https://elixir.bootlin.com/linux/v6.9.3/source/kernel/sched/core.c#L9921)粉墨登場,後者初始化 CPU 排程器,以下摘錄和 idle 任務相關的程式碼: ```c /* * The idle task doesn't need the kthread struct to * function, but it is dressed up as a per-CPU * kthread and thus needs to play the part if we want * to avoid special-casing it in code that deals with * per-CPU kthreads. */ WARN_ON(!set_kthread_struct(current)); /* * Make us the idle thread. Technically, schedule() * should not be called from this thread, however * somewhere below it might be, but because we are the * idle thread, we just pick up running again when this * runqueue becomes "idle". */ init_idle(current, smp_processor_id()); ``` 第一段的註解提及目前執行的執行緒為 "idle task",說明這是特別的 Linux 核心執行緒:絕大多數的核心執行緒由 `kthreadd` 執行,也就是稍早我們從 `ps -eaf` 命令見到的 PID 2,但後者目前尚未存在。 第二段程式碼明確告訴 CPU 排程器目前運行的執行緒是引導 CPU 核心的 "idle thread"。`current` 是指向目前執行的 `task_struct` 的指標,此刻,它指向 `init_task`。 `start_kernel` 函式剩餘的初始化程式碼和本主題無關,因此我們可直接跳到 `rest_init` 的呼叫,後者的目的是: * 產生任務 1,這將成為使用者空間中的 init 行程 * 產生任務 2 給 `kthreadd`,管理所有隨後的核心執行緒 我們將繼續關注任務 1,儘管它最終會成為使用者空間中的 PID 1,起初它會執行 `kernel_init`,但此刻尚未到那一步。這些新任務已存在且被 CPU 排程器所識別,不過尚未運行,因為 Linux 核心仍未要求 CPU 排程器執行其任務。 這階段稱作核心啟動階段 (kernel startup stage),該階段的尾聲才會建立 `init` 行程,以下程式碼列表取自 Linux v6.9 的 [init/main.c](https://elixir.bootlin.com/linux/v6.9.3/source/init/main.c#L693): ```c static noinline void __ref __noreturn rest_init(void) { ... pid = kernel_thread(kthreadd, NULL, NULL, CLONE_FS | CLONE_FILES); ... /* * The boot idle thread must execute schedule() * at least once to get things moving: */ schedule_preempt_disabled(); /* Call into cpu_idle with preempt disabled */ cpu_startup_entry(CPUHP_ONLINE); } ``` `kernel_thread` 函式建立 PID 2,亦即 `kthreadd` 且最後呼叫 [`cpu_startup_entry` 函式](https://elixir.bootlin.com/linux/v6.9.3/source/kernel/sched/idle.c#L424),進入無窮迴圈,呼叫 `do_idle` 函式。 ```c void cpu_startup_entry(enum cpuhp_state state) { current->flags |= PF_IDLE; arch_cpu_idle_prepare(); cpuhp_online_idle(state); while (1) do_idle(); } ``` 現在,我們成為 bootstrap core 的 idle 任務。起初 Linux 不會讓 CPU 進入休眠狀態,因為還有其他可執行的任務,也就是上述任務 1 和任務 2。隨後跳到 `do_idle` 的底部,進入 [`schedule_idle` 函式](https://elixir.bootlin.com/linux/v6.9.3/source/kernel/sched/core.c#L6853)。 CPU 排程器至此終於運作,自任務 0 切換出去。`kthreadd` 在任務 2 中進行一些初始化,然後再次讓出 CPU,直到有其他請求建立核心執行緒。我們繼續關注任務 1,這更有趣。 任務 1 從 [`kernel_init` 函式](https://elixir.bootlin.com/linux/v6.9.3/source/init/main.c#L1435)開始,進行更多的核心初始化,包括啟動所有裝置驅動程式並掛載 initramfs 或最終的 root file system (簡稱 rootfs)。最後,它呼叫 [`run_init_process` 函式](https://elixir.bootlin.com/linux/v6.9.3/source/init/main.c#L1354),準備離開核心模式並執行使用者空間的 init 程式。以下程式碼列表取自 Linux v6.9 的 [init/main.c](https://elixir.bootlin.com/linux/v6.9.3/source/init/main.c#L1435): ```c static int __ref kernel_init(void *unused) { ... /* * We try each of these until one succeeds. * * The Bourne shell can be used instead of init if we are * trying to recover a really broken machine. */ if (execute_command) { ret = run_init_process(execute_command); if (!ret) return 0; panic("Requested init %s failed (error %d).", execute_command, ret); } ... /* * We try each of these until one succeeds. * * The Bourne shell can be used instead of init if we are * trying to recover a really broken machine. */ ... if (!try_to_run_init_process("/sbin/init") || !try_to_run_init_process("/etc/init") || !try_to_run_init_process("/bin/init") || !try_to_run_init_process("/bin/sh")) return 0; panic("No working init found. Try passing init= option to kernel. " "See Linux Documentation/admin-guide/init.rst for guidance."); } ``` `kernel_init` 函式最後執行 `exec` 系統呼叫,作為 `init` 行程的映像載入到主記憶體。Linux 核心會尋找合適的 `init` 行程來執行,該行程會設定使用者空間和使用者環境所需的行程,最終讓使用者得以操作。Linux 核心本身隨後會進入閒置 (idle) 狀態,等待其他行程的呼叫。 若 [init(1)](https://man7.org/linux/man-pages/man1/init.1.html) 詢問 Linux 核心它是誰,核心會說,它是執行緒編號 1,是執行緒群組編號 1 的一部分。也就是說,在使用者空間的術語中,[init(1)](https://man7.org/linux/man-pages/man1/init.1.html) 是 PID 1 中的執行緒 1。 任務 1 或 PID 1 在變成我們熟悉的使用者空間行程之前,需要處理大量的核心工作,換言之,Linux 核心啟動過的部分工作完成於 PID 1,這引來新問題:為何不在任務 0 中完成前述核心準備,就像 Linux 核心初始化的早期工作? ### 多核處理器系統 在 Linux 核心中,計時器 (timer) 是實作 CPU 排程和資源管理的重要機制。高解析度計時器 (hrtimer)則是 Linux 核心提供的高精度計時機制,允許系統設定和管理精確到奈秒 (nanosecond) 等級的計時事件。當高解析度計時器觸發時,[`hrtimer_wakeup` 函式](https://elixir.bootlin.com/linux/v6.9.3/source/kernel/time/hrtimer.c#L1938)被呼叫,用於喚醒之前被阻塞的行程。 當行程需要等待特定時間後再繼續執行時,核心會將其狀態設定為睡眠 (sleeping),並將其從可執行的佇列中移走。同時,核心會設定一個高解析度計時器來監控該等待時間。當計時器到期時,`hrtimer_wakeup` 會被呼叫,將該行程從睡眠狀態轉移到可執行狀態,並將其重新加入到可執行的佇列中。該機制確保行程在需要時能夠準時被喚醒,且不會因為長時間的等待而影響系統的性能和響應速度。 以下展示藉由 [bpftrace](https://github.com/bpftrace/bpftrace) 來觀察[`hrtimer_wakeup` 函式](https://elixir.bootlin.com/linux/v6.9.3/source/kernel/time/hrtimer.c#L1938)被呼叫的統計: ```shell $ sudo bpftrace -e "kprobe:hrtimer_wakeup { @[comm] = count(); }" ``` 大約等待 3 秒,按下 Ctrl-C,參考的執行輸出如下: ``` Attaching 1 probe... ^C @[swapper/3]: 130 @[swapper/2]: 160 @[swapper/1]: 171 @[swapper/0]: 229 ``` 從上方統計中,我們可見到 4 個名為 `swapper` 的任務,分別是`swapper/0`, `swapper/1`, `swapper/2` 和 `swapper/3`,它們的 PID 都是 0,參考的執行環境有 4 個處理器核,而每個處理器核都有對應的 `swapper` 任務。 `swapper` 確保 CPU 在閒置時不會沒有任務可執行。在多核處理器系統中,每個處理器核都有自己的 `swapper` 任務,以便有效管理多個處理器的資源和配置。該機制使得 Linux 系統能夠高效地利用多核處理器的計算能力,並在不同的 CPU 核之間平衡工作負載,從而提升整體性能和運行效率。 也可以用 [Ftrace](https://www.kernel.org/doc/Documentation/trace/ftrace.txt) 觀察 CPU 排程狀況,以 CPU2 為例: (假設已切換到 `root` 使用者) ```bash $ cd /sys/kernel/debug/tracing $ echo 1 > events/sched/enable ; sleep 2 ; echo 0 > events/sched/enable $ cat per_cpu/cpu2/trace ``` 參考執行輸出: ``` # entries-in-buffer/entries-written: 960/960 #P:4 # # _-----=> irqs-off/BH-disabled # / _----=> need-resched # | / _---=> hardirq/softirq # || / _--=> preempt-depth # ||| / _-=> migrate-disable # |||| / delay # TASK-PID CPU# ||||| TIMESTAMP FUNCTION # | | | ||||| | | <idle>-0 [002] dNh2. 43323.411025: sched_wakeup: comm=bash pid=31424 prio=120 target_cpu=002 <idle>-0 [002] d..2. 43323.411061: sched_switch: prev_comm=swapper/2 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=bash next_pid=31424 next_prio=120 sleep-31424 [002] d..2. 43323.411306: sched_stat_runtime: comm=sleep pid=31424 runtime=286542 [ns] vruntime=58078545512 [ns] sleep-31424 [002] d..2. 43323.411311: sched_switch: prev_comm=sleep prev_pid=31424 prev_prio=120 prev_state=D ==> next_comm=swapper/2 next_pid=0 next_prio=120 <idle>-0 [002] d.h5. 43323.411505: sched_waking: comm=sleep pid=31424 prio=120 target_cpu=002 <idle>-0 [002] dNh6. 43323.411511: sched_wakeup: comm=sleep pid=31424 prio=120 target_cpu=002 <idle>-0 [002] d..2. 43323.411546: sched_switch: prev_comm=swapper/2 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=sleep next_pid=31424 next_prio=120 sleep-31424 [002] ..... 43323.411620: sched_process_exec: filename=/usr/bin/sleep pid=31424 old_pid=31424 sleep-31424 [002] d..2. 43323.412297: sched_stat_runtime: comm=sleep pid=31424 runtime=789083 [ns] vruntime=58079334595 [ns] sleep-31424 [002] d..2. 43323.412302: sched_switch: prev_comm=sleep prev_pid=31424 prev_prio=120 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120 ... ``` 不難見到 `idle` 任務及 `swapper` 的身影。 之前探討 Linux 核心的原始程式碼,只考慮單執行緒的執行狀況,當我們初始化 CPU 排程器時,我們明確告訴它將任務 0 固定在 bootstrap core,這樣會發生什麼? 稍早提到的任務 1 就是答案:在多核處理器環境中,`kernel_init` 函式首先啟動所有其他 CPU 核,意味著在 `kernel_init` 中發生的大部分啟動過程可運用所有有效的 CPU,而非只能用於單一執行緒。啟動 CPU 核相當複雜,但最重要的是呼叫 [`smp_init` 函式](https://elixir.bootlin.com/linux/v6.9.3/source/kernel/smp.c#L971),後者會依次為每個非 bootstrap core 呼叫 [`fork_idle` 函式](https://elixir.bootlin.com/linux/v6.9.3/source/kernel/fork.c#L2705),建立新的 idle 執行緒並將其固定在該處理器核上。 所有處理器核上執行的 idle 執行緒共享相同的身份,也就是執行緒 ID 0 和執行緒群組 ID 0。 [`fork_idle` 函式](https://elixir.bootlin.com/linux/v6.9.3/source/kernel/fork.c#L2705),建立新的 idle 執行緒並將其固定在該處理器核上。 呼叫 [`copy_process` 函式](https://elixir.bootlin.com/linux/v6.9.3/source/kernel/fork.c#L2134),以目前執行的任務為副本,建立新任務,通常這會為新任務配置新的 TID。然而,有個例外狀況:若呼叫者表示它正在建立一個 idle 任務,則跳過新 `struct pid` 的配置。 ```c __latent_entropy struct task_struct *copy_process( struct pid *pid, int trace, int node, struct kernel_clone_args *args) { ... if (pid != &init_struct_pid) { pid = alloc_pid(p->nsproxy->pid_ns_for_children, args->set_tid, args->set_tid_size); if (IS_ERR(pid)) { retval = PTR_ERR(pid); goto bad_fork_cleanup_thread; } } ... ``` 隨後 `fork_idle` 呼叫 `init_idle_pids`,以重置所有任務的識別碼,使其與 `init_struct_pid` 匹配,即 `init_task` 的識別。因此,每個 CPU 核上的每個 idle 任務都與我們在早期核心啟動過程中跟隨的 `init_task` 共享識別,根據 Linux 核心和使用者空間對 PID 的定義,它們都具有 PID 0。 之後,`smp_init` 執行 [`bringup_nonboot_cpus` 函式](https://elixir.bootlin.com/linux/v6.9.3/source/kernel/cpu.c#L1908),進行針對處理器的操作以喚醒處理器核。隨著每個處理器核的啟動,它會進行一些架構相關的設定,使其可用並執行 `cpu_startup_entry` 和 `do_idle`,一如 bootstrap core 對任務 0 所做的事。現在所有 CPU 核都已啟動並可以運行任務,`kernel_init` 函式繼續進行剩餘的啟動過程,複製 `init_task`,為每個 CPU 建立 `swapper/<cpu>` 執行緒 (`INIT_TASK_COMM`),最終進入 idle 迴圈。 ## PID 0 確實存在 PID 0 執行 Linux 核心早期的初始化,然後成為 bootstrap core 的 idle 任務,並作為 CPU 排程和電源管理的配角。 打從第一版 UNIX以來,PID 0 一直在做上述工作,雖然實作手法已大相逕庭,但基本原理卻相似。對 Linux 核心來說,PID 0 與記憶體管理無關,儘管早期的 UNIX 核心在行程排程中進行附帶的記憶體管理,不過 PID 0 幾十年前就變成現在的樣貌。 Linux 核心解讀 PID 不是容易的事,因為使用者空間和核心對 PID 的定義不同 ── 對 Linux 核心來說是 TID,而對使用者空間來說是 TGID,而且 Linux 不允許藉由系統呼叫,讓使用者得以操作 PID 0。 在多核 Linux 系統上,每個 CPU 核都有一個 idle 執行緒。所有這些 idle 執行緒都是執行緒群組 0 的一部分,使用者空間會稱之為 PID 0。它們在核心中也是特例,並且都共享單一的執行緒 ID 0。 ## 參考資料 * [What is PID 0?](https://blog.dave.tf/post/linux-pid0/) * [The Linux Process Journey](https://thelearningjourneyebooks.com/)