linux下殭屍程序(Defunct程序)的產生與避免
在測試基於 DirectFB+Gstreamer 的視訊聯播系統的一個 Demo 的時候,其中大量使用 system 呼叫的語句,例如在 menu 程式碼中的 system("./play") ,而且多次執行,這種情況下,在 ps -ef 列表中出現了大量的 defunct 程序,對程式的執行時有害的。按說system的原始碼中應該已經包含了wait,但也不能排除開發板上這個版本的system中可能沒有wait,總之,開發板上在呼叫system後新增wait之後,defunct程序不復存在了。
下面談談 defunct 程序,中文翻譯叫殭屍程序。下文整理於網路以及APUE一書。
一、什麼是殭屍程序
在UNIX 系統中,一個程序結束了,但是他的父程序沒有等待(呼叫wait / waitpid)他,那麼他將變成一個殭屍程序。當用ps命令觀察程序的執行狀態時,看到這些程序的狀態列為defunct。殭屍程序是一個早已死亡的程序,但在程序表(processs table)中仍佔了一個位置(slot)。
但是如果該程序的父程序已經先結束了,那麼該程序就不會變成殭屍程序。因為每個程序結束的時候,系統都會掃描當前系統中所執行的所有程序,看看有沒有哪個程序是剛剛結束的這個程序的子程序,如果是的話,就由Init程序來接管他,成為他的父程序,從而保證每個程序都會有一個父程序。而Init程序會自動wait其子程序,因此被Init接管的所有程序都不會變成殭屍程序。
二、UNIX下程序的運作方式
每個Unix程序在程序表裡都有一個進入點(entry),核心程序執行該程序時使用到的一切資訊都儲存在進入點。當用 ps 命令察看系統中的程序資訊時,看到的就是程序表中的相關資料。當以fork()系統呼叫建立一個新的程序後,核心程序就會在程序表中給這個新程序分配一個進入點,然後將相關資訊儲存在該進入點所對應的程序表內。這些資訊中有一項是其父程序的識別碼。
子程序的結束和父程序的執行是一個非同步過程,即父程序永遠無法預測子程序到底什麼時候結束。那麼會不會因為父程序太忙來不及 wait 子程序,或者說不知道子程序什麼時候結束,而丟失子程序結束時的狀態資訊呢?
不會。因為UNIX提供了一種機制可以保證,只要父程序想知道子程序結束時的狀態資訊,就可以得到。這種機制就是:當子程序走完了自己的生命週期後,它會執行exit()系統呼叫,核心釋放該程序所有的資源,包括開啟的檔案,佔用的記憶體等。但是仍然為其保留一定的資訊(包括程序號the process ID,退出碼exit code,退出狀態the terminationstatus of the process,執行時間the amount of CPU time taken by the process等),這些資料會一直保留到系統將它傳遞給它的父程序為止,直到父程序通過wait / waitpid來取時才釋放。
也就是說,當一個程序死亡時,它並不是完全的消失了。程序終止,它不再執行,但是還有一些殘留的資料等待父程序收回。當父程序 fork() 一個子程序後,它必須用 wait() (或者 waitpid())等待子程序退出。正是這個 wait() 動作來讓子程序的殘留資料消失。
三、殭屍程序的危害
如果父程序不呼叫wait / waitpid的話,那麼保留的那段資訊就不會釋放,其程序號就會一直被佔用,但是系統的程序表容量是有限的,所能使用的程序號也是有限的,如果大量的產生殭屍程序,將因為沒有可用的程序號而導致系統不能產生新的程序。
所以,defunct程序不僅佔用系統的記憶體資源,影響系統的效能,而且如果其數目太多,還會導致系統癱瘓。而且,由於排程程式無法選中Defunct 程序,所以不能用kill命令刪除Defunct 程序,惟一的方法只有重啟系統。
四、殭屍程序的產生
如果子程序死亡時父程序沒有 wait(),通常用 ps 可以看到它被顯示為“<defunct>”,這樣就產生了殭屍程序。它將永遠保持這樣直到父程序 wait()。
由此可見,defunct程序的出現時間是在子程序終止後,但是父程序尚未讀取這些資料之前。利用這一點我們可以用下面的程式建立一個defunct 程序:
- #include <stdio.h>
- #include<sys/types.h>
- main()
- {
- if(!fork())
- {
- printf(“child pid=%d\n”, getpid());
- exit(0);
- }
- sleep(20);
- printf(“parent pid=%d \n”, getpid());
- exit(0);
- }
當上述程式以後臺的方式執行時,第17行強迫程式睡眠20秒,讓使用者有時間輸入ps -e指令,觀察程序的狀態,我們看到程序表中出現了defunct程序。當父程序執行終止後,再用ps -e命令觀察時,我們會發現defunct程序也隨之消失。這是因為父程序終止後,init 程序會接管父程序留下的這些“孤兒程序”(orphan process),而這些“孤兒程序”執行完後,它在程序表中的進入點將被刪除。如果一個程式設計上有缺陷,就可能導致某個程序的父程序一直處於睡眠狀態或是陷入死迴圈,父程序沒有wait子程序,也沒有終止以使Init接管,該子程序執行結束後就變成了defunct程序,這個defunct 程序可能會一直留在系統中直到系統重新啟動。
在看一個產生殭屍程序的例子。
子程序要執行的程式test_prog
- //test.c
- #include <stdio.h>
- int main()
- {
- int i = 0;
- for (i = 0 ; i < 10; i++)
- {
- printf ("child time %d\n", i+1);
- sleep (1);
- }
- return 0;
- }
父程序father的程式碼father.c
- #include <stdio.h>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/wait.h>
- int main()
- {
- int pid = fork ();
- if (pid == 0)
- {
- system ("./test_prog");
- _exit (0);
- }else
- {
- int i = 0;
- /*
- int status = 0;
- while (!waitpid(pid, &status, WNOHANG))
- {
- printf ("father waiting%d\n", ++i);
- sleep (1);
- }*/
- while (1)
- {
- printf ("father waiting over%d\n", ++i);
- sleep (1);
- }
- return 0;
- }
- }
執行./father,當子程序退出後,由於父程序沒有對它的退出進行關注,會出現殭屍程序
- 20786 pts/0 00:00:00 father
- 20787 pts/0 00:00:00 father <defunct>
總結:子程序成為 defunct 直到父程序 wait(),除非父程序忽略了 SIGCLD 。更進一步,父程序沒有 wait() 就消亡(仍假設父程序沒有忽略 SIGCLD )的子程序(活動的或者 defunct)成為 init 的子程序,init 著手處理它們。
五、如何避免殭屍程序
1、父程序通過wait和waitpid等函式等待子程序結束,這會導致父程序掛起。
在上個例子中,如果我們略作修改,在第8行sleep()系統呼叫前執行wait()或waitpid()系統呼叫,則子程序在終止後會立即把它在程序表中的資料返回給父程序,此時系統會立即刪除該進入點。在這種情形下就不會產生defunct程序。
2. 如果父程序很忙,那麼可以用signal函式為SIGCHLD安裝handler。在子程序結束後,父程序會收到該訊號,可以在handler中呼叫wait回收。
3. 如果父程序不關心子程序什麼時候結束,那麼可以用signal(SIGCLD, SIG_IGN)或signal(SIGCHLD, SIG_IGN)通知核心,自己對子程序的結束不感興趣,那麼子程序結束後,核心會回收,並不再給父程序傳送訊號
4. fork兩次,父程序fork一個子程序,然後繼續工作,子程序fork一個孫程序後退出,那麼孫程序被init接管,孫程序結束後,init會回收。不過子程序的回收還要自己做。 下面就是Stevens給的採用兩次folk避免殭屍程序的示例:
- #include "apue.h"
- #include <sys/wait.h>
- int
- main(void)
- ...{
- pid_t pid;
- if ((pid = fork()) < 0) ...{
- err_sys("fork error");
- } else if (pid == 0) ...{ /**//* first child */
- if ((pid = fork()) < 0)
- err_sys("fork error");
- else if (pid > 0)
- exit(0); /**//* parent from second fork == first child */
- /**//*
- * We're the second child; our parent becomes init as soon
- * as our real parent calls exit() in the statement above.
- * Here's where we'd continue executing, knowing that when
- * we're done, init will reap our status.
- */
- sleep(2);
- printf("second child, parent pid = %d ", getppid());
- exit(0);
- }
- if (waitpid(pid, NULL, 0) != pid) /**//* wait for first child */
- err_sys("waitpid error");
- /**//*
- * We're the parent (the original process); we continue executing,
- * knowing that we're not the parent of the second child.
- */
- exit(0);
- }