1. 程式人生 > >Linux下的訊號(三)----捕捉訊號與sleep模擬

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限定符的理由。
這裡寫圖片描述
執行結果:
這裡寫圖片描述