如何让同一个管道中的同一个文件的读写总是“失败”?

如何让同一个管道中的同一个文件的读写总是“失败”?

假设我有以下脚本:

#!/bin/bash
for i in $(seq 1000)
do
    cp /etc/passwd tmp
    cat tmp | head -1 | head -1 | head -1 > tmp  #this is the key line
    cat tmp
done

在关键行上,我读取和写入同一个文件tmp,有时会失败。

(我读到这是因为竞争条件,因为管道中的进程是并行执行的,我不明白为什么 - 每个进程都head需要从前一个进程中获取数据,不是吗?这不是我的主要问题,但你也可以回答。)

当我运行该脚本时,它输出大约 200 行。有什么方法可以强制此脚本始终输出 0 行(因此 I/O 重定向tmp始终首先准备好,因此数据始终被破坏)?需要明确的是,我的意思是更改系统设置,而不是此脚本。

感谢您的想法。

答案1

为什么存在竞争条件

管道的两侧是并行执行的,而不是一个接一个。有一个非常简单的方法来演示这一点:运行

time sleep 1 | sleep 1

这需要一秒钟,而不是两秒钟。

shell 启动两个子进程并等待它们完成。这两个进程并行执行:其中一个进程与另一个进程同步的唯一原因是需要等待对方。最常见的同步点是右侧阻塞等待在其标准输入上读取数据,并在左侧写入更多数据时解除阻塞。相反的情况也可能发生,当右侧读取数据很慢并且左侧阻塞其写入操作,直到右侧读取更多数据(管道本身有一个缓冲区,由管道管理)内核,但它的最大尺寸较小)。

要观察同步点,请观察以下命令(sh -x在执行时打印每个命令):

time sh -x -c '{ sleep 1; echo a; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { sleep 1; cat; }'
time sh -x -c '{ sleep 2; echo a; } | { cat; sleep 1; }'

尝试各种变化,直到您对所观察到的结果感到满意为止。

给定复合命令

cat tmp | head -1 > tmp

左侧进程执行以下操作(我只列出了与我的解释相关的步骤):

  1. cat使用参数执行外部程序tmp
  2. 开放tmp阅读。
  3. 当它尚未到达文件末尾时,从文件中读取一个块并将其写入标准输出。

右侧流程执行以下操作:

  1. 将标准输出重定向到tmp,并截断进程中的文件。
  2. head使用参数执行外部程序-1
  3. 从标准输入读取一行并将其写入标准输出。

唯一的同步点是 right-3 等待 left-3 处理完一整行。 left-2 和 right-1 之间没有同步,因此它们可以按任意顺序发生。它们发生的顺序是不可预测的:它取决于 CPU 架构、shell、内核、进程恰好被调度在哪个内核上、CPU 在该时间接收到的中断等。

如何改变行为

您无法通过更改系统设置来更改行为。计算机会按照您的指示执行操作。您告诉它并行截断tmp和读取tmp,因此它并行执行这两件事。

好吧,有一个“系统设置”你可以改变:你可以/bin/bash用一个不是 bash 的不同程序来替换。我希望不用说这不是一个好主意。

如果您希望截断发生在管道左侧之前,则需要将其放在管道之外,例如:

{ cat tmp | head -1; } >tmp

或者

( exec >tmp; cat tmp | head -1 )

我不知道你为什么想要这个。从一个你知道是空的文件中读取有什么意义呢?

相反,如果您希望在完成读取后发生输出重定向(包括截断)cat,那么您需要将数据完全缓冲在内存中,例如

line=$(cat tmp | head -1)
printf %s "$line" >tmp

或写入不同的文件,然后将其移动到位。这通常是在脚本中执行操作的可靠方法,并且具有以下优点:文件在通过原始名称可见之前已被完整写入。

cat tmp | head -1 >new && mv new tmp

更多实用程序Collection 包含一个专门执行此操作的程序,称为sponge.

cat tmp | head -1 | sponge tmp

如何自动检测问题

如果您的目标是处理写得不好的脚本并自动找出它们的问题所在,那么抱歉,生活并没有那么简单。运行时分析无法可靠地找到问题,因为有时cat会在截断发生之前完成读取。静态分析原则上可以做到;你问题中的简化示例被捕获外壳检查,但它可能无法在更复杂的脚本中捕获类似的问题。

答案2

吉尔斯的回答解释了竞争条件。我只想回答这部分:

有什么方法可以强制此脚本始终输出 0 行(因此始终先准备好到 tmp 的 I/O 重定向,因此数据始终会被破坏)?明确地说,我的意思是更改系统设置

我不知道是否已经存在这样的工具,但我知道如何实现该工具。 (但请注意,这不会是总是0行,只是一个有用的测试器,可以轻松捕捉这样的简单比赛,并且一些更复杂的比赛。看@Gilles 的评论.) 它不能保证脚本是安全的,但在测试中可能是一个有用的工具,类似于在不同 CPU 上测试多线程程序,包括弱排序的非 x86 CPU,如 ARM。

你可以将其运行为racechecker bash foo.sh

strace -f使用与ltrace -f附加到每个子进程相同的系统调用跟踪/拦截工具。 (在 Linux 上,这是相同的ptraceGDB 和其他调试器使用的系统调用设置断点、单步执行以及修改另一个进程的内存/寄存器。)

检测openopenat系统调用:当在此工具下运行的任何进程进行系统open(2)调用(或openat) 与O_RDONLY,休眠大约 1/2 或 1 秒。让其他open系统调用(尤其是包括O_TRUNC)立即执行。

这应该允许编写者在几乎所有竞争条件下赢得比赛,除非系统负载也很高,或者这是一个复杂的竞争条件,直到其他读取之后才发生截断。所以open()s(可能是read()s 或写入)被延迟的随机变化会增加这个工具的检测能力,但是当然,如​​果没有使用延迟模拟器进行无限时间的测试,最终将覆盖您在现实世界中可能遇到的所有可能的情况,您就无法当然你的脚本没有竞争,除非你仔细阅读它们并证明它们没有。


您可能需要它将open文件列入白名单(而不是延迟)/usr/bin,因此/usr/lib进程启动不会永远持续下去。 (运行时动态链接必须到open()多个文件(查看strace -eopen /bin/true/bin/ls有时),尽管如果父 shell 本身正在执行截断,那也可以。但对于这个工具来说,不使脚本变得不合理地慢仍然是有好处的)。

或者可能将调用进程首先无权截断的每个文件列入白名单。即跟踪进程可以在实际挂起需要文件access(2)的进程之前进行系统调用。open()


racechecker本身必须用 C 语言编写,而不是用 shell 编写,但可以使用strace的代码作为起点,并且可能不需要太多工作来实现。

您也许可以获得相同的功能使用 FUSE 文件系统。可能有一个纯直通文件系统的 FUSE 示例,因此您可以向其中的open()函数添加检查,使其在只读打开时休眠,但让截断立即发生。

相关内容