🎬慕斯主页:修仙—别有洞天
♈️今日夜电波:it's 6pm but I miss u already.—bbbluelee
0:01━━━━━━️💟──────── 3:18
🔄 ◀️ ⏸ ▶️ ☰
💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍
目录
前言
信号的保存
首先了解几个概念
什么是信号递达(Delivery)?
什么是信号未决(Pending)?
什么是阻塞 (Block )?
信号在内核中的表示
sigset
sigprocmask
信号的处理
信号的捕捉
使用 sigaction() 捕捉信号
信号处理时机
前言
本文书接上回Linux进程信号处理:深入理解与应用(2),本文是Linux进程信号处理的最后一篇文章。主要介绍信号的保存以及信号的处理。其中较为重要的是sigset以及基于它的函数调用。
信号的保存
首先了解几个概念
实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)
进程可以选择阻塞 (Block )某个信号
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
什么是信号递达(Delivery)?
实际执行信号的处理动作称为信号递达(Delivery),我们在前面的文章Linux进程信号处理:深入理解与应用(1)中有提到可以通过signal替换信号,实际上就是替换的处理动作。其中提到了有三种处理方式:
-
- SIG_IGN:表示忽略该信号,即信号发生时不采取任何行动。
- SIG_DFL:表示采用系统默认的处理方式,通常是终止进程或忽略该信号。
- 自定义处理函数:如果你希望在信号发生时执行特定的操作,可以设置一个自定义的处理函数。这个函数通常需要接受一个整数参数(信号编号)并且返回void。
信号的递达就是上述的三种处理,信号的递达就是处理信号!我们在认识signal的时候,对于函数原型的认识如下:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
可以看到我们传递的SIG_IGN、SIG_DFL和自定义处理函数都是一个函数指针,自定义处理函数就是函数,那么对于SIG_IGN和SIG_DFL该怎么理解呢?实际上的定义如下:
#define SIG_DFL ((sighandler_t) 0)
#define SIG_IGN ((sighandler_t) 1)
他们实际上就是对于0和1的强转,系统可以根据该特征来判断是默认还是自定义还是忽略,因为自定义的地址肯定不会是0和1。
什么是信号未决(Pending)?
信号从产生到递达之间的状态,称为信号未决(Pending)。信号产生的时候,当前的进程可能在做更重要的事情,信号无法被立即处理,他需要在合适的时候的处理,所以需要有对信号进行保存的能力,这就是前面的文章Linux进程信号处理:深入理解与应用(1)中提到的系统可以保存信号!保持信号则是通过位图来进行保存对应的信号!
什么是阻塞 (Block )?
信号的阻塞(Block)是指将某个信号设置为不可递达状态,即暂时忽略该信号。当一个信号被阻塞时,即使它被发送给进程,也不会立即递达,而是保持未决状态,直到信号解除阻塞。说大白话:阻塞实际上就是未决之后,暂时不递达,直到解决对信号的阻塞!
信号在内核中的表示
如下,根据我们上面的了解,我们可以对以下这张图进行了解,每一个进程都会有这三张表,用于保存信号,这也是前面提到,为什么改变了其中的handler,其他的进程不会改变,因为handler的改变只是对于对应的进程而言的。我们通过横向的看着三张表,就可以很好的理解信号的保存以及后续的处理过程:
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
sigset
sigset_t
是Linux系统中用于表示信号集的数据类型。它用于存储一组信号,以便在进程之间进行信号的发送和接收操作。
sigset_t
实际上就是一个由操作系统提供的位图结构,实际上就是结构体里套了一个数组:
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
typedef __sigset_t sigset_t;
以下是一些常用的函数和操作与sigset_t
相关的:
sigemptyset(sigset_t *set)
: 初始化一个空的信号集,将所有位设置为0。sigfillset(sigset_t *set)
: 初始化一个包含所有信号的信号集,将所有位设置为1。sigaddset(sigset_t *set, int signum)
: 向信号集中添加一个信号。sigdelset(sigset_t *set, int signum)
: 从信号集中删除一个信号。sigismember(const sigset_t *set, int signum)
: 检查信号是否在信号集中。sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
: 修改进程的信号掩码,即阻塞或解除阻塞信号集中的信号。sigpending(sigset_t *set)
: 获取当前进程未决的信号集。sigsuspend(const sigset_t *set)
: 暂停进程执行,直到收到信号集中的一个信号为止。sigwait(const sigset_t *set, int *sig)
: 等待信号集中的一个信号,并返回接收到的信号编号。
下面他们通过sigprocmask和sigpending来理解这些操作:
sigprocmask
sigprocmask()
是一个用于修改进程信号掩码的函数,它允许进程阻止或解除阻止某些特定信号。
函数原型:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数说明:
how
:指定要执行的操作,可以是以下三个值之一:
SIG_BLOCK
:将set
中的信号加入到进程的信号掩码中,即阻塞这些信号。SIG_UNBLOCK
:从进程的信号掩码中移除set
中的信号,即解除阻塞这些信号。SIG_SETMASK
:直接将set
设置为进程的信号掩码,替换原有的信号掩码。
set
:指向一个sigset_t
类型的信号集,包含了要添加到或从进程信号掩码中移除的信号。根据how
的值不同,该参数的含义也不同。oldset
:指向一个sigset_t
类型的信号集,用于保存修改前的信号掩码。如果不需要保存旧的信号掩码,可以将此参数设置为NULL
。
返回值:
- 成功时返回0,失败时返回-1,并设置
errno
为相应的错误码。
使用示例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
void handler(int signo)
{
std::cout << "获得一个:NO." << signo << "信号" << std::endl;
}
int main()
{
//替换2号信号的处理方法
signal(2, handler);
std::cout << "i am running,but block~,pid:" << getpid() << std::endl;
// 初始化信号集
sigset_t block,oblock;
sigemptyset(&block);
sigemptyset(&oblock);
sigaddset(&block,2);
// 阻塞2号信号
sigprocmask(SIG_BLOCK,&block,&oblock);
int time=5;
while(time--)
{
sleep(1);
}
std::cout<<std::endl;
std::cout << "i am running and do not block~,pid:" << getpid() << std::endl;
//恢复原来的信号掩码
//sigprocmask(SIG_UNBLOCK,&block,&oblock);
sigprocmask(SIG_SETMASK,&oblock,&block);
return 0;
}
注意事项:
- 在使用
sigprocmask()
函数时,需要注意避免产生死锁。例如,如果在信号处理函数中调用了sigprocmask()
,则可能导致死锁。- 在多线程环境中,每个线程都有自己的信号掩码。因此,当在一个线程中调用
sigprocmask()
时,只会影响该线程的信号掩码。sigprocmask()
函数会返回之前的信号掩码,以便在后续操作中恢复。如果不需要在后续操作中恢复信号掩码,可以将oldset
参数设置为NULL
。
sigpending
sigpending()
函数用于获取当前进程未决(pending)的信号集。
函数原型:
#include <signal.h>
int sigpending(sigset_t *set);
参数说明:
set
:指向一个sigset_t
类型的信号集,用于存储获取到的未决信号。
返回值:
- 成功时返回0,失败时返回-1,并设置
errno
为相应的错误码。
使用示例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
void handler(int signo)
{
std::cout << "获得一个:NO." << signo << "信号" << std::endl;
}
int main()
{
// 替换2号信号的处理方法
signal(2, handler);
std::cout << "i am running,but block~,pid:" << getpid() << std::endl;
// 初始化信号集
sigset_t block, oblock;
sigemptyset(&block);
sigemptyset(&oblock);
sigaddset(&block, 2);
// 阻塞2号信号
sigprocmask(SIG_BLOCK, &block, &oblock);
sigset_t pend;
while (true)
{
//获取当前进程未决信号集
sigpending(&pend);
//打印直观显示
for (int i = 31; i > 0; i--)
{
if (sigismember(&pend, i))
{
std::cout << "1";
}
else
{
std::cout << "0";
}
}
std::cout << std::endl;
sleep(1);
}
return 0;
}
注意事项:
sigpending()
函数只能获取未决信号集,不能获取阻塞或忽略的信号。- 在多线程环境中,每个线程都有自己的未决信号集。因此,当在一个线程中调用
sigpending()
时,只会获取该线程的未决信号集。
信号的处理
信号的捕捉
信号捕捉是操作系统提供的一种机制,允许进程指定特定函数(称为信号处理程序或信号处理器)来响应特定信号的接收。当一个进程收到一个它可以捕捉的信号时,操作系统会暂停进程当前的执行流程,转而调用与该信号关联的处理程序。一旦信号处理程序执行完毕,进程会继续执行被信号打断的操作。
在Linux系统编程中,信号捕捉通常通过以下两个系统调用实现:
signal()
- 这是一个较老的系统调用,用于设置信号处理程序。sigaction()
- 这是一个更现代、功能更丰富的系统调用,用于设置信号处理程序。
使用 sigaction()
捕捉信号
sigaction()
提供了比signal()
更多的控制,包括可以设置信号屏蔽、指定信号处理选项等。其原型如下:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum
参数是你想要捕捉的信号的编号。act
参数是一个指向sigaction
结构体的指针,其中包含了新的信号处理程序、信号集和其他标志。oldact
参数是一个指向sigaction
结构体的指针,用于接收旧的信号处理程序信息,如果不需要可以设置为NULL
。
sigaction
结构体定义如下:
struct sigaction {
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sa_handler
: 信号处理程序。sa_mask
: 信号屏蔽字,指定在处理信号时阻塞哪些其他信号。sa_flags
: 影响信号处理的一组标志。sa_restorer
: 不常用,通常设为NULL
。
信号处理程序的编写
编写信号处理程序时,需要注意以下几点:
- 信号处理程序应尽可能短小,避免长时间阻塞。
- 信号处理程序不应调用非异步信号安全的函数。
- 不要在信号处理程序中进行复杂的逻辑操作,特别是涉及数据结构和锁定的操作。
- 尽量避免在信号处理程序中使用全局变量,因为信号处理程序可能会在任何时间运行,从而可能导致竞态条件。
- 如果需要修改全局状态,可以使用原子操作或者锁来保护共享数据。
示例代码
下面是一个简单的使用 sigaction
捕捉 SIGINT
信号并设置信号处理程序的示例:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void int_handler(int signum) {
printf("Interrupt signal (%d) received.
", signum);
}
int main() {
struct sigaction sa;
sa.sa_handler = int_handler;
sa.sa_flags = 0;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}
while (1) {
pause(); // 等待信号到来
}
return 0;
}
在上面的代码中,我们创建了一个信号处理程序
int_handler
来处理SIGINT
信号。我们使用sigaction
系统调用来注册这个处理程序,并设置sa_flags
为 0,表示使用默认的信号处理选项。我们还清空了sa_mask
,表示在处理信号时不阻塞任何其他信号。最后,我们使用无限循环和pause
函数使进程等待信号的到来。
信号处理时机
前面我们提到,信号会在合适的时候被处理,那么是什么时候呢?答案是在进程从内核态回到用户太的到时候进行信号的检测和信号的处理。这里牵扯到了用户态和内核态的互相切换,就不细展开了。我们只需要知道用户态是一种受控的状态,能够访问的资源是有限的。内核态是一种操作系统的工作状态,能够访问大部分的资源。系统调用的背后就包含了身份的变化。下图中3和5的交点即为信号处理的时候:
对上图的解释:如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o!
给个三连再走嘛~