xv6學習筆記(3):中斷處理和系統呼叫

1. tvinit函式

這個函式位於main函式內

表明了就是設定idt表

void
tvinit(void)
{
int i; for(i = 0; i < 256; i++)
SETGATE(idt[i], 0, SEG_KCODE<<3, vectors[i], 0);
SETGATE(idt[T_SYSCALL], 1, SEG_KCODE<<3, vectors[T_SYSCALL], DPL_USER); initlock(&tickslock, "time");
}

1. SETGATE函式

這裡的setgate是一個巨集定義是用來設定idt表的

#define SETGATE(gate, istrap, sel, off, d)                \
{ \
(gate).off_15_0 = (uint)(off) & 0xffff; \
(gate).cs = (sel); \
(gate).args = 0; \
(gate).rsv1 = 0; \
(gate).type = (istrap) ? STS_TG32 : STS_IG32; \
(gate).s = 0; \
(gate).dpl = (d); \
(gate).p = 1; \
(gate).off_31_16 = (uint)(off) >> 16; \
}

下面是函式引數的說明

Sel : 表示對於中斷處理程式程式碼所在段的段選擇子

off:表示中斷處理程式程式碼的段內偏移

(gate).gd_off_15_0 : 儲存偏移值的低16位

(gate).gd_off_31_16 : 儲存偏移值的高16位

(gate).gd_sel : 儲存段選擇子

(gate).gd_dpl : dpl 表示該段對應的

熟悉了這些之後參考intel的開發手冊找一下istrap的值,這裡注意系統呼叫的dpl = 3不然我們無法從使用者模式進去

這裡只要按照上述巨集定義的格式書寫就好,而且這裡的中斷處理函式我們都不用關心怎麼實現,只用給他一個佔位符。

可以發現這裡就是這是IDT表格了

2. idtinit函式

void
idtinit(void)
{
lidt(idt, sizeof(idt));
}

這裡就是呼叫lidt函式

static inline void
lidt(struct gatedesc *p, int size)
{
volatile ushort pd[3]; pd[0] = size-1;
pd[1] = (uint)p;
pd[2] = (uint)p >> 16; asm volatile("lidt (%0)" : : "r" (pd));
}

這個函式最後會呼叫lidt這個彙編程式碼

而lidt這個彙編程式碼做的事情就是把pd載入到GDTR。

也就是有對應的IDT表的基地址 和 IDT表的大小

CS暫存器儲存的是核心程式碼段的段編號SEG_KCODE,offset部分儲存的是vector[i]的地址。在XV6系統中,所有的vector[i]地址均指向trapasm.S中的alltraps函式。

2. XV6中斷處理過程

1. 中斷例子

當XV6的遇到中斷志龍,首先CPU硬體會發現這個錯誤,觸發中斷處理機制。在中斷處理機制中,硬體會執行如下步驟:下面的過程我們成為保護現場xv6官方文件

  1. 從IDT 中獲得第 n 個描述符,n 就是 int 的引數。
  2. 檢查CS的域 CPL <= DPL,DPL 是描述符中記錄的特權級。
  3. 如果目標段選擇符的 PL < CPL,就在 CPU 內部的暫存器中儲存ESP和SS的值。
  4. 從一個任務段描述符中載入SS和ESP。
  5. 將SS壓棧。
  6. 將ESP壓棧。
  7. 將EFLAGS壓棧。
  8. 將CS壓棧。
  9. 將EIP壓棧。
  10. 清除EFLAGS的一些位。
  11. 設定CS和EIP為描述符中的值。

此時,由於CS已經被設定為描述符中的值(SEG_KCODE),所以此時已經進入了核心態,並且EIP指向了trapasm.S中alltraps函式的開頭。在alltrap函式中,系統將使用者暫存器壓棧,構建Trap Frame,並且設定資料暫存器段為核心資料段,然後跳轉到trap.c中的trap函式。

alltraps繼續壓入暫存器儲存現場,得到trapframe結構體,trapframe結構體如圖所示,其中oesp沒有用處,這是pushal指令統一壓棧的。

.globl alltraps
alltraps:
# Build trap frame.
pushl %ds
pushl %es
pushl %fs
pushl %gs
pushal

這裡的pushal就是壓入所有通用暫存器

在這之後重新設定段暫存器,進入核心態,壓入當前棧esp,然後呼叫C函式trap處理中斷,在trap返回時,彈出esp

# Set up data segments.
movw $(SEG_KDATA<<3), %ax
movw %ax, %ds
movw %ax, %es # Call trap(tf), where tf=%esp
pushl %esp
call trap

trap函式是通過tf->trapno來進行邏輯分支處理的。下面介紹一下系統呼叫的處理。

系統呼叫

當tr->trapno是 T_SYSCALL的時候,核心呼叫syscall函式。

if(tf->trapno == T_SYSCALL){
if(myproc()->killed)
exit();
myproc()->tf = tf;
syscall();
if(myproc()->killed)
exit();
return;
}

這是syscalls的對應陣列嗷

extern int sys_chdir(void);
extern int sys_close(void);
extern int sys_dup(void);
extern int sys_exec(void);
extern int sys_exit(void);
extern int sys_fork(void);
extern int sys_fstat(void);
extern int sys_getpid(void);
extern int sys_kill(void);
extern int sys_link(void);
extern int sys_mkdir(void);
extern int sys_mknod(void);
extern int sys_open(void);
extern int sys_pipe(void);
extern int sys_read(void);
extern int sys_sbrk(void);
extern int sys_sleep(void);
extern int sys_unlink(void);
extern int sys_wait(void);
extern int sys_write(void);
extern int sys_uptime(void); static int (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
[SYS_wait] sys_wait,
[SYS_pipe] sys_pipe,
[SYS_read] sys_read,
[SYS_kill] sys_kill,
[SYS_exec] sys_exec,
[SYS_fstat] sys_fstat,
[SYS_chdir] sys_chdir,
[SYS_dup] sys_dup,
[SYS_getpid] sys_getpid,
[SYS_sbrk] sys_sbrk,
[SYS_sleep] sys_sleep,
[SYS_uptime] sys_uptime,
[SYS_open] sys_open,
[SYS_write] sys_write,
[SYS_mknod] sys_mknod,
[SYS_unlink] sys_unlink,
[SYS_link] sys_link,
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
};

這裡的systemcall函式利用eax暫存器獲得系統呼叫號。最後的返回值也利用eax暫存器返回

如果系統呼叫號合理的話,返回值就是對應系統呼叫函式產生的返回值

void
syscall(void)
{
int num;
struct proc *curproc = myproc(); num = curproc->tf->eax;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
curproc->tf->eax = syscalls[num]();
} else {
cprintf("%d %s: unknown sys call %d\n",
curproc->pid, curproc->name, num);
curproc->tf->eax = -1;
}
}

下面是對於除0的處理。

    if(myproc() == 0 || (tf->cs&3) == 0){
// In kernel, it must be our mistake.
cprintf("unexpected trap %d from cpu %d eip %x (cr2=0x%x)\n",
tf->trapno, cpuid(), tf->eip, rcr2());
panic("trap");
}
// In user space, assume process misbehaved.
cprintf("pid %d %s: trap %d err %d on cpu %d "
"eip 0x%x addr 0x%x--kill proc\n",
myproc()->pid, myproc()->name, tf->trapno,
tf->err, cpuid(), tf->eip, rcr2());
myproc()->killed = 1;

根據觸發中斷的是核心態還是使用者程序,執行不同的處理。如果是使用者程序出錯了,那麼系統會殺死這個使用者程序;如果是核心程序出錯了,那麼在輸出一段錯誤資訊後,整個系統進入死迴圈。

如果是一個可以修復的錯誤,比如頁錯誤,那麼系統會在處理完後返回trap()函式進入trapret()函式,在這個函式中恢復程序的執行上下文,讓整個系統返回到觸發中斷的位置和狀態。

2. 系統呼叫全過程

首先在檔案user.h中儲存了提供的系統呼叫,這裡以exec這個系統呼叫為例,考察在使用者態執行的整個流程。

// system calls
int fork(void);
int exit(void) __attribute__((noreturn));
int wait(void);
int pipe(int*);
int write(int, const void*, int);
int read(int, void*, int);
int close(int);
int kill(int);
// .......

1. 考慮系統呼叫號如何傳遞

這裡需要去看一下usys.S和反彙編一下usys.o

1. 首先看去看usys.S

可以發現這裡定義了一個巨集定義就是根據傳遞過來的系統呼叫名稱把系統呼叫號傳遞到%eax暫存器中

隨後觸發int中斷陷入核心態

#include "syscall.h"
#include "traps.h" #define SYSCALL(name) \
.globl name; \
name: \
movl $SYS_ ## name, %eax; \
int $T_SYSCALL; \
ret SYSCALL(fork)
SYSCALL(exit)
SYSCALL(wait)
SYSCALL(pipe)
SYSCALL(read)
SYSCALL(write)
SYSCALL(close)
SYSCALL(kill)
SYSCALL(exec)
SYSCALL(open)
SYSCALL(mknod)
SYSCALL(unlink)
SYSCALL(fstat)
SYSCALL(link)
SYSCALL(mkdir)
SYSCALL(chdir)
SYSCALL(dup)
SYSCALL(getpid)
SYSCALL(sbrk)
SYSCALL(sleep)
SYSCALL(uptime)
2. 在看usys.o

我們這裡反彙編一下usys.o

以fork為例子它把系統呼叫號1傳遞給了eax暫存器

3.執行系統呼叫函式

隨後在syscall.c中到syscall函式

在這裡利用系統呼叫號獲取對應的系統呼叫函式

void
syscall(void)
{
int num;
struct proc *curproc = myproc(); num = curproc->tf->eax;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
curproc->tf->eax = syscalls[num]();
} else {
cprintf("%d %s: unknown sys call %d\n",
curproc->pid, curproc->name, num);
curproc->tf->eax = -1;
}
}

exec為例子就是執行這個函式sys_exec進行系統呼叫處理

2. 系統呼叫函式執行

經過上面的一頓分析,最後exec系統呼叫會進入這裡進行執行

int
sys_exec(void)
{
char *path, *argv[MAXARG];
int i;
uint uargv, uarg; if(argstr(0, &path) < 0 || argint(1, (int*)&uargv) < 0){
return -1;
}
memset(argv, 0, sizeof(argv));
for(i=0;; i++){
if(i >= NELEM(argv))
return -1;
if(fetchint(uargv+4*i, (int*)&uarg) < 0)
return -1;
if(uarg == 0){
argv[i] = 0;
break;
}
if(fetchstr(uarg, &argv[i]) < 0)
return -1;
}
return exec(path, argv);
}

對於exec而言,exec需要一個可執行檔案的路徑和需要執行的引數。而獲取引數和路徑的函式下面來介紹一下

1. argstr函式

可以發現這個函式呼叫了argint函式以及fetchstr()函式

這裡的(myproc()->tf->esp) + 4 + 4*n就是獲取上述棧幀裡儲存的第幾個引數

Eg: n = 0 時候就說獲取edi暫存器的引數我們以exec為例子第一個引數使用edi暫存器傳遞的因此就是獲取可執行檔案的路徑的地址

而真正的字串還要利用fetchstr函式獲取

int
argstr(int n, char **pp)
{
int addr;
if(argint(n, &addr) < 0)
return -1;
return fetchstr(addr, pp);
}
2. argint函式
int
argint(int n, int *ip)
{
return fetchint((myproc()->tf->esp) + 4 + 4*n, ip);
}
3. fetchint函式
// Fetch the int at addr from the current process.
int
fetchint(uint addr, int *ip)
{
struct proc *curproc = myproc(); if(addr >= curproc->sz || addr+4 > curproc->sz)
return -1;
*ip = *(int*)(addr);
return 0;
}
4. fetchstr函式
int
fetchstr(uint addr, char **pp)
{
char *s, *ep;
struct proc *curproc = myproc(); if(addr >= curproc->sz)
return -1;
*pp = (char*)addr;
ep = (char*)curproc->sz;
for(s = *pp; s < ep; s++){
if(*s == 0)
return s - *pp;
}
return -1;
}

3. 真正系統呼叫的執行

而構建好引數之後最後sys_exec實際上會呼叫exec(path, argv);函式

而exec函式還是比較複雜的這裡簡單分析一下即可。

  1. 根據提供的path獲取檔案資訊讀入到inode
  2. 然後把inode資訊解析到elf頭中
int
exec(char *path, char **argv)
{
char *s, *last;
int i, off;
uint argc, sz, sp, ustack[3+MAXARG+1];
struct elfhdr elf;
struct inode *ip;
struct proghdr ph;
pde_t *pgdir, *oldpgdir;
struct proc *curproc = myproc(); begin_op(); if((ip = namei(path)) == 0){
end_op();
cprintf("exec: fail\n");
return -1;
}
ilock(ip);
pgdir = 0; // Check ELF header
if(readi(ip, (char*)&elf, 0, sizeof(elf)) != sizeof(elf))
goto bad;
if(elf.magic != ELF_MAGIC)
goto bad;
  1. 這裡會給每一個程序分配一個核心頁表然後在返回使用者空間之前把它copy到使用者空間
  2. 然後按照elf段把它分配memory然後載入到記憶體,分配和載入分別通過allocuvmloaduvm函式實現
  if((pgdir = setupkvm()) == 0)
goto bad; // Load program into memory.
sz = 0;
for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
if(readi(ip, (char*)&ph, off, sizeof(ph)) != sizeof(ph))
goto bad;
if(ph.type != ELF_PROG_LOAD)
continue;
if(ph.memsz < ph.filesz)
goto bad;
if(ph.vaddr + ph.memsz < ph.vaddr)
goto bad;
if((sz = allocuvm(pgdir, sz, ph.vaddr + ph.memsz)) == 0)
goto bad;
if(ph.vaddr % PGSIZE != 0)
goto bad;
if(loaduvm(pgdir, (char*)ph.vaddr, ip, ph.off, ph.filesz) < 0)
goto bad;
}

算了這裡先看一下分配和載入分別是如何做的‘

allocuvm函式

這個函式就是逐頁為每一段分配頁表並做對應的對映。

int
allocuvm(pde_t *pgdir, uint oldsz, uint newsz)
{
char *mem;
uint a; if(newsz >= KERNBASE)
return 0;
if(newsz < oldsz)
return oldsz; a = PGROUNDUP(oldsz);
for(; a < newsz; a += PGSIZE){
mem = kalloc();
if(mem == 0){
cprintf("allocuvm out of memory\n");
deallocuvm(pgdir, newsz, oldsz);
return 0;
}
memset(mem, 0, PGSIZE);
if(mappages(pgdir, (char*)a, PGSIZE, V2P(mem), PTE_W|PTE_U) < 0){
cprintf("allocuvm out of memory (2)\n");
deallocuvm(pgdir, newsz, oldsz);
kfree(mem);
return 0;
}
}
return newsz;
}

loaduvm函式

這裡載入到了核心的高地址區域(說實話現在還不懂為啥要這樣做。後面慢慢來吧

// Load a program segment into pgdir.  addr must be page-aligned
// and the pages from addr to addr+sz must already be mapped.
int
loaduvm(pde_t *pgdir, char *addr, struct inode *ip, uint offset, uint sz)
{
uint i, pa, n;
pte_t *pte; if((uint) addr % PGSIZE != 0)
panic("loaduvm: addr must be page aligned");
for(i = 0; i < sz; i += PGSIZE){
if((pte = walkpgdir(pgdir, addr+i, 0)) == 0)
panic("loaduvm: address should exist");
pa = PTE_ADDR(*pte);
if(sz - i < PGSIZE)
n = sz - i;
else
n = PGSIZE;
if(readi(ip, P2V(pa), offset+i, n) != n)
return -1;
}
return 0;
}
  1. 這裡用來構建引數
  2. 然後為返回使用者空間做準備
  3. 這裡把curproc->tf->eip = elf.entry;這樣就設定好了所需要執行函式的入口地址
 iunlockput(ip);
end_op();
ip = 0; // Allocate two pages at the next page boundary.
// Make the first inaccessible. Use the second as the user stack.
sz = PGROUNDUP(sz);
if((sz = allocuvm(pgdir, sz, sz + 2*PGSIZE)) == 0)
goto bad;
clearpteu(pgdir, (char*)(sz - 2*PGSIZE));
sp = sz; // Push argument strings, prepare rest of stack in ustack.
for(argc = 0; argv[argc]; argc++) {
if(argc >= MAXARG)
goto bad;
sp = (sp - (strlen(argv[argc]) + 1)) & ~3;
if(copyout(pgdir, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
goto bad;
ustack[3+argc] = sp;
}
ustack[3+argc] = 0; ustack[0] = 0xffffffff; // fake return PC
ustack[1] = argc;
ustack[2] = sp - (argc+1)*4; // argv pointer sp -= (3+argc+1) * 4;
if(copyout(pgdir, sp, ustack, (3+argc+1)*4) < 0)
goto bad; // Save program name for debugging.
for(last=s=path; *s; s++)
if(*s == '/')
last = s+1;
safestrcpy(curproc->name, last, sizeof(curproc->name)); // Commit to the user image.
oldpgdir = curproc->pgdir;
curproc->pgdir = pgdir;
curproc->sz = sz;
curproc->tf->eip = elf.entry; // main
curproc->tf->esp = sp;
switchuvm(curproc);
freevm(oldpgdir);
return 0; bad:
if(pgdir)
freevm(pgdir);
if(ip){
iunlockput(ip);
end_op();
}
return -1;
}

參考

部落格1

部落格2

xv6中文文件