Operating Systems 📂 Introduction · 4 of 5 43 min read

Process Concept — Scheduling, Operations & Inter-Process Communication

A Galvin-style deep dive into the process concept in operating systems. Covers what a process really is (program vs process, memory layout, PCB), the five process states, scheduling queues and the three schedulers, context switching, operations on processes (fork, exec, wait, exit) with C code, and inter-process communication via shared memory, pipes, message queues, sockets and signals — with SVG diagrams and real-world examples.

Section 01

What Is a Process? — The Program Comes Alive

The Recipe vs. The Cook Baking a Cake
Imagine a printed recipe for chocolate cake sitting in a drawer — flour, sugar, oven at 180°C, 30 minutes. That recipe is static text. It does nothing on its own. Now imagine a cook picks it up, grabs a mixing bowl, preheats the oven, and starts measuring ingredients. Suddenly the recipe is alive — it has a state (which step we're on), resources (bowl, oven, hands), and a purpose.

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.

💾
The Four Sections of a Process in Memory

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.

📈 Memory Layout of a Process
TEXT (Code) DATA (globals) HEAP grows ↓ downward in address (upward on diagram) STACK 0x0000 0xFFFF heap grows ↓ stack grows ↑

Text and Data are fixed at load time. Heap grows via malloc/new; Stack grows on each function call.


Section 02

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.

🏃
New
just created
The OS has just created the process control block (PCB), allocated a PID, and is preparing to admit it to the ready queue. No CPU time yet.
Ready
waiting for CPU
Loaded into memory, all resources granted, sitting in the ready queue and waiting for the scheduler to hand it a CPU core. Many processes are usually here at once.
💫
Running
on the CPU now
Currently executing instructions on a CPU core. On a single-core machine only one process is here at a time; on N cores, up to N.
😴
Waiting (Blocked)
waiting for I/O or event
Has requested something slow — disk read, network packet, semaphore — and cannot continue. The scheduler skips it until the event arrives.
🏁
Terminated
exit or killed
Finished (called exit()) or was forcibly killed. PCB briefly kept as a "zombie" so the parent can read its exit status, then removed.
🔑
Key Insight
only 1 running per core
The illusion of many programs running at once comes from the OS switching processes hundreds of times per second — so fast you cannot perceive the gaps.
🔄 Process State Transition Diagram
NEW READY RUNNING WAITING TERMINATED admit dispatch interrupt / quantum I/O wait I/O done exit

Arrows show legal transitions. A process cannot jump from Waiting straight to Running — it must return to Ready first.


Section 03

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.

📄 What's Inside a PCB
PID
Process ID — a unique integer. Also parent PID (PPID), user ID (UID), group ID (GID).
State
Current state: New / Ready / Running / Waiting / Terminated.
PC
Program Counter — address of the next instruction to execute after resumption.
Regs
CPU register contents (RAX, RBX, RSP, RBP, flags…) saved on every context switch.
Mem
Memory management info — base register, limit register, page table pointer.
I/O
Open file descriptors, allocated devices, pending I/O requests.
Acct
Accounting — CPU time used, real time, priority, scheduling parameters.
🔑
Try It Yourself — Linux

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.


Section 04

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).

📈
Why Scheduling Exists — Multiprogramming

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).

🛠️ The Scheduling Queue Diagram
Ready Queue CPU dispatch time slice expired Disk I/O Queue Network I/O Queue I/O completes → back to Ready Queue

A running process either finishes its slice (returns to ready), issues I/O (moves to a device queue), or terminates.

The Three Schedulers

📚
Long-Term Scheduler
job scheduler
Decides which jobs from the job queue are admitted into memory. Runs seconds or minutes apart. Controls the degree of multiprogramming — too many processes and thrashing begins.
Short-Term Scheduler
CPU scheduler
Picks the next process from the ready queue and hands it the CPU. Runs every few milliseconds. Must be extremely fast — its own cost is pure overhead.
💾
Medium-Term Scheduler
swapper
Present in swapping systems. Temporarily removes ("swaps out") a process to disk when memory is tight, and swaps it back in later. Reduces the degree of multiprogramming.
⚠️
CPU-Bound vs I/O-Bound

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.

🔄 Anatomy of a Context Switch
Step 1
Interrupt or system call transfers control to the kernel.
Step 2
Save process A's CPU registers (PC, general-purpose regs, flags) into A's PCB.
Step 3
Scheduler picks process B from ready queue.
Step 4
Load B's registers from B's PCB. Switch page table (memory map).
Step 5
Return from kernel mode. Process B resumes at its saved PC.

Section 05

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.

🌲 Example Process Tree
init (PID 1) login (450) sshd (521) systemd (300) bash (612) bash (720) vim (890) gcc (925)

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;
}
OUTPUT (order may vary)
Parent: my PID is 4821, child is 4822 Child: my PID is 4822, parent is 4821
💡
Copy-On-Write — The Real Magic

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;
}
📌 fork() alone
ParentChild
same codesame code
PID 4821PID 4822
fork → 4822fork → 0
Two identical processes
📌 fork() + exec()
ParentChild
shell code/bin/ls code
PID 4821PID 4822
waitsruns 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.

TermWhat happenedFix
ZombieChild exited but parent never called wait()Parent must call wait() — or handle SIGCHLD
OrphanParent died before childinit adopts and reaps automatically
DaemonDeliberately orphaned to run in backgroundfork twice, session-leader tricks — intentional

Process Termination

🏁 How Processes End
Normal
The process calls exit(0) or returns from main(). Returns success to parent.
Error
The process calls exit(non-zero) to signal failure. Parent can inspect the code with WEXITSTATUS.
Signal
Another process sent a fatal signal (e.g. kill -9 sends SIGKILL). Cannot be caught.
Parent-kill
Parent may terminate its children — cascading termination — when the parent itself dies (e.g. VMS).

Section 06

Inter-Process Communication (IPC)

Two Isolated Offices That Need to Talk
Imagine two employees in soundproof offices with locked doors. They cannot see or hear each other, yet they must collaborate on a report. They have two choices: shove notes under the shared door (fast, they see the same paper) or use the office phone (slower, but works even between different buildings).

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

🔒
Independent
no shared state
A process that doesn't affect and isn't affected by any other. It runs in its own bubble. Example: a background disk defragmenter unrelated to your word processor.
👥
Cooperating
shares data / signals
Two or more processes that exchange data. Example: a web browser and its GPU helper process, or a compiler and a linker in a pipeline.
🛠️
Why Cooperate?
4 reasons
Information sharing (many editing one file), computation speedup (parallel workers), modularity (small trusted services), convenience (a user editing, compiling and printing at once).

The Two IPC Models

📈 Shared Memory vs Message Passing
Shared Memory Process A Process B Shared Region both read/write same memory kernel not involved after setup Message Passing Process A Process B Kernel send() → kernel copies → recv() works across machines too

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

AspectShared MemoryMessage Passing
SpeedVery fast — direct memory accessSlower — kernel copy each message
Setup complexityHigh — mmap, permissions, layoutLow — send() / recv()
SynchronisationManual — semaphores or mutexesBuilt-in (send/receive blocks)
Best forLarge data, single machineSmall messages, distributed systems
OS examplesPOSIX shm_open, System V shmgetPipes, sockets, message queues

Section 07

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;
⚠️
The Race Condition Waiting to Happen

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.


Section 08

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;
}
OUTPUT
Child got: Hello, child!
TypeOrdinary PipeNamed Pipe (FIFO)
LifetimeDies with the processesPersists on the filesystem
DirectionUnidirectionalBidirectional (with care)
RelatednessOnly between related (fork'd) processesAny two processes on the same host
APIpipe(fd)mkfifo("/tmp/myfifo", 0666)

Section 09

IPC Mechanism 2 — Message Queues, Shared Memory, Sockets

📫
Message Queues
Kernel-maintained linked list of messages. Producers msgsnd(), consumers msgrcv(). Persist beyond process lifetime until explicitly removed. Supports message types (priorities).
POSIX mq_open / System V msgget
💾
Shared Memory Segment
One kernel-allocated region mapped into multiple processes' address spaces. Once mapped, access is at pointer speed. But you must synchronise with semaphores.
shm_open + mmap
🌐
Sockets
Endpoints for network communication. Same API works within a machine (UNIX sockets) or across the internet (TCP/UDP). The foundation of every client-server system.
socket / bind / listen / accept
📱
Signals
Software interrupts. One process sends a signal (SIGINT, SIGTERM, SIGUSR1) to another; the receiver runs a handler. Simple notification only — no data payload beyond the signal number.
kill(pid, SIGUSR1)
📡
Remote Procedure Call
Function-call abstraction over the network. Caller invokes a "stub" that marshals arguments, sends them, waits for the reply. Foundation of gRPC, XML-RPC, distributed systems.
RPC / gRPC / DCOM
🔒
Semaphores
Not a data-transfer channel but a synchronisation primitive used alongside shared memory. Prevents two processes from writing the same slot simultaneously.
sem_open / P and V operations

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;
}
OUTPUT
Child reads: Message via shared memory

Section 10

Synchronous vs Asynchronous Messaging

⏳ Blocking (Synchronous)
CallBehaviour
send()Blocks until message received
recv()Blocks until message arrives
Simple; caller waits
🚀 Non-blocking (Asynchronous)
CallBehaviour
send()Returns immediately (queued)
recv()Returns immediately with data or "would block"
Concurrent; caller polls or is notified
📰
Direct vs Indirect Naming

With direct communication each process names the other explicitly — send(P2, msg). With indirect communication messages go to a named mailboxsend(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.


Section 11

Real-World IPC in Action

01
Chrome's Multi-Process Architecture
Each browser tab is a separate process. They communicate with the main browser process using named pipes (Mojo IPC on Chromium). A crashed tab cannot bring down the whole browser.
02
The UNIX Shell Pipeline
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().
03
Postgres Database
A single postmaster process forks a new backend for every client connection. Backends share a huge shared-memory buffer pool for the disk cache — synchronised via semaphores and lightweight locks.
04
Android Binder
Every Android app is an isolated process. All communication between apps and system services goes through Binder — a custom message-passing IPC in the kernel — so a broken app cannot corrupt others.
05
Redis / Memcached Clients
Application processes on many machines talk to a Redis server via TCP sockets. This is IPC extended over the network — same abstractions, just a longer wire.

Section 12

Common Pitfalls

PitfallSymptomFix
Zombie explosionProcess table fills with defunct entriesParent must call wait() or install SIGCHLD handler
Fork bombSystem freezes; recursive fork()Set ulimits (ulimit -u); never fork in an unbounded loop
Shared memory raceData corruption, random crashesAlways guard with semaphores or mutexes
Deadlock in send()Both processes waiting foreverUse non-blocking sends or timeouts
Pipe with no readerWriter receives SIGPIPE and diesHandle SIGPIPE or check write() return
Lost signalsSignal sent while previous still pendingSignals are not queued — use signalfd or message queues instead

Section 13

Golden Rules

🏆 Process, Scheduling & IPC — Non-Negotiable Rules
1
A program is passive (bytes on disk); a process is active (a program in execution with its own PC, registers, stack, heap and PCB). Never confuse them.
2
Every ready process waits in the ready queue; every I/O-blocked process waits in a device queue. The short-term scheduler is the hot path — keep it O(1) or O(log n) in Galvin's terms.
3
Context switches are pure overhead. Minimise them by picking the right time quantum (Round Robin) and priority scheme. Each switch = save + reload of the whole register set.
4
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.
5
Never leave a zombie behind. Either the parent calls wait() for every child, or it installs a SIGCHLD handler that reaps them. Zombies consume PIDs — eventually the table fills.
6
Shared memory is the fastest IPC but the most dangerous. Every shared write must be guarded by a mutex or semaphore, or your producer–consumer becomes a corrupt-data generator.
7
Prefer message passing when clarity and portability matter (works across machines). Prefer shared memory when raw throughput on one machine is critical.
8
Signals are notifications, not messages. They cannot carry payloads reliably and are not queued. For anything richer, use message queues or sockets.