Repository navigation
Add signal.get_wakeup_fd() #145638
Description
Feature or enhancement
Proposal:
We have signal.set_wakeup_fd(), which sets a new file descriptor and returns the old one. However, there are at least three cases where we want to get the current wakeup fd without setting a new one:
- Obtaining debug information about the current state of the process, which also applies to tests (how can we ensure that a particular function actually sets the wakeup fd?).
- Raising warnings if it has been changed (reset to -1?) in some scope we are considering.
- Conditionally setting the wakeup fd depending on whether it has been changed in another scope.
The reason I opened this issue relates to the third case. I have an "inner" wakeup fd (guest) and an "outer" wakeup fd (host). When the outer wakeup fd is either not set (equal to -1) or equal to the inner wakeup fd, it is updated along with the inner one. In this case:
- If I do not set/get wakeup fd, I will not know the current inner wakeup fd, and as a result, I will not be able to perform checks.
- If I always set the outer wakeup fd when exiting the inner scope, then in case of equality, this may happen when the wakeup fd has already been closed (and reset to -1) in the inner scope, which in turn will lead to either an
OSErroror a possible write to a false file descriptor (since file descriptors are reusable). - I also cannot set the inner wakeup fd, since it could have been updated in the inner scope.
- The code in the inner scope is not under my control.
The simplest implementation of signal.get_wakeup_fd() at the Python level looks like this:
import signal
def get_wakeup_fd():
fd = signal.set_wakeup_fd(-1)
signal.set_wakeup_fd(fd)
return fdHowever, it has two drawbacks:
- We lose the current value of the
warn_on_full_bufferparameter. - There is a race condition between the two
signal.set_wakeup_fd()calls (what if a signal is sent to the process during this time?), which can lead to lost signals (and thus the guest not waking up).
To solve the race condition problem, we would have to create our own wakeup pair, for example in the form of a non-blocking pipe, and send signals from the pipe to the current wakeup fd, effectively emulating the trip_signal() function. But this only sounds simple on Unix. On Windows:
-
It is not possible to create a non-blocking pipe using the public API without
ctypesuntil Python 3.12. The closest alternative using the private API would look like this:from _winapi import SetNamedPipeHandleState from msvcrt import get_osfhandle def set_blocking(pipefd, blocking, /): SetNamedPipeHandleState( get_osfhandle(pipefd), 0 if blocking else 1, None, None, )
-
We cannot use
os.write()if the wakeup fd is a socket. In this case, we would either have to try to handle sockets viasocket.fromfd(), or use an alternative approach withsignal.signal()+signal.raise_signal()(disable the current signal handlers, implicitly calltrip_signal()by resending signals to the current process, enable them back), but the latter is even more non-trivial and is unlikely to have a correct solution.
Meanwhile, at the signalmodule.c level, to get the current wakeup fd, it is enough to just get wakeup.fd. So why not add the corresponding function?
import signal
fd = signal.get_wakeup_fd()Has this already been discussed elsewhere?
This is a minor feature, which does not need previous discussion elsewhere
Links to previous discussion of this feature:
No response
Linked PRs
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsTodo
Activity
anand-official commentedon Mar 8, 2026
picnixz commentedon Mar 8, 2026
@anand-official Please avoid posting LLM-generated answers without reviewing them and curating them. There are un-necessary parts. And read https://devguide.python.org/getting-started/generative-ai/.
x42005e1f commentedon Mar 8, 2026
This would be counterintuitive (it contradicts the function name). And
set_wakeup_fd()also does not return the old value of thewarn_on_full_bufferparameter, so the different behavior would be inconsistent. This is certainly a problem, but there is nothing that can be done about it (sadly). In any save-restore sequence, users already have to ignore the oldwarn_on_full_buffervalue (see aioguest, trio), and adding a way to get the old value would only complicate signal handling.anand-official commentedon Mar 8, 2026
Thanks for the note , understood. I’ll make sure to review and curate responses more carefully.
I agree that get_wakeup_fd() should just return the fd to keep the API consistent with set_wakeup_fd(). Exposing the warn_on_full_buffer state would be out of place.
pythongh-145638: Add signal.get_wakeup_fd()
mvanhorn commentedon Mar 10, 2026
I've submitted an implementation in PR #145726. Adds
signal.get_wakeup_fd()that readswakeup.fddirectly without modifying it, avoiding the race condition in the current set/restore workaround.encukou commentedon Mar 12, 2026
Should we have a differently named function, then? A
get_wakeup()function that returns an object withfdandwarn_on_full_bufferattributes?mvanhorn commentedon Mar 12, 2026
My PR #145726 implements the simpler
get_wakeup_fd()(fd-only return). If the direction shifts to aget_wakeup()returning an object with bothfdandwarn_on_full_buffer, I'm happy to update #145726 to match that API, or close it if someone else wants to take it in a different direction.x42005e1f commentedon Mar 12, 2026
This would lead to more than one way of using
set_wakeup_fd()(with and without usingget_wakeup_?()), which I think is not in line with the Zen of Python. Instead ofset_wakeup_fd(new_fd)+set_wakeup_fd(old_fd), a more correct solution would beget_wakeup_?()+set_wakeup_fd(new_fd)+set_wakeup_fd(old_fd, warn_on_full_buffer=warn_on_full_buffer), and its problem is that it is less trivial. Moreover, restoring withwarn_on_full_buffer=Trueindicates a conflict: if a new wakeup fd was set, reading signal numbers via the old wakeup fd will not process all received signals.In my opinion, this only hides the lack of an explicit save-restore mechanism. Suppose we have
get_wakeup_token(),save_wakeup_token(), andrestore_wakeup_token(). Then we could suggest the following behavior:token = get_wakeup_token()returns the current information (in the form of token attributes):fd,warn_on_full_buffer, possiblyis_socket.outer_token = save_wakeup_token(maybe_token)returns the current information and replaces it with the passed one. If the token was not passed (isNone?),fd=-1is implied.inner_token = restore_wakeup_token(outer_token)also returns the current information and replaces it with the passed one, but the token is not optional. Both functions can be expressed as one (asset_wakeup_token()/reset_wakeup_token()), but separating them makes things clearer and prevents errors.This allows to notify about signals using different strategies (add a switch function?):
warn_on_full_buffer=True) will be notified. A relatively expensive method, but it allows all guests to be woken up (all guests will be able to execute their signal handlers).The second strategy is currently impossible to implement without using a wakeup pair, an additional thread, and monkey patching (to track the current wakeup fd). And unlike the approach I described in this issue, it allows to completely isolate the wakeup fd (the host will know nothing about the guests).
However, these are just thoughts, and the above seems like something that would require a PEP. But sorry, my browser is no longer supported by discuss.python.org, so we can forget about it until someone else needs it.
I would like to add some explanations about my approach with conditional setting. If the host does not set the wakeup fd, the main thread may not wake up when a signal arrives and thus may not execute signal handlers (there are several very old issues about this). Therefore, the guest spreads its wakeup fd to the host level while this is the case, thereby waking up the host (the main thread) when a signal arrives. Otherwise, this is not required, and the wakeup fds are isolated from each other (not least to prevent conflicts between guests of the same depth level).
The approach solves the problem of waking up the main thread, but not the problem of waking up the guest. Signal handlers registered by the guest's event loop will only be executed if the host does not use
set_wakeup_fd(). This may be sufficient in most cases, but it is still a limitation.encukou commentedon Mar 13, 2026
Yes, and more generally: how do you know whether the previous user of
set_wakeup_fdwants to get all the signals, or is happy with the new handler handling them?What's not clear to me: who/what would decide which strategy to use?
Maybe let's ladd a CAS mechanism?
signal.get_wakeup()gives you an object with readablefdandwarn_on_full_bufferattributes. (I don't like the term “token” for something you read your data from)signal.set_wakeup_fd's first argument will accept the object fromget_wakeup, in addition toint. (Renaming toset_wakeup, or adding it as a new function, would be “cleaner” but I don't think it's worth it.)signal.set_wakeup_fdgrows a new argument, saycheck_previous=None. If passed an object that's not the current wakeup settings (or None), it fails.This allows arbitrary checks/logic with the current state before you call
set_wakeup_fd, while staying “atomic” (if someone else won the race, you loop back toget_wakeup& retry everything).Still, a race can only happen if there are two users of
set_wakeup_fdthat aren't cooperating, which is not a good situation to be in :/A lot of a such a document's value is in the motivation section -- clearly documenting the use cases to be solved.
x42005e1f commentedon Mar 13, 2026
The documentation for
set_wakeup_fd()describes two approaches. The first involves reading signal numbers, while the second deals only with wakeups. If the user setswarn_on_full_buffer=True(the default), this implies that they want to know the actual signal numbers and perform their own handling. By itself,set_wakeup_fd()has no direct relation to signal handlers and is most often used simply to wake up the main thread (warn_on_full_buffer=False). Note that there is no way to separate the set of signals handled by the guest from the set of signals handled by the host, and therefore, in the worst case, signal numbers will have to be sent to both wakeup pairs, unless the host usesset_wakeup_fd()only for wakeups (in which case the host can be ignored, since the guest will wake up the host itself as it runs in the host's context).I am not sure. Probably the application designer, since both approaches have their drawbacks. But I think it is also sufficient to implement only the second strategy.
You can see tokens in contextvars; you can see tokens in Trio. To me, "get wakeup" sounds a bit odd (unless you remember that "wakeup" is an object).
Even if we rename the function, we will still have to maintain the old alias. If we do not rename the function, then another name for the getter would be inconsistent. Note that
get_wakeup_fd()does not prevent the addition of another family of functions, and they serve different purposes (conditional setting is just one case, and for everything else, obtaining additional information would be redundant). However, this does not solve the problem with the return value ofset_wakeup_fd()(if we do not get the "wakeup" object, we cannot set the nesting for the second strategy, butset_wakeup_fd()can also be used simply to set a different file descriptor in the same context).Not sure what race you are talking about.
set_wakeup_fd()only works from the main thread. And the problem inherent in an uncontrolledset_wakeup_fd(-1)call (such as the one made by asyncio event loops) applies to any global state.vstinner commentedon Mar 13, 2026
I don't understand your use case. Can you please elaborate? Can you provide an example?
There is a single wakeup fd per process, so I don't get the idea of setting it to a new value if it has been changed in another scope.
x42005e1f commentedon Mar 13, 2026
Here is why I opened this issue:
This code runs as a callback from the host (some event loop, such as a GUI), and
guest_tick()executes one pass of the guest's event loop. When a signal is sent to the guest's wakeup pair, it thread-safely reschedules itself and thereby wakes up the host. There are two problems with signal loss here:get_wakeup_fd()that would not create a race condition with other OS processes (e.g., child processes, if we want to handle SIGCHLD).guest_tick()may callset_wakeup_fd(-1), causing the host to miss the signal.I suggested
get_wakeup_fd()to solve at least the first problem and simplify debugging in the process. The second problem is irrelevant when the host usesset_wakeup_fd()only for wakeups, and it can only be solved using the second strategy:Here,
guest_tokenwill be a child ofhost_tokenas long as its file descriptor is not -1. And in line with the second strategy:set_wakeup_fd()call in the guest's context will apply only to its own token and will not prevent signals from being sent to the host.warn_on_full_buffer=True, it will receive signals regardless of the guest.I did not describe this family of functions when I opened the issue, since they address different problems. What they have in common is the problem of supporting signal handlers at the event loop level (see the relevant asyncio event loop functions).
get_wakeup_fd()allows to support them when the host is not usingset_wakeup_fd()and there are no other guests here. But ifget_wakeup_fd()is not atomic, then they cannot be considered supported at all.Related: #145342
x42005e1f commentedon Mar 13, 2026
For additional context. The host may call
set_wakeup_fd()at any time, for example when registering its own signal handlers, asasyncio.SelectorEventLoopdoes. Other guests that also spread their wakeup fd to the host level (hello, Trio) will also have an impact. Therefore, we cannot assume that the host usesset_wakeup_fd()only at the beginning and end of its lifetime (it may do so between guest ticks).Everything would be much simpler if asyncio registered not no-op via
signal.signal(), but its own waking up (self._write_to_self()). That said, there are still a lot of problems here that I have to deal with. In particular, because ofasyncio.ProactorEventLoop, which... I am really starting to dislike (it callsset_wakeup_fd()when creating and closing the event loop, thereby forcing even a simple call toloop_factory()to be isolated; it cancels the read from the wakeup pair when exitingloop.run_forever(), thereby scheduling a new callback that impliestimeout=0after every stop, and this is a particularly painful issue for which I no longer see a cleansolutionworkaround other than directly usingloop._run_once()to bypass setup/cleanup...).Actually, the above is also an alternative solution (the methods can be patched), but it is just a special case.
And it will not work if someone else registers other handlers. And so, in reality, registering anything other than a no-op does not actually solve anything, due to the same lack of control on the host side and other guests.x42005e1f commentedon Mar 17, 2026
Okay, I will solve my specific case using custom event loop subclasses and context-local monkey patching for
signal.set_wakeup_fd(). The first will allow me to change the approach used in asyncio towarn_on_full_buffer=False+signal.signal(signum, schedule_and_wakeup)(currently,warn_on_full_buffer=True+signal.signal(signum, noop)is used, which causes the signal dispatching problem); in a sense, it reverts fe5649c but still solves #65844 (which is irrelevant anyway after 4ae0149). The second will make the undesirable behavior fictitious and thereby prevent the guest from "eating" signals (relevant for hosts usingwarn_on_full_buffer=True). Handling the same signal by multiple guests (may be relevant for SIGINT) can be implemented via a shared signal handler.However, even with these changes,
signal.get_wakeup_fd()is still needed (no longer on the guest, but on the host, for the samehost_fd == -1 or host_fd == guest_fdcheck:host_fd = get_wakeup_fd()).Related: #88378 (for the case of multiple guests)
encukou commentedon Mar 17, 2026
I'm labelling this
asyncio: even though it's not a change there, reviewing the motivating use cases needs an asyncio expert. I'm out of my (current) depth here; I'm also wary of making the simplest change to make one use case work.Ultimately, this needs a wider discussion. If you can use another browser long enough to subscribe to Discourse's mailing list mode, it would make things much easier.
Vizonex commentedon Apr 9, 2026
I would be down to implement this later in the future without the use AI generated materials. But if we manage to make this. Could we add this to CPython's C-API and victor's cpython-api-compat possibly? I think this would be a good addition for uvloop and winloop to have.