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
键盘快捷键退出程序。
此外,某些信号不重要,不应中断应用程序。有两个选项可以仅对某些信号中断应用程序
- 设置一个自定义信号处理程序,该处理程序引发异常,例如
SIGINT
的KeyboardInterrupt
。 - 将 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.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
异常不应该再出现。因此,可以移除在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 上受支持
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