The guide to signal handling in Rust
Tags: rust, webdev
Published: 2023-04-12 18:04:04

**Written by [Eze Sunday](https://blog.logrocket.com/author/ezesunday/)✏️**

A signal is a software interrupt sent to a process by the operating system or another process to notify it of an event. For example, when you try pressing **`Control+C`** while your program runs on a terminal, it terminates the process, correct? That's one of the most common signals and signal handling you can see in action. We'll explore how to handle that signal and others in Rust. 

A signal can be triggered by different things such as hardware, the operating system, user input, or other processes. When a process receives a signal, it means that an event has occurred, and the process can take a specific action depending on the type of signal. For example, the process may need to stop running, restart, or handle an error. 

In this article, we'll discover the purpose of signals and how to handle signals in the [Rust programming language](https://blog.logrocket.com/tag/rust/). Let's get started, shall we? 🦀 

_Jump ahead:_

*   [Introduction to signals and signal handling in Rust](#introduction-signals-signal-handling-rust)
    *   [Exploring the types of signals](#exploring-types-signals)
    *   [Understanding signal dispositions](#understanding-signal-dispositions)
*   [Signal handling in Rust](#signal-handling-rust)
    *   [Signal handling with tokio](#signal-handling-with-tokio)
*   [Exploring signal masking in Rust](#exploring-signal-masking-rust)
    *   [Blocking and unblocking signals with nix](#blocking-unblocking-signals-with-nix)

## Introduction to signals and signal handling in Rust <a name="introduction-signals-signal-handling-rust">

It has been established that signals serve as notifications of events. Just like how we react to notifications in our daily lives, when you are notified of an event, you are expected to either take responsibility and address it or choose to ignore it. Similarly, OS signals enable a process to take action or do nothing in response to a triggered event. 

For instance, signals can pause or halt a running process, notify the user of an error such as a floating point exception, or provide information such as a system alarm wake-up call. When such signals are received, an application may need to close open handles to free up system resources or terminate any activity the event could impact. Such is the case of an application quitting when a user presses **`Control+C`**.

### Exploring the types of signals <a name="exploring-types-signals">

There are several types of signals. Some can be handled, while others can't. The table below shows some signal types with their available codes based on [POSIX](https://en.wikipedia.org/wiki/POSIX) standards. This standard is a set of standards that defines APIs for Unix-like operating systems, including Linux, macOS, and various flavors of Unix. Refer to the following table: 

| Signal type | Use | 
| --------------------- | -------------------------- | 
| `SIGHUP`, code: `1` | This signal is sent to a process when its controlling terminal is closed or disconnected | 
| `SIGINT`, code: `2` | This signal is sent to a process when the user presses **`Control+C`** to interrupt its execution | 
| `SIGQUIT`, code: `3` | This signal is similar to `SIGINT` but is used to initiate a core dump of the process, which is useful for [debugging](https://blog.logrocket.com/how-to-debug-rust-vs-code/) |
| `SIGILL`, code: `4` | This signal is sent to a process when it attempts to execute an illegal instruction | 
| `SIGABRT`, code `6` | This signal is sent to a process when it calls the `abort()` function | 
| `SIGFPE`, code: `8` | This signal is sent to a process when it attempts to perform an arithmetic operation that is not allowed, such as division by zero | 
| `SIGKILL`, code: `9` | This signal is used to terminate a process immediately and cannot be caught or ignored | 
| `SIGSEGV`, code: `11` | This signal is sent to a process when it attempts to access memory that is not allocated to it | 
| `SIGTERM` | This signal is sent to a process to request that it terminate gracefully. Code: `15` | 
| `SIGUSR1`, code: `10` | These signals can be used by a process for custom purposes | 
| `SIGUSR2`, code: `12` | Same as `SIGUSR1`, code: `10` | 

Before discussing handling signals in Rust, let's talk about signal dispositions.

### Understanding signal dispositions

Signal disposition refers to the default action that the OS takes when a process receives a particular signal. The three possible signal dispositions are:

*   `Terminate`: The process is terminated immediately without any chance to clean up or save state
*   `Ignore`: The process does nothing in response to the signal
*   `Catch`: The process runs a user-defined signal handler function to handle the signal

This means that not all signals can be handled. Your application can only handle the signals the OS permits it to handle. All of the pre-defined signals we mentioned above can be handled. However, there are other signals, such as `SIGKILL`, `SIGSTOP`, and `SIGCONT`, that cannot be handled. For example, `SIGKILL` is used to forcefully terminate a process and cannot be caught, blocked, or ignored.

## Signal handling in Rust <a name="signal-handling-rust">

Now that we have covered the fundamentals of signals, let's delve into the world of handling signals in Rust! Unlike C, where signal handling is built into the language modules, Rust provides several libraries that enable developers to handle signals with ease. Libraries such as [s](https://docs.rs/signal-hook/latest/signal_hook/)[ignal_hook](https://docs.rs/signal-hook/latest/signal_hook/), [nix](https://crates.io/crates/nix), [libc](https://crates.io/crates/libc), and [tokio](https://crates.io/crates/tokio) handle signals that primarily use C bindings to make it possible to work with signals.

### Signal handling with tokio

Let's take a look at an example to show how we can handle signals in Rust with the [t](https://crates.io/crates/tokio)[okio crate](https://crates.io/crates/tokio). The tokio signal crate is a perfect choice for handling signals because it is [a](https://blog.logrocket.com/asynchronous-i-o-and-async-await-packages-in-rust/)[synchronous](https://blog.logrocket.com/asynchronous-i-o-and-async-await-packages-in-rust/) and [safe](https://blog.logrocket.com/pinning-rust-async-data-types-memory-safety/). By the way, it uses [libc](https://crates.io/crates/libc) behind the scenes. 

First, create a Rust project with [Cargo](https://blog.logrocket.com/demystifying-cargo-in-rust/) and install tokio by running the following command:

```rust
 init && cargo add tokio
```

Once your installation is complete, open the `cargo.toml` file and activate tokio `full` features by updating the `features` flag with the `full` argument like so: `features=["full"]`. \

Your code should look like this:

```rust
[package]
name = "practice"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tokio = { version="1.25.0", features=["full"] }
```

Then, let's write a sample code to handle the `SIGINT` signal — the signal triggered when you press **`Control+C`** against a running process in your terminal. Here's the code:

```rust
use tokio::signal::unix::{signal, SignalKind};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut sigint = signal(SignalKind::interrupt())?;

    match sigint.recv().await {
        Some(()) => println!("Received SIGINT signal"),
        None => eprintln!("Stream terminated before receiving SIGINT signal"),
    }

    for num in 0..10000 {
        println!("{}", num)
    }

    Ok(())
}
```

Now, run `cargo run` on your terminal to test the code. While the code is running, press **`Control+C`**, and you'll see a response like this: ![An Example of Signal Handling in Rust](https://blog.logrocket.com/wp-content/uploads/2023/04/signal-handling-rust-cargo.png) 

In the above code, we initialize the type of signal by calling its `signalKind` method. `SIGINT` is referred to as `interrupt()`, and `SIGTERM` is referred to as `terminate()`. You can find the methods for others in the [documentation](https://github.com/tokio-rs/tokio/blob/master/tokio/src/signal/unix.rs). In our case, we are calling the `interrupt()` kind:

```rust
let mut sigint = signal(SignalKind::interrupt())?;
```

Once the method is called, you'll be ready to listen to that signal and handle it using the `.recv()` method, as shown below:

```rust
match sigint.recv().await {
    Some(()) => println!("Received SIGINT signal"),
    None => eprintln!("Stream terminated before receiving SIGINT signal"),
}
```

That's basically how you handle signals in Rust in just a few lines of code.

## Exploring signal masking in Rust <a name="exploring-signal-masking-rust">

[Signal masking](http://www.cs.um.edu.mt/~jcor1/SystemsProgramming/CourseMaterials/9_AdvancedSignalOperations.pdf) is the process of temporarily blocking the delivery of certain signals to a process or a thread. When masked, a signal is added to a set of blocked signals and will not be delivered to the process or thread until it is unblocked. 

Signal masking is often used to prevent the interruption of critical sections of code that must execute without being interrupted by a signal handler. For example, in a multi-threaded program, a critical section of code may need to execute atomically without being interrupted by a signal handler. In this case, the programmer can temporarily mask the signals that could interrupt the critical section and then unmask them once the critical section has been completed.

### Blocking and unblocking signals with nix <a name="blocking-unblocking-signals-with-nix">

Let's look at an example of how to block and unblock signals using the nix crate. For this example, we'll use the libc crate. So, start by installing it with `cargo add libc` on your terminal. Then, add this to your `src/main.rs file`:

```rust
use libc::{sigaddset, sigemptyset, sigprocmask, SIGINT, SIG_BLOCK, SIG_UNBLOCK};
use std::thread;
use std::time::Duration;
fn main() {
    unsafe {
        // Create an empty signal mask
        let mut mask: libc::sigset_t = std::mem::zeroed();
        sigemptyset(&mut mask);
        // Add the SIGINT signal to the signal mask
        sigaddset(&mut mask, SIGINT);
        // Block the SIGINT signal using the signal mask
        sigprocmask(SIG_BLOCK, &mask as *const libc::sigset_t, std::ptr::null_mut());
    }
    println!("Blocked SIGINT signal for 5 seconds");
    thread::sleep(Duration::from_secs(5));
    unsafe {
        // Unblock the SIGINT signal using the signal mask
        let mut mask: libc::sigset_t = std::mem::zeroed();
        sigemptyset(&mut mask);
        sigaddset(&mut mask, SIGINT);
        sigprocmask(SIG_UNBLOCK, &mask as *const libc::sigset_t, std::ptr::null_mut());
    }
    println!("Unblocked SIGINT signal");
}
```

Notice that we mark the function as `unsafe`. We do this because it involves direct interaction with the OS signal handling mechanisms through the C standard library's `libc` interface. As you can see, we are de-referencing the `sigset_t` raw pointer `*const libc::sigset_t` because that part of the code is unsafe. 

In the above code, we are blocking the delivery of the `SIGINT` signal until after `5` seconds. Within those `5` seconds, if you press **`Control+C`**, nothing will happen. However, the `SIGINT` signal will be triggered after the `5` seconds elapse. 

If you run this code with `cargo run` and press **`Control+C`** and also run it without pressing `Control+C`, you'll get something like this: ![Final Result of Signal Handling in Rust](https://blog.logrocket.com/wp-content/uploads/2023/04/signal-handling-rust-final.png) For the first executed command in the image above, the signal did execute after `5` seconds because we pressed **`Control+C`**. However, for the second one, it did not. As you can see, the code executed to the end.

## Conclusion

Handling signals in Rust is very straightforward. Despite the limited documentation available for the Rust signal crates, I trust that the insights provided here will serve as a useful starting point for implementing signal handling using Rust. 

Happy hacking!

---

## LogRocket: Full visibility into web frontends for Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, [try LogRocket](https://lp.logrocket.com/blg/signup). 

[![LogRocket Dashboard Free Trial Banner](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://lp.logrocket.com/blg/signup)

[LogRocket](https://logrocket.com/signup) is like a DVR for web apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — [start monitoring for free](https://lp.logrocket.com/blg/signup).