【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/hust-open-atom-club/linux-insides-zh点击查看免费下载导读本文是《Linux 内核揭秘》中同步原语章节的第一部分聚焦 Linux 内核中最基础、使用最广泛的同步机制——自旋锁spinlock。文章从kernel/time/clocksource.c中真实加锁场景切入逐层剖析spinlock_t、raw_spinlock、arch_spinlock的类型定义完整跟踪spin_lock_init初始化、spin_lock获取、spin_unlock释放的底层调用链并深入讲解 x86_64 上票据自旋锁ticket spinlock基于head/tail票号的公平排队原理。读完本文你将掌握自旋锁在内核中的真实代码形态、如何正确初始化与使用它以及它为什么需要配合关闭抢占、关闭中断等约束才能保证正确性。1. 从同步原语说起1.1 什么是同步原语同步原语synchronization primitive是一种软件机制它允许两个或多个并行进程或线程在不同时刻执行同一段代码即进入所谓的临界区critical section而不会相互破坏。Linux 内核是一个高度并行的系统多个 CPU 上同时运行着大量进程、软中断与硬中断处理程序它们可能同时访问同一个全局数据结构。若没有同步机制保护就会产生竞态条件race condition——多个执行流交错写入同一数据导致结果不可预期。Linux 内核提供了多种同步原语以满足不同场景的需求mutex互斥锁可睡眠的互斥机制适合长时间持有semaphore信号量计数型同步机制允许有限多个进程进入临界区seqlock顺序锁读者无锁、写者独占的机制避免写者饥饿atomic operations原子操作对单个变量的无锁读写spinlock自旋锁低延迟、不可睡眠的短临界区保护机制即本文主角。1.2 一个真实的加锁场景注册时钟源在深入自旋锁之前先看一个真实的使用场景。下面的代码出自 kernel/time/clocksource.c一节中有详细介绍它负责把给定的时钟源注册进时钟源列表mutex_lock(clocksource_mutex); ... ... ... clocksource_enqueue(cs); clocksource_enqueue_watchdog(cs); clocksource_select(); ... ... ... mutex_unlock(clocksource_mutex);这段代码对clocksource_list注册列表执行了两个不同的操作clocksource_enqueue将给定时钟源插入到按rating等级即频率高低排序的注册列表clocksource_list中clocksource_enqueue_watchdog则根据时钟源标志将其插入看门狗列表。这两行操作被mutex_lock/mutex_unlock包围锁对象是全局的clocksource_mutex——这正是基于互斥锁的加锁/解锁示例目的是阻止两个或两个以上线程并发操作clocksource_list。为什么必须加锁看clocksource_enqueue的实现static void clocksource_enqueue(struct clocksource *cs) { struct list_head *entry clocksource_list; struct clocksource *tmp; list_for_each_entry(tmp, clocksource_list, list) if (tmp-rating cs-rating) entry tmp-list; list_add(cs-list, entry); }如果两个并行处理同时执行该函数它们可能找到相同的插入点entry随后两个list_add相互覆盖破坏链表结构——这就是典型的竞态条件。需要说明的是clocksource注册本身对延迟要求不高因此这里选用可睡眠的mutex而内核中大量对延迟极其敏感、临界区极短的场合则必须使用自旋锁。2. 自旋锁的理论基础自旋锁spinlock是一种低级同步机制其核心是一个只有两种状态的变量acquired已获取/上锁released已释放/解锁每个想要获取自旋锁的执行流必须原子地为这个变量写入已获取状态当它完成临界区工作后再写入已释放状态。若一个执行流试图进入被自旋锁保护的代码而锁正被其他执行流持有它将原地自旋等待不断轮询锁状态占用 CPU 但不睡眠直到持有者释放锁。正因为等待者不睡眠、不切换上下文自旋锁的获取与释放延迟极低但同时意味着临界区必须足够短——长时间持有自旋锁会让其他 CPU 空转浪费。为了保证状态转换的正确性所有相关操作必须是原子的atomic否则两个执行流同时写入锁变量依然会引发竞态。3.spinlock_t自旋锁的类型金字塔自旋锁在内核中以spinlock_t类型表示该类型被广泛使用。它的定义位于 include/linux/spinlock_types.htypedef struct spinlock { union { struct raw_spinlock rlock; #ifdef CONFIG_DEBUG_LOCK_ALLOC # define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map)) struct { u8 __padding[LOCK_PADSIZE]; struct lockdep_map dep_map; }; #endif }; } spinlock_t;可以看到spinlock_t的实现依赖内核配置选项CONFIG_DEBUG_LOCK_ALLOC的状态。这是一个锁调试选项启用时联合体中额外嵌入一个lockdep_map字段用于内核的锁验证器lock validator / lockdep检测死锁、循环依赖等锁使用错误。锁调试相关内容将在本章节后续部分专门讨论这里先忽略它。当CONFIG_DEBUG_LOCK_ALLOC未启用时spinlock_t简化为一个只含raw_spinlock的联合体typedef struct spinlock { union { struct raw_spinlock rlock; }; } spinlock_t;raw_spinlock原始自旋锁即普通自旋锁同样定义在 include/linux/spinlock_types.h 中typedef struct raw_spinlock { arch_spinlock_t raw_lock; #ifdef CONFIG_GENERIC_LOCKBREAK unsigned int break_lock; #endif } raw_spinlock_t;这里出现了两个新概念arch_spinlock_t——体系结构相关的自旋锁实现break_lock——在对称多处理SMP系统中当某个处理器等待的锁被另一个处理器持有时该字段被置为1用于支持锁中断lock break防止长时间等待导致不公平。本书以x86_64架构为例因此arch_spinlock_t定义于 arch/x86/include/asm/spinlock_types.h#ifdef CONFIG_QUEUED_SPINLOCKS #include asm-generic/qspinlock_types.h #else typedef struct arch_spinlock { union { __ticketpair_t head_tail; struct __raw_tickets { __ticket_t head, tail; } tickets; }; } arch_spinlock_t;arch_spinlock的结构同样由内核配置选项决定。这里涉及内核提供的第二种自旋锁——队列自旋锁queued spinlock由CONFIG_QUEUED_SPINLOCKS选项控制。当该选项启用时arch_spinlock_t由 include/asm-generic/qspinlock_types.h 中的qspinlock表示typedef struct qspinlock { atomic_t val; } arch_spinlock_t;队列自旋锁用单个atomic_t值结合队列与 MCS 锁思想替代简单的已获取/已释放原子标志使每个等待者在自己的本地per-cpu变量上自旋从而避免所有 CPU 争抢同一个缓存行。关于队列自旋锁的完整实现将在本系列的第二部分队列自旋锁中深入讲解。先把类型金字塔总结如下类型作用层次定义位置spinlock_t通用 API 层调试时可嵌入dep_mapinclude/linux/spinlock_types.hraw_spinlock_t原始自旋锁含arch_spinlock_t raw_lock与可选break_lockinclude/linux/spinlock_types.harch_spinlock_t体系结构相关实现x86_64ticket 或 qspinlockarch/x86/include/asm/spinlock_types.hqspinlock队列自旋锁内部表示CONFIG_QUEUED_SPINLOCKS启用时include/asm-generic/qspinlock_types.h4. 自旋锁 API 全景在深入具体实现之前先概览 Linux 内核为自旋锁提供的主要操作全部定义于 include/linux/spinlock.hspin_lock_init——初始化给定的自旋锁spin_lock——获取给定的自旋锁spin_lock_bh——禁止软件中断softirq/bottom half并获取给定的自旋锁spin_lock_irqsave与spin_lock_irq——禁止本地处理器上的硬件中断前者保存之前的中断状态标志flag后者不保存spin_unlock——释放给定的自旋锁spin_unlock_bh——释放给定的自旋锁并重新启用软件中断spin_unlock_irqrestore/spin_unlock_irq——释放锁并恢复/启用硬件中断spin_is_locked——返回给定自旋锁的状态是否已被持有spin_trylock——尝试获取锁失败则立即返回而不自旋。其中_bh、_irq、_irqsave系列变体的存在说明自旋锁的使用必须与中断上下文协调——如果临界区可能被中断处理程序访问持有锁期间必须屏蔽中断否则中断处理程序会与当前执行流死锁。5. 初始化自旋锁spin_lock_init的完整展开5.1 宏定义与两步操作spin_lock_init宏同样定义于 include/linux/spinlock.h#define spin_lock_init(_lock) \ do { \ spinlock_check(_lock); \ raw_spin_lock_init((_lock)-rlock); \ } while (0)它接收一个spinlock_t *参数执行两步操作spinlock_check(_lock)——检查给定自旋锁raw_spin_lock_init((_lock)-rlock)——初始化其内部的原始自旋锁。5.2spinlock_check取回原始锁spinlock_check的实现极其简单它仅仅返回给定自旋锁内部的raw_spinlock_t指针用于确保后续操作精确作用于普通原生自旋锁而非调试包裹层static __always_inline raw_spinlock_t *spinlock_check(spinlock_t *lock) { return lock-rlock; }5.3raw_spin_lock_init赋值为未锁定状态raw_spin_lock_init宏把__RAW_SPIN_LOCK_UNLOCKED的值赋给给定的raw_spinlock_t# define raw_spin_lock_init(lock) \ do { \ *(lock) __RAW_SPIN_LOCK_UNLOCKED(lock); \ } while (0)从名字可知__RAW_SPIN_LOCK_UNLOCKED负责将锁初始化为**已释放released**状态。它定义于 include/linux/spinlock_types.h并继续展开为#define __RAW_SPIN_LOCK_UNLOCKED(lockname) \ (raw_spinlock_t) __RAW_SPIN_LOCK_INITIALIZER(lockname) #define __RAW_SPIN_LOCK_INITIALIZER(lockname) \ { \ .raw_lock __ARCH_SPIN_LOCK_UNLOCKED, \ SPIN_DEBUG_INIT(lockname) \ SPIN_DEP_MAP_INIT(lockname) \ }忽略调试相关的SPIN_DEBUG_INIT与SPIN_DEP_MAP_INIT宏之后展开结果就是*((_lock)-rlock) __ARCH_SPIN_LOCK_UNLOCKED;而__ARCH_SPIN_LOCK_UNLOCKED依据自旋锁类型有两种形态普通票据自旋锁ticket#define __ARCH_SPIN_LOCK_UNLOCKED { { 0 } }队列自旋锁CONFIG_QUEUED_SPINLOCKS启用时针对 x86_64#define __ARCH_SPIN_LOCK_UNLOCKED { ATOMIC_INIT(0) }于是spin_lock_init展开完成后给定的自旋锁被初始化并处于**解锁unlocked**状态。6. 获取自旋锁spin_lock的调用链6.1 入口spin_lock→raw_spin_lock自旋锁 API 的入口是内联函数static __always_inline void spin_lock(spinlock_t *lock) { raw_spin_lock(lock-rlock); }它把spinlock_t剥壳为raw_spinlock_t后调用raw_spin_lock宏#define raw_spin_lock(lock) _raw_spin_lock(lock)6.2 按CONFIG_SMP选择实现_raw_spin_lock的具体定义取决于内核是否启用了 SMP 支持include/linux/spinlock.h#if defined(CONFIG_SMP) || defined(CONFIG_DEBUG_SPINLOCK) # include linux/spinlock_api_smp.h #else # include linux/spinlock_api_up.h #endif启用 SMP 或调试自旋锁CONFIG_DEBUG_SPINLOCK时使用 SMP 版 API否则单处理器 UP 环境使用 UP 版 API——单 CPU 上不存在真正的并行锁操作退化为抢占与中断管理。在 SMP 且未启用队列自旋锁的 x86_64 上_raw_spin_lock定义于 arch/x86/include/asm/spinlock.h#define _raw_spin_lock(lock) __raw_spin_lock(lock)6.3__raw_spin_lock关闭抢占 锁依赖登记 争用处理static inline void __raw_spin_lock(raw_spinlock_t *lock) { preempt_disable(); spin_acquire(lock-dep_map, 0, 0, _RET_IP_); LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); }这里依次做了三件事preempt_disable()关闭抢占持有自旋锁期间当前任务不允许被更高优先级的任务抢占否则临界区被挂起其他 CPU 上等待该锁的执行流会无限自旋。preempt_disable定义于 include/linux/preempt.h中start_kernel调用preempt_disable的讲解。相应地释放锁时__raw_spin_unlock会调用preempt_enable()重新启用抢占static inline void __raw_spin_unlock(raw_spinlock_t *lock) { ... ... ... preempt_enable(); }spin_acquire登记锁依赖通过宏链展开调用lock_acquire_exclusive再到lock_acquire定义于 kernel/locking/lockdep.c向 lockdep 锁验证器登记独占性获取事件用于运行时检测死锁与锁序问题#define spin_acquire(l, s, t, i) lock_acquire_exclusive(l, s, t, NULL, i) #define lock_acquire_exclusive(l, s, t, n, i) lock_acquire(l, s, t, 0, 1, n, i)lock_acquire的关键行为是用raw_local_irq_save(flags)临时关闭本地硬件中断再执行登记因为锁可能正被中断上下文获取最后用raw_local_irq_restore(flags)恢复中断void lock_acquire(struct lockdep_map *lock, unsigned int subclass, int trylock, int read, int check, struct lockdep_map *nest_lock, unsigned long ip) { unsigned long flags; if (unlikely(current-lockdep_recursion)) return; raw_local_irq_save(flags); check_flags(flags); current-lockdep_recursion 1; trace_lock_acquire(lock, subclass, trylock, read, check, nest_lock, ip); __lock_acquire(lock, subclass, trylock, read, check, irqs_disabled_flags(flags), nest_lock, ip, 0, 0); current-lockdep_recursion 0; raw_local_irq_restore(flags); }真正的锁依赖分析在__lock_acquire函数中完成该函数体量很大与 lockdep 锁验证器密切相关不属于本部分主题。LOCK_CONTENDED执行实际的体系结构加锁LOCK_CONTENDED宏定义于 include/linux/lockdep.h只是用给定自旋锁调用已知函数#define LOCK_CONTENDED(_lock, try, lock) \ lock(_lock)在本例中lock是do_raw_spin_lock_lock是给定的raw_spinlock_t。do_raw_spin_lock定义于 include/linux/spinlock.hstatic inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock) { __acquire(lock); arch_spin_lock(lock-raw_lock); }其中__acquire是给 Sparse 静态检查工具用的标注宏运行时不产生代码。6.4arch_spin_lock进入体系结构层至此调用链已到达体系结构相关层。arch_spin_lock的定义位置取决于两件事体系架构以及是否使用队列自旋锁。使用队列自旋锁时它映射为 include/asm-generic/qspinlock.h 中的queued_spin_lock#define arch_spin_lock(l) queued_spin_lock(l)使用普通票据自旋锁时arch_spin_lock定义于 arch/x86/include/asm/spinlock.h。队列自旋锁留待第二部分专门讲解本部分聚焦普通票据自旋锁。7. 票据自旋锁ticket spinlock的加解锁原理7.1 结构与直觉再次回顾普通arch_spinlock的结构typedef struct arch_spinlock { union { __ticketpair_t head_tail; struct __raw_tickets { __ticket_t head, tail; } tickets; }; } arch_spinlock_t;这种自旋锁变体被称为票据自旋锁ticket spinlock它不再用单一已获取/已释放标志而是维护两个票号head头部当前持有锁的执行流所对应的票号tail尾部最后一个尝试获取锁的执行流所对应的票号即发号位置。每个新来者把自己登记为新的tail票号并自旋等待直到head追上自己。这保证了解锁后按申请顺序FIFO依次放行天然公平不会出现后来者反复抢锁而先到者饿死的情况——这正是对朴素 test-and-set 自旋锁公平性缺陷的改进朴素实现的不足在队列自旋锁部分有详细对比。7.2arch_spin_lock的实现static __always_inline void arch_spin_lock(arch_spinlock_t *lock) { register struct __raw_tickets inc { .tail TICKET_LOCK_INC }; inc xadd(lock-tickets, inc); if (likely(inc.head inc.tail)) goto out; for (;;) { unsigned count SPIN_THRESHOLD; do { inc.head READ_ONCE(lock-tickets.head); if (__tickets_equal(inc.head, inc.tail)) goto clear_slowpath; cpu_relax(); } while (--count); __ticket_lock_spinning(lock, inc.tail); } clear_slowpath: __ticket_check_and_clear_slowpath(lock, inc.head); out: barrier(); }逐段解读取票先用TICKET_LOCK_INC值为1初始化一个__raw_tickets结构作为增量#define __TICKET_LOCK_INC 1然后对lock-tickets执行 xaddexchange-and-add原子操作。该操作同时完成两件事把tickets的旧值含head与tail原子地取回存入inc并把tickets.tail增加1——尾部 1即代表又有一个执行流登记申请锁。快速路径检查取回的inc.head与inc.tail是否相等。如果相等说明当前没有其他执行流持有锁自己是队列里唯一的票直接跳到out标签。慢速路径自旋等待如果锁正被前一个执行流持有则tail比head大1自己排在持有者之后进入循环每轮用READ_ONCE读取lock-tickets.head与自己的tail比较相等意味着前面的执行流已释放锁、轮到自己了。不等则执行#define cpu_relax() asm volatile(rep; nop)即发射一条 NOPrep; nop又称 pause 指令让出流水线、降低功耗与总线争用然后进入下一轮迭代。若连续自旋超过SPIN_THRESHOLD阈值还会调用__ticket_lock_spinning进入慢路径提示机制用于 MWAIT 等节能与调试统计场景。收尾屏障barrier()是编译器屏障指令保证编译器不会重排临界区内存操作与锁状态变化的顺序。关于内存屏障更系统的知识可阅读内核文档 memory-barriers.txt。7.3arch_spin_unlock的实现与加锁对称释放锁时spin_unlock一路走到arch_spin_unlock其核心是递增头部票号把锁交给队列中的下一位__add(lock-tickets.head, TICKET_LOCK_INC, UNLOCK_LOCK_PREFIX);UNLOCK_LOCK_PREFIX是释放场景所需的 lock 前缀x86 的xadd/lock指令族保证递增操作的原子性。7.4 head/tail 队列图示把spin_lock与spin_unlock组合起来看head保存当前持有锁的执行流的票号tail保存最后登记申请锁的执行流的票号中间则排着等待队列------- ------- | | | | head | 7 | - - - | 7 | tail | | | | ------- ------- | ------- | | | 8 | | | ------- | ------- | | | 9 | | | -------当前持有者票号为 78 号正在自旋等待 7 号释放9 号又排在其后。每次解锁后head递增 1把锁移交给下一号从而保证严格的 FIFO 公平。8. 总结与后续路线本文作为Linux 内核中的同步原语章节的第一部分完成了以下探索从 kernel/time/clocksource.c梳理了spinlock_t→raw_spinlock_t→arch_spinlock_t的类型金字塔以及CONFIG_DEBUG_LOCK_ALLOC、CONFIG_GENERIC_LOCKBREAK、CONFIG_QUEUED_SPINLOCKS、CONFIG_SMP各配置选项对锁形态的影响完整跟踪了spin_lock_init的初始化调用链确认锁初始化后处于已释放状态完整跟踪了spin_lock的获取调用链preempt_disable关抢占 →spin_acquire登记 lockdep →LOCK_CONTENDED→do_raw_spin_lock→arch_spin_lock以及对应的preempt_enable恢复深入讲解了 x86_64 票据自旋锁的xadd取票、head/tail比较、cpu_relax自旋与barrier屏障语义。本部分并未穷尽所有自旋锁 API但其核心思想——在极短临界区内以自旋等待换取最低延迟、配合关闭抢占/中断保证互斥——已经清晰。下一部分将继续深入这个主题介绍自旋锁的另一形态队列自旋锁queued spinlock随后章节还将依次讲解信号量、互斥锁、读者/写者信号量与顺序锁。整个同步原语章节的导读与目录见 SyncPrim/README.md。赞分享【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/hust-open-atom-club/linux-insides-zh点击查看免费下载相关推荐Linux内核同步原语详解自旋锁机制剖析Linux内核同步原语详解自旋锁机制剖析 引言为什么需要自旋锁 在多核处理器SMP系统中多个CPU核心可能同时访问共享资源这就产生了 竞态条件R文档教程操作系统Linux 内核同步原语解析队列自旋锁实现原理Linux 内核同步原语解析队列自旋锁实现原理 引言为什么需要队列自旋锁 在现代多核处理器系统中同步原语Synchronization Primiti文档教程操作系统Linux内核同步原语详解自旋锁机制Linux内核同步原语详解自旋锁机制 引言为什么需要自旋锁 在现代多核处理器系统中并发编程已成为常态而非例外。当多个CPU核心同时访问共享资源时如果没文档教程操作系统上一篇Captura折叠面板可访问性键盘导航与屏幕阅读器支持下一篇WeChatTweak-macOS性能监控与多开稳定性优化创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考