xv6_cpu_alarm

系统调用的小实践

问题重述

  本实验要求我们为 xv6 增加一个系统调用 alarm(ticks, handler),同样是通过中断处理实现其逻辑。每当程序使用了 ticks 个时间片(tick),alarm 就会调用 handler 指向的函数来提醒程序。问题的关键点在于如何在中断处理中调用这个用户态函数。   我们已经知道函数调用的机理,即把函数参数依次压栈,再将下一条指令 eip 压栈,然后跳转到函数代码处执行。由于处理函数 alarm-handler 没有参数,我们只需要把进程的 eip 压栈并指向 alarm-handler 即可。当 alarm-handler 运行完后,eip 也会恢复,继续执行后续的指令,这样就实现了手动把该函数注入到进程的上下文中。以下为本实验的具体实现流程。

系统调用的添加

  系统调用的添加过程比较简单,执行下述几个固定步骤即可:1) syscall.h 定义系统调用号:

1
#define SYS_alarm 23

  2) user.h 声明系统调用函数对应的内核函数

1
int alarm(int ticks, void(*handler)());

  3) syscall.c 定义系统调用号和内核函数的对应关系并声明内核函数

1
2
3
4
5
6
7
8
9
extern int sys_alarm(void);
static int (*syscalls[])(void) = {
  ......
[SYS_alarm]   sys_alarm,
};
static char syscalls_names[][6] = {
  ......
[SYS_alarm]	  "alarm",
};

  4) sysproc.c 利用参数解析封装系统调用函数(已经给出)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int
sys_alarm(void)
{
  int ticks;
  void (*handler)();

  if(argint(0, &ticks) < 0)
    return -1;
  if(argptr(1, (char**)&handler, 1) < 0)
    return -1;
  myproc()->alarmticks = ticks;
  myproc()->alarmhandler = handler;
  return 0;
}

  5) usys.S 定义系统调用的过程

1
SYSCALL(alarm)

  6) 增加一个用来测试的用户程序 alarmtest.c(已经给出)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "types.h"
#include "stat.h"
#include "user.h"

void periodic();

int main(int argc, char *argv[])
{
    int i;
    printf(1, "alarmtest starting\n");
    alarm(10, periodic);
    for (i = 0; i < 500 * 500000; i++)
    {
        if ((i % 2000000) == 0)
            write(2, ".", 1);
    }
    exit();
}

void periodic()
{
    printf(1, "alarm!\n");
}

  7) Makefile 添加需要的用户代码信息 UPROGS

1
2
3
UPROGS=\
	......
	_alarmtest\

proc结构体的完善

  注意到 sys_alarm() 里的 myproc()->alarmticks = ticks;myproc()->alarmhandler = handler; 语句,这提示我们系统调用alarm的功能,一是初始化进程消耗的时间片 alarmticks,二是把 handler 函数的地址存入进程的结构体中,因此需要为进程的结构体增加相应字段:

1
2
3
4
5
6
7
8
9
10
// proc.h
// Per-process state
struct proc {
  uint sz;                     // Size of process memory (bytes)
  pde_t* pgdir;                // Page table
  ......
  int alarmticks; // 时间片上限
  int ticks_cnt;  // 时间片消耗数
  void (*alarmhandler)();
};

alarm-handler的实现

  xv6 已经定义好了硬件的时钟触发机制,trap(): case T_IRQ0 + IRQ_TIMER 在每一个 tick 都会触发一次时钟中断;alarm-handler 的逻辑实现比较简单,先判断 ticks 是否消耗完毕,如果消耗完则计数清零,通过 eip 压栈执行 alarm-handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// trap.c
void
trap(struct trapframe *tf)
{
  ......
  switch(tf->trapno){
  case T_IRQ0 + IRQ_TIMER:
    ......
    // 判断是否为用户态
    if(myproc() && (tf->cs & 3) == 3 && myproc()->alarmhandler && myproc()->killed != 1){
      myproc()->ticks_cnt++;
      // ticks消耗完毕
      if(myproc()->alarmticks == myproc()->ticks_cnt){
        myproc()->ticks_cnt = 0;
        // eip压栈
        tf->esp -= 4;    
        *((uint *)(tf->esp)) = tf->eip;
        // eip指向alarm-handler
        tf->eip =(uint) myproc()->alarmhandler;
      }
    }
  ......

  启动 xv6 键入 alarmtest 得到如下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ alarmtest
alarmtest starting
............alarm!
............alarm!
.............alarm!
................alarm!
...........alarm!
............alarm!
..............alarm!
............alarm!
...........alarm!
............alarm!
$