IO调度器详解

一、调度器演进

1.1 什么是IO调度器?

       5de7233f700b3071c68ede9d9d3a5c75.png

传统的磁盘因为有磁头,磁头移动有开销。最早的调度器会对访问磁盘的IO做基于磁盘访问位置的排序和合并, 让磁头以最少的移动来完成最大的IO量, 以提升系统IO带宽。

b854585bca3e9b53088fea4694712c73.png       af51cb51bc1c481c526b4a0e3f8b59aa.png

现在的SSD, 物理上已没有磁头的概念, 访问的位置也是LBA(Logical block address), 器件内部负责找到LBA到物理位置的映射关系。排序就没有太大作用, 随机访问的速度也接近顺序读写。IO调度主要是以服务应用场景为主(读优先,VIP线程优先,时间片/带宽公平调度等) 。    

IO scheduler处于内核IO栈 中block layer的一部分,主要作用就是对IO 进行qos, 根据不同的qos目标(延迟、公平、带宽、功耗等)有各种各样的调度器:历史到现在mainline出现过(红色是现存的):noop(只做简单的merge )/deadline(排序,读写区分对待,延迟控制)/cfs(时间片公平)/mq-deadline/bfq(带宽公平)/kyber(不同的IO类型 q不同的延迟)    

     
未进mainline的调度器:

SIO(不合并IO)、VR(翻转磁头惩罚机制)、Row(read over write)、Zen(不合并也不排序,仅有超期处理) 、Maple(不合并不排序,亮屏和灭屏策略不一样) 、FIO(基于IOPS指标 进程公平)等。

这些调度器随着发展, 目前只有右图这样子:

030bda4168b0401cceece5034206a633.png

 现在看看这些调度器的策略有什么不同:    

1.2 kernel 6.1 社区调度器

Kyber调度器

2017年引入,2018后就不怎么修改(目前为止,没有支持io priority /blk cg)。核心思想, 就是把IO分成不同的类型(读、写、擦除、其他),  每个类型的队列单独控制延时, q的是不同的延迟, 所有策略围绕这个目标展开。

22f0c411ea856ad2e6978c9dd253471b.png     

cde0e333667bc7a2c0f7d19642e72b30.png

默认情况下延迟控制如上

         
1.3 MQ-DEADILINE调度器    

作为deadline的mult queue版本。deadline在内核2.5时代已经存在。         
核心思想没有变化:     

1. IO进入调度器会被同时加入 读写rb tree 和fifo 队列中, 按访问sector排序。         
2. 如果有超期的IO ,先发送超期的IO, 无超期的IO 按优先级发送IO, 同优先级读写非严格按照2:1 dispatch,  发送IO时 按各自rb tree 发送(sector 增长方向)。         
3. 读和写的超期时间不一样。         
1ceb374466dc5e24fa5ce4ebcb24fe78.png          
4. 预留了1/4 req给同步IO(除去write)。

7b6ba24cd6d1e9b8d0efef90cf9a7373.png          
5. dispatch先检查超期req (读和写超期时间不一样),  无超期 再按读写2:1的比例下发IO。

6. linux-5.14开始支持ioclass/ionice。

7. 不支持blk cgroup

1.4 BFQ调度器    

改自CFQ,BFQ 是 带宽公平的调度器, 支持cgroup.  从4.12版本开始支持, 设计原则想兼顾带宽和响应。实际上由于算法复杂,最低lat会其他调度器多数倍。         

核心特性:

1. “budget fair”策略:每个线程公平的分配sector数进行IO, 因交互式进程只需少量的budget, 交互式进程得以快速响应。

2. 优先级支持:进程bfq weight越大(新进程权重临时提升),budget增长越慢(意味着调度更频繁)。         
3. 调度效率。引入idle_time,做完一个thread的IO后 。为了不让这个APP后续IO重新排队(也为了新的APP得到比较好速度), 会idle一定时间再切换下一个线程。

二、kernel6.1的elv(bfq/kyber/mq-deadline)

elv 实现其实是一套call back 嵌入在block layer, 搞清楚这些call back就可以看出不同elv的核心思想。

           149e8c512ecfc7c8eba6c28e008731f7.png

        cff8037de95c1201e4cd0691ef92a995.png

 一笔IO 要在elv中经过:allow_megre->bio_merge->requset_merge->request_merged->limit_depth->prepare_request->insert_request->has_work->dispatch_request->completed_request->finish_request 才可以一笔IO。

在block layer, 什么时候IO 会进入调度器?

实际上有很多入口,整体上submit_bio是从fs下来的普通IO(常规的文件访问),其他一些入口主要给一些特别的IO使用,比如ioctl(查询器件状态等), flush等。这些特别的IO 虽然会经历这个flow,但通常by pass调度器。         

        d53c3c7a430ee0c7860adb8947ee39e0.png

insert req就是把req挂入到elv的队列中(每种elv的队列实现方式通常都不一样), dispatch的线程不一定是IO的发起者, 有两个case:          
    1.  IO业务发起者直接下发到设备驱动,(需要同步IO + 设备ready)          
    2.  经过run_work异步线程转发(异步IO,或同步IO发起时,设备没ready)    

        dff2dcd6f7ccc51b5691006663940b38.png          
2.1 Kyber调度器

6e5583c58ace9ab1a9705941543777f0.png

Kyber引入调度域的概念:给IO划分了4个调度域,每个域都有一个bitmap, 表示该域req 资源占用情况。

d8123706998bfaf951e32e332a05060a.png

每个调度域的深度不一样, 即可用req数量不一样。插入IO实际上就是插入到这个阈所属的队列上。

4a854dd896653624c6d3f97d08360eeb.png

每个调度域batch size不一样,即每次一个调度域单次最多dispatch处理batch_size个reqs。    

b3e2138230e70fead65e783e914e2cc8.png

异步IO 占比75%, 即总是预留25% 的req给同步IO。

123a57cce1e8ceb2e5241285b064b2ae.png

不同类型IO延迟控制不一样。

2759b06cb49923906f65f3f37f29c7dd.png

Kyber insert req

可以看到就是根据IO的类型, 比如是读 还是写 或擦除操作,把IO加入到不同阈的队列当中。    

08667c5aab9afc7241621f4efeac3dd1.png

Kyber dispatch

Dispatch则是根据不同阈IO的质量(kyber以IO花费的平均时间作为指标,),选择合适的队列进行dispatch

                       
 选择一个domain 最多发送batch个req.

3047b07b78e3696a3023f192b7385318.png

找到队列中的req进行dispatch 

 b6e94d466a0a23d985a41b83af9b6f70.png

kyber在选择domain时,要根据IO的质量(IO花费时间), 下面说说质量是如何算的:         
Kyber 时延计算    

04c65aa527f312874d3d3507c0a48797.png

KYBER_IO_LATENCY: rq执行的时间(类似D2C)

KYBER_TOTAL_LATENCY:rq从申请出来到执行完的时间(类似G2C)

13bf64e4e3f43ec01ae0fab362054012.png

 简单的可以理解为,kyber在IO完成的时候,根据IO的花费时间,在bucket中选择合适的位置进行计数。 比如1 为延迟10ms, 如果这笔IO完成在3/4 * 10ms -> 10ms之间, 则在1的bucket里+1;

 这样定期查看下某个类型的buckets 比如READ domian, 就可以看到99% READ 延迟在哪个区段。如果落在1之内认为IO是好的, 如果都大于1 认为IO是坏的。

         
如果IO是坏的,那以后就多发这个类型的IO,少发其他类型的IO, 这类型IO的延迟就可能会好转。Kyber就是通过调整发不同类型IO的数量来完成IO的时延控制。     

   
看下他是如何调整分发数量的:    

  Kyber 根据时延调整 队列深度

d18b52e6e55fe8c736b096e7e5375146.png 

good_buckets即上述所指1的位置, 计算99%的IO和90%的IO在什么区段,来调整队列的depth

e9f2ad93e3caa45505feaa074a0c00be.png

如果比较差,就增加对应队列一倍的depth. 如果很好就缩减到3/4的深度。

延迟的计算如下    

b5430441dd2bd229528b3a93316a600d.png

Kyber小结

4e833e0f9ec0995385f443b7e59be419.png

2.2  mq_deadline调度器   

从数据结构来看, 他有优先级的概念,并且有所谓的方向(读or写)

struct deadline_data *dd = q->elevator->elevator_data;

19d219da63fc4e35330e89397b69f0f0.png

d45485443f669f0864c28855f5a636ca.png

同时也有expire的概念。

43419ee392697f66dfee775f764a074b.png

从数据上看, 总结为:

mq_deadline 三个优先级,两种队列,两种IO类型

         
mq_deadline 插入req,  就是根据IO是读写, 把IO插入到对应优先级的双重队列里:即插入到fifo队列 和sort队列(根据IO访问磁盘的位置)中    

mq_deadline insert req

63daeaacb9e4fa7c4144dbf90449a456.png

分发条件也简单, 统计进入elv的总数,有IO 就可以发

mq_deadline has work

f5658aa8627c3ea1334bf0715ed92858.png

所谓的deadline,即IO进入elv开始到dispatch有一个deadline.          
因此在发送顺序上, 要先找超期的req进行发送, 如果没有超期的req,则按优先级进行发送。    

mq_deadline dispatch

6439d1f807d28b2d37bbdc81f222ed03.png

mq_deadline 小结

IO 按磁盘位置在对应优先级的sort tree上排序,发送时先发超期IO,再按优先级发送。(实际上还有write_starved的概念,deadline倾向于发读IO,为了防止写饿死, 会发write_starved次读,才会发一次写)

4982ed565d7267e16318c735c23ad86a.png

         
2.3  BFQ(Budget Fair Queueing)调度器   

21f3853262c21823da887c2eb6643428.png

BFQ的核心思想,就是by thread的发送IO,有点CPU 调度的味道, 每次能发的最大IO量即所谓的Budget。BFQ想保证不同的thread在IO量上是“公平的”。

整体思想就是每个thread在IO上也有一个vtime, 每次总是选vtime小的thread进行发送IO。对于IO的vtime的增长, 在BFQ上是根据这个线程发过的IO量*系数来决定的, 每次能发的最大虚拟IO量 即所谓的buget。

而这里的系数,则比较复杂,BFQ会根据不同类型的IO负载 ,给thread打上标签,比如是交互式线程 还是IO消耗性线程,甚至是刚起来的新线程,依赖多个维度权衡去动态调整系数。从而保证整个系统有比较好的IO响应且比较好的带宽。         

6037acef08cab03c7a48cf605340e697.png

由于BFQ的细节流程比较复杂,这里列出其flow:

 BFQ insert req

8154a93a9760753cbd025da7a602079a.png    

BFQ dispatch req

b989328bda5153966f026e54bb989ccc.png

BFQ dispatch req

总是选vtime最小的进行发送

e4dfd219a35417931cff262fde1f887c.png          

三、cgroup/ionice          


有些调度器是支持IO优先级和cgroup的,业务方时间上可以根据这两个机制 来调整自己IO的质量。

usage: ionice [-t] [-c CLASS] [-n LEVEL] [COMMAND...|-p PID]

deadline idle/RT的带宽差距

7f7efb0190149b6c4bca67c98206c3d8.png

d6143e3a965582778563efb4aefbe25f.png

两个cgroup: target为idle, load为RT

ae790977c2f9b92b9719a323910f1109.png

cgroup

dc247e0f9383b6644b1266f9dbc10039.png6fec2c55dbcbfb65ce4c0371d4759278.pngexample:

v1:echo 100 > /dev/blkio/target/blkio.bfq.weight          
v2:echo "8:16 rbps=2097152 wiops=120" > io.max    

eba59a047475cb8938d28c374fae926f.png

19cd3551119df951bfa29b6b83a860d2.png

四、other vendor elv

https://github.com/codediablos/io-scheduler/blob/master/vr-iosched.c

https://github.com/codediablos/io-scheduler/blob/master/sio-iosched.c

https://github.com/Pzqqt/android_kernel_xiaomi_whyred/blob/5e5993a2fa5c19d29fe63ee628299ff97ba910fd/block/fiops-iosched.c

https://github.com/codediablos/io-scheduler/blob/master/row-iosched.c

https://github.com/Pzqqt/android_kernel_xiaomi_whyred/blob/5e5993a2fa5c19d29fe63ee628299ff97ba910fd/block/maple-iosched.c#L4

https://github.com/codediablos/io-scheduler/blob/master/zen-iosched.c

参考资料:

1.https://elixir.bootlin.com/linux/v6.1.80/source/Documentation/block    

2.BFQ:

https://blog.csdn.net/hu1610552336/article/details/125862606    

Android分区挂载原理介绍(上)

Android分区挂载原理介绍(下)

Perfetto数据流架构故障分析:带你研究 trace 为何丢失

603f853616b56e0beb43ead4095b16c6.gif

长按关注内核工匠微信

Linux内核黑科技| 技术文章| 精选教程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

OPPO内核工匠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值