史上最詳細的信號使用說明(已被收藏和轉載N次)

Unix環境高級編程(第三版)

第10章 信號

1. 引言

​ 信號是一種軟中斷。很多比較重要的應用程序都需要處理信號。信號提供了一種異步處理事件的方法,例如:終端用戶輸入中斷鍵,會通過信號機制終止一個程序等。早期的信號存在丟失的風險,且執行在臨界代碼區時無法關閉所選擇的信號,後來一些系統便增加了可靠信號機制。下面的章節提供詳細的說明。

2. 信號的概念

​ 首先,每一個信號都有一個名字。這些名字都是以"SIG"開頭的。Linux支持31種基本信號,不同的操作系統可能支持的信號數量略有不同。信號是在頭文件<signal.h>中定義的,且每一種信號都被定義爲整形常量(信號編號)。

toney@ubantu:~$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

​ 不存在編號爲0的信號。在後面的章節中會說明編號爲0的信號的特殊用途。

​ 產生信號的條件有很多:

  • 當用戶按某些終端鍵時會產生信號。例如使用‘Delete’鍵會產生SIGINT信號(有些系統中組合鍵Ctrl+C也會產生相同的效果)。
  • 硬件異常產生信號。例如:除數爲0、無法的內存訪問(常見的有段錯誤)等。這些條件通常是由硬件檢測到的,並通知內核,之後由內核產生適當的信號並通知該進程。
  • 進程調用kill(2)函數可將任意信號發送給另一個進程或者進程組。對此有一個限制:要麼發送信號的進程所有者是超級用戶,要麼發送進程和接收進程擁有相同的所有者。
  • 用戶調用kill(1)命令將信號發送給其他的進程。我們常用此命令終止(個人更喜歡說殺死)一個後臺進程。
  • 當檢測到某種軟件條件發生時,系統也會產生相應的信號通知該進程。例如定時時間到產生SIGALRM信號、管道讀進程已經關閉卻任然要往管道中寫數據時產生SIGPIPE信號。

​ 信號是異步事件的典型示例。產生信號的事件對進程而言是隨機出現的。進程不能通過測試一個簡單的變量(如errno)來判斷是否有信號發生,而是應該告訴內核:“當此信號發生時,應該執行如下操作”。這裏一共有三種方式可供選擇:

(1)忽略此信號。

(2)捕捉此信號。

(3)執行系統默認操作。

2.1 信號操作之忽略信號

​ 首先來說忽略信號的用法。大多數的信號都可以使用這種方式來處理信號,但是有兩種信號是絕不能被忽略的,它們分別是SIGKILLSIGSTOP信號。這有兩種信號不能被忽略的原因是:它們向內核和用戶提供了使進程終止或者停止的可靠方法。此外,如果忽略某些由硬件產生的信號(例如SIGSEG信號),會導致軟件出現無法預料的問題。

2.2 信號操作之捕捉信號

​ 爲了實現捕捉信號的目的,我們必須通知內核在某種信號發生時,調用一個用戶函數。在用戶函數中,我們可以執行我們希望對該信號的處理方式。例如我們可以捕捉SIGALRM信號,當定時時間到時打印某些提示信息等。注意:不能捕捉SIGKILL和SIGSTOP信號

2.3 信號操作之執行系統默認操作

​ 對於大多數信號的系統默認操作都是終止該進程。

2.4 常見的信號

​ 下表中列出了31中信號編號、信號名稱,Linux系統的默認操作,並對其中常見或者常用到的信號做了一個簡單的說明。如果以後用到再做詳細補充說明。

序號 信號名稱 說明 默認操作
1 SIGHUP 暫不介紹 terminate
2 SIGINT 當用戶按中斷鍵(一般是Ctrl+C或Delete鍵)時,驅動程序會產生此信號來終止進程。 terminate
3 SIGQUIT 當用戶按退出鍵(一般是Ctrl+)時,中斷驅動程序會產生該信號,發送給所有前臺進程。 coredump
4 SIGILL 該信號表示已經執行一條非法硬件指令。 coredump
5 SIGTRAP 指示一個實現的硬件故障。 coredump
6 SIGABRT 調用abort()函數來終止進程時會產生該信號。 coredump
7 SIGBUS 指示一個已定義的硬件故障 coredump
8 SIGFPE 表示算數運算異常。例如除0操作,浮點溢出等。 coredump
9 SIGKILL 無法被忽略和捕捉的信號。它向系統提供一種可以殺死任意進程的可靠方法。 terminate
0 SIGUSR1 用戶定義的信號,可用於應用程序 terminate
11 SIGSEGV 無效的內存訪問。例如經典的“段錯誤”。 coredump
12 SIGUSR2 用戶定義的另一個信號,可用於應用程序 terminate
13 SIGPIPE 管道的讀進程已經終止時寫管道會產生該信號。 terminate
14 SIGALRM 當使用alarm()函數,或者setitimer()設置的定時時間到時會產生此信號 terminate
15 SIGTERM 是由kill(1)命令發送的系統默認終止信號。該信號可被應用程序捕獲,從而進行清理工作,完成優雅的終止(相對於SIGKILL而言,SIGKILL信號不能被捕獲或者忽略)。 terminate
16 SIGSTKFLT 暫不介紹
17 SIGCHLD 一個進程終止或者停止時,SIGCHLD信號會發送給其父進程。按系統默認,將忽略此信號。如果父進程需要被告知該子進程退出狀態,則需要捕捉此信號。一般在信號處理函數中調用wait()函數回收子進程的資源。 ignore
18 SIGCONT 作業控制信號。它用來發送給需要繼續運行,但當前處於停止狀態的進程。收到此信號後,掛起的進程繼續運行。如果本來已經在運行則忽略該信號。 ignore
19 SIGSTOP 作業控制信號,它停止一個進程。不能被捕捉或忽略 stop
20 SIGTSTP 交互停止信號。當用戶按掛起鍵(一般是Ctrl+z)時,中斷驅動程序產生此信號。 stop
21 SIGTTIN 暫不介紹 stop
22 SIGTTOU 暫不介紹 stop
23 SIGURG 暫不介紹 ignore
24 SIGXCPU 暫不介紹 coredump
25 SIGXFSZ 暫不介紹 coredump
26 SIGVTALRM 暫不介紹 terminate
27 SIGPROF 暫不介紹 terminate
28 SIGWINCH 暫不介紹 ignore
29 SIGIO 暫不介紹 terminate
30 SIGPWR 暫不介紹 terminate
31 SIGUNUSED / SIGSYS 一個無效的系統調用 coredump

3. 函數signal

3.1 signal函數介紹

Unix系統信號機制最簡單的接口是signal函數。

#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
						返回值:若成功,返回之前的信號處理配置;若失敗,返回SIG_ERR.

代碼中:

  • signo是指信號名稱(詳情參見2.4常見的信號

  • func是常量SIG_IGNSIG_DFL或者接收到信號時自定義的信號處理函數地址

    • 如果爲SIG_IGN, 則向內核表示要忽略此信號
    • 如果爲SIG_DFL, 則表示接收到此信號時執行系統的默認操作。
    • 當指定的是函數地址時,則在信號發生時,由內核調用該函數。我們稱此函數爲信號處理函數,或者信號捕捉函數

    說句心裏話,signal的函數原型看起來有點看不懂:( 😦 😦 。下面我們也按前輩先人的說法再熟悉下(原話):

本節開頭所示的signal函數原型太複雜了,如果使用下面的typedef,則可以使其簡單些。

typedef void Sigfunc(int);			(3-1)

然後,可將signal函數原型寫成:

Sigfunc *signal(int, Sigfunc *);3-2

這樣,signal的函數看起來就簡單了很多:signal函數要求兩個參數,並返回一個函數指針(如3-2所示),而該函數指針指向的函數有一個整型參數且無返回值(如3-1所示)。

​ 用通俗一點的話描述:定義一個信號處理函數,它有一個整型參數signo, 無返回值;當調用signal函數設置信號處理程序時,signal函數的第二個參數是指向該信號處理函數的指針,signal函數的返回值是指向未修改之前的信號處理函數指針。

​ 在上述的描述中,我們提到了三個宏定義: SIG_IGNSIG_DFLSIG_ERR。這三個宏Linux上的原型如下:

typedef void __sighandler_t(int);

#define SIG_DFL	((__sighandler_t)0)		/* default signal handling */
#define SIG_IGN	((__sighandler_t)1)		/* ignore signal */
#define SIG_ERR	((__sighandler_t)-1)	/* error return from signal */

這三個常量可用於表示“指向函數的指針”。

3.2 signal函數示例

​ 該實例中定義了兩個信號處理函數,捕獲了三個信號(SIGUSR1, SIGUSR2共用一個信號處理函數)。

/*************************************************************************
             > File Name: signal_demo.c
             > Author: Toney Sun
             > Mail: [email protected]
       > Created Time: 2020年04月27日 星期一 11時50分47秒
 ************************************************************************/

#include <stdio.h>
#include <signal.h>

static void sig_handler(int); /*自定義的信號處理函數*/
static void sig_usr(int);     /*自定義的信號處理函數*/
int signal_install()
{
     if(signal(SIGINT, sig_handler)==SIG_ERR){
           printf("SIGINT handle function register error\n");
     } 
     if(signal(SIGUSR1, sig_usr)==SIG_ERR){
           printf("SIGUSR1 handle function register error\n");
     }
     if(signal(SIGUSR2, sig_usr)==SIG_ERR){
           printf("SIGUSR2 handle function register error\n");
     }
}

void sig_handler(int signo)
{
      if(signo == SIGINT){
            printf("Recieved SIGINT signal\n");
      }else{
            printf("sig_handler receieve Error signal\n");
      }
}
void sig_usr(int signo)
{
      if(signo == SIGUSR1){
            printf("Recieved SIGUSR1 signal\n");
      }else if(signo == SIGUSR2){
            printf("Recieved SIGUSR2 signal\n");
      }else{
            printf("sig_usr receieve Error signal\n");
      }
}
void main(int argc, char *argv[])
{
	//signal_demo();
	//exec_funcs();
	signal_install();
	while(1){
		pause();
	}
}

結果如下:

toney@ubantu:/mnt/hgfs/em嵌入式學習記錄/schedule調度器$ ./demo.out 

^CRecieved SIGINT signal

^CRecieved SIGINT signal

^CRecieved SIGINT signal
^Z
[3]+  Stopped                 ./demo.out
toney@ubantu:/mnt/hgfs/em嵌入式學習記錄/schedule調度器$ 
toney@ubantu:/mnt/hgfs/em嵌入式學習記錄/schedule調度器$ ./demo.out &
[6] 19518
toney@ubantu:/mnt/hgfs/em嵌入式學習記錄/schedule調度器$ kill -USR2 19518
toney@ubantu:/mnt/hgfs/em嵌入式學習記錄/schedule調度器$ Recieved SIGUSR2 signal

toney@ubantu:/mnt/hgfs/em嵌入式學習記錄/schedule調度器$ kill -USR1 19518
toney@ubantu:/mnt/hgfs/em嵌入式學習記錄/schedule調度器$ Recieved SIGUSR1 signal

toney@ubantu:/mnt/hgfs/em嵌入式學習記錄/schedule調度器$ 

3.3 signal函數的限制

  • 如果想使用signal函數來獲取當前進程對某一信號的處理方式,會修改當前的處理方式,否則無法確定當前的處理方式。常見的用法如下:
if(signal(SIGINT, SIG_IGN)!=SIG_IGN)
	signal(SIGINT, sig_handler);
if(signal(SIGUSR1, SIG_IGN)!=SIG_IGN)
	signal(SIGINT, sig_usr);

後面我們將使用另一種信號處理方式:sigaction()函數,此函數無需修改便可以查詢當前的處理方式。

  • 進程創建

    當一個進程調用fork時,其子進程繼承了父進程的信號處理方式。因爲子進程在創建時複製了父進程的內存映像,所以信號捕捉函數的地址在子進程中是有效的。

4. 不可靠的信號

4.1 什麼是不可靠的信號

​ 在早期的Unix版本中,信號是不可靠的。這裏的不可靠指的是:信號可能會丟失(一個信號已經發生了,但是該進程卻不知道這一點)。除此之外,進程對信號的控制能力也特別差,它只能捕捉或者忽略信號。但是有時用戶希望通知內核阻塞某個信號,不要忽略該信號;而在進程準備好處理該信號時在由內核重新通知該進程。

某些書籍提到signal函數每觸發一次,得重新調用signal重新註冊安裝信號處理函數。這已經很很久以前的了,現在是一次signal註冊,之後多次使用,無需每次在信號處理函數中重新調用signal函數(Linux便是如此)

4.2 信號丟失的例子:

​ 正常情況下,信號的發生頻率很低,因此信號丟失的情況比較少。但是如果信號發生的頻率比較高,且信號處理函數費時的話就很容易發生信號丟失的情形。下面我們通過在信號處理函數中調用延時函數模擬實現:

/*************************************************************************
             > File Name: signal_lost.c
             > Author: Toney Sun
             > Mail: [email protected]
       > Created Time: 2020年04月27日 星期一 15時03分18秒
 ************************************************************************/

#include<stdio.h>
#include<signal.h>

extern void my_msleep(int mseconds);//自己實現的睡眠函數,與庫函數並無區別

static void sig_handler(int signo)
{
      static int flag=0;
      printf("sig_handler finish: flag=%d\n", flag);
      if(signo == SIGINT){
            printf("Recieved SIGINT signal\n");
            flag++;
            my_msleep(5000);//延時5秒
      }else{
            printf("sig_handler receieve Error signal\n");
      }
      printf("sig_handler finish: flag=%d\n", flag);
}
int signal_lost_test()
{
     if(signal(SIGINT, sig_handler)==SIG_ERR){
           printf("SIGINT handle function register error\n");
     } 
}

執行結果如下:

toney@ubantu:/mnt/hgfs/em嵌入式學習記錄/schedule調度器$ ./demo.out 
^Csig_handler finish: flag=0
Recieved SIGINT signal
^C^C^Csig_handler finish: flag=1
sig_handler finish: flag=1
Recieved SIGINT signal
sig_handler finish: flag=2

	運行程序後我是通過連續按下四次“Ctrl+C”,產生四個SIGINT信號,但實際上只捕獲了其中的兩個信號,另外兩個則**丟失**了。之所以說丟失,是因爲程序只打印了兩次信號處理函數中的信息,該進程也只知道發生了兩次事件。出現這種情形的主要原因在於信號處理程序處理的太慢(程序中我們把她睡了會兒),而信號又發生的太頻繁,CPU處理不過來導致的。

​ 此外,上述結果中還有個奇怪的現象:信號處理程序不是一次性執行完畢的(專業點稱爲存在競態),而是在第一個執行過程中又去相應下一個信號處理函數,等下一個處理完畢了再回來繼續處理先前未執行完畢的信號處理函數。這個現像應該在編寫程序中特別注意下。

5. 可靠的信號

​ 在第4部分,我們簡要的說明了下什麼是不可靠的信號,這裏我們再來簡要的說明下什麼是可靠的信號。

5.1 常見的術語

  • 遞送: 當對信號採取某種動作時,我們說向進程遞送了一個信號。
  • 未決的:在信號產生(generate)和信號遞送之間的這段時間間隔內,稱信號爲未決的(pending)

5.2 可靠的信號說明

​ 進程可以選擇使用“阻塞信號遞送”。如果爲進程產生了一個阻塞的信號,而且對信號的動作是系統默認動作或捕捉該信號(非忽略狀態),則爲該進程將此信號保持爲未決狀態,直到該進程對此信號接觸阻塞狀態(或者修改爲忽略此信號)。

​ 內核在遞送一個原來被阻塞的信號給進程時,才決定對它的處理方式(因此之前的狀態稱爲未決的)。於是乎進程在信號遞送之前是可以修改對該信號的動作。系統可以調用sigpending函數來判斷哪些信號是設置爲阻塞同時懸而未決的。

​ 如果進程在解除多某個信號阻塞之前,該信號已經發生了多次(就是我們4.2的例子),那麼該如何處理呢?目前大多數系統仍然是隻遞送一次該信號,也就是說不支持信號排隊

​ 每一個進程都有一個信號屏蔽字(signal mask),它規定了當前要阻塞遞送到該進程的信號集。對於每一種可能的信號,該屏蔽字中都有與之相對應的位。如果該位被設置,則當前進程會阻塞該信號。程序中可以使用sigprocmask(後面我會詳細說明)來查詢和設置當前進程的信號屏蔽字。

​ 信號編號可能會超過一個整數所包含的二進制位數(32位系統是32位),因此POSIX.1專門定義了一個新的類型sigset_t,它可以容納一個信號集。信號屏蔽字就存放在其中一個信號集中。後面我們對這部分進行詳細說明。

6. 函數kill和raise

  • kill函數用來將信號發送給進程或者進程組。
  • raise函數則是進程用來向本進程發送信號的。
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
		返回值說明:成功返回0;失敗返回-1

調用

raise(signo);

相當於調用

kill(getpid(), signo);

函數kill的pid參數有以下四種不同的情況

序號 pid範圍 說明
1 pid > 0 將信號發送給進程ID爲pid的進程
2 pid == 0 將信號發送給與當前進程屬於同一進程組的所有進程(進程組ID相同的進程),當然這裏不包括系統進程和內核進程
3 pid < 0 將信號發送給進程組ID等於|pid|的所有進程。同樣不包括系統進程和內核進程
4 pid == -1 將信號發送給具有權限的其他所有進程

​ 這裏面有一個前提: 要麼是超級用戶,擁有所有的權限。要麼是擁有相同進程ID的進程,否則無法發送信號給其他進程。

7. 函數alarm和pause

7.1 alarm()

​ 使用alarm函數用來設置一個定時器,在將來的某一時間該定時器會超時。當定時器超時時,產生SIGALRM信號。如果忽略或者不捕捉該信號,則執行默認的動作:終止當前進程

#include <signal.h>
unsigned int alarm(unsigned int seconds);
	返回值說明: 0或者以前設置的鬧鐘時間剩餘的秒數。

​ 參數seconds的值是產生SIGALRM信號需要經過的秒數。當定時時間到時由內核產生,但是由於進程調度的延時,時間上有一定的延時。

​ 每一個進程只允許有一個鬧鐘時間:

/*************************************************************************
             > File Name: alarm.c
             > Author: Toney Sun
             > Mail: [email protected]
       > Created Time: 2020年04月27日 星期一 17時35分42秒
 ************************************************************************/

#include <stdio.h>
#include <signal.h>

void alarm_test(void)
{
	int ret;
	
	ret=alarm(10);
	printf("First alarm :ret = %d\n", ret);

	sleep(2);

	ret=alarm(2);
	printf("Second alarm :ret = %d\n", ret);
}

驗證結果如下:

toney@ubantu:/mnt/hgfs/em嵌入式學習記錄/schedule調度器$ ./demo.out 
First alarm :ret = 0
Second alarm :ret = 8
Alarm clock

​ 這會引入一個問題: 如果先前該進程已經註冊了一個鬧鐘時間,但是還沒有超時;如果重新設置定時器,先前剩餘的時間會最爲alarm()函數的返回值返回,與此同時,以前註冊的鬧鐘時間被取代而不再生效。 如果有以前註冊的尚未超時的鬧鐘時間,而且本地設置的seconds爲0,則是取消該鬧鐘,剩餘的時間仍然作爲alarm函數的返回值。

7.2 pause()

​ pause函數使調用進程掛起,直至捕捉到某個信號。

#include <signal.h>
int pause(void);
		返回值:-1, errno設置爲EINTR

​ 只有執行了一個信號處理程序並從其返回,pause函數纔會返回。它的返回值一直爲-1,並設置相應的錯誤碼。

8 信號集

8.1信號集的基本操作函數

​ 前面我們已經知道,不同的信號的編號可能超過一個整型量所包含的位數,因此我們不能使用整型量中的一位來表示一種信號,也就是說我們不能使用整型變量來表示信號集。POSIX.1定義了一種新的數據類型:信號集(sigset_t), 並且還定義了5個處理信號的函數。

#include <signal.h>

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
		上述四個函數返回值:若成功返回0;失敗返回-1
int sigismember(const sigset_t *set, int signo);
		返回值:爲真返回1;爲假返回0
  • 函數sigemptyset初始化由set指向的信號集,清除其中所有的信號;
  • 函數sigfillset初始化set指向的信號集,使其包含所有的信號;

所有的應用程序在使用信號集之前,都需要調用sigemptyset或者sigfillset對信號集進行初始化。一旦初始化了一個信號集,以後便可以對信號集進行增加、刪除特定的信號:

  • 函數sigaddset將一個信號添加到已有的信號集中;
  • 函數sigdelset從一個現有的信號集中刪除一個特定的信號;

對所有以信號集爲參數的函數,總是以信號集的地址作爲傳遞的參數。

​ 這個原因在於:因爲我們要在函數中修改信號集中的信號,因此需要地址傳遞方式(簡單的值傳遞是行不通的),否則無法實現真正的修改。

8.2 函數sigprocmask: 進程屏蔽字

​ 我們在前面已經提到過:進程的信號屏蔽字規定了當前阻塞而不能遞送給該進程的信號集。進程的信號屏蔽字是通過函數sigprocmask來進行檢測或修改的,或者同時進行檢測和修改。

#include <signal.h>
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
		返回值:成功返回0;失敗返回-1
  • 首先,如果oset是非空指針,那麼進程的當前信號屏蔽字通過oset返回
  • 其次,如果set爲非空指針,則參數how只是如何修改當前進程的信號屏蔽字;下表便是how的取值以及相關的說明。
  • 如果set爲空指針,則不修改該進程的信號屏蔽字,how值是無意義的
序號 how 說明
1 SIG_BLOCK 將set中的信號添加到當前進程的信號屏蔽字中(兩者取或運算,即並集)
2 SIG_UNBLOCK 將set中的信號從當前進程信號屏蔽字中刪除(set補集的交集)
3 SIG_SETMASK 將set設置爲當前進程的信號屏蔽字(不關心原進程的信號屏蔽字)

在調用sigprocmask後如果有任何懸而未決、不再阻塞的信號(原來信號是阻塞,現在改爲非阻塞),則在sigprocmask函數返回前至少將其中之一遞送給該進程。

示例:獲取當前進程屏蔽字

/*************************************************************************
             > File Name: sigprocmask_demo.c
             > Author: Toney Sun
             > Mail: [email protected]
       > Created Time: 2020年04月28日 星期二 08時15分06秒
 ************************************************************************/

#include <stdio.h>
#include <error.h>
#include <signal.h>
/*獲取當前進程的信號屏蔽字,並測試包括哪些信號。*/
void getSigProcMask()
{
      sigset_t set;
      printf("Enter %s\n", __func__);
      if(sigemptyset(&set)!=0){
            perror("sigemptyset error:");
      }else if(sigprocmask(0,NULL,&set)!=0){
            perror("sigprocmask error:");
      }else{
            printf("Sigprocmask contains following signals:");
            
            if(sigismember(&set, SIGINT))
                  printf("    SIGINT");
            if(sigismember(&set, SIGQUIT))
                  printf("    SIGQUIT");
            if(sigismember(&set, SIGALRM))
                  printf("    SIGALRM");
            if(sigismember(&set, SIGCHLD))
                  printf("    SIGCHLD");
            
            printf("\n");
      }
      printf("Exit %s\n", __func__);
}

8.3 函數sigpending

​ sigpending函數返回一個信號集,對於調用進程而言,其中的各信號是阻塞不能遞送的,因而也一定是當前懸而未決的。該信號集通過set參數返回。

#include <signal.h>
int sigpending(sigset_t *set);
		返回值:成功返回0;失敗返回-1

示例:查詢當前掛起的信號

/*************************************************************************
             > File Name: sigpending_demo.c
             > Author: Toney Sun
             > Mail: [email protected]
       > Created Time: 2020年04月28日 星期二 08時36分45秒
 ************************************************************************/

#include <stdio.h>
#include <signal.h>

/*初始化進程屏蔽字*/
void testPendingSignal()
{
      /*定義三個信號集*/
      sigset_t new_set;       /*,用來設置新的屏蔽字*/
      sigset_t old_set;       /*,用來獲取之前的信號屏蔽字*/
      sigset_t pending_set;   /*,獲取正在掛起的信號屏蔽字*/

      /*清空三個信號集*/
      sigemptyset(&new_set);
      sigemptyset(&old_set);
      sigemptyset(&pending_set);

      /*將SIGQUIT信號添加至信號集中*/
      sigaddset(&new_set, SIGINT);

      /*設置當前進程的信號屏蔽字*/
      if(sigprocmask(SIG_BLOCK, &new_set, &old_set)!=0){
            printf("%s error!!!\n", __func__);
            return;
      }
      printf("sleeping.......\n");
      sleep(5);

      if(sigpending(&pending_set)!=0){
            printf("%s error!!!\n", __func__);
            return;
      }
      if(sigismember(&pending_set, SIGINT))
            printf("SIGINT is in pending_set\n");

      if(sigismember(&pending_set, SIGQUIT))
            printf("SIGQUIT is in pending_set\n");


      /*將進程的信號屏蔽字回覆到修改之前的狀態*/
      if(sigprocmask(SIG_SETMASK, &old_set, NULL)!=0){
            printf("%s error!!!\n", __func__);
            return;
      }
      
      printf("%s...exit....\n", __func__);

}

void main(int argc, char *argv[])
{
	testPendingSignal();

	while(1){
		pause();
	}
}

程序的執行結果如下:

toney@ubantu:/mnt/hgfs/em嵌入式學習記錄/schedule調度器$ ./demo.out 
sleeping.......
^C^C^C^C^CSIGINT is in pending_set

toney@ubantu:/mnt/hgfs/em嵌入式學習記錄/schedule調度器$

​ 因爲我們在程序中設置了進程屏蔽字,所以進程會阻塞SIGINT信號。在sleep(5)的睡眠等待的過程中,我連續按了5次"Ctrl+C“產生了五次SIGINT信號。但是由於進程暫時屏蔽該信號,不會講該信號遞送給進程,因此程序也不會響應(SIGINT的默認動作是結束當前進程),而當我重新恢復進程的信號屏蔽字後,先前產生的SIGINT信號被立即遞送到進程(應該是在sigprocmask函數返回之前就遞送了),因此函數testPendingSignal()的最後一行並沒有打印出來。

爲了解除對該信號的阻塞,我們用先前的信號屏蔽字重新設置了(SIG_SETMASK)進程的信號屏蔽字。

這樣做的原因在於: 可能先前的進程已經屏蔽了該信號(例如其他函數接口設置了,但是自己並不知道)。如果我們簡單的使用SIG_UNBLOCK來將信號從進程屏蔽字中刪除可能會影響其他程序正常的功能。因此這裏推薦使用SIG_SETMASK的方式重新設置修改之前信號屏蔽字。

8.4 函數sigaction

​ sigaction函數的功能是檢查或者修改(也可以是檢查和修改)與指定信號相關聯的動作。 該函數是對signal函數的改進

#include <signal.h>
int sigaction(int signo, const struct sigaction *restrict act, struct sinaction *restrict oact);
		返回值:成功返回0;失敗返回-1

​ 其中:

  • 參數signo是要檢測或者修改其具體動作的信號編號。如果act指針非空,則要修改信號的動作;如果oact指針非空,則是通過oact指針將返回該信號的上一個處理動作(如果act爲空,即未修改,就是當前信號的處理動作)。struct sigaction的結構如下(Linux形式上可能有所不同):
struct sigaction{
	void (*sa_handler)(int); /*addr of signal handler*/
							 /*or SIG_IGN, or SIG_DFL*/
	sigset_t sa_mask;		 /*adddtional signals to block*/
	int 	 sa_flags;		 /*signal options, 詳細信息見下表*/
	/*alternate handler*/
	void (*sa_sigaction)(int, siginfo_t *, void *);
};

​ 當設置信號動作時,如果sa_handler指針指向一個信號捕捉函數的地址(不是常量SIG_IGN、SIG_DEL), 則sa_mask字段說明了一個信號集。 在調用該信號捕捉函數時,該信號屏蔽字要添加到進程的信號屏蔽字中,當信號處理函數處理完畢後,在重新將進程的信號屏蔽字恢復爲原先的值。這樣做最主要的目的在於:防止兩個相同的信號同時(間隔很短)到來時產生競態。也就是說應該在處理該信號的過程中阻塞該類型的後續信號

​ 當然,這個函數也是不支持信號入隊的:如果阻塞過程中發生了多個該信號,那麼只會遞送一次該信號。

  • sa_flags字段指明瞭對信號處理的各個選項:
序號 選項 說明
1 SA_INTERRUPT 由此信號中斷的系統調用不自動重啓(sigaction默認處理方式)
2 SA_NOCLDSTOP 若signo爲SIGCHLD, 當子進程停止時不產生此信號;當子進程終止時產生此信號
3 SA_NOCLDWAIT 若signo爲SIGCHLD, 當子進程終止時不產生殭屍進程;若調用進程隨後調用wait,則阻塞到它所有進程都終止。
4 SA_NODEFER 捕捉到此函數時,在執行信號捕捉函數時系統不自動阻塞該信號(除非sa_mask中包括此信號)
5 SA_ONSTACK 遞交給替換棧上的進程
6 SA_RESETHAND 在信號捕捉函數入口處將此信號的處理函數改爲SIG_DFL, 並清除SA_SIGINFO標誌
7 SA_RESTART 由此信號中斷的系統調用自動重啓
8 SA_SIGINFO 此選項對信號處理程序提供一個附加信息。
  • sa_sigaction字段是一個替代的信號處理程序,在sigaction結構中使用SA_SIGINFO標誌時,使用該信號處理程序。對於sa_handlersa_sigaction,實現上一般是一個共用體,因此應用只能一次使用他們中的一個。下面是Linux2.6.12上的實現,從這裏可以看出_sa_sigaction和sa_handler是一個共用體:
struct sigaction {
	union {
	  __sighandler_t _sa_handler;
	  void (*_sa_sigaction)(int, struct siginfo *, void *);
	} _u;
	sigset_t sa_mask;
	unsigned long sa_flags;
	void (*sa_restorer)(void);
};

示例:使用sigaction實現signal

/*************************************************************************
             > File Name: sigaction2signal.c
             > Author: Toney Sun
             > Mail: [email protected]
       > Created Time: 2020年04月28日 星期二 10時30分48秒
 ************************************************************************/

#include <stdio.h>
#include <signal.h>

/*
*     我們使用sigaction函數來實現signal的功能
*/

typedef void Sigfunc(int);
Sigfunc *signal(int signo, Sigfunc *func)
{
      struct sigaction act, oact;

      act.sa_handler = func;
      sigemptyset(&act.sa_mask);
      act.sa_flags = 0;

      #ifdef SA_INTERRUPT
            act.sa_flags |= SA_INTERRUPT;
      #endif
      if(sigaction(signo, &act, &oact) < 0)
            return (SIG_ERR);

      return (oact.sa_handler);
}

static void  alarm_handler(int signo)
{
      printf("Catch  SIGALRM signal!!!\n");
}

void sigaction2signal_demo()
{
      signal(SIGALRM, alarm_handler);

      alarm(1);
      sleep(2);
      alarm(1);
}

void main(int argc, char *argv[])
{
	//signal_demo();
	//exec_funcs();
	//signal_lost_test();

	sigaction2signal_demo();
	
	while(1){
		pause();
	}
}

執行結果如下:

toney@ubantu:/mnt/hgfs/em嵌入式學習記錄/schedule調度器$ ./demo.out 
Catch  SIGALRM signal!!!
Catch  SIGALRM signal!!!
^C
toney@ubantu:/mnt/hgfs/em嵌入式學習記錄/schedule調度器$

8.5 函數sigsuspend

​ 首先看sigsuspend的函數原型:

#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
		返回值:-1

sigsuspend函數的使用方法在下面的示例中進行說明。·

示例1:錯誤用法

​ 我們已經知道,修改進程的信號屏蔽字可以阻塞所選擇的信號,或者解除對他們的阻塞。使用這種技術可以保護不希望被信號打斷的臨界代碼區。如果希望對一個信號解除阻塞,然後調用pause以等待以前被阻塞的信號發生,發生什麼事情呢? 我們依然使用程序來說明:

/*************************************************************************
             > File Name: sigsuspend_demo.c
             > Author: Toney Sun
             > Mail: [email protected]
       > Created Time: 2020年04月28日 星期二 10時58分19秒
 ************************************************************************/

#include <stdio.h>
#include <signal.h>


static void handler(int signo)
{
      printf("Catch SIGINT!!!\n");
}

void invalid_usages()
{
      sigset_t newmask, oldmask;

      sigfillset(&newmask);//阻塞所有信號
      sigemptyset(&oldmask);

      //sigaddset(&newmask, SIGINT);
      signal(SIGINT, handler);
      if(sigprocmask(SIG_BLOCK, &newmask, &oldmask)<0){
            printf("sigprocmask SIG_BLOCK error\n");
            return;
      }
      /*臨界代碼區:critical region of code   begin*/
      sleep(2);
            /*此區域的代碼不會被信號打斷*/

      /*臨界代碼區:critical region of code   end*/
      /*解除對SIGINT的阻塞*/
      if(sigprocmask(SIG_SETMASK, &oldmask, NULL)<0){
            printf("sigprocmask SIG_SETMASK error\n");
            return;
      }   
      printf("Before pause\n");
      pause();
      printf("After pause\n");
}
void main(int argc, char *argv[])
{
	//signal_demo();
	//exec_funcs();
	//signal_lost_test();

	invalid_usages();
}

我在程序執行到sleep(2);時連續按下“Ctrl+C”, 這裏會被阻塞,沒有什麼問題。2秒過後,進程屏蔽字解除信號阻塞,然後調用pause()等待信號的到來。那麼問題來了,信號是在sigprocmask調用返回之前就遞送到進程的,因此paus()是捕捉不到SIGINT信號的,之後程序會一直阻塞,直到下一個信號的到來(後面我又重新按下“Ctrl+C“退出程序的)。結果如下:

toney@ubantu:/mnt/hgfs/em嵌入式學習記錄/schedule調度器$ ./demo.out 


^C^C^C^C^CCatch SIGINT!!!
Before pause



^CCatch SIGINT!!!
After pause
toney@ubantu:/mnt/hgfs/em嵌入式學習記錄/schedule調度器$ 

針對上述的問題,系統實現了一個原子操作:解除阻塞信號並等待

之前,我對於這個sigsuspend()函數的功能總是不得要領,不明白它的真正目的。現在有一點清楚了: suspend()函數的出現就是爲了解決上述的問題。它的作用包括兩部分:

  • 修改當前的進程屏蔽字(將其修改爲sigmask
  • 阻塞當前進程
  • 非sigmask中的信號到來時:
    • 首先執行信號的處理函數
    • 然後恢復進程的信號屏蔽字(調用suspend之前的信號屏蔽字)
    • 最後進程繼續運行

上述這幾步都是suspend函數自動完成的。

示例2:suspend的用法

void sigsuspend_demo()
{
      sigset_t newmask, oldmask, waitmask;

      sigfillset(&newmask);//阻塞所有信號
      sigemptyset(&oldmask);
      sigfillset(&waitmask);//等待所有信號

      sigdelset(&waitmask, SIGINT);//等待除了SIGINT之外的所有信號

      signal(SIGINT, handler);
      if(sigprocmask(SIG_BLOCK, &newmask, &oldmask)<0){
            printf("sigprocmask SIG_BLOCK error\n");
            return;
      }
      if(sigsuspend(&waitmask) < 0){
            printf("sigsuspend error\n");
            return;
      }
      /*臨界代碼區:critical region of code   begin*/
      sleep(2);
            /*此區域的代碼不會被SIGINT信號打斷*/

      /*臨界代碼區:critical region of code   end*/
      
      /*阻塞等待SIGINT信號*/
      if(sigsuspend(&waitmask) < 0){
            printf("sigsuspend error\n");
            return;
      }
    
     /* 
     	.... 
     */

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