• linux 锁-- atomic & per_cpu


    atomic引入背景

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

    典型问题描述

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

    1. ldr r3, [r3, #0]
    2. adds r2, r3, #1
    3. str r2, [r3, #0]

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

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

    atomic 实现

    arm32 实现

    1. /* arch/arm/include/asm/atomic.h */
    2. #undef ATOMIC_OPS
    3. #define ATOMIC_OPS(op, c_op, asm_op) \
    4. ATOMIC_OP(op, c_op, asm_op) \
    5. ATOMIC_FETCH_OP(op, c_op, asm_op)
    6. #define ATOMIC_OP(op, c_op, asm_op) \
    7. static inline void atomic_##op(int i, atomic_t *v) \
    8. { \
    9. unsigned long tmp; \
    10. int result; \
    11. \
    12. prefetchw(&v->counter); \
    13. __asm__ __volatile__("@ atomic_" #op "\n" \
    14. "1: ldrex %0, [%3]\n" \ ①
    15. " " #asm_op " %0, %0, %4\n" \ ②
    16. " strex %1, %0, [%3]\n" \ ③
    17. " teq %1, #0\n" \ ④
    18. " bne 1b" \ ⑤
    19. : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter) \
    20. : "r" (&v->counter), "Ir" (i) \
    21. : "cc"); \
    22. } \
    23. 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实现

    1. /* arch/arm64/include/asm/atomic_lse.h */
    2. #define ATOMIC64_OP(op, asm_op) \
    3. static inline void __lse_atomic64_##op(s64 i, atomic64_t *v) \
    4. { \
    5. asm volatile( \
    6. __LSE_PREAMBLE \
    7. " " #asm_op " %[i], %[v]\n" \
    8. : [i] "+r" (i), [v] "+Q" (v->counter) \
    9. : "r" (v)); \
    10. }
    11. ATOMIC64_OP(andnot, stclr)
    12. ATOMIC64_OP(or, stset)
    13. ATOMIC64_OP(xor, steor)
    14. ATOMIC64_OP(add, stadd) // 定义了 __lse_atomic64_add 函数, asm_op 是 stadd
    15. /* arch/arm64/include/asm/lse.h */
    16. #define __lse_ll_sc_body(op, ...) \
    17. ({ \
    18. system_uses_lse_atomics() ? \
    19. __lse_##op(__VA_ARGS__) : \
    20. __ll_sc_##op(__VA_ARGS__); \
    21. })
    22. /* arch/arm64/include/asm/atomic.h */
    23. #define ATOMIC64_OP(op) \
    24. static __always_inline void arch_##op(long i, atomic64_t *v) \
    25. { \
    26. __lse_ll_sc_body(op, i, v); \
    27. }
    28. ATOMIC64_OP(atomic64_andnot)
    29. ATOMIC64_OP(atomic64_or)
    30. ATOMIC64_OP(atomic64_xor)
    31. ATOMIC64_OP(atomic64_add) //这里传入的 op 是 atomic64_add, 定义了 arch_atomic64_add
    32. ATOMIC64_OP(atomic64_and)
    33. ATOMIC64_OP(atomic64_sub)

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

    这样 atomic64_add 就有了定义:

    1. /* lib/atomic64.c */
    2. #define ATOMIC64_OPS(op, c_op) \
    3. ATOMIC64_OP(op, c_op) \
    4. ATOMIC64_OP_RETURN(op, c_op) \
    5. ATOMIC64_FETCH_OP(op, c_op)
    6. ATOMIC64_OPS(add, +=)
    7. ATOMIC64_OPS(sub, -=)

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

    atomic典型使用

    1. atomic_t val;
    2. atomic_set(&val, 10);
    3. 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 变量定义

    1. #define __PCPU_ATTRS(sec) \
    2. __percpu __attribute__((section(PER_CPU_BASE_SECTION sec))) \
    3. PER_CPU_ATTRIBUTES
    4. #define DEFINE_PER_CPU_SECTION(type, name, sec) \
    5. __PCPU_ATTRS(sec) __typeof__(type) name
    6. #endif
    7. #define DEFINE_PER_CPU(type, name) \
    8. DEFINE_PER_CPU_SECTION(type, name, "")

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

    1. #ifndef PER_CPU_BASE_SECTION
    2. #ifdef CONFIG_SMP
    3. #define PER_CPU_BASE_SECTION ".data..percpu"
    4. #else
    5. #define PER_CPU_BASE_SECTION ".data"
    6. #endif
    1. #ifdef MODULE
    2. #define PER_CPU_SHARED_ALIGNED_SECTION ""
    3. #define PER_CPU_ALIGNED_SECTION ""
    4. #else
    5. #define PER_CPU_SHARED_ALIGNED_SECTION "..shared_aligned"
    6. #define PER_CPU_ALIGNED_SECTION "..shared_aligned"
    7. #endif
    8. #define DEFINE_PER_CPU_SHARED_ALIGNED(type, name) \
    9. DEFINE_PER_CPU_SECTION(type, name, PER_CPU_SHARED_ALIGNED_SECTION) \
    10. ____cacheline_aligned_in_smp
    11. #define DEFINE_PER_CPU_PAGE_ALIGNED(type, name) \
    12. DEFINE_PER_CPU_SECTION(type, name, "..page_aligned") \
    13. __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变量的原始变量。(参考蜗窝科技).

    典型应用

    1. DEFINE_PER_CPU(int, state);
    2. int cpu = 0;
    3. per_cpu(state, cpu) = 1;
    4. int got_state = per_cpu(state, cpu);

  • 相关阅读:
    软件项目管理期末复习---项目立项
    NeurIPS 2023 | MQ-Det: 首个支持多模态查询的开放世界目标检测大模型
    SpringCloud(36):Nacos服务发现基础应用
    微信DAT文件解密(dat转图像)
    ORBSLAM3的跟踪方式
    typeorm利用mongodb,save的时候更新会出现重复数据的问题。
    [libglog][FFmpeg] 如何把 ffmpeg 的库日志输出到 libglog里
    【Vue系列】vuex详解,一篇彻底搞懂vuex
    Windows 下 MSVC 编译器在 CMake 生成时提示 RC failed 或库文件缺失
    记录一下在Ubuntu18.04下,程序窗口之间切换快捷键
  • 原文地址:https://blog.csdn.net/kakaBack/article/details/126904191