XV6实验(2020)

news2024/12/26 0:13:59

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)

  1. 进程和内存

每个进程都拥有自己的用户空间内存以及内核空间状态,当进程不再执行时,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格式。

  1. 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;
        }
      }
    
  • readwrite:原型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相同。

  1. 管道Pipes

管道是暴露给进程的一对文件描述符,一个文件描述符用来读,另一个文件描述符用来 写,将数据从管道的一端写入,将使其能够被从管道的另一端读出。

pipe也是一个系统调用,原型是int pipe(int p[])p[0]为读取的文件描述符,p[1]为写入的文件描述符。

  1. 文件系统

xv6文件系统包含了文件(byte arrays)和目录(对其他文件和目录的引用)。目录生成了一个树,树从根目录/开始。对于不以/开头的路径,认为是是相对路径

相关系统调用:

  • mknod:创建设备文件,一个设备文件有一个major device #和一个minor device #用来唯一确定这个设备。当一个进程打开了这个设备文件是,内核会将readwrite系统调用重新定向到设备上。

  • 一个文件的名称和文件本身是不一样的,文件本身,也叫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 ca, 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.cexec()系统调用
file.c文件描述符支持
fs.c文件系统
kalloc.c物理页面分配器
kernelvec.S处理来自内核的陷入指令以及计时器中断
log.c文件系统日志记录以及崩溃修复
main.c在启动过程中控制其他模块初始化
pipe.c管道
plic.cRISC-V中断控制器
printf.c格式化输出到控制台
proc.c进程和调度
sleeplock.cLocks that yield the CPU
spinlock.cLocks that don’t yield the CPU.
start.c早期机器模式启动代码
string.c字符串和字节数组库
swtch.c线程切换
syscall.cDispatch 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 to user/user.h, a stub to user/usys.pl, and a syscall number to kernel/syscall.h. The Makefile invokes the perl script user/usys.pl, which produces user/usys.S, the actual system call stubs, which use the RISC-V ecall 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 in kernel/sysproc.c that implements the new system call by remembering its argument in a new variable in the proc structure (see kernel/proc.h). The functions to retrieve system call arguments from user space are in kernel/syscall.c, and you can see examples of their use in kernel/sysproc.c.
  • Modify fork() (see kernel/proc.c) to copy the trace mask from the parent to the child process.
  • Modify the syscall() function in kernel/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 a struct sysinfo (see kernel/sysinfo.h). The kernel should fill out the fields of this struct: the freemem field should be set to the number of bytes of free memory, and the nproc field should be set to the number of processes whose state is not UNUSED. We provide a test program sysinfotest; you pass this assignment if it prints “sysinfotest: OK”.

这个实验的过程和上个实验的流程基本是一致的,首先要根据上个实验的提示,在相关文件中添加内容,实现创建系统调用的功能。然后根据提示进行推进实验。

提示内容为:

  • Add $U/_sysinfotest to UPROGS in Makefile

  • Run 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 of struct 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; see sys_fstat() (kernel/sysfile.c) and filestat() (kernel/file.c) for examples of how to do that using copyout().

  • 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又了解了那么一点。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/197163.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

代码随想录算法训练营第五十三天|● 1143.最长公共子序列 ● 1035.不相交的线 ● 53. 最大子序和 动态规划

一、1143.最长公共子序列 题目&#xff1a; 给定两个字符串 text1 和 text2&#xff0c;返回这两个字符串的最长公共子序列的长度。 一个字符串的 子序列 是指这样一个新的字符串&#xff1a;它是由原字符串在不改变字符的相对顺序的情况下删除某些字符&#xff08;也可以不…

Android 自定义控件

文章目录Canvas 画布类 画布背景 点 线 矩形 椭圆 圆 弧形 路径 字符 对画布裁剪及变形Paint 画笔类 常用方法 图形线条相关 字符相关 Path设置样式如果是一个自定义控件&#xff0c;则需要派生自 Vie…

【Vue】模板语法——内置指令

指令&#xff08;Directives&#xff09;是 vue 为开发者提供的模板语法&#xff0c;用于辅助开发者渲染页面的基本结构。vue 中的指令按照不同的用途可以分为如下几大类&#xff1a;① 内容渲染指令&#xff1a;v-text、v-html② 属性绑定指令&#xff1a;v-bind③ 事件绑定指…

<Java EE 进阶> 3.Spring简单的读和取

目录 1.存储Bean对象 &#xff08;1&#xff09;准备工作&#xff1a;配置扫描路径 &#xff08;2&#xff09;添加注解存储Bean对象 ① 类注解 ② 方法注解Bean 在String中更简单的存储和读取对象的核心是使用注解 1.存储Bean对象 &#xff08;1&#xff09;准备工作&am…

Linux内核的安装与加载

目录 一、tftp加载Linux内核和roootfs 二、 EMMC加载Linux内核和rootfs 三、tftp加载Linux内核nfs挂在根文件系统 四、EMMC加载uboot 一、tftp加载Linux内核和roootfs 这个就是Linux内核&#xff0c;它很轻量级只有2.949MB所以在嵌入式领域很受欢迎。 上面那个就是设备树文…

初识 Python 科学计算库之 NumPy(创建多维数组对象)

文章目录参考描述NumPy特点获取导入多维数组对象np.array()np.asarray()范围随机概览np.random.randn()np.random.normal()np.random.choice()np.random.random()np.random.randint()np.random.shuffle()np.random.seed()数列等差数列等比数列填充np.zeros()np.zeros_like()np.…

Spotify Q4用户增长再超预期,但为何还是赚不到钱?

2022年&#xff0c;美联储接连7次暴力加息&#xff0c;科技行业整体低迷&#xff0c;从Meta、Google再到亚马逊&#xff0c;大型科技公司接连宣告裁员过冬。 寒气已经传递到了更广阔的地方。1月下旬&#xff0c;瑞典音乐流媒体巨头Spotify宣布将裁员6%。 音乐流媒体的生意变得…

Python自动化测试实战篇(1)读取xlsx中账户密码,unittest框架实现通过requests接口post登录网站请求,JSON判断登录是否成功

Python接口项目实战篇&#xff08;1&#xff09;读取xlsx中账户密码&#xff0c;unittest框架实现通过requests接口post登录网站请求&#xff0c;JSON判断登录是否成功实现功能描述1.首先获取到接口谷歌浏览器中获取接口信息fiddler里面抓取接口信息2.创建一个xlsx文档3.导入我…

【C++】继承详解

目录继承的概念及定义继承的概念继承的定义定义格式继承关系和访问限定符继承基类成员访问方式的变化基类和派生类对象的赋值转换继承中的作用域派生类的默认成员函数继承和友元继承与静态成员复杂的菱形继承及菱形的虚拟继承菱形继承的概念虚拟继承**虚拟继承的原理**&#xf…

IT6512可编程直流电源的工作原理

现在各种的电子设备不断的发展&#xff0c;它们对直流供电的电源也有了更高的要求&#xff0c;相对于电子设备来说&#xff0c;用单一的直流电源是没有办法达到供电的要求&#xff0c;所以需要不同的直流电源来给电子设备供电。可编程直流电源就是这一种。在生产测试中&#xf…

Pandas的apply, map, transform介绍和性能测试

apply函数是我们经常用到的一个Pandas操作。虽然这在较小的数据集上不是问题&#xff0c;但在处理大量数据时&#xff0c;由此引起的性能问题会变得更加明显。虽然apply的灵活性使其成为一个简单的选择&#xff0c;但本文介绍了其他Pandas函数作为潜在的替代方案。 在这篇文章…

软测(基础)· 软件测试的生命周期 · 如何描述一个 Bug · Bug 的级别 · Bug 的生命周期 · 争执 · Bug 评审

一、软件测试的生命周期软件测试的生命周期 & 软件开发的生命周期二、如何描述一个 Bug三、如何定义 Bug 的级别四、Bug 的生命周期五、发生争执了怎么办&#xff1f;Bug 评审一、软件测试的生命周期 软件测试的生命周期&#xff1a;需求分析 → 测试计划 → 测试设计、测…

《巫师3:狂猎》4.01版更新 PC端已上线

去年12月&#xff0c;《巫师3》免费升级次世代版&#xff0c;加入DLSS 3支持&#xff0c;RTX 40系显卡的用户能直接提升体验感&#xff0c;RTX 30系用户能通过DLSS 2获得更稳定的帧数。 目前。《巫师3&#xff1a;狂猎》4.01版已更新上线&#xff0c;在PC、PlayStation 和 Xbo…

【配电网规划】配电网N-1扩展规划研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

OJ万题详解––[NOIP2010 提高组] 机器翻译(C++详解)

题目背景 小晨的电脑上安装了一个机器翻译软件&#xff0c;他经常用这个软件来翻译英语文章。 题目描述 这个翻译软件的原理很简单&#xff0c;它只是从头到尾&#xff0c;依次将每个英文单词用对应的中文含义来替换。对于每个英文单词&#xff0c;软件会先在内存中查找这个单词…

openstack cinder对接两个ceph后端配置

需求 需要做卷迁移的工作&#xff0c;从一个ceph集群迁移到另一个集群&#xff0c;因此需要配置两个ceph后端。由此开展后续工作&#xff0c;将配置过程及出现的问题做一记录。 另外两套ceph后端的访问用户都是cinder用户&#xff0c;网上找的资料均为两个用户&#xff0c;当为…

电子技术——BJT的物理结构

电子技术——BJT的物理结构 本节我们介绍另一种基本三端元件&#xff0c;BJT。 物理结构 下图展示了NPN型和PNP型BJT的物理结构简图。 从图中看出&#xff0c;BJT主要由三个区域组成&#xff0c;发射极&#xff08;n类型&#xff09;&#xff0c;基极&#xff08;p类型&#…

如何跑起一个Python Flask 项目

最近做项目迁移&#xff0c;从Google cloud 迁移到 AWS项目&#xff1a;Python Flask ORM是Alembic(我不是搞python的 这边看到这个了)python 是docker 跑起来的&#xff0c;一个docker-compose up就完事但我要进行数据库迁移测试&#xff0c;所以本地要跑起来我是mac先安装pyt…

财报解读:大裁员后Meta的元宇宙还有新故事吗?

美股科技巨头Facebook自更名为Meta Platforms后全面发力元宇宙&#xff0c;作为美国第一大社交平台以及全球流量池&#xff0c;转型后的Meta一度被市场寄予厚望&#xff0c;但同样受累于其元宇宙策略&#xff0c;年初至今&#xff0c;Meta的股价累计一度下跌近65%&#xff0c;也…

【超详细】一文看懂如何在PyCharm中集成Git

PyCharm环境集成Git 当我们在官网下载好Git后&#xff0c;按照要求进行安装&#xff0c;就可以通过快捷方式对本地仓库进行版本控制啦。但是这种方式处理整个工作环境还是比较麻烦的&#xff0c;接下来&#xff0c;我们将在PyCharm环境中配置Git。 基础配置 在设置中&#xf…