ucore-lab7

操作系统kernel

Posted by X1ng on September 8, 2020

知识点

同步互斥

临界区——进程中访问临界资源的一段需要互斥执行的代码

进入区——检查是否可进入临界区的一段代码(如可进入,则设置相应“正在访问临界区”标记)

退出区——清除“正在访问临界区”标记

剩余区——代码其余部分(与同步互斥无关)

临界区的访问规则

  • 空闲则入——没有进程在临界区时,任何进程都可以进入
  • 忙则等待——有进程在临界区时,任何进程均不能进入
  • 有限等待——等待进入临界区的进程不能无限期等待
  • 让权等待(可选)——不能进入等待区的进程应释放cpu,如转化到阻塞状态

临界区的实现方法

禁用硬件中断(仅限单处理器)

基于软件的同步方法——进程间通过共享变量的访问实现进程同步(复杂)

  • Peterson算法(两个线程)

    共享变量

      int turn;//表示哪个进程要进入临界区
      boolean flag[N];//表示进程是否准备好进入临界区
    

    进入区代码(对于线程pid为i)

      flag[i] = true;//编号为i的进程已经准备好进入临界区
      turn = i;//编号为i的进程要进入临界区
      while(flag[j] && turn = i);//进程满足条件则循环等待
    

    退出区代码

      flag[i] = false;
    

    有一个进程时,等待条件不满足,直接进入临界区

    有两个进程时,对于进程编号为i的进程和进程编号为j的进程

  • Dekkers算法(多个线程)

    暂未理解

更高级的抽象同步方法(单处理器多处理器均可)

  • 一个二进制变量表示两种状态(锁定/解锁)
  • Lock::Acquire() 锁被释放前一直等待,返回的时候得到锁(也就是访问临界区)
  • Lock::Release() 释放锁,唤醒其他等待这个锁的线程

锁基于原子操作指令,如测试与置位指令(TS)、交换指令

使用TS指令实现自旋锁

忙等待锁

初始化锁

class Lock{
	int value = 0;//解锁状态
}

请求操作

void Lock::Acquire() {
	while(test-and-set(value));//锁定
		; //spin
}

释放操作

void Lock::Release() {
	value = 0;//解锁
}

无忙等待锁

初始化锁

class Lock{
	int value = 0;
	WaitQueue q;//等待该锁的线程所排成的队列
}

请求操作

void Lock::Acquire() {
	while(test-and-set(value)) {//如果返回1,则代表锁正在被其他线程使用
    ...(将该线程的TCB加入队列q)
    schedule();//该进程进入等待状态,调度其他线程
  }
}

释放操作

void Lock::Release() {
	value = 0;
  ...(将该线程的TCB从等待队列q删除)
  wakeup(t);//唤醒在等待状态的线程
}

信号量

是一种抽象数据类型,由一个整型和两个原子操作组成

int sem; //可用的资源数
P(); //sem减一,如果sem小于0则进入等待,否则继续
V(); //sem加一,如果sem小于等于0则唤醒一个等待进程

信号量的实现

信号量的使用

  • 互斥访问——临界区的互斥访问控制

    每类资源设置一个信号量,其初值为1

    初始化

      mutex = new Samaphore(1);
    

    互斥访问控制

      mutex->P();
      Critical Section;//临界区执行
      mutex->V();
    
  • 条件同步——线程间的事件等待

    设置一个信号量,其初值为0

    初始化

      condition = new Samaphore(0);
    

    如必须先执行X再执行N

生产者-消费者问题

  • 一个或多个生产者在生成数据后放在一个缓冲区里
  • 单个消费者从缓冲区里取出数据处理
  • 任何时刻只能有一个生产者或消费者可访问缓冲区

分析

  • 任何时刻只能有一个线程操作缓冲区(互斥访问)
  • 缓冲区空时,消费者必须等待生产者(条件同步)
  • 缓冲区满时,生产者必须等待消费者(条件同步)

初始化资源信号量

class BoundedBuffer {
	mutex = new Samaphore(1);
	fullBuffers = new Samaphore(0);
	emptyBuffers = new Samaphore(n);
}

生产者

void BoundedBuffer::Deposit(c) {
	emptyBuffers->P();//条件同步,检查是否有空缓冲区
	mutex->P();//互斥访问临界区,检查是否有其他生产者或消费者在访问缓冲区
	...()//向缓冲区中写入
	mutex->V();
	fullBuffers->V();//写数据后释放一个满缓冲区资源
}

消费者

void BoundedBuffer::Remove(c) {
  fullBuffers->V();//条件同步,检查缓冲区中是否有数据
	mutex->P();//互斥访问临界区,检查是否有生产者在访问缓冲区
	...()//从缓冲区中取出
	mutex->V();
	emptyBuffers->P();//取出后释放一个空缓冲区资源
}
管程

是一种用于多线程互斥访问共享资源的程序结构

管程与临界区相似,只是正在管程中的线程可临时放弃管程的互斥访问,等待时间出现时恢复

通过0个到多个条件变量(0个)管理共享数据的并发访问

  • 每个条件变量对应一个等待队列,进入管程的线程因某种资源被占用而进入等待状态,则放在对应的等待队列中
  • 条件变量中的Wait()操作
    • 将自己阻塞在等待队列中
    • 唤醒一个等待或释放管程的互斥访问
  • 条件变量中的Signal()操作
    • 将等待队列中的一个线程唤醒
    • 如果等待队列为空,则等同空操作

管程的实现

  • 对于某个条件Condition,设置一个等待队列和一个整型代表等待的线程数
  • A线程条件不满足时调用Wait函数,则将A线程加入等待队列,并释放锁,调度B线程
  • A线程条件满足时,B线程调用Signal,唤醒原进程,有两种唤醒方式:
    • 让B线程继续执行直到释放B线程后再调度A线程
    • 马上调度A线程,A线程执行完释放之后再恢复B线程(ucore采用的方式)

用管程解决生产者-消费者问题

初始化

class BoundedBuffer {
	...
	Lock lock;
	int count = 0;
	Condition notFull, notEmpty;
  //缓冲区全满 这个条件满足的时候,进程进入notFull对应队列,等待不再缓冲区全满时唤醒
  //缓冲区全空 这个条件满足的时候,进程进入notEmpty对应队列,等待不再缓冲区全为空时唤醒
}

生产者

void BoundedBuffer::Deposit(c) {
	lock->Acquire();//访问临界区
	while(count == n)//条件变量,判断是否有空的缓冲区
		notFull.wait(&lock);//没有空的缓冲区则进入notFull条件变量的等待队列
	...(向缓冲区写入字符)
	count++;
	notEmpty.signal();//如果notEmpty条件变量的等待队列中有线程在等待,则唤醒
	lock->Release;
}

消费者

void BoundedBuffer::Remove(c) {
	lock->Acquire();//访问临界区
	while(count == 0)//条件变量,判断所有缓冲区都是空的
		notEmpty.wait(&lock);//所有缓冲区都是空的则进入notEmpty条件变量的等待队列
	...(从缓冲区读取字符)
	count--;
	notFull.signal();//如果notFull条件变量的等待队列中有线程在等待,则唤醒
	lock->Release;
}

对条件变量的判断使用while,当前运行的线程优先级更高,被唤醒的线程条件判断不满足,只有当前进程释放后才能运行被唤醒的线程

用管程解决哲学家就餐问题

假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃面,或者思考。吃面的时候停止思考,思考的时候也停止吃面。每两个哲学家之间有一只餐叉。假设哲学家必须用两只餐叉吃东西。他们吃面的时候会拿起自己左右手边的两只餐叉,两只餐叉都拿起才可以吃面。要保证哲学家们动作有序地进行,即不会有人永远也拿不到餐叉

对五位哲学家编号,让奇数编号的哲学家先拿右边的餐叉

#define N 5
samphore fork[5];//五只餐叉

void philosopher(int i){
  while(true){
    think();//思考
    if(i%2==0){//编号为偶数的哲学家
      P(fork[i]);//先拿左边
      P(fork[(i+1) % N]);
    }
    else{//编号为奇数的哲学家
      P(fork[(i+1) % N]);//先拿右边
      P(fork[i]);
    }
    eat();//吃面条中...
    V(fork[i]);//放下餐叉(V操作)不会引起死锁
    V(fork[(i+1) % N]);
  }
}

lab7

练习0

练习0:填写已有实验

本实验依赖实验1/2/3/4/5/6。请把你做的实验1/2/3/4/5/6的代码填入本实验中代码中有“LAB1”/“LAB2”/“LAB3”/“LAB4”/“LAB5”/“LAB6”的注释相应部分。并确保编译通过。注意:为了能够正确执行lab7的测试应用程序,可能需对已完成的实验1/2/3/4/5/6的代码进行进一步改进。

需要修改的只有

trap_dispatch函数(trap/trap.c)

static void
trap_dispatch(struct trapframe *tf) {
				...      
        /* LAB7 YOUR CODE */
        /* you should upate you lab6 code
         * IMPORTANT FUNCTIONS:
	       * run_timer_list
         */
        ticks ++;
        run_timer_list();//更新,使用run_timer_list函数
        break;
        ...
}

练习1

练习1: 理解内核级信号量的实现和基于内核级信号量的哲学家就餐问题(不需要编码)

完成练习0后,建议大家比较一下(可用meld等文件diff比较软件)个人完成的lab6和练习0完成后的刚修改的lab7之间的区别,分析了解lab7采用信号量的执行过程。执行make grade,大部分测试用例应该通过。

请在实验报告中给出内核级信号量的设计描述,并说明其大致执行流程。

请在实验报告中给出给用户态进程/线程提供信号量机制的设计方案,并比较说明给内核级提供信号量机制的异同。

内核级信号量

结构体:内核级信号量中设置一个信号量当前值和一个信号量等待队列

P操作:

  • 通过关开中断保证原子操作
  • 如果信号量减一后还是大于0,则直接返回;否则申请一个等待项放入等待队列,并将该线程设置为等待状态,然后调用schedule函数进行调度
  • 在该线程被唤醒的时候,还要将对应等待项从等待队列中删除掉,并检查唤醒线程的原因与线程进入等待状态的原因是否相同

V操作:

  • 通过关开中断保证原子操作
  • 先判断等待队列是否为空,如果为空,则信号量加一后返回;如果等待队列不为空,则从队列中找到对应线程,将其从等待状态变为就绪状态

在ucore中,down函数相当于调用信号量的P操作,up函数相当于调用信号量的V操作

用户态进程/线程的信号量机制与内核相差不多,也要设置信号量当前值和信号量等待队列,但是进行PV操作时需要通过系统调用进入内核态执行

练习2

练习2: 完成内核级条件变量和基于内核级条件变量的哲学家就餐问题(需要编码)

首先掌握管程机制,然后基于信号量实现完成条件变量实现,然后用管程机制实现哲学家就餐问题的解决方案(基于条件变量)。

执行:make grade 。如果所显示的应用程序检测都输出ok,则基本正确。如果只是某程序过不去,比如matrix.c,则可执行

make run-matrix

命令来单独调试它。大致执行结果可看附录。

请在实验报告中给出内核级条件变量的设计描述,并说明其大致执行流程。

请在实验报告中给出给用户态进程/线程提供条件变量机制的设计方案,并比较说明给内核级提供条件变量机制的异同。

请在实验报告中回答:能否不用基于信号量机制来完成条件变量?如果不能,请给出理由,如果能,请给出设计说明和具体实现。

对于条件变量的定义:

typedef struct condvar{
    semaphore_t sem;        //条件变量对应队列 用semaphore结构体 即一个信号量表示
    int count;              //对应等待队列中的线程数
    monitor_t * owner;      //这个条件变量的宿主管程
} condvar_t;

假设有两个线程——线程A和线程B

先运行线程A,线程A因某种条件不满足进入等待状态后调度线程B,线程B在运行时检查到线程A的条件满足时,线程B进入等待状态并唤醒线程A,线程A执行完毕后,唤醒线程B继续执行

typedef struct monitor{
    semaphore_t mutex;      //互斥锁,下图中对线程A中mutex成员的wait和signal操作类似于PV操作
    semaphore_t next;       //用于线程B的wait操作使线程B睡眠或线程A的signal操作唤醒线程B
    int next_count;         //执行signal操作将当前线程(线程A)唤醒的线程(线程B) 的个数
    condvar_t *cv;          //条件变量,线程A执行wait操作让线程A进入条件对应等待队列或线程B执行signal操作唤醒线程A
} monitor_t;

  1. 7处monitor.next_count++使计数加一,且9处sem_wait(monitor.next)使线程B进入等待状态,在线程A执行完后进入12的if分支,执行13处sem_signal(monitor.next)函数唤醒线程B继续运行
  2. 0 3、5 17 是两对PV操作,即在线程B中5处通过sem_wait(monitor.mutex)函数申请对临界区的访问(P操作),直到唤醒线程A后执行完线程A,再唤醒线程B后执行完线程B,才在17处释放对临界区的访问(V操作)

在sync/monitor.c中实现cond_signal和cond_wait函数,实现唤醒和等待功能

用down函数来执行P操作,用up函数来执行V操作

cond_signal函数

// Unlock one of threads waiting on the condition variable. 
void 
cond_signal (condvar_t *cvp) {
   //LAB7 EXERCISE1: YOUR CODE
   cprintf("cond_signal begin: cvp %x, cvp->count %d, cvp->owner->next_count %d\n", cvp, cvp->count, cvp->owner->next_count);  
  /*
   *      cond_signal(cv) {
   *          if(cv.count>0) {
   *             mt.next_count ++;
   *             signal(cv.sem);
   *             wait(mt.next);
   *             mt.next_count--;
   *          }
   *       }
   */
  if(cvp->count>0) {//判断该条件变量对应等待队列中是否有等待的线程
    cvp->owner->next_count++;
    up(&(cvp->sem));
    down(&(cvp->owner->next));
    cvp->owner->next_count--;
  }//没有则相当于空操作
   cprintf("cond_signal end: cvp %x, cvp->count %d, cvp->owner->next_count %d\n", cvp, cvp->count, cvp->owner->next_count);
}

cond_wait函数

// Suspend calling thread on a condition variable waiting for condition Atomically unlocks 
// mutex and suspends calling thread on conditional variable after waking up locks mutex. Notice: mp is mutex semaphore for monitor's procedures
void
cond_wait (condvar_t *cvp) {
    //LAB7 EXERCISE1: YOUR CODE
    cprintf("cond_wait begin:  cvp %x, cvp->count %d, cvp->owner->next_count %d\n", cvp, cvp->count, cvp->owner->next_count);
   /*
    *         cv.count ++;
    *         if(mt.next_count>0)
    *            signal(mt.next)
    *         else
    *            signal(mt.mutex);//release
    *         wait(cv.sem);
    *         cv.count --;
    */
    cvp->count++;
    if(cvp->owner->next_count>0)
      up(&(cvp->owner->next));
    else
      up(&(cvp->owner->mutex));
    down(&(cvp->sem));
    cvp->count --;
    cprintf("cond_wait end:  cvp %x, cvp->count %d, cvp->owner->next_count %d\n", cvp, cvp->count, cvp->owner->next_count);
}

实现哲学家就餐问题的解决方案

ucore通过phi_test_condvar函数来判断哲学家i在hungry状态是否可以eating

void phi_test_condvar (i) { 
    if(state_condvar[i]==HUNGRY&&state_condvar[LEFT]!=EATING
            &&state_condvar[RIGHT]!=EATING) {
      //当哲学家i饥饿时,如果其左右的哲学家都不在eating状态,则哲学家i进入eating状态
        cprintf("phi_test_condvar: state_condvar[%d] will eating\n",i);
        state_condvar[i] = EATING ;
        cprintf("phi_test_condvar: signal self_cv[%d] \n",i);
        cond_signal(&mtp->cv[i]) ;//唤醒哲学家i对应进程
    }
}

补充phi_take_forks_condvar函数(sync/check_sync.c)

/*
 *  void pickup(int i) {
 *      state[i] = hungry;
 *      if ((state[(i+4)%5] != eating) && (state[(i+1)%5] != eating)) {
 *        state[i] = eating;
 *      else
 *         self[i].wait();
 *   }
 */
void phi_take_forks_condvar(int i) {
     down(&(mtp->mutex));//占用临界区资源
//--------into routine in monitor--------------
     // LAB7 EXERCISE1: YOUR CODE
     // I am hungry
     // try to get fork
//--------leave routine in monitor--------------
      state_condvar[i] = HUNGRY;//将哲学家i设置为hungry状态
      phi_test_condvar(i);//调用test函数判断是否可以eating
      if(state_condvar[i] != EATING)//如果不满足条件,未能进入eating状态
        cond_wait(&mtp->cv[i]);     //则进入等待状态,等待邻座哲学家都放下餐叉后才被唤醒
      if(mtp->next_count>0)
         up(&(mtp->next));
      else
         up(&(mtp->mutex));//唤醒释放临界区资源
}

补充phi_put_forks_condvar函数(sync/check_sync.c)

/*
 *   void putdown(int i) {
 *      state[i] = thinking;
 *      if ((state[(i+4)%5] == hungry) && (state[(i+3)%5] != eating)) {
 *          state[(i+4)%5] = eating;
 *          self[(i+4)%5].signal();
 *      }
 *      if ((state[(i+1)%5] == hungry) && (state[(i+2)%5] != eating)) {
 *          state[(i+1)%5] = eating;
 *          self[(i+1)%5].signal();
 *      }
 *   }
 */
void phi_put_forks_condvar(int i) {
     down(&(mtp->mutex));//占用临界区资源
//--------into routine in monitor--------------
     // LAB7 EXERCISE1: YOUR CODE
     // I ate over
     // test left and right neighbors
//--------leave routine in monitor--------------
     state_condvar[i] = THINKING;//哲学家线程状态变为thinking
     phi_test_condvar(LEFT);//检测左右邻座是否处于hungry状态,将处于hungry状态的邻座置位eating状态并唤醒
     phi_test_condvar(RIGHT);
     if(mtp->next_count>0)
        up(&(mtp->next));
     else
        up(&(mtp->mutex));//唤醒释放临界区资源
}