原文章作者: rock (遊手好閒的石頭成)


在 unix 系統中,程序間有父子關係存在,當程序經由系統呼叫 fork() 產生另一個 程序時,這兩個程序間就存在了父子關係。

當子程序結束時,系統會將子程序的結束狀態保存起來,接著發出訊號 SIGCHLD 通 知父程序來取子程序的結束狀態碼,通常是透過系統呼叫 wait() 來取得。

如果父程序比子程序早結束的話,則子程序將成為孤兒,那麼系統會將此孤兒交給 程序 init (pid = 0) ,由init 程序取得子程序的結束狀態碼。 ps.在 unix 系統中,第一個被執行的程序,一定叫 init ,因此它的 pid 為 0 。

不論是由父程序還是 init 程序取得,當子程序的結束狀態碼被取出後,系統就會將 子程序的狀態資訊,從處理程序表中移除,到此,一個程序才算是整個結束掉。

而如果父程序仍然存在,但是卻忘了處理子程序結束的事情,那麼系統會將此子程序 的結束狀態碼一直保存在處理程序表中,直到父程序處理為止。

如果父程序根本忘了要處理的話,那麼就會形成一個 zombie 程序,已經完全沒有活 動,所使用的資源也都被系統回收了,但是仍然佔了處理程序表的一筆記錄。

因為系統對於 SIGCHLD 訊號的處理方式,預設是不理會,如果父程序忘了設定捕捉 SIGCHLD 訊號,也沒有使用 wait() 去等待子程序結束的話,就會出現 zombie 程 序。

這種情形通常發生在 deamon 程式或專門在背景運作的程式的身上,由於設計者的 疏失,導致 zombie 程序的產生,消耗處理程序表的空間,當處理程序表的空間被 佔滿後,則系統將無法將產生任何程序執行工作。

一般的程式由於生命週期有限,所以通常不需在意這個問題,當父程序的生命週期 結束後,子程序就成為孤兒,被系統交給 init 程序處理掉了。

但對一個 deamon 來說,由於生命週期無限,這個問題就不得不處理了。

以下有幾個方法可以解決這個問題:

1.利用兩次 fork() ,形成祖孫關係,由於系統只承認程序間的父子關係,並不承認 祖孫關係,這樣可讓新生的程序在結束時,自然地形成孤兒,由系統交給 init 程 序執行。

代碼:
if( fork() )
  if( fork() )
    exit(0); (父程序)
  else
    . . . (子程序)
else
  . . . (祖程序)

這個方法,適用在所有的系統中,不管是 BSD 還是 System V (SVR) 都可以,缺 點是要跑兩次 fork()。

不過在使用這個方法時,請確定父程序完全不需要知道子程序的結束狀態。

2.對一個 SYSV 系統來說,如果不需要知道子程序的結束狀態,父程序可以明確地告 訴系統它忽略子程序的結束狀態,系統在子程序結束時,就會直接將子程序的資訊 從處理程序表中移除,而不會通知並等待父程序。

代碼:
signal(SIGCHLD, SIG_IGN);


特別注意的是,雖然 SIGCHLD 的預設處理方式就是忽略,但對系統而言,預設處理 方式是設定為 SIG_DFL ,跟設定為 SIG_IGN 的意義不同,如果不明確地設定 SIGCHLD 訊號的處理方式為 SIG_IGN ,系統仍然會通知並等待父程序。

由於 POSIX 中,對於設定 SIGCHLD 的處理方式為 SIG_IGN 的後續動作,沒有定義 ,因此在不是 SYSV 的系統中,這個方法不能用。

3.對一個 BSD 系統來說,則必須捕捉 SIGCHLD 訊號,如下:

代碼:
void reapchild() {
  while( wait(NULL) <= 0 )
    /*NOTHING*/;
}
. . .
signal( SIGCHLD, reapchild);
這的方法仍然是在父程序確定不需要子程序結束狀態時用的。

這個方法只適用於 BSD 系統,這是由於 signal() 的處理方法,存在系統間的差異 ,在 BSD 系統上,當一個訊號發生,並且被捕捉後,系統仍然會繼續使用舊的設定 ,但在 SYSV 的系統上,當一個訊號發生並被捕捉後,系統會自動重設此訊號的處 理方式為 SIG_DFL ,這樣上面的寫法就會有問題: reapchild() 只能用一次。

在 System V 的系統中,應用第二種方法,或改用 POSIX 定義的 sigaction() 取 代 signal()、或修改 reapchild() 的寫法,改成:

代碼:
void reapchild() {
  signal( SIGCHLD, reapchild);   // 補上再設定的動作。
  while( wait(NULL) <= 0 )
    /*NOTHING*/;
}
修改後的內容,可以同時適用在 BSD 或 SVR 的系統上。

4.如果父程序想知道子程序的結束狀態,那只有捕捉 SIGCHLD 的方法可用了,為了避 免 signal() 在系統上的行為差異,這裡是用 sigcation() 。

代碼:
int child_stat;
void reapchild() {
  while( wait(&child_stat) <= 0 )
    /*NOTHING or your code*/;
}
. . .
struct sigaction act;
act.sa_handler = reapchild;
act.sa_flags = SA_NOCLDSTOP;
sigaction( SIGCHLD, &act, NULL);

由於 SIGCHLD 在子程序暫停時也會產生,但這裡要處理的是子程序結束的情形,而 不是子程序暫停,所以加上了 SA_NOCLDSTOP 的設定,要求系統在子程序暫停時, 不要對父程序發出 SIGCHLD 訊號。

PS. sigaction(), struct sigaction, SIGCHLD, SA_NOCLDSTOP 都是 POSIX 有定 義的。

由於 POSIX 沒有定義 signal() ,因此這裡如果不用 sigaction() 而用 signal() 的話,將會有系統間的差異存在。

如果真的想用 signal() (雖然我不知道有什麼理由非用不可) ,請參考第三種方法 中,關於 SVR 適用問題的修改內容。

最後在補充一點的是,這裡討論的方法,是假設這是一個非同步多工程式,在同一時 間需要產生多個子程序同時工作,在此非同步工作的情形下,父程序為了當握子程序 的動態,只有捕捉 SIGCHLD 訊號。

而如果這是一個同步工作程式,父程序需等待子程序完成特定工作,才能繼續後來 的工作時,就不需要捕捉 SIGCHLD ,直接利用系統呼叫 wait() 即可。

例如:

代碼:
if( fork() == 0 ) {
  /* 子程序 */ . . .
} else {
  /* 父程序 */
  while( wait(NULL) < 0 ) {
    /* 等待子程序結束 */
    if( errno == EINTR )
      continue;
    else if ( errno == ECHILD )
      FATAL("There are no children process!");
  }
  . . . /* 子程序結束後,才可進行的工作 */
}
附註:

System V Release = SYSV = SVR

SVR4 = System V Release 4.0

等待結束:等待子程序的結束狀態碼被取出
創作者介紹

邱小新の工作筆記

台南小新 發表在 痞客邦 PIXNET 留言(0) 人氣()