Skip to content

LV020-原子操作

一、原子操作简介

“原子” 是化学世界中不可再分的最小微粒, 一切物质都由原子组成。 在 Linux 内核中的原子操作可以理解为“不可被拆分的操作” , 就是不能被更高等级中断抢夺优先的操作。

一般原子操作用于变量或者位操作。假如现在要对无符号整形变量 a 赋值,值为 3,对于 C 语言来讲很简单,直接就是:

c
a = 3

但是 C 语言要先编译为成汇编指令, ARM 架构不支持直接对寄存器进行读写操作,比如要借助寄存器 R0、 R1 等来完成赋值操作。假设变量 a 的地址为 0X3000000,“a = 3”这一行 C 语言可能会被编译为如下所示的汇编代码:

assembly
ldr r0, =0X30000000 /* 变量 a 地址 */
ldr r1, = 3         /* 要写入的值 */
str r1, [r0]        /* 将 3 写入到 a 变量中 */

只是一个简单的举例说明,实际的结果要比示例代码复杂的多。从上述代码可以看出, C 语言里面简简单单的一句“a = 3”,编译成汇编文件以后变成了 3 句,那么程序在执行的时候肯定是按照汇编语句一条一条的执行。假设现在线程 A 要向 a 变量写入 10 这个值,而线程 B 也要向 a 变量写入 20 这个值,我们理想中的执行顺序如图所示:

image-20250121105044537

按照上图所示的流程,确实可以实现线程 A 将 a 变量设置为 10,线程 B 将 a 变量设置为 20。但是实际上的执行流程可能如下所示:

image-20250121105204784

按照图这个图所示的流程,线程 A 最终将变量 a 设置为了 20,而并不是要求的 10!线程 B 没有问题。这就是一个最简单的设置变量值的并发与竞争的例子,要解决这个问题就 要保证那三行汇编指令作为一个整体运行,也就是作为一个原子存在

二、相关数据结构与 API

Linux 内核提供了一组原子操作 API 函数来完成此功能, Linux 内核提供了两组原子操作 API 函数,一组是对整形变量进行操作的,一组是对位进行操作的,我们接下来看一下这些 API 函数和相关的数据结构。

1. atomic_t

在 Linux 内核中使用 atomic_tatomic64_t 结构体分别来完成 32 位系统和 64 位系统的 整形数据原子操作, 两个结构体定义在 types.h - include/linux/types.h 文件中

c
typedef struct {
	int counter;
} atomic_t;

#ifdef CONFIG_64BIT
typedef struct {
	long counter;
} atomic64_t;
#endif

2. 原子整形操作 API

如果要使用原子操作 API 函数,首先要先定义一个 atomic_t 的变量,如下所示 :

c
atomic_t a; // 定义 a

也可以在定义原子变量的时候给原子变量赋初值,如下所示 :

c
atomic_t b = ATOMIC_INIT(0); //定义原子变量 b 并赋初值为 0

原子变量有了,接下来就是对原子变量进行操作,比如读、写、增加、减少等等, Linux 内核提供了大量的原子操作 API 函数,其实这些大部分都是宏:

函数描述
ATOMIC_INIT(int i)定义原子变量的时候对其初始化, 赋值为 i
int atomic_read(atomic_t *v)读取 v 的值, 并且返回。
void atomic_set(atomic_t *v, int i)向原子变量 v 写入 i 值。
void atomic_add(int i, atomic_t *v)原子变量 v 加上 i 值。
void atomic_sub(int i, atomic_t *v)原子变量 v 减去 i 值。
void atomic_inc(atomic_t *v)原子变量 v 加 1
void atomic_dec(atomic_t *v)原子变量 v 减 1
int atomic_dec_return(atomic_t *v)原子变量 v 减 1, 并返回 v 的值。
int atomic_inc_return(atomic_t *v)原子变量 v 加 1, 并返回 v 的值。
int atomic_sub_and_test(int i, atomic_t *v)原子变量 v 减 i, 如果结果为 0 就返回真, 否则返回假
int atomic_dec_and_test(atomic_t *v)原子变量 v 减 1, 如果结果为 0 就返回真, 否则返回假
int atomic_inc_and_test(atomic_t *v)原子变量 v 加 1, 如果结果为 0 就返回真, 否则返回假
int atomic_add_negative(int i, atomic_t *v)原子变量 v 加 i, 如果结果为负就返回真, 否则返回

这些都分散定义在这些头文件,它们是:

atomic.h - include/linux/atomic.h

atomic.h - include/asm-generic/atomic.h

相应的也提供了 64 位原子变量的操作 API 函数,这里我们就不详细了解,和上表中的 API 函数有用法一样,只是将“atomic_”前缀换为“atomic64_”,将 int 换为 long long。如果使用的是 64 位的 SOC,那么就要使用 64 位的原子操作函数。原子变量和相应的 API 函数使用起来很简单,参考如下:

c
atomic_t v = ATOMIC_INIT(0); /* 定义并初始化原子变零 v = 0 */
atomic_set(&v, 10);          /* 设置 v = 10 */
atomic_read(&v);             /* 读取 v 的值,肯定是 10 */
atomic_inc(&v);              /* v 的值加 1, v = 11 */

3. 原子位操作 API

位操作也是很常用的操作, Linux 内核也提供了一系列的原子位操作 API 函数,只不过原子位操作不像原子整形变量那样有个 atomic_t 的数据结构,原子位操作是直接对内存进行操作, API 函数如下:

函数描述
void set_bit(int nr, void *p)将 p 地址的第 nr 位置 1。
void clear_bit(int nr, void *p)将 p 地址的第 nr 位清零。
void change_bit(int nr, void *p)将 p 地址的第 nr 位进行翻转。
int test_bit(int nr, void *p)获取 p 地址的第 nr 位的值。
int test_and_set_bit(int nr, void *p)将 p 地址的第 nr 位置 1,并且返回 nr 位原来的值。
int test_and_clear_bit(int nr, void *p)将 p 地址的第 nr 位清零,并且返回 nr 位原来的值。
int test_and_change_bit(int nr, void *p)将 p 地址的第 nr 位翻转,并且返回 nr 位原来的值。

Tips:

atomic.h - include/asm-generic/bitops/atomic.h

non-atomic.h - include/asm-generic/bitops/non-atomic.h

三、原子操作 demo

1. demo 源码

在 xxx_open()函数和 xxx_release()函数中加入原子整形变量 v 的赋值代码, 并且在 open()函数中加入原子整形变量 v 的判断代码, 从而实现同一时间内只允许一个应用打开该设备节点, 以此来防止共享资源竞争的产生。

07_concurrency/03_atomic

2. 开发板验证

我们将编译得到的 sdriver_demo.ko、app_demo.out 拷贝到开发板。

  • (1)加载驱动
shell
insmod sdriver_demo.ko
image-20250122095804419
  • (2)1 个应用程序访问 设备节点
shell
./app_demo.out /dev/sdevchr 2 0 sumu1
image-20250122100058803

在 open 的时候,原子变量变为 0,在 release 的时候,原子变量的值恢复到 1。

  • (3)2 个应用程序访问 设备节点
shell
./app_demo.out /dev/sdevchr 2 0 sumu1 &
./app_demo.out /dev/sdevchr 2 0 sumu2 &
image-20250122100356113

可以看到应用程序在打开第二次 /dev/sdevchr 文件的时候, 出现了“can't open file /dev/sdevchr !”打印, 证明文件打开失败, 只有在第一个应用关闭相应的文件之后, 下一个应用才能打开, 通过限制同一时间内设备访问数量, 来对共享资源进行保护。

参考资料:

自旋锁在抢占(或非抢占)单核和多核中的作用_多核系统中, 任务可以通过自旋锁或者中断的方式独占 cpu-CSDN 博客