xv6-labs-2020.lab0.utils

lab0 环境配置及系统调用

  • 环境配置:https://pdos.csail.mit.edu/6.828/2020/tools.html

实验环境

实验环境基于 WSL(windows linux subsystem)

  • WSL 1.0
1
lsb_release -a
1
2
3
4
5
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.2 LTS
Release: 20.04
Codename: focal

换源

  • 打开文件 "/etc/apt/sources.list"
    • 可以备份一下原始的文件
1
2
sudo cp /etc/apt/sources.list /etc/apt/sources.list.back
sudo vim /etc/apt/sources.list
  • 修改文件为清华源
    • https://mirrors.tuna.tsinghua.edu.cn/help/ubuntu/
    • 根据版本选择即可
1
sudo apt-get update

配置 VSCode 作为编辑器

  • 安装好直接 code . 即可
  • 权限问题
1
/mnt/c/Users/User/.vscode/extensions/ms-vscode-remote.remote-wsl-0.54.6/scripts/wslServer.sh: Permission denied
  • 修改文件 /etc/wsl.conf
1
sudo vim /etc/wsl.conf
1
2
[automount]
enable = false
  • 修改访问权限
1
cd /mnt/c/Users/XXX/.vscode/extensions/ms-vscode-remote.remote-wsl-0.54.6/scripts/
1
chmod +x *

安装工具

1
sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu
1
2
sudo apt-get remove qemu-system-misc
sudo apt-get install qemu-system-misc=1:4.2-3ubuntu6
  • 评分需要安装 python
1
sudo apt-get install python

其他

  • 例如写好了 sleep.c 之后需要修改 makefile 文件
  • Add your sleep program to UPROGS in Makefile

Lab0

准备工作

预备知识

  • pipe
    • 管道通信
    • https://blog.csdn.net/skyroben/article/details/71513385
  • linux 标准流
    • 标准输入流:0
    • 标准输出流:1
    • 标准错误流:2
  • 命令行中括号的含义
    • []:可写可不写
    • {}:那就必须要在 {} 内给出的选择里选一个
    • <>:表示必选

一些准备

  • 修改 makefile
  • 添加程序
1
2
3
4
5
6
7
8
UPROGS=\
# ...
$U/_sleep\
$U/_pingpong\
$U/_primes\
$U/_find\
$U/_xargs\

  • 在 user 文件夹下新建文件
1
2
3
4
5
sleep.c
pingpong.c
primes.c
find.c
xargs.c

函数封装

  • 在 user/user.h 中添加封装函数
1
2
3
4
5
// user/user.h
// wrapped system call(printf.c)
void unix_error(char*);
int Fork(void);
void Pipe(int*);
  • 在某个文件中添加实现
  • 这里我们为了不修改 makefile,直接在 user/printf.c 中实现了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// user/printf.c
// 放在这感觉不太合适, 但是这样不需要修改 makefile
// wrapped system call
void unix_error(char *msg) {
fprintf(2, "%s\n", msg);
exit(1);
}

int Fork(){
int pid;
if((pid = fork()) < 0){
unix_error("Fork Error!");
exit(1);
}
return pid;
}

void Pipe(int* fd) {
int ret;
ret = pipe(fd);
if(ret == -1) {
unix_error("Failed to creater a pipe!\n");
}
}

sleep

  • 通过系统调用 sleep 实现即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// user/sleep.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main(int argc, char *argv[]) {
if(argc != 2) {
// 输出到标准错误流
fprintf(2, "Invalid args! Usage: sleep <number>!\n");
exit(1);
}
int sleepTime = atoi(argv[1]);
sleep(sleepTime);
exit(0);
}

pingpong

  • 实现进程间通信
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// user/pingpong.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

#define ARRAY_SIZE 10

int main(int argc, char *argv[]) {
// 变量定义
// parent to child, child to parent
// 使用一个管道会引发锁的问题
int fdP2C[2];
int fdC2P[2];
int id;
char msg[ARRAY_SIZE];
int readNumber;

// start
readNumber = 0;
memset(msg, 0, sizeof(msg));

// 创建一个管道
// 返回两个文件描述符 fd[0] 读, fd[1] 写
// fd[1] 的输出是 fd[0] 的输入
Pipe(fdP2C);
Pipe(fdC2P);

// 创建父子进程
id = Fork();
if(id != 0) {
// father
close(fdP2C[0]);
close(fdC2P[1]);
// write
write(fdP2C[1], "ping", 4);
// read
readNumber = read(fdC2P[0], msg, ARRAY_SIZE);
if(readNumber > 0) {
fprintf(1, "%d: received %s\n", getpid(), msg);
}
close(fdP2C[1]);
close(fdC2P[0]);
} else {
// child
close(fdP2C[1]);
close(fdC2P[0]);
// read
readNumber = read(fdP2C[0], msg, ARRAY_SIZE);
if(readNumber > 0) {
fprintf(1, "%d: received %s\n", getpid(), msg);
}
// write
write(fdC2P[1], "pong", 4);
close(fdP2C[0]);
close(fdC2P[1]);
// sleep(10);
}
exit(0);
}

思考

  • 题目的设计很巧妙,如果是子进程先传消息的话可能出现如下的问题
  • pingpong 输出如下
    • 这个错误是子进程先写导致的
    • 父进程结束了,但是子进程还在
1
2
3
$ pingpong
3: received ping
4: $r eceived pong
1
2
3
4
5
if(fork() == 0) {
// 子进程
} else {
// 父进程
}
  • 只需要等到父进程结束,shell 就可以输出
    • linux 代码测试
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(){
if(fork()!=0) {
printf("Father!");
} else {
sleep(5);
printf("Child!");
}
return 0;
}

primes

  • https://swtch.com/~rsc/thread/
  • 保证这样一个性质:每一个进程接收到的一个数字序列的第一个数字一定是质数
  • 主要想法如下
    • 第一个进程把数字 2-32 通过管道输出给下一个进程
    • 之后每一个进程记录第一个数(记作 a),输出 a(a 是质数),将序列中的其他数字除以 a
      • 如果能够被 a 整除,说明是合数,丢弃
      • 如果能够不被 a 整除,说明可能是质数,传递给下一个进程
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

void recurve(int fd) {
int status;
int first = 1;
int hasForked = 0;
int firstNumber, num;
int p1[2];

while(read(fd, &num, sizeof(int)) != 0) {
// 输出第一个数
if(first == 1) {
firstNumber = num;
first = 0;
printf("prime %d\n", num);
}
// 逐个检查之后的数, 若不能被记录的数整除, 传至下一进程
else {
if(num % firstNumber != 0) {
if(hasForked == 0) {
Pipe(p1);
if(Fork() != 0) {
// father
close(p1[0]);
} else {
// child
close(fd);
close(p1[1]);
recurve(p1[0]);
exit(0); // 退出, 因为之后的代码都是父进程的
}
hasForked = 1;
}
// 写入管道
write(p1[1], &num, sizeof(int));
}
}
}
if(hasForked == 1) {
close(p1[1]);
wait(&status);
}
close(fd);
}

// 核心问题就是每次只 write/read 一个 int
int main(int argc, char *argv[]) {
// 忽略错误处理
// 第一个进程输出 2-32 到管道里
int status;
int i;
int p1[2];

Pipe(p1);
if (Fork() != 0) {
// father
close(p1[0]);
for(i = 2; i < 35; ++i) {
write(p1[1], &i, sizeof(int));
}
close(p1[1]);
// 等待直到子进程全被回收
while(wait(&status) != -1) {}
} else {
// child
close(p1[1]);
recurve(p1[0]);
}
exit(0);
}

xargs

  • 实现功能 xargs
  • 具体功能见注释
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// user/xargs.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/param.h"

#define MAX_BUFFER_SIZE 512
#define MAX_ARG_LENGTH 32

// xargs: 将前一个命令的输出作为后面的输入的附加参数, 按照 " "(空格)分开
// 例如: 当前文件目录下有文件 a.txt, b.txt
// ls | xargs rm
// 等价于
// rm a.txt b.txt

// echo -e : 字符转义

// 高级语法(未实现)
// TODO
// -n num 后面加次数, 表示命令在执行的时候一次用的参数的个数, 默认是用所有的
// (1)
// echo -e "1\n2" | xargs -n 1 echo line
// line 1
// line 2
// (2)
// echo -e "1\n2" | xargs echo line
// line 1 2


// 单行读取, 每一行执行一次命令
// 将每一行按照空格分开, 作为参数传入
int main(int argc, char **argv) {
int i;
int status;
char pass[MAXARG][MAX_ARG_LENGTH];
int n;
char buffer[MAX_BUFFER_SIZE + 1]; // 多留一位
char *start, *now, *last;
int pos; // 当前应该写入的参数位置
char *passHelp[MAXARG]; // 用于辅助传参

pos = argc - 1;
memset(pass, 0, sizeof(pass));
memset(passHelp, 0, sizeof(passHelp));

// 复制参数
// xargs 这个参数不需要
for(i = 1; i < argc; ++i) {
strcpy(pass[i-1], argv[i]);
}

// 从标准输入流读取输出
while((n = read(0, buffer, MAX_BUFFER_SIZE)) > 0) {
buffer[n] = '\0'; //因为多保留了一位
last = buffer + n;
// 将读取到的字符按照空格/回车分开
start = buffer;
for(now = buffer; now < last; ++now) {
if(*now == ' ') {
*now = '\0';
if(pos == MAXARG) {
unix_error("To Many Args!");
}
strcpy(pass[pos++], start);
start = now + 1;
} else if(*now == '\n') {
*now = '\0';
if(pos == MAXARG) {
unix_error("To Many Args!");
}
strcpy(pass[pos++], start);
start = now + 1;
// 每行执行一次
for(i = 0;i < pos; ++i) {
passHelp[i] = pass[i];
}
passHelp[pos] = 0;
if(Fork() == 0) {
exec(passHelp[0], passHelp);
} else {
wait(&status);
}
pos = argc - 1;
}
}
// 最后一个参数
if(pos == MAXARG) {
unix_error("To Many Args!");
}
strcpy(pass[pos++], start);
// 要求下一次复制的时候, 保留最后一个参数
// 处理一个字符串被分割为两段
// 事实上无法区分是两个还是一个
}
// 因为我们不实现高级功能, 不需要使用 fork
for(i = 0;i < pos; ++i) {
passHelp[i] = pass[i];
}
passHelp[pos] = 0;
if(Fork() == 0) {
exec(passHelp[0], passHelp);
} else {
wait(&status);
}

exit(0);
}
// echo 3 4 5 | xargs echo 1 2