驱动学习之互斥与同步

一、并发的来源

  1. 中断处理路径
  2. 调度器的可抢占性
  3. 多处理器的并发执行

二、解决竟态的办法

2.1 中断屏蔽

对于单cpu范围来说,解决竟态的简单方法就是屏蔽系统的中断,屏蔽中断使得中断与进程之间的并发不会发生,并且由于Linux内核进程调度等操作都是依赖于中断来实现,内核抢占之间的并发也就可以避免了。

local _ irq _ disable() //屏蔽中
local _ irq _ enable() //开中断

下面这两个宏是上面的变体,目的是为了防止在关闭中断的环境中使用上面两个宏之后,最后的环境是打开中断。

local _ irq _ save() //屏蔽中
local _ irq _ restore() //开中断   

一般我们不推荐在驱动程序中直接使用这两个宏。

2.2 自旋锁spin_lock

自旋锁最初的设计目的是解决多处理器中对共享数据的保护,核心思想就是:
设置一个多处理器之间共享的全局变量V,并定义V=1为上锁,V=0为解锁状态,如果一个处理器上的进程想要进入共享区域,则应该先获得V,然后判断V,若为0,则设置V为1,进入临界区,并且获取与判断以及设置应该是原子操作。

最原始的上锁解锁(会关闭调度):

spinlock_t lock        //定义自旋锁
spin_lock_init(&lock)  //自旋锁初始化
spin_lock(spinlock_t *lock)  //上锁
spin_unlock(spinlock_t *lock)  //解锁

上面的自旋锁对多处理器的并发所引起的竟态处理的很好,但是如果单处理器,且该锁会在中断上下文用到,上面的上锁操作就不够用了,这时应该使用下面的上锁解锁,相对于上面而言,下面宏在上锁的之前关闭掉了本处理器的中断。

中断上下文用到的自旋锁(关闭中断,关闭调度):

spinlock_t lock        //定义自旋锁
spin_lock_init(&lock)  //自旋锁初始化
spin_lock_irq(spinlock_t *lock)   //上锁,并关闭中断 
spin_unlock_irq(spinlock_t *lock) //解锁,打开中断

特点:自旋锁会关闭抢占
缺点:

  1. 自旋锁会消耗cpu的资源
  2. 同一个进程中如果想多次获取锁,linux系统会产生死锁。
  3. 在自旋锁的临界资源内如果有,调度(schedule();),延时,copy_from_user可能会产生内核崩溃

读写自旋锁:
由于自选所得缺点,会特别消耗cpu的资源,所以出现了读写自旋锁。允许多用户进行读操作,但是同一时间只能有一个用户写,若是有用户写,则不能读。读写自旋锁保存了自旋锁的所有特性。

rwlock_t lock;  //定义读写锁
rwlock_init(&lock) //读写锁的初始化
read_lock(&lock)     //读上锁
read_unlock(&lock)   //读解锁
write_lock(&lock)    //写上锁
write_lock(&lock)   //写解锁

2.3 信号量

信号量最大的特点是允许拥有他的线程进入睡眠状态,这意味着会出现进程的切换。
信号量的定义:
在这里插入图片描述
由定义可知,信号量的变量中有一个自旋锁,一个用来计数的count,跟一个list_head的队列,count表示允许进入临界区的执行路径的个数。
信号量接口解析:
定义及初始化:
信号量初始化函数会将参数val的值赋值给count,把自旋锁设置为解锁状态,然后初始化wait_list链表

struct semaphore  sem; 定义信号量
sema_init(&sem, int val)   //val 信号量被获取的次数

DOWN操作
这里主要分析三个down操作

down(struct semaphore *sem); //上锁
down_interruptible(struct semaphore *sem); //上锁期间可以被信号打断
down_trylock(struct semaphore *sem); //尝试获取锁     成功0  失败返回错误码      

驱动程序中最常用的是down_interruptible,我们这里不粘出源码了,大概说一下该函数的流程:
首先函数会调用spin_lock_irqsave(lock),然后进入临界区操作count,判断count>0是否成立,如果成立,则count–退出,代表获取到了信号量,若是不大于0,则调用_down_interruptible,进而调用_down_common,该函数功能是把当前进程放到信号量中wait_listl链表中,然后把当前进程设置为TASK_INTERRUPTIBLE,再调用schedule_timeout使当前进程进入睡眠。之后该进程被再次调度执行的时候,schedule_timeout返回,接下来根据进程被再次调度的原因进行处理,如果waiter.up不为0,则说明进程是被up操作唤醒,说明进程可以获得信号量,否则,是被用户空间发送的信号中断或者超时引起的调度,则返回错误码。因此down_interruptible调用总是判断返回值,对于非0的返回值,驱动程序需要返回 -ERESTARTSYS

对于down操作,与down_interruptible相比,他是不能被中断的,这意味着调用他的进程如果无法获取信号量,将一直处于休眠状态知道别的进程释放了该信号量。 因此,如非必要,我们一般不使用down()

对于down_trylock,进程无法获取信号量则返回1,获取到了返货0,不进行睡眠。

up操作
相对于众多的DOWN操作,linux下只有一个up操作。
调用up之后会进行什么操作呢?分两种情况,如果链表上没有等待的进程,则只需要吧count加一退出就行,如果链表上有睡眠的进程,则需要唤醒链表的第一个进程,并将进程从链表删除。唤醒之前会将waiter.up置为1。用来判断是否是Up操作唤醒的。即使不是信号的拥有者,也可以调用up来释放一个信号量。

linux内核提供了一个宏来来定义并初始化一个信号量:
在这里插入图片描述

信号量和自旋锁的区别

1.工作性质
自旋锁获取不到锁处于自旋转状态
信号量休眠状态
2.场景
自旋锁:中断上下文和进程上下文
信号量:进程上下文
3.cpu使用情况
自旋锁消耗CPU
信号量不消耗cpu
4.临界资源
自旋锁保护的临界资源小
信号量保护的临界资源大
5.调度
自旋锁会关闭调度
信号量不会关闭调度

2.4 互斥锁

我理解的互斥锁跟count=1的信号量没有区别,这里就不具体分析了。
#include <linux/mutex.h>
使用:
struct mutex lock;
mutex_init(&lock);
void mutex_lock(struct mutex *lock);
int mutex_lock_interruptible(struct mutex *lock);
mutex_trylock(struct mutex *lock); //成功返回真,失败返回0
void mutex_unlock(struct mutex *lock);

2.5 原子操作

atomic_t atm = ATOMIC_INIT(1); //原子变量的定义并初始化
atomic_read(v) //读取值
atomic_set(v,i) //设置值
atomic_inc(v) //加1
atomic_dec(v) //减去1
atomic_inc_and_test(v) //对变量加1并测试是否为0 ,为0真,否则为假
atomic_dec_and_test(v) //对变量减1并测试是否为0 ,为0真,否则为假

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值