殭屍程序的產生、危害及避免方法
1.殭屍程序:前文已經對殭屍程序的定義進行了說明。那麼defunct程序只是在process table(程序表項)裡有一個記錄,其他的資源沒有佔用,除非你的系統的process個數已經快超過限制了,zombie程序不會有更多的壞處。
2.產生原因:在子程序終止後到父程序呼叫wait()前的時間裡,子程序被稱為zombie;
具體a. 子程序結束後向父程序發出SIGCHLD訊號,父程序預設忽略了它
b. 父程序沒有呼叫wait()或waitpid()函式來等待子程序的結束
c. 網路原因有時會引起殭屍程序;
3. 危害
殭屍程序會佔用系統資源,如果很多,則會嚴重影響伺服器的效能;
孤兒程序不會佔用系統資源,最終是由init程序託管,由init程序來釋放;
signal(SIGCHLD, SIG_IGN); // 忽略SIGCHLD訊號,這是一個常用於提升併發伺服器效能的技巧
// 因為併發伺服器常常fork很多子程序,子程序終結之後需要伺服器程序去wait清理資源。
// 如果將此訊號的處理方式設定為忽略,可讓核心把殭屍程序轉交給init程序去處理,省去了大量殭屍進 程佔用系統資源。
4.如何防止殭屍程序
(1) 讓殭屍程序成為孤兒程序,由init程序回收;(手動殺死父程序)
(2) 呼叫fork()兩次;
(3) 捕捉SIGCHLD訊號,並在訊號處理函式中呼叫wait函式;
下面給出一個具體的案例來說明這種方法。
在上面的訊號處理函式sig_handler中我們呼叫了wait函式,目的是為了讓父程序在捕獲到子程序結束髮出的SIGCHLD訊號後對子程序進行回收,避免子程序成為殭屍程序。這裡的wait函式不同於下面第4中方法中的wait的用法,這裡只有在父程序捕獲到子程序結束時才呼叫wait對其進行回收,其他時間父程序還是繼續執行。而在方法4中,呼叫wait函式會發生阻塞。#include <signal.h> #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> void sig_handler(int signo) { printf("child process deaded, signo: %d\n", signo); wait(0);// 當捕獲到SIGCHLD訊號,父程序呼叫wait回收,避免子程序成為殭屍程序 } void out(int n) { int i; for(i = 0; i < n; ++i) { printf("%d out %d\n", getpid(), i); sleep(2); } } int main(void) { // 登記一下SIGCHLD訊號 if(signal(SIGCHLD, sig_handler) == SIG_ERR) { perror("signal sigchld error"); } pid_t pid = fork(); if(pid < 0) { perror("fork error"); exit(1); } else if(pid > 0) { // parent process out(100); } else { // child process out(10); } return 0; }
(4) 讓殭屍程序的父程序來回收,父程序每隔一段時間來查詢子程序是否結束並回收,呼叫wait()或者waitpid(),通知核心釋放殭屍程序;
wait函式的原型是:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
程序一旦呼叫了wait,就立即阻塞自己,由wait自動分析是否當前程序的某個子程序已經退出,如果讓它找到了這樣一個已經變成殭屍程序的子程序,wait就會收集這個子程序的資訊,並把它徹底銷燬後返回;如果沒有找到這樣一個子程序,wait就會一直阻塞在這裡,直到有一個這樣的程序出現為止。
引數status用來儲存被回收程序退出時的一些狀態,如果我們不想知道這個子程序是如何死掉的,只想把它消滅掉的話,那麼我們可以設定這個引數為NULL,就像下面這樣:
pid = wait(NULL);
如果成功,wait會返回被回收子程序的程序ID,如果呼叫程序沒有子程序,呼叫就會失敗,此時wait返回-1,同時errno被置為ECHILD。
waitpid函式的原型是:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
waitpid相比於wait函式多了兩個引數,下面對這兩個引數做一個詳細說明。
pid
從引數的名字pid和型別pid_t 就可以看出,這裡需要的是一個程序ID。當pid取不同的值時,在這裡有不同的意義。
- pid > 0時,只等待程序ID等於pid的子程序,不管其他已經有多少個子程序執行結束退出了,只要指定的子程序還沒結束,waitpid就會一直等下去;
- pid = -1時,等待任何一個子程序退出,沒有 任何限制,此時和wait函式作用一樣;
- pid = 0時,等待同一個程序組中的任何子程序,如果 子程序已經加入了別的程序組,waitpid不會對它做任何理睬;
- pid < -1時,等待一個指定程序組中的任何子程序,這個程序組的ID等於pid的絕對值;
options目前只支援WNOHANG和WUNTRACED兩個選項,這是兩個常數,可以用“|”運算子把它們連線起來使用,比如:
ret = waitpid(-1, NULL, WNOHANG | WUNTRACED);
如果我們不想使用它們,也可以把options設為0,如:
ret = waitpid(-1, NULL, 0);
如果使用了WNOHANG引數呼叫waitpid,即使沒有子程序退出,它也會立即返回,不會像wait那樣永遠等下去;
返回值和錯誤:
waitpid的返回值比wait稍微複雜一些,一共有三種情況。
- 當正常返回時,waitpid返回收集到的子程序的程序ID;
- 如果設定了選項WNOHANG,而呼叫中waitpid發現沒有已退出的子程序可以收集,則返回0;(非阻塞)
- 如果呼叫中出錯,則返回-1,這時errno會被設定成相應的值以指示錯誤所在;
- 在一個子程序終止前,wait使其呼叫者阻塞,而waitpid則提供了非阻塞版本;
- waitpid等待一個指定的子程序,而wait等待第一個終止的子程序;
- waitpid支援作業控制(以WUNTRACED選項,由pid指定的任一子程序狀態,且其狀態自暫停以來還未報告過,則返回其狀態);
a.WIFEXITED/WEXITSTATUS(status)
若為正常終止子程序返回的狀態,則為真。b.WIFSIGNALED/WTERMSIG(status)
若為異常終止子程序返回的狀態,則為真(接到一個不能捕捉的訊號)
c.WIFSTOPPED/WSTOPSIG(status) (看當前子程序在終止前是否暫停過)
若為當前暫停子程序的返回狀態,則為真。
===============================================================
下面以一個案例來說明wait和waitpid的用法
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
void out_status(int status)
{
// printf("status: %d\n", status);
if(WIFEXITED(status))//正常終止
{
printf("normal exit: %d\n", WEXITSTATUS(status));
}
else if(WIFSIGNALED(status))// 非正常終止
{
printf("abnormal term: %d\n", WTERMSIG(status));
}
else if(WIFSTOPPED(status))// 終止前是否暫停過
{
printf("stopped sig: %d\n", WSTOPSIG(status));
}
else//未知
{
printf("unknown sig\n");
}
}
int main(void)
{
int status;
pid_t pid;
// 正常終止
if((pid = fork()) < 0)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
printf("pid: %d, ppid: %d\n", getpid(), getppid());
exit(3);// 子程序終止執行(狀態碼為3也算是一種正常終止)
}
// 父程序呼叫阻塞,等待子程序結束並回收
wait(&status);
out_status(status);
printf("--------------------------\n");
//異常終止
if((pid = fork()) < 0)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
printf("pid: %d, ppid: %d\n", getpid(), getppid());
int i = 3, j = 0;
int k = i / j;
printf("k: %d\n", k);
}
wait(&status);
out_status(status);
printf("-------------------------\n");
if((pid = fork()) < 0)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
printf("pid: %d, ppid: %d\n", getpid(), getppid());
pause();// 暫停,等待一個訊號來喚醒/終止
/* int i = 0;
while(++i > 0)
sleep(3);
*/
}
//wait(&status);
// 用waitpid的非阻塞方式
do
{
pid = waitpid(pid, &status, WNOHANG | WUNTRACED);
if(pid == 0)
sleep(1);
}while(pid == 0);
out_status(status);
return 0;
}
程式中第一個子程序算是正常退出,所以最後返回其退出狀態碼為3;
程式中第二個子程序是除0操作,屬於異常終止;
程式中第三個子程序如果使用pause或while迴圈會讓程式“暫停”下來,但是這裡的暫停並不是WIFSTOPPED/WSTOPSIG(status)所說的暫停。這裡程式其實並沒有停下來,只是不停的在做迴圈,我們可以通過kill將其殺掉,同樣是屬於異常終止;
那麼要想實現上述所說的第三種狀態,即子程序在退出前曾暫停過,我們就不能使用wait函式來回收子程序,而是要使用waitpid並且配合WNOHANG和WUNTRACED選項。
下圖是程式執行後的執行結果:
可見,程式執行到pause出停了下來,相當於是在做死迴圈;
下面給該程序一個暫停訊號,kill -SIGSTOP 2620,執行結果如下:
輸出的狀態碼是19,也就是訊號SIGSTOP對應的編號。