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