Before you start#
Read Btrfs, XFS, and OverlayFS and buffered and direct I/O. The source baseline is upstream Linux v6.12, chosen for teaching rather than as a current security recommendation. The source exercise requires no kernel build; the read-only observation can run in an x86-64 guest or on a similarly provisioned Linux system. Device, scheduler, configuration and architecture determine what is visible.
Learning goals#
Trace a bio into a blk-mq request when the target uses blk-mq; describe software contexts, hardware contexts, tags, scheduler, plugging, driver dispatch and completion; explain resource backpressure without treating every retry as an error.
A quick mental model#
A bio is the block layer's description of a read or write over some data. The block layer may turn one or more bios into a request, then offer that request to a driver; later, completion reports the result back up the stack. These are different stages, so one application write is not guaranteed to become one device command. On blk-mq devices, per-CPU software staging and hardware dispatch contexts help organize concurrent work. Some requests bypass staging, and a scheduler, merging opportunity, queue limits or resource shortage can change the route. This is a model for orientation, not a fixed pipeline. See the v6.12 blk-mq operation overview.
Source walkthrough#
- submit_bio
Block core checks the target; custom submit path is an alternative.
- blk_mq_submit_bio
For the applicable queue, merge, allocate and insert or issue.
- queue_rq
Driver accepts, reports resource pressure, or returns an error.
- Completion
Driver reports result; request and bio lifetime ends under their contracts.
A filesystem submits block I/O as one or more struct bio objects. A bio describes an operation and a range of data; a request is the block layer's unit for scheduling and driver dispatch and may combine compatible bios. Merging is conditional on address adjacency, operation, queue limits and other constraints. Neither one userspace write nor one filesystem page necessarily corresponds to exactly one bio, request, device command or interrupt.
A block device's request_queue describes queue state and limits. A blk-mq blk_mq_tag_set describes driver operations and tag/queue resources, and can be shared by queues under the driver's setup. Software contexts (struct blk_mq_ctx) collect work associated with CPUs; a hardware context (struct blk_mq_hw_ctx, or hctx) represents a dispatch context mapped to one or more software contexts and driver hardware queues. The mapping reflects configuration and CPU topology; hctx is not a promise of a one-to-one CPU-to-physical-queue relationship. struct request carries its queue, operation, position, attached bios and lifetime state. A tag is a bounded identifier/resource used to track a request in the relevant tag map. Drivers and schedulers can have different tag allocation arrangements. See include/linux/blk-mq.h and block/blk-mq-tag.c.
submit_bio enters block-core processing, including checks and accounting appropriate to its caller. In block/blk-core.c, __submit_bio, after crypto preparation and plug setup, v6.12 selects between paths (GPL-2.0; surrounding statements omitted):
if (!bdev_test_flag(bio->bi_bdev, BD_HAS_SUBMIT_BIO)) {
blk_mq_submit_bio(bio);
The condition matters: devices with a custom submit_bio operation take the other branch, which invokes the disk operation after entering the queue. A stacking device can remap and resubmit I/O. Therefore submit_bio -> blk_mq_submit_bio is a verified conditional path, not a universal direct call chain for every device. In the ordinary accounted entry, submit_bio sets accounting and I/O priority, then calls submit_bio_noacct; after its checks and any throttling, that calls submit_bio_noacct_nocheck. The latter selects __submit_bio_noacct_mq or __submit_bio_noacct, both of which eventually invoke __submit_bio. A pre-existing current->bio_list queues nested submissions for later processing instead of recursing indefinitely, and validation or throttling can prevent immediate arrival at the dispatch branch. Source: block/blk-core.c.
The sequence below is a teaching model of the normal blk-mq path for a device without a custom submit operation.
- Filesystemsubmit_bio and __submit_bio
submit_bio(bio)
- submit_bio and __submit_bioblk_mq_submit_bio
normal branch: no BD_HAS_SUBMIT_BIO
- blk_mq_submit_biodriver queue_rq
request prepared; queue_rq(hctx, &bd)
- driver queue_rqdevice
command issued to hardware
- devicedriver queue_rq
Return. completion status
- driver queue_rqblk_mq_submit_bio
Return. driver ends request with status
Time runs downward. Dashed arrows show returns.
Read the text version
- Filesystem → submit_bio and __submit_bio: submit_bio(bio)
- submit_bio and __submit_bio → blk_mq_submit_bio: normal branch: no BD_HAS_SUBMIT_BIO
- blk_mq_submit_bio → driver queue_rq: request prepared; queue_rq(hctx, &bd)
- driver queue_rq → device: command issued to hardware
- device → driver queue_rq (return): completion status
- driver queue_rq → blk_mq_submit_bio (return): driver ends request with status
From submission to hardware dispatch#
On the blk-mq path, blk_mq_submit_bio considers queue limits and potential merging, obtains or prepares a request as needed, attaches bio data, and chooses an insertion or issue path. The exact branch depends on operation flags, plug state, scheduler and queue state. A task's plug temporarily holds eligible I/O, allowing merging and batching before a flush or scheduling point; it is not durable storage. An I/O scheduler, if attached, can choose ordering and allocation behavior before dispatch. The none scheduler selection does not mean a driver has no queues, tags, or backpressure. Check block/blk-mq.c and block/blk-mq-sched.c.
The driver callback queue_rq receives an hctx and blk_mq_queue_data containing a request and a batching hint. In one dispatch-list path, the code branches on its return (GPL-2.0; omitted surrounding cases are in source):
ret = q->mq_ops->queue_rq(hctx, &bd);
switch (ret) {
case BLK_STS_OK:
queued++;
break;
A successful callback means the driver accepted the request under that interface. It is not the completion of the data transfer. The same switch treats BLK_STS_RESOURCE and BLK_STS_DEV_RESOURCE as resource conditions using blk_mq_handle_dev_resource; other status values can end the request with an error. The code's dispatch-list restart and the driver's responsibility to restart work matter for progress. A resource return can mean a retry is appropriate, not that the application has already received a permanent I/O error. Other issue paths must be read separately; do not assume every request visits this particular loop.
The flow below shows what each dispatch return means in the loop quoted above.
- BLK_STS_OK
counted as queued; completion arrives later
- BLK_STS_RESOURCE
backpressure: needs_resource is set, then handled for retry
- BLK_STS_DEV_RESOURCE
blk_mq_handle_dev_resource; the request stays for a later run
- Other status
the default case ends the request with that error
- Requeue
unsent requests go to hctx->dispatch for the next queue run
A queue has finite resources. If all applicable tags or budgets are busy, issuing more work can wait or be deferred, depending on flags and context. A scheduler can have its own internal requests and tags, and the driver tag needed for hardware dispatch is a separate practical bottleneck. Increasing queue depth can change latency and memory use without increasing hardware throughput. A queued request can outlive the submitting task, so request memory, bio pages, device state and completion callbacks must remain valid under their respective ownership contracts.
Completion and a worked request#
For a hypothetical 8 KiB read of an aligned, already-mapped range, a filesystem might issue two contiguous 4 KiB bios. If flags, boundaries and limits permit, blk-mq may merge them into one 8 KiB request. Alternatively it can issue two requests or split work due to device limits. The sector unit in the block layer is 512 bytes, independent of the device's logical block size: 8192 bytes cover 16 such sectors. Starting at sector 100, the range ends just before sector 116. This arithmetic does not establish that sector 100 is correctly aligned for a particular device or filesystem.
The mapping below uses the chapter's hypothetical 8 KiB example. Sector arithmetic uses the block layer's 512-byte units.
Read the text version
- Bio A: 4 KiB at sectors 100 to 107 → Request: 8 KiB at sectors 100 to 115 (16 units) (merged if flags and limits allow)
- Bio B: 4 KiB at sectors 108 to 115 → Request: 8 KiB at sectors 100 to 115 (16 units) (merged if flags and limits allow)
Suppose the driver accepts the merged request, then its device reports an error for completion. blk_mq_end_request updates the request's remaining data and invokes the request completion path with that status. In block/blk-mq.c, it calls blk_update_request for the request's remaining bytes, then __blk_mq_end_request. The latter finishes bookkeeping and either lets a request end callback control freeing or frees the request. Bio completion propagates status through the relevant bio callbacks. A driver returning BLK_STS_OK at dispatch followed by an I/O error at completion is entirely possible; acceptance and success are distinct.
In blk_mq_end_request, v6.12, SPDX GPL-2.0 (surrounding code omitted):
void blk_mq_end_request(struct request *rq, blk_status_t error)
{
if (blk_update_request(rq, error, blk_rq_bytes(rq)))
BUG();
__blk_mq_end_request(rq, error);
}
EXPORT_SYMBOL(blk_mq_end_request);
The helper passes the request's remaining bytes to blk_update_request and calls BUG() if any remain, then calls __blk_mq_end_request. The BUG() is an invariant check, not a status an application can observe. The excerpt does not show how the driver handles partial completion.
The example also illustrates lifetime: the request and its payload cannot be released merely because the submission function returned. The completion path or the specific owner must release them after the device and callbacks are finished. Conversely, a final error must not leave a request permanently occupying a tag. Timeout, queue quiescing, driver reset and device removal introduce additional synchronization; a request waiting for hardware cannot safely retain references to freed queue or device state. Driver implementations differ on how they abort or retry, so trace that driver's timeout and teardown code rather than asserting a universal sequence.
Scheduler, queue mapping, and fairness#
A scheduler can order or throttle work, but its view and goals depend on the scheduler and the cgroup and queue configuration. A hardware controller may also reorder commands internally. Thus a source-level request dispatch order need not be a physical media completion order. A device's advertised logical block size, maximum sectors, segment limits and discard capabilities constrain requests; a request that violates them may be split or rejected along an applicable path. The block layer's 512-byte sector numbering is a unit for interface arithmetic, not an assertion that all disks have 512-byte physical sectors.
A blk_plug belongs to the submitting task's scope and batches suitable requests. If a task blocks, the plug can be flushed to avoid holding I/O indefinitely. The callback's last flag helps a driver decide when to ring or commit a batch; it is not a promise that no later I/O will ever arrive. Queue freeze, quiesce and teardown serve different synchronization needs. Code that prevents new submissions still has to account for requests already dispatched and callbacks already in flight. Likewise, turning off an I/O scheduler does not bypass the filesystem's cache or eliminate driver queuing.
A queue under pressure#
Imagine an hctx with a finite pool of driver tags and a device that can accept only a limited number of commands at once. A burst of 100 application writes does not mean there are 100 commands in the controller. Some writes can remain dirty in the page cache, some bios can be merged, and some requests can wait for tags or an I/O scheduler. If the driver returns a resource status, the dispatch code has to preserve the request and arrange another opportunity for progress. The exact number of requests in each stage is an observation requiring instrumentation, not something /sys/block/.../queue/nr_requests directly reports.
Now assume the device recovers one resource and a queued request is issued. Completion of a different request can make resources available, but whether that completion triggers a particular queue run depends on driver and block-layer protocol. This distinction is important when debugging a stalled queue: examine whether the driver actually restarts dispatch, whether a tag or budget is released, and whether quiescing or freeze state prevents progress. A log line saying a request was accepted does not establish that its end callback ran. Conversely, a request that has never reached the driver cannot have completed on the physical device through that driver.
In a measured system, capture timestamps at defined tracepoints for insertion, issue and completion. Subtracting issue time from completion time measures the interval at those observation points, not pure media service time: controller queues, driver processing and virtualization may be included. Requests can merge or split, making a naive count comparison between userspace writes and completions misleading. A percentile needs a sample population and a workload description; one slow request does not establish the average. Consult the relevant block tracepoint definitions and the tracing chapter before changing production tracing settings.
Tags and ownership in the completion path#
Tags identify requests within a tag set's context and are reused after release. The same numeric tag at two different times is not the same operation. A debugging trace must include queue identity and timing, and for a particular driver may require a generation or command identifier. A tag is also a constrained resource: a leak can prevent later requests from being dispatched even if a device appears idle. The kernel's request lifecycle supplies the rules for allocation, start, completion and freeing; code must not manually reuse a tag while a completion can still arrive.
Advanced lifecycle detail: the completion callback may decide that the request should be freed by returning RQ_END_IO_FREE; otherwise it can retain responsibility as specified by that interface. The absence of a custom callback leads __blk_mq_end_request to free the request itself. This is why a teardown audit follows both callback and no-callback paths. In a partial completion, a driver can update only the bytes it completed before the request is ultimately ended; the shown blk_mq_end_request helper expects the remaining request bytes to be fully handled. The code's BUG() on remaining bytes is an invariant check, not normal error handling for an application to rely on.
In __blk_mq_end_request, v6.12, SPDX GPL-2.0 (surrounding code omitted):
inline void __blk_mq_end_request(struct request *rq, blk_status_t error)
{
if (blk_mq_need_time_stamp(rq))
__blk_mq_end_request_acct(rq, blk_time_get_ns());
blk_mq_finish_request(rq);
if (rq->end_io) {
rq_qos_done(rq->q, rq);
if (rq->end_io(rq, error) == RQ_END_IO_FREE)
blk_mq_free_request(rq);
} else {
blk_mq_free_request(rq);
}
}
The request is freed here unless its end_io callback returns RQ_END_IO_FREE, and a request with no callback is always freed. A driver that needs the request to outlive completion must therefore supply a callback that returns the right value. The excerpt does not show the timestamp accounting or blk_mq_finish_request.
Practice#
Source-only route: in an upstream v6.12 checkout run:
git -C "$KERNEL_SRC" describe --tags --exact-match
rg -n -A22 'static void __submit_bio\(' "$KERNEL_SRC/block/blk-core.c"
rg -n -A18 'ret = q->mq_ops->queue_rq\(hctx, &bd\)' "$KERNEL_SRC/block/blk-mq.c"
rg -n -A18 'void blk_mq_end_request\(' "$KERNEL_SRC/block/blk-mq.c"
Expect tag v6.12, the custom-submit conditional, one or more dispatch sites, and completion code. Draw both the conditional custom-device route and the blk-mq route; mark BLK_STS_RESOURCE as backpressure, not successful completion. Explain where your drawing stops because driver behavior is not yet inspected.
Read-only guest observation, requiring ls and standard shell tools:
for q in /sys/block/*/queue; do
[ -d "$q" ] || continue
printf '%s\n' "$q"
for f in scheduler nr_requests logical_block_size max_sectors_kb; do
[ ! -r "$q/$f" ] || printf '%s: %s\n' "$f" "$(cat "$q/$f")"
done
done
head -5 /proc/diskstats
Each available queue prints the exposed attributes; scheduler selection is bracketed on systems exposing the usual scheduler format. nr_requests and queue limits do not directly report current outstanding hardware commands. /proc/diskstats contains cumulative counters and may change during the read. In a container, restricted guest or unusual configuration, there may be no readable queue entries; record that condition rather than inventing a device. max_sectors_kb is a limit, not a measured I/O size. The command changes no queue settings and creates no files. Deliver the queue attributes and a source diagram with the conditional path, possible backpressure and completion.
Knowledge check#
- Why does
__submit_bionot always invokeblk_mq_submit_biodirectly? - What is the difference between an hctx, a tag and a request?
- What does
BLK_STS_RESOURCEatqueue_rqmean in the illustrated dispatch loop? - Why can an accepted request still produce an I/O error?
- What does an 8 KiB range represent in the block layer's sector units?
Answers#
- A disk with its own submit operation takes the custom branch; a stacking driver may map and resubmit work.
- An hctx is a dispatch context; a tag is a bounded tracking/resource identifier; a request is the operation dispatched to a driver and may contain bios.
- It reports a resource condition, and this dispatch path handles the request as pending for later progress rather than treating it as successful completion.
- The callback accepts responsibility for work; a later hardware or transport completion can report failure.
- It spans sixteen 512-byte sector units; this unit does not establish physical-sector size or alignment.
Checkpoint#
Save the conditional dispatch diagram and observed queue values. Pass when you can explain merge versus split, scheduler versus hctx, tag exhaustion, resource retry, and final completion without assuming every bio uses the same path. Re-read the cited switch and completion function if you conflate acceptance and completion. This records practice, not certification.
Sources#
The blk-mq documentation gives the conceptual overview; the version-pinned source links below support implementation-specific statements. Queue attribute descriptions are in the block queue sysfs documentation, and disk statistics are described in the I/O statistics documentation.