目录
代码实现:syscall add sysinfotest func · zion6135/xv6@0b1cdcc · GitHub
- Lab: System calls
- xv6-book翻译(自用)第二章 - 知乎
- 阅读xv6 book章节2、4.3、4.4
2.1 抽象物理资源
- 系统调用接口经过精心设计,既为程序员提供了方便,又提供了强大隔离的可能性。**Unix接口不是抽象资源的唯一方法,但它已被证明是一种非常好的方法。**
- CPU为强隔离提供硬件支持。例如,RISC-V有三种CPU可以执行指令的模式:Machine mode、Supervior mode和User mode。 在机器模式下执行的指令具有完全权限;CPU以机器模式启动。机器模式主要用于配置计算机。xv6在Machine mode下执行几行,然后更改为Supervior mode。
2.2 特权模式与系统调用
- 应用程序只能执行User mode指令,并被称为在用户空间中运行,而处于Supervior mode的软件可以执行特权指令,并被称为在内核空间中运行
- 在Supervior mode中,CPU可以执行特权指令:例如,启用和禁用中断,读取和写入保存页表地址的寄存器等。如果User mode的应用程序尝试执行特权指令,则CPU不会执行该指令,但是会切换到Supervior mode,以便Supervior mode的代码可以终止应用程序。
2.3 内核的组织
- 为了降低内核出错的风险,操作系统设计人员可以最小化在Supervior mode下运行的操作系统代码量,并在User mode下执行大部分操作系统代码。这种内核组织称为微内核。
2.5 进程概览
- xv6使用页表(硬件实现)来为每个进程提供其独有的地址空间。页表将虚拟地址映射为物理地址。xv6为每个进程维护不同的页表,一片地址空间包含了从虚拟地址0开始的用户内存。它的地址最低处放置进程的指令,接下来则是全局变量,栈区,以及一个用户可按需拓展的“堆”区。有许多因素限制了进程地址空间的最大大小,在Xv6这个值被定义为MAXVA
- xv6内核为每个进程维护许多状态,并将其收集到struct proc中(kernel/proc.h:86)
- 系统在进程之间切换实际上就是挂起当前运行的线程,恢复另一个进程的线程。线程的大多数状态(局部变量和函数调用的返回地址)都保存在线程的栈上。每个进程都有用户栈和内核栈(p->kstack)。
在xv6中,一个进程由一个地址空间和一个线程组成。在实际的操作系统中,一个进程可能有多个线程来利用多个cpu。
2.6 Code: 启动xv6,第一个进程和系统调用
。。。
有三种事件会导致CPU不按照原先的执行顺序执行:系统调用(ecall)、异常、硬件中断。
4.2 Trap from user space
uservec(trampoline.S) -> usertrap(trap.c) -> usertrapret(trap.c) -> userret(trampoline.S)
- 参考
xv6-lab2-syscall_Wound+=s的博客-CSDN博客
- 实现一个系统调用的跟踪
例如,要跟踪fork系统调用,程序调用trace(1 << SYS_fork),其中SYS_fork是kernel/syscall.h中的一个系统调用号 [ #define SYS_fork 1 ]
- 切到分支syscall
$ git fetch
$ git checkout syscall
$ make clean
- 以kill.c的系统调用为例:调用了syscall函数 kill() ,syscall定义在/user/user.h文件下。
每个syscall是由usys.pl自动生成为usys.S
- Makefile的编译如下:usys.pl通过perl工具生成usys.S
$U/usys.S : $U/usys.pl perl $U/usys.pl > $U/usys.S
- 做了些省略,以open函数为例
#!/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"; } entry("open");
- 生成的usys.S如下:以open函数为例
# generated by usys.pl - do not edit #include "kernel/syscall.h" .global open open: li a7, SYS_open ecall ret
finish trace syscall funnction · zion6135/xv6@cbf80c3 · GitHub
用户空间:
- 执行trace 32 grep hello README
user/trace.c里执行grep hello README 并且执行trace 32, 而trace和grep都算系统调用。
上述usys.pl通过perl生成usys.S代码,可在其中找到trace和grep的汇编实现。以trace为例
.global trace trace: li a7, SYS_trace #等价于 li a7 22, 表示将22这个数字加载到寄存器a7 ecall #系统调用 ret而执行trace系统调用(li a7 22 和 ecall之后,),程序会跳转到syscall.c的void syscall(void)函数,这里就可理解为什么要把a7赋值为22了。
执行syscall函数的时候,会将num赋值为寄存器a7的值,并通过num找到syscalls中的系统调用号对应的函数指针。从而去执行sys_trace函数!!并将执行结果赋值给寄存器a0
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; } }至此可以去调用sys_trace函数了,这里需要关注:trace如何拿到传入的参数。可见argint(0. &n); 可将传入的int参数从寄存器p->trapframe->a0中拿到。
uint64 sys_trace(void) { int n; //拿到trace传递的第一个参数,到变量n if(argint(0, &n) < 0) return -1; myproc()->trace_mask = n; return 0; }具体实现如下:argint用于拿到系统调用传递的参数,0-5的参数,对应寄存器a0-a5
// Fetch the nth 32-bit system call argument. int argint(int n, int *ip) { *ip = argraw(n); return 0; } 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; }
- MIT-6.S081-2020实验(xv6-riscv64)二:syscall - YuanZiming - 博客园
- 在用户空间:通过系统调用进入内核,需在trampoline.S文件去执行uservec函数:保存了上下文,执行trap.c文件的usertrap()函数---》执行到syscall.c的syscall()函数---》在根据syscall函数的具体实现的sys_xxx函数。
系统调用流程小结:
- 用户空间:trace调用
- (由perl生成的汇编函数:trace) 调用ecall +调用号(SYS_trace)存入寄存器a7
- 进入trampline.S【通过ecall从用户态陷入内核态】的调用syscall()函数(syscall.c)
- 这里会去拿到寄存器a7的数据,并根据 a7去调用调用号对应的系统函数
- 执行系统调用的真正实现sys_trace
实现一个系统调用sysinfo (struct sysinfo*)
struct sysinfo {
uint64 freemem; // 剩余可用的内存大小bytes
uint64 nproc; // 记录进程状态 != UNUSED的进程
};
本例子有一个测试程序user/sysinfotest.c ===>sysinfotest 打印sysinfotest ok即代表测试通过
- kalloc.c数据结构
内核程序后的第一个地址, 由kernel.ld定义 extern char end[]; // first address after kernel. // defined by kernel.ld. struct run { struct run *next; 将内存分为一块一块的,直到没有内存为止 }; struct { struct spinlock lock; struct run *freelist; 指向可用内存的链表 } kmem;
- kinit:初始化spinlock, 和将kernel后的第一个地址开始,全部整理到freelist链表。
void kinit() { initlock(&kmem.lock, "kmem"); // 初始化spinlock freerange(end, (void*)PHYSTOP); 初始化 从end到PHYSTOP的物理内存 }
- kalloc:从freelist链表取PGSIZE大小的内存去使用。并更新freelist指向的链表头。
- kfree:添加PGSIZE大小的数据到链表freelist。
- freerange:从pa_start到pa_end以PGSIZE大小为单位作为链表节点添加到freelist。
每一页的大小为PGSIZE (#define PGSIZE 4096 // bytes per page) 大小为4K
void freerange(void *pa_start, void *pa_end) { char *p; p = (char*)PGROUNDUP((uint64)pa_start); for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE) kfree(p); }注:kalloc.c操作的是直接的物理地址。并不等同于我们在系统之上分配的内存。
- 实现sysinfo的系统调用函数接口,先return 0
- 在kalloc.c中添加获取可用内存大小
- 在proc中去遍历proc[NPROC]得到所有的UBUSED的进程,从而可以得到可用进程数
- sysinfo调用接口获取到struct sysinfo的内容
- 从内核拷贝数据到userspace