同步与通信
实验目的
- 系统调用的进一步理解。
- 进程上下文切换。
- 同步与通信方法。
实验题目
题目一
通过fork的方式,产生4个进程P1,P2,P3,P4,每个进程打印输出自己的名字,例如P1输出“I am the process P1”。要求P1最先执行,P2、P3互斥执行,P4最后执行。通过多次测试验证实现是否正确。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pthread_mutex_t mutex;
pthread_mutex_init(&mutex,NULL);
pid_t pid1 = fork();
if(pid1 == 0)
{
printf("I am the process P1. PID : %d\n",getpid());
return 0;
}
waitpid(pid1, NULL, 0);
pid_t pid2 = fork();
if(pid2 == 0)
{
pthread_mutex_lock(&mutex);
printf("I am the process P2. PID : %d\n",getpid());
pthread_mutex_unlock(&mutex);
return 0;
}
waitpid(pid1, NULL, 0);
pid_t pid3 = fork();
if(pid3 == 0)
{
pthread_mutex_lock(&mutex);
printf("I am the process P3. PID : %d\n",getpid());
pthread_mutex_unlock(&mutex);
return 0;
}
waitpid(pid2, NULL, 0);
waitpid(pid3, NULL, 0);
pid_t pid4 = fork();
if(pid4 == 0)
{
printf("I am the process P4. PID : %d\n",getpid());
return 0;
}
else if(pid1 != -1){
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);
waitpid(pid3, NULL, 0);
waitpid(pid4, NULL, 0);
}
pthread_mutex_destroy(&mutex);
return 0;
}
运行截图如下:
题目二
火车票余票数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;
若不添加同步机制,设置初始值余票为1000,观察运行结果
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
int ticketCount = 1000;
int temp;
int pthread_yield(void);
void *sale(void *arg) {
for (int i = 0; i < 1000; i++){
temp = ticketCount;
pthread_yield();
temp = temp - 1;
pthread_yield();
ticketCount = temp;
printf("售票: %d 余票: %d \n",i, ticketCount);
}
return NULL;
}
void *preturn(void *arg) {
for (int i = 0; i < 1000; i++){
temp = ticketCount;
pthread_yield();
temp = temp + 1;
pthread_yield();
ticketCount = temp;
printf("退票: %d 余票: %d \n",i, ticketCount);
}
return NULL;
}
int main()
{
pthread_t p1, p2;
pthread_create(&p1, NULL, sale, NULL);
pthread_create(&p2, NULL, preturn, NULL);
pthread_join(p1, NULL);
pthread_join(p2, NULL);
return 0;
}
运行图下图可见余票数出现错误
若加同步机制
添加product和consume两个信号量,售出一张票product
代码实现如下:product+1,退一张票consume+1
实现代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
int ticketCount = 1000;
int temp;
int pthread_yield(void);
sem_t product;
sem_t consume;
void *sale() {
for (int i = 0; i < 1000; i++){
sem_wait(&consume);
temp = ticketCount;
pthread_yield();
temp = temp - 1;
pthread_yield();
ticketCount = temp;
printf("售票: %d 余票: %d \n",i, ticketCount);
sem_post(&product);
}
return NULL;
}
void *preturn(void *arg) {
for (int i = 0; i < 1000; i++){
sem_wait(&product);
temp = ticketCount;
pthread_yield();
temp = temp + 1;
pthread_yield();
ticketCount = temp;
printf("退票: %d 余票: %d \n",i, ticketCount);
sem_post(&consume);
}
return NULL;
}
int main()
{
sem_init(&product, 0, 0);
sem_init(&consume, 0, 1000);
pthread_t p1, p2;
pthread_create(&p1, NULL, sale, NULL);
pthread_create(&p2, NULL, preturn, NULL);
pthread_join(p1, NULL);
pthread_join(p2, NULL);
sem_destroy(&consume);
sem_destroy(&product);
return 0;
}
题目三
一个生产者一个消费者线程同步。设置一个线程共享的缓冲区, char buf[10]。一个线程不断从键盘输入字符到buf,一个线程不断的把buf的内容输出到显示器。要求输出的和输入的字符和顺序完全一致。(在输出线程中,每次输出睡眠一秒钟,然后以不同的速度输入测试输出是否正确)。要求多次测试添加同步机制前后的实验效果。
较快输入
较慢输入
题目四
进程通信问题。阅读并运行共享内存、管道、消息队列三种机制的代码
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发送的字符串?如果把其中互斥的代码删除,观察实验结果有何不同?如果在发送和接收进程中打印输出共享内存地址,他们是否相同,为什么?
发送端和接收端的运行结果如下图所示
receiver能够读出sender发出的字符串
若把互斥拆除运行则receiver无法接收sender的message 截图如下:
打印输出地址相同,互斥访问的是相同的地址
测试b
有名管道和无名管道通信系统调用是否已经实现了同步机制?通过实验验证,发送者和接收者如何同步的。比如,在什么情况下,发送者会阻塞,什么情况下,接收者会阻塞?
无名管道是一类特殊的文件,在内核中对应着一 段特殊的内存空间,内核在这段内存空间中以循环队列 的方式存储数据;无名管道的内核资源在通信双方退出后自动消失,无需人为回收;无名管道主要用于连通亲缘进程(父子进程),用于双方的快捷通信,通信为单向通信
有名管道又称FIFO(Fisrt In First out), 是磁盘上的一个特殊文件,没有在磁盘存储真正的信息,其所存储的信息在内存中通信双方进程结束后,自动丢弃FIFO中的信息,但其在磁盘的文件路径本身依然存在
有名管道和无名管道的通信系统调用实现了同步机制
有名管道运行截图如下
无名管道运行截图如下:
在没有接收者的时候发送者会发生阻塞
没有发送者的时候接受者也会发生阻塞
测试c
消息通信系统调用是否已经实现了同步机制?通过实验验证,发送者和接收者如何同步的。比如,在什么情况下,发送者会阻塞,什么情况下,接收者会阻塞?
server:
等待接收客户端发送的数据,若时间超出600s,则自动exit;当收到信息后,打印接收到的数据;并原样的发送给客户端,由客户端显示
client:
启动两个进程(父子进程),父进程用于发送数据,子进程接收由server发送的数据。发送数据:由使用者手动输入信息,回车后发送;当写入“end~”后,退出本进程。接收数据:接收由Server端发送的数据信息,并打印
实验运行截图如下
经过测试剋看出接收者和发送者是同步的
当没有message被写入队列时接收者发生阻塞
当发送message数大于队列容量时发送者发生阻塞
题目五
阅读Pintos操作系统,找到并阅读进程上下文切换的代码,说明实现的保存和恢复的上下文内容以及进程切换的工作流程。
pintos操作系统在进行进程切换时采用中断,pintos的中断在interrupt.h和interrupt.c之中。
enum intr_level
{
INTR_OFF, /* Interrupts disabled. */
INTR_ON /* Interrupts enabled. */
};
pintos默认每一个ticks调用一次时间中断。换句话说,每一个线程最多可以占据CPU一个ticks的时长,之后就必须放手。
timer_sleep的作用是让此线程等待ticks单位时长,然后再执行。函数原型:
void
timer_sleep (int64_t ticks) //参数的意思是你想要等待的时间长度
{
int64_t start = timer_ticks (); //记录开始时的系统时间
ASSERT (intr_get_level () == INTR_ON);
while (timer_elapsed (start) < ticks) //如果elapse(流逝)的时间>=ticks时就返回。否则将持续占用cpu。
thread_yield ();
}
在timer_sleep()函数中让该进程暂时阻塞(调用thread_block()),然后过了ticks个时间段后再把它加回到ready queue中。
至于因为每一次时间中断的时候恰好是ticks加一的时候,因此我们可以改进timer_interrup()函数,使得系统每次调用他的时候都检查一下我的这个进程是否已经等待了足够长得时间了。如果还没有够,则不管它,如果已经足够长了,则调用thread_unblock()函数将它召唤回ready_queue中。
thread可以帮助pintos得到一个线程被阻塞了多长时间,加入一个整形变量int block_ticks就可以了。当这个线程被block的时候,将block_ticks记录为需要等待的时间长度。之后每次中断的时候检查它一次,并且顺便使其自减。当它小到等于0的时候,把线程调到ready queue中。
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. */
int block_ticks; /* 存储该进程已经被block多久了*/
/* 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. */
#endif
/* Owned by thread.c. */
unsigned magic; /* Detects stack overflow. */
};
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中,作用是返回关中断,然后返回中断关闭前的状态。
学习资料:https://www.cnblogs.com/laiy/p/pintos_project1_thread.html
完整代码链接
https://github.com/notdz56/16281023-/tree/master/操作系统第三次实验代码