Linux信号——概念

概述

在程序正常执行过程中可能出现各种情况,系统会产生硬件中断去执行响应动作。

在进程的PCB中通过bitmap存放信号的状态,操作系统若想给进程发信号,只需要修改PCB的bitmap。

进程收到信号并不是立即处理的,而是等cpu调度,切换内核态检查处理信号。

信号产生

信号相对与进程是异步的,在任何时候都有可能产生信号。

产生信号的可能:

  • 硬件异常 非法内存访问,MMU产生的异常
  • 系统调用 kill
  • 终端按键组合 ctr +c \ z 向前台进程发送信号
  • 软条件产生 alarm函数,SIGPIPE只写不读管道

查看系统信号:

  • kill -l

常用信号

HUP 关闭终端 INT 中断 QUIT crt+\ ABRT abort KILL 杀死进程

SEGV 段错误 PIPE 管道破裂 ALRM 闹钟信号 TERM 终止进程

CHLD 子进程死亡 CONT 继续 STOP 暂停 IO 异步IO

信号的分类

  1. 不可靠信号:会出现丢失,执行完定义的信号处理函数会恢复缺省动作
  2. 可靠信号:34~64 号,不会丢失,不会恢复缺省动作
  3. 非实时信号:不可靠信号
  4. 实时信号:立即处理的可靠信号

例如段错误 core dump (核心转储)产生:

一个c语言程序访问了非法内存,MMU发现了非法访问告诉操作系统,操作系统向出错进程发送SIGSEGV信号。

信号的处理方式

  1. 默认处理
  2. 忽略,KILL 和 STOP不能忽略
  3. 捕获,自行处理

注册信号

void (*signal(int signum, void (*handler)(int)))(int)

signum:要注册的信号

handler: 信号处理函数,SIG_IGN忽略||SIG_DFL缺省处理

这里c库把SIG_IGN这种标记的整数值强转为函数指针类型。

返回值:旧的信号处理函数

分析复杂函数声明的方法,从不是关键字的主体向右看,遇到右括号向左结合看

例,捕获SIGINT信号,捕获SIGQUIT信号

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void (*handlerOld)(int) = NULL;//旧的信号处理函数
void handler_int(int s)
{
    printf("recv %d, 关不了\n",s); 
}

void handler_quit(int s)
{
    printf("收到 quit %d 信号\n", s);
    //将int信号重新绑定为int原来的处理函数
    signal(SIGINT, handlerOld);
}
int main()
{
    handlerOld = signal(SIGINT, handler_int);
    signal(SIGQUIT, handler_quit);
    while(1){
        printf(".");
        fflush(stdout);
        sleep(1);
    }
}

执行结果:

..^Crecv 2, 关不了
…^Crecv 2, 关不了
….^\收到 quit 3 信号
..^C
[root@localhost 6_3]#

系统调用发信号:

  1. kill 向指定进程发信号
  2. raise 可以给当前进程发送指定信号
  3. abort 使当前进程异常终止

信号的状态

信号的状态分为三个:产生还没处理——未决,实际处理的动作——递达,在尚未递达可以选择屏蔽——阻塞。

被屏蔽的信号一直保持未决状态,直到解除屏蔽信号才随后一段时间才会被递达。

从PCB的角度来看,信号通过三个表完成信号处理的任务,分别是block表,pending表和handler表

block存放屏蔽位信息,pending表存放信号的未决状态,handler存放处理信号的函数指针。

通过信号集改变信号的状态

由于进程的信号信息都存于PCB中,在内核空间的数据是不能由用户进行更改的,所以操作系统提供信号集类型 sigset_t 以及对应的系统调用函数让用户可以改变信号状态。

例:屏蔽SIGINT信号,并且查看进程的pending表。

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

void printsigset(sigset_t* p)
{
    int i = 0;
    //pending表一共就32个信号,逐一检查
    while(i < 32){
        if(sigismember(p, i)){
            putchar('1');
        }
        else{
            putchar('0');
        }
        i++;
    }   
    printf("\n");
}

int main()
{
    sigset_t s, p;
    sigemptyset(&s);
    sigaddset(&s, SIGINT);
    //设置pcb的block表
    sigprocmask(SIG_BLOCK, &s, NULL);
    while(1){
        sigpending(&p);
        printsigset(&p);
        sleep(1);
    }
}

执行结果说明阻塞信号后,2号信号一直处于未决。

10000000000000000000000000000000
10000000000000000000000000000000
^C10100000000000000000000000000000
10100000000000000000000000000000
10100000000000000000000000000000

信号的捕捉过程

系统在递达过程如何调用对应的处理函数?这个过程叫做信号捕捉

比如信号处理动作是自定义的函数,过程如下图所示,4个黄点是切换状态的时机!这里写图片描述

其中为何2到3要切换到用户态去执行自定义函数?

因为自定义函数有不安全因素,不能用直接用内核的高权限去执行。

sigaction 是signal函数的加强版,能自定义处理函数时屏蔽的信号。

pause 函数使调用进程挂起,直到有信号递达,之后返回控制流程继续执行。

例:实现sleep的功能

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

void my_alrm(int signo){
    //空
}
unsigned int mysleep(unsigned int s)
{
    //注册信号处理函数
    struct sigaction new, old;
    unsigned int unslept = 0;
    new.sa_handler = my_alrm;
    new.sa_flags = 0;
    sigemptyset(&new.sa_mask);
    sigaction(SIGALRM, &new, &old);
    //设定闹钟
    alarm(s);
    pause();
    //查看闹钟剩余时间,恢复ALRM信号默认处理 
    unslept = alarm(0);
    sigaction(SIGALRM, &old, NULL); 
    return unslept;
}
int main(){
    while(1){
        if(mysleep(1)){
            printf("闹钟时间异常");
        }
        printf("i am sleeping\n");
        fflush(stdout);
    }
}

为什么需要给ALRM信号注册一个空函数?

因为闹钟时间到后,进程收到ALRM信号,若是默认处理会退出进程。再者因为pause函数在ALRM信号递达以后才能继续执行主流程代码。

为什么在mysleep函数最后要恢复ALRM的默认处理?

因为接口不应该改变信号原本的处理方式。

上述代码存在隐患:竟态条件。

猜你喜欢

转载自blog.csdn.net/hanzheng6602/article/details/80642877
今日推荐