管程:
基础
定义:集中式同步工具:共享变量及其所有相关操作集中在一个摸块中。
优点:可读性好;正确性易于保证;易于修改。
缺点:不甚灵活,效率略低
和PV操作对比:分散式同步机制,可读性差;正确性不易保证;不易修改,但是高效灵活
管程的PCM方法:Proces and Class and Monitor(管程)
solo就是基于管程创建的
管程的形式:
Type monitor_name=MONITOR(形参表)
共享变量说明
define 本管程内定义,本管程外使用的子程序名表;
use 本管程外定义,本管程内使用的子程序名表;
Procedure 过程名(形参表);
局部变量说明
Begin 语句序列 End;
…………
Function 函数名(形参表):返回值类型;
局部变量说明
Begin 语句序列 End;
……………
Begin 共享变量初始化语句序列 End;
明显,这样的设计更加模块,信息更加隐蔽,提高了程序的抽象层次
管程:中有等待唤醒机制:
(P唤醒Q):
(Hoare):P紧急等待,Q继续,直到Q退出或等待;
(Java):Q等待,P继续,直到P退出或等待;被唤醒进程需要重新检查等待条件,可能再次等待.
(Hansen):唤醒是管程中可执行的最后一个操作。
下面以Hoare管程设施为例:
入口等待队列:每个管程一个,用于排队进入;
紧急等待队列:每个管程一个,用于唤醒等待;
内部等待队列:var c: condition; 可根据需要定义多个,用于设置等待条件。
管程队列:
有入口对了,紧急等待队列,
管程组成:除了入口队列,紧急对了,还有初始化代码和共享变量
管程的进入和离开:
进入:申请管程互斥权。
离开:如紧急等待队列非空,唤醒第一个等待者;否则开放管程。
条件变量操作:
Var c:condition;
wait(c):如紧急队列非空,唤醒第一个等待者,否则释放管程互斥权;执行此操作的进程(线程)进入c链尾。
signal(c):如c链空,相当空操作。否则唤醒第一个,执行此操作的进程(线程)进入紧急队列。
管程的使用:
-1:生产者和消费者
Type producer_consumer = MONITOR
Var B:Array[0..n-1]Of integer;
count, in, out: integer;
pq, cq:condition;
define put_in, get_out;
Procedure put_in(item:integer);
Begin
If(count==n)Then //放满了,阻塞自己,等待唤醒
wait(pq);
B[in]:=item; //生产出来了
in:=(in+1)%n; //循环生产
count++; //生产出来了,加一
signal(cq); //唤醒消费者
End;
Procedure get_out(Var item:integer)
Begin
If(count==0)Then //没有可以消费的.等
wait(cq);
item:=B[out]; //消费了
out:=(out+1)%n;//循环消费
count--; //减少一个
signal(pq); //唤醒生产者
End;
Begin
in:=0; out:=0; count:=0;
End;
例2:读写问题:
不能同时写,不能又读又写,可以同时读
Type readers_writers = Monitor;
Var rq,wq: condition; // 读者和写者的条件变量
reading_count,write_count:integer; // 记录读者和写者的数量
Procedure start_read;
Begin
If write_count>0 Then
wait(rq); // 如果有写者,当前读者线程阻塞
reading_count++; // 允许进入时,读者数量加1
signal(rq); // 唤醒下一个可能等待的读者(如果有)
End;
Procedure finish_read;
Begin
reading_count--; // 读者离开,计数减1
If reading_count=0 // 如果没有读者了
Then signal(wq) // 唤醒一个等待的写者
End;
Procedure start_write
Begin
write_count++; // 写者计数加1(包括等待和执行的)
If (write_count>1) or // 如果已有其他写者
(reading_count>0) // 或者有读者正在读
Then
wait(wq) // 当前写者阻塞
End;
Procedure finish_write;
Begin
write_count--; // 写者离开,计数减1
If write_count>0 // 如果还有其他写者等待
Then signal(wq) // 唤醒下一个写者
Else signal(rq) // 否则唤醒等待的读者
End;
Begin
reading_count:=0;
write_count:=0;
End;
Var rw:readers_writers;
例题3,哲学家就餐
:
Type dining-philosophers=MONITOR
Var fork:Array[0..4]Of(free,used); // 5个叉子的状态(自由/占用)
q:Array[0..4]Of condition; // 每个哲学家的等待队列
define pick_up, try_pick_up, put_down; // 定义操作接口
Procedure pick_up(I:0..4);
Begin
If fork[I]=used Then // 如果叉子已被占用
wait(q[I]); // 当前哲学家在叉子I的队列等待
fork[I]:=used // 成功获取叉子,标记为已使用
End;
Function try_pick_up(I:0..4):boolean;
Begin
If fork[I]=used Then
try_pick_up:=false // 叉子已被占用,返回失败
Else Begin
fork[I]:=used;
try_pick_up:=true // 成功获取叉子,返回成功
End
End;
I号哲学家活动:
Repeat
{thinking}
with_two_forks:=false;
Repeat
forks.pick_up(I); // 尝试拿起左边叉子
If forks.try_pick_up((I+1)mod 5)Then // 尝试拿起右边叉子
with_two_forks:=true // 两把叉子都拿到
Else forks.put_down(I) // 失败则放下左边叉子
Until with_two_forks;
{Eating}
forks.put_down(I); // 放下左边叉子
forks.put_down((I+1)mod 5) // 放下右边叉子
Until false;
管程操作和PV
实际上是等价的,我们可以用PV操作可以构造管程,亦可以用管程可以构造PV操作
用管程构造PV:
TYPE semaphore=MONITOR(init_value)
VAR c:condition;
count: integer;
DEFINE P,V;
PROCEDURE P;
BEGIN
count:=count-1;
IF count<0 THEN
wait(c);
END;
PROCEDURE V;
BEGIN
count:=count+1;
IF count <=0 THEN
signal(c);
END;
BEGIN
count:=init_value;
END;
用PV实现管程
关于管程的嵌套调用问题:
我们需要考虑:1. 是否允许嵌套;2. 若允许,如何处理互斥权。
方法:1. 禁止嵌套(Modula)
2. 允许嵌套,等待时释放当前管程互斥权(CPASCAL);
3. 允许嵌套,等待时释放所有管程互斥权;
4. 允许嵌套,调用时释放路经管程互斥权
会合:
两个并发执行流汇集到一处
会合指两个并发执行流汇集到一处的过程。在并发编程中,不同执行路径(如不同任务、线程等)的代码执行流,在特定点相遇并进行交互。
涉及操作:
调用:一个执行流发起操作请求,如同向另一个执行流发出 “邀请” 。
接受:另一个执行流对该请求做出响应,同意进行交互。
同步效果:当调用和接受操作都发生时,就如同两个执行流 “握手”,实现了二者的同步。此时它们可以交换数据、协调后续执行步骤等 。例如,一个任务准备好数据后,通过会合机制通知另一个任务来获取并处理,保证数据传递和处理的有序性,避免数据竞争等问题。
引入:分布式系统
在分布式系统场景下,图中P1和P2两个进程分别在站点 1 和站点 2 ,它们通过共享变量(信号量Semaphore s )进行 PV 操作。但分布式系统中,各站点可能在不同物理位置、不同主机上,共享变量与访问进程需在同一存储区才能有效操作,这种方式在分布环境下难以实现,存在局限性。比如不同站点间网络延迟、数据一致性维护困难等问题,会导致 PV 操作不能可靠执行,无法有效实现进程同步与互斥。
进程P1和P2要访问管程内的临界区CR1和CR2 。管程机制要求管程与调用它的进程处于同一存储区,这样才能保证进程对管程内共享变量的有效访问和操作。然而,在分布式环境中,不同的进程可能位于不同的物理节点上,这些节点可能具有独立的存储系统,无法保证管程与所有调用进程都在同一存储区。这就导致管程难以在分布式环境中像在集中式环境中那样可靠地实现进程同步与互斥功能 。例如,网络延迟可能导致进程对管程内共享变量的访问不一致,无法准确控制临界区的进入和退出,进而影响系统的正确性和稳定性 。
任务交互过程
调用语句:左侧和右侧的任务发出调用语句,主动发起交互请求,这类似于向其他任务发出 “合作邀请” 。
接受语句与选择语句:中间的任务通过接受语句来接收调用请求,并且可以利用选择语句,根据自身状态或外部条件等,从多个可能的调用请求中选择合适的进行响应 。
执行方式:被调用者代调用者执行调用代码:意味着当调用和接受成功匹配后,被调用的任务会代表调用者去执行相应的调用代码,实现了任务间的协作与同步 。
优势与应用场景:这种机制相比传统的同步方式(如 PV 操作、管程等在分布式环境存在局限性的方式),更适应分布式系统中任务分布在不同位置的情况,通过这种灵活的交互方式,能够有效解决分布式环境下任务间的同步与通信问题,提升系统的可靠性和效率 。
Ada同步语句:
1. 调用语句
<任务名称> . <入口名称>[<实参表>]
2. 接受语句
accept<入口名称>[<形参表>]
[do<语句序列>end<入口名称>]
初始等待:首先判断是否有调用者。若没有调用者(结果为 F),则进入等待状态,持续检查是否有调用者到来。
选取调用者:当有调用者(结果为 T)时,选取第一个调用者,会合开始,此时调用者进入等待状态,等待会合相关操作完成。
处理 in 参数(若有):检查是否存在 in 参数。若有(结果为 T) ,则获取 in 参数的值,用于后续操作;若没有(结果为 F),则跳过此步骤。
执行语句序列(若有):判断是否存在语句序列。若有(结果为 T) ,则执行相应的语句序列;若没有(结果为 F),则跳过此执行环节。
处理 out 参数(若有):查看是否有 out 参数。若有(结果为 T) ,则将相关结果通过 out 参数送出;若没有(结果为 F),则不进行此输出操作。
会合结束:完成上述步骤后,会合结束,调用者从等待状态恢复,继续后续执行流程。
例子:
task single_resource is
entry acquire;
entry return;
end single_resource;
task body single_resource is
begin
loop
accept acquire;
accept return;
end loop
end single_resource;
single_resource.acquire;
使用
single_resource.return;
3. 选择语句
select
[when <布尔表达式> =>]
<接受语句>
[<语句序列>]
{or [when <布尔表达式> =>]
<接受语句>
[<语句序列>] }
[else <语句序列>]
end select
四:进程高级通讯(communication)
-1:进程通讯:
进程之间的相互作用。
低级通讯(简单信号):进程互斥;进程同步
高级通讯(大宗信息)
-2:进程通讯的模式:
1. 共享内存模式(shared memory):
OS来提供公共内存和互斥同步机制
2. 消息传递模式(message passing):
分为直接和间接:
直接方式:
分为对称和非对称
对称方式
就是地位等同,谁都可以发(p2p)
非对称方式(c/s)
缓冲途径:
接受者会有一个缓冲的队列,
下面以消息直接传递,非对称,有缓存为例,模拟一个过程
载有消息的缓冲,包含 Size(消息大小)、text(消息内容)、sender(发送者信息)、link(可能用于链接到下一个缓冲等 )。这是消息在传递过程中的存储载体
进程消息队列管理:
使用信号量 Sm(初始值为 0 )。收取消息前执行 P(Sm) 操作,会使进程阻塞等待,直到有消息入队(入队后执行 V(Sm) 唤醒等待进程 )。这保证了进程在没有消息时不会盲目读取,实现了消息接收的同步。
消息队列互斥:
利用信号量 m_mutex(初始值为 1 ),在入列或出列操作前后分别执行 P(m_mutex) 和 V(m_mutex) ,保证同一时刻只有一个进程能对消息队列进行操作,避免队列操作时的竞争条件。
缓冲池管理
缓冲池结构:第二张图展示了缓冲池由多个 buf 组成的链式结构,Head 指向缓冲池头部。
信号量设置:定义信号量 Sb(初始值为 k ,表示缓冲池中可用缓冲的数量 )和 b_mutex(初始值为 1 ,用于互斥访问缓冲池 )。
申请缓冲:申请时先执行 P(Sb) ,若可用缓冲数不足(Sb 值小于 0 )则阻塞,再执行 P(b_mutex) 确保互斥访问缓冲池,然后将头缓冲出链,最后执行 V(b_mutex) 释放对缓冲池的互斥访问。
释放缓冲:释放时先执行 P(b_mutex) 保证互斥,将缓冲入链头,再依次执行 V(b_mutex) 和 V(Sb) ,增加可用缓冲数量并唤醒等待获取缓冲的进程。
Send(R,M)
根据R找接收者;
P(Sb); //申请缓冲
P(b_mutex); //申请访问缓冲临界区
取一空buf;
V(b_mutex); //退出缓冲临界区
size,text,sender => buf //将信息放入buffer中
P(m_mutex); //申请访问队列临界区
消息入链尾;
V(m_mutex); //退出
V(Sm); //消息队列里面多了一个buffer,释放一个消息
Receive(pid,N)
P(Sm); //申请一个队列中的消息
P(m_mutex); //队列互斥访问
头消息出链;
V(m_mutex); //同上
size,text=> N //取出信息
sender => pid
P(b_mutex); //申请访问缓冲临界区
空buf入链;
V(b_mutex);//退出缓冲临界区
V(Sb); //释放一个buffer块给缓冲池
这里的send和receive可以中断
无缓冲途径:
发送-接收都发生,信息由发送者复制到接收者.
PCB中两个信号灯, S_m, S_w, 初值0.
发送过程:send(R,M):
根据R找到消息接收者
发送消息进程增1, 如接收进程等待将其唤醒, 即执行V(S_m)
等待消息传送完毕, 即执行P(S_w)
接收过程:receive(pid,N)
等待消息到达, 即执行P(S_m)
消息由发送进程空间复制到接收进程空间;
唤醒发送消息进程, 即执行V(S_w)
优点:节省空间(不需要buffer)
缺点:并发性差: 发送进程需要等待接收进程执行receive复制信息到进程空间后才能继续.
间接方式:
支持多发送者 - 多接收者、多发送者 - 单接收者等多种通信模式
实例:
-- 定义信箱数据结构
Type mailbox=record
in,out:0..k; -- 循环缓冲区的读写指针(in指向写入位置,out指向读取位置)
s1,s2:semaphore; (k,0) -- s1表示可用空槽数量(初始值为k),s2表示已占用槽数量(初始值为0)
mutex:semaphore; (1) -- 互斥信号量,确保对信箱内部数据的互斥访问
letter:array[0..k-1]of message; -- 存储消息的循环缓冲区
end;
Var mb:mailbox; -- 创建一个信箱实例
-- 系统调用:创建信箱
create_mb(mb); -- 初始化信箱数据结构,设置信号量初始值
-- 系统调用:删除信箱
delete_mb(mb); -- 释放信箱占用的系统资源
-- 发送消息到信箱的过程
Procedure send_mb(var mb:mailbox; m:message);
begin
with mb do -- 使用with语句简化对mb成员的访问
begin
P(s1); -- 申请一个空槽:若没有空槽则阻塞,否则减少空槽计数
P(mutex); -- 进入临界区:获取对信箱内部数据的独占访问权
letter[in]:=m; -- 将消息存入缓冲区的当前写入位置
in:=(in+1)mod k; -- 更新写入指针(循环递增)
V(mutex); -- 离开临界区:释放对信箱的独占访问权
V(s2); -- 通知有新消息:增加已占用槽的计数,唤醒可能等待的接收者
end;
end;
-- 从信箱接收消息的过程
Procedure receive_mb(var mb:mailbox; var n:message);
begin
with mb do -- 使用with语句简化对mb成员的访问
begin
P(s2); -- 申请一个消息:若没有消息则阻塞,否则减少已占用槽计数
P(mutex); -- 进入临界区:获取对信箱内部数据的独占访问权
n:=letter[out]; -- 从缓冲区的当前读取位置取出消息
out:=(out+1)mod k; -- 更新读取指针(循环递增)
V(mutex); -- 离开临界区:释放对信箱的独占访问权
V(s1); -- 通知有空槽:增加空槽计数,唤醒可能等待的发送者
end;
end;
不同进程可以调用 create_MB 创建信箱,使用 send_MB 发送消息到信箱,通过 receive_MB 从信箱接收消息,以及利用 delete_MB 删除信箱 。
UNIX 进程高级通讯机制 - 管道(Pipe)
管道基本概念:管道是一种无名文件,有两个文件描述符(fd) ,一个用于写(fd[1]),一个用于读(fd[0])。其文件大小限制为 4 个块,每块 512 字节,即总共 2048 字节 。
创建与使用:通过 pipe(fd) 函数创建管道,成功创建后,fd[0] 为读描述符,fd[1] 为写描述符 。如子进程 1 可使用 write(fd[1], buf1, count1) 向管道写入数据,子进程 2 使用 read(fd[0], buf2, count2) 从管道读取数据 。并且管道可被子进程继承,方便父子进程或兄弟进程间通信。
速度与特点:从速度上看,管道虽基于文件系统实现,涉及两次 IO 操作,但常采用缓冲与延迟写技术。数据先写入内存缓冲区,只要内存资源充足且缓冲区未被挪作他用,就不会写入外存,读取时直接从内存缓冲区获取 。另外,管道文件大小有限且循环使用,可避免缓冲资源紧张 。其特点是基于文件系统,与文件操作有统一的接口。
UNIX 信号通讯机制
相关数据结构:struct proc 结构体中有成员 p_sig 用于记录信号相关信息;struct user 结构体中的 u_signal[NSIG] 数组用于存储信号处理程序 。
信号处理程序预置:通过系统调用 signal(sig, func) 来预置信号处理程序 。u_signal[sig] = func 实现具体设置。当 Func = 0 时,接收信号的进程会终止自身;Func 为奇数时信号被忽略;Func 为偶数时则作为处理程序入口 。进程初创时会继承其父进程的信号处理程序 。
信号发送与接收处理:进程在任何时候都可接收信号,接收到的信号记录在 p_sig 中 。当进程被调度选中,即将从核心态转到目态时,会对记录的信号进行相应处理 。 信号机制可看作是用户处理的中断,用于实现异步通知等功能 。