目錄
?一 前言
二? 信號在內核中的表示
三 sigset_t? ?
四?信號集操作
1. sigpending()?
2. sigemptyset()
3.?sigfillset()
4.?sigaddset ()和sigdelset()?
5.?sigismember()
6.?sigprocmask()
五 深入理解信號的捕捉流程?
?一 前言
在Linux: 進程信號初識-CSDN博客信號的初識這一篇我們已經了解了什么是信號,和信號的產生及信號的捕捉,但是那些都是信號在用戶層的理解,同時也產生了幾個問題:?
-
之前講到的所有進程信號的產生都需要OS來執行,為什么?
-
我們提到進程在接收到信號之后通常有著三種處理方式(1. 忽略此信號。 2. 執行該信號的默認處理動作。 3. 自定義處理動作),那么針對這三種處理方式,進程是立即處理的嗎?如果不是立即處理,那么信號是否需要暫時被進程記錄下來?記錄下來放在哪里呢?
-
一個進程在沒有收到信號的時候,怎么知道自己應該對合法信號作何處理呢?
-
怎么理解OS向進程發送信號?是怎么發送的?具體情況是什么?
接下來我們將從系統內核層面著重討論和理解進程信號產生之后進程處理信號的詳細操作以及進程信號的產生到進程接收之間內核做了哪些事情。
為了后續學習,我們需要知道信號其他相關概念
- 執行信號的處理動作稱為信息遞達(Delivery)
- 信號從產生到遞達之間的狀態成為信號未決(Pending)
- 進程可以選擇阻塞(Block)某個信號。
- 被阻塞的信號產生時將保持在未決狀態,直到進程接觸對信號的阻塞。才執行遞達的動作。
- 阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是遞達之后的可選的一種處理動作。
二? 信號在內核中的表示
在上篇Linux: 進程信號初識-CSDN博客,我們簡單提到過,進程信號保存是以位圖的方式存儲在進程的PCB中的,通常每個信號對應位圖中的一個特定位置,即一個比特位,如果該比特位設置為1,表示對應信號已經收到但是尚未處理;若為0.則表示沒有收到該信號。事實上,在PCB中描述著一個有關進程信號的位圖和一個有關進程信號的指針數組如下
- 每個信號都有兩個標志位分別表示阻塞(block)和未決(pending),還有一個函數指針表示處理動作。信號產生時,內核在進程控制塊中設置該信號的未決標志(pending),位圖比特位設為1,直到信號遞達才清除該標志。在上圖的例子 中,SIGHUP信號未阻塞也未產生過,當它遞達時執行默認處理動作。
- SIGINT信號產生過,但正在被阻塞,所以暫時不能遞達。雖然它的處理動作是忽略,但在沒有解除阻塞之前 不能忽略這個信號,因為進程仍有機會改變處理動作之后再解除阻塞。
- SIGQUIT信號未產生過,一旦產生SIGQUIT信號將被阻塞,也就是說阻塞信號,可以在沒有信號傳遞時就可以阻塞,它的處理動作是用戶自定義函數sighandler。
- 如果在進程解除對某信號的阻塞之前這種信號產生過多次,將如何處理? POSIX.1允許系統遞送該信號一次 或多次。Linux是這樣實現的:常規信號在遞達之前產生多次只計一次,而實時信號在遞達之前產生多次可 以依次放在一個隊列里。本章不討論實時信號。
-
pending位圖:pending表示信號未決,所以它是未決位圖,用來表示進程收到了信號,對應位置即為對應編號的信號,當該位圖中的某個位置設置為1時,即表示此位的信號在進程中處于未決狀態,即接收到了信號但是還未處理。
-
handler指針數組:顯而易見,存儲的是信號處理方法的數組,每位對應一個處理方法。
-
block位圖:阻塞位圖,表示對應位置的進程信號是否阻塞,當指定位置為1時,即表示此位置的信號會被阻塞??
三 sigset_t? ?
?從上面來看,pending位圖和block位圖所表示的信息能力都是有限的,其每一位的0 1都只能表示進程信號是否存在或者阻塞并不能表示有多少信號產生?并?發送給了進程。
在Linux操作系統中pending和block并不是以整型來表示位圖的,而是以一個結構體的形式sigset_t
sigset_t?是一個 typedef 出來的類型, 實際上是一個結構體?__sigset_t,?不過這個結構體內部只有一個?unsigned long int?類型的數組 ,也就是說pending和block位圖其實是以數組的形式表現出來的。
其中, 實際以 sigset_t 形式表現的 pending位圖, 被稱為未決信號集; 同樣以 sigset_t 形式表現的 block位圖, 被稱為阻塞信號集, 也叫信號屏蔽集。
四?信號集操作
信號集實際上是以數組來表示位圖的,且系統為用戶提供了相關的系統調用接口:
int sigpending(sigset_t *set);
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);
1. sigpending()?
該接口的作用是檢查未決信號集,即獲取進程的未決信號集。其參數是一個輸出型參數,獲取的未決信號集的內容會存儲在傳入的變量中,成功則返回0,錯誤返回-1.?
2. sigemptyset()
調用此接口會將傳入的信號集初始化為空, 即所有信號、阻塞會被消除, 信號集的所有位設置為0,成功返回0, 錯誤返回-1
3.?sigfillset()
調用此函數,?會將傳入的信號集所有位設置為1.成功返回0, 錯誤返回-1
4.?sigaddset ()和sigdelset()?
前者的作用是,?給指定信號集中添加指定信號, 即將指定信號集中的指定位置設置為1
后者的作用是,?刪除指定信號集中的指定信號, 即將指定信號集中的指定位置設置為0
這兩個函數, 都是成功返回0, 失敗返回-1
5.?sigismember()
調此函數, 可以判斷信號集中是否有某信號??即??判斷信號集的某位是否為1
如果信號在信號集中返回1,如果不在返回0,如果出現錯誤 則返回-1
6.?sigprocmask()
這個接口的作用是:獲取?和?修改?信號屏蔽集合(阻塞信號集)?
如果oset是非空指針,則讀取進程的當前信號屏蔽字通過oset參數傳出。如果set是非空指針,則 更改進程的信 號屏蔽字,參數how指示如何更改。如果oset和set都是非空指針,則先將原來的信號 屏蔽字備份到oset里,然后 根據set和how參數更改信號屏蔽字。假設當前的信號屏蔽字為mask,下表說明了how參數的可選值。
下面我用圖片的形式 解釋一下:
? 1.為指定位置添加阻塞
2.為指定信號解除阻塞
3.直接設置信號屏蔽字
?直接將傳入的
set覆蓋進程原來的信號屏蔽字
,將傳入的set作為進程新的信號屏蔽字
?
?測試:
#include <iostream>
#include <signal.h>
#include <unistd.h>#define BLOCK_SIGNAL 2 //需要屏蔽信號2
#define MAX_SIGNUM 31static void show_pending(const sigset_t & pending)
{for(int signo=MAX_SIGNUM;signo>=1;signo--){if(sigismember(&pending,signo)){std::cout<<"1";}else std::cout<<"0";}std::cout<<"\n";
}int main()
{//先嘗試屏蔽指定信號sigset_t block,oblock,pending;//1.初始化sigemptyset(&block);sigemptyset(&oblock);sigemptyset(&pending);//1.2添加要屏蔽的信號sigaddset(&block,BLOCK_SIGNAL);//1.3開始屏蔽sigprocmask(SIG_SETMASK,&block,&oblock);//2.遍歷打印pending 信號集while(true){//2.1初始化sigemptyset(&pending);//2.2獲取sigpending(&pending);//2.3打印show_pending(pending);sleep(1);}return 0;
}
五 深入理解信號的捕捉流程?
在前言部分,我們曾說過信號產生的時候,不會立即被處理,而是在合適的時候,那是在什么時候呢?
🍉:從內核態返回用戶態的時候,進行處理。所以什么是內核態?什么又是用戶態呢?
1.進程的內核態與用戶態
我們知道對于系統中的每一個進程其都有自己的一份獨立的程序地址空間
且進程地址空間與物理內存是通過頁表映射的。但是之前講到的Linux : 進程地址空間-CSDN博客只是用戶空間部分與物理內存之間的相互映射。?
事實上 ,對于1GB的內核空間,也存在著一張頁表,用于內核空間和物理內存之間的相互映射,稱為內核級頁表
如上圖所示,所有的進程都用著同一張內核級頁表,也就是說每個進程的內核空間的內容是一樣的,也就是說物理內存中只加載著一份有關于進程內核空間內容的數據代碼。
如果每個進程都可以訪問及隨意修改內核空間中的數據代碼,這是一件很恐怖的事情,畢竟操作系統做了那么多工作,提供了那么多系統調用封裝了那么多系統接口,就是為了不讓用戶直接操作系統內核。所以為了保護這部分數據代碼,進程會分為兩種狀態: 內核態 和 用戶態
🌔當進程 需要訪問、調用、執行 內核數據或 代碼(系統調用等)時, 就會 陷入內核, 轉化為內核態, 因為只有進程處于內核態時, 才有權限訪問內核級頁表, 即有權限訪問內核數據與代碼。
🌖當進程不需要訪問、調用、執行內核數據或代碼或系統調用結束時, 就會返回用戶, 轉化為用戶態 , 此時 進程將不具備訪問內核級頁表的權限, 只能訪問用戶級頁表
?那么系統如何分清當前的進程處于哪一種狀態下呢?
事實上,在CPU內部存在著一個 狀態寄存器CR3,此寄存器內有比特標識位標識當前進程的狀態。
若標識位 表示0, 則表明進程此時處于內核態
若標識位 表示3, 則表明進程此時處于用戶態?
在操作系統中,當進程處于運行時,它會有兩種狀態,用戶態和內核態,且在進程的整個周期內會發生著無數次的狀態轉換。我們平常寫的代碼大部分情況下是沒有資格直接訪問系統的軟硬件資源的 ,本質上我們都是通過調用系統所提供的接口,通過系統去訪問這些資源,這樣的情況下,進程需要訪問硬件資源地時候,就會無數次地陷入內核(切換狀態、切換頁表),再訪問內核代碼數據, 然后完成訪問,再將結果返回給用戶(切換狀態,切換頁表),最終用戶得到結果。
還有一種情況,在用戶不調用任何函數的時候,這時候還會發生進程狀態的轉換嗎?答案是會的。因為只要是進程, 那么他就有一定的時間片. 即使是一個什么都不執行的死循環, 只要時間片用完了, 那么就需要將此進程從CPU上剝離下來, 而剝離操作一定是操作系統做的, 那么也就是說將 進程從CPU上剝離下來也是需要陷入內核執行內核代碼的. 將進程從CPU上剝離下來的時候, 需要維護一下進程的上下文, 以便下次接著執行進程的代碼.剝離下來之后, 操作系統執行調度算法, 將下一個需要運行的進程的上下文加載到CPU中。
🍰:下面我們舉個例子詳細分析一下在進程切換狀態的時候,信號在什么時候處理。
如圖所示:
- 代碼在運行到需要執行系統調用signal()接口的時候,此時進程就需要陷入內核態執行signal()代碼
- 陷入內核并執行完signal()代碼后, 需要將signal()結果返回給用戶, 需要轉換回用戶態
- 在轉換回用戶態之前, 需要先在進程PCB中檢測進程的未決信號集
- 在未決信號集中, 檢測到1和2信號未決, 并且均未被屏蔽(阻塞). 就需要在handler數組中尋找指定的處理方法
- 1信號默認處理, 需要執行內核中的默認處理方法(一般為進程終止); 2信號忽略處理, 直接將未決信號集中2信號改為0
- 處理完信號, 再將signal()結果返回給用戶, 這個過程需要轉換為用戶態
🍅:上面的信號的處理方式都是默認或者是忽略,但是如果我們捕捉了一個信號并且讓它按照自定義的方式處理,這時候在最后一步怎么辦?
? ? 進程首先會從內核態切換為用戶態去執行用戶自定義的信號處理動作,(雖然內核態也能處理用戶態的代碼,但是操作系統不會這樣做,因為萬一用戶自己寫個Bug,內核態去執行的話會影響操作系統。)
? ? ? ? ? ?其次進程現在是用戶態,此時進程是無法返回到進程原本代碼的執行位置的。因為進程執行系統調用之后的返回信息還在內核中,以用戶態的身份是無法訪問并返回給用戶的。?所以, 進程還需要再次陷入內核,轉換成內核態 然后根據內核中的返回信息使用特定的返回調用 返回到用戶.
圖示如下:
🍎可以看到如果處理信號需要執行用戶自定義的處理方法
時, 那么 從調用內核代碼到返回用戶的整個過程一共需要經歷4次狀態轉換
🍏而, 如果處理信號不需要執行用戶自定義處理方法
時, 那么 從調用內核代碼到返回用戶的整個過程 就只需要經歷2次狀態轉換
簡化圖如下:
縮略圖: