欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

信号

程序员文章站 2022-03-19 13:21:19
...

信号的基本理解

  • 什么是信号
    提到信号,大部分人的第一反应都是红绿灯,没错,这是日常生活中的一种信号,它给了人们提示,当各种颜色的灯亮起时我们应该做什么样的处理动作。不过我们今天说的信号时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.

对以上函数进行测试:
代码:
信号

测试结果:
信号