Linux中的信号
Linux中的信號
文章目錄
- Linux中的信號
- 一、信號的概念
- 二、信號的產生
- 三、信號的捕捉
- 四、阻塞信號
- 五、再談捕捉信號
- 六、可重入函數
- 七:volatile
- 八、SIGCHLD信號
一、信號的概念
-
1.信號:信號是進程之間異步通知的一種方式,屬于軟中斷。
-
2.用kill -l來查看系統定義的信號列表:
-
從圖中可以看到每個信號都有一個編號和宏定義的名稱,這些宏都可以在signal.h中找到
-
注意并不是一共有64個信號,自己仔細看,共有62種信號
-
31號信號之前都是不可靠信號,也是非實時信號
-
編號34以上的是實時信號,可靠信號,各種信號各自在什么條件下產生什么默認的動作都可以在signal(7)中查看
3.信號的處理方式:
- 忽略此信號
- 執行給信號的默認動作
- 提供一個信號處理函數,要求用戶在處理該信號時切換到用戶態去執行處理函數,即捕捉信號
二、信號的產生
-
1.通過終端按鍵產生信號:ctrl + c —>2號信號、ctrl + z —>19號信號
-
2.硬件異常產生信號
-
硬件異常被硬件以某種方式檢測到并通知給內核,然后內核向當前進程發送適當的信號。
-
如當前進程執行處以0的指令,CPU的運算單元就會產生異常,內核將這個異常解釋為SIGFPE信號發送給進程。
-
再比如當前進程訪問了非法內存地址,MMU會產異常,內核將這個異常解釋為SIGSEGV信號等。
-
3.調用系統函數向進程發送信號:
-
如在后臺執行死循環程序,然后用kill命令給它發送SIGSEGV信號。
-
其中kill命令是調用kill函數實現的,kill函數可以給一個指定的進程發送一個指定的信號。
-
還有raise函數可以給當前進程發送指定信號(自己給自己發送信號)
-
kill函數:
-
參數:pid:進程pid
sig:信號的編號 -
返回值:成功返回0,失敗返回-1
-
abort函數:
- 這個函數和exit函數一樣,終會成功,沒有返回值
- 4.有軟件條件產生信號
- 如SIGPIPE是一種由軟件條件產生的信號
如:alarm函數會在設定的秒數結束后給當前進程發送SIGLRM信號,是用來終止當前進程的信號
三、信號的捕捉
1.例
#include <stdio.h> #include <signal.h>void handler(int sig) {printf("catch a sig : %d\n", sig); }int main() {signal(2, handler); //前文提到過,信號是可以被自定義捕捉的,siganl函數就是來進行信號捕捉的,while(1);return 0; }
模擬野指針異常:
總結:
- 信號處理函數是一個獨立的執行流,和原來的代碼間沒有調用關系,他是由內核直接調用的,在處理信號處理函數期間,原有的執行流就阻塞,等待信號處理函數執行結束,才繼續執行原有執行流
- 上面所說的所有的信號產生,最終都由操作系統來執行,是因為操作系統是進程的管理者
- 信號函數的處理時間:是由操作系統決定,在合適的時候操作系統進行調用
四、阻塞信號
- 進程可以選擇阻塞某個信號
- 被阻塞的信號產生時將保持在未決信號集中,直到解除對此信號的阻塞,才執行遞達的動作
- 注意阻塞的信號和忽略的信號是不同的,只要信號被阻塞就不會遞達,而忽略是在遞達之后可選的一種處理動作
- 每個信號都有兩個標志位分別表示阻塞(block)和未決(pending),還有一個函數指針表示處理動作。
- 信號產生時,內核在進程控制塊中設置該信號的未決標志,直到信號遞達才清除該標志。
- 在上圖的例子中,SIGHUP信號未阻塞也未產生過,當它遞達時執行默認處理動作。
- SIGINT信號產生過,但正在被阻塞,所以暫時不能遞達。
- 雖然它的處理動作是忽略,但在沒有解除阻塞之前不能忽略這個信號,因為進程仍有機會改變處理動作之后再解除阻塞。
- SIGQUIT信號未產生過,一旦產生SIGQUIT信號將被阻塞,它的處理動作是用戶自定義函數sighandler。
- 如果在進程解除對某信號的阻塞之前這種信號產生過多次,將如何處理?
- POSIX.1允許系統遞送該信號一次或多次。Linux是這樣實現的:常規信號(31號之前)在遞達之前產生多次只計一次,而實時信號(34號之后)在遞達之前產生多次可以依次放在一個隊列里
- 3.信號集的相關函數:
- 每個信號只有一個bit的未決標志,非0即1,不記錄該信號產生了多少次,阻塞標志也是這樣表示的。
- 因此,未決和阻塞標志可以用相同的數據類型sigset_t來存儲,sigset_t稱為信號集,這個類型可以表示每個信號的“有效”或“無效”狀態,在阻塞信號集中“有效”和“無效”的含義是該信號是否被阻塞
- 而在未決信號集中“有效”和“無效”的含義是該信號是否處于未決狀態
- 阻塞信號集也叫做當前進程的信號屏蔽字(Signal Mask),這里的“屏蔽”應該理解為阻塞而不是忽略
| int sigemptyset(sigset_t *set); | 函數sigemptyset初始化set所指向的信號集,使其中所有信號的對應bit清零,表示該信號集不包含 任何有效信號 |
| int sigfillset(sigset_t *set); | 函數sigfillset初始化set所指向的信號集,使其中所有號的對應bit位置為1,表示該信號集的有效信號包括系統支持的所有信號 |
| int sigaddset (sigset_t *set, int signo); | 將指定信號加入到信號集合中去 |
| int sigdelset(sigset_t *set, int signo); | 將指定信號從信號集中刪去 |
| int sigismember(const sigset_t *set, int signo) | 查詢指定信號是否在信號集合之中 |
- 注意,在使用sigset_ t類型的變量之前,一定要調 用sigemptyset或sigfillset做初始化,使信號集處于確定的狀態。
- 初始化sigset_t變量之后就可以在調用sigaddset和sigdelset在該信號集中添加或刪除某種有效信號,前4個函數的返回值都是成功返回0,出錯返回-1
- sigismember是一個布爾函數,用于判斷一個信號集的有效信號中是否包含某種 信號,若包含則返回1,不包含則返回0,出錯返回-1
- sigprocmask函數:
- sigpending函數:
讀取當前進程的未決信號集,通過set參數傳出。調用成功則返回0,出錯則返回-1
測試代碼:
#include <stdio.h> #include <unistd.h> #include <signal.h>void Printsigset(sigset_t* set) {int i = 0;for(;i < 32;i++){if(sigismember(set,i)){putchar('1');}else{putchar('0');}}puts(""); }int main() {sigset_t s;sigset_t p;sigemptyset(&s);sigaddset(&s,SIGINT);sigprocmask(SIG_BLOCK,&s,NULL);while(1){sigpending(&p);Printsigset(&p);sleep(1);}return 0; } #include <stdio.h> #include <unistd.h> #include <signal.h>void handler(int sig) {printf("catch a sig : %d\n",sig); }int main() {signal(2,handler);signal(40,handler);sigset_t set,oldset;sigemptyset(&set);sigemptyset(&oldset);//把set位圖置為全1sigfillset(&set);//設為阻塞sigprocmask(SIG_BLOCK,&set,&oldset);//如果輸入回車就繼續執行,否則一直阻塞getchar();//設為非阻塞sigprocmask(SIG_UNBLOCK,&set,NULL);return 0; }
這里也更好的解釋了阻塞集和非阻塞集的區別,也解釋了可靠信號和不可靠信號的區別
五、再談捕捉信號
1.信號捕捉的流程:
- 在執行主控程序時,因為軟件、硬件或系統調用等產生異常,操作系統想當前進程發送相關信號,從而導致當前進程進入內核狀態
- 內核處理完異常準備回到用戶態之前先處理當前進程可以遞送的信號
- 如果信號的處理函數是自定義的信號處理函數,則還需要回到用戶態執行信號處理函數(注意這里的回到用戶態并不是回到主執行流,這里的信號處理函數和主執行流沒有任何調用關系)
- 信號處理函數返回時執行特殊的系統調用sigreturn再次進入內核態
- 最后從內核態返回到用戶態,返回到從主控制流中上次被中斷的地方繼續執行,(注意??并不是回到信號處理函數的結束,可以把這里的信號處理函數理解為C++中的智能指針)
- 2.內核如何實現信號的捕捉:
- 如果信號的處理動作是**用戶自定義函數,**在信號遞達時就調用這個函數,這稱為捕捉信號。
- 由于信號處理函數的代碼是在用戶空間的,處理過程比較復雜
- 舉例如下: 用戶程序注冊了SIGQUIT信號的處理函數sighandler。
- 當前正在執行main函數,這時發生中斷或異常切換到內核態。
- 在中斷處理完畢后要返回用戶態的main函數之前檢查到有信號SIGQUIT遞達。
- 內核決定返回用戶態后不是恢復main函數的上下文繼續執行,而是執行sighandler函 數,sighandler和main函數使用不同的堆棧空間,它們之間不存在調用和被調用的關系,是 兩個獨立的控制流程。
- sighandler函數返回后自動執行特殊的系統調用sigreturn再次進入內核態。 如果沒有新的信號要遞達,這次再返回用戶態就是恢復main函數的上下文繼續執行了
- sigaction函數
- sigaction函數可以讀取和修改與指定信號相關聯的處理動作。調用成功則返回0,出錯則返回- 1。
- signo是指定信號的編號。若act指針非空,則根據act修改該信號的處理動作。若oact指針非 空,則通過oact傳出該信號原來的處理動作。act和oact指向sigaction結構體
- 將sa_handler賦值為常數SIG_IGN傳給sigaction表示忽略信號,賦值為常數SIG_DFL表示執行系統默認動作,賦值為一個函數指針表示用自定義函數捕捉信號,或者說向內核注冊了一個信號處理函 數
- 該函數返回值為void,可以帶一個int參數,通過參數可以得知當前信號的編號,這樣就可以用同一個函數處理多種信號。
- 顯然,這也是一個回調函數,不是被main函數調用,而是被系統所調用
六、可重入函數
看一個例子:
- main函數調用insert函數向一個鏈表head中插入節點node1,插入操作分為兩步
- 剛做完第一步的 時候,因為硬件中斷使進程切換到內核,再次回用戶態之前檢查到有信號待處理
- 于是切換 到sighandler函數,sighandler也調用insert函數向同一個鏈表head中插入節點node2,插入操作的 兩步都做完之后從sighandler返回內核態,再次回到用戶態
- 就從main函數調用的insert函數中繼續 往下執行,先前做第一步之后被打斷,現在繼續做完第二步。
- 結果是,main函數和sighandler先后 向鏈表中插入兩個節點,而最后只有一個節點真正插入鏈表中了
- 像上例這樣,insert函數被不同的控制流程調用,有可能在第一次調用還沒返回時就再次進入該函數,這稱為重入
- insert函數訪問一個全局鏈表,有可能因為重入而造成錯亂,像這樣的函數稱為 不可重入函數,
- 反之,如果一個函數只訪問自己的局部變量或參數,則稱為可重入(Reentrant) 函數
再看一個例子:
#include <stdio.h> #include <unistd.h> #include <signal.h>int g_val = 0;void handler(int sig) {g_val++;printf("sig :%d\n",sig); }int main() {signal(2,handler);int count = 100;while(count--){g_val++;printf("count: %d\n",count);//sleep(100000);sleep(1);}printf("g_val: %d\n",g_val);return 0; }
可以看到程序最后的打印結果并不是100,這樣就更好的理解重入
總結:
- 重入:不同的執行流可以訪問同樣的資源(同樣的代碼–和臨界資源類似)
- 可重入函數:不同的執行流可以訪問同樣的資源,不會對結果產生影響,如果不同的執行流調用了可重入函數,不會對結果產生影響,就叫可重入函數
- 不可重入函數:不同的執行流可以訪問同樣的資源,不會對結果產生影響,調用了可重入函數,會對結果產生影響,就叫不可重入函數
如果一個函數符合以下條件之一則是不可重入的:調用了malloc或free,因為malloc也是用全局鏈表來管理堆的。調用了標準I/O庫函數。標準I/O庫的很多實現都以不可重入的方式使用全局數據結構
七:volatile
- 1.使變量保存內存的可見性,意味著,每次去訪問這個變量的時候,都是從內存中重新去加載
- 注意測試該代碼是編譯優化最好是O2選項
- 標準情況下,鍵入 CTRL-C ,2號信號被捕捉,執行自定義動作,修改 flag=1 , while 條件不滿足,退出循環,進程退出
但是真正的結果是:
- 優化情況下,鍵入 CTRL-C ,2號信號被捕捉,執行自定義動作,修改 flag=1
- 但是 while 條件依舊滿足,進程繼續運行!但是很明顯flag肯定已經被修改了,但是為何循環依舊執行?
- 很明顯, while 循環檢查的flag,并不是內存中最新的flag,這就存在了數據二異性的問題。
- while 檢測的flag其實已經因為優化,被放在了CPU寄存器當中。如何解決呢?很明顯需要 volatile
volatile 作用:保持內存的可見性,告知編譯器,被該關鍵字修飾的變量,不允許被優化,對該變量的任何操作,都必須在真實的內存中進行操作
八、SIGCHLD信號
- 進程時說過 用wait和waitpid函數清理僵尸進程,父進程可以阻塞等待子進程結束,也可以非阻塞地查詢是否有子進程結束等待清理(也就是輪詢的方式)。
- 采用第一種方式,父進程阻塞了就不 能處理自己的工作了;
- 采用第二種方式,父進程在處理自己的工作的同時還要記得時不時地輪詢一 下,程序實現復雜。
- 其實,子進程在終止時會給父進程發SIGCHLD信號,該信號的默認處理動作是忽略,父進程可以自 定義SIGCHLD信號的處理函數,這樣父進程只需專心處理自己的工作,不必關心子進程了,子進程 終止時會通知父進程,父進程在信號處理函數中調用wait清理子進程即可。
- 請編寫一個程序完成以下功能:
- 父進程fork出子進程,子進程調用exit(2)終止,父進程自定義SIGCHLD信號的處理函數,在其中調用wait獲得子進程的退出狀態并打印。
- 事實上,由于UNIX 的歷史原因,要想不產生僵尸進程還有另外一種辦法:父進程調 用sigaction將SIGCHLD的處理動作置為SIG_IGN,這樣fork出來的子進程在終止時會自動清理掉,不 會產生僵尸進程,也不會通知父進程。
- 系統默認的忽略動作和用戶用sigaction函數自定義的忽略 通常是沒有區別的,但這是一個特例。
- 此方法對于Linux可用,但不保證在其它UNIX系統上都可 用。請編寫程序驗證這樣做不會產生僵尸進程
總結
- 上一篇: 区分多种类型的输入输出
- 下一篇: Linux线程(二)