MIT 6.S081 2021: Lab Utilities
实验准备
课程主页上面给了安装工具链的方法,根据自己的系统来按步骤操作就可以了。我的运行环境是WSL中的Ubuntu 20.04.2 LTS,安装过程中没有遇到问题。安装好之后挑一个目录,运行
git clone git://g.csail.mit.edu/xv6-labs-2020
把代码clone到本地,然后启动qemu虚拟机
cd xv6-labs-2020
make qemu
就可以启动操作系统了。
Sleep.c
需要注意,这个实验是在给xv6操作系统编写程序而不是真实的linux,这个xv6操作系统能提供的系统调用和C语言库函数是很少的。一定要搞清楚能用的系统调用和库函数有哪些!你作为一个用户能够调用的函数都在user/user.h里面(xv6手册里也有)。还要注意的就是看每道题目的Hints!
现在开始写实验代码,第一个题目是实现一个Sleep()函数,这个还是比较简单的。只需要传一个整数当参数,然后调用sleep()系统调用就可以了。首先是传入参数。我们知道main()传参数是使用int argc,char* argv[]来进行的,argc是参数个数,argv里存储的是命令行输入的参数。直接写程序:
#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 time\n");
exit(1);
}
int sleep_time=atoi(argv[1]);
if(sleep_time==0)
{
fprintf(2, "sleep: args error\n");
exit(1);
}
sleep(sleep_time);
exit(0);
}
实验要求没有输入参数的时候输出一条错误信息,所以先检查argc个数,如果不是两个(包括Sleep自己和一个整数)就往stderror(也就是2号文件)输出信息并终止程序。
pingpong.c
实验要求是创建一对父子进程,用管道把一个字节从父进程传到子进程,再把这个字节传回父进程。
先总结一下管道的特点。
1.管道是一种进程间通信的机制,进程可以用read()和write()系统调用直接对管道进行读写。
2.管道提供无边界的字节流通信,写端write()发送一次数据可以多次调用read()读出,也可以多次发送数据之后用一次read()读出。注意,管道是不记录边界的,所以要设定好读写字节的数目。
3.管道是一种半双工的通信机制。如果两个进程之间只使用一个管道的话,需要提供可靠的同步机制,否则进程可能会读取自己不久前发出去的数据。因此,父子进程之间通信应该尽量使用两个管道,但是仍然需要考虑死锁的问题。
4.pipe()创建的是匿名管道,只能用于同祖先的进程之间相互通信。
5.读取管道时,关闭写端则读端read()调用返回0;关闭读端则写端进程收到SIGPIPE信号;若管道为空,且写端未关闭,则程序被阻塞。
知道了管道的特点之后来写代码。由于父子进程之间需要双向传递,因此使用两个管道pfd1和pfd2。解题思路很直白,父进程用pfd1向子进程里写入一个字节,子进程用read()等着读取pfd1,把它存在一个char*数组里,然后再用write()往pfd2里写这个字节。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main()
{
int pfd1[2];//parent写->child读
int pfd2[2];//child写->parent读
pipe(pfd1);
pipe(pfd2);
if(fork()!=0)
{
//parent
int parent_pid=getpid();
char parent_msg[50];
close(pfd1[0]);
close(pfd2[1]);
//parent send a byte to child by pipe1
char* ping="a";
write(pfd1[1],ping,1);
//close pipe write
if(read(pfd2[0],parent_msg,1)!=0)
{
printf("%d: received pong\n",parent_pid);
}
exit(0);
}
else
{
//child
int child_pid=getpid();
char child_msg[50];
close(pfd1[1]);
close(pfd2[0]);
//child receive byte
if(read(pfd1[0],child_msg,1)!=0)
{
printf("%d: received ping\n",child_pid);
}
//close pipe read
//child send a byte
write(pfd2[1],child_msg,1);
exit(0);
}
}
需要注意的是:fork()创建的子进程会继承父进程的文件描述符表,也就是父子进程都可以访问管道的读端和写端。为了防止空管道阻塞程序,要关闭写进程的读端和读进程的写端。这操作要在fork()之后进行,否则会直接把父子进程的管道端口都关闭。
Primes.c
现在来看第三道题目,是需要我们用管道实现埃氏筛法找素数。点这里看埃氏筛法的原理 课程页面上也给出了算法:
p = get a number from left neighbor
print p
loop:
n = get a number from left neighbor
if (p does not divide n)
send n to right neighbor
也就是说,我们需要创建一排进程,给最左边的进程注入数字,每个进程都筛一个素数出来,然后过滤掉一部分数字,把过滤得到的数字传给右边的进程,如此级联进行直到数字被过滤完。
程序思路就是按照算法复现。设计一个void sieve(int* pfd)函数递归创建子进程。函数接受管道数组pfd[2]作为参数。
1.父进程关掉pfd[1]写端,然后读第一个数,如果没读到数说明已经筛完了,直接终止进程(递归终止条件)。读到的数字就是p。
2.父进程创建通向下一级的管道pfd_new,fork()创建子进程,先关闭父进程的读描述符pfd[0]再调用sieve(pfd_new),准备接受上一级传来的数字。
3.因为父进程是相对下一级的写端,所以还要关闭通向下一级管道的读端pfd_new[0],执行loop里的算法,然后关闭上一级的读端pfd[0]和通往下一级的写端pfd_new[1],调用wait()等待子进程结束。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "kernel/param.h"
#include "user/user.h"
#include "kernel/fs.h"
void sieve(int* pfd)
{
//读端,关闭写
close(pfd[1]);
int min_prime;
if(read(pfd[0],&min_prime,sizeof(int))==0)//如果未读入数据
{
close(pfd[0]);//关闭读端
exit(0);
}
printf("prime %d\n",min_prime);
int pfd_new[2];//创建通向下一级的管道
pipe(pfd_new);
if(fork()==0)
{
close(pfd[0]);//子进程也有这个描述符表
sieve(pfd_new);
}
close(pfd_new[0]);//相对下一级的写端。关闭通向下一级读端
int buffer;
while(read(pfd[0],&buffer,sizeof(int)))
{
if((buffer%min_prime)!=0)
{
write(pfd_new[1],&buffer,sizeof(int));
}
}
close(pfd[0]);//关闭上一级的读端
close(pfd_new[1]);//关闭通向下一级的写端
wait((int*)0);
exit(0);
}
int main()
{
int status;
int pfd[2];
pipe(pfd);
if(fork()==0)
{
sieve(pfd);
}
close(pfd[0]);//写端,关闭读,务必要在fork后关闭
for(int i=2;i<=35;i++)
{
write(pfd[1],&i,sizeof(int));
}
close(pfd[1]);
wait(&status);
exit(0);
}
find.c
题目要求实现find函数。题目提示里面说,看user/ls.c,我们看一下这个ls.c的具体内容:
void ls(char *path)
{
char buf[512], *p;
int fd;
struct dirent de;
struct stat 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)){
if(de.inum == 0)
continue;
memmove(p, de.name, DIRSIZ);
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);
}
这就是实现ls的核心部分。ls函数首先用open()打开path,使用fstat()读取path的信息并放到stat类型的结构体st中。然后检查这个path的类型,如果是文件,就直接输出存在st中的path各项信息。如果是目录文件,则需要输出目录下的所有信息。首先在buf里拼出目录相对path的完整路径,然后读入目录。
根据xv6手册里关于目录的描述,"Its inode has type T_DIR and its data is a sequence of directory entries. Each entry is a struct dirent (kernel/fs.h:56), which contains a name and an inode number. The name is at most DIRSIZ (14) characters; if shorter, it is terminated by a NUL (0) byte. Directory entries with inode number zero are free."既然目录是一串dirent结构体,那么可以循环读入目录文件,每次读一个dirent结构体st。从st中得到文件名,和buf里的目录路径拼在一起得到新路径,然后用stat打开这个新路径并输出信息。fmtname()函数的作用是取出路径中最后一个斜杠里的文件名。
了解了ls的工作原理,我们来设计find。find有两个参数,第一个是目录名dir_name,第二个是文件名file_name。
find的基本思想是使用BFS算法,使用函数void search(char* dir_name, const char* file_name)递归搜索以输入路径为根节点的目录树。
首先确定递归的边界条件之一:第一个参数dir_name是一个文件名。使用fmtname(需要修改一下)处理文件名之后直接比对即可,然后返回函数。find遍历目录的方式和ls基本相同。遍历目录时,遇到.和..两个文件要跳过,遇到文件时就和file_name比对,如果相同就打印这个文件的相对路径。如果遇到了目录,就递归调用search函数。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
const char* this_dir=".";
const char* parent_dir="..";
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 search(char* dir_name,const char* file_name)
{
char buf[512], *p;
int fd;
struct dirent de;
struct stat st;
if((fd = open(dir_name, 0)) < 0)
{
fprintf(2, "find: cannot open dir %s\n", dir_name);
return;
}
if(fstat(fd, &st) < 0)
{
fprintf(2, "find: cannot stat dir %s\n", dir_name);
close(fd);
return;
}
if(st.type==T_FILE)//如果传入的是文件名(递归边界条件)
{
if(!strcmp(fmtname(dir_name),file_name))
{
printf("%s\n",dir_name);//直接打印目录
}
return;
}
if(st.type==T_DIR)
{
if(strlen(dir_name) + 1 + DIRSIZ + 1 > sizeof buf)
{
printf("find: path too long\n");
return;
}
strcpy(buf, dir_name);
p = buf+strlen(buf);//p是定位指针
*p++ = '/';
struct stat st_tmp;//遍历目录下的文件
while(read(fd, &de, sizeof(de)) == sizeof(de))
{
if(de.inum == 0)
continue;
memmove(p, de.name, DIRSIZ);//把文件名复制到字符串buf的最后面
p[DIRSIZ] = 0;//这里准备遍历文件名 准备好生成文件或者目录的相对路径,存在buf里面,把这串字符的后一位置为0来生成字符串
if(stat(buf, &st_tmp) < 0)
{
//printf("ls: cannot stat %s\n", buf);
continue;
}
if(st_tmp.type==T_FILE)//如果是普通文件
{
if(!strcmp(de.name,file_name))//找到文件
{
printf("%s\n",buf);//打印文件的相对路径
}
}
if(st_tmp.type==T_DIR)//如果是目录
{
//递归搜索,使用BFS遍历directory tree
//禁止遍历. .. 这两个目录
if((!strcmp(de.name,this_dir))||(!strcmp(de.name,parent_dir)))
continue;
search(buf,file_name);//递归搜索
}
}
}
return;
}
int main(int argc, char *argv[])
{
if(argc==2)
{
search(".",argv[2]);
}
else
{
search(argv[1],argv[2]);
}
exit(0);
}
有一点很奇怪的是fmtname需要修改一下,因为
memset(buf+strlen(p), ' ', DIRSIZ-strlen(p))
是把buf里所有空余都填充为空格,如果不删这条,实验里的strcmp在比较fmtname(dir_name)和file_name会出现问题。
xargs.c
题目要求实现xargs指令,需要把前一条指令输出的结果传到后一条指令的参数列表里面。所以这个程序的核心其实就是处理前面指令的输出。
接下来以
find . b | xargs grep hello
为例说明。
find要用exec执行grep,这需要使用exec()系统调用。xv6手册里提供的exec()原型如下:
int exec(char *file, char *argv[])
真实世界里的exec是一个函数族,xv6提供的这个相当于是execv(),接受一个文件路径file和一个char*类型的指针数组argv,argv的每一个元素都指向一个字符串(也就是file的参数)。
首先分配一个指针数组argument,这就是稍后传exec的参数列表。先从argv[]里逐个读入grep的参数,然后使用malloc()分配一块能容下参数argv[i]的空间,把malloc()返回的指针存入argument[i]里,把argv[i]复制到这块内存中。
然后逐个字符读入从管道里流来的数据。shell的管道是把xargs的stdin文件重定向到了管道读端,所以可以用read直接从0号文件stdin里读取数据。设置一个读入缓冲buffer,用read逐个字符读取并存到buffer内,如果读到空字符就停止读取。这里使用一个char* p来指向read读入数据的位置。
如果遇到\n,说明读完了一行。把*p的内容换成'\0'来形成字符串,同样使用malloc()开辟空间并将这个字符串存到argument里面,并将p复位,清除buffer.另外根据xv6手册里面的例子,需要把argument所有参数的后面一项置为0。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "kernel/param.h"
#include "user/user.h"
#include "kernel/fs.h"
int main(int argc,char* argv[])
{
int status;
if(argc==1)//如果xargs后面没参数
{
printf("Usage: xargs [OPTION]... COMMAND [INITIAL-ARGS]...\n");
exit(0);
}
char** argument=(char**)malloc(sizeof(char*)*MAXARG);//稍后传exec的参数
//从argv[1]开始是需要执行的程序和其参数。先将这部分参数传入argument中
int i;
for(i=0;i<argc-1;i++)
{
int len=strlen(argv[i+1])+1;
argument[i]=(char*)malloc(sizeof(char)*len);//把argv[i+1]传到argument[i]
strcpy(argument[i],argv[i+1]);
}
//现在传入管道流过来的数据。
//然后存到argument中
char buffer[60];//buffer是暂存空间
char* p=buffer;//指示器,指示读入的地址
read(0,p,1);//从stdin读入一个字符
while (*p)//不断读入,直到遇到空字符位置
{
if(*p=='\n')//读到一行末尾
{
*p='\0';//附加上空字符从而构成字符串
//存入argument中
int len=strlen(buffer)+1;
argument[i]=(char*)malloc(sizeof(char)*len);
strcpy(argument[i],buffer);
//指示器复位到暂存空间开头
p=buffer;
//暂存空间清0
memset(buffer,0,60);
i++;
read(0,p,1);
continue;
}
p++;//读入本行下一个字符
read(0,p,1);
}
// for(int j=0;j<i;j++)
// {
// printf("%s\n",argument[j]);
// }
// printf("%d\n",i);
argument[i]=0;//argument最后一项应该置为0?
if(fork()==0)
{
if(exec(argv[1],argument)==-1)
{
fprintf(2,"xargs: exec error\n");
exit(1);
}
}
//记得收回子进程
wait(&status);
exit(0);
}