当前位置:   article > 正文

iscsi:IO操作流程(二)_scsi_prep_fn

scsi_prep_fn

上次我们讨论了iscsi initiator IO操作需要经过的各个层次,以及每层所涉及的IO数据结构的变化。今天主要讨论IO如何形成SCSI指令并下发的。
我们知道在通用块层,IO最终放在request_queue中暂时保存。为了减少寻道时间,通用块层采用“蓄流”的方式,将IO蓄到队列里一段时间,使接下来的IO有机会与已经存在的IO进行合并,即所谓的电梯算法。
当在一定合适的时机,“蓄流”打开时,IO继续进行向下层传递,进而调用块设备驱动为其请求队列提供的request_fn进行处理。SCSI磁盘驱动在扫描发现并为磁盘分配请求队列时将期实例化。具体指向为scsi_request_fn函数。
下图展现了scsi_request_fn的调用情况。


这里写图片描述

该函数调用blk_peek_request中逐个取出请求,并进行处理。每次调用它就从可处理的请求队列中获到一个请求。请求有可能是新的,有可能是由于SCSI子系统的底层相关设备不能处理请求再次加入到请求队列的。如果已经处理过意味着scsi cmd已经申请;如果是新的,调用prep_rq_fn进行一些请求处理前的准备工作。scsi磁盘驱动(以下简称sd)在这个时间进行SCSI命令的构建(SCSI CDB)。这个预期可能有三种返回值:


- BLKPREP_OK:表示命令初期准备成功,则继续往下执行
- BLKPREP_DEFER:暂时不能够进行处理,则将请求再次加入到队列
- BLKPREP_KILL:请求没办法继续进行下去。这时从队列中请下请求,向上层报告IO失败。


接下来做三件事:


首先,进行设备的检查工作。
当前的计算机体系结构中,设备的操作明显慢于CPU的计算能力,是现代计算机的关键瓶颈之一。因而,在真正需要设备进行处理之前,尽可能的完善检查IO处理的条件是否已经满足。总之,针对IO层面的错误处理,要么尽可能多的恢复现场使业务能够正常的运行下去,要么尽可能的早的失败,给予应用进行错误处理的机会。此次的检查主要针对后者,主要是硬件层面的检查,具体点是对设备状态、目标端、主机适配器等。
设备不能接收IO的情况分为三种分别为设备离线、设备正忙于处理其他事务、设备被阻塞住:
- 设备离线时,当设备再次上线时,需要重新初始化、重新建立请求队列,当前的状态和数据不需要处理,因而IO请求不需要进行处理了,直接放弃掉并向上层返回错误信息。
- 设备忙和设备阻塞住意味着设备临时不能处理IO请求,一旦状态移除,IO将会被继续处理,此时系统将IO请求放置到延迟队列中,择机进行处理。
- 如果设备的状态没有问题,而设备所属的目标端(target)、适配器(host)存在问题时,可以确保是临时不能处理IO,此时将IO再次加入到设备的请求队列中,下次再次进行重试。


其次,初始化错误处理。
scsi_init_cmd_errh用于命令执行出错处理的初始化,设置超时定时器(新内核中的出理只进行了一些初始化的工作,超时没有进行设置)。


最后,将scsi命令转交到底层设备(LLD Low Level Device)进行处理。
scsi_dispatch_cmd用于将命令传递到底层设备驱动进行处理,一旦这个操作完成,意味着IO请求已经穿过SCSI中间层,传递到SCSI transport layer层。如果该返回0表示分发成功,否则表示出现错误,不再继续处理请求队列中的其它请求。
这个函数的主要工作如下:
- 检查设备是否已经被删除。如果被删除,不需要下发了,直接返回错即可。
- 检查设备是否被阻塞住。
- 检查命令的长度是否已经超过设备可以接受的长度。
- 调用主要适配器的queuecommand命令将数据进行分发

如果SCSI指令没有被分发,最终可能因为:
- SCSI_MLQUEUE_DEVICE_BUSY:设备临时被阻塞。
- SCSI_MLQUEUE_TARGET_BUSY:目标被阻塞
- SCSI_MLQUEUE_HOST_BUSY:主机适配器被阻塞
在上述三种情况下,将IO请求再次放入队列,以便下次重试。

到此为止,SCSI层面的处理完成。
总结一下,IO请求将视系统当前的情况进行处理,最终都会执行结果分为三种情况:
当设备因故障移除时,直接向上层报告IO错误;当SCSI子系统的相关设备(适配器、目标节点、设备)暂时不能处理IO,加入到队列后期处理;当设备正常时,回调适配器的queuecommand进一步进行处理。


返回来,我们再讨论一下SCSI命令初始化的问题。
上面说明,系统根据请求信息准备scsi指令在prep_rq_fn中进行。sd初始化时,将其初始化为scsi_prep_fnscsi_prep_fn调用scsi_setup_cmnd进行scsi命令的初始化工作。scsi子系统的请求两个层次(1)上层用请求(内核中称做来源于文件系统的请求,这种说法不是特别的准确);(2)来源于scsi中间层的请求。前者需要构建SCSI命令,后者SCSI命令已经构建完成。

来自上层应用的请求需要构建成哪些指令,如何构建指令与设备类型有很大关系:来源于磁盘的IO请求需要构建的命令与块操作相关,遵循SCSI SPC标准,而来源于磁带的IO请求需要遵循相关的标准。下图表达了SCSI协议族各协议之间的关系。一般地,设备类型特定的命令集由具体设备驱动来实现。此时,将调用scsi上层驱动的init_cmd实现。对于磁盘设备,设备加载时,将其指向函数:sd_init_command
综上sd设备来源于上层应用请求的scsi命令构建调用关系为:
prep_rq_fn->scsi_prep_fn->scsi_setup_cmnd->scsi_setup_fs_cmnd->sd_init_command

个人感觉上层应用请求命令构建的时机找的有点别扭。道理上,由sd来构建磁盘读写相关的命令是符合常理设计的。sd作为SCSI服务的上层驱动,主要通过设备类型与其他相同层次的上层驱动在功能进行区分。必然的,磁盘IO相关的SCSI指令的构建由sd来完成;磁盘带IO相关的SCSI指令由st来完成…………但是时机选择回调机制实现,而不是在请求下发之前就准备好,不确定是什么原因。

这里写图片描述

sd_init_command函数实现如下,

static int sd_init_command(struct scsi_cmnd *cmd)
{
    struct request *rq = cmd->request;

    switch (req_op(rq)) {
    case REQ_OP_DISCARD:
        switch (scsi_disk(rq->rq_disk)->provisioning_mode) {
        case SD_LBP_UNMAP:
            return sd_setup_unmap_cmnd(cmd);
        case SD_LBP_WS16:
            return sd_setup_write_same16_cmnd(cmd, true);
        case SD_LBP_WS10:
            return sd_setup_write_same10_cmnd(cmd, true);
        case SD_LBP_ZERO:
            return sd_setup_write_same10_cmnd(cmd, false);
        default:
            return BLKPREP_INVALID;
        }
    case REQ_OP_WRITE_ZEROES:
        return sd_setup_write_zeroes_cmnd(cmd);
    case REQ_OP_WRITE_SAME:
        return sd_setup_write_same_cmnd(cmd);
    case REQ_OP_FLUSH:
        return sd_setup_flush_cmnd(cmd);
    case REQ_OP_READ:
    case REQ_OP_WRITE:
        return sd_setup_read_write_cmnd(cmd);
    case REQ_OP_ZONE_REPORT:
        return sd_zbc_setup_report_cmnd(cmd);
    case REQ_OP_ZONE_RESET:
        return sd_zbc_setup_reset_cmnd(cmd);
    default:
        BUG();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

它根据请求的操作的类型构建不同的scsi指令。
- REQ_OP_DISCARD:告诉块设备放弃使用某指定的块。这个请求是配合自动精简配置使用。这个功能对应SCSI指令的UNMAP命令。当此操作没调用时,target不再为相应 LBA映射存储空间;
- REQ_OP_WRITE_ZEROES:对指定的1个或者多个扇区写0
- REQ_OP_WRITE_SAME: 将1个或者多个扇区写成相同的数据。
- REQ_OP_FLUSH:通知target同步缓存数据
- REQ_OP_READ:读操作
- REQ_OP_WRITE:写操作
- REQ_OP_ZONE_REPORT REQ_OP_ZONE_RESET:应用于瓦式存储的特殊操作
请写请求将sd_setup_read_write_cmnd函数构建相应的scsi命令结构。由于协议不断的发展,SCSI标准中有4种不同规格的写读操作称作:read(10)、read(12)、read(16)、read(32)和write(10)、write(12)、write(16)、write(32)。括号中的数字与命令描述块的大小,如read(10)的命令描述符为10个字节。10、12、16这三种的命令的区别是表求LBA所占用的空间不同,分别为4、6、8个字节。read(32)与write(32)主要是用于端到端T10 type2数据验证时使用。
理解了上述区别就比较好理解操作的构建过程了。
- 如果需要支持T10 type2数据验证,使用read(32)、write(32)
- 否则根据硬盘的最大扇区数分析,尽可能使用最小的命令描述块(CDB)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/217371
推荐阅读
  

闽ICP备14008679号