原创 scsi middle level驱动分析

2008-7-7 09:07 6114 6 6 分类: 软件与OS

1、写在前面



       去年有一段时间对scsi middle level层驱动进行了分析,形成了零零散散的文档。今天抽空对scsi middle
level
层驱动进行总结,希望对大家在理解scsi以及Linux驱动机制方面有所帮助。另外,scsi middle level比较庞大,所以很多细节部分的内容会在后继的blog中给出。



       另外,大家可以广泛交流,我的mailtl_wzj@yahoo.com.cn



 



2scsi驱动体系结构



2.1 认识Linux中的scsi middle
level
驱动层文件



       Scsi
middle level
驱动相关的文件列表如下:



1)       
Scsi_ioctl.c:该文件处理用户层的IOCTL命令,这些函数由设备层驱动调用,例如sd.c



2)       
Scsi.cgeneral scsi middle level,封装了scsi middle层初始化、SCSI命令处理相关函数。



3)       
Scsi_lib.c:封装了SCSI请求队列相关处理函数,top level层调用的request_fn等函数都在该层实现。



4)       
Scsi_debug.c:这是一个非常不错的scsi host开发样板,另外也可以参考libata-scsi.c进行scsi host驱动的开发。scsi host driver的开发可以参考《scsi host driver编写总结》



5)       
Scsi_error.c:该文件封装了scsi错误处理的方法。



6)       
Scsi_scan.c:该文件封装了scsi设备扫描算法,提供了设备扫描方法。



7)       
Hosts.c:向low level driver提供scsi middle level的接口函数,例如scsi_add_host等。



 



2.2 scsi驱动在内核中的位置



        scsi驱动的核心为总线层驱动,在总线层驱动之上为各种不同的scsi设备驱动,在总线层驱动之下为scsi host驱动。其在内核中的位置如下图所示:
点击看大图


2.3 Linuxscsi驱动框架



       Linuxscsi驱动基本分为三大层:top
level
middle level以及lower leveltop level为具体的scsi设备驱动,例如我们常用的磁盘设备驱动就在该层(Linux中的实现为sd.c),scsi disk的驱动向上表现为块设备,因此,具有块设备的接口及一切属性,向下表现scsi设备,因为scsi disk基于scsi总线进行数据通信。top level驱动与具体的scsi设备相关,所以该类驱动往往由设备开发者提供,但是如果scsi设备为标准类设备,那么驱动可以通用。middle level实际上就是scsi总线层驱动,按照scsi协议进行设备枚举、数据传输、出错处理。middle level层的驱动与scsi specification相关,在一类操作系统平台上只需实现一次,所以该类驱动往往由操作系统开发者提供。lower levelscsi控制器的驱动,该驱动与具体的硬件适配器相关,其需要与scsi middle level层进行接口,所以往往由提供适配器的硬件厂商完成驱动开发,只有硬件厂商才对自己定义的register file(寄存器堆)最清楚。当然,在lower level层可以做虚拟的scsi host,所以该层的驱动也不一定对硬件进行操作。



 

       Linux中,scsi三层驱动模型如下图所示:

28790523-e5f3-4483-a12a-bd63f734bc95.jpg

    三层驱动模型进行了完善的功能划分,并且定义了很好的接口,便于不同开发者提供的产品能够无缝集成。本文将重点讨论操作系统提供的scsi middle level驱动的实现机制及功能接口。



3scsi middle level层重要数据结构



3.1 scsi device的抽象



       scsi middle level定义了scsi device的数据结构,用于描述一个scsi的具体功能单元,其在scsi host中通过channelidlun进行寻址。



       scsi host中可以存在多个channel,每个channel是一条完整的scsi总线,在scsi总线上可以连接多个scsi节点,每个节点采用id进行编号,编号的大小与具体的scsi specification相关,与总线层的驱动能力等因素相关。每个节点可以根据功能划分成多个lun,每个lun才是我们通常所说的scsi设备。这种逻辑可以采用如下的总线拓扑结构描述:
c08bffd6-c901-42f0-9437-ecba9b12ad5d.jpg



通过上述描述可以知道scsi_device是对lun的抽象。下面对scsi_device中的重要域进行说明:



struct scsi_device {



       struct
Scsi_Host *host;                     /*
scsi device相关的scsi host */



       struct
request_queue *request_queue;  /*
块设备接口的请求队列 */



 



       unsigned
int device_busy;                    /*
命令执行标记 */



      



       struct
list_head cmd_list;                     /* scsi command
队列 */



      



       struct
scsi_cmnd *current_cmnd;        /*
当前执行的命令 */



 



       unsigned
int id, lun, channel;              /* SCSI
设备的标识 */



 



       void
*hostdata;                                   /*
通常指向low-level driver定义的scsi device */



      



} __attribute__((aligned(sizeof(unsigned
long))));



 



       scsi总线probe的过程中,scsi
middle level
会为每个lun抽象成scsi device,实现的核心函数为scsi_probe_and_add_lun()



 



3.2 scsi host的抽象



       scsi
host
的语义很清晰,其描述了一个scsi总线控制器。在很多实际的系统中,scsi host为一块基于PCI总线的HBA或者为一个SCSI控制器芯片。每个scsi
host
可以存在多个channel,一个channel实际扩展了一条SCSI总线。每个channel可以连接多个scsi节点,具体连接的数量与scsi总线带载能力有关。scsi host的重要域描述如下:



struct Scsi_Host {



       struct
list_head       __devices;                    /* scsi device
链表 */



       struct
list_head       __targets;



      



       struct
scsi_host_template *hostt;                /*
scsi host
操作接口方法 */



       struct
scsi_transport_template *transportt;   /*
scsi host transport
方法 */



 



unsigned int
host_busy;                            /*
scsi host
忙标记 */



       unsigned
int host_failed;                           /*
commands that failed. */



   



unsigned int
max_id;                                  /*
最大的scsi node数量 */



       unsigned
int max_lun;                                /*
最大的lun数量 */



       unsigned
int max_channel;                          /*
最大的channel数量 */



 



       unsigned
char max_cmd_len;                      /*
scsi
命令的长度 */



 



       int
this_id;                                                /* scsi host
在总线的id */



       int
can_queue;                                           /*
scsi cmd
是否可以queuehost标记 */



       short
cmd_per_lun;                                          /*
每个lun可以queue多少scsi cmd */



       short
unsigned int sg_tablesize;                   /*
scatter-gather table
大小 */



       short
unsigned int max_sectors;



       ……



};



 



3.3 scsi target的抽象



       scsi
target
scsi总线上的scsi node进行了抽象。每个scsi target可能拥有多个lun,即多个scsi deviescsi target数据结构中的重要域定义如下:



struct scsi_target {



       struct
scsi_device   *starget_sdev_user;              /*
当前活动的scsi device */



       struct
list_head       siblings;



       struct
list_head       devices;                        /* scsi device
链表 */



       struct
device          dev;



       unsigned
int           reap_ref;



       unsigned
int           channel;                       /*
当前channel */



       unsigned
int           id;                               /* scsi target
ID */



      



} __attribute__((aligned(sizeof(unsigned
long))));



 



3.4 low-level接口方法——scsi_host_template



       scsi
middle level
通过scsi_host_template接口调用scsi host的具体方法。在scsi host drivermiddle level注册host对象的同时需要注册scsi_host_template方法,该方法被注册到scsi host对象中。



       scsi_host_template数据结构中的重要域说明如下:



struct scsi_host_template {



       /*
scsi middle level
层驱动通过该函数将scsi
command
提交给low level层驱动,并且告 low level驱动完成scsi命令之后需要调用done()函数 */



       int
(* queuecommand)(struct scsi_cmnd *,



                          void (*done)(struct scsi_cmnd *));   



 



       /*
scsi host
出错处理函数 */



       int
(* eh_abort_handler)(struct scsi_cmnd *);



       int
(* eh_device_reset_handler)(struct scsi_cmnd *);



       int
(* eh_bus_reset_handler)(struct scsi_cmnd *);



       int
(* eh_host_reset_handler)(struct scsi_cmnd *);



 



       /*
更改scsi设备的队列深度 */



       int
(* change_queue_depth)(struct scsi_device *, int);



 



       int
can_queue;                      /* scsi
host
队列深度 */



       int
this_id;                           /* scsi
host
ID */



       unsigned
short sg_tablesize;   /* scatter-gather
table
的容量 */



       short
cmd_per_lun;                     /*
每个lun能够queue的命令数 */



 



       unsigned
emulated:1;             /*
虚拟scsi host flag */



};



一个典型的scsi_host_template方法定义如下:



struct
scsi_host_template bscsi_host_template = {



       .module                 =
THIS_MODULE,



       .name                    = BSCSI_HOST_IDENT,             /* scsi host的名字 */



       .info                      = bscsi_info,



       .slave_configure     = bscsi_slave_configure,



       .queuecommand     = bscsi_queuecommand,                     /* scsi cmd请求接收函数 */



       .can_queue            = BSCSI_HOST_CAN_QUEUE,   /*
host cmd queue depth */



       .cmd_per_lun         = BSCSI_CMD_PER_LUN,          /* lun cmd queue depth */



       .sg_tablesize          = SG_ALL,                                 /*
scatter-gather table
容量 */



       .use_clustering       = BSCSI_CLUSTERING,



       .this_id                  = BSCSI_HOST_ID,                   /* scsi host ID */



       .emulated              = 1,                                           /*
virtual scsi host */



       .bios_param           = bscsi_std_bios_param,



       .proc_name           = "bscsi",



       .proc_info             = bscsi_proc_info,



       .shost_attrs            = bscsi_host_attrs,



};



4、关键函数分析



4.1 scsi_scan_host函数



       scsi middle level层提供了scsi host扫描函数,在设备枚举过程中scsi host可以调用该函数对scsi总线适配器进行扫描,当然host驱动也可以调用更加底层的函数对scsi总线进行扫描。scsi_scsn_host函数实现流程如下:
add987cb-1964-4343-b745-9d259a4d014f.jpg


4.2 scsi_request_fn函数



       scsi_request_fn函数为scsi设备请求队列处理函数,该函数通常被注册到request_queue->request_fn上。块设备请求的bio最终会mergerequest queue中,然后通过unplug_fn函数调用request_queue->request_fn,实现scsi_reuqest_fn函数的调用。





 


       Scsi_request_fn函数实现了请求队列的处理,首先从请求队列中摘取一个request,然后通过q->prep_rq_fn函数将请求转换成scsi命令,并且对scsi command进行初始化,最后通过scsi_dispatch_cmd函数将scsi命令分发给底层的scsi host驱动。

 

       scsi_request_fn函数的实现过程中,需要通过块设备发下来的请求构造相应的scsi命令,而scsi命令的生成与具体的设备驱动相关,其需要调用设备驱动提供的scsi命令初始化函数*_init_command完成命令初始化过程。假设请求发送给scsi disk设备,那么在各层之间的函数调用关系如下图所示:
点击看大图


从前面分析可以看出,请求队列queuetop levelmiddle level之间的纽带。上层请求会在请求队列中维护,处理函数的方法由上下各层提供。在请求队列的处理过程中,将普通的块设备请求转换成标准的scsi命令,然后再通过middle
level
low level之间的接口将请求递交给scsi host



4.3 scsi_dispatch_cmd函数



scsi_dispatch_cmd函数将一个scsi命令提交给底层scsi host驱动。在命令dispatch的过程中,middle level会检查scsi host是否出于busy状态,是否还有空间存放新的scsi command。如果所有条件都满足,那么会调用上下层之间的接口函数queuecommand函数转发请求。



 



Queuecomand函数的实现由scsi host driver完成。通常该函数的实现很简单,只需要将传下来的scsi命令挂载到hostscsi命令队列中。由于queuecommand函数在持有spinlock的上下文中运行,所以不宜做过多复杂的操作,否则很容易导致程序睡眠,从而使程序运行不稳定。



 



5 scsi设备扫描过程描述



       在计算机系统启动过程中,操作系统会扫描默认的PCI根节点,从而触发了PCI设备扫描的过程,开始构建PCI设备树。



       scsi
host
作为PCI设备会被PCI总线驱动层扫描到(PCI设备的扫描采用配置信息读取的方式),扫描到scsi host之后,操作系统开始加载scsi host的驱动,scsi host driver就是上面说所的low level driverscsi host driver初始化scsi控制器,通过PCI配置空间的信息分配硬件资源,注册中断服务。最后开始扫描通过scsi控制器扩展出来的下一级总线——scsi bus



       scsi
bus
的扫描通过scsi
middle level
提供的服务完成。scsi
host driver
可以调用scsi
middle level
提供的扫描算法完成scsi总线设备的扫描,扫描过程可以描述如下:



1、 
采用scsi_add_host()函数为扫描出来的scsi host添加一个对象,注册到scsi middle level



2、 
通过__scsi_add_device()函数循环扫描scsi host,扫描过程采用了scsi middle level的服务scsi_probe_and_add_lun()



3、 
通过向scsi device发送INQUIRY命令获取scsi设备信息,得知scsi设备的vendor idproduct id以及设备类型等关键信息。至此,操作系统得知了scsi设备所具备的各种能力,并且向scsi middle level注册了scsi设备对象——scsi device



4、 
根据scsi设备的信息初始化scsi device对象,并且通知内核去加载该设备的驱动程序。如果被枚举的设备为scsi
disk
,那么scsi磁盘的驱动程序将被加载,至此,一个scsi设备被枚举成功。



5、 
循环(2)(3)(4)将scsi总线扫描完毕。结束scsi
host
的扫描工作。





 


scsi总线扫描过程中用到了scsi
middle level
层的如下重要函数:

1scsi_scan_host:对scsi host设备进行扫描。



2__scsi_add_device:探测具体的device,并且将其加入系统。



3scsi_probe_and_add_lun:探测具体指定的lun,并且将其加入系统。



4scsi_probe_lun采用INQUIRY命令对lun节点进行探测。



5scsi_add_lun加入lun节点并且初始化SCSI设备。



 

       scsi总线scan过程中的函数调用情况如下图所示:

5ca43d37-9283-4303-8124-16931ea27e68.JPG



6 scsi设备读写过程



     
        
在此给出一个scsi设备的读写数据流程,通过该例子,读者可以方便查找Linux源代码,并且能够理清繁杂的代码结构。假设读写的scsi设备为scsi disk设备,数据首先通过文件系统,进入到文件系统的Cache。文件系统的pdflush daemon会将Cache住的数据刷新到磁盘,其根据buffer head的内容构造bio,然后调用块设备接口(submit_bio)将请求发送给块设备层。bio在块设备层多次转发,最后被merge到块设备的请求队列中。请求可能会在请求队列滞留一段时间,然后在软中断或者用户上下文中调用request_fn去处理请求队列。在scsi middle level驱动层,块设备的请求被转换成scsi command,然后通过queuecommand函数接口将scsi command提交给scsi host,通常scsi host会发起DMA操作将数据传输给具体的设备。至此,数据从应用程序转移到了scsi设备,当然上述过程还没有涉及到回调过程,实际的回调会在中断上下文、软中断上下文中完成,在请求发送的每一层都保存了相应的回调上下文。整个数据流的过程中,涉及到的函数如下:
点击看大图


7scsi middle level层驱动设计思想总结



       scsi
middle level
还有很多实现细节没有探讨,很多细节性的问题会在后继的blog中给出,这里只是给出了scsi
middle level
的框架性东西,意在对scsi总线层有个提纲挈领的效果。在分析scsi middle level的过程中,有如下几点感想:



1、 
scsi驱动采用了规范的分层设计思想,其一共分为三层,分层之后使得设计分工更加明确,而且在逻辑上也更加清晰,设计工作也更加简单。



2、 
scsi驱动中比较固定的层次为scsi middle level,该层可以称之为scsi通用中间层,或者为总线驱动层。该层向上和向下都需要提供接口,所以上层驱动开发时需要注册相关接口函数,下层驱动工作时也需要注册接口函数,只有这样中间层才可以很灵活的进行上下层数据传输。



    scsi middle level主要实现了scsi总线扫描算法,scsi命令转换算法,scsi出错处理等机制,这些东西都是scsi的核心所在。








文章评论0条评论)

登录后参与讨论
我要评论
0
6
关闭 站长推荐上一条 /2 下一条