前言
处理器在提供了硬件级别的原子操作保障后,软件可以基于硬件机制实现软件级别的原子操作接口,提供给软件模块使用。
原子操作接口
Linux内核提供了两类数据类型的原子操作接口:一类是针对整数类型进行原子操作;另一类是针对单独的数据位进行原子操作。
原子整数操作
Linux内核中定义原子整数操作只能针对atomic_t或atomic64_t类型的数据进行处理,为此内核引入了特殊的原子变量数据类型,目的在于屏蔽不同硬件平台实现的差异以及提供一套统一的操作接口,这些类型本质上也是对普通整型变量进行的封装。32位及64位体系结构下,内核定义的原子变量类型如下:
针对原子变量类型,内核提供的基本操作接口如下:
操作接口 | 说明 |
void atomic_read(const atomic_t *v) | 原子读操作 |
void atomic_set(atomic_t *v, int i ) | 原子性地设置v的值 |
void atomic_add(int i, atomic_t *v) | 原子性地增加v的值 |
void atomic_sub(int i, atomic_t *v) | 原子性地减小v的值 |
对于64位体系结构,将所有函数的前缀换成atomic64_
,就是对应atomic64_t数据类型的操作接口。
原子位操作
同样,内核也提供了一组原子位操作的函数接口
操作接口 | 说明 |
void set_bit(int nr, volatile unsigned long *p) | 原子性地设置p所指对象的第nr位 |
void clear_bit(unsigned int nr, volatile unsigned long *p) | 原子性地清除p所指对象的第nr位 |
void change_bit(unsigned int nr, volatile unsigned long *p) | 原子性地翻转p所指对象的第nr位 |
void test_and_set_bit(unsigned int nr, volatile unsigned long *p) | 原子性地设置p所指对象的第nr位,并返回旧值 |
void test_and_clear_bit(unsigned int nr, volatile unsigned long *p) | 原子性地清除p所指对象的第nr位,并返回旧值 |
void test_and_change_bit(unsigned int nr, volatile unsigned long *p) | 原子性地翻转p所指对象的第nr位,并返回旧值 |
上面简要罗列了Linux内核提供的原子操作接口,下面针对x86架构和ARM架构,分别看一下这些原子操作接口在内核中的实现。
x86原子操作实现
这里以atomic_add接口的为例,atomic_add在内核中的代码实现如下:
static __always_inline void arch_atomic_add(int i, atomic_t *v)
{
asm volatile(LOCK_PREFIX "addl %1,%0"
: "+m" (v->counter)
: "ir" (i));
}
static __always_inline void atomic_add(int i, atomic_t *v)
{
kasan_check_write(v, sizeof(*v));
arch_atomic_add(i, v);
}
可以看到,x86架构是在需要保证原子性的指令操前增加了LOCK _PREFIX的前缀,LOCK_PREFIX的具体实现如下:
#ifdef CONFIG_SMP
#define LOCK_PREFIX_HERE \
".pushsection .smp_locks,\"a\"\n" \
".balign 4\n" \
".long 671f - .\n" /* offset */ \
".popsection\n" \
"671:"
#define LOCK_PREFIX LOCK_PREFIX_HERE "\n\tlock; "
#else /* ! CONFIG_SMP */
#define LOCK_PREFIX_HERE ""
#define LOCK_PREFIX ""
#endif
在单处理器架构下,LOCK_PREFIX被定义为空,理由是单处理系统中,单条指令的执行已经保证了原子性;而对于多处理器架构,LOCK_PREFIX的核心实现是lock指令,通过总线锁定保证了atomic_add操作的执行流程不会受到其它处理器的干扰。
ARM原子操作实现
ARM架构下,Linux内核最早的实现是基于独占访问机制,也叫LL/SC(Load-Link/Store-Conditional
)。ARM v8.1新增了LSE指令集,提供了新的方式实现原子操作。这里我们主要看LL/SC的方式,代码如下:
#define ATOMIC_OP(op, asm_op) \
__LL_SC_INLINE void \
__LL_SC_PREFIX(atomic_##op(int i, atomic_t *v)) \
{ \
unsigned long tmp; \
int result; \
\
asm volatile("// atomic_" #op "\n" \
" prfm pstl1strm, %2\n" \
"1: ldxr %w0, %2\n" \
" " #asm_op " %w0, %w0, %w3\n" \
" stxr %w1, %w0, %2\n" \
" cbnz %w1, 1b" \
: "=&r" (result), "=&r" (tmp), "+Q" (v->counter) \
: "Ir" (i)); \
} \
LL/SC方式实现原子操作的流程如下:
- 使用ldxr指令加载数据到寄存器中,并标记对应内存为独占访问状态。若对应内存已被其它处理器标记为独占访问,则清空重置;
- 在寄存器中完成数据的修改;
- 使用stxr指令保存数据到内存中,保存时,会检查对应内存是否为之前ldxr标记的独占访问状态,若是,则返回成功;否则返回失败,并回到步骤1中进行重试
相关参考
- 《Linux内核设计与实现》
- 《Intel处理器手册》