1.6.1 scsi总线驱动的初始化

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

前面我们已经知道了上三层的工作,接下来大部分知识来自底下三层。

在Linux中scsi驱动基本分为三大层:top level,middle level以及lower level。top level为具体的scsi设备驱动,例如我们常用的磁盘设备驱动就在该层(Linux中的实现为sd.c),scsi disk的驱动向上表现为块设备,因此,具有块设备的接口及一切属性,向下表现scsi设备,因为scsi disk基于scsi总线进行数据通信。top level驱动与具体的scsi设备相关,所以该类驱动往往由设备开发者提供,但是如果scsi设备为标准类设备,那么驱动可以通用。

middle level实际上就是scsi总线层驱动,按照scsi协议进行设备枚举、数据传输、出错处理。middle level层的驱动与scsi specification相关,在一类操作系统平台上只需实现一次,所以该类驱动往往由操作系统开发者提供。

lower level为scsi控制器的驱动,该驱动与具体的硬件适配器相关,其需要与scsi middle level层进行接口,所以往往由提供适配器的硬件厂商完成驱动开发,只有硬件厂商才对自己定义的register file(寄存器堆)最清楚。当然,在lower level层可以做虚拟的scsi host,所以该层的驱动也不一定对硬件进行操作。

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

而前面提到的scsi device的数据结构就是在scsi middle level定义的,用于描述一个scsi的具体功能单元,其在scsi host中通过channel、id、lun进行寻址。

首先,什么是channel、id和lun。通常SCSI总线适配器作为PCI设备的形式存在,其在计算机体系结构中的位置如下图所示:

在系统初始化时会扫描系统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的扫描过程在系统初始化中进行,详细的代码我们就不去分析了,这里从网上摘录了一段伪代码对其进行简单地描述:

For (channel = 0; channel

  /* 对一个适配器的每个通道中的设备进行识别 */

  …

  For (id=0; id

  /* 对一个通道中的每个ID对应设备进行识别 */

  …

    For (lun=1; lun

    /* 对一个ID对应设备的每个LUN进行识别 */

    …

    }

  }

}

在计算机系统启动过程中,操作系统会扫描默认的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_device就是对lun的抽象。下面对scsi_device中的重要域进行说明:

那么,什么又是Scsi_Host呢?在scsi middle level定义了scsi设备的数据结构,用于描述一个scsi的具体功能单元,其在scsi host中通过channel、id、lun进行寻址。

通过上述描述可以知道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_cmnd队列 */

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

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

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

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

在scsi总线对一个ID对应设备的每个LUN进行识别的过程中,scsi middle level会为每个lun生成一个scsi_device结构,实现的核心函数为scsi_probe_and_add_lun(),来自drivers/scsi/scsi_scan.c:

static int scsi_probe_and_add_lun(struct scsi_target *starget,

uint lun, int *bflagsp,

struct scsi_device **sdevp, int rescan,

void *hostdata)

{

struct scsi_device *sdev;

unsigned char *result;

int bflags, res = SCSI_SCAN_NO_RESPONSE, result_len = 256;

struct Scsi_Host *shost = dev_to_shost(starget->dev.parent);

/*

* The rescan flag is used as an optimization, the first scan of a

* host adapter calls into here with rescan == 0.

*/

sdev = scsi_device_lookup_by_target(starget, lun);

if (sdev) {

if (rescan || sdev->sdev_state != SDEV_CREATED) {

SCSI_LOG_SCAN_BUS(3, printk(KERN_INFO

"scsi scan: device exists on %s/n",

sdev->sdev_gendev.bus_id));

if (sdevp)

*sdevp = sdev;

else

scsi_device_put(sdev);

if (bflagsp)

*bflagsp = scsi_get_device_flags(sdev,

sdev->vendor,

sdev->model);

return SCSI_SCAN_LUN_PRESENT;

}

scsi_device_put(sdev);

} else

sdev = scsi_alloc_sdev(starget, lun, hostdata);

if (!sdev)

goto out;

result = kmalloc(result_len, GFP_ATOMIC |

((shost->unchecked_isa_dma) ? __GFP_DMA : 0));

if (!result)

goto out_free_sdev;

if (scsi_probe_lun(sdev, result, result_len, &bflags))

goto out_free_result;

if (bflagsp)

*bflagsp = bflags;

/*

* result contains valid SCSI INQUIRY data.

*/

if (((result[0] >> 5) == 3) && !(bflags & BLIST_ATTACH_PQ3)) {

/*

* For a Peripheral qualifier 3 (011b), the SCSI

* spec says: The device server is not capable of

* supporting a physical device on this logical

* unit.

*

* For disks, this implies that there is no

* logical disk configured at sdev->lun, but there

* is a target id responding.

*/

SCSI_LOG_SCAN_BUS(2, sdev_printk(KERN_INFO, sdev, "scsi scan:"

" peripheral qualifier of 3, device not"

" added/n"))

if (lun == 0) {

SCSI_LOG_SCAN_BUS(1, {

unsigned char vend[9];

unsigned char mod[17];

sdev_printk(KERN_INFO, sdev,

"scsi scan: consider passing scsi_mod."

"dev_flags=%s:%s:0x240 or 0x800240/n",

scsi_inq_str(vend, result, 8, 16),

scsi_inq_str(mod, result, 16, 32));

});

}

res = SCSI_SCAN_TARGET_PRESENT;

goto out_free_result;

}

/*

* Non-standard SCSI targets may set the PDT to 0x1f (unknown or

* no device type) instead of using the Peripheral Qualifier to

* indicate that no LUN is present.  For example, USB UFI does this.

*/

if (starget->pdt_1f_for_no_lun && (result[0] & 0x1f) == 0x1f) {

SCSI_LOG_SCAN_BUS(3, printk(KERN_INFO

"scsi scan: peripheral device type"

" of 31, no device added/n"));

res = SCSI_SCAN_TARGET_PRESENT;

goto out_free_result;

}

res = scsi_add_lun(sdev, result, &bflags);

if (res == SCSI_SCAN_LUN_PRESENT) {

if (bflags & BLIST_KEY) {

sdev->lockable = 0;

scsi_unlock_floptical(sdev, result);

}

}

out_free_result:

kfree(result);

out_free_sdev:

if (res == SCSI_SCAN_LUN_PRESENT) {

if (sdevp) {

if (scsi_device_get(sdev) == 0) {

*sdevp = sdev;

} else {

__scsi_remove_device(sdev);

res = SCSI_SCAN_NO_RESPONSE;

}

}

} else

scsi_destroy_sdev(sdev);

out:

return res;

}

传给该函数的参数是scsi_target类型,表示scsi总线上的一个scsi node。注意结合前面那个图观察,每个scsi target可能拥有多个lun,即多个scsi devie,而这个。scsi 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))));

而scsi_probe_and_add_lun函数就是在对一个ID对应设备的LUN进行识别的过程中向这个Scsi节点增加这个lun。函数内部还有一个Scsi_Host类型的内部变量shost,表示对一个scsi适配器(很多地方又称为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是否可以queue在host标记 */

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

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

short unsigned int max_sectors;

……

};

这里有一个重点字段,是scsi_host_template ,翻译成SCSI总线端口样板,表示scsi host操作接口方法:

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 */

};

比如,USB的scsi_host_template方法就定义如下:

struct scsi_host_template usb_stor_host_template = {

/* basic userland interface stuff */

.name =                        "usb-storage",

.proc_name =                "usb-storage",

.proc_info =                 proc_info,

.info =                         host_info,

/* command interface -- queued only */

.queuecommand =                queuecommand,

/* error and abort handlers */

.eh_abort_handler =              command_abort,

.eh_device_reset_handler =    device_reset,

.eh_bus_reset_handler =        bus_reset,

/* queue commands only, only one command per LUN */

.can_queue =                1,

.cmd_per_lun =                    1,

/* unknown initiator id */

.this_id =               -1,

.slave_alloc =                slave_alloc,

.slave_configure =         slave_configure,

/* lots of sg segments can be handled */

.sg_tablesize =                     SG_ALL,

/* limit the total size of a transfer to 120 KB */

.max_sectors =                  240,

/* merge commands... this seems to help performance, but

* periodically someone should test to see which setting is more

* optimal.

*/

.use_clustering =           1,

/* emulated HBA */

.emulated =                  1,

/* we do our own delay after a device or bus reset */

.skip_settle_delay =              1,

/* sysfs device attributes */

.sdev_attrs =                 sysfs_device_attr_list,

/* module management */

.module =                     THIS_MODULE

};

整个块设备驱动层的工作,其实就是一个东西:封装一个scsi_cmnd结构。在完成scsi_cmnd的封装后,SCSI 中间层通过调用scsi_host_template结构中定义的queuecommand函数将 SCSI 命令提交给 SCSI 底层驱动部分。

queuecommand函数,是一个 SCSI 命令队列处理函数,在 SCSI 底层驱动中,定义了queuecommand函数的具体实现。因此,SCSI 中间层,调用queuecommand函数实际上就是调用了底层驱动定义的queuecommand函数的处理实体,将 SCSI 命令提交给了各个厂家定义的 SCSI 底层驱动进行处理。这个过程和通用块设备层调用 SCSI 中间层的处理函数进行块请求处理的机制很相似,这也体现了 LINUX 内核代码具有很好的扩展性。

底层驱动接受到请求后,就要开始处理 SCSI 命令了,这一层和硬件关系紧密,所以这块代码一般都是由各个厂家自己实现。基本流程可概括为:从底层驱动维护的队列中,取出一个 SCSI 命令,封装成厂家自定义的请求格式,然后采用 DMA 或者其他方式,将请求提交给 SCSI TARGET 端,由 SCSI TARGET 端对请求处理,并返回执行结果给 SCSI 底层驱动层。

scsi总线驱动的初始化相关推荐

  1. *Linux下的USB总线驱动 u盘驱动分析*

    Linux下的USB总线驱动(三) u盘驱动分析 版权所有,转载请说明转自 http://my.csdn.net/weiqing1981127 https://www.xuebuyuan.com/13 ...

  2. linux驱动:设备-总线-驱动(以TI+DM8127中GPIO为例)

    一:说明:这次学习设备-总线-驱动是以TI+DM8127的GPIO为例 1.GPIO资源注册到omap_hwmod链表中 2.初始化GPIO 3.将GPIO注册到plarform层 4.将GPIO注册 ...

  3. SylixOS iMX6平台I2C总线驱动

    原理概述 I2C总线驱动概述 I2C总线驱动是I2C适配器的软件实现,提供I2C适配器与从设备间完成数据通信的能力,比如起始,停止,应答信号和MasterXfer的实现函数.驱动程序包含初始化I2C总 ...

  4. Linux总线驱动-02: struct bus_type 结构体

    http://blog.csdn.net/cppgp/article/details/6333359 本文测试系统为:Ubuntu 10.10 x86_64 2.6.35-24-generic 上节中 ...

  5. Linux下的USB总线驱动 mouse

    Linux下的USB总线驱动(03)--USB鼠标驱动 usbmouse.c USB鼠标驱动 usbmouse.c 原文链接:http://www.linuxidc.com/Linux/2012-12 ...

  6. Linux I2C子系统分析-I2C总线驱动

    在drivers/i2c/busses下包含各种I2C总线驱动,如S3C2440的I2C总线驱动i2c-s3c2410.c,使用GPIO模拟I2C总线的驱动i2c-gpio.c,这里只分析i2c-gp ...

  7. SCSI 总线和协议

    I/O 技术实现在计算机和存储设备之间的数据交换.实现从CPU 到存储系统的I/O通路的一个中间就是SCSI(Small Computer System Interface). 一.  I/O 通路 ...

  8. Exynos4412 IIC总线驱动开发(一)—— IIC 基础概念及驱动架构分析

    关于Exynos4412 IIC 裸机开发请看 :Exynos4412 裸机开发 -- IIC总线 ,下面回顾下 IIC 基础概念 一.IIC 基础概念 IIC(Inter-Integrated Ci ...

  9. Exynos4412 IIC总线驱动开发(二)—— IIC 驱动开发

    前面在Exynos4412 IIC总线驱动开发(一)-- IIC 基础概念及驱动架构分析 中学习了IIC驱动的架构,下面进入我们的驱动开发过程 首先看一张代码层次图,有助于我们的理解 上面这些代码的展 ...

最新文章

  1. esp32 camera_利用Phyphox和ESP32蓝牙制作欧姆表测电阻
  2. 一道让你拍案叫绝的算法题
  3. Java isfile()与exists()的区别
  4. 有趣的c语言程序Code,一个有趣的小程序
  5. 求一个简单的java线程代码,Java线程代码的实现方法
  6. [ZJOI2007]仓库建设(斜率优化)
  7. php正则替换imgsrc_php如何替换img中src内容
  8. public ServiceException() { super(); } public ServiceException(String message, Throwable cause,
  9. vue-cli 打包部署
  10. idea中新建javaWeb项目
  11. Android系统Camera录像过程分析
  12. Python金融大数据分析-蒙特卡洛仿真
  13. js原型继承的几种方式
  14. k8s高可用二进制部署
  15. BetaFlight模块设计之十一:GyroAcc任务分析
  16. 阿里云OSS使用Java上传文件
  17. SQL注入测试神器sqlmap
  18. simpread-PCB 基本布线规范与设计原则
  19. 网络安全是什么意思?网络安全产品又包含哪些?
  20. unity制作仿原神水面(1)——上色、造浪

热门文章

  1. 与物联网结合,ABB如何抢滩电力大数据?
  2. Android实战【可可爱爱一零一动植物志】(开发)
  3. 分销小程序开发教你如何分清分销商和代理商
  4. 计算机系女学霸,女学霸高考692分,未来想当“程序员”,霸气发言让男生无地自容...
  5. 录取人数逐年攀升,西交电信学部的8个CS院系实力如何?
  6. 《微服务设计》读书笔记(上)
  7. springboot 接入cas-client-core单点登录
  8. 数字媒体声音设计 第二章 声学基础知识
  9. 解决python3 UnicodeEncodeError: ‘gbk‘ codec can‘t encode character ‘\U0001f608‘ in position。。。
  10. CSS锥形渐变实现环形进度条