diff --git a/Doc/library/signal.rst b/Doc/library/signal.rst --- a/Doc/library/signal.rst +++ b/Doc/library/signal.rst @@ -13,9 +13,6 @@ rules for working with signals and their underlying implementation), with the exception of the handler for :const:`SIGCHLD`, which follows the underlying implementation. -* There is no way to "block" signals temporarily from critical sections (since - this is not supported by all Unix flavors). - * Although Python signal handlers are called asynchronously as far as the Python user is concerned, they can only occur between the "atomic" instructions of the Python interpreter. This means that signals arriving during long calculations @@ -119,6 +116,28 @@ The variables defined in the :mod:`signa in user and kernel space. SIGPROF is delivered upon expiration. +.. data:: SIG_BLOCK + + A possible value for the *how* parameter to :func:`pthread_sigmask` + indicating that signals are to be blocked. + + .. versionadded:: 3.3 + +.. data:: SIG_UNBLOCK + + A possible value for the *how* parameter to :func:`pthread_sigmask` + indicating that signals are to be unblocked. + + .. versionadded:: 3.3 + +.. data:: SIG_SETMASK + + A possible value for the *how* parameter to :func:`pthread_sigmask` + indicating that the signal mask is to be replaced. + + .. versionadded:: 3.3 + + The :mod:`signal` module defines one exception: .. exception:: ItimerError @@ -161,6 +180,30 @@ The :mod:`signal` module defines the fol :manpage:`signal(2)`.) +.. function:: pthread_sigmask(how, mask) + + Fetch and/or change the signal mask of the calling thread. The signal mask + is the set of signals whose delivery is currently blocked for the caller. + The old signal mask is returned. + + The behavior of the call is dependent on the value of *how*, as follows. + + * :data:`SIG_BLOCK`: The set of blocked signals is the union of the current + set and the *mask* argument. + * :data:`SIG_UNBLOCK`: The signals in *mask* are removed from the current + set of blocked signals. It is permissible to attempt to unblock a + signal which is not blocked. + * :data:`SIG_SETMASK`: The set of blocked signals is set to the *mask* + argument. + + *mask* is a list of signal numbers (e.g. :const:`signal.SIGUSR1`). + + Availability: Unix. See the man page :manpage:`sigprocmask(3)` and + :manpage:`pthread_sigmask(3)` for further information. + + .. versionadded:: 3.3 + + .. function:: setitimer(which, seconds[, interval]) Sets given interval timer (one of :const:`signal.ITIMER_REAL`, diff --git a/Doc/whatsnew/3.3.rst b/Doc/whatsnew/3.3.rst --- a/Doc/whatsnew/3.3.rst +++ b/Doc/whatsnew/3.3.rst @@ -118,7 +118,16 @@ threading * The :mod:`threading` module has a new :func:`~threading._info` function which provides informations about the thread implementation. - (:issue:`11223`) +(:issue:`11223`) + +signal +------ + +* The :mod:`signal` module has a new :func:`~signal.pthread_sigmask` function + to fetch and/or change the signal mask of the calling thread. + +(Contributed by Jean-Paul Calderone in :issue:`8407`) + Optimizations ============= diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py --- a/Lib/test/test_io.py +++ b/Lib/test/test_io.py @@ -2627,6 +2627,8 @@ class SignalsTest(unittest.TestCase): in the latter.""" read_results = [] def _read(): + if hasattr(signal, 'pthread_sigmask'): + signal.pthread_sigmask(signal.SIG_BLOCK, [signal.SIGALRM]) s = os.read(r, 1) read_results.append(s) t = threading.Thread(target=_read) diff --git a/Lib/test/test_signal.py b/Lib/test/test_signal.py --- a/Lib/test/test_signal.py +++ b/Lib/test/test_signal.py @@ -483,11 +483,71 @@ class ItimerTest(unittest.TestCase): # and the handler should have been called self.assertEqual(self.hndl_called, True) + +@unittest.skipUnless(hasattr(signal, 'pthread_sigmask'), + 'need signal.pthread_sigmask()') +class PthreadSigmaskTests(unittest.TestCase): + def test_arguments(self): + self.assertRaises(TypeError, signal.pthread_sigmask) + self.assertRaises(TypeError, signal.pthread_sigmask, 1) + self.assertRaises(TypeError, signal.pthread_sigmask, 1, 2, 3) + with self.assertRaisesRegexp(ValueError, "^invalid how value$"): + signal.pthread_sigmask(1700, []) + + def test_block_unlock(self): + pid = os.getpid() + signum = signal.SIGUSR1 + + global tripped + tripped = False + + def handler(signum, frame): + global tripped + tripped = True + + def read_sigmask(): + return signal.pthread_sigmask(signal.SIG_BLOCK, []) + + old_handler = signal.signal(signum, handler) + self.addCleanup(signal.signal, signum, old_handler) + + # unblock SIGUSR1, copy the old mask and test our signal handler + old_mask = signal.pthread_sigmask(signal.SIG_UNBLOCK, [signum]) + self.addCleanup(signal.pthread_sigmask, signal.SIG_SETMASK, old_mask) + os.kill(pid, signum) + self.assertTrue(tripped) + + # block SIGUSR1 + tripped = False + signal.pthread_sigmask(signal.SIG_BLOCK, [signum]) + os.kill(pid, signum) + self.assertFalse(tripped) + + # check the mask + blocked = read_sigmask() + self.assertIn(signum, blocked) + self.assertEqual(set(old_mask) ^ set(blocked), {signum}) + + # unblock SIGUSR1 + signal.pthread_sigmask(signal.SIG_UNBLOCK, [signum]) + os.kill(pid, signum) + self.assertTrue(tripped) + + # check the mask + unblocked = read_sigmask() + self.assertNotIn(signum, unblocked) + self.assertEqual(set(blocked) ^ set(unblocked), {signum}) + self.assertSequenceEqual(old_mask, unblocked) + + del tripped + + def test_main(): try: support.run_unittest(BasicSignalTests, InterProcessSignalTests, WakeupSignalTests, SiginterruptTest, - ItimerTest, WindowsSignalTests) + ItimerTest, WindowsSignalTests, + PthreadSigmaskTests) finally: support.reap_children() diff --git a/Misc/NEWS b/Misc/NEWS --- a/Misc/NEWS +++ b/Misc/NEWS @@ -110,6 +110,9 @@ Core and Builtins Library ------- +- Issue #8407: Add signal.pthread_sigmask() function to fetch and/or change the + signal mask of the calling thread. + - Issue #11223: Add threading._info() function providing informations about the thread implementation. @@ -490,6 +493,10 @@ Extensions Tests ----- +- Issue #8407, #11859: Fix tests of test_io using threads and an alarm: use + pthread_sigmask() to ensure that only the main thread receives the SIGALRM + signal. + - Issue #11223: Skip test_lock_acquire_interruption() and test_rlock_acquire_interruption() of test_threadsignals if a thread lock is implemented using a POSIX mutex and a POSIX condition variable. A POSIX diff --git a/Modules/signalmodule.c b/Modules/signalmodule.c --- a/Modules/signalmodule.c +++ b/Modules/signalmodule.c @@ -22,6 +22,14 @@ #include #endif +#if defined(HAVE_PTHREAD_SIGMASK) && !defined(HAVE_BROKEN_PTHREAD_SIGMASK) +# define USE_PTHREAD_SIGMASK +#endif + +#if defined(USE_PTHREAD_SIGMASK) && defined(HAVE_PTHREAD_H) +# include +#endif + #ifndef SIG_ERR #define SIG_ERR ((PyOS_sighandler_t)(-1)) #endif @@ -495,6 +503,107 @@ PyDoc_STRVAR(getitimer_doc, Returns current value of given itimer."); #endif +#ifdef USE_PTHREAD_SIGMASK +/* Convert an iterable to a sigset. + Return 0 on success, return -1 and raise an exception on error. */ + +static int +iterable_to_sigset(PyObject *iterable, sigset_t *mask) +{ + int result = -1; + PyObject *iterator, *item; + long signum; + int err; + + sigemptyset(mask); + + iterator = PyObject_GetIter(iterable); + if (iterator == NULL) + goto error; + + while (1) + { + item = PyIter_Next(iterator); + if (item == NULL) { + if (PyErr_Occurred()) + goto error; + else + break; + } + + signum = PyLong_AsLong(item); + Py_DECREF(item); + if (signum == -1 && PyErr_Occurred()) + goto error; + if (0 < signum && signum < NSIG) + err = sigaddset(mask, (int)signum); + else + err = 1; + if (err) { + PyErr_Format(PyExc_ValueError, + "signal number %ld out of range", signum); + goto error; + } + } + result = 0; + +error: + Py_XDECREF(iterator); + return result; +} + +static PyObject * +signal_pthread_sigmask(PyObject *self, PyObject *args) +{ + int how, sig; + PyObject *signals, *result, *signum; + sigset_t mask, previous; + + if (!PyArg_ParseTuple(args, "iO:pthread_sigmask", &how, &signals)) + return NULL; + + if (iterable_to_sigset(signals, &mask)) + return NULL; + + if (pthread_sigmask(how, &mask, &previous) != 0) { + PyErr_Format(PyExc_ValueError, "invalid how value"); + return NULL; + } + + result = PyList_New(0); + if (result == NULL) + return NULL; + + for (sig = 1; sig < NSIG; sig++) { + if (sigismember(&previous, sig) != 1) + continue; + + /* Handle the case where it is a member by adding the signal to + the result list. Ignore the other cases because they mean the + signal isn't a member of the mask or the signal was invalid, + and an invalid signal must have been our fault in constructing + the loop boundaries. */ + signum = PyLong_FromLong(sig); + if (signum == NULL) { + Py_DECREF(result); + return NULL; + } + if (PyList_Append(result, signum) == -1) { + Py_DECREF(signum); + Py_DECREF(result); + return NULL; + } + Py_DECREF(signum); + } + return result; +} + +PyDoc_STRVAR(signal_pthread_sigmask_doc, +"pthread_sigmask(how, mask) -> old mask\n\ +\n\ +Fetch and/or change the signal mask of the calling thread."); +#endif /* #ifdef USE_PTHREAD_SIGMASK */ + /* List of functions defined in the module */ static PyMethodDef signal_methods[] = { @@ -515,10 +624,14 @@ static PyMethodDef signal_methods[] = { #endif #ifdef HAVE_PAUSE {"pause", (PyCFunction)signal_pause, - METH_NOARGS,pause_doc}, + METH_NOARGS, pause_doc}, #endif {"default_int_handler", signal_default_int_handler, METH_VARARGS, default_int_handler_doc}, +#ifdef USE_PTHREAD_SIGMASK + {"pthread_sigmask", (PyCFunction)signal_pthread_sigmask, + METH_VARARGS, signal_pthread_sigmask_doc}, +#endif {NULL, NULL} /* sentinel */ }; @@ -603,6 +716,27 @@ PyInit_signal(void) goto finally; Py_DECREF(x); +#ifdef SIG_BLOCK + x = PyLong_FromLong(SIG_BLOCK); + if (!x || PyDict_SetItemString(d, "SIG_BLOCK", x) < 0) + goto finally; + Py_DECREF(x); +#endif + +#ifdef SIG_UNBLOCK + x = PyLong_FromLong(SIG_UNBLOCK); + if (!x || PyDict_SetItemString(d, "SIG_UNBLOCK", x) < 0) + goto finally; + Py_DECREF(x); +#endif + +#ifdef SIG_SETMASK + x = PyLong_FromLong(SIG_SETMASK); + if (!x || PyDict_SetItemString(d, "SIG_SETMASK", x) < 0) + goto finally; + Py_DECREF(x); +#endif + x = IntHandler = PyDict_GetItemString(d, "default_int_handler"); if (!x) goto finally;