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