14.1 存储技术 291
13
14
16
18
22
23
20
17
15
21
19
块设备驱动程序
本章内容
存储技术
Linux块I/O层
I/O调度器
块驱动程序数据结构和方法
设备实例:简单存储控制器
高级主
调试
查看源代码
块设备是一种能随机访问的存储介质。与字符设备不同,块设备能保存文件系统数据。本章
介绍Linux是如何支持存储总线和设备的。
14.1 存储技术
首先回顾当今计算机系统中流行的存储技术,同时我们会将这些技术与内核源码树中相应设
备驱动程序子系统联系起来。
IDE(Integrated Drive Electronics,集成驱动电子设备)是PC中常见的存储接口技术,ATA
(Advanced Technology Attachment,高级技术配件)则是相关规范的官方名称。IDE/ATA
的第
一个版本是ATA-1,最新版本是支持最高带宽达133MB/s的ATA-7。其间的规范版本包括:引入了
LBA(Logic Block Addressing,逻辑块设备寻址)的ATA-2,支持SMART功能的ATA-3,支持Ultra
DMA的具有33MB/s吞吐量的ATA-4,最大传输速率达66MB/s的ATA-5,以及最大传输速率达
100MB/s的ATA-6。
CD-ROM和磁带等存储设备则使用一种称为ATAPI(ATA Packet Interface,ATA包接口)①的
特殊协议与标准IDE电缆相连接,ATAPI是在ATA-4中引入的。
在PC系统中,软盘控制器传统上一直是超级I/O芯片组的一部分,这个我们已经在第6章学习
① ATAPI协议更接近于SCSI而不是IDE。
第 14 章
292 第 14 章 块设备驱动程序
过了。然而在当今的PC中,这些内部驱动器已经让位给更快的外部USB软盘启动器了。
图14-1显示了一个ATA-7磁盘驱动器连接到IDE适配器的情形,这个适配器是PC系统上南桥
芯片组的一部分。同时也显示了与一个ATAPI CD-ROM驱动器和一个软盘驱动器的连接情形。
图14-1 PC系统中的存储媒介
第10章介绍PCIe时说过,IDE/ATA是一种并行总线技术(有时称为并行ATA或PATA),它不
能扩展至高速传输。SATA(Serial ATA,串行ATA)是由PATA发展而来的现代串行总线技术,它
支持300MB/s甚至更高的传输速度。除了有比PATA更高的传输速度外,SATA还支持热交换(hot
swapping)等功能。SATA技术正在逐步取代PATA。有关内核中的新型ATA子系统——libATA的介
绍见下面的补充内容。libATA能同时支持SATA和PATA。
libATA
libATA是Linux内核中的一种新型ATA子系统。它由一套ATA库例程和使用这个库例程的
底层驱动程序组成。 libATA同时支持SATA和PATA。 libATA中的SATA驱动程序之前在
drivers/scsi/目录下,但是从2.6.19内核版本开始, PATA驱动程序和新目录drivers/ata/已经囊括
了所有libATA源代码。
如果你的系统支持SATA存储器,你需要libATA和SCSI子系统的服务。支持PATA的libATA
仍处于试验阶段,默认情况下,PATA驱动程序继续使用原来的IDE驱动程序,它挂在driver/ide/
处理器
北桥
集线器接口
南桥
IDE主机
适配器 ATA-7硬盘驱动器
ATAPI CD-ROM
超级I/O
软盘
驱动器
LPC
PCI
14.1 存储技术 293
13
14
16
18
22
23
20
17
15
21
19
目录下。
假设系统在Intel ICH7南桥芯片组支持下具有SATA功能,你需要使能以下libATA部件来访
问磁盘。
(1) libATA核心。要装载这个,请在内核配置阶段设置CONFIG_ATA。要查明核心提供的库
函数列表,请在drivers/ata/目录下查找EXPORT_SYMBOL_GPL字符串。
(2) AHCI(Advanced Host Controller Interface,高级主机控制器接口)支持。AHCI
了
SATA主机适配器所支持的寄存器接口,可以在配置阶段通过选择CONFIG_AHCI来启用AHCI。
(3) 主机控制器适配驱动程序。对于ICH7,请启用CONFIG_ATA_PIIX。
另外,还需要中级和更高一级的SCSI驱动程序(CONFIG_SCSI及友员)。在下载了所有这
些内核部件后,你的SATA磁盘分区在系统中会显示为/dev/sd*,就像SCSI或USB大存储分区一样。
libATA 项目的主页为 http://linux-ata.org/ 。 Doc 文档是内核源码树的一部分,在
Documentation/DocBook/libata.tmp/目录下, libATA开发手册可从www.kernel.org/pub/linux/
kernel/people/jgarzik/libata.pdf下载。
SCSI(Small Computer System Interface,小型计算机系统接口)是服务器和高端工作站选用
的一种存储技术,SCSI比SATA要快一点,支持320MB/s的读写速度。SCSI传统上是一种并行接
口标准,但是像ATA一样,随着SAS(Serial Attached SCSI,串行附加SCSI)总线技术的发展,
它也在转向串行操作。
内核的SCSI子系统在结构上分为3层:介质(如磁盘、CD-ROM和磁带)所对应的顶层驱动
程序;扫描SCSI总线或配置设备的中间层;底层主机适配器驱动程序。我们在11.6.1节中已经学
习过这些层。参考图11-4可以明白SCSI子系统的这些不同部件间是如何进行互操作的①。USB大
容量存储器内部使用闪存,但使用SCSI协议与主机通信。
RAID(Redundant Array of Inexpensive Disk,廉价磁盘冗余阵列)是某些SCSI和SATA控制
器的一种内建技术,有了它就可以实现冗余性和可靠性。人们已经定义了各种RAID级别,如
RAID-1规定了在分散磁盘上备份数据的磁盘镜像。Linux驱动程序能用于许多有RAID功能的磁盘
驱动器。内核还提供了md(multidisk,多磁盘)驱动程序,它用软件方式实现了大部分的RAID
级别。
在嵌入式消费电子领域常使用小型存储器,这些存储器的传输速度比迄今讨论的技术所提供
的要慢得多。SD(Secure Digital,安全数码)卡和其衍生的外形更小的miniSD卡、microSD卡是
相机、手机、播放器等设备广泛采用的存储介质②。遵循SD卡规范1.01版本的卡支持最高10MB/s
的传输速率。SD存储器是从早期较慢的兼容性技术多媒体MMC(MultiMediaCard)卡(支持最
高2.5MB/s的数据率)发展起来的。内核在/drivers/mmc/目录下包含了一个SD/MMC子系统。
① SCSI支持还将在本书的其他部分讨论。19.4节讨论sg(SCSI Generic,SISI通用)接口,有了它,就可以将命令从
用户空间直接发送到SCSI设备。20.10.14节简要介绍iSCSI协议,它能将SCSI分组通过TCP/IP网络传输给远端块
设备。
② 要了解基于SD形式的非存储技术,请参见第16章。
294 第 14 章 块设备驱动程序
9.8节介绍了不同PCMCIA/CF存储卡的特点以及它们相应的内核驱动程序。PCMCIA记忆卡
(如微硬盘机等)支持真正的IDE操作,不过这些卡内部用固态记忆体模拟IDE,并向内核呈现
为IDE编程模型(IDE programming model)。这两种情形下内核的IDE子系统都可以用于启用
存储卡。
表14-1
了重要的存储技术以及相关设备驱动程序在内核源码树中的位置。
表14-1 存储技术及相关设备驱动程序
存储技术 说 明 源 文 件
IDE/ATA PC的存储接口技术,ATA-7支持133MB/s
的速率
drivers/ide/ide-disk.c 、 drivers/ide/ide-io.c 、
drivers/ide/ide-prob.c或者drivers/ata/(实验性)
ATAPI CD-ROM和磁带等存储设备,用ATAPI协
议连接标准IDE电缆
drivers/ide/ide-cd.c或drivers/ata/(实验性)
软盘(内部) 软盘控制器存在于PC兼容系统LPC总线上
的超级I/O芯片中,支持150KB/s的传输速率
drivers/block/floppy.c
SATA
IDE/ATA的串行演进,支持超过300MB/s
的传输速率
drivers/ata/、drivers/scsi/
SCSI 服务器环境中流行的存储技术,Ultra320
SCSI支持320MB/s
drivers/scsi/
USB大容量存储技术 是指USB硬盘、笔驱动器、CD-ROM以及
软盘驱动器。参见11.6.1节,USB 2.0设备能
以最高60MB/s速度通信
drivers/usb/storage/和
drivers/scsi/
RAID
硬件RAID 为达到冗余性和可靠性在高端SCSI/SATA
磁盘控制器中建立的功能
drivers/scsi/和drivers/ata/
软件RAID 在Linux中,md驱动程序用软件实现了几
级RAID
drivers/md/
SD/miniSD/microSD 在相机、手机等消费电子设备中流行使用
的小外形的介质,支持最高10MB/s的传输速率
drivers/mmc/
MMC 早期的可移动存储器标准,与SD卡兼容,
支持2.5MB/s
drivers/mmc/
PCMCIA/ CF存储卡 PCMCIA/ CF迷你外形IDE驱动器,或模拟
IDE的固态内存卡。参见第9章
drivers/ide/legacy/ide-cs.c或drivers/ata/pata_
pcmcia.c(实验用的)
闪存上的块设备枚举 在闪存上模拟硬盘。参见17.6.1节 drivers/mtd/mtdblock.c和drivers/mtd/mtd_
blkdevs.c
Linux上的虚拟块设备
RAMDISK 实现对将一块RAM区域作为一个块设备
的支持
drivers/block/rd.c
loopback设备 实现对将一个规则文件作为一个块设备的
支持
drivers/block/loop.c
14.3 I/O 调度器 295
13
14
16
18
22
23
20
17
15
21
19
14.2 Linux 块 I/O 层
块I/O层在内核的2.4和2.6发布版本间做了大量的修改,重新
的目的是因为块层相比于其
他内核子系统更可能影响系统整体性能。
根据图14-2我们可以了解Linux块I/O层的工作。存储介质包含了驻留于文件系统(如EXT3
或Reiserfs)中的文件。用户应用程序唤醒I/O系统调用来访问这些文件,相关文件系统操作在到
达各自文件系统驱动程序前,会先经过通用VFS(Virtual File System,虚拟文件系统)层。高速
缓冲区通过缓冲磁盘块来加速文件系统对块设备的访问。如果能够在高速缓冲区中找到块,就可
以节省通过访问磁盘读取块的时间。每个块设备指定的数据在请求队列中排队。文件系统驱动程
序将请求加入指定块设备的请求队列,同时块驱动程序从相应队列中取出请求。在这期间,I/O
调度器操控请求队列,使磁盘访问延时最小,同时使吞吐量最大。
图14-2 Linux的块I/O
我们接下来介绍Linux系统中的I/O调度器。
14.3 I/O 调度器
块设备会有寻道时间,即磁头从当前位置移动到目标磁盘扇区花费的时间。I/O调度器的主
要目标是通过尽量减少寻道时间来增加系统吞吐量。为此,I/O调度器维持一个排序过的请求队
文件I/O 文件I/O
用户空间
内核空间
VFS层
单个文件系统(EXT3、EXT4、JFFS2、Reiseris、VFAT等)
缓冲区高速缓存(页面缓存)
I/O调度
块驱动程序 块驱动程序
磁盘 CD驱动设备
内核空间
存储媒介
请求队列 请求队列
296 第 14 章 块设备驱动程序
列,排序是将请求按相关磁盘扇区连续性进行排列。新的请求依据排序算法被插入到适当位置。
如果队列中已有的某个请求正好要访问一个邻近的磁盘扇区,那么新请求就与它合二为一。因为
这些属性,I/O调度器的运行方式跟电梯类似:电梯只会在一个方向上安排各个请求,直到队列
中的最后一个请求者得到服务。
2.4内核中的I/O调度器简明地实现了这个算法,称为Linus电梯。然而实践证明,仅仅这么做
是不够的,于是在2.6内核中被含4个调度器的一套算法所取代:限时式(deadline)、预测式
(anticipatory)、完全公平排队(CFQ)、空操作(noop)。默认的调度器是预测式,不过这可以在
内核配置时更改,或者通过改动/sys/block/[disk]/queue/scheduler的值来更改(比如,如果使用IDE
磁盘,就将[disk]换成hda)。表14-2简要描述了Linux I/O调度器。
表14-2 Linux I/O调度器
I/O调度器 说 明 源 文 件
Linus电梯 标准的合并与排序I/O调度算法的直接实现 drivers/block/elevator.c ( 在
2.4内核树中)
限时式 除了Linus电梯实现的之外,限时式调度器对每个请求还设置
了溢出时间,以防止大量对同一磁盘区域的请求会“饿死”那些
对远处区域访问的请求。并且,读操作被赋予比写操作更高的优
先级,因为用户进程通常更多地会被阻塞,直到他们的读请求完
成。限时调度器因此确保每个I/O请求能在一定时限内得到服务,
这对一些数据库应用很重要
block/deadline-iosched.c (在
2.6内核树中)
预测式 预测式调度器与限时式调度器类似,不同的是在为读请求服务
后,它会等待一个预定的时间以期待未来的请求,这种调度技术
适合于工作站/交互式的负荷
block/as-iosched.c(在 2.6 内
核树中)
完全公平调度
队列(CFQ)
与Linus电梯类似,不同的是CFQ会为每个操作进程维护一个
请求队列,而不是所有进程使用一个公共队列。这确保了每个进
程(或进程组)得到公平的I/O,防止一个进程“饿死”其他进
程
block/cfq-iosched.c(在 2.6 内
核树中)
空操作 空操作调度器不需要搜寻请求队列来找最优插入点,而只是简
单地将新请求插入请求队列尾部。因此这种调度器对半导体存储
介质是理想的,因为它们没有移动部件,没有寻道延时。例如
DOM内部使用闪存
block/noop-iosched.c(在 2.6
内核树中)
从概念上看,I/O调度类似于进程调度。I/O调度让进程觉得它拥有磁盘,而进程调度则让进
程觉得它拥有CPU。近年来,Linux的I/O和进程调度器都发生了很大变化。进程调度将在第19章
中讨论。
14.4 块驱动程序数据结构和方法
现在我们将注意力集中到本章的主题块驱动程序。本节介绍重要的数据结构和驱动程序函
数,这在你实现块设备驱动程序时可能会遇到。在下一节我们在实现一个虚拟存储控制器的块驱
动程序时会使用这些结构体和方法。
下面是一个块驱动程序的主要数据结构。
14.4 块驱动程序数据结构和方法 297
13
14
16
18
22
23
20
17
15
21
19
(1) 内核用include/linux/genhd.h中定义的gendisk[通用磁盘(generic disk)的缩写]结构体
表示一个磁盘:
struct gendisk {
int major; /* Device major number */
int first_minor; /* Starting minor number */
int minors; /* Maximum number of minors.You have one
minor number per disk partition */
char disk_name[32]; /* Disk name */
/* ... */
struct block_device_operations *fops;
/* Block device operations.Described soon. */
struct request_queue *queue; /* The request queue associated with this disk.
Discussed next. */
/* ... */
};
(2) 与每个块驱动程序相关的I/O请求队列用request_queue结构体描述,该结构体定义在
include/linux/blkdev.h中。这是一个大的结构,但你可能只会用到域request结构体(稍后介绍)。
(3) 每个在request_queue队列中的请求用request结构体描述,该结构体定义在
include/linux/blkdev.h中。
struct request {
/* ... */
struct request_queue *q; /* The container request queue */
/* ... */
sector_t sector; /* Sector from which data access is requested */
/* ... */
unsigned long nr_sectors; /* Number of sectors left to submit */
/* ... */
struct bio *bio; /* The associated bio. Discussed soon. */
/* ... */
char *buffer; /* The buffer for data transfer */
/* ... */
struct request *next_rq; /* Next request in the queue */
};
(4) block_device_operations是与file_operations结构体对应的块驱动程序结构体。后
者用于字符驱动程序,前者包含了下面与块驱动程序相关的入口函数:
标准的函数,如open()、release()、ioctl();
特别的函数,如支持可移动块设备的media_changed()和revalidate_disk()。
block_device_operations在include/linux/fs.h中定义,如下所示:
struct block_device_operations {
int (*open) (struct inode *, struct file *); /* Open */
int (*release) (struct inode *, struct file *); /* Close */
int (*ioctl) (struct inode *, struct file *,
unsigned, unsigned long); /* I/O Control */
/* ... */
298 第 14 章 块设备驱动程序
int (*media_changed) (struct gendisk *); /* Check if media is available or
ejected */
int (*revalidate_disk) (struct gendisk *); /* Gear up for newly inserted media */
/* ... */
};
(5) 如果观察request结构体,会发现它关联了bio。bio结构体是块I/O操作在页级粒度的底
层描述,它在include/linux/bio.h中的定义如下:
struct bio {
sector_t bi_sector; /* Sector from which data access is requested */
struct bio *bi_next; /* List of bio nodes */
/* .. */
unsigned long bi_rw; /* Bottom bits of bi_rw contain
the data-transfer direction */
/* ... */
struct bio_vec *bi_io_vec; /* Pointer to an array of bio_vec structures */
unsigned short bi_vcnt; /* Size of the bio_vec array */
unsigned short bi_idx; /* Index of the current bio_vec in the array */
/* ... */
};
块数据通过一个bio_vec结构体数组在内部被表示成I/O向量。每个bio_vec数组元素由三元
组(页,页偏移,长度)组成,表示该块I/O的一个段。将I/O请求作为一个页向量有诸多优点,
如更简洁的实现和高效的分散/聚集操作。
最后,让我们简单地看一下块驱动程序入口函数。大部分情况下使用3类方法构建块驱动程
序。
常见的初始化和退出函数。
前述的block_device_operations中的部分函数。
request函数。块驱动程序不像字符设备,它不支持read()/write()的数据传输方式,
而是用称为“请求方法(request method)”的特殊方式实现磁盘访问。
块核心层提供了一套驱动程序方法可以使用的库例程,下一节的示例驱动程序会调用其中一
些库例程的服务。
14.5 设备实例:简单存储控制器
考虑图14-3中表示的嵌入式设备。SoC包含了一个内建的与块设备通信的存储控制器。其结
构类似于SD/MMC介质,不同的是我们的存储控制器实例用表14-3所列的基本寄存器集描述。
SECTOR_NUMBER_REGISTER寄存器规定了请求所要访问的起始数据扇区①,SECTOR_COUNT_
REGISTER寄存器存放了要传送的扇区数,数据经由DATA_REGISTER寄存器传输。COMMAND_
REGISTER寄存器指定了存储控制器的操作(如从介质读或向介质写)。STATUS_REGISTER寄存器
① 我们的示例设备的存储介质采用扁平扇区空间布局,而IDE控制器是支持柱面磁头扇区(Cylinder Head Sector,
CHS)布局的,它除了扇区号寄存器(sector number register)外,还由磁头寄存器(device head register)、低位
柱面寄存器(low cylinder register)以及高位柱面寄存器(high cylinder register)一起指定。
14.5 设备实例:简单存储控制器 299
13
14
16
18
22
23
20
17
15
21
19
包含了反映控制器是否正忙于进行操作的位。
图14-3 嵌入式设备上的存储器
表14-3 存储控制器寄存器
寄存器名称 内容描述
SECTOR_NUMBER_REGISTER 下一个磁盘操作所在的扇区
SECTOR_COUNT_REGISTER 要读写的扇区数
COMMAND_REGISTER 要执行的操作(如读或写)
STATUS_REGISTER 操作结果、中断状态、错误标识
DATA_REGISTER 在读通道上,存储控制器将数据从磁盘读到内部缓冲区。驱动程序
通过该寄存器访问内部缓冲区。在写通道上,由驱动程序写入寄存器
的数据被转到内部缓冲区,控制器再将它从内部缓冲区复制到磁盘
示例存储控制器被命名为myblkdev。这个简单的设备既不是中断驱动的,也不支持DMA。
假设介质是非移动的。我们的任务是为myblkdev编写块驱动程序。该驱动程序虽然简短,但很完
整。它不处理电源管理,也不追求I/O性能。
14.5.1 初始化
代码清单14-1包含了驱动程序初始化方法,即myblkdev-int(),它执行以下步骤。
(1) 用register_blkdev()注册块设备。该块库例程为myblkdev分配一个未使用的主设备号,
并为设备在/proc/devices中增加一个入口。
(2) 将一个请求方法与该设备关联。这是通过向blk_init_queue()提供myblkdev_
request()的地址实现的。调用blk_init_queue()会返回myblkdev的请求队列request_
queue。参考图14-2可以了解队列request_queue是如何与驱动程序相关联上的。blk_init_
queue()的第二个参数(也就是myblkdev_lock)是自旋锁,用于保护request_queue队列不被
同时访问。
(3) 硬件执行磁盘事务是以扇区为单位的,而软件子系统(如文件系统)是以块为单位处理数
据的。通常扇区大小是512B,块大小是4096B。需要将存储硬件所能支持的扇区大小和驱动程序
在一次请求中能接收的最大扇区数通知块层, myblkdev_init()会通过分别调用
blk_queue_hardsect_size()和blk_queue_max_sectors()完成这个通知。
(4) 通过alloc_disk()分配一个与myblkdev对应的gendisk并初始化其成员。一个由
myblkdev_init()提供的gendisk的重要成员是驱动程序的block_device_operations地址。
嵌入式SoC
CPU
核心
存储
控制器
介质插槽/
连接器
块介质
CPU内连总线
300 第 14 章 块设备驱动程序
另一个由myblkdev_init()填写的参数是myblkdev的存储容量,其单位是扇区。这是通过调用
set_capacity()完成的。每个gendisk还包含一个表示下层存储硬件属性的标识。比如说,如
果驱动器是可移动的,那么gendisk的标识域应该被标记为GENHD_FL_REMOVABLE。
(5) 将步骤 (4)中准备好的gendisk和步骤 (2)中得到的request_queue队列关联,并将
gendisk和设备的主/次设备号及名称连接起来。
(6) 调用add_disk()将磁盘添加到块I/O层。当这一步完成后,驱动程序就要准备接收请求
了。因此这通常是初始化化过程的最后步骤。
现在设备/dev/myblkdev对系统来说是可用的了。如果设备支持多个磁盘分区,它们会显示成
/dev/myblkdevX,其中X是分区号。
代码清单14-1 初始化驱动程序
#include
#include
static struct gendisk *myblkdisk; /* Representation of a disk */
static struct request_queue *myblkdev_queue;
/* Associated request queue */
int myblkdev_major = 0; /* Ask the block subsystem
to choose a major number */
static DEFINE_SPINLOCK(myblkdev_lock);/* Spinlock that protects
myblkdev_queue from
concurrent access */
int myblkdisk_size = 256*1024; /* Disk size in kilobytes. For
a PC hard disk, one way to
glean this is via the BIOS */
int myblkdev_sect_size = 512; /* Hardware sector size */
/* Initialization */
static int __init
myblkdev_init(void)
{
/* Register this block driver with the kernel */
if ((myblkdev_major = register_blkdev(myblkdev_major, "myblkdev")) <= 0) {
return -EIO;
}
/* Allocate a request_queue associated with this device */
myblkdev_queue = blk_init_queue(myblkdev_request, &myblkdev_lock);
if (!myblkdev_queue) return -EIO;
/* Set the hardware sector size and the max number of sectors */
blk_queue_hardsect_size(myblkdev_queue, myblkdev_sect_size);
blk_queue_max_sectors(myblkdev_queue, 512);
/* Allocate an associated gendisk */
myblkdisk = alloc_disk(1);
if (!myblkdisk) return -EIO;
/* Fill in parameters associated with the gendisk */
myblkdisk->fops = &myblkdev_fops;
14.5 设备实例:简单存储控制器 301
13
14
16
18
22
23
20
17
15
21
19
/* Set the capacity of the storage media in terms of number of
sectors */
set_capacity(myblkdisk, myblkdisk_size*2);
myblkdisk->queue = myblkdev_queue;
myblkdisk->major = myblkdev_major;
myblkdisk->first_minor = 0;
sprintf(myblkdisk->disk_name, "myblkdev");
/* Add the gendisk to the block I/O subsystem */
add_disk(myblkdisk);
return 0;
}
/* Exit */
static void __exit
myblkdev_exit(void)
{
/* Invalidate partitioning information and perform cleanup */
del_gendisk(myblkdisk);
/* Drop references to the gendisk so that it can be freed */
put_disk(myblkdisk);
/* Dissociate the driver from the request_queue. Internally calls
elevator_exit() */
blk_cleanup_queue(myblkdev_queue);
/* Unregister the block device */
unregister_blkdev(myblkdev_major, "myblkdev");
}
module_init(myblkdev_init);
module_exit(myblkdev_exit);
MODULE_LICENSE("GPL");
14.5.2 块设备操作
接下来看一下块驱动程序的block_device_operations操作中的主要方法。
块驱动程序的open()方法是在诸如挂载介质上的文件系统或执行文件系统检查(fsck)等操
作期间调用的。在open()期间完成的许多任务是依赖于硬件的。比如CD-ROM驱动程序锁住驱
动器门,SCSI驱动程序检查设备是否设置了写保护。如果是,开启写允许请求就会不成功。如果
设备是可移除的,open()会调用check_disk_change()服务例程,检查介质是否已经改变。
如果驱动程序需要支持特殊命令,可以用ioctl()方法实现你的支持。比如,软盘驱动程序
支持弹出介质的命令。
media_changed()方法会检查存储介质是否已经改变(因此myblkdev等固定介质与它无
302 第 14 章 块设备驱动程序
关)。比如SCSI磁盘驱动程序的media_changed()方法会检查已经插入的USB笔驱动器是否已经
改变等。
唯一被myblkdev支持的块设备操作是ioctl()方法,即myblkdev_ioctl()。块层处理一般
I/O控制,或调用驱动程序的ioctl()方法处理设备相关的命令。在代码清单14-2中,
myblkdev_ioctl()实现了GET_DEVICE_ID命令,该命令探知控制器的设备 ID。该命令经
COMMAND_REGISTER寄存器发出,ID数据从DATA_REGISTER寄存器获得。
代码清单14-2 块设备操作
#define GET_DEVICE_ID 0xAA00 /* Ioctl command definition */
/* The ioctl operation */
static int
myblkdev_ioctl (struct inode *inode, struct file *file,
unsigned int cmd, unsigned long arg)
{
unsigned char status;
switch (cmd) {
case GET_DEVICE_ID:
outb(GET_IDENTITY_CMD, COMMAND_REGISTER);
/* Wait as long as the controller is busy */
while ((status = inb(STATUS_REGISTER)) & BUSY_STATUS);
/* Obtain ID and return it to user space */
return put_user(inb(DATA_REGISTER), (long __user *)arg);
default:
return -EINVAL;
}
}
/* Block device operations */
static struct block_device_operations myblkdev_fops = {
.owner = THIS_MODULE, /* Owner of this structure */
.ioctl = myblkdev_ioctl,
/* The following operations are not implemented for our example
storage controller: open(), release(), unlocked_ioctl(),
compat_ioctl(), direct_access(), getgeo(), revalidate_disk(), and
media_changed() */
};
14.5.3 磁盘访问
前面已经提过,块驱动程序使用request()方法进行磁盘访问操作。当块I/O子系统要处理
驱动程序的request_queue队列中的请求时,它会调用驱动程序的request()方法。但request()
函数并不是运行在发出数据传输请求的用户进程的上下文中。相关request_queue队列的地址被
作为参数传递给request()函数。
14.5 设备实例:简单存储控制器 303
13
14
16
18
22
23
20
17
15
21
19
正如前文所述,内核在调用request()前先占有一个请求锁,这是保护相关request_queue
队列不被并发访问。因此,如果你的request()函数要调用一个可能会引起睡眠的函数,就必须
要在调用前先释放锁,并在它返回前重新获得锁。
代码清单14-3包含了该驱动程序的请求方法,即myblkdev_request()。这个函数使用
elv_next_request()服务来从request_queue队列得到下一个请求。如果队列不再包含等待处
理的请求,elv_next_request()返回NULL。已经介绍了,I/O调度算法会随电梯算法采用的基
本方法的变化而变化,所以才有elv_next_request()这个名称。在处理完一个请求后,驱动程
序通过调用end_request()告诉块层结束该请求的I/O。可以使用传给end_request()的第二个
参数设置成功或失败代号。
request_queue队列中的请求包含了请求访问数据的起始扇区(代码清单14-3中的
req->sector、需要执行 I/O的扇区数(req->nr_sectors)、被传输数据所在的缓冲区
(request_buffer)以及数据搬移方向(rq_data_dir(req))。如代码清单 14-3所示,
myblkdev_request()在这些参数的帮助下运行了要求的寄存器程序。
代码清单14-3 请求函数
#define READ_SECTOR_CMD 1
#define WRITE_SECTOR_CMD 2
#define GET_IDENTITY_CMD 3
#define BUSY_STATUS 0x10
#define SECTOR_NUMBER_REGISTER 0x20000000
#define SECTOR_COUNT_REGISTER 0x20000001
#define COMMAND_REGISTER 0x20000002
#define STATUS_REGISTER 0x20000003
#define DATA_REGISTER 0x20000004
/* Request method */
static void
myblkdev_request(struct request_queue *rq)
{
struct request *req;
unsigned char status;
int i, good = 0;
/* Loop through the requests waiting in line */
while ((req = elv_next_request(rq)) != NULL) {
/* Program the start sector and the number of sectors */
outb(req->sector, SECTOR_NUMBER_REGISTER);
outb(req->nr_sectors, SECTOR_COUNT_REGISTER);
/* We are interested only in filesystem requests. A SCSI command
is another possible type of request. For the full list, look
at the enumeration of rq_cmd_type_bits in
include/linux/blkdev.h */
if (blk_fs_request(req)) {
switch(rq_data_dir(req)) {
304 第 14 章 块设备驱动程序
case READ:
/* Issue Read Sector Command */
outb(READ_SECTOR_CMD, COMMAND_REGISTER);
/* Traverse all requested sectors, byte by byte */
for (i = 0; i < 512*req->nr_sectors; i++) {
/* Wait until the disk is ready. Busy duration should be
in the order of microseconds. Sitting in a tight loop
for simplicity; more intelligence required in the real
world */
while ((status = inb(STATUS_REGISTER)) & BUSY_STATUS);
/* Read data from disk to the buffer associated with the
request */
req->buffer[i] = inb(DATA_REGISTER);
}
good = 1;
break;
case WRITE:
/* Issue Write Sector Command */
outb(WRITE_SECTOR_CMD, COMMAND_REGISTER);
/* Traverse all requested sectors, byte by byte */
for (i = 0; i < 512*req->nr_sectors; i++) {
/* Wait until the disk is ready. Busy duration should be
in the order of microseconds. Sitting in a tight loop