深入理解Linux内核--每cpu变量,内核抢占,屏障,原子变量,自旋锁,互斥锁,信号量,读拷贝更新,性能优化

1.每 CPU 变量 (Per-CPU Variables)

核心思想:为每个 CPU 分配一份独立的数据副本,避免缓存行 bouncing(缓存行在多个 CPU 间来回迁移)。

c 复制代码
// 定义
DEFINE_PER_CPU(int, my_counter);
// 或动态分配
void __percpu *ptr = alloc_percpu(int);

// 安全访问(关抢占 + 获取当前 CPU 指针)
int *p = get_cpu_var(my_counter);
(*p)++;
put_cpu_var(my_counter);

// 远程访问(其他 CPU)
per_cpu(my_counter, cpu_id);

关键点:

  • 通过 get_cpu_var() / put_cpu_var() 配对使用,内部会关闭内核抢占,防止在访问期间被调度到其他 CPU
  • 数据按 CPU 缓存行对齐(通常 64/128 字节),避免 false sharing
  • 适用于计数器、统计信息等高频写、低频聚合读的场景

2.内核抢占 (Kernel Preemption)

概念:允许高优先级任务在内核态执行时抢占当前任务。

c 复制代码
// 抢占控制 API
preempt_disable();      // 增加抢占计数
preempt_enable();       // 减少抢占计数,必要时检查抢占
preempt_count();        // 当前抢占计数

// 自愿调度
schedule();
cond_resched();         // 如果 need_resched 被设置,则主动让出

抢占点:

  • 中断返回时(irq_exit)
  • 开启抢占时(preempt_enable)
  • 显式调用 schedule()
  • 从系统调用返回用户态时

与自旋锁的关系:持有自旋锁时会自动关闭抢占(preempt_disable 嵌入在 spin_lock 中),防止持有锁时被抢占比导致死锁。

3.内存屏障 (Memory Barriers)

目的:防止编译器和 CPU 对内存操作进行乱序重排,保证多核间的内存可见性顺序。

c 复制代码
// 编译器屏障
barrier();

// 通用内存屏障
smp_mb();       // 全屏障:之前的读写都完成后,才能执行之后的读写
smp_rmb();      // 读屏障
smp_wmb();      // 写屏障

// 带依赖的屏障(ARM/弱序架构常用)
smp_read_barrier_depends();

// 与锁配对的隐式屏障
spin_lock/unlock()  // 隐含全屏障
mutex_lock/unlock() // 隐含全屏障
atomic_*_return()   // 隐含全屏障

使用原则:通常不需要显式使用,应优先使用原子操作、锁等高层抽象,它们已内含正确的屏障语义。

4.原子变量 (Atomic Variables)

实现:依赖底层 CPU 的原子指令(如 x86LOCK 前缀、XCHGCMPXCHGARMLDREX/STREX)。

c 复制代码
atomic_t v = ATOMIC_INIT(0);
atomic_set(&v, 5);
int val = atomic_read(&v);
atomic_inc(&v);
atomic_dec_and_test(&v);      // 递减并测试是否为 0
atomic_add_return(3, &v);     // 加法并返回新值(含内存屏障)
atomic_cmpxchg(&v, old, new); // CAS 操作

位原子操作:

c 复制代码
set_bit(0, &bitmap);          // 原子设置第 0 位
test_and_set_bit(0, &bitmap); // 测试并设置(CAS 语义)

适用场景:计数器、标志位、引用计数等极短临界区。

5.自旋锁 (Spinlock)

机制:忙等待锁,持有锁期间关闭内核抢占(但不关中断)。

c 复制代码
spinlock_t lock;
spin_lock_init(&lock);

spin_lock(&lock);      // 获取锁,关抢占
// 临界区
spin_unlock(&lock);    // 释放锁,开抢占

// 中断安全版本(防止中断处理程序也尝试获取同一把锁导致死锁)
spin_lock_irqsave(&lock, flags);
spin_unlock_irqrestore(&lock, flags);

// 下半部安全版本
spin_lock_bh(&lock);
spin_unlock_bh(&lock);

核心规则:

  • 临界区必须极短(不能睡眠、不能调度、不能访问用户空间)
  • 单核非抢占内核中,自旋锁退化为空操作(仅关抢占)
  • 锁粒度要细,减少竞争

5.1.spin_lock --- 基础版

c 复制代码
spin_lock(&lock);
// 临界区
spin_unlock(&lock);

实际做了什么:

c 复制代码
preempt_disable();   // 关闭内核抢占
while (!trylock(&lock))  // 忙等待
    ;

保护范围:仅屏蔽同 CPU 上的其他进程上下文(通过关闭抢占),以及其他 CPU 上的所有上下文(通过锁变量本身)。

不保护:本地 CPU 上的中断、软中断、NMI。

5.2.spin_lock_irqsave --- 中断安全版

c 复制代码
unsigned long flags;
spin_lock_irqsave(&lock, flags);
// 临界区
spin_unlock_irqrestore(&lock, flags);

实际做了什么:

c 复制代码
local_irq_save(flags);   // 保存当前中断状态,关闭本地硬中断
preempt_disable();       // 关闭内核抢占
while (!trylock(&lock))
    ;

保护范围:屏蔽了本地 CPU 上的一切------进程、中断、软中断、NMI。以及其他 CPU 上的所有上下文(通过锁变量本身)。

5.3.spin_lock_bh --- 下半部安全版

c 复制代码
spin_lock_bh(&lock);
// 临界区
spin_unlock_bh(&lock);

实际做了什么:

c 复制代码
local_bh_disable();      // 关闭本地软中断(包括 tasklet、timer、NET_RX 等)
preempt_disable();       // 关闭内核抢占
while (!trylock(&lock))
    ;

保护范围:屏蔽本地 CPU 上的进程上下文和软中断上下文,但不关闭硬中断。

5.4.对比总结

变体 关闭抢占 关闭硬中断 关闭软中断 保护范围 开销
spin_lock 进程 ↔ 进程 最小
spin_lock_bh 进程 ↔ 软中断 中等
spin_lock_irqsave 进程 ↔ 中断/一切 最大

选择决策树

c 复制代码
你在进程上下文持锁?
    │
    ├── 中断处理程序会抢这把锁? ──→ spin_lock_irqsave
    │
    ├── 软中断/tasklet 会抢这把锁? ──→ spin_lock_bh
    │
    └── 只有进程上下文竞争? ──→ spin_lock

内核源码中的实际模式:

c 复制代码
// Linux 内核中典型的"分层"锁使用(以网络子系统为例)

// 1. 硬中断(最内层,已关中断)
irqreturn_t rx_irq(int irq, void *dev_id) {
    spin_lock(&queue->lock);        // ✅ 中断里只用基础 spin_lock
    // 收包入队
    spin_unlock(&queue->lock);
    raise_softirq(NET_RX_SOFTIRQ);  // 触发软中断
}

// 2. 软中断(关软中断,但硬中断可发生)
void net_rx_action(struct softirq_action *h) {
    spin_lock(&queue->lock);        // ✅ 软中断里也用基础 spin_lock
    // 协议栈处理
    spin_unlock(&queue->lock);
}

// 3. 进程上下文(可能和中断/软中断竞争)
void dev_queue_xmit(struct sk_buff *skb) {
    spin_lock_bh(&queue->lock);     // ✅ 防软中断抢锁
    // 发包处理
    spin_unlock_bh(&queue->lock);
}

// 4. 通用路径(不确定上下文)
void some_generic_fn(void) {
    unsigned long flags;
    spin_lock_irqsave(&queue->lock, flags);  // ✅ 最安全,但开销最大
    // ...
    spin_unlock_irqrestore(&queue->lock, flags);
}

6.互斥锁 (Mutex)

机制:睡眠锁,无法获取时当前任务进入睡眠状态,让出 CPU。

c 复制代码
struct mutex mtx;
mutex_init(&mtx);

mutex_lock(&mtx);           			// 不可中断睡眠
mutex_lock_interruptible(&mtx);  	// 可中断(返回 -EINTR)
mutex_trylock(&mtx);        			// 非阻塞尝试

// 临界区
mutex_unlock(&mtx);

与自旋锁对比:

特性 自旋锁 (Spinlock) 互斥锁 (Mutex)
等待方式 忙等待(消耗 CPU) 睡眠(让出 CPU)
临界区长度 极短(几行代码) 较长(可睡眠)
可重入 不可重入 不可重入
上下文 中断上下文可用 仅进程上下文
开销 获取快(无上下文切换) 获取慢(可能切换上下文)

7.信号量 (Semaphore)

机制:计数锁,允许指定数量的持有者同时进入临界区。

c 复制代码
struct semaphore sem;
sema_init(&sem, 1);   							// 二值信号量(类似互斥锁)
sema_init(&sem, 5);   							// 计数信号量(最多 5 个并发)

down(&sem);           							// P 操作:计数减 1,若为负则睡眠
down_interruptible(&sem);
up(&sem);             							// V 操作:计数加 1,唤醒等待者

现代内核中的定位:

  • 二值信号量基本被 Mutex 取代(Mutex 有 owner 追踪,调试更友好)
  • 计数信号量用于资源池限制(如同时打开的 socket 数量限制)

8.读拷贝更新 (RCU - Read-Copy-Update)

核心思想:读端零开销(无需锁、无需原子操作),写端通过拷贝+替换+延迟回收实现。

c 复制代码
// ========== 读端 ==========
rcu_read_lock();        													// 轻量级,仅关抢占(或记录 QS 状态)
struct my_struct *p = rcu_dereference(global_ptr); 	// 解引用,含内存屏障
// 读取 p 的数据(禁止写操作!)
rcu_read_unlock();

// ========== 写端 ==========
struct my_struct *new_p = kmalloc(...);
*new_p = *old_p;        								// 复制旧数据
new_p->field = new_value; 							// 修改副本
rcu_assign_pointer(global_ptr, new_p);  	// 原子替换指针,含写屏障

// 延迟释放旧数据(等待所有读端完成)
synchronize_rcu();      								// 阻塞,直到所有已存在的读端结束
kfree(old_p);

// 或使用回调异步释放
call_rcu(&old_p->rcu_head, my_free_func);

宽限期 (Grace Period):

  • 所有 CPU 都至少经历一次上下文切换(或静止状态 quiescent state)
  • 在此之后,旧的读端引用已不存在,可以安全释放

适用场景:

  • 读多写极少的链表、哈希表、路由表
  • 内核中 dentry_cache、路由表、进程链表 大量使用 RCU

9.性能优化策略

9.1.锁的选择层次

cpp 复制代码
读多写极少  →  RCU(读端零开销)
读多写少   →  读写锁 / Seqlock(顺序锁,写优先)
读写均衡   →  自旋锁(短临界区)/ 互斥锁(长临界区)
计数限制   →  信号量 / 每 CPU 计数器聚合

9.2.关键优化原则

策略 说明
缩小临界区 只保护必要的数据访问,I/O、复杂计算移到锁外
降低锁粒度 全局锁 → 分片锁(per-bucket、per-cpu、per-node)
避免锁 用每 CPU 变量、RCU、无锁算法(如 llist)替代
锁顺序 多锁时必须全局统一获取顺序,防止死锁
缓存对齐 锁本身和数据结构按缓存行对齐,避免 false sharing
自适应自旋 mutex 在短等待时先自旋(CONFIG_MUTEX_SPIN_ON_OWNER),避免上下文切换开销

9.3.内核中的无锁数据结构

  • llist:基于 CAS 的单链表(用于工作队列)
  • percpu_ref:引用计数 + RCU 延迟释放
  • seqlock:写端无阻塞,读端通过序列号检测冲突并重试(用于 jiffies、xtime)

10.总结速查

机制 等待行为 能否睡眠 中断上下文 典型用途
每 CPU 变量 统计计数器
原子变量 引用计数、标志位
自旋锁 忙等 可(irqsave 版本) 极短临界区
互斥锁 睡眠 一般同步
信号量 睡眠 资源池限制
RCU 无(读端) 否(读端) 可(读端) 读多写少的数据结构
相关推荐
华科大胡子25 天前
条款16:保证const成员函数的线程安全
互斥锁·线程安全·modern c++·const成员函数·原子变量
是个西兰花1 个月前
Linux:深入解析Linux线程原理与实现
linux·运维·c++·线程·互斥锁
Dlrb12113 个月前
Linux系统编程-信号量(线程同步机制)
linux·条件变量·互斥锁·信号量·线程同步
Dlrb12113 个月前
Linux系统编程-线程与多线程模块的封装
linux·线程·互斥锁·线程同步·线程互斥
mftang3 个月前
Zephyr RTOS 中k_mutex(互斥锁)功能介绍
互斥锁·zephyr·zephyr rtos·k_mutex
代码中介商4 个月前
Linux多线程编程完全指南(下):线程同步与互斥锁
linux·redis·线程·互斥锁
W.W.H.5 个月前
优先级反转问题(含解决案例)
互斥锁·rtos·互斥量·实时系统·优先级反转·优先级继承
硅基导游5 个月前
bpf监控某个应用里各线程锁的申请得到及释放时间
服务器·互斥锁·性能监控
冉佳驹5 个月前
Linux ——— 多线程编程中的核心概念与技术实现
linux·条件变量·互斥锁·自旋锁·线程的概念·线程的创建、等待、分离·生产者消费模型