进程间通信之信号

643 阅读17分钟

信号是软件中断,它提供了一种处理异步事件的方法,需要告诉内核在某个信号出现时按照下面三种方式之一进行处理:忽略此信号、捕捉信号、执行系统默认动作。 在利用信号进行进程间通信之前,先介绍下信号及其常用处理函数。

哪些情况会引发信号?

1.键盘事件 ctrl +c ctrl +\
2.非法内存 如果内存管理出错,系统就会发送一个信号进行处理
3.硬件故障 同样的,硬件出现故障系统也会产生一个信号
4.环境切换 比如说从用户态切换到其他态,状态的改变也会发送一个信号,这个信号会告知给系统

“kill -l”这个命令可以查看所有的信号,现在信号已经增加到65个了,其中从33-64这些信号一般不会采用,这是为了区分可靠信号(34-64重新设计的一套信号集合 ,不会出现信号丢失,支持排队,信号处理函数执行完毕,不会恢复成缺省处理方式,实时信号是可靠信号,非实时信号不可靠信号)和不可靠信号(1.信号处理函数执行完毕,信号恢复成默认处理方式(Linux已经改进) ;2.会出现信号丢失,信号不排队;1-31 都是不可靠的,会出现信号丢失现象 )而新增加的32个信号。

“kill -信号值 pid”发送信号给指定进程。

1、signal函数

声明:
	#include <signal.h>
	typedef void (*sighandler_t)(int);
	sighandler_t signal(int signum, sighandler_t handler);
参数说明:
	第一个参数signum:指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。   
	第二个参数handler:描述了与信号关联的动作,它可以取以下三种值:
	(1)SIG_IGN  表示忽略该信号
	(2)SIG_DFL   表示恢复对信号的系统默认处理
	(3sighandler_t类型的函数指针
		当接收到一个类型为sig的信号时,就执行handler 所指定的函数。(int)signum是传递给它的唯一参数。执行了signal()调用后,
		进程只要接收到类型为sig的信号,不管其正在执行程序的哪一部分,就立即执行handler函数。
返回值:
	成功返回函数地址,该地址为此信号上一次注册的信号处理函数地址,如果有错误则返回SIG_ERR(-1)。

注意:当一个信号的信号处理函数执行时,如果进程又接收到了该信号,该信号会自动被储存而不会中断信号处理函数的执行,直到信号处理函数执行完毕再重新调用相应的处理函数。但是如果在信号处理函数执行时进程收到了其它类型的信号,该函数的执行就会被中断。 

示例:

	int main(void){
		if(signal(SIGUSR1, sig_usr) == SIG_ERR){
			...
		}
		if(signal(SIGUSR2, sig_usr) == SIG_ERR){
			...
		}
		for(;;)
			pause();
	}
	void sig_usr(int signo){  /*argument is signal number*/
		if(signo == SIGUSR1){
			...
		}
		else if(signo == SIGUSR2){
			...
		}
		else{
			...
		}
	}

2、sigaction函数 

signal 函数的使用方法简单,但并不属于 POSIX 标准,在各类 UNIX 平台上的实现不尽相同,因此其用途受到了一定的限制。而 POSIX 标准定义的信号处理接口是 sigaction 函数。

头文件:#include <signal.h>
定义:	int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数:		
	signum:要操作的信号。
	act:要设置的对信号的新处理方式。struct sigaction类型用来描述对信号的处理,定义如下:
		struct sigaction
		{
			void     (*sa_handler)(int);
			void     (*sa_sigaction)(int, siginfo_t *, void *);
			sigset_t  sa_mask;
			int       sa_flags;
			void     (*sa_restorer)(void);
		};
		成员 sa_handler 是一个函数指针,其含义与 signal 函数中的信号处理函数类似。
		成员 sa_sigaction 则是另一个信号处理函数,它有三个参数,可以获得关于信号的更详细的信息。
                    当 sa_flags 成员的值包含了 SA_SIGINFO 标志时,系统将使用 sa_sigaction 函数作为信号
                    处理函数,否则使用 sa_handler 作为信号处理函数。在某些系统中,成员 sa_handler 与 
                    sa_sigaction 被放在联合体中,因此使用时不要同时设置。
		sa_mask 成员用来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,它
                    自身会被自动放入进程的信号掩码,因此在信号处理函数执行期间这个信号不会再度发生。
		sa_flags 成员用于指定信号处理的行为,它可以是以下值的“按位或”组合。
			SA_RESTART:使被信号打断的系统调用自动重新发起。
			SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。
			SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程。
			SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
			SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
			SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。
		re_restorer 成员则是一个已经废弃的数据域,不要使用。
	oldact:原来对信号的处理方式。
返回值:0表示成功,-1表示有错误发生。

示例:

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

	static void sig_usr(int signum)
	{
		if(signum == SIGUSR1)
		{
			printf("SIGUSR1 received\n");
		}
		else if(signum == SIGUSR2)
		{
			printf("SIGUSR2 received\n");
		}
		else
		{
			printf("signal %d received\n", signum);
		}
	}

	int main(void)
	{
		char buf[512];
		int  n;
		struct sigaction sa_usr;
		sa_usr.sa_flags = 0;
		sa_usr.sa_handler = sig_usr;   //信号处理函数

		sigaction(SIGUSR1, &sa_usr, NULL);
		sigaction(SIGUSR2, &sa_usr, NULL);

		printf("My PID is %d\n", getpid());

		while(1)
		{
			if((n = read(STDIN_FILENO, buf, 511)) == -1)
			{
				if(errno == EINTR)
				{
					printf("read is interrupted by signal\n");
				}
			}
			else
			{
				buf[n] = '\0';
				printf("%d bytes read: %s\n", n, buf);
			}
		}    
		return 0;
	}
输出:
	My PID is 5904 
	SIGUSR1 received	(从另外一个终端向进程发送 SIGUSR1 或 SIGUSR2 信号,用类似如下的命令:kill -USR1 5904)
	read is interrupted by signal

由上可见,sigaction 注册信号处理函数时,不会自动重新发起被信号打断的系统调用。如果需要自动重新发起,则要设置 SA_RESTART 标志,比如在上述例程中可以进行类似以下的设置:sa_usr.sa_flags = SA_RESTART;

3、sigsetjmp和siglongjmp函数 

在信号处理程序中经常调用longjmp函数以返回到程序的主循环中,而不是从该处理程序返回。但是,调用longjmp有一个问题。当捕捉到一个信号时,进入信号捕捉函数,此时当前信号被自动地加到进程的信号屏蔽字中。这阻止了后来产生的这种信号中断该信号处理程序(仅当从信号捕捉函数返回时再将进程的信号屏蔽字复位为原先值)。如果用longjmp跳出信号处理程序,那么,对此进程的信号屏蔽字会发生什么呢?setjmp和longjmp保存和恢复信号屏蔽字,还是不保存和恢复,不同的实现各有不同。 

POSIX.1并没有说明setjmp和longjmp对信号屏蔽字的作用,而是定义了两个新函数sigsetjmp和siglongjmp。在信号处理程序中进行非局部转移时使用这两个函数。

	#include <setjmp.h>
	int sigsetjmp(sigjmp_buf env, int savemask);	返回值:若直接调用则返回0,若从siglongjmp调用返回则返回非0void siglongjmp(sigjmp_buf env, int val);
	如果savemask非0,则sigsetjmp在env中保存进程的当前信号屏蔽字。调用siglongjmp时,如果带非0 savemask的sigsetjmp调
        用已经保存了env,则siglongjmp从其中恢复保存的信号屏蔽字。

示例(摘自APUE):

	#include "apue.h"
	#include <setjmp.h>
	#include <time.h>

	static void			sig_usr1(int), sig_alrm(int);
	static sigjmp_buf   jmpbuf;
	static volatile sig_atomic_t	canjump;
	//sig_atomic_t,这是由ISO C标准定义的变量类型,在写这种类型的变量时不会被中断。
	//这种类型的变量总是包括ISO类型修饰符voaltile。
	int main(void)
	{
		if (signal(SIGUSR1, sig_usr1) == SIG_ERR)
			err_sys("signal(SIGUSR1) error");

		if (signal(SIGALRM, sig_alrm) == SIG_ERR)
			err_sys("signal(SIGALRM) error");

		pr_mask("starting main: ");

		if (sigsetjmp(jmpbuf, 1))
		{
			pr_mask("ending main: ");  // pr_mask打印调用进程的信号屏蔽字中信号的名称
			exit(0);
		}
		canjump = 1;    /* now sigsetjmp() is OK */

		for(; ;)
			pause();
	}

	static void sig_usr1(int signo)
	{    
		time_t starttime;

		if (canjump == 0)
			return;        /* unexpected signal, ignore */

		pr_mask("starting sig_usr1: ");
		alarm(3);          /* SIGALRM in 3 seconds */
		starttime = time(NULL);
		for(; ;)           /* busy wait for 5 seconds */
			if (time(NULL) > starttime + 5)
				break;
		pr_mask("finishing sig_usr1: ");

		canjump = 0;
		siglongjmp(jmpbuf, 1);    /* jump back to main, don't return */
	}

	static void sig_alrm(int signo)
	{
		pr_mask("in sig_alrm: ");
	}	
输出:
	$ ./a.out &
	starting main
	[1] 	531
	$ kill -USR1 531				向该进程发送SIGUSR1
	starting sig_usr1: SIGUSR1
	$ in sig_alrm: SIGUSR1 SIGALRM
	finishing sig_usr1: SIGUSR1
	ending main:

4、可重入函数 

可重入函数是指函数可以由多个任务并发使用,而不必担心数据错误。 

编写可重入函数:
	a.不使用(返回)静态的数据、全局变量(除非用信号量互斥);
	b.不调用动态分配、释放的函数;
	c.不调用任何不可重入的函数(如标准I/O函数)。
	d.进行了浮点运算,许多处理器/编译器中,浮点一般都是不可重入的,浮点运算大多使用协处理器或者软件模拟来实现。

注:即使信号处理函数使用的都是可重入函数,也要注意进入处理函数时,首先保存errno变量的值,结束时,再恢复原值。因为信号处理过程中,errno的值随时可能被改变。(这里有点不理解!改变就改变了呗,有什么问题?) 一种说法:因为每个线程只有一个errno变量,信号处理函数可能会修改其值,要了解经常被捕捉到的信号是SIGCHLD(在一个进程终止或者停止时,将SIGCHLD信号发送给其父进程,按系统默认将忽略此信号,如果父进程希望被告知其子系统的这种状态,则应捕捉此信号),其信号处理程序通常要调用一种wait函数,而各种wait函数都能改变errno。常见可重入函数如下:

5、信号集 

一种能够表示多个信号——信号集的数据类型。通常信号种类数目可能超过一个整形量所包含的位数,所以不能用一个整形量的其中一位代表一种信号,也就不能用一个整形量表示信号集。POSIX.1定义了数据类型sigset_t以包含一个信号集,并且定义了下列五个处理信号集的函数。

#include <signal.h> 
int sigemptyset(sigset_t *set);//初始化当前的信号集,并将所有信号排除在外 
int sigfillset(sigset_t *set); //将所有信号的信号集设置为满 
int sigaddset(sigset_t *set, int signum);//往信号集中增添信号 
int sigdelset(sigset_t *set, int signum);//从信号集中删除某个信号
					以上成功返回0,出错返回-1
int sigismember(const sigset_t *set, int signum);//检测信号是否在信号集中
					若真返回1,若假返回0,出错返回-1

6、信号阻塞集 

每个进程都有一个阻塞集,它用来描述哪些信号递送到该进程的时候被阻塞(在信号发生时记住它,直到进程准备好时再将信号通知进程)。所谓阻塞并不是禁止传送信号,而是暂缓信号的传送。若将被阻塞的信号从信号阻塞集中删除,且对应的信号在被阻塞时发生了,进程将会收到相应的信号。

比如,当前2号、4号、7号和11号信号在阻塞集中,则此时发送这4个信号过来,进程不会响应,但是这些发送过来的信号会排队。以4号信号为例,当4号信号从阻塞集中去除后,排队的信号就会被重新响应。但是需要注意的是,阻塞期间同一个信号多次到来只会有一次进行排队,即4号信号目前已经在排队了,那后续收到不管多少次该信号,都不会重复进入到排队序列中。因此,阻塞集合中的信号不重复。

(1)sigprocmask函数

#include <signal.h>
int sigprocmask( int how, const sigset_t *restrict set, sigset_t *restrict oset );
返回值:若成功则返回0,若出错则返回-1	
首先,若oset是非空指针,那么进程的当前信号屏蔽字通过oset返回。
其次,若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。
如果set是空指针,则不改变该进程的信号屏蔽字,how的值也无意义。
注意:
	a.不能阻塞SIGKILL和SIGSTOP信号。
	b.如果调用sigprocmask解除了对当前若干个未决信号的阻塞,且这若干个信号中有信号在阻塞期间到达了,
        则在sigprocmask返回前,至少会将其中一个信号递送给进程。

how				说明
SIG_BLOCK		该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集。set包含了我们希望阻塞的附加信号
SIG_UNBLOCK		该进程新的信号屏蔽字是其当前信号屏蔽字和set所指向信号集补集的交集。set包含了我希望解除阻塞的信号
SIG_SETMASK		该进程新的信号屏蔽字将被set指向的信号集的值代替	

(2)sigpending函数

sigpending返回信号集,其中的各个信号对于调用进程是阻塞的而不能递送,因而也一定是当前未决的,该信号集通过set参数返回。

#include <signal.h> 
int sigpending(sigset_t *set); //读取当前进程的未觉信号集 
			成功返回0,失败返回-1

示例:
void sig_quit(int signo){
	printf("caught SIGQUIT\n");
	if(signal(SIGQUIT, SIG_DFL) == SIG_ERR){
		printf("can't reset SIGQUIT");
	}
}

int main(void){
	sigset_t newmask, oldmask, pendmask;

	if(signal(SIGQUIT, sig_quit) == SIG_ERR)
		printf("can't catch SIGQUIT");

	//Block SIGQUIT and save current signal mask
	sigemptyset(&newmask);
	sigaddset(&newmask, SIGQUIT);
	if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0){
		printf("SIG_BLOCK error");
	}

	sleep(5);  //SIG_QUIT here will remain pending

	if(sigpending(&pendmask) < 0){
		printf("sigpending error");
	}
	if(sigismember(&pendmask, SIG_QUIT)){
		printf("\nSIG_QUIT pending\n");
	}

	//Reset signal mask which unblocks SIG_QUIT
	if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0){
		printf("SIG_SETMASK error");
	}
	printf("SIGQUIT unblocked\n");

	sleep(5);
	exit(0);
}

输出:
./a.out
^\			产生信号一次(5秒钟之内)
SIG_QUIT pending	从sleep返回后
caught SIGQUIT		在信号处理程序中
SIGQUIT unblocked	从sigprocmask返回后
^\Quit(coredump)	再次产生信号

./a.out
^\^\^\^\^\^\^\^\	产生信号多次(5秒钟之内)
SIG_QUIT pending
caught SIGQUIT		只递送一次(未排队)
SIGQUIT unblocked
^\Quit(coredump)

7、sigsuspend函数

考虑下面一段代码:

sigemptyset(&new);
sigaddset(&new, SIGINT);
sigprocmask(SIG_BLOCK, &new, &old);  //将SIGINT信号阻塞,同时保存当前信号集
printf("Blocked");
sigprocmask(SIG_SETMASK, &old, NULL);  //取消阻塞
pause();
return 0

本来期望pause()之后,来SIGINT信号,可以结束程序;可是,如果当“取消阻塞”和“pause”之间,正好来了SIGINT信号,结果程序因为pause的原因会一直挂起。为了纠正此问题,需要在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。这种功能是由sigsuspend函数提供的。

	#include <signal.h>
	int sigsuspend( const sigset_t *sigmask );
	返回值:-1,并将errno设置为EINTR

将进程的信号屏蔽字设置为由sigmask指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则sigsuspend返回,并且将该进程的信号屏蔽字设置为调用sigsuspend之前的值。此函数没有成功返回值,如果它返回到调用者,则总是返回-1,并将errno设置为EINTR(表示一个被中断的系统调用)。 

示例:

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

void handler(int sig)   //信号处理程序
{
   if(sig == SIGINT)
	  printf("SIGINT sig");
   else if(sig == SIGQUIT)
	  printf("SIGQUIT sig");
   else
	  printf("SIGUSR1 sig");
}

int main()
{
	sigset_t new,old,wait;   //三个信号集
	struct sigaction act;
	act.sa_handler = handler;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	sigaction(SIGINT, &act, 0);    //可以捕捉以下三个信号:SIGINT/SIGQUIT/SIGUSR1
	sigaction(SIGQUIT, &act, 0);
	sigaction(SIGUSR1, &act, 0);

	sigemptyset(&new);
	sigaddset(&new, SIGINT);  //SIGINT信号加入到new信号集中
	sigemptyset(&wait);
	sigaddset(&wait, SIGUSR1);  //SIGUSR1信号加入wait
	sigprocmask(SIG_BLOCK, &new, &old);       //将SIGINT阻塞,保存当前信号集到old中

	//临界区代码执行    

	//程序在此处挂起;用wait信号集替换new信号集。即:过来SIGUSR1信号,阻塞掉,程序继续挂起;过来其他信号,
	//例如SIGINT,则会唤醒程序,执行sigsuspend的原子操作。注意:如果“sigaddset(&wait, SIGUSR1);”这句没有,
	//则此处不会阻塞任何信号,即过来任何信号均会唤醒程序。
	if(sigsuspend(&wait) != -1)    
		printf("sigsuspend error");
	printf("After sigsuspend");
	sigprocmask(SIG_SETMASK, &old, NULL);
	return 0;
}

sigsuspend的原子操作是:  

(1)设置新的mask阻塞当前进程(上面是用wait替换new,即阻塞SIGUSR1信号); 

(2)收到SIGUSR1信号,阻塞,程序继续挂起;收到其他信号,恢复原先的mask(即包含SIGINT信号的); 

(3)调用该进程设置的信号处理函数;  

(4)待信号处理函数返回,sigsuspend返回。(sigsuspend将捕捉信号和信号处理函数集成到一起了)

8、kill函数

头文件:
	#include <sys/types.h> 
	#include <signal.h>
定义:
	int kill(pid_t pid, int sig);
参数: 
	pid可能选择有以下四种:
	1\. pid大于零时,pid是信号欲送往的进程的标识。
	2\. pid等于零时,信号将送往所有与调用kill()的那个进程属同一个进程组的进程,而且发送进程具有
        向这些进程发送信号的权限。注意:这里用的术语“所有进程”不包括实现定义的系统进程集。对于大多数
        UNIX系统,系统进程集包括内核进程和init(pid 1).
	3\. pid等于-1时,信号将送往所有调用进程有权给其发送信号的进程,除了进程1(init)。
	4\. pid小于-1时,信号将送往以-pid为组标识的进程。
	sig:准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用
        sig值为零来检验某个进程是否仍在执行。如果向一个并不存在的进程发送空信号,则kill返回-1,并将
        errno设置为ESRCH。但是对于进程是否存在的这种测试并非原子操作。在kill向调用者返回测试结果时,
        原来存在的被测试进程此时可能已经终止。反之亦然。
返回值:
	成功执行,返回0
	失败返回-1,errno被设为以下的某个值
	- EINVAL:指定的信号码无效(参数 sig 不合法)
	- EPERM;权限不够无法传送信号给指定进程 
	- ESRCH:参数 pid 所指定的进程或进程组不存在

注意:

使用kill函数发送信号给进程或者进程组,进程将信号发送给其他进程需要权限。超级用户可将信号发送给任一进程。对于非超级用户,其基本规则是发送者的实际或者有效用户ID必须等于接收者的实际或者有效用户ID。另外,“杀死”这个术语是不恰当的,kill只是将一个信号发送给一个进程或者进程组,进程是否终止取决于信号类型以及进程是否安排了捕捉该信号。

9、alarm函数

头文件:	#include <unistd.h>
函数原型:	unsigned int alarm(unsigned int seconds);
函数说明:	主要功能是设置信号传送闹钟,即用来设置信号SIGALRM(由内核产生)在经过参数seconds秒
                数后发送给目前的进程。如果未设置信号SIGALRM的处理函数,那么alarm()默认处理终止进程。
返回值:	        返回0或者以前设置的闹钟剩余秒数

注意:

每个进程只能有一个闹钟时钟。如果在seconds秒内再次调用了alarm函数设置了新的闹钟,则后面定时器的设置将覆盖前面的设置,且前面闹钟的剩余秒数将作为本次ALARM调用的返回值。当参数seconds为0时,之前设置的定时器闹钟将被取消,并将剩下的时间返回。如果想要捕捉信号SIGALRM,则最好在调用alarm函数之前设置该信号的处理程序。

10、pause函数

头文件:	#include <unistd.h>
函数原型:	int pause(void);
函数说明:	只有执行了一个信号处理程序并从其返回时,pause才返回,也即将调用进程挂起直至捕捉到
                信号为止。在这种情况下,pause返回-1,并将errno设置成EINTR。这个函数通常用于判断信号是否已到。
返回值:	        -1,并将errno设置成EINTR。

11、raise函数

头文件:	#include <signal.h>
函数原型:	int raise(int signo);
函数功能:	向进程本身发送一个信号,相当于 kill(getpid(), sig)
函数参数:	signo:要发送的信号值
返回值:	成功返回0,出错返回-1

12、abort函数

头文件:	#include <stdlib.h>
函数原型:	void abort(void);
功能:		向进程发送一个SIGABRT信号,默认情况下进程会退出。

注意: 即使SIGABRT信号被加入阻塞集,一旦进程调用了abort函数,进程也还是会被终止,且在终止前会刷新缓冲区,关闭文件描述符。