Linux下的訊號詳解及捕捉訊號
訊號的基本概念
每個訊號都有一個編號和一個巨集定義名稱 ,這些巨集定義可以在 signal.h
中找到。
使用kill -l
命令檢視系統中定義的訊號列表: 1-31是普通訊號 regular signal(非可靠訊號); 34-64是實時訊號 real time signal(可靠訊號)
所有的訊號都由作業系統來發!
對訊號的三種處理方式
1、忽略此訊號:大多數訊號都可使用這種方式進行處理,但有兩種訊號卻決不能被忽略。它們是:SIGKILL
和SIGSTOP
。這兩種訊號不能被忽略的,原因是:它們向超級使用者提供一種使程序終止或停止的可靠方法。另外,如果忽略某些由硬體異常產生的訊號(例如非法儲存訪問或除以0),則程序的行為是示定義的。
2、直接執行程序對於該訊號的預設動作 :對大多數訊號的系統預設動作是終止該程序。
3、捕捉訊號:執行自定義動作(使用signal
函式),為了做到這一點要通知核心在某種訊號發生時,呼叫一個使用者函式handler
。在使用者函式中,可執行使用者希望對這種事件進行的處理。注意,不能捕捉SIGKILL
和SIGSTOP
訊號。
1 2 3 |
|
signal函式的作用:給某一個程序的某一個特定訊號(標號為signum)註冊一個相應的處理函式,即對該訊號的預設處理動作進行修改,修改為handler
函式所指向的方式。
1、第一個引數是訊號的標號
2、第二個引數,sighandler_t
是一個typedef
來的,原型是void (*)(int)
函式指標,int
的引數會被設定成signum
舉個程式碼例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
修改了2號訊號(Ctrl-c)的預設處理動作為handler
函式的內容,則當該程式在前臺執行時,鍵入Ctrl-c後不會執行它的預設處理動作(終止該程序)
訊號的處理過程:
程序收到一個訊號後不會被立即處理,而是在恰當 時機進行處理!什麼是適當的時候呢?比如說中斷返回的時候,或者核心態返回使用者態的時候(這個情況出現的比較多)。
訊號不一定會被立即處理,作業系統不會為了處理一個訊號而把當前正在執行的程序掛起(切換程序),掛起(程序切換)的話消耗太大了,如果不是緊急訊號,是不會立即處理的。作業系統多選擇在核心態切換回使用者態的時候處理訊號,這樣就利用兩者的切換來處理了(不用單獨進行程序切換以免浪費時間)。
總歸是不能避免的,因為很有可能在睡眠的程序就接收到訊號,作業系統肯定不願意切換當前正在執行的程序,於是就得把訊號儲存在程序唯一的PCB(task_struct)當中。
產生訊號的條件
1.使用者在終端按下某些鍵時,終端驅動程式會發送訊號給前臺程式。
例如:Ctrl-c產生SIGINT訊號,Ctrl-\產生SIGQUIT訊號,Ctrl-z產生SIGTSTP訊號
2.硬體異常產生訊號。
這類訊號由硬體檢測到並通知核心,然後核心向當前程序傳送適當的訊號。
例如:當前程序執行除以0的指令,CPU的運算單元會產生異常,核心將這個程序解釋為SIGFPE訊號傳送給當前程序。
當前程序訪問了非法記憶體地址,MMU會產生異常,核心將這個異常解釋為SIGSEGV訊號傳送給程序。
3.一個程序呼叫kill(2)
函式可以傳送訊號給另一個程序。
可以用kill(1)
命令傳送訊號給某個程序,kill(1)
命令也是呼叫kill(2)
函式實現的,如果不明確指定訊號則傳送SIGTERM訊號,該訊號的預設處理動作是終止程序。
訊號的產生
1.通過終端按鍵產生訊號
舉個栗子:寫一個死迴圈,前臺執行這個程式,然後在終端鍵入Ctrl-c
當CPU正在執行這個程序的程式碼 , 終端驅動程式傳送了一 個 SIGINT
訊號給該程序,記錄在該程序的 PCB中,則該程序的使用者空間程式碼暫停執行 ,CPU從使用者態 切換到核心態處理硬體中斷。
從核心態回到使用者態之前, 會先處理 PCB中記錄的訊號 ,發現有一個 SIGINT
訊號待處理, 而這個訊號的預設處理動作是終止程序,所以直接終止程序而不再返回它的使用者空間程式碼執行。
2.呼叫系統函式向程序發訊號
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
寫一個上面的程式在後臺執行死迴圈,並獲取該程序的id,然後用kill命令給它傳送SIGSEGV
訊號,可以使程序終止。也可以使用kill -11 5796,11是訊號SIGSEGV
的編號。
開啟終端1,執行程式:
利用終端2,給程序傳送訊號
終端1 顯示程序被core了:
kill命令是呼叫kill函式實現的。kill函式可以給一個指定的程序傳送指定訊號
。
raise函式可 以給當前程序傳送指定的訊號 (自己給自己發訊號 )
1 2 3 |
|
這兩個函式都是成功返回0,錯誤返回-1.
除此之外,abort
函式使當前程序接收到SIGABRT
訊號而異常終止。
1 2 |
|
就像 exit
函式一樣 ,abort
函式總是會成功的 ,所以沒有返回值。
3.由軟體條件產生訊號
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
通過實現以上程式碼,呼叫alarm
函式可以設定一個鬧鐘,告訴核心在seconds
秒之後給當前程序發SIGALRM
訊號, 該訊號的預設處理動作是終止當前程序。
該程式會在1秒鐘之內不停地數數,並列印計數器,1秒鐘到了就被SIGALRM
訊號終止。由於電腦配置等的不同,每臺電腦一秒鐘之內計數值是不同的一般是不同的。
1 2 |
|
alarm
函式的返回值是0或上次設定鬧鐘剩餘的時間。
阻塞訊號
1.訊號在核心中的表示:
訊號遞達delivery:實際執行訊號處理訊號的動作
訊號未決pending:訊號從產生到抵達之間的狀態,訊號產生了但是未處理
忽略:抵達之後的一種 動作
阻塞block:收到訊號不立即處理 被阻塞的訊號將保持未決狀態,直到程序解除對此訊號的阻塞,才執行抵達動作
訊號產生和阻塞沒有直接關係 抵達和解除阻塞沒有直接關係!
程序收到一個訊號後,不會立即處理,它會在恰當的時機被處理。
每個訊號都由兩個標誌位分別表示阻塞和未決,以及一個函式指標表示訊號的處理動作。
在上圖的例子中,
1. SIGHUP
訊號未阻塞也未產生過,當它遞達時執行預設處理動作。
2. SIGINT
訊號產生過,但正在被阻塞,所以暫時不能遞達。雖然它的處理動作是忽略,但在沒 有解除阻塞之前不能忽略這個訊號,因為程序仍有機會改變處理動作之後再解除阻塞。
3. SIGQUIT
訊號未產生過,一旦產生SIGQUIT
訊號將被阻塞,它的處理動作是使用者自定義函式sighandler
。阻塞訊號集也叫作訊號遮蔽字。
訊號產生但是不立即處理,前提條件是要把它儲存在pending
表中,表明訊號已經產生。
2.訊號集操作函式
1 2 3 4 5 6 |
|
引數解析:
sigset_t
結構體的引數表示訊號集,訊號操作的時候都是以訊號集合的方式進行操作,需要事先建立一個該結構體的物件,然後把想要操作的訊號新增到訊號集合物件當中去
signo就是訊號的標號了
3.呼叫函式sigprocmask可以讀取或更改程序的訊號遮蔽字(阻塞訊號集)。
1 2 |
|
一個程序的訊號遮蔽字規定了當前阻塞而不能遞送給該程序的訊號集。呼叫函式sigprocmask
可以檢測或更改(或兩者)程序的訊號遮蔽字。如果呼叫sigprocmask
解除了對當前若干個未決訊號的阻塞,則在sigprocmask
返回前,至少將其中 一個訊號遞達。
引數解析:
how,有三個巨集
SIG_BLOCK 新增到block表當中去
SIG_UNBLOCK 從block表中刪除
SIG_SETMASK 設定block表 設定當前訊號遮蔽字為set所指向的值
set表示新設定的訊號遮蔽字,oset表示當前訊號遮蔽字
處理方式:
set 非空, oset 為NULL :按照how指示的方法更改set指向訊號集的訊號遮蔽字。
set 為NULL,oset 非空:讀取oset指向訊號集的訊號遮蔽字,通過oset引數傳出。
set 和 oset 都非空 :現將原來的訊號遮蔽字備份到oset裡,然後根據set和how引數更改訊號遮蔽字。
4. sigpending讀取當前程序的未決訊號集,通過set引數傳出
1 2 |
|
這是一個輸出型引數,會把當前程序的pending
表列印到傳入的set集中。
例項驗證上面幾個函式:
一開始沒有任何訊號,所以pending
表中全是0,我通過Ctrl+C傳入2號訊號,看到pending表中有2號被置位了,經過10秒取消阻塞,2號訊號被處理(經過我自定義的函式)
Linux下捕捉訊號
訊號由三種處理方式:
忽略
執行該訊號的預設處理動作
捕捉訊號
如果訊號的處理動作是使用者自定義函式,在訊號遞達時就呼叫這個自定義函式,這稱為捕捉訊號。
程序收到一個訊號後不會被立即處理,而是在恰當時機進行處理!即核心態返回使用者態之前 !
但是由於訊號處理函式的程式碼在使用者空間,所以這增加了核心處理訊號捕捉的複雜度。
核心實現訊號捕捉的步驟:
1、使用者為某訊號註冊一個訊號處理函式sighandler
。
2、當前正在執行主程式,這時候因為中斷、異常或系統呼叫進入核心態。
3、在處理完異常要返回使用者態的主程式之前,檢查到有訊號未處理,並發現該訊號需要按照使用者自定義的函式來處理。
4、核心決定返回使用者態執行sighandler
函式,而不是恢復main
函式的上下文繼續執行!(sighandler
和main
函式使用的是不同的堆疊空間,它們之間不存在呼叫和被呼叫的關係,是兩個獨立的控制流程)
5、sighandler
函式返回後,執行特殊的系統呼叫sigreturn
從使用者態回到核心態
6、檢查是否還有其它訊號需要遞達,如果沒有 則返回使用者態並恢復主程式的上下文資訊繼續執行。
signal
給某一個程序的某一個訊號(標號為signum)註冊一個相應的處理函式,即對該訊號的預設處理動作進行修改,修改為handler
函式指向的方式;
1 2 3 |
|
signal函式接受兩個引數:一個整型的訊號編號,以及一個指向使用者定義的訊號處理函式的指標。
此外,signal函式的返回值是一個指向呼叫使用者定義訊號處理函式的指標。
sigaction
sigaction函式可以讀取和修改與指定訊號相關聯的處理動作。
1 2 3 4 5 6 7 8 9 10 |
|
signum
是指定訊號的編號。
處理方式:
1、若act指標非空,則根據act結構體中的訊號處理函式來修改該訊號的處理動作。
2、若oact指標非 空,則通過oact傳出該訊號原來的處理動作。
3、現將原來的處理動作備份到oact裡,然後根據act修改該訊號的處理動作。
(注:後兩個引數都是輸入輸出型引數!)
將sa_handler三種可選方式:
1、賦值為常數SIG_IGN
傳給sigaction
表示忽略訊號;
2、賦值為常數SIG_DFL
表示執行系統預設動作;
3、賦值為一個函式指標表示用自定義函式捕捉訊號,或者說向核心註冊一個訊號處理函 數,該函式返回值為void,可以帶一個int引數,通過引數可以得知當前訊號的編號,這樣就可以用同一個函式處理多種訊號。
(注:這是一個回撥函式,不是被main函式呼叫,而是被系統所呼叫)
當某個訊號的處理函式被呼叫時,核心自動將當前訊號加入程序的訊號遮蔽字,當訊號處理函式返回時自動恢復原來的訊號遮蔽字,這樣就保證了在處理某個訊號時,如果這種訊號再次產生,那麼 它會被阻塞到當前處理結束為止。
pause
pause函式使呼叫程序掛起直到有訊號遞達!
1 2 |
|
處理方式:
如果訊號的處理動作是終止程序,則程序終止,pause
函式沒有機會返回;
如果訊號的處理動作是忽略,則程序繼續處於掛起狀態,pause
不返回;
如果訊號的處理動作是捕捉,則呼叫了訊號處理函式之後pause
返回-1,errno設定為EINTR。
所以pause
只有出錯的返回值(類似exec函式家族)。錯誤碼EINTR表示“被訊號中斷”。
舉個栗子
1、定義一個鬧鐘,約定times秒後,核心向該程序傳送一個SIGALRM
訊號;
2、呼叫pause
函式將程序掛起,核心切換到別的程序執行;
3、times秒後,核心向該程序傳送SIGALRM訊號,發現其處理動作是一個自定義函式,於是切回用戶態執行該自定義處理函式;
4、進入sig_alrm
函式時SIGALRM
訊號被自動遮蔽,從sig_alrm
函式返回時SIGALRM
訊號自動解除遮蔽。然後自動執行特殊的系統呼叫sigreturn
再次進入核心,之後再返回使用者態繼續執行程序的主控制流程(main
函式呼叫的mytest
函式)。
5、pause
函式返回-1,然後呼叫alarm(0)
取消鬧鐘,呼叫sigaction
恢復SIGALRM
訊號以前的處理 動作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
定義一個鬧鐘並掛起等待,收到訊號後執行自定義處理動作,在沒有恢復預設處理動作前,收到SIGALRM
訊號都會按照其自定義處理函式來處理。恢復自定義處理動作之後收到SIGALRM
訊號則執行其預設處理動作即終止程序!
總結