Linux下的訊號(三)----捕捉訊號與sleep模擬
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限定符的理由。
執行結果: