I/O subsystem, buffering and spooling
How the operating system handles input and output: controllers, drivers and interrupt handlers, memory-mapped and isolated I/O and how it is protected, polling against interrupts against DMA, the services of the kernel I/O subsystem (scheduling, buffering, caching, spooling), blocking, non-blocking and asynchronous I/O, the time saved by single and double buffering, and CPU utilisation when processes wait for I/O.
Why it exists
Programs need the keyboard, the disk, the network and the screen, but devices differ wildly (a keyboard delivers a byte now and then, a disk a block at a time, a network card thousands of packets a second), they are slow next to the CPU, and a program must not be allowed to use them as it likes. The I/O subsystem is the part of the operating system that hides the differences behind a small set of calls, shares the devices safely, overlaps the slow device with useful computation, and protects them. GATE asks what happens in which order when a device interrupts, how I/O is protected, what buffering, caching and spooling are and what they gain, how I/O requests interact with scheduling (a process that does I/O leaves the CPU and later returns to the ready queue), and how much a given buffering scheme saves.
Prerequisites
The hardware side (device registers, polling, interrupts and DMA) is on the I/O interface, interrupts and DMA pages; this page is about what the operating system does with it.
Precise definitions
Hardware parts. A device controller (or adapter) is the electronics that operates a device and has registers (status, command, data) that the CPU reads and writes. A device driver is the kernel module that knows one kind of controller and gives the kernel a uniform interface to it. An interrupt handler (interrupt service routine) is the kernel code that runs when the device signals completion.
How the CPU reaches the registers.
- Isolated (port-mapped) I/O: the registers have their own address space and special instructions (
IN,OUT). These instructions are privileged, so a user program that tries them traps: that is the protection. - Memory-mapped I/O: the registers appear at ordinary memory addresses, read and written with ordinary load and store instructions; no special I/O instruction exists to make privileged. Protection is then done by the operating system with the memory protection hardware: the pages that hold device registers are mapped only into the kernel's address space (a user program that touches them takes a protection fault), unless the OS deliberately maps them for one trusted process.
Three ways to move data.
| Method | Who moves the data | CPU cost | Best for |
|---|---|---|---|
| Polling (programmed I/O) | the CPU, in a loop that tests the status register | busy for the whole transfer | very fast devices and short waits |
| Interrupt-driven | the CPU, one unit per interrupt | one interrupt (save, handle, restore) per unit | slow, sporadic devices |
| DMA | the DMA controller, straight to memory | a set-up and one interrupt at the end | block devices, fast transfers |
What happens at an interrupt, in order: (1) the device raises the interrupt line; (2) the CPU finishes the current instruction; (3) it saves the state of the running process (program counter and status word) on the control stack; (4) it loads the new program counter from the interrupt vector for that device; (5) the interrupt service routine runs; (6) the saved state is restored and the interrupted process resumes (or another is chosen). The state is saved before the new program counter is loaded and the old one is restored last.
An I/O request through the layers. A process calls read (a system call); the kernel checks the arguments and whether the data is already in a buffer or cache; if not, it passes the request to the driver, which starts the device and the process is put to wait (moved from running to the device's wait queue); when the device finishes it interrupts, the handler completes the request and moves the process back to the ready queue: a process that does I/O enters the ready queue once more for every I/O that finishes.
Kinds of device and I/O call. Block devices (disks) transfer fixed-size blocks and allow random access; character devices (keyboard, serial line) deliver a stream of bytes; network devices have their own interface (sockets). A call can be:
- blocking: the process is suspended until the I/O completes;
- non-blocking: the call returns at once with whatever can be done now (possibly nothing, as an error such as "would block");
- asynchronous: the call returns at once and the whole transfer happens later; the process is told when it is complete (a flag, signal or callback).
Services of the kernel I/O subsystem. I/O scheduling (ordering the requests of each device, as in disk scheduling); buffering; caching; spooling and device reservation; error handling (retry, report); protection (users reach devices only through system calls).
Buffering. A buffer is a memory area that holds data while it moves between two parties. It is used (1) to match speeds (a fast producer and a slow consumer), (2) to match transfer sizes (a network message in packets, a file read in blocks), (3) to keep copy semantics (what is written is the data at the time of the call, even if the program changes its own copy before the device has taken it). Suppose each block takes to read from the device into a system buffer, to move from the system buffer to the process's area, and for the process to work on. The time per block in the steady state is
- no buffering (read, then compute): ;
- single buffer (the next read overlaps the processing, but the buffer must be emptied first): ;
- double buffer (the device fills one buffer while the process uses the other): .
A circular buffer is a ring of more than two buffers, which smooths out bursts. Adding buffers cannot make the whole faster than the slower of the device and the process.
Caching, buffering and spooling. A cache holds a copy of data that also exists elsewhere, kept for speed (the original is still on disk). A buffer may hold the only copy of data in transit. Spooling (simultaneous peripheral operations on-line) stores the output for a device that cannot be shared, typically a printer, in a file on disk: each process writes its output to its own spool file, and a daemon sends the files to the printer one at a time. Several processes then appear to use the printer at once, nobody holds the printer while doing other work, and one source of deadlock disappears.
CPU utilisation and the degree of multiprogramming. If each of processes in memory waits for I/O a fraction of its time, independently of the others, the CPU is idle only when all are waiting: .
How it works
Interrupt sequence. The processor-level view: the device interrupts, the current instruction completes, the state is saved, the handler runs, the state is restored.
An interrupt: finish, save, vector, service, return: step 1
Machine run, not typed(main program running)| addr | decimal | meaning | |
|---|---|---|---|
| 064 | main: ADD R0,#1 | PC | |
| 065 | main: ADD R0,#1 | ||
| 066 | main: ADD R0,#1 | ||
| 004 | 200 | vector table entry 4 (keyboard) | |
| 0C8 | ISR: PUSH R0 | ||
| 0C9 | ISR: IN R0,[DEV] | ||
| 0CA | ISR: ST R0,[300] | ||
| 0CB | ISR: POP R0 | ||
| 0CC | ISR: IRET | ||
| 12C | . | input buffer | |
| 3E7 | . | stack | |
| 3E6 | . | stack | |
| 3E5 | . | stack |
Step 1
The main program is adding 1 to R0 (R0 = 5). The interrupt-enable flag I is 1, so interrupts are accepted. The stack grows downward from 1000. Entry 4 of the interrupt vector table holds the address of the keyboard routine (200). A key is about to be pressed: the keyboard will put its code (66) in its data register DEV and raise an interrupt request.
DMA. The CPU sets up the controller and goes on; the controller moves the block and interrupts once at the end.
A DMA transfer of 4 words by cycle stealing: step 1
Machine run, not typed(CPU running a program)| addr | decimal | meaning | |
|---|---|---|---|
| 200 | . | memory buffer | |
| 201 | . | memory buffer | |
| 202 | . | memory buffer | |
| 203 | . | memory buffer |
Step 1
A disk controller must deliver 4 words to memory at 512..515. With programmed I/O or interrupts the CPU would move every word. With DMA the CPU only starts the transfer. The DMA controller (DMAC) has an address register AR, a word count WC and a control register; it can take over the buses (the CPU is the bus master now).
What each method costs the CPU. A 4096-byte block from a slow device and from a fast one. A fast device cannot be served by an interrupt per byte at all: the CPU is overloaded.
CPU time to transfer a 4096-byte block: polling, an interrupt per byte, or DMA: start
Computed by a script, not typed| device | transfer time | CPU time, polling | CPU time, interrupt per byte (8 µs each) | CPU time, DMA (30 µs set-up + 8 µs interrupt) |
|---|---|---|---|---|
| slow device: one byte every 50 µs (20 KB/s) | ||||
| fast device: one byte every 0.5 µs (2 MB/s) |
Step 0
Interrupts are the right tool for slow, sporadic devices; fast devices that move blocks need DMA. The 8 µs, 30 µs figures are illustrative round numbers.
What buffering buys. Ten blocks, three combinations of times. One buffer saves the overlap of reading and computing; a second buffer also hides the move; when the process is the bottleneck, nothing helps.
Time to input and process 10 blocks with no buffer, one buffer and two buffers: start
Computed by a script, not typed| parameters (time units) | no buffer | single buffer | double buffer |
|---|---|---|---|
| T = 10, C = 5, M = 1 | |||
| T = 4, C = 10, M = 1 | |||
| T = 8, C = 8, M = 2 |
Step 0
Event simulation of the device, the system buffers and the process (scripts/os_io.py). Per block in the steady state: no buffer T + C, one buffer max(T, C) + M, two buffers max(T, C + M). The second line shows that when the process is the bottleneck, a second buffer gains nothing.
Why the operating system keeps several processes. While one process waits for its I/O, another can use the CPU.
CPU utilisation as the degree of multiprogramming grows: 1 - p^n: start
Computed by a script, not typed| degree of multiprogramming n | utilisation, p = 0.5 | utilisation, p = 0.8 | simulated, p = 0.8 |
|---|---|---|---|
| 1 process in memory | |||
| 2 processes in memory | |||
| 3 processes in memory | |||
| 4 processes in memory | |||
| 5 processes in memory | |||
| 6 processes in memory | |||
| 7 processes in memory | |||
| 8 processes in memory |
Step 0
p is the fraction of its time a process spends waiting for I/O; the processes are assumed independent, which is a model, not a law (real processes queue behind the same disk).
Worked examples
EX 1Order of events at an interrupt, and who handles the protection
EasyA device controller interrupts while process L runs. The correct order is: the processor finishes the current instruction, pushes the status of L on the control stack, loads the new program counter from the vector, executes the interrupt service routine, and finally pops the status of L. The state of L must be saved before the program counter is overwritten, and restored only after the routine has finished. For protection: a CPU with explicit I/O instructions makes them privileged; a CPU with memory-mapped I/O has no such instruction, so the operating system protects the device registers by mapping their addresses only into the kernel's space with the memory protection hardware. A user process cannot reach the device except through system calls.
EX 2Buffering: how much does each scheme save?
MediumA program reads 10 blocks and processes each. A block takes to arrive, to copy into the program's area and to process. The C program simulates the device, the buffers and the process for no buffer, one buffer and two buffers, for two parameter sets.
#include <stdio.h>
#define MAXN 64
static int max(int a, int b) { return a > b ? a : b; }
/* time at which block n has been processed; buffers = 0 (read straight into the program's area), 1 or 2 */
static int total(int n, int T, int C, int M, int buffers) {
if (buffers == 0) return n * (T + C);
int read_end[MAXN] = {0}, move_end[MAXN] = {0}, comp_end[MAXN] = {0};
for (int i = 1; i <= n; i++) {
int start;
if (buffers == 1) start = move_end[i - 1]; /* the buffer is free once the previous block has been copied out */
else start = max(read_end[i - 1], i >= 2 ? move_end[i - 2] : 0); /* the device fills the buffer that is free, one block at a time */
read_end[i] = start + T;
move_end[i] = max(read_end[i], comp_end[i - 1]) + M; /* the program is ready and the block has arrived */
comp_end[i] = move_end[i] + C;
}
return comp_end[n];
}
int main(void) {
int cases[2][3] = {{10, 5, 1}, {4, 10, 1}}; /* T, C, M */
for (int k = 0; k < 2; k++) {
int T = cases[k][0], C = cases[k][1], M = cases[k][2];
printf("T=%d C=%d M=%d, 10 blocks: no buffer %d, single %d, double %d; per block %d, %d, %d\n", T, C, M,
total(10, T, C, M, 0), total(10, T, C, M, 1), total(10, T, C, M, 2),
total(11, T, C, M, 0) - total(10, T, C, M, 0), total(11, T, C, M, 1) - total(10, T, C, M, 1), total(11, T, C, M, 2) - total(10, T, C, M, 2));
}
return 0;
}T=10 C=5 M=1, 10 blocks: no buffer 150, single 115, double 106; per block 15, 11, 10
T=4 C=10 M=1, 10 blocks: no buffer 140, single 114, double 114; per block 14, 11, 11Reading it. First set: without a buffer every block costs ; with one buffer reading the next block overlaps the processing, and the cost per block is ; with two buffers it is : the device is now the bottleneck, so it cannot get better. Without a buffer the total is exactly 10 times 15. With buffers the totals (115 and 106) are a little more than 10 times the per-block time (110 and 100), because the first block has to arrive before the overlap starts. Second set: the process is the bottleneck ( against ); one buffer gives and two buffers give : a second buffer gains nothing.
EX 3Blocking, non-blocking, and how many calls a buffer saves
MediumA pipe is a small character device inside the kernel. The program makes the read end non-blocking and reads from the empty pipe, then writes five bytes and reads again; then it counts how many write system calls are needed to send 64 KiB to a file with different chunk sizes.
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
static long calls_for(int fd, size_t total, size_t chunk) {
static char buf[65536];
long calls = 0;
while (total > 0) {
size_t k = total < chunk ? total : chunk;
ssize_t w = write(fd, buf, k);
if (w < 0) return -1;
total -= (size_t)w;
calls++;
}
return calls;
}
int main(void) {
int p[2];
if (pipe(p) != 0) return 1;
if (fcntl(p[0], F_SETFL, fcntl(p[0], F_GETFL) | O_NONBLOCK) != 0) return 1;
char buf[16];
ssize_t n = read(p[0], buf, sizeof buf);
printf("empty pipe, non-blocking read: returned %zd, error is 'would block': %s\n", n, (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) ? "yes" : "no");
if (write(p[1], "hello", 5) != 5) return 1;
n = read(p[0], buf, sizeof buf);
printf("after writing 5 bytes, the same read returned %zd\n", n);
int fd = open("/dev/null", O_WRONLY);
if (fd < 0) return 1;
size_t chunk[4] = {1, 512, 4096, 65536};
for (int i = 0; i < 4; i++) printf("sending 65536 bytes in chunks of %5zu bytes: %ld write calls\n", chunk[i], calls_for(fd, 65536, chunk[i]));
close(fd);
return 0;
}empty pipe, non-blocking read: returned -1, error is 'would block': yes
after writing 5 bytes, the same read returned 5
sending 65536 bytes in chunks of 1 bytes: 65536 write calls
sending 65536 bytes in chunks of 512 bytes: 128 write calls
sending 65536 bytes in chunks of 4096 bytes: 16 write calls
sending 65536 bytes in chunks of 65536 bytes: 1 write callsReading it. A blocking read on the empty pipe would have put the process to sleep; the non-blocking one returns at once with an error that means "nothing now, ask again". After the write it returns the five bytes. Every write is a system call, with the trap into the kernel and the checks that this costs, so collecting the output in a buffer and writing it in blocks of 4096 bytes needs 16 calls where one byte at a time needs 65536: this is the reason that the C library buffers printf output, and why a buffer is useful even when the device is not slow.
EX 4CPU utilisation with several processes
MediumEach process in memory spends 80 per cent of its time waiting for I/O (). One process uses the CPU per cent of the time. With processes the CPU is idle only if all wait: , which is per cent for 3 processes and per cent for 8 (the table above, with a simulation that agrees). To reach 90 per cent we need , so : eleven processes ( per cent is not enough, per cent is). The formula assumes the processes wait independently; if they all queue for one disk, the real figure is lower.
EX 5Which statement is true?
GATE levelSpooling lets several processes share a printer. True, by giving each its own file and printing the files in turn. A cache and a buffer are the same thing. False: a cache holds a copy of data that exists elsewhere, a buffer can hold the only copy. With one buffer, the next block can be read while the previous one is being processed, but the buffer must first be copied out. True (the in ). Double buffering always halves the time. False: it gives per block, which is at best : no better than the slower of the device and the process. DMA is useful for a keyboard. No: a keyboard delivers a byte at a time slowly, and interrupts handle that comfortably; DMA is for block transfers. A process that has finished an I/O returns to the ready queue, not directly to the CPU. True: the scheduler then chooses. In the question about the number of times a process enters the ready queue because of I/O, count one entry for each I/O that completes.
Cheat box
Cheat box
- Interrupt order: finish instruction save state load new PC run ISR restore.
- Isolated I/O: privileged
IN/OUT. Memory-mapped I/O: no special instruction; the OS protects the registers with memory protection (kernel-only mapping). - Polling: CPU busy. Interrupts: per unit. DMA: set-up plus one interrupt; for blocks.
- Per block: no buffer ; single ; double . Never better than the slower side.
- Cache: copy of data elsewhere. Buffer: data in transit. Spool: output file for a device that cannot be shared.
- Blocking: sleeps until done. Non-blocking: returns at once with what is possible. Asynchronous: returns at once, completes later.
- CPU utilisation .
- After an I/O completes the process goes to the ready queue.
Traps and GATE patterns
Connections
Across subjects
The interrupt and the DMA transfer are hardware mechanisms (interrupts, DMA); here the operating system builds buffering, caching and spooling on top of them. The request enters the kernel through a system call; the process waits and later returns to the ready queue as in the process state diagram. The disk is the device with the longest delays and has its own request ordering in disk scheduling. Spooling removes the "no sharing" condition for printers and so is one of the answers to deadlock.
PYQs
5 previous year questions from your book. I re-solved each one myself (3 by running or simulating, 2 by reasoning only); the book's key is shown next to my answer. All PYQs
Three processes A, B and C each execute a loop of 100 iterations. In each iteration of the loop, a process performs a single computation that requires tc CPU milliseconds and then initiates a single I/O operation that lasts for tio milliseconds. It is assumed that the computer where the processes execute has sufficient number of I/O devices and the OS of the computer assigns different I/O devices to each process. Also, the scheduling overhead of the OS is negligible. The processes have the following characteristics: Process id TC Tio A 100 ms 500 ms B 350 ms 500 ms C 200 ms 500 ms The processes A, B, and C are started at times 0, 5 and 10 milliseconds respectively, in a pure time sharing system (round robin scheduling) that uses a time slice of 50 milliseconds. The time in milliseconds at which process C would complete its first I/O operation is ___________.
Consider three processes, all arriving at time zero, with total execution time of 10, 20, and 30 units, respectively. Each process spends the first 20% of execution time doing I/O, the next 70% of time doing computation, and the last 10% of time doing I/O again. The operating system uses a shortest remaining compute time first scheduling algorithm and schedules a new process either when the running process gets blocked on I/O or when the running process finishes its compute burst. Assume that all I/O operations can be overlapped as much as possible. For what percentage of time does the CPU remain idle?
- (a)0%
- (b)10.6%
- (c)30.0%
- (d)89.4%
Suppose in a multiprogramming environment, the
following C program segment is executed. A process
goes into I/O queue whenever an I/O related
operation is performed. Assume that there will always
be a context switch whenever a process requests for
an I/O, and also whenever the process returns from an
I/O. The number of times the process will enter the
ready queue during its lifetime (not counting the time
the process enters the ready queue when it is run
initially) is ______
Int main()
{
int x = 0, i = 0;
scanf("%d", &x);
for(i = 0; i < 20; i++)
{
x = x + 20;
printf("%d\n", x);
}
return 0;
}
OPERATING SYSTEMSThe following are some events that occur after a device controller issues an interrupt while process L is under execution: P. The processor pushes the process status of L onto the control stack Q. The processor finishes the execution of the current instruction R. The processor executes the interrupt service routine S. The processor pops the process status of L from the control stack T. The processor loads the new PC value based on the interrupt Which of the following is the correct order in which the events above occur?
- (a)QPTRS
- (b)PTRSQ
- (c)TRPQS
- (d)QTPRS OPERATING SYSTEMS
Normally user programs are prevented from handling I/O directly by I/O instructions in them. For CPUs having explicit I/O instructions, such I/O protection is ensured by having the I/O instruction privileged. In a CPU with memory mapped I/O, there is no explicit I/O instruction. Which one of the following is true for a CPU with memory mapped I/O?
- (a)I/O protection is ensured by operating system routine(s)
- (b)I/O protection is ensured by a hardware trap
- (c)I/O protection is ensured during system configuration
- (d)I/O protection is not possible OPERATING SYSTEMS
Self-check
Question 1
Put in order: (a) the interrupt service routine runs, (b) the CPU finishes the current instruction, (c) the CPU saves the process state, (d) the CPU loads the program counter of the routine, (e) the saved state is restored.
Show answerHide
(b), (c), (d), (a), (e).
Question 2
With memory-mapped I/O, which part of the system stops a user program from writing to a device register?
Show answerHide
The operating system, through the memory protection hardware: the device pages are mapped only into kernel space, so the user access faults.
Question 3
, , . Time per block with no buffer, one buffer and two buffers?
Show answerHide
No buffer: . One buffer: . Two buffers: .
Question 4
Three processes, each waiting for I/O half of the time. CPU utilisation?
Show answerHide
, 87.5 per cent.
Question 5
A process reads from a pipe that is empty, with a non-blocking call. What happens?
Show answerHide
The call returns at once with an error meaning "would block" (nothing available now); it does not sleep. A blocking call would put the process in the wait queue until data arrived.
Revision card
One-screen revision
- Interrupt: finish instruction, save state, load PC, ISR, restore. After I/O the process goes to the ready queue.
- Isolated I/O: privileged instructions. Memory-mapped: OS protects through memory mapping.
- Polling / interrupt per unit / DMA for blocks.
- Buffer time per block: , , .
- Cache = copy; buffer = data in transit; spooling = shared output through a disk file.
- Blocking / non-blocking / asynchronous. Utilisation .
References
- ·Silberschatz, Galvin, Gagne, Operating System Concepts, Chapter 12 I/O Systems (I/O hardware, application I/O interface, kernel I/O subsystem, transforming I/O requests to hardware operations)
- ·Tanenbaum and Bos, Modern Operating Systems, Chapter 5 Input/Output (principles of I/O hardware and software, I/O software layers)
- ·Stallings, Operating Systems: Internals and Design Principles, Chapter 11 I/O Management and Disk Scheduling (I/O buffering)
Verification
The buffering totals (no buffer, one buffer, two buffers) come from an event simulation (scripts/os_io.py) that scripts/enum-checks/os_io_check.py recomputes with a different, tick-by-tick simulation on 1728 parameter combinations; the steady-state periods T + C, max(T, C) + M and max(T, C + M) are checked against both. The CPU-utilisation table uses the formula 1 - p^n and a seeded Monte Carlo run, which the check repeats with other seeds. The C programs (a buffering simulation whose output is compared by the check with the table, a non-blocking read on a real pipe, and the number of write calls for different chunk sizes) are run by npm run verify:c. The polling, interrupt and DMA times use made-up round numbers for the interrupt and set-up costs. The sequence of events at an interrupt, the services of the kernel I/O subsystem and the device classes are textbook descriptions, not computed. Memory-mapped I/O protection is described as the operating system's job using memory protection (the usual answer); a particular architecture may differ. Textbook chapter titles are from memory. SECOND PASS (2026-11-02, done by the assistant, not a human check): every Self-check question was solved before the printed answer was read and all agreed; the definitions, worked examples, cheat box and traps were read claim by claim and the numbers re-derived by hand. No defect found. Status raised to self-checked; verified needs a textbook or human check.
last verified: never
My notes (saved in this browser)