一、信號的基本概念
信號在我們生活中隨處可見,上課鈴聲、喇叭、紅綠燈、警報、鬧鐘、電話鈴聲.....等等。我們知道即使信號沒有產生,我們也知道該如何處理它,比如,紅燈我們就該停,電話響就該接....。那是因爲在第一次遇到他時,我們就記住了它的特徵及其處理方法,所以我麼就知道,如果產生這種信號該怎麼辦。那麼總結如下:
生活中:① 信號的產生的隨機的;② 信號沒有產生時,我們也知道它的作用;③ 我們曾經把信號的特徵及處理方法記錄下來了,已變成了條件反射。
計算機中也是信號的。信號是進程之間事件異步通知的一種方式,屬於軟中斷。
在計算機中:①相對於進程而言,信號的產生是異步的;② 即使還未產生信號,也知道如何處理;③ 進程知道如何處理一個信號是因爲提前記錄了什麼信號、如何處理。
那麼我們需要知道進程是怎麼記錄信號、在哪裏記錄信號呢?
首先先來看下信號列表:在Linux中,kill -l可以查看信號列表,在信號列表中,1-34號爲普通信號(其中,不存在0、32、33號信號),34以上爲實時信號。這裏我們只討論1-34號普通信號。
每個信號都有一個編號和一個宏定義名稱,這些宏定義可以在signal.h中找到,例如其中有定義 #define SIGINT 2
二、信號的產生
信號的產生必須直接或者間接通過操作系統
其實準確的說不是發信號,而是操作系統向進程的PCB寫信號
信號的產生一共有四種途徑:
1.通過終端按鍵產生信號
如果我們不下心寫了一個死循環,當我們要終止這個死循環就會按Ctrl+c,那麼Ctrl+c其實就是向目標進程或是前臺進程發送一個2號SIGINT信號,鍵盤產生了信號,但是其實是操作系統把這個信號寫到進程的PCB的,操作系統獲取到鍵盤上的信號,把它解析成2號信號,然後發給進程。進程收到2號信號之後默認就是當前進程退出。所以你就可以結束掉你寫的這個死循環。
SIGINT的默認處理動作是終止進程,SIGQUIT的默認處理動作是終止進程並且Core Dump,Core Dump 是什麼以及它的驗證可以看這篇文章--->【Linux】什麼是Core Dump值
2.系統調用(命令或者函數)
①命令kill
我們在後臺跑起來一個死循環進程 a.out ,再打開一個終端,使用命令查看該進程,我們發現該進程的狀態爲R狀態,此時我們使用kill+進程id命令,發現該進程立馬退出。這樣就結束掉了一個死循環。
其實我們可以看看,kill實際上也是發了一個SIGSEGV信號才結束掉了這個死循環。
17261是test進程的id。之所以要再次回車才顯示 Segmentation fault ,是因爲在4568進程終止掉之前已經回到了Shell提示符等待用戶輸入下一條命令,Shell不希望Segmentation fault信息和用戶的輸入交錯在一起,所以等用戶輸入命令之後才顯示。
指定發送某種信號的kill命令可以有多種寫法,上面的命令還可以寫成 kill -SIGSEGV 17261 或 kill -11 17261,11是信號SIGSEGV的編號。以往遇 到的段錯誤都是由非法內存訪問產生的,而這個程序本身沒錯,給它發SIGSEGV也能產生段錯誤。
②系統調用函數
如果不想用命令結束掉死循環,還可以用系統調用函數來完成。kill命令就是調用kill函數實現的。kill函數可以給一個指定的進程發送指定的信號。還是剛剛的例子:
#include <signal.h> #include <sys/types.h> int kill(pid_t pid,int sig);
函數寫好後,我們來看結果:
再打開一個終端,查看死循環進程的進程ID,運行kill程序,將該進程的進程ID以及要發送的信號以命令行參數進行傳入kill系統調用函數,最後使用kill系統調用函數將該死循環進程終止。
3.由軟件條件產生信號
(1)比如在某個進程裏創建了一個管道,操作系統識別到這個事件,然後讀端被關閉了,這叫做軟件條件,操作系統檢測到之後認爲沒有必要在進行寫了,所以操作系統就會自發的向該進程發送13號信號:SIGPIPE 來關閉寫端,從而終止該進程。
也就是說:操作系統識別了一個事件,雖然沒有發生錯誤,但是發現這個事件不具備某些條件了,所以操作系統就會自發的向進程發送信號。大部分信號都會導致進程退出。
(2)來看一下14號信號:SIGALRM,這個信號叫做超時信號或者鬧鐘信號。如果我們在某個進程中用alarm函數設定了一個鬧鐘,那麼鬧鐘一旦超時之後,操作系統就會給該進程發送SIGALRM。
#include <unistd.h> unsigned int alarm(unsigned int seconds);
調用alarm函數可以設定一個鬧鐘,也就是告訴內核在seconds秒之後給當前進程發SIGALRM信號, 該信號的默認處理動作是終止當前進程。
來驗證一下:一秒之後鬧鐘響,操作系統給進程發送了一個SIGALARM信號,進程終止。
4.異常(硬件、軟件引發的)
進程出現異常,觸發軟硬件的異常機制,操作系統主動發送異常信號給進程。
例如:我們此時在代碼裏寫了一個野指針,那麼這個指針會指向任意位置,這是不被允許的,那麼在進行虛擬內存地址空間映射到物理內存的這個過程中,MMU就會對頁表進行檢查,此時MMU就發現了這是不對的,所以MMU就會告訴操作系統,然後操作系統就會給該進程發送11號SIGSEGV信號(segmentation fault:段錯誤),然後這個進程就會在合適的時間對信號進行處理。
再舉個栗子:我們此時寫了一個除法運算的程序,但是在該程序的代碼中,我們出現了除0操作。那麼在編譯運算期間,CPU就會檢測到你的除0非法操作,所以此時CPU就會告訴操作系統,然後操作系統就會給該進程發送8號SIGFPE信號(floating point exception:浮點數異常),然後這個進程就會在合適的時間對信號進行處理。
注意:
- Ctrl-C 產生的信號只能發給前臺進程。一個命令後面加個&可以放到後臺運行,這樣Shell不必等待進程結束就可以接受新的命令,啓動新的進程。
- Shell可以同時運行一個前臺進程和任意多個後臺進程,只有前臺進程才能接到像 Ctrl-C 這種控制鍵產生的信號。
- 前臺進程在運行過程中用戶隨時可能按下 Ctrl-C 而產生一個信號,也就是說該進程的用戶空間代碼執行到任何地方都有可能收到 SIGINT 信號而終止,所以信號相對於進程的控制流程來說是異步(Asynchronous)的
三、信號的記錄
進程要對操作系統發出的信號進行處理,從而做出反應,肯定要先記錄下來,那麼要記錄,就要看看進程是怎麼知道一個信號產生了呢?
- 實際執行信號的處理動作稱爲信號遞達(Delivery)
- 信號從產生到遞達之間的狀態,稱爲信號未決(Pending)。
- 進程可以選擇阻塞 (Block )某個信號。
- 被阻塞的信號產生時將保持在未決狀態,直到進程解除對此信號的阻塞,才執行遞達的動作。
- 注意:阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是在遞達之後可選的一種處理動作
描述進程用的是PCB,信號要麼產生了要麼沒產生,不會有第三態,所以最好的記錄信號的方法就是位圖(0-31對應的比特位代表信號的編號,比特位的內容代表是或否收到信號),所以在進程PCB裏會用位圖來記錄信號的產生,當進程發現PCB裏的位圖對應的比特位由0變爲1時,就知道產生了對應的信號,然後會在合適的時候進行信號的處理。信號是由操作系統直接或間接產生的,發信號的本質是修改PCB中位圖對應的比特位(把對應信號的比特位從0變爲1),修改PCB只能通過操作系統修改,因此信號是由操作系統直接或間接產生的。
那麼在PCB中描述阻塞信號和未決信號都用的是位圖,分別是block表和pending表,31個信號對應的信號操作都是函數,因此在PCB中還有一個函數指針數組來存放信號處理動作的函數的地址,我們稱其爲handler表。
- 每個信號都有兩個標誌位分別表示阻塞(block)和未決(pending),還有一個函數指針表示處理動作。信號產生時,內核在進程控制塊中設置該信號的未決標誌,直到信號遞達才清除該標誌。在上圖的例子中,SIGHUP信號未阻塞也未產生過,當它遞達時執行默認處理動作。
- SIGINT信號產生過,但正在被阻塞,所以暫時不能遞達。雖然它的處理動作是忽略,但在沒有解除阻塞之前不能忽略這個信號,因爲進程仍有機會改變處理動作之後再解除阻塞。
- SIGQUIT信號未產生過,一旦產生SIGQUIT信號將被阻塞,它的處理動作是用戶自定義函數sighandler。如果在進程解除對某信號的阻塞之前這種信號產生過多次,將如何處理?Linux是這樣實現的:常規信號在遞達之前產生多次只記一次,而實時信號在遞達之前產生多次可以依次放在一個隊列裏。
四、信號的處理
現在我們知道了信號的產生有四種方式,也知道了信號在處理之前會被記錄在位圖裏,那麼接下來就該處理了。信號產生後不是立即被處理的,而是在合適的時候,那麼什麼是合適的時候呢?主要是由操作系統決定的。那麼來看一下信號的幾種處理方式。可歸納爲三額動作。
信號處理的三種動作:
- ①默認動作:執行該信號的默認動作
- ②忽略動作:忽略此信號
- ③自定義動作:提供一個信號處理函數,要求內核在處理信號時切換到用戶態執行這個函數,這種方式也叫信號捕捉。
這裏的前兩個動作很好理解,通過位圖就可以知道它的處理,最後一個自定義纔是最麻煩的。下面分兩篇文章來說。