目錄
一 前言
?二 信號捕捉的方法
1.sigaction()?編輯
2. sigaction() 使用?
?三??可重入函數
四 volatile 關鍵字?
一 前言
如果信號的處理動作是用戶自定義函數,在信號遞達時就調用這個函數,這稱為捕捉信號。在Linux: 進程信號初識-CSDN博客?這一篇中已經學習到了一種信號捕捉的調用接口:signal(),為了深入理解操作系統內核中的信號捕獲機制,我們今天再來看一個接口:sigaction()。
?二 信號捕捉的方法
1.sigaction()
🍪:?sigaction
?作為一個系統調用,用于修改和/或查詢信號的處理方式。相較于?signal
?函數來說,它提供了對信號處理函數更精細的控制。
- int signum:該參數需要傳入指定的進程信號,表示要捕捉的信號。
- const struct sigaction *act:該參數與函數同名,是一個結構體指針。
- void (*sa_handler)(int)? :此成員的含義其實就是自定義處理信號的函數指針;
- void (*sa_sigcation)(int, siginfo_t *, void *): 此成員也是一個函數指針. 但是這個函數的意義是用來 處理實時信號的, 本節不考慮分析。
- sigset_t sa_mask: 此成員是一個信號集。
- int sa_flags:此成員包含著系統提供的一些選項, 本篇文章使用中都設置為0
- void (*sa_restorer)(void):很明顯 此成員也是一個函數指針. 但我們暫時不考慮他的意義.
也就是說我們暫時將該接口的第二個參數簡單理解為一個結構體指針,并且結構體里有一個成員是用來自定義處理信號的。
- struct sigaction *oldact :第三個參數是輸出型參數,且其作用是獲取?此次修改信號?struct sigaction之前的原本的?struct sigaction?,?如果傳入的是空指針,則不獲取。??
2. sigaction() 使用?
#include <iostream>
#include <signal.h>
#include <unistd.h>using namespace std;//自定義處理動作
void handler( int signo)
{cout<<"get a signno: " <<signo<<endl;
}
int main()
{struct sigaction act,oact;act.sa_handler=handler;//函數指針,自定義處理動作act.sa_flags=0;sigisemptyset(&act.sa_mask);//初始化信號集sa.masksigaction(SIGINT,&act,&oact);while(true) sleep(1);return 0;}
運行結果:
?接下來我們對測試做一點修改
/其實就是相當于在handler中休眠了20s
//計時函數
void Count(int cnt)
{while(cnt){printf("cnt: %d\r",cnt);//\r回車fflush(stdout);//刷新緩沖區cnt--;sleep(1);}printf("\n");
}
//自定義處理動作
void handler( int signo)
{cout<<"get a signno: " <<"正在處理中"<<signo<<endl;Count(20);//休眠20s
}int main()
{struct sigaction act,oact;act.sa_handler=handler;//函數指針,自定義處理動作act.sa_flags=0;sigisemptyset(&act.sa_mask);//初始化信號集sa.masksigaction(SIGINT,&act,&oact);while(true) sleep(1);return 0;}
?運行結果:
?🍉:從運行結果我們可以看到,我們向進程發了大量2信號,但實際上進程只處理了幾次(即20s處理一個信號)。當我們進行正在遞達的某一個信號期間,同類信號無法遞達,這是因為當前信號正在被捕捉時,系統會自動將當前信號加入到進程的信號屏蔽字。(block)
? ? ? ? ? ? ? ? ? ?當信號完成捕捉動作,系統又會自動解除對該信號的信號屏蔽
當我們正在處理某一種信號的時候,我們也想屏蔽其他信號,我們可以利用? sigaction()函數中的一個參數 sa_mask,將額外想屏蔽的信號類型加入到sa_mask.?
int main()
{struct sigaction act,oact;act.sa_handler=handler;//函數指針,自定義處理動作act.sa_flags=0;sigisemptyset(&act.sa_mask);//初始化信號集sa.mask
這是修改部分/sigaddset(&act.sa_mask,3);//除了屏蔽2號我們也想屏蔽3號sigaction(SIGINT,&act,&oact);while(true) sleep(1);return 0;}
?小結:當某個信號的處理函數被調用時,內核自動將當前信號加入進程的信號屏蔽字,當信號處理函數返回時自動恢復原來 的信號屏蔽字,這樣就保證了在處理某個信號時,如果這種信號再次產生,那么 它會被阻塞到當前處理結束為止。 如果 在調用信號處理函數時,除了當前信號被自動屏蔽之外,還希望自動屏蔽另外一些信號,則用sa_mask字段說明這些需 要額外屏蔽的信號,當信號處理函數返回時自動恢復原來的信號屏蔽字。
🍎::signal() 和sigaction() 比較: 正如上文所說?相較于?signal
?函數來說,它提供了對信號處理函數更精細的控制。一個直觀的表現,在調用信號處理函數時,除了當前信號被自動屏蔽之外,還可以自動屏蔽另外一些信號。
?三??可重入函數
可重入函數(Reentrant Function)是指可以在多線程環境中安全使用的函數,即這個函數可以被多個線程同時調用而不會導致數據錯誤或不一致。
下面用個例子解釋一下:
一個進程中, 存在一個單鏈表結構. 并且此時需要執行一個節點的頭插操作:
頭插的操作就是:
node1->next = head;
head = node1;
如果在剛執行完第一步之后, 進程因為硬件中斷或者其他原因?陷入內核了
.
陷入內核之后需要回到用戶態繼續執行, 切換回用戶態時 進程會檢測未決信號, 如果此時剛好存在一個信號未決, 且此信號自定義處理.并且, 自定義處理函數動作也是一個新節點頭插操作:
?那么此時, 就會執行?node2
?的頭插操作, 執行完畢的結果就是:
即,?node2 成為了鏈表的第一個節點 head
,信號處理完畢之后, 需要返回用戶繼續執行代碼, 用戶剛執行完?node1->next = head;
? ?所以下面應該執行?head = node1;
, 結果就成了這樣:
結果就是,?node2
?無法被找到了.造成了內存泄漏問題。
那么造成這個結果的原因是什么?
是因為單鏈表的頭插函數, 被不同的控制流調用了, 并且是在第一次調用還沒返回時就再次進入該函數, 這個行為稱為 重入
像例子中這個單鏈表頭插函數, 訪問的是一個全局鏈表, 所以可能因為重入而造成數據錯亂, 這樣的函數被稱為 不可重入函數, 即此函數不能重入, 重入可能會發生錯誤
反之, 如果一個函數即使重入, 也不會發生任何錯誤,這樣的函數被稱為可重入函數.?
如果一個函數符合以下條件之一,則稱為不可重入函數
調用了malloc和free。
調用了標準I/O庫函數, 標準I/O庫的很多實現都以不可重入的方式使用全局數據結構
四 volatile 關鍵字?
在之前學習C/C++的時候,我們就已經接觸過這個關鍵字了,它的作用是防止編譯器對該關鍵字修飾的變量的優化,確保每次訪問這個變量時都直接從內存中讀取,而不是使用可能存在的寄存器中的緩存值。
接下來我們用一個例子分析一下該關鍵字
#include<stdio.h>#include<signal.h>int quit=0;void handler(int signo){printf(" %d 號信號:正在被捕捉\n",signo);printf("quit :%d",quit);quit=1;printf("->%d\n",quit); }int main(){signal(2,handler);//對2號信號進行捕捉while(!quit);//邏輯反,quit為0 !quit 為真 printf("我是正常退出的!\n");return 0;}
接下來我們做個改動
?測試結果
?我們看到雖然quit 變為1了,但是? 我是正常退出的這句話并沒有打印出來也就是說? ? ? ? ? ? ? ? ? while(!quit);
? printf("我是正常退出的!\n");?這個while循環依然在運行,這是為什么呢?
:? 我們使用了?-03?選項讓編譯器做出了優化?
🍪未優化前:
CPU訪問數據時 會將數據從內存中拿到寄存器中. 然后再根據寄存器中的數據進行處理.所以在優化前,cpu一直在內存中讀取quit值,main()和signal()是兩個執行流,當2號信息傳來的時候,signal()把quit的值改為1,main()執行流中cpu再次從內存讀取quit的值時候已經變為1,跳出循環,所以打印了?我是正常退出的這句話。
🍪優化后:?
所以??volatile 關鍵字的作用就是保持內存可見性