AR

Signals and Exceptional Control Flow (CMPSC 311)

Control Flow Basics

  • Central premise: CPUs execute one instruction at a time from power-on to shutdown.
    • The sequenced list of instructions constitutes the program’s control flow.
    • Diagram (verbal):
  • Whenever something disrupts or diverts this straight-line flow, we enter the realm of exceptional control flow (ECF).

Exceptional Control Flow (ECF)

  • Purpose: allow hardware + OS + user programs to react to events (errors, I/O, timers, user requests, …).
  • Generic timeline:
    • Event occurs while executing I_{current}.
    • CPU/OS perform exception processing ➜ possibly run special handler.
    • Optional exception return transfers back to the next instruction I_{next} (or elsewhere).

Low-level ECF mechanisms (hardware + kernel)

  • Exceptions – umbrella term including:
    • Interrupts (asynchronous external events, e.g., I/O complete)
    • Traps (intentional software-generated, e.g., system calls)
    • Faults (potentially recoverable errors, e.g., page fault, divide-by-zero)
    • Aborts (fatal, non-recoverable errors)
  • Implemented jointly by CPU hardware vectors & kernel exception tables.

High-level ECF mechanisms (OS + libraries)

  • Process context switch – driven by hardware timer interrupt + scheduler.
  • Signals – software messages between kernel and user space (focus of this lecture).
  • Non-local jumps – setjmp() / longjmp() in C runtime for intra-process ECF.

UNIX Signals: Definition & Lifecycle

  • A signal is a lightweight asynchronous message delivered by the OS to a process (or specific thread).
    • Triggers delivery: kernel sets a flag → next time process runs in user mode, execution suspends.
    • Then a signal handler runs (default or user-supplied C function).
    • Upon handler return, process normally resumes at interrupted point (some signals terminate or stop instead).
  • Analogy: think of signals as software “interrupts” at the process level.

Canonical Signal Types (subset)

  • Numeric IDs are standardized by POSIX / ANSI; typical GNU/Linux list:
    • 1\;=\;\text{SIGHUP} – terminal hang-up / reload config
    • 2\;=\;\text{SIGINT} – interactive interrupt (Ctrl-C)
    • 3\;=\;\text{SIGQUIT} – quit & core dump (Ctrl-)
    • 6\;=\;\text{SIGABRT} – abort()
    • 8\;=\;\text{SIGFPE} – floating-point exception
    • 9\;=\;\text{SIGKILL} – forced kill, unblockable & uncatchable
    • 11\;=\;\text{SIGSEGV} – segmentation violation
    • 15\;=\;\text{SIGTERM} – polite termination request (default for kill)
    • User-defined: 10=\text{SIGUSR1},\;12=\text{SIGUSR2}
    • Others: 17=\text{SIGCHLD},\;18=\text{SIGCONT},\;19=\text{SIGSTOP}, …
  • Remember: list & numbers vary slightly across OSes; use kill\; -l to print local table.

Signals as Process-Control Instrument

  • Kernel sends signals on errors (e.g., SIGSEGV on invalid memory access).
  • Users/other processes send signals for coordination/control (e.g., reload config, graceful restart, orchestration).
  • Frequently paired opposites:
    • SIGSTOP vs SIGCONT → pause & resume.
    • SIGTERM vs SIGKILL → graceful vs forced shutdown.

Process Identification & ps Utility

  • Each running process obtains a unique PID (positive integer) at fork/exec.
  • Inspect live processes with ps:
  $ ps -U mcdaniel
    PID TTY      TIME CMD
  30908 ?     00:00:00 gnome-keyring-d
  30919 ?     00:00:00 gnome-session
  ...
  • Use PID as operand for most signal-handling tools.

The kill Program

  • Syntax: kill [-<sig>|-SIGname] <pid>
    • Omit <sig> ⇒ defaults to SIGTERM\;(15).
  • Example interactive session:
  $ ./signals
  Sleeping ...zzzzz ....
  ...
  $ kill -1 57613   # sends SIGHUP
  $ kill -2 57613   # sends SIGINT
  $ kill -9 57613   # sends SIGKILL (force)
  • Behaviour: program’s signal handler prints message, wakes up, loops; SIGKILL finally terminates.

SIGTERM vs SIGKILL (Graceful vs Brutal)

  • SIGTERM: request; handler can cleanup (flush logs, close sockets, save state → graceful shutdown).
  • SIGKILL: cannot be intercepted; kernel instantly removes process → potential data loss, inconsistent files.
  • Rule of thumb: attempt SIGTERM first; escalate to SIGKILL only if target is stuck.

killall Utility

  • Broadcast variant: killall [-<sig>] <name> sends chosen signal to all processes whose executable name matches.
    • Good for shutting down multiple worker instances, e.g., killall -SIGKILL firefox.

Self-Signalling with raise()

  • Prototype: int raise(int sig); (from <signal.h>)
  • Uses: self-suspend, self-terminate, trigger custom reload path…
  void suicide_signal(void) {
      raise(SIGKILL);   // program kills itself immediately
  }
  • Implementation detail: simply calls kernel’s kill(getpid(), sig).

Implementing Custom Signal Handlers

  • Minimal API: sighandler_t signal(int signum, sighandler_t handler);

    • handler must have signature void handler(int signo).
    • Example:
    void signal_handler(int no) {
        printf("Sig handler got a [%d]\n", no);
    }
    
    signal(SIGHUP, signal_handler);
    signal(SIGINT, signal_handler);
    
  • After registration, kernel diverts matching signals to your function instead of default.

Function Pointers Refresher (C)

  • Syntax template: <ret_type> (*<var_name>)(<param_list>);
  • Example walk-through:
  int myfunc(int i) { printf("%d\n", i); return 0; }
  int (*func)(int);
  func = myfunc;   // assignment
  func(7);         // call via pointer
  • Signal handlers are specialised usage—pointer passed to signal() or sigaction().

sigaction(): The Modern, Robust API

  • Prototype:
  int sigaction(int signum,
                const struct sigaction *act,
                struct sigaction *oldact);
  • Important struct sigaction fields:
    • sa_handler OR sa_sigaction – pointer to handler.
    • sa_mask – signals to block while handler runs.
    • sa_flags – fine-tuning (examples below).
  • Example setup:
  struct sigaction new_action, old_action;
  new_action.sa_handler = signal_handler;
  new_action.sa_flags   = SA_NODEFER | SA_ONSTACK;
  sigaction(SIGINT, &new_action, &old_action);

Why prefer sigaction()?

  • signal() historically resets handler to default after every delivery → need to re-install inside handler.
  • No control over simultaneous signals or masking.
  • sigaction() extras:
    • SA_NODEFER – allow identical signal to re-enter handler (no implicit block).
    • SA_ONSTACK – run handler on alternate stack (useful for deep stack faults).
    • SA_RESETHAND – auto-restore default after first receipt.

Full Worked Example (Putting It All Together)

void signal_handler(int no) {
    printf("Signal received : %d\n", no);
    if (no == SIGHUP)  printf("Signal handler got a SIGHUP!\n");
    else if (no == SIGINT) printf("Signal handler got a SIGNINT!\n");
}

void cleanup_handler(int no) {
    printf("Killed\n");
    exit(0);
}

int main(void) {
    struct sigaction new_action, old_action;

    // Install robust handler for SIGINT
    new_action.sa_handler = signal_handler;
    new_action.sa_flags   = SA_NODEFER | SA_ONSTACK;
    sigaction(SIGINT, &new_action, &old_action);

    // Legacy registration for SIGHUP
    signal(SIGHUP, signal_handler);

    // Termination cleanup
    signal(SIGTERM, cleanup_handler);

    while (1) {
        printf("Sleeping ...zzzzz ....\n");
        select(0, NULL, NULL, NULL, NULL);  // block until signal
        printf("Woken up!!\n");
    }
    return 0;
}
  • Demo run (condensed):
  $ ./signals
  Sleeping ...zzzzz ....
  Signal received : 1
  Signal handler got a SIGHUP!
  Woken up!!
  Signal received : 2
  Signal handler got a SIGNINT!
  ...
  Killed   # (SIGTERM handled by cleanup_handler)

Practical, Ethical & Debugging Considerations

  • Graceful shutdown: always capture SIGTERM to flush buffers, sync filesystems, release locks—prevents corruption.
  • Security: ensure only authorised users can send sensitive signals (use correct UNIX permissions / process ownership).
  • Race conditions: asynchronous nature means handler may interleave with any code—avoid non-reentrant funcs (e.g., malloc, printf in high-security contexts) or protect with sigprocmask.
  • Debugging: use strace -e signal or /proc/<pid>/status to observe pending/blocked signals.

Connections to Earlier Course Material

  • Builds on earlier lectures covering:
    • Processes & fork/exec – signals operate on PIDs from these calls.
    • CPU exceptions & traps – signals abstract similar ideas at user level.
    • I/O Multiplexing (select/poll) – example program blocks in select, woken up by signals.
  • Prepares for future topics:
    • Thread cancellation & pthread signals.
    • Event-driven servers – epoll + signals for hot reload (e.g., nginx -s reload).

Handy Command & API Cheat-Sheet

  • List signals: kill -l
  • Send polite termination: kill <pid> (or kill -SIGTERM <pid>)
  • Force termination: kill -9 <pid>
  • Broadcast to all copies: killall -SIGINT <name>
  • Programmatic send: kill(pid, SIGUSR1); or raise(SIGUSR1);
  • Install handler (portable):
  struct sigaction sa = {0};
  sa.sa_handler = myhandler;
  sigemptyset(&sa.sa_mask);
  sigaction(SIGUSR1, &sa, NULL);