Làm thế nào để bạn sử dụng lệnh coproc trong các shell khác nhau?


Câu trả lời:


118

đồng xử lý là một kshtí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 bashvà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, acung cấp dữ liệu đến cmdbđọc đầu ra của nó. Chạy cmdnhư một đồng xử lý cho phép vỏ được cả hai ab.

đồng quy trình ksh

Trong ksh, bạn bắt đầu một bộ đồng xử lý như:

cmd |&

Bạn cung cấp dữ liệu cmdbằng cách thực hiện những việc như:

echo test >&p

hoặc là

print -p test

Và đọc cmdkế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, killvào nó và tham khảo nó bằng %job-numberhoặ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

đồng quy trình zsh

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 coproctừ 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ộ coprocmô 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, cmdlà 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 evalhoặ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.

bash đồng quy trì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.

bashcung 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

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 kshhoặc zsh, các đường ống đến và từ quá trình đồng được truy cập với >&p<&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 $COPROCmảng (tương ứng ${COPROC[0]}${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 đó.

Cú pháp mở rộng

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 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]}${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 kshzsh, 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, 1hoặ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.)

quá trình yash và chuyển hướng đường ống

yashkhô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 ốngquy trình của nó . yashcó 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<&-

Bây giờ, tại sao chúng không quá phổ biến

hầu như không có lợi ích gì khi sử dụng ống dẫn

Đồ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 coproclà bạn không phải dọn sạch những ống có tên sau khi sử dụng.

bế tắc

Vỏ sử dụng đường ống trong một vài cấu trúc:

  • Ống vỏ: cmd1 | cmd2 ,
  • thay thế lệnh: $(cmd) ,
  • thay thế quá trình: <(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.

hoạt động tồi tệ hơn so expectvới những gì nó được thiết kế cho

Mụ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), readkhố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 stdbufvớ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 expecthoặc zptysử dụng thiết bị đầu cuối giả thay thế. expectlà 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.

Xử lý mô tả tệp rất khó và khó để lấy đúng

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'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 catcuố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 kshsử dụng socketpairsthay vì pipes/dev/fd/nhoạ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}>&$i1và sử dụng evalcho rằng giá trị mới của $i1, để được chuyển tới teecat...

Trong bashnà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

Phần kết luận

Nếu bạn muốn tương tác với một lệnh, sử dụng expect, hoặc zshcá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.


Câu trả lời tuyệt vời thực sự. Tôi không biết cụ thể nó đã được sửa khi nào, nhưng ít nhất 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ạn exec {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]}stdin của coproc , và phải được đóng lại <&-). Việc khắc phục phải xảy ra ở đâu đó giữa 4.2.25, vẫn còn vấn đề, và 4.3.11, không.
mkuity0

1
@ mkuity0, cảm ơn. 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 đó.
Stéphane Chazelas

1
@ mkuity0, điểm tốt, tôi đã cập nhật và thêm yash.
Stéphane Chazelas

1
Một lợi thế hơn mkfifolà 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.
Otheus

1
Về những bế tắc: stdbuflệ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.
shub

7

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 ksh88shell (1988), và sau đó zshtạ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 commandvào đầu vào tiêu chuẩn với print -pvà đọ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 coproclệ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

-1

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


Bạn có thể thêm một số bài viết vào câu trả lời của bạn? Tôi đã cố gắng để chủ đề này được đề cập trong U & L vì nó dường như được đại diện. Cảm ơn câu trả lời của bạn! Cũng lưu ý rằng tôi đặt thẻ là Bash, không phải zsh.
slm

@slm Bạn đã chỉ vào tin tặc Bash. Tôi thấy có đủ ví dụ. Nếu ý định của bạn là làm cho câu hỏi này được chú ý thì có bạn đã thành công:>
Valentin Bajrami

Chúng không phải là loại ống đặc biệt, chúng là những ống giống như được sử dụng |. (đó 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. mkfifolàm cho đường ống được đặt tên, coprocesses không sử dụng đường ống được đặt tên.
Stéphane Chazelas

@slm xin lỗi vì zsh ... thực ra tôi làm việc trên zsh. Tôi có xu hướng làm điều đó đôi khi với dòng chảy. Nó cũng hoạt động tốt ở Bash ...
Munai Das Udasin

@ Stephane Chazelas Tôi khá chắc chắn rằng tôi đã đọc nó ở đâu đó rằng I / O được kết nối với các loại ống đặc biệt gọi là FIFO ...
Munai Das Udasin

-1

Đâ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)
$
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.