一、概述
操作系统设计中的核心问题是进程和线程的管理:
- 多道程序设计技术: 管理单处理器系统中的多个进程
- 多处理器技术: 管理多处理器系统中的多个进程
- 分布式处理器技术: 管理多台分布式计算机系统中多个进程的执行
并发是所有问题的基础,也是操作系统的基础,支持并发的基本需求是加强互斥能力,拥有三种解决方法:信号量、管程、消息传递。
并发术语:
术语 | 解释 |
---|---|
原子操作 | 一个函数、动作由一个 / 多个指令实现,对外不可见。即指令序列要么一次执行,要么都不执行。 |
临界区 | 一段代表共享资源的代码,一次只能有一个进程访问该资源。 |
死锁 | 两个 / 两个以上的进程因每个进程都在等待其他进程做完某些事情而不能继续执行的情形。 |
活锁 | 两个 / 两个以上的进程为响应其他进程中的变化而持续改变自己的状态但不做有用工作的情形。 |
互斥 | 一个进程访问临界区,而其他进程不能访问的情形 |
竞争条件 | 多个进程 / 线程在读写一个共享数据时,结果依赖于他们执行相对时间的情形 |
饥饿 | 一个可运行进程尽管能执行,却被无限期忽视不被调度的情形 |
二、原理
单处理器多道 程序设计系统中,进程会被交替执行。在多处理器系统中,不仅可以交替执行进程,而且可以重叠执行进程。
无论哪种方式,其中都存在以下问题:
- 全局资源共享难以保证有序性、可见性、一致性
- 操作系统很难对资源进行最优化分配:处理不慎容易导致死锁
- 定位程序设计错误十分困难:结果通常不确定、不可再现
1. 竞争条件
竞争条件发生在多个 进程 / 线程 读取数据时,最后结果取决于多个进程的指令执行顺序。
2. 关注问题
- 处理器时间:调度功能
- 存储器:大多数采用虚存方案
- 文件
- I/O 设备
3. 进程交互
感知程度 | 关系 | 进程对其他进程影响 | 潜在的控制问题 |
---|---|---|---|
进程间不知道对方存在 | 竞争 | 进程的结果与另一进程的活动无关 进程的执行时间可能会受到影响 | 互斥 死锁(可复用资源) 饥饿 |
进程间接知道对方存在(共享对象) | 通过共享合作 | 一个进程的结果可能取决于从另一进程获得的信息 进程的执行时间可能会受到影响 | 互斥 死锁(可复用资源) 饥饿 数据一致性 |
进程直接知道对方存在(通信原语) | 通过通信合作 | 一个进程的结果可能取决于从另一进程获得的信息 进程的执行时间可能会受到影响 | 死锁(可消耗资源) 饥饿 |
临界资源 & 临界区:
两个/多个进程需要访问一个不可共享的资源,每个进程都给该设备发命令,接收状态信息,发送、接收数据,这类资源称为 临界资源。使用临界资源的部分程序称为程序的 临界区
进程交互的控制问题:
- 互斥(mutual exclusion):
一次只允许一个资源在临界区中 - 死锁(deadlock):
每个进程都持有对方需要的资源,并且不主动释放,形成逻辑链条并持续等待 - 饥饿(starvation):
进程原则上可以被分配资源,却被无限期地拒绝访问资源
进程交互三种情形:
- 进程间的资源竞争
当并发进程竞争使用同一资源时,它们之间会发生冲突 - 进程间通过共享合作
通过共享进行合作,进程间在互相并不确切知道对方的情况下进行交互。数据保存在资源中,涉及有关互斥、死锁、饥饿问题,并且只有写操作必须保持互斥,此外必须保证数据一致性。 - 进程间通过通信合作
通常,通信可由各种类型的消息组成 ,发送、接收消息的原语由程序设计语言、操作系统内核提供。这类合作无需互斥,但仍存在死锁、饥饿问题。
4. 互斥要求
- 强制实施互斥:相同资源、共同对象的临界区有关的所有进程中,一次只允许一个进程进入临界区。
- 在非临界区停止的进程不能干涉其他进程。
- 不允许需要访问临界区的进程出现死锁、饥饿,进程在临界区中驻留时间有限。
- 没有进程在临界区中,需进入的进程能立即进入。
- 对相关进程的执行速度、处理器数量没有人任何要求、限制。
三、常用并发机制
并发机制 | 说明 |
---|---|
信号量 | 用于进程间传递信号的一个整数值。 |
二元信号量 | 只取0和1的信号量 |
互斥量 | 类似于二元信号量,区别在于为其加锁(设定值0)、解锁(设定值1)的进程必须为一个进程 |
条件变量 | 一种数据类型,用于阻塞 进程 / 线程,直至特定条件为真 |
管程 | |
事件标志 | |
信箱 / 消息 | 两个进程间交换信息的一种方法,也可用于同步 |
自旋锁 | 一种互斥机制,进程在一个无条件循环中执行,等待锁变量的值可用 |
1. 信号量
信号量:两个或多个进程可以通过简单的信号进行合作,可以强迫一个进程在某个位置停止,直到接收一个特定的信号。
基本原理:
为达到效果,将信号量视为一个值为整数的变量,并且定义了信号量的三个操作:
- 初始化:将信号量初始化为 非负整数。
- 增加:semWait 操作使信号量减1。
若值变为负数,则阻塞执行 semWait 的进程,否则继续执行。 - 减少:semSignal 操作使信号量加1。
若值小于等于0,则被 semWait 操作阻塞的进程解除阻塞。
流程解释:
- 开始时,信号量值非负
- 若值为正数:信号量值等同于 在发出 semWait 操作后可立即继续执行 的进程数量
- 若值为0:发出 semWait 操作的下一个进程会被阻塞,此时信号量为负,之后的 semWait 操作会使得负值绝对值加大
- 若值为负数:信号量值代表正在等待解除阻塞的进程数量,此时,每个 semSignal 操作都会使等待进程中的一个进程解除阻塞
信号量机制示例:
二元信号量
二元信号量的值只能是0或1,可由下面三个操作定义:
- 二元信号量可以初始化为0或1
- semWait 操作、检查 信号量的值:
若值为0,则进程执行 semWait 就会受阻
若值为1,则受阻进程会被唤醒
若没有进程受阻,则值设置为1 - semSignal 操作、检查 是否有进程在该信号上受阻:
若有进程受阻,则通过semWait 操作,受阻进程被唤醒
若没有进程受阻,则值设置为1
非二元信号量又称为 计数信号量(counting semaphore)、一般信号量(general semaphore)
采用先进先出(FIFO,被阻塞时间最久的进程最先释放)策略的信号量称为 强信号量(strong semaphore),若没有规定顺序则称为 弱信号量(weak semaphore)。强信号量由于机会均等 保证不会饥饿,而弱信号量无法保证。
互斥锁
互持锁(mutex)是一个编程标记位,用来获取、释放一个对象,当需要的数据不能被分享、处理,进而导致在系统的其他地方不能同时执行时,互斥锁设置为锁定(设定值为0)以阻塞其他程序使用数据。当 数据不再需要 / 程序结束运行 时,互斥锁设置为非锁定(设定值为1),且加锁、解锁需为同一进程。
2. 管程
管程,由一个/多个过程、一个序列化布局、局部数据组成的软件模块。
特点:
- 局部数据变量只能被管程的过程访问,任何外部过程都不能访问。
- 一个进程通过调用管程的一个过程进入管程。
- 任何时候,只允许一个进程在管程中执行,调用管程的其他进程将被阻塞,等待可用。
管程通过 条件变量(condition variable) 支持同步,这些条件变量包含在管程中,并且只有在管程中才能被访问。有两个函数可以操作该变量:
- cwait©:调用进程的执行在 c条件 上阻塞,管程可被其他进程使用
- csignal©:恢复在 cwait 之后因某些条件而被阻塞的进程。若有多个则任选其一,否则什么也不做
管程的两个方法与信号量不同,如果管程中的一个进程发信号,但没有在这个条件变量上等待,则丢弃信号。
管程结构:
上图是一个管程的结构,尽管一个进程可以通过调用管程的任何一个过程进入管程,但我们仍然可视管程只有一个进程可以进入。其他hi图进入管程的进程被阻塞并加入等待管程的可用的进程队列中。
- 当一个进程在管程中时,它可能会通过发送 cwait(x) 把自己暂时阻塞在 x条件 上,随后它被放入等待条件改变以重新进入管程的进程队列中,在 cwait(x) 调用的下一条指令开始恢复执行。
- 若在管程中执行的一个进程发现 条件x 发生了变化,则发送 csignal(x),通知响应的条件队列条件已改变。
3. 消息传递
进程交互两个基本要求:同步、通信,同步需要进程间交换信息,而消息传递则可以提供这种方法。
消息传递的实际功能以一对原语的形式提供(最小操作集):
send(destination,message)
receive(source,message)
- 一个进程以消息(message)形式给另一个指定的目标(destination)进程发送信息
- 进程通过执行 receive 原语接收信息,receive 原语中指明了 源进程(source)、消息(message)
同步:
- 阻塞 send,阻塞 receive(会合rendezvous):
发送者、接收者都被阻塞,直到完成信息投递 - 无阻塞 send,阻塞 receive:
尽管发送者可以继续,但接收者会被阻塞,直至请求的消息到达。它允许一个进程给各个目标进程尽快地发送一条/多条消息 - 无阻塞 send,无阻塞 receive:
不要求任何一方等待
寻址:
直接寻址(direct addressing):send 原语包含目标进程的标识号,而 receive 原语要么显式地指定源进程,要么不指定所期望的源进程。
间接寻址(indirect addressing):此时,消息不直接发送,而是通过临时保存消息的队列通信,这些队列称为 信箱(mailbox)
- 一对一(one-to-one):在两个进程间建立专用的通信链接,隔离它们的交互,避免干扰
- 多对一(many-to-one):在 C/S 间的交互时,一个进程给其他进程提供服务,信箱被称为 端口 (port)
- 一对多(one-to-many):适用于一个发送者、多个接收者,用于在一组进程间广播一条消息/某些消息的应用程序
- 多对多(many-to-many):可让多个服务进程对多个客户进程提供服务
消息格式:
消息格式取决于机制的目标,以及该机制运行在一台计算机上还是运行在分布式系统,有些操作系统支持变长消息,下图是一种经典的变长消息格式:
消息头包含消息 源&目标标识符、长度域、判定各种消息类型的类型域,可能还有额外的控制信息(消息链表指针域;记录源之间传递消息数量、顺序、序号;优先域)。
排队规则:
最简单的规则是 先进先出,除此之外还可以根据 发送者、接收者 指定消息的优先级。
互斥:
一种实施互斥的消息传递方式:
假设使用阻塞 receive 原语、无阻塞 send 原语,且一组并发进程共享一个信箱 box,该信箱可供所有进程在发送、接收消息时使用,并初始化为一个无内容的消息。
希望进入临界区的进程首先试图接收一条消息,若信箱为空,则阻塞该进程;一旦进程获得消息,它就执行其临界区,然后把消息放回信箱。因此,消息函数可视为在进程间传递的一个令牌。