信号
信号的基本理解
什么是信号
提到信号,大部分人的第一反应都是红绿灯,没错,这是日常生活中的一种信号,它给了人们提示,当各种颜色的灯亮起时我们应该做什么样的处理动作。不过我们今天说的信号时Linux下的信号(signal),我们回想一种场景,当我们在Linux下打开一个终端,假设现在正有一个进程在运行,它的工作就是不断循环输出“hello”,此时,当我们按下Ctrl-c时,这个进程就会被终止掉。在此,我们就可以认为Ctrl-c就是一种信号,当我们按下Ctrl-C时,就是向操作系统发出了一个信号,而对于该信号的处理动作就是终止掉当前进程。
总结:
·信号一定是与特定行为紧密相关的
·信号是一种通知机制,告诉进程即将发生的事情
·信号的产生是随机的—异步产生
附:Linux下的信号(输入kill -l命令就可以看到)
注意:这里的信号只有62个,32、33时没有的。其中1-31号信号为普通信号,34-64为实时信号。普通信号
1、普通信号在计算机中的存储:
这里的普通信号只有31个,很明显,我们用一种数据结构就可以对其进行表示—位图,因此,计算机采用了4字节的空间来存储普通信号,其中bit位的下表对应信号的编号。(bit位的内容我们在讲“阻塞信号”时再提出)
2、操作系统给一个进程发送信号的本质:
每个进程的PCB模块里都有一个关于信号的字段,操作系统在给一个目标进程发送信号时就是修改了目标进程PCB中的信号字段的内容。(具体是修改了什么,也是在讲“阻塞信号”时给出)
产生信号
以下任意一个条件都可以产生信号:
1、使用组合键:
当用户在终端模式下,使用一些组合键会使终端驱动程序给前台进程发送信号。比如上文提到的Ctrl+c,它会产生SIGINT信号,Ctrl+\会产生SIGQUIT信号,Ctrl+z会产生SIGTSTP信号,这些进程都会使前台进程停止。注意,这里只是针对前台进程。
假设我们现在有一个mykill进程,让其先在前台运行,按下Ctrl+c后观察:
现在我们再将其切为后台运行的程序:
2、硬件异常产生信号:
当我们在写程序的过程中,如果代码中有除0错误,或者写入野指针,指针越界等情况,都会产生信号使当前进程终止。这些情况由硬件检测到,然后通知内核,内核会向当前进程发送信号。
例如:我们给一段程序中写入这两行代码:
int m=10;
int a=m/0;
就会产生如下情况:
这是因为一旦程序中出现了除0错误,CPU运算单元会产生异常,内核将这个异常解释为SIGFPE信号发给当前进程。
相应的,如果一旦检测出当前进程访问了非法地址,MMU就会产生异常,内核会将这个异常解释为SIGSEGV信号发送给当前进程。
3、由软件条件产生:
使用alarm函数,当闹钟时间到的时候,内核会产生一个SIGALRM信号给当前进程,默认动作是是当前进程终止;或者向一个读端已经关闭的管道内写数据时,就会产生SIGPIPE的信号。
4、使用系统调用kill产生信号:
kill命令可以给任意一个进程发送信号,而kill也是由kill函数实现的。
如果不明确指定信号,则可以发送SIGTERM信号,该信号的默认处理动作是终止进程。
- 信号的处理
信号的处理包括三种方法:
1、忽略该信号;
2、对信号执行其默认的处理动作,例如SIGINT信号的默认处理动作就是终止进程;
3、对信号执行自定义动作,即提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号。
阻塞信号
关于阻塞信号的几个基本概念:
信号递达:实际正在对信号进行的处理动作(即上文中提到的三种方式)。
信号未决:从信号产生到信号递达之间的状态。
进程可以选择阻塞某个信号,被阻塞的信号在产生时处于未决状态,直到进程解除对该信号的阻塞时,才有可能被递达。信号在内核中的表示:
每个进程的PCB中都存有一个关于信号的字段,当操作系统向该进程发信号时就是修改这些字段的内容。上文提到了,信号在操作系统中是以位图的方式存储的,其实在PCB中有三张表来分别表示信号的状态及处理,即block表,pending表以及handler表:
block表:4字节位图表示,描述信号的阻塞状态,其下标表示信号编号,内容表示信号是否被阻塞,0表示没有阻塞,1表示已经阻塞。
pending表:4字节位图表示,描述信号的产生状态。下标同上,内容表示信号是否已经产生,0表示还未产生,1表示已经产生。
handler表:这个表中存储着对于对应信号的处理方式。
如果在进程解除对某个信号阻塞之前,这个信号产生了多次,那么该如何处理呢?
Linux是这样处理的:普通信号在产生多次时只计一次,而实时信号产生多次时可以依次放在一个队列里。
信号的有效无效:
从上图中来看,无论是block表还是pending表,都只有一个bit的标志位来标识每一个信号是否阻塞或未决,因此,未决和阻塞标志可以用相同的数据结构sigset_t来存储,sigset_t是信号集,这个类型中用一个bit位表示每个信号的“有效”和“无效”状态。
阻塞信号集中:“有效”或“无效”表示该信号是否被阻塞。(阻塞信号集也叫做信号屏蔽字)
未决信号集中:”有效“或”无效“表示该信号是否处于未决状态。信号集的操作:
信号集操作的函数:
int sigemptyset(sigset_t *set);
//初始化set所指向的信号集,清零其中所有信号对应bit位
int sigfillset(sigset_t *set);
//使其中所有信号的对应bit置位,表示该信号集的有效信号集的有效信号包括系统支持的所有信号。
int sigaddset(sigset_t *set, int signo);//在该信号集中添加某种有效信号
int sigdelset(sigset_t *set, int signo);//删除某种有效信号
int sigismember(const sigset_t *set, int signo);
//判断一个信号集中的有效信号是否包含某种信号包含返回1,不包含返回0,出错返回-1
注意:在使用sigset_t类型的变量前,一定要使用sigemptyset或sigfillset函数对其进行初始化,使信号集处于确定的状态。
-
sigprocmask函数:
功能:该函数用于读取或更改进程的信号屏蔽字(即阻塞信号集)。
函数原型:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:成功返回0,出错返回-1.
参数解释:
oset:传出当前信号屏蔽字,如果oset是非空指针,则读取进程的当前信号屏蔽字并通过oset传出,因此oset在这里是一个输出型参数。
set:如果set是非空指针,则更改进程的信号屏蔽字。
how:指示如何更改,如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset中,然后再根据set和how参数更改信号屏蔽字。
how参数的可选值及其含义:
-
sigpending函数:
功能:读取当前进程的未决信号集,通过set传出。
函数原型:
#include <signal.h>
int sigpending(sigset_t *set);
返回值:调用成功返回0,出错返回-1.
对以上函数进行测试:
代码:
测试结果:
上一篇: 关于店铺装修设计的几个要点!