一、实验内容
模拟实现用同步机构避免并发进程执行时可能出现的与时间有关的错误。
实验报告要求:
(1)实验题目。
(2)打印源程序并附上注释。
(3)从键盘上输入一组字符,由生产者每次读入一个字符供消费者输出。运行模拟程序,打印依次读入的字符和消费者输出的字符。
(4)把生产者和消费者进程中的P操作、V操作都改成空操作指令,观察在两者不同步的情况下可能出现的与时间有关的错误。打印依次读入的字符和消费者输出的字符。
二、实验目的
进程是程序在一个数据集合上运行的过程,进程是并发执行的,也即系统中的多个进程轮流地占用处理器运行。
我们把若干个进程都能进行访问和修改的那些变量称为公共变量。由于进程是并发执行的,所以,如果对进程访问公共变量不加限制,那么就会产生“与时间有关”的错误,即进程执行后,所得到的结果与访问公共变量的时间有关。为了防止这类错误,系统必须要用同步机构来控制进程对公共变量的访问。一般说,同步机构是由若干条原语——同步原语——所组成。本实验要求学生模拟PV操作同步机构的实现,模拟进程的并发执行,了解进程并发执行时同步机构的作用。
三、实验题目
模拟PV操作同步机构,且用PV操作解决生产者——消费者的问题。
(1) PV操作同步机构,由P操作原语和V操作原语组成,它们的定义如下:
P操作原语P(s):将信号量s减去1,若结果小于0,则执行原语的进程被置成等待信号量s的状态。
V操作原语V(s): 将信号量s加1,若结果不大于0,则释放一个等待信号量s的进程。
(2)生产者-消费者问题
假定有一个生产者和消费者,生产者每次生产一件产品,并把生产的产品存入共享缓冲器以供消费者取走使用。消费者每次从缓冲器内取出一件产品去消费。禁止生产者将产品放入已满的缓冲器内,禁止消费者从空缓冲器内取产品。假定缓冲器内可同时存放10件产品。那么,用PV操作来实现生产者和消费者之间的同步。
(3)进程控制块PCB
为了记录进程执行时的情况、以及进程让出处理器后的状态、断点等信息,每个进程都有一个进程控制块PCB。在模拟实验中,假设进程控制块的结构如图1。其中进程的状态有:运行态、就绪态、等待态和完成态。当进程处于等待态时,在进程控制块PCB中要说明进程等待原因(在模拟实验中进程等待原因为等待信号量s 1 s_1s1或s 2 s_2s2。当进程处于等待态或就绪态时,PCB 中保留了断点信息,一旦进程再度占有处理器则就从断点位置继续运行;当进程处于完成状态,表示进程执行结束。
进程名 |
状态 |
等待原因 |
断点 |
(4)处理器的模拟
计算机硬件提供了一组机器指令,处理器的主要职责是解释执行机器指令。为了模拟生产者和消费者进程的并发执行,我们必须模拟一组指令和处理器职能。
模拟的一组指令见图2,其中每条指令的功能由一个过程来实现。用变量PC来模拟“指令计数器”,假设模拟的指令长度为1,每执行一条模拟指令后, PC加1,指出下一条指令地址。使用模拟的指令,可把生产者和消费者进程的程序表示为图3的形式。定义两个一维数组PA[5]和SA[5],每一个PA[i]存放生产者程序中的一条模拟指令执行的入口地址;每个SA[i]存放消费者程序中的一条模拟指令执行的入口地址。于是模拟处理器执行条指令的过程为: 取出PC之值, 按PA[PC] 或SA[PC]得模拟指令执行的入口地址,将PC之值加1,转向由入口地址确定的相应的过程执行。
模拟的指令 | 功能 |
P(s) | 执行P操作原语 |
V(s) | 执行V操作原语 |
put | B[in]=product;in=(in+1)%10 |
get | x=B[out],out=(out+1)%10 |
produce | 输出一个字符放入C中 |
consume | 消费一个x中的字符 |
Goto L | 跳转,回到第0个过程 |
nop | 空操作 |
序号 | 生产者程序 | 消费者程序 |
0 | produce | P(s 2 s_2s2) |
1 | P(s 1 s_1s1) | get |
2 | put | V(s 2 s_2s2) |
3 | V(s 1 s_1s1) | consume |
4 | Goto 0 | Goto 0 |
(5)程序设计
本实验中的程序由三部分组成:初始化程序、处理器调度程序、模拟处理器指令执行程序。各部分程序的功能及相互间的关系由图4至图7指出。
初始化程序:模拟实验的程序从初始化程序入口启动,初始化工作包括对信号量S1、S2赋初值,对生产者、消费者进程的PCB初始化。初始化后转向处理器调度程序,其流程如图4。
处理器调度程序:在计算机系统中,进程并发执行时,任一进程占用处理器执行完一条指令后就有可能被打断而让出处理器由其他进程运行。故在模拟系统中也类似处理,每当执行一条模拟的指令后,保护当前进程的现场,让它成为非运行状态,由处理器调度程序按随机数再选择一个就绪进程占用处理器运行。处理器调度程序流程见图5.
模拟处理器指令执行程序:按“ 指令计数器”PC之值执行指定的过程,且PC加1指向下一条指令。模拟处理器指令执行的程序流程见图6和7。
另外,为了使得模拟程序有一个结束条件,在图6中附加了“生产者运行结束”的条件判断,模拟时可以采取人工选择的方法实现。图7给出了P (S)和V (S)模拟指令执行过程的流程。其他模拟指令的执行过程已在图4-2中指出。
总体流程
处理器调度流程
处理器执行流程(实验指导书有误!)
PV操作流程(实验指导书有误!!!)
四、程序代码
1.数据结构与变量
struct PCB {//进程控制块 string name;//进程名 int status; // 1 就绪, 2 等待,3 完成,0 正在运行 int waitreason;//等待原因 int breakingpoint;//断点 };
因为本问题有且只有一个生产者进程,有且只有一个消费者进程,所以不需要用到队列等数据结构。只需一个指针指向当前运行的进程。
PCB producer;//生产者进程 PCB consumer;//消费者进程 PCB* currp;//当前进程
本程序需要两个信号量,分别为s1和s2。
s1值<0时,表示缓冲区已经放满了,生产者不能再放数据了。S2值>=0,表示有数据,可以让消费者去拿数据。
int s1;//信号量:生产者可以往缓冲区放物品 int s2;//消费者可以从缓冲区拿,生产者有物品
同样的,也需要缓冲区。缓冲区命名为B,与图2是一致的。缓冲区的长度为10。设置指针in为生产者放的位置,out为消费者拿的位置。Char变量c为生产者生产的字符,变量x为消费者消费的字符。
int in;//生产者放的位置 int out;//消费者拿的位置 char B[10]; char c;//生产者生产的字符 char x;//消费者消费的字符
Pc为程序计数器,指向生产者或消费者当前在运行哪条程序。生产者需要运行的程序地址在pa数组中,消费者运行的程序地址在sa数组中。“地址”并非真正的地址,在后文的算法说明中会体现,算法会读出“地址”之值,利用case语句根据“地址”值决定执行哪条指令。
int pc; int pa[5];//生产者的程序 int sa[5];//消费者程序
2.程序设计与算法
在主程序中,整体流程为:先初始化,后进行处理机运行。
int main() { init(); cpuwork(); return 0; }
init()函数进行初始化。在初始化时,先初始化信号量,再初始化当前生产者和消费者指向的缓冲区的位置0。初始化生产者进程和消费者进程的状态。初始化程序的“地址”。
void init() { s1 = 10; s2 = 0;//初始化信号量 out = 0; in = 0;//初始化生产者与消费者在缓冲区放的商品的位置 producer.name = "producer"; producer.status = 1; producer.waitreason = 0; producer.breakingpoint = 0;//pcb状态为就绪,断点为0 consumer.name = "consumer"; consumer.status = 1; consumer.waitreason = 0; consumer.breakingpoint = 0;//pcb状态为就绪,断点为0 currp = &producer;//将现行进程设置为生产者进程 pc = 0;//pc=0 for (int i = 0; i <= 4; i++) { pa[i] = i; sa[i] = i;//初始化生产者程序与消费者程序 } }
P操作根据信号量值决定当前状态是被阻塞,并设置断点,或者是程序能够继续就绪运行。
void p(int& s, int p, PCB* pcb) { s = s - 1; cout << "mutex s" << p<<endl; if (s < 0) { pcb->status = 2;//等待状态 pcb->waitreason = p;//等待的信号量是p cout << pcb->name << " begins waiting!\n"; } else { cout << pcb->name << " doesn't need waiting!\n"; pcb->status = 1;//将调用p(s)的进程置为就绪 } }
V操作根据信号量值决定是否要将等待当前信号量的进程设置为就绪。需要说明的是,判断条件是s<=0,而不是s<0。指导书有误
void v(int& s, int p, PCB* pcb) { s = s + 1; if (s <= 0) { if (producer.waitreason == p) producer.status = 1; else if (consumer.waitreason == p) consumer.status = 1;//将一个等待s信号量的进程置为就绪 if (currp->name == "producer") cout << "consumer released!"; else cout << "producer released!"; } pcb->status = 1;//将调用V(s)的进程置为就绪 }
在处理机调度时,若有多个就绪进程,随机选择一个就绪进程置为运行态。把pcb断点值赋值,并对该进程进行运行。
while (1) { currp->breakingpoint = pc;//当前进程pcb断点为pc if (consumer.status != 1 && producer.status != 1) return;//无就绪进程 if (consumer.status == 1 && producer.status == 1) { srand(time(NULL));//随机选择一个就绪进程 int randnum = rand() % 2; if (randnum >= 1) currp =& consumer; else currp =& producer; } else if (consumer.status == 1) currp = &consumer; else if(producer.status==1) currp = &producer; currp->status = 0;//将现行进程改为运行态 pc = currp->breakingpoint;//现行进程pcb断点值赋值给pc exec(); }
在处理器执行当前正在运行的程序时,利用程序计数器选择当前进程指令“地址”i,通过“地址”i找到需要运行的指令j。之后,程序计数器指向下一条地址。利用case语句,根据j值执行对应指令。指令如图3所示。每条指令的内容如图2所示。可以发现,对于每条指令,在程序中的执行与图表要求执行的语句是一致的。
void exec() {//模拟处理器指令执行 int i = pc; int j; int curq; cout << "current process " << currp->name << endl; if (currp->name == "producer") {//现行进程为生产者? j = pa[i]; curq = 0; } else { j = sa[i]; curq = 1; } pc = i + 1; switch (j) {//按j转向各模拟指令对应的过程 case 0: if (curq == 0) {//produce char pronum; cout << "please produce a char"; cin >> pronum; c = pronum; cout << "produced!"; currp->status = 1; } else p(s2, 2, currp);//p(s2) break; case 1: if (curq == 0) p(s1, 1, currp);//p(s1) else {//GET cout << "get goods!"; x = B[out]; out = (out + 1) % 10; cout << x << endl; currp->status = 1; } break; case 2: if (curq == 0) {//PUT cout << "put goods!"; B[in] = c; in = (in + 1) % 10; cout << c << endl; currp->status = 1; } else v(s1, 1, currp);//V(s1) break; case 3: if (curq == 0)//V(s2) v(s2, 2, currp); else {//consume cout << "goods consumed!"; cout << x << endl; currp->status = 1; } break; case 4://goto 0 pc = 0; currp->status = 1; cout << "goto 0!"; break; } if (producer.status != 3) {//模拟时采用人工选择的办法实现“生产者运行结束”的判断 cout << "Has producer finished?Y/N"; char st; cin >> st; if (st == 'Y') producer.status = 3; } return; }