2. Lab: system calls
笔记目标:
- 按照11个lab的顺序,整理所涉及到的知识
- 对每个lab,给出清晰的解答
- 整理自己在做实验过程中遇到的问题与思考
- 对于课程中有涉及但是没有相应lab的内容做出补充(比如,后边的那些论文
笔记说明:
- 针对的是20的课程,其他年份可能会有出入
- 如果读到一些在本章课程、指导书中完全没提及的内容,那很有可能是引用了后续实验的知识,我会尽量标明,如果遇到看不懂的,可以先跳过
- 因为自己刚经历了纯小白的苦恼,所以会努力写得让零基础的人也能看懂,但这也会导致笔记比较琐碎
2.1 System call tracing
2.1.1 理论知识
[1] mask:掩码的定义
“It should take one argument, an integer “mask”, whose bits specify which system calls to trace.”
实验要求以及运行示例中都对mask的定义给出了清晰的说明:mask的bits表明了我们要trace的系统调用号
如:
- mask = 2,转换为二进制为
0010
,仅第1次位为true,则仅追踪系统调用号为1的系统调用 - mask = 32, 转换为二进制为
100000
,仅第5次位为true,则追踪系统调用号为5的系统调用
[2] 用户空间和内核空间之间的消息传递
实验指导书4.3介绍了一种用户和内核之间的通信方式:寄存器。
用户将特定信息放入对应的寄存器当中,内核需要时就去特定的寄存器读取。同理,当内核需要向用户返回信息时,也是这样。
用户代码将 exec 的参数放在寄存器 a0 和 a1 中,并将系统调用号放在 a7 中。
syscall (kernel/syscall.c:133)从 trapframe中的 a7 中得到系统调用号,并其作为索引在syscalls 查找相应函数。
当系统调用函数返回时,syscall 将其返回值记录在 p->trapframe->a0 中。用户空间 的 exec()将会返回该值。
2.1.2 实验步骤
在hints中,每一步都给了很详细的提示
[1] 添加trace系统调用的声明
像函数调用一样,我们在调用系统调用前也需要对其进行声明和定义。对系统调用的声明分为两个部分,一个是在用户空间的声明,一个在内核空间的声明:
[1.1] 用户空间的声明
-
user.h
和往常的头文件的作用类似,保存函数的声明,以便程序知道应该如何调用。通过观察
trace.c
程序中对trace()
的调用:if (trace(atoi(argv[1])) < 0) { // ... }
可知,trace函数的返回值和参数列表均为int类型:
// user/user.h // 打开user.h文件,发现有许多已经声明的系统调用,模仿已有的系统调用声明,对trace系统调用进行声明: int trace(int);
-
usys.pl
根据提示,这应该是生成“由用户空间到内核空间”的“入口”的文件,因为系统调用其实是请求内核的特定服务,所以需要在这个文件中设定trace系统调用的“入口”。
打开文件,模仿其他系统调用即可:
// user/usys.pl entry("trace");
[1.2] 内核空间的声明
-
syscall.h
在内核空间中,也需要对系统调用进行“声明”,这里的声明是给系统调用一个唯一对应的整数,以便内核能够选择正确的系统调用提供给用户。
// kernel/syscall.h #defsine SYS_trace 22 // 注意系统调用号是唯一对应的,不能重复
-
syscall.c
另外,在
syscall.c
中也需要对已有的类似于“函数指针的”声明进行模仿:// kernel/syscall.c // ... extern uint64 sys_trace(void); // ... [SYS_trace] sys_trace, // ...
这是为了后续方便通过系统调用号索引到特定的系统调用。
[2] 添加trace系统调用的定义
前边提到,类比函数调用,除了声明,我们在调用系统调用前还需要对其进行定义,即定义系统调用的功能。
Add a
sys_trace()
function inkernel/sysproc.c
that implements the new system call by remembering its argument in a new variable in theproc
structure (seekernel/proc.h
)根据提示,我们需要在
kernel/sysproc.c
中添加sys_trace()
函数,这个函数通过保存其传入的参数到proc
结构体中,来实现新的系统调用。
[2.1] 获取参数
根据提示,阅读模仿sysproc.c
中的其他示例。我们可以从我们最熟悉的sys_sleep
入手,在lab1中我们对sleep有过一次应用:“sleep
should pause for a user-specified number of ticks”,显然,sleep系统调用也需要获取用户传入的参数。从sleep的使用情景出发,再去观察sys_sleep
会更有针对性。
根据lab2开头让我们阅读的代码kernel/syscall.c
(自己仔细完整地看一遍)以及实验指导书4.3、4.4:
// Fetch the nth 32-bit system call argument.
int argint(int n, int *ip);
不难看出sys_sleep
中的if(argint(0, &n) < 0)
语句就是在试图保存用户传入的参数。
于是,模仿可得:
int mask;
if(argint(0, &mask) < 0)
return -1;
这样,传入参数就被获取到了变量mask中。
[2.2] 保存参数
根据提示,需要把获取的参数保存到proc
结构体的一个新变量当中。
-
首先,我们需要知道
proc
结构体是啥。根据提示打开proc.h
,可以看到proc
结构体保存了许多进程的信息,如进程运行状态、父进程指针、进程ID等。现在,我们需要其多保存一个信息,也就是我们的mask参数。但是结构体中有两个不同的数据段,mask参数应该加到哪个数据段下边呢?
根据注释,其中一段数据是需要被lock锁保护的,而另一段则不用。什么样的数据需要被lock呢?当一个数据可能会被多个进程读写时,为了保证数据的同步,我们需要对数据进行加锁。可以看到,被lock的数据段如父进程指针、子进程指针等,都是有可能会被多个进程读写的;而未被lock的数据段如进程大小、进程页表等,都只是会被当前进程读写。mask代表着我们要追踪的系统调用号,并不需要被其他进程读取,因此放入不被lock保护的数据段即可。
// these are private to the process, so p->lock need not be held. int mask;
-
其次,如何将参数保存进
proc
结构体中的变量呢?根据lab2开头提示阅读的kernel/proc.c
的函数定义:// Return the current struct proc *, or zero if none. struct proc* myproc(void);
可知
sys_sleep
中的if(myproc()->killed)
就是在访问当前进程的proc
结构体,模仿可得:myproc()->mask = mask;
[2.3] 总结可得:
// kernel/sysproc.c
uint64 sys_trace(void) {
int mask;
if (argint(0, &mask) < 0)
return -1;
myproc()->mask = mask;
return 0;
}
[3] 修改fork()
修改fork,将父进程的mask数据复制到子进程。根据提示,阅读kernel/proc.c
中的fork()
代码,其中有一段代码是负责copy的,模仿其形式,对mask进行同样的复制即可:
// kernel/proc.c
int
fork(void)
{
//...
np->sz = p->sz;
np->mask = p->mask; // 复制父进程的mask数据到子进程
np->parent = p;
// ...
}
因为我们需要对子进程的系统调用也进行追踪,而追踪系统调用需要根据proc
结构体中保存的mask数据,所以需要进行复制。
[4] 修改syscall()
[4.1] 判断是否是需要trace的系统调用
即,判断正在执行的系统调用是否在用户输入的mask中。2.1.1[1]
中有提到,若mask的n次位为1,则n号系统调用需要被追踪。
if(1 << n & mask) /* n为正准备执行的系统调用号,对1做左移运算,得到n次位为1的数,同mask做与运算,判断mask的n次位是否为1 */
[4.2] 打印所要求的信息
-
process id
有了
2.1.2[2.2]
保存参数的经验,获取process id已经很轻松了,这些进程的状态信息都保存在了proc
结构体中。int pid = myproc()->pid;
-
the name of the system call
根据提示,可以增加一个保存系统调用名称的数组,便于通过系统调用号索引输出。
char *names[] = {"fork","exit","wait","pipe","read","kill","exec","fstat","chdir","dup","getpid","sbrk","sleep","uptime","open","write","mknod","unlink","link","mkdir","close","trace"};
-
return value
由实验指导书可知,“syscall 将其返回值记录在 p->trapframe->a0 中”,于是:
int returnValue = p->trapframe->a0;
-
总结得:
// kernel/sysproc.c void syscall(void) { int num; struct proc *p = myproc(); char *names[] = {"fork", "exit", "wait", "pipe", "read", "kill", "exec", "fstat", "chdir", "dup", "getpid", "sbrk", "sleep", "uptime", "open", "write", "mknod", "unlink", "link", "mkdir", "close", "trace"}; num = p->trapframe->a7; if (num > 0 && num < NELEM(syscalls) && syscalls[num]) { p->trapframe->a0 = syscalls[num](); if ((1 << num) & myproc()->mask) { printf("%d: syscall %s -> %d\n", p->pid, names[num - 1], p->trapframe->a0); // 按照格式打印信息 } } else { printf("%d %s: unknown sys call %d\n", p->pid, p->name, num); p->trapframe->a0 = -1; } }
[5] 编译、运行
-
在
Makefile
文件中加入trace
UPROGS=\ ...... $U/_zombie\ $U/_trace\
-
在
xv6-labs-2020
目录下输入make qemu
-
在命令行输入
trace 32 grep hello README
,得到结果:其他测试也均能满足要求。
-
按住
Ctrl + a + x
,退出xv6,在xv6-labs-2020
目录下输入make grade
:
2.2 Sysinfo
2.2.1 理论知识
没有什么新的理论知识,主要得根据提示看懂所涉及的代码逻辑。
2.2.2 实验步骤
[1] 添加sysinfo系统调用的声明
与上一个实验类似
[1.1] 用户空间的声明
-
user.h
struct sysinfo; int sysinfo(struct sysinfo *);
-
usys.pl
entry("sysinfo");
[1.2] 内核空间的声明
-
syscall.h
#defsine SYS_sysinfo 23
-
syscall.c
// ... extern uint64 sys_sysinfo(void); // ... [SYS_sysinfo] sys_sysinfo, // ...
[2] 理清实现思路
根据提示,sysinfo系统调用的动作会比较多、比较复杂,建议先用伪代码把实现思路写出来,再模块化地去实现思路中需要的功能。
//1.获取指向sysinfo结构体的指针
//2.计算空闲的memory,并将数据放入sysinfo结构体相应的数据段中
//3.计算unused的process,并将数据放入sysinfo结构体相应的数据段中
//4.将内核中处理好的sysinfo传到用户空间
[3] 获取指向sysinfo结构体的指针
与上一个实验的获取整数参数相似,这里也是获取用户传入的参数,只不过参数类型由整数变为了指针。
uint64 pSysinfo;
if(argaddr(0, &pSysinfo) < 0)
return -1;
[4] 计算空闲的memory
实验并没有给出如何计算free memory的提示,但是指明了函数要添加到kalloc.c
文件中。打开这个文件,可以发现其功能是为进程分配内存,通过观察分配内存的整个过程,获得计算free memory的灵感。
先快速浏览代码,找到分配内存的实现函数:kalloc()
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{
struct run *r; // 声明一个单链表(头结点指针)
acquire(&kmem.lock); // 因为内存是临界资源,所以在分配内存时需要上锁
r = kmem.freelist; // 令单链表头结点指针指向内存链表的头结点
if(r) // 若内存结点存在,则从链表中摘下
kmem.freelist = r->next;
release(&kmem.lock); // 释放锁
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
可以看到,free memory是保存在一个kmem.freelist
的链表结构中的。保险起见,通过观察释放内存的函数来印证我们的想法:kfree()
// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc(). (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(void *pa)
{
struct run *r;
// ...
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE); // 以页为单位进行分配和释放
acquire(&kmem.lock);
r->next = kmem.freelist; // 将释放掉的内存放入freelist当中
kmem.freelist = r;
release(&kmem.lock);
}
于是,可以确定,kmem.freelist
链表中保存了空闲的内存,一个结点表示一页free memory,通过遍历链表即可计算出空闲内存的大小:
// kernel/kalloc.c
uint64 countFreememory() {
struct run *r;
uint64 freememory = 0;
acquire(&kmem.lock);
r = kmem.freelist;
while (r) {
++freememory;
r = r->next;
}
release(&kmem.lock);
return PGSIZE * freememory;
}
[5] 计算unused的proc
根据提示,打开proc.c
文件,注意到allocproc()
函数中有关于unused proc的描述:
// 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++) { // 遍历proc数组(进程表)
acquire(&p->lock);
if(p->state == UNUSED) { // 找到state为UNUSED的进程
goto found;
} else {
release(&p->lock);
}
}
// ...
}
于是,通过遍历proc数组统计state为UNUSED的进程数量:
uint64 countUnusedproc() {
struct proc *p;
uint64 unusedproc = 0;
for (p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if (p->state != UNUSED) {
++unusedproc;
}
release(&p->lock);
}
return unusedproc;
}
[6] 将内核中得到数据的sysinfo传到用户空间
根据提示,观察sys_fstat() (kernel/sysfile.c)
和filestat() (kernel/file.c)
,学习如何使用copyout()
:
uint64
sys_fstat(void)
{
// ...
if(argfd(0, 0, &f) < 0 || argaddr(1, &st) < 0) // 获取用户传来的指针参数
// ...
}
// Get metadata about file f.
// addr is a user virtual address, pointing to a struct stat.
int
filestat(struct file *f, uint64 addr)
{
// ...
struct stat st;
// ...
if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
// 将从&st开始的sizeof(st)长的数据复制到p->pagetable的addr处
// ...
}
光看这两个函数,只能连蒙带猜地去理解copyout()
,应该顺着defs.h
中函数声明的位置找到copyout()
的函数定义:
// kernel/vm.c
// 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){
// 甚至只看函数描述就够了
}
copyout()
的功能是将内核中从src开始的len长的数据复制到指定pagetable中的dstva位置处。模仿可得:
copyout(p->pagetable, addr, (char *)&sysinfo, sizeof(sysinfo));
[7] 添加sysinfo系统调用的定义
因为countFreememory()
和countUnusedpro()
都是新定义的函数,所以需要在defs.h
中声明函数接口。
kernel中含有多个.c文件,若想要使用其他.c文件中的函数或者变量,就需要在
defs.h
中设置好接口,defs.h
就像是所有函数的头文件。为了便于管理,defs.h
中的函数声明是按照函数所处的不同.c文件进行分块放置的,比如,kalloc.c文件中的函数就放在// kalloc.c
的注释下方。
// kernel/defs.h
// kalloc.c
uint64 countFreememory(void);
// proc.c
uint64 countUnusedproc(void);
在sysproc.c
中给出系统调用的定义:
// kernel/sysproc.c
uint64
sys_sysinfo(void) {
struct sysinfo info;
uint64 addr;
info.freemem = countFreememory();
info.nproc = countUnusedproc();
if (argaddr(0, &addr) < 0)
return -1;
if (copyout(myproc()->pagetable, addr, (char *) &info, sizeof(info)) < 0)
return -1;
return 0;
}
[8] 编译、运行
-
在
Makefile
文件中加入sysinfotest
UPROGS=\ ...... $U/_trace\ $U/_sysinfotest\
-
在
xv6-labs-2020
目录下输入make qemu
-
在命令行输入
sysinfotest
,得到结果: -
按住
Ctrl + a + x
,退出xv6,在xv6-labs-2020
目录下输入make grade
:
2.2.3 问题与思考
[1] 为什么sysinfo结构体中的数据不能通过指针直接修改,而是需要借助copyout()
函数传到用户空间呢
因为argaddr(0, &addr)
获得的地址是用户空间的虚拟地址,并不直接指向数据对象,而是指向用户页表中的页表项,通过页表项才能最终访问到数据对象。copyout
其实就是封装好的帮助内核空间访问用户空间虚拟地址的函数。
2.3 Lab2总结
2.3.1 理解系统调用的发生过程
其实就是一个一个的函数调用,只不过有的函数只能在内核空间运行,所以在执行这些函数时,需要有用户空间到内核空间的切换。传递的变量在这个切换的过程中可能会“失去”它的作用域(因为页表的问题),这个时候可以通过寄存器作为用户与内核的消息传递方式,也可以在内核中获取用户页表来通过地址直接访问用户数据,但是用户不能获取内核页表。