Try   HackMD

PID 0 行程之謎

資料整理: jserv

管中窺豹

UNIX 從第 4 版開始,就嘗試以 C 語言撰寫,而現代相容於 UNIX 的作業系統核心也多以 C 語言開發,例如 Linux, FreeBSD 和 macOS 底層的 XNU。依據 C 語言的慣例,索引值從零開始,這原則也適用於 UIDGID,不過作為 UNIX 首個使用者層級的行程,init (在 Linux 系統可能是 systemd) 的 PID 則從 1 開始呢?而且對照 Linux man pages 後,更讓這問題變得更費解 ── kill 系統呼叫提及:

If pid equals 0, then sig is sent to every process in the process group of the calling process.

這暗示著 PID 0 是相當特殊的案例,以至於 kill 系統呼叫無法直接處理該執行單元,且 fork 系統呼叫也提到:

On success, the PID of the child process is returned in the parent, and 0 is returned in the child.

PID 0 根本直接被 fork 系統呼叫跳過,0 這個數值被挪用在子行程判斷 fork 成功的依據。更甚者,2001 年才正式納入 POSIX 標準的 sched_setscheduler 系統呼叫,其行為是

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 是解析 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 函式程式碼:

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 系統呼叫的前世今生〉。從早期 UNIX 原始程式碼來看,行程表格的第 0 個進入點初始化作業系統核心,然後進入 sched 函式,顯然,其排程演算法非常簡單,sched 大部分程式處理將行程映像在核心記憶體和磁碟之間進行交換。此外,當沒有其他行程可運行時,名為 idle 的任務就會運行,其具有最低的優先級,因此在沒有其他行程可運行時,會被執行,這簡化排程的實作方式:不必擔心特殊情況「若沒有行程可執行會發生什麼?」,因為總有一個行程可運行,亦即 idle 任務。此外,可藉此計算每個行程使用的 CPU 時間。

這種置換形式的記憶體管理,讓 UNIX 效率不彰,於是在 UNIX System V R2V5 和 4BSD (1980 年代) 就將 swapper 更換為 demand paging 的虛擬記憶體管理機制,後者是 BSD 開發團隊向 Mach 微核心學習並改造的重大突破。

上述 UNIX 基本結構延續至今,並變得更複雜,當記憶體分頁 (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:

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,其程式碼註解提到:

Set up the first task table

那,什麼是任務 (task) 呢?解釋之前,我們需要知道,Linux 核心和使用者空間對 PID 的定義存在分歧:在 Linux 核心模式中,執行單位是 task_struct,它代表一個執行緒,而非整個行程。

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

對核心來說,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 核心設計: 不只挑選任務的排程器

不幸的是,稍後情況會變得更混亂,因為執行緒群組編號 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 核心在初始化階段,完成 initkthreadd 核心執行緒的建立後,核心會發生 CPU 排程,此時核心將使用 init_task 作為其 task_struct 結構體描述子,init_task 不可能藉由 fork建立,而是 Linux 核心靜態建立的,也是唯一不透過 kernel_thread 函式建立的核心執行緒。

在一系列 Linux 核心早期初始化尾聲,sched_init 函式粉墨登場,後者初始化 CPU 排程器,以下摘錄和 idle 任務相關的程式碼:

/*
 * 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:

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 函式,進入無窮迴圈,呼叫 do_idle 函式。

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 函式

CPU 排程器至此終於運作,自任務 0 切換出去。kthreadd 在任務 2 中進行一些初始化,然後再次讓出 CPU,直到有其他請求建立核心執行緒。我們繼續關注任務 1,這更有趣。

任務 1 從 kernel_init 函式開始,進行更多的核心初始化,包括啟動所有裝置驅動程式並掛載 initramfs 或最終的 root file system (簡稱 rootfs)。最後,它呼叫 run_init_process 函式,準備離開核心模式並執行使用者空間的 init 程式。以下程式碼列表取自 Linux v6.9 的 init/main.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) 詢問 Linux 核心它是誰,核心會說,它是執行緒編號 1,是執行緒群組編號 1 的一部分。也就是說,在使用者空間的術語中,init(1) 是 PID 1 中的執行緒 1。

任務 1 或 PID 1 在變成我們熟悉的使用者空間行程之前,需要處理大量的核心工作,換言之,Linux 核心啟動過的部分工作完成於 PID 1,這引來新問題:為何不在任務 0 中完成前述核心準備,就像 Linux 核心初始化的早期工作?

多核處理器系統

在 Linux 核心中,計時器 (timer) 是實作 CPU 排程和資源管理的重要機制。高解析度計時器 (hrtimer)則是 Linux 核心提供的高精度計時機制,允許系統設定和管理精確到奈秒 (nanosecond) 等級的計時事件。當高解析度計時器觸發時,hrtimer_wakeup 函式被呼叫,用於喚醒之前被阻塞的行程。

當行程需要等待特定時間後再繼續執行時,核心會將其狀態設定為睡眠 (sleeping),並將其從可執行的佇列中移走。同時,核心會設定一個高解析度計時器來監控該等待時間。當計時器到期時,hrtimer_wakeup 會被呼叫,將該行程從睡眠狀態轉移到可執行狀態,並將其重新加入到可執行的佇列中。該機制確保行程在需要時能夠準時被喚醒,且不會因為長時間的等待而影響系統的性能和響應速度。

以下展示藉由 bpftrace 來觀察hrtimer_wakeup 函式被呼叫的統計:

$ 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/2swapper/3,它們的 PID 都是 0,參考的執行環境有 4 個處理器核,而每個處理器核都有對應的 swapper 任務。

swapper 確保 CPU 在閒置時不會沒有任務可執行。在多核處理器系統中,每個處理器核都有自己的 swapper 任務,以便有效管理多個處理器的資源和配置。該機制使得 Linux 系統能夠高效地利用多核處理器的計算能力,並在不同的 CPU 核之間平衡工作負載,從而提升整體性能和運行效率。

也可以用 Ftrace 觀察 CPU 排程狀況,以 CPU2 為例: (假設已切換到 root 使用者)

$ 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 函式,後者會依次為每個非 bootstrap core 呼叫 fork_idle 函式,建立新的 idle 執行緒並將其固定在該處理器核上。

所有處理器核上執行的 idle 執行緒共享相同的身份,也就是執行緒 ID 0 和執行緒群組 ID 0。

fork_idle 函式,建立新的 idle 執行緒並將其固定在該處理器核上。
呼叫 copy_process 函式,以目前執行的任務為副本,建立新任務,通常這會為新任務配置新的 TID。然而,有個例外狀況:若呼叫者表示它正在建立一個 idle 任務,則跳過新 struct pid 的配置。

__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 函式,進行針對處理器的操作以喚醒處理器核。隨著每個處理器核的啟動,它會進行一些架構相關的設定,使其可用並執行 cpu_startup_entrydo_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。

參考資料