Skip to content

Processes and Signals

Every running program in Linux is a process — an instance of an executing program with its own Virtual address space, file descriptors, and execution context. The kernel manages processes through A task_struct (in include/linux/sched.h), which tracks PID, state, scheduling priority, open Files, signal handlers, and more.

The POSIX process creation model consists of three operations:

sequenceDiagram
    participant Parent as Parent Process
    participant Kernel as Kernel
    participant Child as Child Process

    Parent->>Kernel: fork()
    Kernel-->>Parent: Returns child_pid
    Kernel-->>Child: Returns 0
    Note over Parent,Child: Both processes execute from here
    Child->>Kernel: exec("/bin/program")
    Note over Child: Child"s memory replaced with new program
    Parent->>Kernel: waitpid(child_pid)
    Kernel-->>Parent: Returns child exit status

fork creates a new process by duplicating the calling process. The child is an exact copy — same Code, same data, same open file descriptors, same signal dispositions. The only difference is the Return value (parent gets child PID, child gets 0).

pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(1);
} else if (pid == 0) {
// Child process
printf("Child PID: %d\n", getpid());
} else {
// Parent process
printf("Parent PID: %d, Child PID: %d\n", getpid(), pid);
}

Modern Linux uses Copy-on-Write (COW) pages for fork: the parent’s page tables are duplicated, But the physical pages are shared and marked read-only. When either process writes to a page, a copy Is made. This means fork is O(n) in page table size, not O(n) in memory.

execve replaces the current process’s memory image with a new program. The PID remains the same, But the code, data, heap, and stack are replaced. Open file descriptors with the close-on-exec flag Cleared remain open across exec.

// In the child process after fork:
char *argv[] = {"/bin/ls", "-la", "/tmp", NULL};
char *envp[] = {"PATH=/usr/bin:/bin", NULL};
execve("/bin/ls", argv, envp);
// If execve returns, it failed
perror("execve");
exit(1);

Variants of exec: execl``execlp``execle``execv``execvp``execvpe — they differ in how Arguments and environment are passed.

A parent must call wait (or waitpid) to collect the child’s exit status. If a child terminates And the parent does not wait, the child becomes a zombie (state Z) — it retains its PID and Exit status in the kernel’s process table until the parent waits.

int status;
pid_t pid = waitpid(child_pid, &status, 0);
if (WIFEXITED(status)) {
printf("Child exited with code %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child killed by signal %d\n", WTERMSIG(status));
} else if (WIFSTOPPED(status)) {
printf("Child stopped by signal %d\n", WSTOPSIG(status));
}
IdentifierField in task_structDescription
PIDpidProcess identifier (unique system-wide)
TIDpidThread identifier (same namespace as PID)
PPIDreal_parent->pidParent process ID
PGIDgroup_leader->pidProcess group ID (for signal delivery)
SIDsignal->leader->pidSession ID (for job control)
UID/EUIDreal_cred/credReal and effective user ID
GID/EGIDreal_cred/credReal and effective group ID

In Linux, threads are implemented as processes that share certain resources (address space, file Descriptors, signal handlers). Each thread has its own TID, but all threads in a process share the Same PID (the thread group leader’s TID). The clone(2) system call controls exactly what is Shared:

// Create a thread (shares address space, FDs, signal handlers)
clone(thread_fn, stack, CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, arg);
// fork is equivalent to:
clone(NULL, 0, SIGCHLD, 0);
Terminal window
# View process/thread IDs
ps -eLf # LWP (Lightweight Process) = TID
ps -T -p $PID # Show threads of a specific process
ls /proc/$PID/task/ # Each directory is a TID
# Process relationships
ps -eo pid,ppid,pgid,sid,comm
pstree # Tree view of process hierarchy

A Linux process can be in one of several states, tracked by the state field in task_struct:

stateDiagram-v2
    [*] --> TASK_NEW: fork/clone
    TASK_NEW --> TASK_RUNNING: sched_fork
    TASK_RUNNING --> TASK_RUNNING: preempted (still runnable)
    TASK_RUNNING --> TASK_INTERRUPTIBLE: wait for I/O, sleep
    TASK_RUNNING --> TASK_UNINTERRUPTIBLE: wait for disk I/O
    TASK_RUNNING --> TASK_DEAD: exit
    TASK_RUNNING --> TASK_STOPPED: SIGSTOP/SIGTSTP
    TASK_INTERRUPTIBLE --> TASK_RUNNING: I/O complete, signal
    TASK_UNINTERRUPTIBLE --> TASK_RUNNING: I/O complete
    TASK_STOPPED --> TASK_RUNNING: SIGCONT
    TASK_DEAD --> [*]: release resources
    TASK_DEAD --> ZOMBIE: parent has not waited
    ZOMBIE --> [*]: parent calls wait
StateCodeDescription
TASK_RUNNINGRRunnable (either executing or on a run queue)
TASK_INTERRUPTIBLESSleeping, waiting for an event (can be interrupted by signals)
TASK_UNINTERRUPTIBLEDSleeping, waiting for disk I/O (cannot be interrupted)
TASK_STOPPEDTStopped by SIGSTOP``SIGTSTPOr ptrace
TASK_TRACEDtStopped by debugger (ptrace)
EXIT_ZOMBIEZTerminated, parent has not called wait
EXIT_DEADXCompletely dead, waiting to be reaped
Terminal window
# View process states
ps -eo pid,stat,comm
# State codes in ps output:
# R running or runnable
# S interruptible sleep
# D uninterruptible sleep
# T stopped
# Z zombie
# I idle kernel thread
# + foreground process group
# s session leader
# l multi-threaded