拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux 内核互斥锁(mutex)全解析:结构定义、三条获取路径与锁/解锁 API 实现——linux-insides 同步原语系列

Linux 内核互斥锁(mutex)全解析:结构定义、三条获取路径与锁/解锁 API 实现——linux-insides 同步原语系列
  • 文档
  • 教程
  • 操作系统

【免费下载链接】linux-insides

A book-in-progress about the Linux kernel and its insides.

项目地址:https://gitcode.com/gh_mirrors/li/linux-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 的概念与信号量非常相似,但语义更严格,主要有两点差异:

  1. 独占性:同一时刻只有一个进程可以持有 mutex,且只有 mutex 的持有者(owner)才能释放或解锁它。这与二进制信号量的"任意进程都可 up"不同。
  2. 实现策略不同:信号量的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 };

各字段的含义如下:

字段类型作用
countatomic_tmutex 状态:1表示未锁定(unlocked);0表示已锁定(locked);负值表示已锁定且存在等待者
wait_lockspinlock_t保护等待队列的自旋锁
wait_liststruct list_head等待该锁的进程列表(即等待队列)
ownerstruct task_struct *当前持有锁的进程,仅当CONFIG_DEBUG_MUTEXES或CONFIG_MUTEX_SPIN_ON_OWNER开启时存在,是乐观自旋的基础
osqstruct optimistic_spin_queue乐观自旋使用的 MCS 锁等待队列,仅CONFIG_MUTEX_SPIN_ON_OWNER开启时存在
magicvoid *调试用,存储 mutex 相关信息,仅CONFIG_DEBUG_MUTEXES开启时存在
dep_mapstruct 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)使用的键。

函数执行流程:

  1. atomic_set(&lock->count, 1)将锁置为 unlocked 状态;
  2. spin_lock_init初始化保护等待队列的自旋锁;
  3. INIT_LIST_HEAD初始化等待队列链表;
  4. mutex_clear_owner(lock)清空 owner(仅当CONFIG_DEBUG_MUTEXES或CONFIG_MUTEX_SPIN_ON_OWNER开启时相关);
  5. 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; }
  6. 最后调用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; } #endif

midpath:乐观自旋(optimistic spinning)

当CONFIG_MUTEX_SPIN_ON_OWNER开启且系统调度器认为当前不需要重新调度(没有更高优先级的就绪任务)时,进入乐观自旋。其要点如下:

  1. 先调用osq_lock(&lock->osq)把自己登记进MCS 锁等待队列,保证同一时刻只有一个自旋者去竞争该 mutex,避免多 CPU 同时自旋产生的缓存行乒乓(cache line bouncing)。
  2. 进入自旋循环:
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(); }

对循环内逻辑的逐一说明:

  1. 先尝试把count从1原子交换为-1(atomic_read先确认非负),成功则break获得锁——循环开始前的这次重试和循环内的尝试,都是为了保证一旦锁被释放就能立即收到唤醒,同时允许进程在睡眠后被唤醒时重新拿到锁;
  2. 若当前任务有 pending 信号(signal_pending_state检查,mutex_lock 场景 state 为TASK_UNINTERRUPTIBLE,正常不会被信号打断;但mutex_lock_interruptible/mutex_lock_killable等变体会使用可打断状态),则以-EINTR错误返回(goto err);
  3. 否则设置任务状态为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 的方方面面:

  1. 概念层:mutex 是语义更严格的二进制信号量——独占持有、只能由 owner 释放,且通过乐观自旋避免不必要的上下文切换;
  2. 结构层: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表示;
  3. 初始化层:静态DEFINE_MUTEX/__MUTEX_INITIALIZER与动态mutex_init/__mutex_init两条路径;
  4. 加锁层:fastpath(lock; decl+jns的 asm goto 原子路径)、midpath(基于 MCS 锁osq的乐观自旋)、slowpath(atomic_xchg_acquire重试 + 加入wait_list+schedule_preempt_disabled睡眠)三条路径的完整流转;
  5. 解锁层: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.

项目地址:https://gitcode.com/gh_mirrors/li/linux-insides
点击查看免费下载

相关推荐

上一篇:5分钟上手:使用ens-contracts构建自定义域名解析器的完整教程
下一篇:如何在3分钟内从国家中小学智慧教育平台下载电子课本PDF:新手完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表