From 12614c3a332103228689d73cd9e39588e468d64a Mon Sep 17 00:00:00 2001 From: Didnelpsun <2675350965@qq.com> Date: Tue, 17 Aug 2021 23:34:38 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=BF=9B=E7=A8=8B=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E4=B8=8E=E8=AE=A1=E7=AE=97=E6=9C=BA=E7=BB=84=E6=88=90?= =?UTF-8?q?=E5=8E=9F=E7=90=86=E6=A6=82=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Computer-Organization/0-summary-ex.md | 23 ++ Computer-Organization/0-summary.md | 40 ++- Operate-System/1-process-management-ex.md | 84 +++++ Operate-System/1-process-management.md | 353 +++++++++++++--------- 4 files changed, 343 insertions(+), 157 deletions(-) create mode 100644 Computer-Organization/0-summary-ex.md diff --git a/Computer-Organization/0-summary-ex.md b/Computer-Organization/0-summary-ex.md new file mode 100644 index 0000000..a828a3f --- /dev/null +++ b/Computer-Organization/0-summary-ex.md @@ -0,0 +1,23 @@ +# 概述习题 + +## 计算机的发展 + +**例题** 只有当程序执行时才将源程序翻译成机器语言,并且一次只能翻译一行语句,边翻译边执行的是()程序,把汇编语言源程序转变为机器语言程序的过程是()。 + +Ⅰ.编译 + +Ⅱ.目标 + +Ⅲ.汇编 + +Ⅳ.解释 + +$A.$Ⅰ、Ⅱ + +$B.$Ⅳ、Ⅱ + +$C.$Ⅳ、Ⅰ + +$D.$Ⅳ、Ⅲ + +解:$D$。解释程序的特点是翻译一句执行一句,边翻译边执行;由高级语言转化为汇编语言的过程称为编译,把汇编语言源程序翻译成机器语言程序的过程称为汇编。 \ No newline at end of file diff --git a/Computer-Organization/0-summary.md b/Computer-Organization/0-summary.md index 41151fb..fa01c6a 100644 --- a/Computer-Organization/0-summary.md +++ b/Computer-Organization/0-summary.md @@ -4,7 +4,7 @@ + 计算机系统=硬件+软件。 + 软件分为: - + 系统软件:用来管理整个计算机系统,如操作系统、数据库管理系统(DBMS)、标准程程序库、网络软件、语言处理程序、服务程序。 + + 系统软件:用来管理整个计算机系统,如操作系统、数据库管理系统($DBMS$)、标准程程序库、网络软件、语言处理程序、服务程序。 + 应用软件:按任务需要编制成的各种程序。 ## 计算机的发展 @@ -19,9 +19,9 @@ 第四代|1972-现在|大规模、超大规模集成电路|上千万-万亿|半导体存储器|磁盘、磁带、光盘、半导体存储器 + 第一代使用纸带磁带编程。 -+ 第二代出现了面向过程的程序设计语言FORTRAN,有了操作系统雏形。 ++ 第二代出现了面向过程的程序设计语言$FORTRAN$,有了操作系统雏形。 + 第三代主要用于科学计算等专业用途,高级语言快速发展,开始有了分时系统。 -+ 第四代开始出现CPU、PC,如Windows、MacOS等。 ++ 第四代开始出现$CPU$、$PC$,如$Windows$、$MacOS$等。 ### 软件的发展 @@ -29,6 +29,22 @@ + 汇报语言。 + 高级语言。 +### 计算机的分离 + +电子计算机分为: + ++ 电子模拟计算机。 ++ 电子数字计算机: + + 专用计算机。 + + 通用计算机:巨型机、大型机、中型机、小型机、微型机、单片机。 + +按指令和数据流分为: + ++ 单指令流和单数据流系统($SISD$),即传统冯·诺依曼体系结构。 ++ 单指令流和多数据流系统($SIMD$),包括阵列处理器和向量处理器系统。 ++ 多指令流和单数据流系统($MISD$),这种计算机实际上不存在。 ++ 多指令流和多数据流系统($MIMD$),包括多处理器和多计算机系统。 + ## 计算机系统层次结构 ### 计算机结构 @@ -52,7 +68,7 @@ 1. 计算机由两个部分组成: + 主机: - + CPU: + + $CPU$: + 运算器。 + 控制器。 + 主存。 @@ -69,7 +85,7 @@ + 存储体:存储数据的主体,按地址存储。 + MAR:(Memory Address Register)存储地址寄存器。MAR位数反映存储单元个数的幂值。 + MDR:(Memory Data Register)存储数据寄存器。MDR位数=存储字长。 -+ 现代MAR和MDR被划分进入了CPU。 ++ 现代MAR和MDR被划分进入了$CPU$。 + 存储单元:每个存储单元存放一串二进制代码。 + 存储字(word):存储单元中二进制代码的组合。 + 存储字长:存储单元中二进制代码的位数。 @@ -201,21 +217,21 @@ int main(){ ### 中央处理器 -+ CPU主频:CPU内数字脉冲信号振荡的频率。单位为赫兹。 -+ CPU时钟周期:CPU主频(时钟主频)=1÷CPU时钟周期。单位为纳秒或微秒。 ++ $CPU$主频:$CPU$内数字脉冲信号振荡的频率。单位为赫兹。 ++ $CPU$时钟周期:$CPU$主频(时钟主频)=1÷$CPU$时钟周期。单位为纳秒或微秒。 + CPI (Clock cycle Per Instruction):执行一条指令所需的时钟周期数。 -+ 执行一条指令的耗时= CPIxCPU时钟周期。 -+ CPU执行时间(整个程序的耗时)=CPU时钟周期数÷主频=(指令条数×CPI)÷主频。 ++ 执行一条指令的耗时= CPIx$CPU$时钟周期。 ++ $CPU$执行时间(整个程序的耗时)=$CPU$时钟周期数÷主频=(指令条数×CPI)÷主频。 + IPS(Instructions Per Second):每秒执行多少条指令,IPS=主频÷平均CPI。 + FLOPS(Floating-point Operations Per Second):每秒执行多少次浮点运算。 -**例题** 某CPU主频为10o00Hz,某程序包含100条指令,平均来看指令任CPI=3,该程序在该CPU上执行需要多久? +**例题** 某$CPU$主频为10o00Hz,某程序包含100条指令,平均来看指令任CPI=3,该程序在该$CPU$上执行需要多久? 100×3÷1000=0.3s。 ### 系统整体 + 数据通路带宽:数据总线一次所能并行传送信息的位数(各硬件部件通过数据总线传输数据)。 -+ 吞吐量:指系统在单位时间内处理请求的数量。它取决于信息能多快地输入内存,CPU能多快地取指令,数据能多快地从内存取出或存入,以及所得结果能多快地从内存送给一台外部设备。这些步骤中的每一步都关系到主存,因此,系统吞吐量主要取决于主存的存取周期。 -+ 响应时间:指从用户向计算机发送一个请求,到系统对该请求做出响应并获得它所需要的结果的等待时间。通常包括CPU时间(运行一个程序所花费的时间)与等待时间(用于磁盘访问、存储器访问、I/O操作、操作系统开销等时间)。 ++ 吞吐量:指系统在单位时间内处理请求的数量。它取决于信息能多快地输入内存,$CPU$能多快地取指令,数据能多快地从内存取出或存入,以及所得结果能多快地从内存送给一台外部设备。这些步骤中的每一步都关系到主存,因此,系统吞吐量主要取决于主存的存取周期。 ++ 响应时间:指从用户向计算机发送一个请求,到系统对该请求做出响应并获得它所需要的结果的等待时间。通常包括$CPU$时间(运行一个程序所花费的时间)与等待时间(用于磁盘访问、存储器访问、I/O操作、操作系统开销等时间)。 + 基准程序是用来测量十算机处理速度的一种实用程序,以便于被测量的计算机性能可以与运行相向程序的其它计算机性能进行比较。 diff --git a/Operate-System/1-process-management-ex.md b/Operate-System/1-process-management-ex.md index dea9ba2..7572931 100644 --- a/Operate-System/1-process-management-ex.md +++ b/Operate-System/1-process-management-ex.md @@ -376,3 +376,87 @@ $D.10ms$ 4|8:50|20min|6|8:50|10:20|90min 2)平均周转时间$为(70+30+90+90)\div4=70min$。 + +## 进程同步与互斥 + +### 进程同步与互斥的基本概念 + +#### 进程关系 + +**例题** 进程$A$和进程$B$通过共享缓冲区协作完成数据处理,进程$A$负责产生数据并放入缓冲区,进程$B$从缓冲区读数据并输出。进程$A$和进程$B$之间的制约关系是()。 + +$A.$互斥关系 + +$B.$同步关系 + +$C.$互斥和同步关系 + +$D.$无制约关系 + +解:$C$。并发进程因为共享资源而产生相互之间的制约关系,可以分为两类:①互斥关系,指进程之间因相互竞争使用独占型资源(互斥资源)所产生的制约关系;②同步关系,指进程之间为协同工作需要交换信息、相互等待而产生的制约关系。本题中两个进程之间的制约关系是同步关系,进程$B$必须在进程$A$将数据放入缓冲区后才能从缓冲区中读出数据。此外,共享的缓冲区一定是互斥访问的,所以它们也具有互斥关系。 + +#### 临界资源 + +**例题** 以下()属于临界资源。 + +$A.$磁盘存储介质 + +$B.$公用队列 + +$C.$私用数据 + +$D.$可重入的程序代码 + +解:$B$。临界资源与共享资源的区别在于,在一段时间内能否允许被多个进程访问(并发使用),显然磁盘属于共享设备。在域环境中,公用队列是$Active\,Directory$中发布的队列,因此通过整个$Windows\,Server\,2003$家族林进行复制。公用队列可供多个进程使用,但一次只可供一个进程使用,试想若多个进程同时使用公用队列,势必造成队列中的数据混乱而无法使用。私用数据仅供一个进程使用,不存在临界区问题,可重入的程序代码一次可供多个进程使用。 + +### 信号量 + +**例题** 设与某资源关联的信号量初值为$3$,当前值为$1$。若$M$表示该资源的可用个数,$N$表示等待该资源的进程数,则$M$,$N$分别是()。 + +$A.0,1$ + +$B.1,0$ + +$C.1,2$ + +$D.2,0$ + +解:$D$。信号量是一个特殊的整型变量,只有初始化和$PV$操作才能改变其值。通常,信号量分为互斥量和资源量,互斥量的初值一般为$1$,表示临界区只允许一个进程进入,从而实现互斥。当互斥量等于$0$时,表示临界区已有一个进程进入,临界区外尚无进程等待;当互斥量小于$0$时,表示临界区中有一个进程,互斥量的绝对值表示在临界区外等待进入的进程数。同理,资源信号量的初值可以是任意整数,表示可用的资源数,当资源量小于$0$时,表示所有资源已全部用完,而且还有进程正在等待使用该资源,等待的进程数就是资源量的绝对值。 + +**例题** 用$PV$操作实现进程同步,信号量的初值为()。 + +$A.-1$ + +$B.0$ + +$C.1$ + +$D.$由用户确定 + +解:$D$。与互斥信号量初值一般为$1$时不同,用$PV$操作实现进程同步,信号量的初值应根据具体情况来确定。若期望的消息尚未产生,则对应的初值应为$0$,若期望的消息已存在,则信号量的初值应设为一个非$0$的正整数。(一般看之前的同步互斥应用一般设置为$0$是因为还没有产生消息) + +**例题** 有三个进程共享同一程序段,而每次只允许两个进程进入该程序段,若用$PV$操作同步机制,则信号量$S$的取值范围是()。 + +$A.2,1,0,-1$ + +$B.3,2,1,0$ + +$C.2,1,0,-1,-2$ + +$D.1,0,-1,-2$ + +解:$A$。因为每次允许两个进程进入该程序段,即资源(这里指可进入的空间)的最大容量为$2$,所以信号量最大值取$2$。至多有三个进程申请,则信号量最小为$-1$,即最多只存在一个进程在等待,所以信号量可以取$2,1,0,—1$。 + +**例题** 一个进程因在互斥信号量`mutex`上执行`V(mutex)`操作而导致唤醒另一个进程时,执行$V$操作后`mutex`的值为()。 + +$A.$大于$0$ + +$B.$小于$0$ + +$C.$大于等于$0$ + +$D.$小于等于$0$ + +解:$D$。由题意可知,$V$操作导致唤醒另一个进程,所以系统原来就存在等待进入临界区的进程,`mutex`小于等于$-1$,因此在执行`V(mutex)`操作后,`mutex`的值小于等于$0$。 + +### 管程 diff --git a/Operate-System/1-process-management.md b/Operate-System/1-process-management.md index 997d7e0..72c93c6 100644 --- a/Operate-System/1-process-management.md +++ b/Operate-System/1-process-management.md @@ -1052,25 +1052,25 @@ P6() { $PV$操作题目分析步骤: 1. 关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系。 -2. 整理思路。根据各进程的操作流程确定P、V操作的大致顺序。 +2. 整理思路。根据各进程的操作流程确定$P$、$V$操作的大致顺序。 3. 设置信号量。设置需要的信号量,并根据题目条件确定信号量初值。 -4. 互斥信号量初值一般为1,同步信号量的初始值要看对应资源的初始值是多少。 +4. 互斥信号量初值一般为$1$,同步信号量的初始值要看对应资源的初始值是多少。 #### 生产者消费者问题 + 系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。(这里的“产品”理解为某种数据) -+ 生产者、消费者共享一个初始为空、大小为n的缓冲区。 ++ 生产者、消费者共享一个初始为空、大小为$n$的缓冲区。 + 只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待。 + 只有缓冲区不空时,消费者才能从中取出产品,否则必须等待。 + 缓冲区是临界资源,各进程必须互斥地访问。 由缓冲区是临界资源,所以对其访问是互斥关系。 -缓冲区满时,生产者要等待消费者取走产品,所以是同步关系。 +缓冲区满时,生产者要等待消费者取走产品,消费者取必须在生产者放之前,所以是同步关系。 -缓冲区空时(即没有产品时),消费者要等待生产者放入产品,所以是同步关系。 +缓冲区空时(即没有产品时),消费者要等待生产者放入产品,消费者取必须在生产者放之后,所以是同步关系。 -所以分析得到生产者每次要消耗(P)一个空闲缓冲区,并生产(V)一个产品。消费者每次要消耗(P)一个产品,并释放一个空闲缓冲区V。往缓冲区放入/取走产品需要互斥。 +所以分析得到生产者每次要消耗$P$一个空闲缓冲区,并生产$V$一个产品。消费者每次要消耗$P$一个产品,并释放一个空闲缓冲区$V$。往缓冲区放入/取走产品需要互斥。 所以设置三个变量,不能合并: @@ -1081,12 +1081,15 @@ semaphore full = 0; // 同步信号量,表示产品的数量,也即非空缓 // 生产者 producer() { - while(1){ + // 不断循环 + while(1){ 生产一个产品; - P(empty); // 消耗一个空闲缓冲区 + P(empty); // 获取一个空闲缓冲区 + // 进入临界区 P(mutex); // 上锁缓冲区 把产品放入缓冲区; V(mutex); // 解锁缓冲区 + // 离开临界区 V(full); // 增加一个产品 } } @@ -1103,25 +1106,26 @@ consumer() { } ``` -若调换P操作顺序会怎么样? +若调换$P$操作顺序会怎么样?($empty$和$full$的$P$操作必然在$mutex$的$P$操作之前) -+ 若此时缓冲区内已经放满产品,则empty=o,full=n。 -+ 则生产者进程执行`P(mutex)`使mutex变为0上锁,再执行`P(empty)`,由于已没有空闲缓冲区,因此生产者被阻塞。 -+ 由于生产者阻塞,因此切换回消费者进程。消费者进程执行`P(mutex)`,由于mutex为0,即生产者还没释放对临界资源的“锁”,因此消费者也被阻塞。 ++ 若此时缓冲区内已经放满产品,则`empty=0`,`full=n`。 ++ 则生产者进程执行`P(mutex)`使`mutex`变为`0`上锁,再执行`P(empty)`,由于已没有空闲缓冲区,因此生产者被阻塞。 ++ 由于生产者阻塞,因此切换回消费者进程。消费者进程执行`P(mutex)`,由于`mutex`为`0`,即生产者还没释放对临界资源的“锁”,因此消费者也被阻塞。 + 这就造成了生产者等待消费者对产品消费来释放空闲缓冲区,而消费者又等待生产者解锁临界区的情况,生产者和消费者循环等待被对方唤醒,出现“死锁”。 -+ 同样的,若缓冲区中没有产品,即full=0,empty=n,按先消费者后生产者的顺序执行也会发生死锁。因此,实现互斥的P操作一定要在实现同步的P操作之后。 ++ 同样的,若缓冲区中没有产品,即`full=0`,`empty=n`,按先消费者后生产者的顺序执行也会发生死锁。 ++ 因此,实现互斥的$P$操作一定要在实现同步的$P$操作之后。可以理解为要先拿到这个空间再上锁,若没有就拿到就上锁,那么其他进程也用不了这个空间。 -而由于V操作是释放不会导致进程阻塞,所以两个V操作可以交换顺序。 +而由于$V$操作是释放不会导致进程阻塞,所以两个V操作可以交换顺序。 + 生产者消费者问题是一个互斥、同步的综合问题。 + 对于初学者来说最难的是发现题目中隐含的两对同步关系。 -+ 有时候是消费者需要等待生产者生产,有时候是生产者要等待消费者消费,这是两个不同的“一前一后问题”,因此也需要设置两个同步信号量。前生产者V后消费者P就是full,前消费者V后生产者者P就是empty。 ++ 有时候是消费者需要等待生产者生产,有时候是生产者要等待消费者消费,这是两个不同的“一前一后问题”,因此也需要设置两个同步信号量。前生产者$V$后消费者$P$的关系就是`full`,前消费者$V$后生产者者$P$的关系就是`empty`。 #### 多生产者多消费者问题 -桌子上有一只盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等着吃盘子中的橘子,女儿专等着吃盘子中的苹果。只有盘子空时,爸爸或妈妈才可向盘子中放一个水果。仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出水果。用PV操作实现上述过程。 +桌子上有一只盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等着吃盘子中的橘子,女儿专等着吃盘子中的苹果。只有盘子空时,爸爸或妈妈才可向盘子中放一个水果。仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出水果。用$PV$操作实现上述过程。 -所以盘中是一个大小为1,初始为空的缓冲区。父母分别为两个生产者进程,子女分别为两个消费者进程,其消费的产品类别不同。 +所以盘中是一个大小为$1$,初始为空的缓冲区。父母分别为两个生产者进程,子女分别为两个消费者进程,其消费的产品类别不同。 对缓冲区盘子需要互斥使用。 @@ -1131,7 +1135,9 @@ consumer() { 只有盘子空时,父亲或母亲才能放水果,所以也是同步关系。 -所以对于互斥关系设置一个互斥信号量mutex=1,对于苹果设置为apple=0,对于橘子设置为orange=0,对于向拍子放水果设置plate=1(也可以设置为0,后面的处理不同)。 +而儿子女儿之间没有同步和互斥关系,所以不用管。 + +所以对于互斥关系设置一个互斥信号量`mutex=1`,对于苹果设置为`apple=0`,对于橘子设置为`orange=0`,对于向拍子放水果设置`plate=1`(也可以设置为$0$,后面的处理不同)。 ```cpp // 实现互斥访问盘子(缓冲区) @@ -1143,26 +1149,33 @@ semaphore orange= 0; // 盘子中还可以放多少个水果 semaphore plate = 1; +// 先准备一个苹果,放苹果之前,先判断盘子里是否为空(P一下盘子,检查盘子中还可以放多少个水果),如果盘子为空进行加锁,然后再将苹果放入进去(V一下苹果,数量+1) dad () { - while (1){ + while (1){ 准备一个苹果; P(plate); P(mutex); 把苹果放入盘子; V(mutex); + V(apple); } } + +// 先准备一个橘子,放橘子之前,先判断盘子里是否为空(P一下盘子,检查盘子中还可以放多少个水果),如果盘子为空进行加锁,然后再将橘子放入进去(V一下橘子,数量+1) mom() { - while (1){ + while (1){ 准备一个橘子; P(plate); P(mutex); 把橘子放入盘子; V(mutex); + V(orange); } } + +// 拿苹果之前,先判断盘子里有没有苹果(P一下苹果,若没有苹果,自己被阻塞),如果有就锁定盘子,取出苹果,解锁,然后告诉父母,盘子为空了(V一下盘子) daughter() { - while (1){ + while (1){ P(apple); P(mutex); 从盘中取出苹果; @@ -1171,8 +1184,10 @@ daughter() { 吃掉苹果; } } + +// 拿橘子之前,先判断盘子里有没有橘子(P一下橘子,若没有橘子,自己被阻塞),如果有就锁定盘子,取出橘子,解锁,然后告诉父母,盘子为空了(V一下盘子) son(){ - while (1){ + while (1){ P(orange); P(mutex); 从盘中取出橘子; @@ -1183,12 +1198,14 @@ son(){ } ``` +`P(plate/apple/orange)`和`V(plate/apple/orange)`实现了同步,而`P(mutex)`和`V(mutex)`实现互斥。 + 如果不使用互斥量会怎么样? -+ 刚开始,儿子、女儿进程即使上处理机运行也会被阻塞。如果刚开始是父亲进程先上处理机运行,则父亲P(plate),可以访问盘子→母亲P(plate),阻塞等待盘子→父亲放入苹果V(apple),女儿进程被唤醒,其他进程即使运行也都会阻塞,暂时不可能访问临界资源(盘子)→女儿P(apple),访问盘子,V(plate),等待盘子的母亲进程被唤醒→母亲进程访问盘子(其他进程暂时都无法进入临界区)。 -+ 即使不设置专门的互斥信号量mutex,也不会出现多个进程同时访问盘子的现象。 -+ 原因在于本题中的缓冲区大小为1,在任何时刻,apple、 orange、 plate三个同步信号量中最多只有一个是1。因此在任何时刻,最多只有一个进程的P操作不会被阻塞,并顺利地进入临界区,此时互斥关系全部变成同步关系。 -+ 而如果缓冲区大于1,则父母两个可以同时访问临界区,可能导致数据覆盖,所以必须使用互斥信号量。 ++ 刚开始,儿子、女儿进程即使上处理机运行也会被阻塞($apple$和$orange$刚开始都是$0$,所以被阻塞)。如果刚开始是父亲进程先上处理机运行,则父亲`P(plate)`,可以访问盘子→母亲`P(plate)`,阻塞等待盘子→父亲放入苹果`V(apple)`→女儿进程被唤醒,其他进程即使运行也都会阻塞,暂时不可能访问临界资源(盘子)→女儿`P(apple)`,访问盘子,`V(plate)`,等待盘子的母亲进程被唤醒→母亲进程访问盘子(其他进程暂时都无法进入临界区)。 ++ 即使不设置专门的互斥信号量`mutex`,也不会出现多个进程同时访问盘子的现象。 ++ 原因在于本题中的缓冲区大小为`1`,在任何时刻,`apple`、`orange`、`plate`三个同步信号量中最多只有一个是$1$。因此在任何时刻,最多只有一个进程的$P$操作不会被阻塞,并顺利地进入临界区,此时互斥关系全部变成同步关系。 ++ 而如果缓冲区大于$1$,则父母两个可以同时访问临界区,可能导致数据覆盖,所以必须使用互斥信号量。 + 解决“多生产者-多消费者问题”的关键在于理清复杂的同步关系。 + 在分析同步问题(一前一后问题)的时候不能从单个进程行为的角度来分析,要把“一前一后”发生的事看做是两种“事件”的前后关系。 @@ -1198,18 +1215,171 @@ son(){ + 正确的分析方法应该从“事件”的角度来考虑,我们可以把上述四对“进程行为的前后关系”抽象为一对“事件的前后关系”。 + 盘子变空事件→放入水果事件。“盘子变空事件”既可由儿子引发,也可由女儿引发;“放水果事件"既可能是父亲执行,也可能是母亲执行。这样的话,就可以用一个同步信号量解决问题了。 +#### 读者写者问题 + +有读者和写者两组并发进程,共享一个文件,当两个或两个以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求: + +1. 允许多个读者可以同时对文件执行读操作。 +2. 只允许一个写者往文件中写信息。 +3. 任一写者在完成写操作之前不允许其他读者或写者工作写。 +4. 写者执行写操作前,应让已有的读者和写者全部退出。 + +具有两类进程:写进程、读进程。 + +互斥关系:写进程-写进程、写进程-读进程。读进程与读进程不存在互斥问题。 + +写者进程和任何进程都互斥,设置一个互斥信号量`rw`,在写者访问共享文件前后分别执行$P$、$V$操作。 + +读者进程和写者进程也要互斥,因此读者访问共享文件前后也要对`rw`执行$P$、$V$操作。 + +如果所有读者进程在访问共享文件之前都执行$P(rw)$操作,那么会导致各个读进程之间也无法同时访问文件。所以读者写者问题的核心思想――怎么处理该读者共享的问题呢?即读同步。 + +`P(rw)`和`V(rw)`其实就是对共享文件的“加锁”和“解锁”。既然各个读进程需要同时访问,而读进程与写进程又必须互斥访问,那么我们可以让第一个访问文件的读进程“加锁”,让最后一个访问完文件的读进程“解锁”。可以设置一个整数变量`count`来记录当前有几个读进程在访问文件,这个也需要保持读进程互斥,避免多个读进程同时操作导致计数错误。 + +```cpp +// 用于实现对文件的互斥访问。表示当前是否有进程在访问共享文件,1代表空闲 +semaphore rw = 1; +// 记录当前有几个读进程在访问文件 +int count = 0; +// 用于保证对count变量的互斥访问 +semaphore mutex = 1; + +writer() { + while(1){ + P(rw); // 写之前”加锁” + 写文件... + V(rw); // 写之后"解锁” + } +} + +reader() { + while(1){ + P(mutex); // 各读进程互斥访问count + if(count==0){ + P(rw); //第一个读进程负责"加锁” + } + count++; // 访问文件的读进程数+1 + V(mutex); // 释放对count的锁 + 读文件... + P(mutex); // 各读进程互斥访问count + count--; // 访问文件的读进程数-1 + if(count==0){ + v(rw); // 最后一个读进程负责”解锁" + } + V(mutex); // 释放对count的锁 + } +} +``` + +这个算法中读进程是优先的,(因为只要有读进程读写进程就永远无法写而读进程可以一直读),所以写进程可能饥饿。 + +若希望写进程优先,则当读进程读共享文件时,有写进程访问就立刻禁止后续读进程的请求,当前所有读进程都执行完毕后立刻执行写进程,只有无写进程执行再执行读进程。 + +所以需要再添加一个信号量并进行一对$PV$操作。 + +```cpp +// 用于实现对文件的互斥访问。表示当前是否有进程在访问共享文件,1代表空闲 +semaphore rw = 1; +// 记录当前有几个读进程在访问文件 +int count = 0; +// 用于保证对count变量的互斥访问 +semaphore mutex = 1; +// 用于实现"写优先" +semaphore w=1; + +writer() { + while(1){ + P(w); // 当无其他写进程时进入写 + P(rw); // 写之前”加锁” + 写文件... + V(rw); // 写之后"解锁” + V(w); // 恢复对共享文件的可读可写 + } +} + +reader() { + while(1){ + P(w); // 当无写进程时进入,若当前有写进程就不允许有新的读进程读,在这里堵塞 + P(mutex); // 各读进程互斥访问count + if(count==0){ + P(rw); //第一个读进程负责"加锁” + } + count++; // 访问文件的读进程数+1 + V(mutex); // 释放对count的锁 + V(w); // 恢复对共享文件的可读可写,w的V操作位置无所谓 + 读文件... + P(mutex); // 各读进程互斥访问count + count--; // 访问文件的读进程数-1 + if(count==0){ + v(rw); // 最后一个读进程负责”解锁" + } + V(mutex); // 释放对count的锁 + } +} +``` + +当前实现的是实际上是按照进程进入顺序来执行,是**读写公平法**,若有多个读进程下一个执行的进程仍是读进程,没有真正实现写进程最优先。 + ++ 读者写者问题为我们解决复杂的互斥问题提供了一个参考思路。 ++ 其核心思想在于设置了一个计数器`count`用来记录当前正在访问共享文件的读进程数。我们可以用`count`的值来判断当前进入的进程是否是第一个/最后一个读进程,从而做出不同的处理。 ++ 另外,对`count`变量的检查和赋值不能一气呵成导致了一些错误,如果需要实现“一气呵成”,自然应该想到用互斥信号量对`count`进行$PV$操作。 ++ 最后,还要认真体会我们是如何解决“写进程饥饿”问题的。 ++ 绝大多数的考研$PV$操作大题都可以用之前介绍的几种生产者消费者问题的思想来解决,如果遇到更复杂的问题,可以想想能否用读者写者问题的这几个思想来解决。 + +#### 哲学家进餐问题 + +一张圆桌上坐着$5$名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭。哲学家们倾注毕生的精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿时,才试图拿起左、右两根筷子(一根一根地拿起)。如果筷子已在他人手上,则需等待。饥饿的哲学家只有同时拿起两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考。 + +系统中有$5$个哲学家进程,$5$位哲学家与左右邻居对其中间筷子的访问是互斥关系。 + +这个问题中只有互斥关系,但与之前遇到的问题不同的是,每个哲学家进程需要同时持有两个临界资源才能开始吃饭。如何避免临界资源分配不当造成的死锁现象,是哲学家问题的精髓。 + +信号量设置。定义互斥信号量数组`chopstick[5]={1,1,1,1,1}`用于实现对$5$个筷子的互斥访问。并对哲学家按$0\sim4$编号,哲学家$i$左边的筷子编号为$i$,右边的筷子编号为$(i+1)\%5$。 + +为了防止死锁: + +1. 可以对哲学家进程施加一些限制条件,比如最多允许四个哲学家同时进餐。这样可以保证至少有一个哲学家是可以拿到左右两只筷子的。 +2. 要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反。用这种方法可以保证如果相邻的两个奇偶号哲学家都想吃饭,那么只会有其中一个可以拿起第一只筷子,另一个会直接阻塞。这就避免了占有一支后再等待另一只的情况。 +3. 同时仅允许当一个哲学家左右两支筷子都可用时才允许他抓起筷子。(一个时刻只有一个哲学家才能尝试拿筷子) + +使用第三种方法: + +```cpp +semaphore chopstick[5]={1,1,1,1,1}; // 设置五根筷子的信号量 +semaphore mutex = 1; // 互斥地取筷子 + +//i号哲学家的进程 +Pi() { + while(1){ + P(mutex); // 取筷子前获取互斥量 + P(chopstick[i]); // 拿左 + P(chopstick[(i+1)%5]); // 拿右 + V(mutex); // 释放互斥量 + 吃饭... + V(chopstick[i]); // 放左 + V(chopstick[(i+1)%5]); // 放右 + 思考... + } +} +``` + ++ 哲学家进餐问题的关键在于解决进程死锁。 ++ 这些进程之间只存在互斥关系,但是与之前接触到的互斥关系不同的是,每个进程都需要同时持有两个临界资源,因此就有“死锁”问题的隐患。 ++ 如果在考试中遇到了一个进程需要同时持有多个临界资源的情况,应该参考哲学家问题的思想,分析题中给出的进程之间是否会发生循环等待,是否会发生死锁。 + #### 抽烟者问题 假设一个系统有三个抽烟者进程和一个供应者进程。每个抽烟者不停地卷烟并抽掉它,但是要卷起并抽掉一支烟,抽烟者需要有三种材料:烟草、纸和胶水。三个抽烟者中,第一个拥有烟草、第二个拥有纸、第三个拥有胶水。供应者进程无限地提供三种材料,供应者每次将两种材料放桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者进程一个信号告诉完成了,供应者就会放另外两种材料再桌上,这个过程一直重复(让三个抽烟者轮流地抽烟)。 -桌子可以抽象为容量为1的缓冲区,要互斥访问。 +桌子可以抽象为容量为$1$的缓冲区,要互斥访问。 组合一:纸+胶水;组合二:烟草+胶水;组合三:烟草+纸。 -同步关系(从事件的角度来分析):桌上有组合一→第一个抽烟者取走东西;桌上有组合二→第二个抽烟者取走东西;桌上有组合三→第三个抽烟者取走东西;发出完成信号→供应者将下一个组合放到桌上 -。 +同步关系(从事件的角度来分析):桌上有组合一→第一个抽烟者取走东西;桌上有组合二→第二个抽烟者取走东西;桌上有组合三→第三个抽烟者取走东西;发出完成信号→供应者将下一个组合放到桌上。 -对于同步关系分别设置offer1=0、offer2=0、offer3=0、finish=0。 +抽烟者抽烟与供应者准备烟互斥,同理由于缓冲区为$1$,所以互斥变量`mutex`可有可无,因为四个信号量同时只能有一个为$1$,天然互斥。 + +对于同步关系分别设置`offer1=0`、`offer2=0`、`offer3=0`、`finish=0`。 ```cpp semaphore offer1 = 0; // 桌上组合一的数量 @@ -1241,8 +1411,8 @@ provider() { } smoker1() { - while(1){ - P(offer1); + while(1){ + P(offer1); // 占有组合 从桌上拿走组合一; 卷烟; 抽掉; @@ -1252,8 +1422,8 @@ smoker1() { } smoker2() { - while(1){ - P(offer2); + while(1){ + P(offer2); // 占有组合 从桌上拿走组合而; 卷烟; 抽掉; @@ -1263,8 +1433,8 @@ smoker2() { } smoker3() { - while(1){ - P(offer3); + while(1){ + P(offer3); // 占有组合 从桌上拿走组合三; 卷烟; 抽掉; @@ -1279,113 +1449,6 @@ smoker3() { + 值得吸取的精华是“轮流让各个吸烟者吸烟”必然需要“轮流的在桌上放上组合一、二、三”,注意体会我们是如何用一个整型变量i 实现这个“轮流”过程的。 + 若一个生产者要生产多种产品(或者说会引发多种前驱事件),那么各个V操作应该放在各自对应的“事件”发生之后的位置。 -#### 读者写者问题 - -有读者和写者两组并发进程,共享一个文件,当两个或两个以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求: - -1. 允许多个读者可以同时对文件执行读操作。 -2. 只允许一个写者往文件中写信息。 -3. 任一写者在完成写操作之前不允许其他读者或写者工作写。 -4. 写者执行写操作前,应让已有的读者和写者全部退出。 - -具有两类进程:写进程、读进程。 - -互斥关系:写进程-写进程、写进程-读进程。读进程与读进程不存在互斥问题。 - -写者进程和任何进程都互斥,设置一个互斥信号量rw,在写者访问共享文件前后分别执行P、V操作。 - -读者进程和写者进程也要互斥,因此读者访问共享文件前后也要对rw执行P、V操作。 - -如果所有读者进程在访问共享文件之前都执行P(rw)操作,那么会导致各个读进程之间也无法同时访问文件。所以读者写者问题的核心思想――怎么处理该读者共享的问题呢? - -P(rw)和V(rw)其实就是对共享文件的“加锁”和“解锁”。既然各个读进程需要同时访问,而读进程与写进程又必须互斥访问,那么我们可以让第一个访问文件的读进程“加锁”,让最后一个访问完文件的读进程“解锁”。可以设置一个整数变量count来记录当前有几个读进程在访问文件。 - -```cpp -// 用于实现对文件的互斥访问。表示当前是否有进程在访问共享文件,1代表空闲 -semaphore rw = 1; -// 记录当前有几个读进程在访问文件 -int count = 0; -// 用于保证对count变量的互斥访问 -semaphore mutex = 1; -// 用于实现"写优先" -semaphore w=1; - -writer() { - while(1){ - P(w); - P(rw); // 写之前”加锁” - 写文件... - V(rw); // 写之后"解锁” - } -} - -reader() { - while(1){ - P(w); - P(mutex); // 各读进程互斥访问count - if(count==0){ - P(rw); //第一个读进程负责"加锁” - } - count++; // 访问文件的读进程数+1 - V(mutex); - V(w); - 读文件.... - P(mutex); // 各读进程互斥访问count - count--; // 访问文件的读进程数-1 - if(count==0){ - v(rw); // 最后一个读进程负责”解锁" - } - V(mutex); - } -} -``` - -+ 读者写者问题为我们解决复杂的互斥问题提供了一个参考思路。 -+ 其核心思想在于设置了一个计数器count用来记录当前正在访问共享文件的读进程数。我们可以用count的值来判断当前进入的进程是否是第一个/最后一个读进程,从而做出不同的处理。 -+ 另外,对count变量的检查和赋值不能一气呵成导致了一些错误,如果需要实现“一气呵成”,自然应该想到用互斥信号量。 -+ 最后,还要认真体会我们是如何解决“写进程饥饿”问题的。 -+ 绝大多数的考研PV操作大题都可以用之前介绍的几种生产者消费者问题的思想来解决,如果遇到更复杂的问题,可以想想能否用读者写者问题的这几个思想来解决。 - -#### 哲学家进餐问题 - -一张圆桌上坐着5名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭。哲学家们倾注毕生的精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿时,才试图拿起左、右两根筷子(一根一根地拿起)。如果筷子已在他人手上,则需等待。饥饿的哲学家只有同时拿起两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考。 - -系统中有5个哲学家进程,5位哲学家与左右邻居对其中间筷子的访问是互斥关系。 - -这个问题中只有互斥关系,但与之前遇到的问题不同的是,每个哲学家进程需要同时持有两个临界资源才能开始吃饭。如何避免临界资源分配不当造成的死锁现象,是哲学家问题的精髓。 - -信号量设置。定义互斥信号量数组 -chopstick[5]={1,1,1,1,1}用于实现对5个筷子的互斥访问。并对哲学家按0~4编号,哲学家i左边的筷子编号为i,右边的筷子编号为(i+1)%5。 - -为了防止死锁: - -1. 可以对哲学家进程施加一些限制条件,比如最多允许四个哲学家同时进餐。这样可以保证至少有一个哲学家是可以拿到左右两只筷子的。 -2. 要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反。用这种方法可以保证如果相邻的两个奇偶号哲学家都想吃饭,那么只会有其中一个可以拿起第一只筷子,另一个会直接阻塞。这就避免了占有一支后再等待另一只的情况。 -3. 仅当一个哲学家左右两支筷子都可用时才允许他抓起筷子。 - -```cpp -semaphore chopstick[5]={1,1,1,1,1}; -semaphore mutex = 1; // 互斥地取筷子 - -//i号哲学家的进程 -Pi() { - while(1){ - P(mutex); - P(chopstick[i]); // 拿左 - P(chopstick[(i+1)%5]); // 拿右 - V(mutex); - 吃饭... - V(chopstick[i]); // 放左 - V(chopstick[(i+1)%5]); // 放右 - 思考... - } -} -``` - -+ 哲学家进餐问题的关键在于解决进程死锁。 -+ 这些进程之间只存在互斥关系,但是与之前接触到的互斥关系不同的是,每个进程都需要同时持有两个临界资源,因此就有“死锁”问题的隐患。 -+ 如果在考试中遇到了一个进程需要同时持有多个临界资源的情况,应该参考哲学家问题的思想,分析题中给出的进程之间是否会发生循环等待,是否会发生死锁。 - ### 管程 此前一般使用信号量机制来完成进程互斥同步,但是编写程序困难,易出错。 @@ -1477,8 +1540,8 @@ monitor Demo { #### 死锁发生的情况 -1. 对系统资源的竞争。各进程对不可剥夺的资源(如打印机)的竞争可能引起死锁,对可剥夺的资源($CPU$)的竞争是不会引起死锁的。 -2. 进程推进顺序非法。请求和释放资源的顺序不当,也同样会导致死锁。例如,并发执行的进程P1、P2分别申请并占有了资源R1、R2,之后进程P1又紧接着申请资源R2,而进程P2又申请资源R1,两者会因为申请的资源被对方占有而阻塞,从而发生死锁。 +1. 对系统资源的竞争。各进程对不可剥夺的资源(如打印机)的竞争可能引起死锁,对可剥夺的资源($CPU$)的竞争是不会引起死锁的。 +2. 进程推进顺序非法。请求和释放资源的顺序不当,也同样会导致死锁。例如,并发执行的进程$P1$、$P2$分别申请并占有了资源$R1$、$R2$,之后进程$P1$又紧接着申请资源$R2$,而进程$P2$又申请资源$R1$,两者会因为申请的资源被对方占有而阻塞,从而发生死锁。 3. 信号量的使用不当也会造成死锁。如生产者-消费者问题中,如果实现互斥的P操作在实现同步的Р操作之前,就有可能导致死锁。(可以把互斥信号量、同步信号量也看做是一种抽象的系统资源) 总之,对不可剥夺资源的不合理分配,可能导致死锁。