MIT6.S081 - Lab1: Xv6 and Unix utilities
Part1:sleep
实验要求与提示
- 可以参考
user/echo.c
,user/grep.c
和user/rm.c
文件 - 如果用户忘记传递参数,
sleep
应该打印一条错误消息 - 命令行参数传递时为字符串,可以使用
atoi
函数将字符串转为数字 - 使用系统调用
sleep
,有关实现 sleep 系统调用的内核代码参考kernel/sysproc.c
(查找sys_sleep
),关于可以从用户程序调用的 sleep 的 C 定义,参阅user/user.h
,以及user/usys.S
表示从用户跳转到内核休眠的汇编代码 - 确保 main 调用
exit()
以退出程序 - 在
Makefile
中将 sleep 程序条件到UPROGS
中,这样可以使得make qemu
能够编译程序,并在xv6 shell
中运行
遇到的问题
问题一
- 问题:运行
./grade-lab-util sleep
显示错误/usr/bin/env: ‘python’: No such file or directory
,可能是没装 python2 或者装的是 python3 - 解决:将
grade-lab-util
文件第一行的!/usr/bin/env python
改为!/usr/bin/env python3
问题二
- 问题:
make qemu
后无法退出 - 解决:输入
ctrl+a
后抬起按键,然后再输入x
最终代码
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc, char *argv[])
{
if(argc < 2){ // 判断用户是否输入了参数
fprintf(2, "Usage: sleep has not input parameters!\n"); //将错误信息写入到标准错误2
exit(1); // 非正常运行导致退出程序
}
sleep(atoi(argv[1])); // 使用sleep系统调用,使用atoi将输入的字符串转为数字
exit(0); // 正常退出,注意这里没用return
}
注意要将 sleep 添加到 Makefile 的 UPROGS 中
- 可以使用
./grade-lab-util sleep
来进行打分,使用make grade
可以给整个实验打分
实验思考
- 实现
sleep
比较容易,但是要掌握sleep
、exit
、atoi
等的使用 - exit 和 return 的不同点:
-
exit(0)
:正常运行程序并退出程序 -
exit(1)
:非正常运行导致退出程序 -
return()
:返回函数,若在主函数中,则会退出函数并返回一值return
返回函数值,是关键字;exit
是一个函数return
是语言级别的,由 C 语言提供,它表示了调用堆栈的返回;而exit
是系统调用级别的,是由操作系统提供的(或者函数库中给出的),它表示了一个进程的结束return
是函数的退出(返回);exit
是进程的退出return
用于结束一个函数的执行,将函数的执行信息传出个其他调用函数使用;exit
函数是退出应用程序,删除进程使用的内存空间,并将应用程序的一个状态返回给 OS,这个状态标识了应用程序的一些运行信息,这个信息和操作系统有关,一般是 0 为正常退出,非 0 为非正常退出- 非主函数中调用
return
和exit
效果很明显,但是在main
函数中调用return
和exit
的现象就很模糊,多数情况下现象都是一致的
Part2:pingpong
实验要求与提示
- 调用一对管道(每个方向一个管道)在两个进程间"ping-pong"传递一个字节。父进程向子进程发送一个字节,子进程输出
<pid>: received ping
,其中<pid>
是它的进程 ID,然后子进程将字节写入管道,随后退出,父进程从子进程读取字节,打印<pid>: received pong
,随后退出 - 使用
pipe
创建一个管道;使用fork
创建子进程;使用read
从管道中读数据,使用write
将数据写入到管道;使用getpid
查找进程的 ID - xv6 上的用户程序中可供使用的库函数可以在
user/user.h
中查看,它们的源代码(除了用于系统调用)在user/ulib.c
、user/printf.c
和user/umalloc.c
中
遇到的问题
问题一
-
问题:VScode 中怎么调试用户程序
-
配置:首先应该将
launch.json
中的"stopAtEntry":
改为true
-
调试步骤:
- 点调试按键开启调试,此时会停在
kernerl/main.c
的入口处 - 在调试控制台输入
-exec file ./user/_filename
,filename
为需要调试的文件名称 - 在终端输入
filename
,点击继续开始调试的按键,然后就可以进入文件调试了 - 如果需要对该文件进行多次调试,直接在终端重新输入
filename
就行
注意:第一次调试某文件时,不要先设置断点,有的地方设置断点可能会导致进入不了该文件,等第一次调试之后再将断点打在能变为红色的地方
- 点调试按键开启调试,此时会停在
问题二
- 问题:python 用多了,C 语言中关于字符串、指针的用法就有点模糊了,程序错误都是因为这里
- 解决:韦东山有个视频是关于指针的,然后再找一个数组的视频或文档看一看
最终代码
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc, char *argv[]){
int p1[2];
int p2[2];
int pid;
char recv1[64];
char recv2[64];
pipe(p1);
pipe(p2);
pid = fork();
if(pid == 0){ // 子进程
close(p1[1]); // 关闭写通道
read(p1[0], recv1, sizeof("ping")); // 等待父进程将数据写入通道
printf("%d: received %s\n", getpid(), recv1);
close(p1[0]);
close(p2[0]);
write(p2[1], "pong", sizeof("pong"));
close(p2[1]);
exit(0);
}else{ // 父进程
close(p1[0]); // 关闭写通道
write(p1[1], "ping", sizeof("ping")); // 写入通道
close(p1[1]);
close(p2[1]);
read(p2[0], recv2, sizeof("pong"));
printf("%d: received %s\n", getpid(), recv2);
close(p2[1]);
}
exit(0);
}
实验思考
- 关于通道读写的过程一定要知道在什么情况下会发生什么,在不使用读端或者写端的时候一定要关闭,不然可能会造成自己被自己阻塞的现象
- 这个实验实现起来比较简单,但是能深挖的逻辑关系有很多,之后需要再进行复习,理清之间的关系
Part3: primes
实验要求与提示
- 使用 pipe 和 fork 来设置管道,首先将数字 2 到 35 输入管道。对于每个素数将安排创建一个进程,该进程通过一个管道从其左侧邻居读取数据,并通过另一个管道向其右侧邻居写入数据。由于 xv6 的文件描述符和进程数量有限,第一个进程可以在 35 时停止
- 要小心关闭进程不需要的文件描述符,否则程序将在第一个进程达到 35 之前耗尽 xv6 的资源
- 一旦第一个进程达到 35,它应该等到整个管道终止,包括所有的子进程、孙子进程等等。因此,主质数进程应该只在所有输出都打印出来之后退出,并且在所有其他质数进程都退出之后退出
- 当管道的写端关闭时,
read
返回零 - 最简单的方法是直接将 32 位(4 字节)整数写入管道,而不是使用格式化的 ASCII I/O
- 仅在需要时在管道中创建进程
最终代码
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#define WRITE 1
#define READ 0
void primeprocess(int p[]){
int first_num;
close(p[WRITE]);
if(read(p[READ], &first_num, sizeof(first_num)) == 0){ // 递归终止条件,读不到数据
close(p[READ]);
exit(0);
}
printf("prime %d\n", first_num); // 第一个进入管道的肯定是素数
int p_child[2];
pipe(p_child); // 创建下一个pipe
int pid = fork();
if(pid == 0){ // 子进程
primeprocess(p_child); // 递归函数
}else{ // 父进程
int num;
close(p_child[READ]);
while(read(p[READ], &num, sizeof(num)) != 0){
if(num % first_num != 0){
write(p_child[WRITE], &num, sizeof(num));
}
}
close(p[READ]);
close(p_child[WRITE]);
wait(0); // 需要等待子进程退出才能退出
}
exit(0); // 子进程结束
}
int main(int argc, char *argv[]){
int p[2];
pipe(p);
int pid = fork();
if(pid == 0){ // 子进程
primeprocess(p);
}else{
close(p[READ]);
for(int i = 2; i < 36; i++){
write(p[WRITE], &i, sizeof(i)); // 注意这里是将i的地址给write函数
}
close(p[WRITE]);
wait(0);
}
exit(0);
}
实验思考
- 这道题关键在于理解问题所表达的意思,用递归的方法主要是因为父进程需等待子进程退出,不过递归的思路比较简单
- 注意
write(p[WRITE], &i, sizeof(i))
中是传递的i
的地址
Part4: find
实验要求与提示
- 查看
user/ls.c
了解如何读取目录 - 使用递归查找子目录,但除去"."和".."
- 对文件系统的更改在
qemu
运行期间持续存在;要获得一个干净的文件系统,请运行make clean
,然后运行qemu
- 需要使用 C 字符串,注意比较字符串不能像 python 一样直接
==
,而是应该用strcmp()
等
遇到的问题
问题一
- 问题:不太熟悉 find 函数的使用,不知道它后面都带能带哪些函数
- 解决:这个实验仅仅是实现了 find 函数的部分功能,它的语法为
find [路径] [匹配条件] [动作]
,之后可以再尝试实现它里面更多的功能
最终代码
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
#include "kernel/fcntl.h"
char* fmtname(char *path)
{
char *p;
// 查找末尾斜杠后的第一个字符
for(p=path+strlen(path); p >= path && *p != '/'; p--);
p++;
return p;
}
void find(char *path, char *target)
{
char buf[512], *p;
int fd;
struct dirent de; // 记录文件前缀
struct stat st; // inode
if((fd = open(path, O_RDONLY)) < 0){
fprintf(2, "find: cannot open %s\n", path);
return;
}
if(fstat(fd, &st) < 0){
fprintf(2, "find: cannot stat %s\n", path);
close(fd);
return;
}
switch(st.type){
case T_FILE: // 文件
if(strcmp(fmtname(path), target) == 0)
printf("%s\n", path);
break;
case T_DIR: // 目录
if(strlen(path) + 1 + DIRSIZ + 1 > sizeof(buf)){
printf("ls: path too long\n");
break;
}
strcpy(buf, path); // 复制path到buf里
p = buf+strlen(buf); // 将p指向buf的末尾
*p++ = '/'; // 将buf的末尾添加/,从a/b变为a/b/
while(read(fd, &de, sizeof(de)) == sizeof(de)){ // 依次读取目录里面的文件
// 这里的判断注意加上"."和".."的判断,它们不进入递归
if(de.inum == 0 || strcmp(de.name, ".") == 0 || strcmp(de.name, "..") == 0)
continue;
memmove(p, de.name, DIRSIZ); // 合并文件为a/b/de.name
p[DIRSIZ] = 0; // 结束字符串
if(stat(buf, &st) < 0){
printf("find: cannot stat %s\n", buf);
continue;
}
find(buf, target); // 递归,从开始路径一直往深处查找文件
}
break;
}
close(fd);
}
int main(int argc, char *argv[])
{
if(argc != 3){
printf("Usage: find <dirName> <fileName>\n");
exit(1);
}
find(argv[1], argv[2]);
exit(0);
}
实验思考
read(fd, &de, sizeof(de))
是读取文件的方法,其中struct dirent de
用来记录文件前缀,它的结构体如下:
struct dirent {
ushort inum;
char name[DIRSIZ];
};
- 这道题在
user/ls.c
的基础上进行修改,但要注意在文件判断时,要排除"."
和".."
的情况,它们不能进入递归
Part5: xargs
实验要求与提示
- 使用
fork
和exec
对每一行输入调用命令。在父进程中使用wait
来等待子进程完成命令 - 要读取单独的输入行,每次读取一个字符,直到出现换行符
'\n'
。 kernel/param.h
声明MAXARG
,如果需要声明argv
数组,这可能很有用。
最终代码
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
#include "kernel/fcntl.h"
#include "kernel/param.h"
#define MAXBUF 1024
int main(int argc, char *argv[]){
char *xargs_argv[MAXARG]; // 字符串数组
char buf[MAXBUF]; // 字符数组
int i;
if(argc < 2){
printf("Usage: xargs <command>\n");
exit(1);
}
for(i = 0; i < argc; i++){
xargs_argv[i - 1] = argv[i]; // argv里面为管道|后面的输入,字符串数组
}
while(1){
int index = 0; // buf写入字节顺序
int buf_index = 0; // buf遇到' '或'\n'的首地址
int xargs_index = argc - 1;
int re; // read返回值
char ch; // 读到的一个字节
while(1){
re = read(0, &ch, sizeof(ch)); // 读取shell标准输入的一个字节
if(re == 0){
exit(0); // 表示没有读到字节,结束程序(这里是程序正常结束的唯一出口)
}
if(ch == ' ' || ch == '\n'){
buf[index++] = '\0';
xargs_argv[xargs_index++] = &buf[buf_index]; //将buf当前的字符串传给xargs_argv
buf_index = index; // 更新buf当前命令首地址
if(ch == '\n')break; // 跳出循环,执行一行命令
}else{
buf[index++] = ch;
}
}
xargs_argv[xargs_index] = (char *)0; // 结束一行命令
int pid = fork();
if(pid == 0){ // 子程序
exec(xargs_argv[0], xargs_argv);
}else{
wait((int *) 0); //等待子程序执行完毕
}
}
exit(0);
}
实验思考
- 这道题主要是要理解
xargs
的用法以及灵活使用指针和数组,其中字符串数组和字符数组的用法要区分清楚 - 可以用
'\0'
来标记字符串的结束 argv
的字符串只包括了管道最后一个输入,这里是整个代码的关键