[TOC]
include/asm-generic/bitops/generic-non-atomic.h
- generic_test_bit 和 const_test_bit 是两个用于测试位图中某个位是否被设置的函数。它们的实现方式略有不同,主要体现在对 volatile 关键字的使用上。generic_test_bit 函数使用了 volatile 关键字,这意味着它会读取内存中的值,而不是使用寄存器中的缓存值。这对于多线程或中断上下文中的位操作非常重要,因为它确保了读取的是最新的值。const_test_bit 函数则不使用 volatile 关键字,这意味着它可以在编译时进行优化,适用于编译时常量的测试。两者的实现都使用了位操作来确定指定的位是否被设置。这里仅进行了读取操作,没有进行写入操作.所以不需要进行原子操作
generic_test_bit
- 这里具有volatile会在每次读取时都从内存中读取值,而不是进行优化
1 | /** |
const_test_bit
- 编译器可能会优化掉对 addr 的多次访问,例如将其缓存到寄存器中
1 | /** |
include/linux/atomic/atomic-long.h
raw_atomic_long_read
1 | /** |
include/linux/atomic/atomic-instrumented.h
xchg
- xchg 的主要功能是以原子方式交换两个变量的值。原子操作意味着该操作在执行过程中不会被中断,因此在多线程环境中是线程安全的。
1 |
|
atomic_long_try_cmpxchg
1 | /** |
atomic_long_read
1 | /** |
atomic_long_read_acquire
raw_atomic_long_read_acquire
是一个与原子操作相关的函数或宏,通常用于在多线程或多核环境中安全地读取一个 atomic_long_t
类型的变量,同时确保读取操作具有“获取语义”(acquire semantics)。以下是对其的详细解释:
原子操作的背景
在多线程编程中,原子操作是指不可分割的操作,能够在不使用锁的情况下保证线程安全。atomic_long_t
是 Linux 内核中常用的原子类型,用于表示一个可以被多个线程安全访问的长整型变量。
获取语义(Acquire Semantics)
“获取语义”是一种内存屏障(memory barrier)的概念,用于确保在读取操作完成后,所有后续的内存操作都可以看到该读取操作的结果。换句话说,raw_atomic_long_read_acquire
不仅读取了变量的值,还确保了读取操作的顺序性,防止编译器或 CPU 对指令进行重排序。
raw_atomic_long_read_acquire
的作用
读取原子变量:
- 它读取一个
atomic_long_t
类型的变量的值。 - 读取操作是线程安全的,确保不会因为其他线程的并发修改而导致数据不一致。
- 它读取一个
获取语义:
- 通过获取语义,确保读取操作的结果对后续的内存操作可见。
- 这在需要严格的内存访问顺序时非常重要,例如在实现锁、信号量或其他同步原语时。
使用场景
同步原语:
- 在实现锁或条件变量时,确保在读取某个状态变量后,后续操作能够正确地感知到该状态的变化。
多线程数据共享:
- 在多线程环境中,确保一个线程读取的共享变量值是最新的,并且后续操作能够基于该值进行正确的逻辑处理。
性能优化:
- 与传统的锁机制相比,原子操作通常具有更高的性能,因为它们避免了上下文切换和锁竞争。
注意事项
底层实现:
raw_atomic_long_read_acquire
的具体实现可能依赖于硬件架构和编译器支持。它通常通过内联汇编或内存屏障指令实现。
与普通读取的区别:
- 普通的读取操作可能会被编译器或 CPU 重排序,而
raw_atomic_long_read_acquire
通过获取语义防止了这种重排序。
- 普通的读取操作可能会被编译器或 CPU 重排序,而
仅限读取:
- 该操作仅用于读取变量的值。如果需要修改变量的值,应该使用其他原子操作(如
atomic_long_set
或atomic_long_add
)。
- 该操作仅用于读取变量的值。如果需要修改变量的值,应该使用其他原子操作(如
总结
raw_atomic_long_read_acquire
是一个用于读取 atomic_long_t
类型变量的线程安全操作,同时提供获取语义以确保内存访问的顺序性。它在多线程编程中非常重要,尤其是在需要严格同步的场景中。通过这种操作,可以在不使用锁的情况下实现高效的线程间数据共享和同步。
include/linux/atomic/atomic-arch-fallback.h
raw_xchg
- 根据使用选择对应的实现
1 |
|
raw_atomic_try_cmpxchg 尝试比较和交换 true是发生交换,否则@false
1 | /** |
include/linux/atomic.h
__atomic_op_fence 原子操作围栏
- 原子操作前后设置内存屏障等操作保证顺序
- 执行对应的
op##_relaxe
操作
1 |
arch/arm/include/asm/cmpxchg.h
arch_xchg_relaxed __arch_xchg
- 交换数据的逻辑都一样
- 使用
ldrexb
加载数据,它还会设置一个标志,表示该地址被标记为“独占访问”,以便后续的存储操作可以检查是否有其他处理器访问了该地址。 - 使用
strexb
存储数据,如果成功,返回0;如果失败,返回1,并且会清除“独占访问”标志。 - 如果
strexb
失败,循环会重新尝试加载和存储数据,直到成功为止。 - 该函数的返回值是交换前的值。
1 |
|
arch/arm/include/asm/bitops.h
Native endian atomic definitions
1 |
|
为什么常量位号使用禁用中断的方式?
编译时优化:
- 如果
nr
是常量,编译器可以在编译时确定具体的位号,从而生成更高效的代码。 - 使用禁用中断的方式可以避免引入额外的汇编代码,同时保持代码的可读性和可移植性。
- 如果
简单性:
- 对于常量位号,禁用中断的方式已经足够确保原子性,尤其是在单核环境或不需要跨核同步的场景中。
- 这种方式避免了引入复杂的汇编代码,同时利用了 C 语言的灵活性。
适合单核环境:
- 在单核环境中,禁用中断可以确保当前 CPU 不会被中断打断,从而保证操作的原子性。
- 这种方式在单核系统中性能开销较小,且实现简单。
为什么动态位号使用汇编实现?
动态位号的复杂性:
- 如果
nr
不是常量,编译器无法在编译时确定具体的位号,因此需要在运行时处理。 - 汇编代码可以直接操作寄存器或内存,灵活处理动态位号。
- 如果
性能优化:
- 汇编实现通常比禁用中断的方式更高效,尤其是在多核环境中。
- 汇编代码可以利用硬件提供的原子指令(如
ldrex
和strex
),避免禁用中断带来的性能开销。
多核环境支持:
- 在多核系统中,禁用中断无法解决跨核的竞争问题,而汇编实现可以利用硬件的原子操作指令来确保跨核的同步。
总结
- 常量位号:使用禁用中断的方式,因为这种方式简单、高效,适合处理固定位号的操作。
- 动态位号:使用汇编实现,因为汇编代码更灵活,可以处理运行时确定的位号,同时在多核环境中性能更优。
这种设计在性能和灵活性之间取得了平衡:对于简单的常量位号操作,使用禁用中断的方式;对于复杂的动态位号操作,使用汇编代码以确保高效和正确性。
____atomic_test_and_set_bit 测试某个位是否已设置,如果该位未设置,则将其设置为 1。
1 | static inline int |
arch/arm/lib/testsetbit.S
_test_and_set_bit
1 | //orreq:在条件为“等于”(equal)时执行 OR 操作。 |
find_first_zero_bit find_next_zero_bit
1 |
|
1 |
|
arch/arm/include/asm/atomic.h
arch_atomic_read arch_atomic_set
1 | /* 在 ARM 上,普通赋值(str 指令)不会清除某些实现上的本地 strex/ldrex 监视器。我们可以将它用于 atomic_set() 的原因是在每个异常返回时完成的 clrex 或 dummy strex。 |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 wdfk-prog的个人博客!
评论