我们在前面讲了处理竞争和并发问题的四种机制,下面可以通过一些驱动来检验一下。
原子操作
原子操作用了最基础的一个虚拟的设备来演示,在设备模块被加载后生成了设备节点,我们使用APP程序打开设备节点后是有个线程访问了该设备里的数据,当另外一个APP重新要打开这个数据时就无法正常访问了。
1 /** 2 * @file atomic.c 3 * @author your name (you@domain.com) 4 * @brief 原子变量测试 5 * @version 0.1 6 * @date 2022-07-07 7 * 8 * @copyright Copyright (c) 2022 9 * 10 */ 11 #include <linux/module.h> 12 #include <linux/kernel.h> 13 #include <linux/init.h> 14 #include <linux/fs.h> 15 #include <linux/uaccess.h> 16 #include <linux/io.h> 17 #include <linux/types.h> 18 #include <linux/cdev.h> 19 #include <linux/device.h> 20 #include <linux/of.h> 21 #include <linux/of_address.h> 22 #include <linux/of_irq.h> 23 #include <linux/gpio.h> 24 #include <linux/of_gpio.h> 25 #include <linux/atomic.h> 26 27 #define DEVICE_CNT 1 28 #define DEVICE_NAME "DEV" 29 30 struct test_dev 31 { 32 dev_t dev_id; 33 int major; 34 int minor; 35 struct class *class; 36 struct device *device; 37 struct cdev cdev; 38 struct device_node *dev_nd; 39 atomic_t atomic_data; //原子变量 40 }; 41 42 struct test_dev dev; 43 44 static ssize_t dev_open(struct inode *inode, struct file *filp) 45 { 46 filp->private_data = &dev; //设置私有数据 47 //首次打开文件时,流程为else,原子变量值减1为0,再次打开后值为0走if流程,返回-EBUSY状态 48 if(atomic_read(&dev.atomic_data) <= 0){ //获取原子变量值 49 return -EBUSY; 50 } 51 else{ 52 atomic_dec(&dev.atomic_data); 53 } 54 return 0; 55 } 56 57 static ssize_t dev_write(struct file *filp,const char __user *buf, 58 size_t count,loff_t *ppos) 59 { 60 int ret = 0; 61 return ret; 62 } 63 64 static int dev_release(struct inode *inode,struct file *filp) 65 { 66 //文件关闭,原子变量自增(打开文件时减一为0,关闭后变成1,可以再次打开文件) 67 struct test_dev *dev = filp->private_data; 68 atomic_inc(&dev->atomic_data); 69 return 0; 70 } 71 72 /** 73 * @brief 文件操作集合 74 * 75 */ 76 static const struct file_operations gpiofops = { 77 .owner = THIS_MODULE, 78 .open = dev_open, 79 .release = dev_release, 80 }; 81 82 static int __init dev_init(void){ 83 84 int ret = 0; 85 //初始化原子变量,赋值为1 86 atomic_set(&dev.atomic_data,1); 87 88 //申请设备号 89 dev.major = 0; 90 if(dev.major){ 91 //手动指定设备号,使用指定的设备号 92 dev.dev_id = MKDEV(dev.major,0); 93 ret = register_chrdev_region(dev.dev_id,DEVICE_CNT,DEVICE_NAME); 94 } 95 else{ 96 //设备号未指定,申请设备号 97 ret = alloc_chrdev_region(&dev.dev_id,0,DEVICE_CNT,DEVICE_NAME); 98 dev.major = MAJOR(dev.dev_id); 99 dev.minor = MINOR(dev.dev_id); 100 } 101 printk("dev id geted!\r\n"); 102 103 if(ret<0){ 104 //设备号申请异常,跳转至异常处理 105 goto faile_devid; 106 } 107 108 //字符设备cdev初始化 109 dev.cdev.owner = THIS_MODULE; 110 111 cdev_init(&dev.cdev,&gpiofops); //文件操作集合映射 112 113 ret = cdev_add(&dev.cdev,dev.dev_id,DEVICE_CNT); 114 if(ret<0){ 115 //cdev初始化异常,跳转至异常处理 116 goto fail_cdev; 117 } 118 119 printk("chr dev inited!\r\n"); 120 121 //自动创建设备节点 122 dev.class = class_create(THIS_MODULE,DEVICE_NAME); 123 if(IS_ERR(dev.class)){ 124 //class创建异常处理 125 printk("class err!\r\n"); 126 ret = PTR_ERR(dev.class); 127 goto fail_class; 128 } 129 printk("dev class created\r\n"); 130 dev.device = device_create(dev.class,NULL,dev.dev_id,NULL,DEVICE_NAME); 131 if(IS_ERR(dev.device)){ 132 //设备创建异常处理 133 printk("device err!\r\n"); 134 ret = PTR_ERR(dev.device); 135 goto fail_device; 136 } 137 printk("device created!\r\n"); 138 139 return 0; 140 141 fail_device: 142 //device创建失败,意味着class创建成功,应该将class销毁 143 printk("device create err,class destroyed\r\n"); 144 class_destroy(dev.class); 145 fail_class: 146 //类创建失败,意味着设备应该已经创建成功,此刻应将其释放掉 147 printk("class create err,cdev del\r\n"); 148 cdev_del(&dev.cdev); 149 fail_cdev: 150 //cdev初始化异常,意味着设备号已经申请完成,应将其释放 151 printk("cdev init err,chrdev register\r\n"); 152 unregister_chrdev_region(dev.dev_id,DEVICE_CNT); 153 faile_devid: 154 //设备号申请异常,由于是第一步操作,不需要进行其他处理 155 printk("dev id err\r\n"); 156 return ret; 157 } 158 159 static void __exit dev_exit(void) 160 { 161 cdev_del(&dev.cdev); 162 unregister_chrdev_region(dev.dev_id,DEVICE_CNT); 163 164 device_destroy(dev.class,dev.dev_id); 165 class_destroy(dev.class); 166 } 167 168 module_init(dev_init); 169 module_exit(dev_exit); 170 171 MODULE_LICENSE("GPL"); 172 MODULE_AUTHOR("ZeqiZ");
整个程序比较简单,就是在驱动框架的基础上进行了写小变动,主要是文件操作的几个函数做了相应的修改。
第39行,在设备结构体中,我们定义了一个原子变量atomic_data,注意这个变量为整形变量
第86行,驱动模块在初始化过程中对原子变量进行初始化,并赋值为1
第44-55行,设备结构体dev被作为文件的私有数据,在文件首次被打开时,由于atomic_data初始值为1,文件能正常打开,文件打开后atomic_data进行自减操作变成0。此刻如果有另外一个线程进行了文件打开操作,atomic_data值为0,函数返回-EBUSY异常
第64到69行,设备文件被关闭,atomic_data进行自增操作,值重新为1,文件可以再次被打开。
这个APP需要修改一下
1 /** 2 * @file atomicAPP.c 3 * @author your name (you@domain.com) 4 * @brief 原子数据测试APP 5 * @version 0.1 6 * @date 2022-07-07 7 * 8 * @copyright Copyright (c) 2022 9 * 10 */ 11 #include <sys/types.h> 12 #include <sys/stat.h> 13 #include <fcntl.h> 14 #include <unistd.h> 15 #include <stdio.h> 16 #include <stdlib.h> 17 18 /** 19 * @brief 20 * 21 * @param argc //参数个数 22 * @param argv //参数 23 * @return int 24 */ 25 int main(int argc,char *argv[]) 26 { 27 char *filename; //文件名 28 filename = argv[1]; //文件名为命令行后第二个参数(索引值为1) 29 30 int ret = 0; //初始化操作返回值 31 int f = 0; //初始化文件句柄 32 int cnt=0; 33 34 if(argc != 2){ //输入参数个数不为3,提示输入格式错误 35 printf("input format error!\r\n"); 36 } 37 38 f = open(filename, O_RDWR); //打开文件 39 if(f < 0){ 40 printf("file open error\r\n"); 41 return -1; 42 } 43 44 while(1){ 45 printf("file opened\r\n"); 46 sleep(5); 47 cnt++; 48 printf("App running times:%d\r\n",cnt); 49 if(cnt>=3){ 50 break; 51 } 52 } 53 54 printf("App running finished\r\n"); 55 56 close(f); //关闭文件 57 printf("file closed!\r\n"); 58 return 0; 59 }
在APP里主要处理一个文件打开的操作,由于我们没发模拟线程访问数据的过程,只能通过开启文件然后延时来模拟线程持有数据的过程。然后用了个for循环3次显示程序正在运行。最后程序运行的效果如下图
我在运行程序时加了个&符号,意思是后台运行程序,运行期间再在终端重新使用命令打开文件,就会有错误提示。如果在运行程序期间使用ps命令打印进程,就可以看到我们运行的程序
注意上面那个图最下面还显示出来程序正在运行(APP里打印文件打开的语句应该放在for循环外面,这里实在不想改了。。。)
程序优化
这个上面的过程我们使用了下面两组原子变量的操作
atomic_dec(&dev.atomic_data);
atomic_inc(&dev.atomic_data);
另外还结合了if语句进行判定加上else来选择流程,但是Linux内核给我们提供了另一个API,在自增/减的时候直接判定
static ssize_t dev_open(struct inode *inode, struct file *filp) { filp->private_data = &dev; //设置私有数据 //首次打开文件时,流程为else,原子变量值减1为0,再次打开后值为0走if流程,返回-EBUSY状态 #if 0 if(atomic_read(&dev.atomic_data) <= 0){ //获取原子变量值 return -EBUSY; } else{ atomic_dec(&dev.atomic_data); } #endif if(!atomic_dec_and_test(&dev.atomic_data)){ atomic_inc(&dev.atomic_data); return -EBUSY; } return 0; }
注意函数操作顺序
atomic_dec_and_test(&dev.atomic_data)
先将变量减一,再判定,如果减一后结果为0则返回1,否则返回0。我们开始将变量初始化为1,进行减一后变成0,函数返回1,用来一个非运算,就不用执行if结构里的代码直接return,如果文件已经打开,变量值为0,自减后不为0,返回1,非运算后执行if结构体里内容,因为前面判定时减了1,所以要再加回去,返回异常值。这种结构是Linux内核中驱动开发的主要使用方法,在内核里搜索一下很多案例可以供我们参考。总之,这种整形的原子变量,在我们用作计数器的时候非常普遍。
自旋锁操作
自旋锁的演示和前面原子操作差不多,不放全部代码了,区别就是在定义设备结构体时要定义一个锁和一个状态字。
struct test_dev { dev_t dev_id; int major; int minor; struct class *class; struct device *device; struct cdev cdev; struct device_node *dev_nd; int dev_status; //设备状态,0时可被使用,1时不可使用 spinlock_t lock; //自旋锁 };
在设备初始化的时候要将锁和状态字初始化,
static int __init dev_init(void){ int ret = 0; //初始化自旋锁 spin_lock_init(&dev.lock); dev.dev_status = 0; //.....后面省略
把状态初始化为0,表示0未上锁,1时上锁
锁的操作在open和release对应的函数中
static ssize_t dev_open(struct inode *inode, struct file *filp) { filp->private_data = &dev; //设置私有数 printk("open\r\n"); spin_lock(&dev.lock); if(dev.dev_status){ //判定是否被占用 spin_unlock(&dev.lock); return -EBUSY; } dev.dev_status++; //自增,非0表示被占用 spin_unlock(&dev.lock); return 0; } static int dev_release(struct inode *inode,struct file *filp) { struct test_dev *dev = filp->private_data; spin_lock(&dev->lock); if(dev->dev_status){ dev->dev_status--; //未被占用,自减至0,表示未被占用 } spin_unlock(&dev->lock); return 0; }
注意看一下,我们在前面讲过,自旋锁是一种轻量锁,所以自旋锁锁定的是状态字而不是数据,如果是锁数据,当数据量很大的情况下自旋锁操作就不方便了。这就是我们在前面讲到的,自旋锁主要用来锁临界区。其实上面这种效果用原子操作是比较合适的,这么用主要是为了演示自旋锁的用法
这里应用演示和前面的效果一样,我就不在截图了(主要是没想好怎么模拟两条线程访问的方法)
自旋锁中断处理
在上一章里讲到过,因为系统中的中断是不好控制的,如果使用自旋锁最好将本地中断禁止掉,所以上面open和release两个函数最好使用中断禁止的自旋锁请求
static ssize_t dev_open(struct inode *inode, struct file *filp) { unsigned long irqflag; filp->private_data = &dev; //设置私有数 printk("open\r\n"); spin_lock_irqsave(&dev.lock,irqflag); if(dev.dev_status){ spin_unlock_irqrestore(&dev.lock); return -EBUSY;} //if条件为真,不能使用 dev.dev_status++; spin_unlock_irqrestore(&dev.lock,irqflag); return 0; } static int dev_release(struct inode *inode,struct file *filp) { struct test_dev *dev = filp->private_data; unsigned long irqflag; //中断标志 spin_lock_irqsave(&dev->lock,irqflag); if(dev->dev_status){ dev->dev_status--; } spin_unlock_irqrestore(&dev->lock,irqflag); return 0; }
整个流程差不多,只用声明一个变量来描述中断标志就可以了。
信号量
信号量的使用要简单的多,直接看代码
struct test_dev { dev_t dev_id; int major; int minor; struct class *class; struct device *device; struct cdev cdev; struct device_node *dev_nd; struct semaphore sem; //信号量 }; struct test_dev dev; static ssize_t dev_open(struct inode *inode, struct file *filp) { filp->private_data = &dev; //设置私有数 printk("open\r\n"); down(&dev.sem); //获取信号量 return 0; } static int dev_release(struct inode *inode,struct file *filp) { struct test_dev *dev = filp->private_data; up(&dev->sem);
return 0; }
在初始化函数要对变量进行初始化
static int __init dev_init(void){ int ret = 0; //初始化信号量 sema_init(&dev.sem,1);
将信号量初始化为1,在打开文件时会down操作将信号量自减1,如果自减后变成0线程就会休眠,直到有up操作将信号量恢复到非0状态(注意变量类型,如果在定义设备结构体时没有加*指针后面使用时要加取址符,加了*后面初始化和up,down是就不加取址符了!还有关键字是semaphore,还有另外一个关键字跟他很像:semphore,用自动补全时一定要注意,当初在这里卡了半天!)。是不是简单的多!把APP程序稍微修改一下
/** * @file semaAPP.c * @author your name (you@domain.com) * @brief * @version 0.1 * @date 2022-07-10 * * @copyright Copyright (c) 2022 * */ #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> /** * @brief * * @param argc //参数个数 * @param argv //参数 * @return int */ int main(int argc,char *argv[]) { char *filename; //文件名 filename = argv[1]; //文件名为命令行后第二个参数(索引值为1) char *id; id = argv[2]; int ret = 0; //初始化操作返回值 int f = 0; //初始化文件句柄 int cnt=0; if(argc != 3){ //输入参数个数不为3,提示输入格式错误 printf("input format error!\r\n"); } f = open(filename, O_RDWR); //打开文件 printf("id=%s file open\r\n",id); if(f < 0){ printf("file open error\r\n"); return -1; } while(1){ sleep(5); cnt++; printf("App id = %s running times:%d\r\n",id,cnt); if(cnt>=5){ break; } } printf("App id=%s running finished\r\n" ,id); close(f); //关闭文件 return 0; }
运行程序时,第三个参数为程序id,运行一下看看效果
运行APP测试一下,当我们第一次运行程序,程序正常执行,当第2个程序调用的时候,由于信号量初始值为1,就会被挂起来,直到第一个程序运行完后执行力up命令恢复了信号量后第二个程序开始运行,注意打印的id号。如果初始化信号量时候将信号量的值设置为大于1时,那么可以同时运行的程序就不止1个了。
互斥体
互斥体的使用方法和信号量差不多,把部分代码放下面
struct test_dev { dev_t dev_id; int major; int minor; struct class *class; struct device *device; struct cdev cdev; struct device_node *dev_nd; struct mutex mut; }; struct test_dev dev; static ssize_t dev_open(struct inode *inode, struct file *filp) { filp->private_data = &dev; //设置私有数 printk("open\r\n"); mutex_lock(&dev.mut); return 0; } static int dev_release(struct inode *inode,struct file *filp) { struct test_dev *dev = filp->private_data; mutex_unlock(&dev->mut); return 0; }
还有初始化
static int __init dev_init(void){ int ret = 0; //初始化 mutex_init(&dev.mut);
使用效果跟前面的信号量值为1时是一样的。
信号量和互斥体还有个用法是在休眠时允许信号打断,这个用法以后用到的概率会比较大,这里就先不讲了,后面肯定会用到。