Files
2016-10-03 11:21:30 +08:00
..
2016-10-03 11:21:30 +08:00

内核同步控制机制

在讲述内核的各种进程间通信(interprocess communication, IPC)和数据同步机制之前, 我们简 单讨论一下相互通信的进程彼此干扰的可能情况, 以及如何防止. 我们的讨论只限于基本和核心的方面. 对于经典问题的详细解释和大量例子, 请参考市面上操作系统方面的通用教科书.

#1 临界区与竞态条件

所谓临界区(也称为临界段)就是访问和操作共享数据段的代码的段. 多个执行线程并发访问同一资源通常是不安全的, 为了避免在临界区中并发访问, 编程者必须保证这些代码原子地执行----也就是说, 操作在执行结束之前不可被打断, 就如同整个临界区是一个不可分割的指令一样. 如果两个执行现成有可能处于同一临界区中同时执行. 那么这就是程序中包含的一个bug. 如果这种情况确实发生了, 我们就称之为竞态条件(race conditions), 这样命名是因为这里会存在线程竞争. 这种情况出现的机会往往非常小----就是因为竞争引起的错误非常不易重视, 所以调试这种错误才会非常困难. 避免并发和防止竞争条件称之为同步(synchronization)

##1.1 竞争条件

我们考虑系统通过两种接口从外部设备读取数据的情况. 独立的数据包以不定间隔通过两个接口到达, 存在不同文件中. 录数据包到达的次序, 文件名之后添加了一个号码, 明数据包的 序号. 通常的一系列文件名是act1.filact2.filact3.fil, 等等. 可使用一个独立的变量来简化两个进程的工作. 该变量保存在由两个进程共享的内存页中, 且指定了下一个未使用的序号(为简 明起见,在下文我称该变量为 counter).

在一个数据包到达时, 进程必须执行一些操作, 才能正确地保存数据.

  1. 从接口读取数据

  2. 用序号counter构造文件名,打开一个文件

  3. 将序号加1

  4. 将数据写入文件,然后关闭文件。

上述的软件系统会发生错误吗? 如果每个进程都严格遵守上述过程, 并在适当的位置对状态变量加1, 那么上述过程不仅适用于两个进程, 也可用于多个进程.

事实上, 大多数情况下上述过程都会正确运作, 但在某些情况下会出错, 而这也是分布式程序设计的真正困难所在. 我们设个陷阱, 分别将从接口读取数据的进程称作进程1和进程2.

我们给出的场景中, 已经保存了若干文件, 比如说总共有12文件. 因此counter的值是13. 显然是个"凶兆"......

进程1从接口接收一个刚到达的新数据块. 它忠实地用序号13构造文件名并打开一个文件, 而同时调度器被激活并确认该进程已经消耗了足够的CPU时间, 必须由另一个进程替换, 并且假定是进程2. 要注意, 此时进程1读取了counter的值,但尚未对counter加1.

在进程2开始运行后, 同样从其对应的接口读取数据, 并开始执行必要的操作以保存这些数据.

它会读取counter的值, 用序号13构造文件名打开文件, 将 counter加1, counter从13变为14. 接下来它将数据写入文件, 最后结束.

不久, 又轮到进程1再次运行. 它从上次暂停处恢复执行, 并将counter加1, counter从14变为15. 接下来它将数据写入到用序号13打开的文件, 当然, 在这样做的时候, 会覆盖进程2已经保存的数据.

这简直是祸不单行, 丢失了一个数据记录, 而且序号14也变得不可用了.

修改程序接收数据之后的处理步骤, 可以防止该错误. 举例来说,进程可以在读取counter的值之后立即将counter加1, 然后再去打开文件. 但再想想, 问题远远不会这么简单. 因为我们总是可以设计出一些导致致命错误的情形. 因此, 我们很快就意识到了 : 如果在读取counter的值和对其加1 之间发生调度, 则仍然会产生不一致的情况.

几个进程在访问资源时彼此干扰的情况通常称之为竞态条件(race condition). 在对分布式应用编程时, 这种情况是一个主要的问题, 因为竞态条件无法通过系统的试错法检测. 相反, 只有彻底研究源代码(深入了解各种可能发生的代码路径)并通过敏锐的直觉, 才能找到并消除竞态条件.

由于导致竞态条件的情况非常罕见, 因此需要提出一个问题 : 是否值得做一些(有时候是大量的)工作来保护代码避免竞态条件.

在某些环境中(飞机的控制系统、重要机械的监控、危险装备), 竞态条件是致命问题. 即使在日常软件项目中, 避免潜在的竞态条件也能大大提高程序的质量以及用户的满意度. 为改进Linux内核对多处理器的支持, 我们需要精确定位内核中暗藏竞态条件的范围,并提供适当的防护. 由于缺乏保护而导致的出乎意料的系统崩溃和莫名其妙的错误, 这些都是不可接受的.

##1.2 临界区

这个问题的本质是 : 进程的执行在不应该的地方被中断, 从而导致进程工作得不正确. 显然, 一种可能的解决方案是标记出相关的代码段, 使之无法被调度器中断. 尽管这种方法原则上是可行的, 但有几个内在问题. 在某种情况下, 有问题的程序可能迷失在标记的代码段中无法退出, 因而无法放弃CPU, 进而导致计算机不可用. 因此我们必须立即放弃这种解决方案.

问题的解决方案不一定要求临界区是不能中断的. 只要没有其他的进程进入临界区, 那么在临界区中执行的进程完全是可以中断的. 这种严格的禁止条件, 可以确保几个进程不能同时改变共享的值, 我们称为互斥(mutual exclusion). 也就是说, 在给定时刻, 只有一个进程可以进入临界区代码.

有许多方法可以设计这种类别的互斥方法(不考虑技术实现问题). 但所有的设计都必须保证, 无论在何种情况下都要确保排他原则(exclusion principle). 这种保证决不能依赖于所涉及处理器的数目或速度. 如果存在这样的依赖(以至于解决方案只适用于特定硬件配置下的给定计算机系统), 那 么该方案将是不切实际的. 因为它无法提供通用的保护机制, 而这正是我们所需要的. 进程不应该允许彼此阻塞或永久停止. 尽管这里描述了一个可取的目标, 但它并不总是能够用技术手段实现, 读者从下文可以看到这一点. 经常需要程序员未雨绸缪,以避免问题的发生.

应用何种原理来支持互斥方法? 在多任务和多用户系统的历史上, 人们提出了许多不同的解决方案, 但都各有利弊. 一些解决方案只是纯理论的, 而另一些则已经在各种操作系统中付诸实践了.

#2 锁机制

##2.1 造成并发执行的原因

##2.2 我们要保护什么

#4 死锁

#5 争用和扩展性