4.3 同步机制
多任务是操作系统最为关键的特性之一,现代操作系统中可能同时存在多个进程,每个进程又可能包含多个同时执行的线程。在Linux操作系统中,某个线程正在操作的数据很可能也在被另一个线程访问。并发访问的线程可能有以下来源:
1)另一个CPU核上的线程。这是真正的多处理器系统。
2)处于中断上下文的线程。中断处理程序打断当前线程的执行。
3)因调度而抢占的另一线程。中断处理后调度而来的其他内核线程。
当线程之间出现资源访问的冲突时,需要有同步和通信的机制来保证并发数据访问的正确性。如在3.2节中所提到的中断原子性,线程之间的共享数据访问都应该实现原子性:要么完全完成对数据的改动,要么什么改变都没有发生。Linux中包含部分原子操作,如atomic_inc()函数等,这些操作在某些指令系统中可以有特定的实现方法(如X86的lock类指令)。同步机制通常包括基于互斥(Mutual Exclusive)和非阻塞(Non-Blocking)两类。
4.3.1 基于互斥的同步机制
为了使更复杂的操作具有原子性,Linux使用了锁机制。锁是信号量机制的一种简单实现,是对特定数据进行操作的“门票”,访问同一数据的软件都要互相协作,同一时刻只能有一个线程操作该数据,任何访问被锁住数据的线程将被阻塞。
对数据进行原子操作的程序段叫作临界区,在临界区前后应该包含申请锁和释放锁的过程,申请锁失败的线程被阻塞,占有锁的进程在完成临界区操作后应该及时释放锁。
当确认竞争者在另一个CPU核上,而且临界区程序很短时,让等待锁的线程循环检查锁状态直至锁可用显然是合理的,这也是Linux为SMP(Symmetric Multi-Processing)实现的自旋锁。但当竞争者都在同一个CPU核上时,在不可抢占的内核下进行自旋可能导致死锁,此时自旋锁将退化为空操作。
当自旋锁不可用时,需要使用互斥锁的机制。当一个线程获取锁失败时,会将自己阻塞并调用操作系统的调度器。在释放锁的时候还需要同时让其他等待锁的线程离开阻塞状态。挂起和唤醒线程的操作与指令系统无关,但测试锁状态和设置锁的代码依赖于原子的“测试并设置”指令,而LoongArch指令系统的实现方式是LL/SC指令(对32位操作加.W后缀,64位加.D后缀)。LL指令设置LL bit,并检测访问的物理地址是否被修改或可能被修改,在检测到时将LL bit清除。在SMP中,检测LL bit通常使用Cache一致性协议的监听逻辑来实现。在单处理器系统中,异常处理会破坏LL bit。SC指令实现带条件的存储。当LL bit为0时,SC不会完成存储操作,而是把保存值的源操作数寄存器清零以指示失败。
Linux中的atomic_inc()原子操作函数可以使用LL/SC来实现,如下所示。
atomic_inc: ll.w $t0, $a0, 0 addi.w $t0, $t0, 1 sc.w $t0, $a0, 0 beqz $t0, atomic_inc add.w $a0, $t0, $zero jr $ra
当SC失败时,程序会自旋(循环重试)。由于程序很短,上述程序自旋很多次的概率还是很低的。但当LL和SC之间的操作很多时,LL bit就有较大可能被破坏,因此单纯的LL/SC对复杂的操作并不适合。操作复杂时,可以使用LL/SC来构造锁,利用锁来完成线程间的同步和通信需求。LoongArch指令系统中的“测试并设置”和自旋锁指令的实现如下所示。“测试并设置”指令取回锁的旧值并设置新的锁值,自旋锁指令反复自旋得到锁后再进入临界区。
4.3.2 非阻塞的同步机制
基于锁的资源保护和线程同步有以下缺点:
1)若持有锁的线程死亡、阻塞或死循环,则其他等待锁的线程可能永远等待下去。
2)即使冲突的情况非常少,锁机制也有获取锁和释放锁的代价。
3)锁导致的错误与时机有关,难以重现。
4)持有锁的线程因时间片中断或页错误而被取消调度时,其他线程需要等待。
一些非阻塞同步机制可以避免上述不足之处,其中一种较为有名的就是事务内存(Tran-sactional Memory)。事务内存的核心思想是通过尝试性地执行事务代码,在程序运行过程中动态检测事务间的冲突,并根据冲突检测结果提交或取消事务。
可以发现事务内存的核心思想与LL/SC是一致的,事实上LL/SC可以被视为事务内存的一种最基础的实现,只不过LL/SC的局限在于其操作的数据与寄存器宽度相同,只能用于很小的事务。
软件事务内存通过运行时库或专门的编程语言来提供支持,但仍需要最小的硬件支持,如“测试并设置”指令。虽然非常易于多线程编程,但软件事务内存有相当可观的内存空间和执行速度的代价。同时,软件事务内存不能用于无法取消的事务,如多数对IO的访问。
近年来,许多处理器增加了对事务内存的硬件支持。Sun公司在其Rock处理器中实现了硬件事务内存,但在2009年被Oracle公司收购前取消了该处理器,也没有实物发布。2011年,IBM公司在其Blue Gene/Q中首先提供了对事务内存的支持,并在后续的Power8中持续支持。Intel公司最早在Haswell处理器核中支持硬件事务内存,其扩展叫作TSX(Transactional Synchronization Extension)。