6设备驱动程序
6.1 I/O体系结构
扩展硬件
硬件设备可能以多种方式连接到系统。主板上的扩展槽或外部连接器
是最常用的方法。当然,扩展硬件也可以直接集成到主板上。主板上能够容纳网络、USB、SCSI、图形卡等芯片,而不需要庞大的扩展卡。
-
总线系统
外设通过总线与CPU通信PCI
(Peripheral Component Interconnect):许多体系结构上使用的主要系统总线,该总线的现代版本也支持热插拔,使得设备可以在系统运行时连入或断开.PCI的传输速度最大能够达到每秒几百兆字节
ISA
(Industrial Standard Architecture):一种比较古老的总线.ISA在电学原理上非常简单
,这使得爱好者和小公司很容易设计制造额外的硬件.但随着时间推移,该总线引起了越来越多的问题,在更高级的系统中最终被替换掉SBus
:这是一个非常高级的总线,不过已经出现很多年了。它由SUN公司设计,是一种非私有的开放总线,但未能在其他体系结构上为自身赢得一个位置.仍然在旧一点的SparcStation上发挥着重要作用IEEE1394
:对市场而言,这显然不是一个较通俗的名字。因而某些厂商将其称之为FireWire
,而另一些则称之为I.link
。它有几个非常有趣的技术特性,包括预先设计的热插拔能力、非常高的传输速率
。IEEE1394是高端笔记本电脑上非常流行的一种外部总线,提供了一种高速的扩展选项USB
(Universal Serial Bus):这也是一种广泛应用的外部总线,有很高的市场接受度。该总线的主要特性是热插拔能力
,及其自动检测新硬件的能力。其最高速度只是中等水平
,但足以用于CD刻录机、键盘、鼠标之类的设备。该总线的一种新版本(2.0)的最大传输速率更大,但实际上软件没什么变化(硬件层次上的差别要大得多,但幸运的是我们无需操心这些)。USB系统的拓扑结构异乎寻常,其中的设备不是按一条单链排布,而是按树型结构排布
。在内核寻址此类设备时,该事实就显而易见了。USB集线器用作树的结点,在它上面可以进一步连接其他设备(包括其他USB集线器)。USB还有一个异乎寻常的特性,可以为各个设备预留固定的带宽。在实现均匀数据流时,这是一个重要因素。SCSI
(Small Computer System Interface):这种总线过去称为专业人员的总线,因为相关外设的成本很高。由于SCSI支持非常高的数据吞吐率
,因此它主要用在服务器系统上寻址硬盘,可适用于大多数处理器体系结构。它很少用于工作站系统,因为与其他总线相比,SCSI的电气安装非常复杂(每个SCSI链都必须终结,才能正常工作并口与串口
(Parallel and Serial Interface):这些存在于大多数体系结构上,无论整个系统的设计如何。这些总线非常简单而速率极低
,用于外部连接,已经非常古老。这些总线用于寻址慢速设备
(如打印机、调制解调器和键盘等),此类设备没什么性能要求
-
与外设的交互
-
I/O端口
I/O地址空间通常不关联到普通的系统内存。因为端口也可以映射到内存中,这通常会引起混淆。
IA-32 上端口地址空间由2^16(65536)个不同的8位地址组成,0x0-0xFFFF,可以将连续8位端口组成16位端口,连续16位端口组成32位端口。处理器提供了一些适当的汇编语句,可以进行输入输出操作。
每种处理器类型实现端口访问的方式都不同。因此,内核必须提供一个适当的抽象层。诸如outb
(写一个字节)、outw
(写一个字)、inb
(读取一个字节)之类的命令在asm-arch/io.h
中实现。 这些定义与具体处理器非常相关 -
I/O内存映射
现代处理器提供了对I/O端口进行内存映射的选项,将特定外设的端口地址映射到普通内存中,可以像处理普通内存那样操作外设。图形卡通常会使用这类操作,因为与使用特定的端口命令相比,处理大量图像数据时使用普通处理器命令要更容易。诸如PCI之类的系统总线通常也是通过I/O地址映射进行寻址的。
为使用内存映射,首先必须将I/O端口映射到普通的系统内存中(使用特定于处理器的例程)。在不同的底层体系结构之上,完成这一任务的方法有很大的不同,内核再次提供了一个小的抽象层,主要包括ioremap
和iounmap
命令,分别用于映射I/O内存区和解除映射。 -
轮询和中断
系统如何知道某个设备的数据已经就绪、可以读取?有两种方法可以判断:使用轮询或中断.
轮询比较浪费资源
每个CPU都提供了中断线(interrupt line),可由各个系统设备共享(几个设备也可能共享一个中断)。每个中断通过一个唯一的号码标识,内核对使用的每个中断提供一个服务函数。
-
-
通过总线控制设备
并非所有设备都是直接通过I/O语句寻址,也有通过总线系统访问的。具体的方式与所用的总线和设备相关。
就系统总线而言(对很多处理器类型和体系结构来说,是PCI总线),可使用I/O语句和内存映射与总线自身和附接的设备通信。内核也为驱动程序提供了几个命令,以调用特殊的总线功能:查询可用设备的列表、按统一的格式读取或设置配置信息,等等。这些命令都是平台无关的,相应的代码在各种平台上使用时无需改变,因而简化了驱动程序的开发。
扩展总线如USB、IEEE1394、SCSI等,通过明确定义的总线协议与附接的设备交换数据和命令。内核通过I/O语句和内存映射与总线自身通信,同时提供了平台无关的例程,使总线能够与附接的设备通信。
与总线上附接的设备通信
,不见得一定在内核空间中由设备驱动程序进行,有时也可能在用户空间中实现
。最初的例子是SCSI刻录机,通常通过cdrecord工具访问。该工具产生需要的SCSI命令,然后利用内核经SCSI总线将命令发送到对应的设备,并处理设备返回的信息和响应。
6.2 访问设备
设备特殊文件(设备文件)用于访问扩展设备。这些文件并不关联到硬盘或任何其他存储介质上的数据段,而是建立了与某个设备驱动程序的连接,以支持与扩展设备的通信。就应用程序而言,普通文件和设备文件的处理有一点差别。二者都可以通过同样的库函数处理。但为了处理方便,系统还提供了几个额外的命令用于设备文件,这些对普通文件是不可用的。
6.2.1 设备文件
如过附接到串行接口的调制解调器.对应的设备文件名称是/dev/ttyS0
。设备并不是通过其文件名标识,而是通过文件的主、从设备号
标识。这些号码在文件系统中作为特别的文件属性管理。
6.2.2 字符设备,块设备和其他设备
根据外设与系统之间交换数据的方法,可以将设备分为几类
- 字符设备,数据传输量低(串口,文本终端等)
- 块设备,处理包含固定数目字节的数据块(硬盘,光驱等)
-
标识设备文件
root@elk# ls -l /dev/sd{a,b} /dev/ttyS{0,1} brw-rw---- 1 root disk 8, 0 8月 8 08:50 /dev/sda brw-rw---- 1 root disk 8, 16 8月 8 08:50 /dev/sdb crw-rw---- 1 root dialout 4, 64 8月 8 08:50 /dev/ttyS0 crw-rw---- 1 root dialout 4, 65 8月 8 08:50 /dev/ttyS1
- 访问权限前的字母b表示块设备,c表示字符设备
- 设备文件没有文件长度,而增加了另外两个值,为主设备号和从设备号,二者共同组成唯一编号代表该设备
设备文件通过
主从设备号
标识匹配驱动程序而不是设备名,mknod命令用于创建设备文件主设备号
用于寻址设备驱动程序
自身。根据上述的例子可知,硬盘sda和sdb所在的第1个SATA控制权的主设备号是8。 驱动程序管理的各个设备
(即第1个和第2个硬盘)则通过不同的从设备号指定
。sda对应于0,sdb对应于16。两个从设备号之间为什么有很大的差距?我们来看一下/dev目录中与sda硬盘有关的其他设备文件root@elk# ls -l /dev/sd* brw-rw---- 1 root disk 8, 0 8月 8 08:50 /dev/sda brw-rw---- 1 root disk 8, 1 8月 8 08:50 /dev/sda1 brw-rw---- 1 root disk 8, 16 8月 8 08:50 /dev/sdb brw-rw---- 1 root disk 8, 17 8月 8 08:50 /dev/sdb1 brw-rw---- 1 root disk 8, 18 8月 8 08:50 /dev/sdb2 brw-rw---- 1 root disk 8, 19 8月 8 08:50 /dev/sdb3 brw-rw---- 1 root disk 8, 32 8月 8 08:50 /dev/sdc
硬盘的各个
分区
可以通过设备文件进行寻址(如/dev/sda1、/dev/sda2等),而/dev/sda则代表了整个硬盘
。连续的副设备号用于标识各个分区,这使得驱动程序可以区分不同的分区。一个驱动程序可以分配多个主设备号。如果系统上有两个SATA总线
,那么第2个SATA通道的主设备号将与第1个不同。- 块设备和字符设备的主设备号可能是相同的。因此,除非同时指定设备号和设备类型(块设备/字符设备),否则找到的驱动程序可能不是唯一的
设备号的当前列表可以从
http://www.lanana.org
获取。这个网址看起来相当古怪,它是Linux assigned name and numbers authority
的首字母缩写。内核源代码的标准发布版也包括了Documentation/devices.txt
文件,其中给出了该版本发布时的最新数据。比纯粹的数字更容易读的预处理器常数,则定义在<major.h>
中。该文件的设备号与LANANA列表中分配的号码是同步的,但并非LANANA分配的所有设备号都对应了一个预处理器符号。SCSI磁盘(SATA设备归于此类)和TTY设备的主设备号分别是8和4
,由下列预处理器符号表示://include/linux/major.h #define TTY_MAJOR 4 #define SCSI_DISK0_MAJOR 8
-
动态创建设备文件
/dev中的设备结点一般是在基于磁盘的文件系统中静态创建的。随着支持的设备越来越多,必须安置和管理越来越多的项,典型的发布版大约包含20 000项。一般的系统只包含少量(与可用的20 000个设备结点相比)设备,因而大多数项是不必要的。因此几乎所有的发布版都
将/dev内容的管理工作切换到udevd
,这是一个守护进程
,允许从用户层动态创建设备文件。
udevd的基本思想如图6-3所示。即使从用户层管理设备文件,内核的支持也是绝对必要的,否则就无法判断系统上有哪些设备可用。
每当内核
检测到一个设备
时,都会创建一个内核对象kobject
(参见第1章)。该对象借助于sysfs文件系统导出到用户层
(更多细节请参见10.3节)。此外,内核还向用户空间发送一个热插拔消息
,这一点会在7.4节讨论如果在系统启动期间发现新设备,或在运行期间有新设备接入(如USB 存储棒),内核产生的热插拔消息包含了驱动程序为设备分配的主从设备号。
udevd守护进程所需完成的所有工作,就是监听这些消息。在注册新设备时,会在/dev中创建对应的项
,接下来就可以从用户层访问该设备了由于引入了
udev
机制,/dev
不再放置到基于磁盘的文件系统中,而是使用tmpfs文件系统
,这是RAM磁盘文件系统ramfs的一种轻型变体
。这意味着设备结点不是持久性的,系统关机/重启后就会消失
。如果在关机后卸下一个设备,则对应的设备结点将不再包含于/dev中。由于系统中已经没有该设备,对应的设备结点不会重新创建,内核也不会发送设备注册的消息,这确保了/dev中没有旧的、过时的设备文件。尽管并不限制udevd使用基于磁盘的文件系统,这实际上也没有什么意义除了上文列出的任务之外,udev守护进程还有一些职责,如确保无论采用何种设备拓扑结构,特定设备对应的设备结点总是名称相同。例如,用户在计算机上插入一个USB 存储棒时,通常希望总是建立同样的设备结点,这应该是与USB 存储棒插入的时间和地点无关的。 有关udev守护进程处理此类情况的方式,更多信息请参考手册页udevd(5)。这完全是用户空间问题,内核无需关注。
6.2.3 使用ioctl进行设备寻址
有些任务只使用输入输出命令很难完成。这些涉及检查特定于设备的功能和属性,超出了通用文件框架的限制。主要的例子是设置设备的配置选项
内核必须提供一种方法,能够支持设备的特殊属性,而无需依靠普通的读写命令。
- 一种方法是引入特殊的
系统调用
。但在内核开发者当中,这种做法很难得到赞同,因而只用于少数非常普及的设备。 - 一种更适当的解决方案称之为
IOCTL
,它表示输入输出控制接口,是用于配置和修改特定设备属性的通用接口。 - 还有第三种备选方案:
Sysfs是一种文件系统
,层次化地表示了系统中的所有设备,并提供了设置设备参数的方法。有关这种机制的更多信息,将在10.3节讲述。
在这里,我仍然继续讲述稍显过时、但仍然有效的IOCTL方法。
ioctl通过一种可用于处理文件的特殊方法实现。该方法对设备文件可产生所需的效果,但对普通文件无效。第8章讨论了该实现如何融入到虚拟文件系统的方案中。目前,我们只需要了解,每个设备驱动程序都可以定义一个ioctl函数,使得控制数据的传输可以独立于实际的输入输出通道。
从用户和程序设计的角度来看,ioctl如何使用呢?标准库提供了ioctl函数,可以通过特殊的码值将ioctl命令发送到打开的文件。
该函数的实现基于ioctl系统调用,由内核中的sys_ioctl
处理(该系统调用实现的有关信息,请参见第13章)。
ioctl码值(cmd)传递到由文件描述符(fd)标识的打开文件,ioctl码值一般定义为比较易读的预处理器常数。第三个参数(arg)传输更多的信息(有关内核支持的所有ioctl码值和相关参数的详表,可以参考系统程序设计方面的大量手册)。6.5.9节更详细地讨论了内核端对ioctl的实现。
//fs/ioctl.c
asmlinkage long sys_ioctl(unsigned int fd, unsigned int cmd, unsigned long arg)
- 网卡及其他设备
网卡无法融入前面字符设备和块设备,网卡没有设备文件.用户使用套接字与网卡通信.套接字是一个抽象层,对所有网卡提供了一个抽象视图。标准库的网络相关函数调用socketcall系统调用与内核通信交互,进而访问网卡。
//net/socket.c
asmlinkage long sys_socketcall(int call, unsigned long __user *args)
还有其他一些没有设备文件的系统设备。这些设备或者通过特别定义的系统调用访问,或者在用户空间无法访问。后者的例子包括所有的扩展总线,如USB和SCSI。尽管这些总线可以通过设备驱动程序寻址,但相应的函数只在内核内部可用(因此,USB扩展卡也没有设备文件,无法通过设备文件寻址)。所以,需要由底层的设备驱动程序提供函数,导出到用户空间,供应用程序访问。
6.2.4 主从设备号的表示
因为历史原因,有两种方法可以管理设备的主从设备号(在一个复合数据类型中)。在内核版本2.6开发期间,使用一个16位的整数(通常是unsigned short)来表示主从设备号。该整数按1:1比例划分,8个比特位表示主设备号,8个比特位表示从设备号
。这意味着刚好有256个主设备号和256个从设备号可用。当前的有些系统规模远远超出了上述的限制,我们只需要考虑SCSI存储阵列的例子即可,其中包含了数目非常庞大的硬盘。
因而16位整数的定义被替换为32位整数
(相关的抽象类型是dev_t),但这样做会有一些后果。我们意识到,16个比特位已经超出主设备号的需要。因此,主设备号分配了12个比特位,剩余的20个比特位用于从设备号
。这引起了下述的问题。
- 许多驱动程序作出了不正确的假设,认为只有16个比特位可用来表示主从设备号。
- 存储在旧的文件系统上的设备文件号只使用了16个比特位,但仍然必须正常。因此,必须解决现在对主从设备号占用比特位的非对称划分所引起的问题。
第一个问题可以通过修改驱动程序来消除,而第二个问题在更大程度上是本质性的。为处理新的情况,内核使用了用户空间可见的数据类型u32来表示设备号,主从设备号的划分如图6-4所示。
- 在内核中,比特范围
0~19共20个比特位用于从设备号
。而比特范围20~31中的12个比特位用于主设备号
。 - 当需要在外部(用户空间)表示dev_t时,则将比特范围
0~7中的8个比特位用作从设备号的第一部分,接下来的12个比特位(比特范围8~19)用作主设备号,最后12个比特位(比特范围20~31)用作从设备号剩余的部分
旧的布局共包括16个比特位,主从设备号各占8比特。如果主设备号和从设备号都小于255,那么新旧表示是兼容的。
如果代码坚持使用在dev_t和外部表示之间进行转换的函数,那么即使将来内部数据类型再次发生改变,代码也无需变动。
这种划分的优点在于,该数据结构的前16个比特位,可以与旧设备号兼容
内核提供了下列函数/宏(定义在<kdev_t.h>
中),以便从u32表示提取信息,并在u32和dev_t之间进行转换。
//include/linux/kdev_t.h
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
/*从dev_t提取主设备号*/
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
/*从dev_t提取从设备号*/
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
/*将主从设备号组成一个设备号dev_t*/
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
/*将内核层设备号dev_t转换为应用层表示的u32类型设备号,应用层前8位和后12位组成从设备号,中间12位作为主设备号*/
static inline u32 new_encode_dev(dev_t dev)
/*将用户层设备号转为内核层设备号*/
static inline dev_t new_decode_dev(u32 dev)
/*将内核设备号dev_t转为旧的u16类型应用层设备号*/
static inline u16 old_encode_dev(dev_t dev)
/*将旧的用户层设备号u16转为内核层设备号dev_t*/
static inline dev_t old_decode_dev(u16 val)
6.2.5 块设备和字符设备注册
内核如果能了解到系统中有些哪些字符设备和块设备可用,那自然是很有利的,因而需要维护一个数据库。此外必须提供一个接口,以便驱动程序编写者能够将新项添加到数据库
-
数据结构
- 设备数据库
尽管块设备和字符设备彼此的行为确实有很大不同,但用于跟踪所有可用设备的数据库是相同的。因为字符设备和块设备都通过唯一的设备号标识。但是,数据库会根据块设备/字符设备,来跟踪记录不同的对象
- 字符设备用
struct cdev
表示 - 块设备分区用
struct gendisk
表示,块设备没分区认为只有一个分区
有两个全局数组(
bdev_map用于块设备,cdev_map用于字符设备
)用来实现散列表,使用主设备号作为散列键
。cdev_map和bdev_map都是同一数据结构struct kobj_map的实例。散列方法相当简单:major % 255。由于当前只有非常少量设备的主设备号大于255,因此这种方法工作得很好,散列碰撞也很少。struct kobj_map的定义也包括了散列链表元素struct probe的定义。
//drivers/base/map.c //字符设备/块设备,管理data 使用了散列,用主设备号作为散列键查找对应的设备(major % 255) struct kobj_map { struct probe { struct probe *next;//单链表,下一个节点,连接同一散列行中的所有散列元素,按设备号从小到大顺序排列,同一主设备号的从设备通过链表连接 dev_t dev;//设备号,主设备号+次设备号 unsigned long range;//次设备号的连续范围 ,与设备关联的各次设备号范围为[MINORS(dev), MINORS(dev) + range - 1] struct module *owner;//指向提供设备驱动程序的模块,如果有的话 kobj_probe_t *get;//探测函数,用于获取实际的设备对象,返回与设备关联的 kobject 实例的函数,字符设备为 exact_match 函数 int (*lock)(dev_t, void *); void *data;//不同设备的数据结构,字符设备指向struct cdev,块设备指向struct gendisk } *probes[255];//数组,下标是主设备号 struct mutex *lock; };
- 字符设备范围数据库
第二个数据库只用于字符设备
。它用于管理为驱动程序分配的设备号范围。驱动程序可以请求一个动态的设备号
,或者指定一个范围,从中获取。前一种情况,内核需要找到一个空闲的范围,而对于后一种情况,必须确保指定的范围不与现存的范围重叠。
这里再次使用了散列表来跟踪已经分配的设备号范围,并同样使用主设备号作为散列键
。所述的数据结构如下所示:
//fs/char_dev.c //字符设备,管理为驱动程序分配的设备号范围,动态申请设备号相关,使用了散列,用主设备号作为键值 static struct char_device_struct { struct char_device_struct *next;//单链表,同一主设备号的不同从设备号在这个链表从小到大排序 unsigned int major;//主设备号 unsigned int baseminor;//包含 minorct 个从设备号的连续范围中最小的从设备号 int minorct;//连续的从设备数 char name[64];//通常,该名称会选择类似于该设备对应的设备特殊文件的名称,但没有严格的要求 struct file_operations *fops;//与该设备相关的操作函数接口结构体 struct cdev *cdev; /* will die *///指向 struct cdev 的实例 } *chrdevs[CHRDEV_MAJOR_HASH_SIZE];//下标为主设备号
- 设备数据库
-
注册过程
-
字符设备
在内核中注册字符设备需要两个步骤来完成-
注册或分配一个设备号范围。如果驱动程序需要使用特定范围内的设备号,则必须调用
register_chrdev_region
,而alloc_chrdev_region
则由内核来选择适当的范围//fs/char_dev.c //注册设备号范围,自己指定设备号范围 int register_chrdev_region(dev_t from, unsigned count, const char *name) //注册设备号范围,内核选择适当范围 int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
-
在获取了设备号范围之后,需要将设备添加到字符设备数据库,以激活设备。这需要用
cdev_init
初始化一个struct cdev
的实例,接下来调用cdev_add
。这些函数的原型定义如下//include/linux/cdev.h //字符设备结构,存入inode结构体的i_cdev成员中 struct cdev { struct kobject kobj; struct module *owner;//指向提供驱动程序的模块(如果有的话) const struct file_operations *ops;//文件操作函数指针集合,实现了与硬件通信的具体操作,与file结构体的f_op成员相关联 struct list_head list;//双向链表,存入inode结构体中的i_devices成员,包含所有表示该设备的设备特殊文件的inode dev_t dev;//设备号 unsigned int count;//与该设备关联的从设备号的数目 }; //fs/char_dev.c void cdev_init(struct cdev *cdev, const struct file_operations *fops) //将设备添加到字符设备数据库,全局变量cdev_map,以激活设备,count:从设备号的数量 int cdev_add(struct cdev *p, dev_t dev, unsigned count) //老版字符设备的标准注册函数,为兼容老版本而保留,分配设备号+增加字符设备 int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
-
-
块设备
//block/genhd.c //将分区信息添加到内核列表bdev_map中 void add_disk(struct gendisk *disk) //老版块设备的标准注册函数,为兼容老版本而保留,该函数注册的块设备将显示在 /proc/devices int register_blkdev(unsigned int major, const char *name)
-
6.3 与文件系统关联
除极少数例外,设备文件都是由标准函数处理,类似于普通文件。设备文件也是通过将在第8章讨论的虚拟文件系统管理。普通文件和设备文件都是通过完全相同的接口访问
。
6.3.1 inode中的设备文件成员
虚拟文件系统中的每个文件都关联到恰好一个inode,用于管理文件的属性
。inode数据结构非常冗长,我在这里就不完全复制了,只给出其中与设备驱动程序有关的成员
//include/linux/fs.h
//管理文件的属性,每个文件和块设备都表示为struct inode的一实例.struct file是通过open系统调用打开的文件的抽象,与此相反,inode表示文件系统自身中的对象
struct inode {
dev_t i_rdev;//inode表示设备文件时,表示主次设备号,表示与哪个设备关联,用于找到目标设备的实例,如块设备会找到struct block_device实例,后面的联合体指针就指向对应的实例.该数据类型的定义决不是持久不变的,在内核开发者认为必要时会进行修改。因此,只应该使用两个辅助函数imajor和iminor来从i_rdev提取主设备号和从设备号,这两个函数都只需要一个指向inode实例的指针作为参数。与 i_mode 同时使用唯一地标识与一个设备文件关联的设备
umode_t i_mode;//文件类型(面向块,或者面向字符)和访问权限,与 i_rdev 同时使用唯一地标识与一个设备文件关联的设备
const struct file_operations *i_fop;//对文件操作的函数集合的指针(如打开,读取,写入等),由虚拟文件系统使用来处理块设备 /* former ->i_op->default_file_ops */
struct list_head i_devices;//链表成员,链表头为cdev->list,加入设备实例的双向链表中,如cdev的list成员中
union {//inode表示设备文件时,根据i_mode文件类型决定联合体使用哪一个
struct pipe_inode_info *i_pipe;//管道实例
struct block_device *i_bdev;//块设备实例
struct cdev *i_cdev;//字符设备实例
};
};
6.3.2 字符设备和块设备文件inode创建函数
在打开一个设备文件时,各种文件系统的实现会调用init_special_inode
函数,为块设备或字符设备文件创建一个inode。
//fs/inode.c
//打开设备文件时各文件系统的实现会调用这个函数
//根据不同文件类型的inode节点,关联对应的操作函数集合,主次设备号
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
6.3.3 字符设备inode的操作函数集合结构体
字符设备的情况最初非常含混,因为只有一个文件操作可用
//fs/char_dev.c
//字符设备操作函数集合,注册在字符设备文件的inode中,由init_special_inode函数添加
const struct file_operations def_chr_fops = {
.open = chrdev_open,
};
字符设备彼此非常不同。因而内核在开始不能提供多个操作,因为每个设备文件都需要一组独立、自定义的操作。因而chrdev_open函数的主要任务就是向该结构填入适用于已打开设备的函数指针,使得能够在设备文件上执行有意义的操作,并最终能够操作设备自身。
6.3.4 块设备inode的操作函数集合结构体
相比字符设备,块设备遵循的方案更加一致。这使得内核刚开始就有很多操作可供选择。这些操作的指针群集到一个称作blk_fops的通用结构中。
//fs/block_dev.c
//块符设备操作函数集合,注册在块设备文件的inode中,由init_special_inode函数添加
const struct file_operations def_blk_fops = {
.open = blkdev_open,
.release = blkdev_close,
.llseek = block_llseek,
.read = do_sync_read,
.write = do_sync_write,
.aio_read = generic_file_aio_read,
.aio_write = generic_file_aio_write_nolock,
.mmap = generic_file_mmap,
.fsync = block_fsync,
.unlocked_ioctl = block_ioctl,
.splice_read = generic_file_splice_read,
.splice_write = generic_file_splice_write,
};
读写操作由通用的内核例程进行。内核中的缓存自动用于块设备。
尽管file_operations与block_device_operations的结构类似,但不能混淆二者。file_operations由VFS层用来与用户空间通信
,其中的例程会调用block_device_operations中的函数
,以实现与块设备的通信。block_device_operations必须针对各种块设备分别实现
,对设备的属性加以抽象,而在此之上建立的file_operations,使用同样的操作即可处理所有的块设备。
与字符设备相比,上述数据结构无法完全描述块设备,因为对块设备的访问不是分别处理单个的请求,而是通过由缓存和请求队列构成的精细、复杂的系统来高效地管理。缓存主要由通用的内核代码操作,而请求队列则由块设备层管理。
在我更详细地讨论可能的块设备驱动程序操作时,读者会看到更多用于管理请求队列的数据结构,它将汇集并重排发给相关设备的指令。
6.4 字符设备操作函数和结构体介绍
字符设备的硬件通常非常简单,而且相关的驱动程序并不难于实现,这并不令人惊讶。
6.4.1 字符设备结构体 cdev
字符设备由struct cdev
表示。同时,内核维护了一个数据库,包括所有活动的cdev实例。下面将讲解该结构的内容,其定义如下:
//include/linux/cdev.h
//字符设备结构,存入inode结构体的i_cdev成员中
struct cdev {
struct kobject kobj;
struct module *owner;//指向提供驱动程序的模块(如果有的话)
const struct file_operations *ops;//文件操作函数指针集合,实现了与硬件通信的具体操作,与file结构体的f_op成员相关联
struct list_head list;//链表头,链表元素为inode->i_devices,双向链表,存入inode结构体中的i_devices成员,包含所有表示该设备的设备特殊文件的inode
dev_t dev;//设备号
unsigned int count;//与该设备关联的从设备号的数目
};
最初,字符设备的文件操作只包含用于打开相关设备文件(在使用驱动程序时,这总是第一个操作)的一个方法。因此,我们首先讲解该方法:
6.4.2 打开设备文件 chrdev_open
chrdev_open
是用于打开字符设备的通用函数
//fs/char_dev.c
//注册在字符设备inode->i_fop中file_operations的open回调函数,在 def_chr_fops 全局变量中定义
int chrdev_open(struct inode * inode, struct file * filp)
假定表示设备文件的inode此前没有打开过。根据给出的设备号,kobj_lookup
查询cdev_map,并返回与该驱动程序关联的kobject实例(cdev->kobj)。该返回值可用于获取cdev实例
获得了对应于设备的 cdev实例,内核通过 cdev->ops还可以访问特定于设备的 file_operations。接下来设置各种数据结构之间的关联,如图6-7所示
下图为字符设备的例子,其主设备号为1。根据LANANA标准,该设备有10个不同的从设备号。每个都提供了一个不同的功能,这些都与内存访问操作相关。表6-1列出了一些从设备号,以及相关的文件名和含义。
上图各设备实现的功能有很大的差别.chrdevs项的结构中只定义了一个函数指针,在打开上述某个文件之后,open指向memory_open
//drivers/char/mem.c
//主设备号为1的内存设备关联的open函数,用于为不同从设备号的文件关联不同的文件操作函数
static int memory_open(struct inode * inode, struct file * filp)
memory_open根据从设备号选择不同的函数指针,如下面两个
//drivers/char/mem.c
//用于/dev/null
static const struct file_operations null_fops = {
.llseek = null_lseek,
.read = read_null,
.write = write_null,
.splice_write = splice_write_null,
};
//drivers/char/random.c
//用于/dev/random
const struct file_operations random_fops = {
.read = random_read,
.write = random_write,
.poll = random_poll,
.ioctl = random_ioctl,
};
其他设备类型也采用了同样的方法。首先根据主设备号设置一个特定的文件操作集。其中包含的操作,接下来可以由根据从设备号选择的其他操作替代。
6.4.3 读写操作
调用标准库的读写操作,将向内核发出一些系统调用(第8章讨论),最终调用file_operations结构中相关的操作(主要是read和write)。这些方法的具体实现依设备而不同
上述的内存设备不必费力与实际的外设交互,它们只需调用其他的内核函数来完成。
例如,/dev/null设备使用read_null
和write_null
函数实现比特位桶的读写操作。快速浏览一下内核源代码,就可以知道这些函数的实现实际上非常简单。
从空设备读取时,什么也不返回,这很容易实现。返回的结果是一个长度为0字节的数据流。向该设备写入的数据直接被忽略,但无论任何长度的数据,都会报告写入成功。
更复杂的字符设备,需要提供读写真正有意义结果的函数,但一般机制是不变的。
6.5 块设备操作函数和结构体介绍
在内核通过VFS接口支持的外设中,块设备总数是第二多的.块设备驱动程序面对的情况比字符设备复杂得多。导致这种情况的环境因素很多,主要包括:块设备层的设计导致需要持续地调整块设备的速度,块设备的工作方式,块设备层开发方面的历史原因
块设备与字符设备在3个主要方面有根本的不同
- 可以在数据中的
任何位置进行访问
。对字符设备来说,这是可能的,但不是必然的 - 数据总是
以固定长度的块进行传输
。即使只请求一个字节的数据,设备驱动程序也会从设备取出一个完全块的数据。相比之下,字符设备能够返回单个字节。 - 对块设备的访问
有大规模的缓存
,即已经读取的数据保存在内存中。如果再次需要,则直接从内存获得。写入操作也使用了缓存,以便延迟处理。这对字符设备没有意义(如键盘)。因为,字符设备的每次读请求都必须真正与设备交互才能完成。
块
(block)是一个特定长度的字节序列,用于保存在内核和设备之间传输的数据。块的长度可通过软件方法修改。扇区
(sector)是一个固定的硬件单位,指定了某个设备最少能够传输的数据量。块不过是连续扇区的序列而已。因此,块长度总是扇区长度的整数倍
。由于扇区是特定于硬件的常数,它也用来指定设备上某个数据块的位置。内核将每个块设备都视为一个线性表,由按整数编号的扇区或块组成。
当前几乎所有常见块设备的扇区长度都是512字节,块长度则有512、1 024、2 048、4 096字节等。但应该注意到,块的最大长度,会受到特定体系结构的内存页长度的限制
。IA-32系统支持的块长度为4 096字节,因为其内存页长度是4 096字节。另一方面,IA-64和Alpha系统能够处理8 192字节的块
块长度的选择相对自由,这对许多块设备应用程序有好处,例如,读者在学习文件系统实现方式时,会注意到这一点。文件系统会将硬盘划分为不同长度的块
,以便在处理许多小文件或少数大文件时分别优化性能。因为文件系统能够将传输的块长度与自身块长度匹配,所以实现起来要容易得多。
块设备层不仅负责寻址块设备,也负责执行其他任务,以提高系统中所有块设备的性能。此类任务包括预读算法
的实现,在内核判断应用程序稍后将需要使用某数据时,会使用预读算法从块设备预先将数据读入内存。
如果预读的数据不是立即需要,那么块设备层必须提供缓冲区/缓存来保存这些数据。这种缓冲区/缓存不仅用于保存预读数据,也用于临时保存经常用到的块设备数据。
内核在访问块设备时,使用了大量的技巧和优化。不过,本章没有一一详解这部分内容。下面将讲解块设备层的各种组件以及交互方式。
6.5.1 块设备inode表示 bdev_inode
块设备有一组属性,由内核管理。内核使用所谓的请求队列管理
(request queue management),使得此类与设备的通信尽可能高效。它能够缓存并重排读写数据块的请求
。请求的结果也同样保存在缓存中,使得可以用非常高效的方式读取/重新读取数据。在进程重复访问文件的同一部分时,或不同进程并行访问同一数据时,该特性尤其有用。
块设备层各结构关系:
裸块设备由struct block_device表示
内核将与块设备关联的block_device实例紧邻块设备的inode之前存储。该行为由以下数据结构实现:
//fs/block_dev.c
struct bdev_inode {
struct block_device bdev;//裸块设备, 伪文件系统
struct inode vfs_inode;//块设备的inode
};
所有表示块设备的inode都保存在伪文件系统bdev中(参见8.4.1节),这些对用户层不可见。这使得可以使用标准的VFS函数,来处理块设备inode的集合。
辅助函数bdget
就利用了这一点。给定由dev_t表示的设备号,该函数查找伪文件系统,看对应的inode是否已经存在。如果存在,则返回指向inode的指针。由于struct bdev_inode的存在,利用返回的inode指针,立即就可以找到该设备的block_device实例。如果此前设备没有打开过,致使inode尚未存在,bdget和伪文件系统会确保自动分配一个新的bdev_inode并进行适当的设置。
与字符设备层相比,块设备层提供了丰富的队列功能,每个设备都关联了请求队列
。这种队列也是块设备层最复杂的部分了。如图6-9所示,各个数组项(简化形式)中都包含了指向各种结构和函数的指针
每个块设备都必须提供一个探测函数(kobj_map->probes->get),该函数通过blk_register_region
直接注册到内核,或者通过下文讨论的gendisk对象,使用add_disk
间接地注册到内核。该函数由文件系统代码调用,以找到匹配的gendisk对象。
对块设备的读写请求不会立即执行
对应的操作。相反,这些请求会汇总起来,经过协同之后传输到设备。因此,对应设备文件的file_operations结构中没有保存用于执行读写操作的具体函数
。相反,其中包含了通用函数,如generic_read_file和generic_write_file,这两个函数会在第8章讨论。
值得注意的是,其中只使用了通用函数,这是块设备的一个特征。在字符设备的情形中,这些函数都是特定于驱动程序的。所有特定于硬件的细节都在请求执行时处理。所有其他函数处理的都是一个抽象队列,它们从缓冲区/缓存接收结果
,一般不与底层设备交互(除非绝对必要)。因而,从read或write系统调用到实际与外设通信的路径长而复杂。
6.5.2 块设备数据结构,块设备block_device,硬盘gendisk,分区hd_struct,块设备操作函数集结构体block_device_operations,请求队列request_queue
-
块设备
//include/linux/fs.h //块设备描述符, 伪文件系统,分区或主块设备都对应一个该结构,(个人理解:sda,sda1,sda2都对应一个这个结构体,sda为主块,sda1,sda2为该块的两个分区) struct block_device { dev_t bd_dev;//该设备(分区)的设备号,是一个键值用于搜索 /* not a kdev_t - it's a search key */ struct inode * bd_inode;//表示该块设备的inode, 因为能通过bdget 获取因此是冗余的数据,该字段将在未来的内核版本中删除 /* will die */ int bd_openers;//统计用 do_open 打开该块设备的次数 struct list_head bd_inodes;//链表头,该链表包含了表示该块设备的设备特殊文件的所有inode,块设备的inode和普通文件的inode是不同的概念不能混淆 void * bd_holder;//块设备持有者,blkdev_open(blkdev_open调用bd_claim) 打开块设备并请求独占使用时,与该设备文件关联的 file 实例会持有该块设备 struct block_device * bd_contains;//如果当前结构实例表示的是一个分区,则指向对应主块设备的block_device结构;如果表示的是主块设备,则指向自己(个人理解:sda1,sda2关联到sda) unsigned bd_block_size; struct hd_struct * bd_part;//如果该实例描述的是一个分区,则该变量指向分区信息,与struct gendisk结构体中的分区数组指针共享 unsigned bd_part_count;//如果该实例描述的是一个分区,该变量记录了分区被打开的次数(在用 rescan_partitions 重新扫描分区前,要保证该计数值为0。如果 bd_part_count 大于零,则禁止重新扫描,因为旧的分区仍然在使用中) int bd_invalidated;//设置为1,表示该分区在内核中的信息无效,因为磁盘上的分区已经改变,下一次打开该设备时,将要重新扫描分区表,在使用 add_disk 注册一个新磁盘时置为1 struct gendisk * bd_disk;//每个struct block_device的该成员都指向其对应的通用磁盘数据结构(一个磁盘上多个分区,所以每个磁盘结构实例可以对应多个block_device结构实例,但磁盘只有一个gendisk) struct list_head bd_list;//链表元素,用于跟踪记录系统中所有可用的 block_device 实例。该链表的表头为全局变量 all_bdevs 。使用该链表,无需查询块设备数据库,即可遍历所有块设备 unsigned long bd_private;//持有者的私有数据,只有这个数据结构实例的持有者才能使用 };
要成为持有者,必须对块设备成功调用bd_claim。bd_claim在bd_holder是NULL指针时才会成功,即尚未注册持有者。在这种情况下,bd_holder指向当前持有者,可以是内核空间中任意一个地址。调用bd_claim,实际上是向内核的其他部分表明,该块设备已经与之无关了。
块设备的持有者没有固定规定。例如在Ext3文件系统中,会持有已装载文件系统的外部日志的块设备,并将超级块注册为持有者。如果某个分区用作交换区,那么在用swapon系统调用激活该分区之后,页交换代码将持有该分区。
在使用blkdev_open打开块设备并请求独占使用时(6.5.4节会讨论这一点),与该设备文件关联的file实例会持有该块设备。
我们注意到有一点很有趣:当前在内核源代码中尚未使用bd_private字段。即使当前没有持有者需要将私有数据关联到块设备,但bd_claim机制仍然很有用。
最后,使用bd_release释放块设备。 -
通用硬盘和分区
尽管struct block_device对设备驱动程序层表示一个块设备,而另一个抽象则强调与通用的内核数据结构的关联
。由此角度来看,我们对块设备自身并不感兴趣。相反,硬盘的概念(可能包含子分区)更为有用。设备上分区的信息不依赖于表示该分区的block_device实例。实际上,将一个磁盘添加到系统中时,内核将读取并分析底层块设备上的分区信息,但并不会对各个分区创建block_device实例
。为此,内核使用以下数据结构,对已经分区的硬盘提供了一种表示(与统计簿记有关的一些字段已经省去)://include/linux/genhd.h //通用硬盘结构体,表示已经分区的硬盘或没有任何分区的设备,这个结构体必须由alloc_disk函数创建实例,del_gendisk删除实例,一个磁盘对应一个gendisk,一个gendisk上有一个或多个分区block_device struct gendisk { int major;//块设备的主设备号 /* major number of driver */ int first_minor;//起始次设备号(每个分区会有自己的从设备号,如sda,sda1,sda2;sda是一个硬盘,sda1和sda2表示这个硬盘上的两个分区) int minors; /* maximum number of minors, =1 for //次设备号的个数(分区个数),=1表明磁盘无分区 * disks that can't be partitioned. */ char disk_name[32];//主驱动程序的名称(磁盘名)。它用于在sysfs和 /proc/partitions 中表示该磁盘 /* name of major driver */ struct hd_struct **part;//这是一个数组,每个分区对应一个项,索引是从设备号,与struct block_device结构体共享 /* [indexed by minor] */ int part_uevent_suppress;//设置正值,在检测到设备的分区信息改变时,就不会向用户空间发送热插拔事件。只有在磁盘尚未完全集成到系统之前,初始分区扫描时,才会这样做 struct block_device_operations *fops;//指向特定于设备的底层操作函数集 struct request_queue *queue;//块设备的请求队列,所有针对该设备的请求都会放入该请求队列中,经过I/O scheduler的处理再进行提交 void *private_data;//指向私有的驱动程序数据 sector_t capacity;//指定了磁盘容量,单位是扇区 int flags; struct device *driverfs_dev;//标识该磁盘所属的硬件设备 struct kobject kobj;//该变量中的entry加到 block_subsys 块设备子系统的链表中 }; //磁盘分区描述符,用于描述该分区在设备内的信息 struct hd_struct { sector_t start_sect;//分区在块设备上的起始扇区号 sector_t nr_sects;//分区的大小,分区的扇区个数 struct kobject kobj;//该变量其中的parent指针指向struct gendisk中的kobject结构体 }; //block/genhd.c //minors:从设备数目,要创建的磁盘的分区数,创建 gendisk,分配了 指向各个分区的hd_struct的指针所需的空间 struct gendisk *alloc_disk(int minors) //fs/partitions/check.c //向通用硬盘数据结构gendisk添加一个新的分区hd_struct void add_partition(struct gendisk *disk, int part, sector_t start, sector_t len, int flags) // 释放gendisk void del_gendisk(struct gendisk *disk)
-
各个部分的联系
struct block_device、struct gendisk和struct hd_struct之间的关系:
对块设备上已经打开的每个分区,都对应于一个struct block_device的实例
。对应于分区的block_device实例通过bd_contains关联到对应于整个块设备的block_device实例。所有的block_device实例都通过bd_disk,指向其对应的通用磁盘数据结构gendisk。要注意,尽管一个已分区的磁盘有多个block_device实例,但只对应于一个gendisk实例。一个磁盘对应一个 gendisk,一个 gendisk 有多个分区(part成员为hd_struct数组) ,一个分区为一个 block_device(bd_part成员为hd_struct指针) 和 hd_struct(block_device->bd_part和gendisk->part), block_device->bd_part在gendisk->part数组中
此外,通用硬盘gendisk还集成到kobject框架中,如图6-11所示。块设备子系统由kset实例
block_subsys
表示。kset 中有一个链表,每个gendisk实例所包含的kobject实例都放置在该链表上。由struct hd_struct表示的分区对象也包含了一个嵌入的kobject。概念上,分区是硬盘的子元素,这一点也被内核对象的数据结构所捕获。hd_struct中嵌入的kobject的parent指针,将指向通用硬盘gendisk中嵌入的kobject。
-
块设备操作
//块设备操作函数集合 struct block_device_operations { int (*open) (struct inode *, struct file *);//打开 int (*release) (struct inode *, struct file *);//关闭 int (*ioctl) (struct inode *, struct file *, unsigned, unsigned long);//自定义特殊命令 int (*media_changed) (struct gendisk *);//检查存储介质是否已经改变,对软盘和ZIP软驱等设备使用,硬盘通常不支持该函数,因为不能换盘 int (*revalidate_disk) (struct gendisk *);//使存储介质重新生效.当前只有在直接移除旧的介质并替换新的介质时(没有进行卸载和加载)才有必要使用该函数 struct module *owner;//如果驱动程序实现为模块,那么 owner 字段指向内存中的一个模块结构。否则,该成员为 NULL 指针。 }
这些函数不是由VFS代码直接调用,而是由块设备的标准文件操作def_blk_fops中包含的操作间接地调用。
- file_operations->open
- block_device_operations->open
- file_operations->open
-
请求队列
块设备的读写请求放置在一个队列上,称之为请求队列。gendisk结构包括了一个指针,指向这个特定于设备的队列
//include/linux/blkdev.h typedef void (request_fn_proc) (struct request_queue *q); typedef int (make_request_fn) (struct request_queue *q, struct bio *bio); typedef int (prep_rq_fn) (struct request_queue *, struct request *); typedef void (unplug_fn) (struct request_queue *); typedef int (merge_bvec_fn) (struct request_queue *, struct bio *, struct bio_vec *); typedef void (prepare_flush_fn) (struct request_queue *, struct request *); typedef void (softirq_done_fn)(struct request *); //请求队列,块设备的读写请求放在这个队列结构体中 struct request_queue { /* * Together with queue_head for cacheline sharing */ /* 主要成员,表头,待处理请求的链表,请求队列中的请求用链表组织在一起.用于构建一个I/O请求的双向链表,链表成员数据类型是request,代表向块设备读取数据的一个请求。内核会重排该链表以取得更好的I/O性能(提供了几个算法来执行I/O调度器任务),elevator成员中有对应的函数集合 */ struct list_head queue_head; struct request *last_merge;//指向队列中首先可能合并的请求描述符 elevator_t *elevator;//算法调度函数集合来对queue_head操作(电梯算法) /* * the queue request freelist, one for reads and one for writes */ struct request_list rq;//用作 request 实例的缓存,此外还提供了两个计数器,来记录可用的空闲输入和输出请求数目 /* 向队列添加新请求的标准接口,内核期望驱动从设备读写数据时内核调用,也称为策略例程(strategy routine)*/ request_fn_proc *request_fn; /* 创建新请求.内核的标准实现向请求链表添加请求,链表请求够多就会调用 request_fn 处理请求,驱动可以自己实现该函数,因为某些设备不使用队列(RAM磁盘),这可能是由于按任意顺序访问数据都不会影响性能,也可能是由于驱动程序比内核更了解如何处理请求,因而使用内核的标准方法不会带来好处(例如卷管理器)。但这种惯例还是比较罕见的。*/ make_request_fn *make_request_fn; /* 将处理请求的命令发给硬件设备.请求预备函数。大多数驱动程序不使用该函数,会将对应的指针设置为NULL 。如果实现了该函数,它会产生所需的硬件命令,用于在发送实际的请求之前预备一个请求。辅助函数 blk_queue_prep_rq 会设置给定队列的 prep_rq_fn 。*/ prep_rq_fn *prep_rq_fn; /* 拔出一个块设备时调用.插入的设备不会执行请求,而是将请求收集起来,在拔出时执行。巧妙地使用该方法,能够提高块设备层的性能。*/ unplug_fn *unplug_fn; /* 确定是否允许向一个现存的请求增加更多数据。由于请求队列的长度通常是固定的,限制了其中请求的数目,因此内核可使用这种机制来避免可能的问题。但更专门的驱动程序,特别是复合设备的驱动,队列长度的限制可能不同,因此需要提供该函数。内核提供了辅助例程 blk_queue_merge_bvec 来设置队列的 merge_bvec_fn 。*/ merge_bvec_fn *merge_bvec_fn; /* 在预备刷出队列时,即一次性执行所有待决请求之前,会调用 prepare_flush_fn 。在该方法中,设备可以进行必要的清理。辅助函数 blk_queue_ordered 可以用来向请求队列设置特定的方法。*/ prepare_flush_fn *prepare_flush_fn; /* 通知驱动程序I/O请求已经完成。对于大的请求来说,完成请求,即完成所有I/O,可能是一个耗时的过程。在内核版本2.6.16开发期间,添加了使用软中断SoftIRQ异步完成请求的特性。可以通过调用 blk_complete_request 要求异步完成请求, softirq_done_fn 在这种情况下用作回调函数,通知驱动程序请求已经完成。*/ softirq_done_fn *softirq_done_fn; /* * Auto-unplugging state */ struct timer_list unplug_timer; int unplug_thresh; /* After this many requests *//*队列拔出的阈值,读写请求数(request_queue->rq->count)超过该值则拔出队列进行处理*/ unsigned long unplug_delay; /* After this many jiffies *//*提交请求后定时器在经过该值之后超时拔出队列进行处理*/ struct work_struct unplug_work; struct backing_dev_info backing_dev_info; /* * queue needs bounce pages for pages above this limit */ unsigned long bounce_pfn; gfp_t bounce_gfp; /* * various queue flags, see QUEUE_* below */ unsigned long queue_flags;//队列标志每一位不同含义,如 QUEUE_FLAG_PLUGGED 等 /* * queue settings */ unsigned long nr_requests; /*定义每个队列中request结构的最大数目。通常,该数目设置为BLKDEV_MAX_RQ,其值为128,但可以使用/sys/block/<device>/queue/nr_requests修改*//* Max # of requests */ unsigned int nr_congestion_on;//表示队列请求数目达到拥塞的阈值。发生拥塞时,空闲request结构的数目必定小于该值 unsigned int nr_congestion_off;//指定了一个阈值,该阈值表示队列不再被认为拥塞。当空闲request结构的数目多于该值时,内核认为该队列不是拥塞的 unsigned int nr_batching; unsigned int max_sectors;//指定设备在单个请求中可以处理的扇区的最大数目。长度单位是具体设备的扇区长度( hardsect_size ) unsigned int max_hw_sectors; unsigned short max_phys_segments;//指定用于运输不连续数据的分散—聚集请求中,不连续的段的最大数目 unsigned short max_hw_segments;//与 max_phys_segments 相同,但考虑了(可能的)I/O MMU所进行的重新映射。该成员指定了驱动程序可以传递到设备的地址/长度对的最大数目 unsigned short hardsect_size;//指定了设备的物理扇区长度,该值通常是512。只有少数非常新的设备使用不同的设置 unsigned int max_segment_size;//单个请求的最大段长度(按字节计算) }; //block/ll_rw_blk.c //用于产生一个标准的请求队列,在调用add_disk激活磁盘之前调用来创建请求队列 struct request_queue * blk_init_queue_node(request_fn_proc *rfn, spinlock_t *lock, int node_id)
请求队列可以在系统超负荷时插入。接下来新的请求都会处于未处理状态,直至队列“拔出”,该特性称之为队列插入(queue plugging)。以
unplug_为前缀的各个成员用于实现一种定时器机制
,在一定时间间隔后自动“拔出”队列。unplug_fn负责实际的拔出操作。
6.5.3 向系统添加磁盘和分区
-
添加分区
add_partition
负责向通用硬盘数据结构添加一个新的分区 -
添加磁盘
add_disk
//fs/partitions/check.c //识别分区,将分区结构加入磁盘结构 int rescan_partitions(struct gendisk *disk, struct block_device *bdev)
6.5.4 打开块设备文件
用户程序open一个块设备,虚拟文件系统调用file_operations
结构的open函数,最终会调用到blkdev_open
- sys_open
- inode->i_fop->open(blkdev_open函数)
- do_open
- disk->fops->open(块设备驱动的open函数)
- rescan_partitions 扫描分区,将分区结构加入磁盘结构中
- add_partition 将分区加入磁盘结构
- add_partition 将分区加入磁盘结构
- do_open
- inode->i_fop->open(blkdev_open函数)
6.5.5 请求结构 request
//include/linux/blkdev.h
//发送给块设备的请求结构体
struct request {
struct list_head queuelist;//链表元素,表头为request_queue->queue_head,等待请求被执行
struct list_head donelist;//链表元素,存入请求执行完成(即所有需要的I/O操作都已经执行完毕)的链表头中
struct request_queue *q;//指向该请求所属的请求队列(如果有的话),存着queuelist的表头,这只对异步完成请求有必要。通常不需要该链表
unsigned int cmd_flags;//包含了用于请求的一组通用标志,每一位的含义看enum rq_flag_bits
enum rq_cmd_type_bits cmd_type;//请求的类型
/* 有hard_前缀涉及的是实际硬件而非虚拟设备,通常两组变量的值相同,但在使用RAID或逻辑卷管理器(Logical Volume Manager)时可能会有差别,因为这些机制实际上是将几个物理设备合并为一个虚拟设备。*/
sector_t sector; /* next sector to submit *///数据传输的起始扇区
sector_t hard_sector; /* next sector to complete *///数据传输的起始扇区,有hard_前缀涉及的是实际硬件而非虚拟设备
unsigned long nr_sectors; /* no. of sectors left to submit *///当前请求还需要传输的扇区数目
unsigned long hard_nr_sectors; /* no. of sectors left to complete *///当前请求还需要传输的扇区数目,有hard_前缀涉及的是实际硬件而非虚拟设备
unsigned int current_nr_sectors;//当前请求在当前段中还需要传输的扇区数目
unsigned int hard_cur_sectors;//当前请求在当前段中还需要传输的扇区数目,有hard_前缀涉及的是实际硬件而非虚拟设备
struct bio *bio;//用于在系统和设备之间传输数据,标识传输尚未完成的当前BIO实例
struct bio *biotail;//指向最后一个BIO实例,因为一个请求中可使用多个BIO
/* 两个私有数据,由处理该请求的I/O调度器设置 */
void *elevator_private;
void *elevator_private2;
struct gendisk *rq_disk;
unsigned long start_time;
unsigned short nr_phys_segments;//请求中段的数目,分散—聚集I/O操作相关
unsigned short nr_hw_segments;//经过I/O MMU可能的重排序之后段的数目,分散—聚集I/O操作相关
/*
* when request is used as a packet command carrier
*/
/* 当请求用于向设备传送控制命令,请求可以用作数据包命令载体 */
unsigned int cmd_len;//命令长度
unsigned char cmd[BLK_MAX_CDB];//要发送的命令放入这个数组
};
/*请求类型*/
enum rq_cmd_type_bits {
REQ_TYPE_FS = 1, /* fs request *//* 文件系统请求 *///最常见的请求类型是REQ_TYPE_FS:它用于与块设备之间的实际数据传输。
REQ_TYPE_BLOCK_PC, /* scsi command *//* scsi命令 */
REQ_TYPE_SENSE, /* sense request *//* 请求检测,用于scsi/atapi设备 */
REQ_TYPE_PM_SUSPEND, /* suspend request *//* 电源管理命令,要求暂停设备 */
REQ_TYPE_PM_RESUME, /* resume request *//* 电源管理命令,要求唤醒设备 */
REQ_TYPE_PM_SHUTDOWN, /* shutdown request *//* 电源管理命令,要求将设备停机 */
REQ_TYPE_FLUSH, /* flush request *//* 刷出请求 */
REQ_TYPE_SPECIAL, /* driver defined type *//* 驱动程序定义的请求类型 */
REQ_TYPE_LINUX_BLOCK, /* generic block layer message *//* 一般性的块设备层消息 */
};
/*请求标志*/
enum rq_flag_bits {
__REQ_RW, /* not set, read. set, write *//* 该位没设置表示读,设置了表示写 */
__REQ_FAILFAST, /* no low level driver retries *//* 底层驱动程序不进行重试 */
__REQ_SORTED, /* elevator knows about this request *//* 该请求由I/O调度器使用 */
__REQ_SOFTBARRIER, /* may not be passed by ioscheduler *//* 不能由I/O调度器传递 */
__REQ_HARDBARRIER, /* may not be passed by drive either *//* 不能由驱动程序传递 */
__REQ_FUA, /* forced unit access *//* 启用FUA( forced unit access),即写入的数据直接存储到块设备的介质,不使用块设备自身的缓存 */
__REQ_NOMERGE, /* don't touch this for merging *//* 该请求不能进行合并 */
__REQ_STARTED, /* drive already may have started this one *//* 驱动程序已经开始处理该请求 */
__REQ_DONTPREP, /* don't call prep for this one *//* 对该请求,不要调用请求队列的prep_rq_fn方法来预先准备发送到设备的命令 */
__REQ_QUEUED, /* uses queueing *//* 表明潜在设备具有排队处理多个命令的能力 */
__REQ_ELVPRIV, /* elevator private data attached *//* 附加了I/O调度器的私有数据 */
__REQ_FAILED, /* set if the request failed *//* 如果请求失败,则置位 */
__REQ_QUIET, /* don't worry about errors *//* 不报告失败 */
__REQ_PREEMPT, /* set for "ide_preempt" requests *//* 对ide_preempt请求置位,此类请求用于IDE磁盘,将强占队列中的当前请求 */
__REQ_ORDERED_COLOR, /* is before or after barrier *//* 在屏障之前或之后 */
__REQ_RW_SYNC, /* request is sync (O_DIRECT) *//* 请求是同步的(O_DIRECT) */
__REQ_ALLOCED, /* request came from our alloc pool *//* 请求来自分配池 */
__REQ_RW_META, /* metadata io request *//* 元数据I/O请求 */
__REQ_NR_BITS, /* stops here *//* 到此为止 */
};
6.5.6 块设备io操作单位,BIO
//include/linux/bio.h
//块设备io操作单位,BIO不仅可用于传输数据,还可以传输诊断信息
struct bio {
sector_t bi_sector; /* device address in 512 byte //第一个要访问的扇区
sectors */
struct bio *bi_next; /* request queue link *//* 将与请求关联的几个BIO组织到一个单链表中 */
struct block_device *bi_bdev;//bio要操作的块设备
unsigned short bi_vcnt; /* how many bio_vec's *//* bi_io_vec数组的长度 */
unsigned short bi_idx; /* current index into bvl_vec *//* bi_io_vec数组中,当前处理数组项的索引 */
/* Number of segments in this BIO after
* physical address coalescing is performed.
*/
unsigned short bi_phys_segments;//物理地址合并后段的数目,重映射前的段数.传输中段的数目,由I/O MMU重新映射之前的数值。
/* Number of segments after physical and DMA remapping
* hardware coalescing is performed.
*/
unsigned short bi_hw_segments;//物理和DMA重映射硬件合并后的段数.传输中段的数目,由I/O MMU重新映射之后的数值。
unsigned int bi_size; /* residual I/O count *//* 请求所涉及数据的长度,单位为字节 */
struct bio_vec *bi_io_vec; /* the actual vec list *//* 实际的I/O向量数组,每个数组项指向一个内存页的page结构体,用于从设备接收数据、向设备发送数据 */
bio_end_io_t *bi_end_io;//函数指针,在硬件传输完成时,设备驱动程序必须调用bi_end_io。这使得块设备层有机会进行清理,或唤醒等待该请求结束的睡眠进程。
void *bi_private;//驱动使用的私有数据
bio_destructor_t *bi_destructor; /* destructor *///析构函数,在从内存删除一个bio实例之前调用
};
struct bio_vec {
struct page *bv_page;//内存页实例,用于从设备接收数据、向设备发送数据.这里显然可以使用高端内存域的页面,这些页帧无法直接映射到内核中,因而无法通过内核虚拟地址访问。对于下面的情况,该做法很有用:数据直接复制给用户空间应用程序,而应用程序可以使用页表访问高端内存域页帧。这些内存页可以但不必一定按连续方式组织,这简化了分散—聚集I/O操作的实现
unsigned int bv_len;//指定了用于数据的字节数目(如果整页不完全填充的话)。要传输数据的长度
unsigned int bv_offset;//该页内的偏移量,通常该值为0,因为页边界通常用作I/O操作的边界
};
6.5.7 提交请求
在本节中,讨论内核将数据请求提交给外设的机制。这也涉及缓冲和请求的重排,以减少磁头寻道的移动,或捆绑多个操作以提高性能。此外还涵盖了设备驱动程序的操作,驱动程序与具体的硬件交互以处理请求。本节还包括了虚拟文件系统中与设备文件相关的通用代码,这部分代码通过设备文件又关联到用户应用程序以及内核的其他部分。读者从第8和16章会看到,内核将已经从块设备读取的数据保存在缓存中,以便在未来重复提交同样的请求时重用。我们在这里对缓存这方面不是特别感兴趣。我们将讨论内核如何向设备提交物理请求来读取和写入数据。
内核分两个步骤提交请求:
- 它首先创建一个bio实例以描述请求,然后将该实例嵌入到请求中request->bio,并置于请求队列上。
- 接下来内核将处理请求队列并执行bio中的操作。
bio的创建只涉及指定块设备上的位置并提供用于保存/传输相关数据的页帧。
在BIO创建后,调用make_request_fn产生一个新请求以插入到请求队列。请求通过request_fn提交。
直到内核版本2.6.24,这些操作的实现都在block/ll_rw_blk.c
中。文件名看起来很古怪,实际上是low level read write handling for block device的缩写。以后的内核将实现拆分到一些较小的文件中,命名遵循block/blk-*.c
的模式。
-
创建请求并加入请求队列submit_bio
submit_bio
是一个关键函数,负责根据传递的bio实例创建一个新请求,并使用make_request_fn将请求置于驱动程序的请求队列上//block/ll_rw_blk.c //根据传递的bio实例创建一个新请求,并使用make_request_fn将请求置于驱动程序的请求队列上 void submit_bio(int rw, struct bio *bio)
内核中的一些块设备驱动程序(磁盘和设备映射器)不能使用内核提供的标准函数,而需要自行实现相应的函数。但这些自行实现的函数会递归调用
generic_make_request
!
尽管递归函数调用在用户空间是没问题的,但由于内核中栈空间非常有限,因此可能会引起问题。因而需要确定一个合理的值,来限制递归的最大深度。task_struct结构,其中包含了两个与BIO处理有关的成员用于限制递归深度为1//include/linux/sched.h struct task_struct { /*递归调用累积的块设备信息,用于对块设备发出读写请求时防止对generic_make_request函数的递归调用,使递归最大深度限制为1*/ struct bio *bio_list, **bio_tail; };
图:
__make_request
合并bio,申请request,将bio数据填入request,插入队列阻止队列中的请求被处理,等待更多请求加入被合并,设置定时器稍后拔出队列,将request加入队列。如果是同步请求则拔出队列,立即处理请求-
submit_bio 根据传递的bio实例创建一个新请求,并使用make_request_fn将请求置于驱动程序的请求队列上
- generic_make_request 根据bio产生请求并发送给设备驱动程序,处理了递归调用的问题
- __generic_make_request 根据bio产生请求并发送给设备驱动程序
- bdev_get_queue 获取块设备磁盘的请求队列
- q->make_request_fn(q, bio) 根据bio产生请求并发送给设备驱动程序。对大多数设备,发送操作调用内核的标准函数(__make_request)完成。
- __generic_make_request 根据bio产生请求并发送给设备驱动程序
- generic_make_request 根据bio产生请求并发送给设备驱动程序,处理了递归调用的问题
-
__make_request struct request_queue请求队列make_request_fn回调函数的默认实现,合并bio,申请request,将bio数据填入request
- 如果请求队列空,插入队列并将请求加入队列,并判断是否处理队列
- 否则如果队列不空,合并请求,并判断是否处理队列
- elv_merge 检查请求能否与请求队列中的请求合并
- e->ops->elevator_merge_fn(q, req, bio) I/O调度器的实现函数。检查一个新的请求是否可以与现存请求合并,它还指定了请求插入到请求队列中的位置
- elv_merge 检查请求能否与请求队列中的请求合并
- get_request_wait 分配一个新请求实例
- blk_plug_device 插入队列,并设置拔出队列的时间
- add_request 将请求加入队列
- __elv_add_request
- elv_insert 将请求插入请求队列,如果队列请求达到阈值则拔出队列
- __generic_unplug_device 拔出请求队列
- elv_insert 将请求插入请求队列,如果队列请求达到阈值则拔出队列
- __elv_add_request
- 同步请求调用 __generic_unplug_device 拔出请求队列
-
-
请求插入请求队列elv_insert
就性能而言,我们当然希望重排各个请求,并将可能的请求合并为更大的请求,以提升数据传输的性能。显然,这只适用于队列包含了多个可以合并的请求的情况。因而,内核首先需要在队列中汇集一些请求,然后一次性处理所有请求,这样就自动创造了合并请求的时机。
内核使用队列插入(queue plugging)机制,来有意阻止请求的处理
。请求队列可能处于空闲状态或者插入状态
。如果队列外于空闲状态,队列中等待的请求将会被处理
。否则,新的请求只是添加到队 列 , 但 并 不 处 理
。 如 果 队 列 处 于 插 入 状 态 , 则 request_queue 的 queue_flags 成 员 中 QUEUE_FLAG_PLUGGED标志置位。内核提供了blk_queue_plugged辅助函数检查该标志。在讲解
__make_request
时,已经提到,内核用blk_plug_device
插入一个队列,但如果没有发送同步请求,则不会显式拔出队列。几种拔出队列情况:
- 调用
submit_bio
提交io操作请求后,最后调用了blk_plug_device
函数设置了定时器到期后拔出队列,如果是同步请求则调用__generic_unplug_device
立即拔出队列 - 当读写请求的数目(保存在请求链表的
count
数组的两个数组项中(request_queue->rq->count))达到unplug_thresh
指定的阈值,则elv_insert
中调用__generic_unplug_device
以触发拔出操作,使得等待的请求得到处理
- 调用
-
执行请求
在请求队列中的请求即将处理时,会调用特定于设备的
request_fn
函数。该任务与硬件的关联非常紧密,因此内核不会提供默认的实现。相反,内核总是使用blk_init_queue
注册队列时传递的方法sample_request
是一个与硬件无关的示例例程,用于说明所有驱动程序在request_fn中所执行的基本步骤。void sample_request (request_queue_t *q) { int status; struct request *req; //遍历队列中的请求 while ((req = elv_next_request(q)) != NULL) //检查实际上传输的是否是数据,BIO不仅可用于传输数据,还可以传输诊断信息 if (!blk_fs_request(req)) end_request(req, 0); continue; //执行请求 status = perform_sample_transfer(req); //从队列删除请求 end_request(req, status); } int perform_sample_transfer(request *req) { switch(req->cmd){ case READ: /* 执行特定于硬件的数据读取功能 */ break; case WRITE: /* 执行特定于硬件的数据写入功能 */ break; default: return -EFAULT; } }
6.5.8 I/O调度
内核采用的各种用于调度和重排I/O操作
的算法,称之为I/O调度器(对比通常的进程调度器,或网络中控制通信数据量的数据包调度器)。通常,I/O调度器也称作电梯(elevator)。它们由下列数据结构中的一组函数表示
//include/linux/elevator.h
//电梯算法函数集合结构体(I/O调度器)
struct elevator_ops
{
elevator_merge_fn *elevator_merge_fn;//检查一个新的请求是否可以与现存请求合并,它还指定了请求插入到请求队列中的位置。
elevator_merged_fn *elevator_merged_fn;//在两个请求已经合并后调用(它执行清理工作,并返回I/O调度器中因为合并而不再需要的那部分管理数据)。
elevator_merge_req_fn *elevator_merge_req_fn;//将两个请求合并为一个请求,合并后调用上面那个函数
elevator_allow_merge_fn *elevator_allow_merge_fn;
elevator_dispatch_fn *elevator_dispatch_fn;//从给定的请求队列中选择下一步应该调度执行的请求。
elevator_add_req_fn *elevator_add_req_fn;//向请求队列添加请求
elevator_activate_req_fn *elevator_activate_req_fn;
elevator_deactivate_req_fn *elevator_deactivate_req_fn;
elevator_queue_empty_fn *elevator_queue_empty_fn;//检查队列是否包含可供处理的请求
elevator_completed_req_fn *elevator_completed_req_fn;
elevator_request_list_fn *elevator_former_req_fn;//查找给定请求的前一个请求。在进行合并时,这个函数很有用
elevator_request_list_fn *elevator_latter_req_fn;//查找给定请求的后一个请求。在进行合并时,这个函数很有用
elevator_set_req_fn *elevator_set_req_fn;//在创建新请求时调用(此时请求尚未与任何队列关联),使得I/O调度器可以分配、初始化和释放用于管理的数据结构
elevator_put_req_fn *elevator_put_req_fn;//在释放回内存管理子系统时调用(此时请求不再与任何队列关联,或已经完成),使得I/O调度器可以分配、初始化和释放用于管理的数据结构
elevator_may_queue_fn *elevator_may_queue_fn;
elevator_init_fn *elevator_init_fn;//队列初始化时调用,效果等同于构造函数
elevator_exit_fn *elevator_exit_fn;//队列释放时调用,效果等同于析构函数
void (*trim)(struct io_context *);
};
每个I/O调度器都封装在下列数据结构中,其中还包含了供内核使用的其他管理信息:
//include/linux/elevator.h
/* I/O调度器结构体,下面列举调度器的实例
* 1.elevator_noop,是一个非常简单的I/O调度器,将新来的请求按“先来先服务”的原则依次添加到队列,以便进行处理。请求会进行合并但无法重排。noop(no operation,空操作)I/O调度器仅对于能够自行重排请求的智能硬件,才是一个好的选择。对于没有活动部件的设备(因而没有寻道时间),如闪存盘,该调度器也是很好的
* 2.iosched_deadline,用于两个目的:它试图最小化磁盘寻道(即,读/写磁头的移动)的次数,并尽可能确保请求在一定时间内处理完成。在后一种情况下,会使用内核的定时器机制实现单个请求的“到期时间”。在前一种情况下,需要使用冗长的数据结构(红黑树和链表)分析各个请求,并按照最低延迟的原则来重排请求,以降低磁盘寻道的次数,linux-2.5之前的默认调度器
* 3.iosched_as实现了预测调度器,顾名思义,它会尽可能预测进程的行为。当然这并不容易,但该调度器假定读请求不是彼此完全独立的,在此前提下试图实现预测调度。在应用程序向内核提交一个读请求时,该调度器会作出以下假定:在一定时间内会有另一个相关请求提交。如果读请求在磁盘忙于写操作期间提交,那么这个假定就很重要。为确保良好的交互行为,内核会延迟写操作,并优先选择读操作。如果第一个读请求之后立即恢复写操作,则需要一个磁盘寻道操作,而稍后会有另一个新的读请求到达,这又消除了寻道操作的效果。在这种情况下,较好的选择是在第一个读请求之后不移动磁头,等待稍后的下一个读请求到达。如果在预期时间内第二个读请求没有到达,内核就可以恢复写操作。linux-2.5-linux-2.6.17的默认调度器
* 4.iosched_cfq提供了完全公平排队(complete fairness queuing)的特性。它围绕几个队列展开,所有的请求都在这些队列中排序。同一给定进程的请求,总是在同一队列中处理。时间片会分配到每个队列,内核使用一个轮转算法来处理各个队列。这确保了I/O带宽以公平的方式在不同队列之间共享。如果队列的数目大于等于同时进行I/O的进程数目,这就意味着I/O带宽也公平地分配到了各个进程之上。一些实际问题(如多个进程映射到同一队列、可变的请求长度、不同的I/O优先级,等等)使得带宽的分配不是完全公平的,但该方法基本上很好地达到了预期目标。linux-2.6.18开始的默认调度器*/
struct elevator_type
{
struct list_head list;//I/O调度器的链表元素,表头为全局变量 elv_list
struct elevator_ops ops;//电梯算法函数集合结构体(I/O调度器)
struct elv_fs_entry *elevator_attrs;//sysfs中的属性保存在这
char elevator_name[ELV_NAME_MAX];//调度器的名字
struct module *elevator_owner;
};
6.5.9 ioctl的实现
ioctl使得我们能够使用特殊的、特定于设备的功能,这些功能无法通过普通的读写操作访问。这种支持通过ioctl系统调用实现,该系统调用可以用于普通的文件
该系统调用在sys_ioctl
实现,但主要工作由vfs_ioctl
完成。所需的ioctl通过传递的一个常数指定。通常,为此定义了一些预处理器符号常数
6.6 i/o资源介绍
I/O端口和I/O内存是两种概念上的方法,用以支持设备驱动程序和设备之间的通信。为使得各种不同的驱动程序彼此互不干扰,有必要事先为驱动程序分配端口和I/O内存范围。这确保几种设备驱动程序不会试图访问同样的资源。
X86体系中,具有两个地址空间:IO空间
和内存空间
,而RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,即内存空间。
寄存器在 IO空间 就是IO端口
寄存器在 内存空间 就是IO内存
6.6.1 i/o资源管理
-
树数据结构
Linux提供了一个通用构架,用于在内存中构建数据结构。这些结构描述了系统中可用的资源,使得内核代码能够管理和分配资源。注意,其中关键的数据结构是resource,定义如下://include/linux/ioport.h //资源结构体 struct resource { resource_size_t start;//start和end可以自己定义使用解释,一般用来表示某个地址空间中的一个区域 resource_size_t end; const char *name;//资源名,在proc文件系统中有用 unsigned long flags;//用于更准确地描述资源及其当前状态 struct resource *parent, *sibling, *child;//组成树型结构,父节点,兄弟节点,子节点 };
resource树形结构:
如何将该层次结构用于设备驱动程序?我们来考察一个系统总线的例子,其上附接了一块网卡。网卡支持两个输出,每个都分配了一个特定的内存区域,用于数据的输入和输出。总线自身也有一个I/O内存区域,其中一些部分由网卡使用。
该方案可以完美地融入到树形层次结构中。总线的内存区域理论上占用了(假想的)0和1 000之间的内存范围,充当根结点(最高的父结点)。网卡要求使用100和199之间的内存区域,这是根结点(总线自身)的一个子结点。网卡的子结点表示各个网络输出,分配的I/O内存区分别是100到149和150到199。原来较大的资源区域重复地细分为较小的部分,每次细分都表示了抽象模型中的一个层次。因此,子结点可用于将内存区划分为越来越小、功能越来越具体的部分。
-
请求和释放资源
确保可靠地配置资源(无论何种类型),内核必须提供一种机制来分配和释放资源。一旦资源已经分配,则不能由任何其他驱动程序使用。请求和释放资源,无非是从资源树中添加和删除项而已
-
请求资源
内核提供了__request_resource
函数,用于请求一个资源区域。这函数需要一系列参数,包括一个指向父结点的指针,资源区域的起始和结束地址,表示该区域名称的字符串//kernel/resource.c //申请资源,将new插入root子节点中 int request_resource(struct resource *root, struct resource *new)
-
释放资源
//kernel/resource.c //释放资源,将old节点从树型结构中去除 int release_resource(struct resource *old)
-
6.6.2 I/O内存
资源、管理还有一个很重要的方面是I/O内存的分配方式,因为在所有平台上这都是与外设通信的主要方法(IA-32除外,其中I/O端口更为重要)。
I/O内存不仅包括与扩展设备通信直接使用的内存区域,还包括系统中可用的物理内存和ROM存储器,以及包含在资源列表中的内存(可以使用proc文件系统中的iomem文件,显示所有的I/O内存)
所有分配的I/O内存地址,都通过一棵资源树管理,树的根结点是全局内核变量iomem_resource
。上述输出中,每个缩进表示一个子结点层次。具有相同的缩进层次的所有项是兄弟结点,会通过链表联系起来。下图给出了该数据结构在内存中的一部分,proc文件系统中的信息即由此获取
但在使用I/O内存
时,分配内存区域并不是所需的唯一操作。取决于总线系统和处理器类型,可能必需将扩展设备的地址空间映射到内核地址空间
中之后,才能访问该设备(称之为软件I/O映射)。这是通过使用ioremap
内核函数适当设置系统页表而实现的,内核源代码中有若干不同地方使用了该函数,其定义是体系结构相关的。同样地,还提供了特定于体系结构的iounmap
函数来解除映射。
将一个物理地址映射到处理器的虚拟地址空间
中,使得内核可以使用该地址。就设备驱动程序而言,这意味着扩展总线的地址空间映射到CPU的地址空间中,使得能够用普通内存访问函数操作总线/设备。
- 在一些平台上,即使I/O区域已经映射之后,仍然有必要使用专门方法来访问各个I/O内存区域,而不是直接对指针进行反引用。表6-3给出了所有平台上用于完成该工作的函数(通常声明在
<asm-arch/io.h>
中)。可移植的驱动程序总是应该使用这些函数,即使在某些平台上与I/O区域通信时不需要其他步骤而只要简单的指针反引用(如IA-32系统)。
6.6.3 I/O端口
I/O端口是一种与设备和总线通信的流行方法,特别是在IA-32平台上。类似于I/O内存,按良好范例编写的驱动程序在访问所需的区域之前,相应的区域必须已经注册。糟糕的是,处理器无法检查注册是否已经完成。
kernel/resource.c中的 ioport_resource 充当资源树的根结点。 proc文件系统中的 ioports
文件可以显示已经分配的端口地址。
可以同时通过I/O端口和I/O内存访问一个设备
在汇编程序层次上,端口通常必须通过特殊的处理器命令访问。因此内核提供了对应的宏,以便向驱动程序开发者提供一个系统无关的接口。这些在表6-4中列出
即使在不使用端口的体系结构上,也会声明和实现这些函数(通常通过访问“普通”的I/O内存),以简化不同体系结构上驱动程序的开发。
6.7 总线系统
扩展设备通过设备驱动程序处理,而驱动程序与内核其余的代码通过一组固定的接口通信,因而扩展设备/驱动程序对核心的内核源代码没什么影响,但内核需要解决一个更基本的问题:设备如何通过总线附接到系统的其余部分。
与具体设备的驱动程序相比,总线驱动程序与核心内核代码的工作要密切得多。另外,总线驱动程序向相关的设备驱动程序提供功能和选项的方式,也不存在标准的接口。这是因为,不同的总线系统之间,使用的硬件技术可能差异很大。但这并不意味着,负责管理不同总线的代码没有共同点。相似的总线采用相似的概念,还引入了通用驱动程序模型,在一个主要数据结构的集合中管理所有系统总线,采用最小公分母的方式,尽可能降低不同总线驱动程序之间的差异。
内核支持大量总线,可能涉及多种硬件平台,也有可能只涉及一种平台。所以我不可能详细地讨论所有版本,这里我们只会仔细讨论PCI总线。因为其设计相对现代,而且具备一种强大的系统总线所应有的所有共同和关键要素。此外,在Linux支持的大多数体系结构上都使用了PCI总线。我还会讨论广泛使用、系统无关的的USB总线,该总线用于外设
6.7.1 通用驱动程序模型
现代总线系统在布局和结构的细节上可能有所不同,但也有许多共同之处,内核的数据结构即反映了这个事实。结构中的许多成员用于所有的总线(以及相关设备的数据结构中)。在内核版本2.6开发期间,一个通用驱动程序模型(设备模型,device model)并入内核,以防止不必要的复制。所有总线共有的属性封装到特殊的、可以用通用方法处理的数据结构中,再关联到总线相关的成员。
通用驱动程序模型主要基于第1章讨论的通用对象模型,与10.3节将讨论的sysfs文件系统也有密切的关联。
-
设备的表示
驱动程序模型采用一种特殊数据结构来表示几乎所有
总线类型通用的设备属性
。该结构直接嵌入到特定于总线的数据结构中,而不是通过指针引用,这与前文介绍的kobject相似。其定义如下://include/linux/device.h //总线共有(通用)的设备属性结构体,嵌入到特定的总线结构体中 struct device { struct klist klist_children;//链表头,链表元素为device->knode_parent,所有子设备放入这个链表 struct klist_node knode_parent; /* node in sibling list *///链表元素,链表头为device->klist_children,链表头在父设备中 struct klist_node knode_driver;//链表元素,链表头为device->driver->klist_devices,用于将所有被同一驱动程序管理的设备连接到一个链表中。如系统中安装了两个相同的扩展卡。driver指向控制该设备的设备驱动程序的数据结构 struct klist_node knode_bus; struct device *parent;//指向父设备的device,knode_parent加入父设备的链表 struct kobject kobj; char bus_id[BUS_ID_SIZE]; /* position on parent bus *///指定了该设备在宿主总线上的唯一位置(不同总线类型使用的格式也会有所不同)。例如,设备在PCI总线上的位置由一个具有以下格式的字符串唯一地定义:<总线编号>:<插槽编号>.<功能编号> struct bus_type * bus; /* type of bus device is on *///指向该设备所在总线 struct device_driver *driver; /* which driver has allocated this//指向控制该设备的设备驱动程序的数据结构,knode_driver作为链表元素加入链表 device */ void *driver_data; /* data private to the driver *///驱动程序的私有成员,不能由通用代码修改。它可用于指向与设备协作必需、但又无法融入到通用方案的特定数据 void *platform_data; /* Platform specific data, device//私有成员,可用于将特定于体系结构的数据和固件信息关联到设备。通用驱动程序模型也不会访问这些数据。 core doesn't touch it */ void (*release)(struct device * dev);//析构函数,用于在设备(或device实例)不再使用时,将分配的资源释放回内核。 };
klist和klist_node数据结构是我们熟悉的list_head数据结构的增强版,其中增加了与锁和引用计数相关的成员。klist是一个表头,而klist_node是一个链表元素。通过该机制实现的各种链表操作位于
<klist.h>
。这种类型的链表只用于通用设备模型
,内核的其余部分不会使用内核提供了标准函数
device_register
,用于将一个新设备添加到内核的数据结构。该函数在下文讨论。device_get和device_put
一对函数用于引用计数。
通用驱动程序模型也为设备驱动程序
单独设计了一种数据结构//include/linux/device.h //驱动共有(通用)的设备属性结构体 struct device_driver { const char * name; struct bus_type * bus;//指向一个表示总线的对象,并提供特定于总线的操作 struct kobject kobj; struct klist klist_devices;//链表的表头,链表元素为device->knode_driver,该驱动程序控制的所有设备的device实例。 struct klist_node knode_bus;//用于连接一条公共总线上的所有设备 int (*probe) (struct device * dev);//用于检测系统中是否存在能够用该设备驱动程序处理的设备 int (*remove) (struct device * dev);//删除系统中的设备时被调用 void (*shutdown) (struct device * dev);//电源管理调用 int (*suspend) (struct device * dev, pm_message_t state);//电源管理调用 int (*resume) (struct device * dev);//电源管理调用 };
驱动程序使用内核的标准函数
driver_register
注册到系统中 -
总线的表示
通用驱动程序模型不仅表示了设备,还用另一个数据结构表示了总线,定义如下:
//include/linux/device.h //总线结构体 struct bus_type { const char * name;//是总线的文本名称。它用于在sysfs文件系统中标识该总线 struct kset subsys;//提供与总线子系统的关联,对应的总线出现在/sys/bus/busname struct kset drivers;//与总线关联的所有驱动集合,与sysfs文件系统关联,内核还会创建链表(klist_drivers)来保存相同的数据 struct kset devices;//与总线关联的所有设备集合,与sysfs文件系统关联,内核还会创建链表(klist_devices)来保存相同的数据 struct klist klist_devices;//链表头,与总线关联的所有设备链表 struct klist klist_drivers;//链表头,与总线关联的所有驱动链表 int (*match)(struct device * dev, struct device_driver * drv);//查找与给定设备匹配的驱动程序 int (*uevent)(struct device *dev, struct kobj_uevent_env *env);//在设备注册、移除,或者状态更改时,内核负责发送通知事件到用户空间 int (*probe)(struct device * dev);//在有必要将驱动程序关联到设备时,会调用probe。该函数检测设备在系统中是否真正存在 int (*remove)(struct device * dev);//删除驱动程序和设备之间的关联。例如,在将可热插拔的设备从系统中移除时,会调用该函数 void (*shutdown)(struct device * dev);//用于电源管理 int (*suspend)(struct device * dev, pm_message_t state);//用于电源管理 int (*resume)(struct device * dev);//用于电源管理 };
-
注册过程
-
注册总线
在可以注册设备及其驱动程序之前,需要有总线。因此bus_register
函数向系统添加一个新总线。 -
注册设备
//drivers/base/core.c //注册设备 int device_register(struct device *dev)
-
注册设备驱动程序
//drivers/base/driver.c //注册驱动程序 int driver_register(struct device_driver * drv)
-
6.7.2 PCI总线
PCI是peripheral component interconnect的缩写,是英特尔公司开发的一种标准总线,它迅速在系统组件和体系结构厂商中间确立了自身的地位,成为一种非常流行的总线。其原因不在于市场策略方面的技巧,而是因为其技术水平。它成功替代了ISA总线(ISA是影响过这个程序设计里、最令人苦恼的灾难这一)。为一劳永逸地解决ISA总线设计上固有的缺陷,PCI总线规定了以下设计目标。
- 支持高传输带宽,以适应具有大数据流的多媒体应用。
- 简单且易于自动化配置附接的外设。
- 平台独立性,即不绑定到特定的处理器类型或系统平台。
PCI规范存在几个版本,因为在PCI的发展过程中添加了各种增强特性,以涵盖更多新近技术进展。
例如,最近一个主要的更新涉及热插拔(在系统运行时,添加和移除设备)。
由于PCI规范与处理器无关的性质,该总线不仅用于IA-32系统(及其或多或少的直接后继IA-64和AMD64),还用于其他的体系结构(如PowerPC、Alpha、SPARC等)。看看为该总线生产的大量廉价的扩展卡,就知道为什么了。
-
PCI系统的布局
在讨论内核中PCI的实现之前,我们先来了解该总线的主要原理。如果读者需要更多详细的讲解,可以参考硬件技术方面的教科书(例如[BH01]
)-
设备标识
系统的某个PCI总线上的每个设备,都由一组3个编号标识- 总线编号(bus number)是该设备所在总线的编号,编号照例从0开始。PCI规范准许每个系统最多255个总线。
- 插槽编号(slot number)是总线内部的一个唯一标识编号。一个总线最多能够附接32个设备。不同总线上的设备插槽编号可能相同。
- 功能编号(function number)用于在一个扩展卡上,实现包括多个(经典意义上)扩展设备的设备。例如,为节省空间,可以将两个网卡放置在一块扩展卡上,在这种情况下通过不同的功能编号来指定不同的接口。笔记本电脑中多功能芯片组使用很多,这些芯片组附接到PCI总线,以最小的空间集成了一整套扩展设备(IDE控制器、USB控制器、调制解调器、网络等)。这些扩展设备必须通过功能编号进行区分。PCI标准将一个设备上功能部件的最大数目定义为8。
每个设备都通过一个
16位编号唯一地标识,其中8个比特位用于总线编号,5个比特位用于插槽编号,3个比特位用于功能编号
。驱动程序无需费力处理这种极其紧凑的记法,因为内核建立了一个数据结构的网络,其中也包含了同样的信息,从C语言的角度来看更容易处理 -
地址空间
有3个地址空间支持与PCI设备的通信- I/O空间通过32个比特位描述,因而,对用于与设备通信的端口地址,提供了最大4 GB的空间
- 取决于处理器类型,数据空间由32或64个比特位描述。当然,只有CPU字长为64位时,才支持后者。系统中的设备分配到上述两个地址空间中,因而有唯一的地址。
- 配置空间包含了各个设备的类型和特征的详细信息,以省去危险的自动探测工作
这些地址空间会根据处理器类型映射到系统虚拟内存中的不同位置,使得内核和设备驱动程序能够访问对应的资源
-
配置信息
与许多先前的总线相比,PCI总线是一种无跳线系统
。换言之,扩展设备能够完全通过软件手段配置,而无需用户干涉。为支持这种配置,每个PCI设备都有一个256字节长的配置空间
,其中包括该设备的特点和要求的有关信息。尽管按当前计算机的内存配置水平来看,256字节初看起来数目很小,但其中可以存储大量信息,图6-23给出了PCI规范规定的配置空间的布局:
尽管该结构长度必须是256字节,但
只有前64字节是标准化的。其余空间可以自由使用
,通常用于在设备和驱动程序之间交换附加信息。该信息的结构(或应该)定义在硬件文档中。也应注意到,并非前64字节中所有的信息都是强制性的。一些项是可选的,如果设备不需要,可以填充字节0
。在图中,强制性的项用深灰色突出显示Vendor ID和Device ID唯一地标识了厂商和设备类型
。前者由PCI Special Interest Group(一个工业界的联盟)分配,用于标识各个公司(英特尔ID为0x8086)。后者可以由厂商自由选择,只用于确保在地址空间中不会出现重复。这两个ID合起来通常称之为设备的签名
。两个具有相似名称的附加字段:Subsystem Vendor ID和Subsystem Device ID
,也可以同时使用,以更精确地描述设备的通用接口。Rev ID用于区分不同的设备修订级别
。这有助于用户选择设备驱动程序的版本,新版本的设备可能消除了已知的硬件故障或添加了新特性。Class Code字段
用于将设备分配到各种不同的功能组,该字段分为两部分。前8个比特位表示基类(base class)
,而剩余的16个比特位表示基类的一个子类
。基类及其子类的例子如下给出(我使用了<pci_ids.h>
中对应常数的名称)。//include/linux/pci_ids.h #define PCI_BASE_CLASS_STORAGE 0x01 /* 基类:大容量存储器 */ #define PCI_CLASS_STORAGE_SCSI 0x0100 /* 子类:SCSI控制器 */ #define PCI_CLASS_STORAGE_IDE 0x0101 /* 子类:IDE控制器 */ #define PCI_CLASS_STORAGE_RAID 0x0104 /* 子类:RAID控制器,用于组合多个磁盘驱动器 */ #define PCI_BASE_CLASS_NETWORK 0x02 /* 网络 */ #define PCI_CLASS_NETWORK_ETHERNET 0x0200 /* 以太网 */ #define PCI_CLASS_NETWORK_FDDI 0x0202 /* FDDI */ #define PCI_BASE_CLASS_SYSTEM 0x08 /* 系统组件 */ #define PCI_CLASS_SYSTEM_DMA 0x0801 /* DMA控制器 */ #define PCI_CLASS_SYSTEM_RTC 0x0803 /* 实时时钟 */
6个基地址字段每个包含32个比特位
,用于定义PCI设备和系统其余部分通信所用的地址。在涉及64位设备时(Alpha和Sparc64系统上是有可能的),需要将基地址字段两两合并,以描述内存中的位置。这样可用的基地址数目就只有3个了。就内核而言,剩余字段中相关的只有IRQ编号,可以接受0和255之间的任意值,用于指定设备使用的中断。值为0表示该设备并不使用中断尽管PCI标准支持最多255个中断,实际能够使用的编号通常会受限于具体的体系结构。在这样的系统上,如果要支持比中断请求线数目更多的设备数目,那么必须采用诸如中断共享(在第14章讨论)之类的方法
剩余的字段由硬件使用,与软件无关
-
-
内核中的实现
内核为PCI驱动程序提供了一个广泛的框架,可以粗略地划分为两个类别
- PCI系统的初始化(和资源的分配,这取决于系统),以及预备对应的数据结构以反映各个总线和设备的容量和能力,使得能够较为容易地操作总线/设备
- 支持访问所有PCI选项的标准化函数接口
在各个不同类型的系统上,PCI系统初始化有时差异非常大。例如,IA-32系统会在启动时间借助于BIOS自行分配所有相关的PCI资源,内核需要做的事情很少。Alpha系统没有BIOS或适当的等价物,相关工作必须由内核完成。因此,在讲解内核内存中相关的数据结构时,我会假定所有PCI设备和总线都已经完全初始化
-
数据结构
内核提供了几个数据结构来管理系统的PCI结构。这些结构声明在<pci.h>
中,通过一个由指针构成的网络互相连接。在仔细讲解结构成员的定义之前,我首先给出一个概述- 系统中的各个总线由pci_bus的实例表示
pci_dev
结构表示各个设备、扩展卡和功能部件- 每个驱动程序都通过
pci_driver
的一个实例描述
内核定义了两个全局的list_head变量(都定义在
<pci.h>
中),用作PCI数据结构形成的网络的入口。pci_root_buses
列出了系统中所有的PCI总线。在“向下”扫描数据结构以查找附接到各个总线的所有设备时,该链表是一个起点pci_devices
将系统中的所有PCI设备都连接起来,不考虑总线结构的影响。在驱动程序想要搜索它支持的所有设备时,该链表很有用,因为此时无需关注总线拓扑结构(当然,通过PCI数据结构之间的许多关联,是可以找到与一个设备关联的总线的,读者在后面会看到)。
-
总线的表示
在内存中,每个PCI总线都通过pci_bus数据结构的一个实例表示,该结构定义如下://include/linux/pci.h #define PCI_BUS_NUM_RESOURCES 8 //PCI总线结构体,系统中的各个总线由 pci_bus 的实例表示 struct pci_bus { struct list_head node; /* node in list of buses *///链表元素,链表头为pci_root_buses或pci_bus->children struct pci_bus *parent; /* parent bus this bridge is on *///此桥接器(总线)所在的父总线 struct list_head children; /* list of child buses *///子总线链表头,链表元素为pci_bus->node struct list_head devices; /* list of devices on this bus *///总线上的设备的链表头,链表元素为 pci_dev->bus_list //除总线0以外,所有系统总线都可以只通过一个PCI桥接器寻址,桥接器类似于一个普通的PCI设备。每个总线的 self 成员是一个指针,指向描述桥接器的 pci_dev 实例 struct pci_dev *self; /* bridge device as seen by parent */ //保存该总线在虚拟内存中占用的地址区域,该数组包含4项,第一个数组项包含用于I/O端口的地址区域。第二项总是保存I/O内存区域的地址范围 struct resource *resource[PCI_BUS_NUM_RESOURCES]; /* address space routed to this bus */ struct pci_ops *ops; /* configuration access functions *///访问配置信息的各函数 void *sysdata; /* hook for sys-specific extension *///用于特定于硬件的扩展,使得总线结构可以关联到特定于硬件(因而也是特定于驱动程序)的函数,尽管内核很少使用该选项 struct proc_dir_entry *procdir; /* directory entry in /proc/bus/pci *proc/bus/pci中的目录项 unsigned char number; /* bus number *///总线号 unsigned char primary; /* number of primary bridge *///主桥接器编号 unsigned char secondary; /* number of secondary bridge *///次桥接器编号 unsigned char subordinate; /* max number of subordinate buses *///下级总线的最大数目 char name[48];//总线名,(如:PCI Bus #01 ),当然也可以是空白 };
在PCI子系统初始化时,会建立所有系统总线的列表。这些总线以两种不同的方式彼此连接。第一种方法使用一个线性链表,表头是上文所述的pci_root_buses全局变量,包括系统中所有的总线。node成员充当链表元素。
parent和children结构成员,方便了以树的形式表示PCI总线的二维拓扑结构 -
设备管理
struct pci_dev数据结构用于表示系统中的各个PCI设备
在这里,内核不仅将术语设备解释为扩展卡,还意指用于连接各个总线的PCI桥接器。不仅有将PCI总线彼此连接起来的桥接器,(在旧的系统上)还有连接PCI总线与ISA总线的桥接器。//include/linux/pci.h //PCI设备结构体 struct pci_dev { struct list_head global_list; /* node in list of all PCI devices *///链表元素,链表头为全局变量pci_devices struct list_head bus_list; /* node in per-bus list *///链表元素,链表头为 pci_bus->devices struct pci_bus *bus; /* bus this device is on *///用于建立设备和总线之间的逆向关联。指向设备所在总线 //仅当该设备表示连接两个PCI总线的PCI桥接器时,该成员才包含有效值(否则为 NULL 指针),如果确实如此(桥接器),则 subordinate 指向“下级”PCI总线的数据结构 struct pci_bus *subordinate; /* bus this device bridges to */ void *sysdata; /* hook for sys-specific extension *///存储特定于驱动程序的数据 struct proc_dir_entry *procent; /* device entry in /proc/bus/pci *///用于管理设备在 proc 文件系统中的目录项 //devfn 和 rom_base_reg 之间的所有成员只是用于存储PCI的配置空间数据(PCI总线有256字节配置空间,里面有配置信息),其中填充的是系统初始化时从硬件读取的数据 unsigned int devfn; /* encoded device & function index */ unsigned short vendor; unsigned short device; unsigned short subsystem_vendor; unsigned short subsystem_device; unsigned int class; /* 3 bytes: (base,sub,prog-if) */ u8 revision; /* PCI revision, low byte of class word */ u8 hdr_type; /* PCI header type (`multi' flag masked out) */ u8 pcie_type; /* PCI-E device/port type */ u8 rom_base_reg; /* which config register controls the ROM */ u8 pin; /* which interrupt pin this device uses */ struct pci_driver *driver; /* which driver has allocated this device *///指向用于控制该设备的驱动程序 struct device dev; /* Generic device interface *///关联到通用设备模型 /* device is compatible with these IDs */ unsigned short vendor_compatible[DEVICE_COUNT_COMPATIBLE]; unsigned short device_compatible[DEVICE_COUNT_COMPATIBLE]; int cfg_size; /* Size of configuration space */ /* * Instead of touching interrupt line and base address registers * directly, use the values stored here. They might be different! */ unsigned int irq;//指定了该设备使用的中断数目 //保存了驱动程序为I/O内存分配的资源实例 struct resource resource[DEVICE_COUNT_RESOURCE]; /* I/O and memory regions + expansion ROMs */ };
-
驱动程序函数
PCI层中最后一个基本的数据结构是pci_driver。它用于实现PCI驱动程序,表示了通用内核代码和设备的底层硬件驱动程序之间的接口。每个PCI驱动程序都必须将其函数填到该接口中,使得内核能够一致地控制可用的驱动程序。
该结构定义如下(为简明起见,我省去了用于实现电源管理的项)://pci驱动程序结构体 struct pci_driver { char *name;//通常,是实现驱动程序的模块名称 //内核支持设备ID的一个完整的搜索列表,该列表存储了支持的设备的(子)设备和(子)厂商ID(PCI前256字节配置信息中前64字节固定定义中的Vendor ID和Device ID),确定驱动程序所支持的设备(因为一个驱动可能支持多个不同(大体上兼容)的设备) const struct pci_device_id *id_table; /* must be non-NULL for probe to be called */ //检测该驱动程序是否支持某个PCI设备(该过程称之为探测,也是该函数指针得名的原因) int (*probe) (struct pci_dev *dev, const struct pci_device_id *id); /* New device inserted */ //用于移除设备。只有系统支持热插拔(通常不支持)时,移除PCI设备才有意义 void (*remove) (struct pci_dev *dev); /* Device removed (NULL if not a hot-plug capable driver) */ struct device_driver driver;//用于建立与通用设备模型(struct device)的关联 };
-
注册驱动程序
PCI驱动程序可以通过pci_register_driver
注册。该函数十分简单。其主要任务是,对相关函数已经分配的一个pci_device实例,填充一些剩余的字段。该实例使用driver_register
传递到通用设备层//include/linux/pci.h //PCI驱动程序注册 static inline int __must_check pci_register_driver(struct pci_driver *driver) //include/linux/mod_devicetable.h /*pci设备id,pci配置信息中规定的*/ struct pci_device_id { //对应PCI前64字节的信息,vendor id:0-2字节,device id:2-4字节 __u32 vendor, device; /* Vendor and device ID or PCI_ANY_ID*/ //对应PCI前64字节的信息,subsystem vendor id:41-43字节,subsystem device id:43-45字节 __u32 subvendor, subdevice; /* Subsystem ID's or PCI_ANY_ID */ //位掩码用于过滤可能的类别 __u32 class, class_mask; /* (class,subclass,prog-if) triplet */ kernel_ulong_t driver_data; /* Data private to the driver */ };
在很多情况下,(struct pci_device_id)只描述一个设备既不必要也不可取。如果驱动程序支持大量兼容设备,这将很快导致源代码中出现无穷的声明列表。这不仅难于阅读,而且也存在一个现实的缺点。我们可能只是因为驱动程序没有将某个兼容设备加入到支持设备的列表中,所以在运行时就无法找到该设备。因此内核提供了通配符常数
PCI_ANY_ID,可以与任何PCI设备ID匹配
。我们来考察一下,该机制如何用于下述的eepro100驱动程序中(这是一个广泛使用的芯片组,由英特尔公司生产)://drivers/net/e100.c #define INTEL_8255X_ETHERNET_DEVICE(device_id, ich) {\ PCI_VENDOR_ID_INTEL, device_id, PCI_ANY_ID, PCI_ANY_ID, \ PCI_CLASS_NETWORK_ETHERNET << 8, 0xFFFF00, ich } static struct pci_device_id e100_id_table[] = { INTEL_8255X_ETHERNET_DEVICE(0x1029, 0), INTEL_8255X_ETHERNET_DEVICE(0x1030, 0), INTEL_8255X_ETHERNET_DEVICE(0x1031, 3), INTEL_8255X_ETHERNET_DEVICE(0x1032, 3), INTEL_8255X_ETHERNET_DEVICE(0x1033, 3), ... INTEL_8255X_ETHERNET_DEVICE(0x245D, 2), INTEL_8255X_ETHERNET_DEVICE(0x27DC, 7), { 0, } };
0x8086是英特尔公司的厂商ID,英特尔公司是该芯片组的生产商(驱动程序也可以使用预处理器常数
PCI_VENDOR_ID_INTEL
定义相同的值)。每个项包含一个特定的设备ID,标识了当前市场上出售的该设备的所有版本。子厂商和子设备ID在这里是无关的,因此由PCI_ANY_ID
表示。这意味着任何子厂商或子设备都被认为是有效的。
内核提供了pci_match_id
函数,将PCI设备数据与ID表中的数据进行比较。它将给定的pci_dev实例与ID表进行比较,来确定该设备是否包含在ID表中。//drivers/pci/pci-driver.c /* 将PCI设备数据与ID表中的数据进行比较。它将给定的 pci_dev 实例与ID表进行比较,来确定该设备是否包含在ID表中如果一个ID表项的所有成员与设备配置中的所有成员都相同,那么就找到了匹配项。如果ID表中一个字段为特殊值 PCI_ANY_ID ,那么无论 pci_dev 实例中对应字段的值如何,都是匹配的*/ const struct pci_device_id *pci_match_id(const struct pci_device_id *ids, struct pci_dev *dev)
6.7.3 USB
USB(Universal Serial Bus,通用串行总线)开发于上世纪90年代末。它是一种外部总线,用于满足不断发展的PC的需求,并用于建立针对新类型计算机的解决方案,如手持设备、PDA等。作为一种通用的外部总线,在用于连接中低数据传输速率的设备
时(如鼠标、网络摄像头、键盘),USB很有优势。但带宽要求更高的设备如外部硬盘、光驱、CD刻录机也可以通过USB总线运行。USB 1.1的最大传输速率限于12 兆比特/秒,该标准的2.0版本最高速率提升到480 兆比特/秒
。
在设计该总线时,尤其要注意易用性,以方便不熟练的计算机用户。因此,热插拔和相关驱动程序的透明安装是USB设计的核心。与早期的PCI热插拔卡(很难弄到)和PCMCIA/PC卡(价格较高,几乎没有使用)相比,USB是将内核的热插拔能力提供给大量用户的第一种总线
-
特性和运作模式
相比其他总线,USB有哪些特别特性?除了对最终用户的易用性之外,必须提及该总线用于排布附接设备的拓扑结构,这很容易使人想起网络结构。从单一的根控制器开始,设备通过集线器连接到树形结构中,如图6-24所示。用这样的方式,一个系统最多可附接127个终端设备(2的7次方,所以加上根集线器最多支持7层结构,USB2.0规定)
每个usb设备都有一个唯一的7bit从机地址(不包括0地址)
设备
从来不会直接连接到宿主机控制器
,总是通过集线器
。为确保驱动程序的视图一致,内核用一个小的仿真层替换了根控制器,使得系统的其余部分将该控制器视为一个虚拟集线器。这简化了驱动程序的开发。在讨论USB相关问题时,
设备
这个术语应该谨慎使用,因为它被分为3个层次- 设备(device)是用户可以连接到USB总线的任何东西,例如集成了麦克风的摄像机,等等。上例表明,一个设备可能由几种功能部件组成,分别通过不同的驱动程序控制。
- 每个设备由一个或多个配置(configuration)组成,配置支配着设备的全局特征。例如,一个设备可能带有两个接口。如果总线提供电源,则使用其中一个接口;如果使用外部电源,则使用另一个接口
- 同样,每个配置由一个或多个接口(interface)组成,每个接口提供不同的设置选项。对摄像机可以想象到3个接口:只启用麦克风、只启用摄像机或同时启用两者。根据所选的接口,设备的带宽需求可能不同
- 最后,每个接口可能有一个或多个端点(end point),由驱动程序控制。可能有这样的情形:一个驱动程序控制设备的所有端点,但每个端点都可能需要一个不同的驱动程序。在上述的例子中,两个端点分别是图像视频单元和麦克风。另一个带有两个不同端点的设备的例子是,集成了一个USB集线器的USB键盘,可以将其他USB设备连接到集线器上(从根本上讲,集线器是一种特殊的USB设备)
所有USB设备都划分到不同的类别中。在内核源代码中,我们可以看到这样的划分:各个驱动程序的源代码按照所属类别,归入不同的目录。drivers/usb/包含若干子目录,其内容如下:
- image目录下是图形和视频设备的驱动程序,如数字照相机、扫描仪,等等
- input目录下是输入输出装置的驱动程序,用于与计算机用户的交互。此类别中典型的设备不仅包括键盘和鼠标,还有触摸屏、数据手套,等等
- media目录下是很多多媒体设备的驱动程序,近几年涌现了很多此类设备
- net目录下是通过USB附接到计算机的网卡的驱动,因而此类设备通常称之为适配器,负责桥接以太网和USB
- storage目录下是大容量储存设备的驱动程序,如硬盘等
- 在class目录下,包括了支持USB定义的某个标准类别设备的所有驱动程序
- core包含了宿主机适配器的驱动程序,这种设备上通常会附接一个USB链
粗略地说,驱动程序的源代码源自以下3个领域:
标准设备
(如键盘、鼠标等),这些设备总是可以通过相同的驱动程序支持,无需考虑设备厂商;专有硬件
,如MP3播放器和其他需要特殊驱动程序的小器具;宿主机适配器的驱动程序
,这种设备通过一种不同的总线系统(通常是PCI)附接到系统的其他部分,它负责建立与USB设备链的(物理)连接USB标准定义了4种不同的传输模式,内核必须考虑到所有这4种模式:
- 控制传输(control transfer)涉及传输所需的控制信息,
(主要)用于设备的初始配置
。此类通信必须安全可靠,但只需要较窄的带宽。其中通过预定义的令牌传输各种控制命令,USB标准定义了令牌的符号名称和语义,如GET_STATUS、SET_INTERFACE等。在内核源代码这些令牌都在<usb.h>
中声明为预处理器常数,其前缀为USQ_REQ_
,以防止名称冲突。标准强制要求了一个命令的最小集合,所有设备都必须支持这些命令。但厂商可以随意添加其他特定于设备的命令,厂商提供的驱动程序必须能够理解/使用这些命令 - 块传输(bulk transfer)按数据包发送数据,可以占据总线的全部带宽。在这种模式下,数据传输的安全性由总线保证。换句话说,发送的数据总是原样到达其目的地。扫描仪或大容量存储器之类的设备会使用这种模式
- 中断传输(interrupt transfer)类似于块传输,但按一定的周期重复。 驱动程序可以自由地定义周期长度(在一定的限度内)。网卡和类似设备会优先选择使用这种传输模式
- 同步传输(isochronous transfer)具有特殊作用,它是能够使用固定的预定义带宽的唯一方法(尽管不可靠)。在某些方面,这种模式可以与网卡的数据报技术类比,后者将在第12章讨论。在需要确保连续数据流,而能够容忍偶尔数据丢失的情况下,该传输模式是最适用的。使用这种模式的一个主要的例子就是网络摄像头,该设备通过USB总线发送视频数据
-
驱动程序的管理
内核中按两个层次实现USB总线系统:- 宿主机适配器的驱动程序必须是可用的。该适配器必须为USB链提供连接选项,并承担与终端设备的电子通信。适配器自身必须连接到另一个系统总线(当前,有3种不同宿主机适配器类型,分别称之为OHCI、EHCI和UHCI,这些涵盖了市售的所有控制器类型)。
- 设备驱动程序与各个USB设备通信,并将设备的功能导出到内核的其他部分,进而到用户空间。这些驱动程序与宿主机控制器通过一种标准化接口交互,因而控制器类型与USB驱动程序是不相关的。任何其他方法显然都是不切实际的,因为需要为每个USB设备开发与宿主机控制器相关的驱动程序
接下来讲解USB驱动程序的结构和运作模式。将宿主机控制器视为一个透明接口,而不会讨论其实现细节
尽管从数据结构的内容和常数的名称来看,USB子系统的结构和布局都是严格地基于USB标准,但必须考虑到实际开发USB驱动程序时涉及的一些微妙细节。为使接下来阐述的信息尽可能简明,我会将
讨论的范围限制到USB子系统的核心方面
。因此,除了我讲解的数据成员之外,其他无关的成员大多略去了。如果读者已经清楚了该子系统的结构,那么在内核源代码中查找对应的细节是非常简单的USB子系统有4项主要的任务
- 注册和管理现存的设备驱动程序
- 为USB设备查找适当的驱动程序,以及初始化和配置
- 在内核内存中表示设备树
- 与设备通信(交换数据)
与上述列表中各个任务关联的数据结构如下:
usb_driver
是USB设备驱动程序和内核其余部分(特别是USB层)之间协作的起始点//include/linux/usb.h /*usb驱动*/ struct usb_driver { const char *name;//驱动名(通常使用模块的文件名) int (*probe) (struct usb_interface *intf, const struct usb_device_id *id); void (*disconnect) (struct usb_interface *intf); int (*ioctl) (struct usb_interface *intf, unsigned int code, void *buf); const struct usb_device_id *id_table;//驱动程序支持的所有设备列表,用于匹配驱动 struct usbdrv_wrap drvwrap;//区分接口驱动程序和设备驱动程序 }; struct usbdrv_wrap { struct device_driver driver; int for_devices;//0:为接口驱动程序,其他:设备驱动程序 }; //include/linux/mod_devicetable.h /*usb id表*/ struct usb_device_id { /* which fields to match against? */ /* 针对哪些字段进行匹配? */ __u16 match_flags;//指定将该结构的哪些字段与设备数据比较,值为 USB_DEVICE_ID_MATCH_VENDOR 等 /* Used for product specific matches; range is inclusive */ /* 用于特定于产品的匹配,范围包含边界在内 */ __u16 idVendor; __u16 idProduct; __u16 bcdDevice_lo; __u16 bcdDevice_hi; /* Used for device class matches */ /* 用于设备类别的匹配 */ __u8 bDeviceClass; __u8 bDeviceSubClass; __u8 bDeviceProtocol; /* Used for interface class matches */ /* 用于接口类别的匹配 */ __u8 bInterfaceClass; __u8 bInterfaceSubClass; __u8 bInterfaceProtocol; };
函数指针probe和disconnect与id_table共同构成了USB子系统热插拔能力的支柱。在宿主机适配器检测到新设备插入时,即发起一个探测过程,以查找适当的设备驱动程序。
内核接下来遍历设备树的所有结点,确定是否有驱动程序与该设备相关
内核首先扫描驱动程序支持的所有设备列表,即id_table中。在找到设备和表项的匹配之后,则调用特定于驱动程序的probe函数,执行进一步的检查和初始化工作。
不仅在新设备添加到系统时,会建立驱动程序和设备之间的关联。在加载新驱动程序时,也会如此。采用的方法同上文所述。起始点是usb_register
例程,在注册新USB驱动程序时必须调用它。
probe和remove函数处理的是USB接口,由一个独立的数据结构(usb_interface)描述。除了接口特征之外,其中还包括指向相关的设备、驱动程序和该接口所属USB类的指针。 -
设备树的表示
下面的数据结构描述了USB设备树以及内核中各种设备的特征://include/linux/usb.h //内核设备树及各种设备的特征 struct usb_device { int devnum; /* Address on USB bus *///该设备的唯一编号(在整个USB树中全局唯一) char devpath [16]; /* Use in messages: /port/port/... *///指定了该设备在USB树的拓扑结构中的位置。从根结点移动到保存在各个数组项中的设备,必须遍历所有集线器的端口号 enum usb_device_state state; /* configured, not attached, etc *///设备的状态(已连接、已配置,等等) enum usb_device_speed speed; /* high/full/low (or error) *///设备的速度,USB_SPEED_LOW 和USB_SPEED_FULL 用于USB 1.1,而 USB_SPEED_HIGH 用于USB 2.0 unsigned int toggle[2]; /* one bit for each endpoint * ([0] = IN, [1] = OUT) */ struct usb_device *parent; /* our hub, unless we're the root *///该设备所属的集线器的数据结构 struct usb_bus *bus; /* Bus we're part of *///总线对应的数据结构 struct usb_host_endpoint ep0; struct device dev; /* Generic device interface *///建立了与通用设备模型的关联 struct usb_device_descriptor descriptor;/* Descriptor *///将描述USB设备的特征数据群集到一个数据结构中(包括厂商ID、产品ID、设备类别等信息)。 struct usb_host_config *config; /* All of the configs *///所有可能的配置 struct usb_host_config *actconfig;/* the active configuration *///设备的当前配置 u8 portnum; /* Parent port number (origin 1) */ /* static strings from the device */ char *product; /* iProduct string, if present *///ASCII字符串,产品名称,由硬件自身提供 char *manufacturer; /* iManufacturer string, if present *///ASCII字符串,生产商,由硬件自身提供 char *serial; /* iSerialNumber string, if present *///ASCII字符串,设备序列号,由硬件自身提供 #ifdef CONFIG_USB_DEVICEFS struct dentry *usbfs_dentry; /* usbfs dentry entry for the device *///连接到USB文件系统,通常装载在 /proc/bus/usb 中,提供从用户空间访问设备的入口 #endif int maxchild; /* Number of ports if hub *///如果该设备是集线器,这个成员表示集线器的端口数目(即,可以附接的设备数目) struct usb_device *children[USB_MAXCHILDREN];//如果该设备是集线器,这个指针数组成员包含了指向对应 usb_device 实例的指针 };
尽管到目前为止我只提到一个USB设备树,但内核内存中可能有几个这样的树(不共享同一个根结点)。如果计算机有几个USB宿主机控制器,就会发生这种情况。所有总线的根结点,都保存在一个独立的链表中,表头是全局变量
usb_bus_list
,定义在drivers/usb/core/hcd.c
中总线链表中的各个元素,由以下数据结构表示:
//usb总线 struct usb_bus { struct device *controller; /* host/master side hardware *///指向 device实例的指针,对应于实现了该总线的硬件设备 int busnum; /* Bus number (in order of reg) *///在总线注册时按顺序分配 char *bus_name; /* stable id (PCI slot_name etc) *///指向短字符串的指针,其中保存了一个唯一的名称 struct usb_devmap devmap; /* device address allocation map *///位图,长度(最少)为128(一个总线最多可接128个设备)个比特位。它用于跟踪哪些USB编号已经分配,哪些仍然是空闲的 struct usb_device *root_hub; /* Root hub *///指向(虚拟)根集线器的数据结构,表示总线设备树的根结点 struct list_head bus_list; /* list of busses *///链表元素,链表头为usb_bus_list,将所有 usb_bus 实例连接到一个链表中管理 #ifdef CONFIG_USB_DEVICEFS struct dentry *usbfs_dentry; /* usbfs dentry entry for the bus *///建立总线与虚拟文件系统的必要的关联 #endif }; //drivers/usb/core/hcd.c //为与底层的控制器硬件通信,使用了USB请求块(USB request block, URB)。在与USB设备的所有可能形式的传输中,都使用URB交换数据 int usb_hcd_submit_urb (struct urb *urb, gfp_t mem_flags)
我们对URB的准确布局不特别感兴趣,因此就不必仔细讨论相关的struct urb了。该结构中许多地方涉及各种传输类型的细节差异,这意味着,如果没有对USB数据传输的广泛了解,该结构是难于理解的,所以本书没有详细讲解。
但事实上,USB设备驱动程序很少接触到urb实例,而是使用一整套宏和辅助函数,来简化发出请求时对URB的填充,以及返回数据的读取。这些宏和函数涉及USB设备操作的深入知识,在这里就不讨论了。
总结
动态加载设备
linux2.6 通过用户空间的 udevd
守护进程来动态创建设备文件
内核检测新设备接入时,创建kobject对象并加入sysfs文件系统中,还会给该进程发送热插拔消息包,该进程根据消息注册对应的设备驱动,并在 ramfs文件系统的 /dev目录中创建对应的项
字符和块设备
字符设备用全局 cdev_map 管理,块设备用 bdev_map 。这两个结构体相同,使用了255大小的hash表管理,主设备号为 键
字符设备还用全局 chrdevs 管理设备号分配,同样使用了255大小的hash表,主设备号为 键
字符设备的注册先从 chrdevs 中获取可用设备号,再注册到全局字符设备管理hash表 cdev_map 中
块设备直接注册到全局块设备管理hash表 bdev_map 中
块设备定义了单独的块设备inode=>bdev_inode 将块设备结构体和inode结构体关联
块设备结构体 block_device 关联一个硬盘结构体 gendisk 和一个块操作集结构体block_device_operations
一个硬盘结构体关联多个分区结构体 hd_struct 和 一个请求队列 request_queue
块设备的读写操作机制
请求队列的使用:
- submit_bio 创建请求,并将请求加入请求队列,判断是否处理请求队列
- 如果请求为空,则创建并插入请求队列,设置拔出和处理请求队列的超时时间。
- 否则如果不为空则判断请求能否合并
- 如果能合并,将请求与请求队列中的请求合并。
- 否则如果不能合并,创建请求,并加入请求队列
- 如果请求队列中的请求超过阈值则拔出队列,并处理队列
- 如果是同步请求,则直接拔出请求队列,并处理请求队列中的请求
所以有3种情况处理请求队列中的请求(如果是硬盘,就是进行实际的硬盘读写处理)
- 插入请求时,请求队列中的请求超过了阈值
- 请求队列处理的超时时间到了
- 是同步请求
IO端口和IO内存
X86体系中,具有两个地址空间:IO空间
和内存空间
,而RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,即内存空间。IO资源使用resource树管理。
寄存器在 IO空间 就是IO端口
寄存器在 内存空间 就是IO内存
总线机制
总线为 bus_type 其中有两个链表,一个存着设备,一个存着驱动。创建总线后加入总线子系统
设备 device 和驱动 device_driver 都关联同一个总线 bus_type
设备或驱动调用注册函数加入(device_add,加入了总线和sysfs,并匹配驱动)总线后,注册函数会自动调用探测函数在总线的对应链表(注册设备会在驱动链表找,注册驱动会在设备链表找)中,根据名字匹配,如果匹配上了就调用驱动的初始化函数(probe函数)
额外
=========================================
涉及的命令和配置:
mknod 创建设备文件
Documentation/devices.txt
http://www.lanana.org 设备号列表
udev守护进程:监听设备连接消息,调用对应的初始化驱动,在/dev中创建设备文件
全局变量 bdev_map 用于块设备,实现散列表,使用主设备号作为散列键,保存设备对象 gendisk
全局变量 cdev_map 用于字符设备,实现散列表,使用主设备号作为散列键,保存设备对象 cdev
全局变量 chrdevs 用于字符设备,管理设备号分配
全局变量 all_bdevs ,链表头,链表元素为block_device->bd_list,管理了所有块设备
全局函数指针数组 check_part 用于识别特定的分区类型
全局变量 elv_list I/O调度器链表头,里面存着所有I/O调度器结构体(struct elevator_type->list)
全局变量 iomem_resource 为io内存树根结点
全局变量 ioport_resource 为io端口树根结点
全局变量 pci_root_buses PCI总线链表,线性链表,成员为pci_bus->node,列出了系统中所有的"PCI总线"。在“向下”扫描数据结构以查找附接到各个总线的所有设备时,该链表是一个起点
全局变量 pci_devices PCI设备链表,成员为pci_dev->global_list 将系统中的所有"PCI设备"都连接起来,不考虑总线结构的影响。在驱动程序想要搜索它支持的所有设备时,该链表很有用,因为此时无需关注总线拓扑结构
全局变量 usb_bus_list 表头,链表元素为所有usb根结点
一个磁盘对应一个 gendisk,一个 gendisk 有多个分区(part成员为hd_struct数组) ,一个分区为一个 block_device(bd_part成员为hd_struct指针) 和 hd_struct(block_device->bd_part和gendisk->part), block_device->bd_part在gendisk->part数组中
/sys/block/<device>/queue/nr_requests
用于设置每个队列中request结构的最大数目
//查看系统io内存树形结构
cat /proc/iomem
//查看系统io端口树形结构
cat /proc/ioports