当前位置: 首页 > news >正文

【6S.081】Lab1 Xv6 and Unix utilities

配置xv6环境

参考这篇文章即可:环境配置

  • 对于xv6的使用,更推荐用VSCode等文本编辑器进行启动,毕竟面对着命令提示符,还是太过寒酸了。

  • 每次实验完成之后需要进行提交和测试,并且要新创一个time.txt文件告知完成时间。

  • 每次创建文件会需要在Makefile中指定文件的编译顺序。即一般进行以下操作

    $U/_操作名\

  • 可以通过github进行提交。

  • 注意:代码中的注释往往包含重要信息,务必要仔细查看

实验1 sleep(难度:Easy)

实验要求

实现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语言。

实验思路

这题很简单,要求我们实现一个sleep函数来进行休眠操作。

我们看要求中,让我们去看user中的调用参数示例,这也是我们后续使用的很多的一个范式。

我们去看看echo.c

image-20250307112729855

可以看到,这个主函数中调用了两个参数,argc*argv[],一个是命令行参数的数量,一个是用来接收命令行参数的数组。

我们回到sleep.c中,知道了该如何接受命令行参数,也就知道了如何写它的框架。

#include<kernel/types.h>
#include<user/user.h>int main(int argc,char** argv)
{if(argc!=2)//这里指的是在命令行中输出时要用到的参数,我们这里要输出sleep 10(也就是休眠10个时间单位),所以也就有两个参数。{printf("Error Example:sleep 2\n");exit(-1);}...
}

然后我们再参考sleep要求我们输入的参数,它是一个int类型的参数,是用来表示休眠的时间。

那么我们如果需要接受一个int类型的参数来休眠指定时间,也就需要将命令行参数转类型成整数类型。这里使用到了atoi

ulib.c中可以找到

image-20250307134746296

我们照葫芦画瓢。

int num_of_tick = atoi(argv[1]);//这里argv[1]是指的int具体是多少,用一个变量来读取。
if(sleep(num_of_tick) < 0)
{printf("Can not sleep\n");exit(-1);
}

到此,一个简单的sleep程序就实现完成了。实际上我们并不需要去写sleep的实现过程,因为我们只需要直接调用即可。我们要做的重要的事情是:在命令行中实现该函数的调用。

完整代码

#include<kernel/types.h>
#include<user/user.h>int main(int argc,char** argv)
{if(argc!=2)//两个参数,一个是程序名,一个是参数,这里是sleep int{printf("Error Example:sleep 2\n");exit(-1);}//sleep系统调用函数在在user.h中被定义,但深层是通过汇编语言绑定到sys_sleep函数上的int num_of_tick = atoi(argv[1]);if(sleep(num_of_tick)<0){printf("Can not sleep\n");exit(-1);}exit(0);
}

实验2 pingpong(难度:Easy)

实验要求

编写一个使用UNIX系统调用的程序来在两个进程之间“ping-pong”一个字节,请使用两个管道,每个方向一个。父进程应该向子进程发送一个字节;子进程应该打印“<pid>: received ping”,其中<pid>是进程ID,并在管道中写入字节发送给父进程,然后退出;父级应该从读取从子进程而来的字节,打印“<pid>: received pong”,然后退出。您的解决方案应该在文件*user/pingpong.c*中。

提示

  • 使用pipe来创造管道
  • 使用fork创建子进程
  • 使用read从管道中读取数据,并且使用write向管道中写入数据
  • 使用getpid获取调用进程的pid
  • 将程序加入到***Makefile***的UPROGS
  • xv6上的用户程序有一组有限的可用库函数。您可以在user/user.h*中看到可调用的程序列表;源代码(系统调用除外)位于user/ulib.c*、**user/printf.c*user/umalloc.c*中。

实验思路

我们看提示,使用pipe来创造管道的目的是什么呢?因为这是作为父进程与子进程之间进行传输的方式。我们的实验目的是实现子父进程之间的字符串传递,那么就可以这样去想:通过两个管道实现了父进程和子进程之间的简单通信。父进程向子进程发送 “ping” 消息,子进程接收到后打印消息并向父进程发送 “pong” 消息,父进程接收到后打印消息。

所以说我们创建两个pipe,每个pipe都有两个文件描述符,一个用于读一个用于写。

int p1[2], p2[2];
pipe(p1), pipe(p2);

我们知道了文件描述符0、1、2分别表示什么意思之后,这里就好理解了。

  1. 标准输入(stdin):文件描述符为 0,用于从键盘或其他输入设备读取数据。
  2. 标准输出(stdout):文件描述符为 1,用于向屏幕或其他输出设备写入数据。
  3. 标准错误(stderr):文件描述符为 2,用于向屏幕或其他输出设备写入错误信息。

好,我们知道了这些含义,那么后续就可以使用它们来进行读写操作。

其次,我们需要建立一个缓冲区来进行数据的缓存,保存从管道读取的信息,相当于子进程和父进程之间的一个“中介”。

char buf[5]; // 用于保存从管道读取的信息
int size; //用于保存读取的字节数

好,一切前置准备就绪后,我们来创建子进程和父进程。

在创建前,我们需要知道pid 用于区分父进程和子进程,并根据不同的返回值执行相应的代码逻辑。

并且我们根据实验要求,来理解子进程和父进程分别要做些什么。

在子进程中:

  • 关闭管道 p1 的写端。
  • 从管道 p1 的读端读取数据,并打印接收到的信息。
  • 关闭管道 p2 的读端。
  • 向管道 p2 的写端写入 “pong\n”。
  • 退出子进程。
int pid = fork();//pid用于判断创建的进程
//如果pid==0,那么说明是创建的子进程
if (pid == 0) {// 子进程执行的代码close(p1[1]); // 关闭管道1的写端if ((size = read(p1[0], buf, sizeof buf)) > 0) { // 从管道1读取数据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写数据exit(0);
}

在父进程中:

  • 关闭管道 p1 的读端。
  • 向管道 p1 的写端写入 “ping\n”。
  • 等待子进程结束。
  • 关闭管道 p2 的写端。
  • 从管道 p2 的读端读取数据,并打印接收到的信息。
//如果返回的是子进程的进程ID,那么说明创建的是父进程
else if (pid > 0) {// 父进程执行的代码close(p1[0]); // 关闭管道1的读端write(p1[1], "ping\n", 5); // 向管道1写数据wait(0); // 等待子进程结束close(p2[1]); // 关闭管道2的写端if ((size = read(p2[0], buf, sizeof buf)) > 0) { // 从管道2读取数据printf("%d: received ", getpid());write(1, buf, size);} else {printf("%d: receive failed\n", getpid());}
}
//错误处理
else {printf("fork error\n");
}

注意:我们发现在主要代码的前后,通常都会有close操作,也就是关闭管道的读端或者写端。其实这里的作用是防止管道进入或者出去什么“奇怪的东西”,避免了意外的读写操作,从而产生资源泄露。

完整代码

// 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个字节的数据到bufprintf("%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);
}

实验3 primes(难度:Moderate)

实验要求

使用管道编写prime sieve(筛选素数)的并发版本。这个想法是由Unix管道的发明者Doug McIlroy提出的。请查看这个网站(翻译在下面),该网页中间的图片和周围的文字解释了如何做到这一点。您的解决方案应该在*user/primes.c*文件中。

您的目标是使用pipefork来设置管道。第一个进程将数字2到35输入管道。对于每个素数,您将安排创建一个进程,该进程通过一个管道从其左邻居读取数据,并通过另一个管道向其右邻居写入数据。由于xv6的文件描述符和进程数量有限,因此第一个进程可以在35处停止。

提示

  • 仔细关闭进程不需要的文件描述符,否则您的程序将在第一个进程达到35之前就会导致xv6系统资源不足。
  • 一旦第一个进程达到35,它应该使用wait等待整个管道终止,包括所有子孙进程等等。因此,主primes进程应该只在打印完所有输出之后,并且在所有其他primes进程退出之后退出。
  • 提示:当管道的write端关闭时,read返回零。
  • 最简单的方法是直接将32位(4字节)int写入管道,而不是使用格式化的ASCII I/O。
  • 您应该仅在需要时在管线中创建进程。
  • 将程序添加到***Makefile***中的UPROGS

实验思路

首先我们看提示中,需要注意的第一点:要及时关闭不需要的文件描述符,也就与pingpong中管道读写的关闭类似,及时解除引用。

其次,我们需要定义一个最大常量,保证在超过这个常量时,应该调用wait直到管道中止。

#define SIZE 34

接下来,我们就进行程序的分析:如果我们需要进行筛选操作,筛选出1-34中所有的素数,那么我们可以如何进行思考呢?没错,使用递归就可以实现。

好,那我们直接递归函数recur,进行该函数的编写即可。

//recur函数的功能:递归调用,找出质数(筛选)
void recur(int p[2])
{int primes, nums;int p1[2];close(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); // 将不是primes的倍数的数写入管道}}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; i++){write(p[1], &i, 4); // 将 2 到 SIZE (34) 写入管道}if (fork() == 0) // 创建子进程{recur(p);} else {close(p[1]);wait(0);}exit(0);
}

这里的递归函数的妙处就在于:

它利用管道和递归调用来实现质数筛选算法。每一层递归创建一个新的管道,用于传递筛选后的数。

主进程将 2 到 SIZE 的数写入管道,子进程通过递归调用 recur 函数筛选出质数,并将不是当前质数倍数的数传递给下一个子进程。每个子进程负责筛选一个质数,并将结果打印出来。

完整代码

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"#define SIZE 34//recur函数的功能:递归调用,找出质数(筛选)
void recur(int p[2])
{int primes, nums;int p1[2];close(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); // 将不是primes的倍数的数写入管道}}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; i++){write(p[1], &i, 4); // 将2到SIZE(34)写入管道}if(fork()==0)//创建子进程{recur(p);}else{close(p[1]);wait(0);}
exit(0);
}

实验4 find(难度:Moderate)

实验要求

写一个简化版本的UNIX的find程序:查找目录树中具有特定名称的所有文件,你的解决方案应该放在*user/find.c*

提示

  • 查看***user/ls.c***文件学习如何读取目录
  • 使用递归允许find下降到子目录中
  • 不要在“.”和“..”目录中递归
  • 对文件系统的更改会在qemu的运行过程中一直保持;要获得一个干净的文件系统,请运行make clean,然后make qemu
  • 你将会使用到C语言的字符串,要学习它请看《C程序设计语言》(K&R),例如第5.5节
  • 注意在C语言中不能像python一样使用“==”对字符串进行比较,而应当使用strcmp()
  • 将程序加入到Makefile的UPROGS

实验思路

提示中要我们看ls.c,我们照做:

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"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
ls(char *path)
{char buf[512], *p;int fd;struct dirent de;//目录项struct stat st;//文件属性if((fd = open(path, 0)) < 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);
}int
main(int argc, char *argv[])
{int i;if(argc < 2){ls(".");exit(0);}for(i=1; i<argc; i++)ls(argv[i]);exit(0);
}

我们逐帧分析:

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 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,列出目录内容。

它分为两种情况:1.如果是文件,就直接打印文件信息;2.如果是目录,就遍历目录中每个文件和子目录,并打印其信息。

我们试着使用一下这个ls.c

假设你的目录结构如下:

/home/joolin/xv6-labs-2020/user/├── ls.c├── file1.txt├── file2.txt└── subdir├── file3.txt└── file4.txt`

首先,你需要在 xv6 中编译 ls.c 文件:

gcc -o ls ls.c
  1. 列出当前目录的内容

    如果你在 [user](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html) 目录下运行 ls 命令而不带任何参数,它将列出当前目录的内容:

    ./ls

    输出可能如下:

    file1.txt 1 2 100file2.txt 1 3 200subdir  2 4 4096
    

    我们只举这么一个例子,目的只是为了知道它究竟有什么用。

    解释输出:

  • file1.txtfile2.txt 是文件名。
  • 1 是文件类型(1 表示普通文件,2 表示目录)。
  • 23456 是 inode 号。
  • 1002003004004096 是文件大小(以字节为单位)。

通过例子,你可以看到 ls.c 程序如何列出目录中的文件和子目录,并显示它们的详细信息。

好!现在我们知道了ls.c的作用,那么再去模仿其改写find.c岂不是得心应手?

我们需要根据find.c的需求来进行程序的编写。

它要我们做什么?

ls.c 用于列出目录内容,而 find.c 用于查找符合条件的文件。

可以理解为ls.c是遍历列出,而find.c是指定内容。

我们都知道文件系统是一层一层的,那么自然而然联想到递归。

所以我们的函数实现也会使用到递归。

而模仿ls.c中的代码,我们知道首先要判断是否能获取到目录和文件属性,然后要针对不同类型的文件进行不同的处理。

//错误情况
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;
}
//不同类型不同处理
switch (st.type)
{
case T_FILE:fprintf(2, "path error\n");return;
case T_DIR:...break;
}

针对文件就直接返回错误

针对目录就继续处理。

首先处理目录项。注意目录与目录项是不一样的,这里着重讲一下:

目录(Directory)

  • 定义:目录是文件系统中的一种特殊文件,用于包含其他文件和子目录。目录本身是一个文件,但它的内容是一个文件列表。
  • 也就是说它是一个列表。

目录项(Directory Entry)

  • 定义:目录项是目录中的一个条目,表示目录中的一个文件或子目录。每个目录项包含文件或子目录的名称和 inode 号。
  • 也就是说它可以是一个实际的文件。
//处理目录
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;memmove(p, de.name, DIRSIZ);...
}
//处理目录项
if ((fd1 = open(buf, 0)) >= 0)
{if (fstat(fd1, &st) >= 0){switch (st.type){case T_FILE:if (!strcmp(de.name, name)){printf("%s\n", buf);}close(fd1);break;case T_DIR:close(fd1);find(buf, name);break;case T_DEVICE:close(fd1);break;}}
}

打开目录项并获取其文件属性。如果是文件且文件名匹配,打印文件路径;如果是目录,递归调用 find 函数继续搜索;如果是设备文件,直接关闭文件描述符。

完整代码

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"void find(char *path, char *name)
{char buf[128], *p; // 缓冲区int fd, fd1;       // 文件描述符、struct dirent de;  // 目录项struct stat st;    // 文件属性// 如果文件描述符<0,则说明文件打开失败if ((fd = open(path, 0)) < 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;}switch (st.type){case T_FILE:// 文件/* code */fprintf(2, "path error\n"); // 路径错误return;case T_DIR:// 目录strcpy(buf, path);     // 复制路径p = buf + strlen(buf); // 指针指向路径末尾*p++ = '/';            // 路径末尾加上'/',表示这是个目录while (read(fd, &de, sizeof(de)) == sizeof(de))//循环读取目录项{{ // 遍历搜索目录if (de.inum == 0)continue; // 如果inum为0,则说明该目录为空}if (!strcmp(de.name, ".") || !strcmp(de.name, "..")) // 如果目录名为.或者..,则跳过{continue;}memmove(p, de.name, DIRSIZ); // 将目录名复制到p指向的位置if ((fd1 = open(buf, 0)) >= 0){if (fstat(fd1, &st) >= 0){switch (st.type){case T_FILE:                    // 如果是文件,进行文件路径打印if (!strcmp(de.name, name)) // 如果找到了文件,即目标文件名和文件名一致{printf("%s\n", buf); // 打印文件路径}close(fd1);break;case T_DIR:     // 如果是目录,就递归调用find函数,直到找到最终的目标文件名close(fd1); // 关闭文件描述符find(buf, name);break;case T_DEVICE://如果是设备文件,直接关闭文件描述符close(fd1);break;           }}}}break;}close(fd);
}int main(int argc, char *argv[])
{if (argc != 3){fprintf(2, "Usage: find path name\n");exit(1);}find(argv[1], argv[2]);exit(0);
}

实验5 xargs(难度:Moderate)

实验要求

编写一个简化版UNIX的xargs程序:它从标准输入中按行读取,并且为每一行执行一个命令,将行作为参数提供给命令。你的解决方案应该在user/xargs.c**

提示

  • 使用forkexec对每行输入调用命令,在父进程中使用wait等待子进程完成命令。
  • 要读取单个输入行,请一次读取一个字符,直到出现换行符(‘\n’)。
  • ***kernel/param.h***声明MAXARG,如果需要声明argv数组,这可能很有用。
  • 将程序添加到***Makefile***中的UPROGS
  • 对文件系统的更改会在qemu的运行过程中保持不变;要获得一个干净的文件系统,请运行make clean,然后make qemu

实验思路

实验要求我们编写一个程序,读取行内容,并且按照行来执行命令,执行完后将行作为参数提供给命令。

提示中说我们可以看看forkexec,前者我们已经了解,我们直接去看看后者。

image-20250308011848902

这里传了两个参数,一个是表示要执行的可执行文件的路径,一个是表示命令行参数数组

我们从名字上来分析:exec——execute,也就是执行的意思,那么再结合其给定的参数不难猜——它的作用是用来通过给定参数读取并执行文件

好,接下来我们根据实验要求逐步实现。

首先它要我们读取行内容,那么我们就使用read就完事了。

char stdIn[512];
int size = read(0, stdIn, sizeof stdIn);

其次它要我们按照行来执行指令,那么我们首先需要知道的是行的数量,在C语言中,换行通常使用"\n"来表示,我们将其作为指标进行计算即可。

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;++i;j = 0;}
}

接下来,我们就要进行执行的过程了。我们根据提示,得知命令行参数数组的大小不能超过MAXARG;将每一行数据作为参数拼接到命令参数后,并通过 fork 创建子进程执行命令。exec(argv[1], arguments) 用于执行指定的命令,并将参数传递给它。父进程等待子进程结束。

char *arguments[MAXARG];
for (j = 0; j < argc - 1; ++j) {arguments[j] = argv[1 + j];
}
i = 0;
while (i < line) {arguments[j] = output[i++];if (fork() == 0) {exec(argv[1], arguments);exit(0);}wait(0);
}

主要代码都已完成。需要注意的是,这个程序可以用来批量处理数据,但是不能MAXARG大小,否则xv6会资源报错。

完整代码

// 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);
}

]) {
//从标准输入读取数据
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);
}


相关文章:

  • Django全栈开发实战与架构思考
  • Python 训练营打卡 Day 44-预训练模型
  • 爬虫系统异常监控并邮件通知源码
  • @DNS服务器搭建配置优化-Linux
  • 微服务--消息队列mq
  • 一个小错误:Content-Type ‘text/plain;charset=UTF-8‘ is not supported 的粗心
  • list使用及模拟
  • Vue3+Element Plus动态表格列宽设置
  • ShardingSphere实现分库分表
  • 比特币---第1关:矿工任务及所需硬件
  • 如何存储和和使用比特币---第1关:比特币的存储
  • 升级openssl后无法使用cmake和curl的解决方法
  • 【C/C++ 为什么 unique_ptr 不支持拷贝构造、赋值构造等操作】
  • 大模型_Ubuntu24.04安装RagFlow_使用hyper-v虚拟机_超级详细--人工智能工作笔记0251
  • ubuntu24.04.2安装docker自动化脚本
  • 强化学习 A2C算法
  • java 将多张图片合成gif动态图
  • 微服务--nacos+feign
  • NY197NY205美光闪存固态NY218NY226
  • 两个矩阵的卷积运算
  • 台州知名的网站建设/站长域名查询工具
  • 一种子网站做的很好的视频广告/抚顺优化seo
  • 怎样做网站的优化/搜索引擎优化培训班
  • ICP备案不停网站/百度联系方式人工客服
  • 网易短链接生成/优化seo招聘
  • 如何维护网站建设/本溪seo优化