xiaoqiang 2019-04-07
今天我们分析一下当向磁盘提交了一些数据请求后,是如何经过以太网发送出去的。本文暂时不考虑文件系统和设备堆叠(例如md和dm)的情况。这部分内容本身就非常复杂,如果放到一起将更加复杂,不容易将清楚。关于文件系统的读写流程本号之前介绍过Ext2和ocfs2的相关内容,大家可以看一下历史文章。设备堆叠的内容我们后面再单独介绍。
废话少说,我们开始介绍吧。我们先从整体上了解一下整个IO流程中涉及到的各个系统。如图1所示,最上面是通用块设备层,这里是大家公用的,包括磁盘、光盘和优盘等等。然后是SCSI的高层驱动,比如磁盘驱动。然后是SCSI的中间层和SCSI的底层驱动。底层驱动通常是针对设备的比如FC卡和SAS卡等,也可能是虚拟的,比如通过TCP协议虚拟一个启动器等。
磁盘IO的起始是从一个名为submit_bio的函数开始的,从名字也可以看出来,这个函数用于向通用块设备提交一个IO,而这个IO在内核中叫做bio。我们先看一下这个函数的声明:
void submit_bio(int rw, struct bio *bio)
这个函数只有2个参数,一个是rw,它是readwrite的缩写,用于表示这个请求是读请求还是写请求;另外一个是bio,这个用来存储数据及处理IO的目的设备(也就是到底由谁来处理这个IO)。
图2 submit_bio概要流程
函数submit_bio进行io合法性的基本判断后会调用generic_make_request函数进行处理。从函数名称上也可以看出该函数是进行常规处理的。该函数有一些处理技巧,我们暂时不关注,看一下主要处理逻辑,具体如下:
可以看到这里其实主要分为2步,一个是从设备中获取请求队列,另外一个就是通过请求队列的处理函数对该IO进行处理。我们这里暂且记住对于scsi设备这里的处理函数是scsi_request_fn即可,至于如何初始化的我们后面再介绍(后面也直接给出结果)。这里给出一个整体的处理流程。
进一步,可以看到该函数最终调用的是host模板中的queuecommand函数指针,而该指针指向的函数为iscsi_queuecommand。对于iSCSI来说,该函数构建一个新的task,并将该task放入一个队列中,交给一个后台线程去处理这个task。
这里的后台进程是一个工作队列,工作任务是由iscsi_xmitworker完成的。下图是整个调用过程,我们这里暂时忽略一些处理细节,只给出核心函数的调用流程。从图中可以看出,最后会调到socket的kernel_sendmsg函数,该函数将数据或者请求发送到存储端。
前面概要的介绍了整个流程,我们并没有介绍很多细节,也没有介绍一些函数指针是如何初始化的。本节我们介绍一下这些用到的函数指针的初始化环节。
make_request_fn函数指针
我们在图1中的块设备层包含两个IO路径,一个是IO调度路径,另外一个是多队列路径。我们在前文并没有对这部分进行介绍。本节我们具体介绍一下这2个路径的具体含义。
IO调度路径大家都比较清楚了,就是IO到块设备后,块设备会进行相应的合并和调度(比如deadline等),以保证性能最优。这个调度算法通常是针对机械磁盘设计的,因为机械磁盘的寻址(磁盘摆臂)等操作非常耗时,因此这里通过算法减少寻址的次数。
另外一条路径是多队列路径,多队列是在内核3.1版本后加入的新特性。这个特性主要是针对日益留下的SSD磁盘做的优化。因为,SSD磁盘并不需要进行调度优化,而且传统单队列的方式在内核层面已经成为性能瓶颈,因此这里开发出多队列的特性,以提高系统的整体性能。
废话一箩筐,我们看一下make_request_fn指针,在generic_make_request函数中最终调用设备队列中的make_request_fn函数指针处理IO请求。而设备队列属于一个具体的SCSI设备。我们找一下这个SCSI设备初始化的地方,具体为函数scsi_alloc_sdev,在该函数中我们看到如下内容:
从上面代码可以看出,请求队列是在这里初始化的,并且会判断初始化为多队列模式,还是单队列模式。具体初始化过程大家可以自行阅读一下图中的代码,这里的代码逻辑并不复杂,本文不在赘述。
queuecommand函数指针
我们观察一下该函数指针所属的结构体,发现是scsi_host_template模板,而该模块有属于Scsi_Host。我们之前一篇文章提到过,Scsi_Host对应适配器(HBA卡),也就是说处理命令的函数指针是在适配卡数据结构创建的时候初始化的。这个也符合我们的日常认知,因为适配卡是负责传输数据的,处理命令的接口自然应该在其中。对于iSCSI来说,创建适配器数据结构的iscsi_sw_tcp_session_create,我们看一下该函数的实现。
通过上面代码可以看到,在这里初始化了很多函数指针,包括我们这里所说的处理命令的queuecommand。具体细节本文不再废话,大家可以自行看代码理解。
另外上面的xmit_task和xmit_pdu等函数指针也是在这里初始化的,请自行阅读代码,本文不再赘述。
磁盘的IO调度是整个IO路径上非常重要的部分。虽然未来多队列可能会取代IO调度部分的功能,但还是有必要介绍一下磁盘的IO调度部分的内容。
我们知道在Linux操作系统中有Noop、DeadLine和CFQ等调度算法,而且可以通过下面的命令修改每一个磁盘的调度算法。但是本文今天并不介绍这些调度算法的原理和实现,而是介绍早IO路径中这些调度算法是如何起作用的。
echo 'cfq'>/sys/block/sda1/queue/scheduler
调度算法其实是整个IO路径的一部分(这句是废话),它在请求队列初始化的时候进行的初始化。我们先看一下算法是在IO路径中的使用情况:
从图上可以看到该函数的内部调用了某种类型的函数指针,观察可以看到是e-type类型。这个类型其实就是上面说的调度类,比如CFQ。我们需要知道每一个请求队列都有一个对应的调度队列,而每一个调度队列都有一个调度类相关联。在调度类中有相关的调度函数实现具体的调度算法。下图是它们之间的关系:
下面是CFQ调度类的初始化代码,具体在block/cfq-iosched.c中定义:
今天先到这里,后面我们介绍一下多队列的实现。