最近更新於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()的大致執行流程。