文章目录
- 📝可重⼊函数
- 🌠 volatile
- 🚩总结
📝可重⼊函数
- main函数调⽤insert函数向⼀个链表head中插⼊节点node1,插⼊操作分为两步,刚做完第⼀步的时候,因为硬件中断使进程切换到内核,再次回⽤⼾态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调⽤insert函数向同⼀个链表head中插⼊节点node2,插⼊操作的两步都做完之后从sighandler返回内核态,再次回到⽤⼾态就从main函数调⽤的insert函数中继续往下执⾏,先前做第⼀步之后被打断,现在继续做完第⼆步。结果是,main函数和sighandler先后向链表中插⼊两个节点,⽽最后只有⼀个节点真正插⼊链表中了。
- 像上例这样,insert函数被不同的控制流程调⽤,有可能在第⼀次调⽤还没返回时就再次进⼊该函数,这称为重⼊,insert函数访问⼀个全局链表,有可能因为重⼊⽽造成错乱,像这样的函数称为不可重⼊函数,反之,如果⼀个函数只访问⾃⼰的局部变量或参数,则称为可重⼊(Reentrant)函数。
想⼀下,为什么两个不同的控制流程调⽤同⼀个函数,访问它的同⼀个局部变量或参数就不会造成错乱?
- 假设以下是一个简单的
func
函数示例(C语言):int func(int a) { int b = a * 2; return b; }
- 当这个函数被调用时,会发生以下情况:
- 栈帧创建:
- 首先,调用
func
的函数(假设是main
函数或者其他函数)会将参数a
的值通过某种方式(例如将a
的值压栈或者通过寄存器传递等,这里假设是压栈)传递给func
。 - 然后,在栈上为
func
开辟一个栈帧。这个栈帧包含了func
的局部变量b
的存储空间。
- 首先,调用
- 函数执行:
- 在
func
函数内部,b = a * 2
这一操作是在当前栈帧的范围内进行的。它从栈帧中获取参数a
的值,计算a * 2
后将结果存储到栈帧中局部变量b
的存储空间。 - 当函数返回时,会从栈帧中取出
b
的值(通过某种返回机制,如将b
的值放入寄存器等)返回给调用者。
- 在
- 栈帧创建:
- 当这个函数被调用时,会发生以下情况:
- 当有两个不同的控制流程调用
func
时:- 假设第一个控制流程是在
main
函数中调用func
,传入参数a = 3
。- 会创建一个栈帧,在这个栈帧中,参数
a
的值为3
,计算得到b = 6
,这个过程都在这个栈帧内完成。
- 会创建一个栈帧,在这个栈帧中,参数
- 假设第二个控制流程是在一个信号处理函数(类似于之前提到的
sighandler
)中调用func
,传入参数a = 5
。- 会为这个调用重新创建一个栈帧。在这个新的栈帧中,参数
a
的值为5
,计算得到b = 10
。这个过程和第一个控制流程调用func
时是完全独立的,因为它们有各自独立的栈帧。
- 会为这个调用重新创建一个栈帧。在这个新的栈帧中,参数
- 这两个控制流程对
func
的调用不会相互干扰,因为它们操作的是各自栈帧中的参数和局部变量,从而体现了可重入函数访问自己的局部变量或参数不会造成错乱的特性。
- 假设第一个控制流程是在
如果⼀个函数符合以下条件之⼀则是不可重⼊的:
- 调⽤了malloc或free,因为malloc也是⽤全局链表来管理堆的。
- 调⽤了标准I/O库函数。标准I/O库的很多实现都以不可重⼊的⽅式使⽤全局数据结构。
🌠 volatile
- volatile关键字的基本概念
- 在编程语言(如C和C++)中,
volatile
是一个类型修饰符。它用于告诉编译器,被修饰的变量是易变的,编译器不应对该变量进行优化。 - 例如,对于一个普通的变量
int a;
,编译器可能会根据代码的上下文对变量a
的访问进行优化。假设代码中有a = 1;
和a = 2;
两条语句,编译器可能会认为这两条语句是连续的赋值操作,中间没有其他代码改变a
的值,于是可能会将这两条语句合并或者优化访问路径。 - 但是,当
a
被声明为volatile int a;
时,编译器就不能进行这样的优化。因为volatile
表示变量a
的值可能会在编译器无法预知的情况下发生变化,比如被硬件(如外部设备通过内存映射I/O)或者其他异步执行的代码(如中断服务程序)改变。
- 在编程语言(如C和C++)中,
- volatile在并发或异步环境中的作用
- 考虑一个简单的嵌入式系统场景,有一个全局变量用于和外部设备通信。
volatile unsigned char *device_register = (volatile unsigned char *)0x1000;
- 这里将一个指针
device_register
声明为volatile
,它指向一个内存地址0x1000
,这个地址可能是外部设备的寄存器地址。 - 当读取
*device_register
的值时,由于它是volatile
的,每次读取编译器都会真正地从内存地址0x1000
获取数据,而不会使用之前缓存的值。同样,当向*device_register
写入数据时,也会真正地将数据写入到内存地址0x1000
,而不会因为优化而忽略这个写入操作。 - 在多线程或中断环境中,
volatile
也非常有用。假设一个全局变量volatile int flag;
用于在主线程和中断服务程序之间通信。主线程可能会检查flag
的值来判断是否有中断发生相关的事件。如果flag
不是volatile
的,编译器可能会优化掉对flag
的检查,导致主线程无法正确地检测到flag
的变化,因为编译器可能认为flag
的值在没有显式赋值的情况下是不变的。而volatile
关键字确保了主线程每次检查flag
的值时,都是从内存中获取最新的值。
该关键字在C当中我们已经有所涉猎,今天我们站在信号的⻆度重新理解⼀下
Makefile
文件
sig:sig.c
gcc -o sig sig.c #-O2
#在Makefile中,#后面的内容是注释。在gcc -o sig sig.c #-O2这一行中,-O2是被注释掉的内容。
#正常情况下,如果没有被注释,-O2是gcc编译器的一个优化选项。它用于指定编译器进行一定级别的优化,-O2通常会执行较多的#优化,比如指令重排、函数内联等操作,以提高生成的可执行程序的性能。但是在这个Makefile规则里,因为被注释了,所以gcc#编译sig.c生成sig可执行文件时不会使用-O2这个优化选项。
.PHONY:clean
clean:
rm -f sig
sig.c
文件
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
printf("change flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
printf("processs quit normal\n");
return 0;
}
标准情况下,键入(CTRL-C
,2
号信号被捕捉,执行自定义动作,修改flag=1 , while
条件不满足,退出循环,进程退出
第二种:
优化情况下,键入CTRL-C
,2
号信号被捕捉,执行自定义动作,修改flag=1
,但是 while
条件依旧满足,进程继续运行!但是很明显flag
肯定已经被修改了,但是为何循环依旧执行?很明显while
循环检查的flag,并不是内存中最新的flag,这就存在了数据二异性的问题。while检测的flag
其实已经因为优化,被放在了CPU寄存器当中。如何解决呢?很明显需要volatile
sig:sig.c
gcc -o sig sig.c -O2
.PHONY:clean
clean:
rm -f sig
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
printf("change flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
printf("processs quit normal\n");
return 0;
}
volatile
作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作
2第三种:
sig:sig.c
gcc -o sig sig.c -O2
.PHONY:clean
clean:
rm -f sig
#include <stdio.h>
#include <signal.h>
volatile int flag = 0;
void handler(int sig)
{
printf("change flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
printf("processs quit normal\n");
return 0;
}
常用的gcc
优化选项:
- -O(等价于 -O1)
- 优化内容:
- 进行简单的优化,如常量折叠,即将编译期可计算的常量表达式直接计算出结果,例如
int a = 2 + 3;
会直接计算为int a = 5;
。 - 简单的公共子表达式消除,当程序中多次出现相同的子表达式(其运算对象在每次出现时都没有变化),编译器会只计算一次,用计算结果代替后续相同子表达式的计算。
- 优化循环结构,例如将一些可以在循环外计算的表达式移到循环外,减少不必要的计算。
- 减少函数调用开销,例如对一些简单的函数(如内联函数)进行适当优化,提高执行效率。
- 进行简单的优化,如常量折叠,即将编译期可计算的常量表达式直接计算出结果,例如
- 适用场景:
- 当对编译速度要求较高,且对程序性能提升有一定需求,但不希望过度优化时可以使用。适用于一些简单的程序或者在开发阶段初步优化的情况。
- 示例:
gcc -o output_file input_file.c -O
- 优化内容:
- -O2
- 优化内容:
- 包含了
-O
(或-O1
)的所有优化。 - 进行更复杂的指令重排,使程序执行流程更符合CPU的流水线特性,提高CPU的执行效率。
- 更多的函数内联,将一些短小的函数体直接嵌入到调用它的地方,减少函数调用的开销。不过,过度的函数内联可能会导致代码膨胀。
- 进一步优化循环,如循环展开,在适当的情况下将循环体展开,减少循环控制的开销,但这可能会增加代码大小。
- 包含了
- 适用场景:
- 是比较常用的优化级别,适用于大多数需要较好性能的应用程序。在性能和编译时间、代码大小之间取得了较好的平衡。
- 示例:
gcc -o output_file input_file.c -O2
- 优化内容:
- -O3
- 优化内容:
- 包含了
-O2
的所有优化。 - 更激进的函数内联,几乎会尝试内联所有可以内联的函数,可能会导致代码大小显著增加。
- 更多的循环展开,甚至会对一些复杂的循环进行深度展开,进一步减少循环控制开销,但也更容易导致代码膨胀和缓存性能下降。
- 还会进行一些复杂的优化,如对全局变量和指针的优化,以提高程序的整体性能。
- 包含了
- 适用场景:
- 适用于对性能要求极高的场景,如一些性能敏感的算法实现、高性能计算等。但需要注意代码大小和可能出现的性能下降(如过度优化导致缓存不命中等情况)。
- 示例:
gcc -o output_file input_file.c -O3
- 优化内容:
- -Os
- 优化内容:
- 主要侧重于优化代码大小。在不显著降低程序性能的前提下,通过多种方式减小生成的可执行文件的大小。
- 例如,选择更紧凑的指令集,避免一些会导致代码膨胀的优化(如过度的函数内联和循环展开),同时也会进行一些基本的性能优化,如常量折叠等。
- 适用场景:
- 非常适合在存储空间有限的环境中使用,如嵌入式系统,或者对可执行文件大小有严格限制的应用程序。
- 示例:
gcc -o output_file input_file.c -Os
- 优化内容: