New POSIX Signal Handling Features in .NET 6
- 2022-01-30 - Initial publication.
- 2022-02-01 - I originally had a 'limitation' section that listed a few bugs that I can no longer reproduce. I removed it and also add and "Other signals" section.
.NET 6 introduced a new feature which enables limited handling of POSIX Signals, also called Linux/Unix signals.
This article introduces this feature, and is primarily aimed at Windows developers and others that may not be familiar with systems-level Linux/Unix programming and conventions. I hope it'll make developing applications that target Linux, MacOS, and even WSL slightly less scary.
All examples in F#.
(Skip this section if you already know what POSIX signals are.)
POSIX Signals, which is the more formal and standardized name for Linux/Unix signals, are low-level messages that processes can send to one another. It is one of the oldest interprocess communication mechanisms in use today. They are used extensively for coordinating communication between related processes, and for controlling the process life cycle.
Some basic things you might want to know:
- Signals contain no "content". You can't pack a JSON message or similar into one.
- Processes can define custom handlers for most signals.
- All signals have "default behaviors" associated with them. A process receiving SIGINT, without a custom handler, will by default terminate.
- Processes can completely ignore most signals.
- Some signals such as SIGKILL and SIGSTOP cannot be ignored or handled in custom ways.
- Processes only have permission to send signals to certain processes (such as processes ran by the same user).
The New Classes
The primary new class of interest is PosixSignalRegistration in
This class has a
Create method which accepts a signal identifier and a handler as parameters:
open System.Runtime.InteropServices PosixSignalRegistration.Create(PosixSignal.SIGINT, fun context -> context.Cancel <- true) |> ignore
PosixSignal is an enum containing available signal identifiers. Currently, the signals included (and thus presumably supported by .NET) are:
This is only a small subset of POSIX signals. Hopefully, more will be added in future versions. Of these, the most interesting are the four "termination" signals SIGHUP, SIGINT, SIGTERM, and SIGQUIT. They are called termination signals because the default behavior is to terminate the process. The others are likely not interesting unless you have very specific needs. We will cover all of these below.
The second parameter to
PosixSignalRegistration.Create, the signal handler, accepts a PosixSignalContext.
The context object contains a single property of interest called
If set to true, it cancels the default behavior of the signal.
If set to false (the default), the default behavior occurs after the handler.
SIGINT signal is meant to 'interrupt' a running application.
You can send a console applications running in the foreground (i.e. an application you started in a shell, without a
& at the end) a SIGINT signal by pressing Ctrl-C.
The default behavior of SIGINT is to terminate a process.
(Of course, you can also explicitly send a SIGINT (or any other signal) to a process using the Linux
kill -INT 12345)
Below, we see a SIGINT handler in action:
open System.Threading open System.Runtime.InteropServices let mutable intPreviouslyReceived = false PosixSignalRegistration.Create(PosixSignal.SIGINT, fun context -> if intPreviouslyReceived then printfn "Ok, you asked for it!" // should we cancel the default signal handling behavior? context.Cancel <- false // (this is the default) else printfn "Are you sure you want to quit? Press Ctrl-C again if you're sure" intPreviouslyReceived <- true // actually quit this time context.Cancel <- true) |> ignore // just some busy work so the app doesn't naturally close immediately for i in 1 .. 30 do printfn "%d" i Thread.Sleep 1000 |> ignore
The first time a SIGINT is received by the above script, we prevent the default behavior by setting
context.Cancel to true and set
intPreviouslyReceived to true. The second time the script receives SIGINT, it terminates itself for real.
SIGHUP is the "hang up" signal. Like SIGINT, its default behavior is to terminate the receiving process. SIGHUP has two "interpretations".
For console application, SIGHUP is sent to an application when its controlling terminal closes. This can happen in a few ways, including:
- You close the window of the terminal running your console application;
- You disconnect from an SSH session with a process running; or
- Historically, when your 300-baud dial-up connection to the mainframe at your University was dropped. This is where the term "hang up" comes from.
The snippet below would prevent your application from dying under the above circumstances:
PosixSignalRegistration.Create(PosixSignal.SIGHUP, fun context -> context.Cancel <- true) |> ignore
Note that the above can also be achieved by running any Unix command with the 'nohup'.
You can test out the above handler by monitoring it's parent process:
- Run the SIGHUP-handling program normally.
- Open a process monitor with a tree-view, like
htopor Ubuntu's System Monitor. Note that the process's parent will be the shell you ran the command in.
- Close the terminal.
- Find the process in the monitor -- you'll see it is still running, but it will be the child of the init process (the topmost process in Unix with process ID 1). (Processes whose parent died gets 'adopted' by the init process).
For server/daemon applications which do not usually have a controlling terminal, SIGHUP is used in a completely different way: A SIGHUP received by a process is generally interpreted as a signal to reload its configuration files.
For example, after you change your nginx.conf or other nginx configuration files, running
nginx reload will send nginx a SIGHUP.
nginx will then re-read the configuration files and change its runtime parameters to reflect the changes.
Because it does not stop and restart the entire daemon, it maintains and serves existing client connections during the reload.
SIGTERM and SIGQUIT
SIGTERM and SIGQUIT are the signals meant for ending a process. Their default behavior is the same: end the process. Actually, SIGQUIT also generates a core dump file, by default. Handling these two signals can be accomplished in exactly the same way as SIGHUP and SIGINT above.
SIGTERM is meant to be the "polite" way of telling a process to terminate. A process receiving SIGTERM should close all temporary files, free all resources, write to logs appropriately, etc.
SIGQUIT is meant to be, by convention, a more "urgent" way of telling a process to terminate. A process receiving SIGQUIT may skip cleaning up temporary files or other non-critical tasks.
Your handlers for these signals should strive to meet these convention and clean up accordingly.
SIGKILL is the "forceful" way of ending a process. It stops the process dead in its tracks without any cleanup of any kind.
There is no SIGKILL in the
PosixSignal enum because SIGKILL cannot be caught or ignored by a process.
It is meant to be a last-resort to terminate a process, when a process refuses to quit upon receiving a SIGQUIT and/or SIGTERM.
As a side note: this means that if you have some process you want to end on your machine, you should not start with a
kill -KILL $pid.
In fact, the default signal for the
kill command (when you don't specify a signal) is SIGTERM.
Start with SIGTERM, then SIGQUIT, then SIGKILL if necessary.
SIGTSTP and SIGCONT
SIGTSTP and SIGCONT are the signals sent by the Ctrl-Z and
(Background for non-Unix people: you can suspend most running processes in Unix by pressing Ctrl-Z and then resume it later by 'foregrounding' it with
It isn't too popular these days since modern systems can run many terminal windows.
However, I still do this often if I'm in an SSH session and I don't have tmux or screen available (or configured to my liking).)
Generally, you wouldn't be messing with these signals since the default behavior is sufficient and is expected from users.
Also, SIGTSTP and SIGCONT handling seem broken as of now. See the github issue I submitted.
SIGTTIN and SIGTTOU
SIGTTIN and SIGTTOU are sent when a background process tries to read or write from a parent's terminal session (the 'controlling terminal'). Like SIGTSTP and SIGCONT, these are unlikely to be useful unless you are doing something very Unix-y, such as implementing a Unix shell like Bash.
SIGCHLD is a signal received by a parent process when a child process (any of them, if there are multiple children) terminates.
However, I don't think this will be used often from .NET since .NET programmers would and should prefer
System.Diagnostics.Process.Start() to launch child processes.
(Linux programmers using C or other Unix-native language would often use SIGCHLD handlers in conjunction with other Unix classics like
pipe and the SIGPIPE to launch and manage child processes.
fork directly from .NET (technically possible through P/Invoke) is ill-advised, unless you're one of the few people in the world that can understand how .NET multithreading and forking interact, on Linux muchless.)
SIGWINCH is received when the window is resized... like, when you click on the corner of a window and drag it to resize it.
This signal might be interesting to those writing terminal-UI's with curses (think: the
top command), or animated terminal applications like progress bars.
Signals that are not listed in
PosixSignal can be caught by providing
PosixSignalRegistration.Create a raw signal number (cast to the
PosixSignal enum type).
You can find the raw signal numbers by looking at
man 7 signal on Linux, or
man 3 signal on MacOS.
For example, on my Linux machine, we see:
Signal x86/ARM Alpha/ MIPS PARISC Notes most others SPARC ---------------------------------------------------------- SIGHUP 1 1 1 1 SIGINT 2 2 2 2 SIGQUIT 3 3 3 3 SIGILL 4 4 4 4 SIGTRAP 5 5 5 5 SIGABRT 6 6 6 6 SIGIOT 6 6 6 6 SIGBUS 7 10 10 10 SIGEMT - 7 7 - SIGFPE 8 8 8 8 SIGKILL 9 9 9 9 SIGUSR1 10 30 16 16 SIGSEGV 11 11 11 11 SIGUSR2 12 31 17 17 SIGPIPE 13 13 13 13 ....
Note that they are architecture AND OS dependent, so please code accordingly!
We see above that the user-definable signal SIGUSR1 is 10 on my system. To catch the SIGUSR1 signal:
PosixSignalRegistration.Create( enum<PosixSignal>(10), fun context -> printfn "usr1 received" context.Cancel <- true) |> ignore
Before going crazy with handling unlisted signal, though, please be aware that POSIX signal handling has a lot of nuances, some of which may not make sense in a .NET application. Users are encouraged to read more about signal handling in a traditional Unix programming book such as Steven's Advanced Programming in the Unix Environment.