因为现代操作系统是多处理器计算的架构,必然更容易遇到多个进程,多个线程访问共享数据的情况,如下图所示:
图中每一种颜色代表一种竞态情况,主要归结为三类:
-
进程与进程之间:单核上的抢占,多核上的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 用来读取内存中的值,并标记对该段内存的独占访问:
读取寄存器 Ry 指向的4字节内存值,将其保存到 Rx 寄存器中,同时标记对 Ry 指向内存区域的独占访问。如果执行 ldrex 指令的时候发现已经被标记为独占访问了,并不会对指令的执行产生影响。ldrex Rx, [Ry]
- strex 在更新内存数值时,会检查该段内存是否已经被标记为独占访问,并以此来决定是否更新内存中的值:
如果执行这条指令的时候发现已经被标记为独占访问了,则将寄存器 Ry 中的值更新到寄存器 Rz 指向的内存,并将寄存器 Rx 设置成 0。指令执行成功后,会将独占访问标记位清除。如果执行这条指令的时候发现没有设置独占标记,则不会更新内存,且将寄存器 Rx 的值设置成 1。strex Rx, Ry, [Rz]
自旋锁 spin_lock
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 |