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

Python 增强提案

PEP 475 – 对因 EINTR 失败的系统调用进行重试

作者:
Charles-François Natali <cf.natali at gmail.com>, Victor Stinner <vstinner at python.org>
BDFL-代表:
Antoine Pitrou <solipsis at pitrou.net>
状态:
最终
类型:
标准跟踪
创建:
2014年7月29日
Python 版本:
3.5
决议:
Python-Dev 消息

目录

摘要

标准库中提供的系统调用包装器在使用EINTR失败时应自动重试,以减轻应用程序代码执行此操作的负担。

系统调用是指标准 C 库公开的与 I/O 或其他系统资源处理相关的函数。

基本原理

中断的系统调用

在 POSIX 系统上,信号很常见。调用系统调用的代码必须准备好处理它们。信号示例

  • 最常见的信号是SIGINT,即按下 CTRL+c 时发送的信号。默认情况下,当接收到此信号时,Python 会引发KeyboardInterrupt异常。
  • 在运行子进程时,当子进程退出时会发送SIGCHLD信号。
  • 调整终端大小会向在终端中运行的应用程序发送SIGWINCH信号。
  • 将应用程序置于后台(例如:按下 CTRL-z,然后输入bg命令)会发送SIGCONT信号。

编写 C 信号处理程序很困难:只能调用“异步信号安全”函数(例如,printf()malloc()不是异步信号安全的),并且存在重入问题。因此,当进程在执行系统调用期间接收到信号时,系统调用可能会使用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 模块列表

  • 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() 和 EINTR

下面的与 EINTR 相关的 Python 问题部分提供了由EINTR引起的错误示例。

此用例中的期望是 Python 隐藏InterruptedError并自动重试系统调用。

用例 2:尽快收到信号通知

但是,有时您希望收到某些信号并希望尽快处理它们。例如,您可能希望立即使用CTRL+c键盘快捷键退出程序。

此外,某些信号不重要,不应中断应用程序。有两个选项可以仅对某些信号中断应用程序

  • 设置一个自定义信号处理程序,该处理程序引发异常,例如SIGINTKeyboardInterrupt
  • 将 I/O 多路复用函数(如select())与 Python 的信号唤醒文件描述符一起使用:请参阅函数signal.set_wakeup_fd()

此用例中的期望是 Python 信号处理程序能够及时执行,并且如果处理程序引发异常,则系统调用失败 - 否则重新启动。

提案

此 PEP 提出在最低级别(即在 stdlib 提供的包装器中,而不是更高级别的库和应用程序中)处理 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.closeclose()方法和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异常不应该再出现。因此,可以移除在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 提供了PyErr_SetInterrupt()函数,该函数调用SIGINT信号处理程序以中断主 Python 线程。

Windows 上的信号

控制事件

Windows 使用“控制事件”

  • CTRL_BREAK_EVENT: 中断(SIGBREAK)
  • CTRL_CLOSE_EVENT: 关闭事件
  • CTRL_C_EVENT: CTRL+C(SIGINT)
  • CTRL_LOGOFF_EVENT: 注销
  • CTRL_SHUTDOWN_EVENT: 关机

SetConsoleCtrlHandler() 函数可用于安装控制处理程序。

CTRL_C_EVENTCTRL_BREAK_EVENT事件可以使用GenerateConsoleCtrlEvent() 函数发送到进程。此函数在 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对象。因此,睡眠可以被SIGINT中断。

_winapi.WaitForMultipleObjects()自动将sigint_event添加到监视句柄列表中,因此它也可以被中断。

PyOS_StdioReadline()fgets()失败时也使用sigint_event来检查是否按下了 Ctrl-C 或 Ctrl-Z。

实现

该实现跟踪在issue 23285中。它已于 2015 年 2 月 7 日提交。


来源:https://github.com/python/peps/blob/main/peps/pep-0475.rst

上次修改时间:2023-09-09 17:39:29 GMT