🦄 個人主頁: 小米里的大麥-CSDN博客
🎏 所屬專欄: Linux_小米里的大麥的博客-CSDN博客
🎁 GitHub主頁: 小米里的大麥的 GitHub
?? 操作環境: Visual Studio 2022
文章目錄
- 進程控制 —— 進程等待
- 1. 進程等待必要性
- 2. 常用等待方法(重點掌握)
- 1. `wait()` 示例(阻塞等待子進程)
- 2. `waitpid()` 示例(等待指定子進程,更靈活)
- 3. status 退出狀態詳解
- 1. 什么是 `status`?
- 2. `status` 的位布局(Linux 下)
- 3. WIFEXITED 和 WEXITSTATUS 的底層原理
- 1. `WIFEXITED(status)`
- 2. `WEXITSTATUS(status)`
- 4. 實驗測試
- 4. 非阻塞輪詢
- 1. 什么是非阻塞輪詢(Non-blocking Polling)?
- 場景一:阻塞等待(wait)
- 場景二:非阻塞輪詢(WNOHANG)
- 場景三:阻塞輪詢(極端示例)
- 術語與現實對應表
- 2. 聯系總結(術語圖譜)
- 3. 我該怎么選?怎么使用?
- 4. 小結一句話
- 5. 總結記憶點
- 共勉
進程控制 —— 進程等待
1. 進程等待必要性
- 當父進程通過
fork()
創建了子進程后,子進程終止時,其退出信息必須由父進程讀取,父進程如果不管不顧,就可能造成 僵尸進程 的問題,進而造成內存泄漏。 - 另外,進程一旦變成僵尸狀態,那就刀槍不入,“殺人不眨眼”的
kill -9
也無能為力,因為誰也沒有辦法殺死一個已經死去的進程。 - 最后,父進程派給子進程的任務完成的如何,我們需要知道。如子進程運行完成,結果對還是不對,或者是否正常退出。
- 父進程通過進程等待的方式,回收子進程資源,獲取子進程退出信息。
如果不等待會怎樣?
- 子進程退出了,但父進程沒有調用
wait()
系列函數。- 子進程的“退出狀態”會保留在內核中,直到父進程讀取它。
- 此時子進程的 PCB 沒有完全釋放,占用系統資源。
- 如果產生大量僵尸進程,系統資源將耗盡,導致無法創建新進程。、
所以:父進程需要“等待”子進程終止并獲取其退出狀態,以釋放系統資源。
面試點撥: 如果不調用
wait()
會怎樣?回答:子進程的退出信息留在內核,
PCB
未釋放,形成僵尸進程,長期不回收會占滿系統資源。
2. 常用等待方法(重點掌握)
函數名 | 作用 |
---|---|
wait(int *status) | 阻塞等待任意一個子進程結束,并獲取其退出狀態 |
waitpid(pid, &status, options) | 更靈活:等待指定子進程,或非阻塞等 |
1. wait()
示例(阻塞等待子進程)
wait()
:
- 原型:
pid_t wait(int *status);
- 功能:阻塞等待任意一個子進程退出,并回收其資源。
- 參數:
status
(輸出型參數):保存/獲取子進程退出狀態(需用宏解析,如WIFEXITED
)。不關心可設置為 NULL。- 返回值:成功返回子進程 PID,失敗返回
-1
。
實驗目的:
- 學會使用
wait()
函數阻塞等待子進程結束。 - 理解如何通過
status
獲取子進程的退出狀態。 - 掌握如何判斷子進程是否正常退出以及獲取其退出碼。
[!CAUTION]
下面代碼會涉及部分知識盲區,在文章后面會講到!
實驗:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>int main()
{pid_t id = fork();if (id == 0) // 子進程{int count = 10;while (count--){printf("我是子進程...PID:%d, PPID:%d\n", getpid(), getppid()); // 子進程邏輯:打印 PID 和 PPIDsleep(1);}exit(0); // 子進程退出}int status = 0; // 存儲子進程退出狀態pid_t ret = wait(&status); // 阻塞等待子進程結束if (ret > 0) // 父進程{// 父進程等待子進程結束printf("等待子進程結束...\n");if (WIFEXITED(status)) // 判斷子進程是否正常退出{// 子進程正常結束printf("子進程正常結束,退出狀態碼:%d\n", WEXITSTATUS(status));}}sleep(3);return 0;
}
實驗示例結果:
我是子進程...PID:1234, PPID:1233
我是子進程...PID:1234, PPID:1233
...
等待子進程結束...
子進程正常結束,退出狀態碼:0
2. waitpid()
示例(等待指定子進程,更靈活)
waitpid()
- 原型:
pid_t waitpid(pid_t pid, int *status, int options);
- 功能:更靈活地等待指定子進程,支持非阻塞模式。
- 參數:
pid
:指定子進程 PID,或 -1 表示任意子進程。options
:常用的有 WNOHANG 表示非阻塞等待(立即返回,無子進程退出時返回0
)。- 返回值:成功返回子進程 PID,WNOHANG 模式下無退出子進程時返回
0
,失敗返回-1
。
實驗目的:
- 學會使用
waitpid()
函數等待指定子進程。 - 理解非阻塞等待(WNOHANG)的使用場景和優勢。
- 掌握如何在等待子進程的同時處理其他任務。
實驗 1:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>int main()
{pid_t pid = fork();if (pid == 0){exit(10);}else{int status;pid_t wpid;while ((wpid = waitpid(pid, &status, WNOHANG)) == 0){printf("父進程忙別的事...\n");sleep(1);}if (WIFEXITED(status)){printf("子進程退出碼 = %d\n", WEXITSTATUS(status));}}return 0;
}
實驗示例結果:
父進程忙別的事...
父進程忙別的事...
...
子進程退出碼 = 10
WNOHANG 的用途(后面詳講):它用于非阻塞輪詢場景,讓父進程可以邊處理任務邊檢查子進程狀態。
實驗 2:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{pid_t id = fork(); // 創建子進程if (id == 0){int time = 5;int n = 0;while (n < time){printf("我是子進程,我已經運行了:%d秒 PID:%d PPID:%d\n", n + 1, getpid(), getppid());sleep(1);n++;}exit(244); // 子進程退出}int status = 0; // 狀態pid_t ret = waitpid(id, &status, 0); // 參數3 為0,為默認選項if (ret == -1){printf("進程等待失敗!進程不存在!\n");}else if (ret == 0){printf("子進程還在運行中!\n");}else{printf("進程等待成功,子進程已被回收\n");}printf("我是父進程, PID:%d PPID:%d\n", getpid(), getppid());//通過 status 判斷子進程運行情況if ((status & 0x7F)){printf("子進程異常退出,core dump:%d 退出信號:%d\n", (status >> 7) & 1, (status & 0x7F));}else{printf("子進程正常退出,退出碼:%d\n", (status >> 8) & 0xFF);}return 0;
}
實驗示例結果:
我是子進程,我已經運行了:1秒 PID:1234 PPID:1233
...
進程等待成功,子進程已被回收
我是父進程, PID:1233 PPID:1232
子進程正常退出,退出碼:244
3. status 退出狀態詳解
1. 什么是 status
?
當你用 wait()
或 waitpid()
等函數回收子進程時,會通過一個整型變量 status
返回子進程的 終止狀態/狀態碼 status 信息。這個 status
是一個 32 位整數,它的 各個位(bit)存儲了子進程退出的不同信息,主要包括:
- 子進程是否正常退出
- 退出的返回碼
- 是否是被信號中斷
- 是否是
core dump
等
當子進程結束時,它就會返回一個 狀態碼 status,通過宏函數解讀它:
宏函數 | 判斷或提取內容 | 實現底層邏輯 | 本質 |
---|---|---|---|
WIFEXITED() | 是否正常退出 | (status & 0x7F) == 0 | 判斷是否未被信號終止(是否正常退出) |
WEXITSTATUS() | 獲取退出碼 | (status >> 8) & 0xFF | 提取退出碼所在的 8 位(獲取 exit 返回碼) |
這些宏的 設計目的 就是為了 屏蔽底層實現細節,讓你寫代碼時更易讀。但其實就是對 status
進行的位運算封裝。
2. status
的位布局(Linux 下)
通常(glibc 實現下),status 的位布局如下:
31...........16 | 15.....8 | 7......0保留位 | 退出碼 | 信號位
31 16 15 8 7 0
+-----------------------------+-------------+--------+
| 保留 | 退出碼(exit) | 信號碼 |
+-----------------------------+-------------+--------+↑ ↑| |(status >> 8) status & 0x7F
3. WIFEXITED 和 WEXITSTATUS 的底層原理
1. WIFEXITED(status)
判斷子進程是否 正常退出(調用了 exit()
或 return
)
#define WIFEXITED(status) (((status) & 0x7F) == 0)
🔸 它檢測的是 低 7 位(status & 0x7F)是否為 0,即 沒有被信號終止。
2. WEXITSTATUS(status)
獲取子進程的 退出碼(exit() 或 return 的值)
#define WEXITSTATUS(status) (((status) >> 8) & 0xFF)
🔸 它提取的是 第 8~15 位,因為退出碼就被編碼在這里。
4. 實驗測試
實驗目的:
- 學會解析
status
的各個位,了解子進程的退出狀態。 - 掌握如何通過宏函數判斷子進程是否正常退出以及獲取其退出碼。
- 理解如何手動解析
status
的位信息。
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>int main()
{pid_t pid = fork();if (pid == 0){exit(66); // 子進程退出碼設為 66}else{int status = 0;waitpid(pid, &status, 0);printf("原始 status:%d (0x%x)\n", status, status);if (WIFEXITED(status)){printf("正常退出,返回值 = %d\n", WEXITSTATUS(status));printf("手動解析返回值 = %d\n", (status >> 8) & 0xFF);}else{printf("非正常退出\n");}}return 0;
}
輸出示例:
原始 status:16896 (0x4200)
正常退出,返回值 = 66
手動解析返回值 = 66
示例:手動解析 status
:
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>int main()
{pid_t pid = fork();if (pid == 0){exit(66); // 子進程退出碼設為 66}else{int status = 0;waitpid(pid, &status, 0);printf("原始 status: 0x%x\n", status);// 手動解析 statusif ((status & 0x7F) == 0) // 判斷是否正常退出{ int exit_code = (status >> 8) & 0xFF; // 提取退出碼printf("手動解析:子進程正常退出,退出碼: %d\n", exit_code);}else{printf("手動解析:子進程異常退出,信號碼: %d\n", (status & 0x7F));}}return 0;
}
擴展:
寫法模板:
#include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h>int main() {pid_t pid = fork();if (pid == 0) // 子進程邏輯{exit(0);}else if (pid > 0) // 父進程邏輯{int status;pid_t ret = waitpid(pid, &status, 0);if (ret == -1){perror("waitpid error");}else if (WIFEXITED(status)){printf("子進程正常退出,退出碼: %d\n", WEXITSTATUS(status));}else{printf("子進程異常退出\n");}}else{perror("fork error");}return 0; }
若子進程是被信號殺死的,還可用:
WIFSIGNALED(status)
:是否被信號終止。WTERMSIG(status)
:哪個信號導致的。這些也都是對
status
特定位的封裝。
面試點撥:
Q:只創建一個子進程也要
wait()
嗎?A:要,不然會產生僵尸進程。
Q:
wait(NULL)
和wait(&status)
有何不同?A:前者不關心子進程退出碼,后者可以判斷退出狀態。
Q:
wait()
和waitpid()
的區別是什么?(詳見下文)A:
wait()
阻塞等待任意一個子進程,而waitpid()
可以指定子進程,并支持非阻塞模式。Q:怎么判斷子進程是否異常退出?
A:
WIFEXITED(status)
為假時即為異常,可結合WIFSIGNALED
查看是否被信號終止。
4. 非阻塞輪詢
1. 什么是非阻塞輪詢(Non-blocking Polling)?
非阻塞輪詢 是一種在程序中檢查某項資源狀態(比如文件描述符、輸入輸出、子進程狀態等)時,不會阻塞(掛起)當前線程或進程的技術。非阻塞輪詢其實是 進程等待的一種特殊形式,本質上就是使用 waitpid()
函數時,配合選項 WNOHANG
,來實現 非阻塞地檢查子進程是否退出。
非阻塞輪詢底層依賴:
waitpid(..., WNOHANG)
:設置為非阻塞檢查子進程。read()
/write()
配合O_NONBLOCK
標志。select()
/poll()
/epoll()
這些高級接口也支持非阻塞 I/O 檢測。
聯系:
- 非阻塞輪詢 ≈ 進程等待 +
WNOHANG
參數。 - 是進程等待的一種實現方式,可以避免父進程“卡死”在等待中。
- 適合場景:父進程還有其他任務要處理、需要同時監控多個子進程、構造后臺守護程序等。
這樣說難以理解,我們用一個示例來幫助理解:假如你是快遞員,你今天安排了送貨任務,但你同時還在等一個客戶簽收你的包裹。現在有兩種做法:
場景一:阻塞等待(wait)
你在客戶門口等著他開門簽字,你哪兒也不去,什么都不干,就在那兒等。就是 wait()
或 waitpid(pid, NULL, 0)
。
- 優點:等到了就能馬上處理。
- 缺點:你被“卡住”了,浪費了等的這段時間。
場景二:非阻塞輪詢(WNOHANG)
你不一直站在門口,而是 每隔 10 分鐘回來敲一次門,空閑的時候你還可以去送別的快遞。就是 waitpid(pid, &status, WNOHANG)
+ sleep(1)
。
- 優點:你不會被“卡住”,還能干其他事。
- 缺點:客戶簽收可能不能第一時間知道(需要“輪詢”)。
場景三:阻塞輪詢(極端示例)
你不停敲門、再敲門,一直不走,一直問:“你簽了沒?你簽了沒?” 程序中表現為沒有 sleep
的非阻塞 waitpid(pid, WNOHANG)
死循環。
- 缺點:會讓 CPU 瘋狂運轉(忙等待)。
術語與現實對應表
系統術語 | 現實中的你 |
---|---|
阻塞等待 | 在門口站著等,不做別的事 |
非阻塞輪詢 | 每隔一段時間回來問一次,期間干別的事 |
阻塞輪詢 | 瘋狂按門鈴,問個不停,CPU 很累 |
進程等待 | 等子進程結束,獲取退出狀態 |
2. 聯系總結(術語圖譜)
wait/waitpid┌────────────┐│ 進程等待機制│└────┬───────┘│┌─────────────┴────────────┐│ │┌─────▼─────┐ ┌─────▼─────────┐│ 阻塞等待 │ │ 非阻塞輪詢 ││ wait() │ │ waitpid(pid, WNOHANG) │└────────────┘ └──────────────────────┘
3. 我該怎么選?怎么使用?
場景 | 推薦方法 | 原因 |
---|---|---|
父進程只等子進程結束,沒別的事干 | 阻塞等待 (wait ) | 簡單、直接、不會浪費資源 |
父進程還有其他重要任務 | 非阻塞輪詢 (waitpid + WNOHANG ) | 不中斷其他邏輯,更靈活 |
你同時要監控多個子進程 | 非阻塞輪詢 | 可以處理多個子進程,適合服務端/守護程序 |
寫簡單的練習題/實驗代碼 | 阻塞等待即可 | 寫起來方便,看得懂 |
實驗目的:
- 學會使用非阻塞輪詢等待子進程結束。
- 理解如何在等待子進程的同時處理其他任務。
- 掌握如何通過
WNOHANG
選項實現非阻塞等待。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{pid_t pid = fork();if (pid == 0) // 子進程{printf("子進程開始運行...\n");sleep(5);printf("子進程即將退出\n");exit(0);}else // 父進程:非阻塞方式輪詢子進程狀態{int status;while (1){pid_t result = waitpid(pid, &status, WNOHANG); // 非阻塞調用if (result == 0){// 子進程還未退出printf("父進程:子進程還在運行...\n");}else if (result == pid){// 子進程已經退出if (WIFEXITED(status)){printf("父進程:子進程正常退出,退出碼為 %d\n", WEXITSTATUS(status));}break;}else{perror("waitpid error");break;}sleep(1); // 輪詢間隔}}return 0;
}
實驗示例結果:
父進程:子進程還在運行...
父進程:子進程還在運行...
...
子進程開始運行...
子進程即將退出
父進程:子進程正常退出,退出碼為 0
4. 小結一句話
非阻塞輪詢是一種“智能等待”方式,讓父進程在等待子進程的同時,還能處理其他任務,是并發編程的常見技巧。
5. 總結記憶點
內容 | 說明 |
---|---|
為什么等待 | 防止僵尸進程,釋放系統資源,獲取子進程退出信息,確保系統穩定性和資源高效利用。 |
常用函數 | wait() :阻塞等待任意子進程結束;waitpid() :靈活等待指定子進程,支持非阻塞模式。 |
狀態解析 | 使用宏函數 WIFEXITED() 判斷子進程是否正常退出,WEXITSTATUS() 獲取退出碼。 |
非阻塞輪詢 | 適用于父進程需要同時處理其他任務或監控多個子進程的場景,通過 waitpid() 配合 WNOHANG 實現。 |
推薦寫法 | 常用 waitpid(pid, &status, 0) ,安全靈活,適合大多數場景。 |
注意事項 | 父進程必須回收子進程資源,否則會導致僵尸進程,長期不回收會耗盡系統資源。 |
適用場景 | 簡單程序使用阻塞等待,復雜程序或需要并發處理時使用非阻塞輪詢。 |
實戰技巧
- 調試技巧:在調試時,若發現僵尸進程,檢查父進程是否正確調用了
wait()
或waitpid()
。 - 性能優化:在高并發場景下,使用非阻塞輪詢避免父進程被長時間阻塞,提高系統響應速度。
- 代碼健壯性:始終檢查
wait()
和waitpid()
的返回值,處理可能的錯誤情況。
共勉