上一章主要講述了信號的產(chǎn)生:【linux】進(jìn)程信號——信號的產(chǎn)生
這篇文章主要講后面兩個過程。
一、阻塞信號
1.1 信號的相關(guān)概念
- 實(shí)際執(zhí)行信號的處理動作稱為信號遞達(dá)(Delivery)。
- 信號從產(chǎn)生到遞達(dá)之間的狀態(tài),稱為信號未決(Pending)。
因?yàn)樾盘?strong>不是被立即處理的,所以在信號產(chǎn)生之后,遞達(dá)之前的這個時間窗口稱作信號未決,也就是把信號暫時保存起來。
- 進(jìn)程可以選擇阻塞 (Block )某個信號。
- 被阻塞的信號產(chǎn)生時將保持在未決狀態(tài),直到進(jìn)程解除對此信號的阻塞,才執(zhí)行遞達(dá)的動作。
注意,阻塞和忽略是不同的,只要信號被阻塞就不會遞達(dá),而忽略是在遞達(dá)之后可選的一種處理動作。
而且沒有信號產(chǎn)生我們也可以選擇阻塞某個信號。
1.2 在內(nèi)核中的構(gòu)成
我們知道發(fā)送信號的本質(zhì):修改PCB中的信號位圖。 而阻塞和未決也是通過位圖的方式來保存信號。它們的位圖也存在于進(jìn)程的PCB內(nèi)。
位圖的第幾個比特位代表第幾個信號。
對于block,比特位的內(nèi)容代表是否阻塞信號。
對于pending,比特位的內(nèi)容代表是否收到信號。
對于handler,他是一個函數(shù)指針數(shù)組,代表處理動作。
信號產(chǎn)生時,內(nèi)核在進(jìn)程控制塊中設(shè)置該信號的未決標(biāo)志,直到信號遞達(dá)才清除該標(biāo)志。
而只要阻塞位圖對應(yīng)比特位為1,那么信號永遠(yuǎn)不能遞達(dá)。
如果一個信號想要遞達(dá),那么pending位圖對應(yīng)的比特位為1,block位圖對應(yīng)的比特位為0。
總結(jié):
1?? 因?yàn)閜ending和block是兩個位圖,所以不會互相影響,一個信號沒產(chǎn)生并不影響先被阻塞。
2?? 進(jìn)程能夠識別信號是因?yàn)椋瑑?nèi)核當(dāng)中有這三種結(jié)構(gòu),它們組合起來就能夠識別信號。
3?? 當(dāng)有多個信號同時來的時候,因?yàn)槲粓D只有一個比特位,所以只會處理一次,其他的信號都會被丟失。
二、捕捉信號概念
2.1 內(nèi)核態(tài)和用戶態(tài)
信號產(chǎn)生后不會立即進(jìn)行處理,而是在合適的時候進(jìn)行處理。那么什么時候是合適的時候呢?
從內(nèi)核態(tài)返回用戶態(tài)的時候進(jìn)行處理。
如果用戶態(tài)想要獲得操作系統(tǒng)自身資源(getpid……)或者硬件資源(write……)的時候必須通過系統(tǒng)調(diào)用接口完成訪問。
而我們無法以用戶態(tài)的身份調(diào)用系統(tǒng)調(diào)用,必須讓自己的狀態(tài)變成內(nèi)核態(tài)。
所以往往系統(tǒng)調(diào)用比較花費(fèi)時間,我們應(yīng)該避免頻繁調(diào)用系統(tǒng)接口。
既然有內(nèi)核態(tài)和用戶態(tài),那么我們怎么辨別我們當(dāng)前是哪個身份呢?
CPU內(nèi)存有寄存器,而寄存器又分為可見寄存器(EXP)和不可見寄存器(狀態(tài)寄存器),而所有保存在寄存器跟當(dāng)前進(jìn)程強(qiáng)相關(guān)的數(shù)據(jù)叫做上下文數(shù)據(jù)。
CPU里面有一個叫做CR3的寄存器,它表征的就是當(dāng)前進(jìn)程的運(yùn)行級別:
0表示內(nèi)核態(tài)
3表示用戶態(tài)
那么一個進(jìn)程是如何進(jìn)入操作系統(tǒng)中執(zhí)行方法呢?
因?yàn)椴僮飨到y(tǒng)會加載到內(nèi)存且只有一份,所以內(nèi)核級頁表也只需要一份。在CPU里有一塊寄存器指向這個內(nèi)核級頁表,進(jìn)程切換時這個寄存器不變。
所以進(jìn)程可以在特定的區(qū)域內(nèi)以內(nèi)核級頁表的方式訪問操作系統(tǒng)的代碼和數(shù)據(jù)。當(dāng)進(jìn)程想要訪問OS的接口,直接在自己的進(jìn)程地址空間跳轉(zhuǎn)即可。
而我們知道操作系統(tǒng)有自己的保護(hù)機(jī)制,用戶憑什么能執(zhí)行訪問操作系統(tǒng)數(shù)據(jù)的接口呢?
當(dāng)想要跳轉(zhuǎn)到內(nèi)核區(qū)會進(jìn)行權(quán)限認(rèn)證,如果CR寄存器顯示的是內(nèi)核態(tài)就可以訪問,反之阻止訪問。但是我們怎么把用戶態(tài)切換成內(nèi)核態(tài)呢?當(dāng)我們調(diào)用系統(tǒng)接口的時候,起始的位置會幫忙改變,先把CR3中的用戶態(tài)改成內(nèi)核態(tài),然后再跳轉(zhuǎn)到內(nèi)核區(qū)。
2.2 信號捕捉流程圖
當(dāng)執(zhí)行代碼要調(diào)用系統(tǒng)調(diào)用接口時,本來應(yīng)該調(diào)用完成后返回用戶態(tài)繼續(xù)執(zhí)行代碼。但是我們知道用戶態(tài)和內(nèi)核態(tài)的轉(zhuǎn)換消耗時間很大,所以這里不會直接返回。
它會去找task_struct中的三張表先遍歷block,當(dāng)發(fā)現(xiàn)為1就跳過,如果不是1就看pending表如果為1就進(jìn)入handler完成動作。而默認(rèn)動作直接殺死進(jìn)程,忽略動作直接把pending位圖中的1置為0。
但是如果時自定義動作,我們自己寫的handler方法在用戶態(tài),因?yàn)?strong>內(nèi)核態(tài)不能直接訪問用戶態(tài)(從技術(shù)上可以,但是不能,為了安全),所以又要把自己的身份變成用戶態(tài)再進(jìn)入用戶態(tài)執(zhí)行handler方法。
當(dāng)我們執(zhí)行完handler后能不能直接返回代碼區(qū)繼續(xù)執(zhí)行呢?
答案是不能,因?yàn)樯舷挛男畔⒍歼€在操作系統(tǒng)里。所以要先回到內(nèi)核,經(jīng)過特殊的系統(tǒng)調(diào)用回到代碼區(qū)繼續(xù)執(zhí)行代碼。
我們可以把線路簡化一下方便觀察:
分析:
這里的綠色部位交點(diǎn)代表身份的切換,而箭頭的指向:
向下表示從用戶態(tài)切換到內(nèi)核態(tài)
向上表示從內(nèi)核態(tài)切換到用戶態(tài)
三、信號操作
經(jīng)過上面的的學(xué)習(xí)我們知道了內(nèi)核中有block和pending位圖,為了方便我們操作,操作系統(tǒng)定義了一個類型sigset_t。
#include <signal.h>
int sigemptyset(sigset_t *set);// 清0
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);// 比特位由0變?yōu)?
int sigdelset(sigset_t *set, int signo);// 比特位由1變?yōu)?
int sigismember(const sigset_t *set, int signo);
- 函數(shù)sigemptyset初始化set所指向的信號集,使其中所有信號的對應(yīng)bit清零,表示該信號集不包含 任何有效信號。
- 函數(shù)sigfillset初始化set所指向的信號集,使其中所有信號的對應(yīng)bit置位,表示 該信號集的有效信號包括系統(tǒng)支持的所有信號。
- 注意,在使用sigset_ t類型的變量之前,一定要調(diào) 用sigemptyset或sigfillset做初始化,使信號集處于確定的狀態(tài)。初始化sigset_t變量之后就可以在調(diào)用sigaddset和sigdelset在該信號集中添加或刪除某種有效信號。
- sigismember是一個布爾函數(shù),用于判斷一個信號集的有效信號中是否包含某種 信號,若包含則返回1,不包含則返回0,出錯返回-1。
3.1 sigset_t信號集
我們能看到阻塞和未決都是用一個比特位進(jìn)行標(biāo)記(非0即1),所以在用戶層采用相同的類型sigset_t進(jìn)行描述。這個類型表示每個信號有效和無效的狀態(tài):在阻塞信號集就表示是否處于阻塞;在未決信號集就表示是否處于未決。
阻塞信號集有一個專業(yè)的名詞叫做信號屏蔽字。
3.2 信號集操作函數(shù)
sigset_t對每個信號用一個比特位表示有效或者無效的狀態(tài)。它的底層操作對于我們用戶層來說不必要知道,我們只能調(diào)用下面的接口函數(shù)來操作sigset_ t變量。
3.2.1 更改block表sigprocmask
調(diào)用函數(shù)sigprocmask可以讀取或更改進(jìn)程的信號屏蔽字(阻塞信號集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
RETURN VALUE
sigprocmask() returns 0 on success and -1 on error.
In the event of an error, errno is set to indicate the cause.
參數(shù)介紹:
how
:怎么修改。set
:主要是用來跟how一起使用,用來重置信號。oldset
:輸出型參數(shù),把老的信號屏蔽字保存,方便恢復(fù)
3.2.2 獲取pending信號集sigpending
#include <signal.h>
int sigpending(sigset_t *set);
RETURN VALUE
sigpending() returns 0 on success and -1 on error.
In the event of an error, errno is set to indicate the cause.
讀取當(dāng)前進(jìn)程的未決信號集,通過set參數(shù)傳出。 set是輸出型參數(shù)。
3.3 驗(yàn)證
首先要知道默認(rèn)情況所有信號都不會被阻塞。獲取pending表對應(yīng)的比特位變成1。
而如果被阻塞了,信號永遠(yuǎn)不會被遞達(dá),獲取pending表對應(yīng)的比特位永遠(yuǎn)為1。
static void show_pending(const sigset_t &Pending)
{
// 信號只有1 ~ 31
for(int signo = 31; signo >= 1; signo--)
{
if(sigismember(&Pending, signo))
{
std::cout << "1";
}
else std::cout << "0";
}
std::cout << std::endl;
}
int main()
{
sigset_t Block, oBlock, Pending;
// 初始化全0
sigemptyset(&Block);
sigemptyset(&oBlock);
sigemptyset(&Pending);
// 在Block集添加阻塞信號
sigaddset(&Block, 2);
// 修改block表
sigprocmask(SIG_SETMASK, &Block, &oBlock);
// 打印
while(true)
{
// 獲取pending
sigpending(&Pending);
show_pending(Pending);
sleep(1);
}
return 0;
}
前面我們使用signal函數(shù)捕捉信號不能自定義捕捉9號信號,這里也是一樣不能屏蔽9號信號。
當(dāng)然我們也可以解除阻塞,讓信號遞達(dá),信號一旦遞達(dá),pending就會先由1置0,然后就會處理信號,進(jìn)程退出。
static void show_pending(const sigset_t &Pending)
{
// 信號只有1 ~ 31
for(int signo = 31; signo >= 1; signo--)
{
if(sigismember(&Pending, signo))
{
std::cout << "1";
}
else std::cout << "0";
}
std::cout << std::endl;
}
int main()
{
sigset_t Block, oBlock, Pending;
// 初始化全0
sigemptyset(&Block);
sigemptyset(&oBlock);
sigemptyset(&Pending);
// 在Block集添加阻塞信號
sigaddset(&Block, 2);
// 修改block表
sigprocmask(SIG_SETMASK, &Block, &oBlock);
// 打印
int cnt = 8;
while(true)
{
// 獲取pending
sigpending(&Pending);
show_pending(Pending);
sleep(1);
if(--cnt == 0)
{
// 恢復(fù)
sigprocmask(SIG_SETMASK, &oBlock, &Block);
std::cout << "恢復(fù)對信號的屏蔽" << std::endl;
}
}
return 0;
}
而為什么沒有打印后面那句話呢?
因?yàn)檫M(jìn)程在內(nèi)核態(tài)直接退出來,就不會返回到用戶態(tài)執(zhí)行代碼。
四、捕捉信號操作
4.1 內(nèi)核捕捉信號sigaction
在上一章【linux】進(jìn)程信號——信號的產(chǎn)生中我們學(xué)習(xí)了捕捉信號自定義函數(shù)signal
。
sighandler_t signal(int signum, sighandler_t handler);
而sigaction
使用起來要比signal
使用起來復(fù)雜。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
參數(shù)說明:
signum
代表指定的信號。act
是一個跟函數(shù)名同名的結(jié)構(gòu)體,輸入型參數(shù)。
struct sigaction {
void (*sa_handler)(int); //自己寫的方法
void (*sa_sigaction)(int, siginfo_t *, void *);// null
sigset_t sa_mask;// 信號集
int sa_flags;// 設(shè)置0
void (*sa_restorer)(void);// null
};
oldact
輸出型參數(shù),保存過去的數(shù)據(jù),方便恢復(fù)。
話不多說,直接上代碼:
#include <iostream>
#include <signal.h>
#include <unistd.h>
void handler(int signo)
{
std::cout << "catch signo: " << signo << std::endl;
}
int main()
{
struct sigaction act, oact;
// 初始化
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGINT, &act, &oact);
while(1) sleep(1);
return 0;
}
我們可以看到它可以實(shí)現(xiàn)跟signal
函數(shù)一樣的功能。
那么它跟signal
有什么區(qū)別呢?
我們想象這樣一個場景:
假設(shè)我們在handler設(shè)置等待15秒的倒計時函數(shù),先發(fā)送一個SIGINT信號,在自定義處理等待15s的期間再次發(fā)送一個SIGINT信號,那么會不會遞歸似的調(diào)用handler呢?
運(yùn)行中:
結(jié)束:
現(xiàn)象:
我們發(fā)了許多的二號信號,但是只處理了兩個。
當(dāng)我們處理第一個信號的時候,后邊的信號不會再次被提交,當(dāng)處理完后,后續(xù)信號就會遞達(dá),但是一共就兩個信號遞達(dá)了,后續(xù)信號全部丟失了。
結(jié)論:
當(dāng)我們正在處理一個遞達(dá)的信號時,同類信號無法被遞達(dá),因?yàn)楫?dāng)前信號正在被捕捉時,系統(tǒng)會自動把該信號設(shè)置進(jìn)信號屏蔽字中(block)
當(dāng)信號完成捕捉動作,系統(tǒng)又會自動解除對該信號的屏蔽。
所以為什么我們發(fā)送了一堆的二號信號,處理完第一次后會處理第二次?
當(dāng)一個信號被遞達(dá)時,pending位圖的位置就由1置為0,后邊再次發(fā)送多個,又由0置為1(只有一個比特位所以只收到一個),當(dāng)一個信號被解除屏蔽的時候,OS會去檢查pending位圖,如果被置1,就再次遞達(dá)。
4.1.1 act.sa_mask參數(shù)
上面我們是捕獲了2號信號,如果我們想在處理某種信號的時候順便屏蔽其他信號,就可以添加進(jìn)sa_mask信號集中。
可以看到處理二號信號的時候3號信號被屏蔽了,那么為什么最后3號信號會起作用呢?
sa_mask :在執(zhí)行捕捉函數(shù)時,設(shè)置阻塞其它信號,sa mask進(jìn)程阻塞信號集,退出捕捉函數(shù)后,還原回原有的阻塞信號集。
五、可重入函數(shù)
假設(shè)一種場景:一個信號的處理方法是給一個鏈表進(jìn)行頭插,現(xiàn)在我們在main函數(shù)調(diào)用頭插,而在頭插的過程觸發(fā)了信號的捕捉動作,又要進(jìn)行頭插,這樣就會導(dǎo)致失去了頭節(jié)點(diǎn)的位置。
因?yàn)閮蓚€執(zhí)行流重復(fù)進(jìn)入insert函數(shù)導(dǎo)致出現(xiàn)錯誤,我們把insert函數(shù)叫做不可重入函數(shù)。
沒出問題就叫做可重入函數(shù)。
可不可重入是個特性(中義詞),我們用的大部分接口都是不可重入的。
如果一個函數(shù)符合以下條件之一則是不可重入的:
- 調(diào)用了malloc或free,因?yàn)閙alloc也是用全局鏈表來管理堆的。
- 調(diào)用了標(biāo)準(zhǔn)I/O庫函數(shù)。標(biāo)準(zhǔn)I/O庫的很多實(shí)現(xiàn)都以不可重入的方式使用全局?jǐn)?shù)據(jù)結(jié)構(gòu)。
六、volatile關(guān)鍵字
#include <signal.h>
#include <unistd.h>
#include <iostream>
#include <cstdio>
int quit = 0;
void handler(int signo)
{
printf(" %d signo is being caught\n",signo);
printf("quit:%d\n",quit);
quit = 1;
printf("->%d\n",quit);
}
int main()
{
signal(2,handler);
while(!quit);
printf("i am quit\n");
return 0;
}
這樣退出是正常情況。但是如果我們讓編譯器進(jìn)行優(yōu)化:
可以看到quit確實(shí)被改為1了,但是卻沒有終止循環(huán)。
這里是因?yàn)榫幾g器把quit數(shù)據(jù)優(yōu)化到了寄存器中。
如果不優(yōu)化,每次判斷quit都需要從物理內(nèi)存獲取quit的內(nèi)容:
而如果要優(yōu)化,編譯器看到main中while(!quit)
并沒有被修改,所以直接把quit的值放進(jìn)寄存器中,不用再從物理內(nèi)存中獲取。
而我們后邊修改quit改的是內(nèi)存中的quit,并不會印象到寄存器,所以不會退出循環(huán)。這是因?yàn)?strong>寄存器的存在遮蓋了物理內(nèi)存的quit的值。文章來源:http://www.zghlxwxcb.cn/news/detail-778215.html
而加上volatile關(guān)鍵字就可以避免這種情況。
volatile的作用:保持內(nèi)存的可見性,告知編譯器,被該關(guān)鍵字修飾的變量,不允許被優(yōu)化,對該變量的任何操作,都必須在真實(shí)的內(nèi)存中進(jìn)行操作 。文章來源地址http://www.zghlxwxcb.cn/news/detail-778215.html
到了這里,關(guān)于【linux】進(jìn)程信號——信號的保存和處理的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!