【转】【总结汇聚】SCSI开发基础

原文链接: https://wenku.baidu.com/view/764b4899fd0a79563c1e72ae.html

目录

Linux SCSI子系统的分层架构

​scsi驱动在内核中的位置

Linux中scsi驱动框架

scsi middle level层重要数据结构

scsi device的抽象

 scsi host的抽象

low-level接口方法——scsi_host_template

关键函数分析

scsi_scan_host函数

scsi_request_fn函数

scsi_dispatch_cmd函数

scsi设备扫描过程描述

SCSI设备读写过程

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


scsi驱动在内核中的位置

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

【转】【总结汇聚】SCSI开发基础_第1张图片

Linux SCSI子系统的分层架构

【转】【总结汇聚】SCSI开发基础_第2张图片

  1. top level为具体的scsi设备驱动(代表各种scsi设备类型的驱动),例如我们常用的磁盘、磁带等设备驱动就在该层(Linux中的实现为sd.c),scsi disk的驱动向上表现为块设备、对上提供块设备接口,因此,具有块设备的接口及一切属性,向下表现scsi设备,因为scsi disk基于scsi总线进行数据通信。top level驱动与具体的scsi设备相关,所以该类驱动往往由设备开发者提供,但是如果scsi设备为标准类设备,那么驱动可以通用。高层驱动认领低层驱动发现的scsi设备,为这些设备分配名称,将对设备的IO转换为scsi命令,交由低层驱动处理。
  2. middle level包含scsi栈的公共服务函数, 它实际上就是scsi总线层驱动,按照scsi协议进行设备枚举、数据传输、出错处理。middle level层的驱动与scsi specification相关,在一类操作系统平台上只需实现一次,所以该类驱动往往由操作系统开发者提供。高层和低层通过调用中层的函数完成其功能,而中层在执行过程中,也需要调用高层和低层注册的回调函数做一些个性化处理
  3. lower level为scsi控制器(HBA)的驱动,该驱动与具体的硬件适配器相关、代表与SCSI的物理接口的实际驱动器。其需要与scsi middle level层进行接口,所以往往由提供适配器的硬件厂商完成驱动开发,只有硬件厂商才对自己定义的register file(寄存器堆)最清楚, 例如各个厂商为其特定的主机适配器(Host Bus Adapter, HBA)开发的驱动。当然,在lower level层可以做虚拟的scsi host,所以该层的驱动也不一定对硬件进行操作。低层驱动主要作用是发现连接到主机适配器的scsi设备,在内存中构建scsi子系统所需的数据结构,并提供消息传递接口,将scsi命令的接受与发送解释为主机适配器的操作。

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

 


scsi middle level层重要数据结构

scsi device的抽象

  在scsi middle level定义了scsi device的数据结构,用于描述一个scsi的具体功能单元,其在scsi host中通过channel、id、lun进行寻址。首先,什么是channel、id和lun。通常SCSI总线适配器作为PCI设备的形式存在,其在计算机体系结构中的位置如下图所示:

【转】【总结汇聚】SCSI开发基础_第3张图片

 

  在scsi host中可以存在多个channel,每个channel是一条完整的scsi总线,在scsi总线上可以连接多个scsi节点,每个节点采用id进行编号,编号的大小与具体的scsi specification相关,与总线层的驱动能力等因素相关。每个节点可以根据功能划分成多个lun,每个lun才是我们通常所说的scsi设备

在系统初始化时会扫描系统PCI总线,由于scsi端口适配器挂接在pci总线上,因此会被pci扫描软件扫描得到,并且生成一个pci device(PDO)。然后扫描软件需要为该pci device加载相应的驱动程序。

在linux系统中,系统初始化时会遍历pci bus上存在的所有驱动程序,检查是否有符合要求的驱动程序存在,这里假设scsi host是USB或marwell中的设备,那么,如果存在USB或marwell提供的scsi端口驱动,就会被成功调用。加载scsi端口驱动时,pci扫描程序会调用对应scsi端口驱动提供的probe函数,该probe函数是scsi端口驱动程序在初始化驱动时注册到pci-driver上的(Linux的总线驱动都是采用的这种思路)。

在scsi host具体的probe函数中会初始化scsi host,注册中断处理函数,并且调用scsi_host_alloc函数生成一个scsi host,然后添加到scsi middle level,最后调用scsi_scan_host函数扫描scsi端口适配器所管理的所有scsi总线。

一个scsi端口适配器可能拥有多个channel,每个channel拥有一条scsi总线。传统scsi总线是并行共享总线,现有的SATA、SAS等P2P接口在逻辑上可以理解成总线的一种特例,所以scsi middle level驱动程序是通用的。由于一个scsi host可能存在多个channel,因此依次扫描每个channel。按照spec,传统scsi bus上最多可以连接16个scsi target,因此,scsi扫描程序会依次探测target。一个scsi target可以存在多种功能,每种功能称之为LUN,对于单功能设备(例如磁盘),其LUN通常为0。

Scsi host的扫描过程在系统初始化中进行,详细的代码我们就不去分析了,这里从网上摘录了一段伪代码对其进行简单地描述:
【转】【总结汇聚】SCSI开发基础_第4张图片

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

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

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

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

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

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

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

e.   循环(b)(c)(d)将scsi总线扫描完毕。结束scsi host的扫描工作。

通过上述扫描过程可以知道,在系统中可以采用如下方法对一个scsi device进行描述:host_id : channel_id : target_id : lun_id

其中,host_id是系统动态分配的,这与PCI总线的扫描顺序相关,对于固定硬件的系统host_id扫描得到的结果不会改变,但是,如果动态添加一个scsi host(PCI device),系统的host_id可能会发生变化,这一点需要注意。

最终,上述过程结束之后,scsi的硬件逻辑可以采用如下的总线拓扑结构进行描述:
【转】【总结汇聚】SCSI开发基础_第5张图片

驱动体系结构:

【转】【总结汇聚】SCSI开发基础_第6张图片

scsi_device是对LUN对象的抽象,其数据结构可以参考内核代码:

【转】【总结汇聚】SCSI开发基础_第7张图片

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


 scsi host的抽象

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

【转】【总结汇聚】SCSI开发基础_第8张图片


SCSI target抽象

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

/*
 * scsi_target: representation of a scsi target, for now, this is only
 * used for single_lun devices. If no one has active IO to the target,
 * starget_sdev_user is NULL, else it points to the active sdev.
 */
struct scsi_target {
	struct scsi_device	*starget_sdev_user;
	struct list_head	siblings;
	struct list_head	devices;
	struct device		dev;
	struct kref		reap_ref; /* last put renders target invisible */
	unsigned int		channel;
	unsigned int		id; /* target id ... replace
				     * scsi_device.id eventually */
	unsigned int		create:1; /* signal that it needs to be added */
	unsigned int		single_lun:1;	/* Indicates we should only
						 * allow I/O to one of the luns
						 * for the device at a time. */
	unsigned int		pdt_1f_for_no_lun:1;	/* PDT = 0x1f
						 * means no lun present. */
	unsigned int		no_report_luns:1;	/* Don't use
						 * REPORT LUNS for scanning. */
	unsigned int		expecting_lun_change:1;	/* A device has reported
						 * a 3F/0E UA, other devices on
						 * the same target will also. */
	/* commands actually active on LLD. */
	atomic_t		target_busy;
	atomic_t		target_blocked;
    ......
}

low-level接口方法——scsi_host_template

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

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

struct scsi_host_template {
	struct module *module;
	const char *name;

	int (* detect)(struct scsi_host_template *);
	int (* release)(struct Scsi_Host *);
	const char *(* info)(struct Scsi_Host *);
	int (* ioctl)(struct scsi_device *dev, int cmd, void __user *arg);
	int (* queuecommand)(struct Scsi_Host *, struct scsi_cmnd *);
	int (* eh_abort_handler)(struct scsi_cmnd *);
	int (* eh_device_reset_handler)(struct scsi_cmnd *);
	int (* eh_target_reset_handler)(struct scsi_cmnd *);
	int (* eh_bus_reset_handler)(struct scsi_cmnd *);
	int (* eh_host_reset_handler)(struct scsi_cmnd *);
	int (* slave_alloc)(struct scsi_device *);
	int (* slave_configure)(struct scsi_device *);
	void (* slave_destroy)(struct scsi_device *);
	int (* target_alloc)(struct scsi_target *);

	void (* target_destroy)(struct scsi_target *);
	int (* scan_finished)(struct Scsi_Host *, unsigned long);
	void (* scan_start)(struct Scsi_Host *);
	int (* change_queue_depth)(struct scsi_device *, int);
	int (* map_queues)(struct Scsi_Host *shost);
	int (* bios_param)(struct scsi_device *, struct block_device *,
			sector_t, int []);

	void (*unlock_native_capacity)(struct scsi_device *);
	int (*show_info)(struct seq_file *, struct Scsi_Host *);
	int (*write_info)(struct Scsi_Host *, char *, int);
    ......
}

    .queuecommand:通过该接口函数,scsi middle level将请求提交给scsi host driver。所以该函数是上下层之间的数据通道。

    .can_queue:描述了scsi host命令队列的长度。通常scsi host具有一个命令队列,scsi middle level通过queuecommand接口将命令直接挂入host的命令队列中,然后一步返回。

    .cmd_per_lun:该参数描述了每个lun通路所能缓存命令的数量。

    .sg_tablesize:scatter-gather表的长度。

关键函数分析

scsi_scan_host函数

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

【转】【总结汇聚】SCSI开发基础_第9张图片

scsi_request_fn函数

   scsi_request_fn函数为scsi设备请求队列处理函数,该函数通常被注册到request_queue->request_fn上。块设备请求的bio最终会merge到request 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设备,那么在各层之间的函数调用关系如下图所示:

【转】【总结汇聚】SCSI开发基础_第10张图片

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

scsi_dispatch_cmd函数

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

  Queuecomand函数的实现由scsi host driver完成。通常该函数的实现很简单,只需要将传下来的scsi命令挂载到host的scsi命令队列中。由于queuecommand函数在持有spinlock的上下文中运行(一般是先unlock,然后处理各种scsi命令,最后在lock回去),所以不宜做过多复杂的操作,否则很容易导致程序睡眠,从而使程序运行不稳定。


scsi设备扫描过程描述

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

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

scsi bus的扫描通过scsi middle level提供的服务完成。scsi host driver可以调用scsi middle level提供的扫描算法完成scsi总线设备的扫描。在scsi总线扫描过程中用到了scsi middle level层的如下重要函数:

  1.   scsi_scan_host:对scsi host设备进行扫描。
  2.   __scsi_add_device:探测具体的device,并且将其加入系统。
  3.   scsi_probe_and_add_lun:探测具体指定的lun,并且将其加入系统。
  4.   scsi_probe_lun:采用INQUIRY命令对lun节点进行探测。
  5.   scsi_add_lun:加入lun节点并且初始化SCSI设备。

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

【转】【总结汇聚】SCSI开发基础_第11张图片


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设备,当然上述过程还没有涉及到回调过程,实际的回调会在中断上下文、软中断上下文中完成,在请求发送的每一层都保存了相应的回调上下文。整个数据流的过程中,涉及到的函数如下:

【转】【总结汇聚】SCSI开发基础_第12张图片

IO在scsi层细化流程:

【转】【总结汇聚】SCSI开发基础_第13张图片


scsi 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的核心所在。


本文内容主要转载自: 

https://wenku.baidu.com/view/764b4899fd0a79563c1e72ae.html

https://blog.csdn.net/yunsongice/article/details/6171286

http://chinaunix.net/uid-26293227-id-3973714.html

你可能感兴趣的:(【转】【总结汇聚】SCSI开发基础)