姓名:郑煜烁 学号: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