https://github.com/mit-pdos/xv6-riscv/
一个简单,类UNIX的 MIT(麻省理工) 教学用操作系统
用户态 syscall
Xv6内核提供了Unix内核传统上提供的服务和系统调用的子集
系统调用 | 描述 |
---|---|
int fork() |
创建一个进程,返回子进程的PID |
int exit(int status) |
终止当前进程,并将状态报告给wait()函数。无返回 |
int wait(int *status) |
等待一个子进程退出; 将退出状态存入*status; 返回子进程PID。 |
int kill(int pid) |
终止对应PID的进程,返回0,或返回-1表示错误 |
int getpid() |
返回当前进程的PID |
int sleep(int n) |
暂停n个时钟节拍 |
int exec(char *file, char *argv[]) |
加载一个文件并使用参数执行它; 只有在出错时才返回 |
char *sbrk(int n) |
按n 字节增长进程的内存。返回新内存的开始 |
int open(char *file, int flags) |
打开一个文件;flags表示read/write;返回一个fd(文件描述符) |
int write(int fd, char *buf, int n) |
从buf 写n 个字节到文件描述符fd; 返回n |
int read(int fd, char *buf, int n) |
将n 个字节读入buf;返回读取的字节数;如果文件结束,返回0 |
int close(int fd) |
释放打开的文件fd |
int dup(int fd) |
返回一个新的文件描述符,指向与fd 相同的文件 |
int pipe(int p[]) |
创建一个管道,把read/write文件描述符放在p[0]和p[1]中 |
int chdir(char *dir) |
改变当前的工作目录 |
int mkdir(char *dir) |
创建一个新目录 |
int mknod(char *file, int, int) |
创建一个设备文件 |
int fstat(int fd, struct stat *st) |
将打开文件fd的信息放入*st |
int stat(char *file, struct stat *st) |
将指定名称的文件信息放入*st |
int link(char *file1, char *file2) |
为文件file1创建另一个名称(file2) |
int unlink(char *file) |
删除一个文件 |
用户态的基础接口
- 文件操作 open read write close link unlink stat fstat mkdir chdir
- 进程相关 fork dup wait pipe getpid exit kill
- log相关 printf
- 内存 malloc free
- 时间相关 uptime(获取当前的tick) sleep
文件系统
提供了一个精简版的文件系统, 支持ramdisk, 支持文件读写
可改code, 将用户态程序打包进文件系统, 将文件系统镜像融入到 kernel, 使之成为 kernel .rodata段的内容, 作为ramdisk使用
不依赖加载器, 可以将用户态应用程序和 little kernel
打包到一起
cpu mode
从M-mode 启动到 S-mode 的xv6 little kernel, 最后启动到 U-mode 的sh 终端
smp
支持多核启动
内存布局
启用mmu
kernel 为线性映射 VA 等同于 PA
Xv6 为每个进程维护一个用于描述进程的用户地址空间的页表,外加一个单独的描述内核地址空间的页表。内核配置其地址空间的布局,使其能够通过可预测的虚拟地址访问物理内存和各种硬件资源。
内核地址空间
内核对RAM和内存映射的设备寄存器使用“直接映射”,也就是将这些资源映射到和它们物理地址相同的虚拟地址上。例如,内核本身在虚拟地址空间和物理内存中的位置都是KERNBASE=0x80000000
。直接映射简化了读/写物理内存的内核代码。例如,当 fork
为子进程分配用户内存时,分配器返回该内存的物理地址;fork
在将父进程的用户内存复制到子进程时,直接使用该地址作为虚拟地址。
有几个内核虚拟地址不是直接映射:
- trampoline 页。它被映射在虚拟地址空间的顶端;用户页表也有这个映射。Xv6在内核页表和每个用户页表中的同一个虚拟地址上映射了trampoline页
- 内核栈页。每个进程都有自己的内核栈,内核栈被映射到高地址处,所以 xv6 可以在它后面留下一个未映射的守护页。守护页的 PTE 是无效的(不设置
PTE_V
位),这样如果内核栈溢出,很可能会引起异常,内核会报错。如果没有防护页,栈溢出时会覆盖其他内核内存,导致不正确的操作。
内核通过高地址映射使用它的栈空间,栈空间也可以通过直接映射的地址被内核访问。
每个用户态进程都有一个单独的页表,当 xv6 在进程间切换时,也会改变页表
用户态进程地址空间
为了检测用户栈溢出分配的栈内存,xv6 会在 stack 的下方放置一个无效的保护页。如果用户栈溢出,而进程试图使用栈下面的地址,硬件会因为该映射无效而产生一个缺页异常。
用户页表并不映射内核, 因为RISC-V硬件在trap过程中不切换页表,所以用户页表必须包含uservec
的映射,即stvec
指向的trap处理程序地址。uservec
必须切换satp
,使其指向内核页表;为了在切换后继续执行指令,uservec
必须被映射到内核页表与用户页表相同的地址。
调度
轮询调度
xv6周期性地强制切换,以应对长时间不进行sleep操作的计算进程, 用定时器中断来驱动上下文切换
sleep和wakeup允许一个进程放弃CPU,并睡眠等待某一事件,并允许另一个进程将睡眠的进程唤醒
优点
文件结构简单, 资料丰富, 编出的文件小, 不依赖加载器, 支持比较常用的posix api.