Linux信号编程、signal函数范例详解( 4 ) -【Linux通信架构系列 】

系列文章目录

C++技能系列
Linux通信架构系列
C++高性能优化编程系列
深入理解软件架构设计系列
高级C++并发线程编程

期待你的关注哦!!!
在这里插入图片描述

现在的一切都是为将来的梦想编织翅膀,让梦想在现实中展翅高飞。
Now everything is for the future of dream weaving wings, let the dream fly in reality.

一、signal 函数初识

收到一个信号之后,可以使用signal函数来忽略或者捕捉,看如下范例:

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

void sig_usr(int signo)
{
    
    
	if(signo == SIGUSR1)
	{
    
    
		printf("收到了SIGUSR1信号!\n");
	}else if(signo == SIGUSR2){
    
    
		printf("收到了SIGUSR2信号!\n");
	}else{
    
    
		printf("收到了未捕捉的信号%d!\n", signo);
	}
}
int main(int argc, char *const *argv)
{
    
    
	if(signal(SIGUSR1, sig_usr) == SIG_ERR)
	{
    
    
		printf("无法捕捉SIGUSR1信号!\n");
	}
	//系统函数。参数1是个信号,参数2是个函数指针,代表一个针对该信号的捕捉处理函数
	if(signal(SIGUSR2, sig_usr) == SIG_ERR)
	{
    
    
		printf("无法捕捉SIGUSR2信号!\n");
	}
	for(;;)
	{
    
    
		sleep(1); //休息1s
		printf("休息1s\n");
	}
	return 0;
}

通过两次调用signal函数,分别注册信号SIGUSR1和信号SIGUSR2对应的信号处理函数(sig_usr),当收到这两个信号时,sig_usr就会被调用。在sig_usr信号处理函数过程中,只是做了一些信息输出的工作。

编译运行,然后用kill命令发送两个信号:

kill -usr1 4155
kill -usr2 4155

查看进程:
在这里插入图片描述

图1.1 向nginx进程分别发送了USR1和USR2的信号,输出窗口出现一些提示

可以看出,nginx进程收到两个信号,并且还能继续运行不受影响。还可以看到kill命令的另外一种形式:直接用信号名的方式向进程发送信号。

通过这个范例,应该认识到两个问题:

(1)signal函数捕捉了系统的SIGUSR1和SIGUSR2信号,并用自己的函数来处理,获得了成功。

如果程序中不捕捉SIGUSR1或者SIGUSR2信号,用kill向改进程发送SIGUSR1或者SIGUSR2信号,进程会有什么表现呢?

答案:当然是终止进程,因为这两个信号的系统默认动作是终止进程。(可以自行测试下)

(2)信号可能是某个进程发出的,也可能是内核发出的,但不管是怎么发出的,总之目标进程(nginx)收到了这个信号。目标进程收到信号这件事,会被内核注意到,这时内核就有动作了。内核动作是什么呢?
如图:

在这里插入图片描述

图1.1 突然到来的信号导致进程从用户态切换到内核态,处理完毕再切换回用户态

二、引申出的思考问题 - 可重入函数概念

我们看下如下代码,会出现什么问题?

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


int g_mysign = 0;
void muNEfunc(int value)
{
    
    
	//...其他处理
	g_mysign = value;//函数muNEfunc能够修改全局变量g_mysign的值
	//...其他处理
}

void sig_usr(int signo)
{
    
    
	muNEfunc(22);
	if(signo == SIGUSR1)
	{
    
    
		printf("收到了SIGUSR1信号!\n");
	}else if(signo == SIGUSR2){
    
    
		printf("收到了SIGUSR2信号!\n");
	}else{
    
    
		printf("收到了未捕捉的信号%d!\n", signo);
	}
}

int main(int argc, char *const *argv)
{
    
    
	if(signal(SIGUSR1, sig_usr) == SIG_ERR)
	{
    
    
		printf("无法捕捉SIGUSR1信号!\n");
	}
	//系统函数。参数1是个信号,参数2是个函数指针,代表一个针对该信号的捕捉处理函数
	if(signal(SIGUSR2, sig_usr) == SIG_ERR)
	{
    
    
		printf("无法捕捉SIGUSR2信号!\n");
	}
	for(;;)
	{
    
    
		sleep(1); //休息1s
		printf("休息1s\n");

		muNEfunc(15);
		printf("g_mysign = %d\n", g_mysign);
	}
	return 0;
}

请思考一下,这样写代码会出现什么问题?(当然这种问题是在极端的情况下出现,平时不出现,不太容易看出)

本来期望每次输出的g_mysign的值是15,但偏偏收到一个信号,信号处理程序中改变了g_mysign的值,导致printf输出的g_mysign变成了22。这个结果是不是出乎意料呢!

所以,引出一个概念,叫做“可重入函数”。

可重入函数又称可重入的函数异步信号安全的函数,指在信号处理函数中调用是安全的函数。(显然muNEfunc是不安全的)

⚠️有些周知的函数是不可重入的(在信号处理函数中不要调用的)如malloc分配内存的函数、printf屏幕输出函数等。(实际商业代码中避免在信号处理函数中调用printf函数)。

根据分析,得到一些结论和处理方法:

(1)在信号处理函数中,应尽量使用简单的语句做简单的事情,尽量不要调用系统函数,以免引起麻烦。
(2)如果必须在信号处理函数中调用系统函数,只调用可重入函数,不要调用不可重入的系统函数。
(3)如果必须在信号处理函数中调用那可能修改errno的值的可重入系统函数,应考虑事先备份errno的值,事后再从信号处理函数返回之前恢复errno的值。(errno的值的系统函数被认为是可重入的系统函数

#include <errno.h> //用到errno则需要包含此头文件
void sig_usr(int signo)
{
    
    
	int myerrno = errno; //备份errno值
	//......进行一系列处理,如调用可重入函数
	//......
	errno = myerrno; //还原errno值
}

三、信号集(信号屏蔽字)

思考一个问题:收到一个SIGUSR1信号,开始执行信号处理函数sig_usr,尚未执行完成时,突然又收到一个SIGUSR1信号,系统会不会再次触发sig_usr函数开始执行呢? 一般不会,也就是说,当收到某个信号,启动执行信号处理函数的时候,通常会”屏蔽/阻塞“其后相同的信号,直到信号处理函数执行结束(系统自动处理)。

一个进程必须记住当前阻塞了哪些信号。如收到信号SIGUSR1时,系统将标记正在处理的该信号的标志设置为1,然后去执行信号处理函数,如果信号处理函数未执行完成时再次收到该信号,系统检测到该信号的标志已经为1,后来的SIGUSR1信号就需要排队等候(等待调用信号处理函数来处理)或直接被忽略(丢失)。当信号处理函数执行完毕,再把信号SIGUSR1信号对应的标志设置回0,此时如果有排队的SIGUSR信号或者新收到的SIGUSR1信号,就可以继续调用信号处理函数处理了。

这时候引入了信号集的概念,一种叫做信号集的数据类型,这种数据类型能把60个信号的状态(0或者1)都保存下来。用0表示没收到某个信号,用1表示收到某个信号并正在处理中。

	1、例如如果约定第五个位置表示信号SIGUSR1,程序开始执行后,收到一个SIGUSR1信号,就立即把第5个位置标记1:
		 0000100000,0000000000,0000000000,.....
	2、然后,等待调用信号处理函数处理这个到来的信号;
	3、此时,如果再收到一个SIGUSR1信号,因为第5个位置已经被标记为1,后面的这个SIGUSR1信号就会排队等候或者忽略;
	4、调用完处理函数后,把信号集的第5个位置标记回0:
	     0000000000,0000000000,0000000000,......
	5、此时,如果有排队等候或者新收到的SIGUSR1信号,就会又可以继续调用信号处理函数来处理了。

信号集这种数据类型用 sigset_t 来表示。
sigset_t 结构大概这样:

typedef struct{
    
    
	unsigned long sig[2]; //long是4字节32位,两个就是64位,代表64个信号
}sigset_t;

四、信号相关函数

有了这些信号集类型就可以介绍 sigempty、sigfillset、sigaddset、sigdest、sigprocmask、sigismember 等几个函数了。

(1)sigemptyset。把信号集中所有的信号清零,表示这60多个信号都没来。

    0000000000,0000000000,000000000,......

(2)sigfillset。把信号集中的所有信号都设置为1,与sigemptyset功能正好相反。会导致到来任何信号都会排队等候或者被忽略。

	111111111,1111111111,111111111, ......

(3)信号集中支持60多个信号,可以向信号集中增加(信号标志设置为1)或删除(信号标志设置为0)特定的信号,用sigaddset和sigdelset就可以做到。

	sigaddset用于将某个信号设置为1,sigdelset用于将某个信号设置为0。

(4)sigprocmask、sigismember。

	sigpromask函数用于设置进程所对应的信号集(进程有默认的信号集,但可以用sigpromask函数设置其他信号集)。
	sigismember函数用于检测信号集的特定信号是否被置位。

五、sigprocmask等信号函数的范例演示

范例如下:

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

//信号处理函数
void sig_quit(int signo)
{
    
    
	printf("收到了SIGQUIT信号!\n");
}
int main(int argc, char *const *argv)
{
    
    
	//定义新的信号集和原有的信号集
	sigset_t newmask, oldmask, pendmask; 
	//注册信号对应的处理函数
	if(signal(SIGQUIT, sig_quit) == SIG_ERR)
	{
    
    
		printf("无法捕捉SIGUSR1信号!\n");
		//退出程序,参数是错误代码,0表示正常退出,非0表示错误,但具体什么错误,没有特别的规定
		exit(1);
	}
	//newmask信号集中所有的信号都清零(表示这些信号都没有来)
	sigemptyset(&newmask);
	//设置newmask信号集中的SIGQUIT信号位为1,再来SIGQUIT信号时进程就收不到
	sigaddset(&newmask, SIGQUIT);
	//设置该进程所对应的信号集
	//第1个参数用了SIG_BLOCK,表明设置进程新的信号屏蔽字为当前信号屏蔽字和第2个参数指向的信号集的并集。
	//一个进程的当前信号屏蔽字,开始全部为0,相当于把当前信号屏蔽字设置成newmask(屏蔽了SIGQUIT)。
	//第三个参数不为空,则进程老的(调用本sigprocmask()之前的)信号集会保存到第3个参数里,以备后续恢复用
	if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0){
    
    
		printf("sigprocmask(SIG_BLOCK)失败!\n");
		exit(1);
	}
	printf("我要开始休息10s了-------begin----,此时我无法接受SIGQUIT信号!\n");
	sleep(10);
	printf("我已经休息10s了-------end----!\n");
	//测试一个指定的信号位是否被置位,测试的是newmask
	if(sigismember(&newmask, SIGQUIT))
	{
    
    
		printf("SIGQUIT信号被屏蔽了!\n");
	}else{
    
    
		printf("SIGQUIT信号没有被屏蔽了!\n");
	}
	//测试一个指定的信号位是否被置位,测试的是newmask
	if(sigimember(&newmask,SIGHUP))
	{
    
    
		printf("SIGQUIT信号被屏蔽了!\n");
	}else{
    
    
		printf("SIGQUIT信号没有被屏蔽了!\n");
	}
	//现在取消SIGQUIT信号的屏蔽(阻塞)-- 把信号集还原回去
	//第一个参数用了SIG_SETMASK表明设置进程新的信号屏蔽字为第2个参数指向的信号集,第3个参数没用
	if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
	{
    
    
		printf("sigprocmask(SIG_SETMASK)失败!\n");
	}else{
    
    
		printf("sigprocmask(SIG_SETMASK)成功!\n");
	}
	//测试一个指定的信号位是否被置位,这里测试的是oldmask
	if(sigismember(&oldmask, SIGQUIT))
	{
    
    
		printf("SIGQUIT信号被屏蔽了!\n");
	}else{
    
    
		printf("SIGQUIT信号没有被屏蔽,您可以发送SIGQUIT信号了,我要睡10s!!!!!!!\n");
		int mysl = sleep(10);
		if(mysl > 0)
		{
    
    
			printf("sleep还没睡够,剩余%d\n", mysl);
		}
	}
	printf("再见了!\n");
	return 0;
}

运行结果如下:
在这里插入图片描述

图5.1 案例运行结果分析

六、小结

还有一个sigaction函数,用来取代signal函数。商业代码中只用sigaction。可以了解下。

猜你喜欢

转载自blog.csdn.net/weixin_30197685/article/details/131343078