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部分就完成了。