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信號則執行其默認處理動作即終止進程!

總結

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