一、信號的基本概念
1.引入
計算機中常見的信號:(1) ⽤戶輸⼊命令,在Shell下啟動⼀個前台進程;
(2)⽤戶按下Ctrl-C/Ctrl-Z等,這個鍵盤輸⼊產⽣⼀個硬件中斷。如此類的組合鍵等被操作系統解釋為信號(注意,Ctrl-C產⽣的信號只能發給前台進程。 );
(3)如果CPU當前正在執⾏這個進程的代碼,則該進程的⽤戶空間代碼暫停執⾏,CPU從⽤ 戶態 切換到內核態處理硬件中斷。(硬件異常產生的信號)
(4)終端驅動程序將Ctrl-C解釋成⼀個SIGINT信號,記在該進程的PCB中。(也可以說發送了 ⼀ 個SIGINT信號給該進程);
(5)當某個時刻要從內核返回到該進程的⽤戶空間代碼繼續執⾏之前,⾸先處理PCB中記錄的信號,發現有⼀個SIGINT信號待處理,⽽這個信號的默認處理動作是終⽌進程,所以直接終⽌進程⽽不再返回它的⽤戶空間代碼執⾏。
./a.out & 像這樣的⼀個命令 后⾯加個&可以放到后台運⾏,這樣Shell不必等待進程結束就可以接受新的命令,啟動新的進程。
2.基本理解
信號(signal)是Linux進程間通信的一種機制,全稱為軟中斷信號,也被稱為軟中斷。信號本質上是在軟件層次上對硬件中斷機制的一種模擬。它提供了一種處理異步事件的方法,也是進程間惟一的異步通信方式。體現為操作系統修改了目標進程的PCB內容,即為對其發送了信號。信號的來源有多種方式,前面的引入部分已經其實就已經提出了,下面做以總結:(1)硬件方式
a.當用戶在終端上按下某鍵時,將產生信號。如按下<Ctral + C>組合鍵后將產生一個SIGINT信號。
b.硬件異常產生信號:除數據、無效的存儲訪問等。這些事件通常由硬件(如:CPU)檢測到,並將其通知給Linux操作系統內核,然后內核生成相應的信號,並把信號發送給該事件發生時正在進行的程序。
程序示例:
(2) 軟件方式
c.用戶在終端下調用kill命令向進程發送任務信號。
d.進程調用kill或sigqueue函數發送信號。
e.當檢測到某種軟件條件已經具備時發出信號,如由alarm或settimer設置的定時器超時時將生成SIGALRM信號。
3.信號的種類
⽤kill -l命令可以察看系統定義的信號列表:
每個信號都有⼀個編號和⼀個宏定義名稱,定義在signal.h中,例如其中有定義#define SIGINT 2。其中1~31號信號為普通信號,34~64為實時信號,在Linux中沒有33和32這兩個信號。這里只研究普通信號,上面的普通信號的含義如下:
(1) SIGHUP:當用戶退出Shell時,由該Shell啟的發所有進程都退接收到這個信號,默認動作為終止進程。
(2) SIGINT:用戶按下<Ctrl + C>組合鍵時,用戶端時向正在運行中的由該終端啟動的程序發出此信號。默認動作為終止進程。
(3) SIGQUIT:當用戶按下<Ctrl + />組合鍵時產生該信號,用戶終端向正在運行中的由該終端啟動的程序發出此信號。默認動作為終止進程並產生core文件。
(4) SIGILL :CPU檢測到某進程執行了非法指令。默認動作為終止進程並產生core文件。
(5) SIGTRAP:該信號由斷點指令或其他trap指令產生。默認動作為終止進程並產生core文件。
(6) SIGABRT:調用abort函數時產生該信號。默認動作為終止進程並產生core文件。
(7) SIGBUS:非法訪問內存地址,包括內存地址對齊(alignment)出錯,默認動作為終止進程並產生core文件。
(8) SIGFPE:在發生致命的算術錯誤時產生。不僅包括浮點運行錯誤,還包括溢出及除數為0等所有的算術錯誤。默認動作為終止進程並產生core文件。
(9) SIGKILL:無條件終止進程。本信號不能被忽略、處理和阻塞。默認動作為終止進程。它向系統管理員提供了一種可以殺死任何進程的方法。
(10) SIGUSR1:用戶定義的信號,即程序可以在程序中定義並使用該信號。默認動作為終止進程。
(11) SIGSEGV:指示進程進行了無效的內存訪問。默認動作為終止進程並使用該信號。默認動作為終止進程。
(12) SIGUSR2:這是另外一個用戶定義信號,程序員可以在程序中定義並使用該信號。默認動作為終止進程。
(13) SIGPIPE:Broken pipe:向一個沒有讀端的管道寫數據。默認動作為終止進程。
(14) SIGALRM:定時器超時,超時的時間由系統調用alarm設置。默認動作為終止進程。
(15) SIGTERM:程序結束(terminate)信號,與SIGKILL不同的是,該信號可以被阻塞和處理。通常用來要求程序正常退出。執行Shell命令kill時,缺少產生這個信號。默認動作為終止進程。
(17) SIGCHLD:子程序結束時,父進程會收到這個信號。默認動作為忽略該信號。
(18) SIGCONT:讓一個暫停的進程繼續執行。
(19) SIGSTOP:停止(stopped)進程的執行。注意它和SIGTERM以及SIGINT的區別:該進程還未結束,只是暫停執行。本信號不能被忽略、處理和阻塞。默認作為暫停進程。
(20) SIGTSTP:停止進程的動作,但該信號可以被處理和忽略。按下<Ctrl + Z>組合鍵時發出該信號。默認動作為暫停進程。
(21) SIGTTIN:當后台進程要從用戶終端讀數據時,該終端中的所有進程會收到SIGTTIN信號。默認動作為暫停進程。
(22) SIGTTOU:該信號類似於SIGTIN,在后台進程要向終端輸出數據時產生。默認動作為暫停進程。
(23) SIGURG:套接字(socket)上有緊急數據時,向當前正在運行的進程發出此信號,報告有緊急數據到達。默認動作為忽略該信號。
(24) SIGXCPU:進程執行時間超過了分配給該進程的CPU時間,系統產生該信號並發送給該進程。默認動作為終止進程。
(25) SIGXFSZ:超過文件最大長度的限制。默認動作為yl終止進程並產生core文件。
(26) SIGVTALRM:虛擬時鍾超時時產生該信號。類似於SIGALRM,但是它只計算該進程占有用的CPU時間。默認動作為終止進程。
(27) SIGPROF:類似於SIGVTALRM,它不僅包括該進程占用的CPU時間還抱括執行系統調用的時間。默認動作為終止進程。
(28) SIGWINCH:窗口大小改變時發出。默認動作為忽略該信號。
(29) SIGIO:此信號向進程指示發出一個異步IO事件。默認動作為忽略。
(30) SIGPWR:關機。默認動作為終止進程。
(31) SIGRTMIN~SIGRTMAX:Linux的實時信號,它沒有固定的含義(或者說可以由用戶自由使用)。注意,Linux線程機制使用了前3個實時信號。所有的實時信號的默認動作都是終止進程。
這些信號各⾃在什么條件下產⽣,默認的處理動作是什么,在signal(7) 中都有 詳細說明:
man 7 signal
在Linux系統中,信號的可靠性是指信號是否會丟失,或者說該信號是否支持排除。SIGHUP( 1 ) ~ SIGSYS( 31 )之間的信號都是繼承自UNIX系統是不可靠信號。Linux系統根據POSIX標准定義了SIGRTMIN(34) ~ SIGRTMAX(64)之間的信號,它們都是可靠信號,也稱為實時信號。9號信號不可被捕捉/修改。位圖中比特位32位與信號編號對應,其內容表示是否收到信號,1為收到信號,0為沒有收到信號。
4.信號處理
對於信號,可選的處理動作有以下三種:
(1) 忽略此信號。
(2)執⾏該信號的默認處理動作。
(3)提供⼀個信號處理函數,要求內核在處理該信號時切換到⽤戶態執⾏這個處理函數,這種⽅式稱為捕捉(Catch)⼀個信號。
二、信號產生
1.通過終端按鍵產生信號
SIGINT的默認處理動作是終⽌進程,SIGQUIT的默認處理動作是終⽌進程並且Core Dump,⾸先解釋什么是Core Dump。當⼀個進程要異常終⽌時,可以選擇把進程的⽤戶空間內存數據全部 保存到磁盤上,⽂件名通常是core,這叫做Core Dump。進程異常終⽌通常是因為有Bug,⽐如⾮法內存訪問導致段錯誤,事后可以⽤調試器檢查core⽂件以查清錯誤原因,這叫做Post-mortem Debug(事后調試) 。⼀個進程允許產⽣多⼤的core⽂件取決於進程的Resource Limit(這個信息保存 在PCB中)。默認是不允許產⽣core⽂件的,因為core⽂件中可能包含⽤戶密碼等敏感信息,不安全。在開發調試階段可以⽤ulimit命令改變這個限制,允許產⽣core⽂件。
Core Dump,即核心轉儲,進程異常退出之前,將其內存中的有效數據以文件形式存貯到內存中,方便事后進行GDB調試和定位。
⾸先⽤ulimit命令改變Shell進程的Resource Limit,允許core⽂件最⼤為1024K:
1 #include<stdio.h> 2 int main() //除0異常 3 { 4 a = 5; 5 a /= 0; 6 return 0; 7 }
2. 調⽤系統函數向進程發信號
⾸先在后台執⾏死循環程序,然后⽤kill命令給它發SIGSEGV信號。
13224是signal進程的id。之所以要再次回車才顯⽰Segmentation fault,是因為在13224進程終⽌掉 之前已經回到了Shell提⽰符等待⽤戶輸⼊下⼀條命令,Shell不希望 Segmentation fault信息和⽤ 戶的輸⼊交錯在⼀起,所以等⽤戶輸⼊命令之后才顯⽰。指定某種信號的kill命令可以有多種寫 法,上⾯的命令還可以寫成kill -SIGSEGV 13224或kill -11 13224, 11 是信號SIGSEGV的編號。以往遇 到的段錯誤都是由⾮法內存訪問產⽣的,⽽這個程序本⾝沒錯,給它發SIGSEGV也能產⽣段錯誤。 kill命令是調⽤kill函數實現的。 kill函數可以給⼀個指定的進程發送指定的信號。r a i s e函數可 以給當前進程發送指定的信號(⾃⼰給⾃⼰發信號)
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
這兩個函數都是成功返回0,錯誤返回-1 。
abort函數使當前進程接收到信號⽽異常終⽌。
#include <stdlib.h>
void abort(void);
就像exit函數⼀樣,abort函數總是會成功的,所以沒有返回值。
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<signal.h> 4 void myhandler(int sig) 5 { 6 printf("sig is : %d",sig); 7 } 8 int main() 9 { 10 int i = 0; 11 for(;i<32;i++) 12 { 13 signal(i, myhandler); 14 } 15 abort(); 16 return 0; 17 }
3.由軟件條件產⽣信號
SIGPIPE是⼀種由軟件條件產⽣的信號,在“管道”中已經介紹過了。主要介紹 alarm函數 和SIGALRM信號。
#include <unistd.h> unsigned int alarm(unsigned int seconds);
調⽤alarm函數可以設定⼀個鬧鍾,也就是告訴內核在seconds秒之后給當前進程發 SIGALRM信號, 該信號的默認處理動作是終⽌當前進程。 這個函數的返回值是0或者是以前設定的鬧鍾時間還余下 的秒數。打個⽐⽅,某⼈要⼩睡⼀覺,設定鬧鍾為30分鍾之后響,20分鍾后被⼈吵醒了,還想多睡 ⼀會⼉,於是重新設定鬧鍾為15分鍾之后響,“以前設定的鬧鍾時間還余下的時間”就是10分鍾。如果 s e c o n d s值為0,表⽰取消以前設定的鬧鍾,函數的返回值仍然是以前設定的鬧鍾時間還余下的秒數。然是以前設定的鬧鍾時間還余下的秒數然是以前設定的鬧鍾時間還余下的秒數然是以前設定的鬧鍾時間還余下的秒數
例 alarm
三、阻塞信號
1.信號在內核中的表⽰
以上我們討論了信號產⽣(Generation )的各種原因,⽽實際執⾏信號的處理動作稱為信號遞達(Delivery),信號從產⽣到遞達之間的狀態,稱為信號未決(Pending)。進程可以選擇阻塞(Block )某個信號。被阻塞的信號產⽣時將保持在未決狀態,直到進程解除對此信號的阻塞,才 執⾏遞達的動作。 注意,阻塞和忽略是不同的,只要信號被阻塞就不會遞達,⽽忽略是在遞達之后 可選的⼀種處理動作。信號在內核中的表⽰可以看作是這樣的:
達之后 可選的⼀種處理動作。信號在內核中的表⽰可以看作是這樣的:
每個信號都有兩個標志位分別表⽰阻塞(block)和未決(pending),還有⼀個函數指針表⽰處理動作。信號產⽣時,內核在進程控制塊中設置該信號的未決標志,直到信號遞達才清除該標志。 在上圖的例⼦中:
1. SIGHUP信號未阻塞也未產⽣過,當它遞達時執⾏默認處理動作。
2. SIGINT信號產⽣過,但正在被阻塞,所以暫時不能遞達。雖然它的處理動作是忽略,但在沒有解除阻塞之前不能忽略這個信號,因為進程仍有機會改變處理動作之后再解除阻塞。
3. SIGQUIT信號未產⽣過,⼀旦產⽣SIGQUIT信號將被阻塞,它的處理動作是⽤戶⾃定義函數sighandler。
如果在進程解除對某信號的阻塞之前這種信號產⽣過多次,將如何處理?POSIX.1 允許系統遞送該信號⼀次或多次。 Linux是這樣實現的:常規信號在遞達之前產⽣多次只計⼀次,⽽實時信號在遞達之前產⽣多次可以依次放在⼀個隊列⾥。 本章不討論實時信號。從上圖來看,每個信號只有⼀ 個bit的未決標志,⾮0即1 ,不記錄該信號產⽣了多少次,阻塞標志也是這樣表⽰的。因此,未決和阻塞標志可以⽤相同的數據類型sigset_t來存儲, s i g s e t _ t稱為信號集,這個類型可以表⽰每個信號的“有效”或“⽆效”狀態,在阻塞信號集“有效”和“⽆效”的含義是該信號是否被阻塞,⽽在未決信號集中“有效”和“⽆效”的含義是該信號是否處於未決狀態。⼀節將詳細介紹信號集的各種操作。 阻塞信號集也叫做當前進程的信號屏蔽字(Signal Mask),這⾥的“屏蔽”應該理解為阻塞⽽不是忽略。
2.信號集操作函數
sigset_t類型對於每種信號⽤⼀個bit表⽰“有效”或“⽆效”狀態,⾄於這個類型內部如何存儲這 些bit則依賴於系統實現,從使⽤者的⾓度是不必關⼼的,使⽤者只能調⽤以下函數來操 作sigset_t變量,⽽不應該對它的內部數據做任何解釋,⽐如⽤printf直接打印sigset_t變量是沒 有意義的。
1 #include <signal.h> 2 int sigemptyset(sigset_t *set); //初始化set所指向的信號集,使其中所有信號的對應bit清零,表⽰該信號集不包含 任何有效信號。 3 int sigfillset(sigset_t *set); //初始化set所指向的信號集,使其中所有信號的對應bit置位,表⽰ 該信號集的有效信號包括系統⽀持的所有信號。 4 // 注意,在使⽤sigset_t類型的變量之前,⼀定要調 ⽤sigemptyset或sigfillset做初始化,使信號集處於確定的狀態。 5 int sigaddset(sigset_t *set, int signo); //初始化sigset_t變量之后就可以 在調⽤sigaddset和sigdelset在該信號集中添加或刪除某種有效信號。 6 int sigdelset(sigset_t *set, int signo); 7 int sigismember(const sigset_t *set, int signo); //sigismember是⼀個布爾函數,⽤於判斷⼀個信號集的有效信號中是否包含某種 信號,若包含則返回1,
8 不包含則返回0,出錯返回-1 。
前面四個函數都是成功返回0,出錯返回-1 。
3.sigprocmask
調⽤函數sigprocmask可以讀取或更改進程的信號屏蔽字(阻塞信號集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功則為0,若出錯則為-1。如果oset是⾮空指針,則讀取進程的當前信號屏蔽字通過oset參數傳出。如果set是⾮空指針,則 更改進程的信號屏蔽字,參數how指⽰如何更改。如果oset和set都是⾮空指針,則先將原來的信號 屏蔽字備份到oset⾥,然后根據set和how參數更改號屏蔽字。假設當前的信號屏蔽字為mask,下表說明了how參數的可選值。
如果調⽤ s i g p r o c m a s k解除了對當前若⼲個未決信號的阻塞,則在 s i g p r o c m a s k返回前,⾄少將其中 ⼀個信號遞達。
4.sigpending
#include <signal.h>
int sigpending(sigset_t *set);
sigpending讀取當前進程的未決信號集,通過set參數傳出。調⽤成功則返回0,出錯則返回-1 。
下面的程序測試信號屏蔽與解除並遞達:
輸出結果:
程序運⾏時,每秒鍾把各信號的未決狀態打印⼀遍,由於我們阻塞了SIGINT信號,按Ctrl-C將會使SIGINT信號處於未決狀態,按Ctrl-\仍然可以終⽌程序,因為SIGQUIT信號沒有阻塞。