一、信號(hào)的保存——信號(hào)的三個(gè)表——block表,pending表,handler表
我們知道,操作系統(tǒng)是進(jìn)程的管理者,只有操作系統(tǒng)才有資格向進(jìn)程發(fā)信號(hào),具體點(diǎn),是給進(jìn)程的PCB發(fā)信號(hào)。
更具體點(diǎn),就是將進(jìn)程的task_struct中的signal整形的某一個(gè)比特位由0置1?。?!
那么該信號(hào)如何被保存下來(lái)呢?
實(shí)際執(zhí)行信號(hào)的處理動(dòng)作稱為信號(hào)遞達(dá)(Delivery)
信號(hào)從產(chǎn)生到遞達(dá)之間的狀態(tài),稱為信號(hào)未決(Pending)。
進(jìn)程可以選擇阻塞 (Block )某個(gè)信號(hào)。
被阻塞的信號(hào)產(chǎn)生時(shí)將保持在未決狀態(tài),直到進(jìn)程解除對(duì)此信號(hào)的阻塞,才執(zhí)行遞達(dá)的動(dòng)作.
注意,阻塞和忽略是不同的,只要信號(hào)被阻塞就不會(huì)遞達(dá),而忽略是在遞達(dá)之后可選的一種處理動(dòng)作。
也就是說(shuō),block表記錄的是對(duì)某一個(gè)信號(hào)是否阻塞,如果對(duì)2號(hào)信號(hào)阻塞,那么block表中2號(hào)下標(biāo)的位圖就由0置1。
pending表記錄的是收到了哪一個(gè)信號(hào),且還未處理的信號(hào),就保存在pending表中。
handler表保存的是處理對(duì)應(yīng)信號(hào)的方法,handler表的本質(zhì)就是一個(gè)函數(shù)指針數(shù)組。
函數(shù)指針類型是:
typedef void (*handler_t)(int);
這些函數(shù)方法,如果用戶不提供,就使用默認(rèn)的,如果用戶提供,就使用用戶的。
handler表的定義如下:
handler_t handler[31];
需要注意的:
一個(gè)信號(hào)如果被阻塞了,只是意味著該信號(hào)將暫時(shí)保存在pending表中,沒(méi)有被遞達(dá),直到該信號(hào)被解除阻塞,才會(huì)將該信號(hào)進(jìn)行遞達(dá)處理。
注意:阻塞和忽略是不同的,只要信號(hào)被阻塞就不會(huì)遞達(dá),而忽略是在遞達(dá)之后可選的一種處理動(dòng)作。
總結(jié):block表:阻塞表,pending表:保存表,handler表:方法表。
sigset_t
上圖的三張表都是內(nèi)核的數(shù)據(jù)結(jié)構(gòu),是操作系統(tǒng)管理的,用戶層無(wú)法直接訪問(wèn),只能由操作系統(tǒng)提供的結(jié)構(gòu)進(jìn)行修改。
從上圖來(lái)看,每個(gè)信號(hào)只有一個(gè)bit的未決標(biāo)志,非0即1,不記錄該信號(hào)產(chǎn)生了多少次,阻塞標(biāo)志也是這樣表示的。
因此,未決和阻塞標(biāo)志可以用相同的數(shù)據(jù)類型sigset_t來(lái)存儲(chǔ),sigset_t稱為信號(hào)集,這個(gè)類型可以表示每個(gè)信號(hào)的“有效”或“無(wú)效”狀態(tài),在阻塞信號(hào)集中“有效”和“無(wú)效”的含義是該信號(hào)是否被阻塞,而在未決信號(hào)集中“有效”和“無(wú)效”的含義是該信號(hào)是否處于未決狀態(tài)。
阻塞信號(hào)集也叫做當(dāng)前進(jìn)程的信號(hào)屏蔽字(Signal Mask),這里的“屏蔽”應(yīng)該理解為阻塞而不是忽略。
總結(jié):sigset_t是一個(gè)信號(hào)集,也就是位圖。
信號(hào)集操作函數(shù)——用戶層
#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);
這些函數(shù)都是對(duì)用戶層的位圖進(jìn)行操作的。
函數(shù)sigemptyset初始化set所指向的信號(hào)集,使其中所有信號(hào)的對(duì)應(yīng)bit清零,表示該信號(hào)集不包含 任何有效信號(hào)。
函數(shù)sigfillset初始化set所指向的信號(hào)集,使其中所有信號(hào)的對(duì)應(yīng)bit置位,表示 該信號(hào)集的有效信號(hào)包括系統(tǒng)支持的所有信號(hào)。
注意,在使用sigset_ t類型的變量之前,一定要調(diào) 用sigemptyset或sigfillset做初始化,使信號(hào)集處于確定的狀態(tài)。
初始化sigset_t變量之后就可以在調(diào)用sigaddset和sigdelset在該信號(hào)集中添加或刪除某種有效信號(hào)。
這四個(gè)函數(shù)都是成功返回0,出錯(cuò)返回-1。sigismember是一個(gè)布爾函數(shù),用于判斷一個(gè)信號(hào)集的有效信號(hào)中是否包含某種信號(hào),若包含則返回1,不包含則返回0,出錯(cuò)返回-1。
sigprocmask和sigpending——內(nèi)核層
sigprocmask
該函數(shù)的意思就是:將set位圖設(shè)置到內(nèi)核的block表里面
如果how參數(shù)選擇
- 1.SIG_BLOCK:將原來(lái)的block表保存到oset中,并將新的set位圖的新的屏蔽信號(hào)添加到block表中,也就是說(shuō)如果原來(lái)的block表只有2號(hào)位置的比特位為1,即只有2號(hào)信號(hào)被屏蔽,且新的set位圖中的1號(hào)比特位為1,那么調(diào)用完該函數(shù)后,新的內(nèi)核block表中的1號(hào)和2號(hào)的比特位都為1,也就是增加了1號(hào)信號(hào)屏蔽字。
-
- mask = mask|set
- 2.SIG_UNBLOCK:與第一個(gè)的操作相反
-
- mask = mask&~set
- 3.SIG_SETMASK:直接將set位圖覆蓋到內(nèi)核的block表即可,簡(jiǎn)單粗暴。
-
- mask = set
sigpending(sigset_t * set)
獲取內(nèi)核數(shù)據(jù)結(jié)構(gòu)中的pending表并保存到set位圖中。
以下代碼就是對(duì)上面兩個(gè)系統(tǒng)調(diào)用的應(yīng)用
void PrintPending(sigset_t& pending)
{
for(int signo = 31;signo>=1;signo--)
{
if(sigismember(&pending,signo)) //判斷pending表中的比特位
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << "\n\n";
}
int main()
{
sigset_t bset,oset;
sigemptyset(&bset); //設(shè)置一個(gè)位圖,清0
sigemptyset(&oset); //設(shè)置一個(gè)位圖,清0
//添加信號(hào)屏蔽字
sigaddset(&bset,2);
// for(int signo = 1;signo<=31;signo++)
// {
// sigaddset(&bset,signo);
// }
//到這里其實(shí)沒(méi)有修改內(nèi)核中的block表,只是創(chuàng)建一個(gè)位圖而已
sigprocmask(SIG_SETMASK,&bset,&oset); //到這里才是修改內(nèi)核block表
//mask = set
//打印pending表,如果收到2號(hào)信號(hào),就不會(huì)被遞達(dá),而是一直在pending表里存著。
sigset_t pending;
while(true)
{
sigpending(&pending);
PrintPending(pending);
sleep(1);
}
return 0;
}
首先創(chuàng)建一個(gè)位圖,將位圖的某些位置設(shè)置成1。
這時(shí)候并沒(méi)有修改內(nèi)核中的block表。
然后調(diào)用了sigprocmask系統(tǒng)調(diào)用后,才真正地修改內(nèi)核的block表。
再將pending表打印出來(lái),如果設(shè)置的某個(gè)信號(hào)被屏蔽后,意味著在block表中的該位置的比特位為1,一旦進(jìn)程收到該信號(hào),就不會(huì)被遞達(dá), 就會(huì)被pending,所以pending表中的比特位就被設(shè)置成了1,直到未來(lái)某個(gè)時(shí)候解除屏蔽后才遞達(dá)該信號(hào)。
二、信號(hào)的捕捉
重談進(jìn)程地址空間(第三次)
在進(jìn)程的地址空間中,有1GB的內(nèi)存是專門(mén)留給操作系統(tǒng)的
在啟動(dòng)電腦時(shí),是操作系統(tǒng)的數(shù)據(jù)和代碼先被放到物理內(nèi)存的較底部的位置先運(yùn)行起來(lái)。
然后有關(guān)操作系統(tǒng)的進(jìn)程也被操作系統(tǒng)跑起來(lái)。
無(wú)論是哪些進(jìn)程,只要是一個(gè)進(jìn)程,該進(jìn)程的虛擬地址空間中的3~4GB這個(gè)空間區(qū)域,一定是屬于操作系統(tǒng)所有的?。?!
而對(duì)應(yīng)的,由于每個(gè)進(jìn)程的1GB空間都屬于操作系統(tǒng),所以,任何進(jìn)程,看到的操作系統(tǒng)的數(shù)據(jù)和代碼都是一樣的!??!
而當(dāng)進(jìn)程調(diào)用系統(tǒng)調(diào)用時(shí),這個(gè)過(guò)程就顯得非常簡(jiǎn)單。
因?yàn)檫M(jìn)程的虛擬地址空間中的1GB空間可以直接通過(guò)內(nèi)核級(jí)別頁(yè)表,映射到物理空間中的固定位置。
所以!進(jìn)程想要調(diào)用系統(tǒng)調(diào)用,直接去自己的進(jìn)程地址空間中的內(nèi)核空間中執(zhí)行對(duì)應(yīng)的代碼即可!??!
這也側(cè)面驗(yàn)證了:
內(nèi)核級(jí)頁(yè)表只有一份,而用戶級(jí)頁(yè)表有多份的結(jié)論。
因?yàn)椴僮飨到y(tǒng)的代碼在進(jìn)程地址空間中的內(nèi)核空間是固定的,所以只需要一份頁(yè)表直接映射到固定位置就能訪問(wèn)操作系統(tǒng)的代碼和數(shù)據(jù)。
所以:(不考慮權(quán)限問(wèn)題的話)
- 從進(jìn)程視角來(lái)看:調(diào)用系統(tǒng)調(diào)用的方法就是直接在我自己的地址空間中進(jìn)行執(zhí)行的。
- 從操作系統(tǒng)來(lái)看:任何時(shí)刻都有進(jìn)程執(zhí)行,只要進(jìn)程想執(zhí)行操作系統(tǒng)的代碼,就可以隨時(shí)執(zhí)行。
用戶態(tài)和內(nèi)核態(tài)
前面在講到進(jìn)程要調(diào)用系統(tǒng)調(diào)用時(shí),沒(méi)有考慮權(quán)限。
但實(shí)際上要想執(zhí)行操作系統(tǒng)的代碼和數(shù)據(jù)是要有權(quán)限的。
而這個(gè)所謂的權(quán)限就是內(nèi)核態(tài)。
- 內(nèi)核態(tài):允許進(jìn)程訪問(wèn)操作系統(tǒng)的代碼和數(shù)據(jù)
- 用戶態(tài):只能訪問(wèn)用戶自己的代碼和數(shù)據(jù)
在CPU內(nèi)部,其中有兩個(gè)寄存器,一個(gè)寄存器叫CR3寄存器,保留的是當(dāng)前進(jìn)程用戶級(jí)頁(yè)表的物理地址。
還有一個(gè)寄存器叫做ecs寄存器,該寄存器的后兩位比特位就是記錄當(dāng)前進(jìn)程屬于用戶態(tài)還是屬于內(nèi)核態(tài)。
00表示用戶態(tài),11表示內(nèi)核態(tài)。
并且要想修改當(dāng)前進(jìn)程從用戶態(tài)轉(zhuǎn)變成內(nèi)核態(tài),就需要調(diào)用系統(tǒng)調(diào)用,int 80;
80就是系統(tǒng)調(diào)用的編號(hào)。
而在用戶態(tài)到內(nèi)核態(tài)之間的切換,如下:
上面的圖比較繁瑣,這樣非常好理解:
并且在從用戶態(tài)進(jìn)入內(nèi)核態(tài)時(shí),一定不僅只有調(diào)用系統(tǒng)調(diào)用才會(huì)由用戶態(tài)進(jìn)入內(nèi)核態(tài)。
當(dāng)操作系統(tǒng)要對(duì)進(jìn)程進(jìn)行調(diào)度時(shí),就要將進(jìn)程的PCB加載到運(yùn)行隊(duì)列,等待隊(duì)列等待這些管理結(jié)構(gòu)中,然后將進(jìn)程的上下文加載到CPU和操作系統(tǒng)中,這個(gè)加載的過(guò)程一定是在內(nèi)核態(tài)完成的!在加載完成后,操作系統(tǒng)轉(zhuǎn)而就會(huì)執(zhí)行進(jìn)程自己的代碼和數(shù)據(jù),而執(zhí)行進(jìn)程的代碼和數(shù)據(jù)的過(guò)程一定是在用戶態(tài)執(zhí)行的?。?!
這就有了進(jìn)程可以有無(wú)數(shù)次機(jī)會(huì)從用戶態(tài)進(jìn)入內(nèi)核態(tài),再由內(nèi)核態(tài)進(jìn)入用戶態(tài)的過(guò)程?。。?/p>
所以這也驗(yàn)證了一個(gè)結(jié)論:
信號(hào)不會(huì)被進(jìn)程立即處理,而是在合適的時(shí)間處理,這個(gè)合適的時(shí)間,其實(shí)就是在內(nèi)核態(tài)中信號(hào)的檢測(cè)階段處理。
sigaction
sigaction函數(shù)與signal函數(shù)有一樣的功能。
不同的是sigaction的功能更多一些。
sigaction也是一個(gè)結(jié)構(gòu)體,該結(jié)構(gòu)體的名字與該函數(shù)名相同。
結(jié)構(gòu)體中的主要兩個(gè)成員是:
void (*sa_handler)(int);
sigset_t sa_mask;
一個(gè)是捕捉信號(hào)是對(duì)應(yīng)的處理方法,一個(gè)是block表。
具體功能就是signum信號(hào)對(duì)應(yīng)的自定義捕捉方法存入act函數(shù)指針指向的結(jié)構(gòu)體的sa_handler方法中,oact存的就是舊的捕捉方法。
重點(diǎn)不在這里,重點(diǎn)在于從發(fā)送信號(hào),到保存信號(hào),捕捉信號(hào)的過(guò)程中,block表,pending表,handler表是如何協(xié)同工作的:
在我們向進(jìn)程發(fā)送信號(hào)時(shí),假如發(fā)送二號(hào)信號(hào),此時(shí)進(jìn)程收到信號(hào)后,首先將pending表中的2號(hào)位置由0置1,意味著先將2號(hào)信號(hào)保存起來(lái),進(jìn)程會(huì)在合適的時(shí)間處理。當(dāng)這個(gè)2號(hào)信號(hào)被遞達(dá),也就是被處理時(shí),在調(diào)用handler方法中的2號(hào)位置對(duì)應(yīng)的處理方法前,將pending表中的2號(hào)位置就由1置空0,且會(huì)將block表中的2號(hào)位置由0置1,這就意味這當(dāng)進(jìn)程在處理2號(hào)信號(hào)時(shí),再發(fā)送2號(hào)信號(hào)過(guò)來(lái)時(shí),pending表中的2號(hào)位置一定是由0置1的,因?yàn)橐欢ㄊ堑壬弦粋€(gè)2號(hào)信號(hào)處理完成后,再處理這個(gè)2號(hào)信號(hào)。
在將上面的2號(hào)信號(hào)處理完成后,調(diào)用處理方法返回前,會(huì)將block表中的2號(hào)位置再由1置成0,此時(shí)就完成了整個(gè)信號(hào)的捕捉處理過(guò)程。
具體如下:
在進(jìn)程處理2號(hào)信號(hào)期間,當(dāng)我再次發(fā)送2號(hào)信號(hào)時(shí),就能看到pending表中的2號(hào)位置由0置1.
void PrintPending()
{
sigset_t set;
sigpending(&set);
for (int signo = 1; signo <= 31; signo++)
{
if (sigismember(&set, signo))
cout << "1";
else
cout << "0";
}
cout << "\n";
}
void handler(int signo)
{
cout << "catch a signal, signal number : " << signo << endl;
while (true)
{
PrintPending();
sleep(1);
}
}
int main()
{
struct sigaction act, oact;
memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(oact));
// sigemptyset(&act.sa_mask);
// sigaddset(&act.sa_mask, 1);
// sigaddset(&act.sa_mask, 3);
// sigaddset(&act.sa_mask, 4);
act.sa_handler = handler; // SIG_IGN SIG_DFL
sigaction(2, &act, &oact);
while (true)
{
cout << "I am a process: " << getpid() << endl;
sleep(1);
}
return 0;
}
可重入函數(shù)
假設(shè)main函數(shù)內(nèi)部再調(diào)用insert函數(shù),進(jìn)行鏈表的頭插操作。
當(dāng)執(zhí)行完newnode->next = head;
后,本來(lái)要執(zhí)行下一條代碼時(shí),此時(shí)收到某個(gè)信號(hào),該信號(hào)有一個(gè)自定義處理函數(shù),該函數(shù)的內(nèi)部又調(diào)用了insert函數(shù),此時(shí)再次進(jìn)入了insert函數(shù)內(nèi)部,再次執(zhí)行了newnode->next = head;
情況如上圖所示,然后再執(zhí)行head = newnode
完成頭插工作。
在處理完該信號(hào)后,會(huì)回到main函數(shù)調(diào)用insert函數(shù)執(zhí)行完上一條代碼的地方,將要執(zhí)行下一條代碼,執(zhí)行后,最終結(jié)果如上圖。node2就會(huì)找不到了,也就意味著內(nèi)存泄露了。
如果一個(gè)函數(shù),在被重復(fù)進(jìn)入的情況下,出錯(cuò)了,或者可能會(huì)出錯(cuò),這樣的函數(shù)叫做不可重入函數(shù)。
否則,叫做可重入函數(shù)
顯然,上面的insert函數(shù)就是不可重入函數(shù)。
volatile
volatile 作用:保持內(nèi)存的可見(jiàn)性,告知編譯器,被該關(guān)鍵字修飾的變量,不允許被優(yōu)化,對(duì)該變量的任何操作,都必須在真實(shí)的內(nèi)存中進(jìn)行操作。
請(qǐng)看下面一段代碼:
int flag = 0;
void handler(int signo)
{
cout << "catch a signal: " << signo << endl;
flag = 1;
}
int main()
{
signal(2, handler);
// 在優(yōu)化條件下, flag變量可能被直接優(yōu)化到CPU內(nèi)的寄存器中
while(!flag); // flag 假, !flag 真
cout << "process quit normal" << endl;
return 0;
}
在進(jìn)程接收到2號(hào)信號(hào)時(shí),調(diào)用handler函數(shù),將flag的值修改成1,當(dāng)進(jìn)入循環(huán)時(shí),!flag邏輯表達(dá)式為假,退出循環(huán),這就是我們預(yù)期的結(jié)果。
但事實(shí)并非如此,因?yàn)榫幾g器對(duì)該變量進(jìn)行了優(yōu)化,將flag變量存在了寄存器中,flag = 1這條語(yǔ)句修改的是內(nèi)存中的flag,對(duì)寄存器中的flag并未修改。
并且!flag邏輯表達(dá)式在判斷的時(shí)候,是對(duì)寄存器的值進(jìn)行判斷的。
所以在寄存器中的flag一直為真,就不會(huì)退出循環(huán)。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-752976.html
在flag變量之前加上一個(gè)volatile關(guān)鍵字后,flag就不會(huì)被優(yōu)化到寄存器中,對(duì)flag變量的邏輯運(yùn)算就會(huì)在內(nèi)存執(zhí)行。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-752976.html
到了這里,關(guān)于【Linux】信號(hào)的保存和捕捉的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!