環(huán)境
ubuntu 18.04 64位,virtualbox 虛擬機
實驗地址:CPU alarm
正文
本次實驗我認為需要認真看一下xv6 book trap那一節(jié),本次實驗要為xv6添加一個新的特性,使它能夠定期的提示一個進程它所有的CPU時間。這對一些進程來說非常有用,比如說compute-bound進程來限制他們CPU使用時間,或者那些需要計算但是又需要在固定的時間間隔執(zhí)行一些事件(比如說每10s輸出內容)。
你需要增加一個系統(tǒng)調用alarm(interval,handler),如果一個程序調用了alarm(n,fn),其中n表示進程所消耗的n ticks個cpu 時間,然后內核會調用fn。當fn執(zhí)行結束的時候,又會返回之前代碼正在執(zhí)行的地方??赡苓@段話比較難理解,看接下來的代碼就理解了。
什么是compute-bound 和 IO-hound:點這里
下面是一個alarmtest進程,它調用了alarm(10, periodic),也就是說當每次cpu執(zhí)行了10 ticks,kernel就回去調用periodic函數(shù) ,輸出一個alarm!。因為這個程序主要的時間都是在執(zhí)行那個for循環(huán),這個循環(huán)會輸出一個點再屏幕上,然后當這個程序使用了10個tick的時候,就會去輸出alarm!,然后程序繼續(xù)返回for循環(huán),直到循環(huán)結束。
#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循環(huán)的指令就可以執(zhí)行很多次,所以實際的輸出結果有點不同。
$ alarmtest
alarmtest starting
.....alarm!
....alarm!
.....alarm!
......alarm!
.....alarm!
....alarm!
....alarm!
......alarm!
.....alarm!
...alarm!
...$
本次實驗首先要做的就是新增加一個系統(tǒng)調用,這個應該很熟悉,在前面增加date這個系統(tǒng)調用的時候,已經(jīng)做過了。下面是幾個官網(wǎng)給出的提示,先稍微翻一下。
- 你需要修改Makefile文件,將alarmtest.c也編譯到xv6的用戶程序去
- 在user.h中聲明一下:
int alarm(int ticks, void (*handler)());
那個handler表示無返回值且無參數(shù)的函數(shù)指針。
- 修改一下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)這個系統(tǒng)調用的時候,兩個參數(shù)分別在棧當中。第一個參數(shù)是tick,第二個參數(shù)指向handler的指針。進入sys_alarm()首先先對參數(shù)檢查,然后再設置alarmticks和alarmhandler。所以我們很自然的需要在proc struture添加這個兩個成員。
別忘了,還需要再syscall.c中的數(shù)組中添加sys_alarm。
- 你需要追蹤自從上次調用handler之后已經(jīng)過了多少ticks。所以需要在porc sturct當中加入一個成員來記錄這個。
- 你只要在有進程運行的時候以及時間中斷來時用戶空間的時候使用相應alarm(),所以需要下面的條件:
if(myproc() != 0 && (tf->cs & 3) == 3)
8.在你的IRQ_TIMER的代碼中,當進程的ticks超過alarmticks,就去執(zhí)行alarm handler,但是這個要做怎么做呢?
- 你還需要做的事,當handler返回的時候,程序會繼續(xù)執(zhí)行它執(zhí)行的地方,這個又怎么做呢?
- 你可以看下alarmtest.asm代碼
實驗:
下面的代碼我有一些地方是沒有理解的,希望有知道的鐵子告訴一下。先上代碼再解釋吧,第一段是先增加一個新的system call,和之前類似應該不是怎么難,我將修改了的代碼都放在一起。
// 新創(chuàng)建一個文件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函數(shù)
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,并且還要放到數(shù)組當中
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中增加一個函數(shù)聲明,還要在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;
};
到這里位置創(chuàng)建好了一個新的系統(tǒng)調用。用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++; // 發(fā)生中斷的時候如果是運行user process,ticks ++
if (myproc()->ticks == myproc()->alarmticks) // 超過了alarmticks
{
myproc()->ticks = 0; //重新計數(shù)ticks
//myproc()->alarmhandler(); // 不知道為什么不可以直接使用函數(shù)指針
tf->esp -= 4;
*(uint *)(tf->esp) = tf->eip;
tf->eip = (uint)myproc()->alarmhandler;
}
}
//下面的代碼是xv6原來的時鐘中斷處理函數(shù),我原封不動的復制過來
if (cpuid() == 0)
{
acquire(&tickslock);
ticks++;
wakeup(&ticks);
release(&tickslock);
}
lapiceoi();
break;
我最開始想直接使用myproc()->alarmhandler()來調用,不知道為什沒有用。上面的代碼的部分是從別人那邊抄的原文在這里。我來解釋一下比較關鍵的幾句(這些內容需要閱讀一下xv6 book Trap那一節(jié))。當中發(fā)生后且此時正在運行的程序是alarmtest,轉入到內核去執(zhí)行中斷處理函數(shù),tf(trapframe)中因為發(fā)生了特權級的轉換,所以trapframe中壓入了alarmtest的esp和ss。當中斷處理函數(shù)執(zhí)行結束后將trapframe中的內容彈出到對應的寄存器,恢復到之前在執(zhí)行的進程,這就是一個最基本的中斷響應與恢復。
tf->esp -= 4;
*(uint *)(tf->esp) = tf->eip;
tf->eip = (uint)myproc()->alarmhandler;
tf中的esp是原進程的esp,第一句代碼的意思就是空出一個4字節(jié)的內容,然后原進程的eip放入到用戶棧里面去,然后把trapframe的eip設置為alarmhandler。當中斷處理函數(shù)結束,返回到trapasm.S,然后trapasm.S中下面的代碼:

.globl trapret
trapret:
popal
popl %gs
popl %fs
popl %es
popl %ds
addl $0x8, %esp # trapno and errcode
iret
iret本來的話,應該是跳轉到原來的進程去執(zhí)行,但是我們此時對tf->eip重新設置了。所以iret就跳轉到了alarmhandler去執(zhí)行,也就是periodic()。此時應該會發(fā)生從高特權級到特權級的轉換(中斷處理函數(shù)的特權級應該是0,然后跳轉到用戶程序去了)。所以又發(fā)生了棧的切換,切換到了用戶棧。來看一下periodic的匯編代碼:

此時在用戶棧,而且我們又用
*(uint *)(tf->esp) = tf->eip;將eip寫入到了棧當中,所以ret語句就會跳轉到之前的用戶程序去執(zhí)行,就是我們最開始的那個for循環(huán)。下面是我沒有理解的問題,畢竟這個代碼代碼是抄的,沒有理解所有內容:
- myproc()->alarmhandler()為什么不能直接調用函數(shù)?
- trapret后,跳轉到alarmhandler()去執(zhí)行。因為在trapret當中,我們恢復了所有的寄存器內容,如果在alarmhandler()中修改了一些寄存器的內容,會不會導致原進程(也就是前面的那個for循環(huán))無法正常運行?
看起來本次實驗比較簡單,但是做起來還是有點不容易的。下面是我的實驗結果截圖:
可能是因為CPU速度相對比較快,一個tick可以循環(huán)的次數(shù)更多。不過看結果應該是實現(xiàn)了題目要求。
