xv6的系统调用


最近更新于2021/08/05.

 

我们以xv6的一个系统调用getpid()为例,观察xv6的系统调用大致过程。

系统调用的声明位于 user.h 中,xv6的用户程序若要使用系统调用需要包括这个头文件,其中getpid()声明如下:

int getpid(void);

此函数定义于 usys.S 中,函数体通过宏SYSCALL定义:

usys.S/getpid
#define SYSCALL(name) \
  .globl name; \
  name: \
    movl $SYS_ ## name, %eax; \
    int $T_SYSCALL; \
    ret
    
...
SYSCALL(getpid)
...

 

将宏SYSCALL展开后如下所示:

.globl getpid;  # 声明全局变量getpid
getpid:
	movl $SYS_getpid, %eax;
	int $T_SYSCALL;
	ret

可以看到getpid()将系统调用的类型编号SYS_getpid(定义于 syscall.h 中,其值为11)写入寄存器 %eax, 之后通过指令 int $T_SYSCALL 产生软中断(陷阱),从而调用相应的中断处理程序后返回。(T_SYSCALL定义于 traps.h 中,其值为64,通常系统调用的编号为0x80, 即128).

产生中断后,硬件会在中断描述符表(IDT, Interrupt Descriptor Table)中查找用于处理系统调用的项。在xv6中,IDT是一个数组,定义于文件 trap.c 中:

struct gatedesc idt[256];

结构体gatedesc保存了中断处理程序的相关信息,例如中断处理程序的首地址。数组idt在函数tvinit()中被初始化:

trap.c/tvinit
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");
}

 

SETGATE用户初始化结构体gatedesc. 可以看出,处理系统调用的程序首地址为vectors[T_SYSCALL],数组vectors由Perl脚本 vectors.pl 生成,执行如下命令:

$ perl vectors.pl > vectors.S

在生成的文件 vectors.S 中,可以看到数组vectors的第X项为函数vectorX的首地址。因此处理系统调用的函数应为vector64:

.globl vector64
vector64:
  pushl $0
  pushl $64
  jmp alltraps

向栈中压入0和系统调用对应的编号64后,跳转到函数alltraps(). 函数alltraps()定义于文件 trapasm.S 中:

trapasm.S/alltraps
.globl alltraps
alltraps:
  # Build trap frame.
  pushl %ds
  pushl %es
  pushl %fs
  pushl %gs
  pushal
  
  # 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
  addl $4, %esp
  
  # Return falls through to trapret...
.globl trapret
trapret:
  popal
  popl %gs
  popl %fs
  popl %es
  popl %ds
  addl $0x8, %esp  # trapno and errcode
  iret

 

此函数首先将一些寄存器压入栈中,然后调用函数trap(), 随后从栈中恢复寄存器的值,最后以指令iret恢复到用户权限并返回。寄存器压入栈中的顺序是有讲究的,在压入这些寄存器后,在栈顶构造了一个结构体trapframe,因此可以通过向函数trap()传入栈顶寄存器%esp来引用此结构体。结构体trapframe定义于 x86.h 中:

x86.h/struct trapframe
// Layout of the trap frame built on the stack by the
// hardware and by trapasm.S, and passed to trap().
struct trapframe {
  // registers as pushed by pusha
  uint edi;
  uint esi;
  uint ebp;
  uint oesp;      // useless & ignored
  uint ebx;
  uint edx;
  uint ecx;
  uint eax;

  // rest of trap frame
  ushort gs;
  ushort padding1;
  ushort fs;
  ushort padding2;
  ushort es;
  ushort padding3;
  ushort ds;
  ushort padding4;
  uint trapno;

  // below here defined by x86 hardware
  uint err;
  uint eip;
  ushort cs;
  ushort padding5;
  uint eflags;

  // below here only when crossing rings, such as from user to kernel
  uint esp;
  ushort ss;
  ushort padding6;
};

 

函数trap()位于 trap.c 中,我们考虑与系统调用相关的那一部分:

void
trap(struct trapframe *tf)
{
  if(tf->trapno == T_SYSCALL){  // 若为系统调用
    if(myproc()->killed)
      exit();
    myproc()->tf = tf;
    syscall();
    if(myproc()->killed)
      exit();
    return;
  }
  ...
}

此函数首先检查发起系统的进程是否已被杀死,如果已被杀死就退出;否则调用函数syscall(),调用完成后再次检查发起系统调用的进程是否被杀死,若已被杀死就退出,否则返回。

看来函数syscall()是真正用于处理系统调用的函数,它位于syscall.c 中:

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;
  }
}

还记得吗?一开始我们就将系统调用的编号保存到寄存器%eax中,现在我们要通过查看%eax的值确定发起系统调用的类型,在这里%eax中保存的值为11(SYS_getpid), 因此我们调用函数syscalls[11](), 即函数sys_getpid.(数组syscalls就定义在函数syscall的上面)。函数sys_getpid位于 sysproc.c 中,它做的事情也很简单:返回当前进程的pid:

int
sys_getpid(void)
{
  return myproc()->pid;
}

以上就是xv6中系统调用getpid()的大致执行流程。

 

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM