12 Signals, interrupts, and exceptions

This chapter covers

This chapter describes the process by which the outside world communicates with your operating system (OS). The network constantly interrupts program execution when bytes are ready to be delivered. This means that after connecting to a database (or at any other time), the OS can demand that your application deal with a message. This chapter describes this process and how to prepare your programs for it.

In chapter 9, you learned that a digital clock periodically notifies the OS that time has progressed. This chapter explains how those notifications occur. It also introduces the concept of multiple applications running at the same time via the concept of signals. Signals emerged as part of the UNIX OS tradition. These can be used to send messages between different running programs.

We’ll address both concepts—signals and interrupts—together, as the programming models are similar. But it’s simpler to start with signals. Although this chapter focuses on the Linux OS running on x86 CPUs, that’s not to say that users of other operating systems won’t be able to follow along.

12.1 Glossary

Learning how CPUs, device drivers, applications, and operating systems interact is difficult. There is a lot of jargon to take in. To make matters worse, the terms all look similar, and it certainly does not help that these are often used interchangeably. Here are some examples of the jargon that is used in this chapter. Figure 12.1 illustrates how these interrelate:

Figure 12.1 A visual taxonomy of how the terms interrupt, exception, trap, and fault interact within Intel’s x86 family of CPUs. Note that signals do not appear within this figure. Signals are not interrupts.

Note The meaning of the term exception may differ from your previous programming experience. Programming languages often use the term exception to refer to any error, whereas the term has a specialized meaning when referring to CPUs.

12.1.1 Signals vs. interrupts

The two concepts that are most important to distinguish between are signals and interrupts. A signal is a software-level abstraction that is associated with an OS. An interrupt is a CPU-related abstraction that is closely associated with the system’s hardware.

Signals are a form of limited interprocess communication. They don’t contain content, but their presence indicates something. They’re analogous to a physical, audible buzzer. The buzzer doesn’t provide content, but the person who presses it still knows what’s intended as it makes a very jarring sound. To add confusion to the mix, signals are often described as software interrupts. This chapter, however, avoids the use of the term interrupt when referring to a signal.

There are two forms of interrupts, which differ in their origin. One form of interrupt occurs within the CPU during its processing. This is the result of attempting to process illegal instructions and trying to access invalid memory addresses. This first form is known technically as a synchronous interrupt, but you may have heard it referred to by its more common name, exception.

The second form of interrupt is generated by hardware devices like keyboards and accelerometers. This is what’s commonly implied by the term interrupt. This can occur at any time and is formally known as an asynchronous interrupt. Like signals, this can also be generated within software.

Interrupts can be specialized. A trap is an error detected by the CPU, so it gives the OS a chance to recover. A fault is another form of a recoverable problem. If the CPU is given a memory address that it can’t read from, it notifies the OS and asks for an updated address.

Interrupts force an application’s control flow to change, whereas many signals can be ignored if desired. Upon receiving an interrupt, the CPU jumps to handler code, irrespective of the current state of the program. The location of the handler code is predefined by the BIOS and OS during a system’s bootup process.

Treating signals as interrupts

Handling interrupts directly means manipulating the OS kernel. Because we would prefer not to do that in a learning environment, we’ll play fast and loose with the terminology. The rest of this chapter, therefore, treats signals as interrupts.

Why simplify things? Writing OS components involves tweaking the kernel. Breaking things there means that our system could become completely unresponsive without a clear way to fix anything. From a more pragmatic perspective, avoiding tweaks to the kernel means that we’ll avoid learning a whole new compiler toolchain.

To our advantage, code that handles signals looks similar to code that handles interrupts. Practicing with signals allows us to keep any errors within our code constrained to our application rather than risk bringing the whole system down. The general pattern is as follows:

  1. Model your application’s standard control flow.

  2. Model the interrupted control flow and identify resources that need to be cleanly shut down, if required.

  3. Write the interrupt/signal handler to update some state and return quickly.

  4. You will typically delegate time-consuming operations by only modifying a global variable that is regularly checked by the main loop of the program.

  5. Modify your application’s standard control flow to look for the GO/NO GO flag that a signal handler may have changed.

12.2 How interrupts affect applications

Let’s work through this challenge by considering a small code example. The following listing shows a simple calculation that sums two integers.

Listing 12.1 A program that calculates the sum of two integers

1 fn add(a: i32, b:i32) -> i32 {
2   a + b
3 }
4  
5 fn main() {
6   let a = 5;
7   let b = 6;
8   let c = add(a,b);
9 }

Irrespective of the number of hardware interrupts, c is always calculated. But the program’s wall clock time becomes nondeterministic because the CPU performs different tasks every time it runs.

When an interrupt occurs, the CPU immediately halts execution of the program and jumps to the interrupt handler. The next listing (illustrated in figure 12.2) details what happens when an interrupt occurs between lines 7 and 8 in listing 12.1.

Listing 12.2 Depicting the flow of listing 12.1 as it handles an interrupt

 1 #[allow(unused)]
 2 fn interrupt_handler() {      
 3   / / ..
 4 }
 5  
 6 fn add(a: i32, b:i32) -> i32 {
 7   a + b
 8 }
 9  
10 fn main() {
11   let a = 5;
12   let b = 6;
13  
14   / / Key pressed on keyboard!
15   interrupt_handler()
16  
17   let c = add(a,b);
18 }

Although presented in this listing as an extra function, the interrupt handler is typically defined by the OS.

 

Figure 12.2 Using addition to demonstrate control flow for handling signals

One important point to remember is that, from the program’s perspective, little changes. It isn’t aware that its control flow has been interrupted. Listing 12.1 is still an accurate representation of the program.

12.3 Software interrupts

Software interrupts are generated by programs sending specific instructions to the CPU. To do this in Rust, you can invoke the asm! macro. The following code, available at ch12/asm.rs, provides a brief view of the syntax:

#![feature(asm)]        
 
use std::asm;
 
fn main() {
    unsafe {
        asm!("int 42");
    }
}

Enables an unstable feature

Running the compiled executable presents the following error from the OS:

$ rustc +nightly asm.rs 
$ ./asm 
Segmentation fault (core dumped)

As of Rust 1.50, the asm! macro is unstable and requires that you execute the nightly Rust compiler. To install the nightly compiler, use rustup:

$ rustup install nightly

12.4 Hardware interrupts

Hardware interrupts have a special flow. Devices interface with a specialized chip, known as the Programmable Interrupt Controller (PIC), to notify the CPU. Figure 12.3 provides a view of how interrupts flow from hardware devices to an application.

Figure 12.3 How applications are notified of an interrupt generated from a hardware device. Once the OS has been notified that data is ready, it then directly communicates with the device (in this case, the keyboard) to read the data into its own memory.

12.5 Signal handling

Signals require immediate attention. Failing to handling a signal typically results in the application being terminated.

12.5.1 Default behavior

Sometimes the best approach is to let the system’s defaults do the work. Code that you don’t need to write is code that’s free from bugs that you inadvertently cause.

The default behavior for most signals is shutting down the application. When an application does not provide a special handler function (we’ll learn how to do that in this chapter), the OS considers the signal to be an abnormal condition. When an OS detects an abnormal condition within an application, things don’t end well for the application—it terminates the application. Figure 12.4 depicts this scenario.

Figure 12.4 An application defending itself from marauding hoards of unwanted signals. Signal handlers are the friendly giants of the computing world. They generally stay out of the way but are there when your application needs to defend its castle. Although not part of everyday control flow, signal handlers are extremely useful when the time is right. Not all signals can be handled. SIGKILL is particularly vicious.

Your application can receive three common signals. The following lists them and their intended actions:

You’ll find many other less common signals. For your convenience, a fuller list is provided in table 12.2.

You may have noticed that the three examples listed here are heavily associated with terminating a running program. But that’s not necessarily the case.

12.5.2 Suspend and resume a program’s operation

There are two special signals worth mentioning: SIGSTOP and SIGCONT. SIGSTOP halts the program’s execution, and it remains suspended until it receives SIGCONT. UNIX systems use this signal for job control. It’s also useful to know about if you want to manually intervene and halt a running application but would like the ability to recover at some time in the future.

The following snippet shows the structure for the sixty project that we’ll develop in this chapter. To download the project, enter these commands in the console:

$ git clone https:/ /github.com/rust-in-action/code rust-in-action
$ cd rust-in-action/ch12/ch12-sixty

To create the project manually, set up a directory structure that resembles the following and populate its contents from listings 12.3 and 12.4:

ch12-sixty
├── src
│   └── main.rs      
└── Cargo.toml       

See listing 12.4.

See listing 12.3.

The following listing shows the initial crate metadata for the sixty project. The source code for this listing is in the ch12/ch12-sixty/ directory.

Listing 12.3 Crate metadata for the sixty project

[package]
name = "sixty"
version = "0.1.0"
authors = ["Tim McNamara <author@rustinaction.com>"]
 
[dependencies]

The next listing provides the code to build a basic application that lives for 60 seconds and prints its progress along the way. You’ll find the source for this listing in ch12/ch12-sixty/src/main.rs.

Listing 12.4 A basic application that receives SIGSTOP and SIGCONT

 1 use std::time;
 2 use std::process;
 3 use std::thread::{sleep};
 4  
 5 fn main() {
 6     let delay = time::Duration::from_secs(1);
 7  
 8     let pid = process::id();
 9     println!("{}", pid);
10  
11     for i in 1..=60 {
12         sleep(delay);
13         println!(". {}", i);
14     }
15 }

Once the code from listing 12.4 is saved to disk, two consoles open. In the first, execute cargo run. A 3–5 digit number appears, followed by a counter that increments by the second. The first line number is the PID or process ID. Table 12.1 shows the operation and expected output.

Table 12.1 How processes can be suspended and resumed with SIGSTOP and SIGCONT

Step

Console 1

Console 2

 

Executes application

Sends signals

1

$ cd ch12/ch12-sixty

 

2

$ cargo run

23221

. 1

. 2

. 3

. 4

 

3

 

$ kill -SIGSTOP 23221

4

[1]+ Stopped cargo run

$

 

5

 

$ kill -SIGCONT 23221

6

. 5

. 6

. 7

. 8

 

 

. 60

 

The program flow in table 12.1 follows:

  1. In console 1, move to the project directory (created from listings 12.3 and 12.4).

  2. Compile and run the project.

    cargo provides debugging output that is omitted here. When running, the sixty program prints the PID, and then prints some numbers to the console every second. Because it was the PID for this invocation, 23221 appears as output in the table.

  3. In console 2, execute the kill command, specifying -SIGSTOP.

    If you are unfamiliar with the shell command kill, its role is to send signals. It’s named after its most common role, terminating programs with either SIGKILL or SIGTERM. The numeric argument (23221) must match the PID provided in step 2.

  4. Console 1 returns to the command prompt as there is no longer anything running in the foreground.

  5. Resume the program by sending SIGCONT to the PID provided in step 2.

  6. The program resumes counting. It terminates when it hits 60, unless interrupted by Ctrl-C (SIGINT).

SIGSTOP and SIGCONT are interesting special cases. Let’s continue by investigating more typical signal behavior.

12.5.3 Listing all signals supported by the OS

What are the other signals and what are their default handlers? To find the answer, we can ask the kill command to provide that information:

$ kill -l        
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGEMT       8) SIGFPE       9) SIGKILL     10) SIGBUS
11) SIGSEGV     12) SIGSYS      13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGURG      17) SIGSTOP     18) SIGTSTP     19) SIGCONT     20) SIGCHLD
21) SIGTTIN     22) SIGTTOU     23) SIGIO       24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGPWR      30) SIGUSR1
31) SIGUSR2     32) SIGRTMAX

-l stands for list.

That’s a lot, Linux! To make matters worse, few signals have standardized behavior. Thankfully, most applications don’t need to worry about setting handlers for many of these signals (if any). Table 12.1 shows a much tighter list of signals. These are more likely to be encountered in day-to-day programming.

Table 12.2 List of common signals, their default actions, and shortcuts for sending them from the command line

Signal

Read as

Default action

Comment

Shortcut

SIGHUP

Hung up

Terminate

Originally from telephone-based digital communications. Now often sent to background applications (daemons/services) to request that these reread their configuration files. Sent to running programs when you log out from a shell.

Ctrl-D

SIGINT

Interrupt (or perhaps interactive)

Terminate

User-generated signal to terminate a running application.

Ctrl-C

SIGTERM

Terminate

Terminate

Asks application to gracefully terminate.

 

SIGKILL

Kill

Terminate

This action is unstoppable.

 

SIGQUIT

Quit

 

Writes memory to disk as a core dump, then terminates.

Ctrl-\

SIGTSTP

Terminal stop

Pause execution

The terminal requests the application to stop.

Ctrl-Z

SIGSTOP

Stop

Pause execution

This action is unstoppable.

 

SIGCONT

Continue

Resume execution when paused

 

 

Note SIGKILL and SIGSTOP have special status: these cannot be handled or blocked by the application. Programs can avoid the others.

12.6 Handling signals with custom actions

The default actions for signals are fairly limited. By default, receiving a signal tends to end badly for applications. For example, if external resources such as database connections are left open, they might not be cleaned up properly when the application ends.

The most common use case for signal handlers is to allow an application to shut down cleanly. Some common tasks that might be necessary when an application shuts down include

To stop the current workload and shut down, a signal handler is required. To set up a signal handler, we need to create a function with the signature f(i32) -> (). That is, the function needs to accept an i32 integer as its sole argument and returns no value.

This poses some software engineering issues. The signal handler isn’t able to access any information from the application except which signal was sent. Therefore, because it doesn’t know what state anything is in, it doesn’t know what needs shutting down beforehand.

There are some additional restrictions in addition to the architectural one. Signal handlers are constrained in time and scope. These must also act quickly within a subset of functionality available to general code for these reasons:

Signal handlers have reduced scope in what they’re permitted to do. For example, they must avoid executing any code that might itself generate signals.

To wriggle out of this constrained environment, the ordinary approach is to use a Boolean flag as a global variable that is regularly checked during a program’s execution. If the flag is set, then you can call a function to shutdown the application cleanly within the context of the application. For this pattern to work, there are two requirements:

To avoid race conditions caused by multiple signal handlers running at the same time, signal handlers typically do little. A common pattern is to set a flag via a global variable.

12.6.1 Global variables in Rust

Rust facilitates global variables (variables accessible anywhere within the program) by declaring a variable with the static keyword in global scope. Suppose we want to create a global value SHUT_DOWN that we can set to true when a signal handler believes it’s time to urgently shut down. We can use this declaration:

static mut SHUT_DOWN: bool = false;

Note static mut is read as mutable static, irrespective of how grammatically contorted that is.

Global variables present an issue for Rust programmers. Accessing these (even just for reading) is unsafe. This means that the code can become quite cluttered if it’s wrapped in unsafe blocks. This ugliness is a signal to wary programmers—avoid global state whenever possible.

Listing 12.6 presents a example of a static mut variable that reads from line 12 and writes to lines 7–9. The call to rand::random() on line 8 produces Boolean values. Output is a series of dots. About 50% of the time, you’ll receive output that looks like what’s shown in the following console session:1

$ git clone https:/ /github.com/rust-in-action/code rust-in-action
$ cd rust-in-action/ch12/ch2-toy-global
$ cargo run -q 
.

The following listing provides the metadata for listing 12.6. You can access its source code in ch12/ch12-toy-global/Cargo.toml.

Listing 12.5 Crate metadata for listing 12.6

[package]
name = "ch12-toy-global"
version = "0.1.0"
authors = ["Tim McNamara <author@rustinaction.com>"]
edition = "2018"
 
[dependencies]
rand = "0.6"

The following listing presents our toy example. Its source code is in ch12/ch12-toy-global/src/main.rs.

Listing 12.6 Accessing global variables (mutable statics) in Rust

 1 use rand;
 2  
 3 static mut SHUT_DOWN: bool = false;
 4  
 5 fn main() {
 6   loop {
 7     unsafe {                          
 8       SHUT_DOWN = rand::random();     
 9     }
10     print!(".");
11  
12     if unsafe { SHUT_DOWN } {
13       break
14     };
15   }
16   println!()
17 }

Reading from and writing to a static mut variable requires an unsafe block.

rand::random() is a shortcut that calls rand::thread_rng().gen() to produce a random value. The required type is inferred from the type of SHUT_DOWN.

12.6.2 Using a global variable to indicate that shutdown has been initiated

Given that signal handlers must be quick and simple, we’ll do the minimal amount of possible work. In the next example, we’ll set a variable to indicate that the program needs to shut down. This technique is demonstrated by listing 12.8, which is structured into these three functions:

When run, the resulting executable produces a trace of where it is. The following console session shows the trace:

$ git clone https:/ /github.com/rust-in-action/code rust-in-action
$ cd rust-in-action/ch12/ch12-basic-handler
$ cargo run -q 
1
SIGUSR1
2
SIGUSR1
3
SIGTERM
4
*        

I hope that you will forgive the cheap ASCII art explosion.

Note If the signal handler is not correctly registered, Terminated may appear in the output. Make sure that you add a call to register_signal_handler() early within main(). Listing 12.8 does this on line 38.

The following listing shows the package and dependency for listing 12.8. You can view the source for this listing in ch12/ch12-basic-handler/Cargo.toml.

Listing 12.7 Crate setup for listing 12.10

[package]
name = "ch12-handler"
version = "0.1.0"
authors = ["Tim McNamara <author@rustinaction.com>"]
edition = "2018"
 
[dependencies]
libc = "0.2"

When executed, the following listing uses a signal handler to modify a global variable. The source for this listing is in ch12/ch12-basic-handler/src/main.rs.

Listing 12.8 Creating a signal handler that modifies a global variable

 1 #![cfg(not(windows))]               
 2  
 3 use std::time::{Duration};
 4 use std::thread::{sleep};
 5 use libc::{SIGTERM, SIGUSR1};
 6  
 7 static mut SHUT_DOWN: bool = false;
 8  
 9 fn main() {
10   register_signal_handlers();       
11  
12   let delay = Duration::from_secs(1);
13  
14   for i in 1_usize.. {
15     println!("{}", i);
16     unsafe {                        
17       if SHUT_DOWN {
18         println!("*");
19         return;
20       }
21     }
22  
23     sleep(delay);
24  
25     let signal = if i > 2 {
26       SIGTERM
27     } else {
28       SIGUSR1
29     };
30  
31     unsafe {                        
32       libc::raise(signal);
33     }
34   }
35   unreachable!();
36 }
37  
38 fn register_signal_handlers() {
39   unsafe {                          
40     libc::signal(SIGTERM, handle_sigterm as usize);
41     libc::signal(SIGUSR1, handle_sigusr1 as usize);
42   }
43 }
44  
45 #[allow(dead_code)]                 
46 fn handle_sigterm(_signal: i32) {
47   register_signal_handlers();       
48  
49   println!("SIGTERM");
50  
51   unsafe {                          
52     SHUT_DOWN = true;
53   }
54 }
55  
56 #[allow(dead_code)]                 
57 fn handle_sigusr1(_signal: i32) {
58   register_signal_handlers();       
59  
60   println!("SIGUSR1");
61 }

Indicates that this code won’t run on Windows

Must occur as soon as possible; otherwise signals will be incorrectly handled

Accessing a mutable static is unsafe.

Calling libc functions is unsafe; their effects are outside of Rust’s control.

Without this attribute, rustc warns that these functions are never called.

Reregisters signals as soon as possible to minimize signal changes affecting the signal handler itself

Modifying a mutable static is unsafe.

In the preceding listing, there is something special about the calls to libc::signal() on lines 40 and 41. libc::signal takes a signal name (which is actually an integer) and an untyped function pointer (known in C parlance as a void function pointer) as arguments and associates the signal with the function. Rust’s fn keyword creates function pointers. handle_sigterm() and handle_sigusr1() both have the type fn(i32) -> (). We need to cast these as usize values to erase any type information. Function pointers are explained in more detail in section 12.7.1.

Understanding the difference between const and static

Static and constant seem similar. Here is the main difference between them:

Duplicating const values can be a CPU-friendly optimization. It allows for data locality and improved cache performance.

Why use confusingly similar names for two different things? It could be considered a historical accident. The word static refers to the segment of the address space that the variables live in. static values live outside the stack space, within the region where string literals are held, near the bottom of the address space. That means accessing a static variable almost certainly implies dereferencing a pointer.

The constant in const values refers to the value itself. When accessed from code, the data might get duplicated to every location that it’s needed if the compiler believes that this will result in faster access.

12.7 Sending application-defined signals

Signals can be used as a limited form of messaging. Within your business rules, you can create definitions for SIGUSR1 and SIGUSR2. These are unallocated by design. In listing 12.8, we used SIGUSR1 to do a small task. It simply prints the string SIGUSR1. A more realistic use of custom signals is to notify a peer application that some data is ready for further processing.

12.7.1 Understanding function pointers and their syntax

Listing 12.8 includes some syntax that might be confusing. For example, on line 40 handle_sigterm as usize appears to cast a function as an integer.

What is happening here? The address where the function is stored is being converted to an integer. In Rust, the fn keyword creates a function pointer.

Readers who have worked through chapter 5 will understand that functions are just data. That is to say, functions are sequences of bytes that make sense to the CPU. A function pointer is a pointer to the start of that sequence. Refer back to chapter 5, especially section 5.7, for a refresher.

A pointer is a data type that acts as a stand-in for its referent. Within an application’s source code, pointers contain both the address of the value referred to as well as its type. The type information is something that’s stripped away in the compiled binary. The internal representation for pointers is an integer of usize. That makes pointers very economical to pass around. In C, making use of function pointers can feel like arcane magic. In Rust, they hide in plain sight.

Every fn declaration is actually declaring a function pointer. That means that listing 12.9 is legal code and should print something similar to the following line:

$ rustc ch12/fn-ptr-demo-1.rs && ./fn-ptr-demo-1 
noop as usize: 0x5620bb4af530

Note In the output, 0x5620bb4af530 is the memory address (in hexadecimal notation) of the start of the noop() function. This number will be different on your machine.

The following listing, available at ch12/noop.rs, shows how to cast a function to usize. This demonstrates how usize can be used as a function pointer.

Listing 12.9 Casting a function to usize

fn noop() {}
 
fn main() {
    let fn_ptr = noop as usize;
 
    println!("noop as usize: 0x{:x}", fn_ptr);
}

But what is the type of the function pointer created from fn noop()? To describe function pointers, Rust reuses its function signature syntax. In the case of fn noop(), the type is *const fn() -> (). This type is read as “a const pointer to a function that takes no arguments and returns unit.” A const pointer is immutable. A unit is Rust’s stand-in value for “nothingness.”

Listing 12.10 casts a function pointer to usize and then back again. Its output, shown in the following snippet, should show two lines that are nearly identical:

$ rustc ch12/fn-ptr-demo-2.rs && ./fn-ptr-demo-2 
noop as usize:    0x55ab3fdb05c0
noop as *const T: 0x55ab3fdb05c0

Note These two numbers will be different on your machine, but the two numbers will match each other.

 

Listing 12.10 Casting a function to usize

fn noop() {}
 
fn main() {
  let fn_ptr = noop as usize;
  let typed_fn_ptr = noop as *const fn() -> ();
 
  println!("noop as usize:    0x{:x}", fn_ptr);
  println!("noop as *const T: {:p}", typed_fn_ptr);      
}

Note the use of the pointer format modifier, {:p}.

12.8 Ignoring signals

As noted in table 12.2, most signals terminate the running program by default. This can be somewhat disheartening for the running program attempting to get its work done. (Sometimes the application knows best!) For those cases, many signals can be ignored.

SIGSTOP and SIGKILL aside, the constant SIG_IGN can be provided to libc:: signal() instead of a function pointer. An example of its usage is provided by the ignore project. Listing 12.11 shows its Cargo.toml file, and listing 12.12 shows src/main.rs. These are both available from the ch12/ch12-ignore project directory. When executed, the project prints the following line to the console:

$ cd ch12/ch12-ignore
$ cargo run -q 
ok

The ignore project demonstrates how to ignore selected signals. On line 6 of listing 12.12, libc::SIG_IGN (short for signal ignore) is provided as the signal handler to libc::signal(). The default behavior is reset on line 13. libc::signal() is called again, this time with SIG_DFL (short for signal default) as the signal handler.

Listing 12.11 Project metadata for ignore project

[package]
name = "ignore"
version = "0.1.0"
authors = ["Tim McNamara <author@rustinaction.com>"]
edition = "2018"
 
[dependencies]
libc = "0.2"

Listing 12.12 Ignoring signals with libc::SIG_IGN

 1 use libc::{signal,raise};
 2 use libc::{SIG_DFL, SIG_IGN, SIGTERM};
 3  
 4 fn main() {
 5   unsafe {                        
 6     signal(SIGTERM, SIG_IGN);     
 7     raise(SIGTERM);               
 8   }
 9  
10   println!("ok");
11  
12   unsafe {
13     signal(SIGTERM, SIG_DFL);     
14     raise(SIGTERM);               
15   }
16  
17   println!("not ok");             
18 }

Requires an unsafe block because Rust does not control what happens beyond the function boundaries

Ignores the SIGTERM signal

libc::raise() allows code to make a signal; in this case, to itself.

Resets SIGTERM to its default

Terminates the program

This code is never reached, and therefore, this string is never printed.

12.9 Shutting down from deeply nested call stacks

What if our program is deep in the middle of a call stack and can’t afford to unwind? When receiving a signal, the program might want to execute some cleanup code before terminating (or being forcefully terminated). This is sometimes referred to as nonlocal control transfer. UNIX-based operating systems provide some tools to enable you to make use of that machinery via two system calls—setjmp and longjmp:

Why bother with such programming gymnastics? Sometimes using low-level techniques like these is the only way out of a tight spot. These approach the “Dark Arts” of systems programming. To quote the manpage:

“setjmp() and longjmp() are useful for dealing with errors and interrupts encountered in a low-level subroutine of a program.”

—Linux Documentation Project: setjmp(3)

These two tools circumvent normal control flow and allow programs to teleport themselves through the code. Occasionally an error occurs deep within a call stack. If our program takes too long to respond to the error, the OS may simply abort the program, and the program’s data may be left in an inconsistent state. To avoid this, you can use longjmp to shift control directly to the error-handling code.

To understand the significance of this, consider what happens in an ordinary program’s call stack during several calls to a recursive function as produced by the code in listing 12.13. Each call to dive() adds another place that control eventually returns to. See the left-hand side of table 12.3. The longjmp system call, used by listing 12.17, bypasses several layers of the call stack. Its effect on the call stack is visible on the right-hand side of table 12.3.

Table 12.3 Comparing the intended output from listing 12.13 and listing 12.17

Listing 12.13 produces a symmetrical pattern. Each level is caused by a nested call to dive(), which is removed when the calls return.

Listing 12.17 produces a much different pattern. After a few calls to dive(), control teleports back to main() without returning the calls to dive().

#
##
###
####
#####
###
##
#
#
##
###
early return!
finishing!

On the left side of table 12.3, the call stack grows one step as functions are called, then shrinks by one as each function returns. On the right side, the code jumps directly from the third call to the top to the call stack.

The following listing depicts how the call stack operates by printing its progress as the program executes. The code for this listing is in ch10/ch10-callstack/src/main.rs.

Listing 12.13 Illustrating how the call stack operates

 1 fn print_depth(depth:usize) {
 2     for _ in 0..depth {
 3         print!("#");
 4     }
 5     println!("");
 6 }
 7  
 8 fn dive(depth: usize, max_depth: usize) {
 9     print_depth(depth);
10     if depth >= max_depth {
11         return;
12  
13     } else {
14         dive(depth+1, max_depth);
15     }
16     print_depth(depth);
17 }
18  
19 fn main() {
20     dive(0, 5);
21 }

There’s a lot of work to do to make this happen. The Rust language itself doesn’t have the tools to enable this control-flow trickery. It needs to access some provided by its compiler’s toolchain. Compilers provide special functions known as intrinsics to application programs. Using an intrinsic function with Rust takes some ceremony to set up, but that operates as a standard function once the set-up is in place.

12.9.1 Introducing the sjlj project

The sjlj project demonstrates contorting the normal control flow of a function. With the help of some assistance from the OS and the compiler, it’s actually possible to create a situation where a function can move to anywhere in the program. Listing 12.17 uses that functionality to bypass several layers of the call stack, creating the output from the right side of table 12.3. Figure 12.5 shows the control flow for the sjlj project.

Figure 12.5 Control flow of the sjlj project. The program’s control flow can be intercepted via a signal and then resumed from the point of setjmp().

12.9.2 Setting up intrinsics in a program

Listing 12.17 uses two intrinsics, setjmp() and longjmp(). To enable these in our programs, the crate must be annotated with the attribute provided. The following listing provides this documentation.

Listing 12.14 Crate-level attribute required in main.rs

#![feature(link_llvm_intrinsics)]

This raises two immediate questions. We’ll answer the following shortly:

Additionally, we need to tell Rust about the functions that are being provided by LLVM. Rust won’t know anything about them, apart from their type signatures, which means that any use of these must occur within an unsafe block. The following listing shows how to inform Rust about the LLVM functions. The source for this listing is in ch12/ch12-sjlj/src/main.rs.

Listing 12.15 Declaring the LLVM intrinsic functions within listing 12.17

extern "C" {
  #[link_name = "llvm.eh.sjlj.setjmp"]      
  pub fn setjmp(_: *mut i8) -> i32;         
 
  #[link_name = "llvm.eh.sjlj.longjmp"]     
  pub fn longjmp(_: *mut i8);
}

Provides specific instructions to the linker about where it should look to find the function definitions

As we’re not using the argument’s name, uses an underscore (_) to make that explicit

This small section of code contains a fair amount of complexity. For example

What is an intrinsic function?

Intrinsic functions, generally referred to as intrinsics, are functions made available via the compiler rather than as part of the language. Whereas Rust is largely target-agnostic, the compiler has access to the target environment. This access can facilitate extra functionality. For example, a compiler understands the characteristics of the CPU that the to-be-compiled program will run on. The compiler can make that CPU’s instructions available to the program via intrinsics. Some examples of intrinsic functions include

What is LLVM?

From the point of view of Rust programmers, LLVM can be considered as a subcomponent of rustc, the Rust compiler. LLVM is an external tool that’s bundled with rustc. Rust programmers can draw from the tools it provides. One set of tools that LLVM provides is intrinsic functions.

LLVM is itself a compiler. Its role is illustrated in figure 12.6.

Figure 12.6 Some of the major steps required to generate an executable from Rust source code. LLVM is an essential part of the process but not one that is user-facing.

LLVM translates code produced by rustc, which produces LLVM IR (intermediate language) into machine-readable assembly language. To make matters more complicated, another tool, called a linker, is required to stitch multiple crates together. On Windows, Rust uses link.exe, a program provided by Microsoft as its linker. On other operating systems, the GNU linker ld is used.

Understanding more detail about LLVM implies learning more about rustc and compilation in general. Like many things, getting closer to the truth requires exploring through a fractal-like domain. Learning every subsystem seems to require learning about another set of subsystems. Explaining more here would be a fascinating, but ultimately distracting diversion.

12.9.3 Casting a pointer to another type

One of the more arcane parts of Rust’s syntax is how to cast between pointer types. You’ll encounter this as you make your way through listing 12.17. But problems can arise because of the type signatures of setjmp() and longjmp(). In this code snippet, extracted from listing 12.17, you can see that both functions take a *mut i8 pointer as an argument:

extern "C" {
  #[link_name = "llvm.eh.sjlj.setjmp"]
  pub fn setjmp(_: *mut i8) -> i32;
 
  #[link_name = "llvm.eh.sjlj.longjmp"]
  pub fn longjmp(_: *mut i8);
}

Requiring a *mut i8 as an input argument is a problem because our Rust code only has a reference to a jump buffer (e.g., &jmp_buf).2 The next few paragraphs work through the process of resolving this conflict. The jmp_buf type is defined like this:

const JMP_BUF_WIDTH: usize =
  mem::size_of::<usize>() * 8;        
type jmp_buf = [i8; JMP_BUF_WIDTH];

This constant is 64 bits wide (8 × 8 bytes) in 64-bit machines and 32 bits wide (8 × 4 bytes) on 32-bit machines.

The jmp_buf type is a type alias for an array of i8 that is as wide as 8 usize integers. The role of jmp_buf is to store the state of the program, such that the CPU’s registers can be repopulated when needed. There is only one jmp_buf value within listing 12.17, a global mutable static called RETURN_HERE, defined on line 14. The following example shows how jmp_buf is initialized:

static mut RETURN_HERE: jmp_buf = [0; JMP_BUF_WIDTH];

How do we treat RETURN_HERE as a pointer? Within the Rust code, we refer to RETURN_ HERE as a reference (&RETURN_HERE). LLVM expects those bytes to be presented as a *mut i8. To perform the conversion, we apply four steps, which are all packed into a single line:

unsafe { &RETURN_HERE as *const i8 as *mut i8 }

Let’s explain what those four steps are:

  1. Start with &RETURN_HERE, a read-only reference to a global static variable of type [i8; 8] on 64-bit machines or [i8; 4] on 32-bit machines.

  2. Convert that reference to a *const i8. Casting between pointer types is considered safe Rust, but deferencing that pointer requires an unsafe block.

  3. Convert the *const i8 to a *mut i8. This declares the memory as mutable (read/write).

  4. Wrap the conversion in an unsafe block because it deals with accessing a global variable.

Why not use something like &mut RETURN_HERE as *mut i8? The Rust compiler becomes quite concerned about giving LLVM access to its data. The approach provided here, starting with a read-only reference, puts Rust at ease.

12.9.4 Compiling the sjlj project

We’re now in a position where possible points of confusion about listing 12.17 should be minor. The following snippet again shows the behavior we’re attempting to replicate:

$ git clone https:/ /github.com/rust-in-action/code rust-in-action
$ cd rust-in-action/ch12/ch12-sjlj 
$ cargo run -q 
#
#
early return!
finishing!

One final note: to compile correctly, the sjlj project requires that rustc is on the nightly channel. If you encounter the error “#![feature] may not be used on the stable release channel,” use rustup install nightly to install it. You can then make use of the nightly compiler by adding the +nightly argument to cargo. The following console output demonstrates encountering that error and recovering from it:

$ cargo run -q 
error[E0554]: #![feature] may not be used on the stable release channel
 --> src/main.rs:1:1
  |
1 | #![feature(link_llvm_intrinsics)]
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
error: aborting due to previous error
 
For more information about this error, try `rustc --explain E0554`.
 
$ rustup toolchain install nightly 
...
 
$ cargo +nightly run -q 
#
##
###
early return!
finishing!

12.9.5 sjlj project source code

The following listing employs LLVM’s compiler to access the operating system’s longjmp facilities. longjmp allows programs to escape their stack frame and jump anywhere within their address space. The code for listing 12.6 is in ch12/ch12-sjlj/Cargo.toml and listing 12.17 is in ch12/ch12-sjlj/src/main.rs.

Listing 12.16 Project metadata for sjlj

[package]
name = "sjlj"
version = "0.1.0"
authors = ["Tim McNamara <code@timmcnamara.co.nz>"]
edition = "2018"
 
[dependencies]
libc = "0.2"

Listing 12.17 Using LLVM’s internal compiler machinery (intrinsics)

  1 #![feature(link_llvm_intrinsics)]
  2 #![allow(non_camel_case_types)]
  3 #![cfg(not(windows))]                                  
  4  
  5 use libc::{
  6   SIGALRM, SIGHUP, SIGQUIT, SIGTERM, SIGUSR1,
  7 };
  8 use std::mem;
  9  
 10 const JMP_BUF_WIDTH: usize =
 11   mem::size_of::<usize>() * 8;
 12 type jmp_buf = [i8; JMP_BUF_WIDTH];
 13  
 14 static mut SHUT_DOWN: bool = false;                    
 15 static mut RETURN_HERE: jmp_buf = [0; JMP_BUF_WIDTH];
 16 const MOCK_SIGNAL_AT: usize = 3;                       
 17  
 18 extern "C" {
 19   #[link_name = "llvm.eh.sjlj.setjmp"]
 20   pub fn setjmp(_: *mut i8) -> i32;
 21  
 22   #[link_name = "llvm.eh.sjlj.longjmp"]
 23   pub fn longjmp(_: *mut i8);
 24 }
 25  
 26 #[inline]                                              
 27 fn ptr_to_jmp_buf() -> *mut i8 {
 28   unsafe { &RETURN_HERE as *const i8 as *mut i8 }
 29 }
 30  
 31 #[inline]                                              
 32 fn return_early() {
 33   let franken_pointer = ptr_to_jmp_buf();
 34   unsafe { longjmp(franken_pointer) };                 
 35 }
 36  
 37 fn register_signal_handler() {
 38   unsafe {
 39     libc::signal(SIGUSR1, handle_signals as usize);    
 40   }
 41 }
 42  
 43 #[allow(dead_code)]
 44 fn handle_signals(sig: i32) {
 45   register_signal_handler();
 46  
 47   let should_shut_down = match sig {
 48     SIGHUP => false,
 49     SIGALRM => false,
 50     SIGTERM => true,
 51     SIGQUIT => true,
 52     SIGUSR1 => true,
 53     _ => false,
 54   };
 55  
 56   unsafe {
 57     SHUT_DOWN = should_shut_down;
 58   }
 59  
 60   return_early();
 61 }
 62  
 63 fn print_depth(depth: usize) {
 64   for _ in 0..depth {
 65     print!("#");
 66   }
 67   println!();
 68 }
 69  
 70 fn dive(depth: usize, max_depth: usize) {
 71   unsafe {
 72     if SHUT_DOWN {
 73       println!("!");
 74       return;
 75     }
 76   }
 77   print_depth(depth);
 78  
 79   if depth >= max_depth {
 80     return;
 81   } else if depth == MOCK_SIGNAL_AT {
 82     unsafe {
 83       libc::raise(SIGUSR1);
 84     }
 85   } else {
 86     dive(depth + 1, max_depth);
 87   }
 88   print_depth(depth);
 89 }
 90  
 91 fn main() {
 92   const JUMP_SET: i32 = 0;
 93  
 94   register_signal_handler();
 95  
 96   let return_point = ptr_to_jmp_buf();
 97   let rc = unsafe { setjmp(return_point) };
 98   if rc == JUMP_SET {
 99     dive(0, 10);
100   } else {
101     println!("early return!");
102   }
103  
104   println!("finishing!")
105 }

Only compile on supported platforms.

When true, the program exits.

Allows a recursion depth of 3

An #[inline] attribute marks the function as being available for inlining, which is a compiler optimization technique for eliminating the cost of function calls.

This is unsafe because Rust cannot guarantee what LLVM does with the memory at RETURN_HERE.

Asks libc to associate handle_signals with the SIGUSR1 signal

12.10 A note on applying these techniques to platforms without signals

Signals are a “UNIX-ism.” On other platforms, messages from the OS are handled differently. On MS Windows, for example, command-line applications need to provide a handler function to the kernel via SetConsoleCtrlHandler. That handler function is then invoked when a signal is sent to the application.

Regardless of the specific mechanism, the high-level approach demonstrated in this chapter should be fairly portable. Here is the pattern:

12.11 Revising exceptions

At the start of the chapter, we discussed the distinction between signals, interrupts, and exceptions. There was little coverage of exceptions, directly. We have treated these as a special class of interrupts. Interrupts themselves have been modeled as signals.

To wrap up this chapter (and the book), we explored some of the features available in rustc and LLVM. The bulk of this chapter utilized these features to work with signals. Within Linux, signals are the main mechanism that the OS uses to communicate with applications. On the Rust side, we have spent lots of time interacting with libc and unsafe blocks, unpacking function pointers, and tweaking global variables.

Summary


1.Output assumes a fair random number generator, which Rust uses by default. This assumption holds as long as you trust your operating system’s random number generator.

2.jmp_buf is the conventional name for this buffer, which might be useful for any readers who want to dive deeper themselves.