lab2 是关于系统调用的,很有意思,带我简单领略了如何进行系统调用。
# System call tracing
任务就是跟踪系统调用,如果内核执行了系统调用就打印追踪的结果。
首先,需要在 user.h
中定义这么一个 trace
函数,用户可以执行。
int trace(int); |
然后,在 usys.pl
添加一个 entry:
sub entry { | |
my $name = shift; | |
print ".global $name\n"; | |
print "${name}:\n"; | |
print " li a7, SYS_${name}\n"; | |
print " ecall\n"; | |
print " ret\n"; | |
} | |
entry("sysinfo") |
这个脚本运行后会生成 usys.S
汇编文件,里面定义了每个 system call 的用户态跳板函数:
trace: # 定义用户态跳板函数
li a7, SYS_trace # 将系统调用 id 存入 a7 寄存器
ecall # ecall,调用 system call ,跳到内核态的统一系统调用处理函数 syscall() (syscall.c)
ret
注意这里的 li a7, SYS_trace
,xv6 将系统调用的调用函数 ID 存入 a7 寄存器中,接着执行 ecall 进行系统调用。到这里,用户态已经完成了自己的工作了。
很显然, ecall
让 xv6 执行系统调用,xv6 做的应该是从寄存器 a7
中去除系统调用函数的 id 号,然后检索找到这个 id 号对应的系统调用函数,接着再执行这个系统调用函数。
事实上,xv6 正是这么做的,在内核的 sys_call.c
文件中定义了如何进行系统调用:
首先这个文件用 extern
关键字声明了,这些系统调用函数,例如:
// 片段 | |
extern uint64 sys_unlink(void); | |
extern uint64 sys_wait(void); | |
extern uint64 sys_write(void); | |
extern uint64 sys_uptime(void); | |
extern uint64 sys_trace(void); |
然后声明了一个数组,数组的索引是系统调用函数的 id,value 就是这些调用函数:
static uint64 (*syscalls[])(void) = { | |
// 片段 | |
[SYS_fork] sys_fork, | |
[SYS_exit] sys_exit, | |
[SYS_wait] sys_wait, | |
[SYS_pipe] sys_pipe, | |
[SYS_read] sys_read, | |
[SYS_kill] sys_kill, | |
} |
最后就是 syscall
函数,再次说明, ecall
实际上做的就是将进程切换到内核态,并且调用该 syscall
函数。
void | |
syscall(void) | |
{ | |
int num; | |
struct proc *p = myproc(); | |
num = p->trapframe->a7; | |
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) { | |
p->trapframe->a0 = syscalls[num](); | |
check_trace(num, p); // 添加 check trace,进行追踪 | |
} else { | |
printf("%d %s: unknown sys call %d\n", | |
p->pid, p->name, num); | |
p->trapframe->a0 = -1; | |
} | |
} |
果然, syscall
首先从寄存器 a7
中拿出了 user 放在 a7
中的系统调用 id,然后判断这个 id 是否合法,如果合法就执行该系统调用函数,并把调用结果放在 a0
。
check_trace 中我们会判断是否需要最终该调用。
现在我们终于明白了,整个系统调用的过程了:
- 进程在 start 后进入 user mode,会保存系统调用函数的 id,但是它并不知道这些系统调用的代码,它只是把函数 id 号放在寄存器 a7 中以及把系统调用的参数放在
a0-a6
寄存器中,然后通过 ecall 告知内核,请帮我执行这个系统调用函数。 - 内核收到 ecall 后,会把进程的模式修改为内核模式,然后在进程的内核栈执行 syscall。
syscall
从寄存器a7
中拿到该调用函数 id,检查这个 id 号是否合法,如果合法就执行对应的函数,今后把调用返回的结果放在a0
。
对于 trace 该系统调用,我们还没做的就是添加该系统调用 id,以及实现该系统调用函数。
因此,我们接下来需要在 syscall.h
中注册该 id:
#define SYS_trace 22 |
然后,在 sysproc.c
中实现 uint64 sys_trace(void)
:
uint64 | |
sys_trace(void){ | |
int n; | |
if(argint(0, &n) < 0){ | |
return -1; | |
} | |
return trace(n); // trace | |
} |
接着在 proc.c 中添加 int trace(int)
:
int | |
trace(int mask) | |
{ | |
struct proc *p = myproc(); | |
p->mask = mask; | |
return 0; | |
} |
至此,整个系统调用过程我们已经完全搞定了!
# 遇到的坑
在刚开始,我也遇到了一个坑,比如在 proc.c
中定义好了 trace
函数却没法在 sysproc.c
中调用。
原因是我没有在头文件中声明该函数,只需要在 defs.h
文件中添加:
int trace(int); |
就可以使用该函数了,如果不想在头文件中添加,也可以在 sysproc.c
中使用 extern
关键字来声明:
extern int trace(int); |
这里有一个文章,详细描述了 extern
和头文件的关系:
https://www.runoob.com/w3cnote/extern-head-h-different.html
# Sysinfo
这个没什么好说了,按照刚才的步骤一步步实现该调用就行了,我的代码
void unusedproc(void* u){ | |
struct proc* p; | |
uint s = 0; | |
for(p=proc; p < &proc[NPROC] ; p++){ | |
if(p->state != UNUSED){ | |
s++; | |
} | |
} | |
*(uint64*)u = s; | |
} | |
// 检查有有多少个已用 proc |
void freemem(void *f) | |
{ | |
acquire(&kmem.lock); | |
uint64 s = 0; | |
struct run *r = kmem.freelist; | |
while(r){ | |
s+=4096; | |
r = r->next; | |
} | |
*(uint64*)f = s; // 指针强转 | |
release(&kmem.lock); | |
} | |
// 检查剩余空间 |
系统的剩余空间是已 freelist
这样一个链表形式进行保存的,每当进程申请空间就在这个空闲空间链表中拿下一个节点,free 就是把节点加到这个 freelist 中:
void * | |
kalloc(void) | |
{ | |
struct run *r; | |
acquire(&kmem.lock); // 分配内存需要加锁 | |
r = kmem.freelist; | |
if(r) | |
kmem.freelist = r->next; | |
release(&kmem.lock); | |
if(r) | |
memset((char*)r, 5, PGSIZE); // fill with junk | |
return (void*)r; | |
} |
上面的就是分配内存空间代码,kv6 每个节点表示 4096B 大小。
以及 sysproc.c
代码:
uint64 | |
sys_sysinfo(void){ | |
struct sysinfo info; | |
struct proc *p = myproc(); | |
uint64 add; // user pointer to struct stat | |
if(argaddr(0, &add) < 0) | |
return -1; | |
freemem(&info.freemem); | |
unusedproc(&info.nproc); | |
if(copyout(p->pagetable, add, (char *)&info, sizeof(info)) < 0)return -1; | |
return 0; | |
} |