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
如下:
在 user/usys.pl 文件加入下面的语句:
然后在 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 文件可以看到下面的语句:
这是一个进程数组的定义,这里保存了所有的进程。我们再阅读 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;
要想更深入了解的话就详细看看当前这个文件(下面摘了部分内容):
extern char end[]; // first address after kernel.
// defined by kernel.ld.
void
kinit()
{
initlock(&kmem.lock, "kmem");
freerange(end, (void*)PHYSTOP);
}
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
}
// 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;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);
r = (struct run*)pa;
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
这里把从 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)
大小的的数据复制到这个指针上。模仿上面的例子,我们在 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
...
init: starting sh
$ sysinfo
free space: 133386240
used process: 3
$ sysinfotest
sysinfotest: start
sysinfotest: OK
退出 xv6 ,运行单元测试检查结果是否正确。
./grade-lab-syscall sysinfo
通过测试样例。
make: 'kernel/kernel' is up to date.
== Test sysinfotest == sysinfotest: OK (2.6s)
5
Lab 2 所有实验测试
退出 xv6 ,使用命令 vim time.txt
新建文件写入你做该实验所花的时间(小时),运行整个 Lab 2 测试,检查结果是否正确。
$ make grade
...
== Test trace 32 grep ==
$ make qemu-gdb
trace 32 grep: OK (2.3s)
== Test trace all grep ==
$ make qemu-gdb
trace all grep: OK (1.0s)
== Test trace nothing ==
$ make qemu-gdb
trace nothing: OK (0.8s)
== Test trace children ==
$ make qemu-gdb
trace children: OK (10.1s)
== Test sysinfotest ==
$ make qemu-gdb
sysinfotest: OK (2.3s)
== Test time ==
time: OK
Score: 35/35