在同一个操作系统中,不同的进程经常需要相互协同工作,协同的方法一般有两种,一是直接共享逻辑地址空间,二是通过文件或消息共享数据。如果共享逻辑地址空间,则在进程执行的时候有可能会发生多个进程同时访问同一个数据的冲突问题,特别是在多处理器的情况下。对于这类冲突,内核采用了一些方法进行进程同步,例如原子操作、自旋锁、信号量等方法。接下来的四篇(包括本文)将分别介绍原子操作、自旋锁、信号量和死锁的一些概念,同时以Linux4.8.1版本的内核代码(x86架构部分)为例进行分析。
临界区
临界区(critical-section)是解决进程协作的一个方法。将多个进程可能修改同一个共享变量的代码段设为临界区,当有进程进入临界区后,其他进程会被禁止进入,直到前一个进程离开临界区,其他进程才可以进入。即同一时刻只允许一个进程位于临界区内。伪代码形式可以表示为:
do{
//进入区
//临界区
//退出区
//剩余区
}while(TRUE)
临界区的实现需要满足以下三个条件:
1, 互斥,即同一时刻只能有一个进程位于临界区内;
2, 前进,当多个进程同时等待进入临界区的时候,会有一个进程被选择进入;
3, 有限等待,在进入区等待的进程必须在有限时间后进入临界区。
作为所有软件都可能会调用的操作系统,同一时刻内同一段代码可能会有多个进程在执行,而像文件读写、硬件调用等操作都是排他性的,因此操作系统更应该做好临界区的设置。
对于操作系统的临界区实现,要分为抢占内核和非抢占内核来讨论。显然,非抢占内核不存在竞争的问题,因为在临界区内的进程不会被打断,除非进程主动退出。对抢占内核来说,就需要硬件或者软件(算法)上的支持来实现临界区。
软件支持的一个例子是Peterson算法。Peterson算法的精髓在于用两个变量(或数组)来记录当前是否有进程位于临界区以及哪个进程位于临界区,这样通过在进入区检测并设置标记、退出区恢复标记可以实现临界区排他的特性。
硬件支持的方法是锁,从底层硬件的层面来看则是实现原子操作。进程在进入临界区前检测并申请锁,离开后释放锁。原子操作保证锁的正常运行。
原子操作
原子操作指的是不可再分的指令操作,即在执行原子操作时不可能被打断,要么原子操作没有执行,要么已经执行完毕。
原子操作的实现必须需要硬件的支持,操作系统仅仅是在硬件指令的基础之上进行一次封装。对于没有实现原子操作的硬件,则需要操作系统从软件算法层面进行支持。
linux下的实现
linux下原子操作的数据结构是atomic_t,其定义放在<linux/types.h>下:
typedef struct {
int counter;
}atomic_t;
可以看出原子操作仅支持int整形变量,为了与正常的整形区分,采用atomic_t来定义这个原子操作。