【ONE·Linux || 信号】

总言

  信号相关认识:简述信号产生、信号保存、信号捕获,以及涉及的函数指令。

文章目录

  • 总言
  • 1、是什么和为什么
  • 2、如何使用信号(理论→实践)
    • 2.1、信号产生
      • 2.1.1、概述
      • 2.1.2、方式一:通过键盘(按键终端)
        • 2.1.2.1、signal
        • 2.1.2.2、core dump
      • 2.1.3、方式二:调用系统函数向进程发信号
        • 2.1.3.1、kill
        • 2.1.3.2、raise
        • 2.1.3.3、abort
      • 2.1.4、方式三:由软件条件产生信号
        • 2.1.4.1、SIGPIPE
        • 2.1.4.2、alarm
      • 2.1.5、方式四:硬件异常产生信号
        • 2.1.5.1、演示一:除0
        • 2.1.5.2、演示二:野指针、越界
    • 2.2、信号保存
      • 2.2.1、一些概念
      • 2.2.2、信号集操作相关函数及其使用
        • 2.2.2.1、相关函数介绍
        • 2.2.2.2、编码样例
    • 2.3、信号处理
      • 2.3.1、原理说明
        • 2.3.1.1、什么时候处理信号?
        • 2.3.1.2、如何处理信号?
      • 2.3.2、信号捕捉操作
        • 2.3.2.1、signal
        • 2.3.2.2、sigaction
  • 3、其它
    • 3.1、可重入函数
    • 3.2、volatile关键字
    • 3.3、SIGCHLD信号

  
  

1、是什么和为什么

  1)、Linux信号说明
  Linux信号本质是一种通知机制,用户/操作系统通过发送一定的信号,通知进程某些事件的发送。进程能够接收到信号,并根据信号通知做出后续的处理工作。
  
  
  2)、概述(结合进程理解)
  1、进程要处理信号,必须具备识别信号的能力(看到信号+处理操作。进程能够看到信号,是因为进程编写时,其内置了相关操作流程(程序员))
  2、信号处理可以延迟。信号发送的时间是随机的,进程接受到信号时可能在处理其它事件。此时,信号被临时记录下,以便后续处理。
  3、一般而言,信号的产生和处理相对进程而言是异步的。(不同频,发送端发送信号后可能直接去处理其它事件了,而接受端接受信号也不一定会立马就对其响应)
  
  
  PS:信号与信号量是两个不同的概念,没有关系。
  
  
  
  
  

2、如何使用信号(理论→实践)

2.1、信号产生

  所有的信号有其来源,但最终都被OS识别,解释,并发送。

2.1.1、概述

  1)、信号处理的常见方式

  1、默认处理:进程自带的处理方式(原先程序设计时就写好的处理逻辑)。
  2、忽略:忽略此信号。
  3、自定义动作:Catch捕捉信号(提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号)。
  
  
  
  2)、查看信号的相关指令介绍

   方式一:kill -l,可查看系统定义的信号列表

【ONE·Linux || 信号】_第1张图片

  说明:
  1、每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到。
  2、一共有62个信号。没有0号信号,31~34中,没有32、33信号。
     前31个[1,31]:普通信号
     后31个[34,64]:带RT字段,是实时信号
  
  
  
  
   方式二:man 7 singal ,可查看特定信号的默认处理方式

NAME
       signal - overview of signals

DESCRIPTION
       Linux supports both POSIX reliable signals (hereinafter "standard signals") and POSIX real-time signals.

   Signal dispositions
       Each signal has a current disposition, which determines how the process behaves when it is delivered the signal.

       The entries in the "Action" column of the tables below specify the default disposition for each signal, as follows://默认处理行为:

       Term   Default action is to terminate the process.//终止进程

       Ign    Default action is to ignore the signal.//忽略

       Core   Default action is to terminate the process and dump core (see core(5)).//终止,并生成核心存储

       Stop   Default action is to stop the process.//停止

       Cont   Default action is to continue the process if it is currently stopped.//继续

  
  
  
  
  3)、问答

  1、如何理解组合键变成信号?
  键盘是以中断方式进行工作的,OS既然有键盘驱动,能识别每个键位,自然也能识别组合键。
  结合下述内容,当我们打出组合键时,OS 会解析该组合键,在进程列表中查找到前台运行的进程,然后将相关信号写入对应的进程内部的位图结构中。(例如,0000 0000 → 0000 0001 表示发送1号信号)
  
  
  
  2、如何理解信号被进程保存?
  进程需要知道:a.接收到的是什么信号,b.该信号当前是否产生。
  由于信号有很多,这些信号也需要管理起来,而进程内部有专门保存信号的相关数据结构,实际上,PCB内部保存了信号位图字段。
  
  
  
  3、信号发送的本质?
  根据上述可知,信号位图是在task_struct上的,而task_struct是内核数据结构,属于操作系统OS。信号发送时,OS会向目标进程写入信号,并修改目标进程对应的pcb中指定的位图结构,完成信号"发送"的过程。
  
  
  
  
  

2.1.2、方式一:通过键盘(按键终端)

2.1.2.1、signal

  1)、基本说明
  相关函数:man signal

NAME
       signal - ANSI C signal handling

SYNOPSIS
       #include 

       typedef void (*sighandler_t)(int);

       sighandler_t signal(int signum, sighandler_t handler);

  typedef void (*sighandler_t)(int);,可看到sighandler_t是一个函数指针,故而参数handler处需要传入一个函数。根据之前所学,这样子的函数称为回调函数。在这里,signal通过回调的方式,修改对应信号的处理方式。
  signum,该参数可以传递信号名称,也可以传递信号编号。(PS:signum此处还可以传递一下宏)

/* Fake signal functions.  */
#define SIG_ERR	((__sighandler_t) -1)		/* Error return.  */
#define SIG_DFL	((__sighandler_t) 0)		/* Default action.  */
#define SIG_IGN	((__sighandler_t) 1)		/* Ignore signal.  */

#ifdef __USE_UNIX98
# define SIG_HOLD	((__sighandler_t) 2)	/* Add signal to hold mask.  */
#endif

  返回值是原先信号的处理方式。

RETURN VALUE
       signal()  returns  the  previous value of the signal handler, or SIG_ERR on error.
       In the event of an error, errno is set to indicate the cause.

  
  
  
  2)、使用演示
  相关代码:

#include
#include
#include
#include
using namespace std;

void catchSig(int signum)//typedef void (*sighandler_t)(int);
{
    cout<<"进程捕捉到了一个信号,正在处理该信号:" << signum << "pid:" << getpid() <<endl;
}

int main()
{
    signal(SIGINT,catchSig);//捕捉SIGINT,2号信号,将其处理方式修改为catchSig中的处理方式

    while(true)//死循环:用于让进行一直运行,方便后续传入信号时,观察现象
    {
        cout << "当前进程正在运行,pid: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

  演示结果:

【ONE·Linux || 信号】_第2张图片
  
  对1:signal函数,仅仅是用于修改对特定信号的后续处理动作,并不是直接调用对应的处理动作。(相当于后续在进行运行中,发送了signum信号,该信号才会真正实现修改后的相关动作。若后续当前进程没有接收到信号,则handler方法中实现的信号操作函数不会被调用)
  
  对2:特定信号的处理动作一般只有一个,当选择自定义捕捉后,无法执行原先设置的默认动作/忽略(三选一)。此时即使用kill指令(kill -信号 进程id),获取到的信号操作也是修改后的结果。
【ONE·Linux || 信号】_第3张图片
  
  
  以下为多个信号执行相同的处理动作的演示:
【ONE·Linux || 信号】_第4张图片

  
  man 7 signal查看信号默认操作。对于2、3信号,其都是终止进程,这里Term、Core的区别是什么?这就需要谈到核心转储。

       Signal     Value     Action   Comment
       ──────────────────────────────────────────────────────────────────────
       SIGHUP        1       Term    Hangup detected on controlling terminal
                                     or death of controlling process
       SIGINT        2       Term    Interrupt from keyboard

       SIGQUIT       3       Core    Quit from keyboard

  
  
  
  

2.1.2.2、core dump

  1)、基础说明
  在进程等待章节,我们曾谈过status的低16个比特位含义。其中,进程被信号所杀时,最低7为表示进程接收到的信号,第8位为core dump,在进程等待中,这里表示该进程终止时,是否发生核心转储。
【ONE·Linux || 信号】_第5张图片
  核心转储:当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这就叫做Core Dump。

  上述信号2SIGINT和信号3SIGQUIT的区别就在于,2号信号的默认处理动作是终止进程,而3号信号终止进程的同时会生成核心存储文件。
  但根据上述演示,我们发现并没有所谓的core文件,这是因为一般对于云服务器而言,其核心存储功能是被关闭的
  
  
  因core文件中可能包含用户密码等敏感信息并不安全,在开发调试阶段,可以用ulimit命令改变这个限制:
  ulimit -a :用于查看当前服务器的各项配置
  ulimit -c 10240 :可打开当前云服务器的core dump,通常而言只在当前终端生效。(PS,这里10240是指允许生成的core文件大小最大为10240K)
【ONE·Linux || 信号】_第6张图片
  PS:一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中)。
  
  
  
  2)、相关演示一(应用场景举例)
  所生成的core文件主要用于调试。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。
【ONE·Linux || 信号】_第7张图片
  
  相比于逐行调试,使用core文件,可以相对方便的帮助我们找出错误原因。
  step1:gdb XXX 打开生成的可执行程序
  step2:core-file core.XXX 打开生成的核心数据文件,gdb可查看到相关错误原因

[wj@VM-4-3-centos signal]$ gdb #打开gdb
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later //gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
//www.gnu.org/software/gdb/bugs/>.

(gdb) core-file core.11146   #调试:打开core文件,可查看原因。此处主要是我们使用ctrl+\终止了进程。
[New LWP 11146]
Missing separate debuginfo for the main executable file
Try: yum --enablerepo='*debug*' install /usr/lib/debug/.build-id/db/c2fcd6a93ca927c05259d60af199084d385166
Core was generated by `./signal.out'.
Program terminated with signal 3, Quit.
#0  0x00007f313d7549e0 in ?? ()
(gdb) 

  
  
  
  3)、相关演示二:验证进程等待中的core dump标记位
  相关代码:

int main()
{
    pid_t id = fork();
    if(id == 0)
    {   //子进程
        int a = 100;
        a /= 0;//除数不能为0,此处会报错,信号8:【SIGFPE | Core | Floating point exception】
        exit(0);
    }

    //父进程:进程等待
    int status = 0;
    waitpid(id, &status, 0);//阻塞式等待
    cout <<"父进程:" << getpid() << " ,子进程:" << id << " ,退出信号:" << (status & 0X7F) << ", is core dump:" << ((status >> 7) & 1) << endl;

    return 0;
}

  
  
  演示结果如下:假如使用ulimit命令关闭了核心转储的功能,则此处即使使用core相关信号,is core dump仍旧为0。就如1)中所说,statusd的第8位core dump表示在进程终止时,是否发生核心转储(根据实际真实情况,而非根据信号原先设置)。

【ONE·Linux || 信号】_第8张图片

  
  
  
  
  
  
  
  
  

2.1.3、方式二:调用系统函数向进程发信号

  总结理解: ①用户调用系统接口,执行OS提供的系统调用代码。②OS提取参数或设置特定数值,向目标进程中写入信号,修改进程中的信号标记位。③进程接收到信号,后续会处理该信号,即执行信号对应的动作。
  
  

2.1.3.1、kill

  man 2 kill :可查看相关使用。实际上kill命令是调用kill函数实现的,kill函数可以给一个指定的进程发送指定的信号。

NAME
       kill - send signal to a process

SYNOPSIS
       #include 
       #include 

       int kill(pid_t pid, int sig);
      
RETURN VALUE
       On success (at least one signal was sent), zero is returned.  On error, -1 is returned, and errno is set appropriately.

  
  
  演示代码如下:

//int kill(pid_t pid, int sig);
//演示内容:在命令行中,./mykill 9 pid,终止进程
static void Usage(string proc)
{
    cout << "Usage:\r\n\t" << proc << " signumber processid" << endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3)//检测命令行指令
    {
        Usage(argv[0]);
        exit(1);
    }

    int signumber = atoi(argv[1]);
    int procid = atoi(argv[2]);
    kill(procid, signumber);

    return 0;
}

  演示结果如下:
【ONE·Linux || 信号】_第9张图片

  
  
  
  
  
  

2.1.3.2、raise

   man 3 raise:raise函数可以给当前进程发送指定的信号,即自己给自己发信号。

NAME
       raise - send a signal to the caller

SYNOPSIS
       #include 

       int raise(int sig);

DESCRIPTION
       The  raise()  function sends a signal to the calling process or thread.  In a sin‐
       gle-threaded program it is equivalent to

           kill(getpid(), sig);

  
  
  使用演示:

int main( )
{
    cout << "当前进程正在运行,pid: " << getpid() << endl;
    sleep(2);
    raise(8);
    return 0;
}

【ONE·Linux || 信号】_第10张图片

  
  
  
  

2.1.3.3、abort

  man 3 abort:abort函数使当前进程接收到信号而异常终止。就像exit函数一样,abort函数总是会成功的,所以没有返回值。

NAME
       abort - cause abnormal process termination

SYNOPSIS
       #include 

       void abort(void);

DESCRIPTION
       The abort() first unblocks the SIGABRT signal, and then raises that signal for the
       calling process.  This results in the abnormal termination of the  process  unless
       the  SIGABRT  signal  is  caught  and  the  signal  handler  does  not return (see
       longjmp(3)).

       If the abort() function causes process termination, all open  streams  are  closed
       and flushed.

       If the SIGABRT signal is ignored, or caught by a handler that returns, the abort()
       function will still terminate the process.  It does this by restoring the  default
       disposition for SIGABRT and then raising the signal for a second time.

RETURN VALUE
       The abort() function never returns.

  
  
  使用演示:实际上abort()有时也和exit()一样用于终止进程。

int main( )
{
    cout << "当前进程正在运行,pid: " << getpid() << endl;
    sleep(2);
    abort();//等同于rasie(6)、kill(getpid(),6)
    return 0;
}

【ONE·Linux || 信号】_第11张图片

  
  
  
  
  
  

2.1.4、方式三:由软件条件产生信号

  总结理解: ①OS识别到某种软件条件被触发或不满足,②OS构建信号,将其发送给指定的进程,③进程接收到信号,后续会对信号做出处理。
  

2.1.4.1、SIGPIPE

  
  
  
  
  
  
  

2.1.4.2、alarm

  1)、基础说明
  man alarm:调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒(参数)之后,给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。

NAME
       alarm - set an alarm clock for delivery of a signal

SYNOPSIS
       #include 

       unsigned int alarm(unsigned int seconds);

DESCRIPTION
       alarm()  arranges  for  a SIGALRM signal to be delivered to the calling process in
       seconds seconds.

       If seconds is zero, any pending alarm is canceled.

       In any event any previously set alarm() is canceled.

RETURN VALUE
       alarm() returns the number of seconds remaining  until  any  previously  scheduled
       alarm was due to be delivered, or zero if there was no previously scheduled alarm.

  SIGALRM信号为14,其默认action是Term,即终止进程。

       Signal     Value     Action   Comment
       ──────────────────────────────────────────────────────────────────────
       SIGALRM      14       Term    Timer signal from alarm(2)

  
  
  
  2)、相关演示一:验证1s内程序能进行多少次累加
  
  代码如下:

int main()
{
    alarm(1);//设定闹钟:一秒后发送信号14,终止进程

    int count = 0;
    while(true)
    {
        cout << " count:" << count++ << endl;
    }

    return 0;
}

【ONE·Linux || 信号】_第12张图片

  
  问题:观察上述多次运行,我们发现count值的位数都在万级别,以CPU运行速度,不可能只到万位,这是为什么呢?
  回答:上述代码执行过程涉及IO,如cout数据输出、网络发送(本地程序和远端云服务器),这些都严重拖垮累算速度。
  
  
  
  3)、相关演示二:基于上述演示,若想要单纯计算CPU算力(无IO干扰),如何操作?

  代码如下:

uint64_t count = 0;//为了防止int大小不够

void catchSig(int signum)//typedef void (*sighandler_t)(int);
{
    cout<<"进程捕捉到信号:" << signum << " ,pid:" << getpid() << ", count:" << count <<endl;
}

int main()
{
    signal(SIGALRM, catchSig);//将SIGALRM的处理动作自定义捕捉。

    alarm(1);//设定闹钟:一秒后发送信号14,SIGALRM

    while(true) count++; 

    return 0;
}

  演示结果:
【ONE·Linux || 信号】_第13张图片

  
  
  需要注意的是,alarm设定的闹钟一旦触发,就会自动移除。若想周期性执行,只需要在catchSig中重复设置alarm即可。【定时器功能】。

uint64_t count = 0;//为了防止int大小不够

void catchSig(int signum)//typedef void (*sighandler_t)(int);
{
    cout<<"进程捕捉到信号:" << signum << " ,pid:" << getpid() << ", count:" << count <<endl;
    alarm(1);
}

int main()
{
    signal(SIGALRM, catchSig);//将SIGALRM的处理动作自定义捕捉。

    alarm(1);//设定闹钟:一秒后发送信号14,SIGALRM

    while(true) count++; 

    return 0;
}

  解释:main函数中alarm设置,1秒后触发闹钟并被catchSig捕获,catchSig内设置有alram闹钟,再1s后闹钟发送SIGALRM信号又被catchSig捕获,如此反复循环。

[wj@VM-4-3-centos signal]$ ls
makefile  signal.cc  signal.out
[wj@VM-4-3-centos signal]$ ./signal.out //上述设置后结果如下,每隔1s,catchSig就会自动捕获一次SIGALRM信号。
进程捕捉到信号:14 ,pid:7693, count:505841653
进程捕捉到信号:14 ,pid:7693, count:1011844295
进程捕捉到信号:14 ,pid:7693, count:1517480886
进程捕捉到信号:14 ,pid:7693, count:2024547213
进程捕捉到信号:14 ,pid:7693, count:2530452059
进程捕捉到信号:14 ,pid:7693, count:3036268502
进程捕捉到信号:14 ,pid:7693, count:3542241957
进程捕捉到信号:14 ,pid:7693, count:4045296230
^C //CTRL+C
[wj@VM-4-3-centos signal]$ 

  
  
  
  
  

2.1.5、方式四:硬件异常产生信号

   硬件异常被硬件以某种方式检测到并通知内核,然后内核向当前进程发送适当的信号。
  
  

2.1.5.1、演示一:除0
void catchSig(int signum)
{
    sleep(1);
    cout << "捕获到一个信号:" << signum << " ,当前进程为:" << getpid() << endl;
}

int main()
{
    signal(SIGFPE, catchSig);
    
    int a = 5;
    a /= 0; //error

    while(true) sleep(1);

    return 0;
}

  演示结果如下:
【ONE·Linux || 信号】_第14张图片

  问题一: 如上述,除数为0时会生成8号信号SIGFPE,如何理解这里的/0错误,为什么说/0是硬件异常导致的信号发送?
  
  回答: 在计算机中进行计算的是CPU,属于硬件设备。CPU内部具有寄存器,除了供给我们使用的通用寄存器(EA、EC、EB、EX等)外,还有其它寄存器,如状态寄存器。

  状态寄存器又名条件码寄存器,它是计算机系统的核心部件——运算器的一部分。
  状态寄存器用来存放两类信息:
    一类是体现当前指令执行结果的各种状态信息(条件码),如有无进位(CF位)、有无溢出(OV位)、结果正负(SF位)、结果是否为零(ZF位)、奇偶标志位(P位)等;
    另一类是存放控制信息(PSW:程序状态字寄存器),如允许中断(IF位)、跟踪标志(TF位)等。有些机器中将PSW称为标志寄存器FR(Flag Register)。

  /0会导致溢出,OS在计算完毕后进行检测,当发现状态寄存器中溢出标记位为1时,能够识别到有溢出问题。此时OS会找到当前运行的进程,提取其pid,并将信号发送给进程。进程在合适时候会根据接受到的信号做出动作处理。(也正因此,/0错误看似是我们的代码问题,实则是硬件异常。)
  
  
  
  问题二: 为什么上述程序运行后会出现死循环?
  回答: 根据上述代码,我们的while循环一直存在,进程一直在运行,寄存器中的异常一直没有被解决。(即使CPU调度,进程上下文被切换,该异常状态也会随之保存,并在下一次再被调度时随进程上下文回来,那么该异常状态又被OS识别到。)
  PS: 实际上,出现硬件异常时,一般默认行为是退出进程,即使不退出(比如,更改其默认行为,被自定义捕获),我们也做不了什么(无法将其状态标志位有1改为0)。进程终止会释放PCB结构,其上下文也被释放,所以该异常状态能被清除。

void catchSig(int signum)
{
    sleep(1);
    cout << "捕获到一个信号:" << signum << " ,当前进程为:" << getpid() << endl;
    exit(8);//测试:捕捉信号后让进程退出
}

int main()
{
    signal(SIGFPE, catchSig);
    
    int a = 5;
    a /= 0; //error

    while(true) sleep(1);

    return 0;
}

  echo $? :获取进程退出码。
【ONE·Linux || 信号】_第15张图片  
  
  
  
  

2.1.5.2、演示二:野指针、越界

   野指针、越界这类属于段错误,对应信号为SIGSEGV(11).

       Signal     Value     Action   Comment
       ──────────────────────────────────────────────────────────────────────
       SIGSEGV      11       Core    Invalid memory reference

  
  演示代码:

void catchSig(int signum)
{
    sleep(1);
    cout << "捕获到一个信号:" << signum << " ,当前进程为:" << getpid() << endl;
    exit(11);//测试:进程退出
}

int main()
{
    signal(SIGSEGV, catchSig);
    
    int *p = nullptr;
    *p = 2; //error

    while(true) sleep(1);

    return 0;
}

  演示结果:可以看到这里野指针问题发送的信号正是SIGSEGV,因此才会被自定义捕获。

【ONE·Linux || 信号】_第16张图片
  
  
  问题: 如何理解野指针和越界问题?
  回答: 无论是指针或者下标访问,它们都需要通过地址,找到对应的目标位置。而我们语言层面上的地址属于虚拟地址,实际中要将虚拟地址转换为物理地址,此时就需要涉及页表和MMU(Memory Manager Unit)。其中,MMU属于硬件结构(硬件设备里也会存在寄存器),当出现野指针、越界行为,属于非法地址,那么MMU在转化时就会报错,此时OS识别到,会将该错误转换为信号发送给对应进程。
  
  
  
  

2.2、信号保存

2.2.1、一些概念

  1)、相关说明
  信号递达(Delivery):实际执行信号的处理动作称为信号递达。(之前描述的三种常见信号处理方式(默认、捕获、忽略),在执行时都称为信号递达。)
  
  信号未决(Pending) :信号从产生到递达之间的状态,称为信号未决。(根据之前所学,进程接受到信号时并不总是立即处理,因此会产生一段空白期,这段收到信号,将信号保存下来但并未处理的时期,就称之为信号未决。)
  
  阻塞 (Block):进程可以选择阻塞某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
  阻塞和忽略的区别: 阻塞发生在递达之前,信号尚未被处理;忽略发生在阻塞之后,是进程对信号的一种处理方式。
  
  
  
  
  2)、信号在内核中的示意图表
【ONE·Linux || 信号】_第17张图片

  block:阻塞信号集。结构和pending一样是位图,区别在于位图的内容。表示对应信号是否被阻塞。
  pending:未决信号集。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
  handler:handler处理方法表。其中填入的都是函数指针,指向一个个信号的具体处理方法。

#define SIG_ERR	((__sighandler_t) -1)		/* Error return.  */
#define SIG_DFL	((__sighandler_t) 0)		/* Default action.  */
#define SIG_IGN	((__sighandler_t) 1)		/* Ignore signal.  */

  根据上图,对于handler表,当获取一个信号signal是,handler会进行匹配,(int)handler[signal] == 0,则执行SIG_DFL(Default action)默认动作(int)handler[signal] == 1,则执行SIG_IGN(Ignore signal)忽略操作。若都不符则执行自定义操作,handler[signal]()
  
  
  问题: 综合上述,信号被处理是什么样的处理过程?
  回答: pending→block→handler。
【ONE·Linux || 信号】_第18张图片

  
  
  
  3)、sigset_t类型
  sigset_t,信号集: 位图结构,属于操作系统提供的一个数据类型,对于每种信号用一个bit表示每个信号的“有效”或“无效”状态。和使用内置类型、自定义类型无区别,对于sigset_t,用户直接使用即可。
  例如, 未决和阻塞标志可以用该数据类型来存储,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
  阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
  
  
  注意事项:
  ①sigset类型变量,不允许用户自己进行位操作,OS会给我们提供对应的操作位图的方法。【对sigset本身做操作】
  ②OS提供的一些用于完成某些功能的系统接口,其参数有可能包含sigset_t定义的变量或对象【使用sigset来完成一些特定功能】
  
  
  
  
  
  

2.2.2、信号集操作相关函数及其使用

2.2.2.1、相关函数介绍

  1)、对sigset本身进行操作的函数

【ONE·Linux || 信号】_第19张图片

  1、 在使用sigset_ t类型的变量之前,,一定要调用sigemptysetsigfillset做初始化,使信号集处于确定的状态。

int sigemptyset(sigset_t *set);
//初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。

int sigfillset(sigset_t *set);
//初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。

  2、在对信号集初始化后,就可以在该信号集中加入或删除某个信号。这四个函数都是成功返回0,出错返回-1。

int sigaddset (sigset_t *set, int signo);//在set指向的信号集中添加某种有效信signo

int sigdelset(sigset_t *set, int signo);//在set指向的信号集中删除某种有效信signo

  3、sigismember是一个布尔函数,用于判断一个信号集(set)的有效信号中是否包含某种信号(signo)。若包含则返回1,不包含则返回0,出错返回-1。

int sigismember(const sigset_t *set, int signo); 

  
  
  
  2)、使用sigset来完成一些特定功能的函数
  1、int sigpending(sigset_t *set):获取当前调用进程的pending信号集,通过set参数传出(可以看到此处set类型即sigset_t)。调用成功则返回0,出错则返回-1。

【ONE·Linux || 信号】_第20张图片

  
  
  
  2、int sigprocmask(int how, const sigset_t *set, sigset_t *oldset):调用sigprocmask函数可以读取或更改进程的信号屏蔽字(阻塞信号集)。若成功则为0,若出错则为-1 。

【ONE·Linux || 信号】_第21张图片

  如果oldset是非空指针,则读取进程的当前信号屏蔽字通过oldset参数传出。
  如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。
  如果oldsetset都是非空指针,则先将原来的信号屏蔽字备份到oldset里,然后根据sethow参数更改信号屏蔽字。
  
  假设当前的信号屏蔽字为maskhow参数的可选值如下:

       SIG_BLOCK
              The set of blocked signals is the union of the current set and the set argument.
              //mask = mask | set. 参数set中包含有希望添加到信号屏蔽字的信号

       SIG_UNBLOCK
              The signals in set are removed from the current set of blocked signals.  
              It is permissible to attempt to unblock a signal which is not blocked.
              //mask = mask & ~set.参数set中包含有希望解除阻塞的信号

       SIG_SETMASK
              The set of blocked signals is set to the argument set.
              //mask = set.直接设置信号屏蔽字为set所指向的值
              

  
  
  
  

2.2.2.2、编码样例

  1)、使用演示一
  问题:捕获所有信号,将其处理动作都自定义后,是否能够生成一个不会被异常或杀掉的进程?
  相关操作如下:

void catchSig(int signum)
{
    cout << "当前捕捉到的信号是:" << signum << endl;
}

int main()
{
    for(int i = 1; i <= 31; ++i )
    {
        signal(i, catchSig);//捕获1~31信号,将其处理动作都设置为自定义行为
    }

    while(true) sleep(1);//死循环:不让进程结束,方便演示观察现象

    return 0;
}

  
  演示如下:
  说明: 9号信号SIGKILL属于管理员信号,无法被捕捉。(实际上信号设计者本身就考虑到该问题,故有此设置)
【ONE·Linux || 信号】_第22张图片

  
  
  
  2)、使用演示二
  问题:将2号信号阻塞,获取打印pending信号集,此时再发送2号信号,pending信号集中2号信号对应的比特位是否0→1?

  演示代码如下:

//对信号进程捕捉:为了方便观察,不让信号执行默认处理动作。
void handler(int signum)
{
    cout << "捕获信号:" << signum << " , pid:" << getpid() << endl;
}

//显示信号集
void Showpending(sigset_t &pending)
{
    //int sigismember(const sigset_t *set, int signum);
    for(int i = 1; i <= 31; ++i)
    {
        if(sigismember(&pending, i))
            cout << "1" ;
        else
            cout << "0" ;
    }
    cout << endl;
}

int main()
{
    //1、定义信号集对象
    sigset_t bset, obset;

    //2、初始化信号集
    //int sigemptyset(sigset_t *set);
    sigemptyset(&bset);
    sigemptyset(&obset);

    //3、添加要进行屏蔽(阻塞)的信号
    //int sigaddset(sigset_t *set, int signum);
    sigaddset(&bset,2);//此处也可以传递SIGINT

    //4、设置set到内核中对应的进程内部:上述3步骤只是在用户栈上进行处理,并未设置到OS内部,因此需要借助相关函数达成该功能。
    //int  sigprocmask(int  how,  const  sigset_t*set, sigset_t *oldset);
    int ret = sigprocmask(SIG_BLOCK, &bset, &obset);
    assert(ret == 0);
    (void)ret;
    cout << "成功阻塞信号,pid:"  << getpid() << endl;

    //5、重复打印当前进程的pending信号集:用以后续发送2号信号时观察变化
    sigset_t pending;
    sigemptyset(&pending);
    int count = 0;//用于后续信号恢复
    signal(2,handler);//捕获2号信号:
    //下述一旦解除了2号信号的阻塞状态,2号信号递达后,执行默认操作则进程终止,此处为了方便观察将其捕获

    while(true)
    {
        //5.1、获取当前进程的pending信号集
        //int sigpending(sigset_t *set);
        sigpending(&pending);
        
        //5.2、显示pending信号集
        Showpending(pending);
        sleep(1);
        
        //5.3、对2号信号展开恢复:演示0→1→0的过程
        if(count++ == 10)
        {
            cout << "解除对于2号信号的阻塞" << endl;
            int ret = sigprocmask(SIG_SETMASK, &obset, nullptr);
            assert(ret == 0 );
            (void)ret;
        }
    }

    return 0;
}

  
  演示结果如下:

[wj@VM-4-3-centos signal]$ ./signal.out 
成功阻塞信号,pid:5994
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
^C0100000000000000000000000000000  //使用CTRL+C:发送2号信号
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
解除对于2号信号的阻塞 //此处文字顺序与编写代码时cout打印顺序有关,不影响验证
捕获信号:2 , pid:5994
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
^\Quit

  
   说明:
  1、需要注意,虽然我们阻塞了2号信号,但若不对进程发送2号信号,则pending信号集中2号信号的标志位不会由0变为1。
  2、上述有对block阻塞信号集修改的函数,也有对handler处理方法表修改的函数,但似乎没有对pending未决信号集修改的相关函数?实际上,发送信号给进程的过程就是对pending做修改的过程,因此并不需要设置相关接口来直接设置pending。
  
  
  
  
  3)、使用演示三
  问题:阻塞所有信号,是否能够生成一个不会被异常或者用户杀掉的进程?(基于演示二的延伸)
   演示代码:

void Showpending(sigset_t pending)
{
    for(int sig = 1; sig <= 31; ++sig)
    {
        if(sigismember(&pending, sig))
            cout << "1" ;
        else cout << "0";
    }
    cout << endl;
}

void Blocksig(int signum)
{
    sigset_t obset;
    sigemptyset(&obset);
    sigaddset(&obset,signum);

    int n = sigprocmask(SIG_BLOCK, &obset, nullptr);
    assert(n == 0);
    (void)n;
}

int main()
{
    //依次屏蔽所有信号
    for(int signum = 1; signum <= 31; ++signum)
    {
        Blocksig(signum);
    }
    //重复打印当前进程的pending信号集
    sigset_t pending;
    cout << "当前进程正在运行,pid:" << getpid() << endl;
    while(true)
    {
        sigpending(&pending);
        Showpending(pending);
        sleep(1);
    }

    return 0;
}

  观察脚本:bash sentsignal.sh

#!/bin/bash

i=1
id=$(pidof signal.out)
while [ $i -le 31 ]
do
    if [ $i -eq 9 ];then
        let i++
        continue
    fi
    if [ $i -eq 19 ];then
        let i++
        continue
    fi
    kill -$i $id
    echo "kill -$i $id"
    let i++
    sleep 1
done

  
  
  演示结果如下:同演示一,总有信号能无法被阻塞,9号信号、19、20。
【ONE·Linux || 信号】_第23张图片
  
  
  
  
  
  

2.3、信号处理

2.3.1、原理说明

  问题引入:在上述内容介绍中,我们一直强调信号被接受后,进程会在合适的时候处理信号,那么,什么时候合适?如何处理(具体流程)?以下将进行简要说明。
  

2.3.1.1、什么时候处理信号?

  1)、简述
   信号相关的数据字段保存在进程的PCB内部,其属于内核范畴。只有处于内核状态,才能处理内核范畴内的数据。
  进程运行起来一般处于用户态,只有当进程因系统调用、缺陷、异常等缘故,才会从用户态进入内核态进行相关操作。而当进程从内核状态返回用户态时,就会对当前进程接受到的信号进行检测并处理
  
  
  
  
  2)、用户态VS内核态
  用户态:一个受诸多管控的状态。
  内核态:操作系统执行自己的代码的一个状态,拥有非常高的优先级。
【ONE·Linux || 信号】_第24张图片
  根据上图可知,内核也是在所有进程的地址空间上下文中跑的。根据处于用户态还是内核态来判断是否有权限执行OS相关代码。
  CPU中存在两套寄存器,一套供给用户使用,另一套CPU自用。其中,CR3寄存器有相应比特位可表示当前CPU的执行权限(内核/用户),而在调用系统接口时,这些接口内置了int 80指令,能够切换状态。
  
  
  

2.3.1.2、如何处理信号?

【ONE·Linux || 信号】_第25张图片

  
  
  
  
  

2.3.2、信号捕捉操作

  如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。

2.3.2.1、signal

  在信号产生的小节中,我们已经学习了解过该函数,此处不做过多演示。
  
  
  

2.3.2.2、sigaction

  1)、基本介绍

  man sigaction:可以读取和修改与指定信号相关联的处理动作。

NAME
       sigaction - examine and change a signal action

SYNOPSIS
       #include 

       int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);

DESCRIPTION
       The  sigaction()  system call is used to change the action taken by a process
       on receipt of a specific signal.  (See signal(7) for an overview of signals.)

       signum specifies the signal and can be any valid signal  except  SIGKILL  and
       SIGSTOP. //SIGKILL(9)、SIGSTOP(19),此二者不被捕获

       If  act  is non-NULL, the new action for signal signum is installed from act.
       If oldact is non-NULL, the previous action is saved in oldact.//这里的参数用法和sigprocmask中set\oldset同

       The sigaction structure is defined as something like://struct sigaction 实际是OS提供的类型,该结构体类型如下述:

           struct sigaction {
               void     (*sa_handler)(int); //处理方法:1、SIG_IGN忽略信号;2、SIG_DFL默认动作;3、自定义捕获
               void     (*sa_sigaction)(int, siginfo_t *, void *); //用于实时信号,这里不涉及到
               sigset_t   sa_mask; //可添加需要屏蔽的信号
               int        sa_flags; //段包含一些选项,若不需要则设为0
               void     (*sa_restorer)(void); //暂时不考虑
           };

RETURN VALUE //调用成功则返回0,出错则返回- 1。
       sigaction() returns 0 on success; on error, -1 is returned, and errno  is  set  to
       indicate the error.

  
   sigset_t sa_mask
  ①当某个信号正在被处理时,内核自动将当前信号加入进程的信号屏蔽字,直到处理完成,自动恢复原来的信号屏蔽字。这样就保证了在处理某个信号时,相同信号再次产生,那么它会被阻塞到当前处理结束为止。
  ②如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时,自动恢复原来的信号屏蔽字。

       sa_mask  specifies  a  mask of signals which should be blocked (i.e., added to the
       signal mask of the thread in which the signal handler is invoked) during execution
       of  the  signal handler.  In addition, the signal which triggered the handler will
       be blocked, unless the SA_NODEFER flag is used.

  
  
  
  
  2)、使用演示一
  基础演示:举例sigaction的用法。

void handler(int signum)
{
    cout << "捕获到一个信号:" << signum << endl;
}

int main()
{   
    cout << "getpid: " << getpid() << endl;
    //1、用户在栈上定义一个内核数据类型的对象
    struct sigaction act, oact;
    sigemptyset(&(act.sa_mask));
    act.sa_flags = 0;
    act.sa_handler = handler;//函数指针

    //2、将用户定义的对象设置进当前调用的进程PCB中
    sigaction(2, &act, &oact);

    //3、为方便观察,设置死循环
    while(true) sleep(1);

    return 0;
}

  演示结果如下:

[wj@VM-4-3-centos signal]$ ls
makefile  sentsignal.sh  signal.cc  signal.out
[wj@VM-4-3-centos signal]$ ./signal.out 
getpid: 8501
^C捕获到一个信号:2
^C捕获到一个信号:2
^C捕获到一个信号:2
^C捕获到一个信号:2
^C捕获到一个信号:2
^C捕获到一个信号:2
^C捕获到一个信号:2
^\Quit
[wj@VM-4-3-centos signal]$ 

  
  
  
  
  
  3)、使用演示二:解释为什么要有block
  (即上述介绍sigset_t sa_mask时所说的内容)
  问题:处理信号的时候,执行自定义动作,若在处理信号期间,又收到相同信号,OS如何处理?
  回答:OS在处理信号时,只能处理一个信号,再发相同信号或者其它被载入sa_mask的信号,都会被屏蔽(block)。
  
  对上述代码做一些处理:

void Showpending(sigset_t & pending)
{
    for(int i = 1; i <= 31; ++i)
    {
        if(sigismember(&pending, i)) 
            cout << 1;
        else cout << 0;
    }
    cout << endl;
}


void handler(int signum)
{
    cout << "捕获到一个信号:" << signum << endl;
    sigset_t pending;
    int count = 30;
    while(count--)
    {
        sigpending(&pending);
        Showpending(pending);
        sleep(1);
    }
}

int main()
{   
    cout << "getpid: " << getpid() << endl;
    //1、用户在栈上定义一个内核数据类型的对象
    struct sigaction act, oact;
    sigemptyset(&(act.sa_mask));
    act.sa_flags = 0;
    act.sa_handler = handler;//函数指针

    sigaddset(&(act.sa_mask), 1);//把1、3信号也同样屏蔽掉
    sigaddset(&(act.sa_mask), 3);


    //2、将用户定义的对象设置进当前调用的进程PCB中
    sigaction(2, &act, &oact);

    //3、为方便观察,设置死循环
    while(true) sleep(1);

    return 0;
}

【ONE·Linux || 信号】_第26张图片

  
  
  
  
  
  

3、其它

3.1、可重入函数

  说明:当一个函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这种现象称为重入。若重入引起错乱,则称这样的函数为不可重入函数,反之称为可重入(Reentrant) 函数。
  
  
  以下是判断函数是否可重入的条件之一:
  调用了malloc或free:因为malloc也是用全局链表来管理堆的。
  调用了标准I/O库函数:标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
  
  
  

3.2、volatile关键字

  volatile 作用:保持内存的可见性。告知编译器,被该关键字修饰的变量不允许被优化,对该变量的任何操作都必须在真实的内存中进行操作。
  演示如下:
【ONE·Linux || 信号】_第27张图片
  出现上述情况,是因为 while 循环检查的flag并不是内存中最新的flag,这就存在了数据二异性的问题。,while循环中并未对flag修改使用,那么因优化处理会在编译阶段就把flag存放入CPU寄存器当中。
  
  相关代码:

int flag = 0;//全局变量

void handler(int sig)
{
    (void)sig;
    cout << "flag: " << flag << " →" ;
    flag = 1;
    cout << flag << endl;
}

int main()
{
    signal(2,handler);
    cout << "进程运行,捕获信号成功。pid: " << getpid() << endl;
    while(!flag);
    cout << "进程正常退出,pid:" << getpid() << " , flag: " << flag << endl;
    return 0;
}
signal.out:signal.cc
	g++ -o $@ $^ -std=c++11  -O3

.PHONY:clean
clean:
	rm -rf *.out

  
  
  
  
  

3.3、SIGCHLD信号

  说明:子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略。
  
  1)、验证子进程退出,父进程能接收到信号SIGCHLD

【ONE·Linux || 信号】_第28张图片

void handler(int signum)
{
    cout << "捕获到一个信号:" << signum << " ,father:"<< getpid() <<endl;
}

int main()
{
    signal(SIGCHLD ,handler);
    if( fork() == 0)//子进程
    {
        cout << "child pid: " << getpid() << endl;
        sleep(1);//休眠1s就退出
        exit(1);
    }
    while(true)//父进程
    {
        sleep(1);
    }
    return 0;
}

  
  
  
  
  2)、演示:父进程自定义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印
  

void handler(int sig)
{
    pid_t id;
    while ((id = waitpid(-1, NULL, WNOHANG)) > 0)//等待任意一个退出的子进程
    {
        printf("wait child success: %d\n", id);
    }
    printf("child is quit! %d\n", getpid());
}

int main()
{
    signal(SIGCHLD, handler);
    pid_t cid;
    if ((cid = fork()) == 0)
    { // child
        printf("child : %d\n", getpid());
        sleep(3);
        exit(1);
    }
    while (1)
    {
        printf("father proc is doing some thing!\n");
        sleep(1);
    }
    return 0;
}

  
  
  
  
  3)、演示用户层对SIGCHLD处理动作设置为忽略
  说明:父进程捕获SIGCHLD信号,将其处理动作置为SIG_IGN。这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。
  
【ONE·Linux || 信号】_第29张图片

int main()
{
    signal(SIGCHLD, SIG_IGN);

    pid_t cid;
    if ((cid = fork()) == 0)//子进程
    { 
        printf("child : %d\n", getpid());
        sleep(3);
        exit(1);
    }

    while (true)
    {
        printf("father proc:%d,  doing another thing!\n", getpid());
        sleep(1);
    }
    return 0;
}

  PS:系统默认的忽略动作和用户捕获自定义的忽略,通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
  
  
  
  
  
  
  

你可能感兴趣的:(#,【ONE·,Linux】,linux)