Chapter 12: Signals, interrupts, and exceptions
The chapter introduces how outside events interrupt program execution and how signals let running programs communicate.
After this chapter you will be able to
- Outside events and execution. Explain that external events can interrupt a program’s execution and cause the OS to require the application to deal with a message.
- Signals between running programs. Describe signals as a way to send messages between different running programs, originating in the UNIX OS tradition.
- CPU exception categories. Distinguish aborts, faults, and traps as kinds of CPU exception, and explain how this use of “exception” differs from its common programming-language meaning.
- Interrupt and signal terminology. Distinguish hardware interrupts, software interrupts, and signals by where they originate and whether the OS or an application handles them.
- Signals and interrupts distinction. Distinguish an OS-level signal from a CPU-related interrupt, and explain what a signal communicates.
- Interrupt control flow. Explain how an interrupt changes control flow and how this contrasts with signals that may be ignored.
- Signals as safer practice. Justify why the chapter treats signals as interrupts for practice despite distinguishing them.
- Quick handler and main loop. Describe the general signal-handling pattern outlined for an application.
- Interrupts and program flow. Describe what happens when an interrupt occurs during the addition program and why the program’s calculation remains valid.
- Program-generated software interrupts. Explain that a program generates a software interrupt by sending a specific instruction to the CPU.
- Hardware interrupt notification path. Explain that a hardware device notifies the CPU of an interrupt through a specialized chip called the Programmable Interrupt Controller (PIC).
- Default signal behavior. Explain that, for most signals, an application without a special handler is terminated by the OS, and why relying on that default can be preferable to writing unnecessary code.
- SIGSTOP and SIGCONT behavior. Explain that SIGSTOP halts a program’s execution and that it remains suspended until it receives SIGCONT, which lets someone pause a running application and resume it later.
- Signal lists and defaults. Explain how the section suggests finding information about signals and why an application usually need not set handlers for all of them.
- SIGKILL and SIGSTOP limits. Distinguish SIGKILL and SIGSTOP from other signals in terms of what an application can do about them.
- Global shutdown flag pattern. Trace how a signal handler and the application cooperate through a Boolean global flag to perform a clean shutdown.
- Mutable global shutdown flag. Describe how Rust declares a global variable with `static` in global scope, and how a mutable global such as `SHUT_DOWN` could let a signal handler indicate that the program should shut down.
- Unsafe global state access. Explain why the subsection treats global variables cautiously: accessing them, even to read them, is unsafe, and the resulting `unsafe` blocks are a warning to avoid global state when possible.
- Shutdown indication through a variable. Explain why the example has a signal handler set a variable to indicate that the program needs to shut down, rather than do extensive work in the handler.
- Application-defined signal messages. Explain how an application can assign meanings to SIGUSR1 and SIGUSR2 and use them for limited messaging, such as notifying a peer application that data is ready.
- Function pointers and addresses. Explain that a function pointer points to the start of a function’s bytes and that casting it to usize yields an integer representation of its address. Describe why casting the value back produces a matching address.
- Ignoring and restoring signals. Explain how the ignore project changes a signal’s handling with libc::signal() and then restores the default behavior. State the exception for SIGSTOP and SIGKILL.
- Nonlocal control transfer. Explain how setjmp and longjmp let a program bypass ordinary returns through a deep call stack.
- Bypassing the call stack. Explain that the sjlj project demonstrates how a function can depart from normal control flow and bypass several layers of the call stack with assistance from the OS and compiler.
- LLVM intrinsic setup. Describe how the program enables and declares the setjmp() and longjmp() intrinsics, and explain why calls to them require unsafe blocks.
- Compiler-provided intrinsic functions. Explain what an intrinsic function is and why a compiler can provide functionality that a largely target-agnostic language does not provide directly.
- Jump buffer pointer conversion. Trace how the example turns &RETURN_HERE into the *mut i8 expected by LLVM, and explain why the conversion uses an unsafe block.
- Nightly toolchain for sjlj. Explain why the sjlj project needs the nightly Rust compiler and describe how to switch to it if compilation fails on stable Rust.
- LLVM access to longjmp. Explain that the listing uses LLVM’s compiler to access the operating system’s longjmp facilities, which let a program escape its stack frame and jump elsewhere in its address space.
- Portable interrupt callback pattern. Explain the high-level pattern the section presents as portable across operating systems.
- Exceptions interrupts and signals. Describe how the chapter treats exceptions in relation to interrupts and how it models interrupts in relation to signals.
What it builds on
- Implementing a CPU to establish that functions are also data (chapter 5) · Functions can be understood as data made of bytes.
- Defining functions (chapter 2) · The syntax of function declarations and their arguments and return values.
- Compiling single files with rustc (chapter 2) · The earlier introduction to compiling Rust files with rustc.
- Managing Rust toolchains with rustup (chapter 2) · Managing Rust toolchains with rustup
Section by section
Introduction
The chapter introduces how outside events interrupt program execution and how signals let running programs communicate.
- Outside events and execution: Explain that external events can interrupt a program’s execution and cause the OS to require the application to deal with a message.
Why it matters: It explains why an application may need to respond to events it did not initiate. - Signals between running programs: Describe signals as a way to send messages between different running programs, originating in the UNIX OS tradition.
Why it matters: It establishes what signals are for before the chapter examines how programs handle them.
Key terms: interrupts, OS, program execution, signals, UNIX
12.1 Glossary
Defines the chapter’s terminology for CPU-level interruptions and OS-level signals, including the kinds of exception.
- CPU exception categories: Distinguish aborts, faults, and traps as kinds of CPU exception, and explain how this use of “exception” differs from its common programming-language meaning.
Why it matters: These distinctions make it easier to follow later discussion of exceptions without treating every error as the same kind of event. - Interrupt and signal terminology: Distinguish hardware interrupts, software interrupts, and signals by where they originate and whether the OS or an application handles them.
Why it matters: The distinction identifies whether the chapter is talking about an event at the CPU and OS level or an interruption experienced by an application.
Key terms: abort, fault, trap, exception, hardware interrupt, software interrupt, interrupt, signal
12.1.1 Signals vs. interrupts
This section distinguishes OS-level signals from CPU-related interrupts, then explains why the chapter practices interrupt-handling ideas with signals.
- Signals and interrupts distinction: Distinguish an OS-level signal from a CPU-related interrupt, and explain what a signal communicates.
Why it matters: The distinction keeps the chapter’s deliberately simplified terminology from obscuring what each mechanism is. - Interrupt origins and forms: Describe the two origins of interrupts and identify the exception, trap, and fault descriptions given here.
Why it matters: These names clarify whether an interruption arises during CPU processing or can arrive independently from a device. - Interrupt control flow: Explain how an interrupt changes control flow and how this contrasts with signals that may be ignored.
Why it matters: A forced change of control flow is why interrupt handling must account for what the application was doing when interrupted. - Signals as safer practice: Justify why the chapter treats signals as interrupts for practice despite distinguishing them.
Why it matters: It explains why the exercises use a safer approximation rather than modify the kernel. - Quick handler and main loop: Describe the general signal-handling pattern outlined for an application.
Why it matters: This pattern lets the normal program flow respond to an interruption without doing lengthy work inside the handler.
Key terms: signal, interrupt, interprocess communication, synchronous interrupt, exception, asynchronous interrupt, trap, fault, control flow, handler code
12.2 How interrupts affect applications
A simple addition program shows that an interrupt diverts the CPU without changing the program’s calculation, though it can change how long the program takes.
- Interrupts and program flow: Describe what happens when an interrupt occurs during the addition program and why the program’s calculation remains valid.
Why it matters: An interrupt can change what the CPU does temporarily without changing the calculation the application describes. - Variable wall clock time: Explain why the addition program’s wall clock time can vary even though c is always calculated.
Why it matters: The same calculation can finish in different amounts of elapsed time.
Key terms: interrupt, interrupt handler, control flow, hardware interrupts, wall clock time, nondeterministic
12.3 Software interrupts
This section explains that programs generate software interrupts with CPU instructions and briefly identifies Rust’s asm! macro and its compiler requirement.
- Program-generated software interrupts: Explain that a program generates a software interrupt by sending a specific instruction to the CPU.
Why it matters: This identifies what makes an interrupt software-generated. - Rust assembly compiler requirement: Describe the section’s Rust approach: invoke asm! to issue the instruction, and, as of Rust 1.50, use the nightly compiler because the macro is unstable.
Why it matters: It connects the CPU-level idea to the Rust tool named in this section.
Key terms: software interrupts, CPU, asm! macro, nightly Rust compiler, rustup
12.4 Hardware interrupts
This section identifies the Programmable Interrupt Controller as the link between hardware devices and the CPU in a hardware interrupt.
- Hardware interrupt notification path: Explain that a hardware device notifies the CPU of an interrupt through a specialized chip called the Programmable Interrupt Controller (PIC).
Why it matters: The PIC is the key intermediary in the hardware-interrupt flow described here.
Key terms: hardware interrupts, Programmable Interrupt Controller (PIC)
12.5 Signal handling
12.5.1 Default behavior
This section explains why an application can often rely on default signal handling and distinguishes three common termination signals.
- Default signal behavior: Explain that, for most signals, an application without a special handler is terminated by the OS, and why relying on that default can be preferable to writing unnecessary code.
Why it matters: You can decide when the system’s behavior is sufficient without adding a handler unnecessarily. - Common termination signals: Distinguish the stated effects and usual sources of SIGINT, SIGTERM, and SIGKILL, without treating them as representative of every signal.
Why it matters: These examples make the default termination behavior concrete without implying that all signals mean termination.
Key terms: signal, default behavior, handler function, SIGINT, SIGTERM, SIGKILL
12.5.2 Suspend and resume a program’s operation
This section explains how SIGSTOP and SIGCONT suspend and resume a UNIX process, then demonstrates sending those signals to a running program.
- SIGSTOP and SIGCONT behavior: Explain that SIGSTOP halts a program’s execution and that it remains suspended until it receives SIGCONT, which lets someone pause a running application and resume it later.
Why it matters: These signals let you distinguish pausing a process from terminating it. - PID-targeted suspension demonstration: Describe how the sixty example uses its printed PID and a second console to send SIGSTOP and then SIGCONT with kill, and what happens to its counter.
Why it matters: The printed PID lets you direct both signals to the same running process and observe the pause and resumption.
Key terms: SIGSTOP, SIGCONT, suspended, job control, sixty, PID, kill
12.5.3 Listing all signals supported by the OS
The section points readers to the kill command for signal information and explains why most applications need only consider a small subset, with SIGKILL and SIGSTOP as exceptions.
- Signal lists and defaults: Explain how the section suggests finding information about signals and why an application usually need not set handlers for all of them.
Why it matters: It helps you focus on signals an application is likely to encounter rather than treating every listed signal as a handling requirement. - SIGKILL and SIGSTOP limits: Distinguish SIGKILL and SIGSTOP from other signals in terms of what an application can do about them.
Why it matters: These exceptions set a firm limit on an application's control over signals.
Key terms: kill command, signals, default handlers, SIGKILL, SIGSTOP, handled, blocked
12.6 Handling signals with custom actions
This section explains why applications use signal handlers for clean shutdown and how a minimal handler can request that shutdown through a global flag.
- Clean shutdown purpose: Explain why an application might handle a signal instead of relying on its default action, naming cleanup it may need to perform.
Why it matters: A clean shutdown gives an application a chance to finish work such as writing pending data and closing connections. - Signal handler constraints: Describe the handler function’s signature and why its limited information and operating constraints make it unsuitable for doing all shutdown work directly.
Why it matters: These limits explain why the handler should request cleanup rather than attempt the whole shutdown itself. - Global shutdown flag pattern: Trace how a signal handler and the application cooperate through a Boolean global flag to perform a clean shutdown.
Why it matters: The flag lets a constrained handler notify ordinary application code to do the cleanup.
Key terms: default actions, signal handlers, shut down cleanly, signal handler, f(i32) -> (), signals, Boolean flag, global variable, race conditions
12.6.1 Global variables in Rust
This subsection introduces Rust global variables through a mutable shutdown flag and explains why accessing global state calls for caution.
- Mutable global shutdown flag: Describe how Rust declares a global variable with `static` in global scope, and how a mutable global such as `SHUT_DOWN` could let a signal handler indicate that the program should shut down.
Why it matters: This gives the shutdown example a way to communicate a signal handler’s decision to the rest of the program. - Unsafe global state access: Explain why the subsection treats global variables cautiously: accessing them, even to read them, is unsafe, and the resulting `unsafe` blocks are a warning to avoid global state when possible.
Why it matters: The convenience of a global shutdown flag comes with a caution about using global state.
Key terms: global variables, static, static mut, SHUT_DOWN, unsafe blocks, global state
12.6.2 Using a global variable to indicate that shutdown has been initiated
This section shows how a minimal signal handler can indicate that shutdown has begun, explains how handlers are registered, and distinguishes static values from const values.
- Shutdown indication through a variable: Explain why the example has a signal handler set a variable to indicate that the program needs to shut down, rather than do extensive work in the handler.
Why it matters: It explains why the handler communicates a shutdown request instead of performing the shutdown work itself. - Signal handler registration: Describe how the example associates a signal with a handler through libc::signal(), including the function pointer passed to it.
Why it matters: Registration is what connects an incoming signal to the handler intended to receive it. - Static and const storage: Distinguish static from const values by where their values may appear in memory, and explain the possible performance benefit of duplicating const values.
Why it matters: The distinction explains why similarly named values can have different memory-access behavior.
Key terms: signal handler, global variable, shut down, libc::signal(), signal name, function pointer, usize, static, const, data locality
12.7 Sending application-defined signals
This section explains how application-defined meanings for SIGUSR1 and SIGUSR2 let signals serve as limited messages.
- Application-defined signal messages: Explain how an application can assign meanings to SIGUSR1 and SIGUSR2 and use them for limited messaging, such as notifying a peer application that data is ready.
Why it matters: It shows how applications can use a signal to prompt another application to act.
Key terms: signals, SIGUSR1, SIGUSR2, custom signals
12.7.1 Understanding function pointers and their syntax
Explains how Rust function pointers represent function addresses, how they can be cast to and from usize, and how to read their type syntax.
- Function pointers and addresses: Explain that a function pointer points to the start of a function’s bytes and that casting it to usize yields an integer representation of its address. Describe why casting the value back produces a matching address.
Why it matters: It makes the cast in the signal-handling code understandable as an address conversion rather than a conversion of the function’s behavior. - Function pointer type syntax: Describe how this section reads the type *const fn() -> () for noop: a const pointer to a function with no arguments that returns unit.
Why it matters: Reading the type makes clear what kind of function address the example represents.
Key terms: function pointer, usize, address, *const fn() -> (), const pointer, unit
12.8 Ignoring signals
This section shows how to ignore selected signals with libc::SIG_IGN and later restore their default behavior with SIG_DFL.
- Ignoring and restoring signals: Explain how the ignore project changes a signal’s handling with libc::signal() and then restores the default behavior. State the exception for SIGSTOP and SIGKILL.
Why it matters: A program can avoid the default termination caused by selected signals without ignoring them permanently.
Key terms: libc::signal(), libc::SIG_IGN, SIG_DFL, SIGSTOP, SIGKILL
12.9 Shutting down from deeply nested call stacks
This section explains why a program might jump out of a deep call stack, how setjmp and longjmp change control flow, and why Rust needs compiler-provided support for the technique.
- Nonlocal control transfer: Explain how setjmp and longjmp let a program bypass ordinary returns through a deep call stack.
Why it matters: It explains how the program can reach an earlier point without returning through every nested call. - Urgent error response: Justify why a program might use a direct jump to error-handling code when trouble occurs deep in its call stack.
Why it matters: The direct jump has a practical purpose when ordinary control flow may take too long. - Compiler-provided intrinsics: Explain where Rust programs obtain support for this control-flow technique.
Why it matters: It clarifies why implementing the jump in Rust requires something beyond the language’s own facilities.
Key terms: nonlocal control transfer, setjmp, longjmp, call stack, error-handling code, intrinsics, compiler’s toolchain
12.9.1 Introducing the sjlj project
Introduces sjlj as a project that alters normal function control flow to bypass layers of the call stack.
- Bypassing the call stack: Explain that the sjlj project demonstrates how a function can depart from normal control flow and bypass several layers of the call stack with assistance from the OS and compiler.
Why it matters: This explains the unusual control flow that the sjlj project is built to demonstrate.
Key terms: sjlj project, control flow, call stack
12.9.2 Setting up intrinsics in a program
This section explains how the sjlj program makes LLVM intrinsics available to Rust and what intrinsics and LLVM do.
- LLVM intrinsic setup: Describe how the program enables and declares the setjmp() and longjmp() intrinsics, and explain why calls to them require unsafe blocks.
Why it matters: These declarations let the Rust program refer to the LLVM functions used by the sjlj project. - Compiler-provided intrinsic functions: Explain what an intrinsic function is and why a compiler can provide functionality that a largely target-agnostic language does not provide directly.
Why it matters: This explains why the sjlj project obtains these functions through the compiler rather than ordinary Rust language features. - LLVM compilation role: Describe LLVM’s place in Rust compilation and distinguish its translation work from the linker’s job.
Why it matters: It places the intrinsics’ provider in the compilation process without confusing LLVM with the linker.
Key terms: setjmp(), longjmp(), unsafe block, extern "C", link_name, intrinsic functions, intrinsics, target environment, atomic operations, exception handling
12.9.3 Casting a pointer to another type
This section explains why a jump-buffer reference must be cast to a mutable byte pointer for setjmp() and longjmp(), and walks through the conversion used in the example.
- Jump buffer pointer mismatch: Explain what jmp_buf stores and why a reference to it cannot be passed directly to setjmp() or longjmp() in this example.
Why it matters: The type mismatch explains why the pointer cast is needed. - Jump buffer pointer conversion: Trace how the example turns &RETURN_HERE into the *mut i8 expected by LLVM, and explain why the conversion uses an unsafe block.
Why it matters: This conversion lets the example present the jump-buffer bytes in the form LLVM expects.
Key terms: jmp_buf, setjmp(), longjmp(), *mut i8, RETURN_HERE, *const i8, unsafe block
12.9.4 Compiling the sjlj project
This subsection explains how to compile the sjlj project with Rust’s nightly toolchain.
- Nightly toolchain for sjlj: Explain why the sjlj project needs the nightly Rust compiler and describe how to switch to it if compilation fails on stable Rust.
Why it matters: Using the required toolchain lets you compile the project instead of being stopped by a stable-channel error.
Key terms: sjlj project, rustc, nightly channel, rustup install nightly, +nightly, cargo
12.9.5 sjlj project source code
This subsection identifies how the sjlj source code uses LLVM to access longjmp for escaping a stack frame.
- LLVM access to longjmp: Explain that the listing uses LLVM’s compiler to access the operating system’s longjmp facilities, which let a program escape its stack frame and jump elsewhere in its address space.
Why it matters: It identifies the facility the sjlj example uses to leave a stack frame.
Key terms: LLVM, longjmp, stack frame, address space
12.10 A note on applying these techniques to platforms without signals
The section explains how the chapter’s interrupt-handling approach can apply on platforms that do not use UNIX signals.
- Platform-specific OS notifications: Distinguish UNIX signals from the way other platforms deliver OS messages, using the MS Windows console-handler example.
Why it matters: It separates the chapter’s UNIX-specific examples from the broader idea they illustrate. - Portable interrupt callback pattern: Explain the high-level pattern the section presents as portable across operating systems.
Why it matters: It identifies the common idea behind different platforms’ handling mechanisms.
Key terms: signals, SetConsoleCtrlHandler, handler function, interrupts, callback system, function pointer
12.11 Revising exceptions
This closing section revisits how exceptions relate to interrupts and signals, and summarizes the role of Linux signals in OS-to-application communication.
- Exceptions interrupts and signals: Describe how the chapter treats exceptions in relation to interrupts and how it models interrupts in relation to signals.
Why it matters: This relationship ties the chapter’s three central terms together. - Linux signals communication role: Explain the role signals play in communication between Linux and applications, as stated in this section.
Why it matters: It explains why the chapter spends so much time working with signals.
Key terms: exceptions, interrupts, signals, Linux, OS, applications
12.12 Summary
Concepts explained in this chapter
- exceptionAn exception is a synchronous or otherwise special execution event that interrupts normal program flow because of an error or exceptional condition.
- interruptA hardware interrupt is an asynchronous request from a hardware device that causes a processor to suspend or redirect its current execution to handle an event.
- signalA signal is an asynchronous operating-system notification delivered to a process to report an event or request handling.
- signal handlerA signal handler is a function or disposition associated with a signal that determines how a process responds when the signal is delivered.
- setjmp/longjmplongjmp is a non-local control-flow function or compiler intrinsic that restores an execution context previously saved by setjmp.
- function pointer typeA function pointer is a value that identifies a function and can be passed or stored so that the function can be invoked indirectly.
- compiler intrinsicA compiler intrinsic is a compiler-recognized low-level operation exposed through a programming-language interface rather than an ordinary library implementation.
- unsafe blockAn unsafe block is a Rust code region in which operations requiring explicit programmer responsibility for safety are permitted.
- execution context stackA call stack is the runtime structure that records active function calls, their local data, and the points to which control returns.
- static mutstatic mut declares a mutable Rust static variable with a program-wide lifetime and mutable access requiring unsafe handling.
- page faultA page fault is a memory-access condition in which the requested address is unavailable and the required data must be fetched into memory.
- global variableA global variable is a variable whose storage and scope make it accessible across multiple parts of a program.
- CargoCargo is Rust's package manager and build system for creating, compiling, running, documenting, and managing Rust projects.
- operating system kernelA kernel is the privileged core of an operating system that manages hardware resources and provides fundamental services for software execution.
- extern "C"extern "C" is Rust syntax that specifies C-compatible calling and linkage conventions for functions or foreign-function interfaces.
- unsafe RustThe unsafe keyword marks Rust code that may perform operations requiring guarantees the compiler cannot verify automatically.
These guides are generated from the text of Manning books by Manning's knowledge graph and link to the sections they draw on. The full books are available on liveBook and at manning.com.
