Chapter 9 - 内核同步介绍
Created by : Mr Dk.
2019 / 10 / 23 14:18
Nanjing, Jiangsu, China
需要特别留意保护共享资源,防止共享资源并发访问。
9.1 临界区和竞争条件
临界区:访问和操作共享数据的代码段。多个线程并发访问同一个资源通常不安全,必须保证进入临界区中的代码 原子地 执行:操作在执行结束前不可被打断,要么执行完,要么完全不执行,如同整个临界区是一条不可分割的指令。
如果两个线程同时进入临界区,那么就发生了 竞争条件 (race condition)。避免并发和防止竞争条件称为 同步 (synchronization)。
9.1.1 为什么我们需要保护
银行 ATM 机的例子
- 必须保证在操作期间对账户加锁
- 确保每个事物相对于其它事物的操作时原子性的
- 事物必须完整地发生,要么干脆不发生,但是绝不能打断
9.1.2 单个变量
i++;
的例子。多数 CPU 都提供了指令来原子地读变量、增加变量、并写回变量,使用这样的指令能够解决一定的问题,因为两个原子操作交错执行根本不可能发生。CPU 从物理上确保了这种不可能。
9.2 加锁
对数据结构的操作显然不能通过原子指令来实现。当共享资源是一个复杂的数据结构时,竞争条件往往会使该数据结构遭到破坏。硬件不可能对不定长度的临界区提供原子指令,需要 锁 机制来确保一次有且只有一个线程对数据结构进行操作:线程持有锁,锁保护了数据。
锁的使用是 自愿、非强制 的,不强制使用,但为了防止发生竞争条件,必须使用。锁有多种形式,且加锁的粒度和范围也各不相同。锁由 原子操作 实现:
- 原子操作不存在竞争
- 单一指令可以验证锁的关键部分是否被抓住
- 几乎所有 CPU 都提供了 测试 和 设置 指令,对锁进行原子地验证
9.2.1 造成并发执行的原因
用户空间需要同步:
- 因为用户程序会被调度程序抢占和重新调度
- 程序在临界区中时,可能会被非自愿抢占
这类并发实际上并不是同时发生的。如果有多核 CPU,则存在着真正意义上的并发(并行?)。内核可能造成并发执行的原因:
- 中断 - 任何时刻异步发生
- 软中断、tasklet
- 内核抢占
- 睡眠、与用户空间同步
- 对称多处理
应当在最开始设计代码时就考虑加锁,而不是事后。如果代码已经写好,在其中找到需要加锁的地方并加锁,是很困难的,而且结果也不好。
9.2.2 了解要保护些什么
执行线程的局部数据仅仅被它本身访问,显然不需要保护。要给数据而不是给代码加锁。编写内核代码时,要问问自己这些问题:
- 数据是否是全局的?
- 数据会不会在进程上下文和中断上下文中共享
- 访问数据时可不可能被抢占
- 当前进程是不是会睡眠在某些资源上
- 函数如果在另一个 CPU 上被调度将会发生什么
9.3 死锁
每个线程都在等待一个资源,所有的资源都已经被占用,所有线程都在互相等待,但永远不会释放已经占用的资源。
- 自死锁:一个执行线程试图获得一个自己已经持有的锁
- ABBA 死锁:线程 A 和 B 分别获得了 A 和 B 锁,都想要另一个锁
没有线程会释放一开始就持有的锁。一些避免死锁的策略:
- 按顺序加锁:保证以相同的顺序获取锁,如果两个或多个锁在同一时间里被请求,其它函数也应当按照前次的加锁顺序进行
- 防止发生饥饿
- 不要重复请求同一个锁
- 设计力求简单
只要嵌套使用多个锁,就要按照相同的顺序去获取它们。
9.4 争用和扩展性
锁的争用,指锁被占用时,有其它线程试图获得该锁——多个线程等待获得该锁:高度争用状态。锁使程序以串行的方式对资源进行访问,使用锁会降低系统的性能,被高度争用的锁会成为系统的性能瓶颈,严重降低系统性能。
扩展性是对系统可扩展程度的一个量度,任何可以被计量的计算机组件都可以涉及可扩展性。在 Linux 2.6 内核中,内核加的锁是非常细的粒度。加锁粒度用于描述加锁保护的数据规模:
- 一个过粗的锁保护大块数据
- 一个精细的锁保护很小的一块数据
细粒度锁在大型系统上的性能可能会很好,但在小型系统上会增加复杂度,并加大开销
- 锁争用太严重时,加锁太粗会降低可扩展性
- 锁争用不明显时,加锁太细会加大系统开销,带来浪费
两种情况都会造成系统性能下降。