简介
在PartC 部分,我们要实现抢占非协作式环境,并且实现进程间通信。
Clock Interrupts and Preemption
如果一个进程获得CPU后一直死循环而不主动让出CPU的控制权, 整个系统都将 halt。为了允许内核抢占正在运行的环境,强行重获CPU控制权,我们必须扩展JOS内核以支持来自时钟的外部硬件中断。
Interrupt discipline
外部中断(如设备中断)被称为 IRQs。 IRQ号到 IDT 项的映射不是固定的,其会加上一个IRQ_OFFSET的偏移,在picirq.c
的pic_init
中进行了这个映射过程。外部中断的初始化,实际上就是对硬件 8259A的初始化。
In inc/trap.h, IRQ_OFFSET is defined to be decimal 32. Thus the IDT entries 32-47 correspond to the IRQs 0-15.
In JOS, we make a key simplification compared to xv6 Unix. External device interrupts are always disabled when in the kernel (and, like xv6, enabled when in user space).
我们必须确保在用户环境中运行时设置FL_IF标志,以便在中断到达时,它将被传递到处理器并由中断代码处理。 否则,中断将被屏蔽或被忽略,直到重新启用中断为止。Bootloader 的第一条指令屏蔽了中断,到目前为止,我们还没有重新使能它们。
Exercise 13
Modify kern/trapentry.S and kern/trap.c to initialize the appropriate entries in the IDT and provide handlers for IRQs 0 through 15. Then modify the code in env_alloc() in kern/env.c to ensure that user environments are always run with interrupts enabled.
- 修改
Trapentry.s
,当调用硬件中断处理时,处理器不会传入错误代码,因此我们需要调用TRAPHANDLER_NOEC
宏。
// IRQs
TRAPHANDLER_NOEC(timer_handler, IRQ_OFFSET + IRQ_TIMER);
TRAPHANDLER_NOEC(kbd_handler, IRQ_OFFSET + IRQ_KBD);
TRAPHANDLER_NOEC(serial_handler, IRQ_OFFSET + IRQ_SERIAL);
TRAPHANDLER_NOEC(spurious_handler, IRQ_OFFSET + IRQ_SPURIOUS);
TRAPHANDLER_NOEC(ide_handler, IRQ_OFFSET + IRQ_IDE);
TRAPHANDLER_NOEC(error_handler, IRQ_OFFSET + IRQ_ERROR);
- 修改
trap.c
, 注册IDT。
void dblflt_handler();
void timer_handler();
void kbd_handler();
void serial_handler();
void spurious_handler();
void ide_handler();
void error_handler();
...
// IRQ
SETGATE(idt[IRQ_OFFSET + IRQ_TIMER], 0, GD_KT, timer_handler, 3);
SETGATE(idt[IRQ_OFFSET + IRQ_KBD], 0, GD_KT, kbd_handler, 3);
SETGATE(idt[IRQ_OFFSET + IRQ_SERIAL], 0, GD_KT, serial_handler, 3);
SETGATE(idt[IRQ_OFFSET + IRQ_SPURIOUS], 0, GD_KT, spurious_handler, 3);
SETGATE(idt[IRQ_OFFSET + IRQ_IDE], 0, GD_KT, ide_handler, 3);
SETGATE(idt[IRQ_OFFSET + IRQ_ERROR], 0, GD_KT, error_handler, 3);
- IDT表项中的每一项都初始化为中断门,这样在发生任何中断/异常的时候,陷入内核态的时候,CPU都会将%eflags寄存器上的FL_IF标志位清0,关闭中断;切换回用户态的时候,CPU将内核栈中保存的%eflags寄存器弹回%eflags寄存器,恢复原来的状态。You will have to ensure that the FL_IF flag is set in user environments when they run so that when an interrupt arrives, it gets passed through to the processor and handled by your interrupt code。在
env_allco
中加入以下代码, 同时取消sched_halt()
中sti
的注释,使能中断。
// Enable interrupts while in user mode.
// LAB 4: Your code here.
e->env_tf.tf_eflags |= FL_IF;
Handling Clock Interrupts
The calls to lapic_init and pic_init (from i386_init in init.c), which we have written for you, set up the clock and the interrupt controller to generate interrupts. You
Exercise 14
Modify the kernel's trap_dispatch() function so that it calls sched_yield() to find and run a different environment whenever a clock interrupt takes place.
在dispatch 中加入一个case,lapic_eoi
函数的调用没有想到。
case (IRQ_OFFSET + IRQ_TIMER):
// 回应8259A CPU已经接收中断。
lapic_eoi();
sched_yield();
break;
Inter-Process communication (IPC)
我们一直专注于操作系统的隔离方面,它提供了每个程序都独占一台机器的错觉。 操作系统的另一项重要服务是允许程序在需要时彼此通信。
进程间通信有很多模型。 即使在今天,仍然存在关于哪种 models 最佳的争论。 我们不去辩论。 相反,我们将实现一个简单的IPC机制,然后尝试一下。
IPC in JOS
You will implement two system calls,sys_ipc_recv
andsys_ipc_try_send
. Then you will implement two library wrappersipc_recv
andipc_send
.
- a single 32-bit value.
- optionally a single page mapping.
第二种方式相对前者更灵活。
Sending and Receiving Messages
进程阻塞调用 sys_ipc_recv
,在此过程中,其他进程可以发送信息。进程可以以接受者进程ID和信息,调用sys_ipc_try_send
发送信息。若接收者进程处于等待接收信息的状态,发送者则派送消息,并返回0,否则返回错误-E_IPC_NOT_RECV
。
Exercise 15
Implement sys_ipc_recv and sys_ipc_try_send in kern/syscall.c. Read the comments on both before implementing them, since they have to work together. When you call envid2env in these routines, you should set the checkperm flag to 0, meaning that any environment is allowed to send IPC messages to any other environment, and the kernel does no special permission checking other than verifying that the target envid is valid.
static int
sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
{
// LAB 4: Your code here.
struct Env *dstenv;
pte_t *pte;
struct PageInfo *pp;
int r;
if ( (r = envid2env( envid, &dstenv, 0)) < 0)
return r;
// 不处于等待接收状态, 或有进程已经请求发送数据
if ( (dstenv->env_ipc_recving != true) || dstenv->env_ipc_from != 0)
return -E_IPC_NOT_RECV;
if ((uint32_t)srcva < UTOP) {
if ( PGOFF(srcva))
return -E_INVAL;
if ( !(perm & PTE_P ) || !(perm & PTE_U) )
return -E_INVAL;
if (perm & (~ PTE_SYSCALL))
return -E_INVAL;
if ((pp = page_lookup(curenv->env_pgdir, srcva, &pte)) == NULL )
return -E_INVAL;
if ((perm & PTE_W) && !(*pte & PTE_W) )
return -E_INVAL;
// 接收进程愿意接收一个页
if (dstenv->env_ipc_dstva) {
// 开始映射
if( (r = page_insert(dstenv->env_pgdir, pp, dstenv->env_ipc_dstva, perm)) < 0)
return r;
dstenv->env_ipc_perm = perm;
}
}
dstenv->env_ipc_from = curenv->env_id;
dstenv->env_ipc_recving = false;
dstenv->env_ipc_value = value;
dstenv->env_status = ENV_RUNNABLE;
// 返回值
dstenv->env_tf.tf_regs.reg_eax = 0;
return 0;
//panic("sys_ipc_try_send not implemented");
}
static int
sys_ipc_recv(void *dstva)
{
// LAB 4: Your code here.
if ((uint32_t) dstva < UTOP ) {
if (PGOFF(dstva))
return -E_INVAL;
}
// 大于小于都可以赋值为desva。
curenv->env_ipc_dstva = dstva;
curenv->env_status = ENV_NOT_RUNNABLE;
curenv->env_ipc_recving = true;
curenv->env_ipc_from = 0;
sched_yield();
// panic("sys_ipc_recv not implemented");
return 0;
}
Then implement the ipc_recv and ipc_send functions in lib/ipc.c.
void
ipc_send(envid_t to_env, uint32_t val, void *pg, int perm)
{
// LAB 4: Your code here.
int r;
if (!pg)
pg = (void *) UTOP;
do {
r = sys_ipc_try_send(to_env, val, pg, perm);
if (r == -E_IPC_NOT_RECV) {
// 用户级程序不能直接调用 sched_yeild();
sys_yield();
}
else if( (r != -E_IPC_NOT_RECV)&& (r < 0 )) {
panic("ipc_send failed %e\n", r);
}
} while ( r < 0) ;
}
int32_t
ipc_recv(envid_t *from_env_store, void *pg, int *perm_store)
{
// LAB 4: Your code here.
//panic("ipc_recv not implemented");
int r;
if (!pg)
pg = (void *)UTOP;
r = sys_ipc_recv(pg);
if (from_env_store)
*from_env_store = r < 0? 0:thisenv->env_ipc_from;
if (perm_store)
*perm_store = r<0? 0:thisenv->env_ipc_perm;
if (r < 0)
return r;
else
return thisenv->env_ipc_value;
}
Challenge!
占一个Challenge的坑,有关提高IPC性能的!
Make JOS's IPC mechanism more efficient by applying some of the techniques from Liedtke's paper, Improving IPC by Kernel Design, or any other tricks you may think of. Feel free to modify the kernel's system call API for this purpose, as long as your code is backwards compatible with what our grading scripts expect.
问题
- 写最后一个Exercise15时, 发现一个有意思的东西,
thisenv
和currenv
这两个指针有什么区别。我们在syscall中使用thisenv
编译出错了(很正常)。
thisenv
在lib/libmain.c
中定义。const volatile struct Env *thisenv;
。currenv
在kern/env.h
中定义#define curenv (thiscpu->cpu_env)
。thisenv到底起到了什么作用?记录需要运行的用户环境,可以传递给env_run()
去运行。 而curenv实实在在得记录了当前CUP运行的环境。
Lab4 总结
OS对多处理器的支持,可以实现多进程并行。内核互斥和循环调度是多进程实现的一个要点。
PartB 部分为了实现 COW, 引入了用户级页错误处理机制(文档说与传统的Unix方法不同,暂不了解Unix是如何实现的..)。 COW 只是用户级页错误处理的应用之一。使用这种机制,可以灵活得实现用户对自身发生页错误的处理需要。 因为不同的用户程序,可能需要不同的页错误处理形式。例如,COW错误需要复制相应 page 内容,分开父子进程共同映射的可写空间; 空间过小引发页错误,需要分配一个 page 而增加空间的大小。令我印象深刻的是:COW的实现过程中使用了一种 clever mapping trick
, 十分巧妙。利用二级页表机制,通过插入no-op
指针(其中一个页目录项指针指回页目录),使得我们可以直接通过虚拟地址访问到页目录和页表。COW的其他实现还是比较容易理解的。
PartC 重点在抢占非协作式环境和IPC,前者通过CPU处理中断来实现进程的抢占(调度),需要在用户态使能中断,后者传送信息有传32bit值与页面两种方式。信息的传输与控制过程通过 env 结构体成员实现。这种简单的 IPC 还是容易理解的,但其性能较差,因为其接收进程是阻塞式的。IPC性能提升的方法,之后需要重点学习一下。
Comments | NOTHING