Linux進(jìn)程信號(hào)的捕捉&&信號(hào)補(bǔ)充內(nèi)容方式
一、信號(hào)的捕捉
對(duì)于如何處理信號(hào),前面也講了 signal 接口,如 signal(2, handler),所以對(duì) 2 號(hào)信號(hào)執(zhí)行 handler 捕捉動(dòng)作,本質(zhì)是 OS 去 task_struct 通過(guò) 2 號(hào)信號(hào)作索引,找到內(nèi)核中 handler 函數(shù)指針數(shù)組中對(duì)應(yīng)的方法,然后把數(shù)組內(nèi)容改成你自己在用戶層傳入的 handler 函數(shù)指針?lè)椒ā?/p>
這里我們要討論的是上面遺留下來(lái)的問(wèn)題 —— 進(jìn)程收到信號(hào)時(shí),不是立即處理的,而是在合適的時(shí)候再處理,那合適的時(shí)候是什么時(shí)候呢 ?
這里先把結(jié)論寫出來(lái)
當(dāng)進(jìn)程從內(nèi)核態(tài)返回到用戶態(tài)的時(shí)候,進(jìn)行信號(hào)的檢測(cè)和處理。
1、用戶態(tài)和內(nèi)核態(tài)
進(jìn)程如果訪問(wèn)的是用戶空間的代碼,此時(shí)的狀態(tài)就是用戶態(tài);如果訪問(wèn)的是內(nèi)核空間,此時(shí)的狀態(tài)就是內(nèi)核態(tài)。
我們經(jīng)常需要通過(guò)系統(tǒng)調(diào)用訪問(wèn)內(nèi)核,系統(tǒng)調(diào)用是 OS 提供的方法,執(zhí)行 OS 的方法就可能訪問(wèn) OS 中的代碼和數(shù)據(jù),普通用戶沒(méi)有這個(gè)權(quán)限。所以在調(diào)用系統(tǒng)接口時(shí),系統(tǒng)會(huì)自動(dòng)進(jìn)行身份切換 user ? kernel
那 OS 是怎么知道現(xiàn)在的狀態(tài)是用戶態(tài)還是內(nèi)核態(tài)?
因?yàn)?CPU 中有一個(gè)狀態(tài)寄存器或者說(shuō)權(quán)限相關(guān)的寄存器,它可以表示所處的狀態(tài)。每個(gè)用戶進(jìn)程都有自己的用戶級(jí)頁(yè)表,OS 中也有且只有一份內(nèi)核級(jí)頁(yè)表。也就是說(shuō),多個(gè)進(jìn)程可以通過(guò)權(quán)限提升來(lái)訪問(wèn)同一張內(nèi)核級(jí)頁(yè)表,每個(gè)進(jìn)程變成內(nèi)核態(tài)的時(shí)候訪問(wèn)的就是同一份數(shù)據(jù)。所以,OS 區(qū)分是用戶態(tài)還是內(nèi)核態(tài),除了寄存器保存了權(quán)限相關(guān)的數(shù)據(jù)之外,還要看進(jìn)程使用的是哪個(gè)種類的頁(yè)表。
在什么情況下會(huì)觸發(fā)從用戶態(tài)到內(nèi)核態(tài)呢?
這里有很多種方式:比如,自己寫的一個(gè) cin 程序一運(yùn)行就卡在那里,你按了 abc,然后程序就會(huì)拿到 abc,本質(zhì)就是鍵盤在觸發(fā)的時(shí)候被 OS 先識(shí)別到,然后放在 OS 的緩沖區(qū)中,而你的程序在從 OS 的緩沖區(qū)中讀取。其中 OS 是通過(guò)一種中斷技術(shù),這個(gè)中斷指的是硬件方面的中斷,如 8259 中斷器,它是一種芯片,用于管理計(jì)算機(jī)系統(tǒng)中的中斷請(qǐng)求,通常和 CPU 一起使用。
再舉個(gè)例子,如果了解過(guò)匯編,可能聽(tīng)說(shuō)過(guò) int 80,它就是傳說(shuō)中系統(tǒng)調(diào)用接口的底層原理,系統(tǒng)調(diào)用的底層原理就是通過(guò)指令 int 80 來(lái)中斷陷入內(nèi)核。還有一種比較好理解的,就是在調(diào)用系統(tǒng)接口后就陷入內(nèi)核,然后就可以執(zhí)行內(nèi)核代碼。然后當(dāng)從內(nèi)核態(tài)返回用戶態(tài)時(shí)就更簡(jiǎn)單了,當(dāng)我們調(diào)完系統(tǒng)接口就返到用戶態(tài)了。總之,這里只需要知道從用戶態(tài)到內(nèi)核態(tài)是有很多種方式的就行。
用戶態(tài)和內(nèi)核態(tài)的權(quán)限級(jí)別不同,那么自然能看到的資源是不一樣的。內(nèi)核態(tài)的權(quán)限級(jí)別一定更高,但它并不代表內(nèi)核態(tài)能直接訪問(wèn)用戶態(tài)。前面說(shuō)了信號(hào)捕捉的時(shí)間點(diǎn)是內(nèi)核態(tài) ? 用戶態(tài)的時(shí)候,信號(hào)被處理叫做信號(hào)遞達(dá),遞達(dá)有忽略、默認(rèn)、自定義,自定義動(dòng)作就叫做捕捉動(dòng)作,只要理解了捕捉,那么忽略和默認(rèn)就簡(jiǎn)單了。上圖就是整個(gè)信號(hào)的捕捉過(guò)程:在 CPU 執(zhí)行我們的代碼時(shí),一定會(huì)調(diào)用系統(tǒng)調(diào)用。
系統(tǒng)調(diào)用是函數(shù),是 OS 提供的,也有代碼,需要被執(zhí)行,那么應(yīng)該以 “什么態(tài)” 執(zhí)行呢?實(shí)際上用戶態(tài)中進(jìn)程調(diào)用系統(tǒng)調(diào)用時(shí)必須得陷入內(nèi)核以用戶態(tài)身份執(zhí)行,執(zhí)行完畢后又返回用戶態(tài),繼續(xù)執(zhí)行用戶態(tài)中的代碼,那么問(wèn)題就是可以直接以內(nèi)核態(tài)的身份去執(zhí)行用戶態(tài)中的代碼嗎?
從內(nèi)核態(tài)返回到用戶態(tài)之前,OS 會(huì)做一系列的檢測(cè)捕捉工作,它會(huì)檢測(cè)當(dāng)前進(jìn)程是否有信號(hào)需要處理,如果沒(méi)有就會(huì)返回系統(tǒng)調(diào)用,如果有,那就先處理(具體它會(huì)遍歷識(shí)別位圖: 假如信號(hào) pending 了,且沒(méi)有被 block,那就會(huì)執(zhí)行 handler 方法,比如說(shuō)終止進(jìn)程,那就會(huì)釋放這個(gè)進(jìn)程,如果是暫停,那就不用返回系統(tǒng)調(diào)用,然后再把進(jìn)程 pcb 放在暫停隊(duì)列中,如果是忽略那就把 pending 中對(duì)應(yīng)的比特位由 1 變?yōu)?0,然后返回系統(tǒng)調(diào)用)。所以,可以看到比較難處理的是自定義捕捉,當(dāng) 3 號(hào)信號(hào)捕捉時(shí)且收到了 pending,沒(méi)有被 block,那么就會(huì)執(zhí)行用戶空間中的捕捉方法。換而言之,我們因?yàn)橄到y(tǒng)調(diào)用而陷入內(nèi)核,執(zhí)行系統(tǒng)方法,執(zhí)行完方法后做信號(hào)檢測(cè),檢測(cè)到信號(hào)是自定義捕捉,那么就會(huì)執(zhí)行自定義捕捉的方法。此時(shí),應(yīng)該以 “什么態(tài)” 執(zhí)行信號(hào)捕捉方法?
理論來(lái)說(shuō),內(nèi)核態(tài)是絕對(duì)可以的,因?yàn)閮?nèi)核態(tài)的權(quán)限比用戶態(tài)的權(quán)限高,但實(shí)際并不能以內(nèi)核態(tài)的身份去執(zhí)行用戶態(tài)的代碼,因?yàn)?OS 不相信任何人寫的任何代碼,這樣設(shè)計(jì)就很有可能讓惡意用戶利用導(dǎo)致系統(tǒng)不安全。所以必須是用戶態(tài)執(zhí)行用戶空間的代碼,內(nèi)核態(tài)執(zhí)行內(nèi)核空間的代碼,所以你是用戶態(tài)要執(zhí)行內(nèi)核態(tài)的代碼,你是內(nèi)核態(tài)要執(zhí)行用戶態(tài)的代碼,必須進(jìn)行狀態(tài)或者說(shuō)權(quán)限切換。所以,信號(hào)捕捉的完整流程就是在用戶區(qū)中因?yàn)橹袛?、異常或系統(tǒng)調(diào)用,接著切換權(quán)限陷入內(nèi)核執(zhí)行系統(tǒng)方法,然后再返回發(fā)現(xiàn)有信號(hào)需要被捕捉執(zhí)行,接著切換權(quán)限去執(zhí)行捕捉方法,然后再執(zhí)行特殊的系統(tǒng)調(diào)用sigretum再次陷入內(nèi)核,再執(zhí)行 sys_sigreturn() 系統(tǒng)調(diào)用返回用戶區(qū)。
注意切換到用戶態(tài)執(zhí)行捕捉方法后不能直接返回系統(tǒng)調(diào)用,因?yàn)樵?jīng)執(zhí)行捕捉方法時(shí)是由 OS 進(jìn)入的,所以必須得利用系統(tǒng)接口再次陷入內(nèi)核,最后由內(nèi)核調(diào)用系統(tǒng)接口返回用戶區(qū)。
2、內(nèi)核如何實(shí)現(xiàn)信號(hào)的捕捉
上面的圖和文字都說(shuō)的太復(fù)雜了,這里我們簡(jiǎn)化一下,宏觀來(lái)看信號(hào)的捕捉過(guò)程就是狀態(tài)權(quán)限切換的過(guò)程,這里的藍(lán)點(diǎn)表示信號(hào)捕捉過(guò)程中狀態(tài)權(quán)限切換的次數(shù)。其中完整流程就是:
- 調(diào)用系統(tǒng)調(diào)用,陷入內(nèi)核。
- 執(zhí)行完系統(tǒng)任務(wù)。
- 進(jìn)行信號(hào)檢測(cè)。
- 執(zhí)行捕捉代碼,調(diào)用 sigturm 再次陷入內(nèi)核。
- 調(diào)用 sys_sigreturn,返回到用戶區(qū)中系統(tǒng)調(diào)用點(diǎn)。
如果信號(hào)的處理動(dòng)作是用戶自定義函數(shù), 在信號(hào)遞達(dá)時(shí)就調(diào)用這個(gè)函數(shù) , 這稱為捕捉信號(hào)。由于信號(hào)處理函數(shù)的代碼是在用戶空間的,處理過(guò)程比較復(fù)雜。
舉例如下: 用戶程序注冊(cè)了 SIGQUIT 信號(hào)的處理函數(shù) sighandler 。當(dāng)前正在執(zhí)行 main 函數(shù), 這時(shí)發(fā)生中斷或異常切換到內(nèi)核態(tài)。在中斷處理完畢后要返回用戶態(tài)的 main 函數(shù)之前檢查到有信號(hào) SIGQUIT 遞達(dá)。
內(nèi)核決定返回用戶態(tài)后不是恢復(fù) main 函數(shù)的上下文繼續(xù)執(zhí)行,而是執(zhí)行 sighandler 函數(shù), sighandler 和 main 函數(shù)使用不同的堆棧空間, 它們之間不存在調(diào)用和被調(diào)用的關(guān)系, 是兩個(gè)獨(dú)立的控制流程。
sighandler 函數(shù)返回后自動(dòng)執(zhí)行特殊的系統(tǒng)調(diào)用 sigreturn 再次進(jìn)入內(nèi)核態(tài)。 如果沒(méi)有新的信號(hào)要遞達(dá),這次再返回用戶態(tài)就是恢復(fù) main 函數(shù)的上下文繼續(xù)執(zhí)行了。
3、sigaction
對(duì)于修改 handler 表的操作接口,前面已經(jīng)了解過(guò) signal 了,下面再講講 sigaction,sigaction 相比 signal 有更多的選項(xiàng),不過(guò)只需要知道它怎么用就行了,因?yàn)樗骖櫫藢?shí)時(shí)信號(hào)。
(1)接口介紹
man sigaction int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- signum:指定捕捉信號(hào)的編號(hào)。
- act:輸入性參數(shù),如何處理信號(hào),它是一個(gè)結(jié)構(gòu)體指針,第 2 與第 5 個(gè)字段是實(shí)時(shí)信號(hào)相關(guān)的,可以不管它。
- oldact:輸出型參數(shù),如果需要可以把老的信號(hào)捕捉方式保存,不需要?jiǎng)t NULL。
成功返回0,失敗返回-1
這是這個(gè)結(jié)構(gòu)體的內(nèi)容
在這個(gè)結(jié)構(gòu)體中,我們只關(guān)心這兩個(gè)字段。其他的字段與實(shí)時(shí)信號(hào)有關(guān)
如下樣例所示可以簡(jiǎn)單的先用起來(lái)這個(gè)函數(shù)
#include <iostream> #include <signal.h> #include <cstring> #include <unistd.h> void handler(int signo) { std::cout<<"捕捉到2號(hào)信號(hào)"<<std::endl; } int main() { struct sigaction act,oct; memset(&act,0,sizeof(act)); memset(&oct,0,sizeof(oct)); act.sa_handler=handler; sigaction(2,&act,&oct); while(true) { std::cout<<"hello linux"<<std::endl; sleep(1); } return 0; }
運(yùn)行結(jié)果如下
那我們想知道pending位圖接收到信號(hào),會(huì)在位圖里把0->1,那什么時(shí)候把1->0呢?
#include <iostream> #include <signal.h> #include <cstring> #include <unistd.h> using namespace std; void PrintPending() { sigset_t pending; sigpending(&pending); for(int signo=31;signo>=1;signo--) { if(sigismember(&pending,signo)) { cout<<"1"; } else { cout<<"0"; } } cout<<endl; } void handler(int signo) { PrintPending(); std::cout<<"捕捉到2號(hào)信號(hào)"<<std::endl; } int main() { struct sigaction act,oct; memset(&act,0,sizeof(act)); memset(&oct,0,sizeof(oct)); act.sa_handler=handler; sigaction(2,&act,&oct); while(true) { cout<<"hello linux"<<endl; sleep(1); } return 0; }
所以pending位圖,執(zhí)行捕捉方法之前,先清0,在調(diào)用
我們現(xiàn)在可以驗(yàn)證之前的結(jié)論,當(dāng)某個(gè)信號(hào)的處理函數(shù)被調(diào)用時(shí),內(nèi)核自動(dòng)將當(dāng)前信號(hào)加入進(jìn)程信號(hào)的屏蔽字中,當(dāng)信號(hào)處理函數(shù)返回自動(dòng)恢復(fù)原來(lái)的信號(hào)屏蔽字
我們寫一份代碼驗(yàn)證一下
我們先發(fā)送2號(hào)信號(hào)的時(shí)候,被捕捉,執(zhí)行自定義函數(shù),我們這里寫了一個(gè)循環(huán),是為了證明當(dāng)某個(gè)信號(hào)在處理的時(shí)候,內(nèi)核將該信號(hào)先加入到block中不執(zhí)行該信號(hào)的動(dòng)作。循環(huán)5秒后解除屏蔽會(huì)自動(dòng)執(zhí)行。
我們來(lái)看下運(yùn)行結(jié)果
即操作系統(tǒng)不允許對(duì)某個(gè)信號(hào)重復(fù)捕捉,最多只能捕捉一層
信號(hào)被處理的時(shí)候,對(duì)應(yīng)的信號(hào)也會(huì)被添加到block表中,防止信號(hào)捕捉被嵌套調(diào)用
(2)sa_mask
我們?cè)谏厦嬷v了sigaction結(jié)構(gòu)體的函數(shù)指針,接下來(lái)給大家了解sa_mask
sigaction中的sa_mask字段代表什么呢?
這個(gè)字段是一個(gè)sigset_t 類型的字段。它代表著屏蔽的信號(hào)。也就是會(huì)將block表給設(shè)置
如果在調(diào)用信號(hào)處理函數(shù)時(shí),除了當(dāng)前信號(hào)被自動(dòng)屏蔽之外,還希望自動(dòng)屏蔽另外一些信號(hào),則用sa_mask字段說(shuō)明這些需要額外屏蔽的信號(hào),當(dāng)信號(hào)處理函數(shù)返回時(shí)自動(dòng)恢復(fù)原來(lái)的信號(hào)屏蔽字。
運(yùn)行結(jié)果如下:
二、可重入函數(shù)
main 函數(shù)調(diào)用 insert 函數(shù)向一個(gè)鏈表 head 中插入節(jié)點(diǎn) node1,插入操作分為兩步,剛做完第一步時(shí),因?yàn)橛布袛嗍惯M(jìn)程切換到內(nèi)核,再次回用戶態(tài)之前檢查到有信號(hào)待處理,于是切換到 sighandler 函數(shù),sighandler 也調(diào)用 insert 函數(shù)向同一個(gè)鏈表 head 中插入節(jié)點(diǎn) node2,插入操作的兩步都做完之后從 sighandler 返回內(nèi)核態(tài),再次回到用戶態(tài)就從 main 函數(shù)調(diào)用的 insert 函數(shù)中繼續(xù)往下執(zhí)行,之前做第一步后被打斷,現(xiàn)在繼續(xù)做完第二步。結(jié)果是 main 函數(shù)和 sighandler 先后向鏈表中插入兩個(gè)節(jié)點(diǎn),而最后只有一個(gè)節(jié)點(diǎn)真正插入鏈表中了。
這里insert函數(shù)被main和handler執(zhí)行流重復(fù)進(jìn)入,這樣會(huì)導(dǎo)致節(jié)點(diǎn)丟失,內(nèi)存泄漏。
概念:如果一個(gè)函數(shù),被重復(fù)進(jìn)入的情況下,出錯(cuò)了或者可能出錯(cuò),這樣叫做不可重入函數(shù),否則叫做可重入函數(shù)。
目前我們所學(xué)的大部分函數(shù)都是不可重入的
三、volatile
volatile 是屬于 C 語(yǔ)言中的關(guān)鍵字,也叫做易變關(guān)鍵字(被它修飾后的變量就是在告訴編譯器這個(gè)變量是易變的),它的作用是保持內(nèi)存的可見(jiàn)性。
這里給一個(gè)全局標(biāo)志位 flag,利用 flag 讓程序死循環(huán)執(zhí)行,此時(shí)就可以通過(guò)信號(hào)捕捉,在捕捉方法中改變 flag 的值,然后結(jié)束死循環(huán)。
#include <iostream> #include <unistd.h> #include <signal.h> #include <cstring> using namespace std; int flag=0; void handler(int signo) { cout<<"接收到2號(hào)信號(hào)"<<signo<<endl; flag=1; } int main() { signal(2,handler); while(!flag) { cout<<"haha"<<endl; sleep(1); } cout<<"process quit!"<<endl; return 0; }
運(yùn)行結(jié)果如下:
上面可以看到 main 函數(shù)中沒(méi)有更改 flag 的任何操作,那么可能會(huì)被優(yōu)化,所以 flag 一變化不會(huì)立馬被檢測(cè)到。
這里我們可以看到默認(rèn) g++(gcc 也一樣) 并沒(méi)有優(yōu)化這段代碼,所以 flag 一變化立馬就被檢測(cè)到。其實(shí),gcc 和 g++ 中有很多優(yōu)化級(jí)別,man gcc 文檔篩選后就可以看到 gcc 有 -O0/1/2/3 等優(yōu)化級(jí)別,gcc -O0 表示不會(huì)優(yōu)化代碼。
經(jīng)過(guò)驗(yàn)證(注意這里不同平臺(tái)結(jié)果可能不一樣):
- gcc 在 -O0 時(shí)不會(huì)作優(yōu)化處理,此時(shí)同上默認(rèn),進(jìn)程一收到信號(hào),進(jìn)程就終止了。
- gcc 在 -O1/2/3 時(shí)會(huì)作優(yōu)化處理,此時(shí)發(fā)現(xiàn) flag 已經(jīng)置為 1 了,但是進(jìn)程并沒(méi)有終止。
這個(gè)優(yōu)化是在是在編譯時(shí)就處理好了。
因?yàn)檫@里主執(zhí)行流下并沒(méi)有對(duì) flag 的修改操作,所以 gcc -O1 在優(yōu)化的時(shí)候可能會(huì)將局部變量 flag 優(yōu)化成寄存器變量,定義 flag 時(shí)一定會(huì)在內(nèi)存開辟空間。此時(shí),gcc 在編譯時(shí)發(fā)現(xiàn)以 flag 作為死循環(huán)條件,且主執(zhí)行流中沒(méi)有對(duì) flag 修改的操作,所以就把 flag 優(yōu)化成寄存器變量。
一般默認(rèn)情況沒(méi)有優(yōu)化級(jí)時(shí),gcc -O0 while 循環(huán)檢測(cè)的是內(nèi)存中的變量,而在優(yōu)化的情況下 gcc -O1 會(huì)將內(nèi)存中的變量?jī)?yōu)化到寄存器中,然后 while 循環(huán)檢測(cè)時(shí)只檢測(cè)寄存器中 flag 的值,當(dāng)執(zhí)行信號(hào)捕捉代碼時(shí),flag = 1 又只會(huì)對(duì)內(nèi)存進(jìn)行修改,而此時(shí) wihle 循環(huán)只檢測(cè)寄存器中的 flag = 0。所以,短暫出現(xiàn)了內(nèi)存數(shù)據(jù)和寄存器數(shù)據(jù)不一致的現(xiàn)象,然后就出現(xiàn)了好像把 flag 改了,但 while 循環(huán)又不退出的現(xiàn)象。因?yàn)橐獪p少代碼體積和提高效率,所以在優(yōu)化時(shí)需要優(yōu)化成寄存器變量。
所以在 gcc -O1(gcc -O3) 優(yōu)化時(shí)還需要加上 volatile,此時(shí)要告訴編譯器:不要把 flag 優(yōu)化到寄存器上,每次檢測(cè)必須把 flag 從內(nèi)存讀到寄存器中,然后再進(jìn)行檢測(cè),不要因?yàn)榧拇嫫鞫蓴_ while 循環(huán)的判斷。這就叫做保持內(nèi)存的可見(jiàn)性。
volatile 作用:保持內(nèi)存的可見(jiàn)性,告知編譯器:被該關(guān)鍵字修飾的變量不允許被優(yōu)化,對(duì)該變量的任何操作都必須在真實(shí)的內(nèi)存中進(jìn)行操作。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Linux系統(tǒng)中Tomcat環(huán)境配置方式
這篇文章主要介紹了Linux系統(tǒng)中Tomcat環(huán)境配置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04Linux使用sosreport實(shí)現(xiàn)生成系統(tǒng)報(bào)告
sosreport?命令是許多?Linux?發(fā)行版上可用的工具,特別是基于?Red?hat?的系統(tǒng),下面我們來(lái)看看如何使用sosreport實(shí)現(xiàn)生成系統(tǒng)報(bào)告吧2025-02-02Linux服務(wù)器如何設(shè)置啟動(dòng)自動(dòng)登錄
在遠(yuǎn)程操作服務(wù)器時(shí),配置自動(dòng)登錄可以帶來(lái)便利,首先修改/etc/passwd文件,將root用戶的密碼字段清空,接著,修改/etc/gdm/custom.conf文件,在[daemon]部分添加或修改自動(dòng)登錄的用戶信息,完成后重啟服務(wù)器,即可實(shí)現(xiàn)用戶自動(dòng)登錄2024-09-09Linux下用SSH退出符切換SSH會(huì)話的實(shí)現(xiàn)方法
這篇文章主要介紹了Linux下用SSH退出符切換SSH會(huì)話的實(shí)現(xiàn)方法,需要的朋友可以參考下2015-07-07