---
title: Block Device Layer
url: https://doc.liz6.com/en/linux-kernel/07-block-device-layer/01-block-device-layer
locale: en
area: linux-kernel
tags:
- linux-kernel
- block-device-layer
date: 2026-06-30
modified: 2026-07-16
description: 'Coverage: bio → request → plug/unplug → blk-mq (multi-queue) → IO scheduler (mq-deadline/kyber/bfq) → blktrace/blkparse debugging. Kernel versions: 2.6 ~ 6.x, with a focus on the blk-mq refactoring (3.13~5.x)'
---

# Block Device Layer

> Coverage: bio → request → plug/unplug → blk-mq (multi-queue) → IO scheduler (mq-deadline/kyber/bfq) → blktrace/blkparse debugging
> Kernel versions: 2.6 ~ 6.x, with a focus on the blk-mq refactoring (3.13~5.x)

## Overview

The block layer is a translation layer between the file system and device drivers. Its input is the `bio` submitted by the file system ("read sector N, place it in these pages, notify me when done"), and its output is the `request` sent to the device driver. In between, the block layer performs merging, sorting, and scheduling, aiming to minimize IO latency while maximizing throughput.

From the single-request queue in 2.6 to the multi-queue architecture (blk-mq) in 5.x, the Linux block layer underwent a complete rewrite—the driver model shifted from "one queue and one lock per device" to "per-CPU submit queues + multiple hardware queues."

---

## bio: The Atomic Unit of Block IO

```c
// include/linux/blk_types.h
struct bio {
    struct block_device *bi_bdev;        // Target block device
    sector_t            bi_sector;       // Starting sector (in 512B units)
    unsigned int        bi_size;         // Remaining IO bytes
    unsigned int        bi_status;       // Completion status (BLK_STS_OK/error)

    struct bio_vec      *bi_io_vec;      // Page vector (scatter-gather list)
    unsigned short      bi_vcnt;         // Number of vector entries
    unsigned short      bi_max_vecs;

    bio_end_io_t        *bi_end_io;      // Completion callback
    void                *bi_private;     // Callback context

    unsigned short      bi_write_hint;   // IO priority hint
    unsigned short      bi_ioprio;
};
```

### bio_vec: Describing IO on a Memory Page

```c
// include/linux/bvec.h
struct bio_vec {
    struct page     *bv_page;    // Target page
    unsigned int    bv_len;      // Length of this segment
    unsigned int    bv_offset;   // Offset within the page
};
```

A bio can contain multiple `bio_vec`s—this is scatter-gather IO. The file system can pack non-contiguous memory pages into a single IO, and the block layer and driver will then split them into sizes acceptable to the device.

### Bio Submission Path

```c
// block/blk-core.c
void submit_bio(struct bio *bio) {
    // 1. Check if using blk-cgroup → account to cgroup
    // 2. Check if bio size exceeds device limits → split if needed
    // 3. Call submit_bio_noacct() → __submit_bio_noacct_mq()
    //    → blk_mq_submit_bio()  (blk-mq path, current default)
}
```

---

## blk-mq: Multi-Queue Architecture

### Why Migrate from blk-sq to blk-mq

```
blk-sq (Single Queue, 2.6 ~ 3.x):
  struct request_queue *q;  ← One large lock protects all submissions
  All CPUs compete for q->queue_lock → Multi-core scalability bottleneck
  Single-threaded model of IO scheduler → Peaks for high IOPS devices (NVMe)

blk-mq (3.13+, fully replaces blk-sq in 5.x):
  Two-layer queue structure:
    Software Queues (ctx): per-CPU or per-cgroup → Zero-lock contention
    Hardware Queues (hctx): Mapped to device hardware queues (NVMe SQ, SCSI tag)

  Tags: Used to manage the number of concurrent requests in hardware queues
    → Each hardware queue has a fixed tag pool → Natural throttling
```

### Data Structures

```c
// include/linux/blk-mq.h
struct blk_mq_hw_ctx {
    unsigned int        queue_num;       // Hardware queue number
    struct request_queue *queue;
    struct blk_mq_tags  *tags;           // Tag pool (limits concurrency)
    unsigned int        nr_ctx;          // Number of software queues

    struct list_head    dispatch;        // Requests coming from the scheduler
    unsigned int        dispatched;

    struct blk_mq_ctx   **ctxs;          // Child software queues
};

struct blk_mq_ctx {
    unsigned int        cpu;             // Bound CPU
    struct blk_mq_hw_ctx *hctxs[];       // Mapped hardware queues
    struct list_head    rq_lists[];      // Submit list
};
```

### Submission Path: Plug → Scheduler → Dispatch → Driver

<svg viewBox="0 0 720 420" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="blk_mq_submit_bio submission path flowchart: bio passes through five stages: split check, plug merge, request and tag allocation, initialization, and insertion into scheduler, finally queuing to the IO scheduler or directly dispatching to the hardware queue">
  <defs>
    <marker id="bioarrow" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker>
  </defs>
  <rect width="720" height="420" fill="#ffffff"/>
  <text x="360" y="24" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">Bio Submission Path: Five Stages of blk_mq_submit_bio()</text>
  <text x="660" y="38" text-anchor="end" font-size="10" fill="#94a3b8">block/blk-mq.c</text>

  <rect x="60" y="48" width="600" height="40" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="76" y="64" font-size="13" font-weight="700" fill="#3730a3">① Is bio too large?</text>
  <text x="76" y="80" font-size="10.5" fill="#4f46e5">Exceeds limit → blk_bio_discard_split()/blk_bio_segment_split() splits into multiple bios</text>
  <line x1="360" y1="88" x2="360" y2="100" stroke="#475569" stroke-width="1.6" marker-end="url(#bioarrow)"/>

  <rect x="60" y="100" width="600" height="40" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="76" y="116" font-size="13" font-weight="700" fill="#3730a3">② Plug merge attempt</text>
  <text x="76" y="132" font-size="10.5" fill="#4f46e5">blk_mq_attempt_plug_merge() → front/back merge (merge to front/back of existing request)</text>
  <line x1="360" y1="140" x2="360" y2="152" stroke="#475569" stroke-width="1.6" marker-end="url(#bioarrow)"/>

  <rect x="60" y="152" width="600" height="40" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="76" y="168" font-size="13" font-weight="700" fill="#3730a3">③ Allocate request + tag</text>
  <text x="76" y="184" font-size="10.5" fill="#4f46e5">blk_mq_get_new_requests() → blk_mq_get_tag() fetches tag, waits if exhausted</text>
  <line x1="360" y1="192" x2="360" y2="204" stroke="#475569" stroke-width="1.6" marker-end="url(#bioarrow)"/>

  <rect x="60" y="204" width="600" height="40" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="76" y="220" font-size="13" font-weight="700" fill="#3730a3">④ Initialize request</text>
  <text x="76" y="236" font-size="10.5" fill="#4f46e5">blk_mq_rq_ctx_init() → associates bio, completes initialization</text>
  <line x1="360" y1="244" x2="360" y2="256" stroke="#475569" stroke-width="1.6" marker-end="url(#bioarrow)"/>

  <rect x="60" y="256" width="600" height="40" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="76" y="272" font-size="13" font-weight="700" fill="#3730a3">⑤ Insert into scheduler</text>
  <text x="76" y="288" font-size="10.5" fill="#4f46e5">blk_mq_sched_insert_request() → decides whether to go through scheduler or dispatch directly</text>

  <line x1="360" y1="296" x2="200" y2="314" stroke="#475569" stroke-width="1.6" marker-end="url(#bioarrow)"/>
  <text x="280" y="303" text-anchor="middle" font-size="11" fill="#475569">Yes</text>
  <line x1="360" y1="296" x2="520" y2="314" stroke="#475569" stroke-width="1.6" marker-end="url(#bioarrow)"/>
  <text x="440" y="303" text-anchor="middle" font-size="11" fill="#475569">No</text>

  <rect x="55" y="314" width="290" height="42" rx="8" fill="#e0e7ff" stroke="#c7d2fe"/>
  <text x="71" y="331" font-size="12.5" font-weight="700" fill="#3730a3">Has IO Scheduler</text>
  <text x="71" y="348" font-size="9.5" fill="#4f46e5">blk_mq_sched_try_insert_merge() → queues to scheduler</text>

  <rect x="375" y="314" width="290" height="42" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="391" y="331" font-size="12.5" font-weight="700" fill="#115e59">No IO Scheduler</text>
  <text x="391" y="348" font-size="9.5" fill="#0f766e">blk_mq_run_dispatch_ops() → direct dispatch</text>

  <rect x="60" y="372" width="600" height="44" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="76" y="390" font-size="12.5" fill="#3730a3">The five stages execute sequentially: split if too large, merge into existing request if possible, wait if tags are insufficient;</text>
  <text x="76" y="408" font-size="12.5" fill="#3730a3">finally, the insertion stage decides whether to queue for the IO scheduler or dispatch directly to the hardware queue.</text>
</svg>

### Plug: Batch Submission

```c
// block/blk-mq.c + block/blk-plug.c
// Plug is a per-task bio accumulation mechanism:
//   blk_start_plug(current)     // Start plug
//   submit_bio() × N            // Accumulate N bios, do not dispatch immediately
//   blk_finish_plug(current)    // Flush all bios at once

// Design rationale:
//   1. Reduce lock contention (acquire lock once, insert N requests)
//   2. Increase merge opportunities (front/back merge possible during plug)
//   3. File systems typically utilize plug in the write path:
//      ext4_writepages() → blk_start_plug() → multiple submit_bio → blk_finish_plug()
```

---

## IO Scheduler

The fundamental difference between multi-queue IO schedulers and the old single-queue schedulers is: **the scheduler is not global, but per-hctx**. Each hardware queue is scheduled independently because different queues correspond to submissions from different CPUs.

### mq-deadline

```c
// block/mq-deadline.c
// Maintains separate Red-Black trees (sorted by sector) + FIFO (sorted by clock time) for reads and writes
//
// Core parameters:
//   read_expire:  Max wait time for read requests (default 500ms)
//   write_expire: Max wait time for write requests (default 5000ms)
//   writes_starved: Write request quota (how many batches of reads to process before handling a batch of writes)
//
// Scheduling logic:
//   1. Check FIFO first: if there are timed-out requests → fetch requests near that sector from the RB tree (batch dispatch)
//   2. Otherwise: dispatch by direction batch (prefer reads, but writes_starved ensures writes are not starved)
//   3. Each dispatch sends no more than 16 requests (to avoid starving other queues)
//
// Suitable for: NVMe, general-purpose SSDs
```

### kyber

```c
// block/kyber-iosched.c
// Adaptive scheduling based on token bucket
// Goal: Control read/write queue depth to keep latency within target bounds
//
// Working principle:
//   Maintains latency histograms for reads and writes
//   Dynamically adjusts the token bucket rate for each direction based on latency
//   Low latency → increase bucket → more IO concurrency
//   High latency → decrease bucket → reduce IO concurrency (lower device queue depth)
//
// Suitable for: Latency-sensitive SSD workloads (e.g., databases)
```

### bfq (Budget Fair Queuing)

```c
// block/bfq-iosched.c
// Allocates IO bandwidth per cgroup/process
// Each process has a "budget" (number of sectors it can issue)
// Based on B-WF2Q+ algorithm (similar to CFS fair scheduling)
//
// Features:
//   - Interactive processes automatically get higher weight (detects think time)
//   - Supports cgroup blkio controller
//   - Sequential reads detected → given more budget → high throughput
//
// Suitable for: Desktop systems (ensures background backups do not affect foreground application responsiveness)
```

### Scheduler Switching

```bash
cat /sys/block/nvme0n1/queue/scheduler
# [mq-deadline] kyber bfq none

echo kyber > /sys/block/nvme0n1/queue/scheduler
```

---

## Queue Parameter Tuning

```bash
# Number of hardware queues (usually equal to CPU count)
cat /sys/block/nvme0n1/mq/*/cpu_list

# max_sectors_kb: Maximum size of a single IO (default 512KB = 1024 sectors)
cat /sys/block/sda/queue/max_sectors_kb

# nr_requests: Maximum number of requests per hardware queue
cat /sys/block/nvme0n1/queue/nr_requests  # Default 256

# Scheduler parameters
cat /sys/block/nvme0n1/queue/iosched/read_expire  # mq-deadline
```

---

## Debugging and Observation

```bash
# blktrace: Trace the full lifecycle of each IO
blktrace -d /dev/nvme0n1 -o trace
blkparse -i trace > trace.txt
# Output: time, CPU, action (Q=queued, G=get, I=inserted, D=dispatch, C=complete)

# iostat: Device-level statistics
iostat -x 1 nvme0n1
# r/s, w/s, r_await, w_await, aqu-sz (average queue depth), %util

# Raw counters for a single device
cat /sys/block/nvme0n1/stat
# Fields: read_ios, read_merges, read_sectors, read_ticks, write_ios, ...

# blk-mq debugging
cat /sys/kernel/debug/block/nvme0n1/hctx*/tags
# Tag usage for each hardware queue

# ftrace to trace block layer events
echo 1 > /sys/kernel/debug/tracing/events/block/block_bio_queue/enable
echo 1 > /sys/kernel/debug/tracing/events/block/block_rq_complete/enable
cat /sys/kernel/debug/tracing/trace_pipe
```

---

## Key Configurations and Compilation

```bash
# Kernel boot parameters
scsi_mod.use_blk_mq=1    # Force SCSI to use blk-mq (default in 5.0+)
elevator=mq-deadline      # Default IO scheduler
elevator=bfq              # Recommended for desktops

# Kernel compilation options
CONFIG_BLK_MQ=y           # Enable blk-mq
CONFIG_IOSCHED_BFQ=y      # BFQ support
CONFIG_BLK_DEV_THROTTLING=y  # cgroup blkio
```

---

## References and Further Reading

- **Source Code**: `block/blk-mq.c` (~4000 lines, blk-mq core), `block/blk-merge.c`, `block/mq-deadline.c`, `block/bfq-iosched.c`, `block/kyber-iosched.c`
- **Kernel Documentation**: `Documentation/block/`, `Documentation/block/bfq-iosched.rst`
- **LWN**:
  - "The multi-queue block layer" (lwn.net/Articles/552904/)
  - "blk-mq and the I/O schedulers" (lwn.net/Articles/738449/)

---

*Keywords: bio, bio_vec, blk-mq, software queue, hardware queue, plug, mq-deadline, kyber, bfq, blktrace, iostat, IO scheduler*
