Linux 多工程式設計——特殊程序:殭屍程序、孤兒程序、守護程序
殭屍程序(Zombie Process)
程序已執行結束,但程序的佔用的資源未被回收,這樣的程序稱為殭屍程序。
在每個程序退出的時候,核心釋放該程序所有的資源、包括開啟的檔案、佔用的記憶體等。 但是仍然為其保留一定的資訊,這些資訊主要主要指程序控制塊的資訊(包括程序號、退出狀態、執行時間等)。直到父程序通過 wait() 或 waitpid() 來獲取其狀態並釋放(具體用法,請看《等待程序結束》)。 這樣就會導致一個問題,如果程序不呼叫wait() 或 waitpid() 的話, 那麼保留的那段資訊就不會釋放,其程序號就會一直被佔用,但是系統所能使用的程序號是有限的,如果大量的產生僵死程序,將因為沒有可用的程序號而導致系統不能產生新的程序.此即為殭屍程序的危害,應當避免。
子程序已執行結束,父程序未呼叫 wait() 或 waitpid() 函式回收子程序的資源是子程序變為殭屍程序的原因。
殭屍程序測試程式如下:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
pid_t pid;
pid = fork(); //建立程序
if( pid < 0 ){ // 出錯
perror("fork error:");
exit(1);
}else if( 0 == pid ){ // 子程序
printf("I am child process.I am exiting.\n");
printf("[son id]: %d\n", getpid() );
exit(0);
}else if( pid > 0){ // 父程序
// 父程序沒有呼叫 wati() 或 watipid()
sleep(1); // 保證子程序先執行
printf("I am father process.I will sleep two seconds\n");
printf("[father id]: %d\n", getpid() );
while(1); // 不讓父程序退出
}
return 0;
}
我們在一個終端執行以上程式:
在終端敲:ps -ef | grep defunct ,後面尖括號裡是 defunct 的都是殭屍程序。
我們另啟一個終端,檢視程序的狀態,有哪些是殭屍程序:
或者:
如何避免殭屍程序?
1)最簡單的方法,父程序通過 wait() 和 waitpid() 等函式等待子程序結束,但是,這會導致父程序掛起。具體用法,請看《程序的控制:結束程序、等待程序結束》。
2)如果父程序要處理的事情很多,不能夠掛起,通過 signal() 函式人為處理訊號 SIGCHLD , 只要有子程序退出自動呼叫指定好的回撥函式,因為子程序結束後, 父程序會收到該訊號 SIGCHLD ,可以在其回撥函式裡呼叫 wait() 或 waitpid() 回收。關於訊號的更詳細用法,請看《訊號中斷處理》。
測試程式碼如下:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
void sig_child(int signo)
{
pid_t pid;
//處理殭屍程序, -1 代表等待任意一個子程序, WNOHANG代表不阻塞
while( (pid = waitpid(-1, NULL, WNOHANG)) > 0 ){
printf("child %d terminated.\n", pid);
}
}
int main()
{
pid_t pid;
// 建立捕捉子程序退出訊號
// 只要子程序退出,觸發SIGCHLD,自動呼叫sig_child()
signal(SIGCHLD, sig_child);
pid = fork(); // 建立程序
if (pid < 0){ // 出錯
perror("fork error:");
exit(1);
}else if(pid == 0){ // 子程序
printf("I am child process,pid id %d.I am exiting.\n",getpid());
exit(0);
}else if(pid > 0){ // 父程序
sleep(2); // 保證子程序先執行
printf("I am father, i am exited\n\n");
system("ps -ef | grep defunct"); // 檢視有沒有殭屍程序
}
return 0;
}
執行結果如下:
3)如果父程序不關心子程序什麼時候結束,那麼可以用signal(SIGCHLD, SIG_IGN)通知核心,自己對子程序的結束不感興趣,父程序忽略此訊號,那麼子程序結束後,核心會回收, 並不再給父程序傳送訊號。關於訊號的更詳細用法,請看《訊號中斷處理》。
測試程式碼如下:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
int main()
{
pid_t pid;
// 忽略子程序退出訊號的訊號
// 那麼子程序結束後,核心會回收, 並不再給父程序傳送訊號
signal(SIGCHLD, SIG_IGN);
pid = fork(); // 建立程序
if (pid < 0){ // 出錯
perror("fork error:");
exit(1);
}else if(pid == 0){ // 子程序
printf("I am child process,pid id %d.I am exiting.\n",getpid());
exit(0);
}else if(pid > 0){ // 父程序
sleep(2); // 保證子程序先執行
printf("I am father, i am exited\n\n");
system("ps -ef | grep defunct"); // 檢視有沒有殭屍程序
}
return 0;
}
執行結果如下:
4)還有一些技巧,就是 fork() 兩次,父程序 fork() 一個子程序,然後繼續工作,子程序 fork() 一 個孫程序後退出,那麼孫程序被 init 接管,孫程序結束後,init (1 號程序)會回收。不過子程序的回收還要自己做。《UNIX環境高階程式設計》8.6節說的非常詳細。原理是將子程序成為孤兒程序,從而其的父程序變為 init 程序(1 號程序),通過 init 程序(1 號程序)可以處理殭屍程序。更多詳情,請看《特殊程序之孤兒程序》。
測試程式如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int main()
{
pid_t pid;
//建立第一個子程序
pid = fork();
if (pid < 0){ // 出錯
perror("fork error:");
exit(1);
}else if (pid == 0){//子程序
//子程序再建立子程序
printf("I am the first child process.pid:%d\tppid:%d\n",getpid(),getppid());
pid = fork();
if (pid < 0){
perror("fork error:");
exit(1);
}else if(pid == 0){ // 子程序
//睡眠3s保證下面的父程序退出,這樣當前子程序的父親就是 init 程序
sleep(3);
printf("I am the second child process.pid: %d\tppid:%d\n",getpid(),getppid());
exit(0);
}else if (pid >0){ //父程序退出
printf("first procee is exited.\n");
exit(0);
}
}else if(pid > 0){ // 父程序
// 父程序處理第一個子程序退出,回收其資源
if (waitpid(pid, NULL, 0) != pid){
perror("waitepid error:");
exit(1);
}
exit(0);
}
return 0;
}
執行結果如下:
孤兒程序(Orphan Process)
父程序執行結束,但子程序還在執行(未執行結束)的子程序就稱為孤兒程序(Orphan Process)。孤兒程序最終會被 init 程序(程序號為 1 )所收養,並由 init 程序對它們完成狀態收集工作。
孤兒程序是沒有父程序的程序,為避免孤兒程序退出時無法釋放所佔用的資源而變為殭屍程序,程序號為 1 的 init 程序將會接受這些孤兒程序,這一過程也被稱為“收養”。init 程序就好像是一個孤兒院,專門負責處理孤兒程序的善後工作。每當出現一個孤兒程序的時候,核心就把孤 兒程序的父程序設定為 init ,而 init 程序會迴圈地 wait() 它的已經退出的子程序。這樣,當一個孤兒程序淒涼地結束了其生命週期的時候,init 程序就會代表黨和政府出面處理它的一切善後工作。因此孤兒程序並不會有什麼危害。
注意:如果是64位系統,孤兒程序的父程序號並不是 1 號。
孤兒程序的測試例子:
-
#include <stdio.h>
-
#include <stdlib.h>
-
#include <unistd.h>
-
#include <errno.h>
-
int main()
-
{
-
pid_t pid;
-
//建立程序
-
pid = fork();
-
if (pid < 0){ // 出錯
-
perror("fork error:");
-
exit(1);
-
}else if (pid == 0){//子程序
-
sleep(2); // 保證父程序先結束
-
printf("son proess: [son id] = %d, [son's father id] = %d\n", getpid(), getppid());
-
exit(0);
-
}else if(pid > 0){ // 父程序
-
printf("father process, i am exited\n");
-
exit(0);
-
}
-
return 0;
-
}
執行結果如下:
守護程序(Daemon Process)
守護程序(Daemon Process),也就是通常說的 Daemon 程序(精靈程序),是 Linux 中的後臺服務程序。它是一個生存期較長的程序,通常獨立於控制終端並且週期性地執行某種任務或等待處理某些發生的事件。
守護程序是個特殊的孤兒程序,這種程序脫離終端,為什麼要脫離終端呢?之所以脫離於終端是為了避免程序被任何終端所產生的資訊所打斷,其在執行過程中的資訊也不在任何終端上顯示。由於在 Linux 中,每一個系統與使用者進行交流的介面稱為終端,每一個從此終端開始執行的程序都會依附於這個終端,這個終端就稱為這些程序的控制終端,當控制終端被關閉時,相應的程序都會自動關閉。
Linux 的大多數伺服器就是用守護程序實現的。比如,Internet 伺服器 inetd,Web 伺服器 httpd 等。
如何檢視守護程序
在終端敲:ps axj
a 表示不僅列當前使用者的程序,也列出所有其他使用者的程序
x 表示不僅列有控制終端的程序,也列出所有無控制終端的程序
j 表示列出與作業控制相關的資訊
從上圖可以看出守護進行的一些特點:
守護程序基本上都是以超級使用者啟動( UID 為 0 )
沒有控制終端( TTY 為 ?)
終端程序組 ID 為 -1 ( TPGID 表示終端程序組 ID)
一般情況下,守護程序可以通過以下方式啟動:
在系統啟動時由啟動指令碼啟動,這些啟動指令碼通常放在 /etc/rc.d 目錄下;
利用 inetd 超級伺服器啟動,如 telnet 等;
由 cron 定時啟動以及在終端用 nohup 啟動的程序也是守護程序。
如何編寫守護程序?
下面是編寫守護程序的基本過程:
1)遮蔽一些控制終端操作的訊號
這是為了防止守護進行在沒有執行起來前,控制終端受到干擾退出或掛起。關於訊號的更詳細用法,請看《訊號中斷處理》。
signal(SIGTTOU,SIG_IGN);
signal(SIGTTIN,SIG_IGN);
signal(SIGTSTP,SIG_IGN);
signal(SIGHUP ,SIG_IGN);
2)在後臺執行
這是為避免掛起控制終端將守護程序放入後臺執行。方法是在程序中呼叫 fork() 使父程序終止, 讓守護進行在子程序中後臺執行。
if( pid = fork() ){ // 父程序
exit(0); //結束父程序,子程序繼續
}
3)脫離控制終端、登入會話和程序組
有必要先介紹一下 Linux 中的程序與控制終端,登入會話和程序組之間的關係:程序屬於一個程序組,程序組號(GID)就是程序組長的程序號(PID)。登入會話可以包含多個程序組。這些程序組共享一個控制終端。這個控制終端通常是建立程序的 shell 登入終端。 控制終端、登入會話和程序組通常是從父程序繼承下來的。我們的目的就是要擺脫它們 ,使之不受它們的影響。因此需要呼叫 setsid() 使子程序成為新的會話組長,示例程式碼如下:
setsid();
setsid() 呼叫成功後,程序成為新的會話組長和新的程序組長,並與原來的登入會話和程序組脫離。由於會話過程對控制終端的獨佔性,程序同時與控制終端脫離。
4)禁止程序重新開啟控制終端
現在,程序已經成為無終端的會話組長,但它可以重新申請開啟一個控制終端。可以通過使程序不再成為會話組長來禁止程序重新開啟控制終端,採用的方法是再次建立一個子程序,示例程式碼如下:
if( pid=fork() ){ // 父程序
exit(0); // 結束第一子程序,第二子程序繼續(第二子程序不再是會話組長)
}
5)關閉開啟的檔案描述符
程序從建立它的父程序那裡繼承了開啟的檔案描述符。如不關閉,將會浪費系統資源,造成程序所在的檔案系統無法卸下以及引起無法預料的錯誤。按如下方法關閉它們:
// NOFILE 為 <sys/param.h> 的巨集定義
// NOFILE 為檔案描述符最大個數,不同系統有不同限制
for(i=0; i< NOFILE; ++i){// 關閉開啟的檔案描述符
close(i);
}
6)改變當前工作目錄
程序活動時,其工作目錄所在的檔案系統不能卸下。一般需要將工作目錄改變到根目錄。對於需要轉儲核心,寫執行日誌的程序將工作目錄改變到特定目錄如 /tmp。示例程式碼如下:
chdir("/");
7)重設檔案建立掩模
程序從建立它的父程序那裡繼承了檔案建立掩模。它可能修改守護程序所建立的檔案的存取許可權。為防止這一點,將檔案建立掩模清除:
umask(0);
8)處理 SIGCHLD 訊號
但對於某些程序,特別是伺服器程序往往在請求到來時生成子程序處理請求。如果父程序不等待子程序結束,子程序將成為殭屍程序(zombie)從而佔用系統資源。如果父程序等待子程序結束,將增加父程序的負擔,影響伺服器程序的併發效能。在 Linux 下可以簡單地將 SIGCHLD 訊號的操作設為 SIG_IGN 。關於訊號的更詳細用法,請看《訊號中斷處理》。
signal(SIGCHLD, SIG_IGN);
這樣,核心在子程序結束時不會產生殭屍程序。
示例程式碼如下:
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/syslog.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int init_daemon(void)
{
int pid;
int i;
// 1)遮蔽一些控制終端操作的訊號
signal(SIGTTOU,SIG_IGN);
signal(SIGTTIN,SIG_IGN);
signal(SIGTSTP,SIG_IGN);
signal(SIGHUP ,SIG_IGN);
// 2)在後臺執行
if( pid=fork() ){ // 父程序
exit(0); //結束父程序,子程序繼續
}else if(pid< 0){ // 出錯
perror("fork");
exit(EXIT_FAILURE);
}
// 3)脫離控制終端、登入會話和程序組
setsid();
// 4)禁止程序重新開啟控制終端
if( pid=fork() ){ // 父程序
exit(0); // 結束第一子程序,第二子程序繼續(第二子程序不再是會話組長)
}else if(pid< 0){ // 出錯
perror("fork");
exit(EXIT_FAILURE);
}
// 5)關閉開啟的檔案描述符
// NOFILE 為 <sys/param.h> 的巨集定義
// NOFILE 為檔案描述符最大個數,不同系統有不同限制
for(i=0; i< NOFILE; ++i){
close(i);
}
// 6)改變當前工作目錄
chdir("/tmp");
// 7)重設檔案建立掩模
umask(0);
// 8)處理 SIGCHLD 訊號
signal(SIGCHLD,SIG_IGN);
return 0;
}
int main(int argc, char *argv[])
{
init_daemon();
while(1);
return 0;
}
執行結果如下:
---------------------
作者:Mike__Jiang
來源:CSDN
原文:https://blog.csdn.net/tennysonsky/article/details/45966571
版權宣告:本文為博主原創文章,轉載請附上博文連結!