Linux System Programming · intermediate · ~10 min

kill() — sending a signal to another process

- By the end you can call `kill(pid, signo)` to send a signal to a process you own, checking its return value and `errno`. - By the end you can use the special signal `0` to probe whether a pid is still alive and signalable without disturbing it. - By the end you can explain the uid-based permission rules that decide who is allowed to signal whom. - By the end you can avoid the pid-reuse race by only signalling children you spawned and have not yet reaped. - By the end you can wire up a parent that signals a cooperating child, and a child that handles the signal cleanly and exits.

Overview

In the previous lesson you used raise(sig), which delivers a signal to your own process — raise(sig) is essentially kill(getpid(), sig). This lesson generalises that idea: kill() lets one process send a signal to a different process, named by its process ID (pid). That single step — targeting someone else instead of yourself — is what turns signals from a self-interrupt into a genuine inter-process communication (IPC) mechanism.

Despite the alarming name, kill() is not primarily about terminating processes. It is a general-purpose way to poke another process: ask it to reload config, tell it to shut down gracefully, wake it from a pause(), or merely check that it still exists. Because kill() reaches across process boundaries, the kernel enforces permission rules, and pids can be reused — so using it safely is as much about who and which pid as it is about which signal.

Why it matters

Every service-management tool you have ever used is built on kill(): systemctl reload nginx sends SIGHUP, docker stop sends SIGTERM then SIGKILL, and Ctrl-C in your shell ultimately becomes signals delivered to a process group. Getting the details right matters for correctness (a missed SIGCHLD reap leaks zombies) and for security (sending a signal to the wrong pid after reuse can disrupt an unrelated process, and unchecked pid handling has caused real denial-of-service bugs). Knowing the uid permission model also tells you exactly why an attacker in a low-privilege account cannot signal a root daemon — a boundary you rely on when hardening a system.

Core concepts

1. What kill() actually does

kill(pid, signo) asks the kernel to post signal number signo to the process identified by pid. It returns 0 on success and -1 on failure (setting errno). It does not wait for the signal to be handled — delivery is asynchronous. Once posted, the target's disposition for that signal decides what happens: run a handler, take the default action (often terminate), or stay pending if the signal is blocked.

   Process A (sender)                 Process B (target, pid=B)
   ------------------                 -------------------------
   kill(B, SIGUSR1)  ── kernel ──▶    pending signal set: {SIGUSR1}
        │                                     │
   returns 0 immediately            at next scheduling point:
   (does NOT block)                  run handler / default action

2. Choosing the signal — and the meaning of pid values

The signo you pass decides intent. A few you will use constantly:

Signal Number* Default action Typical kill() use
SIGTERM 15 Terminate Polite "please shut down" — the default kill command signal
SIGKILL 9 Terminate (uncatchable) Force-kill; cannot be caught, blocked, or ignored
SIGHUP 1 Terminate Conventionally "reload configuration" for daemons
SIGUSR1/SIGUSR2 10/12 Terminate Application-defined custom actions
SIGSTOP/SIGCONT 19/18 Stop / continue Pause and resume a process
0 — (none) Existence/permission probe only — sends nothing

*Numbers are Linux/x86 values; always use the names, not the integers.

The pid argument is also overloaded, and this trips people up:

pid value Meaning
> 0 Send to that specific process
0 Send to every process in the caller's process group
-1 Send to every process you may signal (broadcast)
< -1 Send to the process group whose id is -pid

In this lesson we stick to pid > 0 (one specific child) — the safe, predictable case.

3. Signal 0: the probe that sends nothing

kill(pid, 0) performs all the permission and existence checks but delivers no signal. It is the idiomatic "is this pid still there and am I allowed to signal it?" test:

  • returns 0 → the process exists and you could signal it;
  • fails with errno == ESRCH → no such process;
  • fails with errno == EPERM → it exists but you lack permission.

Knowledge check: After you waitpid() a child and reap it, what does kill(child_pid, 0) return, and what is errno?

AnswerIt returns `-1` with `errno == ESRCH` ("no such process"). Reaping removes the zombie entry, so the pid no longer names a live process. This is exactly what the demo program shows in its final line.

4. Who may signal whom (the permission model)

The kernel lets you signal a target only if you are privileged (root / CAP_KILL) or your real or effective uid matches the target's real or saved-set uid. Otherwise kill() fails with EPERM.

  caller uid == target real uid  ─┐
  caller uid == target saved uid ─┼─▶  allowed
  caller is root / CAP_KILL       ─┘
  otherwise                        ──▶  EPERM

Practical consequence: an ordinary user can signal only their own processes, never another user's, and certainly not a root-owned daemon. SIGCONT is a minor special case (it can be sent to processes in the same session) but you can ignore that nuance for now. This boundary is a load-bearing part of Linux's isolation between accounts.

5. The pid-reuse race — the subtle safety hazard

Pids are recycled. When a process exits and is reaped, its pid becomes free and the kernel may hand it to a brand-new, unrelated process. So this sequence is a bug:

  t0  child pid=5000 doing work
  t1  child exits, you reap it            (pid 5000 now free)
  t2  unrelated program starts  ──▶ gets pid 5000
  t3  you call kill(5000, SIGTERM)  ──▶ hits the WRONG process!

Defence: only signal a pid you spawned yourself and have not yet reaped, and treat the pid as invalid the instant waitpid() returns for it. For cooperating parent/child code this is easy — the child cannot be reaped behind your back, so between fork() and waitpid() the pid unambiguously refers to your child. (Modern Linux offers pidfd_open()/pidfd_send_signal() to eliminate the race entirely; that is beyond this lesson but worth knowing exists.)

6. Ethics and scope

This lesson is about signalling cooperating processes: a child you fork()ed, a daemon you wrote, or a test fixture you control. Do not scan for arbitrary pids on a shared machine and signal them — even where the kernel would permit it, disrupting other users' work is not what this teaches. Every example here is self-contained and local.

Syntax notes

#include <signal.h>
#include <sys/types.h>   /* pid_t */

int kill(pid_t pid, int signo);
//   pid   > 0  : one process; 0 : caller's group; -1 : broadcast; < -1 : group -pid
//   signo      : signal number (use names, e.g. SIGTERM); 0 = probe only, sends nothing
//   returns 0 on success, -1 on error (errno set)
//   errno: ESRCH  no such process
//          EPERM  process exists but you may not signal it
//          EINVAL invalid signal number

Installing a handler in the target (so a caught signal does something useful):

#include <signal.h>
#include <string.h>

void handler(int signo);

struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = handler;   /* function run on delivery */
sigemptyset(&sa.sa_mask);  /* extra signals to block during the handler */
sa.sa_flags = SA_RESTART;  /* auto-restart interruptible syscalls */
sigaction(SIGUSR1, &sa, NULL);   /* returns 0 / -1, errno set on failure */

Related calls you will pair with kill():

pid_t fork(void);                     /* 0 in child, child-pid in parent, -1 on error */
int   pause(void);                    /* sleep until any signal arrives; returns -1/EINTR */
pid_t waitpid(pid_t pid, int *status, int options);  /* reap child, prevents zombie */
// Inspect status with WIFEXITED(status) and WEXITSTATUS(status).

Inside a handler you may only touch volatile sig_atomic_t variables and call async-signal-safe functions (write, _exit, kill, …) — never printf.

Lesson

What kill() really does

kill(pid, signo) sends a signal to another process. Despite its name, it is not only about terminating processes. It is a general-purpose way to signal them.

Common uses:

  • SIGUSR1 — tell a process to do something custom, such as reload its configuration.
  • SIGTERM — politely ask a process to exit.
  • Signal 0 — send no signal at all, just probe whether the pid still exists. If the process is gone, kill() fails with ESRCH.

Who is allowed to signal whom

On Linux, you may signal a process if either of these is true:

  • You are root.
  • Your real or effective uid matches the target's real or saved uid.

uid = user ID, the number the system uses to identify the account a process belongs to.

The practical result: a normal user can only signal their own processes, never another user's.

An ethical note

This lesson is about signalling cooperating processes:

  • a child you spawned,
  • a daemon you wrote,
  • a test fixture you control.

Do not go hunting for arbitrary pids on a shared machine.

Code examples

#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>

/* Set by the SIGUSR1 handler in the child. volatile sig_atomic_t is the
   only kind of variable a handler may touch safely. */
static volatile sig_atomic_t got_usr1 = 0;

static void on_usr1(int signo) {
    (void)signo;
    got_usr1 = 1;
    /* write() is async-signal-safe; printf() is NOT. */
    const char msg[] = "  [child] handler ran: caught SIGUSR1\n";
    write(STDOUT_FILENO, msg, sizeof(msg) - 1);
}

int main(void) {
    pid_t child = fork();
    if (child < 0) {
        perror("fork");
        return 1;
    }

    if (child == 0) {
        /* ---- CHILD ---- */
        struct sigaction sa;
        memset(&sa, 0, sizeof sa);
        sa.sa_handler = on_usr1;
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = SA_RESTART;
        if (sigaction(SIGUSR1, &sa, NULL) < 0) {
            perror("sigaction");
            _exit(1);
        }
        /* Wait for a signal to arrive. pause() returns after the handler. */
        while (!got_usr1)
            pause();
        _exit(42);   /* distinctive exit code the parent can verify */
    }

    /* ---- PARENT ---- */
    printf("[parent] spawned child pid=%d\n", (int)child);

    /* Probe with signal 0: no signal sent, just an existence/permission check. */
    if (kill(child, 0) == 0)
        printf("[parent] kill(pid,0): child is alive and signalable\n");

    sleep(1);  /* give the child time to install its handler */

    printf("[parent] sending SIGUSR1 to child\n");
    if (kill(child, SIGUSR1) < 0) {
        perror("kill");
        return 1;
    }

    int status = 0;
    waitpid(child, &status, 0);   /* reap the child, avoid a zombie */
    if (WIFEXITED(status))
        printf("[parent] child exited with code %d\n", WEXITSTATUS(status));

    /* Now the pid is reaped. Probing it should fail with ESRCH. */
    if (kill(child, 0) < 0)
        printf("[parent] kill(pid,0) now fails: %s (ESRCH=%d)\n",
               strerror(errno), errno == ESRCH);

    return 0;
}

Line by line

  • static volatile sig_atomic_t got_usr1 — the flag the handler flips. volatile stops the compiler caching it across the while loop; sig_atomic_t guarantees reads/writes are indivisible with respect to signal delivery. Ordinary int is not safe here.
  • on_usr1() — the handler. It sets the flag and writes a message using write(), which is async-signal-safe. Using printf() here would be undefined behaviour because a signal can interrupt the program mid-printf, then re-enter it.
  • fork() — splits into two processes. It returns 0 in the child and the child's pid in the parent; the two branches below are the two processes running the same code.
  • Child branch — sigaction(SIGUSR1, ...) — installs on_usr1 so an incoming SIGUSR1 runs the handler instead of taking its default action (terminate). SA_RESTART asks the kernel to resume interrupted syscalls automatically.
  • while (!got_usr1) pause(); — the child sleeps until a signal arrives. pause() returns after the handler runs; the loop guards against spurious wakeups and re-checks the flag. Then the child _exit(42)s with a code the parent will verify.
  • Parent — first kill(child, 0) — the existence probe. It sends nothing but confirms the child exists and is signalable, printing the alive message.
  • sleep(1) — a simple race-avoider so the child has installed its handler before the signal arrives. (A pipe or sigsuspend handshake would be more robust in production.)
  • kill(child, SIGUSR1) — the real point of the lesson: the parent signals the child by pid. Its return value is checked; perror reports any failure.
  • waitpid(child, &status, 0) — reaps the child so it does not linger as a zombie, and captures its exit status. WIFEXITED/WEXITSTATUS confirm it exited with 42.
  • Final kill(child, 0) — now that the child is reaped, the same probe fails with ESRCH, demonstrating that the pid no longer names a live process (and why reusing it later would be dangerous).

Common mistakes

1. Treating kill(pid, 0) as if it terminates something.

kill(pid, 0);   /* WRONG if you expected the process to die */

Why it breaks: signal 0 sends nothing; it is only a probe. The process keeps running and you may wrongly assume it is gone.

kill(pid, SIGTERM);   /* actually ask it to terminate */

2. Ignoring the return value / errno.

kill(pid, SIGTERM);   /* did it even work? who knows */

Why it breaks: on EPERM (not yours) or ESRCH (already gone) the signal was never delivered, and you silently carry on with a false assumption.

if (kill(pid, SIGTERM) < 0)
    perror("kill");   /* now you know: ESRCH? EPERM? */

3. Signalling a pid after reaping it (pid-reuse race).

waitpid(child, &status, 0);
kill(child, SIGTERM);   /* WRONG: pid may now belong to someone else */

Why it breaks: once reaped, the pid can be recycled for an unrelated process, which you then disrupt.

kill(child, SIGTERM);          /* signal BEFORE reaping */
waitpid(child, &status, 0);    /* then reap; treat pid as dead afterwards */

4. Doing real work (like printf) inside the handler.

void on_usr1(int s){ printf("got %d\n", s); }   /* WRONG: not async-signal-safe */

Why it breaks: if the signal interrupts the program inside printf, re-entering it is undefined behaviour (deadlock or corruption).

void on_usr1(int s){ (void)s; got_usr1 = 1; }   /* set a flag; act in main() */

Debugging tips

  • strace -f -e trace=kill,rt_sigaction,wait4 ./prog (Linux) shows the exact kill() calls, their arguments, and the return/errno — the fastest way to confirm a signal was actually sent and to whom. -f follows the forked child.
  • Print the pid you are about to signal and eyeball it against ps -ef | grep prog. A wrong or stale pid is the number-one cause of "my signal did nothing."
  • Check errno explicitly. ESRCH means the target is gone (often the reuse/reaping bug); EPERM means it exists but you are not allowed (uid mismatch); EINVAL means a bad signal number.
  • Zombies left behind? ps showing <defunct> means you signalled/spawned but never waitpid()ed. Add the reap.
  • Handler never fires? Confirm the disposition is installed before the signal can arrive (the sleep(1) in the demo), and that the signal is not blocked in the target's signal mask (sigprocmask).
  • gdb: run under gdb with handle SIGUSR1 nostop pass so gdb forwards the signal to the program instead of trapping it, letting you breakpoint inside the handler.

Memory safety

The hazards here are less about the heap and more about concurrency and async delivery:

  • Handlers run asynchronously. A signal can interrupt your main at any instruction. Only touch volatile sig_atomic_t variables from a handler; anything else risks a torn read/write or reordering. Non-atomic flags or shared structs can be seen half-updated.
  • Async-signal-safety. Inside a handler call only functions on the async-signal-safe list (write, _exit, kill, sigaction, …). Calling printf, malloc, or anything using a lock can deadlock or corrupt state if the signal interrupted that same function.
  • The pid-reuse race is a use-after-free at the OS level. A reaped pid is like a freed pointer: reusing it silently targets a different "object" (process). Never signal a pid after waitpid() returns for it.
  • SIGKILL/SIGSTOP cannot be caught — no handler, no cleanup. A process killed with SIGKILL cannot flush buffers or free resources, so prefer SIGTERM first and reserve SIGKILL for the unresponsive case.
  • fork() copies memory but not everything cleanly. After fork(), avoid stdio buffering surprises by using _exit() (not exit()) in the child once you have done raw I/O, so you do not double-flush buffers inherited from the parent.

Real-world uses

  • Service managers: systemd, init scripts, and supervisors send SIGTERM to ask a service to stop and escalate to SIGKILL after a grace period — exactly the pattern behind systemctl stop and docker stop.
  • Config reload without downtime: long-running daemons (nginx, sshd, many custom servers) catch SIGHUP to re-read their configuration in place. kill -HUP <pid> is the classic operator move.
  • Graceful shutdown of your own servers: trap SIGTERM, stop accepting new work, drain in-flight requests, then exit — so orchestrators can roll pods safely.
  • Liveness checks: monitoring code uses kill(pid, 0) to test whether a supervised process is still alive without disturbing it.
  • Custom coordination: SIGUSR1/SIGUSR2 are yours to define — e.g. "rotate the log file now" or "dump internal stats."

Best practice: always send SIGTERM before SIGKILL; check the return value; track child pids and reap them; and where the reuse race matters (long-lived supervisors), use pidfd_send_signal() on modern Linux to bind the signal to a specific process, not a recyclable number.

Practice tasks

  1. Write a program that forks a child which sleeps in pause(), then have the parent print the result of kill(child, 0) before and after sending SIGTERM and reaping — observe the ESRCH transition.

  2. Extend the demo so the child handles both SIGUSR1 (print "reload" via write) and SIGUSR2 (set a flag to exit), and have the parent send SIGUSR1 three times before finally sending SIGUSR2.

  3. Deliberately trigger EPERM: from a normal account, attempt kill(1, 0) (pid 1 is root-owned) and print strerror(errno) — explain in a comment why it fails.

  4. Implement a SIGTERM-then-SIGKILL graceful-stop helper: send SIGTERM, waitpid with a timeout loop using kill(pid,0), and escalate to SIGKILL only if the child is still alive after ~2 seconds.

  5. Demonstrate the pid-reuse hazard safely: fork a child, reap it, record its pid, then fork many short-lived children and check with kill(oldpid,0) whether the old pid ever gets reused — report if/when it happens.

Summary

  • kill(pid, signo) posts a signal to another process by pid; it is general-purpose IPC, not just termination, and it returns immediately (delivery is asynchronous).
  • Always check the return value: ESRCH = no such process, EPERM = exists but not yours, EINVAL = bad signal.
  • Signal 0 sends nothing — it is the idiomatic existence/permission probe.
  • The pid argument is overloaded (>0 one process, 0 your group, -1 broadcast, <-1 a group); stick to pid > 0 for predictable code.
  • You may signal a process only if you are root or your uid matches its real/saved uid — the boundary that keeps users from signalling each other's (and root's) processes.
  • Beware pid reuse: a reaped pid is like a freed pointer. Signal a child before reaping it and treat the pid as dead afterward.
  • Keep handlers tiny: only volatile sig_atomic_t and async-signal-safe calls; do the real work back in main.

Practice with these exercises