【Linux】简单聊信号

大家对信号应该不陌生吧。
在生活中:比如红绿灯,这个信号让我们知道什么时候过马路更安全。你妈叫你回家吃饭,这个信号让你知道你接下来该回家了。又或者你网购了衣服,即使快递还没有到,但你知道快递来了以后你该怎么处理。
在计算机中,我们经常通过 Ctrl+C 结束当前进程的过程,其实是键盘产生了一个硬件中断,被OS获取解释成信号,再发送给目标前台,前台进程收到了信号进而引起进程终止并退出。
所以信号可以让接收者做出相应的反应。
并且,接收方在收到信号之前就知道应该怎样处理合法的信号。

初步认识

操作系统中一共有62 个信号,其中有31个普通信号和31个实时信号。我们这里只了解31个普通信号。
信号是由操作系统发出,让进程接收并作出相应的反应。进程中存储信号的地方是PCB,并用int 的位图存储收到的每个信号。

注意

  1. Ctrl+C 产生的信号只能发送给前台进程。
    在一个命令后加&可以将当前进程放到后台运行,这样shell无需等当前进程结束就可以启动新的进程。
  2. 系统发出的信号相对进程的控制流程是异步的。
    因为无论当前用户空间的代码执行到哪里,都可能因为收到OS的信号而终止退出,所以二者是异步的(不可预料的)。
信号的概念
  • 信号是进程间时间异步通知的一种方式,属于软中断
    我们可以通过kill -l 查看系统定义的信号列表
    在这里插入图片描述
    注意: 9号信号是kill 命令,只有它无法被自定义修改处理方式,且永不会失效。

与信号相关的概念:

  • OS 发送信号 的过程:OS先找到进程的PCB,再找到位图中于当前信号相对应的位置,将当前位置的0置成1,表示进程收到信号。其实我们更喜欢称此过程为 “写信号”,看完这篇博客你就会有更深的理解。
  • 安装中断:设置信号来时不执行默认操作,而是执行自己的代码,即期望某个信号来到时让进程执行相应的中断服务程序
  • 忽略信号:进程已经被递送给目标进程,但是目标进程不处理,直接丢弃。
  • 捕获(递达)信号: 实际执行信号的处理动作被称为捕获
  • 屏蔽(阻塞)信号: 进程暂时不接受某些信号。如果在屏蔽期间向进程发送了被屏蔽的信号,该信号不会被进程捕获;一段时间后如果进程解除该信号的屏蔽,该信号将被捕获到
  • 未决信号: 信号已经产生,但因为目标进程暂时屏蔽该信号而不能被目标进程捕获信号
信号的一生

要了解信号的产生和处理,销毁等过程,我们就要先知道信号的一生,下面这张图可以很好的帮助我们理解:
下图中需要注意的是:一个进程向另一个进程发送信号时,其实并非直接发送到目标进程,而是通过操作系统转发完成的 我们不过多的研究
在这里插入图片描述

1. 产生信号的方式
  1. 通过终端按键产生信号:键盘 — 只能发送给前台进程
    SIGINT 的默认动作是终止进程,SIGQUIT 的默认动作是终止进程并且Core Dump。

Core Dump:核心转储。意味Core Dump的出现会带来core文件,可以方便进程异常终止后进行调试的时候查看错误原因,也叫事后调试。但系统默认是不允许产生core文件的,因为文件中可能包含着用户的敏感信息,不过我们需要core文件时,可以在开发调试阶段用ulimite 进行修改。
具体步骤:ulimite -c 1024(文件大小)
ulimite -a(查看当前所有core文件)
(gdb) core -file core.2884(调试文件名为core.2884的文件,会直接打开)

  1. 调用系统函数向进程发信号
    int kill(pid_t pid,int signo);
    kill 命令是调用kill 函数实现的,可以给一个指定的进程发送指定的信号。
    ·int raise(int signo);
    raise 函数可以给当前进程发送指定的信号(即自己给自己发信号)。
    void abort(void);
    类似exit()函数,让当前进程异常终止,所以它总是会成功的。
  2. 由软件条件产生:SIGPIPE
    SIGPIPE 是一种由软件条件产生的信号,主要用于管理进程。在这里我们主要介绍alarm 函数和SIGALRM信号。
    unsigned int alarm(unsigned int second);
    调用alarm 函数,我们可以设定一个闹钟,也就是告诉内核在second 秒后给当前进程发SIGALRM信号,此信号默认动作是终止进程。
    注意: alarm 在时间未到前也可以被唤醒,此时返回函数剩余时间,又可重新再设定。
  3. 异常硬件产生信号
    硬件异常被硬件以某种方式检测到并通知内核,然后内核再向该进程发送信号。

例如 SIGFPE 表示CPU的运算单元产生异常;SIGSEGV 表示进程地址上的MMU出现非法访问等异常。

5.-kill 命令
永不失效,且无法被自定义的handler方法修改

2. 信号捕捉:

捕捉函数:sighandler_t signal(int ssignum,sighandler_t haandler);
模拟实现野指针异常的捕捉处理:
我们如果定义一个野指针,则会发生以下错误:
在这里插入图片描述
我们可以通过signal(signo,handler);捕捉此信号,如下

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

void handler(int sig)
{
   sleep(2);
	printf("catch a sig:%d\n",sig);
}
int main()
{
	signal(SIGSEGV,handler);
	sleep(1);
	int *p=NULL;
	*p=100;
	while(1);
	return 0;
}

在这里插入图片描述
此时我们便捕捉到这个内存访问越界的信号编号为11。
这里需要解释,为什么进入while后仍然不停的打印结果:因为信号已被注册,我们只修改了它的处理方式,并没有处理它,所以信号一直存在。

3. 处理信号的常见方式:
  • 忽略此信号
  • 执行该信号的默认动作:一般为终止进程
  • 自定义处理信号函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方法称为捕捉(自定义)一个信号。下面讲
  • 进程收到信号不利己处理,在合适的时机再去处理
阻塞信号

1. 信号在内核中的表示示意图:
在这里插入图片描述

OS对信号管理的过程:查看pending表 – block表 – handler – 修改pending表

解释在上图中:
SIGHUP 信号未产生,且在产生时不会阻塞,当它递达时执行默认的动作。
SIGINT 已产生并且阻塞,当它递达时执行忽略动作。但在阻塞解除时可修改处理动作。
SIGQUIT 未产生,但在产生时会被阻塞,它的处理动作由用户自定义。

这里还有几个重要的概念:

  • block:表明某个信号是否阻塞

内容:是否阻塞 1–是 0–否
下标:对应信号的编号

  • pending:表明是否未决信号

内容:是否收到信号 1-是 0-否
下标:对应信号的编号

  • handler:指明信号的处理动作 是一个函数的指针数组
  • 在信号递达之前时,多次产生的普通信号都只按一次处理,但实时信号会依次放入一个消息队列中。

2. sigset_t 信号集
从上图可以看出,每个信号的阻塞和未决标志都可以用一个bit位来表示,所以我们可以用相同的数据类型 sigset_t 来存储,将sigset_t 称为信号集,它可以表示每个信号的“有效”或者“无效”状态。

  • 信号集的操作函数:
#include <signal.h>
//初始化信号集内bit位全为0
int sigemptyset (sigset_t *set);

//初始化信号集内bit位全为0
int sigfillset(sigset_t *set);

//向信号集内添加信号
int sigaddset(sigset_t *set,int signo);

//删除信号集内信号
int sigdelset(sigset_t *set,int signo);

//判断当前信号是否在信号集内
bool sigismember(const sigset_t *set,int signo);

注意: 在使用sigset_t 类型的变量前,一定要调用函数进行初始化,使信号集内的信号处于确定状态,然后再进行添加和删除操作。这些函数成功返回0,出错返回-1。

3. 信号屏蔽字(阻塞信号集)sigprocmask
int sigprocmask (int how,const sigset_t *set,sigset_t *oset);
函数中,如果oset 是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出;如果set 是非空指针,则更改进程的信号屏蔽字通过how 指示如何更改;如果oset 和set 都是非空指针,则将原信号屏蔽字备份到oset 中,再根据set和how 参数更改信号屏蔽字。
假设当前信号屏蔽字为mask,how 参数的可选值如下:

添加阻塞信号:SIG_BLOCK 相当于mask=mask | set
解除阻塞信号:SIG_UNBLOCK 相当于 mask=mask & ~set
重设set指的值:SIG_SETMASK 相当于 mask=set

4. 未决信号集 sigpending

sigpending
读取当前进程的未决信号集,通过set参数传出。
看下面的程序,应该会更清除一些:
在这里插入图片描述
运行结果:
在这里插入图片描述
由运行结果可知,Ctrl+C 已失效,因为我们将该信号加入阻塞队列,所以一直处于未决状态,不被处理。但是Ctrl+Z仍然可以停止程序,因为该信号并未被阻塞。

信号的捕捉及处理

处理信号的时机:在内核返回用户时。
当信号的自定义处理动作在用户区时,在信号递达时就调用该函数,称为捕捉信号

  • 信号处理过程
    在这里插入图片描述
    我们都知道操作系统是计算机中权限最高的管理者,操作系统不相信任何人,所以处理信号这种行为就是执行内核区的代码。有的同学会问什么是执行内核区的代码?操作系统给我提供的系统调用api就是执行内核区代码,而我们执行自己写的代码就是执行用户区的代码。之所以需要在内核区中捕获处理信号是因为,操作系统在捕获信号时需要检测未决信号集合,屏蔽信号集等,并需要在处理信号后将他们由1置为0,这是用户无法做到的。这也就说明main函数的执行流与信号的执行流是两个独立的执行流,我们将上面的理论抽象出以下图形就发现很好记忆:
    在这里插入图片描述
  • sigaction
    在这里插入图片描述
    sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功返回0 ,失败返回-1。signo 表示指定的信号编号。
    此函数的第一个参数为要安装的信号,第二三个参数都为信号结构体sigaction(用于描述要采取的操作及相关信息)变量。第二个参数用来指定欲设置的信号处理方式,第三个参数将存储执行此函数前针对此信号的安装信息,说白了就是一个备份。
    接下来我们了解一下struct sigaction :
struct sigaction {

	void (*sa_handler)(int);//类似于调用signal函数
	
	void (*sa_sigaction)(int, siginfo_t *, void *);//信号捕获函数,可以获取其他信息
	
	sigset_t sa_mask;//执行信号捕获函数期间要屏蔽的其他信号集合
	
	int sa_flags;//影响信号行为的特殊标志
	
	void (*sa_restorer)(void);//没有使用,作为了解

}

结构体中sa_mask是一个信号集合(位图),用于标识在执行信号捕获函数时,添加到进程屏蔽信号集合中的信号集。但是不会屏蔽9,18,19号信号。
上面的sa_handler和sa_sigaction函数指针只需要二取一即可,如果使用前者那么行为和signal函数相同,而后者可以获取更多的信息,这看起来像时signal的升级版。

SIGCHID – 实现一个不产生僵尸进程的函数
  1. 进程的管理一章中,我讲到过用wait和waitpid函数可以清理僵尸进程,父进程可以阻塞式等待子进程结束,也可以非阻塞式的轮询是否有子进程结束需要清理。使用第一种方式父进程无法处理自己的工作,使用第二种方式父进程需要一直轮询,程序实现复杂,效率还较低。
  2. 其实在子进程终止时总是会给父进程法SIGCHID信号,该信号的默认处理动作是忽略。所以如果我们自定义父进程处理SIGCHID信号的动作,让它在收到信号时再wait()清理子进程即可。
  3. 事实上,要想不产生僵尸进程还有另一种方法:父进程调用sigaction将SIGCHID的处理动作定义成SIG_IGN,这样子进程在终止时会自动被清理,不会产生僵尸进程也不会通知子进程。
    注意: 通常系统默认SIG_IGN忽略动作与用户自定义的忽略无差别,但这是个特例。

测试代码如下:

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

void handler(int sig)
{
	pid_t id;
	while(id=waitpid(-1,NULL,WNOHANG)>0)
	{
		printf("wait child success:%d\n",id);
	}
	printf("child quit\n");
}
int main()
{
	signal(SIGCHLD,handler);
	pid_t cid;
	if(cid=fork()==0) //chid
	{
		printf("child :%d\n",getpid());
		sleep(3);
		exit(1);
	}
	while(1)
	{
		printf("father doing other\n");
		sleep(1);
	}
	return 0;
}

在这里插入图片描述
由运行结果可以看出,子进程在退出前,父进程在做其他事情,当子进程退出时,handler函数捕捉到到退出信号并处理它,父进程仍在做其他事情。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值