Linux 内核信号量(Semaphore)同步原语全解:从数据结构到 down/up 实现剖析
发布时间:2026/9/28 2:20:08来源:尧图网络
【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/hust-open-atom-club/linux-insides-zh点击查看免费下载导读本文是「Linux 内核揭秘」同步原语章节的第三部分对应仓库 SyncPrim/linux-sync-3.md聚焦内核同步原语中的信号量Semaphore。在前面两部分中我们已学习过自旋锁与排队自旋锁ticket spinlock本篇将从理论出发讲解信号量为何存在、其struct semaphore数据结构如何组织并逐行剖析down/up及__down_common等核心 API 的内核源码实现。读完本文你将能够说清信号量与自旋锁的适用场景差异理解TASK_INTERRUPTIBLE/TASK_KILLABLE等任务状态在等待队列中的流转以及schedule_timeout、wake_up_process在锁释放时的协作机制。为什么在自旋锁之外还需要信号量Linux 内核已经提供了自旋锁spinlock这种同步机制为什么还需要信号量答案在于两者设计理念的差异自旋锁被设计为仅在极短时间持有。持有自旋锁期间禁止进入睡眠因为其他等待者正在原地自旋同时为避免死锁上下文切换在持锁期间也是不允许的。当需要长时间持有锁时信号量 就是更好的解决方案反过来对于需要短期持锁的应用信号量又并非最优。像一般的同步原语一样信号量基于一个变量构建这个变量的值可以增大或减小其状态就代表了获取锁的能力。关键点在于这个变量的取值并不限于0和1由此衍生出两种信号量类型二值信号量值只能为1或0普通信号量值可以为任意非负数。当信号量的值大于1时它被称为计数信号量允许多于一个进程同时获取它。这种机制天然适合记录现有资源数量如空闲缓冲区个数而自旋锁一次只能为一个任务上锁。此外还有一个重要特性信号量允许进入睡眠状态。当某进程等待一个已被其他进程获取的锁时调度器 可以切入其他进程把 CPU 让给真正有事可做的任务这正是长临界区场景下的关键优势。信号量的数据结构struct semaphore所有信号量相关的 API 的组织脉络理解。内核用如下结构体表示信号量struct semaphore { raw_spinlock_t lock; unsigned int count; struct list_head wait_list; };结构体由三部分组成lock保护信号量自身的自旋锁raw_spinlock_t类型与第一部分讲述的 raw spinlock 一脉相承参考 SyncPrim/linux-sync-1.mdcount当前可用资源的数量wait_list等待获取该锁的进程序列内核侵入式双向链表见 DataStructures/linux-datastructures-1.md。可以看到信号量的资源计数 等待者队列结构使其既能表达资源池又能公平地按队列顺序唤醒等待者这与自旋锁单纯基于 ticket/原子变量的设计截然不同。信号量的初始化静态与动态两种方式在考察信号量 API 之前必须先知道如何初始化信号量。Linux 内核提供了两种初始化途径静态初始化与动态初始化。静态初始化DEFINE_SEMAPHORE使用DEFINE_SEMAPHORE宏可以在编译期静态初始化一个信号量#define DEFINE_SEMAPHORE(name) \ struct semaphore name __SEMAPHORE_INITIALIZER(name, 1)注意DEFINE_SEMAPHORE只提供二值信号量计数固定为1的初始化。它展开为一个信号量结构体定义并通过__SEMAPHORE_INITIALIZER宏完成各字段初始化#define __SEMAPHORE_INITIALIZER(name, n) \ { \ .lock __RAW_SPIN_LOCK_UNLOCKED((name).lock), \ .count n, \ .wait_list LIST_HEAD_INIT((name).wait_list), \ }该宏传入信号量结构体的名字并逐域初始化.lock使用__RAW_SPIN_LOCK_UNLOCKED宏初始化为未锁定状态。正如第一部分所讲该宏定义于include/linux/spinlock_types.h最终展开为__ARCH_SPIN_LOCK_UNLOCKED——即零值/无锁状态#define __ARCH_SPIN_LOCK_UNLOCKED { { 0 } }.count初始化为传入的资源数n.wait_list通过LIST_HEAD_INIT初始化为空链表等待者队列从空开始。动态初始化sema_init第二种方式是把信号量和期望的资源数目传给sema_init函数该函数同样定义在include/linux/semaphore.hstatic inline void sema_init(struct semaphore *sem, int val) { static struct lock_class_key __key; *sem (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val); lockdep_init_map(sem-lock.dep_map, semaphore-lock, __key, 0); }实现非常直白复用__SEMAPHORE_INITIALIZER宏整体初始化传入的信号量然后调用lockdep_init_map为锁注册 lockdep 锁验证 所需的类键dep_map。与前几部分一致本文不深入 lockdep 验证器的细节只需知道CONFIG_DEBUG_LOCK_ALLOC等配置项开启时它会介入记录锁依赖。信号量 API 总览Linux 内核为信号量提供了如下操作接口void down(struct semaphore *sem); void up(struct semaphore *sem); int down_interruptible(struct semaphore *sem); int down_killable(struct semaphore *sem); int down_trylock(struct semaphore *sem); int down_timeout(struct semaphore *sem, long jiffies);各接口语义如下接口行为down获取信号量若count 0则计数减一并成功返回否则让当前任务进入不可中断睡眠等待up释放信号量若等待队列非空则唤醒队首等待者否则countdown_interruptible尝试获取获取失败时以TASK_INTERRUPTIBLE状态睡眠可被信号唤醒返回-EINTRdown_killable与down_interruptible类似但置TASK_KILLABLE标志仅可被杀死类信号中断down_trylock类似spin_trylock尝试获取失败立即返回而不等待down_timeout尝试获取以传入的jiffies为上限等待超时后以TASK_UNINTERRUPTIBLE中断进入等待并返回-ETIME关于任务状态标志down_interruptible若成功获取计数减少且锁被获取同时当前任务被置为TASK_INTERRUPTIBLE受阻状态——该标志表示进程可以通过信号被唤醒例如用户按下 CtrlC 发送 SIGINT从而退出等待。down_killable将TASK_KILLABLE标志置位表示等待进程只能被杀死信号如SIGKILL中断不会被普通信号打扰常用于那些需要可被杀但不必响应普通信号的内核路径。down_timeout的等待时间以 jiffies内核时钟滴答为单位计量。down 与 up获取与释放的实现down 函数down定义在kernel/locking/semaphore.c源文件中void down(struct semaphore *sem) { unsigned long flags; raw_spin_lock_irqsave(sem-lock, flags); if (likely(sem-count 0)) sem-count--; else __down(sem); raw_spin_unlock_irqrestore(sem-lock, flags); } EXPORT_SYMBOL(down);函数开头的flags变量会传给raw_spin_lock_irqsave与raw_spin_unlock_irqrestore宏定义于include/linux/spinlock.h用来保护当前信号量的计数器。这两个宏与spin_lock/spin_unlock作用相似但会保存/恢复当前中断标志并额外禁止中断——保证 count 的读改写是原子的且持锁期间不会被本地中断打断。down的核心逻辑很简单将count与零比较——若count 0说明有可用资源执行count--即成功获取锁若count 0所有资源都已被占用需要等待于是调用__down。__down与down定义在同一个源文件kernel/locking/semaphore.c中static noinline void __sched __down(struct semaphore *sem) { __down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT); }__down只是__down_common的薄封装传入三个参数semaphore目标信号量flag当前任务的等待状态标志timeout最长等待信号量的时间。值得注意的是所有基于睡眠等待的下沉操作最终都汇聚到__down_commonstatic noinline int __sched __down_interruptible(struct semaphore *sem) { return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT); }static noinline int __sched __down_killable(struct semaphore *sem) { return __down_common(sem, TASK_KILLABLE, MAX_SCHEDULE_TIMEOUT); }static noinline int __sched __down_timeout(struct semaphore *sem, long timeout) { return __down_common(sem, TASK_UNINTERRUPTIBLE, timeout); }也就是说down_interruptible、down_killable、down_timeout只是通过传入不同的任务状态标志与超时值来改变等待行为调度与唤醒的骨架完全共用。__down_common核心等待循环__down_common同样定义在kernel/locking/semaphore.c以两个本地变量开场struct task_struct *task current; struct semaphore_waiter waiter;task表示当前想获取锁的任务。current宏定义于arch/x86/include/asm/current.h#define current get_current()get_current返回current_task这个 per-cpu 变量的值DECLARE_PER_CPU(struct task_struct *, current_task); static __always_inline struct task_struct *get_current(void) { return this_cpu_read_stable(current_task); }per-cpu 变量机制详见 Concepts/linux-cpu-1.md。waiter表示semaphore.wait_list列表中的一个入口节点struct semaphore_waiter { struct list_head list; struct task_struct *task; bool up; };该结构包含三部分链表节点list用于挂入 wait_list、等待的任务指针task以及up标志——当持有者释放锁唤醒等待者时置位作为锁已到手的唤醒信号。接下来把当前进程加入wait_list并填充waiter各域list_add_tail(waiter.list, sem-wait_list); waiter.task task; waiter.up false;注意使用list_add_tail从尾部入队等待者按到达顺序排队保证 FIFO 公平性。随后进入无限循环for (;;) { if (signal_pending_state(state, task)) goto interrupted; if (unlikely(timeout 0)) goto timed_out; __set_task_state(task, state); raw_spin_unlock_irq(sem-lock); timeout schedule_timeout(timeout); raw_spin_lock_irq(sem-lock); if (waiter.up) return 0; }由于此前waiter.up false只要up没有被持有者置为true任务就会一直在循环中打转。循环的每一步依次检查信号检查调用signal_pending_state判断当前任务是否处于 pending 状态即带TASK_INTERRUPTIBLE或TASK_WAKEKILL标志有信号则跳转interrupted标签。signal_pending_state定义于include/linux/sched.hstatic inline int signal_pending_state(long state, struct task_struct *p) { if (!(state (TASK_INTERRUPTIBLE | TASK_WAKEKILL))) return 0; if (!signal_pending(p)) return 0; return (state TASK_INTERRUPTIBLE) || __fatal_signal_pending(p); }逻辑分三步先检查state位掩码 是否含TASK_INTERRUPTIBLE或TASK_WAKEKILL位不含则返回 0不可中断等待不会被打断再检查任务是否有挂起信号没有则返回 0最后综合TASK_INTERRUPTIBLE位与致命信号状态给出结论。若判定有信号可中断跳到interrupted: list_del(waiter.list); return -EINTR;即把等待者从链表中删除返回-EINTR错误码调用方如系统调用据此得知等待被信号打断。超时检查if (unlikely(timeout 0)) goto timed_out;跳转目标timed_out: list_del(waiter.list); return -ETIME;与interrupted流程一致——从链表摘除自身但返回-ETIME错误码表示等待超时。睡眠与调度若既无信号、超时也未过期就把任务设置为传入的state__set_task_state(task, state);然后暂时释放信号量的自旋锁并调用schedule_timeoutraw_spin_unlock_irq(sem-lock); timeout schedule_timeout(timeout); raw_spin_lock_irq(sem-lock);schedule_timeout定义于kernel/time/timer.c它的作用是把当前任务休眠到设定的超时为止睡眠期间调度器可切入其他任务这正是信号量允许上下文切换的体现。被唤醒后重新获取sem-lock回到循环起点继续判断。成功路径若waiter.up已被持有者置位说明我们被唤醒且锁已交接返回0任务成功获得信号量。归纳__down_common的等待语义一个想获取已被占用锁的函数会在无限循环中挂起直至以下三件事之一发生——被信号中断仅对可中断变体、超时到期仅对down_timeout、或持有者释放锁并唤醒它。up 函数up与down定义在同一个源文件kernel/locking/semaphore.c负责释放锁void up(struct semaphore *sem) { unsigned long flags; raw_spin_lock_irqsave(sem-lock, flags); if (likely(list_empty(sem-wait_list))) sem-count; else __up(sem); raw_spin_unlock_irqrestore(sem-lock, flags); } EXPORT_SYMBOL(up);其骨架与down对称仅有两处不同增加计数若等待列表为空list_empty成立说明没有人在等锁直接count归还资源唤醒等待者若等待列表非空则调用__up把锁让渡给队列中第一个等待者static noinline void __sched __up(struct semaphore *sem) { struct semaphore_waiter *waiter list_first_entry(sem-wait_list, struct semaphore_waiter, list); list_del(waiter-list); waiter-up true; wake_up_process(waiter-task); }__up完成四件事list_first_entry取出等待队列的第一个semaphore_waiter队列按 FIFO 排序队首等待最久list_del将其从链表中删除置waiter-up true——这一操作让__down_common无限循环中的if (waiter.up) return 0;条件成立等待者得以退出循环拿到锁wake_up_process(waiter-task)唤醒该任务。wake_up_process来自kernel/sched/core.c是唤醒休眠任务的调度器入口。回看等待侧__down_common中的schedule_timeout让任务进入睡眠现在持有者释放锁必须调用wake_up_process把它从睡眠状态唤醒任务被重新放回运行队列随后在循环中看到waiter.up true完成获取流程。总结与延伸至此Linux 内核中信号量同步原语的核心实现已全部走通。回顾本部分SyncPrim/linux-sync-3.md与同步原语章节的整体脉络第一部分介绍了自旋锁 API 与 ticket spinlockSyncPrim/linux-sync-1.md第二部分介绍了队列自旋锁SyncPrim/linux-sync-2.md本部分介绍了信号量它适合长时间持有的锁因为等待者会睡眠让出 CPU代价则是引入上下文切换开销。信号量的核心设计可概括为count记录资源余量wait_list以 FIFO 组织睡眠等待者down在资源耗尽时挂起任务up在释放时唤醒队首——整个过程由__down_common统一调度通过任务状态标志与超时参数衍生出可中断、可杀死、带超时等变体。后续章节将介绍更严格的同步原语——互斥锁mutex。互斥锁与信号量的核心差异在于互斥锁同一时刻只能由一个进程持有且只有持有者能解锁其 API 实现还允许通过乐观自旋避免昂贵的上下文切换这将是下一篇的主题。相关阅读同步原语章节总览SyncPrim/README.md自旋锁与 ticket spinlockSyncPrim/linux-sync-1.md队列自旋锁MCS 锁实现SyncPrim/linux-sync-2.md互斥锁下一个主题SyncPrim/linux-sync-4.md读者/写者信号量SyncPrim/linux-sync-5.md顺序锁SyncPrim/linux-sync-6.md信号量依赖的内核双向链表DataStructures/linux-datastructures-1.mdjiffies 与内核时间管理Timers/linux-timers-1.mdper-cpu 变量current/current_task的基础Concepts/linux-cpu-1.md赞分享【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/hust-open-atom-club/linux-insides-zh点击查看免费下载相关推荐Linux内核同步原语信号量机制详解Linux内核同步原语信号量机制详解 前言 在Linux内核开发中处理并发访问是一个核心问题。作为内核同步原语系列文章的第三部分本文将深入探讨Linux内文档教程操作系统Linux内核同步原语信号量机制详解Linux内核同步原语信号量机制详解 Linux内核揭秘项目中的同步原语是保证多任务并发安全的核心机制而信号量作为其中重要的一员在资源管理和进程同步中发挥文档教程操作系统Deepseek Coder 1.3b Instruct模型故障恢复机制高可用推理服务Deepseek Coder 1.3b Instruct模型故障恢复机制高可用推理服务 Deepseek Coder 1.3b Instruct作为开源代码生上一篇SpiffWorkflow部署指南从开发环境到生产环境的完整流程下一篇Linkerd服务网格终极配置指南如何快速优化API性能与可靠性创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网