Linux之進程信號

1. 信號概述

在Linux這個多用戶多進程的系統中,信號的存在是必然的。信號可以理解爲一個軟中斷,在某個條件下,系統會發出某個信號給正在運行的進程,通知進程需要執行某一特定的事件。

1.1 在終端中查看常見的信號
在終端輸入命令"kill -l",可以看出Linux系統中的所有信號。(每個信號類型前面都有一個正整數,這個正整數與信號代表相同的含義,稱之爲信號編號)。

信號的宏定義和編號都定義在signal.h的頭文件中。在終端中可以通過輸入命令"man 7 signal"查看Linux系統中支持的信號的詳細含義。

下圖爲Linux支持的所有的信號:
Linux支持的所有信號
1.2 信號處理
信號作爲一種進程間通信的機制,主要用於處理異步事件。通常,如果沒有信號發送到正在執行的進程中,進程會有如下3種處理信號的方法:
(1) 默認信號的處理方法。系統爲每一個信號都設置了默認的處理方法,通常爲終止進程。
(2) 捕捉信號,使進程執行指定的程序代碼。
(3) 忽略信號,對該信號不做任何處理,進程繼續執行。
這三種處理捕捉到的信號的方法只是比較基本的方法。在實際應用中,對信號的處理並不會這麼單一。例如,有些進程在執行時不希望被信號突然打斷,但是又不希望忽略此信號,此時進程會將該信號掛起,需要時再回頭處理它。

2. 產生信號

信號的產生有多種多樣,主要有如下幾種:
(1) 可以通過鍵盤終端產生。例如:使用Ctrl+C可以產生SIGINT信號;使用Ctrl+\可以產生SIGQUIT信號;使用Ctrl+Z可以產生SIGSTP信號。
(2) 通過終端中的kill命令產生信號,使用格式如下:

kill -信號類型 進程號
//進程號可以通過命令"ps -aux"獲取

信號類型可以輸入信號的編號,也可以輸入信號的宏定義,例如,命令“kill -SIGTERM 進程號”或者"kill -15 進程號",表示編號爲15、宏定義爲SIGTERM的信號用來結束指定的進程。
(3) 調用系統函數向進程發送信號
在Linux系統中,kill()、raise()和alarm()函數都可以產生信號。

2.1 kill()函數
前面提到的在終端中通過kill命令產生信號的方法,原理是主要通過kill命令調用了kill()函數實現了這個功能。
kill()函數主要用於向指定的進程或進程組發送信號,該函數的定義如下:

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

參數pid爲進程號或進程組號;參數sig爲要發送的信號類型的編號。如果參數sig爲0,就沒有信號可以發送,但會進行錯誤檢查。

2.2 raise()函數
raise()函數主要用於將信號發送給當前進程,該函數原型:

#include <signal.h>
int raise(int sig);

參數sig爲發送的信號類型的編號。
如果函數調用成功,返回0;失敗,返回非0.
由raise()函數的功能可以知道,使用kill(getpid(), sig)也可以實現該功能。

2.3 alarm()函數
該函數主要用於爲發送的信號設定一個時間警告,使系統在設定的時間之後發送信號,函數原型爲:

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

參數seconds爲設定的時間值。如果seconds爲0,那麼alarm()函數設置的警告時鐘將無效。
alarm()函數安排在seconds時間後,發送一個信號SIGALRM給進程。在默認的情況下,進程收到SIGALRM
信號會終止運行。如果不希望終止,可以在進程捕捉到該信號後修改默認的處理函數。
調用alarm()函數後,之前設置的任何警告時鐘都將取消。

3. 捕捉信號

從前面信號的介紹中瞭解到有3種對信號的處理方法,一種是系統對信號的默認處理方法;一種是忽略;一種是捕捉信號。其實,對於忽略信號和捕捉信號,都是修改系統默認信號的處理方式。在Linux系統中,可以使用signal()函數和sigaction()函數對默認信號處理方法進行修改。

3.1 signal()函數
signal()函數用於修改某個信號的處理方法,該函數的定義如下:

 #include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

如果signal()函數調用成功,返回先前的信號,並處理調用的函數指針;反之返回SIG_ERR.
參數signum代表信號類型的編號;參數handler代表指向信號新的處理方法的指針,如果指針指向一個函數,那麼捕捉到signum信號時,會執行這個特殊函數處理信號;參數handler還可以設置爲SIG_IGN或SIG_DFL, SIG_IGN代表忽略該信號,而SIG_DFL代表採取默認的處理方法。
使用一個自己定義的特殊函數作爲信號的處理方法,這種處理信號的方法叫做“捕捉信號”。
在系統提供的信號類型中,SIGKILL和SIGSTOP信號不能被捕獲或者忽略。

示例代碼:signal()函數的使用

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

void sigint(int sig);
void sigcont(int sig);

int main()
{
	char a[100];
	if(signal(SIGINT, &sigint)==SIG_ERR)
	{
		//修改SIGINT信號的處理方法爲sigint()函數
		perror("sigint signal error!");	
	}
	if(signal(SIGCONT,&sigcont)==SIG_ERR)//修改SIGCONT信號處理方法爲sigcont函數
	{
		perror("sigcont error!");
	}
	if(signal(SIGQUIT, SIG_IGN)) //修改SIGQUIT信號的處理方法爲SIG_IGN
	{
		perror("sigquit error!");
	}
	printf("current process is:%d\n\n", getpid()); //獲取當前進程ID
	while(1)
	{
		printf("input a:");
		fgets(a, sizeof(a), stdin);//獲取鍵盤輸入的字符串
		if(strcmp(a, "terminate\n")==0)  //比較字符串a與terminate字符
		{
			raise(SIGINT);  //若兩個字符串相同,則將SIGINT信號發送給當前進程
		}
		else if(strcmp(a, "continue\n") == 0)
		{
			raise(SIGCONT);//獲取的字符串若與比較的字符串相同,則產生SIGCONT信號給當前進程
		}
		else if(strcmp(a, "quit\n") == 0)
		{
			raise(SIGQUIT);
		}
		else if(strcmp(a,"game over\n") == 0)
		{
			raise(SIGTSTP);
		}
		else
		{
			printf("your input is: %s\n\n", a);
		}
	}
	return 0;
}

void sigint(int sig)  //SIGINT信號的新的處理方法
{
	printf("SIGINT signal %d.;\n", sig);
}

void sigcont(int sig)   //SIGCONT信號的新的處理方法
{
	printf("SIGCONT signal %d.;\n", sig);
}

在這裏插入圖片描述
sigaction()函數
sigaction()函數主要用於讀取和修改指定信號的處理動作,定義如下:

#include <signal.h>
int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);

參數signum表示要捕獲信號類型的編號;參數act和oldact都是指向sigaction結構體的指針。參數act表示需要修改的指定的新的處理動作,而該信號的原有處理動作保存到參數oldact指向的緩衝區中。(如果act和oldact都指向空,則兩個指針參數不會實現上述功能。)

示例代碼:
調用sigaction()函數修改SIGINT信號的處理方法,修改爲顯示接收到的信號編號,並累加計時,直到收到下一個信號。

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

int i = 0;
void new_handler(int sig) /*SIGINT信號的新的處理方法*/
{
	printf("receive signal number is: %d\n", sig);
	for(; i<100; i++)  /*每隔一秒累加計時*/
	{
		printf("sleep2  %d\n", i);
		sleep(1);
	}
}

int main()
{
	struct sigaction newact, oldact;
	newact.sa_handler = new_handler; //處理方法
	sigaddset(&newact.sa_mask, SIGQUIT);  //將SIGQUIT信號加到新的處理方法的屏蔽信號中
	newact.sa_flags = SA_RESETHAND | SA_NODEFER;
	printf("change SIGINT(2)signal__[ctrl+c]\n");
	sigaction(SIGINT, &newact, &oldact);
	while(1)
	{
		sleep(1);
		printf("sleep1  %d\n", i);
		i++;
	}
	return 0;
}

在這裏插入圖片描述

4. 信號的阻塞

信號的處理並沒有那麼簡單,有時候進程並不希望被突如其來的信號中斷當前的執行,也不希望此信號被忽略,而是希望一段時間後再去處理這個信號。在這裏,就要用阻塞信號的方法來實現。
這裏有三個系統調用函數,分別是sigprocmask(), sigsuspend(), sigpending()。
說明:信號屏蔽字就是進程中被阻塞的信號集,這些信號不能發送給該進程,它們在該進程中被“屏蔽”了,也就是被阻塞了。

4.1 sigprocmask()函數
可用於檢測和改變進程的信號掩碼,定義如下:

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
//該函數只爲單線程定義的,在多線程中要使用pthread_sigmask變量,使用之前要聲明和初始化。

參數how表示修改信號屏蔽字的方式;newset表示把這個信號集設爲新的信號屏蔽字,如果爲NULL,則不改變;oldset表示保存進程舊的信號屏蔽字,如果爲NULL,則不保存。
how的取值有以下幾個:
SIG_BLOCK:代表是將newset所指向的信號集中所包含的信號加到當前的信號掩碼中作爲新的信號屏蔽字;
SIG_UNBLOCK:將參數newset所指向的信號集中的信號從當前的信號掩碼中移除;
SIG_SETMASK: 設置當前信號掩碼爲參數newset所指向的信號集中所包含的信號。
函數成功調用返回0,失敗返回-1.

4.2 sigsuspend()函數
該函數實現的是等待一個信號的到來,即將當前進程掛起,定義如下:

 #include <signal.h>
int sigsuspend(const sigset_t *mask);

mask是一個sigset_t結構體類型的指針,指向一個信號集。當該函數被調用時,參數mask所指向的信號集中的信號被複制給信號掩碼。隨後,進程會被掛起,直到信號被捕捉到,執行信號相應的處理方法返回時,該函數才返回。此時,信號掩碼恢復爲調用前的值。

4.3 sigpending()函數
在調用信號屏蔽的相關函數後,被屏蔽的信號對於調用進程是阻塞的,不能發送給調用進程,因此是待定(pending)的,而調用該函數可以取得這些阻塞的信號集,定義如下:

#include <signal.h>
int sigpending(sigset_t *set);

set爲指向一個信號集的sigset_t 類型的結構體指針。調用sigpending()函數成功時,參數set會取得被懸掛的信號集,返回值爲0;若失敗,返回-1.

示例代碼:調用信號阻塞函數將SIGINT信號阻塞。

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

static void sig_handler(int signo)   //自定義的信號SIGINT處理函數
{
	printf("信號SIGINT被捕捉!\n");
}

int main()
{
	sigset_t new, old, pend;
	if(signal(SIGINT, sig_handler) == SIG_ERR)  //註冊一個信號處理函數sig_handler
	{
		perror("signal");
		exit(1);
	}
	if(sigemptyset(&new) < 0)   //清空信號集
		perror("sigemptyset");
	if(sigaddset(&new, SIGINT)<0)  //向new信號集中添加SIGINT信號
		perror("sigaddset");
	if(sigprocmask(SIG_SETMASK, &new, &old) < 0)  //將信號集new阻塞
	{
		perror("sigpromask");
		exit(1);
	}
	printf("SIGQUIT被阻塞! \n");
	printf("試着按下Ctrl+C,程序會暫停5秒等待處理事件! \n");
	sleep(5);
	if(sigpending(&pend) < 0) //獲得未決的信號類型
		perror("sigpending");
	if(sigismember(&pend, SIGINT))  //檢查信號SIGINT是否爲未決的信號類型
		printf("SIGINT信號未決!\n");
	if(sigprocmask(SIG_SETMASK, &old, NULL) < 0) //恢復爲原始的信號掩碼,解開阻塞
	{
		perror("sigpromask");
		exit(1);
	}
	printf("SIGINT已被解開阻塞 \n");
	printf("再按下 Ctrl+C  \n");
	sleep(5);
	return 0;
}

運行結果:
在這裏插入圖片描述
該程序中,首先註冊了一個SIGINT的信號處理函數,改變了SIGINT信號的默認處理方法,然後阻塞了該信號的處理方法。因此,當在提示下按 Ctrl+C時,系統沒有反應,沒有捕捉信號的處理方法,通過sigpending()函數獲取了未決信號的類型,在調用sigprocmask()函數解開阻塞時,才捕捉到該信號的處理方法,這時再在提示信息下按Ctrl+C時,系統會立即捕捉該信號的處理方法。

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