Process vs Thread in OS: PCB, States, fork()

What a process and a thread are, what a PCB holds, the process state diagram, how a context switch works, fork() and exec(), and the multithreading models.

What is the difference between a process and a thread?

A process is a program in execution with its own address space, open files and resources. A thread is a single path of execution inside a process; threads of the same process share its code, data, heap and open files but each has its own program counter, registers and stack. Creating and switching threads is therefore cheaper than for processes, but a bug in one thread can corrupt the others.

A process is a program in execution: the code plus everything that changes while it runs. A thread is one flow of execution inside a process. Every program you start becomes at least one process with one thread, and the operating system's job of "running many programs at once" is really the job of creating, switching and ending processes and threads. This note covers what the kernel keeps about each, how it moves between them, and why threads exist at all.

What a process is

A program is a passive file on disk; a process is that program loaded into memory and running. One program can be several processes at the same time, each with its own state.

Each process gets its own address space in four parts: text (the machine code, read-only), data (global and static variables), the heap (memory allocated at run time with malloc or new, growing upward) and the stack (one frame per active function call, growing downward).

high addresseslow addressesmain(): p = block 0free16 bytes16 bytesdata: count = 5text: main, f, gstackheap
A process's address space while a program runs. Example: int count = 5; main() mallocs p and calls f(3), which mallocs q and calls g(x + count)
  1. The program is loaded: its code in text, the global count = 5 in data, and one stack frame for main(). The heap is empty, and the free space between heap and stack is where both will grow.
  2. main() calls malloc(16). The heap grows upward by one block, and the pointer p, a local variable on the stack, holds its address.
  3. main() calls f(3). The stack grows downward: f gets a new frame with its argument x = 3 and its own local q.
  4. f() calls malloc(16) too, and a second block sits above the first: the heap grows up while the stack grows down.
  5. f() calls g(x + count), so g's frame holds y = 3 + 5 = 8. Each call adds a frame; deep recursion keeps adding them until the stack overflows.
  6. g returns 16 and f returns: their frames are gone the moment they return. The heap blocks stay until free() — and q's block is now unreachable, a memory leak.

Besides memory, a process owns resources the kernel tracks for it: open files, network connections, a current directory, a user identity and signal handlers.

The process control block

The kernel describes every process with a process control block (PCB), also called a task control block. On Linux it is the task_struct. The PCB is what lets a process stop and later continue exactly where it was.

PCB fieldWhat it holds
Process IDA unique number (PID), plus the parent's PID
Process stateNew, ready, running, waiting or terminated
Program counterAddress of the next instruction to run
CPU registersSaved general registers, stack pointer, flags
Scheduling informationPriority, pointers into scheduling queues, time used
Memory-management informationPage table base or segment table, memory limits
Accounting informationCPU time used, start time, limits
I/O status informationOpen file descriptors, devices allocated

Process states

A process moves through states as it runs. The standard five-state model:

  • New: being created; the PCB is being set up.
  • Ready: in memory, able to run, waiting only for a CPU.
  • Running: its instructions are executing on a CPU. With one core, at most one process is running.
  • Waiting (Blocked): cannot continue until something happens, such as an I/O completion, a lock or a child exiting.
  • Terminated: finished; the kernel is cleaning up.
admitteddispatchinterruptI/O waitI/O doneexitnewreadyrunningterminatedwaiting
Process states, and one process's life through them.
  1. The five states and the only legal moves between them, each arrow labelled with what causes it. The steps that follow trace one process from creation to exit.
  2. The OS finishes creating the process and admits it: it is in memory and only needs a CPU.
  3. The scheduler dispatches it: its registers are loaded and it runs.
  4. It asks to read a file. It cannot continue until the disk answers, so it waits and gives up the CPU.
  5. The disk's interrupt reports the read is complete. The process becomes ready — not running: it must queue for the CPU like everyone else.
  6. Dispatched again, it continues where it left off: its saved program counter was in its PCB.
  7. The timer fires at the end of its time slice and the scheduler preempts it, back to ready.
  8. Dispatched again, it runs to its end.
  9. It calls exit(). The kernel frees its memory and files; its PCB remains until the parent collects the exit status.

Two transitions do not exist: Waiting never goes straight to Running (it must queue in Ready first), and Ready never goes to Waiting (only a running process can ask for I/O).

Systems that swap processes out of memory add two suspended states, ready-suspended and blocked-suspended. Three schedulers manage the whole picture: the long-term scheduler decides which new jobs are admitted, the short-term (CPU) scheduler picks the next ready process many times a second, and the medium-term scheduler swaps processes out and back in to control memory pressure.

Context switch

A context switch moves the CPU from one process (or thread) to another. Something enters the kernel (a timer interrupt, an I/O request, a blocking system call), and the kernel saves the running process's state into its PCB and loads the next one's:

CPU — user mode, P2PC0x52c4SP0x7f80R17page table0x2000PCB of P1statereadyPC0x401aSP0x7ff0R142page table0x1000PCB of P2staterunningPCSPR1page table
A context switch from P1 to P2.
  1. P1 is running: its program counter, stack pointer, registers and page-table base are live in the CPU. P2 is ready, and its PCB holds the values it had when it last stopped.
  2. The timer interrupt enters the kernel. It saves P1's CPU state into P1's PCB and marks P1 ready — exactly what it needs to resume P1 later.
  3. The scheduler picks the next process from the ready queue: P2. Nothing useful has run for either process since the interrupt; the switch is pure overhead.
  4. The kernel loads P2's page-table base (0x2000) into the CPU: from now on addresses mean P2's memory, and the TLB's cached translations for P1 are useless. A switch between two threads of one process skips this step.
  5. Finally it restores P2's registers and returns to user mode: P2 continues at 0x52c4, exactly where it stopped, unaware it was ever off the CPU.

A context switch is pure overhead: the CPU does no useful work for either process during it. The direct cost is small, typically microseconds, but the indirect cost is larger: the new process finds the caches and the TLB filled with the old process's data. Switching between two threads of the same process skips the address-space switch, which is one reason threads are cheaper.

Creating processes: fork() and exec()

On Unix-like systems, fork() creates a new process by duplicating the caller. The new child gets a copy of the parent's address space, open files and registers. fork() returns 0 in the child, the child's PID in the parent, and -1 in the parent if it failed. exec() then replaces the child's program with a new one, and wait() lets the parent collect the child's exit status.

#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void) {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");                      /* no child was created */
        return 1;
    }
    if (pid == 0) {                          /* child */
        printf("child %d, parent %d\n", getpid(), getppid());
        execlp("ls", "ls", "-l", (char *)NULL);
        perror("execlp");                    /* reached only if exec failed */
        return 1;
    }
    waitpid(pid, NULL, 0);                   /* parent waits and reaps the child */
    printf("parent: child %d finished\n", pid);
    return 0;
}

Copying a whole address space would be wasteful when the child calls exec at once, so modern kernels use copy-on-write: parent and child share the same physical pages, marked read-only, and a page is copied only when one of them writes to it.

Counting is a favourite question. Each fork() doubles the number of processes running past it, so three fork() calls in a row give 2³ = 8 processes (7 new ones), and a printf after them prints 8 times.

P0P1P2P3P4P5P6P78 processes
How many processes do three fork() calls make?. Example: fork(); fork(); fork(); printf("hi\n");
  1. One process, P0, is about to run fork(); fork(); fork(); and then a printf.
  2. Fork 1: P0 runs it and gets a child, P1, so there are 2.
  3. Fork 2: each of the 2 processes alive runs it and gets a child, doubling the count to 4.
  4. Fork 3: each of the 4 processes alive runs it and gets a child, doubling the count to 8. The printf after them runs 8 times: 2³ = 8 processes, 7 of them new.

Two special cases:

  • A zombie is a child that has exited but whose parent has not yet called wait(). Its memory is freed, but its PCB entry stays so the parent can read the exit status.
  • An orphan is a child whose parent exited first. On Linux it is re-parented to init (PID 1) or a designated subreaper, which reaps it when it exits.

Threads

A thread is the unit the CPU actually schedules: a program counter, a set of registers and a stack. A process with several threads runs several paths through the same program at once.

one processshared by all threadscodeglobal dataheapopen filessignal handlersprivate to each threadthread 1PC, registersstackthread 2PC, registersstackthread 3PC, registersstackeach also has its own thread ID and thread-local storage
What threads share, and what each one owns. Every thread of a process runs the same code against the same globals, heap and open files, so they share data without any IPC — and can corrupt it without synchronization. Each thread has its own program counter, registers and stack, which is all a thread switch must change.

Why use threads: responsiveness (a UI thread stays live while a worker computes), resource sharing (threads share memory without any IPC), economy (creating a thread is far cheaper than a process) and scalability (threads run in parallel on multiple cores). The price is that shared memory needs synchronization, covered in Process Synchronization.

User threads vs kernel threads

AspectUser-level threadsKernel-level threads
Managed byA library in user spaceThe kernel
Kernel aware of themNo, it sees one processYes, it schedules each
Create and switchVery fast, no system callSlower, needs the kernel
One thread blocks in a system callWhole process may blockOnly that thread blocks
Run on several cores at onceNoYes

Multithreading models

User threads must eventually run on kernel threads. The mapping is the multithreading model:

user threadKkernel threadmany-to-oneKone blocks, all blockone-to-oneKKKKLinux, Windowsmany-to-manyKKGo, virtual threads
Mapping user threads onto kernel threads. Many-to-one runs all of a process's user threads on one kernel thread, one-to-one gives each user thread a kernel thread of its own, and many-to-many multiplexes many user threads onto fewer kernel threads, remapping them as threads block.
  • Many-to-one: switching is cheap, but one blocking call stops every thread and there is no parallelism. Early Java "green threads" worked this way.
  • One-to-one: true parallelism and independent blocking, at the cost of a kernel thread per user thread. Linux (NPTL pthreads) and Windows use this model.
  • Many-to-many: cheap threads with parallelism, but the runtime is harder to build. Go's goroutines and Java's virtual threads are runtime-level versions of it.
  • Two-level: many-to-many, but a chosen user thread can also be bound to its own kernel thread.

Process vs thread

AspectProcessThread
DefinitionA program in executionA path of execution within a process
Address spaceIts ownShared with the process's other threads
Creation costHigh (new address space, PCB)Low (stack and registers only)
Context switchSlower, changes address spaceFaster within one process
CommunicationNeeds IPC: pipes, sockets, shared memoryDirectly through shared memory
IsolationA crash usually affects only that processA crash can bring down the whole process
Kernel recordPCBThread control block (TCB)

Common mistakes

  • Saying threads share the stack. Each thread has its own stack; they share the heap and globals.
  • Answering that fork() returns the parent's PID to the child. The child gets 0; it calls getppid() for the parent.
  • Drawing a Waiting → Running arrow. A woken process always goes to Ready first.
  • Thinking a zombie still uses CPU or memory. Only its PCB entry and exit status remain.
  • Assuming more threads always means faster. Past the number of cores, extra CPU-bound threads add switching and contention.
  • Calling a mode switch (a system call) a context switch.

Interview questions

What is the difference between a process and a program? A program is a passive set of instructions in a file. A process is an active instance of it with a program counter, registers, an address space and resources. The same program can run as many independent processes.

What is stored in a PCB, and when is it used? The PID, state, saved program counter and registers, scheduling data, memory-management data, accounting data and open files. The kernel writes the CPU state into it when the process leaves the CPU and reads it back when the process is dispatched again.

How many processes does fork(); fork(); fork(); create? Eight processes in total, the original plus seven new ones, because each call doubles the processes that execute past it: 2³ = 8.

How many processes does fork() && fork() || fork(); create? Five in total. The parent's first fork is non-zero, so it runs the second fork and the || is skipped. The first child sees 0, skips the second fork and runs the third. The second child sees 0 from its fork and runs the third one too. That makes the parent plus four children.

What is a zombie process and how do you avoid one? A child that has exited but has not been reaped by its parent, so its entry stays in the process table. The parent avoids it by calling wait() or waitpid(), or by handling SIGCHLD; if the parent exits, init adopts and reaps the child.

Why is a thread switch cheaper than a process switch? Threads of one process share the address space, so the kernel does not change the page table base and the TLB and caches stay useful. Only the registers, program counter and stack pointer change.

Which multithreading model does Linux use? One-to-one: every pthread is a kernel-scheduled task created with the clone() system call, sharing the address space with the other threads of its process.

When would you use processes instead of threads? When isolation matters more than sharing: a crash in one process does not take down the others, and their memory cannot be corrupted by each other. Modern browsers run web pages in separate processes for this reason.

Next, read CPU Scheduling Algorithms, or test yourself with the Operating Systems (Basic) skill test.

Common questions

What is the difference between a program and a process?

A program is a passive file of instructions on disk. A process is that program loaded into memory and running, with a program counter, registers, a stack, a heap and resources such as open files. One program can run as many processes at once, for example several browser or terminal windows.

What is a PCB in an operating system?

The process control block is the kernel's record of one process. It holds the process ID, state, saved program counter and registers, scheduling information, memory-management information, accounting data and the list of open files. On a context switch the kernel saves the running process's CPU state into its PCB and loads the next one's.

What are the states of a process?

The five-state model has New, Ready, Running, Waiting (also called Blocked) and Terminated. A process is admitted to Ready, dispatched to Running, preempted back to Ready, moved to Waiting when it needs I/O or an event, returned to Ready when that completes, and moved to Terminated when it exits.

What is a context switch?

A context switch is the kernel saving the CPU state of the running process or thread into its control block and loading the saved state of another, so the CPU continues the second one where it left off. It is pure overhead, because no useful work is done during the switch, and it also leaves caches and the TLB cold.

What does fork() return?

fork() creates a child process that is a copy of the caller. It returns twice: 0 in the child, and the child's process ID in the parent. If no child could be created it returns -1 in the parent only. Both processes continue from the instruction after the call.

Why are threads called lightweight processes?

Because creating, switching and destroying a thread costs much less than doing the same for a process. Threads share their process's address space and resources, so the kernel does not set up a new page table or copy any memory, and switching between threads of one process keeps the same address space.

Test yourself

← Introduction to Operating Systems · CPU Scheduling Algorithms →