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
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;
}
$ ./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);