目录
前言:
1.1:启动xv6(Easy)
1.2:sleep(Easy)
1.2.1:sleep.c
1.2.2:makefile
1.3:pingpong(Easy)
1.4:primes(Moderate/Hard)
1.5:find(Moderate)
1.6:xargs(Moderate)
这节实验是MIT6.S08的2021版本的首个实验,主要是构建后续实验运行的环境,以及实现几个简单的小实验,大部分都挺简单的,唯一一个有难度的应该是1.4,主要是理解它的流程有点费劲。中文版的MIT6.S081实验指导书在这:http://xv6.dgs.zone/labs/requirements/lab1.html
这节按照实验指导书上的步骤来就行了,很简单。
在xv6-labs-2021(这个目录根据你的实验版本来)的目录下运行make qemu,就可以构建并运行xv6了:
在xv6环境下想要退出,可以再打开一个控制台,输入pkill -9 qemu即可。
按照实验指导书上的提示,在user文件夹中新建一个sleep.c的文件,完成调用sleep的代码。按照以下步骤进行代码书写工作:
1.参考其他/user/*.c文件,添加对应的头文件到sleep.c中。
2.完成main()函数,main函数具有两个参数,argc代表argv[]数组的大小,也就是传入参数的个数;argv[]数组按顺序存储了传入main函数的参数。其中,argv[0]指向程序运行的全路径名,argv[1]表示实际的第一个参数,因此argc<=1表示没有任何参数传入main函数,可以直接打印错误。
3.通过观察/user/user.h文件中sleep的定义,我们可以发现,调用sleep只需要传入一个参数即可,这个参数表示需要睡眠的事件:
// /user/user.h
int sleep(int);
在main函数中调用sleep系统调用,睡眠结束后调用exit(0)退出main函数完成代码。
#include "kernel/types.h"
#include "user/user.h"
int main(int argc, char *argv[])
{
if (argc <= 1) {
printf("error");
exit(1);
}
// argv[1]是传入main的第一个参数,表示sleep的睡眠时间
sleep(atoi(argv[1]));
exit(0);
}
关于makefile可以看这篇 :Makefile入门(超详细一文读懂)_我的小卷呀的博客-CSDN博客,sleep这个实验的makefile书写可以直接参考原本的代码,在UPROGS=\后面增加一条sleep的语句,其中UPROGS表示的就是/user文件夹。
UPROGS=\
$U/_cat\
......
$U/_zombie\
$U/_sleep\
这一小节实验和网络通信的内容差不多,就是创建一个管道,然后fork出一个子进程,利用管道实现父子进程之前的通信。
和1.2一样,在/user目录下创建一个pingpong.c文件完成相应的代码。
1.首先创建两个管道p1和p2,由于管道是半双工的,数据只能单向流动,因此需要两个管道分别负责数据从父到子和从子到父。
2.fork()出一个子进程,并根据pid的值分别进入到父或子的处理流程中。
3.管道具有以下特性:
如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次 read 会阻塞,直到管道中有数据可读了才读取数据并返回。
因此,当父进程利用管道p1向子进程写数据时,父进程要关闭管道的读端,子进程要关闭管道的写端,当子进程利用管道p2向父进程写数据时同理。
4.按照实验指导书中传递数据的步骤,父进程应该先向子进程传输一个字节,子进程接收后打印信息并向父进程传输一个字节,父进程接收后打印信息,至此完成所有工作。
5.如1.2一样,在makefile中添加pingpong
代码如下:
#include "kernel/types.h"
#include "user/user.h"
int main()
{
// 创建两个管道,p1负责将数据从父传到子,p2相反
int p1[2], p2[2];
pipe(p1);
pipe(p2);
// 创建一个缓冲区用来存储数据
char buf[1];
// 创建一个子进程
int pid = fork();
// 当pid == 0代表这是子进程,进入子进程的处理流程
if (pid == 0) {
// 关闭管道p1的写段和p2的读端
close(p1[1]);
close(p2[0]);
read(p1[0], buf, sizeof buf);
printf("%d: received ping\n", getpid());
write(p2[1], buf, sizeof buf);
exit(0);
}
else {
// 关闭p1的读端和p2的写端
close(p1[0]);
close(p2[1]);
write(p1[1], buf, sizeof buf);
read(p2[0], buf, sizeof buf);
printf("%d: received pong\n", getpid());
exit(0);
}
}
根据实验指导书上的提示,我们首先可以去查看/user/ls.c是如何读取目录的:
void
ls(char *path)
{
char buf[512], *p;
// 创建一个文件描述符用来描述文件
int fd;
// 创建一个目录项,存储文件的inode号和文件名
struct dirent de;
// 存储文件的基本信息比如文件的磁盘号、inode号、文件类型等
struct stat st;
// open()函数打开path路径指定的文件,返回一个文件描述符
if((fd = open(path, 0)) < 0){
fprintf(2, "ls: cannot open %s\n", path);
return;
}
// 将文件基本信息传入st中
if(fstat(fd, &st) < 0){
fprintf(2, "ls: cannot stat %s\n", path);
close(fd);
return;
}
// 根据文件的类型是FILE还是DIR进行不同的处理
switch(st.type){
case T_FILE:
......
case T_DIR:
......
}
close(fd);
}
仿造ls.c,我们可以把find的大致框架给写出来:
void find(char *curr_path, char *target) {
char buf[512], *p;
int fd;
struct dirent de;
struct stat st;
if ((fd = open(curr_path, 0)) < 0) {
fprintf(2, "find: cannot open %s\n", curr_path);
return;
}
if (fstat(fd, &st) < 0) {
fprintf(2, "find: cannot stat %s\n", curr_path);
close(fd);
return;
}
// 用于存储文件的名称
char *f_name;
switch (st.type) {
case T_FILE:
......
case T_DIR:
......
close(fd);
break;
}
}
接下来要完成的就是对于文件路径的处理,也就是switch语句块内部的逻辑,首先根据打开的文件是FILE还是DIR进入不同的分支进行处理。
对于FILE,由于curr_path给出的是一个文件的路径,如/home/user/ls.c,我们要做的是对这个路径进行解析,将路径中的目录名舍去,只留下ls.c,并判断是否和target目标文件一致,如果一致,则直接输出完整路径curr_path:
char *basename(char *pathname) {
char *prev = 0;
char *curr = strchr(pathname, '/');
while (curr != 0) {
prev = curr;
curr = strchr(curr + 1, '/');
}
return prev;
}
case T_FILE:
// 接收返回回来的最后一个目录,如curr_path是/home/user/ls.c,则返回user/ls.c
f_name = basename(curr_path);
int match = 1;
// 如果文件名为空,或者文件名和target不一致,则说明没找到目标文件
if (f_name == 0 || strcmp(f_name + 1, target) != 0) {
match = 0;
}
if (match)
// 输出完整路径
printf("%s\n", curr_path);
close(fd);
break;
case T_DIR:
如果是DIR文件的话,则循环读取当前目录下的文件,对于非.和..的路径,递归的调用find进入路径名下寻找目标文件:
case T_DIR:
// 初始化buf
memset(buf, 0, sizeof(buf));
uint curr_path_len = strlen(curr_path);
// 将完整的路径存入buf中
memcpy(buf, curr_path, curr_path_len);
buf[curr_path_len] = '/';
p = buf + curr_path_len + 1;
// 循环读取当前文件,如果读取到了非.和..的路径,则进入该路径再次find
while (read(fd, &de, sizeof(de)) == sizeof(de)) {
if (de.inum == 0 || strcmp(de.name, ".") == 0 ||
strcmp(de.name, "..") == 0)
continue;
// DIRSIZ是文件名的最大长度,值为14
memcpy(p, de.name, DIRSIZ);
p[DIRSIZ] = 0;
find(buf, target);
}
close(fd);
break;
}
完整代码如下:
/* retrieve the filename from whole path */
char *basename(char *pathname) {
char *prev = 0;
char *curr = strchr(pathname, '/');
while (curr != 0) {
prev = curr;
curr = strchr(curr + 1, '/');
}
return prev;
}
/* recursive */
void find(char *curr_path, char *target) {
char buf[512], *p;
int fd;
struct dirent de;
struct stat st;
if ((fd = open(curr_path, 0)) < 0) {
fprintf(2, "find: cannot open %s\n", curr_path);
return;
}
if (fstat(fd, &st) < 0) {
fprintf(2, "find: cannot stat %s\n", curr_path);
close(fd);
return;
}
char *f_name;
switch (st.type) {
case T_FILE:
// 接收返回回来的最后一个目录,如curr_path是/home/user/ls.c,则返回user/ls.c
f_name = basename(curr_path);
int match = 1;
// 如果文件名为空,或者文件名和target不一致,则说明没找到目标文件
if (f_name == 0 || strcmp(f_name + 1, target) != 0) {
match = 0;
}
if (match)
// 输出完整路径
printf("%s\n", curr_path);
close(fd);
break;
case T_DIR:
// 初始化buf
memset(buf, 0, sizeof(buf));
uint curr_path_len = strlen(curr_path);
// 将完整的路径存入buf中
memcpy(buf, curr_path, curr_path_len);
buf[curr_path_len] = '/';
p = buf + curr_path_len + 1;
// 循环读取当前文件,如果读取到了非.和..的路径,则进入该路径再次find
while (read(fd, &de, sizeof(de)) == sizeof(de)) {
if (de.inum == 0 || strcmp(de.name, ".") == 0 ||
strcmp(de.name, "..") == 0)
continue;
// DIRSIZ是文件名的最大长度,值为14
memcpy(p, de.name, DIRSIZ);
p[DIRSIZ] = 0;
find(buf, target);
}
close(fd);
break;
}
}
这一小节的实验室要用xargs命令将echo后的字符连接起来,首先按照题目的意思,在kernel/param.h声明MAXARG:
#define MAXARG 32
按照实验指导书的要求,我们需要先将echo后的参数已经xargs echo后的参数读取:
// 声明一个buf,存储标准输入
char buf[buf_size];
int line = 0;
// 声明一个数组,用来存储xargs echo后的字符
char *xargv[MAXARG] = {0};
// 依次将命令行中输入的参数放到xargv数组中,argv[1]为实际的第一个参数,因此i从1开始
for (int i = 1; i < argc; ++i) {
xargv[i - 1] = argv[i];
}
// 读取标准输入,并存储到buf中
int len = read(0, buf, sizeof buf);
考虑到参数可能存在\n分行的情况,需要扫描一遍读取到的数据,并分段存储:
for (int i = 0; i < len; ++i) {
// 由于如果遇到\n需要换行输出,因此要存储行数
if (buf[i] == '\n') {
++line;
}
}
// 储存每一行的字符,也就是两个\n之间的字符
char output[line][128];
int l = 0, index = 0;
for (int i = 0; i < len; ++i) {
// 存储该行数据
output[l][index++] = buf[i];
// 遇到换行符则进入下一行,令当前行l+1,令当前行的索引index=0
if (buf[i] == '\n') {
output[l][index - 1] = 0;
++l;
index = 0;
}
}
最后,将读取到的数据按段连接到xargv后面即可:
int out_line = 0, size = sizeof(xargv);
while (out_line < line) {
xargv[size] = output[out_line++]; // 将每一行数据都分别拼接在原命令参数后
if (fork() == 0) {
// 子进程执行输出
exec(argv[1], xargv);
exit(0);
}
wait(0);
}
完整代码如下:
#include "kernel/param.h"
#include "kernel/types.h"
#include "user/user.h"
#define buf_size 512
int main(int argc, char *argv[]) {
// 声明一个buf,存储标准输入
char buf[buf_size];
int line = 0;
// 声明一个数组,用来存储xargs echo后的字符
char *xargv[MAXARG] = {0};
// 依次将命令行中输入的参数放到xargv数组中,argv[1]为实际的第一个参数,因此i从1开始
for (int i = 1; i < argc; ++i) {
xargv[i - 1] = argv[i];
}
// 读取标准输入,并存储到buf中
int len = read(0, buf, sizeof buf);
for (int i = 0; i < len; ++i) {
// 由于如果遇到\n需要换行输出,因此要存储行数
if (buf[i] == '\n') {
++line;
}
}
// 储存每一行的字符,也就是两个\n之间的字符
char output[line][128];
int l = 0, index = 0;
for (int i = 0; i < len; ++i) {
// 存储该行数据
output[l][index++] = buf[i];
// 遇到换行符则进入下一行,令当前行l+1,令当前行的索引index=0
if (buf[i] == '\n') {
output[l][index - 1] = 0;
++l;
index = 0;
}
}
// out_line表示当前输出的是第几行,size表示的是xargv的总长
int out_line = 0, size = sizeof(xargv);
while (out_line < line) {
xargv[size] = output[out_line++]; // 将每一行数据都分别拼接在原命令参数后
if (fork() == 0) {
// 子进程执行输出
exec(argv[1], xargv);
exit(0);
}
wait(0);
}
exit(0);
}