环境
ubuntu 18.04 64位,virtualbox 虚拟机
实验地址:CPU alarm
正文
本次实验我认为需要认真看一下xv6 book trap那一节,本次实验要为xv6添加一个新的特性,使它能够定期的提示一个进程它所有的CPU时间。这对一些进程来说非常有用,比如说compute-bound进程来限制他们CPU使用时间,或者那些需要计算但是又需要在固定的时间间隔执行一些事件(比如说每10s输出内容)。
你需要增加一个系统调用alarm(interval,handler)
,如果一个程序调用了alarm(n,fn)
,其中n表示进程所消耗的n ticks个cpu 时间,然后内核会调用fn。当fn执行结束的时候,又会返回之前代码正在执行的地方。可能这段话比较难理解,看接下来的代码就理解了。
什么是compute-bound 和 IO-hound:点这里
下面是一个alarmtest进程,它调用了alarm(10, periodic)
,也就是说当每次cpu执行了10 ticks,kernel就回去调用periodic函数 ,输出一个alarm!。因为这个程序主要的时间都是在执行那个for循环,这个循环会输出一个点再屏幕上,然后当这个程序使用了10个tick的时候,就会去输出alarm!,然后程序继续返回for循环,直到循环结束。
#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 < 25*500000; i++){
if((i % 250000) == 0)
write(2, ".", 1);
}
exit();
}
void
periodic()
{
printf(1, "alarm!\n");
}
所以,alarmtest的输出结果就是以下形式,不过会因为CPU的性能不同,可能在10ticks当中for循环的指令就可以执行很多次,所以实际的输出结果有点不同。
$ alarmtest
alarmtest starting
.....alarm!
....alarm!
.....alarm!
......alarm!
.....alarm!
....alarm!
....alarm!
......alarm!
.....alarm!
...alarm!
...$
本次实验首先要做的就是新增加一个系统调用,这个应该很熟悉,在前面增加date这个系统调用的时候,已经做过了。下面是几个官网给出的提示,先稍微翻一下。
- 你需要修改Makefile文件,将alarmtest.c也编译到xv6的用户程序去
- 在user.h中声明一下:
int alarm(int ticks, void (*handler)());
那个handler表示无返回值且无参数的函数指针。
- 修改一下syscall.h 和usys.S是的alarmtest来调用alarm 这个system call.
- 你的sys_alarm()应该将alarm interval和指向handler的指针,存放在proc structure(proc.h)
- 下面是sys_alarm()的代码:
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;
}
当我们调用alarm(n,fn)这个系统调用的时候,两个参数分别在栈当中。第一个参数是tick,第二个参数指向handler的指针。进入sys_alarm()首先先对参数检查,然后再设置alarmticks和alarmhandler。所以我们很自然的需要在proc struture添加这个两个成员。
别忘了,还需要再syscall.c中的数组中添加sys_alarm。
- 你需要追踪自从上次调用handler之后已经过了多少ticks。所以需要在porc sturct当中加入一个成员来记录这个。
- 你只要在有进程运行的时候以及时间中断来时用户空间的时候使用相应alarm(),所以需要下面的条件:
if(myproc() != 0 && (tf->cs & 3) == 3)
8.在你的IRQ_TIMER的代码中,当进程的ticks超过alarmticks,就去执行alarm handler,但是这个要做怎么做呢?
- 你还需要做的事,当handler返回的时候,程序会继续执行它执行的地方,这个又怎么做呢?
- 你可以看下alarmtest.asm代码
实验:
下面的代码我有一些地方是没有理解的,希望有知道的铁子告诉一下。先上代码再解释吧,第一段是先增加一个新的system call,和之前类似应该不是怎么难,我将修改了的代码都放在一起。
// 新创建一个文件alarmtest.c
#include "types.h"
#include "stat.h"
#include "user.h"
void periodic();
int
main(int argc, char *argv[])
{
int i;
printf(1, "alarmtest starting\n");
//当alarmtest这个进程运行超过10ticks,就回去调用perodic函数
alarm(10, periodic);
for(i = 0; i < 25*500000; i++){
if((i % 250000) == 0)
write(2, ".", 1);
}
exit();
}
void
periodic()
{
printf(1, "alarm!\n");
}
//syscall.c中引用一下新增加的system call,并且还要放到数组当中
extern int sys_alarm(void);
static int (*syscalls[])(void) = {
....
[SYS_alarm] sys_alarm
}
//usys.S
SYSCALL(alarm)
//sysproc.c
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;
}
还有一个就是要在user.h中增加一个函数声明,还要在porc.h中增加新的成员,alarmticks,(*alarmhandler)(),ticks。
//user.h
int alarm(int ticks, void (*handler)());
//proc.h
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)
uint alarmticks;
void (*alarmhandler)();
uint ticks;
};
到这里位置创建好了一个新的系统调用。用struct proc中的ticks来记录当前进程消耗了多少ticks,当进程的ticks超过了alarmticks就去调用alarmhandler,也就是我们在alarmtest.c中调用alarm(10,periodic)所设置的.接下来就是要完成本次实验:
case T_IRQ0 + IRQ_TIMER:
if (myproc() != 0 && (tf->cs & 3) == 3) //只处理来自用户进程
{
myproc()->ticks++; // 发生中断的时候如果是运行user process,ticks ++
if (myproc()->ticks == myproc()->alarmticks) // 超过了alarmticks
{
myproc()->ticks = 0; //重新计数ticks
//myproc()->alarmhandler(); // 不知道为什么不可以直接使用函数指针
tf->esp -= 4;
*(uint *)(tf->esp) = tf->eip;
tf->eip = (uint)myproc()->alarmhandler;
}
}
//下面的代码是xv6原来的时钟中断处理函数,我原封不动的复制过来
if (cpuid() == 0)
{
acquire(&tickslock);
ticks++;
wakeup(&ticks);
release(&tickslock);
}
lapiceoi();
break;
我最开始想直接使用myproc()->alarmhandler()
来调用,不知道为什没有用。上面的代码的部分是从别人那边抄的原文在这里。我来解释一下比较关键的几句(这些内容需要阅读一下xv6 book Trap那一节)。当中发生后且此时正在运行的程序是alarmtest,转入到内核去执行中断处理函数,tf(trapframe)中因为发生了特权级的转换,所以trapframe中压入了alarmtest的esp和ss。当中断处理函数执行结束后将trapframe中的内容弹出到对应的寄存器,恢复到之前在执行的进程,这就是一个最基本的中断响应与恢复。
tf->esp -= 4;
*(uint *)(tf->esp) = tf->eip;
tf->eip = (uint)myproc()->alarmhandler;
tf中的esp是原进程的esp,第一句代码的意思就是空出一个4字节的内容,然后原进程的eip放入到用户栈里面去,然后把trapframe的eip设置为alarmhandler。当中断处理函数结束,返回到trapasm.S,然后trapasm.S中下面的代码:
![Screenshot from 2020-08-04 19-36-08.png](https://upload-images.jianshu.io/upload_images/10683218-aee112fd3de84647.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
.globl trapret
trapret:
popal
popl %gs
popl %fs
popl %es
popl %ds
addl $0x8, %esp # trapno and errcode
iret
iret本来的话,应该是跳转到原来的进程去执行,但是我们此时对tf->eip重新设置了。所以iret就跳转到了alarmhandler去执行,也就是periodic()。此时应该会发生从高特权级到特权级的转换(中断处理函数的特权级应该是0,然后跳转到用户程序去了)。所以又发生了栈的切换,切换到了用户栈。来看一下periodic的汇编代码:
此时在用户栈,而且我们又用
*(uint *)(tf->esp) = tf->eip;
将eip写入到了栈当中,所以ret语句就会跳转到之前的用户程序去执行,就是我们最开始的那个for循环。
下面是我没有理解的问题,毕竟这个代码代码是抄的,没有理解所有内容:
- myproc()->alarmhandler()为什么不能直接调用函数?
- trapret后,跳转到alarmhandler()去执行。因为在trapret当中,我们恢复了所有的寄存器内容,如果在alarmhandler()中修改了一些寄存器的内容,会不会导致原进程(也就是前面的那个for循环)无法正常运行?
看起来本次实验比较简单,但是做起来还是有点不容易的。下面是我的实验结果截图:
可能是因为CPU速度相对比较快,一个tick可以循环的次数更多。不过看结果应该是实现了题目要求。