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

news2025/1/12 20:01:02

今天,带来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号信号。

满足软件条件发送信号


补充

核心转储

预备: 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. 根据信号产生动作——信号递达(Delivery)
  2. 信号被收到,但未处理——信号未决(Pending)
  3. 进程可以**阻塞(Block)**某个信号
    1. 被阻塞的信号会一直处于未决状态,直到进程解除对此信号的阻塞,才能递达
    2. *阻塞和忽略不同,前者决定信号是否抵达,后者是信号递达的一种

举个例子:

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

这就是没被阻塞的信号的一生:信号产生 → 信号未决 → 信号递达

  1. 布置作业 = 信号产生
  2. 从我记下来到开始写作业的期间 = 信号未决
  3. 写作业 = 信号递达

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

这就是被阻塞的信号的一生:信号产生 → 信号被进程阻塞 → … → 进程解除对信号的阻塞 → 信号递达

  1. 布置作业 = 信号产生
  2. 从我记下来到开始写作业的期间 = 信号未决
  3. 讨厌他,不想写他的作业 = 阻塞信号
  4. 突然发现这个老师很好,我消除了对他的偏见 = 解除对信号的阻塞
  5. 写作业 = 信号递达
  6. *从我记下作业到我写的整个过程都是信号未决

总结: 信号的一生

  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得知, 为进程切换运行级别
切换运行级别的开销

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

在这里插入图片描述

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

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

信号的递达处理

默认动作 & 忽略动作

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

自定义动作

详细:

在这里插入图片描述

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

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


怎么做 (信号操作)

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,期待与你共同进步!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1137323.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Flutter——最详细(Scaffold)使用教程

Scaffold简介 相当于界面的主体&#xff08;类似于安卓最外层PhoneWindow&#xff09;&#xff0c;组件的展示都必须依附于它。 使用场景&#xff1a; 每一个界面都是脚手架&#xff0c;通过它来进行架构实现&#xff0c;优美的布局效果。 属性作用appBar顶部的标题栏body显示整…

Qwt QwtPlotMarker标记类详解

1.概述 QwtPlotMarker类是Qwt绘图库中用于在图表上绘制标记的类。标记可以是垂直或水平线、直线、文本或箭头等。它可用于标记某个特定的位置、绘制参考线或注释信息。 以下是类继承关系图&#xff1a; 2.常用方法 设置标记的坐标。传入x和y坐标值&#xff0c;标记将被放置在…

红黑树--讲解以及详细实现过程

目录 红黑树理解红黑树概念红黑树性质 红黑树实现红黑树图解基础结构实现插入实现 红黑树理解 红黑树概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个结点上增加一个存储位表示结点的颜色&#xff0c;可以是Red或Black。 通过对任何一条从根到叶子的路径上各个…

六零导航页SQL注入漏洞复现(CVE-2023-45951)

0x01 产品简介 LyLme Spage&#xff08;六零导航页&#xff09;是中国六零&#xff08;LyLme&#xff09;开源的一个导航页面。致力于简洁高效无广告的上网导航和搜索入口&#xff0c;支持后台添加链接、自定义搜索引擎&#xff0c;沉淀最具价值链接&#xff0c;全站无商业推广…

前端(二十五)——前端实现 OCR 图文识别的详细步骤与示例代码

&#x1f601;博主&#xff1a;小猫娃来啦 &#x1f601;文章核心&#xff1a;前端实现 OCR 图文识别的详细步骤与示例代码 文章目录 简介确定使用的 OCR API创建前端界面添加图像上传功能发送识别请求和处理识别结果完善代码添加注释结论附录 简介 在现代应用程序中&#xff…

如何选择向量数据库|Weaviate Cloud v.s. Zilliz Cloud

随着以 Milvus 为代表的向量数据库在 AI 产业界越来越受欢迎&#xff0c;传统数据库和检索系统也开始在快速集成专门的向量检索插件方面展开角逐。 例如 Weaviate 推出开源向量数据库&#xff0c;凭借其易用、开发者友好、上手快速、API 文档齐全等特点脱颖而出。同样&#xff…

使用AOP切面实现日志记录功能

系列文章 1.SpringBoot整合RabbitMQ并实现消息发送与接收 2. 解析JSON格式参数 & 修改对象的key 3. VUE整合Echarts实现简单的数据可视化 4. Java中运用BigDecimal对字符串的数值进行加减乘除等操作 5. List&#xff1c;HashMap&#xff1c;String,String&#xff1e;&…

【Javascript】函数(变量作用域)

变量&#xff1a;全局变量&#xff0c;局部变量 全局变量 挂载到window对象上的 var a全局变量;console.log(a);var a全局变量;console.log(window.a);var a全局变量;在控制台里输入a也能打印a的值 局部变量 函数体内部声明的变量 var a全局变量;function test(){var b局部…

软考高级之系统架构师系列之UP、RUP、4+1视图、JAD、JRP、RAD

概述 软件工程是一个很庞杂的系统工程&#xff0c;而我们面对的软件需求也很复杂&#xff1a; 面对不同规模&#xff08;复杂度&#xff0c;模块量&#xff0c;用户量&#xff0c;开发周期等等&#xff09;的软件项目&#xff0c;人员储备不尽不同的开发团队也会采用不同的软…

数据可视化在行业解决方案中的实践应用 ——华为云Astro Canvas大屏开发研究及指南

本文主要探讨华为云Astro Canvas在数据可视化大屏开发中的应用及效果。首先阐述Astro Canvas的基本概念、功能和特性说明&#xff0c;接着集中分析展示其在教育、金融、交通行业等不同领域实际应用案例&#xff1b;之后&#xff0c;详细介绍使用该工具进行大屏图表创建的开发指…

22年下半年上午题

计算机指令集 cpu的构成 存储器 决策表 原型模型 白盒测试 活动图 构件图 半圆是需接口&#xff0c;满圆是供接口&#xff0c;上图有小错误。 故障类型 b-树 排序算法复杂度 二分查找平均比较次数 成功查找比较平均次数 失败查找平均比较次数 如有 OSI 模型层次对应典型机器…

Vue+ElementUI项目打包部署到Ubuntu服务器中

1、修改config/index.js中的assetsPublicPath: /,修改为assetsPublicPath: ./ assetsPublicPath: ./2、在build/utils.js中增加publicPath: ../../ publicPath: ../../3、打开终端&#xff0c;在根目录下执行npm run build进行打包&#xff0c;打包成功后会生成dist npm run…

前端使用 printJS 插件打印多页:第一页空白问题解决

printJS({printable: [data:image/jpg;base64,${this.printData.url}],type: image,style: media print { page {size: auto; margin: 0; } body{margin:0 5px}} // 解决出现多页打印时第一页空白问题 })

java基础 集合2

9.List遍历方式&#xff1a; 10.Arraylist底层原理&#xff1a; 11.Linklist底层原理&#xff1a; 1.LinkedList做队列和栈&#xff1a; package day01;import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List;publ…

Vue3 + Tsx 集成 ace-editor编辑器

Ace Editor介绍 Ace Editor&#xff08;全名&#xff1a;Ajax.org Cloud9 Editor&#xff09;是一个开源的代码编辑器&#xff0c;旨在提供强大的代码编辑功能&#xff0c;通常用于构建基于Web的代码编辑应用程序。它最初由Cloud9 IDE开发&#xff0c;现在由开源社区维护。 主…

计算机网络 第四章网络层

文章目录 1 网络层的功能2 数据交换方式&#xff1a;电路交换3 数据交换方式&#xff1a;报文交换4 数据交换方式&#xff1a;分组交换5 数据交换方式&#xff1a;数据报方式6 数据交换方式&#xff1a;虚电路方式及各种方式对比7 路由算法及路由协议8 IP数据报的概念和格式9 I…

数据存储成本降低50%!图匠数据搭载OceanBase全新出发

近日&#xff0c;AI 技术公司 ImageDT 图匠数据&#xff08;以下简称“图匠”&#xff09;上线 OceanBase。目前&#xff0c;公司两大核心业务“数货宝”、“数智柜”已全面接入 OB Cloud 云数据库&#xff0c;保障图匠一站式全渠道销售数字化闭环作战平台的每一笔「数据」都算…

浮动面试题

浮动元素特点&#xff1a;

找不到mfc100u.dll怎么解决,总结了多种修复方法帮你解决

首先&#xff0c;让我们来了解一下mfc100u.dll文件是什么&#xff1f;其实&#xff0c;mfc100u.dll是Microsoft Foundation Class(MFC)库中的一个动态链接库文件&#xff0c;它包含了一些常用的类、函数和变量等资源&#xff0c;用于支持Windows应用程序的开发。 那么&#xf…

顺序表的查找(按位查找、按值查找)(数据结构与算法)

顺序表的基本操作&#xff1a;按位查找、按值查找 顺序表的按位查找 GetElem(L, i) :按位查找&#xff0c;获取表L中第 i 个位置元素的值 #define MaxSize 10 //定义最大长度 typedef struct{ElemType data[MaxSize]; //用静态的“数组”存放数据元…