Nachos操作系统实习-lab3

内容一:总体概述

本次lab的主要内容是在理解Nachos信号量的实现基础上扩展锁和条件变量的实现方式,并通过它们切实解决一些常见的同步问题。

本次lab的重点在于掌握同步的实现方法,并设计对常见的同步问题的解决策略。

内容二:任务完成情况

任务完成列表(Y/N)

Exercise1Exercise2Exercise3Exercise4Challenge1Challenge2
完成情况YYYYYY

具体Exercise的完成情况

Exercise1 调研
Linux中实现的同步机制主要有以下几种:

原子操作:汇编语言实现,保证一个线程对数据进行操作时不会被其他线程打断。

自旋锁:当一个线程获得了锁之后,其他线程对锁的获取都会进入循环等待这个锁,直至锁重新可用。线程对锁的循环等待会造成CPU处理时间的浪费,因此自旋锁适合被用于处理快的临界区。

读写自旋锁:读写自旋锁除了有自旋特性之外,读锁之间是共享的,即当一个线程获得了读锁之后,其他线程也可以以读的方式获得这个锁;但是写锁之间是互斥的,当一个线程获得了写锁之后,其他线程不能再以读或者写的方式获得这个锁;而且读写锁之间是互斥的,即一个线程获得了读锁之后,其他线程不能以写的方式获得这个锁。

信号量:是锁的一种,与自旋锁的区别在于,当线程获取不到信号量的时候,不会循环去等待这个信号量,而是进入睡眠。直到有信号量释放,线程才会被从睡眠中唤醒,进入临界区执行。由于线程会睡眠,不占用CPU时间,所以信号量适用于处理慢的临界区。但如果使线程进入睡眠的时间和唤醒线程的时间要超过线程获取自旋锁需要等待的时间,那么就可以考虑使用自旋锁而不是信号量。

读写信号量:如同读写自旋锁与自旋锁的区别一样,读写信号量与信号量区别也在于读锁之间共享,写锁之间互斥,读写锁之间互斥。

互斥量:互斥量也是一种可以睡眠的锁,互斥量要求值只能为0或1,加锁和解锁必须由同一个线程来进行。由于互斥量和信号量都会引起线程睡眠,所以都不能用在中断上下文中,以防影响系统的吞吐量。

大内核锁:与自旋锁类似,区别在于大内核锁可以递归的获得,用于保护整个内核。

顺序锁:与读写锁类似,区别在于当读锁被获取时,写锁仍然可以被获取。使用顺序锁在读之前和读之后都会检查顺序锁的序列值,如果前后不符就说明读的过程中发生了写操作,顺序锁会重复读直到读到的前后序列值相同。

此外,Linux还提供了许多其他的同步机制,各种各样的机制本质上是为了控制多个进程按照一定的规则或顺序访问某些系统资源,进而确保Linux系统高效稳定的运行。

Exercise2 源代码阅读
synch.h头文件中定义了与线程同步的数据结构,包括信号量Semaphore、锁Lock和条件变量Condition这三个类,每个类中相应的定义了一些成员变量并声明了一些成员函数。其中主要的成员函数就是Semaphore的P和V函数,他们都是原子操作;Lock的Acquire和Release函数,也是原子操作,Acquire用来获得锁,Release用来释放锁;Condition的Wait、Signal、Braodcast函数,同样是原子操作,Wait用来释放锁,让当前线程让出CPU直到被信号唤醒,然后重新获取锁,Signal用来唤醒等待锁的一个线程,Broadcast用来唤醒等待锁的所有线程。

synch.cc中对Semaphore中的成员函数进行了具体实现,包括构造函数、析构函数、P操作和V操作。P操作等到信号量的值大于0时将其减1,否则就让当前线程不断进入睡眠状态;V操作将信号量的值加1,在必要时唤醒一个线程。对于Lock和Condition的成员函数没有实现。

synchlist.h头文件中定义了一个互斥访问的队列,即SynchList类,定义了其成员变量list为指向这个队列的指针、lock为指向确保实现互斥访问的锁的指针、listEmpty为指向条件变量的指针;并声明了一些成员函数。

synchlist.cc中对SynchList类的成员函数进行了具体实现,包括构造函数;析构函数;Append函数,用来向队列末尾添加元素;Remove函数,用来删除队列中最前的元素;Mapcar函数,用来对队列中的所有元素运用同样的操作。其中Append、Remoe和Mapcar函数都是被锁和信号量保护的,每个操作都要先获得锁之后,才能对列表进行相应操作。

Exercise3 实现锁和条件变量
锁本质上就是一个值为1的信号量,所以利用已经实现的Semaphore类来完善Lock类。

第一步:在synch.h头文件中为Lock类添加两个私有成员变量。一个是Semaphore *lock,是一个用来表示锁的信号量;另一个是Thread *locker,由于锁只能由获得它的线程释放,所以locker用来指向当前获得这个锁的线程。

第二步:在synch.cc中实现Lock类。首先是构造函数,初始化name, lock, locker,其中lock初始化为一个指向值为1的信号量的指针,locker一开始指向NULL。析构函数中将lock和locker指针删除。Acquire函数中首先关闭中断以保证操作的原子性,然后通过lock调用P操作,之后用locker记录当前获得这个锁的线程currentThread,最后再将中断状态恢复为之前的状态。Release函数中首先关闭中断以保证操作的原子性,然后用声明语句确保当前要执行Release操作的线程是获得锁的线程,之后通过lock调用V操作,之后将locker指向NULL,最后再将中断恢复为之前的状态。isHeldByCurrentThread函数判断locker和currentThread是不是指向同一个线程,如果是则返回true,否则返回false。

第三步:在synch.h头文件中为Condition类添加一个私有成员变量List *queue,这是指向一个队列的指针,这个队列用来维护所有休眠在当前信号量上的线程。

第四步:在synch.cc中实现Condition类。首先是构造函数,初始化name和queue。析构函数中将queue指针删除。Wait函数中首先关闭中断以保证操作的原子性,然后将当前的锁释放,将当前的线程加入队列中,并使当前线程休眠,之后直到当前线程接收到信号被唤醒后重新获得锁,最后再将中断恢复为之前的状态。Signal函数中首先关闭中断以保证操作的原子性,然后取出队列中第一个线程,调用ReadyToRun函数让其成为就绪态,最后再将中断恢复为之前的状态。Broadcast函数中首先关闭中断以保证操作的原子性,然后不断从队列中取出第一个线程让其成为就绪态,直到队列为空,最后再将中断恢复为之前的状态。

Exercise4 实现同步互斥实例
我选择的是生产者-消费者问题,分别通过信号量、条件变量来实现。先是通过信号量实现的步骤。

第一步:在threadtest.cc中包含synch.h头文件,以方便定义相关的信号量、锁和条件变量,并定义三个指向信号量的指针Empty, Full, mutex,它们的初始值分别为N=4, 0, 1。此外定义当前生产的产品数product,初值为0。

第二步:定义相关的生产者、消费者函数。生产者函数中根据传入的参数循环which次,表示生产的次数,首先调用Empty的P操作,确保当前缓冲区不是满的;然后调用mutex的P操作,以实现生产和消费的互斥操作;之后将产品数量加1,然后调用mutex的V操作和Full的V操作。消费者函数中根据传入的参数循环which次,表示消费的次数,首先调用Full的P操作,确保当前缓冲区不是空的;然后调用mutex的P操作,以实现生产和消费的互斥操作;之后将产品数量减1,然后调用mutex的V操作和Empty的V操作。具体实现如下:

void
Se_Producer(int which)
{
    for (int i = 0; i < which; ++i) {
        Empty->P();
        mutex->P();

        ++product;
        printf("*** Thread %s produced. Current products: %d\n", currentThread->getName(), product);
        if (product == N) {
            printf("*** Product full, waiting for consumer...\n");
        }

        mutex->V();
        Full->V();
    }
}

void
Se_Consumer(int which)
{
    for (int i = 0; i < which; ++i) {
        Full->P();
        mutex->P();

        --product;
        printf("*** Thread %s consumed. Current products: %d\n", currentThread->getName(), product);
        if (product == 0) {
            printf("*** Product empty, waiting for producer...\n");
        }

        mutex->V();
        Empty->V();
    }
}

第三步:定义测试函数ThreadTest6,其中创建四个线程,分别Fork出子线程,其中两个调用生产者函数,两个调用消费者函数,观察打印结果如下:
在这里图片描述
可以看到生产者和消费者有条不紊的运行,当缓冲区已满时只能消费者线程去消费,当缓冲区为空时只能生产者线程去生产。

下面是通过条件变量实现的步骤。

第一步:在threadtest.cc中定义指向条件变量的指针produce和consume,以及指向与这两个条件变量相关的锁的指针pc_lock。

第二步:定义相关的生产者、消费者函数。生产者函数中根据传入的参数循环which次,表示生产的次数,首先调用pc_lock的Acquire函数来获得锁;然后判断若当前缓冲区产品数已满,就调用produce的Wait函数让当前线程休眠,直到再次被信号唤醒;唤醒后可以将产品数加1,然后调用consume的Signal函数唤醒因为缓冲区为空而等待的线程;最后调用pc_lock的Release函数释放锁。消费者函数中根据传入的参数循环which次,表示消费的次数,首先调用pc_lock的Acquire函数来获得锁;然后判断若当前缓冲区产品数为空,就调用consume的Wait函数让当前线程休眠,直到再次被信号唤醒;唤醒后可以将产品数减1,然后调用produce的Signal函数唤醒因为缓冲区已满而等待的线程;最后调用pc_lock的Release函数释放锁。具体实现如下:

void
Con_Producer(int which)
{
    for (int i = 0; i < which; ++i) {
        pc_lock->Acquire();
        while (product == N) {
            printf("*** Product full, %s waiting for consumer...\n", currentThread->getName());
            produce->Wait(pc_lock);
        }

        ++product;
        printf("*** Thread %s produced. Current products: %d\n", currentThread->getName(), product);

        consume->Signal(pc_lock);
        pc_lock->Release();
    }
}

void
Con_Consumer(int which)
{
    for (int i = 0; i < which; ++i) {
        pc_lock->Acquire();
        while (product == 0) {
            printf("*** Product empty, %s waiting for producer...\n", currentThread->getName());
            consume->Wait(pc_lock);
        }

        --product;
        printf("*** Thread %s consumed. Current products: %d\n", currentThread->getName(), product);

        produce->Signal(pc_lock);
        pc_lock->Release();
    }
}

第三步:定义测试函数ThreadTest7,其中创建四个线程,分别Fork出子线程,其中两个调用生产者函数,两个调用消费者函数,观察打印结果如下:
在这里插入图片描述
可以看到生产者和消费者有条不紊的运行,当缓冲区已满时只能消费者线程去消费,当缓冲区为空时只能生产者线程去生产。

Challenge1 实现barrier
第一步:在threadtest.cc中定义指向条件变量的指针barrier_cond,以及指向与这个条件变量相关的锁的指针barrier_lock。此外定义得以继续运行的线程数,设置为6;以及到达的线程数,初值为0。

第二步:定义函数BarrierTest,其中首先调用barrier_lock的Acquire函数获得锁,然后将到达的线程数加1;如果当前到达的线程数达到或超过了得以继续运行的线程数,那么通过barrier_cond的Broadcast函数向所有因为这个条件变量阻塞的线程发送信号唤醒它们,然后通过barrier_lock的Release函数将当前锁释放;如果没有达到得以继续运行的线程数,那么通过barrier_cond的Wait函数阻塞当前线程,直到再次被信号唤醒,最后通过barrier_lock的Release函数将当前锁释放。具体实现如下:

void
BarrierTest(int which)
{
    barrier_lock->Acquire();
    ++barrier_threads;
    if (barrier_threads >= BARRIER) {
        printf("*** Already %d threads, Continue!\n", barrier_threads);
        barrier_cond->Broadcast(barrier_lock);
        barrier_lock->Release();
    }
    else {
        printf("*** Only %d threads, need more threads!\n", barrier_threads);
        barrier_cond->Wait(barrier_lock);
        barrier_lock->Release();
    }
    printf("*** Thread do something.\n");
}

第三步:定义测试函数ThreadTest8,其中循环创建BARRIER+4个线程,分别Fork出子线程,调用BarrierTest函数,观察打印结果如下:
在这里插入图片描述
可以看到当线程数未达到标准的时候线程一直被阻塞,不会去输出”Thread do something”,而当到达一定数量时之前的线程都会被唤醒得以继续执行,之后的线程也可以继续执行。

Challenge2 实现read/write lock
第一步:在threadtest.cc中定义指向信号量的指针rw_lock,以及指向锁的指针r_lock。由于可以允许多个读者同时读数据,所以rw_lock在读者中需要由第一个到达的读者获取,由最后一个读完的读者释放,而由于无法保证释放锁的线程与获取锁的线程一致,所以采用信号量表示。此外定义读者的数量,初值为0。

第二步:定义函数Reader和Writer,Reader函数中根据传入的参数循环which次,表示一个读者读的次数;首先调用r_lock的Acquire函数获取锁以保护对read_cnt的修改;然后read_cnt加1,此时判断如果这是第一个读者,那么调用rw_lock的P函数,使读写互斥,否则可以直接进行读取,之后调用r_lock的Release函数释放锁;读取结束后需要调用r_lock的Acquire函数以保护对read_cnt的修改;将read_cnt减1,此时判断如果这是最后一个读完的读者,那么调用rw_lock的V函数,最后调用r_lock的Release函数释放锁。Writer函数中根据传入的参数循环which次,表示一个写者写的次数;然后调用rw_lock的P函数,使读写互斥,写完后再嗲用rw_lock的V函数释放。具体实现如下:

void
Reader(int which)
{
    for (int i = 0; i < which; ++i) {
        r_lock->Acquire();

        ++read_cnt;
        if (read_cnt == 1) {
            rw_lock->P();
        }

        r_lock->Release();

        printf("*** %s reading...\n", currentThread->getName());

        r_lock->Acquire();

        --read_cnt;
        if (read_cnt == 0) {
            rw_lock->V();
        }

        r_lock->Release();
    }
}

void
Writer(int which)
{
    for (int i = 0; i < which; ++i) {
        rw_lock->P();

        printf("*** %s writing...\n", currentThread->getName());

        rw_lock->V();
    }
}

第三步:定义测试函数ThreadTest9,其中创建四个线程,分别Fork出子线程,其中两个调用读者函数,两个调用写者函数,观察打印结果如下:
在这里插入图片描述
可以看到读者写者操作是互斥的,且若干线程可以同时读取某共享数据区内的数据,但是在某一特定的时刻,只有一个线程可以向该共享数据区写入数据。

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

困难1 实现Challenge2的读者写者问题时不知道怎么实现多个读者同时读

在实现Challenge2的问题时,因为要保证多个读者可以同时读取,所以我的思路是只有第一个读者进来读的时候会加锁以保证读写互斥,之后的读者进来读的时候不会加锁,到最后一个读者读完的时候会释放锁。但是第一个读者和最后一个读者可能并不是同一个线程调用的,然而锁要求获得它的线程来释放,所以这里我思考过后将其换成了信号量表示,这样就可以成功实现读者写者问题啦。

内容四:收获及感想

通过完成这次lab,我对Nachos的同步机制有了更深入的理解。对Linux中同步机制的调研,让我更加了解了多种多样的同步机制实现方法,对同步机制的一步步探索,彰显着程序员们思维的一步步完善,让我十分敬佩。

这次在实现了锁和条件变量之后又利用它们实现了很多经典的问题,让我获益匪浅,亲手将这些问题实现之后,我对于同步机制才可以说有了切实的体会。

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

我建议讨论课的时候可以大家多交流交流自己lab的实现方式,看看有没有一些比较好的想法,也可以看看大家有没有碰到一些相同的问题,可以讨论一起解决,一起进步。

内容六:参考文献

[1] Andrew S. Tanenbaum著.陈向群 马洪兵 译 .现代操作系统[M].北京:机械工业出版社,2011:47-95.
[2] Linux内核中同步机制: https://www.cnblogs.com/liuwei0773/articles/9506748.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值