proxy wrote:Are you referring to the fact that POSIX doesn't allow a signal to be interrupted by another signal of the same type? If so, can you explain why that is incompatible with a queue? If I understand it correctly, let's say two SIGUSR1 and one SIGUSR2 arrive, so the queue would contain: [SIGUSR1, SIGUSR1, SIGUSR2]. At the next opportunity, the scheduler would interrupt an appropriate thread and make run the signal handler associate with SIGUSR1. If we detect the second SIGUSR1 either before or during dispatching the first SIGUSR1, I think we are allowed to simply ignore the second SIGUSR1 since it is allowed to be "merged" with the first one. So now the scheduler is free to interrupt the currently executing SIGUSR1 with a SIGUSR2, which will eventually return to SIGUSR1 and finally return to normal running code. Do I have that right?
Weird. Now that I come to research it, I cannot find where it says that. Maybe POSIX changed it at some point...
Anyway, what I was alluding to earlier was that you have a queue of [SIGUSR1, SIGUSR2], now someone sends another SIGUSR1. There used to be a sentence in POSIX that meant you should now discard the second SIGUSR1, since it is already pending. Now POSIX says it is implementation-defined (XSI ยง2.4.1). OK.
Next problem is when you have [SIGUSR1, SIGUSR2] in your queue and SIGUSR1 is blocked in all threads of the destination process. SIGUSR2 must now be able to be a front-runner. You cannot just remove the SIGUSR1 from the queue, because it must still be delivered if the block is eventually removed, but you must deliver SIGUSR2 immediately.
Next problem is that POSIX specifies that if multiple signals between SIGRTMIN and SIGRTMAX are queued, they must be delivered in order of signal number ascending. Multiple calls to sigqueue() with
the same signal number are to be delivered in FIFO order, though.
All of this is to say that you will need a signal queue
for each signal number and also
both in the thread and the process. One queue per process is not enough. One set of queues in the thread is not enough, because the semantics of sending a signal to a process are that the signal is held pending until
any thread takes it. The queues for the classic signals can have a max depth of 1, though. Not for the RT signals.
What you were writing above is just implemented with signal masks. By default (i.e. when SA_NODEFER is not specified in the sigaction() call), the signal number itself is part of the signal mask. When a signal handler is invoked, the kernel atomically adds the signals in the mask to the thread's signal mask. When the signal handler returns, restoring the signal mask is part of what sigreturn() does. Indeed, the upper context's signal mask is writable by the signal handler. Musl uses that in its cancel handling. Musl's cancel handler contains roughly:
Code: Select all
__sigaddset(&ctx->uc_sigset, SIGCANCEL);
__syscall(SYS_tkill, self->tid, SIGCANCEL);
This is in the handler for SIGCANCEL, so the signal is masked. This code adds SIGCANCEL to the parent context's mask and sends another one to itself. The signal is blocked in the signal handler so it cannot be taken there, and it is blocked in the parent context as well, so it also cannot be taken there. The idea is that if the parent context happens to also be a signal handler (for another signal) then at some point it will return, and that will restore a signal mask where SIGCANCEL is unblocked, and then the signal will be taken again.
proxy wrote:I'm not sure I see the benefit of an "altstack" as that would make it more complex (I think) to have signal handlers interrupted by more signal handlers, unless the alt stack is one per signal?
The idea behind an altstack is the same as behind the AMD64 IST: Maybe you want to be able to handle a signal like SIGSEGV when the stack pointer is out-of-bounds. The altstack is generally implemented like this: You have one altstack per process. By default it is disabled. When a signal is to be handled by a process, you check that the signal handler was established with SA_ONSTACK, and that there is an altstack that is currently neither disabled nor in use (flags word doesn't contain SS_DISABLE, and stack pointer is not on the altstack. The altstack is given as base and length, so that is easy enough to check). If all of this works out, you use the altstack, else you use the normal delivery mechanism. On x86_64, this means decreasing the stack pointer by the red zone size.
proxy wrote:Right. I did say that I would plan to "push the current state things, similar to an interrupt and then call the signal handler". I think that would be sufficient to resume without worrying about clobbering... am I missing something else?
The thing you missed is that you cannot simply make the signal handler return to where you were in the main program afterward. You have to make it "return" to a shim that invokes a system call that restores everything from the data put on stack.
proxy wrote:Anyway, you seem to have a very strong understanding of this subject so I'm curious about more of your thoughts.
Well, I do read a lot. But honestly, there is not a hell of a lot more to say about signals I haven't said already. One queue per thread/process and signal number. You send a signal to a process by adding it to the right queue. I already went over signal handler invocation, so there are no surprises here. And if the processor faults in user space, you force a signal against the thread (which means that you send the signal, but if it is blocked or ignored, you force it to be unblocked and defaulted. Obviously, you cannot continue the thread normally after a CPU fault hits).