• 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时是一样的。

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

  • 相关阅读:
    光盘和U盘
    解决时间同步
    僵尸进程 和 孤儿进程
    Centos虚拟机设置网络模式
    常用CDN 和 后台管理模板
    微信小程序wxs如何使用
    kubernetes/client-go--使用 Clientset 获取 Kubernetes 资源对象
    samplecontroller
    volcano
    DNS欺骗
  • 原文地址:https://www.cnblogs.com/yinsedeyinse/p/16453223.html
Copyright © 2020-2023  润新知