Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 707ce82

Browse files
committed
Issue #10956: Buffered I/O classes retry reading or writing after a signal
has arrived and the handler returned successfully.
1 parent 7e1b9af commit 707ce82

4 files changed

Lines changed: 173 additions & 7 deletions

File tree

Lib/_pyio.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import io
1616
from io import (__all__, SEEK_SET, SEEK_CUR, SEEK_END)
17+
from errno import EINTR
1718

1819
# open() uses st_blksize whenever we can
1920
DEFAULT_BUFFER_SIZE = 8 * 1024 # bytes
@@ -943,7 +944,12 @@ def _read_unlocked(self, n=None):
943944
current_size = 0
944945
while True:
945946
# Read until EOF or until read() would block.
946-
chunk = self.raw.read()
947+
try:
948+
chunk = self.raw.read()
949+
except IOError as e:
950+
if e.errno != EINTR:
951+
raise
952+
continue
947953
if chunk in empty_values:
948954
nodata_val = chunk
949955
break
@@ -962,7 +968,12 @@ def _read_unlocked(self, n=None):
962968
chunks = [buf[pos:]]
963969
wanted = max(self.buffer_size, n)
964970
while avail < n:
965-
chunk = self.raw.read(wanted)
971+
try:
972+
chunk = self.raw.read(wanted)
973+
except IOError as e:
974+
if e.errno != EINTR:
975+
raise
976+
continue
966977
if chunk in empty_values:
967978
nodata_val = chunk
968979
break
@@ -991,7 +1002,14 @@ def _peek_unlocked(self, n=0):
9911002
have = len(self._read_buf) - self._read_pos
9921003
if have < want or have <= 0:
9931004
to_read = self.buffer_size - have
994-
current = self.raw.read(to_read)
1005+
while True:
1006+
try:
1007+
current = self.raw.read(to_read)
1008+
except IOError as e:
1009+
if e.errno != EINTR:
1010+
raise
1011+
continue
1012+
break
9951013
if current:
9961014
self._read_buf = self._read_buf[self._read_pos:] + current
9971015
self._read_pos = 0
@@ -1098,7 +1116,12 @@ def _flush_unlocked(self):
10981116
written = 0
10991117
try:
11001118
while self._write_buf:
1101-
n = self.raw.write(self._write_buf)
1119+
try:
1120+
n = self.raw.write(self._write_buf)
1121+
except IOError as e:
1122+
if e.errno != EINTR:
1123+
raise
1124+
continue
11021125
if n > len(self._write_buf) or n < 0:
11031126
raise IOError("write() returned incorrect number of bytes")
11041127
del self._write_buf[:n]

Lib/test/test_io.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2623,7 +2623,8 @@ def alarm_interrupt(self, sig, frame):
26232623
@unittest.skipUnless(threading, 'Threading required for this test.')
26242624
def check_interrupted_write(self, item, bytes, **fdopen_kwargs):
26252625
"""Check that a partial write, when it gets interrupted, properly
2626-
invokes the signal handler."""
2626+
invokes the signal handler, and bubbles up the exception raised
2627+
in the latter."""
26272628
read_results = []
26282629
def _read():
26292630
s = os.read(r, 1)
@@ -2703,6 +2704,98 @@ def test_reentrant_write_buffered(self):
27032704
def test_reentrant_write_text(self):
27042705
self.check_reentrant_write("xy", mode="w", encoding="ascii")
27052706

2707+
def check_interrupted_read_retry(self, decode, **fdopen_kwargs):
2708+
"""Check that a buffered read, when it gets interrupted (either
2709+
returning a partial result or EINTR), properly invokes the signal
2710+
handler and retries if the latter returned successfully."""
2711+
r, w = os.pipe()
2712+
fdopen_kwargs["closefd"] = False
2713+
def alarm_handler(sig, frame):
2714+
os.write(w, b"bar")
2715+
signal.signal(signal.SIGALRM, alarm_handler)
2716+
try:
2717+
rio = self.io.open(r, **fdopen_kwargs)
2718+
os.write(w, b"foo")
2719+
signal.alarm(1)
2720+
# Expected behaviour:
2721+
# - first raw read() returns partial b"foo"
2722+
# - second raw read() returns EINTR
2723+
# - third raw read() returns b"bar"
2724+
self.assertEqual(decode(rio.read(6)), "foobar")
2725+
finally:
2726+
rio.close()
2727+
os.close(w)
2728+
os.close(r)
2729+
2730+
def test_interrupterd_read_retry_buffered(self):
2731+
self.check_interrupted_read_retry(lambda x: x.decode('latin1'),
2732+
mode="rb")
2733+
2734+
def test_interrupterd_read_retry_text(self):
2735+
self.check_interrupted_read_retry(lambda x: x,
2736+
mode="r")
2737+
2738+
@unittest.skipUnless(threading, 'Threading required for this test.')
2739+
def check_interrupted_write_retry(self, item, **fdopen_kwargs):
2740+
"""Check that a buffered write, when it gets interrupted (either
2741+
returning a partial result or EINTR), properly invokes the signal
2742+
handler and retries if the latter returned successfully."""
2743+
select = support.import_module("select")
2744+
# A quantity that exceeds the buffer size of an anonymous pipe's
2745+
# write end.
2746+
N = 1024 * 1024
2747+
r, w = os.pipe()
2748+
fdopen_kwargs["closefd"] = False
2749+
# We need a separate thread to read from the pipe and allow the
2750+
# write() to finish. This thread is started after the SIGALRM is
2751+
# received (forcing a first EINTR in write()).
2752+
read_results = []
2753+
write_finished = False
2754+
def _read():
2755+
while not write_finished:
2756+
while r in select.select([r], [], [], 1.0)[0]:
2757+
s = os.read(r, 1024)
2758+
read_results.append(s)
2759+
t = threading.Thread(target=_read)
2760+
t.daemon = True
2761+
def alarm1(sig, frame):
2762+
signal.signal(signal.SIGALRM, alarm2)
2763+
signal.alarm(1)
2764+
def alarm2(sig, frame):
2765+
t.start()
2766+
signal.signal(signal.SIGALRM, alarm1)
2767+
try:
2768+
wio = self.io.open(w, **fdopen_kwargs)
2769+
signal.alarm(1)
2770+
# Expected behaviour:
2771+
# - first raw write() is partial (because of the limited pipe buffer
2772+
# and the first alarm)
2773+
# - second raw write() returns EINTR (because of the second alarm)
2774+
# - subsequent write()s are successful (either partial or complete)
2775+
self.assertEqual(N, wio.write(item * N))
2776+
wio.flush()
2777+
write_finished = True
2778+
t.join()
2779+
self.assertEqual(N, sum(len(x) for x in read_results))
2780+
finally:
2781+
write_finished = True
2782+
os.close(w)
2783+
os.close(r)
2784+
# This is deliberate. If we didn't close the file descriptor
2785+
# before closing wio, wio would try to flush its internal
2786+
# buffer, and could block (in case of failure).
2787+
try:
2788+
wio.close()
2789+
except IOError as e:
2790+
if e.errno != errno.EBADF:
2791+
raise
2792+
2793+
def test_interrupterd_write_retry_buffered(self):
2794+
self.check_interrupted_write_retry(b"x", mode="wb")
2795+
2796+
def test_interrupterd_write_retry_text(self):
2797+
self.check_interrupted_write_retry("x", mode="w", encoding="latin1")
2798+
27062799

27072800
class CSignalsTest(SignalsTest):
27082801
io = io

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ Core and Builtins
3535
Library
3636
-------
3737

38+
- Issue #10956: Buffered I/O classes retry reading or writing after a signal
39+
has arrived and the handler returned successfully.
40+
3841
- Issue #10784: New os.getpriority() and os.setpriority() functions.
3942

4043
- Issue #11114: Fix catastrophic performance of tell() on text files (up

Modules/_io/bufferedio.c

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,39 @@ _buffered_init(buffered *self)
714714
return 0;
715715
}
716716

717+
/* Return 1 if an EnvironmentError with errno == EINTR is set (and then
718+
clears the error indicator), 0 otherwise.
719+
Should only be called when PyErr_Occurred() is true.
720+
*/
721+
static int
722+
_trap_eintr(void)
723+
{
724+
static PyObject *eintr_int = NULL;
725+
PyObject *typ, *val, *tb;
726+
PyEnvironmentErrorObject *env_err;
727+
728+
if (eintr_int == NULL) {
729+
eintr_int = PyLong_FromLong(EINTR);
730+
assert(eintr_int != NULL);
731+
}
732+
if (!PyErr_ExceptionMatches(PyExc_EnvironmentError))
733+
return 0;
734+
PyErr_Fetch(&typ, &val, &tb);
735+
PyErr_NormalizeException(&typ, &val, &tb);
736+
env_err = (PyEnvironmentErrorObject *) val;
737+
assert(env_err != NULL);
738+
if (env_err->myerrno != NULL &&
739+
PyObject_RichCompareBool(env_err->myerrno, eintr_int, Py_EQ) > 0) {
740+
Py_DECREF(typ);
741+
Py_DECREF(val);
742+
Py_XDECREF(tb);
743+
return 1;
744+
}
745+
/* This silences any error set by PyObject_RichCompareBool() */
746+
PyErr_Restore(typ, val, tb);
747+
return 0;
748+
}
749+
717750
/*
718751
* Shared methods and wrappers
719752
*/
@@ -1269,7 +1302,14 @@ _bufferedreader_raw_read(buffered *self, char *start, Py_ssize_t len)
12691302
memobj = PyMemoryView_FromBuffer(&buf);
12701303
if (memobj == NULL)
12711304
return -1;
1272-
res = PyObject_CallMethodObjArgs(self->raw, _PyIO_str_readinto, memobj, NULL);
1305+
/* NOTE: PyErr_SetFromErrno() calls PyErr_CheckSignals() when EINTR
1306+
occurs so we needn't do it ourselves.
1307+
We then retry reading, ignoring the signal if no handler has
1308+
raised (see issue #10956).
1309+
*/
1310+
do {
1311+
res = PyObject_CallMethodObjArgs(self->raw, _PyIO_str_readinto, memobj, NULL);
1312+
} while (res == NULL && _trap_eintr());
12731313
Py_DECREF(memobj);
12741314
if (res == NULL)
12751315
return -1;
@@ -1678,7 +1718,14 @@ _bufferedwriter_raw_write(buffered *self, char *start, Py_ssize_t len)
16781718
memobj = PyMemoryView_FromBuffer(&buf);
16791719
if (memobj == NULL)
16801720
return -1;
1681-
res = PyObject_CallMethodObjArgs(self->raw, _PyIO_str_write, memobj, NULL);
1721+
/* NOTE: PyErr_SetFromErrno() calls PyErr_CheckSignals() when EINTR
1722+
occurs so we needn't do it ourselves.
1723+
We then retry writing, ignoring the signal if no handler has
1724+
raised (see issue #10956).
1725+
*/
1726+
do {
1727+
res = PyObject_CallMethodObjArgs(self->raw, _PyIO_str_write, memobj, NULL);
1728+
} while (res == NULL && _trap_eintr());
16821729
Py_DECREF(memobj);
16831730
if (res == NULL)
16841731
return -1;

0 commit comments

Comments
 (0)