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


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這個匯編代碼

image-20210703224315388

而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函數。

image-20210818221952620

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

image-20210819225435952

以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中文文檔


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM