其他分享
首页 > 其他分享> > 从0开始的ucosii学习(二) 什么是临界区

从0开始的ucosii学习(二) 什么是临界区

作者:互联网

内容简介

​ 临界区是什么?为什么定义了三种进入方式却只能在代码里找到第一种方式?

​ 我们会结合有关c语言的运行时栈的有关知识来探讨三种方法的可行性。

正文

​ 在第一次建立能够骗过编译器的空ucosii工程中,我们在os_cpu.h中定义了两个函数,简单看下。

//OS_CRITICAL_METHOD = 1 :直接使用处理器的开关中断指令来实现宏 
//OS_CRITICAL_METHOD = 2 :利用堆栈保存和恢复CPU的状态 
//OS_CRITICAL_METHOD = 3 :利用编译器扩展功能获得程序状态字,保存在局部变量cpu_sr

#define  OS_CRITICAL_METHOD   3	 	//进入临界段的方法

#if OS_CRITICAL_METHOD == 3
#define  OS_ENTER_CRITICAL()  {cpu_sr = OS_CPU_SR_Save();}
#define  OS_EXIT_CRITICAL()   {OS_CPU_SR_Restore(cpu_sr);}

OS_CPU_SR  OS_CPU_SR_Save(void);
void       OS_CPU_SR_Restore(OS_CPU_SR cpu_sr);
#endif

​ 其实,在很多的实时操作系统中,我们都只能找到第三种,前两种方法或多或少都有着问题,我们会介绍这三种方法的实现,探讨其问题与优势,当然,我对gcc编译器的理解,对c语言栈的理解可能不是很到位,难免会有疏漏,欢迎指正。

什么是临界区

临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。与普通的c语言写的单个程序不同,在一个操作系统中,存在这大量的只能够被互斥访问的资源。我们可以在信号量经典的生产者消费者的问题中发现,如果不能保证一些资源的互斥访问,有些设备的运行会出现问题(比如打印机在打印时只能同时被一个”人“访问),甚至整个系统的运行都会陷入不确定性,这是不可接受的。

进入临界区的方法

​ 进入临界区的方法有很多,比如关中断,禁止调度等等。我们今天要将的ucosii中的进入临界区的三个方案,都是基于关中断的。

​ PS,关中断是一种与硬件强相关的方法,我们这里结合STM32F401RE进行讲解。

method1 直接关中断

​ 定义如下

#define OS_ENTER_CRITICALL() (Cli())
#define OS_EXIT_CRITICAL() (Sti())
Cli
	CPSID   I ;PRIMASK=1,该位是中断总开关
	BX LR ;函数返回
Sti
	CPSIE	I ;PRIMASK=0,关中断
	BX LR 

​ 需要注意的是,在STM32F401RE中是不存在FIQ的,所以CPSID I将IRQ中断关闭就是将所有的中断都关闭了,包括systick等一系列中断都不会响应。systick是系统的心跳,从理论上来讲是不可以被关闭的,但是实际上,我们在临界区内所做的事情往往只有几行汇编,执行这些代码的时间和系统的心跳时间之间差了好几个数量级,几乎可以忽略不记,所以在这里这种简单粗暴的关中断是可行的。听说还有别的可以不屏蔽systick的问题,但是这相关的问题就更多了,按下不表。

​ 这种方法是存在很大问题的,他不支持访问临界区的嵌套,因为他无法区分你是进的临界区是哪一级的临界区,只要离开一次临界区,本来关闭的中断就会打开,外层的临界区就失效了,可以参考下图。

直接关中断进入临界区的问题

method2 通过堆栈保存程序状态字寄存器

​ demo如下

#define OS_ENTER_CRITICAL() (PushAndCli())
#define OS_EXIT_CRITICAL() (Pop())
PushAndCli
    PUSHF
    CPSID   I				
    BX      LR			    

Pop
    POPF
    BX      LR		

​ 看起来是不是很完美,我走之前先把之前的状态存到堆栈里去,走之后又从堆栈中取出来,这样就可以记录下来每一次进出临界区时候的状态,不管堆叠几次都不会出问题。

​ 实际上,一般在使用这种方式的时候是不会出问题的,我们可以先来看看函数堆栈在函数运行时的变化。

​ 当一个函数被调用时,编译器会自动为这个函数分配一个栈,我们可以在《c和指针》的第18章找到x86构架下的一种实现。

​ 我们首先要对bp sp等寄存器有一些了解(建议百度),我们这里假设只有a1使用寄存器传值,其他的参数使用堆栈传值(在实际情况里面,在参数较少时会优先使用寄存器传值,只有当寄存器不足时才会使用堆栈传值)。

int add(register int a0, int a1, int a2, int a3, int a4){
	int x = a1 + a2 + a3 + a4;						//(2)	
	return x;
}
int main(){
	add(5,1,2,3,4);									//(1)
	return 0;
}

​ 在进入执行子函数的代码之前(1),编译器会在主函数中把add的第一个参数存到r0中,其他四个参数逆序压入堆栈中(逆序的原因是因为子函数不知道究竟会传给他几个参数,如果逆序传入的话,距离自己bp指针最近的那个就是第一个参数,一路找下去就行),接下来会执行跳转到子函数的指令,这条指令同时将返回地址压入堆栈中。现在的堆栈情况如图所示,上方是低地址

主函数结束时堆栈

​ 同时进入子函数(2),在子函数的开始我们将旧的bp指针的内容入栈,同时让bp指针指向当前sp指针的位置,当前sp指针的内容是旧bp指针在栈中存在的位置,将这里作为子函数的栈底,同时将我们之前存在寄存器里的值和函数的局部变量一起丢进子函数的堆栈中,他们的顺序如图所示

子函数指行时的函数栈

​ 现在我们有了一个堆栈,从高地址到低地址分别存了反序压入的参数,旧的bp(旧堆栈帧指针),局部变量和被保存的寄存器的值,bp的上面的子函数的堆栈,bp的下面是主函数的堆栈。当我们希望找一个传入参数时,就让bp指针向高地址寻找,当我们希望找一个局部变量时,就让bp指针向低地址寻找,完美。

​ 好了现在子函数结束,准备返回,子函数会把保存的寄存器的值返还给寄存器,让bp指针指向主函数的bp指针,然后返回地址出栈给pc,此时sp指针指向的是返回地址。我们需要注意的是此时传入的参数还没有被清楚,因为只有主函数才知道到底传入了几个参数,只有由主函数清楚这些数据才是安全的。

​ 现在我们讲完了在x86构架下的函数栈的知识,然后我发现在STM32F4平台上的实现和上面有些出入,他并没有栈底指针bp,而是通过sp指针进行相对寻址,我们可以结合由MDK生成的STM32F4的一段汇编来讲解,源代码和汇编代码如下

void mypush(void);
void mypop(void);

int add1(int a, int a2, int a3, int a4, int a5, int a6, int a7){
    int x = 8, x2 = 9, x3 = 10, x4 = 11, x5 = 12, x6 = 13, x7 = 14;
	mypush();
    //这里加这么多变量只是为了把寄存器用完,逼他去栈里面把数据取出来,从而暴露问题
	x = a + a2 + a3 + a4 + a5 + a6 + a7 + x + x2 + x4 + x5 + x6 + x7 + x3;
	mypop();
    return 0;
}

int main(){
    add1(1,2,3,4,5,6,7);
    return 0;
}
		EXPORT mypush
		EXPORT mypop

		PRESERVE8 
		
		AREA    |.text|, CODE, READONLY
        THUMB 

mypush
	PUSH {r0}
	BX LR

mypop
	POP {r0}
	BX LR
	end

​ 我们可以跟着汇编走一走流程,这里会把汇编与对应的c语言都对应起来注释也加的比较全我觉得是可以看懂的

​ main函数

    12: int main(){ 0x080004B0 B50E      PUSH          {r1-r3,lr}		;主函数也是一个函数,bsp在调用主函数时也要保存上文  ;sp_a    13:     add1(1,2,3,4,5,6,7); 0x080004B2 2007      MOVS          r0,#0x070x080004B4 2106      MOVS          r1,#0x060x080004B6 2205      MOVS          r2,#0x050x080004B8 2304      MOVS          r3,#0x04			;前面四行在将add1的参数传给寄存器0x080004BA E9CD2100  STRD          r2,r1,[sp,#0]	;这里将r1和r2寄存器的值存到sp对应的位置,也就是r1r2入栈0x080004BE 9002      STR           r0,[sp,#0x08]	;r0入栈									;sp_b													;这里发现了奇怪的事情,往栈里存东西把第一行push的信息覆盖了													;是不是忘记给主函数开辟栈空间了,但是代码就是这样,等一手dl解惑0x080004C0 2203      MOVS          r2,#0x030x080004C2 2102      MOVS          r1,#0x020x080004C4 2001      MOVS          r0,#0x01			;继续存入add1的参数到寄存器0x080004C6 F7FFFFBD  BL.W          add1 (0x08000444);调用函数add1									;(1)    14:     return 0; 0x080004CA 2000      MOVS          r0,#0x00			;把0赋值给r0,这个0就是return后面的0    15: } 0x080004CC BD0E      POP           {r1-r3,pc}		;开始保存上文了现在恢复上文0x080004CE F04F7040  MOV           r0,#0x30000000x080004D2 EEE10A10  VMSR           FPSCR, r00x080004D6 4770      BX            lr

​ add1函数

     4: int add1(int a, int a2, int a3, int a4, int a5, int a6, int a7){ 0x08000444 E92D4FF0  PUSH          {r4-r11,lr}			;函数开始 保存之前的寄存器值,方便自己用这些寄存器	;sp_c0x08000448 B087      SUB           sp,sp,#0x1C			;为子函数开辟堆栈,arm应该是有办法计算出到底需要多少的栈的														;这里开辟的栈是刚刚好的,你调整参数个数发现不是巧合														;sp_d0x0800044A 4604      MOV           r4,r00x0800044C 460D      MOV           r5,r10x0800044E 4616      MOV           r6,r20x08000450 461F      MOV           r7,r3				;之前保存在r0-r3寄存器的参数传入自己的寄存器0x08000452 E9DD9A11  LDRD          r9,r10,[sp,#0x44]	;从栈里面取出之前传入的参数4和50x08000456 F8DD8040  LDR           r8,[sp,#0x40]		;从栈里面取出之前传入的参数6     5:                 int x = 8, x2 = 9, x3 = 10, x4 = 11, x5 = 12, x6 = 13, x7 = 14; 0x0800045A F04F0B08  MOV           r11,#0x08			;保存在子函数中定义的局部变量0x0800045E 2009      MOVS          r0,#0x090x08000460 9006      STR           r0,[sp,#0x18]		;寄存器存不下局部变量了,把局部变量往栈里放			;(2)0x08000462 200A      MOVS          r0,#0x0A0x08000464 9005      STR           r0,[sp,#0x14]0x08000466 200B      MOVS          r0,#0x0B0x08000468 9004      STR           r0,[sp,#0x10]0x0800046A 200C      MOVS          r0,#0x0C0x0800046C 9003      STR           r0,[sp,#0x0C]0x0800046E 200D      MOVS          r0,#0x0D0x08000470 9002      STR           r0,[sp,#0x08]0x08000472 200E      MOVS          r0,#0x0E0x08000474 9001      STR           r0,[sp,#0x04]		;终于把存不下的数据存入栈里了						;(3)     6:                 mypush();      7:     //这里加这么多变量只是为了把寄存器用完,逼他去栈里面把数据取出来,从而暴露问题0x08000476 F7FFFEEF  BL.W          mypush (0x08000258)	;调用函数,这个函数没有参数     8:                 x = a + a2 + a3 + a4 + a5 + a6 + a7 + x + x2 + x4 + x5 + x6 + x7 + x3; 0x0800047A 1960      ADDS          r0,r4,r5				;开始做加法,不断将存在寄存器里的数加起来0x0800047C 4430      ADD           r0,r0,r60x0800047E 4438      ADD           r0,r0,r70x08000480 4440      ADD           r0,r0,r80x08000482 4448      ADD           r0,r0,r90x08000484 4450      ADD           r0,r0,r100x08000486 EB00010B  ADD           r1,r0,r110x0800048A 9806      LDR           r0,[sp,#0x18]		;有部分数据存在栈中,从栈中取出来加起来			;(4)0x0800048C 4401      ADD           r1,r1,r00x0800048E 9804      LDR           r0,[sp,#0x10]0x08000490 4401      ADD           r1,r1,r00x08000492 9803      LDR           r0,[sp,#0x0C]0x08000494 4401      ADD           r1,r1,r00x08000496 9802      LDR           r0,[sp,#0x08]0x08000498 4401      ADD           r1,r1,r00x0800049A 9801      LDR           r0,[sp,#0x04]0x0800049C 4401      ADD           r1,r1,r00x0800049E 9805      LDR           r0,[sp,#0x14]0x080004A0 EB010B00  ADD           r11,r1,r0			;加完了									;(5)     9:                 mypop(); 0x080004A4 F7FFFEDA  BL.W          mypop (0x0800025C)	;调用mypop函数    10:     return 0; 0x080004A8 2000      MOVS          r0,#0x00				;返回值0存入r0    11: } 0x080004AA B007      ADD           sp,sp,#0x1C			;销毁子函数的栈0x080004AC E8BD8FF0  POP           {r4-r11,pc}			;返回调用子函数时的现场,同时pc回到主函数,继续执行

push和pop函数

    9: mypush    10:         PUSH {r0} 0x08000258 B401      PUSH          {r0}					;将r0压栈,这里模拟的是第二种进入临界区的办法,往栈中压了一个值														;sp_E    13:         BX LR 									;等于return,函数返回0x0800025A 4770      BX            lr    15: mypop     16:         POP {r0} 								0x0800025C BC01      POP           {r0}					;栈顶数据出栈到r0寄存器    17:         BX LR 0x0800025E 4770      BX            lr					;函数返回

​ 大致浏览上面的汇编,我们至少能够明白,在没有bp指针的这段代码中,存在栈中的数据是通过栈顶指针sp进行相对定位的,仔细观察(2)-(3)和(4)-(5)的代码,我们不难发现他们使用的相对于sp的相对地址都是一样的,也就是说,不管我们在中间怎么操作sp指针,我们的函数始终认为以下均为4字节的起始地址。

[sp-0x04] = 0xE[sp-0x08] = 0xD[sp-0x0C] = 0xC  	 ;函数以为的数据

​ 但是很遗憾的是,我们在将数据存入堆栈和将数据取出堆栈之间进行了push操作,push操作的副作用是sp–,也就是说,如果说我们在存数据的时候的sp我们称为sp0的话,我们在取数据的时候的sp已经不是sp0了,变成sp0–了,所以所有的数据都错位了一格,即

[sp-0x04] = ?[sp-0x08] = 0xE[sp-0x0C] = 0xD		;实际上的数据

​ 最后我们也得不到正确的0x69,在我这里我发现最后存入r11的是0x60,反正,结果已经变的不可预知了。

​ 下面简单画下整个栈的情况,下图中我使用sp_a sp_b sp_c来表示sp的第一次变动第二次变动

STM32F401RE下栈的情况

​ 当然,你说如果我们在汇编代码里先给mypush分配栈空间,也就是先让sp–,然后在函数返回的时候再sp++,销毁栈,这样add1函数内的局部变量访问就不会出问题了,但是,这样做还有一个更大的逻辑问题,当栈指针sp++,意义是mypush的栈被销毁,那么在这个栈空间中的数据都失去了意义,哪怕你马上进入mypop确实会返回正确的存入的值,但是我们只需要在mypush和mypop之间进行一次使用了栈空间的函数调用,直接会把在不受保护空间内的状态信息覆盖,我们就失去了这个信息,哪怕是在有bp指针的x86下也会出现这个问题(以上一段叙述没有实践过,纯理论)

​ 此外,在查资料的过程中我还发现了一个编译选项 -fno-defer-pop,官方的解释如下:For machines that must pop arguments after a function call, always pop the arguments as soon as each function returns. At levels -O1 and higher, -fdefer-pop is the default; this allows the compiler to let arguments accumulate on the stack for several function calls and pop them all at once. 个人觉得这种策略可能也会影响第二种方案的安全性,但是我不想想了。

method3

#define  OS_CRITICAL_METHOD   3	 	//进入临界段的方法#if OS_CRITICAL_METHOD == 3#define  OS_ENTER_CRITICAL()  {cpu_sr = OS_CPU_SR_Save();}#define  OS_EXIT_CRITICAL()   {OS_CPU_SR_Restore(cpu_sr);}OS_CPU_SR  OS_CPU_SR_Save(void);void       OS_CPU_SR_Restore(OS_CPU_SR cpu_sr);#endif
OS_CPU_SR_Save    MRS     R0, PRIMASK  	;R0是默认的返回参数存放的位置,可以理解为在函数结束时return R0    CPSID   I				    BX      LR			    OS_CPU_SR_Restore    MSR     PRIMASK, R0	   	;同理,R0也是默认的传入的第一个参数,也就是这个函数唯一的参数cpu_sr    BX      LR				

​ 其实第二种方法和第三种的唯一区别就是,第二种是要用了才想起来要给PRIMASK一个空间存放他,第三种是一开始就给你准备好了空间(在栈里或者寄存器里由编译器给你留了一个空间cpu_sr)等着你去放,这样明显更加安全,实际上ucosii最后所采用的也是第三种方式。

标签:ucosii,r0,函数,int,sp,学习,临界,寄存器,OS
来源: https://blog.csdn.net/xj_lance/article/details/116265650