文章目录
- 信号保存
- 信号相关的概念
- 信号是如何保存的呢?
- 有关信号保存的系统调用
- sigprocmask
- 信号的增删查改
- 查看pending表
- 验证接口
- 信号捕捉
- 用户态与内核态
- 信号捕捉流程
- 总结
信号保存
信号相关的概念
信号递达:指 操作系统 将一个信号(Signal)从内核传递到目标进程 的过程。它是 信号处理机制 中的关键步骤。
信号未决:信号从产生到递达之间的状态
信号阻塞 进程或线程可以暂时屏蔽某些信号,使它们在阻塞期间不会递达和处理。一旦解除阻塞,信号会被递达并处理。
被阻塞的信号将保持未决状态,直到进程解除对此信号的阻塞,才能执行递达的动作。
注意:阻塞信号和忽略信号不同,阻塞信号表示信号没有递达,但是忽略信号表示信号已经抵达了,但是我们的处理方式是忽略处理。
信号是如何保存的呢?
在task_struct中有三张表,分别是下面三张:
前两张表是位图,第三张表是函数指针表。
我们从左到右说起:
第一张表是位图,比特位的位置是信号编号,比特位的含义是是否阻塞信号,1表示阻塞当前编号的信号,0表示为阻塞当前信号。
第二张表也是位图,比特位的位置是信号编号,比特位的含义是是否收到信号,1表示收到信号,0表示未收到信号。
第三张表是函数指针表,表示每个信号对应方法,当收到信号之后,我们将拿着信号编号,然后在handler表中查找对应信号的方法。
内核中三个表的结构,如上图。
有关信号保存的系统调用
sigprocmask
这个函数的作用是获取或者设置当前进程的block表。
第一个参数表示我们用这个函数干嘛,有以下三种选项:
三种选项的作用也有批注。
在讲第二个参数之前我们先讲讲sigset_t这个类型:
sigset_t
是一个用于表示 信号集合的数据类型,定义在 <signal.h> 头文件中。它通常用于 阻塞信号、解除信号阻塞 和 检查信号 等操作。
第二个参数是新的信号集,是我们修改后的信号集,而第三个参数是旧的信号集,是修改之前的信号集,方便我们修改之后方便恢复。
信号的增删查改
上面五个函数是增删查改,第一个函数是将一个信号集置为零,第二个函数是将信号集全部设置为1,第三个函数是添加新的信号到信号集当中,第四个函数表示在信号集中删除指定信号,第五个函数是在指定信号集中查找指定信号。
查看pending表
这个函数很简单,参数是输出参数,可以将当前进程的pending表输出出来,但是为什么没有设置pending表的呢?因为产生信号的方式有很多种,我们在上一章中讲到过,我们拿一个作为例子,比如说命令kill就可以产生信号。
我们讲完上面的接口,我们来写一个简单的代码,来验证一下,这些接口是否有屏蔽信号的功能。
验证接口
代码
#include <iostream>
#include <signal.h>
using namespace std;
void Printf(const sigset_t& pending)
{
for(int i = 31;i > 0;i--)
{
if(sigismember(&pending,i) == 1)
{
cout<<1;
}
else cout<<0;
}
cout<<endl;
}
int main()
{
sigset_t block,oblock;
//重置两个block表
sigemptyset(&block);
sigemptyset(&oblock);
//设置屏蔽信号
sigaddset(&block,2);
//将信号设置进内核
sigprocmask(SIG_BLOCK,&block,&oblock);
sigset_t pending;
while(true)
{
//打印pending表
sigpending(&pending);
Printf(pending);
sleep(1);
}
return 0;
}
运行结果
为什么当我们输入ctrl+c的时候,为什么在pending表中会一直存在2号信号呢?因为我们之前做了阻塞,当收到2号信号的时候,将其阻塞,所以pending表中会一直受到信号。所以如何解决这种情况呢?
我们可以定义一个计数器,当计数器走到10的时候将2号信号进入递达状态。
代码:
#include <iostream>
#include <signal.h>
using namespace std;
void Printf(const sigset_t& pending)
{
for(int i = 31;i > 0;i--)
{
if(sigismember(&pending,i) == 1)
{
cout<<1;
}
else cout<<0;
}
cout<<endl;
}
int main()
{
sigset_t block,oblock;
//重置两个block表
sigemptyset(&block);
sigemptyset(&oblock);
//设置屏蔽信号
sigaddset(&block,2);
//将信号设置进内核
sigprocmask(SIG_BLOCK,&block,&oblock);
sigset_t pending;
int count = 0;
while(true)
{
//打印pending表
sigpending(&pending);
Printf(pending);
if(count == 10)
{
sigprocmask(SIG_SETMASK,&oblock,nullptr);
cout<<"Unblock signal"<<endl;
}
count++;
sleep(1);
}
return 0;
}
结果
因为ctrl+c默认处理方式就是结束进程所以这里,我们看到的是结束,没有看到pending表的变化,我们加入signal函数进行信号捕捉,进行我们的自定义方法,不结束进程,查看pending表的变化状态:
可以看见,当信号从屏蔽字中去除的时候,执行自定义方法,然后pending表中2号信号消失。
信号捕捉
用户态与内核态
在操作系统中,CPU 主要运行在 用户态(User Mode) 或 内核态(Kernel Mode)。这两种模式是 操作系统的特权级别,用于保护系统的安全和稳定性。
-
用户态(User Mode):
- 权限受限,不能直接访问硬件(如磁盘、网络、内存管理等)。
- 运行用户进程(如应用程序)。
- 只能通过系统调用请求内核提供服务(比如
read()
、write()
、open()
)。
-
内核态(Kernel Mode):
- 最高权限,可以直接访问所有资源(如 CPU、内存、I/O 设备)。
- 运行操作系统内核代码(进程管理、文件系统、设备驱动等)。
- 可以直接操作硬件,比如修改页表、控制设备中断。
CPU 通过 模式位(mode bit) 来区分这两种状态:
0
表示 内核态(特权模式)。1
表示 用户态(受限模式)。
信号捕捉流程
之前讲到处理信号是在合适的时候处理的,什么是合适的时候呢?
进程从内核态切换到用户态的时候,操作系统检测当前进程的pending表&&block表,决定是否处理handler表处理信号
假如我们写了一个代码,当我们进行某些系统调用的时候,会出现中断,中断之后会进入内核态,当进入内核态之后,会处理当前进程中可以传递信号,如果信号的处理方式是自定义处理方式会直接返回用户态调用自定义方法,处理完后返回内核态,从上次中断的地方继续执行。
总结
通过本文的探讨,我们深入了解了Linux中进程信号的保存和捕捉机制。信号作为进程间通信的一种重要方式,能够有效地处理异步事件和异常情况。我们学习了信号的基本概念、信号的保存方式(如信号掩码和未决信号集),以及如何通过信号处理函数来捕捉和处理信号。
在实际应用中,合理地使用信号机制可以大大提高程序的健壮性和响应能力。然而,信号处理也需要注意一些细节,例如信号处理函数的可重入性、信号竞争条件的避免等。掌握这些知识点,能够帮助我们在编写多进程、多线程程序时更加得心应手。
希望本文的内容能够帮助你更好地理解Linux信号机制,并在实际开发中灵活运用。如果你有任何问题或建议,欢迎在评论区留言讨论!