공유 파일 설명자(STDOUT/STDERR을 나타냄)가 있는 상위 프로세스에서 분기된 여러 프로세스가 있는 경우 프로세스 중 하나가 STDOUT에 쓰고 ~64K 버퍼를 초과하면 예상대로 차단됩니다. 다른 프로세스의 모든 공유 파일 설명자가 닫히면 프로세스는 차단을 해제하고 계속해서 STDOUT에 씁니다.
공유 파일 설명자를 닫는 행위로 인해 쓰기 차단된 프로세스가 어떻게 차단 해제되나요? (버퍼가 플러시되고 있다고 가정하지만 이에 대한 증거를 찾을 수 없습니다)
재현을 위해 상태를 설정하는 두 개의 스크립트가 있습니다. 내 목표는 문제를 해결하는 것이 아니라 이러한 설명자를 닫는 행위로 인해 차단된 프로세스가 어떻게 계속되는지 이해하는 것입니다. (즉, 의도는아니요Python 또는 하위 프로세스 문제 해결)
파일:A.py
#!/usr/bin/env python2.6
import subprocess
if __name__ == "__main__":
subprocess.Popen("./B.sh 70000", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
subprocess.Popen("./B.sh 100", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
subprocess.Popen("./B.sh 100", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
파일: B.sh
#!/usr/bin/env bash
for i in `seq 1 $1`; do
echo -n "#"
done
echo ""
while true; do
echo > /dev/null
done
Python 2.6의 subprocess.Popen은 상태를 설정하는 수단으로 사용됩니다. 그 아래에서 파이프 및 포크(아마도 그 순서가 아닐 수도 있음)는 현재 프로세스가 포크된 각 프로세스에 대한 파일 설명자를 복사하여 공유 파일 설명자가 있는 프로세스 체인을 생성합니다.
쉘 스크립트는 B.sh
단지 데이터를 STDOUT으로 출력한 다음 루프합니다(htop과 같은 것에서 실행 중 상태와 절전 상태를 구별할 수 있도록 의도적으로 절전 모드를 해제함).
두 스크립트를 동일한 작업 디렉터리에 넣고 A.py를 실행하여 동작을 복제합니다(CentOS 6.7이지만 CentOS 6.X 버전이 복제될지는 의심스럽습니다).
참고로 다음은 공유 상태를 보여주는 프로세스 파일 설명자의 디렉터리 목록입니다.
# Process 1: ./B.sh 70000
ls -la /proc/4144/fd
total 0
lrwx------ 1 root root 64 Sep 28 14:18 0 -> /dev/pts/0
l-wx------ 1 root root 64 Sep 28 14:18 1 -> pipe:[53061]
l-wx------ 1 root root 64 Sep 28 14:18 2 -> pipe:[53061]
lr-x------ 1 root root 64 Sep 28 14:18 255 -> /root/B.sh
# Process 2: ./B.sh 100
ls -la /proc/4145/fd
total 0
lrwx------ 1 root root 64 Sep 28 14:18 0 -> /dev/pts/0
l-wx------ 1 root root 64 Sep 28 14:18 1 -> pipe:[53063]
l-wx------ 1 root root 64 Sep 28 14:18 2 -> pipe:[53063]
lr-x------ 1 root root 64 Sep 28 14:18 255 -> /root/B.sh
lr-x------ 1 root root 64 Sep 28 14:18 3 -> pipe:[53061]
# Process 3: ./B.sh 100
ls -la /proc/4146/fd
total 0
lrwx------ 1 root root 64 Sep 28 14:18 0 -> /dev/pts/0
l-wx------ 1 root root 64 Sep 28 14:18 1 -> pipe:[53065]
l-wx------ 1 root root 64 Sep 28 14:24 10 -> pipe:[53065]
l-wx------ 1 root root 64 Sep 28 14:18 2 -> pipe:[53065]
lr-x------ 1 root root 64 Sep 28 14:18 255 -> /root/B.sh
lr-x------ 1 root root 64 Sep 28 14:18 3 -> pipe:[53061]
lr-x------ 1 root root 64 Sep 28 14:18 4 -> pipe:[53063]
생성된 첫 번째 프로세스(프로세스 1)는 ~>64K 데이터를 STDOUT으로 출력하여 쓰기 중에 차단되기 때문에 절전 모드로 전환합니다(htop 및 pid에 strace 추가에서 볼 수 있음).
두 번째 및 세 번째 프로세스(각각 프로세스 2 및 프로세스 3)는 계속 실행되고 프로세스 1의 일부로 설정된 파이프를 참조하는 중복 파일 설명자가 있습니다.
프로세스 2 또는 프로세스 3을 종료하고 프로세스 1은 여전히 자고 있습니다. 둘 다 종료하면 프로세스 1이 잠금 해제되고(왜?) 실행 상태로 전환됩니다.
테스트를 다시 시작하고 gdb
프로세스 2 또는 프로세스 3에 연결을 사용한 다음 p close(#)
프로세스 1이 아직 대기 중인 동안 프로세스 1과 공유된 파일 설명자를 닫습니다. 다른 프로세스에 연결하고 공유 설명자를 닫으면 프로세스 1이 차단을 해제하고 실행 상태로 들어갑니다.
따라서 차단된 프로세스와 공유된 모든 설명자를 닫는 행위로 인해 차단이 해제됩니다. 이 상황에서 이전에 쓰기 차단된 프로세스가 해제되는 원인은 무엇입니까?
답변1
파이프의 읽기 끝이 닫히면 쓰기 시도에 오류가 발생합니다. SIGPIPE를 사용하여 프로세스를 종료합니다. 또는 신호가 차단되면 쓰기가 즉시 반환됩니다 errno == EPIPE
. 이것은 당신의 행동을 설명해야합니다. 이는 UNIX 파이프의 원래 기능 중 하나입니다.
이는 파이프의 읽기 끝 부분에 대한 마지막 남은 참조가 닫힐 때 발생합니다. 예를 들어 에서 다른 참조가 있을 수 있습니다 dup()
.
귀하의 경우 fork()
새 프로세스를 생성하여 하위 프로세스가 모두 동일한 파일 설명자로 시작됩니다. 파이프를 닫으려면 상위 및 하위 파일 설명자를 닫아야 합니다. 상위 close()
파일 설명자는 하위 파일 설명자에 영향을 주지 않으며 그 반대도 마찬가지입니다.
이는 참조 카운팅의 일반적인 개념의 예입니다. 커널은 파이프의 읽기 끝을 참조하는 파일 설명자 수를 추적합니다. 호출할 때마다 개수가 1씩 감소합니다 close()
. 개수가 0으로 떨어지면 커널은 적절한 정리 기능을 실행합니다. Linux 커널에서는 이를 함수 포인터라고 합니다. .release
연관된 모든 리소스를 해제하기 때문입니다.
참조 계산 시스템은 UNIX 파일 설명자에 매우 중요합니다. 예를 들어 다음을 찾을 수 있습니다.dup() 및 포크()연구에는 UNIX V5가 사용되었습니다.
python2.6으로 시작하는 하위 프로세스에서 SIGPIPE가 차단되는 이유를 알고 싶다면 다음을 참조하세요.https://bugs.python.org/issue1652.
파이프 FD가 P2와 P3으로 새고 있다는 사실에 놀랐다면 다음을 참조하세요.https://bugs.python.org/issue7213. 즉, 더 많은 정보를 바탕으로 동작을 얻으려면 Popen()
pass 를 하면 됩니다 close_fds=True
.
그렇지 않으면 만약 당신이생각하다pass_fds
특정 추가 FD를 P2와 P3에 전달하려면 매개변수를 사용하여 이를 명시적으로 만들고 싶습니다 .
나는 당신이 실제로 이것을 하고 싶어한다고 가정하고 있습니다. 그렇지 않으면 이 예제 프로그램이 무엇을 해야하는지 정말로 이해하지 못합니다. 하위 프로세스 개체를 삭제한 다음 종료합니다. 따라서 상위 프로세스는 적어도 종료될 때 파이프 FD를 닫습니다.
특정 버전의 Python에 의존할 것 같은 세부 사항에 의존하지 않고 이를 셸에서 재현할 수 있습니다.
$ strace -f sh -c 'cat </dev/zero | { sleep 1& sleep 2& }'
...
[pid 26477] read(0, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 131072) = 131072
[pid 26477] write(1, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 131072 <unfinished ...>
...
[pid 26480] nanosleep({tv_sec=2, tv_nsec=0}, <unfinished ...>
...
[pid 26479] nanosleep({tv_sec=1, tv_nsec=0}, NULL) = 0
...
[pid 26479] +++ exited with 0 +++
[pid 26480] <... nanosleep resumed> NULL) = 0
...
[pid 26480] +++ exited with 0 +++
[pid 26477] <... write resumed> ) = 65536
[pid 26477] --- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=26477, si_uid=1001} ---
[pid 26477] +++ killed by SIGPIPE +++
나는 여기서 이전에 생각하지 못했던 세부 사항을 발견했습니다. write()
파이프 버퍼에 64K를 쓰는 데 성공했다는 결과만 반환됩니다. 호출자가 SIGPIPE의 기본 종료를 비활성화하면 어떻게 되나요? "짧은 쓰기"는 재시도를 통해 파이프나 소켓에서 허용해야 하는 경우가 많습니다. 예를 들어, 프로세스가 관련 없는 신호를 수신하고 해당 신호에 대해 핸들러 기능이 설정된 경우 이런 일이 발생할 수 있습니다. 따라서 호출자는 write()
남은 데이터를 계속해서 재시도해야 하며,저것 write()
호출이 즉시 반환됩니다 errno == EPIPE
.