Linux之信號的保存方式
文章目錄 信號相關概念信號遞達信號未決信號阻塞內核中的示意圖 信號集的操作函數
前面對于信號的產生中對操作系統(tǒng)有了一個基礎的認知,對于一個真正的操作系統(tǒng)來說,進程是由操作系統(tǒng)進行調度的,那操作系統(tǒng)本身也是代碼,是由誰進行調度的?
實際上是有一個CMOS時鐘這樣的硬件,通過特定的時鐘周期不斷地向CPU發(fā)送并觸發(fā)時鐘中斷,那么在觸發(fā)時鐘中斷的時候,實際上操作系統(tǒng)的內部已經綁定好了對應的調度方法,所以在操作系統(tǒng)啟動的時候,就會提前把觸發(fā)的工作做好,在啟動之后就會變成一個死循環(huán)的軟件,這也就解釋了為什么在啟動了之后,操作系統(tǒng)雖然是軟件,但是卻不會關機,只有當電腦關機后操作系統(tǒng)才會關機的原因,就是因為它本質上就是一個死循環(huán),所以基于中斷,一旦對應的時鐘周期到了,就會執(zhí)行時鐘中斷對應的方法,也就有了調度的方法,基于這樣的進度就可以把進程按照時間的節(jié)奏一步一步的走起來
其實換個角度來講,操作系統(tǒng)其實是一卡一卡的執(zhí)行的,因為它在執(zhí)行中間的這個時間間隔就是發(fā)送時鐘中斷的時間間隔,時鐘中斷的這個時間其實就是提醒操作系統(tǒng)去執(zhí)行對應的調度方法,同時在中斷向量表中還會綁定一些硬件對應的操作方法,所以最后得出的結論是,操作系統(tǒng)實際上是由硬件促使操作系統(tǒng)跑起來的
有了上述的思想認知,再進行對于進程信號產生的回顧,進程的信號產生是由操作系統(tǒng)寫入到進程中,相當于是操作系統(tǒng)向進程發(fā)送信號,而在前面的認知中知道,進程對于信號的處理也并非是及時處理,而可能會保存到某個位置,在合適的時候進行處理,那么現在接下來的話題就是,這個信號會如何進行存儲,存儲之后又該如何進行處理呢?
信號相關概念
信號遞達
第一個問題是,信號會被記錄存儲在哪里,結論是會被存儲到PCB中的位圖中,這個是之前就已經有的結論,每一個進程的PCB中都會有一個用來描述進程接受的信號的位圖,借助這個位圖就可以獲取到該進程收到了什么信號
接下來的問題是關于處理信號及其相關概念:
- 實際執(zhí)行信號的處理動作稱為信號遞達
- 信號從產生到遞達之間的狀態(tài),稱為信號未決
- 進程可以選擇阻塞某個信號
- 被阻塞的信號產生時將保持在未決狀態(tài),直到進程解除對此信號的阻塞,才執(zhí)行遞達的動作
注意,阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是在遞達之后可選的一種處理動作
下面基于這幾個名詞進行解釋,首先解釋的信號遞達
所謂信號遞達,說的是當進程收到一個信號后,它需要在合適的時候處理這個信號,而這里的處理信號這個過程就叫做信號的遞達,簡單來說可以理解成,已經收到這個信號,并且準備處理這個信號了,這個處理的動作就叫做信號遞達
在前面的內容中提到過,對于信號的處理有三種方式,第一種叫做忽略,第二種是默認,第三種是自定義捕捉,這其實就是說信號遞達的問題,當信號遞達后,也就是說此時信號已經要進行處理了,那么有上述的三種處理方式
給出下面的參考代碼
void handler(int signo) { cout << "收到了" << signo << "號信號" << endl; } int main() { cout << "pid:" << getpid() << endl; signal(2, handler); while (true) ; return 0; }
此時對進程發(fā)送2號信號,那么對應的這個進程就會調用自定義的處理方式,對應的結果也符合預期,這個過程就是一個自定義捕捉的過程
SIG_DFL和SIG_IGN
void handler(int signo) { cout << "收到了" << signo << "號信號" << endl; } int main() { cout << "pid:" << getpid() << endl; signal(2, handler); signal(3, SIG_IGN); signal(4, SIG_DFL); while (true) ; return 0; }
上面的兩個選項也是一種處理方式,可能這里會有疑問,為什么自定義函數的能和宏放到一起呢?signal函數的第二個參數可是函數指針
其實在內部,是通過強轉轉換而來的,也是把一個宏對應的內容轉換成了函數指針類型
在這當中需要理清的一個邏輯是,在這當中是有三種處理方式,忽略默認自定義捕捉,這個忽略該如何理解?
忽略也算是處理
忽略也算處理,現在進程收到了一個信號,那它該如何處理它呢?
答案是不處理,不處理就是忽略了這個信號,所以說忽略本質上也算是三種處理方式中的一種,處理方式就是不管這個信號,忽略它
所以之后對于信號處理的三種方式,默認自定義忽略,這三種處理方式有了一個統(tǒng)一的名字就叫做信號遞達,信號處理這個名詞也會被信號遞達這個概念所代替
信號未決
下面講述的概念是信號未決,信號未決通俗來講就是信號從產生到遞達這個階段的狀態(tài)就叫做信號未決,可以這樣理解,就是信號暫時還沒有被決定該如何處理,這個就叫做信號未決,就是說信號此時已經有了,但是還沒有處理,在這個階段的狀態(tài)就叫做信號未決,這也是可以理解的內容,因為在這個時間內進程可能在做更重要的事,還不能對這個信號做出處理,所以此時就要求需要對這個進程有一定的保存能力,在保存信號的方面可以采用一個位圖來進行保存普通信號,所以在信號產生到遞達之間的狀態(tài),就叫做信號未決
換句話說,信號未決就是從產生到遞達這樣的一個狀態(tài),當信號產生的時候就要把它保存起來,遞達就要把信號處理掉,但是信號的處理不是立刻處理的,在這個過程中就是說信號是未決的
信號阻塞
這是一個新的概念,叫做阻塞,那如何理解阻塞呢?
阻塞簡單來講就是說某一個信號可以被阻塞,也有一種說法叫做被阻塞的信號可以保持在一個未決的狀態(tài)中,直到進程解除對于該信號的阻塞才會調用對應的執(zhí)行動作。
阻塞的含義可以理解為,信號產生后會保存到對應的位圖中,此時信號所處的狀態(tài)就是信號未決,信號未決后,如果該信號被阻塞,那么這個信號就會一直保持未決的狀態(tài),直到這個信號解除阻塞
忽略和阻塞
忽略是信號處理中的一種,也就是說信號遞達中包含忽略這種處理方式,而信號阻塞是導致不能夠信號遞達的一個原因,這兩個概念是不一樣的。
信號忽略是說,這個信號被忽略了,對于該信號的處理方式是忽略,而信號阻塞是壓根不處理這個信號,這個信號一直處于產生到遞達這樣的一個階段中,處于未決狀態(tài),這兩個是截然不同的兩個概念,這也是可以理解的
信號是未決的,該信號一定被阻塞?
顯然是不對的,信號是未決的,可能是出于阻塞狀態(tài),但是也可能是因為這個進程正在做更重要的事,所以它暫時沒有被處理,處于未決狀態(tài),但是當這個進程做完了當前最重要的事,那么它一定會立刻對信號進行處理,此時就不再是信號未決的狀態(tài)了
內核中的示意圖
上圖表示的是,在進程的PCB中存儲的關于信號的結構信息,在PCB中關于信號會維護三張表,分別存儲的是信號的阻塞情況,表示有哪些信號被阻塞了,也存儲了信號的未決情況,表示有哪些信號此時遞達了,但是還沒有處理,也存儲了信號對應的處理方式,表示信號對應的處理方式是什么,默認忽略或是自定義捕捉
在內核源碼中,對于上述這三張表的定義也總結如下,可以看到對應的handler處理方法中存儲了對應的函數指針,表示的就是不同信號的處理方式:
由此,對于信號的存儲有了一個更深層次的理解,為什么進程可以識別到信號,本質上來說就是對于幾號信號在pending位圖中已經存儲好了,幾號信號,是否阻塞,對應的解決方式,都在三張表中有具體的體現,根據數組的下標就能很輕松的獲取到對應的存儲情況和處理方式,在操作系統(tǒng)運行的時候,最起碼的pending表和handler表是已經存儲好的,所以才有上述的這一套邏輯
而對于block表來說,也有一些不同的理解:
那這個block表該如何理解呢?
block表,表示的是對特定信號的屏蔽,也可以說是對一些信號的阻塞,換句話說,這個位圖和后面的兩個位圖是完全一樣的位圖結構,有了一個信號,就先在pending位圖中記錄下這個信號已經處于未決狀態(tài)了,再在合適的時機去到block位圖中尋找,如果這個信號沒有被阻塞,那么就執(zhí)行handler表中的方法,如果這個信號被block阻塞了,那么就讓這個信號一直處于pending的狀態(tài),等block表中什么時候恢復了,再去執(zhí)行,當然這當中還有邊角的問題,比如誰先置1和置0的問題,后續(xù)會進行相關的實驗
正是因為有了這三張表,所以對于信號的操作其實都是圍繞這三張表進行展開的,比如對于PCB來說,這三張表是由操作系統(tǒng)提供的,那么操作系統(tǒng)就會想辦法去獲取并設置修改block表來表示對于一個或多個信號的屏蔽的目的,也可以比如說是對于pending位圖做修改,或是獲取pending位圖,比如在之前的bash中的kill命令,本質上就是向指定的進程中寫入信號,實際上就是在對這個pending表進行的寫入工作,而在之前的signal這樣的自定義捕捉函數,本質上也是在修改handler對應的表,這也和前面的知識進行了一定的串聯
由此可以看出,操作系統(tǒng)提供對應的系統(tǒng)調用,就是對于這三張表的修改過程,但是這還不夠,用戶該如何去修改?直接深入到內核中去修改位圖中比特位的情況,這對于用戶來說是一個很大的挑戰(zhàn),同時對于操作系統(tǒng)來說也違背了它設計的初衷,因此操作系統(tǒng)還會提供對應修改位圖的方法,提供了一些新的數據類型,用來幫助用戶對于這三張表實現一些操作更改等
多信號問題
現在保存的信號用pending位圖表示是否收到了這個信號,但是這個進程可能會在很短的時間內同時收到信號,這個時間短到可能不能及時處理這個信號,在相當短的時間內,連續(xù)收到了多個同一個信號,pending位圖中只能記錄一次,換句話說,此時可能發(fā)送了10個相同的信號,但是只記錄了一次,剩下的九次就相當于直接被操作系統(tǒng)丟棄了,本質上來說是比特位只能是0和1,如果不斷的從1變成1,實際上也獲得不了什么新的效果,只能保存歷史上最近的一次封信,所以在進程解除對于某個信號的阻塞之前,可能這個信號已經被發(fā)送了很多次了,只是不能進行獲取,不管發(fā)送多少次,最終都是一次
因此操作系統(tǒng)允許向進程推送信號多次,但是在遞達之前,不管推送多少次,操作系統(tǒng)只看一次,這是由操作系統(tǒng)本身的位圖結構決定的,不過這樣情況出現的概率不大,其次是也可以用在信號處理內部放一個計數器,來表示如果設定不夠就重新再發(fā),這樣的處理方式也是可以接受的
不過值得注意的是,這種只記錄一次的信號叫做普通信號,而與之對應的還有一個實時信號,實時信號在前面的內容中也有所涉獵,它的實時信號中的實時概念也就體現在在進程的PCB中有一個實時的信號隊列,每一個信號就相當于一個結構體對象,那么就用隊列的形式來管理這種信號,也就叫做實時信號,但是這里不考慮實時信號,只是對普通信號做出一個基本的理解
信號集的操作函數
下面進行的模塊就是對于信號集的操作函數,下面進行一一列舉內容:
下圖描述的是對于信號的一些函數,根據這些函數來對于信號的操作函數有一個基本的理解
sigemptyset函數
這個函數的主要作用是對于set所指向的操作集進行一個基本的初始化,簡單來說就是把比特位置0,并且這當中不應該有任何有效的信號
sigfillset函數
這個函數的主要作用是把信號集都置為1,表示這當中存儲的是有效的信號
sigaddset和sigdelset函數
這兩個函數是對于信號的增加和刪除
sigismember函數
這個函數是用來查詢某個函數是否在當前的pending信號集中,返回值是bool類
sigprocmask函數
這個函數是用來讀取或更改進程的信號屏蔽字,也就是阻塞信號集,而這個后面的參數,一個是用什么方法來傳遞,后面的兩個參數都是對應的信號集,簡單來說就是通過參數來覆蓋當前的信號集
對于第一個參數來說,它有下面的幾種方式進行傳遞
SIG_BLOCK
這個操作會把當前信號阻塞集合和set所指向的信號集合取并集,簡單來說是把set集合加入到當前的信號阻塞集合中
SIG_SETMASK
這個操作會把當前信號阻塞集合設置為set所指向的信號集合,會把當前集合直接覆蓋掉
SIG_UNBLOCK
這個操作是把當前信號阻塞集合與set集合中的信號的補集取交集,簡單來說就是把set中的信號進行解除
后面的兩個參數值得注意一下,一個是set,一個是oset,這兩個參數是有其對應的意義的,第一個set表示的是要傳入覆蓋的對應的位圖是什么樣的,第二個oset是一個輸出型參數,它保存的是當前位圖的情況,所以本質上來說可以理解成是一個保存了前面位圖的參數,這樣可以方便后續(xù)進行恢復等等操作,具體的后續(xù)進行使用
代碼實踐
下面用代碼實踐來表示
第一個要完成的動作是把2號信號加到信號屏蔽集中,現在有一個問題是,我設置了加到屏蔽集合中就真的屏蔽了嗎?嚴格意義來說并不是,因為這些內容本質上是在棧上開辟的空間,所以它本質上是在代碼區(qū)域上,并沒有真正設置到操作系統(tǒng)中,所以此時把2號信號添加到集合中也只是在棧上修改了一個變量的信息,這只是語言層面上的設置,而只有通過調用sigprocmask函數后,才能是真正意義上的進行屏蔽的操作,表示的是直接修改了在內核中對于阻塞表的操作,修改了內核的字段,不過,從廣義的角度來講,其實這樣的操作就被叫做是加入到了內核中
這里由于是第一次使用,所以要將語言層面和內核層面分開,再怎么說對于位圖的修改也只是語言層面上,實際的運用中并沒有進行位圖的修改,而只有用sigprocmask函數之后,才是進入內核的層面上修改了內核中的相應字段
寫出示例代碼,如下所示
void handler(int signo) { cout << "收到了" << signo << "號信號" << endl; } int main() { signal(2, handler); cout << "當前pid:" << getpid() << endl; // 1. 屏蔽2號信號 sigset_t set, oset; sigemptyset(&set); sigemptyset(&oset); sigaddset(&set, 2); sigprocmask(SIG_BLOCK, &set, &oset); while(true) sleep(1); return 0; }
對上述代碼進行運行得到如下結果:
由此可以看出,此時確實對于2號信號進行了屏蔽效果,只有發(fā)送其他信號才會有反應
這是由于,經過了sigprocmask之后,此時的2號信號已經存儲在了pending表中,那么它此時就不能再被執(zhí)行了
kill -9
9號信號是最特殊的信號,它本身是不能被屏蔽的,它也被叫做管理員信號,也叫做管理員之光,如果有任何進程出現問題,都可以用kill -9來殺掉,并且保證這個信號不會被屏蔽
sigpending函數
這個函數的作用也很簡單,就是讀取當前進程的pending信號集,通過set參數傳出,調用成功返回0,失敗返回-1
則可以借助這個函數實現下面的代碼內容:
void handler(int signo) { cout << "收到了" << signo << "號信號" << endl; } void PrintSignal(const sigset_t &set) { for (int i = 31; i >= 1; i--) { if (sigismember(&set, i)) cout << "1"; else cout << "0"; } cout << endl; } int main() { signal(2, handler); cout << "當前pid:" << getpid() << endl; // 1. 屏蔽2號信號 sigset_t set, oset; sigemptyset(&set); sigemptyset(&oset); sigaddset(&set, 2); sigprocmask(SIG_BLOCK, &set, &oset); // 2. 讓進程獲取現在的pending sigset_t pending; while(true) { sigpending(&pending); PrintSignal(pending); sleep(1); } while (true) sleep(1); return 0; }
而想要解除屏蔽也很簡單,這個時候就用上了oset的內容:
void handler(int signo) { cout << "收到了" << signo << "號信號" << endl; } void PrintSignal(const sigset_t &set) { for (int i = 31; i >= 1; i--) { if (sigismember(&set, i)) cout << "1"; else cout << "0"; } cout << endl; } int main() { signal(2, handler); cout << "當前pid:" << getpid() << endl; // 1. 屏蔽2號信號 sigset_t set, oset; sigemptyset(&set); sigemptyset(&oset); sigaddset(&set, 2); sigprocmask(SIG_BLOCK, &set, &oset); // 2. 讓進程獲取現在的pending sigset_t pending; int cut = 0; while (true) { sigpending(&pending); PrintSignal(pending); cut++; sleep(1); if (cut == 5) { // 3. 解除屏蔽 cout << "已解除屏蔽" << endl; sigprocmask(SIG_SETMASK, &oset, nullptr); sigpending(&pending); PrintSignal(pending); sleep(1); } } while (true) sleep(1); return 0; }
由此,信號的保存也就完成了
總結
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
ubuntu 16.04系統(tǒng)完美解決pip不能升級的問題
這篇文章主要介紹了ubuntu 16.04系統(tǒng)完美解決pip不能升級的問題 ,本文圖文并茂給大家介紹的非常詳細,需要的朋友可以參考下2018-04-04