Câu trả lời:
đồng xử lý là một ksh
tính năng (đã có ksh88
). zsh
đã có tính năng này từ đầu (đầu những năm 90), trong khi nó chỉ mới được thêm bash
vào 4.0
(2009).
Tuy nhiên, hành vi và giao diện khác nhau đáng kể giữa 3 shell.
Mặc dù vậy, ý tưởng là như nhau: nó cho phép bắt đầu một công việc ở chế độ nền và có thể gửi đầu vào và đọc đầu ra của nó mà không cần phải dùng đến các đường ống có tên.
Điều đó được thực hiện với các ống không tên với hầu hết các vỏ và ổ cắm với các phiên bản gần đây của ksh93 trên một số hệ thống.
Trong a | cmd | b
, a
cung cấp dữ liệu đến cmd
và b
đọc đầu ra của nó. Chạy cmd
như một đồng xử lý cho phép vỏ được cả hai a
và b
.
Trong ksh
, bạn bắt đầu một bộ đồng xử lý như:
cmd |&
Bạn cung cấp dữ liệu cmd
bằng cách thực hiện những việc như:
echo test >&p
hoặc là
print -p test
Và đọc cmd
kết quả đầu ra với những thứ như:
read var <&p
hoặc là
read -p var
cmd
được bắt đầu như bất kỳ công việc nền, Bạn có thể sử dụng fg
, bg
, kill
vào nó và tham khảo nó bằng %job-number
hoặc thông qua $!
.
Để đóng đầu viết của ống cmd
đang đọc, bạn có thể làm:
exec 3>&p 3>&-
Và để đóng đầu đọc của ống khác (người cmd
đang viết):
exec 3<&p 3<&-
Bạn không thể bắt đầu quá trình hợp tác thứ hai trừ khi trước tiên bạn lưu bộ mô tả tệp đường ống vào một số fds khác. Ví dụ:
tr a b |&
exec 3>&p 4<&p
tr b c |&
echo aaa >&3
echo bbb >&p
Trong zsh
, đồng xử lý gần giống với những người trong ksh
. Sự khác biệt thực sự duy nhất là các zsh
đồng xử lý được bắt đầu với coproc
từ khóa.
coproc cmd
echo test >&p
read var <&p
print -p test
read -p var
Đang làm:
exec 3>&p
Lưu ý: Điều này không di chuyển bộ coproc
mô tả tệp sang fd 3
(như trong ksh
), nhưng sao chép nó. Vì vậy, không có cách rõ ràng để đóng ống cấp liệu hoặc đọc, bắt đầu khác coproc
.
Ví dụ, để đóng kết thúc cho ăn:
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
Ngoài các đồng xử lý dựa trên đường ống, zsh
(kể từ 3.1.6-dev19, được phát hành năm 2000) có các cấu trúc dựa trên giả như thế nào expect
. Để tương tác với hầu hết các chương trình, các đồng xử lý kiểu ksh sẽ không hoạt động, vì các chương trình bắt đầu đệm khi đầu ra của chúng là một đường ống.
Dưới đây là một số ví dụ.
Bắt đầu quá trình hợp tác x
:
zmodload zsh/zpty
zpty x cmd
(Ở đây, cmd
là một lệnh đơn giản. Nhưng bạn có thể thực hiện những điều lạ hơn với eval
hoặc các chức năng.)
Cung cấp dữ liệu đồng xử lý:
zpty -w x some data
Đọc dữ liệu đồng xử lý (trong trường hợp đơn giản nhất):
zpty -r x var
Giống như expect
, nó có thể chờ một số đầu ra từ đồng xử lý khớp với một mẫu nhất định.
Cú pháp bash mới hơn rất nhiều và được xây dựng dựa trên một tính năng mới được thêm gần đây vào ksh93, bash và zsh. Nó cung cấp một cú pháp để cho phép xử lý các mô tả tệp được phân bổ động trên 10.
bash
cung cấp một cú pháp cơ bản coproc
và một cú pháp mở rộng .
Cú pháp cơ bản để bắt đầu một quá trình hợp tác trông giống như zsh
:
coproc cmd
Trong ksh
hoặc zsh
, các đường ống đến và từ quá trình đồng được truy cập với >&p
và <&p
.
Nhưng trong đó bash
, các mô tả tập tin của đường ống từ quá trình đồng xử lý và đường ống khác đến đồng xử lý được trả về trong $COPROC
mảng (tương ứng ${COPROC[0]}
và ${COPROC[1]}
.
Cung cấp dữ liệu cho quá trình hợp tác:
echo xxx >&"${COPROC[1]}"
Đọc dữ liệu từ quá trình hợp tác:
read var <&"${COPROC[0]}"
Với cú pháp cơ bản, bạn chỉ có thể bắt đầu một quá trình đồng xử lý tại thời điểm đó.
Trong cú pháp mở rộng, bạn có thể đặt tên cho các đồng xử lý của mình (như trong các zsh
đồng xử lý zpty):
coproc mycoproc { cmd; }
Lệnh có được một lệnh ghép. (Lưu ý cách ví dụ trên gợi nhớ đến function f { ...; }
.)
Lần này, các mô tả tập tin là trong ${mycoproc[0]}
và ${mycoproc[1]}
.
Bạn có thể bắt đầu nhiều hơn một quá trình đồng thời tại một thời điểm, nhưng bạn sẽ nhận được cảnh báo khi bạn bắt đầu một quá trình đồng xử lý trong khi một quá trình vẫn đang chạy (ngay cả trong chế độ không tương tác).
Bạn có thể đóng bộ mô tả tệp khi sử dụng cú pháp mở rộng.
coproc tr { tr a b; }
echo aaa >&"${tr[1]}"
exec {tr[1]}>&-
cat <&"${tr[0]}"
Lưu ý rằng việc đóng theo cách đó không hoạt động trong các phiên bản bash trước 4.3 mà bạn phải viết nó thay thế:
fd=${tr[1]}
exec {fd}>&-
Như trong ksh
và zsh
, những mô tả tập tin đường ống được đánh dấu là close-on-exec.
Nhưng trong bash
, cách duy nhất để vượt qua những để thực hiện các lệnh là để lặp lại chúng để FDS 0
, 1
hoặc 2
. Điều đó giới hạn số lượng đồng xử lý mà bạn có thể tương tác với một lệnh duy nhất. (Xem ví dụ bên dưới.)
yash
không có tính năng đồng xử lý mỗi se, nhưng khái niệm tương tự có thể được thực hiện với các tính năng chuyển hướng đường ống và quy trình của nó . yash
có một giao diện cho pipe()
cuộc gọi hệ thống, vì vậy loại điều này có thể được thực hiện tương đối dễ dàng bằng tay ở đó.
Bạn sẽ bắt đầu hợp tác với:
exec 5>>|4 3>(cmd >&5 4<&- 5>&-) 5>&-
Đầu tiên tạo ra một pipe(4,5)
(5 đầu viết, 4 đầu đọc), sau đó chuyển hướng fd 3 sang một đường ống đến một quá trình chạy với stdin của nó ở đầu kia và stdout đi đến đường ống được tạo trước đó. Sau đó, chúng tôi đóng đầu viết của ống đó trong phần cha mẹ mà chúng tôi sẽ không cần. Vì vậy, bây giờ trong shell chúng ta có fd 3 được kết nối với stdin của cmd và fd 4 được kết nối với thiết bị xuất chuẩn của cmd với các đường ống.
Lưu ý rằng cờ close-on-exec không được đặt trên các mô tả tệp đó.
Để cung cấp dữ liệu:
echo data >&3 4<&-
Để đọc dữ liệu:
read var <&4 3>&-
Và bạn có thể đóng fds như bình thường:
exec 3>&- 4<&-
Đồng quy trình có thể dễ dàng được thực hiện với các ống có tên tiêu chuẩn. Tôi không biết khi nào các ống được đặt tên chính xác được giới thiệu nhưng có thể sau khi ksh
đã đưa ra các quy trình hợp tác (có lẽ vào giữa những năm 80, ksh88 đã được "phát hành" vào năm 88, nhưng tôi tin rằng ksh
đã được sử dụng nội bộ tại AT & T vài năm trước đó) sẽ giải thích tại sao.
cmd |&
echo data >&p
read var <&p
Có thể được viết với:
mkfifo in out
cmd <in >out &
exec 3> in 4< out
echo data >&3
read var <&4
Tương tác với những thứ đó đơn giản hơn, đặc biệt là nếu bạn cần chạy nhiều hơn một quy trình. (Xem ví dụ dưới đây.)
Lợi ích duy nhất của việc sử dụng coproc
là bạn không phải dọn sạch những ống có tên sau khi sử dụng.
Vỏ sử dụng đường ống trong một vài cấu trúc:
cmd1 | cmd2
,$(cmd)
,<(cmd)
, >(cmd)
.Trong đó, dữ liệu chỉ chảy theo một hướng giữa các quy trình khác nhau.
Tuy nhiên, với các đồng xử lý và các đường ống được đặt tên, thật dễ dàng để đi vào bế tắc. Bạn phải theo dõi lệnh nào có mô tả tệp nào mở, để ngăn chặn một lệnh mở và giữ một tiến trình còn sống. Bế tắc có thể là khó khăn để điều tra, bởi vì chúng có thể xảy ra không xác định; chẳng hạn, chỉ khi có nhiều dữ liệu để điền vào một đường ống.
expect
với những gì nó được thiết kế choMục đích chính của các đồng xử lý là cung cấp cho shell một cách để tương tác với các lệnh. Tuy nhiên, nó không hoạt động tốt.
Hình thức bế tắc đơn giản nhất được đề cập ở trên là:
tr a b |&
echo a >&p
read var<&p
Bởi vì đầu ra của nó không đi đến một thiết bị đầu cuối, nên tr
đệm đầu ra của nó. Vì vậy, nó sẽ không xuất bất cứ thứ gì cho đến khi nó nhìn thấy tập tin cuối của nó stdin
, hoặc nó đã tích lũy một bộ đệm đầy dữ liệu để xuất ra. Vì vậy, ở trên, sau khi shell có đầu ra a\n
(chỉ có 2 byte), read
khối sẽ bị chặn vô thời hạn vì tr
đang chờ shell gửi cho nó nhiều dữ liệu hơn.
Nói tóm lại, ống không tốt cho việc tương tác với các lệnh. Các quy trình đồng chỉ có thể được sử dụng để tương tác với các lệnh không đệm đầu ra của chúng hoặc các lệnh có thể được yêu cầu không đệm đầu ra của chúng; ví dụ, bằng cách sử dụng stdbuf
với một số lệnh trên các hệ thống GNU hoặc FreeBSD gần đây.
Đó là lý do tại sao expect
hoặc zpty
sử dụng thiết bị đầu cuối giả thay thế. expect
là một công cụ được thiết kế để tương tác với các lệnh và nó hoạt động tốt.
Các quy trình đồng có thể được sử dụng để thực hiện một số hệ thống ống nước phức tạp hơn những gì ống vỏ đơn giản cho phép.
rằng câu trả lời Unix.SE khác có một ví dụ về việc sử dụng coproc.
Đây là một ví dụ đơn giản: Hãy tưởng tượng bạn muốn một hàm cung cấp một bản sao đầu ra của lệnh cho 3 lệnh khác và sau đó đầu ra của 3 lệnh đó được nối với nhau.
Tất cả sử dụng đường ống.
Ví dụ: ăn đầu ra của printf '%s\n' foo bar
để tr a b
, sed 's/./&&/g'
và cut -b2-
để có được một cái gì đó như:
foo
bbr
ffoooo
bbaarr
oo
ar
Đầu tiên, nó không nhất thiết phải rõ ràng, nhưng có khả năng bế tắc ở đó, và nó sẽ bắt đầu xảy ra chỉ sau vài kilobyte dữ liệu.
Sau đó, tùy thuộc vào vỏ của bạn, bạn sẽ gặp phải một số vấn đề khác nhau phải được giải quyết khác nhau.
Chẳng hạn, với zsh
, bạn sẽ làm điều đó với:
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
Ở trên, các fds đồng xử lý có cờ đóng-on-exec, nhưng không phải là các cờ được sao chép từ chúng (như trong {o1}<&p
). Vì vậy, để tránh bế tắc, bạn sẽ phải đảm bảo rằng họ đã đóng trong bất kỳ quy trình nào không cần đến chúng.
Tương tự như vậy, chúng ta phải sử dụng một lớp con và sử dụng exec cat
cuối cùng, để đảm bảo không có quá trình vỏ nằm ở việc giữ một ống mở.
Với ksh
(ở đây ksh93
), đó sẽ phải là:
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
( Lưu ý: Điều đó sẽ không hoạt động trên các hệ thống ksh
sử dụng socketpairs
thay vì pipes
và /dev/fd/n
hoạt động như trên Linux.)
Trong ksh
, fds ở trên 2
được đánh dấu bằng cờ close-on-exec, trừ khi chúng được truyền rõ ràng trên dòng lệnh. Đó là lý do tại sao chúng ta không cần phải đóng file descriptor không sử dụng giống như với zsh
-Nhưng nó cũng lý do tại sao chúng ta phải làm {i1}>&$i1
và sử dụng eval
cho rằng giá trị mới của $i1
, để được chuyển tới tee
và cat
...
Trong bash
này không thể được thực hiện, bởi vì bạn không thể tránh đóng cửa-on-exec cờ.
Ở trên, nó tương đối đơn giản, vì chúng tôi chỉ sử dụng các lệnh bên ngoài đơn giản. Nó trở nên phức tạp hơn khi bạn muốn sử dụng các cấu trúc shell trong đó và bạn bắt đầu gặp phải các lỗi shell.
So sánh ở trên với cùng sử dụng ống có tên:
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
Nếu bạn muốn tương tác với một lệnh, sử dụng expect
, hoặc zsh
các zpty
đường ống được đặt tên.
Nếu bạn muốn làm một số hệ thống ống nước lạ mắt với đường ống, sử dụng ống có tên.
Các quy trình hợp tác có thể thực hiện một số điều trên, nhưng hãy chuẩn bị để thực hiện một số thao tác gãi đầu nghiêm trọng cho bất cứ điều gì không tầm thường.
exec {tr[1]}>&-
thực sự có vẻ hoạt động với các phiên bản mới hơn và được tham chiếu trong mục nhập CWRU / changelog ( cho phép các từ như {mảng [ind]} là chuyển hướng hợp lệ ... 2012-09-01). exec {tr[1]}<&-
(hoặc >&-
tương đương chính xác hơn mặc dù điều đó không có gì khác biệt vì chỉ cần gọi close()
cho cả hai) không đóng stdin của coproc, nhưng kết thúc bằng văn bản của ống dẫn đến coproc đó.
yash
.
mkfifo
là bạn không phải lo lắng về điều kiện chủng tộc và bảo mật cho việc tiếp cận đường ống. Bạn vẫn phải lo lắng về sự bế tắc với fifo.
stdbuf
lệnh có thể giúp ngăn chặn ít nhất một số trong số họ. Tôi đã sử dụng nó trong Linux và bash. Dù sao, tôi tin rằng @ StéphaneChazelas đã đúng trong Kết luận: giai đoạn "gãi đầu" chỉ kết thúc với tôi khi tôi quay trở lại các đường ống được đặt tên.
Các quy trình đồng được giới thiệu lần đầu tiên bằng ngôn ngữ kịch bản lệnh shell với ksh88
shell (1988), và sau đó zsh
tại một số thời điểm trước năm 1993.
Cú pháp để khởi chạy một đồng xử lý theo ksh là command |&
. Bắt đầu từ đó, bạn có thể ghi command
vào đầu vào tiêu chuẩn với print -p
và đọc đầu ra tiêu chuẩn của nó với read -p
.
Hơn một vài thập kỷ sau đó, bash thiếu tính năng này cuối cùng đã giới thiệu nó trong phiên bản 4.0. Thật không may, một cú pháp không tương thích và phức tạp hơn đã được chọn.
Trong bash 4.0 và mới hơn, bạn có thể khởi chạy đồng xử lý với coproc
lệnh, ví dụ:
$ coproc awk '{print $2;fflush();}'
Sau đó, bạn có thể truyền một cái gì đó cho lệnh stdin theo cách đó:
$ echo one two three >&${COPROC[1]}
và đọc đầu ra awk với:
$ read -ru ${COPROC[0]} foo
$ echo $foo
two
Theo ksh, đó sẽ là:
$ awk '{print $2;fflush();}' |&
$ print -p "one two three"
$ read -p foo
$ echo $foo
two
Một "coproc" là gì?
Nó là viết tắt của "co-process" có nghĩa là một quá trình thứ hai hợp tác với vỏ. Nó rất giống với một công việc nền được bắt đầu bằng "&" ở cuối lệnh, ngoại trừ việc thay vì chia sẻ cùng một đầu vào và đầu ra tiêu chuẩn như vỏ mẹ của nó, I / O tiêu chuẩn của nó được kết nối với vỏ cha mẹ bằng một đặc biệt loại ống được gọi là FIFO. Để tham khảo bấm vào đây
Người ta bắt đầu một coproc trong zsh với
coproc command
Lệnh phải được chuẩn bị để đọc từ stdin và / hoặc ghi vào thiết bị xuất chuẩn, hoặc nó không được sử dụng nhiều như một coproc.
Đọc bài viết này ở đây nó cung cấp một nghiên cứu trường hợp giữa exec và coproc
|
. (đó là sử dụng đường ống trong hầu hết các vỏ và ổ cắm trong ksh93). ống và ổ cắm là đầu tiên, đầu tiên, tất cả đều là FIFO. mkfifo
làm cho đường ống được đặt tên, coprocesses không sử dụng đường ống được đặt tên.
Đây là một ví dụ tốt (và đang hoạt động) - một máy chủ đơn giản được viết bằng BASH. Xin lưu ý rằng bạn sẽ cần OpenBSD netcat
, phiên bản cổ điển sẽ không hoạt động. Tất nhiên bạn có thể sử dụng ổ cắm inet thay vì unix một.
server.sh
:
#!/usr/bin/env bash
SOCKET=server.sock
PIDFILE=server.pid
(
exec </dev/null
exec >/dev/null
exec 2>/dev/null
coproc SERVER {
exec nc -l -k -U $SOCKET
}
echo $SERVER_PID > $PIDFILE
{
while read ; do
echo "pong $REPLY"
done
} <&${SERVER[0]} >&${SERVER[1]}
rm -f $PIDFILE
rm -f $SOCKET
) &
disown $!
client.sh
:
#!/usr/bin/env bash
SOCKET=server.sock
coproc CLIENT {
exec nc -U $SOCKET
}
{
echo "$@"
read
} <&${CLIENT[0]} >&${CLIENT[1]}
echo $REPLY
Sử dụng:
$ ./server.sh
$ ./client.sh ping
pong ping
$ ./client.sh 12345
pong 12345
$ kill $(cat server.pid)
$
bash 4.3.11
, bây giờ bạn có thể đóng trực tiếp các mô tả tệp coproc mà không cần phải có một phụ trợ. Biến đổi; về mặt ví dụ trong câu trả lời của bạnexec {tr[1]}<&-
bây giờ sẽ hoạt động (để đóng stdin của coproc; lưu ý rằng mã của bạn (gián tiếp) cố gắng đóng{tr[1]}
bằng cách sử dụng>&-
, nhưng{tr[1]}
là stdin của coproc , và phải được đóng lại<&-
). Việc khắc phục phải xảy ra ở đâu đó giữa4.2.25
, vẫn còn vấn đề, và4.3.11
, không.