I/O & Kernel — Cheat Sheet
Operating Systems · 4 topics. Download the PDF or the Instagram carousel and share it.
I/O Techniques: Polling, Interrupts & DMA
Three I/O techniques — polling (CPU busy-waits), interrupt-driven (CPU is notified when device is ready), and DMA (device transfers data directly to memory) — trade CPU utilisation against I/O throughput.
- ✓Polling wastes CPU cycles and is only appropriate for very fast devices where interrupt overhead exceeds benefit.
- ✓Interrupt-driven I/O allows the CPU to do useful work while waiting for slow devices like disks and networks.
- ✓DMA offloads bulk data transfer from the CPU to a dedicated controller, interrupting the CPU only on completion.
- ✓Java NIO Selector uses OS-level mechanisms (epoll on Linux, kqueue on macOS) — the same interrupt model, applied to sockets.
- ✓AsynchronousFileChannel provides non-blocking file I/O with completion callbacks, hiding DMA-level operations behind the JVM.
- ✓Context-switch overhead makes polling viable only for sub-microsecond I/O (e.g., network cards with busy-polling mode).
import java.nio.channels.*;
import java.nio.ByteBuffer;
import java.net.InetSocketAddress;
import java.util.Set;
// Polling analogy (BAD — busy-wait):
// while (!socket.hasData()) { /* spin */ }
// data = socket.read();
// NIO Selector: OS interrupt-driven I/O surfaced to Java
// One thread handles thousands of connections via event notification
Selector selector = Selector.open();
// Open a non-blocking server socket
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false); // non-blocking mode
server.bind(new InetSocketAddress(8080));
// Register interest in ACCEPT events (OS will notify us)
server.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// Blocks until at least one channel is ready (OS interrupt → wakes up)
int readyCount = selector.select(); // equivalent to epoll_wait() on Linux
if (readyCount == 0) continue;
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isAcceptable()) {
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println("Client connected: " + client.getRemoteAddress());
} else if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int bytes = client.read(buf);
if (bytes > 0) {
buf.flip();
System.out.println("Received: " + new String(buf.array(), 0, bytes));
}
}
}
keys.clear(); // must clear processed keys
}Disk Scheduling Algorithms
Disk scheduling algorithms determine the order in which I/O requests are serviced to minimise total disk head seek time and improve throughput.
- ✓FCFS is fair but has the worst average seek time; never use it for high-throughput disk workloads.
- ✓SSTF minimises seek time on average but can starve requests at the extremes of the disk.
- ✓SCAN (elevator) prevents starvation by guaranteeing the head will eventually reach every position.
- ✓C-SCAN provides more uniform wait times than SCAN by only servicing in one direction.
- ✓SSDs have no moving parts, so seek time is negligible — disk scheduling algorithms are largely irrelevant for NVMe.
- ✓Linux's mq-deadline is the recommended scheduler for HDDs; use none for SSDs.
import java.util.*;
int[] requests = {98, 183, 37, 122, 14, 124, 65, 67};
int head = 53;
// ── FCFS ─────────────────────────────────────────────────────────────────────
// Service in arrival order: 53→98→183→37→122→14→124→65→67
// Movement: |98-53|+|183-98|+|37-183|+|122-37|+|14-122|+|124-14|+|65-124|+|67-65|
// = 45 + 85 + 146 + 85 + 108 + 110 + 59 + 2 = 640
int fcfsTotal = 0, cur = head;
for (int r : requests) { fcfsTotal += Math.abs(r - cur); cur = r; }
System.out.println("FCFS total movement: " + fcfsTotal); // 640
// ── SSTF ─────────────────────────────────────────────────────────────────────
// Always service closest request: 53→65→67→37→14→98→122→124→183
// Movement: 12+2+30+23+84+24+2+59 = 236
List<Integer> sstfQueue = new ArrayList<>();
for (int r : requests) sstfQueue.add(r);
int sstfTotal = 0, sstfCur = head;
while (!sstfQueue.isEmpty()) {
int nearest = sstfQueue.stream()
.min(Comparator.comparingInt(r -> Math.abs(r - sstfCur))).get();
sstfTotal += Math.abs(nearest - sstfCur);
sstfCur = nearest;
sstfQueue.remove(Integer.valueOf(nearest));
}
System.out.println("SSTF total movement: " + sstfTotal); // 236
// ── SCAN (Elevator) ──────────────────────────────────────────────────────────
// Head moves toward higher tracks first: 53→65→67→98→122→124→183→37→14
// (reaches 183, reverses direction, services 37 then 14)
// Movement: 12+2+31+24+2+59+146+23 = 299 (approximately)
int[] scanOrder = {65, 67, 98, 122, 124, 183, 37, 14};
int scanTotal = 0, scanCur = head;
for (int r : scanOrder) { scanTotal += Math.abs(r - scanCur); scanCur = r; }
System.out.println("SCAN total movement: " + scanTotal); // 299System Calls & Kernel Mode
System calls are the controlled interface through which user-space programs request privileged kernel services, requiring a CPU mode switch from user mode to kernel mode.
- ✓System calls are the only legal way for user-space programs to request kernel services; direct kernel access is forbidden.
- ✓A mode switch (user → kernel → user) costs ~100–1000 ns; minimising unnecessary system calls improves performance.
- ✓Buffered I/O (BufferedInputStream) reduces system call frequency by batching reads into large kernel buffer requests.
- ✓Java's FileInputStream, Thread.start(), and Socket operations all eventually invoke OS system calls via JNI.
- ✓ProcessHandle.current().pid() maps to getpid(); Files.createFile() maps to open() with O_CREAT.
- ✓strace on Linux and dtrace on macOS reveal exactly which system calls a Java application makes.
// Java I/O ultimately triggers OS system calls
// Every FileInputStream.read() call eventually invokes sys_read
// You can observe this with strace on Linux:
// strace -e trace=read,write,open java MyProgram
import java.io.*;
import java.nio.file.*;
// FileInputStream.read() → native method → sys_read(fd, buf, count)
try (var fis = new FileInputStream("/tmp/data.txt")) {
byte[] buf = new byte[1024];
int n = fis.read(buf); // → JNI → sys_read() system call
System.out.println("Read " + n + " bytes");
}
// Files.createFile() → sys_open(path, O_CREAT | O_WRONLY, mode)
Path p = Path.of("/tmp/newfile.txt");
Files.createFile(p); // → sys_open / sys_creat system call
// Thread.start() → eventually → sys_clone() (Linux) / CreateThread (Windows)
Thread t = new Thread(() -> System.out.println("PID: " +
ProcessHandle.current().pid())); // getpid() system call
t.start();
// InetSocketAddress / ServerSocket → sys_socket, sys_bind, sys_listen
// Socket.connect() → sys_connect
// Socket.accept() → sys_accept
// ProcessHandle.current().pid() → sys_getpid()
long pid = ProcessHandle.current().pid();
System.out.println("JVM PID: " + pid);
// System.currentTimeMillis() → sys_gettimeofday / clock_gettime
long now = System.currentTimeMillis();Memory-Mapped I/O & Files
Memory-mapped files map file contents directly into a process's virtual address space, allowing file I/O to be performed as ordinary memory operations with OS-managed demand paging.
- ✓Memory-mapped files eliminate double-buffering by mapping the OS page cache directly into the process virtual address space.
- ✓The OS loads pages on demand via page faults — the entire file is never loaded at once, saving memory.
- ✓FileChannel.map() returns a MappedByteBuffer; use MapMode.READ_ONLY, READ_WRITE, or PRIVATE (copy-on-write).
- ✓Memory-mapped I/O is ideal for large files with random access patterns; sequential small files may not benefit.
- ✓MappedByteBuffer.force() is equivalent to msync() — it flushes modified mapped pages to disk.
- ✓High-performance Java libraries (Chronicle Map, Aeron, Disruptor) use memory-mapped files for sub-microsecond IPC.
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.io.*;
Path file = Path.of("/tmp/large.csv");
// ── Traditional: FileInputStream (double-buffered) ────────────────────────
long startTraditional = System.nanoTime();
try (var fis = new FileInputStream(file.toFile());
var bis = new BufferedInputStream(fis, 65536)) {
byte[] buf = new byte[65536];
long totalBytes = 0;
int n;
while ((n = bis.read(buf)) != -1) totalBytes += n;
System.out.println("Traditional: " + totalBytes + " bytes");
}
long traditionalMs = (System.nanoTime() - startTraditional) / 1_000_000;
// ── Memory-Mapped: FileChannel.map() (zero extra copy) ───────────────────
long startMapped = System.nanoTime();
try (FileChannel fc = FileChannel.open(file)) {
long fileSize = fc.size();
// Map entire file into virtual address space (READ_ONLY)
// Mode: READ_ONLY, READ_WRITE, or PRIVATE (copy-on-write)
MappedByteBuffer mapped = fc.map(
FileChannel.MapMode.READ_ONLY,
0, // starting position in file
fileSize // number of bytes to map
);
// mapped now lives in virtual address space
// OS pages in data on demand — no full load upfront
long totalBytes = 0;
while (mapped.hasRemaining()) {
mapped.get(); // direct memory read — no syscall per byte
totalBytes++;
}
System.out.println("Memory-mapped: " + totalBytes + " bytes");
// Force all pages to be loaded and written to disk (if READ_WRITE)
mapped.force(); // optional: flush modified mapped region to disk
}
long mappedMs = (System.nanoTime() - startMapped) / 1_000_000;
System.out.printf("Traditional: %dms | Memory-mapped: %dms%n",
traditionalMs, mappedMs);
// Memory-mapped is typically 2-5x faster for large sequential reads