Điều gì ngăn cản stdout / stderr xen kẽ?


13

Nói rằng tôi chạy một số quy trình:

#!/usr/bin/env bash

foo &
bar &
baz &

wait;

Tôi chạy đoạn script trên như vậy:

foobarbaz | cat

theo như tôi có thể nói, khi bất kỳ quá trình nào ghi vào stdout / stderr, đầu ra của chúng không bao giờ xen kẽ - mỗi dòng của stdio dường như là nguyên tử. Làm thế nào mà làm việc? Tiện ích nào kiểm soát làm thế nào mỗi dòng là nguyên tử?


3
Các lệnh của bạn xuất ra bao nhiêu dữ liệu? Hãy thử làm cho chúng xuất ra một vài kilobyte.
Kusalananda

Bạn có nghĩa là một trong những lệnh xuất ra một vài kb trước một dòng mới?
Alexander Mills

Không, một cái gì đó như thế này: unix.stackexchange.com/a/452762/70524
muru

Câu trả lời:


22

Họ làm xen kẽ! Bạn chỉ thử các cụm đầu ra ngắn, vẫn không ổn định, nhưng trong thực tế, thật khó để đảm bảo rằng bất kỳ đầu ra cụ thể nào vẫn không ổn định.

Bộ đệm đầu ra

Nó phụ thuộc vào cách các chương trình đệm đầu ra của họ. Các thư viện stdio rằng hầu hết các chương trình sử dụng khi họ đang viết sử dụng bộ đệm để làm cho sản lượng hiệu quả hơn. Thay vì xuất dữ liệu ngay khi chương trình gọi hàm thư viện để ghi vào tệp, hàm sẽ lưu dữ liệu này vào bộ đệm và chỉ thực sự xuất dữ liệu sau khi bộ đệm đã đầy. Điều này có nghĩa là đầu ra được thực hiện theo lô. Chính xác hơn, có ba chế độ đầu ra:

  • Unbuffered: dữ liệu được ghi ngay lập tức mà không cần sử dụng bộ đệm. Điều này có thể chậm nếu chương trình ghi đầu ra của nó thành từng phần nhỏ, ví dụ như từng ký tự. Đây là chế độ mặc định cho lỗi tiêu chuẩn.
  • Được đệm hoàn toàn: dữ liệu chỉ được ghi khi bộ đệm đầy. Đây là chế độ mặc định khi ghi vào một đường ống hoặc vào một tệp thông thường, ngoại trừ với stderr.
  • Bộ đệm dòng: dữ liệu được ghi sau mỗi dòng mới hoặc khi bộ đệm đầy. Đây là chế độ mặc định khi ghi vào thiết bị đầu cuối, ngoại trừ với thiết bị lỗi chuẩn.

Các chương trình có thể lập trình lại mỗi tệp để hành xử khác nhau và có thể xóa bộ đệm một cách rõ ràng. Bộ đệm được tự động xóa khi chương trình đóng tệp hoặc thoát bình thường.

Nếu tất cả các chương trình đang ghi vào cùng một đường ống đều sử dụng chế độ đệm dòng hoặc sử dụng chế độ không có bộ đệm và viết từng dòng với một lệnh gọi đến một hàm đầu ra, và nếu các dòng đó đủ ngắn để viết trong một đoạn đơn, thì đầu ra sẽ là một xen kẽ của toàn bộ dòng. Nhưng nếu một trong các chương trình sử dụng chế độ đệm hoàn toàn hoặc nếu các dòng quá dài, thì bạn sẽ thấy các dòng hỗn hợp.

Đây là một ví dụ trong đó tôi xen kẽ đầu ra từ hai chương trình. Tôi đã sử dụng lõi GNU trên Linux; các phiên bản khác nhau của các tiện ích này có thể hoạt động khác nhau.

  • yes aaaaghi aaaamãi mãi trong những gì cơ bản tương đương với chế độ đệm dòng. Các yestiện ích thực sự viết nhiều dòng cùng một lúc, nhưng mỗi lần nó phát ra đầu ra, đầu ra là một số nguyên của dòng.
  • echo bbbb; done | grep bviết bbbbmãi mãi trong chế độ đệm hoàn toàn. Nó sử dụng kích thước bộ đệm là 8192 và mỗi dòng dài 5 byte. Vì 5 không chia 8192, nên ranh giới giữa các lần ghi không nằm ở ranh giới dòng nói chung.

Chúng ta hãy ghép chúng lại với nhau.

$ { yes aaaa & while true; do echo bbbb; done | grep b & } | head -n 999999 | grep -e ab -e ba
bbaaaa
bbbbaaaa
baaaa
bbbaaaa
bbaaaa
bbbaaaa
ab
bbbbaaa

Như bạn có thể thấy, có đôi khi grep bị gián đoạn và ngược lại. Chỉ có khoảng 0,001% số dòng bị gián đoạn, nhưng nó đã xảy ra. Đầu ra được chọn ngẫu nhiên nên số lần gián đoạn sẽ khác nhau, nhưng tôi đã thấy ít nhất một vài lần gián đoạn mỗi lần. Sẽ có một phần cao hơn của các dòng bị gián đoạn nếu các dòng dài hơn, vì khả năng bị gián đoạn tăng khi số lượng dòng trên mỗi bộ đệm giảm.

Có một số cách để điều chỉnh bộ đệm đầu ra . Những cái chính là:

  • Tắt bộ đệm trong các chương trình sử dụng thư viện stdio mà không thay đổi cài đặt mặc định của nó với chương trình stdbuf -o0được tìm thấy trong lõi GNU và một số hệ thống khác như FreeBSD. Bạn có thể thay thế chuyển sang bộ đệm dòng với stdbuf -oL.
  • Chuyển sang bộ đệm dòng bằng cách điều hướng đầu ra của chương trình thông qua một thiết bị đầu cuối được tạo ra chỉ với mục đích này unbuffer. Một số chương trình có thể hoạt động khác nhau theo các cách khác, ví dụ grepsử dụng màu theo mặc định nếu đầu ra của nó là một thiết bị đầu cuối.
  • Cấu hình chương trình, ví dụ bằng cách chuyển qua --line-bufferedGNU grep.

Chúng ta hãy xem đoạn trích ở trên một lần nữa, lần này với bộ đệm dòng ở cả hai bên.

{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & } | head -n 999999 | grep -e ab -e ba
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb

Vì vậy, lần này có không bao giờ bị gián đoạn grep, nhưng đôi khi grep bị gián đoạn có. Tôi sẽ đến tại sao sau.

Ống xen kẽ

Miễn là mỗi chương trình xuất ra một dòng tại một thời điểm và các dòng đủ ngắn, các dòng đầu ra sẽ được phân tách gọn gàng. Nhưng có một giới hạn về thời gian các dòng có thể hoạt động. Các ống chính nó có một bộ đệm chuyển. Khi một chương trình xuất ra một đường ống, dữ liệu sẽ được sao chép từ chương trình ghi vào bộ đệm truyền của ống và sau đó từ bộ đệm chuyển của ống sang chương trình đọc. (Ít nhất là về mặt khái niệm - hạt nhân đôi khi có thể tối ưu hóa điều này thành một bản sao duy nhất.)

Nếu có nhiều dữ liệu để sao chép hơn phù hợp với bộ đệm chuyển của đường ống, thì nhân sẽ sao chép từng bộ đệm một lần. Nếu nhiều chương trình đang ghi vào cùng một ống và chương trình đầu tiên mà kernel chọn muốn ghi nhiều hơn một bộ đệm, thì không có gì đảm bảo rằng kernel sẽ chọn lại chương trình đó lần thứ hai. Ví dụ: nếu P là kích thước bộ đệm, foomuốn ghi 2 * P byte và barmuốn ghi 3 byte, thì một xen kẽ có thể là P byte từ foo, sau đó 3 byte từ barP byte từ foo.

Quay trở lại ví dụ yes + grep ở trên, trên hệ thống của tôi, yes aaaatình cờ viết càng nhiều dòng càng tốt trong bộ đệm 8192 byte trong một lần. Vì có 5 byte để ghi (4 ký tự có thể in và dòng mới), điều đó có nghĩa là nó ghi 8190 byte mỗi lần. Kích thước bộ đệm ống là 4096 byte. Do đó, có thể nhận được 4096 byte từ có, sau đó một số đầu ra từ grep và phần còn lại của ghi từ có (8190 - 4096 = 4094 byte). 4096 byte để lại chỗ cho 819 dòng aaaavà một mình a. Do đó, một dòng với đơn độc này atheo sau là một ghi từ grep, đưa ra một dòng với abbbb.

Nếu bạn muốn xem chi tiết về những gì đang diễn ra, thì getconf PIPE_BUF .sẽ cho bạn biết kích thước bộ đệm ống trên hệ thống của bạn và bạn có thể xem danh sách đầy đủ các cuộc gọi hệ thống được thực hiện bởi mỗi chương trình với

strace -s9999 -f -o line_buffered.strace sh -c '{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & }' | head -n 999999 | grep -e ab -e ba

Làm thế nào để đảm bảo đường xen kẽ sạch

Nếu độ dài dòng nhỏ hơn kích thước bộ đệm ống, thì bộ đệm dòng đảm bảo rằng sẽ không có bất kỳ dòng hỗn hợp nào trong đầu ra.

Nếu độ dài dòng có thể lớn hơn, không có cách nào để tránh trộn lẫn tùy ý khi nhiều chương trình được ghi vào cùng một ống. Để đảm bảo phân tách, bạn cần làm cho mỗi chương trình ghi vào một ống khác nhau và sử dụng một chương trình để kết hợp các dòng. Ví dụ GNU Parallel thực hiện điều này theo mặc định.


thật thú vị, vì vậy điều gì có thể là một cách tốt để đảm bảo rằng tất cả các dòng được viết thành catnguyên tử, sao cho quá trình mèo nhận được toàn bộ các dòng từ foo / bar / baz nhưng không phải là nửa dòng từ một và nửa dòng từ dòng khác, v.v. Có điều gì tôi có thể làm với kịch bản bash không?
Alexander Mills

1
âm thanh này cũng đúng với trường hợp của tôi khi tôi có hàng trăm tệp và awkđược tạo ra hai (hoặc nhiều) dòng đầu ra cho cùng một ID find -type f -name 'myfiles*' -print0 | xargs -0 awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }' nhưng với find -type f -name 'myfiles*' -print0 | xargs -0 cat| awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }'nó chỉ tạo ra một dòng chính xác cho mỗi ID.
αғsнιη

Để ngăn chặn bất kỳ sự xen kẽ nào, tôi có thể làm điều đó với một env lập trình như Node.js, nhưng với bash / shell, không biết làm thế nào để làm điều đó.
Alexander Mills

1
@JoL Đó là do bộ đệm ống đầy lên. Tôi biết tôi phải viết phần thứ hai của câu chuyện Hoàn thành.
Gilles 'SO- ngừng trở nên xấu xa'

1
@OlegzandrDenman TLDR đã thêm: họ thực hiện xen kẽ. Lý do rất phức tạp.
Gilles 'SO- ngừng trở nên xấu xa'

1

http://mywiki.wooledge.org/BashPitfall#Non-atomic_writes_with_xargs_-P đã xem xét điều này:

GNU xargs hỗ trợ chạy nhiều công việc song song. -P n trong đó n là số lượng công việc chạy song song.

seq 100 | xargs -n1 -P10 echo "$a" | grep 5
seq 100 | xargs -n1 -P10 echo "$a" > myoutput.txt

Điều này sẽ hoạt động tốt trong nhiều tình huống nhưng có một lỗi lừa đảo: Nếu $ a chứa hơn ~ 1000 ký tự, tiếng vang có thể không phải là nguyên tử (nó có thể được chia thành nhiều lệnh ghi ()) và có nguy cơ hai dòng sẽ được trộn lẫn.

$ perl -e 'print "a"x2000, "\n"' > foo
$ strace -e write bash -c 'read -r foo < foo; echo "$foo"' >/dev/null
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 1008) = 1008
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 993) = 993
+++ exited with 0 +++

Rõ ràng cùng một vấn đề phát sinh nếu có nhiều cuộc gọi tới echo hoặc printf:

slowprint() {
  printf 'Start-%s ' "$1"
  sleep "$1"
  printf '%s-End\n' "$1"
}
export -f slowprint
seq 10 | xargs -n1 -I {} -P4 bash -c "slowprint {}"
# Compare to no parallelization
seq 10 | xargs -n1 -I {} bash -c "slowprint {}"
# Be sure to see the warnings in the next Pitfall!

Đầu ra từ các công việc song song được trộn lẫn với nhau, bởi vì mỗi công việc bao gồm hai (hoặc nhiều hơn) các lệnh write () riêng biệt.

Nếu bạn cần các đầu ra không trộn lẫn, do đó nên sử dụng một công cụ đảm bảo đầu ra sẽ được tuần tự hóa (chẳng hạn như GNU Parallel).


Phần đó là sai. xargs echokhông gọi hàm bash echo, nhưng echotiện ích từ $PATH. Và dù sao tôi cũng không thể tái tạo hành vi bash echo đó bằng bash 4.4. Trên Linux, ghi vào một đường ống (không / dev / null) lớn hơn 4K không được đảm bảo là nguyên tử.
Stéphane Chazelas
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.