详谈信号捕捉
内核如何实现信号的捕捉
如果信号的处理动作是用户自定义函数(调用signal函数自定义处理函数),在信号递达时就调用这个函数,这称为信号捕捉。由于信号处理函数的代码是在用户空间的,处理过程比较复杂
典型的操作系统中信号处理的机制
1.进入内核态:当程序因为中断、异常或系统调用陷入内核态时,系统会检查进程的信号屏蔽位图(block)和待处理信号位图(pending)。这些位图用于管理进程当前需要阻塞的信号和待处理的信号。
2.信号处理:如果待处理的信号存在且没有被阻塞,内核会准备处理这个信号。如果处理方式不是默认动作(SIG_DFL)或忽略信号(SIG_IGN),则需要进入用户态执行相应的信号处理函数。
3.清除信号位:在执行信号处理函数时,内核会将待处理信号位图中该信号对应的位清零,表示信号已被处理。同时,为了防止在信号处理函数执行时再次收到相同的信号,内核会自动将该信号加入进程的信号屏蔽字(block),直到信号处理函数返回时,恢复原来的信号屏蔽字。
4.再次检查信号:当信号处理函数执行完成并返回内核态时,系统会再次检查是否还有待处理的信号。如果有,系统会继续处理其他待处理的信号。如果没有,则恢复用户态继续执行主流程。
这种机制的优点在于:
•减少用户态与内核态之间的上下文切换:在内核态处理完毕后,通过顺带处理信号,减少了频繁的上下文切换开销。
•信号的屏蔽与自动恢复:信号处理时会暂时屏蔽同种信号,避免信号处理函数嵌套调用,从而保证信号处理的安全性。
举例:用户程序注册了SIGQUIT信号的处理函数SigHandler的前提下。当前正在执行main函数,这时发生中断或异常切换到内核态。在中断处理完毕后要返回用户态的main函数之前检测到有信号SIGQUIT递达,内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行SigHandler函数,SigHandler函数使用户态的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。SigHandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
信号处理过程涉及以下三个堆栈:
1.主执行流调用堆栈:这是进程在用户态正常执行时的调用堆栈。当进程在用户态运行时,函数的调用和返回都在这个堆栈上进行。
2.内核调用堆栈:当进程因系统调用、中断或异常陷入内核态时,内核会使用内核态专门的堆栈来处理这些事件。内核堆栈与用户态的主执行流堆栈是独立的,防止了用户态的执行流干扰内核态的处理。
3.信号处理函数调用堆栈:当信号到达并且需要调用相应的处理函数时,操作系统会切换到一个专门用于信号处理的堆栈上(可以是主执行流的堆栈,也可以是专门分配的信号处理堆栈)。信号处理函数执行时并不是主执行流堆栈上的函数调用,内核也不会直接调用信号处理函数。
这些堆栈之间的关系:
•信号处理函数的独立性:信号处理函数的调用是异步的,它可以在主执行流的任意时刻打断当前执行的用户态代码,执行完信号处理函数后再返回主执行流。因此,信号处理函数并不作为主执行流的一个普通函数调用,而是在独立的上下文中进行处理。
•内核调用堆栈的独立性:当程序因为系统调用、中断或异常进入内核态时,内核使用内核堆栈处理应的任务。内核的任务完成后,可能会处理信号的挂起状态,但处理信号的函数仍然是在用户态堆栈中执行。内核本身不会调用用户态的信号处理函数。
•主执行流和信号处理的关系:当信号到来时,信号处理函数可能打断主执行流的执行,但它并不从主执行流堆栈中弹出当前正在执行的函数,而是在信号处理完后返回继续执行主流代码。这是通过保存当前的上下文,并在处理完信号后恢复主流执行流的上下文来实现的。
因此,这三个堆栈之间的独立性确保了信号处理可以在任何时刻插入,且不会破坏主程序的调用栈或与内核态的操作混淆。这种设计也是为了提高系统的安全性和健壮性,避免不同堆栈之间的相互影响。
若某个信号的处理函数位于用户空间,且当前只有该信号需要递达,没有其他信号的前提下。一次信号处理过程中,总共会发生2次用户态到内核态的转化,2次内核态到用户态的转化
★ps:如果收到2号信号的时候收到3号信号,在执行完2号信号后,会自动屏蔽2号信号,此时会处理3号信号。因此,在整个信号处理的过程中,可能会出现交替处理不同信号的情况,但是不会出现连续重复处理同一信号的情况
【验证】处理当前信号时,当前信号屏蔽字为1,即被阻塞
#include <iostream>
#include <signal.h>
void SigHandler(int signo)
{
std::cout << "catch a signo : " << signo << std::endl;
sigset_t set;
sigemptyset(&set);
sigprocmask(SIG_BLOCK, NULL, &set);
if(sigismember(&set, 2)) std::cout << "the block bit is 1" << std::endl;
else std::cout << "the block bit is 0" << std::endl;
}
int main()
{
signal(2, SigHandler);
while(1)
{}
return 0;
}
下面代码验证,在执行处理函数前,该信号的pending位图标记为已经被置0
#include <iostream>
#include <signal.h>
void SigHandler(int signo)
{
std::cout << "catch a signo : " << signo << std::endl;
sigset_t set;
sigemptyset(&set);
sigpending(&set);
if(sigismember(&set, signo)) std::cout << "sig's pending bit is 1" << std::endl;
else std::cout << "sig's pending bit is 0" << std::endl;
}
int main()
{
signal(2, SigHandler);
while(1)
{}
return 0;
}
sigaction
sigaction也是一种信号捕捉函数,但相比于signal函数,它具有更大的灵活性。sigaction()
函数是 POSIX 标准中定义的一个系统调用,用于设置或查询与指定信号关联的处理动作。这个函数允许您更改信号的处理方式,例如设置自定义的信号处理函数或使用信号默认处理函数。
-
signum
:指定要操作的信号编号。 -
act
:指向struct sigaction
结构的指针,用于指定新的信号处理动作。如果这个参数为NULL
,则表示不更改信号的处理动作。 -
oldact
:指向struct sigaction
结构的指针,用于接收旧的信号处理动作。如果这个参数为NULL
,则表示不获取旧的信号处理动作。
struct sigaction
结构定义如下:
-
sa_handler
:指向信号处理函数的指针。如果设置了这个字段,当信号发生时,会调用这个函数来处理信号。 -
sa_sigaction
:指向高级信号处理函数的指针。这个函数可以接收更多的信息,如信号的详细信息(siginfo_t
结构)和额外的参数(void *
)。 -
sa_mask
:指定信号屏蔽字,用于指定哪些信号在信号处理函数执行期间应该被阻塞。 -
sa_flags
:指定与信号处理相关的标志位
sa_flags | 描述 |
---|---|
0 | 使用标准的、未经修改的信号处理行为 |
SA_NODEFER | 表示在信号处理函数执行期间不阻塞该信号 |
SA_RESETHAND | 表示处理函数执行完毕后,信号的处理方式恢复为默认处理方式 |
SA_SIGINFO | 表示使用 sa_sigaction 而不是 sa_handler 。 |
返回值:如果成功,sigaction()
函数返回 0;如果失败,它返回 -1,并设置 errno
以指示错误。
【示例】用sigaction演示当正在使用一个信号的时候,OS会自动屏蔽这个信号
当前如果正在对n号信号进行处理,默认n号信号会被自动屏蔽
对n号信号处理完成的时候,会自动解除对n号信号的屏蔽
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
void Print(sigset_t &pending)
{
for(int sig = 31; sig > 0; sig--)
{
if(sigismember(&pending, sig))
{
std::cout << 1;
}
else
{
std::cout << 0;
}
}
std::cout << std::endl;
}
void handler(int signum)
{
std::cout << "get a sig: " << signum << std::endl;
while(true)
{
sigset_t pending;
sigpending(&pending);
Print(pending);
sleep(1);
// sleep(30);
//break;
}
// exit(1);
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
sigemptyset(&act.sa_mask); // 如果你还想处理2号(OS对2号自动屏蔽),同时对其他信号也进行屏蔽
sigaddset(&act.sa_mask, 3);
act.sa_flags = 0;
for(int i = 0; i <= 31; i++)
sigaction(i, &act, &oact);
while(true)
{
std::cout << "I am a process, pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
第一次捕捉到2号信号会进入捕捉函数,第二次捕捉到时候会进入未决状态
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生.,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。sa_flags字段包含一些选项,本章不详细解释这两个字段,有兴趣的同学可以在了解一下。
标签:调用,函数,信号处理,堆栈,内核,信号,Linux,详谈 From: https://blog.csdn.net/2202_75331338/article/details/143831061