R0KernelPath
module 058 / 262

Processes & Signals in Linux

Processes are the kernel's atoms of execution — this module teaches you to see them, poke them, signal them, and understand why they sometimes refuse to die.

~8h focused study10 concepts

Overview

Every program you run becomes a process — a living, breathing instance of your code that the kernel breathes life into, schedules, isolates, and eventually reaps. Understanding processes is not just theoretical; it is the lens through which everything else in Linux makes sense: how your shell runs commands, how a web server handles thousands of clients, how Ctrl+C stops a runaway program, and why sometimes a process won't die no matter what you do.

This module takes you from the high-level view — what you see in top or htop — all the way down to the kernel data structures that make it real. You will learn to navigate /proc, understand the process tree, send and handle signals (the kernel's asynchronous notification system), and control scheduling priority with nice. Along the way you will encounter two of Linux's most evocative concepts: zombie processes (the undead) and orphan processes (the abandoned), both of which illuminate how Unix process lifecycle management actually works.

By the end of this module you will be comfortable reading a process's state from the command line, writing C programs that install custom signal handlers, deliberately creating and observing zombies and orphans in a controlled environment, and tuning process priority. These skills are the bedrock of systems programming, debugging, and — eventually — kernel work itself.

The beauty here is that Linux exposes almost everything through files. The /proc filesystem is a window directly into the kernel's process table, readable with nothing more than cat. There is no mystery, only structure waiting to be understood.

why this matters for the kernel

Before you can write a kernel patch, you need to think like the kernel. The kernel's primary job is managing processes: creating them, scheduling them, delivering signals, and cleaning them up. Every syscall you will ever study (fork, execve, waitpid, kill, clone) is about processes. When you read kernel source for the scheduler (kernel/sched/), the signal subsystem (kernel/signal.c), or process management (kernel/exit.c), you need a concrete mental model of what these data structures represent in the real world. This module builds that mental model from the outside in.

Signals are also one of the most common sources of subtle bugs in production systems — a mishandled SIGPIPE, an ignored SIGCHLD causing zombie accumulation, a SIGTERM handler that doesn't clean up resources before exit. Understanding signals at the level of how the kernel delivers them (by setting a flag in task_struct and checking it on the return-to-userspace path) lets you reason about race conditions in signal handlers that no amount of documentation reading will teach. This is the kind of knowledge that separates a kernel contributor from someone who merely writes code.

Process Lifecycle State Machine

rendering diagram…

The six main process states and the transitions between them. Notice that D-state has no escape via signals — only I/O completion can free it — and that Z-state persists until the parent calls waitpid().

The concepts

01What is a Process? The kernel's view

A process is an instance of a running program. But that definition glosses over what makes it real. The kernel represents every process as a task_struct — a large C structure in include/linux/sched.h that contains everything the kernel needs to know: the process ID (PID), the parent PID (PPID), the memory map, open file descriptors, signal masks, scheduling parameters, credentials (UID/GID), and much more.

An analogy: if your program's binary (the .elf file on disk) is a cookie-cutter, a process is the cookie — the same cutter can produce many cookies simultaneously, and each cookie has its own state (some half-eaten, some fresh).

Key attributes every process carries:

  • PID: unique numeric identity, assigned by the kernel, reused after the process dies.
  • PPID: the PID of the parent that created it via fork().
  • State: running, sleeping, zombie, stopped, etc. (see the State Machine concept below).
  • Virtual address space: each process believes it owns all of memory; the MMU enforces isolation.
  • File descriptor table: the process's open files, sockets, pipes — everything it can read/write.
  • Credentials: UID/GID determine what the process is allowed to do.

A thread in Linux is just a process that shares its address space and file descriptor table with another process (created with clone() instead of fork()). The kernel sees no fundamental difference — both are task_struct instances.

// Peek at your own PID and PPID from C
#include <stdio.h>
#include <unistd.h>

int main(void) {
    printf("PID:  %d\n", getpid());
    printf("PPID: %d\n", getppid());
    return 0;
}
// Compile: gcc -o whoami whoami.c && ./whoami
// Then run: echo "shell PID is $$" to compare

02ps, top, and htop — Reading the Process Table

ps, top, and htop are your windows into the kernel's process table. They work by reading /proc (more on that soon), but each presents the information differently.

ps (process snapshot):

  • ps aux — BSD style: all processes, all users, with CPU% and MEM%.
  • ps -ef — POSIX style: every process with full command line and PPID.
  • ps -o pid,ppid,stat,comm — custom columns. Useful for scripting.
  • The STAT column tells you the process state: R (running/runnable), S (interruptible sleep), D (uninterruptible sleep — usually I/O), Z (zombie), T (stopped), s (session leader), + (foreground).

top (live updating):

  • Press P to sort by CPU, M by memory, k to kill, r to renice.
  • load average: 0.5, 1.2, 0.8 — the 1-minute, 5-minute, 15-minute averages of the run queue length. A load of 1.0 on a single-core machine means fully loaded; on an 8-core machine, 8.0 is fully loaded.
  • The %us/%sy/%id/%wa breakdown in the CPU line: user time, kernel (system) time, idle, and I/O wait.

htop (interactive, color-coded):

  • Tree view (F5) shows the parent–child hierarchy visually.
  • Use F9 to send signals from inside htop.
  • Each CPU bar gives you a per-core view — critical for diagnosing single-threaded bottlenecks.

A subtlety worth knowing: ps reads a snapshot; top polls /proc every few seconds. Neither is perfectly atomic — the process table can change between reads. For observability in production, tools like perf, bpftrace, or pidstat (from sysstat) give more precision.

# Show all processes with their states, sorted by CPU
ps aux --sort=-%cpu | head -20

# Show process tree
ps -axjf

# Show only my processes, custom format
ps -u $USER -o pid,ppid,stat,%cpu,%mem,cmd

# One-liner: find a process by name
pgrep -l firefox

# top non-interactively: one snapshot, then exit
top -b -n 1 | head -30

03/proc/<pid> — The Kernel's Process Filesystem

/proc is a virtual filesystem — it has no files on disk. When you read /proc/1234/status, the kernel dynamically generates the text from the task_struct of PID 1234. Writing to certain files (like /proc/sys/kernel/...) adjusts kernel parameters live. This design — "everything is a file" — means you need no special tools: cat, grep, and echo are enough to inspect and tune the kernel.

Key files under /proc/<pid>/:

  • status — human-readable: Name, State, PID, PPID, UIDs, memory (VmRSS = resident set size in RAM, VmSize = virtual size).
  • stat — machine-readable version of the same; parsed by ps and top. Field 3 is the single-character state.
  • cmdline — the command line that started the process, null-byte separated. cat /proc/$$/cmdline | tr '\0' ' '
  • maps — the process's virtual memory map: every segment (text, data, heap, stack, shared libs) with start/end addresses, permissions, and the file it maps (if any).
  • fd/ — a directory of symbolic links, one per open file descriptor. ls -la /proc/$$/fd shows stdin (0), stdout (1), stderr (2), plus any open files.
  • environ — the environment variables, null-byte separated.
  • exe — a symlink to the executable binary.
  • wchan — the kernel function the process is sleeping in (if sleeping). Useful for diagnosing stuck processes.
  • oom_score — the OOM killer's current score for this process. Higher = killed first when memory runs out.

/proc/self/ is a magic symlink that always points to the current process, so cat /proc/self/status shows your own cat process's status.

Global files (not under a PID) worth knowing: /proc/cpuinfo, /proc/meminfo, /proc/loadavg, /proc/sys/ (tunables).

# Inspect the shell's own process
cat /proc/$$/status

# See every memory mapping of the current shell
cat /proc/$$/maps

# List open file descriptors
ls -la /proc/$$/fd

# Find what kernel function a sleeping process is waiting in
cat /proc/1/wchan

# Read the init process's command line cleanly
tr '\0' ' ' < /proc/1/cmdline; echo

04The Process Tree — fork, exec, and init

Every process (except PID 1) has a parent. This creates a tree rooted at PID 1. Understanding this tree is essential for understanding how signals propagate, how job control works, and why orphan processes get reparented.

fork() creates a child process that is an almost-exact copy of the parent: same code, same data (copy-on-write), same file descriptors, same signal masks. The only difference is the return value: fork() returns the child's PID to the parent, and 0 to the child. This is how your shell creates a new process to run a command.

exec() family (execve, execvp, etc.) replaces the current process's memory image with a new program. After exec, the process is still the same PID with the same file descriptors — but it is now running a completely different binary. Fork + exec is the Unix way of launching programs.

PID 1 (historically init, now usually systemd) is special: it is the ancestor of all other processes, it cannot be killed with SIGKILL (the kernel refuses), and it reaps orphans — when a process dies whose parent has already died, the kernel reparents it to PID 1, which calls waitpid() on it.

pstree visualizes this hierarchy beautifully. pstree -p shows PIDs. pstree -a shows command-line arguments.

// Classic fork + exec pattern
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork"); return 1;
    } else if (pid == 0) {
        // Child: replace ourselves with 'ls'
        execlp("ls", "ls", "-l", NULL);
        perror("exec"); // only reached if exec fails
        return 1;
    } else {
        // Parent: wait for child to finish
        int status;
        waitpid(pid, &status, 0);
        printf("Child exited with status %d\n",
               WEXITSTATUS(status));
    }
    return 0;
}

05Process States — The Lifecycle State Machine

A process is not just "running" or "not running". The kernel tracks a precise state for each task_struct. Understanding these states lets you diagnose stuck processes, I/O bottlenecks, and zombie accumulation.

R — Running / Runnable: Either currently executing on a CPU, or in the run queue waiting for a CPU. A high R count (visible in top's top line as running count) means CPU contention.

S — Interruptible Sleep: Waiting for an event (a keypress, a timer, data on a socket). Can be woken by a signal. This is the normal state for most processes most of the time — your shell sits here waiting for you to type.

D — Uninterruptible Sleep: Waiting for I/O in a way that cannot be interrupted, typically waiting for a disk read or an NFS call to complete. A process in D state cannot be killed — not even with SIGKILL. If you see many D state processes, you likely have an I/O bottleneck or an NFS server that's unreachable. The kernel uses this state to prevent data corruption from partial I/O.

Z — Zombie: The process has exited (its code is done, memory is freed) but its entry in the process table remains because its parent hasn't called waitpid() yet. It consumes no memory or CPU — only a PID and a table entry. A few zombies are normal; thousands indicate a buggy parent.

T — Stopped: Paused by SIGSTOP or SIGTSTP (Ctrl+Z). Resumes with SIGCONT.

t — Traced: Stopped by a debugger (ptrace). What gdb does to a process under a breakpoint.

I — Idle kernel thread: Kernel threads that are idle. Not relevant for user processes.

The transition diagram shows how these states connect. Notice that D has no signal escape — only the awaited I/O can free it.

# Count processes by state right now
ps -eo stat | grep -oE '^.' | sort | uniq -c | sort -rn

# Watch for D-state processes (stuck in I/O)
watch -n 1 'ps aux | awk "\$8 ~ /D/ {print}"'

# See the detailed state of a specific PID
grep -E '^(Name|State|Pid|PPid):' /proc/$$/status

06Signals — Asynchronous Notifications

A signal is an asynchronous notification sent to a process. Think of it as a software interrupt: at almost any point during execution, the kernel can tap a process on the shoulder and say "here's signal 11" — and the process must deal with it.

Signals have numbers (integers) and symbolic names. The most important ones:

SignalNumberDefault actionMeaning
SIGHUP1TerminateTerminal hangup / reload config (convention)
SIGINT2TerminateKeyboard interrupt (Ctrl+C)
SIGQUIT3Core dumpKeyboard quit (Ctrl+\)
SIGKILL9TerminateCannot be caught or ignored
SIGSEGV11Core dumpInvalid memory access
SIGPIPE13TerminateWrite to broken pipe
SIGALRM14TerminateTimer expired (alarm())
SIGTERM15TerminateGraceful termination request
SIGCHLD17IgnoreChild stopped or exited
SIGSTOP19StopCannot be caught or ignored
SIGTSTP20StopCtrl+Z (can be caught)
SIGCONT18ContinueResume a stopped process
SIGUSR1/210/12TerminateUser-defined; convention: log rotate, reload

How the kernel delivers a signal: When you call kill(pid, sig), the kernel sets a bit in the target process's task_struct (in pending.signal or shared_pending.signal). The signal is not delivered immediately. On the return path from a syscall or interrupt, the kernel checks if any signals are pending. If so, before returning to userspace, it delivers the signal: either invoking the registered handler, taking the default action, or discarding it (if SIG_IGN).

This delivery mechanism explains a crucial subtlety: a process in D state (uninterruptible sleep) will not receive signals — they stay pending until the process wakes up (or, for SIGKILL, until the I/O completes).

Three dispositions for each signal:

  1. Default (SIG_DFL): the kernel's built-in action (terminate, core dump, stop, ignore).
  2. Ignore (SIG_IGN): discard the signal silently.
  3. Custom handler: a function you register with signal() or sigaction().

SIGKILL and SIGSTOP can never be caught, ignored, or blocked — they are the kernel's override mechanism.

// Minimal: catch Ctrl+C gracefully
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

static volatile sig_atomic_t got_sigint = 0;

void handle_sigint(int sig) {
    got_sigint = 1;  // async-signal-safe: just set a flag
}

int main(void) {
    struct sigaction sa = {0};
    sa.sa_handler = handle_sigint;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGINT, &sa, NULL);  // prefer sigaction over signal()

    printf("Running. Press Ctrl+C to stop.\n");
    while (!got_sigint) {
        pause();  // sleep until any signal
    }
    printf("\nCaught SIGINT, shutting down cleanly.\n");
    return 0;
}

07kill, killall, pkill — Sending Signals

Despite its ominous name, kill does not necessarily kill anything — it sends a signal. By default it sends SIGTERM, which the target process can catch and handle gracefully.

kill <pid>          # sends SIGTERM
kill -9 <pid>       # sends SIGKILL (use as last resort)
kill -SIGSTOP <pid> # pause a process
kill -SIGCONT <pid> # resume it
kill -0 <pid>       # doesn't send anything; just checks if pid exists and you have permission

killall <name> sends a signal to all processes with that exact name. pkill <pattern> matches by pattern (supports regex), pgrep finds without killing.

Best practice for termination:

  1. Send SIGTERM first — give the process a chance to clean up.
  2. Wait a reasonable timeout (e.g., 5 seconds).
  3. If still alive, send SIGKILL.

This is exactly what systemctl stop <service> does: SIGTERM, wait TimeoutStopSec seconds, then SIGKILL.

From C, kill(pid, sig) is the syscall. raise(sig) sends a signal to yourself. killpg(pgid, sig) sends to an entire process group — this is how kill %1 in your shell kills the whole pipeline.

Permissions: You can only send signals to processes you own (same UID), unless you are root. Exception: SIGCONT can always be sent to processes in the same session.

# Graceful stop pattern in a shell script
stop_process() {
    local pid=$1
    kill -SIGTERM "$pid" 2>/dev/null
    # wait up to 5 seconds
    for i in $(seq 1 10); do
        kill -0 "$pid" 2>/dev/null || return 0  # gone
        sleep 0.5
    done
    echo "SIGTERM ignored, escalating to SIGKILL"
    kill -SIGKILL "$pid" 2>/dev/null
}

# Find and signal a process by name pattern
pkill -SIGUSR1 -f 'myapp --worker'

# Check if a PID is alive without sending a signal
if kill -0 "$pid" 2>/dev/null; then
    echo "Process $pid is alive"
fi

08Signal Handlers and async-signal safety

Writing a correct signal handler is harder than it looks. The handler can fire at any point in your program's execution — between any two machine instructions. This creates a re-entrancy problem: if your handler calls malloc() and your main code was in the middle of malloc() when the signal fired, you have two threads of execution fighting over the same heap lock — a deadlock.

The POSIX standard defines a list of async-signal-safe functions: functions that can safely be called from a signal handler. This list is shorter than you'd expect. It includes: _exit(), write(), read(), open(), close(), kill(), signal(), sigaction(), sleep(), getpid(), and a handful of others. It does not include printf(), malloc(), free(), exit(), or any stdio function.

The canonical pattern: Do the minimum in the handler — set a volatile sig_atomic_t flag. Check the flag in your main loop and do the real work there.

sigaction() vs signal(): Always prefer sigaction(). The old signal() has undefined behavior on whether the signal disposition resets to SIG_DFL after delivery (it does on some systems, it doesn't on others). sigaction() is explicit and portable.

Signal masks: You can temporarily block signals from being delivered using sigprocmask(). Blocked signals stay pending — they are delivered when you unblock them. Useful for critical sections. sigfillset/sigemptyset/sigaddset manipulate signal sets.

SA_RESTART flag: By default, a slow syscall (like read() blocking on stdin) interrupted by a signal returns -1 with errno = EINTR. Setting SA_RESTART in sigaction() tells the kernel to automatically restart the syscall after the handler returns — convenient for most use cases.

// Proper SIGTERM handler for a long-running service
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

static volatile sig_atomic_t running = 1;

void handle_term(int sig) {
    // async-signal-safe: just set flag
    running = 0;
}

int main(void) {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handle_term;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;  // restart interrupted syscalls
    sigaction(SIGTERM, &sa, NULL);
    sigaction(SIGINT,  &sa, NULL);

    while (running) {
        // do work...
        sleep(1);
    }

    // safe to call non-async-signal-safe functions here
    printf("Caught shutdown signal, cleaning up...\n");
    // flush buffers, close DB connections, etc.
    return 0;
}

09Zombies and Orphans

These are two of the most evocative concepts in Unix, and both arise from the parent–child relationship.

Zombie processes (Z state): When a child process exits, it does not vanish immediately. The kernel keeps its entry in the process table (holding exit status, resource usage, PID) so that the parent can call waitpid() to retrieve this information. Until waitpid() is called, the child is a zombie: it consumes no CPU or memory, but it occupies a PID and a process table slot.

A few zombies are completely normal — they appear briefly between a child exiting and the parent calling waitpid(). The problem is when a parent never calls waitpid(). If a buggy server forks children for each request but never reaps them, PIDs accumulate until the system exhausts its PID namespace (~4 million on a 64-bit Linux). To fix zombie accumulation: fix the parent to install a SIGCHLD handler that calls waitpid(-1, &status, WNOHANG) in a loop, or set SIGCHLD to SIG_IGN to tell the kernel to auto-reap children.

Orphan processes: When a parent process dies before its child, the child becomes an orphan. The kernel immediately reparents it to PID 1 (init/systemd), which continuously calls wait() and reaps all orphans. Orphans are completely harmless — they just get a new parent.

A common pattern: a server forks a background worker, then calls _exit() in the parent so the worker becomes an orphan reparented to init. This is the classic Unix double-fork daemonization pattern.

The crucial difference:

  • Zombie: child is dead, parent is alive but not calling waitpid().
  • Orphan: child is alive, parent is dead.
// Deliberately create a zombie to observe it
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(void) {
    pid_t child = fork();
    if (child == 0) {
        // Child exits immediately
        printf("Child (PID %d) exiting\n", getpid());
        return 0;
    }
    // Parent sleeps WITHOUT calling waitpid -> zombie!
    printf("Parent (PID %d): child is %d\n", getpid(), child);
    printf("Run: ps aux | grep %d\n", child);
    printf("You will see state 'Z' (zombie)\n");
    sleep(30);  // observe the zombie during this window
    // When parent exits, init reaps the zombie
    return 0;
}

10nice, renice, and Scheduling Priority

The Linux scheduler decides which process runs on which CPU and for how long. While the full CFS (Completely Fair Scheduler) is complex, understanding nice values gives you direct, practical control over relative CPU priority.

Nice value: An integer from -20 (highest priority) to +19 (lowest priority). The default is 0. "Nice" as in "being nice to other processes" — a high nice value means you're willing to let others go first.

  • nice -n 10 ./my_script — start a process with nice=10 (lower priority).
  • renice -n -5 -p 1234 — change an already-running process's nice value (requires root to decrease below 0).
  • nice with no arguments prints the current shell's nice value.

Why you'd use it:

  • Batch jobs (backups, video encoding): nice -n 19 so they don't interfere with interactive use.
  • High-priority daemon: nice -n -10 (requires root) to ensure responsiveness.
  • In your shell, nice is the tool; in C, setpriority(PRIO_PROCESS, 0, 10) does the same.

Real-time priorities: Beyond nice, Linux has SCHED_FIFO and SCHED_RR scheduling policies for real-time tasks (audio servers, industrial control). These use a separate priority range (1–99) and preempt all normal tasks. Set with chrt. This is advanced territory — misuse can make your system unresponsive.

CPU affinity: taskset -c 0,1 ./my_program pins a process to specific CPU cores. Useful for benchmarking (reduces cache effects) and real-time workloads. In C, use sched_setaffinity().

OOM score: When the system runs out of memory, the OOM killer selects a victim based on /proc/<pid>/oom_score. You can adjust a process's susceptibility with echo 1000 > /proc/<pid>/oom_score_adj (range -1000 to 1000; -1000 disables OOM killing for that process).

# Run a CPU-intensive task at lowest priority
nice -n 19 gzip -9 huge_file.tar

# Change priority of a running process
sudo renice -n -5 -p $(pgrep myapp)

# See the nice value of all your processes
ps -eo pid,ni,comm | sort -k2 -n | head -20

# From C: lower our own priority
#include <sys/resource.h>
// int setpriority(PRIO_PROCESS, 0, 10);

# Pin a process to cores 0 and 1
taskset -c 0,1 ./my_benchmark

# Show scheduling policy and priority
chrt -p $(pgrep myapp)

Key insights

  • ✦Every process is a `task_struct` in the kernel — ps, top, and /proc are just different views of the same in-kernel data structure.
  • ✦fork() copies, exec() replaces: these two syscalls together are how every new program launch works in Unix, from your shell to systemd.
  • ✦SIGKILL and SIGSTOP cannot be caught, ignored, or blocked — they are the kernel's hard override, not a polite request.
  • ✦A zombie consumes nothing but a PID; the real cost is a buggy parent that never calls waitpid(), potentially exhausting the PID namespace.
  • ✦D-state (uninterruptible sleep) processes cannot be killed because the kernel must protect ongoing I/O from interruption — if you see them, diagnose the I/O subsystem, not the process.
  • ✦Signal handlers must be async-signal-safe: set a flag, do real work in the main loop. Calling printf() from a signal handler is a latent bug.
  • ✦The /proc virtual filesystem exposes the kernel's live process table as readable files — no special tools needed, just cat and grep.
  • ✦Nice values are relative: setting one process to nice=19 only helps if competing processes are at nice=0. On an idle system it makes no visible difference.

Common pitfalls

  • ⚠Calling non-async-signal-safe functions (printf, malloc, exit) from signal handlers: causes deadlocks under just the wrong timing. The fix is to set a flag in the handler and do real work in the main loop.
  • ⚠Using signal() instead of sigaction(): signal() has unspecified re-arming behavior on some platforms. Always use sigaction() in new code.
  • ⚠Sending SIGKILL as a first resort: many processes need SIGTERM first to flush buffers, close database connections, or release locks. Blind SIGKILL can leave corrupted state.
  • ⚠Assuming a zombie is harmful: a momentary zombie is normal. The problem is only when they accumulate (missing waitpid calls in the parent).
  • ⚠Confusing orphan (harmless, reparented to init) with zombie (parent alive but not calling waitpid). They require completely different fixes.
  • ⚠Ignoring SIGCHLD without calling waitpid: POSIX says setting SIGCHLD to SIG_IGN causes children to be auto-reaped, but on some older systems this has surprising behavior. Explicitly calling waitpid in a loop is cleaner.
  • ⚠Assuming kill -9 always works immediately: a process in D state (uninterruptible sleep) will not receive SIGKILL until the blocking I/O completes. No signal can break D state.
  • ⚠Forgetting that nice values require root to decrease: any user can be nicer (increase nice value), but only root can be meaner (decrease nice value below the current value).

Practice

  1. 01
    easy

    Write a C program that forks three children. Each child should print its PID, sleep for a random number of seconds (1-5), then exit with a unique exit code. The parent must wait for all three children and print each one's PID and exit code as they finish. Verify your output by also running `pstree -p <parent_pid>` while the children are sleeping.

  2. 02
    medium

    Build a minimal process monitor in C (or shell) that watches a given PID and prints a message when it exits. Then extend it to restart the process automatically if it exits with a non-zero status. This is a baby version of a process supervisor like s6 or daemontools.

  3. 03
    medium

    Write a C program that registers a SIGUSR1 handler. Each time SIGUSR1 is received, it increments a counter. The main loop prints the counter every second. Test it by running the program and then repeatedly sending `kill -SIGUSR1 <pid>` from another terminal. Verify the counter increments correctly. Then investigate: what happens if you send SIGUSR1 very rapidly (use a shell loop) — do any signals get lost? Why?

  4. 04
    easy

    Explore /proc deeply: write a shell script that, given a PID as argument, prints a formatted report showing: the command line, the process state, the parent PID, the number of open file descriptors, the resident memory (VmRSS), and the kernel function it is sleeping in (wchan). Run it on PID 1, your shell, and a running `sleep 999`.

  5. 05
    hard

    Implement the double-fork daemonization pattern in C: fork once, have the parent exit (making the child an orphan reparented to init), then fork again in the child (to guarantee it cannot acquire a controlling terminal), close stdin/stdout/stderr, and redirect them to /dev/null. The resulting daemon should write its PID to /tmp/mydaemon.pid and then loop writing a heartbeat to /tmp/mydaemon.log every 5 seconds. Verify it appears under PID 1 in pstree and survives terminal close.

Resources

Checkpoints — you can now…

  • ✓You can run ps aux, identify every column, and explain what the STAT field values (R, S, D, Z, T) mean for a process.
  • ✓You can navigate /proc/<pid>/ and extract the command line, memory usage, open file descriptors, and sleep location of any running process using only shell tools.
  • ✓You can write a C program that forks a child, has the parent wait for it with waitpid, and correctly handles the child's exit status.
  • ✓You can write a signal handler in C that is async-signal-safe, explain why printf() is not safe to call there, and describe the canonical flag-setting pattern.
  • ✓You can explain the difference between a zombie and an orphan, produce both deliberately in a C program, and describe how each is resolved.
  • ✓You can send SIGTERM and SIGKILL from the command line, explain why you should try SIGTERM first, and describe why SIGKILL cannot kill a D-state process.
  • ✓You can use nice and renice to adjust process priority and explain the range, default value, and permission requirements.
  • ✓You can draw (from memory or describe precisely) the process state machine with all six states and the signal/event transitions between them.