本文以LSI公司基于PCIe接口的MegaSAS RAID卡驱动为例,分析SCSI总线在内核注册对象的过程。SCSI设备在内核初始化可以分为两部分,LLDD的部分和HLDD的部分,但无论哪部分,和其他内核对象的构造流程一样,都遵循”分配->初始化->注册“的流程。
LLDD INIT
LLDD的init主要是在内核中构造与SCSI设备直接相关的对象。在主板上已经插入了RAID卡的前提下,内核在启动过程中基于相关协议标准获取PCIe设备信息,并根据这些信息在内核中构造相应的PCIe设备对象,此外,内核在启动过程还会加载该RAID卡的驱动,本质上这是一个PCIe驱动,无论PCIe设备对象和PCIe驱动谁先谁后,只要二者都被加载进内核,就会match并回调PCI驱动的probe()。对于该款RAID卡的驱动来说,probe()被注册为megasas_probe_one(),由该函数根据板卡需求在内核SCSI子系统中构造相关对象。
梳理mega_probe_one()的流程,可以发现,该函数依然遵循内核驱动框架的”分配->初始化->注册“的流程, 其中,又分为针对SCSI适配器对象的三步走( scsi_host_alloc()scsi_add_host())以及针对SCSI设备的三步走(scsi_scan_host())。
megasas_probe_one() scsi_host_alloc() //alloc scsi host init Scsi_Host shost //init scsi host scsi_proc_hostdir_add() megasas_io_attach() scsi_add_host() //register scsi host scsi_add_host_with_dma() scsi_scan_host() //alloc, init and register scsi device scsi_prep_async_scan() do_scsi_scan_host() async_schedule(do_scan_async)
scsi_host_alloc()
主要负责Scsi_Host对象的分配和部分域的缺省值的初始化,相关逻辑整理如下:
struct Scsi_Host * scsi_host_alloc(struct scsi_host_template *sht, int privsize) //alloc scsi host { Scsi_Host shost = kzalloc(); shost->shost_state = SHOST_CREATED; init_waitqueue_head(&shost->host_wait); //init list_head INIT_LIST_HEAD(list_heads in shost); //set default values which can be overwritten shost->max_channel = 0; shost->max_id = 8; shost->max_lun = 8; shost->transportt = &blank_transport_template; //init Scsi_Host by scsi_host_template shost->hostt = sht; shost->this_id = sht->this_id; //init DD fields device_initialize(&shost->shost_gendev); shost->shost_gendev.bus = &scsi_bus_type; shost->shost_gendev.type = &scsi_host_type; device_initialize(&shost->shost_dev); shost->ehandler = kthread_run(scsi_error_handler); shost->tmf_work_q = alloc_workqueue(); scsi_proc_hostdir_add() }
scsi_add_host()
负责将已经分配好并部分初始化的Scsi_Host对象继续初始化并注册到内核SCSI子系统中。该函数的实质是调用scsi_add_host_with_dma函数。
static inline int __must_check scsi_add_host(struct Scsi_Host *host, struct device *dev) { return scsi_add_host_with_dma(host, dev, dev); }
int scsi_add_host_with_dma(struct Scsi_Host *shost, struct device *dev, struct device *dma_dev) { if (!shost->shost_gendev.parent) shost->shost_gendev.parent = dev ? dev : &platform_bus; device_add(&shost->shost_gendev); pm_runtime_set_active(&shost->shost_gendev); pm_runtime_enable(&shost->shost_gendev); device_enable_async_suspend(&shost->shost_gendev); scsi_host_set_state(shost, SHOST_RUNNING); device_enable_async_suspend(&shost->shost_dev); device_add(&shost->shost_dev); scsi_sysfs_add_host(shost); scsi_proc_host_add(shost); }
scsi_scan_host()
可以分为两部分,同步执行的部分和异步执行的部分。相关调用关系整理如下。可以看出,对于SCSI总线的扫描,按照scsi_scan_host_selected()->scsi_scan_channel()->__scsi_scan_target()->scsi_probe_and_add_lun()->scsi_add_lun()的流程向内核中添加SCSI总线上的设备相应的设备对象。
scsi_scan_host() //同步执行 scsi_prep_async_scan() do_scsi_scan_host() async_schedule(do_scan_async) //调度do_scan_async作为异步执行部分
do_scan_async() do_scsi_scan_host() shost->hostt->scan_start()//如果驱动需要执行特定的扫描逻辑,就要执行这两个函数, megasas没注册 shost->hostt->scan_finished() scsi_scan_host_selected() scsi_host_scan_allowed() scsi_autopm_get_host() scsi_scan_channel() __scsi_scan_target() dev_to_shost() //驱动中的trick之一, scsi_alloc_target() scsi_probe_and_add_lun() scsi_device_lookup_by_target() scsi_device_lookup_by_target() list_for_each_entry() scsi_alloc_sdev() scsi_probe_lun() scsi_add_lun() //添加lun scsi_finish_async_scan()
HLDD Init
LLDD初始化过程为在SCSI总线上探测到的LUN构造相应的scsi_device对象并注册到内核,即注册到scsi_bus_type管理的总线中,根据SubSySScSi – RoLES一文讨论的,如此,就会match到相应的SCSI设备驱动,执行其probe函数——sd_probe(),HLDD的初始化即由此开始,和scsi_scan_host()一样,sd_probe也分为同步执行的部分和异步执行部分,但总的来说,HLDD初始化主要工作是分配、初始化并注册供Block层使用的sd_disk,gendisk等关键对象。
sd_probe()
的调用树梳理如下:
sd_probe() struct scsi_disk sdkp device_initialize() device_add(sdkp) dev_set_drvdata() async_schedule_domain(sd_probe_async)
sd_probe_async() init sdp sdkp gendisk gd=sdkp->disk init gd add_disk(gd) bdi_register_dev() blk_register_region() register_disk() blk_register_queue() disk_add_events() blk_integrity_add() sd_revalidate_disk(gd) sd_spinup_disk() //转动磁盘 scsi_execute_req() sd_readcapacity() set_capacity(disk)