3 scsi_debug模块实现详解
3.1 模块功能简介
scsi_debug是由Eric Youngdale和Douglas Gilbert编写的scsi调试驱动模块,相关链接为。 目前已经合并入linux kernel 2.6内核.与图8中QLogic等物理设备的驱动类似,scsi_debug虚拟出来的设备可以被device mapper和file system使用.与物理设备不同的是由于设备本身是虚拟的,因此可以在此虚拟设备上添加任意多的功能,目前最主要的功能是scsi_debug可以模拟出各种出错情况借以测试scsi mid layer, upper layer和应用对于错误情况的处理是否完善。scsi_debug模块之所以能够被主机识别和使用是因为代码中有很大的篇幅是用来处理SCSI标准中的各种命令,填充命令返回结果,并且虚拟出来了一个LLDD(Lower Level Device Driver)来与mid layer衔接,因此下面的介绍主要分为两部分进行:首先介绍scsi_debug与mid layer衔接和一些必要的工作函数,随后详细介绍scsi_debug对于各种SCSI命令的模拟和处理。scsi_debug主要实现了SPC和SBC中定义的scsi命令,因此对于scsi disk的虚拟比较完善,虽然scsi_debug的ptype选项可以指定虚拟的设备类型,但是由于对于相应设备类别的标准并未实现,因此无法使用,例如当指定ptype=1加载scsi_debug模块后,lsscsi -g可以看到对应的设备:
[casualfish@tank ~]$ lsscsi -g
[0:0:0:0] disk ATA HITACHI HTS54161 SB4I /dev/sda /dev/sg0
[1:0:0:0] cd/dvd HL-DT-ST RW/DVD GCC-T10N 1.04 /dev/sr0 /dev/sg1
[2:0:0:0] tape Linux scsi_debug 0004 /dev/st0 /dev/sg2
但如果使用tar归档则会报错:
[casualfish@tank ~]$ sudo tar -cvf /dev/st0 scsi_debug.note
scsi_debug.note
tar: /dev/st0: Cannot close: Input/output error
tar: Exiting with failure status due to previous errors
同时/var/log/messages中输出如下:
Nov 18 10:18:35 tank kernel: st0: Sense Key : Illegal Request [current]
Nov 18 10:18:35 tank kernel: st0: Add. Sense: Invalid command operation code
Nov 18 10:18:35 tank kernel: st0: Sense Key : Illegal Request [current]
Nov 18 10:18:35 tank kernel: st0: Add. Sense: Invalid command operation code
Nov 18 10:18:35 tank kernel: st0: Error on write filemark.
因此此处的分析主要针对scsi_debug对于SPC和SBC的实现,使用的scsi_debug版本为
#define SCSI_DEBUG_VERSION "1.82"
static const char * scsi_debug_version_date = "20100324";
3.2 与mid layer及外部的衔接
scsi_debug定义了sdebug_driver_template向kernel注册了一个LLDD,定义如下:
static struct scsi_host_template sdebug_driver_template = {
.proc_info = scsi_debug_proc_info,
.proc_name = sdebug_proc_name,
.name = "SCSI DEBUG",
.info = scsi_debug_info,
.slave_alloc = scsi_debug_slave_alloc,
.slave_configure = scsi_debug_slave_configure,
.slave_destroy = scsi_debug_slave_destroy,
.ioctl = scsi_debug_ioctl,
.queuecommand = scsi_debug_queuecommand,
.eh_abort_handler = scsi_debug_abort,
.eh_bus_reset_handler = scsi_debug_bus_reset,
.eh_device_reset_handler = scsi_debug_device_reset,
.eh_host_reset_handler = scsi_debug_host_reset,
.bios_param = scsi_debug_biosparam,
.can_queue = SCSI_DEBUG_CANQUEUE,
.this_id = 7,
.sg_tablesize = 256,
.cmd_per_lun = 16,
.max_sectors = 0xffff,
.use_clustering = DISABLE_CLUSTERING,
.module = THIS_MODULE,
};
可以看到定义了一些自己的函数实现,struct scsi_host_template定义在include/scsi/scsi_host.h中,里面规定了scsi_host_template哪些是必须实现的函数,哪些是可选的,哪些函数已经废除,sdebug_driver_template只是实现了其中的部分函数。下文将以sdbug_driver_template为例,将展开对于这个变量实现函数的分析,为了节省篇幅,分析过程中仅列出关键的代码,详细的代码请读者自己查阅源码。
-
scsi_debug_proc_info、proc_name
主要功能是向用户空间导出一些驱动和设备当前的一些配置信息,关键部分代码如下:
pos = len = sprintf(buffer, "scsi_debug adapter driver, version "
"%s [%s]\n"
"num_tgts=%d, shared (ram) size=%d MB, opts=0x%x, "
"every_nth=%d(curr:%d)\n"
"delay=%d, max_luns=%d, scsi_level=%d\n"
"sector_size=%d bytes, cylinders=%d, heads=%d, sectors=%d\n"
"number of aborts=%d, device_reset=%d, bus_resets=%d, "
"host_resets=%d\ndix_reads=%d dix_writes=%d dif_errors=%d\n",
SCSI_DEBUG_VERSION, scsi_debug_version_date, scsi_debug_num_tgts,
scsi_debug_dev_size_mb, scsi_debug_opts, scsi_debug_every_nth,
scsi_debug_cmnd_count, scsi_debug_delay,
scsi_debug_max_luns, scsi_debug_scsi_level,
scsi_debug_sector_size, sdebug_cylinders_per, sdebug_heads,
sdebug_sectors_per, num_aborts, num_dev_resets, num_bus_resets,
num_host_resets, dix_reads, dix_writes, dif_errors);
-
scsi_debug_slave_alloc
在SCSI mid layer试图扫描新设备之前,会调用driver实现的slave_alloc,如果driver要做任何资源申请及初始化工作,则在此函数中进行,函数返回0表示成功,非0表示失败。
scsi_debug_slave_alloc使用include/linux/blkdev.h中的 queue_flag_set_unlocked函数为scsi device设置了QUEUE_FLAG_BIDI标志位,如下:
queue_flag_set_unlocked(QUEUE_FLAG_BIDI, sdp->request_queue);
return 0;
函数始终返回0,因此会调用scsi_debug_slave_configure。
-
scsi_debug_slave_configure
函数返回0表示成功,非0表示失败,失败的话设备会被标识为offline,如果设备对于INQUIRY返回OK的话,就会调用driver注册的这个函数,在这个函数中必须设置设备的queue_depth,如下代码,也可以按需实现一些其他的功能。
if (sdp->host->cmd_per_lun)
scsi_adjust_queue_depth(sdp, SDEBUG_TAGGED_QUEUING,
sdp->host->cmd_per_lun);
其中,scsi_adjust_queue_depth的原型如下:
void scsi_adjust_queue_depth(struct scsi_device *sdev, int tagged, int tags)
当为tagged设置为0时,表示这个设备是untagged的,scsi_debug.c中有如下定义:
#define SDEBUG_TAGGED_QUEUING 0 /* 0 | MSG_SIMPLE_TAG | MSG_ORDERED_TAG */
即scsi_debug虚拟的设备是untagged的。
该函数中也设置了max_cmd_len和max_segment_size,同时如果设置了scsi_debug_no_uld项的话则设置传入scsi device的no_uld_attach位。
-
scsi_debug_slave_destroy
在此函数中释放为slave申请的资源,scsi_debug_slave_destroy实现为重用这个scsi_device:
if (devip) {
/* make this slot available for re-use */
devip->used = 0;
sdp->hostdata = NULL;
}
-
scsi_debug_ioctl
对于物理设备,ioctl可以控制本设备的参数,但对于scsi_debug没有需要设置的物理参数,故实现为空,如下:
static int scsi_debug_ioctl(struct scsi_device *dev, int cmd, void __user *arg)
{
if (SCSI_DEBUG_OPT_NOISE & scsi_debug_opts) {
printk(KERN_INFO "scsi_debug: ioctl: cmd=0x%x\n", cmd);
}
return -EINVAL;
/* return -ENOTTY; // correct return but upsets fdisk */
}
-
scsi_debug_queuecommand
此函数是LLDD中最重要的工作函数,当工作完成时执行传入的scsi_cmd中done回调函数。函数返回0表示本请求可以被接受处理,如果不能处理则不需要调用done函数,这种情况下有两种返回值,为了保持兼容性,其他的非零返回值等同于SCSI_MLQUEUE_HOST_BUSY处理。
SCSI_MLQUEUE_DEVICE_BUSY:设备当前暂时处于阻塞状态,但允许其他被此driver操作的设备处理请求。
SCSI_MLQUEUE_HOST_BUSY:设备当前暂时处于阻塞状态,本driver操作的所有设备均不处理请求。
“暂时”的评判标准可以针对系统的当前IO压力而定。scsi_debug中实现此功能的函数为scsi_debug_queuecommand_lck函数,通过scsi_host.h中定义的DEF_SCSI_QCMD宏完成转化,定义如下:
#define DEF_SCSI_QCMD(func_name) \
int func_name(struct Scsi_Host *shost, struct scsi_cmnd *cmd) \
{ \
unsigned long irq_flags; \
int rc; \
spin_lock_irqsave(shost->host_lock, irq_flags); \
scsi_cmd_get_serial(shost, cmd); \
rc = func_name##_lck (cmd, cmd->scsi_done); \
spin_unlock_irqrestore(shost->host_lock, irq_flags); \
return rc; \
}
可见,就是在锁的保护下执行queuecommand函数。scsi_debug_queuecommand_lck函数定义较长,主要内容如下所示:
…
switch (*cmd) {
case INQUIRY: /* mandatory, ignore unit attention */
delay_override = 1;
errsts = resp_inquiry(SCpnt, target, devip);
break;
case REQUEST_SENSE: /* mandatory, ignore unit attention */
delay_override = 1;
errsts = resp_requests(SCpnt, devip);
break;
case REZERO_UNIT: /* actually this is REWIND for SSC */
case START_STOP:
errsts = resp_start_stop(SCpnt, devip);
break;
case ALLOW_MEDIUM_REMOVAL:
errsts = check_readiness(SCpnt, 1, devip);
if (errsts)
break;
if (SCSI_DEBUG_OPT_NOISE & scsi_debug_opts)
printk(KERN_INFO "scsi_debug: Medium removal %s\n",
…