前言
作者:小蝸牛向前沖
名言:我可以接受失敗,但我不能接受放棄
??如果覺的博主的文章還不錯的話,還請
點贊,收藏,關(guān)注??支持博主。如果發(fā)現(xiàn)有問題的地方歡迎?大家在評論區(qū)指正
目錄
一、信號的保存?
1、信號其他相關(guān)常見概念
2、信號在內(nèi)核中的表示
3、sigset_t
4. 信號集操作函數(shù)?
二、?模仿實現(xiàn)內(nèi)核對信號的保存
1、信號函數(shù)
?2、實驗代碼
三、信號的的捕捉?
1、內(nèi)核態(tài)和用戶態(tài)
2、信號的捕捉流程?
四、信號的補充知識
?1、sigaction函數(shù)
2、可重入函數(shù)
3、?volatile關(guān)鍵字
本期學(xué)習(xí)目標(biāo):信號的保存,信號的捕捉,什么是可重入函數(shù)和關(guān)鍵字volatile。
一、信號的保存?
1、信號其他相關(guān)常見概念
- 實際執(zhí)行信號的處理動作稱為信號遞達(Delivery)
- 信號從產(chǎn)生到遞達之間的狀態(tài),稱為信號未決(Pending)。
- 進程可以選擇阻塞 (Block )某個信號。
- 被阻塞的信號產(chǎn)生時將保持在未決狀態(tài),直到進程解除對此信號的阻塞,才執(zhí)行遞達的動作.
- 注意: 阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是在遞達之后可選的一種處理動作。
2、信號在內(nèi)核中的表示
信號在內(nèi)核中的表示示意圖
? ? ? ? 每個信號都有兩個標(biāo)志位分別表示阻塞(block)和未決(pending),還有一個函數(shù)指針表示處理動作。
從位圖中我們理解pending位圖和信號的關(guān)系,?操作系統(tǒng)發(fā)信號,本質(zhì)也就是將信號寫入到pending位圖中。pending位圖中的比特位的內(nèi)容也就表征了是否收到該信號
對于block位圖:比特位的位置也是代表,信號的編號,但是比特位的內(nèi)容表示是否阻塞對應(yīng)的信號。如果阻塞了就不在執(zhí)行該信號,除非解除阻塞。?
if((1<<(signo -1)) & pcb->block)
{
//signo信號被阻塞,不遞達
}
else
{
if((1<<(signo -1)) & pcb->pending)
{
//遞達該信號
}
}
?上面我們寫了一份偽代碼,來理解內(nèi)核是如何大致處理信號未決到遞達的過程和信號阻塞。
其中在內(nèi)核中還有一個handler的數(shù)組用來存放信號,其中數(shù)組的下標(biāo)表示信號的編號,數(shù)組
下標(biāo)對應(yīng)的內(nèi)容表示信號的內(nèi)容。
所以:當(dāng)一個信號沒有產(chǎn)生這并不妨礙他可以先被阻塞。
3、sigset_t
? ? ? ? 從上圖來看,每個信號只有一個bit的未決標(biāo)志,非0即1,不記錄該信號產(chǎn)生了多少次,阻塞標(biāo)志也是這樣表示的。
? ? ? ? 因此,未決和阻塞標(biāo)志可以用相同的數(shù)據(jù)類型sigset_t來存儲,sigset_t稱為信號集,這個類型可以表示每個信號 的“有效”或“無效”狀態(tài),在阻塞信號集中“有效”和“無效”的含義是該信號是否被阻塞,而在未決信號集中“有 效”和“無效”的含義是該信號是否處于未決狀態(tài)。
? ? ?阻塞信號集也叫做當(dāng) 前進程的信號屏蔽字(Signal Mask),這里的“屏蔽”應(yīng)該理解為阻塞而不是忽略
4. 信號集操作函數(shù)?
? ? ? ? sigset_t類型對于每種信號用一個bit表示“有效”或“無效”狀態(tài),至于這個類型內(nèi)部如何存儲這些bit則依賴于系統(tǒng) 實現(xiàn),從使用者的角度是不必關(guān)心的,使用者只能調(diào)用以下函數(shù)來操作sigset_ t變量,而不應(yīng)該對它的內(nèi)部數(shù)據(jù)做 任何解釋,比如用printf直接打印sigset_t變量是沒有意義的?。
#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ù)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在該信號集中添加或刪除某種有效信號
這四個函數(shù)都是成功返回0,出錯返回-1。sigismember是一個布爾函數(shù),用于判斷一個信號集的有效信號中是否包含某種信號,若包含則返回1,不包含則返回0,出錯返回-1。?
二、?模仿實現(xiàn)內(nèi)核對信號的保存
? ? ?為了更好的理解,內(nèi)核如何進行對信號的保存的,下面我們自己寫一份代碼,去驗證信號在內(nèi)核中的保存。
1、信號函數(shù)
sigprocmask函數(shù)
功能:可以讀取或更改進程的信號屏蔽字(阻塞信號集)。
原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oset)
返回值:若成功則為0,若出錯則為-1
- ?如果oset是非空指針,則讀取進程的當(dāng)前信號屏蔽字通過oset參數(shù)傳出。
- 如果set是非空指針,則 更改進程的信號屏蔽字,
- 參數(shù)how指示如何更改。
- 果oset和set都是非空指針,則先將原來的信號 屏蔽字備份到oset里,然后 根據(jù)set和how參數(shù)更改信號屏蔽字。
上面一直說信號屏蔽字,那這到底有上面用?
信號屏蔽字是一個位掩碼,用于指定哪些信號被阻塞(屏蔽)而不會被遞送給進程。當(dāng)某個信號被屏蔽時,它將被擱置,直到信號解除屏蔽為止。這允許進程在關(guān)鍵部分屏蔽某些信號,以確保在執(zhí)行臨界區(qū)代碼時不會被中斷。?
對于sigprocmask函數(shù),當(dāng)假設(shè)當(dāng)前屏蔽字為mask?下表說明了how參數(shù)的可選值.
SIG_BLOCK | set包含了我們希望添加到當(dāng)前信號屏蔽字的信號,相當(dāng)于mask=mask|set |
SIG_UNBLOCK | set包含了我們希望從當(dāng)前信號屏蔽字中解除阻塞的信號,相當(dāng)于mask=mask&~set |
SIG_SETMASK | 設(shè)置當(dāng)前信號屏蔽字為set所指向的值,相當(dāng)于mask=se |
sigpending?函數(shù)
功能:讀取當(dāng)前進程的未決信號集,通過set參數(shù)傳出
原型:int sigpending(sigset_t *set)
返回值:調(diào)用成功則返回0,出錯則返回-1。
?2、實驗代碼
這里我們驗證2號信號為例(ctrl+c)
#include<iostream>
#include<vector>
#include<signal.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;
#define MAX_SIGNUM 31
static vector<int> sigarr = {2};
static void show_pending(const sigset_t &pending)
{
for(int signo = MAX_SIGNUM; signo >= 1;signo--)
{
if(sigismember(&pending,signo))
{
cout<< "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
static void myhandler(int signo)
{
cout << signo << " 號信號已經(jīng)被遞達!!" << endl;
}
int main()
{
for(const auto &sig : sigarr) signal(sig, myhandler);
sigset_t block, oblock, pending;
//初始化
sigemptyset(&block);
sigemptyset(&oblock);
sigemptyset(&pending);
//添加要屏蔽的信號
for(const auto& sig : sigarr) sigaddset(&block,sig);
//開始屏蔽,設(shè)置內(nèi)核
sigprocmask(SIG_SETMASK,&block,&oblock);
//遍歷打印penging信號集
int cnt = 10;
while(true)
{
//初始化
sigemptyset(&pending);
//獲取
sigpending(&pending);
//打印
show_pending(pending);
sleep(1);
printf("%d\n",cnt);
if(cnt-- == 0)
{
sigprocmask(SIG_SETMASK,&oblock,&block);
cout << "恢復(fù)對信號的屏蔽,不屏蔽任何信號"<<endl;
}
}
return 0;
}
?
三、信號的的捕捉?
1、內(nèi)核態(tài)和用戶態(tài)
在理解信號捕捉流程前,我們要明白什么是內(nèi)核態(tài)和用戶態(tài):
內(nèi)核態(tài)(Kernel Mode):
- 權(quán)限高:?內(nèi)核態(tài)擁有系統(tǒng)的最高權(quán)限,可以執(zhí)行特權(quán)指令和訪問系統(tǒng)的所有資源。
- 操作系統(tǒng)內(nèi)核運行:?在內(nèi)核態(tài)下,操作系統(tǒng)的內(nèi)核代碼運行,可以執(zhí)行對硬件的直接訪問和控制。
- 敏感指令:?內(nèi)核態(tài)可以執(zhí)行一些敏感指令,如修改全局頁表、禁止中斷等。
- 特權(quán)級別:?通常,內(nèi)核態(tài)運行在較高的特權(quán)級別(Ring 0或Supervisor Mode),這是計算機體系結(jié)構(gòu)(如x86)中的一個常見術(shù)語。
用戶態(tài)(User Mode):
- 權(quán)限低:?用戶態(tài)擁有較低的權(quán)限,受到更多的限制,無法直接訪問底層硬件資源。
- 用戶應(yīng)用程序運行:?在用戶態(tài)下,用戶應(yīng)用程序運行,其執(zhí)行受到操作系統(tǒng)的控制和限制。
- 受限指令:?用戶態(tài)下的程序不能直接執(zhí)行一些特權(quán)指令,如修改頁表、禁止中斷等。
- 特權(quán)級別:?用戶態(tài)通常運行在較低的特權(quán)級別(Ring 3或User Mode)。
? ? ? ?在正常的程序執(zhí)行中,處理器在用戶態(tài)和內(nèi)核態(tài)之間進行切換。當(dāng)應(yīng)用程序需要執(zhí)行需要更高權(quán)限的操作時(如訪問硬件、執(zhí)行特權(quán)指令),會觸發(fā)一個從用戶態(tài)到內(nèi)核態(tài)的切換。這通常通過系統(tǒng)調(diào)用(system call)來實現(xiàn),應(yīng)用程序請求操作系統(tǒng)執(zhí)行某些特權(quán)操作,操作系統(tǒng)會在內(nèi)核態(tài)執(zhí)行相應(yīng)的服務(wù)例程。?
在進行切換身份進行系統(tǒng)調(diào)用的時候,調(diào)用的人是進程,但是身份是內(nèi)核。系統(tǒng)調(diào)用是比較占用時間的,所以我們應(yīng)該盡量頻繁的使用系統(tǒng)調(diào)用。
理解什么是內(nèi)核態(tài)和用戶態(tài)
那一個進程,是怎么跑到OS中去執(zhí)行方法的呢?
?這是因為每個進程都有自己的地址空間(用戶獨占的空間)內(nèi)核空間(被映射到了每個進程的3~4G),
這時進程要訪問OS的接口,只要在自己的地址空間上跳轉(zhuǎn)就好了
注意:每個進程都會3~4GB地址空間,都會共享一個內(nèi)核級頁表,無論進程如何切換,都吧會更該這個頁表
2、信號的捕捉流程?
其實上面的進程捕捉流程,我們可以用倒寫的8字來記憶
注意:
- 默認(rèn)情況下:我們所以的信號是不被阻塞的
- 默認(rèn)情況下:如果一個信號被屏蔽了,該信號就不會被遞達
四、信號的補充知識
?1、sigaction函數(shù)
功能:讀取和修改與指定信號相關(guān)聯(lián)的處理動作
原型:?int sigaction(int signum, const struct sigaction *act,?struct sigaction *oldact)
參數(shù):
- signum?是指定信號的編號
- 若act指針非空,則根據(jù)act修改該信號的處理動作
- 若oact指針非空,則通過oact傳 出該信號原來的處理動作
返回:調(diào)用成功則返回0,出錯則返回- 1
act和oact指向sigaction結(jié)構(gòu)體?:
將sa_handler賦值為常數(shù)SIG_IGN傳給sigaction表示忽略信號,賦值為常數(shù)SIG_DFL表示執(zhí)行系統(tǒng)默認(rèn)動作,賦值為一個函數(shù)指針表示用自定義函數(shù)捕捉信號,或者說向內(nèi)核注冊了一個信號處理函數(shù),該函數(shù)返回 值為void,可以帶一個int參數(shù),通過參數(shù)可以得知當(dāng)前信號的編號,這樣就可以用同一個函數(shù)處理多種信 號。顯然,這也是一個回調(diào)函數(shù),不是被main函數(shù)調(diào)用,而是被系統(tǒng)所調(diào)用
當(dāng)某個信號的處理函數(shù)被調(diào)用時,內(nèi)核自動將當(dāng)前信號加入進程的信號屏蔽字,當(dāng)信號處理函數(shù)返回時自動恢復(fù)原來 的信號屏蔽字,這樣就保證了在處理某個信號時,如果這種信號再次產(chǎn)生,那么 它會被阻塞到當(dāng)前處理結(jié)束為止。 如果 在調(diào)用信號處理函數(shù)時,除了當(dāng)前信號被自動屏蔽之外,還希望自動屏蔽另外一些信號,則用sa_mask字段說明這些需 要額外屏蔽的信號,當(dāng)信號處理函數(shù)返回時自動恢復(fù)原來的信號屏蔽字。?
下面我們通過代碼來理解一下sigaction函數(shù)
#include<iostream>
#include<cstdio>
#include<signal.h>
#include<unistd.h>
using namespace std;
void Count(int cnt)
{
while(cnt)
{
printf("cnt: %2d\r", cnt);
fflush(stdout);
cnt--;
sleep(1);
}
printf("\n");
}
void handler(int signo)
{
cout << "get a signo: " << signo << "正在處理中..." << endl;
Count(20);
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask); // 當(dāng)我們正在處理某一種信號的時候,我們也想順便屏蔽其他信號,就可以添加到這個sa_mask中
sigaddset(&act.sa_mask, 3);
sigaction(SIGINT, &act, &oact);
while(true) sleep(1);
return 0;
}
這里我們觀察到,我們一直給進程發(fā)2號信號,發(fā)現(xiàn)他不馬上執(zhí)行所育的2號信號的,而是一個一個的執(zhí)行。
這里我們就知道了,正在遞達某個信號期間,同類信號無法被遞達,系統(tǒng)會自動將當(dāng)前信號加入到進程的信號屏蔽字block,當(dāng)信號完成捕捉動作,系統(tǒng)又會自動解除對該信號的屏蔽(一般一個信號被解除屏蔽的時候,會自動遞達當(dāng)前屏蔽信號,如果該信號已經(jīng)被pending的話,就不做任何處理)。
2、可重入函數(shù)
為了理解可重寫入函數(shù),這里我們有一個鏈表,我們做如下操作:
這里我們發(fā)現(xiàn)node2丟失了,?我們代碼也沒有寫錯,而僅僅是在一個mian函數(shù)中執(zhí)行了二個執(zhí)行流。
這里在mian函數(shù)handler中該函數(shù)重復(fù)進入 ,出了問題,我們就稱函數(shù)insert為不可以重入函數(shù)。
這里在mian函數(shù)handler中該函數(shù)重復(fù)進入 ,沒有出問題,我們就稱函數(shù)insert為可以重入函數(shù)。
如果一個函數(shù)符合以下條件之一則是不可重入的:
- 調(diào)用了malloc或free,因為malloc也是用全局鏈表來管理堆的。
- 調(diào)用了標(biāo)準(zhǔn)I/O庫函數(shù)。標(biāo)準(zhǔn) 比特科技 I/O庫的很多實現(xiàn)都以不可重入的方式使用全局?jǐn)?shù)據(jù)結(jié)構(gòu)
3、?volatile關(guān)鍵字
下面我們看一個現(xiàn)象:
int quit = 0;
void handler(int signo)
{
printf("pid: %d, %d 號信號,正在被捕捉!\n", getpid(), signo);
printf("quit: %d", quit);
quit = 1;
printf("-> %d\n", quit);
}
int main()
{
signal(2,handler);
while(!quit);
printf("我是正常退出\n");
}
?
這個結(jié)果是顯而易見。
當(dāng)我們調(diào)整 gcc的優(yōu)化程度,可用man?gcc手冊查看
//調(diào)整優(yōu)化gcc
g++ -o $@ $^ -O3 #-std=c++11
在次運行代碼?
?發(fā)現(xiàn)代碼進入了死循環(huán),這是為什么呢?
這是因為main函數(shù)中有二個流,編譯器認(rèn)為在main執(zhí)行流中quit沒有改,所以編譯器只將物理內(nèi)存中的值由0變1,但是Cpu中寄存器中0沒有變。(編譯器的優(yōu)化)
為了解決編譯器的優(yōu)化這就要用到volatile?關(guān)鍵字
volatile 作用:保持內(nèi)存的可見性,告知編譯器,被該關(guān)鍵字修飾的變量,不允許被優(yōu)化,對該變量的任何操作,都必須在真實的內(nèi)存中進行操作
volatile int quit = 0;
在次運行代碼:問題就解決了文章來源:http://www.zghlxwxcb.cn/news/detail-752868.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-752868.html
到了這里,關(guān)于[Linux打怪升級之路]-信號的保存和遞達的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!