- 文档
- 教程
- 操作系统
【免费下载链接】linux-insides
A book-in-progress about the Linux kernel and its insides.
导读
本文是 linux-insides 开源书籍(当前仓库 SyncPrim 章节)中关于同步原语的第四部分,聚焦内核中最常用的互斥锁mutex(MUTual EXclusion)。文章从信号量与 mutex 的理论差异出发,逐步拆解struct mutex结构、静态/动态初始化宏,并深入kernel/locking/mutex.c级别的三条获取路径(fastpath、midpath、slowpath)以及解锁慢路径的唤醒机制。读完本文,你将能完整掌握 Linux 内核 mutex 的数据结构、配置选项影响(CONFIG_DEBUG_MUTEXES、CONFIG_MUTEX_SPIN_ON_OWNER)、乐观自旋(optimistic spinning)与 MCS 锁的协作方式,以及mutex_lock/mutex_unlock及衍生 API 的底层调用链。
mutex 概念:与信号量的区别
在上一篇 信号量(semaphore) 中我们了解到,信号量在内核中以如下结构表示:
struct semaphore { raw_spinlock_t lock; unsigned int count; struct list_head wait_list; };该结构保存了锁的状态以及等待者列表。count字段决定了信号量能允许多少进程同时访问受保护的资源(即计数信号量,count 可以大于 1)。
mutex 的概念与信号量非常相似,但语义更严格,主要有两点差异:
- 独占性:同一时刻只有一个进程可以持有 mutex,且只有 mutex 的持有者(owner)才能释放或解锁它。这与二进制信号量的"任意进程都可 up"不同。
- 实现策略不同:信号量的
down实现会强制把等待者放入等待列表并触发进程重新调度(schedule),代价是昂贵的上下文切换;而 mutex 的锁 API 实现允许在锁持有者仍在运行期间进行乐观自旋(optimistic spinning),从而尽量避免调度与上下文切换。
需要强调的是,mutex 适用于"临界区持有时间较长"的场景,而 spinlock 只适合极短临界区(持有期间禁止抢占、原地自旋)。mutex 允许等待者睡眠,代价是慢路径上的调度开销,因此它介于 spinlock 与信号量之间,是内核代码中使用频率最高的锁原语之一。例如在 linux-sync-1.md 中展示的kernel/time/clocksource.c的__clocksource_register_scale函数,就用mutex_lock(&clocksource_mutex)/mutex_unlock(&clocksource_mutex)保护clocksource_list的插入操作,防止两个进程并发执行list_add造成竞态。
struct mutex:内核中的互斥锁结构
Linux 内核中的 mutex 由struct mutex表示(位于上游内核头文件include/linux/mutex.h):
struct mutex { atomic_t count; spinlock_t wait_lock; struct list_head wait_list; #if defined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_MUTEX_SPIN_ON_OWNER) struct task_struct *owner; #endif #ifdef CONFIG_MUTEX_SPIN_ON_OWNER struct optimistic_spin_queue osq; #endif #ifdef CONFIG_DEBUG_MUTEXES void *magic; #endif #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_map dep_map; #endif };各字段的含义如下:
| 字段 | 类型 | 作用 |
|---|---|---|
count | atomic_t | mutex 状态:1表示未锁定(unlocked);0表示已锁定(locked);负值表示已锁定且存在等待者 |
wait_lock | spinlock_t | 保护等待队列的自旋锁 |
wait_list | struct list_head | 等待该锁的进程列表(即等待队列) |
owner | struct task_struct * | 当前持有锁的进程,仅当CONFIG_DEBUG_MUTEXES或CONFIG_MUTEX_SPIN_ON_OWNER开启时存在,是乐观自旋的基础 |
osq | struct optimistic_spin_queue | 乐观自旋使用的 MCS 锁等待队列,仅CONFIG_MUTEX_SPIN_ON_OWNER开启时存在 |
magic | void * | 调试用,存储 mutex 相关信息,仅CONFIG_DEBUG_MUTEXES开启时存在 |
dep_map | struct lockdep_map | 内核锁校验器(lockdep/lock validator)使用,仅CONFIG_DEBUG_LOCK_ALLOC开启时存在 |
注意:count字段与信号量不同,它是atomic_t(原子类型),因为 fastpath 需要直接在它上面做原子减/加操作;而wait_list正是 linux-datastructures-1.md 中介绍的内核侵入式双向链表struct list_head——链表节点嵌入对象内部,配合container_of宏可以从节点指针反推出宿主结构。
从第一个字段count之后,mutex 与信号量的结构相似性就结束了:剩余字段全部由内核配置选项决定,体现了内核"按需编译"的模块化设计思想。
等待者结构 mutex_waiter
当进程在慢路径上睡眠时,它会被加入等待队列,队列节点由struct mutex_waiter表示(同样定义于include/linux/mutex.h):
struct mutex_waiter { struct list_head list; struct task_struct *task; #ifdef CONFIG_DEBUG_MUTEXES void *magic; #endif };它与上一篇 linux-sync-3.md 中kernel/locking/semaphore.c的struct semaphore_waiter结构非常相似:
struct semaphore_waiter { struct list_head list; struct task_struct *task; bool up; };两者都包含list(等待队列节点)和task(等待的进程)字段。唯一的区别是:mutex_waiter没有up标志位(信号量用它来通知__down_common的无限循环退出),但多了受CONFIG_DEBUG_MUTEXES控制的magic字段用于调试。在 mutex 的慢路径中,等待者被唤醒后是通过重新原子交换count来判断是否获得锁的,因此无需up标志。
mutex 的初始化:静态与动态两种方式
静态初始化:DEFINE_MUTEX 宏
#define DEFINE_MUTEX(mutexname) \ struct mutex mutexname = __MUTEX_INITIALIZER(mutexname)DEFINE_MUTEX接收锁的名字,展开为定义一个以该名字命名的struct mutex变量,并用__MUTEX_INITIALIZER完成字段初始化:
#define __MUTEX_INITIALIZER(lockname) \ { \ .count = ATOMIC_INIT(1), \ .wait_lock = __SPIN_LOCK_UNLOCKED(lockname.wait_lock), \ .wait_list = LIST_HEAD_INIT(lockname.wait_list) \ }初始化语义:
.count = ATOMIC_INIT(1):把计数原子地初始化为1,即unlocked(未锁定)状态;.wait_lock = __SPIN_LOCK_UNLOCKED(...):保护等待队列的自旋锁初始化为未锁定状态;.wait_list = LIST_HEAD_INIT(...):等待队列初始化为空的双向链表(关于list_head的初始化与使用,可参考 linux-datastructures-1.md)。
动态初始化:mutex_init 宏与 __mutex_init 函数
实际代码中很少直接调用__mutex_init,而是通过mutex_init宏(定义于include/linux/mutex.h):
# define mutex_init(mutex) \ do { \ static struct lock_class_key __key; \ \ __mutex_init((mutex), #mutex, &__key); \ } while (0)该宏定义了一个静态的lock_class_key(供 lockdep 使用),然后调用__mutex_init。其实现位于kernel/locking/mutex.c:
void __mutex_init(struct mutex *lock, const char *name, struct lock_class_key *key) { atomic_set(&lock->count, 1); spin_lock_init(&lock->wait_lock); INIT_LIST_HEAD(&lock->wait_list); mutex_clear_owner(lock); #ifdef CONFIG_MUTEX_SPIN_ON_OWNER osq_lock_init(&lock->osq); #endif debug_mutex_init(lock, name, key); }__mutex_init接收三个参数:
lock:mutex 本身;name:mutex 名称,供调试使用(由宏中的#mutex字符串化传入);key:锁校验器(lockdep)使用的键。
函数执行流程:
atomic_set(&lock->count, 1)将锁置为 unlocked 状态;spin_lock_init初始化保护等待队列的自旋锁;INIT_LIST_HEAD初始化等待队列链表;mutex_clear_owner(lock)清空 owner(仅当CONFIG_DEBUG_MUTEXES或CONFIG_MUTEX_SPIN_ON_OWNER开启时相关);osq_lock_init(&lock->osq)初始化乐观自旋队列(仅CONFIG_MUTEX_SPIN_ON_OWNER开启时),它把 MCS 队列的 tail 置为未锁定值:static inline bool osq_is_locked(struct optimistic_spin_queue *lock) { return atomic_read(&lock->tail) != OSQ_UNLOCKED_VAL; }- 最后调用
debug_mutex_init做调试初始化(本文略去调试相关内容)。
mutex_lock:三条获取路径
mutex_lock与mutex_unlock的实现位于上游内核kernel/locking/mutex.c。先看mutex_lock:
void __sched mutex_lock(struct mutex *lock) { might_sleep(); __mutex_fastpath_lock(&lock->count, __mutex_lock_slowpath); mutex_set_owner(lock); }- 开头的
might_sleep宏:取决于CONFIG_DEBUG_ATOMIC_SLEEP配置选项。若开启,且该函数在原子上下文(如持有自旋锁、关中断等)中被执行,会打印栈回溯以提示"在原子上下文中睡眠"的潜在 bug;否则为空操作,是纯粹的调试辅助手段。 __mutex_fastpath_lock:架构相关的 fastpath 尝试;本书聚焦 x86_64,其实现位于arch/x86/include/asm/mutex_64.h。mutex_set_owner(lock):在 fastpath 直接成功(未进入慢路径)后设置 owner:
static inline void mutex_set_owner(struct mutex *lock) { lock->owner = current; }根据 mutex 当前状态,一个进程尝试获取锁时可能走三条路径:
| 路径 | 名称 | 触发条件 | 行为 |
|---|---|---|---|
fastpath | 快速路径 | 无人持有锁,count可直接原子减 1 | 立即成功,开销最小 |
midpath | 乐观自旋路径 | 锁被其他进程持有,但持有者正在运行且无更高优先级任务 | 基于 MCS 锁自旋等待,不睡眠 |
slowpath | 慢速路径 | 前两条路径无法完成(持有者睡眠、出现高优先级任务、或配置关闭自旋) | 类似信号量,加入等待队列并睡眠 |
fastpath:原子递减与 asm goto
__mutex_fastpath_lock的实现由两部分组成,第一部分是内联汇编(关于内联汇编的asm goto语法,可参考本仓库 Toolchain/linux-toolchain-4.md):
asm_volatile_goto(LOCK_PREFIX " decl %0\n" " jns %l[exit]\n" : : "m" (v->counter) : "memory", "cc" : exit);其中asm_volatile_goto宏(include/linux/compiler-gcc.h)展开为:
#define asm_volatile_goto(x...) do { asm goto(x); asm (""); } while (0)即一个带goto说明符的汇编语句加上一条空汇编语句作为内存屏障。逐条解读汇编:
LOCK_PREFIX展开为lock;指令前缀(对应 x86 的lock指令),确保后续指令原子执行:#define LOCK_PREFIX LOCK_PREFIX_HERE "\n\tlock; "decl %0:对mutex->counter(内存操作数"m")做原子减 1;jns %l[exit]:若减完后的值不为负(SF标志为 0),则跳转到exit标签。exit是函数第二部分,直接返回:exit: return;
也就是说:只要count从 1 减到 0(成功拿到锁),就立刻返回;如果减完后为负(说明之前已经是 0,即锁已被持有),则jns不跳转,落入下面的fail_fn(v)调用:
fail_fn(v);fail_fn是__mutex_fastpath_lock的第二个参数,即指向 midpath/slowpath 的函数指针,在本例中为__mutex_lock_slowpath。
进入慢路径:__mutex_lock_slowpath
__visible void __sched __mutex_lock_slowpath(atomic_t *lock_count) { struct mutex *lock = container_of(lock_count, struct mutex, count); __mutex_lock_common(lock, TASK_UNINTERRUPTIBLE, 0, NULL, _RET_IP_, NULL, 0); }该函数先用container_of宏从传入的&lock->count反推出宿主struct mutex *(这正是 linux-datastructures-1.md 中介绍的container_of技巧——通过结构体成员指针计算宿主结构地址),然后调用__mutex_lock_common,其中任务状态为TASK_UNINTERRUPTIBLE(不可被信号打断的睡眠)。
__mutex_lock_common的第一步是关闭抢占直到重新调度完成:
preempt_disable();随后进入乐观自旋阶段:
if (mutex_optimistic_spin(lock, ww_ctx, use_ww_ctx)) { preempt_enable(); return 0; }若CONFIG_MUTEX_SPIN_ON_OWNER未开启,mutex_optimistic_spin是空实现,直接返回false,流程将跳过自旋进入真正的 slowpath:
#ifndef CONFIG_MUTEX_SPIN_ON_OWNER static bool mutex_optimistic_spin(struct mutex *lock, struct ww_acquire_ctx *ww_ctx, const bool use_ww_ctx) { return false; } #endifmidpath:乐观自旋(optimistic spinning)
当CONFIG_MUTEX_SPIN_ON_OWNER开启且系统调度器认为当前不需要重新调度(没有更高优先级的就绪任务)时,进入乐观自旋。其要点如下:
- 先调用
osq_lock(&lock->osq)把自己登记进MCS 锁等待队列,保证同一时刻只有一个自旋者去竞争该 mutex,避免多 CPU 同时自旋产生的缓存行乒乓(cache line bouncing)。 - 进入自旋循环:
while (true) { owner = READ_ONCE(lock->owner); if (owner && !mutex_spin_on_owner(lock, owner)) break; if (mutex_try_to_acquire(lock)) { lock_acquired(&lock->dep_map, ip); mutex_set_owner(lock); osq_unlock(&lock->osq); return true; } }- 每次循环先读取当前 owner(使用
READ_ONCE避免编译器重排);若 owner 存在,则调用mutex_spin_on_owner持续自旋等待持有者释放锁; - 若等待期间出现了更高优先级的任务,则
break跳出循环、放弃自旋转而去睡眠; - 若持有者已经释放了锁(owner 为 NULL 或
mutex_try_to_acquire的原子尝试成功),则通过mutex_set_owner设置新 owner、osq_unlock退出 MCS 队列并返回true。
一旦mutex_optimistic_spin返回true,__mutex_lock_common重新开启抢占并成功返回,即"锁已拿到"。
之所以称为乐观(optimistic),是因为等待任务在锁持有者仍处于运行状态时不自旋睡眠、不触发调度,从而避免昂贵的上下文切换;这是 mutex 相比信号量性能上的关键优势。
slowpath:类信号量的睡眠等待
当乐观自旋未成功(出现更高优先级任务、自旋被打断、或配置关闭),__mutex_lock_common退化为类似信号量的行为。它首先再尝试一次获取锁——因为持有者可能恰好在此之前已释放:
if (!mutex_is_locked(lock) && (atomic_xchg_acquire(&lock->count, 0) == 1)) goto skip_wait;atomic_xchg_acquire原子地把count交换为0并返回旧值;若旧值为1,说明锁此刻无人持有,直接跳转到skip_wait成功路径。若尝试失败,则把当前任务加入等待队列:
list_add_tail(&waiter.list, &lock->wait_list); waiter.task = task;若随后再次尝试成功,则走skip_wait更新 owner、开启抢占并返回:
skip_wait: mutex_set_owner(lock); preempt_enable(); return 0;若依然拿不到锁,则进入无限等待循环:
for (;;) { if (atomic_read(&lock->count) >= 0 && (atomic_xchg_acquire(&lock->count, -1) == 1)) break; if (unlikely(signal_pending_state(state, task))) { ret = -EINTR; goto err; } __set_task_state(task, state); schedule_preempt_disabled(); }对循环内逻辑的逐一说明:
- 先尝试把
count从1原子交换为-1(atomic_read先确认非负),成功则break获得锁——循环开始前的这次重试和循环内的尝试,都是为了保证一旦锁被释放就能立即收到唤醒,同时允许进程在睡眠后被唤醒时重新拿到锁; - 若当前任务有 pending 信号(
signal_pending_state检查,mutex_lock 场景 state 为TASK_UNINTERRUPTIBLE,正常不会被信号打断;但mutex_lock_interruptible/mutex_lock_killable等变体会使用可打断状态),则以-EINTR错误返回(goto err); - 否则设置任务状态为
TASK_UNINTERRUPTIBLE并调用schedule_preempt_disabled睡眠,等待持有者唤醒。
注:
mutex_lock_interruptible、mutex_lock_killable、mutex_trylock及对应解锁变体的实现与信号量的down_interruptible、down_killable、down_trylock一一对应,语义已在 linux-sync-3.md 中详细描述:_interruptible允许被任意信号唤醒(TASK_INTERRUPTIBLE),_killable仅允许被 kill 信号唤醒(TASK_KILLABLE),trylock不等待、立即返回。此处不再重复展开。
mutex_unlock:解锁的快速路径与慢路径
mutex_unlock的完整实现:
void __sched mutex_unlock(struct mutex *lock) { __mutex_fastpath_unlock(&lock->count, __mutex_unlock_slowpath); }fastpath 解锁
__mutex_fastpath_unlock(arch/x86/include/asm/mutex_64.h)与加锁 fastpath 几乎一样,唯一区别是加变减、判断条件相反:
static inline void __mutex_fastpath_unlock(atomic_t *v, void (*fail_fn)(atomic_t *)) { asm_volatile_goto(LOCK_PREFIX " incl %0\n" " jg %l[exit]\n" : : "m" (v->counter) : "memory", "cc" : exit); fail_fn(v); exit: return; }incl %0:对mutex->count原子加 1,恢复 unlocked 状态;jg %l[exit]:若加完后的值大于 0(即从 0 加到了 1,无等待者),直接返回;- 若加完仍小于等于 0(说明之前为负,存在等待者),则调用
fail_fn,即__mutex_unlock_slowpath,需要处理等待队列。
slowpath 解锁:唤醒等待者
__mutex_unlock_slowpath(atomic_t *lock_count) { struct mutex *lock = container_of(lock_count, struct mutex, count); __mutex_unlock_common_slowpath(lock, 1); }__mutex_unlock_common_slowpath的核心逻辑是:若等待队列非空,取出队首等待者并唤醒对应进程:
if (!list_empty(&lock->wait_list)) { struct mutex_waiter *waiter = list_entry(lock->wait_list.next, struct mutex_waiter, list); wake_up_process(waiter->task); }list_entry从list_head节点反推struct mutex_waiter(同样是container_of的封装),wake_up_process把睡眠中的等待者置为可运行。此后,前一个进程完成释放,锁由等待队列中的下一个进程接管——这与信号量up中wake_up_process(waiter->task)的唤醒模式一致(参见 linux-sync-3.md 中__up的实现)。
由 mutex 延伸开去:本章后续内容
mutex 的语义与结构在本仓库后续章节中被继续复用与扩展:
- linux-sync-5.md 介绍读写信号量(rw_semaphore):其
struct rw_semaphore同样包含count、wait_list、wait_lock,并且当CONFIG_RWSEM_SPIN_ON_OWNER开启时同样携带struct optimistic_spin_queue osq与struct task_struct *owner字段——这正是从 mutex 继承的乐观自旋基础设施; - 需要回顾前文时,可依次阅读 linux-sync-1.md(spinlock 基础)、linux-sync-2.md(queued spinlocks)、linux-sync-3.md(信号量);本文所属章节的完整目录见 SyncPrim/README.md。
小结
本文完整梳理了 Linux 内核 mutex 的方方面面:
- 概念层:mutex 是语义更严格的二进制信号量——独占持有、只能由 owner 释放,且通过乐观自旋避免不必要的上下文切换;
- 结构层:
struct mutex的count/wait_lock/wait_list三件套与信号量同源,owner/osq/magic/dep_map则分别服务于乐观自旋与调试(受CONFIG_MUTEX_SPIN_ON_OWNER、CONFIG_DEBUG_MUTEXES、CONFIG_DEBUG_LOCK_ALLOC控制);等待者由struct mutex_waiter表示; - 初始化层:静态
DEFINE_MUTEX/__MUTEX_INITIALIZER与动态mutex_init/__mutex_init两条路径; - 加锁层:fastpath(
lock; decl+jns的 asm goto 原子路径)、midpath(基于 MCS 锁osq的乐观自旋)、slowpath(atomic_xchg_acquire重试 + 加入wait_list+schedule_preempt_disabled睡眠)三条路径的完整流转; - 解锁层:fastpath(
lock; incl+jg)与 slowpath(取队首mutex_waiter并wake_up_process)。
掌握这套机制,你就理解了内核中大量mutex_lock/mutex_unlock保护临界区代码(如时钟源注册、文件系统、驱动等子系统)背后的真实执行路径,也为阅读下一章读写信号量打下了结构基础。
- 文档
- 教程
- 操作系统
【免费下载链接】linux-insides
A book-in-progress about the Linux kernel and its insides.
相关推荐
Linux 内核揭秘:互斥锁(mutex)同步原语的实现与三条获取路径深度解析
Linux 内核揭秘:互斥锁(mutex)同步原语的实现与三条获取路径深度解析 互斥锁( mutex ,即 MUTual EXclusion )是 Linux
Linux内核同步原语:深入理解互斥锁(Mutex)
Linux内核同步原语:深入理解互斥锁 Mutex 前言 在多任务操作系统中,同步原语是确保多个执行线程或进程能够正确共享资源的关键机制。Linux内核提供了多
文档教程操作系统自旋锁、互斥锁到顺序锁:Linux 内核揭秘(linux-insides-zh)6 大同步原语完全指南
自旋锁、互斥锁到顺序锁:Linux 内核揭秘(linux insides zh)6 大同步原语完全指南 在多核处理器上,多个 CPU 同时读写同一份数据,轻则数
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考