信號的處理
- 1 信號的處理
- 2 內核態 VS 用戶態
- 3 鍵盤輸入數據的過程
- 4 如何理解OS如何正常的運行
- 5 如何進行信號捕捉
- 信號處理的總結
- 6 可重入函數
- volatile關鍵字
- Thanks?(・ω・)ノ謝謝閱讀!!!
- 下一篇文章見
1 信號的處理
處理信號本質就是遞達這個信號!首先我們來看如何進行捕捉信號:信號的處理有三種:
signal(2 , handler);//自定義
signal(2 , SIG_IGN);//忽略
signal(2 , SIG_DFL);//默認
注意handler
表是函數指針表,傳入的參數一定是函數指針類型!!!
我們說過:信號可能不會被立即處理,而是在合適的時候進行處理。那么這個合適的時候到底是什么時候?!
進程從內核態(處于操作系統的狀態)返回到用戶態(處在用戶狀態)的時候進行處理!
- 首先用戶運行一個進程,在執行代碼指令時因為中斷,異常或者系統調用進如操作系統。
- 進入操作操作系統就變為內核態,操作系統處理完之后,就對進程的三張表進行檢查:如果pending中存在,繼續判斷,如果被block了了就不進行處理,反之執行對應方法!
- 執行對應的方法時,如果是自定義方法,會返回到用戶層面的代碼,執行對應的方法。然后通過系統調用再次回到內核態。
- 進入內核態之后,再返回到原本的用戶指令位置中
注意:
- 操作系統不能直接轉過去執行用戶提供的handler方法!因為操作系統權限太高了,必須回到用戶權限來執行方法!
- 類似一個∞符號:
2 內核態 VS 用戶態
再談地址空間
這樣無論進程如何切換,都可以找到OS!!!
所以我們訪問OS,其實還是在我們的地址空間進行的,和訪問庫函數沒有區別!OS不相信任何用戶,用戶訪問[3 , 4]地址空間,要受到一定約束(只能通過系統調用!)
3 鍵盤輸入數據的過程
操作系統如何知道我們按下鍵盤呢?肯定不能是每一時刻都進行檢查,這樣消耗太大!
在CPU中,鍵盤按下時會向cpu發送硬件中斷,CPU就會讀取中斷號讀到寄存器中,CPU會告訴OS,后續通過軟件來讀取寄存器。
內存中,操作系統在啟動時就會維護一張函數指針數組(中斷向量表),數組下標是中斷號,數組內容是讀磁盤函數,讀網卡函數等方法。每個硬件都有自己的中斷號,鍵盤也是。按下鍵盤時,向CPU發送中斷信號,然后調用鍵盤讀取方法,將鍵盤數據讀取到內存中!這樣就不需要輪詢檢查鍵盤是否輸入了!
4 如何理解OS如何正常的運行
根據我們使用電腦的經驗,電腦開機到關機的過程中,本質一定是一個死循環。那這死循環是如何工作的呢?那么CPU內部有一個時鐘,可以不斷向CPU發送中斷(例如每隔10納秒),所以CPU可以被硬件推動下在死循環內部不斷執行中斷方法。來看Linux內核:
在操作系統的主函數中,首先是進行一些初始化(包括系統調用方法),然后就進入到了死循環!
操作系統本質是一個死循環 + 時鐘中斷 (不斷調度系統任務)
那么系統調用時什么東西呢?
在操作系統內部,操作系統提供給我們一張表:系統調用函數表
平時我們用戶層使用的fork , getpid , dup2...
等都對應到底層的sys_fork , sys_getpid ...
。只有我們找到特定數組下標(系統調用號)的方法,就能執行系統調用了!
回到之前的函數指針數組,我們在這里再添加一個新方法,用來調度任何的系統調用。使用系統調用就要有:
- 系統調用號
- 系統調用函數指針表(操作系統內部)
用戶層面如何使用到操作系統中的函數指針表呢?
這就要回到CPU中來談,CPU中兩個寄存器,假設叫做X 和 eax
,當用戶調用fork
時,函數內部有類似
mov 2 eax //將系統調用號放入寄存器中
而所謂的中斷不也是讓CPU中的寄存器儲存一個中斷號來進行調用嗎!那CPU內部可不可以直接寫出數字呢?可以,當eax獲取到數字時,寄存器X就會形成對應的數字,來執行操作系統的系統調用。
通過這種方法就可以通過用戶的代碼跳轉到內核,來執行系統調用。但操作系統不是不相信任何用戶嗎?怎么就直接跳轉了呢?用戶是無法直接跳轉到內存中的內核空間(3~4GB)。那么就有幾個問題:
- 操作系統如何阻止用戶直接訪問?
- 系統調用最終是可以被調用的,又是如何做到的?
在操作系統中,解決這兩種問題是非常復雜的!有很多概念,所以簡單單來講:做到這些需要硬件CPU配合,在CPU中存在一個寄存器code semgent
記錄代碼段的起始與終止地址。就可以通過兩個cs寄存器來分別儲存用戶與操作系統的代碼!CS寄存器中單獨設置出兩個比特位來記錄是OS還是用戶,這樣就要區分了內核態和用戶態。運行代碼時就會檢測當前權限與代碼權限是否匹配,進而做到阻止用戶直接訪問
。而當我們調用系統調用(中斷,異常)時,會改變狀態,變成內核態,此時就可以調用系統調用
5 如何進行信號捕捉
今天我們來認識一個新的系統調用:
NAMEsigaction, rt_sigaction - examine and change a signal actionSYNOPSIS#include <signal.h>int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
使用方法和signal很像,先介紹struct 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);
};
在這其中我們只需要注意 void (*sa_handler)(int);
,這是個函數指針,就是自定義捕捉的函數方法。這樣看來是不是就和signal很類似了
再來看看參數
- int signum : 表示要對哪個信號進行捕捉
- const struct sigaction *act : 輸入型參數,表示要執行的結構體方法
- struct sigaction *oldact: 輸出型參數,獲取更改前的數據
我們寫一段代碼來看看:
// 創建一個進行,進入死循環
// 對2號信號進行自定義捕捉void handler(int signum)
{std::cout << "get a sig : " << signum << " pid: " << getpid() << std::endl;
}int main()
{struct sigaction act, oact;// 自定義捕捉方法act.sa_handler = handler;sigemptyset(&act.sa_mask);act.sa_flags = 0;sigaction(2, &act, &oact);while (true){std::cout << "I am a process... pid: " << getpid() << std::endl;sleep(1);}return 0;
}
我們運行看看:
這樣就成功捕捉了2號信號!用起來和之前的signal很類似!那么我們介紹這個干什么呢?我們慢慢來說:
首先信號處理有一個特性,比如我們在處理二號信號的時候,默認會對二號信號進行屏蔽!對2號信號處理完成的時候,會自動解除對2號信號的屏蔽!也就是操作系統不允許對同一個信號進行遞歸式的處理!!!
我們來簡單驗證一下:我們在handler方法中進行休眠,看看傳入下一個2號信號是否會進行處理
void handler(int signum)
{std::cout << "get a sig : " << signum << " pid: " << getpid() << std::endl;sleep(100);
}
來看:
可見進程就屏蔽了對2號信號的處理!
我們之前學習過三張表:阻塞,未決和抵達
既然操作系統對信號進行來屏蔽,那么再次傳入的信號應該就會被記錄到未決表(pending表)中,我們打印這個表來看看:
void Print(sigset_t &pending)
{for (int sig = 31; sig > 0; sig--){if (sigismember(&pending, sig)){std::cout << 1;}else{std::cout << 0;}}std::cout << std::endl;
}void handler(int signum)
{std::cout << "get a sig : " << signum << " pid: " << getpid() << std::endl;while (true){// 建立位圖sigset_t pending;// 獲取pendingsigpending(&pending);Print(pending);}
}
來看:
可以看的我們在傳入2號信號時就進入到了未決表中!處理信號完畢,就會解除屏蔽!
接下來我們既可以來介紹sa_mask
了,上面只是對2號信息進行了屏蔽,當我傳入3號新號ctrl + \
時就正常退出了,那么怎么可以在處理2號信號時屏蔽其他信號呢?就是通過sa_mask
,將想要屏蔽的信號設置到sa_mask
中,就會在處理2號信號的時候,屏蔽所設置的信號!
int main()
{struct sigaction act, oact;// 自定義捕捉方法act.sa_handler = handler;sigemptyset(&act.sa_mask);//向sa_mask中添加3號信號sigaddset(&act.sa_mask , 3);act.sa_flags = 0;sigaction(2, &act, &oact);while (true){std::cout << "I am a process... pid: " << getpid() << std::endl;sleep(1);}return 0;
}
這樣就也屏蔽了3號信號
當然如果把所有信號都屏蔽了,肯定是不行的,所以有一部分信號不能被屏蔽,比如9號信號永遠都不能屏蔽!!!
信號處理的總結
對于信號我們學習了三個階段:
- 信號的產生與發送:中斷,異常,系統調用。
- 信號的保存:三張表:阻塞,未決和遞達
- 信號的處理
6 可重入函數
介紹一個新概念:可重入函數。
我們先來看一個情景:
這是一個鏈表,我們的inser函數會進行一個頭插,頭插會有兩行代碼:
void insert(node_t* p)
{p->next = head;//------在這里接收到信號-----head = p;
}
我們進行頭插時,進行完第一步之后,突然來了一個信號,但是我們之前說過:信號處理時在用戶態到內核態進行切換時才進行處理,這鏈表的頭插沒有進行狀態的切換啊?其實狀態的切換不一定只能是系統調用方法,在時間片到了(時鐘中斷)之后,也進行了狀態的切換。
而且恰好,該信號的自定義捕捉方法也是insert
這時就導致node2插入到了鏈表中,信號處理完之后,頭指針又被掰到node1了,就造成node2丟失了(內存泄漏了)!!!
這就叫做insert
函數被重入了!!!
在重入過程中一旦造成了問題,就叫做不可重入函數!!!(因為一旦重入就造成了問題,那當然不能重入了)
絕大部分函數都是不可重入函數!
volatile關鍵字
我們今天在信號的角度再來重溫一下:
volatile 作用:保持內存的可見性,告知編譯器,被該關鍵字修飾的變量,不允許被優化,對該變量的任何操作,都必須在真實的內存中進行操作保持數據可見性!
看這樣一段代碼:
#include <iostream>
#include <signal.h>int flag = 0;
void changdata(int signo)
{std::cout << "get a sig : " << signo << " change flag 0->1" << std::endl;flag = 1;
}int main()
{signal(2 , changdata);while(!flag);std::cout << "process quit normal" << std::endl;
}
主函數會一直進行死循環,只有接收到了2號信號才會退出!
但當我們進行編譯優化時(因為如果進程不接受到2號信號,那么flag就沒有人來修改,編譯器就認為沒有任何代碼對flag進行修改),共同有四級優化00 01 02 03
而while(!flag)是一個邏輯運算,CPU 一般進行兩種類別計算:算術運算和邏輯運算!會從內存進行讀取,然后進行運算
g++ main main.cc -01
我們再次運行,卻發現,進程不會結束了?!這是為什么!因為優化直接將數據優化到寄存中,因為編譯器認為后續不會進行修改,所以寄存器中的值不會改變,程序只會讀到寄存器中的值。所以就有了volatile關鍵字解決了這樣的問題!!!