﻿# Signals

> What a signal is and where it comes from, handlers with sigaction and what is safe inside them, blocking and sigsuspend instead of a racy pause, the parent–child “ready” handshake, and reaping children with waitpid, WNOHANG and SIGCHLD.

Source: https://laszlonemes.com/university/operating-systems/signals · Author: László Nemes

Session 4 · Operating Systems

What a signal is and where it comes from, handlers with sigaction and what is safe inside them, blocking and sigsuspend instead of a racy pause, the parent–child “ready” handshake, and reaping children with waitpid, WNOHANG and SIGCHLD.

18 min read `sigaction vs signal` `volatile sig_atomic_t` `sigprocmask + sigsuspend` `Ready handshake` `waitpid and SIGCHLD`

After [fork](https://laszlonemes.com/university/operating-systems/processes-fork) and [wait](https://laszlonemes.com/university/operating-systems/exec-wait) you can start processes and collect their exit codes. What you cannot do yet is tell a running process something. **Signals** are the simplest way: a small, numbered notification that the kernel delivers to a process, interrupting whatever it is doing. There is no data in a signal – only its number – but that is often enough: “stop”, “I am ready”, “a child has finished”.

Signals are also the first place where timing bites. A signal can arrive at any instruction, before you are ready for it, or twice so quickly that you see it once. Most of this session is about writing code that stays correct no matter when the signal comes – the same skill you need later for pipes and semaphores.

> **The model in one breath:** a signal interrupts the process, runs a handler (or the default action), and the process continues. Block the signal until you are ready, wait for it with `sigsuspend`, and keep the handler tiny.

After this session you can

- Name the common signals, where they come from and what their default action is.
- Install a handler with sigaction() and explain why it is preferred over signal().
- Say what a handler may safely do, and share a flag with main through volatile sig_atomic_t.
- Block a signal with sigprocmask() and wait for it with sigsuspend() without a race.
- Build the “child is ready” handshake between a parent and several children.
- Collect finished children with waitpid(), WNOHANG and a SIGCHLD handler, and decode the status.

## What a signal is

A signal is a software interrupt. Something happens – you press Ctrl+C, the program divides by zero, a child process exits, another process calls `kill()` – and the kernel marks a signal for the target process. The next time that process runs, the kernel delivers it (Fig. 1).

> **Fig. 1** · Signals come from the terminal, from the kernel or from other processes. The target reacts in one of three ways.
>
> *Diagram:* A signal can come from three places: the terminal (Ctrl+C, Ctrl+\), the kernel itself (a crash, a finished child, a broken pipe) or another process that calls kill(). Whatever the source, the target process reacts in one of three ways: the default action, ignoring it, or running its own handler.

Every signal has a number and a symbolic name. Use the names in code: the numbers of some signals (for example `SIGUSR1`, `SIGCHLD`) differ between architectures. `kill -l` lists them all. The ones you meet in this course:

| Signal | No. on Linux x86 | Typical source | Default action |
| --- | --- | --- | --- |
| `SIGINT` | 2 | Ctrl+C in the terminal | terminate |
| `SIGQUIT` | 3 | Ctrl+\ in the terminal | terminate + core dump |
| `SIGKILL` | 9 | `kill -9 pid` | terminate – cannot be caught or ignored |
| `SIGUSR1` | 10 | your own program | terminate |
| `SIGSEGV` | 11 | invalid memory access | terminate + core dump |
| `SIGUSR2` | 12 | your own program | terminate |
| `SIGPIPE` | 13 | writing to a pipe nobody reads | terminate |
| `SIGALRM` | 14 | `alarm()` | terminate |
| `SIGTERM` | 15 | plain `kill pid` | terminate |
| `SIGCHLD` | 17 | a child stopped or exited | ignore |
| `SIGSTOP` | 19 | `kill -STOP pid` | stop – cannot be caught or ignored |
| `SIGTSTP` | 20 | Ctrl+Z in the terminal | stop |

Good to know

**Ctrl+D is not a signal**

Ctrl+D tells the terminal that the input has ended – a program reading the keyboard gets end-of-file. No signal is sent. The quit signal `SIGQUIT` is Ctrl+\.

From the shell, `kill` sends any signal, not only the deadly ones:

```shell
kill -l                 # every signal name and number on this system
kill -TERM 4711         # ask process 4711 to terminate (same as plain kill 4711)
kill -USR1 4711         # send a user-defined signal
kill -9 4711            # SIGKILL – cannot be caught, the process ends immediately
```

The important property is that delivery is **asynchronous**: the process does not ask for the signal, it is interrupted by it. The kernel saves where the program was, runs the handler, and resumes the program at the same point (Fig. 2). For the main program it looks as if nothing happened – except that a blocking call such as `sleep()` or `read()` may return early, and whatever the handler changed is now different.

> **Fig. 2** · The handler runs in the middle of the main program; afterwards main continues where it stopped.
>
> *Diagram:* A signal interrupts the program wherever it is. The kernel saves the program's state, runs the handler, and when the handler returns, the main program continues exactly where it stopped – it does not notice anything except what the handler changed.

- **signal** – A numbered notification the kernel delivers to a process, interrupting it.
- **disposition** – What a process does with a signal: the default action, ignore it, or run a handler.
- **handler** – A function void f(int signum) that the kernel calls when the signal is delivered.
- **pending** – Generated but not delivered yet – for example because the process blocks the signal.

## Default, ignore or handle: signal()

Each signal has a **disposition** in each process: the default action, `SIG_IGN` (ignore) or a handler function. The oldest way to set it is `signal(signum, handler)`. The first demo of the session installs a handler for `SIGTERM`, then tries the same for `SIGKILL`:

1_signal.c – catching SIGTERM, failing on SIGKILL

```c
void handler(int signumber)
{
    printf("My signal handler %i\n", signumber);   /* demo only – see "What is safe in a handler" */
}

int main()
{
    signal(SIGTERM, handler);              /* SIGTERM now runs handler() instead of ending the program */

    if (signal(SIGKILL, handler) == SIG_ERR)
        perror("signal(SIGKILL)");         /* EINVAL: SIGKILL cannot be caught */

    for (int i = 0; ; i++) {
        printf("i = %i\n", i);
        sleep(1);
    }
}
```

```text
$ ./signal_demo
signal(SIGKILL): Invalid argument
i = 0
i = 1
My signal handler 15        <- kill -TERM <pid> from another terminal
i = 2
i = 3
Killed                      <- kill -9 <pid>: no handler, no clean-up
```

`SIGTERM` is the polite request: a program may catch it, finish its work, close its files and exit. Here the handler only prints, so the loop simply continues – the program has chosen to ignore the request. Ctrl+C still ends it, because `SIGINT` keeps its default action. `SIGKILL` and `SIGSTOP` are reserved for the kernel: `signal()` refuses them with `SIG_ERR` and `errno = EINVAL`, so a process can always be killed or stopped.

Common mistake

**Check the return value of signal() and sigaction()**

Writing `signal(SIGKILL, handler);` compiles and runs without a word – the call just fails. Compare the result with `SIG_ERR` (or `-1` for `sigaction`) and print `perror()`, otherwise you will wonder why your handler never runs.

Good to know

**What fork and exec do with dispositions**

A child created by `fork()` inherits the parent's handlers and its blocked-signal mask, but starts with no pending signals. After `exec()` the handlers are reset to the default (the handler code is gone), while ignored signals stay ignored and the mask is kept.

## sigaction() instead of signal()

`signal()` is old and its exact behaviour differs between Unix systems: on some, the handler is reset to the default after the first delivery, and interrupted system calls are restarted on some systems but not others. The man page itself recommends `sigaction()`, which spells everything out in a struct:

Installing a handler with sigaction

```c
struct sigaction sa;
sa.sa_handler = handler;          /* the function to run */
sigemptyset(&sa.sa_mask);         /* signals to block WHILE the handler runs (none extra) */
sa.sa_flags = 0;                  /* no SA_RESTART: a blocking call is interrupted with EINTR */

if (sigaction(SIGUSR1, &sa, NULL) == -1) {   /* NULL: we don't need the old setting back */
    perror("sigaction");
    exit(EXIT_FAILURE);
}
```

| Field | Meaning |
| --- | --- |
| `sa_handler` | The function to run, or `SIG_DFL` / `SIG_IGN`. |
| `sa_mask` | Extra signals to block while the handler runs. The signal being handled is blocked anyway, so the same handler is never nested. |
| `sa_flags` | Options. `SA_RESTART`: restart a `read()`, `write()` or `waitpid()` the signal interrupted instead of failing with `EINTR`. |

The third argument receives the previous setting if you want to restore it later; `NULL` means you don't. Always start `sa_mask` with `sigemptyset()` and set `sa_flags` explicitly – the struct is a local variable with garbage in it.

## What is safe inside a handler

The handler can run between any two instructions of your program – including in the middle of `printf()` or `malloc()`. Those functions keep internal state (the stdout buffer, the heap lists). If the handler calls the same function while main is halfway through it, both work on half-updated state (Fig. 3).

> **Fig. 3** · A handler that calls printf can meet main halfway through its own printf. Set a flag or use write() instead.
>
> *Diagram:* printf is not safe in a handler. If the signal arrives while main is in the middle of printf, the handler's printf works on the same half-updated stdout buffer and the output gets mixed up or lost. A safe handler only sets a volatile sig_atomic_t flag, or uses write(), which is async-signal-safe.

POSIX lists the functions that are **async-signal-safe** – safe to call from a handler. Among them: `write()`, `read()`, `_exit()`, `kill()`, `waitpid()`, `sigprocmask()`. Not on the list: `printf()`, `malloc()`, `free()`, `exit()` and everything in `stdio.h`. The teaching examples print from handlers to make the order of events visible; in a program you hand in, follow the rule below.

The rule: **the handler sets a flag, main does the work.** The flag must be declared `volatile sig_atomic_t`. `sig_atomic_t` is an integer type that is read and written in one step, so main never sees half a value. `volatile` tells the compiler that the variable can change behind its back: without it an optimised build may read the variable once, keep it in a register, and loop forever.

The flag pattern

```c
volatile sig_atomic_t got_term = 0;    /* shared by the handler and main */

void on_term(int sig)
{
    (void)sig;
    got_term = 1;                        /* the only thing the handler does: set a flag */
}

/* in main(), after installing on_term for SIGTERM: */
while (!got_term) {
    do_one_step();
}
cleanup();                               /* printf, free, fclose … are fine here, outside the handler */
```

If the handler really has to print, use `write()` on the file descriptor directly:

```c
void on_usr1(int sig)
{
    (void)sig;
    const char msg[] = "SIGUSR1 arrived\n";
    write(STDOUT_FILENO, msg, sizeof(msg) - 1);   /* write() is async-signal-safe, printf() is not */
}
```

Common mistake

**sig_atomic_t does not make ++ atomic**

`ready_count++` is a read, an add and a write. If a second handler (for another signal) interrupts the first one between the read and the write, one increment is lost. Either use one handler per signal or list the other signals in `sa_mask`, so they wait until the running handler returns.

## Blocking, pending and waiting

Each process has a **signal mask**: the set of signals it blocks right now. A blocked signal is not lost – it becomes **pending** and is delivered the moment it is unblocked. `sigprocmask()` changes the mask: `SIG_BLOCK` adds signals, `SIG_UNBLOCK` removes them, `SIG_SETMASK` replaces the whole set. Signal sets are built with `sigemptyset`, `sigfillset`, `sigaddset` and `sigdelset`.

There is a catch. For the classic signals the kernel keeps **one pending bit per signal**, not a counter. If the same signal arrives twice while it is pending, the second one merges into the first (Fig. 4). Two children that both report with `SIGUSR1` can therefore look like one child.

> **Fig. 4** · One pending bit per signal: two SIGUSR1s can merge; SIGUSR1 + SIGUSR2 are both delivered.
>
> *Diagram:* While a signal is blocked it only becomes pending, and the kernel keeps one pending bit per signal. Two children sending the same SIGUSR1 can collapse into a single delivery, so the parent counts only one. With two different signals, SIGUSR1 and SIGUSR2, both bits are kept and the handler runs twice.

### Sending: kill and raise

`kill(pid, sig)` sends a signal to another process – a child knows its parent from `getppid()`, the parent knows its children from the return value of `fork()`. `raise(sig)` sends a signal to the calling process itself. Neither waits for the signal to be handled.

### Waiting: pause and its race

`pause()` sleeps until any signal is handled. The obvious code – “if the flag is not set yet, pause” – has a gap: if the signal arrives after the check but before `pause()`, the handler runs too early and `pause()` then waits for a signal that has already come (Fig. 5). It is rare, which makes it worse: the program hangs once in a hundred runs.

> **Fig. 5** · pause() leaves a gap between the check and the sleep. Block first, then sigsuspend() – unblocking and sleeping are one step.
>
> *Diagram:* With pause(), a signal that arrives between checking the flag and calling pause() is handled too early, and pause() then waits for a signal that never comes. Blocking the signal first and waiting with sigsuspend(), which unblocks and sleeps in one atomic step, leaves no such gap.

`sigsuspend(&mask)` closes the gap. It replaces the signal mask with `mask` and goes to sleep in one atomic step; when a handler has run, it puts the old mask back and returns (always `-1` with `EINTR`). The pattern is: block the signal early, check your flag, and wait with a mask that unblocks it. A signal that came early is pending and is delivered the moment `sigsuspend` starts.

The `sigaction.c` example uses it to wait for exactly one signal. The mask passed to `sigsuspend` blocks everything except `SIGTERM`, so a `SIGUSR1` that arrives meanwhile stays pending and does not wake the parent:

sigaction.c – wait only for SIGTERM

```c
sigset_t sigset;
sigfillset(&sigset);            /* start with every signal blocked … */
sigdelset(&sigset, SIGTERM);    /* … except SIGTERM */

sigsuspend(&sigset);            /* sleep with this mask; only SIGTERM can wake us */
printf("The program comes back from suspending\n");
```

The program blocks `SIGTERM` before `fork()`, so even a child that sends it immediately cannot beat the parent to `sigsuspend`. When `SIGTERM` wakes the parent and the old mask comes back, the pending `SIGUSR1` is delivered too – its handler runs right after `sigsuspend` returns.

## The parent–child ready handshake

The most common use of signals in this course: the parent starts children and waits until each one says “I am ready” before it sends them work. It looks easy, and the first version is almost right:

2_signal.c – the naive version

```c
int ready = 0;                 /* plain int – the compiler may keep it in a register */

void readyHandler(int sig) { if (sig == SIGUSR1) ready++; }

while (ready < 1)
    ;                          /* busy waiting: 100% CPU, and maybe never sees the change */
puts("Child1 ready!");
```

It works when you try it – and has three problems:

1. `ready` is a plain `int`. With optimisation the compiler may read it once and spin forever. It must be `volatile sig_atomic_t`.
2. **Busy waiting**: the empty loop burns a whole CPU core while it waits.
3. Both children send `SIGUSR1`. If the second one arrives while the first is still pending, they merge (Fig. 4), `ready` stops at 1 and the parent never continues.

The improved version (`2_signal_waitpid_better.c`) fixes the first two: it blocks `SIGUSR1` before `fork()`, installs the handler with `sigaction()` and sleeps in `sigsuspend()`. Its children still send the same signal, one and two seconds after starting – only the timing keeps the two signals apart. The final version (`fork_sigaction.c`) removes the timing assumption: child 1 sends `SIGUSR1`, child 2 sends `SIGUSR2`, and each has its own pending bit (Fig. 6).

> **Fig. 6** · The ready handshake: block, install, fork; children report with different signals; the parent sleeps in sigsuspend until both arrived.
>
> *Diagram:* The ready handshake: the parent blocks SIGUSR1 and SIGUSR2 and installs the handler before it forks. Each child resets its signal mask and reports with its own signal. The parent sleeps in sigsuspend until it has counted two signals, restores the mask and then waits for both children with waitpid.

The parent, step by step. First block both signals and remember the old mask:

Parent – before fork

```c
sigset_t block_mask, old_mask;
sigemptyset(&block_mask);
sigaddset(&block_mask, SIGUSR1);
sigaddset(&block_mask, SIGUSR2);
sigprocmask(SIG_BLOCK, &block_mask, &old_mask);   /* from now on they only become pending */

/* install the handler with sigaction, then fork the children */
pid_t c1 = child1();
pid_t c2 = child2();
```

One handler serves both signals and counts. Its `sa_mask` blocks both, so if they arrive together the second waits until the first handler has finished its `ready_count++`:

One handler, both signals blocked while it runs

```c
sa.sa_handler = signal_handler;        /* one handler for both signals: ready_count++ */
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGUSR1);       /* while the handler runs, the other signal waits */
sigaddset(&sa.sa_mask, SIGUSR2);
sa.sa_flags = 0;
sigaction(SIGUSR1, &sa, NULL);
sigaction(SIGUSR2, &sa, NULL);
```

Then the parent sleeps until both have reported:

Parent – waiting without busy waiting

```c
while (ready_count < 2)
    sigsuspend(&old_mask);     /* atomically: unblock, sleep, run the handler, block again */

sigprocmask(SIG_SETMASK, &old_mask, NULL);   /* back to the original mask */
```

The `while` loop matters: `sigsuspend` returns after **any** handled signal, so the parent checks the counter every time it wakes up. The child side is short. It inherited the blocked mask, so it clears it first (it does not need it, and a child that execs another program would otherwise pass it on), then it reports:

Child – report ready

```c
pid_t child1(void)
{
    pid_t process = fork();
    if (process == -1)
        exit(EXIT_FAILURE);
    if (process > 0)
        return process;                          /* parent: back to main */

    sigset_t empty;                              /* the child inherited the blocked mask – reset it */
    sigemptyset(&empty);
    sigprocmask(SIG_SETMASK, &empty, NULL);

    kill(getppid(), SIGUSR1);                    /* "I am ready" (child2 sends SIGUSR2) */
    exit(EXIT_SUCCESS);
}
```

In your assignment

**In the simulation tasks**

The simulation tasks at the end of the semester typically start exactly like this: the parent waits until every child has “taken its place” and signalled, and only then starts talking to them through pipes or message queues. Write this part once, correctly – block before `fork()`, one signal per child (or one per role), a `sigsuspend` loop – and reuse it. See [Simulation tasks](https://laszlonemes.com/university/operating-systems/simulation-tasks).

Common mistake

**Installing the handler after fork()**

If the parent forks first and calls `sigaction` afterwards, a fast child can send its signal while the parent still has the default action – and the default for `SIGUSR1` is to terminate. Install handlers and block the signals **before** the first `fork()`.

## Children ending: waitpid and SIGCHLD

A child that has exited stays a **zombie** until its parent collects its exit status. You know `wait()` from [the previous session](https://laszlonemes.com/university/operating-systems/exec-wait); `waitpid(pid, &status, options)` waits for one specific child (or any child with `pid = -1`). With signal handlers around, a blocking `waitpid` can be interrupted and return `-1` with `errno == EINTR` – retry in that case:

Waiting for one child and decoding the status

```c
int status;
pid_t done;
do {
    done = waitpid(child_pid, &status, 0);       /* 0: block until this child ends */
} while (done == -1 && errno == EINTR);          /* a signal handler interrupted the wait: retry */

if (WIFEXITED(status))
    printf("exited with code %d\n", WEXITSTATUS(status));
else if (WIFSIGNALED(status))
    printf("killed by signal %d\n", WTERMSIG(status));
```

`status` is not the exit code. It is an encoded number (Fig. 7), and printing it raw is confusing: `exit(1)` shows up as 256, `exit(-1)` as 65280. The exit code is only 8 bits, so `-1` becomes 255. Always decode with `WIFEXITED`/`WEXITSTATUS` and `WIFSIGNALED`/`WTERMSIG`.

> **Fig. 7** · On Linux, the exit code sits in bits 8–15 of the status, the number of a fatal signal in bits 0–6.
>
> *Diagram:* The status that wait and waitpid fill in is an encoded number. On Linux a normal exit puts the exit code in bits 8 to 15, and a fatal signal puts its number in the low 7 bits. exit(-1) therefore gives 0xFF00 = 65280: exit code 255, no signal. Decode it with the W macros, never by hand.

### Not waiting: WNOHANG

With the `WNOHANG` option `waitpid` never blocks. It returns the child's PID if that child has finished, `0` if it is still running, and `-1` on error. That is how a parent can check on its children while it keeps working:

```c
pid_t r = waitpid(child_pid, &status, WNOHANG);
if (r == 0)
    puts("still running");                       /* WNOHANG: don't wait, answer immediately */
else if (r == child_pid)
    puts("finished – status is valid");
else
    perror("waitpid");
```

### SIGCHLD

Whenever a child exits (or stops), the kernel sends `SIGCHLD` to the parent. The default action is to ignore it, which is why you never noticed. A parent that starts many children and does not want to sit in `waitpid` can reap them from a `SIGCHLD` handler (Fig. 8). `waitpid` is async-signal-safe, so it may be called there – in a loop, because one `SIGCHLD` can stand for several finished children:

> **Fig. 8** · A finished child is a zombie until waitpid collects it; a SIGCHLD handler can do that as soon as it happens.
>
> *Diagram:* When a child ends it becomes a zombie that keeps its exit status, and the kernel sends SIGCHLD to the parent (ignored by default). A SIGCHLD handler that calls waitpid(-1, …, WNOHANG) in a loop collects every finished child without ever blocking, and the zombies disappear.

Reaping children from a SIGCHLD handler

```c
void on_child(int sig)
{
    (void)sig;
    int saved = errno;                           /* waitpid may change errno – restore it */
    while (waitpid(-1, NULL, WNOHANG) > 0)       /* reap EVERY finished child, never block */
        ;
    errno = saved;
}

/* in main(), before the first fork(): */
sa.sa_handler = on_child;
sa.sa_flags = SA_RESTART;                        /* interrupted read/write calls continue */
sigaction(SIGCHLD, &sa, NULL);
```

The handler saves and restores `errno`, because main may be just about to read `errno` from a failed call. `SA_RESTART` makes interrupted `read()`/`write()` calls continue, so the rest of the program does not have to handle `EINTR` after every child that ends.

Good to know

**Mixing a SIGCHLD reaper with waitpid(pid)**

If the handler reaps every child, a later `waitpid(child_pid, …)` in main fails with `ECHILD` – the status is already gone. Pick one place that collects children. For the small programs of this course, a plain `waitpid` per child at the end of main is the simplest correct choice.

## Signals in the shell: trap

Shell scripts receive the same signals. `trap` sets what the script runs when one arrives – typically to delete temporary files before exiting. Use straight ASCII quotes: a command copied with typographic quotes (‘…’) from a document is a syntax error in the shell.

```shell
trap 'rm -f /tmp/work$$; exit' INT TERM   # clean up, then exit, on Ctrl+C or kill
trap 'echo Testing...' INT                 # just print, keep running
trap - INT                                 # back to the default action
```

`trap - INT` restores the default action (bash also accepts `trap INT`). Details and examples are in the [Computer Systems session on trap](https://laszlonemes.com/university/computer-systems/processes-filters#trap).

## Practice

Task 1

**Stop politely**

Write a program that prints a counter every second. On `SIGTERM` it must finish the current step, print how far it got and exit with code 0. `kill -9` should still end it immediately – check that you cannot change that.

**Approach:**

A `volatile sig_atomic_t` flag set by the handler, installed with `sigaction`. The loop checks the flag; all printing happens in main. `sleep()` returns early when a signal is handled.

```c
volatile sig_atomic_t stop = 0;
void on_term(int sig) { (void)sig; stop = 1; }

while (!stop) {
    printf("working %d\n", i++);
    sleep(1);                    /* a signal cuts the sleep short – the loop checks stop at once */
}
```

Task 2

**Wait for a ready child**

The parent forks one child. The child prints its PID, sends `SIGUSR1` to the parent and exits. The parent prints “child ready” only after the signal arrived, then waits for the child and prints its exit code with `WEXITSTATUS`. Then put a `sleep(1)` in the parent between `fork()` and the waiting, so the signal surely arrives early, and check that the parent still never hangs.

**Approach:**

Block `SIGUSR1` and install the handler before `fork()`; wait with a `sigsuspend` loop on the old mask; restore the mask afterwards.

```c
/* parent, before fork(): */
sigprocmask(SIG_BLOCK, &usr1_only, &old_mask);

/* parent, after fork(): */
while (!child_ready)
    sigsuspend(&old_mask);

/* child: */
kill(getppid(), SIGUSR1);
```

Task 3

**Three children, three signals**

Extend the handshake to three children. Children 1 and 2 report with `SIGUSR1` and `SIGUSR2`. What can child 3 use? Print which child is ready, in the order the signals arrive.

**Approach:**

There are only two user signals, but the real-time signals `SIGRTMIN` … `SIGRTMAX` are free to use and – unlike the classic ones – are queued, so repeated ones are not merged. Another option: a separate handler per signal, each setting its own flag. Either way, block all of them before forking.

Task 4

**Reap without waiting**

Start five children that sleep a random 1–5 seconds and exit. The parent keeps printing a dot every half second and, when all five have finished, prints how many it collected. No child may stay a zombie.

**Approach:**

Either poll with `waitpid(-1, &status, WNOHANG)` in the main loop, or reap in a `SIGCHLD` handler that only counts. Seed the random generator in each child differently (for example with its PID), otherwise all five sleep the same time.

```c
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
    reaped++;                    /* in the handler: count only, print later in main */
}
```

## Summary

- A signal is a numbered, data-less notification; it interrupts the process, runs the handler or the default action, and the process continues.
- Ctrl+C is `SIGINT`, Ctrl+\ is `SIGQUIT`, Ctrl+Z is `SIGTSTP`; Ctrl+D is end of input, not a signal. `SIGKILL` and `SIGSTOP` cannot be caught.
- Install handlers with `sigaction()` and check the return value; `signal()` behaves differently on different systems.
- In a handler: set a `volatile sig_atomic_t` flag or call `write()`. No `printf`, no `malloc`.
- A blocked signal is pending, not lost – but one pending bit per signal means identical signals can merge. Use different signals for different senders.
- Block before `fork()`, wait with a `sigsuspend` loop. `pause()` has a race.
- Collect every child with `waitpid`, retry on `EINTR`, decode with `WIFEXITED`/`WEXITSTATUS`; `WNOHANG` and `SIGCHLD` let you reap without blocking.
