第 5 章 并发和竞争情况

目录

5.1. scull 中的缺陷
5.2. 并发和它的管理
5.3. 旗标和互斥体
5.3.1. Linux 旗标实现
5.3.2. 在 scull 中使用旗标
5.3.3. 读者/写者旗标
5.4. Completions 机制
5.5. 自旋锁
5.5.1. 自旋锁 API 简介
5.5.2. 自旋锁和原子上下文
5.5.3. 自旋锁函数
5.5.4. 读者/写者自旋锁
5.6. 锁陷阱
5.6.1. 模糊的规则
5.6.2. 加锁顺序规则
5.6.3. 细 -粗- 粒度加锁
5.7. 加锁的各种选择
5.7.1. 不加锁算法
5.7.2. 原子变量
5.7.3. 位操作
5.7.4. seqlock 锁
5.7.5. 读取-拷贝-更新
5.8. 快速参考

迄今, 我们未曾关心并发的问题 -- 就是说, 当系统试图一次做多件事时发生的情况. 然而, 并发的管理是操作系统编程的核心问题之一. 并发相关的错误是一些最易出现又最难发现的问题. 即便是专家级 Linux 内核程序员偶尔也会出现并发相关的错误.

早期的 Linux 内核, 较少有并发的源头. 内核不支持对称多处理器(SMP)系统, 并发执行的唯一原因是硬件中断服务. 那个方法提供了简单性, 但是在有越来越多处理器的系统上注重性能并且坚持系统要快速响应事件的世界中它不再可行了. 为响应现代硬件和应用程序的要求, Linux 内核已经发展为很多事情在同时进行. 这个进步已经产生了很大的性能和可扩展性. 然而, 它也很大地使内核编程任务复杂化. 设备启动程序员现在必须从一开始就将并发作为他们设计的要素, 并且他们必须对内核提供的并发管理设施有很强的理解.

本章的目的是开始建立那种理解的过程. 为此目的, 我们介绍一些设施来立刻应用到第 3 章的 scull 驱动. 展示的其他设施暂时还不使用. 但是首先, 我们看一下我们的简单 scull 驱动可能哪里出问题并且如何避免这些潜在的问题.

5.1. scull 中的缺陷

让我们快速看一段 scull 内存管理代码. 在写逻辑的深处, scull 必须决定它请求的内存是否已经分配. 处理这个任务的代码是:

if (!dptr->data[s_pos]) {
    dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
    if (!dptr->data[s_pos])
        goto out;
}

假设有 2 个进程( 我们会称它们为"A"和"B" ) 独立地试图写入同一个 schull 设备的相同偏移. 每个进程同时到达上面片段的第一行的 if 测试. 如果被测试的指针是 NULL, 每个进程都会决定分配内存, 并且每个都会复制结果指针给 dptr->datat[s_pos]. 因为 2 个进程都在赋值给同一个位置, 显然只有一个赋值可以成功.

当然, 发生的是第 2 个完成赋值的进程将"胜出". 如果进程 A 先赋值, 它的赋值将被进程 B 覆盖. 在此, scull 将完全忘记 A 分配的内存; 它只有指向 B 的内存的指针. A 所分配的指针, 因此, 将被丢掉并且不再返回给系统.

事情的这个顺序是一个竞争情况的演示. 竞争情况是对共享数据的无控制存取的结果. 当错误的存取模式发生了, 产生了不希望的东西. 对于这里讨论的竞争情况, 结果是内存泄漏. 这已经足够坏了, 但是竞争情况常常导致系统崩溃和数据损坏. 程序员可能被诱惑而忽视竞争情况为相当低可能性的事件. 但是, 在计算机世界, 百万分之一的事件会每隔几秒发生, 并且后果会是严重的.

很快我们将去掉 scull 的竞争情况, 但是首先我们需要对并发做一个更普遍的回顾.