英语

通用互斥锁子系统

由 Ingo Molnar <mingo@redhat.com> 发起

由 Davidlohr Bueso <davidlohr@hp.com> 更新

什么是互斥锁?

在 Linux 内核中,互斥锁指的是一种特定的锁定原语,它在共享内存系统上强制执行序列化,而不仅仅是指学术界或类似理论教科书中发现的指代“互斥”的一般术语。 互斥锁是睡眠锁,其行为类似于二进制信号量,于 2006 年 [1] 作为这些信号量的替代品引入。 这种新的数据结构提供了许多优点,包括更简单的接口,以及当时更小的代码(参见缺点)。

[1] https://lwn.net/Articles/164802/

实现

互斥锁由“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 系统使用一个旋转 MCS 锁 (->osq),如下面 (ii) 中所述。

当获取互斥锁时,根据锁的状态,可以采用三种可能的路径

  1. fastpath:尝试通过 cmpxchg() 将所有者与当前任务进行原子交换来获取锁。 这仅适用于无竞争的情况(cmpxchg() 针对 0UL 进行检查,因此上面的所有 3 个状态位都必须为 0)。 如果锁存在竞争,则会转到下一个可能的路径。

  2. midpath:又名乐观旋转,尝试在锁所有者正在运行且没有其他准备运行且优先级更高的任务时旋转以进行获取(need_resched)。 理由是,如果锁所有者正在运行,则很可能会很快释放锁。 互斥锁旋转器使用 MCS 锁排队,以便只有一个旋转器可以竞争互斥锁。

    MCS 锁(由 Mellor-Crummey 和 Scott 提出)是一个简单的自旋锁,具有公平的理想属性,并且每个 CPU 尝试获取锁时都在本地变量上旋转。 它可以避免常见的测试和设置自旋锁实现所产生的昂贵的缓存行反弹。 类似于 MCS 的锁是专门为睡眠锁实现的乐观旋转而定制的。 定制 MCS 锁的一个重要特性是,当旋转器需要重新调度时,它们能够退出 MCS 自旋锁队列。 这进一步有助于避免需要重新调度的 MCS 旋转器继续等待旋转互斥锁所有者,而只是在获得 MCS 锁后直接进入慢速路径的情况。

  3. slowpath:最后的手段,如果仍然无法获取锁,则将任务添加到等待队列并休眠,直到被解锁路径唤醒。 在正常情况下,它会以 TASK_UNINTERRUPTIBLE 状态阻塞。

虽然形式上内核互斥锁是可睡眠锁,但正是路径 (ii) 使它们在实践中更像是一种混合类型。 通过简单地不中断任务,并忙等待几个周期而不是立即休眠,已经看到这种锁的性能显着提高了许多工作负载。 请注意,该技术也用于读写信号量。

语义

互斥锁子系统检查并强制执行以下规则

  • 一次只能有一个任务持有互斥锁。

  • 只有所有者才能解锁互斥锁。

  • 不允许多次解锁。

  • 不允许递归锁定/解锁。

  • 互斥锁必须仅通过 API 初始化(见下文)。

  • 任务不得在持有互斥锁的情况下退出。

  • 不得释放持有锁所在的内存区域。

  • 不得重新初始化持有的互斥锁。

  • 互斥锁不得用于硬件或软件中断上下文,例如 tasklet 和定时器。

当启用 CONFIG_DEBUG_MUTEXES 时,将完全强制执行这些语义。 此外,互斥锁调试代码还实现了许多其他功能,使锁调试更容易和更快

  • 只要在调试输出中打印互斥锁,就使用互斥锁的符号名称。

  • 获取点跟踪、函数名称的符号查找、系统中持有的所有锁的列表、它们的打印输出。

  • 所有者跟踪。

  • 检测自递归锁并打印出所有相关信息。

  • 检测多任务循环死锁并打印出所有受影响的锁和任务(以及仅那些任务)。

互斥锁 - 以及大多数其他睡眠锁(如 rwsems)- 不会为其占用的内存提供隐式引用,该引用会通过 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);

获取互斥锁,可中断,如果 dec 为 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 缓存和内存占用。

何时使用互斥锁

除非互斥锁的严格语义不合适,并且/或者临界区阻止锁被共享,否则始终优先于任何其他锁定原语。