嵌入式之Linux驱动(五)

姓名:郑煜烁  学号:19029100010  学院:电子工程学院

转自:https://blog.csdn.net/u012142460/article/details/79017329

【嵌牛导读】简单介绍相关的控制和命令

【嵌牛鼻子】linux设备驱动中的并发控制

【嵌牛提问】何为并发控制。为什么会出现并发控制。

【嵌牛正文】

在应用层学习时,我们学习过多个进程处理共享资源的情况。实际上在驱动中也有类似的情况,并且相对于应用层,并发的情况会更多。

    并发(concurrency)指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源。(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态(race conditions)

竞态发生的原因主要有以下几点:

    1,对称多处理器的cpu

      2,单CPU内进程与抢占它的进程

      3,中断(硬中断、软中断、Tasklet、底半部)与进程之间

我们在应用层的学习中,对临界资源的保护主要有信号量、互斥量等等。内核中的并发处理也有类似的机制,并且除此之外还有其他的一些机制。我们来详细看一看;

1、中断屏蔽

local_irq_disable() /* 屏蔽中断 */

. . .

critical section /* 临界区*/

. . .

local_irq_enable()  /* 开中断*/

中断屏蔽的使用很简单,进入临界区使用屏蔽中断函数,出临界区再打开中断函数。但是有一点要注意,屏蔽的中断只是该CPU中断,其他CPU的中断是无法屏蔽,所以在多核的CPU中起到的作用有限。

2、原子操作

原子操作可以保证对一个整型数据(注意只有整型数据)的修改是排他性的。Linux提供了一系列API来实现内核的原子操作。这些API分为两类,一类是对整型数据的操作。一类是对位的原子操作。原子操作最终都是靠硬件保证的。因此与CPU的架构有密切关系。在ARM架构中,底层最终使用LDREX和STREX指令。

2.1 整型原子操作

在使用上还是比较简单的,内核已经给我们写好了API函数,我们参照使用即可

  1.设置原子变量的值

  void atomic_set(v, i)            //设置原子变量为i

  atomic_t ATOMIC_INIT(i)    //定义原子变量v并初始化为0

2. 获取原子变量的值

  atomic_read(v)                  //返回原子变量的值

3. 原子变量加/减 

void atomic_add(int i, atomic_t *v)    //原子变量加i

void atomic_sub(int i, atomic_t *v)    //原子变量减i

4.原子变量自增/自减 

void atomic_inc(atomic_t *v)          //原子变量自增1

void atomic_dec(atomic_t *v)        //原子变量自减1

5. 操作并测试

atomic_sub_and_test(i, v) 原子减i并测试

atomic_dec_and_test(v) 原子变量减1并测试

atomic_inc_and_test(v) 原子变量加1并测试

上述操作对原子变量执行减i、自减、自增操作后,测试其是否为0,为0返回true,否则返回返回false

6 操作并返回

int atomic_add_return(int i, atomic_t *v);

int atomic_sub_return(int i, atomic_t *v);

int atomic_inc_return(atomic_t *v);

int atomic_dec_return(atomic_t *v);

上述操作对原子变量进行加/减和自增/自减操作,并返回新的值

总结一下使用原子操作的步骤(可以想一想在应用层使用信号量的步骤):

1、初始化一个原子变量,一般为0或1,(1表示第一次获取时可以成功,0表示只等待释放后才能使用,我们以初始化为1)

2、操作并测试,其实就是尝试获取临界资源,所以也就是用自减测试或减i测试,自加并测试很少会用到。

3、操作临界资源

4、释放原子变量

我们使用驱动中动态创建设备号、设备节点文章中的例程,添加相应程序,是该驱动程序在同一时刻只能被打开一次。(增加的程序后面用+++++++++++表示一下,没办法,CSDN的编辑器依旧那么渣,单独修改某一行程序的颜色或者字体大小无法显示)

#include

#include

#include

#include

#include

#include

#include

#include

MODULE_LICENSE("GPL");

dev_t devno;

int major = 0;

int minor = 0;

int count = 1;

struct cdev *pdev;

struct class * pclass;

struct device * pdevice;

atomic_t  v = ATOMIC_INIT(1);  //初始化一个原子变量+++++++++++++++++

int demo_open(struct inode * inodep, struct file * filep)

{

if(!atomic_sub_and_test(1, &v))  //获取原子变量+++++++++++++++++

{

printk("v:%d\n", atomic_read(&v));

atomic_add(1, &v);

return -EBUSY;

}

printk("%s,%d\n", __func__, __LINE__);

return 0;

}

int demo_release(struct inode *inodep, struct file *filep)

{

printk("%s,%d\n", __func__, __LINE__);

atomic_inc(&v);    //释放原子变量++++++++++++++

return 0;

}

struct file_operations  fops = {

.owner =THIS_MODULE,

.open = demo_open,

.release = demo_release,

};

static int __init demo_init(void)

{

int ret = 0;

printk("%s,%d\n", __func__, __LINE__);

ret = alloc_chrdev_region(&devno,minor,count, "xxx");

if(ret)

{

printk("Failed to alloc_chrdev_region.\n");

return ret;

}

printk("devno:%d , major:%d  minor:%d\n", devno, MAJOR(devno), MINOR(devno));

pdev = cdev_alloc();

if(pdev == NULL)

{

printk("Failed to cdev_alloc.\n");

goto err1;

}

cdev_init(pdev, &fops);

ret = cdev_add(pdev, devno, count);

if(ret < 0)

{

    printk("Failed to cdev_add.");

goto err2;

}

pclass = class_create(THIS_MODULE, "myclass");

if(IS_ERR(pclass))

{

printk("Failed to class_create.\n");

ret = PTR_ERR(pclass);

goto err3;

}

pdevice = device_create(pclass, NULL, devno, NULL, "hello");

if(IS_ERR(pdevice))

{

printk("Failed to device_create.\n");

ret = PTR_ERR(pdevice);

goto err4;

}

return 0;

err4:

class_destroy(pclass);

err3:

cdev_del(pdev);

err2:

kfree(pdev);

err1:

unregister_chrdev_region(devno, count);

return ret;

}

static void __exit demo_exit(void)

{

printk("%s,%d\n", __func__, __LINE__);

device_destroy(pclass, devno);

class_destroy(pclass);

cdev_del(pdev);

kfree(pdev);

unregister_chrdev_region(devno, count);

}

module_init(demo_init);

module_exit(demo_exit);

2.2 位原子操作

        1、设置位

        void set_bit(int nr, volatile void *addr)

        设置addr地址的第nr位,将nr位写1


        2、清楚位

        void clear_bit(int nr, unsigned long *addr)

        清楚addr的第nr位

        3、改变位

        void change_bit(unsigned long nr, volatile void *addr)

        反转地址addr处的第nr位

        4、测试位

        int test_bit(unsigned int nr, const unsigned long *addr)

        上述操作返回addr地址的第nr位

      5、测试并操作位

        int test_and_set_bit(unsigned nr, volatile unsigned long *addr)

      int test_and_clear_bit(unsigned nr, volatile unsigned long *addr)

      int test_and_change_bit(unsigned nr,  volatile unsigned long *addr)

      上述操作等同于执行test后再执行操作位相关函数

3、自旋锁

      自旋锁是一种典型的对临界资源进行互斥访问的手段,从字面上就很好理解这种机制,我们可以理解成不断的轮询某个变量,变量没有被释放就一直轮询,知道变量被释放获得了临界资源的访问权。

      linux中与自旋锁相关的操作有下面几个“

      1、定义自旋锁

      spinlock_t  lock


  2、初始化自旋锁

      spin_lock_init(spinlock_t  *_lock)

    3、获得自旋锁

      void spin_lock(spinlock_t *lock)  获取自旋锁,如果不成功,则一直获取直到成功

      void spin_lock_irq(spinlock_t *lock) 获取自旋锁,成功后关闭中断,相当于spin_lock +local_irq_disable

      spin_lock_irqsave(lock, flags)  循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。关中断,将状态寄存器值存入flags。

      spin_lock_bh(lock)

int spin_trylock(spinlock_t *lock)          循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。阻止软中断的底半部的执行。

      上述获得自旋锁的过程是,若获得不成功,则直接返回FALSE,成功返回TRUE


  4、释放自旋锁

      void spin_unlock(spinlock_t *lock)       

      void spin_unlock_irq(spinlock_t *lock)  相当于spin_unlock+local_irq_enable

      spin_unlock_irqrestore(lock, flags)  将自旋锁解锁(置为1)。开中断,将状态寄存器值从flags存入状态寄存器。

        s pin_unlock_bh(lock)                    将自旋锁解锁(置为1)。开启底半部的执行。

自旋锁的使用过程

      /*定义一个自旋锁*/

        spinlock_t  lock;

      /*初始化该自旋锁*/

      spin_lock_init(&lock);

      /*获取自旋锁*/

    spin_lock(&lock);


    /*执行临界操作*/

    ......

    /*释放自旋锁*/

    spin_unlock(&lock);

在有中断抢占资源的情况下,我们一般在进程中调用spin_lock_irqsave/spin_unlock_irqrestore,在中断中调用spin_lock/spin_unlock来配合使用

我们在使用自旋锁时要非常谨慎,主要因为以下几点

1、自选锁相当于在不断轮询,在等待自旋锁时,当前CPU只是在无意义的等待,无法做其他事情。所以自旋锁内的临界区一定要尽量短。

2、自旋锁可能导致死锁,例如在获取了锁之后,再次获取一下锁,该CPU将会死锁。

3、在自旋锁期间,不能调用可能引起进程调度的函数,如果进程获得自旋锁之后在阻塞,则可能引起内核的崩溃

例程:驱动文件不能同时打开

#include  

#include  

#include  

#include  

#include  

#include  

#include  



MODULE_LICENSE("GPL"); 


dev_t devno; 

int major = 0; 

int minor = 0; 

int count = 1; 

int open_count =0;

struct cdev *pdev; 


struct class * pclass; 

struct device * pdevice; 

static  spinlock_t open_lock;  //+++++++++++++++++++++++

int demo_open(struct inode * inodep, struct file * filep) 

spin_lock(&open_lock);          //+++++++++++++++++++++

if(open_count){                //++++++++++++++++++++

spin_unlock(&open_lock);//++++++++++++++++

return -EBUSY;          //++++++++++++++++++

}                              //+++++++++++++++++++

open_count++;                  //++++++++++++++++++++++

spin_unlock(&open_lock);      //+++++++++++++++++++++

    printk("%s,%d\n", __func__, __LINE__); 


    return 0; 


int demo_release(struct inode *inodep, struct file *filep) 

spin_lock(&open_lock);      //++++++++++++++++++++

open_count--;                //+++++++++++++++++++

spin_unlock(&open_lock);    //++++++++++++++++++

    printk("%s,%d\n", __func__, __LINE__); 


    return 0; 


struct file_operations  fops = { 

    .owner =THIS_MODULE, 

    .open = demo_open, 

    .release = demo_release, 

}; 


static int __init demo_init(void) 

    int ret = 0; 


    printk("%s,%d\n", __func__, __LINE__); 


    ret = alloc_chrdev_region(&devno,minor,count, "xxx"); 

    if(ret) 

    { 

        printk("Failed to alloc_chrdev_region.\n"); 

        return ret; 

    } 

    printk("devno:%d , major:%d  minor:%d\n", devno, MAJOR(devno), MINOR(devno)); 


    pdev = cdev_alloc(); 

    if(pdev == NULL) 

    { 

        printk("Failed to cdev_alloc.\n"); 

        goto err1; 

    } 


    cdev_init(pdev, &fops); 


    ret = cdev_add(pdev, devno, count); 

    if(ret < 0) 

    { 

        printk("Failed to cdev_add."); 

        goto err2; 

    } 


    pclass = class_create(THIS_MODULE, "myclass"); 

    if(IS_ERR(pclass)) 

    { 

        printk("Failed to class_create.\n"); 

        ret = PTR_ERR(pclass); 

        goto err3; 

    } 


    pdevice = device_create(pclass, NULL, devno, NULL, "hello"); 

    if(IS_ERR(pdevice)) 

    { 

        printk("Failed to device_create.\n"); 

        ret = PTR_ERR(pdevice); 

        goto err4; 

    } 



    return 0; 

err4: 

    class_destroy(pclass); 

err3: 

    cdev_del(pdev); 

err2: 

    kfree(pdev); 

err1: 

    unregister_chrdev_region(devno, count); 

    return ret; 


static void __exit demo_exit(void) 

    printk("%s,%d\n", __func__, __LINE__); 


    device_destroy(pclass, devno); 

    class_destroy(pclass); 

    cdev_del(pdev); 

    kfree(pdev); 

    unregister_chrdev_region(devno, count); 




module_init(demo_init); 

module_exit(demo_exit);

4、信号量

信号量和应用层中思路是一样的,原理就不再详细讲了,主要还是PV操作

1、定义信号量

struct semaphore sem;

2、初始化信号量

void sema_init(struct semaphore *sem, int val)

初始化信号量值为val

3、获取信号量P操作

void down(struct semaphore *sem);

int  down_interruptible(struct semaphore *sem);

int down_trylock(struct semaphore *sem)

前两个的区别是,第一个函数获取信号量不成功后,此时没有信号(不是信号量)要打断执行,就进入休眠,直到被cup唤醒。这中间谁都无法打断。第二个函数则不同,获取信号量不成功后,此时没有信号打断,就进入休眠,休眠期间可以被信号打断。网上看到一个例子很形象,天黑了,就睡觉,直到天亮了再醒来,这就是down函数。天黑了,睡觉,天还没亮,闹钟响了,那就醒来把。这样做是为了防止出现信号量死锁,整个程序挂掉了。就比如北极冬天出现极夜现象,但不能就此一睡不醒,还是可以被闹钟叫醒的。

第三个是不引起阻塞的方式,如果获取不到,返回非0错误值,继续向下执行。

4、释放信号量V操作

void up(struct semaphore *sem);

5、互斥体

互斥我们在应用层也用过,思路也没什么好说的了,直接上函数把

1、定义互斥体

struct mutex my_mutex;

2、初始化互斥体

mutex_init(mutex) 

3、获取互斥体

void  mutex_lock(struct mutex *lock)

mutex_lock_interruptible(struct mutex *lock)

int  mutex_trylock(struct mutex *lock)

与信号量类似

4、释放互斥体

void  mutex_unlock(struct mutex *lock)

信号量和互斥体功能很类似,对于保护临界资源,我们一般使用互斥体就行,对于生产/消费的类似问题,我们可以用信号量来解决。

我们来对比一下原子操作、自旋锁、互斥体这三者的区别。

        这三者我们都可以理解成设置一个标志位、标志位自增、标志位自减,获取标志位这几个过程。他们最大的区别在于获取不成功时下一步动作。

    原子操作获取不成功,跳过向下执行,就像if(flag) else的过程。

    自旋锁获取不成功会一直等待,就是if(flag)--->if(flag)--->if(flag)--->if(flag),或者再直接点,while(!flag);获取不成功就不走了。

    互斥体获取不成功,相当于if(flag),else{ 切换进程 } (当然,互斥体也是存在获取不成功,直接返回,执行下面其他程序的,跟原子操作很像)

我们主要来区别一下自旋锁和互斥体选用的原则。

1、当锁不能被获取时,使用互斥体的开销是进程上下文的切换时间,自旋锁则是等待获取自旋锁的时间。若临界区很小,自旋锁的等待时间我们是可以接受的,如果临界区很大,使用互斥体较好

2、若临界区包含会引起阻塞的代码,就必须用互斥体了,在自旋锁的情况下包含阻塞代码将引起程序死锁。

3、在中断中或软中断中是不允许出现休眠情况的,所以如果在中断或软中断中保护临界资源,那就使用自旋锁或者是不引起阻塞的互斥体mutex_trylock。

————————————————

版权声明:本文为CSDN博主「念念有余」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/u012142460/article/details/79017329

你可能感兴趣的:(嵌入式之Linux驱动(五))