Loading content...
By the late 1950s, batch processing had solved the setup-time problem—computers no longer sat idle while programmers set up their jobs. But a new problem had become painfully clear: the CPU was still idle most of the time, waiting for I/O operations to complete.
Consider a typical batch job. It reads data from a tape drive, computes on that data, writes results, reads more data, computes again. The CPU sprints through the computation in microseconds, then waits milliseconds for the next tape read. In effect, the million-dollar processor was a sleeping giant, awake for tiny fractions of each second.
This chapter explores the solution to this crisis: multiprogramming—the revolutionary idea that changed computing forever.
By the end of this page, you will understand how multiprogramming works, why it dramatically improves CPU utilization, the memory management challenges it introduced, and how this paradigm shaped all subsequent operating system development. You'll see how the concepts of process management, scheduling, and memory protection were born from multiprogramming's requirements.
The insight behind multiprogramming is elegantly simple: when one job is waiting for I/O, switch to another job that can use the CPU.
Instead of loading one job into memory, load several. When Job A initiates a tape read and must wait 100 milliseconds, immediately switch to Job B. When Job B initiates a disk write, switch to Job C. By the time you've cycled through several jobs, Job A's tape read has completed, and you can resume it.
The Key Realization:
Different jobs have different resource needs at different times:
| Job Type | CPU Intensity | I/O Intensity | Example |
|---|---|---|---|
| Compute-Bound | High (80-95%) | Low (5-20%) | Scientific simulations, matrix computations |
| I/O-Bound | Low (5-20%) | High (80-95%) | Data processing, report generation |
| Balanced | Medium (40-60%) | Medium (40-60%) | Compilers, typical business applications |
By keeping a mix of job types in memory simultaneously, the operating system can ensure that there's almost always a job ready to use the CPU. When an I/O-bound job pauses to read data, a compute-bound job can step in and continue processing.
Mathematical Justification:
Let's model CPU utilization mathematically. Suppose each job spends a fraction p of its time waiting for I/O. If we have n jobs in memory, the probability that ALL jobs are waiting simultaneously is p^n.
CPU Utilization = 1 - p^n
For a typical batch job where p = 0.8 (80% I/O wait):
| Jobs in Memory | Probability All Waiting | CPU Utilization |
|---|---|---|
| 1 | 0.80 | 20% |
| 2 | 0.64 | 36% |
| 3 | 0.51 | 49% |
| 4 | 0.41 | 59% |
| 5 | 0.33 | 67% |
| 10 | 0.11 | 89% |
| 20 | 0.01 | 99% |
With just 5 jobs in memory, a system that was 20% utilized becomes 67% utilized—a 3x improvement. With 10 jobs, that becomes 89%. This mathematical relationship explains why computing centers invested heavily in memory to support more simultaneous jobs.
Multiprogramming introduced a fundamental new problem: how do you fit multiple programs into memory simultaneously, and how do you keep them from interfering with each other?
In batch processing, this was simple—only one job was in memory at a time, and it could use all available memory (except for the resident monitor). Multiprogramming shattered this simplicity.
Early Solutions: Fixed Partitions
The simplest approach divided memory into fixed-size partitions. Each partition could hold one job:
+------------------------+
| Operating System | (Always resident)
+------------------------+
| Partition 1 (64KB) | ← Job A
+------------------------+
| Partition 2 (128KB) | ← Job B
+------------------------+
| Partition 3 (256KB) | ← Job C
+------------------------+
| Partition 4 (256KB) | ← Empty (waiting)
+------------------------+
This approach had obvious limitations:
Variable Partitions
A more flexible approach allocated exactly as much memory as each job needed:
Initial State:
+------------------------+
| Operating System |
+------------------------+
| Job A (50KB) |
+------------------------+
| Job B (100KB) |
+------------------------+
| Job C (75KB) |
+------------------------+
| Free (275KB) |
+------------------------+
After Job B completes:
+------------------------+
| Operating System |
+------------------------+
| Job A (50KB) |
+------------------------+
| Free (100KB) | ← Hole left by Job B
+------------------------+
| Job C (75KB) |
+------------------------+
| Free (275KB) |
+------------------------+
Now the system has two free regions—a fragmented memory layout. If a new 200KB job arrives, it can't run even though 375KB total is free, because neither hole is large enough.
External fragmentation—having enough total free memory but not in contiguous chunks—became a major problem. Solutions included compaction (moving jobs to consolidate free space, expensive) and later, virtual memory (eliminated the problem entirely).
When a programmer writes code, they use symbolic addresses (variable names, function names). The compiler converts these to numeric addresses. But at what point are these addresses finalized?
This question became critical with multiprogramming. A program compiled to run at address 0x1000 couldn't simply be loaded at 0x3000—all its jumps, data references, and pointer calculations would be wrong.
Three Approaches to Address Binding:
The Base Register Solution:
Hardware provided a elegant solution: the base register (also called relocation register). Every memory address generated by a program would have the base register's value added to it automatically by hardware:
Physical Address = Base Register + Logical Address
This meant a program could be compiled assuming it starts at address 0, but actually loaded anywhere in memory. The OS simply sets the base register to the program's actual starting address.
| Logical Address | Base Register | Physical Address | Explanation |
|---|---|---|---|
| 0x0000 | 0x5000 | 0x5000 | Start of program maps to actual location |
| 0x0100 | 0x5000 | 0x5100 | Data section, 256 bytes into program |
| 0x0500 | 0x5000 | 0x5500 | Function code within program |
| 0x1000 | 0x5000 | 0x6000 | End of 4KB program |
The limit register complemented the base register. It contained the size of the program. Hardware would check every address: if (Logical Address > Limit Register), trap to OS with protection fault. This prevented programs from accessing memory outside their partition—a crucial security mechanism.
The Address Translation Flow:
┌─────────────────────────────────────────────────────────┐
│ CPU │
└─────────┬───────────────────────────────────────────────-┘
│ Logical Address (0x0500)
▼
┌─────────────────────────────────────────────────────────┐
│ Memory Management Unit (MMU) │
│ ┌─────────────┐ ┌───────────────┐ │
│ │Limit Reg: │ │ Base Reg: │ │
│ │ 0x1000 │ │ 0x5000 │ │
│ └─────────────┘ └───────────────┘ │
│ │
│ if (0x0500 < 0x1000): ✓ Valid │
│ Physical = 0x0500 + 0x5000 = 0x5500 │
│ else: │
│ TRAP → Protection Fault │
└─────────────────────────┬───────────────────────────────┘
│ Physical Address (0x5500)
▼
┌─────────────────────────────────────────────────────────┐
│ Physical Memory │
└─────────────────────────────────────────────────────────┘
This hardware-assisted address translation happened on every memory access—completely transparent to the running program. The program 'thought' it was accessing address 0x0500, but actually accessed 0x5500.
With multiple jobs competing for the CPU, a new responsibility fell upon the operating system: deciding which job runs next. This decision—CPU scheduling—became one of the most important OS functions.
When Does Scheduling Happen?
In multiprogramming systems, scheduling decisions occur when:
Non-Preemptive Scheduling:
Early multiprogramming systems used non-preemptive (also called cooperative) scheduling. A job kept the CPU until it voluntarily gave it up by:
This was simple to implement—context switches only occurred at well-defined points. However, it had significant downsides:
Early Scheduling Algorithms:
The simplest scheduling algorithm was First-Come, First-Served (FCFS): jobs ran in arrival order. When Job A blocked on I/O, the system would run Job B (the next in queue), then Job C, and so on.
FCFS worked reasonably well for multiprogramming because the goal was maximize CPU utilization, not minimize response time. As long as some job was always ready to run, the CPU stayed busy.
The Job Pool Concept:
Operators would maintain a job pool—a collection of jobs on disk waiting to be loaded into memory. When a job in memory completed, the OS would select the next job from the pool. Selection criteria included:
Multiprogramming introduced the distinction between long-term scheduling (which jobs to admit to memory) and short-term scheduling (which in-memory job runs next). Long-term scheduling controlled the 'degree of multiprogramming'—how many jobs were active simultaneously. This distinction persists in modern systems: admission control limits active processes, while the CPU scheduler picks which ready process actually runs.
Switching from one job to another isn't as simple as jumping to different code. Each job has state—register values, memory pointers, condition flags—that must be preserved so the job can resume exactly where it left off.
The context switch became a fundamental OS operation: saving one job's state and restoring another's.
What State Must Be Saved?
The Process Control Block (PCB):
To manage job state, the OS introduced a data structure that would become fundamental to all operating systems: the Process Control Block (in early systems, often called the Job Control Block or Task State Block).
Each job had an associated PCB containing:
1234567891011121314151617181920212223242526272829
struct ProcessControlBlock { // Job identification int job_id; char job_name[32]; int priority; // CPU state (saved on context switch) unsigned long program_counter; unsigned long stack_pointer; unsigned long registers[16]; unsigned int flags; // Memory management unsigned long base_register; unsigned long limit_register; // Process state enum { NEW, READY, RUNNING, WAITING, TERMINATED } state; // Accounting information unsigned long cpu_time_used; unsigned long io_time_used; time_t arrival_time; // Scheduling information struct ProcessControlBlock* next_in_queue; int io_devices_needed; unsigned long estimated_time;};The Context Switch Procedure:
1. Save outgoing job's PC to its PCB
2. Save outgoing job's registers to its PCB
3. Save outgoing job's stack pointer to its PCB
4. Update outgoing job's state (RUNNING → WAITING or READY)
5. Select incoming job (scheduling decision)
6. Restore incoming job's base/limit registers
7. Restore incoming job's general registers from PCB
8. Restore incoming job's stack pointer from PCB
9. Update incoming job's state (READY → RUNNING)
10. Jump to incoming job's PC (resume execution)
This entire sequence had to be performed atomically—partially switching would leave the system in an inconsistent state. Hardware support (interrupt disabling, atomic state save/restore instructions) made this possible.
Context switches aren't free. Each switch requires saving and restoring dozens of registers, updating data structures, and potentially flushing caches. In the 1960s, a context switch might take hundreds of microseconds. This overhead meant the OS couldn't switch too frequently, or it would spend all its time switching instead of executing jobs. Finding the right balance became an ongoing challenge.
Multiprogramming amplified a problem that existed in batch systems: device contention. If multiple jobs want to use the printer simultaneously, chaos ensues—output from different jobs would be interleaved nonsensically.
Spooling (Simultaneous Peripheral Operations On-Line) solved this by inserting a buffering layer between jobs and devices:
Job A output ─┐
Job B output ─┼─→ Disk (Spool area) ─→ Printer
Job C output ─┘ ↑
│
OS manages queue,
prints complete jobs
one at a time
Benefits of Spooling:
Device Drivers and Abstraction:
Multiprogramming also drove the creation of device drivers—OS components that hide hardware details from the rest of the system. Instead of jobs directly programming I/O controllers (dangerous and error-prone), they would request I/O through the OS:
Job → OS (system call) → Device Driver → Hardware Controller → Device
This abstraction meant:
Spooling remains ubiquitous. When you print a document, it goes to a print spool on disk before reaching the printer. Email is spooled through mail queues. Logging systems spool to disk before processing. Any time you see 'spool', you're seeing this 1960s innovation at work.
Several systems made multiprogramming practical and widespread. Understanding these systems illuminates how the concepts we've discussed were realized in practice.
IBM OS/360 (1964):
IBM's OS/360 was the first operating system designed to work across an entire product line—from small to large machines. It introduced or popularized many multiprogramming concepts:
OS/360's development was infamously troubled. Fred Brooks, the project manager, wrote 'The Mythical Man-Month' based on his experience. His observation that 'adding manpower to a late software project makes it later' (Brooks's Law) emerged from OS/360's struggles. The project's complexity exceeded all expectations—a lesson the industry continues to relearn.
EXEC (CDC):
Control Data Corporation's EXEC operating system for the CDC 6600 (1964) took a different approach. The CDC 6600 was designed for massive scientific computations, and EXEC emphasized:
MCP (Burroughs):
Burroughs' Master Control Program was revolutionary for being written in a high-level language (a variant of ALGOL) rather than assembly. This demonstrated that operating systems didn't have to be low-level artifacts—a controversial idea at the time that would eventually become standard.
Impact on Future Systems:
These multiprogramming systems established patterns that persist today:
| 1960s Concept | Modern Equivalent |
|---|---|
| Process Control Block (PCB) | task_struct (Linux), EPROCESS (Windows) |
| Variable Partitions | Heap memory allocation, malloc/free |
| Base/Limit Registers | Segment registers, virtual memory boundaries |
| Device Drivers | Kernel modules, driver frameworks |
| Job Queues | Process ready queues, scheduler run queues |
| Spooling | Print spools, mail queues, logging buffers |
| Priority Scheduling | Nice values, priority classes, CFS weights |
Multiprogramming was a watershed moment in computing history. It transformed operating systems from simple job sequencers into sophisticated resource managers. Let's consolidate the key insights:
What's Next:
Multiprogramming maximized CPU utilization for batch jobs—but it didn't help the programmer staring at their desk, waiting hours for results. The next evolution would address this human cost: time-sharing systems that gave users the illusion of having the computer to themselves, with interactive terminals and sub-second response times.
You now understand how multiprogramming revolutionized computing by overlapping computation with I/O operations. The concepts introduced—memory protection, CPU scheduling, context switching, and spooling—remain fundamental to every operating system. Next, we'll see how time-sharing systems brought interactivity to computing.