linux设备驱动中的并发(一篇文章讲清楚,中断屏蔽、原子操作、自旋锁、信号量、互斥体)

本期主题:
linux设备驱动并发



1.并发与竞态

并发是指多个执行单元同时、并行的被执行;
并发时,执行单元的共享资源的访问(硬件资源、软件变量等)会导致竞态;
例如两个进程都打开了串口设备,并且同时往串口发送缓存中写入不同的值,这样就会导致竞态。

2.并发的主要场景

1.对称多处理器(SMP)的多个CPU

SMP是一种共享存储的系统模型,特点是:多个CPU使用共同的系统总线,因此可以访问共同的外设和存储器
因此 两个CPU之间的进程、两个CPU之间的中断都有并发的可能性;

2.单CPU内的进程与抢占进程

在单个CPU中,一个进程执行完成之后,有可能消耗完了自己的时间片,也可能被另外一个高优先级的进程给打断,高优先级进程与被打断的进程中之间有共同访问共享资源的可能,产生竞态。

3.中断与进程

中断可以打断正在执行的进程,所以如果中断服务程序也访问进程正在访问的共享资源,这样就有产生竞态的可能。

3.面对竞态的常用方法

1.中断屏蔽

在单CPU的范围内,可以使用中断屏蔽的方法来避免竞态,中断屏蔽可以使中断和进程之间的并发不再发生。

该方法的核心原理
让CPU不再响应中断,对于ARM处理器而言,就是屏蔽 ARM CPSR寄存器的I位

但是这种方式同样存在一些问题:

  • linux内核的进程调度也依赖于中断,所以在屏蔽中断后,应尽快执行完临界区代码
  • 只能屏蔽当前CPU的中断,不能解决SMP的多CPU并发,而且驱动编程一般要考虑代码的通用性,放在其他平台上跑的情况,中断屏蔽的方式放在SMP上就会有问题;

2.原子操作

原子操作可以保证对一个数据的修改是排他性的,特点:

  • 原子操作具有排他性
  • 原子操作依赖底层CPU的原子操作,与CPU架构密切相关,对于ARM处理器而言,底层使用LDREX和STREX指令

3.自旋锁

自旋锁(spin lock) 是一种典型的对临界资源进行互斥访问的手段,主要针对的是SMP以及单CPU但是内核可抢占的情况,常用方法如下:

  1. 获取自旋锁的操作实际上是一个原子操作,这个原子操作是测试并设置某个内存变量;由于是原子操作,在该操作完成之前,其他执行单元无法访问这个变量
  2. 其他执行单元想获取自旋锁时,如果上述的 测试并设置的结果是空闲的,则代表此时锁已经空闲,那么该执行单元就会获取这个锁并继续执行
  3. 如果上述测试结果是锁被占用了,则该执行单元就会在一个小的循环内重复这个测试并设置的操作,即进行自旋,在原地打转

Linux中自旋锁的主要操作

//1.定义自旋锁
spin_lock_t lock;
//2.初始化自旋锁
spin_lock_init(lock)
动态初始化自旋锁
//3.获得自旋锁
spin_lock(lock)
用于获得自旋锁,如果能立即获得锁就立马返回,否则,将在那里自锁,直到自旋锁的保持者释放
spin_trylock(lock)
尝试获得自旋锁,如果能理解获得锁就立马返回true,否则返回false,不再原地打转
//4.释放自旋锁
spin_unlock(lock)

//自旋锁的一般使用
spinlock_t lock;
spin_lock_init(&lock);

spin_lock(&lock); //获取自旋锁,保护临界区
/* 临界区代码 */
spin_unlock(&lock); //解锁

自旋锁的衍生

  • 自旋锁可以保证临界区不受别的CPU和本CPU的抢占进程打扰,但是还是有可能受到中断的影响,所以会将自旋锁和中断保护一起使用。
  • spin_lock()/spin_unlock()的自旋锁机制 与 关中断并保存状态字 local_irq_save() / 开中断恢复状态字 local_irq_restore() 组合成自旋锁机制
  • 我们在使用的过程中一般这样:在进程的上下文使用 spin_lock_irqsave()和 spin_unlock_irqrestore(),在中断上下文中使用 spin_lock()和 spin_unlock()

spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()

  1. 在进程上下文中使用 spin_lock_irqsave/restore,因为需要屏蔽调中断的影响
  2. 在中断上下文中使用 spin_lock/unlock
    在这里插入图片描述

4.信号量

信号量(semaphore)是操作系统中典型的用于互斥和同步的手段,信号量的值可以是0、1甚至为n。信号量与操作系统的PV操作对应,具体可看这篇blog的介绍,UNIX环境编程——信号量与互斥量对比

Linux中与信号量相关的操作主要有以下几种:

//1.定义初始化信号量
struct semaphore sem;
//2.初始化信号量
void sema_init(struct semaphore *sem, int val);
//3.获得信号量
void down(struct semaphore *sem);
//该函数用于获得信号量sem,如果没有获取到会让出cpu,导致睡眠,因此不能在中断的上下文中使用

int down_trylock(struct semaphore *sem);
//该函数尝试获取信号量sem,如果能立即获得,就获得该信号量并且返回0,否则返回非0,不会导致调用者
//睡眠,可以在中断上下文中使用

//4.释放信号量
void up(struct semaphore *sem);
//该函数释放信号量,唤醒等待者

1.信号量可以用作互斥,信号量保护临界区的方式与自旋锁类似,都是只有拿到信号量/自旋锁的进程才能够执行临界区的代码,但是不同的是,拿不到信号量的进程不会原地打转而是进入休眠等待。

2.信号量也可以用于作同步,进程P2执行down()等待信号量,进程P1执行up()释放信号量,典型的生产者/消费者问题,可以用信号量来保证数据的同步;
在这里插入图片描述

5.互斥体

尽管信号量已经可以实现互斥的功能,但是Linux还有一个互斥体mutex的存在,这在linux中应用更为广泛。
常用的方式:

//1.定义mutex
struct mutex my_mutex;
//2.初始化mutex
mutex_init(&my_mutex);
//3.获取Mutex
mutex_lock(&my_mutex);

//4.临界区....

//5.释放
mutex_unlock(&my_mutex);

自旋锁和互斥体都是解决互斥问题的手段,两者属于不同层次上的互斥手段:

1.自旋锁更为底层,互斥体属于进程级别的互斥手段;
2.互斥体的原子性实现依赖于自旋锁;

重点: 可以总结一下,自旋锁和互斥体的选用原则

  • 当锁不能被获取时,使用互斥体的开销是进程的上下文切换,而自旋锁的开销是一直跑,等待获取自旋锁,所以从系统效率角度,如果临界区代码较小时,适合用自旋锁,很大时,用互斥体;
  • 从系统安全的角度,如果保护的临界区中有阻塞操作,一定要用互斥体,因为进程阻塞会导致进程的切换,如果此时使用自旋锁,会导致死锁,因为切换出去的其他进程无法拿到自旋锁;
  • 如果被保护的共享资源在中断的情况下使用,要用自旋锁,因为互斥体会导致睡眠,但是中断的响应又需要及时;
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值