01_并发和竟态

并发(Concurrency) 指的是多个执行单元同时、 并行被执行, 而并发的执行单元对共享资源(硬件资源和软件上的全局变量、 静态变量等) 的访问则很容易导致竞态(Race Conditions)

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

SMP是一种紧耦合、 共享存储的系统模型, 它的特点是多个CPU使用共同的系统总线, 因此可访问共同的外设和储存器。

2、中断嵌套

中断也有可能被新的更高优先级的中断打断, 因此, 多个中断之间本身也可能引起并发而导致

竞态。 但是Linux 2.6.35之后, 就取消了中断的嵌套。 老版本的内核可以在申请中断时, 设置标记

IRQF_DISABLED以避免中断嵌套, 由于新内核直接就默认不嵌套中断, 这个标记反而变得无用了。

3、编译乱序和执行乱序

1)编译乱序

现代的高性能编译器在目标码优化上都具备对指令进行乱序优化的能力。 编译器可以对访存的指令进行乱序, 减少逻辑上不必要的访存, 以及尽量提高Cache命中率和CPU的Load/Store单元的工作效率。 因此在打开编译器优化以后, 看到生成的汇编码并没有严格按照代码的逻辑顺序, 这是正常的。

例如:C语言顺序的“p->a=1; p->b=2; p->c=3; gp=p; ”的编译结果的指令顺序可能是gp的赋值指令发生在a、 b、 c的赋值之前。

解决办法呢:

通过barrier() 编译屏障进行。

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

C语言volatile关键字的作用较弱, 它更多的只是避免内存访问行为的合并, 对C编译器而言, volatile是暗示除了当前的执行线索以外, 其他的执行线索也可能改变某内存, 所以它的含义是“易变的”。 换句话说, 就是如果线程A读取var这个内存中的变量两次而没有修改var, 编译器可能觉得读一次就行了, 第2次直接取第1次的结果。 但是如果加了volatile关键字来形容var, 则就是告诉编译器线程B、 线程C或者其他执行实体可能把var改掉了, 因此编译器就不会再把线程A代码的第2次内存读取优化掉了。

2)执行乱序

处理器运行时的行为。即便编译的二进制指令的顺序按照“p->a=1; p->b=2; p->c=3; gp=p; ”排放, 在处理器上执行时, 后发射的指令还是可能先执行完, 这是处理器的“乱序执行(Out-of-Order Execution) ”策略。 高级的CPU可以根据自己缓存的组织特性, 将访存指令重新排序执行。 连续地址的访问可能会先执行, 因为这样缓存命中率高。 有的还允许访存的非阻塞, 即如果前面一条访存指令因为缓存不命中, 造成长延时的存储访问时, 后面的访存指令可以先执行, 以便从缓存中取数。 因此, 即使是从汇编上看顺序正确的指令, 其执行的顺序也是不可预知的。

解决办法:

内存屏障:

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

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

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

自旋锁、 互斥体等互斥逻辑, 需要用到上述指令: 在请求获得锁时, 调用屏障指令; 在解锁时, 也需要调用屏障指令。

在Linux内核中, 定义了读写屏障mb() 、 读屏障rmb() 、 写屏障wmb() 、 以及作用于寄存器读写的__iormb() 、 __iowmb() 这样的屏障API。 读写寄存器的readl_relaxed() 和readl() 、writel_relaxed() 和writel() API的区别就体现在有无屏障方面

4、中断屏蔽

单CPU范围内避免竞态的一种简单而有效的方法是在进入临界区之前屏蔽系统的中断,中断屏蔽将使得中断与进程之间的并发不再发生

屏蔽方法:

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

 

长时间屏蔽中断是很危险的, 这有可能造成数据丢失乃至系统崩溃等后果。这就要求在屏蔽了中断之后, 当前的内核执行路径应当尽快地执行完临界区的代码。

local_irq_save(flags) 除了进行禁止中断的操作以外, 还保存目前CPU的中断位信息,

local_irq_restore(flags) 进行的是与local_irq_save(flags) 相反的操作。 对于ARM处理器而言, 其实就是保存和恢复CPSR。

如果只是想禁止中断的底半部, 应使用local_bh_disable() , 使能被local_bh_disable() 禁止的底半部应该调用local_bh_enable() 。

5、原子操作

原子操作可以保证对一个整型数据的修改是排他性的

对于ARM处理器而言, 底层使用LDREX和STREX指令, 比如atomic_inc() 底层的实现会调用到atomic_add() , 其代码如下:

 

6、自旋锁

1)使用方法:

 

自旋锁主要针对SMP或单CPU但内核可抢占的情况, 对于单CPU和内核不支持抢占的系统, 自旋锁退化为空操作。 在单CPU和内核可抢占的系统中, 自旋锁持有期间中内核的抢占将被禁止。 由于内核可抢占的单CPU系统的行为实际上很类似于SMP系统, 因此, 在这样的单CPU系统中使用自旋锁仍十分必要。 另外, 在多核SMP的情况下, 任何一个核拿到了自旋锁, 该核上的抢占调度也暂时禁止了, 但是没有禁止另外一个核的抢占调度。(忙等)

2)自旋锁的衍生:

尽管用了自旋锁可以保证临界区不受别的CPU和本CPU内的抢占进程打扰, 但是得到锁的代码路径在执行临界区的时候, 还可能受到中断和底半部(BH, 稍后的章节会介绍) 的影响。

 

 

在CPU0上, 无论是进程上下文, 还是中断上下文获得了自旋锁, 此后, 如果CPU1无论是进程上下文, 还是中断上下文, 想获得同一自旋锁, 都必须忙等待, 这避免一切核间并发的可能性。由于每个核的进程上下文持有锁的时候用的是spin_lock_irqsave() , 所以该核上的中断是不可能进入的, 这避免了核内并发的可能性。

 

3)注意事项:

a.自旋锁实际上是忙等锁, 当锁不可用时, CPU一直循环执行“测试并设置”该锁直到可用而取得该锁, CPU在等待自旋锁时不做任何有用的工作, 仅仅是等待。

b.自旋锁可能导致系统死锁。 引发这个问题最常见的情况是递归使用一个自旋锁, 即如果一个已经拥有某个自旋锁的CPU想第二次获得这个自旋锁, 则该CPU将死锁。

c.在自旋锁锁定期间不能调用可能引起进程调度的函数。 如果进程获得自旋锁之后再阻塞, 如调用copy_from_user() 、 copy_to_user() 、 kmalloc() 和msleep() 等函数, 则可能导致内核的崩溃。

d.在单核情况下编程的时候, 也应该认为自己的CPU是多核的, 驱动特别强调跨平台的概念。比如, 在单CPU的情况下, 若中断和进程可能访问同一临界区, 进程里调用spin_lock_irqsave() 是安全的, 在中断里其实不调用spin_lock() 也没有问题, 因为spin_lock_irqsave() 可以保证这个CPU的中断服务程序不可能执行。 但是, 若CPU变成多核, spin_lock_irqsave() 不能屏蔽另外一个核的中断, 所以另外一个核就可能造成并发问题。 因此, 无论如何, 我们在中断服务程序里也应该调用spin_lock() 。

4)读写自旋锁

读写自旋锁是一种比自旋锁粒度更小的锁机制, 它保留了“自旋”的概念, 但是在写操作方面, 只能最多有1个写进程, 在读操作方面, 同时可以有多个读执行单元。

5)顺序锁

顺序锁(seqlock) 是对读写锁的一种优化, 若使用顺序锁, 读执行单元不会被写执行单元阻塞, 也就是说, 读执行单元在写执行单元对被顺序锁保护的共享资源进行写操作时仍然可以继续读, 而不必等待写执行单元完成写操作, 写执行单元也不需要等待所有读执行单元完成读操作才去进行写操作。 但是, 写执行单元与写执行单元之间仍然是互斥的。

但是如果读执行单元在读操作期间, 写执行单元已经发生了写操作, 那么, 读执行单元必须重新读取数据, 以便确保得到的数据是完整的。 所以, 在这种情况下, 读端可能反复读多次同样的区域才能读到有效的数据。

读顺序锁方式:

 

6)读-复制-更新

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

示例:

RCU思路, 它直接制造一个新的节点M, 把N的内容复制给M, 之后在M上修改a、 b, 并用M来代替N原本在链表的位置。 之后进程A等待在链表前期已经存在的所有读端结束后(即宽限期, 通过下文说的synchronize_rcu() API完成) , 再释放原来的N。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值