How the Linux Block Layer Works: bios, Request Queues, and Why I/O Is Batched
A filesystem that wants to write a dirty page does not issue a single system call that waits for the disk. It builds a bio, submits it, and continues. The block layer may hold that bio, merge it with neighbors, and only later hand a larger request to the device driver. Completion arrives asynchronously, often from an interrupt or a polling path.
This article is about that contract: the bio as the submission unit, the request as the unit the driver sees, why the layer plugs and merges, and how multi-queue (blk-mq) removes the old single lock. It is not a second tour of the page cache, VFS path walks, or NAND flash translation. Those sit above or below. See How the Linux Page Cache Works, How the Linux VFS Works, and How NAND Flash and the FTL Work.
The contract
The block layer sits between higher layers that know about files or logical blocks and device drivers that know about hardware queues. A bio describes one or more contiguous sectors and the memory pages that hold or will receive the data. Multiple bios can be merged into one request if they are adjacent. The driver receives requests, not raw bios. Submission is almost never the moment the I/O finishes.
Core objects
A struct bio carries the operation (read, write, discard, etc.), the target block device, the starting sector, the size, a list of bio_vec segments that point at pages, and an end_io callback. It is the unit the filesystem or the page-cache writeback path builds.
A struct request is what the I/O scheduler and the driver work with. It holds one or more bios that have been merged, a starting sector, total length, and references to the software and hardware queue contexts in the multi-queue design. The request is the unit that can be reordered, tagged, and completed as a whole.
How submission actually works
A caller builds a bio and calls submit_bio. Modern paths go through submit_bio_noacct into the blk-mq entry point. The layer first tries to merge the new bio into an existing request that is still in the per-process plug list or in a software staging queue. Adjacent sectors of the same type can be combined, limited by the device's maximum request size and maximum scatter-gather segments.
If no merge is possible, a new request is allocated from the tag set. Depending on the I/O scheduler (none, mq-deadline, bfq), the request may be inserted into a sorted or fairness-aware structure, or it may be sent straight toward a hardware queue. The per-CPU software queues (blk_mq_ctx) exist so that concurrent submissions from different CPUs do not contend on a single lock. Hardware queues (blk_mq_hw_ctx) map to the device's actual submission queues—one or many for NVMe, fewer for older SCSI adapters.
Plugging is the deliberate delay that makes merging possible. Code that will issue several bios brackets them with blk_start_plug and blk_finish_plug. Bios accumulate on the current task's plug list. On unplug they are merged and dispatched together. This is why a burst of sequential writes often becomes a handful of larger requests rather than one request per page.
A concrete write path
Page-cache writeback has decided a dirty page must go to disk. The filesystem builds a bio that points at that page (or several consecutive pages), sets the operation to write, and calls submit_bio. The bio may merge with other write bios already sitting in the plug list for the same device. After unplug, the resulting request is dispatched to a hardware queue. The driver programs the device (or the NVMe submission queue). Later, the device completes the I/O; the completion path walks the request, calls each bio's end_io, and the page is marked clean. The original write() or the background writeback thread has long since continued.
Misconceptions
Every write becomes its own disk command. Adjacent bios are routinely merged. Plugging exists precisely to create opportunities for that merge.
The system call that dirtied the page waits for the device. Most paths return after the page is marked dirty or after the bio is submitted. Completion is asynchronous.
blk-mq is only for NVMe. It is the modern path for nearly all block devices. Devices with a single hardware queue still use it; they simply have one hardware context.
The scheduler always reorders. The "none" scheduler is common for fast SSDs; requests flow with minimal reordering. Deadline and BFQ still exist for devices that benefit from sorting or fairness.
Takeaways
The Linux block layer turns bios into requests, merges adjacent ones, and dispatches them through per-CPU software queues onto hardware queues. Plugging batches work so that sequential I/O becomes larger transfers. Submission and completion are separate events. That separation is why a process can continue while the device is still busy, and why the page cache and the block layer can be understood independently.
Related: How io_uring Works for the userspace side of batched submission, and How sendfile and splice Work for zero-copy paths that still touch the same block layer underneath.