[譯] 如何殺死一個程式和它的所有子程式
- 原文地址:Killing a process and all of its descendants
- 原文作者:igor_sarcevic
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:江五渣
- 校對者:TokenJan,portandbridge
如何殺死一個程式和它的所有子程式
在類 Unix 系統中殺死程式比預期中更棘手。上週我在除錯一個在 Semaphore 中終止作業的問題。更具體地說,這是一個有關於在作業中終止正在執行的程式的問題。以下是我從中學到的要點:
- 類 Unix 作業系統有著複雜的程式間關係:父子程式、程式組、會話、會話的領導程式。但是,在 Linux 與 MacOS 等作業系統中,這其中的細節並不統一。符合 POSIX 的作業系統支援使用負 PID 向程式組傳送訊號。
- 使用系統呼叫向會話中的所有程式傳送訊號並非易事。
- 用 exec 啟動的子程式將繼承其父程式的訊號配置。例如,如果父程式忽略 SIGHUP 訊號,它的子程式也會忽略 SIGHUP 訊號。
- “孤兒程式組內發生了什麼”這一問題的答案並不簡單。
殺死父程式並不會同時殺死子程式
每個程式都有一個父程式。我們可以使用 pstree
或 ps
工具來觀察這一點。
# 啟動兩個虛擬程式
$ sleep 100 &
$ sleep 101 &
$ pstree -p
init(1)-+
|-bash(29051)-+-pstree(29251)
|-sleep(28919)
`-sleep(28964)
$ ps j -A
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 1 1 1 ? -1 Ss 0 0:03 /sbin/init
29051 1470 1470 29051 pts/2 2386 SN 1000 0:00 sleep 100
29051 1538 1538 29051 pts/2 2386 SN 1000 0:00 sleep 101
29051 2386 2386 29051 pts/2 2386 R+ 1000 0:00 ps j -A
1 29051 29051 29051 pts/2 2386 Ss 1000 0:00 -bash
複製程式碼
呼叫 ps
命令可以顯示 PID(程式 ID) 和 PPID(父程式 ID)。
我對父子程式間的關係有著錯誤的假設。我認為如果我殺死了父程式,那麼也會殺死它的所有子程式。然而這是錯誤的。相反,子程式將會成為孤兒程式,而 init 程式將重新成為它們的父程式。
讓我們看看通過終止 bash 程式(sleep 命令的當前父程式)來重建程式間的父子關係後發生了哪些變化。
$ kill 29051 # 殺死 bash 程式
$ pstree -A
init(1)-+
|-sleep(28919)
`-sleep(28965)
複製程式碼
於我而言,重新分配父程式的行為很奇怪。例如,當我使用 SSH 登入一臺伺服器,啟動一個程式,然後退出時,我啟動的程式將會被終止。我錯誤地認為這是 Linux 上的預設行為。當我離開一個 SSH 會話時,程式的終止與程式組、會話的領導程式和控制終端都有關。
什麼是程式組和會話領導程式?
讓我們再次觀察上述事例中 ps j
命令的輸出。
$ ps j -A
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 1 1 1 ? -1 Ss 0 0:03 /sbin/init
29051 1470 1470 29051 pts/2 2386 SN 1000 0:00 sleep 100
29051 1538 1538 29051 pts/2 2386 SN 1000 0:00 sleep 101
29051 2386 2386 29051 pts/2 2386 R+ 1000 0:00 ps j -A
1 29051 29051 29051 pts/2 2386 Ss 1000 0:00 -bash
複製程式碼
除了使用 PPID 和 PID 表示的父子程式關係外,程式間還有其他兩種關係:
- 用 PGID 表示的程式組
- 用 SID 表示的會話
我們可以在支援作業控制的 Shell 環境中觀察到程式組,例如 bash
和 zsh
,它們為每個管道命令都建立了一個程式組。程式組是一個或多個程式(通常與一個作業關聯)的集合,可以從同一個終端接收訊號。每個程式組都有一個唯一的程式組 ID。
# 啟動一個由 tail 和 grep 命令組成的程式組
$ tail -f /var/log/syslog | grep "CRON" &
$ ps j
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
29051 19701 19701 29051 pts/2 19784 SN 1000 0:00 tail -f /var/log/syslog
29051 19702 19701 29051 pts/2 19784 SN 1000 0:00 grep CRON
29051 19784 19784 29051 pts/2 19784 R+ 1000 0:00 ps j
29050 29051 29051 29051 pts/2 19784 Ss 1000 0:00 -bash
複製程式碼
請注意,在前半段中,tail
和 grep
的 PGID 是相同的。
會話是程式組的集合,通常由一個控制終端和一個會話領導程式組成。如果會話中有一個控制終端,它就具有單個前臺程式組,除了該控制終端,會話中的所有其他程式組都是後臺程式組。
並非所有的 bash 程式都是會話,但是當你使用 SSH 登入一臺遠端伺服器時,你通常會得到一個會話。當 bash 作為會話領導程式執行時,它將 SIGHUP 訊號傳播給它的子程式。SIGHUP 訊號的傳播方式就是我一直以來堅信子程式會與父程式一起消亡的核心原因。
在 Unix 中會話的實現並非一致
在上述事例中,你可以注意到 SID (程式的會話 ID)出現的位置。它是會話中所有程式共享的 ID。
但是,你需要記住,並非所有的 Unix 系統都遵循這一實現。單一 UNIX 規範只討論“會話領導程式”,沒有類似於程式 ID 或程式組 ID 的“會話 ID”。會話領導程式是一個具有唯一程式 ID 的單程式,因此我們可以討論的會話 ID 是會話領導者的程式 ID。
System V Release 4 引入了會話 ID。
實際上,這意味著你能在 Linux 上通過 ps
命令獲取會話 ID,但是在 BSD 及其變體(如 MacOS)上,會話 ID 並不存在,或始終為零。
殺死程式組或會話中的所有程式
我們可以使用該 PGID,通過 kill 命令向整個程式組傳送訊號:
$ kill -SIGTERM -- -19701
複製程式碼
我們用一個負數 -19701
向程式組傳送訊號。如果我們傳遞的是一個正數,這個數將被視為程式 ID 用於終止程式。如果我們傳遞的是一個負數,它被視為 PGID,用於終止整個程式組。
負數來自系統呼叫的直接定義。
殺死會話中的所有程式與之完全不同。如我們在前一節說到的,有些系統沒有會話 ID 的概念。即使是具有會話 ID 的系統,例如 Linux,也沒有提供系統呼叫來終止會話中的所有程式。你需要遍歷 /proc
輸出的程式樹,收集所有的 SID,然後一一終止程式。
Pgrep 實現了遍歷、收集並通過會話 ID 殺死程式的演演算法。使用以下命令:
pkill -s <SID>
複製程式碼
被 nohup 忽略的訊號傳播到子程式
被忽略的訊號,就像是被 nohup
忽略的訊號那樣,都被傳播到程式的所有子程式中。這種訊號傳播方式就是我上週在 bug 排查中遇到的最終瓶頸。
我的程式是用於執行 bash 命令的代理程式,而我在該程式中驗證到的是,我已經建立了一個具有控制終端的 bash 會話。該控制終端是 bash 會話中其他啟動程式的會話領導程式。我的程式樹如下所示:
agent -+
+- bash (session leader) -+
| - process1
| - process2
複製程式碼
我假設,當我使用 SIGHUP 殺死 bash 會話時,它的子程式也會同時終止。對代理的整合測試也證明瞭這一點。
但是,我忽略了這個代理是以 nohup
啟動的。當你使用 exec
啟動子程式時,就像我們在代理中啟動 bash 程式一樣,它會從它的父程式繼承訊號狀態。
最後一個結論使我驚訝萬分。
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。