【Linux:进程间信号】

news2024/12/23 0:24:18

文章目录

  • 1 生活角度的信号
  • 2 技术应用角度的信号
  • 3 信号的产生
    • 3.1 由系统调用向进程发信号
      • 3.1.1 `signal`
      • 3.1.2 `kill`
      • 3.1.3 `raise`
    • 3.2 由软件条件产生信号
    • 3.3 硬件异常产生信号
    • 3.4 通过终端按键产生信号
    • 3.5 总结思考一下
  • 4 信号的保存
    • 4.1信号其他相关常见概念
    • 4.2在内核中的表示
    • 4.3 sigset_t
    • 4.4信号集操作函数
    • 4.5 sigprocmask
    • 4.6 sigpending
  • 5 信号的处理
    • 5.1 问题引入
    • 5.2内核态和用户态
    • 5.3 信号的捕捉
    • 5.4 sigaction
  • 6 可重入函数
  • 7 volatile


1 生活角度的信号

你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”,当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取。在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话。


2 技术应用角度的信号

我们之前提到过,可以通过kill -l 命令来查看信号:

[grm@VM-8-12-centos lesson16]$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

其中我们将标号34以上的叫做实时信号 ,本文章不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明 man 7 signal每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到。

我们之前讲过的,当显示屏中不断有数据在刷屏时我们可以用ctrl+c来终止进程,其本质用户按下ctrl+c ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出。但是我们也提过前台进程可以用ctrl+c终止前台进程,但是却不能终止后台进程,我们将可执行程序运行时加上&就能够变成后台进程。此时我们只有通过kill -9命令来杀死进程。
注意:

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

3 信号的产生

3.1 由系统调用向进程发信号

3.1.1 signal

其实signal本质上来说并不是向进程发送信号,而是在收到了信号后我们用户自定义处理信号的方式,我们上面提到过,信号的处理方式有三种:

  • 执行默认动作
  • 执行自定义动作
  • 忽略信号

首先我们来看看signal的介绍:
Alt
我们观察参数,发现其中有一个函数指针,这个函数指针是由我们自己实现的,也就是当我们收到特定的信号时我们自定义的采取怎样的方式去处理。
我们可以写代码来验证一下:

signal.cc:

#include<iostream>
#include<string>
#include<signal.h>
#include<unistd.h>
using namespace std;

void hander(int signo)
{
    cout<<"get a signal:"<<signo<<endl;
    exit(2);
}
int main()
{
    signal(2,hander);

    while(1)
    {
        cout<<"my pid is:"<<getpid()<<endl;
        sleep(1);
    }

    return 0;
}

当我们在键盘上敲下kill -9 进程pid时我们可以观察到下面现象:

这是由于我们自定义了信号的处理方式,当进程收到了2号信号时就不会执行默认2号信号的处理方式,而是执行了我们自己定义的处理方式(用函数指针实现)
注意:signal(2, handler)调用完这个函数的时候,hander方法被调用了吗?
答案是没有的,是接受到2号信号后才回调这个hander方法。

3.1.2 kill

int kill(pid_t pid, int signo);

这个函数的使用很简单,通过参数的命名我们就能够知道如何给指定进程发送信号。
那我们可以自己实现一个mykill:

#include <iostream>
#include <cstring>
#include <cerrno>
#include <string>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
void Usage(string proc)
{
    std::cout << "\tUsage: \n\t";
    std::cout << proc << " 信号编号 目标进程\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }
    int sig = stoi(argv[1]);
    int pid = stoi(argv[2]);
    int n = kill(pid, sig);
    return 0;
}

其实很好理解,杀死目标进程我们是创建了新进程来处理。

3.1.3 raise

int raise(int signo);

在这里插入图片描述kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
除此之外还有一个C语言的abort函数(使当前进程接收到信号而异常终止)

#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。


3.2 由软件条件产生信号

SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了。本节主要介绍alarm函数 和SIGALRM信号

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

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

int cnt=1;
void hander(int sigo )
{
    cout<<cnt<<endl;
    exit(1);
}
int main()
{
  
    signal(14,hander);
    alarm(1);
    while(true)
    {
      cout<<cnt++<<endl;
    }
 
    return 0;
}

这个程序的作用是统计出1秒中cnt累加的次数,我们运行起来看看:
在这里插入图片描述
当我们修改代码,不要加上输出语句再试试:
在这里插入图片描述
我们发现第二次的cnt累加次数明显远大于第一次的值,这其实也是很好理解的因为IO很慢


3.3 硬件异常产生信号

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

#include<iostream>
#include<signal.h>
using namespace std;

void hander(int sign)
{
    cout<<"初零错误"<<endl;
}

int main()
{
    signal(SIGFPE,hander);
    int a=10;
    cout<<a/0<<endl;
    return 0;
}

当我们运行时:
在这里插入图片描述
为什么会一直在重复打印呢?原因是在于我们自定义8号信号时执行但是没有退出程序,而除零错误·一直存在,所以会一直在显示屏上刷屏。野指针问题也类似。


3.4 通过终端按键产生信号

SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,现在我们来验证一下。
什么是Core Dump?

当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K: ulimit -c 1024.

那我们究竟如何查看当前进程的资源上限呢?
我们可以使用ulimit -a:
在这里插入图片描述
我们发现在云服务器上默认是关闭了核心转储文件的,如果我们想设置可以使用命令:
ulimit -c XXX来自定义的设置好核心转储文件大小。
在这里插入图片描述
我们之前通过man 7 signal命令查看时:
在这里插入图片描述
不难发现有些信号是带有Core的,Term的默认执行动作是终止进程,没有其他动作;而Core是先会进行核心转储,再终止进程的。
我们可以来试试:
当我们写了一个除零错误的代码时让其自定义执行默认动作时:
在这里插入图片描述
就会生成一个core.xxx的文件,这个文件就是核心转储文件,那么这个文件有啥用呢?
我们在用gdb调试时可以使用core-file core.xxx来帮助我们快速定位到错误所在,但是当我们没重复运行一次时就会重新生成一个core文件,所以云服务器上是默认关闭掉CoreDump文件。不知道大家忘记了没有,我们在讲解进程控制时就已经提过了一个Core Dump标志:
在这里插入图片描述是否具有core dump是由这个标志位所决定的,这个标志位的获取方法有很多种,我这里给出一种:
(status<<7)&1


3.5 总结思考一下

  • 上面所说的所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者.
  • 信号的处理是否是立即处理的?在合适的时候.
  • 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
  • 一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
  • 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

4 信号的保存

4.1信号其他相关常见概念

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

4.2在内核中的表示

在这里插入图片描述

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

4.3 sigset_t

从上图来看,每个信号只有一个bit的未决标志,01,不记录该信号产生了多少次,阻塞标志也是这样表示的。
因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号
的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有
效”和“无效”的含义是该信号是否处于未决状态。下面将详细介绍信号集的各种操作。 阻塞信号集也叫做当
前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

4.4信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印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。


4.5 sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
在这里插入图片描述

4.6 sigpending

#include <signal.h>
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

现在我们可以用刚才介绍的函数做实验:实验内容为屏蔽2号信号,对2号信号进行自定义捕捉,获取pending信号集并打印,过一段时间后解除对2号信号的屏蔽,再次打印pending信号集。

代码实现:

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;


void printSigpending(sigset_t& pending)
{
    for(int i=1;i<=31;++i)
    {
        if(sigismember(&pending,i)) cout<<"1";
        else cout<<"0";
    }
    cout<<endl;
}

void hander(int signo)
{
    cout<<"执行对"<<signo<<"号信号的自定义捕捉动作"<<endl;
}

int main()
{
    signal(2,hander);

    sigset_t set,oset;
    sigemptyset(&set);
    sigemptyset(&oset);
    sigaddset(&set,2);

    sigprocmask(SIG_SETMASK,&set,&oset);
    int cnt=0;
    while(true)
    {
        sigset_t pending;
        sigemptyset(&pending);

        int n=sigpending(&pending);
        printSigpending(pending);
        sleep(1);

      
        if(cnt++==10)
        {
            cout<<"解除对2号信号的屏蔽"<<endl;
            sigprocmask(SIG_SETMASK,&oset,nullptr);
        }
    }
    return 0;
}

效果展示:
在这里插入图片描述

我们不难从上面发现当我们发送了多次2号信号并且2号信号处于屏蔽状态时只会保留一次。
此时我们想终止该进程时可以用ctrl+\

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。


5 信号的处理

5.1 问题引入

我们之前讲解过,信号可以不是被立即处理的,而是在一个合适的时候,这个合适的时候是什么时候呢?
当进程从内核态转换到用户态的时候会在OS的指导下进行信号的检测。
那什么是用户态,什么是内核态呢?


5.2内核态和用户态

先用通俗易懂语言来描述下:
用户态:当执行用户自己的代码时,进程所处于的状态;
内核态:当执行OS的代码时,进程所处于的状态。
那么在哪些情况下进程会从用户态转换到内核态呢?

进程的时间片到了,需要进行进程的切换时;
进行系统调用等。

我们还可以从地址空间上来理解,不知道大家忘记了下面这张图片了没有?
在这里插入图片描述

在32位的地址下,内存有4GB大小,其中【0,3】是用户空间,【3,4】是内核空间。执行用户空间的代码的进程就处于用户态,执行内核空间的代码的进程就处于内核态。
在用户空间中每一个进程都有一张用户级别的页表,用户级别的页表在不同的进程下有可能是不相同的;除此之外每一个进程还会为内核空间分配一个内核级别的页表,而不同的进程的内核级页表是相同的,所以不同进程就看到了同一份页表。所以OS运行的本质是在进程的地址空间上运行的,当我们调用OS的代码是直接在进程地址空间进行跳转即可。

那么问题来了,OS是如何判别用户态和内核态呢?
在CPU中有一种叫做CR3的寄存器,寄存器中3表示进程处于用户态0表示进程处于内核态

所以我们现在可以解释一下进程调度的过程:OS时钟硬件每隔一段时间都会给OS发送时钟中断,OS会执行对应中断的处理方法(检测时间片),然后将进程的上下文进程保存和切换,选择合适的进程,OS处理时采用的是schedule函数。


5.3 信号的捕捉

当我们自定义了信号的捕捉方式时,整个过程中进程从用户态到内核态的转换:

在这里插入图片描述

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态而不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

这时我们可能就会有一些小问题:当执行了某种信号时,pending位图是在hander方法处理完之前还是处理完之后被置为0的呢?
这个问题很好验证,我们只需要在自定义捕捉时打印一下即可,我们放在sigaction一起验证。


5.4 sigaction

我们可以查一下man手册:
在这里插入图片描述

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
  • sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:

  • 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数。这里就不在过于多说,感兴趣的同学可以自己去查询。

我们可以写代码来验证下:

static void PrintPending(const sigset_t &pending)
{
    cout << "当前进程的pending位图: ";
    for(int signo = 1; signo <= 31; signo++)
    {
        if(sigismember(&pending, signo)) cout << "1";
        else cout << "0";
    }
    cout << "\n";
}

static void handler(int signo)
{
    cout << "对特定信号:"<< signo << "执行捕捉动作" << endl;
    int cnt = 30;
    while(cnt)
    {
        cnt--;

        sigset_t pending;
        sigemptyset(&pending); // 不是必须的
        sigpending(&pending);
        PrintPending(pending);
        sleep(1);
    }
}

int main()
{
    struct sigaction act, oldact;
    memset(&act, 0, sizeof(act));
    memset(&oldact, 0, sizeof(oldact));
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);

    sigaddset(&act.sa_mask,3);
    sigaddset(&act.sa_mask,4);
    sigaddset(&act.sa_mask,5);


    sigaction(2, &act, &oldact);


    while(true)
    {
        cout << getpid() << endl;
        sleep(1);
    }
}

效果展示:
在这里插入图片描述
通过上面的演示我们可以验证pending位图是在hander方法处理完之前就已经清0了


6 可重入函数

首先我们先来看看这样一种场景:
在这里插入图片描述当我们执行其中一个进程代码的insert方法时,执行了第一行代码后该进程的时间片到了,调用了另外进程同样也执行了insert方法的第一行代码,然后切回到最开始的进程执行第二行代码,此时就造成了node2的内存泄漏,而这种函数就叫做不可重入函数
反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。但是使用局部变量就不会造成上面的混乱问题。

如果一个函数符合以下条件之一则是不可重入的:

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

7 volatile

这个关键字我们在C语言时就已经涉猎过,现在我们在从系统的角度再来理解一下。
我们先来看这样一段代码:

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;

int g_val=1;
void hander(int signo)
{
    cout<<"g_val form 1 to 0"<<endl;
    g_val=0;
    cout<<g_val<<endl;
}

int main()
{
    signal(2,hander);

    while(g_val);
    cout<<" success quit"<<endl;
    return 0;
} 

Makefile中代码的编译我们加了O2优化
在这里插入图片描述

当我们运行时:
在这里插入图片描述
我们发现当我们在终端下一直敲Ctrl+C时程序并不会运行,最后敲了Ctrl+\才退出,为什么呢?
这是由于编译器做了优化,它是怎么优化的呢?
我们知道编译器取数据都是到内存上面去取到的,当编译器识别到变量g_val时,由于在主函数里面没有直接修改该变量的值,所以编译器认为你没有修改这个变量,那我就将变量放到寄存器中,我们使用该变量时就不用在到内存上面去取了,直接在寄存器中取出数据即可,而寄存器中的数据一直保存的是1,所以该程序就死循环出不来了。
有什么办法去掉编译器的优化吗?我们可以加volatile关键字在变量前面,这就告诉编译器不要做优化了,也就是不要将该变量放在寄存器中,每次你取数据时还是老老实实到内存上面来取,这样做可不可行呢?我们接下来试试:
在这里插入图片描述
运行结果:
在这里插入图片描述
我们可以观察到这种方式是可行的。
其实不仅仅在Linux上,在VS中也会出现这样的优化,来看看下面的代码:
在这里插入图片描述
大家可以猜猜结果:
我们来调试一下:
在这里插入图片描述在内存窗口中发现他们都是20,但是当我们看打印结果时就傻眼了:
在这里插入图片描述打印结果居然是10和20,这里其实也是编译器做了优化,编译器认为既然你a加了const属性,那么我就认为你不可以直接修改,就直接把变量a放在了寄存器中,所以在打印结果中我们看到的是10而内存窗口看到的是20,我们加了volatile关键字后看到的结果都为20了:
在这里插入图片描述


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/618524.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何评价 Facebook 发布的数字货币 Libra?

一句话总结&#xff1a;Libra 最大的亮点&#xff0c;在于它是 Facebook 做的。 随着数字货币市场的迅速发展&#xff0c;各类加密货币层出不穷。然而&#xff0c;在这个领域中&#xff0c;Facebook 所推出的 Libra 显得尤为引人关注。那么&#xff0c;Libra 到底有何特点&…

专家助阵!IoTDB X EMQ 智慧工厂主题 Meetup 讲师曝光!

期待已久的智慧工厂主题 Meetup 活动将在 4 天后线下线上同步举办&#xff01; 天谋科技将联手 EMQ&#xff0c;通过数据基础设施平台的核心技术与实践经验分享&#xff0c;提供流程协同、运营提效、生产质量保障等智能制造目标的可行方案。快来在推文结尾预约直播&#xff0c;…

【大数据工具】Flink集群搭建

Flink 集群安装 1. 单机版 Flink 安装与使用 1、下载 Flink 安装包并上传至服务器 下载 flink-1.10.1-bin-scala_2.11.tgz 并上传至 Hadoop0 /software 下 2、解压 [roothadoop0 software]# tar -zxvf flink-1.10.1-bin-scala_2.11.tgz3、创建快捷方式 [roothadoop0 soft…

100天精通Python(可视化篇)——第90天:Pyecharts可视化神器基础入门

文章目录 专栏导读一、pyecharts 介绍1. 简介2. 版本说明 二、pyecharts 特点三、pyecharts 安装四、基本步骤五、快速开始1. 数据准备1&#xff09;类别数据2&#xff09;时间数据3&#xff09;颜色数据4&#xff09;地理数据5&#xff09;世界人口数据6&#xff09;选择数据7…

Netty核心源码剖析(三)

1.Pipeline,Handler和HandlerContext创建源码剖析 1.1.三者的关系 1>.每当ServerSocket创建一个新的连接,就会创建一个Socket,对应的就是目标客户端; 2>.每一个新创建的Socket都将会分配一个全新的ChannelPipeline(以下简称pipeline); 3>.每一个ChannelPipeline内…

LKY_OfficeTools 一键优雅的安装并激活你的Office

何为优雅&#xff1f; 说到Office办公软件 相信都不陌生&#xff0c;一般包括Word、Excel、PowerPoint默认三件套&#xff0c;和Outlook、OneNote、Access。 几乎每台电脑都会配置的 但大多数的情况下 都是先去软件仓库下载 Office 然后使用激活工具去激活Office 这种操作听起…

国内首款医疗大语言模型MedGPT发布,专业医疗标注数据成关键

5月25日&#xff0c;国内互联网医院、慢病管理平台医联今日正式发布了自主研发的基于Transformer架构的国内首款医疗大语言模型——MedGPT。 与通用型的大语言模型产品不同&#xff0c;MedGPT主要致力于在真实医疗场景中发挥实际诊疗价值&#xff0c;实现从疾病预防、诊断、治疗…

机器学习 day14 ( 神经网络,计算机视觉中的引用:人脸识别和汽车识别)

神经网络的发展 最开始的动机&#xff1a;是通过构建软件来模拟大脑&#xff0c;但今天的神经网络几乎与大脑的学习方式无关 我们依据大脑中的神经网络&#xff0c;来构建人工神经网络模型。左图中&#xff1a;一个神经元可以看作一个处理单元&#xff0c;它有很多的输入/树突…

图论与算法(6)最小生成树

1. 带权图及实现 1.1 带全图概述 带权图是一种图形结构&#xff0c;其中图中的边具有权重或成本。每条边连接两个顶点&#xff0c;并且具有一个与之关联的权重值&#xff0c;表示了两个顶点之间的某种度量、距离或成本。 带权图可以用邻接矩阵或邻接表来表示。邻接矩阵是一个…

集成电路(芯片)中VCC、VDD、VSS、GND和AGND等概念

IC芯片 Integrated Circuit Chip 即集成电路芯片&#xff0c;是将大量的微电子元器件(晶体管、电阻、电容、二极管等) 形成的集成电路放在一块塑基上&#xff0c;做成一块芯片。目前几乎所有看到的芯片&#xff0c;都可以叫做 IC芯片 。 SOP与DIP SOP(Small Outline Package…

浅谈备考 系统架构师

这里写自定义目录标题 准备步骤考试形式考试内容学习考试内容训练考试内容其他觉得好的同类参考资料2023年度计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试工作计划 第一次产生萌芽的时候三年前&#xff0c;当初备考没有想过要评职称或者成为什么人才&#xf…

antd3和dva-自定义组件初始化值的操作演示和自定义组件校验

前言 在antd3 (react)版和dva下,好像有的项目使用的是getFieldDecorator来获取表单的值的,现在就遇到了一个问题,getFieldDecorator针对antd自带的组件实现效果很好,除去一个form.item只能有一个getFieldDecorator的限制,其他都很好用,但是假如是自定义组件或者说在getFieldDec…

Linux内存管理7——深入理解 slab cache 内存分配全链路实现

1. slab cache 如何分配内存 当我们使用 fork() 系统调用创建进程的时候&#xff0c;内核需要为进程创建 task_struct 结构&#xff0c;struct task_struct 是内核中的核心数据结构&#xff0c;当然也会有专属的 slab cache 来进行管理&#xff0c;task_struct 专属的 slab cac…

iperf3使用

目录 写在前面&#xff1a;带宽和吞吐量安装使用测试TCP吞吐量测试UDP吞吐量测试上下行带宽&#xff08;TCP双向传输&#xff09;测试多线程TCP吞吐量测试上下行带宽&#xff08;UDP双向传输&#xff09;测试多线程UDP吞吐量 iperf3常用参数通用参数server端参数client端参数 i…

一种星载系统软件定义平台的设计与实现.v3

摘要 针对星载综合射频开放式系统架构&#xff0c;为了在软件综合层面上实现波形应用软件与具体平台的解耦&#xff0c;设计并实现了一种基于软件通信架构&#xff08;Software Communication Architecture, SCA&#xff09;的软件平台及其环境工具。通过解决星载平台软件的分…

linuxOPS基础_linux自有服务systemctl

自有服务概述 ​ 服务是一些特定的进程&#xff0c;自有服务就是系统开机后就自动运行的一些进程&#xff0c;一旦客户发出请求&#xff0c;这些进程就自动为他们提供服务&#xff0c;windows系统中&#xff0c;把这些自动运行的进程&#xff0c;称为"服务" ​ 举例…

总结888

学习目标&#xff1a; 月目标&#xff1a;6月&#xff08;线性代数强化9讲2遍&#xff0c;背诵15篇短文&#xff0c;考研核心词过三遍&#xff09; 周目标&#xff1a;线性代数强化1讲&#xff0c;英语背3篇文章并回诵&#xff0c;检测 每日必复习&#xff08;5分钟&#xff…

Java 基础第八章: 接口、内部类、包装类

参考资料 &#xff1a;康师傅的视频课 方法 、 有继承的代码块的加载顺序&#xff1a;先执行父类的静态代码块、子类的静态代码块&#xff1b;然后&#xff0c;执行父类的普通代码块和构造器 子类的的普通代码块和构造器&#xff1b; 总结&#xff1a;由父到子&#xff0c;静…

【Web服务器】Nginx之Rewrite与location的用法

文章目录 前言一、正则表达式1. Nginx 的正则表达式2. 正则表达的优势3. Nginx 使用正则的作用 二、location 的概念1. location 和 rewrite 区别2. location 匹配的分类3. location 常用的匹配规则3.1 location 匹配优先级3.2 location 匹配的实例3.3 实际网站规则定义第一个必…

深度学习应用篇-计算机视觉-图像分类[2]:LeNet、AlexNet、VGG、GoogleNet、DarkNet模型结构、实现、模型特点详细介绍

【深度学习入门到进阶】必看系列&#xff0c;含激活函数、优化策略、损失函数、模型调优、归一化算法、卷积模型、序列模型、预训练模型、对抗神经网络等 专栏详细介绍&#xff1a;【深度学习入门到进阶】必看系列&#xff0c;含激活函数、优化策略、损失函数、模型调优、归一化…