我也算是新手,对于计算机的知识浅显了解,实践几乎为0,就慢慢造吧.....
目录
xv6文档学习
首先对于第一章讲课前需要把XV6书的第一章读完,不想读英文,便直接来看中文翻译版的。链接如下第零章 操作系统接口 | xv6 中文文档。
操作系统的接口
个人理解操作系统就是对于电脑资源的调配和管理者,属于底层硬件和上层应用程序的中间者。所以大体的功能可以分为对于底层硬件的管理和抽象,和对于上层应用程序提供服务。上层应用程序一般是多个程序同时运行(单个程序串行太浪费时间和资源了),所以book强调的就是资源共享,多路复用支持程序并行以及程序交互可以协作。
在本次学习和实验中搞定一些概念,XV6是一个操作系统,如下图,采用了传统的内核概念(内核可以理解为一个特殊的程序)。分为内核空间和用户空间,用户程序(也叫进程)通过系统调用来使用内核服务,即某些十分重要的功能用户程序是不能随便使用的,只能交给内核来使用,这就涉及到权限问题。

xv6操作系统中对外暴露的部分系统调用如下:
| 系统调用 | 描述 |
|---|---|
| fork() | 创建进程 |
| exit() | 结束当前进程 |
| wait() | 等待子进程结束 |
| kill(pid) | 结束 pid 所指进程 |
| getpid() | 获得当前进程 pid |
| sleep(n) | 睡眠 n 秒 |
| exec(filename, *argv) | 加载并执行一个文件 |
| sbrk(n) | 为进程内存空间增加 n 字节 |
| open(filename, flags) | 打开文件,flags 指定读/写模式 |
| read(fd, buf, n) | 从文件中读 n 个字节到 buf |
| write(fd, buf, n) | 从 buf 中写 n 个字节到文件 |
| close(fd) | 关闭打开的 fd |
| dup(fd) | 复制 fd |
| pipe( p) | 创建管道, 并把读和写的 fd 返回到p |
| chdir(dirname) | 改变当前目录 |
| mkdir(dirname) | 创建新的目录 |
| mknod(name, major, minor) | 创建设备文件 |
| fstat(fd) | 返回文件信息 |
| link(f1, f2) | 给 f1 创建一个新名字(f2) |
| unlink(filename) | 删除文件 |
进程和内存
xv6中的进程由两部分组成,一部分是用户内存空间(指令,数据,栈),一部分是仅对内核可见的进程状态。xv6提供了分时的特性,即CPU的使用时间分成了很多个时间片,每个时间片可能有不同的进程运行。同时每个进程和一个唯一的pid绑定,方便内核进行调度。
进程的创建如
上是fork()指令。被创建的进程是子进程,创建进程的是父进程。之前在学习的时候对于进程的了解很少,这里额外恶补了一下。总结点如下:
-
子进程的内存是父进程的完整副本(包括
fork()前已执行的代码结果),但通过写时复制优化减少实际内存占用。 -
子进程从
fork()的返回点开始执行,而非忽略之前的代码,而是继承父进程已执行代码的所有状态。 -
父子进程的执行顺序由调度器决定,默认情况下无法预测,需通过同步机制(如
wait())控制顺序。
这里fork()还会返回pid,向父进程返回子进程的pid,向子进程返回0(特殊情况会返回<0的情况)。
具体例子见以下代码:
int pid;
pid = fork();
if(pid > 0){
printf("parent: child=%d\n", pid);
pid = wait();
printf("child %d is done\n", pid);
} else if(pid == 0){
printf("child: exiting\n");
exit();
} else {
printf("fork error\n");
}
父进程会进入pid>0的分支,子进程会进入pid==0的分支。这里的wait()函数,表示该进程等待其子进程结束,并返回其pid。可以看出
parent: child=1234
child: exiting
可以被任意顺序打印,因为父子进程无法预知谁先运行。
parent: child=1234
parent: child 1234 is done
但以上输出的语句相对顺序是确定的,这里的wait相当于实现了一个同步操作。
接下来的系统调用是exec。具体功能是从给定文件读取内存镜像,并将其替换到调用它的进程的内存空间。当exec执行成功时,不再执行源程序,而是执行给定文件中的程序(因为给定文件的内存镜像,代码,数据等都替换到当前进程的内存中去了)。这就很奇妙了,通过fork创建子进程,再通过exec就可以使子进程去完成其他的任务。exec接受两个参数:可执行文件名和一个字符串参数数组。实例如下:
char *argv[3];
argv[0] = "echo ";
argv[1] = "hello!";
argv[2] = 0;
exec("/bin/echo", argv);
printf("exec error!");
这段代码将调用程序替换为 /bin/echo 这个程序,这个程序的参数列表为echo hello。大部分的程序都忽略第一个参数,这个参数惯例上是程序的名字(此例是 echo)。
I/O和文件描述符
文件描述符是一个整数,指向的对象称为一个“文件”,可以是文件、目录、设备或者一个管道。总之,利用文件对这些资源进行抽象,使得这些资源都同等的被认作是字节流(字面意思:字节像水流一样)。之前对文件描述符不太了解,这里有几个重要的点。
- 文件描述符是针对进程而言的。每个进程都维护一个索引表,里面包含了文件描述符及其对应的信息(位置,一些标志等)。
- 不同进程之间的文件描述符互不影响。由于每个进程都有自己维护的内存空间以及信息,所以每个进程相当于独立的个体,其各自维护各自的文件描述符,且每个进程都是从0开始的(0,1,2一般是标准输入,输出和错误输出)。所以不同进程的相同文件描述符可能代表不同的文件。
- 文件系统由内核统一管理。在内核层面,对每个文件有其唯一的标识(文件表项、inode等信息)。进程的所有文件操作都是通过系统调用来由内核对文件完成相关操作,在进程通过系统调用想要实现某个文件功能时,内核会调用其进程的PCB(进程控制块)找到文件描述符以及进程维护的文件表,找到该文件的相关信息,定位该文件并实现相应功能。
这里介绍两个系统调用,read()和write()。read(fd, buf, n) 从 fd 读最多 n 个字节(fd 可能没有 n 个字节),将它们拷贝到 buf 中,然后返回读出的字节数。write(fd, buf, n) 写 buf 中的 n 个字节到 fd 并且返回实际写出的字节数。前者可以理解为从前一个文件读并写到第二个文件,后者理解为从后一个文件写到前一个文件。
以下代码就是标准输入到标准输出的代码,即cat的本质实现:
char buf[512]
int n;
for(;;){
n = read(0, buf, sizeof buf); //从控制台读到buf中,每次最多读512
if(n == 0) break; //文件内容为空,退出循环
if(n<0) //读文件错误
{
fprintf(2, "read error!\n");
exit();
}
//写文件,写出的字节数不足n,报错
if(write(1, buf, sizeof buf) != n)
{
fprintf(2, "write error!\n");
exit();
}
}
统调用 close 会释放一个文件描述符,使得它未来可以被 open, pipe, dup 等调用重用。一个新分配的文件描述符永远都是当前进程的最小的未被使用的文件描述符。
文件描述符和 fork 的交叉使用使得 I/O 重定向能够轻易实现。fork 会复制父进程的文件描述符和内存,所以子进程和父进程的文件描述符一模一样。exec 会替换调用它的进程的内存但是会保留它的文件描述符表。这种行为使得 shell 可以这样实现重定向:fork 一个进程,重新打开指定文件的文件描述符,然后执行新的程序。下面是一个简化版的 shell 执行 cat<input.txt 的代码:
char *argv[2];
argv[0] = "cat";
argv[1] = 0;
//子进程
if(fork() == 0){
close(0); //先把pid=0释放
open("input.txt", O_RDONLY);
exec("cat", argv);
}
以上代码为子进程先把pid=0的文件描述符释放,再打开要重定向的文件,这时一定会把pid=0分配给input.txt,之后再用exec,执行cat程序(cat程序见上),由于cat程序默认是从文件描述符为0开始读入,所以这里就是从input.txt开始读并最终展示在标准输出上。
虽然 fork 复制了文件描述符,但每一个文件当前的偏移仍然是在父子进程之间共享的,考虑下面这个例子:
if(fork() == 0) {
write(1, "hello ", 6);
exit();
} else {
wait();
write(1, "world\n", 6);
}
在这段代码的结尾,绑定在文件描述符1上的文件有数据"hello world",父进程的 write 会从子进程 write 结束的地方继续写 (因为 wait ,父进程只在子进程结束之后才运行 write)。这种行为有利于顺序执行的 shell 命令的顺序输出,例如 (echo hello; echo world)>output.txt。
dup 复制一个已有的文件描述符,返回一个指向同一个输入/输出对象的新描述符。这两个描述符共享一个文件偏移,正如被 fork 复制的文件描述符一样。这里有另一种打印 “hello world” 的办法:
fd = dup(1);
write(1, "hello ", 6);
write(fd, "world\n", 6);
从同一个原初文件描述符通过一系列 fork 和 dup 调用产生的文件描述符都共享同一个文件偏移,而其他情况下产生的文件描述符就不是这样了,即使他们打开的都是同一份文件。
管道
管道是一个小的内核缓冲区,它以文件描述符对的形式提供给进程,一个用于写操作,一个用于读操作。从管道的一端写的数据可以从管道的另一端读取。管道提供了一种进程间交互的方式。
接下来的示例代码运行了程序 wc,它的标准输出绑定到了一个管道的读端口。
int p[2]; //分别表示管道的两个端口,0代表读,1代表写
char *argv[2];
argv[0] = "wc";
argv[1] = 0; //argv为一会子进程执行exec的字符串
pipe(p); //创建一个管道,p[0]和p[1]分别是读和写文件描述符
if(fork() == 0){ //子进程
close(0);
dup(p[0]); //将文件描述符0赋给文件描述符p[0]代表的文件
close(p[0]);
close(p[1]);
exec("/bin/wc", argv); //执行wc,这时文件描述符0对应的是管道读端口
}
else { //父进程
write(p[1], "hello world\n", 12); //向管道写端口写
close(p[0]);
close(p[1]);
}
这段程序调用 pipe,创建一个新的管道并且将读写描述符记录在数组 p 中。在 fork 之后,父进程和子进程都有了指向管道的文件描述符。子进程将管道的读端口拷贝在描述符0上,关闭 p 中的描述符,然后执行 wc。当 wc 从标准输入读取时,它实际上是从管道读取的。父进程向管道的写端口写入然后关闭它的两个文件描述符。
如果数据没有准备好,那么对管道执行的read会一直等待,直到有数据了或者其他绑定在这个管道写端口的描述符都已经关闭了。在后一种情况中,read 会返回 0,就像是一份文件读到了最后。读操作会一直阻塞直到不可能再有新数据到来了,这就是为什么我们在执行 wc 之前要关闭子进程的写端口。如果 wc 指向了一个管道的写端口,那么 wc 就永远看不到 eof 了。
xv6 shell 对管道的实现(比如 fork sh.c | wc -l)和上面的描述是类似的(7950行)。子进程创建一个管道连接管道的左右两端。然后它为管道左右两端都调用 runcmd,然后通过两次 wait 等待左右两端结束。管道右端可能也是一个带有管道的指令,如 a | b | c, 它 fork 两个新的子进程(一个 b 一个 c),因此,shell 可能创建出一颗进程树。树的叶子节点是命令,中间节点是进程,它们会等待左子和右子执行结束。理论上,你可以让中间节点都运行在管道的左端,但做的如此精确会使得实现变得复杂。
但管道和临时文件起码有三个关键的不同点。首先,管道会进行自我清扫,如果是 shell 重定向的话,我们必须要在任务完成后删除 /tmp/xyz。第二,管道可以传输任意长度的数据。第三,管道允许同步:两个进程可以使用一对管道来进行二者之间的信息传递,每一个读操作都阻塞调用进程,直到另一个进程用 write 完成数据的发送。
文件系统
xv6的文件系统包括两个对象,一个是文件,简单的字节数组、一个是目录,包括对文件和其他目录的命名引用。所有的目录形成一颗树,根节点是root。/a/b/c表示root目录下的a,a目录下的b,b目录下的c文件或目录。不从根节点出发的位置表示从当前进程所在位置的相对位置。调用进程的当前目录可以通过 chdir 这个系统调用进行改变。下面的这些代码都打开同一个文件(假设所有涉及到的目录都是存在的)
chdir("/a");
chdir("b");
open("c", O_RDONLY);
open("/a/b/c", O_RDONLY)
上下两种代码表达的意思相同。
这里解释下open系统调用第二个参数中常用的几个O_RDONLY(Read Only)只读,O_WRONLY(Write Only)只写,O_CREATE(create)没有时创建,需指定权限,通常和前两个一起用,用 | 运算符连接。
有很多的系统调用可以创建一个新的文件或者目录:mkdir 创建一个新的目录,open 加上 O_CREATE 标志打开一个新的文件,mknod 创建一个新的设备文件。下面这个例子说明了这3种调用:
mkdir("/dir");
fd = open("/dir/file", O_WRONLY | O_CREATE);
close(fd);
mknod("/console", 1, 1);
其中mknod是创建一个设备文件,设备文件和普通的文件不同,只存储了主设备号和次设备号。实际上它不存储实际的数据,而是当作用户程序和设备驱动的桥梁。比如说,当某个用户进程在某个设备文件发出read或write的系统调用,内核首先识别到这是一个设备文件,然后根据设备文件中的存储的元信息(主设备号和次设备号)找到在内核中注册过的该设备驱动,将对该文件的系统调用转发到该设备驱动中对应的实现函数中。从这可以看出,计算机设计“一切皆文件”的抽象特点。
fstat 可以获取一个文件描述符指向的文件的信息。它填充一个名为 stat 的结构体,它在 stat.h 中定义为:
//文件种类的宏定义
#define T_DIR 1 //1代表目录
#define T_FILE 2 //2代表文件
#define T_DEV 3 //3代表设备
struct stat {
short type; //代表文件种类
int dev; //代表文件系统的磁设备编号
uint ino; //该文件的inode编号,可唯一识别该文件
short nlink; //该文件的link个数
uint size; //该文件的大小
}
文件名和这个文件本身是有很大的区别。同一个文件(称为 inode)可能有多个名字,称为连接 (links)。系统调用 link 创建另一个文件系统的名称,它指向同一个 inode。下面的代码创建了一个既叫做 a 又叫做 b 的新文件。
open("a", O_CREATE | O_WRONLY);
link("a", "b");
读写 a 就相当于读写 b。每一个 inode 都由一个唯一的 inode 号 直接确定。在上面这段代码中,我们可以通过 fstat 知道 a 和 b 都指向同样的内容:a 和 b 都会返回同样的 inode 号(ino),并且 nlink 数会设置为2。
系统调用 unlink 从文件系统移除一个文件名。一个文件的 inode 和磁盘空间只有当它的链接数变为 0 的时候才会被清空,也就是没有一个文件再指向它。因此在上面的代码最后加上
unlink("a"),
我们同样可以通过 b 访问到它。另外,
open("/tmp/xyz", O_CREATE | O_WRONLY);
unlink("/tmp/xyz");
是创建一个临时 inode 的最佳方式,这个 inode 会在进程关闭 fd 或者退出的时候被清空。
xv6 关于文件系统的操作都被实现为用户程序,诸如 mkdir,ln,rm 等等。这种设计允许任何人都可以通过用户命令拓展 shell 。现在看起来这种设计是很显然的,但是 Unix 时代的其他系统的设计都将这样的命令内置在了 shell 中,而 shell 又是内置在内核中的。
有一个例外,那就是 cd,它是在 shell 中实现的(8016)。cd 必须改变 shell 自身的当前工作目录。如果 cd 作为一个普通命令执行,那么 shell 就会 fork 一个子进程,而子进程会运行 cd,cd 只会改变子进程的当前工作目录。父进程的工作目录保持原样。
现实情况
UNIX 将“标准”的文件描述符,管道,和便于操作它们的 shell 命令整合在一起,这是编写通用、可重用程序的重大进步。这个想法激发了 UNIX 强大和流行的“软件工具”文化,而且 shell 也是首个所谓的“脚本语言”。UNIX 的系统调用接口在今天仍然存在于许多操作系统中,诸如 BSD,Linux,以及 Mac OS X。
现代内核提供了比 xv6 要多的多的系统调用和内核服务。最重要的一点,现代基于 Unix 的操作系统遵循早期 Unix 将设备暴露为特殊文件的设计,比如刚才所说的控制台文件。Unix 的作者继续打造Plan 9 项目,它将“资源是文件”的概念应用到现代设备上,将网络、图形和其他资源都视作文件或者文件树。
文件系统抽象是一个强大的想法,它被以万维网的形式广泛的应用在互联网资源上。即使如此,也存在着其他的操作系统接口的模型。Multics,一个 Unix 的前辈,将文件抽象为一种类似内存的概念,产生了十分不同的系统接口。Multics 的设计的复杂性对 Unix 的设计者们产生了直接的影响,他们因此想把文件系统的设计做的更简单。
这本书考察 xv6 是如何实现类似 Unix 的接口的,但涉及的想法和概念可以运用到 Unix 之外很多地方上。任何一个操作系统都需要让多个进程复用硬件,实现进程之间的相互隔离,并提供进程间通讯的机制。在学习 xv6 之后,你应该了解一些其他的更加复杂的操作系统,看一下他们当中蕴含的 xv6 的概念。
视频课程的学习
【操作系统工程】精译【MIT 公开课 MIT6.S081】_哔哩哔哩_bilibili
copy
copy.c源码如下,比较简单:
//实际上实现的是从文件描述符为0的输入,到文件描述符1的输出
int main(){
char buf[64];
while(1){
int n = read(0, buf, sizeof(buf));
if(n <= 0) break;
write(1, buf, n);
}
exit(0);
}
注意默认下载的文件中并没有copy.c源码,也没法使用这个命令。这里视频里的意思是可以自己写添加进去。简单研究了下原理(我基础比较差请见谅...),从C语言文件到可执行文件需要经历四个核心阶段:预处理-编译-汇编-链接。
- 预处理阶段出要处理的工作是把注释去掉,inluce包含的代码包含过来,替换所有的宏定义等工作。
- 编译阶段将C语言文件转换成汇编语言文件,通常以.s(Unix和Linux)或者.asm(Windows和DOS)为扩展名。
- 汇编阶段将汇编语言代码文件转换成机器代码并生成符号表(记录函数和变量的地址信息),扩展名通常为.o,生成的文件成为目标文件。通常此时的地址未确定,都是可重定向的,绝对地址待链接阶段确定。
- 链接阶段,合并多个.o文件并最终生成可执行文件。简单扩展,静态链接:将库代码直接嵌入可执行文件中;动态链接:运行时加载共享库,节省内存占用。
发现user中的文件包含.c,.d,.o,.asm,.sym文件,分别代表C语言代码源文件,依赖文件(记录的是源文件中依赖的关系),目标文件,汇编文件,符号文件(源代码的符号表信息)。其中依赖文件,符号文件都是中间文件。
扯远了...回归正题,想要将自己编写的.c文件当成xv6系统中的指令,首先需要在Makefile文件中找到UPROGS模块(第180行左右),在上面仿照格式加入自己编写的copy即可。注意这里新加入命令后,需要make clean清除缓存,再make qemu进入虚拟环境重新编译,否则会直接进入上次编译后的环境。
最后加一点提醒,想退出copy命令,ctrl+c我这里不太管用,ctrl+d管用。
退出qemu模拟环境需要命令ctrl+a,松开后再按x即可。
open
#include "kernel/types.h"
#include "user/user.h"
#include "kernel/fcntl.h"
int main()
{
int fd = open("output.txt", O_WRONLY | O_CREATE);
write(fd, "Hello World!\n", 13);
exit(0);
}
一些xv6中的shell命令
ls可以列出当前目录的所有文件,grep x搜索带有x的一行。
shell提供IO重定向<输入重定向,>输出重定向。
ls > out,把ls的结果保存在out中。
gerp x < out ,在out中寻找带有x的那一行。
发现很多代码讲解都是文档中学习过的。略了...
Lab作业
如何配环境等等就不细说了,多看看教程和攻略就没啥大问题。
Sleep
第一次算是比较简单的,上面两个宏是必要的,因为系统调用所需的参数都是unsigned类型的,所以第一个types.h里面都是一些宏定义,把unsigned做了对应的缩写。第二个包含一些体统调用和用户程序的C语言接口。
#include "kernel/types.h"
#include "user/user.h"
int main(int argc, char *argv[])
{
if(argc != 2)
{
write(1, "Sorry, you should pass an argument!", 35);
}
else
{
int len = atoi(argv[1]);
sleep(len);
}
exit(0);
}
PingPong
#include "kernel/types.h"
#include "user/user.h"
int p[2];//代表管道的两个端口,0代表读,1表示写
int main()
{
pipe(p);//创建管道
//子进程
if(fork() == 0)
{
//子进程首先接受一个字节数据
char ch[64];
read(p[0], ch, 1);
//打印输出
int pid = getpid();
printf("%d: received ping\n", pid);
//通过管道写给父进程
write(p[1], ch, 1);
// close(p[1]);
}
else //父进程
{
//向子进程发送一个字节
write(p[1], "A", 1);
//接受子进程传递的数据
char ch[64];
read(p[0], ch, 1);
// close(p[0]);
//输出
int pid = getpid();
printf("%d: received pong\n", pid);
}
exit(0);
}
Prime
这个着实有点困难,题意不太明白的可以去看原题中的链接文章,以下的伪代码和图片比较清晰:
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

具体做法如下:
1.总体采用递归的形式去创建一个个子进程进行重复操作。
2.主程序在main函数中,用for循环2-35,并写入与子进程的管道中。
3.子进程进入递归函数,完成相应的操作,并有需要向右传递的数字时,创建管道继续递归。
4.细节很多,注意并发(注意创建子进程的时机)和管道端口的关闭时机。
代码如下:
#include "kernel/types.h"
#include "user/user.h"
//递归的创建子进程
void get_primes(int *p)
{
//先将左边集合创建的管道写关闭,改为只读
close(p[1]);
//从左边接收数字
int org;
if(read(p[0], &org, 4) == 0) //左边集合的管道数字写已关闭
{
close(p[0]);//关闭左边集合的读
exit(0);
}
//输出该质数
printf("prime %d\n", org);
//接着往后接收看有无需要传送的数字
int x = -1;
while(read(p[0], &x, 4) != 0)
{
if(x%org == 0) continue;
break;
}
if(x == -1 || x%org == 0)
{
close(p[0]);
exit(0);
}
//创建自己和右边集合的管道
int ps[2];
pipe(ps);
//创建右边的集合
if(fork() == 0)
{
get_primes(ps);
}
else
{
close(ps[0]);//该管道只需写即可
write(ps[1], &x, 4);
while(read(p[0], &x, 4) != 0)
{
if(x%org ==0) continue;
write(ps[1], &x, 4);
}
//关闭右边集合的写,方便最后一个进程退出
close(ps[1]);
//等待子进程
//注意这里关闭左集合读和等待子进程结束的顺序!!!
wait(0);
//关闭左边集合的读
close(p[0]);
exit(0);
}
return;
}
int main()
{
//创建管道,这个管道为主程序给第一个2进程送数据的管道
int p[2];
pipe(p); //p[0]读,p[1]
//子进程
if(fork() == 0)
{
get_primes(p);
}
else
{
//父进程只写,关闭读
close(p[0]);
int i;
for(i=2; i<=35; ++i)
{
write(p[1], &i, 4);
}
//写完以后关闭写进程
close(p[1]);
//等待子进程结束
wait(0);
}
exit(0);
}
Find
这个实验没有进程相关的任务,感觉更多是感受文件的本质。重点是理解“一切皆文件”的抽象概念。这里用到的目录也是文件,里面是一些内容标准的dirnet,通过读里面的内容即可知道目录里面有什么。具体对字符串的处理有很多对应的函数调用可以使用。仔细看ls.c的话就能理解其含义。查找子目录用递归实现。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
char* fmtname(char *path)
{
//用static定义一块文件名称内存,方便指针指向
static char buf[DIRSIZ + 1];
char *p;
for(p = path + strlen(path); p >= path && *p != '/'; p--) ;
p++; //p指向该文件的第一个字母
if(strlen(p) >= DIRSIZ) return p;
strcpy(buf, p);
buf[strlen(p)] = 0;
return buf;
}
//find函数表示从当前path目录及其子目录中所有包含fname的文件
void find(char *path, char *fname)
{
// printf("path=%s, fname=%s\n", path, fname);
int fd;
//这个结构体可以记录文件信息
struct stat st;
// path[strlen(path)] = 0;
//如果没打开文件报错返回
if((fd = open(path, 0)) < 0)
{
printf("find: cannot open %s\n", path);
return;
}
//获取该文件的信息
if(fstat(fd, &st) < 0)
{
printf("find: cannot stat %s\n", path);
close(fd);
return;
}
//缓存
char buf[512], *p;//这两个数组操作path方便替换
struct dirent de;
switch(st.type)
{
case T_DIR:
if(strlen(path) + 1 + DIRSIZ + 1 > sizeof(buf))
{
printf("find: path too long\n");
break;
}
strcpy(buf, path); //将path的值交给buf
p = buf + strlen(buf); //p指向buf的下一个没有值的地方
*p++ = '/'; //给地址加上一个'/'
//目录是个特殊的文件,内容包含一个个dirent,只需要一个一个读即可.
while(read(fd, &de, sizeof(de)) == sizeof(de))
{
if(de.inum == 0) //inum=0为无效条目,忽略不计
continue;
if(strcmp(de.name, ".") == 0 || strcmp(de.name, "..") == 0) continue;
strcpy(p, de.name);
find(buf, fname);//进入下一层
}
break;
case T_FILE:
if(strcmp(fmtname(path), fname) == 0)
{
printf("%s\n", path);
}
}
close(fd);
return;
}
int main(int argc, char *argv[])
{
//假设需要找很多文件,依次把文件名送入find查找函数
int i;
for(i=2; i<argc; ++i)
{
find(argv[1], argv[i]);
}
exit(0);
}
Xargs
这个命令之前没有了解过,所以得从基础慢慢说起.....
说这个命令得先说管道符 | ,shell中一般都是一个命令一个命令执行的,我们一般也都是一个命令一行直接执行。但若是我想同时执行若干个命令,并且想将这些命令串联起来协作呢?这就需要管道符 | ,它的作用就是把上一个命令的标准输出(stdout)当作下一个命令的标准输入(stdin),无需中间文件保存结果,直接将数据进行流动。以下举个例子:
# 统计当前目录下文件数量
ls | wc -l
ls 列出文件 → 输出传递给 wc -l 统计行数(即文件数量)。
但有些命令不接受标准输入的数据,只接受参数的形式,这就需要xargs命令了。
Xargs的作用是把管道符 | ,传递过来的数据处理成下一个命令的命令行参数。
比如说:rm命令不接受从stdin读取数据,只接受参数形式,以下就是xargs的例子:
find . -name "*.log" | rm # 错误!rm 不能从管道读取文件名
这样就会直接失败,因为rm需要命令行参数,不能从stdin中读取。
改成xargs的形式如下:
find . -name "*.log" | xargs rm
# 等效于 rm file1.log file2.log ...
xargs的编写主要采用fork和exec的结合。我感觉具体的难点在于如何从上个命令中读取信息并处理,放入下个命令的参数中。仔细查看定义,发现是上个命令的标准输出放到下一个命令的标准输入中去,所以直接read(1,...,...)即可。这里有很多换行符,根据题意,大致是让我们一个字符一个字符的处理,这样确实方便了许多。
#include "kernel/types.h"
#include "user/user.h"
#include "kernel/param.h"
int main(int argc, char *argv[])
{
char *arg[MAXARG];
if(argc > MAXARG)
{
write(1, "Error, The arguments are too long!\n", 35);
exit(0);
}
//初始化exec参数
arg[0] = argv[1];
int i;
for(i = 2; i < argc; ++i)
{
arg[i-1] = argv[i];
}
// 每次从上一个命令的标准输出中读取作为参数
char onec;
char buf[64];
int n, id = 0;
while((n = read(0, &onec, 1)) != 0)
{
if(onec == '\n')
{
arg[argc - 1] = buf;
arg[argc] = 0;
if(fork() == 0)
{
if(exec(arg[0], arg) == -1)
{
write(1, "error exec failed:", 18);
write(1, buf, sizeof(buf));
write(1, "\n", 1);
}
exit(0);
}
wait(0);
id = 0;
memset(buf, 0, sizeof(buf));
continue;
}
buf[id++] = onec;
}
exit(0);
}
这样的话,Lab1主要的实验和课程就结束了,不得不说,这个实验的互动感拉满,随时编写,随时可以测试。我也简单放个成果图,小小的骄傲一下吧。

完结撒花!!!
492

被折叠的 条评论
为什么被折叠?



