Linux下的信号

信号

什么是信号

在linux我们经常会碰到这样的场景,有一个进程一直在前台运行,比如前台运行了一个死循环的程序,这个时候我们要终止程序怎么办?

我们通常会选择按Ctrl + c终止进程,或者按Ctrl + \来终止,当然也可以按Ctrl + z使其变为后台进程,那么这些都是怎么实现呢?

这里就涉及到信号,我们可以认为当进程在前台运行时我们按下Ctrl + c相当于发送了一个信号,在linux下Ctrl + c被解释为SIGINT中断信号(INT是interrupt的缩写),而Ctrl + \就被解释为SIGQUIT,进程收到这个信号,就会做出相应的动作,比如这里的两个信号是退出,那么进程就会退出。

查看信号

使用kill -l可以查看信号,如下图,信号编号从1-64,中间32,33号是没有信号的,也就是说一共有62个信号,其中编号小于34的信号都是非实时信号(非可靠信号),大于等于34的都是实时信号(可靠信号)。

这些在man手册的第七章都有详细介绍以及信号的默认处理是什么,我们可以通过键入命令man 7 signal查看
信号

信号的注册(递达)和注销

在linux系统中,进程的PCB(进程控制块)里面有几个和信号相关的东西,一个叫sigpending,他是一个信号的位图,存储当前收到的信号,还有一个sigblock是一个阻塞信号集,同样是个位图,被放入集合中的信号就会被阻塞。除此之外,PCB中还有一个sigqueue,他是一个信号的队列,所有被注册过的信号都会放入这里排队等待处理。

信号的注册(递达)

信号的注册又称作信号的递达,指的是这个信号被进程收到后是怎么对待的。

  1. 系统收到一个信号之后,首先会去sigpending中查看位图是否已经有这个信号,如果没有,那么位图对应位会被置1,继续执行下面流程;如果位图中已经有这个信号,那么要看是可靠信号还是非可靠信号,可靠信号就继续执行下面流程,非可靠信号直接丢弃
  2. sigqueue中添加信号节点,可靠信号可能会有多个节点,非可靠信号至多有一个节点,添加完毕信号的注册就完成了。额外加一句,如果有多个实时信号加入的话,会优先添加编号小的节点

信号的注销

  1. 信号注销首先会删除所有sigqueue中的该信号的节点
  2. sigpending中的位图置0

信号的分类

信号根据信号的可靠程度可分为可靠和不可靠,实时和非实时只是他们的另一种叫法,其实是同一种东西,信号的可靠不可靠(实时和非实时)针对的是这个信号被接收到之后会被系统怎么对待,我们接下来详细聊聊

可靠信号和非可靠信号

信号的可靠与不可靠指的是信号注册时和注销时系统不同的对待方式。

可靠信号

可靠信号指的是当该信号被进程收到后,发现进程中这个信号已经被注册过了,但是仍然会被添加到队列中进行排队等待,他不会被丢弃,当前面的信号处理完毕后就会处理这个信号,一个队列中可以有多个可靠信号。
多个实时信号加入的话,会优先添加编号小的节点。

非可靠信号

非可靠信号指的是当该信号被进程收到后,发现进程中这个信号已经被注册过了,那就直接丢弃,忽略他。

总结

非实时信号都不支持排队,队列中最多只有一个非实时信号,都是不可靠信号;实时信号都支持排队,队列中可以有多个实时信号,都是可靠信号。

信号的屏蔽(阻塞)

单独把信号的阻塞拿出来是因为这不同于recv或者read等系统调用的阻塞。

在recv和read调用过程中,默认是阻塞的,阻塞直到缓冲区有数据,才会开始读取,读取完成才返回,这里的阻塞是为了完成某一件事而阻塞等待,不完成我就不走。

对比信号的阻塞,则不是同一个阻塞的意思,信号的屏蔽(阻塞)则是将信号暂时挂起,现在暂时不处理,等之后合适再处理,这里的屏蔽(阻塞)应理解为将信号暂时挂起阻塞,信号依旧存在,只是暂时不做处理,而不能理解为阻塞等待信号处理完成

小学生写作业的例子:
read和recv这种就相当于小学生被妈妈逼着写作业,不写完作业不能走,我只能一直写,写完了才能走,阻塞等待事件完成。
信号被阻塞则就像一个小学生爱玩玩,作业什么我才不管,小学生很贪玩,先玩再说,等到作业被妈妈发现了(相当于信号被解除阻塞了),这个时候只能乖乖回去写作业了,阻塞期间程序依旧可以执行,一旦取消阻塞信号就要被立即处理。

注意:SIGSTOPSIGKILL不能被屏蔽或忽略

如何屏蔽一个信号

		#include <signal.h>
		//sigmask将指定信号添加到一个信号集中,返回这个信号集
		int sigmask(int signum);
		//要得到一个有SIGINT和SIGQUIT的信号集
		//int mask = sigmask(SIGINT) | sigmask(SIGQUIT);

		//按照mask信号集来屏蔽对应信号,返回屏蔽之前的信号集
		int sigblock(int mask);
		//屏蔽SIGINT和SIGQUIT如下
		//sigblcok(sigmask(SIGINT) | sigmask(SIGQUIT));

		//将进程中的信号集设置为对应的信号集,返回设置之前的信号集
		int sigsetmask(int mask);

		//获取当前PCB中的信号集
		int siggetmask(void);
		//int mask = siggetmask();

		int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
		//how	SIG_BLOCK	向阻塞集合中添加set中的信号
		//		SIG_UNBLOCK	解除阻塞set集合中的信号
		//		SIG_SETMASK	设置当前阻塞集合为mask
		//set	这是一个需要处理的信号集合,处理方式按照how进行操作,如果set为NULL
		//		那么处理毫无意义
		//oldset	存放PCB原来的信号集合,如果为NULL,那么就不保存原信号集

阻塞一个SIGINT

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

int main()
{
	int mask = sigmask(SIGINT);
	sigprocmask(SIG_BLOCK, &mask, NULL);
	while (1);
	return 0;
}

信号是如何被处理的

用户态和内核态

首先要理解一个概念叫用户态和内核态。

在代码执行过程中经常会出现用户态和内核态之间的切换,我对用户态的理解就是我们平时写的代码大多都在用户态,比如for循环、创建变量等这些都是用户态下的。

当我们调用read、write、alarm、fork等系统调用函数(read、write涉及到缓冲区的读写,fork则需要内核去创建进程并复制父进程,这些都是我们无法做到的),或者是中断(收到信号)、异常等,则会从用户态切换到内核态,处理完成后再回到用户态。

所以我们代码其实大部分跑在用户态下,但是一旦涉及到系统调用、中断、异常就会进入内核态,由内核执行内核代码。

以上是我对用户态和内核态的简单理解,具体可以看这篇jakielong写的关于内核态用户态的博客

信号处理过程

  1. 一般程序正常顺着主控制流程往下跑,一旦碰到系统调用、中断、异常时就会进入内核态
  2. 在系统中信号一般在内核态返回用户态之前处理,由do_signal函数来决定是直接返回主控制流程在系统中信号一般在内核态返回用户态之前处理,由do_signal函数来决定是直接返回主控制流程,这个步骤可分为以下三种情况
    1. 默认信号处理方式
      信号没有被设置为忽略或者阻塞处理,则选择默认处理方式,如果是SIGQUIT或者SIGINT则会直接终止进程,如果是其他不会终止信号的进程,则会返回主控制流程继续执行信号没有被设置为忽略或者阻塞处理,则选择默认处理方式,如果是SIGQUIT或者SIGINT则会直接终止进程,如果是其他不会终止进程的信号,则会返回主控制流程继续执行
    2. 忽略
      忽略直接将信号丢弃,不做任何处理,当内核代码执行完毕后就会直接返回用户态
    3. 用户自定义处理方式
      内核会返回用户态调用自定义信号处理函数,处理完毕返回内核态调用sigreturn函数,之后才会返回主控制流程
  3. 执行完毕上面的过程,又从内核态返回用户态,继续愉快的执行代码
    信号处理

信号处理实操

signal函数

首先是signal函数,这个函数优点是使用非常方便,但是也有不足之处,当一个信号执行的时候可能会受其他信号的影响。

//函数指针
typedef void (*sighandler_t)(int);

//signal函数,可以指定信号的处理方式
sighandler_t signal(int signum, sighandler_t handler);
//signum	指定信号
//handler	指定信号的处理方式,可以有三种参数
//			SIG_DFL	默认处理方式
//			SIG_IGN	忽略信号
//			函数指针	自定义处理,当遇到对应信号会调用此函数,我们称之为回调函数
//返回值:	成功 信号处理程序之前的值 失败 SIG_ERR,并设置errno为错误码

sigaction函数

sigaction的参数比signal多,而且用起来也略比signal复杂一点,但是他的优点是当有信号需要处理时,可以设置一个阻塞集合,当执行信号的时候,可以保证不被其他信号影响,暂时阻塞他,比如自定义SIGINT信号处理方式,但是不希望受SIGQUIT影响,可以将SIGQUIT设置到集合里面阻塞。

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
//signum	需要处理的信号
//act		act是一个结构体,存放了需要阻塞的集合,信号的处理方式等
//oldact	可以存储之前信号的处理方式
//返回值:	成功 0
//			失败 -1,且将errno设置成错误码

struct sigaction {
	//自定义处理方式
	void		(*sa_handler)(int);
	//带参数的自定义处理方式
	void		(*sa_sigaction)(int, siginfo_t *, void *);
	//需要阻塞的集合
	sigset_t	sa_mask;
	//选项标志,选择用哪个处理方式
	//0 -> sa_handler
	//SA_SIGINFO -> sa_sigaction
	int			sa_flags;
	//预留参数
	void		(*sa_restorer)(void);
};

代码实现

使用signal和sigaction实现忽略、自定义的信号处理

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

void my_SIGINT(int signo)
{
	printf("receive signo -> %d\n", signo);
	sleep(10);
}

int main()
{
	//1. 忽略处理SIGINT
	//signal(SIGINT, SIG_IGN);

	//2. 自定函数处理
	//当遇见SIGINT时调用my_SIGINT去处理
	//signal(SIGINT, my_SIGINT);

	//3. sigaction自定处理
	//这里的作用就是当处理SIGINT的时候,SIGQUIT会被阻塞
	//直到SIGINT处理完毕,才会处理SIGQUIT退出
	struct sigaction n_act, o_act;
	//不希望其他信号受影响,所以先将其设置为空
	sigemptyset(&n_act.sa_mask);
	//设置SIGQIUT放到阻塞集合中
	sigaddset(&n_act.sa_mask, SIGQUIT);
	n_act.sa_handler = my_SIGINT;
	n_act.sa_flags = 0;
	sigaction(SIGINT, &n_act, &o_act);
	
	while (1)
	{
		printf("hello~~~~~~~~\n");
		sleep(1);
	}
	return 0;
}

执行效果
可以看到键入SIGINT时SIGQUIT没有反应
执行效果

实用:父进程等待子进程

我们以前对于父进程回收子进程一般都是waitpid阻塞等待,现在我们有了全新的方式来处理,子进程退出,会发送SIGCHILD信号,只要我们收到这个信号调用自己的信号处理函数就行了

void my_SIGCHLD(int signo)
{
	pid_t pid;
	//不断调用waitpid非阻塞来等待
	while (id = waitpid(-1, NULL, WNOHANG) != 0)
	{}
	printf("child process quit\n");
}

int main()
{
	signal(SIGCHLD, my_SIGCHLD);
	//fork子进程就可以处理了
	//.....
	//不必阻塞等待,想干啥就干啥
	return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值