Linux 内核揭秘 · 同步原语(四):互斥锁 mutex 的语义、初始化与 fast/mid/slow 三条获取路径
文档教程操作系统【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/gh_mirrors/li/linux-insides-zh点击查看免费下载本篇是《Linux 内核揭秘》同步原语章节的第四部分聚焦内核中最常用的同步原语之一——互斥锁mutex全称MUTual EXclusion。在读完前几篇的自旋锁与信号量之后本文将带你从理论语义出发完整剖析struct mutex的数据结构、静态/动态两种初始化方式以及mutex_lock/mutex_unlock背后的 fastpath、midpath乐观自旋与 slowpath 三条获取路径最终掌握mutex与semaphore的本质差异及其在内核中的工程实现。从信号量到互斥锁为什么需要更严格的语义在上一篇中我们已经认识了信号量它在内核中由如下结构表示struct semaphore { raw_spinlock_t lock; unsigned int count; struct list_head wait_list; };该结构包含一把自旋锁lock、一个记录现有资源数量的计数器count以及一个等待者列表wait_list。根据count的取值semaphore可以同时向多个进程授予资源访问权——它是一个计数型原语。mutex的概念与信号量相似却有着更严格的语义互斥性与信号量不同任意时刻一个mutex只能被一个进程持有且只有持锁者本人才能对其执行释放解锁操作避免强制重调度信号量原语会在等待列表上强制触发重新调度而mutexAPI 的实现允许在锁未被占用时通过自旋等待避免这一昂贵的上下文切换开销。从语义上讲mutex可以视作一个二进制信号量值只能为 0 或 1但它的实现与内核中信号量的实现并不相同。理解这一差异是把握本篇全部内容的前提。struct mutex内核中互斥锁的表示互斥锁同步原语在内核中由如下结构表示定义于内核头文件include/linux/mutex.hstruct 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 };与struct semaphore类似mutex也包含count、wait_lock、wait_list三要素但两者的相似之处到此为止。逐字段来看count——记录互斥锁的状态也是 fastpath 操作的核心值为1互斥锁处于**无锁unlocked**状态值为0互斥锁处于**上锁locked**状态值为负数互斥锁处于上锁状态且存在其他等待者。wait_lock——一把保护等待队列的自旋锁wait_list——由该锁的等待者构成的等待队列双向链表owner——持有该锁的进程指针其存在与否取决于CONFIG_DEBUG_MUTEXES或CONFIG_MUTEX_SPIN_ON_OWNER两个配置选项主要服务于下文将要介绍的乐观自旋optimistic spinningosq——optimistic_spin_queue即乐观自旋所用的 MCS 锁 队列仅在CONFIG_MUTEX_SPIN_ON_OWNER下存在magic——仅在CONFIG_DEBUG_MUTEXES下存在用于存储与互斥锁调试相关的信息dep_map——仅在CONFIG_DEBUG_LOCK_ALLOC下存在供内核的锁验证器lock validator使用。各配置选项与字段的对应关系可汇总如下内核配置选项影响的mutex字段作用CONFIG_MUTEX_SPIN_ON_OWNERowner、osq启用乐观自旋midpath避免上下文切换CONFIG_DEBUG_MUTEXESowner、magic互斥锁调试信息存储CONFIG_DEBUG_LOCK_ALLOCdep_map支持 lockdep 锁验证器CONFIG_DEBUG_ATOMIC_SLEEP—影响might_sleep在原子上下文中睡眠时打印栈追踪一个进程想获取锁时直观想法是直接对mutex-count做递减释放锁时则递增。方向没错但内核中的真实实现远没有这么简单——它要根据当前 mutex 的状态在三条路径之间做选择。mutex_waiter等待队列的节点当进程无法立即获得锁而必须排队时它会被以struct mutex_waiter的形式加入等待队列定义于include/linux/mutex.hstruct mutex_waiter { struct list_head list; struct task_struct *task; #ifdef CONFIG_DEBUG_MUTEXES void *magic; #endif };如果你读过本章节的前一部分会注意到它与kernel/locking/semaphore.c中的semaphore_waiter结构十分相似struct semaphore_waiter { struct list_head list; struct task_struct *task; bool up; };两者的list与task字段都用于表示等待队列list将等待者挂入双向链表task指向等待中的进程。唯一差异在于mutex_waiter没有up字段信号量的up字段用于标记“锁已被释放你可以醒来了”取而代之的是一个受CONFIG_DEBUG_MUTEXES控制的magic字段用于在调试互斥锁问题时存储有用信息。获取互斥锁的三条路径fastpath / midpath / slowpath当某进程试图获取互斥锁时内核会根据锁的当前状态在三条路径中做选择fastpath快路径——最快的一条路径。当互斥锁尚未被任何人持有时直接对mutex-count做原子递减即可释放时则对称地做原子递增。所有操作都必须是原子的midpath中路径——即乐观自旋。当锁已被其他进程持有、但持有者仍在运行时等待进程在已经熟悉的 MCS 锁 上循环自旋。这条路径只有在没有其他更高优先级进程准备运行时才会执行它被称作“乐观”是因为等待进程既不会睡眠也不会被调度走从而避免了昂贵的上下文切换slowpath慢路径——当 fastpath 与 midpath 都无法执行时的兜底路径行为与信号量的锁操作相似如果锁无法获取进程被以struct mutex_waiter加入等待队列然后进入睡眠。下面我们按“初始化 → 上锁三条路径→ 解锁”的顺序逐层拆解内核的mutexAPI 实现。初始化互斥锁静态与动态两种方式与信号量一样mutex支持两种初始化方式。静态初始化DEFINE_MUTEX 与 __MUTEX_INITIALIZER第一种是静态初始化内核提供了DEFINE_MUTEX宏#define DEFINE_MUTEX(mutexname) \ struct mutex mutexname __MUTEX_INITIALIZER(mutexname)它接受新定义的互斥锁名称展开成一个新的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被初始化为1代表互斥锁处于无锁状态ATOMIC_INIT(1)保证计数器初始值是原子的wait_lock自旋锁被初始化为解锁状态wait_list被初始化为空的双向链表LIST_HEAD_INIT。动态初始化mutex_init 与 __mutex_init第二种方式是动态初始化调用定义于kernel/locking/mutex.c的__mutex_init函数。实践中很少直接调用它而是使用封装好的mutex_init宏# define mutex_init(mutex) \ do { \ static struct lock_class_key __key; \ \ __mutex_init((mutex), #mutex, __key); \ } while (0)mutex_init宏先声明一个静态的lock_class_key供锁验证器使用再以互斥锁本身、其名称字符串与__key为参数调用__mutex_init。来看__mutex_init的实现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——互斥锁本身name——调试用的互斥锁名称key——锁验证器lockdep使用的 key。函数体与静态初始化宏做的事情几乎一一对应atomic_set(lock-count, 1)以原子方式将count设为1使互斥锁处于无锁状态spin_lock_init(lock-wait_lock)初始化保护等待队列的自旋锁INIT_LIST_HEAD(lock-wait_list)初始化空的等待队列mutex_clear_owner(lock)清除锁的持有者置空owner在CONFIG_MUTEX_SPIN_ON_OWNER开启时调用osq_lock_init(lock-osq)初始化乐观队列——它只是将乐观队列的tail设为无锁状态正如include/linux/osq_lock.h中osq_is_locked所体现的判定逻辑static inline bool osq_is_locked(struct optimistic_spin_queue *lock) { return atomic_read(lock-tail) ! OSQ_UNLOCKED_VAL; }末尾调用debug_mutex_init完成调试相关初始化本系列不展开讨论调试与 lockdep 内容。至此一个mutex已可投入使用。接下来看它最重要的两个 APImutex_lock与mutex_unlock二者均实现于kernel/locking/mutex.c。mutex_lock 与 fastpath一条内联汇编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宏来自include/linux/kernel.h。该宏的实现取决于CONFIG_DEBUG_ATOMIC_SLEEP内核配置选项若启用当它在原子上下文中被执行时会打印栈追踪。这纯粹是调试辅助手段除此之外不做任何事。随后调用__mutex_fastpath_lock。该函数是体系结构相关的针对本书讨论的 x86_64 架构其实现位于arch/x86/include/asm/mutex_64.h。从函数名即可看出它尝试通过 fastpath 获取锁——也就是试图原子地递减给定互斥锁的count字段。__mutex_fastpath_lock由两部分组成。第一部分是内联汇编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特性第二个空的内联汇编充当内存屏障barrier。汇编本体以LOCK_PREFIX开头它只是展开为lock指令前缀#define LOCK_PREFIX LOCK_PREFIX_HERE \n\tlock; lock前缀保证被修饰的指令原子地执行。所以汇编的第一条指令decl %0就是在原子地递减mutex-count即v-counter。递减之后如果结果为非负数jnsjump if not sign指令会跳转到exit标签——也就是__mutex_fastpath_lock函数的出口exit: return;此时 fastpath 成功锁已被获取。但如果mutex-count递减后为负说明锁已被其他进程持有count从0变成-1汇编之后会调用 fail 处理函数fail_fn(v);fail_fn是__mutex_fastpath_lock的第二个参数即指向“获取锁的 midpath/slowpath 函数”的指针。在我们的例子中它是__mutex_lock_slowpath。在进入 slowpath 之前先看完mutex_lock的收尾在最简单的情况下fastpath 成功mutex_lock末尾会调用mutex_set_owner(lock);mutex_set_owner定义于内核的kernel/locking/mutex.h作用是把锁的持有者设为当前进程static inline void mutex_set_owner(struct mutex *lock) { lock-owner current; }深入 slowpath__mutex_lock_slowpath 与 __mutex_lock_common当进程因锁已被他人持有而无法通过 fastpath 获取时__mutex_lock_slowpath被调用。该函数实现于kernel/locking/mutex.c开头用container_of从__mutex_fastpath_lock传入的互斥锁状态变量反推出互斥锁本身__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); }随后把得到的互斥锁交给__mutex_lock_common。__mutex_lock_common一上来先关闭抢占直到下一次重新调度时才恢复preempt_disable();接下来进入**乐观自旋optimistic spinning**阶段——这正是前面提到的 midpath。该阶段受CONFIG_MUTEX_SPIN_ON_OWNER控制若选项被禁用则直接跳过并落入最后的 slowpathif (mutex_optimistic_spin(lock, ww_ctx, use_ww_ctx)) { preempt_enable(); return 0; }midpath乐观自旋的循环mutex_optimistic_spin首先检查当前进程是否需要被重新调度——换言之检查是否没有其他更高优先级的任务可以运行。若检查通过它把当前的自旋者spinner挂入MCS 锁的等待队列保证互斥锁在某一时刻只会被一个自旋者获取osq_lock(lock-osq)随后在下面的循环中迭代并尝试获取锁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; } }逐行解读这个循环首先尝试读取锁的持有者信息READ_ONCE(lock-owner)。若持有者存在进程释放互斥锁后该字段可能为空就在mutex_spin_on_owner中等待直到持有者释放锁若在等待持有者的过程中出现了更高优先级的任务就跳出循环放弃乐观自旋转入睡眠若锁已被持有者释放就通过mutex_try_to_acquire尝试获取若获取成功为该互斥锁设置新持有者mutex_set_owner将自身从 MCS 等待队列中移除osq_unlock并从mutex_optimistic_spin返回。此时锁已被成功获取接着恢复抢占并离开__mutex_lock_commonif (mutex_optimistic_spin(lock, ww_ctx, use_ww_ctx)) { preempt_enable(); return 0; }乐观自旋失效时的退化行为但并非所有情况都这么顺利。可能出现三种情形在乐观自旋循环期间有新任务到来进入循环前系统就存在更高优先级的任务或者CONFIG_MUTEX_SPIN_ON_OWNER干脆被禁用——此时mutex_optimistic_spin什么也不做直接返回false#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在所有这类情况下__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路径更新锁的持有者、允许抢占并从__mutex_lock_common返回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 0锁已释放且原子交换成功旧值为1时才会break。循环体开头会重复一次循环前已做过的尝试这是有意的一方面确保一旦锁在之后被解锁进程能够被唤醒另一方面允许进程在睡眠之后获得锁检查当前进程是否有 pending 的信号如果进程在等待期间被信号中断则返回-EINTR并跳转到err标签退出若既没拿到锁也没被中断将任务状态设为TASK_UNINTERRUPTIBLE并调用schedule_preempt_disabled进入睡眠等待下次被唤醒后再回到循环。至此进程获取互斥锁可能经过的三条路径——fastpath、midpath乐观自旋、slowpath——就全部走完了。mutex_unlock释放与唤醒mutex_unlock由希望释放锁的进程调用同样定义于kernel/locking/mutex.c它调用来自arch/x86/include/asm/mutex_64.h的__mutex_fastpath_unlockvoid __sched mutex_unlock(struct mutex *lock) { __mutex_fastpath_unlock(lock-count, __mutex_unlock_slowpath); }__mutex_fastpath_unlock的实现与__mutex_fastpath_lock非常相似唯一的区别是这里递增mutex-countstatic 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把count递增使互斥锁回到无锁状态若结果为正jgjump if greater说明此前count已为非负、没有等待者排队直接跳转到exit返回。但如果释放时等待队列中还有条目count递增后仍可能 ≤ 0此时调用fail_fn即__mutex_unlock_slowpath。__mutex_unlock_slowpath同样先用container_of从mutex-count反推出mutex实例再调用__mutex_unlock_common_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); }wake_up_process唤醒的进程会从之前__mutex_lock_common的睡眠循环中被调度回来重新尝试获取锁。至此前一个进程释放了互斥锁由等待队列中排在最前面的进程接手——这也正是mutex的公平排队语义。其他互斥锁 API 一览除了mutex_lock与mutex_unlockLinux 内核还提供以下互斥锁 APImutex_lock_interruptiblemutex_lock_killablemutex_trylock以及与之对应的同前缀unlock系列函数。这些 API 的实现逻辑与信号量提供的对应 API 高度类似核心差异在于传入的任务状态标志mutex_lock_interruptible允许进程被信号中断对应TASK_INTERRUPTIBLEmutex_lock_killable只允许被致命信号杀死对应TASK_KILLABLE而mutex_trylock则像down_trylock一样尝试获取后立即返回、失败时不排队等待。想深入了解这些变体的读者可以回到信号量 API 一篇对照阅读。总结本篇作为《Linux 内核揭秘》同步原语章节的第四部分完整剖析了互斥锁mutex语义层面mutex与信号量相似但语义更严格——任意时刻只能有一个持有者且只有持有者能释放概念上等价于二进制信号量实现却完全不同结构层面struct mutex在count/wait_lock/wait_list三要素之上按CONFIG_MUTEX_SPIN_ON_OWNER、CONFIG_DEBUG_MUTEXES、CONFIG_DEBUG_LOCK_ALLOC等配置选项有条件地携带owner、osq、magic、dep_map字段初始化层面DEFINE_MUTEX/__MUTEX_INITIALIZER静态初始化与mutex_init/__mutex_init动态初始化两条路径获取路径层面fastpath原子递减count的内联汇编→ midpath基于 MCS 锁的乐观自旋→ slowpath像信号量一样排队睡眠三条路径的完整决策与实现释放层面__mutex_fastpath_unlock原子递增count有等待者时通过__mutex_unlock_common_slowpath唤醒队首进程。在下一部分中我们将继续深入内核同步原语研究基于信号量实现的特殊类型——读者/写者信号量reader/writer semaphore届时你会看到count字段如何进一步编码活动读者数、等待写者等复杂状态。赞分享文档教程操作系统【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/gh_mirrors/li/linux-insides-zh点击查看免费下载相关推荐Linux 内核揭秘互斥锁mutex同步原语的实现与三条获取路径深度解析Linux 内核揭秘互斥锁mutex同步原语的实现与三条获取路径深度解析 互斥锁 mutex 即 MUTual EXclusion 是 LinuxLinux 内核互斥锁mutex全解析结构定义、三条获取路径与锁/解锁 API 实现——linux-insides 同步原语系列Linux 内核互斥锁mutex全解析结构定义、三条获取路径与锁/解锁 API 实现——linux insides 同步原语系列 导读 本文是 linux文档教程操作系统Linux内核同步原语深入理解互斥锁(Mutex)Linux内核同步原语深入理解互斥锁 Mutex 前言 在多任务操作系统中同步原语是确保多个执行线程或进程能够正确共享资源的关键机制。Linux内核提供了多文档教程操作系统上一篇如何快速掌握开源免费的ModBus调试神器QModMaster完整实战指南下一篇docx 表格完全指南用 TypeScript 声明式 API 创建、合并与美化 Word 表格创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考