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 模块列表:
asyncioasyncoreio,_pyio多进程selectorssocketsocketserversubprocess
其他编程语言,如 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 键盘快捷键立即退出程序。
此外,一些信号不重要,不应干扰应用程序。只有在处理*某些*信号时才中断应用程序,有两种选择:
- 设置一个自定义信号处理程序,该程序会引发异常,例如
SIGINT的KeyboardInterrupt。 - 使用 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.close、close() 方法和 os.dup2() 是特殊情况:它们将忽略 EINTR 错误而不是重试。原因很复杂,但涉及 Linux 下的行为以及即使返回 EINTR,文件描述符也可能确实已关闭的事实。参见文章:
- Returning EINTR from close()
- (LKML) Re: [patch 7/7] uml: retry host close() on EINTR
- close() and 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_EVENT 和 CTRL_BREAK_EVENT 事件可以使用 GenerateConsoleCtrlEvent() 函数发送到进程。此函数在 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 对象。因此,睡眠可以被 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