2.1前驱图和程序执行
前驱图
定义
- 是指一个有向无循环图,用于描述进程之间执行的先后顺序。
- 进程(程序)之间的前趋惯关系可用“→”表示,如果进程Pi和进程Pj之间存在着前趋关系,可表示为(Pi,Pj)∈→,也可以写作Pi→Pj。
- 没有前趋的节点称为初始节点,没有后继的节点称为终止节点。
程序顺序执行
- 程序的顺序执行是指若干个程序或程序段之间必须严格按照某种先后次序来执行,仅当前一长须或程序段执行完后,才能执行后面的程序或程序段。
- 程序的顺序执行特征:顺序性,封闭性,可再现性
- 优点:方便程序员,系统资源利用率低
程序并发执行
- 程序的并发执行是指两个或两个以上的程序或程序段可在一段时间间隔内同时执行
- 极大提高了资源利用率和系统吞吐量
- 程序并发执行的特征:间断性,失去封闭性,不可再现性
2.2进程的描述
典型的进程定义
- 进程是程序的一次执行
- 进程是以恶搞程序及其数据在处理机上顺序执行时所发生的活动
- 进程是具有独立功能的程序在以恶搞数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位
- 进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位
- 进程实体由程序段,相关的数据段和PCB三部分构成,一般简称为进程
- PCB:进程控制块,是一个专门的数据结构,系统利用他来描述进程的基本情况和活动过程,进而控制和管理进程。
进程的特征
- 动态性——进程的最基本特征
- 并发性——程序在建立进程后并发运行
- 独立性——可以独立运行,是系统进行资源分配和调度的独立单位
- 异步性——进程以不可预知的速度向前推进
进程和程序的对比
- 从定义上看,进程是程序处理数据的过程,而陈旭是一组指令的有序集合
- 进程具有动态性、并发性、独立性和异步性等,而程序不具有这些特性
- 从进程结构特性上看,它包含程序、数据(栈)和PCB
- 进程和程序并非一一对应:通过多次执行,一个程序可对应多个进程;通过调用关系,一个进程可执行多个程序
- 进程可以理解为正在进行(运行)的程序
进程的三种基本状态
- 就绪状态:进程分配到必要的资源,等待获得CPU执行的状态(就绪队列)
- 执行状态:进程已经分配到CPU,正在CPU上执行时的状态
- 阻塞状态(等待状态,封锁状态):正在执行的进程由于等待某事件的发生而暂时无法继续执行时的状态(阻塞队列)
三种状态的转换
五种状态
- 创建状态:进程创建尚未完成之前,不能被CPU调用的状态
- 终止状态:进程运行结束,系统已收回了除进程控制块外的所有资源,等待其他进程从它的进程控制块中收集相关信息时的状态
五种状态的转换
挂起操作和进程状态的转换
挂起操作
- ”挂起“的实质就是使进程不能继续执行
- 为什么要挂起
-
- 终端用户的需要
- 父进程的请求
- 负荷调节的需要
- 操作系统的需要
进程状态的转换
- 被挂起的进程处于静止状态,没被挂起的进程处于活动状态
- 挂起原语:Suspend,激活原语:Active
- 静止就绪和活动就绪,静止阻塞和活动阻塞
进程控制块(PCB)
- 作为独立运行基本单位的标志(进程存在于系统中的唯一标志)
- 能实现间断性运行方式——保存CPU现场信息
- 提供进程管理所需的信息(描述进程所需资源)
- 提供进程调度所需的信息(进程状态,优先级)
- 实现与其他进程的同步与通信
PCB中的信息
- 进程表示符:包括用户标识符(UID),家族信息等外部标识以及内部标识符(PID)
- 处理机状态:处理机的各种寄存器的内容,用于在CPU切换时保护现场信息和恢复现场信息
- 进程调度信息:包括进程状态,优先级,时间片,等待事件等相关信息
- 进程控制信息:程序和数据地址,进程同步和通信机制,资源清单和连接指针
PCB的组织方式
- 线性方式
- 链接方式
- 索引方式
2.3进程控制
- 系统使用一些具有特定功能的程序来创建,撤销进程以及完成进程各状态间的转换,从而达到多进程,高效率,并发执行和协调,实现资源共享的目的
- 进程控制时操作系统的内核通过原语来实现
- 原语:由若干条指令构成的“原子操作”过程,用于完成特定的功能,作为一个整体而不可分割——要么全部都完成,要么全都不做
操作系统的内核
- 在设计OS时把一些与硬件紧密相关的模块,各钟常用设备的驱动程序以及运行频率较高的模块,安排在紧靠硬件的软件层中,并使他们常驻内存,以提高OS的运行效能,通常把这部分叫OS的内核。
处理机运行的状态
- 系统态:又称为管态,内核态。他具有较高的特权,能执行一切指令,访问所有寄存器和存储区,传统的OS都在内核太运行
- 用户态:又称为目态。具有较低特权的运行状态,仅能执行规定的指令,访问指定的寄存器和存储区。一般情况线下,应用程序只能运行在用户态。
进程的创建
进程创建的步骤
进程的终止
- 正常结束
- 异常结束
- 外界干预
进程终止的过程
进程的阻碍和唤醒
- 向系统请求资源失败
- 等待某种操作的完成
- 前趋进程尚未完成
- 当前进程无新工作可做
进程阻碍的过程
进程唤醒的过程
进程的挂起与激活
- 活动就绪→静止就绪,活动阻塞→静止阻塞,执行→转向调度程序重新调度
- 由外存调入内存,静止就绪→活动就绪,静止阻塞→活动阻塞,根据需要决定是否由调度程序进行重新调度
2.4进程同步
进程同步的基本概念
- 进程同步的主要任务:协调多个并发程序,按照一定规则(或时序)共享系统资源
- 间接制约关系(进程互斥):源于临界资源共享
- 直接制约关系(进程同步):源于进程合作
区别可以从进程互斥和进程同步来看
进程互斥
进程互斥是指当一个进程进入临界区使用临界资源时,其他进程必须等待。当占用临界资源的进程退出临界区后,另一个进程才被允许使用临界资源。
若要实现各进程对临界资源的互斥访问,则需要保证各进程互斥地进入自己的临界区。进程在进入临界区之前,应先对临界资源进行检查,确认该资源是否正在被访问。若临界资源正被其他进程访问,则该进程不能进入临界区;若临界资源空闲,该进程便可以进入临界区对临界资源进行访问,并将该资源的标志设置为正在被访问。因此,进程访问临界资源前,应增加一段用于进行上述检查的代码,这段代码称为进入临界区;临界资源访问结束后,也要增加一段用于将临界资源标志恢复为未被访问的代码,这段代码称为退出临界区。临界区的框架如下:
do{
进入临界区(检测资源是否正在被访问)
访问临界资源
退出临界区(将访问资源恢复为未被访问)
其余代码
}while(1);
进程同步
进程同步是指多个进程为了合作完成同一个任务,在执行次序上相互协调、相互合作,在一些关键点上还需要相互等待或相互通信。
进程同步的例子在现实生活中随处可见,例如司机与售票员的关系。公共汽车的司机负责开车和到站停车,售票员负责售票和开关车门,他们之间是相互合作、相互配合的。例如车门关闭后才能启动,到站停车后才能打开车门,即”启动汽车“在”关闭车门“之后,而”打开车门“在”到站停车“之后。
临界资源
- 进程以互斥方式访问的资源
同步机制应遵循的规则
- 空闲让等
- 忙则等待
- 有限等待
- 让权等待
硬件实现同步机制——关中断
缺点:
- 可能增加系统风险
- 只能用于单处理机系统
硬件实现同步机制-Test-and-Set(TS或TSL)
先记录,再上锁,再检查
boolean TS(boolean *lock) // 这样的写法表明这是一个 C 语言中的函数声明
/*boolean:这表明函数的返回类型是 boolean,即布尔值。在一些编程语言中,布尔类型用于表示真(true)或假(false)的逻辑值。
TS:这是函数的名称。通常,函数的名称用来描述函数的功能或行为。在这里,TS 可能表示 "Test-and-Set",表明这个函数执行了测试并设置操作。
(boolean *lock):这是函数的参数列表。它表明函数接受一个名为 lock 的指针,该指针指向一个布尔值。这种参数列表的形式表示函数将通过引用修改传递给它的参数,因此在函数内对 lock 的修改将影响调用它的地方。
*/
{
boolean old; // 用于保存锁的原始状态
old=*lock; //读取锁的当前状态
*lock=TRUE; //将锁的状态设置为 TRUE,即锁定
return old; //返回锁在调用之前的状态
}
这是一个典型的 Test-and-Set(TS)原子操作,用于实现互斥锁。这个函数接受一个指向布尔值的指针作为参数,将锁的当前状态读取到 old 变量中,然后将锁的值设置为 TRUE,最后返回 old,即锁在调用之前的状态。
do{
// 一些代码...
while(TS(&lock)); // 自旋锁,等待锁被释放
// 进入临界区
// 在这里执行需要互斥访问的代码
lock=FALSE; // 释放锁
// 退出临界区后的剩余代码
}while(TURE);
这段代码看起来是一个典型的使用自旋锁的结构,它用于实现对临界区的互斥访问。
1.do { ... } while (TRUE);:这是一个无限循环,表示代码段将一直执行。
2.while (TS(&lock));:这是一个自旋锁。TS 是一个函数或宏,它用于测试并设置锁。如果锁已被占用(lock 的值为真),则会一直循环等待,直到锁被释放(lock 的值为假)才会退出循环。
3.临界区:在获取到锁之后,进入临界区,执行需要互斥访问的代码。在这个区域内,通过锁来确保同时只有一个线程可以执行。
4.lock = FALSE;:在临界区代码执行完毕后,释放锁,将 lock 的值设置为假,表示锁已经被释放,其他线程可以进入临界区。
5.剩余区:在退出临界区后,执行剩余的代码。
总体来说,这段代码使用自旋锁实现了对临界区的互斥访问,确保同一时刻只有一个线程可以执行临界区内的代码。请注意,这种自旋锁的实现方式可能会导致一些性能上的问题,因为线程在等待锁时会一直循环检查,可能会浪费 CPU 资源。
硬件实现同步机制-Swap
void Swap(boolean *a,boolean *b)
{
boolean temp; // 临时变量用于保存 a 的值
temp=*a; // 将 a 的值保存到 temp 中
*a=*b; // 将 b 的值赋给 a
*b=temp; // 将 temp 中保存的 a 的值赋给 b
}
这是一个用于交换两个布尔值的函数,通常被称为"Swap"函数。这种函数通常用于交换两个变量的值,无论这些变量是什么类型
1.temp = *a;:将指针 a 指向的变量的值赋给临时变量 temp。
2.*a = *b;:将指针 b 指向的变量的值赋给指针 a 指向的变量,实现了变量值的交换。
3.*b = temp;:将临时变量 temp 的值赋给指针 b 指向的变量,完成了变量值的交换。
这样写的好处是,通过传递指针,函数可以直接修改调用者提供的变量的值,而不是仅仅在函数内部操作参数的副本。这种方式可以在函数外部观察到变量值的真正变化,实现了变量值的交换。
do{
key=TRUE; // 设置 key 为 TRUE,表示当前线程有意进入临界区
do{
Swap(&lock,&key); // 尝试将 lock 和 key 的值交换,如果交换成功(key 变为 FALSE),则退出循环
}while(key!=FALSE);
// 进入临界区
// 在这里执行需要互斥访问的代码
lock=FALSE; // 释放锁
// 退出临界区后的剩余代码
}while(TRUE);
1.do { ... } while (TRUE);:这是一个无限循环,表示代码段将一直执行。
2.key = TRUE;:设置 key 为 TRUE,表示当前线程有意进入临界区。
3.do { ... } while (key != FALSE);:内层循环使用 Swap 函数不断尝试将 lock 和 key 的值交换。如果交换成功,即 key 变为 FALSE,则退出内层循环。这个循环的目的是等待 lock 变为 FALSE,表示锁已经被释放。
4.临界区:在获取到锁之后,进入临界区,执行需要互斥访问的代码。
5.lock = FALSE;:在临界区代码执行完毕后,释放锁,将 lock 的值设置为 FALSE,表示锁已经被释放,其他线程可以进入临界区。
6.剩余区:在退出临界区后,执行剩余的代码。
这段代码的核心是通过 Swap 函数来实现自旋锁,确保临界区内的代码只能被一个线程执行。不过,这种自旋锁的实现方式可能会导致性能问题,因为线程在等待锁时会一直循环检查。
信号量机制
- 信号量:表示系统中某种资源可用的数量的一个变量
- 整形信号量:资源数目,只有初始化和P,V操作
- 记录型信号量:采用记录型数据结构
- 信号量集:每次申请N类资源,每类资源申请M个
整型信号量
- 初始化操作:非负整数
- P操作:wait(S)原语
- V操作:signal(S)原语
wait( S)
{
while( S≤0);
S--;
}
signal(S)
{
S++ ;
}
记录型信号量
typedef struct{
int value; // 信号量的值,用于控制资源的访问
structure PCB *list; // 指向进程控制块(Process Control Block)链表的指针
} semaphore ;
- value: 初始化为一个非负整数值,表示空闲资源总数.
- value若为非负值表示当前的空闲资源数,若为负值其绝对值表示当前等待临界资源的进程个数。
- list:进程链表指针。
1.int value;:这是信号量的值,通常用于控制对共享资源的访问。信号量的值可以为任意整数,具体的含义和用法取决于信号量的应用场景。
2.struct PCB *list;:这是一个指向进程控制块链表的指针。进程控制块是操作系统中用于管理进程信息的数据结构,包含了进程的状态、程序计数器、寄存器值等信息。在这里,指针指向了一个包含了等待该信号量的进程的进程控制块链表。
这个数据结构的目的是提供一个抽象的、结构化的方式来管理信号量,使得在程序中更容易使用信号量进行线程同步和进程同步的操作。
示例
wait(semaphore *S){
S.value --; // 减小信号量的值
if (S.value<0) block(S.list); // 如果信号量的值小于0,阻塞当前线程(将其加入阻塞队列)
}
wait(semaphore *S):该函数将信号量 S 的值减小,然后检查新值是否小于0。如果是,表示有线程需要等待(信号量的值为负),则调用 block(S->list) 阻塞当前线程,将其加入信号量的阻塞队列(PCB链表)中。
阻塞当前线程: 如果信号量的值小于0(表示没有足够的资源可用),则调用 block(S.list) 阻塞当前线程。这个线程被加入到阻塞队列中等待资源。
signal(semaphore *S){
S.value ++; // 增加信号量的值
if (S.value<=0) wakeup(S.list); // 如果信号量的值小于等于0,唤醒一个被阻塞的线程(从阻塞队列中移除并加入就绪队列)
}
signal(semaphore *S):该函数将信号量 S 的值增加,然后检查新值是否小于等于0。如果是,表示有线程被阻塞等待该信号量,需要唤醒其中一个。调用 wakeup(S->list) 从信号量的阻塞队列中移除一个线程并加入就绪队列,使其可以继续执行。
唤醒阻塞的线程: 如果信号量的值小于等于0(表示有线程在等待资源),则调用 wakeup(S.list) 唤醒一个被阻塞的线程。这个线程从阻塞队列中移除并加入就绪队列。
AND型信号量
- 进程需要两种或更多种资源,当这些资源同时得到后,进程才能继续执行。
Swait(S1,S2,……,Sn){
while(TRUE){
if(Si>=1&&……&&Sn>=1){
for (i=1;i<=n;i++)Si--;
break;
}
else{
place the process in the waiting queue associated with the first Si found with Si<1,and set the program count of this process to the beginning of Swait operation
}
}
}
1.S1, S2, ..., Sn:一组信号量,表示 n 个资源。
2.while (TRUE):无限循环。
3.if (S1 >= 1 && ... && Sn >= 1):检查所有信号量是否都满足条件。
4.for (i = 1; i <= n; i++) { Si--; }:如果所有信号量都满足条件,减小每个信号量的值。
5.break;:跳出循环,表示成功获取了资源。
else 部分:如果有任何一个信号量不满足条件,将当前进程放入等待队列,并将程序计数器设置为 Swait 操作的开始位置。这样当某个信号量满足条件时,该进程将会被唤醒,重新执行 Swait 操作。
信号量集
- 进程一次可申请多类资源;每类资源可申请多个。
- Swait(S1,t1,d1,……,Sn,tn,dn)表示信号量S1代表的资源大于等于分配下限t1才可以分配,分配量为d1,……
- Ssignal(S1,d1,……,Sn,dn):释放S1代表的资源d1个。
特殊的信号量集
- Swait(S,d,d):信号量集中只有一个信号量S,允许它每次申请d个资源,当现有资源数少于d时,不予配。
- Swait(S,1,1):S>1,则为记录型信号量;S=1,为互斥信号量。
- Swait(S1, 1,1,S2,1,1, …,Sn,1,1):表示AND型信号量。
- Swait(S,1,0):准入开关。
利用信号量实现进程互斥
- 为临界资源设置一个互斥信号量mutex,初值为1:
semaphore mutex=1
- 在每个进程中将临界区代码置于wait(mutex)和signal(mutex)原语之间
利用信号量实现前趋关系
- Pi→Pj,在Pi和Pj之间设置一个初值为0的公用信号量S
- 将signal(S)放在Pi后面
- 将wait(S)放在Pj前面
semaphore S=0;
P1() {S1;signal (S);}
P2() {wait(S); S2;}
main() {
cobegin
P1();P2();
coend
}
1.semaphore S = 0;:初始化一个信号量 S 的值为 0。这个信号量将用于协调 P1 和 P2 的执行。
2.P1() { S1; signal(S); }:P1 进程执行 S1 操作,然后通过 signal(S) 释放信号量 S。signal 操作用于增加信号量的值。
3.P2() { wait(S); S2; }:P2 进程执行 wait(S) 操作,等待信号量 S 的值变为大于等于 1。一旦信号量 S 的值满足条件,P2 就可以继续执行 S2 操作。wait 操作用于等待信号量的值满足某个条件,如果不满足条件,则阻塞进程。
4.main() { cobegin P1(); P2(); coend }:在主程序中,使用 cobegin 和 coend 表示 P1 和 P2 是并发执行的。这意味着它们可以同时运行。
5.这个模型中,P1 和 P2 通过信号量 S 进行协同工作。P1 执行完 S1 操作后释放了信号量 S,使得 P2 可以继续执行 S2 操作。这种机制可以用于同步不同进程之间的操作,确保它们按照特定的顺序执行。
cobegin
// 并发执行的代码块1
// 例如:P1();
// 并发执行的代码块2
// 例如:P2();
coend
1.在这个结构中,cobegin 和 coend 之间的代码块中的子进程(P1、P2等)可以并发执行。这意味着它们可以同时运行,而不必等待彼此的完成。
2.在并发编程中,cobegin 和 coend 提供了一种结构化的方式来表达并发执行的代码片段,以便更容易理解和维护。这样的结构通常用于表示同时执行多个独立任务的情况,例如在多核处理器上并行执行多个线程或进程。
3.请注意,实际的编程语言可能使用不同的关键字或语法结构来表示并发,但 cobegin 和 coend 的使用方式通常是类似的。
这行代码的含义是原本程序中运行时存在先后顺序,用并发来打破先后顺序的执行
练习1
- 用信号量实现进程前趋关系
S1-S2之间是Sa
S1-S3之间是Sa
S2-S4之间是Sb
S3-S4之间是Sc
semaphore Sa = 0;
semaphore Sb = 0;
semaphore Sc = 0;
P1() { S1; signal(Sa); }
P2() { wait(Sa); S2; signal(Sb); }
P3() { wait(Sa); S3; signal(Sc); }
P4() { wait(Sb,Sc); S4;}
main() {
cobegin
P1();P2();P3();P4();
coend
}
练习2
有8个程序段p1……p8,它们在并发执行时有如图的制约关系,使用信号量实现这些程序段间的同步。
2.5经典进程的同步问题
生产者—消费者问题
生产者代码解析
void producer( ){
do{
produce an item nextp; //生产一个东西
……
wait(empty); //仓库有没有空的位置
wait(mutex); //有没有人访问
buffer[in]=nextp; //资源放入仓库的[in]位置
in=(in+1)%n; //标记下一个资源放入仓库的位置
signal(mutex); //标记没有人访问
signal(full); //满的位置数量加一
}while(TRUE);
}
1.do { ... } while(TRUE);:无限循环,表示生产者将一直运行。
2.produce an item nextp;:生成一个新的数据项 nextp。
3.wait(empty);:等待信号量 empty。empty 表示缓冲区中空闲的位置数量,如果为0,则表示缓冲区已满,需要等待。
4.wait(mutex);:等待互斥信号量 mutex。这是为了保护对共享资源(例如缓冲区)的互斥访问,防止多个进程同时修改缓冲区。
5.buffer[in] = nextp;:将生成的数据项放入缓冲区。
6.in = (in + 1) % n;:更新缓冲区的下一个可用位置。
7.signal(mutex);:释放互斥信号量 mutex,允许其他进程访问缓冲区。
8.signal(full);:释放信号量 full。full 表示缓冲区中已经存放的数据项数量,如果为0,则表示缓冲区为空,通知消费者可以开始消费。
ps:整体上,这段代码实现了一个典型的生产者行为,它等待缓冲区有空闲位置,然后在缓冲区中放入生成的数据项,并通过信号量 full 通知消费者可以进行消费。使用互斥信号量 mutex 来确保对缓冲区的操作是互斥的,防止多个进程同时修改缓冲区的情况。这是典型的用于解决生产者-消费者问题的同步机制。
消费者代码解析
void consumer ( ){
do{
// 等待缓冲区非空
wait(full);
// 等待互斥信号量,保护对共享资源的访问
wait(mutex);
// 从缓冲区中取出数据项
nextc=buffer[out];
// 更新缓冲区的下一个可用位置
out=(out+1)%n;
// 释放互斥信号量,允许其他进程访问缓冲区
signal(mutex);
// 释放信号量,通知生产者可以继续生产
signal(empty);
// 消费数据项
consume the item in nextc;
……
}while(TRUE);
}
void consumer ( ){
do{
wait(full); //看仓库里有没有东西
wait(mutex); //有没有人访问
nextc=buffer[out]; //从仓库拿东西
out=(out+1)%n; //标记下一个取东西的位置
signal(mutex); //标记没人访问
signal(empty); //标记现在仓库没东西,生产者该生产了
consume the item in nextc;
……
}while(TRUE);
}
1.do { ... } while(TRUE);:这是一个无限循环,表示消费者将一直运行。
2.wait(full);:full 是一个信号量,表示缓冲区中已经存放的数据项数量。如果数量为零,说明缓冲区为空,消费者需要等待。
3.wait(mutex);:mutex 是一个互斥信号量,用于确保对共享资源(缓冲区)的互斥访问。等待互斥信号量表示消费者正在对缓冲区进行访问。
4.nextc = buffer[out];:从缓冲区中取出数据项。
5.out = (out + 1) % n;:更新缓冲区的下一个可用位置,保证循环使用。
6.signal(mutex);:释放互斥信号量,允许其他进程访问缓冲区。
7.signal(empty);:释放信号量,通知生产者可以继续生产。
ps:consume the item in nextc;:消费数据项的操作。这部分代码表示消费者对取出的数据项进行实际的消费操作。
练习
有三个进程PA、PB、PC协作解决文件打印问题。PA将文件记录从磁盘读入内存的缓冲区1,每执行一次读一个记录;PB将缓冲区1的内容复制到缓冲区2中,每执行一次复制一个记录;PC将缓冲区2的内容打印出来,每执行一次打印一个记录。缓冲区的大小与记录大小一样。请用信号量来保证文件的正确打印。
semaphore empty1=1,full1=0,empty2=1,full2=0;
PA(){
while(1){
从磁盘读一个记录;
wait(empty1);
将记录放到缓冲区1中;
signal(full1);
}
PB(){
while(1){
wait(full1);
从缓冲区1取出一个记录;
signal(empty1);
wait(empty2);
将记录放到缓冲区2中;
signal(full2);
}
}
PC(){
while(1){
wait(full2);
从缓冲区2读一个记录;
signal(empty2);
将取出的记录打印出来;
}
}
main(){
cobegin PA();PB();PC(); coend
}
哲学家进餐问题
semaphore chopstick[5]={1,1,1,1,1};
do{
wait(chopstick[i];
wait(chopstick[(i+1)%5]);
eat;
signal(chopstick[i]);
signal(chopstick[(i+1)%5]);
think;
}while(TRUE);
读者写者问题
- 最基本的读者-写者问题解决方案。
- 使用一个二进制信号量
mutex
来控制对共享资源的访问,保证一次只有一个读者或写者能够访问共享资源。 - 这种方法可能导致写者饥饿的问题,因为一旦有一个读者访问,其他读者也能够进入,而写者需要等待所有读者完成。
semaphore mutex = 1;
// 读者(伪代码)
void reader(){
......
wait(mutex);
read;
signal(mutex);
......
}
// 写者(伪代码)
void writer(){
while(TRUE){
......
wait(mutex);
write;
signal(mutex);
......
}
}
- 引入了一个计数器
readcount
,用来记录当前有多少个读者正在访问共享资源 - 通过这个计数器,读者可以决定何时锁住
mutex
,以减少写者的饥饿问题
semaphore mutex = 1;
int readcount = 0;
// 读者(伪代码)
void reader(){
......
if(readcount==0) wait(mutex);
readcount++;
read;
readcount--;
if(readcount==0) signal(mutex);
......
}
// 写者(伪代码)
void writer(){
while(TRUE){
......
wait(mutex);
write;
signal(mutex);
......
}
}
- 引入了两个信号量
rmutex
和mutex
。 rmutex
用来控制对readcount
计数器的访问,以避免读者对计数器的并发访问。- 这可以减少对
mutex
的竞争,进一步提高读者的并发性。
semaphore mutex = 1;
int readcount = 0;
// 读者(伪代码)
void reader(){
......
wait(rmutex);
if(readcount==0) wait(mutex);
readcount++;
signal(rmutex);
read;
wait(rmutex);
readcount--;
if(readcount==0) signal(mutex);
signal(rmutex);
......
}
// 写者(伪代码)
void writer(){
while(TRUE){
......
wait(mutex);
write;
signal(mutex);
......
}
}
进程同步工具——管程
定义
- 代表共享资源的数据结构以及由对该共享结构实施操作的一组过程所组成的资源管理程序共同构成了一个操作系统的资源管理模块,我们称之为管程。
Monitror monitor_name {
share varible declarations;
cond declarations;
public:
viod P1(……)
{……}
viod P2(……)
{……}
……
void(……)
{……}
……
{
initialization code;
……
}
}
特点
- 模块化:管程是一个基本程序单位,可以单独编译;
- 抽象数据类型:数据+操作。
- 管程中的数据结构只能被管程中的过程访问,管程中的过程只能访问管程内的数据结构;
- 任一时刻,最多只能有一个进程在管程中执行。
思考题
同一时刻,管程中可不可能同时存在两个或更多的进程?
- Hore:P等待,直至Q离开或者等待另一个条件;
- MESA:Q等待,直至P离开或者等待另一个条件;
- Hansen:规定唤醒操作为管程中过程所执行的最后一个操作。
使用管程解决生产者—消费者问题
Monitor producerconsumer{
item buffer[N];
int in, out;
int count;
condition notfull,notempt;
public:
viod put(item x){
if(count>=N)cwait(notfull);
buffer[in]=x;
in=(in+1)%N;
count ++;
csignal(notempty);
}
viod get(item x){
if(count<=0)cwait(notempty);
x=buffer[out];
out=(out+1)%N;
count - -;
csignal(notfull);
}
{in=0;out=0;count=0;}
}PC;
void producer(){
item x;
while(TRUE){
……
produce an item in nextp;
PC.put(x);
}
}
void consumer(){
item x;
while(TRUE){
PC.get(x);
consume the item in nextc;
……
}
}
2.6进程通信
定义:是指进程之间的信息交换
实现方式:
- 低级进程通信:效率低,主要针对控制信息的传送。
- 高级进程通信:能传送大量数据,效率高,通信过程对用户透明。
高级进程通信的类型:
- 共享存储器系统
- 管道通信系统
- 消息传递系统
- 客户机-服务器系统
消息传递系统
直接通信:send和receive原语
- send(receiver,message);把消息发送给接收进程。
- receive(sender,message);接收sender发来的消息。
- 非对称寻址:receive(id,message)。
- 进程同步方式:
-
- 发送进程阻塞,接收进程阻塞;
- 发送进程不阻塞,接收进程阻塞;(应用最广泛)
- 发送进程和接收进程均不阻塞。
消息缓冲队列通信机制
typedef struct message_buffer
{
int sender;
int size;
char *text;
struct message_buffer *next;
……
}buffer;
发送原语
void send(receiver,a){
getbuf(a.size,i); //根据a.size申请缓冲区i
i.sender=a.sender;
i.size=a.size;
copy(i.text,a.text); //将发送区a的信息复制到i
i.next=0;
getid(PCBset,receiver.j); //获得接收进程内部的标识符
wait(j.mutex);
insert(&j.mq, i); //将消息缓冲区插入消息队列 signal(j.mutex);
signal(j.sm)
}
接收原语
void receive(b){
j=internal name;
wait(j.sm);
wait(j.mutex);
remove(j.mq,i); //将消息队列中第一个消息移出;
signal(j.mutex);
b.sender=i.sender;
b.size=i.size;
copy(b.text, j.text); //将缓冲区i中的信息复制到接收区b;
releasebuf(i); //释放消息缓冲区;
}
间接通信(信箱通信)
- 通信双方利用一个共享的称为信箱的中间实体实现信息交换。
客户机-服务器系统
- 套接字(Socket)
-
- 文件型:一个套接字关联一个特殊的文件,通过对文件的读写实现通信。
- 网络型:通过套接字实现通信连接。
- 远程过程调用(RPC)
-
- RPC允许客户机上的进程通过网络调用位于远程主机上的过程。
- 若软件采用面向对象编程,则可称为远程方法调用。
2.7 线程的基本概念
线程与进程的比较
- 调度的基本单位(进程、线程)
- 并发性(进程和线程都可以并发执行)
- 拥有资源(线程不拥有系统资源)
- 独立性(线程独立性弱)
- 系统开销(进程>线程)
- 支持多处理机系统(线程)
线程运行的三个状态
- 执行状态、就绪状态、阻塞状态
线程控制块(TCB)
线程标识符 寄存器 线程运行状态标识 优先级
线程专有存储区 信号屏蔽 堆栈指针
2.8 线程的实现
- KST在内核支持下运行,它的创建、阻塞、撤销和切换都是在内核空间实现,操作过程与进程类似。
优点
- 多处理机系统中同一个进程的多个线程可以并发执行。
- 一个进程拥有多个线程,一个线程阻塞,进程不会阻塞。
- 具有很小的数据结构和堆栈,切换较快,开销小。
- 内核可以使用多线程技术,提高系统执行速度和效率。
缺点
- 同一用户进程的多个线程之间切换需要内核参与,系统开销大。
- 用户级线程ULT是在用户空间中实现的,内核完全不知道该线程的存在。
- 只设置了用户级线程的系统,其调度仍然是以进程为单位。
- 实现:运行时系统、内核控制线程。
运行时系统(Runtime system)
又称为线程库,实质上是用于管理和控制线程的函数(过程)的集合,作为用户线程与内核之间的接口。
内核控制线程
又称为轻型进程(LWP),通过系统调用来获得内核提供的服务。用户控制线程与内核通信,内核看到的是多个LWP,而不是用户控制线程。
用户级线程的优点
- 线程切换不需要转换到内核空间。
- 调度算法可以是进程专用的。
- 用户级线程的实现与操作系统无关。
组合方式
内核支持多个内核支持线程的建立、调度和管理,同时也允许用户应用程序建立、调度和管理用户级线程。
线程的创建
应用程序在启动时,通常仅有一个线程在执行,人们把这个线程称为“初始化线程”,它的主要功能是用于创建新线程。在创建新线程时,需要利用一个线程创建函数(或系统调用),并提供相应的参数,如指向线程主程序的入口指针、堆栈的大小,以及用于调度的优先级等。在线程的创建函数执行完后,将返回一个线程标识符供以后使用。
线程的终止
- 通过调用相应的函数(或系统调用)终止线程。
- 有些线程(主要是系统线程)一旦被建立起来之后,便一直运行下去而不被终止。
- 在大多数的OS中,线程被中止后并不立即释放它所占有的资源,只有当进程中的其它线程执行了分离函数后,被终止的线程才与资源分离,此时的资源才能被其它线程利用