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

【MIT-OS6.S081作业1.5】Lab1-utilities xargs

本文记录MIT-OS6.S081 Lab1 utilities 的xargs函数的实现过程

文章目录

  • 1. 作业要求
    • xargs(moderate)
  • 2. 实现过程
    • 2.1 代码实现
  • 附录:fork下我们为什么不用深拷贝呢,既然不用深拷贝,那么子进程会深拷贝父进程是嘛?
    • 形象地理解 `fork()` (在 xv6 中)
    • 这就是为什么不需要 `deepMalloc`

1. 作业要求

xargs(moderate)

Write a simple version of the UNIX xargs program: read lines from the standard input and run a command for each line, supplying the line as arguments to the command. Your solution should be in the file user/xargs.c.

Some hints:

  • Use fork and exec to invoke the command on each line of input. Use wait in the parent to wait for the child to complete the command.
  • To read individual lines of input, read a character at a time until a newline (‘\n’) appears.
  • kernel/param.h declares MAXARG, which may be useful if you need to declare an argv array.
  • Add the program to UPROGS in Makefile.
  • Changes to the file system persist across runs of qemu; to get a clean file system run make clean and then make qemu.

$ make qemu

init: starting sh
$ sh < xargstest.sh
$ $ $ $ $ $ hello
hello
hello
$ $

2. 实现过程

根据hint我们可以梳理出基本的步骤:

  • 解析 xargs 自身的参数,确定要执行的命令和其初始参数。
  • 循环读取标准输入的每一行,作为额外参数。
  • 对每行,组合 “命令 + 初始参数 + 行内容” 为完整参数列表。
  • 用 fork + exec 执行命令,父进程等待子进程完成后继续处理下一行。

2.1 代码实现

重点是解析xargs自身的参数,这里完整的参数列表定义为:

char* realArgv[MAXARG];

我们知道argv[0]xargs,我们要传递给exec的参数应该从argv[1]开始,遍历i从1 到 (argc - 1),将argv[i]依次赋值给realArgv的元素,这里使用了一个char**argvPtr 指针来指向realArgv数组的元素。 这样我们就处理完了初始参数。

然后我们解析从标准输入传过来的行内容的参数,这里分情况讨论:

  1. 获取的字符是’\n’,当前的命令行参数获取完毕,行参数最后添加’\0’,需要增加一个,并且增加一个命令行参数为0(NULL),并且执行exec。
  2. 获取的字符是’ ‘,当前的命令行参数获取完毕,行参数最后添加’\0’,需要增加一个。
  3. 获取的字符不是上面的两种情况,当前的命令行参数还没获取完毕。
#include "user/user.h"
#include "kernel/param.h"
#include "kernel/types.h"
#define bool uint8
#define true 1
#define false 0int main(int argc, char* argv[])
{char argvStr[1024];char* realArgv[MAXARG];char** argvPtr = realArgv;char** argvDiffPtr;char curChar = 'a';for (int i = 1; i < argc; ++i){*argvPtr = argv[i];argvPtr++;}argvDiffPtr = argvPtr;char* argvStrPtr = argvStr;char* argvStrCurArgvPtr = argvStr;bool canRun = false;while (read(0, argvStrPtr, 1) > 0){if (curChar == ' ' || curChar == '\n'){if (curChar == '\n') canRun = true;*argvStrPtr = '\0';*argvPtr = argvStrCurArgvPtr;argvStrCurArgvPtr= argvStrPtr + 1;argvPtr++;}if (canRun){*argvPtr = 0;canRun = false;argvPtr = argvDiffPtr;int ret = fork();if (ret == 0){exec(argv[1], realArgv);exit(0);}}}while (wait(0) != -1) {}exit(0);}

这里写的是一次性把所有的exec派发给子进程,然后在外面用while (wait(0) != -1) {}等待所有的子进程结束。为了子进程能够运行,这里使用静态的char数组argvStr存放从标准输入传过来的命令行参数,不同参数之间用’\0’间隔,我们看一下argvStr的布局,假如命令行读取的参数是12\n34\n45\n,而我们的xargs后面跟的是echo a,理论上我们应该得到:

a12
a34
a56

大概的逻辑为:
在这里插入图片描述

这里有个问题,就是执行完第一次的exec,argvStr的“12”参数其实没有用了,但是还会一直占着数组的位置,如果从输入传过的命令行参数特别多,那我们可能就得为argvStr数组开辟一个特别长的长度。一个思路是我们运行完一个exec,然后就从头开始赋值字符串,但是解析和执行exec都会操作数组,可能导致竞争发生,我们的exec可能会发生错乱(这里是我理解有问题,可以看再后面的代码,不过也记录一下深拷贝的实现,写得还是比较复杂的)。所以我们需要进行深拷贝,不仅拷贝指针,还要把指针指向的字符串也拷贝过去,于是我写了下面的代码,每一次exec以前先深拷贝,这里使用的是动态内存的写法,等到exec结束父进程再释放申请的动态内存。

在这里插入图片描述

#include "user/user.h"
#include "kernel/param.h"
#include "kernel/types.h"
#define bool uint8
#define true 1
#define false 0void* Malloc(int mallocUnitSize, int mallocLen, bool* success)
{void* ptr = malloc(mallocUnitSize * mallocLen);if (ptr == 0){*success = false;}return ptr;
}void deepFree(char** argv, int argvCnt)
{char** argvPtr = argv;for (int i = 0; i < argvCnt; ++i){if (*argvPtr) free(*argvPtr);argvPtr++;}free(argv);
}char** deepMalloc(char** argvPtr, int argvCnt, bool* successMalloc)
{char** copyArgv = (char**) Malloc(sizeof(char*), argvCnt, successMalloc);if (*successMalloc == false) return 0;char** copyArgvPtr = copyArgv;for (int i = 0; i < argvCnt - 1; ++i){int strLen = strlen(*argvPtr) + 1;*copyArgvPtr = (char*) Malloc(sizeof(char), strLen, successMalloc);if (*successMalloc == false){deepFree(copyArgv, i);return 0;}strcpy(*copyArgvPtr, *argvPtr);++copyArgvPtr;++argvPtr;}*copyArgvPtr = 0;return copyArgv;
}int main(int argc, char* argv[])
{char argvStr[1024];char* realArgv[MAXARG];char** argvPtr = realArgv;char** argvDiffPtr;for (int i = 1; i < argc; ++i){*argvPtr = argv[i];argvPtr++;}argvDiffPtr = argvPtr;char* argvStrPtr = argvStr;char* argvStrCurArgvPtr = argvStr;bool canRun = false;while (read(0, argvStrPtr, 1) > 0){if (*argvStrPtr == ' ' || *argvStrPtr == '\n'){if (*argvStrPtr == '\n') canRun = true;*argvStrPtr = '\0';*argvPtr = argvStrCurArgvPtr;argvStrCurArgvPtr = argvStrPtr + 1;argvPtr++;}if (canRun){*argvPtr = 0;int realArgvCnt = argvPtr - realArgv + 1;bool canMalloc = true;char** copyRealArgv = deepMalloc(realArgv, realArgvCnt, &canMalloc);int ret = fork();if (ret == 0){if (canMalloc)exec(argv[1], copyRealArgv);exit(0);}else{wait(0);if (canMalloc)deepFree(copyRealArgv, realArgvCnt);argvPtr = argvDiffPtr;argvStrPtr = argvStr;argvStrCurArgvPtr = argvStr;canRun = false;}}else++argvStrPtr;}exit(0);}

好吧,其实我这里有一个误区:当父进程循环回去准备读取下一行时,它会覆盖 argvStr 缓冲区,而此时子进程可能还在使用它。重新了解一下 fork() 的工作机制:

  • fork() 创建内存副本: 当调用 fork() 时,子进程会得到父进程内存的一个完整副本(在 xv6 中是这样,在现代 OS 中是“写时复制”,但效果类似)。

  • 子进程有自己的 argvStr: 子进程得到的副本包括了你栈上的 argvStr[1024] 缓冲区和 realArgv[MAXARG] 数组。

  • exec() 使用副本: 子进程调用 exec() 时,它使用的是它自己的 realArgv 副本,这些指针指向的字符串(无论是来自原始的 argv 还是来自 argvStr 副本)在子进程的地址空间中都是完全有效的。

  • 父进程安全地覆盖: 父进程调用 wait(),它会等待子进程完全退出(exec 成功或失败,然后 exit)。当 wait() 返回时,子进程已经彻底结束了。

  • 循环复用: 此时,父进程循环回到 read,它可以安全地覆盖 argvStr 缓冲区来处理下一行输入,因为上一个子进程已经不再需要它了。

我们可以进一步简化代码,不用深拷贝了!

在这里插入图片描述

#include "user/user.h"
#include "kernel/param.h"
#include "kernel/types.h"
#define bool uint8
#define true 1
#define false 0
int main(int argc, char* argv[])
{char argvStr[1024];char* realArgv[MAXARG];char** argvPtr = realArgv;char** argvDiffPtr;for (int i = 1; i < argc; ++i){*argvPtr = argv[i];argvPtr++;}argvDiffPtr = argvPtr;char* argvStrPtr = argvStr;char* argvStrCurArgvPtr = argvStr;bool canRun = false;while (read(0, argvStrPtr, 1) > 0){if (*argvStrPtr == ' ' || *argvStrPtr == '\n'){if (*argvStrPtr == '\n') canRun = true;*argvStrPtr = '\0';*argvPtr = argvStrCurArgvPtr;argvStrCurArgvPtr = argvStrPtr + 1;argvPtr++;}if (canRun){*argvPtr = 0;int ret = fork();if (ret == 0){exec(realArgv[0], realArgv);exit(0);}else{wait(0);argvPtr = argvDiffPtr;argvStrPtr = argvStr;argvStrCurArgvPtr = argvStr;canRun = false;}}else++argvStrPtr;}exit(0);}

我们在Makefile里加入对这个函数的编译:

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\ $U/_pingpong\ $U/_primes\ $U/_find\ $U/_xargs\ 

重新编译通过,测试xargs,如题所述正确打印:
在这里插入图片描述

然后我们再使用作业所说的测试命令:

./grade-lab-util xargs

在这里插入图片描述

完活!还是要理解fork的含义!可以参考下面的附录~

附录:fork下我们为什么不用深拷贝呢,既然不用深拷贝,那么子进程会深拷贝父进程是嘛?

以下是询问AI得到的答案:

答案是: fork() 所做的,比你代码中的 deepMalloc(深拷贝一个数据结构)要强大得多

  • 你代码中的 deepMalloc 是一个用户态操作:你遍历一个数组,在**堆(Heap)**上为每个字符串分配新内存,然后复制字符串内容。
  • fork() 是一个内核态操作:内核创建了一个全新的进程,这个进程的整个虚拟地址空间(包括栈 Stack堆 Heap 和所有数据)是父进程在 fork() 时刻的一个完整、独立的副本

形象地理解 fork() (在 xv6 中)

想象一下 fork() 就像一个神奇的复印机:

  1. 按下 fork() 按钮:

    • 父进程的内存里有:
      • 栈 (Stack): 包含 argvStr[1024] 缓冲区,里面存着 “hello\0world\0”;还包含 realArgv 数组,其指针指向 argvStr 内部(例如,指向 “hello” 和 “world”)。
      • 堆 (Heap): (在你的深拷贝版本中,这里有 malloc 来的内存)。
      • 代码段 (Code): 你的程序指令。
  2. 复印机工作:

    • 内核(复印机)为子进程分配全新的物理内存
    • 内核把父进程的整个栈(包括 argvStrrealArgv逐字节复制到子进程的新栈中。
    • 内核把父进程的整个堆逐字节复制到子进程的新堆中。
    • …等等。
  3. fork() 结束:

    • 现在你有两个独立的进程。
    • 父进程有一个 argvStr 缓冲区。
    • 子进程另一个完全相同但物理上分离argvStr 缓冲区。
    • 父进程修改它的 argvStr 不会影响子进程的 argvStr
    • 子进程修改它的 argvStr 不会影响父进程的 argvStr

这就是为什么不需要 deepMalloc

因为 fork() 已经为你复制了 argvStr 缓冲区:

  1. 父进程:在 fork() 之前,栈上的 argvStr 存着 “hello\0”。realArgv 指向这个 “hello”。
  2. fork() 发生
  3. 子进程:诞生了。它自己的栈上也有一个 argvStr 缓冲区,里面也存着 “hello\0”。它自己的 realArgv 也指向它自己的 “hello”。
  4. 子进程:调用 exec(realArgv[0], realArgv)。它使用的是它自己的 realArgvargvStr
  5. 父进程:调用 wait(0) 等待子进程结束。
  6. 父进程wait() 返回后,循环回去,read 覆盖它自己的 argvStr 来读取下一行(比如 “world\0”)。

这个覆盖动作绝对安全,因为它覆盖的是父进程的内存。而子进程(此时已经 exec 或者退出了)使用的是它自己的、早已被复制走的内存副本

总结:

你不需要 deepMalloc(在堆上做用户态深拷贝),因为 fork()(在内核态)已经为你做了整个进程内存的“深拷贝”(在xv6中是物理复制,在现代OS中是写时复制COW)。

你只需要使用栈上的 argvStr 缓冲区,fork() 会自动保证子进程得到一个安全的副本供其 exec 使用。

http://www.dtcms.com/a/618388.html

相关文章:

  • 文档抽取技术:通过OCR、NLP和机器学习技术,将非结构化的合同、发票等文档转化为结构化数据
  • 网站调研怎样做装修公司设计图
  • 西安网站制作优化顺德网站建设itshunde
  • 46 修改购物车数据
  • VUE的创建与配置
  • 44_FastMCP 2.x 中文文档之FastMCP集成:AWS Cognito 指南
  • 旅游网站建设市场分析公司就两个开发
  • 武昌手机网站59网一起做网站
  • 对抗拖库 —— Web 前端慢加密
  • BMAD-METHOD 开发方法论实践指南
  • MVC 模型
  • 【图像处理基石】如何从色彩的角度分析一张图是否是好图?
  • 从 1.56% 到 62.9%:SFT 推理微调优化实战
  • Java 实战:图书管理系统(ArrayList 应用)
  • 网站建设客户资料收集清单普洱茶网站建设
  • 网站反链数淮南网站建设报价
  • Week 25: 深度学习补遗:多模态学习
  • 广汉市建设局网站做外发的网站
  • html5商城网站开发h5制作的网站
  • 传统机器学习算法:基于手工特征
  • OpenCV(二十七):中值滤波
  • 建设部网站实名制举报学校网站规划
  • 免费网站域名使用手机免费表格软件app
  • Vue I18n 实现语言的切换
  • 动态规划基础题型
  • DotMemory系列:3. 堆碎片化引发的内存暴涨分析
  • 截图按钮图标素材网站网站建设掌握技能
  • 力扣-环形链表
  • 04总结-索引
  • 3C硬件:数码相机从入门到落地