操作系统实验三:同步问题
Y.xj
任务一:
实验要求:
通过fork的方式,产生4个进程P1,P2,P3,P4,每个进程打印输出自己的名字,例如P1输出“I am the process P1”。要求P1最先执行,P2、P3互斥执行,P4最后执行。通过多次测试验证实现是否正确。
分析:
我们所需要的应该是如下图所示的进程创建顺序
- 先创建P1进程
- 随后P2,P3共同竞争互斥信号量互斥创建
- 当P2,P3全部创建完成后,P4才可以创建
代码如下:
#include "stdio.h"
#include "sys/types.h"
#include "unistd.h"
#include "stdlib.h"
#include <semaphore.h>
#include <fcntl.h>
#include <sys/stat.h>
sem_t sem23,sem2,sem3;
//sem23为互斥信号量,sem2,sem3用于启动P4
int main()
{
int cnt=0;
sem_init(&sem23,1,0);
sem_init(&sem4,1,0);
pid_t p1, p2, p3, p4;
p1 = fork();
if (p1==0) //P1进程
{
printf("I\'am the process P1\n");
i=sem_post(&sem23); //P1执行完
}
else
{
sem_wait(&sem23); //P2P3竞争互斥信号量
p2 = fork();
if (p2==0) //P2进程
{
printf("I\'am the process P2\n");
sem_post(&sem23);
sem_post(&sem2);
}
else
{
sem_wait(&sem23); //P2P3竞争互斥信号量
p3=fork();
if (p3 == 0)
{
//p3进程
printf("I\'am the process P3\n");
sem_post(&sem23); //释放
sem_post(&sem3); //释放
}
else if(p3>0)
{
//p4进程
sem_wait(&sem2);
sem_wait(&sem3); //只有当p2,p3同时创建完p4才可以被创建
p4 = fork();
if (p4 == 0)
{
printf("I\'am the process P4\n");
}
}
else {printf("erro");}
}
}
}
说明: 由于P1进程没有特别要求,所以不加以信号量限制,当P1执行完毕之后,释放2和3的互斥信号量sem23,随后P2,P3进程竞争互斥信号量开始执行,一个执行完毕之后另一个才可执行。在P2个P3都执行完毕之后,释放自己相应的信号量。当两个进程全部执行完,P4进程前面的两个wait操作才可跳过,执行P4程序,即完成题目所需的要求。
实验结果:
几次实验结果都是p1->p2->p3->p4,也会出现P1->P3->P2->P4的执行顺序,此图中没有体现。
出现的问题:
- 进程创建次数问题,一不小心就会写的创建出好多进程
- 信号量操作使用并不是很熟悉
- 一开始对于P4开始的想法是,设置一个初始值为
-1的信号量sem4,P2和P3每次执行完都去释放sem4,结果在操作时发现,信号量初始值的数据类型居然是unsigned int(即不存在负数),后来考虑用两个信号量来限制P4的执行。
任务二:
实验要求:
火车票余票数ticketCount 初始值为1000,有一个售票线程,一个退票线程,各循环执行多次。添加同步机制,使得结果始终正确。要求多次测试添加同步机制前后的实验效果。(说明:为了更容易产生并发错误,可以在适当的位置增加一些pthread_yield(),放弃CPU,并强制线程频繁切换)
分析:
由于只有1000张票,所以应该满足的需求为:
-
售票完之后余票数非负
-
退票之后余票数不能超过1000
-
无票时不能退票
-
余票为0时不能卖票
所以考虑用信号量来进行同步操作
参考代码:
#include <stdio.h>
#include <pthread.h>
#include <time.h>
#include <semaphore.h>
int ticketCount=1000,temp;//定义
sem_t b,r,multex;
//购票线程
void *buy(void *arg){
while(1){
if(ticketCount==0)printf("无票,无法购票\n");
sem_wait(&b);
sem_wait(&multex);
temp=ticketCount;
pthread_yield();
temp=temp-1;
sem_post(&r);
pthread_yield();
ticketCount=temp;
printf("成功购票,还剩%d张车票\n",ticketCount);
sem_post(&multex);
}
}
//退票线程
void *refund(void *arg) {
while(1){
if(ticketCount==1000)printf("无票,无法退票\n");
sem_wait(&r);
sem_wait(&multex);
temp=ticketCount;
pthread_yield();
temp=temp+1;
sem_post(&b);
pthread_yield();
ticketCount=temp;
printf("成功退票,还剩%d张车票\n",ticketCount);
sem_post(&multex);
}
}
int main(void) {
pthread_t pid, cid;
//初始化信号量
sem_init(&b,0,1000);
sem_init(&r,0,0);
sem_init(&multex,0,1);
//创建两个线程
pthread_create(&pid,NULL,buy,NULL);
pthread_create(&cid,NULL,refund,NULL);
//等待两个线程的汇合
pthread_join(pid,NULL);
pthread_join(cid,NULL);
//销毁信号量
sem_destroy(&b);
sem_destroy(&r);
return 0;
}
实验结果:
无票时无法购票
退完时无法再退票
发现的问题:
- 购票和退票的线程应该是互斥的,如不加互斥信号量,会发生如下图结果
票数会跳,因为两个进程在切换的时候恢复现场时共享的参数会乱掉,并且票数会高于1000或者小于0。而且实验测试在退票与购票不并发时,票数并不会发生错误,可以确定是并发时出现了错误,所以考虑在购票与退票进程加上互斥信号量,防止对于临界区的共同访问。
任务三:
实验要求:
一个生产者一个消费者线程同步。设置一个线程共享的缓冲区, char buf[10]。一个线程不断从键盘输入字符到buf,一个线程不断的把buf的内容输出到显示器。要求输出的和输入的字符和顺序完全一致。(在输出线程中,每次输出睡眠一秒钟,然后以不同的速度输入测试输出是否正确)。要求多次测试添加同步机制前后的实验效果。
分析:
典型的生产者消费者问题,当有资源时,才可以输出,每读入一个字符,释放一个资源,以此来实现同步,并且防止数组越界。
参考代码:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<semaphore.h>
sem_t buffer,rest; //缓冲区的未被输出的字符 //缓冲区剩余可接纳个数
char buf[10];
void *worker1() {
int i=0;
while(1){
sem_wait(&rest);
printf("\033[0m"); //关闭输出颜色
scanf("%c",&buf[i%10]);
i++;
sem_post(&buffer);
}
}
void *worker2() {
int i=0;
while(1){
sem_wait(&buffer);
printf("\033[31m"); //修改输出字符颜色,便于识别
printf("%c",buf[i%10]);
sleep(1);
printf("\033[0m");
i++;
sem_post(&rest);
}
}
int main()
{
sem_init(&buffer,0,0);
sem_init(&rest,0,10);
pthread_t p1, p2;
pthread_create(&p1, NULL, worker1, NULL);
pthread_create(&p2, NULL, worker2, NULL);
pthread_join(p1, NULL);
pthread_join(p2, NULL);
return 0;
}
实验结果:
输出速度快时,会几乎同步输出,很明显没有问题
在输出速度慢时,为了便于识别输入与输出,将输出字体修改为红色,可以发现,不管输入多长多快,输出依然正确。
任务四:
实验要求:
- 通过实验测试,验证共享内存的代码中,receiver能否正确读出sender发送的字符串?如果把其中互斥的代码删除,观察实验结果有何不同?如果在发送和接收进程中打印输出共享内存地址,他们是否相同,为什么?
- 有名管道和无名管道通信系统调用是否已经实现了同步机制?通过实验验证,发送者和接收者如何同步的。比如,在什么情况下,发送者会阻塞,什么情况下,接收者会阻塞?
- 消息通信系统调用是否已经实现了同步机制?通过实验验证,发送者和接收者如何同步的。比如,在什么情况下,发送者会阻塞,什么情况下,接收者会阻塞?
实验代码:
实验1:代码github上以全部上传Sender.c与Receiver.c,此处只特别说明删除互斥代码与增加地址输出
//receive_flaw.c
//代码概述:删除代码中的互斥部分
//代码说明:前面代码与Receiver.c完全相同,只将while循环中的判断sem_id值注释掉,并且注释掉下面的错误判断
while(1)
{
// if(1 == (value = semctl(sem_id, 0, GETVAL)))
// {
printf("\nNow, receive message process running:\n");
printf("\tThe message is : %s\n", shm_ptr);
/* if(-1 == semop(sem_id, &sem_b, 1))
{
perror("semop");
exit(EXIT_FAILURE);
}*/
// }
//if enter "end", then end the process
if(0 == (strcmp(shm_ptr ,"end")))
{
printf("\nExit the receiver process now!\n");
break;
}
}
//receive_addrs.c
//代码概述:输出时同时输出共享内存地址
//代码说明:其余代码与receiver.c完全相同,只增加一句输出
if(1 == (value = semctl(sem_id, 0, GETVAL)))
{
printf("\nNow, receive message process running:\n");
printf("\tThe message is : %s\n", shm_ptr);
printf("\tThe message address is : %x\n", &shm_ptr);
//此句为输出共享地址语句
if(-1 == semop(sem_id, &sem_b, 1))
{
perror("semop");
exit(EXIT_FAILURE);
}
}
实验结果:
-
实验1:
代码可以完成同步发送接收。
而如果删除掉互斥代码部分,接收端会无线重复输出最近一条输入。
同时将他们的共享地址打印出来,发现地址并不相同。
实验2:
有向管道:参考代码已上传github,fifo_snd.c和fifo_rcv.c
由实验得出:
-
当只有写者,无读者时,写者阻塞
-
当只有读者,无写者时,读者阻塞
-
当只有读者阻塞时,写者进入,写者也阻塞
-
当只有写者阻塞时,读者进入,程序正常执行,读者可以读出写入的文件
无向管道
它实现了同步机制
上图为正常先写后读,可以正常执行。
上图为不写入直接读取,读进程阻塞。
实验3:
消息通信系统调用也实现了同步机制
- 先执行Client,输入请求后,再执行Server,Server会将所有输入依次输出,并且返回给Client
- 先执行Server进程,再执行Client进程,每次在Client输入都会立即输出并且返回
任务五:
有点困难,参考了一些网上的分析:
参考链接:http://www.cnblogs.com/laiy/p/pintos_project1_thread.html#timer_sleep
分析一下这个汇编代码: 先4个寄存器压栈保存寄存器状态(保护作用), 这4个寄存器是switch_threads_frame的成员:
/* switch_thread()'s stack frame. */
struct switch_threads_frame
{
uint32_t edi; /* 0: Saved %edi. */
uint32_t esi; /* 4: Saved %esi. */
uint32_t ebp; /* 8: Saved %ebp. */
uint32_t ebx; /* 12: Saved %ebx. */
void (*eip) (void); /* 16: Return address. */
struct thread *cur; /* 20: switch_threads()'s CUR argument. */
struct thread *next; /* 24: switch_threads()'s NEXT argument. */
};
然后全局变量thread_stack_ofs记录线程和棧之间的间隙, 我们都知道线程切换有个保存现场的过程,
来看34,35行, 先把当前的线程指针放到eax中, 并把线程指针保存在相对基地址偏移量为edx的地址中。
38,39: 切换到下一个线程的线程棧指针, 保存在ecx中, 再把这个线程相对基地址偏移量edx地址(上一次保存现场的时候存放的)放到esp当中继续执行。
这里ecx, eax起容器的作用, edx指向当前现场保存的地址偏移量。
简单来说就是保存当前线程状态, 恢复新线程之前保存的线程状态。
然后再把4个寄存器拿出来, 这个是硬件设计要求的, 必须保护switch_threads_frame里面的寄存器才可以destroy掉eax, edx, ecx。
然后注意到现在eax(函数返回值是eax)就是被切换的线程棧指针。
我们由此得到一个结论, schedule先把当前线程丢到就绪队列,然后把线程切换如果下一个线程和当前线程不一样的话。
然后再看shedule最后一行的函数thread_schedule_tail做了什么鬼, 这里参数prev是NULL或者在下一个线程的上下文中的当前线程指针。
/* Completes a thread switch by activating the new thread's page
tables, and, if the previous thread is dying, destroying it.
At this function's invocation, we just switched from thread
PREV, the new thread is already running, and interrupts are
still disabled. This function is normally invoked by
thread_schedule() as its final action before returning, but
the first time a thread is scheduled it is called by
switch_entry() (see switch.S).
It's not safe to call printf() until the thread switch is
complete. In practice that means that printf()s should be
added at the end of the function.
After this function and its caller returns, the thread switch
is complete. */
void
thread_schedule_tail (struct thread *prev)
{
struct thread *cur = running_thread ();
ASSERT (intr_get_level () == INTR_OFF);
/* Mark us as running. */
cur->status = THREAD_RUNNING;
/* Start new time slice. */
thread_ticks = 0;
#ifdef USERPROG
/* Activate the new address space. */
process_activate ();
#endif
/* If the thread we switched from is dying, destroy its struct
thread. This must happen late so that thread_exit() doesn't
pull out the rug under itself. (We don't free
initial_thread because its memory was not obtained via
palloc().) */
if (prev != NULL && prev->status == THREAD_DYING && prev != initial_thread)
{
ASSERT (prev != cur);
palloc_free_page (prev);
}
}
先是获得当前线程cur, 注意此时是已经切换过的线程了(或者还是之前run的线程, 因为ready队列为空)。
然后把线程状态改成THREAD_RUNNING, 然后thread_ticks清零开始新的线程切换时间片。
然后调用process_activate触发新的地址空间。
/* Loads page directory PD into the CPU's page directory base
register. */
void
pagedir_activate (uint32_t *pd)
{
if (pd == NULL)
pd = init_page_dir;
/* Store the physical address of the page directory into CR3
aka PDBR (page directory base register). This activates our
new page tables immediately. See [IA32-v2a] "MOV--Move
to/from Control Registers" and [IA32-v3a] 3.7.5 "Base
Address of the Page Directory". */
asm volatile ("movl %0, %%cr3" : : "r" (vtop (pd)) : "memory");
}
这个汇编指令将当前线程的页目录指针存储到CR3(页目录表物理内存基地址寄存器)中,也就是说这个函数更新了现在的页目录表。
最后来看tss_update:
/* Sets the ring 0 stack pointer in the TSS to point to the end
of the thread stack. */
void
tss_update (void)
{
ASSERT (tss != NULL);
tss->esp0 = (uint8_t *) thread_current () + PGSIZE;
}
首先要弄清楚tss是什么, tss是task state segment, 叫任务状态段, 任务(进程)切换时的任务现场信息。
这里其实是把TSS的一个棧指针指向了当前线程棧的尾部, 也就是更新了任务现场的信息和状态。
好, 到现在process_activate分析完了, 总结一下: 其实就是做了2件事情: 1.更新页目录表 2.更新任务现场信息(TSS)
工程github地址:
https://github.com/Yxj-yxj/OS/tree/master/lab3