为什么需要并发控制
通常一个 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的值 */
int result = atomic_inc_and_test(&n); /* 将变量n加上1后,再判断变量n的值 */
int result = atomic_sub_and_test(2, &n); /* 将变量n加减去2后,再判断变量n的值 */
如果n的值是是0,则返回非0值,否则返回0。
上述的atomic_t是为了32位的 Linux 系统定义的,随着技术的不断发展,现在的CPU和操作系统有很多都已经使用64位的。由此在 Linux内核中重新定义了一个新的数据类型atomic64_t,它在**asm/atomic.h头文件中定义,其代码实现如下:
typedef struct {
u64 _aligned(8) counter;
} atomic64_t;
u64实际上就是long long数据类型,它不同于long和int类型。int类型无论是在32位系统还是64位系统中,所占用的内存空间都是4字节的;long类型在32位系统所占用的内存空间是4字节,在64位系统中所占用的内存空间是8字节;long long数据类型无论是在32位系统还是64位系统中,所占用的内存空间都是8字节的。所以想要在32位系统中定义一个8位的数据类型,可以使用long long数据类型来实现。
操作atomic64_t类型的变量在 Linux 内核中提供了和atomic_t类似的接口函数:
atomic64_t n; /* 定义atomic_t类型的变量 */
atomic64_set(&n, 2); /* 将变量n的初始值设为2 */
atomic64_add(5, &n); /* 将变量n的值加5 */
atomic64_inc(&n); /* 将变量你的值加1 */
atomic64_sub(4, &n); /* 将变量的值减4 */
atomic64_dec(&n); /* 将变量n的值减1 */
printk("n = %d", atomic64_read(&n)); /* 输出变量n的值,输出值为3 */
int result = atomic_dec_and_test(&n); /* 将变量n减去1后,再判断变量n的值 */
int result = atomic_inc_and_test(&n); /* 将变量n加上1后,再判断变量n的值 */
int result = atomic_sub_and_test(2, &n); /* 将变量n加减去2后,再判断变量n的值 */
如果n的值是是0,则返回给result一个非0值,否则返回0。
位原子操作就是以原子的方式对位进行操作,这种操作对应的数据类型是unsigned long,所以其在32位系统总占用4个字节内存,在64位系统中占用8个字节的内存,位原子操作的主要功能是将指定的位设置成0或者1。这种操作方式在编写输入设备驱动程序是最为常见的一种,对应的函数接口是set_bit(),示例代码如下:
unsigned long value = 0;
set_bit(0, &value); /* 设置value的第0位为1,value当前的值是1 */
set_bit(2, &value); /* 设置value的第2位为1,value当前的值是5,二进制为101 */
printk("value=%ld\n", value); /* 输出value的值,value等于5 */
clear_bit(0, &value); /* 设置value的第0位为0,value当前的值是4,二进制为100 */
change_bit(0, &value); /* 设置value的第0位的值,如果第0位是1,就设置为0,否则设置为1 */
printk("value=%ld\n", value); /* 输入value的值,value等于5 */
实例:用原子操作阻止设备文件被多个进程同时打开
让设备文件同时只能被一个任务线程或进程打开是防止竞态最简单的方法,通过整型原子操作即可很容易的实现这个功能。本例演示了如何利用整型原子操作防止**/dev/atomic设备文件被多个任务线程或进程打开,有效避免了多个进程同时与/dev/atomic**设别文件交互而产生数据紊乱的问题。
Linux 驱动程序程序代码实现如下:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <asm/uaccess.h>
#include <asm/atomic.h>
/* 定义设备文件名 */
#define DEVICE_NAME "atomic"
/*************************************************
* 0:表示多个进程可以同时打开/dev/atomic设备文件
* 非0:表示同时只能有一个进程打开/dev/atoimc设备文件
*************************************************/
static int atom = 0;
/* 初始化atomic_avaliable变量 */
static atomic_t atomic_avaliable = ATOMIC_INIT(1);
/* /dev/atomic设备文件的open函数 */
static int atomic_open(struct inode *node, struct file *file)
{
/* 允许原子操作(同时只能有一个任务线程或进程打开/dev/atomic设备文件) */
if(atom) {
/**************************************************************
* 先将atomic_avaliable的值减1,然后判断atomic_avaliable变量的值,
* 如果不为0,则表示/dev/atomic设备文件已经被其他任务线程或进程打开,
* 直接返回-EBUSY。
**************************************************************/
if(!atomic_dec_and_test(&atomic_available)) {
/**********************************************************
* 由于判断atomic_available的值时已经将该变量的值减1,所这里需
* 要再将该变量的值加1。
**********************************************************/
atomic_inic(&atomic_available);
/* 由于只有返回负数才表示错误,所以这里返回的是-EBUSY,而不是EBUSY。*/
return -EBUSY;
}
}
return 0;
}
/**********************************************************************
* /dev/atomic设备文件的释放函数,如果使用open函数成功打开了设备文件,并使用
* close函数成功关闭了设备文件,则设备文件的释放函数会被调用。
**********************************************************************/
static int atomic_release(struct inode *node, struct file *file)
{
/* 允许原子操作(同时只能有一个任务线程或进程打开/dev/atomic设备文件) */
if(atom) {
/**************************************************************
* 当正常打开设备文件时,atomic_available变量的值变为0,而1才代表
* 设备文件未被打开,所以当关闭设备文件时将atomic_available变量
* 的值加1,恢复设备文件的状态为未打开的状态。
**************************************************************/
atomic_inc(&atomic_available);
}
return 0;
}
static struct file_operation dev_fops = {
.owner = THIS_MODULE,
.open = atomic_open,
.release = atomic_release
};
static struct miscdevice misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
.fops = &dev_fops
};
/* Linux驱动入口函数 */
static int _init atomic_init(void)
{
/* 创建设备文件 */
int ret = 0;
ret = misc_register(&misc);
printk("atomic_init_success\n");
return ret;
}
/* Linux驱动出口函数 */
static void _exit atomic_exit(void)
{
printk("atomic_exit_success\n");
/* 删除设备文件 */
misc_deregister(&misc);
}
/* 注册初始化Linux驱动入口函数atomic_init */
module_init(atomic_init);
/* 注册卸载Linux驱动出口函数atomic_exit */
module_exit(atomic)exit);
/* 定义模块参数 */
module_param(atom, int, S_IRUGO|S_IWUSR);
MODULE_LICENSE("GPL");
测试驱动的用户空间实现代码如下:
#include <stdio.h>
#include <errno.h>
int main()
{
/* 打开/dev/atomic设备文件 */
int handler = open("/dev/atomic", 0);
/* 输出open函数返回的值,如果该值≤0,表示打开设备文件失败 */
printf("handler: %d\n", handler);
/* 如果成功打开设备文件 */
if(handler > 0) {
/* 要求输入一个字符才继续执行 */
getchar();
/* 关闭/dev/atomic设备文件 */
close(handler);
} else {
/* 打开/dev/atomic设备文件失败,输出错误号-EBUSY(-16) */
printf("errno:%d\n", errno);
)