ucore lab7 同步互斥机制 学习笔记
阅读原文时间:2023年07月09日阅读:1

管程的设计实在是精妙,初看的时候觉得非常奇怪,这混乱的进程切换怎么能保证同一时刻只有一个进程访问管程?理清之后大为赞叹,函数中途把前一个进程唤醒后立刻把自己挂起,完美切换.后一个进程又在巧妙的时机将自己唤醒,同时让后一个挂起.看似松散的跳转背后竟然是无比严丝合缝的逻辑,真的就滴水不漏.

等待状态

在proc.h中又增加了等待定时器和等待内核信号量的宏供本节使用

#define WT_INTERRUPTED // the wait state could be interrupted
#define WT_CHILD       // wait child process
#define WT_KSEM        // wait kernel semaphore
#define WT_TIMER       // wait timer

定时器timer

static list_entry_t timer_list; //定时器链表
typedef struct {
    unsigned int expires;       //与前一个timer的时间差
    struct proc_struct *proc;   //关联的进程
    list_entry_t timer_link;    //定时器链表,管理所有定时器
} timer_t;

timer_init: 设置指定timer的proc和expires,并timer_link的next和prev指向自身

add_timer: 把timer插入到timer_list的适应位置,并调整自身和后一项的expires

del_timer: 调整后一项的expires,把自身从timer_list里删除

run_timer_list: timer_list头的时差-1,减到0时唤醒它和它之后的时差为0进程,并从timer_list中删除.返回前调用进程调度框架的proc_tick()

值得注意在其他项目中我们令一个链表项为空时都是让它的next指向NULL,而在UCORE中则是让next指向自身.

等待队列

typedef struct {
    list_entry_t wait_head;
} wait_queue_t;

typedef struct {
    struct proc_struct *proc;    //关联的进程
    uint32_t wakeup_flags;    //标志
    wait_queue_t *wait_queue;    //所属的等待队列
    list_entry_t wait_link;    //等待队列的链表,决定了自己的位置
} wait_t;

与之对应的定义了各种增删查找初始化的函数

信号量semaphore

typedef struct {
    int value;
    wait_queue_t wait_queue;
} semaphore_t;

信号量的本质可以想象成红绿灯,大于0时可以通行,否则就按顺序排队等待.

value>0: 信号量可用

value=0: 信号量被占用

value<0: 等待信号量的进程数

主要操作为一下四个:

sem_init(sem,value): 初始化

down(sem): 申请占用信号量

up(sem): 释放信号量

try_down(sem): 检查信号量value>0?,为真的时候令value--并返回1

down间接调用了__down

void
down(semaphore_t *sem) {
    uint32_t flags = __down(sem, WT_KSEM);
    assert(flags == 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;
    //把当前进程变为SLEEPING态并插入等待队列
    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;
}

up间接调用了__up

void
up(semaphore_t *sem) {
    __up(sem, WT_KSEM);
}

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);
}

这时候我们来分析一下基于信号量的哲学家就餐问题

int state_sema[N]; /* 记录每个人状态的数组 */
/* 信号量是一个特殊的整型变量 */
semaphore_t mutex; /* 临界区互斥 */
semaphore_t s[N]; /* 每个哲学家一个信号量 */

struct proc_struct *philosopher_proc_sema[N];

void phi_test_sema(i) /* i:哲学家号码从0到N-1 */
{
    if(state_sema[i]==HUNGRY&&state_sema[LEFT]!=EATING
            &&state_sema[RIGHT]!=EATING)
    {
        state_sema[i]=EATING;
        up(&s[i]);
    }
}

void phi_take_forks_sema(int i) /* i:哲学家号码从0到N-1 */
{
        down(&mutex); /* 进入临界区 */
        state_sema[i]=HUNGRY; /* 记录下哲学家i饥饿的事实 */
        phi_test_sema(i); /* 试图得到两只叉子 */
        up(&mutex); /* 离开临界区 */
        down(&s[i]); /* 如果得不到叉子就阻塞 */
}

void phi_put_forks_sema(int i) /* i:哲学家号码从0到N-1 */
{
        down(&mutex); /* 进入临界区 */
        state_sema[i]=THINKING; /* 哲学家进餐结束 */
        phi_test_sema(LEFT); /* 看一下左邻居现在是否能进餐 */
        phi_test_sema(RIGHT); /* 看一下右邻居现在是否能进餐 */
        up(&mutex); /* 离开临界区 */
}

int philosopher_using_semaphore(void * arg) /* i:哲学家号码,从0到N-1 */
{
    int i, iter=0;
    i=(int)arg;
    cprintf("I am No.%d philosopher_sema\n",i);
    while(iter++<TIMES)
    { /* 无限循环 */
        cprintf("Iter %d, No.%d philosopher_sema is thinking\n",iter,i); /* 哲学家正在思考 */
        do_sleep(SLEEP_TIME);
        phi_take_forks_sema(i);
        /* 需要两只叉子,或者阻塞 */
        cprintf("Iter %d, No.%d philosopher_sema is eating\n",iter,i); /* 进餐 */
        do_sleep(SLEEP_TIME);
        phi_put_forks_sema(i);
        /* 把两把叉子同时放回桌子 */
    }
    cprintf("No.%d philosopher_sema quit\n",i);
    return 0;
}

void check_sync(void){

    int i;

    //check semaphore
    sem_init(&mutex, 1);
    for(i=0;i<N;i++){
        sem_init(&s[i], 0);
        int pid = kernel_thread(philosopher_using_semaphore, (void *)i, 0);
        if (pid <= 0) {
            panic("create No.%d philosopher_using_semaphore failed.\n");
        }
        philosopher_proc_sema[i] = find_proc(pid);
        set_proc_name(philosopher_proc_sema[i], "philosopher_sema_proc");
    }

    //.......

}

注意mutex初始值为1,可以被占用.而s数组值为0.

假设0号哲学家第一个行动,执行它的take_fork函数.先占用mutex,保证这一时刻只有他能行动.他HUNGRY了,一看左右都没有在EATING,于是把自己设为EATING态,并把信号量s0释放,证明自己现在可以吃了.再把mutex释放,允许别人行动.此时再把s0占用掉,证明自己已经开始吃了.最后返回到using_semaphore里,让自己睡眠一会儿.

在此期间1号哲学家被调度,不过要等到0号哲学家释放mutex后才能执行take_fork.1号占用到mutex后感觉HUNGRY了,一看左边0号还拿着叉子不放手.没办法就先释放了mutex,等在了自己的s1上.

风水轮流转,等着0号执行到put_fork,把叉子放下,才能唤醒1号

管程monitor

//条件变量结构体
typedef struct condvar{
    semaphore_t sem;        // 关联的信号量
    int count;              // 等待进程个数
    monitor_t * owner;      // 所属的管程
} condvar_t;

//管程结构体
typedef struct monitor{
    semaphore_t mutex;      // 初始值为1,保证每次只有一个进程进入管程
    semaphore_t next;       // the next semaphore is used to down the signaling proc itself, and the other OR wakeuped waiting proc should wake up the sleeped signaling proc.
    int next_count;         // the number of of sleeped signaling proc
    condvar_t *cv;          // 条件变量数组
} monitor_t;

// Initialize monitor.
void
monitor_init (monitor_t * mtp, size_t num_cv) {
    int i;
    assert(num_cv>0);
    mtp->next_count = 0;
    mtp->cv = NULL;
    sem_init(&(mtp->mutex), 1); //unlocked
    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;
    }
}

// Unlock one of threads waiting on the condition variable.
void
cond_signal (condvar_t *cvp) {
   if(cvp->count>0){
       cvp->owner->next_count++;
       up(&(cvp->sem));
       down(&(cvp->owner->next));
       cvp->owner->next_count--;
   }
}

// 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) {
    cvp->count++;
    if(cvp->owner->next_count>0){
        up(&(cvp->owner->next));
    }
    else{
        up(&(cvp->owner->mutex));
    }
    down(&(cvp->sem));
    cvp->count--;
}

基于管程的哲学家就餐问题

struct proc_struct *philosopher_proc_condvar[N]; // N philosopher
int state_condvar[N];                            // the philosopher's state: EATING, HUNGARY, THINKING
monitor_t mt, *mtp=&mt;                          // monitor

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));
    state_condvar[i]=HUNGRY;
    phi_test_condvar(i);
    if(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));
    state_condvar[i]=THINKING;
    phi_test_condvar((i+1)%5);
    phi_test_condvar((i+4)%5);
    if(mtp->next_count>0)
        up(&(mtp->next));
    else
        up(&(mtp->mutex));
}

//---------- philosophers using monitor (condition variable) ----------------------
int philosopher_using_condvar(void * arg) { /* arg is the No. of philosopher 0~N-1*/

    int i, iter=0;
    i=(int)arg;
    cprintf("I am No.%d philosopher_condvar\n",i);
    while(iter++<TIMES)
    { /* iterate*/
        cprintf("Iter %d, No.%d philosopher_condvar is thinking\n",iter,i); /* thinking*/
        do_sleep(SLEEP_TIME);
        phi_take_forks_condvar(i);
        /* need two forks, maybe blocked */
        cprintf("Iter %d, No.%d philosopher_condvar is eating\n",iter,i); /* eating*/
        do_sleep(SLEEP_TIME);
        phi_put_forks_condvar(i);
        /* return two forks back*/
    }
    cprintf("No.%d philosopher_condvar quit\n",i);
    return 0;
}

void check_sync(void){

    int i;

    //......

    //check condition variable
    monitor_init(&mt, N);
    for(i=0;i<N;i++){
        state_condvar[i]=THINKING;
        int pid = kernel_thread(philosopher_using_condvar, (void *)i, 0);
        if (pid <= 0) {
            panic("create No.%d philosopher_using_condvar failed.\n");
        }
        philosopher_proc_condvar[i] = find_proc(pid);
        set_proc_name(philosopher_proc_condvar[i], "philosopher_condvar_proc");
    }
}

monitor_init中会将除mutex以外的所有成员置零

0号先行动,调用take_forks,占用mutex,检查合法后将自己设为EATING.调用cond_signal,因为cv[0]->count初始值为0,所以直接跳过.然后mtp->next_count为初始值0,释放mutex,返回.

1号再行动,占用mutex,检查后发现自己不满足EATING条件,调用cond_wait.mtp->next_count为0,释放mutex.cv[1]->count变成1.申请信号量,因为cv[1]->sem->value为0,进入等待状态.

等到轮完一遍再到0号时,执行put_fork,占用mutex,放下叉子后检查左右的状态,执行1号的cond_signal.此时cv[1]->count为1,满足条件,将monitor的next_count增加1,解除cv[1]->sem的占用,1号进入RUNNABLE态.因为管程中同一时刻只能有一个进程访问,故马上申请占用monitor的next信号量,让自己进入等待状态.

此时1号得以继续在cont_wait的down中执行.因为是从一个占用管程的进程切换到了另一个占用管程的进程,保证了管程中同一时刻只能有一个进程访问,所以mutex也无须变更.将cv[1]->count变回0,返回.

再往后,随便哪个n号进程运行到cond_wait时,都会因为next_count>0而唤醒等待next信号量的0号进程.又因为n号进程一定是因为没有进入EATING才执行的cond_wait,所以n号进程一定会在down(cv[n]->sem)处卡住,切换到0号进程,有一次保证了保证了管程中同一时刻只能有一个进程访问.

0号进程切回后next_count--,直接返回.

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章