基于ARM的排他访问原理及应用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/tissar/article/details/83008719

版权声明

本文为博主原创文章,未经博主允许不得转载。

摘要

自操作系统诞生以来,并行编程就是一个值得关注问题。本文以一个全局变量为例,就ARMv7内核处理器中断屏蔽和排他访问两种临界区的实现方式进行了一些讨论,主要阐述了排他访问的原理及其代码实现。文中介绍的排他访问能提高系统的实时性,对各种并行编程的研究有着重大意义。

关键词

ARM;临界区;资源竞争;同步原语;排他访问;汇编语言

正文

引言

一个嵌入式操作系统含有许多临界区,最简单的方法是禁止上下文切换(如全局中断屏蔽)保证临界区的原子操作。但禁止上下文切换,会加大中断等待时间,而且只能用于单处理器设计。ARMv7 架构内核对资源竞争问题提供了新的方法——排他访问 ( exclusive access ) 。而排他访问的实现在硬件基础上还需要软件支持。本文研究单核 ARMv7 系统下的排他访问原理的硬件机制和软件编程。对于操作系统的共享资源并发访问的研究有着重大意义。

资源竞争

总的来说,当有多条执行路径并发访问一个共享资源时,就会造成资源竞争。以C语言对全局变量i的访问为例。ARM 处理器执行C语言的 “ i++ ” 语句等 “ 读 - 改 - 写 ” 操作需要3条汇编指令,分别为LDR、ADD、STR指令。多数情况下,多任务交叉运行不会出现资源竞争,如图1所示。

图1 “读-改-写”操作示意图

实线箭头表示数据传递的方向,S型曲线表示发生任务调度。低优先级任务执行“读-改-写”操作后,在 t11 时刻,高优先级任务就绪,系统进行任务调度。随后,高优先级任务也执行 “ 读-改-写 ” 操作。由于两个任务都连续地执行了LDR、ADD、STR指令,在这种情况下,全局变量i顺利自增两次。但有少数情况,高优先级任务会与低优先级任务并发地访问一个共享资源,如图2所示。

图2 资源竞争示意图

在低优先级任务执行ADD指令后,在 t21 时刻,高优先级任务就绪,发生一次任务调度。高优先级任务执行“读-改-写”操作把自增后的数值2写入变量 i 中。随后在 t22 时刻,系统进行一次任务调度,低优先级任务将再一次将数值2写入变量 i中。最后,两条 “ i++ ” 语句将数据相互覆盖,全局变量只自增一次,明显不符合我们的期望。

资源竞争问题导致系统存在潜在的危险,而这些危险都是难以重现、难以调试的。

临界区

产生资源竞争问题的原因是:对共享资源的 “ 读 - 改 - 写 ” 操作应该是原子级的,称为临界区,又称为原子操作。临界区是不允许被交叉运行的。为了保证多任务依次执行临界区代码,一次只有一个任务可以进入临界区。一旦进入临界区,那么其他任务就不能进入直至该任务完成。在嵌入式操作系统中,通常由全局中断屏蔽来实现,如图3。

图3 临界区代码实现示意图

在执行临界区前,先锁中断以全局中断屏蔽,在执行临界区后解锁,恢复任务调度。在 t31 时刻,尽管高优先级任务休眠时间到,但在执行临界区期间,不能进入就绪态,需要等待临界区完成。在 t32 时刻,高优先级任务获得 CPU 使用权,访问变量 i 并使其自增,变量 i 顺利自增两次。

临界区使任务有秩序地访问共享资源,避免资源竞争。但全局中断屏蔽不仅需要许多额外的指令周期,而且降低了紧急中断的响应速度。ARMv7 的排他访问旨在改善这一问题。

ARMv7的排他访问

ARMv7指令集包含三对同步原语 ( synchronization primitives ) 。这三对同步原语分别为:

  • 32位数据读写的 LDREX 和 STREX,
  • 16位数据读写的 LDREXH 和 STREXH,
  • 8位数据读写的 LDREXB 和 STREXB,
  • 此外,还有用于清零排他访问标识的 CLREX 。

这些同步原语给多任务提供了一种不阻塞的资源竞争应对方法 —— 排他访问。排他访问实现有保障的 “ 读 - 改 - 写 ” 操作,可以用于信号量、自旋锁等系统服务。

同步原语语法

同步原语的基本语法如下:

  1. LDREX {cond} Rt, [Rn {, #offset}]
  2. STREX {cond} Rd, Rt, [Rn {, #offset}]
  3. CLREX {cond}

使用说明如如下:

  • LDREX 指令与普通 LDR 指令相似,将 Rn 寄存器上的地址指向的内存加载到 Rt 寄存器;
  • STREX指令将Rt上的值写入到Rn寄存器保存的地址的内存中,并将结果返回到Rd寄存器,写入成功返回0,写入失败放回1;
  • CLREX可以清空一个排他访问标识位。LDREX指令需要和对应的STREX指令或CLREX指令一起使用。

排他访问原理

排他访问的实现还有内核上一个排他访问监控器的支持,如图4所示。

图4 排他访问监控器

排他访问监控器监控内核上的一个特殊寄存器——排他标识寄存器,而排他标识寄存器上有一个排他标识位。执行同步原语中的加载指令如 LDREX 时,排他访问监控器使排他标识置1。

排他标识有三种情况会被清零:

  1. 执行CLREX命令;
  2. 执行STREX等命令(无论成功与否);
  3. 处理器产生异常或中断。

当且仅当排他标识为1时,STREX指令才能成功执行并返回0,否则STREX指令执行失败并返回1。因此,排他访问提供了检验原子操作被交叉运行的方法。通过检验STREX的返回结果,判断出原子操作被交叉运行,从而重新执行新的“读-改-写”操作,更新变量的值,如图5所示(图5在不妨碍理解的前提下,省略了比较指令、跳转指令等指令。)。

图5 排他访问示意图

在 t41 时刻,系统发生任务调度,高优先级任务抢占了共享资源,率先将全局变量 i 由1修改为2;然后在 t42 时刻,系统发生任务调度,高优先级任务让出 CPU 使用权和共享资源。此前低优先级任务加载的全局变量i已经出现滞后,如果此时写成功,则会和图2中紊乱的读写顺序如出一辙,导致数据相互覆盖。

排他访问的机制避免了悲剧的发生。由于任务调度依赖系统的定时器中断,因此被 LDREX 置位的排他标识被清零,STREX 指令执行失败。程序检查出 STREX 执行失败,并重新执行一次 “ 读 - 改 - 写 ” 操作

排他访问是通过监视系统异常或中断实现共享资源的并行访问的,因此系统的中断并没有受到阻塞,提高了系统的实时性。

排他访问的汇编语言实现

排他访问的软件编程分为四步:

  1. 使用LDREX指令加载内存;
  2. 修改对应寄存器的值;
  3. 使用STREX指令存储内存;
  4. 使用CMP指令检查写指令的返回结果,判断是否成功写入内存。

假设R2寄存器保存了某个信号量在内存上的地址,那么,可以使用下面的代码实现释放信号量:

try  LDREX   r1, [r2]          ; 加载信号量到寄存器
     ADD     r1, r1, #1        ; 将寄存器的值加一
     STREX   r0, r1, [r2]      ; 将R1写入内存
     CMP     r0, #0            ; 检查是否成功写入
     BNE     try               ; 写入失败则重新写入

假设R3寄存器保存了某个自旋锁在内存上的地址,那么,可以使用下面的代码实现申请自旋锁,使用R0寄存器返回结果,1代表申请成功,0代表申请失败:

try  MOV     r0, #0            ; 预先设置R0为0
     LDREX   r2, [r3]          ; 加载自旋锁到寄存器
     CMP     r2, #0            ; 检查自旋锁的值
     ADDEQ   r2, r2, #1        ; 若空闲,数值加一
     MOVEQ   r0, #1            ; 申请成功,返回1
     STREX   r1, r2, [r3]      ; 将R2写入内存
     CMP     r1, #0            ; 检查是否成功写入
     BNE     try               ; 写入失败则重新写入

排他访问的C语言实现

通常标准C编译器不能编译出特殊的同步原语,此时可以使用汇编函数嵌入汇编内联汇编等方法实现排他访问。但汇编与C语言的混合使用依然增加了程序员的负担。除了以上方法,还可以使用 ARM C 编译器提供的内在函数,如表1。

汇编指令 内在函数
LDREX uint32_t __LDREXW ( uint32_t *addr )
LDREXH uint16_t __LDREXH ( uint16_t *addr )
LDREXB uint8_t __LDREXB ( uint8_t *addr )
STREX uint32_t __STREXW ( uint32_t value, uint32_t *addr )
STREXH uint16_t __STREXH ( uint16_t value, uint16_t *addr )
STREXB uint8_t __STREXB ( uint8_t value, uint8_t *addr )
CLREX void __CLREX ( void )

根据 ARM C 编译器提供的内在函数,可以使用下面的C语言代码实现释放信号量:

void function1 ( uint32_t *addr )
{
    while ( __STREXW ( __LDREXW ( addr ) + 1, addr ) );
}

也可以使用下面的C语言代码实现申请自旋锁,结果通过函数返回值返回,0代表申请失败,1代表申请成功:

uint32_t function2 ( uint32_t *addr )
{
    do
    { 
        if ( __LDREXW ( addr ) )
        {
            __CLREX ();
            return 0;
        }
    } while ( __STREXW ( 1, addr ) )
    return 1;
}

结语

排他访问的进步在于,它意识到问题的根本原因是资源竞争使其中一条执行路径的加载操作出现时间上的滞后。这种机制相比全局中断屏蔽,其操作更加精细,提高了系统的实时性。

尽管操作系统内核和系统服务中使用 ARM 排他访问会降低操作系统的可移植性,但现在ARM处理器有着越来越高的市场地位,故为ARM处理器的操作系统针对性优化亦符合市场的需求。

并且排他访问的应用不限于操作系统内核服务,在应用程序中也有实用价值。本文中的研究基于 ARMv7 架构的单核 Cortex-M4 处理器,对于其他支持同步原语的处理器依然奏效,但在多核系统中有着机制上的差异。

应当指出,排他访问指令相比普通访问指令,有着更多的执行周期;因此,出于性能考虑,在不涉及资源竞争的情况下,请不要使用不必要的排他访问指令。

参考文献

  1. Joseph Yiu. ARM Cortex-M3与Cortex-m4权威指南(第3版)[M]. 吴常玉,曹孟娟,王丽红,译. 北京:清华大学出版社,2015.
  2. Cortex-M4 Devices Generic User Guide [EB/OL]. [2010-12-16]. http://infocenter.arm.com/help/topic/com.arm.doc.dui0553a/index.html

猜你喜欢

转载自blog.csdn.net/tissar/article/details/83008719