Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

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

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

표준 라이브러리가 제공하는 시스템 호출 래퍼는 EINTR로 실패하면 자동으로 재시도되어야 합니다. 그래야 애플리케이션 코드가 직접 재시도해야 하는 부담을 덜 수 있습니다.

여기서 시스템 호출이란 표준 C 라이브러리가 제공하는 함수 중 I/O나 그 밖의 시스템 자원 처리와 관련된 함수를 말합니다.

근거

인터럽트된 시스템 호출

POSIX 시스템에서는 시그널이 흔히 발생합니다. 시스템 호출을 하는 코드는 시그널을 처리할 준비가 되어 있어야 합니다. 시그널의 예는 다음과 같습니다.

  • 가장 흔한 시그널은 CTRL+c를 누를 때 전송되는 SIGINT입니다. 기본적으로 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 모듈은 소수에 불과하며, 하나의 모듈 전체에 수정이 적용되기까지 보통 수년이 걸렸습니다. InterruptedError 발생 시 file.read()를 재시도하는 코드 예:

while True:
    try:
        data = file.read(size)
        break
    except InterruptedError:
        continue

InterruptedError를 처리하는 표준 라이브러리의 Python 모듈 목록:

  • asyncio
  • asyncore
  • io, _pyio
  • multiprocessing
  • selectors
  • socket
  • socketserver
  • subprocess

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를 일으키는 것처럼, 예외를 일으키는 사용자 정의 시그널 핸들러를 설정하십시오.
  • select() 같은 I/O 다중화 함수를 Python의 시그널 웨이크업 파일 디스크립터와 함께 사용하십시오: signal.set_wakeup_fd() 함수를 참고하십시오.

이 사용 사례에서 기대하는 바는 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() 메서드는 논블로킹 소켓에서 connect()가 시그널에 의해 중단되면(EINTR로 실패하면) 재시도하지 않습니다. 연결은 백그라운드에서 비동기적으로 진행됩니다. 호출자는 소켓이 쓰기 가능한 상태가 될 때까지 기다린 다음(예: 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는 “제어 이벤트(control events)”를 사용합니다:

  • CTRL_BREAK_EVENT: Break (SIGBREAK)
  • CTRL_CLOSE_EVENT: 닫기 이벤트
  • CTRL_C_EVENT: CTRL+C (SIGINT)
  • CTRL_LOGOFF_EVENT: 로그오프
  • CTRL_SHUTDOWN_EVENT: 시스템 종료

SetConsoleCtrlHandler() 함수를 사용하여 제어 핸들러를 설치할 수 있습니다.

GenerateConsoleCtrlEvent() 함수를 사용하여 CTRL_C_EVENT와 CTRL_BREAK_EVENT 이벤트를 프로세스에 보낼 수 있습니다. 이 함수는 Python에서 os.kill()로 노출됩니다.

시그널

Windows에서는 다음 시그널이 지원됩니다:

  • SIGABRT
  • SIGBREAK (CTRL_BREAK_EVENT): Windows에서만 사용할 수 있는 시그널
  • SIGFPE
  • SIGILL
  • SIGINT (CTRL_C_EVENT)
  • SIGSEGV
  • SIGTERM

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 처리입니다.

미해결 문제:

해결된 문제:

시그널과 관련된 Python 이슈

미해결 문제:

해결된 문제:

구현

구현은 이슈 23285에서 추적합니다. 2015년 2월 7일에 커밋되었습니다.