因为现代操作系统是多处理器计算的架构,必然更容易遇到多个进程,多个线程访问共享数据的情况,如下图所示:

图中每一种颜色代表一种竞态情况,主要归结为三类:
进程与进程之间:单核上的抢占,多核上的SMP;
进程与中断之间:中断又包含了上半部与下半部,中断总是能打断进程的执行流;
中断与中断之间:外设的中断可以路由到不同的CPU上,它们之间也可能带来竞态;
这时候就需要一种同步机制来保护并发访问的内存数据。文章分为两部分,这一章主要讨论原子操作,自旋锁,信号量和互斥锁。
原子操作是在执行结束前不可打断的操作,也是最小的执行单位。以 arm 平台为例,原子操作的 API 包括如下:
| API | 说明 |
|---|---|
| int atomic_read(atomic_t *v) | 读操作 |
| void atomic_set(atomic_t *v, int i) | 设置变量 |
| 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 |
| void atomic_inc_and_test(atomic_t *v) | 加 1 是否为 0 |
| void atomic_dec_and_test(atomic_t *v) | 减 1 是否为 0 |
| void atomic_add_negative(int i, atomic_t *v) | 加 i 是否为负 |
| void atomic_add_return(int i, atomic_t *v) | 增加 i 返回结果 |
| void atomic_sub_return(int i, atomic_t *v) | 减少 i 返回结果 |
| void atomic_inc_return(int i, atomic_t *v) | 加 1 返回 |
| void atomic_dec_return(int i, atomic_t *v) | 减 1 返回 |
原子操作通常是内联函数,往往是通过内嵌汇编指令来实现的,如果某个函数本身就是原子的,它往往被定义成一个宏。
可见原子操作的原子性依赖于 ldrex 与 strex 实现,ldrex 读取数据时会进行独占标记,防止其他内核路径访问,直至调用 strex 完成写入后清除标记。
ldrex 和 strex 指令,是将单纯的更新内存的原子操作分成了两个独立的步骤:
ldrex Rx, [Ry] 读取寄存器 Ry 指向的4字节内存值,将其保存到 Rx 寄存器中,同时标记对 Ry 指向内存区域的独占访问。如果执行 ldrex 指令的时候发现已经被标记为独占访问了,并不会对指令的执行产生影响。strex Rx, Ry, [Rz] 如果执行这条指令的时候发现已经被标记为独占访问了,则将寄存器 Ry 中的值更新到寄存器 Rz 指向的内存,并将寄存器 Rx 设置成 0。指令执行成功后,会将独占访问标记位清除。如果执行这条指令的时候发现没有设置独占标记,则不会更新内存,且将寄存器 Rx 的值设置成 1。Linux内核中最常见的锁是自旋锁,自旋锁最多只能被一个可执行线程持有。如果一个线程试图获取一个已被持有的自旋锁,这个线程会进行忙循环——旋转等待(会浪费处理器时间)锁重新可用。自旋锁持有期间不可被抢占。
另一种处理锁争用的方式:让等待线程睡眠,直到锁重新可用时再唤醒它,这样处理器不必循环等待,可以去执行其他代码,但是这会有两次明显的上下文切换的开销,信号量便提供了这种锁机制。
自旋锁的使用接口如下:
| API | 说明 |
|---|---|
| spin_lock() | 获取指定的自旋锁 |
| spin_lock_irq() | 禁止本地中断并获取指定的锁 |
| spin_lock_irqsave() | 保存本地中断当前状态,禁止本地中断,获取指定的锁 |
| spin_unlock() | 释放指定的锁 |
| spin_unlock_irq() | 释放指定的锁,并激活本地中断 |
| spin_unlock_irqrestore() | 释放指定的锁,并让本地中断恢复以前状态 |
| spin_lock_init() | 动态初始化指定的锁 |
| spin_trylock() | 试图获取指定的锁,成功返回0,否则返回非0 |
| spin_is_locked() | 测试指定的锁是否已被占用,已被占用返回非0,否则返回0 |