iscsi:IO操作流程(一)

从应用的视角,iscsi展现为一个块设备,即一块硬盘。在Linux操作系统中可以通过fdisk -l看到这块磁盘。iscsi协议所涉及的一系列的组件经过层层虚拟化,在多个层次上其操作与本地硬盘无异。这实际上是一个性能、可靠性、实现多个角度权衡的结果。事实上,在所经历的各层次中,有一些次层对于iscsi磁盘来说是多余的,但操作系统的设计者为了共用SCSI实现层,将scsi initiator对接到SCSI中间层以下,作为一个scsi传输层实现。因此,自scsi层以上,iscsi提供的块设备与sas、SATA提供的块设备所经历的IO流转一致。

本文所涉及的iscsi集中考虑iscsi协议承接块设备应用说起。事实上,iscsi作为一个承载SCSI协议的传输层,除能够支持基于磁盘价值的块设备外,还可以支持其他的SCSI设备。比如磁 带、CDROM。业界也有通过iscsi实现为磁带的应用,如VTL。虚拟为其他设备时,其IO流转略有区别。scsi子系统通过INQUIRY探测LUN的设备类型,进而由上层驱动“认领”对应的设备。

iscsi:IO操作流程(一)_第1张图片

用户的数据在initiator节点上依次经过文件系统层、块设备层、SCSI层。理论上我们也可以将通过裸scsi设备直接发送IO相关SCSI指令到SCSI中间层,但这条路径的初衷并非如此,没有针对IO处理进行优化。故只在上图中表示出来,并不做具体分析。
文件系统、裸设备在应用角度,其一般流程均为:
- 打开文件,获得句柄(系统调用open
- 一次或多次以字节为单位对文件进行读写(系统调用readwrite
- 关闭文件(系统调用close)

应用的IO请求通过系统调用到达内存。在适合当的时机,操作系统会将上述操作构建为bio结构,调用submit_bio将请求传递到通用块层。bio是通用块核心的IO处理单元。bio核心表达的是数据在内存和在磁盘中的映射关系。其中,
缓存中的地址以内存页号和页面偏移表示。以struct bio_vec表达:

struct bio_vec {
    struct page    *bv_page;   /*存储数据的页面*/
    unsigned int    bv_len;     /*数据长度*/
    unsigned int    bv_offset; /*数据在页面中的偏移*/
};

在磁盘上的地址以bvec_iter表达:

struct bvec_iter {
    sector_t            bi_sector;    /* device address in 512 byte sectors */
    unsigned int     bi_size;    /* residual I/O count */
    unsigned int     bi_idx;        /* current index into bvl_vec */
    unsigned int     bi_done;    /* number of bytes completed */
    unsigned int     bi_bvec_done;    /* number of bytes completed in current bvec */
};

在新的内核版本中,对bio数据结构进行了一些调整。老的版本中扇区号、扇区数等信息直接放在bio成员中。新的版本中将其集中放到bev_iter中。此变化不影响bio在设计中的位置

submit_bio最终调用generic_make_request函数将bio插入到请求队列。generic_make_request接收一个bio组成的链表,并对其进行分个处理。对于每个bio首先进行一系列应用的检查,然后调用具体块磁盘驱动的make_request_fn进行处理。scsi磁盘驱动初始化的过程中,将make_request_fn直接指定为blk_queue_bio。这个函数根据内核配置的IO电梯算法,将IO拆分合并到请求队列中。
上述的故事就是大家耳熟能祥的IO缓存与IO调度。在此不详细描述。可以参考其他资料研究buffer cache以及电梯调度算法相关的算法。转到iscsi实现的角度。initiator的处理过程并不需要电梯算法。毕竟到目前为止,我们数据离落盘还有很远很远。况且系统数据落盘是在target端进行,在initiator端进行电梯算法意义不大,target端有可能对数据进行重新散列,当下排序也用的LBA还需要经过再次映射,逻辑上相邻不代表物理是想邻。
还是那句话,在iscsi流程过程中,之所以将这些流程纳入,完全是一种设计的权衡、抽象的哲学问题。
我们的故事刚刚开始。
自sd设备驱动开始,便进入SCSI处理的流程。也即iscsi相关语境的上下文。从此刻,系统要回答的问题有以下几方面:
1. IO如何封装成scsi指令,封装成什么样的scsi指令
2. IO如何根据设备的特点构建任务,经过协议所定义的host、taget、lun等相应的处理
3. IO处理过程中的异常采用什么方式,以什么形式报告给initiator等

sd实现了一个块设备(drivers/scsi/sd.c)用以接受IO请求,并最终调用scsi_setup_fs_cmnd将请求向scsi_cmd转换,进而分发到scsi中间层进行处理。scsi中间层可以理解为SCSI处理的一个框架实现,提供了SCSI系统初始化、设备扫描、SCSI命令调度执行以及错误恢复框架等。从IO角度,SCSI中间层主要是接受IO相关的SCSI指令,进一步完善相应用的信息,调用scsi_dispatch_cmd传递到下层进行处理。
最终的scsi指令的执行是在target端进行。scsi transport层是负责SCSI指令传输的,像SAS、FC、iscsi等都是传输层的具体实现。传输层有对应的协议实现,其核心实现思路是scsi请求进行的封装。上述的SCSI命令只是scsi请求的一个方面,除此之外还有目的LUN、任务管理、数据等信息,具体信息可参考SAM相关文档。一个SCSI的具体执行可以形式性的表达为:

Service Response =Execute Command (IN ( I_T_L_Q Nexus, CDB, Task Attribute, [Data-In Buffer Size],
[Data-Out Buffer], [Data-Out Buffer Size], [Command Reference Number], [Task
Priority]), OUT ( [Data-In Buffer], [Sense Data], [Sense Data Length], Status ))

一个SCSI指令相关的请求在传输层被称为一个task(任务),当SCSI命令被发送到scsi传输协议服务系统时,任务被建立。任务用于管理SCSI在传输层流转的一系列状态信息。当SCSI target返回响应之后task重命周期完成。一个传输任务可分为一个或者多个数据包传输给target。在iscsi协议中用PDU表示每个数据包。iscsi协议是scsi传输层的具体实现,它实现的了initiator与target间的认证、会话建立、数据联接管理、数据传输、任务控制、数据安全等一系列的服务。iscsi采用PDU描述传输数据包,是iscsi层各节点间交互的基本单位,iscsi协议是scsi传输层的具体实现。iscsi PDU将使TCP协议或者其它流式协议进行传输。在Linux系统中实现了iscsi_tcpiscsi_iser两个传输模块。

以上就是Linux内核中iscsi协议initiator端实现的具体流程,流转过程中,一个IO请求自上而下经历了biorequestscsi_cmdiscsi_task以及iscsi hdr(PDU)等变化。

思考:
本文提到为了系统实现概念的统一性、设计的一致性以及层次抽象与共用,操作系统将iscsi协议对接到scsi中间层。这种实现对性能的损失有多大?如果我们在device-mapper之下实现又会怎么样?欢迎讨论。

你可能感兴趣的:(块存储,存储技术原理分析,磁盘,iscsi,存储技术原理分析,存储协议)