OS ucore lab 7

练习零: 填写已有实验:

复制以下文件 其中 trap.c 需要进行修正
vmm.c trap.c default_pmm.c pmm.c proc.c swap_fifo.c
trap.c:
static void trap_dispatch(struct trapframe *tf) {
   
    ++ticks;
    /** 注销掉下面这一句 因为这一句被包含在了 run_timer_list() run_timer_list() 在之前的基础上 加入了对 timer 的支持 ***/
    // sched_class_proc_tick(current);
    run_timer_list();
}

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

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

请在实验报告中

  • Q1: 给出内核级信号量的设计描述,并说其大致执行流流程。

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

  • Q1:
// 先是定义了一个信号量的数据结构
typedef struct {
   
    int value; // 计数值 用于 PV操作
    wait_queue_t wait_queue; // 进程等待队列
} semaphore_t;

// 用于等待队列 存放了当前等待的线程PCB和唤醒原因 等待队列 用于还原结构体的等待队列标志
typedef struct {
   
    struct proc_struct *proc;
    uint32_t wakeup_flags;
    wait_queue_t *wait_queue;
    list_entry_t wait_link;
} wait_t;

// 初始化信号量中的计数值和等待队列
void sem_init(semaphore_t *sem, int value) {
   
    sem->value = value;
    wait_queue_init(&(sem->wait_queue));
}

/***P操作: 要关闭中断并保存 eflag 寄存器的值 避免共享变量被多个线程同时修改 判断计数值是否大于0,若大于0说明此时没有其他线程访问临界区则直接将计数值减1并返回 若计数值小于0则已经有其他线程访问临界区了,就将当前线程放入等待队列中并调用调度函数 等到进程被唤醒再将当前进程从等待队列中,取出并删去,最后判断等待的线程是因为什么原因被唤醒。 ***/
static __noinline uint32_t __down(semaphore_t *sem, uint32_t wait_state) {
   
    bool intr_flag;
    local_intr_save(intr_flag);
    if (sem->value > 0) {
   
        sem->value --;
        local_intr_restore(intr_flag);
        return 0;
    }
    wait_t __wait, *wait = &__wait;
    wait_current_set(&(sem->wait_queue), wait, wait_state);
    local_intr_restore(intr_flag);

    schedule();

    local_intr_save(intr_flag);
    wait_current_del(&(sem->wait_queue), wait);
    local_intr_restore(intr_flag);

    if (wait->wakeup_flags != wait_state) {
   
        return wait->wakeup_flags;
    }
    return 0;
}

/***V操作: 也要关闭中断并保存eflag寄存器的值,防止共享变量同时被多个线程访问或修改 先判断等待队列是否为空。若为空,则将计数值加1并返回 若不为空,说明还有线程在等待,此时取出等待队列的第一个线程并将其唤醒,唤醒的过程中 将其从等待队列中删除 ***/
static __noinline void __up(semaphore_t *sem, uint32_t wait_state) {
   
    bool intr_flag;
    local_intr_save(intr_flag);
    {
   
        wait_t *wait;
        if ((wait = wait_queue_first(&(sem->wait_queue))) == NULL) {
   
            sem->value ++;
        }
        else {
   
            assert(wait->proc->wait_state == wait_state);
            wakeup_wait(&(sem->wait_queue), wait, wait_state, 1);
        }
    }
    local_intr_restore(intr_flag);
}
将内核信号量机制迁移到用户态的最大麻烦在于,用于保证操作原子性的禁用中断机制、以及CPU提供的Test and Set指令机制都只能在用户态下运行,而使用软件方法的同步互斥又相当复杂,这就使得没法在用户态下直接实现信号量机制;于是,为了方便起见,可以将信号量机制的实现放在OS中来提供,然后使用系统调用的方法统一提供出若干个管理信号量的系统调用:
申请创建一个信号量的系统调用,可以指定初始值,返回一个信号量描述符(类似文件描述符);
将指定信号量执行P操作;
将指定信号量执行V操作;
将指定信号量释放掉;

  • Q2:

    相同点:
    提供信号量机制的代码实现逻辑是相同的;
    不同点:
    由于实现原子操作的中断禁用、Test and Set指令等均需要在内核态下运行,因此提供给用户态进程的信号量机制是通过系统调用来实现的,而内核级线程只需要直接调用相应的函数;
    

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

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

// 管程数据结构
typedef struct monitor{
    semaphore_t mutex; // 二值信号量 用来互斥访问管程
    semaphore_t next; // 用于 条件同步 用于发出signal操作的进程等条件为真之前进入睡眠
    int next_count; // 记录睡在 signal 操作的进程数
    condvar_t *cv; // 条件变量
} monitor_t;

// 条件变量数据结构
typedef struct condvar{
    semaphore_t sem; // 用于条件同步 用于发出wait操作的进程等待条件为真之前进入睡眠
    int count; // 记录睡在 wait 操作的进程数(等待条件变量成真)
    monitor_t * owner; // 所属管程
} condvar_t;

// 初始化管程
void monitor_init (monitor_t * mtp, size_t num_cv) {
    int i;
    assert(num_cv>0);
    mtp->next_count = 0; // 睡在signal进程数 初始化为0
    mtp->cv = NULL;
    sem_init(&(mtp->mutex), 1); // 二值信号量 保护管程 使进程访问管程操作为互斥的
    sem_init(&(mtp->next), 0); // 条件同步信号量
    mtp->cv =(condvar_t *) kmalloc(sizeof(condvar_t)*num_cv); // 获取一块内核空间 放置条件变量
    assert(mtp->cv!=NULL);
    for(i=0; i<num_cv; i++){
        mtp->cv[i].count=0;
        sem_init(&(mtp->cv[i].sem),0);
        mtp->cv[i].owner=mtp;
    }
}

// 管程wait操作
/*
先将 因为条件不成立而睡眠的进程计数加1
分支1. 当 管程的 next_count 大于 0 说明 有进程睡在了 signal 操作上 我们将其唤醒
分支2. 当 管程的 next_count 小于 0 说明 当前没有进程睡在 signal操作数 只需要释放互斥体
然后 再将 自身阻塞 等待 条件变量的条件为真 被唤醒后 将条件不成立而睡眠的进程计数减1 因为现在成立了
*/
void cond_wait (condvar_t *cvp) {
    cprintf("cond_wait begin:  cvp %x, cvp->count %d, cvp->owner->next_count %d\n", cvp, cvp->count, cvp->owner->next_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);
}

// 管程signal操作
/*
分支1. 因为条件不成立而睡眠的进程计数小于等于0 时 说明 没有进程需要唤醒 则直接返回
分支2. 因为条件不成立而睡眠的进程计数大于0 说明有进程需要唤醒 就将其唤醒
同时设置 条件变量所属管程的 next_count 加1 以用来告诉 wait操作 有进程睡在了 signal操作上
然后自己将自己阻塞 等待条件同步 被唤醒 被唤醒后 睡在 signal 操作上的进程应该减少 故 next_count 应减 1
*/
void cond_signal (condvar_t *cvp) {
    cprintf("cond_signal begin: cvp %x, cvp->count %d, cvp->owner->next_count %d\n", cvp, cvp->count, cvp->owner->next_count);  

    if (cvp->count > 0) { // 若存在因为当前条件变量而等待的进程的话
        up(&(cvp->sem));
        cvp->owner->next_count++; // 所属管程的 next 计数 加 1 表示当前进程会被等待者堵塞
        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);
}
check_sync.c:
// 测试编号为i的哲学家是否能获得刀叉 如果能获得 则将状态改为正在吃 并且 尝试唤醒 因为wait操作睡眠的进程
// cond_signal 还会阻塞自己 等被唤醒的进程唤醒自己
void phi_test_condvar (i) {
    
    if(state_condvar[i]==HUNGRY&&state_condvar[LEFT]!=EATING
            &&state_condvar[RIGHT]!=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]) ;
    }
}
// 拿刀叉
void phi_take_forks_condvar(int i) {
   
    down(&(mtp->mutex));    // P操作进入临界区
    state_condvar[i] = HUNGRY; // 饥饿状态 准备进食
    phi_test_condvar(i); // 测试当前是否能获得刀叉 
    while (state_condvar[i] != EATING) {
   
        cond_wait(&mtp->cv[i]); // 若不能拿 则阻塞自己 等其它进程唤醒
    }
    if(mtp->next_count>0)
        up(&(mtp->next));
    else
        up(&(mtp->mutex));
}

// 放刀叉
void phi_put_forks_condvar(int i) {
   
    down(&(mtp->mutex)); // P操作进入临界区
    state_condvar[i] = THINKING; // 思考状态
    phi_test_condvar(LEFT); // 试试左右两边能否获得刀叉
    phi_test_condvar(RIGHT);
    if(mtp->next_count>0) // 有哲学家睡在 signal操作 则将其唤醒
        up(&(mtp->next));
    else
        up(&(mtp->mutex)); // 离开临界区
}
相同点:基本的实现逻辑相同;
不同点:最终在用户态下实现管程和条件变量机制,需要使用到操作系统使用系统调用提供一定的支持; 而在内核态下实现条件变量是不需要的;

实验结果:

图

参考链接