Bash

如何使在同一個管道中讀取和寫入同一個文件總是“失敗”?

  • December 9, 2017

假設我有以下腳本:

#!/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 重定向,因此始終銷毀數據)?需要明確的是,我的意思是更改系統設置,而不是這個腳本。

謝謝你的想法。

吉爾斯的回答解釋了比賽條件。我只回答這部分:

有什麼辦法可以強制此腳本始終輸出 0 行(因此始終首先準備到 tmp 的 I/O 重定向,因此數據始終被破壞)?明確地說,我的意思是更改系統設置

IDK 如果已經存在用於此的工具,但我對如何實現它有一個想法。(但請注意,這並不總是0 行,它只是一個有用的測試器,可以輕鬆擷取像這樣的簡單比賽和一些更複雜的比賽。請參閱@Gilles 的評論。) 它不能保證腳本是安全的,但可能是一個有用的測試工具,類似於在不同的 CPU 上測試多執行緒程序,包括弱排序的非 x86 CPU,如 ARM。

你會執行它racechecker bash foo.sh

strace -f使用與ltrace -f附加到每個子程序相同的系統呼叫跟踪/攔截工具。(在 Linux 上,這與GDB 和其他調試器用來設置斷點、單步執行和修改另一個程序的記憶體/寄存器的ptrace系統呼叫相同。)

open檢測和openat系統呼叫:當在此工具下執行的任何程序使用 進行系統呼叫open(2)openat)時O_RDONLY,睡眠時間可能為 1/2 或 1 秒。讓其他open系統呼叫(尤其是包括 的系統呼叫O_TRUNC)毫不延遲地執行。

這應該允許作者在幾乎所有競爭條件下贏得比賽,除非系統負載也很高,或者這是一個複雜的競爭條件,直到在其他一些讀取之後才發生截斷。因此,延遲哪些**s(可能還有s 或寫入)的隨機變化open()``read()**會增加此工具的檢測能力,但當然無需使用延遲模擬器進行無限時間測試,最終將涵蓋您可能遇到的所有可能情況在現實世界中,除非您仔細閱讀並證明它們不是,否則您無法確定您的腳本沒有種族。


您可能需要將其列入白名單(而不是延遲open),/usr/bin因此/usr/libprocess-startup 不會永遠持續下去。(執行時動態連結必須到open()多個文件(查看strace -eopen /bin/true/bin/ls有時),儘管如果父 shell 本身正在執行截斷,那沒關係。但對於這個工具來說,不要讓腳本變得不合理地變慢仍然是件好事)。

或者也許將呼叫程序首先無權截斷的每個文件都列入白名單。即跟踪程序可以access(2)在實際掛起想要open()文件的程序之前進行系統呼叫。


racechecker本身必須用 C 編寫,而不是 shell,但可以使用strace’s 程式碼作為起點,並且可能不需要太多工作來實現。

您也許可以使用 FUSE 文件系統獲得相同的功能。可能有一個純直通文件系統的 FUSE 範例,因此您可以在open()函式中添加檢查,使其在只讀打開時休眠,但讓截斷立即發生。

為什麼會有競態條件

管道的兩側是並行執行的,而不是一個接一個。有一個非常簡單的方法來證明這一點:執行

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

moreutils集合包括一個程序,稱為sponge.

cat tmp | head -1 | sponge tmp

如何自動檢測問題

如果您的目標是編寫糟糕的腳本並自動找出它們在哪里中斷,那麼抱歉,生活沒有那麼簡單。執行時分析無法可靠地找到問題,因為有時會cat在截斷發生之前完成讀取。靜態分析原則上可以做到;您問題中的簡化範例已被Shellcheck擷取,但在更複雜的腳本中可能無法擷取類似問題。

引用自:https://unix.stackexchange.com/questions/409893