这是《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; // 段地址低16位
uint cs : 16; // 代码段选择器
uint args : 5; // # args, 0 for interrupt/trap gates
uint rsv1 : 3; // 保留
uint type : 4; // 类型STS_IG32 32位中断,STS_TG32 32位陷入
uint s : 1; // must be 0 (system)
uint dpl : 2; // 可用于内核权限还是用户态权限
uint p : 1; // 保留字段
uint off_31_16 : 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; // gate大小
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 {
// 下面的寄存器由pusha保存到栈上
uint edi;
uint esi;
uint ebp;
uint oesp; // useless & ignored
uint ebx;
uint edx;
uint ecx;
uint eax;

// 手动push保存到栈上
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;

// below here only when crossing rings, such as from user to kernel
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);
}