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

  1. 修改kern/trapentry.Skern/trap.c,初始化IDT,并提供IRQs 0~15的中断处理函数.
  2. 修改kern/env.c中的env_alloc(),以确保用户态进程外部中断启用.
  3. 取消sched_halt()中的sti注释,使得空闲CPU相应外部中断.
  4. 当调用硬件中断处理函数时,CPU生成的Trapframe和之前稍有不同,不再有error code字段.为了保持Trap frame结构体一致,我们在其中补了0.具体可参考section 9.2section 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(),以触发进程调度.

测试

  1. user/spin将可以正常运行,父进程可以被正确调度运行并最终杀死子进程.
  2. 运行user/forktree()
  3. 运行make CPUS=2 XXX
  4. 运行user/stresssched
  5. 运行./grade-lab4 -v,得分应为65/80

进程间通信IPC

我们之前一直专注在操作系统提供的隔离性方面.它给各个进程一种错觉,以为自己独享整个系统.但同时操作系统还需要提供进程间互相通信的功能.进程间通信使得各进程可以交互.Unix的管道模型就是典型的进程间通信.

进程间通信有许多实现模型,至今仍有许多争论谁是最好的实现模型.在JOS中,我们不去讨论哪种实现更好.我们将会实现一个简单的IPC机制.

JOS中的IPC

我们需要实现一些新的系统调用,通过这些系统调用可以实现一个简单的进程间通信.首先我们需要实现两个系统调用sys_ipc_recvsys_ipc_try_send,其次我们需要实现两个封装它们的库函数ipc_recvipc_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时,参数dstvaUTOP以下一个有效地址时,表明接收进程愿意接收一个内存页映射.如果发送进程发送一个内存页,则该页将会被映射在dstva.如果之前dstva已经有内存页映射了,旧页将会被取消映射.

当进程调用sys_ipc_try_send时,参数srcvaUTOP以下一个有效地址时,表明发送进程想将srcva对应的内存页发送给接收进程,权限为perm.在一次内存页传递之后,发送进程和接收进程就共享了这样一个物理内存页.

只要发送进程或者接收进程有一个表明不愿意传递内存页,则不会有内存页被传递.在IPC之后,如果有内存页传递,Env结构体中的env_ipc_perm将会置为接收页面的权限.如果没有内存页传递,则置为0.

Exercise15

  1. kern/syscall.c中,实现sys_ipc_recvsys_ipc_try_send.
  2. 在实现前,请注意阅读两者的注释.
  3. 在使用envid2env,注意将checkperm设为0,表示不需要权限检查,即任何进程都可以发消息到其他进程,内核仅会对envid做有效性检查.
  4. lib/ipc.c中,实现ipc_recvipc_send.

测试APK

  1. 运行user/pingpong
  2. 运行user/primes.
  3. 请重点阅读user/primes,理解其实现原理.

results matching ""

    No results matching ""