1. 程式人生 > >作業系統ucore lab7實驗報告

作業系統ucore lab7實驗報告

作業系統lab7實驗報告

lab6完成了使用者程序的排程框架和排程演算法的具體實現,即到lab6位置,ucore系統已經可以同事排程執行多個程式。但是這又引來了一個新的問題,那就是當多個同時執行的程序要協同操作或是訪問共享記憶體的時候,如何解決同步和有序競爭的問題。
本次實驗的主要就是解決程序的同步問題

練習0:填寫已有實驗

同樣使用一款名為meld的軟體進行對比即可,大致截圖如下:
enter image description here

這裡把需要填充的檔案羅列如下:

proc.c
default_pmm.c
pmm.c
swap_fifo.c
vmm.c
trap.c
sche.c

另外需要說明的是,上述需要填充的部分,不需要在以前的基礎上再額外加以修改,直接講lab1-lab6的程式碼根據對比複製過來即可。

練習1 理解核心級訊號量的實現和基於核心級訊號量的哲學家就餐問題

在完成本練習之前,先說明下什麼是哲學家就餐問題

哲學家就餐問題,即有五個哲學家,他們的生活方式是交替地進行思考和進餐。哲學家們公用一張圓桌,周圍放有五把椅子,每人坐一把。在圓桌上有五個碗和五根筷子,當一個哲學家思考時,他不與其他人交談,飢餓時便試圖取用其左、右最靠近他的筷子,但他可能一根都拿不到。只有在他拿到兩根筷子時,方能進餐,進餐完後,放下筷子又繼續思考。

在分析之前先對訊號量進行簡介,直接看訊號量的虛擬碼如下

struct semaphore {
int count;
queueType queue;
};

void
P(semaphore S){ S.count--; if (S.count<0) { 把程序置為睡眠態; 將程序的PCB插入到S.queue的隊尾; 排程,讓出CPU; } } void V(semaphore S){ S.count++; if (S.count0) { 喚醒在S.queue上等待的第一個程序; } }

基於上訴訊號量實現可以認為,當多個程序可以進行互斥或同步合作時,一個程序會由於無法滿足訊號量設定的某條件而在某一位置停止,直到它接收到一個特定的訊號(表明條件滿足了)。為了發訊號,需要使用一個稱作訊號量的特殊變數。為通過訊號量s傳送訊號,訊號量通過V、P操作來修改傳送訊號量。
接下來進入程式碼的分析。
lab7和之前的lab6的總體步驟基本沒有多大的變化,開始的執行流程都與實驗六相同,而二者的差異主要是從,而我們跟著程式碼繼續往下看,一直到建立第二個核心執行緒init_main時,我們可以看到,init_main的內容有一定的修改,函式在開始執行排程之前多執行了一個check_sync函式,check_sync函式如下:

void check_sync(void){

    int i;

    //check semaphore
    sem_init(&mutex, 1);
    for(i=0;i<N;i++){d
        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");
    }

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

根據註釋可以看到,該函式分為了兩個部分,第一部分是實現基於訊號量的哲學家問題,第二部分是實現基於管程的哲學家問題。
練習1要求分析基於訊號量的哲學家問題,這裡我們先只用看該函式的前半部分。
首先實現初始化了一個互斥訊號量,然後建立了對應5個哲學家行為的5個訊號量,並建立5個核心執行緒代表5個哲學家,每個核心執行緒完成了基於訊號量的哲學家吃飯睡覺思考行為實現。現在我們繼續跟進philosopher_using_semaphore函式觀察它的具體實現。

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

看到核心就是phi_take_forks_sema和phi_put_forks_sema兩個函式,具體的函式註釋如下:

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); /* 離開臨界區 */
}

而這裡到了訊號量的核心部分,就是上述程式碼中的up和down函式就分別呼叫了__up函式和__down函式,而這兩個函式分別對應著訊號量的V,P操作。
先看__up函式,它實現了訊號量的的V操作

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 ++;//訊號量的value加一
        }
        else {//有程序在等待
            assert(wait->proc->wait_state == wait_state);
            wakeup_wait(&(sem->wait_queue), wait, wait_state, 1);//將`wait_queue`中等待的第一個wait刪除,並將該程序喚醒
        }
    }
    local_intr_restore(intr_flag);//開啟中斷返回
}

首先通過local_intr_save函式關閉中斷,如果訊號量對應的wait queue中沒有程序在等待,直接把訊號量的value加一,然後通過local_intr_restore函式開中斷返回。如果有程序在等待且程序等待的原因是semophore設定的,則呼叫wakeup_wait函式將wait_queue中等待的第一個wait刪除,且把此wait關聯的程序喚醒,最後通過local_intr_restore函式開中斷返回。

再來看看__down函式,它實現了訊號量的P操作

static __noinline uint32_t __down(semaphore_t *sem, uint32_t wait_state) {
    bool intr_flag;
    local_intr_save(intr_flag);  //關掉中斷
    if (sem->value > 0) {//當前訊號量value大於0
        sem->value --;//直接讓value減一
        local_intr_restore(intr_flag);//開中斷返回
        return 0;
    }
    //當前訊號量value小於等於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);//被V操作喚醒,從等待佇列移除
    local_intr_restore(intr_flag);//開中斷

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

首先關掉中斷,然後判斷當前訊號量的value是否大於0。如果是大於0,則表明可以獲得訊號量,故讓value減一,並開啟中斷返回即可;如果小於0,則表明無法獲得訊號量,故需要將當前的程序加入到等待佇列中,並開啟中斷,然後執行排程器選擇另外一個程序執行。如果被V操作喚醒,則把自身關聯的wait從等待佇列中刪除(此過程需要先關中斷,完成後開中斷)

至此,基於訊號量的哲學家問題的解決就分析完畢了。

練習2 完成核心級條件變數和基於核心級條件變數的哲學家就餐問題

即要求首先掌握管程機制,然後基於訊號量實現完成條件變數實現,然後用管程機制實現哲學家就餐問題的解決方案。

管程,即定義了一個數據結構和能為併發程序所執行(在該資料結構上)的一組操作,這組操作能同步程序和改變管程中的資料。
管程相當於一個隔離區,它把共享變數和對它進行操作的若干個過程圍了起來,所有程序要訪問臨界資源時,都必須經過管程才能進入,而管程每次只允許一個程序進入管程,從而需要確保程序之間互斥。
管程主要由這四個部分組成

  • 1、管程內部的共享變數;
  • 2、管程內部的條件變數;
  • 3、管程內部併發執行的程序;
  • 4、對區域性於管程內部的共享資料設定初始值的語句。

所謂條件變數,即將等待佇列和睡眠條件包裝在一起,就形成了一種新的同步機制,稱為條件變數。個條件變數CV可理解為一個程序的等待佇列,佇列中的程序正等待某個條件C變為真。

每個條件變數關聯著一個斷言Pc。當一個程序等待一個條件變數,該程序不算作佔用了該管程,因而其它程序可以進入該管程執行,改變管程的狀態,通知條件變數CV其關聯的斷言Pc在當前狀態下為真。

因而條件變數兩種操作如下:
- wait_cv: 被一個程序呼叫,以等待斷言Pc被滿足後該程序可恢復執行. 程序掛在該條件變數上等待時,不被認為是佔用了管程。
- 被一個程序呼叫,以指出斷言Pc現在為真,從而可以喚醒等待斷言Pc被滿足的程序繼續執行。

大概瞭解了原理之後,接下來我們開始分析具體的程式碼。
ucore中的管程機制是基於訊號量和條件變數來實現的。管程的資料結構monitor_t如下:

typedef struct monitor{
    semaphore_t mutex;      // 二值訊號量,只允許一個程序進入管程,初始化為1
    semaphore_t next;       //配合cv,用於程序同步操作的訊號量
    int next_count;         // 睡眠的程序數量
    condvar_t *cv;          // 條件變數cv
} monitor_t;

管程中的條件變數cv通過執行wait_cv,會使得等待某個條件C為真的程序能夠離開管程並睡眠,且讓其他程序進入管程繼續執行;而進入管程的某程序設定條件C為真並執行signal_cv時,能夠讓等待某個條件C為真的睡眠程序被喚醒,從而繼續進入管程中執行。發出signal_cv的程序A會喚醒睡眠程序B,程序B執行會導致程序A睡眠,直到程序B離開管程,程序A才能繼續執行,這個同步過程是通過訊號量next完成的;而next_count表示了由於發出singal_cv而睡眠的程序個數。

條件變數condvar_t的資料結構如下:

typedef struct condvar{
    semaphore_t sem; //用於發出wait_cv操作的等待某個條件C為真的程序睡眠
    int count;       // 在這個條件變數上的睡眠程序的個數
    monitor_t * owner; // 此條件變數的宿主管程
} condvar_t;

分析完資料結構之後,我們開始分析管程的實現。
ucore設計實現了條件變數wait_cv操作和signal_cv操作對應的具體函式,即cond_wait函式和cond_signal函式,此外還有cond_init初始化函式。
先看看cond_signal函式,實現如下:

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) {  //當前存在睡眠的程序
        cvp->owner->next_count ++;//睡眠的程序總數加一
        up(&(cvp->sem));//喚醒等待在cv.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);
}

首先程序B判斷cv.count,如果不大於0,則表示當前沒有睡眠的程序,因此就沒有被喚醒的物件了,直接函式返回即可;
如果大於0,這表示當前有睡眠的程序A,因此需要喚醒等待在cv.sem上睡眠的程序A。由於只允許一個程序在管程中執行,所以一旦程序B喚醒了別人(程序A),那麼自己就需要睡眠。故讓monitor.next_count加一,且讓自己(程序B)睡在訊號量monitor.next上。如果睡醒了,這讓monitor.next_count減一。

同樣,再來看看cond_wait函式,實現如下:

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);
      cvp->count++;//需要睡眠的程序個數加一
      if(cvp->owner->next_count > 0)
         up(&(cvp->owner->next));//喚醒程序連結串列中的下一個程序
      else
         up(&(cvp->owner->mutex));//否則喚醒睡在monitor.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);
}

可以看出如果程序A執行了cond_wait函式,表示此程序等待某個條件C不為真,需要睡眠。因此表示等待此條件的睡眠程序個數cv.count要加一。接下來會出現兩種情況。
情況一:如果monitor.next_count如果大於0,表示有大於等於1個程序執行cond_signal函式且睡著了,就睡在了monitor.next訊號量上。假定這些程序形成S程序連結串列。因此需要喚醒S程序連結串列中的一個程序B。然後程序A睡在cv.sem上,如果睡醒了,則讓cv.count減一,表示等待此條件的睡眠程序個數少了一個,可繼續執行。
情況二:如果monitor.next_count如果小於等於0,表示目前沒有程序執行cond_signal函式且睡著了,那需要喚醒的是由於互斥條件限制而無法進入管程的程序,所以要喚醒睡在monitor.mutex上的程序。然後程序A睡在cv.sem上,如果睡醒了,則讓cv.count減一,表示等待此條件的睡眠程序個數少了一個,可繼續執行了!

這樣我們就可以在此基礎上繼續完成哲學家就餐問題的解決了,主要是就是如下的兩個函式:

void phi_take_forks_condvar(int i) {
     down(&(mtp->mutex));  //通過P操作進入臨界區
      state_condvar[i]=HUNGRY; //記錄下哲學家i是否飢餓,即處於等待狀態拿叉子
      phi_test_condvar(i);
      while (state_condvar[i] != EATING) {
          cprintf("phi_take_forks_condvar: %d didn't get fork and will wait\n",i);
          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)
        up(&(mtp->next));
     else
        up(&(mtp->mutex));
}

至此,基於條件變數的哲學家就餐問題也得以解決。

實驗結果

通過make qemu得到結果如下圖:
enter image description here

enter image description here
對比實驗指導書,實驗成功!