1. 前言
本专题我们开始学习SCSI子系统的相关内容。本专题主要参考了《存储技术原理分析》、ULA、ULK的相关内容。本专题主要以硬件UFS为例,记录SCSI子系统的框架流程。
驱动通过消息传递机制获得的SCSI设备最终都被添加到系统中。实际上,它被挂接在SCSI总线类型(scsi_bus_type),实际代码参考分配SCSI设备描述符时,调用的scsi_alloc_sdev->scsi_sysfs_device_initialize函数。
接下来,要真正用上这些设备,就需要高层驱动,这些高层驱动挂载SCSI总线类型,在被加载时,扫描并认领总线类型上未被绑定的SCSI设备。
SCSI设备属于sdev_class类,Linux SCSI子系统实现了sg(SCSI通用接口)和ses(SCSI磁盘柜服务)接口,注册到这个类下。他们都相当于类特定的高层驱动。
上一节在scsi设备探测时讲到scsi_sysfs_add_sdev(sdev)执行此函数将会添加scsi_device,引发devcice_add(scsi_device->device),而sd_init->scsi_register_driver时会执行driver_add,匹配成功将执行 sd_probe,从此处可以看出每一个探测到的scsi_devei也就是每个逻辑单元LU都会执行一次sd_probe,本文就说明一下sd_probe的执行过程。
kernel版本:5.10
平台:arm64
注:
为方便阅读,正文标题采用分级结构标识,每一级用一个"-“表示,如:两级为”|- -", 三级为”|- - -“
2. SCSI驱动模型
- scsi_device
给出了作为SCSI设备方面的属性 - scsi_disk
给出了作为SCSI磁盘方面的属性,它作为SCSI设备的代表与上层IO关联 - gendisk
给出了作为通用磁盘方面的属性
这三个结构之间相互联系,通过任何一个结构,可以找到另外两个结构。
在SCSI设备被发现(scsi_scan_host),或者SCSI(高层)驱动被加载(init_sd->scsi_register_driver)时,会在SCSI总线类型上和驱动链表中驱动或者设备链表中的设备尝试绑定。在SCSI总线类型(scsi_bus_type,文件drivers/scsi/scsi_sysfs.c)中定义的match回调函数将首先被调用,这个匹配函数就是scsi_bus_match。SCSI总线类型将设备和驱动的匹配留给驱动来做。
如果总线类型的match回调函数匹配成功,接下来会调用总线类型或驱动的probe回调函数。对于SCSI总线没有定义probe回调,因此会调用scsi_driver的probe,也就是sd_probe。
因此,总结SCSI系统的工作模式:
SCSI中层注册SCSI总线类型; SCSI较高层注册SCSI设备驱动;SCSI较低层(SCSI HBA驱动)则负责扫描SCSI总线得到所有的SCSI设备,SCSI设备将和SCSI设备驱动的匹配是根据SCSI INQUIRY响应中的SCSI设备类型域。
3. init_sd
static int __init init_sd(void)
|--for (i = 0; i < SD_MAJORS; i++)
| if (register_blkdev(sd_major(i), "sd") != 0)
| continue;
| majors++;
| blk_register_region(sd_major(i), SD_MINORS, NULL,sd_default_probe, NULL, NULL);
| //注册scsi_disk类
|--class_register(&sd_disk_class);
|--sd_cdb_cache = kmem_cache_create("sd_ext_cdb", SD_EXT_CDB_SIZE,0, 0, NULL);
|--sd_cdb_pool = mempool_create_slab_pool(SD_MEMPOOL_SIZE, sd_cdb_cache);
|--sd_page_pool = mempool_create_page_pool(SD_MEMPOOL_SIZE, 0);
|--scsi_register_driver(&sd_template.gendrv);
scsi_register_driver会触发执行driver_register
4. sd_probe
<drivers/scsi/sd.c>
static int sd_probe(struct device *dev)
|--struct scsi_device *sdp = to_scsi_device(dev);
| struct scsi_disk *sdkp;
| struct gendisk *gd;
|--sdkp = kzalloc(sizeof(*sdkp), GFP_KERNEL);
|--gd = alloc_disk(SD_MINORS);
|--index = ida_alloc(&sd_index_ida, GFP_KERNEL);
| //根据index来确定磁盘名字"sdx"
|--sd_format_disk_name("sd", index, gd->disk_name, DISK_NAME_LEN);
|--sdkp->device = sdp;
| sdkp->driver = &sd_template;
| sdkp->disk = gd;
| sdkp->index = index;
| sdkp->max_retries = SD_MAX_RETRIES;
| if (!sdp->request_queue->rq_timeout)
| if (sdp->type != TYPE_MOD)
| blk_queue_rq_timeout(sdp->request_queue, SD_TIMEOUT);
| else
| blk_queue_rq_timeout(sdp->request_queue, SD_MOD_TIMEOUT);
|--device_initialize(&sdkp->dev);
|--sdkp->dev.parent = dev;
|--sdkp->dev.class = &sd_disk_class;
|--dev_set_name(&sdkp->dev, "%s", dev_name(dev));
|--device_add(&sdkp->dev);
|--dev_set_drvdata(dev, sdkp);
|--gd->major = sd_major((index & 0xf0) >> 4);
| gd->first_minor = ((index & 0xf) << 4) | (index & 0xfff00);
| gd->major = sd_major((index & 0xf0) >> 4);
| gd->first_minor = ((index & 0xf) << 4) | (index & 0xfff00);
| gd->fops = &sd_fops;
| gd->private_data = &sdkp->driver;
| gd->queue = sdkp->device->request_queue;
| /* defaults, until the device tells us otherwise */
|--sdp->sector_size = 512;
| sdkp->capacity = 0;
| sdkp->media_present = 1;
| sdkp->write_prot = 0;
| sdkp->cache_override = 0;
| sdkp->WCE = 0;
| sdkp->RCD = 0;
| sdkp->ATO = 0;
| sdkp->first_scan = 1;
| sdkp->max_medium_access_timeouts = SD_MAX_MEDIUM_TIMEOUTS;
|--sd_revalidate_disk(gd);//第一次调用
|--gd->flags = GENHD_FL_EXT_DEVT;
| if (sdp->removable)
| gd->flags |= GENHD_FL_REMOVABLE;
| gd->events |= DISK_EVENT_MEDIA_CHANGE;
| gd->event_flags = DISK_EVENT_FLAG_POLL | DISK_EVENT_FLAG_UEVENT;
|--blk_pm_runtime_init(sdp->request_queue, dev);
|--device_add_disk(dev, gd, NULL);
|--sd_revalidate_disk(gd);//第二次调用
经前述init_sd会触发执行scsi driver register,而在设备探测时scsi_scan_host->scsi_probe_and_add_lun->scsi_add_lun->scsi_sysfs_add_sdev时触发执行了scsi device register,因此最终经过match成功后会执行sd_probe。sd_probe是SCSI磁盘驱动的实际入口,它在驱动初始化期间,以及有新的SCSI设备被挂载到系统中时(scsi_device注册到scsi bus时)被调用。每个出现的SCSI设备都会调用一次,它在给定的<host,channel,id,lun>和新的设备名(如/dev/sda)之间建立映射。更准确的说,在这里选定了块设备的主设备号和次设备号。唯一的参数是scsi_device的内嵌通用设备sdev_gendev,因为SCSI设备是通过内嵌通用设备链入SCSI总线类型的设备链表的。
-
过滤掉本驱动不能处理的非磁盘设备,因为总线把权利交给驱动,因此有可能传入的不是SCSI设备,需要过滤掉
-
sdkp = kzalloc(sizeof(*sdkp), GFP_KERNEL);
分配scsi_disk描述符,参考通用block基础学习笔记 - 2. 添加磁盘到系统 -
gd = alloc_disk(SD_MINORS);
分配gendisk,需要处理分区,对于SCSI磁盘可以固定有15个分区,加上作为整体使用的分区有16个 -
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
关于主次设备号的生成,TODO -
构建scsi_device, scsi_disk, gendisk的关系
-
blk_queue_rq_timeout
设置请求队列超时时间 -
device_initialize(&sdkp->dev);
初始化scsi磁盘内嵌device
device_add(&sdkp->dev)
为scsi磁盘在sysfs中创建目录 -
对通用磁盘描述符gendisk进行一些初始化
-
找到通用磁盘描述符指针和SCSI设备描述符指针,对SCSI设备描述符、SCSI磁盘描述符进行初始化,设置为默认值,随后会通过命令从SCSI设备获取信息修改
-
sd_revalidate_disk(gd); 该函数的第一次调用,在device_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命令查询到的磁盘容量设置 -
blk_queue_prep_rq(sdp->request_queue, sd_prep_fn)
将scsi请求队列的prep_rq_fn设置为sd_prep_fn
此后不仅可以处理来自scsi层的请求,也可以处理来自通用io层的请求,可以为bio请求构建scsi命令 -
gd->driverfs_dev = &sdp->sdev_gendev;
联系通用磁盘和scsi设备,每个SCSI device对应一个通用磁盘 -
dev_set_drvdata(&sdp->sdev_gendev, sdkp);
将scsi磁盘描述符指针赋值给sdp->sdev_gendev->drv_data域,sdp为scsi_device类型指针
scsi磁盘驱动的其它回调函数(如sd_rescan, sd_shutdown等)都需要拿到scsi磁盘描述符 -
device_add_disk:将通用磁盘添加到系统
注:add_disk是对device_add_disk的封装,参考通用block基础学习笔记 - 2. 添加磁盘到系统 -
sd_revalidate_disk(gd); 第二次调用,在add_disk之后
参考文档
存储技术原理分析