1. 程式人生 > >殭屍程序的產生、危害及避免方法

殭屍程序的產生、危害及避免方法

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函式;

下面給出一個具體的案例來說明這種方法。

#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;
}
在上面的訊號處理函式sig_handler中我們呼叫了wait函式,目的是為了讓父程序在捕獲到子程序結束髮出的SIGCHLD訊號後對子程序進行回收,避免子程序成為殭屍程序。這裡的wait函式不同於下面第4中方法中的wait的用法,這裡只有在父程序捕獲到子程序結束時才呼叫wait對其進行回收,其他時間父程序還是繼續執行。而在方法4中,呼叫wait函式會發生阻塞。

(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

options目前只支援WNOHANGWUNTRACED兩個選項,這是兩個常數,可以用“|”運算子把它們連線起來使用,比如:

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的區別
  • 在一個子程序終止前,wait使其呼叫者阻塞,而waitpid則提供了非阻塞版本;
  • waitpid等待一個指定的子程序,而wait等待第一個終止的子程序;
  • waitpid支援作業控制(以WUNTRACED選項,由pid指定的任一子程序狀態,且其狀態自暫停以來還未報告過,則返回其狀態);
舉個例子:當同時有5個客戶連上伺服器,也就是說有5個子程序分別對應了5個客戶。若此時,5個客戶幾乎同時請求終止,即5個FIN發現伺服器,同樣的,5個SIGCHLD訊號到達伺服器,然而,UNIX的訊號往往是不會排隊的,這樣一來,訊號處理函式將會只執行一次,殘留剩餘4個子程序作為殭屍程序駐留在核心空間。此時,正確的解決辦法就是利用waitpid(-1, &status, WNOHANG)防止留下殭屍程序。其中的pid為-1表明等待第一個終止的子程序,而WNOHANG選擇項通知核心在沒有已終止程序時不要阻塞。 檢查wait和waitpid兩個函式返回終止狀態的巨集

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對應的編號。