赞
踩
在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 middle level定义了scsi device的数据结构,用于描述一个scsi的具体功能单元,其在scsi host中通过channel、id、lun进行寻址。
在scsi host中可以存在多个channel,每个channel是一条完整的scsi总线,在scsi总线上可以连接多个scsi节点,每个节点采用id进行编号,编号的大小与具体的scsi specification相关,与总线层的驱动能力等因素相关。每个节点可以根据功能划分成多个lun,每个lun才是我们通常所说的scsi设备。这种逻辑可以采用如下的总线拓扑结构描述
通过上述描述可以知道scsi_device是对lun的抽象。下面对scsi_device中的重要域进行说明:
/* SCSI逻辑设备描述符 */ struct scsi_device { /* 所在的主机适配器 */ struct Scsi_Host *host; /* 该设备的请求队列的指针 */ struct request_queue *request_queue; /* 链入到所属主机适配器的SCSI设备链表 */ struct list_head siblings; /* 链入到所属目标节点的SCSI设备链表 */ struct list_head same_target_siblings; /* 已经派发给SCSI设备底层驱动的命令数 */ volatile unsigned short device_busy; /* SCSI命令队列 */ struct list_head cmd_list; /* queue of in use SCSI Command structures */ /* 链入所属主机适配器的饥饿链表的连接件 */ struct list_head starved_entry; /* 当前活动命令 */ struct scsi_cmnd *current_cmnd; /* 队列深度,即允许链入队列的命令数量 */ unsigned short queue_depth; /* 内嵌通用设备 */ struct device sdev_gendev; /* 内嵌类设备 */ struct class_device sdev_classdev; }
在scsi总线probe的过程中,scsi middle level会为每个lun抽象成scsi device,实现的核心函数为scsi_probe_and_add_lun()。
scsi target对scsi总线上的scsi node进行了抽象。每个scsi target可能拥有多个lun,即多个scsi devie。scsi target数据结构中的重要域定义如下:
/* SCSI目标节点描述符 */
struct scsi_target {
/* 如果没有IO,则为NULL。否则为指向正在进行IO的SCSI设备 */
struct scsi_device *starget_sdev_user;
/* 内嵌通用设备 */
struct device dev;
/* 所在通道号 */
unsigned int channel;
/* 目标节点的ID */
unsigned int id;
/* 如果为1,表示需要被添加 */
unsigned long create:1;
/* 用于传输层 */
unsigned long starget_data[0];
} __attribute__((aligned(sizeof(unsigned long))));
scsi host的语义很清晰,其描述了一个scsi总线控制器。在很多实际的系统中,scsi host为一块基于PCI总线的HBA或者为一个SCSI控制器芯片。每个scsi host可以存在多个channel,一个channel实际扩展了一条SCSI总线。每个channel可以连接多个scsi节点,具体连接的数量与 scsi总线带载能力有关。scsi host的重要域描述如下:
/* SCSI主机适配器描述符 */ struct Scsi_Host { /* 指向这个主机适配器的SCSI设备链表 */ struct list_head __devices; /* 分配SCSI命令的存储池 */ struct scsi_host_cmd_pool *cmd_pool; /* 用于保护free_list链表的锁 */ spinlock_t free_list_lock; /* 预先准备的SCSI命令结构的链表,如果从缓冲池中分配结构失败,则从这里分配。 */ struct list_head free_list; /* 饥饿设备链表 */ struct list_head starved_list; /* 主机编号,用于标识这个主机适配器 */ unsigned short host_no; /* 用于主机适配器的唯一标识号 */ unsigned int unique_id; /* 主机的SCSI ID */ int this_id; };
scsi middle level通过scsi_host_template接口调用scsi host的具体方法。在scsi host driver向middle level注册host对象的同时需要注册scsi_host_template方法,该方法被注册到scsi host对象中,一个典型的scsi_host_template实例如下:
PCI总线扫描时会扫描到SCSI主机适配器,假设主机适配器注册在/sys/devices/pci0000:00/0000:00:10.0,我们以mptspi驱动为例拉看看SCSI设备的初始化。
module_init(mptspi_init)注册mptspi驱动
mptspi_init(void) return pci_register_driver(&mptspi_driver); return __pci_register_driver(driver, THIS_MODULE); //drv类型是pci_driver drv->driver.name = drv->name; drv->driver.bus = &pci_bus_type; drv->driver.probe = pci_device_probe; drv->driver.remove = pci_device_remove; drv->driver.owner = drv->owner; drv->driver.kobj.ktype = &pci_driver_kobj_type; pci_init_dynids(&drv->dynids); driver_register(&drv->driver); return bus_add_driver(drv); struct bus_type * bus = get_bus(drv->bus); kobject_set_name(&drv->kobj, "%s", drv->name); drv->kobj.kset = &bus->drivers; // mptspi驱动注册到/sys/bus/pci/drivers kobject_register(&drv->kobj) driver_attach(drv); struct bus_type * bus = drv->bus; //遍历pci总线上的设备 list_for_each(entry, &bus->devices.list) struct device * dev = container_of(entry, struct device, bus_list); //调用驱动的probe函数,判断是否能和当前设备匹配 driver_probe_device(drv, dev); dev->driver = drv; // 对于mptspi驱动来说就是mptspi_probe error = drv->probe(dev); if (error) return error; // 将设备和驱动关联起来 device_bind_driver(dev); list_add_tail(&dev->driver_list, &dev->driver->devices); // 在/sys/bus/pci/drivers/mptspi目录下创建0000:00:10.0链接文件 // 链接到/sys/devices/pci0000:00/0000:00:10.0 sysfs_create_link(&dev->driver->kobj, &dev->kobj, kobject_name(&dev->kobj)); // 在/sys/devices/pci0000:00/0000:00:10.0目录下创建driver链接文件 // 链接到/sys/bus/pci/drivers/mptspi sysfs_create_link(&dev->kobj, &dev->driver->kobj, "driver"); module_add_driver(drv->owner, drv); driver_add_attrs(bus, drv); pci_populate_driver_dir(drv);
mptspi_init主要内容如下
(1)将mptspi驱动注册到/sys/bus/pci/drivers。
(2)调用驱动探测函数(mptspi_probe)去匹配pci总线上的设备,如果探测到匹配的设备,就调用device_bind_driver把设备和驱动对应起来。
mptspi_probe匹配SCSI主机适配器之后,就会依次调用scsi_host_alloc, scsi_add_host ,scsi_scan_host来初始化SCSI相关设备。
首先就是scsi_host_alloc
// 分配SCSI模型中的最底层主机适配器描述符 struct Scsi_Host *shost = scsi_host_alloc(&inia100_template, sizeof(ORC_HCS)); shost = kmalloc(sizeof(struct Scsi_Host) + privsize, gfp_mask); shost->host_no = scsi_host_next_hn++; shost->hostt = sht; shost->cmd_per_lun = sht->cmd_per_lun; /* 默认值,驱动中可能改写 */ shost->max_channel = 0; shost->max_id = 8; shost->max_lun = 8; /* Give each shost a default transportt */ shost->transportt = &blank_transport_template; /* 初始化内嵌通用设备 */ device_initialize(&shost->shost_gendev); kobj_set_kset_s(dev, devices_subsys); snprintf(shost->shost_gendev.bus_id, BUS_ID_SIZE, "host%d", shost->host_no); shost->shost_gendev.release = scsi_host_dev_release; /* 初始化内嵌类设备 */ class_device_initialize(&shost->shost_classdev); shost->shost_classdev.dev = &shost->shost_gendev; shost->shost_classdev.class = &shost_class; snprintf(shost->shost_classdev.class_id, BUS_ID_SIZE, "host%d", shost->host_no); /* 在proc中为主机适配器添加一个目录 */ scsi_proc_hostdir_add(shost->hostt);
分配主机适配器描述符之后,就会调用scsi_add_host来注册SCSI主机适配器设备
// 注册SCSI最底层的主机适配器设备
scsi_add_host(shost, &pdev->dev);
struct scsi_host_template *sht = shost->hostt;
/* 设置父设备 /sys/devices/pci0000:00/0000:00:10.0*/
if (!shost->shost_gendev.parent)
shost->shost_gendev.parent = dev ? dev : &platform_bus;
/* 将内嵌设备添加到系统中 名字host0,也就是/sys/devices/pci0000:00/0000:00:10.0/host0*/
device_add(&shost->shost_gendev);
set_bit(SHOST_ADD, &shost->shost_state);
class_device_add(&shost->shost_classdev);
scsi_sysfs_add_host(shost);
scsi_proc_host_add(shost);
struct scsi_host_template *sht = shost->hostt;
sprintf(name,"%d", shost->host_no);
create_proc_read_entry(name, S_IFREG | S_IRUGO | S_IWUSR, sht->proc_dir, proc_scsi_read, shost);
scsi_add_host的主要内容就是注册主机适配器,这里就是/sys/devices/pci0000:00/0000:00:10.0/host0
scsi_scan_host用于扫描SCSI主机适配器上的逻辑设备
// 扫描SCSI主机适配器上的逻辑设备 scsi_scan_host(shost); scsi_scan_host_selected(shost, SCAN_WILD_CARD, SCAN_WILD_CARD, SCAN_WILD_CARD, 0); for (channel = 0; channel <= shost->max_channel; channel++) scsi_scan_channel(shost, channel, id, lun, rescan);/* 扫描该通道 */ for (id = 0; id < shost->max_id; ++id) // id代表了target的序号 scsi_scan_target(shost, channel, order_id, lun, rescan); /* 先探测LUN0,目标节点必须响应对LUN0的扫描 */ res = scsi_probe_and_add_lun(shost, channel, id, 0, &bflags, &sdev, rescan, NULL); struct scsi_device *sdev = scsi_alloc_sdev(host, channel, id, lun, hostdata); struct scsi_device *sdev = kmalloc(sizeof(*sdev) + shost->transportt->device_size, GFP_ATOMIC); sdev->vendor = scsi_null_device_strs; sdev->model = scsi_null_device_strs; sdev->rev = scsi_null_device_strs; sdev->host = shost; sdev->id = id; sdev->lun = lun; sdev->channel = channel; sdev->sdev_state = SDEV_CREATED; // 给scsi_device分配请求队列 sdev->request_queue = scsi_alloc_queue(sdev); struct Scsi_Host *shost = sdev->host; // 这里会指定IO调度程序里的请求处理函数为scsi_request_fn q = blk_init_queue(scsi_request_fn, &sdev->sdev_lock); ... sdev->request_queue->queuedata = sdev; scsi_sysfs_device_initialize(sdev); // 初始化内嵌的通用设备 device_initialize(&sdev->sdev_gendev); kobj_set_kset_s(dev, devices_subsys); // sdev_gendev会注册到scsi_bus_type下 sdev->sdev_gendev.bus = &scsi_bus_type; sdev->sdev_gendev.release = scsi_device_dev_release; // 比如0:0:0:0 sprintf(sdev->sdev_gendev.bus_id,"%d:%d:%d:%d", sdev->host->host_no, sdev->channel, sdev->id, sdev->lun); class_device_initialize(&sdev->sdev_classdev); sdev->sdev_classdev.dev = &sdev->sdev_gendev; sdev->sdev_classdev.class = &sdev_class; snprintf(sdev->sdev_classdev.class_id, BUS_ID_SIZE, "%d:%d:%d:%d", sdev->host->host_no, sdev->channel, sdev->id, sdev->lun); scsi_sysfs_target_initialize(sdev); struct scsi_target *starget = NULL; struct Scsi_Host *shost = sdev->host; // 每个目标节点的LUN0会加入到shost->_devices链表 list_for_each_entry(device, &shost->__devices, siblings) if (device->id == sdev->id && device->channel == sdev->channel) // 如果存在相同目标节点的逻辑设备,则将此逻辑设备加入到LUN0的same_target_siblings链表 list_add_tail(&sdev->same_target_siblings, &device->same_target_siblings); sdev->scsi_level = device->scsi_level; starget = device->sdev_target; break; // scsi_target(中间设备)没有被注册,开始初始化 if (!starget) starget = kmalloc(size, GFP_ATOMIC); dev = &starget->dev; device_initialize(dev); kobj_set_kset_s(dev, devices_subsys); dev->parent = get_device(&shost->shost_gendev); sprintf(dev->bus_id, "target%d:%d:%d", shost->host_no, sdev->channel, sdev->id); starget->id = sdev->id; starget->channel = sdev->channel; create = starget->create = 1; sdev->scsi_level = SCSI_2; sdev->sdev_gendev.parent = &starget->dev; sdev->sdev_target = starget; list_add_tail(&sdev->siblings, &shost->__devices); return sdev; sreq = scsi_allocate_request(sdev, GFP_ATOMIC); /* 发送INQUIRY命令探测逻辑单元 */ scsi_probe_lun(sreq, result, &bflags); /* 根据规范,这个结果表示目标单元存在,但是没有物理设备。 */ if ((result[0] >> 5) == 3) goto out_free_result; /* 将逻辑设备添加到系统中 */ scsi_add_lun(sdev, result, &bflags); /* 根据INQUIRY响应数据来设置SCSI设备描述符各个域 */ sdev->inquiry = kmalloc(sdev->inquiry_len, GFP_ATOMIC); sprintf(sdev->devfs_name, "scsi/host%d/bus%d/target%d/lun%d",sdev->host->host_no, sdev->channel,sdev->id, sdev->lun); scsi_device_set_state(sdev, SDEV_RUNNING); /* 将scsi设备及对应的目标节点添加到sysfs文件系统,并创建对应的属性文件 */ scsi_sysfs_add_sdev(sdev); struct scsi_target *starget = sdev->sdev_target; struct Scsi_Host *shost = sdev->host; create = starget->create; starget->create = 0; // 注册目标节点,只需要在探测LUN0的时候注册 if (create) // 添加到/sys/devices/pci0000:00/0000:00:10.0/host0,名字是target0:0:0 device_add(&starget->dev); scsi_device_set_state(sdev, SDEV_RUNNING) // 注册逻辑设备 //添加到/sys/devices/pci0000:00/0000:00:10.0/host0/target0.0.0 ,名字是0:0:0 device_add(&sdev->sdev_gendev); class_device_add(&sdev->sdev_classdev); for (i = 0; scsi_sysfs_sdev_attrs[i]; i++) struct device_attribute * attr = attr_changed_internally(sdev->host, scsi_sysfs_sdev_attrs[i]); device_create_file(&sdev->sdev_gendev, attr); if (res == SCSI_SCAN_LUN_PRESENT) {/* LUN0有逻辑单元 */ /* 通过REPORT LUN命令探测逻辑单元数量,并对每个逻辑单元进行探测 */ if (scsi_report_lun_scan(sdev, bflags, rescan) != 0) /* 探测失败,从1到最大编号进行依次探测 */ scsi_sequential_lun_scan(shost, channel, id, bflags, res, sdev->scsi_level, rescan); } else if (res == SCSI_SCAN_TARGET_PRESENT) { scsi_sequential_lun_scan(shost, channel, id, BLIST_SPARSELUN, SCSI_SCAN_TARGET_PRESENT, SCSI_2, rescan); }
scsi_scan_host扫描主机适配器的每一个通道里的每一个taregt目标设备,对每一个目标设备,先调用scsi_probe_and_add_lun探测LUN0设备,如果目标节点必须相应对LUN0探测的响应,scsi_probe_and_add_lun的主要内容如下:
(1)给scsi_device分配请求队列,后面通用磁盘设备的请求队列都是这个请求队列。
(2)每个目标节点的LUN0都会加入到shost->_devices链表,同时会初始化scsi_target。
(3)发送INQUIRY命令探测逻辑单元,如果LUN0不存在就退出。
(4)注册目标设备,探测LUN0的时候就是target0,这里会注册到/sys/devices/pci0000:00/0000:00:10.0/host0/target0.0.0。
(5)注册LUN0逻辑设备,注册到/sys/devices/pci0000:00/0000:00:10.0/host0/target0.0.0/0.0.0.0。
(6)如果LUN0有逻辑单元,通过REPORT LUN命令探测逻辑单元数量,并对每个逻辑单元进行探测。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。