Linux 信號,殭屍進程,(面試)

1,信號有那些是不能被屏蔽的???(SIGKILL和SIGSTOP

2,殭屍進程是什嘛樣子的,產生子進程的時候給其分配空間了之後,在變成殭屍進程之後,對這塊空間是如何處理

  的,是將其全部釋放掉???還是保留部分空間給(子進程的進程ID、終止狀態以及資源利用信息(CPU時間,內

  存使用量),因爲父進程可能會用到這些東西,????


信號(signal)是Linux進程間通信的一種機制,全稱爲軟中斷信號,也被稱爲軟中斷。信號本質上是在軟件層次上對硬件中斷機制的一種模擬。信號提供了一種處理異步事件的方法。每個信號的名字都以SIG字符開頭,爲正整型常量,定義在<signal.h>頭文件中(實際上,實現將信號定義在內核頭文件中,<signal.h>又包含該內核頭文件,如:Linux 3.2.0將信號定義在<bits/signum.h>中,FreeBSD 8.0將信號定義在<sys/signal.h>中)


信號有3種處理方式:

(1) 忽略此信號。大多數信號都使用這種方式處理。但是有兩種信號不能被忽略:SIGKILL和SIGSTOP。原因是這兩種信號向內核和超級用戶提供了使進程終止或停止的可靠方法。另外,如果忽略某些由硬件異常產生的信號(如非法內存引用、除0錯誤等),那進程的運行行爲是未定義的。

(2) 捕捉信號。通知內核在某種信號發生時,調用一個用戶函數來處理該信號。同樣地,不能捕捉SIGKILL和SIGSTOP信號。

(3) 執行系統默認動作。對大多數信號而言,系統默認動作就是終止該信號。



與其他進程間通信方式(例如管道、共享內存等)相比,信號所能傳遞的信息比較粗糙,只是一個整數。但正是由於傳遞的信息量少,信號也便於管理和使用,可以用於系統管理相關的任務,例如通知進程終結、中止或者恢復等。

每種信號用一個整型常量宏表示,以SIG開頭,比如SIGCHLD、SIGINT等,它們在系統頭文件<signal.h>中定義。

信號由內核(kernel)管理,產生方式多種多樣:
  • 可以由內核自身產生,比如出現硬件錯誤、內存讀取錯誤,分母爲0的除法等,內核需要通知相應進程。
  • 也可以由其他進程產生併發送給內核,再由內核傳遞給目標進程。

信號傳遞的過程:
  • 內核中針對每一個進程都有一個表來保存信號。
  • 當內核需要將信號傳遞給某個進程時,就在該進程對應的表中寫入信號,這樣就生成了信號。
  • 當該進程由用戶態陷入內核態,再次切換到用戶態之前,會查看錶中的信號。如果有信號,進程就會首先執行信號對應的操作,此時叫做執行信號。
  • 從生成信號到將信號傳遞給對應進程這段時間,信號處於等待狀態。
  • 我們可以編寫代碼,讓進程阻塞(block)某些信號,也就是讓這些信號始終處於等待的狀態,直到進程取消阻塞(unblock)或者忽略信號。







可再入函數

進程捕捉到信號並對其進行處理時,正常執行的指令序列就會被中斷,首先需要執行信號處理程序,之後則應該接着執行之前未完成的指令序列。但是在信號處理程序中,並不能判斷捕捉到信號時進程執行到什麼地方,如果進程正在執行malloc,而此時由於捕捉到信號而插入信號處理函數也要調用malloc,此時,由於malloc通常會爲它所分配的存儲區維護一個鏈表,而插入信號處理函數時,該進程正在修改鏈表,那麼結果是進程環境遭到破壞,丟失重要信息。

Single Unix Specification說明了在信號處理程序中保證調用安全的函數,這些函數是可再入的,稱爲異步信號安全(async-signal safe)函數。除了可再入外,在信號處理期間,它會阻塞任何引起不一致的信號發送。

一般不可再入函數有如下特點:

(1)   使用靜態數據結構;

(2)   調用malloc或free;

(3)   屬於標準I/O函數,因爲很多標準I/O庫都以不可再入的方式使用了全局數據結構。

 

可靠信號術語

不可靠的信號是指信號在處理之前可能丟失。在發送一個信號給進程時,我們說向進程遞送(delivery)了一個信號。在信號產生(generation)和遞送之間的時間間隔內,稱該信號是未決(pending)的。

如果進程採用“阻塞信號遞送”(每個信號都有一個信號屏蔽字(signal mask),它規定了當前要阻塞遞送到該進程的信號集。進程可以調用sigprocmask來檢測和更改當前信號屏蔽字。進程調用sigpending函數來判斷哪些信號是設置爲阻塞並處於pending狀態),而且對該信號的處理是採用系統默認動作或者捕捉該信號,那麼該信號將一直保持未決狀態,直到進程對信號解除阻塞,或者對該信號的處理改爲忽略。


信號種類

下表列出了一些常見信號:


每種信號都會有一個默認動作。默認動作就是腳本或程序接收到該信號所做出的默認操作。常見的默認動作有終止進

程、退出程序、忽略信號、重啓暫停的進程等,上表中也對部分默認動作進行了說明。



發送信號

有多種方式可以向程序或腳本發送信號,例如按下<Ctrl+C>組合鍵會發送SIGINT信號,終止當前進程。


還可以通過 kill 命令發送信號,語法爲:

kill -signal pid
signal爲要發送的信號,可以是信號名稱或數字;pid爲接收信號的進程ID。例如:

kill -1 1001
將SIGHUP信號發送給進程ID爲1001的程序,程序會終止執行

又如,強制殺死ID爲1001的進程:

kill -9 1001

捕獲信號

通常情況下,直接終止進程並不是我們所希望的。例如,按下<Ctrl+C>,進程被立即終止,不會清理創建的臨時文件,帶來系統垃圾,也不會保存正在進行的工作,導致需要重做。

可以通過編程來捕獲這些信號,當終止信號出現時,可以先進行清場和保存處理,再退出程序。

用戶程序可以通過C/C++等代碼捕獲信號,這將在Linux C編程中進行講解,這裏僅介紹如果通過Linux命令捕獲信號。

通過 trap 命令就可以捕獲信號,語法爲:

trap commands signals

commands爲Linux系統命令或用戶自定義命令;signals爲要捕獲的信號,可以爲信號名稱或數字。



捕獲到信號後,可以有三種處理:

  • 執行一段腳本來做一些處理工作,例如清理臨時文件;
  • 接受(恢復)信號的默認操作;
  • 忽略當前信號。

1) 清理臨時文件

腳本捕獲到終止信號後一個常見的動作就是清理臨時文件。例如:
<span style="font-size:18px;">trap "rm -f $WORKDIR/work1$$ $WORKDIR/dataout$$; exit" 2</span>

當用戶按下<Ctrl+C>後,腳本先清理臨時文件 work1$$ 和 dataout$$ 再退出。

注意:exit 命令是必須的,否則腳本捕獲到信號後會繼續執行而不是退出。

修改上面的腳本,使接收到 SIGHUP 時進行同樣的操作:
<span style="font-size:18px;">trap "rm $WORKDIR/work1$$ $WORKDIR/dataout$$; exit" 1 2</span>

幾點注意:
  • 如果執行多個命令,需要將命令用引號包圍;
  • 只有腳本執行到 trap 命令時纔會捕獲信號;
  • 再次接收到信號時還會執行同樣的操作。

上面的腳本,執行到 trap 命令時就會替換 WORKDIR 和 $$ 的值。如果希望接收到 SIGHUP 或 SIGINT 信號時再替換其值,那麼可以將命令放在單引號內,例如:

trap 'rm $WORKDIR/work1$$ $WORKDIR/dataout$$; exit' 1 2

2) 忽略信號

如果 trap 命令的 commands 爲空,將會忽略接收到的信號,即不做任何處理,也不執行默認動作。例如:
$ trap '' 2
也可以同時忽略多個信號:
$ trap '' 1 2 3 15
注意:必須被引號包圍,不能寫成下面的形式:
$ trap  2

3) 恢復默認動作

如果希望改變信號的默認動作後再次恢復默認動作,那麼省略 trap 命令的 commands 即可,例如:
$ trap 1 2
將恢復SIGHUP 和 SIGINT 信號的默認動作。




接下來談談殭屍進程:

比如進程採用exit()退出的時候,操作系統會進行一系列的處理工作,包括關閉打開的文件描述符、佔用的內存等等,但是,操作系統也會爲該進程保留少量的信息,比如進程ID號等信息,因而佔用了系統的資源。在一種極端的情況下,檔殭屍進程過多的時候,佔用了大量的進程ID,系統將無法產生新的進程,相當於系統的資源被耗盡。


給進程設置殭屍狀態的目的是維護子進程的信息,以便父進程在以後某個時間獲取。這些信息包括子進程的進程ID、終止狀態以及資源利用信息(CPU時間,內存使用量等等)。

這裏提到的這些信息等等都是將其寫入到內核中的,該進程所佔有的全部空間都已經釋放掉了。。。。

如果一個進程終止,而該進程有子進程處於殭屍狀態,那麼它的所有殭屍子進程的父進程ID將被重置爲1(init進程)。繼承這些子進程的init進程將清理它們(init進程將wait它們,從而去除殭屍狀態)。

        但通常情況下,我們是不願意留存殭屍進程的,它們佔用內核中的空間,最終可能導致我們耗盡進程資源。那麼爲什麼會產生殭屍進程以及如何避免產生殭屍進程呢?下邊我將從這兩個方面進行分析。

殭屍進程的原因

        我們知道,要在當前進程中生成一個子進程,一般需要調用fork這個系統調用,fork這個函數的特別之處在於一次調用,兩次返回,一次返回到父進程中,一次返回到子進程中,我們可以通過返回值來判斷其返回點:

pid_t child = fork();
if( child < 0  ) {     //fork error.
    perror("fork process fail.\n");
} else if( child ==0  ) {   // in child process
    printf(" fork succ, this run in child process\n ");
} else {                        // in parent process
    printf(" this run in parent process\n ");
}

如果子進程先於父進程退出, 同時父進程又沒有調用wait/waitpid,則該子進程將成爲殭屍進程。通過ps命令,我們可以看到該進程的狀態爲Z(表示僵死),如圖:

讓父進程休眠600s, 然後子進程先退出,我們就可以看到先退出的子進程成爲殭屍進程了(進程狀態爲Z)


但是,但是:子進程如果處理的時間比較長的話,主進程會被掛起


避免產生殭屍進程:

  1. 父進程使用wait()或者waitpid()之類的函數等待子進程退出
  2. 父進程先產生一個子進程,然後子進程再產生一個孫子進程,子進程在孫子進程之前退出。
  3. 使用信號函數sigaction爲SIGCHLD設置wait處理函數


我們知道了殭屍進程產生的原因,下邊我們看看如何避免產生殭屍進程。


子進程的子進程的避免:

對於這樣的情況可以採取連續fork()兩次的方法。簡而言之,首先父進程首先創建子進程,子進程創建孫子進程,由孫子進程處理事務,
而子進程再創建完孫子進程後,就退出。此時,孫子進程的父進程,也就是子進程退出了,因此孫子進程變爲了一個孤兒進程,Linux進程處理
孤兒的進程的方式,是init進程接管孤兒進程,而init進程的子進程不會成爲殭屍進程。

        一般,爲了防止產生殭屍進程,在fork子進程之後我們都要wait它們;同時,當子進程退出的時候,內核都會給父進程一個SIGCHLD信號,所以我們可以建立一個捕獲SIGCHLD信號的信號處理函數,在函數體中調用wait(或waitpid),就可以清理退出的子進程以達到防止殭屍進程的目的。如下代碼所示:

void sig_chld( int signo ) {
    pid_t pid;
    int stat;
    pid = wait(&stat);    
    printf( "child %d exit\n", pid );
    return;
}

int main() {
    signal(SIGCHLD,  &sig_chld);
}

現在main函數中給SIGCHLD信號註冊一個信號處理函數(sig_chld),然後在子進程退出的時候,內核遞交一個SIGCHLD的時候就會被主進程捕獲而進入信號處理函數sig_chld,然後再在sig_chld中調用wait,就可以清理退出的子進程。這樣退出的子進程就不會成爲殭屍進程。

        然後,即便我們捕獲SIGCHLD信號並且調用wait來清理退出的進程,仍然不能徹底避免產生殭屍進程;我們來看一種特殊的情況:

        我們假設有一個client/server的程序,對於每一個連接過來的client,server都啓動一個新的進程去處理來自這個client的請求。然後我們有一個client進程,在這個進程內,發起了多個到server的請求(假設5個),則server會fork 5個子進程來讀取client輸入並處理(同時,當客戶端關閉套接字的時候,每個子進程都退出);當我們終止這個client進程的時候 ,內核將自動關閉所有由這個client進程打開的套接字,那麼由這個client進程發起的5個連接基本在同一時刻終止。這就引發了5個FIN,每個連接一個。server端接受到這5個FIN的時候,5個子進程基本在同一時刻終止。這就又導致差不多在同一時刻遞交5個SIGCHLD信號給父進程,如圖:

正是這種同一信號多個實例的遞交造成了我們即將查看的問題。       

我們首先運行服務器程序,然後運行客戶端程序,運用ps命令看以看到服務器fork了5個子進程,如圖:

然後我們Ctrl+C終止客戶端進程,在我機器上邊測試,可以看到信號處理函數運行了3次,還剩下2個殭屍進程,如圖:


通過上邊這個實驗我們可以看出,建立信號處理函數並在其中調用wait並不足以防止出現殭屍進程,其原因在於:所有5個信號都在信號處理函數執行之前產生,而信號處理函數只執行一次,因爲Unix信號一般是不排隊的。 更爲嚴重的是,本問題是不確定的,依賴於客戶FIN到達服務器主機的時機,信號處理函數執行的次數並不確定。

       正確的解決辦法是調用waitpid而不是wait,這個辦法的方法爲:信號處理函數中,在一個循環內調用waitpid,以獲取所有已終止子進程的狀態。我們必須指定WNOHANG選項,他告知waitpid在有尚未終止的子進程在運行時不要阻塞。(我們不能在循環內調用wait,因爲沒有辦法防止wait在尚有未終止的子進程在運行時阻塞,wait將會阻塞到現有的子進程中第一個終止爲止),下邊的程序分別給出了這兩種處理辦法(func_wait, func_waitpid)。



殭屍進程的深層次探究:


什麼是殭屍進程

首先內核會釋放終止進程(調用了exit系統調用)所使用的所有存儲區,關閉所有打開的文件等,但內核爲每一個終止子進程保存了一定量的信息。這些信息至少包括進程ID,進程的終止狀態,以及該進程使用的CPU時間,所以當終止子進程的父進程調用wait或waitpid時就可以得到這些信息。

而殭屍進程就是指:一個進程執行了exit系統調用退出,而其父進程並沒有爲它收屍(調用wait或waitpid來獲得它的結束狀態)的進程。

任何一個子進程(init除外)在exit後並非馬上就消失,而是留下一個稱外殭屍進程的數據結構,等待父進程處理。這是每個子進程都必需經歷的階段。另外子進程退出的時候會向其父進程發送一個SIGCHLD信號。


殭屍進程的目的?

設置僵死狀態的目的是維護子進程的信息,以便父進程在以後某個時候獲取。這些信息至少包括進程ID,進程的終止狀態,以及該進程使用的CPU時間,所以當終止子進程的父進程調用wait或waitpid時就可以得到這些信息。如果一個進程終止,而該進程有子進程處於殭屍狀態,那麼它的所有殭屍子進程的父進程ID將被重置爲1(init進程)。繼承這些子進程的init進程將清理它們(也就是說init進程將wait它們,從而去除它們的殭屍狀態)。



如何避免殭屍進程?

  1. 通過signal(SIGCHLD, SIG_IGN)通知內核對子進程的結束不關心,由內核回收。如果不想讓父進程掛起,可以在父進程中加入一條語句:signal(SIGCHLD,SIG_IGN);表示父進程忽略SIGCHLD信號,該信號是子進程退出的時候向父進程發送的。
  2. 父進程調用wait/waitpid等函數等待子進程結束,如果尚無子進程退出wait會導致父進程阻塞waitpid可以通過傳遞WNOHANG使父進程不阻塞立即返回
  3. 如果父進程很忙可以用signal註冊信號處理函數,在信號處理函數調用wait/waitpid等待子進程退出。
  4. 通過兩次調用fork。父進程首先調用fork創建一個子進程然後waitpid等待子進程退出,子進程再fork一個孫進程後退出。這樣子進程退出後會被父進程等待回收,而對於孫子進程其父進程已經退出所以孫進程成爲一個孤兒進程,孤兒進程由init進程接管,孫進程結束後,init會等待回收。

第一種方法忽略SIGCHLD信號,這常用於併發服務器的性能的一個技巧因爲併發服務器常常fork很多子進程,子進程終結之後需要服務器進程去wait清理資源。如果將此信號的處理方式設爲忽略,可讓內核把殭屍子進程轉交給init進程去處理,省去了大量殭屍進程佔用系統資源。


殭屍進程處理辦法

1 wait()函數

#include <sys/types.h> 
#include <sys/wait.h>

pid_t wait(int *status);

進程一旦調用了wait,就立即阻塞自己,由wait自動分析是否當前進程的某個子進程已經退出,如果讓它找到了這樣一個已經變成殭屍的子進程,wait就會收集這個子進程的信息,並把它徹底銷燬後返回;如果沒有找到這樣一個子進程,wait就會一直阻塞在這裏,直到有一個出現爲止。 
參數status用來保存被收集進程退出時的一些狀態,它是一個指向int類型的指針。但如果我們對這個子進程是如何死掉的毫不在意,只想把這個殭屍進程消滅掉,(事實上絕大多數情況下,我們都會這樣想),我們就可以設定這個參數爲NULL,就象下面這樣:

   pid = wait(NULL);

如果成功,wait會返回被收集的子進程的進程ID,如果調用進程沒有子進程,調用就會失敗,此時wait返回-1,同時errno被置爲ECHILD。

  • wait系統調用會使父進程暫停執行,直到它的一個子進程結束爲止。
  • 返回的是子進程的PID,它通常是結束的子進程
  • 狀態信息允許父進程判定子進程的退出狀態,即從子進程的main函數返回的值或子進程中exit語句的退出碼。
  • 如果status不是一個空指針,狀態信息將被寫入它指向的位置

可以上述的一些宏判斷子進程的退出情況:


2 waitpid()函數

#include <sys/types.h> 
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);

參數:

status:如果不是空,會把狀態信息寫到它指向的位置,與wait一樣

options:允許改變waitpid的行爲,最有用的一個選項是WNOHANG,它的作用是防止waitpid把調用者的執行掛起

The value of options is an OR of zero or more  of  the  following  con- 
stants:

WNOHANG     return immediately if no child has exited.

WUNTRACED   also  return  if  a  child  has stopped (but not traced via 
            ptrace(2)).  Status for traced children which have  stopped 
            is provided even if this option is not specified.

WCONTINUED (since Linux 2.6.10) 
            also return if a stopped child has been resumed by delivery 
            of SIGCONT.

返回值:如果成功返回等待子進程的ID,失敗返回-1


對於waitpid的p i d參數的解釋與其值有關:

pid == -1 等待任一子進程。於是在這一功能方面waitpid與wait等效。

pid > 0 等待其進程I D與p i d相等的子進程。

pid == 0 等待其組I D等於調用進程的組I D的任一子進程。換句話說是與調用者進程同在一個組的進程。

pid < -1 等待其組I D等於p i d的絕對值的任一子進程

wait與waitpid區別:

  • 在一個子進程終止前, wait 使其調用者阻塞,而waitpid 有一選擇項,可使調用者不阻塞。
  • waitpid並不等待第一個終止的子進程—它有若干個選擇項,可以控制它所等待的特定進程。
  • 實際上wait函數是waitpid函數的一個特例。waitpid(-1, &status, 0);

 

示例:

如以下代碼會創建100個子進程,但是父進程並未等待它們結束,所以在父進程退出前會有100個殭屍進程。

複製代碼
#include <stdio.h>  
#include <unistd.h>  
   
int main() {  
   
  int i;  
  pid_t pid;  
   
  for(i=0; i<100; i++) {  
    pid = fork();  
    if(pid == 0)  
      break;  
  }  
   
  if(pid>0) {  
    printf("press Enter to exit...");  
    getchar();  
  }  
   
  return 0;  
}  
複製代碼

其中一個解決方法即是編寫一個SIGCHLD信號處理程序來調用wait/waitpid來等待子進程返回。

 

複製代碼
#include <stdio.h>  
#include <unistd.h>  
#include <signal.h>  
#include <sys/types.h>  
#include <sys/wait.h>  
   
void wait4children(int signo) {  
   
  int status;  
  wait(&status);  
   
}  
   
int main() {  
   
  int i;  
  pid_t pid;  
   
  signal(SIGCHLD, wait4children);  
   
  for(i=0; i<100; i++) {  
    pid = fork();  
    if(pid == 0)  
      break;  
  }  
   
  if(pid>0) {  
    printf("press Enter to exit...");  
    getchar();  
  }  
   
  return 0;  
}  
複製代碼

但是通過運行程序發現還是會有殭屍進程,而且每次殭屍進程的數量都不定。這是爲什麼呢?其實主要是因爲Linux的信號機制是不排隊的,假如在某一時間段多個子進程退出後都會發出SIGCHLD信號,但父進程來不及一個一個地響應,所以最後父進程實際上只執行了一次信號處理函數。但執行一次信號處理函數只等待一個子進程退出,所以最後會有一些子進程依然是殭屍進程。

雖然這樣但是有一點是明瞭的,就是收到SIGCHLD必然有子進程退出,而我們可以在信號處理函數裏循環調用waitpid函數來等待所有的退出的子進程。至於爲什麼不用wait,主要原因是在wait在清理完所有殭屍進程後再次等待會阻塞。

 

所以最佳方案如下:

複製代碼
#include <stdio.h>  
#include <unistd.h>  
#include <signal.h>  
#include <errno.h>  
#include <sys/types.h>  
#include <sys/wait.h>  
   
void wait4children(int signo) {  
  int status;  
  while(waitpid(-1, &status, WNOHANG) > 0);  
}  
   
int main() {  
   
  int i;  
  pid_t pid;  
   
  signal(SIGCHLD, wait4children);  
   
  for(i=0; i<100; i++) {  
    pid = fork();  
    if(pid == 0)  
      break;  
  }  
   
  if(pid>0) {  
    printf("press Enter to exit...");  
    getchar();  
  }  
   
  return 0;  
}  
複製代碼

這裏使用waitpid而不是使用wait的原因在於:我們在一個循環內調用waitpid,以獲取所有已終止子進程的狀態。我們必須指定WNOHANG選項,它告訴waitpid在有尚未終止的子進程在運行時不要阻塞。我們不能在循環內調用wait,因爲沒有辦法防止wait在正運行的子進程尚有未終止時阻塞。






本文轉載:http://www.cnblogs.com/yuxingfirst/p/3165407.html

    :http://blog.chinaunix.net/uid-27064719-id-4757432.html

    :http://www.cnblogs.com/wuchanming/p/4020463.html




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章