stdout/stderr이 인터리브되는 것을 방지하는 것은 무엇입니까?

stdout/stderr이 인터리브되는 것을 방지하는 것은 무엇입니까?

몇 가지 프로세스를 실행한다고 가정해 보겠습니다.

#!/usr/bin/env bash

foo &
bar &
baz &

wait;

위의 스크립트를 다음과 같이 실행합니다.

foobarbaz | cat

내가 아는 한, 모든 프로세스가 stdout/stderr에 쓸 때 출력은 인터리브되지 않습니다. stdio의 모든 행은 원자적인 것처럼 보입니다. 어떻게 작동하나요? 각 행의 원자성을 제어하는 ​​유틸리티는 무엇입니까?

답변1

그들은 교차합니다! 짧은 출력 버스트만 시도했지만 분할되지 않은 상태로 유지되었지만 실제로는 특정 출력이 분할되지 않은 상태로 유지된다는 보장이 어렵습니다.

출력 버퍼

프로그램이 어떻게 되느냐에 따라 다르지만완충기그들의 출력. 이것표준 입력 및 출력 라이브러리대부분의 프로그램은 출력 효율성을 높이기 위해 쓰기 시 버퍼를 사용합니다. 이 함수는 프로그램이 파일에 쓰기 위해 라이브러리 함수를 호출할 때 즉시 데이터를 출력하지 않고, 대신 버퍼에 데이터를 저장하고 버퍼가 채워진 후에만 실제로 데이터를 출력합니다. 이는 출력이 일괄적으로 수행됨을 의미합니다. 보다 정확하게는 세 가지 출력 모드가 있습니다.

  • 버퍼링되지 않음: 데이터가 즉시 기록되며 버퍼가 사용되지 않습니다. 프로그램이 출력을 작은 덩어리(예: 문자 단위)로 기록하는 경우 속도가 느려질 수 있습니다. 이는 표준 오류의 기본 모드입니다.
  • 완전히 버퍼링됨: 버퍼가 가득 찬 경우에만 데이터가 기록됩니다. 이는 파이프 또는 일반 파일(stderr 제외)에 쓸 때의 기본 모드입니다.
  • 라인 버퍼링: 각 개행 문자 다음에 또는 버퍼가 가득 찼을 때 데이터가 기록됩니다. 이는 터미널에 쓸 때의 기본 모드입니다(stderr 제외).

프로그램은 각 파일이 다르게 동작하도록 다시 프로그래밍할 수 있으며 버퍼를 명시적으로 플러시할 수 있습니다. 프로그램이 파일을 닫거나 정상적으로 종료되면 버퍼가 자동으로 플러시됩니다.

동일한 파이프에 쓰는 모든 프로그램이 라인 버퍼 모드를 사용하거나, 버퍼링되지 않은 모드를 사용하고 출력 함수에 대한 단일 호출로 각 라인을 쓰는 경우, 라인이 단일 블록을 쓸 만큼 짧은 경우 출력은 전체 라인이 됩니다. 인터레이스의. 그러나 프로그램 중 하나가 완전 버퍼링 모드를 사용하거나 줄이 너무 길면 혼합된 줄이 표시됩니다.

다음은 두 프로그램의 출력을 인터리브한 예입니다. 저는 Linux에서 GNU coreutils를 사용하고 있습니다. 이러한 유틸리티의 버전에 따라 다르게 동작할 수 있습니다.

  • yes aaaaaaaa본질적으로 행 버퍼 모드와 동일한 방식으로 영원히 작성합니다. 유틸리티 yes는 실제로 한 번에 여러 줄을 쓰지만 출력을 내보낼 때마다 출력은 정수 줄 수입니다.
  • while true; do echo bbbb; done | grep bbbbb완전 버퍼링 모드에서 영원히 쓰기. 버퍼 크기는 8192이고 줄 길이는 5바이트입니다. 5는 8192로 나눌 수 없으므로 쓰기 간의 경계는 일반적으로 행 경계에 있지 않습니다.

그것들을 하나로 모아보자.

$ { 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

보시다시피, grep이 때때로 중단되고 그 반대의 경우도 있습니다. 회선이 단절되는 경우는 약 0.001%에 불과하지만 그런 일이 발생합니다. 출력이 무작위이므로 인터럽트 수가 다양하지만 매번 적어도 몇 개의 인터럽트가 표시됩니다. 줄이 길면 버퍼당 줄 수가 줄어들수록 끊어질 가능성이 높아지므로 끊어진 줄의 비율이 높아집니다.

이를 수행하는 방법에는 여러 가지가 있습니다.출력 버퍼링 조정. 다음이 있습니다:

  • 프로그램의 기본 설정을 변경하지 않고 stdio 라이브러리를 사용하는 프로그램에서 버퍼링을 끄십시오.stdbuf -o0GNU coreutils 및 일부 다른 시스템(예: FreeBSD)에서 발견됩니다. 라인 버퍼링으로 전환을 사용할 수도 있습니다 stdbuf -oL.
  • 이 목적으로 생성된 터미널 부트로더의 출력을 통해 라인 버퍼링으로 전환합니다.unbuffer. 일부 프로그램은 grep출력이 터미널인 경우 기본적으로 색상을 사용하는 등 다른 방식으로 다르게 동작할 수 있습니다 .
  • 구성 프로그램(예: --line-bufferedGNU grep에 전달됨)

위의 코드 조각을 다시 살펴보겠습니다. 이번에는 양쪽에 라인 버퍼링이 있습니다.

{ 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

따라서 이번에는 yes가 grep을 중단하지 않지만 grep은 때때로 yes를 중단합니다. 이유는 나중에 설명하겠습니다.

파이프 인터리빙

각 프로그램이 한 번에 한 줄씩 출력하고 줄이 충분히 짧으면 출력 줄은 깔끔하게 분리됩니다. 그러나 이를 달성하려면 대기열 시간이 제한됩니다. 파이프 자체에는 전송 버퍼가 있습니다. 프로그램이 파이프로 출력되면 데이터는 기록기에서 파이프의 전송 버퍼로 복사된 다음 파이프의 전송 버퍼에서 판독기로 복사됩니다. (적어도 개념적으로는 커널이 때때로 이를 단일 복사본으로 최적화할 수 있습니다.)

파이프 전송 버퍼가 보유할 수 있는 것보다 복사할 데이터가 더 많은 경우 커널은 버퍼를 한 번에 하나씩 복사합니다. 여러 프로그램이 동일한 파이프에 쓰고 있고 커널이 선택한 첫 번째 프로그램이 여러 버퍼에 쓰려고 하는 경우 커널이 두 번째에도 동일한 프로그램을 다시 선택한다는 보장이 없습니다. 예를 들어,버퍼 크기입니다. foo2*를 쓰고 싶습니다.바이트를 쓰고 bar3바이트를 쓰고 싶다면 가능한 인터리빙 중 하나는 다음과 같습니다.의 바이트 foo, 의 3바이트 bar, 및.foo

위의 yes+grep 예제로 돌아가서 내 시스템에서는 yes aaaa8192바이트 버퍼에 들어갈 수 있는 만큼 많은 라인을 한 번에 작성했습니다. 5바이트가 기록되므로(4개의 인쇄 가능한 문자 및 개행 문자) 이는 매번 8190바이트가 기록된다는 의미입니다. 파이프 버퍼 크기는 4096바이트입니다. 따라서 yes에서 4096바이트를 얻은 다음 grep에서 일부 출력을 얻은 다음 yes에서 나머지 쓰기를 얻을 수 있습니다(8190 - 4096 = 4094바이트). 819개 라인 에 대해 4096바이트 및 aaaa단일 a.aabbbb

무슨 일이 일어나고 있는지 자세히 알고 싶다면 getconf PIPE_BUF .시스템의 파이프 버퍼 크기를 알려주고 다음을 사용하여 각 프로그램에서 수행한 시스템 호출의 전체 목록을 볼 수 있습니다.

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

깨끗한 라인 인터리빙을 보장하는 방법

라인 버퍼링은 라인 길이가 파이프 버퍼 크기보다 작은 경우 출력에 혼합 라인이 나타나지 않도록 보장합니다.

행 길이가 더 길면 여러 프로그램이 동일한 파이프에 쓸 때 임의 혼합을 피할 수 있는 방법이 없습니다. 분리를 보장하려면 각 프로그램이 다른 파이프에 쓰도록 하고 하나의 프로그램을 사용하여 라인을 결합해야 합니다. 예를 들어GNU 병렬이는 기본적으로 수행됩니다.

답변2

http://mywiki.wooledge.org/BashPitfalls#Non-atomic_writes_with_xargs_-P이것은 연구되었습니다:

GNU xargs는 여러 작업을 병렬로 실행하는 것을 지원합니다. -P n 여기서 n은 병렬로 실행할 작업 수입니다.

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

이는 대부분의 경우 잘 작동하지만 한 가지 기만적인 결함이 있습니다. $a에 1000자를 초과하는 경우 에코가 원자적이지 않을 수 있으며(여러 개의 write() 호출로 분할될 수 있음) 두 개의 길드가 혼합되어 있습니다.

$ 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 +++

분명히 echo 또는 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!

각 작업은 두 개(또는 그 이상)의 별도 write() 호출로 구성되므로 병렬 작업의 출력은 함께 혼합됩니다.

따라서 혼합되지 않은 출력이 필요한 경우 출력이 직렬화되도록 보장하는 도구(예: GNU Parallel)를 사용하는 것이 좋습니다.

관련 정보