Linux教程

Linux驱动开发七.并发与竞争——2.实际操作

本文主要是介绍Linux驱动开发七.并发与竞争——2.实际操作,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

我们在前面讲了处理竞争和并发问题的四种机制,下面可以通过一些驱动来检验一下。

原子操作

原子操作用了最基础的一个虚拟的设备来演示,在设备模块被加载后生成了设备节点,我们使用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时是一样的。

信号量和互斥体还有个用法是在休眠时允许信号打断,这个用法以后用到的概率会比较大,这里就先不讲了,后面肯定会用到。

这篇关于Linux驱动开发七.并发与竞争——2.实际操作的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!