目录
一、进程同步
二、进程互斥
三、进程互斥的实现方法
3.1 软件实现
3.1.1 单标志法(存在的主要问题:违背“空闲让进”原则)
3.1.1.1 基本思想
3.1.1.2 单标志法的基本概念及执行流程
3.1.1.3 特点
3.1.2 双标志先检查法(违反了忙则等待原则)
3.1.2.1 基本思想
3.1.2.2 基本概念及执行流程
3.1.2.3 特点
3.1.3 双标志后检查法
3.1.3.1 算法思想
3.1.3.2 执行流程代码
3.1.4 Peterson算法
3.1.4.1 算法思想
3.1.4.2 算法步骤
3.1.4.3 两个进程并发运行时会带来什么后果
3.1.4.4 Peterson算法的特点
3.2 软件实现方法总结
3.3 硬件实现
3.3.1 中断屏蔽法
3.3.2 利用Test-and-Set指令实现互斥
3.3.3 利用Swap指令实现进程互斥
一、进程同步
知识点回顾:进程具有异步性的特征。异步性是指,各并发执行的进程以各自独立的、不可预知的速度向前推进。
进程同步:同步亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作。
二、进程互斥
进程的“并发”需要“共享”的支持。各个并发执行的进程不可避免地需要共享一些系统资源(比如内存,打印机、摄像头这样的I/O设备)。
两种资源共享
- 互斥共享方式。系统中的某些资源,虽然可以提供给多个进程使用,但一个时间段内只允许一个进程访问该资源。
- 同时共享方式。系统中的某些资源,允许一个时间段内由多个进程“同时”对它们进行访问。
我们把一个时间段内只允许一个进程使用的资源称为临界资源。许多物理设备(比如摄像头、打印机)都属于临界资源。此外还有许多变量、数据、内存缓冲区等都属于临界资源。
对临界资源的访问,必须互斥地进行。互斥,亦称间接制约关系。进程互斥指当一个进程访问某临界资源时,另一个想要访问该临界资源的进程必须等待。当前访问临界资源的进程访问结束,释放该资源之后,另一个进程才能去访问临界资源。
对临界区资源的互斥访问,可以在逻辑上分为如下四个部分:
进入区:负责检查是否可进入临界区,若可进入,则应设置正在访问临界资源的标志(可理解为“上锁”),以阻止其他进程同时进入临界区。
临界区:访问临界资源的那段代码。
退出区:负责解除正在访问临界资源的标志(可理解为“解锁”)。
剩余区:做其他处理。执行临界区操作之外的其他处理任务。
注意:
临界区是进程中访问临界资源的代码段。
进入区和退出区是负责实现互斥的代码段。
临界区也可称为“临界段”
思考问题: 如果一个进程暂时不能进入临界区,那么该进程是否应该一直占着处理机?该进程有没有可能一直进不了临界区?
为实现进程互斥地进入自己的临界区,可用软件方法,更多的是在系统中设置专门的同步机构来协调各进程间的运行。所有同步机制都应遵循下述四条准则:
(1) 空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区;
(2) 忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程必须等待;这种原则的特点是在一个进程试图进入临界区时,如果发现资源正被另一个进程占用,它不会立即让出 CPU,而是不断检查条件,直到资源可用为止。这种行为会导致 CPU 的时间片被浪费在空循环中,这就是“忙等待”。
(3) 有限等待。对请求访问的进程,应保证能在有限时间内进入临界区(保证不会饥饿);
(4) 让权等待。 当进程不能进入临界区时,应立即释放处理机,防止进程忙等待。【进程的忙等待(busy waiting)是指一个进程在等待某个条件满足时,持续地检查该条件,而不让出CPU时间。这个过程通常会导致浪费CPU资源,因为进程在等待期间不断地占用CPU,而不是进行其他有用的工作】
三、进程互斥的实现方法
如果没有进程互斥?会带来什么后果?
假设进程A、进程B在系统中并发地运行,如下:
先调度A上处理机运行
当A在使用打印机的过程中,分配给它的时间片用完了,接下来操作系统调度B让它上处理机运行
进程B也在使用打印机
结局:A、B的打印内容混在一起了。
这种情况是我们不愿意看到的,如果引入互斥,那么在进程A还没有执行完打印任务之前进程B是不可能上处理机运行的。
3.1 软件实现
单标志法、双标志先检查、双标志后检查、Peterson算法。
3.1.1 单标志法(存在的主要问题:违背“空闲让进”原则)
3.1.1.1 基本思想
单标志法(Single Flag Method)是一种简单的进程互斥实现方法,通常用于解决两个进程间的互斥问题。它基于一个标志变量来控制对临界区的访问,确保只有一个进程能进入临界区。
算法思想: 一个进程在访问完临界区后会把使用临界区的权限转交给另一个进程。也就是说每个进程进入临界区的权限只能被另一个进程赋子。
3.1.1.2 单标志法的基本概念及执行流程
标志变量:使用一个共享的标志变量来表示一个进程是否正在访问临界区。
互斥机制:通过设置和检查标志变量来实现互斥,确保在任何时刻只有一个进程能够进入临界区。
执行流程代码如下:
执行流程:
turn 的初值为0,即刚开始只允许0号进程进入临界区。若 P1先上处理机运行,则会一直卡在 ⑤。直到 P1的时间片用完,发生调度,切换 P0上处理机运行。
代码 ① 不会卡住 P0,P0可以正常访问临界区,在 P0访问临界区期间即时切换回 P1,P1依然会卡在 ⑤。只有 P0 在退出区将 turn 改为1后,P1才能进入临界区。
3.1.1.3 特点
-
简单易懂:单标志法的实现和理解都比较简单,适用于两个进程间的互斥问题。
-
缺点:
- 不适用于更多进程:单标志法只适用于两个进程。如果有更多进程,单标志法的实现将变得复杂且难以维护。
- 忙等待:进程在等待时会不断检查标志变量,可能导致忙等待,浪费CPU资源。
- 无法解决饥饿问题:如果没有适当的机制,可能会导致某些进程一直无法获得进入临界区的机会。
- 只能按 P0 → P1 → P0 →P1→.这样轮流访问。这种必须“轮流访问”带来的问题是,如果此时允许进入临界区的进程是 P0,而P0一直不访问临界区,那么虽然此时临界区空闲,但是并不允许P1访问【违反空闲让进的原则】。
3.1.2 双标志先检查法(违反了忙则等待原则)
3.1.2.1 基本思想
设置一个布尔型数组flag[ ],数组中各个元素用来标记各进程想进入临界区的意愿,比如“flag[0]=ture”意味着0号进程P0现在想要进入临界区。每个进程在进入临界区之前先检查当前有没有别的进程想进入临界区,如果没有,则把自身对应的标志flag[ i ]设为true,之后开始访问临界区。
3.1.2.2 基本概念及执行流程
- 标志变量:使用两个标志变量
flag[0]
和flag[1]
,分别表示进程P0和P1是否希望进入临界区。 - 回退机制:通过设置标志变量来指示进程是否希望进入临界区,并通过检查另一个进程的标志变量来确保互斥。
执行流程代码如下:
若P0和P1两个进程并发执行,若按照 ①➄➁➅③➆..的顺序执行,P0和 P1将会同时访问临界区。因此,双标志先检查法的主要问题是: 违反“忙则等待”原则。
原因在于,进入区的“检查”和“上锁”两个处理不是一气呵成的。“检查”后,“上锁”前可能发生进程切换。
3.1.2.3 特点
- 互斥性:只要一个进程在访问临界区,另一个进程就会被阻止,确保了互斥性。
- 进程饥饿:该方法可能会导致进程饥饿问题,即一个进程可能会长时间无法获得进入临界区的机会。
- 忙等待:进程在等待时会不断检查标志变量,导致忙等待,可能浪费CPU资源。
- 公平性:在某些实现中,如果处理得当,可以避免某些进程长时间等待的情况,从而提高公平性。
3.1.3 双标志后检查法
3.1.3.1 算法思想
双标志先检查法的改版。前一个算法的问题是先“检查”后“上锁”,但是这两个操作又无法一气呵成,因此导致了两个进程同时进入临界区的问题。因此,人们又想到先“上锁”后“检查的方法,来避免上述问题。
3.1.3.2 执行流程代码
若按照 ①⑤➁⑥...的顺序执行,P0和 P1将都无法进入临界区。因此,双标志后检查法虽然解决了“忙则等待”的问题,但是又违背了“空闲让进”和“有限等待原则,会因各进程都长期无法访问临界资源而产生“饥饿”现象。
两个进程都争着想进入临界区,但是谁也不让谁,最后谁都无法进入临界区。
3.1.4 Peterson算法
3.1.4.1 算法思想
结合双标志法、单标志法的思想。如果双方都争着想进入临界区,那可以让进程尝试“孔融让梨”(谦让)。做一个有礼貌的进程。
算法思想: Peterson算法通过两个共享变量和一个标志变量,协调两个进程对临界区的访问。它确保在任何时候,最多只有一个进程能够进入临界区,同时避免了“忙等待”问题。
3.1.4.2 算法步骤
进入区:1.主动争取;2.主动谦让;3.检查对方是否也想使用,且最后一次是不是自己说了“客气话”
原理分析
- 当
P0
和P1
想要进入临界区时,它们分别将自己的flag[i]
设置为true
,表示自己准备进入临界区。 - 然后它们会将
turn
设置为对方的值,表示让出优先权给对方。 - 每个进程在进入临界区前都会检查对方的
flag
和turn
值:- 如果对方没有准备进入临界区(
flag[j] == false
),那么可以进入临界区。 - 如果对方准备进入临界区,但轮到自己优先进入(
turn != j
),那么自己也可以进入。 - 如果对方准备进入并且对方优先(
flag[j] == true && turn == j
),则进程会等待,直到条件改变。
- 如果对方没有准备进入临界区(
我们还是使用小渣和老渣的例子来分析:
3.1.4.3 两个进程并发运行时会带来什么后果
按不同顺序穿插执行会发生什么?
①②③⑥⑦⑧....
①⑥②③....
①③⑥⑦⑧....
①⑥②⑦⑧.....
假设按照①⑥②⑦⑧.....顺序执行,会发生什么结果呢?
3.1.4.4 Peterson算法的特点
- 互斥性:通过两个进程的
flag
变量和turn
变量,可以确保在任何时刻,最多只有一个进程能进入临界区。 - 空闲让权:如果一个进程不打算进入临界区(
flag[i] = false
),则另一个进程可以立即进入临界区,而无需等待。 - 有界等待:进程不会无限期等待进入临界区,系统保证在有限时间内每个进程都可以进入临界区。
优点
- 简单实现:Peterson算法不依赖任何特殊的硬件指令或中断机制,仅通过共享变量实现进程间的同步。
- 互斥保证:能够有效防止多个进程同时进入临界区,避免数据竞争和不一致的问题。
- 公平性:没有进程会被无限期阻塞,保证了有限时间内进程的执行。
缺点
- 仅适用于两个进程:Peterson算法仅适用于两个进程的互斥访问问题。如果进程数大于两个,算法需要扩展。
- 忙等待:尽管Peterson算法解决了互斥问题,但它仍然需要等待循环(即“忙等待”),消耗 CPU 资源,效率不高。
总结
Peterson 算法用软件方法解决了进程互斥问题,遵循了空闲让进、忙则等待、有限等待 三个原则,但是依然未遵循让权等待的原则。
Peterson 算法相较于之前三种软件解决方案来说,是最好的,但依然不够好。
3.2 软件实现方法总结
3.3 硬件实现
虽然可以利用软件方法解决诸进程互斥进入临界区的问题,但有一定难度,并且存在很大的局限性,因而现在已很少采用。相应地,目前许多计算机已提供了一些特殊的硬件指令,允许对一个字中的内容进行检测和修正,或者是对两个字的内容进行交换等。可利用这些特殊的指令来解决临界区问题。
3.3.1 中断屏蔽法
与原语的实现思想相同,即在某进程开始访问临界区到结束访问为止都不允许被中断,也就不能发生进程切换,因此也不可能发生两个同时访问临界区的情况
1.基本思想:关中断是实现互斥的最简单的方法之一。在进入锁测试之前关闭中断,直到完成锁测试并上锁之后才能打开中断。这样,进程在临界区执行期间,计算机系统不响应中断,从而不会引发调度,也就不会发生进程或线程切换。由此,保证了对锁的测试和关锁操作的连续性和完整性,有效地保证了互斥。
总结为一幅图,如下图所示:
2.关中断的优缺点
该方法的优点是简单。 但是,关中断的方法存在许多缺点:0. 只适用于操作系统内核进程,不适用于用户进程(因为开/关中断指令只能运行在内核态,这组指令如果让用户随意使用会很危险)① 滥用关中断权力可能导致严重后果;② 关中断时间过长,会影响系统效率,限制了处理器交叉执行程序的能力;③ 关中断方法也不适用于多CPU 系统,因为在一个处理器上关中断并不能防止进程在其它处理器上执行相同的临界段代码。
3.关中断通常仅在以下场景中有效:
-
单处理器系统:在早期的单处理器系统中,关中断是一种常见的实现互斥的方式,因为在单处理器环境下,关中断可以完全阻止中断,从而确保临界区的安全访问。
-
操作系统内核代码:操作系统内核代码需要对硬件资源(如寄存器、缓冲区等)进行直接访问,并且可能需要在处理一些关键任务(如调度、驱动程序)时临时关闭中断。在这种情况下,关中断是合理的,因为它确保了操作系统能够控制对关键资源的独占访问。
4. 关中断为什么只适用于内核进程
关中断是一种比较粗放的同步机制,只有内核进程(或运行在内核态的代码)才能安全有效地使用它,原因包括以下几点:
-
内核态权限:关中断需要较高的权限(内核态权限),因为中断是操作系统用来管理硬件和进程切换的重要机制。用户态进程通常没有权限直接控制中断。如果允许用户态进程关中断,可能会导致系统的不稳定和安全风险。
-
影响系统整体性能:中断机制是操作系统中断点执行、响应外部设备、调度任务的重要手段。如果某个进程在用户态关中断,可能会导致整个系统无法响应外部事件(如键盘、网络或其他硬件设备的请求),这会影响整个系统的正常运行。
-
影响实时性和响应性:如果操作系统无法及时响应中断,会降低系统的响应能力,尤其在实时系统中,可能会导致关键任务被延迟执行。而在多任务操作系统中,关中断会影响调度程序,使得其他进程无法执行。
-
不适合多处理器系统:在多核处理器或多处理器系统中,关中断只能影响本处理器上的中断,但无法防止其他处理器上的进程访问临界区。因此,关中断这种方法在多处理器系统中效果有限,无法保证完全的互斥。
3.3.2 利用Test-and-Set指令实现互斥
这是一种借助一条硬件指令【“测试并建立”指令TS(Test-and-Set)】以实现互斥的方法。在许多计算机中都提供了这种指令。在有的地方称为TestAndSetLock指令,或称TSL指令。
1. Test-and-Set指令的基本原理
Test-and-Set指令通过硬件支持,确保对共享变量的读取和修改操作是原子的,避免了并发情况下可能出现的竞态条件(Race Condition)。它通常被用来实现锁,保证只有一个进程或线程能进入临界区,而其他进程或线程在锁定的情况下会等待。
Test-and-Set指令可以简单理解为以下伪代码:
如果 lock
原本是 false
,Test-and-Set 操作将其设置为 true
并返回 false
,while循环条件不满足,直接跳过循环,这意味着当前进程可以进入临界区,因为它成功获取了锁。
如果 lock
原本是 true
,Test-and-Set 操作仍将 lock
设为 true
并返回 true
,while循环条件满足,会一直循环,表示锁已经被其他进程或线程持有,当前进程需要等待。直到当前访问临界区的进程在退出区进行“解锁”。
2. 工作流程
假设有多个进程,Test-and-Set的流程如下:
-
进程想进入临界区:它执行 Test-and-Set 指令,检查并设置锁变量
lock
。- 如果锁是
false
,进程设置lock
为true
,并进入临界区。 - 如果锁已经是
true
,进程会继续忙等待,直到锁变为false
。
- 如果锁是
-
进程执行临界区代码:只有一个进程能成功进入临界区,因为 Test-and-Set 是原子操作,其他进程会在忙等待循环中。
-
进程离开临界区:它将
lock
变量设为false
,其他等待的进程可以再次执行 Test-and-Set 指令,检查并获取锁。
3. Test-and-Set实现互斥锁示例
// 假设lock变量被初始化为false
bool lock = false;
void acquire_lock() {
while (TestAndSet(&lock)) {
// 如果锁已经被其他进程持有,忙等待
}
}
void release_lock() {
lock = false; // 释放锁
}
void critical_section() {
acquire_lock(); // 获取锁
// 进入临界区
// ...
release_lock(); // 离开临界区
}
在上面的代码中,acquire_lock()
会一直执行 TestAndSet()
操作,直到它成功获取到锁(lock == false
时进入临界区)。当临界区代码执行完毕后,通过 release_lock()
将 lock
变量重新设为 false
,允许其他进程进入临界区。
4. Test-and-Set的特点
优点:
- 简单易实现:Test-and-Set指令通过硬件支持,确保对共享变量的操作是原子的,逻辑上相对简单。
- 原子性:Test-and-Set 是原子操作,能够在多处理器环境下保证互斥访问。
- 适用于多核系统:由于 Test-and-Set 是硬件级的原子操作,它能在多核处理器中使用,确保多个核同时访问共享资源时的同步。
缺点:
- 忙等待(Busy Waiting):如果锁已经被占用,其他进程会不断执行 Test-and-Set 操作,导致忙等待,消耗 CPU 资源。
- 可能导致死锁或活锁:如果多个进程同时竞争锁资源且没有其他机制控制访问次序,可能会出现死锁或活锁的现象。
- 公平性问题:Test-and-Set 操作没有内置的公平性机制,进程可能会长时间无法获取锁,导致饥饿现象。
5. Test-and-Set的应用场景
- 内核态的锁实现:Test-and-Set常用于操作系统内核中实现简单的自旋锁(Spinlock),尤其在多处理器系统中,通过自旋等待来保证临界区的互斥。
- 硬件同步原语:Test-and-Set是硬件级的同步原语,许多 CPU 指令集(如 x86、ARM)都提供了 Test-and-Set 指令,以支持多线程或多进程的并发控制。
6. 总结
Test-and-Set 指令是一种硬件支持的原子操作,用于实现多进程或多线程之间的互斥。它能够保证在并发环境下对共享资源的安全访问,适用于内核态或操作系统中实现锁。然而,它的忙等待特性使得它不适用于高负载场景,现代系统往往使用信号量、互斥锁等更高效的同步机制来替代 Test-and-Set。
3.3.3 利用Swap指令实现进程互斥
Swap指令是一种经典的同步原语,用于实现多进程或多线程之间的互斥和同步。与 Test-and-Set指令 类似,Swap指令通过硬件支持,确保对共享变量的操作是原子的,从而避免竞态条件。它通常用于构建低级别的锁机制,保证共享资源的互斥访问。
该指令称为对换指令,在Intel 80x86中又称为XCHG指令,用于交换两个字的内容。
1. Swap指令的基本原理
Swap指令的核心是交换两个变量的值,并确保交换操作是原子的,即整个交换过程不能被中断或打断。
Swap指令的伪代码如下:
void Swap(bool *a, bool *b) {
bool temp = *a;
*a = *b;
*b = temp;
}
a
表示锁的状态,如果锁是false
,表示没有进程持有锁,临界区可进入;b
是一个变量。
通过这种交换操作,进程能以原子的方式检查并修改锁的状态,确保其他进程不能同时访问临界区。
在获取锁的过程中,Swap操作会将 lock
和 b/old
进行原子的交换:
- 如果
lock
之前是false
,表示没有其他进程持有锁,交换后lock
变为true
,而b
变为false
,这意味着当前进程成功获取锁,进入临界区。 - 如果
lock
之前是true
,表示已经有其他进程持有锁,交换后lock
依旧为true
,而b
依旧为true
,这意味着当前进程没有获取锁,需要继续忙等待。
2. Swap指令的特点
优点:
- 原子性:Swap指令是硬件支持的原子操作,能够保证多处理器系统中的互斥访问,即使有多个处理器同时执行,也能确保共享变量的正确更新。
- 简单性:Swap指令的逻辑相对简单,适合用于实现低级别的锁机制。
- 硬件支持:许多处理器架构(如 x86、ARM)提供了 Swap指令,用于操作系统或应用程序中的锁实现。
缺点:
- 忙等待(Busy Waiting):如果锁已经被其他进程持有,进程会在循环中执行 Swap 操作,导致忙等待,消耗 CPU 资源。
- 可能导致死锁或活锁:多个进程如果同时竞争锁资源,可能会出现活锁现象,即进程不停交换锁的状态,却无法实际进入临界区。
- 不公平性:Swap指令没有内置公平性机制,可能导致某些进程长时间无法获得锁,出现饥饿现象。
3. Swap指令的应用场景
- 操作系统内核中的锁实现:Swap指令通常用于操作系统内核中实现自旋锁(Spinlock)或其他低级别的同步机制。在内核态中,锁的持有时间一般较短,忙等待的开销可以被接受。
- 多处理器系统中的同步:Swap指令适合用于多处理器环境,因为它是硬件原子操作,能够保证多个处理器对共享资源的安全访问。
4. 总结
Swap指令是一种硬件级的原子操作,用于实现多进程或多线程的互斥访问。它通过交换锁变量的值来实现对共享资源的互斥访问,确保进程间的同步。尽管 Swap指令提供了简单有效的同步机制,但由于忙等待问题,它在现代系统中被更加高级的同步机制(如互斥锁、信号量等)所取代。不过,它在操作系统内核和硬件同步中依然有重要的应用。