Linux内核同步机制--转发自蜗窝科技
Posted 逸蒙
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux内核同步机制--转发自蜗窝科技相关的知识,希望对你有一定的参考价值。
Linux内核同步机制之(一):原子操作
http://www.wowotech.net/linux_kenrel/atomic.html
一、源由
我们的程序逻辑经常遇到这样的操作序列:
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的操作。
Linux内核同步机制之(二):Per-CPU变量
http://www.wowotech.net/linux_kenrel/per-cpu.html
一、源由:为何引入Per-CPU变量?
1、lock bus带来的性能问题
在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信号。
lock memory bus对多核系统的性能造成严重的影响(系统中其他的processor对那条被lock的memory bus的访问就被hold住了),如何解决这个问题?最好的锁机制就是不使用锁,因此解决这个问题可以使用釜底抽薪的方法,那就是不在系统中的多个processor之间共享数据,给每一个CPU分配一个不就OK了吗。
当然,随着技术的发展,在ARMv6之后的ARM CPU已经不推荐使用SWP这样的指令,而是提供了LDREX和STREX这样的指令。这种方法是使用软硬件结合的方法来解决原子操作问题,看起来代码比较复杂,但是系统的性能可以得到提升。其实,从硬件角度看,LDREX和STREX这样的指令也是采用了lock-free的做法。OK,由于不再lock bus,看起来Per-CPU变量存在的基础被打破了。不过考虑cache的操作,实际上它还是有意义的。
2、cache的影响
在The Memory Hierarchy文档中,我们已经了解了关于memory一些基础的知识,一些基础的内容,这里就不再重复了。我们假设一个多核系统中的cache如下:
每个CPU都有自己的L1 cache(包括data cache和instruction cache),所有的CPU共用一个L2 cache。L1、L2以及main memory的访问速度之间的差异都是非常大,最高的性能的情况下当然是L1 cache hit,这样就不需要访问下一阶memory来加载cache line。
我们首先看在多个CPU之间共享内存的情况。这种情况下,任何一个CPU如果修改了共享内存就会导致所有其他CPU的L1 cache上对应的cache line变成invalid(硬件完成)。虽然对性能造成影响,但是系统必须这么做,因为需要维持cache的同步。将一个共享memory变成Per-CPU memory本质上是一个耗费更多memory来解决performance的方法。当一个在多个CPU之间共享的变量变成每个CPU都有属于自己的一个私有的变量的时候,我们就不必考虑来自多个CPU上的并发,仅仅考虑本CPU上的并发就OK了。当然,还有一点要注意,那就是在访问Per-CPU变量的时候,不能调度,当然更准确的说法是该task不能调度到其他CPU上去。目前的内核的做法是在访问Per-CPU变量的时候disable preemptive,虽然没有能够完全避免使用锁的机制(disable preemptive也是一种锁的机制),但毫无疑问,这是一种代价比较小的锁。
二、接口
1、静态声明和定义Per-CPU变量的API如下表所示:
声明和定义Per-CPU变量的API
描述
DECLARE_PER_CPU(type, name)
DEFINE_PER_CPU(type, name)
普通的、没有特殊要求的per cpu变量定义接口函数。没有对齐的要求
DECLARE_PER_CPU_FIRST(type, name)
DEFINE_PER_CPU_FIRST(type, name)
通过该API定义的per cpu变量位于整个per cpu相关section的最前面。
DECLARE_PER_CPU_SHARED_ALIGNED(type, name)
DEFINE_PER_CPU_SHARED_ALIGNED(type, name)
通过该API定义的per cpu变量在SMP的情况下会对齐到L1 cache line ,对于UP,不需要对齐到cachine line
DECLARE_PER_CPU_ALIGNED(type, name)
DEFINE_PER_CPU_ALIGNED(type, name)
无论SMP或者UP,都是需要对齐到L1 cache line
DECLARE_PER_CPU_PAGE_ALIGNED(type, name)
DEFINE_PER_CPU_PAGE_ALIGNED(type, name)
为定义page aligned per cpu变量而设定的API接口
DECLARE_PER_CPU_READ_MOSTLY(type, name)
DEFINE_PER_CPU_READ_MOSTLY(type, name)
通过该API定义的per cpu变量是read mostly的
看到这样“丰富多彩”的Per-CPU变量的API,你是不是已经醉了。这些定义使用在不同的场合,主要的factor包括:
-该变量在section中的位置
-该变量的对齐方式
-该变量对SMP和UP的处理不同
-访问per cpu的形态
例如:如果你准备定义的per cpu变量是要求按照page对齐的,那么在定义该per cpu变量的时候需要使用DECLARE_PER_CPU_PAGE_ALIGNED。如果只要求在SMP的情况下对齐到cache line,那么使用DECLARE_PER_CPU_SHARED_ALIGNED来定义该per cpu变量。
2、访问静态声明和定义Per-CPU变量的API
静态定义的per cpu变量不能象普通变量那样进行访问,需要使用特定的接口函数,具体如下:
get_cpu_var(var)
put_cpu_var(var)
上面这两个接口函数已经内嵌了锁的机制(preempt disable),用户可以直接调用该接口进行本CPU上该变量副本的访问。如果用户确认当前的执行环境已经是preempt disable(或者是更厉害的锁,例如关闭了CPU中断),那么可以使用lock-free版本的Per-CPU变量的API:__get_cpu_var。
3、动态分配Per-CPU变量的API如下表所示:
动态分配和释放Per-CPU变量的API
描述
alloc_percpu(type)
分配类型是type的per cpu变量,返回per cpu变量的地址(注意:不是各个CPU上的副本)
void free_percpu(void __percpu *ptr)
释放ptr指向的per cpu变量空间
4、访问动态分配Per-CPU变量的API如下表所示:
访问Per-CPU变量的API
描述
get_cpu_ptr
这个接口是和访问静态Per-CPU变量的get_cpu_var接口是类似的,当然,这个接口是for 动态分配Per-CPU变量
put_cpu_ptr
同上
per_cpu_ptr(ptr, cpu)
根据per cpu变量的地址和cpu number,返回指定CPU number上该per cpu变量的地址
三、实现
1、静态Per-CPU变量定义
我们以DEFINE_PER_CPU的实现为例子,描述linux kernel中如何实现静态Per-CPU变量定义。具体代码如下:
#define DEFINE_PER_CPU(type, name) \
DEFINE_PER_CPU_SECTION(type, name, "")
type就是变量的类型,name是per cpu变量符号。DEFINE_PER_CPU_SECTION宏可以把一个per cpu变量放到指定的section中,具体代码如下:
#define DEFINE_PER_CPU_SECTION(type, name, sec) \
__PCPU_ATTRS(sec) PER_CPU_DEF_ATTRIBUTES \-----安排section
__typeof__(type) name----------------------定义变量
在这里具体arch specific的percpu代码中(arch/arm/include/asm/percpu.h)可以定义PER_CPU_DEF_ATTRIBUTES,以便控制该per cpu变量的属性,当然,如果arch specific的percpu代码不定义,那么在general arch-independent的代码中(include/asm-generic/percpu.h)会定义为空。这里可以顺便提一下Per-CPU变量的软件层次:
(1)arch-independent interface。在include/linux/percpu.h文件中,定义了内核其他模块要使用per cpu机制使用的接口API以及相关数据结构的定义。内核其他模块需要使用per cpu变量接口的时候需要include该头文件
(2)arch-general interface。在include/asm-generic/percpu.h文件中。如果所有的arch相关的定义都是一样的,那么就把它抽取出来,放到asm-generic目录下。毫无疑问,这个文件定义的接口和数据结构是硬件相关的,只不过软件抽象各个arch-specific的内容,形成一个arch general layer。一般来说,我们不需要直接include该头文件,include/linux/percpu.h会include该头文件。
(3)arch-specific。这是和硬件相关的接口,在arch/arm/include/asm/percpu.h,定义了ARM平台中,具体和per cpu相关的接口代码。
我们回到正题,看看__PCPU_ATTRS的定义:
#define __PCPU_ATTRS(sec) \
__percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) \
PER_CPU_ATTRIBUTES
PER_CPU_BASE_SECTION 定义了基础的section name symbol,定义如下:
#ifndef PER_CPU_BASE_SECTION
#ifdef CONFIG_SMP
#define PER_CPU_BASE_SECTION ".data..percpu"
#else
#define PER_CPU_BASE_SECTION ".data"
#endif
#endif
虽然有各种各样的静态Per-CPU变量定义方法,但是都是类似的,只不过是放在不同的section中,属性不同而已,这里就不看其他的实现了,直接给出section的安排:
(1)普通per cpu变量的section安排
SMP
UP
Build-in kernel
".data..percpu" section
".data" section
defined in module
".data..percpu" section
".data" section
(2)first per cpu变量的section安排
SMP
UP
Build-in kernel
".data..percpu..first" section
".data" section
defined in module
".data..percpu..first" section
".data" section
(3)SMP shared aligned per cpu变量的section安排
SMP
UP
Build-in kernel
".data..percpu..shared_aligned" section
".data" section
defined in module
".data..percpu" section
".data" section
(4)aligned per cpu变量的section安排
SMP
UP
Build-in kernel
".data..percpu..shared_aligned" section
".data..shared_aligned" section
defined in module
".data..percpu" section
".data..shared_aligned" section
(5)page aligned per cpu变量的section安排
SMP
UP
Build-in kernel
".data..percpu..page_aligned" section
".data..page_aligned" section
defined in module
".data..percpu..page_aligned" section
".data..page_aligned" section
(6)read mostly per cpu变量的section安排
SMP
UP
Build-in kernel
".data..percpu..readmostly" section
".data..readmostly" section
defined in module
".data..percpu..readmostly" section
".data..readmostly" section
了解了静态定义Per-CPU变量的实现,但是为何要引入这么多的section呢?对于kernel中的普通变量,经过了编译和链接后,会被放置到.data或者.bss段,系统在初始化的时候会准备好一切(例如clear bss),由于per cpu变量的特殊性,内核将这些变量放置到了其他的section,位于kernel address space中__per_cpu_start和__per_cpu_end之间,我们称之Per-CPU变量的原始变量(我也想不出什么好词了)。
只有Per-CPU变量的原始变量还是不够的,必须为每一个CPU建立一个副本,怎么建?直接静态定义一个NR_CPUS的数组?NR_CPUS定义了系统支持的最大的processor的个数,并不是实际中系统processor的数目,这样的定义非常浪费内存。此外,静态定义的数据在内存中连续,对于UMA系统而言是OK的,对于NUMA系统,每个CPU上的Per-CPU变量的副本应该位于它访问最快的那段memory上,也就是说Per-CPU变量的各个CPU副本可能是散布在整个内存地址空间的,而这些空间之间是有空洞的。本质上,副本per cpu内存的分配归属于内存管理子系统,因此,分配per cpu变量副本的内存本文不会详述,大致的思路如下:
内存管理子系统会根据当前的内存配置为每一个CPU分配一大块memory,对于UMA,这个memory也是位于main memory,对于NUMA,有可能是分配最靠近该CPU的memory(也就是说该cpu访问这段内存最快),但无论如何,这些都是内存管理子系统需要考虑的。无论静态还是动态per cpu变量的分配,其机制都是一样的,只不过,对于静态per cpu变量,需要在系统初始化的时候,对应per cpu section,预先动态分配一个同样size的per cpu chunk。在vmlinux.lds.h文件中,定义了percpu section的排列情况:
#define PERCPU_INPUT(cacheline) \
VMLINUX_SYMBOL(__per_cpu_start) = .; \
*(.data..percpu..first) \
. = ALIGN(PAGE_SIZE); \
*(.data..percpu..page_aligned) \
. = ALIGN(cacheline); \
*(.data..percpu..readmostly) \
. = ALIGN(cacheline); \
*(.data..percpu) \
*(.data..percpu..shared_aligned) \
VMLINUX_SYMBOL(__per_cpu_end) = .;
对于build in内核的那些per cpu变量,必然位于__per_cpu_start和__per_cpu_end之间的per cpu section。在系统初始化的时候(setup_per_cpu_areas),分配per cpu memory chunk,并将per cpu section copy到每一个chunk中。
2、访问静态定义的per cpu变量
代码如下:
#define get_cpu_var(var) (*({ \
preempt_disable(); \
&__get_cpu_var(var); }))
再看到get_cpu_var和__get_cpu_var这两个符号,相信广大人民群众已经相当的熟悉,一个持有锁的版本,一个lock-free的版本。为防止当前task由于抢占而调度到其他的CPU上,在访问per cpu memory的时候都需要使用preempt_disable这样的锁的机制。我们来看__get_cpu_var:
#define __get_cpu_var(var) (*this_cpu_ptr(&(var)))
#define this_cpu_ptr(ptr) __this_cpu_ptr(ptr)
对于ARM平台,我们没有定义__this_cpu_ptr,因此采用asm-general版本的:
#define __this_cpu_ptr(ptr) SHIFT_PERCPU_PTR(ptr, __my_cpu_offset)
SHIFT_PERCPU_PTR这个宏定义从字面上就可以看出它是可以从原始的per cpu变量的地址,通过简单的变换(SHIFT)转成实际的per cpu变量副本的地址。实际上,per cpu内存管理模块可以保证原始的per cpu变量的地址和各个CPU上的per cpu变量副本的地址有简单的线性关系(就是一个固定的offset)。__my_cpu_offset这个宏定义就是和offset相关的,如果arch specific没有定义,那么可以采用asm general版本的,如下:
#define __my_cpu_offset per_cpu_offset(raw_smp_processor_id())
raw_smp_processor_id可以获取本CPU的ID,如果没有arch specific没有定义__per_cpu_offset这个宏,那么offset保存在__per_cpu_offset的数组中(下面只是数组声明,具体定义在mm/percpu.c文件中),如下:
#ifndef __per_cpu_offset
extern unsigned long __per_cpu_offset[NR_CPUS];#define per_cpu_offset(x) (__per_cpu_offset[x])
#endif
对于ARMV6K和ARMv7版本,offset保存在TPIDRPRW寄存器中,这样是为了提升系统性能。
3、动态分配per cpu变量
这部分内容留给内存管理子系统吧。
Linux内核同步机制之(三):memory barrier
http://www.wowotech.net/kernel_synchronization/memory-barrier.html
一、前言
我记得以前上学的时候大家经常说的一个词汇叫做所见即所得,有些编程工具是所见即所得的,给程序员带来极大的方便。对于一个c程序员,我们的编写的代码能所见即所得吗?我们看到的c程序的逻辑是否就是最后CPU运行的结果呢?很遗憾,不是,我们的“所见”和最后的执行结果隔着:
1、编译器
2、CPU取指执行
编译器将符合人类思考的逻辑(c代码)翻译成了符合CPU运算规则的汇编指令,编译器了解底层CPU的思维模式,因此,它可以在将c翻译成汇编的时候进行优化(例如内存访问指令的重新排序),让产出的汇编指令在CPU上运行的时候更快。然而,这种优化产出的结果未必符合程序员原始的逻辑,因此,作为程序员,作为c程序员,必须有能力了解编译器的行为,并在通过内嵌在c代码中的memory barrier来指导编译器的优化行为(这种memory barrier又叫做优化屏障,Optimization barrier),让编译器产出即高效,又逻辑正确的代码。
CPU的核心思想就是取指执行,对于in-order的单核CPU,并且没有cache(这种CPU在现实世界中还存在吗?),汇编指令的取指和执行是严格按照顺序进行的,也就是说,汇编指令就是所见即所得的,汇编指令的逻辑被严格的被CPU执行。然而,随着计算机系统越来越复杂(多核、cache、superscalar、out-of-order),使用汇编指令这样贴近处理器的语言也无法保证其被CPU执行的结果的一致性,从而需要程序员(看,人还是最不可以替代的)告知CPU如何保证逻辑正确。
综上所述,memory barrier是一种保证内存访问顺序的一种方法,让系统中的HW block(各个cpu、DMA controler、device等)对内存有一致性的视角。
二、不使用memory barrier会导致问题的场景
1、编译器的优化
我们先看下面的一个例子:
preempt_disable()
临界区
preempt_enable
有些共享资源可以通过禁止任务抢占来进行保护,因此临界区代码被preempt_disable和preempt_enable给保护起来。其实,我们知道所谓的preempt enable和disable其实就是对当前进程的struct thread_info中的preempt_count进行加一和减一的操作。具体的代码如下:
#define preempt_disable() \
do { \
preempt_count_inc(); \
barrier(); \
} while (0)
linux kernel中的定义和我们的想像一样,除了barrier这个优化屏障。barrier就象是c代码中的一个栅栏,将代码逻辑分成两段,barrier之前的代码和barrier之后的代码在经过编译器编译后顺序不能乱掉。也就是说,barrier之后的c代码对应的汇编,不能跑到barrier之前去,反之亦然。之所以这么做是因为在我们这个场景中,如果编译为了榨取CPU的performace而对汇编指令进行重排,那么临界区的代码就有可能位于preempt_count_inc之外,从而起不到保护作用。
现在,我们知道了增加barrier的作用,问题来了,barrier是否够呢?对于multi-core的系统,只有当该task被调度到该CPU上执行的时候,该CPU才会访问该task的preempt count,因此对于preempt enable和disable而言,不存在多个CPU同时访问的场景。但是,即便这样,如果CPU是乱序执行(out-of-order excution)的呢?其实,我们也不用担心,正如前面叙述的,preempt count这个memory实际上是不存在多个cpu同时访问的情况,因此,它实际上会本cpu的进程上下文和中断上下文访问。能终止当前thread执行preempt_disable的只有中断。为了方便描述,我们给代码编址,如下:
地址
该地址的汇编指令
CPU的执行顺序
a
preempt_disable()
临界区指令1
a+4
临界区指令1
preempt_disable()
a+8
临界区指令2
临界区指令2
a+12
preempt_enable
preempt_enable
当发生中断的时候,硬件会获取当前PC值,并精确的得到了发生指令的地址。有两种情况:
(1)在地址a发生中断。对于out-of-order的CPU,临界区指令1已经执行完毕,preempt_disable正在pipeline中等待执行。由于是在a地址发生中断,也就是preempt_disable地址上发生中断,对于硬件而言,它会保证a地址之前(包括a地址)的指令都被执行完毕,并且a地址之后的指令都没有执行。因此,在这种情况下,临界区指令1的执行结果被抛弃掉,因此,实际临界区指令不会先于preempt_disable执行
(2)在地址a+4发生中断。这时候,虽然发生中断的那一刻的地址上的指令(临界区指令1)已经执行完毕了,但是硬件会保证地址a+4之前的所有的指令都执行完毕,因此,实际上CPU会执行完preempt_disable,然后跳转的中断异常向量执行。
上面描述的是优化屏障在内存中的变量的应用,下面我们看看硬件寄存器的场景。一般而言,串口的驱动都会包括控制台部分的代码,例如:
static struct console xx_serial_console = {
……
.write = xx_serial_console_write,
……
};
如果系统enable了串口控制台,那么当你的驱动调用printk的时候,实际上最终是通过console的write函数输出到了串口控制台。而这个console write的函数可能会包含下面的代码:
do {
获取TX FIFO状态寄存器
barrier();
} while (TX FIFO没有ready);
写TX FIFO寄存器;
对于某些CPU archtecture而言(至少ARM是这样的),外设硬件的IO地址也被映射到了一段内存地址空间,对编译器而言,它并不知道这些地址空间是属于外设的。因此,对于上面的代码,如果没有barrier的话,获取TX FIFO状态寄存器的指令可能和写TX FIFO寄存器指令进行重新排序,在这种情况下,程序逻辑就不对了,因为我们必须要保证TX FIFO ready的情况下才能写TX FIFO寄存器。
对于multi core的情况,上面的代码逻辑也是OK的,因为在调用console write函数的时候,要获取一个console semaphore,确保了只有一个thread进入,因此,console write的代码不会在多个CPU上并发。和preempt count的例子一样,我们可以问同样的问题,如果CPU是乱序执行(out-of-order excution)的呢?barrier只是保证compiler输出的汇编指令的顺序是OK的,不能确保CPU执行时候的乱序。 对这个问题的回答来自ARM architecture的内存访问模型:对于program order是A1-->A2的情况(A1和A2都是对Device或是Strongly-ordered的memory进行访问的指令),ARM保证A1也是先于A2执行的。因此,在这样的场景下,使用barrier足够了。 对于X86也是类似的,虽然它没有对IO space采样memory mapping的方式,但是,X86的所有操作IO端口的指令都是被顺执行的,不需要考虑memory access order。
2、cpu architecture和cache的组织
注:本章节的内容来自对Paul E. McKenney的Why memory barriers文档理解,更细致的内容可以参考该文档。这个章节有些晦涩,需要一些耐心。作为一个c程序员,你可能会抱怨,为何设计CPU的硬件工程师不能屏蔽掉memory barrier的内容,让c程序员关注在自己需要关注的程序逻辑上呢?本章可以展开叙述,或许能解决一些疑问。
(1)基本概念
在The Memory Hierarchy文档中,我们已经了解了关于cache一些基础的知识,一些基础的内容,这里就不再重复了。我们假设一个多核系统中的cache如下:
我们先了解一下各个cpu cache line状态的迁移过程:
(a)我们假设在有一个memory中的变量为多个CPU共享,那么刚开始的时候,所有的CPU的本地cache中都没有该变量的副本,所有的cacheline都是invalid状态。
(b)因此当cpu 0 读取该变量的时候发生cache miss(更具体的说叫做cold miss或者warmup miss)。当该值从memory中加载到chache 0中的cache line之后,该cache line的状态被设定为shared,而其他的cache都是Invalid。
(c)当cpu 1 读取该变量的时候,chache 1中的对应的cache line也变成shared状态。其实shared状态就是表示共享变量在一个或者多个cpu的cache中有副本存在。既然是被多个cache所共享,那么其中一个CPU就不能武断修改自己的cache而不通知其他CPU的cache,否则会有一致性问题。
(d)总是read多没劲,我们让CPU n对共享变量来一个load and store的操作。这时候,CPU n发送一个read invalidate命令,加载了Cache n的cache line,并将状态设定为exclusive,同时将所有其他CPU的cache对应的该共享变量的cacheline设定为invalid状态。正因为如此,CPU n实际上是独占了变量对应的cacheline(其他CPU的cacheline都是invalid了,系统中就这么一个副本),就算是写该变量,也不需要通知其他的CPU。CPU随后的写操作将cacheline设定为modified状态,表示cache中的数据已经dirty,和memory中的不一致了。modified状态和exclusive状态都是独占该cacheline,但是modified状态下,cacheline的数据是dirty的,而exclusive状态下,cacheline中的数据和memory中的数据是一致的。当该cacheline被替换出cache的时候,modified状态的cacheline需要write back到memory中,而exclusive状态不需要。
(e)在cacheline没有被替换出CPU n的cache之前,CPU 0再次读该共享变量,这时候会怎么样呢?当然是cache miss了(因为之前由于CPU n写的动作而导致其他cpu的cache line变成了invalid,这种cache miss叫做communiction miss)。此外,由于CPU n的cache line是modified状态,它必须响应这个读得操作(memory中是dirty的)。因此,CPU 0的cacheline变成share状态(在此之前,CPU n的cache line应该会发生write back动作,从而导致其cacheline也是shared状态)。当然,也可能是CPU n的cache line不发生write back动作而是变成invalid状态,CPU 0的cacheline变成modified状态,这和具体的硬件设计相关。
(2)Store buffer
我们考虑另外一个场景:在上一节中step e中的操作变成CPU 0对共享变量进行写的操作。这时候,写的性能变得非常的差,因为CPU 0必须要等到CPU n上的cacheline 数据传递到其cacheline之后,才能进行写的操作(CPU n上的cacheline 变成invalid状态,CPU 0则切换成exclusive状态,为后续的写动作做准备)。而从一个CPU的cacheline传递数据到另外一个CPU的cacheline是非常消耗时间的,而这时候,CPU 0的写的动作只是hold住,直到cacheline的数据完成传递。而实际上,这样的等待是没有意义的,因此,这时候cacheline的数据仍然会被覆盖掉。为了解决这个问题,多核系统中的cache修改如下:
这样,问题解决了,写操作不必等到cacheline被加载,而是直接写到store buffer中然后欢快的去干其他的活。在CPU n的cacheline把数据传递到其cache 0的cacheline之后,硬件将store buffer中的内容写入cacheline。
虽然性能问题解决了,但是逻辑错误也随之引入,我们可以看下面的例子:
我们假设a和b是共享变量,初始值都是0,可以被cpu0和cpu1访问。cpu 0的cache中保存了b的值(exclusive状态),没有a的值,而cpu 1的cache中保存了a的值,没有b的值,cpu 0执行的汇编代码是(用的是ARM汇编,没有办法,其他的都不是那么熟悉):
ldr r2, [pc, #28] -------------------------- 取变量a的地址
ldr r4, [pc, #20] -------------------------- 取变量b的地址
mov r3, #1
str r3, [r2] --------------------------a=1
str r3, [r4] --------------------------b=1
CPU 1执行的代码是:
ldr r2, [pc, #28] -------------------------- 取变量a的地址
ldr r3, [pc, #20] -------------------------- 取变量b的地址
start: ldr r3, [r3] -------------------------- 取变量b的值
cmp r3, #0 ------------------------ b的值是否等于0?
beq start ------------------------ 等于0的话跳转到startldr r2, [r2] -------------------------- 取变量a的值
当cpu 1执行到--取变量a的值--这条指令的时候,b已经是被cpu0修改为1了,这也就是说a=1这个代码已经执行了,因此,从汇编代码的逻辑来看,这时候a值应该是确定的1。然而并非如此,cpu 0和cpu 1执行的指令和动作描述如下:
cpu 0执行的指令
cpu 0动作描述
cpu 1执行的指令
cpu 1动作描述
str r3, [r2]
(a=1)
1、发生cache miss
2、将1保存在store buffer中
3、发送read invalidate命令,试图从cpu 1的cacheline中获取数据,并invalidate其cache line
注:这里无需等待response,立刻执行下一条指令
ldr r3, [r3]
(获取b的值)
1、发生cache miss
2、发送read命令,试图加载b对应的cacheline
注:这里cpu必须等待read response,下面的指令依赖于这个读取的结果
str r3, [r4]
(b=1)
1、cache hit
2、cacheline中的值被修改为1,状态变成modified
响应cpu 1的read命令,发送read response(b=1)给CPU 0。write back,将状态设定为shared
cmp r3, #0
1、cpu 1收到来自cpu 0的read response,加载b对应的cacheline,状态为shared
2、b等于1,因此不必跳转到start执行
ldr r2, [r2]
(获取a的值)
1、cache hit
2、获取了a的旧值,也就是0
响应CPU 0的read invalid命令,将a对应的cacheline设为invalid状态,发送read response和invalidate ack。但是已经酿成大错了。
收到来自cpu 1的响应,将store buffer中的1写入cache line。
对于硬件,CPU不清楚具体的代码逻辑,它不可能直接帮助软件工程师,只是提供一些memory barrier的指令,让软件工程师告诉CPU他想要的内存访问逻辑顺序。这时候,cpu 0的代码修改如下:
ldr r2, [pc, #28] -------------------------- 取变量a的地址
ldr r4, [pc, #20] -------------------------- 取变量b的地址
mov r3, #1
str r3, [r2] --------------------------a=1确保清空store buffer的memory barrier instruction
str r3, [r4] --------------------------b=1
这种情况下,cpu 0和cpu 1执行的指令和动作描述如下:
cpu 0执行的指令
cpu 0动作描述
cpu 1执行的指令
cpu 1动作描述
str r3, [r2]
(a=1)
1、发生cache miss
2、将1保存在store buffer中
3、发送read invalidate命令,试图从cpu 1的cacheline中获取数据,并invalidate其cache line
注:这里无需等待response,立刻执行下一条指令
ldr r3, [r3]
(获取b的值)
1、发生cache miss
2、发送read命令,试图加载b对应的cacheline
注:这里cpu必须等待read response,下面的指令依赖于这个读取的结果
memory barrier instruction
CPU收到memory barrier指令,知道软件要控制访问顺序,因此不会执行下一条str指令,要等到收到read response和invalidate ack后,将store buffer中所有数据写到cacheline之后才会执行后续的store指令
cmp r3, #0
beq start
1、cpu 1收到来自cpu 0的read response,加载b对应的cacheline,状态为shared
2、b等于0,跳转到start执行
响应CPU 0的read invalid命令,将a对应的cacheline设为invalid状态,发送read response和invalidate ack。
收到来自cpu 1的响应,将store buffer中的1写入cache line。
str r3, [r4]
(b=1)
1、cache hit,但是cacheline状态是shared,需要发送invalidate到cpu 1
2、将1保存在store buffer中
注:这里无需等待invalidate ack,立刻执行下一条指令
…
…
…
…
由于增加了memory barrier,保证了a、b这两个变量的访问顺序,从而保证了程序逻辑。
(3)Invalidate Queue
我们先回忆一下为何出现了stroe buffer:为了加快cache miss状态下写的性能,硬件提供了store buffer,以便让CPU先写入,从而不必等待invalidate ack(这些交互是为了保证各个cpu的cache的一致性)。然而,store buffer的size比较小,不需要特别多的store命令(假设每次都是cache miss)就可以将store buffer填满,这时候,没有空间写了,因此CPU也只能是等待invalidate ack了,这个状态和memory barrier指令的效果是一样的。
怎么解决这个问题?CPU设计的硬件工程师对性能的追求是不会停歇的。我们首先看看invalidate ack为何如此之慢呢?这主要是因为cpu在收到invalidate命令后,要对cacheline执行invalidate命令,确保该cacheline的确是invalid状态后,才会发送ack。如果cache正忙于其他工作,当然不能立刻执行invalidate命令,也就无法会ack。
怎么破?CPU设计的硬件工程师提供了下面的方法:
Invalidate Queue这个HW block从名字就可以看出来是保存invalidate请求的队列。其他CPU发送到本CPU的invalidate命令会保存于此,这时候,并不需要等到实际对cacheline的invalidate操作完成,CPU就可以回invalidate ack了。
同store buffer一样,虽然性能问题解决了,但是对memory的访问顺序导致的逻辑错误也随之引入,我们可以看下面的例子(和store buffer中的例子类似):
我们假设a和b是共享变量,初始值都是0,可以被cpu0和cpu1访问。cpu 0的cache中保存了b的值(exclusive状态),而CPU 1和CPU 0的cache中都保存了a的值,状态是shared。cpu 0执行的汇编代码是:
ldr r2, [pc, #28] -------------------------- 取变量a的地址
ldr r4, [pc, #20] -------------------------- 取变量b的地址
mov r3, #1
str r3, [r2] --------------------------a=1确保清空store buffer的memory barrier instruction
str r3, [r4] --------------------------b=1
CPU 1执行的代码是:
ldr r2, [pc, #28] -------------------------- 取变量a的地址
ldr r3, [pc, #20] -------------------------- 取变量b的地址
start: ldr r3, [r3] -------------------------- 取变量b的值
cmp r3, #0 ------------------------ b的值是否等于0?
beq start ------------------------ 等于0的话跳转到startldr r2, [r2] -------------------------- 取变量a的值
这种情况下,cpu 0和cpu 1执行的指令和动作描述如下:
cpu 0执行的指令
cpu 0动作描述
cpu 1执行的指令
cpu 1动作描述
str r3, [r2]
(a=1)
1、a值在CPU 0的cache中状态是shared,是read only的,因此,需要通知其他的CPU
2、将1保存在store buffer中
3、发送invalidate命令,试图invalidate CPU 1中a对应的cache line
注:这里无需等待response,立刻执行下一条指令
ldr r3, [r3]
(获取b的值)
1、发生cache miss
2、发送read命令,试图加载b对应的cacheline
注:这里cpu必须等待read response,下面的指令依赖于这个读取的结果
收到来自CPU 0的invalidate命令,放入invalidate queue,立刻回ack。
memory barrier instruction
CPU收到memory barrier指令,知道软件要控制访问顺序,因此不会执行下一条str指令,要等到收到invalidate ack后,将store buffer中所有数据写到cacheline之后才会执行后续的store指令
收到invalidate ack后,将store buffer中的1写入cache line。OK,可以继续执行下一条指令了
str r3, [r4]
(b=1)
1、cache hit
2、cacheline中的值被修改为1,状态变成modified
收到CPU 1发送来的read命令,将b值(等于1)放入read response中,回送给CPU 1,write back并将状态修改为shared。
收到response(b=1),并加载cacheline,状态是shared
cmp r3, #0
b等于1,不会执行beq指令,而是执行下一条指令
ldr r2, [r2]
(获取a的值)
1、cache hit (还没有执行invalidate动作,命令还在invalidate queue中呢)
2、获取了a的旧值,也就是0
对a对应的cacheline执行invalidate 命令,但是,已经晚了
可怕的memory misorder问题又来了,都是由于引入了invalidate queue引起,看来我们还需要一个memory barrier的指令,我们将程序修改如下:
ldr r2, [pc, #28] -------------------------- 取变量a的地址
ldr r3, [pc, #20] -------------------------- 取变量b的地址
start: ldr r3, [r3] -------------------------- 取变量b的值
cmp r3, #0 ------------------------ b的值是否等于0?
beq start ------------------------ 等于0的话跳转到start确保清空invalidate queue的memory barrier instruction
ldr r2, [r2] -------------------------- 取变量a的值
这种情况下,cpu 0和cpu 1执行的指令和动作描述如下:
cpu 0执行的指令
cpu 0动作描述
cpu 1执行的指令
cpu 1动作描述
str r3, [r2]
(a=1)
1、a值在CPU 0的cache中状态是shared,是read only的,因此,需要通知其他的CPU
2、将1保存在store buffer中
3、发送invalidate命令,试图invalidate CPU 1中a对应的cache line
注:这里无需等待response,立刻执行下一条指令
ldr r3, [r3]
(获取b的值)
1、发生cache miss
2、发送read命令,试图加载b对应的cacheline
注:这里cpu必须等待read response,下面的指令依赖于这个读取的结果
收到来自CPU 0的invalidate命令,放入invalidate queue,立刻回ack。
memory barrier instruction
CPU收到memory barrier指令,知道软件要控制访问顺序,因此不会执行下一条str指令,要等到收到invalidate ack后,将store buffer中所有数据写到cacheline之后才会执行后续的store指令
收到invalidate ack后,将store buffer中的1写入cache line。OK,可以继续执行下一条指令了
str r3, [r4]
(b=1)
1、cache hit
2、cacheline中的值被修改为1,状态变成modified
收到CPU 1发送来的read命令,将b值(等于1)放入read response中,回送给CPU 1,write back并将状态修改为shared。
收到response(b=1),并加载cacheline,状态是shared
cmp r3, #0
b等于1,不会执行beq指令,而是执行下一条指令
memory barrier instruction
CPU收到memory barrier指令,知道软件要控制访问顺序,因此不会执行下一条ldr指令,要等到执行完invalidate queue中的所有的invalidate命令之后才会执行下一个ldr指令
ldr r2, [r2]
(获取a的值)
1、cache miss
2、发送read命令,从CPU 0那里加载新的a值
由于增加了memory barrier,保证了a、b这两个变量的访问顺序,从而保证了程序逻辑。
三、linux kernel的API
linux kernel的memory barrier相关的API列表如下:
接口名称
作用
barrier()
优化屏障,阻止编译器为了进行性能优化而进行的memory access reorder
mb()
内存屏障(包括读和写),用于SMP和UP
rmb()
读内存屏障,用于SMP和UP
wmb()
写内存屏障,用于SMP和UP
smp_mb()
用于SMP场合的内存屏障,对于UP不存在memory order的问题(对汇编指令),因此,在UP上就是一个优化屏障,确保汇编和c代码的memory order是一致的
smp_rmb()
用于SMP场合的读内存屏障
smp_wmb()
用于SMP场合的写内存屏障
barrier()这个接口和编译器有关,对于gcc而言,其代码如下:
#define barrier() __asm__ __volatile__("": : :"memory")
这里的__volatile__主要是用来防止编译器优化的。而这里的优化是针对代码块而言的,使用嵌入式汇编的代码分成三块:
1、嵌入式汇编之前的c代码块
2、嵌入式汇编代码块
3、嵌入式汇编之后的c代码块
这里__volatile__就是告诉编译器:不要因为性能优化而将这些代码重排,我需要清清爽爽的保持这三块代码块的顺序(代码块内部是否重排不是这里的__volatile__管辖范围了)。
barrier中的嵌入式汇编中的clobber list没有描述汇编代码对寄存器的修改情况,只是有一个memory的标记。我们知道,clober list是gcc和gas的接口,用于gas通知gcc它对寄存器和memory的修改情况。因此,这里的memory就是告知gcc,在汇编代码中,我修改了memory中的内容,嵌入式汇编之前的c代码块和嵌入式汇编之后的c代码块看到的memory是不一样的,对memory的访问不能依赖于嵌入式汇编之前的c代码块中寄存器的内容,需要重新加载。
优化屏障是和编译器相关的,而内存屏障是和CPU architecture相关的,当然,我们选择ARM为例来描述内存屏障。
Linux内核同步机制之(四):spin lock
http://www.wowotech.net/kernel_synchronization/spinlock.html
一、前言
在linux kernel的实现中,经常会遇到这样的场景:共享数据被中断上下文和进程上下文访问,该如何保护呢?如果只有进程上下文的访问,那么可以考虑使用semaphore或者mutex的锁机制,但是现在中断上下文也参和进来,那些可以导致睡眠的lock就不能使用了,这时候,可以考虑使用spin lock。本文主要介绍了linux kernel中的spin lock的原理以及代码实现。由于spin lock是architecture dependent代码,因此,我们在第四章讨论了ARM32和ARM64上的实现细节。
注:本文需要进程和中断处理的基本知识作为支撑。
二、工作原理
1、spin lock的特点
我们可以总结spin lock的特点如下:
(1)spin lock是一种死等的锁机制。当发生访问资源冲突的时候,可以有两个选择:一个是死等,一个是挂起当前进程,调度其他进程执行。spin lock是一种死等的机制,当前的执行thread会不断的重新尝试直到获取锁进入临界区。
(2)只允许一个thread进入。semaphore可以允许多个thread进入,spin lock不行,一次只能有一个thread获取锁并进入临界区,其他的thread都是在门口不断的尝试。
(3)执行时间短。由于spin lock死等这种特性,因此它使用在那些代码不是非常复杂的临界区(当然也不能太简单,否则使用原子操作或者其他适用简单场景的同步机制就OK了),如果临界区执行时间太长,那么不断在临界区门口“死等”的那些thread是多么的浪费CPU啊(当然,现代CPU的设计都会考虑同步原语的实现,例如ARM提供了WFE和SEV这样的类似指令,避免CPU进入busy loop的悲惨境地)
(4)可以在中断上下文执行。由于不睡眠,因此spin lock可以在中断上下文中适用。
2、 场景分析
对于spin lock,其保护的资源可能来自多个CPU CORE上的进程上下文和中断上下文的中的访问,其中,进程上下文包括:用户进程通过系统调用访问,内核线程直接访问,来自workqueue中work function的访问(本质上也是内核线程)。中断上下文包括:HW interrupt context(中断handler)、软中断上下文(soft irq,当然由于各种原因,该softirq被推迟到softirqd的内核线程中执行的时候就不属于这个场景了,属于进程上下文那个分类了)、timer的callback函数(本质上也是softirq)、tasklet(本质上也是softirq)。
先看最简单的单CPU上的进程上下文的访问。如果一个全局的资源被多个进程上下文访问,这时候,内核如何交错执行呢?对于那些没有打开preemptive选项的内核,所有的系统调用都是串行化执行的,因此不存在资源争抢的问题。如果内核线程也访问这个全局资源呢?本质上内核线程也是进程,类似普通进程,只不过普通进程时而在用户态运行、时而通过系统调用陷入内核执行,而内核线程永远都是
以上是关于Linux内核同步机制--转发自蜗窝科技的主要内容,如果未能解决你的问题,请参考以下文章