Linux 同步机制之原子操作

2022-11-15 21:37:42 浏览数 (2)

使用原子操作典型例子众所周知就是多个线程操作同一个全局变量 i , 由于对应的汇编指令并不只是一条,在并发访问下可能出现多个线程中的多条指令交错导致部分加操作丢失。全局变量i属于临界资源,当然可以使用加锁的方式保护临界资源,但是加锁开销比较大,用在这里有些杀鸡焉用牛刀。最好的方式是使用内核提供的atomic_t类型的原子变量来进行原子操作。

笔者本次通过源码来窥探原子操作的底层实现, 本次仍以 arm 架构下的 kernel 2.6.35 版本为源码来源。

首先来看下atomic_t的定义, 仅仅只是一个int类型变量

include/linux/types.h

代码语言:javascript复制
typedef struct {
    int counter;
} atomic_t;

以原子加操作为例, 来看下atomic_add的实现

arch/arm/include/asm/atomic.h

代码语言:javascript复制
static inline void atomic_add(int i, atomic_t *v)
{
    unsigned long tmp;
    int result;

    __asm__ __volatile__("@ atomic_addn"
"1: ldrex   %0, [%3]n"
"   add %0, %0, %4n"
"   strex   %1, %0, [%3]n"
"   teq %1, #0n" 
"   bne 1b"
    : "=&r" (result), "=&r" (tmp), " Qo" (v->counter)
    : "r" (&v->counter), "Ir" (i)
    : "cc");
}

先对以上需要用到的内嵌汇编知识做一个简单介绍。

内嵌汇编的格式如下:

代码语言:javascript复制
__asm__ volatile(
    instruction
    : output
    : intput
    : changed);
  • instruction 部分便是要执行的汇编指令
  • input 部分为汇编指令需要执行的输入, 表示将 c语言定义的值传入汇编
  • output 部分为汇编指令执行的输出,表示将汇编执行后的值传给 c语言
  • change 部分用于告诉 gcc 该内嵌汇编改变了一些值,强迫 gcc 在编译这段内嵌汇编之前保存会被修改的值,在执行完后恢复

内嵌汇编中引用 input 部分和 output 部分的值使用 %0, %1, %2 … 占位符, 也就是上述代码中的 result 为 %0, tmp 为 %1, v->counter 为 %2, &v->counte 为 %3, i 为 %4。

除此每个变量都以 “xx”(yy) 形式出现, 其中”xx”部分为修饰, 以下列出理解atomic_add需要用到的修饰,其他可忽略

  • “=&r”: = 表示只写, & 表示仅用作输出, r 使用任何可用的寄存器
  • “ Qo”: 表示可读可写

atomic_add的核心是两条关键的汇编指令, ldrexstrex需要配套使用

代码语言:javascript复制
// 将寄存器 ry 指向的内存值 load 到寄存器 rx 中, 并记录 ry 指向的内存状态为 exclusive(独占的)
ldrex rx, [ry]

// strex 更新内存时,会检查内存 exclusive 状态
// 将寄存器 ry 的值 store 到 rz 指向的内存,如果指向的内存为 exclusive, 则执行成功,否则失败。
// 成功则寄存器 rx 被设置为 0, 否则设置为 1。执行成功后清除 exclusive 标记 (清除后可以认为标记为 open)
strex rx, ry, [rz]

铺垫完上述前提知识后, 以下给出对汇编代码的逐行注释

代码语言:javascript复制
static inline void atomic_add(int i, atomic_t *v)
{
    unsigned long tmp;
    int result;

    __asm__ __volatile__("@ atomic_addn"  // @ 为注释
"1: ldrex   %0, [%3]n"         // 把 v->counter 内存值 load 到 result 中   (v->counter 内存会被记录为 exclusive)
"   add %0, %0, %4n"           // result  = i
"   strex   %1, %0, [%3]n"     // 把 result 的值 store 到 v->counter 的内存,并把 store 成功与否存入 tmp
"   teq %1, #0n"               // tmp 为 0 表示成功
"   bne 1b"                     // 如果不为 0, 则重新执行一遍
    : "=&r" (result), "=&r" (tmp), " Qo" (v->counter)
    : "r" (&v->counter), "Ir" (i)
    : "cc");   // condition register, 状态寄存器标志位
}

考虑这样的一种 case 来帮助理解, 假设有两个 cpu 发起对同一段内存的访问

1.CPU1 发起 ldrex 读操作, 记录当前状态为 exclusive

2.CPU2 发起 ldrex 读操作, 记录当前状态为 exclusive, 状态保持不变

3.CPU2 发起 strex 写操作, 状态从 exclusive 变为 open, 同时数据写回内存

4.CPU1 发起 strex 写操作, 由于当前状态为 open, 则写失败

5.CPU1 由于 strex 写失败, 根据atomic_add"teq %1, #0n" "bne 1b"逻辑会再进行 ldrex 后 strex 直到成功(这就是所谓的自旋), 所以保证了每一个加操作都不会丢失

arm 的 exclusive 标记是通过 exclusive monitor 模块实现的,在老的 x86 架构下实现类似 ldrex/strex 功能会通过锁总线实现导致效率低下

本文作者: Ifan Tsai  (菜菜)

本文链接: https://cloud.tencent.com/developer/article/2164604

版权声明: 本文采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!

0 人点赞