Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

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 模块处理此异常,并且修复通常需要几年才能覆盖整个模块。在 InterruptedError 上重试 file.read() 的代码示例:

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

标准库中处理 InterruptedError 的 Python 模块列表:

  • asyncio
  • asyncore
  • io, _pyio
  • 多进程
  • 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

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

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

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

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

此外,一些信号不重要,不应干扰应用程序。只有在处理*某些*信号时才中断应用程序,有两种选择:

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

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

提案

本 PEP 建议在最低级别处理 EINTR 和重试,即在 stdlib 提供的封装器中(而不是更高级别的库和应用程序)。

具体来说,当系统调用因 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() 也使用 sigint_event,当 fgets() 失败时检查是否按下了 Ctrl-C 或 Ctrl-Z。

实施

该实现记录在 issue 23285 中。它于 2015 年 2 月 7 日提交。


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

最后修改:2025-02-01 08:59:27 GMT