1.什麼是殭屍進程
如果父進程在子進程之前終止,則所有的子進程的父進程都會改變爲init進程,我們稱這些進程由init進程領養。這時使用ps命令查看後可以看到子進程的父進程ppid已經變爲了1。
而當子進程在父進程之前終止時,內核爲每個終止子進程保存了一定量的信息,所以當終止進程的父進程調用wait或waitpid時,可以得到這些信息。這些信息至少包括進程ID、該進程的終止狀態、以及該進程使用的CPU時間總量。其他的進程所使用的存儲區,打開的文件都會被內核釋放。
一個已經終止、但是其父進程尚未對其進行善後處理(獲取終止子進程的有關信息,釋放它仍佔用的資源)的進程被稱爲殭屍進程。ps命令將殭屍進程的狀態打印爲Z。
可以設想一下,比如一個web服務器端,假如每次接收到一個連接都創建一個子進程去處理,處理完畢後結束子進程。假如在父進程中沒有使用wait或waitpid函數進行善後,這些子進程將全部變爲殭屍進程,Linux系統的進程數一般有一個固定限制值,殭屍進程將會逐漸耗盡系統資源。
2.查看殭屍進程的例子
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
int main(int argc, char **argv)
{
pid_t pid;
for (int i=0; i<5; i++) {
if ((pid = fork()) < 0) {
printf("fork error,%s\n", strerror(errno));
return -1;
}
/* child */
if (pid == 0) {
sleep(1);
exit(0);
}
}
/* parent */
sleep(20);
return 0;
}
編譯完成後,在執行程序的命令後加上“&”符號,表示讓當前程序在後臺運行。
之後輸入:
ps –e –o pid,ppid,stat,command|grep [程序名]
可以看到5個子進程都已經是殭屍進程了。
3.SIGCHLD信號和處理殭屍進程
當子進程終止時,內核就會向它的父進程發送一個SIGCHLD信號,父進程可以選擇忽略該信號,也可以提供一個接收到信號以後的處理函數。對於這種信號的系統默認動作是忽略它。
我們不希望有過多的殭屍進程產生,所以當父進程接收到SIGCHLD信號後就應該調用 wait 或 waitpid 函數對子進程進行善後處理,釋放子進程佔用的資源。
下面是一個捕獲SIGCHLD信號以後使用wait函數進行處理的簡單例子:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
void deal_child(int sig_no)
{
wait(NULL);
}
int main(int argc, char **argv)
{
signal(SIGCHLD, deal_child);
pid_t pid;
for (int i=0; i<5; i++) {
if ((pid = fork()) < 0) {
printf("fork error,%s\n",strerror(errno));
return -1;
}
/* child */
if (pid == 0) {
sleep(1);
exit(0);
}
}
/* parent */
for(int i=0; i<100000; i++) {
for (int j=0; j<100000; j++) {
int temp = 0;
}
}
return 0;
}
同樣在後臺運行後使用ps命令查看進程狀態,結果如圖:
發現創建的5個進程,有3個已經被徹底銷燬,但是還有2個仍然處於殭屍進程的狀態。
這是因爲當5個進程同時終止的時候,內核都會向父進程發送SIGCHLD信號,而父進程此時有可能仍然處於信號處理的deal_child函數中,那麼在處理完之前,中間接收到的SIGCHLD信號就會丟失,內核並沒有使用隊列等方式來存儲同一種信號。
4.正確地處理殭屍進程的方法
爲了解決上面出現的這種問題,我們需要使用waitpid函數。
函數原型爲:
pid_t waitpid(pid_t pid, int *statloc, int options);
若成功則返回進程ID,如果設置爲非阻塞方式,返回0表示子進程狀態未改變,出錯時返回-1。
options參數可以設置爲WNOHANG常量,表示waitpid不阻塞,如果由pid指定的子進程不是立即可用的,則立即返回0。
只需要修改一下SIGCHLD信號的處理函數即可:
void deal_child(int sig_no)
{
for (;;) {
if (waitpid(-1, NULL, WNOHANG) == 0)
break;
}
}
再次執行程序後使用ps命令查看,發現已經不會產生殭屍進程了。