Linux笔记---信号(下)

1. sigaction函数

cpp 复制代码
#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

功能:sigaction函数用于检查或修改与指定信号相关联的处理动作。它可以用来设置信号处理函数、信号掩码等。

参数

  • signum:指定要处理的信号编号,如SIGINT、SIGTERM等。

  • act:一个指向sigaction结构体的指针,用于设置新的信号处理动作。

  • oldact:一个指向sigaction结构体的指针,用于存储旧的信号处理动作。

返回值

  • 成功时,sigaction函数返回0。

  • 失败时,它返回-1,并设置errno以指示错误。

用法

相比传统的signal函数,sigaction函数提供了一种更灵活和可移植的方式来处理信号。但是实际上sigaction函数的使用非常麻烦,个人感觉还是signal函数更好用一些。

其第二、三个参数是如下结构体的指针:

cpp 复制代码
struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};

目前我们只需要关注其中的两个参数:sa_handler和sa_mask。

  • sa_handler:自定义捕获函数。
  • sa_mask:信号被捕获时,需要同步屏蔽的信号的掩码位图。

某个信号递达之前,该信号会被屏蔽,以确保在sigaction执行期间,该信号不会再次递达(执行结束之后解除屏蔽)。

为了达成某种目的,我们可以通过设置sa_mask成员变量,在屏蔽这个信号的同时,顺便将sa_mask中的信号一起屏蔽。

但实际上感觉非常鸡肋,可能只有做系统开发的人才有机会用得到吧。下面是一个使用示例:

cpp 复制代码
#include <iostream>
#include <signal.h>

void sighandler(int sig)
{
    std::cout << "获得信号[" << sig << "]" << std::endl;
    while(true)
    {
        sigset_t pending;
        sigpending(&pending);
        for(int i = 31; i >= 1; i--)
        {
            if(sigismember(&pending, i))
                std::cout << 1;
            else
                std::cout << 0;
        }
        std::cout << std::endl;
        sleep(2);
    }
    exit(0);
}

int main()
{
    struct sigaction act, old_act;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, 3);
    sigaddset(&act.sa_mask, 4);
    act.sa_handler = sighandler;

    sigaction(SIGINT, &act, &old_act);
    while(true)
    {
        std::cout << "hello world: " << getpid() << std::endl;
        sleep(2);
    }
    return 0;
}

2. 可重入函数

假设这样一种场景:在main函数与sighandler函数当中,都存在对于同一个链表的头插操作。在程序的执行过程中,main函数中对insert函数进行调用,并且头插核心的两步操作的前一步刚执行完的一瞬间,进程收到一个信号,进而跳转到sighandler函数,并抢先完成了对链表的头插操作。

显然,这造成了我们预料之外的插入结果,并且很有可能因找不到node2而导致内存泄露的发生。

由于信号机制的引入,即使我们的程序并不涉及多线程,也会存在多个执行流,这就导致我们程序中的某些资源需要被看作时临界资源。

但是,并不是所有的函数在作为sighandler时都会造成以上问题,我们将不存在类似问题的函数叫做可重入函数,反之则叫做不可重入函数。

可重入函数

  • 定义 :可重入函数是指在多个执行流(如多个线程或进程)同时调用时,能够正确执行且不会产生错误或不一致结果的函数。
  • 特点
    • 不依赖全局变量:可重入函数不依赖于全局变量或静态变量,而是使用局部变量或通过参数传递数据。
    • 不修改自身状态:函数执行过程中不会修改自身的状态,如修改自身的代码段或数据段。
    • 可被递归调用:可重入函数可以被递归调用,即函数可以在执行过程中再次调用自身,而不会产生错误。
  • 示例:这个函数只使用了局部变量,不依赖于全局状态,因此可以在多个执行流中同时调用而不会产生冲突。
cpp 复制代码
int add(int a, int b) {
    return a + b;
}

不可重入函数

  • 定义 :不可重入函数是指在多个执行流同时调用时,可能会产生错误或不一致结果的函数。
  • 特点
    • 依赖全局变量:不可重入函数通常依赖于全局变量或静态变量来存储中间结果或状态信息。
    • 修改自身状态:函数执行过程中可能会修改自身的代码段或数据段,如修改全局变量的值。
    • 不可被递归调用:不可重入函数通常不可以被递归调用,因为递归调用可能会导致状态的混乱和错误。
  • 示例 :这个函数依赖于全局变量result来存储计算结果,因此在多个执行流同时调用时,可能会导致结果的不一致。
cpp 复制代码
int result = 0;
void add_to_result(int a) {
    result += a;
}

3. volatile关键字

在C语言中,volatile关键字用于告知编译器不要对特定变量进行优化,确保每次访问都直接读写内存而非依赖寄存器中的缓存值。

学过编译原理的都知道,编译器在编译的过程当中会做很多的优化。在大多数情况下,这些优化都是百利而无一害的,但是在某些情况下,可能导致我们的程序出现bug。

例如下面的这段代码:

cpp 复制代码
#include <iostream>
#include <signal.h>

int flag = 0;
void sighandler(int sig)
{
    printf("chage flag 0 to 1\n");
    flag = 1;
}

int main()
{
    signal(SIGINT, sighandler);
    while (!flag)
        ;
    printf("process quit normal\n");
    return 0;
}

预期的结果是,我们按下ctrl + c之后,程序就会跳出循环,进而被终止:

但是,如果在编译时让操作系统做了优化(-On选项,n代表优化等级,越高优化越激进): 此时,我们就会发现ctrl + c并不会使程序跳出循环进而退出,只能使用其他信号来将其杀死。

这是因为,在main函数当中,我们对flag没有任何修改,所以编译器就认为flag的值不会发生变化,每次访问flag时都使用存放在寄存器中的缓冲值,从而导致程序不会退出。

当我们使用volatile来修饰flag之后,编译器就不会对flag进行优化,每次都从内存当中读取flag的值。此时,就算我们将优化等级调的很高,也不会影响我们程序的正常运行:

cpp 复制代码
#include <iostream>
#include <signal.h>

volatile int flag = 0;
void sighandler(int sig)
{
    printf("chage flag 0 to 1\n");
    flag = 1;
}

int main()
{
    signal(SIGINT, sighandler);
    while (!flag)
        ;
    printf("process quit normal\n");
    return 0;
}

4. SIGCHLD

当子进程退出时,其会向父进程发送SIGCHLD信号,该信号的默认处理动作为Ign:

利用这一机制以及信号捕获机制,我们可以让进程等待变成异步完成。

即父进程创建进程之后就可以着手完成其他任务,而不需要专门地去等待子进程,当SIGCHLD到达时,在捕获函数当中等待子进程:

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

void handler(int sig)
{
    pid_t id;
    // 短时间内SIGCHLD信号可能多次到达,但是只会被记录一次
    // 需要循环回收退出的子进程
    while ((id = waitpid(-1, NULL, WNOHANG)) > 0)
    {
        printf("wait child success: %d\n", id);
    }
}

int main()
{
    signal(SIGCHLD, handler);
    pid_t cid;
    for (int i = 0; i < 10; i++)
    {
        if ((cid = fork()) == 0)
        {
            // child
            printf("child : %d\n", getpid());
            sleep(3);
            exit(1);
        }
    }
    while (1)
    {
        printf("father proc is doing some thing!\n");
        sleep(1);
    }
    return 0;
}

忽略SIGCHLD信号

由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:忽略SIGCHLD信号。

若父进程不关心子进程退出状态,可显式忽略 SIGCHLD,系统自动回收子进程:

cpp 复制代码
signal(SIGCHLD, SIG_IGN); // 注意:部分系统(如Linux)支持此行为

注意:

  1. 不同系统对这种行为的处理可能不同(POSIX未强制要求自动回收)。
  2. 默认行为(SIG_DFL)是Ign 和 将处理行为设置为SIG_IGN是两回事。
相关推荐
葡萄杨2 小时前
【软件使用】RSS(Really Simple Syndication)
笔记
东京老树根2 小时前
SAP学习笔记 - 开发13 - CAP 之 添加数据库支持(Sqlite)
笔记·学习
寻丶幽风5 小时前
论文阅读笔记——PixArt-α,PixArt-δ
论文阅读·笔记·文生图·扩散模型·t2i
Lester_11015 小时前
嵌入式学习笔记 - 关于ARM编辑器compiler version 5 and compiler version 6
arm开发·笔记·学习
moxiaoran57537 小时前
uni-app学习笔记八-vue3条件渲染
笔记·学习·uni-app
愚润求学8 小时前
【Linux】进程间通信(四):System V标准(共享内存、消息队列、信息量)
linux·运维·服务器·开发语言·c++·笔记
moxiaoran57538 小时前
uni-app学习笔记九-vue3 v-for指令
笔记·学习·uni-app
北温凉9 小时前
【学习笔记】机器学习(Machine Learning) | 第七章|神经网络(3)
笔记·机器学习
岂是尔等觊觎9 小时前
PCB设计教程【入门篇】——电路分析基础-电路定理
经验分享·笔记·嵌入式硬件·学习·pcb工艺