目录
添加适配器到系统
SCSI低层驱动是面向主机适配器的,低层驱动被加载时,首先要添加主机适配器。主机适配器可以在PCI子系统完成ID匹配时添加,或者通过手工方式添加。所有基于硬件PCI接口的主机适配器都采用前一种方式,而UNH iSCSI启动器采用的是后一种方式。
添加主机适配器包括两部分的内容,为主机适配器分配数据结构,将主机适配器添加到系统。SCSI中间层为此提供了两个公共函数:scsi_host_alloc和scsi_add_host。
在低层驱动,主机适配器(控制器的驱动代码中调用,下面以x86 pm8001控制器驱动为例子)
kernel\drivers\scsi\pm8001\pm8001_init.c
static struct scsi_host_template pm8001_sht = {
.module = THIS_MODULE,
.name = DRV_NAME,
.queuecommand = sas_queuecommand,
.target_alloc = sas_target_alloc,
.slave_configure = sas_slave_configure,
.scan_finished = pm8001_scan_finished,
.scan_start = pm8001_scan_start,
.change_queue_depth = sas_change_queue_depth,
.bios_param = sas_bios_param,
.can_queue = 1,
.this_id = -1,
.sg_tablesize = SG_ALL,
.max_sectors = SCSI_DEFAULT_MAX_SECTORS,
.use_clustering = ENABLE_CLUSTERING,
.eh_device_reset_handler = sas_eh_device_reset_handler,
.eh_bus_reset_handler = sas_eh_bus_reset_handler,
.target_destroy = sas_target_destroy,
.ioctl = sas_ioctl,
.shost_attrs = pm8001_host_attrs,
.track_queue_depth = 1,
};
static int pm8001_pci_probe(struct pci_dev *pdev,
const struct pci_device_id *ent)
{
struct Scsi_Host *shost = NULL;
shost = scsi_host_alloc(&pm8001_sht, sizeof(void *));
}
scsi_host_alloc()
下面介绍函数scsi_host_alloc()代码(摘自文件drivers/scsi/hosts.c)
struct Scsi_Host {
struct list_head __devices;
struct list_head __targets;
struct scsi_host_cmd_pool *cmd_pool;
spinlock_t free_list_lock;
struct list_head free_list; /* backup store of cmd structs */
struct list_head starved_list;
spinlock_t default_lock;
spinlock_t *host_lock;
struct mutex scan_mutex;/* serialize scanning activity */
struct list_head eh_cmd_q;
struct task_struct * ehandler; /* Error recovery thread. */
struct completion * eh_action; /* Wait for specific actions on the host. */
wait_queue_head_t host_wait;
struct scsi_host_template *hostt;
struct scsi_transport_template *transportt;
union {
struct blk_queue_tag *bqt;
struct blk_mq_tag_set tag_set;
};
atomic_t host_busy; /* commands actually active on low-level */
atomic_t host_blocked;
unsigned int host_failed; /* commands that failed. protected by host_lock */
unsigned int host_eh_scheduled; /* EH scheduled without command */
unsigned int host_no; /* Used for IOCTL_GET_IDLUN, /proc/scsi et al. */
unsigned long last_reset;
unsigned int max_channel;
unsigned int max_id;
u64 max_lun;
unsigned int unique_id;
unsigned short max_cmd_len;
int this_id;
int can_queue;
short cmd_per_lun;
short unsigned int sg_tablesize;
short unsigned int sg_prot_tablesize;
unsigned int max_sectors;
unsigned long dma_boundary;
unsigned nr_hw_queues;
unsigned long cmd_serial_number;
unsigned active_mode:2;
unsigned unchecked_isa_dma:1;
unsigned use_clustering:1;
unsigned host_self_blocked:1;
unsigned reverse_ordering:1;
unsigned tmf_in_progress:1;
unsigned async_scan:1;
unsigned eh_noresume:1;
unsigned no_write_same:1;
unsigned use_blk_mq:1;
unsigned use_cmd_list:1;
unsigned short_inquiry:1;
struct workqueue_struct *work_q;
struct workqueue_struct *tmf_work_q;
unsigned no_scsi2_lun_in_cdb:1;
unsigned int max_host_blocked;
unsigned int prot_capabilities;
unsigned char prot_guard_type;
unsigned long base;
unsigned long io_port;
unsigned char n_io_port;
unsigned char dma_channel;
unsigned int irq;
enum scsi_host_state shost_state;
struct device shost_gendev, shost_dev;
struct list_head sht_legacy_list;
void *shost_data;
struct device *dma_dev;
unsigned long hostdata[0] /* Used for storage of host specific stuff */
__attribute__ ((aligned (sizeof(unsigned long))));
};
scsi_host_alloc函数分配一个新的Scsi_Host描述符并进行基本的初始化。第一个参数为指向SCSI主机适配器模板的指针;第二个参数为驱动私有数据分配的额外字节数。返回值为指向Scsi_Host描述符的指针;或者在失败时返回NULL。一次性分配SCSI主机适配器的公有部分和私有部分的情况如下图:
一次性分配SCSI主机适配器的公有部分和私有部分
struct Scsi_Host *scsi_host_alloc(struct scsi_host_template *sht, int privsize)
{
struct Scsi_Host *shost;
gfp_t gfp_mask = GFP_KERNEL;
shost = kzalloc(sizeof(struct Scsi_Host) + privsize, gfp_mask);
if (!shost)
return NULL;
}
Scsi_Host的空间一次性分配。使用kzalloc (GFP_KERNEL)
struct Scsi_Host *scsi_host_alloc(struct scsi_host_template *sht, int privsize)
{
shost->host_no = atomic_inc_return(&scsi_host_next_hn) - 1;
}
Scsi_Host是一个庞大的数据结构,它的初始化也比较烦琐,所幸大多数域都可以根据“主机适配器模板”赋值,或者先赋一个默认的值,等待以后再覆写。需要特别说明的是host_no域,即主机适配器编号。主机适配器是在系统范围内编号的,或者说,系统中的主机适配器统一进行编号。在Linux内核中有一个全局变量scsi_host_next_hn,表示下一个新的主机适配器的编号。它的初值为零,每次发现一个新的主机适配器,将全局变量scsi_host_next_hn的值赋给host_no域,然后再递增。
struct Scsi_Host *scsi_host_alloc(struct scsi_host_template *sht, int privsize)
{
/* These three are default values which can be overridden */
shost->max_channel = 0;
shost->max_id = 8;
shost->max_lun = 8;
}
将max_channel初始化为0,将max_id和max_lun初始化为8,这些域都被先赋予一些默认的值,在本函数返回后,低层驱动可以对这些域进行覆写。
我们关注sysfs文件系统有关的代码。先把主机适配器的子目录树画在图:
SCSI总线的sysfs
主机适配器子目录树下包含主机适配器自身的属性文件,以及目标节点子树和更下级的SCSI设备子树。
标记①为主机适配器目录名host#,其中#为主机适配器编号;
标记②为目标节点目录名target#:#:#,其中#:#:#分别为主机适配器编号、通道编号和ID编号;
标记③为SCSI设备目录名#:#:#:#,其中#:#:#分别为主机适配器编号、通道编号、ID编号,以及逻辑单元编号。
struct Scsi_Host {
struct device shost_gendev, shost_dev;
}
主机适配器属于shost_class类(类名为scsi_host),为了和类关联起来,在左下部分看到有同名的子目录树。因此,每个主机适配器定义了两个内嵌设备描述符,分别和这两个灰色标记的目录对应。对应上层host#目录的内嵌设备描述符为shost_gendev域,被称为内嵌通用设备;对应下层host#目录的内嵌设备描述符为shost_dev域,被称为内嵌类设备。
struct Scsi_Host *scsi_host_alloc(struct scsi_host_template *sht, int privsize)
{
//初始化内嵌通用设备
device_initialize(&shost->shost_gendev);
dev_set_name(&shost->shost_gendev, "host%d", shost->host_no);
shost->shost_gendev.bus = &scsi_bus_type;
shost->shost_gendev.type = &scsi_host_type;
//初始化内嵌类设备
device_initialize(&shost->shost_dev);
shost->shost_dev.parent = &shost->shost_gendev;
shost->shost_dev.class = &shost_class;
dev_set_name(&shost->shost_dev, "host%d", shost->host_no);
shost->shost_dev.groups = scsi_sysfs_shost_attr_groups;
}
初始化内嵌通用设备,初始化内嵌类设备。它们之间是相互关联的,内嵌通用设备为内嵌类设备的父设备shost->shost_dev.parent = &shost->shost_gendev。
struct Scsi_Host *scsi_host_alloc(struct scsi_host_template *sht, int privsize)
{
shost->ehandler = kthread_run(scsi_error_handler, shost,"scsi_eh_%d", shost->host_no);
}
在Linux系统中,每个主机适配器都有一个SCSI错误恢复内核线程。启动之,线程名为scsi_eh_#,其中#为主机适配器编号,线程执行的主体函数为scsi_error_handler。关于更详细的描述,参见后面的SCSI错误恢复。
struct Scsi_Host *scsi_host_alloc(struct scsi_host_template *sht, int privsize)
{
scsi_proc_hostdir_add(shost->hostt);
}
调用scsi_proc_hostdir_add,在proc文件系统中为这个主机适配器添加一个目录
scsi_add_host()
下面介绍函数scsi_add_host()代码(摘自文件include/scsi/scsi_host.h)
static inline int __must_check scsi_add_host(struct Scsi_Host *host,
struct device *dev)
{
return scsi_add_host_with_dma(host, dev, dev);
}
scsi_add_host函数有两个参数。第一个为指向主机适配器描述符的指针,第二个指向这个SCSI主机适配器在驱动模型中的父设备的device描述符,它决定了这个SCSI主机适配器在sysfs文件系统中的位置,可以为NULL。但对于PCI-SCSI驱动,通常将它设置为对应PCI设备的内嵌驱动模型设备。例如某个SCSI主机适配器对应sysfs文件系统的/sys/devices/pci0000:00/0000:00:07.1/host0/目录。成功返回0,错误返回非0值。
函数scsi_add_host_with_dma()代码(摘自文件drivers/scsi/hosts.c)
int scsi_add_host_with_dma(struct Scsi_Host *shost, struct device *dev,
struct device *dma_dev)
{
error = scsi_setup_command_freelist(shost);
}
在调用scsi_add_host_with_dma时增加了一个参数:主机适配器的DMA设备,它只是用于虚拟主机适配器环境下,在我们这里不需要给予太多关注。对于PCI-SCSI设备,传入的就是对应PCI设备的内嵌驱动模型设备。
scsi_setup_command_freelist函数为主机适配器准备用于分配SCSI命令结构和感测缓冲区的存储池结构,以及准备SCSI命令结构的后备仓库链表。
int scsi_add_host_with_dma(struct Scsi_Host *shost, struct device *dev,
struct device *dma_dev)
{
if (!shost->shost_gendev.parent)
shost->shost_gendev.parent = dev ? dev : &platform_bus;
}
确定主机适配器在sysfs中的位置。我们说过,一般情况下,主机适配器会被放在引出它的PCI设备之下,主机适配器驱动一般在调用scsi_add_host函数时会传入对应的pci_dev描述符中的内嵌device地址,在这里赋值给主机适配器内嵌通用设备的parent域。当然,如果传入的dev参数为NULL,我们就把它放在sys/devices/platform/目录下,platform设备是某些特定平台专有的外围设备。
int scsi_add_host_with_dma(struct Scsi_Host *shost, struct device *dev,
struct device *dma_dev)
{
error = device_add(&shost->shost_gendev);
error = device_add(&shost->shost_dev);
error = scsi_sysfs_add_host(shost);
}
主机适配器的内嵌通用设备和内嵌类设备的初始化已经全部完成。将SCSI子系统的驱动模型设备关系整理如上图所示,为方便理解,其中还包含了后面才会添加的目标节点和SCSI设备。device_add(&shost->shost_gendev);将主机适配器的内嵌通用设备添加到系统中。device_add(&shost->shost_dev);将主机适配器的内嵌类设备添加到系统中。scsi_sysfs_add_host(shost)调用scsi_sysfs_add_host将主机适配器添加到子系统,包括为主机适配器属性添加对应的文件。
int scsi_add_host_with_dma(struct Scsi_Host *shost, struct device *dev,
struct device *dma_dev)
{
scsi_proc_host_add(shost);
}
用于proc文件系统为该主机适配器创建目录项。