一、并发与竞态
并发指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态。
在Linux内核中,主要的竞态发生于如下几种情况。
- 对称多处理器(SMP)的多个CPU
SMP是一种紧耦合、共享存储的系统模型,它的特点是多个cup使用共同的系统总线,因此可访问共同的外设和存储器。 - 单CPU内进程与抢占它的进程
Linux2.6以后的内核支持内核抢占调度,一个进程在内核执行的时候可能耗完了自己的时间片,也可能被另一个高优先级进程打断。 - 中断(硬中断、软中断、Tasklet、底半部)与进程之间
中断可以打断正在执行的进程,如果中断服务程序访问进程正在访问的资源,则竞态也会发生。
解决竞态问题的途径是保证对共享资源的互斥访问,即一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问。
二、中断屏蔽
CPU一般都具备屏蔽中断和打开中断的功能,这项功能可以保证正在执行的内核执行路径不被中断处理程序所抢占,防止某些竞态条件的发生。
具体而言,中断屏蔽将使得中断与进程之间的并发不再发生,而且,由于Linux内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也得以避免了。
由于Linux的异步I/O、进程调度等许多重要操作都依赖于中断,中断对于内核的运行非常重要,在屏蔽中断期间所有的中断都无法得到处理,因此长时间屏蔽中断是很危险的,这有可能造成数据丢失乃至系统崩溃等后果。这就要求在屏蔽了中断之后,当前的内核执行路径应当尽快的执行完临界区的代码。
local_irq_disable(); /* 屏蔽中断 */
local_irq_enable(); /* 开中断 */
三、原子操作
原子操作可以保证对一个整型数据的修改是排他性的。
Linux内核提供了一系列函数来实现内核中的原子操作,这些函数又分为两类,分别针对位和整形变量进行原子操作。
1. 整形原子操作
① 设置原子变量的值
void atomic_set(atomic_t *v, int i); /* 设置原子变量的值为i */
atomic_t v = ATOMIC_INIT(0); /* 定义原子变量v并初始化为0 */
② 获取原子变量的值
atomic_read(atomic_t *v); /* 返回原子变量的值 */
③ 原子变量加/减
void atomic_add(int i, atomic_t *v); /* 原子变量增加i */
void atomic_sub(int i, atomic_t *v); /* 原子变量减少i */
④ 原子变量自增/自减
void atomic_inc(atomic_t *v); /* 原子变量增加1 */
void atomic_dec(atomic_t *v); /* 原子变量减少1 */
⑤ 操作并测试
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);
⑥ 操作并返回
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);
2. 位原子操作
① 设置位
void set_bit(nr, void *addr);
② 清除位
void clear_bit(nr, void *addr);
③ 改变位
void change_bit(nr, void *addr);
④ 测试位
void test_bit(nr, void *addr);
⑤ 测试并操作位
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);
四、自旋锁
1. 自旋锁的使用
自旋锁是一种典型的对临界资源进行互斥访问的手段,其名称来源于他的工作方式。为了获得一个自旋锁,在某CPU上运行的代码需先执行一个原子操作,该操作测试并设置某个内存变量。
如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个“测试并设置”操作,即进行所谓的“自旋”。
Linux中与自旋锁相关的操作主要有以下4种。
① 定义自旋锁
spinlock_t lock;
② 初始化自旋锁
spin_lock_init(lock);
③ 获得自旋锁
spin_lock(lock);
spin_lock_irq(lock); /* 同时关中断 */
④ 释放自旋锁
spin_unlock(lock);
spin_unlock_irq(lock); /* 同时开中断 */
2. 使用时注意事项
- 自旋锁实际上是忙等待,CPU在等待自旋锁时不做任何有用的工作。因此只有在占用锁的时间极短的情况下,使用自旋锁才是合理的。
- 自旋锁可能导致系统死锁。引起这个问题最常见的情况是递归使用一个自旋锁。
- 在自旋锁锁定期间不能调用可能引起进程调度的函数,这可能导致内核的崩溃。
- 在单核情况下编程的时候,也应该认为自己的CPU是多核的,驱动特别强调跨平台的概念。
五、信号量
信号量是操作系统中最典型的用于同步和互斥的手段,信号量的值可以是0、1或者n。信号量与操作系统中的经典概念PV操作对应。
Linux中与信号量相关的操作主要有下面几种。
① 定义信号量
struct semaphore sem;
② 初始化信号量
void sema_init(struct semaphore *sem, int val);
③ 获得信号量
void down(struct semaphore *sem); /* 会导致休眠,不能在中断上下文中使用 */
int down_interruptible(struct semaphore *sem); /* 可以在中断上下文中使用 */
int down_trylock(struct semaphore *sem); /* 尝试获得信号量,如果能够立即获得则返回0,否则返回非0 */
④ 释放信号量
void up(struct semaphore *sem);
六、互斥体
互斥体是Linux内核中一种更为正宗的实现互斥的手段。
Linux中与互斥体相关的操作主要有下面几种。
① 定义互斥体
struct mutex my_mutex;
② 初始化互斥体
mutex_init(&my_mutex);
③ 获得互斥体
void mutex_lock(struct mutex *lock); /* 会导致休眠,不能在中断上下文中使用 */
int mutex_lock_interruptible(struct mutex *lock); /* 可以在中断上下文中使用 */
int mutex_lock_trylock(struct mutex *lock); /* 尝试获得信号量,如果能够立即获得则返回0,否则返回非0 */
④ 释放互斥体
void mutex_unlock(struct mutex *lock);
七、完成量
Linux提供了完成量,它用于一个执行单元等待另一个执行单元执行完某事。
Linux中与完成量相关的操作主要有以下4种。
① 定义完成量
struct completion my_completion;
② 初始化完成量
init_completion(&my_completion);
reinit_completion(&my_completion);
③ 等待完成量
void wait_for_completion(struct completion *c);
④ 唤醒完成量
void complete(struct completion *c);
void complete_all(struct completion *c);