本实验需要编写一些用户程序,执行系统调用来达成目标。作为第一个实验,本次实验内容比较简单,主要内容是展示用户如何调用操作系统的接口,即system call,在下一个实验中将详细展示系统调用的工作流程。
本次实验的学习目标是:
学习并理解用户使用shell执行命令时的具体过程
对Unix的进程间通信方式之一pipe有基础了解
了解Unix中的file description,理解Unix中”一切皆文件“的理念
初步了解make项目以及Makefile
实验原文:Lab: Xv6 and Unix utilities (mit.edu)
进入工作文件夹下,输入以下命令
git checkout util #切换到util分支,对应本实验
git clean -xfd #清除无关文件(未追踪的、忽略的文件和文件夹)
git checkout -b solveutil #新建一个solveutil分支,来记录自己的编程过程
写一个用户程序sleep,执行系统调用来实现休眠。
$ make qemu
...
init: starting sh
$ sleep 10
(nothing happens for a little while)
$
user/grep.c
等文件来学习命令行参数如何传入程序。sleep.c
添加进Makefile
中的UPROGS。(添加完后点击Makefile
右上角图标重新加载make项目才能将sleep.c
加入索引)atoi
函数将命令行参数由字符串转为整形。main
函数调用了exit
函数来退出程序。//sleep.c
#include "kernel/types.h"
#include "user/user.h"
int
main(int argc,char* argv[]){
if(argc != 2){
fprintf(2, "Usage: sleep ticks\n");//stderr的文件描述符的值为2
exit(1);
}
int ticks = atoi(argv[1]);
sleep(ticks);
exit(0);
}
写一个用户程序pingpong,实现两个进程间的数据传递。
$ make qemu
...
init: starting sh
$ pingpong
4: received ping
3: received pong
$
pipe
建立管道。关于管道的理解可以看这篇博文:Linux系统编程pipe()
fork
创建子进程。read
和write
对管道进行读和写。getpid
获取当前进程的pid。//pingpong.c
#include "kernel/types.h"
#include "user/user.h"
int
main() {
int p1[2];//parent->child
int p2[2];//child->parent
pipe(p1);
pipe(p2);
if (fork() == 0) {
//child
char buffer[] = {'y'};
read(p1[0],buffer,sizeof(buffer));
fprintf(2,"%d: received ping\n",getpid());
write(p2[1],buffer,sizeof(buffer));
exit(0);
}
//parent
char buffer[] = {'x'};
write(p1[1],buffer,sizeof(buffer));
wait(0);
read(p2[0],buffer,sizeof(buffer));
fprintf(2,"%d: received pong\n",getpid());
exit(0);
}
使用管道写一个并发版本的素数筛来找出2~35中的素数。
相关链接:Bell Labs and CSP Threads (swtch.com)
概念图:
$ make qemu
...
init: starting sh
$ primes
prime 2
prime 3
prime 5
prime 7
prime 11
prime 13
prime 17
prime 19
prime 23
prime 29
prime 31
$
请牢记这个题目的要求是通过多线程来加速素数筛,在编写程序时要检查自己的程序是否满足并发,**一定不要在父进程写入所有数字后子进程才开始处理!**即使这样也能通过测试。
请小心关闭进程不需要的文件描述符,否则程序将在第一个进程达到 35 之前耗尽xv6的资源。(及时关闭管道非常重要!!!)
当一个进程读取完所有数字后,应该等到所有他的子进程终止才能终止,从而避免产生僵尸进程。(善用ctrl+p
)
当一个管道的写端被关闭时,对读端进行read
会返回0。
写入所有数字后再写入一个0来表示写入完毕,否则子进程无法知道父进程是否写入完毕。采取这种做法是因为执行了fork
后父进程才关闭管道写端,子进程并不能感应到管道写端已经关闭了。
//primes.c
#include "kernel/types.h"
#include "user/user.h"
int main() {
int p1[2];
pipe(p1);
int i;
for (i = 2; i <= 35; i++) {
write(p1[1], &i, sizeof(i));
}
i = 0;
write(p1[1], &i, sizeof(i));
while (1) {
int n, prime, p2[2];
pipe(p2);
read(p1[0], &prime, sizeof(prime));
printf("prime %d\n", prime);
if (read(p1[0], &n, sizeof(n)) && n) {
if (fork() == 0) {
//子进程会复制一遍父进程的两个管道,其中父进程用来与祖父进程交流的管道应该被关闭
close(p1[0]);
close(p1[1]);
p1[0] = p2[0];
p1[1] = p2[1];
continue;
}
do {
if (n % prime != 0)
write(p2[1], &n, sizeof(n));
} while (read(p1[0], &n, sizeof(n)) && n);
write(p2[1], &n, sizeof(n));
}
//关闭占用的管道
close(p1[0]);
close(p1[1]);
close(p2[0]);
close(p2[1]);
break;
}
wait(0);
exit(0);
}
写一个简单版本的UNIX find程序,ls path filename
为寻找目录path下所有文件名为filename的文件并输出。
$ make qemu
...
init: starting sh
$ echo > b
$ mkdir a
$ echo > a/b
$ find . b
./b
./a/b
$
user/ls.c
中的代码来学习如何读取文件夹。.
(即当前目录)和..
(即父目录)strcmp
函数而非==
//find.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
#include "kernel/fcntl.h"
char *target;
/*读取path中的文件名*/
char *
filename(char *path) {
char *p = path;
while (*p)
p++;
while (*p != '/' && p != path)
p--;
return p == path ? p : ++p;
}
void
find(char *path) {
int fd;
struct stat st;
struct dirent de;
char buf[512], *p;
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(filename(path), target) == 0) {
printf("%s\n", path);
}
break;
case T_DIR:
if (strlen(path) + 1 + DIRSIZ + 1 > sizeof buf) {
printf("find: 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 || strcmp(de.name, ".") == 0 || strcmp(de.name, "..") == 0)
continue;
memmove(p, de.name, DIRSIZ);
p[DIRSIZ] = 0;
find(buf);
}
break;
}
close(fd);
}
int
main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(2, "Usage: find path filename\n");
exit(1);
}
target = argv[2];
find(argv[1]);
exit(0);
}
写一个简单版本的UNIX xargs程序,从STDIN逐行读取,将每一行作为参数执行命令。比如echo hello too | xargs echo bye
,要输出bye hello too
,等价于echo bye hello too
,建议看原文理解。
使用gets
函数来读取STDIN。
使用fork
和exec
函数来执行子进程。
//xargs.c
#include "kernel/types.h"
#include "user/user.h"
int
main(int argc, char *argv[]) {
char buf[64], *xargs[32];
for (int i = 1; i < argc; i++)
xargs[i - 1] = argv[i];
while (1) {
int x = argc - 1;
gets(buf, 64);
if (buf[0] == 0)
break;
xargs[x++] = buf;
for (char *p = buf; *p; p++) {
if (*p == ' ') {
*p = 0;
xargs[x++] = p + 1;
} else if (*p == '\n') {
*p = 0;
}
}
if (fork() == 0) {
exec(argv[1], xargs);
}
}
wait(0);
exit(0);
}
输入make grade
或python ./grade-lab-util
来评测整个实验。
若想单独评测一个题目比如sleep可以使用python ./grade-lab-util sleep
。
评测整个实验前别忘了在Xv6根目录创建一个名为time.txt
的文件,在里面输入一个整数表明自己在这个实验上花了多少个小时。