diff --git a/Doc/library/signal.rst b/Doc/library/signal.rst --- a/Doc/library/signal.rst +++ b/Doc/library/signal.rst @@ -116,6 +116,22 @@ The variables defined in the :mod:`signa in user and kernel space. SIGPROF is delivered upon expiration. +.. data:: SFD_CLOEXEC + + A possible flag in the *flags* parameter to :func:`signalfd` which causes + the new file descriptor to be marked as close-on-exec. + + .. versionadded:: 3.3 + + +.. data:: SFD_NONBLOCK + + A possible flag in the *flags* parameter to :func:`signalfd` which causes + the new file descriptor to be set non-blocking. + + .. versionadded:: 3.3 + + .. data:: SIG_BLOCK A possible value for the *how* parameter to :func:`pthread_sigmask` @@ -277,6 +293,8 @@ The :mod:`signal` module defines the fol attempting to call it from other threads will cause a :exc:`ValueError` exception to be raised. + See also the :func:`signalfd` function. + .. function:: siginterrupt(signalnum, flag) @@ -340,6 +358,28 @@ The :mod:`signal` module defines the fol .. versionadded:: 3.3 +.. function:: signalfd(fd, mask[, flags]) + + Create a new file descriptor on which to receive signals or modify the + mask of such a file descriptor previously created by this function. + + If *fd* is ``-1``, a new file descriptor will be created. Otherwise, + *fd* must be a file descriptor previously returned by this function. + + *mask* is a list of signal numbers which will trigger data on this file + descriptor. + + *flags* is a bit mask which may include :const:`signal.SFD_CLOEXEC` and + :const:`signal.SFD_NONBLOCK` flags. + + See also the :func:`set_wakeup_fd` function. + + Availability: Linux 2.6.22 or later. See the manpage :manpage:`signalfd(2)` + for further information. + + .. versionadded:: 3.3 + + .. _signal-example: Example 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 @@ -631,12 +631,128 @@ class PendingSignalsTests(unittest.TestC # Finally, restore the previous signal handler and the signal mask +@unittest.skipUnless(hasattr(signal, 'signalfd'), + 'need signal.signalfd()') +class SignalfdTests(unittest.TestCase): + """ + Tests for signal.signalfd. + """ + def signalfd(self, *a, **kw): + try: + return signal.signalfd(*a, **kw) + except OSError as err: + if err.errno == errno.ENOSYS: + raise unittest.Skip( + "signalfd() not implemented on this platform") + + def test_signature(self): + """ + When invoked with fewer than two arguments or more than three, signalfd + raises TypeError. + """ + self.assertRaises(TypeError, self.signalfd) + self.assertRaises(TypeError, self.signalfd, 1) + self.assertRaises(TypeError, self.signalfd, 1, 2, 3, 4) + + + def test_create_signalfd(self): + """ + When invoked with a file descriptor of -1, signalfd allocates a new file + descriptor for signal information delivery and returns it. + """ + fd = self.signalfd(-1, []) + self.assertIsInstance(fd, int) + os.close(fd) + + + def test_non_iterable_signals(self): + """ + If an object which is not iterable is passed for the sigmask list + argument to signalfd, the exception raised by trying to iterate over + that object is raised. + """ + self.assertRaises(TypeError, self.signalfd, -1, object()) + + + def test_non_integer_signals(self): + """ + If any non-integer values are included in the sigmask list argument to + signalfd, the exception raised by the attempt to convert them to an + integer is raised. + """ + self.assertRaises(TypeError, self.signalfd, -1, [object()]) + + + def test_out_of_range_signal(self): + """ + If a signal number that is out of the valid range is included in the + sigmask list argument to signalfd, ValueError is raised. + """ + self.assertRaisesRegex( + ValueError, "^signal number -2 out of range$", + self.signalfd, -1, [-2]) + + def test_handle_signals(self): + """ + After signalfd is called, if a signal is received which was in the + sigmask list passed to that call, information about the signal can be + read from the fd returned by that call. + """ + fd = self.signalfd(-1, [signal.SIGUSR2]) + self.addCleanup(os.close, fd) + previous = signal.pthread_sigmask(signal.SIG_BLOCK, [signal.SIGUSR2]) + self.addCleanup(signal.pthread_sigmask, signal.SIG_SETMASK, previous) + os.kill(os.getpid(), signal.SIGUSR2) + bytes = os.read(fd, 128) + self.assertTrue(bytes) + + + def test_close_on_exec(self): + """ + If the bit mask passed as the 3rd argument to signalfd includes + SFD_CLOEXEC, the returned file descriptor has FD_CLOEXEC set on it. + """ + import fcntl + fd = self.signalfd(-1, [], signal.SFD_CLOEXEC) + self.addCleanup(os.close, fd) + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + self.assertTrue(flags & fcntl.FD_CLOEXEC) + + + def test_nonblocking(self): + """ + If the bit mask passed as the 3rd argument to signalfd includes + SFD_NOBLOCK, the file description referenced by the returned file + descriptor has O_NONBLOCK set on it. + """ + import fcntl + fd = self.signalfd(-1, [], signal.SFD_NONBLOCK) + self.addCleanup(os.close, fd) + flags = fcntl.fcntl(fd, fcntl.F_GETFL) + self.assertTrue(flags & os.O_NONBLOCK) + + + def test_default_flags(self): + """ + If an empty bit mask is passed as the 3rd argument to signalfd, neither + FD_CLOEXEC nor O_NONBLOCK is set on the resulting file + descriptor/description. + """ + import fcntl + fd = self.signalfd(-1, []) + self.addCleanup(os.close, fd) + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + self.assertFalse(flags & fcntl.FD_CLOEXEC) + flags = fcntl.fcntl(fd, fcntl.F_GETFL) + self.assertFalse(flags & os.O_NONBLOCK) + + def test_main(): try: support.run_unittest(PosixTests, InterProcessSignalTests, WakeupSignalTests, SiginterruptTest, ItimerTest, WindowsSignalTests, - PendingSignalsTests) + PendingSignalsTests, SignalfdTests) finally: support.reap_children() diff --git a/Modules/signalmodule.c b/Modules/signalmodule.c --- a/Modules/signalmodule.c +++ b/Modules/signalmodule.c @@ -21,6 +21,9 @@ #ifdef HAVE_SYS_TIME_H #include #endif +#ifdef HAVE_SIGNALFD +#include +#endif #if defined(HAVE_PTHREAD_SIGMASK) && !defined(HAVE_BROKEN_PTHREAD_SIGMASK) # define PYPTHREAD_SIGMASK @@ -125,7 +128,9 @@ double_from_timeval(struct timeval *tv) { return tv->tv_sec + (double)(tv->tv_usec / 1000000.0); } +#endif +#ifdef PYPTHREAD_SIGMASK static PyObject * itimer_retval(struct itimerval *iv) { @@ -506,7 +511,8 @@ PyDoc_STRVAR(getitimer_doc, Returns current value of given itimer."); #endif -#if defined(PYPTHREAD_SIGMASK) || defined(HAVE_SIGWAIT) +#if defined(PYPTHREAD_SIGMASK) || defined(HAVE_SIGWAIT) \ + || defined(HAVE_SIGNALFD) /* Convert an iterable to a sigset. Return 0 on success, return -1 and raise an exception on error. */ @@ -710,6 +716,38 @@ Send a signal to a thread."); +#ifdef HAVE_SIGNALFD +static PyObject * +signal_signalfd(PyObject *self, PyObject *args) +{ + int result, flags = 0; + sigset_t mask; + + int fd; + PyObject *signals; + + if (!PyArg_ParseTuple(args, "iO|i:signalfd", &fd, &signals, &flags)) + return NULL; + + if (iterable_to_sigset(signals, &mask) == -1) + return NULL; + + result = signalfd(fd, &mask, flags); + if (result == -1) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + return PyLong_FromLong(result); +} + +PyDoc_STRVAR(signalfd_doc, +"signalfd(fd, mask, flags)\n\ +\n\ +Create a file descriptor for accepting signals."); +#endif + + /* List of functions defined in the module */ static PyMethodDef signal_methods[] = { #ifdef HAVE_ALARM @@ -749,6 +787,9 @@ static PyMethodDef signal_methods[] = { {"sigwait", (PyCFunction)signal_sigwait, METH_VARARGS, signal_sigwait_doc}, #endif +#ifdef HAVE_SIGNALFD + {"signalfd", signal_signalfd, METH_VARARGS, signalfd_doc}, +#endif {NULL, NULL} /* sentinel */ }; @@ -846,6 +887,15 @@ PyInit_signal(void) goto finally; #endif +#ifdef SFD_CLOEXEC + if (PyModule_AddIntMacro(m, SFD_CLOEXEC)) + goto finally; +#endif +#ifdef SFD_NONBLOCK + if (PyModule_AddIntMacro(m, SFD_NONBLOCK)) + goto finally; +#endif + x = IntHandler = PyDict_GetItemString(d, "default_int_handler"); if (!x) goto finally; diff --git a/configure b/configure --- a/configure +++ b/configure @@ -9262,7 +9262,7 @@ for ac_func in alarm accept4 setitimer g select sem_open sem_timedwait sem_getvalue sem_unlink sendfile setegid seteuid \ setgid sethostname \ setlocale setregid setreuid setresuid setresgid setsid setpgid setpgrp setpriority setuid setvbuf \ - sigaction sigaltstack siginterrupt sigpending \ + sigaction sigaltstack siginterrupt signalfd sigpending \ sigrelse sigwait snprintf strftime strlcpy symlinkat sync \ sysconf tcgetpgrp tcsetpgrp tempnam timegm times tmpfile tmpnam tmpnam_r \ truncate uname unlinkat unsetenv utimensat utimes waitid waitpid wait3 wait4 \ diff --git a/configure.in b/configure.in --- a/configure.in +++ b/configure.in @@ -2507,7 +2507,7 @@ AC_CHECK_FUNCS(alarm accept4 setitimer g select sem_open sem_timedwait sem_getvalue sem_unlink sendfile setegid seteuid \ setgid sethostname \ setlocale setregid setreuid setresuid setresgid setsid setpgid setpgrp setpriority setuid setvbuf \ - sigaction sigaltstack siginterrupt sigpending \ + sigaction sigaltstack siginterrupt signalfd sigpending \ sigrelse sigwait snprintf strftime strlcpy symlinkat sync \ sysconf tcgetpgrp tcsetpgrp tempnam timegm times tmpfile tmpnam tmpnam_r \ truncate uname unlinkat unsetenv utimensat utimes waitid waitpid wait3 wait4 \ diff --git a/pyconfig.h.in b/pyconfig.h.in --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -722,6 +722,9 @@ /* Define to 1 if you have the `siginterrupt' function. */ #undef HAVE_SIGINTERRUPT +/* Define to 1 if you have the `signalfd' function. */ +#undef HAVE_SIGNALFD + /* Define to 1 if you have the `sigpending' function. */ #undef HAVE_SIGPENDING