信號就是軟中斷。
信號提供了異步處理事件的一種方式。例如,用戶在終端按下結束進程鍵,使一個進程提前終止。
1 信號的概念
每一個信號都有一個名字,它們的名字都以SIG打頭。例如,每當進程調用了abort函數時,都會產生一個SIGABRT信號。
每一個信號對應一個正整數,定義在頭文件<signal.h>中。
沒有信號對應整數0,kill函數使用信號編號0表示一種特殊情況,所以信號編號0又叫做空信號(null signal)。
下面的各種情況會產生一個信號:
- 當用戶在終端按下特定的鍵時,會產生信號。例如,當用戶按下DELETE按鍵(或Control-C)時,會產生一個中斷信號(interrupt signal,SIGINIT),該信號使得一個運行中的程序終止。
- 硬件異常可以產生信號。會引發硬件異常的情況如除以0,非法內存引用(invalid memory reference)等。這種情況會被硬件檢測到,並通知內核,然后內核產生相應的信號通知對應的運行進程。例如,當一個進程執行了一個非法的內存引用,會觸發SIGSEGV信號。
- kill函數允許當前進程向其他的進程或者進程組發送任意的信號。當然,這種方法存在限制:我們必須是信號接收進程的所有者,或者我們必須是超級用戶(superuser)。
- kill命令的作用和kill函數類似。這個命令多用戶殺死后台進程。
- 軟件異常可以根據不同的條件產生不同的信號。例如:網絡連接中接受的數據超出邊界時,會觸發SIGURG信號。
對於進程來說,信號是隨機產生的,所以進程不能簡單地根據檢測某個變量是否改變來判斷信號是否發生,而應該告訴內核“當這個信號發生時,做下面的這些事情”。
我們告訴內核當某個信號發生時做的事情叫做信號處理函數。信號處理函數有三種功能可供選擇:
- 忽略該信號。該行為適用於大部分的信號,除了兩個信號不能被忽略:SIGKILL和SIGSTOP。這兩個信號無能被忽略,是因為其作用是為內核和超級用戶提供了一種殺死或者暫停進程的萬無一失的方法(a surefire way)。
- 捕獲該信號。當某個信號發生時,我們告訴進程去執行我們的一段程序。在該程序中,我們可以做任何操作來處理該種情況。兩個信號SIGKILL和SIGSTOP不可以被捕獲。
- 執行默認的信號處理程序。每個信號都有一個默認的處理程序,而大部分的信號默認處理程序都是終止該進程。
對於一些信號發生時,會造成進程終止,同時生成一個core文件,該core文件記錄了該進程終止時的內存情況,可以幫助調試和調查進程的終止狀態。
有幾種情況不會生成core文件:
- 如果進程設置了suid位(chmod u+s file),並且當前用戶不是程序文件的所有者;
- 如果進程設置了guid位(set-group-ID),並且當前用戶不是程序文件的組所有者;
- 如果過戶沒有當前工作目錄的寫權限;
- 如果core文件已經存在,並且用戶沒有該文件的寫權限;
- 該core文件太大(由參數RLIMIT_CORE限制)
2 signal函數
函數聲明
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
Returns: previous disposition of signal if OK, SIG_ERR on err.
函數聲明解析:
void (*signal(int signr, void (*handler)(int)))(int);
================================================
handler是一個函數指針,指向參數為單參數int,返回類型void的函數
signal是一個函數指針、這個函數指針指向一個參數為一個int型和一個handler型的指針、返回值是一個指向參數為int、返回值是void的函數的指針的指針。總結一下:
這個復雜的聲明可以用下面2種比較簡單的型式表達出來,如下:
第一種型式如下:
typedef void (*handler_pt)(int);
handler_pt signal1(int signum,handler_pt ahandler);
第二種型式如下:
typedef void handler_t(int);
handler_t* signal2(int signum, handler_t* ahandler);
------------------------------------------------------
以上這兩種形式結果是等價的,但也有區別,第一種形式定義的是函數指針類型,
sizeof(handler_pt)=4//borland c++ 5.6.4 for win 32,windos xp 32 platform
第二種形式定義的是函數類型,如果對他使用sizeof(handler_t)會提示:
sizeof may not be applied to a function
參數說明:
- signo:信號名
- func:三種取值選擇:常量SIG_IGN,常量SIG_DFL,或信號處理函數的地址。如果func的取值為SIG_IGN,則信號發生時忽略處理(除了兩個信號SIGKILL和SIGSTOP)。如果func的取值為SIG_DFL,則調用信號的默認處理函數。
在上面的聲明解析中我們可以看到,使用typedef可以簡化signal函數的聲明,后面對signal函數的調用也將使用簡化后的聲明:
typedef void Sigfunc(int);
Sigfunc *signal(int, Sigfunc *);
Example
該例子的作用是捕獲兩個用戶自定義的信號,並打印相關的信號信息。
使用函數pause來使程序掛起,知道接收到信號。
Code
#include "apue.h"
staticvoid sig_usr(int); /* one handler for both signals */
int
main(void)
{
if (signal(SIGUSR1, sig_usr) == SIG_ERR)
err_sys("can't catch SIGUSR1");
if (signal(SIGUSR2, sig_usr) == SIG_ERR)
err_sys("can't catch SIGUSR2");
for ( ; ; )
pause();
}
staticvoid
sig_usr(int signo) /* argument is signal number */
{
if (signo == SIGUSR1)
printf("received SIGUSR1\n");
else if (signo == SIGUSR2)
printf("received SIGUSR2\n");
else
err_dump("received signal %d\n", signo);
}
執行結果:
執行時,我們先讓該程序后台執行,然后調用kill命令向該進程發送信號。
kill並不真的會殺死進程,而只是發送信號。所以kill並不是很准確的描述了該命令的作用。
當我們調用kill 2081命令時,進程被終止,因為在信號處理函數中並沒有處理該信號,而該信號的默認處理程序為終止進程。
程序啟動態
程序執行時,所有信號的狀態都為默認值或者被忽略。
如果程序調用了exec系函數,則會改變信號的自定義處理函數為它的默認處理程序,因為在原來的程序中的處理函數地址對於新的程序來說是沒有意義的。
例如,在一個交互式的shell中,啟動一個后台進程,會設置該進程的中斷和退出信號的處理動作為忽略,這樣,當用戶在shell中鍵入中斷命令時,只會中斷前台進程,而不會影響后台進程。
這個例子也告訴我們了signal函數的一個限制:我們無法確認當前進程的一些信號的處理動作,除非我們現在改變它們。后面我們將學習sigaction函數來確認一個信號的處理動作,而不需要改變它們。
程序創建
當調用fork函數時,子進程繼承了父進程的信號處理函數。因為子進程拷貝了父進程的內存,所以信號處理函數的地址對於子進程來說也是有意義的。
3 不可靠的信號(Unreliable Signals)
在早期的Unix系統中,信號是不可靠的。
不可靠的意思是,信號是有可能丟失的。即信號發生了,但是進程沒有捕獲它。
我們希望內核可以記住信號,當我們ready時,告訴我們該信號發生,讓我們去處理。
早期的系統,對於信號機制的實現還有一個問題:當信號發生,執行了信號處理函數,該信號的處理函數就被置為默認的信號處理程序。因此,早期的關於信號的程序框架如下所示:
這段代碼的問題在於,在SIGINT信號發生后,且在對它的信號處理函數重置為sig_int前,有一個時間差,在這個時間差內,可能再發生一次SIGINT信號。
如果第二次SIGINT發生在信號處理函數重置前,則會執行它的默認處理動作,即終止進程。
早期實現還有一個問題,就是如果進程不希望某個信號發生,它只能選擇忽略它,而無法將該信號關閉。
一種使用場景是:我們不希望被信號打斷,但是希望記住它們發生過。代碼可能如下:
在這里,我們假設該信號只發生一次。
代碼的目的在於:我們等待信號發生,信號發生之前,進程停止,等待。
代碼的問題在於,有一個時間差,可能會發生異常情況,如果代碼的執行序列如下:
1 信號發生
2 while (sig_int_flag == 0)
3 sig_int_flag= 1
4 pause()
這時,進程暫停掛起,等待信號發生,但是實際上該信號已經發生過了。這就導致了信號沒有被捕獲。
4 可中斷系統調用
早期Unix操作系統的一個特性是:如果一個進程阻塞在一個“慢”系統調用,則該進程會收到一個信號,導致該進程被中斷。該系統調用返回一個錯誤,並且errno設置為EINTR。
系統調用被分為兩類:慢系統調用和其他系統調用。慢系統調用是那些可能永久阻塞的系統調用。慢系統調用包括:
- 讀數據函數可能阻塞調用者,如果數據不是特定的格式;
- 寫數據函數可能阻塞調用者,如果數據不能按照特定的格式立刻被接收。
- 按照某種格式打開某個文件
- pause函數和wait函數
- 特定的ioctl操作
- 一些進程間通信函數
對於可中斷系統調用,我們需要在代碼中處理errno EINTR:
為了避免需要顯式處理可中斷系統調用,一些可中斷系統調用在發生阻塞時會自動重啟。
這些會自動重啟的可中斷系統調用包括:ioctl, read, readv, write, writev, wait和waitepid。
如果某些應用並不希望這些系統調用自動重啟,可以該系統調用單獨設置SA_RESTART。
5 可重入函數
信號的發生導致程序的指令執行順序被打亂。
但是在信號處理函數中,無法知道原進程的執行情況。
如果原進程這個在分配內存或者釋放內存,或者調用了修改static變量的函數,並在信號處理函數中再次調用該函數,會發生不可預期的結果。
在信號處理函數中可以安全調用的函數稱為可重入函數,也叫做異步信號安全的函數。除了保證可重入,這些函數還會阻塞可能導致結果不一致的信號。
如果函數滿足下面的一種或者幾種條件,則說明是不可重入的函數:
- 使用static數據結構
- 調用malloc或free
- 標准IO庫中的函數,因為大部分的標准IO函數都使用了全局數據結構
6 SIGCLD語義
一直容易混淆的兩個信號是SIGCLD和SIGCHLD。
SIGCLD來自System V,而SIGCHLD來自BSD和POSIX.1。
BSD SIGCHLD的語義:當該信號發生時,說明子進程的狀態發生了改變,這時我們需要調用wait函數確認狀態的變化。
對於System V系統中,對信號SIGCLD的處理說明如下:
- 如果進程設置信號SIGCLD的處理動作為SIG_IGN,該進程的子進程將不會變成僵屍進程。這和默認的處理動作(SIG_DFL)是不同的,雖然默認的動作也是忽略,但是對於SIG_IGN,如果隨后調用了wait函數,調用進程會阻塞直到所有的子進程都終止,wait返回-1,並且設置errno為ECHILD。默認處理動作(SIG_DFL)是沒有后面的動作的。
- 如果我們對信號SIGCLD設置了捕獲,則內核會立刻檢查是否有任何子進程被等待,如果有,調用信號處理函數。
7 可靠信號及其語義
我們先定義幾個信號相關的概念:
- 信號產生:如果某一事件導致信號的發生,則叫做信號產生。該事件可能是硬件異常、軟件條件、終端產生的信號或者kill函數傳遞的信號等。當信號產生,內核需要在進程表中設置某個標志位。
- 信號送達:當信號處理函數被執行,則信號送達。
- 信號掛起:在信號產生和送達之間的時間叫做信號掛起。
- 信號阻塞:進程可以選擇不接收某個信號的傳達,叫做阻塞該信號。如果被阻塞的信號的處理動作為默認處理程序或者被捕獲處理,則該信號會一直處於掛起狀態,直到進程解除對該信號的阻塞或者改變該信號的處理動作為忽略。系統在信號傳遞時決定如何處理被阻塞信號,而不是信號產生時。函數sigpending的作用就是讓進程決定哪些信號被阻塞和掛起。
可靠機制,不同的標准對於異常情況有不同的處理:
- 如果一個被阻塞的信號多次產生,內核簡單地傳遞該信號一次。
- 如果多個信號等待被傳達,POSIX.1並不關注信號的傳達順序,而The Rationale for POSIX.1標准會保證和進程的當前狀態相關的信號先傳達。
- 每個進程都有一個信號掩碼來決定是否屏蔽某個信號,信號掩碼的每一位都對應一個信號,如果要阻塞某個信號,則將對應的信號置為1。
- 數據結構sigset_t被定義用來記錄信號掩碼(signal mask)
參考資料:
《Advanced Programming in the UNIX Envinronment 3rd》