XV6实验记录(2020)
环境搭建
参考连接
Lab guidance (mit.edu)
6.S081 / Fall 2020 (mit.edu)
xv6 book中文版
Lab1:Xv6 and Unix utilities
实现几个unix实用工具,熟悉xv6的开发环境以及系统调用
Boot xv6
就是准备环境,克隆仓库,编译。
git clone git://g.csail.mit.edu/xv6-labs-2020
cd xv6-labs-2020
git checkout util //切换分支
make qemu //build and run xv6
编译通过后会进一个类似shell的界面,退出是Ctrl-a x
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
.
似乎是为了熟悉一些系统调用,需要了解xv6 book的第一章的前置知识。
xv6 book chapter1
参考一个大佬的博客:MIT 6.S081 Lecture Notes | Xiao Fan (樊潇) (fanxiao.tech)
- 进程和内存
每个进程都拥有自己的用户空间内存以及内核空间状态,当进程不再执行时,xv6会存储和这些进程有关的CPU寄存器到下一次运行这些进程。kernel中一个进程有唯一的PID
常用的syscall
-
fork
:原型是int fork()
。作用是让一个进程生成另一个和这个进程的内存内容相同的子进程。在父进程中,fork
的返回子进程的PID,在子进程中,返回值时0 -
exit
:原型int exit(int status)
。作用是让调用它的进程停止执行并且将内存等占用的资源全部释放。status
是状态参数,0代表正常退出,1代表非正常退出 -
wait
:原型int wait(int *status)
。等待子进程退出,返回子进程PID,子进程的退出状态存储到*status地址中。如果没有调用子进程,wait返回-1。 -
exec
:原型int exec(char *file, char *argv[])
.作用是加载 一个文件,获取执行它的参数,执行。执行错误返回-1,执行成功则不会返回,而开始从文件入口位置开始执行命令,文件格式必须是ELF格式。
- IO 和 文件描述符
-
file descriptor:文件描述符,一个被内核管理的、可以被进程读、写的对象的一个整数,通过打开文件、目录、设备等方式获得。一个文件被打开的越早,文件描述符越小。每个进程都有自己独立的文件描述符列表,0是标准输入,1是标准输出,2是标准错误。shell保证总是3个文件描述符是可用的,在给的源码中的
sh.c
中有这样一段代码“int fd; // Ensure that three file descriptors are open. while((fd = open("console", O_RDWR)) >= 0){ if(fd >= 3){ close(fd); break; } }
-
read
和write
:原型int write(int fd, char *buf, int n)
和int read(int fd, char *buf, int n)
。实现从/向文件描述符fd
中写n字节buf
内容,返回值时读取/写入的字节数。每个文件描述符有一个offset,read
会从这个offset开始读取内容,读完n个字节后将offset后移n个字节,下一个read
从新的offset开始读取字节。write
类似。 -
close
:原型int close(int fd)
,作用是将打开的文件fd释放,使该文件描述符可以被后面的系统调用使用。父进程的fd table不会被子进程的变化硬性,但文件中的offset共享。
-
dup
:原型int dup(int fd)
,复制一个新的fd
指向的I/O对象,返回这个新的fd值,两个I/O对象的offset相同。
- 管道Pipes
管道是暴露给进程的一对文件描述符,一个文件描述符用来读,另一个文件描述符用来 写,将数据从管道的一端写入,将使其能够被从管道的另一端读出。
pipe
也是一个系统调用,原型是int pipe(int p[])
,p[0]
为读取的文件描述符,p[1]
为写入的文件描述符。
- 文件系统
xv6文件系统包含了文件(byte arrays)和目录(对其他文件和目录的引用)。目录生成了一个树,树从根目录/
开始。对于不以/
开头的路径,认为是是相对路径
相关系统调用:
-
mknod
:创建设备文件,一个设备文件有一个major device #和一个minor device #用来唯一确定这个设备。当一个进程打开了这个设备文件是,内核会将read
和write
系统调用重新定向到设备上。 -
一个文件的名称和文件本身是不一样的,文件本身,也叫inode,可以有多个名字,也叫link,每个link包括了一个文件名和一个对inode的引用。一个inode存储了文件的元数据,包括该文件的类型(file, directory or device)、大小、文件在硬盘中的存储位置以及指向这个inode的link的个数
-
fstat
:原型为int fstat(int fd, struct stat *st)
,作用是将inode中的相关信息存储到st
中。 -
link
:创建一个指向同一个inode的文件名,unlink
则是将一个文件名从文件系统中移除,只有当指向这个inode的文件名的数量为0时这个inode以及其存储的文件内容才会被从硬盘上移除
学习完提供的系统调用,就得实现sleep函数了。
实现如下:
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc, char *argv[])
{
if(argc < 2)
{
printf("usage: sleep <ticks>\n");
}
sleep(atoi(argv[1]));
exit(0);
return 0;
}
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
.
就是用两个管道实现父进程和子进程之间通信
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc, char *argv[])
{
int pid;
int p1[2], p2[2]; // p1: parent --> child, p2: child --> parent; 0: read fd, 1: write fd
char buf[1];
pipe(p1);
pipe(p2);
pid = fork();
if(pid < 0)
{
printf("fork error\n");
exit(1);
}
else if(pid == 0) // child process
{
close(p1[1]); // 子进程收信息,发信息,关闭p1的write,防止read的时候阻塞
close(p2[0]); // 关闭子进程到父进程管道的read,防止子进程写的时候阻塞,父进程中同理
read(p1[0], buf, 1);
printf("%d: received ping\n", getpid());
write(p2[1], " ", 1);
close(p1[0]);
close(p2[1]);
exit(0);
}
else // parent process
{
close(p1[0]);
close(p2[1]);
write(p1[1], " ", 1);
read(p2[0], buf, 1);
printf("%d: received pong\n", getpid());
close(p1[1]);
close(p2[0]);
exit(0);
}
return 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
.
就是通过管道实现筛选素数的倍数。采用递归实现,每个进程中读取管道中的第一个数,就是一个素数,然后创建一个新的管道,将筛后的素数传到管道里,传给子进程。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
void child(int *pl)
{
int pr[2];
int n;
close(pl[1]);
int resd_size = read(pl[0], &n, sizeof(int));
if(resd_size == 0)
{
exit(0);
}
pipe(pr);
if(fork() == 0)
{
child(pr);
}
else
{
printf("prime %d\n", n);
close(pr[0]);
int t = n;
while(read(pl[0], &n, sizeof(int)) != 0)
{
if(n % t != 0) write(pr[1], &n, sizeof(int));
}
close(pl[0]);
close(pr[1]);
wait(0);
exit(0);
}
}
int main(int argc, char *argv[])
{
int p[2];
pipe(p);
if(fork() == 0)
{
child(p);
}
else
{
close(p[0]);
for(int i=2;i<=35;i++)
write(p[1], &i, sizeof(int));
close(p[1]);
wait(0); // 等待子进程完成
exit(0);
}
return 0;
}
find
Write a simple version of the UNIX find program: find all the files in a directory tree with a specific name. Your solution should be in the file
user/find.c
.
就是实现在给定路径下出发,查找所有路径中给定文件名的路径,输出所有文件的路径,可以根据ls.c
改造。代码如下
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
void find(char *path, char *target)
{
char buf[512], *p;
int fd;
struct dirent de;
struct stat st; // 存储打开的inode中的相关信息。
if((fd = open(path, 0)) < 0){
fprintf(2, "find: cannot open %s\n", path);
return;
}
if(fstat(fd, &st) < 0){ // 将inode中的信息存到结构体st中
fprintf(2, "find: cannot stat %s\n", path);
close(fd);
return;
}
switch(st.type){
case T_FILE:
if(strcmp(path+strlen(path) - strlen(target), target) == 0)// 比较路径结尾是不是和target相等。
{
printf("%s\n", path);
}
break;
case T_DIR:
if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf){
printf("find: path too long\n");
break;
}
strcpy(buf, path);
p = buf+strlen(buf);
*p++ = '/';
while(read(fd, &de, sizeof(de)) == sizeof(de)){
if(de.inum == 0)
continue;
memmove(p, de.name, DIRSIZ);
p[DIRSIZ] = 0;
if(stat(buf, &st) < 0){
printf("find: cannot stat %s\n", buf);
continue;
}
if(strcmp(buf+strlen(buf) - 2, "/.") != 0 && strcmp(buf+strlen(buf)-3, "/..") != 0)// 递归新打开的路径中的文件
{
find(buf, target);
}
}
break;
}
close(fd);
}
int main(int argc, char *argv[])
{
if(argc < 3)
{
printf("error please input: find <path> <target>\n");
exit(0);
}
char target[512];
target[0] = '/';
strcpy(target+1, argv[2]);
find(argv[1], target);
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
.
首先得弄懂xargs的功能,以及一些概念。
命令行参数与标准化输入
命令行参数是在shell中输入命令时跟在命令后边的参数,例如mkdir a b c
,a, b, c
就是mkdir
接收的命令行参数。
标准化输入是程序执行时,在shell中输入的东西,程序所等待的就是标准化输入。
标准化输出: 命令返回结果就是一个标准化输出
管道符 |
管道符的作用是前一个命令的输出会作为后一个命令的输入
例如
cmdA | cmdB
cmdA的输出会作为cmdB的输入
xagrs
xargs与管道符搭配使用,前一个命令的输出会作为后一个命令的命令行参数。
了解了这些知识后,我还是不太会写,不得不找教程了,看了B站大佬的视频,真通透
MIT6.S081操作系统实验-Lab1-实现简易版unix的xargs_哔哩哔哩_bilibili
实现代码如下:
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/param.h"
#include "kernel/fs.h"
#define MSGSIZE 16
int main(int argc, char *argv[])
{
char buf[MSGSIZE];
/*****************
xv6 book 有这样一段描述
每个进程都有一张表,而 xv6 内核就以文件描述符作为这张表的索引,所以每个进程都有一个从0 开始的文件描述符空间。按照惯例,进程从文件描述符0读入(标准输入),从文件描述符1输出(标 准输出),从文件描述符2输出错误(标准错误输出)。我们会看到 shell 正是利用了这种惯例来 实现 I/O 重定向。shell 保证在任何时候都有3个打开的文件描述符(8007),他们是控制台(console)的默认文件描述符。
**********************/
// 所以从fd 0读入xargs的标准输入,通过字符串处理,用exec传递命令行参数
read(0, buf, MSGSIZE);
// printf("%s\n", buf);
int n = read(0, buf, MSGSIZE);
int buf_idx = 0;
while(n>0)
{
buf_idx += n;
n = read(0, &buf[buf_idx], MSGSIZE);
}
char *xargv[MAXARG];
int xargc = 0;
for(int i=1;i<argc;i++)
{
xargv[xargc] = argv[i];
xargc++;
}
char *p = buf;
for(int i=0;i<MAXARG;i++)
{
if(buf[i] == '\n')
{
int pid = fork();
if(pid > 0)
{
p = &buf[i+1];
wait(0);
}
else
{
buf[i] = 0;
xargv[xargc] = p;
xargc++;
xargv[xargc] = 0;
exec(xargv[0], xargv);
exit(0);
}
}
}
exit(0);
}
至此,Lab1:Xv6 and Unix utilities必做实验部分结束,感觉好难啊。
Lab2: system calls
前置知识(对Xv6的了解)
操作系统必须满足三个要求:多路复用、隔离和交互,Xv6 bool第二章就是介绍相关知识的。
1. 用户态,核心态,以及系统调用
RISC-V有三种CPU可以执行的模式: 机器模式、用户模式和管理模式,机器模式是cpu启动时的状态,主要用于配置计算机,然后更改为管理模式。在管理模式下,CPU被允许执行特权指令。
想要调用内核函数的应用程序必须过渡到内核,CPU提供一个特殊的指令,将CPU从用户模式切换到管理模式,并在内核指定的入口点进入内核(RISC-V为此提供ecall
指令)。一旦CPU切换到管理模式,内核就可以验证系统调用的参数,决定是否允许应用程序执行请求的操作,然后拒绝它或执行它。
2. 内核组织
宏内核:整个操作系统都驻留在内核中,所有的系统调用的实现都以管理模式运行。
微内核:操作系统设计者可以最大限度地减少在管理模式下运行的操作系统代码量,并在用户模式下执行大部分操作系统。这种内核组织被称为微内核(microkernel)
图2.1说明了这种微内核设计。在图中,文件系统作为用户级进程运行。作为进程运行的操作系统服务被称为服务器。为了允许应用程序与文件服务器交互,内核提供了允许从一个用户态进程向另一个用户态进程发送消息的进程间通信机制。例如,如果像shell这样的应用程序想要读取或写入文件,它会向文件服务器发送消息并等待响应。
3. XV6架构
XV6的源代码位于***kernel/***子目录中,源代码按照模块化的概念划分为多个文件,图2.2列出了这些文件,模块间的接口都被定义在了def.h(kernel/defs.h)。
文件 | 描述 |
---|---|
bio.c | 文件系统的磁盘块缓存 |
console.c | 连接到用户的键盘和屏幕 |
entry.S | 首次启动指令 |
exec.c | exec() 系统调用 |
file.c | 文件描述符支持 |
fs.c | 文件系统 |
kalloc.c | 物理页面分配器 |
kernelvec.S | 处理来自内核的陷入指令以及计时器中断 |
log.c | 文件系统日志记录以及崩溃修复 |
main.c | 在启动过程中控制其他模块初始化 |
pipe.c | 管道 |
plic.c | RISC-V中断控制器 |
printf.c | 格式化输出到控制台 |
proc.c | 进程和调度 |
sleeplock.c | Locks that yield the CPU |
spinlock.c | Locks that don’t yield the CPU. |
start.c | 早期机器模式启动代码 |
string.c | 字符串和字节数组库 |
swtch.c | 线程切换 |
syscall.c | Dispatch system calls to handling function. |
sysfile.c | 文件相关的系统调用 |
sysproc.c | 进程相关的系统调用 |
trampoline.S | 用于在用户和内核之间切换的汇编代码 |
trap.c | 对陷入指令和中断进行处理并返回的C代码 |
uart.c | 串口控制台设备驱动程序 |
virtio_disk.c | 磁盘设备驱动程序 |
vm.c | 管理页表和地址空间 |
4. 进程概述
Xv6(和其他Unix操作系统一样)中的隔离单位是一个进程。进程抽象防止一个进程破坏或监视另一个进程的内存、CPU、文件描述符等。它还防止一个进程破坏内核本身,这样一个进程就不能破坏内核的隔离机制。
内核用来实现进程的机制包括用户/管理模式标志、地址空间和线程的时间切片。
Xv6为每个进程维护一个单独的页表,定义了该进程的地址空间。如图2.3所示,以虚拟内存地址0开始的进程的用户内存地址空间。首先是指令,然后是全局变量,然后是栈区,最后是一个堆区域(用于malloc
)以供进程根据需要进行扩展。
每个进程都有一个执行线程(或简称线程)来执行进程的指令。一个线程可以挂起并且稍后再恢复。为了透明地在进程之间切换,内核挂起当前运行的线程,并恢复另一个进程的线程。线程的大部分状态(本地变量、函数调用返回地址)存储在线程的栈区上。每个进程有两个栈区:一个用户栈区和一个内核栈区(p->kstack
)。当进程执行用户指令时,只有它的用户栈在使用,它的内核栈是空的。当进程进入内核(由于系统调用或中断)时,内核代码在进程的内核堆栈上执行;当一个进程在内核中时,它的用户堆栈仍然包含保存的数据,只是不处于活动状态。进程的线程在主动使用它的用户栈和内核栈之间交替。内核栈是独立的(并且不受用户代码的保护),因此即使一个进程破坏了它的用户栈,内核依然可以正常运行。
一个进程可以通过执行RISC-V的ecall
指令进行系统调用,该指令提升硬件特权级别,并将程序计数器(PC)更改为内核定义的入口点,入口点的代码切换到内核栈,执行实现系统调用的内核指令,当系统调用完成时,内核切换回用户栈,并通过调用sret
指令返回用户空间,该指令降低了硬件特权级别,并在系统调用指令刚结束时恢复执行用户指令。
5. 启动XV6和第一个进程
当RISC-V计算机上电时,它会初始化自己并运行一个存储在只读内存中的引导加载程序。引导加载程序将xv6内核加载到内存中。然后,在机器模式下,中央处理器从_entry
(kernel/entry.S:6)开始运行xv6。Xv6启动时页式硬件(paging hardware)处于禁用模式:也就是说虚拟地址将直接映射到物理地址。
加载程序将xv6内核加载到物理地址为0x80000000
的内存中。它将内核放在0x80000000
而不是0x0
的原因是地址范围0x0:0x80000000
包含I/O设备。
_entry
的指令设置了一个栈区,这样xv6就可以运行C代码。Xv6在start. c (kernel/start.c:11)文件中为初始栈stack0声明了空间。由于RISC-V上的栈是向下扩展的,所以_entry
的代码将栈顶地址stack0+4096
加载到栈顶指针寄存器sp
中。现在内核有了栈区,_entry
便调用C代码start
(kernel/start.c:21)。
函数start
执行一些仅在机器模式下允许的配置,然后切换到管理模式。RISC-V提供指令mret
以进入管理模式,该指令最常用于将管理模式切换到机器模式的调用中返回。而start
并非从这样的调用返回,而是执行以下操作:它在寄存器mstatus
中将先前的运行模式改为管理模式,它通过将main
函数的地址写入寄存器mepc
将返回地址设为main
,它通过向页表寄存器satp
写入0来在管理模式下禁用虚拟地址转换,并将所有的中断和异常委托给管理模式。
在进入管理模式之前,start
还要执行另一项任务:对时钟芯片进行编程以产生计时器中断。清理完这些“家务”后,start
通过调用mret
“返回”到管理模式。这将导致程序计数器(PC)的值更改为main
(kernel/main.c:11)函数地址。
在main
(kernel/main.c:11)初始化几个设备和子系统后,便通过调用userinit
(kernel/proc.c:212)创建第一个进程,第一个进程执行一个用RISC-V程序集写的小型程序:initcode. S (***user/initcode.S:***1),它通过调用exec
系统调用重新进入内核。正如我们在第1章中看到的,exec
用一个新程序(本例中为 /init
)替换当前进程的内存和寄存器。一旦内核完成exec
,它就返回/init
进程中的用户空间。如果需要,init
(user/init.c:15)将创建一个新的控制台设备文件,然后以文件描述符0、1和2打开它。然后它在控制台上启动一个shell。系统就这样启动了。
trace
看完了这些基础知识,开始做实验,打开文档,懵逼了,不会,读都读不懂。。。
开始找资料,终于看了B站的视频,有点头绪了,关键是看官方给的hint,一步一步的创建,然后在创建的函数里实现功能。
官方hint步骤如下:
- Add
$U/_trace
to UPROGS in Makefile- Run make qemu and you will see that the compiler cannot compile
user/trace.c
, because the user-space stubs for the system call don’t exist yet: add a prototype for the system call touser/user.h
, a stub touser/usys.pl
, and a syscall number tokernel/syscall.h
. The Makefile invokes the perl scriptuser/usys.pl
, which producesuser/usys.S
, the actual system call stubs, which use the RISC-Vecall
instruction to transition to the kernel. Once you fix the compilation issues, run trace 32 grep hello README; it will fail because you haven’t implemented the system call in the kernel yet.- 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
). The functions to retrieve system call arguments from user space are inkernel/syscall.c
, and you can see examples of their use inkernel/sysproc.c
.- Modify
fork()
(seekernel/proc.c
) to copy the trace mask from the parent to the child process.- Modify the
syscall()
function inkernel/syscall.c
to print the trace output. You will need to add an array of syscall names to index into.
第一个hint就是添加编译,第二和第三个hint是创建syscall的一个步骤,涉及到了syscall调用的流程,
user/user.h: 用户态程序,调用函数 trace()
user/usys.S: 跳板函数 trace() 使用 CPU 提供的 ecall 指令,调用到内核态
kernel/syscall.c 到达内核态统一系统调用处理函数 syscall(),所有系统调用都会跳到这里来处理。
kernel/syscall.c syscall() 根据跳板传进来的系统调用编号,查询 syscalls[] 表,找到对应的内核函数并调用。
kernel/sysproc.c 到达 sys_trace() 函数,执行具体内核操作
因此根据hint2和hint3在user/user.h
中添加函数声明,如下:
// system calls
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);
int trace(int); // 添加的trace函数声明
在user/usys.pl
中有样学样的添加entry("trace")
如下:
#!/usr/bin/perl -w
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("trace");// 添加的内容
这个脚本在运行后会生成 usys.S 汇编文件,里面定义了每个 system call 的用户态跳板函数
在kernel/sysproc.c
中添加sys_trace()
函数,同样有样学样的添加:
uint64
sys_trace(void)
{
int mask;
struct proc *p = myproc();
if(argint(0, &mask) < 0)
return -1;
p->trace_mask = mask;
return 0;
}
添加完这些东西,运行make qemu
就能通过了,剩下就是功能的实现,但是其中参数传递又成了一个难题。
这里sys_trace()
如何获得用户态传来的参数mask
需要解决掉,hint里提到了proc,这是个啥,打开一看,原来是存储进程中有关信息的结构体,根据这个提示,再看了b站大佬的视频终于知道了需要在进程结构体中添加一个mask的变量,就能实现了。
在上边工作的基础上,只需稍加修改,就能实现了,其中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]();
if((p->trace_mask >> num) & 1)
printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num-1], p->trapframe->a0);// a0是个寄存器,具体也不太理解。
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
此外,还需要在freeproc
中将mask值清除掉,在fork
中将mask复制给子进程。
至此,trace实验就完成了,没弄懂syscall的流程确实很难理解,好难啊。
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 astruct sysinfo
(seekernel/sysinfo.h
). The kernel should fill out the fields of this struct: thefreemem
field should be set to the number of bytes of free memory, and thenproc
field should be set to the number of processes whosestate
is notUNUSED
. We provide a test programsysinfotest
; you pass this assignment if it prints “sysinfotest: OK”.
这个实验的过程和上个实验的流程基本是一致的,首先要根据上个实验的提示,在相关文件中添加内容,实现创建系统调用的功能。然后根据提示进行推进实验。
提示内容为:
Add
$U/_sysinfotest
to UPROGS in MakefileRun make qemu;
user/sysinfotest.c
will fail to compile. Add the system call sysinfo, following the same steps as in the previous assignment. To declare the prototype for sysinfo()in user/user.h
you need predeclare the existence ofstruct sysinfo
:struct sysinfo; int sysinfo(struct sysinfo *);
Once you fix the compilation issues, run
sysinfotest
; it will fail because you haven’t implemented the system call in the kernel yet.
sysinfo needs to copy a
struct sysinfo
back to user space; seesys_fstat()
(kernel/sysfile.c
) andfilestat()
(kernel/file.c
) for examples of how to do that usingcopyout()
.To collect the amount of free memory, add a function to
kernel/kalloc.c
To collect the number of processes, add a function to
kernel/proc.c
第一条提示是添加编译,第二条提示是在in user/user.h
里面添加声明,第三条提示是让进入相关文件进行学习如何copy一个sysinfo到用户态,好的进去看看。
在kernel/sysfile.c
中的sys_fstat()
实现如下:
sys_fstat(void)
{
struct file *f;
uint64 st; // user pointer to struct stat
if(argfd(0, 0, &f) < 0 || argaddr(1, &st) < 0) // 主要就是这句,argaddr实现了将内容存到用户态的st指针的缓冲区
return -1;
return filestat(f, st);
}
kernel/file.c
中的filestat()
实现如下:
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);
// 使用 copyout,结合当前进程的页表,获得进程传进来的指针(逻辑地址)对应的物理地址
// 并将&st中的内容复制到addr中,供用户使用。
if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
return -1;
return 0;
}
return -1;
}
在kernel/sysproc.c
中有样学样添加一个新的函数,实现系统调用的功能
uint64 sys_info(void)
{
struct proc *p = myproc();
uint64 addr; // 用于存放copy到用户态的sysinfo的结构体信息。
struct sysinfo info;
info.freemem = acquire_sysmem(); // 获取空闲内存的函数
info.nproc = acquire_nproc(); // 获取创建进程数量
if(argaddr(0, &addr) < 0)
return -1;
if(copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0)
return -1;
// printf("sys_info say Hi!\n");
return 0;
}
在kernel/kalloc.c
中实现获取空闲内存的函数,这里涉及到了系统中空闲内存的管理方式,大概了解了一下这里是用一个链表连接了所有的空闲区,空闲区管理以页为单位,一个页大小是4096B
uint64 acquire_sysmem(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
uint64 cnt = 0;
while(r)
{
cnt += PGSIZE;
r = r->next;
}
release(&kmem.lock);
return cnt;
}
在kernel/proc.c
中实现获取创建进程的函数,这里用到的一些东西只是模仿的,还没有理解。
uint64 acquire_nproc(void)
{
struct proc *p;
uint64 cnt = 0;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state != UNUSED) {
cnt++;
}
release(&p->lock);
}
return cnt;
}
至此,lab2的必做实验完成了,还是有点不理解,只是了解了一点进程从用户态到用户态的流程,对这个xv6又了解了那么一点。