Linux设备模型初始化——SCSI子系统初始化

Linux中scsi驱动框架

在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 host的抽象

在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 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 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_host_template

scsi middle level通过scsi_host_template接口调用scsi host的具体方法。在scsi host driver向middle level注册host对象的同时需要注册scsi_host_template方法,该方法被注册到scsi host对象中,一个典型的scsi_host_template实例如下:
在这里插入图片描述

SCSI 设备扫描

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命令探测逻辑单元数量,并对每个逻辑单元进行探测。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值