다음 스크립트가 있다고 가정해 보겠습니다.
#!/bin/bash
for i in $(seq 1000)
do
cp /etc/passwd tmp
cat tmp | head -1 | head -1 | head -1 > tmp #this is the key line
cat tmp
done
중요한 줄에서 동일한 파일을 읽고 쓰는데 tmp
때로는 실패합니다.
(이것은 경쟁 조건 때문이라는 것을 읽었습니다. 파이프라인의 프로세스가 병렬로 실행되기 때문에 왜인지 이해가 되지 않습니다. 각 프로세스는 head
이전 프로세스에서 데이터를 가져와야 합니다. 그렇죠? 그건 제 것이 아닙니다. 주요 질문이지만 답변도 가능합니다.)
스크립트를 실행하면 약 200줄이 출력됩니다. 이 스크립트가 항상 0줄을 출력하도록 강제할 수 있는 방법이 있습니까 tmp
? (따라서 I/O 리디렉션이 항상 먼저 준비되어 데이터가 항상 삭제됩니다.) 분명히 말하면 이 스크립트가 아니라 시스템 설정을 변경한다는 의미입니다.
당신의 생각에 감사드립니다.
답변1
경쟁 조건이 존재하는 이유
파이프라인의 양쪽이 차례로 실행되는 것이 아니라 병렬로 실행됩니다. 이를 보여주는 매우 간단한 방법이 있습니다.
time sleep 1 | sleep 1
이 작업에는 2초가 아니라 1초가 소요됩니다.
쉘은 두 개의 하위 프로세스를 시작하고 완료될 때까지 기다립니다. 두 프로세스는 병렬로 실행됩니다. 둘 중 하나가 다른 프로세스와 동기화되는 유일한 이유는 다음과 같습니다.필요서로를 기다리십시오. 가장 일반적인 동기화 지점은 오른쪽이 표준 입력에서 데이터 읽기를 기다리는 것을 차단하고 왼쪽이 더 많은 데이터를 쓸 때 차단을 해제하는 곳입니다. 반대의 경우도 발생할 수 있습니다. 오른쪽이 데이터를 읽는 속도가 느리고 오른쪽이 더 많은 데이터를 읽을 때까지 왼쪽이 쓰기 작업을 차단하지만(파이프 자체에는 파이프에 의해 관리되는 버퍼가 있음) 커널이 최대 크기는 더 작습니다. ).
동기화 지점을 관찰하려면 다음 명령을 따르십시오( sh -x
각 명령이 실행될 때 인쇄).
time sh -x -c '{ sleep 1; echo a; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { sleep 1; cat; }'
time sh -x -c '{ sleep 2; echo a; } | { cat; sleep 1; }'
관찰한 결과에 만족할 때까지 다양한 변형을 시도해보세요.
복합 명령이 주어지면
cat tmp | head -1 > tmp
왼쪽 프로세스는 다음을 수행합니다(설명과 관련된 단계만 나열했습니다).
cat
매개변수를 사용하여 외부 프로그램을 실행합니다tmp
.- 독서를 위해 열려 있습니다
tmp
. - 파일 끝에 도달하지 않은 경우 파일에서 블록을 읽고 표준 출력에 기록합니다.
오른쪽 프로세스는 다음을 수행합니다.
- 표준 출력을 로 리디렉션
tmp
하고 프로세스에서 파일을 자릅니다. head
매개변수를 사용하여 외부 프로그램을 실행합니다-1
.- 표준 입력에서 한 줄을 읽고 이를 표준 출력에 씁니다.
유일한 동기화 지점은 left-3이 전체 라인 처리를 완료할 때까지 기다리는 right-3입니다. left-2와 right-1 사이에는 동기화가 없으므로 어떤 순서로든 발생할 수 있습니다. 발생 순서는 예측할 수 없습니다. CPU 아키텍처, 셸, 커널, 프로세스가 예약되는 코어, 해당 시점에 CPU가 수신하는 인터럽트 등에 따라 다릅니다.
행동을 바꾸는 방법
시스템 설정을 변경하여 동작을 변경할 수 없습니다. 컴퓨터는 사용자의 지시를 따릅니다. 자르고 tmp
병렬로 읽으라고 지시 tmp
하면 두 가지 작업을 병렬로 수행합니다.
글쎄요, 변경할 수 있는 "시스템 설정"이 있습니다. /bin/bash
이를 bash가 아닌 다른 프로그램으로 교체할 수 있습니다. 이것이 좋은 생각이 아니라는 것은 말할 나위도 없는 일이길 바랍니다.
파이프 왼쪽 앞에서 잘림이 발생하도록 하려면 파이프 외부에 배치해야 합니다. 예를 들면 다음과 같습니다.
{ cat tmp | head -1; } >tmp
또는
( exec >tmp; cat tmp | head -1 )
왜 당신이 이것을 원하는지 모르겠습니다. 비어 있다는 것을 알고 있는 파일에서 읽는 이유는 무엇입니까?
대신 읽기가 완료된 후 출력 리디렉션(잘림 포함)이 발생하도록 하려면 cat
메모리에 데이터를 완전히 버퍼링해야 합니다.
line=$(cat tmp | head -1)
printf %s "$line" >tmp
또는 다른 파일에 쓴 다음 해당 위치로 이동하세요. 이는 일반적으로 스크립트에서 작업을 수행하는 안정적인 방법이며 파일이 원래 이름으로 표시되기 전에 완전히 작성된다는 장점이 있습니다.
cat tmp | head -1 >new && mv new tmp
이것더 많은 유틸리티컬렉션에는 이 작업을 수행하도록 특별히 설계된 이라는 프로그램이 포함되어 있습니다 sponge
.
cat tmp | head -1 | sponge tmp
문제를 자동으로 감지하는 방법
당신의 목표가 잘못 작성된 스크립트를 가져와 자동으로 무엇이 잘못되었는지 알아내는 것이라면 죄송합니다. 인생은 그렇게 간단하지 않습니다. 때때로 cat
잘림이 발생하기 전에 읽기가 완료되기 때문에 런타임 분석에서는 문제를 안정적으로 찾을 수 없습니다 . 정적 분석은 원칙적으로 질문 캡처의 단순화된 예를 통해 수행할 수 있습니다.주택 검사, 그러나 더 복잡한 스크립트에서는 유사한 문제를 포착하지 못할 수도 있습니다.
답변2
Giles의 답변은 경쟁 조건을 설명합니다. 이 부분만 답변드리고 싶습니다.
이 스크립트가 항상 0줄을 출력하도록 강제할 수 있는 방법이 있습니까? (따라서 tmp에 대한 I/O 리디렉션이 항상 먼저 준비되므로 데이터가 항상 손상됩니다.) 명확히 말하면 시스템 설정을 변경한다는 뜻입니다.
그러한 도구가 이미 있는지는 모르겠지만 구현 방법은 알고 있습니다. (단, 그렇지 않다는 점 참고해주세요.언제나0줄, 이와 같은 간단한 일치 항목을 쉽게 잡을 수 있는 유용한 테스터이며,일부더 복잡한 일치. 바라보다@Gilles의 댓글.) 스크립트가 안전하다는 것을 보장하지는 않습니다., 그러나 ARM과 같이 약한 순서의 비x86 CPU를 포함하여 다양한 CPU에서 멀티스레드 프로그램을 테스트하는 것과 유사하게 테스트에 유용한 도구가 될 수 있습니다.
이것을 다음과 같이 실행할 수 있습니다.racechecker bash foo.sh
strace -f
ltrace -f
각 하위 프로세스에 연결된 동일한 시스템 호출 추적/가로채기 도구를 사용합니다 . (리눅스에서도 마찬가지다.ptrace
GDB 및 기타 디버거에서 사용되는 시스템 호출중단점을 설정하고, 단일 단계를 수행하고, 다른 프로세스의 메모리/레지스터를 수정합니다. )
탐지 open
및 openat
시스템 호출: 이 도구에서 실행 중인 프로세스가시스템 open(2)
호출(또는 openat
)를 사용하여 O_RDONLY
약 1/2~1초 동안 잠을 자세요. 다른 open
시스템 호출(특히 include O_TRUNC
)이 즉시 실행되도록 합니다.
이렇게 하면 시스템 로드도 높지 않거나 다른 읽기가 끝날 때까지 잘림이 발생하지 않는 복잡한 경쟁 조건이 아닌 한 거의 모든 경쟁 조건에서 작성자가 승리할 수 있습니다. 그래서open()
s(s 또는 write일 수 있음 read()
)는 무작위 변경으로 인해 지연됩니다.이 도구의 감지 기능이 향상되지만, 실제로 현실 세계에서 접할 수 있는 모든 시나리오를 결국 포괄하는 지연 시간 시뮬레이터를 사용하여 무제한 시간 테스트 없이는 그렇게 할 수 없습니다.틀림없이스크립트를 주의 깊게 읽고 그렇지 않다는 것을 증명하지 않는 한 스크립트에는 경쟁이 없습니다.
프로세스 시작에 시간이 오래 걸리지 않도록 지연 open
대신 파일을 허용 목록에 추가하는 데 필요할 수 있습니다 . (동적 연결은 런타임 시 (보기 또는 가끔) 여러 파일에 연결되어야 하지만 상위 셸 자체가 잘림을 수행하는 경우에도 괜찮습니다. 그러나 스크립트를 비합리적으로 느리게 만들지 않는 것이 이 도구에 여전히 좋습니다.)/usr/bin
/usr/lib
open()
strace -eopen /bin/true
/bin/ls
또는 호출 프로세스가 처음에 자를 권한이 없는 모든 파일을 화이트리스트에 추가할 수도 있습니다. 즉, 추적 프로세스는 access(2)
파일이 필요한 프로세스를 실제로 일시 중단하기 전에 시스템 호출을 할 수 있습니다.open()
racechecker
코드 자체는 셸이 아닌 C로 작성되어야 하지만 strace
코드를 시작점으로 사용할 수 있으며 구현하는 데 많은 작업이 필요하지 않을 것입니다.
동일한 기능을 얻을 수도 있습니다.FUSE 파일 시스템 사용. 순수 패스스루 파일 시스템의 FUSE 예가 있을 수 있으므로 open()
읽기 전용 열기에서 잠자기 기능에 검사를 추가할 수 있지만 즉시 잘림이 발생합니다.