Preemptive Multitasking and Inter-Process communication

PartC 任务是实现一个抢占式的多任务处理和进程间通信。 Concepts: interrupt, ipc, preemptive_multitasking.

Exercise 13,14

Something need to konw

为了防止一个进程长时间的占用CPU而不释放,所以需要能够让kernel剥夺一个正在运行的进程占用CPU,为此,JOS kernel 支持外部硬件中断。在JOS 中,外部中断在inc/trap.h中有定义。在JOS中,对IRQ做了一些化简,当在内核态下的时候,外部中断被禁止,在用户态下的时候,外部中断被允许。禁止和允许外部中断是通过设置EFLAG寄存器中的IF标志位实现的(详见intel x86的手册)。
Trap 和 Interrupt 的区别:

  • 对于Interrupt,在通过Interrupt Gate的时候,会把EFLAGS中的IF标志位清零,不允许中断。
  • 对于Trap,不会修改IF标志位。

在JOS中,通过SETGATE中的istrap可以设置。

#define STS_CG32  0xC     // 32-bit Call Gate
#define STS_IG32  0xE     // 32-bit Interrupt Gate
#define STS_TG32  0xF     // 32-bit Trap Gate

// Set up a normal interrupt/trap gate descriptor.
// - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate.
    //   see section 9.6.1.3 of the i386 reference: "The difference between
    //   an interrupt gate and a trap gate is in the effect on IF (the
    //   interrupt-enable flag). An interrupt that vectors through an
    //   interrupt gate resets IF, thereby preventing other interrupts from
    //   interfering with the current interrupt handler. A subsequent IRET
    //   instruction restores IF to the value in the EFLAGS image on the
    //   stack. An interrupt through a trap gate does not change IF."
// - sel: Code segment selector for interrupt/trap handler
// - off: Offset in code segment for interrupt/trap handler
// - dpl: Descriptor Privilege Level -
//    the privilege level required for software to invoke
//    this interrupt/trap gate explicitly using an int instruction.
#define SETGATE(gate, istrap, sel, off, dpl)      \
{               \
  (gate).gd_off_15_0 = (uint32_t) (off) & 0xffff;   \
  (gate).gd_sel = (sel);          \
  (gate).gd_args = 0;         \
  (gate).gd_rsv1 = 0;         \
  (gate).gd_type = (istrap) ? STS_TG32 : STS_IG32;  \
  (gate).gd_s = 0;          \
  (gate).gd_dpl = (dpl);          \
  (gate).gd_p = 1;          \
  (gate).gd_off_31_16 = (uint32_t) (off) >> 16;   \
}

Exercise 13

初始化IDT使得JOS能够handle外部中断。在kern/trapentry.S中添加相应的条目,同时在kern/trap.c中初始化interrupt gate, 注意在SETGATE的时候,istrap要置位0表示这是一个Interrupt,在进入内核态之后,外部中断被禁止。同时在kern/env.c中的env_alloc中初始化eflags。当在进入trap的时候,保存进程的trapframe,其中包含eflags,这样在中断处理结束之后,从内核态转到用户态的时候,通过trapframe恢复eflags,这样就保证了在恢复到用户态的时候,允许外部中断。

// Enable interrupts while in user mode.
// LAB 4: Your code here.
// Set FL_IF, so when return in user mode, eflags will be enabled.
e->env_tf.tf_eflags = e->env_tf.tf_eflags | FL_IF;

Exercise 14

为了能够让kernel拿到CPU的控制权,需要每隔一段时间差生一个外部中断,在内核初始化的时候,通过调用lapic_initpic_init设置好了硬件会每隔一段时间产生外部中断,这个Exercise要做的就是在产生中断的时候,通过IDT找到中断处理函数,让内核拿到CPU的控制权,调度其他进程。

// Handle clock interrupts. Don't forget to acknowledge the
// interrupt using lapic_eoi() before calling the scheduler!
// LAB 4: Your code here.
if (tf->tf_trapno == IRQ_OFFSET + IRQ_TIMER){
  lapic_eoi();
  sched_yield();
  return ;
}

Exercise 15

Something need to know

这部分是实现IPC,在JOS中,实现IPC,需要实现sys_ipc_recvsys_ipc_try_send系统调用,和两个lib的包装函数ipc_recvipc_send。IPC的简要过程:

  1. sendenv 通过调用 ipc_send, 因为可能recvenv还没有执行到ipc_recv,所以recvenv的env_ipc_recving字段还是false,ipc_send就需要不断的调用sys_ipc_try_send来发送消息
  2. recvenv 通过调用 ipc_recv,ipc_recv调用 sys_ipc_recv系统调用,修改自己env 中的状态信息。
  3. 当recvenv的env_ipc_recvingtrue的时候,kernel负责将32bit value 和 page mapping 传递给recvenv,同时将recvenv的状态修改。使得recvenv可以被调度。

Exercise 15

IPC传送的消息包含两部分:32 bit value 和 single page mapping。 page mapping 作为消息的一部,可以实现任意两个进程共享一块内存,同时,如果传送的消息比较大,可以通过page mapping 实现消息传送。
为了实现IPC,在struct Env中,多出了以下字段:

// Lab 4 IPC
bool env_ipc_recving;   // Env is blocked receiving,表示env等待消息中。如果env_ipc_recving位false,那么任何发送进程都不能将消息发送到这个env中。
void *env_ipc_dstva;    // VA at which to map received page. 消息中的page mapping 部分
uint32_t env_ipc_value;   // Data value sent to us, 消息中的 32bit value 部分
envid_t env_ipc_from;   // envid of the sender,如果收到消息,记录消息的发送者
int env_ipc_perm;   // Perm of page mapping received, 消息中的page mapping 部分

注意在env_alloc中,初始化对应的struct Env字段。

sys_ipc_recv : 进程通过调用这个系统调用,实现接受消息。当进程调用这个系统调用的时候,内核会阻塞这个进程,将env的状态设置位ENV_NOT_RUNNABLE,直到接受到任何进程发送的消息为止,这个进程才能得到调度。注意,对发送消息的进程没有parent 进程的限制,任何进程都能发送消息给任何进程(在JOS中)。注意对参数的检查。如果不接受传递的page mapping,只需要将dstva置为UTOP就可以了,

// Block until a value is ready.  Record that you want to receive
// using the env_ipc_recving and env_ipc_dstva fields of struct Env,
// mark yourself not runnable, and then give up the CPU.
//
// If 'dstva' is < UTOP, then you are willing to receive a page of data.
// 'dstva' is the virtual address at which the sent page should be mapped.
//
// This function only returns on error, but the system call will eventually
// return 0 on success.
// Return < 0 on error.  Errors are:
//  -E_INVAL if dstva < UTOP but dstva is not page-aligned.
static int
sys_ipc_recv(void *dstva)
{
  // LAB 4: Your code here.
  // check dstva
  if ((uintptr_t)dstva < UTOP && (PGOFF(dstva) != 0))
    return -E_INVAL;

  // if dstva >= UTOP , do nothing.

  // Record this env want to receive
  curenv->env_ipc_recving = true;
  curenv->env_ipc_dstva = dstva;
  
  // Block this env, and giveup CPU
  curenv->env_status = ENV_NOT_RUNNABLE;

  return 0;
}

sys_ipc_try_send : 检查参数,同时传递32bit value 和 page mapping。这里有个trick,因为srcva的范围必须在UTOP之下,所以如何判断是否进行page mapping 呢?只要传递srcva >= UTOP 就可以了,只要让系统调用对这种情况不做处理,不抛出-E_INVAL

static int
sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
{
  // LAB 4: Your code here.
  int r;
  struct Env * dstenv;
  pte_t * pte;
  struct PageInfo *pp;
  // Check parameters
  // Check envid and env status
  r = envid2env(envid, &dstenv, 0);
  if (r < 0) 
    return -E_BAD_ENV;
  if (!dstenv->env_ipc_recving)
    return -E_IPC_NOT_RECV;
  
  // Check srcva and perm. notice : if srcva >= UTOP, do nothing.
  if ((uintptr_t)srcva < UTOP) {
    if (PGOFF(srcva) != 0)
      return  -E_INVAL;

    if ((perm & (PTE_U | PTE_P)) != (PTE_U | PTE_P)) 
      return -E_INVAL;

    if (((perm | PTE_SYSCALL) != PTE_SYSCALL))
      return -E_INVAL;

    // Check physical page exist
    pp = page_lookup(curenv->env_pgdir, srcva, &pte);
    if ((uintptr_t)srcva < UTOP && !pp)
      return -E_INVAL;

    // Check perm write conflict
    if ((perm & PTE_W) && !(*pte & PTE_W))
      return -E_INVAL;

    // Send mapping 
    if (dstenv->env_ipc_dstva) {
      // Do page map
      r = page_insert(dstenv->env_pgdir, pp, dstenv->env_ipc_dstva, perm);
      if (r < 0)
        return -E_NO_MEM;
      // Make page perm
      dstenv->env_ipc_perm = perm;
    }
  }

  // If srcva >= UTOP, no mapping transfered and no errors.

  dstenv->env_ipc_recving = false;
  dstenv->env_ipc_value = value;
  dstenv->env_ipc_from = curenv->env_id;
  dstenv->env_status = ENV_RUNNABLE;

  return 0;
}

lib 库对ipc的包装
ipc_recv : 如果不需要page mapping,那么在系统调用的时候,dstva传递UTOP

// Receive a value via IPC and return it.
// If 'pg' is nonnull, then any page sent by the sender will be mapped at
//  that address.
// If 'from_env_store' is nonnull, then store the IPC sender's envid in
//  *from_env_store.
// If 'perm_store' is nonnull, then store the IPC sender's page permission
//  in *perm_store (this is nonzero iff a page was successfully
//  transferred to 'pg').
// If the system call fails, then store 0 in *fromenv and *perm (if
//  they're nonnull) and return the error.
// Otherwise, return the value sent by the sender
//
// Hint:
//   Use 'thisenv' to discover the value and who sent it.
//   If 'pg' is null, pass sys_ipc_recv a value that it will understand
//   as meaning "no page".  (Zero is not the right value, since that's
//   a perfectly valid place to map a page.)
int32_t
ipc_recv(envid_t *from_env_store, void *pg, int *perm_store)
{
  // LAB 4: Your code here.
  int r;
  // Send recv system call
  if (pg)
    r = sys_ipc_recv(pg);
  else 
    r = sys_ipc_recv((void*)UTOP);
  
  if (r < 0) {
    if (from_env_store)
      *from_env_store = 0;
    if (perm_store)
      *perm_store = 0;
    return r;
  }

  // At this time, thisenv is blocked, waiting for an env to send msg to
  // it and change ENV_STATUS. So it can be scheduled.

  if (from_env_store)
    *from_env_store = thisenv->env_ipc_from;
  if (perm_store)
    *perm_store = thisenv->env_ipc_perm;

  return thisenv->env_ipc_value;
}

ipc_send : 如果返回-E_IPC_NOT_RECV,表示接受消息的进程还没有struct Env中的env_ipc_recving,需要继续try。

// Send 'val' (and 'pg' with 'perm', if 'pg' is nonnull) to 'toenv'.
// This function keeps trying until it succeeds.
// It should panic() on any error other than -E_IPC_NOT_RECV.
//
// Hint:
//   Use sys_yield() to be CPU-friendly.
//   If 'pg' is null, pass sys_ipc_recv a value that it will understand
//   as meaning "no page".  (Zero is not the right value.)
void
ipc_send(envid_t to_env, uint32_t val, void *pg, int perm)
{
  // LAB 4: Your code here.
  int r = -E_IPC_NOT_RECV;
  while(r < 0){
    if (r != -E_IPC_NOT_RECV)
      panic("ipc_send : sys_ipc_try_send error : %e.\n",r );
    if (pg)
      r = sys_ipc_try_send(to_env, val, pg, perm);
    else 
      r = sys_ipc_try_send(to_env, val, (void*)UTOP, perm);
  }
}

– EOF –