Linux内核同步机制之(一):原子操作
作者:linuxer 发布于:2014-10-10 17:56 分类:内核同步机制
一、源由
我们的程序逻辑经常遇到这样的操作序列:
1、读一个位于memory中的变量的值到寄存器中
2、修改该变量的值(也就是修改寄存器中的值)
3、将寄存器中的数值写回memory中的变量值
如果这个操作序列是串行化的操作(在一个thread中串行执行),那么一切OK,然而,世界总是不能如你所愿。在多CPU体系结构中,运行在两个CPU上的两个内核控制路径同时并行执行上面操作序列,有可能发生下面的场景:
CPU1上的操作 | CPU2上的操作 |
读操作 | |
读操作 | |
修改 | 修改 |
写操作 | |
写操作 |
多个CPUs和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的变量,内核提供了一个特殊的类型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
#endifstatic 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,原子操作就是通过关闭CPU中断来完成的。
(2)这里的代码和preloading cache相关。在strex指令之前将要操作的memory内容加载到cache中可以显著提高性能。
(3)为了完整性,我还是重复一下汇编嵌入c代码的语法:嵌入式汇编的语法格式是: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。
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 1 | thread 2 | local monitor的状态 |
Open Access state | ||
LDREX | Exclusive Access state | |
LDREX | Exclusive Access state | |
Modify | Exclusive Access state | |
STREX | Open 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。这里%4对应"Ir" (i),这里“I”这个限制符对应ARM平台,表示这是一个有特定限制的立即数,该数必须是0~255之间的一个整数通过rotation的操作得到的一个32bit的立即数。这是和ARM的data-processing instructions如何解析立即数有关的。每个指令32个bit,其中12个bit被用来表示立即数,其中8个bit是真正的数据,4个bit用来表示如何rotation。更详细的内容请参考ARM ARM文档。
(6)这一步将修改后的new value保存在atomic_t变量中。是否能够正确的操作的状态标记保存在%1操作数中,也就是"=&r" (tmp)。
(7)检查memory update的操作是否正确完成,如果OK,皆大欢喜,如果发生了问题(有其他的内核路径插入),那么需要跳转到lable 1那里,从新进行一次read-modify-write的操作。
原创文章,转发请注明出处。蜗窝科技
http://www.wowotech.net/linux_kenrel/atomic.html
评论:
2015-12-23 08:52
嵌入式汇编代码就不会被打断么?
ldrex %0, [%3]\n" --------------------------(4)
" add %0, %0, %4\n" --------------------------(5)
" strex %1, %0, [%3]\n" -------------------------(6)
这三句代码的执行不会被打断么?
感谢您的回复
2015-08-18 21:21
```
/tmp/ccd6iavd.s: Assembler messages:
/tmp/ccd6iavd.s:26: Error: junk at end of line, first unrecognized character is `@'
```
gcc4.9不支持@注释符么?
2015-08-19 14:06
#/usr/bin/gcc asm-test.c -v
Using built-in specs.
COLLECT_GCC=/usr/bin/gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/aarch64-redhat-linux/4.9.2/lto-wrapper
Target: aarch64-redhat-linux
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --enable-multilib --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --with-isl=/home/build/rpmbuild/BUILD/gcc-4.9.2-20141101/obj-aarch64-redhat-linux/isl-install --with-cloog=/home/build/rpmbuild/BUILD/gcc-4.9.2-20141101/obj-aarch64-redhat-linux/cloog-install --enable-gnu-indirect-function --build=aarch64-redhat-linux
Thread model: posix
gcc version 4.9.2 20141101 (Red Hat 4.9.2-1) (GCC)
COLLECT_GCC_OPTIONS='-v' '-mlittle-endian' '-mabi=lp64'
/usr/libexec/gcc/aarch64-redhat-linux/4.9.2/cc1 -quiet -v asm-test.c -quiet -dumpbase asm-test.c -mlittle-endian -mabi=lp64 -auxbase asm-test -version -o /tmp/ccHLG3ae.s
GNU C (GCC) version 4.9.2 20141101 (Red Hat 4.9.2-1) (aarch64-redhat-linux)
compiled by GNU C version 4.9.2 20141101 (Red Hat 4.9.2-1), GMP version 6.0.0, MPFR version 3.1.2, MPC version 1.0.2
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring nonexistent directory "/usr/lib/gcc/aarch64-redhat-linux/4.9.2/include-fixed"
ignoring nonexistent directory "/usr/lib/gcc/aarch64-redhat-linux/4.9.2/../../../../aarch64-redhat-linux/include"
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/aarch64-redhat-linux/4.9.2/include
/usr/local/include
/usr/include
End of search list.
GNU C (GCC) version 4.9.2 20141101 (Red Hat 4.9.2-1) (aarch64-redhat-linux)
compiled by GNU C version 4.9.2 20141101 (Red Hat 4.9.2-1), GMP version 6.0.0, MPFR version 3.1.2, MPC version 1.0.2
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: a6fe4020046dbad896213dcd0a3034ff
COLLECT_GCC_OPTIONS='-v' '-mlittle-endian' '-mabi=lp64'
as -v -EL -mabi=lp64 -o /tmp/ccYexbch.o /tmp/ccHLG3ae.s
GNU assembler version 2.24 (aarch64-redhat-linux) using BFD version version 2.24
/tmp/ccHLG3ae.s: Assembler messages:
/tmp/ccHLG3ae.s:26: Error: junk at end of line, first unrecognized character is `@'
2015-08-19 14:08
#include <stdio.h>
int main()
{
int res = 0, input = 1;
__asm__ __volatile__("@ print res=input\n"
" mov %1,%0"
:"=&r"(res)
:"r"(input)
:"cc"
);
printf("%d",res);
}
2015-08-19 14:33
2015-08-12 07:02
task1:
1 add_waitqueue();
2 set_current_state(TASH_UNINTERRUPTIBLE);
3 schedule();
上面的代码是大多同步机制使用的,让线程进入阻塞的一种方式,个人的理解,对于1,2,3的所有操作应该是一个
临界段,但是内核中所的代码都是这样操作。比如如果在2和3的操作中发生抢占,而获得运行的线程,正好release 相关的资源,并把task1从相应的waitqueue中移除,而当task1再次运行时,接着进行schedule,让自己让出cpu, 但此时的task1则丢失了此次的信号。不知道自己应该从哪方面去理解类似的这种场景,希望解惑!
2015-08-12 11:26
void down(struct semaphore *sem)
{
raw_spin_lock_irqsave(&sem->lock, flags);----加锁
list_add_tail(&waiter.list, &sem->wait_list);--挂入等待队列
for (;;) {
__set_task_state(task, state);-----设定任务状态
raw_spin_unlock_irq(&sem->lock);----解锁
timeout = schedule_timeout(timeout);-----调度
raw_spin_lock_irq(&sem->lock);------上锁
}
raw_spin_unlock_irqrestore(&sem->lock, flags);----解锁
}
从上面的代码可以看出来,1和2是临界区,被spin lock保护的。
2015-04-24 17:33
local/global monitor就是一个简单的状态机,维护 exclusive 和 open两种状态,每个CPU只有一个local monitor。
那么,问题来了:local monitor同一时间只能监控一个地址XXX,还是能同时监控多个地址XXX ,YYY,ZZZ ...
如果只能监控一个地址?
2015-04-26 12:12
可同时监控的地址是可以配置的(2~512个),不同的CPU实现会有不同的配置。
2014-10-20 18:06
2014-10-13 19:48
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这样的指令。这种方法是使用软硬件结合的方法来解决原子操作问题,看起来代码比较复杂,但是系统的性能可以得到提升。其实,从硬件角度看,LDREX和STREX这样的指令也是采用了lock-free的做法。
功能
最新评论
- 毋庸置疑
看完了,感谢,,催更来了 - rzbdz
请教一下,为什么 __queue_work 中读取 wq->... - 水禾田
大神请教一下,mips架构,使用cpufreq框架动态调整C... - bngvomavoj
英雄王座新魔界服务端出售www.45ur.com776356... - zrant
为什么调大cpu.cfs_period_us会有更大吞吐量。... - SuiTang
请教下大神,蓝牙Beacon的Local Name可以重复吗...
文章分类
随机文章
文章存档
- 2024年2月(1)
- 2023年5月(1)
- 2022年10月(1)
- 2022年8月(1)
- 2022年6月(1)
- 2022年5月(1)
- 2022年4月(2)
- 2022年2月(2)
- 2021年12月(1)
- 2021年11月(5)
- 2021年7月(1)
- 2021年6月(1)
- 2021年5月(3)
- 2020年3月(3)
- 2020年2月(2)
- 2020年1月(3)
- 2019年12月(3)
- 2019年5月(4)
- 2019年3月(1)
- 2019年1月(3)
- 2018年12月(2)
- 2018年11月(1)
- 2018年10月(2)
- 2018年8月(1)
- 2018年6月(1)
- 2018年5月(1)
- 2018年4月(7)
- 2018年2月(4)
- 2018年1月(5)
- 2017年12月(2)
- 2017年11月(2)
- 2017年10月(1)
- 2017年9月(5)
- 2017年8月(4)
- 2017年7月(4)
- 2017年6月(3)
- 2017年5月(3)
- 2017年4月(1)
- 2017年3月(8)
- 2017年2月(6)
- 2017年1月(5)
- 2016年12月(6)
- 2016年11月(11)
- 2016年10月(9)
- 2016年9月(6)
- 2016年8月(9)
- 2016年7月(5)
- 2016年6月(8)
- 2016年5月(8)
- 2016年4月(7)
- 2016年3月(5)
- 2016年2月(5)
- 2016年1月(6)
- 2015年12月(6)
- 2015年11月(9)
- 2015年10月(9)
- 2015年9月(4)
- 2015年8月(3)
- 2015年7月(7)
- 2015年6月(3)
- 2015年5月(6)
- 2015年4月(9)
- 2015年3月(9)
- 2015年2月(6)
- 2015年1月(6)
- 2014年12月(17)
- 2014年11月(8)
- 2014年10月(9)
- 2014年9月(7)
- 2014年8月(12)
- 2014年7月(6)
- 2014年6月(6)
- 2014年5月(9)
- 2014年4月(9)
- 2014年3月(7)
- 2014年2月(3)
- 2014年1月(4)
2016-07-11 21:41