Bash

您如何在各種 shell 中使用命令 coproc?

  • July 24, 2021

有人可以提供幾個關於如何使用的例子coproc嗎?

協同程序是一個ksh特性(已經在 中ksh88)。zsh從一開始(90 年代初)就具有該功能,而它只是bash4.0(2009 年)才被添加。

但是,這 3 個 shell 之間的行為和界面存在顯著差異。

不過,這個想法是一樣的:它允許在後台啟動一項工作,並能夠發送輸入並讀取其輸出,而無需求助於命名管道。

這是通過在某些系統上使用帶有最新版本的 ksh93 的大多數 shell 和套接字對的未命名管道來完成的。

a | cmd | b中,a將數據饋送到cmdb讀取其輸出。cmd作為協同程序執行允許 shell 既是a又是b.

ksh 協同程序

ksh中,您啟動一個協同程序:

cmd |&

您可以cmd通過執行以下操作來提供數據:

echo test >&p

或者

print -p test

並使用以下內容讀取cmd輸出:

read var <&p

或者

read -p var

cmd作為任何後台作業啟動,您可以在其上使用fgbg、並通過或通過kill引用它。%job-number``$!

cmd要關閉正在讀取的管道的寫入端,您可以執行以下操作:

exec 3>&p 3>&-

並關閉另一個管道的讀取端(一個cmd正在寫入):

exec 3<&p 3<&-

除非您首先將管道文件描述符保存到其他一些 fd,否則您無法啟動第二個協同程序。例如:

tr a b |&
exec 3>&p 4<&p
tr b c |&
echo aaa >&3
echo bbb >&p

zsh 協同程序

zsh中,協同過程與 中的幾乎相同ksh。唯一真正的區別是zsh協同程序以coproc關鍵字開始。

coproc cmd
echo test >&p
read var <&p
print -p test
read -p var

正在做:

exec 3>&p

注意:這不會將coproc文件描述符移動到 fd 3(如 in ksh),而是複制它。因此,沒有明確的方法可以關閉饋送或閱讀管道,其他方式啟動另一個 coproc.

例如,關閉進料端:

coproc tr a b
echo aaaa >&p # send some data

exec 4<&p     # preserve the reading end on fd 4
coproc :      # start a new short-lived coproc (runs the null command)

cat <&4       # read the output of the first coproc

除了基於管道的協同程序之外,zsh(自 3.1.6-dev19,於 2000 年發布)具有基於偽 tty 的構造,例如expect. 要與大多數程序互動,ksh 樣式的協同程序將不起作用,因為程序在其輸出為管道時開始緩衝。

這裡有些例子。

啟動協同程序x

zmodload zsh/zpty
zpty x cmd

(這裡,cmd是一個簡單的命令。但是你可以用evalor 函式做一些更花哨的事情。)

饋送協同處理數據:

zpty -w x some data

讀取協同處理數據(在最簡單的情況下):

zpty -r x var

就像expect,它可以等待來自與給定模式匹配的協同程序的一些輸出。

bash 協同程序

bash 語法要更新很多,並且建立在最近添加到 ksh93、bash 和 zsh 的新功能之上,該功能提供了一種語法來允許處理大於 10 的動態分配的文件描述符。

bash提供基本 coproc語法和擴展語法。

基本語法

啟動協同程序的基本語法如下所示zsh

coproc cmd

kshorzsh中,進出協同程序的管道使用 and>&p訪問<&p

但是在 中bash,從協程序和另一個管道到協程序的管道的文件描述符在$COPROC數組中返回(分別${COPROC[0]}${COPROC[1]}。所以…

將數據饋送到協同流程:

echo xxx >&"${COPROC[1]}"

從協程中讀取數據:

read var <&"${COPROC[0]}"

使用基本語法,您一次只能啟動一個協同程序。

擴展語法

在擴展語法中,您可以命名您的協同程序(如在zshzpty co-proccesses 中):

coproc mycoproc { cmd; }

該命令必須是複合命令。(注意上面的例子是如何讓人聯想到的function f { ...; }。)

這一次,文件描述符位於${mycoproc[0]}和中${mycoproc[1]}

您一次可以啟動多個協同程序——但是當您啟動一個協同程序而其中一個仍在執行(即使在非互動模式下)時,您確實會收到警告。

使用擴展語法時,您可以關閉文件描述符。

coproc tr { tr a b; }
echo aaa >&"${tr[1]}"

exec {tr[1]}>&-

cat <&"${tr[0]}"

請注意,以這種方式關閉在 4.3 之前的 bash 版本中不起作用,您必須改為編寫它:

fd=${tr[1]}
exec {fd}>&-

ksh和中zsh,這些管道文件描述符被標記為 close-on-exec。

但在 中bash,將這些傳遞給已執行命令的唯一方法是將它們複製到 fds 012. 這限制了您可以與單個命令互動的協同程序的數量。(請參見下面的範例。)

yash 程序和管道重定向

yash本身沒有協同處理功能,但可以通過其管道流程重定向功能實現相同的概念。yash有一個pipe()系統呼叫的介面,所以這種事情可以相對容易地在那里手工完成。

您將開始一個協同程序:

exec 5>>|4 3>(cmd >&5 4<&- 5>&-) 5>&-

它首先創建一個pipe(4,5)(5 寫入端,4 讀取端),然後將 fd 3 重定向到一個管道到一個程序,該程序在另一端使用其 stdin 執行,而 stdout 進入之前創建的管道。然後我們關閉我們不需要的父管道的寫入端。所以現在在 shell 中,我們有 fd 3 連接到 cmd 的標準輸入,而 fd 4 用管道連接到 cmd 的標準輸出。

請注意,這些文件描述符上未設置 close-on-exec 標誌。

提供數據:

echo data >&3 4<&-

讀取數據:

read var <&4 3>&-

你可以像往常一樣關閉 fds:

exec 3>&- 4<&-

現在,為什麼它們不那麼受歡迎

使用命名管道幾乎沒有任何好處

使用標準命名管道可以輕鬆實現協同程序。我不知道何時引入了確切的命名管道,但可能是在ksh提出協同程序之後(可能是在 80 年代中期,ksh88 是在 88 年“發布”的,但我相信ksh幾年前在 AT&T 內部使用過那)這將解釋為什麼。

cmd |&
echo data >&p
read var <&p

可以寫成:

mkfifo in out

cmd <in >out &
exec 3> in 4< out
echo data >&3
read var <&4

與這些互動更加直接——尤其是當您需要執行多個協同程序時。(見下面的例子。)

使用的唯一好處coproc是您不必在使用後清理那些命名管道。

容易死鎖

Shell 在一些結構中使用管道:

  • 殼管: cmd1 | cmd2 ,
  • 命令替換: $(cmd) ,
  • 過程替換: <(cmd) , >(cmd).

其中,數據在不同程序之間僅沿一個方向流動。

但是,使用協同程序和命名管道,很容易陷入死鎖。您必須跟踪哪個命令打開了哪個文件描述符,以防止一個保持打開狀態並使程序保持活動狀態。死鎖可能很難調查,因為它們可能是非確定性的;例如,僅當發送與填滿一個管道一樣多的數據時。

expect它的設計用途更糟糕

協同程序的主要目的是為 shell 提供一種與命令互動的方式。但是,它的效果並不好。

上面提到的最簡單的死鎖形式是:

tr a b |&
echo a >&p
read var<&p

因為它的輸出不去終端,所以tr緩衝它的輸出。所以它不會輸出任何東西,直到它看到它的文件結束stdin,或者它已經積累了一個充滿數據的緩衝區來輸出。所以上面,在 shell 輸出後 a\n(只有 2 個字節),read將無限期地阻塞,因為tr正在等待 shell 向它發送更多數據。

簡而言之,管道不適合與命令互動。協同程序只能用於與不緩衝其輸出的命令可以被告知不緩衝其輸出的命令互動;例如,通過stdbuf在最近的 GNU 或 FreeBSD 系統上使用一些命令。

這就是為什麼expectzpty使用偽終端來代替。expect是一個為與命令互動而設計的工具,它做得很好。

文件描述符處理很繁瑣,很難正確處理

協同過程可以用來做一些比簡單的殼管允許的更複雜的管道。

其他 Unix.SE 答案有一個 coproc 用法的範例。

**這是一個簡化的範例:**假設您想要一個函式,它將命令輸出的副本提供給其他 3 個命令,然後將這 3 個命令的輸出連接起來。

全部使用管道。

例如:將輸出提供printf '%s\n' foo bartr a bsed 's/./&&/g'cut -b2-以獲得類似:

foo
bbr
ffoooo
bbaarr
oo
ar

首先,它不一定很明顯,但那裡有死鎖的可能性,它會在只有幾千字節的數據後開始發生。

然後,根據您的 shell,您將遇到許多必須以不同方式解決的不同問題。

例如,使用zsh,您可以使用:

f() (
 coproc tr a b
 exec {o1}<&p {i1}>&p
 coproc sed 's/./&&/g' {i1}>&- {o1}<&-
 exec {o2}<&p {i2}>&p
 coproc cut -c2- {i1}>&- {o1}<&- {i2}>&- {o2}<&-
 tee /dev/fd/$i1 /dev/fd/$i2 >&p {o1}<&- {o2}<&- &
 exec cat /dev/fd/$o1 /dev/fd/$o2 - <&p {i1}>&- {i2}>&-
)
printf '%s\n' foo bar | f

上面,協同程序 fd 設置了 close-on-exec 標誌,但沒有從它們複製的標誌(如 中{o1}<&p)。因此,為避免死鎖,您必須確保它們在任何不需要它們的程序中都已關閉。

同樣,我們必須使用一個子shell並exec cat最終使用,以確保沒有shell程序在保持管道打開。

使用ksh(這裡ksh93),那必須是:

f() (
 tr a b |&
 exec {o1}<&p {i1}>&p
 sed 's/./&&/g' |&
 exec {o2}<&p {i2}>&p
 cut -c2- |&
 exec {o3}<&p {i3}>&p
 eval 'tee "/dev/fd/$i1" "/dev/fd/$i2"' >&"$i3" {i1}>&"$i1" {i2}>&"$i2" &
 eval 'exec cat "/dev/fd/$o1" "/dev/fd/$o2" -' <&"$o3" {o1}<&"$o1" {o2}<&"$o2"
)
printf '%s\n' foo bar | f

注意:ksh這在使用socketpairs而不是 的系統上不起作用pipes,並且/dev/fd/n在 Linux 上像這樣工作。)

ksh中,上面的 fds2標有​​ close-on-exec 標誌,除非它們在命令行上顯式傳遞。這就是為什麼我們不必像 with 那樣關閉未使用的文件描述符的原因——但這也是為什麼我們必須為 , 的新值zsh{i1}>&$i1和使用,以傳遞給和……eval``$i1``tee``cat

bash此無法完成,因為您無法避免 close-on-exec 標誌。

上面,還是比較簡單的,因為我們只使用了簡單的外部命令。當您想在其中使用 shell 構造時,它變得更加複雜,並且您開始遇到 shell 錯誤。

使用命名管道將上述內容與相同的內容進行比較:

f() {
 mkfifo p{i,o}{1,2,3}
 tr a b < pi1 > po1 &
 sed 's/./&&/g' < pi2 > po2 &
 cut -c2- < pi3 > po3 &

 tee pi{1,2} > pi3 &
 cat po{1,2,3}
 rm -f p{i,o}{1,2,3}
}
printf '%s\n' foo bar | f

結論

如果要與命令互動,請使用expect、 或zsh’szpty或命名管道。

如果你想用管道做一些花哨的管道,使用命名管道。

協同程序可以完成上述一些工作,但要準備好為任何不重要的事情做一些認真的撓頭。

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