这是《Operating System:Three Easy Pieces》的project之一:initial-xv6,使用的是MIT对unix v6修改过的x86版本xv6。
系统调用
汇编指令int n调用interrupt procedure(中断处理过程),n的范围是0~255,n作为IDT(interrupt descriptor table)的索引值,前32个中断描述符Intel自己用(主要用于异常),执行int后由用户态变为内核态。
系统调用初始化
异常分为中断(interrupt)、陷阱(trap)、故障(fault)、终止(abort)。中断主要是来自I/O设备的信号结果,中断是异步的,而陷阱(trap)是有意的异常,主要用于实现系统调用,陷阱是同步的。经典的故障是缺页异常
为了在异常发生时能够调用对应的异常处理过程,需要在booting的时候对IDT进行初始化,这个操作是在main.c/mainc()/tvinit()中进行的。
1 2 3 4 5 6 7 8 9 10 11
| 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宏主要用于设置IDT中每个中断描述符,当对应n的中断发生后就会调用的代码
下面是SETGATE
宏的代码:
1 2 3 4 5 6 7 8 9 10 11 12
| #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表示在代码段的偏移,参数d表示int n
来访问描述符所需要的权限。SETGATE(idt[T_SYSCALL], 1, SEG_KCODE<<3, vectors[T_SYSCALL], DPL_USER);
表示T_SYSCALL对应索引中的描述符类型为trap、代码段位SEG_KCODE、offset为vectors[T_SYSCALL],可以在用户态的时候直接调用,因此这个描述符主要处理系统调用。
下面是描述符结构:
1 2 3 4 5 6 7 8 9 10 11
| struct gatedesc { uint off_15_0 : 16; uint cs : 16; uint args : 5; uint rsv1 : 3; uint type : 4; uint s : 1; uint dpl : 2; uint p : 1; uint off_31_16 : 16; };
|
cs是段选择器,共16位,第0~1位表示权限(0,内核权限;1,用户态),第2位表示指向GDT(0,全局段表)还是LDT(1,局部段表),第3~15位是段表索引。
当中断描述符表设置好后,需要让硬件能够感知,所以汇编指令lidt addr
用于加载中断描述符地址到IDTR(interrupt descriptor table registor)这个地址是6字节(32位)其中低16位表示表大小,高32位是 表地址。在main.c/main/mpmain/idtinit中调用。
1 2 3 4 5
| void idtinit(void) { lidt(idt, sizeof(idt)); }
|
下面是lidt函数:
1 2 3 4 5 6 7 8 9
| 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)); }
|
系统调用执行
在用户态执行下面的代码调用read系统调用:
1
| int sz = read(fd, buf, size);
|
实际上read符号在usys.S文件中定义:
1 2 3 4 5 6 7 8
| #define SYSCALL(name) \ .globl name; \ name: \ movl $SYS_ ## name, %eax; \ int $T_SYSCALL; \ ret
SYSCALL(read)
|
SYSCALL是宏定义,SYSCALL(read)
展开后相当于下面的代码:
1 2 3 4 5
| .global read; read: movl $5, %eax; // SYS_read在syscall.h中定义,等于5 int $64; ret
|
当int指令被调用前,硬件必须保存当前的PC(eip)、eflags、esp等寄存器的值保存到栈上,这些信息被定义为为trapframe结构体(x86.h)因为栈是从高到低顺序,而trapframe是从低到高,所以push的顺序是从trapframe最后一个字段开始的。下面为trapframe结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| struct trapframe { uint edi; uint esi; uint ebp; uint oesp; uint ebx; uint edx; uint ecx; uint eax; ushort gs; ushort padding1; ushort fs; ushort padding2; ushort es; ushort padding3; ushort ds; ushort padding4; uint trapno;
uint err; uint eip; ushort cs; ushort padding5; uint eflags;
uint esp; ushort ss; ushort padding6; };
|
然后int 64
引发的trap,会去IDT中得到中断描述符,根据描述符执行vector64()
在vector.S(有vectors.pl脚本生成)中:
1 2 3 4
| .global vector64 vector64: pushl $64 jmp alltraps
|
这段代码将trap号放入栈中,填充trapframe中trapno这个字段。然后跳转到alltraps
(其实所有的异常都会跳到alltraps,默认的异常handle只是将trapno设置)中执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| .globl alltraps alltraps: # 将trapframe中ds、es、fs、gs保存 pushl %ds pushl %es pushl %fs pushl %gs pushal # 按照EAX, ECX, EDX, EBX, ESP (original value), EBP, ESI,EDI顺序存到栈上 # Set up data segments. movw $(SEG_KDATA<<3), %ax # 切换为内核代码段,SEG_KDATA为地址,第0到3都为0表示内核权限,使用GDT movw %ax, %ds movw %ax, %es
# Call trap(tf), where tf=%esp pushl %esp # 此时esp指向trapframe,作为参数传入trap函数 call trap addl $4, %esp
|
接下来所有的interrupt/trap handle都会进入trap函数,这个函数根据trap号进行选择对应处理函数。下面为trap代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| void trap(struct trapframe *tf) { if(tf->trapno == T_SYSCALL){ if(myproc()->killed) exit(); myproc()->tf = tf; syscall(); if(myproc()->killed) exit(); return; }
switch(tf->trapno){ case T_IRQ0 + IRQ_TIMER: ... break; case T_IRQ0 + IRQ_IDE: ... break; ... }
|
之后进入syscall()
函数继续执行,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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; } }
|
在syscall中syscalls存放了系统调用实现组成的数组,之后就调用sys_read()
:
1 2 3 4 5
| static int (*syscalls[])(void) = { ... [SYS_read] sys_read, ... };
|
返回的值被保存在curproc->tf>eax
中。
执行返回
当trap()
执行完毕后会返回alltrap
(trapasm.S)接着向下执行,alltrap
下面的代码是trapret
:
1 2 3 4 5 6 7 8 9
| .globl trapret trapret: popal popl %gs popl %fs popl %es popl %ds addl $0x8, %esp # trapno and errcode iret
|
从栈中恢复各个寄存器的值,调用iret
返回用户态调用的read函数继续执行。
实现系统调用
实现getreadcount(void)
这个系统调用,返回read()
的使用次数。
首先需要在usys.S
增加这个系统调用的初步实现:
1 2 3 4 5 6 7 8
| SYSCALL(getreadcount)
.global getreadcount; getreadcount: movl $SYS_getreadcount, %eax; int $T_SYSCALL; ret
|
可以看到需要添加SYS_gettreadcount
作为getreadcount()
的系统调用号,因此在syscall.h中添加
1
| #define SYS_getreadcount 21
|
此时调用会进入IDT表,找到系统调用对应的中断描述符,执行syscall()
,因此需要在syscall.c
里面增加getreadcount
对应的内部实现:
1 2 3 4 5 6 7 8
| extern int sys_getreadcount(void);
static int (*syscalls[])(void) = { ... [SYS_read] sys_read, ... [SYS_getreadcount] sys_getreadcount, };
|
sys_getreadcount
也就是我们需要实现的,我这里是增加了一个全局变量readcount
,以及用于限制并发的spinlock。在sysproc.c
增加定义:
1 2 3 4
| #include "spinlock.h"
uint readcount; struct spinlock readcountlock;
|
在sys_getreadcount
的逻辑就是加锁后读取readcount
。
1 2 3 4 5 6 7
| int sys_getreadcount(void) { int count; acquire(&readcountlock); count = readcount; release(&readcountlock); return count; }
|
在sys_read()
里面增加计数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| extern uint readcount; extern struct spinlock readcountlock;
int sys_read(void) { struct file *f; int n; char *p; acquire(&readcountlock); readcount += 1; release(&readcountlock); if(argfd(0, 0, &f) < 0 || argint(2, &n) < 0 || argptr(1, &p, n) < 0) return -1; return fileread(f, p, n); }
|