xv6笔记
# xv6笔记
教程:https://xv6.dgs.zone/
实验代码:https://github.com/Wcacciatori/xv6-labs
# 第一章 操作系统接口
# 1.1 进程与内存
fork复制创建新进程
exec为替换进程内存,即fork复制过来的进程和父进程是完全一样,这样没有意义,所以需要exec来为新进程指定运行内容。
fork和exec分开的原因是中间可以进行io的重定向
# 1.2 IO与文件描述符
文件描述符是一个小整数,可以代指文件,设备,管道等等。是这些东西的抽象。进程可以通过打开一个文件,目录,设备,或创建一个管道或复制现有描述符来获取文件描述符。
每个进程都有一个文件描述符的私有空间
文件描述符“0”一般指标准输入(键盘)
文件描述符“1”一般指标准输出(屏幕)
文件描述符“3”一般指标准错误输出(屏幕)
和io相关的系统调用:
int read(int fd, char *buf, int n)//从文件描述符fd最多读取n字节存到buf中
int write(int fd, char *buf, int n)//从buf中写n字节到文件描述符fd中
dup(int oldfd)//创建一个现有文件描述符的副本指向相同的文件,拥有相同的偏移量,访问权限。
2
3
# 1.3 管道
相关系统调用:
int pipe(int p[])//创建一个管道,把read/write文件描述符放在p[0]和p[1]中
管道读端作为标准输入执行程序wc
int p[2];//声明管道读端和写端文件描述符存放的数组
char *argv[2];//存参数,一会重新给子进程指定程序用
argv[0] = "wc";
argv[1] = 0;
pipe(p);//创建一个管道,读端文件描述符为p[0]写端文件描述符为p[1]
if (fork() == 0) {//创建子进程,返回值为0进入子进程,为1继续父进程。在这里紧跟着if后面的是子进程内容,else里的是父进程要执行的
close(0);//关闭标准输入,给等下的管道读端腾地方
dup(p[0]);//复制一个管道描述符,新分配的文件描述符总是当前进程中编号最小的未使用描述符。故管道的读端替代了标准输入的位置
close(p[0]);//关掉管道的读端文件描述符
close(p[1]);//关掉管道的写端文件描述符
exec("/bin/wc", argv);//参数1:程序所在地址 参数2:程序所需参数
} else {//父进程fork完返回1,故进入else
close(p[0]);//关闭管道的读端因为用不到(父发子读)
write(p[1], "hello world\n", 12);//往管道写端写内容
close(p[1]);//写完关掉
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1.4 文件系统
相关的系统调用:
int mkdir(char *dir)//创建一个新目录
int chdir(char *dir)//改变当前的工作目录,改到dir去
int mknod(char *file, int, int)//创建一个设备文件,参数1是文件地址,参数23是主设备号和次设备号
int fstat(int fd, struct stat *st)//将打开文件fd的信息放入*st
int stat(char *file, struct stat *st)//将指定名称的文件信息放入*st
//结构体的内容:
struct stat {
int dev; // 文件系统的磁盘设备
uint ino; // Inode编号
short type; // 文件类型
short nlink; // 指向文件的链接数
uint64 size; // 文件字节数
};
int link(char *file1, char *file2)//为文件file1创建另一个名称(file2)
int unlink(char *file)//删除一个文件
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Linux系统调用之xargs:
通用命令格式:somecommand |xargs -item command
来个例子:echo "--help" | xargs cat
他等同于:cat --help
于是我们知道,xargs的作用是将管道前面的输出作为管道后面命令的参数。
# 第二章 系统调用
Lab 2-1思路:
添加一个系统调用的一般步骤
- 添加系统调用的原型,添加用户存根
- 添加系统调用编号
- 在内核空间写系统调用的功能代码
# 2.1 系统调用具体过程
通过中断实现

xv6系统调用由用户空间到内核空间的关键:entry(“trace”)相当于中断服务程序
# 第三章 页表
用户指令和内核指令使用的都是虚拟地址(页表形式)
# 3.1 硬件相关
虚拟地址和物理地址之间的转换是通过硬件(寄存器)来建立联系的。
虚拟地址64位,但是只使用低39位,39位中,高27位为页表索引值,低12位为偏移值。
通过页表索引值在页表中找到对应条目。页表条目包括44位物理地址和一些标志位。
最终的物理地址由条目中的44位做高位,虚拟地址中的偏移值做低位形成56位物理地址。
图示:

由此,每一页的大小就是虚拟地址中的偏移位形成的4096bit。索引值相当于页号。
实际的转换是通过三级页表实现的,如图:

分成三级的好处是:三级页表结构允许高效地映射大范围的虚拟地址空间到物理内存,同时在内存中只需要维护实际使用到的页表部分,节省了内存资源。
每个PTE包含标志位,这些标志位告诉分页硬件允许如何使用关联的虚拟地址。PTE_V指示PTE是否存在:如果它没有被设置,对页面的引用会导致异常(即不允许)。PTE_R控制是否允许指令读取到页面。PTE_W控制是否允许指令写入到页面。PTE_X控制CPU是否可以将页面内容解释为指令并执行它们。PTE_U控制用户模式下的指令是否被允许访问页面;如果没有设置PTE_U,PTE只能在管理模式下使用。
# 3.2 内核空间
蹦床页面,相当于中断函数处理界面前的统一处理(保存现场,切换空间,执行代码)
lab3-2
在struct proc中为进程的内核页表增加一个字段,添加了K_pagetable.为一个新进程生成一个内核页表的合理方案是实现一个修改版的kvminit,这个版本中应当创造一个新的页表而不是修改。kernel_pagetable你将会考虑在allocproc中调用这个函数。确保每一个进程的内核页表都关于该进程的内核栈有一个映射。在未修改的XV6中,所有的内核栈都在procinit中设置。你将要把这个功能部分或全部的迁移到allocproc中。修改scheduler()来加载进程的内核页表到核心的satp寄存器(参阅kvminithart来获取启发)。不要忘记在调用完w_satp()后调用sfence_vma()没有进程运行时scheduler()应当使用kernel_pagetable在freeproc中释放一个进程的内核页表。待办,没有做好。- 你需要一种方法来释放页表,而不必释放叶子物理内存页面。
- 调式页表时,也许
vmprint能派上用场 - 修改XV6本来的函数或新增函数都是允许的;你或许至少需要在***kernel/vm.c***和***kernel/proc.c***中这样做(但不要修改***kernel/vmcopyin.c***, ***kernel/stats.c***, ***user/usertests.c***, 和***user/stats.c***)
- 页表映射丢失很可能导致内核遭遇页面错误。这将导致打印一段包含
sepc=0x00000000XXXXXXXX的错误提示。你可以在***kernel/kernel.asm***通过查询XXXXXXXX来定位错误。
lab3-3


# lab中遇到的一些问题
lab1-5问题:忘记排除 . 目录,导致一直循环遍历根目录
lab1-6 make: *** No rule to make target 'user/_xargs', needed by 'fs.img'. Stop.

- 原因,把args.c目录不小心放在了user目录之外。
内核页表和内核栈的关系?栈需要空间,这个空间正是和页表对应。
释放进程内核页面时,为什么不用把叶子物理页面释放了?
- 因为你的叶子物理内存中内核区的会被其他内核页表使用。(每个进程都建立了到设备地址的映射) 程序独有的物理页面可以用之前的代码释放
- 叶子???:第三级页表的页表项
物理地址>>12=第三级页表的物理地址[55-12](0-11为0)=第二级页表项[53-10]
第二级页表项>>9=页表项的索引
lab3-2
- 当vm.c顶部头文件包含顺序如下时
#include "param.h" #include "types.h" #include "memlayout.h" #include "elf.h" #include "riscv.h" #include "defs.h" #include "fs.h" #include "proc.h" #include "spinlock.h"1
2
3
4
5
6
7
8
9
有如下报错:

当vm.c顶部头文件包含顺序如下时:
#include "param.h"
#include "types.h"
#include "memlayout.h"
#include "elf.h"
#include "riscv.h"
#include "defs.h"
#include "fs.h"
#include "spinlock.h"//把自旋锁结构定义的头文件置于proc.h之前
#include "proc.h"
2
3
4
5
6
7
8
9
报错解决。
defs.h中是对自旋锁结构的一个向前声明,spinlock.h中是自旋锁结构的定义,proc.h中使用了自旋锁结构。第一次的尝试中,先包含proc.h,也就是先使用了一下自旋锁结构,此时编译器还不知道自旋锁结构的具体定义是什么,所以会产生报错:lock是不完整的类型。第二次尝试中,自旋锁的定义放在了使用前,就没问题了。