1. 程式人生 > >MIT6.828 LAB4 Part C: Preemptive Multitasking and Inter-Process communication (IPC)

MIT6.828 LAB4 Part C: Preemptive Multitasking and Inter-Process communication (IPC)

  Lab 4的最後一部分就是實現搶佔式排程和程序間通訊。
  
Clock Interrupts and Preemption
  先前的排程是程序資源放棄CPU,但是實際中沒有程序會這樣做的,而為了不讓某一程序耗盡CPU資源,需要搶佔式排程,也就需要硬體定時。但是外部硬體定時在Bootloader的時候就關閉了,至今都沒有開啟。而JOS採取的策略是,在核心中的時候,外部中斷是始終關閉的,在使用者態的時候,需要開啟中斷。
  Exercise 13:
  修改kern/trapentry.S和kern/trap.c 來初始化IDT中IRQs0-15的入口和處理函式。然後修改env_alloc函式來確保程序在使用者態執行時中斷是開啟的。
  回答:
  模仿原先設定預設中斷向量即可,在kern/trapentry.S中定義IRQ0-15的處理例程。

TRAPHANDLER(irq0_entry, IRQ_OFFSET + 0, 0, 0);
TRAPHANDLER(irq1_entry, IRQ_OFFSET + 1, 0, 0);
TRAPHANDLER(irq2_entry, IRQ_OFFSET + 2, 0, 0);
TRAPHANDLER(irq3_entry, IRQ_OFFSET + 3, 0, 0);
TRAPHANDLER(irq4_entry, IRQ_OFFSET + 4, 0, 0);
TRAPHANDLER(irq5_entry, IRQ_OFFSET + 5, 0, 0);
TRAPHANDLER(irq6_entry, IRQ_OFFSET + 6
, 0, 0)
; TRAPHANDLER(irq7_entry, IRQ_OFFSET + 7, 0, 0); TRAPHANDLER(irq8_entry, IRQ_OFFSET + 8, 0, 0); TRAPHANDLER(irq9_entry, IRQ_OFFSET + 9, 0, 0); TRAPHANDLER(irq10_entry, IRQ_OFFSET + 10, 0, 0); TRAPHANDLER(irq11_entry, IRQ_OFFSET + 11, 0, 0); TRAPHANDLER(irq12_entry, IRQ_OFFSET + 12, 0, 0); TRAPHANDLER(irq13_entry, IRQ_OFFSET + 13
, 0, 0)
; TRAPHANDLER(irq14_entry, IRQ_OFFSET + 14, 0, 0); TRAPHANDLER(irq15_entry, IRQ_OFFSET + 15, 0, 0);

  然後在IDT中註冊,修改trap_init,由於先前已經實現簡化,故此無需做處理。
  最後在env_alloc函式中開啟中斷。

        // Enable interrupts while in user mode.
        e->env_tf.tf_eflags |= FL_IF;

Handling Clock Interrupts
  現在雖然中斷使能已經開啟,在使用者態程序執行的時候,外部中斷會產生並進入核心,但是現在還沒有能處理這類中斷。所以需要修改trap_dispatch,在發生外部定時中斷的時候,呼叫排程器,排程另外一個可執行的程序。
  Exercise 14:
  修改trap_dispatch函式,當發生時鐘中斷時呼叫sched_yield函式來排程下一個程序。
  回答:

if (tf->tf_trapno == IRQ_OFFSET + IRQ_TIMER) {
                lapic_eoi();
                sched_yield();
                return;
 }

Inter-Process communication (IPC)
  IPC是計算機系統中非常重要的一部分。在JOS實現IPC的方式是當兩個程序需要通訊的話,一方要發起recv,然後阻塞,直到有一個程序呼叫send向正在接受的程序傳送了資訊,阻塞的程序才會被喚醒。在JOS中,可以允許傳遞兩種資訊,一是一個32位整數,另外一個就是傳遞頁的對映,在這個過程中,接收方和傳送方將同時對映到一個相同的物理頁,此時也就實現了記憶體共享。最後將這兩個功能實現在了同一個系統呼叫。
  
Implementing IPC
  在JOS的IPC實現機制中,修改Env結構體如下:

struct Env {
        struct Trapframe env_tf;        // Saved registers
        struct Env *env_link;           // Next free Env
        envid_t env_id;                 // Unique environment identifier
        envid_t env_parent_id;          // env_id of this env's parent
        enum EnvType env_type;          // Indicates special system environments
        unsigned env_status;            // Status of the environment
        uint32_t env_runs;              // Number of times environment has run
        int env_cpunum;                 // The CPU that the env is running on

        // Address space
        pde_t *env_pgdir;               // Kernel virtual address of page dir

        // Exception handling
        void *env_pgfault_upcall;       // Page fault upcall entry point

        // Lab 4 IPC
        bool env_ipc_recving;           // Env is blocked receiving
        void *env_ipc_dstva;            // VA at which to map received page
        uint32_t env_ipc_value;         // Data value sent to us
        envid_t env_ipc_from;           // envid of the sender
        int env_ipc_perm;               // Perm of page mapping received
};

  其中增加了5個成員:
  env_ipc_recving:
  當程序使用env_ipc_recv函式等待資訊時,會將這個成員設定為1,然後堵塞等待;當一個程序向它發訊息解除堵塞後,傳送程序將此成員修改為0。
  env_ipc_dstva:
  如果程序要接受訊息並且是傳送頁,儲存頁對映的地址,且該地址<=UTOP。
  env_ipc_value:
  若等待訊息的程序接收到訊息,傳送方將接收方此成員設定為訊息值。
  env_ipc_from:
  傳送方負責設定該成員為自己的envid號。
  env_ipc_perm:
  如果程序要接收訊息並且傳送頁,那麼傳送方傳送頁之後將傳送的頁許可權賦給這個成員。
  
  Exercise 15:
  實現在kern/syscall.c中的sys_ipc_recv和sys_ipc_try_send函式。最後實現使用者態的ipc_recv和ipc_send。
  回答:
  首先是sys_ipc_recv函式,其功能是當一個程序試圖去接收資訊的時候,應該將自己標記為正在接收資訊,而且為了不浪費CPU資源,應該同時標記自己為ENV_NOT_RUNNABLE,只有當有程序向自己發了資訊之後,才會重新恢復可執行。最後將自己標記為不可執行之後,呼叫排程器執行其他程序。

static int
sys_ipc_recv(void *dstva)
{
        if (dstva < (void *)UTOP && PGOFF(dstva))
                return -E_INVAL;
        curenv->env_ipc_recving = true;
        curenv->env_ipc_dstva = dstva;
        curenv->env_status = ENV_NOT_RUNNABLE;
        curenv->env_ipc_from = 0;
        sched_yield();
        return 0;
}

  接著是sys_ipc_try_send函式,其實現相對來說麻煩很多,因為有很多的檢測項,包括許可權是否符合要求,要傳送的頁有沒有,能不能將這一頁對映到對方頁表中去等等。如果srcva是在UTOP之下,那麼說明是要共享記憶體,那就首先要在傳送方的頁表中找到srcva對應的頁表項,然後在接收方給定的虛地址處插入這個頁表項。接收完成之後,重新將當前程序設定為可執行,同時把env_ipc_recving設定為0,以防止其他的程序再發送,覆蓋掉當前的內容。

static int
sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
{
        int r;
        pte_t *pte;
        struct PageInfo *pp;
        struct Env *env;

        if ((r = envid2env(envid, &env, 0)) < 0)
                return -E_BAD_ENV;
        if (env->env_ipc_recving != true || env->env_ipc_from != 0)
                return -E_IPC_NOT_RECV;
        if (srcva < (void *)UTOP && PGOFF(srcva))
                return -E_INVAL;
        if (srcva < (void *)UTOP) {
                if ((perm & PTE_P) == 0 || (perm & PTE_U) == 0)
                        return -E_INVAL;
                if ((perm & ~(PTE_P | PTE_U | PTE_W | PTE_AVAIL)) != 0)
                        return -E_INVAL;
        }
        if (srcva < (void *)UTOP && (pp = page_lookup(curenv->env_pgdir, srcva, &pte)) == NULL)
                return -E_INVAL;
        if (srcva < (void *)UTOP && (perm & PTE_W) != 0 && (*pte & PTE_W) == 0)
                return -E_INVAL;
        if (srcva < (void *)UTOP && env->env_ipc_dstva != 0) {
                if ((r = page_insert(env->env_pgdir, pp, env->env_ipc_dstva, perm)) < 0)
                        return -E_NO_MEM;
                env->env_ipc_perm = perm;
        }

        env->env_ipc_from = curenv->env_id;
        env->env_ipc_recving = false;
        env->env_ipc_value = value;
        env->env_status = ENV_RUNNABLE;
        env->env_tf.tf_regs.reg_eax = 0;
        return 0;
}

  完成後需要要加上分發機制,將呼叫號加上。
  最後是2個使用者態庫函式的實現。

int32_t
ipc_recv(envid_t *from_env_store, void *pg, int *perm_store)
{
        int r;

        if (pg == NULL)
                r = sys_ipc_recv((void *)UTOP);
        else
                r = sys_ipc_recv(pg);
        if (from_env_store != NULL)
                *from_env_store = r < 0 ? 0 : thisenv->env_ipc_from;
        if (perm_store != NULL)
                *perm_store = r < 0 ? 0 : thisenv->env_ipc_perm;
        if (r < 0)
                return r;
        else
                return thisenv->env_ipc_value;
}

void
ipc_send(envid_t to_env, uint32_t val, void *pg, int perm)
{
        int r;
        void *dstpg;

        dstpg = pg != NULL ? pg : (void *)UTOP;
        while((r = sys_ipc_try_send(to_env, val, dstpg, perm)) < 0) {
                if (r != -E_IPC_NOT_RECV)
                        panic("ipc_send: send message error %e", r);
                sys_yield();
        }
}

  至此,Lab4的part C部分就完成了。