信号的捕捉与处理

        前边已经详细介绍了信号的产生,经常说到的一句话应该是向进程发送一个信号,但是进程不是像人,可以自己去感知信号。那么,到底是怎么向通过什么方式将进程发送信号的呢?怎么样算是给进程发送了一个信号呢?

      和红绿灯一样,信号其实也就是一种规定。在现实生活中,只有那些在相应领域里有着绝对话语权的人才能给出规定,比如一所学校的校长,能制定校规。那么电脑里边谁能制定规则呢?操作系统,毫无疑问,只有内核能制定规则。操作系统是不信任任何用户的,所以用户的权限永远都比内核的权限要低很多,而有的操作仅仅依靠用户的那点权限是没法完成的,还得依靠内核来处理。所以,信号的规定是在内核里边,进程的信号都是操作系统通过内核发送的。那么,具体是怎样发送的呢?
      我们都知道每个进程都有一个进程控制块(PCB),控制了一个进程PCB就相当于控制了这个进程。操作系统就是通过控制进程的PCB来控制进程的。前边已经说过了普通信号是有 31个 的,如果用一个比特位来代表一个信号,那么,使用一个整型(32位机中整型就是4个字节,总共有32个比特位)就足够表示了。

介绍几个与信号相关的概念
实际执行信号的处理动作叫做信号 递达(Delivery)
信号从产生到递达之间的状态,称为信号 未决(Pending)。 当进程收到一个信号时,并不是立刻就去处理,而是等到合适的时间再去处理。对于这个合适的时间,后边再做详细的介绍。
进程可以 阻塞(block) 某个信号。有时候,我们希望在某段时间内不要受到某种信号的干扰,此时就可以选择将该信号阻塞起来。
说明:
阻塞一个信号并不是不让进程收到这个信号,而是让被阻塞的信号一直保持在未决状态,直到进程解除对该信号的阻塞,该信号才可以被递达。所以,一旦某个信号被阻塞了,那么,它永远不能被递达,除非解除对它的阻塞。
block  标明是否被阻塞。阻塞了则为1,未被阻塞则为0。
pending  标明进程是否某个信号,收到了则为1,没有收到则为0。
handler 标明对该信号的处理动作。
信号的处理动作分三种: (SIG_IGN) 忽略 (SIG_DEL)执行系统默认操作 (通常是终止进程)、用户 对信号捕捉后进行自定义处理 。这个自定义处理函数是用户自己写的,是在用户空间里边。前边两种方法 :SIG_IGN 和 SIG_DEL是  宏; 而第三种处理方法即自定义处理,handler里边存的是一个 函数指针

上图表明一个进程收到了1号信号,但是1号信号被阻塞了,在该进程解除对1号信号的阻塞之前,1号信号永远不会被递达。当解除对1号信号的阻塞后,对1号信号的处理就是忽略它。
进程收到了2号信号,并没有阻塞信号,但是进程对2号信号进行了捕捉,所以当2号信号被递达,就会采取用户自己定义的处理动作。
进程没有收到3号信号,但是未雨绸缪地先将3号信号进行了阻塞,当收到3号信号,并解除对其的阻塞之后,对信号的处理就是执行它的默认动作终止进程。

每个进程的PCB里边都有这样一张“表”,而PCB又是由操作系统控制的,所以内核给进程发送一个信号就很简单了,只需要将相应信号的pending比特位置1就行了。
所以 操作系统给进程发送一个信号的实质就是修改PCB里边相应的比特位,将其置1。

到这里,系统给进程发信号就将清楚了,现在来看看另一个难点:怎么捕捉信号?什么信号都能被捕捉么?
如果信号的处理动作是用户自定义函数,在信号递达的时候就调用这个函数,这称为捕捉信号。
信号捕捉是通过signal函数来实现的

#include <signal.h>
typedef void( *sighandler_t)(int);  
//使用typedef 定义了一个函数指针类型,该函数指针类型的函数的参数是一个整型,返回值是 void

signal函数的函数原型
sighandler_t signal(int signum, sighandler_t handler);
第一个参数是想要捕捉的信号编号,第二个参数是一个函数指针类型的指针,指向一个函数(用于表示信号处理动作的函数)。
sighandler_t 是一个函数指针类型。
signal的第一个参数会作为第二个参数调用信号处理函数时的参数。
举个例子
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handle(int sig)
{
    printf("catch a signal : %d\n",sig);
}
int main()
{
    signal(2,handle);
    while(1)
    {
        printf("I'm action...\n");
        sleep(1);
    }
    return 0;
}


查看结果发现成功的将2号信号捕捉。当然,我们也可以以同样的方式捕捉其他的信号。但是,并不是所有的信号都能被捕捉。比如说9号信号(SIGKILL)和19号信号(SIGSTOP)都不能被捕捉。为什么这两个信号不能被捕捉呢?想象一下,如果所有信号都能被捕捉,那么,你写一个程序将所有的信号都捕捉了,那么这个程序岂不就是杀不死天下无敌了?要是这个程序是在做一些带攻击性的事情,这岂不是很危险?而操作系统是不可能留下这么大一个隐患的。

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

void handle(int signo)
{
    printf("received a signal:%d\n",signo);
}

int main()
{
     signal(9,handle);
     while(1)
     {
        printf("I'm active...\n");
        sleep(1);
     }
    return 0;
}

当程序正在运行时,通过在键盘端输入Ctrl+\产生9号信号发送给进程,发现进程还是被终止了,说明,9号信号并没有成功捕捉到。

在前边说过,当进程在收到一个信号后并不一定会立即处理,如果不是非常紧急的信号是不会立即进行处理的,而是等到一个合适的时机才会处理。而这个合适的时机又是什么时候呢?
当进程由于中断、异常或系统调用而进入到内核,处理完成准备再次切换到用户态去继续执行当前进程之前,会检测该进程是否有可递达的信号,如果有,那么会先处理信号,然后再回去继续执行。
为什么进程不在收到信号的时候就立即执行对信号的处理呢?
如果要去处理信号,就得将当前进程挂起(进程切换),而进程切换是需要保存上下文信息等,还要为了处理信号而切换到内核态去,这样开销是太大了,进程执行的好好的本来就不愿意被信号所打扰,结果还要浪费很多时间和精力去处理它,进程肯定是不愿意的。所以,选择在进程从内核态正要切换回用户态的时候再处理,这是一种很节约处理成本的操作。

信号的处理过程

猜你喜欢

转载自blog.csdn.net/guaiguaihenguai/article/details/79841711