Part C: 进程抢占和进程间通信
在Lab4的最后部分,我们将要实现进程抢占功能以及进程间通信.
时钟中断和抢占
阅读代码user/spin
.代码比较简单,创建了一个子进程,在子进程中不断循环.在当前的条件下运行该代码,我们会发现父进程或者内核都无法再运行了,因为子进程始终在用户空间死循环.显然理想的操作系统必须避免这种情况,因为开发者可能会出Bug,又或者恶意程序故意如此.为了支持内核抢占一个正在运行的用户进程,我们必须使得JOS支持外部硬件的时钟中断.
中断机制
外部中断(比如时钟中断),被称为IRQs.一共有16种可能的IRQs,编号为0~15.从IRQ到IDT(中断向量表)的映射不是固定的.picirq.c
中的pic_init()
将IRQs 0~15映射到IDT的IRQ_OFFSET-IRQ_OFFSET+15.
在inc/trap.h
中,IRQ_OFFSET被定义为十进制32,因此IDT入口32~47对应着IRQs 0~15.如果时钟中断是IRQ 0,那么时钟中断的处理函数地址位于IDT[IRQ_OFFSET+0],即IDT[32].IRQ_OFFSET是经过精心挑选的,主要是为了使得外部中断和内部中断分别使用不同的中断号.
相对于xv6,我们对JOS做了一定的简化.在内核态时,外部中断是被禁止的,只有在用户态外部中断是启用的.通过%eflags
寄存器中的FL_IF
标志位来控制外部中断是否启用.当该标志置1,外部中断启用.基于我们的简化,我们可以在进入和离开用户态时,修改该标志位的值.
我们必须确保在用户态时,该标志位被置位.这样当外部中断发生时,内核才能跳转到对应的中断处理函数.除此之外,FL_IF
应该被置0.到目前为止,我们在bootloader中禁用了外部中断,且尚未启用.
Exercise13
- 修改
kern/trapentry.S
和kern/trap.c
,初始化IDT,并提供IRQs 0~15的中断处理函数. - 修改
kern/env.c
中的env_alloc()
,以确保用户态进程外部中断启用. - 取消
sched_halt()
中的sti
注释,使得空闲CPU相应外部中断. - 当调用硬件中断处理函数时,CPU生成的Trapframe和之前稍有不同,不再有error code字段.为了保持Trap frame结构体一致,我们在其中补了0.具体可参考section 9.2和section 5.8
测试
运行user/spin
,可以发现内核打印硬件trap framee信息.虽然CPU现在启用了外部中断,但是并未正确的处理外部中断.因此此时会销毁用户进程,并进入monitor.
处理时钟中断
在user/spin
中,子进程一旦开始运行就进入死循环,因此内核无法再获取CPU的控制权.我们需要配置硬件以定期生成时钟中断,这会使得CPU的控制权强制转移到内核.一旦内核获得了CPU控制权,就可以再次进行进程调度.
在init.c
中,i386_init()
调用了lapic_init()
和pic_init()
.设置时钟和中断控制器以产生时钟中断.
Exercise14
修改trap_dispatch()
,当时钟中断发生时调用sched_yield()
,以触发进程调度.
测试
user/spin
将可以正常运行,父进程可以被正确调度运行并最终杀死子进程.- 运行
user/forktree()
- 运行
make CPUS=2 XXX
- 运行
user/stresssched
- 运行
./grade-lab4 -v
,得分应为65/80
进程间通信IPC
我们之前一直专注在操作系统提供的隔离性方面.它给各个进程一种错觉,以为自己独享整个系统.但同时操作系统还需要提供进程间互相通信的功能.进程间通信使得各进程可以交互.Unix的管道模型就是典型的进程间通信.
进程间通信有许多实现模型,至今仍有许多争论谁是最好的实现模型.在JOS中,我们不去讨论哪种实现更好.我们将会实现一个简单的IPC机制.
JOS中的IPC
我们需要实现一些新的系统调用,通过这些系统调用可以实现一个简单的进程间通信.首先我们需要实现两个系统调用sys_ipc_recv
和sys_ipc_try_send
,其次我们需要实现两个封装它们的库函数ipc_recv
和ipc_send
.
JOS中进程间通信的消息由两部组成: 一个32bit的int和一个可选的单页映射.允许进程传递内存页映射,可以传输远比32位整数更大的值.更进一步,进程也可以通过这种方式共享内存.
发送和接受消息
进程通过调用sys_ipc_recv
来接收消息.这个进程将不再被调度执行,直到接收到一个消息.当一个进程处于消息接收状态时,任何其他进程都可以给它发消息,不局限于一些特定进程,也不局限于它的父进程或者子进程.因此我们在Part A实现的权限检查在这里就不再需要了.IPC机制需要被精心设计以保证安全:一个进程不能够因为接收了一个消息而崩溃.
进程通过调用sys_ipc_try_send
来发送消息,参数为目标进程的id和消息的内容.如果目标进程正处于消息接收状态(即调用了sys_ipc_recv
后,还未接收到消息),则发送成功,返回值0.否则发送失败,返回值-E_IPC_NOT_RECV
.
库函数ipc_recv
负责调用sys_ipc_recv
,然后会在当前进程结构体中,处理接收的消息.类似地,库函数ipc_send
将会重复调用sys_ipc_try_send
,直到发送成功.
传递内存页
当进程调用sys_ipc_recv
时,参数dstva
是UTOP
以下一个有效地址时,表明接收进程愿意接收一个内存页映射.如果发送进程发送一个内存页,则该页将会被映射在dstva
.如果之前dstva
已经有内存页映射了,旧页将会被取消映射.
当进程调用sys_ipc_try_send
时,参数srcva
是UTOP
以下一个有效地址时,表明发送进程想将srcva
对应的内存页发送给接收进程,权限为perm
.在一次内存页传递之后,发送进程和接收进程就共享了这样一个物理内存页.
只要发送进程或者接收进程有一个表明不愿意传递内存页,则不会有内存页被传递.在IPC之后,如果有内存页传递,Env结构体中的env_ipc_perm
将会置为接收页面的权限.如果没有内存页传递,则置为0.
Exercise15
- 在
kern/syscall.c
中,实现sys_ipc_recv
和sys_ipc_try_send
. - 在实现前,请注意阅读两者的注释.
- 在使用
envid2env
,注意将checkperm
设为0,表示不需要权限检查,即任何进程都可以发消息到其他进程,内核仅会对envid做有效性检查. - 在
lib/ipc.c
中,实现ipc_recv
和ipc_send
.
测试APK
- 运行
user/pingpong
- 运行
user/primes
. - 请重点阅读
user/primes
,理解其实现原理.