1. 同步机制概述:为什么需要同步?
在现代操作系统中,多个执行流(进程、线程、中断处理程序)并发访问共享数据是常态。当这些并发访问没有受到正确约束时,就会产生数据不一致、程序崩溃等严重问题。同步机制正是为了解决这些问题而设计的核心基础设施。
1.1 竞态条件:并发访问的根源问题
当多个执行流同时访问和操作同一份共享数据,并且最终结果依赖于这些操作的执行顺序时,就发生了竞态条件(Race Condition)。竞态条件会导致数据状态不可预测,是并发编程中最常见也最隐蔽的错误来源。
经典案例: 两个线程同时对一个全局计数器执行
counter++ 操作。该操作在机器层面并非原子,而是分解为“读取-修改-写入”三步。当两个线程的这三步交错执行时,最终结果可能只增加1,而不是预期的2。
1.2 临界区:需要保护的代码区域
临界区(Critical Section)是指访问共享资源的代码片段,这些代码片段在同一时刻只允许一个执行流进入。保护临界区的核心原则是互斥(Mutual Exclusion):当一个执行流在临界区内执行时,其他执行流不得进入。
临界区保护需要满足以下四个条件:
- 互斥性:任何时刻最多只有一个执行流在临界区内。
- 前进性:当没有执行流在临界区内时,必须允许一个等待进入的执行流进入。
- 有限等待:执行流在有限时间内必须获得进入临界区的机会,避免饥饿。
- 无忙等(可选):等待进入临界区的执行流不应消耗CPU时间片(现代同步机制通常支持)。
1.3 原子操作:不可分割的最小单元
原子操作(Atomic Operation)是指不可被中断的操作,要么全部执行成功,要么完全不执行,不存在中间状态。原子操作是构建所有高级同步机制的基石。
在Linux内核中,原子操作分为两类:
| 类型 | 描述 | 典型API | 适用场景 |
|---|---|---|---|
| 原子整数操作 | 对整型变量进行原子的读-改-写 | atomic_inc(), atomic_dec_and_test() |
计数器、引用计数、标志位 |
| 原子位操作 | 对内存中的特定位进行原子设置/清除 | set_bit(), test_and_clear_bit() |
位图管理、状态标志 |
| 原子比较交换 | 比较并交换(CAS),无锁编程核心原语 | cmpxchg(), atomic_cmpxchg() |
无锁数据结构、RCU发布机制 |
| 原子内存屏障 | 保证内存访问的顺序性 | smp_mb(), smp_rmb(), smp_wmb() |
多核同步、RCU宽限期检测 |
关键理解:原子操作是硬件级别的保证,通常由CPU指令集直接支持(如x86的
LOCK 前缀指令、ARM的 LDREX/STREX 指令对)。软件层面的同步机制(自旋锁、读写锁、RCU)最终都依赖于这些硬件原子操作。
1.4 为什么需要同步机制?
没有同步机制,并发系统将面临以下灾难性后果:
- 数据损坏:竞态条件导致共享数据结构处于不一致状态,如链表指针错乱、计数器值错误。
- 程序崩溃:访问未正确同步的数据可能导致空指针解引用、内存越界等致命错误。
- 安全漏洞:TOCTOU(Time-of-Check Time-of-Use)竞态可被利用来绕过权限检查。
- 死锁与饥饿:不正确的同步设计可能导致系统永久阻塞或某些执行流永远无法获得资源。
1.5 同步机制的性能权衡
选择同步机制时,需要在以下维度进行权衡:
| 维度 | 说明 | 对性能的影响 |
|---|---|---|
| 粒度 | 锁保护的共享数据范围 | 粒度越细,并发度越高,但锁管理开销越大 |
| 开销 | 获取/释放同步原语所需的CPU周期 | 开销越低,临界区执行效率越高 |
| 扩展性 | 随着CPU核心数增加,同步机制的性能表现 | 扩展性差的机制在多核系统上会成为瓶颈 |
| 公平性 | 是否保证所有执行流都有机会获得锁 | 公平性越好,越不容易出现饥饿,但可能牺牲吞吐量 |
| 读者并发 | 是否允许多个读者同时访问 | 读多写少场景下,读者并发能大幅提升性能 |
常见误区:不要认为“锁越少性能越好”。不正确的同步会导致数据损坏,而过度使用粗粒度锁会导致性能瓶颈。正确的做法是:在保证正确性的前提下,选择最适合场景的同步机制。这正是本课程对比RCU与读写锁的核心目的。
1.6 小结:同步机制的核心概念
本章建立了理解后续内容的基础:
- 竞态条件是并发访问共享数据时出现的不可预测行为,是同步机制要解决的根本问题。
- 临界区是需要互斥保护的代码区域,同步机制的核心目标是确保临界区的正确执行。
- 原子操作是硬件提供的最小不可分割操作单元,是所有高级同步机制的构建基石。
- 同步机制的选择需要在粒度、开销、扩展性、公平性之间进行权衡,没有银弹。