- xv6的安装
- 关于各种系统调用
- Lab util: Unix utilities
- Lab:system calls
- Lab: page tables
- Lab: traps
- Lab: Copy-on-Write Fork for xv6
- Lab: Multithreading
- Lab: networking
- Lab: locks
- Lab: file system
- Lab: mmap
xv6的安装
- xv6 book的中文版
- 条件:Vmware上运行的unbuntu虚拟机
1 | sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu |
关于各种系统调用
- pipe():
int pipe(int fds[2]);
在进程的打开文件表中查找前两个可用位置,并将它们分配给管道的读p[0]写p[1]端。 - chdir():
int chdir(const char *path);
用于改变当前工作目录,其参数为Path 目标目录(类似于cd)
Lab util: Unix utilities
- 以下路径都在xv6-labs-2021/下面
sleep
user/目录下新建sleep.c,代码如下
1 |
|
打开Makefile,在179行找到UPROGS,在其中添加sleep。
回到xv6-labs-2021/,$ make qemu
测试是否通过,在xv6-labs-2021/目录下:$ ./grade-lab-util sleep
pingpong
不加wait,会出现缓冲区冲突,交替写入。
1.8晚上。自己测试没问题,在进行成绩测试时没通过。
在多次执行pingpong时,输出的pid依次增加,按理说不应该这样。
1 |
|
primes
题意:用一堆进程,在他们之间构建pipeline,来构建出一个质数筛选器
参考资料:Bell Labs and CSP Threads 中描述
1 | p = get a number from left neighbor |
每个进程打印一个质数,并做一次筛选,剩下的通过管道传到下一个进程
问题在于如何在多个进程间建立起pipeline?
以下代码参考自知乎-渡船-6.s081 2021 lab1 Xv6 and Unix utilities
- 我们认为进程统一从pipe1得到数据,将剩余数据传入pipe2
1 |
|
find
以官网例子为例,find . b
表示在.目录及其子目录下查找名为b的数据文件。反映在argv[]中,即在名为argv[1]目录及其子目录中查找名为argv[2]的数据文件(这点困扰了我很久)
难点在于:
- 了解LINUX的目录文件的组织结构
- 读懂ls.c中的ls()中的代码。
1 |
|
xargs
完成于2023.2.8 21:42
题意:将来自标准输入的字符串当作命令来执行
难点:
- 读懂题意,每次读题就要读很久
- 掌握挨个挨个读取字符的方法,’\0’和’\n’的作用
- stdin中的一个字符串结束标志就是读到’ ‘,而不是’\0’,在字符数组中才表现为’\0’
1 |
|
lab1的重难点总结:
- 掌握unix下编程的基本格式
- 掌握父子进程通过管道进行通信的原理并实现通信。
- 理解linux下文件的组织形式及相关结构体的组成
Lab:system calls
参考文件:
- user/user.h:罗列系统调用的接口
- user/usys.pl:配置文件
- kernel/syscall.h:罗列系统调用号
System call tracing
要点:
- 命令行中的数,是int32表示,从低位开始依次标号0、1、2、3…31,每一位表示一个系统调用是否跟踪。为1则跟踪,为0则不跟踪。例如
trace 32 grep hello README
跟踪的是read系统调用,read的系统调用号为5。32在补码的表示下,低6位为100 000。也就是说,它这样表示,其实最多只能跟踪31个系统调用。不过目前只有22个,暂时没有超标。 - 对user/trace.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
int main(int argc, char *argv[]){
int i;
char *nargv[MAXARG];
if(argc < 3 || (argv[1][0] < '0' || argv[1][0] > '9')){ //只有两个参数 或者 系统调用号不是数字,报错
fprintf(2, "Usage: %s mask command\n", argv[0]);
exit(1);
}
if (trace(atoi(argv[1])) < 0) { //调用trac,说明trace应该是返回int,且-1表示报错。并且trace的作用是设置mask,把argv[1]传进去
fprintf(2, "%s: trace failed\n", argv[0]);
exit(1);
}
for(i = 2; i < argc && i < MAXARG; i++){ //提取出要执行的命令。例子:grep hello README
nargv[i-2] = argv[i];
}
exec(nargv[0], nargv);
/*
调用exec后,调用该函数的进程的虚拟地址空间的代码段、数据段、堆、栈被释放,
替换成新进程的代码段、数据段、堆、栈,而PCB依旧使用之前进程的PCB。所以之前通过
trace设置的mask还在。接下来,当一个进程调用一个系统调用时,我们都通过myproc()获取它的mask。
对比,确认是要跟踪的系统调用,打印信息。
并且要求子进程也跟踪,也就是要继承父进程的pcb中的mask,还要修改
*/
exit(0);
}
实现步骤:
在Makefile中,将$U/_trace添加到 UPROGS。
在user/user.h中,添加
int trace(int);
在user/usys.pl中,添加
entry("trace");
在kernel/syscall.h中,添加
#define SYS_trace 22
在kernel/proc.h/struct proc中,添加
int mask;
在kernel/sysproc.c中,添加函数:
1
2
3
4
5
6
7uint64 sys_trace(void){ //for lab:TRACE
int mask;
if(argint(0, &mask) < 0)
return -1;
myproc()->mask=mask;
return 0;
}在kernel/proc.c/fork()中,添加:
1
2// Copy mask from parent to child. //for lab:TRACE
np->mask=p->mask;在kernel/syscall.c中,添加关于
sys_trace
的信息扩展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
25void
syscall(void)
{
int num;
struct proc *p = myproc(); //获取当前的进程的pcb
int mask;
char *syscall_name[24]={0,"fork","exit","wait","pipe","read","kill","exec",
"fstat","chdir","dup","getpid","sbrk","sleep","uptime",
"open","write","mknod","unlink","link","mkdir","close",
"trace","sysinfo"};
num = p->trapframe->a7; //从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;
}
mask=p->mask;
if(mask&1<<num){ //按位与,只要一位对应上,就代表需要跟踪这个系统调用
printf("%d: syscall %s -> %d\n",p->pid,syscall_name[num],p->trapframe->a0); //a0存的是系统调用的返回值
}
}
重难点:
- 了解syscall所经过的路径,一定经过syscall函数进入内核空间。通过ecall指令跳转过去
- 了解pcb的组成成员以及exec之后,pcb还是原来的pcb
Sysinfo
目的:打印空闲内存(单位字节)、打印有多少进程的状态是UNUSED
- 使用copyout()来实现从内核复制数据到用户空间,位于kernel/vm.c中。下面是分析
1
2
3
4
5
6
7
8// Copy from kernel to user.
// Copy len bytes from src to virtual address dstva in a given page table.
// Return 0 on success, -1 on error.
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
// 其中pagetable_t是一个uint64 *
// 回顾了一下页表的理论知识,确实就应该是一个数组,每一项表示一个页表条目,最高位表示是否在内存中
实现步骤:
在Makefile中,将$U/_sysinfotest添加到 UPROGS。
在user/user.h中,添加
int sysinfo(struct sysinfo*);
,并且在文件最前面添加struct sysinfo;
在user/usys.pl中,添加
entry("sysinfo");
在kernel/syscall.h中,添加
#define SYS_sysinfo 23
在kernel/kalloc.c中,添加函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/*
用户空间分配内存使用malloc,分配的虚拟内存
kalloc.c是用来分配内核内存的函数,分配物理内存
*/
uint64 get_freemem(void) //获得空闲内存的数量,多少字节。
{
struct run *r;
uint64 len=0;
acquire(&kmem.lock); //获得锁
r = kmem.freelist;
while(r)
{
r = r->next;
len++;
}
release(&kmem.lock);
return len*PGSIZE; //PGSIZE即页面的大小,为4096。定义在riscv.h里面
}在kernel/proc.c中,添加函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15uint64 get_unused_proc(void)
{
struct proc *p;
uint64 num=0;
for (p = proc; p < &proc[NPROC]; p++)
{
acquire(&p->lock); //这是锁,在struct proc的注释中,有写哪些成员在访问时必须要先获取锁
if (p->state != UNUSED)
{
num++;
}
release(&p->lock);
}
return num;
}在kernel/defs.h中找到kalloc.c和proc.c的位置,添加刚才添加的函数原型
在kernel/sysproc.c中,添加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
uint64 sys_sysinfo(void)
{
struct sysinfo info;
info.freemem=get_freemem();
info.nproc=get_unused_proc();
uint64 user_si; // user pointer to struct sysinfo
struct proc *p = myproc();
if(argaddr(0, &user_si) < 0)
return -1;//fetch user pointer to struct sysinfo from syscall arg
if(copyout(p->pagetable, user_si, (char *)&info, sizeof(info)) < 0)
return -1;
return 0;
}在kernel/syscall.c中,添加关于sys_sysinfo的信息。(第一次因为忘了这一步,导致报错unknown sys call 23)
2023.2.18下午完
Lab: page tables
!!!注意,在进行每个大实验之前,一定要切换分支
1 | $ git fetch |
否则代码里根本找不到这次实验需要的结构,比如kernel/memlayout.h/USYSCALL,在切换分支前,根本就没有。
前两个实验没有切换,倒是没有影响。这里要学习git的一些知识,暂时没搞透,搞透了再写写
Speed up system calls
目的:加速getid()这个系统调用
大意:有一些内核和用户共享的只读页面,来避免模式切换,以此来加快系统调用。这里在kernel/memlayout.h中给你定义了一个虚拟地址USYSCALL,是一个页面的地址,这个页面最开始的地方,又有一个struct,里面放了一个pid。
实现步骤:
// 这里基本是仿照着对trapframe的分配和释放来书写代码,都是分配一个页面,都是里面有一个结构体
- 在kernel/proc.h/struct proc中添加
struct usyscall *usyscall;
。(用于指向共享页面) - 在kernel/proc.c/allocproc()中添加
1
2
3
4
5
6
7
8
9
10// Allocate a usyscall page. for lab speedup syscall
if((p->usyscall = (struct usyscall *)kalloc()) == 0){ 、
//这里应该是物理分配和初始化,后面要做一个映射,否则不能保证它所描述的layout
freeproc(p);
release(&p->lock);
return 0;
}
// Initialize the usyscall page. for lab speedup syscall
p->usyscall->pid=p->pid; - 在kernel/proc.c/proc_pagetable()中,添加:
1
2
3
4
5
6
7// map the share page just below TRAPFRAME, for lab speedup syscall
//这里就实现了内存映射,记住,mappages()就是做内存映射的
if(mappages(pagetable, USYSCALL, PGSIZE,
(uint64)(p->usyscall), PTE_R | PTE_U) < 0){
uvmfree(pagetable, 0);
return 0;
} - 在kernrl/proc.c/freeproc()中,添加
1
2
3
4// 释放物理内存
if(p->usyscall)
kfree((void*)p->usyscall);
p->usyscall = 0; - 在kernel/proc.c/proc_freepagetable()中,添加:
1
2// 取消映射
uvmunmap(pagetable, USYSCALL, 1, 0);
重难点:
- 理解物理页面的分配和虚拟地址的映射是分两步
- 了解xv6下虚拟地址空间的组织结构
- 理解页表的组织结构,就是一个uint64的数组,存放虚拟地址
Print a page table
目的:打印pid==1的进程的页表
要点:
- 这里是多级页表,要依次往下探,所以递归进行
- 与kernel/vm.c/freewalk()的逻辑和遍历路径一致,freewalk的功能是释放分配给页表的物理内存,取消虚拟地址映射。并且物理内存的释放是从下到上。
实现步骤:
- 在kernel/exec.c/exec()中的
return argc
之前,添加:1
2
3if(p->pid==1){ //for lab:print ptl
vmprint(p->pagetable);
} - 在kernel/vm.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// for lab:print ptl
void fmt_print(int deepth,int i,pte_t pte,uint64 child){
switch(deepth){
case 0:printf("..");break;
case 1:printf(".. ..");break;
case 2:printf(".. .. ..");break;
default:break;
}
printf("%d: pte %p pa %p\n",i,pte,child);
}
void
vmprint_func(pagetable_t pagetable,int deepth)
{
// 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) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){ //这个页表条目指向了下一级页表
// this PTE points to a lower-level page table.
uint64 child = PTE2PA(pte); //pte转为物理地址,应该说,在直到最底端之前,所谓了pa,都是下一级的虚拟地址而已。
fmt_print(deepth,i,pte,child);
vmprint_func((pagetable_t)child,deepth+1); //递归的进行
} else if(pte & PTE_V){ //到底了
uint64 child = PTE2PA(pte);
fmt_print(deepth,i,pte,child);
//叶子页表,不用递归了
}
}
}
void vmprint(pagetable_t pagetable){
printf("page table %p\n",pagetable);
vmprint_func(pagetable,0);
} - 在kernel/defs.h中找到vm.c对应得地方,添加对vmprint()的原型说明。
重难点:
- 了解页表的组织结构,实现对页表的递归遍历并打印层次结构信息。
- 通过对标识位的逻辑运算,判定PTE是否映射有对象。
Detecting which pages have been accessed
目的:检测哪些页面的Accessed标志位为1,这里要了解sv39结构的虚拟地址的组成。
实现思路:大体也就是通过遍历页表,检测标志位
实现步骤:
- 在kernel/riscv.h中定义Accessed标志位:
1
- 在kernel/sysproc.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
int
sys_pgaccess(void) //for lab detecting accessed
{
// lab pgtbl: your code here.
uint64 start_va; //开始的虚拟地址
if(argaddr(0, &start_va) < 0)
return -1;
int page_num; //页面号
if(argint(1, &page_num) < 0)
return -1;
uint64 result_va; //存储结果的buffer
if(argaddr(2, &result_va) < 0)
return -1;
struct proc *p = myproc();
if(pgaccess(p->pagetable,start_va,page_num,result_va) < 0) //真正的功能实现,在vm.c中实现
return -1;
return 0;
} - 在kernel/vm.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// for lab detect accessed
int pgaccess(pagetable_t pagetable,uint64 start_va, int page_num, uint64 result_va)
{
if (page_num > 64)
{
panic("pgaccess: too much pages");
return -1;
}
unsigned int bitmask = 0;
int cur_bitmask = 1;
int count = 0;
uint64 va = start_va;
pte_t *pte;
for (; count < page_num; count++, va += PGSIZE)
{
if ((pte = walk(pagetable, va, 0)) == 0)
panic("pgaccess: pte should exist");
if ((*pte & PTE_A))
{
bitmask |= (cur_bitmask<<count);
*pte &= ~PTE_A;
}
}
copyout(pagetable,result_va,(char*)&bitmask,sizeof(bitmask));
return 0;
} - 在kernel/defs.h中找到vm.c的位置,添加pgaccess()的原型。
!!注意,这三个实验的验证是通过make qemu,使用pgtaltest命令来验证。
2.22。ugetpid不通过,不知道为啥。
Lab: traps
研究系统调用是如何通过traps机制实现的。
关于traps机制:直译陷阱。异常的一种,请结合csapp笔记来看
实现一个用户级的trap handling
Lab: Copy-on-Write Fork for xv6
就是实现一个写时复制的fork
csapp:fork()为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。
它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
Lab: Multithreading
实现一个用户级的线程包,来实现线程的切换,以此来加快程序运行速度。并且实现一个mem barrier
Lab: networking
为网络接口控制器写一个设备驱动
Lab: locks
parallelism、lock:关于锁和并发。有点复杂。不用写上去,搞不定
Lab: file system
为xv6系统添加大文件和符号链接