我們通過fork
函數創建多個子進程,并通過exec
函數族在子進程中進行其他的工作,但是為了避免僵尸進程,我們要對子進程進行回收。常用的回收方式是wait
或者waitpid
進行阻塞回收,因為如果非阻塞回收很難把握時機,而阻塞回收將導致父進程無法進行其他的工作。通過子進程狀態改變后會發送一個SIGCHLD
信號這一機制,我們可以在父進程中將這一信號進行捕獲然后進行非阻塞的回收子進程并保證能夠回收所有的,也不需要通過sleep
函數去強制保證異步。
通過捕獲SIGCHLD
信號進行回收子進程最害怕的就是父進程還沒有設置完捕獲函數,子進程全部都死翹翹了,然后父進程就等不到SIGCHLD
信號,無法開始回收進程。為了避免這種情況,一般的解決方法是首先對子進程進行一個sleep
等待父進程設置捕獲函數,我覺得這種做法十分低效,我想到的解決方式是在fork
函數前就對SIGCHLD
信號進行屏蔽,等父進程設置好捕獲函數后再解除屏蔽,這樣就不會錯過SIGCHLD
信號啦。
另一方面因為未決信號集只是一個簡單的位圖,只能保存有該信號,不能保存該信號發送了多少次,因此我們每次回收進程都要把已經死亡的所有進程進行回收,因為有可能很多子進程一起死亡,這些信號一起發過來,我們不能一個信號只回收一個子進程。
代碼如下:
Utils.h:封裝了一些簡單的操作,簡化代碼,實現放在文末
#ifndef LINUX_UTILS_H
#define LINUX_UTILS_H#include <string>
#include <initializer_list>
#include <signal.h>/*!* 檢查系統調用返回值* @param x 返回值* @param msg 錯誤提示語句* @param y 錯誤狀態,默認為-1*/
bool check_error(int x, const std::string &msg = "error", int y = -1);
/*!* 清零mask,并將il中的信號加入到mask中* @param mask* @param il*/
void add2mask(sigset_t *mask, std::initializer_list<int> il);
/*!* 將il中的信號從mask中刪除* @param mask* @param il*/
void del2mask(sigset_t *mask, std::initializer_list<int> il);/*!* 向阻塞信號集里面添加信號* @param oldset* @param il*/
void add2procmask(std::initializer_list<int> il);/*!* 從阻塞信號集里面刪除信號* @param il*/
void del2procmask(std::initializer_list<int> il);#endif //LINUX_UTILS_H
創建子進程并回收:
int &wait_child_num() {static int num = 0;return num;
}void wait_child(int signum) {pid_t pid;int wstatus;while ((pid = waitpid(0, &wstatus, WNOHANG)) > 0) {++wait_child_num();if (WIFEXITED(wstatus)) {cout << "process[" << pid << "] exited with " << WEXITSTATUS(wstatus) << endl;} else {cout << "process[" << pid << "] was terminated by signal " << WTERMSIG(wstatus) << endl;}}
}int test_wait() {int idx;pid_t pid;constexpr int N = 5;/*!* 在fork前應該將SIGALRM信號加入阻塞信號集,否則父進程還沒有來得及設置信號捕捉函數回收子進程,他們全都死亡了,回收了個寂寞*/add2procmask({SIGCHLD});for (idx = 0; idx < N; ++idx) {pid = fork();check_error(pid, "fork error");if (pid == 0)break;}if (idx == N) {//父進程//注冊SIGALRM信號捕捉函數struct sigaction act, oldact;act.sa_flags = 0;add2mask(&act.sa_mask, {SIGINT, SIGQUIT, SIGTSTP});act.sa_handler = wait_child;check_error(sigaction(SIGCHLD, &act, &oldact), "sigaction error");//解除對SIGALRM的屏蔽del2procmask({SIGCHLD});cout << "begin to wait for children" << endl;while (wait_child_num() < N);check_error(sigaction(SIGCHLD, &oldact, nullptr), "sigaction error");} else {my_sleep(idx, 0);}
}
其中mysleep
函數是我自己實現的sleep
函數,如果有興趣可以看我的另一篇博客:Linux信號實現精確到微秒的sleep函數:通過sigsuspend函數解決時序競態問題
通過wait_child_num
返回一個局部靜態變量num
引用獲取回收了的子進程的個數,雖然在捕獲函數中使用靜態變量將導致捕獲函數不再是一個可重入函數,但是因為在我的代碼中只有捕獲函數會對num
進行寫操作,因此不會發生全局變量異步IO,而且在捕獲信號期間會對SIGCHLD
信號屏蔽(通過設置sigaction
結構體的sa_flags
為0),也不用擔心會發生重入。
之所以將其變成一個局部靜態變量而不是直接使用一個靜態變量是 Effective C++ 條款18:讓接口容易被正確使用的建議,盡可能使用局部靜態變量,因為這樣一方面可以避免名字污染,另一方面可以避免初始化次序問題,當在多個文件中的時候確保使用到該變量時能夠被初始化。
通過測試和查閱APUE,我發現子進程的阻塞信號集和父進程是一致的,但是未決信號集子進程會清零。
Utils.cpp:工具函數的實現,非常簡單
#include "utils.h"using std::string;bool check_error(int x, const string &msg, int y) {if (x == y) {perror(msg.c_str());exit(1);}return true;
}void add2mask(sigset_t *mask, std::initializer_list<int> il) {check_error(sigemptyset(mask), "sigemptyset error");for (auto signum : il) {check_error(sigaddset(mask, signum), "sigaddset error");}
}void del2mask(sigset_t *mask, std::initializer_list<int> il) {for (auto signum : il) {check_error(sigdelset(mask, signum), "sigdelset error");}
}void add2procmask(std::initializer_list<int> il) {sigset_t mask;add2mask(&mask, il);check_error(sigprocmask(SIG_BLOCK, &mask, nullptr), "sigprocmask error");
}void del2procmask(std::initializer_list<int> il) {sigset_t mask;add2mask(&mask, il);check_error(sigprocmask(SIG_UNBLOCK, &mask, nullptr), "sigprocmask error");
}