【Linux:进程间信号】

文章目录

  • 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了:
在这里插入图片描述


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

命令行创建uniapp项目

命令行创建uniapp项目 除了使用HBuilderX工具可视化搭建项目外&#xff0c;DCloud官方还提供了一个脚手架用于命令行搭建项目。 环境安装 全局安装vue-cli npm i vue/cli4 -g建议使用vue-cli 4.x版本&#xff0c;vue-cli 5.x与webpack存在冲突&#xff0c;会导致运行报错 …

【软件测试】测试用例设计要点总结

文章目录 考试题型简答题(一) 等价类划分1.1 划分等价类1.2 设计测试用例 (二) 边界值分析2.1 列出边界值分析表2.2 设计测试用例 (三) 因果图分析3.1 确定原因和结果3.2 确定原因和结果之间的逻辑关系3.3 在因果图上使用标准的符号标明约束条件 (四) 判定表驱动4.1 将因果图转…

如何利用ChatGPT写毕业论文

如何利用ChatGPT写毕业论文 ChatGPT是什么&#xff1f;利用ChatGPT写毕业论文的步骤1.准备数据2.训练模型3.生成论文4.检查论文 总结地址 ChatGPT是什么&#xff1f; ChatGPT是一个基于GPT-2模型的开源聊天机器人&#xff0c;它可以回答用户的问题&#xff0c;进行闲聊和提供各…

【头歌-Python】9.2 能带曲线绘制(project) 第1~3关

第1关&#xff1a;能带曲线绘制一 任务描述 本关任务&#xff1a;使用matplotlib绘制图形。 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a; 使用 matplotlib 绘制图形python 读取文件 python 读取文件 python读取文件可以用以下函数实现&#xff1a; # …

Java学习笔记(视频:韩顺平老师)4.0

如果你喜欢这篇文章的话&#xff0c;请给作者点赞哟&#xff0c;你的支持是我不断前进的动力。 因为作者能力水平有限&#xff0c;欢迎各位大佬指导。 目录 如果你喜欢这篇文章的话&#xff0c;请给作者点赞哟&#xff0c;你的支持是我不断前进的动力。 控制结构 顺序 分…

用了【WRITE-BUG数字空间】,其他文档软件可以卸载、注销账号了

都3202年了文档都进化成在线协同编辑文档了 让我看看谁还在用本地软件写文档啊~滋滋滋 使用【WRITE-BUG数字空间】云文档全键盘写作不是梦&#xff01;铁汁&#xff0c;听我句劝&#xff0c;把本地软件卸载了奥&#xff0c;你把握不住~ 程序员兄弟姐妹们的最爱编辑器&#x…

TCP协议流程详解,抓包分析

目录 TCP概念TCP工作层TCP协议头部解析TCP抓包解析TCP三次握手&#xff0c;数据收发&#xff0c;四次挥手抓包TCP状态迁移 TCP概念 传输控制协议&#xff08;TCP&#xff0c;Transmission Control Protocol&#xff09;是一种面向连接的、可靠的、基于字节流的传输层通信协议&…

【2023年计划大纲】2023年技术笔记大纲

2023年技术笔记写作计划 按照技术类型&#xff0c;计划写以下三个领域的笔记&#xff1a; &#xff08;1&#xff09;AUTOSAR标准体系的专业知识&#xff0c;T-BOX,BMS,VCU这几个产品的设计和核心开发。包括UDS协议&#xff0c;XCP协议&#xff0c;OS操作系统。 每一个产品和…

Spring Cloud Kubernetes配置使用详情

目录 一、 为什么你需要 Spring Cloud Kubernetes&#xff1f; 二、 Starter 三、 用于 Kubernetes 的 DiscoveryClient 四、Kubernetes 原生服务发现&#xff08;service discovery&#xff09; 五、Kubernetes PropertySource 的实现 1、使用 ConfigMap PropertySource …

【HTML】form标签

<form> 标签用于创建 HTML 表单&#xff0c;它是用于收集用户输入的重要元素。表单可以包含各种输入字段、按钮和其他交互元素&#xff0c;用于向服务器发送用户输入数据。 下面是一个简单的 <form> 标签的示例&#xff1a; <form action"/submit-form&q…

释放 OpenAI 和 ESP-BOX 的力量:ChatGPT 与乐鑫 SoC 融合指南

当前&#xff0c;我们正见证着一场技术革命&#xff0c;而 OpenAI 正处于这场变革的最前沿。其中最激动人心的创新之一就是ChatGPT&#xff0c;它运用自然语言处理的力量&#xff0c;打造出更加引人入胜、直观的用户体验。而将 OpenAI 的 API 与物联网设备相结合&#xff0c;更…

Redis集群部署

Redis集群部署 1.单机安装Redis2.Redis主从集群2.1.集群结构2.2.准备实例和配置2.3.启动2.4.开启主从关系2.5.测试 3.搭建哨兵集群3.1.集群结构3.2.准备实例和配置3.3.启动3.4.测试 4.搭建分片集群4.1.集群结构4.2.准备实例和配置4.3.启动4.4.创建集群4.5.测试4.5.测试 本章是基…

java SSM 摄影作品网站myeclipse开发mysql数据库springMVC模式java编程计算机网页设计

一、源码特点 java SSM 摄影作品网站系统是一套完善的web设计系统&#xff08;系统采用SSM框架进行设计开发&#xff0c;springspringMVCmybatis&#xff09;&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代 码和数据库&#xff0c;系统主要采…

计算机网络-网络体系结构

目录 计算机网络的基本概念计算机网络的定义组成与功能计算机网络的分类按照网络的作用范围进行分类按照网络的使用者进行分类 计算机网络主要性能指标 计算机网络体系结构计算机网络协议、接口、服务等概念ISO/OSI 参考模型和 TCP/IP 模型OSI七层模型TCP/IP 模型封装与分用 计…

攻防渗透第四章(谷歌语法)

一、常用谷歌黑客语法 制定网站的URL site: 包含特定字符的URL inurl: 网页标题中包含特定字符 intitle: 正文中指定字符 intext: 指定类型文件 filetype 开发语言判断 site:163.com filetype:php site:163.com filetype:jsp site:163.com filetype:asp site:163.com filetype…

【裸机开发】内核时钟 PLL1 配置实验(一)—— 寄存器分析篇

本章主要会回答以下问题 &#xff1f; imx6u 的时钟源来自于哪 &#xff1f;为什么一个起始时钟源&#xff0c;最终分成了多路&#xff1f;不同的时钟源是如何与外设对应起来的&#xff1f;&#xff08;时钟树&#xff09;要配置内核时钟频率 有哪些步骤 &#xff1f;涉及到哪…

基于Java购物商城系统设计与实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a; ✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精…

今天面了个35k字节跳动出来,真是砂纸擦屁股,给我露了一手...

​2023年春招已经结束&#xff0c;很多小伙伴收获不错&#xff0c;拿到了心仪的 offer。 各大论坛和社区里也看见不少小伙伴慷慨地分享了常见的面试题和八股文&#xff0c;为此咱这里也统一做一次大整理和大归类&#xff0c;这也算是划重点了。 俗话说得好&#xff0c;他山之石…

深入篇【C++】string类的常用接口介绍:标准库中的string类 【万字总结】

深入篇【C】string类的常用接口介绍&#xff1a;标准库中的string类 Ⅰ.string类介绍Ⅱ.string类的常用接口①.string类对象的常用构造1.string()2.string(const char*ch)3.string(const string& str)4.string(size_t n,char c)5.string(const string& str,size_t pos,…

第四章 Linux网络编程 4.1 网络结构模式 4.2MAC地址、IP地址、端口

第四章 Linux网络编程 4.1 网络结构模式 C/S结构 简介 服务器 - 客户机&#xff0c;即 Client - Server&#xff08;C/S&#xff09;结构。C/S 结构通常采取两层结构。服务器负责数据的管理&#xff0c;客户机负责完成与用户的交互任务。客户机是因特网上访问别人信息的机器…