環(huán)境
ubuntu 18.04 64位,virtualbox 虛擬機(jī)
實(shí)驗(yàn)地址:CPU alarm
正文
本次實(shí)驗(yàn)我認(rèn)為需要認(rèn)真看一下xv6 book trap那一節(jié)析恋,本次實(shí)驗(yàn)要為xv6添加一個(gè)新的特性段直,使它能夠定期的提示一個(gè)進(jìn)程它所有的CPU時(shí)間寺庄。這對一些進(jìn)程來說非常有用越驻,比如說compute-bound進(jìn)程來限制他們CPU使用時(shí)間闯传,或者那些需要計(jì)算但是又需要在固定的時(shí)間間隔執(zhí)行一些事件(比如說每10s輸出內(nèi)容)惕橙。
你需要增加一個(gè)系統(tǒng)調(diào)用alarm(interval,handler)
良蛮,如果一個(gè)程序調(diào)用了alarm(n,fn)
融蹂,其中n表示進(jìn)程所消耗的n ticks個(gè)cpu 時(shí)間帮寻,然后內(nèi)核會調(diào)用fn乍狐。當(dāng)fn執(zhí)行結(jié)束的時(shí)候,又會返回之前代碼正在執(zhí)行的地方固逗∏瞅剑可能這段話比較難理解,看接下來的代碼就理解了烫罩。
什么是compute-bound 和 IO-hound:點(diǎn)這里
下面是一個(gè)alarmtest進(jìn)程惜傲,它調(diào)用了alarm(10, periodic)
,也就是說當(dāng)每次cpu執(zhí)行了10 ticks,kernel就回去調(diào)用periodic函數(shù) ,輸出一個(gè)alarm贝攒!盗誊。因?yàn)檫@個(gè)程序主要的時(shí)間都是在執(zhí)行那個(gè)for循環(huán),這個(gè)循環(huán)會輸出一個(gè)點(diǎn)再屏幕上,然后當(dāng)這個(gè)程序使用了10個(gè)tick的時(shí)候,就會去輸出alarm!哈踱,然后程序繼續(xù)返回for循環(huán)荒适,直到循環(huán)結(jié)束。
#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的輸出結(jié)果就是以下形式,不過會因?yàn)镃PU的性能不同吻贿,可能在10ticks當(dāng)中for循環(huán)的指令就可以執(zhí)行很多次,所以實(shí)際的輸出結(jié)果有點(diǎn)不同哑子。
$ alarmtest
alarmtest starting
.....alarm!
....alarm!
.....alarm!
......alarm!
.....alarm!
....alarm!
....alarm!
......alarm!
.....alarm!
...alarm!
...$
本次實(shí)驗(yàn)首先要做的就是新增加一個(gè)系統(tǒng)調(diào)用,這個(gè)應(yīng)該很熟悉肌割,在前面增加date這個(gè)系統(tǒng)調(diào)用的時(shí)候卧蜓,已經(jīng)做過了。下面是幾個(gè)官網(wǎng)給出的提示把敞,先稍微翻一下弥奸。
- 你需要修改Makefile文件,將alarmtest.c也編譯到xv6的用戶程序去
- 在user.h中聲明一下:
int alarm(int ticks, void (*handler)());
那個(gè)handler表示無返回值且無參數(shù)的函數(shù)指針奋早。
- 修改一下syscall.h 和usys.S是的alarmtest來調(diào)用alarm 這個(gè)system call.
- 你的sys_alarm()應(yīng)該將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;
}
當(dāng)我們調(diào)用alarm(n,fn)這個(gè)系統(tǒng)調(diào)用的時(shí)候,兩個(gè)參數(shù)分別在棧當(dāng)中。第一個(gè)參數(shù)是tick,第二個(gè)參數(shù)指向handler的指針耽装。進(jìn)入sys_alarm()首先先對參數(shù)檢查愤炸,然后再設(shè)置alarmticks和alarmhandler。所以我們很自然的需要在proc struture添加這個(gè)兩個(gè)成員掉奄。
別忘了规个,還需要再syscall.c中的數(shù)組中添加sys_alarm。
- 你需要追蹤自從上次調(diào)用handler之后已經(jīng)過了多少ticks姓建。所以需要在porc sturct當(dāng)中加入一個(gè)成員來記錄這個(gè)诞仓。
- 你只要在有進(jìn)程運(yùn)行的時(shí)候以及時(shí)間中斷來時(shí)用戶空間的時(shí)候使用相應(yīng)alarm(),所以需要下面的條件:
if(myproc() != 0 && (tf->cs & 3) == 3)
8.在你的IRQ_TIMER的代碼中速兔,當(dāng)進(jìn)程的ticks超過alarmticks墅拭,就去執(zhí)行alarm handler,但是這個(gè)要做怎么做呢涣狗?
- 你還需要做的事谍婉,當(dāng)handler返回的時(shí)候,程序會繼續(xù)執(zhí)行它執(zhí)行的地方屑柔,這個(gè)又怎么做呢屡萤?
- 你可以看下alarmtest.asm代碼
實(shí)驗(yàn):
下面的代碼我有一些地方是沒有理解的,希望有知道的鐵子告訴一下掸宛。先上代碼再解釋吧,第一段是先增加一個(gè)新的system call,和之前類似應(yīng)該不是怎么難死陆,我將修改了的代碼都放在一起。
// 新創(chuàng)建一個(gè)文件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");
//當(dāng)alarmtest這個(gè)進(jìn)程運(yùn)行超過10ticks,就回去調(diào)用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ù)組當(dāng)中
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;
}
還有一個(gè)就是要在user.h中增加一個(gè)函數(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)建好了一個(gè)新的系統(tǒng)調(diào)用领虹。用struct proc中的ticks來記錄當(dāng)前進(jìn)程消耗了多少ticks,當(dāng)進(jìn)程的ticks超過了alarmticks就去調(diào)用alarmhandler规哪,也就是我們在alarmtest.c中調(diào)用alarm(10,periodic)所設(shè)置的.接下來就是要完成本次實(shí)驗(yàn):
case T_IRQ0 + IRQ_TIMER:
if (myproc() != 0 && (tf->cs & 3) == 3) //只處理來自用戶進(jìn)程
{
myproc()->ticks++; // 發(fā)生中斷的時(shí)候如果是運(yùn)行user process,ticks ++
if (myproc()->ticks == myproc()->alarmticks) // 超過了alarmticks
{
myproc()->ticks = 0; //重新計(jì)數(shù)ticks
//myproc()->alarmhandler(); // 不知道為什么不可以直接使用函數(shù)指針
tf->esp -= 4;
*(uint *)(tf->esp) = tf->eip;
tf->eip = (uint)myproc()->alarmhandler;
}
}
//下面的代碼是xv6原來的時(shí)鐘中斷處理函數(shù),我原封不動的復(fù)制過來
if (cpuid() == 0)
{
acquire(&tickslock);
ticks++;
wakeup(&ticks);
release(&tickslock);
}
lapiceoi();
break;
我最開始想直接使用myproc()->alarmhandler()
來調(diào)用塌衰,不知道為什沒有用诉稍。上面的代碼的部分是從別人那邊抄的原文在這里。我來解釋一下比較關(guān)鍵的幾句(這些內(nèi)容需要閱讀一下xv6 book Trap那一節(jié))最疆。當(dāng)中發(fā)生后且此時(shí)正在運(yùn)行的程序是alarmtest杯巨,轉(zhuǎn)入到內(nèi)核去執(zhí)行中斷處理函數(shù),tf(trapframe)中因?yàn)榘l(fā)生了特權(quán)級的轉(zhuǎn)換努酸,所以trapframe中壓入了alarmtest的esp和ss服爷。當(dāng)中斷處理函數(shù)執(zhí)行結(jié)束后將trapframe中的內(nèi)容彈出到對應(yīng)的寄存器,恢復(fù)到之前在執(zhí)行的進(jìn)程获诈,這就是一個(gè)最基本的中斷響應(yīng)與恢復(fù)仍源。
tf->esp -= 4;
*(uint *)(tf->esp) = tf->eip;
tf->eip = (uint)myproc()->alarmhandler;
tf中的esp是原進(jìn)程的esp,第一句代碼的意思就是空出一個(gè)4字節(jié)的內(nèi)容,然后原進(jìn)程的eip放入到用戶棧里面去,然后把trapframe的eip設(shè)置為alarmhandler舔涎。當(dāng)中斷處理函數(shù)結(jié)束笼踩,返回到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本來的話,應(yīng)該是跳轉(zhuǎn)到原來的進(jìn)程去執(zhí)行终抽,但是我們此時(shí)對tf->eip重新設(shè)置了戳表。所以iret就跳轉(zhuǎn)到了alarmhandler去執(zhí)行,也就是periodic()昼伴。此時(shí)應(yīng)該會發(fā)生從高特權(quán)級到特權(quán)級的轉(zhuǎn)換(中斷處理函數(shù)的特權(quán)級應(yīng)該是0匾旭,然后跳轉(zhuǎn)到用戶程序去了)。所以又發(fā)生了棧的切換圃郊,切換到了用戶棧价涝。來看一下periodic的匯編代碼:
此時(shí)在用戶棧,而且我們又用
*(uint *)(tf->esp) = tf->eip;
將eip寫入到了棧當(dāng)中持舆,所以ret語句就會跳轉(zhuǎn)到之前的用戶程序去執(zhí)行色瘩,就是我們最開始的那個(gè)for循環(huán)。下面是我沒有理解的問題,畢竟這個(gè)代碼代碼是抄的逸寓,沒有理解所有內(nèi)容:
- myproc()->alarmhandler()為什么不能直接調(diào)用函數(shù)居兆?
- trapret后,跳轉(zhuǎn)到alarmhandler()去執(zhí)行竹伸。因?yàn)樵趖rapret當(dāng)中泥栖,我們恢復(fù)了所有的寄存器內(nèi)容簇宽,如果在alarmhandler()中修改了一些寄存器的內(nèi)容,會不會導(dǎo)致原進(jìn)程(也就是前面的那個(gè)for循環(huán))無法正常運(yùn)行?
看起來本次實(shí)驗(yàn)比較簡單吧享,但是做起來還是有點(diǎn)不容易的魏割。下面是我的實(shí)驗(yàn)結(jié)果截圖:
可能是因?yàn)镃PU速度相對比較快,一個(gè)tick可以循環(huán)的次數(shù)更多钢颂。不過看結(jié)果應(yīng)該是實(shí)現(xiàn)了題目要求钞它。