实验三 同步与通信
16281254 安全1601 黄春浦
实验代码链接: https://github.com/flutgirl/16281254-OS-labs/tree/master/lab3_16281254.
文章目录
一、实验目的
- 系统调用的进一步理解。
- 进程上下文切换。
- 同步与通信方法。
二、实验题目
-
task1: 通过fork的方式,产生4个进程P1,P2,P3,P4,每个进程打印输出自己的名字,例如P1输出“I am the process P1”。要求P1最先执行,P2、P3互斥执行,P4最后执行。通过多次测试验证实现是否正确。
-
task2: 火车票余票数ticketCount 初始值为1000,有一个售票线程,一个退票线程,各循环执行多次。添加同步机制,使得结果始终正确。要求多次测试添加同步机制前后的实验效果。
-
task3: 一个生产者一个消费者线程同步。设置一个线程共享的缓冲区, char buf[10]。一个线程不断从键盘输入字符到buf,一个线程不断的把buf的内容输出到显示器。要求输出的和输入的字符和顺序完全一致。(在输出线程中,每次输出睡眠一秒钟,然后以不同的速度输入测试输出是否正确)。要求多次测试添加同步机制前后的实验效果。
-
task4: 进程通信问题。阅读并运行下列网址中关于共享内存、管道、消息队列三种机制的代码:
- https://www.cnblogs.com/Jimmy1988/p/7706980.html
- https://www.cnblogs.com/Jimmy1988/p/7699351.html
- https://www.cnblogs.com/Jimmy1988/p/7553069.html
- a)通过实验测试,验证共享内存的代码中,receiver能否正确读出sender发送的字符串?如果把其中互斥的代码删除,观察实验结果有何不同?如果在发送和接收进程中打印输出共享内存地址,他们是否相同,为什么?
- b)有名管道和无名管道通信系统调用是否已经实现了同步机制?通过实验验证,发送者和接收者如何同步的。比如,在什么情况下,发送者会阻塞,什么情况下,接收者会阻塞?
- c)消息通信系统调用是否已经实现了同步机制?通过实验验证,发送者和接收者如何同步的。比如,在什么情况下,发送者会阻塞,什么情况下,接收者会阻塞?
-
task5: 阅读Pintos操作系统,找到并阅读进程上下文切换的代码,说明实现的保存和恢复的上下文内容以及进程切换的工作流程。
三、实验内容
3.1 task1实验结果与分析
-
实验要求4个进程P1,P2,P3,P4,P1最先执行,P2、P3互斥执行,P4最后执行,即需要满足如下前驱图和前驱关系:
前驱关系:P1->P2
,P1->P3
,P2->P4
,P3->P4
前驱图:
-
根据前驱关系和前驱图,设计和编写程序task1.c如下:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<pthread.h> #include<semaphore.h> #include<fcntl.h> int main(){ sem_t *P1_signal,*P2_signal,*P3_signal;// 信号量声明,主函数中的进程是P1 pid_t p2,p3,p4; P1_signal=sem_open("P1_signal",O_CREAT,0666,0);//创建并初始化信号灯 P2_signal=sem_open("P2_signal",O_CREAT,0666,0); P3_signal=sem_open("P3_signal",O_CREAT,0666,0); p2=fork();//创建进程P2 if(p2<0){ perror("Error in creating process p2!"); } if(p2==0){//P1子进程P2 sem_wait(P1_signal);//互斥信号量 printf("I am the process P2!\n"); sem_post(P1_signal); sem_post(P2_signal); } if(p2>0){//父进程P1 p3=fork();//创建进程P3 if(p3<0){ perror("Error in creating process p3!"); } if(p3==0){//P1子进程P3 sem_wait(P1_signal);//互斥信号量 printf("I am the process P3!\n"); sem_post(P1_signal); sem_post(P3_signal); } if(p3>0){//父进程P1 printf("I am the process P1!\n"); sem_post(P1_signal); p4=fork(); if(p4<0){ perror("Error in creating process p4!"); } if(p4==0){//P1子进程P4 sem_wait(P2_signal);//等待进程P2 sem_wait(P3_signal);//等待进程P3 printf("I am the process P4!\n"); sem_post(P2_signal); sem_post(P3_signal); } //sleep(2); } } sem_close(P1_signal);//关闭有名信号灯 sem_close(P3_signal); sem_close(P2_signal); sem_unlink("P1_signal");//从系统中删除信号灯 sem_unlink("P2_signal"); sem_unlink("P3_signal"); return 0; }
-
下面对该程序进行一个简单的介绍,在该进程中,首先声明三个信号量P1_signal,P2_signal,P3_signal,并分别赋值为0,P1_signal用于控制P2和P3互斥访问,P2_signal和P3_signal信号量用于控制P4的执行次序。其中主函数的进程为P1,并由主函数产生另外三个子进程,进程树和函数流程图如下:
进程树:
函数流程图:
最后,当所有进程执行完毕后,关闭信号量,并从系统中删除。 -
通过上面的函数设计,已经满足了实验要求,下面来编译和运行程序:
出现上面的结果是因为P1进程提前结束,在P1进程中加入sleep(2),即可观察到下面的正确结果:
可见结果满足P1先执行,然后P4在P2和P3之后执行,但是P2和P3为互斥关系,故谁先执行原则上并不能确定,但是多次实验后仍然为P3先执行,原因可能是P3进程进入运行状态的时间早于P2进程,故可在P3中加入sleep函数进行等待,此时观察到结果:
此时运行结果为P1->P2->P3->P4
,故我们可以得到结论,P2、P3的工作方式是竞争上岗,符合实验要求,但是由于fork的天然问题,使得P2竞争过P3的概率极小,但很多次运行后仍可能得到另一种执行顺序,也可以通过人为调整,较快得到令一种可能的运行结果。
3.2 task2实验结果与分析
-
编写程序,ticketCount 初始值为1000,售票线程为sold(),退票线程为returnT(),各循环执行150。程序源码如下:
#include<sys/types.h> #include<unistd.h> #include<stdio.h> #include<stdlib.h> #include<pthread.h> #include <fcntl.h> #include <sys/stat.h> #include <semaphore.h> #include <sched.h> #define initial 1000 //初始票数 sem_t* mySem = NULL; int ticketCount = initial; void*sold(){//售票线程 int i = 150; while(i--){ // sem_wait(mySem); printf("Current ticketCount number is %d.\n",ticketCount); int temp = ticketCount; // sched_yield();//放弃CPU,强制切换到另外一个进程 temp = temp - 1; // sched_yield(); ticketCount = temp; // sem_post(mySem); } } void*returnT(){//退票线程 int i = 150; while(i--){ // sem_wait(mySem); printf("Current ticketCount number is %d.\n",ticketCount); int temp = ticketCount; // sched_yield(); temp = temp + 1; // sched_yield(); ticketCount = temp; // sem_post(mySem); } } int main(){ pthread_t p1,p2;//线程标识符 mySem = sem_open("Ticket", O_CREAT, 0666, 1);//初始化信号量 pthread_create(&p1,NULL,sold,NULL);//创建线程 pthread_create(&p2,NULL,returnT,NULL); pthread_join(p1,NULL);//等待线程P1的结束 pthread_join(p2,NULL); sem_close(mySem);//关闭信号量 sem_unlink("Ticket");//从系统中删除信号量 printf("The finall value of ticketCount is %d.\n",ticketCount); return 0; }
-
对程序的解释:在sold()线程中,
int i
的值表示卖票的个数,在returnT()线程中,int i
表示退票的个数,为了观察并发过程中的同步问题,代码首先对信号量部分做注释,现在考虑如下运行结果:- 售票、退票均运行150次的编译并运行结果:
- 售票、退票均运行150000次的编译并运行结果:
- 售票、退票均运行150次的编译并运行结果:
- 由实验数据可见,运行结果均正确,这是因为现代CPU的速度较快,一个时间片未用完的情况下已经能够处理大量的数据,当循环次数较小时,等效于顺序执行;当循环次数过大时,一个时间片不能满足进程计算的需求,出现了进程间的切换,但是这种切换的频率太低,读脏数据及覆盖性写入的并发问题并未体现,为了更容易产生并发错误,可以在适当的位置增加一些pthread_yield(),放弃CPU,并强制线程频繁切换。(即将代码中两个线程中注释的
sched_yield();
打开,强行使当前进程让出CPU执行权),此时运行结果如下:- 售票、退票均运行150次的编译并运行结果:
- 售票50、退票150的编译并运行结果:
- 售票150、退票50的编译并运行结果:
- 售票、退票均运行150次的编译并运行结果:
- 由实验数据可以发现,此时运行结果出现了问题,由于线程的频繁切换出现了覆盖性写入的问题导致,运行结果总与最后一次执行线程的结果相一致,当售票数量大于退票数量或售票与退票相等的时候,最终票数等于总票数减去售票数;当售票数量小于退票数量的时候,最终票数等于总票数加上退票数。
- 针对售票150、退票50的编译并运行结果对此现象做出解释:由于现代的处理器运算速度足够快,在退票进程放弃CPU控制权的那个时间片已经完成了ticketCount=temp和temp=ticketCount这两步操作,所以相当于售票进程读取的ticketCount一直是自己本身的值,退票进程处理的数据对售票进程并没有影响。为了验证该猜想,修改代码和运行结果如下:(在ticketCount=temp和temp=ticketCount操作之间强制线程转换)
- 结果表明此时最终票数不再是850,而是930,猜想正确。
- 引入信号量:将代码中的信号量控制语句打开,(保持
sched_yield();
的打开)出现结果如下:- 售票、退票均运行150次的编译并运行结果:
- 售票50、退票150的编译并运行结果:
- 售票150、退票50的编译并运行结果:
- 售票、退票均运行150次的编译并运行结果:
- 由此可见,即便受sched_yield()的影响,也不论是售票数量比退票数量多还是少,运行结果均正确,故得到结论:当给售票和退票线程使用信号量加上同步机制后,保证了每个线程操作的原子性,每个线程操作的过程中其他的线程不能对共享的ticketCount变量进程修改,因此,最终的结果是正确的。
3.3 task3实验结果与分析
-
编写不带信号量机制的输入输出线程,输出线程每输出一个字符等待一秒钟,方便实验结果的查看。代码如下:
#include<sys/types.h> #include<unistd.h> #include<stdlib.h> #include<pthread.h> #include <fcntl.h> #include<stdio.h> #include <sys/stat.h> #include <sched.h> #include <semaphore.h> sem_t* empty_mySem = NULL; sem_t* full_mySem = NULL; char buf[10];//缓冲区大小为10 short i = 0; short j = 0; void*input(){//输入线程 while(1){ //sem_wait(empty_mySem);//当缓冲区有空间时输入 printf("输入:"); scanf("%c",&buf[i++ % 10]); //sem_post(full_mySem);//增加缓冲数据资源 } } void*output(){//输出线程 while(1){ // sem_wait(full_mySem);//当缓冲区有数据时输出 printf("输出:%c\n",buf[j++ % 10]); sleep(1); //sem_post(empty_mySem);//释放缓冲区空间 } } int main(){ pthread_t p1,p2;//线程标识符 empty_mySem = sem_open("inputs", O_CREAT, 0666, 10);//信号量初始化 full_mySem = sem_open("outputs", O_CREAT, 0666, 0); pthread_create(&p1,NULL,input,NULL);//创建线程 pthread_create(&p2,NULL,output,NULL); pthread_join(p1,NULL);//等待线程p1的结束 pthread_join(p2,NULL); sem_close(empty_mySem);//关闭信号量 sem_close(full_mySem); sem_unlink("inputs");//从系统中删除信号量 sem_unlink("outputs"); return 0; }
- 运行结果:运行错误结果分为几类,如下:
-
当输入速度小于输出速度,即输出的位置在输入位置之前,那么输出线程将一直执行完本轮的10次,才能输出输入的字符,如下:
-
当输入暂停时,输出线程将循环输出缓冲区的字符,如下:
-
当输入速度大于输出速度时,部分输入字符将被覆盖而永远不能被输出,结果如下:
-
- 下面编写带有信号量机制的程序,其中empty_mySem初始值为10,表示缓冲区空闲的资源数量,用于保证输入线程在写入数据到缓存的时候缓存中还有空余的位置,保证写入线程后写入的数据不会把前面写入但是为输出的数据给覆盖掉;full_mySem初始值为0,表示缓冲区存有的未被输出的资源数量,用于保证输出线程有数据输出,避免在写入线程还没有写入数据的情况下输出线程输出随机数据。输入线程在写入一个数据前要等待empty_mySem信号量,进入后便消耗一个empty_mySem信号量;完成写入数据操作之后post一个full_mySem信号量,通知输出线程输出数据,输出线程反之。(即将上面的程序中两个线程中的注释的信号量操作打开),运行结果如下:
由上述结果可见,输入和输出进程同步,满足题目要求,故加入信号量之后,实验结果正确,输出的和输入的字符和顺序完全一致。
注意:在实验过程中,由于多线程编程中, 进程异常退出会导致信号量释放失败问题, 进而导致无限等待形成死锁, 这时重新打开一个终端也不能解决问题,将系统重启便可解决。
3.4 task4实验结果与分析
3.4.1 共享内存
-
共享内存,主要是实现进程间大量数据的传输。所谓共享内存,即在内存中开辟一段特殊的内存空间,多个进程可互斥访问,该内存空间具有自身特有的数据结构。下面基于该概念,设计进程通信模型如下:
Sender.c和Receive.c的源码如下:Sender.c
/* * Filename: Sender.c * Description: */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/sem.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/types.h> #include <string.h> int main(int argc, char *argv[]) { key_t key; int shm_id; int sem_id; int value = 0; //1.Product the key key = ftok(".", 0xFF); //2. Creat semaphore for visit the shared memory sem_id = semget(key, 1, IPC_CREAT|0644); if(-1 == sem_id) { perror("semget"); exit(EXIT_FAILURE); } //3. init the semaphore, sem=0 if(-1 == (semctl(sem_id, 0, SETVAL, value))) { perror("semctl"); exit(EXIT_FAILURE); } //4. Creat the shared memory(1K bytes) shm_id = shmget(key, 1024, IPC_CREAT|0644); if(-1 == shm_id) { perror("shmget"); exit(EXIT_FAILURE); } //5. attach the shm_id to this process char *shm_ptr; shm_ptr = shmat(shm_id, NULL, 0); if(NULL == shm_ptr) { perror("shmat"); exit(EXIT_FAILURE); } //6. Operation procedure struct sembuf sem_b; sem_b.sem_num = 0; //first sem(index=0) sem_b.sem_flg = SEM_UNDO; sem_b.sem_op = 1; //Increase 1,make sem=1 while(1) { if(0 == (value = semctl(sem_id, 0, GETVAL))) { printf("\nNow, snd message process running:\n"); printf("\tInput the snd message: "); scanf("%s", 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 sender process now!\n"); break; } } shmdt(shm_ptr); return 0; }
-
receiver.c
/* * Filename: Receiver.c * Description: */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/sem.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/types.h> #include <string.h> int main(int argc, char *argv[]) { key_t key; int shm_id; int sem_id; int value = 0; //1.Product the key key = ftok(".", 0xFF); //2. Creat semaphore for visit the shared memory sem_id = semget(key, 1, IPC_CREAT|0644); if(-1 == sem_id) { perror("semget"); exit(EXIT_FAILURE); } //3. init the semaphore, sem=0 if(-1 == (semctl(sem_id, 0, SETVAL, value))) { perror("semctl"); exit(EXIT_FAILURE); } //4. Creat the shared memory(1K bytes) shm_id = shmget(key, 1024, IPC_CREAT|0644); if(-1 == shm_id) { perror("shmget"); exit(EXIT_FAILURE); } //5. attach the shm_id to this process char *shm_ptr; shm_ptr = shmat(shm_id, NULL, 0); if(NULL == shm_ptr) { perror("shmat"); exit(EXIT_FAILURE); } //6. Operation procedure struct sembuf sem_b; sem_b.sem_num = 0; //first sem(index=0) sem_b.sem_flg = SEM_UNDO; sem_b.sem_op = -1; //Increase 1,make sem=1 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; } } shmdt(shm_ptr); //7. delete the shared memory if(-1 == shmctl(shm_id, IPC_RMID, NULL)) { perror("shmctl"); exit(EXIT_FAILURE); } //8. delete the semaphore if(-1 == semctl(sem_id, 0, IPC_RMID)) { perror("semctl"); exit(EXIT_FAILURE); } return 0; }
-
两个进程通过共享内存传输数据,因共享内存不可同时读写,因此采用二元信号量进行进程互斥,具体操作如下:
- init: 设置信号量为0,此时只允许写入,不允许读取(因为共享内存没有数据);
- Sender: 在sem=0时,写入数据到共享内存(阻塞读);写入完成后,sem=1,此时可以读取,不可以写入;
- Receiver: 在sem=1时,读取数据;读取完成后,sem=0,此时只允许写入。
-
编译运行结果:
因此,在验证共享内存的代码中,receiver能正确读出sender发送的字符串。
-
删掉互斥代码,即把
sender.c
和receiver
进程的主循环中将用于控制互斥访问共享内存的相关代码删除,详细参见github代码链接中的sender2.c
和receiver2.c
,此时编译运行结果如下:
因此,当删除互斥代码时,两个进程便没有限制的访问共享内存,Sender进程受用户输入的速度的影响,会一直等待用户输入,但是Receiver进程会一直输出共享内存中的消息,即便此时sender没有发送内容,receiver也会输出空的内容。
-
在发送和接收进程中打印输出共享内存地址,只需要在原来的代码中加入输出地址语句即可,具体代码参见github中的
sender3.c
和receiver3.c
,此时编译运行结果如下:
实验结果表明在两个进程中共享内存的地址不一样,这是因为现代操作系统中都存在ASLR(地址空间随机化),其为针对缓冲区溢出的安全保护机制,这使每次加载到内存的程序起始地址会随机变化,因此可能可能导致共享内存的地址不一致。另外,进程在挂载内存的时候使用的
shmat()
函数中的第二个参数使用的是NULL,NULL参数的含义是进程让系统分配给共享内存合适的地址,这也是共享内存的地址不一致的一个原因。
- 尝试关闭ASLR,命令:
sudo sysctl -w kernel.randomize_va_space=0
,运行结果如下:
因此上述猜想1正确,同样的道理,当我们
shmat()
函数中的第二个参数赋予指定地址时,两个共享内存地址也会是一致的。
3.4.2 管道
-
无名管道
无名管道是一类特殊的文件,在内核中对应着一段特殊的内存空间,内核在这段内存空间中以循环队列的方式存储数据;无名管道的内核资源在通信双方退出后自动消失,无需人为回收;无名管道主要用于连通亲缘进程(父子进程),用于双方的快捷通信,通信为单向通信。
-
测试代码
pipe.c
如下:/* * Filename: pipe.c */ #include <stdio.h> #include <unistd.h> //for pipe() #include <string.h> //for memset() #include <stdlib.h> //for exit() int main(){ int fd[2]; char buf[20]; if(-1 == pipe(fd)) { perror("pipe"); exit(EXIT_FAILURE); } write(fd[1], "hello,world", 12); memset(buf, '\0', sizeof(buf)); read(fd[0], buf, 12); printf("The message is: %s\n", buf); return 0; }
代码解释: 本代码主要通过pipe函数创建管道,传递一个整形数组fd,fd的两个整形数表示的是两个文件描述符,第一个用于读取数据,第二个用于写数据,两个描述符就像管道的两端,一端负责写数据,另外一端负责读数据。
-
测试代码j结果如下:
-
为了更好的验证无名管道的同步机制,修改
pipe.c
代码得到·pipe2.c·:#include <stdio.h> #include <unistd.h> //for pipe() #include <string.h> //for memset() #include <stdlib.h> //for exit() int main() { int fd[2]; char buf[100]={0}; pid_t p1; //创建管道 if(-1 == pipe(fd)){ perror("pipe"); exit(EXIT_FAILURE); } p1=fork();//创建子进程 if(p1< 0){//创建失败 perror("fork"); exit("EXIT_FAILURE"); } if(p1==0){//子进程 close(fd[1]); while(1){ if(read(fd[0],buf,sizeof(buf))>0) printf("the received message in the child:%s\n",buf); else printf("none in the child\n"); sleep(2); if(strcmp(buf,"end")==0) break; memset(buf,0,sizeof(buf)); } } if(p1>0){//父进程 close(fd[0]); while(1){ printf("this is the father,please input:"); scanf("%s",buf); write(fd[1],buf,strlen(buf)); if(strcmp(buf,"end")==0) break; } } return 0; }
解释: 在父进程中创建了两个文件描述符,fork一个子进程的时候会复制这两个管道文件描述符,因此父进程和子进程都将用不到的文件描述符关闭(
close(fd[i]);
);该程序的主要功能是父进程向管道中写入输入的消息,子进程会输出管道中的消息,若管道中没有消息,则阻塞等待。 -
运行结果:
-
无名管道通信系统调用的时候很好的实现了同步机制,这是因为发送者在向管道内存中写入数据之前,首先检查内存是否被读进程锁定和内存中是否还有剩余空间,如果这两个要求都满足的话write函数会对内存上锁,然后进行写入数据,写完之后解锁;否则就会等待(阻塞)。而写进程在读取管道中的数据之前,也会检查内存是否被读进程锁定和管道内存中是否有数据,如果满足这两个条件,read函数会对内存上锁,读取数据后在解锁;否则会等到(阻塞);因此在测试程序中,输出和输入保持同步,且当没有输入时,输出便会阻塞等待。
-
有名管道
又称FIFO(Fisrt In First out), 是磁盘上的一个特殊文件,没有在磁盘存储真正的信息,其所存储的信息在内存中通信双方进程结束后,自动丢弃FIFO中的信息,但其在磁盘的文件路径本身依然存在
-
测试代码
fifo_snd.c
和fifo_rcv.c
如下://fifo_send.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/stat.h> #include <sys/ipc.h> #include <fcntl.h> #define FIFO "./my_fifo" int main() { char buf[] = "hello,world"; //1. check the fifo file existed or not int ret; ret = access(FIFO, F_OK); if(ret != 0) //file /tmp/my_fifo existed { if(-1 == mkfifo(FIFO, 0766)) { perror("mkfifo"); exit(EXIT_FAILURE); } } //3.Open the fifo file int fifo_fd; fifo_fd = open(FIFO, O_WRONLY); if(-1 == fifo_fd) { perror("open"); exit(EXIT_FAILURE); } //4. write the fifo file int num = 0; num = write(fifo_fd, buf, sizeof(buf)); if(num < sizeof(buf)) { perror("write"); exit(EXIT_FAILURE); } printf("write the message ok!\n"); close(fifo_fd); return 0; }
//`fifo_rcv.c` #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <sys/stat.h> #include <sys/ipc.h> #include <fcntl.h> #define FIFO "./my_fifo" int main() { char buf[20] ; memset(buf, '\0', sizeof(buf)); //`. check the fifo file existed or not int ret; ret = access(FIFO, F_OK); if(ret != 0) //file /tmp/my_fifo existed { if(-1==mkfifo(FIFO,0766)) { perror("mkfifo"); exit("EXIT_FAILURE"); } } // 2.Open the fifo file int fifo_fd; fifo_fd = open(FIFO, O_RDONLY); if(-1 == fifo_fd) { perror("open"); exit(EXIT_FAILURE); } //4. read the fifo file int num = 0; num = read(fifo_fd, buf, sizeof(buf)); printf("Read %d words: %s\n", num, buf); close(fifo_fd); return 0; }
程序的步骤:
1. 创建一个新的fifo文件
2. fifo_snd.c文件负责向fifo中写入数据
2. fifo_rcv.c负责从fifo中读取数据并打印 -
实验运行结果:
-
只启动fifo_send时,fifo_send阻塞等待
-
在结果1的基础上,再启动fifo_rcv,则两个进程均能执行完毕
-
只启动fifo_rcv时,fifo_rcv阻塞等待
-
在结果3的基础上,再启动fifo_send,则两个进程均能执行完毕
两个进程Sender与Receiver由于open函数中分别选用了只写和只读方式,所以会交叉阻塞。即:不论先执行那个进程,先执行的进程都会阻塞等待,待另一个进程执行后两个进程才正常执行。
- 实验表明有名管道也实现了同步机制,在没有接收者接收的情况下,发送进程会被阻塞;在没有发送者发送的情况下,接收进程会被阻塞。这样成对的执行模式,可以避免重复写和重复读问题。
3.4.3 消息队列
消息队列可用于同主机任意多进程的通信,但其可存放的数据有限,应用于少量的数据传递
-
编写实验源代码
client.c
和server.c
如下:
sever.c
:#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/msg.h> #include <sys/ipc.h> #include <signal.h> #define BUF_SIZE 128 //Rebuild the strcut (must be) struct msgbuf { long mtype; char mtext[BUF_SIZE]; }; int main(int argc, char *argv[]) { //1. creat a mseg queue key_t key; int msgId; key = ftok(".", 0xFF); msgId = msgget(key, IPC_CREAT|0644); if(-1 == msgId) { perror("msgget"); exit(EXIT_FAILURE); } printf("Process (%s) is started, pid=%d\n", argv[0], getpid()); while(1) { alarm(0); alarm(600); //if doesn't receive messge in 600s, timeout & exit struct msgbuf rcvBuf; memset(&rcvBuf, '\0', sizeof(struct msgbuf)); msgrcv(msgId, &rcvBuf, BUF_SIZE, 1, 0); printf("Receive msg: %s\n", rcvBuf.mtext); struct msgbuf sndBuf; memset(&sndBuf, '\0', sizeof(sndBuf)); strncpy((sndBuf.mtext), (rcvBuf.mtext), strlen(rcvBuf.mtext)+1); sndBuf.mtype = 2; if(-1 == msgsnd(msgId, &sndBuf, strlen(rcvBuf.mtext)+1, 0)) { perror("msgsnd"); exit(EXIT_FAILURE); } //if scanf "end~", exit if(!strcmp("end~", rcvBuf.mtext)) break; } printf("THe process(%s),pid=%d exit~\n", argv[0], getpid()); return 0; }
client.c
:#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/msg.h> #include <sys/ipc.h> #include <signal.h> #define BUF_SIZE 128 //Rebuild the strcut (must be) struct msgbuf { long mtype; char mtext[BUF_SIZE]; }; int main(int argc, char *argv[]) { //1. creat a mseg queue key_t key; int msgId; printf("THe process(%s),pid=%d started~\n", argv[0], getpid()); key = ftok(".", 0xFF); msgId = msgget(key, IPC_CREAT|0644); if(-1 == msgId) { perror("msgget"); exit(EXIT_FAILURE); } //2. creat a sub process, wait the server message pid_t pid; if(-1 == (pid = fork())) { perror("vfork"); exit(EXIT_FAILURE); } //In child process if(0 == pid) { while(1) { alarm(0); alarm(100); //if doesn't receive messge in 100s, timeout & exit struct msgbuf rcvBuf; memset(&rcvBuf, '\0', sizeof(struct msgbuf)); msgrcv(msgId, &rcvBuf, BUF_SIZE, 2, 0); printf("Server said: %s\n", rcvBuf.mtext); } exit(EXIT_SUCCESS); } else //parent process { while(1) { usleep(100); struct msgbuf sndBuf; memset(&sndBuf, '\0', sizeof(sndBuf)); char buf[BUF_SIZE] ; memset(buf, '\0', sizeof(buf)); printf("\nInput snd mesg: "); scanf("%s", buf); strncpy(sndBuf.mtext, buf, strlen(buf)+1); sndBuf.mtype = 1; if(-1 == msgsnd(msgId, &sndBuf, strlen(buf)+1, 0)) { perror("msgsnd"); exit(EXIT_FAILURE); } //if scanf "end~", exit if(!strcmp("end~", buf)) break; } printf("THe process(%s),pid=%d exit~\n", argv[0], getpid()); } return 0; }
本程序主要是实现两个进程通过消息队列发送信息:msgflg 为0表示阻塞方式,设置IPC_NOWAIT 表示非阻塞方式
server:1等待接收客户端发送的数据,若时间超出600s,则自动exit;当收到信息后,打印接收到的数据;并原样的发送给客户端,由客户端显示.
client:启动两个进程(父子进程),父进程用于发送数据,子进程接收由server发送的数据;发送数据:由使用者手动输入信息,回车后发送;当写入“end~”后,退出本进程;接收数据:接收由Server端发送的数据信息,并打印. -
代码运行结果:
- 只启动sever端,则sever端接收消息队列阻塞,等待服务器端的消息发送;
- 在1的情况下,启动client程序,键入消息,server端正常工作,client接收到server回复消息:
- 只启动client端,client发送进程可以正常发送直至消息队列满阻塞:
- 在3的前提下,启动sever,server端接收到client端之前写入的消息,并返还给client:
- 因此,消息通信系统调用是否已经实现了同步机制,消息队列通过msgrcv和msgsnd两个函数的flag参数控制是否阻塞,将其设置为IPC_NOWAIT表示不阻塞;如果客户端和服务器端都设置阻塞话,就可以达到同步的目的。当只启动sever端,则sever端接收消息队列阻塞,等待服务器端的消息发送;当只启动client端,client发送进程可以正常发送直至消息队列满阻塞,由于消息队列的长度很大,我们很难在低并发情况下造成发送队列满的状态,实际上,当消息队列满时,发送将发生阻塞。
3.5 task5实验结果与分析
阅读Pintos操作系统,找到并阅读进程上下文切换的代码,说明实现的保存和恢复的上下文内容以及进程切换的工作流程。
在github上可以搜到pintos的源码,链接:https://github.com/codyjack/OS-pintos/tree/master/pintos/src
- 阅读的pintos文件列表
Pintos/src/thread/init.c
pintos/src/userprog/process.c
pintos/src/threads/thread.h
pintos/src/threads/thread.c
pintos/src/threads/synch.c
Pintos/src/threads/switch.h
Pintos/src/threads/switch.S
pintos在thread.h中定义了一个结构体struct thread,这个结构体就存方了有关进程的基本信息。
struct thread
{
/* Owned by thread.c. */
tid_t tid; /* Thread identifier. */
enum thread_status status; /* Thread state. */
char name[16]; /* Name (for debugging purposes). */
uint8_t *stack; /* Saved stack pointer. */
int priority; /* Priority. */
struct list_elem allelem; /* List element for all threads list. */
/* Shared between thread.c and synch.c. */
struct list_elem elem; /* List element. */
#ifdef USERPROG
/* Owned by userprog/process.c. */
uint32_t *pagedir; /* Page directory. */
tid_t parent_id; /* parent thread id */
/* signal to indicate the child's executable-loading status:
* - 0: has not been loaded
* - -1: load failed
* - 1: load success*/
int child_load_status;
/* monitor used to wait the child, owned by wait-syscall and waiting
for child to load executable */
struct lock lock_child;
struct condition cond_child;
/* list of children, which should be a list of struct child_status */
struct list children;
/* file struct represents the execuatable of the current thread */
struct file *exec_file;
/* supplemental page table, which stores as hash table */
struct hash suppl_page_table;
/* Memory Maped Files table */
mapid_t mapid_allocator;
struct hash mmfiles;
#endif
/* Owned by thread.c. */
unsigned magic; /* Detects stack overflow. */
int64_t sleep_ticks; /* For alarm-clock */
/* Keep track of a thread's priority before a donation */
int priority_original;
/* For multiple donation */
/* the list of locks that a thread has.
* i.e.: Main (M) thread with priority (P) 31, thread A has P 32,
* B has P 33. At the beginning, M is the lock owner. A does lock_acquire,
* then M->locks includes A, B does lock_acquire, M->locks includes both
* A and B. After B does lock_release, M->locks only have A left.
*/
/* If a thread's priority is donated */
bool is_donated;
struct list locks; /* All locks a thread holds */
struct lock *lock_blocked_by; /* Thread blocked by lock */
/* For advanced schedule */
int nice; /* Thread nice value */
int recent_cpu; /* Thread recent CPU */
};
-
该结构体说的事情很简单,无非是这个线程的几个基本信息。值得注意的是enum thread_status这个枚举类型的变量,他的意思就是这个线程现在所处的状态。
enum thread_status { THREAD_RUNNING, /* Running thread. */ THREAD_READY, /* Not running but ready to run. */ THREAD_BLOCKED, /* Waiting for an event to trigger. */ THREAD_DYING /* About to be destroyed. */ };
-
接下来让我们来关注thread.c代码,该代码中主要有以下函数:
1.thread_current()获取当前当前的线程的指针。
2.thread_foreach(thread_action_func *func, void *aux) 遍历当前ready queue中的所有线程,并且对于每一个线程执行一次func操作。注意到这里的func是一个任意给定函数的指针,参数aux则是你想要传给这个函数的参数。实际上pintos没有多么高深,所有ready的线程被保存在一个链表中。这个函数做得不过是遍历了一遍链表而已。注意这个函数只能在中断关闭的时候调用。
3.thread_block()和thread_unblock(thread *t)。 这是一对儿函数,区别在于第一个函数的作用是把当前占用cpu的线程阻塞掉(就是放到waiting里面),而第二个函数作用是将已经被阻塞掉的进程t唤醒到ready队列中。
4.timer_interrupt (struct intr_frame *args UNUSED)这个函数在timer.c中,pintos在每次时间中断时(即每一个时间单位(ticks))调用一次这个函数。
5.intr_disable () 这个函数在interrupt.c中,作用是返回关中断,然后返回中断关闭前的状态。(状态为INTR_OFF,INTR_ON 两种)
t -
hread.c代码很多我们先看init()函数,它为一个新建的进程指定了状态,分配进程号。调用init_thread()分配地址。
void thread_init (void) { ASSERT (intr_get_level () == INTR_OFF); lock_init (&tid_lock); list_init (&ready_list); list_init (&all_list); /* Set up a thread structure for the running thread. */ initial_thread = running_thread (); init_thread (initial_thread, "main", PRI_DEFAULT); initial_thread->status = THREAD_RUNNING; initial_thread->tid = allocate_tid (); } /* Does basic initialization of T as a blocked thread named NAME. */ static void init_thread (struct thread *t, const char *name, int priority) { enum intr_level old_level; ASSERT (t != NULL); ASSERT (PRI_MIN <= priority && priority <= PRI_MAX); ASSERT (name != NULL); memset (t, 0, sizeof *t); t->status = THREAD_BLOCKED; strlcpy (t->name, name, sizeof t->name); t->stack = (uint8_t *) t + PGSIZE; t->priority = priority; t->magic = THREAD_MAGIC; old_level = intr_disable (); list_push_back (&all_list, &t->allelem); intr_set_level (old_level); }
-
下面我们关注到跟进程切换的函数,首先必须要提的时thread_block (void)函数:
/* Puts the current thread to sleep. It will not be scheduled again until awoken by thread_unblock(). This function must be called with interrupts turned off. It is usually a better idea to use one of the synchronization primitives in synch.h. */ void thread_block (void) { ASSERT (!intr_context ()); ASSERT (intr_get_level () == INTR_OFF); thread_current ()->status = THREAD_BLOCKED; schedule (); }
-
该代码的核心在于调用了schedule ();而且通过搜索,很多pintos都调用了这个函数,包括我们常用的thread_yield(),thread_exit().,我们发现它的声明就是在thread.c中,首先其定义了三个thread结构体的指针,均为局部变量,cur指针指向running_thread ()函数的返回值,指针next也是一个函数的返回值。pre之间为null。
/* Schedules a new process. At entry, interrupts must be off and the running process's state must have been changed from running to some other state. This function finds another thread to run and switches to it. It's not safe to call printf() until thread_schedule_tail() has completed. */ static void schedule (void) { struct thread *cur = running_thread (); struct thread *next = next_thread_to_run (); struct thread *prev = NULL; ASSERT (intr_get_level () == INTR_OFF); ASSERT (cur->status != THREAD_RUNNING); ASSERT (is_thread (next)); if (cur != next) prev = switch_threads (cur, next); thread_schedule_tail (prev); }
-
递归到running_thread()来看它的返回值, 此函数嵌入了汇编代码,将CPU堆栈指针(总是在最顶端)复制到“esp”中,然后四舍五入到页面的开头。因为“struct thread”总是在页面的开头,而堆栈指针位于中间的某个位置,所以它定位当前线程。因此cur指针符合我们的猜想,就是当前运行线程的指针。
/* Returns the running thread. */ struct thread * running_thread (void) { uint32_t *esp; /* Copy the CPU's stack pointer into `esp', and then round that down to the start of a page. Because `struct thread' is always at the beginning of a page and the stack pointer is somewhere in the middle, this locates the curent thread. */ asm ("mov %%esp, %0" : "=g" (esp)); return pg_round_down (esp); }
-
上面next指针对应的函数next_thread_to_run(),是一个返回list_entry中pop的值的函数。如果list为空,返回idle_thread.
/* Chooses and returns the next thread to be scheduled. Should return a thread from the run queue, unless the run queue is empty. (If the running thread can continue running, then it will be in the run queue.) If the run queue is empty, return idle_thread. */ static struct thread * next_thread_to_run (void) { if (list_empty (&ready_list)) return idle_thread; else return list_entry (list_pop_front (&ready_list), struct thread, elem); }
-
pre指针为null,所以我们之间进入到schedule ();的后半部分:断言assert是用来判断的。下面调用switch_threads (cur, next)将当前进程和下一个进程进行切换。查看此函数,竟然是用汇编编写的。存放在siwth.S中,结合官方解释,这是将当前堆栈的指针保存到cur线程的堆栈,接着从next线程的堆栈中恢复当前堆栈的指针,也就是寄存器esp的操作。由此我们可以确定进程的保存与恢复就是利用CPU栈顶指针的变化进行的,进程的状态则是保存在自身的堆栈当中。
#include "threads/switch.h" #### struct thread *switch_threads (struct thread *cur, struct thread *next); #### #### Switches from CUR, which must be the running thread, to NEXT, #### which must also be running switch_threads(), returning CUR in #### NEXT's context. #### #### This function works by assuming that the thread we're switching #### into is also running switch_threads(). Thus, all it has to do is #### preserve a few registers on the stack, then switch stacks and #### restore the registers. As part of switching stacks we record the #### current stack pointer in CUR's thread structure. .globl switch_threads .func switch_threads switch_threads: # Save caller's register state. # # Note that the SVR4 ABI allows us to destroy %eax, %ecx, %edx, # but requires us to preserve %ebx, %ebp, %esi, %edi. See # [SysV-ABI-386] pages 3-11 and 3-12 for details. # # This stack frame must match the one set up by thread_create() # in size. pushl %ebx pushl %ebp pushl %esi pushl %edi # Get offsetof (struct thread, stack). .globl thread_stack_ofs mov thread_stack_ofs, %edx # Save current stack pointer to old thread's stack, if any. movl SWITCH_CUR(%esp), %eax movl %esp, (%eax,%edx,1) # Restore stack pointer from new thread's stack. movl SWITCH_NEXT(%esp), %ecx movl (%ecx,%edx,1), %esp # Restore caller's register state. popl %edi popl %esi popl %ebp popl %ebx ret .endfunc
-
在这个部分中实现了新旧线程的堆栈交换及保存,并返回了旧线程的PCB起始地址。在切换完成后,新的进程以及开始执行。回到schedule函数,最后调用的函数是thread_schedule_tail。这个函数里我们将当前正在运行的进程PCB进行更新,并初始化进程执行计数器。通过process_activate激活用户程序执行。process_activate留在我的操作系统拔草二中展开,其内容已经不再涉及到进程的切换。
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); } }
-
所谓中断其实分两种,一种是IO设备向CPU发出的中断的信息,另一种是CPU决定切换到另一个进程时(轮换时间片)发出的指令。我们现在处理第二种。pintos的中断在interrupt.h和interrupt.c之中。涉及到一个重要的枚举类型intr_lverl如下:
enum intr_level { INTR_OFF, /* Interrupts disabled. */ INTR_ON /* Interrupts enabled. */ };
-
其实这个intr_level表达的意思更简单,就是有两个单词,intr_off表示关中断,on表示开中断。大家都知道,执行原子级别操作的时候,中断必须是关着的。
另外,pintos是以ticks作为基本时间单位的,每秒有TIMER_FREQ个ticks:
/* Number of timer interrupts per second. */
#define TIMER_FREQ 100 //系统默认这个宏为100
还有一点,pintos默认每一个ticks调用一次时间中断。换句话说,每一个线程最多可以占据CPU一个ticks的时长,之后就必须放手。