1. 程式人生 > >Linux下的訊號詳解及捕捉訊號

Linux下的訊號詳解及捕捉訊號

訊號的基本概念

每個訊號都有一個編號和一個巨集定義名稱 ,這些巨集定義可以在 signal.h 中找到。

使用kill -l命令檢視系統中定義的訊號列表: 1-31是普通訊號 regular signal(非可靠訊號); 34-64是實時訊號 real time signal(可靠訊號)

所有的訊號都由作業系統來發!

對訊號的三種處理方式

1、忽略此訊號:大多數訊號都可使用這種方式進行處理,但有兩種訊號卻決不能被忽略。它們是:SIGKILLSIGSTOP。這兩種訊號不能被忽略的,原因是:它們向超級使用者提供一種使程序終止或停止的可靠方法。另外,如果忽略某些由硬體異常產生的訊號(例如非法儲存訪問或除以0),則程序的行為是示定義的。

2、直接執行程序對於該訊號的預設動作 :對大多數訊號的系統預設動作是終止該程序。

3、捕捉訊號:執行自定義動作(使用signal函式),為了做到這一點要通知核心在某種訊號發生時,呼叫一個使用者函式handler。在使用者函式中,可執行使用者希望對這種事件進行的處理。注意,不能捕捉SIGKILLSIGSTOP訊號。

?

1

2

3

#include <signal.h>

typedef void( *sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

signal函式的作用:給某一個程序的某一個特定訊號(標號為signum)註冊一個相應的處理函式,即對該訊號的預設處理動作進行修改,修改為handler函式所指向的方式。

1、第一個引數是訊號的標號

2、第二個引數,sighandler_t是一個typedef來的,原型是void (*)(int)函式指標,int的引數會被設定成signum

舉個程式碼例子:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

#include<stdio.h>

#include<signal.h>

void handler(int sig)

{

 printf("get a sig,num is %d\n",sig);

}

  

int main()

{

 signal(2,handler);

 while(1)

 {

  sleep(1);

  printf("hello\n");

 }

 return 0;

}

  修改了2號訊號(Ctrl-c)的預設處理動作為handler函式的內容,則當該程式在前臺執行時,鍵入Ctrl-c後不會執行它的預設處理動作(終止該程序)

訊號的處理過程:

程序收到一個訊號後不會被立即處理,而是在恰當 時機進行處理!什麼是適當的時候呢?比如說中斷返回的時候,或者核心態返回使用者態的時候(這個情況出現的比較多)。

訊號不一定會被立即處理,作業系統不會為了處理一個訊號而把當前正在執行的程序掛起(切換程序),掛起(程序切換)的話消耗太大了,如果不是緊急訊號,是不會立即處理的。作業系統多選擇在核心態切換回使用者態的時候處理訊號,這樣就利用兩者的切換來處理了(不用單獨進行程序切換以免浪費時間)。

總歸是不能避免的,因為很有可能在睡眠的程序就接收到訊號,作業系統肯定不願意切換當前正在執行的程序,於是就得把訊號儲存在程序唯一的PCB(task_struct)當中。

產生訊號的條件

1.使用者在終端按下某些鍵時,終端驅動程式會發送訊號給前臺程式。

     例如:Ctrl-c產生SIGINT訊號,Ctrl-\產生SIGQUIT訊號,Ctrl-z產生SIGTSTP訊號

2.硬體異常產生訊號。

     這類訊號由硬體檢測到並通知核心,然後核心向當前程序傳送適當的訊號。

     例如:當前程序執行除以0的指令,CPU的運算單元會產生異常,核心將這個程序解釋為SIGFPE訊號傳送給當前程序。
               當前程序訪問了非法記憶體地址,MMU會產生異常,核心將這個異常解釋為SIGSEGV訊號傳送給程序。

3.一個程序呼叫kill(2)函式可以傳送訊號給另一個程序。

     可以用kill(1)命令傳送訊號給某個程序,kill(1)命令也是呼叫kill(2)函式實現的,如果不明確指定訊號則傳送SIGTERM訊號,該訊號的預設處理動作是終止程序。

訊號的產生

1.通過終端按鍵產生訊號

舉個栗子:寫一個死迴圈,前臺執行這個程式,然後在終端鍵入Ctrl-c

  當CPU正在執行這個程序的程式碼 , 終端驅動程式傳送了一 個 SIGINT 訊號給該程序,記錄在該程序的 PCB中,則該程序的使用者空間程式碼暫停執行 ,CPU從使用者態 切換到核心態處理硬體中斷。

  從核心態回到使用者態之前, 會先處理 PCB中記錄的訊號 ,發現有一個 SIGINT 訊號待處理, 而這個訊號的預設處理動作是終止程序,所以直接終止程序而不再返回它的使用者空間程式碼執行。

 2.呼叫系統函式向程序發訊號

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

/*************************************************************************

 > File Name: test.c

 > Author:Lynn-Zhang

 > Mail: [email protected]

 > Created Time: Fri 15 Jul 2016 03:03:57 PM CST

 ************************************************************************/

  

#include<stdio.h>

int main()

{

 printf("get pid :%d circle ...\n",getpid());

 while(1);

 return 0;

}

寫一個上面的程式在後臺執行死迴圈,並獲取該程序的id,然後用kill命令給它傳送SIGSEGV訊號,可以使程序終止。也可以使用kill -11 5796,11是訊號SIGSEGV的編號。

開啟終端1,執行程式:

利用終端2,給程序傳送訊號

 終端1 顯示程序被core了:

kill命令是呼叫kill函式實現的。kill函式可以給一個指定的程序傳送指定訊號

raise函式可 以給當前程序傳送指定的訊號 (自己給自己發訊號 )

?

1

2

3

#include<signal.h>

int kill(pid_t pid,int signo);

int raise(int signo);

這兩個函式都是成功返回0,錯誤返回-1.

除此之外,abort函式使當前程序接收到SIGABRT訊號而異常終止。

?

1

2

#include<stdlib.h>

void abort(void);

就像 exit函式一樣 ,abort 函式總是會成功的 ,所以沒有返回值。

3.由軟體條件產生訊號

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

/*************************************************************************

 > File Name: alarm.c

 > Author:Lynn-Zhang

 > Mail: [email protected]

 > Created Time: Fri 15 Jul 2016 08:52:02 PM CST

 ************************************************************************/

  

#include<stdio.h>

  

int main()

{

 int count=0;

 alarm(1);

 while(1)

 {

 printf("%d\n",count);

 count++;

 }

 return 0;

}

 通過實現以上程式碼,呼叫alarm函式可以設定一個鬧鐘,告訴核心在seconds秒之後給當前程序發SIGALRM訊號, 該訊號的預設處理動作是終止當前程序。

   該程式會在1秒鐘之內不停地數數,並列印計數器,1秒鐘到了就被SIGALRM訊號終止。由於電腦配置等的不同,每臺電腦一秒鐘之內計數值是不同的一般是不同的。

?

1

2

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

  alarm函式的返回值是0或上次設定鬧鐘剩餘的時間。

阻塞訊號

 1.訊號在核心中的表示:

訊號遞達delivery:實際執行訊號處理訊號的動作

訊號未決pending:訊號從產生到抵達之間的狀態,訊號產生了但是未處理

忽略:抵達之後的一種 動作

阻塞block:收到訊號不立即處理     被阻塞的訊號將保持未決狀態,直到程序解除對此訊號的阻塞,才執行抵達動作

訊號產生和阻塞沒有直接關係 抵達和解除阻塞沒有直接關係!

程序收到一個訊號後,不會立即處理,它會在恰當的時機被處理。

每個訊號都由兩個標誌位分別表示阻塞和未決,以及一個函式指標表示訊號的處理動作。

在上圖的例子中,

  1. SIGHUP訊號未阻塞也未產生過,當它遞達時執行預設處理動作。

  2. SIGINT訊號產生過,但正在被阻塞,所以暫時不能遞達。雖然它的處理動作是忽略,但在沒 有解除阻塞之前不能忽略這個訊號,因為程序仍有機會改變處理動作之後再解除阻塞。

  3. SIGQUIT訊號未產生過,一旦產生SIGQUIT訊號將被阻塞,它的處理動作是使用者自定義函式sighandler。阻塞訊號集也叫作訊號遮蔽字。

訊號產生但是不立即處理,前提條件是要把它儲存在pending表中,表明訊號已經產生。

2.訊號集操作函式

?

1

2

3

4

5

6

#include <signal.h>

int sigemptyset(sigset_t *set); //初始化set所指向的訊號集,使所有訊號的對應位清0

int sigfillset(sigset_t *set); //初始化set所指向的訊號集,表示該訊號集的有效訊號包括系統支援的所有訊號

int sigaddset(sigset_t *set, int signo); //在該訊號集中新增有效訊號

int sigdelset(sigset_t *set, int signo); //在該訊號集中刪除有效訊號

int sigismember(const sigset_t *set, int signo); //用於判斷一個訊號集的有效訊號中是否包含某種訊號

引數解析:

sigset_t結構體的引數表示訊號集,訊號操作的時候都是以訊號集合的方式進行操作,需要事先建立一個該結構體的物件,然後把想要操作的訊號新增到訊號集合物件當中去

signo就是訊號的標號了

3.呼叫函式sigprocmask可以讀取或更改程序的訊號遮蔽字(阻塞訊號集)。

?

1

2

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

   一個程序的訊號遮蔽字規定了當前阻塞而不能遞送給該程序的訊號集。呼叫函式sigprocmask可以檢測或更改(或兩者)程序的訊號遮蔽字。如果呼叫sigprocmask解除了對當前若干個未決訊號的阻塞,則在sigprocmask返回前,至少將其中 一個訊號遞達。

引數解析:

 how,有三個巨集

     SIG_BLOCK      新增到block表當中去

     SIG_UNBLOCK  從block表中刪除

     SIG_SETMASK  設定block表 設定當前訊號遮蔽字為set所指向的值

 set表示新設定的訊號遮蔽字,oset表示當前訊號遮蔽字

處理方式:

      set 非空, oset 為NULL :按照how指示的方法更改set指向訊號集的訊號遮蔽字。

      set 為NULL,oset 非空:讀取oset指向訊號集的訊號遮蔽字,通過oset引數傳出。

      set 和 oset 都非空 :現將原來的訊號遮蔽字備份到oset裡,然後根據sethow引數更改訊號遮蔽字。

4. sigpending讀取當前程序的未決訊號集,通過set引數傳出

?

1

2

#include <signal.h>

int sigpending(sigset_t *set);

這是一個輸出型引數,會把當前程序的pending表列印到傳入的set集中。

例項驗證上面幾個函式:

 

一開始沒有任何訊號,所以pending表中全是0,我通過Ctrl+C傳入2號訊號,看到pending表中有2號被置位了,經過10秒取消阻塞,2號訊號被處理(經過我自定義的函式)

Linux下捕捉訊號

訊號由三種處理方式:

     忽略

     執行該訊號的預設處理動作

     捕捉訊號

如果訊號的處理動作是使用者自定義函式,在訊號遞達時就呼叫這個自定義函式,這稱為捕捉訊號

程序收到一個訊號後不會被立即處理,而是在恰當時機進行處理!即核心態返回使用者態之前 !

但是由於訊號處理函式的程式碼在使用者空間,所以這增加了核心處理訊號捕捉的複雜度。

核心實現訊號捕捉的步驟:

      1、使用者為某訊號註冊一個訊號處理函式sighandler

      2、當前正在執行主程式,這時候因為中斷、異常或系統呼叫進入核心態。

      3、在處理完異常要返回使用者態的主程式之前,檢查到有訊號未處理,並發現該訊號需要按照使用者自定義的函式來處理。

      4、核心決定返回使用者態執行sighandler函式,而不是恢復main函式的上下文繼續執行!(sighandlermain函式使用的是不同的堆疊空間,它們之間不存在呼叫和被呼叫的關係,是兩個獨立的控制流程)

      5、sighandler函式返回後,執行特殊的系統呼叫sigreturn從使用者態回到核心態

      6、檢查是否還有其它訊號需要遞達,如果沒有 則返回使用者態並恢復主程式的上下文資訊繼續執行。

signal

給某一個程序的某一個訊號(標號為signum)註冊一個相應的處理函式,即對該訊號的預設處理動作進行修改,修改為handler函式指向的方式;

?

1

2

3

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);<br>//即:<br>void (*signal(int, void(*)(int)))(int);

signal函式接受兩個引數:一個整型的訊號編號,以及一個指向使用者定義的訊號處理函式的指標。  

此外,signal函式的返回值是一個指向呼叫使用者定義訊號處理函式的指標。

sigaction

sigaction函式可以讀取和修改與指定訊號相關聯的處理動作。

?

1

2

3

4

5

6

7

8

9

10

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

struct sigaction

{

  void (*sa_handler)(int);  //訊號處理方式

  void (*sa_sigaction)(int, siginfo_t *, void *); //實時訊號的處理方式 暫不討論

  sigset_t sa_mask; //額外遮蔽的訊號

  int sa_flags;

  void (*sa_restorer)(void);

};

signum是指定訊號的編號。

處理方式:

     1、若act指標非空,則根據act結構體中的訊號處理函式來修改該訊號的處理動作。

     2、若oact指標非 空,則通過oact傳出該訊號原來的處理動作。

     3、現將原來的處理動作備份到oact裡,然後根據act修改該訊號的處理動作。

(注:後兩個引數都是輸入輸出型引數!)

將sa_handler三種可選方式:

     1、賦值為常數SIG_IGN傳給sigaction表示忽略訊號;

     2、賦值為常數SIG_DFL表示執行系統預設動作;

     3、賦值為一個函式指標表示用自定義函式捕捉訊號,或者說向核心註冊一個訊號處理函 數,該函式返回值為void,可以帶一個int引數,通過引數可以得知當前訊號的編號,這樣就可以用同一個函式處理多種訊號。

(注:這是一個回撥函式,不是被main函式呼叫,而是被系統所呼叫)

  當某個訊號的處理函式被呼叫時,核心自動將當前訊號加入程序的訊號遮蔽字,當訊號處理函式返回時自動恢復原來的訊號遮蔽字,這樣就保證了在處理某個訊號時,如果這種訊號再次產生,那麼 它會被阻塞到當前處理結束為止。

 pause

pause函式使呼叫程序掛起直到有訊號遞達!

?

1

2

#include <unistd.h>

int pause(void);

處理方式: 

     如果訊號的處理動作是終止程序,則程序終止,pause函式沒有機會返回;

     如果訊號的處理動作是忽略,則程序繼續處於掛起狀態,pause不返回;

     如果訊號的處理動作是捕捉,則呼叫了訊號處理函式之後pause返回-1,errno設定為EINTR。

     所以pause只有出錯的返回值(類似exec函式家族)。錯誤碼EINTR表示“被訊號中斷”。

 舉個栗子

     1、定義一個鬧鐘,約定times秒後,核心向該程序傳送一個SIGALRM訊號;

     2、呼叫pause函式將程序掛起,核心切換到別的程序執行;

     3、times秒後,核心向該程序傳送SIGALRM訊號,發現其處理動作是一個自定義函式,於是切回用戶態執行該自定義處理函式;

     4、進入sig_alrm函式時SIGALRM訊號被自動遮蔽,從sig_alrm函式返回時SIGALRM訊號自動解除遮蔽。然後自動執行特殊的系統呼叫sigreturn再次進入核心,之後再返回使用者態繼續執行程序的主控制流程(main函式呼叫的mytest函式)。

     5、pause函式返回-1,然後呼叫alarm(0)取消鬧鐘,呼叫sigaction恢復SIGALRM訊號以前的處理 動作。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

/*************************************************************************

 > File Name: Pause.c

 > Author:Lynn-Zhang

 > Mail: [email protected]

 > Created Time: Sun 14 Aug 2016 12:27:03 PM CST

 ************************************************************************/

  

#include<stdio.h>

#include<signal.h>

#include<unistd.h>

void sig_alarm(int signum)

{

 printf("I am a custom handler!\n");

}

void mysleep(unsigned int times)

{

 //註冊兩個訊號處理動作

 struct sigaction new,old;

 new.sa_handler=sig_alarm; //訊號處理函式

 sigemptyset(&new.sa_mask);//不遮蔽任何訊號遮蔽字

 new.sa_flags=0;

  

 //對SIGALRM 訊號的預設處理動作修改為自定義處理動作

 sigaction(SIGALRM,&new,&old);

 alarm(times);

 pause(); //掛起等待

 alarm(1);

 sleep(2);

 alarm(0); //取消鬧鐘

 //恢復SIGALRM 訊號到預設處理動作

 sigaction(SIGALRM,&old,NULL);

 alarm(1);

 sleep(2);

}

int main()

{

 while(1)

 {

 mysleep(2);

 printf("many seconds passed\n");

 printf("###################\n");

 }

 return 0;

}

   

定義一個鬧鐘並掛起等待,收到訊號後執行自定義處理動作,在沒有恢復預設處理動作前,收到SIGALRM訊號都會按照其自定義處理函式來處理。恢復自定義處理動作之後收到SIGALRM訊號則執行其預設處理動作即終止程序!

總結