ARTICLE DETAIL

资讯详情

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

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

Linux 内核互斥锁(mutex)全解析:结构定义、三条获取路径与锁/解锁 API 实现——linux-insides 同步原语系列 文档教程操作系统【免费下载链接】linux-insidesA book-in-progress about the Linux kernel and its insides.项目地址https://gitcode.com/gh_mirrors/li/linux-insides点击查看免费下载导读本文是 linux-insides 开源书籍当前仓库 SyncPrim 章节中关于同步原语的第四部分聚焦内核中最常用的互斥锁mutexMUTual 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.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 };各字段的含义如下字段类型作用countatomic_tmutex 状态1表示未锁定unlocked0表示已锁定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.hstruct 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.cvoid __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接收三个参数lockmutex 本身namemutex 名称供调试使用由宏中的#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_lockvoid __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 直接成功未进入慢路径后设置 ownerstatic 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.mdasm_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做原子减 1jns %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原子交换为-1atomic_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_KILLABLEtrylock不等待、立即返回。此处不再重复展开。mutex_unlock解锁的快速路径与慢路径mutex_unlock的完整实现void __sched mutex_unlock(struct mutex *lock) { __mutex_fastpath_unlock(lock-count, __mutex_unlock_slowpath); }fastpath 解锁__mutex_fastpath_unlockarch/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.mdspinlock 基础、linux-sync-2.mdqueued 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两条路径加锁层fastpathlock; decljns的 asm goto 原子路径、midpath基于 MCS 锁osq的乐观自旋、slowpathatomic_xchg_acquire重试 加入wait_listschedule_preempt_disabled睡眠三条路径的完整流转解锁层fastpathlock; incljg与 slowpath取队首mutex_waiter并wake_up_process。掌握这套机制你就理解了内核中大量mutex_lock/mutex_unlock保护临界区代码如时钟源注册、文件系统、驱动等子系统背后的真实执行路径也为阅读下一章读写信号量打下了结构基础。赞分享文档教程操作系统【免费下载链接】linux-insidesA book-in-progress about the Linux kernel and its insides.项目地址https://gitcode.com/gh_mirrors/li/linux-insides点击查看免费下载相关推荐Linux 内核揭秘互斥锁mutex同步原语的实现与三条获取路径深度解析Linux 内核揭秘互斥锁mutex同步原语的实现与三条获取路径深度解析 互斥锁 mutex 即 MUTual EXclusion 是 LinuxLinux内核同步原语深入理解互斥锁(Mutex)Linux内核同步原语深入理解互斥锁 Mutex 前言 在多任务操作系统中同步原语是确保多个执行线程或进程能够正确共享资源的关键机制。Linux内核提供了多文档教程操作系统自旋锁、互斥锁到顺序锁Linux 内核揭秘linux-insides-zh6 大同步原语完全指南自旋锁、互斥锁到顺序锁Linux 内核揭秘linux insides zh6 大同步原语完全指南 在多核处理器上多个 CPU 同时读写同一份数据轻则数上一篇5分钟上手使用ens-contracts构建自定义域名解析器的完整教程下一篇如何在3分钟内从国家中小学智慧教育平台下载电子课本PDF新手完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表