Linux下的信號(一):信號的基本概念與產生
Linux下的信號(二):阻塞信號
一,什麼是捕捉信號?
1,捕捉信號:信號處理方式三種方式中的一種,意思是既不忽略該信號,又不執行信號默認的動作,而是讓信號執行自定義動作。捕捉信號要使用signal函數,爲了做到這一點要通知內核在某種信號發生時,調用一個用戶函數handler。在用戶函數中,可執行用戶希望對這種事件進行的處理。注意,不能捕捉SIGKILL和SIGSTOP信號。
2,系統捕捉信號的過程:
總結一下上圖:
1>當一個正在運行的進程收到了中斷,異常,或系統調用時,會從用戶 態切換至內核態;
2>當內核處理完異常或中斷時不會立即返回用戶態,在回到用戶態之前系統會檢查要返回進程PCB中的signal位圖信息。如果當前進程的pending表中有還未遞達的信號(pending表中有標誌是1),內核會將懸掛的信號進行處理:
3>如果懸掛信號的處理方式是執行自定義動作,那麼此時會從內核態切換至用戶態執行用戶自定義的handler函數;
4>待系統處理完信號自定義的句柄函數時,系統會執行特殊的系統調用sigreturn再次回到內核態;
5>處理完sigreturn之後再次從內核態切換至用戶態執行從主控流程main函數中上次被中斷的地方繼續向下運行……
3,捕捉信號過程的快速記憶(類似於數學公式中的∞):
0>一張圖,兩半,上爲用戶態(運行態),下面爲內核態(管理態)。
1> 上圖爲信號的捕捉,處理流程。
2>圖中黑色菱形是爲了處理用戶自定義的句柄。
3>圖中有4個內核與用戶的切換(1234)。
4>用戶處理信號的時機:從內核態切回用戶態時。
4,內核捕捉信號舉例:
1>用戶程序註冊了SIGQUIT信號的處理函數sighandler。
2> 當前正在執行main函數,這時發生中斷或異常切換到內核態。
3> 在中斷處理完畢後要返回用戶態的main函數之前檢查到有信號SIGQUIT遞達。
4> 內核決定返回用戶態後不是恢復main函數的上下文繼續執行,而是執行sighandler函數,sighandler和main函數使用不同的堆棧空間,它們之間不存在調用和被調用的關係,是兩個獨立的控制流程。
5> sighandler函數返回後自動執行特殊的系統調用sigreturn再次進入內核態。
6> 如果沒有新的信號要遞達,這次再返回用戶態就是恢復main函數的上下文繼續執行了。
二,捕捉信號用到的函數
1)SIGALRM 信號:
時鐘定時信號, 計算的是實際的時間或時鐘時間, alarm函數使用該信號。
2)alarm函數:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
alarm也稱爲鬧鐘函數。它可以在進程中設置一個定時器,當定時器指定的時間到時,它向進程發送SIGALRM信號。如果忽略或者不捕獲此信號,則其默認動作是終止調用該alarm函數的進程。
返回值是0或者是以前設定的鬧鐘時間還餘下 的秒數。如果seconds值爲0,表示取消以前設定的鬧鐘,函數的返回值仍然是以前設定的鬧鐘時間還餘下的秒數。
3)pause函數:
#include <unistd.h>
int pause(void);
pause函數使調用進程掛起直到有信號遞達。pause只有出錯的返回值。errno設置爲EINTR表示“被信號中斷”。
如果信號的處理動作是終止進程,則進程終止, pause函數沒有機會返回;
如果信號的處理動作是忽略,則進程繼續處於掛起狀態, pause不返回;
如果信號的處理動作是捕捉,則調用了信號處理函數之後pause返回- 1;
4)sigaction函數:
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, structsigaction *oact);
sigaction函數:成功返回0,失敗返回-1
signo是指定信號的編號。若act指針非空,則根據act修改該信號的處理動作。若oact指針非空,則通過oact傳出該信號原來的處理動作。
sigaction的結構體:
sa_handler:賦值爲常數SIG_IGN傳給sigaction表示忽略信號;賦值爲常數SIG_DFL表示執行系統默認動作,賦值爲一個函數指針表示用自定義函數捕捉信號。 向內核註冊了一個信號處理函數,該函數返回值爲void,可以帶一個int參數,通過參數可以得知當前信號的編號,這樣就可以用同一個函數處理多種信號。顯然,這也是一個回調函數,不是被main函數調用,而是被系統所調用。
sa_mask:進程的信號屏蔽字。如果在調用信號處理函數時,除了當前信號被自動屏蔽之外,還希望自動屏蔽另外一些信號,則用sa_mask字段說明這些需要額外屏蔽的信號,當信號處理函數返回時自動恢復原來的信號屏蔽字。
sa_flags:sa_flags字段包含一些選項,本文代碼把sa_flags設爲0。
sa_sigaction是實時信號的處理函數。
三,捕捉信號舉例—-模擬sleep
(一)普通版本的mysleep
1 /**************************************
2 *文件說明:mysleep.c
3 *作者:段曉雪
4 *創建時間:2017年06月09日 星期五 11時32分52秒
5 *開發環境:Kali Linux/g++ v6.3.0
6 ****************************************/
7
8 #include<stdio.h>
9 #include<signal.h>
10 #include<unistd.h>
11
12 void myhandler(int sig) //句柄函數什麼也不做
13 {
14 //printf("get a sig:%d\n",sig);
15 }
16
17 int mysleep(int timeout)
18 {
19 struct sigaction act,oact;
20 act.sa_handler = myhandler;
21 sigemptyset(&act.sa_mask);//信號集的初始化
22 act.sa_flags = 0;
23 sigaction(SIGALRM,&act,&oact);//註冊信號處理函數
24
25 alarm(timeout);//設置鬧鐘
26 pause();//將自己掛起直到有信號遞達
27 int ret = alarm(0);//取消鬧鐘
28 sigaction(SIGALRM,&oact,NULL);//恢復對SIGALRM信號的處理動作
29 return ret;
30 }
31
32 int main()
33 {
34 while(1)
35 {
36 mysleep(2);
37 printf("use mysleep!\n");
38 }
39
40 return 0;
41 }
運行結果:
函數詳解:
1、main函數調用my_sleep函數,後者調用sigaction註冊了SIGALRM信號的處理函數myhandler。
2、調用alarm(timeout)設定鬧鐘。
3、調用pause等待,內核切換到別的進程運行。
4、timeout秒之後,鬧鐘超時,內核發SIGALRM給這個進程。
5、從內核態返回這個進程的用戶態之前處理未決信號,發現有SIGALRM信號,其處理函數是myhandler。
6、切換到用戶態執行handler函數,進入handler函數時SIGALRM信號被自動屏蔽, 從myhandler函數返回時SIGALRM信號自動解除屏蔽。然後自動執行系統調用sigreturn再次進入內核,再返回用戶態繼續執行進程的主控制流程。
7、pause函數返回-1,然後調用alarm(0)取消鬧鐘,調用sigaction恢復SIGALRM信號以前的處理動作。
程序運行過程中遇到一個問題,雖然程序按照流程運行完成,但是並沒有結束,直到人爲的按ctrl C才結束運行,那麼爲什麼會這樣呢?
因爲程序的時序,優先級等問題導致程序沒有按預期運行,有可能在設置鬧鐘之後由於某種原因程序被切出去了,等時鐘到了固定時間後程序仍沒切回來,此時會將信號遞達,等程序切回來時pause有可能再也等不到信號來臨導致程序一直被掛起。
根本原因就是系統運行代碼時並不會按照我們的思路走,雖然alarm(timeout)緊接着的下一行就是pause(),但是無法保證pause()一定會在調用alarm(timeout)之後的timeout秒之內被調用。由於異步事件在任何時候都有可能發生,如果寫程序時考慮不周密,就可能由於時序問題而導致錯誤,這叫做競態條件。
(二)避免競態條件的mysleep
程序改善:用sigsuspend代替pause,sigsuspend函數既包含了pause的掛起等待功能,同時又解決了競態條件的問題。
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
在對時序要求嚴格的場合下都應該調用sigsuspend而不是pause。
如果在調用my_sleep函數時SIGALRM信號沒有屏蔽:
1)調用sigprocmask(SIG_BLOCK,&newmask, &oldmask)時,屏蔽SIGALRM。
2)調用sigsuspend(&suspmask)時,解除對SIGALRM的屏蔽,然後掛起等待。
3)SIGALRM遞達後suspend返回,自動恢復原來的屏蔽字,也就是再次屏蔽SIGALRM。
4)調用sigprocmask(SIG_SETMASK, &oldmask, NULL)時,再次解除對SIGALRM的屏蔽。
1 /**************************************
2 *文件說明:mysleep.c
3 *作者:段曉雪
4 *創建時間:2017年06月09日 星期五 11時32分52秒
5 *開發環境:Kali Linux/g++ v6.3.0
6 ****************************************/
7
8 #include<stdio.h>
9 #include<signal.h>
12 void myhandler(int sig)
13 {
14 //printf("get a sig:%d\n",sig);
15 }
19 struct sigaction act,oact;
20 sigset_t newmask,oldmask,suspmask;//設置信號集
21 act.sa_handler = myhandler;
22 sigemptyset(&act.sa_mask);//信號集的初始化
23 act.sa_flags = 0;
24 sigaction(SIGALRM,&act,&oact);//讀取和修改與SIGALRM信號相關聯的處理動作
25
26 sigemptyset(&newmask);//初始化信號集
27 sigaddset(&newmask,SIGALRM);//爲信號集添加SIGALRM信號
29
30 alarm(timeout);//設置鬧鐘
31 sigdelset(&oldmask,SIGALRM);//從信號集oldmask中刪除SIGALRM信號
32
33 sigsuspend(&oldmask);//將當前進程的信號屏蔽字設爲oldmask,在進程接受到信號之前,進程會掛起,當捕捉一個信號,首先執行信號處理程序,然後從sigsuspend返回,最後將信號屏蔽字恢復爲調用sigsuspend之前的值
34 //pause();//將自己掛起直到有信號遞達
35
36 int ret = alarm(0);//取消鬧鐘
37 sigaction(SIGALRM,&oact,NULL);//恢復對SIGALRM信號的處理動作
38 return ret;
39 }
40
41 int main()
42 {
43 while(1)
44 {
45 mysleep(2);
46 printf("use mysleep!\n");
47 }
48
49 return 0;
50 }
運行結果:
pause與sigsuspend:
1>sigsuspend函數接受一個信號集指針,將信號屏蔽字設置爲信號集中的值,在進程接受到一個信號之前,進程會掛起,當捕捉一個信
號,首先執行信號處理程序,然後從sigsuspend返回,最後將信號屏蔽字恢復爲調用sigsuspend之前的值。
2>pause函數使調用進程掛起直到捕捉到一個信號。只有執行了一個信號處理程序並從其返回時,pause才返回
sigsuspend函數是pause函數的增強版。當sigsuspend函數的參數信號集爲空信號集時,sigsuspend函數是和pause函數是一樣的,可以接受任何信號的中斷。
但,sigsuspend函數可以屏蔽信號,接受指定的信號中斷。
sigsuspend函數=pause函數+指定屏蔽信號
注:信號中斷的是sigsuspend和pause函數,不是程序代碼。
四,可重入函數
五 ,sig_atomic_t類型與volatile限定符
1,先看一段代碼:
1 /**************************************
2 *文件說明:volatile.c
3 *作者:段曉雪
4 *創建時間:2017年06月09日 星期五 17時40分39秒
5 *開發環境:Kali Linux/g++ v6.3.0
6 ****************************************/
7
8 #include<stdio.h>
9 #include<signal.h>
10
11 int flag = 0;
12 void handler(int sig)
13 {
14 flag = 1;
15 printf("change flag 0 -> 1\n");
16 }
17 int main()
18 {
19 signal(2,handler);
20 while(!flag);
21 return 123;
22 }
運行結果:
代碼中我們對2號信號(ctrl C)進行了捕捉,然後執行其自定義的句柄函數,在函數中,我們將全局變量的flag從0改爲了1,而主線程中的while循環條件中的!flag本來應該爲!0退出循環,可是代碼卻仍在死循環,直到殺死進程。
2,改進代碼:在類型前面加volatile:
C語言提供了volatile限定符,如果將 上述變量定義爲volatile sig_atomic_ta=0;那麼即使指定了優化選項,編譯器也不會優化掉對變 量a內存單元的讀寫。
變量屬於以下情況之一的,也需要volatile限定:
1. 變量的內存單元中的數據不需要寫操作就可以自己發生變化,每次讀上來的值都可能不一樣。
2. 即使多次向變量的內存單元中寫數據,只寫不讀,也並不是在做無用功,而是有特殊意義的。
什麼樣的內存單元會具有這樣的特性呢?肯定不是普通的內存,而是映射到內存地址空間的硬件寄存器,例如串口的接收寄存器屬於上述第一種情況,而發送寄存器屬於上述第二種情況。
運行結果:
3,如果在程序中需要使用一個變量,要保證對它的讀寫都是原子操作,C標準定義了一個類型sig_atomic_t,在不同平臺的C語言庫中取不同的類型,例如在32位機 上定義sig_atomic_t爲int類型。
sig_atomic_t類型的變量應該總是加上volatile限定符,因爲要使用sig_atomic_t類型的理由也正是要加volatile限定符的理由。
運行結果: