$ git clone git://g.csail.mit.edu/xv6-labs-2020
$ cd xv6-labs-2020
$ git checkout util
$ make qemu
riscv64-unknown-elf-gcc -c -o kernel/entry.o kernel/entry.S
riscv64-unknown-elf-gcc -Wall -Werror -O -fno-omit-frame-pointer -ggdb -DSOL_UTIL -MD -mcmodel=medany -ffreestanding -fno-common -nostdlib -mno-relax -I. -fno-stack-protector -fno-pie -no-pie -c -o kernel/start.o kernel/start.c
...
riscv64-unknown-elf-ld -z max-page-size=4096 -N -e main -Ttext 0 -o user/_zombie user/zombie.o user/ulib.o user/usys.o user/printf.o user/umalloc.o
riscv64-unknown-elf-objdump -S user/_zombie > user/zombie.asm
riscv64-unknown-elf-objdump -t user/_zombie | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$/d' > user/zombie.sym
mkfs/mkfs fs.img README user/xargstest.sh user/_cat user/_echo user/_forktest user/_grep user/_init user/_kill user/_ln user/_ls user/_mkdir user/_rm user/_sh user/_stressfs user/_usertests user/_grind user/_wc user/_zombie
nmeta 46 (boot, super, log blocks 30 inode blocks 13, bitmap blocks 1) blocks 954 total 1000
balloc: first 591 blocks have been allocated
balloc: write bitmap block at sector 45
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0
xv6 kernel is booting
hart 2 starting
hart 1 starting
init: starting sh
$
*如果你在提示符下输入 ls,你会看到类似如下的输出:
$ ls
. 1 1 1024
.. 1 1 1024
README 2 2 2059
xargstest.sh 2 3 93
cat 2 4 24256
echo 2 5 23080
forktest 2 6 13272
grep 2 7 27560
init 2 8 23816
kill 2 9 23024
ln 2 10 22880
ls 2 11 26448
mkdir 2 12 23176
rm 2 13 23160
sh 2 14 41976
stressfs 2 15 24016
usertests 2 16 148456
grind 2 17 38144
wc 2 18 25344
zombie 2 19 22408
console 3 20 0
这些是mkfs
在初始文件系统中包含的文件;大多数是可以运行的程序。你刚刚跑了其中一个:ls
。
xv6没有ps
命令,但是如果您键入Ctrl-p,内核将打印每个进程的信息。如果现在尝试,您将看到两行:一行用于init,另一行用于sh。
退出 qemu : Ctrl-a x
。
实现xv6的UNIX程序sleep
:您的sleep
应该暂停到用户指定的计时数。一个滴答(tick)是由xv6内核定义的时间概念,即来自定时器芯片的两个中断之间的时间。您的解决方案应该在文件_user/sleep.c_中
提示:
在你开始编码之前,请阅读《book-riscv-rev1》的第一章
看看其他的一些程序(如**/user/echo.c, /user/grep.c, /user/rm.c**)查看如何获取传递给程序的命令行参数
如果用户忘记传递参数,sleep
应该打印一条错误信息
命令行参数作为字符串传递; 您可以使用atoi
将其转换为数字(详见**/user/ulib.c**)
使用系统调用sleep
请参阅**kernel/sysproc.c以获取实现sleep
系统调用的xv6内核代码(查找sys_sleep
),user/user.h提供了sleep
的声明以便其他程序调用,用汇编程序编写的user/usys.S**可以帮助sleep
从用户区跳转到内核区。
确保main
函数调用exit()
以退出程序。
将你的sleep
程序添加到**Makefile**中的UPROGS
中;完成之后,make qemu
将编译您的程序,并且您可以从xv6的shell运行它。
看看Kernighan和Ritchie编著的《C程序设计语言》(第二版)来了解C语言。
从xv6 shell运行程序:
$ make qemu
...
init: starting sh
$ sleep 10
(nothing happens for a little while)
$
如果程序在如上所示运行时暂停,则解决方案是正确的。运行make grade
看看你是否真的通过了睡眠测试。
请注意,make grade
运行所有测试,包括下面作业的测试。如果要对一项作业运行成绩测试,请键入(不要启动XV6,在外部终端下使用):
$ ./grade-lab-util sleep
这将运行与sleep
匹配的成绩测试。或者,您可以键入:
$ make GRADEFLAGS=sleep grade
效果是一样的
根据提示,通过观察已有的程序学习如何从命令行获取参数。阅读程序的完整代码,理解参数是如何获取及使用的。
echo.c
for(i = 1; i < argc; i++){
write(1, argv[i], strlen(argv[i]));
/*因为第0个参数往往是程序名,从第1个参数开始,向文件描述符1所指向的文件写参数,
文件描述符1是标准输出*/
......
}
grep.c
char *pattern = argv[1];
可以发现,命令行参数是通过数组argv[]传给main函数的。
int main(int argc, char *argv[]);
知道了如何获取参数,还必须知道需要获取什么参数。
根据user.h
中对sleep()
函数的定义:
int sleep(int);
发现sleep函数只需要输入一个参数,于是,根据提示有:
// user/sleep.c(需新建sleep.c文件)
#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 ticks\n");
exit(1); // 当参数个数不等于所要求的2时,模仿其他程序输出错误提示
}
sleep(atoi(argv[1])); // 使用atoi()函数将string参数转化为sleep()所需的int
exit(0);
}
根据1.1.1
中的内容,容易推断出,我们需要在UPROGS
下加入sleep函数,模仿得:
UPROGS=\
$U/_cat\
$U/_echo\
$U/_forktest\
$U/_grep\
$U/_init\
$U/_kill\
$U/_ln\
$U/_ls\
$U/_mkdir\
$U/_rm\
$U/_sh\
$U/_stressfs\
$U/_usertests\
$U/_grind\
$U/_wc\
$U/_zombie\
$U/_sleep\
一个可能会用到的vim小技巧,查找目标字符串:在vim命令模式下,输入斜杠
/
+要查找的字符串string
,如/UPROGS
,按回车,即可跳转到目标字符串的位置。此时,按n
为下一处位置,N
为上一处位置。%UPROGS
为自下而上查找,/UPROGS
为自上而下查找。
在xv6-labs-2020
目录下输入make qemu
:
qz@ubuntu:~/xv6-labs-2020$ make qemu
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0
xv6 kernel is booting
hart 1 starting
hart 2 starting
init: starting sh
在$
后输入sleep n
,为了让实验效果更加明显,可以让n稍微大一些:
$ sleep 20
我们可以看到,在输入sleep 20
按下回车后,有一个明显的停顿,这便是sleep了20个ticks。
按住Ctrl + a + x
,退出xv6,在xv6-labs-2020
目录下输入make grade
:
qz@ubuntu:~/xv6-labs-2020$ ./grade-lab-util sleep
== Test sleep, no arguments == sleep, no arguments: OK (4.3s)
== Test sleep, returns == sleep, returns: OK (1.4s)
== Test sleep, makes syscall == sleep, makes syscall: OK (1.3s)
可以看到通过了测试。
YOUR JOB
编写一个使用UNIX系统调用的程序来在两个进程之间“ping-pong”一个字节,请使用两个管道,每个方向一个。父进程应该向子进程发送一个字节;子进程应该打印“
”,其中
是进程ID,并在管道中写入字节发送给父进程,然后退出;父级应该从读取从子进程而来的字节,打印“
”,然后退出。您的解决方案应该在文件_user/pingpong.c_中。
提示:
使用pipe
来创造管道
使用fork
创建子进程
使用read
从管道中读取数据,并且使用write
向管道中写入数据
使用getpid
获取调用进程的pid
将程序加入到** Makefile **的UPROGS
xv6上的用户程序有一组有限的可用库函数。您可以在**user/user.h中看到可调用的程序列表;源代码(系统调用除外)位于user/ulib.c、user/printf.c和user/umalloc.c**中。
运行程序应得到下面的输出
$ make qemu
...
init: starting sh
$ pingpong
4: received ping
3: received pong
$
如果您的程序在两个进程之间交换一个字节并产生如上所示的输出,那么您的解决方案是正确的。
管道是一个小的内核缓冲区,作为一对文件描述符暴露给进程,一个用于读,一个用于写。将数据写入管道的一端就可以从管道的另一端读取数据。管道为进程提供了一种通信方式。
可以使用 fork 系统调用创建一个新的进程。fork 创建的新进程,称为子进程,其内存内容与调用的进程完全相同,原进程被称为父进程。
read/write 系统调用可以从文件描述符指向的文件读写数据。调用 read(fd, buf, n)从文件描述符 fd 中读取不超过 n 个字节的数据,将它们复制到 buf 中,并返回读取的字节数。当没有更多的字节可读时,读返回零,表示文件的结束。write(fd, buf, n)表示将buf中的n个字节写入文件描述符fd中,并返回写入的字节数。若写入字节数小于 n 则该次写入发生错误。
由实验指导书的1.3可知,“程序调用 pipe,创建一个新的管道,并将读写文件描述符记录在数组 p 中” :
int p[2];
pipe(p);
从管道读数据是一次性操作,数据一旦被读取,它就从管道中被抛弃,释放空间以便写更多数据。管道只能采用半双工通信,即某一时刻只能单向传输。要实现父子进程双方互动通信,需要定义两个管道。
于是:
int p1[2], p2[2];
pipe(p1), pipe(p2);
由实验指导书1.1可知,通过fork创建子进程,通过返回值判断是父进程还是子进程:
int pid = fork();
if(pid > 0){
//父进程代码;
}else if(pid == 0){
//子进程代码;
}else{
//fork出现错误。
}
如果没有数据写入,读会无限阻塞,直到新数据不可能到达为止(写端被关闭)。
为避免被自己阻塞,读管道之前先将写端关闭:
close(p1[1]);
通过观察user/user.h
中对read、write函数的定义以及实验指导书中的描述可知,参数二是一个指针,对于read、write函数来说,并不知道其指向的是什么类型的数据,也因此使函数获得了通用性。不论是int还是char还是什么类型,都只是按照参数要求,读/写n个字节的数据。
int write(int, const void*, int);
int read(int, void*, int);
调用时注意参数的一一对应:
read(0, buf, sizeof buf);
write(1, "pong\n", 5);
// user/pingpong.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc, char *argv[]) {
int p1[2], p2[2];
pipe(p1), pipe(p2);
char buf[5]; // 用于保存从管道读取的信息
int size;
int pid = fork();
if (pid == 0) {
//读取父进程传过来的信息
close(p1[1]); // 关闭管道1的写端
if ((size = read(p1[0], buf, sizeof buf)) > 0) { // 从管道1读取不大于buf个字节的数据到buf
printf("%d: received ", getpid());
write(1, buf, size);
} else {
printf("%d: receive failed\n", getpid());
}
//向父进程写信息
close(p2[0]); // 关闭管道2的读端
write(p2[1], "pong\n", 5); // 向管道2写从“pong\n"开始的不大于5个字节的数据
exit(0);
} else if (pid > 0) {
//向子进程写信息
close(p1[0]);
write(p1[1], "ping\n", 5);
wait(0);
//读取子进程传过来的信息
close(p2[1]);
if ((size = read(p2[0], buf, sizeof buf)) > 0) {
printf("%d: received ", getpid());
write(1, buf, size);
} else {
printf("%d: receive failed\n", getpid());
}
} else {
printf("fork error\n");
}
exit(0);
}
在Makefile
文件中加入pingpong
UPROGS=\
......
$U/_sleep\
$U/_pingpong\
在xv6-labs-2020
目录下输入make qemu
在命令行输入pingpong
,得到结果:
xv6 kernel is booting
hart 1 starting
hart 2 starting
init: starting sh
$ pingpong
5: received ping
4: received pong
$
Ctrl + a + x
,退出xv6,在xv6-labs-2020
目录下输入./grade-lab-util pingpong
:qz@ubuntu:~/xv6-labs-2020$ ./grade-lab-util pingpong
make: 'kernel/kernel' is up to date.
== Test pingpong == pingpong: OK (1.6s)
通过测试。
YOUR JOB
使用管道编写prime sieve
(筛选素数)的并发版本。这个想法是由Unix管道的发明者Doug McIlroy提出的。请查看这个网站(翻译在下面),该网页中间的图片和周围的文字解释了如何做到这一点。您的解决方案应该在 user/primes.c 文件中。
您的目标是使用pipe
和fork
来设置管道。第一个进程将数字2到35输入管道。对于每个素数,您将安排创建一个进程,该进程通过一个管道从其左邻居读取数据,并通过另一个管道向其右邻居写入数据。由于xv6的文件描述符和进程数量有限,因此第一个进程可以在35处停止。
提示:
请仔细关闭进程不需要的文件描述符,否则您的程序将在第一个进程达到35之前就会导致xv6系统资源不足。
一旦第一个进程达到35,它应该使用wait
等待整个管道终止,包括所有子孙进程等等。因此,主primes
进程应该只在打印完所有输出之后,并且在所有其他primes
进程退出之后退出。
提示:当管道的write
端关闭时,read
返回零。
最简单的方法是直接将32位(4字节)int写入管道,而不是使用格式化的ASCII I/O。
您应该仅在需要时在管线中创建进程。
将程序添加到**Makefile**中的UPROGS
如果您的解决方案实现了基于管道的筛选并产生以下输出,则是正确的:
$ 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
$
这个实验所需的关于pipe和fork的知识在本文中已有所涉及,理论知识部分不难,难点更多的是在理解题目意思与“通过管道实现并发”的模型上。请仔细阅读题目要求与并发模型的介绍。
根据提示,容易发现每个进程的工作都是类似的(很重要,多理解几遍!):
p = get a number from left neighbor // 将从左边进程获得的第一个数字作为p
print p
loop: // 循环判断从左边进程获得的其余数字
n = get a number from left neighbor
if (p does not divide n) // 若不能被p整除,则传向右边进程
send n to right neighbor
即每个进程都是“读、判断、写”、“读、判断、写”,很自然地想到用递归的方式解决问题。
受文件表的大小限制,整个系统的文件描述符是有限的,因此在递归过程中要及时关闭不需要的文件描述符,防止程序因为文件描述符不足而提早结束。当前进程只需要用到与父进程连接管道的读端和与子进程连接管道的写端,且用完后都需及时关闭。
另外,类似于1.3.2[3.1]的操作,为了让read能正常结束,需要解除管道写端的所有引用。
// user/primes.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#define SIZE 34
void recur(int p[2]) {
int primes, nums;
int p1[2];
close(0); // 0的复用
dup(p[0]);
close(p[0]);
close(p[1]);
if (read(0, &primes, 4)) {
printf("prime %d\n", primes); // 打印由父进程传来的第一个数字
pipe(p1);
if (fork() == 0) {
recur(p1); // 由子进程筛选下一批质数
} // 思考:考虑子进程已经在读、但是父进程还没写完的情况,子进程会等吗,还是报错呢?
else {
while (read(0, &nums, 4)) { // 从父进程读取数据
if (nums % primes != 0) { // 筛查,将符合条件的数字传给子进程
write(p1[1], &nums, 4);
}
}
close(p1[1]);
close(0);
wait(0);
}
} else {
close(0); // 递归出口:若父进程无数据输入,则说明筛查完毕
}
exit(0);
}
int main() {
int p[2];
pipe(p);
for (int i = 2; i < SIZE + 2; ++i) {
write(p[1], &i, 4);
}
if (fork() == 0) {
recur(p);
} else {
close(p[1]);
wait(0);
}
exit(0);
}
在Makefile
文件中加入primes
UPROGS=\
......
$U/_pingpong\
$U/_primes\
在xv6-labs-2020
目录下输入make qemu
在命令行输入primes
,得到结果:
$ primes
prime 2
prime 3
prime 5
prime 7
prime 11
prime 13
prime 17
prime 19
prime 23
prime 29
prime 31
$
YOUR JOB
写一个简化版本的UNIX的find
程序:查找目录树中具有特定名称的所有文件,你的解决方案应该放在_user/find.c_
提示:
find
下降到子目录中.
”和“..
”目录中递归make clean
,然后make qemu
==
”对字符串进行比较,而应当使用strcmp()
UPROGS
如果你的程序输出下面的内容,那么它是正确的(当文件系统中包含文件**b和a/b**的时候)
$ make qemu
...
init: starting sh
$ echo > b
$ mkdir a
$ echo > a/b
$ find . b
./b
./a/b
$
根据提示,仔细阅读user/ls.c
代码,结合ls
实际的使用过程,进行模仿。
一个可能会用到的小技巧,结合函数的实际使用分析源代码:比如,输入
ls
就会输出当前目录下的所有目录项,输入ls a
就会输出当前目录中的a
子目录下的所有目录项。将这个使用情景代入到源码的阅读当中,很多陌生的代码也会变得熟络起来。另一个可能会用到的小技巧,如果对函数本身很陌生不会用怎么办:在linux命令行中输入
man
+func
,比如man ls
,就会跳出关于函数的详细说明。
void
ls(char *path)
{
char buf[512], *p;
int fd;
/* 遇到陌生的结构体或函数,想办法找到源码,看看结构体里都有啥
去哪儿找呢?看看程序开头都include了些什么头文件,去这些头文件里边找*/
struct dirent de; // 在kernel/fs.h中定义:“目录项”,包含存储文件内容的inode号和文件名
//这里有个疑问:如果某文件被打开两次,有两个文件描述符吗?那该文件de中的文件描述符项填充的是哪个呢?⁉️
struct stat st; // 在kernel/stat.h中定义:存储文件的基本信息,如inode块号、文件类型、引用链接数、文件长度等
if((fd = open(path, 0)) < 0){ // 打开path所指向的文件,返回对应打开文件的文件描述符
fprintf(2, "ls: cannot open %s\n", path); // 若返回的文件描述符小于0,则意味着打开失败
return;
}
if(fstat(fd, &st) < 0){ // 读取fd所指文件的信息
fprintf(2, "ls: cannot stat %s\n", path);
close(fd);
return;
}
switch(st.type){ // 判断文件类型,即判断参数path所指向的文件的类型
case T_FILE: // 若path指向的是“文件”,则报错,因为ls的功能是输出指定“目录”下的所有目录项
printf("%s %d %d %l\n", fmtname(path), st.type, st.ino, st.size);
// fmtname():输出文件本身的名字(删去路径中除文件外的其他部分)
break;
case T_DIR: // 若path指向的是“目录”,则进入下个流程
if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf){ // 若“path/FileName\n"(文件完整路径)的长度超过了缓存区的大小
printf("ls: path too long\n"); // 则报错
break;
}
strcpy(buf, path); // 复制path到buf
p = buf+strlen(buf); // 指针p指向buf中已写path的末尾,准备续写文件路径
*p++ = '/'; // 按照文件路径的输出规则,在目录后添加斜杠‘/’
while(read(fd, &de, sizeof(de)) == sizeof(de)){ // fd是我们打开的目录,从目录中每次读取一个de,直到read读取失败为止,相当于是遍历了目录项
if(de.inum == 0) // 若de.inum为0,则选择跳过不打印。结合ls的使用场景,什么情况下的目录项不打印呢?
/* 在lab9 file system中有关于de.inum的进一步使用,可以发现de.inum是指保存文件内容的inode号
若inode号为0,意味着文件没有保存,是无效文件*/
continue;
memmove(p, de.name, DIRSIZ); // 将从de.name开始的内容写DIRSIZ个长度到p指针所指位置
p[DIRSIZ] = 0; // 在末尾写0,表示字符串的结束(在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); // 关闭打开的文件
}
find
与ls
的异同之处find:find all the files in a directory tree with a specific name,找到指定目录中所有名为filename的文件
ls:打印出指定目录中的所有目录项
同:
path
参数path
参数所指向的文件类型异:
path
参数外,find还需要filename
参数,ls不需要find(path, filename){
判断path类型
若为文件,则报错退出
若为目录{
遍历每个目录项{
判断目录项类型
若目录项为文件,则判断是否为要找的filename
若目录项为目录,则递归find(path1,filename)
}
}
}
// user/find.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
void
find(char *path, char *fileName) {
char buf[128], *p;
int fd, fd1;
struct dirent de;
struct stat st, st1;
if ((fd = open(path, 0)) < 0) {
fprintf(2, "path error\n");
return;
}
if (fstat(fd, &st) < 0) {
fprintf(2, "path stat failed\n");
close(fd);
return;
}
switch (st.type) {
case T_FILE:
fprintf(2, "path error\n");
return; // 以上部分判断输入路径是否正确
case T_DIR:
strcpy(buf, path);
p = buf + strlen(buf);
*p++ = '/'; // 保存当前正在搜索目录的完整路径,作为模板输出,新内容都是固定附加在p指针所指位置
while (read(fd, &de, sizeof(de)) == sizeof(de)) { // 遍历搜索目录
if (de.inum == 0)
continue;
if (!strcmp(de.name, ".") || !strcmp(de.name, "..")) { // 若是'.'或'..'目录,则跳过
continue;
}
memmove(p, de.name, DIRSIZ); // 在模板后添加属于自己的内容:自己的文件名
if ((fd1 = open(buf, 0)) >= 0) {
if (fstat(fd1, &st1) >= 0) {
switch (st1.type) {
case T_FILE:
if (!strcmp(de.name, fileName)) {
printf("%s\n", buf); // 若文件名与目标文件名一致,则输出其完整路径
}
close(fd1); // 注意及时关闭不用的文件描述符
break;
case T_DIR:
close(fd1);
find(buf, fileName); // 若为目录,则递归查找子目录
break;
case T_DEVICE:
close(fd1);
break;
}
}
}
}
break;
}
close(fd);
}
int
main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(2, "Usage:find path fileName\n");
exit(0);
}
find(argv[1], argv[2]);
exit(0);
}
在Makefile
文件中加入find
UPROGS=\
......
$U/_primes\
$U/_find\
在xv6-labs-2020
目录下输入make qemu
在命令行依次输入:
$ echo > b
$ mkdir a
$ echo > a/b
$ find . b
得到结果:
xv6 kernel is booting
hart 1 starting
hart 2 starting
init: starting sh
$ echo > b
$ mkdir a
$
$ echo > a/b
$ find . b
./b
./a/b
$
Ctrl + a + x
,退出xv6,在xv6-labs-2020
目录下输入./grade-lab-util find
:qz@ubuntu:~/xv6-labs-2020$ ./grade-lab-util find
make: 'kernel/kernel' is up to date.
== Test find, in current directory == find, in current directory: OK (2.1s)
(Old xv6.out.find_curdir failure log removed)
== Test find, recursive == find, recursive: OK (2.2s)
(Old xv6.out.find_recursive failure log removed)
通过测试。
在写代码的过程中,有遇到过许多问题,比如只能在一级子目录中查找而无法继续递归查找下去等,在适当的位置输出一些中间信息或许可以很好地帮助我们找到问题:判断可能会出错地位置,打印出可以判断是否错误的信息。
YOUR JOB
编写一个简化版UNIX的xargs
程序:它从标准输入中按行读取,并且为每一行执行一个命令,将行作为参数提供给命令。你的解决方案应该在**user/xargs.c**
下面的例子解释了xargs
的行为
$ echo hello too | xargs echo bye
bye hello too
$
注意,这里的命令是echo bye
,额外的参数是hello too
,这样就组成了命令echo bye hello too
,此命令输出bye hello too
请注意,UNIX上的xargs
进行了优化,一次可以向该命令提供更多的参数。 我们不需要您进行此优化。 要使UNIX上的xargs
表现出本实验所实现的方式,请将-n
选项设置为1。例如
$ echo "1\n2" | xargs -n 1 echo line
line 1
line 2
$
提示:
fork
和exec
对每行输入调用命令,在父进程中使用wait
等待子进程完成命令。MAXARG
,如果需要声明argv
数组,这可能很有用。UPROGS
。make clean
,然后make qemu
xargs
、find
和grep
结合得很好
$ find . b | xargs grep hello
将对“.
”下面的目录中名为**b**的每个文件运行grep hello
。
要测试您的xargs
方案是否正确,请运行shell脚本**xargstest.sh**。如果您的解决方案产生以下输出,则是正确的:
$ make qemu
...
init: starting sh
$ sh < xargstest.sh
$ $ $ $ $ $ hello
hello
hello
$ $
你可能不得不回去修复你的find
程序中的bug。输出有许多$
,因为xv6 shell没有意识到它正在处理来自文件而不是控制台的命令,并为文件中的每个命令打印$
。
通过给定参数加载并执行一个文件。
exec 需要两个参数:包含可执行文件的文件名和一个字符串参数数组。
exec的具体使用示例可以查看实验指导书。
与1.4 primes实验一样,本实验的理论也不难,重点是在理解题目意思上:“read lines from the standard input and run a command for each line, supplying the line as arguments to the command”,从标准输入读取,然后将读取的内容作为命令参数执行。
我们的工作只要从“从标准输入读取内容”开始就可以了,至于这additional arguments
是如何通过|
操作符跑到标准输入去的,并不需要我们关心。
这么一来,整个实验的内容就变得清晰起来了:1.读;2.添加命令;3.执行命令。
char stdIn[512];
int size = read(0, stdIn, sizeof stdIn);
根据示例二:
$ echo "1\n2" | xargs -n 1 echo line
line 1
line 2
及提示:“To read individual lines of input, read a character at a time until a newline (‘\n’) appears.”,我们知道,标准输入中可能会有多行内容出现,且不同行的内容会作为不同的参数进行执行。因此,我们需要将读取的内容分行存储,以便下一步作为不同的命令参数执行:
int i = 0, j = 0;
int line = 0;
for (int k = 0; k < size; ++k) {
if (stdIn[k] == '\n') { // 根据换行符的个数统计数据的行数
++line;
}
}
char output[line][64];
for (int k = 0; k < size; ++k) {
output[i][j++] = stdIn[k];
if (stdIn[k] == '\n') {
output[i][j - 1] = 0; // 用0覆盖掉换行符。C语言没有字符串类型,char类型的数组中,'0'表示字符串的结束
++i; // 继续保存下一行数据
j = 0;
}
}
根据1.2.2[1.1]如何获取参数
的内容可知,argv数组中保存着的是命令参数,其中argv[0]是命令本身,argv[1]往后是命令参数。注意参数不一定只有一个,有可能从argv[1]开始一直到argv[MAXARG]都是命令参数,所以不能把数据直接加在argv[1]后边。另外,由于这里多了一个xargs
命令,因此所有参数都往后递增一个位置。
char *arguments[MAXARG];
for (j = 0; j < argc - 1; ++j) {
arguments[j] = argv[1 + j]; // 从argv[1]开始,保存原本的命令+命令参数
}
i = 0;
while (i < line) {
arguments[j] = output[i++]; // 将每一行数据都分别拼接在原命令参数后
if (fork() == 0) {
exec(argv[1], arguments);
exit(0);
}
wait(0);
}
// user/xargs.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/param.h"
int main(int argc, char *argv[]) {
//从标准输入读取数据
char stdIn[512];
int size = read(0, stdIn, sizeof stdIn);
//将数据分行存储
int i = 0, j = 0;
int line = 0;
for (int k = 0; k < size; ++k) {
if (stdIn[k] == '\n') { // 根据换行符的个数统计数据的行数
++line;
}
}
char output[line][64]; // 根据提示中的MAXARG,命令参数长度最长为32个字节
for (int k = 0; k < size; ++k) {
output[i][j++] = stdIn[k];
if (stdIn[k] == '\n') {
output[i][j - 1] = 0; // 用0覆盖掉换行符。C语言没有字符串类型,char类型的数组中,'0'表示字符串的结束
++i; // 继续保存下一行数据
j = 0;
}
}
//将数据分行拼接到argv[2]后,并运行
char *arguments[MAXARG];
for (j = 0; j < argc - 1; ++j) {
arguments[j] = argv[1 + j]; // 从argv[1]开始,保存原本的命令+命令参数
}
i = 0;
while (i < line) {
arguments[j] = output[i++]; // 将每一行数据都分别拼接在原命令参数后
if (fork() == 0) {
exec(argv[1], arguments);
exit(0);
}
wait(0);
}
exit(0);
}
在Makefile
文件中加入xargs
UPROGS=\
......
$U/_find\
$U/_xargs\
在xv6-labs-2020
目录下输入make qemu
在命令行依次输入:sh < xargstest.sh
得到结果:
$ sh < xargstest.sh
$ $ $ $ $ $ hello
hello
$ $
Ctrl + a + x
,退出xv6,在xv6-labs-2020
目录下输入./grade-lab-util xargs
:qz@ubuntu:~/xv6-labs-2020$ ./grade-lab-util xargs
make: 'kernel/kernel' is up to date.
== Test xargs == xargs: OK (2.8s)
通过测试。
int exec(char *file, char *argv[])
char **arguments
与 char *arguments[]
是等价的,但最后发现不行。建议按照参数列表的格式进行定义:char *arguments[]
xargs echo good morning
就是长度为4的参数,所以在拼接参数时,不能直接在固定位置后拼接,需要根据具体的参数长度进行拼接:arguments[j] = output[i++];
,“j“为计算出的原参数长度。每个进程都有自己的父进程,操作系统的初始化进程相当于是第一个进程,是所有进程的祖先进程。当需要一个新进程执行命令时,调用fork
函数创建一个子进程,子进程会复制父进程的内存、文件描述符表等,复制后的内容和父进程是相互独立的,更改内容时互相不会影响。子进程调用exit
退出,但实际的子进程资源的释放,是在父进程的wait
函数中进行的,这也是”每个进程都有自己的父进程“的原因。
exec
函数会使用新的内存映像来替换进程的内存。先fork
,再exec
,因为父子进程是相互独立的,因此在子进程exec的内容不会影响到父进程,这为执行提供了更多的自由度。
文件描述符是一个小整数,代表了一个内核管理对象,这将文件、管道和设备之间的差异抽象化,隐藏了管理对象的细节,使操作者不需要了解底层的原理,只需要关心接口,从而能够更加高效地设计程序。这是实现操作系统”既能提供复杂功能又能实现简单接口“的重要一环。
I/O指输入输出,文件描述符使我们能够很方便地从各个对象读取、写入信息。每个进程单独维护一个以文件描述符为索引的表,因此不同进程的文件描述符n,可能指向不同的对象。如果两个文件描述符是通过一系列的 fork和dup调用从同一个原始文件描述符衍生出来的,那么这两个文件描述符共享一个偏移量。
管道是一个小的内核缓存区,(相当于加了限定的共享内存),提供一对文件描述符作为读写的接口提供给进程,可以从读端读到从写端写入的数据。
注意,在读取数据前,需要先关闭管道的写端,否则会出现管道中没有数据导致读取无限等待的情况。
lab1没有用到本节的内容。
讲述了文件名与inode之间的关系,等Lab9文件系统用到再细说。