Skip to content

Add signal.get_wakeup_fd() #145638

Open
Open

Description

@x42005e1f

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:

  1. 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?).
  2. Raising warnings if it has been changed (reset to -1?) in some scope we are considering.
  3. 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 OSError or 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 fd

However, it has two drawbacks:

  1. We lose the current value of the warn_on_full_buffer parameter.
  2. 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:

  1. It is not possible to create a non-blocking pipe using the public API without ctypes until 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,
        )
  2. We cannot use os.write() if the wakeup fd is a socket. In this case, we would either have to try to handle sockets via socket.fromfd(), or use an alternative approach with signal.signal()+signal.raise_signal() (disable the current signal handlers, implicitly call trip_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

Activity

  1. anand-official commented on Mar 8, 2026

    @anand-official
  2. pinned and unpinned a comment on Mar 8, 2026
  3. picnixz commented on Mar 8, 2026

    @picnixz
    Member

    @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/.

  4. x42005e1f commented on Mar 8, 2026

    @x42005e1f
    ContributorAuthor

    Should get_wakeup_fd() also return the warn_on_full_buffer state?

    This would be counterintuitive (it contradicts the function name). And set_wakeup_fd() also does not return the old value of the warn_on_full_buffer parameter, 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 old warn_on_full_buffer value (see aioguest, trio), and adding a way to get the old value would only complicate signal handling.

  5. anand-official commented on Mar 8, 2026

    @anand-official

    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.

  6. added a commit that references this issue on Mar 10, 2026
  7. mvanhorn commented on Mar 10, 2026

    @mvanhorn
    Contributor

    I've submitted an implementation in PR #145726. Adds signal.get_wakeup_fd() that reads wakeup.fd directly without modifying it, avoiding the race condition in the current set/restore workaround.

  8. encukou commented on Mar 12, 2026

    @encukou
    Member

    [returning warn_on_full_buffer] would be counterintuitive (it contradicts the function name).

    Should we have a differently named function, then? A get_wakeup() function that returns an object with fd and warn_on_full_buffer attributes?

  9. mvanhorn commented on Mar 12, 2026

    @mvanhorn
    Contributor

    My PR #145726 implements the simpler get_wakeup_fd() (fd-only return). If the direction shifts to a get_wakeup() returning an object with both fd and warn_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.

  10. x42005e1f commented on Mar 12, 2026

    @x42005e1f
    ContributorAuthor

    Should we have a differently named function, then? A get_wakeup() function that returns an object with fd and warn_on_full_buffer attributes?

    This would lead to more than one way of using set_wakeup_fd() (with and without using get_wakeup_?()), which I think is not in line with the Zen of Python. Instead of set_wakeup_fd(new_fd)+set_wakeup_fd(old_fd), a more correct solution would be get_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 with warn_on_full_buffer=True indicates 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(), and restore_wakeup_token(). Then we could suggest the following behavior:

    1. token = get_wakeup_token() returns the current information (in the form of token attributes): fd, warn_on_full_buffer, possibly is_socket.
    2. outer_token = save_wakeup_token(maybe_token) returns the current information and replaces it with the passed one. If the token was not passed (is None?), fd=-1 is implied.
    3. 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 (as set_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?):

    1. Send signal numbers via the current token's fd. Corresponds to the current behavior. The simplest way to wake up the main thread, but not suitable for guests (since they will not be woken up and therefore will not be able to execute their signal handlers registered by the event loop).
    2. Send signal numbers via the fd of the "deepest" token. The information will have to be stored as a list of lists or a tree (perhaps storing child tokens as an attribute of the parent?). Notification will be done by walking the tree, where only the fds of tokens that have no children (or have 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.

  11. encukou commented on Mar 13, 2026

    @encukou
    Member

    restoring with warn_on_full_buffer=True indicates a conflict: if a new wakeup fd was set, reading signal numbers via the old wakeup fd will not process all received signals.

    Yes, and more generally: how do you know whether the previous user of set_wakeup_fd wants to get all the signals, or is happy with the new handler handling them?

    This allows to notify about signals using different strategies (add a switch function?)

    What's not clear to me: who/what would decide which strategy to use?

    Moreover, restoring with warn_on_full_buffer=True indicates a conflict: if a new wakeup fd was set, reading signal numbers via the old wakeup fd will not process all received signals.


    Maybe let's ladd a CAS mechanism?

    • new function signal.get_wakeup() gives you an object with readable fd and warn_on_full_buffer attributes. (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 from get_wakeup, in addition to int. (Renaming to set_wakeup, or adding it as a new function, would be “cleaner” but I don't think it's worth it.)
    • signal.set_wakeup_fd grows a new argument, say check_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 to get_wakeup & retry everything).
    Still, a race can only happen if there are two users of set_wakeup_fd that aren't cooperating, which is not a good situation to be in :/


    However, these are just thoughts, and the above seems like something that would require a PEP.

    A lot of a such a document's value is in the motivation section -- clearly documenting the use cases to be solved.

  12. x42005e1f commented on Mar 13, 2026

    @x42005e1f
    ContributorAuthor

    Yes, and more generally: how do you know whether the previous user of set_wakeup_fd wants to get all the signals, or is happy with the new handler handling them?

    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 sets warn_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 uses set_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).

    What's not clear to me: who/what would decide which strategy to use?

    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.

    I don't like the term “token” for something you read your data from

    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).

    Renaming to set_wakeup, or adding it as a new function, would be “cleaner” but I don't think it's worth it.

    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 of set_wakeup_fd() (if we do not get the "wakeup" object, we cannot set the nesting for the second strategy, but set_wakeup_fd() can also be used simply to set a different file descriptor in the same context).

    Still, a race can only happen if there are two users of set_wakeup_fd that aren't cooperating, which is not a good situation to be in :/

    Not sure what race you are talking about. set_wakeup_fd() only works from the main thread. And the problem inherent in an uncontrolled set_wakeup_fd(-1) call (such as the one made by asyncio event loops) applies to any global state.

  13. vstinner commented on Mar 13, 2026

    @vstinner
    Member

    Conditionally setting the wakeup fd depending on whether it has been changed in another scope.

    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.

  14. x42005e1f commented on Mar 13, 2026

    @x42005e1f
    ContributorAuthor

    Here is why I opened this issue:

    host_fd = set_wakeup_fd(guest_fd, warn_on_full_buffer=True)
    try:
        guest_tick()
    finally:
        if host_fd == -1 or host_fd == guest_fd:
            guest_fd = get_wakeup_fd()
        else:
            guest_fd = set_wakeup_fd(host_fd, warn_on_full_buffer=False)

    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:

    1. There is no get_wakeup_fd() that would not create a race condition with other OS processes (e.g., child processes, if we want to handle SIGCHLD).
    2. guest_tick() may call set_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 uses set_wakeup_fd() only for wakeups, and it can only be solved using the second strategy:

    host_token = save_wakeup_token(guest_token)
    try:
        guest_tick()
    finally:
        guest_token = restore_wakeup_token(host_token)

    Here, guest_token will be a child of host_token as long as its file descriptor is not -1. And in line with the second strategy:

    1. Any 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.
    2. If the host uses 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 using set_wakeup_fd() and there are no other guests here. But if get_wakeup_fd() is not atomic, then they cannot be considered supported at all.


    Related: #145342

  15. x42005e1f commented on Mar 13, 2026

    @x42005e1f
    ContributorAuthor

    For additional context. The host may call set_wakeup_fd() at any time, for example when registering its own signal handlers, as asyncio.SelectorEventLoop does. 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 uses set_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 of asyncio.ProactorEventLoop, which... I am really starting to dislike (it calls set_wakeup_fd() when creating and closing the event loop, thereby forcing even a simple call to loop_factory() to be isolated; it cancels the read from the wakeup pair when exiting loop.run_forever(), thereby scheduling a new callback that implies timeout=0 after every stop, and this is a particularly painful issue for which I no longer see a clean solution workaround other than directly using loop._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.

  16. x42005e1f commented on Mar 17, 2026

    @x42005e1f
    ContributorAuthor

    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 to warn_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 using warn_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 same host_fd == -1 or host_fd == guest_fd check: host_fd = get_wakeup_fd()).


    Related: #88378 (for the case of multiple guests)

  17. encukou commented on Mar 17, 2026

    @encukou
    Member

    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.

  18. Vizonex commented on Apr 9, 2026

    @Vizonex

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    • Status
      Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions