一、实验前须知
阅读 xv6 文档的第 2 章和第 4 章的 4.3 节和 4.4 节以及相关源文件:
系统调用的用户空间代码在 user/user.h 和 user/usys.pl 中。
内核空间代码在 kernel/syscall.h 和 kernel/syscall.c 中。
与进程相关的代码在 kernel/proc.h 和 kernel/proc.c 中。
二、基础知识
第二章 操作系统组织结构
一个操作系统必须满足3点要求:多路复用、隔离、交互;
Xv6采用宏内核设计;
xv6运行在多核risc-v微处理单元;
risc-v是一个64位cpu,并且xv6用"LP64"( L:long类型,P:pointer类型在c语言中是64位,int是32位)C写的。
(一)抽象物理资源
实现强隔离。例如,
1.unix应用与内存交互仅通过文件系统的open、read、write、close system call,而不是直接读写硬盘。
2.unix进程使用exec来构建他们的内存镜像,而不是直接与物理内存交互。这允许操作系统决定一个进程该在内存中的哪个位置。如果内存紧张,操作系统甚至可能将进程的一些数据存储到硬盘上。
许多形式的unix进程间交互通过文件描述符发生。
(二)user mode、supervisor mode、system calls
强隔离需要一个坚固的边界。如果程序出错,我们不想让操作系统失败,或其他应用失败。操作系统应该能够清理失败应用,并继续运行其他应用。为了实现强隔离,操作系统必须这么安排:应用不能更改(甚至读)操作系统的数据结构和指令,应用不能访问其他进程的内存。
cpu为强隔离提供硬件支持。例如:risc-v有3种cpu执行指令的模式:machine mode、supervisor mode、user mode。
在machine mode下执行的指令有全部权限。cpu从machine mode开始启动。machine mode绝大多数用于配置一台电脑。xv6在machine mode下执行几行代码,然后切换到supervisor mode。
在supervisor mode中,cpu被允许执行权限指令:例如,启用、禁用中断,读写寄存器(保存页表地址)等等。如果一个应用在user mode下尝试执行权限指令,cpu不会执行这个指令,而是切换到supervisor mode下终止应用,因为这个应用做了它不该做的事。在kernel space(或in supervisor mode)中运行的软件称作kernel。
将cpu从user mode切换到supervisor mode,从kernel声明的入口点处进入kernel(risc-v提供ecall指令达到这个目的)。一旦cpu切换到supervisor mode,kernel会校验system call参数,决定应用是否被允许执行请求操作,然后拒绝它或者执行它。kernel控制过渡到supervisor mode的入口是很重要的事;如果一个应用可以决定kernel入口,一个恶意程序可以进入到kernel中逃脱参数校验的点。
(三)kernel组织
设计问题的关键是:操作系统的哪部分应该运行在supervisor mode下。一种可能是整个操作系统搁置在kernel中,因此所有system calls的实现运行在supervisor mode下。这个组织叫做monolithic kernel。
在宏内核下,操作系统各部分之间协作更容易。例如:一个操作系统可能有一个buffer cache,它可以被file system和virtual memory system共享。
monolithic组织的一个缺点是:操作系统不同部分间的接口通常是复杂的(我们将在之后文中看到),因此操作系统开发人员很容易犯错。在monolithic kernel中,一个错误是致命的,因为一个supervisor mode下的错误通常将导致kernel挂掉。如果kernel挂掉了,计算机停止工作,因此所有应用也挂掉。计算机必须重启。
为了减少kernel错误风险,os设计者会最小化操作系统代码(运行在supervisor mode下)的体积,并在用户mode下执行操作系统块。这个kernel组织称作microkernel。
图2.1阐述了微内核设计。在这个图中,文件系统作为一个用户级别的进程运行。为了允许应用和file server交互,kernel提供一个内部进程交互机制来让用户进程之间发送消息。例如:如果一个像shell这样的应用想读写文件,它向file server发消息并等待响应。
因为xv6不提供一些服务,它的内核比一些微内核小,但xv6的理念是monolithic。
(四)代码:xv6组织
xv6 kernel源码在kernel子目录中。资源被分成多个文件,跟随了模块化理念。内部模块接口定义在defs.h(kernel/defs)中。
(五)进程概览
xv6中的隔离单元是进程(正如其他unix中一样)。进程抽象阻止一个进程破坏、侦测另外一个进程的内存、cpu、文件描述符等等。也阻止一个进程破坏kernel本身,因此一个进程不能推翻kernel隔离机制。
为了帮助强制隔离,进程为程序提供一个私有内存系统或者地址空间(看起来是),其他进程不能读写。进程也给程序提供它自己的cpu(看起来是)来执行程序的指令。
xv6使用页表(硬件实现)来给每个进程分配自己的地址空间。risc-v页表翻译一个虚拟地址(risc-v指令操作的地址)到物理地址(cpu分配给主存的地址)。
xv6为每个进程保留一个独立的页表(定义了进程的地址空间)。正如图2.3阐述的那样,一个地址空间包括进程的用户内存(起始于虚拟内存地址0)。首先是指令,紧跟全局变量,然后栈,最后是一个堆(进程可以根据需要拓展)。
一些因素限制进程地址空间的最大尺寸:risc-v上面的指针是64位的;在页表中查找虚拟地址时,硬件仅使用低39位;xv6仅使用39位中的38位。因此最大地址是238-1=0x3fffffffff,这是最大虚拟地址(MAXVA max virtual address, kernel/riscv.h:348)。在xv6地址空间顶部保留一个page用作trampoline和一个page匹配进程的trapframe来切换到kernel,正如我们将在第四章解释的那样。
xv6 kernel给每个进程保留了许多个state,这些state集合在一起组成一个结构体struct_proc(kernel/proc.h)。
每个进程有一个执行线程,执行进程指令。一个线程可能会被挂起,然后再唤醒。在两个进程之间透明地切换,kernel挂起当前运行线程,并且唤醒另外一个进程的线程。thread的多个state(本地变量、函数调用返回地址)被存储在线程栈中。
每个进程有两个栈:一个用户栈,一个kernel栈。当进程正在执行用户指令,仅它的用户栈在使用,它的内核栈是空的。当进程进入kernel(system call或interrupt),kernel代码在进程kernel stack;当进程在kernel中时,它的用户栈仍然保留存储的数据,但不会被使用。因此kernel仍然可以执行,即使进程已经毁坏了它的用户stack。
一个进程通过执行risc-v的ecall指令可以做一个system call。这个指令提升硬件权利,改变程序计数器到一个kernel定义的入口。代码在入口处切换到kernel stack,并执行system call实现的kernel指令。当system call完成后,kernel切换回用户栈,通过调用sret指令返回到用户空间,这个指令拉低了硬件权利级别,并在system call指令之后恢复执行用户指令。
当执行进程在用户空间中时,xv6引发paging硬件来使用一个进程的p->pagetable。
(六)现实世界
****现代操作系统支持一个进程中多个线程,让一个进程利用多个cpu。****进程支持多线程的机器(xv6不支持)有很多,包含潜在的接口改变(例如:linux的clone、fork变形),来控制进程(多线程共享)的各方面。
(七)traps和system calls
调用system call
用户代码为exec在寄存器a0和a1放置参数,并把system call序号放到a7。system call序号匹配入口在syscalls数组,一个函数指针表(kernel/syscall.c:108)。ecall指令traps into kernel并执行uservec、usertrap,然后syscall,正如我们上面看到的。
syscall(kernel/syscall.c:133)从trapframe存的a7取得system call序号,并用它在system calls中找出。对于第一个system call,a7包含SYS_exec(kernel/syscall.h:8),结果是调用system call 实现函数sys_exec。
当system call实现函数返回,syscall记录它的返回值在p->trapframe->a0 。这将导致原始用户空间call exec()返回该值 ,因为risc-v的c调用约定是将返回值放到a0。system call约定返回负值来表明错误,0或正值表明成功。如果system call序号无效,syscall打印一个错误,并返回-1。
system call参数
system call kernel实现需要找出user code传递的参数。kernel trap代码保存用户寄存器到当前进程的trap frame,kernel code可以访问。函数argint,argaddr,和argfd,从trap frame获取第n个system call参数,作为整数、指针、文件描述符。他们都调用argraw来获取对应保存参数的寄存器(kernel/syscall.c:35)。
一些system call传递指针作为参数,并且kernel必须使用那些指针来读取或写user内存。exec system call,传递kernel一个指针数组,来标识用户空间的字符串参数。这些指针有两个挑战。首先用户程序可能是有bug或恶意的,并且可能传给kernel一个无效指针或一个欺骗kernel访问kernel 内存(而不是user内存)的指针。第二,xv6 kernel page table映射不同于user page table映射,因此kernel不会使用原始指令来加载或存储用户提供地址。
The kernel implements functions that safely transfer data to and from user-supplied addresses. fetchstr是一个例子(kernel/syscall.c:25)。file system calls例如exec使用fetchstr从用户空间来获取字符串文件名参数。fetchstr调用copyinstr来做这个工作。
三、实验开始
新建文件夹,重新使用下面的命令下载代码作为实验 2 工作区。
$ git clone git://g.csail.mit.edu/xv6-labs-2020
$ cd xv6-labs-2020
$ git checkout syscall
四、System call tracing
1 . 实验目的
添加一个系统调用跟踪功能,该功能可以在以后的实验中为你提供帮助。
你将创建一个新的 trace 系统调用来控制跟踪。
它应该有一个参数,一个整数"mask(掩码)",其指定要跟踪的系统调用。例如,为了跟踪 fork 系统调用,程序调用 trace (1 << SYS_fork) ,其中 SYS_fork 是来自 kernel/syscall.h 的系统调用号。
如果掩码中设置了系统调用的编号,则必须修改 xv6 内核以在每个系统调用即将返回时打印出一行。
该行应包含 进程 ID 、系统调用名称 和 返回值 ;您不需要打印系统调用参数。 trace 系统调用应该为调用它的进程和它随后派生的任何子进程启用跟踪,但不应影响其他进程。
2 . 实验要求及提示
将 $U/_trace 添加到 Makefile 的 UPROGS 中
运行 make qemu , 你将看到编译器无法编译 user/trace.c ,因为系统调用的用户空间存根还不存在:将系统调用的原型添加到 user/user.h ,将存根添加到 user/usys.pl ,以及将系统调用号添加到 kernel/syscall.h 中。 Makefile 调用 perl 脚本 user/usys.pl ,它生成 user/usys.S ,实际的系统调用存根,它使用 RISC-V ecall 指令转换到内核。修复编译问题后,运行 trace 32 grep hello README ;它会失败,因为你还没有在内核中实现系统调用。
在 kernel/sysproc.c 中添加一个 sys_trace() 函数,该函数通过在 proc 结构中的新变量中记住其参数来实现新系统调用(请参阅 kernel/proc.h )。从用户空间检索系统调用参数的函数位于 kernel/syscall.c 中,你可以在 kernel/sysproc.c 中查看它们的使用示例。
修改 fork() (参见 kernel/proc.c )以将跟踪的掩码从父进程复制到子进程。
修改 kernel/syscall.c 中的 syscall() 函数以打印跟踪输出。你将需要添加要索引的系统调用名称数组。
3 . 实验步骤
作为一个系统调用,我们先要定义一个系统调用的序号。系统调用序号的宏定义在 kernel/syscall.h 文件中。我们在 kernel/syscall.h 添加宏定义,模仿已经存在的系统调用序号的宏定义,我们定义 SYS_trace 如下:
#define SYS_trace 22
查看了一下 user 目录下的文件,发现官方已经给出了用户态的 trace 函数( user/trace.c ),所以我们直接在 user/user.h 文件中声明用户态可以调用 trace 系统调用就好了。
但有一个问题,该系统调用的参数和返回值分别是什么类型呢?接下来我们还是得看一看 trace.c 文件,可以看到 trace(atoi(argv[1])) < 0 ,即 trace 函数传入的是一个数字,并将返回值和 0 进行比较,结合实验提示,我们知道传入的参数类型是 int ,返回值类型也是 int 。这样就可以把 trace 这个系统调用加入到内核中声明了:
接下来我们查看 user/usys.pl 文件,这里 perl 语言会自动生成汇编语言 usys.S ,是用户态系统调用接口。所以在 user/usys.pl 文件加入下面的语句:
entry("trace");
查看上一次实验编译后的 usys.S 文件,可以看到如下的代码块:
.global fork
fork:
li a7, SYS_fork
ecall
ret
li a7, SYS_fork 指令就是把 SYS_fork 的系统调用号放入 a7 寄存器,使用 ecall 指令进入系统内核。
执行ecall指令之后,cpu跳转到 kernel/syscall.c 中 syscall 那个函数处,执行此函数。下面是 syscall 函数的源码:
void
syscall(void)
{
int num;
struct proc *p = myproc();
num = p->trapframe->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;
}
}
num = p->trapframe->a7;:从寄存器 a7 中读取系统调用号,放入num中。
接下来是 p->trapframe->a0 = syscalls[num](); 语句,通过调用 syscalls[num](); 函数,把返回值保存在了 p->trapframe->a0中。我们看看 syscalls[num](); 函数,这个函数在当前文件中。我们把新增的 trace 系统调用添加到其中:
static uint64 (*syscalls[])(void) = {
...
[SYS_trace] sys_trace,
};
接下来在文件开头(kernel/syscall.c)给内核态的系统调用 trace 加上声明
extern uint64 sys_trace(void);
在实现这个函数之前,我们可以看到实验最后要输出每个系统调用函数的调用情况,依照实验说明给的示例,可以知道最后输出的格式如下:
<pid>: syscall <syscall_name> -> <return_value>
其中, <pid> 是进程序号, <syscall_name> 是系统调用名称, <return_value> 是该系统调用的返回值。注意:冒号和 syscall 中间有个空格。
根据提示,我们的 trace 系统调用应该有一个参数,一个整数"mask(掩码)",其指定要跟踪的系统调用。所以,我们在 kernel/proc.h 文件的 proc 结构体中,新添加一个变量 mask ,使得每一个进程都有自己的 mask ,即要跟踪的系统调用。
struct proc {
...
int mask; // Mask
};
然后我们就可以在 kernel/sysproc.c 给出 sys_trace 函数的具体实现了,只要把传进来的参数给到现有进程的 mask 就好了:
uint64
sys_trace(void)
{
int mask;
// 取 a0 寄存器中的值返回给 mask
if(argint(0, &mask) < 0)
return -1;
// 把 mask 传给现有进程的 mask
myproc()->mask = mask;
return 0;
}
接下来我们就要把输出功能实现,因为 RISCV 的 C 规范是把返回值放在 a0 中,所以我们只要在调用系统调用时判断是不是 mask 规定的输出函数,如果是就输出。
在proc 结构体(见 kernel/proc.h )自己定义一个数组,作为调用的系统调用名称。我这里直接在 kernel/syscall.c 中定义了,这里注意系统调用名字一定要按顺序,第一个为空,当然你也可以去掉第一个空字符串,但要记得取值的时候索引要减一,因为这里的系统调用号是从 1 开始的。
static char *syscall_names[] = {
"", "fork", "exit", "wait", "pipe",
"read", "kill", "exec", "fstat", "chdir",
"dup", "getpid", "sbrk", "sleep", "uptime",
"open", "write", "mknod", "unlink", "link",
"mkdir", "close", "trace"};
进程序号直接通过 p->pid 就可以取到,函数名称需要从我们刚刚定义的数组中获取,即 syscall_names[num] ,其中 num 是从寄存器 a7 中读取的系统调用号,系统调用的返回值就是寄存器 a0 的值了,直接通过 p->trapframe->a0 语句获取即可。注意上面说的那个空格。
void
syscall(void)
{
int num;
struct proc *p = myproc();
num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num]();
// 下面是添加的部分
if((1 << num) & p->mask) {//p->mask为输入的掩码,num为当前系统调用的序号
printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num], p->trapframe->a0);
}
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
然后在 kernel/proc.c 中 fork 函数调用时,添加子进程复制父进程的 mask 的代码:
int
fork(void)
{
...
pid = np->pid;
np->state = RUNNABLE;
// 子进程复制父进程的 mask
np->mask = p->mask;
...
}
最后在 Makefile 的 UPROGS 中添加:
UPROGS=\
...
$U/_trace\
4 . 实验结果
编译并运行 xv6 进行测试。
$ make qemu
只要和 2的系统调用号次幂相与之后为真(如2147483647与当前系统调用号相与),就会被打印出来
退出 xv6 ,运行单元测试检查结果是否正确。
五、 Sysinfo
1 . 实验要求
在本实验中,您将添加一个系统调用 sysinfo ,它收集有关正在运行的系统信息。系统调用接受一个参数:一个指向 struct sysinfo 的指针(参见 kernel/sysinfo.h )。内核应该填写这个结构体的字段: freemem 字段应该设置为空闲内存的字节数, nproc 字段应该设置为状态不是 UNUSED 的进程数。我们提供了一个测试程序 sysinfotest ;如果它打印 "sysinfotest:OK" ,则实验结果通过测试。
2 . 实验提示
将 $U/_sysinfotest 添加到 Makefile 的 UPROGS 中。
运行 make qemu , 你将看到编译器无法编译 user/sysinfotest.c 。添加系统调用 sysinfo ,按照与之前实验相同的步骤。要在 user/user.h 中声明 sysinfo() 的原型,您需要预先声明 struct sysinfo :
struct sysinfo;
int sysinfo(struct sysinfo *);
修复编译问题后,运行 sysinfotest 会失败,因为你还没有在内核中实现系统调用。
sysinfo 需要复制一个 struct sysinfo 返回用户空间;有关如何使用 copyout() 执行此操作的示例,请参阅 sys_fstat() ( kernel/sysfile.c ) 和 filestat() ( kernel/file.c )。
要收集空闲内存量,请在 kernel/kalloc.c 中添加一个函数。
要收集进程数,请在 kernel/proc.c 中添加一个函数。
3 . 实验步骤
跟上个实验一样,首先定义一个系统调用的序号。系统调用序号的宏定义在 kernel/syscall.h 文件中。我们在 kernel/syscall.h 添加宏定义 SYS_sysinfo 如下:
#define SYS_sysinfo 23
在 user/usys.pl 文件加入下面的语句:
entry("sysinfo");
然后在 user/user.h 中添加 sysinfo 结构体以及 sysinfo 函数的声明:
struct stat;
struct rtcdate;
// 添加 sysinfo 结构体
struct sysinfo;
// system calls
...
int sysinfo(struct sysinfo *);
在 kernel/syscall.c 中新增 sys_sysinfo 函数的定义:
extern uint64 sys_sysinfo(void);
在 kernel/syscall.c 中函数指针数组新增 sys_trace :
[SYS_sysinfo] sys_sysinfo,
记得在 kernel/syscall.c 中的 syscall_names 新增一个 sys_trace :
static char *syscall_names[] = {
"", "fork", "exit", "wait", "pipe",
"read", "kill", "exec", "fstat", "chdir",
"dup", "getpid", "sbrk", "sleep", "uptime",
"open", "write", "mknod", "unlink", "link",
"mkdir", "close", "trace", "sysinfo"};
接下来我们就要开始写相应的函数实现了。
首先我们写获取可用进程数目的函数实现。通过阅读 kernel/proc.c 文件可以看到下面的语句:
struct proc proc[NPROC];
这是一个进程数组的定义,这里保存了所有的进程。
我们再阅读 kernel/proc.h 查看进程结构体的定义:
enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
// 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; // Mask
};
可以看到,进程里面已经保存了当前进程的状态,所以我们可以直接遍历所有进程,获取其状态判断当前进程的状态是不是为 UNUSED 并统计数目就行了。当然,通过 proc 结构体的定义,我们知道使用进程状态时必须加锁,我们在 kernel/proc.c 中新增函数 nproc 如下,通过该函数以获取可用进程数目:
// Return the number of processes whose state is not UNUSED
uint64
nproc(void)
{
struct proc *p;
// counting the number of processes
uint64 num = 0;
// traverse all processes
for (p = proc; p < &proc[NPROC]; p++)
{
// add lock
acquire(&p->lock);
// if the processes's state is not UNUSED
if (p->state != UNUSED)
{
// the num add one
num++;
}
// release lock
release(&p->lock);
}
return num;
}
接下来我们来实现获取空闲内存数量的函数。可用空间的判断在 kernel/kalloc.c 文件中。
这里定义了一个链表,每个链表都指向上一个可用空间,这里的 kmem 就是一个保存最后链表的变量。
struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
继续分析文件kernel/kalloc.c可知,这里把从 end (内核后的第一个地址) 到 PHYSTOP (KERNBASE + 128*1024*1024) 之间的物理空间以 PGSIZE 为单位全部初始化为 1 ,然后每次初始化一个 PGSIZE,就把上一次初始化好的页放到当前页的下一个,然后把当前页挂在了 kmem.freelist 上, 所以 kmem.freelist 永远指向最后一个可用页,那我们只要顺着这个链表往后走,直到 NULL 为止。所以我们就可以在 kernel/kalloc.c 中新增函数 free_mem ,以获取空闲内存数量:
// Return the number of bytes of free memory
uint64
free_mem(void)
{
struct run *r;
// counting the number of free page
uint64 num = 0;
// add lock
acquire(&kmem.lock);
// r points to freelist
r = kmem.freelist;
// while r not null
while (r)
{
// the num add one
num++;
// r points to the next
r = r->next;
}
// release lock
release(&kmem.lock);
// page multiplicated 4096-byte page
return num * PGSIZE;
}
然后在 kernel/defs.h 中添加上述两个新增函数的声明:
// kalloc.c
...
uint64 free_mem(void);
// proc.c
...
uint64 nproc(void);
接下来我们按照实验提示,添加 sys_sysinfo 函数的具体实现,这里提到 sysinfo 需要复制一个 struct sysinfo 返回用户空间,根据实验提示使用 copyout() 执行此操作,我们查看 kernel/sysfile.c 文件中的 sys_fstat() 函数,如下:
uint64
sys_fstat(void)
{
struct file *f;
uint64 st; // user pointer to struct stat
if(argfd(0, 0, &f) < 0 || argaddr(1, &st) < 0)
return -1;
return filestat(f, st);
}
这里可以看到调用了 filestat() 函数,该函数在 kernel/file.c 中,如下:
// Get metadata about file f.
// addr is a user virtual address, pointing to a struct stat.
int
filestat(struct file *f, uint64 addr)
{
struct proc *p = myproc();
struct stat st;
if(f->type == FD_INODE || f->type == FD_DEVICE){
ilock(f->ip);
stati(f->ip, &st);
iunlock(f->ip);
if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
return -1;
return 0;
}
return -1;
}
我们可以知道,复制一个 struct sysinfo 返回用户空间需要调用 copyout() 函数,上面是一个例子,我们来查看一下 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)
{
uint64 n, va0, pa0;
while(len > 0){
va0 = PGROUNDDOWN(dstva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0)
return -1;
n = PGSIZE - (dstva - va0);
if(n > len)
n = len;
memmove((void *)(pa0 + (dstva - va0)), src, n);
len -= n;
src += n;
dstva = va0 + PGSIZE;
}
return 0;
}
该函数其实就是把在内核地址 src 开始的 len 大小的数据拷贝到用户进程 pagetable 的虚地址 dstva 处,所以 sys_sysinfo 函数实现里先用 argaddr 函数读进来我们要保存的在用户态的数据 sysinfo 的指针地址,然后再把从内核里得到的 sysinfo 开始的内容以 sizeof(info) 大小的的数据复制到这个指针上。模仿上面filestat()函数,我们在 kernel/sysproc.c 文件中添加 sys_sysinfo 函数的具体实现如下:
// add header
#include "sysinfo.h"
uint64
sys_sysinfo(void)
{
// addr is a user virtual address, pointing to a struct sysinfo
uint64 addr;
struct sysinfo info;
struct proc *p = myproc();
if (argaddr(0, &addr) < 0)
return -1;
// get the number of bytes of free memory
info.freemem = free_mem();
// get the number of processes whose state is not UNUSED
info.nproc = nproc();
if (copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0)
return -1;
return 0;
}
最后在 user 目录下添加一个 sysinfo.c 用户程序:
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/sysinfo.h"
#include "user/user.h"
int
main(int argc, char *argv[])
{
// param error
if (argc != 1)
{
fprintf(2, "Usage: %s need not param\n", argv[0]);
exit(1);
}
struct sysinfo info;
sysinfo(&info);
// print the sysinfo
printf("free space: %d\nused process: %d\n", info.freemem, info.nproc);
exit(0);
}
最后在 Makefile 的 UPROGS 中添加:
$U/_sysinfotest\
$U/_sysinfo\
4 . 实验结果
编译并运行 xv6 进行测试。
$ make qemu
退出 xv6 ,运行单元测试检查结果是否正确。
./grade-lab-syscall sysinfo
通过测试样例。
六、参考文档
[1] 操作系统实验Lab 2:system calls(MIT 6.S081 FALL 2020)_编写 system_call.c 文件输出进程号 pid-CSDN博客
[2] Russ Cox, Frans Kaashoek, Robert Morris, xv6: A simple, Unix-like teaching operating system, 2020.