灵魂拷问:
什么叫并发?
并发会导致什么不良后果?
并发的场景都有哪些?
怎么解决并发带来的问题?
并发和竟态
并发:
多个执行单元同时并行被执行,访问同一块共享资源(硬件资源,软件资源(静态变量,全局变量等)),产生竞态。
后果:
结果与预期不一致
场景:
-
单核
进程时间片耗完,轮到其他进程执行;
进程被高优先级进程抢占;
进程被中断抢占;
中断被高优先级中断抢占; -
SMP多核CPU
每个CPU内部情况如单核,另外多核CPU核间如下情景也会发生并发导致的竟态:
核间进程之间;
核间进程与中断之间;
核间中断与中断之间。
思考:
竟态的产生的根本原因是计算机中CPU、存储、外设的拓扑架构引起的。不同时刻CPU上运行的执行单元(进程或中断)通过总线访问外设或者内存(这两个都是共享资源)带来了竟态。
解决办法:
核心:保证访问共享资源的临界区(代码区域)在同一时刻的互斥访问
方式:中断屏蔽,原子操作,自旋锁,信号量,互斥体。
Tips 总结:
- (多核架构)驱动中使用
local_irq_disable()/local_irq_enalbe()
通常意味着一个bug
;
1. 中断屏蔽
1). 用途是什么?
禁止本地CPU的中断,从而防止中断导致的并发竟态;另外,由于Linux的进程调度也是基于中断实现,所以屏蔽中断也可以防止进程抢占导致的并发竟态产生。
2). 实现原理是什么?
屏蔽 ARM CPSR的I
位。
3). 怎么用?
local_irq_disable() //禁止中断
---临界区---
local_irq_enable() //开中断
另外,还有 local_irq_save(flags)/local_irq_restore(flags),除了有禁止中断的功能外,还能保存和恢复CPSR;
禁止中断底半部 local_bh_disable()/local_bh_enable()。
4). 有什么要注意的
- 中断屏蔽只能禁止本地CPU的中断,适用于单核架构;如果是SMP多核架构,核间中断导致的并发竟态同样无法避免。鉴于现在的产品几乎都是多核架构,所以单独使用中断屏蔽避免竟态并不合适,需要配合spinlock使用;
- 中断屏蔽会导致本地CPU无法响应中断,所以要求
临界区代码必须尽快执行完毕
,否则中断长时间得不到响应,会导致数据丢失甚至系统崩溃等严重问题;
2. 原子操作
1). 用途是什么?
对位
和整型变量
进行排他性操作。
2). 实现原理是什么?
基于底层CPU的汇编指令使用嵌入式C代码实现。对于ARM架构,指令是 LDREX和STREX。
#if __LINUX_ARM_ARCH__ >= 6
static inline void atomic_add(int i, atomic_t *v)
{
unsigned long tmp;
int result;
prefetchw(&v->counter);
__asm__ __volatile__("@ atomic_add\n"
"1: ldrex %0, [%3]\n"
" add %0, %0, %4\n"
" strex %1, %0, [%3]\n"
" teq %1, #0\n"
" bne 1b"
: "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)
: "r" (&v->counter), "Ir" (i)
: "cc");
}
...
#endif
3). 怎么使用?
首先明确一点,原子操作的作用是保证对变量的 read-modify-store操作一气呵成,不被打断。那么基于此技术原理,结合我们的代码业务和运用场景进行思考和运用。
编写设备驱动程序,对共享资源(变量)设置一个共享变量。该共享变量可以是引用计数,比如事件发生计数;也可以是设备状态记录标志等。
如果该共享变量可能会被被多个线程访问(增加/减少计数,改变标志位状态),那么就可以将它设置为atomic_t类型,用原子操作的API来操作它。
举例:
- 内核kobject的引用计数kref_get()就是对atomic_inc_return()的封装引用。
- 使用原子变量,使得设备或资源同一时刻只能被一个进程打开。
具体API
- static inline void atomic_add(int i, atomic_t *v) 给一个原子变量v增加i
- static inline int atomic_add_return(int i, atomic_t *v) 同上,只不过将变量v的最新值返回
- static inline void atomic_sub(int i, atomic_t *v) 给一个原子变量v减去i
- static inline int atomic_sub_return(int i, atomic_t *v) 同上,只不过将变量v的最新值返回
- static inline int atomic_cmpxchg(atomic_t *ptr, int old, int new) 比较old和原子变量ptr中的值,如果相等,那么就把new值赋给原子变量。返回旧的原子变量ptr中的值
- atomic_read 获取原子变量的值
- atomic_set 设定原子变量的值
- atomic_inc(v) 原子变量的值加一
- atomic_inc_return(v) 同上,只不过将变量v的最新值返回
- atomic_dec(v) 原子变量的值减去一
- atomic_dec_return(v) 同上,只不过将变量v的最新值返回
- atomic_sub_and_test(i, v) 给一个原子变量v减去i,并判断变量v的最新值是否等于0
- atomic_add_negative(i,v) 给一个原子变量v增加i,并判断变量v的最新值是否是负数
- static inline int atomic_add_unless(atomic_t *v, int a, int u) 只要原子变量v不等于u,那么就执行原子变量v加a的操作。如果v不等于u,返回非0值,否则返回0值
参考:
《Linux设备驱动开发详解》 第7章
http://www.wowotech.net/sort/kernel_synchronization