A process is a program in execution. It is the unit the operating system schedules, protects and accounts for: every browser tab, every terminal command and every server you run is one or more processes. Almost every OS interview spends time here because processes connect everything else: memory layout, scheduling, system calls and communication.
Interviewers commonly probe a predictable set of topics: the difference between a process and a program, what lives in each memory segment, what a process control block (PCB) holds, the state diagram, what a context switch costs, and above all fork() puzzles such as "how many times does this print?". This lesson covers each one with runnable C code and step-by-step reasoning. All C examples compile with gcc on Linux or macOS.
Process versus program
A program is a passive file on disk: machine instructions plus initial data, for example /usr/bin/python3. A process is an active instance of that program: the code loaded into memory plus its current state, such as register values, the program counter (the address of the next instruction), its stack, its heap, its open files and its process ID.
One program can have many processes. If you open three terminals and run python3 in each, you get three processes from one program file. Each has its own variables and its own position in the code, and they cannot see each other's memory.
| Program | Process |
|---|---|
| Passive: a file on disk | Active: running or ready to run |
| Exists until deleted | Exists from creation until it terminates |
| Contains code and initial data | Contains code, data, heap, stack, registers, open files |
| No OS resources held | Holds a PID, memory, file descriptors, CPU time |
| One copy | Many processes can run the same program |
Memory layout of a process
Each process gets its own virtual address space: a private range of addresses that the OS and hardware map onto physical RAM. (How that mapping works is the subject of memory management.) The address space is divided into regions, or segments, with different purposes.
high addresses
+---------------------------+
| kernel space | not accessible from user mode
+---------------------------+
| command-line args, env |
+---------------------------+
| stack | grows DOWN; one frame per call:
| | | locals, return address, saved regs
| v |
| |
| (memory-mapped region: | shared libraries, mmap files,
| libc.so, mmap'd files) | thread stacks
| |
| ^ |
| | |
| heap | grows UP; malloc / new
+---------------------------+
| BSS | uninitialized / zero globals
+---------------------------+
| data | initialized globals, statics
+---------------------------+
| text (code) | machine instructions, read-only
+---------------------------+
low addresses
Here is what each segment holds:
- Text (code) segment: the compiled machine instructions. It is marked read-only and executable, so a bug cannot overwrite the code, and it can be shared between all processes running the same program.
- Data segment: global and
staticvariables that have an explicit non-zero initial value, such asint limit = 100;. Their values are stored in the executable file. - BSS segment: global and
staticvariables that are uninitialized or initialized to zero, such asint count;orstatic char buf[4096];. The file only records the size; the OS fills the region with zeros at load time. This keeps executables small. (BSS stands for "block started by symbol", a historical assembler term.) - Heap: memory allocated at runtime with
malloc/freein C ornewin C++ and Java. It grows upward. The allocator gets more memory from the kernel with thebrkormmapsystem calls. - Stack: one stack frame per active function call, holding local variables, function arguments (those not passed in registers), the return address and saved registers. It grows downward and shrinks automatically when functions return. Each thread has its own stack.
A small program shows where each variable lands:
#include <stdio.h>
#include <stdlib.h>
int initialized = 7; /* data segment */
int zeroed; /* BSS segment */
static char buffer[1024]; /* BSS segment */
int main(void) { /* main's code: text segment */
int local = 3; /* stack */
static int calls = 1; /* data segment, despite being in a function */
int *dynamic = malloc(sizeof *dynamic); /* pointer on stack, int on heap */
*dynamic = local + initialized + zeroed + calls + buffer[0];
printf("%d\n", *dynamic); /* prints 11 */
free(dynamic);
return 0;
}
On Linux you can view a running process's regions with cat /proc/<pid>/maps, and the size command shows the text, data and BSS sizes of an executable.
Common mistake
"A static variable inside a function lives on the stack." It does not. static gives it static storage duration: it lives in the data or BSS segment and keeps its value between calls. Only ordinary (automatic) local variables live on the stack.
Two classic failures map directly to this layout. Stack overflow happens when the stack grows past its limit, typically through very deep or infinite recursion; the default limit on many Linux systems is 8 MB (ulimit -s). A memory leak happens on the heap when allocated memory is never freed.
The process control block (PCB)
The kernel needs to keep track of every process. It does so with a data structure called the process control block (PCB), also called a task control block. In Linux, the PCB is struct task_struct; in Windows, it is the EPROCESS structure (with per-thread ETHREAD structures).
A PCB contains:
| Field | What it stores | Why it is needed |
|---|---|---|
| Process ID (PID), parent PID | Unique number, parent's number | Identification, wait, signals |
| Process state | New, ready, running, blocked... | Scheduler decisions |
| Program counter | Address of next instruction | Resuming after a switch |
| CPU registers | General-purpose, stack pointer, flags | Resuming exactly where it stopped |
| Scheduling information | Priority, time used, queue pointers | Choosing who runs next |
| Memory-management information | Page-table pointer, segment limits | Setting up its address space |
| Open files | File-descriptor table | read/write on descriptors |
| Accounting | CPU time used, start time, limits | top, time, resource limits |
| Credentials | User ID, group ID, capabilities | Permission checks |
| Signal information | Pending and blocked signals, handlers | Signal delivery |
The kernel links PCBs into lists or trees: a ready queue, wait queues for each device or event, and the parent-child tree. Saying "the scheduler moves PCBs between queues" is an accurate short description of what scheduling looks like inside the kernel.
Process states
During its life a process moves between states. The basic model has five.
admitted dispatch
+-----+ +---------+ ----------------> +---------+ exit +------------+
| New | ---> | Ready | | Running | ------> | Terminated |
+-----+ +---------+ <---------------- +---------+ +------------+
^ interrupt / |
| time slice expired | I/O or event wait
| v
| I/O or event done +-----------+
+----------------------- | Blocked |
| (Waiting) |
+-----------+
- New: the process is being created; the PCB is set up but it is not yet allowed to run.
- Ready: it could run right now and is waiting only for a CPU.
- Running: its instructions are executing on a CPU. On a machine with N cores, at most N processes are running at any instant.
- Blocked (Waiting): it cannot continue until some event happens, such as disk I/O completing, a network packet arriving, a lock becoming free or a timer expiring.
- Terminated (Exit): it has finished; the kernel keeps a little information until the parent collects its exit status.
The transitions are worth knowing by name:
- Ready to Running (dispatch): the scheduler picks the process.
- Running to Ready (preemption): the time slice expires or a higher-priority process becomes ready.
- Running to Blocked: the process makes a request it must wait for, like
readon a socket with no data. - Blocked to Ready: the event happens. Note that it goes to Ready, not straight to Running; it must wait its turn for a CPU.
Common mistake
There is no transition from Blocked directly to Running, and none from Ready to Blocked. A process can only start waiting on an event while it is running (it has to execute the request), and when the event completes it rejoins the ready queue.
The seven-state model with suspension
When memory is full, the OS may swap out (suspend) a whole process to disk to make room. That adds two states:
- Ready/Suspend: ready to run, but its memory is on disk. It must be swapped in first.
- Blocked/Suspend: waiting for an event and swapped out.
+-------+ +--------+ +------------+
| New | ---> | Ready | --- dispatch -> | Running | --> Exit
+-------+ +--------+ <-- timeout --- +------------+
| ^ | ^ |
admit to| activate| |suspend | wait
suspend | | v | event v
| +---------------+ occurs +------------+
+--> | Ready/Suspend | | | Blocked |
+---------------+ +--------- +------------+
^ | ^
| event occurs suspend | | activate
| v |
| +-----------------+
+----------------- | Blocked/Suspend |
+-----------------+
The new transitions:
- Blocked to Blocked/Suspend: the OS swaps out a waiting process to free memory. A blocked process is the best candidate because it cannot use the CPU anyway.
- Blocked/Suspend to Ready/Suspend: the event the process was waiting for happens while it is still on disk.
- Ready/Suspend to Ready (activate): the OS brings it back into memory.
- Ready to Ready/Suspend: rarer; done when memory pressure is severe, or to make room for a higher-priority blocked process.
- New to Ready/Suspend: a new process may be admitted directly to the swapped-out state if memory is tight.
Modern general-purpose systems usually page out individual pages rather than swapping entire processes (see virtual memory), but the seven-state model is a standard textbook and exam question, and the stopped state you get with Ctrl+Z in a shell is a close real-world relative.
On Linux, ps shows the real states with letters: R (running or runnable), S (interruptible sleep, the usual blocked state), D (uninterruptible sleep, typically waiting on disk I/O), T (stopped, for example by Ctrl+Z), and Z (zombie, explained below). Note that Linux's R covers both Ready and Running.
Schedulers: long-term, short-term, medium-term
Textbooks describe three schedulers by how often they run and what they decide.
| Scheduler | Also called | Decides | Runs how often | Controls |
|---|---|---|---|---|
| Long-term | Job scheduler | Which new jobs are admitted into memory | Seconds to minutes | Degree of multiprogramming (how many processes are in memory) |
| Short-term | CPU scheduler | Which ready process gets the CPU next | Every few milliseconds | CPU allocation |
| Medium-term | Swapper | Which processes to swap out to disk and back | Occasionally, under memory pressure | Mix and number of active processes |
- The long-term scheduler matters most in batch systems. A good one admits a balanced mix of I/O-bound processes (that spend most time waiting for I/O, with short CPU bursts) and CPU-bound processes (long CPU bursts). Too many I/O-bound jobs leave the CPU idle; too many CPU-bound jobs leave devices idle. In interactive systems like Linux and Windows there is effectively no long-term scheduler: every program you start is admitted.
- The short-term scheduler runs very often, so it must be fast. Its algorithms are the topic of the CPU scheduling lesson.
- The medium-term scheduler implements the suspend states above. It reduces the degree of multiprogramming when memory is overcommitted, which helps fix thrashing.
The dispatcher is the piece of code that carries out the short-term scheduler's choice: it performs the context switch, switches to user mode and jumps to the right instruction. The time it takes is called dispatch latency.
Context switching
A context switch is the act of saving the state of the currently running process and restoring the state of another one, so the CPU can continue the second process exactly where it left off.
When does it happen?
- The running process makes a blocking system call (it waits for I/O, sleeps or waits on a lock).
- A timer interrupt shows its time slice is used up.
- A higher-priority process becomes ready (for example, its I/O completes) and preempts the running one.
- The running process exits.
The steps
Suppose process A is running and the timer interrupt fires.
- The CPU switches to kernel mode and saves A's user-mode registers (including the program counter and stack pointer) on A's kernel stack.
- The kernel's timer handler runs and decides that A's time slice is over. It calls the scheduler.
- The scheduler updates A's PCB (state becomes Ready, CPU time used is updated) and puts A back in the ready queue.
- The scheduler picks process B from the ready queue.
- The kernel saves A's remaining kernel-side context (callee-saved registers and kernel stack pointer) into A's PCB.
- It switches the address space: it loads B's page-table base into the memory-management unit (on x86, writing
CR3). This is skipped when switching between two threads of the same process. - It restores B's kernel stack pointer and registers from B's PCB, and B's state becomes Running.
- It returns from the interrupt into B's user code, restoring B's user registers.
Process A Kernel Process B
running --- timer interrupt -->
save A's regs -> PCB(A)
schedule(): pick B
switch page tables
load PCB(B) -> regs
--- return to user --> running
<---- (later) interrupt / syscall in B, and back
Why context switches cost more than they look
The direct cost is the work above: entering the kernel, running the scheduler, saving and loading registers, switching page tables. On modern hardware this is typically on the order of a few microseconds.
The indirect cost is often larger and harder to measure:
- Cache pollution: B's data is probably not in the CPU caches, so B suffers cache misses until it warms them up again, and when A returns it suffers the same.
- TLB flush: the TLB (translation lookaside buffer) caches virtual-to-physical address translations. Switching address spaces can invalidate it, causing extra page-table walks. Modern CPUs reduce this with tagged TLB entries (PCID on x86, ASID on ARM).
- Pipeline and branch-predictor state is also disturbed.
Context switches are pure overhead: no user work happens during them. That is why a too-small Round Robin quantum hurts throughput, and why switching between threads of the same process (no address-space change) is cheaper than switching between processes. On Linux, vmstat 1 shows context switches per second in the cs column, and pidstat -w breaks them down per process into voluntary (the process blocked) and involuntary (it was preempted).
Interview tip
When asked about context switch overhead, mention both parts: "the direct cost of saving and restoring registers and switching page tables, and the indirect cost of cold caches and TLB misses afterwards, which is often the larger part." Then add that thread switches within one process skip the page-table switch.
Creating processes: fork, exec, wait, exit
Unix-like systems create processes with a pair of calls that do surprising things. Understanding them precisely is the most commonly tested practical skill in this topic.
fork()
fork() creates a new process, the child, that is an almost exact copy of the caller, the parent. The child gets a copy of the parent's address space (code, data, heap, stack), a copy of its open file descriptors (both point to the same open files), and starts executing at the same place: the return from fork().
The only way they can tell themselves apart is the return value:
- In the parent,
fork()returns the child's PID (a positive number). - In the child,
fork()returns 0. - On failure, it returns -1 in the parent and no child is created.
Copying a whole address space would be slow, so modern kernels use copy-on-write (COW): parent and child initially share the same physical pages, marked read-only. Only when one of them writes to a page does the kernel copy that single page. Most children call exec almost immediately, so most pages are never copied.
exec()
The exec family (execve is the system call; execl, execlp, execv, execvp and others are library wrappers) replaces the current process's program with a new one. The PID stays the same and open file descriptors stay open (unless marked close-on-exec), but code, data, heap and stack are thrown away and replaced. If exec succeeds, it never returns, because the code that called it no longer exists.
wait() and exit()
exit(status)ends the process. The kernel frees its memory and closes its files, but keeps a small record with the exit status until the parent collects it.wait(&status)blocks the parent until any child terminates and returns that child's PID;waitpid(pid, &status, options)waits for a specific child or polls withWNOHANG. Macros such asWIFEXITEDandWEXITSTATUSdecode the status.
The fork-exec-wait pattern
This is how a shell runs every command you type:
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
}
if (pid == 0) {
printf("child: pid=%d parent=%d\n", getpid(), getppid());
fflush(stdout); /* exec discards unflushed buffers */
execlp("echo", "echo", "hello from exec", (char *)NULL);
perror("execlp"); /* reached only if exec failed */
exit(1);
}
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status))
printf("parent: child %d exited with %d\n", pid, WEXITSTATUS(status));
return 0;
}
Output (PIDs will differ):
child: pid=4122 parent=4121
hello from exec
parent: child 4122 exited with 0
Why separate fork and exec instead of one "spawn" call? Because the gap between them lets the child adjust its own environment before the new program starts: redirect standard output to a file (ls > out.txt), connect pipes (ls | wc -l), change directory or drop privileges. Windows takes the other approach with a single CreateProcess call that takes many parameters, and POSIX also offers posix_spawn for the common case.
Common mistake
Forgetting fflush(stdout) before exec. When output goes to a pipe or file, printf output sits in a user-space buffer; exec replaces the whole memory image, so the buffered text is silently lost. In testing, the first line of the program above disappeared when output was piped and the fflush was missing.
Parent and child have separate memory
After fork, a variable has the same virtual address in both processes but lives in different physical memory once written:
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int x = 10;
int main(void) {
pid_t pid = fork();
if (pid == 0) {
x += 5;
printf("child: x=%d &x=%p\n", x, (void *)&x);
return 0;
}
wait(NULL);
printf("parent: x=%d &x=%p\n", x, (void *)&x);
return 0;
}
child: x=15 &x=0x104d1c000
parent: x=10 &x=0x104d1c000
The same address, two different values. That is virtual memory at work: each process has its own mapping from virtual to physical addresses. Interviewers love this question because the "same address, different value" result surprises people who think addresses are physical.
fork() puzzles, solved step by step
The trick to every fork puzzle: after each fork, there are two processes running the rest of the code. Draw a tree, and count.
Puzzle 1: three forks in a row
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void) {
fork();
fork();
fork();
printf("hi\n");
while (wait(NULL) > 0)
;
return 0;
}
How many times is hi printed?
Step by step:
- Before the first
fork: 1 process. - After the first
fork: 2 processes, both continue to the secondfork. - After the second: each of the 2 forks, giving 4.
- After the third: each of the 4 forks, giving 8.
- All 8 execute
printf.
Answer: 8. In general, n sequential forks give 2^n processes, of which 2^n - 1 are newly created children.
P
+-------+-------+ fork #1
P C1
+---+---+ +---+---+ fork #2
P C2 C1 C3
/ \ / \ / \ / \ fork #3
P C4 C2 C5 C1 C6 C3 C7 -> 8 processes print "hi"
Puzzle 2: fork with && and ||
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void) {
if ((fork() && fork()) || fork())
;
printf("x\n");
while (wait(NULL) > 0)
;
return 0;
}
How many times is x printed? (The parentheses match C's precedence; && binds tighter than ||.)
Remember short-circuit evaluation: A && B skips B if A is false (zero); A || B skips B if A is true (non-zero). And fork() returns non-zero (true) in the parent and 0 (false) in the child.
- The original process P runs the first
fork(). It creates child C1.- In P, the result is non-zero (true), so P evaluates the second
fork(). - In C1, the result is 0 (false), so
fork() && fork()is false without running the second fork. C1 must evaluate the right side of||: it runs the thirdfork(), creating C3. Neither C1 nor C3 forks again.
- In P, the result is non-zero (true), so P evaluates the second
- P runs the second
fork(), creating C2.- In P, the result is true, so
(true && true)is true and||short-circuits. P is done. - In C2, the result is 0, so the
&&is false and C2 runs the thirdfork(), creating C4.
- In P, the result is true, so
- Count the processes: P, C1, C2, C3, C4.
Answer: 5.
P: fork1 -> true, fork2 -> true (done)
|-- C1: fork1 -> 0, so run fork3 (done)
| `-- C3: fork3 -> 0 (done)
`-- C2: fork2 -> 0, so run fork3 (done)
`-- C4: fork3 -> 0 (done)
Puzzle 3: fork in a loop
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void) {
for (int i = 0; i < 3; i++) {
if (fork() == 0)
printf("child at i=%d\n", i);
}
while (wait(NULL) > 0)
;
return 0;
}
How many lines are printed when run in a terminal? Note that children do not break out of the loop; they keep looping and forking too.
- At
i = 0, 1 process forks; 1 new child printsi=0. Now 2 processes. - At
i = 1, both of them fork; 2 new children printi=1. Now 4. - At
i = 2, all 4 fork; 4 new children printi=2. Now 8.
Total lines: 1 + 2 + 4 = 7, and 8 processes exist in total.
Now the twist that strong candidates know. Run the same program with output piped, as in ./a.out | cat, and you get more lines: 12 in testing. When stdout is a terminal it is line-buffered, so each printf ending in \n is written immediately. When stdout is a pipe or file it is fully buffered, so the text stays in the process's user-space buffer, and that buffer is copied into every child created by later forks. Each copy is flushed at exit. The fix is fflush(stdout) before fork, or writing with write(2).
Puzzle 4: buffered output duplicated
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void) {
printf("hello ");
fork();
printf("world\n");
while (wait(NULL) > 0)
;
return 0;
}
Output, even on a terminal:
hello world
hello world
"hello " has no newline, so it is still sitting in the buffer when fork copies the process. Both processes then append "world\n" and flush. Many candidates expect hello world world; the buffer explanation is what the interviewer wants to hear.
Interview tip
For any fork puzzle, say your method out loud: "After each fork the number of processes doubles for the code that follows; I track the return value, which is 0 in the child, and apply short-circuit rules." Then draw the tree. If the code prints without a newline before a fork, or output is piped, mention stdio buffering.
Zombie and orphan processes
Zombies
When a child exits, the kernel frees almost everything but keeps its PCB entry with the exit status, so the parent can later find out how the child ended. A process that has exited but whose parent has not yet called wait is a zombie (state Z, shown as <defunct> in ps).
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
printf("child %d exiting\n", getpid());
exit(0);
}
printf("parent %d sleeping; run: ps -o pid,stat,comm -p %d\n", getpid(), pid);
fflush(stdout);
sleep(30); /* child is a zombie for these 30 seconds */
return 0;
}
A zombie uses no CPU and almost no memory, but it holds a PID and a process-table slot. A long-running server that forks children and never reaps them can accumulate thousands of zombies and eventually fail to create new processes. Fixes:
- Call
wait/waitpidfor each child, often from aSIGCHLDhandler usingwaitpid(-1, &status, WNOHANG)in a loop. - Set the
SIGCHLDdisposition toSIG_IGN, which on Linux tells the kernel to reap children automatically. - If the parent itself exits, the zombies are re-parented to init, which reaps them.
You cannot kill a zombie with kill -9: it is already dead. You can only make its parent reap it, or kill the parent.
Orphans
An orphan is a child whose parent exits first. The child keeps running normally. The kernel re-parents it to PID 1 (init/systemd), or on Linux to a designated "subreaper" process if one is set, and that new parent will reap it when it exits.
#include <stdio.h>
#include <unistd.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
printf("child: parent is %d\n", getppid());
fflush(stdout);
sleep(1);
printf("child: parent is now %d\n", getppid());
return 0;
}
printf("parent %d exiting\n", getpid());
return 0;
}
parent 51046 exiting
child: parent is 51046
child: parent is now 1
(On a Linux desktop the new parent may be a user-session subreaper such as systemd --user rather than 1.) Orphans are deliberately created by daemons: a background service forks, the parent exits, and the child continues detached from the terminal.
| Zombie | Orphan | |
|---|---|---|
| Is it running? | No, it has exited | Yes, still running |
| Cause | Parent did not call wait | Parent exited first |
| Who fixes it | Parent calls wait; or init after parent dies | Adopted by init (or a subreaper) |
| Harmful? | In large numbers: exhausts PIDs | No; normal for daemons |
Inter-process communication (IPC)
Processes have separate address spaces, so they cannot simply share variables. Inter-process communication (IPC) is the set of mechanisms the OS provides for processes to exchange data and coordinate. There are two broad models:
- Message passing: the kernel carries data from one process to another (pipes, message queues, sockets). Easier to use correctly; each message costs system calls and copying.
- Shared memory: the kernel maps the same physical pages into both processes. After setup, reads and writes are ordinary memory accesses with no kernel involvement, which is the fastest option, but the processes must synchronize themselves (see synchronization).
Pipes
A pipe is a one-way byte stream with a read end and a write end, held in a kernel buffer (64 KiB by default on Linux). An anonymous pipe has no name, so it can only connect related processes, typically a parent and child that inherit its descriptors across fork. The shell's | operator is built on it.
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void) {
int fd[2]; /* fd[0] = read end, fd[1] = write end */
if (pipe(fd) == -1) {
perror("pipe");
return 1;
}
pid_t pid = fork();
if (pid == 0) { /* child: reader */
close(fd[1]);
char buf[64];
ssize_t n = read(fd[0], buf, sizeof buf - 1);
if (n > 0) {
buf[n] = '\0';
printf("child read: %s\n", buf);
}
close(fd[0]);
return 0;
}
close(fd[0]); /* parent: writer */
const char *msg = "ping";
write(fd[1], msg, strlen(msg));
close(fd[1]);
wait(NULL);
return 0;
}
Output: child read: ping. Rules worth knowing: read blocks while the pipe is empty and returns 0 (end of file) once all write ends are closed, which is why each side closes the end it does not use. write blocks when the pipe is full, and writing to a pipe with no readers raises SIGPIPE.
Named pipes (FIFOs)
A named pipe or FIFO is a pipe with a name in the file system, created with mkfifo. Unrelated processes can open it by path. Data still flows through the kernel, never to disk.
$ mkfifo /tmp/chan
$ cat /tmp/chan & # reader blocks until a writer opens it
$ echo hello > /tmp/chan # reader prints: hello
Shared memory
Shared memory maps one region into several processes. POSIX offers shm_open + mmap; System V offers shmget/shmat. For a parent and child, an anonymous shared mapping is the simplest form:
#include <stdio.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void) {
/* One int that parent and child both map: writes are visible to both. */
int *counter = mmap(NULL, sizeof *counter, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (counter == MAP_FAILED) {
perror("mmap");
return 1;
}
*counter = 0;
if (fork() == 0) {
*counter = 42;
return 0;
}
wait(NULL);
printf("parent sees %d\n", *counter);
munmap(counter, sizeof *counter);
return 0;
}
This prints parent sees 42. Compare with the earlier global-variable example, where the parent still saw 10: MAP_SHARED is what turns off copy-on-write for this region. Real programs pair shared memory with a semaphore or mutex placed in the shared region.
Message queues
A message queue is a kernel-managed list of discrete messages, each with boundaries preserved (unlike a pipe's byte stream) and, in POSIX queues (mq_open, mq_send, mq_receive), a priority. System V queues (msgget, msgsnd, msgrcv) let a receiver select messages by type. Useful when you want structured messages without building your own framing.
Sockets
A socket is an endpoint for bidirectional communication. Unix domain sockets (AF_UNIX) connect processes on the same machine through a file-system path and are what many local services use, for example the Docker daemon and database clients connecting locally. Network sockets (AF_INET, AF_INET6) use TCP or UDP and work across machines. Sockets are the most flexible IPC mechanism and the only one in this list that spans machines; see transport layer for TCP and UDP.
Signals
A signal is a small asynchronous notification sent to a process, identified only by a number: SIGINT (Ctrl+C), SIGTERM (polite termination request, the default for kill), SIGKILL (forced kill), SIGSEGV (invalid memory access), SIGCHLD (a child changed state), SIGSTOP/SIGCONT. A process can ignore most signals or install a handler, except SIGKILL and SIGSTOP, which can be neither caught nor ignored.
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
static volatile sig_atomic_t got_signal = 0;
static void on_sigint(int signo) {
(void)signo;
got_signal = 1; /* only set a flag: handlers must stay tiny */
}
int main(void) {
struct sigaction sa = {0};
sa.sa_handler = on_sigint;
sigemptyset(&sa.sa_mask);
sigaction(SIGINT, &sa, NULL);
printf("pid %d: press Ctrl+C\n", getpid());
fflush(stdout);
while (!got_signal)
pause(); /* sleep until any signal arrives */
printf("caught SIGINT, cleaning up\n");
return 0;
}
A handler can interrupt the program at any instruction, so it should only call async-signal-safe functions (printf and malloc are not), which is why it just sets a volatile sig_atomic_t flag. Signals carry almost no data, so they are for notification, not data transfer.
Comparison table
| Mechanism | Direction | Related processes only? | Data model | Speed | Typical use |
|---|---|---|---|---|---|
| Anonymous pipe | One-way | Yes (inherited fd) | Byte stream | Good | Shell pipelines, parent-child |
| Named pipe (FIFO) | One-way (open two for both) | No | Byte stream | Good | Simple local producer/consumer |
| Shared memory | Both | No | Raw memory | Fastest (no copy after setup) | Large data, high throughput; needs locks |
| Message queue | Both | No | Discrete messages, priorities | Good | Structured messages between local services |
| Unix domain socket | Two-way | No | Stream or datagram | Good | Local client-server (databases, daemons) |
| Network socket | Two-way | No, even across machines | Stream (TCP) or datagram (UDP) | Network-bound | Distributed systems |
| Signal | One-way | No (needs permission) | A number only | Fast, tiny | Notifications, termination, child exit |
Interview tip
If asked "which IPC is fastest?", say shared memory, then explain why (no kernel copy after setup) and the catch (you must synchronize access yourself). If asked which to pick for two programs on different machines, the answer is sockets. Showing the trade-off matters more than the single answer.
Interview questions
Q1. What is the difference between a process and a program?
A program is a passive executable file on disk. A process is a running instance of it: the loaded code plus its dynamic state, including registers, program counter, stack, heap, open files and a PID. One program can be run as many independent processes at the same time.
Q2. Describe the memory layout of a process.
From low to high addresses: the text segment with read-only code, the data segment with initialized globals and statics, BSS with zero or uninitialized globals, then the heap growing upward for dynamic allocation. Near the top is the stack, growing downward, holding one frame per active call; shared libraries and mmap regions sit between heap and stack. The kernel's memory is mapped above and is inaccessible from user mode.
Q3. Where are global, static, local and malloc'd variables stored?
Initialized globals and statics go in the data segment, and zero or uninitialized ones go in BSS. Ordinary local variables and parameters live on the stack. Memory returned by malloc or new is on the heap, while the pointer variable holding its address is wherever that variable was declared, often the stack.
Q4. What is a PCB and what does it contain?
The process control block is the kernel's record for a process, task_struct in Linux. It holds the PID and parent PID, state, saved program counter and registers, scheduling information such as priority, memory-management information like the page-table pointer, the open-file table, credentials, signal state and accounting data. The kernel saves into and restores from it on every context switch.
Q5. Explain the process state diagram.
A process is created in New, admitted to Ready, and dispatched to Running by the scheduler. From Running it can be preempted back to Ready, block on I/O or an event and move to Blocked, or exit to Terminated. When its event completes it moves from Blocked to Ready, never straight to Running. The seven-state model adds Ready/Suspend and Blocked/Suspend for processes swapped out to disk.
Q6. What happens during a context switch, and why is it expensive?
The kernel saves the current process's registers and program counter into its PCB, chooses the next process, switches the address space by loading its page-table base, restores its saved registers and returns to user mode. The direct cost is the kernel entry and the save/restore work. The indirect cost, often larger, is the cache and TLB misses the new process suffers because its data is not cached.
Q7. What are the long-term, short-term and medium-term schedulers?
The long-term or job scheduler decides which jobs are admitted to memory, controlling the degree of multiprogramming; it hardly exists on interactive systems. The short-term or CPU scheduler picks the next ready process to run every few milliseconds. The medium-term scheduler swaps processes out to disk and back in to relieve memory pressure.
Q8. What does fork() return and why?
In the parent it returns the child's PID, so the parent can track and wait for it; in the child it returns 0, since the child can find its own PID with getpid() and its parent's with getppid(); on failure it returns -1. Both processes continue from the same point, and the return value is the only way the code can tell them apart.
Q9. How does fork() avoid copying the entire address space?
With copy-on-write. Parent and child initially share all physical pages, marked read-only. When either one writes to a page, the CPU raises a page fault and the kernel copies just that page for the writer. Since most children call exec soon after, most pages are never copied.
Q10. What is the difference between fork() and exec()?
fork() creates a new process that is a copy of the caller. exec() does not create a process; it replaces the program running in the current process with a new one, keeping the PID and open descriptors, and does not return on success. Shells use them together, with the gap between them used to set up redirection and pipes.
Q11. What is a zombie process, and how do you get rid of zombies?
A zombie is a process that has exited but whose exit status has not yet been collected by its parent with wait. It uses no CPU but holds a PID and a process-table entry. You cannot kill it because it is already dead; the parent must reap it, for example in a SIGCHLD handler with waitpid(-1, &status, WNOHANG), or the parent must exit so init adopts and reaps it.
Q12. What is an orphan process?
An orphan is a still-running process whose parent has already terminated. The kernel re-parents it to init (PID 1) or a subreaper, which will reap it when it exits. Daemons are intentionally orphaned so they run detached from the terminal that launched them.
Q13. How many processes does for (i = 0; i < n; i++) fork(); create?
2^n processes exist at the end including the original, so 2^n - 1 new ones are created. Each iteration doubles the number of processes because every existing process, including children, runs the remaining iterations.
Q14. Which IPC mechanism is fastest, and what is the catch?
Shared memory, because after the region is mapped, data moves through ordinary loads and stores with no system calls or kernel copies. The catch is synchronization: the processes must coordinate access with semaphores, mutexes or atomic operations, or they will race. Message-passing mechanisms like pipes are slower but synchronize naturally because reads block until data arrives.
Q15. Why can't SIGKILL be caught?
So that the system always has a guaranteed way to stop a process, regardless of what the process does. If a program could catch or ignore SIGKILL, a buggy or malicious one could make itself unkillable. SIGSTOP is uncatchable for the same reason; SIGTERM is the catchable signal used for graceful shutdown.
Key takeaways
- A program is a file; a process is a running instance with its own address space, registers, PID and resources.
- A process's memory is text, data, BSS, heap (grows up) and stack (grows down), plus mapped libraries; static locals live in data or BSS, not the stack.
- The PCB is the kernel's record of a process, saved and restored on each context switch.
- Know the five-state diagram cold, and the seven-state version with Ready/Suspend and Blocked/Suspend; Blocked always returns to Ready.
- Context switches have a direct cost (saving state, switching page tables) and a larger indirect cost (cold caches and TLB).
forkcopies the process (with copy-on-write), returning the child's PID in the parent and 0 in the child;execreplaces the program;waitreaps the child.- For fork puzzles, draw the process tree, use the return value and short-circuit rules, and watch for stdio buffering.
- Zombies are dead children not yet reaped; orphans are live children adopted by init.
- IPC options range from pipes and FIFOs to message queues, shared memory (fastest, needs synchronization), sockets (cross-machine) and signals (notification only).
Next lesson
Continue with Threads.

