I/O Priorities
ioprio_set(), priority classes, cgroup I/O weight, and how the block scheduler uses priorities
What I/O priority means
CPU scheduling has hard preemption: a higher-priority task can evict a lower-priority task from the CPU mid-instruction. I/O priority is weaker — it is advisory. The block scheduler tries to honor it but makes no hard guarantees except for the RT class on BFQ.
Priority affects:
- Order of request dispatch from the scheduler queue — RT requests go before BE, BE before IDLE
- Bandwidth allocation — BFQ allocates I/O bandwidth proportional to per-process weight, which is derived from ioprio
- Latency targets — BFQ's latency mode boosts cgroups whose actual latency exceeds a configured target
What priority does not affect:
- The
nonescheduler ignores ioprio entirely (no scheduler, no priority enforcement) mq-deadlineuses ioprio only for ordering, not bandwidth- Page cache writeback is not bound to any process's ioprio (writeback runs as a kernel thread)
ioprio_set() and ioprio_get()
There is no glibc wrapper; these are raw syscalls.
#include <linux/ioprio.h>
#include <sys/syscall.h>
/* Set I/O priority for the calling process */
int ret = syscall(SYS_ioprio_set,
IOPRIO_WHO_PROCESS, /* target: process */
0, /* 0 = current process */
IOPRIO_PRIO_VALUE(IOPRIO_CLASS_BE, 0));
/* Get I/O priority for a specific PID */
int prio = syscall(SYS_ioprio_get, IOPRIO_WHO_PROCESS, pid);
int class = IOPRIO_PRIO_CLASS(prio); /* extract class */
int level = IOPRIO_PRIO_DATA(prio); /* extract level */
Target selectors
| Constant | Meaning |
|---|---|
IOPRIO_WHO_PROCESS |
A single process (or thread) by PID |
IOPRIO_WHO_PGRP |
All processes in a process group |
IOPRIO_WHO_USER |
All processes owned by a UID |
Priority classes
/* include/uapi/linux/ioprio.h */
#define IOPRIO_CLASS_NONE 0 /* inherit from CPU nice */
#define IOPRIO_CLASS_RT 1 /* real-time: served first */
#define IOPRIO_CLASS_BE 2 /* best-effort: default */
#define IOPRIO_CLASS_IDLE 3 /* idle: only when nothing else is pending */
Building a priority value
/* Encode class and level into a single u16 */
#define IOPRIO_PRIO_VALUE(class, data) \
(((class) << IOPRIO_CLASS_SHIFT) | (data))
/* Examples */
IOPRIO_PRIO_VALUE(IOPRIO_CLASS_RT, 0) /* RT, highest level */
IOPRIO_PRIO_VALUE(IOPRIO_CLASS_BE, 4) /* BE, level 4 — default */
IOPRIO_PRIO_VALUE(IOPRIO_CLASS_IDLE, 0) /* IDLE (level ignored) */
Priority levels within RT and BE run from 0 (highest) to 7 (lowest). The IDLE class has no meaningful level.
The ionice command
ionice is the userspace wrapper around these syscalls:
# Check current I/O priority for a PID
ionice -p 1234
# Set BE class, level 0 (highest BE) for a running process
ionice -c 2 -n 0 -p $(pgrep postgres)
# Launch a process as IDLE class
ionice -c 3 rsync -av /data /backup
# Launch a process with RT class, level 0 (requires CAP_SYS_ADMIN)
ionice -c 1 -n 0 bash -c 'exec ./latency-critical-job'
# Set ioprio for all processes in a shell subtree
ionice -c 2 -n 0 bash -c 'exec your-critical-job'
Priority classes in detail
RT (Real-time)
RT is the highest priority class. The block scheduler (BFQ or mq-deadline) dispatches RT requests before any BE or IDLE request in the same queue.
- Levels: 0–7, where 0 is highest within RT
- Requires
CAP_SYS_ADMIN— unprivileged processes cannot set RT class - Risk: RT can starve lower-priority I/O completely if the RT process submits continuously
- Typical uses: real-time audio playback, industrial control systems, storage I/O for VMs that require bounded latency
# Grant a process RT I/O — must be run as root or with CAP_SYS_ADMIN
ionice -c 1 -n 0 -p $(pgrep jackd)
Under BFQ, RT queues are served in a separate budget round before any BE queue gets a turn. Under mq-deadline, RT requests are placed on a high-priority FIFO that is drained before BE and IDLE FIFOs.
BE (Best-effort)
BE is the default class for every process. If no explicit ioprio_set() has been called, the process is in IOPRIO_CLASS_BE with a level derived from its CPU nice value:
So nice 0 → BE level 4 (the midpoint), nice -20 → BE level 0, nice 19 → BE level 7.
- Levels: 0–7 (0 = most I/O bandwidth, 7 = least)
- BFQ allocates bandwidth proportionally: a process at level 0 gets more sectors per round than one at level 7
- CFQ (removed in Linux 5.0) ordered BE processes strictly within the class; BFQ uses a weighted fair queue
IDLE
IDLE class processes are served only when no RT or BE I/O is pending. This makes them ideal for background workloads that should not compete with foreground I/O at all.
# Background backup — will not slow down anything else
ionice -c 3 nice -n 19 rsync -av /data /backup
# Filesystem scrub as a background task
ionice -c 3 btrfs scrub start /
# Combine ionice -c 3 with CPU nice for truly background work
ionice -c 3 nice -n 19 find / -name '*.log' -mtime +30 -delete
The IDLE class is implemented in BFQ by checking whether any RT or BE bfq_queue has pending requests before dispatching from an IDLE queue.
How priority flows through the kernel
A priority set with ioprio_set() must travel from task_struct all the way to the block scheduler. Here is the full path:
ioprio_set() syscall
→ stored in task_struct->ioprio
write() / read() syscall entry
→ init_sync_kiocb() / init_kiocb()
→ kiocb.ki_ioprio = get_current_ioprio()
bio submission (submit_bio / iomap_dio_rw)
→ bio->bi_ioprio = kiocb->ki_ioprio
blk-mq request allocation (blk_mq_get_request)
→ rq->ioprio = bio->bi_ioprio
Scheduler (BFQ) enqueue
→ bfq_queue->ioprio from rq->ioprio
→ RT queues served before BE before IDLE
→ weight within BE class derived from ioprio level
get_current_ioprio()
/* include/linux/ioprio.h */
static inline int get_current_ioprio(void)
{
return __get_task_ioprio(current);
}
static inline int __get_task_ioprio(struct task_struct *p)
{
struct io_context *ioc = p->io_context;
int prio;
if (!ioc) /* no io_context: derive class + level from CPU nice */
return IOPRIO_PRIO_VALUE(task_nice_ioclass(p), task_nice_ioprio(p));
prio = ioc->ioprio;
if (IOPRIO_PRIO_CLASS(prio) == IOPRIO_CLASS_NONE) /* class NONE: use nice */
prio = IOPRIO_PRIO_VALUE(task_nice_ioclass(p), task_nice_ioprio(p));
return prio;
}
If the process has an io_context with an explicit ioprio set via ioprio_set(), that value is used. Otherwise the ioprio is derived from the CPU nice value, keeping the two scheduling dimensions loosely coupled.
bio->bi_ioprio assignment
/* block/blk-core.c — a bio inherits ioprio from the submitting task */
static void bio_set_ioprio(struct bio *bio)
{
/* Nobody set ioprio yet? Initialize from the task's (nice-derived) ioprio */
if (IOPRIO_PRIO_CLASS(bio->bi_ioprio) == IOPRIO_CLASS_NONE)
bio->bi_ioprio = get_current_ioprio();
blkcg_set_ioprio(bio); /* then apply any cgroup I/O-priority policy */
}
For direct I/O the bio carries the originating kiocb's ki_ioprio (copied when the DIO bio is built in fs/iomap/direct-io.c); for buffered writes the bio is created later by the writeback path, so its ioprio comes from the cgroup/writeback context rather than the originating process.
cgroup v2 I/O weight
With cgroup v2, I/O priorities are administered per-cgroup rather than per-process. The cgroup hierarchy overrides or supplements individual task_struct->ioprio values.
io.weight
# Set I/O weight for a cgroup (range 1–10000, default 100)
echo "100" > /sys/fs/cgroup/myapp/io.weight
# Per-device weight override (major:minor weight)
echo "8:16 200" > /sys/fs/cgroup/myapp/io.weight
# Read current weights
cat /sys/fs/cgroup/myapp/io.weight
# default 100
# 8:16 200
# Set weight for a cgroup relative to its siblings
# A cgroup with weight 200 gets twice the I/O bandwidth of one with weight 100
echo "200" > /sys/fs/cgroup/highprio/io.weight
echo "50" > /sys/fs/cgroup/background/io.weight
io.weight is not translated into a per-task ioprio level — it is a separate, proportional mechanism. The value (1–10000, default 100) is consumed directly by the block I/O controller (BFQ, or the blk-iocost cost model that backs io.weight on other schedulers) to divide device bandwidth among sibling cgroups in proportion to their weights: a cgroup with weight 200 gets roughly twice the share of one with weight 100. Per-task ioprio (via ionice) and per-cgroup io.weight are independent knobs — the former orders requests within BFQ's priority classes, the latter partitions bandwidth between cgroups.
io.bfq.weight
BFQ exposes its own weight interface directly:
# BFQ weight: 1–1000 (default 100)
echo "500" > /sys/fs/cgroup/myapp/io.bfq.weight
# Per-device BFQ weight
echo "8:16 500" > /sys/fs/cgroup/myapp/io.bfq.weight
# Read current BFQ weights
cat /sys/fs/cgroup/myapp/io.bfq.weight
# default 100
# 8:16 500
io.bfq.weight maps directly into BFQ's internal weight without the intermediate conversion that io.weight applies.
Moving a process into a cgroup
# Create a cgroup
mkdir /sys/fs/cgroup/background
# Set low I/O weight
echo "10" > /sys/fs/cgroup/background/io.weight
# Move a process into it
echo $PID > /sys/fs/cgroup/background/cgroup.procs
# Everything the process submits now uses the cgroup's weight
io.latency: latency targeting
io.latency lets a cgroup declare a target I/O latency. BFQ monitors whether the cgroup meets its target and boosts its weight temporarily when latency exceeds the threshold.
# Set a 10ms read latency target for device 8:16
echo "8:16 10000" > /sys/fs/cgroup/critical/io.latency
# Read current latency targets
cat /sys/fs/cgroup/critical/io.latency
# 8:16 10000
# Multiple devices
echo "8:0 5000" > /sys/fs/cgroup/db/io.latency # 5ms for sda
echo "8:16 5000" >> /sys/fs/cgroup/db/io.latency # 5ms for sdb
The latency is specified in microseconds.
How io.latency works with BFQ
Cgroup submits I/O request
→ BFQ records submission timestamp
I/O completes
→ BFQ computes actual latency
→ compares to io.latency target
If actual > target:
→ blkcg_iolatency_throttle() temporarily boosts cgroup weight
→ competing cgroups are throttled until latency recovers
If actual <= target consistently:
→ weight returns to nominal io.weight value
This is especially useful for databases sharing storage with bulk background workloads:
# Database cgroup: low latency target
mkdir /sys/fs/cgroup/db
echo "100" > /sys/fs/cgroup/db/io.weight
echo "8:0 5000" > /sys/fs/cgroup/db/io.latency # 5ms target
# Backup cgroup: no latency target, low weight
mkdir /sys/fs/cgroup/backup
echo "10" > /sys/fs/cgroup/backup/io.weight
# Move processes
echo $DB_PID > /sys/fs/cgroup/db/cgroup.procs
echo $BACKUP_PID > /sys/fs/cgroup/backup/cgroup.procs
Now if the backup causes database latency to spike above 5ms, BFQ will throttle the backup cgroup until the database is back within target.
BFQ: Budget Fair Queueing
BFQ is the scheduler that most fully honors ioprio and cgroup I/O weights. It is the default on many desktop and general-purpose Linux systems.
# Check if BFQ is active
cat /sys/block/sda/queue/scheduler
# [bfq] mq-deadline none
# Enable BFQ
echo bfq > /sys/block/sda/queue/scheduler
Per-process queues
BFQ maintains a bfq_queue for each process (or cgroup entity):
/* block/bfq-iosched.h */
struct bfq_queue {
struct bfq_data *bfqd; /* owning scheduler */
struct rb_root sort_list; /* requests sorted by sector */
struct request *next_rq; /* next request to dispatch */
unsigned short ioprio; /* ioprio level */
unsigned short ioprio_class; /* IOPRIO_CLASS_* */
int max_budget; /* sectors to serve this round */
unsigned long budget_timeout;
/* ... */
};
Each bfq_queue is served for at most max_budget sectors per activation; when the budget is exhausted (or times out), BFQ moves on to the next queue. max_budget itself is auto-tuned by BFQ (starting from bfq_default_max_budget, 16K sectors) — it is not derived from the ioprio level. What the ioprio level controls is the queue's weight, which sets its share of disk time in BFQ's proportional-share (WF2Q+) scheduler.
From ioprio level to weight
weight ← ioprio level (block/bfq-wf2q.c):
weight = (IOPRIO_NR_LEVELS − level) × BFQ_WEIGHT_CONVERSION_COEFF
= (8 − level) × 10
BE level 0 → weight 80
BE level 4 → weight 40 ← default ioprio
BE level 7 → weight 10
A process at BE level 0 therefore carries 8× the weight of one at BE level 7 (80 vs 10), and receives a correspondingly larger share of the device.
Priority class ordering
BFQ enforces a strict class hierarchy: RT before BE before IDLE. This ordering is not a simple if ladder — it falls out of BFQ's hierarchical scheduler. bfq_select_queue() (in block/bfq-iosched.c) picks the next queue to serve by walking BFQ's service-tree hierarchy via bfq_get_next_queue(), and that hierarchy is ordered by class: an RT queue with pending I/O is always chosen before any BE queue, and BE queues before IDLE. Within the BE class, queues are served in weighted-fair order according to their per-queue weights (derived from the ioprio level).
Soft real-time detection
BFQ heuristically detects interactive applications (web browsers, text editors) by observing short bursts of I/O separated by idle periods (think time above a threshold). Such queues are temporarily boosted even while in the BE class — the "soft real-time" promotion. Internally this is implemented as weight raising: BFQ multiplies the queue's weight by a coefficient (bfqq->wr_coeff > 1) for a bounded interval, so a boosted queue is scheduled ahead of ordinary BE queues. Queues flagged as part of a "large burst" of process creation (bfq_bfqq_in_large_burst()) are excluded, since a storm of short-lived processes is not the interactive pattern the heuristic targets.
Seeky detection
BFQ tracks whether a queue submits sequential or random (seeky) I/O, classifying a queue as seeky via the BFQQ_SEEKY() macro when its average seek distance is large. Seeky queues are budgeted differently: because random I/O cannot make good use of a long, uninterrupted service slot, BFQ caps their budget so a seeky queue cannot hold the device while doing little useful work. This protects sequential workloads — which benefit from sustained access — from being starved by a random-access queue.
io_uring and BFQ
io_uring can confuse BFQ's per-process model when application-level queuing is used. With SQPOLL or fixed files, multiple processes may submit I/O through a single io_uring context. BFQ sees a single queue rather than per-process queues, potentially undermining per-process weight differentiation. For cgroup-based isolation, io.weight is more reliable than per-process ioprio when io_uring is in use.
mq-deadline: simpler priority support
mq-deadline is the default scheduler for NVMe and SCSI devices on many systems. Its priority support is simpler than BFQ's — ordering only, no bandwidth proportioning.
How mq-deadline handles ioprio
mq-deadline maintains separate FIFO queues per priority class:
/* block/mq-deadline.c */
enum dd_prio {
DD_RT_PRIO = 0, /* IOPRIO_CLASS_RT */
DD_BE_PRIO = 1, /* IOPRIO_CLASS_BE */
DD_IDLE_PRIO = 2, /* IOPRIO_CLASS_IDLE */
DD_PRIO_MAX = 2,
};
/* One set of queues per priority class (DD_DIR_COUNT = 2: read, write) */
struct dd_per_prio {
struct rb_root sort_list[DD_DIR_COUNT]; /* sorted by sector (merging) */
struct list_head fifo_list[DD_DIR_COUNT]; /* sorted by deadline (anti-starvation) */
/* ... */
};
struct deadline_data {
struct dd_per_prio per_prio[DD_PRIO_COUNT]; /* RT, BE, IDLE (DD_PRIO_COUNT = 3) */
/* ... */
};
Dispatch order: 1. RT read / RT write requests (by deadline then sector) 2. BE read / BE write requests 3. IDLE requests — only when RT and BE FIFOs are empty
Within a class, mq-deadline dispatches by deadline (anti-starvation timer) then sector order (reduce seeks). It does not differentiate levels within a class — BE level 0 and BE level 7 are treated identically by mq-deadline.
# Tuning mq-deadline priorities
cat /sys/block/sda/queue/iosched/read_expire # default 500ms
cat /sys/block/sda/queue/iosched/write_expire # default 5000ms
cat /sys/block/sda/queue/iosched/prio_aging_expire # ms before low-prio ages up
ionice in practice
Background maintenance jobs
# Filesystem backup — IDLE class, no impact on foreground I/O
ionice -c 3 rsync -av --progress /data /backup/
# Combine with CPU deprioritisation
ionice -c 3 nice -n 19 tar -czf /backup/archive.tar.gz /var/lib/pgsql/
# btrfs balance — can be very I/O intensive
ionice -c 3 btrfs balance start -dusage=50 /
# Find and process large files without disturbing production
ionice -c 3 find /var/log -name '*.log' -size +100M -exec gzip {} \;
Interactive and database I/O
# High-priority database backup (needs CAP_SYS_ADMIN for RT)
ionice -c 1 -n 0 pg_basebackup -D /backup/pgbase
# Improve I/O for a running database (BE class, level 0)
ionice -c 2 -n 0 -p $(pgrep -x postgres | head -1)
# Check what ionice class a process is currently using
ionice -p $(pgrep mysqld)
# best-effort: prio 4
Adjusting a running process
# Lower a running process's I/O priority without restarting it
PID=$(pgrep -x bacula-fd)
ionice -c 3 -p $PID # demote to IDLE
ionice -p $PID # verify
# Raise I/O priority for a stuck backup that is taking too long
ionice -c 2 -n 0 -p $BACKUP_PID
Scripting with ioprio_set directly
#include <sys/syscall.h>
#include <linux/ioprio.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
/* Set this process to IDLE class before doing background work */
int ret = syscall(SYS_ioprio_set,
IOPRIO_WHO_PROCESS,
0,
IOPRIO_PRIO_VALUE(IOPRIO_CLASS_IDLE, 0));
if (ret < 0) {
perror("ioprio_set");
return 1;
}
/* All I/O from here on is IDLE class */
do_background_scan();
return 0;
}
Priority inversion
A subtle hazard in the I/O stack: a high-priority process can be blocked waiting for a low-priority process to release a resource.
Page lock contention
High-priority process: wants to read page at offset X
→ page is locked (PG_locked set)
→ waits on page lock
Low-priority process: holds PG_locked (doing writeback or readahead)
→ scheduled less frequently by I/O scheduler
→ high-priority process is effectively at low-priority
This is a form of priority inversion in the page cache layer. The I/O scheduler cannot help because the high-priority process is not blocked at the scheduler — it is blocked on a page lock above the scheduler.
inode and filesystem locks
Similarly, if a low-priority process holds inode->i_rwsem during a truncate or write, a high-priority process wanting to read the same file will block for the duration, regardless of its ioprio.
Partial mitigation: IOCB_NOWAIT
IOCB_NOWAIT tells the filesystem and block layer to return -EAGAIN rather than blocking if any lock or resource is not immediately available:
/* High-priority code that cannot afford to wait */
kiocb->ki_flags |= IOCB_NOWAIT;
ret = file->f_op->read_iter(kiocb, &iter);
if (ret == -EAGAIN) {
/* Resource temporarily unavailable — handle or retry later */
schedule_retry();
}
io_uring uses IOCB_NOWAIT by default for all non-blocking operations, falling back to a worker thread only when -EAGAIN is returned. This avoids priority inversion at the cost of a worker thread for the slow path.
cgroup io.latency as a compensating mechanism
io.latency can help indirectly. It is implemented by the blk-iolatency controller (independent of BFQ): you set a target latency for a protected cgroup, and when that cgroup's observed latency exceeds the target, the controller throttles the competing cgroups — reducing their allowed I/O depth — until the protected cgroup recovers. It cannot undo a lock-based inversion, but by squeezing lower-priority I/O it keeps a latency-sensitive cgroup's delays bounded. This is a containment mechanism, not prevention.
Kernel data structures summary
task_struct
└── io_context->ioprio (u16) ← set by ioprio_set(), read by get_current_ioprio()
struct kiocb
└── ki_ioprio (u16) ← copied from the task's ioprio at I/O submission
struct bio
└── bi_ioprio (u16) ← copied from kiocb; carries priority to block layer
struct request
└── ioprio (u16) ← copied from bio; seen by the scheduler
struct bfq_queue
└── ioprio ← derived from request->ioprio on queue creation
└── ioprio_class ← RT / BE / IDLE
└── weight ← (8 − level) × 10; sets share of disk time
└── max_budget ← sectors per round (auto-tuned, not weight-derived)
Choosing the right mechanism
| Scenario | Recommended approach |
|---|---|
| Single background process | ionice -c 3 (IDLE class) |
| Multiple competing applications | cgroup v2 io.weight |
| Database with latency SLA | cgroup v2 io.latency |
| Real-time audio / hardware control | ionice -c 1 -n 0 (RT, needs root) |
| Cloud VM storage isolation | cgroup v2 io.weight per container |
| NVMe workload | BFQ or mq-deadline; consider none for pure throughput |
| io_uring-heavy application | cgroup io.weight rather than per-process ioprio |
Observing I/O priorities in action
# See ioprio for all threads of a process
for tid in /proc/$(pgrep postgres)/task/*/; do
pid=$(basename $tid)
echo -n "tid $pid: "
ionice -p $pid
done
# BFQ queue stats per device
cat /sys/block/sda/queue/iosched/stats
# blktrace: see ioprio in the I/O trace
blktrace -d /dev/sda -o - | blkparse -i - | grep 'prio'
# ftrace: trace BFQ dispatch decisions
echo 1 > /sys/kernel/debug/tracing/events/block/block_rq_issue/enable
cat /sys/kernel/debug/tracing/trace_pipe | grep -E 'prio|ioprio'
# Verify cgroup I/O weight is being applied
cat /sys/fs/cgroup/myapp/io.stat # BFQ per-cgroup statistics
Key source files
| File | Contents |
|---|---|
include/uapi/linux/ioprio.h |
IOPRIO_CLASS_* constants, IOPRIO_PRIO_VALUE(), IOPRIO_PRIO_CLASS() macros |
fs/ioprio.c |
ioprio_set() / ioprio_get() syscall implementation |
kernel/sched/core.c |
get_current_ioprio() — reads from io_context or derives from nice |
block/bfq-iosched.c |
BFQ scheduler: per-queue budget, weight, RT/BE/IDLE class dispatch |
block/bfq-iosched.h |
struct bfq_queue, struct bfq_data definitions |
block/mq-deadline.c |
mq-deadline: per-class FIFO queues and deadline dispatch |
block/blk-ioprio.c |
cgroup I/O priority integration — maps io.weight to ioprio |
block/blk-cgroup.c |
cgroup blkio core — blkcg_get_queue(), weight inheritance |
Further reading
- I/O Schedulers — BFQ, mq-deadline, none, Kyber
- blk-mq — multi-queue block layer, request lifecycle
- Direct I/O — O_DIRECT path where ioprio most reliably reaches the scheduler
- Async I/O — io_uring interaction with BFQ's per-process model
- cgroups I/O — io.weight, io.max, io.latency reference
- Observability — blktrace, ftrace, iostat for verifying priority enforcement