由于多核和中断的存在,系统中存在多种并发的执行流,如果在这些并发的执行流之间共享数据,则可能引发竞争条件。这就是操作系统中常说的 “同步” 问题(不访问共享数据的并发执行流是不会引起同步问题的)。由于我们主要讨论操作系统实现编码,对同步互斥问题不熟悉的读者请参考操作系统原理性的材料。
互斥需要原子操作的支持,而且无法在 C 语言层面得以实现,必须在汇编 / 机器语言级实现。C 语言操作无法保证原子性,即便是一条对变量 “读-修改-写” 操作,例如变量 a 的自减 1 操作 a–,经编译后由多条汇编组成(读 a 的初值,减一,更 a 的新值)并有可能被中断打断。
下面分别讨论内核代码见的同步和用户代码间的同步机制,用户态代码和内核代码之间通常没有并发访问的共享数据,不考虑它们之间的同步问题。
需要考虑的内核同步的情况如下:
在单处理器情况下,每条指令的执行都是原子性的(不会被中断所打断),内核代码间的访问共享变量的代码只要关中断就万事大吉。多处理器情况下,那些单独的读操作或写操作是原子性的(例如 a=1 这样的写操作)。但是前面提到的 a--
虽然不会被中断所打断,但是如果两个核同时执行 a--
,那么可能出现以下情况:CPU A 从内存中读取变量 a=1 的初值,同时 CPU B 也从内存中读取变量 a=1 的初值;然后各自都执行 a– 将 a 变量的值修改为 0;然后 CPU A 和 CPU B 先后执行写内存的操作,最终使得变量 a 的值为 0(正确值应该是 -1
)。这样 一来就无法将变量 a 作为信号量并执行 p 操作。
针对多核环境下内存访问中的 “读-修改-写” 操作非原子性问题,X86 提供了附加的 lock 前缀。lock 前缀使得执行该指令的处理器将内存总线锁住,其他处理器无法访问内存,从而使得带 lock 前缀的 “读-修改-写” 指令(例如 a--
)也能原子性执行。
xchg
指令也是原子性执行(即使不带 lock 前缀),也就是说 xchg
执行时默认会锁内存总线。交换指令 XCHG 是两个寄存器、寄存器和内存变量之间的原子性、数值交换指令,两个操作数的数据类型要相同,可以是一个字节,也可以是一个字,也可以是双字。寄存器不能是段寄存器,两个操作数也不能同时为内存变量。 XCHG 指令不影响标志位。
其指令格式如下:寄存器-内存间的 XCHG Reg,Mem、内存-寄存器间的 XCHG Mem,Reg 以及今存其之间的 XCHG Reg,Reg。例如 XCHG CH, AL
寄存器之间相互交换,字节操作;XCHG BX,SI
寄存器之间相互交换,字操作;XCHG [SI],CX
存储器与寄存器之间交换,字操作。
借助这个原子性的交换操作,XV6 构建 acquire()
加锁和 release()
解锁操作,从而提供所需 的护持保护。
各进程如果对共享变量进行 “读-修改-写” 操作则产生了竞争条件,因此这些代码间也需要互斥地完成 “读-修改-写” 操作。用户代码间的互斥可以在用户态完成,但是操作系统通常提供用户态使用的系统调用来提供建锁的创建、加锁、解锁和销毁功能。
无论是单核环境下的关中断,还是多核环境下的用户代码间的同步没有内核代码那么严格,内核代码不允许睡眠,因此只能采用自旋锁,而用户代码在加锁失败后允许睡眠。
内核代码中有可能需要同时持有多个锁,为了避免造成死锁的情况,XV6 中要求加锁路径要有相同的顺序,从而不会构成 “请求-保持” 环。