Linux 内核锁机制从自旋锁到 RCU 的并发控制演进一、并发控制的锁焦虑选择错误的锁等于选择错误的性能Linux 内核中锁的选择直接影响系统吞吐和延迟。某数据库系统在高并发场景下使用自旋锁保护共享链表CPU 利用率飙升至 95% 但有效吞吐仅 12%——大量 CPU 时间消耗在自旋等待上。将自旋锁替换为互斥锁后CPU 利用率降至 45%吞吐提升 3 倍。但在另一个中断处理场景中互斥锁导致睡眠而触发死锁必须使用自旋锁。锁的选择不是哪个更好而是哪个场景用哪个。Linux 内核提供了从轻量到重量的多种锁机制理解它们的底层实现和适用场景是内核开发的必备能力。二、Linux 内核锁机制的层级与适用场景flowchart TB subgraph 锁层级[锁机制层级从轻到重] direction TB L1[原子操作br/atomic_tbr/无锁单条指令br/适用计数器、标志位] L2[自旋锁br/spinlock_tbr/忙等不睡眠br/适用中断上下文、短临界区] L3[互斥锁br/mutexbr/可睡眠开销较大br/适用长临界区、可睡眠上下文] L4[读写锁br/rwlock_t / rw_semaphorebr/读共享、写独占br/适用读多写少] L5[RCUbr/Read-Copy-Updatebr/读无锁写延迟回收br/适用读远多于写] end L1 --|竞争加剧| L2 L2 --|临界区变长| L3 L3 --|读多写少| L4 L4 --|读远多于写| L5 style 锁层级 fill:#eef,stroke:#333三、内核锁机制的代码实现与分析#include linux/spinlock.h #include linux/mutex.h #include linux/rwlock.h #include linux/rwsem.h #include linux/rcupdate.h #include linux/atomic.h #include linux/slab.h #include linux/module.h #include linux/interrupt.h /* 1. 原子操作 */ /* * 原子操作是最轻量的并发控制基于 CPU 的原子指令 * 适用于简单的计数器和标志位操作 */ struct ref_counter { atomic_t count; }; /* 原子递增并返回新值 */ static inline int ref_counter_inc(struct ref_counter *rc) { return atomic_inc_return(rc-count); } /* 原子递减并测试是否归零 */ static inline int ref_counter_dec_and_test(struct ref_counter *rc) { return atomic_dec_and_test(rc-count); } /* 2. 自旋锁 */ /* * 自旋锁在等待时忙等busy-wait不会让出 CPU * 适用于中断上下文、临界区极短 微秒级的场景 * 禁忌持有自旋锁时不能睡眠不能调用可能调度的函数 */ struct spinlock_protected_list { spinlock_t lock; struct list_head head; int count; }; /* 初始化 */ static void spl_init(struct spinlock_protected_list *spl) { spin_lock_init(spl-lock); INIT_LIST_HEAD(spl-head); spl-count 0; } /* 在中断处理中安全地添加节点 */ static void spl_add_irqsafe(struct spinlock_protected_list *spl, struct list_head *node) { unsigned long flags; /* * spin_lock_irqsave保存中断状态并禁用本地中断 * 防止中断处理程序在同一 CPU 上死锁 */ spin_lock_irqsave(spl-lock, flags); list_add(node, spl-head); spl-count; spin_unlock_irqrestore(spl-lock, flags); } /* 普通上下文中的添加 */ static void spl_add(struct spinlock_protected_list *spl, struct list_head *node) { spin_lock(spl-lock); list_add(node, spl-head); spl-count; spin_unlock(spl-lock); } /* 3. 互斥锁 */ /* * 互斥锁在竞争时让出 CPU等待者进入睡眠 * 适用于临界区较长、可以在进程上下文中睡眠的场景 * 开销加锁/解锁涉及上下文切换约 1-10 微秒 */ struct mutex_protected_buffer { struct mutex lock; void *data; size_t size; size_t used; }; static int mpb_init(struct mutex_protected_buffer *mpb, size_t size) { mutex_init(mpb-lock); mpb-data kzalloc(size, GFP_KERNEL); if (!mpb-data) return -ENOMEM; mpb-size size; mpb-used 0; return 0; } /* 写入数据可能阻塞 */ static ssize_t mpb_write(struct mutex_protected_buffer *mpb, const void *buf, size_t len) { ssize_t written; /* * mutex_lock可能睡眠不能在中断上下文使用 * 适合进程上下文中的长临界区 */ mutex_lock(mpb-lock); if (mpb-used len mpb-size) { written -ENOSPC; goto out; } memcpy(mpb-data mpb-used, buf, len); mpb-used len; written len; out: mutex_unlock(mpb-lock); return written; } /* 4. 读写信号量 */ /* * 读写信号量读操作共享写操作独占 * 适用于读多写少的数据结构如配置表、路由表 * rw_semaphore 比 rwlock_t 更重但允许读操作睡眠 */ struct rwsem_config_table { struct rw_semaphore rwsem; struct hlist_head *buckets; int bucket_count; }; static void rct_init(struct rwsem_config_table *rct, int buckets) { init_rwsem(rct-rwsem); rct-buckets kzalloc(sizeof(struct hlist_head) * buckets, GFP_KERNEL); rct-bucket_count buckets; } /* 读操作多个读者可并发 */ static void *rct_lookup(struct rwsem_config_table *rct, int key) { void *result NULL; down_read(rct-rwsem); /* 获取读锁 */ /* 查找逻辑 */ result NULL; /* 实际查找省略 */ up_read(rct-rwsem); /* 释放读锁 */ return result; } /* 写操作独占访问 */ static void rct_update(struct rwsem_config_table *rct, int key, void *value) { down_write(rct-rwsem); /* 获取写锁等待所有读者退出 */ /* 更新逻辑 */ up_write(rct-rwsem); /* 释放写锁 */ } /* 5. RCURead-Copy-Update */ /* * RCU 是 Linux 内核最精妙的并发控制机制 * 核心思想读者无锁访问旧数据写者创建副本修改宽限期后回收旧数据 * 适用于读远多于写的场景如路由表、进程描述符 * 优势读操作零开销写操作延迟回收 */ struct rcu_protected_entry { int key; int value; struct rcu_head rcu; /* RCU 回收头 */ struct list_head node; }; struct rcu_protected_list { struct list_head head; spinlock_t update_lock; /* 写者之间的互斥 */ }; /* RCU 读操作无锁零开销 */ static struct rcu_protected_entry * rpl_lookup(struct rcu_protected_list *rpl, int key) { struct rcu_protected_entry *entry; /* * rcu_read_lock / rcu_read_unlock * 标记读侧临界区禁止此期间的宽限期结束 * 实际开销极低仅禁用抢占或递增计数器 */ rcu_read_lock(); list_for_each_entry_rcu(entry, rpl-head, node) { if (entry-key key) { rcu_read_unlock(); return entry; } } rcu_read_unlock(); return NULL; } /* RCU 写操作创建副本原子替换延迟回收 */ static void rpl_update(struct rcu_protected_list *rpl, int key, int new_value) { struct rcu_protected_entry *old, *new_entry; /* 分配新节点在锁外分配减少锁持有时间 */ new_entry kmalloc(sizeof(*new_entry), GFP_KERNEL); if (!new_entry) return; new_entry-key key; new_entry-value new_value; spin_lock(rpl-update_lock); /* 查找旧节点 */ old rpl_lookup(rpl, key); if (old) { /* 用新节点替换旧节点 */ list_replace_rcu(old-node, new_entry-node); /* * call_rcu注册回调在宽限期结束后回收旧节点 * 宽限期所有预存的读者退出读侧临界区 */ call_rcu(old-rcu, rpl_free_callback); } else { /* 新增节点 */ list_add_rcu(new_entry-node, rpl-head); } spin_unlock(rpl-update_lock); } /* RCU 回调宽限期结束后安全释放旧数据 */ static void rpl_free_callback(struct rcu_head *head) { struct rcu_protected_entry *entry container_of(head, struct rcu_protected_entry, rcu); kfree(entry); } /* RCU 删除操作 */ static void rpl_delete(struct rcu_protected_list *rpl, int key) { struct rcu_protected_entry *entry; spin_lock(rpl-update_lock); entry rpl_lookup(rpl, key); if (entry) { list_del_rcu(entry-node); /* 延迟回收等待所有读者退出 */ call_rcu(entry-rcu, rpl_free_callback); } spin_unlock(rpl-update_lock); }四、内核锁机制的 Trade-offs自旋锁的 CPU 浪费。自旋锁在竞争时忙等CPU 时间被空转消耗。临界区越长、竞争越激烈浪费越严重。经验法则临界区超过 10 微秒时应考虑互斥锁。在中断上下文中别无选择时必须将临界区压缩到最短。互斥锁的优先级反转。低优先级任务持有互斥锁时高优先级任务被阻塞而中等优先级任务可以抢占低优先级任务导致高优先级任务间接被中等优先级任务阻塞。Linux 内核通过优先级继承PI机制缓解此问题但 PI 本身增加了锁的开销。读写锁的写饥饿。读操作持续到来时写操作可能被无限期阻塞。rw_semaphore 的实现中写者等待期间新的读者仍可获取读锁。解决方案是在写者等待时阻止新读者但这又降低了读吞吐。RCU 的内存开销与延迟。RCU 读操作零开销的代价是写操作需要分配副本双倍内存旧数据的回收延迟到宽限期结束通常数十毫秒。在写操作频繁的场景下内存占用会显著增加。RCU 不适用于写多读少的场景。五、总结Linux 内核锁机制从轻到重分为原子操作、自旋锁、互斥锁、读写锁和 RCU 五个层级每个层级针对不同的并发场景。原子操作适用于简单计数器自旋锁适用于中断上下文和极短临界区互斥锁适用于可睡眠的长临界区读写锁适用于读多写少场景RCU 适用于读远多于写的极致性能场景。选择锁的关键是匹配场景错误的选择要么导致 CPU 浪费长临界区用自旋锁要么导致死锁中断上下文用互斥锁要么导致写饥饿高写频率用读写锁。
Linux 内核锁机制:从自旋锁到 RCU 的并发控制演进
Linux 内核锁机制从自旋锁到 RCU 的并发控制演进一、并发控制的锁焦虑选择错误的锁等于选择错误的性能Linux 内核中锁的选择直接影响系统吞吐和延迟。某数据库系统在高并发场景下使用自旋锁保护共享链表CPU 利用率飙升至 95% 但有效吞吐仅 12%——大量 CPU 时间消耗在自旋等待上。将自旋锁替换为互斥锁后CPU 利用率降至 45%吞吐提升 3 倍。但在另一个中断处理场景中互斥锁导致睡眠而触发死锁必须使用自旋锁。锁的选择不是哪个更好而是哪个场景用哪个。Linux 内核提供了从轻量到重量的多种锁机制理解它们的底层实现和适用场景是内核开发的必备能力。二、Linux 内核锁机制的层级与适用场景flowchart TB subgraph 锁层级[锁机制层级从轻到重] direction TB L1[原子操作br/atomic_tbr/无锁单条指令br/适用计数器、标志位] L2[自旋锁br/spinlock_tbr/忙等不睡眠br/适用中断上下文、短临界区] L3[互斥锁br/mutexbr/可睡眠开销较大br/适用长临界区、可睡眠上下文] L4[读写锁br/rwlock_t / rw_semaphorebr/读共享、写独占br/适用读多写少] L5[RCUbr/Read-Copy-Updatebr/读无锁写延迟回收br/适用读远多于写] end L1 --|竞争加剧| L2 L2 --|临界区变长| L3 L3 --|读多写少| L4 L4 --|读远多于写| L5 style 锁层级 fill:#eef,stroke:#333三、内核锁机制的代码实现与分析#include linux/spinlock.h #include linux/mutex.h #include linux/rwlock.h #include linux/rwsem.h #include linux/rcupdate.h #include linux/atomic.h #include linux/slab.h #include linux/module.h #include linux/interrupt.h /* 1. 原子操作 */ /* * 原子操作是最轻量的并发控制基于 CPU 的原子指令 * 适用于简单的计数器和标志位操作 */ struct ref_counter { atomic_t count; }; /* 原子递增并返回新值 */ static inline int ref_counter_inc(struct ref_counter *rc) { return atomic_inc_return(rc-count); } /* 原子递减并测试是否归零 */ static inline int ref_counter_dec_and_test(struct ref_counter *rc) { return atomic_dec_and_test(rc-count); } /* 2. 自旋锁 */ /* * 自旋锁在等待时忙等busy-wait不会让出 CPU * 适用于中断上下文、临界区极短 微秒级的场景 * 禁忌持有自旋锁时不能睡眠不能调用可能调度的函数 */ struct spinlock_protected_list { spinlock_t lock; struct list_head head; int count; }; /* 初始化 */ static void spl_init(struct spinlock_protected_list *spl) { spin_lock_init(spl-lock); INIT_LIST_HEAD(spl-head); spl-count 0; } /* 在中断处理中安全地添加节点 */ static void spl_add_irqsafe(struct spinlock_protected_list *spl, struct list_head *node) { unsigned long flags; /* * spin_lock_irqsave保存中断状态并禁用本地中断 * 防止中断处理程序在同一 CPU 上死锁 */ spin_lock_irqsave(spl-lock, flags); list_add(node, spl-head); spl-count; spin_unlock_irqrestore(spl-lock, flags); } /* 普通上下文中的添加 */ static void spl_add(struct spinlock_protected_list *spl, struct list_head *node) { spin_lock(spl-lock); list_add(node, spl-head); spl-count; spin_unlock(spl-lock); } /* 3. 互斥锁 */ /* * 互斥锁在竞争时让出 CPU等待者进入睡眠 * 适用于临界区较长、可以在进程上下文中睡眠的场景 * 开销加锁/解锁涉及上下文切换约 1-10 微秒 */ struct mutex_protected_buffer { struct mutex lock; void *data; size_t size; size_t used; }; static int mpb_init(struct mutex_protected_buffer *mpb, size_t size) { mutex_init(mpb-lock); mpb-data kzalloc(size, GFP_KERNEL); if (!mpb-data) return -ENOMEM; mpb-size size; mpb-used 0; return 0; } /* 写入数据可能阻塞 */ static ssize_t mpb_write(struct mutex_protected_buffer *mpb, const void *buf, size_t len) { ssize_t written; /* * mutex_lock可能睡眠不能在中断上下文使用 * 适合进程上下文中的长临界区 */ mutex_lock(mpb-lock); if (mpb-used len mpb-size) { written -ENOSPC; goto out; } memcpy(mpb-data mpb-used, buf, len); mpb-used len; written len; out: mutex_unlock(mpb-lock); return written; } /* 4. 读写信号量 */ /* * 读写信号量读操作共享写操作独占 * 适用于读多写少的数据结构如配置表、路由表 * rw_semaphore 比 rwlock_t 更重但允许读操作睡眠 */ struct rwsem_config_table { struct rw_semaphore rwsem; struct hlist_head *buckets; int bucket_count; }; static void rct_init(struct rwsem_config_table *rct, int buckets) { init_rwsem(rct-rwsem); rct-buckets kzalloc(sizeof(struct hlist_head) * buckets, GFP_KERNEL); rct-bucket_count buckets; } /* 读操作多个读者可并发 */ static void *rct_lookup(struct rwsem_config_table *rct, int key) { void *result NULL; down_read(rct-rwsem); /* 获取读锁 */ /* 查找逻辑 */ result NULL; /* 实际查找省略 */ up_read(rct-rwsem); /* 释放读锁 */ return result; } /* 写操作独占访问 */ static void rct_update(struct rwsem_config_table *rct, int key, void *value) { down_write(rct-rwsem); /* 获取写锁等待所有读者退出 */ /* 更新逻辑 */ up_write(rct-rwsem); /* 释放写锁 */ } /* 5. RCURead-Copy-Update */ /* * RCU 是 Linux 内核最精妙的并发控制机制 * 核心思想读者无锁访问旧数据写者创建副本修改宽限期后回收旧数据 * 适用于读远多于写的场景如路由表、进程描述符 * 优势读操作零开销写操作延迟回收 */ struct rcu_protected_entry { int key; int value; struct rcu_head rcu; /* RCU 回收头 */ struct list_head node; }; struct rcu_protected_list { struct list_head head; spinlock_t update_lock; /* 写者之间的互斥 */ }; /* RCU 读操作无锁零开销 */ static struct rcu_protected_entry * rpl_lookup(struct rcu_protected_list *rpl, int key) { struct rcu_protected_entry *entry; /* * rcu_read_lock / rcu_read_unlock * 标记读侧临界区禁止此期间的宽限期结束 * 实际开销极低仅禁用抢占或递增计数器 */ rcu_read_lock(); list_for_each_entry_rcu(entry, rpl-head, node) { if (entry-key key) { rcu_read_unlock(); return entry; } } rcu_read_unlock(); return NULL; } /* RCU 写操作创建副本原子替换延迟回收 */ static void rpl_update(struct rcu_protected_list *rpl, int key, int new_value) { struct rcu_protected_entry *old, *new_entry; /* 分配新节点在锁外分配减少锁持有时间 */ new_entry kmalloc(sizeof(*new_entry), GFP_KERNEL); if (!new_entry) return; new_entry-key key; new_entry-value new_value; spin_lock(rpl-update_lock); /* 查找旧节点 */ old rpl_lookup(rpl, key); if (old) { /* 用新节点替换旧节点 */ list_replace_rcu(old-node, new_entry-node); /* * call_rcu注册回调在宽限期结束后回收旧节点 * 宽限期所有预存的读者退出读侧临界区 */ call_rcu(old-rcu, rpl_free_callback); } else { /* 新增节点 */ list_add_rcu(new_entry-node, rpl-head); } spin_unlock(rpl-update_lock); } /* RCU 回调宽限期结束后安全释放旧数据 */ static void rpl_free_callback(struct rcu_head *head) { struct rcu_protected_entry *entry container_of(head, struct rcu_protected_entry, rcu); kfree(entry); } /* RCU 删除操作 */ static void rpl_delete(struct rcu_protected_list *rpl, int key) { struct rcu_protected_entry *entry; spin_lock(rpl-update_lock); entry rpl_lookup(rpl, key); if (entry) { list_del_rcu(entry-node); /* 延迟回收等待所有读者退出 */ call_rcu(entry-rcu, rpl_free_callback); } spin_unlock(rpl-update_lock); }四、内核锁机制的 Trade-offs自旋锁的 CPU 浪费。自旋锁在竞争时忙等CPU 时间被空转消耗。临界区越长、竞争越激烈浪费越严重。经验法则临界区超过 10 微秒时应考虑互斥锁。在中断上下文中别无选择时必须将临界区压缩到最短。互斥锁的优先级反转。低优先级任务持有互斥锁时高优先级任务被阻塞而中等优先级任务可以抢占低优先级任务导致高优先级任务间接被中等优先级任务阻塞。Linux 内核通过优先级继承PI机制缓解此问题但 PI 本身增加了锁的开销。读写锁的写饥饿。读操作持续到来时写操作可能被无限期阻塞。rw_semaphore 的实现中写者等待期间新的读者仍可获取读锁。解决方案是在写者等待时阻止新读者但这又降低了读吞吐。RCU 的内存开销与延迟。RCU 读操作零开销的代价是写操作需要分配副本双倍内存旧数据的回收延迟到宽限期结束通常数十毫秒。在写操作频繁的场景下内存占用会显著增加。RCU 不适用于写多读少的场景。五、总结Linux 内核锁机制从轻到重分为原子操作、自旋锁、互斥锁、读写锁和 RCU 五个层级每个层级针对不同的并发场景。原子操作适用于简单计数器自旋锁适用于中断上下文和极短临界区互斥锁适用于可睡眠的长临界区读写锁适用于读多写少场景RCU 适用于读远多于写的极致性能场景。选择锁的关键是匹配场景错误的选择要么导致 CPU 浪费长临界区用自旋锁要么导致死锁中断上下文用互斥锁要么导致写饥饿高写频率用读写锁。