操作系统实验三
1 通过fork产生四个进程
因为要产生四个进程,所以我们只需要两次fork。又因为要求P1最先执行,P2 P3互斥,P4最后执行,所以我们总共需要五个信号量,其中一个信号量保证p2 p3互斥,另外四个信号量保证执行的顺序,对应的前趋图如下。
P1{print,signal( a ),signal( b )}
P2{wait( b ),print,signal( d )}
P3{wait( a ),print,signal( c )}
P4{wait( c ),wait( d ),print}
下图中左侧为程序刚刚执行时的结果,右侧为程序执行一段之间之后的结果,可以看到,无论在什么时候,都是p1先执行,p4最后执行,而因为p2 p3时互斥执行的,所以会得到如下的两种结果。
2 火车票票数
添加同步机制前:
可以看到,在添加同步机制前,剩余票数没有正确的。这是因为当售票进程卖出票之后,此时还未完成对ticketCount的修改,但退票进程却已经读取了ticketCount的值。举个例子也就是说,退票进程本应读到的值为999,但却读到了修改之前的1000。因此剩余票数出现了错误。
添加同步机制后:
可以看到,在我们添加了同步机制之后,因为使用了pthred_mutex_lock,所以只有当售票进程完成修改之后,退票进程才能进入执行自己的代码。中途虽然使用了一些sched_yield,但依然不会影响到我们的结果。
ps:实验指导书中给我们的是使用pthread_yield函数,但由于其不是官方库里的函数,所以在编译的过程中一直出问题,故改为了使用sched_yield函数,sched_yield是官方库函数,拥有着与pthread_yield一样的功能。
3 生产者-消费者线程同步
添加同步机制前
可以看到,当我们输入10位及以内的字符时,程序可以正常输出,但是当我们输入的字符超过10位时,我们一开始输入的内容就会被覆盖,无法得到我们想要的结果。
添加同步机制后
可以看到,在添加了同步机制之后,无论我们是输入10位以内的字符,还是输入多于10位的字符,程序都可以正确的输出我们刚刚输入的内容。
4 进程通信问题
4.1共享内存
4.1.1测试并验证
sender:
receiver:
可以看到,当我们仅发送“hello”时,receiver可以正确的接收到我们的信息。但是,当我们的内容中还有空格,即发送“how are you ”时,receiver便不能正确的接受信息,而是以空格为分隔符,将信息分成了三部分。
4.1.2删除互斥代码
当我们将互斥的代码删除之后,可以看到receiver端一直在输出我们发送的信息,即使没有信息发送时,也依然在输出着空白内容。
4.1.3共享内存地址
由函数的定义我们可以了解到,void *shmat()的返回值即为共享内存的地址,所以我们打印输出它即可。通过输出结果我们可以看到,二者的共享内存地址并不相同。这是因为共享内存地址其实是两个进程通过各自的页表将虚拟地址映射到同一块物理内存,从而达到共享的目的。我们看到的,其实是进程的虚拟地址,所以两个进程的地址会有所不同。
(参考自 https://blog.csdn.net/ypt523/article/details/79958188)
4.2 有名管道和无名管道
4.2.1无名管道
无名管道实现了同步机制。无名管道用于父子进程之间的通信,是单向的,允许两个进程按标准的生产者-消费者方式进行通信。父进程作为生产者,向管道的一端写入消息,子进程作为消费者,从管道的另一端读出数据。同步机制的实现,也与生产者-消费者类似。
当进行写操作时,若管道内数据未满时,则可执行正常的写操作,若管道内数据已满,则需要等待。进行读操作时,若管道内有数据,则获取数据。如下图,我们先写入数据,再读出,操作成功。
当进行读操作时,若管道为空,则进行等待,直到管道内有数据写入。如下图,因为管道为空且没有数据写入,所以程序一直处于等待状态。
当进行写操作时,若管道未满,则可一直进行写操作。进行读操作时,若管道不空,则可以一直读。如下两图所示。
除此之外,当一个进程在对管道进行读写操作时,另一个进程必须等待。而且只有管道两端的进程都存在时,才能进行管道通信。
4.2.2有名管道通信
与无名管道不同的是,有名管道可以实现两个互不相关进程之间的通信,有名管道可以通过路径名来指出。对于有名管道,其同步机制大体上与无名管道类似。若只有读(写)进程,则进程阻塞,直到打开对应的写(读)进程。若有名管道为空,则可以执行写进程,读进程需等待,若有名管道已满,则可执行读进程,写进程需等待。当一个进程执行读\写操作时,另一个进程需要等待。
当管道内为空时,我们可以正常执行写操作
当管道内有数据时,读操作也可以正常执行,我们可以获得管道内的数据
当管道内没有数据时,我们执行读操作,进程就会被阻塞
4.3消息队列
与有名管道类似,消息队列也是用于实现不同进程间通信的一种方法,但是它减少了在打开与关闭有名管道之间同步的困难。发送者首先创建一个消息队列,若消息队列有空间,那就可以在队列中放入一些数据,此时发送者就没有什么事了,甚至可以在接收者启动之前退出,即消息队列不需要两个进程同时开启。但需要注意的是当队列中没有数据时,serer还是需等待的。而且除了在消息队列满时不能写入数据,若即将写入的数据超过了预期的设置,那么也是不能成功的,只能够被写入一部分。
由下图可以看到,当我们再client端发送消息时,server端可正常收到,并能向client返回消息,而且client也可以收到该返回的消息并进行输出。
正如刚刚所说的那样,当client发送完消息之后,即使关闭client,也不会影响消息的传输。如下图所示,此时我们仅打开了client端,开始发送消息,此时因为我们没有打开server端,所以不会收到返回信息。
现在,我们关闭client端,打开server端,可以看到,server已经接收到了刚刚发送的消息。
同时,我们在刚刚打开client的那个终端,接收到了返回信息,而内容也正好是我们刚刚发给server的。
5 pintos操作系统
在进程上下文切换的过程中,需要保存的主要是进程控制块里的内容,包括进程状态、名字、栈顶指针、优先级以及magic
在进程切换的过程中,首先调用了thread_init函数,为我们做了最基本的PCB初始化,并将initial_thread存放到了all_list中,main进程获取的tid值是1。
而thread_start函数则创建了idle进程,开启中断,并通过调用thread_create函数,创建并初始化PCB块,并将idel进程加入到read_list中。此时,idle位read_list中的唯一进程,并通过信号量操作让其他正在执行的线程(main线程)等待。
通过使用thread_block函数,将进程从main切换到idle,它通过thread_current函数获取当前正在执行的进程PCB首地址,将当前正在执行的进程设为阻塞状态,最后调用schedule函数。
接下来调用switch_threads函数,实现新旧线程的堆栈交换及保存,返回旧线程的PCB起始地址,再切换完成后,新的进程开始执行。
本实验涉及到的所有程序的源代码,均已上传至本人github,地址为 https://github.com/16281307/OS/tree/master/lab3