Loading learning content...
Every running operating system contains a complete genealogy of its processes—a hierarchical structure known as the process tree. This tree is not a mere visualization convenience; it is a living data structure that the kernel maintains and uses for process management, signal delivery, resource accounting, and termination cascades.
When you view the process tree on a running system, you are observing the cumulative history of process creation since system boot. Each node represents a process that was created by its parent. Each edge represents a creation event—a fork() call that brought a new process into existence. The root of the tree is always the init process, the primordial ancestor from which all user processes descend.
By the end of this page, you will understand how process trees are constructed and maintained, how to visualize and traverse them, how the tree structure influences process behavior, and how operating systems use trees for essential management tasks like signal propagation and cascading termination.
The process tree exhibits classical tree properties from computer science, with every node (except the root) having exactly one parent, and nodes potentially having zero or more children.
A process tree is a rooted tree T = (V, E) where:
| Property | Definition | Process Tree Interpretation |
|---|---|---|
| Depth | Distance from root | Number of fork() calls from init to reach this process |
| Height | Maximum depth of any node | Deepest process nesting in the system |
| Leaf | Node with no children | Process that has never called fork(), or whose children all terminated |
| Internal Node | Node with children | Process that has created at least one child still running |
| Ancestor | Any node on path to root | Any process in the creation chain above |
| Descendant | Any node reachable below | Any process transitively created through fork() |
| Subtree | Node and all descendants | A process and all its descendants |
Root (init/systemd): The singular ancestor of all user processes. It exists before any user process and persists until system shutdown.
Service Daemons: Processes like sshd, cron, and dockerd are children of init. They are long-lived processes that spawn children as needed.
Interactive Shells: The bash processes are created either through local login or SSH sessions. They become parents of user-invoked commands.
Worker Processes: The python process might be a web application server that forks worker processes to handle requests.
Container Processes: Docker containers appear as child processes of the Docker daemon, but may have their own internal process trees.
Technically, a Unix system has a tree rooted at PID 1. However, kernel threads (PID 2 and its children in Linux) form a separate structure. Some tools display this as a forest (multiple trees), while others show a unified tree. Additionally, container technologies can create multiple apparent init processes, creating a forest-like structure from the host perspective.
Understanding how the kernel constructs and maintains the process tree reveals the elegance of the parent-child model. Every tree operation—insertion, deletion, traversal—has precise semantics.
When fork() is called, a new node is inserted into the tree:
Algorithm: INSERT_PROCESS(parent_pcb)
1. new_pcb = ALLOCATE_PCB()
2. new_pcb.PPD = parent_pcb.PID // Link to parent
3. new_pcb.PID = GET_UNIQUE_PID() // Assign identity
4. parent_pcb.children.ADD(new_pcb) // Add to parent's children list
5. PROCESS_TABLE.INSERT(new_pcb) // Global registration
6. SCHEDULER.ENQUEUE(new_pcb) // Ready for execution
7. return new_pcb.PID to parent, 0 to child
The parent maintains a list (or linked structure) of its children, enabling:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
#include <stdio.h>#include <unistd.h>#include <sys/wait.h>#include <string.h> /** * Demonstrates explicit tree construction through nested forks * * Creates a balanced binary tree of depth 3: * root * / \ * left right * / \ / \ * ll lr rl rr */ typedef struct { pid_t pid; char name[32]; int depth;} ProcessInfo; void log_process(const char* name, int depth) { for (int i = 0; i < depth; i++) printf(" "); printf("├─ %s (PID: %d, PPID: %d)\n", name, getpid(), getppid());} void create_subtree(const char* my_name, int depth, int max_depth) { log_process(my_name, depth); if (depth >= max_depth) { return; // Leaf node } // Create left child pid_t left = fork(); if (left == 0) { char left_name[64]; snprintf(left_name, sizeof(left_name), "%s-L", my_name); create_subtree(left_name, depth + 1, max_depth); _exit(0); } // Create right child pid_t right = fork(); if (right == 0) { char right_name[64]; snprintf(right_name, sizeof(right_name), "%s-R", my_name); create_subtree(right_name, depth + 1, max_depth); _exit(0); } // Wait for both children waitpid(left, NULL, 0); waitpid(right, NULL, 0);} int main() { printf("Process Tree Construction Demo\n"); printf("==============================\n"); create_subtree("Root", 0, 2); printf("\nTree traversal complete.\n"); return 0;}Process termination is more complex than creation because it involves maintaining tree integrity:
Algorithm: REMOVE_PROCESS(dying_pcb)
1. IF dying_pcb has children:
FOR each child IN dying_pcb.children:
child.PPID = 1 // Reparent to init
init_pcb.children.ADD(child)
2. dying_pcb.parent.children.REMOVE(dying_pcb)
3. dying_pcb.state = ZOMBIE
4. SIGNAL(dying_pcb.parent, SIGCHLD)
5. // Wait for parent to call wait()
6. ON wait() call from parent:
COLLECT_EXIT_STATUS(dying_pcb)
PROCESS_TABLE.DELETE(dying_pcb)
FREE_PCB(dying_pcb)
Key Insight: A process cannot be fully removed from the tree until its parent acknowledges its termination. This is why zombies exist—they are removed from active scheduling but remain in the tree structure until reaped.
When a process with many descendants terminates, all descendants become orphans and are reparented to init. This can temporarily create a large number of direct children under init, potentially stressing init's child management. Well-designed daemons manage their own process groups to enable cleaner termination.
The kernel maintains the process tree using linked data structures within each Process Control Block:
// Simplified Linux task_struct fields for tree management
struct task_struct {
pid_t pid; // This process's ID
pid_t tgid; // Thread group ID
struct task_struct *real_parent; // Biological parent (original creator)
struct task_struct *parent; // Current parent (may differ after ptrace)
struct list_head children; // List of my children
struct list_head sibling; // Linkage in my sibling list
struct pid *thread_pid; // PID structure pointer
// ... many more fields
};
real_parent vs parent: Usually identical, but parent can differ when a debugger (using ptrace) temporarily becomes the 'parent' for signal purposes. real_parent always points to the original creator.
Process tree visualization is essential for system administration, debugging, and understanding system behavior. Different tools provide different perspectives.
| Command | Platform | Key Features |
|---|---|---|
pstree | Linux, BSD | ASCII art tree visualization, thread grouping, command-line highlighting |
ps axjf or ps -ejH | Linux | Hierarchical ps output with PID, PPID, PGID columns |
ps -o pid,ppid,command | POSIX | Custom column output showing parent relationship |
htop (F5 for tree view) | Linux | Interactive process viewer with tree toggle |
proctree (procps) | Linux | Focused subtree from specified PID |
Task Manager > Details | Windows | GUI view with parent-child expansion |
wmic process | Windows | Command-line process enumeration with ParentProcessId |
1234567891011121314151617181920212223242526272829303132333435363738
#!/bin/bash # ============================================# Process Tree Visualization Examples# ============================================ echo "=== 1. Basic pstree output ==="pstree echo -e "\n=== 2. pstree with PIDs ==="pstree -p echo -e "\n=== 3. pstree from specific PID (e.g., PID 1) ==="pstree -p 1 echo -e "\n=== 4. Show all threads (-t) ==="pstree -pt $$ # $$ is current shell's PID echo -e "\n=== 5. Highlight current process (-h) ==="pstree -h echo -e "\n=== 6. Show command line arguments (-a) ==="pstree -a | head -30 echo -e "\n=== 7. ps with hierarchy (forest format) ==="ps axjf | head -20 echo -e "\n=== 8. Custom ps columns ==="ps -eo pid,ppid,uid,cmd --forest | head -20 echo -e "\n=== 9. Find children of a specific process ==="PARENT_PID=1echo "Children of PID $PARENT_PID:"ps --ppid $PARENT_PID -o pid,cmd echo -e "\n=== 10. Tree depth analysis ==="# Count processes at each depth levelpstree -p | grep -Eo '^[│├└─ ]*' | awk '{print length}' | sort | uniq -csystemd(1)─┬─ModemManager(832)───2*[{ModemManager}]
├─NetworkManager(829)───2*[{NetworkManager}]
├─accounts-daemon(835)───2*[{accounts-daemon}]
├─cron(821)
├─dbus-daemon(493)
├─docker(1024)─┬─containerd(1089)───10*[{containerd}]
│ └─8*[{docker}]
├─gdm3(1176)─┬─gdm-session-wor(1234)─┬─gdm-wayland-ses(1250)─┬─gnome-session-b(1260)───...
│ │ │ └─2*[{gdm-wayland-ses}]
│ │ └─2*[{gdm-session-wor}]
│ └─2*[{gdm3}]
├─sshd(1045)───sshd(23456)───sshd(23458)───bash(23459)───python(23500)─┬─worker(23501)
│ ├─worker(23502)
│ └─worker(23503)
└─systemd-logind(840)
Reading the Output:
When debugging, pstree -p $(pgrep your_process) shows only the subtree rooted at your process. Combined with pstree -h (highlight ancestors), you can quickly understand where a process sits in the system hierarchy and what might be affecting it.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
#include <stdio.h>#include <stdlib.h>#include <dirent.h>#include <string.h>#include <ctype.h> /** * Programmatically traverse process tree via /proc filesystem * * On Linux, /proc/[pid]/stat contains PPID at field 4 * /proc/[pid]/status contains it in a more readable format */ typedef struct ProcessNode { pid_t pid; pid_t ppid; char name[256]; struct ProcessNode* children; struct ProcessNode* sibling;} ProcessNode; // Read process info from /proc/[pid]/statint read_process_info(pid_t pid, pid_t* ppid, char* name) { char path[64]; snprintf(path, sizeof(path), "/proc/%d/stat", pid); FILE* f = fopen(path, "r"); if (!f) return -1; int scanned = fscanf(f, "%*d (%255[^)]) %*c %d", name, ppid); fclose(f); return (scanned == 2) ? 0 : -1;} // Print process tree recursivelyvoid print_tree(pid_t pid, int depth, pid_t target_ppid) { DIR* proc = opendir("/proc"); if (!proc) return; struct dirent* entry; while ((entry = readdir(proc)) != NULL) { // Skip non-numeric entries if (!isdigit(entry->d_name[0])) continue; pid_t curr_pid = atoi(entry->d_name); pid_t curr_ppid; char name[256]; if (read_process_info(curr_pid, &curr_ppid, name) == 0) { if (curr_ppid == target_ppid) { // Print this child for (int i = 0; i < depth; i++) printf(" "); printf("├─ %s (PID: %d)\n", name, curr_pid); // Recurse to find this process's children closedir(proc); print_tree(curr_pid, depth + 1, curr_pid); proc = opendir("/proc"); } } } closedir(proc);} int main(int argc, char* argv[]) { pid_t root_pid = 1; // Default to init if (argc > 1) { root_pid = atoi(argv[1]); } // Print root char root_name[256]; pid_t root_ppid; if (read_process_info(root_pid, &root_ppid, root_name) == 0) { printf("%s (PID: %d)\n", root_name, root_pid); } else { printf("PID %d (cannot read info)\n", root_pid); } // Print tree starting from root print_tree(root_pid, 1, root_pid); return 0;}The kernel and system tools frequently traverse the process tree for various operations. Understanding these traversal patterns illuminates how the OS performs process management.
1. Find All Descendants (Depth-First Search)
Used for: Sending signals to process groups, resource accounting, cascading termination
def find_all_descendants(process):
"""Returns all processes in the subtree rooted at 'process'"""
descendants = []
stack = [process]
while stack:
current = stack.pop()
descendants.append(current)
stack.extend(current.children)
return descendants
2. Find Path to Root (Ancestor Chain)
Used for: Permission checking, session identification, debugging
def find_ancestors(process):
"""Returns chain from process to init"""
ancestors = []
current = process
while current is not None:
ancestors.append(current)
current = current.parent
return ancestors # [process, parent, grandparent, ..., init]
3. Find Common Ancestor
Used for: Determining relationship between processes, IPC permission checks
def find_common_ancestor(p1, p2):
"""Lowest common ancestor of two processes"""
ancestors_p1 = set(find_ancestors(p1))
current = p2
while current is not None:
if current in ancestors_p1:
return current
current = current.parent
return None # Should never happen (init is common ancestor)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
#include <stdio.h>#include <stdlib.h>#include <dirent.h>#include <string.h>#include <ctype.h>#include <sys/types.h> /** * Tree traversal algorithms implemented using /proc filesystem */ // Read PPID from /proc/[pid]/statpid_t get_ppid(pid_t pid) { char path[64]; snprintf(path, sizeof(path), "/proc/%d/stat", pid); FILE* f = fopen(path, "r"); if (!f) return -1; pid_t ppid; fscanf(f, "%*d %*s %*c %d", &ppid); fclose(f); return ppid;} // Algorithm 1: Print ancestor chain to initvoid print_ancestor_chain(pid_t pid) { printf("Ancestor chain for PID %d:\n", pid); pid_t current = pid; int depth = 0; while (current > 0) { for (int i = 0; i < depth; i++) printf(" "); printf("└─ PID %d\n", current); if (current == 1) break; // Reached init current = get_ppid(current); depth++; }} // Algorithm 2: Count all descendantsint count_descendants(pid_t root_pid) { int count = 0; DIR* proc = opendir("/proc"); if (!proc) return -1; struct dirent* entry; while ((entry = readdir(proc)) != NULL) { if (!isdigit(entry->d_name[0])) continue; pid_t pid = atoi(entry->d_name); // Check if this process is a descendant of root_pid pid_t current = pid; while (current > 1) { if (current == root_pid) { count++; break; } current = get_ppid(current); if (current <= 0) break; // Error reading } } closedir(proc); return count;} // Algorithm 3: Calculate tree depth from initint calculate_depth(pid_t pid) { int depth = 0; pid_t current = pid; while (current > 1) { depth++; current = get_ppid(current); if (current <= 0) return -1; // Error } return depth;} // Algorithm 4: Find Lowest Common Ancestorpid_t find_lca(pid_t p1, pid_t p2) { // Build ancestor set for p1 pid_t ancestors1[256]; int n1 = 0; pid_t current = p1; while (current >= 1 && n1 < 256) { ancestors1[n1++] = current; if (current == 1) break; current = get_ppid(current); } // Walk up from p2, looking for match current = p2; while (current >= 1) { for (int i = 0; i < n1; i++) { if (ancestors1[i] == current) { return current; // Found LCA } } if (current == 1) break; current = get_ppid(current); } return 1; // Default to init} int main(int argc, char* argv[]) { pid_t target_pid = getpid(); if (argc > 1) { target_pid = atoi(argv[1]); } printf("=== Process Tree Analysis for PID %d ===\n\n", target_pid); // 1. Ancestor chain print_ancestor_chain(target_pid); // 2. Tree depth int depth = calculate_depth(target_pid); printf("\nTree depth (distance from init): %d\n", depth); // 3. Descendant count int descendants = count_descendants(target_pid); printf("Number of descendants: %d\n", descendants); // 4. LCA with init's first child (usually a system daemon) printf("\nLCA with PID 1: %d\n", find_lca(target_pid, 1)); return 0;}The kernel doesn't scan /proc for these operations—it has direct pointers in task_struct. The children list and parent pointer allow O(1) access to immediate relatives. However, finding all descendants still requires O(n) tree traversal. The kernel optimizes this with iterator macros like for_each_process() and for_each_thread().
While the process tree captures creation relationships, process groups and sessions provide orthogonal grouping mechanisms for job control and signal delivery.
A process group is a collection of processes that can receive signals collectively. Each process group has a Process Group ID (PGID), which equals the PID of the process group leader.
Properties:
setpgid() or become a new group leaderkill(-pgid, SIGTERM) sends SIGTERM to all membersA session is a collection of process groups, typically associated with a terminal login. Each session has a Session ID (SID), which equals the PID of the session leader.
Properties:
setsid() (must not already be a group leader)The process tree (parent-child) and process groups (PGID) are independent axes of organization:
| Aspect | Process Tree | Process Groups/Sessions |
|---|---|---|
| Relationship | Parent → Child (creation) | Peer membership (voluntary) |
| Determined by | fork() | setpgid(), setsid() |
| Spans generations? | Yes (ancestors to descendants) | No (flat membership) |
| Signal delivery | Not directly used | kill(-pgid, sig) targets group |
| Resource accounting | Hierarchical | Can span tree boundaries |
| Termination cascade | Orphan reparenting | No automatic cascade |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
#include <stdio.h>#include <unistd.h>#include <sys/wait.h>#include <signal.h> /** * Demonstrates process groups and their independence from tree structure */ void print_ids(const char* label) { printf("[%s] PID=%d, PPID=%d, PGID=%d, SID=%d\n", label, getpid(), getppid(), getpgrp(), getsid(0));} int main() { print_ids("Parent"); pid_t child1 = fork(); if (child1 == 0) { print_ids("Child1-before"); // Create new process group with self as leader setpgid(0, 0); // Equivalent to setpgid(getpid(), getpid()) print_ids("Child1-after setpgid"); // Create grandchild - inherits new PGID pid_t grandchild = fork(); if (grandchild == 0) { print_ids("Grandchild (in child1's group)"); sleep(5); _exit(0); } waitpid(grandchild, NULL, 0); _exit(0); } pid_t child2 = fork(); if (child2 == 0) { print_ids("Child2 (stays in parent's group)"); sleep(2); _exit(0); } sleep(1); // Let children print printf("\n--- Group Structure ---\n"); printf("Parent (PID %d) is in group %d\n", getpid(), getpgrp()); printf("Child1 (PID %d) is group leader of group %d\n", child1, child1); printf("Child2 (PID %d) is in parent's group %d\n", child2, getpgrp()); // Signal entire groups printf("\nSending SIGUSR1 to child1's group...\n"); kill(-child1, SIGUSR1); // Send to entire group wait(NULL); wait(NULL); return 0;}To create a proper daemon: (1) fork and exit parent, (2) child calls setsid() to become session leader (detaches from terminal), (3) fork again and exit (ensures process is not session leader, cannot acquire terminal), (4) grandchild is the daemon. This pattern uses tree structure (fork) to escape session/group structure.
The kernel performs several operations that leverage the tree structure for correct semantics.
Some operating systems support cascading termination, where terminating a parent automatically terminates all descendants. Unix does NOT do this by default—orphans are reparented to init. However, this behavior can be achieved:
Linux: prctl(PR_SET_PDEATHSIG, signal)
A child can request to receive a signal when its parent dies:
prctl(PR_SET_PDEATHSIG, SIGTERM); // Die when parent dies
This must be combined with checking if parent already died (race condition).
cgroups/systemd Scope Units
Systemd can manage process trees using cgroups:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <signal.h>#include <sys/prctl.h>#include <sys/wait.h> /** * Demonstrates cascading termination using prctl() * * Child requests to receive SIGTERM when parent dies */ void sigterm_handler(int sig) { printf("[Child %d] Received SIGTERM because parent died!\n", getpid()); _exit(0);} int main() { printf("Parent PID: %d\n", getpid()); pid_t child = fork(); if (child == 0) { // Child process // Request SIGTERM when parent exits if (prctl(PR_SET_PDEATHSIG, SIGTERM) == -1) { perror("prctl"); _exit(1); } // RACE CONDITION CHECK: Parent may have already died // between fork() and prctl(). Check PPID. if (getppid() == 1) { // Already orphaned to init printf("[Child] Parent already dead, exiting\n"); _exit(0); } // Install signal handler signal(SIGTERM, sigterm_handler); printf("[Child %d] Waiting for parent to die...\n", getpid()); // Do work while waiting while (1) { pause(); // Wait for signal } } // Parent printf("Parent sleeping for 3 seconds then exiting...\n"); sleep(3); printf("Parent exiting now!\n"); // Note: don't use _exit() here - we want to trigger PDEATHSIG return 0;}Resource limits (rlimits) are inherited through the tree:
Process A (RLIMIT_NOFILE = 1024)
└── Process B (inherits 1024)
└── Process C (inherits 1024)
However, resource usage is tracked independently—each process has its own counters.
Accounting rollup: Some systems (especially with cgroups) can aggregate resource usage across subtrees for billing or monitoring.
Security contexts flow down the tree:
Children can drop privileges (setuid to lower privilege) but cannot gain privileges without executing setuid binaries.
Linux namespaces (for containers) follow tree structure:
// Parent in namespace A
fork()
// Child in namespace A (inherited)
clone(CLONE_NEWPID | CLONE_NEWNS, ...)
// Child in new namespace B (child is PID 1 in its namespace)
The tree structure determines namespace membership—entering a new namespace is a fork-like operation.
In a PID namespace (used by Docker/containerd), processes have different PIDs depending on observer. PID 1 inside a container is NOT the system's PID 1. The container's init is responsible for reaping zombies within the container. If it doesn't, zombies accumulate inside the container.
The process tree is more than a visualization—it is a living data structure that the kernel maintains for process management. Understanding its structure, construction, and traversal is essential for system programming and administration.
What's Next:
With the parent-child relationship and tree structure established, the next page examines resource sharing options—the different ways a parent can share (or not share) its resources with newly created children. This decision fundamentally affects process isolation, performance, and memory footprint.
You now understand the process tree as both a conceptual model and a kernel data structure. You can visualize, traverse, and reason about process hierarchies—skills essential for debugging process issues, managing daemons, and understanding container architecture.