1. 并发编程基础
并发编程是内核性能调优的基石。在深入读写锁与 RCU 之前,必须精确理解三个核心概念:竞态条件、临界区以及并发与并行的区别。这些概念直接决定了锁机制的设计思路与性能边界。
1.1 竞态条件(Race Condition)
定义:当多个执行单元(线程/进程/CPU 核)同时访问共享资源,且最终结果依赖于这些执行单元的精确时序时,就产生了竞态条件。竞态条件会导致数据不一致、系统崩溃或安全漏洞。
核心特征: 结果不可预测,且难以通过简单测试复现。竞态条件在低负载下可能“正常”,在高并发下突然爆发。
典型竞态场景(检查-使用模式):
// 伪代码:两个线程同时执行
if (counter > 0) { // 步骤1:检查
counter--; // 步骤2:使用
// 预期:counter 从 1 减到 0
// 实际:两个线程都通过检查,counter 变成 -1
}
上述代码中,步骤1和步骤2之间不是原子操作。当线程A检查完 counter > 0 后,线程B也检查并通过,随后两个线程都执行 counter--,导致 counter 被多减一次。
1.2 临界区(Critical Section)
定义:临界区是访问共享资源的代码片段,在同一时刻只允许一个执行单元进入。临界区必须满足互斥(Mutual Exclusion)原则。
临界区的三个关键属性:
| 属性 | 说明 | 违反后果 |
|---|---|---|
| 互斥 | 最多一个线程在临界区内执行 | 数据损坏、竞态条件 |
| 前进 | 没有线程在临界区时,请求进入的线程必须被允许进入 | 死锁、活锁 |
| 有限等待 | 线程不能无限等待进入临界区 | 饥饿、优先级反转 |
调优视角: 临界区的粒度直接影响性能。临界区越短,锁竞争越小,但设计复杂度越高。读写锁和 RCU 的核心目标就是缩小临界区或消除读端临界区。
1.3 并发与并行的区别
这两个术语常被混用,但在并发编程中含义截然不同:
| 维度 | 并发(Concurrency) | 并行(Parallelism) |
|---|---|---|
| 定义 | 多个任务在逻辑上同时执行(交替执行) | 多个任务在物理上同时执行(真正同时) |
| 硬件要求 | 单核 CPU 即可实现(时间片轮转) | 必须多核 CPU 或多处理器 |
| 关注点 | 任务调度、资源访问控制 | 计算吞吐、负载均衡 |
| 典型场景 | 单核服务器处理多个网络连接 | 多核 CPU 并行处理矩阵乘法 |
| 竞态风险 | 存在(时间片切换点) | 极高(真正同时访问) |
常见误区: 并发不一定是并行,但并行一定是并发。Linux 内核中,即使只有一个 CPU,也可能因为中断、软中断、抢占等机制产生并发竞态。多核环境下,并行竞态更加频繁且难以调试。
1.4 概念关系图
下图展示了竞态条件、临界区与并发/并行之间的逻辑关系:
1.5 内核中的并发来源
Linux 内核中,竞态条件可能来自以下五种并发源:
- 中断处理:硬件中断异步触发,与当前执行流并发。
- 软中断 / tasklet:在中断上下文或特定 CPU 上异步执行。
- 内核抢占:高优先级进程抢占当前进程的执行。
- 多处理器并行:多个 CPU 同时执行内核代码。
- 进程间通信:信号、共享内存等导致的同步问题。
实战要点: 在编写内核模块或驱动时,必须假设任何时刻都可能被中断、抢占或并行执行。使用
spin_lock_irqsave() 等变体正是为了同时屏蔽中断和抢占,确保临界区的绝对互斥。
1.6 本章小结
| 概念 | 一句话记忆 | 对性能调优的意义 |
|---|---|---|
| 竞态条件 | 结果依赖时序,不可预测 | 必须通过同步机制消除 |
| 临界区 | 一次只让一个人进的代码段 | 临界区越小,锁竞争越低 |
| 并发 vs 并行 | 并发是逻辑同时,并行是物理同时 | 并行场景下锁竞争更激烈,需要更高效的同步机制 |