【万字详解】Linux进程信号||一文搞定进程信号||附测试代码

0

🍳信号理解

🧈什么是信号?

  • 在生活中,有很多的例子,例如:红绿灯、旗语、铃声等等,这些东西都是给人传递一种特定信号的,如在交通中,红灯停,绿灯行。
  • Q:我们怎么知道红灯停绿灯行?
    A:通过学习,了解的!即使我们现在不在道路上,我们也知道这个常识,知道当这个信号出现时我们应该怎么处理,即使当前信号还没出现!

🥞进程信号

信号是给进程发送的,那么进程也有对应的信号处理的机制(这个好比我们知道交规一样,进程信号处理机制是程序员预先设定好的!),一样的道理,即便是信号还没有产生,但是进程已然存在对应信号的处理机制!

🥓查看系统信号

  • kill-l 命令
    0

🥩在技术角度理解信号

  1. 用户输入命令,在Shell下启动一个前台进程。
    . 用户按下 Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程
    . 前台进程因为收到信号,进而引起进程退出
[hb@localhost code_test]$ cat sig.c
#include <stdio.h>
int main()
{
while(1){
printf("I am a process, I am waiting signal!\n");
sleep(1);
}
}
[hb@localhost code_test]$ ./sig
I am a process, I am waiting signal!
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C
[hb@localhost code_test]$

假设你是一个进程,而快递员代表操作系统。你的任务是一直等待快递员送信号(快递)给你。在这里,快递员相当于操作系统,会向你发送各种信号,如Ctrl-C信号。
代码中的while(1)表示你一直在等待,就像一直在家里等待快递。每秒你都打印出一条消息:“I am a process, I am waiting signal!”,表示你不断地检查是否有快递到达。
当你收到了Ctrl-C信号时(就像快递员按响了你家门铃),你会看到在终端上出现"^C",并且你的程序会终止执行。
这就是代码信号处理过程的模拟。你作为一个进程一直在等待信号,而操作系统会发送不同的信号给你,如Ctrl-C信号,你需要对这些信号做出相应的处理,比如终止程序的执行。
总结来说,代码中的进程等待信号的过程就像你一直在家等待快递的到来。当操作系统发送信号给你,你需要对信号做出相应的处理,就像按下了门铃后你会去开门签收快递。不同的信号可以触发不同的处理动作,让你的程序做出相应的反应。

🍗注意

  1. Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
  2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
  3. 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的

🍖信号处理

🧇信号异步机制

  • 什么是异步?

异步是指事件之间不需要严格的同步和等待,而是可以独立地进行处理。在异步操作中,一个事件的触发并不会导致程序的立即停顿或阻塞,而是允许程序继续执行其他任务,而后在合适的时间点再去处理该事件或结果。
异步操作通常用于处理耗时较长的任务,如网络请求、文件读写、数据库查询等。在传统的同步操作中,当执行这些耗时任务时,程序会一直等待任务完成才能继续执行后续代码。而在异步操作中,程序可以先发起这些耗时任务,然后继续执行其他代码,等待任务完成后再进行后续处理。
异步操作可以提高程序的响应性能和效率,尤其在涉及到多任务并行处理的场景下,异步操作能够更好地利用系统资源和提高系统的并发能力。
在编程中,异步操作通常通过回调函数、事件驱动机制、多线程或异步IO等方式来实现。一些编程语言和框架提供了异步编程的支持,使得开发者能够更方便地处理异步操作。

== 因为信号的产生是异步的,当一个信号产生的时候,对应的进程可能正在处理其他的更加重要的事情,那么进程可以暂时不去处理这个信号 ==

当一个信号产生时 进程可能执行的操作:

  • 🦴处理信号
  1. 默认动作
  2. 忽略
  3. 自定义函数处理
  • 🌭暂时不处理 (标记)

在操作系统中,当进程收到信号后,如果不设置忽略,操作系统会在进程的 PCB(进程控制块)中记录该信号的待处理状态。这通常通过在 PCB 中的位图(或类似的数据结构)来实现。

在 Linux 中,进程的 PCB 数据结构中有一个名为 sigpending 的位图,用于表示当前已经到达但还未处理的待处理信号。当进程收到信号但还未处理时,相应信号的位会被设置为 1。一旦进程开始处理该信号,操作系统会将对应位重新设置为 0,表示信号已经处理完成。

具体来说,sigpending 位图在 PCB 数据结构中用于存储当前进程收到但还未处理的信号。当进程收到信号时,相应信号的位会被设置为 1,表示信号已经到达。当进程准备处理信号时,会检查 sigpending 位图,找到所有待处理的信号,并依次处理它们。处理完成后,相应信号的位会被重新设置为 0,表示信号已经处理完毕。

这样,即使进程在收到信号后没有立即处理,操作系统也能够记录信号的状态,并在适当的时候通知进程处理相应的信号。这种方式实现了异步信号处理,允许进程在合适的时候处理优先级较高的信号,而不会被阻塞在处理低优先级的信号上。

🍔信号产生

🍟通过终端按键产生信号

  • man 2 signal 查看函数
    2

  • signal函数是用于在Unix/Linux系统中设置信号处理函数的函数。它允许我们指定在收到指定信号时应该执行的处理函数,从而实现对信号的处理。

  • 函数原型

#include <signal.h>

void (*signal(int signum, void (*handler)(int)))(int);

  • 🍕参数说明:
  1. signum:指定要设置处理函数的信号的编号。可以使用预定义的宏(如SIGINT、SIGTERM等),也可以使用对应的信号编号。例如,SIGINT表示用户键入Ctrl+C产生的中断信号。
  2. handler:指定要注册的信号处理函数。它是一个函数指针,指向一个形如void func(int)的函数,该函数接收一个整数参数(表示信号编号)并无返回值。
  3. 函数的返回值是一个函数指针,表示之前注册的处理函数。
  • 使用signal函数时,一般会先定义一个自定义的信号处理函数,然后通过signal函数将其注册到指定的信号上。当进程收到相应的信号时,操作系统会调用该信号处理函数来处理该信号。函数回调机制在这里体现在信号发生时,系统通过函数指针调用我们提供的处理函数。

  • 测试代码

#include <stdio.h>
#include <signal.h>

void sigHandler(int signum) {
    printf("Received signal %d\n", signum);
}

int main() {
    // 注册SIGINT信号处理函数为sigHandler
    //只是注册 当singint产生的时候才会被调用,如果不产生,就不会被调用
    signal(SIGINT, sigHandler);

    printf("Waiting for SIGINT...\n");
    while (1) {
        // 进程持续运行,等待信号发生
    }

    return 0;
}


在上述示例中,当用户在终端中按下Ctrl+C(产生SIGINT信号)时,进程会调用sigHandler函数来处理该信号,并输出"Received signal 2"(因为SIGINT的编号是2)。

注意:使用signal函数时,需要注意信号的可重入性问题。在一些情况下,建议使用更加安全可靠的sigaction函数来替代signal函数。

  • 结果
    在这里插入图片描述

🥪signal函数注意事项

signal函数可以自定义信号的处理机制,如上面所示,当在终端按下Ctrl+c(也就是2号进程)时会打印一个: Received signal 2 这就是我们的自定义行为

那么所有的信号都可以被自定义吗?
答案:不是 9号信号不可以被定义 (管理员信号 )

Linux中的9号信号是SIGKILL,也称为强制终止信号。SIGKILL用于立即终止一个进程,并且该信号无法被捕获或忽略。当进程收到SIGKILL信号时,它会立即终止,不会有任何处理和清理工作。
通常情况下,应该避免直接使用SIGKILL信号来终止进程,除非有特殊原因需要强制终止进程。因为进程没有机会进行资源清理和善后工作,可能会导致数据丢失或其他不稳定的情况。
相比之下,可以使用SIGTERM信号来通知进程进行正常退出,这样进程有机会在收到信号后进行资源释放和善后工作,保证系统的稳定性。

🥙 通过系统接口完成发送信号

  • man 2 kill
    5
    在Linux和类Unix操作系统中,kill函数用于向指定进程发送信号。它可以用来发送预定义的信号,如终止进程、中断进程、挂起进程等。

函数原型如下

#include <signal.h>

int kill(pid_t pid, int sig);

参数说明:

  1. pid: 指定目标进程的进程ID。可以是正整数表示目标进程的进程ID,也可以是负整数:
  2. 正整数:发送信号给指定进程ID的进程。
  3. 0:发送信号给当前进程组中的所有进程。
  4. -1:发送信号给系统中的所有进程,除了init进程(进程ID为1)和调用进程的父进程。
  5. 负整数:发送信号给指定进程组ID的所有进程(进程组ID为-pid)。
  6. sig: 指定要发送的信号编号。可以是预定义的信号宏,也可以是自定义的信号编号。

🧆手写一个kill命令

//手写一个kill命令
static void Usage(const std::string &proc)
{
    cerr<<"Usege:\n\t"<<proc<<"signo pid"<<endl;
}
int main(int argc,char *argv[]) {
 
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(1);
    }
    if(kill(static_cast<pid_t>(atoi(argv[2])),atoi(argv[1]))==-1) //类型转换 调用函数
    {
        //失败报错
        cerr<<"kill"<<strerror(errno)<<endl;
        exit(2);
    }


    return 0;
}

  • 代码解释:

if (argc != 3): 这行代码判断命令行参数的数量是否为3,即程序名本身和两个额外参数。如果不是3个参数,说明用户输入有误,程序没有正确使用,因此调用Usage函数输出使用方法,并通过exit(1)终止程序运行。

kill(static_cast<pid_t>(atoi(argv[2])), atoi(argv[1])): 这行代码使用kill函数向目标进程发送信号。argv[2]是第二个命令行参数,即目标进程的进程ID,通过atoi函数将字符串转换为整数,并使用static_cast<pid_t>进行类型转换,以满足kill函数的参数要求。argv[1]是第一个命令行参数,即要发送的信号编号,也通过atoi函数将字符串转换为整数。最终调用kill函数发送信号,如果发送失败,kill函数会返回-1,此时程序会输出相应的错误信息,使用cerr输出错误消息,然后通过exit(2)终止程序运行。

总的来说,这段代码用于向指定进程发送信号,并根据发送结果输出相应的错误信息,是一个简单的进程通信示例

  • 代码测试
    test
using namespace std;
#include<iostream>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    while(1)
    {
        cout<<"这是一个进程,pid是:"<<getpid()<<endl;
    }
    return 0;
}
  • 运行结果
    11
    成功~
    00

🌮由软件产生信号

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动
作是终止当前进程

这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数

0
这个程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止。

🌯硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

  • 异常捕捉
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
}
int main()
{
 //signal(2, handler); // 信号是可以被自定义捕捉的,siganl函数就是来进行信号捕捉的 
sleep(1);
//野指针使用
int *p = NULL;
*p = 100;
while(1);
 
return 0;
}

66
== Segmentation fault 对应系统第11号信号 SIGSEGV ==

🥗信号阻塞

通过前文的介绍,我们知道,当进程接收到信号后处理有三种行为:

  1. 默认行为 (由系统默认行为处理)
  2. 忽略行为(忽略不处理)
  3. 自定义行为(用户自定义函数处理)

🥘信号相关概念

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

🍝内核中的信号

02
图中我们可以看到 信号在内核中的数据结构分为有三张页表:

  1. block:是否阻塞
  2. pending:存储阻塞的信号集
  3. handler:处理该对应信号的方法
  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。
  • 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

🍜sigset_t函数

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态

🍲信号集操作函数介绍

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);

== 相当于对相关数据结构进行增删查改 ==

  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
  • 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
  • 这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

🍛sigprocmask

  • 函数原型
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

  • how:表示抑制或解除抑制信号的方式,可以取以下三个值:
  1. SIG_BLOCK:将set指向的信号集中的信号添加到进程的信号发光字中,即阻止这些信号。
  2. SIG_UNBLOCK:将set指向的信号集中的信号从进程的信号发光字中关闭,即解除对这些信号的阻塞。
  3. SIG_SETMASK:将set指向的信号集中的信号设置为进程的信号发光字,即用set中的信号集完全替换原来的信号发光字。
  • set:一个指向sigset_t类型的指针,用于指定需要阻塞或解除阻塞的信号集。
  • oldset:一个指向sigset_t类型的指针,用于保存之前的信号提示字。如果之前不为NULL,oldset指向的信号集将被填充为调用sigprocmask之前的信号提示字
  • 实例代码
#include <signal.h>

int main() {
    sigset_t new_mask, old_mask;

    // 初始化新的信号屏蔽字,屏蔽SIGINT和SIGQUIT信号
    sigemptyset(&new_mask);
    sigaddset(&new_mask, SIGINT);
    sigaddset(&new_mask, SIGQUIT);

    // 阻塞新的信号屏蔽字,并保存旧的信号屏蔽字
    if (sigprocmask(SIG_BLOCK, &new_mask, &old_mask) == -1) {
        perror("sigprocmask");
        return 1;
    }

    // 这里的代码执行时,SIGINT和SIGQUIT信号会被阻塞

    // 解除对SIGINT信号的阻塞,保持SIGQUIT信号仍然被阻塞
    sigdelset(&new_mask, SIGINT);
    if (sigprocmask(SIG_SETMASK, &new_mask, NULL) == -1) {
        perror("sigprocmask");
        return 1;
    }

    // 这里的代码执行时,只有SIGQUIT信号会被阻塞,SIGINT信号会被接收

    return 0;
}

在示例中,首先使用sigprocmask函数将SIGINT和SIGQUIT信号添加到进程的信号提示字中,然后解除对SIGINT信号的阻塞,保持SIGQUIT信号仍然被上述阻塞。这样,在不同阶段执行代码时,就会根据信号信号字的设置决定是否接收相应的信号。

🍣sigpending函数

sigpending是一个系统调用,用于获取当前被阻塞的未决信号集,即当前进程接收但尚未处理的被阻塞信号集合。

函数原型为:

int sigpending(sigset_t *set);

set:一个指向sigset_t类型的指针,用于存储
调用sigpending函数后,被阻塞的未决信号集将会被填充到set指向的sigset_t类型的信号中。如果进程没有设置信号提示字或者没有未决信号,则set中的信号集将会被清空。

使用示例:

#include <stdio.h>
#include <signal.h>

int main() {
    sigset_t blocked_set;

    // 创建一个信号集,并将SIGINT信号添加到信号集中
    sigemptyset(&blocked_set);
    sigaddset(&blocked_set, SIGINT);

    // 设置信号屏蔽字,阻塞SIGINT信号
    if (sigprocmask(SIG_BLOCK, &blocked_set, NULL) == -1) {
        perror("sigprocmask");
        return 1;
    }

    printf("Waiting for SIGINT signal...\n");

    // 这里可以做一些其他的工作

    // 获取被阻塞的未决信号集
    sigset_t pending_set;
    if (sigpending(&pending_set) == -1) {
        perror("sigpending");
        return 1;
    }

    // 检查是否有SIGINT信号在未决信号集中
    if (sigismember(&pending_set, SIGINT)) {
        printf("Received SIGINT signal.\n");
    } else {
        printf("SIGINT signal is not pending.\n");
    }

    return 0;
}

在示例中,首先设置信号提示字,阻止SIGINT信号。然后获取被阻塞的未决信号集,并检查其中是否有SIGINT信号。如果有,则表示进程收到了上述但开始处理的SIGINT信号,否则表示该信号不在信号集中

🍱信号捕捉

信号捕捉是指程序在运行过程中,当系统接收到特定的信号时,可以指定一个信号处理函数来处理该信号。整个信号捕捉的过程涉及到内核态和用户态之间的切换。

内核态:当系统接收到一个信号时,内核负责处理该信号。首先,内核会在进程控制块(PCB)中设置相应的信号标志位,表示有特定信号待处理。然后,内核会检查该进程是否有对应信号的信号处理函数注册(通过sigaction等函数注册的处理函数),如果有,就会将信号处理函数的地址存储到进程的PCB中。

用户态:当程序运行在用户态时,它会不断地执行自己的指令,进行正常的业务逻辑。但是,当系统接收到一个待处理的信号,内核会介入,并且在下次切换到用户态运行时,会检查进程的PCB,发现有待处理的信号。这时,内核会终止当前指令的执行,将程序从用户态切换到内核态(上下文切换),跳转到相应的信号处理函数。

信号处理函数:在信号处理函数中,程序可以根据信号的类型和需要采取相应的处理措施,例如终止进程、忽略信号、捕捉信号并处理、或者执行默认动作等。处理完信号后,内核会再次将控制权交还给用户态,继续执行原来的指令。

需要注意的是,信号处理函数是异步执行的,即信号的产生和处理是相互独立的,而不会影响进程当前正在执行的指令。因此,在信号处理函数中应该尽量避免执行复杂或耗时的操作,以免影响正常业务逻辑。

总结来说,信号捕捉的过程涉及到内核态和用户态之间的切换。当系统接收到特定的信号时,内核会将信号处理函数的地址存储到进程的PCB中,并在适当的时候切换到信号处理函数来处理该信号,然后再切换回用户态继续执行程序。这种机制使得程序能够对不同的信号做出及时响应和处理。

  • 一张图描述该过程
    22

🥟sigaction函数

sigaction是一个用于设置信号处理函数的系统调用。它提供了更强大和灵活的信号处理方式,相比较signal函数,使用sigaction可以避免一些信号处理函数设置上的限制

#include <signal.h>

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

  • 参数说明:
  1. signum:要设置处理函数的信号编号,如SIGINT、SIGTERM等。
  2. act:指向一个struct sigaction结构体,其中包含新的信号处理函数和处理标志等信息。
  3. oldact:指向一个struct sigaction结构体,用于保存原有的信号处理函数和处理标志。
  • struct sigaction结构体定义如下
struct sigaction {
    void (*sa_handler)(int);      // 信号处理函数指针
    void (*sa_sigaction)(int, siginfo_t *, void *); // 信号处理函数指针(高级信号处理)
    sigset_t sa_mask;             // 指定在信号处理期间要屏蔽的信号集合
    int sa_flags;                 // 指定信号处理的行为选项
    void (*sa_restorer)(void);    // 已经被废弃,通常置为NULL
};

使用sigaction可以更加精确地控制信号处理行为。它支持两种类型的信号处理函数:sa_handler和sa_sigaction。sa_handler是一个普通的信号处理函数,只接收一个整数参数表示信号编号;而sa_sigaction是一个高级信号处理函数,它接收三个参数,分别是信号编号、指向siginfo_t结构体的指针和指向ucontext_t结构体的指针,提供了更多关于信号的信息。

此外,sa_mask字段允许指定在信号处理期间要屏蔽的其他信号,这样可以避免处理函数被其他信号中断。sa_flags字段用于指定信号处理的行为选项,例如设置SA_RESTART可以使被信号中断的系统调用自动重新启动。

总体来说,sigaction函数提供了更为灵活和可靠的信号处理方式,适用于更复杂的信号处理需求

  • sigactiom测试代码
#include <iostream>
#include <csignal>
#include <unistd.h>

void signalHandler(int signum) {
    std::cout << "Received signal: " << signum << std::endl;
    std::cout << "Exiting..." << std::endl;
    exit(signum);
}

int main() {
    struct sigaction sa;
    sa.sa_handler = signalHandler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    // 注册信号处理函数
    if (sigaction(SIGINT, &sa, nullptr) == -1) {
        std::cerr << "sigaction error" << std::endl;
        return 1;
    }

    std::cout << "Press Ctrl+C to trigger the signal..." << std::endl;

    // 进入一个无限循环
    while (true) {
        // 假装在做一些其他的事情
        sleep(1);
    }

    return 0;
}

  • 结果
    00
    在运行这个程序后,当你按下Ctrl+C时,会触发SIGINT信号,然后信号处理函数signalHandler会被调用,输出提示信息并退出程序。这个案例演示了如何使用sigaction函数来捕捉信号,并在收到信号时执行相应的处理操作。

🦪可重入函数

03

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?

  • 可重入函数的条件
  1. 不使用全局变量:可重入函数不能使用全局变量,因为全局变量在多线程环境下可能会导致竞争条件和数据不一致性。
  2. 不使用静态变量:类似全局变量,静态变量也可能引起多线程竞争的问题。
  3. 不使用动态内存分配:在多线程环境下,动态内存分配可能导致内存泄漏或竞争条件。
  4. 不调用不可重入函数:可重入函数本身不能调用不可重入函数,否则可能导致不正确的结果

🍤volatile函数

在C/C++编程中,volatile 是一个关键字,用于告诉编译器该变量可能在程序执行过程中被意外地修改,从而== 防止编译器对该变量进行一些优化操作 ==。通常情况下,编译器会对变量进行优化,例如使用寄存器存储变量的值,或者在代码中缓存变量值以提高性能。然而,有些变量可能由于硬件或其他线程/进程的操作而在编译器无法预测的时刻被修改,此时就需要使用 volatile 关键字来防止编译器的优化。

  • volatile 关键字的主要作用是:
  1. 禁止编译器对变量的优化:使用 volatile 关键字告诉编译器,该变量的值可能在编译器无法预测的时刻被修改,因此编译器不应该对该变量进行优化,而是每次都从内存中读取最新的值。
  2. 防止编译器缓存变量的值:编译器通常会将变量的值缓存到寄存器或者其他地方,这样在使用变量的时候可以更快地访问。但是对于被声明为 volatile 的变量,编译器不会缓存其值,而是每次都直接读取内存中的值,从而保证变量的实时性。
  • 需要注意的是,volatile 关键字并不能保证多线程的正确性或避免并发问题。它只能保证变量在被意外修改的情况下能够及时地更新值,但是并不能解决线程同步的问题。在多线程环境下,如果有多个线程同时访问一个变量,并且其中有线程对该变量进行写操作,那么还是需要使用其他同步手段来保证线程安全性,如互斥锁(Mutex)或原子操作等。

  • 总结起来,volatile 关键字主要用于标记那些在程序执行过程中可能被意外修改的变量,防止编译器对其进行优化,从而确保每次都从内存中读取最新的值。

  • 测试代码

#include <iostream>
#include <csignal>

volatile int globalVar = 0;

void signalHandler(int sig) {
    globalVar = 1;
}

int main() {
    std::signal(SIGINT, signalHandler); // 注册信号处理函数

    while (globalVar == 0) {
        // 在这里可以做一些其他的工作
        std::cout << "Waiting for signal..." << std::endl;
    }

    std::cout << "Received signal! Exiting..." << std::endl;

    return 0;
}

在上面的代码中,我们定义了一个全局变量 globalVar 并使用 volatile 关键字来标记它。然后,我们注册了一个信号处理函数 signalHandler,当程序收到 SIGINT 信号(即用户按下 Ctrl+C)时,将会调用该处理函数。

在主函数中,我们进入一个循环,不断检查 globalVar 的值是否为 0。由于 globalVar 被标记为 volatile,编译器会每次从内存中读取最新的值,而不会对其进行优化。因此,即使 globalVar 在信号处理函数中被修改,主函数仍然能够及时地读取到修改后的值,从而退出循环。

请注意,上面的代码仅用于演示 volatile 关键字的用法,并没有处理多线程并发等情况。在真实的多线程环境中,需要使用更多的同步手段来确保线程安全性。

00
创作不易,点赞支持,拒绝白嫖~
🍙 🍚 🍘 🍥 🥠 🥮 🍢 🍡 🍧 🍨 🍦 🥧 🧁 🍰 🎂 🍮 🍭 🍬 🍫 🍿

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值