The guide to signal handling in Rust

mangelosanto

Matt Angelosanto

Posted on April 12, 2023

The guide to signal handling in Rust

Written by Eze Sunday✏️

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. Let's get started, shall we? 🦀

Jump ahead:

Introduction to signals and signal handling in 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

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

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 signal_hook, nix, libc, and 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 tokio crate. The tokio signal crate is a perfect choice for handling signals because it is asynchronous and safe. By the way, it uses libc behind the scenes.

First, create a Rust project with Cargo and install tokio by running the following command:

 init && cargo add tokio
Enter fullscreen mode Exit fullscreen mode

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:

[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"] }
Enter fullscreen mode Exit fullscreen mode

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:

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(())
}
Enter fullscreen mode Exit fullscreen mode

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

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. In our case, we are calling the interrupt() kind:

let mut sigint = signal(SignalKind::interrupt())?;
Enter fullscreen mode Exit fullscreen mode

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

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

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

Exploring signal masking in Rust

Signal masking 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

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:

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");
}
Enter fullscreen mode Exit fullscreen mode

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

LogRocket Dashboard Free Trial Banner

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

💖 💪 🙅 🚩
mangelosanto
Matt Angelosanto

Posted on April 12, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related