Bash

yes 是如何快速寫入文件的?

  • November 2, 2021

讓我舉個例子吧:

$ timeout 1 yes "GNU" > file1
$ wc -l file1
11504640 file1
$ for ((sec0=`date +%S`;sec<=$(($sec0+5));sec=`date +%S`)); do echo "GNU" >> file2; done
$ wc -l file2
1953 file2

在這裡您可以看到該命令在一秒鐘內yes寫入11504640行,而我只能1953在 5 秒內使用 bashforecho.

正如評論中所建議的,有各種技巧可以提高效率,但沒有一個能接近匹配的速度yes

$ ( while :; do echo "GNU" >> file3; done) & pid=$! ; sleep 1 ; kill $pid
[1] 3054
$ wc -l file3
19596 file3
$ timeout 1 bash -c 'while true; do echo "GNU" >> file4; done'
$ wc -l file4
18912 file4

這些可以在一秒鐘內寫入多達 20,000 行。它們可以進一步改進為:

$ timeout 1 bash -c 'while true; do echo "GNU"; done >> file5' 
$ wc -l file5
34517 file5
$ ( while :; do echo "GNU"; done >> file6 ) & pid=$! ; sleep 1 ; kill $pid
[1] 5690
$ wc -l file6
40961 file6

這些可以讓我們在一秒鐘內完成多達 40,000 行。yes更好,但與每秒可寫約 1100 萬行的情況相去甚遠!

那麼,如何yes快速寫入文件呢?

簡而言之:

yes表現出與大多數其他標準實用程序類似的行為,這些實用程序通常寫入FILE STREAM,輸出由 libC 通過stdio. 這些僅write()每隔 4kb (16kb 或 64kb)或任何輸出塊執行一次系統呼叫BUFSIZecho是一個write()per GNU。這是很多模式切換 (顯然,它不像context-switch那樣昂貴)

更不用說,除了它的初始優化循環之外,它yes是一個非常簡單、微小、編譯過的 C 循環,而且你的 shell 循環與編譯器優化的程序完全不同。


但是我錯了:

當我之前說yesusedstdio時,我只是假設它確實如此,因為它的行為與那些行為非常相似。這是不正確的——它只是以這種方式模仿他們的行為。它實際上所做的非常類似於我在下面對 shell 所做的事情:它首先循環以合併其參數*(或者y如果沒有)*,直到它們可能不再增長而不超過BUFSIZ.

緊接相關循環之前來自的評論指出:for

/* Buffer data locally once, rather than having the
large overhead of stdio buffering each item.  */

yes``write()此後做自己的。


題外話:

(最初包含在問題中,並保留在此處已寫的可能資訊豐富的解釋的上下文中)

我試過timeout 1 $(while true; do echo "GNU">>file2; done;)但無法停止循環。

您在命令替換方面遇到的timeout問題 - 我想我現在明白了,並且可以解釋為什麼它不會停止。timeout不會啟動,因為它的命令行永遠不會執行。您的 shell 派生出一個子 shell,在其標準輸出上打開一個管道並讀取它。當孩子退出時它將停止閱讀,然後它將解釋所有孩子為$IFSmangling 和 glob 擴展而編寫的內容,並根據結果替換從$(匹配的所有內容)

但是,如果孩子是一個永遠不會寫入管道的無限循環,那麼孩子永遠不會停止循環,並且timeout’ 的命令行永遠不會在之前完成*(我猜)*你這樣做Ctrl+C並殺死孩子循環。所以timeout永遠 不能殺死需要在它開始之前完成的循環。


其他timeout

… 與您的性能問題的相關性與您的 shell 程序必須花費在使用者模式和核心模式之間切換以處理輸出的時間量無關。timeout但是,它不像 shell 那樣靈活:shell 擅長處理參數和管理其他程序的能力。

正如其他地方所指出的那樣,簡單地將*[fd-num] >> named_file*重定向移動到循環的輸出目標,而不是僅僅將輸出定向到循環的命令可以顯著提高性能,因為這樣至少open()系統呼叫只需要執行一次。這也是在下面使用|管道作為內部循環的輸出來完成的。


直接比較:

你可能會喜歡:

for cmd in  exec\ yes 'while echo y; do :; done'
do      set +m
       sh  -c '{ sleep 1; kill "$$"; }&'"$cmd" | wc -l
       set -m
done
256659456
505401

有點像之前描述的命令子關係,但是沒有管道,並且子程序是後台的,直到它殺死父程序。在這種yes情況下,自從子程序生成後,父程序實際上已經被替換,但是 shellyes通過用新程序覆蓋自己的程序來呼叫,因此 PID 保持不變,它的殭屍子程序仍然知道要殺死誰。


更大的緩衝區:

現在讓我們看看增加 shell 的write()緩衝區。

IFS="
";    set y ""              ### sets up the macro expansion       
until [ "${512+1}" ]        ### gather at least 512 args
do    set "$@$@";done       ### exponentially expands "$@"
printf %s "$*"| wc -c       ### 1 write of 512 concatenated "y\n"'s  
1024

我選擇了這個數字,因為長度超過 1kb 的輸出字元串對我來說會被分成單獨write()的 ‘s。所以這裡又是循環:

for cmd in 'exec  yes' \
          'until [ "${512+:}" ]; do set "$@$@"; done
           while printf %s "$*"; do :; done'
do      set +m
       sh  -c $'IFS="\n"; { sleep 1; kill "$$"; }&'"$cmd" shyes y ""| wc -l
       set -m
done
268627968
15850496

這是本次測試在相同時間內寫入的數據量的 300 倍,是上次測試的 300 倍。不是太寒酸。但事實並非如此yes


感覺:

根據要求,在此連結上對此處所做的操作進行了比僅程式碼註釋更全面的描述。

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