自学内容网 自学内容网

【Linux-进程信号】详谈信号捕捉

详谈信号捕捉

内核如何实现信号的捕捉

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

5f5d339dd5e54b409de4d18f08a2971c.png

典型的操作系统中信号处理的机制

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次内核态到用户态的转化

07a9dc8b271c4126bbdd5fecc0956ff9.png

 

★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;
}

f3d69ae59f164af08aa7695de0cd53fa.png

下面代码验证,在执行处理函数前,该信号的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;
}

 6b674b3aabdd4630a94376739dc35ca4.png

sigaction

sigaction也是一种信号捕捉函数,但相比于signal函数,它具有更大的灵活性。sigaction() 函数是 POSIX 标准中定义的一个系统调用,用于设置或查询与指定信号关联的处理动作。这个函数允许您更改信号的处理方式,例如设置自定义的信号处理函数或使用信号默认处理函数。

394507b782714322b4643267237eff30.png

  • signum:指定要操作的信号编号。

  • act:指向 struct sigaction 结构的指针,用于指定新的信号处理动作。如果这个参数为 NULL,则表示不更改信号的处理动作。

  • oldact:指向 struct sigaction 结构的指针,用于接收旧的信号处理动作。如果这个参数为 NULL,则表示不获取旧的信号处理动作。

 

struct sigaction 结构定义如下:

f0459f02386e49c486955286c7d90445.png

 

  • 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号信号会进入捕捉函数,第二次捕捉到时候会进入未决状态

4d5c8b76a6f34887bb17c7ca2787044b.png

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生.,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。sa_flags字段包含一些选项,本章不详细解释这两个字段,有兴趣的同学可以在了解一下。 

 


原文地址:https://blog.csdn.net/2202_75331338/article/details/143831061

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!