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

Linux进程信号探究

程序员文章站 2022-07-12 12:34:59
...

概述

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

在进程的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
[aaa@qq.com 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个黄点是切换状态的时机!
进程收到信号不是立即处理的,不能为了处理信号而挂起当前进程,造成资源浪费。而是在恰当 时机进行处理!比如说中断返回的时候,或者内核态返回用户态的时候。因此信号实时性差
(图中1有点错误,不是收到信号产生硬件中断,而是因为执行到其他语句进入内核态)
Linux进程信号探究

其中为何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的默认处理?

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

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