baron · 2023年10月13日

Linux Kernel(armv8-aarch64) 的原子操作的底层实现

通常我们代码中的a = a + 1这样的一行语句,翻译成汇编后蕴含着 3 条指令:

ldr x0, &a  
add x0,x0,#1  
str x0,&a

(1) 从内存中读取 a 变量到 X0 寄存器
(2)X0 寄存器加 1
(3) 将 X0 写入到内存 a 中

既然是 3 条指令,那么就有可能并发,也就意味着返回的结果可能不说预期的。

然后在 linux kernel 的操作系统中,提供访问原子变量的函数,用来解决上述问题。其中部分原子操作的 API 如下:

atomic_read
atomic_add_return(i,v)
atomic_add(i,v)
atomic_inc(v)
atomic_add_unless(v,a,u)
atomic_inc_not_zero(v)
atomic_sub_return(i,v)
atomic_sub_and_test(i,v)
atomic_sub(i,v)
atomic_dec(v)
atomic_cmpxchg(v,old,new)

那么操作系统 (仅仅是软件而已) 是如何保证原子操作的呢?(还是得靠硬件),硬件原理是什么呢?

以上的那些 API 函数,在底层调用的其实都是如下__lse_atomic_add_return##name宏的封装,这段代码中最核心的也就是ldadd指令了,这是 armv8.1 增加的 LSE(Large System Extension)feature。

(linux/arch/arm64/include/asm/atomic_lse.h)

static inline int __lse_atomic_add_return##name(int i, atomic_t *v)    \
{                                    \
    u32 tmp;                            \
                                    \
    asm volatile(                            \
    __LSE_PREAMBLE                            \
    "    ldadd" #mb "    %w[i], %w[tmp], %[v]\n"            \
    "    add    %w[i], %w[i], %w[tmp]"                \
    : [i] "+r" (i), [v] "+Q" (v->counter), [tmp] "=&r" (tmp)    \
    : "r" (v)                            \
    : cl);                                \
                                    \
    return i;                            \
}

那么系统如果没有 LSE 扩展呢,即 armv8.0,其实现的原型如下所示,这段代码中最核心的也就是ldxrstxr指令了

(linux/arch/arm64/include/asm/atomic_ll_sc.h)

static inline void __ll_sc_atomic_##op(int i, atomic_t *v)\
{                                    \
    unsigned long tmp;                        \
    int result;                            \
                                    \
    asm volatile("// atomic_" #op "\n"                \
    __LL_SC_FALLBACK(                        \
"    prfm    pstl1strm, %2\n"                    \
"1:    ldxr    %w0, %2\n"                        \
"    " #asm_op "    %w0, %w0, %w3\n"                \
"    stxr    %w1, %w0, %2\n"                        \
"    cbnz    %w1, 1b\n")                        \
    : "=&r" (result), "=&r" (tmp), "+Q" (v->counter)        \
    : __stringify(constraint) "r" (i));                \
}

那么在 armv8.0 之前呢,如 armv7 是怎样实现的?如下所示, 这段代码中最核心的也就是ldrexstrex指令了

(linux/arch/arm/include/asm/atomic.h)

static inline void atomic_##op(int i, atomic_t *v)            \
{                                    \
    unsigned long tmp;                        
    int result;                            \
                                    \
    prefetchw(&v->counter);                        \
    __asm__ __volatile__("@ atomic_" #op "\n"            \
"1:    ldrex    %0, [%3]\n"                        \
"    " #asm_op "    %0, %0, %4\n"                    \
"    strex    %1, %0, [%3]\n"                        \
"    teq    %1, #0\n"                        \
"    bne    1b"                            \
    : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)        \
    : "r" (&v->counter), "Ir" (i)                    \
    : "cc");                            \
}

总结:

在很早期,使用 arm 的 exclusive 机制来实现的原子操作,exclusive 相关的指令也就是ldrexstrex了,但在 armv8 后,exclusive 机制的指令发生了变化变成了ldxrstxr。但是又由于在一个大系统中,处理器是非常多的,竞争也激烈,使用独占的存储和加载指令可能要多次尝试才能成功,性能也就变得很差,在 armv8.1 为了解决该问题,增加了ldadd等相关的原子操作指令。

作者:baron
文章来源:Arm精选

推荐阅读

欢迎关注ARM精选专栏, 欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
9438
内容数
191
以易懂、渐进、有序的方式,深入探讨ARMv8/ARMv9架构的核心概念。我们将从基础知识开始,逐步深入,覆盖最新的架构,不再纠缠于过时技术。本系列内容包含但不限于ARM基础、SOC芯片基础、Trustzone、gic、异常和中断、AMBA、Cache、MMU等内容,并将持续更新。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息