Linux-浅识信号机制

xiaoxiao2021-02-28  61

信号的基本概念

首先需要区分信号机制与通信机制不是一回事,通信机制是为了传输数据,信号机制是一种通知机制,通知事件的发生。信号量是进程间通信的一种方式,与信号没有关系,信号是一种通知机制。 我们可以先抽象的描述一下信号,就像我们过马路时,遇到的红绿灯一样,我们记录下该信号,由于我们知道如何处理不同颜色的信号,所以我可以做出所对应正确的行为。信号也是如此,进程由于认识并记录下了该信号,然后再处理该信号。但是进程是在一个合适的时候处理信号。这个”合适时候“到底是什么时间后面会详细解释。 认识一下所有信号:

kill -l

其中,1-31是普通信号,34-64是实时信号。

信号的常见产生方式

1.通过键盘按键产生 用户在终端下按下某些键时,会触发信号,终端驱动程序会发送信号给前台进程,例如:ctrl+c:产生SIGINT信号,ctrl+\:产生SIGQUIT信号,ctrl+z:产生SIGSTP信号。 2.硬件异常 硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向该进程发送适当的信号。 两种常见的硬件异常触发信号的情形: 当前进程执行了除0的指令,CPU的运算单元会产生异常,内核将这个异常解为SIGFPE信号发送给进程。 当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。 3.系统调用 我们都使用过kill命令实现对一个进程发送信号,实际上kill命令也是调用了kill函数。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发送信号) 举例: 写一个test函数里实现一个while(1)死循环。 然后对该进程发送一个11号信号。 以往遇到的段错误都是由于非法内存访问产生的,而这个程序本身没有问题,而是给它发送了11号SIGSEVG信号也能产生段错误。

abort函数使当前进程接收到信号而异常终止。(该函数总是会成功,所以没有返回值) 用如下代码:想要打印十次,然后abort函数导致进程收到该信号而异常终止。

int i=0; while(1) { i++; printf("%d!\n",i); sleep(1); if(i==10) { abort(); } }

4.软件条件 SIGPIPE是一种由软件产生的信号,在管道中我们已经接触过了,写端不止不写入数据,并且将写端关闭了,读端一直在读取数据,这种情况对于内核来说时不能忍的,所以会触发SIGPIPE信号,使进程异常终止。 SIGALRM信号使用来向进程发送一个闹钟信号的。设定闹钟需要使用alarm函数,也就是告诉操作系统在设定的秒数之后给当前进程发送SIGALRM信号,该信号的默认处理动作是终止当前进程。 该函数的返回值是0或者是以前设定的闹钟事件还余下的秒数。如果将seconds设为0,表示取消闹钟。

int i=0; alarm(3); while(1) { i++; printf("%d\n",i); sleep(1); }

设定闹钟在三秒钟之后闹钟响,操作系统向该进程发送SIGALRM信号,终止该进程。

综上所述:信号常见产生方式分四种,但实际上都是由操作系统给进程发送信号。

阻塞信号

与信号相关的几个概念:

实际执行信号的处理动作称为:信号递达。 信号从产生到递达之间的状态称为:信号未决(pending)。 进程可以阻塞某个信号,被阻塞的信号将永远保持在未决状态,除非进程对该信号接触阻塞。

信号在内核中的示意图: 其实由之前的学习,我们可以联想到,信号的发送一定与进程的pcb有关,我们常说发送信号,什么才叫做发送信号呢? 发送信号,实际上是写信号,操作系统修改了目标进程的task_struct中的上面三张表中的比特位,只需要将表中的对应比特位由0修改为1 比如:上图中的一号信号未阻塞也未产生过 二号信号产生过,但正在被阻塞,所以暂时不能递达。 三号信号未产生过,一旦产生就会被阻塞。 但在Linux下:普通信号在递达之前产生多次只记一次,而实时信号在抵达之前产生多次可以依次放入一个队列中。

举例:用代码演示事实确实如上所言。 编写一个代码,获取进程的pending表,并且打印该位图的每一位,当它接收到2号信号时,该对应比特位由0变1. 介绍所需要的函数接口:

sigset_t,是一个数据类型,称为信号集。不允许直接对信号集类型直接操作,必须使用信号集操作函数。 信号集操作函数: int sigemptyset(sigset_t *set);//初始化信号集,表示该信号集不含任何有效位 int sigfillset(sigset_t *set);//初始化信号集,表示该信号集的所有有效信号包括系统支持的所有信号 int sigaddset(sigset_t *set, int signum);//添加某个有效信号 int sigdelset(sigset_t *set, int signum);//删除某个有效信号 int sigismember(const sigset_t *set, int signum);//判断一个信号集的有效信号中是否含有某种信号。

sigprocmask函数:用来读取或更改进程的信号屏蔽字。 int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); sigpending函数:用来获取pending表。 int sigpending(sigset_t *set);

void printsigset(sigset_t *set) { int i=1; for(;i<32;i++) { if(sigismember(set,i))//判断该信号是否在该信号量集中 { printf("1"); } else { printf("0"); } } puts("\n"); } int main() { sigset_t s,p;//定义信号量集对象,并清空初始化 sigemptyset(&s); sigaddset(&s,SIGINT);//添加2号信号至信号量集 sigprocmask(SIG_BLOCK,&s,NULL);//阻塞该信号,否则一旦递达无法看到现象 while(1) { sigpending(&p);//获取pending表 printsigset(&p); sleep(1); } return 0; }

首先前几秒获取pending表一直全0,因为未获得任何信号,触发一个2号信号后,可以看到第二个比特位被修改为了1,说明确实修改了pending表中的第二个比特位。

递达信号的三种方式

递达信号的三种处理方式:

忽略此信号:SIG_IGN默认此信号:SIG_DEF,大多数进程的默认方法都是终止该进程捕捉此信号:用户自定义handler方法

捕捉信号

如何实现信号的捕捉: 如果信号的处理动作是用户自定义函数,再信号递达时就需要调用此函数,这称为捕捉信号。但在内核时如何实现呢?我们知道在操作系统中分为用户态和内核态。在用户态下执行控制流程时遇到中断异常或系统调用进入内核。但是用户自定义的处理信号函数是在用户空间的,所以这一过程可用一张图来解释: 由于中断异常或系统调用导致切换为内核态执行,在内核态处理完毕需要切换回用户态之前,需要处理当前该进程中可以递达的信号,这个时候就是前文中我们所说的“适合的时候”。 处理信号的方式有三种,若是默认,则执行默认动作,大多数进程的默认动作是终止进程,则不会返回,但在返回前会修改pending表中的对于比特位。 若是忽略,则直接返回,并且在返回前修改pending表中的比特位。 若是自定义,则回到用户态去处理用户自定义函数,执行结束后由于特殊的系统调用又会返回内核,再从内核中返回主控制流程中,从中断处继续执行。

相关函数接口

sigaction:用于捕捉信号 int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact); 参数: signo:指定信号的编号。 若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact恢复原来的处理动作。 将sa_handler赋值为SIG_IGN表示忽略信号,赋值为SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用户自定义函数捕捉信号。

转载请注明原文地址: https://www.6miu.com/read-2631102.html

最新回复(0)