The bio (short for block I/O) is the kernel’s fundamental, in-flight description of a block-storage transfer: move these specific bytes of memory to or from this range of sectors on this device. Concretely, a struct bio names a target device (bi_bdev), an operation and flags (bi_opf — e.g. REQ_OP_READ), a starting sector and residual byte count (bi_iter), and a vector of memory segments (bi_io_vec, an array of struct bio_vec) that hold the data (blk_types.h v6.12). Everything above the block layer — the page cache, direct I/O, swap, filesystem journals — hands work down by allocating a bio, filling it, and calling submit_bio(). Everything below — stacking drivers like the device mapper and software RAID, and finally the real device driver — consumes bios and may emit new ones. The bio is therefore the lingua franca of the entire block stack; this note dissects its anatomy.
This note is pinned to Linux 6.12 LTS; every structure and line number below was read from the v6.12 blobs in sources.
Boundary with the neighbouring notes
This note owns the bio object itself — its fields, its segment array, its iterator, its lifecycle. Request Queues and struct request owns the request. The Block IO Submission Path owns the journey (submit_bio()’s recursion trick, partition remap, the stacking-driver loop). Software and Hardware Queues in blk-mq owns the queues a request is staged in, and The Multi-Queue Block Layer blk-mq owns the architecture above all of them. The one place this note deliberately steps across the line is the section immediately below, because the bio’s three possible fates are what define it by contrast with the request.
The Central Distinction: bio Is Submission, request Is Dispatch
The single most clarifying sentence about the block layer is this: the bio is the unit of submission, and the request is the unit of dispatch. They are not two names for the same thing at different altitudes, and they are not a simple 1:1 wrapping. They exist because the two ends of the block layer have incompatible ideas of what “one I/O” means.
Upward, toward the filesystem, the natural unit is “what the caller asked for.” A filesystem doing readahead, a write() through the page cache, a swap-out, a journal commit — each knows a set of pages and a place on disk, and wants to say so in one object. That object must be cheap to create (they are created constantly), arbitrary in size (the caller does not know the device’s limits and should not have to), and composable (a stacking driver must be able to take one and produce others). That is a bio.
Downward, toward the driver, the natural unit is “one command the hardware can execute.” A device command has a hard maximum transfer size, a hard maximum number of scatter-gather segments, possibly alignment constraints, and — crucially — it consumes a tag, a slot in a fixed-size pool of in-flight commands. That object must be bounded (it must fit the hardware), accountable (it has a tag, a timeout, and a state machine), and stable (the driver holds it until the device completes it). That is a struct request.
The block layer’s central job is converting between the two, and because the units are incompatible, the conversion is not a function — it is a three-way branch.
The Three-Way Fate of a bio
Every bio that enters blk_mq_submit_bio() meets exactly three possibilities, and they appear in the source in this order (block/blk-mq.c lines 2940–3008):
bio = __bio_split_to_limits(bio, &q->limits, &nr_segs); /* (1) SPLIT — line 2988 */ if (!bio) goto queue_exit; if (!bio_integrity_prep(bio)) goto queue_exit; if (blk_mq_attempt_bio_merge(q, bio, nr_segs)) /* (2) MERGE — line 2995 */ goto queue_exit; if (blk_queue_is_zoned(q) && blk_zone_plug_bio(bio, nr_segs)) goto queue_exit;new_request: /* (3) PROMOTE — line 3001 */ if (!rq) { rq = blk_mq_get_new_requests(q, plug, bio, nr_segs); if (unlikely(!rq)) goto queue_exit; } else { blk_mq_use_cached_rq(rq, plug, bio); }
Read in order, those three lines are the whole story:
#
Outcome
Triggered when
What happens to the bio
Tag consumed?
Cost
1
Split
The bio exceeds a queue limit — max_sectors, max_segments, or a virt/SG-gap boundary
A prefix is carved off as a new bio and submitted; the original is shortened and re-submitted. Repeats until every piece fits
One per resulting piece, eventually
One bio_alloc_clone per split. No data copy — the pieces share the parent’s bio_vec array
2
Merged
An adjacent request already exists on the plug list or in the elevator, and the bio is bio_mergeable()
The bio is absorbed: its bio_vecs are appended to that request’s bio chain and the request grows. The bio ceases to exist as an independent thing
No — this is the point
Cheapest outcome. A list walk and a pointer update
3
Promoted
Neither of the above applied
A fresh struct request is allocated, a driver tag taken from the hardware queue’s sbitmap, and the bio becomes that request’s first and only bio
Yes, one
Most expensive. May block on the tag waitqueue if the pool is exhausted
The three fates of a bio, in the order blk_mq_submit_bio() tries them. What it shows: the outcomes are ordered from “make it legal” through “make it free” to “make it real”. The insight: the ordering is not arbitrary — splitting must come first because you cannot merge an illegal bio, and merging must come before promotion because merging is the only outcome that costs no tag. Tags are the scarcest resource in the block layer, so the fast path is organised to avoid spending one.
Three consequences follow, and each is a common source of confusion.
The bio-to-request relationship is many-to-many, not many-to-one. The old formula “many bios, one request” is only half true. Merging makes many bios into one request; splitting makes one bio into many requests. A 1 MiB write on this host does both: it is split into eight 128 KiB pieces, and each piece becomes its own request. The honest statement is that there is no fixed arithmetic relationship at all — the block layer is free to re-cut the boundary wherever the hardware and the queue state suggest.
A bio can vanish without ever becoming a request. Outcome 2 consumes the bio entirely. This is why you cannot instrument the block layer by counting bios in and requests out and expecting the numbers to relate: on a merge-friendly workload, requests are far fewer than bios, and on a large-I/O workload, far more.
The goto queue_exit in outcomes 1 and 2 is not an error path. It is easy to misread — the label sounds like failure. It releases the q_usage_counter reference this invocation took and returns, because in outcome 1 the pieces have been separately submitted and in outcome 2 the bio is now someone else’s problem. Only outcome 3 falls through to build a request.
flowchart TB
START["submit_bio(bio)<br/>→ blk_mq_submit_bio()"] --> BOUNCE["blk_queue_bounce()<br/>bounce-buffer unaddressable pages"]
BOUNCE --> ALIGN{"bio_unaligned()?<br/>vs logical_block_size"}
ALIGN -->|yes| IOERR["bio_io_error()<br/>BLK_STS_IOERR"]
ALIGN -->|no| SPLITQ{"__bio_split_to_limits()<br/>exceeds max_sectors,<br/>max_segments or SG gap?"}
SPLITQ -->|yes| SPLIT["FATE 1: SPLIT<br/>carve prefix via bio_split()<br/>shares parent's bio_vec array<br/>shorten original, resubmit"]
SPLIT -.->|"remainder re-enters"| SPLITQ
SPLITQ -->|"no — it fits"| MERGEQ{"blk_mq_attempt_bio_merge()<br/>adjacent request on plug<br/>list or in elevator?"}
MERGEQ -->|yes| MERGE["FATE 2: MERGED<br/>absorbed into existing request<br/>NO tag consumed<br/>bio ceases to exist"]
MERGEQ -->|no| NEW["FATE 3: PROMOTED<br/>blk_mq_get_new_requests()<br/>allocate request + DRIVER TAG"]
NEW --> TAGQ{"tag available in<br/>hctx->tags sbitmap?"}
TAGQ -->|"no"| SLEEP["block on tag waitqueue<br/>until a completion frees one"]
SLEEP --> TAGQ
TAGQ -->|yes| RQ["struct request<br/>rq->bio = bio<br/>ready for a hardware queue"]
MERGE --> GROWN["the absorbed-into request<br/>is now larger"]
RQ --> DISPATCH["dispatch → driver → device"]
GROWN --> DISPATCH
The three fates of a bio as one decision tree. What it shows: the ordering of split → merge → promote, the loop that re-feeds a split remainder back through the limit check, and the single place a tag is consumed. The insight: only the rightmost path costs a tag, and only that path can block. Everything the block layer does on the fast path — plugging, merging, the per-CPU staging queues of Software and Hardware Queues in blk-mq — is machinery for reaching the middle branch more often, because a merged bio is an I/O the device never has to be told about separately.
Mental Model
Think of a bio as a scatter-gather list with a destination address on a disk. The destination is a (device, starting sector, length) triple. The source (or sink) is a list of physical memory chunks, each a struct bio_vec — a (page, length, offset-into-page) tuple. A single read() of 64 KiB whose target buffer spans sixteen scattered 4 KiB pages becomes one bio with sixteen bio_vec entries: the disk sectors are contiguous, but the memory is not, and the bio records exactly which pages receive which sectors.
The subtle, modern part of the design is the bvec_iter — a small cursor (bi_iter) that tracks how far through the bio_vec array the transfer has progressed. The bio_vec array itself is treated as immutable: when the block layer needs to consume the front of a bio, split it, or clone it, it does not rewrite the array — it only advances or copies the cheap little iterator. This “immutable biovec” property, introduced by Kent Overstreet in kernel 3.14 (2014), is what makes splitting and cloning bios cheap, and it is the single most important thing to understand about the structure.
flowchart TB
subgraph BIO["struct bio"]
HDR["bi_bdev (target device)<br/>bi_opf (REQ_OP_READ | flags)<br/>bi_status, __bi_cnt, __bi_remaining"]
ITER["bi_iter (bvec_iter cursor)<br/>bi_sector = 1000<br/>bi_size = 16384<br/>bi_idx = 0<br/>bi_bvec_done = 0"]
VEC["bi_io_vec -> bio_vec[ ] (immutable)"]
end
VEC --> V0["bio_vec[0]<br/>page A, len 4096, off 0"]
VEC --> V1["bio_vec[1]<br/>page B, len 4096, off 0"]
VEC --> V2["bio_vec[2]<br/>page C, len 8192, off 0<br/>(multipage: 2 phys-contig pages)"]
V0 --> DISK["sectors 1000..1031 on /dev/sda"]
V1 --> DISK
V2 --> DISK
Anatomy of a bio. What it shows: the header names the device and operation; the bvec_iter cursor (bi_iter) records the current position and the residual size; the bi_io_vec array points at memory segments, one of which (bio_vec[2]) is a multipage bvec covering two physically-contiguous pages in a single entry. The insight: consuming or splitting the bio only moves the cursor (bi_idx, bi_bvec_done, bi_sector, bi_size) — the bio_vec array is never rewritten, which is why split and clone are O(1).
The bio Header, Field by Field
The struct bio definition in blk_types.h is the canonical source. Walking the load-bearing fields (blk_types.h v6.12 lines 214–278):
struct bio *bi_next — links bios into a singly-linked list. Used both for the [[The Block IO Submission Path#the-bio_list-recursion-to-iteration-trick|bio_list]] machinery and for chaining bios onto a struct request.
struct block_device *bi_bdev — the target. A block_device may be a whole disk or a partition; the partition’s starting offset is added during partition remap.
blk_opf_t bi_opf — a 32-bit word whose bottom 8 bits encode the operation (enum req_op) and top 24 bits encode flags (enum req_flag_bits). REQ_OP_BITS is 8, REQ_OP_MASK is (1<<8)-1. So bio_op(bio) is just bio->bi_opf & REQ_OP_MASK.
unsigned short bi_flags — internal state flags (BIO_*), distinct from bi_opf. These track lifecycle state: BIO_CLONED, BIO_CHAIN, BIO_REFFED, BIO_REMAPPED, BIO_BPS_THROTTLED, etc. (lines 286–304).
blk_status_t bi_status — the completion status (BLK_STS_OK, BLK_STS_IOERR, BLK_STS_AGAIN, …), set on the way back up through [[#bio_endio-and-completion|bio_endio()]].
atomic_t __bi_remaining — the chain-completion counter (see chaining).
struct bvec_iter bi_iter — the cursor. This is where the “current position” lives.
bio_end_io_t *bi_end_io / void *bi_private — the completion callback and an opaque pointer for the submitter (or for chaining, the parent bio).
unsigned short bi_vcnt — how many bio_vec entries are populated.
unsigned short bi_max_vecs — capacity of the bi_io_vec array.
atomic_t __bi_cnt — the reference (pin) count.
struct bio_vec *bi_io_vec — the segment array.
struct bio_vec bi_inline_vecs[] — a flexible-array tail. For small bios the array is inlined into the same allocation as the header to avoid a second allocation; bi_io_vec then points at bi_inline_vecs. BIO_INLINE_VECS is 4 (blk.h v6.12), so up to four segments need no separate bvec allocation.
BIO_MAX_VECS caps a single bio at 256 segments (bio.h v6.12 line 13); larger transfers are expressed as multiple bios, typically chained.
The same fields grouped by the question they answer
The bullet list above follows declaration order, which is an artefact of cache-line packing rather than meaning. Grouped by purpose, the structure is much easier to hold in the head — there are only five questions a bio answers, and every field serves one of them:
Group
Fields
The question it answers
Mutable during flight?
Destination
bi_bdev
Which device or partition? Rewritten by stacking drivers — dm-crypt replaces it when re-submitting downstream
Yes — remapped at each stacking layer (BIO_REMAPPED guards double-remap)
Intent
bi_opf, bi_ioprio, bi_write_hint
What operation, with what semantics? Bottom 8 bits are the op, the rest are the REQ_* flags
Rarely — bio_clear_polled() on split is the notable case
Cgroup accounting, inline encryption, integrity metadata — compiled out entirely when the feature is off
Varies
The struct bio regrouped by role, from include/linux/blk_types.h lines 214–278. What it shows: six groups, of which the middle two carry the actual transfer. The insight is in the last column. “Position” is mutable and “Payload” is not, and that single asymmetry is the immutable-biovec design: because all progress is confined to a 20-byte bvec_iter and none of it touches the segment array, two bios can share one array and differ only by their cursors. Every cheap operation in this note — advance, clone, split — is a consequence of that one row-pair.
A correction on the flag word while we are here: bi_opf is blk_opf_t, which is typedef __u32 __bitwise blk_opf_t (line 205) — a genuine 32-bit word. REQ_OP_BITS is 8 (line 308), so the operation occupies bits 0–7 and the flags start at bit 8. But the flags do not fill the remaining 24 bits: enum req_flag_bits runs from __REQ_FAILFAST_DEV (bit 8) to __REQ_NOUNMAP, twenty-one flags in all, ending at bit 28 with __REQ_NR_BITS = 29. Three bits at the top are currently unused — which is why new flags such as REQ_ATOMIC (added for atomic writes) could be appended without widening the type.
One more structural detail worth extracting, because it explains bio_reset(): the declaration carries the comment “Everything starting with bi_max_vecs will be preserved by bio_reset()”, and the boundary is made explicit immediately after the struct as #define BIO_RESET_BYTES offsetof(struct bio, bi_max_vecs). Resetting a bio for reuse therefore zeroes the destination, intent, position and lifecycle groups, while keeping bi_max_vecs, __bi_cnt, bi_io_vec and bi_pool — precisely the fields that describe the allocation rather than the transfer. This is what lets a driver recycle one bio across many I/Os without reallocating its segment array.
struct bio_vec { struct page *bv_page; /* first page of the range */ unsigned int bv_len; /* number of bytes */ unsigned int bv_offset; /* start offset within bv_page */};
bv_page is the first page; bv_offset is where the data starts inside it; bv_len is how many bytes. Crucially, a single bio_vec can describe a range that spans several physically-contiguous pages — this is the multipage bvec (see below). The header comment states the invariant directly: “The following holds for a bvec if n * PAGE_SIZE < bv_offset + bv_len: nth_page(@bv_page, n) == @bv_page + n” (bvec.h lines 25–29) — i.e. the pages a multipage bvec covers must be contiguous in the struct page array.
struct bvec_iter { sector_t bi_sector; /* device address in 512-byte sectors */ unsigned int bi_size; /* residual I/O count (bytes left) */ unsigned int bi_idx; /* current index into bvl_vec */ unsigned int bi_bvec_done; /* bytes completed in current bvec */} __packed __aligned(4);
bi_sector — the current disk sector, in 512-byte units (SECTOR_SHIFT is 9, so one sector is 512 bytes regardless of the device’s logical block size).
bi_size — bytes remaining in the transfer. When this hits zero the bio is fully consumed.
bi_idx — index of the bio_vec the cursor is currently in.
To advance a bio by nbytes, the layer calls bio_advance(), which adds nbytes >> 9 sectors to bi_sector, subtracts nbytes from bi_size, and walks bi_idx/bi_bvec_done forward (bio.h lines 98–143; bvec_iter_advance() in bvec.h lines 140–162). Notice what it does not touch: bi_io_vec and its entries are untouched. The “position” of a bio is entirely in the iterator.
The division of labour, in one table
bio_vec and bvec_iter are two halves of one mechanism, and the cleanest way to hold them is by what each is allowed to do:
struct bio_vec (the array)
struct bvec_iter (the cursor)
Reached via
bio->bi_io_vec, bio->bi_vcnt entries
bio->bi_iter, exactly one
Size
3 words (bv_page, bv_len, bv_offset) — 16 B on x86-64
4 fields, __packed __aligned(4) — 20 B
Answers
Which memory?
How far through it are we, and where on disk?
Describes
A (first page, byte offset, byte length) range; may span several physically-contiguous pages (multipage bvec)
bi_sector (current disk sector, 512 B units), bi_size (bytes remaining), bi_idx (current array index), bi_bvec_done (bytes consumed within that entry)
Mutated by the block layer?
Never, once built. Only bio_add_page() and friends write it, at construction
Constantly.bio_advance(), bio_split(), every bio_for_each_* step
Shared between bios?
Yes — a split or clone points at the parent’s array
No — each bio owns its own cursor. This is the only difference between a split and its parent
Cost to copy
O(n) in segments — never done on the fast path
O(1), 20 bytes
Who may read all of it
Only an owner of the whole bio (bio_for_each_segment_all)
Anyone holding the bio
The two halves of a bio’s payload description. What it shows: identical information content, opposite mutability. The insight is the “Shared between bios?” row. Because the array is immutable and the cursor is not, two bios can point at one array and mean different things purely by holding different cursors. That is the entire trick — and it is why bio_split() allocates only a 100-odd-byte header and copies a 20-byte iterator, no matter whether the bio describes 4 KiB or 1 MiB.
Concretely, here is what one 16 KiB bio looks like in memory after a split, with both children sharing the parent’s array:
flowchart LR
subgraph PARENT["bio A (shortened original)"]
PI["bi_iter<br/>bi_sector = 1008<br/>bi_size = 8192<br/>bi_idx = 2<br/>bi_bvec_done = 0"]
end
subgraph CHILD["bio B (the split-off prefix)"]
CI["bi_iter<br/>bi_sector = 1000<br/>bi_size = 8192<br/>bi_idx = 0<br/>bi_bvec_done = 0"]
end
subgraph ARR["ONE shared bio_vec array (immutable)"]
direction TB
V0["[0] page P0<br/>bv_offset 0, bv_len 4096"]
V1["[1] page P1<br/>bv_offset 0, bv_len 4096"]
V2["[2] page P2<br/>bv_offset 0, bv_len 8192<br/>MULTIPAGE: P2 and P3<br/>physically contiguous"]
end
PI -->|"bi_io_vec"| ARR
CI -->|"bi_io_vec (same pointer)"| ARR
V0 --> PG0["struct page P0"]
V1 --> PG1["struct page P1"]
V2 --> PG2["struct page P2"]
V2 --> PG3["struct page P3<br/>(reached as P2+1)"]
CI -.->|"covers entries 0-1<br/>sectors 1000-1015"| V0
PI -.->|"covers entry 2 onward<br/>sectors 1016-1031"| V2
A 16 KiB bio after bio_split(bio, 16, ...), drawn as it actually sits in memory. What it shows: two bio headers, two cursors, onebio_vec array — and one array entry ([2]) that is a multipage bvec covering two physically-contiguous pages in a single 16-byte slot. The insight: nothing was copied and nothing was rewritten. The child’s cursor starts at index 0 with 8,192 bytes remaining; the parent’s cursor was advanced to index 2 and its bi_sector moved forward by 16 sectors. The dotted arrows are not pointers — they show which slice of the shared array each cursor’s (bi_idx, bi_bvec_done, bi_size) triple currently designates. This is also exactly why bio_for_each_segment_all is forbidden to drivers: walking the raw array from bio B would march straight into bio A’s data.
This is the immutable biovec design. Before kernel 3.14, advancing a bio mutated the bv_offset/bv_len of the first bio_vec and bumped bi_idx, which meant a clone had to be a fresh copy and a split was awkward. By moving all mutable state into bvec_iter and freezing the array, two bios can share the same bio_vec array and merely hold different iterators — which is exactly how bio_split() works.
Multipage bvecs
A multipage bvec is a bio_vec whose bv_len exceeds PAGE_SIZE, describing several physically-contiguous pages in one entry. Introduced by Ming Lei and merged in kernel 5.1 (2019), this reduces the number of bio_vec entries — and therefore the memory and per-segment iteration cost — for large transfers over contiguous memory (such as huge pages or large folios) (bvec.h invariant comment).
Resolved (2026-09-12) by existence-checking the headers at each tag. An earlier revision of this note flagged both dates as recalled rather than verified. Fetching the relevant header at successive release tags and testing for the defining symbol dates each change precisely — a 404-vs-200 or present-vs-absent boundary between two tags brackets the merge to exactly one release:
Tag
include/linux/bio.h size
Occurrences of struct bvec_iter
Verdict
v3.12
17,626 B
0
before
v3.13
17,726 B
0
before
v3.14
19,446 B
5
← immutable biovecs land here
v3.15
19,472 B
5
after
Tag
include/linux/bvec.h size
nth_page invariant comment
mp_bvec_iter_* helpers
Verdict
v4.20
3,626 B
0
0
before
v5.0
3,626 B
0
0
before
v5.1
5,657 B
7
13
← multipage bvecs land here
v5.2
4,650 B
0
13
after
Dating two design changes by existence-checking rather than by recall. What it shows:struct bvec_iter is absent from bio.h at v3.13 and present at v3.14; the mp_bvec_iter_* multipage helpers are absent from bvec.h at v5.0 and present at v5.1. The insight: both previously-flagged dates are confirmed — immutable biovecs in 3.14, multipage bvecs in 5.1. Note the v5.2 row as a caution about weak tests: the nth_page comment count drops back to 0 there because the comment was reworded, while the mp_bvec_iter_* symbols (the real feature) persist. Test for the symbol, not for prose.
Attribution to Kent Overstreet (immutable biovecs) and Ming Lei (multipage bvecs) is separately corroborated by the kernel git log, reachable through cgit’s grep interface (git.kernel.org log search for “immutable biovecs”), which returns the 3.14-era series including “block: Generic bio chaining” and “block: Remove bi_idx hacks” — the commits that moved the position state out of the array and into the iterator.
bio_for_each_segment(bvl, bio, iter) yields single-pagebio_vecs. Each iteration produces a chunk no larger than PAGE_SIZE - offset, splitting a multipage bvec back into page-sized pieces. Use this when the consumer can only handle one page at a time (bvec_iter_len caps the length at the page boundary, bvec.h lines 125–127).
bio_for_each_bvec(bvl, bio, iter) yields the whole multipagebio_vec unchanged (mp_bvec_iter_* helpers). Use this when the consumer can DMA a multi-page contiguous range in one descriptor — e.g. when building scatter-gather lists for the device, since the hardware happily takes a longer contiguous segment.
The __segment_all variants (bio_for_each_segment_all, bio_for_each_bvec_all) iterate the raw array directly and carry a loud warning: “drivers should never use the all version - the bio may have been split before it got to the driver and the driver won’t own all of it” (bio.h lines 91–96). A split bio shares the parent’s full array but only “owns” the slice its iterator covers, so iterating all entries would touch memory belonging to a sibling bio.
How segments merge as pages are added
Pages enter a bio through bio_add_page() (and bio_add_folio()). Before appending a new bio_vec, the code tries to grow the last one via bvec_try_merge_page() (bio.c lines 920–945): if the new page’s physical address is exactly contiguous with the end of the last segment (vec_end_addr + 1 == page_addr + off), it simply does bv->bv_len += len — extending the existing segment into a (longer) multipage bvec rather than consuming a new array slot. This is how a large contiguous folio collapses into a single bio_vec. bio_full() (bio.c lines 911–918) stops the merge when bi_vcnt reaches bi_max_vecs or bi_size would overflow UINT_MAX.
Chaining bios
A single logical transfer often needs more than one bio — because it exceeds BIO_MAX_VECS segments, or because a stacking driver must split it to fit device limits. bio_chain() ties a child bio to a parent so the parent’s completion is deferred until the child also completes (bio.c lines 338–346):
void bio_chain(struct bio *bio, struct bio *parent){ BUG_ON(bio->bi_private || bio->bi_end_io); bio->bi_private = parent; /* remember the parent */ bio->bi_end_io = bio_chain_endio; /* our completion hook */ bio_inc_remaining(parent); /* parent owes one more completion */}
bio_inc_remaining() sets BIO_CHAIN and atomically increments the parent’s __bi_remaining (bio.h lines 609–614). When the child finishes, bio_chain_endio() propagates any error up to the parent (if (bio->bi_status && !parent->bi_status) parent->bi_status = bio->bi_status;), frees the child with bio_put(), and calls bio_endio() on the parent (bio.c lines 312–325). The parent’s real bi_end_io runs only when its __bi_remaining reaches zero. Chains are how the block layer turns “one big request” into a tree of device-sized bios while preserving a single completion to the original submitter.
bio_split
bio_split() carves the first sectors worth off the front of a bio, returning a new bio for that prefix and shortening the original to the remainder (bio.c lines 1726–1754):
struct bio *bio_split(struct bio *bio, int sectors, gfp_t gfp, struct bio_set *bs){ struct bio *split; BUG_ON(sectors <= 0); BUG_ON(sectors >= bio_sectors(bio)); if (WARN_ON_ONCE(bio_op(bio) == REQ_OP_ZONE_APPEND)) /* zone append cannot split */ return NULL; split = bio_alloc_clone(bio->bi_bdev, bio, gfp, bs); /* shares bi_io_vec */ if (!split) return NULL; split->bi_iter.bi_size = sectors << 9; /* prefix is `sectors` long */ if (bio_integrity(split)) bio_integrity_trim(split); bio_advance(bio, split->bi_iter.bi_size); /* original skips the prefix */ ... return split;}
The doc comment makes the immutable-biovec payoff explicit: “Unless this is a discard request the newly allocated bio will point to @bio’s bi_io_vec” (bio.c lines 1722–1725). The split shares the parent’s bio_vec array; only the two iterators differ — split covers [0, sectors) and the shortened original covers [sectors, end). No memory is copied. A REQ_OP_ZONE_APPENDbio is the one operation that may never be split (the device assigns its write position, so the data must land as one contiguous unit) — see Zone Append and the Zoned Block Interface.
Reference Counting and bio_endio — completion
A bio carries two distinct atomic counters, which is a common point of confusion:
__bi_cnt is the pin/reference count, manipulated by bio_get()/bio_put(). It keeps the biomemory alive — e.g. so the submitter can inspect the bio after submit_bio() returns. bio_put() decrements it; the last put frees the bio (returning it to its bio_set pool, or to the per-CPU cache if REQ_ALLOC_CACHE is set) (bio.c lines 811–823). To avoid an atomic on the common case, bio_get() first sets the BIO_REFFED flag; bio_put() only does the atomic dance when that flag is present.
__bi_remaining is the chain-completion count described above, manipulated by bio_inc_remaining() and consumed in bio_endio().
bio_endio() is the single entry point for completing a bio (bio.c lines 1665–1710). Its logic:
bio_remaining_done() — if the bio is chained (BIO_CHAIN), atomically decrement __bi_remaining; if it is not yet zero, return without completing (bio.c lines 1633–1650). A non-chained bio always has __bi_remaining == 1 and completes on the first call.
Run integrity/zone/rq_qos completion hooks; emit the block_bio_complete tracepoint if BIO_TRACE_COMPLETION is set.
Walk the chain iteratively: if bi_end_io == bio_chain_endio, the function does bio = __bio_chain_endio(bio); goto again; rather than recursing (bio.c lines 1690–1693). The comment explains why: a chain of stacked devices could be arbitrarily deep, and recursion would blow the kernel stack, so completion is hand-rolled into a loop.
Finally call the real bi_end_io(bio) — which for a filesystem read might unlock the page-cache pages and mark them up-to-date.
The whole completion side is asynchronous and runs in whatever context the device’s interrupt or completion thread provides; the original submitter learns of completion only through bi_end_io. On this host that context is hard-IRQ, with neither a softirq nor an IPI in between — measured and explained in the queue note.
stateDiagram-v2
[*] --> Allocated: bio_alloc_bioset()<br/>__bi_cnt = 1<br/>__bi_remaining = 1
Allocated --> Built: bio_add_page() / bio_add_folio()<br/>bvec_try_merge_page() grows<br/>the last segment if contiguous
Built --> Submitted: submit_bio()
Submitted --> Split: exceeds a queue limit
Split --> Submitted: parent shortened<br/>and re-submitted
Split --> Chained: bio_chain(child, parent)<br/>bio_inc_remaining(parent)<br/>__bi_remaining++
Submitted --> Absorbed: blk_mq_attempt_bio_merge()<br/>succeeded
Absorbed --> [*]: no request, no tag<br/>bio lives on inside another request
Submitted --> InRequest: promoted<br/>request + driver tag
Chained --> InRequest
InRequest --> Completing: device completes<br/>bio_endio()
Completing --> Waiting: BIO_CHAIN set and<br/>__bi_remaining > 0 after decrement
Waiting --> Completing: a sibling completes
Completing --> Done: __bi_remaining reached 0<br/>bi_end_io(bio) runs
Done --> Freed: bio_put()<br/>__bi_cnt reached 0
Freed --> [*]: returned to bio_set<br/>or per-CPU cache (REQ_ALLOC_CACHE)
The lifecycle of a bio, with both counters made visible. What it shows: the three exits from Submitted are the three fates from the top of this note, and the Waiting loop is the chain-completion counter holding a parent back until every child reports. The insight: the two counters govern different transitions and neither can substitute for the other. __bi_remaining gates Completing → Done (is the work finished?); __bi_cnt gates Done → Freed (is the memory still referenced?). A bio can be Done but not Freed, because the submitter still holds a reference to read bi_status. Note also that Absorbed is a terminal state reached without ever allocating a request or a tag — the cheapest possible life a bio can have.
Operation and Flag Encoding
bi_opf packs operation and flags into one 32-bit word (blk_types.h lines 305–420). The operation (enum req_op, bottom 8 bits) is one of:
REQ_OP_READ (0) and REQ_OP_WRITE (1) — the everyday data transfers. The least-significant bit encodes direction: odd = to the device.
REQ_OP_DISCARD (3), REQ_OP_SECURE_ERASE (5), REQ_OP_WRITE_ZEROES (9) — “no payload” ops where bi_size means a range to operate on, not data to move; bio_has_data() returns false for these (bio.h lines 54–71).
REQ_OP_ZONE_APPEND (7) and the zone-management ops REQ_OP_ZONE_OPEN/CLOSE/FINISH/RESET/RESET_ALL (10–15) — for zoned devices.
Enumerated exhaustively from include/linux/blk_types.h lines 325–356 — every value in enum req_op at v6.12, with the comment the kernel itself attaches to each:
REQ_OP_*
Value
Kernel comment
Carries data?
Splittable?
Notes
REQ_OP_READ
0
“read sectors from the device”
Yes
Yes (bio_split_rw)
Even value → from device
REQ_OP_WRITE
1
“write sectors to the device”
Yes
Yes (bio_split_rw)
Odd value → to device
REQ_OP_FLUSH
2
“flush the volatile write cache”
No
No
Synthesized by the flush machinery, not normally submitted by a filesystem
REQ_OP_DISCARD
3
“discard sectors”
No
Yes (bio_split_discard)
bi_size is a range, not a buffer length
REQ_OP_SECURE_ERASE
5
“securely erase sectors”
No
Yes (bio_split_discard)
Like discard but the device must make data unrecoverable
REQ_OP_ZONE_APPEND
7
“write data at the current zone write pointer”
Yes
Never — see below
The device chooses the LBA and reports it back
REQ_OP_WRITE_ZEROES
9
“write the zero filled sector many times”
No
Yes (bio_split_write_zeroes)
REQ_NOUNMAP modifies whether blocks are also deallocated
REQ_OP_ZONE_OPEN
10
“Open a zone”
No
No
Zone management
REQ_OP_ZONE_CLOSE
11
“Close a zone”
No
No
Zone management
REQ_OP_ZONE_FINISH
12
“Transition a zone to full”
No
No
Zone management
REQ_OP_ZONE_RESET
13
“reset a zone write pointer”
No
No
Zone management
REQ_OP_ZONE_RESET_ALL
15
“reset all the zone present on the device”
No
No
Zone management
REQ_OP_DRV_IN
34
“Driver private requests”
Driver-defined
No
Passthrough — e.g. an NVMe admin command
REQ_OP_DRV_OUT
35
“Driver private requests”
Driver-defined
No
Passthrough
REQ_OP_LAST
36
—
—
—
Sentinel, not an operation
Every operation in enum req_op at v6.12. What it shows: fifteen real operations across four families — data transfer (0, 1, 7), cache control (2), space management (3, 5, 9), zone management (10–15) and driver passthrough (34, 35). Two insights. First, the values are deliberately sparse — 4, 6, 8, 14 and 16–33 are gaps left by removed operations (REQ_OP_WRITE_SAME among them), so never iterate 0..REQ_OP_LAST assuming every value is valid. Second, the low bit is load-bearing: op_is_write() is literally (op & 1), which is why every device-bound operation was given an odd number and every device-sourced one an even number. That is a deliberate encoding, not a coincidence.
The “Splittable?” column maps directly onto __bio_split_to_limits() (block/blk.h lines 371–393), which is a switch on bio_op(bio) dispatching to four different split functions, with a default that declares “other operations can’t be split” and sets *segs = 0. The one prohibition that is enforced with a warning rather than by omission is zone append — bio_split() itself refuses it:
if (WARN_ON_ONCE(bio_op(bio) == REQ_OP_ZONE_APPEND)) return NULL;
The flags (enum req_flag_bits, bits 8 upward) modify behavior. Enumerated in full from lines 359–415:
Flag
Bit
Kernel comment
Semantics — what it actually changes
REQ_FAILFAST_DEV
8
“no driver retries of device errors”
Error-recovery policy; used by multipath and RAID, which prefer a fast failure they can route around to a slow retry
REQ_FAILFAST_TRANSPORT
9
“no driver retries of transport errors”
As above, for link/fabric errors
REQ_FAILFAST_DRIVER
10
“no driver retries of driver errors”
As above, for driver-internal errors
REQ_SYNC
11
“request is sync (sync write or read) ”
Someone is waiting. Schedulers prioritise it over async writeback, and op_is_sync() feeds blk-mq’s is_sync in blk_mq_submit_bio(). Reads are implicitly sync; writes are sync only when marked
REQ_META
12
“metadata io request”
Filesystem metadata. Lets schedulers and tracing distinguish a journal commit from bulk data
REQ_PRIO
13
“boost priority in cfq”
Vestigial naming — CFQ was removed with the single-queue layer, but the bit still marks elevated priority
REQ_NOMERGE
14
“don’t touch this for merging”
Forecloses fate 2.bio_mergeable() returns false, so the bio can never be absorbed into an existing request
REQ_IDLE
15
“anticipate more IO after this one”
A hint that the submitter will issue more shortly — schedulers may hold the queue briefly rather than dispatching immediately
REQ_INTEGRITY
16
“I/O includes block integrity payload”
bi_integrity is populated; split and clone must carry it (bio_integrity_trim())
REQ_FUA
17
“forced unit access”
Durability, after. This write must be on stable media before it is reported complete — the device may not leave it in its volatile cache. Also blocks merging
REQ_PREFLUSH
18
“request for cache flush”
Durability, before. Everything previously written must be flushed to stable media before this write is issued. Also blocks merging. REQ_PREFLUSH + REQ_FUA on one write is how a journal commit gets ordering and durability in a single submission
REQ_RAHEAD
19
“read ahead, can fail anytime”
Speculative. Nobody is waiting, so the block layer may fail it immediately rather than retrying or blocking — notably it is failed rather than queued when the device is congested. A dropped readahead costs a later cache miss, never correctness
REQ_BACKGROUND
20
“background IO”
Not latency-sensitive; throttled ahead of foreground work by rq_qos/wbt
REQ_NOWAIT
21
“Don’t wait if request will block”
Fail with BLK_STS_AGAIN rather than sleep. Used by io_uring in non-blocking mode. Interacts with splitting: bio_split_rw_at() returns -EAGAIN if a REQ_NOWAIT bio would need splitting, since splitting implies allocation
REQ_POLLED
22
“caller polls for completion using bio_poll”
Routes to an HCTX_TYPE_POLL queue and suppresses the completion interrupt. Cleared on split by bio_clear_polled() — see below
REQ_ALLOC_CACHE
23
“allocate IO from cache if available”
Allocate from the per-CPU bio cache; bio_put() returns it there instead of to the bio_set
REQ_SWAP
24
“swap I/O”
Paging traffic. Must not recurse into reclaim — the allocator constraints differ
REQ_DRV
25
“for driver use”
Private to the driver
REQ_FS_PRIVATE
26
“for file system (submitter) use”
Private to the filesystem
REQ_ATOMIC
27
“for atomic write operations”
The write must be all-or-nothing. Cannot be split at all — bio_split_rw_at() returns -EINVAL rather than dividing it, and get_max_io_size() deliberately ignores lim->max_sectors for atomic writes “because it may less than the actual bio size, which we cannot tolerate”
REQ_NOUNMAP
28
“do not free blocks when zeroing”
Command-specific, for REQ_OP_WRITE_ZEROES only: write zeros but keep the blocks allocated
All twenty-one REQ_* flags at v6.12, in bit order. What it shows: the flags fall into five jobs — error policy (8–10), scheduling hints (11–13, 15, 20), durability (17–18), merge control (14, 17, 18), and submission-context constraints (21–24, 27). The insight: several flags are not hints at all but hard constraints that change the control flow of submission. REQ_NOWAIT and REQ_ATOMIC can make a split fail rather than proceed, and REQ_PREFLUSH/REQ_FUA remove an entire fate from the three-way branch.
That last point has an exact expression in the source, and it is a good example of why enumerating from the header beats recalling from memory (line 419):
Three different flags forbid merging, not one.REQ_NOMERGE does so by name, but REQ_PREFLUSH and REQ_FUA do so as a consequence of their semantics: both are statements about ordering and durability relative to a specific write, and merging that write into a neighbouring request would silently extend those guarantees to data the submitter never asked to protect — or, worse, lose the ordering point entirely. bio_mergeable() tests this mask, so a journal commit carrying REQ_PREFLUSH | REQ_FUA always takes fate 3 and becomes its own request. See Write Barriers FUA and Cache Flushes.
A second interaction worth having in one place, since it is scattered across three files — what splitting does to flags:
Flag on the original bio
Effect when __bio_split_to_limits() must split
Source
REQ_ATOMIC
Split refused: bio_split_rw_at() returns -EINVAL
block/blk-merge.c
REQ_NOWAIT
Split refused: returns -EAGAIN, bio ends with BLK_STS_AGAIN
block/blk-merge.c
REQ_POLLED
Silently cleared by bio_clear_polled(bio) — the I/O falls back to interrupt completion
block/blk-merge.c
REQ_INTEGRITY
Carried and trimmed: bio_integrity_trim(split) narrows the integrity payload to the prefix
block/bio.c
everything else
Inherited verbatim by both pieces via bio_alloc_clone()
block/bio.c
What a split does to the flag word. The insight: the REQ_POLLED row is the surprising one, and the kernel explains itself in a comment — “Bio splitting may cause subtle trouble such as hang when doing sync iopoll in direct IO routine. Given performance gain of iopoll for big IO can be trival, disable iopoll when split needed.” A large polled O_DIRECT read that exceeds max_sectors_kb therefore stops being polled, quietly. An io_uring benchmark that sets IORING_SETUP_IOPOLL and then issues 1 MiB reads on this host is not measuring the polled path at all, because every one of those bios is split into 128 KiB pieces and loses the flag on the way.
Callers are not expected to test these bits by hand. A family of small inline helpers wraps both halves of bi_opf (include/linux/blk_types.h lines 431–490): bio_op(bio) masks off the operation, op_is_write() tests the low bit, and op_is_sync(), op_is_flush(), op_is_discard() and op_is_zone_mgmt() classify by meaning rather than by value. Prefer them — the sparse numbering shown in the operation table above makes hand-rolled comparisons fragile, and op_is_sync() in particular is not a single bit test: it is true for reads, for REQ_SYNC, and for the flush-related operations, so bi_opf & REQ_SYNC and op_is_sync(bi_opf) are genuinely different questions.
Splitting Measured on Real Hardware
The limits that force fate 1 are not abstract — they are four world-readable files, and their values on this host explain exactly where a bio gets cut. Measured 2026-09-12 on a 32-CPU AMD Ryzen AI MAX+ 395 running Fedora kernel 7.1.13-200.fc44.x86_64, with a Crucial CT4000T710SSD8 NVMe drive.
The limits that cut a bio
$ cd /sys/block/nvme0n1/queue$ for f in max_sectors_kb max_hw_sectors_kb max_segments max_segment_size \> logical_block_size physical_block_size virt_boundary_mask nomerges; do> printf "%-22s %s\n" "$f" "$(cat $f)"> donemax_sectors_kb 128max_hw_sectors_kb 128max_segments 33max_segment_size 4294967295logical_block_size 512physical_block_size 512virt_boundary_mask 4095nomerges 0
Each of these appears by name in bio_split_rw_at() (block/blk-merge.c lines 300–371), whose loop walks the bvecs accumulating bytes and nsegs until one of three conditions trips:
Limit
Value here
Where it is tested
What it means physically
max_sectors (max_sectors_kb)
128 KiB
bytes + bv.bv_len <= max_bytes
The largest transfer the block layer will put in one request. Derived from max_hw_sectors_kb, itself from NVMe’s MDTS (Maximum Data Transfer Size)
max_segments
33
nsegs < lim->max_segments
The most scatter-gather entries one command may carry
max_segment_size
4 GiB − 1
inside bvec_split_segs()
Effectively unlimited here — not the binding constraint
virt_boundary_mask
4095
bvec_gap_to_prev()
The NVMe PRP constraint. Every segment after the first must begin on a 4 KiB boundary — a “virtual boundary” gap forces a split even when size and count are both fine
logical_block_size
512 B
ALIGN_DOWN(bytes, lim->logical_block_size)
The split point is rounded down to a multiple of this, so both halves stay block-aligned
The five queue limits that decide where a bio is cut, with this host’s values. The insight is the relationship between rows one and two.max_sectors_kb = 128 KiB is 32 pages of 4 KiB — yet max_segments is 33, one more. That is not a coincidence and not slack: a 128 KiB buffer that is page-aligned occupies 32 pages, but one that starts part-way into a page straddles 33. The driver asks for 33 segments precisely so that an unaligned 128 KiB transfer still fits in one command rather than being split for want of a single segment. Seeing max_segments = max_sectors_kb/4 + 1 is the signature of an NVMe device using PRP lists.
virt_boundary_mask = 4095 deserves the emphasis because it is the limit people forget. NVMe’s Physical Region Page (PRP) addressing cannot express a segment that starts mid-page — only the first segment of a transfer may have a non-zero offset. So bvec_gap_to_prev() splits the bio whenever a later segment would begin at a non-4 KiB-aligned address, regardless of how much room is left under max_sectors. A scatter-gather list that is legal for a SATA device can be illegal for an NVMe one purely because of alignment.
Watching the cut happen
Four hundred 1 MiB writes, each eight times larger than max_sectors_kb:
measured 3,242, the excess being fsync and filesystem metadata
This is bio splitting, directly observed.dd issued 400 write syscalls. The direct-I/O layer built 400 bios of 1 MiB each. The device saw 3,242 commands averaging 126.6 KiB. Nothing between those numbers is mysterious: each 1 MiB bio hit bio_may_need_split(), was carved into eight 128 KiB pieces by repeated bio_split_rw(), and each piece became its own request with its own tag.
The filesystem’s own view agrees:
$ filefrag ~/.v8-io-test.bin/home/linman/.v8-io-test.bin: 3187 extents found
3,187 extents for a 400 MiB file is 128.5 KiB per extent — btrfs allocated in units matching the device’s transfer limit.
A measurement trap worth recording
The first attempt at this used if=/dev/zero and produced incoherent numbers — 400 MiB in, roughly 72 MiB of sectors out. The cause was not the block layer:
/home is btrfs with zstd compression, and a stream of zeros compresses to almost nothing before it ever becomes a bio. Any measurement of bytes-to-device must use incompressible data on a compressing filesystem, which is why the run above sources from /dev/urandom via tmpfs. O_DIRECT does not exempt you — btrfs does not take the pure direct path for a compressed file.
The same bio, split, as a sequence
sequenceDiagram
participant FS as Direct I/O layer
participant SB as blk_mq_submit_bio
participant SP as __bio_split_to_limits
participant BS as bio_split
participant RQ as request + tag
participant DEV as nvme0n1
FS->>SB: submit_bio(bio, 1 MiB, 2048 sectors)
SB->>SP: __bio_split_to_limits(bio, &q->limits)
SP->>SP: bio_may_need_split() — 2048 > 256 sectors
SP->>SP: bio_split_rw_at() walks bvecs, stops at max_bytes
Note over SP: bytes = ALIGN_DOWN(131072, 512)<br/>returns 256 sectors
SP->>BS: bio_split(bio, 256, gfp, bs)
BS->>BS: bio_alloc_clone — child shares parent bi_io_vec
BS->>BS: child bi_size = 131072
BS->>BS: bio_advance(parent, 131072)
Note over BS: NO DATA COPIED<br/>one 20-byte iterator differs
BS-->>SB: child bio (128 KiB prefix)
SB->>RQ: allocate request, take driver tag
RQ->>DEV: queue_rq — command 1 of 8
Note over SB,DEV: parent, now 896 KiB, is re-submitted<br/>and re-enters the same path
SB->>SP: submit remainder (1792 sectors)
Note over SP,DEV: repeats 7 more times
DEV-->>FS: 8 completions, then bi_end_io on the original
One 1 MiB O_DIRECT write being cut into eight device commands. What it shows: the recursion at the heart of fate 1 — split off a legal prefix, submit it, shorten the original, and re-submit the original through the same code. The insight: the expensive-sounding operation in the middle allocates one bio header and copies one 20-byte iterator. The 128 KiB of data is never touched, never copied, and never even read by the splitting code — only the bio_vec descriptors are walked. Multiply by the 3,242 splits measured above and the total data movement attributable to splitting is still zero bytes.
Limits are inherited up a stacking chain
The same four files on the other block devices on this host show where the limits come from:
Limit
nvme0n1
dm-0 (dm-crypt on nvme0n1p3)
zram0 (RAM-backed swap)
max_sectors_kb
128
128
124
max_segments
33
33
128
max_segment_size
4 GiB − 1
4 GiB − 1
64 KiB
virt_boundary_mask
4095
4095
0
logical_block_size
512
512
4096
write_cache
write back
write back
write through
Queue limits across this host’s three interesting block devices. What it shows:dm-0’s limits are identical to the NVMe drive’s, down to the peculiar max_segments = 33 and the NVMe-specific virt_boundary_mask. zram0’s are completely different. The insight: dm-crypt has no hardware and therefore no native limits — it inherits them by stacking, via blk_stack_limits(), so that a bio it emits downstream is already legal for the device below. This is why a bio is usually split once, at the top of the stack, rather than again at every layer. zram0 is the contrast: RAM imposes no DMA constraints at all, so virt_boundary_mask is 0 and any segment alignment is legal, while its 4,096-byte logical_block_size reflects that it stores whole compressed pages.
That inheritance is why the step-5 measurement in the queue note showed dm-0 and nvme0n1p3 reporting identical sector counts: dm-crypt received already-legal 128 KiB bios, encrypted each into a new bio of the same geometry, and passed them down without needing to split again.
What could not be measured here
Two demonstrations were blocked, both by privilege rather than by absence:
Demonstration
Blocked by
Resolving command
Per-hctx queue state, and each queue’s type (DEFAULT/READ/POLL)
/sys/kernel/debug/block/ is root-only — ls: cannot access '/sys/kernel/debug/block/': Permission denied
sudo ls /sys/kernel/debug/block/nvme0n1/hctx0/
Live per-split tracing (block_split, block_bio_queue tracepoints) to count splits directly rather than inferring them from diskstats
/sys/kernel/debug/tracing/ is root-only
sudo cat /sys/kernel/debug/tracing/events/block/block_split/format, then enable it and read trace_pipe
Multi-depth queue-depth sweeps
fio is not installed (which fio → not found)
sudo dnf install fio
The split count reported above is therefore an inference from /proc/diskstats request counts and average sizes, not a direct tracepoint count. The inference is tight — 3,242 measured against 3,200 predicted, with the difference attributable to fsync metadata — but a block_split tracepoint count would settle it exactly.
Common Misunderstandings
“A bio is a request.” No. A bio is the unit of submission — the descriptor of a transfer the caller wants; a [[Request Queues and struct request|struct request]] is the unit of dispatch — one command the hardware can execute, holding a tag. See the central distinction.
“Many bios, one request.” This is the half-truth that replaces the previous misunderstanding, and it is worth correcting explicitly because it is repeated everywhere — including in an earlier revision of this note. Merging does make many bios into one request (fate 2), but splitting makes one bio into many requests (fate 1), and both happen constantly. A 1 MiB write on this host becomes eight requests. The relationship is many-to-many and there is no fixed arithmetic between bio count and request count; see the three-way fate.
“Splitting a bio copies the data.” No — and this is the whole point of immutable biovecs. bio_split() shares the bio_vec array and only allocates a new header; the two bios differ only in their iterators (bio.c lines 1722–1753).
“__bi_cnt and __bi_remaining are the same counter.” They are not. __bi_cnt keeps the memory alive (pin count); __bi_remaining defers completion until a chain finishes. Conflating them leads to reasoning errors about lifetime.
“You can iterate a bio’s pages with bio_for_each_segment_all.” Only if you own the whole bio. After a split, the array is shared, and the _all variants would walk into a sibling’s segments — hence the in-source warning.
See Also
The Block IO Submission Path — what submit_bio() does with the bio you just built; splitting to device limits, the bio_list trick, and where bios become requests.
Software and Hardware Queues in blk-mq — where a promoted bio lands once it is a request: the per-CPU staging queue and the hardware dispatch queue, with the same host’s queue limits measured from the other side.