Linux Device Driver && Device File
catalog
1. 设备驱动程序简介 2. I/O体系结构 3. 访问设备 4. 与文件系统关联 5. 字符设备操作 6. 块设备操作 7. 资源分配 8. 总线系统
1. 设备驱动程序简介
设备驱动程序是内核的关键领域,在很多时候,判断一个操作系统的性能时,主要是通过现有驱动程序可用的外设数目和驱动程序对外设的支持程序来判断,例如
1. 显卡驱动能够多大程序地发挥显卡硬件本身的性能 2. 网卡驱动能否100%挖掘硬件的处理速度 ..
因此,内核源代码的相当大一部分是在致力于设备驱动程序的实现
设备驱动程序基于中心内核提供的许多不同的机制(这也是有时驱动程序被称之为内核"应用程序"的原因),驱动程序的结构通常非常类似,并且与设备无关。我们接下来将讨论内核如何为设备驱动程序设置并管理数据结构和通用的基础设施
Relevant Link:
《Linux Device Drivers》
《essential Linux Device Drivers》
2. I/O体系结构
与外设的通信通常称之为输入输出(I/O),在实现外设的I/O时,内核必须处理3个可能出现问题的领域
1. 必须根据具体的设备类型和模型,使用各种方法对硬件寻址 2. 内核必须向用户应用程序和系统工具提供访问各种设备的方法,并尽最大可能采用统一的方案,确保程序设计的工作量不会过多,同时保证应用程序能够在不考虑特定硬件方法的情况下进行互操作 3. 用户空间需要知道内核中有哪些设备可用
与外设的通信是层次化的
对各个设备的访问,通过层次化的多个抽象层设计,在层次结构的底部是设备自身,它通过总线系统连接到其他设备和系统CPU,设备和内核的通信由该路径进行。在我们开始讨论Linux内核中相关的算法和结构之前,值得简要看看外部的扩展硬件通常如何工作
0x1: 扩展硬件
硬件设备可能以多种方式连接到系统
1. 主板上的扩展槽 2. 外部连接器是最常用的方法 3. 扩展硬件也可以直接集成到主板上
就内核而言,外设连接到系统其他部分的方式通常没有影响,因为抽象屏蔽了这些硬件细节
1. 总线系统
尽管外设的范围看起来是无限的,从CD刻录机、调制解调器、ISDN板、声卡等,但这些都有一个共同点,它们并不直接连接到CPU,而是通过总线连接起来,而是通过总线连接起来,总线负责设备与CPU之间以及各个设备之间的通信,有很多方法可以实现总线,其他大多数方法Linux都能支持,例如
1. PCI(peripheral component interconnect 外围组件通信) 许多体系结构上使用的主要系统总线,PCI设备插入到系统主板的扩展槽中,该总线也支持热插拔,使得设备可以在系统运行时连入或断开,PCI的传输速度最大能够达到几百兆字节/秒,应用很广泛 2. ISA(industrial standard architecture) 一种比较古老的总线,应用仍然很广泛,ISA与IA-32体系结构(及其前辈)的某些特性绑定非常紧密,但也可以用于其他的处理器 3. SBus 这是一个非常高级的总线,已经出现很多年,由SUN公司设计,是一种非私有的开放总线 4. IEEE1394 对市场而言,这显然不是一个较为通俗的数字,因为某些厂商称之为FiewWire,而另一些则称之为Llink,它有很多非常有趣的计数特性,包括 1) 预先设计的热插拔能力 2) 非常高的传输速率 IEEE1394是高端笔记本上非常流行的一种外部总线,提供了一种高速的扩展选项 5. USB(universal serial bus) 这也是一种广泛应用的外部总线,有很高的市场接受率,该总线的主要特性是 1) 热插拔能力 2) 自动检测新硬件的能力 3) 其最高速度只是中等水平,但足以用于CD刻录机、键盘、鼠标之类的设备,该总线的一种新版本(2.0)的最大传输速率更大,但实际软件没有太多变化(硬件层次上的差别要大得多) 4) USB系统的拓朴结构异乎寻常,其中的设备不是按一条单链排布,而是按树形结构排布,在内核寻址此类设备时,该事实就显而易见了,USB集线器用柞树的结点,在它上面可以进一步连接其他设备(包括其他USB集线器) 5) USB可以为各个设备预留固定的带宽,在实现均匀数据流时,这是一个重要因素 6. SCSI(small computer system interface) 这种总线过去称为专业人员的总线,因为相关外设的成本很高,由于SCSI支持非常高的数据吞吐量,因此它主要用在服务器系统上寻址硬盘,可适用于大多数处理器体系结构 7. 并口和串口(parallel and serial interface) 这些存在于大多数体系结构上,无论整个系统的设计如何,这些总线非常简单而速度极低,用于外部连接,已经非常古老,这些总线用于寻址慢速设备(如打印机、调制解调器和键盘等),此类设备没什么性能要求
无论采用的处理器体系结构如何,系统都不会只有一种总线,而是一些总线的组合,当前的PC设计通常包括两个通过"桥接器"互联的PCI总线,处于兼容性的原因,有时也带有ISA总线(大多数情况下只有一个插槽),一些总线(如USB、FireWire)无法作为主总线,始终需要经由另一个系统总线将数据传递给处理器
2. 与外设的交互
有几种方法可以与连接到系统的硬件通信
1. I/O端口
一种选项是使用IA-32和许多其他体系结构上都有的I/O端口,在这种情况下,内核发送数据给"I/O控制器",数据的目标设备通过唯一的端口号标识,数据被传输到设备进行处理,处理器管理了一个独立的虚拟地址空间,可用于管理所有I/O地址
I/O地址空间通常不关联到普通的系统内存,因为端口也可以映射到内存中,这通常会引起混淆,有几种不同类型的端口
1. 只读 2. 只写 3. 双向操作 这使得处理器与外设之间可以双线交换数据,进而应用态再通过RING3 ~ RING0通信方式进行通信,最终的目的是应用程序可以和外设进行通信
在IA-32体系结构上,端口地址空间由2^16个不同的8位地址组成(0x1 ~ 0xFFFF),对其中的每个端口号而言,或者已经分配了一个设备,或者未使用,多个外设不能同时共享一个端口。考虑到当今的复杂技术,8bit位在与外设交换数据并不算多,因此,可以将两个连续的8位端口合并为一个16位端口,进一步的,两个连续的16位端口(实际上是4个连续的8bit端口)可以认为是一个32位端口。处理器提供了一些适当的汇编语句,可以进行输入输出操作
每个处理器类型实现端口访问的方式都不同,因此,内核必须提供一个适当的抽象层,例如
\linux-2.6.32.63\include\asm-generic\io.h 1. outb: 写一个字节 2. outw: 写一个自字 3. inb: 读取一个字节 ..
2. I/O内存映射
应用程序开发者必须寻址许多设备,与内存的处理方式类似,因此处理器提供了对I/O端口进行内存映射的选项,将特定外设的端口地址映射到普通内存中,可以像处理普通内存那样操作外设,图形卡通常会使用这类操作,因为与使用特定的端口命令相比,处理大量图像数据时使用普通处理器命令要更加容易,诸如PCI之类的系统总线通常也是通过I/O映射进行寻址的
为使用内存映射,首先必须将I/O端口映射到普通的系统内存中(使用特定于处理器的例程),在不同的底层体系结构之上,完成这一任务的方法有很大的不同,内核再次提供了一个抽象层,主要包括
1. ioremap: 映射I/O内存区 2. iounmap: 解除映射
3. 轮询和中断
系统如何知道某个设备的设备已经就绪、可以读取,有两种方法可以判断
1. 轮询(polling) 策略非常简单,只需重复询问设备数据是否可用,如果可用,则处理器取回数据。显然,这样做是非常浪费资源的,为检查外设的状态需要花费系统的大量运行时间,导致影响重要任务的执行 2. 中断 中断是更好的备选方案,每个CPU都提供了中断线(interrupt line),可由各个系统设备共享(多个设备也可以共享一个中断)。每个中断通过一个唯一的号码标识,内核对使用的每个中断提供一个服务例程(这其中有一个复杂的转换和寻址例程) 中断将暂停CPU正常的系统工作,在外设的数据已经就绪,需要由内核或应用程序(间接的)处理时,外设会引发一个中断(通过CPU的中断线)。使用这种方法,系统就不再需要频繁检查是否有新的数据可用,因为外设在有新数据的情况下可以自动通知系统
3. 通过总线控制设备
并非所有设备都是直接通过I/O语句寻址,也可以通过总线系统访问的,具体的实现方式与所用的总线和设备相关。并非所有设备类别都可以连接到所有总线系统,例如
1. 可以将硬盘和CD刻录机连接到SCSI接口 2. 但图形卡不行,图形卡只能查到PCI槽中 3. 相比之下,硬盘必须通过另一种接口,通常是IDE,才能连接到PCI总线
不同的总线类型称作"系统和扩展总线",对内核来说,硬件实现方式的差别并不重要,只有总线和附接外设寻址的方式,才和我们讨论的内容相关。就系统总线而言(对很多处理器类型和体系结构而言,是PCI总线),可使用I/O语句和内存映射与总线自身和附接的设备通信,内核也为驱动程序提供了几个命令,以调用特殊的总线功能
1. 查询可用的设备列表 2. 按统一的格式读取配置信息 3. 按统一的格式设置配置信息
这些命令都是平台无关的,相应的代码在各种平台上使用时无需改变,因而简化了驱动程序的开发
扩展总线如USB、IEEE1394、SCSI等,通过明确定义的总线协议与附接的设备交换数据和命令,内核通过I/O语句和内存映射与总线自身通信,同时提供了平台无关的例程,使总线能够与附接的设备通信
与总线上附接的设备通信,不一定要在内核空间中由设备驱动进行,也可能在用户空间实现,例如SCSI刻录机,通常通过cdrecord工具访问,该工具产生需要的SCSI命令,然后利用内核经SCSI总线将命令发送到对应的设备,并处理设备返回的信息和响应
Relevant Link:
3. 访问设备
设备特殊文件(设备文件)用于访问扩展设备,这些文件并不关联到硬盘或者任何其他存储介质上的数据段,而是建立了与某个设备驱动程序的连接,以支持与扩展设备的通信。就应用程序而言,普通文件和设备文件的处理有一点差别,二者都可以通过同样的库函数处理,但为了处理方便,系统还提供了几个额外的命令用于处理设备文件,这些对普通文件是不可用的
0x1: 设备文件
我们通过富姐到串行接口的调制解调器,来讨论设备文件的处理方法,对应的设备文件名称是/dev/ttyS0,设备并不是通过其文件名标识,而是通过文件的主、从设备号标识,这些号码在文件系统中作为特别的文件属性管理
用于读写普通文件的工具,同样用来向设备文件写入数据或读取处理结果,例如
echo "TEST" > /dev/ttyS0 //向连接到第一个串行接口的调制解调器发送一个初始化字符串
0x2: 字符设备、块设备和其他设备
根据与外设与系统之间交换数据的方式,可以将设备分为几种类别
1. 有些设备非常适合面向字符的数据交换,因为数据传输量很低 1) 串行接口 2) 文本终端 2. 其他的设备则更适合于处理包含固定数目字节的数据块 1) 硬盘 2) 光驱 //内核会区分字符设备和块设备
1. 标识设备文件
字符设备和块设备可以通过对应的文件属性来区分
ll /dev/sd{a,b} /dev/ttyS{0,1} brw-r----- 1 root disk 8, 0 May 16 2014 /dev/sda brw-r----- 1 root disk 8, 16 May 16 2014 /dev/sdb crw-rw---- 1 root tty 4, 64 May 8 14:03 /dev/ttyS0 crw-rw---- 1 root uucp 4, 65 May 16 2014 /dev/ttyS1
在很多方面,上面的输出都与普通文件没有什么区别,特别是在访问权限方面,但其中有两个重要区别
1. 访问权限之前的字母是b/c,分别代表块设备和字符设备 2. 设备文件没有文件长度,而增加了另外的两个值 1) 主设备号 2) 从设备号 //二者共同形成一个唯一的号码,内核可以由此查找对应的设备驱动程序
内核采用主从设备号来标识匹配的驱动程序,采用两个号码的原因,与设备驱动程序的通用结构有关,首先,系统可能包含几个同样类型的设备,由同一个设备驱动程序管理,其次,可以将同类设备合并起来,便于插入到内核的数据结构中进行管理
1. 主设备号用于寻址设备驱动程序自身,上面的例子中,硬盘sda、sdb所在的第一个SATA控制权的主设备号是8 2. 驱动程序管理的各个设备(即第一个、第二个硬盘)则通过不同的从设备号指定,sda对应于0,sdb对应于16 /* 两个从设备号之间之所以会有这么大差距 ll /dev/sda* brw-r----- 1 root disk 8, 0 May 16 2014 /dev/sda brw-r----- 1 root disk 8, 1 May 16 2014 /dev/sda1 brw-r----- 1 root disk 8, 2 May 16 2014 /dev/sda2 brw-r----- 1 root disk 8, 4 May 16 2014 /dev/sda4 brw-r----- 1 root disk 8, 5 May 16 2014 /dev/sda5 brw-r----- 1 root disk 8, 6 May 16 2014 /dev/sda6 brw-r----- 1 root disk 8, 7 May 16 2014 /dev/sda7 我们知道,硬盘的各个分区可以通过设备文件进行寻址(/dev/sda1、/dev/sda2),而/dev/sda则代表了整个硬盘,连续的副设备号用于标识各个分区,这使得驱动程序可以区分不同的分区 这个原则也同样适用于字符设备 */
需要明白的是,块设备和字符设备的主设备号可能是相同的,因此,除非同时指定设备号和设备类型(块设备/字符设备),否则找到的驱动程序可能不是唯一的
设备号的当前列表定义在
\linux-2.6.32.63\include\linux\major.h
#define UNNAMED_MAJOR 0 #define MEM_MAJOR 1 #define RAMDISK_MAJOR 1 #define FLOPPY_MAJOR 2 #define PTY_MASTER_MAJOR 2 #define IDE0_MAJOR 3 #define HD_MAJOR IDE0_MAJOR #define PTY_SLAVE_MAJOR 3 #define TTY_MAJOR 4 #define TTYAUX_MAJOR 5 #define LP_MAJOR 6 #define VCS_MAJOR 7 #define LOOP_MAJOR 7 #define SCSI_DISK0_MAJOR 8 #define SCSI_TAPE_MAJOR 9 ...
2. 动态创建设备文件
/dev中的设备结点一般是在基于磁盘的文件系统中静态创建的,随着支持的设备越来越多,必须安置和管理越来越多的项,大多数项是不必要的,因此几乎所有的发布版都将/dev内容的管理工作切换到udevd,这是一个守护进程,允许从用户态动态创建设备文件
每当内核检测到一个设备时,都会创建一个内核对象kobject,该对象借助于sysfs文件系统导出到用户层,此外,内核还向用户空间发送一个热插拔消息
如果在系统启动期间发现新设备,或在运行期间有新设备接入(如USB存储棒),内核产生的热插拔消息包含了驱动程序为设备分配的主从设备号,udevd守护进程所需要完成的所有工作就是监控这些消息,在注册新设备时,会在/dev中创建对应的项,接下来就可以从用户层访问该设备了
由于引入饿了udev机制,/dev不再放置到基于磁盘的文件系统中,而是使用temfs,这是RAM磁盘文件系统ramfs的一种轻型变体,这意味着设备结点不是持久性的,系统关机/重启后就会消失
0x3: 使用ioctl进行设备寻址
字符设备和块设备通常可以适当地融入到文件系统的结构中,并遵守"一切皆文件"的原则,但是有些任务只使用输入输出命令很难完成,这些涉及检查特定于设备的功能和属性,超出了通用文件框架的限制,典型地例如设置设备的配置选项
一种好的方案是IOCTL,它标识输入输出控制接口,是用于配置和修改特定设备属性的通用接口
ioctl通过一种可用于处理文件的特殊方法实现,该方法只能专用于设备文件,每个设备驱动程序都可以定义一个ioctl例程,使得控制数据的传输可以独立于实际的输入输出通道
从用户和程序设计的角度来看,标准库提供了ioctl函数,可以通过特殊的码值将ioctl命令发送到打开的文件。该函数的实现基于itctl系统调用,由内核中的sys_ioctl处理
/* 1. fd: 文件描述符 2. cmd: ioctl码值,一般定义为比较易读的预处理器常数 3. arg: 传输更多的信息 */ asmlinkage long sys_ioctl(unsigned int fd, unsigned int cmd, unsigned long arg);
1. 网卡及其他设备
字符设备和块设备并不是内核管理的全部设备类别,网卡在内核中具有特殊地位,它无法融入到前述的分类方案中,事实是网卡没有设备文件,相反,用户程序必须使用套接字与网卡通信,套接字是一个抽象层,对所有网卡提供了一个抽象视图,标准库的网络相关函数调用socketcall系统调用(这种多路复用在新的内核中逐步由分解后的子系统调用取代)与内核进行通信交互,进而访问网卡
还有其他的一些没有设备文件的系统设备
1. 这些设备或者通过特别定义的系统调用访问 2. 或者在用户空间无法访问,包括所有的扩展总线,例如USB、SCSI 尽管这些总线可以通过设备驱动程序寻址,但相应的函数只在内核内部可用(因此,USB扩展卡也没有设备文件,无法通过设备文件寻址),所以,需要由底层的设备驱动程序提供函数,导出到用户空间,供应用程序访问
0x4: 主从设备号的表示
为了兼容旧的主从设备表示方式,内核使用了用户空间可见的数据类型u32来表示设备号,主从设备的划分如下
1. 在内核中,比特范围0 ~ 19共20个bit用于从设备号,比特范围20 ~ 31中的12个比特用于主设备号 2. 当需要在外部(用户空间)表示dev_t时,则将比特范围0 ~ 7中的8个比特位用作从设备号的第一部分,接下来的12个比特位(8 ~ 19)用作主设备号,最后12个比特位(20 ~ 31)用作从设备号剩余部分 3. 旧的布局共包括16比特位,在主从设备号之间平均分配,如果主设备号和从设备号都小于255,则新旧表示是兼容的
该划分的优点在于,该数据结构的前16个比特,可以解释为旧的设备号,从兼容性的角度考虑,这是很重要的
内核提供了下列函数/宏,以便从u32表示提取信息,并在u32和dev_t之间进行转换
\linux-2.6.32.63\include\linux\kdev_t.h 1. #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) 从dev_t提取主设备号 2. #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) 从dev_t提取从设备号 3. #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) 根据主从设备号产生一个dev_t类型值
0x5: 注册
内核如果能了解到系统中有哪些字符设备和块设备可用,将是非常方便的,因此内核需要维护一个数据库,此外必须提供一个接口,以便驱动程序编写者能够将新项添加到数据库
1. 数据结构
我们接下来重点讨论用于管理设备的数据结构
1. 设备数据库
尽管块设备和字符设备彼此的行为确实有很大不同,但用于跟踪所有可用设备的数据库是相同的。这也是因为字符设备和块设备都是通过唯一的设备号标识的,但是,数据库会根据块设备/字符设备,来跟踪记录不同的对象
1. 每个字符设备都表示为strcut cdev的一个实例 2. struct genhd用于管理块设备的分区,作用类似于字符设备的cdev,这是合理的,如果块设备没有分区,我们也可以视之为具有单一分区的块设备
内核根据所有的cdev、genhd实例
1. 有两个全局数组(bdev_map用于块设备、cdev_map用于字符设备)用来实现散列表 2. 使用主设备号作为散列键 3.cdev_map、bdev_map都是同一数据结构struct kobj_map的实例 /* struct kobj_map { struct probe { //next将所有散列表元素连接在一个单链表中 struct probe *next; //dev表示设备号,该数据中包含了主设备号、从设备号 dev_t dev; /* range博阿村了从设备号的连续范围,与设备关联的各个从设备号的范围是 [MINORS(dev), MINORS(dev) + range - 1] */ unsigned long range; //owner指向提供设备驱动程序的模块(如果有的话) struct module *owner; //get指向一个函数,可以返回与设备关联的kobject实例,通常该任务很简单,但如果使用了设备映射器,则会变得复杂化 kobj_probe_t *get; int (*lock)(dev_t, void *); /* 块设备和字符串设备的区别在于data 1. 字符设备: 指向struct cdev实例 2. 块设备: 指向struct genhd实例 */ void *data; } *probes[255]; //互斥量lock实现了对散列表访问的串行化 struct mutex *lock; }; */ 4. 散列方法相当简单: major % 255
2. 字符设备范围数据库
字符设备范围数据库只用于字符设备,它用于管理为驱动程序分配的设备号范围
1. 驱动程序可以请求一个动态的设备号 内核需要找到一个空闲的范围 2. 或者指定一个范围,从中获取 内核必须确保指定的范围不与现存的范围重叠
内核再次使用了散列表来跟踪已经分配的设备号范围,并同样使用主设备号作为散列键
\linux-2.6.32.63\fs\char_dev.c
static struct char_device_struct { //next连接同一散列行中的所有散列元素(major_to_index根据主设备号计算散列位置) struct char_device_struct *next; //major指定了主设备号 unsigned int major; //baseminor是包含minorct个从设备号的连续范围中最小的从设备号 unsigned int baseminor; int minorct; //name为该设备提供了一个标识符,通常,该名称会选择类似于该设备对应的设备特殊文件的名称,但没有严格的要求 char name[64]; //cdev指向struct cdev的实例 struct cdev *cdev; /* will die */ } *chrdevs[CHRDEV_MAJOR_HASH_SIZE]; /* index in the above */ static inline int major_to_index(int major) { return major % CHRDEV_MAJOR_HASH_SIZE; }
2. 注册过程
我们接下来讨论如何注册块设备和字符设备
1. 字符设备
在内核中注册字符设备需要两个步骤完成
1. 注册或分配一个设备号范围 1) 如果驱动程序需要使用特定范围内的设备号,则必须调用register_chrdev_region 2) 而alloc_chrdev_region则由内核来选择适当的范围 2. 在获取了设备号范围之后,需要将设备添加到字符设备数据库,以激活设备。这需要使用cdev_init初始化一个struct cdev实例 3. 接下来调用cdev_add,在cdev_add成功返回后,设备进入活动状态
2. 块设备
注册块设备只需要调用add_disk一次,较早的内核版本需要使用register_blkdev注册块设备,其好处在于,块设备将显示在/proc/devices中
Relevant Link:
4. 与文件系统关联
除极少数例外,设备文件都是由标准函数处理(Glibc提供的标准文件操作API),类似于普通文件,设备文件也是通过虚拟文件系统管理,普通文件和设备文件都是通过完全相同的接口访问
0x1: inode中的设备文件成员
虚拟文件系统中的每个文件都关联到一个inode,用于管理文件的属性
关于struct inode数据结构的相关知识,请参阅另一篇文章 http://www.cnblogs.com/LittleHann/p/3865490.html //搜索:0x2: struct inode
我们这里重点关注其中与设备驱动程序有关的成员
struct inode { .. //i_rdev中存储了主从设备号 dev_t i_rdev; .. //为唯一地标识与一个设备文件关联的设备,内核在i_mode中存储了文件类型(面向块、字符) umode_t i_mode; .. /* i_fop是一组函数指针的集合,包括许多文件操作(打开、读取、写入),这些由虚拟文件系统使用来处理块设备 */ const struct file_operations *i_fop; .. //内核会根据inode表示块设备还是字符设备,来使用i_bdev或i_cdev指向更多具体的信息 union { //管道信息 struct pipe_inode_info *i_pipe; //块设备驱动 struct block_device *i_bdev; struct cdev *i_cdev; }; .. }
0x2: 标准文件操作
在打开一个设备文件时,各种文件系统的实现会调用init_special_inode函数,为块设备或字符设备文件创建一个inode
\linux-2.6.32.63\fs\inode.c
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev) { //根据传入的mode参数表明的设备类型,向inode提供不同的文件操作 inode->i_mode = mode; if (S_ISCHR(mode)) { inode->i_fop = &def_chr_fops; inode->i_rdev = rdev; } else if (S_ISBLK(mode)) { inode->i_fop = &def_blk_fops; inode->i_rdev = rdev; } else if (S_ISFIFO(mode)) inode->i_fop = &def_fifo_fops; else if (S_ISSOCK(mode)) inode->i_fop = &bad_sock_fops; else printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for" " inode %s:%lu\n", mode, inode->i_sb->s_id, inode->i_ino); } EXPORT_SYMBOL(init_special_inode);
0x3: 用于字符设备的标准操作
字符设备的情况最初非常含混,因为只有一个文件操作可用
struct file_operations def_chr_fops = { .open = chrdev_open, };
字符设备彼此非常不同,因而内核在开始不能提供多个操作,因为每个设备文件都需要一组独立、自定义的操作,因为chrdev_open函数的主要任务就是向该结构填入适用于已打开设备的函数指针,使得能够在设备文件上执行有意义的操作,并最终能够操作设备自身
0x4: 用于块设备的标准操作
相比字符设备,块设备遵循的方案更加一致,这使得内核刚开始就有很多操作可供选择,这些操作的指针群集到def_blk_fopen的通用结构中,这也再次体现了VFS的设计思想,最大程度地向上层提供一个通用的文件操作视图
\linux-2.6.32.63\fs\block_dev.c
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 = blkdev_aio_write, .mmap = generic_file_mmap, .fsync = block_fsync, .unlocked_ioctl = block_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = compat_blkdev_ioctl, #endif .splice_read = generic_file_splice_read, .splice_write = generic_file_splice_write, };
读写操作由通用的内核例程进行,内核中的缓存自动用于块设备
需要明白的是,尽管file_operations与block_device_operations的结构类似,但它们在Linux内核中所处的层次是不同的
1. file_operations由VFS层用来与用户空间通信,其中的例程会调用block_device_operations中的函数,以实现与块设备的通信 2. block_device_operations必须针对各种块设备分别实现,对设备的属性加以抽象,而在此之上建立的file_operations,使用同样的操作即可处理所有的块设备
Relevant Link:
http://www.win.tue.nl/~aeb/linux/lk/lk-11.html
5. 字符设备操作
字符设备的硬件通常非常简单,而且相关的驱动程序并不难于实现
0x1: 表示字符设备
字符设备由struct cdev表示,同时,内核维护了一个数据库,包括所有活动的cdev实例
\linux-2.6.32.63\include\linux\cdev.h
struct cdev { //kobj是一个嵌入在该结构中的内核对象,它用于该数据结构的一般管理 struct kobject kobj; //owner指向提供驱动程序的模块(如果有的话) struct module *owner; //ops是一组文件操作,实现了与硬件通信的具体操作 const struct file_operations *ops; //list用来实现一个链表,其中包含所有表示该设备的设备特殊文件的inode struct list_head list; //dev指定了设备号 dev_t dev; //count表示与该设备关联的从设备号的数目 unsigned int count; };
0x2: 打开设备文件
/source/fs/char_dev.c中的chrdev_open是用于打开字符设备的通用函数
/* * Called every time a character special file is opened */ static int chrdev_open(struct inode *inode, struct file *filp) { struct cdev *p; struct cdev *new = NULL; int ret = 0; spin_lock(&cdev_lock); p = inode->i_cdev; if (!p) { struct kobject *kobj; int idx; spin_unlock(&cdev_lock); /* 假定表示设备文件的inode此前没哟打开过,根据给出的设备号,kobj_lookup查询字符设备的数据库 并返回与该驱动程序关联的kobject实例,该返回值可用于cdev实例 */ kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx); if (!kobj) return -ENXIO; //获得对应设备的cdev实例 new = container_of(kobj, struct cdev, kobj); spin_lock(&cdev_lock); /* Check i_cdev again in case somebody beat us to it while we dropped the lock. */ p = inode->i_cdev; //接下来设置各种数据结构之间的关联 if (!p) { //inode->i_cdev指向所选择的cdev实例,在下一次打开该inode时,就不必再查询字符设备的数据库,而直接使用缓存的值 inode->i_cdev = p = new; //该inode将添加到cdev->list(inode中的i_devices用作链表元素) list_add(&inode->i_devices, &p->list); new = NULL; } else if (!cdev_get(p)) ret = -ENXIO; } else if (!cdev_get(p)) ret = -ENXIO; spin_unlock(&cdev_lock); cdev_put(new); if (ret) return ret; ret = -ENXIO; /* file_f_ops是用于struct file的file_operations,设置为指向struct cdev给出的file_operations实例 即VFS层对设备文件自身的文件操作函数进行进一步的封装 */ filp->f_op = fops_get(p->ops); if (!filp->f_op) goto out_cdev_put; /* 调用struct file新的file_operations中的open方法(现在是特定于设备的),在设备上执行所需的初始化任务 有些外设在第一次使用时,需要通过握手来协商操作的细节,该函数也可以对数据结构作一些修改,以适应特定的从设备号 */ if (filp->f_op->open) { ret = filp->f_op->open(inode,filp); if (ret) goto out_cdev_put; } return 0; out_cdev_put: cdev_put(p); return ret; }
我们考虑一个字符设备的例子,其主设备号为1,根据LANANA标准,该设备有10个不同的从设备号,每个都提供了一个不同的功能,这些都与内存访问操作有关
1. 从设备号1: /dev/mem: 物理内存 2. 从设备号2: /dev/kmem: 内核虚拟地址空间 3. 从设备号3: /dev/null: 比特位捅 4. 从设备号4: /dev/port: 访问I/O端口 5. 从设备号5: /dev/zero: NULL字符源 6. 从设备号8: /dev/random: 非确定性随机数发生器
根据设备描述我们可以很清楚地看到,尽管这些从设备都涉及内存访问,但所实现的功能有很大差别,我们知道,在chrdevs项的结构中只定义了一个函数指针,在打开上述某个文件之后,open指向memory_open
\linux-2.6.32.63\drivers\char\mem.c
static int memory_open(struct inode *inode, struct file *filp) { int minor; const struct memdev *dev; int ret = -ENXIO; lock_kernel(); minor = iminor(inode); if (minor >= ARRAY_SIZE(devlist)) goto out; //根据从设备号选择设备 dev = &devlist[minor]; if (!dev->fops) goto out; //使用VFS将设备的具体操作函数封装起来 filp->f_op = dev->fops; if (dev->dev_info) filp->f_mapping->backing_dev_info = dev->dev_info; //打开设备 if (dev->fops->open) ret = dev->fops->open(inode, filp); else ret = 0; out: unlock_kernel(); return ret; }
函数实现了一个分配器(根据从设备号来区分各个设备,并且选择适当的file_operations)
所涉及的函数逐渐反应了设备了具体特性
1. 最初只知道用于打开字符设备的一般函数 2. 然后由打开与内存相关设备文件的具体函数所替代 3. 接下来根据选择的从设备号,进一步细化函数指针,为不同从设备号最终选定的函数指针未必相同,例如null_fops、random_fops
其他设备类型也采用了同样的方法,首先根据主设备号设置一个特定的文件操作集,其中包含的操作,接下来可以由根据从设备号选择的其他操作替代
0x3: 读写操作
读写字符设备文件的实际工作不是一项特别容易的任务,因为虚拟文件和设备驱动程序代码之间已经建立了关联,调用标准库的读写操作,将向内核发出一系列系统调用,最终调用file_operations结构中相关的操作(主要是read、write),这些方法的具体实现依设备而不同,不能一般化
上述说的内存设备不必费力与实际的外设交互,它们只需要调用其他的内核函数来完成,例如,/dev/null设备使用read_null、write_null函数实现比特位捅的读写操作
\linux-2.6.32.63\drivers\char\mem.c
static ssize_t read_null(struct file * file, char __user * buf, size_t count, loff_t *ppos) { return 0; } //从控设备读取时,什么也不返回,返回的结果是一个长度为0字节的数据流 static ssize_t write_null(struct file * file, const char __user * buf, size_t count, loff_t *ppos) { return count; } //向空设备写入的数据直接被忽略,但无论任何长度的数据,都会报告写入成功
更复杂的字符设备,需要提供读写真正有意义结果的函数,但一般机制是不变的
6. 块设备操作
在内核通过VFS接口支持的外设中,块设备的总数是相对较多的,但是块设备驱动程序面对的情况比字符设备负责得多,导致这种情况的环境因素很多,主要包括
1. 块设备层的设计导致需要持续地调整块设备的速度 2. 块设备的工作方式 3. 块设备层开发方面的历史原因
块设备与字符设备在3个方面有根本的不同
1. 块设备可以在数据中的任何位置进行访问,对字符设备来说,这是可能的,但不是必然的 2. 块设备总是以固定长度的块进行传输,即使只请求一个字节的数据,设备驱动程序也会从设备取出一个完整块的数据,相比之下,字符设备只能够返回单个字节 3. 对块设备的访问有大规模的缓存,即已经读取的数据保存在内存中,如果再次需要,则直接从内存获取,写入操作也使用了缓存,以便延迟处理,这对字符设备没有意义(如键盘),因为字符设备的每次读请求都必须真正与设备交互才能完成,而且往往还要求是准实时完成的
在理解块设备通信之前,我们需要先理清一个概念,即块(block)和扇区(sector)
1. 块(block) 1) 块是一个特定长度的字节序列,用于保存在内核和设备之间传输的数据,块的长度可通过软件方式修改 2) 块是连续扇区的序列 3) 块的长度总是扇区长度的整数倍 2. 扇区(sector) 1) 扇区是一个固定长度的硬件单位,指定了某个设备最少能够传输的数据量 2) 扇区是特定于硬件的常数,它也用来指定设备上某个数据块的位置 3) 内核将每个块设备都视为一个线性表,由按整数编号的扇区或块组成
当前几乎所有常见块设备的扇区长度都是512字节,而块长度则有512、1024、2048、4096字节等,需要明白的是,块的最大长度,会受到特定体系结构的内存页长度的限制,IA-32系统支持的块长度为4096字节,因为其内存页长度是4096字节,而IA-64和Alpha系统能够处理8192字节的块
块长度的选择相对自由,这对许多块设备应用程序有好处,文件系统会将硬盘划分为不同长度的块,以便在处理许多小文件或少数打文件时分别优化性能(整块读写),因为文件系统能够将传输的块长度与自身块长度匹配,所以实现起来要容易的多
块设备层不仅负责寻址块设备,也负责执行其他任务,以提高系统中所有块设备的性能,包括
1. 块设备预读算法的实现 2. 在内核判断应用程序烧好将需要使用某数据时,会使用预读算法从块设备预读数据到内存 3. 如果预读的数据不是立即需要,那么块设备层必须提供缓冲/缓存区来保存这些数据,这种缓冲区不仅用于保存预读数据,也用于临时保存经常用到的块设备数据
0x1: 块设备的表示
块设备有一组属性,由内核管理,内核使用"请求队列管理(request queue management)",使得此类设备的通信尽可能高效,它能够缓存并重排读写数据块的请求,请求的结果也同样保存在缓存中,使得可以用非常高效的方式读取/重新读取数据,在进程重复访问文件(通过硬盘外设块设备)的同一部分时,或不同进程并行访问同一数据时,该特性尤其有用
完成这些任务需要很多数据结构
裸块设备由struct block_device表示,内核将与块设备(block_device)实例紧邻的inode包裹在一个数据结构中
\linux-2.6.32.63\fs\block_dev.c
struct bdev_inode { struct block_device bdev; /* 所有表示块设备的inode都保存在伪文件系统bdev中,这些对用户层不可见,这使得可以使用标准的VFS函数来处理块设备的inode集合 特别的,辅助函数bdget就利用了这一点,给定由dev_t表示的设备号,该函数查找伪文件系统,看对应的inode是否已经存在 1. 如果存在,则返回指向inode的指针 由于struct bdev_inode的存在,利用返回的inode指针,立即就可以找到该设备的block_device实例,如果此前设备没有打开过,致使inode尚未存在,bdget和伪文件系统会确保自动分配一个新的bdev_inode并进行适当的设置 */ struct inode vfs_inode; };
与字符设备层相比,块设备层提供了丰富的队列功能,每个设备都关联了请求队列,这种队列也是块设备层最复杂的部分
每个块设备驱动程序都维护自己的请求队列;每个物理块设备都应该有一个请求队列,以提高磁盘性能的方式对请求进行排序。因此策略程序就可以顺序扫描这种队列,并以最少地移动磁头而为所有的请求提供服务
/source/include/linux/blkdev.h
struct request { struct list_head queuelist; struct call_single_data csd; int cpu; //IO请求request所形成的队列,在linux内核中struct request_queue描述,保存对设备的读写请求 struct request_queue *q; unsigned int cmd_flags; enum rq_cmd_type_bits cmd_type; unsigned long atomic_flags; /* the following two fields are internal, NEVER access directly 特征数据,如扇区、块长度、设备容量 */ sector_t __sector; /* sector cursor */ unsigned int __data_len; /* total data len */ struct bio *bio; struct bio *biotail; struct hlist_node hash; /* merge hash */ /* * The rb_node is only used inside the io scheduler, requests * are pruned when moved to the dispatch queue. So let the * completion_data share space with the rb_node. */ union { struct rb_node rb_node; /* sort/lookup */ void *completion_data; }; /* * two pointers are available for the IO schedulers, if they need * more they have to dynamically allocate it. 函数指针,指向I/O调度器实现,用来重排请求的函数 */ void *elevator_private; void *elevator_private2; struct gendisk *rq_disk; unsigned long start_time; /* Number of scatter-gather DMA addr+len pairs after * physical address coalescing is performed. */ unsigned short nr_phys_segments; unsigned short ioprio; void *special; /* opaque pointer available for LLD use */ char *buffer; /* kaddr of the current segment if available */ int tag; int errors; int ref_count; /* * when request is used as a packet command carrier */ unsigned short cmd_len; unsigned char __cmd[BLK_MAX_CDB]; unsigned char *cmd; unsigned int extra_len; /* length of alignment and padding */ unsigned int sense_len; unsigned int resid_len; /* residual count */ void *sense; unsigned long deadline; struct list_head timeout_list; unsigned int timeout; int retries; /* * completion callback. */ rq_end_io_fn *end_io; void *end_io_data; /* for bidi */ struct request *next_rq; };
每个块设备都必须提供一个探测函数,该函数通过register_blkdev_range直接注册到内核,或者通过gendisk对象,使用add_disk间接地注册到内核,该函数由文件系统代码调用(初始化阶段),以找到匹配的gendisk对象
对块设备的读写请求不会立即执行对应的操作,相反,这些请求会汇总起来,经过协同之后传输到设备
1. 假设进程对一个设备文件发出read( )或write( )系统调用。VFS执行对应文件对象的read或write方法,由此就调用高级块设备处理程序中的一个过程 2. 这个过程执行的所有操作都与对这个硬件设备的具体读写请求有关。内核提供两个名为generic_file_read ( )和generic_file_write ( )通用函数来处理所有事件的发生。因此,在大部分情况下,高级硬件设备驱动程序不必做什么,而设备文件的read和write方法分别指向generic_file_read ( )和generic_file_write ( )方法 3. 但是,有些块设备的处理程序需要自己专用的高级设备驱动程序。典型的例子是软驱的设备驱动程序:它必须检查从上次访问磁盘以来,用户有没有改变驱动器中的磁盘;如果已插入一张新磁盘,那么设备驱动程序必须使缓冲区中所包含的旧数据无效,即使高级设备驱动程序有自己的read和write方法,但是这两个方法通常最终还会调用generic_file_read ( )和generic_file_write ( )函数 4. 这些上层的VFS函数把对I/O设备文件的访问请求转换成对相应硬件设备的块请求 1) 所请求的块可能已在主存,因此generic_file_read ( )和generic_file_write ( )函数调用getblk( )函数来检查缓冲区中是否已经预取了块,还是从上次访问以来缓冲区一直都没有改变 2) 如果块不在缓冲区中,getblk( )就必须调用ll_rw_block( )继续从磁盘中读取这个块,后面这个函数激活操纵设备控制器的低级驱动程序,以执行对块设备所请求的操作 5. 在VFS直接访问某一块设备上的特定块时,也会触发缓冲区I/O操作。例如,如果内核必须从磁盘文件系统中读取一个索引节点,那么它必须从相应磁盘分区的块中传送数据 。对于特定块的直接访问是由bread( )和breada( )函数来执行的,这两个函数又会调用前面提到过的getblk( )和ll_rw_block( )函数。 6. 由于块设备速度很慢,因此缓冲区I/O数据传送通常都是异步处理的:低级设备驱动程序对DMAC和磁盘控制器进行编程来控制其操作,然后结束。当数据传送完成时,就会产生一个中断,从而第二次激活这个低级设备驱动程序来清除(将数据取走)这次I/O操作所涉及的数据结构
值得注意的是,块设备的file_operations中只使用了通用函数,这是块设备的一个特征
在字符设备的情形中,这些函数都是特定于驱动程序的,所有特定于硬件的细节都在请求执行时处理
Relevant Link:
http://oss.org.cn/kernel-book/ch11/11.3.2.htm
0x2: 数据结构
1. 块设备
块设备的核心属性由struct block_device表示
\linux-2.6.32.63\include\linux\fs.h
struct block_device { /* not a kdev_t - it's a search key 块设备的设备号保存在bd_dev中 */ dev_t bd_dev; /* will die bd_inode指向bdev伪文件系统中表示该块设备的inode(本质上该信息也可以使用bdget获取,因而是冗余的) */ struct inode * bd_inode; struct super_block * bd_super; //bd_openers统一用do_open打开该块设备的次数 int bd_openers; /* open/close mutex open/close mutex 打开与关闭的互斥量 */ struct mutex bd_mutex; /* bd_inodes是一个链表的表头,该链表包含了表示该块设备的设备特殊文件的所有普通inode 注意: 不能将表示普通文件的inode和bdev伪文件系统的inode混淆,后者表示块设备自身 */ struct list_head bd_inodes; void * bd_holder; int bd_holders; #ifdef CONFIG_SYSFS struct list_head bd_holder_list; #endif struct block_device * bd_contains; /*分区块大小*/ unsigned bd_block_size; //bd_part指向一个专用的数据结构(struct hd_struct),表示包含在该块设备上的分区 struct hd_struct * bd_part; /* number of times partitions within this device have been opened. bd_part_count是一个使用计数,计算了内核中引用该设备内分区的次数,在用rescan_partitions重新扫描分区时,这个计数很有用处 如果bd_part_count大于0,则禁止重新扫描,因为旧的分区仍然在使用中 */ unsigned bd_part_count; //如果bd_invalidated被设置为1,表示该分区在内核中的信息无效,因为磁盘上的分区已经改变,下一次打开该设备时,将要重新扫描分区表 int bd_invalidated; //bd_disk提供了另一个抽象层,也用来划分硬盘 struct gendisk * bd_disk; /* bd_list是一个链表元素,用于跟踪记录系统中所有可用的block_device实例,该链表的表头为全局变量all_bdevs 使用该链表,无需查询块设备数据库(伪文件系统数据库),即可遍历所有块设备 */ struct list_head bd_list; /* * Private data. You must have bd_claim'ed the block_device * to use this. NOTE: bd_claim allows an owner to claim * the same device multiple times, the owner must take special * care to not mess up bd_private for that case. bd_private可用于在block_device实例中存储特定于持有者的数据 "特定于持有者"意味着只有该block_device实例当前的持有者可以使用bd_private,要成为持有者,必须对块设备成功调用bd_claim, bd_claim在bd_holder是NULL指针时才会成功,即尚未注册持有者(当前调用者是第一个),在这种情况下,bd_holder指向当前持有者, 可以是内核空间中任意一个地址,调用bd_claim,实际上是向内核的其他部分表明,该块设备已经与之无关了(已经专属于持有者了) */ unsigned long bd_private; /* The counter of freeze processes */ int bd_fsfreeze_count; /* Mutex for freeze */ struct mutex bd_fsfreeze_mutex; };
关于内核的哪个部分允许持有块设备,没有固定的规则,例如
1. 在Ext3文件系统中,会持有已装载文件系统的外部日志的块设备,并将超级块注册为持有者 2. 如果某个分区用作交换区,那么在用swapon系统调用激活该分区之后,页交换代码将持有该页交换分区 3. 在使用blkdev_open打开块设备并请求独占使用时,与该设备文件关联的file实例会持有该块设备 //使用bd_release释放块设备
内核用结构block_device实例代表一个块设备对象,如:整个硬盘或特定分区
1. 如果该结构(block_device)代表一个分区,则其成员bd_part指向设备的分区结构 2. 如果该结构代表设备,则其成员bd_disk指向设备的通用硬盘结构gendisk
2. 通用硬盘和分区
尽管struct block_device对设备驱动程序层表示一个块设备,而从另一个角度来说,而另一个抽象则强调与通用的内核数据结构的关联,即硬盘的概念(可能包含子分区)。设备上分区的信息不依赖于表示该分区的block_device实例,实际上,将一个磁盘添加到系统中时,内核将读取并分析底层块设备上的信息,但并不会对各个分区创建block_device实例。为此,内核使用以下数据结构,对已经分区的硬盘提供一种表示
\linux-2.6.32.63\include\linux\genhd.h
struct gendisk { /* major, first_minor and minors are input parameters only, * don't use directly. Use disk_devt() and disk_max_parts(). */ //major指定驱动程序的主设备号 int major; /* major number of driver */ //first_minor、minors表明从设备号的可能范围(每个分区都会分配自身的从设备号) int first_minor; int minors; /* maximum number of minors, =1 for * disks that can't be partitioned. */ //disk_name给出了磁盘的名称,它用于在/proc/partitions中表示该磁盘 char disk_name[DISK_NAME_LEN]; /* name of major driver */ char *(*devnode)(struct gendisk *gd, mode_t *mode); /* Array of pointers to partitions indexed by partno. * Protected with matching bdev lock but stat and other * non-critical accesses use RCU. Always access through * helpers. */ struct disk_part_tbl *part_tbl; //part0表示hd_struct的一个实例,每个磁盘分区对应于一个hd_struct struct hd_struct part0; //fops是一个指针,指向特定于设备、执行各种底层任务的各个函数 const struct block_device_operations *fops; //queue用于管理请求队列 struct request_queue *queue; //private_data是一个指针,指向私有的驱动程序数据,不会由块设备层的通用函数修改 void *private_data; int flags; //driverfs_dev标识该磁盘所属的硬件设备,指针指向驱动程序模型的一个对象 struct device *driverfs_dev; // FIXME: remove struct kobject *slave_dir; struct timer_rand_state *random; atomic_t sync_io; /* RAID */ struct work_struct async_notify; #ifdef CONFIG_BLK_DEV_INTEGRITY struct blk_integrity *integrity; #endif int node_id; };
对每个分区来说,都有一个hd_struct实例,用于描述该分区在设备内的键
struct hd_struct { //start_sect、nr_sects定义了该分区在块设备上的起始扇区和长度,因而唯一地描述了该分区 sector_t start_sect; sector_t nr_sects; sector_t alignment_offset; struct device __dev; struct kobject *holder_dir; int policy, partno; #ifdef CONFIG_FAIL_MAKE_REQUEST int make_it_fail; #endif unsigned long stamp; int in_flight[2]; #ifdef CONFIG_SMP struct disk_stats *dkstats; #else struct disk_stats dkstats; #endif struct rcu_head rcu_head; };
需要明白的是,尽管gendisk表示已分区的磁盘,它们也可以表示没有任何分区的设备。struct gendisk的实例不能由驱动程序分别分配,相反,必须使用辅助函数alloc_disk
/* 给出设备的从设备号数目,调用该函数可以自动分配genhd实例,其中包括了指向各个分区的hd_struct的指针所需的空间 其中只包括指针自身所需的内存,分区实例只有当在设备上检测到实际分区并用add_partition添加时才会分配 */ extern struct gendisk *alloc_disk(int minors);
alloc_disk将新的磁盘集成到设备模型的数据结构中,因此,gendisk不能在销毁时简单地释放,而要使用del_gendisk
3. 各个部分的联系
我们之前讨论的各个数据结构(struct block_device、struct gendisk、struct hd_struct)是彼此直接关联的
Relevant Link:
http://blog.csdn.net/jianchi88/article/details/7212599
4. 块设备操作
特定于块设备的操作汇总如下
\linux-2.6.32.63\include\linux\blkdev.h
struct block_device_operations { //和file_operations中等价函数的语义相同,用于打开文件 int (*open) (struct block_device *, fmode_t); //和file_operations中等价函数的语义相同,用于关闭文件 int (*release) (struct gendisk *, fmode_t); int (*locked_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long); //和file_operations中等价函数的语义相同,用于向块设备发送特殊命令 int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long); int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long); int (*direct_access) (struct block_device *, sector_t, void **, unsigned long *); //media_changed检查存储介质是否已经改变,对于软盘和ZIP软驱等设备,这是有可能的(硬盘通常不支持该函数,因为它们通常不能换盘片) int (*media_changed) (struct gendisk *); unsigned long long (*set_capacity) (struct gendisk *, unsigned long long); //revalidate_disk用于使设备重新生效,只有在直接移除旧的介质并替换为新的介质时(未进行卸载和加载)才有必要使用该函数 int (*revalidate_disk) (struct gendisk *); int (*getgeo)(struct block_device *, struct hd_geometry *); //如果驱动程序实现为模块,那么owner字段指向内存中的一个模块结构,否则该成员为NULL指针 struct module *owner; };
这些函数不是由VFS代码直接调用,而是由块设备的标准文件操作def_blk_fops中包含的操作间接地调用
5. 请求队列
块设备的读写请求放置在一个队列上,称之为请求队列
struct gendisk { ... struct request_queue *queue; ... } struct request_queue { /* * Together with queue_head for cacheline sharing queue_head是该结构的主要成员,是一个表头,用于构建一个I/O请求的双链表 链表的每个元素的数据类型都是request,代表向块设备读写数据的一个请求 内核会"重排"该链表以取得更好的I/O性能 */ struct list_head queue_head; struct request *last_merge; //因为有各种方法可以重排I/O请求,elevator成员以函数指针的形式将所需的函数群集起来 struct elevator_queue *elevator; /* * the queue request freelist, one for reads and one for writes rq用作request实例的缓存,队列中保存空闲请求的列表 除了缓存之外,它还提供了两个计数器,用于记录可用的空闲输入和输出请求的数目 */ struct request_list rq; //下列包含一系列函数指针,表示了请求处理所涉及的只要领域,函数的参数设置和返回类型都通过typedef定义(struct bio管理传输的数据),内核提供了这些函数的标准实现,可以用于大多数设备驱动程序 /* 每个驱动程序都必须实现自身的request_fn函数,该函数是请求队列管理与各个设备的底层功能之间的主要关联,在内核处理当前队列以执行待决的读写请求时,会调用该函数 request_fn是用于向队列添加新请求的标准接口,在内核期望驱动程序执行某些工作时(如从底层读取数据,或向设备写入数据),内核会自动调用该函数 该函数也称之为"策略历程(strategy routine)" */ request_fn_proc *request_fn; /* make_request_fn创建新请求,内核对该函数的标准实现向请求链表添加请求,如果链表中有足够多的请求,则会调用特定于驱动程序的request_fn,以处理这些请求(进行重排) 内核允许设备驱动程序定义自身的make_request_fn函数,因为某些设备(例如RAM磁盘)不使用队列,这可能是由于按任意顺序访问数据都不会影响性能, 也可能是由于驱动程序比内核更了解如何处理请求,因为使用内核的标准方法不会带来好处(例如卷管理器) */ make_request_fn *make_request_fn; /* prep_rq_fn是一个请求预备函数,大多数驱动程序不使用该函数,会将对应的指针设置为NULL,如果实现了该函数,它会产生所需的硬件指令, 用于在发送实际的请求之前预备一个请求 */ prep_rq_fn *prep_rq_fn; //unplug_fn用于拔出一个块设备时调用,插入的设备不会执行请求,而是将请求收集起来,在拔出时执行,有效使用该方法,能够提高块设备层的性能 unplug_fn *unplug_fn; //merge_bvec_fn确定是否允许向一个现存的请求增加更多数据 merge_bvec_fn *merge_bvec_fn; //在预备刷出队列时,即一次性执行所有待决请求之前,会调用prepare_flush_fn,在该方法中,设备可以进行必要的清理 prepare_flush_fn *prepare_flush_fn; /* 对于大的请求来说,完成请求,即完成所有I/O,可能是一个耗时的过程,在内核2.6.16中加入了软中断SoftIRQ异步完成请求的特性 可以通过blk_complete_request要求异步完成请求,softirq_done_fn在这种情况下用作回调函数,通过驱动程序请求已经完成 */ softirq_done_fn *softirq_done_fn; rq_timed_out_fn *rq_timed_out_fn; dma_drain_needed_fn *dma_drain_needed; lld_busy_fn *lld_busy_fn; /* * Dispatch queue sorting */ sector_t end_sector; struct request *boundary_rq; /* * Auto-unplugging state */ struct timer_list unplug_timer; int unplug_thresh; /* After this many requests */ unsigned long unplug_delay; /* After this many jiffies */ struct work_struct unplug_work; struct backing_dev_info backing_dev_info; /* * The queue owner gets to use this for whatever they like. * ll_rw_blk doesn't touch it. */ void *queuedata; /* * queue needs bounce pages for pages above this limit */ gfp_t bounce_gfp; /* * various queue flags, see QUEUE_* below queue_flags借助标志来控制队列的内核状态 */ unsigned long queue_flags; /* * protects queue structures from reentrancy. ->__queue_lock should * _never_ be used directly, it is queue private. always use * ->queue_lock. */ spinlock_t __queue_lock; spinlock_t *queue_lock; /* * queue kobject */ struct kobject kobj; /* * queue settings nr_requests表明了可以管理到队列的请求的最大数目 */ unsigned long nr_requests; /* Max # of requests */ unsigned int nr_congestion_on; unsigned int nr_congestion_off; unsigned int nr_batching; void *dma_drain_buffer; unsigned int dma_drain_size; unsigned int dma_pad_mask; unsigned int dma_alignment; struct blk_queue_tag *queue_tags; struct list_head tag_busy_list; unsigned int nr_sorted; unsigned int in_flight[2]; unsigned int rq_timeout; struct timer_list timeout; struct list_head timeout_list; struct queue_limits limits; /* * sg stuff */ unsigned int sg_timeout; unsigned int sg_reserved_size; int node; #ifdef CONFIG_BLK_DEV_IO_TRACE struct blk_trace *blk_trace; #endif /* * reserved for flush operations */ unsigned int ordered, next_ordered, ordseq; int orderr, ordcolor; struct request pre_flush_rq, bar_rq, post_flush_rq; struct request *orig_bar_rq; struct mutex sysfs_lock; #if defined(CONFIG_BLK_DEV_BSG) struct bsg_class_device bsg_dev; #endif };
内核提供了标准函数blk_init_queue_node,用于产生一个标准的请求队列,在这种情况下,驱动程序自身唯一必须提供的管理函数就是request_fn,任何其他的管理问题都是通过标准函数处理,用这种方法实现请求管理的驱动程序,在调用add_disk激活磁盘之前,需要调用blk_init_queue_node创建请求队列,并将结果request_queue实例添加到设备的gendisk实例
请求队列可能在系统超负荷时插入,接下来新的请求都会处于未处理状态,直至队列"拔出",该特性称之为"队列插入(queue plugging)",以"unplug_"为前缀的各个成员用于实现一种定时器机制,在一定时间间隔后自动"拔出"队列,unplug_fn负责实际的拔出操作
0x3: 向系统添加磁盘和分区
在学习了构成块设备层的数据结构之后,我们接下来讨论一下向系统添加通用硬盘的方式
1. 添加分区
add_partition负责向通用硬盘数据结构添加一个新的分区
\linux-2.6.32.63\fs\partitions\check.c
struct hd_struct *add_partition(struct gendisk *disk, int partno, sector_t start, sector_t len, int flags) { //分配一个新的struct hd_struct struct hd_struct *p; dev_t devt = MKDEV(0, 0); struct device *ddev = disk_to_dev(disk); struct device *pdev; struct disk_part_tbl *ptbl; const char *dname; int err; err = disk_expand_part_tbl(disk, partno); if (err) return ERR_PTR(err); ptbl = disk->part_tbl; if (ptbl->part[partno]) return ERR_PTR(-EBUSY); p = kzalloc(sizeof(*p), GFP_KERNEL); if (!p) return ERR_PTR(-EBUSY); if (!init_part_stats(p)) { err = -ENOMEM; goto out_free; } pdev = part_to_dev(p); p->start_sect = start; p->alignment_offset = queue_sector_alignment_offset(disk->queue, start); p->nr_sects = len; p->partno = partno; p->policy = get_disk_ro(disk); /* 在指定了一个用于显示的名字(例如,在sysfs中)后,将分区的内核对象的父对象设置为通用硬盘对象 与完整的磁盘相比,ktype不是ktype_block,而设置为ktype_part,这使得可以区分源自磁盘和源自分区的uevent */ dname = dev_name(ddev); if (isdigit(dname[strlen(dname) - 1])) dev_set_name(pdev, "%sp%d", dname, partno); else dev_set_name(pdev, "%s%d", dname, partno); device_initialize(pdev); pdev->class = &block_class; pdev->type = &part_type; pdev->parent = ddev; err = blk_alloc_devt(p, &devt); if (err) goto out_free_stats; pdev->devt = devt; /* delay uevent until 'holders' subdir is created */ dev_set_uevent_suppress(pdev, 1); //添加新对象,使新分区成为块设备子系统的一个成员,因此提供有关该分区信息的sysfs项会出现在/sys/block中 err = device_add(pdev); if (err) goto out_put; err = -ENOMEM; p->holder_dir = kobject_create_and_add("holders", &pdev->kobj); if (!p->holder_dir) goto out_del; dev_set_uevent_suppress(pdev, 0); if (flags & ADDPART_FLAG_WHOLEDISK) { err = device_create_file(pdev, &dev_attr_whole_disk); if (err) goto out_del; } /* everything is up and running, commence */ INIT_RCU_HEAD(&p->rcu_head); //最后,必须修改通用硬盘对象,使对应的part数组项指向新的分区 rcu_assign_pointer(ptbl->part[partno], p); /* suppress uevent if the disk supresses it */ if (!dev_get_uevent_suppress(ddev)) kobject_uevent(&pdev->kobj, KOBJ_ADD); return p; out_free_stats: free_part_stats(p); out_free: kfree(p); return ERR_PTR(err); out_del: kobject_put(p->holder_dir); device_del(pdev); out_put: put_device(pdev); blk_free_devt(devt); return ERR_PTR(err); }
2. 添加磁盘
下面给出add_disk的代码流程图
\linux-2.6.32.63\block\genhd.c
void add_disk(struct gendisk *disk) { struct backing_dev_info *bdi; dev_t devt; int retval; /* minors == 0 indicates to use ext devt from part0 and should * be accompanied with EXT_DEVT flag. Make sure all * parameters make sense. */ WARN_ON(disk->minors && !(disk->major || disk->first_minor)); WARN_ON(!disk->minors && !(disk->flags & GENHD_FL_EXT_DEVT)); disk->flags |= GENHD_FL_UP; retval = blk_alloc_devt(&disk->part0, &devt); if (retval) { WARN_ON(1); return; } disk_to_dev(disk)->devt = devt; /* ->major and ->first_minor aren't supposed to be * dereferenced from here on, but set them just in case. */ disk->major = MAJOR(devt); disk->first_minor = MINOR(devt); //首先,调用blk_register_region,确认所要求的设备号范围尚未分配 blk_register_region(disk_devt(disk), disk->minors, NULL, exact_match, exact_lock, disk); //在给内核对象提供了一个名字之后,调用bdget_disk获取了该设备的一个新的block_device实例 register_disk(disk); blk_register_queue(disk); bdi = &disk->queue->backing_dev_info; bdi_register_dev(bdi, disk_devt(disk)); retval = sysfs_create_link(&disk_to_dev(disk)->kobj, &bdi->dev->kobj, "bdi"); WARN_ON(retval); }
0x4: 打开块设备文件
在用户应用程序打开一个块设备的设备文件时,虚拟文件系统将调用file_operations结构的open函数,最终会调用了blkdev_open
\linux-2.6.32.63\fs\block_dev.c
static int blkdev_open(struct inode * inode, struct file * filp) { struct block_device *bdev; int res; /* * Preserve backwards compatibility and allow large file access * even if userspace doesn't ask for it explicitly. Some mkfs * binary needs it. We might want to drop this workaround * during an unstable branch. */ filp->f_flags |= O_LARGEFILE; if (filp->f_flags & O_NDELAY) filp->f_mode |= FMODE_NDELAY; if (filp->f_flags & O_EXCL) filp->f_mode |= FMODE_EXCL; if ((filp->f_flags & O_ACCMODE) == 3) filp->f_mode |= FMODE_WRITE_IOCTL; //bd_acquire首先找到与该设备匹配的block_device实例,如果设备已经使用过,指向该实例的指针可以直接从inode->i_bdev得到,否则需要使用dev_t信息创建实例 bdev = bd_acquire(inode); if (bdev == NULL) return -ENOMEM; filp->f_mapping = bdev->bd_inode->i_mapping; res = blkdev_get(bdev, filp->f_mode); if (res) return res; //如果设置了标志FMODE_EXCL来请求对块设备的独占访问,那么会调用bd_claim要求持有该块设备,这会将与设备文件关联的file实例设置为该块设备的当前持有者 if (filp->f_mode & FMODE_EXCL) { res = bd_claim(bdev, filp); if (res) goto out_blkdev_put; } return 0; out_blkdev_put: blkdev_put(bdev, filp->f_mode); return res; }
0x5: 请求结构
内核提供了数据结构以描述发送给块设备的请求
\linux-2.6.32.63\include\linux\blkdev.h
struct request { //发送给块设备的请求的一个特有性质就是,请求需要保存在请求队列上,这种队列使用双链表实现 struct list_head queuelist; struct call_single_data csd; int cpu; //IO请求request所形成的队列,在linux内核中struct request_queue描述,保存对设备的读写请求 struct request_queue *q; //cmd_flags包含了用于请求的一组通用标志 unsigned int cmd_flags; /* cmd_type表示请求的类型 enum rq_cmd_type_bits { REQ_TYPE_FS = 1, // fs request REQ_TYPE_BLOCK_PC, // scsi command REQ_TYPE_SENSE, // sense request REQ_TYPE_PM_SUSPEND, // suspend request REQ_TYPE_PM_RESUME, // resume request REQ_TYPE_PM_SHUTDOWN, // shutdown request REQ_TYPE_SPECIAL, // driver defined type REQ_TYPE_LINUX_BLOCK, // generic block layer message REQ_TYPE_ATA_TASKFILE, REQ_TYPE_ATA_PC, }; 最常见的请求类型是REQ_TYPE_FS,它用于与块设备之间的实际数据传输 */ enum rq_cmd_type_bits cmd_type; unsigned long atomic_flags; /* the following two fields are internal, NEVER access directly 特征数据,如扇区、块长度、设备容量 */ sector_t __sector; /* sector cursor 需要传输的下一个扇区号 */ unsigned int __data_len; /* total data len */ /* BIO用于在系统和设备之间传输数据 1. bio: 标识传输尚未完成的当前BIO实例 2. biotail: 指向最后一个BIO实例,因为一个请求中可使用多个BIO */ struct bio *bio; struct bio *biotail; struct hlist_node hash; /* merge hash */ /* * The rb_node is only used inside the io scheduler, requests * are pruned when moved to the dispatch queue. So let the * completion_data share space with the rb_node. */ union { struct rb_node rb_node; /* sort/lookup */ void *completion_data; }; /* * two pointers are available for the IO schedulers, if they need * more they have to dynamically allocate it. 函数指针,指向I/O调度器实现,用来重排请求的函数 类似于大多数内核数据结构,request也包含了指向私有数据的指针,elevator_private、elevator_private2它们可以通过当前处理请求的I/O调度器(电梯 elevator)设置 */ void *elevator_private; void *elevator_private2; struct gendisk *rq_disk; unsigned long start_time; /* Number of scatter-gather DMA addr+len pairs after * physical address coalescing is performed. */ unsigned short nr_phys_segments; unsigned short ioprio; void *special; /* opaque pointer available for LLD use */ char *buffer; /* kaddr of the current segment if available */ int tag; int errors; int ref_count; /* when request is used as a packet command carrier 请求可用于向设备传送控制命令,即请求可以用作"数据包命令载体(packet command carrier)",想要的命令在cms数组中列出 */ unsigned short cmd_len; unsigned char __cmd[BLK_MAX_CDB]; unsigned char *cmd; unsigned int extra_len; /* length of alignment and padding */ unsigned int sense_len; unsigned int resid_len; /* residual count */ void *sense; unsigned long deadline; struct list_head timeout_list; unsigned int timeout; int retries; /* * completion callback. */ rq_end_io_fn *end_io; void *end_io_data; /* for bidi */ struct request *next_rq; };
0x6: BIO
我们接下来讨论BIO的实现原理
BIO的每个主要管理结构(bio)关联到一个向量(数组),各个数组项都指向一个内存页(指向对应于该页帧的page实例),这些页用于从设备接收数据、向设备发送数据
需要明白的是,这里显然可以使用高端内存域的页面,这些页帧无法直接映射到内核中,因为无法通过内核虚拟地址访问,对于BIO来说,该做法很有用,数据直接复制给用户空间应用程序,而应用程序可以使用页表访问高端内存域页帧
这些内存页可以但不必一定按连续方式组织,这简化了分散-聚集I/O操作的实现,BIO在内核源代码中对象的结构定义如下
\linux-2.6.32.63\include\linux\bio.h
struct bio { /* device address in 512 byte sectors 指定了传输开始的扇区号 */ sector_t bi_sector; /* request queue link 将与请求关联的几个BIO组织到一个单链表中 */ struct bio *bi_next; //bi_bdev是一个指针,指向请求所属设备的block_device数据结构 struct block_device *bi_bdev; unsigned long bi_flags; /* status, command, etc */ unsigned long bi_rw; /* bottom bits READ/WRITE, * top bits priority */ unsigned short bi_vcnt; /* how many bio_vec's bio_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. bi_phys_segments指定了传输中段的数目,代表了I/O MMU重新映射之前的数值 */ unsigned int bi_phys_segments; /* residual I/O count 表示请求所涉及数据的长度,单位为字节 */ unsigned int bi_size; /* * To keep track of the max segment size, we account for the * sizes of the first and last mergeable segments in this bio. */ unsigned int bi_seg_front_size; unsigned int bi_seg_back_size; unsigned int bi_max_vecs; /* max bvl_vecs we can hold */ unsigned int bi_comp_cpu; /* completion CPU */ atomic_t bi_cnt; /* pin count */ /* the actual vec list bi_io_vec是一个指向I/O向量的指针 struct bio_vec { struct page *bv_page; //bv_page指向用于数据传输的页对应的page实例 unsigned int bv_len; //bv_len指定了用于数据传输的字节数目(如果整页不完全填充的话) unsigned int bv_offset; //bv_offset表示该页内的偏移量,通常该值为0,也为页边界通常用作I/O操作的边界 }; */ struct bio_vec *bi_io_vec; //在硬件传输完成时,设备驱动程序必须调用bi_end_io,这使得块设备层有机会进行清理,或者唤醒等待该请求结束的睡眠进程 bio_end_io_t *bi_end_io; //通常BIO代码不会修改bi_private,该成员可用于驱动程序相关的信息 void *bi_private; #if defined(CONFIG_BLK_DEV_INTEGRITY) struct bio_integrity_payload *bi_integrity; /* data integrity */ #endif /* destructor bi_destructor指向一个析构函数,在从内存删除一个bio实例之前调用 */ bio_destructor_t *bi_destructor; /* * We can inline a number of vecs at the end of the bio, to avoid * double allocations for a small number of bio_vecs. This member * MUST obviously be kept at the very end of the bio. */ struct bio_vec bi_inline_vecs[0]; };
0x7: 提交请求
我们接下来讨论内核将数据请求提交给外设的机制,这涉及以下几个知识
1. 缓冲和请求的重排,以减少磁头寻道的移动 2. 捆绑多个操作以提高性能 3. 设备驱动程序的操作,驱动程序与具体的硬件交互以处理请求 4. 虚拟文件系统中与设备文件相关的通用代码,这部分代码通过设备文件又关联到用户应用程序以及内核的其他部分,内核会将已经从块设备读取的数据保存在缓存中,以便在未来重复提交同样的请求时重用
我们接下来重点讨论内核如何向设备提交物理请求来读取和写入数据,内核分两个步骤提交请求
1. 内核首先创建一个bio实例以描述请求,然后将该实例嵌入到请求中,并置于请求队列上 2. 接下来内核将处理请求队列并执行bio中的操作
在BIO创建后,调用makre_request_fn产生一个新请求以插入到请求队列,请求通过request_fn提交
1. 创建请求
submit_bio是一个关键函数,负责根据传递的bio实例创建一个新请求,并使用generic_make_request将请求置于驱动程序的请求队列上
\linux-2.6.32.63\block
void submit_bio(int rw, struct bio *bio) { int count = bio_sectors(bio); bio->bi_rw |= rw; /* * If it's a regular read/write or a barrier with data attached, * go through the normal accounting stuff before submission. */ if (bio_has_data(bio)) { if (rw & WRITE) { count_vm_events(PGPGOUT, count); } else { task_io_account_read(bio->bi_size); count_vm_events(PGPGIN, count); } if (unlikely(block_dump)) { char b[BDEVNAME_SIZE]; printk(KERN_DEBUG "%s(%d): %s block %Lu on %s\n", current->comm, task_pid_nr(current), (rw & WRITE) ? "WRITE" : "READ", (unsigned long long)bio->bi_sector, bdevname(bio->bi_bdev, b)); } } generic_make_request(bio); } EXPORT_SYMBOL(submit_bio);
内核中的各个地方都会调用submit_bio函数发起物理数据传输,submit_bio只是更新内核的统计量,实际工作在generic_make_request中完成
static inline void __generic_make_request(struct bio *bio) { struct request_queue *q; sector_t old_sector; int ret, nr_sectors = bio_sectors(bio); dev_t old_dev; int err = -EIO; might_sleep(); if (bio_check_eod(bio, nr_sectors)) goto end_io; /* * Resolve the mapping until finished. (drivers are * still free to implement/resolve their own stacking * by explicitly returning 0) * * NOTE: we don't repeat the blk_size check for each new device. * Stacking drivers are expected to know what they are doing. */ old_sector = -1; old_dev = 0; do { char b[BDEVNAME_SIZE]; //调用bdev_get_queue,找到该请求所涉及块设备的请求队列 q = bdev_get_queue(bio->bi_bdev); if (unlikely(!q)) { printk(KERN_ERR "generic_make_request: Trying to access " "nonexistent block-device %s (%Lu)\n", bdevname(bio->bi_bdev, b), (long long) bio->bi_sector); goto end_io; } if (unlikely(!bio_rw_flagged(bio, BIO_RW_DISCARD) && nr_sectors > queue_max_hw_sectors(q))) { printk(KERN_ERR "bio too big device %s (%u > %u)\n", bdevname(bio->bi_bdev, b), bio_sectors(bio), queue_max_hw_sectors(q)); goto end_io; } if (unlikely(test_bit(QUEUE_FLAG_DEAD, &q->queue_flags))) goto end_io; if (should_fail_request(bio)) goto end_io; /* * If this device has partitions, remap block n * of partition p to block n+start(p) of the disk. 如果该设备是分区的,则用blk_partition_remap重新映射该请求,以确保读写正确的区域 这使得内核的其余部分可以间各个分区当作独立的、非分区设备对待 如果分区起始于扇区m而将要访问分区内的扇区m,那么必须创建一个请求来访问块设备的扇区n+m 分区的正确偏移量,保存在与队列关联的gendisk实例的parts数组中 */ blk_partition_remap(bio); if (bio_integrity_enabled(bio) && bio_integrity_prep(bio)) goto end_io; if (old_sector != -1) trace_block_remap(q, bio, old_dev, old_sector); old_sector = bio->bi_sector; old_dev = bio->bi_bdev->bd_dev; if (bio_check_eod(bio, nr_sectors)) goto end_io; if (bio_rw_flagged(bio, BIO_RW_DISCARD) && !blk_queue_discard(q)) { err = -EOPNOTSUPP; goto end_io; } trace_block_bio_queue(q, bio); //q->make_request_fn根据bio产生请求并发送给设备驱动程序,对大多数设备,发送操作调用内核的标准函数(__make_request)完成 ret = q->make_request_fn(q, bio); } while (ret); return; end_io: bio_endio(bio, err); }
我们接着讨论make_request_fn的默认实现: __make_request
static int __make_request(struct request_queue *q, struct bio *bio) { struct request *req; int el_ret; unsigned int bytes = bio->bi_size; const unsigned short prio = bio_prio(bio); const bool sync = bio_rw_flagged(bio, BIO_RW_SYNCIO); const bool unplug = bio_rw_flagged(bio, BIO_RW_UNPLUG); const unsigned int ff = bio->bi_rw & REQ_FAILFAST_MASK; int rw_flags; if (bio_rw_flagged(bio, BIO_RW_BARRIER) && (q->next_ordered == QUEUE_ORDERED_NONE)) { bio_endio(bio, -EOPNOTSUPP); return 0; } /* * low level driver can indicate that it wants pages above a * certain limit bounced to low memory (ie for highmem, or even * ISA dma in theory) */ blk_queue_bounce(q, &bio); spin_lock_irq(q->queue_lock); //在创建请求所需信息已经从传递的bio实例读取之后,内核调用elv_queue_empty检查I/O调度器队列当前是否为空,用于实现请求重排 if (unlikely(bio_rw_flagged(bio, BIO_RW_BARRIER)) || elv_queue_empty(q)) goto get_rq; /* 如果队列中有待决请求,则调用elv_merge,该函数会进一步调用请求队列elevator成员的elvator_merge_fn函数 它返回一个指针,指向请求链表中需要插入新请求的位置。I/O调度器还指定了请求是否以及如何与现存请求合并 1. ELEVATOR_BACK_MERGE、ELEVATOR_FRONT_MERGE: 使新请求与请求链表中找到的请求合并 2. ELV_NO_MERGE: 发下该请求无法与请求队列上现存的请求合并,因而该请求必须添加到请求队列中 */ el_ret = elv_merge(q, &req, bio); switch (el_ret) { case ELEVATOR_BACK_MERGE: BUG_ON(!rq_mergeable(req)); if (!ll_back_merge_fn(q, req, bio)) break; trace_block_bio_backmerge(q, bio); if ((req->cmd_flags & REQ_FAILFAST_MASK) != ff) blk_rq_set_mixed_merge(req); req->biotail->bi_next = bio; req->biotail = bio; req->__data_len += bytes; req->ioprio = ioprio_best(req->ioprio, prio); if (!blk_rq_cpu_valid(req)) req->cpu = bio->bi_comp_cpu; drive_stat_acct(req, 0); if (!attempt_back_merge(q, req)) elv_merged_request(q, req, el_ret); goto out; case ELEVATOR_FRONT_MERGE: BUG_ON(!rq_mergeable(req)); if (!ll_front_merge_fn(q, req, bio)) break; trace_block_bio_frontmerge(q, bio); if ((req->cmd_flags & REQ_FAILFAST_MASK) != ff) { blk_rq_set_mixed_merge(req); req->cmd_flags &= ~REQ_FAILFAST_MASK; req->cmd_flags |= ff; } bio->bi_next = req->bio; req->bio = bio; /* * may not be valid. if the low level driver said * it didn't need a bounce buffer then it better * not touch req->buffer either... */ req->buffer = bio_data(bio); req->__sector = bio->bi_sector; req->__data_len += bytes; req->ioprio = ioprio_best(req->ioprio, prio); if (!blk_rq_cpu_valid(req)) req->cpu = bio->bi_comp_cpu; drive_stat_acct(req, 0); if (!attempt_front_merge(q, req)) elv_merged_request(q, req, el_ret); goto out; /* ELV_NO_MERGE: elevator says don't/can't merge. */ default: ; } ...
这是I/O调度器可以采取的仅有的一些操作,它不能以任何其他方法影响请求队列,从这点也可以看出I/O调度器和CPU调度器之间的差别,尽管两者都面临一个非常类似的问题,但是它们提供的解决方案差别很大
在满足I/O调度器的需求之后,内核必须产生一个新请求,内核会汇集读写操作的请求,并一次性执行收集到的所有请求
2. 队列插入
就性能角度考虑而言,我们希望重排各个请求,并将可能的请求合并为更大的请求,以提升数据传输的性能,显然,这只适用于队列包含了多个可以合并的请求的情况,因此,内核首先需要在队列中汇集一些请求,然后一次性处理所有请求,这样就自动创造了合并请求的时机,这是一种基于cache的请求重排优化思想
内核使用队列插入(queue plugging)机制,来有意阻止请求的处理,请求队列可能处于空闲状态或者插入状态
1. 如果队列处于空闲状态,队列中等待的请求将会被处理 2. 如果队列处于插入状态,新的请求只是添加到队列,但并不处理,request_queue的queue_flags成员中QUEUE_FLAG_PLUGGED标识置位
内核用blk_plug_device插入一个队列,但如果没有发送同步请求,则不会显示拔出队列,内核通过blk_plug_device确保队列将在未来的某个时间再次得到处理
\linux-2.6.32.63\block\blk-core.c
void blk_plug_device(struct request_queue *q) { WARN_ON(!irqs_disabled()); /* * don't plug a stopped queue, it must be paired with blk_start_queue() * which will restart the queueing */ if (blk_queue_stopped(q)) return; if (!queue_flag_test_and_set(QUEUE_FLAG_PLUGGED, q)) { mod_timer(&q->unplug_timer, jiffies + q->unplug_delay); trace_block_plug(q); } } EXPORT_SYMBOL(blk_plug_device);
这段代码确保队列的拔出定时器在q->unplug_delay(单位是jiffies: (3 * HZ) / 1000、3ms)之后启用,定时器会调用blk_unplug_timeout拔出队列
除此之外还有另一种机制可用于拔出队列,如果当前读写请求的数目(保存在请求链表的count数组的两个数组项中)达到unplug_thresh指定的阀值,则elv_insert中调用__generic_unplug_device以触发拔出操作,使得等待的请求得到处理
在重要的I/O操作处于待决状态时,内核还能够手工进行拔出操作,这确保在数据紧急需要时,能够立即执行重要的读取操作,在出现同步请求时,就会发生这种情况
3. 执行请求
在请求队列中的请求即将处理时,会调用特定于设备的request_fn函数,该任务与硬件的关联非常紧密,因此内核不会提供默认的实现,因此,内核总是调用blk_dev_init注册队列时传递的方法
下面使用一个简单的硬件无关示例例程,用于说明所有驱动程序在request_fn中所执行的基本步骤
void sample_request(request_queue_t *q) { int status; struct request *req; //在while循环中嵌入了elv_next_request,用于从队列顺序读取请求 while((req = elv_next_request(q)) != NULL) { if(!blk_fs_request(req)) { //end_request是一个标准的内核函数,用于从请求队列删除请求,并更新内核统计量,并执行任何在request->completion等待的完成量 end_request(req, 0); } continue; } //传输通过perform_sample_transfer执行 status = perform_sample_transfer(req); end_request(req, status); }
BIO不仅可用于传输数据,还可以传输诊断信息,驱动程序必须调用blk_fs_request来检查实际上传输的是否是数据,在真正的驱动程序中,特定于硬件的操作通常会分离到独立的函数中,以保持代码的简洁
int perform_sample_transfer(request *req) { //在判断请求是读操作还是写操作时,会查看cmd字段,然后采取对应的的行动,在系统和硬件之间传输数据 switch(req->cmd) { case READ: //执行特定于硬件的数据读取功能 break; case WRITE: //执行特定于硬件的数据写入功能 break; default: return -EFAULT; } }
0x8: I/O调度
内核采用的各种用于调度和重排I/O操作的算法,称之为I/O调度器(对比通常的进程调度器、网络中控制通信数据量的数据包调度器),通常,I/O调度器也称作电梯(elevator),它们由下列数据结构中的一组函数表示
\linux-2.6.32.63\include\linux\elevator.h
struct elevator_ops { //elevator_merge_fn检查一个新的请求是否可以与现存的请求合并,它还指定了请求插入到队列中的位置 elevator_merge_fn *elevator_merge_fn; elevator_merged_fn *elevator_merged_fn; //elevator_merge_req_fn将两个请求合并而一个请求 elevator_merge_req_fn *elevator_merge_req_fn; //elevator_allow_merge_fn在两个请求已经合并后调用,它执行清理工作,并返回I/O调度器中因为合并而不再需要的那部分管理数据 elevator_allow_merge_fn *elevator_allow_merge_fn; //elevator_dispatch_fn从给定的请求队列中选择下一步应该调度执行的请求 elevator_dispatch_fn *elevator_dispatch_fn; //elevator_add_req_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_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 *elevator_set_req_fn; //elevator_put_req_fn用于在释放会内存管理子系统调用 elevator_put_req_fn *elevator_put_req_fn; elevator_may_queue_fn *elevator_may_queue_fn; //elevator_init_fn在队列初始化时调用,效果等同于构造函数 elevator_init_fn *elevator_init_fn; //elevator_exit_fn在队列释放时调用,效果等同于析构函数 elevator_exit_fn *elevator_exit_fn; void (*trim)(struct io_context *); };
I/O调度器不仅负责请求重排,还负责请求队列全部的管理工作,每个I/O调度器都封装在下列数据结构中,其中还包含了供内核使用的其他管理信息
\linux-2.6.32.63\include\linux\elevator.h
struct elevator_type { struct list_head list; struct elevator_ops ops; struct elv_fs_entry *elevator_attrs; char elevator_name[ELV_NAME_MAX]; struct module *elevator_owner; };
内核将所有I/O调度器在一个标准的双链表中维护,链表元素是list成员(表头由全局变量elv_list表示),还对每个I/O调度器都给出了一个可理解的名称,用于从用户空间选择I/O调度器。sysfs中的属性将会保存在elevator_attrs中,它们可用于以磁盘为单位来微调I/O调度器的行为
内核实现了一些列的I/O调度器,但设备驱动程序可以根据自身的需要修改调度器的特定函数、或者自行实现调度器,I/O调度器有下列属性
1. elevator_noop 是一个非常简单的I/O调度器,将新来的请求按"先来先服务"的原则一次添加到队列,以便进行处理,请求会进行合并但无法重排。noop(no operation 空操作)I/O调度器仅对于能够自行重排请求的智能硬件,才是一个好的选择,对于没有活动部件的设备(因而没有寻道时间),如闪存盘,该调度器也是很好的 2. iosched_deadline 它用于两个目的 1) 试图最小化磁盘寻道(即读写磁头的移动)的次数,需要使用冗长的数据结构(红黑树和链表)分析各个请求,并按照最低延迟的原则来重排请求,以降低磁盘寻道的次数 2) 尽可能确保请求在一定时间内处理完成,会使用内核的定时器机制实现单个请求的"到期时间" 3. iosched_as 实现了预测调度器,它会尽可能预测进程的行为,该调度器假定读请求不是彼此完全独立的,在此前提下试图实现预测调度,在应用程序向内核提交一个读请求时,该调度器会作出以下假设: 在一定时间内会有另一个相关请求提交。如果读请求在磁盘忙于写操作期间提交,那么这个假定就很重要。为确保良好的交互行为,内核会延迟写操作,并优先选择读操作,如果第一个读请求之后立即恢复写操作,则需要一个磁盘寻道操作,而稍后会有另一个新的读请求到达,这又浪费了寻道操作的效果。在这种情况下,较好的选择是在第一个读请求完成之后不立即移动磁头,暂时hold住紧接的写请求,而等待稍后的下一个读请求到达,如果在预期时间内第二个读请求没有到达,内核就可以恢复写操作 4. iosched_cfg 提供了完全公平排队(completel fairness queuing)的特性,它围绕几个队列展开,所有的请求都在这些队列中排序,同一给定进程的请求,总是在同一队列中处理。时间片会分配到每个队列,内核使用一个轮转算法来处理各个队列,这确保了I/O带宽以公平的方式在不同队列之间共享。如果队列的数目大于等于同时进行I/O的进程数目,这就意味着I/O带宽也公平地分配到了各个进程上
0x9: ioctl的实现
ioctl使得我们能够使用特殊的、特定于设备的功能,这些功能无法通过普通的读写操作访问,这种支持ioctl系统调用实现,该系统调用可以用于普通的文件。该系统调用在sys_ioctl实现,但主要工作由vfs_ioctl完成
\linux-2.6.32.63\fs\ioctl.c
static long vfs_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { int error = -ENOTTY; if (!filp->f_op) goto out; if (filp->f_op->unlocked_ioctl) { error = filp->f_op->unlocked_ioctl(filp, cmd, arg); if (error == -ENOIOCTLCMD) error = -EINVAL; goto out; } else if (filp->f_op->ioctl) { lock_kernel(); //调用file_operations中特定于文件的ioctl函数(如果存在的话)以处理该ioctl(普通文件通常不提供ioctl函数,这样该系统调用会返回错误码) error = filp->f_op->ioctl(filp->f_path.dentry->d_inode, filp, cmd, arg); unlock_kernel(); } out: return error; }
7. 资源分配
I/O端口和I/O内存是两种概念上的方法,用以支持设备驱动程序和设备之间的通信。为使得各种不同的驱动程序彼此互补干扰,需要事先为驱动程序分配端口和I/O内存范围,这确保几种设备驱动程序不会试图访问同样的资源
0x1: 资源管理
1. 树数据结构
Linux提供了一个通用构架,用于在内存中构建数据结构,这些结构描述了系统中可用的资源,使得内核代码能够管理和分配资源,其中关键的数据结构是resource
\linux-2.6.32.63\include\linux\ioport.h
struct resource { /* start、end指定了一个一般性的区域,尽管理论上这两个数字的内容可以自由解释,但通常表示某个地址空间中的一个区域 */ resource_size_t start; resource_size_t end; //name存储了一个字符串,以便给资源赋予一个有意义的名字,资源名称实际上与内核无关,但在proc文件系统中以可读形式输出资源列表给管理员看时比较有用 const char *name; //flags用于更准确地描述资源及当前状态 unsigned long flags; //父、兄弟、子节点指针能够建立一个树形层次结构 struct resource *parent, *sibling, *child; };
用于连接parent、child、sibling成员的规则很简单
1. 每个子节点只有一个父节点 2. 每个父节点可以有任意数目的子节点 3. 同一个父节点的所有子节点,会连接到兄弟结点的链表上
在内存中表示数据结构时,必须要注意以下问题
1. 尽管每个子节点都有一个指针指向父节点,但父节点只有一个指针指向第一个子节点,所有其他子节点都通过兄弟结点链表访问 2. 指向父节点的指针同样可以为NULL,在这种情况下,说明已经没有更高层次的结点了
2. 请求和释放资源
为确保可靠地配置资源(无论何种类型),内核必须提供一种机制来分配和释放资源,一旦资源已经分配,则不能由任何其他驱动程序使用,请求和释放资源,本质上就是从资源树中添加和删除项
1. 请求资源 内核提供了__reques_resource函数,用于请求一个资源区域 static struct resource * __request_resource(struct resource *root, struct resource *new) //在扫描特定父节点的子节点时,只会在一个层次上扫描兄弟结点链表,内核不会扫描更底层子节点的链表 2. 释放资源 调用release_resource函数释放使用中的资源 int release_resource(struct resource *old)
0x2: I/O内存
资源、管理的一个很重要的方面就是I/O内存的分配方式,因为在所有平台上这都是与外设通信的主要方法(IA-32除外,其中I/O端口更重要)。I/O内存不仅包括与扩展设备通信直接使用的内存区域,还包括系统中可用的物理内存和ROM存储器,以及包含在资源列表中的内存
zhenghan.zh@miser010249213142.yh $ cat /proc/iomem 00010000-0009dbff : System RAM 0009dc00-0009ffff : reserved 000a0000-000bffff : Video RAM area 000c0000-000c8bff : Video ROM 000c9000-000c97ff : Adapter ROM 000c9800-000cb9ff : Adapter ROM 000f0000-000fffff : System ROM 00100000-dfffcfff : System RAM 00200000-0047dd90 : Kernel code 0047dd91-005c075f : Kernel data dfffd000-dfffffff : reserved f0000000-f1ffffff : 0000:00:02.0 f2000000-f2000fff : 0000:00:02.0 f2010000-f201ffff : 0000:00:02.0 f2020000-f2020fff : 0000:00:03.0 f2020000-f2020fff : virtio-pci f2030000-f203ffff : 0000:00:03.0 f2040000-f2040fff : 0000:00:04.0 f2040000-f2040fff : virtio-pci fffbc000-ffffffff : reserved 100000000-2132fffff : System RAM
所有分配的I/O内存地址,都通过一棵资源树管理,树的根节点是全局内核变量iomem_resource
但在使用I/O内存时,分配内存区域并不是所需的唯一操作,取决于总线系统和处理器类型,可能必需将扩展设备的地址空间映射到内核地址空间之后,才能访问该设备(称之为软件I/O映射),这是通过使用ioremap内核函数适当设置系统页表而实现的,内核源代码中有若干不同地方使用了该函数,其定义是体系结构相关的,同样的,还提供了特定于体系结构的iounmap函数来解除映射
总体来说,实现对进程页表的操作较为复杂,不同系统的实现有很大差别,更重要的是,将一个物理地址映射到处理的虚拟地址空间中,使得内核可以使用该地址,就设备驱动程序而言,这意味着扩展总线的地址空间映射到CPU的地址空间中,使得能够用普通内存访问函数操作总线/设备
0x3: I/O端口
I/O端口是一种与设备和总线通信的常用方法,特别是在IA-32平台上,类似于I/O内存,按良好范例编写的驱动程序在访问所需的区域之前,相应的区域必须已经注册
\linux-2.6.32.63\kernel\resource.c中的ioport_resource充当资源树的根节点,proc文件系统中的ioports文件可以显示已经分配的端口地址
root@iZ231vybmaeZ:/usr/local/aegis/aegis_client/aegis_00_73# cat /proc/ioports 0000-0cf7 : PCI Bus 0000:00 0000-001f : dma1 0020-0021 : pic1 0040-0043 : timer0 0050-0053 : timer1 0060-0060 : keyboard 0064-0064 : keyboard 0070-0071 : rtc0 0080-008f : dma page reg 00a0-00a1 : pic2 00c0-00df : dma2 00f0-00ff : fpu 0170-0177 : 0000:00:01.1 0170-0177 : ata_piix 01f0-01f7 : 0000:00:01.1 01f0-01f7 : ata_piix 0376-0376 : 0000:00:01.1 0376-0376 : ata_piix 03c0-03df : vga+ 03f2-03f2 : floppy 03f4-03f5 : floppy 03f6-03f6 : 0000:00:01.1 03f6-03f6 : ata_piix 03f7-03f7 : floppy 04d0-04d1 : pnp 00:02 08a0-08a3 : pnp 00:02 0cc0-0ccf : pnp 00:02 0cf8-0cff : PCI conf1 0d00-ffff : PCI Bus 0000:00 10c0-1141 : pnp 00:01 1f40-1f7f : 0000:00:01.3 1f40-1f43 : ACPI PM1a_EVT_BLK 1f44-1f45 : ACPI PM1a_CNT_BLK 1f48-1f4b : ACPI PM_TMR 1f68-1f6f : ACPI GPE0_BLK b010-b015 : ACPI CPU throttle b044-b047 : pnp 00:01 c000-c0ff : 0000:00:03.0 c000-c0ff : xen-platform-pci c100-c11f : 0000:00:01.2 c100-c11f : uhci_hcd c120-c12f : 0000:00:01.1 c120-c12f : ata_piix
在汇编程序层次上,端口通常必须通过特殊的处理器命令访问,因此内核提供了对应的宏,以便向驱动程序开发者提供一个系统无关的接口
1. insb(port, addr, num) 2. insl(port, addr, num) //从端口port读取num个字节、字、或长整数,复制到普通地址空间中的地址addr 3. insw(port, addr, num) 4. outsb(port, addr, num) 5. outsl(port, addr, num) 6. outsw(port, addr, num)
8. 总线系统
尽管扩展设备通过设备驱动程序处理,而驱动程序与内核其余的代码通过一组固定的接口通信,因此扩展设备/驱动程序对核心的内核源代码没有太多影响,但内核需要解决一个更基本的问题: 设备如何通过总线附接到系统的其余部分
与具体设备的驱动程序相比,总线驱动程序与核心内核代码的工作要密切得多,另外,总线驱动程序向相关的设备驱动程序提供功能和选项的方式,也不存在标准的接口,这是因为,不同的总线系统之间,使用的硬件技术可能差异很大。但这并不意味着负责管理不同总线的代码没有共同点,而是相似的总线采用相似的概念,还引入了通用驱动程序模型,在一个主要数据结构的集合中管理所有系统总线,采用最小分母的方式,尽可能降低不同总线驱动程序之间的差异
内核支持大量总线,可能涉及多种硬件平台,也可可能只涉及一种平台,我们着重讨论PCI总线,它几乎具备一种强大的系统总线所应有的所有共同和关键要素,在Linux支持的大都数体系结构上都使用了PCI总线
0x1: 通用驱动程序模型
现代总线系统在布局和结构的细节上可能有所不同,但也有许多共同之处,内核的数据结构很好地反映了这个事实,结构中的许多成员用于所有的总线(以及相关设备的数据结构中),在Linux Kernel 2.6开发期间,一个通用驱动程序模型(device model 设备模型)并入内核,以防止不必要的复制,所有总线共有的属性封装到特殊的、可以通用方法处理的数据结构中,在关联到总线相关的成员
1. 设备的表示
驱动程序模型采用一种特殊数据结构来表示几乎所有总线类型通用的设备属性,该结构直接嵌入到特定于总线的数据结构中,而不是通过指针引用,这和Linux内核的kobject类似,定义如下
\linux-2.6.32.63\include\linux\device.h
struct device { //兄弟结点链表中的结点 struct device *parent; struct device_private *p; //嵌入的kobject控制通用对象属性 struct kobject kobj; const char *init_name; /* initial name of the device */ struct device_type *type; struct semaphore sem; /* semaphore to synchronize calls to its driver. */ /* type of bus device is on 所在总线设备的类型 */ struct bus_type *bus; /* which driver has allocated this device 分配当前device实例的驱动程序 */ struct device_driver *driver; /* Platform specific data, device core doesn't touch it 特定于平台的数据,设备模型代码不会访问 */ void *platform_data; struct dev_pm_info power; #ifdef CONFIG_NUMA int numa_node; /* NUMA node this device is close to */ #endif u64 *dma_mask; /* dma mask (if dma'able device) */ u64 coherent_dma_mask;/* Like dma_mask, but for alloc_coherent mappings as not all hardware supports 64 bit addresses for consistent allocations such descriptors. */ struct device_dma_parameters *dma_parms; struct list_head dma_pools; /* dma pools (if dma'ble) */ struct dma_coherent_mem *dma_mem; /* internal for coherent mem override */ /* arch specific additions */ struct dev_archdata archdata; dev_t devt; /* dev_t, creates the sysfs "dev" */ spinlock_t devres_lock; struct list_head devres_head; //klist_node是list_head的数据结构的增强版,其中增加了与锁和引用计数相关的成员,这种类型的链表只用于通用设备模型,内核的其余部分不会使用 struct klist_node knode_class; struct class *class; const struct attribute_group **groups; /* optional groups */ void (*release)(struct device *dev); };
内核提供了一个标准函数device_register,用于将一个新设备添加到内核的数据结构,通用驱动程序模型也为设备驱动单独设计了一种数据结构
struct device_driver { //指向一个字符串,用于唯一标识该驱动程序 const char *name; //bus指向一个表示总线的对象,并提供特定于总线的操作 struct bus_type *bus; struct module *owner; const char *mod_name; /* used for built-in modules */ bool suppress_bind_attrs; /* disables bind/unbind via sysfs */ 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); const struct attribute_group **groups; const struct dev_pm_ops *pm; struct driver_private *p; };
2. 总线的表示
通用驱动程序模型不仅表示了设备,还用另一个数据结构表示了总线
struct bus_type { //name表示总线的名称,它用于早isysfs文件系统中标识该总线 const char *name; struct bus_attribute *bus_attrs; struct device_attribute *dev_attrs; struct driver_attribute *drv_attrs; int (*match)(struct device *dev, struct device_driver *drv); int (*uevent)(struct device *dev, struct kobj_uevent_env *env); 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); const struct dev_pm_ops *pm; struct bus_type_private *p; };
3. 注册过程
设备、设备驱动程序的各个数据结构之间彼此关联
1. 注册总线
在可以注册设备及其驱动程序之前,需要有总线,因此需要通过bus_register开始,该函数向系统添加一个新总线
\linux-2.6.32.63\drivers\base\bus.c
int bus_register(struct bus_type *bus) { int retval; struct bus_type_private *priv; priv = kzalloc(sizeof(struct bus_type_private), GFP_KERNEL); if (!priv) return -ENOMEM; priv->bus = bus; bus->p = priv; BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifier); retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name); if (retval) goto out; /* 首先,通过嵌入的kset类型成员的subsys,将新总线添加到总线子系统 总线需要了解相关设备机器驱动程序的所有有关信息 */ priv->subsys.kobj.kset = bus_kset; priv->subsys.kobj.ktype = &bus_ktype; priv->drivers_autoprobe = 1; retval = kset_register(&priv->subsys); .. }
2. 注册设备
注册设备包括两个独立的步骤
1. 初始化设备的数据结构: device_initialize 2. 将其加入到数据结构的网络中: device_add
3. 注册设备驱动程序
在进行一些检查和初始化工作之后,driver_register调用bus_add_driver将一个新驱动程序添加到一个总线
int bus_add_driver(struct device_driver *drv) { struct bus_type *bus; struct driver_private *priv; int error = 0; bus = bus_get(drv->bus); if (!bus) return -EINVAL; pr_debug("bus: '%s': add driver %s\n", bus->name, drv->name); priv = kzalloc(sizeof(*priv), GFP_KERNEL); if (!priv) { error = -ENOMEM; goto out_put_bus; } klist_init(&priv->klist_devices, NULL, NULL); priv->driver = drv; drv->p = priv; priv->kobj.kset = bus->p->drivers_kset; //驱动程序首先要有名字 error = kobject_init_and_add(&priv->kobj, &driver_ktype, NULL, "%s", drv->name); if (error) goto out_unregister; //如果总线支持自动探测,则调用driver_attach,该函数迭代总线上的所有设备,使用驱动程序的match函数进行检测,确定是否有某些设备可使用该驱动程序管理 if (drv->bus->p->drivers_autoprobe) { error = driver_attach(drv); if (error) goto out_unregister; } //将该驱动程序添加到总线上注册的所有驱动程序的链表中 klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers); module_add_driver(drv->owner, drv); error = driver_create_file(drv, &driver_attr_uevent); if (error) { printk(KERN_ERR "%s: uevent attr (%s) failed\n", __func__, drv->name); } error = driver_add_attrs(bus, drv); if (error) { /* How the hell do we get out of this pickle? Give up */ printk(KERN_ERR "%s: driver_add_attrs(%s) failed\n", __func__, drv->name); } if (!drv->suppress_bind_attrs) { error = add_bind_files(drv); if (error) { /* Ditto */ printk(KERN_ERR "%s: add_bind_files(%s) failed\n", __func__, drv->name); } } kobject_uevent(&priv->kobj, KOBJ_ADD); return 0; out_unregister: kfree(drv->p); drv->p = NULL; kobject_put(&priv->kobj); out_put_bus: bus_put(bus); return error; }
0x2: PCI总线
PCI(peripheral component interconnect)是英特尔公司开发的一种标准总线,它迅速在系统组件和体系结构厂商中确立了标准地位,成为一种非常流行的总线,它成功替代了ISA总线
1. PCI系统的布局
在讨论内核中PCI的实现之前,我们先来了解一下PCI总线的主要原理,系统的某个PCI总线上的每个设备,都由一组3个编号标识
1. 总线编号(bus number) 该设备所在总线的编号(从0开始计数),PCI规范准许每个系统最多255个总线 2. 插槽编号(slot number) 总线内部的一个唯一标识编号,一个总线最多能够附接32个设备,不同总线上的设备插槽编号可能相同 3. 功能编号(function number) 用于在一个扩展卡上,实现包括多个(经典意义上)扩展设备的设备,例如为了节省空间,可以将两个网卡放置在一块扩展卡上,在这种情况下通过不同的功能编号来指定不同的接口。这种情况在笔记本中的多功能芯片组常出现,这些芯片组附接到PCI总线,以最小的空间集成了一整套扩展设备(IDE控制器、USB控制器、调制解调器、网络..),这些扩展设备必须通过功能编号进行区分,PCI标准将一个设备上功能部件的最大数目定义为8
每个设备都通过一个16位编号唯一地标识,其中8bit用于总线编号,5bit用于插槽编号,3bit用于功能编号,驱动程序无需费力处理这些极其紧凑的记法,内核建立了一个数据结构的网络,其中也包含了同样的信息,从C语言的角度来处理这些编号
1. 地址空间
有3个地址空间支持与PCI设备的通信
1. I/O空间通过32个比特位描述,因此,对用于与设备通信的端口地址,提供了最大4GB的空间 2. 取决于处理器类型,数据空间由32bit、64bit描述 //系统中的设备分配到上述两个地址空间中,因而有唯一的地址 3. 配置空间包含了各个设备的类型和特征的详细信息,以省去危险的自动探测工作
这些地址空间会根据处理器类型映射到系统虚拟内存中的不同位置,使得内核和设备驱动程序能够访问对应的资源
2. 配置信息
与古老的总线相比,PCI总线是一种无跳线系统,即扩展设备能够完全通过软件手段配置,而无需用户干涉,为支持这种配置,每个PCI设备都有一个256字节长的配置空间,其中包括该设备的特点和要求的有关信息
1. VendorID 2. DeviceID: VendorID、DeviceID唯一地标识了厂商和设备类型,这两个ID合起来通常称之为设备的签名 3. CmdReg 4. Status Reg 5. RevID: 用于区分不同的设备修订级别,这有助于用于选择设备驱动程序的版本,新版本的设备可能消除了已知的硬件故障或添加了新特性 6. Class Code: 用于将设备分配到各种不同的功能组,该字段分为两部分,前8bit表示基类(base class),而剩余16bit用于表示基类的一个子类,基类及其子类的有如下几种类型 /* 1. PCI_BASE_CLASS_STORAGE(大容量存储器) 1) PCI_CLASS_STORAGE_SCSI(SCSI控制器) 2) PCI_CLASS_STORAGE_IDE(IDE控制器) 3) PCI_CLASS_STORAGE_RAID(RAID控制器): 用于组合做个磁盘驱动器 4) PCI_CLASS_STORAGE_SATA(SATA控制器) 2. PCI_BASE_CLASS_NETWORK(网络) 1) PCI_CLASS_NETWORK_ETHERNET(以太网) 2) PCI_CLASS_NETWORK_FDDI(FDDI) 3. PCI_BASE_CLASS_SYSTEM(系统组件) 1) PCI_CLASS_SYSTEM_DMA(DMA控制器) 2) PCI_CLASS_SYSTEM_RTC(实时时钟) */ .. 7. Subsystem VendorID: 8. Subsystem DeviceID: Subsystem VendorID、Subsystem DeviceID用于更精确地描述设备的通用接口
6个基地址字段每个包含32bit,用于定义PCI设备和系统其余部分通信所用的地址,
尽管该结构长度必须是256字节,但只有前64字节是标准化的,其余空间可以自由使用,通常用于在设备和驱动程序之间交换附加信息
2. 内核中的实现
内核为PCI驱动程序提供了一个广泛的框架,可以粗略地划分为两个类别
1. PCI系统的初始化和资源的分配,以及预备对应的数据结构以反映各个总线和设备的容量和能力,使得能够较为容易地操作总线/设备 2. 支持访问所有PCI选项的标准化函数接口
在各个不同类型的系统上,PCI系统初始化有时差异非常大,例如IA-32系统会在启动时借助于BIOS自行分配所有相关的PCI资源,内核需要做的事情很少,而Alpha系统没有BIOS或适当的等价物,相关工作必须由内核完成
1. 数据结构
内核提供了几个数据结构来管理系统的PCI结构,这些结构声明在pci.h中,通过一个由指针构成的网络互相连接
\linux-2.6.32.63\include\linux\pci.h
//系统中的各个总线由pci_bus的实例表示 struct pci_bus { /* node in list of buses 总线链表中的结点 */ struct list_head node; /* parent bus this bridge is on 此桥接器(总线)所在的父总线 */ struct pci_bus *parent; /* list of child buses 子总线链表 */ struct list_head children; /* list of devices on this bus 总线上设备的链表 */ struct list_head devices; /* bridge device as seen by parent pci_dev结构表示各个设备、扩展卡和功能部件,self表示父总线所看到的桥接器设备 */ struct pci_dev *self; /* list of slots on this bus */ struct list_head slots; //导向到该总线的地址空间 struct resource *resource[PCI_BUS_NUM_RESOURCES]; /* address space routed to this bus */ /* configuration access functions 访问配置信息的各函数 */ struct pci_ops *ops; /* hook for sys-specific extension 挂钩,用于特定硬件的扩展 */ void *sysdata; /* directory entry in /proc/bus/pci */ struct proc_dir_entry *procdir; /* bus number 总线号 */ unsigned char number; /* number of primary bridge 主桥接器编号 */ unsigned char primary; /* number of secondary bridge 次桥接器编号 */ unsigned char secondary; /* max number of subordinate buses 下级总线的最大数目 */ unsigned char subordinate; char name[48]; unsigned short bridge_ctl; /* manage NO_ISA/FBB/et al behaviors */ pci_bus_flags_t bus_flags; /* Inherited by child busses */ struct device *bridge; struct device dev; struct bin_attribute *legacy_io; /* legacy I/O for this bus */ struct bin_attribute *legacy_mem; /* legacy mem */ unsigned int is_added:1; };
2. 总线的表示
3. 设备管理
struct pci_dev是一个关键的数据结构,用于表示系统中的各个PCI设备
/* * The pci_dev structure is used to describe PCI devices. */ struct pci_dev { /* node in per-bus list 在各总线设备链表中的结点 */ struct list_head bus_list; /* bus this device is on 设备所在总线 */ struct pci_bus *bus; /* bus this device bridges to 该桥接器设备接通的总线 */ struct pci_bus *subordinate; /* hook for sys-specific extension 挂钩,用于特定于硬件的扩展 */ void *sysdata; /* device entry in /proc/bus/pci */ struct proc_dir_entry *procent; struct pci_slot *slot; /* Physical slot this device is in */ 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 */ u64 dma_mask; /* Mask of the bits of bus address this device implements. Normally this is 0xffffffff. You only need to change this if your device has broken DMA or supports 64-bit transfers. */ struct device_dma_parameters dma_parms; pci_power_t current_state; /* Current operating state. In ACPI-speak, this is D0-D3, D0 being fully functional, and D3 being off. */ int pm_cap; /* PM capability offset in the configuration space */ unsigned int pme_support:5; /* Bitmask of states from which PME# can be generated */ unsigned int d1_support:1; /* Low power state D1 is supported */ unsigned int d2_support:1; /* Low power state D2 is supported */ unsigned int no_d1d2:1; /* Only allow D0 and D3 */ unsigned int wakeup_prepared:1; #ifdef CONFIG_PCIEASPM struct pcie_link_state *link_state; /* ASPM link state. */ #endif pci_channel_state_t error_state; /* current connectivity state */ struct device dev; /* Generic device interface */ 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; struct resource resource[DEVICE_COUNT_RESOURCE]; /* I/O and memory regions + expansion ROMs */ /* These fields are used by common fixups */ unsigned int transparent:1; /* Transparent PCI bridge */ unsigned int multifunction:1;/* Part of multi-function device */ /* keep track of device state */ unsigned int is_added:1; unsigned int is_busmaster:1; /* device is busmaster */ unsigned int no_msi:1; /* device may not use msi */ unsigned int block_ucfg_access:1; /* userspace config space access is blocked */ unsigned int broken_parity_status:1; /* Device generates false positive parity */ unsigned int irq_reroute_variant:2; /* device needs IRQ rerouting variant */ unsigned int msi_enabled:1; unsigned int msix_enabled:1; unsigned int ari_enabled:1; /* ARI forwarding */ unsigned int is_managed:1; unsigned int is_pcie:1; unsigned int needs_freset:1; /* Dev requires fundamental reset */ unsigned int state_saved:1; unsigned int is_physfn:1; unsigned int is_virtfn:1; unsigned int reset_fn:1; unsigned int is_hotplug_bridge:1; pci_dev_flags_t dev_flags; atomic_t enable_cnt; /* pci_enable_device has been called */ u32 saved_config_space[16]; /* config space saved at suspend time */ struct hlist_head saved_cap_space; struct bin_attribute *rom_attr; /* attribute descriptor for sysfs ROM entry */ int rom_attr_enabled; /* has display of the rom attribute been enabled? */ struct bin_attribute *res_attr[DEVICE_COUNT_RESOURCE]; /* sysfs file for resources */ struct bin_attribute *res_attr_wc[DEVICE_COUNT_RESOURCE]; /* sysfs file for WC mapping of resources */ #ifdef CONFIG_PCI_MSI struct list_head msi_list; #endif struct pci_vpd *vpd; #ifdef CONFIG_PCI_IOV union { struct pci_sriov *sriov; /* SR-IOV capability related */ struct pci_dev *physfn; /* the PF this VF is associated with */ }; struct pci_ats *ats; /* Address Translation Service */ #endif };
4. 驱动程序函数
PCI层中最后一个基本的数据结构是pci_driver,它用于实现PCI驱动程序,表示了通用内核代码和设备的底层硬件驱动程序之间的接口,每个PCI驱动程序都必须将其函数填到该接口中,使得内核能够一致地控制可用的驱动程序,这相当于接口规范
struct pci_driver { struct list_head node; //name代表设备的文本标识符(通常是实现驱动程序的模块名称) char *name; const struct pci_device_id *id_table; /* must be non-NULL for probe to be called */ int (*probe) (struct pci_dev *dev, const struct pci_device_id *id); /* New device inserted */ void (*remove) (struct pci_dev *dev); /* Device removed (NULL if not a hot-plug capable driver) */ int (*suspend) (struct pci_dev *dev, pm_message_t state); /* Device suspended */ int (*suspend_late) (struct pci_dev *dev, pm_message_t state); int (*resume_early) (struct pci_dev *dev); int (*resume) (struct pci_dev *dev); /* Device woken up */ void (*shutdown) (struct pci_dev *dev); struct pci_error_handlers *err_handler; //driver用于建立与通用设备模型的关联 struct device_driver driver; struct pci_dynids dynids; };
PCI驱动程序结构最重要的方面是对检测、安装、移除设备的支持
5. 注册驱动程序
PCI驱动程序可以通过pci_register_driver注册,该函数的主要任务是对相关函数已经分配的一个pci_device实例,填充一些剩余的字段
0x3: USB
USB(universal serial bus 通用串行总线)是一种外部总线,用于不断发展的PC需求,在设计该总线时,尤其要注意易用性,例如热插拔、驱动程序的透明安装是USB设计的核心,USB是将内核的热插拔能力提供给大量用户的第一种总线
1. 特性和运行模式
2. 驱动程序的管理
Copyright (c) 2015 LittleHann All rights reserved