一、并发的来源
- 中断处理路径
- 调度器的可抢占性
- 多处理器的并发执行
二、解决竟态的办法
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) //解锁,打开中断
特点:自旋锁会关闭抢占
缺点:
- 自旋锁会消耗cpu的资源
- 同一个进程中如果想多次获取锁,linux系统会产生死锁。
- 在自旋锁的临界资源内如果有,调度(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真,否则为假