Linux進程信號的捕捉&&信號補充內(nèi)容方式
一、信號的捕捉
對于如何處理信號,前面也講了 signal 接口,如 signal(2, handler),所以對 2 號信號執(zhí)行 handler 捕捉動作,本質(zhì)是 OS 去 task_struct 通過 2 號信號作索引,找到內(nèi)核中 handler 函數(shù)指針數(shù)組中對應(yīng)的方法,然后把數(shù)組內(nèi)容改成你自己在用戶層傳入的 handler 函數(shù)指針方法。
這里我們要討論的是上面遺留下來的問題 —— 進程收到信號時,不是立即處理的,而是在合適的時候再處理,那合適的時候是什么時候呢 ?
這里先把結(jié)論寫出來
當進程從內(nèi)核態(tài)返回到用戶態(tài)的時候,進行信號的檢測和處理。
1、用戶態(tài)和內(nèi)核態(tài)

進程如果訪問的是用戶空間的代碼,此時的狀態(tài)就是用戶態(tài);如果訪問的是內(nèi)核空間,此時的狀態(tài)就是內(nèi)核態(tài)。
我們經(jīng)常需要通過系統(tǒng)調(diào)用訪問內(nèi)核,系統(tǒng)調(diào)用是 OS 提供的方法,執(zhí)行 OS 的方法就可能訪問 OS 中的代碼和數(shù)據(jù),普通用戶沒有這個權(quán)限。所以在調(diào)用系統(tǒng)接口時,系統(tǒng)會自動進行身份切換 user ? kernel
那 OS 是怎么知道現(xiàn)在的狀態(tài)是用戶態(tài)還是內(nèi)核態(tài)?
因為 CPU 中有一個狀態(tài)寄存器或者說權(quán)限相關(guān)的寄存器,它可以表示所處的狀態(tài)。每個用戶進程都有自己的用戶級頁表,OS 中也有且只有一份內(nèi)核級頁表。也就是說,多個進程可以通過權(quán)限提升來訪問同一張內(nèi)核級頁表,每個進程變成內(nèi)核態(tài)的時候訪問的就是同一份數(shù)據(jù)。所以,OS 區(qū)分是用戶態(tài)還是內(nèi)核態(tài),除了寄存器保存了權(quán)限相關(guān)的數(shù)據(jù)之外,還要看進程使用的是哪個種類的頁表。
在什么情況下會觸發(fā)從用戶態(tài)到內(nèi)核態(tài)呢?
這里有很多種方式:比如,自己寫的一個 cin 程序一運行就卡在那里,你按了 abc,然后程序就會拿到 abc,本質(zhì)就是鍵盤在觸發(fā)的時候被 OS 先識別到,然后放在 OS 的緩沖區(qū)中,而你的程序在從 OS 的緩沖區(qū)中讀取。其中 OS 是通過一種中斷技術(shù),這個中斷指的是硬件方面的中斷,如 8259 中斷器,它是一種芯片,用于管理計算機系統(tǒng)中的中斷請求,通常和 CPU 一起使用。
再舉個例子,如果了解過匯編,可能聽說過 int 80,它就是傳說中系統(tǒng)調(diào)用接口的底層原理,系統(tǒng)調(diào)用的底層原理就是通過指令 int 80 來中斷陷入內(nèi)核。還有一種比較好理解的,就是在調(diào)用系統(tǒng)接口后就陷入內(nèi)核,然后就可以執(zhí)行內(nèi)核代碼。然后當從內(nèi)核態(tài)返回用戶態(tài)時就更簡單了,當我們調(diào)完系統(tǒng)接口就返到用戶態(tài)了??傊@里只需要知道從用戶態(tài)到內(nèi)核態(tài)是有很多種方式的就行。

用戶態(tài)和內(nèi)核態(tài)的權(quán)限級別不同,那么自然能看到的資源是不一樣的。內(nèi)核態(tài)的權(quán)限級別一定更高,但它并不代表內(nèi)核態(tài)能直接訪問用戶態(tài)。前面說了信號捕捉的時間點是內(nèi)核態(tài) ? 用戶態(tài)的時候,信號被處理叫做信號遞達,遞達有忽略、默認、自定義,自定義動作就叫做捕捉動作,只要理解了捕捉,那么忽略和默認就簡單了。上圖就是整個信號的捕捉過程:在 CPU 執(zhí)行我們的代碼時,一定會調(diào)用系統(tǒng)調(diào)用。
系統(tǒng)調(diào)用是函數(shù),是 OS 提供的,也有代碼,需要被執(zhí)行,那么應(yīng)該以 “什么態(tài)” 執(zhí)行呢?實際上用戶態(tài)中進程調(diào)用系統(tǒng)調(diào)用時必須得陷入內(nèi)核以用戶態(tài)身份執(zhí)行,執(zhí)行完畢后又返回用戶態(tài),繼續(xù)執(zhí)行用戶態(tài)中的代碼,那么問題就是可以直接以內(nèi)核態(tài)的身份去執(zhí)行用戶態(tài)中的代碼嗎?
從內(nèi)核態(tài)返回到用戶態(tài)之前,OS 會做一系列的檢測捕捉工作,它會檢測當前進程是否有信號需要處理,如果沒有就會返回系統(tǒng)調(diào)用,如果有,那就先處理(具體它會遍歷識別位圖: 假如信號 pending 了,且沒有被 block,那就會執(zhí)行 handler 方法,比如說終止進程,那就會釋放這個進程,如果是暫停,那就不用返回系統(tǒng)調(diào)用,然后再把進程 pcb 放在暫停隊列中,如果是忽略那就把 pending 中對應(yīng)的比特位由 1 變?yōu)?0,然后返回系統(tǒng)調(diào)用)。所以,可以看到比較難處理的是自定義捕捉,當 3 號信號捕捉時且收到了 pending,沒有被 block,那么就會執(zhí)行用戶空間中的捕捉方法。換而言之,我們因為系統(tǒng)調(diào)用而陷入內(nèi)核,執(zhí)行系統(tǒng)方法,執(zhí)行完方法后做信號檢測,檢測到信號是自定義捕捉,那么就會執(zhí)行自定義捕捉的方法。此時,應(yīng)該以 “什么態(tài)” 執(zhí)行信號捕捉方法?
理論來說,內(nèi)核態(tài)是絕對可以的,因為內(nèi)核態(tài)的權(quán)限比用戶態(tài)的權(quán)限高,但實際并不能以內(nèi)核態(tài)的身份去執(zhí)行用戶態(tài)的代碼,因為 OS 不相信任何人寫的任何代碼,這樣設(shè)計就很有可能讓惡意用戶利用導致系統(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)的代碼,必須進行狀態(tài)或者說權(quán)限切換。所以,信號捕捉的完整流程就是在用戶區(qū)中因為中斷、異?;蛳到y(tǒng)調(diào)用,接著切換權(quán)限陷入內(nèi)核執(zhí)行系統(tǒng)方法,然后再返回發(fā)現(xiàn)有信號需要被捕捉執(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)用,因為曾經(jīng)執(zhí)行捕捉方法時是由 OS 進入的,所以必須得利用系統(tǒng)接口再次陷入內(nèi)核,最后由內(nèi)核調(diào)用系統(tǒng)接口返回用戶區(qū)。
2、內(nèi)核如何實現(xiàn)信號的捕捉


上面的圖和文字都說的太復(fù)雜了,這里我們簡化一下,宏觀來看信號的捕捉過程就是狀態(tài)權(quán)限切換的過程,這里的藍點表示信號捕捉過程中狀態(tài)權(quán)限切換的次數(shù)。其中完整流程就是:
- 調(diào)用系統(tǒng)調(diào)用,陷入內(nèi)核。
- 執(zhí)行完系統(tǒng)任務(wù)。
- 進行信號檢測。
- 執(zhí)行捕捉代碼,調(diào)用 sigturm 再次陷入內(nèi)核。
- 調(diào)用 sys_sigreturn,返回到用戶區(qū)中系統(tǒng)調(diào)用點。
如果信號的處理動作是用戶自定義函數(shù), 在信號遞達時就調(diào)用這個函數(shù) , 這稱為捕捉信號。由于信號處理函數(shù)的代碼是在用戶空間的,處理過程比較復(fù)雜。
舉例如下: 用戶程序注冊了 SIGQUIT 信號的處理函數(shù) sighandler 。當前正在執(zhí)行 main 函數(shù), 這時發(fā)生中斷或異常切換到內(nèi)核態(tài)。在中斷處理完畢后要返回用戶態(tài)的 main 函數(shù)之前檢查到有信號 SIGQUIT 遞達。
內(nèi)核決定返回用戶態(tài)后不是恢復(fù) main 函數(shù)的上下文繼續(xù)執(zhí)行,而是執(zhí)行 sighandler 函數(shù), sighandler 和 main 函數(shù)使用不同的堆棧空間, 它們之間不存在調(diào)用和被調(diào)用的關(guān)系, 是兩個獨立的控制流程。
sighandler 函數(shù)返回后自動執(zhí)行特殊的系統(tǒng)調(diào)用 sigreturn 再次進入內(nèi)核態(tài)。 如果沒有新的信號要遞達,這次再返回用戶態(tài)就是恢復(fù) main 函數(shù)的上下文繼續(xù)執(zhí)行了。
3、sigaction
對于修改 handler 表的操作接口,前面已經(jīng)了解過 signal 了,下面再講講 sigaction,sigaction 相比 signal 有更多的選項,不過只需要知道它怎么用就行了,因為它兼顧了實時信號。
(1)接口介紹
man sigaction int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

- signum:指定捕捉信號的編號。
- act:輸入性參數(shù),如何處理信號,它是一個結(jié)構(gòu)體指針,第 2 與第 5 個字段是實時信號相關(guān)的,可以不管它。
- oldact:輸出型參數(shù),如果需要可以把老的信號捕捉方式保存,不需要則 NULL。
成功返回0,失敗返回-1

這是這個結(jié)構(gòu)體的內(nèi)容

在這個結(jié)構(gòu)體中,我們只關(guān)心這兩個字段。其他的字段與實時信號有關(guān)

如下樣例所示可以簡單的先用起來這個函數(shù)
#include <iostream>
#include <signal.h>
#include <cstring>
#include <unistd.h>
void handler(int signo)
{
std::cout<<"捕捉到2號信號"<<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;
}運行結(jié)果如下

那我們想知道pending位圖接收到信號,會在位圖里把0->1,那什么時候把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號信號"<<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)在可以驗證之前的結(jié)論,當某個信號的處理函數(shù)被調(diào)用時,內(nèi)核自動將當前信號加入進程信號的屏蔽字中,當信號處理函數(shù)返回自動恢復(fù)原來的信號屏蔽字
我們寫一份代碼驗證一下
我們先發(fā)送2號信號的時候,被捕捉,執(zhí)行自定義函數(shù),我們這里寫了一個循環(huán),是為了證明當某個信號在處理的時候,內(nèi)核將該信號先加入到block中不執(zhí)行該信號的動作。循環(huán)5秒后解除屏蔽會自動執(zhí)行。

我們來看下運行結(jié)果

即操作系統(tǒng)不允許對某個信號重復(fù)捕捉,最多只能捕捉一層
信號被處理的時候,對應(yīng)的信號也會被添加到block表中,防止信號捕捉被嵌套調(diào)用
(2)sa_mask
我們在上面講了sigaction結(jié)構(gòu)體的函數(shù)指針,接下來給大家了解sa_mask

sigaction中的sa_mask字段代表什么呢?
這個字段是一個sigset_t 類型的字段。它代表著屏蔽的信號。也就是會將block表給設(shè)置
如果在調(diào)用信號處理函數(shù)時,除了當前信號被自動屏蔽之外,還希望自動屏蔽另外一些信號,則用sa_mask字段說明這些需要額外屏蔽的信號,當信號處理函數(shù)返回時自動恢復(fù)原來的信號屏蔽字。

運行結(jié)果如下:

二、可重入函數(shù)
main 函數(shù)調(diào)用 insert 函數(shù)向一個鏈表 head 中插入節(jié)點 node1,插入操作分為兩步,剛做完第一步時,因為硬件中斷使進程切換到內(nèi)核,再次回用戶態(tài)之前檢查到有信號待處理,于是切換到 sighandler 函數(shù),sighandler 也調(diào)用 insert 函數(shù)向同一個鏈表 head 中插入節(jié)點 node2,插入操作的兩步都做完之后從 sighandler 返回內(nèi)核態(tài),再次回到用戶態(tài)就從 main 函數(shù)調(diào)用的 insert 函數(shù)中繼續(xù)往下執(zhí)行,之前做第一步后被打斷,現(xiàn)在繼續(xù)做完第二步。結(jié)果是 main 函數(shù)和 sighandler 先后向鏈表中插入兩個節(jié)點,而最后只有一個節(jié)點真正插入鏈表中了。

這里insert函數(shù)被main和handler執(zhí)行流重復(fù)進入,這樣會導致節(jié)點丟失,內(nèi)存泄漏。
概念:如果一個函數(shù),被重復(fù)進入的情況下,出錯了或者可能出錯,這樣叫做不可重入函數(shù),否則叫做可重入函數(shù)。
目前我們所學的大部分函數(shù)都是不可重入的
三、volatile
volatile 是屬于 C 語言中的關(guān)鍵字,也叫做易變關(guān)鍵字(被它修飾后的變量就是在告訴編譯器這個變量是易變的),它的作用是保持內(nèi)存的可見性。
這里給一個全局標志位 flag,利用 flag 讓程序死循環(huán)執(zhí)行,此時就可以通過信號捕捉,在捕捉方法中改變 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號信號"<<signo<<endl;
flag=1;
}
int main()
{
signal(2,handler);
while(!flag)
{
cout<<"haha"<<endl;
sleep(1);
}
cout<<"process quit!"<<endl;
return 0;
}運行結(jié)果如下:

上面可以看到 main 函數(shù)中沒有更改 flag 的任何操作,那么可能會被優(yōu)化,所以 flag 一變化不會立馬被檢測到。
這里我們可以看到默認 g++(gcc 也一樣) 并沒有優(yōu)化這段代碼,所以 flag 一變化立馬就被檢測到。其實,gcc 和 g++ 中有很多優(yōu)化級別,man gcc 文檔篩選后就可以看到 gcc 有 -O0/1/2/3 等優(yōu)化級別,gcc -O0 表示不會優(yōu)化代碼。
經(jīng)過驗證(注意這里不同平臺結(jié)果可能不一樣):

- gcc 在 -O0 時不會作優(yōu)化處理,此時同上默認,進程一收到信號,進程就終止了。
- gcc 在 -O1/2/3 時會作優(yōu)化處理,此時發(fā)現(xiàn) flag 已經(jīng)置為 1 了,但是進程并沒有終止。

這個優(yōu)化是在是在編譯時就處理好了。
因為這里主執(zhí)行流下并沒有對 flag 的修改操作,所以 gcc -O1 在優(yōu)化的時候可能會將局部變量 flag 優(yōu)化成寄存器變量,定義 flag 時一定會在內(nèi)存開辟空間。此時,gcc 在編譯時發(fā)現(xiàn)以 flag 作為死循環(huán)條件,且主執(zhí)行流中沒有對 flag 修改的操作,所以就把 flag 優(yōu)化成寄存器變量。
一般默認情況沒有優(yōu)化級時,gcc -O0 while 循環(huán)檢測的是內(nèi)存中的變量,而在優(yōu)化的情況下 gcc -O1 會將內(nèi)存中的變量優(yōu)化到寄存器中,然后 while 循環(huán)檢測時只檢測寄存器中 flag 的值,當執(zhí)行信號捕捉代碼時,flag = 1 又只會對內(nèi)存進行修改,而此時 wihle 循環(huán)只檢測寄存器中的 flag = 0。所以,短暫出現(xiàn)了內(nèi)存數(shù)據(jù)和寄存器數(shù)據(jù)不一致的現(xiàn)象,然后就出現(xiàn)了好像把 flag 改了,但 while 循環(huán)又不退出的現(xiàn)象。因為要減少代碼體積和提高效率,所以在優(yōu)化時需要優(yōu)化成寄存器變量。

所以在 gcc -O1(gcc -O3) 優(yōu)化時還需要加上 volatile,此時要告訴編譯器:不要把 flag 優(yōu)化到寄存器上,每次檢測必須把 flag 從內(nèi)存讀到寄存器中,然后再進行檢測,不要因為寄存器而干擾 while 循環(huán)的判斷。這就叫做保持內(nèi)存的可見性。

volatile 作用:保持內(nèi)存的可見性,告知編譯器:被該關(guān)鍵字修飾的變量不允許被優(yōu)化,對該變量的任何操作都必須在真實的內(nèi)存中進行操作。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Linux系統(tǒng)中Tomcat環(huán)境配置方式
這篇文章主要介紹了Linux系統(tǒng)中Tomcat環(huán)境配置方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-04-04
Linux使用sosreport實現(xiàn)生成系統(tǒng)報告
sosreport?命令是許多?Linux?發(fā)行版上可用的工具,特別是基于?Red?hat?的系統(tǒng),下面我們來看看如何使用sosreport實現(xiàn)生成系統(tǒng)報告吧2025-02-02
Linux下用SSH退出符切換SSH會話的實現(xiàn)方法
這篇文章主要介紹了Linux下用SSH退出符切換SSH會話的實現(xiàn)方法,需要的朋友可以參考下2015-07-07

