How do Linux processes work — PID, PPID, fork(), exec(), and zombies?
Quick Answer
Every process gets a unique PID and tracks its parent's PPID. New processes are born via fork() (which clones the parent), then exec() swaps in a new program. Zombies are dead processes whose parent has not yet collected their exit status.
Detailed Answer
Creating a process in Linux is like a cell dividing in biology. The parent cell splits into two identical copies via fork(), and then one copy transforms itself into something completely different via exec(). The parent is responsible for acknowledging each child's death by calling wait(). If it does not, the dead child lingers as a zombie, a ghost entry in the system's process table. This two-step create-then-replace model is how every program launches on Linux.
Every process gets a unique Process ID (PID), a number assigned by the kernel from a counter that wraps around after hitting /proc/sys/kernel/pid_max (default 32768, up to about 4 million on 64-bit systems). Each process also records its Parent PID (PPID), forming a tree rooted at PID 1 (usually systemd). The fork() system call creates a child process that is a near-exact copy of the parent, including open files, signal handlers, memory layout, and environment variables. Modern Linux uses copy-on-write (COW), meaning parent and child share the same memory pages until one of them writes something, at which point only the changed page gets copied. After fork(), the child usually calls exec() (one of the execve family), which replaces the entire program in memory with a new one loaded from disk, while keeping the same PID and open files not marked with close-on-exec.
Inside the kernel, each process is represented by a task_struct, a large data structure holding the process state (running, sleeping, zombie, etc.), scheduling info, memory management details, file descriptor table, signal handlers, and credentials. When fork() is called, the kernel actually uses clone() internally with specific flags, allocates a new task_struct, copies or shares resources based on those flags, assigns a new PID, and places the child on the scheduler's run queue. When exec() is called, the kernel loads the new executable (typically an ELF binary), sets up fresh memory regions, maps the program and its shared libraries, and jumps to the new program's starting point.
In production, understanding this lifecycle is critical for debugging service failures. When a process exits, it enters the zombie state (shown as Z in ps output) until its parent calls wait() or waitpid() to collect the exit status. A zombie uses no CPU or memory but holds onto its PID and process table slot. A few zombies are harmless, but a parent that never reaps its children can pile up thousands of them, eventually using up all available PIDs and blocking new processes from starting. This often happens with badly written daemons or init scripts. The fix is to make the parent handle SIGCHLD properly. If the parent itself dies, init/systemd (PID 1) adopts the orphaned children and reaps them. In containers, PID 1 inside the container must handle zombie reaping, which is why lightweight init tools like tini or dumb-init are used in Docker containers.
A common trap is fork bombs, where a process recursively forks without limit, eating up all available PIDs and locking up the system. Production servers should set ulimit -u (max processes per user) to prevent this. Another subtle issue: fork() in a multithreaded program only copies the calling thread, which can leave mutexes in a broken state in the child. That is why it is safer to use posix_spawn() or to call exec() right after fork() in threaded programs. Also, you cannot kill zombie processes with SIGKILL because they are already dead. You have to fix or kill the parent process instead.