Lab1 utils
热身lab
1.Boot xv6
略
2.sleep (easy)
写一个简单的程序sleep,它调用xv6内核中的sleep系统调用完成休眠一段时间的功能
xv6中得程序分为,系统调用和应用程序,都在user.h中有声明
我们在第一个lab中都写的是应用程序,只需要在user文件夹下创建新的.c文件,并在makefile中UPROGS字段加入相应程序。
系统调用声明:
具体实现如下:
我们只需要写一个sleep.c文件,int main(int argc, char *argv[])就可以在终端后面加参数了,具体sleep.c文件如下:
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc, char *argv[]) {
if (argc < 2){
printf("the number of agrments is valid");
exit(0);
}
int click = atoi(argv[1]);
sleep (click);
exit(1);
}
很好理解,argc存储一共有几个参数,例如sleep 23 有两个 agrv存储的是具体的argc个参数
sleep 32 存储的就是“sleep”和“32”,atoi就是将字符串转成int整型,传参到sleep即可。
3.pingpong (easy)
利用管道进行进程间通信
read和write都是需要一个文件描述符,参数,参数大小这三个参数,创建传到的时候会有一个整型数组存两个文件描述符,0是读端,1是写端,父进程对管道的写端写入数据,子进程在读端读数据,即可通过管道进行进程间通信。具体实现如下:
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc, char *argv[]){
int p[2];
pipe(p);
if (fork()==0){
char send = 'a';
char receive;
read(p[0], &receive, 1);
int subpid = getpid();
printf("%d: received ping\n",subpid);
write(p[1],&send,1);
exit(0);
} else{
char send = 'b';
char receive;
write(p[1],&send,1);
wait(0);
read(p[0], &receive, 1);
int pid = getpid();
printf("%d: received pong\n",pid);
exit(0);
}
close(p[0]);
close(p[0]);
return 0;
}
3.primes(hard)
它要求我们使用管道通信实现Eratosthenes筛法求解35以内的素数,用Eratosthenes筛法,具体原理可自行百度,简单来说就是通过好多个筛子,每到一个筛子,里面最小的数字作为base,base的倍数全部筛掉(包括base本身),筛完之后进入下一个筛子。
结合我们学到的管道知识,就是设置两个管道,一个管道用于把数据写到父进程的管道,传给子进程,子进程进行Eratosthenes筛法,再传给另一个管道,令上一个管道的读写端的文件描述符和这个管道的读写端的文件描述符相等,这样,在读父管道的读端的时候,就相当于读的事子管道刚筛选出来的数据。
#include "kernel/stat.h"
#include "user/user.h"
int
main (int argc, char *argv[]){
int parentpipe[2] ;
int childpipe[2];
//int num = 2;
pipe(parentpipe);
for(int i = 2;i <= 35;i++){
write(parentpipe[1],&i,1);
}
while(1){
if(fork()==0){
int readnum = 0;
int numcount = 0;
close(parentpipe[1]);
pipe(childpipe);
int readnum_flag = -1;
int base = 0;
while(read(parentpipe[0],&readnum,1)){
// printf("-------%d\n",readnum);
if (readnum_flag == -1){
readnum_flag =1;
base = readnum;
printf("prime %d\n",readnum);
} //第一次读
else{
// printf("aaaaaaaaaaaaaaaaaaaaaa\n");
//printf("num : %d\n",readnum);
if(readnum%base!=0){
//printf("base num : %d\n",base);
// printf("qulifyed num : %d\n",readnum);
write(childpipe[1],&readnum,1);
}
}
numcount++;
}
if(numcount==1){
exit(0);
}
parentpipe[0] = childpipe[0];
parentpipe[1] = childpipe[1];
}
else{
close(parentpipe[0]);
close(parentpipe[1]);
wait(0);
exit(0);
}
}
}
5.find (moderate)
在指定的目录下寻找指定名称的文件并打印出来,可以从ls的实现中,找到灵感
学会用fstat确定文件描述符的性质,是文件还是目录,目录的话,就继续find,文件的话就匹配file,如果一致就输出,不一致,
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
// 格式化路径名,只保留最后一部分(文件或目录名),并根据需要填充空格
char* fmtname(char *path)
{
//static char buf[DIRSIZ+1]; // 静态缓冲区用于返回结果,DIRSIZ 是定义在 fs.h 中的目录项名的最大长度
char *p;
// 从字符串末尾向前查找第一个斜杠('/'),定位到路径的最后一部分的起始位置
for(p=path+strlen(path); p >= path && *p != '/'; p--)
;
p++; // 跳过斜杠,指向路径的最后一部分(文件或目录名)
// 如果路径的最后一部分长度超过 DIRSIZ,直接返回该部分
// if(strlen(p) >= DIRSIZ)
return p;
// 否则,将路径的最后一部分复制到 buf 中,并在其后填充空格,直到达到 DIRSIZ 的长度
//memmove(buf, p, strlen(p));
//memset(buf+strlen(p), ' ', DIRSIZ-strlen(p));
//return buf;
}
// 列出给定路径下的所有文件和目录
void find(char *path,char *file)
{
char *file2 = file;
// printf("file2:%s",file2);
char buf[512], *p; // 缓冲区用于构建完整的文件路径
int fd; // 文件描述符
struct dirent de; // 目录项结构
struct stat st; // 文件状态结构
// 尝试打开给定路径
if((fd = open(path, 0)) < 0){
fprintf(2, "ls: cannot open %s\n", path);
return;
}
// 获取文件或目录的状态信息
if(fstat(fd, &st) < 0){
fprintf(2, "ls: cannot stat %s\n", path);
close(fd);
return;
}
// 根据文件类型处理
switch(st.type){
case T_FILE: // 单个文件
//printf("T_FILE\n");
//printf("file2: %s, path: %s\n",file2,fmtname(path));
// 打印文件信息:格式化的文件名,类型,inode编号,大小
//printf("%s %d %d %l\n", fmtname(path), st.type, st.ino, st.size);
if (strcmp(file2,fmtname(path))==0){
//printf("jinlailema?\n");
strcpy(buf, path);
p = buf+strlen(buf);
*p++ = '/'; // 在路径末尾添加斜杠,为接下来添加文件名做准备
memmove(p, file, DIRSIZ); // 添加文件名到路径
p[DIRSIZ] = 0; // 确保字符串以 null 结尾
printf("%s\n",path);
//printf("---------------\n");
break;
}
// printf("---------------end of t_file\n");
break;
case T_DIR: // 目录
// printf("T_DIR: \n");
// 检查路径长度,避免缓冲区溢出
if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf){
printf("ls: path too long\n");
break;
}
// 构建新的路径
strcpy(buf, path);
p = buf+strlen(buf);
*p++ = '/'; // 在路径末尾添加斜杠,为接下来添加文件名做准备
// 读取目录项
while(read(fd, &de, sizeof(de)) == sizeof(de)){
if (strcmp(de.name, ".") == 0 || strcmp(de.name, "..") == 0)
continue; // 跳过当前目录和父目录的处理
if(de.inum == 0) // 跳过空目录项
continue;
memmove(p, de.name, DIRSIZ); // 添加文件名到路径
p[DIRSIZ] = 0; // 确保字符串以 null 结尾
// 获取并打印文件或目录的信息
if(stat(buf, &st) < 0){
printf("ls: cannot stat %s\n", buf);
continue;
}
//printf("p: %s\n",p);
//printf("buf: %s\n",buf);
//printf("file1: %s\n",fmtname(buf));
//printf("file2: %s\n",file2);
find(buf,file2);
// 打印信息:格式化的文件名,类型,inode编号,大小
//printf("%s %d %d %d\n", fmtname(buf), st.type, st.ino, st.size);
}
//printf("---------------end of t_fir\n");
break;
}
close(fd); // 完成后关闭文件描述符
}
int main(int argc, char *argv[])
{
//int i;
// 如果没有指定路径参数,使用当前目录
if(argc < 3){
exit(0);
}
// 否则,对每个给定的路径参数调用 ls 函数
find(argv[1],argv[2]);
exit(0);
}
6.xargs (moderate)
这个应该是lab1中花费时间最长的一个了,xargs就是可将标准输入作为参数加到后面的命令中,但是这里有一个点需要注意,就是这里的标准输入不是终端的输入,而是管道符前面的命令执行后的参数,通过管道传给xargs,作为标准输入,然后把这些参数放在xargs后面的命令后面,进行执行(子进程exec())
#include "kernel/stat.h"
#include "kernel/param.h"
#include "user/user.h"
int
main(int argc, char* argv[]){
if(argc < 2){
printf("the number of arguments is too small!");
exit(1);
}
char* args[MAXARG];
int status;
memset(args,0,sizeof(args));
for(int i = 1;i < argc; i++){
args[i-1] = argv[i];
}
args[argc] = 0;
while(1){
args[argc-1] = 0;
args[argc-1] = malloc(1024); // 为最后一个参数分配内存
char* p = args[argc-1];
char c;
int n;
while((n =read(0,&c,1))&&c!='\n'){
*p = c;
//printf("p:%c\n",*(p));
++p;
}
// printf("111args[argc-1]:%s \n",args[argc-1]);
*p = '\0';
if(n == 0){
exit(1);
//printf("jinru n=0\n");
//printf("args[argc-1]:%s \n",args[argc-1]);
if(fork()==0){
//printf("args:\n");
for(int i =0;i<argc;i++){
//printf("%s,",args[i]);
}
exec(argv[1],args);
exit(1);
}
else{
wait(&status);
}
exit(1);
}
else {
//printf("jinru n=1\n");
if(fork()==0){
exec(argv[1],args);
}
else{
wait(&status);
}
continue;
}
}
return 0;
}
Lab2 Systems call
lab1是利用系统调用实现了一些实用功能,lab2是添加新的系统调用
1.System call tracing
在本作业中,您将添加一个系统调用跟踪功能,该功能可能会在以后调试实验时对您有所帮助。您将创建一个新的
trace
系统调用来控制跟踪。它应该有一个参数,这个参数是一个整数“掩码”(mask),它的比特位指定要跟踪的系统调用。例如,要跟踪fork
系统调用,程序调用trace(1 << SYS_fork)
,其中SYS_fork
是kernel/syscall.h中的系统调用编号。如果在掩码中设置了系统调用的编号,则必须修改xv6内核,以便在每个系统调用即将返回时打印出一行。该行应该包含进程id、系统调用的名称和返回值;您不需要打印系统调用参数。trace
系统调用应启用对调用它的进程及其随后派生的任何子进程的跟踪,但不应影响其他进程。
意思就是添加一个叫trace的系统调用,后面跟一个掩码,用来追踪特定的系统调用,如果后去的进程中调用了要追踪的系统调用,则打印出来对应的进程id,系统调用名称和该系统调用的返回值。所谓掩码就是,二进制翻译成十进制,如果我要追踪系统调用列表(syscall.h)中第二个系统调用,则对应的二进制就是0b000100,第三个就是0b001000,追踪第三、四个就是0b011000.
首先,我先把我做这道题的思路和整个流程介绍一下,然后再介绍整个系统调用从用户态到内核态的整个流程。
首先,我们明确我们要添加的trace是一个系统调用,那既然是系统调用就要在syscall.h加入属于自己的编号,因为我们发现整理包含了所有的系统调用,并且每一个都有属于自己的编号,那我们就照猫画虎:
,然后我们再看看syscall.c(为什么一直纠结syscall的头文件和源文件呢,因为这两个就是系统调用的核心实现,在课程中有提到),我们需要在syscall.c中 用 extern 全局声明新的内核调用函数,并且在 syscalls 映射表中,加入从前面定义的编号到系统调用函数指针的映射
我们虽然照猫画虎,加了这么一个extern uint64 sys_trace(void);但是我们完全没有声明和定义过这个sys_trace(void)函数,在这里,我们可以去找一下上面这些类似sys_uptime, sys_write这些函数都是在哪实现的,我们会发现基本都是在sysproc.c中,因为这些系统调用都与线程有关,所以在sysproc.c中实现,比较方便,具体这些函数在哪实现要看这个系统调用属于哪一个大类,去找到这方面的文件中进行实现最为方便和常见,那么因此,我们也在sysproc.c中加入我们刚extern的sys_trace函数的实现,让我们想一下这个系统调用的原理,给一个掩码,然后之后所有的进程都会跟踪一个特定的系统调用并打印信息对吧,这里需要了解这样一个知识:每当用户调用了一次系统调用,那么都会进入syscall.c的syscall函数中,这个函数长这样:
我们可以看到,这个函数首先会把当前进程的信息用结构体p来存储,陷阱帧中a7用来存系统调用的编号,用num来存储这个编号,编号如果符合要求,就会通过syscalls[num]对相应的系统调用进行调用,这个syscalls,存储的是函数指针。
我们现在可以肯定的是,我们无论调用trace还是read啥的系统调用,我们都会通过这样的形式来进入sys_xxx函数,我们想通过trace来跟踪每个掩码指定的系统调用,是不是可以在proc结构体做出什么文章来,比如在结构体中定义一个变量,这个变量就是我们的掩码参数,我们的sys_trace函数的作用就是给当前proc结构体中这个变量赋值为掩码,然后再在fork中添加”让子进程继承父进程的这个变量“,这样执行trace之后,所有的进程的结构体信息中都会有这个掩码,那么还记得我们之前在syscall函数中用num存储了系统调用的编号了吗?我们是不是可以在syscall函数中用num和我们在proc结构体定义的变量进行一些操作,做个if判断,判断一下当前调用的系统调用是不是我们掩码指定的,如果是,就进入打印信息那一步。现在是不是明确我们在sys_trace中要做的工作了。
首先我们需要在proc结构体中加入一个变量,用于存储掩码:
之后我们再将这个变量在进行sys_trace函数的时候被赋为掩码:
这里argint(0, &mask)就是将我们后面的掩码赋给mask变量。
ok我们现在至少有了sys_trace的定义了。
到现在位置,内核态的工作基本完成,那我们终究是要通过用户态来调用的,所以我们还要把目光从kernel转向user,我们会发现本分支,有一个叫trace.c的文件,这是lab中已经为我们实现好的文件,我们会在trace.c中发现许多系统调用的声明,那我们也照猫画虎好了
然后根据lab的提示,在 usys.pl 中,加入用户态到内核态的跳板函数:
还有 再Makrfile中ROGS添加$U/_trace
还记得我之前提过再fork()也需要添加将子进程继承父进程的mask变量:
至此,实验除了打印信息就完成了大半,我想在介绍打印信息(这里自然就是在syscall函数中判断完掩码之后进行操作啦)之前,梳理一下用户调用系统调用之后,从用户态到内核态的整个流程:
当用户在终端输入 trace 32 之后,首先系统会在user.h中找到int trace(int)这个跳板函数,跳板函数会见系统引导至user/usys.pl,这里的原理是:Makefile调用perl脚本user/usys.pl,它生成实际的系统调用存根user/usys.S ,
entry的具体宏展开是:
sub entry {
my $name = shift;
print ".global $name\n";
print "${name}:\n";
print " li a7, SYS_${name}\n";
print " ecall\n";
print " ret\n";
}
# 经过compiler后, entry("trace")为我们在usys.S里生成了如下的汇编代码
# usys.S
.global trace
trace:
li a7, SYS_trace
ecall
ret
# 我们看到, 上述的重点就是, 把sys_trace的代号放入a7这个寄存器里,
# 然后用riscv提供的ecall指令从, 用户态切入到内核态! 重点!
这里通过ecall指令通过entry宏,从用户态进入内核态,也就是开始执行syscall函数,其中a7存储着系统调用的编号,之后就是执行一遍这个syscall函数就好了(通过syscalls存储的函数指针来具体执行某个系统调用)
这么繁琐的调用流程的主要目的是实现用户态和内核态的良好隔离。
并且由于内核与用户进程的页表不同,寄存器也不互通,所以参数无法直接通过 C 语言参数的形式传过来,而是需要使用 argaddr、argint、argstr 等系列函数,从进程的 trapframe 中读取用户进程寄存器中的参数。
同时由于页表不同,指针也不能直接互通访问(也就是内核不能直接对用户态传进来的指针进行解引用),而是需要使用 copyin、copyout 方法结合进程的页表,才能顺利找到用户态指针(逻辑地址)对应的物理内存地址。
之后打印信息直接看代码即可理解。
值得一提的是 if判断这里 (p->mask)&(1<<num),mask如果和左移一位的num相与为1,则就是我们要追踪的系统调用。
2.Sysinfo