在介绍这几个概念之前,我们先了解一下上下文
一、上下文和并发场合
执行流:有开始有结束总体顺序执行的一段代码 又称上下文
应用编程:任务上下文 内核编程:
-
任务上下文:五状态 可阻塞 a. 应用进程或线程运行在用户空间 b. 应用进程或线程运行在内核空间(通过调用syscall来间接使用内核空间) c. 内核线程始终在内核空间
-
异常上下文:不可阻塞 中断上下文
竞态:多任务并行执行时,如果在一个时刻同时操作同一个资源,会引起资源的错乱,这种错乱情形被称为竞态
共享资源:可能会被多个任务同时使用的资源
临界区:操作共享资源的代码段
为了解决竞态,需要提供一种控制机制,来避免在同一时刻使用共享资源,这种机制被称为并发控制机制
并发控制机制分类:
-
原子操作类
-
忙等待类
-
阻塞类
二、原子变量
原子变量:
存取不可被打断的特殊整型变量,他是一种多线程编程中常用的同步机制,它能够确保对共享变量的操作在执行时不会被其他线程的操作干扰,从而避免竞态条件(race condition)和死锁(deadlock)等问题。原子变量可以看作是一种特殊的类型,它具有类似于普通变量的操作,但是这些操作都是原子级别的,即要么全部完成,要么全部未完成。
在驱动中的一些基本使用函数:
a.设置原子量的值
- void atomic_set(atomic_t *v,int i); //设置原子量的值为i ,需在驱动初始化中将其打开
b.获取原子量的值
- atomic_read(atomic_t *v); //返回原子量的值
c.原子变量加减
- void atomic_add(int i,atomic_t *v);//原子变量增加i
- void atomic_sub(int i,atomic_t *v);//原子变量减少i
d.原子变量自增自减
- void atomic_inc(atomic_t *v);//原子变量增加1
- void atomic_dec(atomic_t *v);//原子变量减少1
e.操作并测试:运算后结果为0则返回真,否则返回假
- int atomic_inc_and_test(atomic_t *v); //返回原子变量自增后的值
- int atomic_dec_and_test(atomic_t *v);//返回原子变量自减后的值
- int atomic_sub_and_test(int i,atomic_t *v);//返回原子变量减去i后的值
原子位操作方法:
a.设置位 void set_bit(nr, void *addr); //设置addr的第nr位为1
b.清除位 void clear_bit(nr , void *addr); //清除addr的第nr位为0
c.改变位 void change_bit(nr , void *addr); //改变addr的第nr位为1
d.测试位 void test_bit(nr , void *addr); //测试addr的第nr位是否为1
适用场合:共享资源为单个整型变量的互斥场合
代码展示:在同一时间只能打开一次的代码
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/fs.h>
#include<linux/cdev.h>
#include<asm/uaccess.h>
#include<asm/atomic.h>
#include<linux/wait.h>
#define BUF_LEN 100
int major = 12;//主设备号
int minor = 0; //次设备号
int openonce_num = 1;//设备数量
struct openonce_dev{
struct cdev mydev;//自定义字符驱动结构体
atomic_t openflag;//初始化原子变量的标志位,1为程序能开,0不能开
}pmydev;
int openonce_open(struct inode *pnode,struct file *pfile){
pfile->private_data = (void *)(container_of(pnode->i_cdev,struct openonce_dev,mydev));
struct openonce_dev *gmydev = NULL;
gmydev = (struct openonce_dev*)pfile->private_data;
/*在程序被打开时判断标志位的值,
因为此函数运算结果为0时返回真,我们设定这个标志位为1时程序可以被打开,
所以当其自减后的值等于0时表示标志位为1,程序可以被打开,这时标识符的值由1减为0,
所以之后此程序将不能再被打开,若原来的状态就为不能打开,则函数的运算值0-1=-1返回假,
就会执行else中的语句,但此时的标志位已经变成了-1而不是我们所设定的判断值0和1
所以需要执行自增函数使其变回0来继续表示程序无法被打开。*/
if(atomic_dec_and_test(&gmydev->openflag)){
return 0;
}else{
atomic_inc(&gmydev->openflag);
printk("can not open\n");
return -1;
}
}
//程序结束时只需要将标志位的值设定为可以打开即可
int openonce_close(struct inode *pnode,struct file *pfile){
struct openonce_dev *gmydev = (struct openonce_dev *)pfile->private_data;
atomic_set(&gmydev->openflag,1);
return 0;
}
struct file_operations myops = {
.owner = THIS_MODULE,//该结构体对象属于哪个内核模块
.open = openonce_open,//打开设备
.release = openonce_close,//关闭设备
};
int __init openonce_init(void){
/* 将主设备号和次设备号组合成32位完整的设备号*/
dev_t devno = MKDEV(major,minor);
int ret = 0;
//申请设备号,方式为手动申请
ret = register_chrdev_region(devno,openonce_num,"openonce");
//手动申请失败则使用动态申请
if(ret != 0){
ret = alloc_chrdev_region(&devno,minor,openonce_num,"openonce");
if(ret != 0){
printk("get chrdev failed\n");
return -1;
}
major = MAJOR(devno);//自动申请的主设备号需要重新赋值
}
//给struct cdev对象制定操作函数集
cdev_init(&pmydev.mydev,&myops);
//将其添加到内核对应的数据结构里
pmydev.mydev.owner = THIS_MODULE;
//将指定的字符设备添加到内核中
cdev_add(&pmydev.mydev,devno,openonce_num);
//将标志位的值初始化为可以打开
atomic_set(&pmydev.openflag,1);
}
void __exit openonce_exit(void){
//获取设备的32位设备号
dev_t devno = MKDEV(major,minor);
//移除对应的设备
cdev_del(&pmydev.mydev);
//将申请的设备号释放
unregister_chrdev_region(devno,openonce_num);
}
module_init(openonce_init);
module_exit(openonce_exit);
三、自旋锁:基于忙等待的并发控制机制
自旋锁:他的机制就是他的名字——自旋,也就是一直在获得自旋锁的位置停留直到他被释放。
a.定义自旋锁 spinlock_t lock;
b.初始化自旋锁 spin_lock_init(spinlock_t *);
c.获得自旋锁 spin_lock(spinlock_t *); //成功获得自旋锁立即返回,否则自旋在那里直到该自旋锁的保持者释放
spin_trylock(spinlock_t *); //成功获得自旋锁立即返回真,否则返回假,而不是像上一个那样"在原地打转”
d.释放自旋锁 spin_unlock(spinlock_t *);
#include <linux/spinlock.h>
定义spinlock_t类型的变量lock
spin_lock_init(&lock)后才能正常使用spinlock
spin_lock(&lock);
临界区
spin_unlock(&lock);
适用场合:
-
异常上下文之间或异常上下文与任务上下文之间共享资源时
-
任务上下文之间且临界区执行时间很短时
-
互斥问题
用自旋锁实现在同一时间只能被打开一次的程序:
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/fs.h>
#include<linux/cdev.h>
#include<asm/uaccess.h>
#include<asm/atomic.h>
#include<linux/wait.h>
#define BUF_LEN 100
int major = 12;//主设备号
int minor = 0; //次设备号
int openonce_num = 1;//设备数量
struct openonce_dev{
struct cdev mydev;//自定义字符驱动结构体
spinlock_t openflag;
int oo;//自定义自旋锁的标志位,1能开0不能开
}pmydev;
int openonce_open(struct inode *pnode,struct file *pfile){
pfile->private_data = (void *)(container_of(pnode->i_cdev,struct openonce_dev,mydev));
struct openonce_dev *gmydev = NULL;
gmydev = (struct openonce_dev*)pfile->private_data;
//打开之前上锁,然后判断自定义标志位的值,若能开则将标志位设置为0,不能开则打印提示语句
spin_lock(&gmydev->openflag);
if(gmydev->oo){
spin_unlock(&gmydev->openflag);
gmydev->oo = 0;
return 0;
}else{
spin_unlock(&gmydev->openflag);
printk("can not open\n");
return -1;
}
}
int openonce_close(struct inode *pnode,struct file *pfile){
struct openonce_dev *gmydev = (struct openonce_dev *)pfile->private_data;
gmydev->oo = 1;
spin_unlock(&gmydev->openflag);
return 0;
}
struct file_operations myops = {
.owner = THIS_MODULE,//该结构体对象属于哪个内核模块
.open = openonce_open,//打开设备
.release = openonce_close,//关闭设备
};
int __init openonce_init(void){
/* 将主设备号和次设备号组合成32位完整的设备号*/
dev_t devno = MKDEV(major,minor);
int ret = 0;
//申请设备号,方式为手动申请
ret = register_chrdev_region(devno,openonce_num,"openonce");
//手动申请失败则使用动态申请
if(ret != 0){
ret = alloc_chrdev_region(&devno,minor,openonce_num,"openonce");
if(ret != 0){
printk("get chrdev failed\n");
return -1;
}
major = MAJOR(devno);//自动申请的主设备号需要重新赋值
}
//给struct cdev对象制定操作函数集
cdev_init(&pmydev.mydev,&myops);
//将其添加到内核对应的数据结构里
pmydev.mydev.owner = THIS_MODULE;
//将指定的字符设备添加到内核中
cdev_add(&pmydev.mydev,devno,openonce_num);
//初始化自旋锁并将自定义标志位设置为可以打开
pmydev.oo = 1;
spin_lock_init(&pmydev.openflag);
}
void __exit openonce_exit(void){
//获取设备的32位设备号
dev_t devno = MKDEV(major,minor);
//移除对应的设备
cdev_del(&pmydev.mydev);
//将申请的设备号释放
unregister_chrdev_region(devno,openonce_num);
}
module_init(openonce_init);
module_exit(openonce_exit);
四、互斥锁:基于阻塞的互斥机制
a.初始化
- 在自定义设备结构体中定义:struct mutex my_mutex
- 在初始化函数中初始化: mutex_init(&my_mutex);
b.获取互斥体 void mutex_lock(struct mutex *lock);
c.释放互斥体 void mutex_unlock(struct mutex *lock);
-
定义对应类型的变量
-
初始化对应变量
适用场合:任务上下文之间且临界区执行时间较长时的互斥问题
五、选择并发控制机制的原则
-
不允许睡眠的上下文需要采用忙等待类,可以睡眠的上下文可以采用阻塞类。在异常上下文中访问的竞争资源一定采用忙等待类。
-
临界区操作较长的应用建议采用阻塞类,临界区很短的操作建议采用忙等待类。
-
中断屏蔽仅在有与中断上下文共享资源时使用。
-
共享资源仅是一个简单整型量时用原子变量