[Linux C内核] 信号

[Linux C内核] 信号

下面对Linux 下的信号的产生, 响应机制, 以及可重入(reentrant)进行介绍~

[首先再次给网易云音乐点个赞~\(≧▽≦)/~, 在写博客的时候听着歌 而且还有歌词滚动, 真的太棒了 ~~~~]

[不行, 我要附上一张截图]

Netease-Blog

恩,心满意足的窝开始写文章了 ~~

什么是信号

在Interprocess Communication(IPC)的情形中,如何让不同的process进行通信是一个很重要的需要解决的问题, 而信号就是不同的进程之间通信的一种方式, 而在Linux下运行的每个程序, 都会受到内核的控制, 内核就通过信号告知进程一些控制信息, 如中断执行, 停止执行, 进程和进程之间也会进行信号的传递, 比如 父进程fork出的子进程, 子进程退出的时候会发送SIGCHLD信号给父进程, 告诉父进程, 我运行完啦~, 那么Linux是如何实现信号的捕捉,并且送达到相应的程序的呢? 下面我们举个具体例子来看一下

我们以最简单的情景: 用户在terminal下执行一个程序,然后由用户通过Ctrl+C打断程序的执行,返回到terminal下的整个过程中, 信号的流向进行说明

  •  输入命令运行程序, 在Shell下启动一个前台进程, 分配了相应的PCB
  • 程序运行过程中,用户按下 Ctrl+C, 因为有键盘输入的介入, 产生了硬件中断, CPU从用户态切换到内核态,去处理中断
  • 终端的相应驱动程序,将Ctrl+C解释为 SIGINT信号, 并记录在当前运行在终端中的程序的PCB中
  • 当要从内核态切换回用户态之前, 会对PCB中记录的信号, 进行处理, 检察到PCB中有 SIGINT信号, 于是对信号进行处理, 而目前这个信号的处理动作是终止进程执行, 于是这个程序终止了

信号分为实时信号和非实时信号, kill 指令后面可以接信号常量,  根据信号常量对应的数值区分实时/非实时信号 > 34的是实时信号 我们这里也值讨论 < 34的信号(即非实时的信号) 关于信号的更详细介绍, 参考 signal(7)即可

产生信号的条件

上面只说了通过Ctrl+C产生一个信号发送给进程, 那么还有什么方式产生一个信号呢? 下面列出主要的几种方式

  •  用户在终端进行某些按键的时候,  终端驱动程序 会发送信号给相应的进程
  • 硬件异常也会产生信号, 如当前进程执行了除以0指令, CPU的运算单元会产生异常
  • 一个进程调用 kill 系统函数可以发送信号给某个进程, 注意这里的kill不仅仅能够kill掉某个进程, kill可以用来发送各种信号给其他的进程
  • 当内核检测到某种软条件可以通过信号通知进程, 常见的是alarm产生的SIGALRM信号 & 管道通信产生的SIGPIPE信号

下面我们通过一个alarm 小程序查看一下产生信号的条件之一 软条件由内核产生信号:

/*************************************************************************
    > File Name: sigalarm.c
    > Author: VOID_133
    > ################### 
    > Mail: ################### 
    > Created Time: Tue 24 May 2016 04:44:22 PM CST
 ************************************************************************/
#include<unistd.h>
#include<stdio.h>

int main(void)
{
    alarm(1);
    int cnt = 0;
    while(1)
    {
        printf("%d\n", cnt++);
    }
    return 0;
}



执行该程序, 该程序会在1s(精确到 0.01s) 时间内数数, 然后时间到了就会退出, 我们用 time 运行一下看看

$time ./sigalarm

结果如下

61102
61103
[2]    3215 alarm      ./sigalarm
./sigalarm  0.03s user 0.07s system 9% cpu 1.001 total

可以看出来 这里就是产生了SIGALRM信号, 然后该进程接收到这个信号之后, 默认的处理动作就是退出程序的执行, 因而程序退出了, 之后我们会介绍如何更改默认的信号处理方式, 先不要着急~~

信号的本质

如果想要对信号进行更高级的操作的话, 那么我们需要了解信号的本质, 也就是信号在内核中到底是如何存储的, 如何表示的, 我们用一张图来说明信号在内核中的表示

Document_2016-05-27_20-24-40

黄色的是PCB, 紫色的就是PCB中负责存储信号相关信息的部分, 然后我们来看一下这个部分, 这个部分规定了该进程对每个信号的处理, 响应方式, pending位表示 信号是否已经到达(叫做产生更准确,不过我认为到达更形象), block位表示是否屏蔽这个信号的响应(即这个信号到来之后,将信号阻塞住)WARN:”阻塞”和”忽略这个信号” 不是一个概念,   “忽略这个信号” 是在信号递达之后的一个对信号的响应方式, 而阻塞, 这是不响应这个信号, 就好比你和你女票闹矛盾了, 你为了防止他不接电话,特地换了个新手机卡,给她打电话,结果电话关机(我们假设她真的是关机)[这是阻塞], 然后, 你等到晚上, 终于电话开机了, 然后你打过去, 她接起来,你开始哄她, 然而整个过程你女票一句话没说,并且淡定的挂了电话, [忽略]了你的信号, 是不是很生动且形象desu~~(怕被广大单身程序员打死的修罗已经逃走)

Hmm, 我们还是看上面那个图, 那个图上还有一部分我们没有说明, 就是 handler部分,  通过你工资就能看出来,这里指定了对每个信号的处理方式 对一个信号的处理方式有以下几种:

  •  默认处理方式 SIG_DFL 这个就是默认的处理方式啦,没什么解释的
  • SIG_IGN 忽略这个信号
  • handler_function 用户自己定义的handler函数~  这种方式又叫做 捕捉一个信号

相信通过上面的内容,大家对信号的产生, 递达 , 处理 有一个基本的了解了~~ 接下来我们就用一个具体的例子 演示一下上述内容

一个无法通过Ctrl + C退出的程序

下面先演示一个最简单的例子, 然后我们还会给出他的升级版, 一个除了****都怎样都无法退出的程序, 首先是第一个程序

/*************************************************************************
    > File Name: sigset_1.c
    > Author: VOID_133
    > ################### 
    > Mail: ################### 
    > Created Time: Thu 26 May 2016 07:32:22 AM CST
 ************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>

void printsigset(const sigset_t *p)
{
    int i = 0;
    for(i = 1; i < 32; i++)
    {
        if(sigismember(p, i) == 1)
            putchar('1');
        else
            putchar('0');
    }
    puts("");
}

int main(void)
{  
    sigset_t set, p;
    //Init the signal
    sigemptyset(&set);
    //Add signals
    alarm(5);
    if(sigaddset(&set, SIGINT) < 0)
    {
        perror("add SIGKILL err");
        exit(1);
    }
    sigprocmask(SIG_BLOCK, &set, NULL);
    while(1)
    {
        sigpending(&p);
        printsigset(&p);
        sleep(1);
    }
}

这个程序运行后, 5秒钟之后才会退出, 之间无论你怎么按Ctrl+C都没法停下这个程序, 不过你会发现按完Ctrl+C之后, 输出的东西改变了, 有一位变成了1 , 这个稍后会进行解释

我们来解释一下这段代码, 先看main函数 首先我们用sigempty将信号集 set 清空 这里解释一下信号集:

信号集就是一个用于存储所有信号的信息的数据集合, 每一个信号的”有效/无效” 都用一个bit来进行表示, 至于具体哪个bit表示哪个信号,这个是不确定的, 由操作系统来决定, 所以对其的操作都要通过相应的函数来实现,而不能直接修改某个bit.

这里就是将所有的信号都设置为”无效” ,不过这里还没有进行任何操作, 要对信号进行操作的时候, 还要用其他的函数,将set作为参数传递进去. 这里可以将sigemptyset理解为 不选择任何一个信号进行操作, 和这个函数相对的是 sigfillset 意为选中所有信号进行操作

然后 我们将SIGINT信号选中, 使用 sigaddset, 他的兄弟函数是 sigdelset

然后 将选中的信号进行操作 sigprocmask, 设定屏蔽字, 让当前进程屏蔽 SIGINT, 即阻塞该信号

另外sigpending函数是取出现在产生但是还未递达的信号, 生成一个信号集, 存储它的信息, 然后使用自定义的一个函数打印出来了这个信号集的内容, 现在我们可以解释为什么按下Ctrl+C之后, 有一位变成了 1, 因为这个表示SIGINT, 信号的产生

然后我们来看下面这个加强版的, “任何”信号都杀不死的程序

/*************************************************************************
    > File Name: sigset_1.c
    > Author: VOID_133
    > ################### 
    > Mail: ################### 
    > Created Time: Thu 26 May 2016 07:32:22 AM CST
 ************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>

void printsigset(const sigset_t *p)
{
    int i = 0;
    for(i = 1; i < 32; i++)
    {
        if(sigismember(p, i) == 1)
            putchar('1');
        else
            putchar('0');
    }
    puts("");
}

int main(void)
{
    printf("This program will run forever, you cannot stop it\n");
    printf("If you are sure to run it, press enter, else Ctrl + C NOW");
    getchar();
    sigset_t set, p;
    //Init the signal
    sigfillset(&set);
    //Add signals
    alarm(5);
    if(sigaddset(&set, SIGKILL) < 0)
    {
        perror("add SIGKILL err");
        exit(1);
    }
    sigprocmask(SIG_BLOCK, &set, NULL);
    while(1)
    {
        sigpending(&p);
        printsigset(&p);
        sleep(1);
    }
}

这个代码不用再解释了, 我们直接暴力的屏蔽了所有的信号当然什么信号都没有用啦~~(可怕

这个程序运行起来之后, 只有一种方式不重启电脑停止这个程序: 那就是向该进程发送SIGKILL信号, 引用兔子的一句话, “没什么是一发SIGKILL不能杀死的, 如果有的话,就两发”. 可是SIGKILL为什么能够不受我们屏蔽字的影响, 而使程序退出呢? 注意我还特意对SIGKILL信号位选中, 生怕是因为我没选中才导致这个信号没被屏蔽.

The SIGKILL signal is sent to a process to cause it to terminate immediately (kill). In contrast to SIGTERM and SIGINT, this signal cannot be caught or ignored, and the receiving process cannot perform any clean-up upon receiving this signal.

根据wiki的解释, SIGKILL这个信号根本就没有被送到当前进程,当前进程没有接收到这个信号, 直接被Kernel杀死了(好可怜, 不明不白的就挂了) en, 这样就能解释同上面的问题了

信号捕捉的实现 自定义信号处理函数

下面我们通过一段代码实现一个自定义的闹钟demo, 当时间到了的时候, 会显示timeup并且退出, 期间可以通过Ctrl-C打断,打断后会输出一个萌萌的表情说明,然后退出, 代码如下

/*************************************************************************
    > File Name: sigcatch.c
    > Author: VOID_133
    > ################### 
    > Mail: ################### 
    > Created Time: Thu 26 May 2016 09:03:32 AM CST
 ************************************************************************/
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>

typedef struct sigaction Action;
void interrupt();
void alarmclock();

int main(void)
{
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGALRM);
    Action sigact;
    sigact.sa_mask = set;
    sigact.sa_handler = alarmclock;
    sigaction(SIGALRM, &sigact, NULL);

    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigact.sa_mask = set;
    sigact.sa_handler = interrupt;
    sigaction(SIGINT, &sigact, NULL);
    //This is useless, non-realtime signal will be sent only once
    printf("The program will sleep for 3 secs...\n");
    while(1)
    {
        alarm(3);
        pause();
    }
    return 0;
}

void interrupt()
{
    printf("0.0 Interruptted");
    fflush(NULL);
    exit(0);
}

void alarmclock()
{
    printf("Time up!!!\n");
    exit(0);
}

对于一个信号的处理方式的描述的结构体叫做 sigaction , sigaction也是改变信号相应响应方式的函数, 通过设定 sigaction 的 sa_handler字段, 再调用sigaction函数, 就可以修改相应的信号的处理方式了

可重入将在下一篇文章进行介绍

Leave a Reply

Your email address will not be published. Required fields are marked *

2 × 4 =

This site uses Akismet to reduce spam. Learn how your comment data is processed.