Process Concept
A deep dive into process states, Process Control Block (PCB) architecture, and the mechanics of process creation in modern operating systems.
Process Concept
Every program you run, every daemon that keeps your server alive, every background service humming along in the cloud — all of them are processes. Understanding processes is the foundation of operating systems knowledge. Without this bedrock, you’ll find yourself lost when debugging that mysterious CPU spike at 3 AM or trying to explain why your application is not utilizing all those cores you paid for.
A process is essentially a program in execution. But here’s where it gets interesting: a program is passive (just bytes on disk), while a process is active — it has state, context, and a life cycle. This distinction matters more than you might think.
Introduction
The operating system must manage processes efficiently. It needs to create them, schedule them, allow them to communicate, and eventually terminate them. To do this, the OS maintains a data structure for each process — the Process Control Block (PCB) — which acts as the process’s fingerprint in the system.
When you execute a command in your terminal, the shell creates a new process by forking itself. The child process then typically calls exec to replace its memory image with the program you wanted to run. This pattern of fork-exec is fundamental to Unix-like systems and understanding it will save you countless hours of debugging.
When to Use
- Debugging performance issues — When CPU usage is unexpectedly high or low, understanding process states helps identify bottlenecks.
- Designing concurrent systems — Knowing how processes are scheduled allows you to write more efficient parallel code.
- System programming — Creating daemons, forking workers, or managing subprocess hierarchies requires solid process concepts.
- Capacity planning — Understanding how many processes your system can handle informs infrastructure decisions.
When Not to Use
- Writing simple scripts — For basic automation, you rarely need to think about process internals.
- High-level application development — Modern runtimes (JVM, Node.js, .NET) abstract away process management for most use cases.
- Database query optimization — Process concepts won’t help you tune your SQL indexes.
Process States
A process doesn’t simply exist as “running” or “not running.” The operating system defines several distinct states that a process can occupy:
graph TD
A[New] --> B[Ready]
B --> C[Running]
C --> D[Waiting]
D --> B
C --> E[Terminated]
B --> E
New: The process is being created. The OS is allocating memory, initializing the PCB, and setting up the address space.
Ready: The process is in memory and waiting to be assigned to a CPU core. The scheduler will pick it when a core becomes available.
Running: The process is actively executing instructions on a CPU core. Only one process (per core) can be in this state at any given moment.
Waiting (Blocked): The process cannot continue execution because it’s waiting for some event — I/O completion, a signal, a resource becoming available, or inter-process communication.
Terminated: The process has finished execution. The OS cleans up resources but may retain the PCB temporarily for the parent to retrieve exit status.
Process Control Block (PCB)
The PCB is the kernel’s representation of a process. It’s a structure — usually defined in the OS source code — that contains all the information the kernel needs to manage a process.
The PCB lives in kernel memory and is never directly accessible to user programs. However, on Linux, you can inspect much of this information via the /proc filesystem — each process has a directory at /proc/<pid>/.
Key PCB fields include:
- Process ID (PID): Unique identifier
- Parent PID (PPID): Who created this process
- State: Current execution state
- Program Counter: Next instruction to execute
- CPU Registers: Current register values
- Stack Pointer: Current top of stack
- Memory Management Info: Page tables, segments
- I/O Status: Open files, pending I/O operations
Process Creation
In Unix-like systems, processes are created via the fork-exec pattern:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
return 1;
}
if (pid == 0) {
// Child process
printf("Child: PID = %d, Parent PID = %d\n", getpid(), getppid());
// Replace with new program
char *args[] = { "ls", "-la", NULL };
execvp("ls", args);
// If execvp fails
perror("exec failed");
return 1;
} else {
// Parent process
printf("Parent: PID = %d, Child PID = %d\n", getpid(), pid);
int status;
waitpid(pid, &status, 0); // Wait for child
printf("Child exited with status: %d\n", WEXITSTATUS(status));
}
return 0;
}
The fork() system call creates a new process by duplicating the current process. After fork, both the parent and child continue execution from the same point — the only difference is that fork returns the child’s PID in the parent and 0 in the child.
Architecture Diagram
graph TB
subgraph Kernel_Space
PCB1[PCB: PID 1001]
PCB2[PCB: PID 1002]
PCB3[PCB: PID 1003]
Scheduler[Scheduler]
end
subgraph User_Space
Process1[Process 1001]
Process2[Process 1002]
Process3[Process 1003]
end
Process1 --> PCB1
Process2 --> PCB2
Process3 --> PCB3
Scheduler -->|schedules| PCB1
Scheduler -->|schedules| PCB2
Scheduler -->|schedules| PCB3
ReadyQ[Ready Queue] --> Scheduler
WaitQ[Waiting Queue] --> Scheduler
The scheduler maintains multiple queues: ready processes wait in the ready queue, while blocked processes wait in the waiting queue. The scheduler picks from the ready queue based on the scheduling algorithm in use.
Core Concepts
Process vs Thread
A key distinction: processes have separate address spaces, while threads share the same address space within a process. Creating a thread is cheaper than creating a process because no memory duplication is needed.
Parent-Child Hierarchy
Processes form a tree. Every process (except init) has a parent. If a parent dies before its child, the child is “adopted” by init (PID 1). You can see this hierarchy with pstree on Linux.
Zombie and Orphan Processes
A zombie is a process that has terminated but whose parent hasn’t yet called wait() to retrieve the exit status. Zombies remain in the process table until the parent reads their status.
An orphan is a process whose parent has died. The init process adopts orphans and eventually reaps them via wait.
Daemon Processes
A daemon is a background process that runs without a controlling terminal. Created by forking, then calling setsid() to detach from the terminal, then often changing the working directory to / and closing stdin/stdout/stderr.
Production Failure Scenarios
Fork Bombs
Problem: A process rapidly forks children that also fork, exhausting process table slots and PID resources.
Symptoms: “fork: Cannot allocate memory” errors, system unresponsiveness, inability to create new processes even for essential services.
Mitigation:
- Set appropriate
ulimit -u(max processes per user) - Use systemd’s
DefaultLimitNPROCin/etc/security/limits.conf - Implement exponential backoff in application code that forks
# Check current process limits
ulimit -a
# See process count per user
ps aux | awk '{print $1}' | sort | uniq -c | sort -rn | head
Zombie Accumulation
Problem: Application fails to call wait() on terminated children, causing zombie processes to accumulate.
Symptoms: ps shows processes with ‘Z’ state, process table fills up, new processes cannot be created.
Mitigation:
- Always call wait()/waitpid() in parent
- Use signal handler for SIGCHLD that reaps children
- For long-running servers, implement a child reaper thread
#include <signal.h>
void sigchld_handler(int sig) {
int saved_errno = errno; // Save errno
while (waitpid(-1, NULL, WNOHANG) > 0);
errno = saved_errno;
}
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);
Resource Leakage in Child Processes
Problem: Child processes inherit open file descriptors but never close them, leading to resource exhaustion.
Symptoms: “Too many open files” errors, file descriptor leaks visible in /proc/<pid>/fd/.
Mitigation:
- Set O_CLOEXEC flag when opening files
- Close unnecessary FDs before exec()
- Use
FD_CLOEXECwithfcntl()on inherited FDs
Trade-off Table
| Aspect | Process | Thread |
|---|---|---|
| Creation Speed | Slower (memory duplication) | Faster (shared address space) |
| Memory Overhead | Higher (separate page tables) | Lower (shares parent’s memory) |
| Communication | Complex (IPC required) | Simple (shared memory) |
| Fault Isolation | Strong (separate address space) | Weak (crash can affect others) |
| Synchronization | Simpler (no shared state) | Complex (must synchronize shared data) |
| Scenario | Best Choice | Reason |
|---|---|---|
| Isolated execution | Process | Crash doesn’t affect parent |
| High-frequency spawning | Thread | Lower overhead |
| CPU-bound parallel work | Thread | Shares memory, low latency |
| I/O-bound concurrent tasks | Either | Depends on isolation needs |
| Scheduler Design | Advantages | Disadvantages |
|---|---|---|
| Short-term (CPU) | Minimizes turnaround time | May starve processes |
| Long-term (Job) | Controls degree of multiprogramming | Slower response to load changes |
| Medium-term | Balances memory and CPU usage | Adds complexity |
Implementation Snippets
Creating a Daemon
#!/usr/bin/env python3
import os
import sys
def become_daemon():
"""Fork and detach from controlling terminal."""
# First fork
if os.fork() > 0:
sys.exit(0) # Parent exits
# Detach from controlling terminal
os.setsid()
# Second fork (prevents acquiring new controlling terminal)
if os.fork() > 0:
sys.exit(0)
# Change to root directory (prevents holding mount point)
os.chdir("/")
# Close stdin, stdout, stderr
sys.stdout.flush()
sys.stderr.flush()
with open(os.devnull, 'r') as devnull:
os.dup2(devnull.fileno(), sys.stdin.fileno())
with open(os.devnull, 'a+') as devnull:
os.dup2(devnull.fileno(), sys.stdout.fileno())
os.dup2(devnull.fileno(), sys.stderr.fileno())
# Write PID file
with open('/var/run/mydaemon.pid', 'w') as f:
f.write(str(os.getpid()))
become_daemon()
# Now running as daemon
import time
while True:
time.sleep(60)
Checking Process State on Linux
#!/bin/bash
# Monitor process state transitions
PID=$1
if [ -z "$PID" ]; then
echo "Usage: $0 <pid>"
exit 1
fi
echo "Monitoring process $PID..."
echo "Press Ctrl+C to stop"
while true; do
if [ -d "/proc/$PID" ]; then
STATE=$(cat /proc/$PID/status | grep "^State:" | awk '{print $2}')
CMD=$(cat /proc/$PID/cmdline 2>/dev/null | tr '\0' ' ' | cut -c1-60)
THREADS=$(cat /proc/$PID/status | grep "^Threads:" | awk '{print $2}')
echo "$(date '+%H:%M:%S') State=$STATE Threads=$THREADS CMD=$CMD"
else
echo "Process $PID no longer exists"
break
fi
sleep 1
done
Process Resource Monitoring in C
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/resource.h>
void print_rlimits(const char *name) {
struct rlimit limit;
printf("%s limits for PID %d:\n", name, getpid());
if (getrlimit(RLIMIT_CPU, &limit) == 0) {
printf(" CPU time: %lu (soft) / %lu (hard)\n",
(unsigned long)limit.rlim_cur,
(unsigned long)limit.rlim_max);
}
if (getrlimit(RLIMIT_NPROC, &limit) == 0) {
printf(" Max processes: %lu (soft) / %lu (hard)\n",
(unsigned long)limit.rlim_cur,
(unsigned long)limit.rlim_max);
}
if (getrlimit(RLIMIT_NOFILE, &limit) == 0) {
printf(" Open files: %lu (soft) / %lu (hard)\n",
(unsigned long)limit.rlim_cur,
(unsigned long)limit.rlim_max);
}
}
int main() {
print_rlimits("Current process");
return 0;
}
Observability Checklist
Metrics to Monitor
- Process count per user:
ps aux | awk '{print $1}' | sort | uniq -c | sort -rn | head - Zombie count:
ps aux | grep ' Z ' | wc -l - Runnable processes:
vmstat 1 | tail -1(look at ‘r’ column) - Process state distribution:
ps -eo state,pid,cmd | sort | uniq -c | sort -rn
Logs to Watch
/var/log/syslogor/var/log/messages— system process creation/terminationdmesg | grep -i "process"— kernel messages about process eventsjournalctl -u <service>— for systemd-managed services
Alerts to Configure
- Zombie process count > 0 for more than 5 minutes
- Total process count approaching
kernel.pid_max(default 32768 on Linux) - Process count per user exceeding 80% of their ulimit
- High rate of fork() failures in application logs
Trace Commands
# Trace process creation
sudo strace -f -e trace=fork,vfork,clone,execve
# Trace signal delivery
sudo strace -p <pid> -e trace=signal
# Monitor process state changes with perf
sudo perf sched record -a -g sleep 10
sudo perf sched latency
Common Pitfalls / Anti-Patterns
Process Isolation
Modern OSes use hardware virtualization (Intel VT-x, AMD-V) and address space layout randomization (ASLR) to isolate processes. Ensure these are enabled in your BIOS and kernel configuration.
Privilege Separation
Run services with minimal privileges. Use setuid and setgid binaries cautiously — these are common privilege escalation vectors.
# Check for setuid binaries (audit regularly)
find / -perm -4000 -type f 2>/dev/null | head -20
Resource Limits
Configure /etc/security/limits.conf to prevent fork bombs and resource exhaustion:
* soft nproc 1024
* hard nproc 4096
* soft nofile 1024
* hard nofile 4096
Capability Model
Linux capabilities split root privileges into fine-grained units. Prefer dropping capabilities with prctl(PR_CAPBSET_DROP, ...) rather than running as root.
Audit Requirements
For compliance (PCI-DSS, SOC2), log process creation and termination:
# Enable process accounting (Debian/Ubuntu)
sudo apt install acct
sudo systemctl enable acct
Common Pitfalls / Anti-patterns
Ignoring SIGCHLD
Not handling SIGCHLD causes zombies. Always either:
- Block SIGCHLD and call wait() in a dedicated thread
- Set SA_NOCLDSTOP and reap in a signal handler
- Ignore SIGCHLD entirely (only if you never need to wait)
Fork without exec
Calling fork() and then continuing in the child without exec() is often wasteful. If you don’t need the parent’s memory image, consider vfork() (deprecated) or clone() with appropriate flags.
Not checking fork() return value
Always check if fork() failed (returns -1). Fork can fail due to process table exhaustion.
Zombie accumulation in production
Applications that fork workers must implement proper reaping. Use waitpid(-1, NULL, WNOHANG) in a loop to reap all terminated children.
Unintended forking loops
Ensure your code cannot enter a path where fork() is called in a loop without proper exit conditions.
Quick Recap Checklist
- A process is a program in execution with its own address space and state
- Process Control Block (PCB) stores all metadata about a process
- Processes transition through states: New → Ready → Running → Waiting → Terminated
- fork() creates a new process; exec() replaces the program’s memory
- Copy-on-write makes fork() efficient even with large memory footprints
- Zombies form when parent doesn’t call wait(); orphans are adopted by init
- Daemons are background processes detached from controlling terminals
- ulimits prevent resource exhaustion; monitor process counts in production
- Always handle SIGCHLD to prevent zombie accumulation
Interview Questions
A program is a passive entity — a file containing executable code and data stored on disk. A process is an active entity — a program that has been loaded into memory and is currently executing with its own execution context, including registers, stack, heap, and program counter.
In technical terms, a process is the unit of execution in an operating system, while a program is the static description of that execution.
A process can be in one of five primary states:
- New: The OS is creating the process, allocating the PCB and initializing address space.
- Ready: The process is in memory and waiting to be scheduled on a CPU core.
- Running: Instructions are being executed on a CPU core.
- Waiting: The process cannot continue because it's blocked waiting for I/O, a signal, or resource.
- Terminated: Execution has completed, but the PCB remains until the parent retrieves exit status.
Transitions: New→Ready (setup complete), Ready→Running (scheduler picks it), Running→Waiting (I/O or blocking event), Waiting→Ready (event completes), Running→Ready (time slice expired or preemption), Running→Terminated (exit syscall), Ready→Terminated (parent reaps before scheduling).
A zombie is a process that has terminated but whose entry still exists in the process table because the parent hasn't read its exit status via wait() or waitpid().
When a process terminates, the kernel retains its PCB temporarily so the parent can retrieve the exit code and resource usage statistics. If the parent never calls wait(), this data is never read and the process entry persists as a zombie. Zombies cannot be killed (they're already dead) and must be reaped by their parent.
To eliminate zombies, either fix the parent process to properly reap children, or kill the parent (then init adopts and reaps them).
Without COW, fork() would need to copy the entire parent's memory (stack, heap, code) to the child's address space before allowing either to execute. This is expensive for processes with gigabytes of mapped memory.
With COW, fork() creates a new PCB and page tables but doesn't immediately copy the physical memory pages. Instead, both parent and child share the same physical pages, marked as read-only. As soon as either process tries to modify a page, the CPU raises a page fault. The kernel then creates a private copy of that page for the writing process, and the other process keeps the original.
This optimization makes fork() nearly instantaneous regardless of the process's memory footprint, while still maintaining the separate address spaces that processes require.
The PCB is the kernel's data structure that represents a process. It contains all information needed to manage, schedule, and control the process:
- Identification: PID, parent PID, user ID, group ID
- State: Current process state (running, waiting, etc.)
- Scheduling: Priority, cumulative CPU time, scheduling policy
- Memory: Pointer to memory descriptor (page tables, segments)
- CPU context: Register values, program counter, stack pointer
- I/O status: Open files, file descriptor table, current directory
- Accounting: Start time, total CPU time used
- Signals: Pending signals, signal handler table
When a process is context-switched out, its CPU context is saved to the PCB. When it's rescheduled, the PCB contents are restored to the CPU.
An orphan is a process whose parent has terminated. The kernel re-parents such processes to init (PID 1), which periodically calls wait() to reap them. Orphans are not problematic — they are cleaned up automatically.
A zombie is a process that has terminated but whose PCB entry remains in the process table because the parent hasn't called wait() to retrieve the exit status. Unlike orphans, zombies cannot be killed (they are already dead) and will persist until the parent reaps them.
The key difference: orphans are still running (from the kernel's perspective) until they terminate, while zombies are already terminated but not yet cleaned up.
A daemon is a background process that has detached from its controlling terminal — it has no terminal associated with it, which means it won't receive terminal-generated signals like SIGINT (Ctrl+C) or SIGHUP (terminal hangup).
Creation steps: fork() → setsid() (detach from terminal) → optionally fork again (prevent acquiring a new terminal) → chdir("/") (prevent holding a mount point) → close stdin/stdout/stderr → write PID file.
Regular background processes started with & are still attached to the terminal and will receive SIGHUP when the terminal closes. Daemons are immune to terminal closure.
A context switch involves several hardware-level operations:
- Timer interrupt fires, transferring control to kernel mode
- Kernel saves current process's registers, program counter, and stack pointer onto the kernel stack
- Kernel switches to the process's kernel stack (for that process's kernel-mode execution)
- Scheduler selects a new process and loads its saved registers from the PCB
- Stack pointer is switched to the new process's kernel stack
- Control returns to user mode with the new process's instruction pointer
On x86-64, this involves saving/restoring 16 general-purpose registers, RIP, RSP, RFLAGS, segment registers, and FPU/SSE state if used.
The init process is the first user-space process, started by the kernel at boot. It serves as the ancestor of all other processes — every process is either a direct or indirect child of init.
Key responsibilities:
- Zombie reaping: init calls wait() on all orphaned children, preventing zombie accumulation
- System initialization: runs startup scripts (/etc/rc.d/, systemd units) to bring up services
- Adoption: re-parents any process whose parent terminates, ensuring no process is orphaned
On modern systems, systemd or openrc often replaces init's traditional role, but the reaping responsibility remains critical.
PID (Process ID) is the unique identifier for a process. TGID (Thread Group ID) is the PID of the thread that started a thread group — all threads in a multi-threaded process share the same TGID. PGID (Process Group ID) groups related processes for signal delivery (e.g., a pipeline of processes).
For a single-threaded process, PID == TGID. For the main thread of a multi-threaded process, PID == TGID. Additional threads get unique PIDs but share the TGID.
The nice value (-20 to +19, default 0) adjusts a process's scheduling priority on Unix systems. Higher nice values mean lower priority — the process is "nicer" to other processes by yielding CPU more readily.
Negative nice values (requires root) increase priority above normal. Positive nice values decrease priority. The kernel converts nice to a static priority value that influences CPU time distribution.
On Linux, CFS ignores nice values for normal scheduling (uses vruntime), but nice values affect the latency target. Batch and idle-class schedulers use nice directly for priority.
The kernel enforces resource limits via the rlimit mechanism. Each process has a soft limit (what the process can change) and a hard limit (root-only ceiling). Limits include max processes (RLIMIT_NPROC), max open files (RLIMIT_NOFILE), max file size (RLIMIT_FSIZE), and CPU time (RLIMIT_CPU).
When a process hits a limit, the kernel returns an error — fork() returns EAGAIN, open() returns EMFILE, etc.
vfork() was introduced as an optimization before copy-on-write existed. It creates a child process without copying the parent's page tables — the child runs in the parent's address space until it calls exec() or exit(). The parent is blocked while the child runs.
vfork() is essentially obsolete now because COW makes fork() efficient. On modern Linux, vfork() is implemented as fork() with COW disabled for comparison purposes.
fork() with COW is faster in practice because many processes only read memory, requiring zero actual copying.
exit() performs several cleanup operations:
- Closes all open file descriptors
- Releases memory (page tables, file mappings)
- Notifies the parent via SIGCHLD (if not ignored)
- Changes state to ZOMBIE, retaining the PCB for parent to read
- Stores exit code (passed to exit() or returned from main)
Forking a multi-threaded program creates a child with only the calling thread — all other threads disappear. This is a critical and often misunderstood behavior.
Only the thread that called fork() continues in the child. Other threads are not replicated. This leads to subtle bugs: a thread holding a mutex during fork() can leave that mutex locked permanently in the child.
Best practice: fork() followed immediately by exec() in the child, so the parent's address space is replaced entirely.
The kernel maintains a process table as an array of PCB structures (task_struct in Linux). Each entry is identified by a PID and contains all process metadata.
The /proc filesystem is a pseudo-filesystem that exposes kernel data structures as files. Each running process has a directory /proc/
- /proc/
/status — process state and resource usage - /proc/
/maps — memory mappings - /proc/
/fd — open file descriptors
A process group is a collection of processes that share a PGID (process group ID). The group leader has a PGID equal to its PID. All members of a group receive signals together — pressing Ctrl+C sends SIGINT to all processes in the foreground process group.
Shells use process groups to manage pipelines: grep foo file | sort creates a process group containing grep and sort.
The kernel tracks CPU time in the PCB via utime (user-mode CPU time) and stime (kernel-mode CPU time), measured in clock ticks. These accumulate during each context switch.
On Linux, /proc/
Accounting happens at every tick (typically 100Hz or 1000Hz depending on CONFIG_HZ), which is why high tick rates increase scheduling overhead.
wait() suspends the caller until any child terminates, returning its PID and exit status. It blocks if no child has exited yet.
waitpid(pid, status, options) waits for a specific child (pid=-1 for any child). WNOHANG option makes it non-blocking — returns 0 if no child has exited.
wait3() and wait4() are older Unix variants that additionally fill a rusage struct with the child's resource usage. WIFEXITED, WEXITSTATUS, WIFSIGNALED, WTERMSIG macros extract information from the status integer.
Docker containers are processes with isolated namespaces. Each container sees its own PID 1, its own network stack, its own filesystem mount, and its own process table — all created by namespace syscalls (clone with CLONE_NEW* flags).
Key namespaces: PID (isolated process numbering), NET (separate network stack), NS (mount points), USER (separate UID/GID mapping), IPC (separate SysV IPC objects).
cgroups (control groups) limit CPU, memory, and I/O per container. Namespaces + cgroups + capabilities = containers.
Further Reading
- Process Scheduling — Understand how schedulers decide which runnable process gets CPU time
- Threads & Lightweight Processes — Explore how threads share address space within a process
- Fork & Exec System Calls — Deep dive into Unix process creation mechanics
- CPU Affinity & Real-Time OS — Control where processes run and guarantee execution deadlines
Conclusion
Processes are the fundamental unit of execution in any operating system. Understanding their lifecycle—creation, scheduling, communication, and termination—provides the foundation for debugging performance issues, designing concurrent systems, and writing robust system software.
The concepts covered here—PCB architecture, fork-exec patterns, zombie and orphan processes, and daemon creation—apply across all Unix-like systems and inform how modern runtimes and container systems work. For example, when Docker runs a container, it’s essentially creating a process with specific namespace isolations.
Continue your learning by exploring process scheduling algorithms, inter-process communication mechanisms (pipes, message queues, shared memory), and thread implementation. These topics build directly on the process concepts you’ve mastered here.
Category
Related Posts
ASLR & Stack Protection
Address Space Layout Randomization, stack canaries, and exploit mitigation techniques
Assembly Language Basics: Writing Code the CPU Understands
Learn to read and write simple programs in x86 and ARM assembly, understanding registers, instructions, and the art of thinking in low-level operations.
Boolean Logic & Gates
Understanding AND, OR, NOT gates and how they combine into arithmetic logic units — the building blocks of every processor.