[英]Why does fork() fail on MacOs Big Sur if the executable that runs it is deleted?
如果刪除正在運行的進程的可執行文件,我注意到fork
失敗,子進程永遠不會執行。
例如,考慮下面的代碼:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void) {
sleep(5);
pid_t forkResult;
forkResult = fork();
printf("after fork %d \n", forkResult);
return 0;
}
如果我在調用fork
之前編譯它並刪除生成的可執行文件,我永遠不會看到fork
返回 0 的 pid,這意味着子進程永遠不會啟動。 我只有一台運行 Big Sur 的 Mac,所以不確定這是否適用於其他操作系統。
有誰知道為什么會這樣? 我的理解是一個可執行文件應該可以正常工作,即使它在運行時被刪除。
即使二進制文件被刪除,該過程仍應繼續的期望是正確的,但在macOS
的情況下並不完全正確。 該示例觸發了 macOS kernel 內部System Integrity Protection
( SIP
) 機制的副作用,但是在解釋到底發生了什么之前,我們需要進行一些實驗,這將有助於我們更好地理解整個場景。
為了演示發生了什么,我將示例修改為 9,而不是 fork,在 fork 之后,孩子將打印一條消息“我完成了”,等待 1 秒並通過打印0
作為 PID 退出. 父級將繼續計數到 14 並打印子 PID。 代碼如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void) {
for(int i=0; i <10; i++)
{
sleep(1);
printf("%i ", i);
}
pid_t forkResult;
forkResult = fork();
if (forkResult != 0) {
for(int i=10; i < 15; i++) {
sleep(1);
printf("%i ", i);
}
} else {
sleep(1);
printf("I am done ");
}
printf("after fork %d \n", forkResult);
return 0;
}
編譯后,我開始了正常的場景:
╰> ./a.out
0 1 2 3 4 5 6 7 8 9 I am done after fork 0
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 after fork 4385
因此,正常情況按預期工作。 我們看到從 0 到 9 的計數兩次的事實是由於在 fork 調用中完成的stdout
緩沖區的副本。
現在是做負面場景的時候了,我們將在啟動后等待 5 秒並刪除二進制文件。
╰> ./a.out & (sleep 5 && rm a.out)
[4] 8555
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 after fork 8677
[4] 8555 done ./a.out
我們看到 output 僅來自父級。 由於父母已經數到 14,並且顯示了孩子的有效 PID,但是孩子失蹤了,它從來沒有打印過任何東西。 因此,在執行fork()
之后子創建失敗,否則fork()
將收到錯誤而不是有效的PID
。 來自ktrace
的跟蹤顯示,孩子是在 pid 下創建並被喚醒的:
test5-ko.txt:2021-04-07 13:34:26.623783 +04 0.3 MACH_DISPATCH 1bc 0 84 4 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623783 +04 0.2 TMR_TimerCallEnter 9931ba49ead1bd17 0 330e7e4e9a59 41 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623783 +04 0.0(0.0) TMR_TimerCallEnter 9931ba49ead1bd17 0 330e7e4e9a59 0 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623783 +04 0.0 TMR_TimerCallEnter 9931ba49ead1bd17 0 330e7e4e9a59 0 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623854 +04 0.0 imp_thread_qos_and_relprio 88775d 20000 20200 6 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623854 +04 0.0 imp_update_thread 88775d 811200 140000100 1f 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623855 +04 0.1(0.8) imp_update_thread 88775d c15200 140000100 25 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623855 +04 0.0(1.1) imp_thread_qos_and_relprio 88775d 30000 20200 40 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623855 +04 0.0 imp_thread_qos_workq_override 88775d 30000 20200 0 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623855 +04 0.0 imp_update_thread 88775d c15200 140000100 25 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623855 +04 0.1(0.1) imp_update_thread 88775d c15200 140000100 25 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623855 +04 0.0(0.2) imp_thread_qos_workq_override 88775d 30000 20200 40 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623857 +04 1.3 TURNSTILE_turnstile_added_to_thread_heap 88775d 9931ba6049ddcc77 0 0 888065 2 a.out(8677)
test5-ko.txt:2021-04-07 13:34:26.623858 +04 1.0 MACH_MKRUNNABLE 88775d 25 0 5 888065 2 a.out(8677)
t
因此,孩子的進程使用MACH_MKRUNNABLE
進行調度,並使用MACH_DISPATCH
使其可運行。 這就是父級在fork()
之后獲得有效PID
的原因。
此外,正常情況下的ktrace
顯示進程已發出BSC_exit
並且發生了imp_task_terminated
系統調用,這是進程退出的正常方式。 但是,在我們刪除文件的第二種情況下,跟蹤不顯示BSC_exit
。 這意味着子節點被 kernel 終止,而不是正常終止。 我們知道終止發生在子節點被正確創建之后,因為父節點已經收到了有效的 PID 並且 PID 被設置為可運行的。
這使我們更接近於理解這里發生了什么。 但是,在我們得出結論之前,讓我們展示另一個更加“扭曲”的例子。
如果我們在啟動進程后替換文件系統上的二進制文件怎么辦?
這是回答這個問題的測試:我們將啟動該過程,刪除二進制文件並使用touch
在他的位置創建一個同名的空文件。
╰> ./a.out & (sleep 5 && rm a.out; touch a.out)
[1] 6264
0 1 2 3 4 5 6 7 8 9 I am done after fork 0
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 after fork 6851
[1] + 6722 done ./a.out
等一下,這行得通?? 這里發生了什么??!?
這個奇怪的例子為我們提供了重要的線索,可以幫助我們解釋發生了什么。
第三個示例有效而第二個示例失敗的原因揭示了這里發生的很多事情。 正如一開始提到的,我們正在絆倒SIP
的副作用,更准確地說是runtime protection
機制。
為保護系統完整性, SIP
將檢查運行進程的system protection
和special entitlement
。 來自蘋果文檔: ...當一個進程啟動時,kernel 檢查主要可執行文件是否在磁盤上受到保護或使用特殊的系統權利進行簽名。 如果其中任何一個為真,則設置一個標志以表示它受到保護以防修改。 kernel 拒絕任何附加到受保護進程的嘗試...
當我們從文件系統中刪除二進制文件時,保護機制無法識別子進程的類型,也無法識別特殊的系統權利,因為磁盤中缺少二進制文件。 這觸發了保護機制將此進程視為系統中的入侵者並終止它,因為我們沒有看到子進程的BSC_exit
。
在第三個示例中,當我們使用touch
在文件系統上創建虛擬條目時, SIP
能夠檢測到這不是特殊進程,也沒有特殊權利,並允許進程繼續。 這是一個非常可靠的跡象,表明我們在SIP
實時保護機制上跳閘。
為了證明是這種情況,我禁用了需要在恢復模式下重新啟動的SIP
並執行了測試
╰> csrutil status
System Integrity Protection status: disabled.
╰> ./a.out & (sleep 5 && rm a.out)
[1] 1504
0 1 2 3 4 5 6 7 8 9 I am done after fork 0
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 after fork 1626
因此,整個問題是由System Integrity Protection
引起的。 更多細節可以在文檔中找到
SIP
需要的只是在文件系統上有一個帶有進程名稱的文件,因此該機制可以運行驗證並決定允許子進程繼續執行。 這向我們表明,我們正在觀察副作用,而不是設計的行為,因為空文件甚至不是有效的dwarf
,但執行已繼續。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.