scsi子系统报告()
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
DFT
SCSI子系统报告
设备驱动分析
彭成章路新疆武文斌
2010-7-7
本次报告基于对SCSI子系统源代码(内核版本为2.6.24),SCSI体系结构模型【1】,命令模型【2】及其他相关文档的理解。
目录
第1章前言 (3)
1.1 实验环境 (3)
1.2 阅读和理解代码的方法 (3)
1.3 阅读过的文档 (3)
1.4 将讨论哪些内容和不讨论哪些内容 (3)
第2章 SCSI子系统概述 (3)
2.1 整体概述 (3)
2.2 上层 (4)
2.3 中层 (5)
2.4 下层 (5)
2.5 SCSI-3架构 (5)
2.5.1 SCSI-3客户-服务器模型 (6)
2.5.2 SCSI端口 (8)
2.5.3 SCSI设备 (8)
2.5.4 SCSI通信模型 (9)
第3章 Linux SCSI 子系统关键数据结构 (9)
3.1 SCSI HOST (9)
3.1.1 scsi host的抽象 (12)
3.2 SCSI DEVICE (13)
3.2.1 scsi device的抽象 (13)
3.3 SCSI TARGET (15)
3.3.1 scsi target的抽象 (15)
3.4 request queue (15)
3.5 request (18)
3.6 others (20)
3.6.1 Scsi host driver (20)
3.6.2 Linux中磁盘扫描流程描述 (21)
3.6.3 low-level接口方法——scsi_host_template (23)
3.6.4 scsi_scan_host函数 (25)
3.6.5 scsi_request_fn函数 (25)
3.6.6 scsi_dispatch_cmd函数 (26)
3.6.7 scsi设备扫描过程描述 (26)
3.6.8 Scsi Disk驱动IO回调路径分析 (28)
第4章 SCSI子系统处理流程分析 (30)
4.1 SCSI启动过程分析 (30)
4.2 SCSI 命令处理分析 (30)
4.2.1 简单命令 (30)
4.2.2 scsi设备读写过程 (30)
4.2.3 读命令 (31)
4.2.4 写命令 (31)
SCSI子系统报告提纲
第1章前言
1.1 实验环境
1.2 阅读和理解代码的方法
1.3 阅读过的文档
1.4 将讨论哪些内容和不讨论哪些内容
第2章 SCSI子系统概述
图2显示了 SCSI 子系统在 Linux 内核中的位置。
内核的顶部是系统调用接口,处理用户空间调用到内核中合适的目的地的路由(例如 open、read 或 write)。
而虚拟文件系统(VFS) 是内核中支持的大多数文件系统的抽象层。
它负责将请求路由到合适的文件系统。
大多数文件系统都通过缓冲区缓存来相互通信,这种缓存通过缓存最近使用的数据来优化对物理设备的访问。
接下来是块设备驱动器层,它包括针对底层设备的各种块驱动器。
SCSI 子系统是这种块设备驱动器之一。
图2 SCSI 子系统在 Linux 内核中的位置
2.1 整体概述
SCSI 子系统与 Linux 内核中的其他主流子系统不同,SCSI 子系统是一种分层的架构,共分为三层。
顶部的那层叫做较高层,代表的是内核针对SCSI 和主要设备类型的驱动器的最高接口。
接下来的是中间层,也称为公共层或统一层。
在这一层包含 SCSI 堆栈的较高层和较低层的一些公共服务。
最后是较低层,代表的是适用于 SCSI 的物理接口的实际驱动器。
(如图2)
图 2.1 Linux SCSI 子系统的分层架构
2.2 上层
SCSI 子系统的较高层代表的是内核(设备级)最高级别的接口。
它由一组驱动器组成,比如块设备(SCSI 磁盘和 SCSI CD-ROM)和字符设备(SCSI 磁带和 SCSI generic)。
较高层接受来自上层(比如 VFS)的请求并将其转换成SCSI 请求。
较高层负责完成 SCSI命令并将状态信息通知上层。
SCSI 磁盘驱动器在 ./linux/drivers/scsi/sd.c 内实现。
SCSI 磁盘驱动器通过调用 register_blkdev(作为块驱动器)进行自初始化并通过
scsi_register_driver 提供一组函数以表示所有 SCSI 设备。
其中 sd_probe 和 sd_init_command 这两个函数很重要。
只要有新的 SCSI设备附加到系统,SCSI 中间层就会调用 sd_probe 函数。
sd_probe 函数可决定此设备是否由SCSI 磁盘驱动器管理,如果是,就创建新的 scsi_disk 结构来表示它。
sd_init_command 函数将来自文件系统层的请求转变成 SCSI 读或写命令(为完成这个 I/O 请求,sd_rw_intr 会被调用)。
SCSI 磁带驱动器在 ./linux/drivers/scsi/st.c 内实现。
磁带驱动器是顺序存取设备,会通过 register_chrdev_region 将自身注册为字符设备。
SCSI 磁带驱动器还提供了一个 probe 函数,称为 st_probe。
该函数会创建一种新磁带设备并将其添加到称为 scsi_tapes 的向量。
SCSI 磁带驱动器的独特之处在于,如果可能,它可以直接从用户空间执行 I/O 传输。
否则,数据会通过驱动器缓冲被分段。
SCSI CD-ROM 驱动器在 ./linux/drivers/scsi/sr.c 内实现。
CD-ROM 驱动器是另一种块设备并为 SCSI 磁盘驱动器提供类似的函数集。
sr_probe 函数可用来创建 scsi_sd 结构以表示 CD-ROM 设备,并用 register_cdrom 注册此 CD-ROM。
SCSI 磁带驱动器还会导出 sr_init_command,以将请求转换成SCSI CD-ROM 读或写请求。
SCSI generic 驱动器在 ./linux/drivers/scsi/sg.c 内实现。
该驱动器允许用户应用程序向设备发送 SCSI 命令(比如格式化、模式感知或诊断命令)。
通过 sg3utils 包还可以从用户空间利用 SCSI generic 驱动器。
这个用户空间包包括多种实用工具,可用来发送 SCSI 命令和解析这些命令的响应。
2.3 中层
SCSI 中间层是 SCSI 较高层和较低层的公共服务层(可以
在 ./linux/drivers/scsi/scsi.c 内部分地实现)。
它提供了很多可供较高层和较低层驱动器使用的函数,因而可以充当这两层间的连接层。
中间层很重要,原因是它抽象化了较低层驱动器(LLD)的实现,可以
在 ./linux/drivers/scsi/hosts.c 中部分地实现。
这意味着可以以同样的方式使用带不同接口的 Fibre Channel 主机总线适配器(HBA)。
低层驱动器注册和错误处理都由 SCSI 中间层提供。
中间层还提供了较高层和较低层间的 SCSI 命令排队。
SCSI 中间层的一个重要功能是将来自较高层的命令请求转换成 SCSI 请求。
它也负责管理特定于 SCSI 的错误恢复。
中间层可以连接 SCSI 子系统的较高层和较低层。
它接受对 SCSI 事务的请求并对这些请求进行排队以便处理
(如 ./linux/drivers/scsi/scsi_lib.c 中所示)。
当这些命令完成后,它接受来自 LLD 的 SCSI 响应并通知较较高层此请求已经完成。
中间层最重要的职责之一是错误和超时处理。
如果 SCSI 命令没有在合理的时间内完成或者 SCSI 请求返回错误,中间层就会管理错误或重新发送此请求。
中间层还可管理较高层恢复,比如请求 HBA (LLD) 或 SCSI 设备重置。
SCSI 错误和超时处理程序在 ./linux/drivers/scsi/scsi_error.c 内实现。
2.4 下层
最低层的是一组驱动器,称为 SCSI低层驱动器。
它们是一些可与物理设备(比如 HBA)链接的特定驱动器。
LLD 提供了自公共中间层到特定于设备的HBA 的一种抽象。
每个 LLD 都提供了到特定底层硬件的接口,但所使用的到中间层的接口却是一组标准接口。
较低层包含大量代码,原因是它要负责处理各种不同的 SCSI 适配器类型。
例如,Fibre Channel 协议包含了针对 Emulex 和 QLogic 的各种适配器的 LLD。
面向 Adaptec 和 LSI 的 SAS 适配器的 LLD 也包括在内。
2.5 SCSI-3架构
SCSI-3指令协议:这包含了对于所有设备来说最主要的指令,同时也包括特定设备的指令,即对给定类别的设备来说是唯一的。
2.5.1 SCSI-3客户-服务器模型
SCSI-3结构继承了客户-服务器关系模型的基础,在这里客户可以直接发服务请求给服务器,然后服务器响应客户的请求。
在SCSI环境里,一个启动器-目标器的概念用于实现客户-服务器模型。
在SCSI-3的客户-服务器模型中,一个特定的SCSI设备作为一个SCSI目标器设备,或一个SCSI启动器设备,或一个SCSI 目标器/启动器设备。
每一个设备都会实现以下功能。
SCSI启动器设备:发出指令到SCSI 目标器设备进行任务描述。
一个SCSI主机适配器就是一个启动器的例子。
SCSI目标器设备:执行SCSI 启动器发送过来的指令并完成任务。
典型地,一个SCSI外围设备就扮演着一个目标器设备的角色。
但是,在特定的实现里面,主机适配器也可以是一个目标器设备。
展示了SCSI-3的客户-服务器模型,在这里一个SCSI启动器,或者说一个客户,发送一个请求给SCSI目标器或者说一个服务器。
然后这个目标器执行任务请求,并且使用协议服务接口发送输出信息给启动器。
2.5.2 SCSI端口
2.5.3 SCSI设备
SCSI设备(SCSI Device)启动器可能是SCSI启动器设备,SCSI目标器设备,还可能既是SCSI启动器设备又是目标器设备(SCSI启动器和目标器设备的组合)。
用于发起命令或任务管理操作的设备称为SCSI启动器设备,在SCSI启动器设备中至少包含一个SCSI启动器端口;用于处理命令、完成任务管理操作的设备称为SCSI目标器设备,在SCSI目标器设备中至少包含一个逻辑单元和一个SCSI目标端口。
每个SCSI设备都有一个设备名,而且该设备名应该是全局唯一的。
SCSI设备名用来标识一个SCSI设备,因而不应改变。
2.5.4 SCSI通信模型
第3章 Linux SCSI 子系统关键数据结构
SCSI 子系统涉及的数据结构较多,这里挑选我们已经理解,并且使用频率较多的几个数据结构。
对每一个数据结构,都将说明其使用场景。
3.1 SCSI HOST
初始化中,在scsi host driver中首先需要定义一个描述host的结构。
这个结构实际上是Scsi_Host的派生类,继承和扩展了Scsi_Host的功能。
vscs i_host_s是virtual scsi host driver中定义的结构。
typedef struct vscsi_host_s {
__u32 host_no; /* host number, Majo r no */
struct Scsi_Host *scsi_host; /* scsi host object */
/* vscsi device array */
struct vscsi_device_s devices[MAX_CHANNEL][CHANNEL_DEV_NU M];
atomic_t dev_cnt; /* vscsi device coun ters */
struct semaphore host_lock; /* vscsi device access lo ck */
/* vscsi command queue */
struct bscsi_cmd_arr_s vscsi_cmd_queue;
}vscsi_host_t;
<!--[if !supportLists]--> <!--[endif]-->scsi_host 为scsi middle level分配的scsi host对象指针。
<!--[if !supportLists]--> <!--[endif]-->devices 为vscsi_host控制器管理的设备,其中MAX_CHANNEL为vscsi_
host最大的通道数,CHANNEL_DEV_NUM为vscsi_host最大的lu
n数。
<!--[if !supportLists]--> <!--[endif]-->dev_cnt 描述了当前vscsi_host管理的设备数量。
<!--[if !supportLists]--> <!--[endif]-->host_lock 为设备访问锁。
<!--[if !supportLists]--> <!--[endif]-->Vscsi_cmd _queue为vscsi_host的命令队列。
scsi middle level可以直
接将scsi命令挂在该命令队列上。
Scsi host的初始化过程如下:
<!--[if !supportLists]-->1、 <!--[endif]-->理所当然,第一步需要初始化scsi host结构中的所有成员,例如vscsi_cmd_qu
eue等。
<!--[if !supportLists]-->2、 <!--[endif]-->通过scsi_host_ alloc函数让内核分配一个scsi host,调用方式为:shost = s
csi_host_alloc(&scsi_host_template, sizeof(struct bscsi_
host_s))。
其中scsi_host_template为模版对象,即scsi host
的属性以及接口方法。
<!--[if !supportLists]-->3、 <!--[endif]-->通过scsi_add_h ost函数将新分配的scsi host添加到系统中。
调用如下:scsi_
add_host(shost, NULL)。
<!--[if !supportLists]-->4、 <!--[endif]-->调用scsi_scan_ host(shost)扫描scsi host。
scsi host的语义很清晰,其描述了一个scsi总线控制器。
在很多实际的系统中,scsi host为一块基于PCI总线的HBA或者为一个SCSI控制器芯片。
每个scsi host可以存在多个channel,一个channel实际扩展了一条SC SI总线。
每个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 tr ansport方法 */
unsigned int host_busy; /* scsi host忙标记 */
unsigned int host_failed; /* comman ds that failed. */
unsigned int max_id; /* 最大的sc si node数量 */
unsigned int max_lun; /* 最大的lun数量 */
unsigned int max_channel; /* 最大的c hannel数量 */
unsigned char max_cmd_len; /* scsi命令的长度 */
int this_id; / * scsi host在总线的id */
int can_queue; /* s csi cmd是否可以queue在host标记 */
short cmd_per_lun; / * 每个lun可以queue多少scsi cmd */
short unsigned int sg_tablesize; /* scatter -gather table大小 */
short unsigned int max_sectors;
……
};
3.1.1 scsi host的抽象
scsi host的语义很清晰,其描述了一个scsi总线控制器。
在很多实际的系统中,scsi host为一块基于PCI总线的HBA或者为一个SCSI控制器芯片。
每个scsi host可以存在多个channel,一个channel实际扩展了一条SC SI总线。
每个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 tr ansport方法 */
unsigned int host_busy; /* scsi host忙标记 */
unsigned int host_failed; /* comman ds that failed. */
unsigned int max_id; /* 最大的sc si node数量 */
unsigned int max_lun; /* 最大的lun数量 */
unsigned int max_channel; /* 最大的c hannel数量 */
unsigned char max_cmd_len; /* scsi命令的长度 */
int this_id; / * scsi host在总线的id */
int can_queue; /* s csi cmd是否可以queue在host标记 */
short cmd_per_lun; / * 每个lun可以queue多少scsi cmd */
short unsigned int sg_tablesize; /* scatter -gather table大小 */
short unsigned int max_sectors;
……
};
3.2 SCSI DEVICE
初始化时,在scsi host驱动中,Scsi host控制的每个scsi设备都有一个对象。
Virtual scsi driver中采用struct vscsi_device_s结构来描述。
在scsi host scan的过程中,驱动程序会扫描scsi host的每个channe l、lun,然后回自动probe每个scsi device。
在virtual scsi host driver 中,用户可以手动的将一个设备添加到scsi host中,并且为其分配channl e,id,lun。
添加scsi device的接口函数为__scsi_add_device,实现如下:
sdev = __scsi_add_device(vscsi_host->scsi_host, channel, id, lun, &v scsi_host);
bscsi_host->scsi_host为已经存在的scsi host对象。
channel, id, lun为新添加的scsi device标识。
vscsi_host为vscsi host的对象,将该scsi host对象告诉新创建的scsi d evice。
通过__scsi_add_device函数,能够在scsi middle level创建一个scsi device,并且建立起scsi device与scsi host之间的关系。
3.2.1 scsi device的抽象
在scsi middle level定义了scsi device的数据结构,用于描述一个sc si的具体功能单元,其在scsi host中通过channel、id、lun进行寻址。
在scsi host中可以存在多个channel,每个channel是一条完整的scsi总线,在scsi总线上可以连接多个scsi节点,每个节点采用id进行编号,编号的大小与具体的scsi specification相关,与总线层的驱动能力等因素相关。
每个节点可以根据功能划分成多个lun,每个lun才是我们通常所说的
scsi设备。
这种逻辑可以采用如下的总线拓扑结构描述:
通过上述描述可以知道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 comman d 队列 */
struct scsi_cmnd *current_cmnd; /* 当前执行的命令 */
unsigned int id, lun, channel; /* SCSI设备的标识 */
void *hostdata; /* 通常指向low-level driver定义的scsi device */
…
} __attribute__((aligned(sizeof(unsigned long))));
在scsi总线probe的过程中,scsi middle level会为每个lun抽象成scsi device,实现的核心函数为scsi_probe_and_add_lun()。
3.3 SCSI TARGET
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))));
3.3.1 scsi target的抽象
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))));
3.4 request queue
struct request_queue{
...
/*自旋锁,保护队列结构体*/
spinlock_t __queue_lock;
spinlock_t* queue_lock;
struct kobject kobj;/*队列kobject*/
/*队列设置*/
unsigned long nr_requests;/*最大的请求数量*/
unsigned int nr_congestion_on;
unsigned int nr_congestion_off;
unsigned int nr_batching;
unsigned short max_sectors;/*最大扇区数*/
unsigned short max_hw_sectors;
unsigned short max_phys_sectors;/*最大的段数*/
unsigned short max_hw_segments;
unsigned short hardsect_size;/*硬件扇区尺寸*/
unsigned int max_segment_size;/*最大的段尺寸*/
unsigned long seg_boundary_mask;/*段边界掩码*/
unsigned int dma_alignment;/*DMA传送内存对齐限制*/
struct blk_queue_tag* queue_tags;
atomic_t refcnt;/*引用计数*/
unsigned int in_flight;
unsigned int sg_timeout;
unsigned int sg_reserved_size;
int node;
struct list_head drain_list;
struct request* flush_rq;
unsigned char ordered;
};
说明:请求队列跟踪等候的块IO请求,它存储用于描述这个设备能够支持的请求的类型信息,他们的最大大小,多少不同的段可以进入一个请求,硬件扇区大小,对齐要求等参数.其结果是:如果请求队列被配置正确了,它不会交给该设备一个不能处理的请求.
请求队列还要实现一个插入接口,这个接口允许使用多个IO调度器,IO调度器以最优性能的方式向驱动提交IO请求.大部分IO调度器是积累批量的IO 请求,并将其排列为递增/递减的块索引顺序后,提交给驱动.另外,IO调度器还负责合并邻近的请求,当一个新的IO请求被提交给调度器后,它会在队列里搜寻包含邻近的扇区的请求.如果找到一个,并且请求合理,调度器会将这两个请求合并.
Linux2.6的四个IO调度器,他们分别是No-op/Anticipatory/Deadline/C FQ IO scheduler.
关于request_queu结构体的操作:
//初始化请求队列
kernel elevator = deadline;/*给kernel添加启动参数*/
request_queue_t* blk_init_queue(request_fn_proc* rfn, spinlo ck_t* lock);
/*
* 两个参数分别是请求处理函数指针和控制队列访问权限的自旋锁.
* 此函数会发生内存分配的行为,需要检查其返回值.一般在加载函数中调用.
*/
//清除请求队列
void blk_cleanup_queue(request_queue_t* q);
/*
* 此函数完成将请求队列返回给系统的任务,一般在卸载函数中调用.
* 此函数即bld_put_queue()的宏定义#define blk_put_queue(q) blk _cleanup_queue((q))
*/
//分配"请求队列"
request_queue_t* blk_alloc_queue(int gfp_mask);
void blk_queue_make_request(request_queue_t* q, make_request _fn* mfn);
/*
* 前一个函数用于分配一个请求队列,后一个函数是将请求队列和"制造函数"进行绑定
* 但函数blk_alloc_queue实际上并不包含任何请求.
*/
//提取请求
struct request* elv_next_request(request_queue_t* q);
//去除请求
void blkdev_dequeue_request(struct request* req);
void elv_requeue_request(request_queue_t* queue, struct requ est* req);
//启停请求
void blk_stop_queue(request_queue_t* queue);
void blk_start_queue(request_queue_t* queue);
//参数设置
void blk_queue_max_sectors(request_queue_t* q, unsigned shor t max);
/*请求可包含的最大扇区数.默认255*/
void blk_queue_max_phys_segments(request_queue_t* q, unsigne d short max);
void blk_queue_max_hw_segments(request_queue_t* q, unsigned short max);
/*这两个函数设置一个请求可包含的最大物理段数(系统内存中不相邻的区),缺省是128*/
void blk_queue_max_segment_size(request_queue_t* q, unsigned int max);
/*告知内核请求短的最大字节数,默认2^16 = 65536*/
//通告内核
void blk_queue_bounce_limit(request_queue_t* queue, u64 dma_ addr);
/*
* 此函数告知内核设备执行DMA时,可使用的最高物理地址dma_addr,常用的宏如下:
* BLK_BOUNCE_HIGH:对高端内存页使用反弹缓冲(缺省)
* BLK_BOUNCE_ISA:驱动只可以在MB的ISA区执行DMA
* BLK_BOUNCE_ANY:驱动可在任何地方执行DMA
*/
blk_queue_segment_boundary(request_queue_t* queue, unsigned long mask);
/*这个函数在设备无法处理跨越一个特殊大小内存边界的请求时,告知内核这个边界.*/
void blk_queue_dma_alignment(request_queue_t* q, int mask);
/*告知内核设备加于DMA传送的内存对齐限制*/
viod blk_queue_hardsect_size(request_queue_t* q, unsigned sh ort max);
/*此函数告知内核块设备硬件扇区大小*/
3.5 request
request和request_queue结构体:Linux块设备驱动中,使用request结
构体来表征等待进行的IO请求;并用request_queue来表征一个块IO请求队列.两个结构体的定义如下:
request结构体
struct request{
struct list_head queuelist;
unsigned long flags;
sector_t sector;/*要传输的下一个扇区*/
unsigned long nr_sectors;/*要传送的扇区数目*/
unsigned int current_nr_sector;/*当前要传送的扇区*/
sector_t hard_sector;/*要完成的下一个扇区*/
unsigned long hard_nr_sectors;/*要被完成的扇区数目*/
unsigned int hard_cur_sectors;/*当前要被完成的扇区数目*/
struct bio* bio;/*请求的bio结构体的链表*/
struct bio* biotail;/*请求的bio结构体的链表尾*/
/*请求在屋里内存中占据的不连续的段的数目*/
unsigned short nr_phys_segments;
unsigned short nr_hw_segments;
int tag;
char* buffer;/*传送的缓冲区,内核的虚拟地址*/
int ref_count;/*引用计数*/
...
};
说明:
request结构体的主要成员包括:
sector_t hard_sector;/*要完成的下一个扇区*/
unsigned long hard_nr_sectors;/*要被完成的扇区数目*/
unsigned int hard_cur_sectors;/*当前要被完成的扇区数目*/ /*
* 上述三个成员依次是第一个尚未传输的扇区,尚待完成的扇区数,当前IO操作中待完成的扇区数
* 但驱动中一般不会用到他们.而是下面的一组成员.
*/
sector_t sector;/*要传输的下一个扇区*/
unsigned long nr_sectors;/*要传送的扇区数目*/
unsigned int current_nr_sector;/*当前要传送的扇区*/
/*
* 这三个成员,以字节为单位.如果硬件的扇区大小不是512字节.如字节,则在开始对硬件进行操作之前,应先用4来除起始扇区号.前三个成员,与后三个成员的关系可以理解为"副本".
*/
关于unsigned short nr_phys_segments:该成员表示相邻的页被合并后,这个请求在物理内存中的段的数目.如果该设备支持SG(分散/聚合,scatter/ga ther),可根据该字段申请sizeof(scatterlist*) nr_phys_segments的内存,并使用下面的函数进行DMA映射:
int blk_rq_map_sg(request_queue_t* q, struct request* rq, struc t scatterlist *sg);
该函数与dma_map_sg()类似,返回scatterlist列表入口的数量.
关于struct list_head queuelist:该成员用于链接这个请求到请求队列的链表结构,函数blkdev_ dequeue_request()可用于从队列中移除请求.宏rq _data_dir(struct request* req)可获得数据传送方向.返回0表示从设备读取,否则表示写向设备.
3.6 others
3.6.1 Scsi host driver
在Linux平台上做了一个virtual SCSI host driver,该virtual scsi host driver主要实现将scsi command转换成块设备的请求,即将块设备伪装成一个标准scsi设备。
Scsi host driver位于SCSI中间层之下,属于SCSI总线控制器的驱动。
如果有人做了一个SCSI适配器(host bus adapter),那么需要为该适配器写一个驱动,这个驱动就是scsi host driver。
如果SCSI适配器是一个真实的硬件设备,绝大多数情况是采用PCI接口,那么scsi host driver也是一个PCI设备驱动。
假设给一个真实的SCSI适配器写了一个驱动,那么内核驱动的体系结构如下图所示:
图3.6驱动体系结构图
SCSI device driver是SCSI设备驱动,也可以称之为功能驱动(Functi on driver)。
SCSI middle level driver为SCSI中间层驱动,抽象了SCSI 的总线逻辑。
Scsi host driver控制SCSI总线控制器,实现SCSI数据的物理层传输。
Scsi host driver需要与scsi middle level进行数据交互,在scsi mi ddle level定了标准的scsi host模版,所有接口函数和属性参数都定义在标准模版中。
模版的类型为struct scsi_host_template。
Virtual scsi host driver的模版定义如下:
struct scsi_host_template scsi_host_template = {
.module = THIS_MODULE,
.name = SCSI_HOST_IDENT,
.info = scsi_info,
.slave_configure = scsi_slave_configure,
.queuecommand = scsi_queuecommand,
.can_queue = SCSI_HOST_CAN_QUEUE, /* host c md queue depth */
.cmd_per_lun = SCSI_CMD_PER_LUN, /* lun cmd qu eue depth */
.sg_tablesize = SG_ALL,
.use_clustering = SCSI_CLUSTERING,
.this_id = SCSI_HOST_ID,
.emulated = 1,
};
.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表的长度。
3.6.2 Linux中磁盘扫描流程描述
通常SCSI总线适配器作为PCI设备的形式存在,其在计算机体系结构中的
位置如下图所示:
图3.6.1 scis host及device在计算机体系结构中的位置
在系统初始化时会扫描系统PCI总线,由于scsi host adapter挂接在pc i总线上,因此会被pci扫描软件扫描得到,并且生成一个pci device(PD O)。
然后扫描软件需要为该pci device加载相应的驱动程序。
在linux系统中,遍历pci bus上存在的所有驱动程序,检查是否有符合要求的驱动程序存在,这里假设scsi host是marwell的设备,那么,如果存在marwell提供的scsi host driver,就会被成功调用。
加载scsi host驱动时,pci扫描程序会调用scsi host driver提供的probe函数,该probe函数是scsi host dri ver在初始化驱动时注册到pci-driver上的(Linux的总线驱动都是采用的这种思路)。
在scsi host具体的probe函数中会初始化scsi host,注册中断处理函数,并且调用scsi_host_alloc函数生成一个scsi host,然后添加到scsi middle level,最后调用scsi_scan_host函数扫描scsi host adapter 所管理的所有scsi总线。
一个scsi host adapter可能拥有多个channel,每个channel拥有一条s csi总线。
传统scsi总线是并行共享总线,现有的SATA、SAS等P2P接口在逻
辑上可以理解成总线的一种特例,所以scsi middle level驱动程序是通用的。
由于一个scsi host可能存在多个channel,因此依次扫描每个channe l。
按照spec,传统scsi bus上最多可以连接16个scsi target,因此,scs i扫描程序会依次探测target。
一个scsi target可以存在多种功能,每种功能称之为LUN,对于单功能设备(例如磁盘),其LUN通常为0。
Scsi host的扫描过程可以简单采用如下伪码进行描述:
For (channel = 0; channel < max_channel; channel++) {
/* 对一个适配器的每个通道中的设备进行识别 */
…
For (id=0; id<max_id; id++) {
/* 对一个通道中的每个ID对应设备进行识别 */
...
For (lun=1; lun<max_dev_lun; lun++) {
/* 对一个ID对应设备的每个LUN进行识别 */
...
}
}
}
通过上述扫描过程可以知道,在系统中可以采用如下方法对一个scsi devi ce进行描述:host_id : channel_id : target_id : lun_id
其中,host_id是系统动态分配的,这与PCI总线的扫描顺序相关,对于固定硬件的系统host_id扫描得到的结果不会改变,但是,如果动态添加一个sc si host(PCI device),系统的host_id可能会发生变化,这一点需要注意。
3.6.3 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 {
/* 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 */ };
一个典型的scsi_host_template方法定义如下:
struct scsi_host_template bscsi_host_template = {
.module = THIS_MODULE,
.name = BSCSI_HOST_IDENT, / * scsi host的名字 */
.info = bscsi_info,
.slave_configure = bscsi_slave_configure,
.queuecommand = bscsi_queuecommand
/* scsi cmd请求接收函数 */
.can_queue = BSCSI_HOST_CAN_QUEUE, /* host cm d queue depth */
.cmd_per_lun = BSCSI_CMD_PER_LUN, /* lun cmd queue depth */
.sg_tablesize = SG_AL
/* scatter-gather table容量 */ .use_clustering = BSCSI_CLUSTERING,
.this_id = BSCSI_HOST_ID, /* scsi host I D号 */
.emulated = 1, /* virtual scsi host */ .bios_param = bscsi_std_bios_param,
.proc_name = "bscsi",
.proc_info = bscsi_proc_info,
.shost_attrs = bscsi_host_attrs,
3.6.4 scsi_scan_host函数
scsi middle level层提供了scsi host扫描函数,在设备枚举过程中sc si host可以调用该函数对scsi总线适配器进行扫描,当然host驱动也可以调用更加底层的函数对scsi总线进行扫描。
scsi_scsn_host函数实现流程如下:
3.6.5 scsi_request_fn函数
scsi_request_fn函数为scsi设备请求队列处理函数,该函数通常被注册到request_queue->request_fn上。
块设备请求的bio最终会merge到requ est queue中,然后通过unplug_fn函数调用request_queue->request_fn,实现scsi_reuqest_fn函数的调用。
Scsi_request_fn函数实现了请求队列的处理,首先从请求队列中摘取一个request,然后通过q->prep_rq_fn函数将请求转换成scsi命令,并且对s csi command进行初始化,最后通过scsi_dispatch_cmd函数将scsi命令分发给底层的scsi host驱动。
在scsi_request_fn函数的实现过程中,需要通过块设备发下来的请求构造相应的scsi命令,而scsi命令的生成与具体的设备驱动相关,其需要调用设备驱动提供的scsi命令初始化函数*_init_command完成命令初始化过程。
假设请求发送给scsi disk设备,那么在各层之间的函数调用关系如下图所示:
从前面分析可以看出,请求队列queue是top level与middle level之间的纽带。
上层请求会在请求队列中维护,处理函数的方法由上下各层提供。
在请求队列的处理过程中,将普通的块设备请求转换成标准的scsi命令,然后再通过middle level与low level之间的接口将请求递交给scsi host。
3.6.6 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命令队列中。
由于que uecommand函数在持有spinlock的上下文中运行,所以不宜做过多复杂的操作,否则很容易导致程序睡眠,从而使程序运行不稳定。
3.6.7 scsi设备扫描过程描述
在计算机系统启动过程中,操作系统会扫描默认的PCI根节点,从而触发了PCI设备扫描的过程,开始构建PCI设备树。
scsi host作为PCI设备会被PCI总线驱动层扫描到(PCI设备的扫描采用配置信息读取的方式),扫描到scsi host之后,操作系统开始加载scsi h ost的驱动,scsi host driver就是上面说所的low level driver。
scsi hos t driver初始化scsi控制器,通过PCI配置空间的信息分配硬件资源,注册中断服务。
最后开始扫描通过scsi控制器扩展出来的下一级总线——scsi bu s。