Duncan Coutts pushed to branch wip/dcoutts/signals-cleanup at Glasgow Haskell Compiler / GHC Commits: 5b0be3ef by Duncan Coutts at 2026-10-27T17:11:50+00:00 Redo the scheme for special fds in the poll and selectbis I/O managers At the moment we only have one special fd for these I/O managers: the pipe (or eventfd) for interrupting them when they're sleeping. We're about to gain a second for signal handling. The existing scheme was rather ad-hoc and hard to extend. It was already complicated by the fact that the interrupt fd is optional. The signal fd will also be optional in the sense that cap 0 will have it but cap >0 will not. So the new scheme is that we add special aiop entries into the aiop_table, and for the most part we can treat these uniformly with other aiops. But the action we take for them if/when they are ready is handled specially. This seems much cleaner and will be easy to extend for signals. - - - - - 3f545d73 by Duncan Coutts at 2026-10-27T17:11:50+00:00 Use the signal self-pipe trick for the poll and selectbis I/O managers So now the low level signal handler delivers signals into a pipe that is monitored by the I/O manager (on cap 0). The I/O manager then dispatches the handler (starting a Haskell thread). There are two important reasons for this change: 1. This method does not have any windows of time where signals can be missed for arbitrary periods of time. The previous method has a race where a signal delivered just before the I/O manager calls poll/select will not notice the signal until some unrelated event causes poll/select to return. See issue #27540. The new method ensures that a fd is ready and so poll/select will notice immediately (if the signal was delivered prior to the call to poll/select) or later. 2. The existing method relies on temporarily masking signals for concurrency safety which cannot work reliably in a multi-threaded process. Fixing this is a necessary pre-requisite to enable these in-RTS I/O managers to work in the threaded RTS. The issue is that startPendingSignalHandlers and the low level signal handler share the pending_handler_buf data structure, and so startPendingSignalHandlers has to mask delivery of signals to avoid the shared data being mutated concurrently. But masking signals in a multi-threaded process is not generally effective because the signal can still be delivered in any thread Document the self-pipe trick in a Note. This fixes issue #27540. - - - - - ddf14de2 by Duncan Coutts at 2026-10-27T17:11:50+00:00 Move the legacy user signal handler into legacy select I/O manager We do not convert the legacy select I/O manager to use the self-pipe trick. It keeps the existing code, but since it is now the only user of it, then that code can move from being a shared utility to live with the select I/O manager. There are some minor modifications to the code moved so that it is not entangled with other remaining shared signal utils. - - - - - d1925b83 by Duncan Coutts at 2026-10-27T17:11:50+00:00 Deprecate (un)blockUserSignals for external RTS users Encourage external users to stop using these actions. - - - - - 12 changed files: - rts/IOManager.c - rts/IOManagerInternals.h - rts/RtsSymbols.c - rts/include/rts/BlockSignals.h - rts/include/rts/storage/Closures.h - rts/posix/IOManSignals.c - rts/posix/IOManSignals.h - rts/posix/Poll.c - rts/posix/Poll.h - rts/posix/Select.c - rts/posix/SelectBis.c - rts/posix/SelectBis.h Changes: ===================================== rts/IOManager.c ===================================== @@ -462,6 +462,20 @@ void startIOManager(void) switch (iomgr_type) { +#if defined(IOMGR_ENABLED_SELECTBIS) + case IO_MANAGER_SELECTBIS: + for (unsigned int i = 0; i < n_capabilities; i++) { + startCapabilityIOManagerSelectBis(getCapability(i)->iomgr); + } + break; +#endif +#if defined(IOMGR_ENABLED_POLL) + case IO_MANAGER_POLL: + for (unsigned int i = 0; i < n_capabilities; i++) { + startCapabilityIOManagerPoll(getCapability(i)->iomgr); + } + break; +#endif #if defined(IOMGR_ENABLED_MIO_POSIX) case IO_MANAGER_MIO_POSIX: /* Posix implementation in posix/Signals.c @@ -514,6 +528,18 @@ restartIOManager(CapIOManager *iomgr, Capability **pcap) { switch (iomgr_type) { +#if defined(IOMGR_ENABLED_SELECTBIS) + case IO_MANAGER_SELECTBIS: + startCapabilityIOManagerSelectBis(iomgr); + break; +#endif + +#if defined(IOMGR_ENABLED_POLL) + case IO_MANAGER_POLL: + startCapabilityIOManagerPoll(iomgr); + break; +#endif + #if defined(IOMGR_ENABLED_MIO_POSIX) case IO_MANAGER_MIO_POSIX: /* Posix implementation in posix/Signals.c @@ -530,8 +556,6 @@ restartIOManager(CapIOManager *iomgr, Capability **pcap) break; #endif /* The IO_MANAGER_SELECT needs no initialisation */ - /* The IO_MANAGER_SELECTBIS needs no initialisation */ - /* The IO_MANAGER_POLL needs no initialisation */ /* No impl for any of the Windows I/O managers, since no forking. */ default: ===================================== rts/IOManagerInternals.h ===================================== @@ -76,6 +76,9 @@ struct _CapIOManager { /* AIOP and timeout collections shared by several I/O manager impls */ ClosureTable aiop_table; StgTimeoutQueue *timeout_queue; + + /* The number of special fds at the start of the aiop table. */ + int n_special_fds; #endif #if defined(IOMGR_ENABLED_SELECTBIS) @@ -83,11 +86,8 @@ struct _CapIOManager { #endif #if defined(IOMGR_ENABLED_POLL) - /* Auxiliary table with size and indexes matching the aiop_table. This is - * aliased to the tail of the full poll table, which has a head entry for - * the wakeup_fd_r above, so we can also poll that fd. - */ - struct pollfd *aiop_poll_table, *full_poll_table; + /* Auxiliary table with size and indexes matching the aiop_table. */ + struct pollfd *aiop_poll_table; #endif #if defined(IOMGR_ENABLED_WIN32_LEGACY) @@ -113,6 +113,17 @@ struct _CapIOManager { */ struct _CapIOManagerShared { +#if defined(IOMGR_ENABLED_POLL) \ + || defined(IOMGR_ENABLED_SELECTBIS) +#if defined(RTS_USER_SIGNALS) + /* Pipe FDs for delivering posix signals to the I/O manager. + * The signal number and siginfo_t are written to the write end by the + * low-level signal handler, and read by the I/O manager on capability 0. + */ + int signal_fd_r, signal_fd_w; +#endif +#endif + #if defined(IOMGR_ENABLED_MIO_POSIX) /* The timer manager control pipe (write end). This is the pipe into which * we will send our signals. ===================================== rts/RtsSymbols.c ===================================== @@ -1233,6 +1233,9 @@ RTS_SYMBOLS_PRIM (void*)(&(xxx)), strength, ty }, +#pragma GCC diagnostic push +/* Don't warn about exporting deprecated RTS functions. */ +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" /* Initialize (if not already initialized) and return an array of symbols with stuff from the RTS. */ void initLinkerRtsSyms (StrHashTable *symhash) { @@ -1270,6 +1273,7 @@ void initLinkerRtsSyms (StrHashTable *symhash) { IF_DEBUG(linker, debugBelch("done with built-in RTS symbols\n")); } +#pragma GCC diagnostic pop // Note [Extra RTS symbols] ===================================== rts/include/rts/BlockSignals.h ===================================== @@ -33,13 +33,17 @@ * This is exported from the RTS but should no longer be used. It is likely to * be removed in future. */ -void blockUserSignals(void); +void blockUserSignals(void) + __attribute__((deprecated("blockUserSignals should no longer be used, " + "use pthread_sigmask or similar."))); /* * Function: unblockUserSignals() * * The inverse of blockUserSignals(); re-enable the deliver of console events. */ -void unblockUserSignals(void); +void unblockUserSignals(void) + __attribute__((deprecated("unblockUserSignals should no longer be " + "used, use pthread_sigmask or similar."))); #endif ===================================== rts/include/rts/storage/Closures.h ===================================== @@ -722,14 +722,16 @@ typedef struct { ------------------------------------------------------------------------- */ union NotifyCompletion { - StgTSO *tso; - StgMVar *mvar; - StgTVar *tvar; + StgTSO *tso; + StgMVar *mvar; + StgTVar *tvar; + StgClosure *unused; }; enum NotifyCompletionType { - NotifyTSO = 0, // thread-synchronous I/O - NotifyMVar = 1, // async I/O with MVar notification - NotifyTVar = 2 // async I/O with TVar notification + NotifyTSO = 0, // thread-synchronous I/O + NotifyMVar = 1, // async I/O with MVar notification + NotifyTVar = 2, // async I/O with TVar notification + NotifyInternal = 3 // used for special I/O manager-internal notification }; /* A node in the leftist heap. */ @@ -873,6 +875,10 @@ typedef struct { // For failed outcomes, this is the error code. uint32_t error; + + // For notify_type NotifyInternal, this is available for internal + // use by individual I/O managers. + uint32_t internal; }; // Note that because we use fixed size Ctypes here then the size in words ===================================== rts/posix/IOManSignals.c ===================================== @@ -92,76 +92,6 @@ more_handlers(int sig) nHandlers = sig + 1; } -#if !defined(THREADED_RTS) - -#define N_PENDING_HANDLERS 16 - -siginfo_t pending_handler_buf[N_PENDING_HANDLERS]; -siginfo_t *next_pending_handler = pending_handler_buf; - -static inline bool signals_pending(void) { - return (next_pending_handler != pending_handler_buf); -} - -#endif /* THREADED_RTS */ - -/* ----------------------------------------------------------------------------- - * Low-level signal handler - * - * Places the requested handler on a stack of pending handlers to be - * started up at the next context switch. - * -------------------------------------------------------------------------- */ - -#if !defined(THREADED_RTS) -void -generic_handler(int sig USED_IF_THREADS, - siginfo_t *info, - void *p STG_UNUSED) -{ - - /* Can't call allocate from here. Probably can't call malloc - either. However, we have to schedule a new thread somehow. - - It's probably ok to request a context switch and allow the - scheduler to start the handler thread, but how do we - communicate this to the scheduler? - - We need some kind of locking, but with low overhead (i.e. no - blocking signals every time around the scheduler). - - Signal Handlers are atomic (i.e. they can't be interrupted), and - we can make use of this. We just need to make sure the - critical section of the scheduler can't be interrupted - the - only way to do this is to block signals. However, we can lower - the overhead by only blocking signals when there are any - handlers to run, i.e. the set of pending handlers is - non-empty. - */ - - /* We use a stack to store the pending signals. We can't - dynamically grow this since we can't allocate any memory from - within a signal handler. - - Hence unfortunately we have to bomb out if the buffer - overflows. It might be acceptable to carry on in certain - circumstances, depending on the signal. - */ - - memcpy(next_pending_handler, info, sizeof(siginfo_t)); - - next_pending_handler++; - - // stack full? - if (next_pending_handler == &pending_handler_buf[N_PENDING_HANDLERS]) { - errorBelch("too many pending signals"); - stg_exit(EXIT_FAILURE); - } - - interruptCapability(&MainCapability); - -} -#endif /* THREADED_RTS */ - /* ----------------------------------------------------------------------------- * Blocking/Unblocking of the user signals * -------------------------------------------------------------------------- */ @@ -267,34 +197,6 @@ installSignalHandler(int sig, int spi, sigset_t *mask, bool nocldstop, * Creating new threads for signal handlers. * -------------------------------------------------------------------------- */ -#if !defined(THREADED_RTS) -void -startPendingSignalHandlers(Capability *cap) -{ - siginfo_t *info; - int sig; - - if (!signals_pending()) return; - - blockUserSignals(); - - while (next_pending_handler != pending_handler_buf) { - - next_pending_handler--; - - sig = next_pending_handler->si_signo; - if (signal_handlers[sig] == STG_SIG_DFL) { - continue; // handler has been changed. - } - - info = next_pending_handler; - startSignalHandler(cap, sig, info); - } - - unblockUserSignals(); -} -#endif - void startSignalHandler(Capability *cap, int sig, siginfo_t *info) { /* Allocate a copy. This is freed by runHandler. */ @@ -321,6 +223,43 @@ void startSignalHandler(Capability *cap, int sig, siginfo_t *info) setThreadLabel(cap, t, "signal handler thread"); } +/* Note [Non-racy signal handling using the self-pipe trick] + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * Reliably picking up unix/posix signals promptly is surprisingly tricky! + * The standard problem is that there is a race between signal delivery and + * waiting/blocking in something like select/poll/epoll_wait. The problem is + * that if one checks for a signal having been delivered (e.g. by looking at a + * shared variable set by the signal handler) just before calling select/poll + * then there's a time window between that point and calling select/poll when + * a signal could be delivered and then not noticed until select/poll returns. + * Once we've called select/poll then it can return EINTR upon signal arrival, + * but the window before remains a problem. + * + * One solution is to use pselect/ppoll: mask signals, then do a final check + * for signals having been delivered, then call pselect/ppoll to unmask + * signals (atomically) for the duration of the wait. This is fine in a single + * threaded process. In a multi-threaded process, masking signals doesn't work. + * See Note [pthread_sigmask and the ineffectiveness of masking signals]. + * + * The standard solution is the "self-pipe trick" where the low level signal + * handler writes the signal info into a pipe, and poll/select monitors that + * pipe. This neatly eliminates the window where signals could be missed. If + * the signal arrives before calling poll/select then the pipe is readable and + * poll/select will return. If the signal arrives during the poll/select call + * then they will return with EINTR. + * + * In the RTS context, this is done by the I/O managers: the in-RTS ones use + * newNonBlockingPipe on startup, have their low level signal handler use + * writeSignalInfoToPipe, and when the I/O manager finds the other end of the + * pipe is readable then it calls readSignalInfoFromPipe, and can then use + * startSignalHandler. + * + * The in-Haskell MIO I/O manager does essentially the same. + * + * See issue #27540. + */ + void newNonBlockingPipe(int *pipe_fd_r, int *pipe_fd_w) { int pipefd[2]; ===================================== rts/posix/IOManSignals.h ===================================== @@ -25,16 +25,6 @@ void initUserSignals(void); */ void finiUserSignals(void); -#if !defined(THREADED_RTS) -/* - * Function: startPendingSignalHandlers() - * - * If there are any queued up posix signals, run the handlers - * associated with them. - */ -void startPendingSignalHandlers(Capability *cap); -#endif - void startSignalHandler(Capability *cap, int sig, siginfo_t *info); typedef void (*SignalHandler)(int sig, siginfo_t *info, void *p); @@ -47,11 +37,6 @@ void newNonBlockingPipe(int *pipe_fd_r, int *pipe_fd_w); void writeSignalInfoToPipe (int sig, siginfo_t *info, int fd); bool readSignalInfoFromPipe(int *sig, siginfo_t *info, int fd); -/* Temporary */ -void generic_handler(int sig USED_IF_THREADS, - siginfo_t *info, - void *p STG_UNUSED); - #endif #include "EndPrivate.h" ===================================== rts/posix/Poll.c ===================================== @@ -1,6 +1,6 @@ /* ----------------------------------------------------------------------------- * - * (c) The GHC Team 2020-2023 + * (c) The GHC Team 2020-2026 * * An I/O manager based on the classic Unix poll() system call. * @@ -107,9 +107,10 @@ timeout (if any) as the poll() timeout parameter. The CapIOManager structure for this I/O manager contains: ClosureTable aiop_table; - struct pollfd *aiop_poll_table, *full_poll_table; + struct pollfd *aiop_poll_table; StgTimeoutQueue *timeout_queue; int interrupt_fd_r, interrupt_fd_w; + int n_special_fds; We also support the Linux-specific ppoll API which supports higher resolution time delays -- nanoseconds rather than milliseconds as in classic poll(). It @@ -118,14 +119,15 @@ also allows the signal mask to be adjusted, but we do not make use of this. int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *tmo_p, const sigset_t *sigmask); -We have both aiop_poll_table and full_poll_table. This is to cope with needing -to wait on the special extra file descriptor interrupt_fd_r. This fd is used to -support waking the I/O manager when we are blocked in a poll call. This -requires waiting on an extra fd that has no corresponding entry in the -aiop_table. To manage this quirk, we alias the aiop_poll_table to be the tail -of the full_poll_table and have the first entry of the full_poll_table be the -interrupt_fd_r. This means the aiop_poll_table indicies match up exactly with -the aiop_table, but still allows the full_poll_table to have an extra entry. +In addition to aiops, there are some special internal fds we have to monitor +and respond to. In particular there is the interrupt_fd_r used to implement +interruptIOManager. Whether this fd is used or not depends on configuration +(use of interrupt_fd_r depends on HAVE_PREEMPTION). To enable multiple such +fds and allow them to be optional we use the following scheme: we create +special internal AsyncIOOps and put them into the aiop_table. These are +treated much like other IOOpCodeWaitRead operations but with special handling +for notification. The variable n_special_fds tracks how many such entries we +have so that we can handle them specially but in a reasonably uniform manner. ******************************************************************************/ @@ -135,7 +137,20 @@ static void notifyIOCompletion(CapIOManager *iomgr, StgAsyncIOOp *aiop); static void removeFromTables(CapIOManager *iomgr, int i); static void reportPollError(int res, nfds_t nfds) STG_NORETURN; +enum SpecialFd { + InterruptFd, /* iomgr->interrupt_fd_r for interruptIOManager */ + SignalFd /* iomgr->shared->signal_fd_r */ +}; +static void insertSpecialFd(CapIOManager *iomgr, + enum SpecialFd special_fd_kind, + int special_fd); +static void processSpecialFd(CapIOManager *iomgr, + StgAsyncIOOp *aiop, bool *interrupt); + +/* Called early in RTS startup _before_ the storage manager is initialised, + * so this is not allowed to allocate on the GC heap. + */ void initCapabilityIOManagerPoll(CapIOManager *iomgr) { initClosureTable(&iomgr->aiop_table, ClosureTableCompact); @@ -145,26 +160,80 @@ void initCapabilityIOManagerPoll(CapIOManager *iomgr) newFdWakeup(&iomgr->interrupt_fd_r, &iomgr->interrupt_fd_w); #endif - iomgr->full_poll_table = stgMallocBytes(sizeof(struct pollfd) /* size 1 */, - "initCapabilityIOManagerPoll"); - iomgr->full_poll_table[0] = (struct pollfd) { + iomgr->n_special_fds = 0 #if defined(HAVE_PREEMPTION) - .fd = iomgr->interrupt_fd_r, - .events = POLLIN, -#else - .fd = -1, // unused - .events = 0, // unused + + 1 +#endif +#if defined(RTS_USER_SIGNALS) + + (iomgr->cap->no == 0 ? 1 : 0) #endif - .revents = 0 - }; - iomgr->aiop_poll_table = iomgr->full_poll_table+1; /* hence empty */ + ; #if defined(RTS_USER_SIGNALS) /* Signal handlers are shared across all caps. Only init once. */ if (iomgr->cap->no == 0) { initUserSignals(); + /* See Note [Non-racy signal handling using the self-pipe trick] */ + newNonBlockingPipe(&iomgr->shared->signal_fd_r, + &iomgr->shared->signal_fd_w); + } +#endif +} + + +/* Called late in RtsStartup, once heap allocation is ready. */ +void startCapabilityIOManagerPoll(CapIOManager *iomgr) +{ + /* ClosureTable allocates on the heap, so we can do it here not in init */ + if (iomgr->n_special_fds > 0) { + bool ok = enlargeClosureTable(iomgr->cap, &iomgr->aiop_table, + iomgr->n_special_fds); + CHECK(ok); } + iomgr->aiop_poll_table = + stgMallocBytes(sizeof(struct pollfd) * iomgr->n_special_fds, + "initCapabilityIOManagerPoll"); + +#if defined(HAVE_PREEMPTION) + insertSpecialFd(iomgr, InterruptFd, iomgr->interrupt_fd_r); #endif +#if defined(RTS_USER_SIGNALS) + if (iomgr->cap->no == 0) { + /* See Note [Non-racy signal handling using the self-pipe trick] */ + insertSpecialFd(iomgr, SignalFd, iomgr->shared->signal_fd_r); + } +#endif + ASSERT(iomgr->n_special_fds == sizeClosureTable(&iomgr->aiop_table)); +} + + +/* For special fds, we add a dummy entry to the aiop_table and a + * corresponding real entry to the aiop_poll_table. + */ +static void insertSpecialFd(CapIOManager *iomgr, + enum SpecialFd special_fd_kind, + int special_fd) +{ + StgAsyncIOOp *aiop = (StgAsyncIOOp *)allocate(iomgr->cap, + sizeofW(StgAsyncIOOp)); + SET_HDR(aiop, &stg_ASYNCIOOP_info, CCS_SYSTEM); + aiop->notify.unused = &stg_ASYNCIO_LIVE0_closure; + aiop->live = &stg_ASYNCIO_LIVE0_closure; + aiop->capno = iomgr->cap->no; + aiop->notify_type = NotifyInternal; + aiop->outcome = IOOpOutcomeInFlight; + aiop->operation = IOOpCodeWaitRead; + aiop->fd = special_fd; + aiop->internal = special_fd_kind; + + int ix = insertClosureTable(iomgr->cap, &iomgr->aiop_table, aiop); + aiop->index = ix; + + iomgr->aiop_poll_table[ix] = (struct pollfd) { + .fd = special_fd, + .events = POLLIN, + .revents = 0 + }; } @@ -173,9 +242,11 @@ void freeCapabilityIOManagerPoll(CapIOManager *iomgr) #if defined(RTS_USER_SIGNALS) if (iomgr->cap->no == 0) { finiUserSignals(); + close(iomgr->shared->signal_fd_r); + close(iomgr->shared->signal_fd_w); } #endif - stgFree(iomgr->full_poll_table); + stgFree(iomgr->aiop_poll_table); #if defined(HAVE_PREEMPTION) closeFdWakeup(iomgr->interrupt_fd_r, iomgr->interrupt_fd_w); #endif @@ -314,22 +385,10 @@ static bool processIOCompletions(CapIOManager *iomgr, int ncompletions) ASSERT(ncompletions > 0); bool interrupt = false; -#if defined(HAVE_PREEMPTION) - /* If the interrupt_fd_r is ready, collect it */ - if (iomgr->full_poll_table[0].revents) { - ASSERT(iomgr->full_poll_table[0].fd == iomgr->interrupt_fd_r); - collectFdWakeup(iomgr->interrupt_fd_r); - ncompletions--; - interrupt = true; - debugTrace(DEBUG_iomanager, "Received interrupt in poll I/O manager"); - } -#endif struct pollfd *aiop_poll_table = iomgr->aiop_poll_table; int i = 0; - while (ncompletions > 0) { - ASSERT(i < sizeClosureTable(&iomgr->aiop_table)); - + while (ncompletions > 0 && i < sizeClosureTable(&iomgr->aiop_table)) { /* Since each aiop_table entry is for a single (fd, rw) pair, we * just need to check if the aiop_poll_table[n].revents contains * anything. We don't need to distinguish read vs write. @@ -337,26 +396,34 @@ static bool processIOCompletions(CapIOManager *iomgr, int ncompletions) if (aiop_poll_table[i].revents) { StgAsyncIOOp *aiop = indexClosureTable(&iomgr->aiop_table, i); + ASSERT(aiop->operation == IOOpCodeWaitRead || + aiop->operation == IOOpCodeWaitWrite); ASSERT(aiop->outcome == IOOpOutcomeInFlight); - /* We do need to handle POLLNVAL, but we do not need to do anything - * special for POLLERR or POLLHUP. (See man poll for details). - * The calling thread will typically try to do I/O after waiting - * for I/O readiness. Thus for POLLERR they'll discover the error - * when doing the I/O. And for POLLHUP, there is typically no - * action required anyway: it's a notification and not an error. - */ - if (aiop_poll_table[i].revents & POLLNVAL) { - aiop->outcome = IOOpOutcomeFailed; - aiop->error = EBADF; + if (aiop->notify_type == NotifyInternal) { + processSpecialFd(iomgr, aiop, &interrupt); + ncompletions--; + i++; } else { - aiop->outcome = IOOpOutcomeSuccess; - aiop->result = 0; + /* We do need to handle POLLNVAL, but we do not need to do anything + * special for POLLERR or POLLHUP. (See man poll for details). + * The calling thread will typically try to do I/O after waiting + * for I/O readiness. Thus for POLLERR they'll discover the error + * when doing the I/O. And for POLLHUP, there is typically no + * action required anyway: it's a notification and not an error. + */ + if (aiop_poll_table[i].revents & POLLNVAL) { + aiop->outcome = IOOpOutcomeFailed; + aiop->error = EBADF; + } else { + aiop->outcome = IOOpOutcomeSuccess; + aiop->result = 0; + } + + removeFromTables(iomgr, i); + notifyIOCompletion(iomgr, aiop); + ncompletions--; } - - removeFromTables(iomgr, i); - notifyIOCompletion(iomgr, aiop); - ncompletions--; } else { /* You'd expect incrementing the table index to be unconditional, * but we don't increment the index if we did process the entry, @@ -366,14 +433,44 @@ static bool processIOCompletions(CapIOManager *iomgr, int ncompletions) i++; } } + ASSERT(ncompletions == 0); return interrupt; } -void pollCompletedTimeoutsOrIOPoll(CapIOManager *iomgr) +static void processSpecialFd(CapIOManager *iomgr, + StgAsyncIOOp *aiop, bool *interrupt) { - ASSERT(iomgr->aiop_poll_table == iomgr->full_poll_table+1); + ASSERT(aiop->notify_type == NotifyInternal); + enum SpecialFd special_fd_kind = aiop->internal; + int fd = aiop->fd; + + switch (special_fd_kind) { +#if defined(HAVE_PREEMPTION) + case InterruptFd: + ASSERT(fd == iomgr->interrupt_fd_r); + collectFdWakeup(fd); + *interrupt = true; + debugTrace(DEBUG_iomanager, "Received interrupt in poll I/O manager"); + break; +#endif +#if defined(RTS_USER_SIGNALS) + /* See Note [Non-racy signal handling using the self-pipe trick] */ + case SignalFd: + ASSERT(fd == iomgr->shared->signal_fd_r); + int sig; siginfo_t info; + readSignalInfoFromPipe(&sig, &info, fd); + startSignalHandler(iomgr->cap, sig, &info); + debugTrace(DEBUG_iomanager, + "Dispatched signal %d in I/O manager", sig); + break; +#endif + } +} + +void pollCompletedTimeoutsOrIOPoll(CapIOManager *iomgr) +{ if (!isEmptyTimeoutQueue(iomgr->timeout_queue)) { Time now = getProcessElapsedTime(); processTimeoutCompletions(iomgr, now); @@ -381,29 +478,20 @@ void pollCompletedTimeoutsOrIOPoll(CapIOManager *iomgr) if (!isEmptyClosureTable(&iomgr->aiop_table)) { - int n_aiops = sizeClosureTable(&iomgr->aiop_table); -#if defined(HAVE_PREEMPTION) - /* the full_poll_table includes interrupt_fd_r */ - nfds_t nfds = n_aiops + 1; - struct pollfd *poll_table = iomgr->full_poll_table; -#else - /* the aiop_poll_table does not include interrupt_fd_r */ - nfds_t nfds = n_aiops + 0; - struct pollfd *poll_table = iomgr->aiop_poll_table; -#endif + nfds_t nfds = sizeClosureTable(&iomgr->aiop_table); /* Poll for I/O readiness, without waiting. */ #if defined(HAVE_DECL_PPOLL) && HAVE_DECL_PPOLL == 1 /* We could use poll here, since we use no timeout, but for consistency we use the same syscall as at the other call site. */ struct timespec tv = (struct timespec) { .tv_sec = 0, .tv_nsec = 0 }; - int res = ppoll(poll_table, nfds, &tv, NULL); + int res = ppoll(iomgr->aiop_poll_table, nfds, &tv, NULL); debugTrace(DEBUG_iomanager, "ppoll(nfds = %lu, timeout = 0) = %d", (unsigned long) nfds, res); #else - int res = poll(poll_table, nfds, 0); + int res = poll(iomgr->aiop_poll_table, nfds, 0); debugTrace(DEBUG_iomanager, "poll(nfds = %lu, timeout = 0) = %d", @@ -426,10 +514,6 @@ void pollCompletedTimeoutsOrIOPoll(CapIOManager *iomgr) reportPollError(res, nfds); } } - -#if defined(RTS_USER_SIGNALS) - startPendingSignalHandlers(iomgr->cap); -#endif } @@ -437,8 +521,6 @@ bool awaitCompletedTimeoutsOrIOPoll(CapIOManager *iomgr) { bool interrupt = false; /* got woken up via interruptIOManager */ - ASSERT(iomgr->aiop_poll_table == iomgr->full_poll_table+1); - /* Loop until we've woken up some threads. This loop is needed because the * poll() timing isn't accurate, we sometimes sleep for a while but not * long enough to wake up a thread in a threadDelay. Or we may need to @@ -461,22 +543,14 @@ bool awaitCompletedTimeoutsOrIOPoll(CapIOManager *iomgr) */ bool wait = emptyRunQueue(iomgr->cap); - int n_aiops = sizeClosureTable(&iomgr->aiop_table); -#if defined(HAVE_PREEMPTION) - /* the full_poll_table includes interrupt_fd_r */ - nfds_t nfds = n_aiops + 1; - struct pollfd *poll_table = iomgr->full_poll_table; -#else - /* the aiop_poll_table does not include interrupt_fd_r */ - nfds_t nfds = n_aiops + 0; - struct pollfd *poll_table = iomgr->aiop_poll_table; -#endif - /* Decide if we are going to wait if no I/O is ready, either: * poll only, wait indefinitely, or wait until a timeout. */ Time timeout = timeoutWaitTime(iomgr, wait, now); + nfds_t nfds = sizeClosureTable(&iomgr->aiop_table); + int n_aiops = nfds - iomgr->n_special_fds; + if (RTS_UNLIKELY(timeout == -1 && n_aiops == 0)) { /* See Note [Deadlock detection without idle GC] */ interrupt = notifyIdleGcDeadlock(); @@ -489,7 +563,7 @@ bool awaitCompletedTimeoutsOrIOPoll(CapIOManager *iomgr) * time, and if that timeout is reached then we schedule an idle GC * (see handleIdleGcTimeout below). */ - bool any_pending_io = nfds > 0; + bool any_pending_io = n_aiops > 0; bool timeout_idlegc_delay; adjustTimeoutForIdleGc(any_pending_io, &timeout, &timeout_idlegc_delay); #endif @@ -497,14 +571,14 @@ bool awaitCompletedTimeoutsOrIOPoll(CapIOManager *iomgr) /* Check for I/O readiness, possibly waiting. */ #if defined(HAVE_DECL_PPOLL) && HAVE_DECL_PPOLL == 1 struct timespec ts, *timeout_ns = timeoutAsTimespec(timeout, &ts); - int res = ppoll(poll_table, nfds, timeout_ns, NULL); + int res = ppoll(iomgr->aiop_poll_table, nfds, timeout_ns, NULL); debugTrace(DEBUG_iomanager, "ppoll(nfds = %lu, timeout = %" FMT_Int64 " ns) = %d", (unsigned long) nfds, timeout, res); #else int timeout_ms = timeoutAsPollTimeout(timeout); - int res = poll(poll_table, nfds, timeout_ms); + int res = poll(iomgr->aiop_poll_table, nfds, timeout_ms); debugTrace(DEBUG_iomanager, "poll(nfds = %lu, timeout = %d ms) = %d", @@ -534,20 +608,23 @@ bool awaitCompletedTimeoutsOrIOPoll(CapIOManager *iomgr) */ } else if (errno == EINTR) { - /* We got interrupted by a signal. */ - -#if defined(RTS_USER_SIGNALS) - /* Start any corresponding user signal handlers. If any, the run - * queue will become non-empty and we will drop out of the loop. - */ - startPendingSignalHandlers(iomgr->cap); -#endif - - /* We can also be interrupted by the shutdown signal handler, which - * will set sched_state and so cause us to drop out of the loop. + /* We got interrupted by a signal. + * + * Signals of note: + * + signals with a user handler: these will be delivered to us + * via the signal_fd_r pipe, which we handle by starting new + * threads, which will terminate the loop (non-empty run queue). + * + The backup ctl-c signal handler sets sched_state, and to avoid + * races, it also calls interruptIOManagerPoll (which causes + * interrupt_fd_r to become ready). We're in the EINTR case here + * so we know the signal arrived while we were waiting, so it is + * the getSchedState condition that will will terminate the loop. + * (If the signal arrives before the call to poll then we will + * notice interrupt_fd_r is ready and set interrupt=true, which + * will terminate the loop). * - * For any other interruption (e.g. timer) we will go round the - * do-while loop again. + * For any other interruption we will go round the do-while loop + * again. */ } else { @@ -585,11 +662,20 @@ void interruptIOManagerPoll(CapIOManager *iomgr USED_IF_PREEMPTION) } +#if defined(RTS_USER_SIGNALS) +static void signal_handler(int sig, siginfo_t *info, void *p STG_UNUSED) +{ + int signal_fd_w = getCapability(0)->iomgr->shared->signal_fd_w; + writeSignalInfoToPipe(sig, info, signal_fd_w); +} + + int installUserSignalHandlerPoll(int sig, int spi, sigset_t *mask, bool nocldstop) { - return installSignalHandler(sig, spi, mask, nocldstop, generic_handler); + return installSignalHandler(sig, spi, mask, nocldstop, signal_handler); } +#endif /* Helper function to double the size of the aiop_table and aiop_poll_table. @@ -602,14 +688,11 @@ static bool enlargeTables(CapIOManager *iomgr) bool ok = enlargeClosureTable(iomgr->cap, &iomgr->aiop_table, newcapacity); if (RTS_UNLIKELY(!ok)) return false; - /* Update the auxiliary aiop_poll_table to match. The full_poll_table is - * one bigger than the aiop_poll_table, since it has an extra entry at the - * front for interrupt_fd_r, with no corresponding aiop. */ - iomgr->full_poll_table = - stgReallocBytes(iomgr->full_poll_table, - sizeof(struct pollfd) * (newcapacity+1), + /* Update the auxiliary aiop_poll_table to match. */ + iomgr->aiop_poll_table = + stgReallocBytes(iomgr->aiop_poll_table, + sizeof(struct pollfd) * newcapacity, "Poll.c: enlargeTables"); - iomgr->aiop_poll_table = iomgr->full_poll_table+1; /* Initialise the new part of the aiop_poll_table */ struct pollfd *aiop_poll_table = iomgr->aiop_poll_table; ===================================== rts/posix/Poll.h ===================================== @@ -18,6 +18,7 @@ void initCapabilityIOManagerPoll(CapIOManager *iomgr); void freeCapabilityIOManagerPoll(CapIOManager *iomgr); +void startCapabilityIOManagerPoll(CapIOManager *iomgr); /* Synchronous I/O and timer operations */ IOSubmitResult syncIOWaitReadyPoll(CapIOManager *iomgr, StgTSO *tso, ===================================== rts/posix/Select.c ===================================== @@ -55,6 +55,8 @@ #define TimeToLowResTimeRoundUp(t) (t) #endif +static void startPendingSignalHandlers(Capability *cap); + void initCapabilityIOManagerSelect(CapIOManager *iomgr) { iomgr->blocked_queue_hd = END_TSO_QUEUE; @@ -524,10 +526,97 @@ awaitCompletedTimeoutsOrIOSelect(CapIOManager *iomgr, bool wait) return !interrupt; } + +/* ----------------------------------------------------------------------------- + * Low-level signal handler + * + * Places the requested handler on a stack of pending handlers to be + * started up at the next context switch. + * -------------------------------------------------------------------------- */ + +#define N_PENDING_HANDLERS 16 + +siginfo_t pending_handler_buf[N_PENDING_HANDLERS]; +siginfo_t *next_pending_handler = pending_handler_buf; + +static void +signal_handler(int sig STG_UNUSED, siginfo_t *info, void *p STG_UNUSED) +{ + + /* Can't call allocate from here. Probably can't call malloc + either. However, we have to schedule a new thread somehow. + + It's probably ok to request a context switch and allow the + scheduler to start the handler thread, but how do we + communicate this to the scheduler? + + We need some kind of locking, but with low overhead (i.e. no + blocking signals every time around the scheduler). + + Signal Handlers are atomic (i.e. they can't be interrupted), and + we can make use of this. We just need to make sure the + critical section of the scheduler can't be interrupted - the + only way to do this is to block signals. However, we can lower + the overhead by only blocking signals when there are any + handlers to run, i.e. the set of pending handlers is + non-empty. + */ + + /* We use a stack to store the pending signals. We can't + dynamically grow this since we can't allocate any memory from + within a signal handler. + + Hence unfortunately we have to bomb out if the buffer + overflows. It might be acceptable to carry on in certain + circumstances, depending on the signal. + */ + + memcpy(next_pending_handler, info, sizeof(siginfo_t)); + + next_pending_handler++; + + // stack full? + if (next_pending_handler == &pending_handler_buf[N_PENDING_HANDLERS]) { + errorBelch("too many pending signals"); + stg_exit(EXIT_FAILURE); + } + + interruptCapability(&MainCapability); + +} + int installUserSignalHandlerSelect(int sig, int spi, sigset_t *mask, bool nocldstop) { - return installSignalHandler(sig, spi, mask, nocldstop, generic_handler); + return installSignalHandler(sig, spi, mask, nocldstop, signal_handler); +} + +static void startPendingSignalHandlers(Capability *cap) +{ + if (next_pending_handler == pending_handler_buf) return; + + // Temporarily mask all signals to stop the signal_handler from mutating + // next_pending_handler and pending_handler_buf while we are looking at + // and modifying them. + static sigset_t maskSignals, savedSignals; + sigfillset(&maskSignals); + pthread_sigmask(SIG_SETMASK, &maskSignals, &savedSignals); + + while (next_pending_handler != pending_handler_buf) { + + next_pending_handler--; + + siginfo_t *info = next_pending_handler; + int sig = info->si_signo; + startSignalHandler(cap, sig, info); + + // It's possible by the time the signal handler runs that the handler + // has changed: runHandlers will run whatever handler is set at the + // time (or none). + } + + // Restore the signal mask. + pthread_sigmask(SIG_SETMASK, &savedSignals, NULL); } #endif /* IOMGR_ENABLED_SELECT */ ===================================== rts/posix/SelectBis.c ===================================== @@ -121,6 +121,17 @@ The CapIOManager structure for this I/O manager contains: ClosureTable aiop_table; StgTimeoutQueue *timeout_queue; int interrupt_fd_r, interrupt_fd_w; + int n_special_fds; + +In addition to aiops, there are some special internal fds we have to monitor +and respond to. In particular there is the interrupt_fd_r used to implement +interruptIOManager. Whether this fd is used or not depends on configuration +(use of interrupt_fd_r depends on HAVE_PREEMPTION). To enable multiple such +fds and allow them to be optional we use the following scheme: we create +special internal AsyncIOOps and put them into the aiop_table. These are +treated much like other IOOpCodeWaitRead operations but with special handling +for notification. The variable n_special_fds tracks how many such entries we +have so that we can handle them specially but in a reasonably uniform manner. ******************************************************************************/ @@ -133,7 +144,20 @@ static int collectFdSets(CapIOManager *iomgr); static void processBadFds(CapIOManager *iomgr); static void reportSelectError(void) STG_NORETURN; +enum SpecialFd { + InterruptFd, /* iomgr->interrupt_fd_r for interruptIOManager */ + SignalFd /* iomgr->shared->signal_fd_r */ +}; +static void insertSpecialFd(CapIOManager *iomgr, + enum SpecialFd special_fd_kind, + int special_fd); +static void processSpecialFd(CapIOManager *iomgr, + StgAsyncIOOp *aiop, bool *interrupt); + +/* Called early in RTS startup _before_ the storage manager is initialised, + * so this is not allowed to allocate on the GC heap. + */ void initCapabilityIOManagerSelectBis(CapIOManager *iomgr) { initClosureTable(&iomgr->aiop_table, ClosureTableCompact); @@ -145,12 +169,21 @@ void initCapabilityIOManagerSelectBis(CapIOManager *iomgr) /* Would never happen in a standalone process, but could plausibly happen * if the RTS is used within another process that already has many open fds. */ - if (iomgr->interrupt_fd_r < 0 || iomgr->interrupt_fd_r >= (int)FD_SETSIZE || - iomgr->interrupt_fd_w < 0 || iomgr->interrupt_fd_w >= (int)FD_SETSIZE) { + if (iomgr->interrupt_fd_r >= (int)FD_SETSIZE || + iomgr->interrupt_fd_w >= (int)FD_SETSIZE) { barf("initCapabilityIOManagerSelectBis: fds out of select range"); } #endif + iomgr->n_special_fds = 0 +#if defined(HAVE_PREEMPTION) + + 1 +#endif +#if defined(RTS_USER_SIGNALS) + + (iomgr->cap->no == 0 ? 1 : 0) +#endif + ; + iomgr->rfds = stgMallocBytes(sizeof (fd_set), "IOManagerSelectBis"); iomgr->wfds = stgMallocBytes(sizeof (fd_set), "IOManagerSelectBis"); @@ -158,8 +191,62 @@ void initCapabilityIOManagerSelectBis(CapIOManager *iomgr) /* Signal handlers are shared across all caps. Only init once. */ if (iomgr->cap->no == 0) { initUserSignals(); + /* See Note [Non-racy signal handling using the self-pipe trick] */ + newNonBlockingPipe(&iomgr->shared->signal_fd_r, + &iomgr->shared->signal_fd_w); + if (iomgr->shared->signal_fd_r >= (int)FD_SETSIZE || + iomgr->shared->signal_fd_w >= (int)FD_SETSIZE) { + barf("initCapabilityIOManagerSelectBis: fds out of select range"); + } + } +#endif +} + + +/* Called late in RtsStartup, once heap allocation is ready. */ +void startCapabilityIOManagerSelectBis(CapIOManager *iomgr) +{ + /* ClosureTable allocates on the heap, so we can do it here not in init */ + if (iomgr->n_special_fds > 0) { + bool ok = enlargeClosureTable(iomgr->cap, &iomgr->aiop_table, + iomgr->n_special_fds); + CHECK(ok); + } + +#if defined(HAVE_PREEMPTION) + insertSpecialFd(iomgr, InterruptFd, iomgr->interrupt_fd_r); +#endif +#if defined(RTS_USER_SIGNALS) + if (iomgr->cap->no == 0) { + /* See Note [Non-racy signal handling using the self-pipe trick] */ + insertSpecialFd(iomgr, SignalFd, iomgr->shared->signal_fd_r); } #endif + ASSERT(iomgr->n_special_fds == sizeClosureTable(&iomgr->aiop_table)); +} + + +/* For special fds, we add a dummy entry to the aiop_table and a + * corresponding real entry to the aiop_poll_table. + */ +static void insertSpecialFd(CapIOManager *iomgr, + enum SpecialFd special_fd_kind, + int special_fd) +{ + StgAsyncIOOp *aiop = (StgAsyncIOOp *)allocate(iomgr->cap, + sizeofW(StgAsyncIOOp)); + SET_HDR(aiop, &stg_ASYNCIOOP_info, CCS_SYSTEM); + aiop->notify.unused = &stg_ASYNCIO_LIVE0_closure; + aiop->live = &stg_ASYNCIO_LIVE0_closure; + aiop->capno = iomgr->cap->no; + aiop->notify_type = NotifyInternal; + aiop->outcome = IOOpOutcomeInFlight; + aiop->operation = IOOpCodeWaitRead; + aiop->fd = special_fd; + aiop->internal = special_fd_kind; + + int ix = insertClosureTable(iomgr->cap, &iomgr->aiop_table, aiop); + aiop->index = ix; } @@ -168,6 +255,8 @@ void freeCapabilityIOManagerSelectBis(CapIOManager *iomgr) #if defined(RTS_USER_SIGNALS) if (iomgr->cap->no == 0) { finiUserSignals(); + close(iomgr->shared->signal_fd_r); + close(iomgr->shared->signal_fd_w); } #endif stgFree(iomgr->rfds); @@ -316,20 +405,9 @@ static bool processIOCompletions(CapIOManager *iomgr, int ncompletions) ASSERT(ncompletions > 0); bool interrupt = false; -#if defined(HAVE_PREEMPTION) - /* If the interrupt_fd_r is ready, collect it */ - if (FD_ISSET(iomgr->interrupt_fd_r, iomgr->rfds)) { - collectFdWakeup(iomgr->interrupt_fd_r); - ncompletions--; - interrupt = true; - debugTrace(DEBUG_iomanager, "Received interrupt in poll I/O manager"); - } -#endif int i = 0; - while (ncompletions > 0) { - ASSERT(i < sizeClosureTable(&iomgr->aiop_table)); - + while (ncompletions > 0 && i < sizeClosureTable(&iomgr->aiop_table)) { StgAsyncIOOp *aiop = indexClosureTable(&iomgr->aiop_table, i); int fd = aiop->fd; enum IOOpCode op = aiop->operation; @@ -339,11 +417,17 @@ static bool processIOCompletions(CapIOManager *iomgr, int ncompletions) if (op == IOOpCodeWaitRead ? FD_ISSET(fd, iomgr->rfds) : FD_ISSET(fd, iomgr->wfds)) { - aiop->outcome = IOOpOutcomeSuccess; - aiop->result = 0; - removeFromTables(iomgr, i); - notifyIOCompletion(iomgr, aiop); - ncompletions--; + if (aiop->notify_type == NotifyInternal) { + processSpecialFd(iomgr, aiop, &interrupt); + ncompletions--; + i++; + } else { + aiop->outcome = IOOpOutcomeSuccess; + aiop->result = 0; + removeFromTables(iomgr, i); + notifyIOCompletion(iomgr, aiop); + ncompletions--; + } } else { /* You'd expect incrementing the table index to be unconditional, * but we don't increment the index if we did process the entry, @@ -353,10 +437,42 @@ static bool processIOCompletions(CapIOManager *iomgr, int ncompletions) i++; } } + ASSERT(ncompletions == 0); return interrupt; } +static void processSpecialFd(CapIOManager *iomgr, + StgAsyncIOOp *aiop, bool *interrupt) +{ + ASSERT(aiop->notify_type == NotifyInternal); + enum SpecialFd special_fd_kind = aiop->internal; + int fd = aiop->fd; + + switch (special_fd_kind) { +#if defined(HAVE_PREEMPTION) + case InterruptFd: + ASSERT(fd == iomgr->interrupt_fd_r); + collectFdWakeup(fd); + *interrupt = true; + debugTrace(DEBUG_iomanager, "Received interrupt in poll I/O manager"); + break; +#endif +#if defined(RTS_USER_SIGNALS) + /* See Note [Non-racy signal handling using the self-pipe trick] */ + case SignalFd: + ASSERT(fd == iomgr->shared->signal_fd_r); + int sig; siginfo_t info; + readSignalInfoFromPipe(&sig, &info, fd); + startSignalHandler(iomgr->cap, sig, &info); + debugTrace(DEBUG_iomanager, + "Dispatched signal %d in I/O manager", sig); + break; +#endif + } +} + + void pollCompletedTimeoutsOrIOSelectBis(CapIOManager *iomgr) { if (!isEmptyTimeoutQueue(iomgr->timeout_queue)) { @@ -397,10 +513,6 @@ void pollCompletedTimeoutsOrIOSelectBis(CapIOManager *iomgr) reportSelectError(); } } - -#if defined(RTS_USER_SIGNALS) - startPendingSignalHandlers(iomgr->cap); -#endif } @@ -440,8 +552,10 @@ bool awaitCompletedTimeoutsOrIOSelectBis(CapIOManager *iomgr) */ Time timeout = timeoutWaitTime(iomgr, wait, now); - if (RTS_UNLIKELY(timeout == -1 && - isEmptyClosureTable(&iomgr->aiop_table))) { + int nfds = sizeClosureTable(&iomgr->aiop_table); + int n_aiops = nfds - iomgr->n_special_fds; + + if (RTS_UNLIKELY(timeout == -1 && n_aiops == 0)) { /* See Note [Deadlock detection without idle GC] */ interrupt = notifyIdleGcDeadlock(); if (interrupt) break; @@ -453,7 +567,7 @@ bool awaitCompletedTimeoutsOrIOSelectBis(CapIOManager *iomgr) * time, and if that timeout is reached then we schedule an idle GC * (see handleIdleGcTimeout below). */ - bool any_pending_io = maxfd >= 0; + bool any_pending_io = n_aiops > 0; bool timeout_idlegc_delay; adjustTimeoutForIdleGc(any_pending_io, &timeout, &timeout_idlegc_delay); #endif @@ -479,7 +593,7 @@ bool awaitCompletedTimeoutsOrIOSelectBis(CapIOManager *iomgr) } else if (res > 0) { int ncompletions = res; - ASSERT(ncompletions <= sizeClosureTable(&iomgr->aiop_table)+1); + ASSERT(ncompletions <= nfds); interrupt = processIOCompletions(iomgr, ncompletions); /* We do _not_ need to check for timeouts again here. We know that * select() returned due to I/O being ready before the timeout. If @@ -489,20 +603,23 @@ bool awaitCompletedTimeoutsOrIOSelectBis(CapIOManager *iomgr) */ } else if (errno == EINTR) { - /* We got interrupted by a signal. */ - -#if defined(RTS_USER_SIGNALS) - /* Start any corresponding user signal handlers. If any, the run - * queue will become non-empty and we will drop out of the loop. - */ - startPendingSignalHandlers(iomgr->cap); -#endif - - /* We can also be interrupted by the shutdown signal handler, which - * will set sched_state and so cause us to drop out of the loop. + /* We got interrupted by a signal. * - * For any other interruption (e.g. timer) we will go round the - * do-while loop again. + * Signals of note: + * + signals with a user handler: these will be delivered to us + * via the signal_fd_r pipe, which we handle by starting new + * threads, which will terminate the loop (non-empty run queue). + * + The backup ctl-c signal handler sets sched_state, and to avoid + * races, it also calls interruptIOManagerPoll (which causes + * interrupt_fd_r to become ready). We're in the EINTR case here + * so we know the signal arrived while we were waiting, so it is + * the getSchedState condition that will will terminate the loop. + * (If the signal arrives before the call to poll then we will + * notice interrupt_fd_r is ready and set interrupt=true, which + * will terminate the loop). + * + * For any other interruption we will go round the do-while loop + * again. */ } else if (errno == EBADF) { @@ -595,11 +712,20 @@ void interruptIOManagerSelectBis(CapIOManager *iomgr USED_IF_PREEMPTION) } +#if defined(RTS_USER_SIGNALS) +static void signal_handler(int sig, siginfo_t *info, void *p STG_UNUSED) +{ + int signal_fd_w = getCapability(0)->iomgr->shared->signal_fd_w; + writeSignalInfoToPipe(sig, info, signal_fd_w); +} + + int installUserSignalHandlerSelectBis(int sig, int spi, sigset_t *mask, bool nocldstop) { - return installSignalHandler(sig, spi, mask, nocldstop, generic_handler); + return installSignalHandler(sig, spi, mask, nocldstop, signal_handler); } +#endif /* Helper function to double the size of the aiop_table. @@ -647,15 +773,6 @@ static int collectFdSets(CapIOManager *iomgr) FD_ZERO(iomgr->rfds); FD_ZERO(iomgr->wfds); -#if defined(HAVE_PREEMPTION) - /* We're always interested in our interrupt fd */ - { - int fd = iomgr->interrupt_fd_r; - maxfd = (fd > maxfd) ? fd : maxfd; - FD_SET(fd, iomgr->rfds); - } -#endif - for (int ix = 0; ix < nentries; ix++) { StgAsyncIOOp *aiop = indexClosureTable(&iomgr->aiop_table, ix); int fd = aiop->fd; ===================================== rts/posix/SelectBis.h ===================================== @@ -38,6 +38,7 @@ void initCapabilityIOManagerSelectBis(CapIOManager *iomgr); void freeCapabilityIOManagerSelectBis(CapIOManager *iomgr); +void startCapabilityIOManagerSelectBis(CapIOManager *iomgr); /* Synchronous I/O and timer operations */ IOSubmitResult syncIOWaitReadySelectBis(CapIOManager *iomgr, StgTSO *tso, View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/0267a1656213f118407577e4cb46114... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/0267a1656213f118407577e4cb46114... You're receiving this email because of your account on gitlab.haskell.org. Manage all notifications: https://gitlab.haskell.org/-/profile/notifications | Help: https://gitlab.haskell.org/help