如何使在同一個管道中讀取和寫入同一個文件總是“失敗”?
假設我有以下腳本:
#!/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/lib
process-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
左側過程執行以下操作(我只列出了與我的解釋相關的步驟):
cat
使用參數執行外部程序tmp
。- 打開
tmp
閱讀。- 雖然它還沒有到達文件末尾,但從文件中讀取一個塊並將其寫入標準輸出。
右手過程執行以下操作:
- 將標準輸出重定向到
tmp
,在過程中截斷文件。head
使用參數執行外部程序-1
。- 從標準輸入讀取一行並將其寫入標準輸出。
唯一的同步點是 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擷取,但在更複雜的腳本中可能無法擷取類似問題。