深入浅出SCSI子系统(六)SCSI 磁盘驱动
目录
深入浅出SCSI子系统(二)SCSI 磁盘驱动
Linux SCSI子系统实现了各种类型的高层驱动,如SCSI磁盘驱动、SCSI磁带驱动和SCSI CDROM驱动等。SCSI磁盘驱动对应的文件sd.c
SCSI总线类型scsi_bus_type SCSI设备和SCSI驱动 -->sd_probe()
在SCSI设备被发现,或者SCSI(高层)驱动被加载时,会在SCSI总线类型上和驱动链表中驱动,或者设备链表中的设备尝试绑定。在SCSI总线类型(scsi_bus_type,文件drivers/scsi/scsi_sysfs.c)中定义的match回调函数将首先被调用。
1.SCSI磁盘驱动重要结构体:
(1)SCSI磁盘结构体scsi_disk
这个结构中看到了一个指向通用磁盘描述符的指针,即disk域,看到了指向SCSI设备描述符的指针,即device域,还有一个指向SCSI驱动描述符的指针,即drives域。
在Linux中,用三个结构来共同表示一个SCSI磁盘,每个结构反映了SCSI磁盘的某方面属性:
• scsi_device:给出了作为SCSI设备方面的属性;
• scsi_disk:给出了作为SCSI磁盘方面的属性;
• gendisk:给出了作为通用磁盘方面的属性。
(2)SCSI驱动结构体 scsi_driver
(3)SCSI磁盘驱动实例为sd_template,定义在文件drivers/scsi/sd.c
PCI子系统和SCSI子系统的工作模式:
(1)PCI子系统的工作模式:PCI核心注册PCI总线类型,扫描PCI总线得到所有的PCI设备。PCI HBA驱动注册PCI设备驱动,逐个和PCI设备根据设备ID匹配;
(2)SCSI子系统的工作模式:SCSI中间层注册SCSI总线类型,SCSI较高层注册SCSI设备驱动,SCSI较低层(SCSI HBA驱动)则负责扫描SCSI总线得到所有的SCSI设备。SCSI设备将和SCSI设备驱动的匹配是根据SCSI INQUIRY响应中的SCSI设备类型域。
2. 同步执行部分
sd_probe函数是SCSI磁盘驱动的实际入口。它在驱动初始化期间,以及有新的SCSI设备被挂载到系统中时被调用。对于每个出现的SCSI设备(不仅仅是磁盘)都会调用一次。这个函数被从SCSI中间层调用,它在给定的<host, channel, id, lun>和新的设备名(如/dev/sda)之间建立映射。更精确地说,在这里选定了块设备的主设备编号和次设备编号。
static int sd_probe(struct device *dev):这个函数具有唯一的参数,即驱动模型中的设备描述符指针。这个指针实际上指向SCSI设备的内嵌通用设备,即sdev_gendev域,因为SCSI设备是通过内嵌通用设备链入SCSI总线类型(scsi_bus_type)的设备链表的。若成功,这个函数返回0;否则返回错误码。
static int sd_probe(struct device *dev)
{
1.struct scsi_device *sdp = to_scsi_device(dev);//获取scsi磁盘设备描述符
2.struct scsi_disk *sdkp;
sdkp = kzalloc(sizeof(*sdkp), GFP_KERNEL);//分配scsi_disk磁盘描述符
3.struct gendisk *gd;
gd = alloc_disk(SD_MINORS);//分配gendisk。gendisk描述符的分配相对复杂,需要处理分区,因此调用专门的alloc_disk函数。对于SCSI磁盘,系统固定可以有15个分区,加上作为整体使用的磁盘,一共是SD_MINORS(16)个,作为参数传入。
4.error = ida_get_new(&sd_index_ida, &index);
error = sd_format_disk_name(“sd”, index, gd->disk_name, DISK_NAME_LEN);//为SCSI磁盘分配设备名,设备名为sda-sdz,sdaa-sdzz, sdaaa-sdzzz
5.sdkp->device = sdp;
sdkp->driver = &sd_template;
sdkp->disk = gd;//构建SCSI磁盘各种结构关系中的一部分,将scsi_disk的device、driver及disk域分别指向scsi_disk、scsi_driver及gendisk描述符。
6.device_initialize(&sdkp->dev);//初始化scsi磁盘内嵌device
sdkp->dev.parent = dev;
sdkp->dev.class = &sd_disk_class;
dev_set_name(&sdkp->dev, "%s", dev_name(dev));
device_add(&sdkp->dev)//为scsi磁盘在sysfs中创建目录
/*SCSI磁盘在sysfs文件系统中创建目录。由于SCSI磁盘的内嵌设备有parent(为SCSI设备的内嵌通用对象),有class(为sd_disk_class),且parent没有class,该目录会在SCSI设备目录中创建在以类名(scsk_disk)为名字的子目录下,目录名字被设置成SCSI设备名。举例说明,/sys/devices/pci0000:00/0000:00:10.0/host2/target2:0:0/2:0:0:0/scsi_disk/2:0:0:0。*/
}
下图:SCSI设备及其派生的SCSI磁盘之间的驱动模型设备关系
3.异步执行部分
sd_probe_async函数解析:
static void sd_probe_async(void *data)
{
1.struct scsi_device *sdp;
struct scsi_disk *sdkp = data;
struct gendisk *gd;
sdp = sdkp->device;
gd = sdkp->disk;
index = sdkp->index;
dev = &sdp->sdev_gendev;//驱动模型中的设备描述符
//通过已存在的关系,找到指向SCSI设备描述符的指针以及指向通用磁盘描述符的指针。
2.
gd->fops = &sd_fops;//块设备操作表指针指向sd_fops
gd->private_data = &sdkp->driver;//private_data域指向scsi_disk结构中driver域的地址
gd->queue = sdkp->device->request_queue;//queue指向SCSI设备描述符的请求队列
3.sd_revalidate_disk(gd); //该函数的第一次调用,在add_disk之前进行
(1)sd_spinup_disk(sdkp);//向scsi磁盘发送命令,让磁盘转动起来
(2)sd_read_capacity(sdkp, buffer); //读取磁盘容量
(3)sd_read_block_limits(sdkp);
sd_read_block_characteristics(sdkp);
(4)sd_read_write_protect_flag(sdkp, buffer);//读取磁盘写保护标志
(5)sd_read_cache_type(sdkp, buffer);//读取磁盘缓存类型
(6)sd_read_app_tag_own(sdkp, buffer)//读取磁盘ATO位
(7)sdkp->first_scan = 0;//此标志是判断是否函数第一次调用
(8)blk_queue_ordered(sdkp->disk->queue, ordered, sd_prepare_flush);//根据SCSI磁盘特性确定IO的屏障能力
(9)set_capacity(disk, sdkp->capacity);//根据INQUERY命令查询到的磁盘容量设置
4.blk_queue_prep_rq(sdp->request_queue, sd_prep_fn);//将scsi请求队列的prep_rq_fn设置为sd_prep_fn此后不仅可以处理来自scsi层的请求,也可以处理来自通用io层的请求,可以为bio请求构建scsi命令
5.gd->driverfs_dev = &sdp->sdev_gendev;//sysfs文件系统中联系通用磁盘和scsi设备,每个SCSI device对应一个通用磁盘
6.dev_set_drvdata(dev, sdkp);//将scsi磁盘描述符指针赋值给sdp->sdev_gendev->drv_data域,sdp为scsi_device类型指针scsi磁盘驱动的其它回调函数(如sd_rescan, sd_shutdown等)都需要拿到scsi磁盘描述符
7.add_disk(gd);//将通用磁盘添加到系统
8.SCSI磁盘探测已经完成,我们在输出消息后返回。
}
3 重新校验磁盘
在SCSI磁盘探测过程中,两次调用sd_revalidate_disk。这是有意安排的,让在add_disk之前和之后两次调用sd_revalidate_disk。其原因在于向块I/O子系统确定注册的方式。
sd_revalidate_disk()
{
1.sd_spinup_disk()//向SCSI磁盘发送命令让磁盘转动起来
2.sd_read_capacity(sdkp, buffer);//读取磁盘容量
3.sd_read_write_protect_flag()//读取SCSI磁盘的写保护标志
4.sd_read_cache_type()//读取SCSI磁盘的缓存类型
5.sd_read_app_tag_own()//读取SCSI磁盘的ATO位,即确定操作系统释放可以访问它的DIF三元组中的应用程序标签
//上述函数都是通过向SCSI磁盘发送命令,并查询返回的数据格式。
6.sdkp->first_scan = 0;//通过first_scan域判断sd_revalidate_disk函数及它调用到的上述函数到底是在第一次运行时(add_disk之前),还是在第二次运行时(add_disk之后)。
7.blk_queue_ordered(sdkp->disk->queue, ordered, sd_prepare_flush);//根据SCSI磁盘特性确定IO的屏障能力 将冲刷磁盘缓存的回调函数设置为sd_prepare_flush:最终调用SCSI命令SYNCHRONIZE_CACHE来实现将磁盘缓冲写到介质上
8.set_capacity(disk, sdkp->capacity);//根据INQUERY命令查询到的磁盘容量,设置通用磁盘结构中的capacity域设置
}
4 让磁盘转起来
sd_spinup_disk函数尝试等到SCSI磁盘已经准备好,必要时使磁盘转起来。前者发送TEST UNIT READY命令,后者发送START UNIT命令。
两层循环,外层循环的目的为让SCSI磁盘“准备好”给予足够的等待时间。
内层循环发送TEST_UNIT_READY命令,如果已知磁盘介质不存在,情况就简单了,可以直接返回。否则给它三次机会以忽略UnitAttention。
内层循环退出,根据其结果,有三种情况可以直接退出外层循环:(1)TEST_UNIT_READY成功,这是显而易见的;其他情况都表示设备还没有准备好,需要在外层循环进一步处理。所以:(2)因为处理必须给予感测数据,但我们拿不到有效的感测数据(3)所谓处理,是发送START_STOP命令,等待磁盘转动起来,但SCSI设备却指定了no_start_on_add域,不希望发送自动启动命令。
外层循环的剩余代码都是处理设备未准备好的情况的,又有以下三个分支:
• 如果感测数据为NOT_READY,向SCSI磁盘发送START UNIT命令,在此之前需要排除三种情况,这些都是通过附加感测码判断的:(1)需要手工介入,(2)磁盘正待机,(3)磁盘不可用。
• 如果感测数据为UNIT_ATTENTION,并且附加感测码为0x28,说明可能是USB闪存设备的情况。它和上一种情况一样,都在spintime为0的情况下设置它,开始或继续外层循环。
• 其他的感测数据都是不支持的,退出外层循环。退出外层循环后,根据结果可能输出一些内核消息,本函数即返回到调用者。
更多推荐
所有评论(0)