如何使在相同管道中读取和写入相同文件始终“失败”?


9

说我有以下脚本:

#!/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行(因此tmp始终首先准备对I / O重定向,因此始终破坏数据)?需要明确的是,我的意思是更改系统设置,而不是更改此脚本。

感谢您的想法。

Answers:


2

吉尔斯的回答解释了比赛情况。我将回答这一部分:

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

IDK(如果已经存在用于此目的的工具),但是我对如何实现这一想法有所了解。(但是请注意,这并不总是 0行,它只是一个有用的测试器,可以轻松地捕获像这样的简单种族,以及一些更复杂的种族。请参阅@Gilles的注释。) 这不能保证脚本是安全的,但是可能是测试中的有用工具,类似于在不同的CPU上测试多线程程序,包括弱排序的非x86 CPU(如ARM)。

您将其运行为 racechecker bash foo.sh

使用相同的系统调用追踪/拦截设施,strace -fltrace -f使用附加到每一个孩子的过程。(在Linux上,这是ptraceGDB和其他调试器用于设置断点,单步并修改另一个进程的内存/寄存器的系统调用。)

仪器openopenat系统调用:当此工具下运行的任何方法使得一个open(2)系统调用(或openat)用O_RDONLY,睡眠也许1/2或1秒。让其他open系统调用(尤其是包括在内的系统调用O_TRUNC)立即执行。

除非系统负载也很高,否则这将使作者能够在几乎所有比赛条件下赢得比赛,除非这是一个复杂的比赛条件,其中直到进行了一些其他阅读之后,截断才发生。因此,延迟s(可能是s或写入)的随机变化open()read()将增加此工具的检测能力,但是当然不用延时模拟器测试无限长的时间,该模拟器最终将涵盖您可能遇到的所有可能情况。在现实世界中,除非您仔细阅读脚本并证明事实并非如此,否则您无法确定自己的脚本是否存在种族问题。


您可能需要将它列入open文件的白名单(而不是delay )/usr/bin/usr/lib因此进程启动不会花很长时间。(运行时动态链接必须到open()多个文件(查看strace -eopen /bin/true/bin/ls某个时间),尽管如果父shell本身正在执行截断操作,那没关系。但是,此工具不要使脚本的运行速度过慢也将是一件好事。)

或者,也许将调用进程没有权限截断的所有文件列入白名单。也就是说,跟踪过程可以access(2)在实际挂起想要存储到open()文件的过程之前进行系统调用。


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

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


您关于比赛检查器的想法实际上是行不通的。首先,存在超时不可靠的问题:有一天,另一个人将花费比您预期更长的时间(这是构建或测试脚本的经典问题,似乎可以工作一段时间,然后以难以调试的方式失败当工作负载扩展并且许多事物并行运行时)。但除此之外,打开你要延迟增加?为了检测任何有趣的事情,您需要使用不同的延迟模式进行多次运行并比较它们的结果。
吉尔斯(Gillles)“所以-别再作恶了”

@吉尔斯:是的,任何合理的短暂延迟都不能保证截断会赢得比赛(如您所指出的,在重载的机器上)。这里的想法是您使用它多次测试脚本,而不是一直使用racechecker。也许您希望开放阅读时间可以配置,以使负载非常重的计算机上的人受益,他们希望将其设置得更高,例如10秒。或将其降低,像0.1秒长期或低效的脚本,重新打开的文件很多
彼得·科德斯

@吉尔斯:关于不同延迟模式的好主意,这可能会让您参加更多比赛,而不仅仅是像OP的情况那样“应该很明显(一旦您知道弹壳如何工作)”的简单的相同管道内的东西。但是“哪个打开?” 具有白名单或其他任何不延迟进程启动的只读打开方式。
彼得·科德斯

我猜您正在考虑使用后台作业进行更复杂的比赛,这些作业要等到其他流程完成后才能截断吗?是的,可能需要随机变化才能捕捉到这一点。或者,也许看一下过程树并延迟“尽早”阅读更多内容,以尝试颠倒通常的顺序。您可以使该工具变得越来越复杂,以模拟越来越多的重新排序可能性,但是在某些情况下,如果要执行多任务处理,则仍然必须正确设计程序。自动化测试对于可能问题受到更多限制的简单脚本可能很有用。
彼得·科德斯

这与测试多线程代码(尤其是无锁算法)非常相似:逻辑原因说明其正确性以及测试都是非常重要的,因为您不能依靠在任何特定机器上进行测试来产生可能重新排序的所有命令。如果您还没有解决所有漏洞,那就麻烦了。但是就像在ARM或PowerPC这样的弱顺序架构上进行测试就像在实践中是一个好主意一样,在人为延迟事情的系统下测试脚本可能会暴露一些竞争,所以总比没有好。您可以随时引入不会捕获的错误!
彼得·科德斯

18

为什么有比赛条件

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

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体系结构,外壳,内核,恰好调度进程的内核,这段时间CPU接收的中断等等。

如何改变行为

您不能通过更改系统设置来更改行为。计算机按照您的指示执行操作。您告诉它要截断tmptmp并行读取,因此它并行执行两项操作。

好的,您可以更改一个“系统设置”:您可以用/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

moreutils集合包括一个程序,做到了这一点,所谓的sponge

cat tmp | head -1 | sponge tmp

如何自动检测问题

如果您的目标是采用编写错误的脚本并自动找出它们在哪里中断,那么对不起,生活就不是那么简单。运行时分析无法可靠地发现问题,因为有时会cat在截断发生之前完成读取。静态分析原则上可以做到;问题中的简化示例被Shellcheck捕获,但是在更复杂的脚本中可能无法捕获类似的问题。


这是我的目标,以确定脚本是否编写正确。如果脚本可能以此方式破坏了数据,我只是希望它每次都破坏它们。听到这几乎是不可能的,这是不好的。多亏了您,我现在知道问题出在哪里,并且将尝试解决方案。
karlosss '17

@karlosss:嗯,我想知道您是否可以使用与strace(例如Linux ptrace)相同的系统调用跟踪/拦截内容来使所有open读取系统调用(在所有子进程中)休眠半秒,所以当与截断,截断几乎总是赢。
彼得·科德斯

@PeterCordes我是这个的新手,如果您可以设法实现这一目标并将其写为答案,我会接受的。
karlosss

@PeterCordes您不能保证截断会延迟赢得。它在大多数情况下都会起作用,但是偶尔在负载很重的计算机上,脚本会以某种神秘的方式失败。
吉尔斯(Gillles)“所以-别再作恶了”

@吉尔斯:让我们在我的回答下讨论一下。
彼得·科德斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.