mit6.s081 lab1:Unix Utilities
1 sleep(easy)
要求:为 xv6实现 UNIX 程序睡眠; 睡眠需要暂停一段用户指定的时间。刻度是由 xv6内核定义的时间概念,即定时器芯片两次中断之间的时间。解决的程序应该在 user/sleep. c 文件中。
一些小提示:
- 查看user/中的其它程序,如echo.c,grep.c或rm.c,明白如何获取传递给程序的命令行参数。
- 如果用户忘记传入参数,程序应打印一条错误信息
- 命令行参数作为字符串传递,可以用atoi转换为整数(可参考ulib.c)
- 需要使用系统调用sleep
- user/user.h头文件中包含了sleep的C定义
- 确保调用exit来退出程序
- 完成解答后应将你的 sleep 程序添加到 Makefile 的 uPROGS 中,这样使用make qemu 才会编译你的程序,以后就可以在 xv6 shell 中运行它了。
- 运行
./grade-lab-util sleep
来测试你的程序
解决:
int main(int argc, char* argv[]){
if(argc < 2){
fprintf(2, "Usage: sleep ...\n");
exit(1);
}
sleep(atoi(argv[1]));
exit(0);
}
对以上程序的说明:
- main的参数列表中,argc表示命令行中传入的参数个数,argv数组则是以字符串形式保存传入的参数,默认传入的第一个参数为文件本身
- fprintf指定输出到一个流文件中,函数原型为int fprintf( FILE *stream, const char *format, [ argument ]...),fprintf()函数根据指定的格式(format),向输出流(stream)写入数据(argument)。这里将错误通过fd2写入屏幕中的stderr。或者也可以使用write函数来打印错误信息,毕竟fprintf函数最终也是要通过系统调用write的。形式为write(int fd, char *buf, int n),参数 fd 是文件描述符,0 表示标准输入,1 表示标准输出,2 表示标准错误。参数 buf 是程序中存放写的数据的字符数组。参数 n 是要传输的字节数,调用 user/ulib.c 的 strlen() 函数就可以获取字符串长度字节数。
2 pingpong(easy)
要求:编写一个程序,使用 UNIX 系统调用通过一对管道在两个进程之间“ ping-pong”一个字节,每个管道对应一个方向。父级应该向子级发送一个字节; 子级应该打印“pid: received ping”,其中 < pid > 是它的进程 ID,将管道上的字节写入父级,然后退出; 父级应该从子级读取字节,打印“ pid: received pong”,然后退出。将解决方案写在 user/pingpong.c 文件中。
一些提示:
- 使用pipe来创建管道
- 使用fork创建子进程
- 使用read和write实现对管道的读和写
- 对于当前进程使用getpid来获得子进程的PID
- 在xv6中可使用的库函数有限,用户可以在user/user.h中看到这个列表;源代码(除系统调用外),位于ulib.c、printf.c和umalloc.c中。
解决:
int main(){
int p[2];
pipe(p);
char s[10];
int pid;
pid = fork();
if(pid > 0){
//close(p[0]);
//char* tmp = "ping";
write(p[1], "ping", 5);
wait(0);//如果不使用wait会导致程序无法正常结束
read(p[0], s, 5);
char* tmp = "pong";
if(strcmp(tmp,s) == 0){
printf("%d: received pong\n", getpid());
}
}else if(pid == 0){
//close(p[1]);
read(p[0], s, 5);
int n = getpid();
char* tmp = "ping";
if(strcmp(s,tmp) == 0) {
printf("%d: received ping\n", n);
write(p[1], "pong", 5);
}
}else{
exit(1);
}
exit(0);
}
对以上程序的说明:
- 父进程中写-读-打印,子进程中读-打印-写
- 好像没有严格按照要求来,因为感觉一个管道就够用了
3 primes(moderate)
要求:使用管道编写一个基本筛选器的并发版本,将2-35的素数筛出来。程序应该使用pipe和fork来设置管道。第一个进程将数字2到35输入到管道中。对于每个素数,需要创建一个进程,该进程通过一个管道从左边的邻居读取数据,并通过另一个管道向右边的邻居写入数据。由于 xv6的文件描述符和进程数量有限,因此第一个进程的输入到35即可。相关提示文档在这里
一些提示:
- 注意关闭进程不需要的文件描述符,否则程序将在第一个进程达到35之前耗尽资源来运行xv6。
- 最开始的父进程要在所有子孙进程都退出之后才能退出。
- 当管道的写端关闭时,read返回0
- 最简单的方法是将32位int直接写入管道,而不是使用格式化的 ASCII I/O。
对帮助文档的说明:
里面大部分是讲历史和一些实验无关的东西,对于本实验的提示主要就是:首先将数字全部输入到最左边的管道,然后第一个进程打印出输入管道的第一个数 2 ,并将管道中所有 2 的倍数的数剔除。接着把剔除后的所有数字输入到右边的管道,然后第二个进程打印出从第一个进程中传入管道的第一个数 3 ,并将管道中所有 3 的倍数的数剔除。接着重复以上过程,最终打印出来的数都为素数。这个过程如下图所示:
实现:
void primes(int p[]){
int i;
read(p[0], &i, sizeof(int));
printf("prime %d\n", i);
close(p[1]);//不关管道写端,会导致死循环输出0
if(i == 31) exit(0);//只能以这样的方式来避免管道的非阻塞读,因为xv6里好像没法设置管道非阻塞模式
int pp[2];
pipe(pp);
int pid = fork();
if(pid > 0){
int digit;
close(pp[0]);
while(1){
if(read(p[0], &digit, sizeof(int)) < 1) {
break;
}
if(digit%i != 0) write(pp[1], &digit, sizeof(int));
}
close(p[0]);
close(pp[1]);
wait(0);
}else if(pid == 0){
primes(pp);
}else{
exit(1);
}
exit(0);
}
int main(){
int p[2];
pipe(p);
int pid = fork();
if(pid > 0){
close(p[0]);
int i = 2;
printf("prime %d\n", i);
int j;
for(j = i+1; j<=35; j++){
if(j%i != 0){
write(p[1], &j, sizeof(int));
}
}
close(p[1]);
wait(0);
}else if(pid == 0){
primes(p);
}else{
fprintf(2, "process create error!\n");
exit(1);
}
exit(0);
}
对以上程序的说明:
- 设计思想不是很难,在main中首先创建一个管道,然后从父进程中把2-35传给子进程,从第一个子进程开始用函数primes递归去看每一个子进程。如果从左边的管道读到了值,第一个值就是我们要输出的素数,然后创建新的管道和子进程,并且把满足条件的数送入管道中,否则就直接退出子进程。
- 对于每一个父进程,都要用wait等待子孙进程全部退出后才能exit
- 注意在每个进程中,对于不需要使用的文件描述符要及时关上,否则会导致死循环的问题
- 每个进程所拥有的管道读写文件描述符都是独立的, 并不是说父进程关闭写端子进程也对应关了
4 find(moderate)
要求:编写一个简单版本的 UNIX 查找程序: 查找指定目录下指定名称的文件。解决方案放在 user/find.c 文件中。
一些提示:
- 查看 user/ls.c 以了解如何读取目录。
- 使用递归的方式以实现find访问到子目录
- 不要循环递归“.”(当前目录)和“..”(父目录)
- 对文件系统的更改在 qemu 运行期间保持不变; 要获得一个干净的文件系统,先运行 make clean,然后使用 make qemu
- 关于C字符串的部分可以参考 《C程序设计语言》 例如第5.5节
- 注意不能像 Python 那样用 == 比较字符串,而是使用 strcmp ()
实现:
void find(char* path, char* target){
char buf[512], *p;
int fd;
struct dirent de;
struct stat st;
if((fd = open(path, 0)) < 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;
}
if(st.type != T_DIR){
fprintf(2, "find: %s is not a directory\n", path);
close(fd);
return;
}
if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf){
printf("find: path too long\n");
close(fd);
return;
}
//把path拷贝到buf中,在要查找的路径后面加上/
strcpy(buf, path);
p = buf+strlen(buf);
*p++ = '/';
//读取目录中每个文件的信息并检查
while(read(fd, &de, sizeof(de)) == sizeof(de)){
//暂不清楚是用来判断啥的,但是不加会递归有问题
if(de.inum == 0) continue;
//对于.和..直接跳过
if(!strcmp(de.name, ".") || !strcmp(de.name, "..")){
continue;
}
//把文件名拷贝给p,也等同于设置了新的路径
memmove(p, de.name, DIRSIZ);
//设置字符串结束位置
p[DIRSIZ] = 0;
if(stat(buf, &st) < 0){
printf("find: cannot stat %s\n", buf);
continue;
}
if(st.type == T_DIR){
find(buf, target);
}
if(st.type == T_FILE && !strcmp(de.name, target)){
printf("%s\n", buf);
}
}
close(fd);
}
int main(int argc, char* argv[])
{
if(argc != 3){
fprintf(2, "Format like: find . b\n");
exit(1);
}else{
find(argv[1], argv[2]);
}
exit(0);
}
对以上程序的补充:
- 整体设计思想参照ls的实现,在main函数中判断输入的参数是否满足条件,当输入参数不是三个的时候,报错并退出,否则调用find函数去找;
- 在find函数中,首先写退出条件:
如果open当前路径失败,直接退出;
open成功后进入给定的路径,调用stat读取文件描述符的状态信息复制给结构体st,复制失败,直接退出;
如果当前路径读到的(st.type)是文件属性,直接退出,因为我们的参数是在某个目录下找一个文件;
路径大小超过设置的文件名缓冲区大小,直接退出(ls中就对这一点进行了判断);
接着把绝对路径进行拷贝,循环获取路径下的文件名,与要查找的文件名进行比较,如果类型为文件且名称与要查找的文件名相同则输出路径,如果是目录类型则递归调用 find() 函数继续在这个目录下查找。 - strcmp(const char* str1, const char* str2)函数,比较str1和str2,str1>str2返回大于0的数,=返回0,<返回小于0的数
- fstat和stat都是用来获取相关文件状态信息的:
int fstat(int fd, struct stat *st)
,将文件描述符fd所指文件的状态信息复制给第二个参数,成功就返回0,否则返回-1;
int stat(const char *path, struct stat *st)
;与fstat的区别在于第一个参数的要求是文件全路径,而fstat需要的是open后得到的文件描述符fd,返回值的情况和fstat是一样的。
5 xargs(moderate)
要求:编写一个简单版本的 UNIX xargs 程序: 从标准输入中读取行,并为每一行运行一个命令,将该行作为参数提供给命令。解决方案应该在 user/xargs.c 文件中。
一些提示:
- 使用 fork 和 exec 在每行输入中调用命令。在父进程中使用 wait 等待子级完成命令
- 若要读取单个输入行,请一次读取一个字符,直到出现换行符(‘\n’),标准输入最后是有一个回车的
- 如果你需要声明一个argv数组,kernel/param.h头文件中声明的MAXARG或许会有用
对几个概念的区分: - 命令:我们在命令行里输入的那一整行东西整体叫做命令
- 命令行参数:
mkdir a b c
命令中的a b c
是mkdir所接收的参数,cd a
命令中a是cd所接收的参数。 - 标准化输入:我们输入一个命令后,等待我们继续输入的就是标准化输入,如
grep a
就是当我们的输入和a有匹配的时候,打印该输入。 - 标准化输出:shell执行一个命令输出的东西
- 管道符
|
:将符号前面命令的输入作为后面命令的命令行参数,如:
$ echo hello too | xargs echo bye
bye hello too
在这个命令中前一条命令打印出"hello too"来传给后一条命令,xargs在这里的作用是让它成为echo这条指令的命令行参数,于是最终的打印结果就是"bye hello too"如果不加xargs,那么"hello to"是要作为echo的标准化输入的。
实现:
int main(int argc, char* argv[])
{
sleep(10);
char buf[MAXARG];
read(0, buf, MAXARG);
//printf("%s\n", buf);
char* argvs[MAXARG];
int ct = 0;
for(int i=1; i<argc; i++){
argvs[ct++] = argv[i];
}
char *p = buf;
//每遇到一个换行符,就开一个子进程把这个换行符之前的参数执行掉
for(int i=0;i<MAXARG;i++){
if(buf[i] == '\n'){
//换行符设置为0,因为字符串是读到0结束的
buf[i] = 0;
int pid = fork();
if(pid == 0){
argvs[ct++] = p;
exec(argvs[0], argvs);
exit(0);
}else if(pid > 0){
wait(0);
//让p指向换行符后面
p = &buf[i+1];
}
}
}
exit(0);
}
对以上程序的补充:
- 根据实验目的,可以将xargs的实现分成三步:
1.用read把前一条命令的结果读出来并保存
2.把第二条命令的参数列表和前一条命令的结果合并
3.用exec加载第二条命令 - 加sleep(10)的原因是在测试
find . b | xargs grep hello
的时候,find操作比较慢,以致于xargs在执行的时候还没有得到find的标准输出,加上sleep后就可以通过了。
6 实验测试
注意要自己添加一个time.txt文件,记录你所花费的时间,不然最终的测评会在Cannot find time.txt那里扣一分