PEP 475 – Retry system calls failing with 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
Abstract
System call wrappers provided in the standard library should be retried
automatically when they fail with EINTR
, to relieve application code
from the burden of doing so.
By system calls, we mean the functions exposed by the standard C library pertaining to I/O or handling of other system resources.
Rationale
Interrupted system calls
On POSIX systems, signals are common. Code calling system calls must be prepared to handle them. Examples of signals:
- The most common signal is
SIGINT
, the signal sent when CTRL+c is pressed. By default, Python raises aKeyboardInterrupt
exception when this signal is received. - When running subprocesses, the
SIGCHLD
signal is sent when a child process exits. - Resizing the terminal sends the
SIGWINCH
signal to the applications running in the terminal. - Putting the application in background (ex: press CTRL-z and then
type the
bg
command) sends theSIGCONT
signal.
Writing a C signal handler is difficult: only “async-signal-safe”
functions can be called (for example, printf()
and malloc()
are not async-signal safe), and there are issues with reentrancy.
Therefore, when a signal is received by a process during the execution
of a system call, the system call can fail with the EINTR
error to
give the program an opportunity to handle the signal without the
restriction on signal-safe functions.
This behaviour is system-dependent: on certain systems, using the
SA_RESTART
flag, some system calls are retried automatically instead
of failing with EINTR
. Regardless, Python’s signal.signal()
function clears the SA_RESTART
flag when setting the signal handler:
all system calls will probably fail with EINTR
in Python.
Since receiving a signal is a non-exceptional occurrence, robust POSIX code
must be prepared to handle EINTR
(which, in most cases, means
retry in a loop in the hope that the call eventually succeeds).
Without special support from Python, this can make application code
much more verbose than it needs to be.
Status in Python 3.4
In Python 3.4, handling the InterruptedError
exception (EINTR
’s
dedicated exception class) is duplicated at every call site on a case-by-case
basis. Only a few Python modules actually handle this exception,
and fixes usually took several years to cover a whole module. Example of
code retrying file.read()
on InterruptedError
:
while True:
try:
data = file.read(size)
break
except InterruptedError:
continue
List of Python modules in the standard library which handle
InterruptedError
:
asyncio
asyncore
io
,_pyio
multiprocessing
selectors
socket
socketserver
subprocess
Other programming languages like Perl, Java and Go retry system calls
failing with EINTR
at a lower level, so that libraries and applications
needn’t bother.
Use Case 1: Don’t Bother With Signals
In most cases, you don’t want to be interrupted by signals and you
don’t expect to get InterruptedError
exceptions. For example, do
you really want to write such complex code for a “Hello World”
example?
while True:
try:
print("Hello World")
break
except InterruptedError:
continue
InterruptedError
can happen in unexpected places. For example,
os.close()
and FileIO.close()
may raise InterruptedError
:
see the article close() and EINTR.
The Python issues related to EINTR section below gives examples of
bugs caused by EINTR
.
The expectation in this use case is that Python hides the
InterruptedError
and retries system calls automatically.
Use Case 2: Be notified of signals as soon as possible
Sometimes yet, you expect some signals and you want to handle them as
soon as possible. For example, you may want to immediately quit a
program using the CTRL+c
keyboard shortcut.
Besides, some signals are not interesting and should not disrupt the application. There are two options to interrupt an application on only some signals:
- Set up a custom signal handler which raises an exception, such as
KeyboardInterrupt
forSIGINT
. - Use a I/O multiplexing function like
select()
together with Python’s signal wakeup file descriptor: see the functionsignal.set_wakeup_fd()
.
The expectation in this use case is for the Python signal handler to be executed timely, and the system call to fail if the handler raised an exception – otherwise restart.
Proposal
This PEP proposes to handle EINTR and retries at the lowest level, i.e. in the wrappers provided by the stdlib (as opposed to higher-level libraries and applications).
Specifically, when a system call fails with EINTR
, its Python wrapper
must call the given signal handler (using PyErr_CheckSignals()
).
If the signal handler raises an exception, the Python wrapper bails out
and fails with the exception.
If the signal handler returns successfully, the Python wrapper retries the system call automatically. If the system call involves a timeout parameter, the timeout is recomputed.
Modified functions
Example of standard library functions that need to be modified to comply with this PEP:
open()
,os.open()
,io.open()
- functions of the
faulthandler
module os
functions: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()
- special cases:
os.close()
andos.dup2()
now ignoreEINTR
error, the syscall is not retried
select.select()
,select.poll.poll()
,select.epoll.poll()
,select.kqueue.control()
,select.devpoll.poll()
socket.socket()
methods:accept()
connect()
(except for non-blocking sockets)recv()
recvfrom()
recvmsg()
send()
sendall()
sendmsg()
sendto()
signal.sigtimedwait()
,signal.sigwaitinfo()
time.sleep()
(Note: the selector
module already retries on InterruptedError
, but it
doesn’t recompute the timeout yet)
os.close
, close()
methods and os.dup2()
are a special case: they
will ignore EINTR
instead of retrying. The reason is complex but involves
behaviour under Linux and the fact that the file descriptor may really be
closed even if EINTR is returned. See articles:
- Returning EINTR from close()
- (LKML) Re: [patch 7/7] uml: retry host close() on EINTR
- close() and EINTR
The socket.socket.connect()
method does not retry connect()
for
non-blocking sockets if it is interrupted by a signal (fails with EINTR
).
The connection runs asynchronously in background. The caller is responsible
to wait until the socket becomes writable (ex: using select.select()
)
and then call socket.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
to check if the connection succeeded (getsockopt()
returns 0
) or failed.
InterruptedError handling
Since interrupted system calls are automatically retried, the
InterruptedError
exception should not occur anymore when calling those
system calls. Therefore, manual handling of InterruptedError
as
described in Status in Python 3.4 can be removed, which will simplify
standard library code.
Backward compatibility
Applications relying on the fact that system calls are interrupted
with InterruptedError
will hang. The authors of this PEP don’t
think that such applications exist, since they would be exposed to
other issues such as race conditions (there is an opportunity for deadlock
if the signal comes before the system call). Besides, such code would
be non-portable.
In any case, those applications must be fixed to handle signals differently, to have a reliable behaviour on all platforms and all Python versions. A possible strategy is to set up a signal handler raising a well-defined exception, or use a wakeup file descriptor.
For applications using event loops, signal.set_wakeup_fd()
is the
recommended option to handle signals. Python’s low-level signal handler
will write signal numbers into the file descriptor and the event loop
will be awaken to read them. The event loop can handle those signals
without the restriction of signal handlers (for example, the loop can
be woken up in any thread, not just the main thread).
Appendix
Wakeup file descriptor
Since Python 3.3, signal.set_wakeup_fd()
writes the signal number
into the file descriptor, whereas it only wrote a null byte before.
It becomes possible to distinguish between signals using the wakeup file
descriptor.
Linux has a signalfd()
system call which provides more information on
each signal. For example, it’s possible to know the pid and uid who sent
the signal. This function is not exposed in Python yet (see
issue 12304).
On Unix, the asyncio
module uses the wakeup file descriptor to
wake up its event loop.
Multithreading
A C signal handler can be called from any thread, but Python signal handlers will always be called in the main Python thread.
Python’s C API provides the PyErr_SetInterrupt()
function which calls
the SIGINT
signal handler in order to interrupt the main Python thread.
Signals on Windows
Control events
Windows uses “control events”:
CTRL_BREAK_EVENT
: Break (SIGBREAK
)CTRL_CLOSE_EVENT
: Close eventCTRL_C_EVENT
: CTRL+C (SIGINT
)CTRL_LOGOFF_EVENT
: LogoffCTRL_SHUTDOWN_EVENT
: Shutdown
The SetConsoleCtrlHandler() function can be used to install a control handler.
The CTRL_C_EVENT
and CTRL_BREAK_EVENT
events can be sent to a
process using the GenerateConsoleCtrlEvent() function.
This function is exposed in Python as os.kill()
.
Signals
The following signals are supported on Windows:
SIGABRT
SIGBREAK
(CTRL_BREAK_EVENT
): signal only available on WindowsSIGFPE
SIGILL
SIGINT
(CTRL_C_EVENT
)SIGSEGV
SIGTERM
SIGINT
The default Python signal handler for SIGINT
sets a Windows event
object: sigint_event
.
time.sleep()
is implemented with WaitForSingleObjectEx()
, it
waits for the sigint_event
object using time.sleep()
parameter
as the timeout. So the sleep can be interrupted by SIGINT
.
_winapi.WaitForMultipleObjects()
automatically adds
sigint_event
to the list of watched handles, so it can also be
interrupted.
PyOS_StdioReadline()
also used sigint_event
when fgets()
failed to check if Ctrl-C or Ctrl-Z was pressed.
Links
Misc
Implementation
The implementation is tracked in issue 23285. It was committed on February 07, 2015.
Copyright
This document has been placed in the public domain.
Source: https://github.com/python/peps/blob/main/pep-0475.txt
Last modified: 2021-09-17 18:18:24 GMT