为什么需要并发控制
通常一个 Linux 驱动程序并不是为了给某个用户空间使用而编写的。调用到这个 Linux 驱动程序的用户有可能会有很多个,这就有可能出现多个用户程序同时对这个 Linux 驱动程序进行read、write、ioctl等操作。由于 Linux 驱动程序还会使用一些全局数据(即共享数据),如果同时对这些全局数据进行操作,就有可能会出现异常数据,这就使得 Linux 驱动必须具有能控制对共享数据访问的能力。
例如:
- 在读共享数据时不能修改共享数据;
- 不能同时有两个或两个以上的任务访问共享数据;
为了避免上述所说的问题,就需要在 Linux 驱动中实现并发控制,由此产生了并发控制的技术。这些技术包括原子操作、自旋锁、RCU、信号量、互斥体和完成量。
什么是并发
并发(concurrency) 指的是多个任务线程或进程同时被执行。
什么是竞态
竞态(race) 指的是多个任务线程或进程同时对共享数据(如硬件资源、程序中的全局变量、静态变量等)进行修改。
主要的并发控制技术
- 原子操作
- 自旋锁(Spin lock)
- 读-复制-更新(RCU)机制
- 信号量(Semaphore)
- 互斥体(Mutex)
- 完成量(Completion)
原子操作
原子操作就是指单位操作,也就是说,原子操作在执行的过程中是不能够被打断的。原子操作根据执行的对象数据类型可分为:整型原子操作、64位整型原子操作、位原子操作。
对整型数据(int)进行操作变成原子操作就是整型原子操作,它的实现依赖于一个数据类型:atomic_t。这个数据类型在头文件 linux/types.h中实现,代码如下:
typedef struct {
int counter;
} atomic_t;
只要将变量类型定义为atomic_t,并通过一些函数来操作atomic_t.counter变量,则atomic_t.counter变量值的变化就是原子的。
定义atomic_t类型的变量和定义普通类型的变量是一样的,只需要使用ATOMIC_INIT宏初始化,可参考如下代码:
atomic_t v; /* 定义atomic_t类型的变量 */
atomic_t n = ATOMIC_INIT(1); /* 定义atomic_t类型的变量,并初始化该变量 */
操作atomic_t类型的变量在 Linux 内核中提供了相应的接口函数。其中包括对原子变量的初始化,增加或减少变量的值。比如,定义一个原子变量n,将该变量赋值为2,然后在这个变量n的值加5,在减去1,最后输出变量的值,它的实现参考代码如下:
atomic_t n; /* 定义atomic_t类型的变量 */
atomic_set(&n, 2); /* 将变量n的初始值设为2 */
atomic_add(5, &n); /* 将变量n的值加5 */
atomic_inc(&n); /* 将变量你的值加1 */
atomic_sub(4, &n); /* 将变量的值减4 */
atomic_dec(&n); /* 将变量n的值减1 */
printk("n = %d", atomic_read(&n)); /* 输出变量n的值,输出值为3 */
由之前的结构体可以知道整型原子变量主要是用来计数的。例如,通过计数跟踪和限制设备文件被打开的次数。可以通过以下原子操作函数来实现:
int result = atomic_dec_and_test(&n); /* 将变量n减去1后,再判断变量n