文章目录
- 系统调用
- 1.实验目标
- 2.实验过程记录
- (1).理解系统调用接口
- (2).阅读argraw、argint、argaddr和argstr
- (3).理解系统调用的解耦合实现方式
- (4).wait系统调用的非阻塞选项实现
- (5).yield系统调用的实现
- 3.存在的问题及解决方案
- 实验小结
系统调用
1.实验目标
阅读并了解xv6内核中关于系统调用的部分,基于实验手册的要求完成几个任务:完成wait系统调用的非阻塞选项、实现yield系统调用、阅读并理解系统调用的代码。
2.实验过程记录
(1).理解系统调用接口
操作内容:在VS Code中查看并理解user/usys.pl的代码
#!/usr/bin/perl -w
# Generate usys.S, the stubs for syscalls.
print "# generated by usys.pl - do not edit\n";
print "#include \"kernel/syscall.h\"\n";
sub entry {
my $name = shift;
print ".global $name\n";
print "${name}:\n";
print " li a7, SYS_${name}\n";
print " ecall\n";
print " ret\n";
}
usys.pl是基于perl语言的代码文件,它的主要作用是为了所有当前内核的系统调用生成对应的汇编代码,简单来理解就是,它会根据传入系统调用的名字生成如下的一段系统调用对应的汇编代码:
.global $name
${name}:
li a7, SYS_${name}
ecall
ret
这段代码就很熟悉了,首先粘贴系统调用的name作为当前汇编代码生成文件的入口(通过.global指令指定入口),之后使用li(load immediate)将系统调用的调用号座位一个立即数加载到a7上,之后调用ecall进入操作系统内核,在执行结束之后再用ret返回。
(2).阅读argraw、argint、argaddr和argstr
操作内容:在VS Code中查看并理解argraw等函数的代码
static uint64 argraw(int n) {
struct proc *p = myproc();
switch (n) {
case 0:
return p->trapframe->a0;
case 1:
return p->trapframe->a1;
case 2:
return p->trapframe->a2;
case 3:
return p->trapframe->a3;
case 4:
return p->trapframe->a4;
case 5:
return p->trapframe->a5;
}
panic("argraw");
return -1;
}
实际上它的过程非常简单,调用系统调用时传入的参数会被存储在当前进程的trapframe当中,它最多可以接受6个参数(这一点和Linux内核是一致的,先前我在完成第一次选做作业的自定义系统调用时,在Linux内核当中看到的基于va_args实现的系统调用接口也最多只能接受6个参数)。
在有了argraw这个函数之后,后续就可以通过argint、argaddr和argstr三个函数来获取系统调用的参数,分别是整数、地址和字符串,这撒个函数的实现相当简单,因为地址和整数本质上都是获取整数,因此可以直接通过argraw获取对应的内容,但是argstr的实现相对复杂一点,因为C语言的字符串实际上字符数组,作为参数传入的时候会退化成指针,所以实现获取字符串的过程可以首先通过argaddr获取地址,之后再通过实现的一个fetchstr函数获取字符串的内容并且存储到buf当中:
// Fetch the nth 32-bit system call argument.
int argint(int n, int *ip) {
*ip = argraw(n);
return 0;
}
// Retrieve an argument as a pointer.
// Doesn't check for legality, since
// copyin/copyout will do that.
int argaddr(int n, uint64 *ip) {
*ip = argraw(n);
return 0;
}
// Fetch the nth word-sized system call argument as a null-terminated string.
// Copies into buf, at most max.
// Returns string length if OK (including nul), -1 if error.
int argstr(int n, char *buf, int max) {
uint64 addr;
if (argaddr(n, &addr) < 0) return -1;
return fetchstr(addr, buf, max);
}
于是我在syscall.c当中又找到了fetchstr函数的实现细节:
// Fetch the nul-terminated string at addr from the current process.
// Returns length of string, not including nul, or -1 for error.
int fetchstr(uint64 addr, char *buf, int max) {
struct proc *p = myproc();
int err = copyinstr(p->pagetable, buf, addr, max);
if (err < 0) return err;
return strlen(buf);
}
fetchstr的实现也并不复杂,不过因为处于内核态,它的实现不像我们会使用的类似strcpy之类的函数,首先获取到当前的进程控制块到p,然后从p的页表中拷贝出对应字节、对应地址的所有数据,在没有发生报错的时候则会正常返回字符串的长度。
(3).理解系统调用的解耦合实现方式
操作内容:在VS Code中查看并理解syscall.c的代码
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,
[SYS_exec] sys_exec, [SYS_fstat] sys_fstat,
[SYS_chdir] sys_chdir, [SYS_dup] sys_dup,
[SYS_getpid] sys_getpid, [SYS_sbrk] sys_sbrk,
[SYS_sleep] sys_sleep, [SYS_uptime] sys_uptime,
[SYS_open] sys_open, [SYS_write] sys_write,
[SYS_mknod] sys_mknod, [SYS_unlink] sys_unlink,
[SYS_link] sys_link, [SYS_mkdir] sys_mkdir,
[SYS_close] sys_close, [SYS_rename] sys_rename,
};
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]();
} else {
printf("%d %s: unknown sys call %d\n", p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
这个过程也比较简单的,syscall.c当中将所有的系统调用存储在同一个指针数组当中,在具体调用某个系统调用的时候,syscall函数会通过trapframe当中的a7来访问刚刚存入的系统调用号,之后再直接通过下标访问就可以比较轻松地访问到对应的系统调用了,这里也可以看到,在根据系统调用号找到了对应的系统调用之后会执行这条指令:
p->trapframe->a0 = syscalls[num]();
也就是说,系统调用的执行结果会被存储在a0当中。
(4).wait系统调用的非阻塞选项实现
操作内容:参考实验手册要求,修改wait系统调用的实现系统,增加一个非阻塞选项参数flags
为了增加sys_wait的非阻塞版本,我们首先需要在sys_wait中增加获取整型变量flags作为非阻塞标志的流程:
uint64 sys_wait(void) {
uint64 p;
if (argaddr(0, &p) < 0) return -1;
int flags;
if (argint(1, &flags) < 0) return -1;
return wait(p, flags);
}
这个过程是比较简单的,参考argraw、argint的实现可以知道,argint函数第一个参数传入的实际上是系统调用参数的位置,因为flags显然是在第二个位置上的参数,因此仿照上面获取地址p的方式,获取flags参数,之后与p一起传入wait函数即可,那么接下来就可以修改wait系统调用的函数声明了
int wait(uint64, int);
接下来就需要直接修改wait系统调用的实现本身了:
int wait(uint64 addr, int flags) {
…
if (flags != 1) sleep(p, &p->lock); // DOC: wait-sleep
else {
release(&p->lock);
return -1;
}
}
}
这里省略了wait函数的一部分,前面一部分的代码没有改动,具体的wait系统调用的实现已经再上一次的实验中分析过了,因此这里只需要改动最后一部分是否阻塞即可。
当flags不为1的时候就阻塞等待,否则释放当前获取的自旋锁,然后返回-1,这样就可以完成非阻塞等待的全流程了,接下来尝试使用waittest完成对于wait系统调用的测试:
可以看到,waittest在xv6内已经测试完毕,之后在内核仓库的目录下使用grade-lab-syscall进行测试:
(5).yield系统调用的实现
操作内容:参考实验手册要求,增加一个新的yield系统调用
首先在kernel/syscall.h当中增加一个SYS_yield系统调用号,这里直接继承最后一个系统调用后的第一个系统调用号23:
之后再kernel/syscall.c当中增加sys_yield的声明,并且在系统调用表syscalls当中增加刚刚添加的系统调用sys_yield:
之后再sysproc.c当中添加sys_yield函数的定义:
uint64 sys_yield(void) {
struct proc *p = myproc();
uint64 pc = p->trapframe->epc;
printf("start to yield, user pc %p\n", pc);
yield();
return 0;
}
因为yield函数本身实现非常简单,所以不需要单独传参,为了满足实验手册的要求,在sys_yield当中获取了当前进程的PCB,从它的trapframe中获取epc即为进入trap时的用户pc,这里没有对PCB进行加锁,这是因为struct proc将trapframe划归为私有部分,访问时不需要持有锁。
然后需要再user.h和usys.pl当中添加代码
最后,在Makefile的UPROGS增加_yieldtest的编译目标:
之后编译运行,在xv6中执行yieldtest测试,发现效果是一致的:
然后我尝试使用了grade-lab-syscall进行测试,发现有一个叫做time的点一直没有办法通过:
于是我去MIT的课程网站上查看了一下实验要求,发现需要自行创建一个time.txt,然后写入做实验的时间:
在创建了time.txt并且写入实验时间之后就100分通过了测试:
3.存在的问题及解决方案
问题:为什么在多CPU的情况下测试yield会出现乱码?
首先我们来看看测试的时候会发生什么:
可以发现,其实这一段输出信息混乱并不是完全没有规律,实际上应该是多个不同的需要打印出来的内容因为一些问题混杂在了一起,我猜测这个问题可能出在打印过程没有对缓冲区加锁,但是参考了printf.c当中对printf的实现,它实际上是加锁了的:
// Print to the console. only understands %d, %x, %p, %s.
void
#ifdef TEST
_printf(const char *filename, unsigned int line, char *fmt, ...)
#else
_printf(char *fmt, ...)
#endif
{
…
locking = pr.locking;
if (locking) acquire(&pr.lock);
…
#endif
if (locking) release(&pr.lock);
}
void printfinit(void) {
initlock(&pr.lock, "pr");
pr.locking = 1;
}
所以问题应该不在给输出过程加锁这件事上,最终我猜测是多核调度的过程中,不同核心上运行的进程的缓冲区不同步,而缓冲区到IO设备上这个过程可能是无法保证线程安全的,因此导致了输出也不同步的问题。
实验小结
- 1、本次实验阅读了xv6内核中系统调用的实现细节,了解了系统调用的实现与调用是如何分离的。
- 2、完成了对于wait系统调用的修改,完成了非阻塞式的wait系统调用,之后还增加了yield系统调用,最后通过grade-lab-syscall完成了这次修改/增加的两个系统调用的测试。