MIT6s081学习之旅

# 介绍

打算开始学习操作系统了,并选择了MIT6s081这门课,这门课程是麻省理工学院的本科生课程,授课教授是Robert Morris和Frans Kaashoek,两位都是非常出名的程序员。

课程是基于一个类似于Unix但是简单的多的基于RISCV架构的操作系统XV6。Robert Morris 教授曾是一位顶尖黑客,世界上第一个蠕虫病毒 Morris 就是出自他之手。

这门课的前身是 MIT 著名的课程 6.828,MIT 的几位教授为了这门课曾专门开发了一个基于 x86 的教学用操作系统 JOS,被众多名校作为自己的操统课程实验。但随着 RISC-V 的横空出世,这几位教授又基于 RISC-V 开发了一个新的教学用操作系统 xv6,并开设了 MIT6.S081 这门课。由于 RISC-V 轻便易学的特点,学生不需要像此前 JOS 一样纠结于众多 x86 “特有的”为了兼容而遗留下来的复杂机制,而可以专注于操作系统层面的开发。

# 配置环境

实验环境 : WSL2 + Ubuntu20.04

当把Ubuntu配置好后,点击下方链接可以进入MIT课程界面,根据教程配置

https://pdos.csail.mit.edu/6.S081/2021/tools.html 如果不想进入的话输入下方两行代码,耐心等待即可配置成功

```
sudo apt-get update && sudo apt-get upgrade

sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu
```

- **运行**:输入 `make quem`即可进入xv6这个小型操作系统 退出的时候需要按住ctrl + a 然后按 x 即可退出

- **调试** : 对于刚刚下载安装的一套工具链中,已经包含了gdb

  `gdb-multiarch`可以进入gdb调试 , 按`q`退出gdb

**最后:本篇文章会持续更新敬请期待(https://github.com/zibens/My-Journey-of-Learning-MIT-6.S081(这里是我的github后续将上传源码以及其他内容))**

# lab1 -utilities

## 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.
```

第一个lab的第一部分:实现sleep

非常简单 , 命令行中会输入类似`sleep 20`这样的命令,于是我们需要先判断命令行中是否传入了两个参数

- argc : 命令行中传入的参数数量
- *argv : 命令行中传入的参数是什么

```c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc , char *argv[]){
    if(argc <= 1){
        fprintf(2 ,"you should pass argument!\n");
        exit(1);
    }
    int time = atoi(argv[1]);

    sleep(time);
    exit(0);
}
```

写完代码后 ,我们通过`make qemu`来运行模拟器

![image](https://github.com/user-attachments/assets/70ba4820-329d-4666-86d7-e930f4c395ec)


看到测试和官方给的一样,于是可以通过`./grade-lab-util sleep`来查看是否通过

![image](https://github.com/user-attachments/assets/be82aa82-29b9-47d3-a13e-63644087a48a)

当看到这个,恭喜你,通过了第一个部分的实验!

## 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 "<pid>: received ping", where <pid> 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 "<pid>: received pong", and exit. Your solution should be in the file user/pingpong.c.
```

实现这样的一对管道。
首先的想法是可以开两个管道 ,第一个管道是 父进程写 ,子进程读
第二个管道是子进程写 ,父进程读

**注意:父进程先写后读 , 子进程先读后写**
否则,如果父进程先读后写,子进程也是先读后写,双方都会阻塞对方

```c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
/*
父进程向子进程发送一个字节 , 子进程打印<pid> :received ping
然后将字节通过管道写回父进程并退出
*/
int main(int argc ,char* argv[]){
    int pipe1[2] , pipe2[2];
    int pid;
    pipe(pipe1);
    pipe(pipe2);
    char buf[] = {'a'};

    int ret = fork();
    /*
    父进程在管道 pipe1[1]发送, 子进程在pipe1[0]读取
    子进程在管道 pipe2[1]发送 ,父进程在pipe2[0]读取
    */
    if(ret == 0){ //子进程
        //子进程要读
        pid = getpid();
        close(pipe2[0]);
        close(pipe1[1]);
        read(pipe1[0] ,buf,1);
        printf("%d: received ping\n",pid);
        write(pipe2[1] ,buf,1);
        exit(0);
    }
    else{//父进程
        pid = getpid();
        close(pipe2[1]);
        close(pipe1[0]);
        write(pipe1[1], buf, 1);
        read(pipe2[0] , buf , 1);
        printf("%d: received pong\n", pid);
        exit(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.
```

通过递归来实现一个素数筛。
父进程会传入2-35的数字给第一个子进程 , 然后子进程输出第一个素数,并把它的倍数删去,然后将剩余的数传给子进程的子进程......(不断的递归)

![image-20241226193642886](C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20241226193642886.png)

就像这张图片中的一样

由于xv6限制了文件描述符和进程 , 所以我们不能用太多的空间和资源。这里我选择了只用一个管道来不断的读写

这里我的思路是用一个 数组 ,最开始都赋值未0,表示都不是质数,然后可以把1 和 2 赋值成1,调用子进程,子进程会把2的质数都删掉(代码中体现为把它们赋成1),并且把第一个为0的数字输出

同时注意管道的关闭 , **父进程必须等待子进程结束**。

```c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

#define MAX 36
#define  NO '0'
#define YES '1'

void prime(int rp , int wp){
    //子进程
    char nums[MAX];
    
    read(rp , nums , MAX);
    int t = 0;
    //素数筛
    for(int i =0;i<MAX;++i){
        if(nums[i] == NO){
            t = i;
            break;
        }
    }

    if(t == 0){
        exit(0);
    }
    printf("prime %d\n", t);
    nums[t] = YES;
    for(int i =t+1;i<MAX;++i){
        if(i % t == 0)nums[i] = YES;
    }

    int pid = fork();
    if(pid == 0){
        prime(rp ,wp);
        exit(0);
    }
    else{
        write(wp , nums , MAX);
        wait(0);
    }


}


int main(int argc , char *argv[]){
    int p[2];
    pipe(p);

    char nums[MAX];
    for(int i =0;i<MAX;++i){
        nums[i] = NO;
    }

    int pid = fork();
    if(pid == 0){
        prime(p[0] , p[1]);
        wait(0);
    }
    else{
        //1和2都是质数
        nums[0] = YES;
        nums[1] = YES;
        close(p[0]);//关闭读
        write(p[1] , nums , MAX);
        wait(0);
        close(p[1]);
    }
   exit(0);
}
```

## find

```c
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`中的代码

```c
char*
fmtname(char *path)
{
  static char buf[DIRSIZ+1];
  char *p;

  // Find first character after last slash.
  for(p=path+strlen(path); p >= path && *p != '/'; p--)
    ;
  p++;

  // Return blank-padded name.
  if(strlen(p) >= DIRSIZ)
    return p;
  memmove(buf, p, strlen(p));
  memset(buf+strlen(p), ' ', DIRSIZ-strlen(p));
  return buf;
}
```

首先会看到以上这段代码,这是在使路径标准化
例如,输入的路径`char * path ="/home/user/docs/report.txt"`
这段代码会先提取最后一个`/`后面的字符`report.txt`然后判断它的长度 为 10 , 小于`DIRSIZ = 14` , 然后补上空格`report.txt    `  最后buf的内容就是report.txt后面加上4个空格。

```c
 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;
  }
```

这一段内容是在打开传入的 文件夹或文件的路径

```c
switch(st.type){
  case T_FILE:
    printf("%s %d %d %l\n", fmtname(path), st.type, st.ino, st.size);
    break;

  case T_DIR:
    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(de.inum == 0)
        continue;
      memmove(p, de.name, DIRSIZ);
      p[DIRSIZ] = 0;
      if(stat(buf, &st) < 0){
        printf("ls: cannot stat %s\n", buf);
        continue;
      }
      printf("%s %d %d %d\n", fmtname(buf), st.type, st.ino, st.size);
    }
    break;
  }
```

这一段对 刚刚打开的东西进行解析 ,看它是文件夹还是文件

```
case T_FILE:
    printf("%s %d %d %l\n", fmtname(path), st.type, st.ino, st.size);
    break;
```

这里就表示它是文件 ,输入它的内容,就类似我们平时`ls 文件`会输出的内容一样(如图)
![image](https://github.com/user-attachments/assets/df3ff4bc-ab0b-440f-9c4d-09adcb238aa0)

```c
case T_DIR:
    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(de.inum == 0)
        continue;
      memmove(p, de.name, DIRSIZ);
      p[DIRSIZ] = 0;
      if(stat(buf, &st) < 0){
        printf("ls: cannot stat %s\n", buf);
        continue;
      }
      printf("%s %d %d %d\n", fmtname(buf), st.type, st.ino, st.size);
    }
    break;
  }
```

这一段 ,如果是个文件夹的话 , 把`path`复制到`buf`中,然后给buf的末尾加上`/`(比如 : path = /home , buf = /home/)

然后会遍历文件夹下的所有目录并表示成`/home/xxx`

最后在标准化后输出`xxx    type ino size`

看完了`ls.c`就可以实现这题的代码了

```c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
/*
函数的主要目的是从给定的路径字符串中提取文件或目录的名称
并将其标准化为固定长度(DIRSIZ)的字符串。如果名称长度不足 DIRSIZ,则用空格填充。
这在目录列表显示(如 ls 命令)中非常有用,确保所有名称在输出中对齐。
*/

char*
fmtname(char *path)  //标准化路径
{
  static char buf[DIRSIZ+1];
  char *p;

  // Find first character after last slash.
  for(p=path+strlen(path); p >= path && *p != '/'; p--)
    ;
  p++;

  // Return blank-padded name.
  if(strlen(p) >= DIRSIZ)
    return p;
  memmove(buf, p, strlen(p));
  memset(buf+strlen(p), 0, DIRSIZ-strlen(p));
  return buf;
}

/*
   1、 当你 find sleep  -> 就会查找当前目录的sleep文件 也就是 ./sleep

*/
void
find(char *path , char * target)//当前的文件夹
{

  //printf("path : %s , target : %s\n",path , target);
  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;
  }

//   if(!strcmp(fmtname(path) , target)){ //它们相等说明是注释上的第一点
//     printf("%s\n" , target);
//     return;
//   }

  switch(st.type){ //对path考察
  case T_FILE: 
    break;

  case T_DIR: //如果是文件夹
    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(de.inum == 0 || !strcmp(de.name , ".") || !strcmp(de.name , ".."))//必须使用双引号
        continue;
      memmove(p, de.name, DIRSIZ);
      p[DIRSIZ] = 0;
      if(stat(buf, &st) < 0){
        printf("ls: cannot stat %s\n", buf);
        continue;
      }
      

      if(!strcmp(fmtname(buf) , target)){ //标准化只会保留后面的参数
        printf("%s\n" , buf);
      }
        find(buf , target);

    }
    break;
  }
  close(fd);
}

int
main(int argc, char *argv[])
{
    if(argc == 1){
        fprintf(2 , "error!\n");
        exit(1);
    }
    if(argc == 2){ // find console
        find("." , argv[1]);
        exit(0);
    }
    if(argc == 3){
        find(argv[1] , argv[2]);
        exit(0);
    }

  exit(0);
}

```

```c
memset(buf+strlen(p), 0, DIRSIZ-strlen(p));

if(de.inum == 0 || !strcmp(de.name , ".") || !strcmp(de.name , ".."))//必须使用双引号

if(!strcmp(fmtname(buf) , target)){ //标准化只会保留后面的参数
        printf("%s\n" , buf);
      }
        find(buf , target);
```

这里是三个不同的地方

- `memset(buf+strlen(p), 0, DIRSIZ-strlen(p));`这里将' '改成0让文件后面不要有空格
- 后面两个是递归 , 上面是因为题目中说不要递归到`.和..`这两个目录中

## xargs

### xargs是什么?

Unix命令都带有参数 , 有些命令可以接收“标准输入”(stdin)作为参数

```shell
cat /etc/passwd | grep root
```

上面的代码使用了管道命令(==|==) 。管道命令的作用,是将左侧命令(`cat /etc/passwd`)的标准输出转换为标准输入,提供给右侧命令(`grep root`)作为参数。

因为`grep`命令可以接受标准输入作为参数,所以上面的代码等同于下面的代码。

```shell
grep root /etc/passwd
```

但是,大多数命令都不接受标准输入作为参数,只能直接在命令行输入参数,这导致无法用管道命令传递参数。举例来说,`echo`命令就不接受管道传参。

**xargs命令的作用就是 将标准输入转为命令行参数**

```shell
$ echo "hello world" | xargs echo
hello world
```

`xargs`的作用在于,大多数命令(比如`rm`、`mkdir`、`ls`)与管道一起使用时,都需要`xargs`将标准输入转为命令行参数。

```shell
echo "one two three" | xargs mkdir
```

上面的代码等同于`mkdir one two three`。如果不加`xargs`就会报错,提示`mkdir`缺少操作参数。

### xargs.c

```
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.
```

这是实验要求。 首先我们先来看 `xargstest.sh`脚本中的内容

```sh
mkdir a
echo hello > a/b
mkdir c
echo hello > c/b
echo hello > b
find . b | xargs grep hello
```

也就是说每当我们运行脚本,它都会自动创建 a/b , c/b , b 然后查找这三个文件中带`hello`的字符串并打印

于是会有一个很直接的想法就是 , 拿到这个路径后 ,`fork() + exec()`来调用grep函数

所以需要考虑一下,怎么拿到这个路径,可以参考`sh.c`中的getcmd函数和`ulib.c`中的gets函数然后将它改编一下。

gets和getcmd是将 标准输入读取一行字符然后存储到一个缓冲区中,这里`find . b`会输出 

```c
./a/b
./b
./c/b
```

gets和getcmd会将它们逐个的作为参数传递buf缓冲区

```c
#include "kernel/types.h"
#include "user/user.h"
#include "kernel/param.h"

//find . b | xargs grep hello
/*
拿到 grep hello xxx(路径) , 然后fork + exec 来调用 grep
1、 拿到路径
参考 ulib.c and sh.c

*/
#define MAXSIZE 512
char*
getss(char *buf, int max)
{
  int i, cc;
  char c;

  for(i=0; i+1 < max; ){
    cc = read(0, &c, 1);
    if(cc < 1)
      break;
    
    if(c == '\n' || c == '\r' || c == ' ')
      break;

    buf[i++] = c; //keng
  }
  buf[i] = '\0';
  return buf;
}

int
getcmdd(char *buf, int nbuf)
{
  fprintf(2, "$ ");
  memset(buf, 0, nbuf);
  getss(buf, nbuf);
  if(buf[0] == 0) // EOF
    return -1;
  return 0;
}

int main(int argc , char * argv[]){
    //xargs grep hello
    if(argc < 2){
        fprintf(2 , "Usage xargs command\n");
        exit(1);
    }
    char buf[MAXSIZE];
    int _argc = argc -1;
    char *_argv[MAXARG];

    for(int i =0;i<_argc;++i){
        _argv[i] = argv[i+1];
    }

    // for(int i =0;i<_argc;++i)printf("_argv = %s\n" , _argv[i]);

    // printf("\n");
    // printf("\n");


    while((getcmdd(buf ,MAXSIZE)) >=0){
        if(fork() == 0){
            //printf("_argc = %d\n" , _argc);
            
            _argv[_argc] = buf;
            _argc++;
            
            // printf("_argc = %d\n" , _argc);

            // printf("buf=%s\n" , buf);

            // for(int i =0;i<_argc;++i){
            //     printf("argv = %s\n" , _argv[i]);
            // }
            exec(argv[1] , _argv);
            fprintf(2, "exec %s failed\n", argv[1]); // 执行失败
            exit(0);

        }
        else{
            wait(0);
        }
    }


    exit(0);
}
```

# lab2 - syscall

## System call tracing

首先在这之前,需要先了解xv6中是如何进行系统调用的。

user中的函数通过 usys.pl脚本产生一个汇编文件 usys.S ,这个汇编会通过RISC-V的ecall指令进入 kernel中 ,此时会来到 kernel/syscall.c文件中,这时候也就进入内核态了。

```
In this assignment you will add a system call tracing feature that may help you when debugging later labs. You'll create a new trace system call that will control tracing. It should take one argument, an integer "mask", whose bits specify which system calls to trace. For example, to trace the fork system call, a program calls trace(1 << SYS_fork), where SYS_fork is a syscall number from kernel/syscall.h. You have to modify the xv6 kernel to print out a line when each system call is about to return, if the system call's number is set in the mask. The line should contain the process id, the name of the system call and the return value; you don't need to print the system call arguments. The trace system call should enable tracing for the process that calls it and any children that it subsequently forks, but should not affect other processes.
```

这是lab要求,在用户态中会有一个`trace(int)`函数,它有一个参数,表示要跟踪的系统调用。 

下面是`trace.c`源码 

```c
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int
main(int argc, char *argv[])
{
  int i;
  char *nargv[MAXARG];

  if(argc < 3 || (argv[1][0] < '0' || argv[1][0] > '9')){
    fprintf(2, "Usage: %s mask command\n", argv[0]);
    exit(1);
  }

  if (trace(atoi(argv[1])) < 0) {
    fprintf(2, "%s: trace failed\n", argv[0]);
    exit(1);
  }
  
  for(i = 2; i < argc && i < MAXARG; i++){
    nargv[i-2] = argv[i];
  }
  exec(nargv[0], nargv);
  exit(0);
}

```

在这里可以看到**trace()**这样一个函数,也就是要实现的系统调用,它通过里面**atoi(argv[1])**这个int类型的数字来选择要跟踪的系统调用。这里**要跟踪的系统调用** 用一个这样的int类型的掩码组成 , 就如**For example, to trace the fork system call, a program calls trace(1 << SYS_fork), where SYS_fork is a syscall number from kernel/syscall.h.** 题目中说的一样。

接下来要开始实现这个系统调用了

==首先应该将这个函数添加到user.h中==

```c
// 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函数
```

==然后应该向脚本中添加这个函数,使得可以进入kernel==

```c
entry("uptime");
entry("trace");
entry("sysinfo"); //这里
```

==进入kernel之后,来到了syscall.c中,然后在外部函数和一个指针数组中添加新加入的这个系统调用,同时在syscall.h中为新加入的系统调用设置一个掩码==
**syscall.h**

```c
#define SYS_link   19
#define SYS_mkdir  20
#define SYS_close  21
#define SYS_trace 22 //这里
```

**syscall.c**

```c
extern uint64 sys_write(void);
extern uint64 sys_uptime(void);
extern uint64 sys_trace(void); //这里
```

```c
SYS_mkdir]   sys_mkdir,
[SYS_close]   sys_close,
[SYS_trace]   sys_trace, //这里
```

==在sysproc.c中添加出系统调用函数==

```c
uint64
sys_trace(void){
    
}
```

**当做完以上这些步骤,程序就可以编译成功了,这时候系统调用函数中新加入了一个trace函数,接下来就应该考虑如何实现这个系统调用了**

通过xv6book,可以知道每次系统调用传参会从  寄存器 a0到 a6存放参数, a7存放的是调用的是哪个系统调用(syscall.h 把系统调用函数define 成了 一个数字)。返回值最后从a0返回。

根据题目要求,现在需要得到**当前进程的pid , 当前的系统调用 , 返回值**。

**pid**
阅读代码可以发现 , kernel中维护着进程的进程控制块,那么可以轻松的从这个结构体中得到pid

```c
struct proc *p = myproc();
int pid = p->pid;
```

**当前的系统调用**
传进来的掩码就是需要追踪的系统调用,那么应该找到这个掩码对应着哪些系统调用。根据hint,可以在进程控制块中添加一个mask表示掩码。

对于如何得到这个掩码,在刚刚sysproc.c添加的函数中

```c
uint64
sys_trace(void)
{
  int mask;
  if(argint(0 , &mask) < 0)return -1; //这个函数获得mask

  struct proc * p = myproc();
  p->mask = mask; //把mask传入进程控制块
  return 0;
}
```

这样就得到了从a0中传入的参数mask

接下来只需要在 syscall.c中通过进程控制块这个结构体拿到mask即可

**返回值**

返回值通过输出a0即可

最后,一个关键点就是 通过mask只能知道系统调用的数字,如何把它转换成名称呢?|
这个也很简单,只要在多添加一个字符数组就行了

```c
char *s[] ={"fork" , "exit" , "wait" , "pipe" , "read" , "kill" , "exec"
,"fstat" , "chdir" , "dup" , "getpid" , "sbrk" , "sleep" , "uptime" ,
"open" , "write" , "mknod" , "unlink" , "link" , "mkdir" ,"close", "trace"
};

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]();
    int mask = myproc()->mask;
    if( (mask>>num) &1){
      printf("%d: syscall %s -> %d\n" ,p->pid , s[num-1] , p->trapframe->a0);
    }
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}
```

那么以上就是System call tracing的全部内容了

## 疑问

- **usys.pl 是如何起作用的? , 它是干嘛的?**

  Makefile invokes the perl script  `user/usys.pl` ,which produces    `user/usys.S`  就是说 Makefile在编译阶段会调用这个脚本,然后产生这样一个汇编,这是实际的系统调用存根文件。然后会使用 RISC-V的   `ecall` 指令来进入kernel

# lab3 - page tables

## Speed up system calls

```
When each process is created, map one read-only page at USYSCALL (a VA defined in memlayout.h). At the start of this page, store a struct usyscall (also defined in memlayout.h), and initialize it to store the PID of the current process. For this lab, ugetpid() has been provided on the userspace side and will automatically use the USYSCALL mapping. You will receive full credit for this part of the lab if the ugetpid test case passes when running pgtbltest.
```

需要将进程的pid映射到页表中,供`getpid()`这个系统调用使用,这样它就不用再去进入内核了而是直接读取映射过后的页面中的内容,而这个任务是让我们将pid映射到页面中 , 所以可以直接读取这个pid而不用进入内核了

首先,根据提示进入`kernel/proc.c`中的这个函数 ,可以看到这个函数是创建一个进程页表并初始化 ,**注意,在做lab之前要仔细阅读源码,尤其是vm.c中的代码,后面可能考虑出一个阅读xv6源码系列** 。会看到这个函数初始化了一个用户进程页表,然后装载了`TRAMPOLINE` 和 `TRAPFRAME`的映射,在根据任务要求,我们需要再添加一个`USYSCALL`映射,这个东西位于`memlayout.h`中,具体位置已经给出 。

```c
pagetable_t
proc_pagetable(struct proc *p)
{
  pagetable_t pagetable;

  // An empty page table.
  pagetable = uvmcreate();
  if(pagetable == 0)
    return 0;

  // map the trampoline code (for system call return)
  // at the highest user virtual address.
  // only the supervisor uses it, on the way
  // to/from user space, so not PTE_U.
  if(mappages(pagetable, TRAMPOLINE, PGSIZE,
              (uint64)trampoline, PTE_R | PTE_X) < 0){
    uvmfree(pagetable, 0);
    return 0;
  }

  // map the trapframe just below TRAMPOLINE, for trampoline.S.
  if(mappages(pagetable, TRAPFRAME, PGSIZE,
              (uint64)(p->trapframe), PTE_R | PTE_W) < 0){
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  }

  if(mappages(pagetable , USYSCALL , PGSIZE ,(uint64)(p->usyscall),PTE_R|PTE_U) < 0){
    uvmunmap(pagetable ,TRAMPOLINE , 1 , 0 ); 
    uvmunmap(pagetable ,TRAPFRAME , 1 , 0 );
    uvmfree(pagetable , 0);
    return 0;
  }

  return pagetable;
}
```

之后,需要到`allocproc()`中 ,这个函数是分配页面。 这是新进程创建的关键阶段, 这个阶段分配和初始化用于存储`PID`的只读页面 , 可以确保新进程再创建时正确的设置这部分内存。 也就是说 ,刚刚那个函数是用于虚拟内存到物理内存的映射 , 这个函数用于初始化物理内存

```c
static struct proc*
allocproc(void)
{
  struct proc *p;

  for(p = proc; p < &proc[NPROC]; p++) {
    acquire(&p->lock);
    if(p->state == UNUSED) {
      goto found;
    } else {
      release(&p->lock);
    }
  }
  return 0;

found:
  p->pid = allocpid();
  p->state = USED;

  // Allocate a trapframe page.
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  //usyscall
  if((p->usyscall = (struct usyscall *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  p->usyscall->pid = p->pid; //这里初始化了进程pid,把它放到了这块物理内中

  // An empty user page table.
  p->pagetable = proc_pagetable(p);

  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // Set up new context to start executing at forkret,
  // which returns to user space.
  memset(&p->context, 0, sizeof(p->context));
  p->context.ra = (uint64)forkret;
  p->context.sp = p->kstack + PGSIZE;

  return p;
}
```

最后,不要忘记释放页面,否则会造成内存泄漏

```c
//释放与进程相关的资源
static void
freeproc(struct proc *p)
{
  if(p->trapframe)
    kfree((void*)p->trapframe);
  p->trapframe = 0;
  if(p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  p->pagetable = 0;
  if(p->usyscall)
    kfree((void*)p->usyscall);
  p->usyscall = 0;
  p->sz = 0;
  p->pid = 0;
  p->parent = 0;
  p->name[0] = 0;
  p->chan = 0;
  p->killed = 0;
  p->xstate = 0;
  p->state = UNUSED;
}
```

下面是释放物理内存

```c
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);
}
```

## Print a page table

```
Define a function called vmprint(). It should take a pagetable_t argument, and print that pagetable in the format described below. Insert if(p->pid==1) vmprint(p->pagetable) in exec.c just before the return argc, to print the first process's page table. You receive full credit for this part of the lab if you pass the pte printout test of make grade.
```

这里让打印一个进程的页表 ,有四部分

- .. :表示它是第几层页表,由xv6book 可以知道页表是一个三级页表
- 数字 : 表示一个有效的pte索引
- pte位信息, 也就是pte是多少
- pte所对应的物理地址

先进入`kernel/vm.c`中,写一个vmprint函数

```c
void vmprint(pagetable_t pagetable){
  printf("page table %p\n" , pagetable);

  pagetableprint(pagetable , 0);

  return;
}
```

接下来思考如何写 `pagetableprint` 首先这个0表示它在第1层,一共有3层。因为已经知道它的页表页的位置了,一个页表有512个pte,可以暴力枚举这512个pte 如果这个pte有效位是1,说明他被使用,然后将它打印出来

```c
void pagetableprint(pagetable_t pagetable , int depth)
{
  
  for(int i =0;i<512;++i){
    pte_t pte = pagetable[i]; //拿到pte 看看是否被使用了
    if((pte & PTE_V) == 0){ //无效的pte
      continue;
    }
    for(int i =0;i<=depth;++i){
      printf(" ..");
    }
    printf("%d: pte %p pa %p\n" , i ,(void*)pte ,(void*)PTE2PA(pte));
    //接着dfs找下一级页表 通过freewalk知道 页表只有 读 写 可执行位都是0 才是高级页表
    if((pte & PTE_V) && (pte & (PTE_R | PTE_W | PTE_X)) == 0){
      //去指向下一级别页表
      uint64 child = PTE2PA(pte);
      pagetableprint((pagetable_t)child , depth + 1);
    }
  }

}
```

**这里还有一个重要的点,就是如何判断现在这个级别的页表不是叶子页表**
可以参考`freewalk` 页表只有 `读 写 可执行位都是0才是高级页表`

## Detecting which pages have been accessed

```
Your job is to implement pgaccess(), a system call that reports which pages have been accessed. The system call takes three arguments. First, it takes the starting virtual address of the first user page to check. Second, it takes the number of pages to check. Finally, it takes a user address to a buffer to store the results into a bitmask (a datastructure that uses one bit per page and where the first page corresponds to the least significant bit). You will receive full credit for this part of the lab if the pgaccess test case passes when running pgtbltest.
```

需要实现一个系统调用,来报告哪些页面被访问过了。先通过`pgtbltest.c`看到了这个函数传进来了什么参数`pgaccess(buf, 32, &abits)` 一个buf(虚拟地址)  一个32用来查看从这个buf开始后32个页面,有哪些页面被访问过了,如果访问过就是1,没有访问过就是0,把它们通过位运算加在一个掩码上(比如有四个页面,1,3访问过,最后答案就是0101)  最后一个就是答案,但是要注意的是传进来的答案是用户态中的,因为用户空间无法直接访问内核空间的数据,所以用`copyout`实现数据的传输

首先需要回顾一下lab2的相关知识,当系统调用时,三个参数分别存放在寄存器 `a0 , a1 , a2`中,想要拿到它们就得通过`argaddr argint`来解析系统调用的参数

值得一提的是,根据xv6book , 是否使用过的标志位是`PTE_A`遇到使用过的要记得将他清零。

```c
int
sys_pgaccess(void)
{
  uint64 strat_va;
  int num_pages;
  uint64 mask;
  
  pagetable_t pagetable = myproc()->pagetable;

  // lab pgtbl: your code here.
  if(argaddr(0 , &strat_va) < 0)
    return -1;
  if(argint(1 , &num_pages) < 0)
    return -1;
  if(argaddr(2 , &mask) < 0)
    return -1;
  if(num_pages > 64)return -1;

  uint64 ans = 0;

  for(int i =0;i<num_pages;++i){
    uint64 now_va = strat_va + PGSIZE*i;
    pte_t * pte= walk(pagetable , now_va,1);
    if(*pte &PTE_A){
      ans |= (1L << i);
      *pte = *pte &(~PTE_A);
    }
  }
  copyout(pagetable , mask,(char *)&ans,sizeof(uint64));
  
  return 0;
}
```

这里我想再解释一下uint64 now_va = strat_va + PGSIZE*i; 有人可能会有疑问 : 这么大的虚拟页面,一个pte是如何与它对应的? 

首先,pte的前44位用于映射虚拟地址, 后10位为标志位,也就是说,一个pte只关心它映射的是哪个页面,而不关心它映射的是页面中的哪个位置 。那么对于一个页面不同的位置,如何区分它们的映射后的物理地址呢?  首先,pte会产生对应的ppn , 即物理页号,对于同一个虚拟页的不同位置,它们的后12位(因为页面是4kb的,也就是$2^{12}$所以需要12位)叫`offset`表示偏移 , 所以最终物理地址就是   ppn + offset  这里用于区分了同一个虚拟页面中的不同位置的物理地址

## 疑问

**疑问中是我做lab或看书产生的一些未解决的疑问**

- 物理地址是`ppn + offset` ,但是在xv6代码中

  ```c
  #define PTE2PA(pte) (((pte) >> 10) << 12)
  pa = PTE2PA(*pte);//pte转ppn 但此时还没有加上offset 不知道为什么
    return pa;
  ```

  直接认为ppn>>12是物理地址。 不知道为什么

### 关于 MIT 6.S081 Lab3 的解决方案与指南 MIT 6.S081 是一门关于操作系统设计与实现的课程,其实验室部分旨在让学生通过实践掌握操作系统的底层原理。Lab3 主要涉及文件系统的设计与实现,学生需要完成一个简单的文件系统功能扩展。 以下是针对 MIT 6.S081 Lab3 的一些指导和建议: #### 文件系统的核心概念 在 Lab3 中,主要目标是对 xv6 文件系统进行修改或增强。xv6 是一个简化版的 Unix 操作系统,用于教学目的。为了成功完成此实验,需理解以下核心概念: - **Inode 结构**:inode 表示文件元数据,包括权限、大小以及指向实际数据块的指针[^4]。 - **超级块管理**:超级块记录整个文件系统的状态信息,例如可用 inode 和数据块的数量。 - **磁盘布局**:了解磁盘如何划分成引导块、超级块、inode 块和数据块是至关重要的。 #### 实验的具体任务 通常情况下,Lab3 可能会要求实现以下几个方面: 1. 支持更大的文件系统容量。 2. 添加新的命令来测试文件系统的行为。 3. 修改现有代码以支持更复杂的文件访问模式。 这些任务都需要深入研究 xv6 的源码结构并熟悉 C 编程语言中的内存管理和 I/O 处理机制。 ```c // 示例代码片段展示如何读取 inode 数据 struct inode *iget(uint dev, uint inum){ struct buf *bp; struct dinode *dip; bp = bread(dev, IBLOCK(inum)); // 将指定编号的 inode 加载到缓存中 dip = (struct dinode *)bp->data + (inum % IPB); // 访问对应的 inode 条目 if(dip->type == 0){ // 如果该 inode 类型为空,则释放缓冲区返回 NULL brelse(bp); return 0; } struct inode *ip = alloc_inode(); // 否则分配一个新的 inode 并填充必要字段 ip->dev = dev; ip->inum = inum; ip->ref = 1; // 初始化引用计数器为 1 ip->valid = 1; // 设置有效标志位 memcpy(&ip->addrs, dip, sizeof(*dip)); brelse(bp); return ip; } ``` 上述代码展示了从磁盘加载 inode 到内存的过程,这是构建复杂文件系统逻辑的基础之一。 #### 提交作业的规定 需要注意的是,在提交任何编程作业时应严格遵循学术诚信原则。具体而言, 不得抄袭他人代码或者查看过往年度的答案;可以与其他同学讨论思路但不可共享具体实现细节[^2]。 #### 总结 完成 MIT 6.S081 Lab3 需要扎实的操作系统理论基础以及良好的程序调试技巧。希望以上介绍能够帮助您更好地理解和解决相关问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值