SIGCHLD
信號
使用wait
和waitpid
函數可以有效地清理僵尸進程。父進程可以選擇阻塞等待,直到子進程結束;或者采用非阻塞的方式,通過輪詢檢查是否有子進程需要被回收。
然而,無論是選擇阻塞等待還是非阻塞的輪詢方式,父進程與子進程之間都無法實現真正的異步執行,因為父進程仍需“分心”來管理子進程的狀態。
當子進程終止時,會向父進程發送一個SIGCHLD
信號,這個信號的默認行為是被忽略。不過,父進程可以通過設置自定義的SIGCHLD
信號處理函數來改變這一行為。這樣,父進程就可以專注于自己的任務,無需直接管理子進程的狀態。一旦子進程終止,它會通知父進程,父進程只需要在其信號處理函數中調用wait
或waitpid
來清理子進程即可。
因此,我們能夠通過自定義處理子進程發出的SIGCHLD
信號,在接收到該信號時利用waitpid
回收子進程資源。這種方法避免了主動等待子進程的結束,使得父子進程之間能夠更加高效地異步執行。
演示代碼如下:因為需要在函數 handler
中使用子進程 id,因此定義了一個全局變量 id
其實還有一種方法不用傳id也不用定義全局變量:waitpid(-1, nullptr, 0);
-1 表示回收該父進程下的任意一個子進程
當
pid
參數為-1
時,waitpid
函數會等待任何一個子進程的狀態變化。這意味著它會捕獲任何已經終止的子進程,并回收其資源。這對于處理多個子進程的情況非常有用,因為父進程不需要知道具體是哪個子進程終止了。
#include<iostream> // 引入輸入輸出流庫
#include<signal.h> // 引入信號處理庫
#include<sys/wait.h> // 引入等待子進程狀態改變的函數庫
#include<sys/types.h> // 引入系統類型定義
#include<unistd.h> // 引入Unix標準函數庫pid_t id; // 定義全局變量id,用于存儲子進程ID// 定義信號處理函數
void handler(int signum)
{waitpid(id, nullptr, 0); // 等待子進程結束,回收子進程資源std::cout << "子進程退出, 我也退出了" << '\n'; // 輸出子進程已退出的信息// 當接收到信號時,調用raise給自己發送9號信號(SIGKILL),強制終止進程raise(9);
}int main()
{id = fork(); // 創建子進程if(id < 0){perror("fork"); // 如果fork失敗,輸出錯誤信息return 1; // 返回錯誤碼1}// 子進程邏輯if(id == 0){std::cout << "I am 子 process" << '\n'; // 子進程輸出標識信息sleep(2); // 子進程暫停2秒exit(0); // 子進程正常退出}// 父進程邏輯else if (id > 0){std::cout << "I am 父 process" << '\n'; // 父進程輸出標識信息signal(SIGCHLD, handler); // 設置SIGCHLD信號的處理函數為handlerint cnt = 0; // 初始化計數器while(1){ sleep(1); // 每秒暫停1秒std::cout << "cnt = " << cnt++ << '\n'; // 輸出當前計數值}} return 0; // 程序正常結束
}
運行結果如下:
問題一:如果同時多個子進程退出,是否會全部回收
但是這樣通過信號回收子進程是有一定風險的!
因為信號是通過 pending
位圖保存的,當一個父進程同時有多個子進程同時退出,同時發送 SIGCHLD
信號,則位圖不能記錄信號接收數量,就大概率會遺漏處理某些子進程,導致多個子進程僵尸的情況
驗證如下:
#include <iostream> // 引入輸入輸出流庫
#include <signal.h> // 引入信號處理庫
#include <sys/wait.h> // 引入等待子進程狀態改變的函數庫
#include <sys/types.h> // 引入系統類型定義
#include <unistd.h> // 引入Unix標準函數庫// 定義信號處理函數
void handler(int signum)
{pid_t id = waitpid(-1, nullptr, 0); // 等待任意一個子進程結束,回收其資源std::cout << "回收子進程 id : " << id << '\n'; // 輸出回收的子進程ID
}int main()
{pid_t id; // 定義變量id,用于存儲子進程ID// 循環創建15個子進程for (int i = 1; i <= 15; ++i){id = fork(); // 創建子進程if (id < 0){perror("fork"); // 如果fork失敗,輸出錯誤信息return 1; // 返回錯誤碼1}// 子進程邏輯if (id == 0){std::cout << "I am 子 process" << '\n'; // 子進程輸出標識信息sleep(2); // 子進程暫停2秒exit(0); // 子進程正常退出}}// 父進程邏輯if (id > 0){std::cout << "I am 父 process" << '\n'; // 父進程輸出標識信息signal(SIGCHLD, handler); // 設置SIGCHLD信號的處理函數為handler,當子進程結束時會觸發此函數int cnt = 0; // 初始化計數器while (1){sleep(1); // 每秒暫停1秒std::cout << "cnt = " << cnt++ << '\n'; // 輸出當前計數值}}return 0; // 程序正常結束
}
運行結果如下:不少子進程沒有被回收,而是變成了僵尸進程
解決辦法:循環等待回收子進程,否則退出
演示代碼如下:
#include <iostream>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>void handler(int signum)
{while (true){pid_t id = waitpid(-1, nullptr, 0);if(id > 0){std::cout << "回收子進程 id : " << id << '\n';}else if(id < 0){std::cout << "回收完畢, 暫時結束回收\n";break;}}
}int main()
{pid_t id;for (int i = 1; i <= 15; ++i){id = fork();if (id < 0){perror("fork");return 1;}// 子進程if (id == 0){std::cout << "I am 子 process" << '\n';sleep(2);exit(0);}}// 父進程if (id > 0){std::cout << "I am 父 process" << '\n';signal(SIGCHLD, handler);int cnt = 0;while (1){sleep(1);std::cout << "cnt = " << cnt++ << '\n';}}return 0;
}
運行結果如下:自己可以去查詢,可以確定當前沒有僵尸子進程
問題二:如果有子進程不退出,問題一中的循環wait,是否會退出循環
演示代碼:
// 創建一個不退出的子進程
id = fork();
if (id == 0)
{std::cout << "I am 不退出的子進程" << '\n';sleep(6);
}
結果就是 循環沒退出,因為 waitpid
是阻塞式等待,會等待子進程退出,因為該子進程不退出則循環不退出一直阻塞等待
因此需要換成非阻塞式等待,同時當 waitpid
的返回值為 0,說明當前沒有退出的子進程,則此時可以主動退出循環
pid_t id = waitpid(-1, nullptr, WNOHANG);
#include <iostream>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>void handler(int signum)
{while (true){pid_t id = waitpid(-1, nullptr, WNOHANG);if (id > 0){std::cout << "回收子進程 id : " << id << '\n';}else if(id == 0) // 表示沒有子進程退出了(注意是沒有退出的子進程了, 不是沒有子進程){std::cout << "暫時沒有子進程退出\n";break;}else if (id < 0) // 表示沒有子進程了{std::cout << "waitpid error\n";break;}}
}int main()
{pid_t id;for (int i = 1; i <= 15; ++i){id = fork();if (id < 0){perror("fork");return 1;}// 子進程if (id == 0){std::cout << "I am 子 process" << '\n';sleep(2);exit(0);}}// 創建一個不退出的子進程id = fork();if (id == 0){std::cout << "I am 不退出的子進程" << '\n';sleep(6); // 時間長點, 模擬短時間內不退出}// 父進程if (id > 0){std::cout << "I am 父 process" << '\n';signal(SIGCHLD, handler);int cnt = 0;while (1){sleep(1);std::cout << "cnt = " << cnt++ << '\n';}}return 0;
}
運行結果如下
waitpid
系統調用的工作原理如下:
- 阻塞等待:當
waitpid
被調用時,如果當前沒有符合條件的已退出子進程,內核會讓父進程進入阻塞狀態。這意味著父進程會被掛起,不再占用CPU時間,直到有子進程的狀態發生變化(通常是退出)。- 非阻塞等待:如果
waitpid
調用時傳遞了WNOHANG
選項,內核會立即返回,即使沒有子進程退出。在這種情況下,waitpid
不會阻塞父進程。- 狀態變化通知:當一個子進程退出時,內核會檢查該子進程的父進程是否正在等待子進程的狀態變化。如果是,內核會喚醒父進程,使其從
waitpid
調用中返回,并傳遞子進程的退出狀態。- 資源回收:父進程通過
waitpid
獲得子進程的退出狀態后,內核會釋放子進程占用的資源,防止子進程變成僵尸進程。
意思是:父進程使用waitpid
系統調用時,若為阻塞等待,則OS將該父進程掛起(即阻塞),當目標子進程退出時,若該父進程正處于等待子進程退出的狀態,則OS會傳遞子進程退出狀態信息并使父進程退出阻塞狀態(即使其從 waitpid
調用中返回)
子進程退出,OS是如何知道的,是因為OS需要輪詢子進程的狀態嗎
當然不是OS輪詢,前面講解過 OS 就是一個躺在中斷向量表上的一個代碼塊,OS的運行基本靠中斷,因此進程退出也是通過中斷通知OS,使其執行相應的后續”善后“工作
意思是子進程退出時,會向OS發送軟件中斷,此時進入內核態,執行該中斷對應的中斷處理例程:即更新子進程的 PCB,將子進程的狀態標記為“已退出”(Zombie 狀態),生成一個 SIGCHLD 信號并發送給父進程
子進程退出的詳細過程
- 子進程調用
exit
或exit_group
系統調用:
- 子進程在調用
exit
或exit_group
系統調用時,會進入內核態。
- 不是子進程退出子進程發送的軟件中斷,而是子進程在調用 exit 或 exit_group 系統調用觸發的軟件中斷
- 進入內核態:
- 當子進程調用
exit
或exit_group
時,控制權轉移到內核,進入內核態。- 內核會執行相應的中斷處理例程(中斷服務程序)。
- 中斷處理例程:
- 內核的中斷處理例程會執行以下操作:
- 更新子進程的 PCB:內核會更新子進程的進程控制塊(PCB),將子進程的狀態標記為“已退出”(Zombie 狀態)。
- 生成
SIGCHLD
信號:內核會生成一個SIGCHLD
信號并發送給父進程。- 父進程接收
SIGCHLD
信號:
- 父進程接收到
SIGCHLD
信號后,會調用預先注冊的信號處理函數(如handler
)默認為忽略
主動忽略子進程的 SIGCHLD
Linux下,將SIGCHLD的處理動作置為SIG IGN,這樣fork出來的子進程在終止時會自動清理掉
由于UNIX 的歷史原因,要想不產?僵?進程還有另外?種辦法:?進程調 ?sigaction將
SIGCHLD的處理動作置為SIG_IGN,這樣fork出來的?進程在終?時會?動清理掉,不 會產?僵?進程,
也不會通知?進程。系統默認的忽略動作和???sigaction函數?定義的忽略 通常是沒有區別的,但這
是?個特例。此?法對于Linux可?,但不保證在其它UNIX系統上都可 ?。
signal(SIGCHLD, SIG_IGN);
底層原理:
父進程未調用
waitpid
的情況
- 子進程退出:
- 子進程調用
exit
或exit_group
系統調用,進入內核態。- 內核更新子進程的 PCB,將其狀態標記為“已退出”(Zombie 狀態)。
- 內核生成
SIGCHLD
信號并發送給父進程。- 父進程處理
SIGCHLD
信號:
- 如果父進程注冊了
SIGCHLD
信號處理函數(如handler
),內核會調用該處理函數。- 在信號處理函數中,父進程可以調用
waitpid
來獲取子進程的退出狀態并釋放資源。- 父進程忽略
SIGCHLD
信號:
- 如果父進程將
SIGCHLD
信號的處理動作設置為SIG_IGN
,內核會自動回收子進程的資源,子進程不會變成僵尸進程。- 這意味著父進程不需要顯式調用
wait
或waitpid
來回收子進程的資源。
問題:父進程忽略了該信號,內核如何知道父進程忽略了,然后進行的自動回收子進程的資源
內核記錄信號處理動作:
內核會記錄每個進程的信號處理動作。當父進程調用
signal
或sigaction
設置SIGCHLD
信號的處理動作時,內核會更新父進程的信號處理表。內核會記錄
SIGCHLD
信號的處理動作為SIG_IGN
。子進程調用退出
- 內核生成
SIGCHLD
信號:
- 內核生成
SIGCHLD
信號并準備發送給父進程。- 內核會檢查父進程的信號處理表,查看
SIGCHLD
信號的處理動作。- 檢查信號處理動作:
- 如果父進程的信號處理動作是
SIG_IGN
,內核會知道父進程忽略了SIGCHLD
信號。- 內核會自動回收子進程的資源,子進程不會變成僵尸進程。
問題:系統對該信號的默認處理不就是忽略嗎,為什么我們還要自己主動忽略
系統默認的忽略動作和???sigaction函數?定義的忽略 通常是沒有區別的,但這
是?個特例。此?法對于Linux可?,但不保證在其它UNIX系統上都可 ?。
其實,因為位圖本身一次只能記錄一個進程退出信號,因此即使循環等待等操作,還是會有極小概率處理不了某些退出子進程