SZ神庙

从此开始,遁入幻想

2017年1月13日

Linux 秘术记闻>

Linux内核同步之自旋锁

一.什么是自旋锁

在操作系统中,经常遇到多个进程争抢某些共享资源的情况,这就需要对这些进程进行同步。例如,当某些进程共享某些数据的时候,需要一次只允许一个进程访问共享数据,避免多个进程同时访问造成数据竞争。自旋锁就是这种情况下保护共享数据的一种解决方案。

自旋锁的基本思想很简单,给每个临界区加一把“锁”,每个进程进入临界区之前,需要检查“锁”是否开启,如果开启,那么进程就将锁锁上并进入临界区。否则,进程执行一个循环等待自旋锁打开。因此,自旋锁本质上是“忙等”的策略。

与一些其他的同步技术,例如互斥锁相比,自旋锁的优点在于其不会引起进程睡眠,因而在多处理器系统上,其效率高于互斥锁。然而,在单处理器系统上,自旋锁的忙等会造成CPU资源的极大浪费。因而,自旋锁主要适用于多处理器系统以及锁的使用时间较短的情况。

二.Linux的自旋锁定义

Linux使用spinlock_t结构表示自旋锁,其定义如下:

可见,其内部主要就是一个int slock字段,表示锁是否开启。

三.自旋锁的操作

3.1 初始化

自旋锁初始化使用spin_lock_init()宏:

__RAW_SPIN_LOCK_UNLOCKED()宏主要做的工作就是将自旋锁初始化为打开状态,并进行debug信息的初始化。

3.2 加锁

加锁使用spin_lock()宏:

可见,spin_lock宏首先调用preemp_disable()禁用内核抢占,然后调用spin_acquire()宏获取该自旋锁。spin_acquire()宏调用_raw_spin_lock()宏来完成这一工作。

在SMP和非SMP环境下,_raw_spin_lock()的行为非常不同,分述如下:

3.2.1 非SMP环境

非SMP环境下,_raw_spin_lock()定义如下:

可见,此时只是简单地禁用内核抢占。

3.2.2 SMP环境

SMP环境下的_raw_spin_lock()定义如下:

可见,其首先禁用内核抢占,然后调用spin_acquire()函数,这个函数主要是用来检查锁的有效性的,比如是否存在递归上锁等情况。实际的上锁操作是在LOCK_CONTENDED宏中完成的。如果没有定义CONFIG_LOCK_STAT(收集锁的统计信息),那么LOCK_CONTENDED宏简单地调用do_raw_spin_lock()函数。后者最终调用arch_spin_trylock()函数来尝试上锁:

arch_spin_trylock()函数和具体平台有关,其主要执行以下一些步骤:

  1. 将某一寄存器置0,然后原子交换锁的slock字段和寄存器的值。随后返回寄存器中的值(即锁slock字段的旧值)。
  2. 如果返回值为1,说明成功取得自旋锁,函数返回。
  3. 否则,调用preemt_enable()函数开启内核抢占,然后执行循环等待。
  4. 跳回1,再次尝试获取锁。

3.3 释放锁

spin_unlock函数释放自旋锁:

在非SMP环境下,raw_spin_unlock()只是简单地调用preempt_enavle()函数。在SMP环境下,其最终调用arch_spin_unlock()函数释放自旋锁。arch_spin_unlock()同样是平台相关的函数,其将锁的slock字段置1。

四.总结

本文简单介绍了自旋锁的原理及其在Linux下的实现。事实上Linux还有许多其他的自旋锁操作,如spin_trylock()、spin_unlock_wait()等,这里就不再介绍了。