Chapter 8: I/O Subsystems: From Block Layer to Disk
Chapter 8: I/O Subsystems: From Block Layer to Disk
The Linux storage I/O subsystem bridges high-level POSIX filesystem operations with physical storage controllers. Over the last decade, storage bandwidth evolved from hundreds of IOPS on spinning rust (HDDs) to millions of IOPS on multi-channel NVMe SSDs. This transition forced a total architectural overhaul of the Linux block layer from the single-lock legacy request queue to the multi-queue (blk-mq) architecture.
This chapter traces an I/O request from the initial system call down to the physical disk controller, examining request queues, I/O schedulers, bio splitting, and completion interrupts.
The I/O Path: From open() to the Disk
Every storage I/O operation traverses distinct kernel layers:
┌────────────────────────────────────────────────────────────────────────┐
│ STORAGE I/O DATA PATH │
│ │
│ Userspace (open / read / write / fsync / io_uring) │
│ │ │
│ ▼ │
│ Virtual Filesystem (VFS) Layer (sys_write / vfs_write) │
│ │ │
│ ▼ │
│ Concrete Filesystem (ext4, XFS, btrfs) │
│ - Translates file offset to logical block addresses (LBAs) │
│ - Allocates extents and builds bio structures │
│ │ │
│ ▼ │
│ Multi-Queue Block Layer (blk-mq) │
│ - Software Staging Queues (per-CPU) ──► I/O Scheduler (Optional) │
│ - Hardware Dispatch Queues (per-channel) │
│ │ │
│ ▼ │
│ Device Driver (nvme, mpt3sas, virtio-blk) │
│ - Maps DMA scatter-gather lists to controller ring buffers │
│ │ │
│ ▼ │
│ Physical Storage Controller & Flash/Disk Media │
└────────────────────────────────────────────────────────────────────────┘
The Block Layer and Request Queues (blk-mq)
In the legacy Linux block layer, all CPU cores submitted requests to a single global request queue protected by a single spinlock. On modern NVMe devices capable of 1M+ IOPS, that single lock became an immediate bottleneck.
The modern Multi-Queue Block Layer (blk-mq) eliminates this contention through a two-tiered queue design:
- Software Staging Queues: Per-CPU or per-core software queues. Tasks submit requests locklessly to their local CPU queue.
- Hardware Dispatch Queues: Mapped directly to the physical submission queues of the storage hardware controller (e.g., NVMe controllers support up to 64,000 independent hardware queues).
I/O Schedulers: noop, deadline, cfq, mq-deadline, kyber, bfq
I/O schedulers decide the order and timing of requests dispatched to storage devices:
| Scheduler | Architecture | Primary Strength | Recommended Use Case |
|---|---|---|---|
none |
Direct pass-through | Zero software queue overhead, lowest CPU utilization | High-speed NVMe drives with native hardware queues |
mq-deadline |
Deadline queues (read/write) | Prevents read starvation by prioritizing reads over writes | SATA SSDs, SAS SSDs, fast cloud block devices |
bfq (Budget Fair Queue) |
Proportional weight sharing | Interactive responsiveness and fairness under heavy write load | Desktop workstations, SATA spinning disks (HDDs) |
kyber |
Latency-target feedback loop | Regulates queue depths to maintain strict read latency limits | Shared cloud nodes, fast flash with high write concurrency |
# Query active and available schedulers for a device
cat /sys/block/nvme0n1/queue/scheduler
# Switch scheduler dynamically (e.g., set to 'none' on NVMe)
echo "none" | sudo tee /sys/block/nvme0n1/queue/schedulerBlock Devices and Queueing Models
A storage device’s ability to absorb parallel requests is governed by queue depth:
- Queue Depth (
nr_requests): The maximum number of requests the block layer permits to be concurrently staged. - Little’s Law for Storage: \[\text{Throughput} = \frac{\text{Queue Depth}}{\text{Latency}}\] To achieve 1,000,000 IOPS on a device with 100\(\mu\)s latency, the host must sustain an active queue depth of: \[QD = 1,000,000 \times 0.0001 = 100\] If the application sends synchronous single-threaded requests with \(QD=1\), the maximum possible throughput is capped at only 10,000 IOPS regardless of how fast the NVMe drive is.
I/O Merge, Splitting and Bio Structures
The fundamental unit of I/O in the Linux block layer is struct bio:
- A bio represents an in-flight I/O request composed of one or more memory vectors (scatter-gather list of physical memory folios).
- Bio Merging: If an application submits adjacent small block requests, the block layer merges them into a single contiguous
struct requestbefore sending it to the controller, amortizing DMA setup overhead. - Bio Splitting: If a bio exceeds the device’s maximum transfer size (
max_sectors_kb), the block layer splits it into multiple smaller requests.
# Inspect maximum request and sector limits
cat /sys/block/nvme0n1/queue/max_sectors_kb
cat /sys/block/nvme0n1/queue/hw_sector_sizeI/O Completion and Interrupt Handling
When the storage controller completes an I/O operation:
- Hardware Interrupt: The controller fires an MSI-X interrupt targeted to the CPU core that submitted the request.
- Interrupt Service Routine (ISR): The driver acknowledges the controller’s completion queue.
- Softirq Callback:
BLOCK_SOFTIRQexecutes, completing thebio, invoking filesystem end-I/O callbacks, and waking up sleeping threads or completingio_uringring entries.
Hybrid Polling Mode for Extreme Latency
On modern ultra-low-latency NVMe and Optane devices, the overhead of interrupt handling (~2–5\(\mu\)s) is larger than the media access latency. Linux supports polling mode (io_uring with IORING_SETUP_IOPOLL or /sys/block/<dev>/queue/io_poll), where the application thread directly polls the NVMe completion queue, achieving sub-10\(\mu\)s deterministic latency.