【摘要】本文分析了Linux內核對於信號的實現機制和應用層的相關處理。首先介紹了軟中斷信號的本質及信號的兩種不同分類方法尤其是不可靠信號的原理。接着分析了內核對於信號的處理流程包括信號的觸發/注冊/執行及注銷等。最后介紹了應用層的相關處理,主要包括信號處理函數的安裝、信號的發送、屏蔽阻塞等,最后給了幾個簡單的應用實例。
【關鍵字】軟中斷信號,signal,sigaction,kill,sigqueue,settimer,sigmask,sigprocmask,sigset_t
1 信號本質
軟中斷信號(signal,又簡稱為信號)用來通知進程發生了異步事件。在軟件層次上是對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一個中斷請求可以說是一樣的。信號是進程間通信機制中唯一的異步通信機制,一個進程不必通過任何操作來等待信號的到達,事實上,進程也不知道信號到底什么時候到達。進程之間可以互相通過系統調用kill發送軟中斷信號。內核也可以因為內部事件而給進程發送信號,通知進程發生了某個事件。信號機制除了基本通知功能外,還可以傳遞附加信息。
收到信號的進程對各種信號有不同的處理方法。處理方法可以分為三類:
第一種是類似中斷的處理程序,對於需要處理的信號,進程可以指定處理函數,由該函數來處理。
第二種方法是,忽略某個信號,對該信號不做任何處理,就象未發生過一樣。
第三種方法是,對該信號的處理保留系統的默認值,這種缺省操作,對大部分的信號的缺省操作是使得進程終止。進程通過系統調用signal來指定進程對某個信號的處理行為。
2 信號的種類
可以從兩個不同的分類角度對信號進行分類:
可靠性方面:可靠信號與不可靠信號;
與時間的關系上:實時信號與非實時信號。
2.1 可靠信號與不可靠信號
Linux信號機制基本上是從Unix系統中繼承過來的。早期Unix系統中的信號機制比較簡單和原始,信號值小於SIGRTMIN的信號都是不可靠信號。這就是"不可靠信號"的來源。它的主要問題是信號可能丟失。
隨着時間的發展,實踐證明了有必要對信號的原始機制加以改進和擴充。由於原來定義的信號已有許多應用,不好再做改動,最終只好又新增加了一些信號,並在一開始就把它們定義為可靠信號,這些信號支持排隊,不會丟失。
信號值位於SIGRTMIN和SIGRTMAX之間的信號都是可靠信號,可靠信號克服了信號可能丟失的問題。Linux在支持新版本的信號安裝函數sigation()以及信號發送函數sigqueue()的同時,仍然支持早期的signal()信號安裝函數,支持信號發送函數kill()。
信號的可靠與不可靠只與信號值有關,與信號的發送及安裝函數無關。目前linux中的signal()是通過sigation()函數實現的,因此,即使通過signal()安裝的信號,在信號處理函數的結尾也不必再調用一次信號安裝函數。同時,由signal()安裝的實時信號支持排隊,同樣不會丟失。
對於目前linux的兩個信號安裝函數:signal()及sigaction()來說,它們都不能把SIGRTMIN以前的信號變成可靠信號(都不支持排隊,仍有可能丟失,仍然是不可靠信號),而且對SIGRTMIN以后的信號都支持排隊。這兩個函數的最大區別在於,經過sigaction安裝的信號都能傳遞信息給信號處理函數,而經過signal安裝的信號不能向信號處理函數傳遞信息。對於信號發送函數來說也是一樣的。
2.2 實時信號與非實時信號
早期Unix系統只定義了32種信號,前32種信號已經有了預定義值,每個信號有了確定的用途及含義,並且每種信號都有各自的缺省動作。如按鍵盤的CTRL ^C時,會產生SIGINT信號,對該信號的默認反應就是進程終止。后32個信號表示實時信號,等同於前面闡述的可靠信號。這保證了發送的多個實時信號都被接收。
非實時信號都不支持排隊,都是不可靠信號;實時信號都支持排隊,都是可靠信號。
3 信號處理流程
對於一個完整的信號生命周期(從信號發送到相應的處理函數執行完畢)來說,可以分為三個階段:
信號誕生
信號在進程中注冊
信號的執行和注銷
3.1 信號誕生
信號事件的發生有兩個來源:硬件來源(比如我們按下了鍵盤或者其它硬件故障);軟件來源,最常用發送信號的系統函數是kill, raise, alarm和setitimer以及sigqueue函數,軟件來源還包括一些非法運算等操作。
這里按發出信號的原因簡單分類,以了解各種信號:
(1) 與進程終止相關的信號。當進程退出,或者子進程終止時,發出這類信號。
(2) 與進程例外事件相關的信號。如進程越界,或企圖寫一個只讀的內存區域(如程序正文區),或執行一個特權指令及其他各種硬件錯誤。
(3) 與在系統調用期間遇到不可恢復條件相關的信號。如執行系統調用exec時,原有資源已經釋放,而目前系統資源又已經耗盡。
(4) 與執行系統調用時遇到非預測錯誤條件相關的信號。如執行一個並不存在的系統調用。
(5) 在用戶態下的進程發出的信號。如進程調用系統調用kill向其他進程發送信號。
(6) 與終端交互相關的信號。如用戶關閉一個終端,或按下break鍵等情況。
(7) 跟蹤進程執行的信號。
Linux支持的信號列表如下。很多信號是與機器的體系結構相關的
信號值 默認處理動作 發出信號的原因
SIGHUP 1 A 終端掛起或者控制進程終止
SIGINT 2 A 鍵盤中斷(如break鍵被按下)
SIGQUIT 3 C 鍵盤的退出鍵被按下
SIGILL 4 C 非法指令
SIGABRT 6 C 由abort(3)發出的退出指令
SIGFPE 8 C 浮點異常
SIGKILL 9 AEF Kill信號
SIGSEGV 11 C 無效的內存引用
SIGPIPE 13 A 管道破裂: 寫一個沒有讀端口的管道
SIGALRM 14 A 由alarm(2)發出的信號
SIGTERM 15 A 終止信號
SIGUSR1 30,10,16 A 用戶自定義信號1
SIGUSR2 31,12,17 A 用戶自定義信號2
SIGCHLD 20,17,18 B 子進程結束信號
SIGCONT 19,18,25 進程繼續(曾被停止的進程)
SIGSTOP 17,19,23 DEF 終止進程
SIGTSTP 18,20,24 D 控制終端(tty)上按下停止鍵
SIGTTIN 21,21,26 D 后台進程企圖從控制終端讀
SIGTTOU 22,22,27 D 后台進程企圖從控制終端寫
處理動作一項中的字母含義如下
A 缺省的動作是終止進程
B 缺省的動作是忽略此信號,將該信號丟棄,不做處理
C 缺省的動作是終止進程並進行內核映像轉儲(dump core),內核映像轉儲是指將進程數據在內存的映像和進程在內核結構中的部分內容以一定格式轉儲到文件系統,並且進程退出執行,這樣做的好處是為程序員提供了方便,使得他們可以得到進程當時執行時的數據值,允許他們確定轉儲的原因,並且可以調試他們的程序。
D 缺省的動作是停止進程,進入停止狀況以后還能重新進行下去,一般是在調試的過程中(例如ptrace系統調用)
E 信號不能被捕獲
F 信號不能被忽略
3.2 信號在目標進程中注冊
在進程表的表項中有一個軟中斷信號域,該域中每一位對應一個信號。內核給一個進程發送軟中斷信號的方法,是在進程所在的進程表項的信號域設置對應於該信號的位。如果信號發送給一個正在睡眠的進程,如果進程睡眠在可被中斷的優先級上,則喚醒進程;否則僅設置進程表中信號域相應的位,而不喚醒進程。如果發送給一個處於可運行狀態的進程,則只置相應的域即可。
進程的task_struct結構中有關於本進程中未決信號的數據成員: struct sigpending pending:
struct sigpending{
struct sigqueue *head, *tail;
sigset_t signal;
};
第三個成員是進程中所有未決信號集,第一、第二個成員分別指向一個sigqueue類型的結構鏈(稱之為"未決信號信息鏈")的首尾,信息鏈中的每個sigqueue結構刻畫一個特定信號所攜帶的信息,並指向下一個sigqueue結構:
struct sigqueue{
struct sigqueue *next;
siginfo_t info;
}
信號在進程中注冊指的就是信號值加入到進程的未決信號集sigset_t signal(每個信號占用一位)中,並且信號所攜帶的信息被保留到未決信號信息鏈的某個sigqueue結構中。只要信號在進程的未決信號集中,表明進程已經知道這些信號的存在,但還沒來得及處理,或者該信號被進程阻塞。
當一個實時信號發送給一個進程時,不管該信號是否已經在進程中注冊,都會被再注冊一次,因此,信號不會丟失,因此,實時信號又叫做"可靠信號"。這意味着同一個實時信號可以在同一個進程的未決信號信息鏈中占有多個sigqueue結構(進程每收到一個實時信號,都會為它分配一個結構來登記該信號信息,並把該結構添加在未決信號鏈尾,即所有誕生的實時信號都會在目標進程中注冊)。
當一個非實時信號發送給一個進程時,如果該信號已經在進程中注冊(通過sigset_t signal指示),則該信號將被丟棄,造成信號丟失。因此,非實時信號又叫做"不可靠信號"。這意味着同一個非實時信號在進程的未決信號信息鏈中,至多占有一個sigqueue結構。
總之信號注冊與否,與發送信號的函數(如kill()或sigqueue()等)以及信號安裝函數(signal()及sigaction())無關,只與信號值有關(信號值小於SIGRTMIN的信號最多只注冊一次,信號值在SIGRTMIN及SIGRTMAX之間的信號,只要被進程接收到就被注冊)
3.3 信號的執行和注銷
內核處理一個進程收到的軟中斷信號是在該進程的上下文中,因此,進程必須處於運行狀態。當其由於被信號喚醒或者正常調度重新獲得CPU時,在其從內核空間返回到用戶空間時會檢測是否有信號等待處理。如果存在未決信號等待處理且該信號沒有被進程阻塞,則在運行相應的信號處理函數前,進程會把信號在未決信號鏈中占有的結構卸掉。
對於非實時信號來說,由於在未決信號信息鏈中最多只占用一個sigqueue結構,因此該結構被釋放后,應該把信號在進程未決信號集中刪除(信號注銷完畢);而對於實時信號來說,可能在未決信號信息鏈中占用多個sigqueue結構,因此應該針對占用sigqueue結構的數目區別對待:如果只占用一個sigqueue結構(進程只收到該信號一次),則執行完相應的處理函數后應該把信號在進程的未決信號集中刪除(信號注銷完畢)。否則待該信號的所有sigqueue處理完畢后再在進程的未決信號集中刪除該信號。
當所有未被屏蔽的信號都處理完畢后,即可返回用戶空間。對於被屏蔽的信號,當取消屏蔽后,在返回到用戶空間時會再次執行上述檢查處理的一套流程。
內核處理一個進程收到的信號的時機是在一個進程從內核態返回用戶態時。所以,當一個進程在內核態下運行時,軟中斷信號並不立即起作用,要等到將返回用戶態時才處理。進程只有處理完信號才會返回用戶態,進程在用戶態下不會有未處理完的信號。
處理信號有三種類型:進程接收到信號后退出;進程忽略該信號;進程收到信號后執行用戶設定用系統調用signal的函數。當進程接收到一個它忽略的信號時,進程丟棄該信號,就象沒有收到該信號似的繼續運行。如果進程收到一個要捕捉的信號,那么進程從內核態返回用戶態時執行用戶定義的函數。而且執行用戶定義的函數的方法很巧妙,內核是在用戶棧上創建一個新的層,該層中將返回地址的值設置成用戶定義的處理函數的地址,這樣進程從內核返回彈出棧頂時就返回到用戶定義的函數處,從函數返回再彈出棧頂時,才返回原先進入內核的地方。這樣做的原因是用戶定義的處理函數不能且不允許在內核態下執行(如果用戶定義的函數在內核態下運行的話,用戶就可以獲得任何權限)。
4 信號的安裝
如果進程要處理某一信號,那么就要在進程中安裝該信號。安裝信號主要用來確定信號值及進程針對該信號值的動作之間的映射關系,即進程將要處理哪個信號;該信號被傳遞給進程時,將執行何種操作。
linux主要有兩個函數實現信號的安裝:signal()、sigaction()。其中signal()只有兩個參數,不支持信號傳遞信息,主要是用於前32種非實時信號的安裝;而sigaction()是較新的函數(由兩個系統調用實現:sys_signal以及sys_rt_sigaction),有三個參數,支持信號傳遞信息,主要用來與 sigqueue() 系統調用配合使用,當然,sigaction()同樣支持非實時信號的安裝。sigaction()優於signal()主要體現在支持信號帶有參數。
4.1 signal()
#include <signal.h>
void (*signal(int signum, void (*handler))(int)))(int);
如果該函數原型不容易理解的話,可以參考下面的分解方式來理解:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler));
第一個參數指定信號的值,第二個參數指定針對前面信號值的處理,可以忽略該信號(參數設為SIG_IGN);可以采用系統默認方式處理信號(參數設為SIG_DFL);也可以自己實現處理方式(參數指定一個函數地址)。
如果signal()調用成功,返回最后一次為安裝信號signum而調用signal()時的handler值;失敗則返回SIG_ERR。
傳遞給信號處理例程的整數參數是信號值,這樣可以使得一個信號處理例程處理多個信號。
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
void sigroutine(int dunno)
{ /* 信號處理例程,其中dunno將會得到信號的值 */
switch (dunno) {
case 1:
printf("Get a signal -- SIGHUP ");
break;
case 2:
printf("Get a signal -- SIGINT ");
break;
case 3:
printf("Get a signal -- SIGQUIT ");
break;
}
return;
}
int main() {
printf("process id is %d ",getpid());
signal(SIGHUP, sigroutine); //* 下面設置三個信號的處理方法
signal(SIGINT, sigroutine);
signal(SIGQUIT, sigroutine);
for (;;) ;
}
其中信號SIGINT由按下Ctrl-C發出,信號SIGQUIT由按下Ctrl-發出。該程序執行的結果如下:
localhost:~$ ./sig_test
process id is 463
Get a signal -SIGINT //按下Ctrl-C得到的結果
Get a signal -SIGQUIT //按下Ctrl-得到的結果
//按下Ctrl-z將進程置於后台
[1]+ Stopped ./sig_test
localhost:~$ bg
[1]+ ./sig_test &
localhost:~$ kill -HUP 463 //向進程發送SIGHUP信號
localhost:~$ Get a signal – SIGHUP
kill -9 463 //向進程發送SIGKILL信號,終止進程
localhost:~$
4.2 sigaction()
#include <signal.h>
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
sigaction函數用於改變進程接收到特定信號后的行為。該函數的第一個參數為信號的值,可以為除SIGKILL及SIGSTOP外的任何一個特定有效的信號(為這兩個信號定義自己的處理函數,將導致信號安裝錯誤)。第二個參數是指向結構sigaction的一個實例的指針,在結構sigaction的實例中,指定了對特定信號的處理,可以為空,進程會以缺省方式對信號處理;第三個參數oldact指向的對象用來保存返回的原來對相應信號的處理,可指定oldact為NULL。如果把第二、第三個參數都設為NULL,那么該函數可用於檢查信號的有效性。
第二個參數最為重要,其中包含了對指定信號的處理、信號所傳遞的信息、信號處理函數執行過程中應屏蔽掉哪些信號等等。
sigaction結構定義如下:
struct sigaction {
union{
__sighandler_t _sa_handler;
void (*_sa_sigaction)(int,struct siginfo *, void *);
}_u
sigset_t sa_mask;
unsigned long sa_flags;
}
1、聯合數據結構中的兩個元素_sa_handler以及*_sa_sigaction指定信號關聯函數,即用戶指定的信號處理函數。除了可以是用戶自定義的處理函數外,還可以為SIG_DFL(采用缺省的處理方式),也可以為SIG_IGN(忽略信號)。
2、由_sa_sigaction是指定的信號處理函數帶有三個參數,是為實時信號而設的(當然同樣支持非實時信號),它指定一個3參數信號處理函數。第一個參數為信號值,第三個參數沒有使用,第二個參數是指向siginfo_t結構的指針,結構中包含信號攜帶的數據值,參數所指向的結構如下:
siginfo_t {
int si_signo; /* 信號值,對所有信號有意義*/
int si_errno; /* errno值,對所有信號有意義*/
int si_code; /* 信號產生的原因,對所有信號有意義*/
union{ /* 聯合數據結構,不同成員適應不同信號 */
//確保分配足夠大的存儲空間
int _pad[SI_PAD_SIZE];
//對SIGKILL有意義的結構
struct{
...
}...
... ...
... ...
//對SIGILL, SIGFPE, SIGSEGV, SIGBUS有意義的結構
struct{
...
}...
... ...
}
}
前面在討論系統調用sigqueue發送信號時,sigqueue的第三個參數就是sigval聯合數據結構,當調用sigqueue時,該數據結構中的數據就將拷貝到信號處理函數的第二個參數中。這樣,在發送信號同時,就可以讓信號傳遞一些附加信息。信號可以傳遞信息對程序開發是非常有意義的。
3、sa_mask指定在信號處理程序執行過程中,哪些信號應當被阻塞。缺省情況下當前信號本身被阻塞,防止信號的嵌套發送,除非指定SA_NODEFER或者SA_NOMASK標志位。
注:請注意sa_mask指定的信號阻塞的前提條件,是在由sigaction()安裝信號的處理函數執行過程中由sa_mask指定的信號才被阻塞。
4、sa_flags中包含了許多標志位,包括剛剛提到的SA_NODEFER及SA_NOMASK標志位。另一個比較重要的標志位是SA_SIGINFO,當設定了該標志位時,表示信號附帶的參數可以被傳遞到信號處理函數中,因此,應該為sigaction結構中的sa_sigaction指定處理函數,而不應該為sa_handler指定信號處理函數,否則,設置該標志變得毫無意義。即使為sa_sigaction指定了信號處理函數,如果不設置SA_SIGINFO,信號處理函數同樣不能得到信號傳遞過來的數據,在信號處理函數中對這些信息的訪問都將導致段錯誤(Segmentation fault)。
5 信號的發送
發送信號的主要函數有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。
5.1 kill()
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid,int signo)
該系統調用可以用來向任何進程或進程組發送任何信號。參數pid的值為信號的接收進程
pid>0 進程ID為pid的進程
pid=0 同一個進程組的進程
pid<0 pid!=-1 進程組ID為 -pid的所有進程
pid=-1 除發送進程自身外,所有進程ID大於1的進程
Sinno是信號值,當為0時(即空信號),實際不發送任何信號,但照常進行錯誤檢查,因此,可用於檢查目標進程是否存在,以及當前進程是否具有向目標發送信號的權限(root權限的進程可以向任何進程發送信號,非root權限的進程只能向屬於同一個session或者同一個用戶的進程發送信號)。
Kill()最常用於pid>0時的信號發送。該調用執行成功時,返回值為0;錯誤時,返回-1,並設置相應的錯誤代碼errno。下面是一些可能返回的錯誤代碼:
EINVAL:指定的信號sig無效。
ESRCH:參數pid指定的進程或進程組不存在。注意,在進程表項中存在的進程,可能是一個還沒有被wait收回,但已經終止執行的僵死進程。
EPERM: 進程沒有權力將這個信號發送到指定接收信號的進程。因為,一個進程被允許將信號發送到進程pid時,必須擁有root權力,或者是發出調用的進程的UID 或EUID與指定接收的進程的UID或保存用戶ID(savedset-user-ID)相同。如果參數pid小於-1,即該信號發送給一個組,則該錯誤表示組中有成員進程不能接收該信號。
5.2 sigqueue()
#include <sys/types.h>
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval val)
調用成功返回 0;否則,返回 -1。
sigqueue()是比較新的發送信號系統調用,主要是針對實時信號提出的(當然也支持前32種),支持信號帶有參數,與函數sigaction()配合使用。
sigqueue的第一個參數是指定接收信號的進程ID,第二個參數確定即將發送的信號,第三個參數是一個聯合數據結構union sigval,指定了信號傳遞的參數,即通常所說的4字節值。
typedef union sigval {
int sival_int;
void *sival_ptr;
}sigval_t;
sigqueue()比kill()傳遞了更多的附加信息,但sigqueue()只能向一個進程發送信號,而不能發送信號給一個進程組。如果signo=0,將會執行錯誤檢查,但實際上不發送任何信號,0值信號可用於檢查pid的有效性以及當前進程是否有權限向目標進程發送信號。
在調用sigqueue時,sigval_t指定的信息會拷貝到對應sig 注冊的3參數信號處理函數的siginfo_t結構中,這樣信號處理函數就可以處理這些信息了。由於sigqueue系統調用支持發送帶參數信號,所以比kill()系統調用的功能要靈活和強大得多。
5.3 alarm()
#include <unistd.h>
unsigned int alarm(unsigned int seconds)
系統調用alarm安排內核為調用進程在指定的seconds秒后發出一個SIGALRM的信號。如果指定的參數seconds為0,則不再發送 SIGALRM信號。后一次設定將取消前一次的設定。該調用返回值為上次定時調用到發送之間剩余的時間,或者因為沒有前一次定時調用而返回0。
注意,在使用時,alarm只設定為發送一次信號,如果要多次發送,就要多次使用alarm調用。
5.4 setitimer()
現在的系統中很多程序不再使用alarm調用,而是使用setitimer調用來設置定時器,用getitimer來得到定時器的狀態,這兩個調用的聲明格式如下:
int getitimer(int which, struct itimerval *value);
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);
在使用這兩個調用的進程中加入以下頭文件:
#include <sys/time.h>
該系統調用給進程提供了三個定時器,它們各自有其獨有的計時域,當其中任何一個到達,就發送一個相應的信號給進程,並使得計時器重新開始。三個計時器由參數which指定,如下所示:
TIMER_REAL:按實際時間計時,計時到達將給進程發送SIGALRM信號。
ITIMER_VIRTUAL:僅當進程執行時才進行計時。計時到達將發送SIGVTALRM信號給進程。
ITIMER_PROF:當進程執行時和系統為該進程執行動作時都計時。與ITIMER_VIR-TUAL是一對,該定時器經常用來統計進程在用戶態和內核態花費的時間。計時到達將發送SIGPROF信號給進程。
定時器中的參數value用來指明定時器的時間,其結構如下:
struct itimerval {
struct timeval it_interval; /* 下一次的取值 */
struct timeval it_value; /* 本次的設定值 */
};
該結構中timeval結構定義如下:
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微秒,1秒 = 1000000 微秒*/
};
在setitimer 調用中,參數ovalue如果不為空,則其中保留的是上次調用設定的值。定時器將it_value遞減到0時,產生一個信號,並將it_value的值設定為it_interval的值,然后重新開始計時,如此往復。當it_value設定為0時,計時器停止,或者當它計時到期,而it_interval 為0時停止。調用成功時,返回0;錯誤時,返回-1,並設置相應的錯誤代碼errno:
EFAULT:參數value或ovalue是無效的指針。
EINVAL:參數which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一個。
下面是關於setitimer調用的一個簡單示范,在該例子中,每隔一秒發出一個SIGALRM,每隔0.5秒發出一個SIGVTALRM信號:
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/time.h>
int sec;
void sigroutine(int signo) {
switch (signo) {
case SIGALRM:
printf("Catch a signal -- SIGALRM ");
break;
case SIGVTALRM:
printf("Catch a signal -- SIGVTALRM ");
break;
}
return;
}
int main()
{
struct itimerval value,ovalue,value2;
sec = 5;
printf("process id is %d ",getpid());
signal(SIGALRM, sigroutine);
signal(SIGVTALRM, sigroutine);
value.it_value.tv_sec = 1;
value.it_value.tv_usec = 0;
value.it_interval.tv_sec = 1;
value.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &value, &ovalue);
value2.it_value.tv_sec = 0;
value2.it_value.tv_usec = 500000;
value2.it_interval.tv_sec = 0;
value2.it_interval.tv_usec = 500000;
setitimer(ITIMER_VIRTUAL, &value2, &ovalue);
for (;;) ;
}
該例子的屏幕拷貝如下:
localhost:~$ ./timer_test
process id is 579
Catch a signal – SIGVTALRM
Catch a signal – SIGALRM
Catch a signal – SIGVTALRM
Catch a signal – SIGVTALRM
Catch a signal – SIGALRM
Catch a signal –GVTALRM
5.5 abort()
#include <stdlib.h>
void abort(void);
向進程發送SIGABORT信號,默認情況下進程會異常退出,當然可定義自己的信號處理函數。即使SIGABORT被進程設置為阻塞信號,調用abort()后,SIGABORT仍然能被進程接收。該函數無返回值。
5.6 raise()
#include <signal.h>
int raise(int signo)
向進程本身發送信號,參數為即將發送的信號值。調用成功返回 0;否則,返回 -1。
6 信號集及信號集操作函數:
信號集被定義為一種數據類型:
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t
信號集用來描述信號的集合,每個信號占用一位。Linux所支持的所有信號可以全部或部分的出現在信號集中,主要與信號阻塞相關函數配合使用。下面是為信號集操作定義的相關函數:
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum)
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
sigemptyset(sigset_t *set)初始化由set指定的信號集,信號集里面的所有信號被清空;
sigfillset(sigset_t *set)調用該函數后,set指向的信號集中將包含linux支持的64種信號;
sigaddset(sigset_t *set, int signum)在set指向的信號集中加入signum信號;
sigdelset(sigset_t *set, int signum)在set指向的信號集中刪除signum信號;
sigismember(const sigset_t *set, int signum)判定信號signum是否在set指向的信號集中。
7 信號阻塞與信號未決:
每個進程都有一個用來描述哪些信號遞送到進程時將被阻塞的信號集,該信號集中的所有信號在遞送到進程后都將被阻塞。下面是與信號阻塞相關的幾個函數:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset));
int sigpending(sigset_t *set));
int sigsuspend(const sigset_t *mask));
sigprocmask()函數能夠根據參數how來實現對信號集的操作,操作主要有三種:
SIG_BLOCK 在進程當前阻塞信號集中添加set指向信號集中的信號
SIG_UNBLOCK 如果進程阻塞信號集中包含set指向信號集中的信號,則解除對該信號的阻塞
SIG_SETMASK 更新進程阻塞信號集為set指向的信號集
sigpending(sigset_t *set))獲得當前已遞送到進程,卻被阻塞的所有信號,在set指向的信號集中返回結果。
sigsuspend(const sigset_t *mask))用於在接收到某個信號之前, 臨時用mask替換進程的信號掩碼, 並暫停進程執行,直到收到信號為止。sigsuspend 返回后將恢復調用之前的信號掩碼。信號處理函數完成后,進程將繼續執行。該系統調用始終返回-1,並將errno設置為EINTR。
8 信號應用實例
linux下的信號應用並沒有想象的那么恐怖,程序員所要做的最多只有三件事情:
安裝信號(推薦使用sigaction());
實現三參數信號處理函數,handler(int signal,struct siginfo *info, void *);
發送信號,推薦使用sigqueue()。
實際上,對有些信號來說,只要安裝信號就足夠了(信號處理方式采用缺省或忽略)。其他可能要做的無非是與信號集相關的幾種操作。
實例一:信號發送及處理
實現一個信號接收程序sigreceive(其中信號安裝由sigaction())。
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void new_op(int,siginfo_t*,void*);
int main(int argc,char**argv)
{
struct sigaction act;
int sig;
sig=atoi(argv[1]);
sigemptyset(&act.sa_mask);
act.sa_flags=SA_SIGINFO;
act.sa_sigaction=new_op;
if(sigaction(sig,&act,NULL) < 0)
{
printf("install sigal error\n");
}
while(1)
{
sleep(2);
printf("wait for the signal\n");
}
}
void new_op(int signum,siginfo_t *info,void *myact)
{
printf("receive signal %d", signum);
sleep(5);
}
說明,命令行參數為信號值,后台運行sigreceive signo &,可獲得該進程的ID,假設為pid,然后再另一終端上運行kill -s signo pid驗證信號的發送接收及處理。同時,可驗證信號的排隊問題。
實例二:信號傳遞附加信息
主要包括兩個實例:
向進程本身發送信號,並傳遞指針參數
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void new_op(int,siginfo_t*,void*);
int main(int argc,char**argv)
{
struct sigaction act;
union sigval mysigval;
int i;
int sig;
pid_t pid;
char data[10];
memset(data,0,sizeof(data));
for(i=0;i < 5;i++)
data[i]='2';
mysigval.sival_ptr=data;
sig=atoi(argv[1]);
pid=getpid();
sigemptyset(&act.sa_mask);
act.sa_sigaction=new_op;//三參數信號處理函數
act.sa_flags=SA_SIGINFO;//信息傳遞開關,允許傳說參數信息給new_op
if(sigaction(sig,&act,NULL) < 0)
{
printf("install sigal error\n");
}
while(1)
{
sleep(2);
printf("wait for the signal\n");
sigqueue(pid,sig,mysigval);//向本進程發送信號,並傳遞附加信息
}
}
void new_op(int signum,siginfo_t *info,void *myact)//三參數信號處理函數的實現
{
int i;
for(i=0;i<10;i++)
{
printf("%c\n ",(*( (char*)((*info).si_ptr)+i)));
}
printf("handle signal %d over;",signum);
}
這個例子中,信號實現了附加信息的傳遞,信號究竟如何對這些信息進行處理則取決於具體的應用。
不同進程間傳遞整型參數:
把1中的信號發送和接收放在兩個程序中,並且在發送過程中傳遞整型參數。
信號接收程序:
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void new_op(int,siginfo_t*,void*);
int main(int argc,char**argv)
{
struct sigaction act;
int sig;
pid_t pid;
pid=getpid();
sig=atoi(argv[1]);
sigemptyset(&act.sa_mask);
act.sa_sigaction=new_op;
act.sa_flags=SA_SIGINFO;
if(sigaction(sig,&act,NULL)<0)
{
printf("install sigal error\n");
}
while(1)
{
sleep(2);
printf("wait for the signal\n");
}
}
void new_op(int signum,siginfo_t *info,void *myact)
{
printf("the int value is %d \n",info->si_int);
}
信號發送程序:
命令行第二個參數為信號值,第三個參數為接收進程ID。
#include <signal.h>
#include <sys/time.h>
#include <unistd.h>
#include <sys/types.h>
main(int argc,char**argv)
{
pid_t pid;
int signum;
union sigval mysigval;
signum=atoi(argv[1]);
pid=(pid_t)atoi(argv[2]);
mysigval.sival_int=8;//不代表具體含義,只用於說明問題
if(sigqueue(pid,signum,mysigval)==-1)
printf("send error\n");
sleep(2);
}
注:實例2的兩個例子側重點在於用信號來傳遞信息,目前關於在linux下通過信號傳遞信息的實例非常少,倒是Unix下有一些,但傳遞的基本上都是關於傳遞一個整數
實例三:信號阻塞及信號集操作
#include "signal.h"
#include "unistd.h"
static void my_op(int);
main()
{
sigset_t new_mask,old_mask,pending_mask;
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_flags=SA_SIGINFO;
act.sa_sigaction=(void*)my_op;
if(sigaction(SIGRTMIN+10,&act,NULL))
printf("install signal SIGRTMIN+10 error\n");
sigemptyset(&new_mask);
sigaddset(&new_mask,SIGRTMIN+10);
if(sigprocmask(SIG_BLOCK, &new_mask,&old_mask))
printf("block signal SIGRTMIN+10 error\n");
sleep(10);
printf("now begin to get pending mask and unblock SIGRTMIN+10\n");
if(sigpending(&pending_mask)<0)
printf("get pending mask error\n");
if(sigismember(&pending_mask,SIGRTMIN+10))
printf("signal SIGRTMIN+10 is pending\n");
if(sigprocmask(SIG_SETMASK,&old_mask,NULL)<0)
printf("unblock signal error\n");
printf("signal unblocked\n");
sleep(10);
}
static void my_op(int signum)
{
printf("receive signal %d \n",signum);
}
編譯該程序,並以后台方式運行。在另一終端向該進程發送信號(運行kill -s 42 pid,SIGRTMIN+10為42),查看結果可以看出幾個關鍵函數的運行機制,信號集相關操作比較簡單。
9 參考鳴謝:
linux信號處理機制(詳解),http://www.zxbc.cn/html/20080712/61613.html
Linux環境進程間通信(二): 信號(上),鄭彥興 (mlinux@163.com)
signal、sigaction、kill等手冊,最直接而可靠的參考資料。
http://www.linuxjournal.com/modules.php?op=modload&name=NS-help&file=man提供了許多系統調用、庫函數等的在線指南。
http://www.opengroup.org/onlinepubs/007904975/可以在這里對許多關鍵函數(包括系統調用)進行查詢,非常好的一個網址
進程間通信信號(上) http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html
進程間通信信號(下)http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index2.html
Linux信號處理機制
在Linux中,信號是進程間通訊的一種方式,它采用的是異步機制。當信號發送到某個進程中時,操作系統會中斷該進程的正常流程,並進入相應的信號處理函數執行操作,完成后再回到中斷的地方繼續執行。
需要說明的是,信號只是用於通知進程發生了某個事件,除了信號本身的信息之外,並不具備傳遞用戶數據的功能。
1 信號的響應動作
每個信號都有自己的響應動作,當接收到信號時,進程會根據信號的響應動作執行相應的操作,信號的響應動作有以下幾種:
- 中止進程(Term)
- 忽略信號(Ign)
- 中止進程並保存內存信息(Core)
- 停止進程(Stop)
- 繼續運行進程(Cont)
用戶可以通過signal或sigaction函數修改信號的響應動作(也就是常說的“注冊信號”,在文章的后面會舉例說明)。另外,在多線程中,各線程的信號響應動作都是相同的,不能對某個線程設置獨立的響應動作。
2 信號類型
Linux支持的信號類型可以參考下面給出的列表。
2.1 在POSIX.1-1990標准中的信號列表
| 信號 | 值 | 動作 | 說明 |
|---|---|---|---|
| SIGHUP | 1 | Term | 終端控制進程結束(終端連接斷開) |
| SIGINT | 2 | Term | 用戶發送INTR字符(Ctrl+C)觸發 |
| SIGQUIT | 3 | Core | 用戶發送QUIT字符(Ctrl+/)觸發 |
| SIGILL | 4 | Core | 非法指令(程序錯誤、試圖執行數據段、棧溢出等) |
| SIGABRT | 6 | Core | 調用abort函數觸發 |
| SIGFPE | 8 | Core | 算術運行錯誤(浮點運算錯誤、除數為零等) |
| SIGKILL | 9 | Term | 無條件結束程序(不能被捕獲、阻塞或忽略) |
| SIGSEGV | 11 | Core | 無效內存引用(試圖訪問不屬於自己的內存空間、對只讀內存空間進行寫操作) |
| SIGPIPE | 13 | Term | 消息管道損壞(FIFO/Socket通信時,管道未打開而進行寫操作) |
| SIGALRM | 14 | Term | 時鍾定時信號 |
| SIGTERM | 15 | Term | 結束程序(可以被捕獲、阻塞或忽略) |
| SIGUSR1 | 30,10,16 | Term | 用戶保留 |
| SIGUSR2 | 31,12,17 | Term | 用戶保留 |
| SIGCHLD | 20,17,18 | Ign | 子進程結束(由父進程接收) |
| SIGCONT | 19,18,25 | Cont | 繼續執行已經停止的進程(不能被阻塞) |
| SIGSTOP | 17,19,23 | Stop | 停止進程(不能被捕獲、阻塞或忽略) |
| SIGTSTP | 18,20,24 | Stop | 停止進程(可以被捕獲、阻塞或忽略) |
| SIGTTIN | 21,21,26 | Stop | 后台程序從終端中讀取數據時觸發 |
| SIGTTOU | 22,22,27 | Stop | 后台程序向終端中寫數據時觸發 |
注:其中SIGKILL和SIGSTOP信號不能被捕獲、阻塞或忽略。
2.2 在SUSv2和POSIX.1-2001標准中的信號列表
| 信號 | 值 | 動作 | 說明 |
|---|---|---|---|
| SIGTRAP | 5 | Core | Trap指令觸發(如斷點,在調試器中使用) |
| SIGBUS | 0,7,10 | Core | 非法地址(內存地址對齊錯誤) |
| SIGPOLL | Term | Pollable event (Sys V). Synonym for SIGIO | |
| SIGPROF | 27,27,29 | Term | 性能時鍾信號(包含系統調用時間和進程占用CPU的時間) |
| SIGSYS | 12,31,12 | Core | 無效的系統調用(SVr4) |
| SIGURG | 16,23,21 | Ign | 有緊急數據到達Socket(4.2BSD) |
| SIGVTALRM | 26,26,28 | Term | 虛擬時鍾信號(進程占用CPU的時間)(4.2BSD) |
| SIGXCPU | 24,24,30 | Core | 超過CPU時間資源限制(4.2BSD) |
| SIGXFSZ | 25,25,31 | Core | 超過文件大小資源限制(4.2BSD) |
注:在Linux 2.2版本之前,SIGSYS、SIGXCPU、SIGXFSZ以及SIGBUS的默認響應動作為Term,Linux 2.4版本之后這三個信號的默認響應動作改為Core。
2.3 其它信號
| 信號 | 值 | 動作 | 說明 |
|---|---|---|---|
| SIGIOT | 6 | Core | IOT捕獲信號(同SIGABRT信號) |
| SIGEMT | 7,-,7 | Term | 實時硬件發生錯誤 |
| SIGSTKFLT | -,16,- | Term | 協同處理器棧錯誤(未使用) |
| SIGIO | 23,29,22 | Term | 文件描述符准備就緒(可以開始進行輸入/輸出操作)(4.2BSD) |
| SIGCLD | -,-,18 | Ign | 子進程結束(由父進程接收)(同SIGCHLD信號) |
| SIGPWR | 29,30,19 | Term | 電源錯誤(System V) |
| SIGINFO | 29,-,- | 電源錯誤(同SIGPWR信號) | |
| SIGLOST | -,-,- | Term | 文件鎖丟失(未使用) |
| SIGWINCH | 28,28,20 | Ign | 窗口大小改變時觸發(4.3BSD, Sun) |
| SIGUNUSED | -,31,- | Core | 無效的系統調用(同SIGSYS信號) |
注意:列表中有的信號有三個值,這是因為部分信號的值和CPU架構有關,這些信號的值在不同架構的CPU中是不同的,三個值的排列順序為:1,Alpha/Sparc;2,x86/ARM/Others;3,MIPS。
例如SIGSTOP這個信號,它有三種可能的值,分別是17、19、23,其中第一個值(17)是用在Alpha和Sparc架構中,第二個值(19)用在x86、ARM等其它架構中,第三個值(23)則是用在MIPS架構中的。
3 信號機制
文章的前面提到過,信號是異步的,這就涉及信號何時接收、何時處理的問題。
我們知道,函數運行在用戶態,當遇到系統調用、中斷或是異常的情況時,程序會進入內核態。信號涉及到了這兩種狀態之間的轉換,過程可以先看一下下面的示意圖:

接下來圍繞示意圖,將信號分成接收、檢測和處理三個部分,逐一講解每一步的處理流程。
3.1 信號的接收
接收信號的任務是由內核代理的,當內核接收到信號后,會將其放到對應進程的信號隊列中,同時向進程發送一個中斷,使其陷入內核態。
注意,此時信號還只是在隊列中,對進程來說暫時是不知道有信號到來的。
3.2 信號的檢測
進程陷入內核態后,有兩種場景會對信號進行檢測:
- 進程從內核態返回到用戶態前進行信號檢測
- 進程在內核態中,從睡眠狀態被喚醒的時候進行信號檢測
當發現有新信號時,便會進入下一步,信號的處理。
3.3 信號的處理
信號處理函數是運行在用戶態的,調用處理函數前,內核會將當前內核棧的內容備份拷貝到用戶棧上,並且修改指令寄存器(eip)將其指向信號處理函數。
接下來進程返回到用戶態中,執行相應的信號處理函數。
信號處理函數執行完成后,還需要返回內核態,檢查是否還有其它信號未處理。如果所有信號都處理完成,就會將內核棧恢復(從用戶棧的備份拷貝回來),同時恢復指令寄存器(eip)將其指向中斷前的運行位置,最后回到用戶態繼續執行進程。
至此,一個完整的信號處理流程便結束了,如果同時有多個信號到達,上面的處理流程會在第2步和第3步驟間重復進行。
4 信號的使用
4.1 發送信號
用於發送信號的函數有raise、kill、killpg、pthread_kill、tgkill、sigqueue,這幾個函數的含義和用法都大同小異,這里主要介紹一下常用的raise和kill函數。
raise函數:向進程本身發送信號
函數聲明如下:
#include <signal.h> int raise(int sig);
函數功能是向當前程序(自身)發送信號,其中參數sig為信號值。
kill函數:向指定進程發送信號
函數聲明如下:
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig);
函數功能是向特定的進程發送信號,其中參數pid為進程號,sig為信號值。
在這里的參數pid,根據取值范圍不同,含義也不同,具體說明如下:
- pid > 0 :向進程號為pid的進程發送信號
- pid = 0 :向當前進程所在的進程組發送信號
- pid = -1 :向所有進程(除PID=1外)發送信號(權限范圍內)
- pid < -1 :向進程組號為-pid的所有進程發送信號
另外,當sig值為零時,實際不發送任何信號,但函數返回值依然有效,可以用於檢查進程是否存在。
4.2 等待信號被捕獲
等待信號的過程,其實就是將當前進程(線程)暫停,直到有信號發到當前進程(線程)上並被捕獲,函數有pause和sigsuspend。
pause函數:將進程(或線程)轉入睡眠狀態,直到接收到信號
函數聲明如下:
#include <unistd.h> int pause(void);
該函數調用后,調用者(進程或線程)會進入睡眠(Sleep)狀態,直到捕獲到(任意)信號為止。該函數的返回值始終為-1,並且調用結束后,錯誤代碼(errno)會被置為EINTR。
sigsuspend函數:將進程(或線程)轉入睡眠狀態,直到接收到特定信號
函數聲明如下:
#include <signal.h> int sigsuspend(const sigset_t *mask);
該函數調用后,會將進程的信號掩碼臨時修改(參數mask),然后暫停進程,直到收到符合條件的信號為止,函數返回前會將調用前的信號掩碼恢復。該函數的返回值始終為-1,並且調用結束后,錯誤代碼(errno)會被置為EINTR。
4.3 修改信號的響應動作
用戶可以自己重新定義某個信號的處理方式,即前面提到的修改信號的默認響應動作,也可以理解為對信號的注冊,可以通過signal或sigaction函數進行,這里以signal函數舉例說明。
首先看一下函數聲明:
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
第一個參數signum是信號值,可以從前面的信號列表中查到,第二個參數handler為處理函數,通過回調方式在信號觸發時調用。
下面為示例代碼:
#include <stdio.h> #include <signal.h> #include <unistd.h> /* 信號處理函數 */ void sig_callback(int signum) { switch (signum) { case SIGINT: /* SIGINT: Ctrl+C 按下時觸發 */ printf("Get signal SIGINT. \r\n"); break; /* 多個信號可以放到同一個函數中進行 通過信號值來區分 */ default: /* 其它信號 */ printf("Unknown signal %d. \r\n", signum); break; } return; } /* 主函數 */ int main(int argc, char *argv[]) { printf("Register SIGINT(%u) Signal Action. \r\n", SIGINT); /* 注冊SIGINT信號的處理函數 */ signal(SIGINT, sig_callback); printf("Waitting for Signal ... \r\n"); /* 等待信號觸發 */ pause(); printf("Process Continue. \r\n"); return 0; }
源文件下載:鏈接
例子中,將SIGINT信號(Ctrl+C觸發)的動作接管(打印提示信息),程序運行后,按下Ctrl+C,命令行輸出如下:
./linux_signal_example
Register SIGINT(2) Signal Action.
Waitting for Signal ...
^CGet signal SIGINT.
Process Continue.
進程收到SIGINT信號后,觸發響應動作,將提示信息打印出來,然后從暫停的地方繼續運行。這里需要注意的是,因為我們修改了SIGINT信號的響應動作(只打印信息,不做進程退出處理),所以我們按下Ctrl+C后,程序並沒有直接退出,而是繼續運行並將"Process Continue."打印出來,直至程序正常結束。
linux內核中異步通知機制--信號處理機制
什么是異步通知:很簡單,一旦設備准備好,就主動通知應用程序,這種情況下應用程序就不需要查詢設備狀態, 特像硬件上常提的“中斷的概念”。 比較准確的說法其實應該叫做“信號驅動的異步I/O”,信號是在軟件層次上對中斷機制的一種模擬。阻塞I/O意味着一直等待設備可訪問再訪問,非阻塞I/O意味着使用poll()來查詢是否可訪問,而異步通知則意味着設備通知應用程序自身可訪問。(希望用這么一句話能表達我的意思)
一、系統中存在的異步機制
我認為異步機制是一種理念,並不是某一種具體實現,同步/異步的核心理解應該是如何獲取消息的問題,你自身(在計算機中當然是進程本身了)親自去獲取消息,那么就是同步機制,但是如果別人使用某種方式通知你某一個消息,那么你采用的就是異步機制。內核中使用到異步機制的大概有:信號,這是一種進程間通信的異步機制;epoll,這是一種高效處理IO的異步通信機制。也就是從通信和IO兩個方面通過不同的方式使用了異步機制。(可能還有別的,暫時不知道)
下面進入正題:
二、信號的基本概念
1)信號的本質
軟中斷信號(signal,又簡稱為信號)用來通知進程發生了異步事件。在軟件層次上是對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一個中斷請求可以說是一樣的。信號是進程間通信機制中唯一的異步通信機制,一個進程不必通過任何操作來等待信號的到達,事實上,進程也不知道信號到底什么時候到達。進程之間可以互相通過系統調用kill發送軟中斷信號。內核也可以因為內部事件而給進程發送信號,通知進程發生了某個事件。信號機制除了基本通知功能外,還可以傳遞附加信息。
收到信號的進程對各種信號有不同的處理方法。處理方法可以分為三類:
第一種是類似中斷的處理程序,對於需要處理的信號,進程可以指定處理函數,由該函數來處理。
第二種方法是,忽略某個信號,對該信號不做任何處理,就象未發生過一樣。
第三種方法是,對該信號的處理保留系統的默認值,這種缺省操作,對大部分的信號的缺省操作是使得進程終止。進程通過系統調用signal來指定進程對某個信號的處理行為。
在進程表的表項中有一個軟中斷信號域,該域中每一位對應一個信號,當有信號發送給進程時,對應位置位。由此可以看出,進程對不同的信號可以同時保留,但對於同一個信號,進程並不知道在處理之前來過多少個。
2)信號的種類
可以從兩個不同的分類角度對信號進行分類:
可靠性方面:可靠信號與不可靠信號;
與時間的關系上:實時信號與非實時信號。
3)可靠信號與不可靠信號
Linux信號機制基本上是從Unix系統中繼承過來的。早期Unix系統中的信號機制比較簡單和原始,信號值小於SIGRTMIN的信號都是不可靠信號。這就是"不可靠信號"的來源。它的主要問題是信號可能丟失。
隨着時間的發展,實踐證明了有必要對信號的原始機制加以改進和擴充。由於原來定義的信號已有許多應用,不好再做改動,最終只好又新增加了一些信號,並在一開始就把它們定義為可靠信號,這些信號支持排隊,不會丟失。
信號值位於SIGRTMIN和SIGRTMAX之間的信號都是可靠信號,可靠信號克服了信號可能丟失的問題。Linux在支持新版本的信號安裝函數sigation()以及信號發送函數sigqueue()的同時,仍然支持早期的signal()信號安裝函數,支持信號發送函數kill()。
信號的可靠與不可靠只與信號值有關,與信號的發送及安裝函數無關。目前linux中的signal()是通過sigation()函數實現的,因此,即使通過signal()安裝的信號,在信號處理函數的結尾也不必再調用一次信號安裝函數。同時,由signal()安裝的實時信號支持排隊,同樣不會丟失。
對於目前linux的兩個信號安裝函數:signal()及sigaction()來說,它們都不能把SIGRTMIN以前的信號變成可靠信號(都不支持排隊,仍有可能丟失,仍然是不可靠信號),而且對SIGRTMIN以后的信號都支持排隊。這兩個函數的最大區別在於,經過sigaction安裝的信號都能傳遞信息給信號處理函數,而經過signal安裝的信號不能向信號處理函數傳遞信息。對於信號發送函數來說也是一樣的。
4)實時信號與非實時信號
早期Unix系統只定義了32種信號,前32種信號已經有了預定義值,每個信號有了確定的用途及含義,並且每種信號都有各自的缺省動作。如按鍵盤的CTRL ^C時,會產生SIGINT信號,對該信號的默認反應就是進程終止。后32個信號表示實時信號,等同於前面闡述的可靠信號。這保證了發送的多個實時信號都被接收。
非實時信號都不支持排隊,都是不可靠信號;實時信號都支持排隊,都是可靠信號。
5)linux 下信號的生命周期如下:
在目的進程中安裝該信號。即是設置捕獲該信號時進程進程該執行的操作碼。采用signal();sigaction()系統調用來實現。
信號被某個進程產生,同時設置該信號的目的進程(使用pid),之后交給操作系統進行管理。采用kill()、arise()、alarm()等系統調用來實現。
信號在目的進程被注冊。信號被添加進進程的PCB(task_struct)中相關的數據結構里——未決信號的數據成員。信號在進程中注冊就是把信號值加入到進程的未決信號集里。並且,信號攜帶的其他信息被保留到未決信的隊列的某個sigqueue結構中。
信號在進程中注銷。在執行信號處理函數前,要把信號在進程中注銷。對於非實時信號(不可靠信號),其在信號未決信號信息鏈中最多只有一個sigqueue結構,因此該結構被釋放后,相應的信號要在未決信號集刪除。而實時信號(可靠信號),如果有多個sigqueue,則不會把信號從進程的未決信號集中刪除。
信號生命的終結。進程終止當前的工作,保護上下文,執行信號處理函數,之后回復。如果內核是可搶占的,那么還需要調度。
三、信 號 機 制
上 一節中介紹了信號的基本概念,在這一節中,我們將介紹內核如何實現信號機制。即內核如何向一個進程發送信號、進程如何接收一個信號、進程怎樣控制自己對信 號的反應、內核在什么時機處理和怎樣處理進程收到的信號。還要介紹一下setjmp和longjmp在信號中起到的作用。
1、內核對信號的基本處理方法
內核給一個進程發送軟中斷信號的方法,是在進程所在的進程表項的信號域設置對應於該信號的位。這里要補充的是,如果信號發送給一個正在睡眠的進程,那么要看 該進程進入睡眠的優先級,如果進程睡眠在可被中斷的優先級上,則喚醒進程;否則僅設置進程表中信號域相應的位,而不喚醒進程。這一點比較重要,因為進程檢 查是否收到信號的時機是:一個進程在即將從內核態返回到用戶態時;或者,在一個進程要進入或離開一個適當的低調度優先級睡眠狀態時。
進程的task_struct結構中有關於本進程中未決信號的數據成員:struct sigpending pending:
struct sigpending{ struct sigqueue *head, *tail; sigset_t signal; };
第三個成員是進程中所有未決信號集,第一、第二個成員分別指向一個sigqueue類型的結構鏈(稱之為"未決信號信息鏈")的首尾,信息鏈中的每個sigqueue結構刻畫一個特定信號所攜帶的信息,並指向下一個sigqueue結構:
struct sigqueue{ struct sigqueue *next; siginfo_t info; }
信號在進程中注冊指的就是信號值加入到進程的未決信號集sigset_t signal(每個信號占用一位)中,並且信號所攜帶的信息被保留到未決信號信息鏈的某個sigqueue結構中。只要信號在進程的未決信號集中,表明進程已經知道這些信號的存在,但還沒來得及處理,或者該信號被進程阻塞。
當一個實時信號發送給一個進程時,不管該信號是否已經在進程中注冊,都會被再注冊一次,因此,信號不會丟失,因此,實時信號又叫做"可靠信號"。這意味着同一個實時信號可以在同一個進程的未決信號信息鏈中占有多個sigqueue結構(進程每收到一個實時信號,都會為它分配一個結構來登記該信號信息,並把該結構添加在未決信號鏈尾,即所有誕生的實時信號都會在目標進程中注冊)。
當一個非實時信號發送給一個進程時,如果該信號已經在進程中注冊(通過sigset_t signal指示),則該信號將被丟棄,造成信號丟失。因此,非實時信號又叫做"不可靠信號"。這意味着同一個非實時信號在進程的未決信號信息鏈中,至多占有一個sigqueue結構。
總之信號注冊與否,與發送信號的函數(如kill()或sigqueue()等)以及信號安裝函數(signal()及sigaction())無關,只與信號值有關(信號值小於SIGRTMIN的信號最多只注冊一次,信號值在SIGRTMIN及SIGRTMAX之間的信號,只要被進程接收到就被注冊)
內核處理一個進程收到的信號的時機是在一個進程從內核態返回用戶態時。所以,當一個進程在內核態下運行時,軟中斷信號並不立即起作用,要等到將返回用戶態時才處理。進程只有處理完信號才會返回用戶態,進程在用戶態下不會有未處理完的信號。
內核處理一個進程收到的軟中斷信號是在該進程的上下文中,因此,進程必須處於運行狀態。前面介紹概念的時候講過,處理信號有三種類型:進程接收到信號后退 出;進程忽略該信號;進程收到信號后執行用戶設定用系統調用signal的函數。當進程接收到一個它忽略的信號時,進程丟棄該信號,就象沒有收到該信號似 的繼續運行。如果進程收到一個要捕捉的信號,那么進程從內核態返回用戶態時執行用戶定義的函數。而且執行用戶定義的函數的方法很巧妙,內核是在用戶棧上創 建一個新的層,該層中將返回地址的值設置成用戶定義的處理函數的地址,這樣進程從內核返回彈出棧頂時就返回到用戶定義的函數處,從函數返回再彈出棧頂時, 才返回原先進入內核的地方。這樣做的原因是用戶定義的處理函數不能且不允許在內核態下執行(如果用戶定義的函數在內核態下運行的話,用戶就可以獲得任何權 限)。
對於非實時信號來說,由於在未決信號信息鏈中最多只占用一個sigqueue結構,因此該結構被釋放后,應該把信號在進程未決信號集中刪除(信號注銷完畢);而對於實時信號來說,可能在未決信號信息鏈中占用多個sigqueue結構,因此應該針對占用sigqueue結構的數目區別對待:如果只占用一個sigqueue結構(進程只收到該信號一次),則執行完相應的處理函數后應該把信號在進程的未決信號集中刪除(信號注銷完畢)。否則待該信號的所有sigqueue處理完畢后再在進程的未決信號集中刪除該信號。
當所有未被屏蔽的信號都處理完畢后,即可返回用戶空間。對於被屏蔽的信號,當取消屏蔽后,在返回到用戶空間時會再次執行上述檢查處理的一套流程。
在信號的處理方法中有幾點特別要引起注意。
第一,在一些系統中,當一個進程處理完中斷信號返回用戶態之前,內核清除用戶區中設 定的對該信號的處理例程的地址,即下一次進程對該信號的處理方法又改為默認值,除非在下一次信號到來之前再次使用signal系統調用。這可能會使得進程 在調用signal之前又得到該信號而導致退出。在BSD中,內核不再清除該地址。但不清除該地址可能使得進程因為過多過快的得到某個信號而導致堆棧溢 出。為了避免出現上述情況。在BSD系統中,內核模擬了對硬件中斷的處理方法,即在處理某個中斷時,阻止接收新的該類中斷。
第二個要 引起注意的是,如果要捕捉的信號發生於進程正在一個系統調用中時,並且該進程睡眠在可中斷的優先級上,這時該信號引起進程作一次longjmp,跳出睡眠 狀態,返回用戶態並執行信號處理例程。當從信號處理例程返回時,進程就象從系統調用返回一樣,但返回了一個錯誤代碼,指出該次系統調用曾經被中斷。這要注 意的是,BSD系統中內核可以自動地重新開始系統調用。
第三個要注意的地方:若進程睡眠在可中斷的優先級上,則當它收到一個要忽略的信號時,該進程被喚醒,但不做longjmp,一般是繼續睡眠。但用戶感覺不到進程曾經被喚醒,而是象沒有發生過該信號一樣。
第四個要注意的地方:內核對子進程終止(SIGCLD)信號的處理方法與其他信號有所區別。當進程檢查出收到了一個子進程終止的信號時,缺省情況下,該進程 就象沒有收到該信號似的,如果父進程執行了系統調用wait,進程將從系統調用wait中醒來並返回wait調用,執行一系列wait調用的后續操作(找 出僵死的子進程,釋放子進程的進程表項),然后從wait中返回。SIGCLD信號的作用是喚醒一個睡眠在可被中斷優先級上的進程。如果該進程捕捉了這個 信號,就象普通信號處理一樣轉到處理例程。如果進程忽略該信號,那么系統調用wait的動作就有所不同,因為SIGCLD的作用僅僅是喚醒一個睡眠在可被 中斷優先級上的進程,那么執行wait調用的父進程被喚醒繼續執行wait調用的后續操作,然后等待其他的子進程。
如果一個進程調用signal系統調用,並設置了SIGCLD的處理方法,並且該進程有子進程處於僵死狀態,則內核將向該進程發一個SIGCLD信號。
2、setjmp和longjmp的作用
前面在介紹信號處理機制時,多次提到了setjmp和longjmp,但沒有仔細說明它們的作用和實現方法。這里就此作一個簡單的介紹。
在 介紹信號的時候,我們看到多個地方要求進程在檢查收到信號后,從原來的系統調用中直接返回,而不是等到該調用完成。這種進程突然改變其上下文的情況,就是 使用setjmp和longjmp的結果。setjmp將保存的上下文存入用戶區,並繼續在舊的上下文中執行。這就是說,進程執行一個系統調用,當因為資 源或其他原因要去睡眠時,內核為進程作了一次setjmp,如果在睡眠中被信號喚醒,進程不能再進入睡眠時,內核為進程調用longjmp,該操作是內核 為進程將原先setjmp調用保存在進程用戶區的上下文恢復成現在的上下文,這樣就使得進程可以恢復等待資源前的狀態,而且內核為setjmp返回1,使 得進程知道該次系統調用失敗。這就是它們的作用。
