linux 锁-- atomic & per_cpu

本文详细介绍了Linux内核中的Atomic操作原理及其在ARM32与ARM64架构下的实现方式,并探讨了Per-CPU变量的设计理念及应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

atomic引入背景

对于 SMP 系统中,在开启 preempt 情况下,对于公共资源,如果存在两个 task 来进行更改,这就面临临界区资源竞争问题,此时会产生意想不到的结果,这是不符合预期的,因此需要来进行解决。

典型问题描述

对于变量的操作: a =0; a++; 汇编是如下实现:

ldr   r3, [r3, #0] 
adds  r2, r3, #1
str   r2, [r3, #0] 

也就是说,一个 a++ 实际上需要三条指令来完成,分别对应上图的 R,M,W。

这样如果 task1 在W之后,紧接着task2 也来W, 此时会产生不符合 task1 预想的结果,会产生问题。因此 arm 提出 atomic 来解决这种问题。

atomic 实现

arm32 实现

/* arch/arm/include/asm/atomic.h */
#undef ATOMIC_OPS
#define ATOMIC_OPS(op, c_op, asm_op)                    \
    ATOMIC_OP(op, c_op, asm_op)                 \
    ATOMIC_FETCH_OP(op, c_op, asm_op)

#define ATOMIC_OP(op, c_op, asm_op)                 \                                                                                
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");                            \
}                                   \

ATOMIC_OPS(add, +=, add)                     

这里选取了 atomic_add 来分析,上面的 #asm_op 就是 add 了,此时代码可以解析为:

  • Prefetch data
  • 将v->counter所在地址的数据加载到 result
  • result += i, 结果存放在 result 中
  • 将result 保存到 v->counter 所在地址,同时结果保存在 tmp
  • 检查 tmp 和 0 比较,如果不等于0需要重新处理一遍

那么为什么仅仅使用了 ldrex 和 strex 就实现了 atomic 功能呢?

实际上 ldrex 和 strex 在使用过程中使用了 monitor 的功能,这里选取蜗窝科技的介绍方式介绍:

 arm64实现

/* arch/arm64/include/asm/atomic_lse.h */
#define ATOMIC64_OP(op, asm_op)                     \                                                                                
static inline void __lse_atomic64_##op(s64 i, atomic64_t *v)        \
{                                   \   
    asm volatile(                           \   
    __LSE_PREAMBLE                          \   
"   " #asm_op " %[i], %[v]\n"                   \   
    : [i] "+r" (i), [v] "+Q" (v->counter)               \   
    : "r" (v));                         \   
}

ATOMIC64_OP(andnot, stclr)
ATOMIC64_OP(or, stset)
ATOMIC64_OP(xor, steor)
ATOMIC64_OP(add, stadd)  // 定义了 __lse_atomic64_add 函数, asm_op 是 stadd

/* arch/arm64/include/asm/lse.h */
#define __lse_ll_sc_body(op, ...)                   \                                                                                
({                                  \   
    system_uses_lse_atomics() ?                 \   
        __lse_##op(__VA_ARGS__) :               \   
        __ll_sc_##op(__VA_ARGS__);              \   
})

/* arch/arm64/include/asm/atomic.h */

#define ATOMIC64_OP(op)                         \
static __always_inline void arch_##op(long i, atomic64_t *v)        \
{                                   \
    __lse_ll_sc_body(op, i, v);                 \
}                                                                                                                                    

ATOMIC64_OP(atomic64_andnot)
ATOMIC64_OP(atomic64_or)
ATOMIC64_OP(atomic64_xor)
ATOMIC64_OP(atomic64_add) //这里传入的 op 是 atomic64_add, 定义了 arch_atomic64_add
ATOMIC64_OP(atomic64_and)
ATOMIC64_OP(atomic64_sub)

 对于 arch_atomic64_add ,其又调用了 __lse_ll_sc_body(atomic64_add, i, v);

这样 atomic64_add 就有了定义:

/* lib/atomic64.c */
#define ATOMIC64_OPS(op, c_op)                      \
    ATOMIC64_OP(op, c_op)                       \
    ATOMIC64_OP_RETURN(op, c_op)                    \
    ATOMIC64_FETCH_OP(op, c_op)

ATOMIC64_OPS(add, +=)                                                                                                                
ATOMIC64_OPS(sub, -=)

这里才真正使用 宏来声明了 atomic64_add 函数,它通过 stladd 将 i 加到 atomic64_add 的变量中的 counter 上面去。stladd 是 armv8.1 提供了原子操作变量,相对于 ldrex, strex 在性能又进一步提升。

atomic典型使用

atomic_t val;

atomic_set(&val, 10);

int read_val = atomic_read(&val);

per_cpu 引入背景

 对于 outer-shareable 的内存而言,由于cache MESI 机制(假设是outer shareable 的),会发生如下变化:

  1. 假设原始的 CPU cache情况如下:

 图中黄色的小球是 Cache 是否命中,且和 RAM 中内容一致

2.更改 CacheB 中内容

此时CacheB 内容被修改,内容发生变化。

3.Invalidate 其它cpu cache

 因为MESI 机制,因为B更改了,此时会自动 invalidate outer shareable 的 cache 内容。

这样会带来性能上的损耗,因为被invalidate 的内容,之后如果用到,还要重新加载。

假入内容有这样的一块内存,属于CPU自己独有,它的加载以及Cache 操作不会影响到别的CPU,这样就解决了上述面临的问题。因此linux中提出 per_cpu 变量来操作。

per_cpu 变量定义

#define __PCPU_ATTRS(sec)                       \
    __percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) \
    PER_CPU_ATTRIBUTES

#define DEFINE_PER_CPU_SECTION(type, name, sec)             \
    __PCPU_ATTRS(sec) __typeof__(type) name
#endif

#define DEFINE_PER_CPU(type, name)                  \
    DEFINE_PER_CPU_SECTION(type, name, "")

定义一个位于 PER_CPU_BASE_SECTION 的一个变量,这是一个静态声明,指定了其位于的地址空间。其section 定义如下:

#ifndef PER_CPU_BASE_SECTION
#ifdef CONFIG_SMP
#define PER_CPU_BASE_SECTION ".data..percpu"
#else
#define PER_CPU_BASE_SECTION ".data"                                                                                
#endif
#ifdef MODULE
#define PER_CPU_SHARED_ALIGNED_SECTION ""                                                                           
#define PER_CPU_ALIGNED_SECTION ""
#else
#define PER_CPU_SHARED_ALIGNED_SECTION "..shared_aligned"
#define PER_CPU_ALIGNED_SECTION "..shared_aligned"
#endif

#define DEFINE_PER_CPU_SHARED_ALIGNED(type, name)           \
    DEFINE_PER_CPU_SECTION(type, name, PER_CPU_SHARED_ALIGNED_SECTION) \
    ____cacheline_aligned_in_smp
    
#define DEFINE_PER_CPU_PAGE_ALIGNED(type, name)             \ 
    DEFINE_PER_CPU_SECTION(type, name, "..page_aligned")    \
    __aligned(PAGE_SIZE)

分别也是定义了位于 section name 为 "..page_aligned" 和 "..shared_aligned" 的变量。

那么为什么需要特殊的 Section呢?

对于kernel中的普通变量,经过了编译和链接后,会被放置到.data或者.bss段,系统在初始化的时候会准备好一切(例如clear bss),由于per cpu变量的特殊性,内核将这些变量放置到了其他的section,位于kernel address space中__per_cpu_start和__per_cpu_end之间,我们称之Per-CPU变量的原始变量。(参考蜗窝科技).

典型应用

DEFINE_PER_CPU(int, state);
int cpu = 0;
per_cpu(state, cpu) = 1;
int got_state = per_cpu(state, cpu);

<think>嗯,用户想了解per-cpu_rwsem的实现细节和在内核编程中的使用。首先,我需要回忆一下per-cpu_rwsem的相关知识。per-cpu_rwsem是Linux内核中的一种读写信号量,它通过每CPU计数器来优化传统读写信号量的扩展性问题。传统读写信号量在高竞争场景下性能可能不佳,而per-cpu_rwsem通过减少争用来提升性能。接下来,我需要考虑它的实现细节。比如,每CPU计数器是如何工作的,写者如何获取,读者如何进入和退出。可能涉及到的结构体有structrw_semaphore,以及相关的函数如percpu_down_read、percpu_up_read等。写者可能需要使用percpu_down_write来获取独占访问权,而读者则使用percpu_down_read来进入临界区。然后,用户可能关心的使用场景,比如RCU机制、内存管理子系统等。需要举一些具体的例子,比如在mm/rmap.c中的使用情况。此外,还需要提到优化方法,比如避免长时间持有写,以减少对读者性能的影响。还要注意用户提到的引用部分,虽然之前的引用是关于mutex和上下文切换的开销,但per-cpu_rwsem的设计正是为了减少这种开销,所以可以适当关联。不过用户指出引用仅供参考,所以重点还是放在per-cpu_rwsem本身。可能生成的相关问题包括per-cpu_rwsem与传统读写信号量的对比,如何在内核模块中使用它,性能分析,以及常见问题排查。这些问题能帮助用户更全面地理解该机制。</think>### per-cpu_rwsem 实现细节 #### 核心设计 per-cpu_rwsem 是 Linux 内核中针对读写信号量(rw_semaphore)的扩展性优化实现,主要特点: 1. **每CPU计数器**:通过维护每个CPU的本地计数器,减少全局争用 2. **写者优先**:使用全局原子计数器处理写者请求,保证写者优先 3. **快速路径**:读者通过`this_cpu_inc()`原子操作快速进入临界区[^1] #### 关键数据结构 ```c // 内核实现片段(简化版) struct rw_semaphore { atomic_t count; struct percpu_counter readers; struct task_struct *writer; }; ``` #### 工作流程 1. **读者进入**: ```c void percpu_down_read(struct rw_semaphore *sem) { preempt_disable(); // 禁用抢占 this_cpu_inc(sem->readers); // 本地计数器递增 smp_mb(); // 内存屏障 } ``` 2. **写者获取**: ```c void percpu_down_write(struct rw_semaphore *sem) { atomic_inc(&sem->count); // 全局计数器递增 synchronize_rcu(); // 等待现有读者退出 } ``` #### 性能优化 - **批量更新**:通过`percpu_counter_sum()`合并多个CPU的计数器更新 - **RCU同步**:利用RCU机制实现无读者检测 - **自适应切换**:当检测到写者等待时,自动切换到传统读写信号量模式 ### 典型使用场景 1. **RCU机制**:用于保护RCU宽限期(grace period)[^1] 2. **内存管理**:在mm/rmap.c中保护反向映射数据结构 3. **文件系统**:XFS等文件系统的元数据保护 ### 开发注意事项 1. **读者上下文**:必须保证读者在临界区内不睡眠 2. **写者延迟**:写者需要等待所有现有读者退出 3. **调试支持**:可通过`CONFIG_DEBUG_RWSEMS`开启调试检查
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值