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中文文档