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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章