Đầu ra thay thế quá trình là ra khỏi thứ tự


16

Các

echo one; echo two > >(cat); echo three; 

lệnh cho đầu ra bất ngờ.

Tôi đọc điều này: Làm thế nào thay thế quá trình được thực hiện trong bash? và nhiều bài viết khác về thay thế quy trình trên internet, nhưng không hiểu tại sao nó lại hoạt động theo cách này.

Sản lượng dự kiến:

one
two
three

Sản lượng thực:

prompt$ echo one; echo two > >(cat); echo three;
one
three
prompt$ two

Ngoài ra, hai lệnh này phải tương đương với quan điểm của tôi, nhưng chúng không:

##### first command - the pipe is used.
prompt$ seq 1 5 | cat
1
2
3
4
5
##### second command - the process substitution and redirection are used.
prompt$ seq 1 5 > >(cat)
prompt$ 1
2
3
4
5

Tại sao tôi nghĩ, họ nên giống nhau? Bởi vì, cả hai kết nối seqđầu ra với catđầu vào thông qua đường ống ẩn danh - Wikipedia, Quá trình thay thế .

Câu hỏi: Tại sao nó hành xử theo cách này? Lỗi của tôi ở đâu? Câu trả lời toàn diện là mong muốn (với lời giải thích về cách bashthực hiện dưới mui xe).


2
Ngay cả khi nó không rõ ràng ngay từ cái nhìn đầu tiên, nó thực sự là một bản sao của bash chờ quá trình thay thế quy trình ngay cả khi lệnh không hợp lệ
Stéphane Chazelas

2
Trên thực tế, sẽ tốt hơn nếu câu hỏi khác được đánh dấu là trùng lặp với câu hỏi này vì câu hỏi này có ý nghĩa hơn. Đó là lý do tại sao tôi sao chép câu trả lời của tôi ở đó.
Stéphane Chazelas

Câu trả lời:


21

Có, bashgiống như trong ksh(tính năng đến từ đâu), các quy trình bên trong quy trình thay thế không được chờ đợi (trước khi chạy lệnh tiếp theo trong tập lệnh).

đối với một <(...), điều đó thường tốt như trong:

cmd1 <(cmd2)

Shell sẽ chờ đợi cmd1cmd1thường sẽ chờ đợi cho cmd2đến khi nó đọc cho đến khi hết tập tin trên đường ống được thay thế, và việc kết thúc tập tin đó thường xảy ra khi cmd2chết. Đó là lý do cùng nhiều vỏ (không bash) không bận tâm chờ đợi cmd2trong cmd2 | cmd1.

Đối cmd1 >(cmd2)Tuy nhiên,, đó là thường không phải như vậy, vì nó là nhiều cmd2mà thường chờ cmd1có như vậy sẽ thường thoát sau đó.

Điều đó đã được sửa trong zshđó chờ cmd2ở đó (nhưng không phải nếu bạn viết nó dưới dạng cmd1 > >(cmd2)cmd1không được dựng sẵn, {cmd1} > >(cmd2)thay vào đó hãy sử dụng như tài liệu ).

kshkhông chờ đợi theo mặc định, nhưng cho phép bạn chờ nó với waitnội trang (nó cũng làm cho pid có sẵn $!, mặc dù điều đó không giúp ích gì nếu bạn làm vậy cmd1 >(cmd2) >(cmd3))

rc(với cmd1 >{cmd2}cú pháp), giống như kshngoại trừ bạn có thể nhận được các pids của tất cả các quy trình nền với $apids.

es(cũng với cmd1 >{cmd2}) chờ đợi cmd2như trong zshvà cũng chờ cmd2trong <{cmd2}quá trình chuyển hướng.

bashkhông làm cho pid của cmd2(hoặc chính xác hơn là lớp con như nó chạy cmd2trong một tiến trình con của lớp con đó mặc dù đó là lệnh cuối cùng ở đó) có sẵn $!, nhưng không cho phép bạn chờ đợi.

Nếu bạn phải sử dụng bash, bạn có thể giải quyết vấn đề bằng cách sử dụng lệnh sẽ chờ cả hai lệnh với:

{ { cmd1 >(cmd2); } 3>&1 >&4 4>&- | cat; } 4>&1

Điều đó làm cho cả hai cmd1cmd2có fd 3 của họ mở ra một đường ống. catsẽ chờ kết thúc tập tin ở đầu kia, vì vậy thường sẽ chỉ thoát khi cả hai cmd1cmd2đã chết. Và shell sẽ chờ catlệnh đó . Bạn có thể thấy rằng như một mạng lưới để bắt đầu chấm dứt tất cả các quy trình nền (bạn có thể sử dụng nó cho những thứ khác bắt đầu trong nền như với &, coprocs hoặc thậm chí các lệnh mà nền đó cung cấp, chúng không đóng tất cả các mô tả tệp của chúng như daemon thường làm ).

Lưu ý rằng nhờ quy trình chia nhỏ bị lãng phí đã đề cập ở trên, nó hoạt động ngay cả khi cmd2đóng fd 3 của nó (các lệnh thường không làm điều đó, nhưng một số thích sudohoặc sshlàm). Các phiên bản trong tương lai bashcuối cùng có thể thực hiện tối ưu hóa như trong các shell khác. Sau đó, bạn cần một cái gì đó như:

{ { cmd1 >(sudo cmd2; exit); } 3>&1 >&4 4>&- | cat; } 4>&1

Để đảm bảo vẫn còn một quá trình shell bổ sung với fd 3 mở đó đang chờ sudolệnh đó .

Lưu ý rằng catsẽ không đọc bất cứ điều gì (vì các quy trình không viết trên fd 3 của họ). Nó chỉ ở đó để đồng bộ hóa. Nó sẽ thực hiện chỉ một read()cuộc gọi hệ thống sẽ trở lại mà không có gì ở cuối.

Bạn thực sự có thể tránh chạy catbằng cách sử dụng thay thế lệnh để thực hiện đồng bộ hóa đường ống:

{ unused=$( { cmd1 >(cmd2); } 3>&1 >&4 4>&-); } 4>&1

Lần này, nó là cái vỏ thay vì catđọc từ ống có đầu kia mở trên fd 3 cmd1cmd2. Chúng tôi đang sử dụng một phép gán biến để trạng thái thoát cmd1có sẵn trong $?.

Hoặc bạn có thể thực hiện thay thế quy trình bằng tay, và sau đó bạn thậm chí có thể sử dụng hệ thống của mình shvì điều đó sẽ trở thành cú pháp shell tiêu chuẩn:

{ cmd1 /dev/fd/3 3>&1 >&4 4>&- | cmd2 4>&-; } 4>&1

mặc dù lưu ý như đã lưu ý trước đó rằng không phải tất cả các shtriển khai sẽ đợi cmd1sau khi cmd2kết thúc (mặc dù điều đó tốt hơn so với cách khác). Thời gian đó, $?chứa trạng thái thoát của cmd2; mặc dù bashzshlàm cho cmd1trạng thái thoát có sẵn trong ${PIPESTATUS[0]}$pipestatus[1]tương ứng (xem thêm pipefailtùy chọn trong một số shell để $?có thể báo cáo sự cố của các thành phần đường ống khác với lần cuối)

Lưu ý rằng yashcó vấn đề tương tự với tính năng chuyển hướng quy trình của nó . cmd1 >(cmd2)sẽ được viết cmd1 /dev/fd/3 3>(cmd2)ở đó. Nhưng cmd2không được chờ đợi và bạn cũng không thể sử dụng waitđể chờ đợi và pid của nó cũng không có sẵn trong $!biến. Bạn sẽ sử dụng cùng một công việc xung quanh như cho bash.


Đầu tiên, tôi đã thử echo one; { { echo two > >(cat); } 3>&1 >&4 4>&- | cat; } 4>&1; echo three;, sau đó đơn giản hóa nó echo one; echo two > >(cat) | cat; echo three;và nó cũng xuất ra các giá trị theo đúng thứ tự. Có phải tất cả các thao tác mô tả này 3>&1 >&4 4>&-là cần thiết? Ngoài ra, tôi không nhận được điều này >&4 4>&- chúng tôi đang chuyển hướng stdoutđến fd thứ tư, sau đó đóng fd thứ tư, sau đó lại sử dụng 4>&1nó. Tại sao nó cần và làm thế nào nó hoạt động? Có thể, tôi nên tạo câu hỏi mới về chủ đề này?
MiniMax

1
@MiniMax, nhưng ở đó, bạn đang ảnh hưởng đến thiết bị xuất chuẩn cmd1cmd2, điểm với điệu nhảy nhỏ với bộ mô tả tệp là khôi phục lại bản gốc và chỉ sử dụng ống bổ sung cho việc chờ thay vì chuyển kênh đầu ra của các lệnh.
Stéphane Chazelas

@MiniMax Phải mất một thời gian để hiểu, tôi đã không nhận được các đường ống ở mức thấp như vậy trước đây. Phần ngoài cùng bên phải 4>&1tạo một bộ mô tả tệp (fd) 4 cho danh sách lệnh niềng răng bên ngoài và làm cho nó bằng với thiết bị xuất chuẩn của dấu ngoặc ngoài. Các dấu ngoặc trong có stdin / stdout / stderr tự động thiết lập để kết nối với các dấu ngoặc ngoài. Tuy nhiên, 3>&1làm cho fd 3 kết nối với stdin của niềng răng bên ngoài. >&4làm cho thiết bị xuất chuẩn của niềng răng bên trong kết nối với niềng răng bên ngoài fd 4 (Cái mà chúng ta đã tạo trước đó). 4>&-đóng fd 4 từ niềng răng bên trong (Vì thiết bị xuất chuẩn của niềng răng bên trong đã được kết nối với fd 4 của niềng răng bên ngoài).
Nicholas Pipitone

@MiniMax Phần khó hiểu là phần từ phải sang trái, 4>&1được thực hiện trước, trước các chuyển hướng khác, vì vậy bạn không "sử dụng lại 4>&1". Nhìn chung, các dấu ngoặc trong đang gửi dữ liệu đến thiết bị xuất chuẩn của nó, được ghi đè bằng bất cứ thứ gì fd 4 mà nó được đưa ra. Fd 4 mà niềng răng bên trong đã được đưa ra, là fd 4 của niềng răng bên ngoài, tương đương với tiêu chuẩn ban đầu của niềng răng bên ngoài.
Nicholas Pipitone

Bash làm cho nó có cảm giác như 4>5là "4 đi đến 5", nhưng thực sự "fd 4 bị ghi đè bằng fd 5". Và trước khi thực hiện, fd 0/1/2 được kết nối tự động (Cùng với bất kỳ fd nào của lớp vỏ ngoài) và bạn có thể ghi đè lên chúng theo ý muốn. Đó ít nhất là cách giải thích của tôi về tài liệu bash. Nếu bạn hiểu một cái gì đó khác về điều này , lmk.
Nicholas Pipitone

4

Bạn có thể chuyển lệnh thứ hai sang lệnh khác cat, lệnh này sẽ đợi cho đến khi đường ống đầu vào của nó đóng lại. Ví dụ:

prompt$ echo one; echo two > >(cat) | cat; echo three;
one
two
three
prompt$

Ngắn gọn và đơn giản.

==========

Đơn giản như nó có vẻ, rất nhiều điều đang diễn ra đằng sau hậu trường. Bạn có thể bỏ qua phần còn lại của câu trả lời nếu bạn không quan tâm đến cách thức hoạt động của nó.

Khi bạn có echo two > >(cat); echo three, >(cat)bị chặn bởi lớp vỏ tương tác và chạy độc lập với echo two. Do đó, echo twokết thúc, và sau đó echo threeđược thực hiện, nhưng trước khi >(cat)kết thúc. Khi bashnhận được dữ liệu từ >(cat)khi nó không mong đợi (một vài mili giây sau), nó sẽ mang đến cho bạn tình huống giống như nhắc nhở khi bạn phải nhấn dòng mới để quay lại thiết bị đầu cuối (Giống như khi người dùng khác sửa mesgbạn).

Tuy nhiên, được đưa ra echo two > >(cat) | cat; echo three, hai subshells được sinh ra (theo tài liệu của |biểu tượng).

Một subshell tên A là cho echo two > >(cat), và một subshell tên B là cho cat. A được tự động kết nối với B (Thiết bị xuất chuẩn của A là stdin của B). Sau đó, echo two>(cat)bắt đầu thực hiện. >(cat)Thiết bị xuất chuẩn của A được đặt thành thiết bị xuất chuẩn của A, bằng với stdin của B. Sau khi echo twokết thúc, A thoát ra, đóng lại thiết bị xuất chuẩn của nó. Tuy nhiên, >(cat)vẫn đang giữ tài liệu tham khảo cho stdin của B. catStdin của người thứ hai đang giữ stdin của B, và điều đó catsẽ không thoát cho đến khi thấy EOF. Một EOF chỉ được đưa ra khi không ai mở tệp ở chế độ ghi nữa, do đó >(cat), thiết bị xuất chuẩn sẽ chặn thứ hai cat. B vẫn chờ ngày thứ hai cat. Kể từ khi echo twothoát ra, >(cat)cuối cùng nhận được EOF, vì vậy>(cat)tuôn ra bộ đệm của nó và thoát ra. Không ai giữ catstdin của B / giây nữa, vì vậy người thứ hai catđọc EOF (B hoàn toàn không đọc stdin của nó, điều đó không quan tâm). EOF này làm cho cái thứ hai cattuôn ra bộ đệm của nó, đóng thiết bị xuất chuẩn của nó và thoát ra, rồi B ​​thoát ra vì catthoát ra và B đang đợi cat.

Một cảnh báo của điều này là bash cũng sinh ra một subshell cho >(cat)! Bởi vì điều này, bạn sẽ thấy rằng

echo two > >(sleep 5) | cat; echo three

vẫn sẽ đợi 5 giây trước khi thực hiện echo three, mặc dù sleep 5không giữ stdin của B. Điều này là do một mạng con ẩn C được sinh ra >(sleep 5)đang chờ đợi sleepvà C đang giữ stdin của B. Bạn có thể thấy như thế nào

echo two > >(exec sleep 5) | cat; echo three

Tuy nhiên, sẽ không chờ đợi, vì sleepkhông giữ stdin của B, và không có con ma nào C giữ stdin của B (người thực thi sẽ buộc ngủ để thay thế C, trái ngược với việc cưỡng bức và khiến C chờ đợi sleep). Bất kể cảnh báo này,

echo two > >(exec cat) | cat; echo three

vẫn sẽ thực hiện đúng các chức năng theo thứ tự, như được mô tả trước đây.


Như đã lưu ý trong quá trình chuyển đổi với @MiniMax trong các nhận xét cho câu trả lời của tôi, tuy nhiên điều đó có nhược điểm là ảnh hưởng đến thiết bị xuất chuẩn của lệnh và có nghĩa là đầu ra cần phải được đọc và viết thêm thời gian.
Stéphane Chazelas

Lời giải thích không chính xác. Akhông chờ đợi catsinh sản trong >(cat). Như tôi đã đề cập trong câu trả lời của mình, lý do tại sao echo two > >(sleep 5 &>/dev/null) | cat; echo threeđầu ra threesau 5 giây là bởi vì các phiên bản hiện tại bashlãng phí một quy trình bổ sung trong >(sleep 5)đó chờ đợi sleepvà quá trình đó vẫn còn xuất hiện để pipengăn chặn thứ hai catkết thúc. Nếu bạn thay thế nó echo two > >(exec sleep 5 &>/dev/null) | cat; echo threeđể loại bỏ quá trình bổ sung đó, bạn sẽ thấy rằng nó trở lại ngay lập tức.
Stéphane Chazelas

Nó làm cho một subshell lồng nhau? Tôi đã cố gắng xem xét việc thực hiện bash để tìm ra nó, tôi khá chắc chắn rằng echo two > >(sleep 5 &>/dev/null)ở mức tối thiểu sẽ có được phần con của chính nó. Đây có phải là một chi tiết thực hiện không có tài liệu gây ra sleep 5để có được lớp con của chính nó không? Nếu nó được ghi lại thì đó sẽ là một cách hợp pháp để hoàn thành nó với ít ký tự hơn (Trừ khi có một vòng lặp chặt chẽ, tôi không nghĩ ai sẽ nhận thấy các vấn đề về hiệu suất với một subshell hoặc một con mèo) `. Nếu nó không được ghi lại thì rip, mặc dù vậy, hack sẽ không hoạt động trên các phiên bản trong tương lai.
Nicholas Pipitone

$(...), <(...)thực sự liên quan đến một subshell, nhưng ksh93 hoặc zsh sẽ chạy lệnh cuối cùng trong subshell đó trong cùng một quy trình, bashđó không phải là lý do tại sao vẫn có một quá trình khác giữ ống mở trong khi sleepđang chạy không mở ống. Các phiên bản trong tương lai bashcó thể thực hiện tối ưu hóa tương tự.
Stéphane Chazelas

1
@ StéphaneChazelas Tôi đã cập nhật câu trả lời của mình và tôi nghĩ rằng lời giải thích hiện tại của phiên bản ngắn hơn là chính xác, nhưng dường như bạn biết chi tiết triển khai của shell để bạn có thể xác minh. Tôi nghĩ rằng giải pháp này nên được sử dụng trái ngược với điệu nhảy mô tả tập tin, vì ngay cả dưới exec, nó hoạt động như mong đợi.
Nicholas Pipitone
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.