What Is a Process? — The Program Comes Alive
A program is the recipe on disk — a passive file of instructions. A process is the recipe being executed — with a program counter, registers, stack, heap, and a slice of the CPU. Same program can spawn many processes, just as one recipe can bake ten cakes in ten kitchens simultaneously.
In Galvin's words: "A process is a program in execution." But it is more than just code — a process is a container of context. The operating system must track every process's state, resources, and progress so it can pause one, run another, and resume the first exactly where it stopped.
Every process lives in memory across four regions: Text (the compiled code), Data (global and static variables), Heap (dynamically allocated memory — grows upward), and Stack (function calls and local variables — grows downward). The heap and stack grow toward each other; if they collide, you get a stack overflow.
Text and Data are fixed at load time. Heap grows via malloc/new; Stack grows on each function call.
Process States — The Life Cycle
A process is never just "running." It shifts through five states during its lifetime, driven by scheduler decisions, I/O events, and user actions. Galvin's state diagram is the foundation of everything that follows in scheduling and IPC.
exit()) or was forcibly killed. PCB briefly kept as a "zombie"
so the parent can read its exit status, then removed.
Arrows show legal transitions. A process cannot jump from Waiting straight to Running — it must return to Ready first.
The Process Control Block (PCB)
For every process, the OS keeps one struct in kernel memory called the
Process Control Block — Linux calls it task_struct, Windows calls it
EPROCESS. This is the process's identity card. When the CPU switches away from a process,
everything needed to resume it is saved here.
Run ps -ef to see every process's PID, PPID, state and CPU time. Run
cat /proc/<pid>/status to see a live snapshot of one PCB — Linux exposes the
task_struct through the /proc pseudo-filesystem.
Introduction to Process Scheduling
Modern computers run dozens to hundreds of processes but have only a handful of CPU cores. Process scheduling is how the OS decides which ready process gets the CPU next, and for how long. The goal is to keep the CPU busy (utilisation), finish many jobs per second (throughput), and keep interactive users happy (short response time).
A single process spends 60–80% of its time waiting for slow I/O. Rather than let the CPU sit idle during those waits, the OS keeps many processes in memory and switches to another one whenever the current process blocks. This is multiprogramming, and scheduling is what makes it work.
Scheduling Queues
The OS maintains several queues. Newly created processes enter the job queue. Processes in memory ready to run sit in the ready queue. Processes waiting for a specific device sit in a device queue (one per device).
A running process either finishes its slice (returns to ready), issues I/O (moves to a device queue), or terminates.
The Three Schedulers
A CPU-bound process (matrix multiply, video encode) uses long bursts of CPU with little I/O. An I/O-bound process (text editor, web server) makes frequent short CPU bursts between long I/O waits. A healthy system needs a mix of both — all CPU-bound leaves I/O devices idle; all I/O-bound leaves the CPU idle.
Context Switching — The Hidden Cost
When the CPU switches from process A to process B, it must save all of A's registers into A's PCB and load B's registers from B's PCB. This is pure overhead — no useful work is done during the switch. Times range from 1 to 1000 microseconds depending on hardware.
Operations on Processes
The OS exposes a small set of system calls that create, destroy, and manage processes. On UNIX/Linux
these are fork(), exec(), wait(), and exit().
On Windows the equivalents are CreateProcess(), WaitForSingleObject(),
and ExitProcess(). Every shell command, every double-clicked app goes through them.
Process Creation — The Parent/Child Tree
A process creates other processes. The creator is the parent; the created is the
child. Every process (except init/systemd, PID 1) has
exactly one parent, forming a tree. Run pstree on Linux to see it.
Each arrow = "parent created this child via fork()". Kill the parent and (usually) children become orphans, adopted by init.
fork() — Cloning a Process
fork() creates an exact copy of the calling process. After the call, two
processes exist: parent and child, both running the same code from the line just after fork.
The only difference is the return value — the child gets 0, the parent
gets the child's PID.
/* fork_demo.c — the classic Galvin example */
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
pid_t pid = fork(); // clones this process
if (pid < 0) {
perror("fork failed");
return 1;
}
else if (pid == 0) {
// Child process — fork returned 0 here
printf("Child: my PID is %d, parent is %d\n",
getpid(), getppid());
}
else {
// Parent process — fork returned the child's PID
printf("Parent: my PID is %d, child is %d\n",
getpid(), pid);
wait(NULL); // wait for child to finish
}
return 0;
}
Naïvely, fork() would duplicate every page of memory — expensive. Modern kernels use
Copy-On-Write (COW): parent and child share the same physical pages
marked read-only. Only when one writes does the kernel actually copy that single page.
This makes fork nearly free until real divergence happens.
exec() — Replacing the Program
fork() gives you a duplicate. To run a different program you follow it with
exec(), which replaces the current process's memory image with a new executable while
keeping the same PID. This is exactly how a shell runs commands.
/* shell.c — how "ls -l" runs from your terminal */
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// Child replaces itself with /bin/ls
execlp("/bin/ls", "ls", "-l", NULL);
perror("exec failed"); // only reached if exec fails
}
else {
// Parent waits for the child to finish
wait(NULL);
printf("Child done. Shell prompt again.\n");
}
return 0;
}
| Parent | Child |
|---|---|
| same code | same code |
| PID 4821 | PID 4822 |
| fork → 4822 | fork → 0 |
| Two identical processes | |
| Parent | Child |
|---|---|
| shell code | /bin/ls code |
| PID 4821 | PID 4822 |
| waits | runs new program |
| Parent + brand-new program | |
wait() and exit() — Cleanup
When a process finishes, it calls exit(status). Its PCB is kept as a zombie
until the parent calls wait() to collect the exit code. If a parent dies before its child,
the child becomes an orphan and is re-parented to init (PID 1) which
cleans it up.
| Term | What happened | Fix |
|---|---|---|
| Zombie | Child exited but parent never called wait() | Parent must call wait() — or handle SIGCHLD |
| Orphan | Parent died before child | init adopts and reaps automatically |
| Daemon | Deliberately orphaned to run in background | fork twice, session-leader tricks — intentional |
Process Termination
exit(0) or returns from main(). Returns success to parent.
exit(non-zero) to signal failure. Parent can inspect the code with WEXITSTATUS.
kill -9 sends SIGKILL). Cannot be caught.
Inter-Process Communication (IPC)
That's exactly IPC in Galvin's book: Shared Memory (the paper on the shared table — both processes read/write the same memory region) versus Message Passing (sending discrete messages through the kernel like a phone call).
Processes are, by design, isolated — one process cannot read another's memory (that's what segmentation faults protect). But cooperating processes need to exchange data. IPC is the set of mechanisms the OS provides for that exchange.
Independent vs Cooperating Processes
The Two IPC Models
Shared memory is faster (no kernel copy per exchange) but requires the programmer to handle synchronisation. Message passing is easier and works over networks.
Comparing the Two Models
| Aspect | Shared Memory | Message Passing |
|---|---|---|
| Speed | Very fast — direct memory access | Slower — kernel copy each message |
| Setup complexity | High — mmap, permissions, layout | Low — send() / recv() |
| Synchronisation | Manual — semaphores or mutexes | Built-in (send/receive blocks) |
| Best for | Large data, single machine | Small messages, distributed systems |
| OS examples | POSIX shm_open, System V shmget | Pipes, sockets, message queues |
Producer–Consumer — The Classic IPC Problem
Galvin uses this problem to illustrate cooperating processes. A producer generates items (say, print jobs) and puts them in a shared buffer. A consumer takes items out and processes them (prints). Two problems arise: the buffer might be full when the producer wants to add, or empty when the consumer wants to remove.
/* Bounded-buffer producer / consumer using shared memory */
#define BUFFER_SIZE 10
typedef struct { int data; } item;
item buffer[BUFFER_SIZE];
int in = 0; // next free slot
int out = 0; // next full slot
/* Producer: add item to buffer */
while (((in + 1) % BUFFER_SIZE) == out)
; // buffer full — busy wait
buffer[in] = next_produced;
in = (in + 1) % BUFFER_SIZE;
/* Consumer: remove item from buffer */
while (in == out)
; // buffer empty — busy wait
next_consumed = buffer[out];
out = (out + 1) % BUFFER_SIZE;
This code holds at most BUFFER_SIZE - 1 items (one slot always wasted so full ≠ empty).
Even so, on a real multiprocessor the reads and writes to in and out
can interleave dangerously. The fix — semaphores — is covered in the synchronisation
chapter. IPC without synchronisation is a bug factory.
IPC Mechanism 1 — Pipes
Pipes are the oldest UNIX IPC mechanism. A pipe is a one-way byte stream between two related
processes — one writes, the other reads. This is exactly the | in shell commands.
/* pipe_demo.c — parent sends a message to child */
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd[2]; // fd[0]=read end, fd[1]=write end
pipe(fd);
if (fork() == 0) {
// Child — read from pipe
close(fd[1]); // no writing
char buf[100];
int n = read(fd[0], buf, sizeof(buf));
buf[n] = '\0';
printf("Child got: %s\n", buf);
} else {
// Parent — write to pipe
close(fd[0]); // no reading
char *msg = "Hello, child!";
write(fd[1], msg, strlen(msg));
}
return 0;
}
| Type | Ordinary Pipe | Named Pipe (FIFO) |
|---|---|---|
| Lifetime | Dies with the processes | Persists on the filesystem |
| Direction | Unidirectional | Bidirectional (with care) |
| Relatedness | Only between related (fork'd) processes | Any two processes on the same host |
| API | pipe(fd) | mkfifo("/tmp/myfifo", 0666) |
IPC Mechanism 2 — Message Queues, Shared Memory, Sockets
msgsnd(), consumers msgrcv().
Persist beyond process lifetime until explicitly removed. Supports message types (priorities).
Shared Memory — Practical Example
/* Parent creates shared memory, child reads it */
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
int main() {
// Anonymous shared memory — inherited by child
char *shared = mmap(NULL, 4096,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (fork() == 0) {
sleep(1);
printf("Child reads: %s\n", shared);
} else {
strcpy(shared, "Message via shared memory");
wait(NULL);
}
return 0;
}
Synchronous vs Asynchronous Messaging
| Call | Behaviour |
|---|---|
| send() | Blocks until message received |
| recv() | Blocks until message arrives |
| Simple; caller waits | |
| Call | Behaviour |
|---|---|
| send() | Returns immediately (queued) |
| recv() | Returns immediately with data or "would block" |
| Concurrent; caller polls or is notified | |
With direct communication each process names the other explicitly —
send(P2, msg). With indirect communication messages go to a
named mailbox — send(mailboxA, msg), and any process with rights to
that mailbox can receive. Mailboxes decouple sender and receiver — the receiver could even
change without the sender knowing.
Real-World IPC in Action
cat log.txt | grep ERROR | wc -l — three processes, two pipes. Each stage's stdout is wired to the next stage's stdin via pipe() before exec().Common Pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
| Zombie explosion | Process table fills with defunct entries | Parent must call wait() or install SIGCHLD handler |
| Fork bomb | System freezes; recursive fork() | Set ulimits (ulimit -u); never fork in an unbounded loop |
| Shared memory race | Data corruption, random crashes | Always guard with semaphores or mutexes |
| Deadlock in send() | Both processes waiting forever | Use non-blocking sends or timeouts |
| Pipe with no reader | Writer receives SIGPIPE and dies | Handle SIGPIPE or check write() return |
| Lost signals | Signal sent while previous still pending | Signals are not queued — use signalfd or message queues instead |
Golden Rules
fork() returns twice — 0 in the child, child's PID in the parent.
Always check for the -1 error case, then branch on 0 vs positive.
wait() for every child, or it
installs a SIGCHLD handler that reaps them. Zombies consume PIDs — eventually the table fills.