操作系统实验三 同步问题
16281049 王晗炜 计科1601
实验目的
- 系统调用的进一步理解
- 进程上下文切换
- 同步的方法
实验题目
-
通过fork的方式,产生4个进程P1,P2,P3,P4,每个进程打印输出自己的名字,例如P1输出“I am the process P1”。要求P1最先执行,P2、P3互斥执行,P4最后执行。通过多次测试验证实现是否正确。
-
根据题目执行顺序要求画出前驱图
-
四个进程实现前驱关系所需要的信号量
经分析可知若需达到以上执行顺序的要求,共需要三个信号量来进行进程之间的约束:
- mySem_1:保证P2和P3在P1之后互斥执行,初始值为0
- mySem_2:保证P4在P2之后运行,初始值为0
- mySem_3:保证P4在P3之后运行,初始值为0
进程 执行前 执行后 P1 无 sem_post(mySem_1) P2 sem_wait(mySem_1) sem_post(mySem_1) sem_post(mySem_2) P3 sem_wait(mySem_1) sem_post(mySem_1) sem_post(mySem_3) P4 sem_wait(mySem_2) sem_wait(mySem_3) sem_post(mySem_2) sem_post(mySem_3) 在进程执行的过程中,因为三个信号量的初始值均为0,只有P1执行前没有wait要求,因此P1一定会是第一个执行的,在其执行完成后,会对mySem_1信号量进行post操作,使其值变为1,如此处于wait状态下的P2和P3进程便可有一个可以进行工作、另一个继续等待,当其中一个执行完成后会再次post信号量mySem_1,另一个进程也可进入工作,如此便可完美实现互斥执行。而对于P4,信号量mySem_2和mySem_3对其的约束使得其只能在P2和P3均完成工作后进入工作,所以P4一定是最后一个进入工作的进程。
如此一来,四个进程的前驱关系便通过这三个信号量实现了。
-
实现源码
3-1.c
:#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <fcntl.h> #include <semaphore.h> int main(int argc,char *argv[]){ sem_t *mySem_1 = NULL; //控制p1、p2和p3进程打信号量 sem_t *mySem_2 = NULL; //控制p4进程打信号量 sem_t *mySem_3 = NULL; //控制p4进程打信号量 mySem_1 = sem_open("mySemName_1",O_CREAT,0666,0); mySem_2 = sem_open("mySemName_2",O_CREAT,0666,0); mySem_3 = sem_open("mySemName_3",O_CREAT,0666,0); pid_t pid_1,pid_2,pid_3,pid_4; pid_2 = fork(); if(pid_1 == 0){ pid_2 = fork(); if(pid_2 == 0){ pid_3 = fork(); if(pid_3 == 0){ pid_4 = fork(); if(pid_4 == 0){ sem_wait(mySem_2); sem_wait(mySem_3); printf("I an the process P4\n"); sem_post(mySem_2); sem_post(mySem_3); } else if(pid_4>0){ sem_wait(mySem_1); printf("I an the process P3\n"); sem_post(mySem_1); sem_post(mySem_3); } } else if(pid_3>0){ sem_wait(mySem_1); printf("I an the process P2\n"); sem_post(mySem_1); sem_post(mySem_2); } } else if(pid_2>0){ printf("I an the process P1\n"); sem_post(mySem_1); } } sem_close(mySem_1); sem_close(mySem_2); sem_close(mySem_3); unlink("mySemName_1"); unlink("mySemName_2"); unlink("mySemName_3"); return 0; }
-
源码执行流程图
题目中要求使用fork的方式创建4个进程,而进程工作执行的顺序已由上文给出,因此以下只给出程序创建进程的顺序。
-
测试情况
在Linux终端下使用gcc编译该源程序
连续多次运行程序并观察结果:
可知只会出现P1->P2->P3->P4和P1->P3->P2->P4两种运行次序,与此前分析的一致。
-
-
火车票余票数ticketCount 初始值为1000,有一个售票线程,一个退票线程,各循环执行多次。添加同步机制,使得结果始终正确。要求多次测试添加同步机制前后的实验效果。(说明:为了更容易产生并发错误,可以在适当的位置增加一些pthread_yield(),放弃CPU,并强制线程频繁切换,例如售票线程的关键代码:
temp=ticketCount;
pthread_yield();
temp=temp-1;
pthread_yield();
ticketCount=temp;
退票线程的关键代码:
temp=ticketCount;
pthread_yield();
temp=temp+1;
pthread_yield();
ticketCount=temp;
)
-
问题分析
这个问题实质上是一个多线程访问并修改临界资源的问题,如果两个线程在不加约束的情况下对临界资源进行修改则会使得有的线程会读取脏数据,造成错误。这里我们便需要使用一个信号量来制造两个线程之间的互斥访问,使得每次读取的数据都是正确的。
在现实生活中火车票的购票与退票是一个并发量巨大的实际问题,这里我们的程序中只有两个线程,肯定是不能与其相提并论的,因此这里为了更加贴近其并发数量,使用了pthread_yield函数。此函数的作用为使当前线程暂时放弃CPU,使用另一个级别等于或高于当前线程的线程先运行。如果没有符合条件的线程,那么这个函数将会立刻返回然后继续执行当前线程的程序。因此在程序加入此函数会增加线程之间的切换,增加并发带来的脏数据问题。
-
未添加同步代码及测试
程序源代码
3-2_1.c
:#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <semaphore.h> volatile int ticketCount = 1000; void *sell(void *arg){ int temp; for(int i = 0;i < atoi(arg);i++){ temp = ticketCount; pthread_yield(); temp = temp - 1; pthread_yield(); ticketCount = temp; } return NULL; } void *refund(void *arg){ int temp; for(int i = 0;i < atoi(arg);i++){ temp = ticketCount; pthread_yield(); temp = temp + 1; pthread_yield(); ticketCount = temp; } return NULL; } int main(int argc,char *argv[]){ pthread_t p1,p2; if(argc!=3){ printf("3-2<sell_num refund_num>\n"); exit(1); } signal = sem_open("signal",O_CREAT,0666,1); pthread_create(&p1,NULL,sell,argv[1]); pthread_create(&p2,NULL,refund,argv[2]); pthread_join(p1,NULL); pthread_join(p2,NULL); printf("余票数为:%d\n",ticketCount); sem_close(signal); return 0; }
此程序除main函数外含两个函数,分别为sell售票函数,refund退票函数。ticketCount函数可被多个线程访问,代表余下的票数。
在程序运行时,需要输入两个参数,分别代表售票数和退票数,否则便会直接在屏幕上打印出*3-2<sell_num refund_num>*提示错误。
下面在Linux终端对程序进行编译并运行测试:
测试条件为卖票1000张,退票800张,正确的余票数应该为800张(1000-1000+800),但运行的5次结果无一为800且各不相同。这其实就是刚刚所说的读取脏数据带来的错误,具有很大的随机性,经分析我们可以得到次程序如此条件运行后的结果范围:
- 最小值:0,假设售票线程和退票线程在开始一同读取了ticketCount(1000),此时退票线程先进行了800次,ticketCount被写为1800,但此后售票线程开始执行,其不会再次读取最新的ticketCount值,第一次执行写回的值为999(1000-1),直接将刚刚退票线程写入的1800覆盖了,之后再执行999次便会得到0余票,这是得到最小值的情况。
- 最大值:1800,假设售票线程和退票线程在开始一同读取了ticketCount(1000),此时售票线程先进行了1000次,ticketCount被写为0,但此后退票线程开始执行,其不会再次读取最新的ticketCount值,第一次执行写回的值为1001(1000+1),直接将刚刚退票线程写入的0覆盖了,之后再执行799次便会得到800余票,这是得到最大值的情况。
可知以上结果均在[0,1800]区间内,符合分析预期。
-
添加同步之后的代码及测试
在程序中使用信号量signal,将其初值设置为1,在售票进程或退票进程之前需进行sem_wait(signal)操作,访问完成之后再进行sem_post(signal)操作,如此便可实现两个线程之间互斥运行,以下为修改过后的源码
3-2_2.c
:#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <semaphore.h> volatile int ticketCount = 1000; sem_t *signal = NULL; void *sell(void *arg){ int temp; for(int i = 0;i < atoi(arg);i++){ sem_wait(signal); temp = ticketCount; pthread_yield(); temp = temp - 1; pthread_yield(); ticketCount = temp; sem_post(signal); } return NULL; } void *refund(void *arg){ int temp; for(int i = 0;i < atoi(arg);i++){ sem_wait(signal); temp = ticketCount; pthread_yield(); temp = temp + 1; pthread_yield(); ticketCount = temp; sem_post(signal); } return NULL; } int main(int argc,char *argv[]){ pthread_t p1,p2; if(argc!=3){ printf("3-2<sell_num refund_num>\n"); exit(1
-