組長:a126845、盧贊名
組員:o100442、徐煒員
組員:a126546、謝恩平
github: https://github.com/wei-yuan/xv6-public
(Master)
Perf是一套仿造Linux內建的系統效能分析工具,能夠紀錄程序執行時的效能數據,讓開發者可以檢視程式的執行狀況,用以做為改善的依據。
Linux Perf提供相當豐富的各項指標與紀錄,但有不少功能是需要硬體支援,在這次的作業中我們選擇優先實作下列幾項指令,主要是stat與sched以及協助提示用的help、list。
perf stat能紀錄執行指令的各項指標,如Context switch的次數、CPU執行時間、等待時間和Page fault的次數等等,例如perf stat cat README
可以看到印出README所需要的執行時間、CPU消耗的時間以及其他指標。
操作流程
1.輸入perf stat [CMD] [ARGS]
2.待程式執行完畢後,會印出相關記錄數據
xv6 os 中目前沒有實作與 kernel tick 對應的實際秒數,因此下列所指的時間都是指 kernel tick 數目。
perf sched 能夠印出所有 process 的總執行時間,以及在 Round Robin 排程演算法中, process 使用 Time slot 的時間,還有 process 等待執行的時間等資訊。
操作流程
1.在command shell中輸入 perf sched
2.等程式執行完畢後,就會印出關於 process 的各項時間資訊
其中 Cmd 為欲測量的 command (此例為 ls, 參數忽略不用)
//Perf 測量紀錄(User space)
struct perfdata {
uint totalticks; //總耗費時間(ticks)
uint cputicks; //CPU耗費時間(ticks)
int conswch; //Context switch數量
int pgfault; //Page fault數量
int cpuswch; //CPU migration數量
};
//Perf 測量指令(User space)
struct perfcmd{
char* test_cmd; //測量process指令
char* cmd; //Internal use.
int arg1; //測量process's pid
};
//Perf 測量紀錄(Kernel space)
struct perf_record{
int recording;
int pid;
int cxtsw;
int pgfault;
int _ticks;
int sumticks;
int startticks;
int endticks;
int cpuid;
int cpusw;
};
//裝填具有成對結構的輸出項目(User space)
struct perfoutput {
char* title;
char* descript;
};
perf 基本用法主要都是去監視某個指令的執行狀態,因此我們希望能夠讓perf當作Proxy,代User去執行各種指令,並在結束後印出報表。
這邊以perf stat
做為例子,首先擷取參數argv[1]
來確定要執行的perf指令(stat or sched),接著fork一個child process,並且呼叫perf_stat()
(system call)開始紀錄該process,最後將剩下的參數套入exec
執行,等到程序執行完畢wait()
被觸發後,再次執行perf_stat()
(system call)取回測量結果並印出。
考量到擴充性,因此執行參數與取回結果皆以指標傳遞,以方便未來增減指令時不需另外修改。
perf.c
int main(int argc, char *argv[]){
//Check arguments
if(argc <= 1){
printf(2,"few arguments\n");
exit();
}
//perf help
if(strcmp(argv[1],"help") == 0){
perf_help();
exit();
}
//perf list
if(strcmp(argv[1],"list") == 0){
perf_list();
exit();
}
/*Memory alloc*/
struct perfcmd *cmd ;
struct perfdata *ed ;
cmd = malloc(sizeof(*cmd));
memset(cmd, 0, sizeof(*cmd));
...
//perf stat [CMD]
if(strcmp(argv[1],"stat") == 0){
//Check arguments
if(argc < 3){
printf(2,"few arguments\n");
exit();
}
/* Fork a child process to execute user command*/
int pid;
pid = fork();
if(pid == 0){
cmd->cmd = "start";
cmd->arg1 = getpid();
//System call here, start meauring.
perf_stat(cmd,0);
//Invoke the command we want measured.
exec(argv[2], &argv[2]);
}
//Wait until child process to finish.
wait();
cmd->test_cmd = argv[2];
cmd->arg1 = pid;
cmd->cmd = "end";
//System call here, rerival perf measuring data
perf_stat(cmd,ed);
//Print out measuring data
printf(1, "\nPerformance counter stats for '%s':\n\n", cmd->test_cmd);
...
exit();
}
perf stat的開始紀錄和取回結果是共用同一隻system call,首次呼叫時傳遞測量的相關參數,在測量結束後再提供空間讓kernel寫回結果。
syscall.h
#define SYS_perf_stat 24
syscall.c
extern int sys_perf_stat(void);
...
static int (*syscalls[])(void) = {
...
[SYS_perf_stat] sys_perf_stat,
};
usys.S
SYSCALL(perf_stat)
主要修改都在這支程式,首先是perf_table
,功能是提供kernel去紀錄各項指標的變數,先宣告陣列,大小是同時間能存在的最大process數量NPROC
,未來可以動態配置以減少空間使用。
struct {
struct spinlock lock;
struct perf_record perfs[NPROC];
} perf_table;
接著再pinit()
中呼叫initperf
進行陣列與lock的初始化。
void
initperf(void)
{
initlock(&perf_table.lock,"perf");
memset(perf_table.perfs, 0, sizeof(perf_table.perfs));
}
初始化到此結束,接下來就等使用者執行perf。
當程序第一次呼叫perf_stat時會執行sys_perf_stat()
(system call),通過user space傳入的pid(cmd->arg1
)調用startperf()
。
int
sys_perf_stat(void)
{
struct perfcmd* cmd;
struct perfdata* data;
if(argptr(0, (void*)&cmd, sizeof(*cmd)) < 0 || argptr(1, (void*)&data, sizeof(*data)) < 0) {
return -1;
}
if(cmd->arg1<0){
return -1;
}
if(strcompare(cmd->cmd,"start") == 0){
startperf(cmd->arg1);
return 0;
}
if(strcompare(cmd->cmd,"end") == 0){
stopperf(cmd->arg1,data);
return 0;
}
return -1;
}
startperf()
首先取得lock
並從perf_table
中找到第一個沒在使用的空間(recording==0
),並將其初始化後設定recording = 1
,釋放lock
完成alloc。
struct perf_record* startperf(int pid){
struct perf_record* i;
acquire(&perf_table.lock);
for( i = perf_table.perfs; i < &perf_table.perfs[NPROC]; i++){
if(i->recording)
continue;
//found and init;
i->pid = pid;
i->cxtsw = 0;
i->pgfault = 0;
i->sumticks = 0;
i->startticks = ticks;
i->endticks = 0;
i->cpuid = -1;
i->cpusw = 0;
i->_ticks = ticks;
i->recording = 1;
release(&perf_table.lock);
return i;
}
release(&perf_table.lock);
return 0;
}
中間的紀錄過程稍後再提,先看當使用者第二次執行sys_perf_stat()
(system call)時會附上pid與紀錄用的空間變數指標perfdata
,sys_perf_stat()
會調用stopperf()
,stopperf()
會取得lock
並將資料寫回perfdata
,最後清除旗標recording
與釋放lock
。
int stopperf(int pid, struct perfdata *data){
struct perf_record* res = getperf(pid);
acquire(&perf_table.lock);
if(res){
data->conswch = res->cxtsw;
//Page fault not working...
data->pgfault = res->pgfault;
data->totalticks = res->endticks - res->startticks;
data->cputicks = res->sumticks;
data->cpuswch = res->cpusw;
res->recording = 0;
release(&perf_table.lock);
return 0;
}
release(&perf_table.lock);
return -1;
}
特別一提的是,因為在紀錄的過程中是每個pid對應自己的紀錄,因此實際上perf_table
的各項資料並非share object,因此是不需要synchronized。但在尋找可用空間與釋放perf_table
時,因為是對表操作,因此必須要特別用lock
去保護。
在perf stat中測量的指標包含,Page fault次數、Context switch次數、Total ticks計數和CPU ticks計數和CPU Migration次數。
sysproc.c
xv6 中的記憶體是系統一開始就分配好的,在一般情況下不容易發生 Page fault,因此我們採用 Lazy page allocation,即當 process 需要記憶體時再分配。我們需要在 System call sys_sbrk()
中將 growproc()
系統分配記憶體的程式碼註解掉
int
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
addr = myproc()->sz;
// For lazy page allocation
myproc()->sz += n;
// if(growproc(n) < 0)
// return -1;
return addr;
}
trap.c
於 trap()
增加 switch case 來處理 Page fault,利用當下 proc->sz
來實際分配記憶體,並於此 section 插入 counter,保存的方式與 Context switch 相似,依 pid 取得 perf_record
結構,並賦予值
// For lazy page allocation
case T_PGFLT:
{
char *mem;
// 依 pid 取得 perf_record 結構
struct perf_record* rec = getperf(myproc()->pid);
// rcr2() returns the virtual address that caused page fault
// PGROUNDDOWN() find the address of page round down
uint va = PGROUNDDOWN(rcr2());
uint newsz = myproc()->sz;
if(rec && rec->recording){
// Page fault 計數
rec->pgfault++;
}
// Until we have allocated enough memory for process
for(; va < newsz; va += PGSIZE){
// 申請記憶體空間
mem = kalloc();
// 若記憶體不足時的例外處理
if (mem == 0) {
cprintf("pid %d trap out of memory--kill proc\n", myproc()->pid);
myproc()->killed = 1;
break;
}
// 將alloc的位址初始化為0
memset(mem, 0, PGSIZE);
// 建立虛擬記憶體與實體記憶體的映射關係,並更新分頁表,以及發生例外的處理
if(mappages(myproc()->pgdir, (char*) va, PGSIZE, V2P(mem), PTE_W | PTE_U) < 0){
cprintf("pid %d trap out of memory--kill proc\n", myproc()->pid);
kfree(mem);
myproc()->killed = 1;
break;
}
}
break;
}
proc.c
於 sched()
插入紀錄 Context switch 的函式perf_inc_context_swtch(pid)
,並於該函式中將值儲存至 perf_record
結構
void
perf_inc_context_swtch(int pid){
// 依 pid 取得 recording 結構
struct perf_record* r = getperf(pid);
if(r && r->recording){
// Context switch 計數
r->cxtsw ++;
}
}
...
void
sched(void)
{
...
// 紀錄 Context switch
perf_inc_context_swtch(p->pid);
// 發生 Context switch
swtch(&p->context, mycpu()->scheduler);
...
}
Total ticks是指程序從開始執行到結束時所耗費的ticks,包含sleeping 和 waiting period,這是最簡單計算的一項紀錄,只要在開始時標上start_ticks
,在結束時標上end_ticks
即可;開始的部分實作放在startperf()
,結束的部分則放在exit()
中的perf_end_recording()
。
void
exit(void)
{
...
curproc->state = ZOMBIE;
perf_end_period(curproc->pid);
perf_end_recording(curproc->pid);
sched();
panic("zombie exit");
}
perf_end_recording()
很單純就是透過getperf(pid)
找到對應的紀錄並且標上endticks
而已。
void perf_end_recording(int pid){
struct perf_record* r = getperf(pid);
if(r && r->recording){
r->endticks = ticks;
r->recording = 0;
}
}
CPU ticks則稍微複雜些,主要是因為只紀錄使用CPU時的ticks,所以會有區間的概念,在實作上透過使用perf_record
結構中的_ticks
和sumticks
來紀錄開閉區間。
struct perf_record{
...
int _ticks;
int sumticks;
...
};
流程如下:
startperf()
初始化_ticks = ticks(目前ticks)
sumticks += ticks - _ticks && _ticks = -1
,進入閉區間。_ticks = ticks(目前ticks)
進入開區間。recording == 0
void perf_start_period(int pid){
struct perf_record* pr = getperf(pid);
if(pr){
if(pr->_ticks >= 0){
panic("_ticks >= 0 ");
}
pr->_ticks = ticks;
}
}
void perf_end_period(int pid){
struct perf_record* pr = getperf(pid);
if(pr){
if(pr->_ticks<0){
panic("_ticks < 0 ");
}
pr->sumticks += ticks - pr->_ticks;
pr->_ticks = -1;
}
}
進入開區間的點只有一個,也就是被cpu schedulers選中的時候,執行perf_start_period(pid)
進入開區間。
void
scheduler(void)
{
...
c->proc = p;
switchuvm(p);
perf_start_period(p->pid);
p->state = RUNNING;
swtch(&(c->scheduler), p->context);
...
}
但在xv6中封閉區間的觸發點則有多個進入點,分別是呼叫sleep()
、執行時間用盡yield()
以及執行完畢exit()
,都會觸發perf_end_period(pid)
紀錄並封閉區間。
void
sleep(void *chan, struct spinlock *lk)
{
...
p->chan = chan;
p->state = SLEEPING;
perf_end_period(p->pid);
sched();
...
}
void
exit(void)
{
...
// Jump into the scheduler, never to return.
curproc->state = ZOMBIE;
perf_end_period(curproc->pid);
...
}
void
yield(void)
{
acquire(&ptable.lock); //DOC: yieldlock
myproc()->state = RUNNABLE;
perf_end_period(myproc()->pid);
sched();
release(&ptable.lock);
}
最後通過sumticks
累計就可以獲得總計的CPU執行時間。
CPU migrations 即 process 運行過程中發生 CPU 切換的次數,我們利用 perf_record
結構中的 cpuid
與 cpusw
來分辨 CPU 與紀錄切換次數
struct perf_record{
...
int cpuid;
int cpusw;
};
於 scheduler()
分配 process 時,利用 perf_add_if_cpu_sw()
取得當前的 APIC ID,即可判斷目前 process 在哪顆 CPU 上運行,再與前一次的紀錄作比對,若不同則代表 CPU 切換過,並做計數
void perf_add_if_cpu_sw(int pid, int apicid){
struct perf_record* pr = getperf(pid);
if(pr){
if(pr->cpuid != -1 && pr->cpuid!=apicid)
pr->cpusw++;
pr->cpuid = apicid;
}
}
void
scheduler(void)
{
...
/* round robin scheduling */
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
...
c->proc = p;
switchuvm(p);
// 紀錄 CPU migrations
perf_add_if_cpu_sw(p->pid, c->apicid);
perf_start_period(p->pid);
p->state = RUNNING;
swtch(&(c->scheduler), p->context);
switchkvm();
...
}
...
}
perf sched 的部分,則是按照前述的部分,註冊 system call 即可。
syscall.h
#define SYS_psched 23
syscall.c
extern int sys_psched(void);
...
static int (*syscalls[])(void) = {
...
[SYS_psched] sys_psched,
};
usys.S
SYSCALL(psched)
sysproc.c
在 xv6 中,process 本身有6種不同的狀態,分別是尚未使用, 被配置, 準備 run, 正在 run, 等待 I/O 以及 exiting。
要紀錄時間,在 process 對應的狀態,插入必要的紀錄 tick 函式即可。
// 程式碼內容較多,因此以 psuedo code 解釋
sys_psched(void)
{
// 指向 proc 這個 struct 的 pointer
struct proc *p;
struct recproc *rp;
// 列出所有的 process state
static char *states[] = {
[UNUSED] "unused",
...
};
char *state;
cprintf("PID\t|\tState\t|\tStart\t|\tEnd\t|\tDuration\t|\tName\t\n");
// loop 過 ptable 中的所有 process,將目前 ptable 中的資料拷貝到自定義(self-defined)的 rectable
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++)
{
//當 pid = 0,代表該欄為空,停止複製 ptable 中的資訊
if(p->pid == 0) break;
//state 為 UNUSED,跳過檢查下一個 process
if(p->state == UNUSED)
continue;
//複製 process 資訊
rp = rectable.recproc; // rp init
if(p->state >= 0 && p->state < NELEM(states) && states[p->state])
{
rp->state = p->state;
state = states[rp->state];
}
else
state = "???";
rp->pid = p->pid;
rp->startTicks = p->startTicks;
rp->endTicks = p->endTicks;
safestrcpy(rp->name, p->name, sizeof(rp->name));
// 以內建的 cprintf() 函式印出結果
}
return 1;
}
在 xv6 資料夾中新增檔案,檔名叫作user_test_psched.c
,用來呼叫自行定義的 system call psched()
#include "types.h"
#include "stat.h"
#include "user.h"
int
main(void)
{
int err = 0;
err = psched(); //若成功,印出 perf sched 的結果並回傳 1
if(err == 1)
{
//Do nothing
}
else
{
printf(1, "\nTest system call psched fail: %d\n", err);
}
exit();
}
perf.c
將字串提出來常數化,若往後有新增項目直接增加到成對結構裡,不用動到 function
// 將字串都常數化
const char hardware_event[] = "[Hardware event]";
const char software_event[] = "[Software event]";
const char Hardware_cache_event[] = "[Hardware cache event]";
const char list_event_message[] = "List of pre-defined events:";
const char usage_message[] = "usage: perf [FLAG] COMMAND [ARGS]";
const char common_message[] = "The most commonly used perf commands are:";
// 裝填到成對的結構裡
struct perfoutput help_output[] = {
{ "list", "List all symbolic event types" },
{ "stat", "Run a command and gather performance counter statistics" },
{ "sched", "Tool to trace/measure scheduler properties (latencies)" }
};
struct perfoutput list_output[] = {
{ "cpu-migrations OR migrations", software_event },
{ "cpu-ticks", software_event },
{ "page-faults OR faults", software_event },
{ "context-switches OR cs", software_event }
};
...
int
perf_help(void)
{
int i, array_size;
array_size = sizeof(help_output) / sizeof(struct perfoutput);
cprintf("\n %s\n\n", usage_message);
cprintf(" %s\n", common_message);
// 迭代輸出
for (i = 0; i < array_size; i++) {
cprintf(" %s%s\n", help_output[i].title, help_output[i].descript);
}
cprintf("\n");
return 0;
}
int
perf_list(void)
{
int i, array_size;
array_size = sizeof(list_output) / sizeof(struct perfoutput);
cprintf("\n %s\n\n", list_event_message);
// 迭代輸出
for (i = 0; i < array_size; i++) {
cprintf(" %s%s\n", list_output[i].title, list_output[i].descript);
}
cprintf("\n");
return 0;
}
實作上分別面臨幾項挑戰
fork
階段所產生的 Page fault,而 stat 要記錄的時機卻是在 fork
之後,因此這部分還需要努力rdtsc
印出CPU cycles,但很困擾的是透過Assembly取得的是64bit(unsigned long long)的資料,在xv6下沒辦法直接顯示,必須要用assembly實作64bit的除法和取餘。這個 API 需要去追蹤所有的 process 切換狀態的時間點,同時記下在某個狀態執行的時間長度為多少。
也就是 process 在 xv6 Round Robin 排程演算法的 Time slot 中,花費了多少 kernel ticks。
因此需要有一張紀錄所有 process 資訊的 process table,記下在當下 time slot 的起始、結束時間點,以及截至目前累計的 ticks。
process 結束時,主要會有 3 種可能的狀況,分別是中斷(yield)、睡眠(sleeping) 以及 離開(exit) 。這三個函式分別由proc.c
中的 yield()
、sleep()
以及exit()
函式中進行處理,因此必須要在這些程式模組中加入紀錄結束 ticks 的函式 。
因此,實作上主要的難點在於:
若上述 3 點都能達成,則 system call perf 中的選項 sched 就完成了。
此項目較無難度,只要事前確認好 spec,將結果輸出即可
功能 | 負責組員 |
---|---|
perf help / perf list | 徐煒員 / 謝恩平 / 盧贊名 |
perf sched | 徐煒員 |
perf stat | 謝恩平 / 盧贊名 |