I have a question about signal handling and Kotlin...
# kotlin-native
k
I have a question about signal handling and Kotlin/Native. I've managed to stumble into a working solution but I'm not certain why it works. Here's my scenario: I am working on
io_uring
bindings for Kotlin/Native. In doing so I need a worker thread that does a blocking operation to poll for completion events from the linux kernel. I need a mechanism to cancel this blocking operation, especially because
runInterruptible
doesn't exist in any form on native. To do this I've had to jump through a few hoops. I grab the underlying thread ID via
pthread_self
and then register a coroutine which invokes
pthread_kill(SIGINT)
when that coroutine is cancelled. Here's where things get weird. Originally I set up a signal handler via the libc
signal
API and invoked
pthread_exit
. This crashed the Kotlin/Native runtime. While debugging I commented that out and everything just...worked? The blocking operation from
io_uring
started magically returning
EINTR
meaning it was interrupted successfully despite my registered signal handler intercepting and suppressing the signal. Does that sound plausible? Here's the PR with a more thorough explanation if anyone is interested.
e
that sounds like normal unix behavior
crash is probably from failing to clean up whatever thread state K/N keeps when you directly exited
empty signal handler still causes interruptible system calls to be interrupted
k
Right. I guess the thing I'm fuzzy on is how the io_uring library code knows
SIGINT
was signaled if I've intercepted it and suppressed it
Ah there it is
I didn't know that
e
that's true of Unix overall
k
Today I learned!
I don't normally do things this low down so it's been fun to flex these muscles.
e
retries are up to user code
read/write are similarly interruptible, you can get a short read/write even without EOF
k
Gotcha. Thanks for the confirmation!
e
also I think it's more common to use SIGALRM or better yet the self-pipe trick
modern linux has timerfd too
k
Go on 👀
This is all new info to me. I'd love some tips and resources.
e
open a pipe. add fd[0] to the set of polled fds. when you want to cause polling to return to your control, write a byte to fd[1], which causes a notification on fd[0]
timerfd is kinda like that but the writer isn't yourself, it's a timeout
k
yeah I had looped on a timeout before which felt clumsy and violated the prompt cancellation guarantee
Thankfully this cancellation isn't expected to happen often, only when the whole ring gets torn down
All other cancellation is done without signals and through normal mechanisms
e
the (unenforced but may cause mysterious issues) requirement to only use async-signal-safe functions from within a signal handler makes them a huge pain to deal with in any memory-managed language (GC can randomly trigger)
plus there's nothing preventing other processes from sending signals to your threads
(aside from the normal id checks)
k
It occurred to me that the file descriptor trick is doing the same thing as piping an event though io_uring just by a different mechanism. I've removed the signal interruption and instead opted to pipe a no-op through io_uring to ensure that cancellation is checked. I think I'm finally happy with this. https://github.com/kevincianfarini/kotlin-io-uring/commit/be9609d9d9fec23309d7894bbdd6040de9777eff