[mit6.s081] Lab1: Unix utilities 实验记录
这是实验过程中参考的一些相关资源:
Lab 1: Unix utilities
主题:熟悉 xv6 及其系统调用。
Boot xv6
这部分的主要内容就是完成实验环境的配置,可以参考课程主页的lab tools page完成相关配置。
sleep
要求:实现 xv6 的 UNIX 程序 sleep ;你的 sleep 应该暂停用户指定的 tick 数。一个 tick 是由 xv6 内核定义的时间概念,即计时器芯片两次中断之间的时间。你的解决方案应在文件 user/sleep.c 中。
重点关注hint中指出的几个文件: user/user.h、user/usys.S、kernel/sysproc.c
首先我们需要明白当实现一个用户程序但需要系统调用时,用户程序是不能直接调用内核函数的。用户态和内核态是隔离的,用户程序想要请求内核服务,必须通过ecall陷入内核。所以每个系统调用都需要一个用户态可见的“函数”,从而让C代码可以像普通函数一样调用。
这个“函数“就是stubs(系统调用桩),放在usys.S内,它本身不是内核代码,而是用户程序调用系统调用时进入内核的入口。
在user.h中有:
1
int sleep(int)
这是一个函数声明,调用它时,参数按照RISC-V调用约定放到a0,返回值也从a0取。
而usys.S中:
1
2
3
4
sleep:
li a7, SYS_sleep # 把系统调用号 SYS_sleep放入a7
ecall # 环境调用指令,让用户态陷入内核态,此时CPU会跳到内核的异常入口,最终进入usertrap()
ret # 返回到调用sleep的C代码
就是这个函数的定义,二者之间的关系可以理解为:user.h声明接口,让用户C代码能编译通过;usys.S提供符号sleep,让链接器能找到实现,SYS_sleep系统调用号对应的内核处理函数就在kernel/sysproc.c中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
uint64
sys_sleep(void)
{
int n; //用户设置的中断数
uint ticks0;
if(argint(0, &n) < 0) //从 trapframe->a0 取出 n
return -1;
acquire(&tickslock);
ticks0 = ticks;
while(ticks - ticks0 < n){
if(myproc()->killed){
release(&tickslock);
return -1;
}
sleep(&ticks, &tickslock);
}
release(&tickslock);
return 0;
}
整个完整的调用链就是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
用户 C 代码:
sleep(10);
usys.S:
li a7, SYS_sleep # a7 = 13
ecall # 陷入内核,a0 = 10
内核 usertrap/syscall:
num = p->trapframe->a7 # num = SYS_sleep = 13
syscalls[num]() # syscalls[13] == sys_sleep
-> 调用 sys_sleep()
sys_sleep():
argint(0, &n) # 从 trapframe->a0 取出 10
...
syscall():
p->trapframe->a0 = 返回值
sret 返回用户态
usys.S:
从 ecall 后面继续执行
ret 返回 C 调用者
所以最终实现为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc, char *argv[]) {
if (argc != 2) { //必须为2个参数
fprintf(2, "usage: sleep [ticks num]\n");
exit(1);
}
uint n = atoi(argv[1]); //将字符串转换为整型
int ret = sleep(n);
exit(ret);
}
pingpong
要求:编写一个程序,使用 UNIX 系统调用在两个进程之间通过一对管道(一个用于每个方向)传递一个字节。父进程应向子进程发送一个字节;子进程应打印<pid>: received ping,其中是其进程 ID,将字节写入管道给父进程,然后退出;父进程应从子进程读取字节,打印<pid>: received pong,然后退出。您的解决方案应保存在 user/pingpong.c 文件中。
这里需要注意的点是管道是单向的,半双工的,一个管道只能固定一个方向传输数据,如果父子进程要互相发送数据,就需要两个方向各一个管道:
p2c:parent → child,父进程写,子进程读;c2p:child → parent,子进程写,父进程读。
最终实现为:
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
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc, char *argv[]) {
if(argc != 1) {
fprintf(2, "usage: [pingpong]\n");
exit(1);
}
int p2c[2]; // parent->child
int c2p[2]; // child -> parent
pipe(p2c);
pipe(c2p);
if(fork() == 0) { //子进程
close(p2c[1]);
close(c2p[0]);
char buf[1];
int n = read(p2c[0], buf, sizeof(buf));
if(n > 0) {
fprintf(1, "%d: received ping\n", getpid());
close(p2c[0]);
write(c2p[1], buf, sizeof(buf));
close(c2p[1]);
exit(0);
}
exit(1);
} else {
close(p2c[0]);
close(c2p[1]);
char buf[1] = {'a'};
write(p2c[1], buf, sizeof(buf));
close(p2c[1]);
int n = read(c2p[0], buf, sizeof(buf));
if(n > 0) {
fprintf(1, "%d: received pong\n", getpid());
}
close(c2p[0]);
wait(0);
exit(n > 0 ? 0: 1);
}
}
primes
要求:使用管道编写一个并发质数筛。你的目标是使用 pipe 和 fork 来设置管道。第一个进程将数字 2 到 35 输入到管道中。对于每个质数,你需要安排创建一个进程,该进程从其左边的邻居通过管道读取,并向其右边的邻居通过另一个管道写入。由于 xv6 的文件描述符和进程数量有限,第一个进程可以在 35 处停止。你的解决方案应保存在文件 user/primes.c 中。
这里需要注意的点是:主进程只负责产生2-35的数字,每个素数由对应的那个进程筛选,而且每个进程只负责一个素数。
整体流程为:
- 第一个进程(主进程):把 2 到 35 全部写进管道。
- 第二个进程:从管道读。它读到的第一个数一定是素数,打印
prime 2。然后继续读剩下的数,把不能被 2 整除的数写到下一个管道。 - 第三个进程:从第二个管道读。它读到的第一个数是
3,打印prime 3。然后继续读,把不能被 3 整除的数写到下一个管道。 - 第四个进程:读到的第一个数是
5,打印prime 5,把不能被 5 整除的数传下去。 - 以此类推,直到某个进程读不到任何数(管道写端关闭,
read返回 0),它就直接退出,不再创建下一个进程。
最终实现为:
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
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
void primes(int p) {
int buf;
if(read(p, &buf, sizeof(buf)) == 0) {
close(p);
exit(0);
}
fprintf(1, "prime %d\n", buf);
int pp[2];
pipe(pp);
if(fork() == 0) {
close(pp[1]);
close(p);
primes(pp[0]);
} else {
close(pp[0]);
int num;
while(read(p, &num, sizeof(num)) > 0) {
if(num % buf != 0) {
write(pp[1], &num, sizeof(num));
}
}
close(p);
close(pp[1]);
wait(0);
exit(0);
}
}
int main(int argc, char *argv[]) {
if(argc != 1) {
fprintf(1, "usage: [primes]\n");
exit(1);
}
int p[2];
pipe(p);
if(fork() == 0) {
close(p[1]);
primes(p[0]);
} else {
close(p[0]);
for(int i = 2; i <= 35; ++i) {
write(p[1], &i, sizeof(i));
}
close(p[1]);
wait(0);
}
exit(0);
}
find
要求:编写一个简单版的 UNIX find 程序:在目录树中查找所有具有特定名称的文件。你的解决方案应放在文件 user/find.c 中。
这里可以参考user/ls.c中ls的实现。
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
#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];
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;
}
void
ls(char *path)
{
char buf[512], *p;
int fd;
struct dirent de;
struct stat st;
// path -> fd -> 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("%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)){
//跳过空的目录项,删除一个文件时,把对应的inum改为0.
if(de.inum == 0)
continue;
memmove(p, de.name, DIRSIZ);
//如果文件名长度正好等于DIRSIZ,那么de.name里没有\0,所以补充\0,保证p指向的是一段合法的字符串
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;
}
close(fd);
}
int
main(int argc, char *argv[])
{
int i;
if(argc < 2){
ls(".");
exit(0);
}
for(i=1; i<argc; i++)
ls(argv[i]);
exit(0);
}
最终实现为:
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
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
char* extract(char* file) { //提取文件名
char* p = file + strlen(file); //'\0'
while(p > file && *p != '/') {
p--;
}
if(*p == '/') return p + 1;
return file;
}
void find(char* path, char* file) {
int fd = open(path, 0);
struct dirent de;
struct stat st;
char buf[512], *p;
if(fd < 0) {
fprintf(2, "cannot open %s\n", path);
return;
}
if(fstat(fd, &st) < 0) {
fprintf(2, "cannot stat %s\n", path);
close(fd);
return;
}
switch (st.type) {
case T_FILE: //如果恰好是需要的文件,直接打印
if(strcmp(extract(path), file) == 0) {
printf("%s\n", path);
}
close(fd);
break;
case T_DIR:
strcpy(buf, path);
p = buf + strlen(buf);
*p++ = '/';
while (read(fd, &de, sizeof(de)) == sizeof(de)) {
if (de.inum == 0) continue; //被删除的文件,跳过
char name[DIRSIZ + 1];
memmove(name, de.name, DIRSIZ);
name[DIRSIZ] = 0;
if (strcmp(de.name, ".") == 0 || strcmp(de.name, "..") == 0) //跳过.和..
continue;
memmove(p, name, DIRSIZ);
p[DIRSIZ] = 0;
find(buf, file); //进入下级目录继续寻找
}
break;
}
close(fd);
}
int
main(int argc, char *argv[])
{
if(argc != 3) {
fprintf(2, "usage: [Dont find path file]\n");
exit(1);
}
find(argv[1], argv[2]);
exit(0);
}
xargs
要求:编写 UNIX xargs 程序的简单版本:从标准输入读取行,并为每一行运行一个命令,将行作为参数传递给该命令。您的解决方案应保存在文件 user/xargs.c 中。
这个实验需要注意的就是标准输入的数据按行处理,对于每行,以空格和制表符作为分割获取参数,最后和固定参数进行拼接。最后用fork + exec替换子进程为新的命令和拼接后的参数。
最终实现:
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
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/param.h"
void run_command(char* command, char** args, int argcount, char* line) {
char* line_args[MAXARG];
int line_argc = 0;
char *p = line;
while(*p) {
while(*p == ' ' || *p == '\t')p++; //遍历行,遇到空格或制表符就当作分隔符
if(*p == 0) break;
line_args[line_argc++] = p;
while(*p && *p != ' ') p++;
if(*p) {
*p = 0;
p++;
}
}
if(line_argc == 0) return;
char *argv[MAXARG];
int idx = 0;
argv[idx++] = command;
for(int i = 0; i < argcount; ++i) {
if(idx >= MAXARG - 1) break;
argv[idx++] = args[i];
}
for(int i = 0; i < line_argc; ++i) { //拼接标准输入的参数
if(idx >= MAXARG - 1) break;
argv[idx++] = line_args[i];
}
argv[idx] = 0;
if(fork() == 0) { //fork + exec
exec(command, argv);
fprintf(2, "xargs: exec %s failed\n", command);
exit(1);
} else {
wait(0);
}
}
int main(int argc, char *argv[]) {
if(argc < 2) {
fprintf(2, "usage: xargs command [args...]\n");
exit(1);
}
char* command = argv[1];
char *args[MAXARG];
int argcount = argc - 2;
for(int i = 0; i < argcount; ++i) {
args[i] = argv[i + 2];
}
char buf[512];
char c;
int n = 0;
while(read(0, &c, 1) == 1) { //按行处理
if(c == '\n') {
buf[n] = 0;
run_command(command, args, argcount, buf);
n = 0;
} else {
if(n < sizeof(buf) - 1) {
buf[n++] = c;
}
}
}
if(n > 0) { //恰好只有一行
buf[n] = 0;
run_command(command, args, argcount, buf);
}
exit(0);
}