blk-mq provides CPU- or node-local software staging queues and hardware dispatch queues. Scheduling and merging can occur before dispatch, while driver and device behavior also affect service. Software submission order does not guarantee completion order. A scheduler can influence how competing requests enter the device without controlling every later decision in the storage stack.

On rotating media, request locality can reduce head movement. Flash lacks a mechanical seek penalty, but merging, concurrency, queueing and arbitration still affect performance. Efficient ordering and fair service answer different questions, and neither determines a universal scheduler ranking. Choose from the schedulers actually available for the device and kernel, then measure the relevant workload.

The head is the budget

At 7200 RPM, half a revolution is about 4.17 ms, an idealized average rotational wait. Transfer and seek costs depend on the device and workload. The original text reported 2000 requests, 67,907,411 versus 99,977 tracks, and 91 versus 231 IOPS, but supplied neither the generating fixture nor retained measurements. Those figures remain unverified source claims and must not be presented as measured Linux scheduler results.

Sorting an invented set of track positions can illustrate less head travel, but it cannot establish an IOPS gain without a validated device model or measurement. The previously reported seek percentages and approximately 40 ms NVMe total have the same missing-evidence limitation. A drawing of a shorter path is therefore qualitative, not a latency prediction.

What each scheduler is for

none adds no scheduler reordering; it does not promise a global first-come completion order. mq-deadline combines sector ordering with expiry checks between bounded batches. These policies guide dispatch rather than guarantee an end-to-end completion deadline. kyber throttles requests toward read and synchronous-write latency targets; a target is not proof every request meets it.

bfq provides weight-based service and low-latency heuristics, with configuration-dependent bandwidth or time sharing. Idling can help service guarantees but can reduce throughput on internally queued devices. Kernel support and actual settings matter. Read the active scheduler from the bracketed name in /sys/block//queue/scheduler; changing it is an operational action requiring an authorized device and reversible test plan.

Fairness is a different question from order

A 90/10 issue mix does not establish a guaranteed service share or wait position. The reported 0.099, 1901 and 0.5 values have no retained fixture here. Equal weights in a simplified continuously backlogged model may illustrate sharing, but real outcomes depend on demand, request sizes, priorities and scheduler settings. Compare service received and application latency separately from request arrival counts.

For either media type, test latency, throughput and isolation under representative contention. cgroup io.max delays I/O when configured BPS or IOPS limits are reached and permits temporary bursts; it is not conditional on choosing BFQ. Weight control depends on the active implementation and configuration; BFQ has io.bfq.weight, while io.weight can use the I/O cost controller. A ceiling and a relative share are different controls.

When it breaks

A rotational device using none is a reason to inspect the workload, not proof of a bug. High utilization, low IOPS and high await can have several causes. await includes queueing and service, while near-100% utilization can indicate saturation for a serial device and is less informative for parallel hardware. Confirm topology, request pattern and competing work before attributing delays to seeking.

A bulk job can worsen interactive tail latency even when averages look acceptable. Scheduler policies or cgroup limits may help, but no single name establishes application-level protection. Compare percentiles and useful throughput before and after a controlled change. Idling, queue depth and device-internal scheduling interact; the rotational flag alone does not select a universally correct setting.

Software staging, scheduler and hardware dispatch are followed by qualified none, mq-deadline, kyber and bfq descriptions. io.max differs from weights; numerical fixture claims are unverified, and the guarded policy-change proposal remains unexecuted. No article example was run for this graphic.
Queue ordering, weighted service and io.max ceilings are distinct; the original numerical claims remain unverified.

Check it quickly

Start with this unexecuted read-only inspection of one explicitly selected whole device. Set DEVICE only after reviewing its topology and scope. It rejects path-like names, checks required readable attributes, and reports absent optional tunables. This first block does not change policy.

(
  : "${DEVICE:?Set DEVICE to the reviewed whole-device name}"
  case "$DEVICE" in
    ''|[!A-Za-z]*|*[!A-Za-z0-9_-]*|ALL) printf 'Invalid device name\n' >&2; exit 2 ;;
  esac
  queue="/sys/block/$DEVICE/queue"
  [ -d "$queue" ] || { printf 'No such whole-device queue\n' >&2; exit 1; }
  for field in rotational scheduler; do
    [ -r "$queue/$field" ] || { printf 'Missing: %s\n' "$field" >&2; exit 1; }
    value=$(cat "$queue/$field") || exit 1
    printf '%s=%s\n' "$field" "$value"
  done
  for field in read_expire fifo_batch; do
    if [ -r "$queue/iosched/$field" ]; then
      value=$(cat "$queue/iosched/$field") || exit 1
      printf '%s=%s\n' "$field" "$value"
    else
      printf '%s: unavailable for the active scheduler\n' "$field"
    fi
  done
  LC_ALL=C iostat -x -y -t "$DEVICE" 2 5
)

The bracketed name is current; absent iosched fields can be normal. -y excludes since-boot statistics. Preserve errors and confirm the selected device row.

Save this helper as scheduler_exercise.py. Only on an explicitly authorized, exclusively controlled disposable test device, set EXPECTED_CURRENT and REQUESTED from the observed choices, and ACK_DEVICE to DISPOSABLE: followed by DEVICE. Python 3.9+, writable scheduler access and sysstat are prerequisites. This temporarily changes policy:

"""Unexecuted proposal: temporary policy change on an exclusively controlled test device."""
import json
import re
import signal
import subprocess
import sys
from pathlib import Path


def device_name(value):
    if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_-]*", value) or value == "ALL":
        raise ValueError("use one named whole device, not a path, option, number or ALL")
    return value


def scheduler_state(path):
    tokens = path.read_text().split()
    active = [s[1:-1] for s in tokens if re.fullmatch(r"\[[A-Za-z0-9_-]+\]", s)]
    if len(active) != 1 or not tokens:
        raise ValueError("scheduler state must have exactly one bracketed choice")
    if any(not re.fullmatch(r"(?:[A-Za-z0-9_-]+|\[[A-Za-z0-9_-]+\])", s) for s in tokens):
        raise ValueError("unexpected scheduler state")
    available = [s.strip("[]") for s in tokens]
    if len(set(available)) != len(available):
        raise ValueError("duplicate scheduler choices")
    return active[0], available


def exercise(path, device, expected, requested, run=subprocess.run, emit=print):
    original, available = scheduler_state(path)
    if original != expected or requested not in available or requested == original:
        raise ValueError("current/requested scheduler does not match the reviewed plan")
    emit(json.dumps({"device": device, "original": original, "requested": requested}), flush=True)
    primary_error = restore_error = None
    attempted = False
    try:
        if scheduler_state(path) != (original, available):
            raise ValueError("scheduler state changed before the test")
        attempted = True  # even a failed write may have changed state
        path.write_text(requested + "\n")
        if scheduler_state(path)[0] != requested:
            raise RuntimeError("requested scheduler was not activated")
        result = run(["iostat", "-x", "-y", "-t", device, "2", "5"], check=False)
        if result.returncode != 0:
            raise RuntimeError(f"iostat exited with status {result.returncode}")
    except BaseException as error:
        primary_error = f"{type(error).__name__}: {error}"
    finally:
        if attempted:
            try:
                if original not in scheduler_state(path)[1]:
                    raise RuntimeError("original scheduler is no longer available")
                path.write_text(original + "\n")
                if scheduler_state(path)[0] != original:
                    raise RuntimeError("original scheduler was not restored")
            except BaseException as error:
                restore_error = f"{type(error).__name__}: {error}"
    emit(json.dumps({"primary_error": primary_error, "restore_error": restore_error,
                     "restored": attempted and restore_error is None}), flush=True)
    # Report both errors; restoration failure never hides the primary failure.
    return (1 if primary_error else 0) + (2 if restore_error else 0)


def main(args):
    if len(args) != 4:
        raise ValueError("usage: scheduler_exercise.py DEVICE EXPECTED_CURRENT REQUESTED DISPOSABLE:DEVICE")
    device, expected, requested, acknowledgement = args
    device = device_name(device)
    if acknowledgement != "DISPOSABLE:" + device:
        raise ValueError("explicit disposable-device acknowledgement required")
    path = Path("/sys/block") / device / "queue/scheduler"
    return exercise(path, device, expected, requested)


if __name__ == "__main__":
    def interrupted(number, frame):
        raise InterruptedError(f"received signal {number}; attempting restoration")
    signal.signal(signal.SIGINT, interrupted)
    signal.signal(signal.SIGTERM, interrupted)
    try:
        sys.exit(main(sys.argv[1:]))
    except (OSError, ValueError) as error:
        sys.exit(f"Scheduler test refused: {error}")
python3 scheduler_exercise.py "${DEVICE:?}" "${EXPECTED_CURRENT:?}" \
  "${REQUESTED:?}" "${ACK_DEVICE:?}"

The helper records the original, verifies activation, captures intervals, then separately restores and verifies the original. Exit statuses are 0 for success, 1 for primary failure, 2 for restoration failure, and 3 for both; the JSON retains both errors. Do not ignore a restore error. SIGINT/SIGTERM request cleanup, but SIGKILL, repeated interruption or host failure can prevent it. Retain the original for manual recovery. This is a reversible policy exercise, not performance or fairness evidence; numerical claims and actual execution remain held.

Sources