进程信号(下)

上文:进程信号(上)-CSDN博客

在上篇中,我们讲了关于信号的保存,信号集的操作,那么这篇我们就来看看信号的原理。

目录

1. 键盘产生信号的原理

2. 信号是如何被处理的? 

2.1 信号处理的原理 

2.2 内核态与用户态

2.2.1 内核空间 

2.2.2 内核态与用户态的切换 

3. 捕捉信号的其他方式 sigaction

3.1 函数定义

3.2 参数说明

3.3 sigaction结构体

3.4 sa_flag标志

3.5 使用示例 

4. 可重入函数 

不可重入

可重入

5. volatile


1. 键盘产生信号的原理

在上篇中,我们讲了信号产生的几种方法, 其中键盘产生信号的原理是什么呢?

那么在看完上面这张图,有没有觉得似曾相识,他的原理和信号技术非常的相似,如果你有这样的想法,那可就有点倒反天罡了,因为信号技术源自于硬件中断技术。 

2. 信号是如何被处理的? 

上一节我们讲了信号处理的操作与过程,那么信号到底是怎么被处理的呢?

2.1 信号处理的原理 

我们上节已经谈到,对信号的写入工作是OS做的,其实啊,对信号的相关工作都是OS做的。

当进程收到信号时,进程就会进入内核态,由内核对信号进行处理,如果我们没有对信号进行捕捉,那么当信号的默认处理是忽略时,进程会重新回到用户态;当信号的默认处理是终止时,进程会直接终止。当我们对信号进行了捕捉时,进程会切换到用户态执行处理函数,信号处理函数在最后是会执行特殊的系统调用进入内核态的(注意:信号捕捉函数与main函数是不同的控制流程)。进入内核态后一切正常时进入用户态继续执行主控制流程的代码。

有人会问,为什么不直接在内核态执行处理函数呢?那可不是内核态没有权限,而是人家压根不相信你的处理函数啊。用户态的权限是很小的,万一你的代码里有违法犯罪的动作,用户态根本执行不了,但要是内核态执行,那可就完蛋了,因此自定义的信号处理函数是由用户态执行的。 

2.2 内核态与用户态

说了这么多,但还没说内核态和用户态到底是什么啊。

记得我们之前学习进程时的一张图吗?没错,这是进程的地址空间。

但此前我们只知其然而不知其所以然,我们知道进程的地址空间内有栈、堆、代码区常量区,但这可都是用户空间内的,内核空间可是没说一点儿。

2.2.1 内核空间 

那么内核空间又是什么呢?

我们知道,进程的地址空间是一个个虚拟地址,而用户空间即指向内存中进程所需资源的部分,而内核空间即指向内存中OS运行所需资源的部分。

注意: 每个进程的内核空间指向相同,即所有进程共享内核空间。

有人会说,那所有的进程都指向同一个内核空间,那大家都可以访问它,我们之前学习的那么多进程间通信方式算什么?我们所学的进程具有独立性又算什么?每个进程都有交集了,还能称作独立吗吗?

别急,所有进程指向同一个内核空间,可不代表进程都能够访问它,这就是内核态与用户态的意义了。

2.2.2 内核态与用户态的切换 

很好,知道了这些,但我们还不知道用户态与内核态是怎么进行切换的啊。

  

那么到底为什么要这么设计呢?

不仅仅是因为系统调用,更是因为进程间调度的问题,我们知道,进程的时间片一到,OS就会进行进程调度,切换进程以达到进程并发的目的。进程的调度是需要OS来做的,OS必须要拿到自己的资源才能做事,所以在每个进程里都存放一个内核空间,使得OS能够随时随地拿到自己的资源,保证自己的超然。 

3. 捕捉信号的其他方式 sigaction

sigaction 是一个在 Unix 和类 Unix 系统中用于查询或设置信号处理方式的函数。它是 POSIX 信号处理接口的一部分,提供了比标准 C 库中的 signal 函数更为灵活和强大的功能。以下是关于 sigaction 函数的详细解释:

3.1 函数定义

#include <signal.h>  
  
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

3.2 参数说明

signum:指定要查询或修改的信号编号。可以指定除 SIGKILL 和 SIGSTOP 以外的所有信号。

act:指向一个 sigaction 结构体的指针,该结构体包含了新的信号处理函数和相关标志。如果此参数为 NULL,则仅查询而不修改信号的处理方式。

oldact:如果此参数不为 NULL,则函数会将当前信号的处理方式保存到这个指向 sigaction 结构体的指针中。

3.3 sigaction结构体

struct sigaction {  
    void     (*sa_handler)(int);       // 信号处理函数,类似于 signal 函数的 handler  
    void     (*sa_sigaction)(int, siginfo_t *, void *); // 另一个信号处理函数,提供额外信息  
    sigset_t   sa_mask;                // 在处理信号时,要阻塞的信号集  
    int        sa_flags;               // 信号处理选项标志  
    void     (*sa_restorer)(void);     // 废弃的字段,不再使用  
};

3.4 sa_flag标志

SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值 SIG_DFL。
SA_NODEFER:一般情况下,当信号处理函数运行时,内核会阻塞该信号。但如果设置了此标志,则不会阻塞。
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用。
SA_SIGINFO:如果设置了此标志,则使用 sa_sigaction 字段作为信号处理函数,并且可以向处理函数发送附加信息。

3.5 使用示例 

#include <unistd.h>
#include <iostream>
#include <signal.h>

void print(sigset_t pending)//打印当前pending位图
{
    std::cout<<"pending:  ";
    for (int i = 31; i > 0; i--)
    {
        if (sigismember(&pending, i))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout<<std::endl;
}

void handler(int signo)//二号信号的自定义捕捉函数
{
    std::cout<<"signo:"<<signo<<std::endl;
    sigset_t pending;
    sigemptyset(&pending);
    while (true)
    {
        sleep(1);
        sigpending(&pending);
        print(pending);
    }
}

int main()
{
    std::cout<<getpid()<<std::endl;
    struct sigaction act;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigaddset(&act.sa_mask, 11);
    sigaddset(&act.sa_mask, 15);
    sigaddset(&act.sa_mask, 5);
    sigaddset(&act.sa_mask, 3);
    sigaddset(&act.sa_mask, 4);//向阻塞信号集内添加信号

    sigaction(2, &act, nullptr);//修改二号信号的捕捉函数,不需要返回旧的处理方式
    while (1)
    {
        sleep(1);
    }
    return 0;
}

在进程运行时我们发现,在进程未接受到二号信号时,其他信号不会被阻塞,只有在进程处理二号信号的过程中,其他信号才会被阻塞。

这是因为sigaction与signal一样,只是告诉OS当进程收到该信号时这样处理,因此只有进程收到该信号时,才会执行sigaction函数。

4. 可重入函数 

 

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?

如果一个函数符合以下条件之一则是不可重入的:
调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

举个例子:

不可重入

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>

void Print(std::string str)
{
    std::cout << str << std::endl;
}

int Add(int a, int b)
{
    return a + b;
}

int main()
{
    pid_t pid = fork();
    if (pid == 0)
    {
        int cnt = 5;
        while (cnt--)
        {
            sleep(1);
            std::string str = "我是子进程";
            Print(str);
        }
        exit(0);
    }
    int cnt = 5;
    while (cnt--)
    {
        sleep(1);
        std::string str = "我是父进程";
        Print(str);
    }
    wait(0);
    return 0;
}

可重入

  pid_t pid = fork();
    if (pid == 0)
    {
        int cnt = 5;
        while (cnt--)
        {
            int ret=Add(1,2);
        }
        exit(0);
    }
    int cnt = 5;
    while (cnt--)
    {
        int ret=Add(3,4);
    }
    wait(0);

5. volatile

在有的平台下运行下面这段代码,程序收到二号信号后不会终结,这是为什么呢? 

int g_val=0;
void handler(int signo)
{
    std::cout<<"g_val: 0-> 1"<<std::endl;
    g_val=1;//修改g_val
}

int main()
{
    signal(2,handler);//捕捉SIGINT
    while(!g_val);//当g_val=1,退出循环
    std::cout<<"g_val:"<<g_val<<std::endl;
    return 0;
}

在很多编译器里,会对代码进行优化。而在上面这段代码里是具有两个执行流的,编译器不会对捕捉函数进行扫描,编译器在主控制流程里并没有检测到对g_val的修改,就会将g_val存放在cpu的寄存器里以方便取用,而我们修改的g_val是内存里的,但程序拿g_val是从寄存器拿的,因此程序不会终结。

这个时候volatite就起到了至关重要的作用,在变量前加volatite,可以强制变量不被放入寄存器,而是从内存读取,这样上面的内存忽略问题就不存在了。 

  • 27
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值