课程地址:https://pdos.csail.mit.edu/6.S081/2023/labs/util.html

基础环境

首先要根据lab tools page 配置Linux环境,我使用的是Ubuntu 22.04的服务器版本

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

环境测试

由于本实验的运行环境在RISC-V架构下,所以需要测试在RISC-V的基本环境下是否可以正常编译并运行xv6系统

1
2
3
# 版本检查
qemu-system-riscv64 --version
riscv64-linux-gnu-gcc --version

获取并启动xv6(easy

从git仓库中获取xv6源代码

1
2
git clone git://g.csail.mit.edu/xv6-labs-2023
cd xv6-labs-2023

编译并运行xv6

1
make qemu

启动后应该会有如下输出:

1
2
3
4
5
6
7
8
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -global virtio-mmio.force-legacy=false -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

xv6 kernel is booting

hart 1 starting
hart 2 starting
init: starting sh
$

此时xv6系统已经正常启动,并且已经启动sh终端,在$的提示符后输入ls会显示当前文件夹的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ ls
. 1 1 1024
.. 1 1 1024
README 2 2 2305
xargstest.sh 2 3 93
cat 2 4 32848
echo 2 5 31696
forktest 2 6 15824
grep 2 7 36224
init 2 8 32192
kill 2 9 31656
ln 2 10 31480
ls 2 11 34784
mkdir 2 12 31712
rm 2 13 31704
sh 2 14 54144
stressfs 2 15 32584
usertests 2 16 180624
grind 2 17 47536
wc 2 18 33800
zombie 2 19 31064
console 3 20 0

此时一个简单的xv6操作系统就启动好了,如果要在Linux终端中退出xv6,需要按下Ctrl+A然后按下X回到正常的Linux终端

实现用户级sleep程序 (easy)

题目要求:Implement a user-level sleep program for xv6, along the lines of the UNIX sleep command. 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.
按照 UNIX sleep 命令的思路,为 xv6 实现用户级 sleep 程序。您的 sleep 应暂停用户指定的刻度数。时钟周期是 xv6 内核定义的时间概念,即定时器芯片两次中断之间的时间。您的解决方案应该位于文件 user/sleep.c 中。

  1. 将代码放入 user/sleep.c 中。查看 user/ 中的一些其他程序(例如 user/echo.cuser/grep.cuser/rm.c )以了解命令行参数如何被传递给一个程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // user/echo.c
    #include "kernel/types.h"
    #include "kernel/stat.h"
    #include "user/user.h"

    int
    main(int argc, char *argv[])
    //argc是命令行参数的数量,包括程序名称本身,当它的值为1时,只有程序名本身
    // argv是一个指向字符指针的指针,用于存储命令行参数的字符串。它的每一个字符串都是一个命令行参数
    {
    int i;

    for(i = 1; i < argc; i++){
    write(1, argv[i], strlen(argv[i]));
    if(i + 1 < argc){
    write(1, " ", 1);
    } else {
    write(1, "\n", 1);
    }
    }
    exit(0);
    }
  2. sleep 程序添加到 Makefile 中的 UPROGS 中;完成此操作后, make qemu 将编译您的程序,并且您将能够从 xv6 shell 运行它

  3. 使用系统调用 sleep 完成sleep.c,如果用户忘记传递参数,sleep 应该打印一条错误消息,命令行参数作为字符串传递,可以使用 atoi 将其转换为整数(可参阅 user/ulib.c)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include "kernel/types.h"
    #include "kernel/stat.h"
    #include "user/user.h"

    int
    main(int argc, char *argv[])
    {
    if(argc < 1){
    fprintf(2, "sleep: missing operand\nusage: sleep time...\n");
    exit(1); //表示异常退出.这个1是返回给操作系统的。
    }else{
    sleep(atoi(argv[1]));
    }
    exit(0); //表示正常退出
    }

fprintf函数的第一个参数为文件指针(文件描述符),通常在Unix系统中:

0:标准输入(stdin)

1:标准输出(stdout)

2:标准错误输出(stderr)

  1. 从 xv6 shell 运行程序:

    1
    2
    3
    make qemu
    sleep 10
    ./grade-lab-util sleep #使用Grade进行成绩测试

pingpong (easy)

题目要求:Write a user-level program that uses xv6 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.
编写一个用户级程序,使用 xv6 系统调用通过一对管道(每个方向一个)在两个进程之间“乒乓”一个字节。父进程应向子进程发送一个字节;子进程应打印“:已收到 ping”,其中 是其进程 ID,将管道上的字节写入到父进程,然后退出;父进程应该从子进程读取字节,打印“:收到 pong”,然后退出。您的解决方案应该位于文件 user/pingpong.c 中。

  • 使用 pipe 创建管道;

    int pipe(int p[]) Create a pipe, put read/write file descriptors in p[0] and p[1].

  • 使用 fork 创建一个子进程;

    int fork() Create a process, return child’s PID.

  • 使用 read 从管道读取,使用 write 写入管道;

    int write(int fd, char *buf, int n) Write n bytes from buf to file descriptor fd; returns n.

    int read(int fd, char *buf, int n) Read n bytes into buf; returns number read; or 0 if end of file.

  • 使用 getpid 查找调用进程的进程ID;

    int getpid() Return the current process’s PID.

xv6 采用传统的内核形式,是一个为正在运行的程序提供服务的特殊程序。每个正在运行的程序(称为进程)都有包含指令、数据和堆栈的内存,这些指令实现程序的计算。

计算机通常具有许多进程,但只有一个内核,当进程需要调用内核服务时,它会调用系统调用,这是操作系统接口中的调用之一。系统调用进入内核,由内核执行服务并返回。

image-20240722181529656

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int
main(int argc, char *argv[])
{
int fd[2];
char buf[5];
char *ping="ping";
char *pong="pong";
int length = 4;
if(pipe(fd)==-1){
fprintf(2, "pipe failed\n");
exit(1);
}
int pid = fork();
if(pid>0){//parent process
if(write(fd[1], ping, length)!=length){
fprintf(2, "write to child failed\n");
exit(1);
}

if(read(fd[0], buf, length)!=length){
fprintf(2, "read from child failed\n");
exit(1);
}
printf("%d:parent received %s\n",getpid(),buf);
exit(0);
}else{
if(read(fd[0], buf, length)!=length){
fprintf(2, "read failed\n");
exit(1);
}
printf("%d:child received %s\n",getpid(),buf);

if(write(fd[1], pong, length)!=length){
fprintf(2, "write failed\n");
exit(1);
}
exit(0);
}
exit(0);
}