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