通用互斥锁子系统¶
由 Ingo Molnar 发起 <mingo@redhat.com>
由 Davidlohr Bueso 更新 <davidlohr@hp.com>
什么是互斥锁?¶
在 Linux 内核中,互斥锁是指在共享内存系统上强制串行化的特定锁定原语,而不仅仅是在学术界或类似的理论教科书中发现的指“互斥”的通用术语。互斥锁是休眠锁,其行为类似于二进制信号量,并在 2006 年[1] 作为这些锁的替代品引入。这种新的数据结构提供了一些优点,包括更简单的接口,以及当时更小的代码(请参阅缺点)。
实现¶
互斥锁由 ‘struct mutex’ 表示,在 include/linux/mutex.h 中定义,并在 kernel/locking/mutex.c 中实现。这些锁使用一个原子变量 (->owner) 来跟踪其生命周期内的锁状态。字段 owner 实际上包含当前锁所有者的 struct task_struct *,因此如果当前没有所有者,则为 NULL。由于 task_struct 指针至少与 L1_CACHE_BYTES 对齐,因此低位 (3) 用于存储额外的状态(例如,等待者列表是否非空)。在其最基本的形式中,它还包括一个等待队列和一个自旋锁,用于串行化对其的访问。此外,CONFIG_MUTEX_SPIN_ON_OWNER=y 系统使用一个 spinner MCS 锁 (->osq),如下面 (ii) 中所述。
当获取互斥锁时,根据锁的状态,可以采取三种可能的路径
快速路径:尝试通过 cmpxchg() 将所有者与当前任务进行原子交换来原子地获取锁。这仅在无争用的情况下有效(cmpxchg() 检查 0UL,因此上面的所有 3 个状态位都必须为 0)。如果锁有争用,则会转到下一个可能的路径。
中间路径:又名乐观自旋,当锁所有者正在运行时,并且没有其他优先级更高的任务准备运行时(need_resched),尝试自旋以获取锁。其基本原理是,如果锁所有者正在运行,则很可能会很快释放锁。互斥锁自旋器使用 MCS 锁进行排队,以便只有一个自旋器可以争用互斥锁。
MCS 锁(由 Mellor-Crummey 和 Scott 提出)是一个简单的自旋锁,具有理想的特性,即公平,并且每个 CPU 尝试获取锁时都在本地变量上自旋。它避免了常见的测试和设置自旋锁实现中产生的昂贵的缓存行反弹。类似 MCS 的锁是专门为睡眠锁实现的乐观自旋而定制的。定制 MCS 锁的一个重要特性是,当自旋器需要重新调度时,它们能够退出 MCS 自旋锁队列。这进一步有助于避免 MCS 自旋器需要重新调度时继续等待在互斥锁所有者上自旋的情况,而获得 MCS 锁后直接进入慢速路径。
慢速路径:最后手段,如果仍然无法获取锁,则将任务添加到等待队列并休眠,直到被解锁路径唤醒。在正常情况下,它会以 TASK_UNINTERRUPTIBLE 状态阻塞。
虽然形式上内核互斥锁是可睡眠的锁,但正是路径 (ii) 使它们在实践中更像一种混合类型。通过简单地不中断任务并忙等待几个周期而不是立即睡眠,可以看到这种锁的性能显着提高了许多工作负载。请注意,此技术也用于读写信号量。
语义¶
互斥锁子系统检查并强制执行以下规则
一次只能有一个任务持有互斥锁。
只有所有者才能解锁互斥锁。
不允许多次解锁。
不允许递归锁定/解锁。
必须仅通过 API 初始化互斥锁(见下文)。
任务不得在持有互斥锁的情况下退出。
持有锁所在的内存区域不得释放。
持有的互斥锁不得重新初始化。
互斥锁不得在硬件或软件中断上下文中使用,例如 tasklet 和定时器。
启用 CONFIG DEBUG_MUTEXES 时,会完全强制执行这些语义。此外,互斥锁调试代码还实现了一些其他功能,使锁调试更容易和更快
在调试输出中打印时,使用互斥锁的符号名称。
获取点跟踪,函数名称的符号查找,系统中持有的所有锁的列表,并打印出来。
所有者跟踪。
检测自递归锁并打印出所有相关信息。
检测多任务循环死锁并打印出所有受影响的锁和任务(以及仅这些任务)。
互斥锁 - 以及大多数其他睡眠锁(如 rwsem) - 不会为其占用的内存提供隐式引用,该引用会通过 mutex_unlock()
释放。
- [ 这与 spin_unlock() [或 completion_done()] 相反,后者
API 可用于保证在 spin_unlock()/completion_done() 释放锁后,锁实现不会触及内存。]
mutex_unlock()
即使在内部已经释放锁后,仍然可以访问互斥锁结构 - 因此另一个上下文获取互斥锁并假设 mutex_unlock()
上下文不再使用该结构是不安全的。
互斥锁用户必须确保在释放操作仍在进行时不会销毁互斥锁 - 换句话说,mutex_unlock()
的调用者必须确保互斥锁在 mutex_unlock()
返回之前保持活动状态。
接口¶
静态定义互斥锁
DEFINE_MUTEX(name);
动态初始化互斥锁
mutex_init(mutex);
获取互斥锁,不可中断
void mutex_lock(struct mutex *lock);
void mutex_lock_nested(struct mutex *lock, unsigned int subclass);
int mutex_trylock(struct mutex *lock);
获取互斥锁,可中断
int mutex_lock_interruptible_nested(struct mutex *lock,
unsigned int subclass);
int mutex_lock_interruptible(struct mutex *lock);
获取互斥锁,可中断,如果减 1 为 0
int atomic_dec_and_mutex_lock(atomic_t *cnt, struct mutex *lock);
解锁互斥锁
void mutex_unlock(struct mutex *lock);
测试是否已获取互斥锁
int mutex_is_locked(struct mutex *lock);
缺点¶
与最初的设计和目的不同,‘struct mutex’ 是内核中最大的锁之一。例如:在 x86-64 上,它是 32 个字节,而 ‘struct semaphore’ 是 24 个字节,rw_semaphore 是 40 个字节。更大的结构大小意味着更多的 CPU 缓存和内存占用。
何时使用互斥锁¶
除非互斥锁的严格语义不适用和/或临界区阻止锁共享,否则始终首选它们而不是任何其他锁定原语。