欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Nachos实习——Lab3同步机制实习报告

程序员文章站 2022-05-12 12:32:56
...

Nachos实习——Lab3同步机制实习报告

内容一:总体概述

本实习希望通过修改Nachos系统平台的底层源代码,达到“扩展同步机制,实现同步互斥实例”的目标。

内容二:任务完成情况

Exercise 1 Exercise 2 Exercise 3 Exercise 4 Challenge
完成情况 Y Y Y Y Y

内容三:具体完成Exercise情况

Exercise 1 调研

​ 调研Linux中实现的同步机制。具体内容见课堂要求。

调研情况:

我是在Linux-4.14版本下做调查,Linux的同步机制的相关变量函数都定义在/include/asm-generic目录下。该目录下定义了:原子操作、自旋锁、读写自旋锁、信号量、读写信号量、互斥量、完成变量、顺序锁、禁止抢占、顺序和屏障等同步机制。

Exercise 2 源代码阅读

​ 阅读下列源代码,理解Nachos现有的同步机制。

  • code/threads/synch.h和code/threads/synch.cc
  • code/threads/synchlist.h和code/threads/synchlist.cc

1、threads/synch.h(cc)

上述文件中声明了三个类Semaphore、Lock、Condition。其中条件变量和锁只是声明,并未定义;而信号量既声明也定义了。下面简单介绍下每个类的关键函数成员。

  • Semaphore

该类定义了一个信号量值和一个等待队列,对信号量有两个操作:P操作和V操作,这两个操作都是原子操作(Nachos因为是一个运行在单一处理器上的操作系统,所以原子操作只需要进行开挂中断即可实现)。其中P操作:当value等于0时,将当前运行线程放入线程等待队列,当前进程进入睡眠状态,并切换到其他线程运行;当value大于0时,value–。V操作:如果线程等待队列中有等待该信号量的线程,取出其中一个将其设置成就绪态,准备运行,value++。

  • Lock

在现有的 Nachos 中,没有给出锁机制的实现,锁有两个操作 Acquire 和 Release,它们都是原子操作。 Acquire: 申请锁:当锁处于 BUSY 态,进入睡眠状态。 当锁处于 FREE 态,当前线程获得该锁,继续运行。Release: 释放锁(注意:只有拥有锁的线程才能释放锁)将锁的状态设置成 FREE 态,如果有其它线程等待该锁,将其中的一个唤醒,进入就绪态。

  • Condition

在现有的 Nachos 中,没有给出条件变量的实现,条件变量有三个操作 Wait、Signal 以及 BroadCast, 所有的这些操作必须在当前线程获得一个锁的前提下,而且所有对一个条件变量进行的操作必须建立在同一个锁的前提下。其中void Wait (Lock *conditionLock)把线程放入条件变量的等待队列上。void Signal (Lock *conditionLock) 唤醒一个等待该条件变量的线程(如果存在的话)。void BroadCast (Lock *conditionLock)) 唤醒所有等待该条件变量的线程(如果存在的话)

2、code/threads/synchlist.h(cc)

利用锁、条件变量实现的一个消息队列。提供了对List的Append(),Remove()和Mapcar()操作。每个操作都要先获得该锁,然后才能对List进行相应的操作,确保在多线程同时访问该List时,不会发生同步错误。

Exercise 3 实现锁和条件变量

​ 可以使用sleep和wakeup两个原语操作(注意屏蔽系统中断),也可以使用Semaphore作为唯一同步原语(不必自己编写开关中断的代码)。

1、Lock实现

a、基本思想

我选择使用值为1的信号量来实现锁功能。首先在Lock类中添加成员变量semaphore和holder(表示持有锁的进程),同时请求锁和释放锁都必须关中断。

b、synch.h
class Lock {
  private:
    Thread* holderThread;
    Semaphore* semaphore; 
};
c、synch.cc

下面代码的思想都很简单,就不做解释了!

Lock::Lock(char* debugName){
    name = debugName;
    semaphore = new Semaphore("Lock",1);//设置值为1的信号量实现锁
}
Lock::~Lock(){
    delete semaphore;
}
void Lock::Acquire(){
    IntStatus oldLevel = interrupt->SetLevel(IntOff);//关中断
    DEBUG('s', "锁\"%s\"被线程\"%s\"请求\n", name, currentThread->getName());
    semaphore->P();
    holder = currentThread;
    (void)interrupt->SetLevel(oldLevel);//恢复中断
}
bool Lock::isHeldByCurrentThread()
{
    return currentThread == holder;
}
void Lock::Release(){
    IntStatus oldLevel = interrupt->SetLevel(IntOff);
    DEBUG('s', "锁\"%s\"被线程\"%s\"释放\n", name, currentThread->getName());
    ASSERT(this->isHeldByCurrentThread());//判断当前进程是否持有锁
    holder=NULL;
    semaphore->V();
    (void) interrupt->SetLevel(oldLevel);
}
d、测试

结合生产者-消费者问题进行测试

2、条件变量Condition

a、设计思想

条件变量的所有操作都是在当前线程获得一个锁的前提下进行的,而且所有对一个条件变量进行的操作 必须建立在同一个锁的前提下。因此需要基于锁机制实现,同时为条件变量添加一个成员变量queue,用于存放所有等待在该条件变量上的线程。

b、synch.h

因为条件变量必须建立在同一个锁的前提下,所以当进行条件变量操作的时候我需要判断该条件变量是被锁,为此我们在类里面添加一个函数isLocked来进行判断

class Condition {
  bool isLocked() { return !semaphore->getValue(); }
  private:
    List* waitQueue;
};
class Semaphore {
  public:
    int getValue() { return value; }
};
c、synch.cc

下面的代码也很简单,只要理解条件变量机制就能实现,就不做过多解释!

Condition::Condition(char* debugName)
{
    name = debugName;
    waitQueue = new List();
}
Condition::~Condition()
{
    delete waitQueue;
}
void Condition::Wait(Lock* conditionLock) { 
    IntStatus oldLevel = interrupt->SetLevel(IntOff);//关中断
    ASSERT(conditionLock->isHeldByCurrentThread());//
    ASSERT(conditionLock->isLocked());//条件变量操作必须建立在锁的前提下
    conditionLock->Release();//释放锁,进入睡眠状态
    waitQueue->Append(currentThread);
    currentThread->Sleep();
    conditionLock->Acquire();//被signal唤醒,重新申请锁
    (void)interrupt->SetLevel(oldLevel);

}
void Condition::Signal(Lock* conditionLock){
    IntStatus oldLevel = interrupt->SetLevel(IntOff);
    ASSERT(conditionLock->isHeldByCurrentThread());
    if(!waitQueue->IsEmpty()){
        Thread* thread = (Thread*) waitQueue->Remove();
        scheduler->ReadyToRun(thread);
    }
    (void)interrupt->SetLevel(oldLevel);
}
void Condition::Broadcast(Lock* conditionLock){

    IntStatus oldLevel = interrupt->SetLevel(IntOff);
    ASSERT(conditionLock->isHeldByCurrentThread());
    DEBUG('b', "Condition \"%s\" Broadcasting: ", name);
    while(!(waitQueue->IsEmpty()))
    {
        Thread* thread = (Thread*) waitQueue->Remove();
        DEBUG('b', "Thread \"%s\", ", thread->getName());
        scheduler->ReadyToRun(thread);
    }
    (void)interrupt->SetLevel(oldLevel);
}
d、测试

结合生产者-消费者问题进行测试

Exercise 4 实现同步互斥实例

​ 基于Nachos中的信号量、锁和条件变量,采用两种方式实现同步和互斥机制应用(其中使用条件变量实现同步互斥机制为必选题目)。具体可选择“生产者-消费者问题”、“读者-写者问题”、“哲学家就餐问题”、“睡眠理发师问题”等。(也可选择其他经典的同步互斥问题)

1、信号量实现生产者消费者问题

a、设计思想

这部分的所有代码全都写在了ThreadTest中,因为需要用到synch.h中的类,所以需要在ThreadTest的头文件中导入synch.h。核心方法包括生产一个产品produceItem、消费一个产品consumeItem、生产者进程ProducerThread、消费者进程ConsumerThread、将产品方法缓冲区putItem、将产品从缓冲区中取走removeItem。具体实现流程:首先定一个buffer类,类中主要包含成员putItem、removeItem;然后实现生产者进程ProducerThread,ProducerThread每次调用produceItem生产出一个产品,然后调用putItem把产品放入缓冲区;消费者进程ConsumerThread每次调用consumeItem消费一个产品,然后调用removeItem把相应产品从缓冲区取走。

b、具体实现
#define BUFFER_SIZE 10//缓冲区大小
typedef struct PRODUCT {
    int value;
} product;//产品编号
class buffer{
  private:
    int count1;
    Lock* buffer_mutex;//互斥访问缓冲区
    Semaphore* fillCount;//满缓冲区个数
    Semaphore* emptyCount;//空缓冲区个数
    product list[BUFFER_SIZE];//缓冲区数组
  public:
    buffer(){
        fillCount = new Semaphore("FillCount",0);
        emptyCount = new Semaphore("EmptyCount",BUFFER_SIZE);
        buffer_mutex = new Lock("Buffer_mutex");
        count1 = 0;
    }
    void putItem(product* item){
        emptyCount->P();//判断是否还有空缓冲区
        buffer_mutex->Acquire();//获得对临界资源的互斥访问权

        list[count1++]=*item;//将产品放入数组

        buffer_mutex->Release();//释放对临界资源的访问权
        fillCount->V();//唤醒在等待的消费者进程
    }
    product* removeItem(){
        fillCount->P();//判断是否有可以消费的产品
        buffer_mutex->Acquire();// 获取对临界资源的互斥访问权

        product* item = &list[count1-- -1];//从临界资源中获取产品

        buffer_mutex->Release();//释放临界资源的访问权
        emptyCount->V(); // 唤醒在等待的生产线程

        return item;
    }
}*shared_buffer;
product* produceItem(int value)//生产一个产品
{
    printf("Producing item with value %d!!\n", value);
    product item;
    item.value = value;
    return &item;
}

void consumeItem(product* item)/消费一个产品
{
    printf("Consuming item with value %d!!\n", item->value);
}

void ProducerThread(int iterNum)//生产者线程
{
    for (int i = 0; i < iterNum; i++) {
        printf("## %s ##: ", currentThread->getName());
        product* item = produceItem(i);
        shared_buffer->putItem(item);
        interrupt->OneTick();//让时钟前进,从而使得使用-rs调用TimerInterruptHandler
    }
}

void ConsumerThread(int iterNum)
{
    for (int i = 0; i < iterNum; i++) {
        printf("$$ %s $$: ", currentThread->getName());
        product* item = shared_buffer->removeItem();
        consumeItem(item);
        interrupt->OneTick();
    }
}
c、测试

我创建了两个生产者进程和两个消费者进程,具体信息如下:

Producer1 9
Producer1 6
Consumer1 7
Consumer2 8
void
Lab3ProducerConsumer()
{
    DEBUG('t', "Entering Lab3ProducerConsumer");

    shared_buffer = new buffer();

    Thread *producer1 = new Thread("Producer 1");
    Thread *producer2 = new Thread("Producer 2");
    Thread *consumer1 = new Thread("Consumer 1");
    Thread *consumer2 = new Thread("Consumer 2");

    producer1->Fork(ProducerThread, (void*)9);
    consumer1->Fork(ConsumerThread, (void*)7);
    consumer2->Fork(ConsumerThread, (void*)8);
    producer2->Fork(ProducerThread, (void*)6);

    currentThread->Yield(); // Yield the main thread
}

测试截图
Nachos实习——Lab3同步机制实习报告

2、条件变量实现生产者消费者问题

a、设计思想

设计思想与信号量几乎一样,只是操作上的区别

b、具体实现

在信号量的基础上修改private定义、构造函数、putItem和removeItem函数

class buffer{
  private:
    int count1;
    Lock* buffer_mutex;//互斥访问缓冲区
    Condition* condc;
		Condition* condp;
    product list[BUFFER_SIZE];//缓冲区数组
  public:
     buffer(){
        condc = new Condition("ConsumerCondition");
        condp = new Condition("ProducerCondition");
        buffer_mutex = new Lock("Buffer_mutex");
        count1 = 0;
    }
    void putItem(product* item){
        buffer_mutex->Acquire();//获得对临界资源的互斥访问权
        while(count1>=BUFFER_SIZE){
            printf("Product alread full:[%d]\n",count1);
            condp->Wait(buffer_mutex);
        }
        list[count1++]=*item;//将产品放入数组
        condc->Signal(buffer_mutex);//唤醒消费者进程
        buffer_mutex->Release();//释放对临界资源的访问权
    }
    product* removeItem(){
        buffer_mutex->Acquire();// 获取对临界资源的互斥访问权
        while(count1<=0)
        {
            printf("-->Product alread empty:[%d],\n",count1);
            condc->Wait(buffer_mutex);
        }
        product* item = &list[count1--];//从临界资源中获取产品
        condp->Signal(buffer_mutex);//唤醒生产者进程
        buffer_mutex->Release();//释放临界资源的访问权
        return item;
    }
}*shared_buffer;
c、测试

Nachos实习——Lab3同步机制实习报告

你可以尝试设置一个更小的buffer_size进行测试,能帮助你更好理解条件变量!

Challenge 1 实现barrier(至少选做一个Challenge)

​ 可以使用Nachos 提供的同步互斥机制(如条件变量)来实现barrier,使得当且仅当若干个线程同时到达某一点时方可继续执行。

TODO

Challenge 2 实现read/write lock

​ 基于Nachos提供的lock(synch.h和synch.cc),实现read/write lock。使得若干线程可以同时读取某共享数据区内的数据,但是在某一特定的时刻,只有一个线程可以向该共享数据区写入数据。

1、设计思想

读者-写者问题的核心思想:使得若干线程可以同时读取某共享数据区内的数据,但是在某一特定的时刻,只有一个线程可以向该共享数据区写入数据。

伪代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QnVieTEz-1605675571326)(/Users/jaggerqian/Library/Application Support/typora-user-images/image-20201118112727901.png)]

2、synch.h

class ReaderWriterLock {
  public:
    ReaderWriterLock(char* debugName); 
    ~ReaderWriterLock(); 
    char* getName() { return (name); } 

    void ReaderAcquire();
    void ReaderRelease();
    void WriterAcquire();
    void WriterRelease();
  	int getBlockingReader(){return blockingReader;};
  private:
    char* name;            
    int blockingReader;     //记录读者访问数量
    Semaphore* binary_semaphore_writer;//读者写着互斥访问临界区
    Lock* mutex_reader;//多个读者互斥访问blockingReader
};

3、synch.cc

ReaderWriterLock::ReaderWriterLock(char* debugName)
{
    name = debugName;
    blockingReader = 0;
    binary_semaphore_writer = new Semaphore("ReaderWriterLock Binary Semaphore for Writer", 1);
    mutex_reader = new Lock("ReaderWriterLock Mutex for Readers");
}
ReaderWriterLock::~ReaderWriterLock()
{
    delete binary_semaphore_writer;
    delete mutex_reader;
}

void
ReaderWriterLock::ReaderAcquire()//完全仿照伪代码实现
{
    IntStatus oldLevel = interrupt->SetLevel(IntOff); 
    mutex_reader->Acquire(); //
    blockingReader++;
    if (blockingReader == 1) {
        binary_semaphore_writer->P(); 
    }
    mutex_reader->Release(); 
    (void) interrupt->SetLevel(oldLevel);  
}
void ReaderWriterLock::ReaderRelease()
{
    IntStatus oldLevel = interrupt->SetLevel(IntOff);  
    mutex_reader->Acquire(); 
    blockingReader--;
    if (blockingReader == 0) {
        binary_semaphore_writer->V(); 
    }
    mutex_reader->Release(); 
    (void) interrupt->SetLevel(oldLevel);  
}

void ReaderWriterLock::WriterAcquire()
{
    binary_semaphore_writer->P();
}
void ReaderWriterLock::WriterRelease()
{
    binary_semaphore_writer->V();
}

4、测试

我设置了一个数组wandr,里面存放的是,写进程需要写的内容;设置了一个quote,里面为写进程已经写好的内容;我最开始设置两个读进程,后来发现读进程已经运行结束了,写进程还没写完(因为随机时钟中断的原因);因此我又设置了3个进程。

const char wandr[9][100] = {
    "自我介绍:", "我", "的", "英", "文", "名", "叫","Jagger","!"
};
int words;//用来记录已经写了多少个单词
char quote[9][100];
ReaderWriterLock* RWLock;
void
WriterThread(int dummy)
{
    while (words < 9) {
        RWLock->WriterAcquire();

        strcpy(quote[words], wandr[words]); 
        words++;
        printf("Writer is writting: ");
        for (int i = 0; i < words; i++) {
            printf("%s ", quote[i]);
        }
        printf("\n");
        RWLock->WriterRelease();
    }
}
void
ReaderThread(int reader_id)
{
    do {
        RWLock->ReaderAcquire();

        printf("Reader %d is reading: ", reader_id);
        for (int i = 0; i < words; i++) {
            printf("%s ", quote[i]);
        }
        printf("\t(blockingReader=%d)\n",RWLock->getBlockingReader());

        RWLock->ReaderRelease();
    } while (words < 9);
}
void
Lab3ReaderWriter()
{
    const int total_reader = 2;
    Thread* writer = new Thread("Writer");
    writer->Fork(WriterThread, (void*)0);

    for (int i = 0; i < total_reader; i++) {
        char readerName[10];
        sprintf(readerName, "Reader %d", i+1); 
        Thread* reader = new Thread(strdup(readerName)); 
        reader->Fork(ReaderThread, (void*)i+1);
    }
    RWLock = new ReaderWriterLock("RWLock");
    currentThread->Yield(); 
}

第一次测试截图
Nachos实习——Lab3同步机制实习报告

第二次测试截图
Nachos实习——Lab3同步机制实习报告

Challenge 3 研究Linux的kfifo机制是否可以移植到Nachos上作为一个新的同步模块。

TODO

内容四:遇到的困难以及解决方法

内容五:收获及感想

这次的实验比较简单,只要掌握了同步互斥机制,就能顺利实现。

内容六:对课程的意见和建议

这是一门非常值得推荐的课。

内容七:参考文献