赞
踩
为什么信号要被保存
- 因为信号不会立即处理,所以要保存起来。
我们看到的普通信号编号是1到31的
对于普通信号而言,对普通进程而言,自己有还是没有,收到哪个信号
给进程发信号,实际上是进程的task_struct发信号
实际上进程的task_struct里面有一个整数,操作系统把它当成二进制序列来用,一共32位,每一位代表1个信号,一共31个普通信号,只用它的31位比特位来一一对应即可
信号是如何记录的?
实际上,当一个进程接收到某种信号后,该信号是被记录在该进程的进程控制块 task_struct 当中的。
你买了一个快递,于我而言,我当然知道寄来的是什么,而快递员是男是女,多大年纪这并不重要,重要的是快递是否到了,里面的东西是否完整无损。所以对进程来说,最重要的无非就是 “是否有信号” + “是谁”。
操作系统提供了 31 个普通信号,所以我们采用位图来保存信号,也就是说在 task_struct 结构中只要写上一个 unsigned int signals; (00000000 … 00000000) 这样一个字段即可。比特位的位置代表是哪一个信号,比特位的内容用 0 1 来代表是否。
其中比特位的位置代表信号的编号,而比特位的内容就代表是否收到对应信号,比如第6个比特位是1就表明收到了6号信号。
信号是如何发送的?
发送信号的本质就是写对应进程 task_struct 信号位图。因为 OS 是系统资源的管理者,所以把数据写到 task_struct 中只有 OS 有资格、有义务。所以,信号是操作系统发送的,通过修改对应进程的信号位图(0 -> 1)完成信号的发送,再朴素点说就是信号不是 OS 发送的,而是写的。
为什么是操作系统来发送信号?
操作系统是进程的管理者,只有它有资格去修改进程的task_struct
操作系统为什么不直接执行信号的内容,而让进程去干?
操作系统还要考虑别的进程,可能别的进程更重要,但是操作系统不知道,贸然让操作系统来干,可能会对其他进程产生影响
信号是如何产生的?
一个进程收到信号,本质就是该进程内的信号位图被修改了,也就是该进程的数据被修改了,而只有操作系统才有资格修改进程的数据,因为操作系统是进程的管理者。也就是说,信号的产生本质上就是操作系统直接去修改目标进程的task_struct中的信号位图。
注意: 信号只能由操作系统发送,但信号发送的方式有多种。
信号是如何被处理的
有3种方法
- 执行该信号的默认处理动作。
- 自定义信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。
- 忽略该信号。
在开始内容之前,先介绍一些信号的专业名词:
Linux内核的进程控制块PCB是一个结构体task_struct,除了包含进程id、状态、工作目录、用户id、组id、文件描述符表、还包含了信号相关的信息,主要指阻塞信号集(下面的block)和未决信号集(下面的pending)。
信号在内核中的表示示意图如下:
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)(就是收到信号,但没有执行信号对应的动作)
- 阻塞信号集(上面的block):
阻塞信号集,就是对信号进行阻塞或屏蔽设置的一个32位信号屏蔽字,同样每一位对应一个信号,如果某一位设置为1,那么该位对应的信号将被屏蔽,该信号会被延后处理,此时如果信号产生,那么未决信号集中对应的位置1,一直到该信号被解除屏蔽的时候(也就是阻塞信号集中对应位置0),才会去处理该信号,处理完信号,未决信号集中对应位反转回0。
也叫信号屏蔽字,将某些信号加入集合,对他们设置屏蔽,当屏蔽某个信号后,如果再收到该信号,该信号的处理将推后至解除屏蔽后。(也就是说,信号一旦被阻塞,就不能递达)
- 未决信号集(上面的pending):
未决信号集就是当前进程未处理的信号的集合,未决信号集实际上是一个32位数,该字的每一个位对应一个信号,如果该位为1表示信号还未被处理,如果改为置为0,表示信号已经被处理或者没有传递该信号。
- 信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态;当信号被处理对应位翻转回为0,这一时刻往往非常短暂。
- 信号产生后由于某些原因主要是阻塞不能抵达,这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。
- 而阻塞信号集会影响到未决信号集
比如说我在阻塞信号集中将2号信号为置为1,也就是将2号信号屏蔽,那么未决信号集中2号信号对应的位就会变为1(未决状态),一直阻塞在这种状态。(也就是说,信号一旦被阻塞,就不能递达,一直处于未决状态)
内核通过读取未决信号集来判断信号是否应被处理,信号屏蔽字mask可以影响未决信号集,而我们可以在应用程序中自定义set来改变mask来达到屏蔽指定信号的目的。
回到这个图
在上图中,
我们下来让大家见见handler里面的SIG_DFL和SIG_IGN
先来SIG_DFL
- #include<iostream>
- #include<signal.h>
- #include<unistd.h>
- using namespace std;
-
- int main()
- {
- signal(2, SIG_DFL); //实施2号信号的默认处理动作
- while (1){
- cout<<"I am running…… , pid:"<<getpid()<<endl;
- sleep(1);
- }
- return 0;
- }
好像跟它原来的差不多
我们再看看
- #include<iostream>
- #include<signal.h>
- #include<unistd.h>
- using namespace std;
-
- int main()
- {
- signal(2, SIG_IGN); //忽略2号信号
-
- while (1){
- cout<<"I am running…… , pid:"<<getpid()<<endl;
- sleep(1);
- }
- return 0;
- }
我们看到进程不理我的2号信号了
再看看第3种
- #include<iostream>
- #include<signal.h>
- #include<unistd.h>
- using namespace std;
-
- void sighandler(int signo)
- {
- cout<<"get a signo :"<<signo<<endl;
- exit(1);
- }
- int main()
- {
- signal(2, sighandler); //将2号信号的默认处理动作修改为sighandler方法
- while (1){
- cout<<"I am running…… , pid:"<<getpid()<<endl;
- sleep(1);
- }
- return 0;
- }

总结一下:
- 在block位图中,比特位的位置代表某一个信号,比特位的内容代表该信号是否被阻塞。
- 在pending位图中,比特位的位置代表某一个信号,比特位的内容代表该信号是否被处理。
- handler表本质上是一个函数指针数组,数组的下标代表某一个信号,数组的内容代表该信号递达时的处理动作,处理动作包括默认、忽略以及自定义。
- block、pending和handler这三张表的每一个位置是一一对应的。
我们对于信号最核心的理解就在上面了
根据信号在内核中的表示方法,每个信号的未决标志只有一个比特位,非0即1,如果不记录该信号产生了多少次,那么阻塞标志也只有一个比特位。
因此,未决和阻塞标志可以用操作系统的数据类型sigset_t来存储。
在我当前的云服务器中,sigset_t类型的定义如下:(不同操作系统实现sigset_t的方案可能不同)
- #define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
- typedef struct
- {
- unsigned long int __val[_SIGSET_NWORDS];
- } __sigset_t;
-
- typedef __sigset_t sigset_t;
sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态。
阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”,至于这个类型内部如何存储这些bit则依赖于系统的实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。
- #include <signal.h>
- typedef unsigned long sigset_t; /*信号集类型,其实就是一个32位的字*/
-
- /*清空信号集,将某个信号集全部清0*/
- int sigemptyset(sigset_t *set);
-
- /*填充信号集,将某个信号集全部置1*/
- int sigfillset(sigset_t *set);
-
- /*将某个信号signum加入信号集set*/
- int sigaddset(sigset_t *set, int signum);
-
- /*将某个信号清出信号集,从信号集ste中删除信号signum,(其实就是本来某个屏蔽信号字中置1的位清0)*/
- int sigdelset(sigset_t *set, int signum);
-
- /*判断某个信号是否在信号集中,
- 返回值 在集合:1;不在:0;出错:-1 (其余四个函数成功返回0,失败返回-1)*/
- int sigismember(const sigset_t *set, int signum);
-

函数解释:
注意: 在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号处于确定的状态。
例如,我们可以按照如下方式使用这些函数。
- #include <stdio.h>
- #include <signal.h>
-
- int main()
- {
- sigset_t s; //用户空间定义的变量
-
- sigemptyset(&s);
-
- sigfillset(&s);
-
- sigaddset(&s, SIGINT);
-
- sigdelset(&s, SIGINT);
-
- sigismember(&s, SIGINT);
- return 0;
- }

注意: 代码中定义的sigset_t类型的变量s,与我们平常定义的变量一样都是在用户空间定义的变量,所以后面我们用信号集操作函数对变量s的操作实际上只是对用户空间的变量s做了修改,并不会影响进程的任何行为。
因此,我们还需要通过系统调用,才能将变量s的数据设置进操作系统。
sigprocmask函数可以用于读取或更改进程的信号屏蔽字(阻塞信号集,也就是block),该函数的函数原型如下:
设置阻塞或解除阻塞信号集,用来屏蔽信号或解除屏蔽,其本质是读取或修改进程的PCB中的信号屏蔽字。
需要注意的是,屏蔽信号只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢弃处理。
函数参数
how:
假设当前的信号屏蔽字(阻塞信号集,也就是block)为mask
set:传入参数,是一个位图(32位),set中哪位置1,就表示当前进程屏蔽哪个信号。
oldset:传出参数,保存旧的信号屏蔽集(阻塞信号集,也就是block),可用于恢复上次设置。
参数说明:
- 如果oldset是非空指针,则读取进程当前的信号屏蔽字(阻塞信号集,也就是block)通过oset参数传出。
- 如果set是非空指针,则更改进程的信号屏蔽字(阻塞信号集,也就是block),参数how指示如何更改。
- 如果oldset和set都是非空指针,则先将原来的信号屏蔽字(阻塞信号集,也就是block)备份到oset里,然后根据set和how参数更改信号屏蔽字。
假设当前的信号屏蔽字(阻塞信号集,也就是block)为mask,下表说明了how参数的可选值及其含义:
返回值说明:
注意: 如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask函数返回前,至少将其中一个信号递达。
sigpending函数可以用于读取进程的未决信号集,该函数的函数原型如下:
sigpending函数读取当前进程的未决信号集,并通过set参数传出,set是个传出参数。和上面的oldest参数是一样的
该函数调用成功返回0,出错返回-1。
好了,接口讲到这里,我们接下来来使用一下
老规矩,先复习makefile
实验步骤如下:
- #include<iostream>
- #include<signal.h>
- #include<unistd.h>
- using namespace std;
-
- void printPending(sigset_t *pending)
- {
- int i = 1;
- for (i = 1; i <= 31; i++){
- if (sigismember(pending, i)){
- printf("1 ");
- }
- else{
- printf("0 ");
- }
- }
- printf("\n");
- }
-
- int main()
- {
- //1.对2号信号进行屏蔽
-
- //1.1.准备好我们要传进去的block信号集
- sigset_t blockset;//创建一个要传入的block信号集
- sigemptyset(&blockset);//清空要传入的block信号集
- sigaddset(&blockset,2);//将要传入的block信号集的2号信号位置于1
-
- //1.2.将准备好的block信号集通过系统调用接口传进进程
- sigset_t oldblockset;//创建1个block信号集来存储当前进程旧的block信号集
- sigemptyset(&oldblockset);//清空block信号集,准备存储当前进程旧的block信号集
- sigprocmask(SIG_SETMASK,&blockset,&oldblockset);//更换block信号集合
-
- //至此我们已经把2号信号屏蔽了
-
- //2.重复打印当前进程的未决信号集
- sigset_t pending;
- while(1)
- {
- //2.1.获取
- int n=sigpending(&pending);
- if(n<0) continue;
-
- //2.2.打印pending
- printPending(&pending);
- sleep(1);
- }
-
- }

可以看到,程序刚刚运行时,因为没有收到任何信号,所以此时该进程的pending表一直是全0,而当我们使用kill命令向该进程发送2号信号后,由于2号信号是阻塞的,因此2号信号一直处于未决状态,所以我们看到pending表中的第二个数字一直是1。
为了看到2号信号递达后pending表的变化,我们可以设置一段时间后,自动解除2号信号的阻塞状态,解除2号信号的阻塞状态后2号信号就会立即被递达。因为2号信号的默认处理动作是终止进程,所以为了看到2号信号递达后的pending表,我们可以将2号信号进行捕捉,让2号信号递达时执行我们所给的自定义动作。
- #include<iostream>
- #include<signal.h>
- #include<unistd.h>
- using namespace std;
-
- void printPending(sigset_t *pending)
- {
- int i = 1;
- for (i = 1; i <= 31; i++){
- if (sigismember(pending, i)){
- printf("1 ");
- }
- else{
- printf("0 ");
- }
- }
- printf("\n");
- }
-
- void handler(int signo)
- {
- printf("handler signo:%d\n", signo);
- }
- int main()
- {
- signal(2, handler);//更改2号信号的动作
-
-
- //1.对2号信号进行屏蔽
-
- //1.1.准备好我们要传进去的block信号集
- sigset_t blockset;//创建一个要传入的block信号集
- sigemptyset(&blockset);//清空要传入的block信号集
- sigaddset(&blockset,2);//将要传入的block信号集的2号信号位置于1
-
- //1.2.将准备好的block信号集通过系统调用接口传进进程
- sigset_t oldblockset;//创建1个block信号集来存储当前进程旧的block信号集
- sigemptyset(&oldblockset);//清空block信号集,准备存储当前进程旧的block信号集
- sigprocmask(SIG_SETMASK,&blockset,&oldblockset);//更换block信号集合
-
- //至此我们已经把2号信号屏蔽了
-
- //2.重复打印当前进程的未决信号集
- sigset_t pending;
- int cnt=0;
- while(1)
- {
- //2.1.获取
- int n=sigpending(&pending);
- if(n<0) continue;
-
- //2.2.打印pending
- printPending(&pending);
- sleep(1);
-
- cnt++;
- if (cnt == 20){
- sigprocmask(2, &oldblockset, NULL); //恢复曾经的信号屏蔽字
- printf("恢复信号屏蔽字\n");
- }
- }
-
- }

此时就可以看到,进程收到2号信号后,该信号在一段时间内处于未决状态,当解除2号信号的屏蔽后,2号信号就会立即递达,执行我们所给的自定义动作,而此时的pending表也变回了全0。
细节: 在解除2号信号后,2号信号的自定义动作是在打印“恢复信号屏蔽字”之前执行的。因为如果调用sigprocmask解除对当前若干个未决信号的阻塞,则在sigprocmask函数返回前,至少将其中一个信号递达。
我们可以通过上面这个操作屏蔽所有信号吗?
- 绝对不可能,不用想都知道9号肯定不能,我没空写这部分啊,感兴趣的自己去试试看,我只能告诉你,我的试验结果是9号和19号不可被屏蔽,这个不就是不能被signal函数捕捉是信号吗!
- 也就是说9号信号和19号信号不可被捕捉,也不可被屏蔽
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。