笔记013 - HW5: xv6 CPU alarm

任务

添加一个alarm(interval, handler)系统调用,表示每过interval ticks个CPU time,内核将会调用handler函数。之后,程序将会在调用alarm系统调用的地方恢复执行。
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
24
#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 < 50*500000; i++){
if((i++ % 500000) == 0)
write(2, ".", 1);
}
exit();
}

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

alarm对应的系统调用示例:

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;
proc->alarmticks = ticks;
proc->alarmhandler = handler;
return 0;
}

执行结果类似于:

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!
...$

思路

参考其他系统调用如uptime的实现,查看alarm系统调用需要声明的位置:

alarmtest处理为可执行用户程序,在Makefile中添加:

注意点:
1、proc->alarmticks记录用户程序指定的CPU time间隔,proc->alarmhandler记录内核将会调用的函数入口地址。此外还需要proc->alarmticked记录是否达到指定的CPU time间隔个数。这三个添加到proc中,见proc.h。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Per-process state
struct proc {
uint sz; // Size of process memory (bytes)
pde_t* pgdir; // Page table
char *kstack; // Bottom of kernel stack for this process
enum procstate state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
struct trapframe *tf; // Trap frame for current syscall
struct context *context; // swtch() here to run process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)

//add by jianzzz, for alarm...
int alarmticks;
int alarmticked;
void (*alarmhandler)();
};

在allocproc中初始化,见proc.h。

1
2
3
4
5
6
7
8
9
10
11
12
13
//在进程表中寻找slot,成功的话更改进程状态embryo和pid,并初始化进程的内核栈
static struct proc*
allocproc(void)
{
...
found:
...
//add by jianzzz,for alarm
p->alarmticks = 0;
p->alarmticked = 0;
p->alarmhandler = 0;
return p;
}

2、每过一个tick的CPU time,硬件clock将会触发一个中断,并在trap()的case T_IRQ0 + IRQ_TIMER下被处理。同时,需要注意我们只在有用户程序被执行并且alarm中断来自用户态的情况下才对alarm中断进行处理。
3、需要保证当alarm中断函数执行完之后,成功返回并恢复执行用户程序。
4、可以通过make CPUS=1 qemu通知qemu只使用一个CPU。

实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//see in trap.c
void
trap(struct trapframe *tf)
{
...
switch(tf->trapno){
case T_IRQ0 + IRQ_TIMER:
if(cpunum() == 0){
acquire(&tickslock);
ticks++;
wakeup(&ticks);
release(&tickslock);
}
lapiceoi();

if(proc && (tf->cs & 3) == 3){
//(*proc->alarmhandler)();
proc->alarmticked += 1;
if(proc->alarmticked == proc->alarmticks) {
proc->alarmticked = 0;
tf->esp -= 4;
*(uint *) tf->esp = tf->eip;
tf->eip = (uint) proc->alarmhandler;
}
}

break;
...
}

补充说明

1、如果直接执行在case T_IRQ0 + IRQ_TIMER执行(*proc->alarmhandler)();会怎样?
结果应该是可以调用到handler但是结果是错误的。可以调用到handler是因为:xv6将内核空间映射到了用户进程的页目录中(见vm.c setupkvm()),而在用户进程调用系统调用并由用户态切换到内核态并执行系统调用的时候,一直使用的是用户进程的页目录(需要确定执行用户进程过程中发生中断是否也一直使用用户进程的页目录!),因此中断发生的时候,alarm(interval, handler)指定的用户函数handler对内核是可见的。调用结果是错误的是因为:由alarmtest.c可以看出用户函数handler会调用printf函数,printf函数会调用write系统调用。在内核态下处理中断时,使用的是进程的内核栈,内核调用write是不会切换栈的,此刻write的参数放到内核栈中而不是用户栈中;但是write的系统函数sys_write会从tf->esp所指向的用户栈来获取参数,从而无法访问正确的栈。
换句话说,handler应该是在用户态而不是在内核态下被执行。

2、如何确保能成功执行handler,并返回用户程序?
发生中断时,触发中断的指令的下一条指令位置会被记录,对应于tf->eip。tf->esp指向用户栈空间。因此,将tf->esp指向的用户栈位置向低位移出一个空位,然后存储触发中断的指令的下一条指令位置,并将tf->eip指向handler入口。这样子会中断返回之后将根据tf->eip在用户态下执行handler,handler函数执行完毕后自动调用ret,从用户栈弹出值作为eip,该值就是触发中断的指令的下一条指令位置,从而返回用户程序。

显示 Gitment 评论