MIT-6.S081导言

本文最后更新于:2023年8月3日 晚上

MIT-6.S081

已完成:

Preparation

这部分比较麻烦,需要我们配置xv6的编译、调试环境、安装编译链等,由于GFW的原因,这个阶段会比较egg pain。比如:

  • 需要在x64机器上生成riscv的可执行文件,因此需要交叉编译器,也即是riscv-gnu-toolchain(源码8GB,动不动就网络错误)
  • qemu作为模拟器去运行xv6系统
  • 使用qemu-gdb remote进行调试
  • 拉取源码,测试使用make grade自动评分
  • ssh、虚拟机配置等等

官方的环境搭建指导:https://pdos.csail.mit.edu/6.828/2020/tools.html 部分如下所述并不同于官方的环境搭建方式:

安装编译链

使用apt安装一些依赖

1
2
$ sudo apt-get install autoconf automake autotools-dev curl libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev
$ sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu

交叉编译器(英语:Cross compiler)是指一个在某个系统平台下可以产生另一个系统平台可执行文件编译器。交叉编译器在目标系统平台(开发出来的应用程序序所运行的平台)难以或不容易编译时非常有用

由于riscv-gnu-toolchain总大小约有8GB左右,通过官方的apt和下载源码的方式都非常的慢,甚至经常中断下载,多次尝试无果后,我使用了在宿主机上下载源码包(使用idm多线程下载github的源码或者百度云现成的源码文件),之后将这一大坨.tar.gz源码复制到Ubuntu虚拟机的方式获取源码,之后进行编译

解压,之后进行编译:

1
2
3
4
$ tar zxvf *.gz
$ cd riscv-gnu-toolchain
$ ./configure --prefix=/usr/local
$ sudo make -j8

这个编译过程将会十分漫长,虚拟机配置低甚至会卡死,建议开个tmux,detach之后使用-j多核编译选项

编译之后环境就全部配好了,测试一下:

1
2
$ riscv64-unknown-elf-gcc --version #查看是不是有版本信息输出
$ qemu-system-riscv64 --version

从github下载xv6源码

1
git clone git://github.com/mit-pdos/xv6-riscv-fall19.git

编译xv6内核

1
2
$ make qemu
$

正常编译成功会得到xv6的终端的提示符,这里我遇到了版本过高的问题,导致依赖错误,无法安装,卸载高版本即可

使用Ctrl-a x退出

顺便看一下riscv-gnu-toolchain恐怖的大小,就知道为什么用git或者apt 下不下来了

1
2
3
4
$ du -hs ../*
8.0G ../riscv-gnu-toolchain
3.6G ../riscv-gnu-toolchain.tar.gz
26M ../xv6-riscv

虚拟机初始设置

  • 使用22.04 Ubuntu LTS虚拟机
  • apt换源、update、upgrade
  • 配置vmware tools
  • 安装编译依赖

ssh

我的宿主机是Windows,xv6安装在Ubuntu虚拟机中

被连接的主机必须要安装openssh服务,比如我要连接到我的Debian或者Ubuntu,那么他们都需要安装openssh

Debian或者Ubuntu的安装如下:

1
sudo apt-get install openssh-server

遇到了依赖错误,导致无法安装openssh-server,卸载openssh-client之后安装openssh-server即可

启动服务:

1
sudo service sshd start

查看是否已经启动:

1
ps -ef | grep sshd

生成公钥与私钥,:

使用key-gen命令生成公钥与私钥

1
2
3
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/c/Users/xxx/.ssh/id_rsa):

之后修改服务器的authorized_keys文件,也即将上述步骤中生成的公钥(.pub文件)写入到远程服务器的 ~/.ssh/authorized_keys文件中

run&debug

run

在Makefile所在文件目录输入

1
$ make qemu

QEMU 的虚拟 BIOS 将从包含在 xv6.img 文件中的虚拟硬盘映像加载 xv6的引导加载程序,boot loader将依次加载并运行 xv6内核

在命令行上,将会出现许多提示文字,最后一行会有一个$,表示是xv6系统的命令提示符

如果想要结束QEMU 会话,并销毁 xv6虚拟机的状态,按下Ctrl-C即可

debug

在xv6目录,打开一个shell,输入 make qemu-gdb,qemu会卡住,等待gdb与他连接

在xv6目录打开另一个shell,输入riscv64-unknown-elf-gdb,即可远程debug

参考

Lab1 Xv6 and Unix utilities

进行系统工具(比如find、xargs)的编写,主要是让你去熟悉操作系统API。这里最有趣的是primes程序,要求你使用并发的方式实现素数筛,涉及管道、进程通信、并发、递归

官方文档:https://pdos.csail.mit.edu/6.828/2020/labs/util.html

Boot xv6

1
2
3
4
5
6
7
$ git clone git clone git://g.csail.mit.edu/xv6-labs-2020 # 拉取仓库
$ cd xv6-labs-2020
$ git checkout util # 切换分支
$ make
$ make qemu
$ make grade # 自动评测所有的程序
$ ./grade-lab-util sleep # 评测单个程序sleep

sleep

Implement the UNIX program sleep for xv6; your sleep should pause for a user-specified number of ticks. A tick is a notion of time defined by the xv6 kernel, namely the time between two interrupts from the timer chip. Your solution should be in the file user/sleep.c.

  • 在Makefile中的UPROG下面加入sleep
  • 在user目录创建sleep.c文件
  • sleep.c加入的三个头文件是模仿了其他user/目录的风格
  • 首先进行参数的检查,异常则退出
  • 直接调用sleep()函数即可,可以在别的程序中找到这种用法
  • exit(0)退出程序
1
2
3
4
5
6
7
8
9
10
11
12
13
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main(int argc,char * argv[])
{
if(argc != 2){
fprintf(2, "Usage: sleep [times]\n");
exit(1);
}
sleep(atoi(argv[1]));
exit(0);
}

报错:user/sh.c:58:1: error: infinite recursion detected [-Werror=infinite-recursion],解决办法:

1
2
3
4
5
6
// Execute cmd.  Never returns.*
__attribute__((noreturn))
void
runcmd(struct cmd *cmd) {
.....
}

pingpong

Write a program that uses UNIX system calls to ‘’ping-pong’’ a byte between two processes over a pair of pipes, one for each direction. The parent should send a byte to the child; the child should print “: received ping”, where is its process ID, write the byte on the pipe to the parent, and exit; the parent should read the byte from the child, print “: received pong”, and exit. Your solution should be in the file user/pingpong.c.

  • int fd[2]创建管道,fd[0]读、fd[1]写
  • 管道用法:一般先创建一个管道,然后进程使用fork函数创建子进程,之后父进程关闭管道的读端,子进程关闭管道的写端
  • 调用fork 后,父进程的 fork() 会返回子进程的 PID,子进程的fork返回 0
  • 注意到write系统调用是ssize_t write(int fd, const void *buf, size_t count);,因此写入的字节是一个地址,由于我们声明buf是一个char类型,因此需要填入&buf
  • 这里踩了一个坑,把pingpong写成了pingpang导致错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main(int argc,char * argv[])
{
int fd1[2],fd2[2];
pipe(fd1);
pipe(fd2);
int pid = fork();

// child
// print "<pid>: received ping"
// write back to parent a byte and exit
// close fd[1]
// zero for read and 1 for read
if(pid==0)
{
close(fd1[1]);//close write
close(fd2[0]);//close read
char buf[3];
read(fd1[0],&buf,1);//read a byte from parent
printf("%d: received ping\n",getpid());
write(fd2[1],&buf,1);//write back to parent
exit(0);
}

// parent
// send a byte to the child
// close fd[0]
else
{
close(fd1[0]);//close read
close(fd2[1]);//close write
char buf[3];
write(fd1[1],"A",1);// send a byte to the child
read(fd2[0],&buf,1);
printf("%d: received pong\n",getpid());
}
exit(0);
}

primes

Write a concurrent version of prime sieve using pipes. This idea is due to Doug McIlroy, inventor of Unix pipes. The picture halfway down this page and the surrounding text explain how to do it. Your solution should be in the file user/primes.c.

Your goal is to use pipe and fork to set up the pipeline. The first process feeds the numbers 2 through 35 into the pipeline. For each prime number, you will arrange to create one process that reads from its left neighbor over a pipe and writes to its right neighbor over another pipe. Since xv6 has limited number of file descriptors and processes, the first process can stop at 35.

  • 使用pipe和fork来设置管道
  • 由于xv6文件描述符很少,所以需要关闭所有不必要的文件描述符,否则将会导致描述符耗尽
  • 主要的素数进程应该只有在所有的输出都打印出来之后,并且在所有其他的素数进程都退出之后才能退出
  • 关于read的用法:当管道的写端关闭时,read 返回零,这个可以控制while的终止条件
  • 修改Makefile 的 UPROGS

普通的C语言写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void get_primes2(){
for(int i=2;i<=n;i++){

if(!st[i]) primes[cnt++]=i;//把素数存起来
for(int j=i;j<=n;j+=i){//不管是合数还是质数,都用来筛掉后面它的倍数
st[j]=true;
}
}
}

void get_primes1(){
for(int i=2;i<=n;i++){
if(!st[i]){
primes[cnt++]=i;
for(int j=i;j<=n;j+=i) st[j]=true;//可以用质数就把所有的合数都筛掉;
}
}
}

并发编程的思路不同于以上方式,我们可以给出伪代码:

1
2
3
4
5
6
p = get a number from left neighbor
print p
loop:
n = get a number from left neighbor
if (p does not divide n)
send n to right neighbor

一个生成过程可以将数字2、3、4、 … … 35输入管道的左端: 管道中的第一个过程消除了2的倍数,第二个过程消除了3的倍数,第三个过程消除了5的倍数,依此类推

https://swtch.com/~rsc/thread/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

#define READ 0
#define WRITE 1

void foo(int *left_fd)
{
close(left_fd[WRITE]);
int prime,i;
read(left_fd[READ],&prime,sizeof(int));//第一个数字必定是素数
printf("prime %d\n",prime);//符合格式的输出

if(read(left_fd[READ],&i,sizeof(int))!=0)
{
int right_fd[2];
pipe(right_fd);
int pid=fork();
if(pid==0)
{
foo(right_fd);
}
else if(pid>0)
{
close(right_fd[READ]);
if(i%prime!=0)//注意已经读取了一个i,这里要判断一下
{
write(right_fd[WRITE],&i,sizeof(int));
}
while(read(left_fd[READ],&i,sizeof(int))!=0)
{
if(i%prime!=0)
{
write(right_fd[WRITE],&i,sizeof(int));
}
}
close(right_fd[WRITE]);
close(left_fd[READ]);
wait(0);//父进程要等待子进程
}
}
}

int main(int argc,char * argv[])
{
int left_fd[2];
pipe(left_fd);

int pid = fork();

if(pid==0)//child
{
foo(left_fd);
}
else if(pid >0)
{
close(left_fd[READ]);
int i;
for(i=2;i<=35;i++)//第一轮,把2~35全都传递给右边
{
write(left_fd[WRITE],&i,sizeof(int));
}
close(left_fd[WRITE]);
wait(0);//主要的素数进程应该只有在所有的输出都打印出来之后,并且在所有其他的素数进程都退出之后才能退出
}

exit(0);
}

find

Write a simple version of the UNIX find program: find all the files in a directory tree whose name matches a string. Your solution should be in the file user/find.c.

  • 功能:查找目录树中名称与字符串匹配的所有文件
  • 可以参考user/ls.c 以了解如何读取目录
  • 避开...的递归

思路:

打开目录,使用while循环得到目录下所有文件/子目录的dirent结构体de

  • 如果是de.name是.或者..,那么直接continue跳过
  • 如果使用字符串拼接获取文件的相对路径buf
    • 如果buf的st类型是文件类型,且文件名字和我们需要搜索的文件名字相同,那么就直接输出文件信息,这就是我们要找的文件
    • 如果buf的st类型是目录,且不为空,那么就继续递归搜索下去

涉及到read、fstat等函数的使用

涉及到dirent、stat结构体的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"

void find(char * path,char *file)
{
int fd;
struct dirent de;
struct stat st;
char buf[512], *p;

if((fd = open(path, 0)) < 0){//打开目录,获得对应的文件描述符
fprintf(2, "find: cannot open %s\n", path);
return;
}

if(fstat(fd, &st) < 0){
fprintf(2, "find: cannot stat %s\n", path);
close(fd);
return;
}

while(read(fd, &de, sizeof(de)) == sizeof(de))//使用while循环得到目录下所有文件/子目录的dirent结构体de
{
if(strcmp(de.name,".")==0||strcmp(de.name,"..")==0)
continue;//跳过

strcpy(buf, path);
p = buf+strlen(buf);
*p++ = '/';
char *pp=de.name;
while(*pp!=0) //字符串拼接
{
*p++ =*pp++;
}
*p=0;//别忘了加终止符

if(stat(buf, &st) < 0)//获取buf的st
{
printf("find: cannot stat %s\n", buf);
continue;
}
if(st.type==T_FILE)//如果buf的st类型是文件类型
{
if(strcmp(de.name,file)==0)//如果文件名字和我们需要搜索的文件名字相同,那么就直接输出文件信息
{
printf("%s\n",buf);
}
}
else if(st.type==T_DIR)//如果buf的st类型是目录类型
{
if (de.inum == 0) //目录下没有文件,如果缺少这个语句xargs命令将会报错
continue;
find(buf,file);//递归,继续搜索
}

}
}

int main(int argc, char *argv[])
{
char * path=argv[1];//查找目录
char * file=argv[2];//查找文件

find(path,file);

exit(0);
}

xargs

Write a simple version of the UNIX xargs program: read lines from the standard input and run a command for each line, supplying the line as arguments to the command. Your solution should be in the file user/xargs.c.

  • 对于管道连接的命令,以find . b | xargs grep hello为例,argv[0]是 xargs,argv[1]是grep,一共有三个参数,如果想要读取find . b,那么需要从标准输入中读取,也即是使用read函数读取fd为0时的数据
  • 每一次读取一行,将该行所有空格替换为\0,这样命令就可以被分割。然后将argv[]指向这些命令。如果遇到换行符,执行fork,父进程等待子进程结束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
#include "kernel/param.h"

#define STDIN 0

int main(int argc,char *argv[])
{
char buf[1024];
char c;
char *Argv[MAXARG];
int index=0;
int i;

for(i=1;i<=argc-1;i++)
{
Argv[i-1]=argv[i];//ignore xargs(argv[0])
}

while(1)
{
index=0;
memset(buf,0,sizeof(buf));
char *p=buf;
i=argc-1;//注意i要写在这里
while(1)
{
int num=read(STDIN,&c,1);//读取标准输入,注意是&c
if(num!=1)
exit(0);//程序的终止条件
if(c==' '||c=='\n')
{
buf[index++]='\0';
Argv[i++]=p;//参数
p=&buf[index];//更新参数首地址
if(c=='\n')
break;
}
else //character
{
buf[index++]=c;
}
}
Argv[i]=0;
int pid = fork();
if(pid==0)
{
exec(Argv[0],Argv);
}
else
{
wait(0);
}
}
exit(0);
}

Lab2 syscall System calls

前置知识

建议听课+熟读xv6手册,并理解清楚lab要求

先切换到syscall分支,并在sh.c中加入代码:

1
2
3
4
5
6
// Execute cmd.  Never returns.*
__attribute__((noreturn))
void
runcmd(struct cmd *cmd) {
.....
}

这样做是为了防止报递归错

这个实验要求我们添加两个系统调用,所以我们需要先搞清楚riscv架构的系统调用流程

用户执行trace命令 - > ecall <SYS_trace> - > 触发trap - > 调用sys_trace

来自用户空间的系统调用、中断、异常都可以引发trap,来自用户空间的trap的处理路径是:

  • uservec(kernel/trampoline.S:16)
  • usertrap(kernel/trap.c:37)
  • usertrapret(kernel/trap.c:90) 返回时
  • userret(kernel/trampoline.S:16)

详细的内容见xv6文档

System call tracing

为了添加trace系统调用,我们需要:

  • 在Makefile中添加$U/_trace,这样的话就可以编译出可执行文件,从而在用户命令行中调用trace命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
UPROGS=\
$U/_cat\
$U/_echo\
$U/_forktest\
$U/_grep\
$U/_init\
$U/_kill\
$U/_ln\
$U/_ls\
$U/_mkdir\
$U/_rm\
$U/_sh\
$U/_stressfs\
$U/_usertests\
$U/_grind\
$U/_wc\
$U/_zombie\
$U/_trace
  • 将trace系统调用的原型添加到 user/user.h
1
int trace(int);
  • trace的stub添加到 user/usys.pl
1
entry("trace");

usys.pl文件会生成usys.S文件,形如:

1
2
3
4
5
6
#include "kernel/syscall.h"
.global trace
trace:
li a7, SYS_trace
ecall
ret
  • 在syscall.h中添加trace的系统调用号
1
#define SYS_trace  22
  • 在syscall.c中添加
1
2
3
4
5
6
7
extern uint64 sys_trace(void);

static uint64 (*syscalls[])(void) = {
.....

[SYS_trace] sys_trace,
}
  • 在proc.h的proc结构体中添加字段mask
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Per-process state
struct proc {
struct spinlock lock;

// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID

// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)

int mask;
};
  • 在sysproc.c中添加sys_trace,照猫画虎,可以参考fork的实现。其中argint获取trace的参数,也即是mask。将当前进程的mask字段设置为要追踪的参数
1
2
3
4
5
6
7
8
9
uint64
sys_trace(void)
{
int mask;
if(argint(0, &mask) < 0)//获取trace的参数:mask
return -1;
myproc()->mask=mask;
return 0;
}
  • 修改 fork ()(参见 kernel/proc.c) ,跟踪掩码从父进程复制到子进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Create a new process, copying the parent.
// Sets up child kernel stack to return as if from fork() system call.
int
fork(void)
{
int i, pid;
struct proc *np;
struct proc *p = myproc();

// Allocate process.
if((np = allocproc()) == 0){
return -1;
}

// Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
np->sz = p->sz;

np->parent = p;

// copy saved user registers.
*(np->trapframe) = *(p->trapframe);

// Cause fork to return 0 in the child.
np->trapframe->a0 = 0;

// increment reference counts on open file descriptors.
for(i = 0; i < NOFILE; i++)
if(p->ofile[i])
np->ofile[i] = filedup(p->ofile[i]);
np->cwd = idup(p->cwd);

safestrcpy(np->name, p->name, sizeof(p->name));

pid = np->pid;

np->state = RUNNABLE;

release(&np->lock);

np->mask=p->mask;//给子进程的mask赋值

return pid;
}
  • 添加要索引到的系统调用名称数组
1
2
3
4
5
char  *syscall_name[] = {
"","fork", "exit", "wait", "pipe", "read",
"kill", "exec", "fstat", "chdir", "dup", "getpid", "sbrk", "sleep",
"uptime", "open", "write", "mknod", "unlink", "link", "mkdir","close","trace","sysinfo"
};
  • 修改 kernel/syscall.c 中的 syscall ()函数以打印跟踪输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void
syscall(void)
{
int num;
struct proc *p = myproc();//myproc():宏,当前的进程
// trap 代码将用户寄存器保存到当前进程的 trapframe 中
num = p->trapframe->a7;//num是系统调用号
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num]();//a0保存返回值
if((p->mask>>num)&1)
printf("%d: syscall %s -> %d\n",p->pid,syscall_name[num],p->trapframe->a0);//进程id、系统调用的名称、返回值
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}

/*

$ trace 32 grep hello README
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0

*/

Sysinfo

In this assignment you will add a system call, sysinfo, that collects information about the running system. The system call takes one argument: a pointer to a struct sysinfo (see kernel/sysinfo.h). The kernel should fill out the fields of this struct: the freemem field should be set to the number of bytes of free memory, and the nproc field should be set to the number of processes whose state is not UNUSED. We provide a test program sysinfotest; you pass this assignment if it prints “sysinfotest: OK”.

任务:添加一个系统调用 sysinfo,它收集关于正在运行的系统的信息

  • 系统调用有一个参数: 一个指向 struct sysinfo 的指针(参见 kernel/sysinfo.h)
  • 内核应该填充这个结构的字段:
    • freemem 字段应该设置为可用内存的字节数
    • nproc 字段应该设置为状态不是 UNUSED 的进程数

Sysinfo 需要将一个 struct sysinfo 复制回用户空间; 请参阅 sys_fstat ()(kernel/sysfile.c)和 filestat()(kernel/file.c)以获得如何使用 copy out ()实现这一点的示例。

  • 添加$U/_sysinfotest
  • 为了收集空闲内存量,向 kernel/kalloc.c 添加一个函数getFreeByte()

这块重点是理解run、kmem结构体的实现,并知道访问资源的时候需要加锁。可以参考一下kalloc.c文件中别的函数的实现,很多地方直接套用就行

  • kmem.freelist是空闲链表,当链表的next指针不为空时说明有一个空闲资源块,遍历次数就是页的数量
  • 由于要求求出字节,因此需要return PGSIZE*total;(每页的字节数乘以total)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int getFreeByte()
{
struct run *p;
int total=0;

acquire(&kmem.lock);//加锁
p=kmem.freelist;
while(p)
{
total++;
p=p->next;
}
release(&kmem.lock);
return PGSIZE*total;//每页的字节数*total
}
  • 为了收集进程数,向 kernel/proc.c 添加一个函数getProcNum()

proc.c中有一个全局变量proc[NPROC],proc[0]是第一个进程,proc[NPROC-1]是最后一个进程,直接for循环遍历即可得到所有的进程结构体的指针,然后取出该指针的state字段和UNUSED比较即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
struct proc proc[NPROC];

struct proc {
struct spinlock lock;

// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID

// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
int mask;
};

int getProcNum()
{
int total=0;
struct proc *p;

for(p = proc; p < &proc[NPROC]; p++)
{
acquire(&p->lock);
if(p->state!=UNUSED)
total++;
release(&p->lock);
}
return total;
}
  • 在sysproc.c中添加函数sys_sysinfo(void)
    • 使用argaddr函数 从 trapframe 中以指针的形式检索第 n 个系统调用,关于argaddr函数的用法,在文件的别的函数处可以参考
    • 声明一个sysinfo类型的结构体变量,然后使用之前写好的两个函数对它进行赋值
    • 由于需要将一个sysinfo类型的结构体变量赋值回用户空间,因此需要使用copyout函数,注意这个函数的用法,比较难写对。一定要有if(xxx) return -1 ;的形式,不然的话会报错(在这里卡了很久)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
uint64
sys_sysinfo(void)
{
/*int sysinfo(struct sysinfo *)
struct sysinfo {
uint64 freemem; // amount of free memory (bytes)
uint64 nproc; // number of process
};
*/

struct proc *p=myproc();
uint64 addr;
struct sysinfo res;//注意这里不可以是指针,需要声明一个sysinfo类型的结构体变量

if(argaddr(0, &addr) < 0)//addr是指向sysinfo结构体的指针
return -1;

res.freemem=getFreeByte();
res.nproc=getProcNum();

if(copyout(p->pagetable, addr, (char*)&res, sizeof(res))<0)
return -1;

/* 如果这样写会失败
copyout(p->pagetable, addr, (char*)&res, sizeof(res));
*/
return 0;
}

有一些头文件的添加,函数或者结构体的声明、定义的细节,暂且略过

Lab3 pgtable

写在lab开始之前:

  • 这个lab很明显和前面不是一个难度
  • 需要补充一些虚拟内存的知识
    • 看一下配套的xv6指导手册
    • B站的课程视频录播

页表

虚拟地址中间的index有27位,因此页表项就有2^27个,也即是0~2^27-1,将index对应的PPN取出和offset拼在一起就成了PA(physical address)

xv6 运行在 Sv39 RISC-V 上,这意味着只使用 64 位虚拟地址的底部 39 位,顶部 25 位未被使用。因此下面图中只需要关注低39位

刚刚是单级页表,但是出于一些考虑,xv6使用三级页表的映射方式

Q:3级page table为什么会比一个超大的page table更好呢?

Frans教授:这是个好问题,这的原因是,3级page table中,大量的PTE都可以不存储。比如,对于最高级的page table里面,如果一个PTE为空,那么你就完全不用创建它对应的中间级和最底层page table,以及里面的PTE。所以,这就是像是在整个虚拟地址空间中的一大段地址完全不需要有映射一样。

3级page table就像是按需分配这些映射块3级page table就像是按需分配这些映射块

对于页表的每一项的内容,0~63位如下

xv6中定义的一些宏如下,和pte作运算之后可以得到页表的某一位,比如pte & PTE_V可以得知页表是否有效

1
2
3
4
#define PTE_V (1L << 0) // valid 		00001
#define PTE_R (1L << 1) // readable 00010
#define PTE_W (1L << 2) // writeable 00100
#define PTE_X (1L << 3) // executable 01000

PA2PTE(pa)将虚拟地址右移截断12位之后再左移10位,可以得到对应的页表项

PTE2PA(pte)将页表项右移截断10位之后再左移12位,可以得到对应的虚拟地址

1
2
3
#define PA2PTE(pa) ((((uint64)pa) >> 12) << 10)

#define PTE2PA(pte) (((pte) >> 10) << 12)

在sh.c中加入代码:

1
2
3
4
5
6
// Execute cmd.  Never returns.*
__attribute__((noreturn))
void
runcmd(struct cmd *cmd) {
.....
}

这样做是为了防止报递归错

在exec.c中加入下面代码,以打印出进程1的页表

1
2
3
4
5
if(p->pid==1) 
vmprint(p->pagetable);

return argc; // this ends up in a0, the first argument to main(argc, argv)

在 kernel/defs.h 中定义 vmprint 的原型

1
2
3
// vm.c
void vmprint(pagetable_t);
void dfs_vmprint(pagetable_t ,int );

我们需要在vm.c中编写vmprint函数的代码

先观察一下给出的输出

1
2
3
4
5
6
7
8
9
10
page table 0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
.. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000

对该输出格式的几点解释:

  • 第一行格式是page table %p,%p是一级页表入口的地址,pagetable[i]就是一级页表第i项
  • 第二行开始,是dfs的结果。从0到512进行循环,看pte是否有效,如果有效那么找到pte指向的下一个条目,直到找到叶子节点
  • 顶级页表页面只有条目0和255的映射。条目0的下一个级别的页表只映射了索引0,而该索引0的底层则映射了条目0、1和2。顶级页表255同理

先写出vmprint函数,把第一行打印出来,然后进入dfs循环

1
2
3
4
5
6
void 
vmprint(pagetable_t pagetable)
{
printf("page table %p\n", pagetable);
dfs_vmprint(pagetable, 0);
}

pte & PTE_V用来判断页表是否有效,如果有效就先将对应的所有信息打印出来

pte & (PTE_R|PTE_W|PTE_X)) == 0用来判断是否有下一级的页表,如果有的话,就将该pte转化为虚拟地址,进入递归

写这部分代码要仔细参考阅读源码中的freewalk函数,很有帮助

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void dfs_vmprint(pagetable_t pagetable,int level)
{
// there are 2^9 = 512 PTEs in a page table.
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
if((pte & PTE_V)){//只要有效就打印出来
// this PTE points to a lower-level page table.

printf("..");//输出..的格式控制
for(int j=0;j<level;j++) printf(" ..");

printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte));//输出页表项信息

if((pte & (PTE_R|PTE_W|PTE_X)) == 0)//如果有下一级的页表
{
uint64 child = PTE2PA(pte);
dfs_vmprint((pagetable_t)child, level+1);
}
}
}
}

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ make qemu
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

xv6 kernel is booting

hart 2 starting
hart 1 starting
page table 0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
.. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
init: starting sh
$

A kernel page table per process

Xv6只有一个内核页表,只要在内核中执行,就会使用它。内核页表是物理地址的直接映射,也即是内核虚拟地址 x 映射到物理地址 x。每个进程也会有自己的用户页表,该页表只包含此进程的用户空间的地址映射。而这些用户页表和用户地址在内核页表中是不存在的。因此就会出现一个问题:假设用户进程调用write系统调用并传递一个指针,由于该指针是用户级的地址,在内核中是无效的,因此实际过程中会先将用户级的虚拟地址转换为物理地址,这样就会造成一定程度的开销。本任务就是为了解决这个问题,让每个用户级的进程都有一个内核页表

任务:修改内核,以便每个进程在内核中执行时使用其自己的内核页表副本

我们自然想到要在 struct proc中加上进程的内核页表条目 kpgtable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Per-process state
struct proc {
struct spinlock lock;

// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID

// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
pagetable_t kpgtable; // Kernel page table
};

后面要用的几个重要函数

函数名称 作用
walk 通过虚拟地址得到 PTE
mappages 将虚拟地址映射到物理地址
copyin 将用户虚拟地址的数据复制到内核空间地址
copyout 将内核数据复制到用户虚拟地址
kvminit 创建内核的页表,对全局的kernel_pagetable进行映射
kvmmap 调用 mappages,将一个虚拟地址范围映射到一个物理地址范围
kvminithart 映射内核页表。它将根页表页的物理地址写入寄存器 satp 中
procinit 为每个进程分配一个内核栈
kalloc 分配物理页
proc_pagetable(struct proc *p) 为给定的进程创建用户态页表
kvminithart 映射内核页表。它将根页表页的物理地址写入寄存器 satp 中

相关函数调用链:

1
2
3
4
main 
kvminit -> kvmmap
procinit
userinit -> allocproc

之后,我们要对进程的内核页表进行初始化

参考kvminit函数和kvmmap函数,这两个函数用来添加对全局kernel_pagetable的映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
* create a direct-map page table for the kernel.
*/
void
kvminit()
{
kernel_pagetable = (pagetable_t) kalloc();
memset(kernel_pagetable, 0, PGSIZE);

// uart registers
kvmmap(UART0, UART0, PGSIZE, PTE_R | PTE_W);

// virtio mmio disk interface
kvmmap(VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

// CLINT
kvmmap(CLINT, CLINT, 0x10000, PTE_R | PTE_W);

// PLIC
kvmmap(PLIC, PLIC, 0x400000, PTE_R | PTE_W);

// map kernel text executable and read-only.
kvmmap(KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);

// map kernel data and the physical RAM we'll make use of.
kvmmap((uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);

// map the trampoline for trap entry/exit to
// the highest virtual address in the kernel.
kvmmap(TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
}

// add a mapping to the kernel page table.
// only used when booting.
// does not flush TLB or enable paging.
void
kvmmap(uint64 va, uint64 pa, uint64 sz, int perm)
{
if(mappages(kernel_pagetable, va, sz, pa, perm) != 0)
panic("kvmmap");
}

类似的,我们创建ukvmmap和ukvminit这两个函数,完成对进程结构体中的内核页表的映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void
ukvmmap(pagetable_t kpgtable,uint64 va, uint64 pa, uint64 sz, int perm)
{
if(mappages(kpgtable, va, sz, pa, perm) != 0)
panic("ukvmmap");
}

pagetable_t
ukvminit()
{
pagetable_t kpgtable=(pagetable_t) kalloc();
memset(kpgtable, 0, PGSIZE);
ukvmmap(kpgtable, UART0, UART0, PGSIZE, PTE_R | PTE_W);
ukvmmap(kpgtable, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
ukvmmap(kpgtable, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
ukvmmap(kpgtable, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
ukvmmap(kpgtable, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
ukvmmap(kpgtable, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
ukvmmap(kpgtable, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
return kpgtable;
}

在defs.h中需要添加

1
2
void            ukvmmap(pagetable_t ,uint64 , uint64 , uint64 , int );
pagetable_t ukvminit();

根据前面函数调用的分析,我们可以发现这个main中userinit -> allocproc的调用。allocproc函数返回一个未被利用的进程,并且做初始化工作,比如下面的这一些代码来自allocproc,做了用户页表的初始化工作

1
2
3
4
5
6
7
// An empty user page table.
p->pagetable = proc_pagetable(p);
if(p->pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}

同样的,既然已经在结构体里面加了kpgtable,那么也要给进程的内核态页表做初始化工作

1
2
3
4
5
6
p->kpgtable=ukvminit();
if(p->kpgtable == 0){
freeproc(p);
release(&p->lock);
return 0;
}

在procinit函数中,为了给进程初始化内核栈,有这样一段代码:

1
2
3
4
5
6
7
8
9
// Allocate a page for the process's kernel stack.
// Map it high in memory, followed by an invalid
// guard page.
char *pa = kalloc();
if(pa == 0)
panic("kalloc");
uint64 va = KSTACK((int) (p - proc));
kvmmap(va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
p->kstack = va;

类似的,在allocproc函数中,我们需要初始化内核栈并将其映射到进程单独的内核页表,需要加上:(在这之后可以将procinit中初始化内核栈的代码注释掉)

1
2
3
4
5
6
7
8
9
10
11
12
p->kpgtable=ukvminit();
if(p->kpgtable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
char *pa = kalloc();
if(pa == 0)
panic("kalloc");
uint64 va = KSTACK((int) (p - proc));
ukvmmap(p->kpgtable, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
p->kstack = va;

freeproc函数,要将进程的内核页表进行清除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// free a proc structure and the data hanging from it,
// including user pages.
// p->lock must be held.
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
/* add here */
if (p->kstack)
{
pte_t* pte = walk(p->kpgtable, p->kstack, 0);
if (pte == 0)
panic("freeproc: walk");
kfree((void*)PTE2PA(*pte));
}
p->kstack = 0;

if (p->kpgtable)
freewalk_kproc(p->kpgtable);
/* add here */
p->pagetable = 0;
p->sz = 0;
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->chan = 0;
p->killed = 0;
p->xstate = 0;
p->state = UNUSED;
}

我们需要模仿freewalk重新实现freewalk_kproc,使得仅仅解除页表的内存映射而不像freewalk一样清除物理页数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void
freewalk_kproc(pagetable_t pagetable) {
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
if((pte & PTE_V)){
pagetable[i] = 0;
if ((pte & (PTE_R|PTE_W|PTE_X)) == 0)
{
uint64 child = PTE2PA(pte);
freewalk_kproc((pagetable_t)child);
}
}
}
kfree((void*)pagetable);
}

在调度程序scheduler中,调度程序之前,我们需要切换到进程自己的内核页表处,并使用sfence.vma刷新当前 CPU 的 TLB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// Per-CPU process scheduler.
// Each CPU calls scheduler() after setting itself up.
// Scheduler never returns. It loops, doing:
// - choose a process to run.
// - swtch to start running that process.
// - eventually that process transfers control
// via swtch back to the scheduler.
void
scheduler(void)
{
struct proc *p;
struct cpu *c = mycpu();

c->proc = 0;
for(;;){
// Avoid deadlock by ensuring that devices can interrupt.
intr_on();

int found = 0;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == RUNNABLE) {
// Switch to chosen process. It is the process's job
// to release its lock and then reacquire it
// before jumping back to us.
p->state = RUNNING;
c->proc = p;
//切换到进程自己的内核页表处
w_satp(MAKE_SATP(p->kpgtable));
//使用sfence.vma刷新当前 CPU 的 TLB
sfence_vma();
//调度进程
swtch(&c->context, &p->context);
//切换回内核页表
kvminithart();

// Process is done running for now.
// It should have changed its p->state before coming back.
c->proc = 0;

found = 1;
}
release(&p->lock);
}
#if !defined (LAB_FS)
if(found == 0) {
intr_on();
asm volatile("wfi");
}
#else
;
#endif
}
}

给 vm.c 添加头文件:

1
2
#include "spinlock.h"
#include "proc.h"//如果顺序颠倒会报结构体定义错误

修改 kvmpa 函数

1
pte = walk(myproc()->kpgtable, va, 0);

Simplify copyin/copyinstr

内核的 copyin 函数读取用户指针指向的内存。它通过将它们转换为物理地址来实现这一点,而内核可以直接取消对它们的引用。它通过在软件中遍历过程页表来执行这种转换。在实验室的这一部分中,您的工作是向每个进程的内核页表(在前一节中创建)添加用户映射,以允许 copyin (以及相关的字符串函数 copinstr)直接取消引用用户指针。

首先声明copyin_new和copyinstr_new

1
2
3
int copyin_new(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len);

int copyinstr_new(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max);

修改copyin函数,使得改为执行copyin_new

1
2
3
4
5
6
7
8
9
10
11
int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
return copyin_new(pagetable, dst, srcva, len);
}

int
copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
{
return copyinstr_new(pagetable, dst, srcva, max);
}

增加一个u2kvmcopy函数,完成页表的复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void 
u2kvmcopy(pagetable_t upagetable, pagetable_t kpagetable, uint64 oldsz, uint64 newsz)
{
oldsz = PGROUNDUP(oldsz);
for (uint64 i = oldsz; i < newsz; i += PGSIZE) {
pte_t* pte_from = walk(upagetable, i, 0);
if(pte_from == 0)
panic("pte_from");
pte_t* pte_to = walk(kpagetable, i, 1);
if(pte_to == 0)
panic("pte_to");
uint64 pa = PTE2PA(*pte_from);
uint flag = (PTE_FLAGS(*pte_from)) & (~PTE_U);
*pte_to = PA2PTE(pa) | flag;
}
}

userinit加上:

1
u2kvmcopy(np->pagetable, np->kpgtable, 0, np->sz);

fork加上:

1
u2kvmcopy(np->pagetable, np->kpgtable, 0, np->sz);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Create a new process, copying the parent.
// Sets up child kernel stack to return as if from fork() system call.
int
fork(void)
{
int i, pid;
struct proc *np;
struct proc *p = myproc();

// Allocate process.
if((np = allocproc()) == 0){
return -1;
}

// Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
np->sz = p->sz;

np->parent = p;

// copy saved user registers.
*(np->trapframe) = *(p->trapframe);

// Cause fork to return 0 in the child.
np->trapframe->a0 = 0;

// increment reference counts on open file descriptors.
for(i = 0; i < NOFILE; i++)
if(p->ofile[i])
np->ofile[i] = filedup(p->ofile[i]);
np->cwd = idup(p->cwd);
u2kvmcopy(np->pagetable, np->kpgtable, 0, np->sz);
safestrcpy(np->name, p->name, sizeof(p->name));

pid = np->pid;

np->state = RUNNABLE;

release(&np->lock);

return pid;
}

在exec函数加上

1
2
3
4
5
6
7
8
9
10
11
12
13
u2kvmcopy(np->pagetable, np->kpgtable, 0, np->sz);
// Push argument strings, prepare rest of stack in ustack.
for(argc = 0; argv[argc]; argc++) {
if(argc >= MAXARG)
goto bad;
sp -= strlen(argv[argc]) + 1;
sp -= sp % 16; // riscv sp must be 16-byte aligned
if(sp < stackbase)
goto bad;
if(copyout(pagetable, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
goto bad;
ustack[argc] = sp;
}

growproc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Grow or shrink user memory by n bytes.
// Return 0 on success, -1 on failure.
int
growproc(int n)
{
uint sz;
struct proc *p = myproc();

sz = p->sz;
if(n > 0){
if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
return -1;
}
u2kvmcopy(p->pagetable, p->kpgtable, sz-n, sz);
} else if(n < 0){
sz = uvmdealloc(p->pagetable, sz, sz + n);
}
p->sz = sz;
return 0;
}

参考

Lab4 trap

前置准备

  • 看完p4、p5

  • 搞清楚lab要求

  • 对riscv指令集、函数调用约定、trap机制有一定理解

RISC-V assembly (easy)

通过make fs.img 命令可以将user/call.c文件转化为user/call.asm文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int g(int x) {
return x+3;
}

int f(int x) {
return g(x);
}

void main(void) {
printf("%d %d\n", f(8)+1, 13);
exit(0);
}

不难观察出:

  1. s0寄存器(也称为fp或帧指针):它用于保存当前函数的帧指针,即指向当前函数栈帧的基址。在函数调用过程中,s0寄存器通常被用于存储上一个函数的栈帧基址,以便在函数返回时恢复调用者的栈帧。
  2. sp寄存器(也称为堆栈指针):它用于指示当前的栈顶位置,即栈指针。栈是一种后进先出(LIFO)的数据结构,用于存储局部变量、函数调用信息和其他临时数据。在函数调用期间,栈会动态增长和收缩,而sp寄存器会随着栈的变化而移动。
  3. a0保存第一个参数,同时具有存储返回值的用途(也即是x,返回x+3)。a1存储第二个参数,以此类推
  4. f函数被内联优化,没有新开辟栈帧,它的内容和g函数相同
  5. ra寄存器存储函数的返回地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
user/_call:     file format elf64-littleriscv


Disassembly of section .text:

0000000000000000 <g>:
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int g(int x) {
0: 1141 addi sp,sp,-16 #给栈指针分配内存
2: e422 sd s0,8(sp) #保存s0寄存器的值
4: 0800 addi s0,sp,16
return x+3;
}
6: 250d addiw a0,a0,3 # a0是第一个参数,也即是x,返回x+3
8: 6422 ld s0,8(sp)
a: 0141 addi sp,sp,16
c: 8082 ret

000000000000000e <f>:
#f函数被内联优化,没有新开辟栈帧,它的内容和g函数相同
int f(int x) {
e: 1141 addi sp,sp,-16
10: e422 sd s0,8(sp)
12: 0800 addi s0,sp,16
return g(x);
}
14: 250d addiw a0,a0,3
16: 6422 ld s0,8(sp)
18: 0141 addi sp,sp,16
1a: 8082 ret

000000000000001c <main>:

void main(void) {
1c: 1141 addi sp,sp,-16 # 初始化工作
1e: e406 sd ra,8(sp)
20: e022 sd s0,0(sp)
22: 0800 addi s0,sp,16
printf("%d %d\n", f(8)+1, 13);
24: 4635 li a2,13
26: 45b1 li a1,12 # f(8)+1
28: 00000517 auipc a0,0x0
2c: 79050513 addi a0,a0,1936 # 7b8 <malloc+0xea>
30: 00000097 auipc ra,0x0
34: 5e6080e7 jalr 1510(ra) # 616 <printf>
exit(0);
38: 4501 li a0,0
3a: 00000097 auipc ra,0x0
3e: 274080e7 jalr 628(ra) # 2ae <exit>

Q1

Which registers contain arguments to functions? For example, which register holds 13 in main’s call to printf?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
reg    | name  | saver  | description
-------+-------+--------+------------
x0 | zero | | hardwired zero
x1 | ra | caller | return address
x2 | sp | callee | stack pointer
x3 | gp | | global pointer
x4 | tp | | thread pointer
x5-7 | t0-2 | caller | temporary registers
x8 | s0/fp | callee | saved register / frame pointer
x9 | s1 | callee | saved register
x10-11 | a0-1 | caller | function arguments / return values
x12-17 | a2-7 | caller | function arguments
x18-27 | s2-11 | callee | saved registers
x28-31 | t3-6 | caller | temporary registers
pc | | | program counter

a0保存第一个参数,同时具有存储返回值的用途(也即是x,返回x+3)。a1存储第二个参数,以此类推

Q2

Where is the call to function f in the assembly code for main? Where is the call to g? (Hint: the compiler may inline functions.)

代码li a1,12 直接将f(8)+1的结果存入a1中,并没有调用函数的过程,这里存在编译器的优化

Q3

At what address is the function printf located?

1
2
3
4
5
6
0000000000000616 <printf>:

void
printf(const char *fmt, ...)
{
616: 711d addi sp,sp,-96

616

jalr 1510(ra)指令用于调用printf函数,其中偏移量1510是相对于全局地址0x0的偏移。(全局地址不确定)

Q4

Run the following code.

1
2
3
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);

What is the output? Here’s an ASCII table that maps bytes to characters.

The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?

Here’s a description of little- and big-endian and a more whimsical description.

57616是0xE110

%s打印出&i指向十六进制数对应的ASCII码,遇0停止打印。变量i存储了字符串”rld\0”

最终打印出HE110 World

Q5

In the following code, what is going to be printed after 'y='? (note: the answer is not a specific value.) Why does this happen?

1
printf("x=%d y=%d", 3);

y=a2寄存器的值

这里printf函数相当于有三个参数,第一个是字符串(a0保存),第二个是3(a1保存),第三个((a2保存))

Backtrace (moderate)

要求:在 kernel/printf.c 中实现一个 backtrace ()函数。在 sys _ sleep 中插入对此函数的调用,然后运行 bttest,它调用 sys _ sleep。你的输出应该如下:

1
2
3
4
backtrace:
0x000000080002cda
0x000000080002bb6
0x0000000080002898

编译器在每个堆栈帧中放入一个包含调用者帧指针地址的帧指针。您的回溯应该使用这些帧指针来遍历堆栈,并在每个堆栈帧中打印保存的返回地址。

补充一下基础知识:

栈指针:sp(stack pointer),指向栈的低地址

帧指针:fp(frame pointer),指向栈的高地址

fp-8:返回地址

fp-16:preview fp

内核会为栈分配一页的内存,因此可以使用 PGROUNDUP(fp)和 PGROUNDDOWN(fp)定位栈的上下边界,以终止循环

具体的栈的内存布局参考lecture中教授手画图

  • 将backtrace的原型添加到 kernel/defs.h,这样就可以在 sys _ sleep 中调用backtrace

    • 在defs.h中添加void backtrace(void);
  • GCC 编译器将当前正在执行的函数的帧指针存储在寄存器 s0中。向 kernel/riscv.h 添加以下函数,并在backtrace中调用这个函数来读取当前帧指针。这个函数使用内联来读取 s0:

1
2
3
4
5
6
7
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}

在printf.c文件中添加函数:

1
2
3
4
5
6
7
8
9
10
11
12
void backtrace(void)
{
uint64 fp=r_fp();//get fp
uint64 up_addr=PGROUNDUP(fp);//up bound
printf("backtrace:\n");//fmt output
while(fp<up_addr)
{
uint64 ret_addr=*(uint64 *)(fp-8);
printf("%p\n",ret_addr);// attention that after %p is \n
fp=*(uint64 *)(fp-16);
}
}

bttest.c中如是写:

1
2
3
4
5
6
7
8
9
10
11
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int
main(int argc, char *argv[])
{
sleep(1);
exit(0);
}

因此bttest作为测试文件会调用sleep,也即是sys_sleep。我们在sys_sleep中加入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
uint64
sys_sleep(void)
{
int n;
uint ticks0;
if(argint(0, &n) < 0)
return -1;
acquire(&tickslock);
ticks0 = ticks;
while(ticks - ticks0 < n){
if(myproc()->killed){
release(&tickslock);
return -1;
}
sleep(&ticks, &tickslock);
}
release(&tickslock);
backtrace();//
return 0;
}

这样的话,sleep返回之前会调用backtrace函数

类似的,为了更好的debug,在panic函数中加入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
void
panic(char *s)
{
pr.locking = 0;
printf("panic: ");
printf(s);
printf("\n");
backtrace();
panicked = 1; // freeze uart output from other CPUs
for(;;)
;
}

这样会在panic输出之后,打印出返回地址

ps :做到此处发现了的qemu卡死的问题,发现是2020版本lab的传统艺能。apt remove qemu*之后使用源码编译qemu5版本即可

Next, retrieve and extract the source for QEMU 5.1.0:

1
2
$ wget https://download.qemu.org/qemu-5.1.0.tar.xz
$ tar xf qemu-5.1.0.tar.xz

Build QEMU for riscv64-softmmu:

1
2
3
4
5
$ cd qemu-5.1.0
$ ./configure --disable-kvm --disable-werror --prefix=/usr/local --target-list="riscv64-softmmu"
$ make
$ sudo make install
$ cd ..

Alarm (hard)

目标:

向 xv6添加一个特性,它将在进程使用 CPU 时间时周期性发出警报。更一般地说,实现用户级中断/错误处理程序的基本形式; 例如,可以使用类似的东西来处理应用程序中的页面错误。

hint:

  • 添加一个新的系统调用sigalarm(interval, handler),如果应用程序调用 sigAlarm (n,fn) ,那么在程序消耗的每 n 个 CPU 时间之后,内核应该调用应用程序函数 fn。当 fn 返回时,应用程序应该从停止的地方恢复
  • 如果一个应用程序调用 sigAlarm (0,0) ,内核应该停止生成周期性的alarm调用
  • user/alarmtest.c是测试文件,需要将其添加到makefile。添加了 sigAlarm 和 sigreturn 系统调用,它才能正确编译
  • 可以在 user/alarmtest.asm 中看到 alarmtest 的汇编代码,这可能有助于调试
  • alarmtest 中在 test0 调用了 sigalarm(2, periodic),要求内核强制每隔2个tick调用 period () ,然后spin一段时间

test0:修改内核以跳转到用户空间中的警报处理程序

先修改makefile编译出alarmtest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
UPROGS=\
$U/_cat\
$U/_echo\
$U/_forktest\
$U/_grep\
$U/_init\
$U/_kill\
$U/_ln\
$U/_ls\
$U/_mkdir\
$U/_rm\
$U/_sh\
$U/_stressfs\
$U/_usertests\
$U/_grind\
$U/_wc\
$U/_zombie\
$U/_alarmtest\

放入 user/user.h 中的正确声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// system calls
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
int fork(void);
int exit(int) __attribute__((noreturn));
int wait(int*);
int pipe(int*);
int write(int, const void*, int);
int read(int, void*, int);
int close(int);
int kill(int);
int exec(char*, char**);
int open(const char*, int);
int mknod(const char*, short, short);
int unlink(const char*);
int fstat(int fd, struct stat*);
int link(const char*, const char*);
int mkdir(const char*);
int chdir(const char*);
int dup(int);
int getpid(void);
char* sbrk(int);
int sleep(int);
int uptime(void);

// ulib.c
int stat(const char*, struct stat*);
char* strcpy(char*, const char*);
void *memmove(void*, const void*, int);
char* strchr(const char*, char c);
int strcmp(const char*, const char*);
void fprintf(int, const char*, ...);
void printf(const char*, ...);
char* gets(char*, int max);
uint strlen(const char*);
void* memset(void*, int, uint);
void* malloc(uint);
void free(void*);
int atoi(const char*);
int memcmp(const void *, const void *, uint);
void *memcpy(void *, const void *, uint);

更新 user/usys.pl (生成 user/usys.S)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/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("fork");
entry("exit");
entry("wait");
entry("pipe");
entry("read");
entry("write");
entry("close");
entry("kill");
entry("exec");
entry("open");
entry("mknod");
entry("unlink");
entry("fstat");
entry("link");
entry("mkdir");
entry("chdir");
entry("dup");
entry("getpid");
entry("sbrk");
entry("sleep");
entry("uptime");
entry("sigalarm");//
entry("sigreturn");//

kernel/syscall.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// System call numbers
#define SYS_fork 1
#define SYS_exit 2
#define SYS_wait 3
#define SYS_pipe 4
#define SYS_read 5
#define SYS_kill 6
#define SYS_exec 7
#define SYS_fstat 8
#define SYS_chdir 9
#define SYS_dup 10
#define SYS_getpid 11
#define SYS_sbrk 12
#define SYS_sleep 13
#define SYS_uptime 14
#define SYS_open 15
#define SYS_write 16
#define SYS_mknod 17
#define SYS_unlink 18
#define SYS_link 19
#define SYS_mkdir 20
#define SYS_close 21
#define SYS_sigalarm 22
#define SYS_sigreturn 23

kernel/syscall.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
extern uint64 sys_chdir(void);
extern uint64 sys_close(void);
extern uint64 sys_dup(void);
extern uint64 sys_exec(void);
extern uint64 sys_exit(void);
extern uint64 sys_fork(void);
extern uint64 sys_fstat(void);
extern uint64 sys_getpid(void);
extern uint64 sys_kill(void);
extern uint64 sys_link(void);
extern uint64 sys_mkdir(void);
extern uint64 sys_mknod(void);
extern uint64 sys_open(void);
extern uint64 sys_pipe(void);
extern uint64 sys_read(void);
extern uint64 sys_sbrk(void);
extern uint64 sys_sleep(void);
extern uint64 sys_unlink(void);
extern uint64 sys_wait(void);
extern uint64 sys_write(void);
extern uint64 sys_uptime(void);
extern uint64 sys_sigalarm(void);//
extern uint64 sys_sigreturn(void);//

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_sigalarm] sys_sigalarm,//
[SYS_sigreturn] sys_sigreturn,//
};

在sysproc.c中加入sys_sigalarm和sys_sigreturn函数处理代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
uint64 
sys_sigalarm(void)
{
int n;
uint64 fn;
if(argint(0, &n) < 0) {
return -1;
}
if(argaddr(1, &fn) < 0) {
return -1;
}
return sigalarm(n, (void(*)())(fn));
}

uint64
sys_sigreturn(void)
{
return sigreturn();
}

关于test1的提示:

  • 解决方案将要求您保存和恢复寄存器– 您需要保存和恢复哪些寄存器才能正确地恢复被中断的代码?(提示: 会有很多)。
  • 当计时器关闭时,用户陷阱在 struct proc 中保存足够的状态,这样签名返回就可以正确地返回到被中断的用户代码。
  • 防止对处理程序的重入调用——如果处理程序还没有返回,内核就不应该再次调用它。

硬件时钟每次都强制执行一个中断,这个中断在 kernel/trap.c 中的 usertrap ()中处理。每次时钟发生硬件中断,都统计一次,到了次数就执行handler

在proc中加入如下变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Per-process state
struct proc {
struct spinlock lock;

// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID

// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)

int alarm_interval; //n in sigalarm(n, (void(*)())(fn));
void(*alarm_handler)();// fn in sigalarm(n, (void(*)())(fn));
int alarm_ticks;//after alarm_ticks will
struct trapframe *saved_trapframe;
int alarm_flag;
};

完善sigalarm和sigreturn函数,并在defs.h中加入声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// defs.h
int sigalarm(int, void (*)());
int sigreturn(void);

//trap.c
int sigalarm(int ticks, void (*handler)())
{
struct proc *p = myproc();
p->alarm_interval=ticks;
p->alarm_handler=handler;
p->alarm_ticks=ticks;
return 0;
}

int sigreturn(void)
{
struct proc *p = myproc();
*p->trapframe = *p->saved_trapframe;
p->alarm_flag = 0;
return 0;
}

allocproc函数和freeproc函数也需要更新

  • 进程创建和销毁时结构体的字段的设置

对进程的字段进行初始化和销毁以及页的申请

  • 使用kalloc给p->saved_trapframe分配空间
  • 使用kfree((void *)p->saved_trapframe);销毁空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// Look in the process table for an UNUSED proc.
// If found, initialize state required to run in the kernel,
// and return with p->lock held.
// If there are no free procs, or a memory allocation fails, return 0.
static struct proc*
allocproc(void)
{
struct proc *p;

for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == UNUSED) {
goto found;
} else {
release(&p->lock);
}
}
return 0;

found:
p->pid = allocpid();

// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
release(&p->lock);
return 0;
}

// An empty user page table.
p->pagetable = proc_pagetable(p);
if(p->pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
//
if((p->saved_trapframe = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// Set up new context to start executing at forkret,
// which returns to user space.
memset(&p->context, 0, sizeof(p->context));
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;

p->alarm_flag=0;//
p->alarm_interval = 0;
p->alarm_handler = 0;
p->alarm_ticks = 0;

return p;
}

// free a proc structure and the data hanging from it,
// including user pages.
// p->lock must be held.
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;

if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);

if(p->saved_trapframe)
kfree((void *)p->saved_trapframe);

p->pagetable = 0;
p->sz = 0;
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->chan = 0;
p->killed = 0;
p->xstate = 0;
p->state = UNUSED;

p->alarm_flag=0;//
p->alarm_interval = 0;
p->alarm_handler = 0;
p->alarm_ticks = 0;
}

评分


MIT-6.S081导言
http://gls.show/p/92b97892/
作者
郭佳明
发布于
2023年7月26日
许可协议