【Linux08-进程信号】信号的一生……

今天,带来Linux下进程信号的讲解。文中不足错漏之处望请斧正!


是什么

生活中的信号

例子:

  • 红绿灯
  • 来电铃声
  • 老妈倒数321叫我起床
  • 外卖小哥叫我下楼拿外卖
    理解:
  1. 过程:收到信号 → 分析信号 → 产生信号对应的行为
  2. 信号不一定会被立即处理(女生偷看我:我看到了她偷看我,但是我在专心学习,就不理她,二者是异步的),也就是**会有时间窗口,而且需要记录信号(**外卖小哥叫我下楼拿外卖:我在忙,就记着,等会再拿)
  3. 处理信号的方式:
  • 默认
  • 自定义
  • 忽略

比如来电铃声:

  • 默认:接听
  • 自定义:原地转圈跳舞
  • 忽略:不接听

同步 和 异步

顺便再带一嘴:
老师上着课,我去上厕所

  1. 老师不等我:我上厕所和老师上课是异步的
  2. 老师等我:我上厕所和老师上课是同步

我们也能感受到,信号是某种标识,如外卖小哥的电话,就标识着我外卖到了。

Linux中的信号

看看Linux下支持的信号:

[bacon@VM-12-5-centos linux]$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-*斜体样式*13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

以上显示的是 信号编号:信号描述,其实就是根据编号进行宏替换。类似于下面:

#define 1 SIGHUP

其中,

[1, 31]:普通信号

[34, 64]:实时信号(不过多了解)
SIGCHLD: 父进程退出时会给子进程发送的信号
9号信号无法被捕捉, 是保险措施, 以防无法终止异常进程.

进程信号

是什么

进程中的信号是OS给进程发送的一种标识(如标识某种事件发生)。

  1. 进程识别信号:收到信号 → 分析信号 → 产生对应动作
  2. 进程不一定会立即处理信号
  3. 进程需要保存信号
  4. 处理信号有三种动作
    1. 默认动作
    2. 自定义动作
    3. 忽略动作

*处理信号也叫 信号被捕捉

保存在哪里

进程的task_struct里。

想想刚刚用kill看到的信号,[1, 31],共32个信号……聪明的你一定想到了,可以用32位整数保存,某位的0/1代表是否收到此信号,其实就是用位图结构保存。

如第一个比特位若为1,就代表收到了“1) SIGHUP ”,反之没收到。

发送信号的本质

发送信号 = 修改PCB中的信号位图。

为什么只有OS能改?

task_struct 是OS维护的内核数据结构,也只有OS能改——任何发送信号的方式都必须通过OS发送

那么,OS也应该提供修改PCB中信号位图的系统调用。老样子,我们就可以学底层系统调用而知上层封装的接口。


为什么

信号表明某个事件发生, 方便我们追溯错误.

那又为什么需要这么多不同的信号? 不同信号可以准确表示不同事件.


信号的产生

键盘热键发送信号

回头看看,我们学过热键ctrl+c 来终止前台进程,是怎么做到的?

键盘是硬件,要通过OS,OS就会把ctrl+c 解释成2号信号2) SIGINT

我们来man 7 signal ,查看一下2号信号对应的默认动作:

Signal     Value     Action   Comment
──────────────────────────────────────────────────────────────────────
SIGINT        2       Term    Interrupt from keyboard

翻译过来就是通过键盘终止进程。

那除了默认动作,我们提到的自定义动作怎么玩?

自定义动作

sighandler_t signal(int signum, sighandler_t handler);

*typedef void (*sighandler_t)(int);

signal函数用到了回调函数: 接收到某个信号的时候,调用handler 函数

用用吧!

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

using namespace std;

void handler(int signo)
{
    printf("进程%d捕捉到了一个信号,信号编号为%d\n", getpid(), signo);
}

int main()
{
    //捕捉到2号信号后调用handler
    //仅仅是设置了对2号信号的捕捉方法
    //可以kill -2 pid
    //也可以ctrl+c
    signal(2, handler);
    while(1)
    {
        printf("我是一个进程 |pid=%d|\n", getpid());
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

为什么ctrl+c 不能终止进程?因为2号信号被设置了自定义动作,就没有默认动作的事了

除了ctrl+cctrl+\也能终止进程:

[bacon@VM-12-5-centos 8-signal]$ ./signal 
我是一个进程 |pid=24809|
我是一个进程 |pid=24809|
我是一个进程 |pid=24809|
我是一个进程 |pid=24809|
^\Quit

ctrl+\对应的是3号信号3) SIGQUIT

还有一点可以提提:Linux下,默认只允许有一个前台进程。

平常我们用shell和系统交互的时候,shell就是唯一的前台进程;当我们运行别的程序,shell就被切到后台。

这也是为什么我们运行别的程序时,输入指令没用:

[bacon@VM-12-5-centos 8-signal]$ ./signal 
我是一个进程 |pid=25594|
我是一个进程 |pid=25594|
pwd
我是一个进程 |pid=25594|
ls
cd我是一个进程 |pid=25594|

到这我们就知道了第一种产生信号的方式:键盘热键

系统调用发送信号

我们提过,系统肯定要提供给进程发信号的系统调用,这就是我们第二种方式。

int kill(pid_t pid, int sig);

  • 作用
    • 向进程发送信号
  • 参数(显而易见)
  • 返回值
    • 发送成功返回0
    • 发送失败返回-1,错误码被设置

sender.cc

#include <iostream>
#include <string>
#include <cstdio>
#include <cassert>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

using namespace std;

void Usage(const string& procName)
{
    printf("%s: pid/signo not found\n");
    printf("\t-pid  -signo\n");
}

//使用方法:命令行参数额外传递pid和信号编号
//argv[0] = ./sender
//argv[1] = pid
//argv[2] = signo
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(-1);
    }

    pid_t pid = atoi(argv[1]);
    int signo = atoi(argv[2]);
    int killRet = kill(pid, signo);
    if(killRet == -1)
    {
        perror("kill:");
    }

    return 0;
}

testproc.cc

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

using namespace std;

int main()
{
    while(1)
    {
        printf("我是一个进程 |pid=%d|\n", getpid());
        sleep(1);
    }
    return 0;
}

测试效果:

在这里插入图片描述

再看些库函数:

int raise(int sig);

  • 作用
    • 发送一个信号给调用者
int main()
{
    int cnt = 0;
    while(1)
    {
        if(cnt == 3)
        {
            cout << getpid() << "给自己发2号信号..." << endl;
            raise(2);
        }

        printf("我是一个进程 |pid=%d|\n", getpid());
        sleep(1);
        ++cnt;
    }
    return 0;
}
[bacon@VM-12-5-centos 8-signal]$ ./testproc 
我是一个进程 |pid=8836|
我是一个进程 |pid=8836|
我是一个进程 |pid=8836|
8836给自己发2号信号...

用系统调用kill模拟:kill(getpid(), 2(SIGINT);

void abort(void);

  • 作用
    • 引起进程结束
int main()
{
    int cnt = 0;
    while(1)
    {
        if(cnt == 3)
        {
            cout << getpid() << "调用abort()..." << endl;
            abort();
        }
        printf("我是一个进程 |pid=%d|\n", getpid());
        sleep(1);
        ++cnt;
    }
    return 0;
}
[bacon@VM-12-5-centos 8-signal]$ ./testproc 
我是一个进程 |pid=10185|
我是一个进程 |pid=10185|
我是一个进程 |pid=10185|
10185调用abort()...
Aborted

用系统调用kill模拟:kill(getpid(), 6);6) SIGABRT)。

库函数基于系统调用kill,系统调用kill基于操作系统本身。

硬件异常产生信号

int main()
{
    while(1)
    {
        printf("我是一个进程,准备除0...\n");
        sleep(1);
        int a = 0;
        a /= 0;
    }
    return 0;
}
[bacon@VM-12-5-centos 8-signal]$ ./testproc 
我是一个进程,准备除0...
Floating point exception

为什么不能除0

乘法的含义是累加。如3 * 5 = 3 + 3 + 3 + 3 + 3。

相反地,除法的含义是累减。如 15 / 5 的含义是能从15中减多少个5,15 - 5 - 5 - 5 = 0,总共3个5。那么x/0的含义就是能从x中减多少个0,x - 0 - 0 - …

在现实中,这是没有意义;在计算机中,这是让硬件进行不合理的死循环运算,足以算得上异常。

因此OS会给/0的进程发送8) SIGFPE,直接终止掉。

void catchSig(int signo)
{
    printf("获取到一个信号,其编号为: %d\n", signo);
}

int main()
{
    signal(SIGFPE, catchSig);
    while(1)
    {
        printf("我是一个进程,准备除0...\n");
        sleep(1);
        int a = 0;
        a /= 0;
    }
    return 0;
}
[bacon@VM-12-5-centos 8-signal]$ ./testproc 
我是一个进程,准备除0...
获取到一个信号,其编号为: 8
获取到一个信号,其编号为: 8
获取到一个信号,其编号为: 8
获取到一个信号,其编号为: 8
//...

现象有点问题:OS在疯狂发信号。不过我们先按下不表。

OS怎么知道我除0?

操作系统怎么知道我除0了?

因为除0会触发CPU异常, CPU会通知OS

在这里插入图片描述

  1. 除0
  2. 产生错误结果
  3. 状态寄存器表示溢出的标记位置1
  4. CPU告知OS出现运算异常
  5. OS检查状态寄存器
  6. OS给CPU当前调度的进程发对应的信号(CPU当前调度的进程就是出错的进程)

OS怎么知道我除0了?因为除0CPU会异常,而OS会被CPU通知。

解释现象: 为什么OS会疯狂发信号

我们现在再谈之前”OS疯狂发信号“的问题,它怎么就疯狂地发了呢?

进程收到信号后是可以不终止的(刚刚的程序捕捉到某信号后就没退出)。当异常进程不终止时,每次CPU切换到异常进程,都会

  1. 把异常进程的上下文载入寄存器
  2. 把状态寄存器溢出标记位置1
  3. 告诉OS自己异常了

因此,才有了“疯狂发信号”的现象。

为什么不能解引用空指针 (MMU)

都讲到这了, 顺便说说, 我们为什么无法真正执行解引用空指针的操作.

为什么不能解引用空指针?

[bacon@VM-12-5-centos 8-signal]$ ./testproc 
我是一个进程,准备解引用空指针...
Segmentation fault

这就是11) SIGSEGV

而我们需要对虚拟地址与物理地址的映射有新一层理解才能解答这个问题。

MMU,内存管理单元,是一种集成在CPU上的硬件。可以为页表的虚拟地址和某个物理地址建立映射。

所以建立映射是这样:

在这里插入图片描述

解引用空指针是这样:
在这里插入图片描述

越界访问MMU肯定不答应啦!不仅拒绝,它还觉得你这种操作不正常,大概率程序出错了,所以会告诉OS这个进程闹事儿,让它治你。于是OS就发送了11号信号。

满足软件条件发送信号

典型的场景就是管道了。当管道的读端关闭,写端也会关闭,因为接着写没有意义且浪费。

把读端关闭是一种软件行为,而读端是否关闭是写端是否关闭的条件,这种软件行为使得写端继续写的软件条件不满足,OS就向写端发送信号来终止写端进程。

接下来谈谈定关于闹钟的软件条件。

需要用到一个系统调用。

unsigned int alarm(unsigned int seconds);

  • 作用
    • 在seconds(n秒)后向调用方发送14) SIGALRM(默认动作是终止进程)
  • 注意
    • alarm(0)表示取消闹钟
      • 若之前未设置闹钟返回值为0
      • 若之前设置了闹钟返回值为剩余秒数
int main()
{
    cout << "设置定时器:1秒后发送14号信号..." << endl;
    alarm(1);
    int cnt = 0;
    while(true)
    {
        cout << "count: " << cnt++ << endl;
    }
    return 0;
}
[bacon@VM-12-5-centos 8-signal]$ ./sigtest
设置定时器:1秒后发送14号信号...
count: 0
count: 1
...
count: 192449
count: 192450
Alarm clock

设置定时器(秒后发送14号信号) → 3秒 → 满足软件条件,发送信号。

这是加了打印后一秒内计算机能累加多少次,我们试试不加打印。

int cnt = 0;

void catchSig(int xxx)
{
    cout << "cnt = " << cnt << endl;
}

int main()
{
    signal(SIGALRM, catchSig);
    alarm(1);
    while(true)
    {
        ++cnt;
    }
    return 0;
}
[bacon@VM-12-5-centos 8-signal]$ ./sigtest 
cnt = 567799259

//没有直接退出,OS却没有“疯狂发信号了”?

看看这差距,虽然我们用云服务器跑的,IO还要经过一层网络,但这也很能说明IO很慢。

诶,没有直接退出,OS却没有“疯狂发信号了”?

这说明这个定时器(闹钟)只响一次。

怎么理解闹钟作为一种软件条件?

我们从“闹钟的管理入手”。每个进程都能设置自己的闹钟,多了以后OS肯定要管理,怎么管理呢?

在这里插入图片描述

OS会获取当前时间戳,跟myalarm1.when对比,如果前者更大,说明超时了。

闹钟的组织也可以用堆结构,让最近一个会超时的闹钟保持在堆顶。

闹钟.when就是一种软件条件。

补充

核心转储

预备: Term 和 Core

还有个问题:我们man 7 signal能看到信号对应的行为,但是这个Action列中的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
       SIGILL        4       Core    Illegal Instruction
       SIGABRT       6       Core    Abort signal from abort(3)
       SIGFPE        8       Core    Floating point exception
       SIGKILL       9       Term    Kill signal
       SIGSEGV      11       Core    Invalid memory reference
       SIGPIPE      13       Term    Broken pipe: write to pipe with no
  • Action为Term的进程终止,是正常终止,OS不做额外动作。
  • Action为Core的进程终止,是异常终止,OS需要做额外动作。

说说Core,以11号信号为例,我们看看解引用空指针的现象:

[bacon@VM-12-5-centos 8-signal]$ ./sigtest
我是一个进程,准备解引用空指针...
Segmentation fault

但Core终止产生的额外动作在云服务器上看不到明显的体现:

[bacon@VM-12-5-centos 8-signal]$ ulimit -a
core file size          (blocks, -c) 0
#...

因为云服务器上默认没有core file。

如果我们想看到明显的体现,可以设置一些core file

[bacon@VM-12-5-centos 8-signal]$ ulimit -c 1024
[bacon@VM-12-5-centos 8-signal]$ ulimit -a
core file size          (blocks, -c) 1024
[bacon@VM-12-5-centos 8-signal]$ ./sigtest 
我是一个进程,准备解引用空指针...
Segmentation fault (core dumped)
[bacon@VM-12-5-centos 8-signal]$ ll
total 288
-rw------- 1 bacon bacon 561152 Mar  1 10:14 core.8865
#...

core dumped是核心转储的意思。

支持事后调试。

是什么

核心转储:进程异常退出(Core)前,会把其在内存中的有效数据转储到磁盘上。

core.8865中,就是pid为8865的进程异常退出时,从内存中转储来的数据。

为什么

方便定位错误, 支持事后调试.

怎么用

gdb 中可以用 core-file 来加载

[bacon@VM-12-5-centos 8-signal]$ ./sigtest 
我是一个进程,准备解引用空指针...
Segmentation fault (core dumped)
[bacon@VM-12-5-centos 8-signal]$ ll
total 300
-rw------- 1 bacon bacon 561152 Mar  1 10:31 core.12165
-rw-rw-r-- 1 bacon bacon    163 Mar  1 10:23 makefile
-rwxrwxr-x 1 bacon bacon  13664 Mar  1 10:31 sender
-rw-rw-r-- 1 bacon bacon    673 Feb 28 18:13 sender.cc
-rwxrwxr-x 1 bacon bacon  24512 Mar  1 10:31 sigtest
-rw-rw-r-- 1 bacon bacon   2109 Mar  1 10:14 sigtest.cc
-rwxrwxr-x 1 bacon bacon   8984 Feb 28 21:14 testproc
[bacon@VM-12-5-centos 8-signal]$ gdb sigtest
#...
(gdb) core-file core.12165 
[New LWP 12165]
Core was generated by `./sigtest'.
Program terminated with signal 11, Segmentation fault.
#0  0x0000000000400705 in main () at sigtest.cc:113
113             *p = 10;
#...

如果进程是Term终止,就不会进行核心转储。

int main()
{
    while(1)
    {
        printf("我是一个进程 |pid=%d|\n", getpid());
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

进程正常终止,Action为Term,无需事后调试,OS不会进行核心转储。

进程异常终止,Action为Core,可能需要事后调试,OS会进行核心转储。

信号相关结论

  1. 所有信号都是由OS生成的(键盘热键, 系统调用, 硬件异常, 软件条件)
  2. 从接收信号到信号产生动作时有时间窗口的 (合适的时候才产生动作)
  3. 信号保存在 task_struct 内 (时间窗口要求我们能保存信号)
  4. 由信号产生的动作有三种 (默认动作, 自定义动作, 忽略动作)
  5. OS向进程发送信号的本质就是修改进程PCB中的信号字段

信号的状态 (概念补充)

之前我们的说法都是图方便理解,现在来列出以上各种过程、操作的庐山真面目。

  1. 信号发送后, 进程可以**阻塞(Block)**某个信号
    1. 被阻塞的信号会一直处于未决状态,直到进程解除对此信号的阻塞,才能递达
    2. *阻塞和忽略不同,前者决定信号是否抵达,后者是信号递达的一种
  2. 信号递达(Delivery) : 信号发送后, 终于到了进程手中, 进程可以开始处理信号了
  3. 信号未决(Pending): 从信号发送到信号递达中间的状态

举个例子:

老师课间布置作业,我记下来,晚上回家了才写作业。

这就是没被阻塞的信号的一生:信号产生 → 信号发送 → 信号递达 → 信号处理

  1. 老师心里知道要布置什么作业 = 信号产生
  2. 老师把作业写到黑板上 = 信号发送
  3. 我看到了黑板上的作业, 可以开始写了 = 信号递达
  4. 写作业 = 信号处理

我很讨厌的一个老师课间布置作业,我不情愿地记下来,晚上回家了,不想写他的作业,将来实在不行了再写。

这就是被阻塞的信号的一生:信号产生 → 信号发送 → 信号被阻塞 → … → 解除对信号的阻塞 → 信号递达 → 信号处理

  1. 老师心里知道要布置什么作业 = 信号产生
  2. 老师把作业写到黑板上 = 信号发送
  3. 讨厌他,不想写他的作业 = 阻塞信号
  4. 突然发现这个老师很好,我消除了对他的偏见 = 解除对信号的阻塞
  5. 写作业 = 信号递达
    *从老师把作业写到黑板上到我写写作业的整个过程都是信号未决

信号的发送

信号发送, 就是向内核中信号相关的数据结构写入. 对信号发送的学习可以转化为对相关数据结构的学习. 也就是信号的保存.
和信号保存相关的数据结构主要是三个: pending位图、block位图和handler表.

在这里插入图片描述

  • 处于pending位图(pending表)中的信号就是pending状态
  • 处于block位图(block表)中的信号就是block状态
  • handler是函数指针表,保存信号对应的动作

所以,从这些数据结构中理解信号是这样的:

  1. 未决:收到x号信号,填入pending位图
  2. 阻塞:如果x号信号被阻塞,填入block位图,等到解除阻塞再处理
  3. 递达:调用handler表中x号信号的处理方法

我们可由此得出些结论:

  1. 进程通过handler表来确定信号的动作
  2. 尽管信号没产生,但它仍然可以被进程阻塞(pending表和block表相互独立)
  3. 由于保存用户信号的结构是位图,只能表示某信号是否出现,无法保存出现次数(若收到多个同一信号,且都没处理,只会保存一个,其他的会丢失)

也能进一步理解系统调用signal

在这里插入图片描述


信号的递达

递达时机

在从内核态返回用户态的时候 (后面才能理解).

#进程的运行级别: 用户态和内核态

如何理解

在这里插入图片描述

告知:

  • 不仅有用户级页表,还有内核级页表
    • 用户级页表每个进程独有,用户级空间每个进程独有
    • 内核级页表每个进程共享,内核级空间共享(通过共享的内核级页表和同一块物理内存建立映射)
  • 进程处于用户态和内核态时有何不同
    • 前者没有资格访问内核资源或硬件资源,后者有
    • 前者通过用户级页表访问用户空间,后者通过内核级页表访问内核空间
  • 用户态和内核态是可以切换的
    • 从用户空间到内核空间 = 用户态→内核态
    • 从内核空间到用户空间 = 内核态→用户态
内核态场景1: 系统调用

系统调用或者需要访问内核资源,或者需要访问硬件,都需要切换成内核态。

本质上只不过是从主执行流直接跳转到1G的内核空间执行OS的代码罢了

内核态场景2:进程切换

进程需要切换代表进程没执行完,那么就需要放到run_queue或者wait_queue内,也就是内核数据结构中,管理起来,这就涉及内核资源的访问。那自然需要切换成内核态。

为什么需要运行级别

是一种安全管理,道理类似权限,是OS对内核的保护。

如何管理运行级别

通过CPU上名为 CR3 的不可见寄存器来管理. 当其中的值为0 = 内核态, 3 = 用户态.

如何切换运行级别
  1. 用户修改CR3寄存器
  2. OS得知, 为进程切换运行级别
切换运行级别的开销

运行级别的切换消耗较大 (需要陷入内核) , 因此系统调用效率不是太高.

在这里插入图片描述

为什么要在运行级别切换时捕捉信号

进程运行级别的切换开销大, 如果切换时顺带做点别的事, 比如递达和处理信号, 就提高了整体效率

信号的处理

默认动作 & 忽略动作

详细:
在这里插入图片描述
简单:
在这里插入图片描述

自定义动作

详细:

在这里插入图片描述

简单:
在这里插入图片描述

为什么自定义动作和其他二者的递达处理过程不一样?
因为自定义动作不一定安全, 不能给进程内核态的运行级别来执行未知的代码.

总结: 信号的一生

  1. 信号产生
  2. 信号发送
  3. (信号阻塞)
  4. 信号递达
  5. 信号处理

在这里插入图片描述


怎么做 (信号操作)

sigset_t

我们看了内核中信号相关的数据结构后,发现每个信号都只有1个比特位来标识是否阻塞或未决,而不记录次数。既然阻塞和未决的表示都只有“有效”或“无效”,那就可以用一个sigset_t信号集类型来存储阻塞和未决状态。

sigset_t用1bit表示每种信号的“有效”或“无效”的状态。

  • 阻塞信号集:有效=阻塞,无效=非阻塞
  • 未决信号集(又称信号屏蔽字):有效=未决,无效=非未决

sigset_t主要是OS为了方便用户修改底层的pending位图和block位图。

操作信号屏蔽字

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

  • 作用:以how方式,将当前进程的阻塞信号集设置成set,并返回原来的阻塞信号集
  • 参数
    • how
      • SIG_BLOCK:按set阻塞 ~= mask |= set
      • SIG_UNBLOCK:按set取消阻塞 ~= mask = maks & (~set)
      • SIG_SETMASK:按set直接设置 ~= mask = set
    • set:设置当前进程信号屏蔽字的一份参考
    • oldset:原来的信号屏蔽字(输出型参数)

操作未决信号集

int sigpending(sigset_t *set);

  • 作用:获取当前进程的未决信号机到set
  • 参数
    • set:输出型参数

示例

[bacon@VM-12-5-centos 2]$ ./sigtest 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
^C0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0

再试试解除对信号的屏蔽:

int main()
{
    //1. 屏蔽2号信号
    sigset_t block, oblock, pending;
    //1.1 初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    //1.2 填入要屏蔽的信号
    sigaddset(&block, SIGNAL_TO_BLOCK);
    //1.3 把block设置进内核(block位图中)
    sigprocmask(SIG_SETMASK, &block, &oblock);

    //2. 打印pending信号集
    int cnt = 5;
    while(true)
    {
        sigpending(&pending);
        displayPending(pending);
        sleep(1);
        if(cnt-- == 0)
        {
            sigprocmask(SIG_SETMASK, &oblock, &block);
            cout << "解除对信号的屏蔽" << endl;
        }
    }

    return 0;
}
[bacon@VM-12-5-centos 2]$ ./sigtest 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
^C0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 

[bacon@VM-12-5-centos 2]$

诶,怎么不打印信息?因为这些接口都是系统调用,准备从内核态返回之前就会捕捉并处理信号(递达)。我们的2号信号默认终止进程,于是就直接结束了。可以验证一下:

void myhandler(int signo)
{
    printf("%d号信号已经递达!\n", signo);
}

//阻塞2号信号
int main()
{
    signal(SIGNAL_TO_BLOCK, myhandler);
    //1. 屏蔽2号信号
    sigset_t block, oblock, pending;
    //1.1 初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    //1.2 填入要屏蔽的信号
    sigaddset(&block, SIGNAL_TO_BLOCK);
    //1.3 把block设置进内核(block位图中)
    sigprocmask(SIG_SETMASK, &block, &oblock);

    //2. 打印pending信号集
    int cnt = 5;
    while(true)
    {
        sigpending(&pending);
        displayPending(pending);
        sleep(1);
        if(cnt-- == 0)
        {
            sigprocmask(SIG_SETMASK, &oblock, &block);
            cout << "解除对信号的屏蔽" << endl;
        }
    }

    return 0;
}
[bacon@VM-12-5-centos 2]$ ./sigtest 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
^C0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 
2号信号已经递达!
解除对信号的屏蔽
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

那如果我们想随意地屏蔽或接触屏蔽呢?

我们3个核心的信号数据结构都有了操作方法:

  • block位图:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • pending位图:int sigpending(sigset_t *set);
  • handler表:sighandler_t signal(int signum, sighandler_t handler);

捕捉信号

除了signal,还有一个。

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

  • 作用:把signum信号的自定义动作设为act,把原来的动作放进oldact

  • 参数

    • act
    					struct sigaction {
                   void     (*sa_handler)(int);
                   void     (*sa_sigaction)(int, siginfo_t *, void *);
                   sigset_t   sa_mask;
                   int        sa_flags;
                   void     (*sa_restorer)(void);
               };
    
    • 第一个sa_handler就是signal中设置的那个函数指针,是自定义动作
    • 第二个sa_mask,是信号集类型,后面谈
    • 其他我们不需要关心,有些是关于实时信号的
#include <iostream>
#include <csignal>
#include <unistd.h>
using namespace std;

void myhandler(int signo)
{
    cout << "got signal: " << signo << endl;
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = myhandler;
		act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(2, &act, &oact);

    while(1) sleep(1);

    return 0;
}
[bacon@VM-12-5-centos 3]$ ./sigtest 
^Cgot signal: 2
^Cgot signal: 2
^Cgot signal: 2

那如果信号正在被递达处理的期间,我持续给进程发同一个信号会怎么样?

#include <iostream>
#include <csignal>
#include <unistd.h>
using namespace std;

void count(int cnt)
{
    for(int i = cnt; i >= 0; --i) 
    {
        printf("cnt: %2d\r", i);
        fflush(stdout);
        sleep(1);
    }
    cout << endl;
}

void myhandler(int signo)
{
    cout << "got signal: " << signo << endl;
    cout << "正在处理中..." << endl;
    count(20);
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = myhandler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(2, &act, &oact);

    while(1) sleep(1);

    return 0;
}

在这里插入图片描述

可得结论:递达某信号期间,再收到此信号也是无法被递达的。

为什么?而且,为什么它能阻塞了,但还是又递达了一次?

告知:

  • 某个信号准备递达前,OS会把此信号从pending位图上移除(1变0),并把此信号阻塞
  • 某个信号递达完毕,OS会解除对此信号的阻塞
  • 某个信号被解除阻塞后,会自动尝试递达此信号

第一次收到信号,pending位图上某个位置0变1。

要递达此信号时,OS会把此信号从pending位图上移除(1变0),然后对此信号阻塞,最后才开始递达处理。

此时的2号信号在pending位图上的位置是0!再发新的2号信号给进程,虽然它被阻塞了,不会被递达,但是可以放在pending位图里!随后第一次收到的2号信号递达完毕,解除对2号信号的阻塞,此时就自动进行。

0→1→0→1→0

没收到信号→收到信号→OS递达信号前置0并阻塞2号信号→递达2号信号→收到新信号→2号信号递达完毕并解除对2号信号的阻塞→自动尝试递达2号信号→递达新的2号信号。

所以处理同类信号的原则是串行处理。

我们再看看参数sa_mask,这有啥用呢?当我们递达某个信号,可以同时对别的信号阻塞,这个mask就是阻塞信号集。

#include <iostream>
#include <csignal>
#include <unistd.h>
using namespace std;

void count(int cnt)
{
    for(int i = cnt; i >= 0; --i) 
    {
        printf("cnt: %-2d\r", i);
        fflush(stdout);
        sleep(1);
    }
    cout << endl;
}

void myhandler(int signo)
{
    cout << "got signal: " << signo << endl;
    cout << "正在处理中..." << endl;
    count(10);
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = myhandler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, 3);
    sigaction(2, &act, &oact);

    while(1) sleep(1);

    return 0;
}
[bacon@VM-12-5-centos 3]$ kill -2 10452
[bacon@VM-12-5-centos 3]$ kill -3 10452
[bacon@VM-12-5-centos 3]$ ./sigtest 
got signal: 2
正在处理中...
cnt: 0 
Quit

发送2号信号后发3号,虽然被阻塞,但是等2号信号递达完毕,也会自动对2、3号解除阻塞,并自动尝试递达。


其他

可重入函数

先看示例:
在这里插入图片描述
在这里插入图片描述
被 “重新进入” 后, 函数运行和其结果不受影响的函数就是可重入函数

可重入 vs 不可重入

  • 可重入: 被重入后, 函数运行和其结果不受影响
  • 不可重入: 被重入后, 函数运行和其结果受影响
    • 如果用了malloc和fre(用全局链表管理), 一般不可重入
    • 如果用了标准库中的I/O函数(很多实现都以不可重入的方式访问了全局的数据结构), 一般不可重入

volatile

是什么

确保变量每次被访问的方式都是通过内存读取.在这里插入图片描述

为什么

编译器可能会对程序优化, 对某些变量的访问通过寄存器或缓存访问, 且变量值实时从内存更新到寄存器或缓存, 因此变量值可能出现不一致.

怎么用

volatile int var = 10;

今天的分享就到这里了,感谢您能看到这里。

这里是培根的blog,期待与你共同进步!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周杰偷奶茶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值