操作系统实验三

实验三: 同步问题

一、实验目的

1、系统调用的进一步理解。

2、进程上下文切换。

3、同步的方法。

二、实验题目

1、通过fork的方式,产生4个进程P1,P2,P3,P4,每个进程打印输出自己的名字,例如P1输出“I am the process P1”。要求P1最先执行,P2、P3互斥执行,P4最后执行。通过多次测试验证实现是否正确。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <semaphore.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>

sem_t *signalA = NULL;
sem_t *signalB = NULL;
sem_t *signalC = NULL;

int main(int argc, char *argv[])
{
    pid_t pid,pid1,pid2;
    signalA=sem_open("signalA",O_CREAT,0666,0);
    signalB=sem_open("signalB",O_CREAT,0666,0);
    signalC=sem_open("signalC",O_CREAT,0666,0);
    pid = fork();
    if(pid == 0)
    {
        sem_wait(signalA);
        printf("I am the process P2\n");
        sem_post(signalA);
        sem_post(signalB);
        pid1 = fork();
        if(pid1 == 0)
        {
            sem_wait(signalB);
            sem_wait(signalC);
            printf("I am the process P4\n");
        }
    }
    else if(pid>0)
    {
        printf("I am the process P1\n");
        sem_post(signalA);
        pid2 = fork();   
        if(pid2 == 0)
        {
            sem_wait(signalA);
            printf("I am the process P3\n");
            sem_post(signalA);
            sem_post(signalC);
            return 0;
        }
    }
    sem_close(signalA);
    sem_close(signalB);
    sem_close(signalC);
    return 0;
}

编译时加上-pthread,否则会提示:对‘sem_open’未定义的引用和对‘sem_wait’未定义的引用。

最终观察到P1最先执行,P2、P3互斥执行,P4最后执行。

2、火车票余票数ticketCount 初始值为1000,有一个售票线程,一个退票线程,各循环执行多次。添加同步机制,使得结果始终正确。要求多次测试添加同步机制前后的实验效果。

(1)不加同步机制:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

int ticketCount = 1000;
int temp;
sem_t *test=NULL;

void *sell1()
{
    for (int i = 0; i < 100; i++)
    {
        temp = ticketCount;
        pthread_yield();
        temp = temp - 1;
        pthread_yield();
        ticketCount = temp;
        printf("卖出票数:%d ,当前票数: %d \n",i, ticketCount);
    }
    return NULL;
}

void *return1()
{
    for (int i = 0; i < 100; i++)
    {
        temp = ticketCount;
        pthread_yield();
        temp = temp + 1;
        pthread_yield();
        ticketCount = temp;
        printf("退回票数:%d ,当前票数: %d \n", i,ticketCount);
    }
    return NULL;
}

int main(int argc, char *argv[])
{
    test=sem_open("test",O_CREAT,0666,1);
    pthread_t p1, p2;
    pthread_create(&p1, NULL, sell1, NULL);
    pthread_create(&p2, NULL, return1,NULL);
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);
    sem_close(test);
    printf("最终票数为:%d\n",ticketCount);
    return 0;
}

不加同步机制时票数有时会超过1000,推测发生了并发错误。

(2)加入同步机制:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

int ticketCount = 1000;
int temp;
sem_t test1;
sem_t test2;

void *sell1()
{
    for (int i = 0; i < 100; i++)
    {
        sem_wait(&test1);
        temp = ticketCount;
        pthread_yield();
        temp = temp - 1;
        pthread_yield();
        ticketCount = temp;
        printf("卖出票数:%d ,当前票数: %d \n",i, ticketCount);
        sem_post(&test2);
    }
    return NULL;
}

void *return1()
{
    for (int i = 0; i < 100; i++)
    {
        sem_wait(&test2);
        temp = ticketCount;
        pthread_yield();
        temp = temp + 1;
        pthread_yield();
        ticketCount = temp;
        printf("退回票数:%d ,当前票数: %d \n", i,ticketCount);
        sem_post(&test1);
    }
    return NULL;
}

int main(int argc, char *argv[])
{
    sem_init(&test2, 0, 0);
    sem_init(&test1, 0, 1000);
    pthread_t p1, p2;
    pthread_create(&p1, NULL, sell1, NULL);
    pthread_create(&p2, NULL, return1,NULL);
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);
    sem_destroy(&test1);
    sem_destroy(&test2);
    printf("最终票数为:%d\n",ticketCount);
    return 0;
}

观察到票数始终在1000之内

3、一个生产者一个消费者线程同步。设置一个线程共享的缓冲区, char buf[10]。一个线程不断从键盘输入字符到buf,一个线程不断的把buf的内容输出到显示器。要求输出的和输入的字符和顺序完全一致。(在输出线程中,每次输出睡眠一秒钟,然后以不同的速度输入测试输出是否正确)。要求多次测试添加同步机制前后的实验效果。

(1)不加同步机制:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h
#include <semaphore.h>

char buf[10]={0};

void *worker1() 
{
    for ( int i = 0; i < 10; ) 
    {
        scanf("%c",&buf[i]);
        getchar();
        i++;
        i = i%10;
    }
    return NULL;
}

void *worker2() 
{
    for (int i = 0; i < 10; )
    {
        printf("%c\n",buf[i]);
        sleep(1);
        i++;
        i = i%10;
    }
    return NULL;
}

int main(int argc, char *argv[])
{  
    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;
}

观察到输入的字符需要等待一段时间才能输出。

(2)加入同步机制:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>

char buf[10]={0};
sem_t test1;
sem_t test2;

void *worker1() 
{
    for ( int i = 0; i < 10; ) 
    {
        sem_wait(&test1);
        scanf("%c",&buf[i]);
        getchar();
        i++;
        i = i%10;
        sem_post(&test2);
    }
    return NULL;
}

void *worker2()
{
    for (int i = 0; i < 10; ) 
    {
        sem_wait(&test2);
        printf("%c\n",buf[i]);
        sleep(1);
        i++;
        i = i%10;
        sem_post(&test1);
    }
    return NULL;
}


int main(int argc, char *argv[])
{  
    sem_init(&test1, 0, 20);
    sem_init(&test2, 0, 0);
    pthread_t p1, p2;
    pthread_create(&p1, NULL, worker1, NULL);
    pthread_create(&p2, NULL, worker2, NULL);
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);
    sem_destroy(&test1);
    sem_destroy(&test2);
    return 0;
}

输入的字符串可以立即输出。

4、进程通信问题。阅读并运行共享内存、管道、消息队列三种机制的代码

实验测试

a)通过实验测试,验证共享内存的代码中,receiver能否正确读出sender发送的字符串?如果把其中互斥的代码删除,观察实验结果有何不同?如果在发送和接收进程中打印输出共享内存地址,他们是否相同,为什么?

b)有名管道和无名管道通信系统调用是否已经实现了同步机制?通过实验验证,发送者和接收者如何同步的。比如,在什么情况下,发送者会阻塞,什么情况下,接收者会阻塞?c)消息通信系统调用是否已经实现了同步机制?通过实验验证,发送者和接收者如何同步的。比如,在什么情况下,发送者会阻塞,什么情况下,接收者会阻塞?

(a)receiver能正确读出sender发送的字符串

把其中互斥的代码删除,发送方发送字符串后,接收方将接收不到发送方发送的字符串。

在发送和接收进程中打印输出共享内存地址,观察到他们的共享地址相同。

共享内存,是在内存中开辟一段特殊的内存空间,多个进程可互斥访问。发送盒接受过程互斥访问的是同一个地址块,所以共享地址相同。

(b)无名管道:无名管道是一类特殊的文件,在内核中对应着一段特殊的内存空间,内核在这段内存空间中以循环队列的方式存储数据;无名管道的内核资源在通信双方退出后自动消失,无需人为回收;无名管道主要用于连通亲缘进程(父子进程),用于双方的快捷通信,通信为单向通信。

有名管道:又称FIFO(Fisrt In First out), 是磁盘上的一个特殊文件,没有在磁盘存储真正的信息,其所存储的信息在内存中,通信双方进程结束后,自动丢弃FIFO中的信息,但其在磁盘的文件路径本身依然存在。

无名管道和有名管道均已实现同步机制。发送者发送消息之后,接收者进行接收,实现同步。如果发送者发送了消息,而接收者未接受,则发送者将被阻塞,不能继续发送消息。如果发送者未发送消息,此时接收者被阻塞,持续等待。

(c)

消息通信系统调用实现了同步机制。发送端传送的消息都会加入一个消息队列。写进程在此机制中不会被阻塞,其写入的字符串会一直被添加至队列的末端,而读进程会从队列的首端一直读取消息,消息节点一旦被读取便会移除队列。当队列中不含其需要类型的消息时便会阻塞。发送者长时间不发送消息也会造成堵塞。

5、阅读Pintos操作系统,找到并阅读进程上下文切换的代码,说明实现的保存和恢复的上下文内容以及进程切换的工作流程。

参考:https://uchicago-cs.github.io/mpcs52030/switch.html

           http://blog.chinaunix.net/uid-20844267-id-5732888.html

进行上下文切换,主要由switch_to宏实现,代码分析如下:

 

__switch_to函数实现如下:

 

实现的保存和恢复的上下文内容工作流程:从正在运行的线程切换到下一个线程时,下一个线程也必须运行switch_threads(),在之后的线程中的上下文中返回前一个进程信息。此功能通过运行switch_threads函数来实现。实现原理为在堆栈上保留某些特定寄存器,切换堆栈并恢复寄存器。切换堆栈时,CUR的线程结构中记录当前堆栈指针。

如果一个线程已经用完了它的时间片,thread_tick将调用函数intr_yield_on_return。但是,这并不会让位于下一个线程。相反,它修改一个标志,让中断处理程序知道,在从中断返回之前,它应该执行到另一个线程的上下文切换(因此,当我们从中断返回时,我们对上下文执行切换,即,堆栈和程序计数器,属于不同的线程)。

因此,在thread_tick和timer_interrupt返回之后,intr_handler将调用thread_yield,后者将调用schedule。schedule选择要运行的下一个线程并调用一个函数switch_threads,在x86程序集中实现,带有两个参数:( cur指向thread 当前线程结构的指针,即被抢占next的指针)和(指向下一个thread结构的指针) 线程运行)。

堆栈如下:

https://img-blog.csdnimg.cn/2019040818552034.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzA1NTU5Nw==,size_16,color_FFFFFF,t_70

switch_threads在switch中实现。理解switch_threads的关键是首先要理解,如果我们切换到另一个线程,那么当它被抢占时,另一个线程一定也在运行switch_threads。事实上,一个主动或非自愿生成CPU的线程总是会有一个看起来像下面这样的堆栈:

https://uchicago-cs.github.io/mpcs52030/_images/stack4.png

witch_threads背后的直觉是,切换到另一个线程,我们只需要“开关栈”(因为每一个线程都是保证运行switch_threads在抢占),我们可以简单地通过改变esp的价值。让我们仔细看看这是如何发生的。

调用switch_threads之后,堆栈的底部将如下所示:

https://uchicago-cs.github.io/mpcs52030/_images/stack5.png

switch_threads堆栈帧(0x0C00)的起始地址是任意的,没有什么深层意义。但是,显示的所有其他值都与从0x0C00开始的switch_threads堆栈帧一致。

首先,switch_threads需要保存一些寄存器(这只是x86架构所需要的):

pushl %ebx
pushl %ebp
pushl %esi
pushl %edi

我们的堆栈现在看起来是这样的:

https://uchicago-cs.github.io/mpcs52030/_images/stack6.png

当线程被抢占时,此字段用于保存esp的值。然而,在x86汇编中,我们不能只编写t->堆栈。相反,如果我们想要访问struct中的值,我们需要内存中的struct地址和我们想要访问的字段的偏移量(以字节为单位)。这是在thread.c中定义的:

uint32_t thread_stack_ofs = offsetof (struct thread, stack);

因为我们需要在switch_threads中使用这个值,所以我们将它加载到寄存器edx中:

.globl thread_stack_ofs
mov thread_stack_ofs, %edx

将SWITCH_CUR和SWITCH_NEXT定义为cur和Next在堆栈帧(20和24;查看这与前面图中显示的内容如何匹配)。在x86汇编中,表达式SWITCH_CUR(%esp)变为20(%esp),转换为内存地址esp + 20。换句话说,这给出了当前线程(cur)的地址。

类似地,SWITCH_NEXT(%esp)给出了下一个线程的地址。

所以,下面这段汇编代码: 

movl SWITCH_CUR(%esp), %eax
movl %esp, (%eax,%edx,1)
movl SWITCH_NEXT(%esp), %ecx
movl (%ecx,%edx,1), %esp

相当于这样做:

cur->stack = esp;
esp = next->stack;

换句话说,我们保存当前线程的堆栈指针,并将esp设置为指向下一个要运行的线程的(先前保存的)堆栈指针。

一旦我们这样做了,我们已经切换了线程,剩下的就是恢复我们之前推入堆栈的寄存器,并从switch_threads返回:

popl %edi
popl %esi
popl %ebp
popl %ebx
ret

.

 

GitHub源码:https://github.com/wwyw/lab3

展开阅读全文

没有更多推荐了,返回首页