Linux内核同步机制之原子操作

       本文转自 http://www.wowotech.net/linux_kenrel/atomic.html,在作者原文基础上(黑体),加入自己的学习理解(红色字体),形成学习笔记并记录于此。蜗窝科技,如果你是一位 Linux 驱动开发工程师,那么强烈建议把它加到你的收藏夹,干货实在太多。

一、源由

       我们的程序逻辑经常遇到这样的操作序列:

1、读一个位于 memory 中的变量的值到寄存器中(read)

2、修改该变量的值(也就是修改寄存器中的值)(modify)

3、将寄存器中的数值写回 memory 中的变量值(write)

       如果这个操作序列是串行化的操作(在一个 thread 中串行执行),那么一切 OK。然而,世界总是不能如你所愿。在多 CPU 体系结构中,运行在两个 CPU 上的两个内核控制路径同时并行执行上面操作序列,有可能发生下面的场景:

CPU1 上的操作CPU2 上的操作
读操作 
 读操作
修改修改
写操作 
 写操作

多个 CPU 和 memory chip 是通过总线互联的,在任意时刻,只能有一个总线 master 设备(例如 CPU、DMA controller)访问该 slave 设备(在这个场景中,slave 设备是 RAM chip)。因此,来自两个 CPU 上的读 memory 操作被串行化执行,分别获得了同样的旧值。完成修改后,两个 CPU 都想进行写操作,把修改的值写回到 memory。但是,硬件 arbiter 的限制使得 CPU 的写回必须是串行化的,因此 CPU1 首先获得了访问权,进行写回动作,随后,CPU2 完成写回动作。在这种情况下,CPU1 对memory 的修改被 CPU2 的操作覆盖了,因此执行结果是错误的。

       不仅是多 CPU,在单 CPU 上也会由于有多个内核控制路径的交错执行而导致上面描述的错误。一个具体的例子如下:

系统调用的控制路径中断 handler 控制路径
读操作 
 读操作
 修改
 写操作
修改 
写操作 

系统调用的控制路径上,完成读操作后,硬件触发中断,开始执行中断 handler。这种场景下,中断 handler 控制路径的写回操作被系统调用控制路径上的写回覆盖了,结果也是错误的。

       关于 read-modify-write 的操作,可能还是有点抽象,为了更好的理解,举两个实际的例子:

i = 1;
i++;

上面的代码非常简单,哪个是 read-modify-write 的操作呢?还是两条语句都是 read-modify-write 的操作呢?其实很简单,窍门就是始终从内存的角度来看。对于 i = 1; 这条语句,我们只是去修改 i 的值,即只有 write 操作,所以一条 store 指令就可以完成。i++; 虽然只是一条语句,但它需要先从 i 的内存地址中读出 i 的值,然后再进行加一操作,最后再写进内存中,这就是典型的 read-modify-write 的操作。从这里可知,i++ 并不是原子操作。

二、对策

       对于那些有多个内核控制路径进行 read-modify-write 的变量,内核提供了一个特殊的类型 atomic_t,具体定义如下:

typedef struct {
    int counter;
} atomic_t;

从上面的定义来看,atomic_t 实际上就是一个 int 类型的 counter,不过定义这样特殊的类型 atomic_t 是有其思考的:内核定义了若干 atomic_xxx 的接口 API 函数,这些函数只会接收 atomic_t 类型的参数,这样可以确保 atomic_xxx 的接口函数只会操作 atomic_t 类型的数据。同样的,如果你定义了 atomic_t 类型的变量(你期望用 atomic_xxx 的接口 API 函数操作它),这些变量也不会被那些普通的、非原子变量操作的 API 函数接受。

       具体的接口 API 函数整理如下:

接口函数描述
static inline void atomic_add(int i, atomic_t *v)给一个原子变量 v 增加 i
static inline int atomic_add_return(int i, atomic_t *v)同上,只不过将变量 v 的最新值返回
static inline void atomic_sub(int i, atomic_t *v)给一个原子变量 v 减去 i
static inline int atomic_sub_return(int i, atomic_t *v)同上,只不过将变量 v 的最新值返回
static inline int atomic_cmpxchg(atomic_t *ptr, int old, int new)比较 old 和原子变量 ptr 中的值,如果相等,那么就把 new 值赋给原子变量。
返回旧的原子变量 ptr 中的值
atomic_read获取原子变量的值
atomic_set设定原子变量的值
atomic_inc(v)原子变量的值加一
atomic_inc_return(v)同上,只不过将变量 v 的最新值返回
atomic_dec(v)原子变量的值减一
atomic_dec_return(v)同上,只不过将变量 v 的最新值返回
atomic_sub_and_test(i, v)给一个原子变量 v 减去 i,并判断变量 v 的最新值是否等于0
atomic_add_negative(i, v)给一个原子变量 v 增加 i,并判断变量 v 的最新值是否是负数
static inline int atomic_add_unless(atomic_t *v, int a, int u)只要原子变量 v 不等于 u,那么就执行原子变量 v 加a 的操作。
如果 v 不等于 u,返回非 0 值,否则返回 0 值

三、ARM中的实现

       我们以 atomic_add 为例,描述 Linux kernel 中原子操作的具体代码实现细节:

#if __LINUX_ARM_ARCH__ >= 6----------------------------------------------(1)
static inline void atomic_add(int i, atomic_t *v)
{
    unsigned long tmp;
    int result;

    prefetchw(&v->counter);----------------------------------------------(2)
    __asm__ __volatile__("@ atomic_add\n"--------------------------------(3)
"1: ldrex %0, [%3]\n"----------------------------------------------------(4)
"   add %0, %0, %4\n"----------------------------------------------------(5)
"   strex %1, %0, [%3]\n"------------------------------------------------(6)
"   teq %1, #0\n"--------------------------------------------------------(7)
"   bne 1b"
    : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)--------对应%0,%1,%2
    : "r" (&v->counter), "Ir" (i)----------------------------对应%3,%4
    : "cc");
}

#else

#ifdef CONFIG_SMP
#error SMP not supported on pre-ARMv6 CPUs
#endif

static inline int atomic_add_return(int i, atomic_t *v)
{
    unsigned long flags;
    int val;

    raw_local_irq_save(flags);
    val = v->counter;
    v->counter = val += i;
    raw_local_irq_restore(flags);

    return val;
}
#define atomic_add(i, v)    (void) atomic_add_return(i, v)

#endif

(1)ARMv6 之前的 CPU 并不支持 SMP,之后的 ARM 架构都是支持 SMP 的(例如我们熟悉的 ARMv7-A)。因此,对于 ARM 处理器,其原子操作分成了两个阵营,一个是支持 SMP 的 ARMv6 之后的 CPU,另外一个就是 ARMv6 之前的,只有单核架构的 CPU。对于 UP(uniprocessor,单核处理器),原子操作就是通过关闭 CPU 中断来完成的(关闭中断也意味着关闭了抢占)

(2)这里的代码和 preloading cache 相关。在 strex(ARM 汇编指令)指令之前将要操作的 memory 内容加载到 cache 中可以显著提高性能(可见 cache的重要性,不管是对于 CPU 而言还是对于我们学好 cache 而言)

(3)为了完整性,我还是重复一下汇编嵌入 c 代码的语法。嵌入式汇编的语法格式是(更多关于嵌入式汇编语法,可参阅 https://www.jianshu.com/p/1782e14a0766

asm(code : output operand list : input operand list : clobber list)

output operand(操作数) list 和 input operand list 是 C 代码和嵌入式汇编代码的接口,clobber(随时物品) list (找不到准确的汉语翻译,内联列表?)描述了汇编代码对寄存器的修改情况。为何要有 clober list?我们的 C 代码是 gcc 来处理的,当遇到嵌入汇编代码的时候,gcc 会将这些嵌入式汇编的文本送给 gas 进行后续处理。这样,gcc 需要了解嵌入汇编代码对寄存器的修改情况,否则有可能会造成大麻烦。例如:gcc 对 C 代码进行处理,将某些变量值保存在寄存器中,如果嵌入汇编修改了该寄存器的值,又没有通知 gcc 的话,那么,gcc 会以为寄存器中仍然保存了之前的变量值,因此不会重新加载该变量到寄存器,而是直接使用这个被嵌入式汇编修改的寄存器,这时候,我们唯一能做的就是静静的等待程序的崩溃。还好,在 output operand list 和 input operand list 中涉及的寄存器都不需要体现在 clobber list 中(gcc 分配了这些寄存器,当然知道嵌入汇编代码会修改其内容),因此,大部分的嵌入式汇编的 clobber list 都是空的,或者只有一个 cc,通知 gcc,嵌入式汇编代码更新了 condition code register。

大家对着上面的 code 就可以分开各段内容了。@符号标识该行是注释。

这里的 __volatile__ 主要是用来防止编译器优化的。也就是说,在编译该 C 代码的时候,如果使用优化选项(-O)进行编译,对于那些没有声明 __volatile__ 的嵌入式汇编,编译器有可能会对嵌入 C 代码的汇编进行优化,编译的结果可能不是原来你撰写的汇编代码。但是如果你的嵌入式汇编使用 __asm__ __volatile__(嵌入式汇编) 的语法格式,那么也就是告诉编译器,不要随便动我的嵌入汇编代码哦。

(4)我们先看 ldrex 和 strex 这两条汇编指令的使用方法。ldr 和 str 这两条指令大家都是非常的熟悉了,后缀的 ex 表示 Exclusive,是 ARMv7 提供的为了实现同步的汇编指令。

LDREX <Rt>, [<Rn>]

<Rn> 是 base register,保存 memory 的 address,LDREX 指令从 base register 中获取 memory address,并且将 memory 的内容加载到 <Rt>(destination register) 中。这些操作和 ldr 的操作是一样的,那么如何体现 exclusive 呢?其实,在执行这条指令的时候,还放出两条 “狗” 来负责观察特定地址的访问(就是保存在 [<Rn>] 中的地址了),这两条狗一条叫做 local monitor,一条叫做 global monitor(monitor 到底是什么东东?大体意思就是对 memory 访问的追踪管理,和处理器内部架构有关)

STREX <Rd>, <Rt>, [<Rn>]

和 LDREX 指令类似,<Rn> 是 base register,保存 memory 的 address,STREX 指令从 base register 中获取 memory address,并且将 <Rt> (source register) 中的内容加载到该 memory 中。这里的 <Rd> 保存了 memeory 更新成功或者失败的结果,0 表示 memory 更新成功,1 表示失败。STREX 指令是否能成功执行是和 local monitor 和 global monitor 的状态相关的。对于 Non-shareable memory(该 memory 不是多个 CPU 之间共享的,只会被一个 CPU 访问),只需要放出该 CPU 的 local monitor 这条狗就 OK 了,下面的表格可以描述这种情况。

thread 1thread 2local monitor 的状态
  Open Access state
LDREX Exclusive Access state
 LDREXExclusive Access state
 ModifyExclusive Access state
 STREXOpen Access state
Modify Open Access state
STREX 在 Open Access state 的状态下,执行 STREX 指令会导致该指令执行失败
  保持 Open Access state,直到下一个 LDREX 指令

开始的时候,local monitor 处于 Open Access state 的状态,thread 1 执行 LDREX 命令后,local monitor 的状态迁移到 Exclusive Access state(标记本地 CPU 对 xxx 地址进行了 LDREX 的操作)。这时候,中断发生了,在中断 handler 中,又一次执行了 LDREX ,此时 local monitor 的状态保持不变,直到 STREX 指令成功执行,local monitor 的状态迁移到 Open Access state 的状态(清除 xxx 地址上的 LDREX 的标记)。返回 thread 1 的时候,在 Open Access state 的状态下,执行 STREX 指令会导致该指令执行失败(没有 LDREX 的标记,何来 STREX),说明有其他的内核控制路径插入了。

对于 shareable memory,需要系统中所有的 local monitor 和 global monitor 共同工作,完成 exclusive access,概念类似,这里就不再赘述了。

大概的原理已经描述完毕,下面回到具体实现面。

"1:    ldrex %0, [%3]\n"

其中 %3 就是 input operand list 中的 "r" (&v->counter),r 是限制符(constraint),用来告诉编译器 gcc,你看着办吧,你帮我选择一个通用寄存器保存该操作数吧。%0 对应 output openrand list 中的 "=&r" (result),= 表示该操作数是 write only 的,& 表示该操作数是一个 earlyclobber operand,具体是什么意思呢?编译器在处理嵌入式汇编的时候,倾向使用尽可能少的寄存器,如果 output operand 没有 & 修饰的话,汇编指令中的 input 和 output 操作数会使用同样一个寄存器。因此,& 确保了 %3 和 %0 使用不同的寄存器。

(5)完成步骤(4)后,%0 这个 output 操作数已经被赋值为 atomic_t 变量的 old value。毫无疑问,这里的操作是要给 old value 加上 i (add 汇编指令 add r2, r1, r0 表示 r2 = r1 + r0,了解即可)。这里 %4 对应 "Ir" (i),这里 “I” 这个限制符对应 ARM 平台,表示这是一个有特定限制的立即数,该数必须是 0~255 之间的一个整数通过 rotation 的操作得到的一个 32bit 的立即数。这是和 ARM 的 data-processing instructions 如何解析立即数有关的。每个指令 32 个 bit,其中 12 个 bit 被用来表示立即数,其中 8 个 bit 是真正的数据,4 个 bit 用来表示如何 rotation。更详细的内容请参考 ARM 文档(了解即可)

(6)这一步将修改后的 new value 保存在 atomic_t 变量中。是否能够正确的操作的状态标记保存在 %1 操作数中,也就是 "=&r" (tmp)。

(7)检查 memory update 的操作是否正确完成(teq 汇编指令用来测试两个数是否相等,需要注意指令做的是异或运算,然后根据计算结果更新 CPSR 寄存器中的条件标志位。bne 汇编指令是 Branch if Not Equal 的缩写,通常上一条指令是比较指令,如果结果不相等就跳转到标签 1 处继续运行,其中 b 表示向后跳转。结果是否相等也是根据 CPSR 寄存器中的条件标志位来判断。对 ARM 汇编感兴趣的读者可以继续深究,对于原子操作而言,笔者所分享的内容足矣。),如果OK,皆大欢喜。如果发生了问题(有其他的内核路径插入),那么需要跳转到 lable 1 那里,重新进行一次 read-modify-write 的操作。

四、精彩评论

    值得一提的是,原文下面的评论部分也相当精彩,笔者特意精选了部分如下,欢迎各路大神参与讨论进来!

nothing
2019-08-26 12:55

a                                    b
ldrex-->设置独占标记
                                      ldrex
                                      modify-->加1操作
                                      strex
                                      ldrex
modify-->加1操作
strex
                                      modify-->加1操作
                                      strex
a、b线程在同一个CPU核上,如果按照上面执行顺序发展的话,a线程的modify是否会成功?

Alex.Rowe
2019-12-10 16:08

@nothing:提到的这个场景,a线程的modify-strex操作完成后,个人也是觉得可能会产生数据的不一致问题。谁能帮忙解决下困惑啊?

a                                                              b
ldrex-->设置独占标记
                                                                ldrex
                                                                modify-->加1操作
                                                                strex-->清除mem_addr对应的独占标记
                                                                ldrex-->设置mem_addr对应的独占标记
modify-->加1操作,修改的旧值
strex-->清除mem_addr对应的独占标记
                                                                 modify-->加1操作
                                                                 strex-->没有mem_addr对应的独占标记,所以执行失败

Alex.Rowe
2019-12-11 13:50

@Alex.Rowe:可能是Context switch时,软件上有clrex或dummy strex操作,所以a的strex会失败?

Saturn
2020-03-12 17:45

@Alex.Rowe:http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitives.pdf
针对这个问题,这份文档(page:1-7)里面有描述。上下文切换的时候local monitor会重置。

Resetting monitors
When an operating system performs a context switch, it must reset the local monitor to open state, to prevent false positives occurrig。

九五二七
2020-07-04 16:20

@Saturn:您好,请教一下,按照您的回复,a线程中的strex会失败,前面的modify并没有写进memory,但由于失败跳到1b,所以下次的ldrex如果没有context switch的话是可以modify成功的是吗,而且是在b线程modify之后?也就是说atomic_add不管怎样都会更新成功,从这个层面来看,atomic_add操作就是原子的,尽管在汇编语句中可能有switch?

cp
2021-05-07 20:34

@Saturn:如果a b是两个硬件线程呢,a线程会成功写入?

这个场景比较有趣,笔者认为需要分为单核和多核场景来分析。如果是单核情况,上面的场景中 a、b 线程必然会发生上下文切换,根据 Saturn 同学的说法,a 线程的 strex 和 b 线程的第二个 strex 都会失败,两个操作都会发生跳转。但不管怎么样,最终三个 modify 都会执行成功,且保证了互斥。如果是多核情况,a、b 线程分别在两个 CPU 上运行,那么就不存在上下文切换一说。这个场景下,操作的内存必然是 shareable memory,那么 local monitor 和 global monitor 会共同工作,此时由于 local monitor 的独占标记,a 线程的 strex 会失败,但 b 线程的第二个 strex 会成功。

 

弄堂小子
2018-10-19 10:10

请教下,
原文中:对于UP,原子操作就是通过关闭CPU中断来完成的。
  是否可以理解关闭中断有包括禁止抢占的作用,不然没法保证原子?
  评论区有提到V6之前用的是SWP和SWPB指令,现在用关中断方式处理了,这两个是如何比较取舍的?

xiaoai
2019-05-14 16:36

@弄堂小子:第一个问题我的理解:因为进程的调度从原始来说是依赖于中断的(往往是定时器中断),如果关闭中断基本上就相当于是终止了进程的调度,这样多线程访问临界资源的问题就规避了。还有一种就是其他中断可能访问这种临界资源,这时候关闭了中断也就避免了并行的修改。这样就实现了原子操作。

九五二七
2020-07-04 16:24

@弄堂小子:您的两个问题我也比较疑惑,请问有找到答案吗?

ARMv6 以前,对于UP,SWP 和关中断的方式确实都可以实现原子操作。但对于 SMP 呢?由于没有 LDREX 和 STREX,所以只能使用 SWP?问题是 ARMv6 以前有 SMP 架构吗?从正文中的源码可知答案是否定的。

 

michael
2017-03-27 11:33

有些看不懂。
对于原子操作,Linux内核是通过哪种原理或者机制,保证了同步?
博主能否用一些常规语言来描述一下。谢谢了

michael
2017-03-27 11:34

@michael:另外,汇编语言也可以被中断打断的吧?

linuxer
2017-03-28 11:47

@michael:原子操作是和CPU ARCH相关的内容,在linux内核中提供了统一的接口,底层有各自的实现。

michael
2017-03-28 15:07

@linuxer:原子操作过程中,interrupt controller是不会响应中断的吧,包括CPU中断和外设中断?

linuxer
2017-03-29 09:53

@michael:除非你disable了interrupt controller的功能,否则它应该是会响应中断请求的,并通知到CPU,当然CPU是否响应中断是看CPU是否disable了local的中断响应,如果没有,那么CPU当然会响应异步中断事件。
当然,如果原子操作只是一条指令(例如SWP指令),那么中断响应要么在该指令之前发生,要么在该指令之后发生。
如果是exclusive指令来完成原子操作,那么中断是可以插入的。

michael
2017-03-30 13:06

@linuxer:感谢您的及时回复。
您的文档中这样描述: 对于UP,原子操作就是通过关闭CPU中断来完成的
对于UP(单核),由于原子操作是不允许打断的,所以需要关闭中断来实现吗?

linuxer
2017-03-31 19:13

@michael:我文章中的原话是:
===============
(1)ARMv6之前的CPU并不支持SMP,之后的ARM架构都是支持SMP的(例如我们熟悉的ARMv7-A)。因此,对于ARM处理,其原子操作分成了两个阵营,一个是支持SMP的ARMv6之后的CPU,另外一个就是ARMv6之前的,只有单核架构的CPU。对于UP,原子操作就是通过关闭CPU中断来完成的。
===============
这里的“对于UP”是指的那些只支持UP的的ARMv6之前的ARM处理器,这些ARM处理器没有原子指令支持,read-modify-write类型的操作只能是采用关中断来实现。

michael
2017-04-04 15:18

@linuxer:感谢您的回复。
常说的,原子操作是最小的执行单元,执行过程中不会休眠,也不会中断。
这是不是针对ARMv6之前的ARM处理器的?
因为根据您的描述,从ARMv7开始,原子操作似乎可以被中断打断的。

 

ron
2017-02-27 11:29

@linuxer: 我有个问题请教一下,arm中atomic_add/atomic_sub这些操作是通过ldex/stex来实现的,但是atomic_set只是通过#define atomic_set(v, i) (((v)->counter) = (i)来实现,那么atomic_set不存在竞争的情况吗?

linuxer
2017-02-27 19:17

@ron:atomic_add/atomic_sub 这些操作三个步骤(read-modify-write):
1. read memory to register
2. add register
3. update the new value to the memory

对于atomic_set,只有上面的步骤3而已,一个str操作就OK了,因此不需要特别处理。

ron
2017-02-28 14:58

@linuxer:多谢。got it。

sgy1993
2018-02-10 11:29

@linuxer:atomic_set(v, i)----会翻译成两条汇编指令啊!
例如:
atomic_t number = ATOMIC_INIT(1);
atomic_set(&number, 4);
上面这条指令的反汇编是
  10:    e3a02004     mov    r2, #4
  14:    e5832000     str    r2, [r3]
这个怎么保证原子操作呢?加入 r2 = 4之后,别的程序修改了r2,不就有问题???

hello-world
2018-02-11 09:31

@sgy1993:能保证:
14:    e5832000     str    r2, [r3]
这条指令是原子的就OK了。

sgy1993
2018-02-11 13:12

@hello-world:我觉的应该是mov r2, #4执行完之后,是可能被中断打断的,但是中断处理的时候,会保存寄存器的值在堆栈上。中断处理返回来之后,r2的值会从堆栈上弹出来,这样counter的值仍然是4...(如果把4改成一个全局变量也是一样,理论上都是从内存某处取数据放到寄存器(ldr指令),然后再str寄存器的值-->内存)...即便ldr执行完,有中断改变了全局变量的值,但由于值已经被ldr存在寄存器里了,所以结果最终还是正确的...个人觉得原子操作应该是即便中断发生,也不影响最终结果的操作。

 

jinxin
2015-12-23 08:52

请教一个问题:
嵌入式汇编代码就不会被打断么?
"    ldrex    %0, [%3]\n"
"    add    %0, %0, %4\n"
"    strex    %1, %0, [%3]\n"
这三句代码的执行不会被打断么?感谢您的回复。

passenger
2015-12-23 13:24

@jinxin:你没有仔细看博主的文章哦,这里并不是通过汇编保证原子操作的,而是通过LDREX/STREX这样的exclusive机制。

 

puppypyb
2015-04-25 10:26

还有 %2 --> "+Qo" (v->counter) 跟没(根本)就没用到,在这里是什么意思呢?

linuxer
2015-04-26 12:18

@puppypyb:v->counter是一个output参数,上面的o说明该变量是一个内存操作数,上面的Q说明:对这个memory的访问是通过寄存器中保存该memory的地址从而间接访问的,我们看看%3那个input参数就明白了:"r" (&v->counter)

 

puppy
2015-04-24 17:33

关于exclusive monitor大神能否再说得详细点。
local/global monitor就是一个简单的状态机,维护 exclusive 和 open 两种状态,每个CPU只有一个local monitor。
那么,问题来了:local monitor同一时间只能监控一个地址XXX,还是能同时监控多个地址XXX ,YYY,ZZZ ...
如果只能监控一个地址?

linuxer
2015-04-26 12:12

@puppy:这一段在ARM ARM中的描述还是比较清楚的,而且比较专业,推荐可以看看原文。

可同时监控的地址是可以配置的(2~512个),不同的CPU实现会有不同的配置。

puppy
2015-04-27 16:25

@linuxer:thanks

benson
2018-01-02 00:31

@linuxer:我觉得这个这是说,Granularity of Exclusive Monitor的范围是2~512个字节,也就是说假如系统的ERG=64的,那么0x00000000和0x00000000c对monitor的将就是一个地址。
我觉得如果系统monitor只有一个,是否是只能监控一个ERG的范围的地址?

 

forion

2014-10-13 18:08

又涨姿势了。开心ing

linuxer
2014-10-13 19:48

@forion:其实这份文档没有描述解决read-modify-write的问题的发展过程。在ARM平台上,ARMv6之前,SWP和SWPB指令被用来支持对shared memory的访问:

SWP <Rt>, <Rt2>, [<Rn>]

Rn中保存了SWP指令要操作的内存地址,通过该指令可以将Rn指定的内存数据加载到Rt寄存器,同时将Rt2寄存器中的数值保存到Rn指定的内存中去。

这份文档中描述的read-modify-write的问题本质上是一个保持对内存read和write访问的原子性的问题。也就是说对内存的读和写的访问不能被打断。对该问题的解决可以通过硬件、软件或者软硬件结合的方法来进行。早期的ARM CPU给出的方案就是依赖硬件:SWP这个汇编指令执行了一次读内存操作、一次写内存操作。但是从程序员的角度看,SWP这条指令就是原子的,读写之间不会被任何的异步事件打断。具体底层的硬件是如何做的呢?这时候,硬件会提供一个lock signal,在进行memory操作的时候设定lock信号,告诉总线这是一个不可被中断的内存访问,直到完成了SWP需要进行的两次内存访问之后再clear lock信号。

当然,随着技术的发展,在ARMv6之后的ARM CPU已经不推荐使用SWP这样的指令,而是提供了LDREX和STREX这样的指令。这种方法是使用软硬件结合的方法来解决原子操作问题,看起来代码比较复杂,但是系统的性能可以得到提升(相对于SWP指令,LDREX和STREX的优势体现在SMP架构上,因为SWP指令会锁住内存总线,那么其他CPU只能干等着。如果是UP,那么SWP和xxxEX之间的性能差异笔者认为基本差不多。)。其实,从硬件角度看,LDREX和STREX这样的指令也是采用了lock-free的做法。

puppypyb
2015-04-25 10:32

@linuxer:linuxer见识真广。这几天我刚看过ARM的文档,文档上说传统的SWP会在硬件层面上锁内存总线,就是lock signal,对系统性能有影响,所以ARM 11处理器开始就被exclusive monitor机制替代了。

linuxer
2015-04-26 12:19

@puppypyb:客气客气,没有事情的时候我也是会翻看ARM文档,了解一些ARM体系结构的东西,有空多交流啊

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值