PEP 475 – EINTR로 실패하는 시스템 호출 재시도
- Author:
- Charles-François Natali <cf.natali at gmail.com>, Victor Stinner <vstinner at python.org>
- BDFL-Delegate:
- Antoine Pitrou <solipsis at pitrou.net>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 29-Jul-2014
- Python-Version:
- 3.5
- Resolution:
- Python-Dev message
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
표준 라이브러리가 제공하는 시스템 호출 래퍼는 EINTR로 실패할 때 자동으로 재시도되어야 하며, 애플리케이션 코드가 이를 수행해야 하는 부담을 덜어야 합니다.
시스템 호출이라고 할 때는 I/O 또는 기타 시스템 리소스 처리를 담당하는 표준 C 라이브러리가 노출하는 함수를 의미합니다.
근거
중단된 시스템 호출
POSIX 시스템에서는 시그널이 흔히 발생합니다. 시스템 호출을 수행하는 코드는 시그널을 처리할 준비가 되어 있어야 합니다. 시그널의 예는 다음과 같습니다.
- 가장 흔한 시그널은
SIGINT이며, CTRL+c를 눌렀을 때 전송되는 시그널입니다. 기본적으로 Python은 이 시그널을 수신하면KeyboardInterrupt예외를 발생시킵니다. - 서브프로세스를 실행할 때 자식 프로세스가 종료되면
SIGCHLD시그널이 전송됩니다. - 터미널 크기를 조정하면 해당 터미널에서 실행 중인 애플리케이션에
SIGWINCH시그널이 전송됩니다. - 애플리케이션을 백그라운드로 전환하면(예: CTRL-z를 누른 다음
bg명령을 입력하면)SIGCONT시그널이 전송됩니다.
C 시그널 핸들러를 작성하는 일은 어렵습니다. “async-signal-safe” 함수만 호출할 수 있으며(예를 들어 printf()와 malloc()은 async-signal-safe하지 않음), 재진입성 문제도 있습니다. 따라서 시스템 호출을 실행하는 동안 프로세스가 시그널을 수신하면, 시그널 안전 함수에 대한 제한 없이 프로그램이 시그널을 처리할 기회를 제공하기 위해 시스템 호출이 EINTR 오류와 함께 실패할 수 있습니다.
이 동작은 시스템에 따라 다릅니다. 일부 시스템에서는 SA_RESTART 플래그를 사용하면 시스템 호출이 EINTR과 함께 실패하는 대신 자동으로 재시도됩니다. 그럼에도 Python의 signal.signal()함수는 시그널 핸들러를 설정할 때 SA_RESTART 플래그를 해제하므로, Python에서는 모든 시스템 호출이 EINTR과 함께 실패할 가능성이 큽니다.
시그널 수신은 예외적인 일이 아니므로, 견고한 POSIX 코드는 EINTR을 처리할 준비가 되어 있어야 합니다(대부분의 경우 호출이 결국 성공하기를 기대하며 루프에서 재시도해야 한다는 의미입니다). Python의 특별한 지원이 없다면 애플리케이션 코드는 필요 이상으로 훨씬 장황해질 수 있습니다.
Python 3.4에서의 상태
Python 3.4에서는 InterruptedError예외(EINTR전용 예외 클래스) 처리가 각 호출 지점에서 사례별로 중복됩니다. 실제로 이 예외를 처리하는 Python 모듈은 소수에 불과하며, 수정 사항이 전체 모듈을 포괄하기까지 보통 수년이 걸렸습니다. file.read()를 InterruptedError에 대해 재시도하는 코드의 예:
while True:
try:
data = file.read(size)
break
except InterruptedError:
continue
InterruptedError를 처리하는 표준 라이브러리의 Python 모듈 목록:
asyncioasyncoreio,_pyiomultiprocessingselectorssocketsocketserversubprocess
Perl, Java, Go와 같은 다른 프로그래밍 언어는 EINTR로 실패한 시스템 호출을 더 낮은 수준에서 재시도하므로 라이브러리와 애플리케이션이 이를 신경 쓸 필요가 없습니다.
사용 사례 1: 시그널은 신경 쓰지 않기
대부분의 경우 시그널로 인해 중단되기를 원하지 않으며 InterruptedError 예외가 발생할 것으로 예상하지도 않습니다. 예를 들어, “Hello World” 예제를 위해 정말 그렇게 복잡한 코드를 작성하고 싶습니까?
while True:
try:
print("Hello World")
break
except InterruptedError:
continue
InterruptedError는 예상하지 못한 곳에서 발생할 수 있습니다. 예를 들어, os.close()와 FileIO.close()는 InterruptedError를 발생시킬 수 있습니다. close() and EINTR 문서를 참조하십시오.
아래의 Python issues related to EINTR 섹션에서는 EINTR로 인해 발생한 버그의 예를 제시합니다.
이 사용 사례에서는 Python이 InterruptedError를 숨기고 시스템 호출을 자동으로 재시도할 것으로 예상합니다.
사용 사례 2: 가능한 한 빨리 시그널 알림 받기
그러나 때로는 일부 시그널을 예상하며 가능한 한 빨리 처리하고 싶을 수 있습니다. 예를 들어, CTRL+c 키보드 단축키를 사용하여 프로그램을 즉시 종료하고 싶을 수 있습니다.
또한 일부 시그널은 중요하지 않으며 애플리케이션을 중단해서는 안 됩니다. 일부 시그널에 대해서만 애플리케이션을 중단하는 방법은 두 가지가 있습니다.
- 예외를 발생시키는 사용자 지정 시그널 핸들러를 설정하십시오. 예를 들어
SIGINT에는KeyboardInterrupt를 사용하십시오. signal.set_wakeup_fd()함수를 참조하여, Python의 시그널 웨이크업 파일 디스크립터와 함께select()와 같은 I/O 다중화 함수를 사용하십시오.
이 사용 사례에서는 Python 시그널 핸들러가 적시에 실행되고, 핸들러가 예외를 발생시키면 시스템 호출이 실패하며, 그렇지 않으면 시스템 호출이 재시작될 것으로 예상합니다.
제안
이 PEP에서는 EINTR 및 재시도를 가장 낮은 수준, 즉 상위 수준의 라이브러리와 애플리케이션이 아니라 표준 라이브러리가 제공하는 래퍼에서 처리할 것을 제안합니다.
구체적으로 시스템 호출이 EINTR로 실패하면 Python 래퍼는 (PyErr_CheckSignals()를 사용하여) 지정된 시그널 핸들러를 호출해야 합니다. 시그널 핸들러가 예외를 발생시키면 Python 래퍼는 중단하고 해당 예외와 함께 실패해야 합니다.
시그널 핸들러가 성공적으로 반환되면 Python 래퍼는 시스템 호출을 자동으로 재시도합니다. 시스템 호출에 시간 제한 매개변수가 포함된 경우 시간 제한을 다시 계산합니다.
수정된 함수
이 PEP를 준수하도록 수정해야 하는 표준 라이브러리 함수의 예는 다음과 같습니다.
open(),os.open(),io.open()faulthandler모듈의 함수os함수:os.fchdir()os.fchmod()os.fchown()os.fdatasync()os.fstat()os.fstatvfs()os.fsync()os.ftruncate()os.mkfifo()os.mknod()os.posix_fadvise()os.posix_fallocate()os.pread()os.pwrite()os.read()os.readv()os.sendfile()os.wait3()os.wait4()os.wait()os.waitid()os.waitpid()os.write()os.writev()- 특수 사례:
os.close()및os.dup2()는 이제EINTR오류를 무시하며, 시스템 호출을 재시도하지 않습니다.
select.select(),select.poll.poll(),select.epoll.poll(),select.kqueue.control(),select.devpoll.poll()socket.socket()메서드:accept()connect()(비차단 소켓 제외)recv()recvfrom()recvmsg()send()sendall()sendmsg()sendto()
signal.sigtimedwait(),signal.sigwaitinfo()time.sleep()
(참고: selector 모듈은 이미 InterruptedError에 대해 재시도하지만, 아직 타임아웃을 재계산하지는 않습니다)
os.close, close() 메서드 및 os.dup2()는 특수한 경우입니다: 재시도하는 대신 EINTR를 무시합니다. 이유는 복잡하지만 Linux에서의 동작과 EINTR이 반환되더라도 파일 디스크립터가 실제로 닫혀 있을 수 있다는 사실이 관련되어 있습니다. 다음 문서를 참조하십시오:
socket.socket.connect() 메서드는 신호에 의해 중단되어 EINTR로 실패하는 경우 비블로킹 소켓에 대해 connect()를 재시도하지 않습니다. 연결은 백그라운드에서 비동기적으로 실행됩니다. 호출자는 소켓이 쓰기 가능해질 때까지 기다린 다음(예: select.select()를 사용하여) socket.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)를 호출하여 연결이 성공했는지(getsockopt()가 0을 반환하는지) 또는 실패했는지 확인해야 합니다.
InterruptedError 처리
중단된 시스템 호출은 자동으로 재시도되므로, 해당 시스템 호출을 호출할 때 InterruptedError 예외가 더 이상 발생하지 않아야 합니다. 따라서 Status in Python 3.4에서 설명된 InterruptedError의 수동 처리는 제거할 수 있으며, 이를 통해 표준 라이브러리 코드가 간소화됩니다.
하위 호환성
시스템 호출이 InterruptedError와 함께 중단된다는 사실에 의존하는 애플리케이션은 멈추게 됩니다. 이 PEP의 저자들은 그러한 애플리케이션이 존재한다고 생각하지 않습니다. 그러한 애플리케이션은 경합 상태와 같은 다른 문제에 노출되기 때문입니다(시스템 호출 전에 신호가 도착하면 교착 상태가 발생할 가능성이 있습니다). 게다가 그러한 코드는 이식성이 없습니다.
어떤 경우든 이러한 애플리케이션은 모든 플랫폼과 모든 Python 버전에서 신뢰할 수 있게 동작하도록 신호를 다르게 처리하도록 수정해야 합니다. 가능한 전략은 잘 정의된 예외를 발생시키는 신호 처리기를 설정하거나 웨이크업 파일 디스크립터를 사용하는 것입니다.
이벤트 루프를 사용하는 애플리케이션에서는 신호를 처리할 때 signal.set_wakeup_fd()를 사용하는 것이 권장되는 방법입니다. Python의 저수준 신호 처리기는 파일 디스크립터에 신호 번호를 기록하고, 이벤트 루프는 이를 읽기 위해 깨어납니다. 이벤트 루프는 신호 처리기의 제약 없이 해당 신호를 처리할 수 있습니다(예를 들어 루프는 주 스레드뿐만 아니라 모든 스레드에서 깨어날 수 있습니다).
부록
웨이크업 파일 디스크립터
Python 3.3부터 signal.set_wakeup_fd()는 파일 디스크립터에 신호 번호를 기록하지만, 이전에는 널 바이트만 기록했습니다. 웨이크업 파일 디스크립터를 사용하여 신호를 구별할 수 있게 됩니다.
Linux에는 각 신호에 대한 더 많은 정보를 제공하는 signalfd() 시스템 호출이 있습니다. 예를 들어 신호를 보낸 pid와 uid를 알 수 있습니다. 이 함수는 아직 Python에 노출되지 않았습니다(issue 12304 참조).
Unix에서 asyncio 모듈은 웨이크업 파일 디스크립터를 사용하여 이벤트 루프를 깨웁니다.
멀티스레딩
C 시그널 핸들러는 모든 스레드에서 호출될 수 있지만, Python 시그널 핸들러는 항상 주 Python 스레드에서 호출됩니다.
Python의 C API는 주 Python 스레드를 중단하기 위해 SIGINT 시그널 핸들러를 호출하는 PyErr_SetInterrupt() 함수를 제공합니다.
Windows의 시그널
제어 이벤트
Windows는 “제어 이벤트”를 사용합니다.
CTRL_BREAK_EVENT: 중단 (SIGBREAK)CTRL_CLOSE_EVENT: 닫기 이벤트CTRL_C_EVENT: CTRL+C (SIGINT)CTRL_LOGOFF_EVENT: 로그오프CTRL_SHUTDOWN_EVENT: 종료
SetConsoleCtrlHandler() function을 사용하여 제어 핸들러를 설치할 수 있습니다.
GenerateConsoleCtrlEvent() function을 사용하여 CTRL_C_EVENT 및 CTRL_BREAK_EVENT 이벤트를 프로세스에 보낼 수 있습니다. 이 함수는 Python에서 os.kill()로 노출됩니다.
시그널
다음 시그널이 Windows에서 지원됩니다.
SIGABRTSIGBREAK(CTRL_BREAK_EVENT): Windows에서만 사용할 수 있는 시그널입니다.SIGFPESIGILLSIGINT(CTRL_C_EVENT)SIGSEGVSIGTERM
SIGINT
SIGINT에 대한 기본 Python 시그널 핸들러는 Windows 이벤트 객체인 sigint_event를 설정합니다.
time.sleep()은 WaitForSingleObjectEx()를 사용하여 구현되며, time.sleep() 매개변수를 시간 제한으로 사용하여 sigint_event 객체를 기다립니다. 따라서 sleep은 SIGINT에 의해 중단될 수 있습니다.
_winapi.WaitForMultipleObjects()는 감시되는 핸들 목록에 sigint_event를 자동으로 추가하므로, 이 함수도 중단될 수 있습니다.
PyOS_StdioReadline()도 fgets()가 실패했을 때 Ctrl-C 또는 Ctrl-Z가 눌렸는지 확인하기 위해 sigint_event를 사용했습니다.
링크
기타
EINTR 관련 Python 이슈
주요 이슈는 표준 라이브러리에서 EINTR 처리입니다.
미해결 이슈:
- 새로운 signal.set_wakeup_socket() 함수 추가
- signal.set_wakeup_fd(fd): fd를 비차단 모드로 설정
- 타임아웃 계산에 단조 시계 사용
- OS X의 sys.stdout.write가 EINTR에 안전하지 않음
- platform.uname()이 EINTR에 안전하지 않음
- asyncore가 recv, send, connect, accept에서 EINTR을 처리하지 못함,
- socket.create_connection()이 EINTR을 제대로 처리하지 못함
종료된 이슈:
- 중단된 시스템 호출을 재시도하지 않음
- Solaris: telnetlib의 select/socket 호출에서 EINTR 예외 발생
- subprocess: 일부 경우에 Popen.communicate()가 EINTR을 처리하지 못함
- multiprocessing.util._eintr_retry가 타임아웃을 다시 계산하지 않음
- file의 readline, readlines 및 readall 메서드가 EINTR 발생 시 데이터를 잃을 수 있음
- multiprocessing BaseManager의 serve_client()가 recv에서 EINTR을 확인하지 않음
- EINTR 발생 시 selectors의 동작이 문서화되지 않음
- asyncio: SA_RESTART로 EINTR 발생 횟수 제한
- smtplib.py의 socket.create_connection()도 EINTR을 제대로 처리하지 못함
- Parser/myreadline.c의 잘못된 RESTART/EINTR 처리
- test_httpservers 간헐적 실패, test_post 및 EINTR
- Linux에서 os.spawnv(P_WAIT, …)가 EINTR을 처리하지 못함
- pol에서 EINTR이 발생하면 asyncore가 실패함
- file.write 및 file.read가 EINTR을 처리하지 못함
- socket.readline() 인터페이스가 EINTR을 제대로 처리하지 못함
- subprocess가 EINTR에 안전하지 않음
- SocketServer가 시스템 호출 중단을 처리하지 못함
- read()가 중단될 때 subprocess에서 교착 상태 발생
- time.sleep(1): sleep이 중단되면 PyErr_CheckSignals() 호출
- 신호를 수신하면 flag=False인 siginterrupt가 재설정됨
- need siginterrupt() on Linux - impossible to do timeouts
- [Windows] Can not interrupt time.sleep()
시그널 관련 파이썬 이슈
미해결 이슈:
- signal.default_int_handler should set signal number on the raised exception
- expose signalfd(2) in the signal module
- missing return in win32_kill?
- Interrupts are lost during readline PyOS_InputHook processing
- cannot catch KeyboardInterrupt when using curses getkey()
- Deferred KeyboardInterrupt in interactive mode
해결된 이슈:
구현
구현 사항은 issue 23285에서 추적되고 있습니다. 이는 2015년 2월 7일에 커밋되었습니다.
Copyright
This document has been placed in the public domain.