dylanin1999

导航

Linux的并发控制

目录

中断屏蔽

原子操作

自旋锁

信号量

互斥体(Mutex)


访问共享资源的代码区域称为临界区(Critical Sections)
解决编译乱序问题, 需要通过barrier() 编译屏障进行

#define barrier() __asm__ __volatile__("": : :"memory")

ARM处理器的屏障指令包括:
DMB(数据内存屏障) : 在DMB之后的显式内存访问执行前, 保证所有在DMB指令之前的内存访问完成;


DSB(数据同步屏障) : 等待所有在DSB指令之前的指令完成(位于此指令前的所有显式内存访问均完成, 位于此指令前的所有缓存、 跳转预测和TLB维护操作全部完成) ;


ISB(指令同步屏障) : Flush流水线, 使得所有ISB之后执行的指令都是从缓存或内存中获得的。

读写屏障mb() 、 读屏障rmb() 、 写屏障wmb()

作用于寄存器读写的__iormb() 、 __iowmb()


中断屏蔽

在单CPU范围内避免竞态的一种简单而有效的方法是在进入临界区之前屏蔽系统的中断。

中断屏蔽将使得中断与进程之间的并发不再发生
 

local_irq_disable() /* 屏蔽中断 */
. . .
critical section /* 临界区*/
. . .
local_irq_enable() /* 开中断*/

其底层的实现原理是让CPU本身不响应中断, 比如, 对于ARM处理器而言, 其底层的实现是屏蔽ARM CPSR的 I 位

static inline void arch_local_irq_disable(void)
{
asm volatile(
" cpsid i @ arch_local_irq_disable"
:::
"memory", "cc");
}

原子操作

原子操作:就是在执行某一操作时不被打断。

linux原子操作问题来源于中断、进程的抢占以及多核smp系统中程序的并发执行。

对于临界区的操作可以加锁来保证原子性,对于全局变量或静态变量操作则需要依赖于硬件平台的原子变量操作。

因此原子操作有两类:一类是各种临界区的锁,一类是操作原子变量的函数。

对于arm来说,单条汇编指令都是原子的,多核smp也是,因为有总线仲裁所以cpu可以单独占用总线直到指令结束,多核系统中的原子操作通常使用内存栅障(memory barrier)来实现,即一个CPU核在执行原子操作时,其他CPU核必须停止对内存操作或者不对指定的内存进行操作,这样才能避免数据竞争问题。但是对于load update store这个过程可能被中断、抢占,所以arm指令集有增加了ldrex/strex这样的实现load update store的原子指令。


自旋锁

自旋锁(Spin Lock) 是一种典型的对临界资源进行互斥访问的手段
为了获得一个自旋锁, 在某CPU上运行的代码需先执行一个原子操作, 该操作测试并设置(Test-AndSet) 某个内存变量。 由于它是原子操作, 所以在该操作完成之前其他执行单元不可能访问这个内存变量。 如果测试结果表明锁已经空闲, 则程序获得这个自旋锁并继续执行; 如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个“测试并设置”操作, 即进行所谓的“自旋”, 通俗地说就是“在原地打转”
自旋锁主要针对SMP或单CPU但内核可抢占的情况, 对于单CPU和内核不支持抢占的系统, 自旋锁退化为空操作。
在多核SMP的情况下, 任何一个核拿到了自旋锁, 该核上的抢占调度也暂时禁止了, 但是没有禁止另外一个核的抢占调度
尽管用了自旋锁可以保证临界区不受别的CPU和本CPU内的抢占进程打扰, 但是得到锁的代码路径在执行临界区的时候, 还可能受到中断和底半部(BH, 稍后的章节会介绍) 的影响。 为了防止这种影响,就需要用到自旋锁的衍生。 spin_lock() /spin_unlock() 是自旋锁机制的基础, 它们和关中断local_irq_disable() /开中断local_irq_enable() 、 关底半部local_bh_disable() /开底半部local_bh_enable() 、 关中断并保存状态字local_irq_save() /开中断并恢复状态字local_irq_restore() 结合就形成了整套自旋锁机制, 关系如下:

spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_enable()


在多核编程的时候, 如果进程和中断可能访问同一片临界资源, 我们一般需要在进程上下文中调用spin_lock_irqsave() /spin_unlock_irqrestore() , 在中断上下文中调用spin_lock() /spin_unlock() , 如图7.8所示。 这样, 在CPU0上, 无论是进程上下文, 还是中断上下文获得了自旋锁, 此后, 如果CPU1无论是进程上下文, 还是中断上下文, 想获得同一自旋锁, 都必须忙等待, 这避免一切核间并发的可能性。
同时, 由于每个核的进程上下文持有锁的时候用的是spin_lock_irqsave() , 所以该核上的中断是不可能进入的, 这避免了核内并发的可能性
 

自旋锁的注意事项:

1) 自旋锁实际上是忙等锁, 当锁不可用时, CPU一直循环执行“测试并设置”该锁直到可用而取得该锁, CPU在等待自旋锁时不做任何有用的工作, 仅仅是等待。 因此, 只有在占用锁的时间极短的情况下,使用自旋锁才是合理的。 当临界区很大, 或有共享设备的时候, 需要较长时间占用锁, 使用自旋锁会降低系统的性能。
2) 自旋锁可能导致系统死锁。 引发这个问题最常见的情况是递归使用一个自旋锁, 即如果一个已经拥有某个自旋锁的CPU想第二次获得这个自旋锁, 则该CPU将死锁

3) 在自旋锁锁定期间不能调用可能引起进程调度的函数。 如果进程获得自旋锁之后再阻塞, 如调用copy_from_user() 、 copy_to_user() 、 kmalloc() 和msleep() 等函数, 则可能导致内核的崩溃
4) 在单核情况下编程的时候, 也应该认为自己的CPU是多核的, 驱动特别强调跨平台的概念。 比如, 在单CPU情况下, 若中断和进程可能访问同一临界区, 进程里调用spin_lock_irqsave() 是安全的, 在中断里其实不调用spin_lock() 也没有问题, 因为spin_lock_irqsave() 可以保证这个CPU的中断服务程序不可能执行。 但是, 若CPU变成多核, spin_lock_irqsave() 不能屏蔽另外一个核的中断, 所以另外一个核就可能造成并发问题。 因此, 无论如何, 我们在中断服务程序里也应该调用spin_lock() 。
 

读写自旋锁:

自旋锁(rwlock) 可允许读的并发

顺序锁:

顺序锁(seqlock) 是对读写锁的一种优化, 若使用顺序锁, 读执行单元不会被写执行单元阻塞, 也就是说, 读执行单元在写执行单元对被顺序锁保护的共享资源进行写操作时仍然可以继续读, 而不必等待写执行单元完成写操作, 写执行单元也不需要等待所有读执行单元完成读操作才去进行写操作。 但是, 写执行单元与写执行单元之间仍然是互斥的, 即如果有写执行单元在进行写操作, 其他写执行单元必须自旋在那里, 直到写执行单元释放了顺序锁。
对于顺序锁而言, 尽管读写之间不互相排斥, 但是如果读执行单元在读操作期间, 写执行单元已经发生了写操作, 那么, 读执行单元必须重新读取数据, 以便确保得到的数据是完整的。 所以, 在这种情况下, 读端可能反复读多次同样的区域才能读到有效的数据。

读-复制-更新(Read-Copy-Update, RCU)

不同于自旋锁, 使用RCU的读端没有锁、 内存屏障、 原子指令类的开销, 几乎可以认为是直接读(只是简单地标明读开始和读结束) , 而RCU的写执行单元在访问它的共享资源前首先复制一个副本,然后对副本进行修改, 最后使用一个回调机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据, 这个时机就是所有引用该数据的CPU都退出对共享数据读操作的时候。等待适当时机的这一时期称为宽限期(Grace Period)
同步RCU

synchronize_rcu()

该函数由RCU写执行单元调用, 它将阻塞写执行单元, 直到当前CPU上所有的已经存在(Ongoing)的读执行单元完成读临界区, 写执行单元才可继续下一步操作。 synchronize_rcu() 并不需要等待后续(Subsequent) 读临界区的完成
 

信号量

作为一种可能的互斥手段, 信号量可以保护临界区, 它的使用方式和自旋锁类似。 与自旋锁相同, 只有得到信号量的进程才能执行临界区代码。 但是, 与自旋锁不同的是, 当获取不到信号量时, 进程不会原地打转而是进入休眠等待状态。
 

互斥体(Mutex)

互斥体是进程级的, 用于多个进程之间对资源的互斥, 虽然也是在内核中, 但是该内核执行路径是以进程的身份, 代表进程来争夺资源的。 如果竞争失败, 会发生进程上下文切换, 当前进程进入睡眠状态,CPU将运行其他进程。 鉴于进程上下文切换的开销也很大, 因此,只有当进程占用资源时间较长时, 用互斥体才是较好的选择。
当所要保护的临界区访问时间比较短时, 用自旋锁是非常方便的, 因为它可节省上下文切换的时间。

互斥体和自旋锁选择的三大原则:

1) 当锁不能被获取到时, 使用互斥体的开销是进程上下文切换时间, 使用自旋锁的开销是等待获取自旋锁(由临界区执行时间决定) 。 若临界区比较小, 宜使用自旋锁, 若临界区很大, 应使用互斥体。
2) 互斥体所保护的临界区可包含可能引起阻塞的代码, 而自旋锁则绝对要避免用来保护包含这样代码的临界区。 因为阻塞意味着要进行进程的切换, 如果进程被切换出去后, 另一个进程企图获取本自旋锁, 死锁就会发生。
3) 互斥体存在于进程上下文, 因此, 如果被保护的共享资源需要在中断或软中断情况下使用, 则在互斥体和自旋锁之间只能选择自旋锁。 当然, 如果一定要使用互斥体, 则只能通过mutex_trylock() 方式进行, 不能获取就立即返回以避免阻塞。


 

posted on 2022-08-13 16:15  DylanYeung  阅读(108)  评论(0编辑  收藏  举报