LINUX 内核中 SCSI 子系统由 SCSI 上层,中间层和底层驱动模块 [1] 三部分组成,主要负责管理 SCSI 资源和处理其他子系统,如文件系统,提交到 SCSI 子系统中的 IO 请求。因此,理解 SCSI 子系统的 IO 处理机制对理解整个 SCSI 子系统就显的十分重要,同时也有助于理解整个 LINUX 内核的 IO 处理机制。本文从 SCSI 设备访问请求的提交,SCSI 子系统对访问请求的处理和 SCSI 子系统错误处理三个方面,阐述了 SCSI 子系统的 IO 处理机制。
SCSI设备访问请求的提交
SCSI 设备访问请求的提交分为两个步骤:用户空间提交访问请求到通用块层以及通用块层提交块访问请求到 SCSI 子系统。
用户空间提交访问请求到通用块层
在 LINUX 用户空间,有三种方式提交对 SCSI 设备的访问请求到通用块层:
通过文件系统提供的文件访问接口进行访问。对建立在 SCSI 设备上的 LINUX 文件系统中的文件读写操作,就属于这种访问方式;RAW 设备访问方式。这种访问方式比较常见的应用就是dd命令。 RAW 设备访问方式和通过文件系统提供的文件访问接口进行访问的最大区别在于前者对 SCSI 设备直接进行线性地址访问,不需要由文件系统进行地址映射;SCSI PASSTHROUGH 方式。通过 LINUX 提供的 SG 进行访问,就属于这种方式,用户可以直接发 CDB[2] 命令给 SCSI 设备。所以,通过该接口,用户可以做一些 SCSI 管理操作,如 SES 管理等。
图 1 显示了 LINUX 内核对于三种请求提交方式的处理过程。
图 1. LINUX 内核处理三种访问请求的方式
经由文件系统或 RAW 设备方式提交的请求,会通过底层块设备访问层(ll_rw_block()),由其生成块 IO 请求(BIO),并提交给通用块层 [3] ;而通过 SG 接口提交的访问请求,会调用 SCSI 中间层提供的接口,将请求直接交由通用块层进行处理。
通用块层提交块访问请求到SCSI子系统
为什么要通过通用块层呢?这是因为首先通用块层会根据磁盘访问的特性对请求进行优化操作;其次,通用块层提供了调度功能,能够对请求进行调度;再次,通用块层可扩展的结构,使各种设备的块驱动都能比较容易的和其集成。
当请求提交到通用块层后,通用块层需要完成准备,调度并交付块访问请求给 SCSI 中间层的操作。块访问请求可以理解为描述了块访问区域,访问方式和关联的 BIO 的请求,在内核中用 'struct request'结构表示。块设备会有对应的块访问请求设备队列,用于记录需要该设备处理的访问请求,新生成的块访问请求会被加入到对应设备的块访问请求队列中。 SCSI 子系统对 IO 的处理,实际上是处理块访问请求队列上的块访问请求。
通用块层提供了两种方式调度处理块访问请求队列:直接调度和通过 LINUX 内核工作队列机制调度执行。两种方式,最后都会调用块访问请求队列处理函数进行处理,而 SCSI 设备在初始化时会向通用块层注册 SCSI 子系统定义的块访问请求队列处理函数。清单 1[4] 显示了这个过程。这样当通用块层处理 SCSI 设备的块访问请求队列时,调用的就是 SCSI 中间层定义的这些处理函数。通过这种方式,通用块层就将块访问请求的处理交给了 SCSI 子系统。
清单 1. 处理函数
struct request_queue *scsi_alloc_queue(struct scsi_device *sdev)
{……
q = blk_init_queue(scsi_request_fn, NULL);
//request generate block layer allocate a request queue
……
blk_queue_prep_rq(q, scsi_prep_fn); //Prepare a scsi request blk_queue_max_hw_segments(q, shost->sg_tablesize);
//define sg table size
……
blk_queue_softirq_done(q, scsi_softirq_done);
}
SCSI子系统处理块访问请求
当 SCSI 子系统的请求队列处理函数被通用块层调用后,SCSI 中间层会根据块访问请求的内容,生成、初始并提交 SCSI 命令 (struct scsi_cmd) 到 SCSI TARGET 端。
SCSI命令初始化和提交
SCSI 命令记录了命令描述块 (CDB),感测数据缓存 (SENSE BUFFER),IO 超时时间等 SCSI 相关的信息和 SCSI 子系统处理命令需要的一些其他信息,如回调函数等。清单 2 显示了这个命令的主要结构。
清单 2. 主要结构
struct scsi_cmnd {
……
void (*done) (struct scsi_cmnd *); /* Mid-level done function */
……
int retries; /*retried time*/
int timeout_per_command; /*timeout define*/
……
enum dma_data_direction sc_data_direction; /*data transfer direction*/
……
unsigned char cmnd[MAX_COMMAND_SIZE]; /*cdb*/
void *request_buffer; /* Actual requested buffer */
struct request *request; /* The command we are working on */
……
unsigned char sense_buffer[SCSI_SENSE_BUFFERSIZE];
/* obtained by REQUEST SENSE when
* CHECK CONDITION is received on original
* command (auto-sense) */
/* Low-level done function - can be used by */
/*low-level driver to point to completion function. */
void (*scsi_done) (struct scsi_cmnd *);
……
};
初始化的过程首先按照电梯调度算法,从块设备的请求队列上取出一个块访问请求,根据块访问请求的信息,定义 SCSI 命令中数据传输的方向,长度和地址。其次,定义 CDB,SCSI 中间层的回调函数等。
在完成初始化后,SCSI 中间层通过调用scsi_host_template[5]结构中定义的queuecommand函数将 SCSI 命令提交给 SCSI 底层驱动部分。queuecommand函数,是一个 SCSI 命令队列处理函数,在 SCSI 底层驱动中,定义了queuecommand函数的具体实现。因此,SCSI 中间层,调用queuecommand函数实际上就是调用了底层驱动定义的queuecommand函数的处理实体,将 SCSI 命令提交给了各个厂家定义的 SCSI 底层驱动进行处理。这个过程和通用块设备层调用 SCSI 中间层的处理函数进行块请求处理的机制很相似,这也体现了 LINUX 内核代码具有很好的扩展性。底层驱动接受到请求后,就要开始处理 SCSI 命令了,这一层和硬件关系紧密,所以这块代码一般都是由各个厂家自己实现。基本流程可概括为:从底层驱动维护的队列中,取出一个 SCSI 命令,封装成厂家自定义的请求格式,然后采用 DMA 或者其他方式,将请求提交给 SCSI TARGET 端,由 SCSI TARGET 端对请求处理,并返回执行结果给 SCSI 底层驱动层。
SCSI命令执行结果的处理
当 SCSI 底层驱动接受到 SCSI TARGET 端返回的命令执行结果后,SCSI 子系统主要通过两次回调过程完成对命令执行结果的处理。 SCSI 底层驱动在接受到 SCSI TARGET 端返回的命令执行结果后,会调用 SCSI 中间层定义的回调函数,将处理结果交付给 SCSI 中间层进行处理,这是第一次回调过程。 SCSI 中间层处理完成后,将调用 SCSI 上层定义的回调函数,结束 IO 在整个 SCSI 子系统中的处理,这为第二次回调过程。
第一次回调:
SCSI 中间层在调用queuecommand函数将 SCSI 命令提交给 SCSI 底层驱动的同时,也将回调函数指针传给了 SCSI 底层驱动。底层驱动接受到 SCSI TARGET 端返回的命令执行结果后,会调用该回调函数,产生一个中断号为 BLOCK_SOFTIRQ 的软中断进行第一次回调处理。在这次回调处理过程中,SCSI 中间层首先会根据 SCSI 底层驱动处理的结果判断请求处理是否成功。处理成功,并不意味着处理没有错误,而是返回的信息,能够让 SCSI 中间层很明确的知道,对于这个命令,中间层已经没有必要继续进行处理了。所以,对于处理成功的 SCSI 命令,SCSI 中间层会调用第二次回调函数进入到第二次回调过程。清单 3 显示了 SCSI 中间层定义的该软中断的处理函数。
清单 3. 该软中断的处理函数
static void scsi_softirq_done(struct request *rq)
{
……
disposition = scsi_decide_disposition(cmd);
……
switch (disposition) {
case SUCCESS:
scsi_finish_command(cmd);
//enter to second callback process
break;
case NEEDS_RETRY:
scsi_retry_command(cmd);
break;
case ADD_TO_MLQUEUE:
scsi_queue_insert(cmd, SCSI_MLQUEUE_DEVICE_BUSY);
break;
default:
if (!scsi_eh_scmd_add(cmd, 0))
scsi_finish_command(cmd);
}
}
本文作者:未知