Linux Block IO V

Block IO 层的请求完成在整个IO完成的路径上紧接SCSI CMD完成之后,相应的回调函数在biorequest以及request_queue初始化时被注册。


blk_done_softirq()
	rq->q->softirq_done_fn(rq);     //scsi_softirq_done()
		case SUCCESS:
		scsi_finish_command()	
			scsi_end_request()
				blk_update_request()
					req_bio_endio()
						bio_endio()
							bio->bi_end_io()	//submit_bio_wait_endio etc
				blk_finish_request()
					req->end_io()   		//发自SCSI层的命令会有这个回调:blk_end_sync_rq()

Linux Block IO IV

内核使用大量的缓存、异步、优化机制来提高IO性能,这一点Block IO子系统也不例外。以最常见的buffered IO为例,所有需要写入磁盘的数据起初都是存在Page Cache中,由文件系统来管理,显然,这样的IO是基于task_struct的,在当前版本中,内核使用task_struct->buffer_head来管理task_struct提交的IO请求。然后,借助write_back机制,在合适的时机将存储了task_struct IO数据的脏页下刷到磁盘,即write_back线程调用Block IO子系统提供的submit_bio()接口。然而,这样的bio也不是直接转换为CDB直接落盘,Block IO子系统也有自己的异步、优化机制,这就是本文要讨论的重点,具体的,IO路径中Block层路段有以下几个重要的话题:

  1. pluging /unpluging 机制
  2. IO Scheduler的优化策略
  3. Block层与SCSI层衔接

pluging/unpluging机制使通用块层的request批量下发到Blk-core层,并进一步下发到底层设备。这个机制说起来也简单,FS下发的bio描述了一个IO的源头与目的地,Block层想要对其进行优化,上层提交一次request就发送一次CDB显然是不行的。而要优化,无非两种前提:池(缓存)使用空间为优化提供机会,异步使用时间为优化提供机会。这里,数据已经在内核空间的主存中,再缓存一次显然是浪费资源,所以,为了优化,Block层使用了异步机制为优化创造条件,也就是pluging/unpluging机制。

具体地,每个设备都有自己的水位线:BLK_MAX_REQUEST_COUNT,表示该设备一次性可以处理的request数量,为了最大限度的提高硬件性能,同时为IO调度器提供优化的机会,绝大多数意欲下发到底层设备的request(IO barrier request除外)都会先进入到task_struct->plug->list缓存起来,待request数量足够的时候,一次性蓄在其中的request加入到elv中优化,再转变为CDB下发到设备,而这掌控蓄放的塞子plug,就由下发线程负责,每下发一个request,都会检查是否达到了警戒水位,如此,便实现了异步下发。对下发的bio/request来说,核心函数是blk_queue_bio().

Continue reading

Linux Page IO

page IO,顾名思义,一端连接着内存,一端连接着IO系统,可以看作内存子系统和IO子系统的中间层,部分资料把这部分划归到Block层, 但我觉得虽然这部分上和IO强相关, 但同时又涉及到很多内存页的数据结构, 所以还是单独划归到一层比较好. 通常,对一个文件的回写IO数据首先通过文件系统写入到Page Cache中,之后,就通过Page IO层,以异步的方式下发到Block IO子系统。当然,文件系统的数据也可以直接写入到Block IO子系统(Direct IO)。

Page IO层的核心工作就是构造并管理bio, bio作为IO的基本单位,描述了IO从内存到磁盘的映射,bio的构造借助了内存管理中的buffer_head结构完成其构造。 buffer_head负责page到扇区的固定映射,在此基础上,bio只需了解IO的内存端,即可计算出它的磁盘端位置

在IO路径中,构造bio的代码流程如下:

Continue reading

Linux Block IO III

Block IO中关键对象通常在发现存储设备的时候已经构造好,其中,最关键的是gendisk及其关联request_queue的构造,二者的构造同样遵循**“分配->初始化->注册”**的流程。由于设备对象的初始化是从软件栈的底层到上层的, 即初始阶段Block层只是准备好相关的代码,供更底层的SCSI子系统或IDE子系统来”调用”,完成Block层相关对象的构造。

以SCSI磁盘驱动为例,其在SCSI LLDD的扫描并构造设备对象时,已经完成了request_queue中相应接口的注册,供Block层使用:

scsi_probe_add_add_lun()
	scsi_alloc_sdev()
		sdev->request_queue = scsi_alloc_queue(sdev)
			__scsi_alloc_queue(sdev->host, scsi_request_fn);
				blk_init_queue(request_fn, NULL)
					blk_init_queue_node()
						blk_init_allocated_queue()
							q->request_fn = rfn;		//scsi_request_fn
							blk_queue_make_request(q, blk_queue_bio)
								 q->make_request_fn = mfn;	//blk_queue_bio
			blk_queue_prep_rq(q, scsi_prep_fn);
				q->prep_rq_fn = pfn						//scsi_prep_fn
			blk_queue_unprep_rq(q, scsi_unprep_fn);
				q->softirq_done_fn = fn					//scsi_unprep_fn
			blk_queue_softirq_done(q, scsi_softirq_done);
				q->softirq_done_fn = fn
			blk_queue_rq_timed_out(q, scsi_times_out);
				q->rq_timed_out_fn = fn					//scsi_times_out
			blk_queue_lld_busy(q, scsi_lld_busy);
				q->lld_busy_fn = fn						//scsi_lld_busy

当位于SCSI HLDD的sd驱动匹配到已经注册到内核的SCSI磁盘对象时,即开始在内核中构造相应的对象,而gendisk等Block层中的对象是在其”同步”阶段分配以及缺省初始化,并在 “异步”阶段初始化并注册的,。


scsi_alloc_sdev()
	 sdev->request_queue = scsi_alloc_queue(sdev);

同步阶段


sd_probe()
	gd = alloc_disk(SD_MINORS)	//分配gendisk对象
		alloc_disk_node(minors, NUMA_NO_NODE)
			kzalloc_node()
			init_part_stats()
			disk_expand_part_tbl()
			hd_ref_init()
			rand_initialize_disk()
			disk_to_dev(disk)->class = &block_class	
			disk_to_dev(disk)->type = &disk_type
			device_initialize(disk_to_dev(disk))

异步阶段


sd_probe_async()
	init sdp sdkp
	gendisk gd=sdkp->disk
	gd->major = sd_major((index & 0xf0) >> 4);
	gd->first_minor = ((index & 0xf) << 4) | (index & 0xfff00);
	gd->minors = SD_MINORS;
	gd->fops = &sd_fops;						//注册gendisk对象的操作方法集
	gd->private_data = &sdkp->driver;
	gd->queue = sdkp->device->request_queue;	//gendisk使用LLDD已经准备好的request_queue
	add_disk(gd)								//注册gendisk对象到内核
		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)

至此,在Block层上使用一个磁盘的工作就准备就绪了

Linux Block IO II

本文主要讨论Block IO子系统核心类。下图是Block子系统软件栈示意图,和很多子系统(驱动尤甚)一样,Block由一个承上启下的core层和分布于其周围的诸多模块组成,通常来讲,Block子系统可以被分为3层:Generic Block Layer、Block core、Block Device Driver。


Generic Block Layer主要是提供对设备(gendisk)的抽象,在提交一个请求到Core之前,显然要知道这个请求是提交给谁的,这个谁可以通过文件的inode获取,是构造请求(bio)的必需步骤。一个Block Device在注册到内核的时候需要注册相应的gendisk到Generic Block Layer
Block Core对上要接收上层提交的bio,通过plug机制以及IO调度算法,对请求进行合并和排序,再派发到底层驱动
Block Device Driver主要是一些块设备驱动。
此外,IO路径的所有逻辑都是为IO服务的,而bio和request作为承载IO请求的直接类,request_queue作为各个层操作IO请求的入口,它们的重要性不言而喻,对于这几个家伙,有很多种形象的比喻,如果每一层IO路径上的每一层逻辑是一个工位,那么它们就是流水线;如果每一层逻辑是一个山楂球,它们就是冰糖葫芦杆…

gendisk

通用块层提供了对设备请求的抽象,系统中的任何BlockDevice在注册的时候都要分配、初始化、注册一个gendisk对象及其必要数据结构到内核。该层主要由以下的对象组成gendisk、hd_struct,其中gendisk是整个通用块层的核心类,围绕着gendisk,通用块层的主要对象的关系如下图所示:

gendisk对象作为对块设备的抽象,用于描述整块磁盘、整个MMC芯片等,这种设计可以很好的体现OOP中的”多态”,gendisk本身封装了块设备的”共性”部分,而对于”个性”的部分,使用Kernel中的常见伎俩——private_data来解决。

对于共性的部分,块设备常用的”分区”的管理对象hd_structdisk_part_tbl必然封装在gendisk中。具体地,在gendisk眼中,如果一个块设备没有分区,那么就只有一个0号分区,该分区由gendisk直接内嵌的hd_struct描述,如果一个块设备有了分区,那么就用disk_part_tbl描述其分区表,该分区表的第一项仍旧是gendisk的0号分区。gendisk通过0号分区hd_struct的device接入sysfs,同理,非0号分区也通过相应的hd_struct接入sysfs
对于个性的部分,对于scsi磁盘,private_data指向scsi_disk->scsi_driver(driver/scsi/sd.c +2904),对于RAID,指向struct mddev(drivers/md/md.c +4850),对于Device Mapper,private_data指向struct mapped_device(drivers/md/dm.c +2194)。此外,fops同样是”多态”,视情况被赋值为sd_fops(drivers/scsi/sd.c +2903)、lsi_fops(drivers/scsi/megaraid_mm.c +85)等

关于gendisk域的更详细解释,参见下表。

Continue reading

Linux Block IO I

块IO子系统上承文件系统,下启SCSI等具体的存储设备子系统,对下层的诸多设备进行统一的抽象,以向上提供统一的块存储视图,同时,也使得deviceMapper,RAID等模块的设计变得容易。在内核IO路径中,块IO子系统到交通枢纽的作用,其在内核中的相对位置如下

对上,Block子系统位于文件系统层的下方,通过bdev伪文件系统管理系统中的所有磁盘抽象,使得其他文件系统等访问接口可以找到一个磁盘的抽象

对下,为具体的存储设备提供通用的服务,包括磁盘和分区抽象、IO请求优化、重映射等。

就块设备本身来说,可以分为三层

  1. 通用块层位于最上,对存储设备的设备和分区进行文件系统可用的抽象
  2. IO调度层位于中间,负责对上层下发的IO的合并优化等工作,提供NOOP,CFQ,DeadLine,Anticipatory 4种IO调度器
  3. 块设备驱动层,将通用块设备作为操作对象的”驱动”,MultiDisk子系统就是工作在这一层。

在内核源码中,块IO子系统的文件主要分布在block/目录下,相关文件的简介如下:

filedescription
include/linux/genhd.h通用块层,封装了gendisk、hd_struct等对象,对所有的设备进行统一的抽象,io下发必须经过这一层,在设备初始化的时候被构造进内核
block/genhd.cregister_blkdev
include/linux/blk_types.hbio定义和相关宏
include/linux/blkdev.hrequest、request_queue及其在block设备上的操作方法
block/blk-core.cblkdev.h ->blk-core.c
include/linux/fs.hinode,file,super_block,file_operations以及block_device的定义
fs/block_dev.c块设备和文件系统的接口部分,eg,bdev文件系统
block/partition-generic.c分区partition的相关操作
block/blk-merge.c和include/linux/elevator.h block/elevator.c一同构成了通用块层到块核心层的边界
blk-sysfs.c封装了块设备在sysfs中输出的信息
blk-setting.c封装了对块设备进行设置(队列深度,DMA地址)的方法
block/bio.h、block/bio.cbio操作函数
include/linux/blk-mq.h、block/blk-mq.c与MultiDisk子系统相关的操作
block/blk-lib.c封装了一下helper类的函数
block/blk-flush.c封装了下刷request相关函数

Linux SCSI 子系统 V

不论请求来自Block层还是SCSI层,当请求完成时的入口只有一个:SCSI设备上报到内核的中断处理函数,请求完成逐级向上传递,直到应用层。和很多的request-response模型一样,IO请求的完成分为以下3类4种:

  1. 请求完成+请求成功
  2. 请求完成+请求失败+重试
  3. 请求完成+请求失败+错误处理
  4. 请求响应超时

Init

以megasas的PCIe RAID卡为例,其在SCSI子系统中的请求完成的初始化如下,

准备硬件中断handler和tasklet结构到scsi_host_template:

static struct megasas_instance_template megasas_instance_template_gen2 = {
  .service_isr = megasas_isr,     
  .tasklet = megasas_complete_cmd_dpc,
};

注册工作队列和硬件中断handler,当磁盘完成SCSI请求时会上报该中断

megasas_probe_one()
	INIT_WORK(&instance->work_init, process_fw_state_change_wq);
	megasas_init_fw()
		tasklet_init(&instance->isr_tasklet, instance->instancet->tasklet);
	request_irq(service_isr)

系统还为Block层的request的完成注册了softirq:

blk_softirq_init()
	INIT_LIST_HEAD(&per_cpu(blk_cpu_done, i));
	open_softirq(BLOCK_SOFTIRQ, blk_done_softirq);
	register_hotcpu_notifier(&blk_cpu_notifier);

做好了准备工作,开始讨论请求完成的这三种情况

Complete

当磁盘完成SCSI请求时会上报中断,内核最终会执行注册的handler,请求完成的传递路径如下所示,经由**硬件中断->work_struct + tasklet->软件中断-(->完成量)**逐层传递。

megasas_isr()
	megasas_deplete_reply_queue()
		some work in top half
		schedule_work(&instance->work_init)
		tasklet_schedule(&instance->isr_tasklet)

系统在合适的时机调度到该tasklet,其流程如下:

megasas_complete_cmd_dpc()
	megasas_complete_cmd()
		cmd->scmd->result =					//设置result,在blk_done_softirq()中需要读取执行结果
		cmd->scmd->scsi_done(cmd->scmd);	//scsi_request_fn()中构造scsi_cmnd时注册为scsi_done()
		blk_complete_request()
			__blk_complete_request()
				raise_softirq_irqoff(BLOCK_SOFTIRQ)

Success

作为该softirq的handler的blk_done_softirq()的流程如下:

blk_done_softirq()
    rq->q->softirq_done_fn(rq);     //scsi_softirq_done()
    	disposition = scsi_decide_disposition(cmd)
    		host_byte(scmd->result)			//读取result
    	case SUCCESS:
        scsi_finish_command()
            scsi_cmd_to_driver()
            drv->done(cmd)          //sd_done()
            	sd_dif_complete()	//回收bio内存
            scsi_io_completion()    //Completion processing for block device I/O requests
            scsi_end_request()
                blk_update_request()
              		req_bio_endio()
                blk_finish_request()
                    req->end_io()   		//blk_end_sync_rq()
                        complete(waiting)	//唤醒队列中发自SCSI层的命令,发自Block层的命令不需要
          		scsi_release_buffers(cmd);
          			scsi_free_sgtable()		//回收SG数据
          		scsi_put_command(cmd);
                scsi_run_queue()
            __scsi_queue_insert()

Failed – retry


blk_done_softirq()
    rq->q->softirq_done_fn(rq);     //scsi_softirq_done()
        disposition = scsi_decide_disposition(cmd)
    		host_byte(scmd->result)			//读取result
    	case NEEDS_RETRY:
    	case ADD_TO_MLQUEUE:
    		scsi_queue_insert()
    			__scsi_queue_insert()
    			blk_requeue_request(q, cmd->request)	//进入Block层
    				blk_delete_timer(rq)
    				blk_clear_rq_complete(rq)
    				elv_requeue_request(q, rq)
    			kblockd_schedule_work(&device->requeue_work)
    				queue_work(kblockd_workqueue, work)

Failed – eh


scsi_host_alloc()    
	shost->ehandler = kthread_run(scsi_error_handler, shost);
blk_done_softirq()
    rq->q->softirq_done_fn(rq);     //scsi_softirq_done()
        disposition = scsi_decide_disposition(cmd)
    		host_byte(scmd->result)			//读取result
    	default:
        	scsi_eh_scmd_add()
        		scsi_host_set_state()
        		list_add_tail(&scmd->eh_entry, &shost->eh_cmd_q);
        		scsi_eh_wakeup(shost)
        			wakeup(shost->ehandler)
        	scsi_finish_command()
scsi_error_handler()
	while(true)
	shost->transportt->eh_strategy_handler(shost);
	scsi_unjam_host(shost);
		scsi_eh_get_sense()
			list_for_each_entry_safe()
			scsi_request_sense(scmd)
				scsi_send_eh_cmnd()
					scsi_eh_prep_cmnd()
					scmd->scsi_done = scsi_eh_done
					shost->hostt->queuecommand(shost, scmd)
					scsi_eh_restore_cmnd(scmd, &ses)
					wait_for_completion_timeout(&done, timeout)
					scsi_eh_completed_normally(scmd)
					scsi_eh_restore_cmnd(scmd, &ses)
			scsi_decide_disposition(scmd)
			scsi_eh_finish_cmd(scmd, done_q)
				list_move_tail(&scmd->eh_entry, done_q)
		scsi_eh_abort_cmds(&eh_work_q, &eh_done_q))
		scsi_eh_ready_devs(shost, &eh_work_q, &eh_done_q);
		scsi_eh_flush_done_q(&eh_done_q);

可以看到,该中断处理函数的工作,需要借助于request_queue中已经注册好回调函数,比如在SCSI子系统中注册的scsi_softirq_done()与sd_done(),此外,参照前文,对于发自SCSI子系统的SCSI命令,其发送线程都会等待完成量waiting的完成,这里,在request的回调函数blk_end_sync_rq()中,该完成量被完成,其线程可以被唤醒,相比之下,发自Block层的SCSI命令就不会等待这个完成量

Timeout


static struct scsi_host_template megasas_template = {
	.eh_timed_out = megasas_reset_timer
}
megasas_reset_timer()	
	instance->host->can_queue = instance->throttlequeuedepth;
	instance->last_time = jiffies;
	instance->flag |= MEGASAS_FW_BUSY;

Linux SCSI 子系统 IV

当上层业务需要和磁盘交互时,包括读写请求,以及设备对象初始化时获取磁盘信息,这些都需要将请求转化为SCSI磁盘可以识别的SCSI命令下发到磁盘,这里就涉及到了SCSI命令的转化和执行。
SCSI命令的执行可以分为两种情况:发自SCSI和发自Block,前者多指在SCSI设备对象初始化过程中,比如SCSI子系统需要转动磁盘、查询磁盘信息等,后者多指源自高层经由Block层下发到SCSI子系统读写命令。本文从这两个角度阐述。

执行发自SCSI的命令

如果命令发自SCSI层,则通过scsi_execute_req()发出,下面是执行scsi_execute_req的调用关系,使用这个接口下发的命令,会被封装到一个request中插入Block层中request_queue中,通过Block层的处理逻辑择时下发,这种设计是一个低层代码调用高层代码例子。


scsi_execute_req()
    scsi_execute_req_flags()
        scsi_execute()
            req = blk_get_request(sdev->request_queue); //从设备的request_queue获取request
            blk_execute_rq()    			//insert a request into queue for execution
                blk_execute_rq_nowait(blk_end_sync_rq)  //SCSI调用Block函数,注册blk_end_sync_rq()函数
                    rq->end_io = done;      		//即blk_end_sync_rq()
                    __elv_add_request() 
                    __blk_run_queue()
                wait_for_completion_io()		//通过完成量等待IO结束,发自SCSI层的SCSI命令独有

执行发自Block的命令

发自Block层的SCSI命令多是读写请求,即Block层通过回调SCSI HLDD的scsi_request_fn()等接口,将Block层的request转化为scsi_cmnd,再通过回调queuecommand()接口下发到SCSI LLDD。然而,无论是Block还是Net等,内核中的IO执行的框架均大体相同,都可看作request-response结构,而实现这种框架,需要三部分工作:

  1. 准备完成回调,并注册之。完成回调根据返回结果将处理发送的消息,常见的有释放承载请求的buffer,相关标志置位,继续向上层通知处理结果等。
  2. 发起request。
  3. 低层执行request并根据执行结果上报response。

这一点,SCSI子系统也不无例外。

为了执行来自Block层的IO请求,SCSI子系统在构造相关对象的时候就必须要注册包括但不限于上述handler的回调接口。下面是SCSI总线扫描过程中的核心函数,可以看出,在构造scsi_device对象之时,便已经将关键的函数注册好,其中,request_queue中两个重要的回调接口:request_fn和softirq_done_fn分别在注册为scsi_request_queue和scsi_softirq_done,前者在执行request时的回调接口,后者是收到response时的回调接口。

Continue reading

Linux SCSI 子系统 III

本文以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函数。

Continue reading

Linux SCSI 子系统 II

SubSySScSi – PreFAce一文中已经讨论了硬件原理,并给出了下图的Scsi与内核关系的关系,本文主要讨论SCSI子系统的关键角色-关键的类与关键的对象。
内核支持的总线众多,每一种总线上挂载的设备又多种多样,涉及的核心类与对象千差万别,但总的来说,根据bus-device-driver模型,在一个总线子系统中,其核心类与对象大体可以分为以下6类:

  1. 总线的抽象->struct bus_type scsi_bus_type
  2. 总线控制器的抽象->struct scsi_host_template, struct Scsi_Host
  3. 总线控制器驱动的抽象->None
  4. 总线设备的抽象->struct scsi_target, struct scsi_device, struct scsi_disk
  5. 总线设备驱动的抽象->struct scsi_driver
  6. 命令的抽象->struct scsi_cmnd

他们之间的关系可以用下图简要表示:

核心的对象有:scsi_bus_type, sdev_class, shost_class, sd_disk_class。,这些对象在内核初始化SCSI子系统时候即创建好,包括总线级的实例scsi_bus_type以及为各种SCSI对象准备的class,它们负责将后续注册到内核的SCSI相关对象通过sysfs融入内核设备驱动框架中,进而被用户使用,个中关系可以用下图简要表示。

scsi_bus_type

scsi_bus_type,封装了SCSI总线级的共性资源,比如总线名、匹配函数等等。一如所有的bus_type实例,通过其上的设备链表与驱动链表,管理着SCSI总线中的”设备”及其”驱动”,有意思的是,在SCSI子系统中,不论是Scsi_Host实例、还是scsi_target实例抑或scsi_device实例,都被看作”设备”被挂接到scsi_bus_type的”设备链表”中,虽然只有scsi_device实例在”驱动链表”有其对应的驱动。

474 struct bus_type scsi_bus_type = {
475         .name           = "scsi",
476         .match          = scsi_bus_match,
477         .uevent         = scsi_bus_uevent,
478 #ifdef CONFIG_PM                               
479         .pm             = &scsi_bus_pm_ops,
480 #endif
481 };
Continue reading