目录
[二、Linux I/O栈:请求如何到达磁盘](#二、Linux I/O栈:请求如何到达磁盘)
[3.1 CFQ:人人有份的公平模式](#3.1 CFQ:人人有份的公平模式)
[3.2 Deadline:拒绝长队等待](#3.2 Deadline:拒绝长队等待)
[3.3 NOOP:简单到极致的"无序"调度](#3.3 NOOP:简单到极致的“无序”调度)
[3.4 三种调度器对比](#3.4 三种调度器对比)
[3.5 查看和修改调度器](#3.5 查看和修改调度器)
[3.6 一个具体的场景分析](#3.6 一个具体的场景分析)
[4.1 I/O优先级体系](#4.1 I/O优先级体系)
[4.2 基本用法](#4.2 基本用法)
[4.3 实战:不打扰用户的备份脚本](#4.3 实战:不打扰用户的备份脚本)
[4.4 ionice的选择策略](#4.4 ionice的选择策略)
一、引言:当多个进程同时读写磁盘
想象一个场景:你正在用浏览器加载网页(需要读取缓存文件),同时后台的备份脚本在疯狂拷贝大量文件到备份目录。这两个操作的I/O请求会同时到达磁盘。
如果没有调度,它们会按到达顺序排队。但浏览器的操作是交互式 的------你在等页面加载,每多等100毫秒都会觉得卡;而备份脚本是批处理任务,晚几秒完成无伤大雅。
I/O调度器就是要在这种资源竞争中找到平衡:让交互式操作快速响应,让批处理任务高效完成,换谁也不至于干等。
二、Linux I/O栈:请求如何到达磁盘
先从整体看清楚一个读/写请求的完整路径:
text
应用程序(write/read系统调用)
↓
VFS(虚拟文件系统层):统一接口,屏蔽不同文件系统的差异
↓
Page Cache(页缓存层):先写入内存缓存,由内核异步刷盘
↓
文件系统(ext4/xfs等):负责将数据组织为文件系统格式
↓
Block Layer(通用块层):组装成块I/O请求(bio),准备下发给磁盘
↓
┌──────────────────────────────────┐
│ I/O调度器(决定请求的排队顺序) │ ← 本文主角
└──────────────────────────────────┘
↓
设备驱动(与物理磁盘通信)
↓
磁盘硬件(HDD/SSD)
队列在这个流程中的位置决定了它的角色:通用块层将上层请求打包好后,不是直接扔给驱动,而是先交给I/O调度器排队。调度器根据算法决定"谁先谁后",然后将排好序的请求派发给磁盘驱动。
为什么VFS和文件系统不做这个调度? 因为它们面对的是文件和目录,而I/O调度器面对的是物理扇区地址------只有到了这个层次,才知道两个请求是否访问相邻位置、是否可以合并、是否在同一个磁盘上。
三、三种经典调度器
3.1 CFQ:人人有份的公平模式
CFQ(Completely Fair Queuing,完全公平排队)曾是Linux默认的I/O调度器,它的设计思想是:为每个进程分配独立的I/O队列,按时间片轮流向每个队列派发请求。
text
进程A队列:[A1 A2 A3] ─┐
进程B队列:[B1 B2] ─┤ → 调度器按时间片轮流分发 → 磁盘
进程C队列:[C1 C2 C3] ─┘
这种设计的意义在于确保没有单个进程可以独占磁盘。即使有一个进程在疯狂读写大文件,其他进程的I/O请求也能得到公平的响应机会,不会被"饿死"。
CFQ还内置了I/O优先级支持 ------优先级高的进程分得更大的时间片,它的请求在队列中等得更短。后面要讲的ionice就是基于这个机制。
适用场景:HDD,尤其是多用户、多任务的桌面或服务器环境。
3.2 Deadline:拒绝长队等待
CFQ太"公平"了------每个进程一个队列轮流服务------但也因此带来了问题:如果一个请求在队列尾部等待太久,它可能要等很多轮才能被派发。
Deadline调度器用一个简单规则解决了这个问题:每个请求都有"截止时间"。读请求默认500ms超时,写请求默认5000ms超时。当某个请求等待时间接近截止时间,调度器会跳过常规的排序逻辑,优先处理这个请求。
这意味着Deadline调度的核心约束是延迟上限可控------一个请求最多等多久,是可以预期而不会无限排队。
适用场景:数据库服务器(对读延迟敏感)或任何要求I/O响应时间可预测的场景。
3.3 NOOP:简单到极致的"无序"调度
NOOP(No Operation)调度器只维护一个FIFO(先进先出)队列,几乎没有调度逻辑。它做的工作就是请求合并:如果两个相邻请求访问连续的磁盘扇区,就合并成一个请求批量处理。
NOOP不对请求做任何重新排序。原因并不是它"懒",而是在某些硬件上,硬件自身已经做了排序,内核再做一遍排序反而浪费CPU。这些硬件包括:
-
SSD和NVMe:无机械寻道,访问任何位置速度相同,软件排序无意义
-
高端存储阵列(SAN/NAS):控制器内部有自己的I/O调度
-
虚拟化环境:宿主机Hypervisor层已经对I/O做了合并和排序
在这些场景下,NOOP作为内核层最简单的通道,把调度工作完全交给硬件,反而效率最高。
3.4 三种调度器对比
| 调度器 | 核心策略 | 队列设计 | 适用硬件 | 典型场景 |
|---|---|---|---|---|
| CFQ | 公平分配I/O带宽 | 每进程独立队列 | HDD | 多任务桌面、多用户服务器 |
| Deadline | 保证延迟上限 | 读/写两队+截止时间 | HDD | 数据库、延迟敏感应用 |
| NOOP | 仅合并相邻请求 | 单一FIFO队列 | SSD/NVMe | 所有SSD类场景、虚拟化 |
3.5 查看和修改调度器
bash
# 查看某块磁盘当前的调度器和可用调度器
cat /sys/block/sda/queue/scheduler
# 输出:noop [deadline] cfq
# [] 括起来的是当前生效的调度器
# 临时修改调度器
echo noop | sudo tee /sys/block/sda/queue/scheduler
# 查看系统中所有磁盘的调度器
for disk in /sys/block/sd*; do
echo "$(basename $disk): $(cat $disk/queue/scheduler)"
done
# 永久修改(添加到GRUB启动参数,以CentOS为例)
# vim /etc/default/grub
# 在 GRUB_CMDLINE_LINUX 中添加 elevator=noop
# sudo grub2-mkconfig -o /boot/grub/grub.cfg
现代Linux的变迁 :内核5.0+引入了全新的多队列(multi-queue)块层设计,对应的调度器也升级为
mq-deadline、bfq(Budget Fair Queuing,CFQ的理念继承者)和kyber(面向低延迟的简单调度器)。但无论调度器怎么升级,核心设计思想------让不同的硬件发挥各自的优势------从未改变。
3.6 一个具体的场景分析
场景 :你有一台HDD服务器,上面既跑着Nginx(需要快速读取静态文件响应Web访问),又有凌晨的rsync备份脚本(连续大量读盘)。用户反馈网站时不时卡一下,iostat显示await很高。
原因:rsync的大批量读请求抢占了CFQ调度器的大量时间片,导致Nginx的I/O请求在队列中排队等待,时延变大。
解决方向:
-
选项A:让rsync从Nginx不在用的时候读取(
cron调时间,最简单) -
选项B:降低rsync的I/O优先级,让它把调度器时间片让给Nginx(此时CFQ的进程级队列起作用)
-
选项C:换成Deadline调度器,利用读优先和截止时间机制,确保交互式读请求不被无限排队
选项B正是接下来要讲的ionice。
四、ionice:精细控制I/O优先级
4.1 I/O优先级体系
ionice控制I/O调度器分配给进程的优先级,它依赖CFQ调度器才能生效(NOOP和Deadline没有每进程队列的概念)。
三个调度类:
| 调度类 | 数字 | 含义 | 使用场景 |
|---|---|---|---|
| realtime | 1 | 实时优先级,其他进程必须等待它完成 | 通常不推荐用 |
| best-effort | 2 | 尽力而为(默认),可在0-7范围内调节 | 最常用 |
| idle | 3 | 空闲类,仅在磁盘完全空闲时才执行I/O | 后台批处理任务 |
best-effort类的优先级值(0=最高优先级,7=最低优先级):
text
优先级 0 ───────────── 7(默认 4)
高优先级 低优先级
(抢得快) (让着别人)
4.2 基本用法
bash
# 查看进程的I/O优先级
ionice -p PID
# 以最低优先级执行备份脚本(不干扰核心服务)
ionice -c 2 -n 7 rsync -av /data/ /backup/
# 以idle类执行日志归档(只有磁盘完全空闲时才干活)
ionice -c 3 tar -czf /archive/logs_$(date +%Y%m%d).tar.gz /var/log/
# 调整已有进程的优先级
ionice -c 2 -n 7 -p $(pgrep -f "rsync")
4.3 实战:不打扰用户的备份脚本
bash
#!/bin/bash
# backup.sh - 以最低I/O优先级执行备份,不干扰在线服务
LOG="/var/log/backup_$(date +%Y%m%d).log"
echo "[$(date)] 备份开始(I/O优先级:最低)" >> "$LOG"
# nice降低CPU优先级(第12篇讲过),+19是最低
# ionice降低I/O优先级,-n 7是最低
nice -n 19 ionice -c 2 -n 7 rsync -av --delete /data/ /backup/data/ >> "$LOG" 2>&1
echo "[$(date)] 备份完成" >> "$LOG"
这个脚本同时使用了nice(降低CPU优先级)和ionice(降低I/O优先级),确保备份操作不会影响白天用户的正常访问。
4.4 ionice的选择策略
| 任务类型 | 调度类 | 优先级 |
|---|---|---|
| 交互式应用(Nginx、MySQL) | best-effort | 0-3 |
| 普通脚本 | best-effort | 4(默认) |
| 定时备份 | best-effort | 6-7 |
| 日志归档 | idle | - |
五、综合实战:磁盘性能优化决策流程
当遇到磁盘I/O性能问题时,按以下步骤分析:
第一步:确认瓶颈在I/O
bash
vmstat 2
# wa列 > 10% = I/O瓶颈
# b列 > 0 = 进程在等待I/O
第二步:按硬件类型选择调度器
bash
# 查看当前磁盘类型
cat /sys/block/sda/queue/rotational
# 0 = SSD, 1 = HDD
# HDD → deadline 或 bfq
# SSD → noop 或 none
# 查看当前调度器
cat /sys/block/sda/queue/scheduler
第三步:降低非关键进程的I/O优先级
bash
# 识别高I/O进程
iotop -o # 按I/O使用率排序
# 对非关键进程降低优先级
ionice -c 2 -n 7 -p PID
第四步:在应用层面优化
bash
# 检查是否有大文件被反复重写
lsof +D /data/ | grep -E "^.*W"
# 适当调大Page Cache容量(让更多数据在内存中完成操作)
# 使用 tmpfs 存放临时文件
sudo mount -t tmpfs -o size=2G tmpfs /tmp
关于
lsof +D:+D表示递归搜索目录,输出访问该目录的所有进程和打开的文件描述符。加上grep W过滤出以"写"模式打开的文件。这个命令可以帮助你找到"谁在不停地往磁盘写数据",但+D递归搜索本身也会消耗I/O,慎在高峰期使用。
六、本篇小结
I/O栈路径 :应用 → VFS → 文件系统 → 通用块层 → I/O调度器 → 驱动 → 磁盘
三种调度器:
| 调度器 | 核心思想 | 一句话 |
|---|---|---|
| CFQ/bfq | 公平分配 | "每个人都得轮到" |
| Deadline/mq-deadline | 截止时间 | "读请求不能再等了" |
| NOOP/none | 无调度 | "硬件自己看着办" |
ionice核心命令:
| 场景 | 命令 |
|---|---|
| 最低优先级备份 | ionice -c 2 -n 7 rsync ... |
| 空闲才干活 | ionice -c 3 tar ... |
| 查看已有进程优先级 | ionice -p PID |
动手练习
bash
# 1. 查看所有磁盘的调度器
for disk in /sys/block/sd* /sys/block/nvme*; do
[ -d "$disk" ] && echo "$(basename $disk): $(cat $disk/queue/scheduler)"
done
# 2. 判断你的磁盘是HDD还是SSD
cat /sys/block/sda/queue/rotational
# 0 = SSD, 1 = HDD
# 3. 创建两个后台测试任务——一个高优先级I/O,一个低优先级
dd if=/dev/zero of=/tmp/high_io bs=1M count=500 &
ionice -c 2 -n 7 dd if=/dev/zero of=/tmp/low_io bs=1M count=500 &
# 用iostat观察磁盘活动和吞吐量
iostat -x 1 5
# 4. 安装iotop查看各进程的I/O使用情况
sudo apt install iotop -y
sudo iotop -o # 只显示有I/O活动的进程
# 5. 清理测试文件
rm /tmp/high_io /tmp/low_io
七、下篇预告
I/O调度器解决了磁盘读写请求的排队问题,但数据最终要通过网络传输给用户。当并发连接数飙升时,你可能遇到过"TIME_WAIT状态满屏"的困扰。
下一篇我们将深入Linux网络协议栈与TCP参数调优 ,学习TCP三次握手/四次挥手的内核实现,理解TIME_WAIT的由来,以及如何通过调整net.ipv4.tcp_tw_reuse等内核参数来优化高并发场景下的网络性能。
延伸思考 :SSD上没有机械寻道,为什么很多发行版在SSD上仍然默认使用mq-deadline而非none?因为多队列框架下,mq-deadline在实现请求合并 和写回优化方面仍然比完全的"无调度"有优势------将相邻扇区的写请求合并成批量操作、通过控制写积压来避免延迟飙升,即使在随机访问同样块的所有位置的SSD上,这些优化依然有意义。