文章目錄
- 1. 信號的保存
- 1.1 信號的狀態管理
- 2. 信號的處理
- 2.1 用戶態與內核態
- 2.2 信號處理和捕捉的內核原理
- 2.3 sigaction函數
- 3. 可重入函數
- 4. Volatile
- 5. SIGCHLD信號
- 序:在上一章中,我們對信號的概念及其識別的底層原理有了一定認識,也知道了信號產生的五種方式,以及core dump是什么,而本章將著重對信號的保存與處理進行講解,去深入了解信號處理的底層邏輯,去了解什么是用戶態和內核態,以及用戶態和內核態轉換的時機,本章還會淺談可重入函數以及Volatile關鍵字
1. 信號的保存
1.1 信號的狀態管理
對于普通信號而言,對于進程(是給進程的PCB發)而言,要識別自己有沒有收到信號,以及收到了哪一個信號。
task_struct{
int signal;// 0000 ...... 0000普通信號,位圖管理信號
}
1. 比特位的內容是0還是1,表明是否收到
2. 比特位的位置(第幾個),表明信號的編號
3. 所謂的 “發信號” ,本質就是操作系統去修改task_struct的信號位圖對應的比特位(寫信號!!!)
問題一:信號為什么要保存?
進程收到信號之后,可能不會立即處理這個信號。此時信號不會被處理,就要有一個時間窗口。信號的范圍[1,31],每一種信號都要有自己的一種處理方法.
信號的幾種狀態:
實際執行信號的處理動作稱為信號遞達(Delivery)
信號從產生到遞達之間的狀態,稱為信號未決(Pending)。
進程可以選擇阻塞 (Block )某個信號。
被阻塞的信號產生時將保持在未決狀態,直到進程解除對此信號的阻塞,才執行遞達的動作.
注意,阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是在遞達之后可選的一種處理動作。
信號在內核中的表示示意圖:
每個信號都有兩個標志位分別表示阻塞(block)和未決(pending),還有一個函數指針表示處理動作。信號產生時,內核在進程控制塊中設置該信號的未決標志,直到信號遞達才清除該標志。在上圖的例子中,SIGHUP信號未阻塞也未產生過,當它遞達時執行默認處理動作。
SIGINT信號產生過,但正在被阻塞,所以暫時不能遞達。雖然它的處理動作是忽略,但在沒有解除阻塞之前不能忽略這個信號,因為進程仍有機會改變處理動作之后再解除阻塞。
SIGQUIT信號未產生過,一旦產生SIGQUIT信號將被阻塞,它的處理動作是用戶自定義函數sighandler。如果在進程解除對某信號的阻塞之前這種信號產生過多次,將如何處理?POSIX.1允許系統遞送該信號一次或多次。Linux是這樣實現的:常規信號在遞達之前產生多次只計一次,而實時信號在遞達之前產生多次可以依次放在一個隊列里。本章不討論實時信號。
sigpending函數:
其中的set參數是一個輸出型參數,他會將當前的pending表傳出。
sigprocmask函數:
第一個參數how:
參數 | 含義 |
---|---|
SIG_BLOCK | set包含了我們希望添加到當前信號屏蔽字的信號 |
SIG_UNBLOCK | set包含了我們希望從當前信號屏蔽字中解除阻塞的信號 |
SIG_SETMASK | 設置當前信號屏蔽字為set所指向的值 |
第二個參數:表示要傳入的block阻塞表
第三個參數:表示之前的block舊表
問題二:我要是將所以信號都進行屏蔽,信號不就不會被處理了嗎?
然而操作系統不會給你這個機會的,比如9號信號和19號信號,用戶就屏蔽不了
2. 信號的處理
2.1 用戶態與內核態
問題一:信號是什么時候被處理的?
當我們的進程從內核態返回到用戶態的時候,進行信號好的檢測和處理
當我們在調用系統調用時,操作系統會將我們的用戶身份轉化為內核身份,然后由操作系統幫我把函數執行完,返回時,再把我的內核身份換回用戶身份 ------ 操作系統是自動會做“身份”切換的,用戶身份變成內核身份,或者反著來!
問題二:什么是用戶態和內核態?
內核態:允許你訪問操作系統的代碼和數據
用戶態:只能訪問用戶自己的代碼和數據
2.2 信號處理和捕捉的內核原理
問題三:信號是如何被處理的?
操作系統不信任用戶,不僅僅體現在不讓用戶訪問自己,也體現在操作系統不會訪問用戶自己寫的代碼!!!所以當在內核態處理信號時,會先將該信號的pending置0,然后去執行,要是執行到了自定義行為,那么進程就要先由內核狀態轉化為用戶態,去執行這個自定義行為,然后再變為內核態繼續在操作系統中執行系統操作,然后再回到用戶態,返回值。(基于用戶捕捉代碼)
問題四:內核如何實現信號的捕捉
如果信號的處理動作是用戶自定義函數,在信號遞達時就調用這個函數,這稱為捕捉信號。由于信號處理函數的代碼是在用戶空間的,處理過程比較復雜,舉例如下: 用戶程序注冊了SIGQUIT信號的處理函數sighandler。 當前正在執行main函數,這時發生中斷或異常切換到內核態。 在中斷處理完畢后要返回用戶態的main函數之前檢查到有信號SIGQUIT遞達。 內核決定返回用戶態后不是恢復main函數的上下文繼續執行,而是執行sighandler函 數,sighandler和main函數使用不同的堆棧空間,它們之間不存在調用和被調用的關系,是 兩個獨立的控制流程。 sighandler函數返回后自動執行特殊的系統調用sigreturn再次進入內核態。 如果沒有新的信號要遞達,這次再返回用戶態就是恢復main函數的上下文繼續執行了。
在這個過程中,一共發生了四次狀態的轉換,所謂的信號的識別,其實就是在進程進入內核態時,順手完成的任務。CPU內部的信號int 80(是一條匯編語句) 從用戶態陷入內核態,這樣就有權利去訪問操作系統的數據了。
2.3 sigaction函數
sigaction函數:
sigaction結構體:
struct sigaction {void (*sa_handler)(int);void (*sa_sigaction)(int, siginfo_t *, void *);sigset_t sa_mask;int sa_flags;void (*sa_restorer)(void);};
由于我們目前只研究普通信號,所以,該結構體當中的第二、第四和第五個參數我們不做討論,感興趣的小伙伴可以自行去搜索。
該函數的第一個參數是要處理的信號數字,第二個參數表示要傳入的sigaction結構體,第三個參數是輸出型參數,他會保存上一次的sigaction結構體的數據。
直觀的代碼和運行結果能讓我們直接看到這個函數的作用:
#include<iostream>
#include<cstring>
#include<signal.h>
#include<unistd.h>using namespace std;void PrintPending()
{sigset_t myset;sigpending(&myset);for(int signo=31;signo>=1;signo--){if(sigismember(&myset,signo)){cout<<"1";}else{cout<<"0";}}cout<<endl;
}void handler(int signo)
{while(true){PrintPending();cout<<"signal : "<<signo<<" acted"<<endl;sleep(1);}
}int main()
{signal(SIGINT,handler);struct sigaction myset,oset;memset(&myset,0,sizeof(myset));memset(&myset,0,sizeof(oset));//sigemptyset(&myset.sa_mask);//sigaddset(&myset.sa_mask,SIGINT);myset.sa_handler=handler;sigaction(SIGINT,&myset,&oset);while(true){cout<<"i am process: "<<getpid()<<endl;sleep(1);}return 0;
}
運行結果如下:
由上圖可知,在處理某個信號之前,該信號的pending表對應的值會先置0,然后才會執行對應的處理行動。
總結:如果某個信號正在進行處理,那么,在這個期間,這個信號的信號屏蔽字將會變成1,此時,無論外界再發送多少個該信號,都不會執行,只會將該信號對應的pending表中的值置1,但是不是執行,因為此時該信號已經阻塞了,這就防止了當該信號處理時,被重復執行,之后當這個信號處理完畢,才會取消阻塞,然后繼續執行之前發的信號,并在信號執行期間繼續阻塞該信號!!!
3. 可重入函數
當我們執行insert函數時,進行到一半,執行完p->next=head;語句后,觸發了信號,執行此時信號中也有insert函數,然后執行信號中的insert,執行信號處理后,再繼續執行main函數中的語句head=p;此時,由于函數重入,引發節點丟失,導致內存泄漏!!!
如果一個函數,被重復進入的情況下,出錯了,或者可能出錯,就叫做不可重入函數。否則就叫做可重入函數。
4. Volatile
在g++中是有優化選項的,編譯器在編譯的時候,有不同的優化級別
源代碼:
#include<iostream>
#include<cstring>
#include<signal.h>
#include<unistd.h>using namespace std;int flag=0;void handler(int signo)
{cout<<" change flag 0->1 "<<endl;flag=1;
}int main()
{signal(SIGINT,handler);while(!flag);cout<<"process quit"<<endl;return 0;
}
1. O0的優化:
sigproc:sigproc.cppg++ -o $@ $^ -O1 -g -std=c++11
結果如圖:
2. O1的優化:
sigproc:sigproc.cppg++ -o $@ $^ -O1 -g -std=c++11
結果如圖:
3. O3的優化:
sigproc:sigproc.cppg++ -o $@ $^ -O3 -g -std=c++11
結果如圖:
問題一:為什么優化過后,程序退不出去?
此時volatile int flag=0;使用volatile關鍵字修飾flag就能防止編譯器過度優化,保持內存的可見性!!!
5. SIGCHLD信號
當子進程退出時,不是靜悄悄的退出,子進程在退出的時候,會主動向父進程發送SIGCHLD(17號)信號。所以在進行進程等待的時候,我們可以采用基于信號的方式進行等待
問題一:等待的好處是什么?
1. 獲取子進程的退出狀態,釋放子進程的僵尸。
2. 雖然不知道父子進程誰先運行(由調度器決定),但是我們清楚,一定是父進程先退出!!!
所以,還是要調用wait/waitpid這樣的接口,且父進程必須保證自己是一直在運行的!
問題二:我們必須等待嗎?必須調用wait嗎?
Signal(17,SIG_IGN);表示忽略17號信號,子進程會自動退出!!!
總結:
本章帶大家理解了信號的保存與處理,知道了信號的不同的保存狀態,以及處理信號時的內核態和用戶態的轉化原理,還擴展了可重入函數和Volatile關鍵字,以及SIGCHLD信號,最后,希望這篇文章對大家有一定幫助。