本節目標:
1. 掌握Linux信號的基本概念
2. 掌握信號產生的一般方式
3. 理解信號遞達和阻塞的概念,原理。
4. 掌握信號捕捉的一般方式。
5. 重新了解可重入函數的概念。
6. 了解競態條件的情景和處理方式
7. 了解SIGCHLD信號, 重新編寫信號處理函數的一般處理機制
?首先聲明:這里所講的信號與上文的信號量毫無關聯。
目錄
1. 信號是什么?
2. 信號的處理過程?
注意:
3. 信號的準備知識??
3.1 信號的種類
3.2 信號的行為
core dump(核心轉儲)?
3.3? 常見信號處理方式??
4. 信號的一生
4.1 信號的產生?
方法1--kill命令?編輯
方法2--鍵盤鍵入?
方法3--?系統調用
1. kill 向任意進程發送任意信號
2. raise 向自己發送任意信號
3. abort 向自己發送六號信號?
方法四--由軟件條件產生信號
方法五--硬件異常產生信號?
4.2 信號的保存?
4.2.1 pending、block與handler?
4.2.2 sigset_t?
4.2.3 信號集操作函數?
4.2.4 sigprocmask與sigpending?
sigprocmask?
sigpending
4.2.5 實驗?
實驗1
代碼?
?結果
?實驗2
?代碼
結果
?
1. 信號是什么?
什么是信號呢?
我們從生活引入。當你在房間里苦學c++的時候,媽媽喊你吃飯,你有兩個選擇--立即去吃飯、看完再吃飯,這個例子就可以完美的解釋什么是信號。
這就是我們日常生活中的信號以及對信號的處理過程。
那么我們要講的信號是什么呢??
我們所將的信號同上例本質是一樣的,不過是由某人發給進程,然后由進程進行一系列處理過程罷了。為什么用某人呢?因為這個某人并不確定,有可能是OS,有可能是用戶,也有可能是其他進程乃至進程自己(這個就涉及信號的產生了)。
那么從上面的例子我們可以知道信號與進程之間有哪些關系呢?
1. 信號既然是由其他人發送的,會中斷我們(進程),且這一信號是我們無法預料的,那么信號本身就是異步的,即信號是OS提供給用戶(進程)向其他進程發送異步信息的一種方式,這一過程是并發的。
2. 信號發來我們就需要能夠進行處理,這就說明我們(進程)具備認識信號的能力,并知道如何對該信號進行處理。因此進程不僅認識信號,而且還儲存有對信號的處理方式。
3. 當我們(進程)接收到信號,如果我們在做更重要的事,我們可以先對信號進行保存,等到合適的時候再進行處理。因此進程應當具備保存信號,以及在合適時機處理信號的能力
2. 信號的處理過程?
下圖是信號的處理過程時間軸示意圖,我們后續的講解線就是根據這個時間軸。
注意:
1. Ctrl-C 產生的信號只能發給前臺進程。一個命令后面加個&可以放到后臺運行,這樣Shell不必等待進程結束就可以接受新的命令,啟動新的進程。
2. Shell可以同時運行一個前臺進程和任意多個后臺進程,只有前臺進程才能接到像 Ctrl-C 這種控制鍵產生的信號。
3. 前臺進程在運行過程中用戶隨時可能按下 Ctrl-C 而產生一個信號,也就是說該進程的用戶空間代碼執行到任何地方都有可能收到 SIGINT 信號而終止,所以信號相對于進程的控制流程來說是異步(Asynchronous)的。
3. 信號的準備知識??
3.1 信號的種類
我們先來看看都有哪些信號(kill -l命令)
這些信號大部分的行為都是終止進程,還有一部分是忽略,暫停等等。?
3.2 信號的行為
下圖信號的行為,三十一個信號的行為都囊括其中。
core dump(核心轉儲)?
其他的信號默認處理動作都很好理解,但core動作是怎么回事,好像有點看不懂。
?
首先解釋什么是Core Dump(核心轉儲)。當一個進程要異常終止時,可以選擇把進程的用戶空間內存數據全部保存到磁盤上,文件名通常是core,這叫做Core Dump。進程異常終止通常是因為有Bug,比如非法內存訪問導致段錯誤,事后可以用調試器檢查core文件以查清錯誤原因,這叫做Post-mortem Debug(事后調試)。一個進程允許產生多大的core文件取決于進程的Resource Limit(這個信息保存 在PCB中)。默認是不允許產生core文件的,因為core文件中可能包含用戶密碼等敏感信息,不安全。在開發調試階段可以用ulimit命令改變這個限制,允許產生core文件。 首先用ulimit命令改變Shell進程的Resource Limit,允許core文件最大為1024K: $ ulimit -c 1024
也就是說,我們的云服務器默認關閉核心轉儲功能,在Linux中,我們的g++/gcc默認是release版本,因此如果要生成core文件,除卻上述的打開core dump功能外,還需要在編譯時加-g選項。
有這樣一種場景,倘若某種大型服務器出現異常,但由于大型服務器出錯的第一件事不是找錯,而是重新啟動,因此我們有自啟服務器的程序。如果服務器在無人發覺的情況下瘋狂終止又瘋狂重啟,經過一段時間后,會生成無數core文件,這會導致空間爆炸的問題。因此在部分系統里,core文件的名字就叫做core,一個進程即便不斷終止與重啟,也只會有一個core文件。
此前我們在講進程返回碼時有一個標志位略過沒有講,現在看他剛剛好。
3.3? 常見信號處理方式??
可選的處理動作有以下三種:
1. 忽略此信號。(即接收到該信號的進程對此信號進行忽略)
2. 執行該信號的默認處理動作。(每一個信號有自己的默認處理動作,如果用戶沒有對信號的處理動作進行自定義,那么就執行該默認處理動作)
3. 對信號進行捕捉。提供一個信號處理函數,要求內核在處理該信號時切換到用戶態執行這個處理函數,這種方式稱為捕捉(Catch)一個信號(即用戶對信號的處理動作進行自定義化)
比如:
SIGINT的默認處理動作是終止進程,我們現在對SIGINT信號進行捕捉,自定義其處理動作為打印一串字符。使用signal函數。
4. 信號的一生
4.1 信號的產生?
信號要想發給進程,首先當然要先產生,那么信號的產生方式有哪些呢?
我們先寫一個正常情況下不會終止的進程。
#include<iostream>
#include<unistd.h>int main()
{while(true){sleep(1);pid_t pid=getpid();std::cout<<"process pid :"<<pid<<std::endl;}return 0;
}
方法1--kill命令
方法2--鍵盤鍵入?
記得我們之前使用的ctrl+c嗎,它可以終止進程,但鍵盤可以輸入的信號可不只有他
ctrl+\后的core dumped是什么呢?
方法3--?系統調用
這里的系統調用一般有三種,我們挨個來看看。
1. kill 向任意進程發送任意信號
kill可以向任意進程發送任意信號,我們來試試吧。
我們看到實驗成功了,不過這一實驗有一些丑陋,大家可以使用父進程發送信號殺死子進程,同時記錄當前進程狀況。?
2. raise 向自己發送任意信號
3. abort 向自己發送六號信號?
相當于kill(getpid(),9)
方法四--由軟件條件產生信號
首先這一方式我們熟知的有SIGPIPE,即管道讀端關閉而寫端還在寫時,OS會向寫端進程發送SIGPIPE強制殺死該進程。
還有我們并未接觸過的SIGALARM,我們來看看。
我們來驗證一下。
這個函數的返回值是0或者是以前設定的鬧鐘時間還余下的秒數。打個比方,某人要小睡一覺,設定鬧鐘為30分鐘之后響,20分鐘后被人吵醒了,還想多睡一會兒,于是重新設定鬧鐘為15分鐘之后響,“以前設定的鬧鐘時間還余下的時間”就是10分鐘。如果seconds值為0,表示取消以前設定的鬧鐘,函數的返回值仍然是以前設定的鬧鐘時還余下的秒數。
鬧鐘是由OS發送的,而OS中的進程何其多,所以OS就需要對鬧鐘進行管理,先描述在組織。
方法五--硬件異常產生信號?
硬件異常被硬件以某種方式被硬件檢測到并通知內核,然后內核向當前進程發送適當的信號。例如當前進程執行了除以0的指令,CPU的運算單元會產生異常,內核將這個異常解釋 為SIGFPE信號發送給進程。再比如當前進程訪問了非法內存地址,,MMU會產生異常,內核將這個異常解釋為SIGSEGV信號發送給進程。
因此我們平時寫的程序崩潰了,就是硬件異常發送給我們進程信號了。
4.2 信號的保存?
欸你可能會疑惑,為什么信號的一生時間線中沒有發送信號的過程呢?別急,我們對信號的一生填充一下。?
我們學習了上面的內容,應該已經明白了向進程發送信號的過程是由OS來做的,因為產生信號的方式全部是系統調用。那么OS是如何發送的呢?
我們知道,進程是承擔系統資源的實體,那么信號既然要保存,自然也是存儲在進程中的某個區域。那么存在哪呢?進程地址空間嗎?
不是的,信號的保存是在pcb中的。信號本質并不屬于進程,而是屬于系統,但由于進程需要能夠對信號及時響應,因此進程需要保存信號,且進程要可以及時察覺到信號的變化,因此將信號保存在pcb中。
4.2.1 pending、block與handler?
那么問題來了,信號在pcb中要怎么保存呢?
由上圖我們可知,信號在pcb中的存儲是三張表, pending(未決信號)、block(阻塞信號)、handler(信號處理函數)。
注意,信號的屏蔽與忽略是截然不同的,信號的屏蔽是指信號始終處于未決,不對其進行處理;信號的忽略本身就是對信號的處理,即信號已然遞達。
這里要注意,我們一開始就說,只談1-31個信號,因此這里的位圖都是三十二個比特位,1-31位標識信號。
我們來看看三張表的作用?
我們之前有一個案例代碼,其中有對信號進行捕捉,其實就是讓我們的捕捉函數覆蓋了該信號的原處理函數。
看到這里,有沒有明白信號是如何發送給進程的呢?
沒錯,就是OS對pending表進行寫入,進程會時刻監視這三張表,并做相應處理。
我們之前所講信號的產生,無論是命令行輸入命令,還是程序代碼調用系統調用,都是讓OS幫我們向進程內寫入信號。那么系統調用究竟是什么呢?系統調用其實就是寫在系統內的函數,只有系統有權限使用。OS內會有一個函數指針數組,其內放的全部都是系統方法。
4.2.2 sigset_t?
每個信號只有一個bit的未決標志,非0即1,不記錄該信號產生了多少次,阻塞標志也是這樣表示的。因此,未決和阻塞標志可以用相同的數據類型sigset_t來存儲,sigset_t稱為信號集,這個類型可以表示每個信號的“有效”或“無效”狀態,在阻塞信號集中“有效”和“無效”的含義是該信號是否被阻塞,而在未決信號集中“有效”和“無效”的含義是該信號是否處于未決狀態。下一節將詳細介紹信號集的各種操作。 阻塞信號集也叫做當前進程的信號屏蔽字(Signal Mask),這里的“屏蔽”應該理解為阻塞而不是忽略。
4.2.3 信號集操作函數?
sigset_t類型對于每種信號用一個bit表示“有效”或“無效”狀態,至于這個類型內部如何存儲這些bit則依賴于系統實現,從使用者的角度是不必關心的,使用者只能調用以下函數來操作sigset_ t變量,而不應該對它的內部數據做任何解釋,比如用printf直接打印sigset_t變量是沒有意義的。
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
函數sigemptyset初始化set所指向的信號集,使其中所有信號的對應bit清零,表示該信號集不包含 任何有效信號。
函數sigfillset初始化set所指向的信號集,使其中所有信號的對應bit置為1,表示 該信號集的有效信號包括系統支持的所有信號。
注意,在使用sigset_ t類型的變量之前,一定要調用sigemptyset或sigfillset做初始化,使信號集處于確定的狀態。初始化sigset_t變量之后就可以在調用sigaddset和sigdelset在該信號集中添加或刪除某種有效信號。
這四個函數都是成功返回0,出錯返回-1。sigismember是一個布爾函數,用于判斷一個信號集的有效信號中是否包含某種信號,若包含則返回1,不包含則返回0,出錯返回-1。
注意:這里的幾個函數僅僅是對創建出的對象進行操作,要設置入進程內需要其他的函數,
4.2.4 sigprocmask與sigpending?
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參數更改信號屏蔽字。(即set為要設置入進程block位圖的信號集,oset為輸出型參數,將會記錄進程原block位圖信號集)假設當前的信號屏蔽字為mask,下表說明了how參數的可選值。
?如果調用sigprocmask解除了若干個對未決信號的阻塞,那么在sigprocmask返回前,OS會立即將其中一個信號遞達。
sigpending
#include <signal.h>
sigpending
讀取當前進程的未決信號集,通過set參數傳出。調用成功則返回0,出錯則返回-1。
4.2.5 實驗?
接下來我們將使用上面的函數做一個實驗
實驗1
將2,3信號屏蔽(sigprocmask)
向進程發送信號,打印pending位圖。
代碼?
#include<iostream>
#include<unistd.h>
#include<signal.h>//打印當前pending信號集
void printsig(sigset_t p)
{std::cout<<getpid()<<" ";for(int i=31;i>0;i--){if(sigismember(&p,i))std::cout<<"1";elsestd::cout<<"0";}std::cout<<std::endl;
}int main()
{sigset_t s,p;//創建信號集sigemptyset(&s);//清空信號集sigaddset(&s,2);//向信號集內添加有效信號2sigaddset(&s,3);//添加3sigprocmask(SIG_BLOCK,&s,nullptr);//這里我們不需要記錄原信號屏蔽字while(true){sleep(1);sigpending(&p);//獲取當前進程pending信號集printsig(p);//打印pending信號集}return 0;
}
?結果
?
?實驗2
將所有信號屏蔽(sigprocmask)
向進程發送信號,打印pending位圖。
?代碼
#include<iostream>
#include<unistd.h>
#include<signal.h>//打印當前pending信號集
void printsig(sigset_t p)
{std::cout<<getpid()<<" ";for(int i=31;i>0;i--){if(sigismember(&p,i))std::cout<<"1";elsestd::cout<<"0";}std::cout<<std::endl;
}int main()
{sigset_t s,p;//創建信號集sigemptyset(&s);//清空信號集for(int i=1;i<32;i++){sigaddset(&s,i);//向信號集內添加有效信號}sigprocmask(SIG_BLOCK,&s,nullptr);//這里我們不需要記錄原信號屏蔽字while(true){sleep(1);sigpending(&p);//獲取當前進程pending信號集printsig(p);//打印pending信號集}return 0;
}
結果
?
下篇我們來看信號的處理。