一、信號的處理時(shí)機(jī)
在前面我們講過信號產(chǎn)生和保存以后,我們知道進(jìn)程對于產(chǎn)生的信號不是立即去處理的,而是在"合適"的時(shí)候去處理信號,這是因?yàn)樾盘柕漠a(chǎn)生的異步的,當(dāng)前進(jìn)程可能正在做更重要的事情!。
那么信號可以被立即處理嗎?答案的可以的,但是要滿足這個(gè)條件:
在
Linux
中如果一個(gè)信號之前被阻塞過,當(dāng)他解除阻塞時(shí),對應(yīng)的信號會被立即遞達(dá)!
那么對于進(jìn)程來說什么是"合適"的時(shí)候呢?
答案是:當(dāng)進(jìn)程從內(nèi)核態(tài)切換回用戶態(tài)的時(shí)候,進(jìn)程會在操作系統(tǒng)的指導(dǎo)下,進(jìn)行信號的檢測與處理!
二、內(nèi)核態(tài)與用戶態(tài)
簡單來說內(nèi)核態(tài)與用戶態(tài)的區(qū)別就是:
用戶態(tài):進(jìn)程只能執(zhí)行用戶所寫的代碼。
內(nèi)核態(tài):進(jìn)程只能執(zhí)行操作系統(tǒng)的代碼。
我們知道操作系統(tǒng)也是一款軟件,而且是一款專注于搞管理的軟件,在對進(jìn)程進(jìn)行調(diào)度、執(zhí)行系統(tǒng)調(diào)用、異常、中斷、陷阱等,都需要借助操作系統(tǒng),執(zhí)行操作系統(tǒng)的代碼,此時(shí)進(jìn)程便處于內(nèi)核態(tài)。
進(jìn)程又是如何被調(diào)度的呢?
- 操作系統(tǒng)的本質(zhì):
- 操作系統(tǒng)也是軟件,并且是一個(gè)死循環(huán)式等待指令的軟件。
- 計(jì)算機(jī)內(nèi)部存在一個(gè)硬件:時(shí)鐘模塊,每隔一段時(shí)間向操作系統(tǒng)發(fā)送時(shí)鐘中斷
- 進(jìn)程被調(diào)度,就意味著它的時(shí)間片到了,操作系統(tǒng)會通過時(shí)鐘中斷,檢測到是哪一個(gè)進(jìn)程的時(shí)間片到了,然后通過系統(tǒng)調(diào)用函數(shù)
schedule()
保存進(jìn)程的上下文數(shù)據(jù),然后選擇合適的進(jìn)程去運(yùn)行,這就完成了一次進(jìn)程調(diào)度。
1、內(nèi)核態(tài)與用戶態(tài)的轉(zhuǎn)化
- 用戶態(tài)向內(nèi)核態(tài)的轉(zhuǎn)化的時(shí)機(jī):
- 當(dāng)進(jìn)程時(shí)間片到了之后,需要進(jìn)行進(jìn)程調(diào)度時(shí)。
-
調(diào)用系統(tǒng)調(diào)用接口,比如
open
、read
時(shí) - 產(chǎn)生異常、中斷、陷阱時(shí)
- 內(nèi)核態(tài)向用戶態(tài)的轉(zhuǎn)化的時(shí)機(jī):
- 進(jìn)程調(diào)度完成以后。
- 系統(tǒng)調(diào)用調(diào)用完畢時(shí)。
- 異常、中斷、陷阱處理完畢時(shí)。
2、重談進(jìn)程地址空間
關(guān)于進(jìn)程地址空間的初級知識可以看這里《進(jìn)程地址空間》
在以前我們只討論了[0, 3]G的用戶空間,并沒有對[3, 4]G的內(nèi)核空間進(jìn)行討論,現(xiàn)在我們對[3, 4]G的內(nèi)核空間進(jìn)行討論。
我們在談?wù)撚脩艨臻g時(shí)提到,用戶空間的地址要經(jīng)過頁表映射到物理地址,這個(gè)用戶空間的頁表其實(shí)其真實(shí)名稱是用戶級頁表,對于內(nèi)核空間來說也有一張頁表,也負(fù)責(zé)將內(nèi)核空間的地址映射到物理地址中,這個(gè)頁表的名稱是內(nèi)核級頁表。這兩張頁表是相互獨(dú)立的!
內(nèi)核空間里面存放的是操作系統(tǒng)代碼和數(shù)據(jù), 所以執(zhí)行操作系統(tǒng)的代碼及系統(tǒng)調(diào)用,其實(shí)就是在使用這 1 GB 的內(nèi)核空間
- 對于所有的進(jìn)程[0, 3]GB是不同的,每一個(gè)進(jìn)程都要有自己的用戶級頁表用來映射自己的代碼和數(shù)據(jù)。
- 所有的進(jìn)程[3,4]GB是一樣的,每一個(gè)進(jìn)程都可以看到同一張內(nèi)核級頁表,所有進(jìn)程都可以通過統(tǒng)一的窗口,看到同一個(gè)操作系統(tǒng)!
- 無論進(jìn)程如何切換,[3,4]GB不變,看到的都是OS的內(nèi)容,與進(jìn)程切換無關(guān),也就是說進(jìn)程切換其實(shí)切換的是[0, 3]G的用戶空間里面的內(nèi)容和用戶級頁表!
- 操作系統(tǒng)運(yùn)行的本質(zhì): 其實(shí)是在進(jìn)程的地址空間內(nèi)運(yùn)行的!
- 由于內(nèi)核空間中存放的是操作系統(tǒng)的代碼和數(shù)據(jù),所以調(diào)用系統(tǒng)調(diào)用的本質(zhì): 其實(shí)就如同調(diào)用動態(tài)庫中的函數(shù),在自己的地址空間中進(jìn)行函數(shù)跳轉(zhuǎn)并返回即可!
由于操作系統(tǒng)的代碼和數(shù)據(jù)是不能夠被輕易訪問的,所以在正文代碼中如果要執(zhí)行操作系統(tǒng)的代碼和數(shù)據(jù),需要先進(jìn)行狀態(tài)轉(zhuǎn)化,由用戶態(tài)轉(zhuǎn)化為內(nèi)核態(tài),才能成功執(zhí)行,那么這個(gè)狀態(tài)轉(zhuǎn)換是怎么實(shí)現(xiàn)的呢?
對于狀態(tài)轉(zhuǎn)化,操作系統(tǒng)采用的是軟硬件結(jié)合的方式。
-
硬件方面:
在CPU
中,存在一個(gè)CR3
寄存器,這個(gè)寄存器的作用就是用來表是當(dāng)前處于進(jìn)程所處的狀態(tài)。
當(dāng)CR3
寄存器中的值為3
時(shí):表示處于用戶態(tài),可以執(zhí)行用戶的代碼。
當(dāng)CR3
寄存器中的值為0
時(shí):表示處于內(nèi)核態(tài),可以執(zhí)行操作系統(tǒng)的代碼。
-
軟件方面
Linux
并沒有給我們提供相應(yīng)的接口讓我們可以更改CR3
寄存器里面的值,因?yàn)椴僮飨到y(tǒng)沒有辦法保證每一個(gè)用戶使用OS的代碼和數(shù)據(jù)時(shí)都要先更改CR3
寄存器的值,所以OS提供的所有的系統(tǒng)調(diào)用,內(nèi)部在正執(zhí)行調(diào)用邏輯的時(shí)候,會去修改執(zhí)行級別! 這樣就保證了用戶使用系統(tǒng)調(diào)用的時(shí)候用戶所處的狀態(tài)是內(nèi)核態(tài)。
三、信號的處理
1、一般信號的處理流程
當(dāng)CPU正在執(zhí)行某條代碼時(shí),可能因?yàn)?strong>中斷、異?;蛳到y(tǒng)調(diào)用進(jìn)入內(nèi)核態(tài),然后在內(nèi)核態(tài)完成相應(yīng)的任務(wù),任務(wù)完成以后并不是直接返回用戶態(tài),而是調(diào)用系統(tǒng)調(diào)用do_signal()
去處理可以遞達(dá)信號。
處理信號時(shí)會從1號到31號逐個(gè)檢查block
表和pending
表,當(dāng)block
和pending
表符合處理?xiàng)l件時(shí)才進(jìn)行信號遞達(dá)。
block表 | pending表 | 是否處理 | 解釋 |
---|---|---|---|
0 | 0 | 否 | pending表為0 代表該信號沒有產(chǎn)生過,無需處理 |
1 | 0 | 否 | block表為0 ,信號被阻塞,無需處理 |
1 | 1 | 否 | block表為0 ,信號被阻塞,無需處理 |
0 | 1 | 是 | 信號沒有被阻塞且pending表為1 ,代表該信號需要遞達(dá) |
當(dāng)信號遞達(dá)時(shí)就需要調(diào)用handler
表里面對應(yīng)位置的的函數(shù)進(jìn)行執(zhí)行:
handler表 | 執(zhí)行動作 |
---|---|
SIG_IGN |
忽略該信號,將該信號的pending 表里面的1 改為0 ,然后調(diào)用sys _sigreturn() 系統(tǒng)調(diào)用進(jìn)行返回原先中斷的位置并恢復(fù)為用戶態(tài) |
SIG_DFL |
執(zhí)行默認(rèn)動作: 1. 如果是暫停,就將該進(jìn)程從運(yùn)行隊(duì)列里面取出放到等待隊(duì)列里面,操作系統(tǒng)開始調(diào)度下一個(gè)進(jìn)程。 2. 如果是終止進(jìn)程,就直接結(jié)束該進(jìn)程,操作系統(tǒng)開始調(diào)度下一個(gè)進(jìn)程。 … |
2、捕捉信號的處理流程
對于被捕捉的信號,與普通信號有所不同,在調(diào)用自定義處理方法時(shí),由handler
表里面的方法是用戶的代碼,所以還要進(jìn)行一次狀態(tài)轉(zhuǎn)換,轉(zhuǎn)換為用戶態(tài),然后執(zhí)行自定義動作,當(dāng)自定義動作執(zhí)行完畢時(shí)OS會自動調(diào)用一次系統(tǒng)調(diào)用sigreturn()
使用戶態(tài)重新陷入內(nèi)核變成內(nèi)核態(tài),然后在內(nèi)核態(tài)再調(diào)用sys _sigreturn()
進(jìn)行返回并恢復(fù)為用戶態(tài)。
下面我們通過一張圖快速記憶捕捉信號的處理過程:
ps: 在執(zhí)行
hadler
表中的方法之前,操作系統(tǒng)會先將pengding
表對應(yīng)位置的1
給清零。
3、信號捕捉函數(shù)sigaction
該函數(shù)是一個(gè)系統(tǒng)調(diào)用,功能與signal()
函數(shù)類似但是功能會更加強(qiáng)大,sigaction
函數(shù)可以讀取和修改指定信號相關(guān)聯(lián)的處理動作。
-
參數(shù):
- 第一個(gè)參數(shù)是要捕捉的信號,第二個(gè)與第三個(gè)都是一個(gè)結(jié)構(gòu)體參數(shù),但是第二個(gè)參數(shù)是輸入型參數(shù),第三個(gè)是輸出形參數(shù)。
- 若
act
指針非空,則根據(jù)act
修改該信號的處理動作。若oact
指針非空,則通過oact
傳出該信號原來的處理動作。act
和oact
指向sigaction
結(jié)構(gòu)體。
-
返回值:
?調(diào)用成功則返回0
,出錯則返回-1
結(jié)構(gòu)體的定義如下:
- 第一個(gè)字段是函數(shù)指針,這個(gè)函數(shù)就是我們捕捉完信號以后要執(zhí)行的處理動作。
- 第二個(gè)與第五個(gè)字段是實(shí)時(shí)信號的處理函數(shù),這里我們不做詳細(xì)解釋,可以直接設(shè)置為0。
- 第三個(gè)字段是一個(gè)信號屏蔽集,這個(gè)字段設(shè)置完畢以后我們可以在處理捕捉信號時(shí)對信號屏蔽集里面的信號進(jìn)行屏蔽。
- 第四個(gè)字段包含了一些選項(xiàng),一般默認(rèn)設(shè)置為
0
。
關(guān)于信號處理時(shí)的一些機(jī)制:
當(dāng)某個(gè)信號的處理函數(shù)被調(diào)用時(shí),內(nèi)核會自動將當(dāng)前信號加入進(jìn)程的信號屏蔽字,當(dāng)信號處理函數(shù)返回時(shí)自動恢復(fù)原來的信號屏蔽字,這樣就保證了在處理某個(gè)信號時(shí),如果這種信號再次產(chǎn)生,那么它會被阻塞到當(dāng)前處理結(jié)束為止。
下面我們來使用該函數(shù)驗(yàn)證一下信號處理時(shí):內(nèi)核會自動將當(dāng)前信號加入進(jìn)程的信號屏蔽字。
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <signal.h>
// 打印pending表
void PrintPending(sigset_t set)
{
std::cout << "當(dāng)前的pending表:";
for (int i = 1; i <= 31; i++)
{
if (sigismember(&set, i))
{
std::cout << '1';
}
else
{
std::cout << '0';
}
}
std::cout << std::endl;
}
// 自定義處理動作
void handler(int signum)
{
std::cout << "捕捉到了" << signum << "信號,執(zhí)行了自定義動作" << std::endl;
int cnt = 0;
sigset_t set;
sigemptyset(&set);
while (cnt < 5)
{
cnt++;
sigpending(&set);
PrintPending(set);
sleep(1);
}
}
int main()
{
struct sigaction act, oact;
memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(act));
act.sa_handler = handler;
sigaction(2, &act, &oact);
while (true)
{
sleep(1);
}
}
這段代碼中我們對2
號信號進(jìn)行了捕捉,自定義處理動作就是在自定義函數(shù)中停留5秒,每秒都打印一下當(dāng)前狀態(tài)的pending
表。
我們可以運(yùn)行程序,然后給該進(jìn)程發(fā)送2
號信號觸發(fā)自定義處理動作,然后再在5秒之內(nèi)再次發(fā)送2
號信號觀察pending
表是否為1
,如果為1
就代表當(dāng)前信號收到了阻塞,如果沒有變成1
代表沒有受到阻塞。
可以看到結(jié)果符合我們的理論。
接下來我們嘗試?yán)?code>sigaction將3, 4
號信號也加入信號屏蔽集中。文章來源:http://www.zghlxwxcb.cn/news/detail-650714.html
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <signal.h>
// 打印pending表
void PrintPending(sigset_t set)
{
std::cout << "當(dāng)前的pending表:";
for (int i = 1; i <= 31; i++)
{
if (sigismember(&set, i))
{
std::cout << '1';
}
else
{
std::cout << '0';
}
}
std::cout << std::endl;
}
// 自定義處理動作
void handler(int signum)
{
std::cout << "捕捉到了" << signum << "信號,執(zhí)行了自定義動作" << std::endl;
int cnt = 0;
sigset_t set;
sigemptyset(&set);
while (cnt < 15)
{
cnt++;
sigpending(&set);
PrintPending(set);
sleep(1);
}
}
int main()
{
struct sigaction act, oact;
sigset_t set, oset;
// 進(jìn)行初始化
memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(act));
sigemptyset(&set);
sigemptyset(&oset);
// 將3, 4也加入信號屏蔽集中
sigaddset(&set, 3);
sigaddset(&set, 4);
act.sa_handler = handler;
// 設(shè)置信號屏蔽字
act.sa_mask = set;
sigaction(2, &act, &oact);
std::cout << "進(jìn)程的pid是:" << getpid() << std::endl;
while (true)
{
sleep(1);
}
}
如果我們還想將其他信號進(jìn)行屏蔽,我們可以繼續(xù)修改sigaction
結(jié)構(gòu)體里面sa_mask
字段。文章來源地址http://www.zghlxwxcb.cn/news/detail-650714.html
到了這里,關(guān)于【Linux】進(jìn)程信號之信號的處理的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!