1. 前言
本专题我们开始学习SCSI子系统的相关内容。本专题主要参考了《存储技术原理分析》、ULA、ULK的相关内容。本专题主要以硬件UFS为例,记录SCSI子系统的框架流程。
在系统启动过程中,扫描到SCSI主机适配器后,操作系统开始加载SCSI主机适配器驱动,即底层驱动。SCSI主机适配器驱动根据SCSI主机适配器模板分配SCSI主机适配器描述符,并添加到系统。在此之后需要通过SCSI主机适配器启动SCSI总线的扫描。
以UFS为例,UFS控制器不仅作为SCSI总线设备,同时也作为platform总线设备,它是通过ufshcd_pltfrm_init来执行Scsi_Host分配与添加,同时对于SCSI设备(即UFS device)的扫描是在ufshcd_async_scan完成。
对于ufs,执行路径为:
ufs_qcom_probe->ufshcd_pltfrm_init->ufshcd_init->async_schedule(ufshcd_async_scan, hba)->ufshcd_probe_hba->scsi_scan_host
kernel版本:5.10
平台:arm64
注:
为方便阅读,正文标题采用分级结构标识,每一级用一个"-“表示,如:两级为”|- -", 三级为”|- - -“
2.SCSI总线扫描方式
SCSI总线扫描的目的是通过协议特定或芯片特定的方式探测挂接在主机适配器后面的target和LUN,为他们在内存中构建相应的数据结构,将他们添加到系统中,不同的主机适配器可能实现不同的拓扑结构和设备添加机制,列举三种:
对于SPI协议,SCSI主机适配器驱动可以调用SCSI中间层提供的扫描算法完成SCSI总线设备的扫描,扫描过程描述如下:
注:
1.对于UFS设备来讲,SCSI低层驱动的queuecommand就是ufshcd_queuecommand
2.INQUIRY、REPORT_LUNS命令是SCSI命令,由它构建CDB,组成了UFS的UTP层UPIU的Transfer specific fields
SCSI中间层提供扫描SCSI总线的服务函数是scsi_scan_host,它一般在适配器平台驱动的probe中执行,它采用的就是上面描述的向各个<channel, id, lun>发送INQUIRY的命令方式
3.scsi_scan_host
void scsi_scan_host(struct Scsi_Host *shost)
|--data = scsi_prep_async_scan(shost);
| if (!data)
| do_scsi_scan_host(shost);
| scsi_autopm_put_host(shost);
| return;
|--async_schedule(do_scan_async, data);
|--do_scan_async(void *_data, async_cookie_t c)
|--struct async_scan_data *data = _data;
|--struct Scsi_Host *shost = data->shost;
|--do_scsi_scan_host(shost);
|--scsi_finish_async_scan(data);
对于ufs,执行路径为:
ufs_qcom_probe->ufshcd_pltfrm_init->ufshcd_init->async_schedule(ufshcd_async_scan, hba)->ufshcd_probe_hba->scsi_scan_host
scsi_scan_host对指定的scsi_host进行扫描,通过异步扫描host, channel, target, device, 最终将扫描到的lun加入到系统中
-
scsi_prep_async_scan:为异步扫描做准备,如果是同步扫描则不做任何准备
-
async_schedule(do_scan_async, data):异步执行do_scan_async->do_scsi_scan_host:
do_scsi_scan_host(shost);
|--if (shost->hostt->scan_finished)
| if (shost->hostt->scan_start)
| shost->hostt->scan_start(shost);
| else
| scsi_scan_host_selected(shost, SCAN_WILD_CARD, SCAN_WILD_CARD,SCAN_WILD_CARD, 0);
| |--if (!shost->async_scan)
| | scsi_complete_async_scans();
| |--if (scsi_host_scan_allowed(shost) && scsi_autopm_get_host(shost) == 0)
| scsi_scan_channel(shost, channel, id, lun, rescan);
| | //扫描所有可能的目标节点或特定ID的目标节点
| |--__scsi_scan_target(&shost->shost_gendev, channel,id, lun, rescan);
| | //分配目标节点,如果目标节点没有扫描到LU,后面会释放
| |--starget = scsi_alloc_target(parent, channel, id);
| | //对要求扫描所有LU的情况,则先扫描LU0
| |--res = scsi_probe_and_add_lun(starget, 0, &bflags, NULL, rescan, NULL);
| |--if (res == SCSI_SCAN_LUN_PRESENT || res == SCSI_SCAN_TARGET_PRESENT)
| | //通过对LU0(REPORT LUN)发命令查询已经实现的LU号,并对其进行扫描
| | if (scsi_report_lun_scan(starget, bflags, rescan) != 0)
| | //顺序扫描所有的LUN
| | scsi_sequential_lun_scan(starget, bflags,starget->scsi_level, rescan);
| | |--for (lun = 1; lun < max_dev_lun; ++lun)
| | //对指定编号的LU进行探测
| | scsi_probe_and_add_lun(starget, lun, NULL, NULL, rescan,NULL)
| |--scsi_autopm_put_target(starget);
| |--scsi_target_reap(starget);
| |--put_device(&starget->dev);
异步执行do_scan_async->do_scsi_scan_host:
-
如果host controller驱动中定义了scan_finished则由host底层驱动完成扫描,这一般是非标准的做法,标准的扫描过程一般通过中间层实现
. -
scsi_scan_host_selected
扫描选定的主机适配器,其中会调用scsi_scan_channel(shost, channel, id, lun,0) -
scsi_scan_channel
扫描channel对应ufs就是ufs总线数目,每条总线接一个ufs target,目前只实现一条总线,接一个ufs target,遍历对channel下的每个target执行__scsi_scan_target。__scsi_scan_target扫描每个target,执行scsi_probe_and_add_lun先扫描LUN0,如果扫描LUN0收到回应,则通过scsi_sequential_lun_scan进一步扫描所有的LUN,为lun创建scsi_device,期间会创建派发队列,并通过命令探测lun是否存在,如果存在则将lun加入到系统中
scsi_probe_and_add_lun(starget, lun, NULL, NULL, rescan,NULL)
|--struct scsi_device *sdev;
| struct Scsi_Host *shost = dev_to_shost(starget->dev.parent);
| //如果sdev不存在则分配并初始化
|--sdev = scsi_alloc_sdev(starget, lun, hostdata);
| |--sdev = kzalloc(sizeof(*sdev) + shost->transportt->device_size,GFP_KERNEL);
| |--初始化sdev
| | |--INIT_WORK(&sdev->event_work, scsi_evt_thread)
| | | INIT_WORK(&sdev->requeue_work, scsi_requeue_run_queue)
| | | sdev->sdev_gendev.parent = get_device(&starget->dev);
| | | sdev->sdev_target = starget;
| | | sdev->hostdata = hostdata;
| | |--sdev->request_queue = scsi_mq_alloc_queue(sdev);//创建多队列
| | | sdev->request_queue->queuedata = sdev;
| | |--scsi_change_queue_depth(sdev, sdev->host->cmd_per_lun ? sdev->host->cmd_per_lun : 1);
| | | //链进target->devices
| | |--scsi_sysfs_device_initialize(sdev);
| //发送SCSI_INQUERY命令探测逻辑单元,result contains valid SCSI INQUIRY data.
|--scsi_probe_lun(sdev, result, result_len, &bflags)
|--scsi_add_lun(sdev, result, &bflags, shost->async_scan);
scsi_probe_and_add_lun对指定编号的LU进行探测,参数分为为:目标节点指针,LU编号,输出参数,输出参数,扫描次数,传递给scsi_alloc_sdev函数的参数。scsi_probe_and_add_lun分为两个阶段:
1.scsi_probe_lun发送SCSI INQUIRY命令逻辑单元;
2.如果响应则表明逻辑单元存在且有效,将它通过scsi_add_lun添加到系统
-
scsi_alloc_sdev: 如果内存中没有找到对应的scsi_dev则分配。其中会通过scsi_mq_alloc_queue创建多队列并初始化;通过scsi_sysfs_device_initialize将sdev链入target->devices链表,对scsi_dev向SCSI总线及scsidev class注册的device的初始化。前面在添加添加SCSI适配器到系统时说明了Scsi_Host的添加时机,而此处就是scsi_target和scsi_dev的添加时机,添加后如下:
-
scsi_probe_lun:发送SCSI_INQUERY命令探测逻辑单元, result保存了结果
-
scsi_add_lun:如果相应逻辑单元有效,则添加到系统中。根据SCSI INQUERY命令的响应数据和标志位来初始化SCSI DEV描述符的各个域。scsi_sysfs_add_sdev(sdev)将scsi dev及其对应的目标节点添加到sysfs,并创建对应的属性文件,此处会用到scsi_device的两个成员内嵌通用设备和内嵌类设备。执行此函数将会添加scsi_device,引发devcice_add(scsi_device->device),而sd_init->scsi_register_driver时会执行driver_add,匹配成功将执行 sd_probe,从此处可以看出每一个探测到的scsi_devei也就是每个逻辑单元LU都会执行一次sd_probe。关于sd_probe的介绍请参考 SCSI子系统基础学习笔记 - 4.scsi_probe
参考文档
存储技术原理分析