Linux驱动(五)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 <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <asm/atomic.h>

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 <linux/init.h>  
#include <linux/module.h>  
#include <linux/kernel.h>  
#include <linux/cdev.h>  
#include <linux/fs.h>  
#include <linux/slab.h>  
#include <linux/device.h>  
  
  
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); 

3、信号量

信号量和应用层中思路是一样的,原理就不再详细讲了,主要还是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);

4、互斥体

互斥我们在应用层也用过,思路也没什么好说的了,直接上函数把
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。
























   


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值