又一只海森虫超越了鳄鱼

又一只海森虫超越了鳄鱼

$> set -o pipefail

$> fortune | head -1 > /dev/null && echo "Повезло!" || echo "Вы проиграли"
Повезло!

$> fortune | head -1 > /dev/null && echo "Повезло!" || echo "Вы проиграли"
Вы проиграли

这是 fortune 条件程序无 exit(rand()).

你可以解释吗 这里出了什么问题?

抒情历史题外话

我第一次认识这个 Heisenbug 是在二十五年前。 然后,对于 FaxNET 中的网关,需要通过以下方式创建多个实用程序 管道 FreeBSD 下的“下棋”。 正如预期的那样,我认为自己是一名高级且经验丰富的程序员。 因此,我打算尽可能小心谨慎地做每件事,特别注意错误处理......

我之前处理 sendmail 和 uucp/uupc 中的错误的经验增加了我在“彻底的错误处理”方面的努力。 深入研究这个故事的细节是没有意义的,但我在这个 Heisenbug 上挣扎了两周 10-14 个小时。 于是,就记住了,昨天这位老熟人又来拜访了。

TL;DR 答案

效用 head может 关闭通道 fortune 立刻 当他读到第一行时。 如果 fortune 输出多一行,则对应调用 write() 将返回错误或报告输出的字节数少于请求的字节数。 反过来,用仔细的错误处理来编写 fortune 有权在其退出状态中反映这种情况。 然后由于安装 set -o pipefail 将工作 || echo "Вы проиграли".

但是, head 可能无法及时赶到 之前关闭 fortune 将完成数据输出。 然后就可以了 && echo "Повезло!".

在我的今天之一 GNUMakefile 有这样 一个片段:

echo '#define MDBX_BUILD_COMPILER "$(shell set -o pipefail; $(CC) --version | head -1 || echo 'Please use GCC or CLANG compatible compiler')"'

翻译成人类

这里常见的是 GNU 制造 и 打坏 以编译器的方式使用选项 --version 它询问他是谁,如果不支持该选项,则会插入一个存根 “请使用 GCC 或 CLANG 兼容编译器”.

样板 随处可见。 它很久以前就出现在这个地方,并且在任何地方都能完美运行(Linux、Solaris、OSX、FreeBSD、 WSL ETC。)。 但昨天在 替代Linux 在平台上 厄尔布鲁士峰 2000 (E2K) 我注意到:

#define MDBX_BUILD_COMPILER "lcc:1.23.20:Sep--4-2019:e2k-v3-linux Please use GCC or CLANG compatible compiler"

坦白说,我并没有立即认出我的老“熟人”。 此外,该项目已经在 Elbrus 和许多不同的发行版(包括 Alt)下进行了多次测试。 具有各种编译器、GNU Make 和 bash 版本。 因此,我不想在这里看到我的错误。

当试图重现问题和/或了解发生了什么时,更多奇怪的事情开始发生。
命令行拼写:

echo "#define MDBX_BUILD_COMPILER '$(set -o pipefail; LC_ALL=C cc --version | head -1 || echo "Please use GCC or CLANG compatible compiler")'"

它时不时地会产生额外的文本,然后就不会了……通常其中一个选项会保留相当长的时间,但如果你戳得更久,你总是会得到两个!

当然, strace 我们的一切! 在输入了 strace 长篇大论,但没有时间按 Enter 键时,我认出了我的老朋友 Heisenbug 先生和开发人员 编译器 25年前的我自己, 怀旧…… 我决定悲伤地写下这张纸条😉

顺便说一句,就像任何有自尊心的人一样 海森巴格strace 宁愿不复制。

发生什么了?

  • 效用 head 一旦读取了请求的行数,就有权利(或者更确切地说,甚至被迫)关闭正在读取的通道。
  • 数据生成程序编写者(在本例中 cc) может 打印多行并 自由的 通过多次调用来做到这一点 write().
  • 如果 在写入端记录结束之前,读取端有时间关闭自己端的通道,然后写入端会收到错误。
  • 作家计划 有权利 两者都会忽略通道写入错误并将其反映在完成代码中。
  • 由于安装原因 set -o pipefail 如果至少一个元素的结果非零,则管道完成代码将非零(错误),然后它将起作用 || echo "Please use GCC or CLANG compatible compiler".

根据编写程序如何处理信号,可能会有所不同。 例如,程序可能异常终止(自动生成非零/错误终止状态),或者 write() 将返回写入的字节数少于请求和设置的字节数的结果 errno = EPIPE.

是谁的错?

在所描述的情况下 一切都一点点。 错误处理在 cc (lcc:1.23.20:Sep—4-2019:e2k-v3-linux) 不是 多余的。 在许多情况下,最好还是谨慎行事,尽管这确实会暴露出为传统行为设计的样板文件中突然存在的缺陷。

怎么办?

错误:

fortune | head -1 && echo "Повезло, но вы рискуете!" || echo "WTF?"

更正:

  1. (fortune && echo "Успешно" || echo "Ошибка") | head -1

    在这里,在服务嵌套管道(括号“内”)时,命令解释器将处理管道的提前关闭。 因此,如果 fortune 会在status中报写入关闭通道错误,然后输出 || echo "Ошибка" 它不会到达任何地方,因为通道已经关闭。

  2. fortune | cat - | head -1 && echo "Успешно" || echo "Ошибка"

    这是实用程序 cat 充当阻尼器,因为它忽略了误差 EPIPE 撤回后。 现在就可以得出结论了 fortune 小(几行)并且适合通道缓冲区(从 512 字节到 ≈64K,在大多数操作系统中≥4K)。 否则问题可能会再次出现。

如何正确处理 EPIPE 以及其他录音错误?

没有单一正确的解决方案,但有一些简单的建议:

  • EPIPE 需要 必须处理 (并反映在退出状态中)当输出需要完整性的数据时。 例如,在归档程序或备份实用程序的操作期间。
  • EPIPE 最好忽略 显示信息和辅助消息时。 例如,当显示选项信息时 --help или --version.
  • 如果正在开发的代码之前可以在管道中使用 | head,然后 EPIPE 最好忽略,否则最好处理并反映在退出状态中。

我想借此机会向团队表示感谢 MCST и 替代Linux 进行富有成效的工作。 你的决心真是惊人!
继续努力 同志们,加油 秋季会议!

谢谢 贝雷兹 用于纠正拼写错误和错误。
KDPV 来自 乔治·A.

来源: habr.com

添加评论