![读取直到管道关闭](https://linux22.com/image/119490/%E8%AF%BB%E5%8F%96%E7%9B%B4%E5%88%B0%E7%AE%A1%E9%81%93%E5%85%B3%E9%97%AD.png)
我现在正在做一项操作系统简介的作业,我很开心,但同时也很困惑。我现在正在做管道工作;我的代码如下。
最初,我的代码如下所示:
// Child process - write
if (fork() == 0) {
fprintf(stderr, "Child\r\n");
close(1);
dup(p[1]);
close(p[0]);
close(p[1]);
runcmd(pcmd->left);
// Parent process - read
} else {
wait(0);
close(0);
dup(p[0]);
close(p[0]);
close(p[1]);
fprintf(stderr, "Parent\r\n");
runcmd(pcmd->right);
}
我对此的思考过程是,父进程会等到子进程终止,然后从管道中读取数据,仅此而已。我在讨论页面上将此代码发布给我的导师,他告诉我该代码存在几个问题,其中之一是:
- 如果子进程运行的输入时间足够长而阻塞了管道,则父进程可能会无限挂起。
他提到,因此(关于wc
),正确的实现是使用阻塞读取命令,该命令将在管道上等待,直到数据可用,然后开始读取,直到管道关闭。
我尝试四处寻找某种方法,以便在管道中有数据时从管道中“读取”数据,但不确定如何绕过它。最后,为了尝试解决可能在阻塞管道上永远等待的问题,我让父级和子级同时并行运行,但这可能意味着读取过程可能会首先终止并且不会读入写入之前的所有数据都已完成。我将如何解决这个问题?
int p[2];
pipe(p);
// Child process - read
if (fork() == 0) {
fprintf(stderr, "Start child\r\n");
close(0);
dup(p[0]);
close(p[0]);
close(p[1]);
fprintf(stderr, "Child\r\n");
runcmd(pcmd->right);
// Parent process - write
} else {
fprintf(stderr, "Start parent\r\n");
close(1);
dup(p[1]);
close(p[0]);
close(p[1]);
fprintf(stderr, "Parent\r\n");
runcmd(pcmd->left);
}
编辑:我也尝试了该read
命令,但不确定如何实际使用它,因为它需要缓冲区,以及读取的预期大小(?)。当您不知道传入数据的大小时,我不确定如何检索其中任何一个。
答案1
管道很简单。如果你跳进泳池的最深处,你会觉得自己很难受。 (或者也许是你的老师没有更好地指导你的错。)
为了更熟悉管道,我建议您编写两个非常简单的程序:
一种仅将一些文本写入标准输出并退出的方法。它可以是简单的事情——“敏捷的棕色狐狸跳过了懒狗。”, “Lorem ipsum dolor sat amet,consectetur adipiscing elit,……”,一个短字符串(甚至可能是一个字符)重复多次 - 无论你想要什么。使用
printf
、write
、fprintf(stdout, …)
或您喜欢的任何其他函数。要测试该程序,只需从 shell 提示符运行它即可。它应该显示所选文本并退出(返回到 shell 提示符)。
一种只是从标准输入读取文本并将其写入标准输出。使用
getc
、gets
、read
或您喜欢的任何其他函数。当到达文件结尾时退出。检查您使用的任何函数的手册页,看看它如何指示文件结束。要测试该程序,请创建一个文本文件(名为
jon_file.txt
)并在其中放入一些文本。您可以通过说出类似的内容来快速完成此操作echo "Hello world" > jon_file.txt
,或者您也可以使用编辑器。然后输入prog2 < jon_file.txt
.它应该显示文件的内容并退出(返回到 shell 提示符)。
不要称呼pipe
, dup
, 或任何花哨的东西——甚至不要称呼open
或close
。 (请务必包含您想要的任何调试和/或审核代码,以确保您了解何时发生的情况。)然后运行prog1 | prog2
.如果你做得正确,你将得到你期望的输出。
现在尝试通过添加sleep
对程序的调用来“破坏”它。如果你打破了它,请告诉我你是怎么做到的。这几乎是不可能的——除非你让一个程序(或两个程序)休眠的时间超过你愿意坐下来等待的时间,否则你总是会prog2
输出所有写入的数据prog1
。
如果上面的示例没有说明清楚:让父进程和子进程(或者一般来说,管道两侧的进程)“同时”运行是正确的做法。1 读取程序不会因为管道中没有数据而“先终止”现在。正如您应该从上面的练习中了解到的,如果程序尝试从当前没有数据的管道中读取数据,read
系统调用将强制程序等待,直到数据到达。直到管道中没有数据时,读取程序才会终止
不再来了,曾经。2 (此时,read
将返回文件结尾。)“不再有数据到来”条件由写入程序关闭管道(或退出,这是等效的,因为exit
调用close
所有打开的文件描述符)来指示。
我不明白你为什么read
在这一点上为系统调用而烦恼——尽管,如果你还不知道如何使用它,这证实了我的怀疑,即你的老师正在以不符合逻辑顺序的方式呈现材料。 (我假设您指的是read
系统调用而不是read
命令。)您的程序有意义的唯一方法是如果runcmd(pcmd->right)
是通过某种方法从标准输入读取的内容(就像prog2
上面的程序一样)。看起来您的程序只是执行 shell 的功能 - 设置管道,然后让程序运行。在该级别上,您的程序(就您向我们展示的范围而言)没有理由执行任何 I/O(读或写)。
__________
1相关阅读:管道命令按什么顺序运行?
2当然,这过于简单化了。您很快就会了解到,如果您还没有这样做,您可以将读取程序设计为在管道中没有数据时终止现在——但这不是默认行为。或者,您可以将读取程序设计为在任意数量的其他条件下终止 - 例如,如果它q
从管道读取 a。或者它可能会被信号杀死,等等......
六个月后我回顾这个答案,我发现我确实没有解决整个问题;我讲了下半场,但没有讲上半场。所以,继续上面的内容,
修改第一个程序,写一个很多数据 — 至少 100,000 (10 5 ) 或 102400 (2 10 ×10 2 ) 个字符 — 发送到标准输出。另外,如果您还没有这样做,请修改它以将一些持续的状态信息写入 stderr。这可以是非常简单的事情;例如,一个“
.
每 1000(或 1024)个字符将 ” 发送到 stderr,!\n
完成后将“ ”发送到 stderr。要对此进行测试,请运行
prog1 > /dev/null
.如果您遵循我的建议(上面),您应该会看到 100 个点(.
),后面跟着!
一个换行符。如果您没有对sleep()
中的任何调用或其他耗时的函数进行操作prog1
,则此输出应该会相当快地出现。然后跑
prog1 | wc -c
。它应该显示您的 stderr 状态信息,如上所述,后跟100000
or102400
或您写入 stdout 的字节数。 (这将是 的输出wc -c
,报告从其标准输入(管道)读取了多少字节。)将第二个程序修改为
sleep
在开始读取之前 10 或 20 秒。要测试这一点,请
prog2 < jon_file.txt
再次运行。显然,它应该暂停您在 中指定的时间sleep()
,然后显示文件的内容并退出(返回到 shell 提示符)。
现在运行prog1 | prog2 > /dev/null
。但是,在这样做之前,您可能想尝试猜测会发生什么。
︙
︙
︙
我预计它会打印一些点——也许是 8 个,也许是 64 或 65,也许是其他数字——然后是暂停,然后是其余的点,以及!
.这是因为prog1
即使prog2
还没有阅读,也可以立即开始写作。管道可以保存数据,直到prog2
准备好开始读取为止——但仅限于一定程度。管道有缓冲限制。这可能是 8000(或 8192)、64000(或 65536)或其他数字。当管道满时,系统会强制prog1
等待。当prog2
开始读取时,它会排空管道;这为管道提供了容纳更多数据的空间,因此prog1
允许再次开始写入。
如果您一开始没有看到上述行为,请尝试增加数字:200,000 字节、30 秒等。
所以,当你的老师批评你的计划初稿时,他是对的。 (或者,也许他是完全正确的,而你错误地引用了他。)如你所知,该版本的程序等待程序runcmd(pcmd->left)
(管道编写器)完成,然后它会启动runcmd(pcmd->right)
(管道读取器)。但如果左边的程序输出 100,000 字节怎么办?它将填充管道,然后等待,直到它可以再写入一些。但在“某人”从管道中读取并耗尽存储缓冲区之前,它无法写入更多内容。但在管道写入器完成之前,主程序不会启动管道读取器。每个人都在等待其他人做某事,但只有在第一个人完成某事后他们才会做。 (“只要你给我钱,我就给你珠宝。”/“不,患病的之后给你钱你给我宝石。”)所以,是的;底线:如果数据因为管道已满并且没有进程从中读取而停止通过管道移动,那么两个进程都会无限挂起。