Linux——信号

目录

  • Linux——信号
    • 1.信号的基础了解
    • 2.技术应用角度的信号
    • 3.产生信号
      • 3.1按键组合
      • 3.2系统调用产生信号
        • 3.2.1 kill()
        • 3.2.2 raise()
        • 3.2.3 abort()
      • 3.3**.** 软件条件产生信号
      • 3.4硬件异常产生信号
        • 3.4.1 /0异常
        • 3.4.2 内存越界异常
    • 4.理解信号的存在
    • 5.总结一下
    • 6.核心转储
    • 7.全部信号都可以被自定义捕获吗?
    • 8.阻塞信号
    • 9.信号在内核中的表示(信号的保存)
    • 10.信号的捕捉流程(重要)
      • 10.1用户态、内核态
      • 10.2 CPU寄存器CR3
      • 10.3进程地址空间的内核区
      • 10.4用户态和内核态的切换时机
      • 10.5总结
      • 10.6官方解释
    • 11. sigset_t
    • 12.信号集操作函数
    • 13.sigprocmask && sigpending
    • 14.实验
      • 让实验更有趣
    • 15.sigaction
      • OS对于信号的处理原则:
    • 16.可重入函数 && 不可重入函数
    • 17.volatile
    • 18.SIGCHLD信号(与进程等待相关)

Linux——信号

信号是有生命周期的:

预备——信号产生——信号保存——信号处理(递达)

1.信号的基础了解

  • 信号的概念

信号是进程之间事件异步通知的一种方式,属于软中断。

  • 生活角度的信号
  1. 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”

  2. 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。

  3. 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”

  4. 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)

  5. 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话

  • 从进程的角度
  1. 信号是给进程主动发送的,进程需要先识别信号【认识+动作】
  2. 进程本身是程序员写的代码数据和逻辑以及属性的集合
  3. 当进程收到信号的时候,进程本身可能在执行其他任务,因此信号可能不会被立即处理
  4. 进程本身必须要有对于信号的保存能力。才能延迟处理信号
  5. 当进程开始处理信号(信号被捕捉)的时候,会有三种动作(默认,自定义,忽略)

那进程将信号保存到哪里了呢?——进程PCB【task_struct】

image-20250316134601402

但是信号一共不是有很多个吗,这里一个unsigned int类型也只能保存32个信号。这是因为信号分为普通信号和实时信号,这里这个信号只保存普通信号[1, 31]

因此,发送信号的本质其实就是修改PCB中的信号标记位的信号位图!!!

而PCB属于内核数据结构,因此只有OS才能对其进行修改,因此不论是什么发送信号的方式,本质都是通过OS修改目标进程的PCB的信号位图【也就是说,OS必须提供信号相关的系统调用】

2.技术应用角度的信号

这里先写一个很简单的代码:

#include<iostream>
#include<unistd.h>using namespace std;int main()
{while(1){sleep(1);cout << "pid: " << getpid() << endl;}return 0;
}

image-20250316141007535

如上图所示,当时遇到一个死循环的时候,输入一个ctrl + c可以终止一个前台进程的运行。为什么?

这是因为ctrl + c是一个热键,本质是一个组合键。OS识别到之后将其解释为2号信号

image-20250316141351293

前面说了,当进程接收到一个信号之后,可能会采取3种动作(默认,自定义,忽略)

如果要查询信号的动作可以输入man 7 signal

image-20250316141635788

来验证一下ctrl + c 就是被OS识别成了信号2

这里需要介绍一个接口signal

image-20250316142624508

这个接口本事就是一个函数指针,当碰到了sig信号后,就会直接调用func函数

#include<iostream>
#include<unistd.h>
#include<signal.h>using namespace std;void handler(int signo)
{cout << "进程捕捉到了一个信号:" << signo << endl;//exit(1);
}int main()
{signal(2, handler); // 当进程接收到了2号信号就会执行handler函数while(1){sleep(1);cout << "pid: " << getpid() << endl;}return 0;
}

进程接收到ctrl + c,即2号信号就是自定义处理

image-20250316142437030

此时输入kill -9 21717就能杀死

如果handler函数自带exit,那么也能退出

image-20250316143133500

注意:

  1. Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。

  2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。

  3. 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

3.产生信号

3.1按键组合

就是类似ctrl + c这样的组合按键,会被OS识别为特定的信号

这里在介绍一个ctrl + /,会被OS识别为3号新号SIGQUIT

SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump

3.2系统调用产生信号

3.2.1 kill()

第一个接口:kill

image-20250316150704917

这个接口也很简单,就是给参数pid的这个进程发送一个sig信号

成功返回0,失败返回-1

实验:模拟实现一个kill

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<cstdio>using namespace std;
// 模拟实现系统调用的killint main(int argc, char* argv[])
{if(argc != 3){cout << "\nUsage: " << argv[0] << " pid signo\n" << endl;exit(1);}pid_t pid = atoi(argv[1]);int signo = atoi(argv[2]);int n = kill(pid, signo); //利用系统调用发送信号if(n != 0){perror("kill error");exit(1);}
}

如果此时有一个进程死循环,可以通过这个进程来实现kill的功能,通过命令参数,先传进程pid,在传signo【要发送的信号】

image-20250316153058787

kill()函数可以向任意进程发送任意信号

3.2.2 raise()

raise函数可以给当前进程发送指定的信号(自己给自己发信号)。

image-20250316153942251

int cnt = 0;
while(cnt != 10)
{cout << "cnt: " << cnt << endl;cnt++;if(cnt == 5)raise(3); // 自己给自己发送3号信号
}

执行结果如下:

image-20250316154231072

这个函数很简单,这里不多讲

3.2.3 abort()

给自己发送指定的6号信号SIGABRT

image-20250316154545994

这个函数其实就等价于kill(getpid(), 6);

这个也很简单,不多说

3.3**.** 软件条件产生信号

SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了。

image-20250316181434982

这里主要介绍alarm函数 和SIGALRM信号

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。

这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数

image-20250316181920148

1s之后,os会给当前进程发送一个SIGALRM信号,即14号信号

这个alarm的意义是什么呢?其实就是统计1s内,某种累加行为能够累加到多少

上述代码执行的话最终会累加到4w~10w多次【取决于是本地虚拟机还是云服务器】,但是如果不输出到显示器上,而是一直while循环让cnt++,直至alarm发14号信号给进程的时候就将最终的cnt输出到屏幕上。此时cnt的值会是3亿多。这是因为cout输出到限制器上有一个与外设的IO过程。速度比起cpu来说很慢

拓展:

alarm是一个系统调用,很有可能有很多进程都在调用这个接口来在内核中设置闹钟。而为了更好的管理多个alarm的调用,依然是对alarm这个闹钟先描述后组织。

image-20250316184005720

将闹钟管理起来的数据结构方法有很多,可以通过最小堆来实现。堆顶就是里超时时间最短的那个闹钟

3.4硬件异常产生信号

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

3.4.1 /0异常

比如下面这段代码:当前进程执行了除以0的指令

image-20250316163015782

运行的时候就会崩溃。这是在学c语言的时候就知道的

image-20250316163152415

可以看到进程确实终止了,这是一个现象,那到底为什么会终止呢?其实就是OS给当进程发了一个8号信号SIGFPE!

但是OS凭什么给进程发送信号,它怎么知道是这个进程运算出错了呢?

这就和硬件相关了!

在CPU中,有很多寄存器,有一些寄存器负责计算,而有一个寄存器负责计算状态是否正确——状态寄存器

当CPU的运算单元/0发生计算出来的数是一个无穷大的数,为异常,那么状态寄存器的溢出标志位就会置为1!本次计算状态为溢出状态!

image-20250316164610344

而CPU出现异常计算状态,那OS就会识别到这个异常,并判断CPU的状态寄存器,识别是什么异常,发现是溢出标志位为1之后,就会找到是那个进程正在调度CPU,就会将这个异常解释 为SIGFPE信号发送给进程。

3.4.2 内存越界异常

其实就是数组越界或者野指针问题

[hb@localhost code_test]$ cat sig.c 
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{printf("catch a sig : %d\n", sig);
}
int main()
{//signal(SIGSEGV, handler);sleep(1);int *p = NULL;*p = 100;while(1);return 0;
}
[hb@localhost code_test]$ ./sig 
Segmentation fault
[hb@localhost code_test]$ ./sig 
catch a sig : 11
catch a sig : 11
catch a sig : 11

11号信号就是SIGSEGV

那OS这里又是怎么知道是那个进程内存越界了呢?

一个进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

这个MMU是内存管理单元,它是集成在CPU中的

image-20250316171058715

4.理解信号的存在

我们发现,目标进程对os发送给它的信号会做处理,但是这个默认的处理,多数都是直接终止进程

那设置这么多信号的意义何在呢?

信号的意义:不同的信号,代表着不同的情况/事件。

而对不同的情况/事件的处理动作可以一样,都是进程终止,但是不同的情况代表着不同的终止原因

就像/0异常和内存越界异常一样,一个是8号信号,一个是11号信号。这就说明了不同的信号可以代表不同的情况,从而快速定位出错的原因,来修正代码!

5.总结一下

上面所说的所有信号产生,最终都要有OS来进行执行,为什么?

OS是进程的管理者,只有OS有权力修改位于进程PCB的信号位图

信号的处理是否是立即处理的?

在合适的时候,这个合适的时候在学习信号捕捉之后就明白了

信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?

是的,并且要被记录在当前进程的PCB的信号标记位

一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?

应该知道

如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

发送信号的本质其实就是修改进程PCB中的信号位图。

6.核心转储

在Linux中,c/c++在发生数组越界访问的时候,不一定会报错。比如下面这个代码:

int main()
{int a[10];//a[10] = 1;//a[100] = 1;//a[1000] = 1a[10000] = 1; // 执行到这里才报错return 0;
}

这是因为在Linux中,向OS申请一个栈帧的时候,虽然a[10],只申请了10个空间,a也确实只能用10个空间,但是这不代表OS实际给你的空间就是10个int类型大小。

这里出现的错误也是段错误,11号信号

注意:在云服务器上,core除了终止进程所做的其他事情,是看不太到的。需要先打开限制

image-20250316191827623

云服务器默认关闭了 这个选项,可以看到是0。

因此要输入ulimit -c 1024来打开这个选项

image-20250316191940941

此时多了一个core dumped,意思就是核心转储,并且还会再当前目录生成一个文件

image-20250316192124050

核心转储的概念:当进程因为异常而退出的时候,将进程在对应时刻的位于内存的有效数据转储到磁盘中!

这个文件的内容全都是二进制,直接打开行不通的

那核心转储的意义是什么呢?

**配合gdb来支持更好的调试,找到出错的原因!**在gdb上下文可以直接找到问题出错的地方和原因

7.全部信号都可以被自定义捕获吗?

先说结论:不可以,哪怕手动捕获了31个普通信号,但是OS仍旧不允许9号信号被捕捉。OS至少会保留一个9号信号来杀死异常的信号

代码如下:

#include<iostream>
#include<unistd.h>
#include<signal.h>using namespace std;void catsig(int i)
{cout << "捕获到信号:" << i << endl;
}int main()
{for(int i = 1; i < 32; i++){signal(i, catsig);}int cnt = 0;while(1){sleep(1);cout << "我正在运行: " << cnt++ << endl;}return 0;
}

image-20250316195226798

尽管我们手动捕获了全部31个普通信号,但是9号仍然可以终止进程。

8.阻塞信号

  • 实际执行信号的处理动作称为信号递达(Delivery)

  • 信号从产生到递达之间的状态,称为信号未决(Pending)。

  • 进程可以选择阻塞 (Block )某个信号。

  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.

  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

9.信号在内核中的表示(信号的保存)

说了这么多,信号未达,信号递达,信号阻塞这些概念,实际上信号在内核中到底是怎么被保存起来的呢?可以看下面这张图:

image-20250316212532170

pending是决定一个进程是否收到信号的位图,是一个32位的位图,可以存储31个普通信号,从右往左,第一个比特位置1就是当前进程接收到信号1

block是决定一个进程是否阻塞了某个信号的32位的位图

image-20250316213952617

结合pending和block两个位图,此时可以判断一个进程是否递达了某个普通信号,但是递达了之后要处理信号(默认、自定义、忽略)。而handler就提供了OS中对某个信号的处理方法

handler本质上是一个函数指针数组!每个下标都对应一个信号编号,而下标对应的内容,就是对应信号的处理方法!

image-20250316215125731

那当一个2号信号发给一个进程的时候,在内核数据结构的角度来看发生了什么?

首先判断是否存在自定义处理信号2的函数,如果有就将该函数的地址填入到该进程的handler数组中下标为2的位置,然后判断该进程的block位图的从右到左第二个比特位是否为1,也就是判断该进程是否阻塞2号信号,如果不阻塞(比特位为1),就判断在pending位图的第二个是否为1,不为1就置为1,然后调用handler中存储的信号2的处理方法

因此,现在在对第一个图片的例子进行一个总结:

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。

  • 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。这里不讨论实时信号

10.信号的捕捉流程(重要)

10.1用户态、内核态

信号产生的时候不会立即被处理,而是在合适的时候!

这个合适的时候就是——从内核态转变为用户态的时候!要弄清楚信号的捕捉流程,就要弄清楚内核态和用户态是什么,内核态如何转变为用户态,什么时候?

很多时候,我们作为用户级,总是有对硬件和内核资源的访问需求,而硬件和内核资源又只能由OS访问,因此当我们的进程调用系统调用的时候,实际上是切换为内核态去执行的系统调用

image-20250316225549936

那是CPU怎么知道,什么时候是用户态,执行用户代码,什么时候是内核态,执行内核级代码,这就要了解CPU的结构了

10.2 CPU寄存器CR3

在CPU中有很多看得见的寄存器也有很多看不见的寄存器,而cpu怎么执行进程的代码呢,这是因为有一个寄存器专门负责记载task_struct,也有一个寄存器专门负责页表的起始地址,因此,也有一个寄存器叫CR3,专门负责当前进程的运行级别

image-20250316230439956

10.3进程地址空间的内核区

但是作为一个进程,我又如何能够切换到内核态,执行OS的方法呢?

这就要回忆之前所学的进程地址空间了。前面学的都是用户层面的进程地址空间**,完整的进程地址空间一共有4G,用户级占3G,内核级占1G**

因此内核级进程地址空间,自然也需要内核级页表来映射物理地址和虚拟地址之间的关系,而每次开机的时候,就会把os的相关数据加载到内存中。

image-20250316233027852

由于所有用户层调用的都是一样的系统调用和内核数据,因此所有内核级页表一个就够了。也就是说,所有进程地址空间的内核区(3~4G)都是同一个内核级页表映射的!即每一个进程地址空间能够找到的OS都是同一个。

这也很合理,OS本来也只有一个,让所有的进程都看到同一个OS才是正常的。

这就意味着:一个进程想要调用系统调用,只需要在自己的进程地址空间,跳转到内核区,然后就可以调用了!

拓展:

进程切换有两种切换方式,一种是时间片策略,一个进程被OS强制切换了,还有一种是进程主动退出【通过系统调用的接口实现】,让给其他进程

10.4用户态和内核态的切换时机

知道了上面三个点之后。我们知道了,用户希望访问内核或者硬件资源,就会在自己进程的地址空间跳转到内核区找到对应的系统调用。但是凭什么用户就能够直接访问内核数据和硬件资源呢?不是说CR3会判断当前进程的执行身份吗?用户的进程肯定是3(用户态)

这是因为,当用户的进程调用系统调用的接口时,在系统调用中,会有代码将当前进程的身份从3(用户态)切换到0(内核态),然后再跳转到进程地址空间的内核区去访问OS的代码和数据。然后CPU才能接着执行

10.5总结

因此,当一个用户进程需要访问OS内核数据和代码的整体过程是怎么样的呢?

首先用户进程执行到系统调用的接口的时候,该接口会先将进程的用户态改为内核态,然后再自己的进程地址空间,从用户区跳转到内核区,然后CPU判断当前进程身份是否是内核态,然后再调用需要的OS的数据或代码。执行完后将内核态切换为用户态,然后回到进程地址空间的用户区,继续执行用户代码

这个时候回到我们一开始的问题,信号的捕捉流程,信号的捕捉其实就是对信号做处理。而信号不会立即被处理,是在内核态转为用户态的时候处理。这个过程是什么样的呢?来看看下图:

image-20250317004752309

在这张图片中,我们可以看到,用户进程在调用系统调用接口之后,切换到了内核态【在切换之前所做的准备工作前面已经学习了】,访问了OS的相关代码和数据,此时执行完了之后,就准备要从内核态切换回用户态

而此时OS就会做一个工作!就是从当前进程的PCB中,找到信号的内核数据结构,判断当前进程是否保存了信号需要处理、如果检测到有一个信号未被block阻塞,也在pending中置1,那就会去handler的对应下标找到处理方法去处理。

而处理又分三种方式【默认,忽略,自定义】。而默认和忽略都很简单。

  • 默认:大部分默认方法就是os内部规定的,而此时还处于内核态,因此顺便就将信号处理了,
  • 忽略:将pending的对应的比特位置0,然后返回用户态继续执行
  • 自定义:就需要在handler这个函数指针数组中,找到用户自定义的函数地址,然后跳转过去执行?不是的,此时进程处于内核态,无法执行用户态的函数!【os实际上从权限上看能执行用户态的代码,但是不可以这样做!】
  • 因此自定义是这样处理的:在handler这个函数指针数组中,在对应下标找到用户自定义的函数地址,然后先切换回用户态!然后跳转过去执行自定义的方法,完成对信号的处理(捕获),然后再切换回内核态,最终在返回到用户态!

image-20250317013621913

将上述过程抽象一下就得到下图,方便记忆

image-20250317013822861

10.6官方解释

上述的理解都是比较偏主观的,没有那么官方,下面是一些官方的说法,会带上具体是如何实现的

image-20250317014030424

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂、

举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了

11. sigset_t

image-20250316212532170

每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。他们用的是同一种位图来表示

因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态

阻塞信号集也叫信号屏蔽字(Signal Mask)

12.信号集操作函数

**sigset_t类型并不是上面我们所讲的一样就是一个32位的位图,只能用来表示每个信号的有效或无效状态,它实际上还封装了一些东西,是一个结构体类型。**因此并不能简单的直接那这个类型的变量去做位运算操作,而是要使用os提供的接口

#include <signal.h>int sigemptyset(sigset_t *set);int sigfillset(sigset_t *set);int sigaddset(sigset_t *set, int signo);int sigdelset(sigset_t *set, int signo);int sigismember(const sigset_t *set, int signo); 
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有 效信号。

  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系 统支持的所有信号。

  • 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的 状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号

  • signsmember: 可以查看set信号集的第signo个比特位是否有效

前四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含 某种 信号,若包含则返回1,不包含则返回0,出错返回-1

13.sigprocmask && sigpending

  • sigprocmask

前面的信号集操作函数都是对一个信号集(sigset_t)类型进行操作,而sigprocmask就是直接对进程当前的阻塞信号集(block)进行操作。调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

#include <signal.h>int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1

how参数是一个选项,表示可以选择的操作,下图都是SIG开头,少截了

image-20250317123449601

set参数是配合how的,如果要添加,就将set参数表示的信号集设置到当前进程的阻塞信号集。如果要解除,也是将其set表示的信号集设置到进程的阻塞信号集

oset参数是一个输出型参数,它就记载修改前的block

  • sigpending

这个接口很简单,谁调用它,谁就能获取当前进程的pending信号集

#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1

14.实验

用前面学的几个系统调用来做一个实验:

#include<iostream>
#include<vector>
#include<signal.h>
#include<unistd.h>using namespace std;#define MAX_SIG 31static vector<int> sig_block = {2, 3}; // 要阻塞的信号都装在这个vector中void Print_Pengding(const sigset_t& pengding)
{//打印for(int i = MAX_SIG; i >= 1; i--){//判断pengding信号集第i位的的比特位是否为1if(sigismember(&pengding, i))cout << "1";else    cout << "0";}cout << "\n";
}int main()
{// 1. 初始化信号集sigset_t block, oblock, pending;sigemptyset(&block);sigemptyset(&oblock);sigemptyset(&pending);//2. 添加要阻塞的信号for(auto& sig : sig_block)sigaddset(&block, sig);//3. 将要阻塞的信号添加到当前进程的block中sigprocmask(SIG_SETMASK, &block, &oblock);//4. 不断获取当前进程的pending信号集int cnt = 10;while(true){//4,1 初始化sigemptyset(&pending);//4.2 获取当前进程pengdingsigpending(&pending);//4.3 打印pending信号集Print_Pengding(pending);//慢一点sleep(1);if(cnt-- == 0){cout << "解除对信号的屏蔽\n";//将修改之前的信号集设置到当前进程sigprocmask(SIG_SETMASK, &oblock, &block); //一旦解除对信号的屏蔽,OS至少会立马处理一个信号!cout << "解除完毕\n";}}return 0;
}

执行结果如下:

这里有个问题,为什么cout << "解除完毕\n";这个代码没有被执行呢?

image-20250317123529920

因为我们这里没有自定义处理信号2,当信号被解除屏蔽之后,会立马处理信号2,而处理方法就是默认,就直接到handler数组中找到默认处理方法,然后处于内核态的情况下就直接处理了信号2,因此不会回到用户态了,自然不会执行用户态的下一步代码。

因此:如果不想要推出,可以用signal自定义捕获2号信号。并且在自定义方法内也不要退出。这样进程就会回到用户态

代码也很简单:

void handler(int signo)
{cout << signo << "号信号被捕获" << endl;    
}
    //对sig_block内的信号进行自定义捕获for(auto& signo : sig_block)signal(signo, handler);

此时执行效果如下:

image-20250317123548624

但是此时打印出来的pengding信号集,无法显示某个信号被发送到该进程了。既无法看到发送的信号变成1了,如下图所示

image-20250317123600441

这是因为,之前被阻塞的信号不在阻塞之后,每次发送到该进程都能直接递达,并且直接自定义方法处理。

让实验更有趣

因此想重新看到某个信号因为阻塞了,而处于未达状态,就要重新设置阻塞。也就是能重新看到pending信号集出现1

代码如下:

#include<iostream>
#include<vector>
#include<signal.h>
#include<unistd.h>using namespace std;#define MAX_SIG 31static vector<int> sig_block = {2, 3}; // 要阻塞的信号都装在这个vector中void Print_Pengding(const sigset_t& pengding)
{//打印for(int i = MAX_SIG; i >= 1; i--){//判断pengding信号集第i位的的比特位是否为1if(sigismember(&pengding, i))cout << "1";else    cout << "0";}cout << "\n";
}void handler(int signo)
{cout << signo << "号信号被捕获" << endl;
}int main()
{//对sig_block内的信号进行自定义捕获for(auto& signo : sig_block)signal(signo, handler);// 1. 初始化信号集sigset_t block, oblock, pending;sigemptyset(&block);sigemptyset(&oblock);sigemptyset(&pending);//2. 添加要阻塞的信号for(auto& sig : sig_block)sigaddset(&block, sig);//3. 将要阻塞的信号添加到当前进程的block中sigprocmask(SIG_SETMASK, &block, &oblock);//4. 不断获取当前进程的pending信号集int cnt = 10;while(true){//4,1 初始化sigemptyset(&pending);//4.2 获取当前进程pengdingsigpending(&pending);//4.3 打印pending信号集Print_Pengding(pending);//慢一点sleep(1);if(cnt-- == 0){cout << "解除对信号的屏蔽\n";//将修改之前的信号集设置到当前进程sigprocmask(SIG_SETMASK, &oblock, &block); //一旦解除对信号的屏蔽,OS至少会立马处理一个信号!cout << "解除完毕\n";//如果想要sig_block内的信号重新被阻塞,这里可以设置cout << "重新设置阻塞\n";sigprocmask(SIG_SETMASK, &block, &oblock); //将之前被解除阻塞的信号重新设置阻塞cnt = 10; //重置,再来10s之后,再次进入该分支}}return 0;
}

此时这个代码会每个10s为一个周期,将阻塞的信号取消阻塞,然后os进行一次集中的递达,然后自定义处理,然后重新设置阻塞信号

执行效果如下:

image-20250317123615231

15.sigaction

除了signal可以对信号进行自定义捕获,还要一个函数sigaction也可以进行捕获。这个函数的功能更加多样

#include <signal.h>int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
  • sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo 是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传 出该信号原来的处理动作。act和oact指向sigaction结构体:

  • 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动 作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回 值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信 号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用

OS对于信号的处理原则:

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字(block),当信号处理函数返回时自动恢复原来的信号屏蔽字(block),这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止

OS处理信号的原则:只允许串行的处理信号,而不允许递归

如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需 要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

sa_flags字段包含一些选项,本章的代码都 把sa_flags设为0,sa_sigaction是实时信号的处理函数,这里不详细解释这两个字段

16.可重入函数 && 不可重入函数

这里不深讲,到线程部分会学习,这里就是引出一个概念,了解一下

一般来说,都认为main函数执行流和信号捕获执行流是两条执行流

就如下图所示:

insert是一个头插函数,本来按照main函数执行流,是应该让node1头插,但是当执行到node1的next指向原来的头节点,即将把head修改到node1的时候,此时发生了信号的捕获(自定义),在自定义处理函数中,再度调用了一次insert,此时头插了另一个节点node2,这就导致了当回到main执行流的时候,head从指向node2再度指向了node1。这造成了内存泄漏,因为node2是无效的

image-20250317133554745

像上例这样**,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数有可能因为重入而造成错乱,像这样的函数称为不可重入函数**

反过来,如果一个函数不会因为重入而出错的话,就称为可重入函数

如果一个函数符合以下条件之一则是不可重入的:

  1. 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
  2. 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

17.volatile

下面来聊一下编译器优化的问题,下面是一个代码,编译的时候不带优化选项

[hb@localhost code_test]$ cat sig.c 
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{printf("chage flag 0 to 1\n");flag = 1;
}
int main()
{signal(2, handler);while(!flag);printf("process quit normal\n");return 0;
}
[hb@localhost code_test]$ cat Makefile 
sig:sig.cgcc -o sig sig.c #-O3
.PHONY:clean
clean:rm -f sig
[hb@localhost code_test]$ ./sig 
^Cchage flag 0 to 1
process quit normal

按照本次代码的思路就是:有一个死循环while,如果当前进程递达了一个信号2,那么就会自定义处理,在handler函数中会将flag的值改为1,然后该进程就会退出循环,然后正常退出

但是当我们带上编译器的优化选项之后:会发现一个奇怪的现象——无论我们怎么发信号给当前进程,尽管会不断的递达,不断的自定义捕获2号信号,但是循环无法退出

这是为什么呢?看下图:

image-20250317143207903

因此,为了解决这个问题,volatile关键字就出现了

volatile:保持内存可见性

它的作用说白话就是告诉CPU,我volatile声明的变量,你不要给我保存在寄存器中,我要你每次取他的时候都要从内存中取

因此上面的代码只需要给flag带上一个volatile声明即可:

volatile int flag = 0;

18.SIGCHLD信号(与进程等待相关)

之前在进程等待和进程状态的时候,我们说过,当一个子进程死亡的时候,会进入僵尸状态,然后等待父进程来回收自己,而父进程也需要以阻塞或者非阻塞的状态去等待子进程。

但是子进程死亡的时候不是直接死的,在死之前会发一个SIGCHLD信号(17号)给父进程

image-20250317144311124

这个信号的默认操作是Ign

在学习了信号后,现在有个想法:我是否能通过递达子进程发给我的信号,然后再自定义处理方法,来对子进程进行资源的回收和退出信息的获取

当然可以,前提是使用正确。【并且这样还有好处,之前是直接使用wait或者waitpid以阻塞的方式或者轮询非阻塞的方式进行等待,现在是一直在干自己的事情,等待子进程自己给我发信号,然后再进行进程等待】

写代码之前要小心两个点:

  1. 信号的处理原则:串行处理,而非递归。也就是如果同一时间有很多个子进程退出,那么在处理一个子进程发来的17号信号,会直接自动阻塞17号信号,这样就会导致大量进程无法被回收。因此在进程等待的时候,一定要while循环不断地进程等待
  2. 一定要选择轮询非阻塞等待,因为如果有很多进程,都是隔一段时间结束一个,那么阻塞式等待就会将整个进程都卡在处理17号信号的自定义函数上,父进程无法再做自己的事情。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
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){ // childprintf("child : %d\n", getpid());sleep(3);exit(1);}while (1){printf("father proc is doing some thing!\n");sleep(1);}return 0;}

事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法**:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。**

此方法对于Linux可用,但不保证在其它UNIX系统上都可用。

也就是将signal(SIGCHLD, handler);改为signal(SIGCHLD, SIG_IGN);

此时子进程退出就不在需要父进程的回收,而是直接被OS回收

  • 注意:

系统默认的忽略动作Ign和用户用sigaction函数自定义的忽略SIG_IGN 是不一样的!

image-20250317144311124

这里的Ign就是按照之前的流程,死之前给父进程发个17号信号,然后等待父进程来回收

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

向量叉积的应用——正反画画

1 解题思路 解题思路涉及的向量积相关知识 c实现 #include<iostream> #include<vector>using namespace std;struct TrianglePoint {int x;int y; };int momentForce(TrianglePoint A, TrianglePoint B, TrianglePoint C) {//AB向量&#xff1a;(B.x-A.x, B.y-A.…

构建自定义MCP天气服务器:集成Claude for Desktop与实时天气数据

构建自定义MCP天气服务器:集成Claude for Desktop与实时天气数据 概述 本文将指导开发者构建一个MCP(Model Control Protocol)天气服务器,通过暴露get-alerts和get-forecast工具,为Claude for Desktop等客户端提供实时天气数据支持。该方案解决了传统LLM无法直接获取天气…

Web安全策略CSP详解与实践

引言 &#xff1a;在黑客攻击频发的今天&#xff0c;你的网站是否像“裸奔”一样毫无防护&#xff1f;跨站脚本&#xff08;XSS&#xff09;、数据注入等攻击随时可能让用户数据泄露。今天我们将揭秘一个网站的隐形保镖——内容安全策略&#xff08;CSP&#xff09;&#xff0c…

HC-05与HC-06蓝牙配对零基础教程 以及openmv识别及远程传输项目的概述

这个是上一年的项目&#xff0c;之前弄得不怎么完整&#xff0c;只有一个openmv的&#xff0c;所以openmv自己去我主页找&#xff0c;这篇主要讲蓝牙 这个是我在使用openmv连接单片机1然后单片机1与单片机2通过蓝牙进行通信 最终实现的效果是&#xff1a;openmv识别到图形和数…

点云分割方法

点云分割 通过判断三维距离&#xff0c;实现对创建3团点云的分割 通过判断三维距离&#xff0c;实现对创建3团点云的分割 * 点云1 gen_object_model_3d_from_points (rand(100), rand(100),rand(100), Points1)* 点云2 gen_object_model_3d_from_points (rand(100), 2rand(100…

SpringBoot3使用CompletableFuture时java.util.ConcurrentModificationException异常解决方案

问题描述 在Spring Boot 3项目中&#xff0c;使用CompletableFuture进行异步编程时&#xff0c;偶发{"code":500,"msg":"java.util.ConcurrentModificationException"}异常&#xff0c;但代码中并未直接操作List或CopyOnWriteArrayList等集合类…

细说卫星导航:测距定位原理

测距定位原理 1. 伪距测量技术 核心原理&#xff1a;卫星发射信号&#xff0c;用户接收并记录传播时间&#xff0c;乘以光速得到距离&#xff08;伪距&#xff09;。 技术细节&#xff1a; 信号传播路径分析 信号结构&#xff1a; 卫星信号包含三部分&#xff1a; 载波&…

Linux系统管理与编程09:任务驱动综合应用

兰生幽谷&#xff0c;不为莫服而不芳&#xff1b; 君子行义&#xff0c;不为莫知而止休。 [环境] windows11、centos9.9.2207、zabbix6、MobaXterm、Internet环境 [要求] zabbix6.0安装环境&#xff1a;Lamp&#xff08;linux httpd mysql8.0 php&#xff09; [步骤] 5 …

RAG(Retrieval-Augmented Generation)基建之PDF解析的“魔法”与“陷阱”

嘿&#xff0c;亲爱的算法工程师们&#xff01;今天咱们聊一聊PDF解析的那些事儿&#xff0c;简直就像是在玩一场“信息捉迷藏”游戏&#xff01;PDF文档就像是个调皮的小精灵&#xff0c;表面上看起来规规矩矩&#xff0c;但当你想要从它那里提取信息时&#xff0c;它就开始跟…

RK3568 I2C底层驱动详解

前提须知&#xff1a;I2C协议不懂的话就去看之前的内容吧&#xff0c;这个文章需要读者一定的基础。 RK3568 I2C 简介 RK3568 支持 6 个独立 I2C: I2C0、I2C1、I2C2、I2C3、I2C4、I2C5。I2C 控制器支持以下特性: ① 兼容 i2c 总线 ② AMBA APB 从接口 ③ 支持 I2C 总线主模式…

UNIX网络编程笔记:基本TCP套接字编程

一、socket函数 一、socket函数核心参数与协议组合 函数原型与基本功能 #include <sys/socket.h> int socket(int family, int type, int protocol);• 功能&#xff1a;创建通信端点&#xff08;套接字&#xff09;&#xff0c;返回描述符供后续操作。 • 返回值&#…

JSON在AutoCAD二次开发中应用场景及具体案例

配置文件的读取 在AutoCAD插件开发中&#xff0c;可能需要生成、修改、读取配置文件中一些参数或设置。JSON格式的配置文件易于编写和修改&#xff0c;且可以方便地反序列化为对象进行使用。 运行后效果如下 using Autodesk.AutoCAD.ApplicationServices; using Autodesk.Au…

自由学习记录(46)

CG语法的数据类型 // uint : 无符号整数&#xff08;32位&#xff09; // int : 有符号整数&#xff08;32位&#xff09; // float : 单精度浮点数&#xff08;32位&#xff09;&#xff0c;通常带后缀 f&#xff08;如 1.0f&#xff09; // half : 半精度浮…

解决Selenium滑动页面到指定元素,点击失效的问题

White graces&#xff1a;个人主页 &#x1f649;专栏推荐:Java入门知识&#x1f649; &#x1f439;今日诗词:君失臣兮龙为鱼&#xff0c;权归臣兮鼠变虎&#x1f439; ⛳️点赞 ☀️收藏⭐️关注&#x1f4ac;卑微小博主&#x1f64f; ⛳️点赞 ☀️收藏⭐️关注&#x1f4…

Vue基础

目录 -Vue基础- 1、插值表达式 {{}} 2、Vue核心特性&#xff1a;响应式 3、开发者工具Vue Devtools(极简插件下载) 4、Vue指令 v-text v-html v-bind v-on v-if v-show v-for v-model 5、Vue指令修饰符 .stop .prevent .capture .self .once .enter、.tab、…

收数据花式画图plt实战

目录 Python plt想把纵坐标化成对数形式代码 子图ax. 我又有ax scatter&#xff0c;又有ax plot&#xff0c;都要去对数 数字接近0&#xff0c;取对数没有定义&#xff0c;怎么办 创建数据 添加一个小的常数以避免对数未定义的问题 创建一个figure和一个子图ax 在子图a…

二项式分布(Binomial Distribution)

二项式分布&#xff08;Binomial Distribution&#xff09; 定义 让我们来看看玩板球这个例子。假设你今天赢了一场比赛&#xff0c;这表示一个成功的事件。你再比了一场&#xff0c;但你输了。如果你今天赢了一场比赛&#xff0c;但这并不表示你明天肯定会赢。我们来分配一个…

【算法工程】大模型开发之windows环境的各种安装

1. 背景 最近由于研究需要&#xff0c;我购置了两块3090显卡&#xff0c;以便在家中进行一些小规模的实验。为此&#xff0c;还更换了主机。当然&#xff0c;新系统上少不了要安装各种开发环境。从开发体验来看&#xff0c;macOS无疑更为流畅&#xff0c;但为了确保所有环境都能…

论文阅读笔记:Denoising Diffusion Probabilistic Models (2)

接论文阅读笔记&#xff1a;Denoising Diffusion Probabilistic Models (1) 3、论文推理过程 扩散模型的流程如下图所示&#xff0c;可以看出 q ( x 0 , 1 , 2 ⋯ , T − 1 , T ) q(x^{0,1,2\cdots ,T-1, T}) q(x0,1,2⋯,T−1,T)为正向加噪音过程&#xff0c; p ( x 0 , 1 , …

vscode查看文件历史git commit记录

方案一&#xff1a;GitLens 在vscode扩展商店下载GitLens 选中要查看的文件&#xff0c;vscode界面右上角点击GitLens的图标&#xff0c;选择Toggle File Blame 界面显示当前打开文件的所有修改历史记录 鼠标放到某条记录上&#xff0c;可以看到记录详情&#xff0c;选中O…