STM32MP157驱动开发——Linux并发与竞争实验
参考资料:
0.前言
在上一节介绍了Linux中常用的处理并发和竞争的机制:原子操作、自旋锁、信号量和互斥体。这节就通过实验进一步了解这些机制。
一、原子操作实验
目的:
使用原子操作实现对LED设备的互斥访问。本次实验在之前的gpioled驱动基础上进行修改。
步骤:
1.设备树修改(与gpio驱动相同,无需修改)
2.驱动程序及测试App修改
3.编译及运行测试
驱动修改
直接将之前的驱动文件复制,并在其基础上进行修改。
①首先在LED设备结构体gpioled_dev中添加一个atomic_t类型的变量,即为设备添加一个原子变量的属性:
/*gpioled设备结构体*/
struct gpioled_dev{
dev_t devid; /*设备号*/
struct cdev cdev; /*cdev*/
struct class *class; /*类*/
struct device *device; /*设备*/
int major; /*主设备号*/
int minor; /*次设备号*/
struct device_node *nd; /*设备节点*/
int led_gpio; /*led所使用的GPIO编号*/
atomic_t lock; /*原子变量
};
②在led_init()函数中,对这个原子变量进行初始化,并赋值为1,表示有一个可用的LED设备:
static int __init led_init(void)
{
......
/*初始化原子变量*/
gpioled.lock = (atomic_t)ATOMIC_INIT(0);
/*设置初始值为1*/
atomic_set(&gpioled.lock, 1);
......
}
③在led_open()函数中,每次需要对设备操作之前,先调用atomic_dec_and_test()函数查看LED的状态,如果此函数的返回结果为0,表示当前有可用设备;如果小于0,则表示没有,此时需要再将原子值+1,返回LED忙状态:
static int led_open(struct inode *inode, struct file *filp)
{
/*通过判断原子变量的值来检车LED是否被其他任务使用*/
if(!atomic_dec_and_test(&gpioled.lock)){ /*函数部分:lock值减1,为0返回真,否则为假*/
atomic_inc(&gpioled.lock); /*如果小于0就加1,使其原子变量等于0*/
return -EBUSY; /*返回LED忙状态*/
}
filp->private_data = &gpioled; /* 设置私有数据 */
return 0;
}
④led_release()函数中,增添对原子操作变量的释放步骤,也就是将该值+1,表示增加了一个可用设备:
static int led_release(struct inode *inode, struct file *filp)
{
struct gpioled_dev *dev = filp->private_data;
/*关闭驱动文件时释放原子变量*/
atomic_inc(&dev->lock);
return 0;
}
测试App修改
在关闭LED设备文件之前,使用循环模拟占用LED一段时间,这期间LED设备的原子操作变量被占用,所以其它任务无法使用LED设备。
int main(){
//......open(){...}
while(1){
sleep(5);
cnt++;
printf("App running times: %d\r\n", cnt);
if(cnt >= 5)break;
}
//close(){...}......
}
编译及测试
编译方式与之前的操作相同,使用Makefile编译出.ko文件,使用交叉编译工具编译测试App文件,将编译出的结果放入nfs的对应目录下,运行。
使用./Atomic_App /dev/gpioled 1 &
命令后,开发板LED打开,'&'
符号表示将这个软件在后台运行。开发板终端输出App运行次数。此时再使用./Atomic_App /dev/gpioled 0
命令关闭LED时,会发现操作失败。原因是当前原子变量正在被使用,其他程序无法操作,只有等到后台程序运行结束,原子变量被释放后才可以继续操作。
二、自旋锁实验
自旋锁要求保护的临界区尽可能短,所以在原子操作实验里,open()函数中获取,release()函数中释放的方式就不适用了。可以更改为使用一个变量来代表设备的使用情况,使用自旋锁来保护这个变量的操作即可。
驱动修改
新建8_spinlock工程,在7_atomic的基础上进行修改
①在设备结构体中添加设备状态变量dev_stat,添加自旋锁变量spinlock_t
/*gpioled设备结构体*/
struct gpioled_dev{
dev_t devid; /*设备号*/
struct cdev cdev; /*cdev*/
struct class *class; /*类*/
struct device *device; /*设备*/
int major; /*主设备号*/
int minor; /*次设备号*/
struct device_node *nd; /*设备节点*/
int led_gpio; /*led所使用的GPIO编号*/
int dev_stats; /*表示设备状态,0:设备未使用,>0:设备已被占用*/
spinlock_t lock; /*自旋锁*/
};
②在led_open()函数中,每次需要改变设备状态时进行自旋锁状态判断
static int led_open(struct inode *inode, struct file *filp)
{
unsigned long flags;
filp->private_data = &gpioled; /*设置私有数据 */
spin_lock_irqsave(&gpioled.lock, flags); /*保存中断状态,禁止本地中断,获取自旋锁*/
if(gpioled.dev_stats){
spin_unlock_irqrestore(&gpioled.lock, flags); /*设备已经被使用,就将中断恢复到之前状态,激活本地终端,释放锁*/
return -EBUSY;
}
gpioled.dev_stats++; /*设备没有被占用,那么就将其标记为已打开*/
spin_unlock_irqrestore(&gpioled.lock, flags); /*解锁*/
return 0;
}
③在led_release()函数中,对设备状态进行释放,同时也需要对自旋锁状态进行判断
static int led_release(struct inode *inode, struct file *filp)
{
unsigned long flags;
struct gpioled_dev *dev = filp->private_data;
/*关闭驱动文件时,将dev_stat减1*/
spin_lock_irqsave(&dev->lock, flags); /*上锁*/
if(dev->dev_stats){
dev->dev_stats--;
}
spin_unlock_irqrestore(&dev->lock, flags); /*上锁*/
return 0;
}
④在led_init()函数中对自旋锁进行初始化:spin_lock_init(&gpioled.lock);
编译及运行测试
可使用原子操作中的程序进行测试,将本节的驱动编译后运行
三、信号量实验
信号量可以导致休眠,因此信号量保护的临界区没有运行时间限制,可以在驱动的 open 函数申请信号量,然后在release 函数中释放信号量。但是信号量不能用在中断中,所以不会在中断中使用信号量。
驱动修改
新建9_semaphore工程,在7_atomic的基础上进行修改
①使用信号量必须添加<linux/semaphore.h>头文件,然后在设备结构体中添加一个信号量成员变量 sem
/*gpioled设备结构体*/
struct gpioled_dev{
dev_t devid; /*设备号*/
struct cdev cdev; /*cdev*/
struct class *class; /*类*/
struct device *device; /*设备*/
int major; /*主设备号*/
int minor; /*次设备号*/
struct device_node *nd; /*设备节点*/
int led_gpio; /*led所使用的GPIO编号*/
struct semaphore sem; /*信号量*/
};
②在led_open()函数和led_release()函数中,添加对信号量的可用判断
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &gpioled; /*设置私有数据 */
/*获取信号量*/
if(down_interruptible(&gpioled.sem)){ /*获取信号量,进入休眠状态的进程可以被信号打断,这时候count为0*/
return -ERESTARTSYS;
}
#if 0
down(&gpioled.sem); /*不能被信号打断*/
#endif
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
struct gpioled_dev *dev = filp->private_data;
up(&dev->sem); /*释放信号量,信号量count加1*/
return 0;
}
当信号量值大于1,设备可用,否则不可用,程序进入休眠。获取信号量的方式可以用能被信号打断的方式,也可以不被信号打断,等待内核调度唤醒。
③在led_init()函数中将信号量初始化为1
sema_init(&gpioled.sem, 1);
编译及运行测试同上,这里不再赘述。
四、互斥体实验
最适合这种程序互斥的使用方式,其实应该是互斥体。其他方式的使用方式与互斥体的实现机制相似。
驱动的修改与前面的几种相似,都是添加设备的对应属性,这里为添加互斥体变量:
/*gpioled设备结构体*/
struct gpioled_dev{
dev_t devid; /*设备号*/
struct cdev cdev; /*cdev*/
struct class *class; /*类*/
struct device *device; /*设备*/
int major; /*主设备号*/
int minor; /*次设备号*/
struct device_node *nd; /*设备节点*/
int led_gpio; /*led所使用的GPIO编号*/
struct mutex lock; /*互斥体变量*/
};
初始化:mutex_init(&gpioled.lock);
然后修改led_open()和led_release()函数的相应判断:
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &gpioled; /*设置私有数据 */
/*获取信号量*/
if(mutex_lock_interruptible(&gpioled.lock)){ /*获取信号量,进入休眠状态的进程可以被信号打断,这时候count为0*/
return -ERESTARTSYS;
}
#if 0
mutex_lock(&gpioled.lock); /*不能被信号打断*/
#endif
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
struct gpioled_dev *dev = filp->private_data;
mutex_unlock(&dev->lock); /*释放信号量,信号量count加1*/
return 0;
}
然后进行编译测试。
五、总结
本节主要对四种处理并发和竞争的机制进行操作实验,以实现LED驱动文件的单一程序访问为目的,分别使用四种方式进行实现。
在实验过程中,主要应该针对这四种不同措施的不同特点进行程序的逻辑设计。其中包括:①自旋锁要求保护的临界区尽可能短,所以设计一个LED的状态变量,由自旋锁对状态值的改变进行保护;②原子操作只能对整型变量或者位进行保护,所以无法让其保护事件性质(表现为结构体类型)的属性;③自旋锁可以在中断中使用,但加锁解锁前需要对中断环境进行存档和读档,并且需要操作本地中断;④信号量不能用于中断中,且开销比自旋锁大,但可以允许线程进入休眠状态,之后再由信号量唤醒,适用于资源占用比较久的场合;⑤当信号量的值为1时(可以等价于只有一个可用资源),此时信号量就退化成(或等价于)互斥体,(并不完全相等,信号量可能会引起优先级翻转问题)。互斥体同样不能用于中断,但是可以允许程序休眠,且可以递归上锁解锁。