Request Queues and struct request

A struct request is the block layer’s unit of work that a device driver actually executes: it bundles one or more merged [[The bio Structure|bios]] that all target a contiguous run of sectors on the same device, into a single descriptor carrying a starting sector, a byte length, an operation type, and a tag (an integer the driver uses to identify the command and its completion). The struct request_queue is the per-device object that owns the whole machinery — the blk-mq software and hardware queues, the O scheduler (the “elevator”), the device’s queue_limits, and the tag set — and it hangs off the gendisk that represents the disk. The pivotal fact, verifiable in the kernel header, is that “a request is one or more bios” (blk-mq docs): the bio is what the upper layers submit, and the request is what the block layer assembles and hands to the driver. This note dissects both structures and the request lifecycle; the optimization layer that builds requests by coalescing bios lives in Request Merging and Plugging.

Mental Model

Think of three nested objects, each owning the one below it. At the top is the request_queue: one per logical block device (one per gendisk), holding global per-device state — limits, the scheduler, the tag pool, the per-CPU staging queues. Inside it, a request is a transient work item allocated from the tag pool when a bio arrives, living only from submission to completion. Inside the request, a chain of bios (rq->biorq->biotail, linked by bio->bi_next) holds the actual memory-to-sector mapping. A bio says “move these pages to/from this sector range”; a request says “here is a batch of adjacent such transfers, identified by tag N, please execute it.”

flowchart TB
  GD["gendisk<br/>(the disk / partition table)"] --> RQQ["struct request_queue<br/>(per-device hub)"]
  RQQ --> LIM["queue_limits<br/>(max_sectors, max_segments,<br/>logical/physical block size)"]
  RQQ --> ELV["elevator<br/>(I/O scheduler)"]
  RQQ --> TAGS["blk_mq_tag_set<br/>(tag pool 0..queue_depth-1)"]
  RQQ --> SW["per-CPU blk_mq_ctx<br/>(software staging queues)"]
  RQQ --> HW["blk_mq_hw_ctx<br/>(hardware dispatch queues)"]
  RQQ -.allocates.-> REQ["struct request<br/>tag=N, __sector, __data_len"]
  REQ --> B1["bio (rq->bio)"]
  B1 --> B2["bio"]
  B2 --> B3["bio (rq->biotail)"]
  B1 -.bi_next.-> B2 -.bi_next.-> B3

The ownership hierarchy from gendisk down to the bio chain inside a request. What it shows: the request_queue is the single per-device object that owns limits, the scheduler, the tag pool, and the two-level queue structure; a request is allocated transiently from the tag pool and internally holds a singly-linked list of bios. The insight to take: the request_queue is long-lived per-device configuration, while a request is short-lived per-I/O state — and the bio chain inside it is exactly what gets longer when merging succeeds.

struct request — Anatomy

The definition lives in include/linux/blk-mq.h (v6.12). The fields fall into a handful of functional groups; walking them is the fastest way to understand what a request is.

Identity and routing. The first three fields wire the request to its queue and its blk-mq queues:

struct request_queue *q;        /* owning queue */
struct blk_mq_ctx *mq_ctx;      /* software (per-CPU) staging queue */
struct blk_mq_hw_ctx *mq_hctx;  /* hardware dispatch queue */

q points back to the owning request_queue. mq_ctx is the per-CPU software staging queue the request was submitted on, and mq_hctx is the hardware dispatch queue it maps to — the two-level structure that blk-mq introduced. These are set during allocation in blk_mq_rq_ctx_init().

Operation and flags.

blk_opf_t cmd_flags;   /* op and common flags */
req_flags_t rq_flags;

cmd_flags packs the operation (read, write, flush, discard, write-zeroes — REQ_OP_*) together with modifier flags (REQ_SYNC, REQ_FUA, REQ_PREFLUSH, REQ_RAHEAD, etc.), all defined in blk_types.h. rq_flags is a separate set of block-layer-internal bits — among them RQF_USE_SCHED (the request goes through an I/O scheduler), RQF_SCHED_TAGS (it holds a scheduler tag rather than a driver tag), RQF_STARTED, RQF_IO_STAT, RQF_FLUSH_SEQ, and RQF_SPECIAL_PAYLOAD (blk-mq.h). The distinction matters: cmd_flags describes what the device should do; rq_flags describes how the block layer is handling the request.

Tags.

int tag;
int internal_tag;

The tag is the request’s identity number. When an I/O scheduler is active, the request first gets a scheduler-internal tag (internal_tag) and only acquires a real hardware driver tag (tag) at dispatch time; with no scheduler, tag is assigned directly and internal_tag is BLK_MQ_NO_TAG. The driver uses tag to index into its submission queue and to match completions back to requests — “every request is identified by an integer, ranging from 0 to the dispatch queue size … generated by the block layer and later reused by the device driver” (blk-mq docs). Tags are covered in depth in Tag Sets and Request Allocation.

The position cursor and the bio chain — the heart of the request.

unsigned int __data_len;  /* total data len */
sector_t __sector;        /* sector cursor */
struct bio *bio;
struct bio *biotail;

__sector is the starting sector on the device (a sector is the 512-byte SECTOR_SIZE unit the block layer counts in, regardless of the device’s real block size), and __data_len is the total byte length of the transfer. The leading underscores are a deliberate warning — the header comments these “are internal, NEVER access directly”; code uses the accessors blk_rq_pos(rq) (returns rq->__sector), blk_rq_bytes(rq) (returns rq->__data_len), and blk_rq_sectors(rq) (returns __data_len >> SECTOR_SHIFT) instead (blk-mq.h). bio and biotail are the head and tail of the request’s bio chain: a singly-linked list threaded through each bio’s bi_next pointer. This is where “a request is one or more bios” physically lives — when a new bio back-merges onto a request, the merge code does exactly req->biotail->bi_next = bio; req->biotail = bio; req->__data_len += bio->bi_iter.bi_size; (blk-merge.c, bio_attempt_back_merge). A single fresh request begins life with rq->bio == rq->biotail == bio and __data_len == bio->bi_iter.bi_size, set by blk_rq_bio_prep().

Segments.

unsigned short nr_phys_segments;
unsigned short nr_integrity_segments;

nr_phys_segments is the number of physically-contiguous, DMA-coalesced address+length pairs after the block layer merges adjacent memory pages — this is what the driver maps into a scatter-gather list, and it is bounded by queue_limits.max_segments (see below).

State and lifecycle.

enum mq_rq_state state;
atomic_t ref;
unsigned long deadline;
u64 start_time_ns;
u64 io_start_time_ns;

state is one of three values from enum mq_rq_state (blk-mq.h):

enum mq_rq_state {
	MQ_RQ_IDLE      = 0,  /* allocated, not yet issued to hardware */
	MQ_RQ_IN_FLIGHT = 1,  /* issued; the driver owns it */
	MQ_RQ_COMPLETE  = 2,  /* completion observed */
};

A freshly allocated request is MQ_RQ_IDLE. blk_mq_start_request() flips it to MQ_RQ_IN_FLIGHT with WRITE_ONCE(rq->state, MQ_RQ_IN_FLIGHT) and arms the per-request timeout (blk_add_timer(rq)) just before the driver takes ownership (blk-mq.c). On completion the state becomes MQ_RQ_COMPLETE. ref is a reference count (req_ref_set(rq, 1) at init) used to safely race timeout handling against normal completion. deadline is the timeout deadline the block-layer timer checks. start_time_ns/io_start_time_ns feed the iostat accounting.

Scheduler-private storage. Several fields are unions multiplexed across the request’s lifetime because a request is never in two of these states at once:

union { struct hlist_node hash; struct llist_node ipi_list; };
union { struct rb_node rb_node; struct bio_vec special_vec; };
struct { struct io_cq *icq; void *priv[2]; } elv;

The hash node lets the elevator hash a request by its end sector for back-merge lookups; once the request leaves the scheduler for the dispatch list, that node is reused as ipi_list for inter-processor-interrupt completion. The rb_node is the elevator’s red-black-tree node for sector-ordered lookup (used by mq-deadline). The elv struct gives the scheduler three private pointers. This union packing is why a request is comparatively cheap despite carrying scheduler, hashing, and completion bookkeeping.

Completion callback.

rq_end_io_fn *end_io;
void *end_io_data;

When the request finishes, the block layer invokes end_io (if set) — passthrough and internal callers use this to be notified directly.

struct request_queue — the Per-Device Hub

Defined in include/linux/blkdev.h (v6.12), the request_queue is one object per block device. Its fields are the configuration and state of everything the block layer does for that device. The notable ones:

struct elevator_queue   *elevator;          /* the I/O scheduler */
const struct blk_mq_ops *mq_ops;            /* driver's queue_rq hooks */
struct blk_mq_ctx __percpu *queue_ctx;      /* per-CPU software queues */
unsigned long           queue_flags;        /* QUEUE_FLAG_* bits */
unsigned int            nr_hw_queues;       /* number of hardware queues */
struct xarray           hctx_table;         /* hardware queue table */
struct request          *last_merge;        /* one-hit merge cache */
struct gendisk          *disk;              /* the owning disk */
struct queue_limits     limits;             /* device geometry/limits */
unsigned long           nr_requests;        /* max # of in-flight requests */
struct blk_mq_tag_set   *tag_set;           /* the shared tag pool */

elevator is the active I/O scheduler (NULL means the none scheduler — pure FIFO pass-through). mq_ops holds the driver’s callbacks, chiefly queue_rq (issue a request to hardware) and optionally queue_rqs (issue a batch). queue_ctx is the per-CPU array of software staging queues; hctx_table indexes the hardware dispatch queues, of which there are nr_hw_queues. disk ties the queue to its gendisk — the structure that represents the disk to the rest of the kernel and carries its partition table, and which is created together with the queue (blk_mq_alloc_disk() allocates both). nr_requests caps how many requests can be in flight; blk_alloc_queue() initializes it to BLKDEV_DEFAULT_RQ, which is 128 (blk-mq.h, block/blk-core.c), exposed and tunable through /sys/block/<dev>/queue/nr_requests.

queue_flags holds per-queue boolean state as a bitmap of QUEUE_FLAG_* bits, tested through macros: QUEUE_FLAG_NOMERGES (disable merging entirely — blk_queue_nomerges(q)), QUEUE_FLAG_NOXMERGES (allow only the cheap one-hit cache, not the hash/RB-tree search), QUEUE_FLAG_SAME_COMP (complete on the submitting CPU’s group), QUEUE_FLAG_STATS, QUEUE_FLAG_QUIESCED, and so on (blkdev.h). The last_merge pointer is the elevator’s one-hit merge cache — the single most-recently-merged request, checked first before any hash lookup (see Request Merging and Plugging).

queue_limits — Device Geometry and Caps

struct queue_limits is embedded in the request_queue and describes the device’s physical and protocol constraints. These are precisely the numbers the merge and split code consults to decide whether a bio can join a request and whether a request must be split before dispatch:

unsigned int   max_hw_sectors;     /* hardware ceiling per command */
unsigned int   max_sectors;        /* soft ceiling actually used */
unsigned int   max_segment_size;   /* bytes per scatter-gather segment */
unsigned int   logical_block_size; /* smallest addressable block */
unsigned int   physical_block_size;/* device's atomic write unit */
unsigned int   io_min, io_opt;     /* min/optimal I/O sizes */
unsigned int   chunk_sectors;      /* stripe/zone boundary */
unsigned short max_segments;       /* max SG entries per request */
unsigned short max_integrity_segments;
unsigned short max_discard_segments;
unsigned int   max_zone_append_sectors;
unsigned long  seg_boundary_mask;  /* segments may not cross this */
unsigned long  virt_boundary_mask;

max_sectors (and the hardware ceiling max_hw_sectors behind it) bound the total size of a request in 512-byte sectors; max_segments bounds the number of scatter-gather segments (nr_phys_segments). Every merge attempt checks both: a bio is refused if appending it would push blk_rq_sectors(req) + bio_sectors(bio) past the sector limit, or push nr_phys_segments past max_segments (blk-merge.c). logical_block_size is the smallest unit the device can address (commonly 512 or 4096 bytes); physical_block_size is the device’s real underlying block (often 4 KiB even when the logical size is 512, the classic “512e” drive). chunk_sectors marks a hard boundary (a RAID stripe, or a zone) that requests may not straddle. Stacking drivers like device-mapper and md compute their limits by stacking the limits of their underlying devices, which is why a logical volume’s max_sectors is the minimum across its physical members.

Mechanical Walk-through — From bio to request to completion

A request comes into being inside blk_mq_submit_bio() (blk-mq.c). The path:

  1. Try to avoid allocation entirely. Before allocating, the submit path tries to merge the incoming bio into an existing request — either one already sitting in this task’s plug or one in a software queue (blk_mq_attempt_bio_merge()). If merging succeeds, no new request is created; the bio simply extends an existing one’s chain.

  2. Split if too big. __bio_split_to_limits() consults queue_limits and splits the bio if it exceeds max_sectors or max_segments, returning the count of segments nr_segs.

  3. Allocate a request and a tag. blk_mq_get_new_requests()__blk_mq_alloc_requests() selects the per-CPU software context (blk_mq_get_ctx) and maps it to a hardware queue (blk_mq_map_queue), then pulls a tag with blk_mq_get_tag(). If a scheduler is active, the request is marked RQF_USE_SCHED | RQF_SCHED_TAGS; otherwise the active-request counter is bumped. blk_mq_rq_ctx_init() initializes the fresh request: wires q/mq_ctx/mq_hctx, copies cmd_flags, sets the tag, zeroes the segment counts, sets the reference to 1, and (with a scheduler) initializes the hash and RB-tree nodes (blk-mq.c). The request is now MQ_RQ_IDLE.

  4. Bind the bio. blk_mq_bio_to_request() sets rq->__sector = bio->bi_iter.bi_sector, copies the write hint, and calls blk_rq_bio_prep(), which sets rq->nr_phys_segments = nr_segs, rq->__data_len = bio->bi_iter.bi_size, and rq->bio = rq->biotail = bio (blk-mq.h). The request now physically is its single bio.

  5. Stage, schedule, or dispatch. If a plug is active the request is added to it (blk_add_rq_to_plug) for batch accumulation; if a scheduler is active or the hardware queue is busy it is inserted into the scheduler/software queue; otherwise the fast path issues it directly with blk_mq_try_issue_directly().

  6. Issue and complete. At dispatch, blk_mq_start_request() sets MQ_RQ_IN_FLIGHT and the driver’s queue_rq is called. When the device signals completion (interrupt or polling), the block layer walks the request’s bio chain calling bio_endio() on each, frees the tag back to the pool, and the request returns to the free state.

The request, then, exists only between steps 3 and 6 — it is transient per-I/O state, recycled from a fixed pool of nr_requests slots, in contrast to the long-lived per-device request_queue.

Failure Modes and Common Misunderstandings

__sector is a byte offset.” No — it counts 512-byte sectors (SECTOR_SIZE), always, even on a 4 KiB-logical-block device. A __sector of 8 is byte offset 4096. Mixing the two is a classic off-by-512× bug; use blk_rq_pos()/blk_rq_bytes() and never touch the underscore fields.

Confusing cmd_flags and rq_flags. They are different u-typed bitfields with overlapping-looking names. REQ_* (in cmd_flags) is the operation and device-facing modifiers; RQF_* (in rq_flags) is block-layer bookkeeping. A driver inspecting the wrong one silently mis-handles the request.

Assuming one bio == one request. The whole point of the request layer is that it is not one-to-one. A sequential write of a megabyte from the page cache typically arrives as many bios that the block layer coalesces into a handful of large requests; conversely a bio larger than max_sectors is split into several requests. Code that walks rq->bio must use __rq_for_each_bio() to traverse the chain, not assume a single bio.

nr_requests vs. the tag depth. nr_requests (default 128) is the block-layer-visible cap on in-flight requests and is what /sys/block/*/queue/nr_requests tunes; the hardware tag depth (tag_set->queue_depth) is what the device can actually accept. When a scheduler is attached, nr_requests is reset from the tag-set queue depth (block/elevator.c); they interact, and raising nr_requests above what tags allow does nothing useful.

Alternatives and Historical Context

Before kernel 5.0 (2019) the block layer had two request infrastructures: the legacy single-queue path (struct request_queue with a single ->request_fn, one global queue lock, the CFQ/deadline/noop elevators) and the newer blk-mq path. The single-queue path was deleted in 5.0; struct request_queue and struct request survive but now only describe the multi-queue world (blk-mq docs). This is why every modern request carries mq_ctx/mq_hctx and a tag — concepts that did not exist in the single-queue request. A reader looking at a pre-5.0 kernel or an old textbook will see a materially different struct request (with ->elevator_private, ->cmd[] SCSI payloads inline, and no tags); pin any claim about request internals to a specific kernel.

Uncertain

Verify: that blk_mq_alloc_disk() is the canonical path that allocates the gendisk and its request_queue together in v6.12. Reason: confirmed the request_queue->disk back-pointer and nr_requests = BLKDEV_DEFAULT_RQ (128) initialization directly in blkdev.h/blk-core.c, but did not read blk_mq_alloc_disk()/__alloc_disk_node() in this task. To resolve: read block/genhd.c and block/blk-mq.c blk_mq_alloc_disk() in v6.12. uncertain

Production Notes

Tuning /sys/block/<dev>/queue/nr_requests upward can help throughput-bound workloads on deep-queue NVMe by allowing more requests to accumulate (and thus more merging) before back-pressure, at the cost of latency and memory; downward it tightens latency. The companion knob /sys/block/<dev>/queue/max_sectors_kb is queue_limits.max_sectors expressed in kilobytes — lowering it bounds per-request size (useful to cap tail latency on slow devices), raising it (up to max_hw_sectors_kb) lets the layer build larger requests for streaming. Observability via blktrace/blkparse and the tracepoints block_getrq (a request was allocated, fired in blk_mq_submit_bio), block_rq_issue, and block_rq_complete lets you watch the exact request lifecycle described above on a live system; the /sys/kernel/debug/block/<dev>/ debugfs tree dumps live per-queue and per-hctx request state.

See Also