首页资源分类嵌入式开发嵌入式系统 > linux设备驱动程序

linux设备驱动程序

已有 456470个资源

下载专区

上传者其他资源

文档信息举报收藏

标    签: linux

分    享:

文档简介

介绍linux的驱动程序

文档预览

Linux 设备驱动程序 (第二版) ii 内容提要 本书面向的读者,是那些想在 Linux 操作系统下支持各种计算机外设,或者想开发新的硬件并在 Linux 下运行的人们。Linux 是 Unix 市场中增长最为快速的部分,并且在许多应用领域获得了广 泛而热情的支持。现在,人们越来越清楚地认识到 Linux 是嵌入式系统的一个极好平台。《Linux 设备驱动程序》已经成为该领域的一流著作,此书将以往那些口述式的经验和知识,或者隐晦的源 代码注释变成了系统地讲述各种设备驱动程序编写方法的著作。 Linux 内核的 2.4 版在设备驱动程序方面发生了重大变化,它简化了许多工作,但同时提供了许多 新的功能,可让驱动程序更加有效而且灵活。本书第二版彻底讲述了这些变化,并介绍了许多新的 处理器和总线结构。 要阅读此书,并不要求读者成为一名内核黑客;我们仅仅希望读者理解 C 语言并熟悉 Unix 系统 调用。我们循序渐进地讲述了字符设备、块设备和网络设备的驱动程序,并且给出了功能完善的示 例驱动程序。这些示例驱动程序说明了驱动程序设计中的许多问题以及解决方法,并且不需要任何 特定的硬件就可以运行。本书第二版的重要修订包括:对对称多处理器(SMP)系统和锁机制的讨 论、对新 CPU 的支持以及新近支持的总线的讨论等等。 如果读者对操作系统完成其任务的方式感兴趣,本书则提供了对地址空间、异步事件和 I/O 的深 入讨论。 可移植性是本书的一个主要关注点。尽管本书主要讲述 2.4 版本,但只要可能,我们也会讲述向 后直到 2.0 版本的相关内容。《Linux 设备驱动程序》也讲述了如何在各种硬件平台上实现最大的 可移植性;示例驱动程序已经在 IA32(PC)和 IA64、PowerPC、SPARC 和 SPARC64、Alpha、 ARM 以及 MIPS 等平台上经过了测试。 iii 作者简介 Alessandro Rubini 在他获得电子工程师职称后不久,就安装了 Linux 0.99.14 版本。后来,他 在 Pavia 大学获得了计算机科学博士学位。但很快他就离开了大学,因为他实在不想写很多的论 文。现在,他是一名自由撰稿人,编写和设备驱动程序相关的文章和论文(很有讽刺意味)。在他 的小孩出世之前,他曾是一名年轻的黑客;而现在则是一名年老的、偏爱非 PC 计算机平台开发 的自由软件鼓吹者。 Jonathan Corbet 早在 1981 年就接触了 BSD Unix 的源代码。那时,科罗拉多大学的一名教 员让他“修正”其中的分页算法。从那时起直到现在,他深入研究了他所遇到的每一个系统,其中 包括 VAX、Sun、Ardent 以及 x86 系统的驱动程序。他在 1993 年第一次接触 Linux 系统,从 此以后一直从事 Linux 的开发。Corbet 先生是 Linux Weekly News (http://LWN.net) 的奠基人和执 行主编;他和妻子及两个孩子生活在科罗拉多州巨石市。 iv 目录 目录 前 言 ....................................................... i Alessandro 的介绍 ............................................................................................................i Jon 的介绍........................................................................................................................ii 本书面向的读者 ...............................................................................................................ii 内容的组织 ......................................................................................................................iii 背景信息 ..........................................................................................................................iii 其它信息来源 ..................................................................................................................iv 在线版本和条款 ............................................................................................................... v 本书使用的约定 ............................................................................................................... v 我们希望得到来自读者的反馈 ......................................................................................vi 致谢 ..................................................................................................................................vi 第 1 章 设备驱动程序简介 .................................... 1 1.1 设备驱动程序的作用 .............................................................................................. 2 1.2 内核功能划分 .......................................................................................................... 3 进程管理...................................................................................................................................................... 3 内存管理...................................................................................................................................................... 4 文件系统...................................................................................................................................................... 4 设备控制...................................................................................................................................................... 4 网络功能...................................................................................................................................................... 4 1.3 设备和模块分类 ...................................................................................................... 5 字符设备...................................................................................................................................................... 5 块设备.......................................................................................................................................................... 5 网络接口...................................................................................................................................................... 5 1.4 安全问题 .................................................................................................................. 6 1.5 版本编号 .................................................................................................................. 7 1.6 许可证条款 .............................................................................................................. 8 1.7 加入内核开发社团 .................................................................................................. 9 1.8 本书概要 .................................................................................................................. 9 第 2 章 构造和运行模块 ..................................... 11 2.1 核心模块与应用程序的对比 ................................................................................ 12 2.1.1 用户空间和内核空间..................................................................................................... 14 2.1.2 内核中的并发................................................................................................................. 15 2.1.3 当前进程......................................................................................................................... 15 2.2 编译和装载 ............................................................................................................ 16 2.2.1 版本依赖......................................................................................................................... 18 2.2.2 平台依赖......................................................................................................................... 19 2.3 内核符号表 ............................................................................................................ 20 2.4 初始化和关闭 ........................................................................................................ 22 2.4.1 init_module 中的出错处理 ............................................................................................ 22 i 目录 2.4.2 使用计数......................................................................................................................... 24 2.4.3 卸载 ................................................................................................................................ 25 2.4.4 显式的初始化和清除函数............................................................................................. 25 2.5 使用资源 ................................................................................................................ 26 2.5.1 I/O 端口和 I/O 内存 .................................................................................................... 27 端口............................................................................................................................................................ 27 内存............................................................................................................................................................ 29 2.5.2 Linux 2.4 中的资源分配 ................................................................................................ 30 2.6 自动和手动配置 .................................................................................................... 31 2.7 在用户空间编写驱动程序 .................................................................................... 33 2.8 向后兼容性 ............................................................................................................ 35 2.8.1 资源管理的改变............................................................................................................. 35 2.8.2 多处理器系统上的编译................................................................................................. 36 2.8.3 在 Linux 2.0 中导出符号 .............................................................................................. 36 2.8.4 模块配置参数................................................................................................................. 37 2.9 快速参考 ................................................................................................................ 37 第 3 章 字符设备驱动程序 ................................... 40 3.1 scull 的设计............................................................................................................ 40 3.2 主设备号和次设备号 ............................................................................................ 41 3.2.1 动态分配主设备号......................................................................................................... 43 3.2.2 从系统中删除设备驱动程序........................................................................................... 45 3.2.3 dev_t 和 kdev_t............................................................................................................. 46 3.3 文件操作 ................................................................................................................ 47 3.3.1 file 结构 ......................................................................................................................... 50 3.3.2 open 和 release.............................................................................................................. 51 open 方法 .................................................................................................................................................. 51 release 方法............................................................................................................................................... 54 3.4 scull 的内存使用................................................................................................... 54 3.5 竞态的简介 ............................................................................................................ 57 3.6 read 和 write ......................................................................................................... 58 3.6.1 read 方法........................................................................................................................ 60 3.6.2 write 方法 ...................................................................................................................... 62 3.6.3 readv 和 writev ............................................................................................................. 63 3.7 试试新设备 ............................................................................................................ 63 3.8 设备文件系统 ........................................................................................................ 64 3.8.1 实际使用 devfs .............................................................................................................. 66 3.8.2 可移植性问题和 devfs ................................................................................................... 68 3.9 向后兼容性 ............................................................................................................ 68 3.9.1 文件操作数据结构的变化............................................................................................. 69 3.9.2 模块使用计数................................................................................................................. 71 3.9.3 信号量支持的变化......................................................................................................... 71 3.9.4 用户空间访问的变化..................................................................................................... 71 3.10 快速索引 .............................................................................................................. 72 ii 目录 第 4 章 调试技术 ........................................... 74 4.1 通过打印调试 ........................................................................................................ 74 4.1.1 printk ............................................................................................................................... 74 4.1.2 消息如何被记录............................................................................................................. 76 4.1.3 开启及关闭消息............................................................................................................. 77 4.2 通过查询调试 ........................................................................................................ 79 4.2.1 使用 /proc 文件系统 .................................................................................................... 79 4.2.2 ioctl 方法 ....................................................................................................................... 83 4.3 通过监视调试 ........................................................................................................ 83 4.4 调试系统故障 ........................................................................................................ 85 4.4.1 oops 消息 ........................................................................................................................ 85 使用 klogd................................................................................................................................................. 87 使用 ksymoops.......................................................................................................................................... 88 4.4.2 系统挂起......................................................................................................................... 91 4.5 调试器和相关工具 ................................................................................................ 93 4.5.1 使用 gdb......................................................................................................................... 93 4.5.2 kdb 内核调试器............................................................................................................. 94 4.5.3 集成的内核调试器补丁................................................................................................. 96 4.5.4 kgdb 补丁....................................................................................................................... 97 4.5.5 内核崩溃转储分析器..................................................................................................... 97 4.5.6 用户模式的 Linux 虚拟机 ........................................................................................... 97 4.5.7 Linux 跟踪工具包 ......................................................................................................... 98 4.5.8 Dynamic Probes .............................................................................................................. 98 第 5 章 增强的字符驱动程序操作 ............................. 99 5.1 ioctl ......................................................................................................................... 99 5.1.1 选择 ioctl 命令 ........................................................................................................... 100 类型(type) ........................................................................................................................................... 101 号码(number)...................................................................................................................................... 101 方向(direction).................................................................................................................................... 101 尺寸(size)............................................................................................................................................ 101 5.1.2 返回值........................................................................................................................... 103 5.1.3 预定义命令................................................................................................................... 103 5.1.4 使用 ioctl 参数 ........................................................................................................... 104 5.1.5 权能与受限操作........................................................................................................... 106 5.1.6 ioctl 命令的实现 .......................................................................................................... 107 5.1.7 非 ioctl 的设备控制.................................................................................................... 108 5.2 阻塞型 I/O............................................................................................................ 109 5.2.1 睡眠和唤醒................................................................................................................... 109 5.2.2 等待队列的深入分析....................................................................................................111 5.2.3 编写可重入代码........................................................................................................... 113 5.2.4 阻塞和非阻塞型操作................................................................................................... 114 5.2.5 一个样例实现:scullpipe ............................................................................................ 115 iii 目录 5.3 poll 和 select....................................................................................................... 118 5.3.1 与 read 和 write 的交互............................................................................................ 121 从设备读取数据 ...................................................................................................................................... 121 向设备写数据.......................................................................................................................................... 121 刷新待处理输出 ...................................................................................................................................... 121 5.3.2 底层的数据结构........................................................................................................... 122 5.4 异步通知 .............................................................................................................. 123 5.4.1 从驱动程序的角度看................................................................................................... 124 5.5 定位设备 .............................................................................................................. 125 5.5.1 llseek 实现 ................................................................................................................... 125 5.6 设备文件的访问控制 .......................................................................................... 126 5.6.1 独享设备....................................................................................................................... 127 5.6.2 关于竞态的问题........................................................................................................... 128 5.6.3 限制每次只由一个用户访问....................................................................................... 129 5.6.4 替代 EBUSY 的阻塞型 open .................................................................................... 129 5.6.5 在打开时复制设备....................................................................................................... 130 5.7 向后兼容性 .......................................................................................................... 132 5.7.1 Linux 2.2 和 2.0 中的等待队列 ................................................................................ 132 5.7.2 异步通知....................................................................................................................... 133 5.7.3 fsync 方法.................................................................................................................... 133 5.7.4 在 Linux 2.0 中访问用户空间 ................................................................................... 134 5.7.5 2.0 中的权能................................................................................................................ 135 5.7.6 Linux 2.0 的 select 方法 ............................................................................................ 135 5.7.7 Linux 2.0 的设备定位 ................................................................................................. 136 5.7.8 2.0 和 SMP.................................................................................................................. 136 5.8 快速参考 .............................................................................................................. 136 第 6 章 时间流 ............................................ 140 6.1 内核中的时间间隔 .............................................................................................. 140 6.1.1 处理器特有的寄存器................................................................................................... 141 6.2 获取当前时间 ...................................................................................................... 142 6.3 延迟执行 .............................................................................................................. 143 6.3.1 长延迟........................................................................................................................... 144 6.3.2 短延迟........................................................................................................................... 145 6.4 任务队列 .............................................................................................................. 146 6.4.1 任务队列的本质........................................................................................................... 147 6.4.2 任务队列的运行........................................................................................................... 148 6.4.3 预定义的任务队列....................................................................................................... 149 示例程序是如何工作的 .......................................................................................................................... 150 调度器队列.............................................................................................................................................. 151 定时器队列.............................................................................................................................................. 152 立即队列.................................................................................................................................................. 153 6.4.4 运行自己的工作队列................................................................................................... 153 6.4.5 Tasklets ......................................................................................................................... 154 iv 目录 6.5 内核定时器 .......................................................................................................... 155 6.6 向后兼容性 .......................................................................................................... 158 6.7 快速参考 .............................................................................................................. 159 第 7 章 获取内存 .......................................... 162 7.1 kmalloc 函数的内幕 ........................................................................................... 162 7.1.1 flags 参数..................................................................................................................... 162 内存区段.................................................................................................................................................. 163 7.1.2 size 参数 ...................................................................................................................... 164 7.2 后备式高速缓存 .................................................................................................. 164 7.2.1 基于高速缓存的 scull:scullc..................................................................................... 166 7.3 get_free_page 和相关函数.................................................................................. 167 7.3.1 使用一整页的 scull: scullp .......................................................................................... 168 7.4 vmalloc 与相关函数 ........................................................................................... 169 7.4.1 使用虚拟地址的 scull: scullv...................................................................................... 171 7.5 引导时的内存分配 .............................................................................................. 172 7.5.1 在引导时获得专用缓冲区........................................................................................... 172 7.5.2 bigphysarea 补丁 ......................................................................................................... 173 7.5.3 保留高端 RAM 地址.................................................................................................. 173 7.6 向后兼容性 .......................................................................................................... 173 7.7 快速参考 .............................................................................................................. 174 第 8 章 硬件管理 .......................................... 176 8.1 I/O 端口和 I/O 内存 ......................................................................................... 176 8.1.1 I/O 寄存器和常规内存 ............................................................................................... 177 8.2 使用 I/O 端口..................................................................................................... 178 8.2.1 串操作........................................................................................................................... 180 8.2.2 暂停式 I/O ................................................................................................................... 180 8.2.3 平台相关性................................................................................................................... 181 8.3 使用数字 I/O 端口............................................................................................. 182 8.3.1 并口简介....................................................................................................................... 182 8.3.2 示例驱动程序............................................................................................................... 183 8.4 使用 I/O 内存..................................................................................................... 185 8.4.1 直接映射的内存........................................................................................................... 186 8.4.2 在 short 中使用 I/O 内存 ......................................................................................... 187 8.4.3 通过软件映射的 I/O 内存 ......................................................................................... 187 8.4.4 1M 地址空间之下的 ISA 内存.................................................................................... 189 8.4.5 isa_readb 及相关函数 ................................................................................................. 190 8.4.6 探测 ISA 内存 ............................................................................................................ 190 8.5 向后兼容性 .......................................................................................................... 192 8.6 快速参考 .............................................................................................................. 193 第 9 章 中断处理 .......................................... 195 9.1 中断的整体控制 .................................................................................................. 195 v 目录 9.2 准备并口 .............................................................................................................. 196 9.3 安装中断处理程序 .............................................................................................. 197 9.3.1 /proc 接口 ..................................................................................................................... 199 9.3.2 自动检测 IRQ 号.......................................................................................................... 200 内核帮助下的探测 .................................................................................................................................. 201 DIY 探测.................................................................................................................................................. 202 9.3.3 快速和慢速处理程序................................................................................................... 203 x86 平台上中断处理的内幕 ................................................................................................................... 204 9.4 实现中断处理程序 .............................................................................................. 204 9.4.1 使用参数....................................................................................................................... 207 9.4.2 打开和禁止中断........................................................................................................... 207 9.5 tasklet 和底半部处理.......................................................................................... 208 9.5.1 tasklet ............................................................................................................................ 209 9.5.2 BH 机制 ........................................................................................................................ 210 9.5.3 编写 BH 底半部 ........................................................................................................... 211 9.6 中断共享 .............................................................................................................. 212 9.6.1 安装共享的处理程序................................................................................................... 213 9.6.2 运行处理程序............................................................................................................... 214 9.6.3 /proc 接口 ..................................................................................................................... 215 9.7 中断驱动的 I/O.................................................................................................... 215 9.8 竞态 ...................................................................................................................... 216 9.8.1 使用循环缓冲区........................................................................................................... 217 9.8.2 使用自旋锁................................................................................................................... 218 9.8.3 使用锁变量................................................................................................................... 220 位操作...................................................................................................................................................... 220 原子性的整数操作 .................................................................................................................................. 221 9.8.4 无竞争地进入睡眠....................................................................................................... 222 9.9 向后兼容性 .......................................................................................................... 223 9.9.1 与 2.2 内核的区别........................................................................................................ 223 9.9.2 与 2.0 内核的更多区别................................................................................................ 224 9.10 快速参考 ............................................................................................................ 224 第 10 章 合理使用数据类型 ................................. 227 10.1 使用标准 C 语言类型 ..................................................................................... 227 10.2 为数据项分配确定的空间大小 ........................................................................ 228 10.3 接口特有的类型 ................................................................................................ 229 10.4 其它有关移植性的问题 .................................................................................... 230 10.4.1 时间间隔..................................................................................................................... 230 10.4.2 页大小......................................................................................................................... 230 10.4.3 字节序......................................................................................................................... 231 10.4.4 数据对齐..................................................................................................................... 232 10.5 链表 .................................................................................................................... 233 10.6 快速索引 ............................................................................................................ 235 第 11 章 kmod 和高级模块化................................ 237 vi 目录 11.1 按需加载模块 .................................................................................................... 237 11.1.1 在内核中请求模块..................................................................................................... 238 11.1.2 用户空间方面............................................................................................................. 238 11.1.3 模块加载和安全性..................................................................................................... 240 11.1.4 模块加载实例............................................................................................................. 240 11.1.5 运行用户态辅助程序................................................................................................. 241 11.2 模块间通讯 ........................................................................................................ 242 11.3 模块中的版本控制 ............................................................................................ 244 11.3.1 在模块中使用版本支持............................................................................................. 245 11.3.2 导出版本化符号......................................................................................................... 246 11.4 向后兼容性 ........................................................................................................ 247 11.5 快速索引 ............................................................................................................ 248 第 12 章 装载块设备驱动程序 ............................... 250 12.1 注册驱动程序 .................................................................................................... 250 12.2 头文件 blk.h ...................................................................................................... 256 12.3 请求处理简介 .................................................................................................... 257 12.3.1 请求队列..................................................................................................................... 257 12.3.2 执行实际的数据传输................................................................................................. 258 12.4 请求处理详解 .................................................................................................... 261 12.4.1 I/O 请求队列 ............................................................................................................. 261 request 结构和缓冲区缓存..................................................................................................................... 261 操作请求队列.......................................................................................................................................... 263 I/O 请求锁 .............................................................................................................................................. 263 blk.h 中的宏和函数是如何工作的 ........................................................................................................ 264 12.4.2 集群请求..................................................................................................................... 265 活动的队列头.......................................................................................................................................... 266 12.4.2 多队列的块驱动程序................................................................................................. 266 12.4.4 没有请求队列的情况................................................................................................. 269 12.5 挂装和卸载是如何工作的 ................................................................................ 271 12.6 ioctl 方法 ........................................................................................................... 272 12.7 可移动设备 ........................................................................................................ 274 12.7.1 revalidation ................................................................................................................. 275 12.7.2 需要特别注意的事项................................................................................................. 275 12.8 可分区设备 ........................................................................................................ 276 12.8.1 一般性硬盘................................................................................................................. 277 12.8.2 分区检测..................................................................................................................... 278 12.8.3 使用 initrd 完成分区检测 ........................................................................................ 280 12.8.4 spull 的设备方法....................................................................................................... 280 12.9 中断驱动的块驱动程序 .................................................................................... 282 12.10 向后兼容性 ...................................................................................................... 283 12.11 快速参考 .......................................................................................................... 285 第 13 章 mmap 和 DMA .................................... 288 vii 目录 13.1 Linux 的内存管理............................................................................................. 288 13.1.1 地址类型..................................................................................................................... 288 用户虚拟地址.......................................................................................................................................... 289 物理地址.................................................................................................................................................. 289 总线地址.................................................................................................................................................. 289 内核逻辑地址.......................................................................................................................................... 289 内核虚拟地址.......................................................................................................................................... 289 13.1.2 高端与低端内存......................................................................................................... 290 低端内存.................................................................................................................................................. 290 高端内存.................................................................................................................................................. 290 13.1.3 内存映射和页结构..................................................................................................... 290 13.1.4 页表............................................................................................................................. 292 页目录(PGD) ...................................................................................................................................... 293 中级页目录(PMD) ............................................................................................................................. 293 页表.......................................................................................................................................................... 293 13.1.5 虚拟内存区域............................................................................................................. 294 13.2 mmap 设备操作 ................................................................................................. 297 13.2.1 使用 remap_page_range ............................................................................................. 299 13.2.2 一个简单的实现......................................................................................................... 299 13.2.3 增加 VMA 操作 ......................................................................................................... 300 13.2.4 使用 nopage 映射内存 ............................................................................................. 301 13.2.5 重映射特定的 I/O 区域 ........................................................................................... 303 13.2.6 重映射 RAM ............................................................................................................. 303 使用 nopage 方法重映射 RAM............................................................................................................... 304 13.2.7 重映射虚拟地址......................................................................................................... 307 13.3 kiobuf 接口........................................................................................................ 308 13.3.1 kiobuf 结构 ................................................................................................................ 308 13.3.2 映射用户空间缓冲区以及裸 I/O .............................................................................. 309 13.4 直接内存访问和总线控制 ................................................................................ 312 13.4.1 DMA 数据传输概览.................................................................................................. 312 13.4.2 分配 DMA 缓冲区.................................................................................................... 313 DIY 分配................................................................................................................................................. 313 13.4.3 总线地址..................................................................................................................... 314 13.4.4 PCI 总线上的 DMA ................................................................................................. 315 处理不同硬件.......................................................................................................................................... 315 DMA 映射............................................................................................................................................... 315 建立一致 DMA 映射 ............................................................................................................................. 316 建立流式 DMA 映射 ............................................................................................................................. 317 分散/集中映射......................................................................................................................................... 318 支持 PCI DMA 的不同体系结构 .......................................................................................................... 319 一个简单的 PCI DMA 例子.................................................................................................................. 320 简单看看 Sbus 上的情况 ...................................................................................................................... 321 13.4.5 ISA 设备的 DMA ..................................................................................................... 321 注册 DMA 的方法................................................................................................................................. 322 viii 目录 与 DMA 控制器通讯 ............................................................................................................................. 323 13.5 向后兼容性 ........................................................................................................ 325 13.5.1 内存管理部分的改变................................................................................................. 325 13.5.2 DMA 的变化 ............................................................................................................. 327 13.6 快速参考 ............................................................................................................ 328 第 14 章 网络驱动程序 ..................................... 331 14.1 snull 的设计 ...................................................................................................... 332 14.1.1 赋于 IP 号 ................................................................................................................. 332 14.1.2 数据包的物理传输..................................................................................................... 334 14.2 连接到内核 ........................................................................................................ 335 14.2.1 模块的装载................................................................................................................. 335 14.2.2 初始化每个设备......................................................................................................... 336 14.2.3 模块的卸载................................................................................................................. 337 14.2.4 模块化和非模块化的驱动程序................................................................................. 338 14.3 net_device 结构的细节..................................................................................... 338 14.3.1 可见的成员................................................................................................................. 339 14.3.2 隐藏的成员................................................................................................................. 339 接口信息.................................................................................................................................................. 340 设备方法.................................................................................................................................................. 342 工具成员.................................................................................................................................................. 344 14.4 打开和关闭 ........................................................................................................ 344 14.5 数据包传输 ........................................................................................................ 346 14.5.1 控制并发传输............................................................................................................. 347 14.5.2 传输超时..................................................................................................................... 347 14.6 数据包的接收 .................................................................................................... 348 14.7 中断处理程序 .................................................................................................... 350 14.8 链路状态的改变 ................................................................................................ 351 14.9 套接字缓冲区 .................................................................................................... 351 14.9.1 重要成员..................................................................................................................... 352 14.9.2 操作套接字缓冲区的函数......................................................................................... 353 14.10 MAC 地址解析 ............................................................................................... 354 14.10.1 在以太网中使用 ARP ............................................................................................. 354 14.10.2 重载 ARP................................................................................................................. 354 14.10.3 非以太网头............................................................................................................... 355 14.11 定制 ioctl 命令 ............................................................................................... 356 14.12 统计信息 .......................................................................................................... 357 14.13 组播 .................................................................................................................. 358 14.13.1 对组播的内核支持................................................................................................... 358 14.13.2 一个典型实现........................................................................................................... 359 14.14 向后兼容性 ...................................................................................................... 360 14.14.1 Linux 2.2 中的不同 ................................................................................................. 360 14.14.2 Linux 2.0 中其它不同 ............................................................................................. 361 14.14.3 探测和 HAVE_DEVLIST ....................................................................................... 362 ix 目录 14.15 快速参考 .......................................................................................................... 362 第 15 章 外设总线综述 ..................................... 365 15.1 PCI 接口 ............................................................................................................ 365 15.1.1 PCI 寻址 .................................................................................................................... 366 15.1.2 引导阶段..................................................................................................................... 368 15.1.3 配置寄存器和初始化................................................................................................. 368 15.1.4 访问配置空间............................................................................................................. 372 配置空间示例.......................................................................................................................................... 373 15.1.5 访问 I/O 和内存空间 ............................................................................................... 374 Linux 2.4 中的 PCI I/O 资源 ................................................................................................................ 375 基地址寄存器.......................................................................................................................................... 376 15.1.6 PCI 中断 .................................................................................................................... 378 15.1.7 处理热插拔设备......................................................................................................... 379 pci_driver 结构 ....................................................................................................................................... 380 15.1.8 硬件抽象..................................................................................................................... 382 15.2 回顾 ISA............................................................................................................ 382 15.2.1 硬件资源..................................................................................................................... 383 15.2.2 ISA 编程 .................................................................................................................... 383 15.2.3 即插即用规范............................................................................................................. 384 15.3 PC/104 和 PC/104+.......................................................................................... 384 15.4 其它 PC 总线 ................................................................................................... 384 15.4.1 MCA ........................................................................................................................... 385 15.4.2 EISA............................................................................................................................ 385 15.4.3 VLB............................................................................................................................. 385 15.5 SBus.................................................................................................................... 386 15.6 NuBus ................................................................................................................. 386 15.7 外部总线 ............................................................................................................ 386 15.7.1 USB............................................................................................................................. 387 15.7.2 编写 USB 驱动程序................................................................................................. 387 15.8 向后兼容性 ........................................................................................................ 389 15.9 快速参考 ............................................................................................................ 389 第 16 章 内核源代码的物理布局 ............................. 391 16.1 引导内核 ............................................................................................................ 391 16.2 引导之前 ............................................................................................................ 393 16.3 init 进程............................................................................................................. 395 16.4 kernel 目录 ........................................................................................................ 395 16.5 fs 目录 ............................................................................................................... 396 16.6 mm 目录 ............................................................................................................ 397 16.7 net 目录 ............................................................................................................. 398 16.8 ipc 和 lib ........................................................................................................... 399 16.9 include 和 arch 目录 ....................................................................................... 399 16.10 drivers 目录 ..................................................................................................... 400 x 目录 16.10.1 drivers/char ............................................................................................................... 400 16.10.2 drivers/block ............................................................................................................. 400 16.10.3 drivers/ide ................................................................................................................. 401 16.10.4 drivers/md ................................................................................................................. 401 16.10.5 drivers/cdrom ............................................................................................................ 401 16.10.6 drivers/scsi ................................................................................................................ 402 16.10.7 drivers/net ................................................................................................................. 402 16.10.8 drivers/sound............................................................................................................. 403 16.10.9 drivers/video ............................................................................................................. 403 16.10.10 drivers/input ............................................................................................................ 404 16.10.11 drivers/media........................................................................................................... 404 16.10.12 总线相关目录......................................................................................................... 404 16.10.13 平台相关目录......................................................................................................... 405 16.10.14 其它子目录............................................................................................................. 405 附录 A 参考书目 ........................................... 407 A.1 Linux 内核书籍 ............................................................................................................. 407 A.2 Unix 设计和内幕........................................................................................................... 407 附录 B 封面故事 ........................................... 409 xi 前言 Linux 设备驱动程序 顾名思义,本书是讲述 Linux 设备驱动程序编写的。面对层出不穷的新硬件产品,必须有人不断 编写新的驱动程序,以便让这些设备能够在 Linux 下正常工作,从这个意义上讲,讲述驱动程序 的编写,本身就是一件非常有意义的工作。但本书也涉及到 Linux 内核的工作原理,同时还讲述 如何根据自己的需要和兴趣来定制 Linux 内核。Linux 是一个开放的系统,我们希望借助本书, 它能够更加开放,从而能够吸引更多的开发人员。 自本书第一版问世以来,Linux 本身的变化非常巨大。现在的 Linux 能够在更多的处理器上运行, 并且支持更加广泛的硬件,许多内部的编程接口也相应发生了重大变化,因此,我们决定编写本书 的第二个版本。这一版本以 Linux 2.4 版本的内核为主,讲述了新内核提供的所有新特色,同时, 仍然兼顾了早期的内核版本。 我们希望读者能够从本书的学习当中获得乐趣,就像我们自己从编写本书的过程中获得乐趣一样。 Alessandro 的介绍 作为一个喜欢 DIY 的电子工程师,我一直乐于使用计算机来控制一些外部的硬件设备。从我小时 候使用父亲的 Apple IIe 计算机开始,我就开始寻找另外一个平台,以便能够将我自制的电路板连 接其上,并能够编写自己的驱动程序。不幸的是,不管是从硬件级别,还是从软件级别看,80 年 代 PC 的功能都不是非常强大:PC 的内部设计比起 Apple II 来简直是差远了,而且可供利用的 文档也远远不能令人满意。但在 Linux 出现之后,我决定尝试利用这个新的操作系统,为此,我 购买了一个昂贵的 386 主板,但没有购买任何受到所有权保护的软件。 那时,我在大学里使用 Unix 系统,这个设计精巧的系统令我激动不已,尤其在有了由 GNU 项 目提供给用户使用的更加精巧的工具之后,这个系统更加令我着迷。对我来讲,在我自己的 PC 主 板上使用 Linux 内核,一直是最为难忘的经历,我不仅可以编写自己的设备驱动程序,而且还有 了机会再次拿起电烙铁。我不停地告诉别人,“我长大之后,一定要成为一名黑客,”而 GNU/Linux 则是实现这一梦想的最佳平台。可是,我不知道我是否真正长大。 随着 Linux 的成熟,越来越多的人开始乐于为自制的电子设备或者商用设备编写驱动程序。就像 Linus Torvalds 所说的那样,“我们又回到了为自己编写设备驱动程序的‘远古’时代。” i 前言 1996 年的时候,我经常为那些从别人那里借来的,或者别人给我的,或者是自己在家里制作的硬 件设备编写自己的设备驱动程序,并且乐此不疲。那时,我已经为 Michael Johnson 所著的《内 核黑客指南》贡献了一些内容,并开始为《Linux 杂志》编写内核相关的文章。有了 Michael 的 帮助,我认识了在 O'Reilly 工作的 Andy Oram,他希望我能就设备驱动程序编写一本书,我非常 乐意地接受了这一工作,有很长一段时间我一直忙于编写这本书。 到了 1999 年,我发现,我已经没有足够的精力来独自完成本书的更新了:我的家庭在长大,而 更多的时间要花费在编写 GPL 软件的工作上。除此之外,内核也变得更大,而且可以支持比已往 更多的平台,而 API 也变得更加复杂和成熟。这时,Jonathan 开始帮助我更新本书。他拥有足 够的技巧、能力和热情,而我则继续负责已经拉下很多的进度跟踪。他利用自己良好的技能和热情, 已经成为推进这个进程的最有力助手,这些却是我无法达到的。我非常高兴能够和他共事,不管在 技术上还是在私人方面。 Jon 的介绍 我从 1994 初开始接触 Linux,那时,我正在说服自己的老伴为我购买一台 Fintronic Systems 公 司生产的笔记本电脑。作为 80 年代初期(那时起我就在和源代码打交道)的一名 Unix 用户,我 立即被 Linux 所吸引。恰好在 1994 年,Linux 已经成为一个非常实用的系统,而且也是我所遇 到的第一个真正自由的系统。那时,我几乎完全丧失了对所有权系统的兴趣。 但我并没有一个完整的计划想为 Linux 编写什么著作。当 O'Reilly 和我讨论有关帮助编写本书第 二版事宜的时候,我刚刚从我工作了 18 年的公司辞职,并成立了一个 Linux 顾问咨询公司。为 了吸引别人的注意力,我们建立了一个 Linux 新闻站点,即 Linux Weekly News(http://lwn.net), 该站点的内容主要集中于内核开发。随着 Linux 的大众化,该 web 站点也变得非常知名,而我 们的咨询业务却最终被人遗忘。 然而,我的第一兴趣却始终是系统编程。早些时候,我“修正”最初 BSD Unix 系统当中的分页 代码(这是一个可怕的黑客工作),或者在 VAX/VMS 系统上编写磁带驱动器的驱动程序(这些源 代码是可获得的,如果你不在意这些由汇编和 BLISS 语言编写的代码的话)。随着时间的推移, 我又为 Alliant、Ardent 和 Sun 等系统编写驱动程序。后来,我开始利用 Linux 开发雷达数据收 集系统,这个时候,也就是编写本书的时候,也正是修正 Linux 软盘驱动程序中 I/O 请求队列锁 的实现的时候。 我为能参与本书的编写而感到高兴。首先,通过本书的编写,我能够更加深入地研究内核代码,同 时能够帮助别人达到同样的目的。Linux 是个实用的系统,而同时也是一个带给人乐趣的系统,而 围绕内核工作,则是所有事情当中最令人兴奋和激动的事情之一。和 Alessandro 一起工作也令人 高兴,我必须感谢他信任我对他优秀的文子所作的修修改改,也感谢他在我出现错误或者不能赶上 进度时的耐心,当然也得感谢那次到 Pavia 的破单车旅行。编写本书的那些时光的确难忘! 本书面向的读者 在技术方面,本书提供了一条理解内核内幕以及理解一些 Linux 开发者所做出的设计决策的行家 途径。尽管本书的主要目的是教读者如何编写设备驱动程序,但同时也给出了内核实现方面的概览。 ii Linux 设备驱动程序 尽管真正的黑客能够从正式的内核源代码中找到所有必要的信息,但通常来讲,编写好的书籍能够 更好地帮助读者提高编程技巧。读者将要看到的文子,来自对内核源代码的仔细分析,我们希望我 们所付出的努力是值得的。 本书对那些希望编写计算机设备驱动程序的人员,或者那些要解决 Linux 机器内部问题的程序员 来讲,将是非常有帮助的。请注意,“Linux 机器”是一个比“运行 Linux 的 PC”更为宽泛的概 念,因为 Linux 现在能够支持许多不同的硬件平台,而内核编程不再绑定到某个特定的平台。我 们希望本书能够成为那些想成为内核黑客,但却不知如何下手的人们的良好起点。 Linux 狂热者将从本书找到深入内核代码的足够精神食粮;通过本书的学习,将有能力加入到为某 个新功能或性能增强不停工作的开发小组当中。本书并没有涵盖 Linux 内核的全部,但是,作为 Linux 设备驱动程序开发人员,你需要了解如何和许多的内核子系统一起工作。因此,本书对内核 编程作了一个一般性的介绍。Linux 仍然在不断改进和发展,因此,新程序员始终有机会加入到这 一 Linux 的开发大军中。 另一方面,如果你只是为了为自己的设备编写一个驱动程序,而不想过多了解内核的内幕信息的话, 本书内容则足够模块化以满足你的需求。如果你不想深入到细节当中,则可以简单跳过大部分的技 术章节,而直接阅读可由设备驱动程序使用的、能够和内核的其它部分无缝结合的标准 API。 本书的主要讲述对象是如何为 Linux 内核 2.4 版本编写内核模块。模块是能够在运行时装载到内 核的目标代码,它能够为正在运行的内核添加新的功能。我们尽其可能地让示例代码也能够在内核 的 2.2 和 2.0 版本中运行,如果需要有所改动则会指出。 内容的组织 本书内容由简到难,并划分为两大部分。第一部分(第 1 章到第 10 章)首先讲述了如何编写内 核模块,然后讲述了编写功能完备的字符设备驱动程序所涉及的各个编程主题。每一章分别讲述某 一个特定问题,并在每章结尾包含一个“符号表”,该符号表可在实际开发中作为参考使用。 在本书第一部分中,内容从软件相关的概念过渡到硬件相关的概念。这种组织方法意味着,你能够 尽可能不在机器中插入任何外部硬件而测试示例代码。每章都包含有源代码,并给出了能够在任意 一台 Linux 计算机上运行的示例驱动程序。但是,在第 8 章和第 9 章中,我们需要读者在并口 上连接一些电线,以便测试硬件处理代码,当然,这一要求对任何人来讲都是可以做到的。 本书的第二部分讲述了块设备驱动程序和网络接口,并深入讨论了一些更加高级的内容。许多驱动 程序作者可能不需要这些内容,但我们鼓励你阅读这些章节。尽管对某个特定的项目来说,你并不 需要了解这些知识,但第二部分的许多内容和了解 Linux 内核的工作原理一样重要。 背景信息 为了更好地利用本书,我们希望读者熟悉 C 语言编程。因为我们经常会提到 Unix 命令和管道, 因此,也需要读者拥有 Unix 的使用经验。 iii 前言 在硬件级,不需要读者有任何预先的经验就可以理解本书内容,当然,一些一般性的概念是必须清 晰的。本书内容并不基于某个特定的 PC 硬件,我们在提到某个特定的硬件时,会提供给读者所 有必要的信息。 建立内核需要一些自由软件工具,而且经常要求使用这些工具的特定版本。太老的工具可能缺少一 些必要的特性,而太新的工具又可能会偶尔生成不能正常工作的内核。通常而言,当前流行的 Linux 发行版所提供的工具能够很好地工作。不同的内核版本对工具的版本需求不同,这时,你可以参考 内核源代码树中的 Documentation/Changes 文件。 其它信息来源 本书提供的大部分信息直接来自内核源代码以及相关文档。我们要特别注意内核源代码树中 的 Documentation 目录,其中包含有大量有用的信息,比如内核 API 中新增的部分(在 DocBook 子 目录)。 还有一些有用的书籍包含了更为广泛的内容,这些书籍列在本书的“参考书目”中。 Internet 上有大量可用的信息,下面将列出部分站点。当然,Internet 站点的信息在以爆炸式的方 式增加,而印刷书籍却难以及时更新。这样,下面的清单可在本书过时的情况下发挥作用。 http://www.kernel.org ftp://ftp.kernel.org 本站点是 Linux 内核开发的主站点,其中包含了最新的内核发行版本以及相关信息。注意该 FTP 站点的镜像遍布全球,因此,应该选择最近的镜像站点下载 Linux 源代码。 http://www.linuxdoc.org “Linux Documentation Project”拥有大量称作“HOWTO”的文档,其中一些是技术性的,并涉 及到一些内核主题。 http://www.linux-mag.com/depts/gear.html “Gearheads only”中经常发布一些转载自《Linux Magazine》的、由知名开发人员编写的关于内 核的文章。 http://www.linux.it/kerneldocs 其中包含有许多 Aleesandro 所著的有关内核的杂志文章。 http://lwn.net 该新闻站点由本书的作者之一编辑维护,提供了定期的内核开发相关报道。 http://kt.zork.net “Kernel Traffic”是一个大众性的站点,它提供了每周 Linux 内核开发邮件列表中的讨论总结。 http://www.atnf.csiro.au/~rgooch/linux/docs/kernel-newsflash.html “Kernel Newsflash”站点是一个内核新闻的集散地。该站点尤其专注于当前内核版本中的兼容性 问题,人们可以非常容易地看到为什么自己的驱动程序不能在最新的内核当中正常工作。 iv Linux 设备驱动程序 http://www.kernelnotes.org “Kernel Notes”是一个关于内核版本信息、非正式补丁等的经典站点。 http://www.kernelnewbies.org 该站点面向新的内核开发人员。其中包含有针对初学者的内容和 FAQ,而且还有一个 IRC 频道, 可获得即时的帮助。 http://lksr.org “Linux Kernel Source Referenct”是几乎所有内核历史版本的 CVS 归档的 web 接口。如果你 想知道某个特定主题的历史变迁,这个站点再合适不过了。 http://www.linux-mm.org 该网页是面向 Linux 内存管理开发的,其中包含有大量有用信息,并且还包含有许多内核相关的 Web 站点链接。 http://www.conecta.it/linux 这个意大利站点包含了几乎所有正在开发的 Linux 相关项目的信息,并且更新及时。也许读者已 经知道了包含有大量 Linux 开发的 HTTP 链接的站点,如果没有,这个站点将是一个非常好的选 择。 在线版本和条款 本书作者已经选择让本书在 GNU Free Documentation License(GNU 自由文档许可证)版本 1.1 的保护下免费获取。 该许可证全文可见: http://www.oreilly.com/catalog/linuxdrive2/chapter/licenseinfo.html; HTML http://www.oreilly.com/catalog/linuxdrive2/chapter/book; DocBook http://www.oreilly.com/catalog/linuxdrive2/chapter/bookindex.xml; PDF http://www.oreilly.com/catalog/linuxdrive2/chapter/bookindexpdf.html. 本书使用的约定 下面是本书中用到的一些印刷约定: 斜体 等宽字体 等宽斜体 等宽黑体 用于文件、目录的名称,程序和命令的名称,命令行选项,URL 以及新的术 语 用于文件内容或者命令的输出,还用于正文中出现的 C 代码或者其它字符串 用于可变选项、关键词,或者需要用户用实际值替换的文字 用在示例中,表示需要用户照原文键入的命令或者其它文字 v 前言 读者还要注意文中带有如下图标的特殊段落: 表示技巧。其中包含了相关主体的有用辅助信息。 表示警告。它可以帮助你解决或者避免一些问题。 我们希望得到来自读者的反馈 我们已经尽我们所能验证了本书内容,但是读者可能会发现某些功能已经改变(或者甚至是我们所 犯的错误!)。请将你发现的所有错误,或者对于将来版本的建议告诉我们,来信请寄: O'Reilly & Associates, Inc. 101 Morris Street Sebastopol, CA 95472 (800) 998-9938 (in the United States or Canada) (707) 829-0515 (international/local) (707) 829-0104 (fax) 我们还为本书建立了一个网页,其中列出了勘误、示例等内容。该网页地址如下: http://www.oreilly.com/catalog/linuxdrive2 如果你希望对本书进行评论,或者遇到有关本书的技术问题,可发电子邮件到: bookquestions@oreilly.com 有关 O'Reilly 图书的更多信息,包括会议、软件、资源中心以及 O'Reilly Network,可访问我们 的 Web 站点: http://www.oreilly.com 致谢 本书的编写得到了许多人的帮助,我们向他们致以诚挚的谢意。 我(Alessandro)要感谢促成本书的那些人。首先要感谢的是 Federica,在我们的蜜月期间,我 在帐篷的笔记本电脑上审校本书第一版时,她给予充分的理解和支持。Giorgio 和 Giulia 只牵涉 到本书的第二版,她们在我集中精力编写的时候吃纸、拉线、哭泣,不时将我带回现实。我还要感 谢四位祖父母,他们在我临近最后期限时,整日代我履行父亲的职责,帮助我集中精力于代码和咖 啡。我仍然非常感激 Michael Johnson,在他的帮助下,我开始了本书的编写。尽管这已经是几年 前的事情了(那时,我离开了学校,以便专心编写程序而不是撰写论文),但他仍然是促使本书问 世的第一人。作为一名独立的技术顾问,没有哪个老板阻止我在工作时间编写本书,但另一方面, 我仍然要感谢 Francesco Magenta 和 Rodolfo Giometti,他们帮助我成了一名“有靠山的顾问。” vi Linux 设备驱动程序 最后,我还要感谢许多自由软件的作者,是他们真正教会了我如何忘我编程,这包括内核作者以及 我所读过的用户级应用程序的作者。限于篇幅,我不能在这里列出他们的名字。 我(Jon)要感谢许多帮助过我的人。首先要感谢的是我的夫人 Lanra,她在我试图建立一个“.com” 公司的同时编写书籍而花费了大量时间。我的两个孩子,Michela 和 Guilia,始终是我快乐和灵感 的源泉。我在 LWN.net 的同事们对我因编写本书而分心表现出了极大的容忍;我还要感谢 LWN 内核网页的读者对我的支持。如果没有 Boulder 的本地社区广播电台(可称为“KGNU”),这一 版本可能就无法问世。这个广播电台播放吸引人的音乐,以及 Lake Eldora 的滑雪广告。有了它, 我才得以在孩子们滑雪的时候,整日带着笔记本电脑露营,并享受咖啡。我还要特别感谢 Evi Nemth,她让我在她的 VAX 机器上研究早期的 BSD 源代码;还要感谢 William Waite,是他真 正教会了我如何编程;最后要感谢 National Center for Atmospheric Research (NCAR) 的 Rit Carbone,是他给了我一个长期的职位,我在那里学到了所有其它的东西。 我们两个作者还要感谢本书的编辑,Andy Oram,在他的努力下,本书成为一本非常好的图书产品。 我们要感激许多推进自由软件思想并让这些软件发挥作用的伟大的人(这主要归功于 Richard Stallman,但他绝不是唯一的)。 许多人还帮助我们建立了硬件环境,没有这些来自外部的帮助,我们不可能研究这么多的平台。我 们要感谢 Intel 借给我们一台早期的 IA-64 系统,还有 Rebel.com 捐赠了一台 Netwinder(基于 ARM 的微型计算机)。Prosa Labs,即 Linuxcare-Italia 的前身,借给我们一台配置非常好的 PowerPC 系统;NEC Electronics 捐赠了他们最有趣的 VR4181 处理器开发系统,这个系统是一 个掌上型电脑,我们可以将 GNU/Linux 系统烧到 Flash 存储器中,并在这个系统上运行。 Sun-Italia 借给我们一台 SPARC 系统和一台 SPARC64 系统。所有这些公司和他们的系统让 Alessandro 忙于解决移植问题,而且还不得不多用一间屋子来建立他的“硅片兽动物园”。 本书第一版由 Alan Cox、Greg Hankins、Hans Lermen、Heiko Eissfeldt,以及 Miguel de Icaza (依照名字字母排序)进行了技术审校。第二版的技术审校人是 Allan B. Cruse、Christian Morgner、Jake Edge、Jeff Garzik、Jens Axboe、Jerry Cooperstein、Jerome Peter Lynch、Michael Kerrisk、Paul Kinzelman 和 Raph Levien。他们花费了大量时间寻找本书的错误或者问题,并且 指出了文中可以提高的地方。 最后,让我们感谢 Linux 开发人员所做出的艰苦工作。这包括内核程序员以及经常会被遗忘的应 用软件开发人员。本书中,我们一直没有提到他们的名字,以避免因为遗忘其他人的名字而显得不 公平。有时也有例外,我们会提到 Linus 的名字,当然,我们希望他不会介意。 vii Linux 设备驱动程序 第 1 章 设备驱动程序简介 随着 Linux 系统变得越来越流行,人们编写 Linux 驱动程序的兴趣也在稳步增长。Linux 的大部 分内容独立于底层硬件运行,许多用户也无需关心硬件问题。但是,Linux 所支持的每一款硬件, 一定有人曾为它编写过驱动程序,否则就无法在 Linux 系统下发挥功能。也就是说,没有设备驱 动程序,就不会有功能完整的运行系统。 设备驱动程序在 Linux 内核中扮演着特殊角色。它们是一个个独立的“黑盒子“,使某个特定硬 件响应一个定义良好的内部编程接口,这些接口完全隐藏了设备的工作细节。用户操作通过一组标 准化的调用执行,而这些调用是和特定的驱动程序无关的。将这些调用映射到作用于实际硬件的设 备特有操作上,则是设备驱动程序的任务。这个编程接口能够使得驱动程序独立于内核的其它部分 而建立,必要的情况下,可在运行时“插入”内核。这种模块化的特点,使得 Linux 驱动程序的 编写非常简单,因此内核驱动程序的数目也增长迅速,目前已有成百上千的驱动程序可用。 促使我们对编写 Linux 驱动程序感兴趣的原因有很多。首先,仅新硬件问世(或过时)的速度就 会使驱动程序编写人员面临很多任务;其次,个人用户需要了解一些驱动程序知识才能访问设备; 另外,硬件厂商通过提供 Linux 驱动程序,能为自己的产品带来数目庞大且日益增长的潜在用户 群;最后,Linux 系统是开放源码的,如果驱动程序作者愿意,驱动程序源码就可以在用户中间迅 速流传。 本书将讲述有关驱动程序编程以及内核的相关知识。我们采取独立于硬件的方法,所讲述的编程技 巧和接口尽可能不依赖于任何具体设备。每个驱动程序都不尽相同,作为驱动程序开发者也应该理 解自己的具体设备。然而,所有驱动程序的基本原理和技巧都是相同的,本书不准备讲述具体的设 备,而主要集中在让设备工作的背景知识上。 刚开始学习编写驱动程序时,我们经常会碰到许多关于 Linux 内核的知识。它将帮助我们理解机 器如何工作,工作为什么不象预期的那样快,或者为什么没有产生预期的结果等等。我们将逐渐介 绍新知识,先从简单的驱动程序开始,然后逐渐构造复杂的驱动程序。每个新概念都带有示例代码, 它们不需要特别的硬件支持就可以运行。 本章不涉及实际的编程。然而,我们会介绍一些有关 Linux 内核的背景知识,这些知识在后来进 1 第 1 章 设备驱动程序简介 行实际编程时将非常有用。 1.1 设备驱动程序的作用 作为驱动程序编写者,我们需要在所需的编程时间以及驱动程序的灵活性之间选择一个可接受的折 中。读者可能奇怪于说驱动程序“灵活”,我们用这个词实际上是强调设备驱动程序的作用在于提 供机制,而不是提供策略。 区分机制和策略是 Unix 设计背后隐含的最好思想之一。大多数编程问题实际上都可以分成两部 分:“需要提供什么功能”(机制)和“如何使用这些功能”(策略)。如果这两个问题由程序的不同 部分来处理,或者甚至由不同的程序来处理,则这个软件包更易开发,也更容易根据需要来调整。 例如,Unix 中图形显示器的管理就分成 X 服务器以及窗口和会话管理器两部分。前者操作硬件, 给用户程序提供统一接口;后者实现特定策略,不用知道任何与硬件相关的知识。我们可以在不同 硬件上运行同样的窗口管理器,不同的用户也可以在相同的工作站上使用不同的配置。即使完全不 同的桌面环境,比如 KDE 和 GNOME,也能在同一个系统中共存。另外一个例子是具有分层结构 的 TCP/IP 网络。位于下层的操作系统负责提供套接字抽象层,但在所传输的数据上则没有附加 任何策略;上面各层的服务器则分别提供不同的服务(以及相关策略)。另外,一个类似 ftpd 这样 的服务器提供文件传输机制,用户可以使用任何自己喜欢的客户端传输文件,例如命令行和图形客 户端;而任何人也可以写一个新的用户界面来传输文件。 驱动程序同样存在机制和策略的分离。例如,软驱驱动程序不带策略,它的作用是将磁盘表示为一 个连续的数据块序列,而系统高层负责提供策略,比如谁有权访问软盘驱动器,是直接访问驱动器 还是通过文件系统,以及用户是否可以在驱动器上挂装文件系统等等。既然不同的环境通常需要不 同的方式来使用硬件,我们应当尽可能做到让驱动程序不带策略。 程序员编写驱动程序时应该特别注意下面这个基本概念:编写访问硬件的内核代码时不要给用户强 加任何策略。因为不同用户有不同需求,驱动程序应该处理如何使硬件可用的问题,而将怎样使用 硬件的问题留给上层应用。因此,当驱动程序只提供了访问硬件的功能而没有附加任何限制时,这 个驱动程序就比较灵活。然而,有时候我们也需要在驱动程序中实现一些策略。例如,某个数字 I/O 驱动程序只提供了以字节为单位访问硬件的方法,这样就省去了编写额外的代码处理单个数据位的 麻烦。 如果从另外一个角度来看驱动程序,它可以看作是应用和设备之间的一个软件层。这种定位使驱动 程序编写者可以选择如何展现设备特性:即使对于相同设备,不同的驱动程序也可以提供不同的功 能。实际的驱动程序设计应该在许多考虑因素之间作出平衡。例如,某个驱动程序可能同时被多个 进程使用,我们就应当考虑如何处理并发问题:可以在设备上实现独立于硬件功能的内存映射;也 可以提供一个用户函数库,以帮助应用程序开发者在原语基础上实现新的策略。总的来说,驱动程 序设计主要还是综合考虑下面三方面的因素:提供给用户尽量多的选项、驱动程序编写占用较少时 间以及尽量保持程序简单而不至于错误丛生。 不带策略的驱动程序包括一些典型的特征:同步和异步操作都支持、驱动程序能够多次打开、能够 充分利用硬件特性以及不具备“简化任务”功能或提供与策略相关的软件层等。这种类型的驱动程 序不仅能很好地服务最终用户,而且易于编写和维护。实际上,不带策略是软件设计者的一个共同 2 Linux 设备驱动程序 目标。 然而,许多驱动程序是同用户程序一起发行的。这些用户程序主要用来帮助配置和访问目标设备。 它们可能是简单的工具,也可能是完整的图形应用程序。例如,用来调整并口打印机驱动程序工作 方式的 tunelp 程序;作为 PCMCIA 驱动程序包一部分的图形化的 cardctl 工具等等。和驱动程 序一起提供的还会有一个客户程序库,它提供了那些不必在驱动程序本身实现的功能。 本书的讨论范围局限于内核,因此我们将尽量避免讨论策略、应用程序和支持库的问题。有时可能 确实会涉及到有关策略以及如何支持策略的内容,但我们不会深入讨论使用设备的用户程序及它们 所实现的策略。另外,我们应该清楚,用户程序是软件包的有机组成部分,即使不带策略的软件包, 也会同时发布配置文件为下层机制提供缺省配置。 1.2 内核功能划分 Unix 系统支持多个进程并发运行,每个进程都请求系统资源,比如处理能力、内存、网络连接和 其它一些资源等。内核负责处理所有这些请求,根据内核完成任务的不同(这些任务之间的区别可 能不总是那么清楚),如图 1-1 所示,可将内核功能分成如下几部分: 图 1-1:内核功能的划分 进程管理 进程管理功能负责创建和撤销进程以及处理它们和外部世界的连接(输入输出)。不同进程之间的 通讯(通过信号、管道或进程间通讯原语)是整个系统的基本功能,因此也由内核处理。除此之外, 3 第 1 章 设备驱动程序简介 控制进程如何共享 CPU 的调度器也是进程管理的一部分。概括来说,内核进程管理活动就是在单 个或多个 CPU 上实现了多个进程的抽象。 内存管理 内存是计算机的主要资源之一,用来管理内存的策略是决定系统性能的一个关键因素。内核在有限 的可用资源之上为每个进程都创建了一个虚拟地址空间。内核的不同部分在和内存管理子系统交互 时使用同一套系统调用,这包括从简单的 malloc/free 到其它一些不常用的系统调用。 文件系统 Unix 在很大程度上依赖于文件系统的概念,Unix 中的每个对象几乎都可以当作文件来看待。内核 在没有结构的硬件上构造结构化的文件系统,构造的文件系统抽象在整个系统中广泛使用。另外, Linux 支持多个文件系统类型,即在物理介质上不同组织数据的方式。例如,磁盘可以格式化为符 合 Linux 标准的 ext2 文件系统,也可格式化为常用的 FAT 文件系统。 设备控制 几乎每条系统操作最终都会映射到物理设备上。除了处理器、内存以及其它很有限的几个对象外, 所有设备控制操作都由与被控制设备相关的代码来完成。这段代码就叫做驱动程序,内核必须为系 统中的每件外设嵌入相应的驱动程序,包括硬盘驱动器、键盘和磁带条(streamer)等。这方面的 内核功能将是本书讨论的主题。 网络功能 网络功能也必须由操作系统来管理,因为大部分网络操作和具体进程无关——数据包的传入是异步 事件。在某个进程处理这些数据包之前必须已经被收集、标识和分发。系统负责在应用程序和网络 接口之间传递数据包,并根据网络活动控制程序的执行。另外,所有的路由和地址解析问题都有内 核处理。 在本书末尾的第 16 章,我们会看到 Linux 内核的导引图(road map),目前先暂时介绍到这儿。 Linux 的优良特性之一是能够在运行时动态扩展内核特性,当系统正在运行时我们就可以给内核增 添新的功能。 运行时向内核中添加的代码称之为模块,Linux 内核支持几种不同类型(或分类)的模块,这当中 包括(但不仅仅局限于)驱动程序。每个模块由目标代码(没有连接为完整的可执行文件)构成, 可以由 insmod 程序动态连接到运行内核,也可以由 rmmod 程序解除连接。 图 1-1 中列出负责特定任务的几个不同类型的模块。根据模块所提供的功能我们确定它属于哪个类 型,图 1-1 中列出了最重要的几个类型,但远远不是全部,因为越来越多的 Linux 功能正在被模 块化。 4 Linux 设备驱动程序 1.3 设备和模块分类 Unix 系统将设备分成三种类型:字符设备、块设备和网络设备。每个模块通常实现其中一种类型 的设备,相应地,模块可分为字符模块、块设备模块和网络模块三种。然而这种分类方式并非非常 严格,程序员可以构造一个大的模块,在其中实现不同类型的设备驱动程序。然而,优秀程序员通 常还是为每个功能创建一个不同的模块,从而实现良好的伸缩性和扩展性。 三种类型的设备如下: 字符设备 字符设备是个能够象字节流(比如文件)一样访问的设备,由字符设备驱动程序来实现这种特性。 字 符 设 备 驱 动 程 序 通 常 至 少 需 要 实 现 open 、 close 、 read 和 write 系 统 调 用 。 字 符 终 端 (/dev/console)和串口(/dev/ttys0 以及类似设备)就是两个字符设备,它们能够很好地表示成流 抽象索。字符设备可以通过文件系统节点(比如 /dev/tty1 和 /dev/lp0 等)来访问,它和普通文 件之间的唯一差别在于对普通文件的访问可以前后移动访问指针,而大多数字符设备是个只能顺序 访问的数据通道。然而,也存在和数据区特性类似的字符设备,访问它们时可前后移动访问指针。 例如帧抓取器就是这样一个设备,应用程序可以用 mmap 或 lseek 访问抓取的整个图象。 块设备 和字符设备一样,块设备也是通过 /dev 目录下的文件系统节点来访问。块设备(例如磁盘)上能 够容纳文件系统。在大多数 Unix 系统中,块设备包含整数个块,而每块包含 1K 或 2 的其它次 幂字节的数据。Linux 可以让应用程序象字符设备一样地读写块设备,允许一次传递任意多字节的 数据。因而,块设备和字符设备的区别仅仅在于内核内部管理数据的方式,也就是内核和驱动程序 的接口不同。象字符设备一样,块设备也是通过文件系统节点来访问,它们之间的差异对用户是透 明的。块设备除了给内核提供和字符设备一样的接口外,另外还提供了专门面向块设备的接口,不 过这些接口对于那些从 /dev 目录下某个目录项打开块设备的用户和应用程序都是不可见的。另 外,块设备的接口必须支持挂装文件系统。 网络接口 任何网络事务都要经过一个网络接口,即一个能够和其它主机交换数据的设备。通常接口是个硬件 设备,但也可能是个纯软件设备,比如回环接口。网络接口由内核中的网络子系统驱动,负责发送 和接收数据包,它不用了解每项事务如何映射到实际传送的数据包。尽管 Telnet 和 FTP 连接都是 面向流的,它们都使用了同一个设备,而这个设备看到的只是数据包,而不是一个个流。 由于不是面向流的设备,因此将网络接口映射到文件系统中的节点(比如 /dev/tty1)比较困难。 Unix 式的访问网络接口的方法是给它们分配一个唯一的名字(比如 eth0),这个名字在文件系统 中不存在对应的节点项。内核和网络驱动程序间的通讯完全不同于内核和字符设备以及块设备驱动 程序之间的通信,内核调用一套和数据包传输相关的函数而不是 open、write 等。 Linux 中还存在其它类型的驱动程序模块,这些模块利用内核提供的公共服务来处理特定类型的设 备。因此我们能够和通用串行总线(USB)模块、串口模块等通信。最常见的非标准类型的设备是 5 第 1 章 设备驱动程序简介 SCSI 设备*。 尽管和 SCSI 总线连接的每一款外部设备都在 /dev 目录下作为块设备或字符设备出现,软件的 内部组织却不一样。 就像网卡给网络子系统提供与硬件相关的功能一样,SCSI 控制器给 SCSI 子系统提供访问实际 接口电缆的能力。SCSI 是计算机和外部设备之间的一个通信协议,不管计算机上插入什么类型的 控制板,每个 SCSI 设备都响应同样的协议。Linux 内核中因此实现了 SCSI 模块(即文件操作 到 SCSI 通信协议之间的映射)。驱动程序开发者必须实现 SCSI 抽象和物理数据线之间的映射, 这种映射依赖于 SCSI 控制器,而与连接到 SCSI 数据线上的设备无关。 最近还有其它类型的设备驱动程序加入内核,例如 USB 驱动程序、FireWire 驱动程序和 I20 驱 动程序等。和处理 SCSI 驱动程序的方法一样,内核开发者实现整个设备类型的共有特性,然后 提供给驱动程序实现者,从而避免了重复工作以及 bug,简化并增强了编写这些驱动程序的过程。 除了驱动程序外,内核中其它一些功能,不管是硬件还是软件功能,都模块化了。文件系统可能是 除驱动程序外 Linux 系统中最重要的模块类型,它决定了信息如何在块设备上组织,以表示目录 和文件树。文件系统并不是设备驱动程序,因为没有任何实际物理设备同这种信息组织方式相关联。 相反,文件系统类型是个软件驱动程序,它将低层数据结构映射到高层数据结构,决定文件名可以 有多长以及在目录项中存储文件的哪些信息等等。文件系统模块必须实现访问目录和文件的底层系 统调用,方法是将文件名和路径(以及其它一些信息,比如访问模式等)映射到位于数据块上的数 据结构中。这种接口完全独立于磁盘(或其它介质)上的数据读写操作,这种操作由块设备驱动程 序负责完成。 由于 Unix 系统严重依赖于底层的文件系统,因此文件系统概念对系统操作具有重要意义。访问文 件系统的功能位于内核层次结构的最底层,具有非常重要的作用。如果我们想为一款新的 CD-ROM 编写块驱动程序,则必须提供对 CD-ROM 上包含的数据进行 ls 或 cp 等操作的功能,否则驱动 程序毫无用处。Linux 支持文件系统模块的概念,它的软件接口声明了可以在文件系统中的节点、 目录、文件以及超级块上执行的不同操作。不过,程序员需要自己编写文件系统模块的情况比较少 见,因为正式发行的内核版本中已经包含了最重要文件系统类型的代码。 1.4 安全问题 安全问题日益引起人们的关注,本书在适当的时候都会讨论这一问题。然而,有必要现在就弄清楚 几个原则性的概念。 安全问题分为偶然性的和故意的两类。前者是由于用户不正确使用现有程序或者不小心使用了程序 中的 bug 而造成的破坏;后者则是由于程序员故意实现的某些带有恶意功能的程序而带来的安全 问题,这种程序员通常比一般用户拥有更多的特权。因此,如果我们运行的程序是从具有 root 帐 户的第三方获得的,它的危险性等同于直接给第三方一个 root 命令解释器。另外,尽管拥有访问 编译器的权限本身并不是一个安全漏洞,然而当执行由这个编译器编译的代码时就可能出现安全漏 洞。由于内核模块可以执行任何操作,它和超级用户命令解释器一样强大,所以编写模块时我们应 * SCSI 是“Small Computer Systems Interface”的缩写,它是工作站和高端服务器领域事实上的标准。 6 Linux 设备驱动程序 当倍加小心。 系统中的所有安全检查都是由内核代码进行的,如果内核有安全漏洞,则整个系统就会有安全漏洞。 在正式发行的内核版本中,只有授权用户才能装载模块,系统调用 create_module 检查调用进程 是否具有装载模块的权利。因此,运行正式发行的内核版本时,只有超级用户*,或者成功成为超 级用户的入侵者,才能使用特权代码。 驱动程序编写者应当尽量避免在代码中实现安全策略,它最好在系统管理员控制之下,由内核的高 层来实现。当然也会有例外,作为驱动程序编写者,我们应当清楚有时候某些设备访问操作能够影 响整个系统,因此应该严格控制。例如,能够影响全局资源(比如设置中断线)的设备操作,或者 影响其它用户(比如给磁带驱动器设置块尺寸的缺省值)的设备操作,通常只能由特权用户执行, 并且只能由驱动程序本身才能检查用户的权限。 当然,驱动程序编写者应当避免由于自身原因引入安全方面的 bug。C 语言很容易产生几种类型 的错误,比如缓冲区溢出就会导致许多安全问题。缓冲区溢出通常是由于程序员忘记检查缓冲区中 已写了多少数据,导致数据写到了缓冲区边界之外,覆盖了系统中其它数据。这种错误可能破坏整 个系统,因此必须尽量避免。幸运的是,在驱动程序环境中避免这种错误通常相对容易,因为此时 用户接口比较有限而且严格控制。 还有其它一些原则性的安全概念值得注意。任何从用户进程得到的输入只有经过内核严格验证后才 能使用,内核内存在分配给用户进程和设备之前必须清零或者以其它方式初始化,否则就会发生信 息泄漏。如果设备能够解释它从内核获得的数据,则确保它不能输出任何可能损害系统的内容。最 后,我们还应当考虑设备操作可能造成的影响,如果某些操作(比如重新装载适配卡上的固件或者 格式化磁盘)能够影响整个系统,则它应当仅限于特权用户使用。 应当小心使用从第三方获得的软件,特别是与内核相关时更是如此。这是因为源码是开放的,每个 人都可以修改和重新编译它。通常我们可以信任发行版本中预先编译的内核,但当使用由一个我们 不是非常熟悉的朋友编译的内核时就得当心。如果我们不想以 root 身份运行一个预先编译过的二 进制文件,则也不应当运行一个预先编译的内核。一个恶意修改过的内核允许任何人装载模块,因 此通过 create_module 开了一扇后门。 Linux 内核也可编译为不支持模块方式,因而关闭了任何相关的安全漏洞,但这种情况下驱动程序 需要直接嵌入内核。2.2 及以后的内核版本还可以通过权能机制禁止内核在系统启动后装载模块。 1.5 版本编号 在深入探讨编程之前,我们希望探讨一下 Linux 使用的版本编号机制以及本书中所讲到的内核版 本。 首先,Linux 的每个软件包都有自己的发行版本号,并且它们之间存在相互依赖关系,例如只有存 在某个软件包的某个特定版本时才能运行另一个软件包的某个特定版本。通常 Linux 正式发行版 本已经解决了复杂的包匹配问题,此时安装系统不需要我们自己处理版本号。然而如果我们需要自 * 内核 2.0 版本只允许超级用户运行特权代码,而在内核 2.2 版本中具有更复杂的权能检查方法。在第 5 章“权能 7 第 1 章 设备驱动程序简介 己替换或者更新系统中某个软件包则另当别论。幸运的是,现在几乎所有的发行版本中都带有包管 理器,它在验证包之间的依赖关系满足后才允许升级包。 本书中的某些示例代码需要特定的某些内核版本才能运行,除此之外,它对其它工具则没有版本要 求。任何最近发行的 Linux 版本都可以运行我们的例子。对内核版本要求的具体细节本文没有描述, 读者遇到任何问题时可参考内核源文件 Documentation/Changes。 偶数编号的内核版本(如 2.2.x 和 2.4.x)是用于正式发行的稳定版本,而奇数编号的版本(如 2.3.x) 则是开发过程中的一个快照,它将很快被下一开发版本更新。最新的开发版本只是代表了内核开发 目前的状态,几天后可能就会过时。 本书中讲到了从 2.0 到 2.4 的各个版本。不过我们的注意力主要集中在内核 2.4(写本书时的最新 稳定版本)给设备驱动程序编写者所提供的各种特性上,但也将尽可能地全面介绍 2.2 内核区别于 2.4 内核的不同之处。我们还会讲到内核 2.0 版本同 2.2 和 2.4 内核相比所缺乏的一些特性以及一 些替代办法。总之,本书中的代码可以在内核的一系列版本中运行,在 2.4.4 内核版本中全部测试 通过,在 2.2.18 和 2.0.38 内核版本中也测试过部分示例程序。 本书很少讨论到奇数编号的内核版本,普通用户也很少会有使用这种版本的需求。然而,如果我们 希望了解、跟踪开发版本的新特性,就需要运行最近发行的开发版本。而且还得随着开发版本的更 新,不断获取 bug 的补丁以及新实现的特性。对于开发版本,我们必须记住它没有任何责任担保 *。而且如果我们碰到的问题是由于老版本奇数编号的内核引起的,则没有人可以求助。那些运行 奇数编号内核版本的程序员通常具有足够的知识,无需求助教科书就可以自己钻研内核代码。这也 是我们为什么不在这儿讨论内核开发版本的另外一个原因。 Linux 的另外一个特性是它是一个平台独立的操作系统,不再仅仅是“PC 克隆上的 Unix 克隆”。 它现在已经成功地应用在 Alpah 和 SPARC 处理器上,以及 68000、PowerPC 和其它几个平台 上。本书尽可能做到与平台无关,所有示例代码都在几个平台上测试过,包括 PC、Alpha、ARM、 IA-64、M68k、PowerPC、SPARC、APARC64 以及 VR41xx(MIPS) 等。本书所有示例代码在 32 位和 64 位处理器上都测试过,在其它平台上基本都能编译运行。然而,如果示例代码依赖于特 定硬件,则不能在所有支持的平台上都正常工作,此时在源码中我们会特别声明这一点。 1.6 许可证条款 Linux 受 GNU 通用公共许可证(GPL)保护。GPL 由自由软件基金会为 GNU 项目而设计,它 允许任何人重新发行甚至销售由 GPL 条款限制的产品,前提是产品接收者能够从源码中重新构建 一个同样的二进制副本。另外,任何从 GPL 保护的产品中派生出来的软件产品,也必须随 GPL 条 款一起才能发行。 这样一个许可证的主要目的是通过允许每个人自由修改程序来实现知识增长;同时,向公众卖软件 的人仍旧可以获利。但就是这样目的简单的条款,关于 GPL 及其使用仍然存在着无休止的争论。 如果读者想去读一下这个许可证原文,可以在系统中几个地方找到它,包括目录 /usr/src/linux 中 的 COPYING 文件。 和受限操作”中我们将具体讨论这种方法。 8 Linux 设备驱动程序 第三方模块和定制的模块不是 Linux 内核的一部分,因此它们可以不受 GPL 条款的限制。模块 通过一个良好的接口使用内核,但不是内核的一部分,这同用户程序通过系统调用使用内核的方式 类似。记住免受 GPL 条款限制仅适用于那些只使用了公开发布的模块接口的模块。深深嵌入内核 的那些模块应当遵守 GPL 中关于“派生工作”的条款。 总之,如果我们的代码嵌入内核内部,则发布代码时就得立即应用 GPL 条款。如果出于个人使用 目的,则不必强制执行 GPL,但如果正式发布代码就必须包含源码,使获得我们软件包的用户能 够自由的重建二进制代码。另一方面,如果我们写了一个模块,我们可以只发布它的二进制形式。 然而,现实中这样做并不总是行的通。因为通常对于每个与之连接的内核版本(第 2 章“版本依赖” 和第 11 章“模块中的版本控制”中解释),模块都需要重新编译。新的内核发布版本,即使是很小 的稳定发布,也经常会破坏编译的模块,因此需要重新编译模块。Linus Torvalds 曾公开表示他认 为这种方式没有什么问题,因为二进制模块应该只工作在它编译时所使用的内核版本上。然而作为 模块编写者,提供源代码通常能够更好的服务用户。 就本书而言,不管是源代码还是二进制形式,大部分代码都可以免费重新发布,并且不管是作者还 是 O'Reilly & Associates 出版社,都不会对任何派生的产品保留任何权利。所有程序都可以从 ftp://ftp.ora.com/pub/examples/linux/drivers/ 中得到,许可证条款在同一目录下的 LICENSE 文件 中表述。 当示例程序包含了部分内核代码时,此时适用 GPL 条款,源文件中的注释已经非常明确地阐明了 这一点。这仅仅发生在与本书主题关系不大的几个源文件中。 1.7 加入内核开发社团 当我们为 Linux 内核编写模块的时候,我们就成为一个巨大开发人员社团中的一员。在这个社团 中,我们不仅发现很多人从事类似的工作,而且发现一帮具有高度使命感的工程师正朝着将 Linux 发展成为一个更好系统的目标前进。这些人是我们获得帮助、思路以及严格评价的源泉。当我们为 新的驱动程序寻找测试者的时候,他们将是我们乐于提交的第一批人。 Linux-kernel 邮件列表是 Linux 内核开发者的聚集中心。从 Linus Torvalds 往下,所有主要内核 开发者都订阅这个邮件列表。请注意这个列表不适合那些心脏比较脆弱的人:该邮件列表每天都会 有 200 条消息或者更多。然而,对于那些对内核开发感兴趣的人来说,跟踪这个列表是必要的, 对于那些需要内核开发帮助的人来讲,它更是一个顶级质量的资源。 要加入 Linux-kernel 列表,遵照 linux-kernel 邮件列表 FAQ:http://www.tux.org/lkml 中的指示 即可。如果已经打开了这个 FAQ,我们还应当看看它的其它内容,它上面有大量的有用信息。Linux 内核开发者都比较忙,他们更愿意帮助那些首先了解了基本知识的人。 1.8 本书概要 从第 2 章起,我们将进入内核编程领域。第 2 章介绍了模块化技术,解释了其实现技巧,并讲解 * 注意即使对于偶数编号的内核版本,同样没有任何责任担保。只有商业版本发行公司才会对他们的产品提供担保。 9 第 1 章 设备驱动程序简介 了运行模块的代码。第 3 章讨论字符驱动程序,给出了一个基于内存的设备驱动程序的完整代码。 将内存作为设备硬件,可允许任何人在无需特殊硬件的情况下,运行我们的示例代码。 对程序员来讲,调试技术是很重要的工具,我们将在第 4 章介绍内核调试技术。随后,带着新的调 试技巧,我们转到字符驱动程序的高级特性,比如阻塞操作、select 使用以及重要的 ioctl 调用等, 这些都是第 5 章的内容。 在讨论硬件管理之前,我们先剖析内核的几个软件接口:第 6 章讨论内核的时间管理,第 7 章讨论 内存分配。 接下来我们集中于硬件问题。第 8 章描述 I/O 端口管理和设备上内存缓冲区的管理。之后,我们 在第 9 章讨论中断处理。不幸的是,并不是所有人都愿意运行这些章节中的示例代码,因为需要一 些硬件支持才能测试中断的软件接口。我们尽可能使必需的硬件支持减到最小,但仍然需要自己手 工建造一些硬件“设备”。这个设备就是一个插到并口上的简单跳线,因此我们希望这不是一个问 题。 第 10 章提供了一些有关编写内核软件以及可移植性问题的建议。 在本书的第二部分,我们将探讨更深层次的内容。因此,第 11 章将再次讨论模块化问题,只不过 这次更加深入一些。 接着第 12 章介绍块设备驱动程序的实现方法,概述了它们区别于字符设备的不同之处。在那之后, 第 13 章讲述了我们在前面讨论内存管理时遗留的内容:mmap 和直接内存访问(DMA)。这个时 候,关于字符和块设备驱动程序的所有要点都已讲述清楚。 接下来介绍驱动程序的第三个主要类型。第 14 章介绍了关于网络接口的一些细节并且解剖了一个 网络驱动程序例子的代码。 驱动程序的有几个特性直接依赖于外围设备所连接的接口总线。因此第 15 章中介绍了现在流行的 几款总线的主要特性,特别重点讨论了内核提供的 PCI 和 USB 支持。 最后,第 16 章是对内核源码的一个概览,它希望给下面的一种人提供一个学习的起点:希望了解 内核整体设计,但又惧怕庞大的内核源码。 10 Linux 设备驱动程序 第 2 章 构造和运行模块 非常高兴现在终于可以开始编程了。本章将介绍所有关于模块编程和内核编程的必要概念,并用有 限的篇幅构建和运行一个完整的模块。掌握这种技能是编写任何驱动程序模块的基础。为了避免一 次引入太多概念,本章将只讨论模块,而避免涉及任何特定类型的设备。 本章中引入的所有内核条目(函数、变量、头文件和宏)在本章末尾的“快速参考”一节会集中描 述。 我们要讨论的第一个模块,其实是一个完整的“Hello, World”模块(这个模块实际上并没有任何 特别的功能),它可以在 Linux 2.0 到 2.4 的各个内核版本中编译、运行。* #define MODULE #include int init_module(void) { printk("<1>Hello, world\n"); return 0; } void cleanup_module(void) { printk("<1>Goodbye cruel world\n"); } 函数 printk 在 Linux 内核中定义,功能和标准 C 库中的函数 printf 类似。内核需要自己单独的 打印输出函数是因为它在运行时不能依赖于 C 库。模块能够调用 printk 是因为在 insmod 函数 装入模块后,模块就连接到内核,因而可以访问内核的公用符号(包括函数和变量,下一节详述)。 代码中的字符串 <1> 定义了这条消息的优先级。我们需要在模块代码中显式指定高优先级(小级 别编号)的原因在于:具有默认优先级的消息可能不会输出在控制台上,这依赖于内核版本、klogd 守护进程的版本以及具体的配置。读者可以暂时忽略这个问题,我们将在第 4 章“printk“一节中 仔细阐述。 如下面的命令行以及屏幕输出所示,读者可以通过调用函数 insmod 和 rmmod 来测试模块。值 得注意的是只有超级用户才有权装入和卸载模块。 要想成功装入和卸载上面的模块,就必须禁止内核的模块版本控制功能。然而,大多数 Linux 发 行版本所预先安装的内核都具有版本控制功能(在第 11 章“模块中的版本控制”中将会讲到版本 控制功能)。虽然老版本的 modutils 允许将不支持版本控制功能的模块装入支持版本控制功能的 内核,新版本的 modutils 却不再支持这一功能。为了解决上面的 hello.c 遇到的这个问题,我们 在示例程序 misc-modules 目录中的源文件中增加几行代码,使它在支持和不支持版本控能功能 11 第 2 章 构造和运行模块 的内核中都可以运行。尽管这样,我们仍然强烈建议读者在运行示例代码前将自己的内核编译为不 支持版本控制功能。* root# gcc -c hello.c root# insmod ./hello.o Hello, world root# rmmod hello Goodbye cruel world root# 根据系统传递消息行机制的不同,我们自己得到的输出结果可能不一样。需要特别指出的是,上面 的屏幕输出是在字符终端上得到的,如果在 xterm 上运行 insmod 和 rmmod,则不会在 xterm 的 TTY 上看到任何输出。实际上,它可能输出到某个系统日志文件里,比如 /var/log/messages (实际的名称随 Linux 发行版的不同可能会有所变化)。内核消息的传递机制将在第 4 章“消息 是如何记录的”中详细讨论。 我们已经看到,编写一个模块并没有想象的那么困难,困难在于理解设备并优化其性能。本章我们 将深入讨论模块问题,而把设备相关的问题留到以后的章节。 2.1 核心模块与应用程序的对比 在进一步讨论之前,有必要搞清楚内核模块和应用程序之间的种种不同之处。 尽管应用程序是从头到尾执行单个任务,而模块却只是预先注册自己以便服务于将来的某个请求, 然后,它的“main”函数就立即结束。换句话说,函数 init_module(模块的入口)的任务是为以 后调用模块函数预先做准备;这就像模块在说,“我在这儿,并且我能做这些工作。”模块的第二个 入口点,cleanup_module,在模块即将卸载之前调用。它告诉内核,“我要离开啦,不要再让我做 任何事情。”能够卸载模块可能是模块化驱动程序编程当中,读者最为喜欢的一个特色,因为它帮 助缩短模块的开发周期:我们可以测试新驱动的一序列版本却不需要每次都经过冗长的关机/重启 过程。 作为程序员,我们知道应用程序可以调用它并未定义的函数,这是因为连接过程能够解析外部引用 从而使用适当的函数库。例如,定义在 libc 中的 printf 函数,就是这种可被调用的函数之一。然 而,模块仅仅被连接到内核,因此它能调用的函数仅仅是由内核导出的那些函数,而没有任何可连 接的库。例如,前面 hello.c 中使用的 printk 函数,就是由内核定义并导出给模块使用的一个 printf 的内核版本。除了几个细小差别外,它和 printf 函数功能类似,最大的不同在于它缺乏对浮点数 的支持*。 图 2-1 展示了如何在模块中使用函数调用和函数指针为运行中的内核增加新功能。 * 这个例子以及本书中的其它例子都可以从第 1 章中提到的 O'Reilly FTP 网站获得。 * 如果读者还不知道怎样构造内核,我们建议你首先阅读 Alessandro 在 http://www.linux.it/kerneldocs/kconf 上发 表的一篇文章,它对初学者很有帮助。 * 在 Linux 2.0 和 Linux 2.2 中 printk 函数不支持 L 和 Z 限定符,Linux 2.4 中才首次增加支持。 12 Linux 设备驱动程序 图 2-1:将模块连接到内核 因为模块不和函数库连接,因此在源文件中不能包含通常的头文件。内核模块只能使用作为内核一 部分的函数。和内核相关的任何内容都声明在内核源码(通常位于/usr/src/linux)的 include/linux 和 include/asm 目录下的头文件里。老的发行版(基于 libc 版本 5 或者更早)中,通常利用符号链 接将 /usr/include/linux 和 /usr/include/asm 指向实际的内核源代码,因而 libc 的头文件树总是 能够指向实际安装的内核源代码的头文件。通过这些符号链接,用户空间的应用程序能够很方便地 引用内核头文件。它们偶尔确实需要这样做。 尽管现在用户空间头文件和核心空间头文件是分离的,有时应用程序仍然需要包含内核头文件。比 如说在使用一个老版本库时,或者需要一些无法从用户空间头文件中获得的新信息时等等。然而, 内核头文件中的许多声明仅仅与内核本身相关,不应暴露给用户空间的应用程序。因此,我们用 #ifdef _ _KERNEL_ _ 块来保护这些声明,这就是为什么驱动程序要象其它内核代码一样,必须在 定义了预处理符号 _ _KERNEL_ _ 的情况下编译。 每个内核头文件的作用将在本书中需要用到它们的时候加以介绍。 开发大型软件系统(比如内核)的程序员必须注意到并且尽力避免“名字空间污染”。当存在大量 的函数和全局变量,并且它们的名字没有明确的含义以至于很难区别时,就会发生所谓的名字空间 污染。当不得不面对这样的一个系统时,程序员需要花费更多的精力去记住这些已经“保留”的名 字并且为新符号寻找一个不重复的名字。名字空间冲突可能造成很多问题,例如模块装载失败,或 者一些古怪的问题,比如它可能只发生在使用代码的远程用户身上,而这个用户仅仅使用了一个不 同的配置选项集来编译内核。 13 第 2 章 构造和运行模块 在编写内核代码时,这种错误将是开发人员的恶梦,这是因为即使最小的模块也将要连接到整个内 核。防止名字空间污染的最好办法是将所有符号定义为静态变量,并且在表示全局变量的符号前加 上一个内核中唯一的前缀。还应该注意的是,作为一个模块编写者,我们可以控制是否导出符号被 外部使用,这一点将在本章后面“内核符号表”中讲到*。 为模块中的私有符号选择前缀也将是一个不错的习惯,因为这样可以简化调试。在测试驱动程序的 时候,就可以导出所有符号而不会污染名字空间。根据惯例,内核中使用的前缀都是小写的,我们 将遵循这个惯例。 内核编程和应用程序编程的最后一点不同之处在于各环境下处理错误的方式不同:应用程序开发过 程中段错误是无害的,并且总是可以使用调试器跟踪到源代码中的问题所在,而一个内核错误即使 不对整个系统是致命的,也至少会对当前进程造成致命错误。在第 4 章“调试系统错误”一节中, 我们将看到如何跟踪内核错误。 2.1.1 用户空间和内核空间 模块运行在所谓的内核空间里,而应用程序运行在所谓的用户空间中。这个概念是操作系统理论的 基础之一。 实际上,操作系统的作用是为应用程序提供一个对计算机硬件的一致视图。除此之外,操作系统必 须负责程序的独立操作以及保护资源不受非法访问。这个重要任务只有在 CPU 能够保护系统软件 不受应用程序破坏时才能完成。 所有的现代处理器都具备这个功能。人们选择的方法是在 CPU 中实现不同的操作模式(或者级 别)。不同的级具有不同功能,在较低的级别中将禁止某些操作。程序代码只能通过有限数目的“门” 来从一级切换到另一级。Unix 系统设计时利用了这些硬件特性,使用了两个这样的级。当前所有 的处理器都至少具有两个保护级,而象其它的一些处理器,比如 x86 系列,则有更多的级。当处理 器存在多个级时,Unix 使用了最高级和最低级。在 Unix 当中,内核运行在最高级(也称作管理 员态),在这级中可以进行所有操作。而应用运行在最低级(即所谓的用户态),在这级当中处理器 控制着对硬件的直接访问以及对内存的非授权访问。 我们通常将运行模式称做内核空间和用户空间。这两个术语不仅说明两种模式具有不同的优先权等 级,而且还说明每个模式都有自己的内存映射,也即自己的地址空间。 每当应用程序执行系统调用或者被硬件中断挂起时,Unix 将执行模式从用户空间切换到内核空间。 执行系统调用的内核代码运行在进程上下文中,它代表调用进程执行操作,因此能够访问进程地址 空间的所有数据;而处理硬件中断的内核代码和进程是异步的,与任何一个特定进程无关。 模块运行在内核空间中,扩展内核的功能。通常一个驱动程序模块既要执行系统调用,也要进行中 断处理。 * 如果模块中没有特定的指令,则大多数版本的 insmod (但不是所有)导出所有非静态符号。因此,如果我们不 想导出某些符号的话,则应该将其声明为静态变量。 14 Linux 设备驱动程序 2.1.2 内核中的并发 驱动程序编程区别于大部分应用程序编程的一个重要方面是并发问题的处理。应用程序通常从头到 尾顺序执行,不必担心发生其它情况而改变它的运行环境。而内核代码却不会运行在这样一个简单 环境中,它必须考虑到可能会同时发生很多事情。 有几方面的原因促使内核编程必须考虑并发问题。首先,Linux 系统中通常正在运行多个并发进程, 并且可能有多于一个的进程同时使用我们的驱动程序。其次,大多数设备能够中断处理器,而中断 处理程序异步运行,而且可能在我们的驱动程序正试图处理其他任务时被调用。另外,还有几个软 件抽象(比如第 6 章中谈到的内核定时器)也异步运行。最后,Linux 还可以运行在对称多处理 器(SMP)系统上,因此可能同时有不止一个 CPU 运行我们的驱动程序。 结果,Linux 内核代码,包括驱动程序代码,必须是可重入的,它必须能够同时运行在多个上下文 中。因此,内核数据结构需要仔细设计才能保证多个线程分开执行,代码访问共享数据时也必须避 免破坏共享数据。要编写能够处理并发问题同时可避免竞态(不同的执行顺序导致不同的,非预期 行为发生的情况)的代码,需要一些技巧和细致的思考。本书中的示例驱动程序在编写时都考虑到 了并发问题,在讲到时我们将具体介绍所使用的技术。 驱动程序编写人员所犯的一个常见错误是认为只要某段代码没有进入睡眠状态(或者阻塞),就不 会产生并发问题。的确,在过去大多数时间里,Linux 内核是非抢占式的(一个重要的例外是对中 断的服务,它不会从不愿放弃处理器的内核代码中抢占处理器)。过去,这种非抢占式行为可避免 大部分意想不到的并发问题,然而,在 SMP 系统中,不需要抢占性就可以导致并发执行。 如果我们编写的代码基于系统不是抢占式的这一假设,它就不能在 SMP 系统中正确运行。即使我 们自己没有 SMP 系统,其他运行我们程序的人也可能拥有 SMP 系统。而且,将来内核或许会 转变为抢占式运行,那么即使是单处理器系统也必须随时随地处理并发问题(一些内核变种已经实 现了抢占式)。因此,一个谨慎的程序员应该总是假设他的程序运行在 SMP 系统中。 2.1.3 当前进程 虽然内核模块不象应用程序那样顺序执行,然而内核当前执行的大多数操作还是和某个特定的进程 相关,这个特定进程就是当前进程。内核通过访问全局变量 current 获得当前进程。current 是一 个指向结构 task_struct 的指针,在内核 2.4 中 task_struct 在 中定义,并包含 在 头文件中。current 指针指向当前正在运行的用户进程,在 open、read 等系 统调用的执行过程中,当前进程指的是调用这些系统调用的进程。如果需要,内核代码可以通过 current 获得与当前进程相关的信息,在第 5 章“设备文件的访问控制”中将会介绍这样一个例 子。 实际上,与早期 Linux 内核版本不同,最近版本的内核中 current 不再是一个全局变量。内核开 发者将描述当前进程的结构隐藏在栈页(stack page)中,从而优化了对该结构的访问。读者可以 查阅 获得 current 的详细细节,尽管这些代码可能看起来有些凌乱,我们必须 记住 Linux 系统是一个 SMP 兼容的系统,全局变量在处理多 CPU 系统时并不能正常工作。然 而 current 的 具 体 实 现 细 节 对 内 核 其 它 子 系 统 是 透 明 的 , 驱 动 程 序 可 以 包 含 头 文 件 来引用当前进程。 15 第 2 章 构造和运行模块 从模块的角度来看,current 和 printk 一样,都是外部引用。模块在需要时总是可以引用 current。 例如,下面的语句通过访问 task_struct 结构的某些成员,打印当前进程的进程 ID 和命令名: printk("The process is \"%s\" (pid %i)\n", current->comm, current->pid); 存储在 current->comm 成员中的命令名是当前进程所执行的程序文件的基本名称(base name). 2.2 编译和装载 本章余下内容将讨论编写一个完整的,然而没有类型的驱动程序模块。也就是说,它不属于第 1 章 “设备和模块类型”中列出的任何一个模块类型。我们把这个示例模块称作 skull,即“Simple Kerne Utility for Loading Localities”的缩写。在去掉它的示例功能后,我们可以重用这个模块,装载自 己的本地代码到内核*。 在讨论 init_module 和 cleanup_module 作用之前,我们先编写一个 makefile 文件,用来构造 可以装入内核的目标代码。 首先,在包含任何头文件之前,我们必须在预处理器中定义 _ _KERNEL_ _ 符号。如前所述,没 有这个符号,模块不能使用内核头文件中针对内核的特殊内容。 另外一个重要的符号是 MODULE,它必须定义在包含 之前(除非驱动程序直 接嵌入内核)。本书中将不介绍直接连接入的模块,因而我们的示例程序中总是定义 MODULE。 如果是为一个 SMP 系统编译模块,我们还需要在包含内核头文件之前定义符号 _ _SMP_ _。在 版本 2.2 中,“多处理器或单处理器”是一个内核配置选项,因此在模块起始部分加上下面几行预 处理代码,就可以为多处理器系统定义符号 _ _SMP_ _: #include #ifdef CONFIG_SMP # define _ _SMP_ _ #endif 因为许多函数在头文件中定义为内嵌函数,所以模块编写者还需要对编译器指定 -O 选项。如果不 使用此优化选项,gcc 就不会展开内嵌函数。gcc 可以同时使用 -g 和 -O 编译选项,允许我们对 调试代码使用内嵌函数。* 因为内核中广泛使用内嵌函数,因此正确展开这些内嵌函数非常重要。 我们还需要参阅内核源码树中的 Documentation/Changes 文件,检查编译所模块使用的编译器和 内核使用的编译器是否匹配。尽管由不同的小组开发,但内核和编译器的开发工作却是同时进行的, 因此,它们其中之一改变了,另一种也得作出相应的调整,否则可能会引出问题。有些 Linux 发 * 我们在这里使用“本地(local)”一词,表示是个人对系统作出的修改。这种用法借鉴了 Unix 中 /usr/local 的 用法。 * 然而,注意不要使用比 -02 更高的优化级别。如果使用了这个选项,编译器将会把那些没有声明为内嵌函数的 函数当作内嵌函数来编译,这对内核代码将是一个问题,因为一些函数在被调用时需要使用一个标准的栈布局。 16 Linux 设备驱动程序 行版本所带的编译工具同内核相比比较新,因此不能用来编译内核模块。在这种情况下,Linux 发 行版本中通常带有一个专门用来编译内核的编译器软件包(通常叫做 kgcc)。 最后,为了避免一些令人讨厌的错误,我们建议使用 -Wall(显示所有警告信息)编译选项,并且 修改代码中所有能引起编译警告的编程习惯,即使这样做会改变我们一贯的编程风格。当编写内核 代码时,我们应该借鉴学习 Linus 本人的编程风格;如果我们还有志于研究内核代码,则必须首 先阅读 Documentation/CodingStyle,以了解内核的编程风格。 到目前为止,我们所介绍的符号定义和编译选项都包含在 make 命令使用的变量 CFLAGS 中。 除了需要合适的 CFLAGS 外,当模块源代码由几个源文件组成时,makefile 文件还需要一条规 则来连接多个目标文件。现实情况下模块大多由多个源文件组成,连接多个目标文件的命令是 ld -r,虽然它使用了连接器,但它并没有执行真正的连接操作。它集中所有输入的目标文件的目标代 码,输出另一个目标文件。选项 -r 的含义是“可重定位”:输出的目标文件中没有包含任何绝对 地址,因而是可重定位的。 下面的 makefile 文件展示了如何构造一个由两个源文件组成的模块。如果读者的模块仅由一个源 文件组成,则只需删去含有 ld –r 的那条规则即可。 # Change it here or specify it on the "make" command line KERNELDIR = /usr/src/linux include $(KERNELDIR)/.config CFLAGS = -D_ _KERNEL_ _ -DMODULE -I$(KERNELDIR)/include \ -O -Wall ifdef CONFIG_SMP CFLAGS += -D_ _SMP_ _ -DSMP endif all: skull.o skull.o: skull_init.o skull_clean.o $(LD) -r $^ -o $@ clean: 若读者不熟悉 make 命令,可能疑惑为什么上面的 makefile 文件中没有 .c 文件和编译规则。其 实,这些声明是不必要的,make 命令能够自动将 .c 文件编译成 .o 文件,并且使用当前(或默 认)的编译器 $(CC) 以及编译标记 $(CFLAGS)。 在构造模块之后,下一步就是装入内核。如前所述,insmod 为我们完成这项工作。insmod 程序 和 ld 有些类似,它使用运行内核的符号表解析模块中任何未解析的符号。然而,它和连接器还是 有些区别,因为它没有修改磁盘文件,而仅仅修改了内存中的副本。insmod 可以接受一些命令行 选项(参见它的手册页),并且可以在模块连接到内核之前给模块中的整型和字符串型变量赋值。 因此,一个良好设计的模块可以在装载时进行配置,这比编译时的配置为用户提供了更多的灵活性, 然而,有些情况下仍然要使用编译时的配置。本章后面的“自动和手动配置”一节中将会介绍装载 时配置。 感 兴 趣 的 读 者 可 能 想 知 道 内 核 是 如 何 支 持 insmod 工 作 的 , 实 际 上 它 依 赖 于 定 义 在 17 第 2 章 构造和运行模块 kernel/module.c 中的几个系统调用。函数 sys_create_module 给模块分配内核内存(函数 vmalloc 负 责 内 核 内 存 分 配 , 详 见 第 7 章 的 “ vmalloc 及 其 相 关 函 数 ”)。 系 统 调 用 get_kernel_syms 返回内核符号表,可用来解析模块中的内核引用。sys_init_module 拷贝可重定 位的模块目标代码到内核空间并且调用模块的初始化函数。 如果仔细阅读内核源码,我们会发现有且只有系统调用的名字前带有 sys_ 前缀,而其它任何函数 都没有这个前缀。这种命名上的区别使我们在源码中 grep 系统调用时非常方便。 2.2.1 版本依赖 当模块需要和不同版本的内核连接时,模块代码就需要重新编译。每个模块都定义了符号 _ _module_kernel_version,insmod 使用它检查模块和当前的内核版本是否匹配。这个符号位于 ELF 的 .modinfo 段,第 11 章中将会详细讲到。值得注意地是,这种检查版本匹配的机制仅仅 适用内核 2.2 和 2.4,内核 2.0 使用不同的方式实现了同样的目标。 只要包含头文件 ,编译器就会自动为我们定义这个符号。(这就是为什么前面的 hello.c 没有显式定义此符号的原因。)这也意味着如果模块由多个源文件组成,则我们只需在一个 源文件中包含 即可。(除非使用了 _ _NO_VERSION_ _,待会儿将会我们讲到 这一点。) 当模块和内核版本不匹配时,我们仍然可以通过指定 insmod 的 -f(“force,强制”)选项来强行 装入模块。然而这种操作是不安全的,可能失败,并且很难预测将会发生什么样的后果。例如,装 载过程可能出现符号不匹配现象,此时我们将得到一条出错消息;也可能由于内核内部发生了变化, 从而导致出现严重错误甚至系统 panic,这也是我们力图避免版本不匹配的一个重要原因。版本不 匹配问题可以通过使用内核的版本控制功能来更好地解决(版本控制功能是个更深层次的话题,将 在第 11 章“模块中的版本控制”中介绍)。 如果读者想为一个特定版本的内核编译模块,则必须在上面的 makeifle 文件中包含该版本内核特 有的头文件(例如,可以声明一个不同的 KERNELDIR)。这种情况在和内核源码打交道时将会很 常见,因为很多情况下我们可能具有多个版本的源码树。本书中的所有示例模块都使用 KERNELDIR 变量指向实际的内核源码,它可以在环境变量中设置或者通过 make 命令行指定。 在装入模块时,insmod 按照自己的搜索路径寻找模块的目标代码,它在 /lib/modules 目录下与内 核版本相关的子目录中寻找。老版本的 insmod 将首先在当前目录下寻找目标代码,而由于安全 方面的原因这一功能现在不再支持(PATH 环境变量中也存在类似的情况)。因此,如果需要从当 前目录中装载模块,则应该使用 ./module.o,它在 insmod 的所有版本中都能正常工作。 我们有时可能碰到在内核 2.0.x 和 2.4.x 之间表现不一致的内核接口。这种情况下,我们要求助 于定义当前内核版本号的宏,它们在头文件 中定义。为了简化 2.4 内核版本的 讨论,我们将在本章里,或者在关于版本依赖的的本章末尾一节里,专门指出接口改变的情况。 自动包含在 linux/module.h 中的头文件定义了下面一些宏: UTS_RELEASE 宏 UTS_RELEASE 扩展为一个描述内核版本的字符串,例如“2.3.48”。 18 Linux 设备驱动程序 LINUX_VERSION_CODE 宏 LINUX_VERSION_CODE 扩展为内核版本的二进制表示,版本发行号中的每一部分对应一个 字节。例如,2.3.48 对应的 LINUX_VERSION_CODE 是 131888(即 0x020330)。* 因此,使用这个宏我们很容易确定正在使用的内核版本。 KERNEL_VERSION(major,minor,release) 宏 KERNEL_VERSION 以组成版本号的三部分(三个整数)为参数,创建“kernel_version_code”。 例如,KERNEL_VERSION(2,3,48) 扩展为 131888。这个宏在我们需要将当前版本和一个已知的 检查点比较时非常有用,本书中将多次用到这个宏。 文件 version.h 包含在 module.h 中,因此我们通常不用显式包含 version.h 。另一方面,如果 我们预先定义 _ _NO_VERSION_ _,则 module.h 就不再包含 version.h。如果我们希望在组成 单个模块的多个源文件中包含 ,比如希望使用 module.h 中定义的预处理宏, 这时,就需要在包含 module.h 之前定义 _ _NO_VERSION_ _。在包含 module.h 之前声明 _ _NO_VERSION_ _ , 可 以 避 免 在 源 文 件 中 不 需 要 的 地 方 自 动 声 明 字 符 串 _ _module_kernel_version 或其他等价的符号(ld –r 将会给出符号多重定义的错误信息)。本书中 的示例模块就是出于这个目的使用 _ _NO_VERSION_ _。 预处理条件语句使用 KERNEL_VERSION 和 LINUX_VERSION_CODE 能够解决大部分基于内 核版本的依赖问题。然而,我们不应该胡乱使用 #ifdef 条件语句将整个驱动程序代码弄得杂乱无 章。最好的一个解决方法就是将所有相关的预处理条件语句集中存放在一个特定的头文件里。我们 的示例代码中就包含了这样一个头文件 sysdep.h,它用适当的宏定义隐藏了不兼容性。 我们碰到的第一个版本依赖问题是为驱动程序定义”make install”规则。我们能够猜到,驱动程 序的安装目录将根据内核版本的不同而不同,因此需要查找 version.h 来确定合适的安装目录。下 面的文件片断摘自 Rules.make,Rules.make 将包含在所有的 makefile 里面。 VERSIONFILE = $(INCLUDEDIR)/linux/version.h VERSION = $(shell awk -F\" '/REL/ {print $$2}' $(VERSIONFILE)) INSTALLDIR = /lib/modules/$(VERSION)/misc 我们选择将驱动程序安装在 misc 目录中,这是添加杂项驱动程序的一个不错的地方,同时还能够 避免 /lib/modules 下目录结构的改变带来的问题――这种变化在 2.4 版本内核发布之前刚刚引 入。尽管新目录结构变得更加复杂,但新老版本的 modutils 包都会使用 misc 目录。 如上所示定义好 INSTALLDIR 后,每个 makefile 的安装规则如下所示: install: install -d $(INSTALLDIR) install -c $(OBJS) $(INSTALLDIR) 2.2.2 平台依赖 每种计算机体系结构都有自己的独特特性,内核设计者可以充分利用这些特性来达到目标平台上目 标文件的最优性能。 * 因此,在两个稳定版本之间,最多可以存在 256 个开发版本。 19 第 2 章 构造和运行模块 对于应用程序开发人员,他们必须将程序代码和预编译过的库连接并且遵循参数传递规则。而内核 开发人员则不同,他们可以根据不同需求,将某些寄存器指定为特定用途――实际上他们也的确这 么做了。而且内核代码可以针对某个 CPU 家族的某种特定处理器进行优化,从而充分利用目标平 台的特性。和应用程序以二进制的形式发布不同,内核需要发布源码,针对目标平台定制编译后才 能达到对某个特定计算机集合的优化。 为了能够和内核互操作,模块代码在编译时需要使用和内核编译时一样的编译选项(例如,为特定 用途保留同样的寄存器并且进行同样的优化)。因此,顶层的 Rules.make 包含一个与平台相关的 文件,对 makefile 补充额外的定义。所有这些文件都叫做 Makefile.platform,并且根据当前的内 核配置给 make 变量赋值。 makefile 文件的这种布局使得它能够支持所有示例文件的交叉编译。当我们为目标平台进行交叉 编译的时候,需要另外一套编译工具(如 m68k-linux-gcc、m68-linux-ld 等等)来代替现在的编译 工具(比如 gcc、ld 等等)。交叉编译工具名字前使用的前缀由 $(CROSS_COMPILE) 表示,在 make 命令行或者环境变量中指定。 SPARC 结构需要 makefile 的特殊处理。运行在 SPARC64(SPARC V9)平台上的用户空间程 序和运行在 SPARC32(SPARC V8)平台上的用户空间进程具有同样的二进制代码,因此, SPARC64 上运行的默认编译器(如 gcc)产生 SPARC32 平台的目标代码。然而,内核必须运行 SPARC V9 目标代码,因此需要一个交叉编译器。所有为 SPARC64 平台发布的 GNU/Linux 版 本都包含有一个适当的交叉编译器,makefile 会选择这个交叉编译器。 版本和平台依赖性的问题清单可能比上述情况稍微复杂一些,然而上面的介绍以及所提供的 makefile 文件已经足以让我们继续进行后面的讨论。如果读者想了解更多的详细信息,可以参看 makefile 文件以及内核源码。 2.3 内核符号表 在上面的讨论中,我们了解了 insmod 使用公用内核符号表解析模块中未定义符号的原理。内核 公用符合表中包含了所有的全局内核符号(即函数和变量)的地址,实现驱动程序模块时,在很多 情况下都需要使用这些全局符号。公用符号表能够从文件 /proc/ksyms 中以文本格式读取(前提 是内核支持 /proc 文件系统)。 当模块被装入内核后,它所导出的任何符号都变成公用符号表的一部分,在 /proc/ksyms 或者 ksyms 命令的输出中我们能够看到这些新增加的符号。 新模块可以使用我们模块导出的符号,而且我们还可以在其它模块上层叠新模块。模块层叠技术也 使用在很多主流的内核代码中。例如 msdos 文件系统依赖于 fat 模块导出的符号;而每个 USB 输入设备模块层叠在 usbcore 和 input 模块之上。 模块层叠技术在复杂的项目中非常有用。如果以设备驱动程序的形式实现一个新的软件抽象,它可 以为硬件相关的实现提供一个插接口(plug)。例如,视频驱动程序可以分出一个通用模块,它导 出符号供下层与具体硬件相关的驱动程序使用。根据安装硬件的不同,我们加载通用视频模块以及 20 Linux 设备驱动程序 与具体硬件相关的模块。另外,并口支持,以及大量可挂接设备的处理,比如 USB 内核子系统, 都使用了类似的层叠方法。图 2-2 中给出了并口子系统的层叠方式,箭头显示了模块之间(带有一 些示例函数和数据结构)以及和内核编程接口之间的消息传输。 图 2-2:并行子系统的层叠方式 modprobe 是处理层叠模块的一个实用工具。它的功能在很大程度上和 insmod 类似,但是它除 了装入指定模块外,还同时装入指定模块所依赖的其它模块。因此,一个 modprobe 命令有时候 相当于调用几次 insmod 命令(然而,在从当前目录装入模块时,仍然需要使用 insmod,因为 modprobe 只能从已安装的模块树中搜索需要装入的模块)。 通过对每一层进行简化,分层的模块化编程缩短了开发时间。这种方法和我们在第 1 章中提到的 机制和策略的分离有点类似。 通常情况下,模块只需实现自己的功能,不必导出任何符号。然而,如果有其它模块需要使用我们 模块导出的符号时,我们就需要导出这些符号。另外,我们可能还得特别引用某些指令来避免导出 所有的非静态符号,因为在默认情况下,大部分版本的 modutils(但并非所有)将导出所有非静 态符号。 Linux 内核头文件提供了一个方便的方法来管理符号对模块外部的可见性,从而减少了可能造成的 名字空间污染并且适当隐藏信息。本节中描述的这种方法适用于 2.1.18 及其后的内核版本,2.0 版 本的内核拥有一套完全不同的机制,我们将在本章末尾进行描述。 如果不希望模块导出任何符号,则可以在源文件中添加如下一行宏调用来显式说明: EXPORT_NO_SYMBOLS; 这个宏将扩展为一条汇编指令,并且可以出现在模块的任何地方。然而,可移植代码应该将它放在 模块的初始化函数 init_module 中,这是因为文件 sysdep.h 中为老内核定义的这个宏只能在模块 的初始化函数中起作用。 如果我们准备导出模块中符号的一个子集,则首先需要定义预处理宏 EXPORT_SYMTAB,而且 这个宏必须在包含 module.h 之前定义。一种很常见的定义方法是在模块编译时在 Makefile 中使 用 -D 编译选项指定。 在定义了 EXPORT_SYMTAB 之后,可以通过下面两个宏之一来导出模块的一个符号: 21 第 2 章 构造和运行模块 EXPORT_SYMBOL(name); EXPORT_SYMBOL_NOVERS(name); 这两个宏都可以用来导出符号,不过第二个宏(EXPORT_SYMBOL_NOVERS)导出的符号不带 版本控制信息(第 11 章将讨论版本控制)。符号导出必须位于任何函数的外部,因为这些宏扩展 为一个变量声明。(感兴趣的读者可以查阅 获得更详细的信息。) 2.4 初始化和关闭 前面已经提到,函数 init_module 负责注册模块所提供的任何设施。这里的设施指的是一个可以被 应用程序访问的新功能,它可能是一个完整的驱动程序或者仅仅是一个新的软件抽象。 模块可以注册许多不同类型的设施。对于每个设施都会有相应的内核函数完成注册工作。传给内核 注册函数的参数通常包括:一个指向描述这个新设施的数据结构的指针以及要注册的设施的名称。 描述设施的数据结构中通常包含有指向模块函数的指针,因此我们可以调用模块中的函数。 能够注册的设施类型超出了在第 1 章中给出的设备类型列表,它们包括串口、杂项设备、/proc 文 件、可执行域以及线路规程(line discipline)等。很多可注册的设施所支持的功能属于“软件抽象” 范畴,而不与任何硬件直接相关。这种类型的设施能够被注册,是因为它们能够以某种方式集成到 驱动程序功能当中(如 /proc 文件系统以及线路规程)。 还有其它一些设施可以注册为特定设备的附加功能,但是它们的用途有限因而不在这里具体讨论。 它们使用在前面“内核符号表”中提到的层叠技术。如果读者想进一步了解,可以在内核源文件中 grep EXPORT_SYMBOL,并找出由不同驱动程序提供的入口点。另外,大部分注册函数名字带有 register_ 前缀,因此找到它们的另一种方法是在 /proc/ksyms 中 grep register_。 2.4.1 init_module 中的出错处理 设施的注册过程中如果出现任何错误,都要将出错之前的注册工作撤销。下面几种情况下都可能发 生错误,例如,系统中没有足够的内存分配给一个数据结构,或者,请求的资源正在被其它驱动程 序使用等。虽然错误不是经常发生,但它仍然有可能发生,因此我们必须做好处理这些错误的准备。 Linux 中没有记录每个模块都注册了哪些设施,因此模块必须自己备份每步操作,防止 init_module 在某步出错。如果由于某种原因我们未能撤销已经注册的设施,则内核会处于一种不稳定状态:一 方面,这些设施处于忙的状态,我们不能重新装入模块来再次注册设施;另一方面,我们也不能撤 销对它们的注册,因为我们失去了注册这些设施时使用的指向描述设施的数据结构的指针。想从这 种处境中恢复比较困难,通常需要重启机器并装入修改后的新模块。 错误恢复的处理有时使用 goto 语句比较有效。通常情况下我们很少使用 goto,但在处理错误时 (可能是唯一的情况)它却非常有用。下面的例子给出了内核中使用 goto 语句处理错误的方式。 不管初始化过程在什么时刻失败,下面的例子(使用了虚构的注册和撤销注册函数)都能正确工作。 int init_module(void) 22 Linux 设备驱动程序 { int err; /* registration takes a pointer and a name */ err = register_this(ptr1, "skull"); if (err) goto fail_this; err = register_that(ptr2, "skull"); if (err) goto fail_that; err = register_those(ptr3, "skull"); if (err) goto fail_those; return 0; /* success */ fail_those: unregister_that(ptr2, "skull"); fail_that: unregister_this(ptr1, "skull"); fail_this: return err; /* propagate the error */ } 这段代码准备注册三个(虚构的)设施。在出错的时候调用 goto 语句,它将只撤销出错时刻以前 所成功注册的那些设施。 错误处理的另一种方法不需要使用杂乱的 goto 语句。它记录任何成功注册的设施,然后在出错的 时候调用 cleanup_module。这个函数将仅仅回滚成功完成的步骤。然而,这种替代方法需要更多 的代码和 CPU 时间,因此在追求效率的代码中仍然使用 goto 语句作为最好的错误恢复机制。 init_module 的返回值 err 是一个错误编码。在 Linux 内核中,错误编码是定义在 中的一个负整数集合。如果我们不想使用从其它函数返回的错误编码而是自己产生的错误码,则应 该包含 ,使用诸如 -ENODEV、-ENOMEM 之类的符号值。每次返回合适的错误 编码将是一个好习惯,因为用户程序可以通过 perror 或类似程序将它们转换为有意义的字符串。 (然而,有趣的是,有几个版本的 modutils 对 init_module 返回的任何错误码都转换为“设备忙”, 不过这个问题在最近发布的版本中得以纠正。) 显然,cleanup_module 需要撤销 init_module 注册的所有设施,并且习惯上(但不是必须的)以 相反于注册的顺序撤销设施。 void cleanup_module(void) { unregister_those(ptr3, "skull"); unregister_that(ptr2, "skull"); unregister_this(ptr1, "skull"); return; } 如果初始化和清除工作涉及很多设施,则 goto 方法可能变得难以管理。因为所有用于清除设施的 代码在 init_module 中重复,同时标号交织在一起。因此,有时候需要考虑重新构思代码结构。 每当发生一个错误时,init_module 就调用 cleanup_module,这种方法将减少代码重复并且使代 码有条理,而清除函数必须在撤销每项设施的注册之前检查它的状态。下面是这种方法的简单示例: struct something *item1; struct somethingelse *item2; int stuff_ok; void cleanup_module(void) { if (item1) 23 第 2 章 构造和运行模块 release_thing(item1); if (item2) release_thing2(item2); if (stuff_ok) unregister_stuff(); return; } int init_module(void) { int err = -ENOMEM; item1 = allocate_thing(arguments); item2 = allocate_thing2(arguments2); if (!item2 || !item2) goto fail; err = register_stuff(item1, item2); if (!err) stuff_ok = 1; else goto fail; return 0; /* success */ fail: cleanup_module(); return err; } 如这段代码所示,根据调用的注册/分配函数语义,我们可以使用或不使用外部标号来标记每个初 始化步骤的成功。不管是否使用标记,这种方式的初始化能够很好地扩展到对大量设施的支持,因 此比前面介绍的技术更具优越性。 2.4.2 使用计数 为了确定模块是否能够安全卸载,系统为每个模块保留一个使用计数。因为在模块忙时不能被卸载, 所以系统需要这个信息确定模块是否忙。例如,当一个文件系统已被安装时,我们就不能卸载它; 当进程正在使用一个字符设备时我们也不能卸载这个字符设备,否则,当我们使用无效的指针时, 可能会遇到某种类型的段错误甚至是系统的崩溃。 在比较新的内核版本中,系统能够自动跟踪使用计数,下一章中我们会描述这种机制。然而,有些 时候仍然需要自己手动去调整使用计数。需要向老版本内核中移植的代码也必须手动维持使用计 数。手动进行使用计数时需要使用下面三个宏: MOD_INC_USE_COUNT 当前模块计数加 1。 MOD_DEC_USE_COUNT 当前模块计数减 1。 MOD_IN_USE 计数非 0 时返回真。 这些宏定义在 中,操作那些不能由程序员直接访问的内部数据结构。虽然模块 管理的内部机制在 2.1 开发过程中改变很大,并且在 2.1.18 版本中完全重写,然而这三个宏的 使用方式并没有改变。 24 Linux 设备驱动程序 注意在 cleanup_module 函数内部不必检查 MOD_IN_USE,因为系统调用 sys_delete_module (定义在 kernel/module.c 中)时预先执行了这种检查。 模块计数管理对系统的稳定性非常重要。因为内核能在任何时刻卸载模块,一个常见的模块编程错 误就是在启动了一系列操作后(例如,响应一个 open 请求),在模块的末尾才增加使用计数。这 时,如果内核在那些操作的中途卸载这个模块,则就会产生混乱。为了避免这种类型错误的发生, 我们应该在模块几乎还没有做任何事情之前就调用 MOD_INC_USE_COUNT。 如果失去了对某个模块使用计数的跟踪,就无法卸载这个模块。这种情况在模块开发过程中经常发 生,我们应当十分小心。例如,如果驱动程序使用了一个空指针指向的内容,就可能导致调用进程 的崩溃,此时驱动程序不能关闭对应的设备,而引用计数无法减小为 0。一个可能的解决办法是在 调试阶段重定义 MOD_INC_USE_COUNT 和 MOD_DEC_USE_COUNT 为空,从而取消引用计 数;另外一种解决方案是使用某种方法将引用计数器强行置 0(在第 5 章“ 使用 ioctl 参数”一节 中我们将会看到这种方法)。虽然在一个正式发布的模块产品中不能省略正确的错误检查机制,然 而,在调试阶段使用“蛮力”,却能够缩短开发时间,因而不失为一种可取的方法。 /proc/module 文件中每项的第三个域给出了引用计数的当前值。这个文件列出了系统中当前加载 的所有模块,它包含若干个项,每一项对应一个模块;每项包含若干个域,分别表示模块名、模块 使用内存的字节数、模块的当前使用计数等。下面是一个典型的 /proc/modules 文件: parport_pc 7604 lp 4800 parport 8084 lockd 33256 sunrpc 56612 ds 6252 i82365 22304 pcmcia_core 1280 1 (autoclean) 0 (unused) 1 [parport_probe parport_pc lp] 1 (autoclean) 1 (autoclean) [lockd] 1 1 0 [ds i82365] 这儿我们看到了系统中装入的几个模块。如图 2-2 所示,并口模块以层叠方式装入系统。autoclean 标记表示模块由 kmod 或 kerneld (见第 12 章)管理;而 unused 标记的意思和字面意思完全 一样,即模块尚未被使用,除这两个标记外,还存在其它一些标记。在 Linux 2.0 当中,第二个域 (size)是以页(大多数平台上每页大小为 4 KB)而不是以字节为单位表示的大小。 2.4.3 卸载 使用 rmmod 可以卸载一个模块。卸载模块不像装入模块那样需要进行连接操作,因而任务比较简 单。rmmod 命令调用系统调用 delete_module,这个系统调用随后检查模块引用计数,为 0 时调 用模块本身的 cleanup_module 函数,否则返回出错信息。 模块所注册的每一项都需要函数 cleanup_module 进行注销,只有模块导出的符号可以自动从内 核符号表中删除。 2.4.4 显式的初始化和清除函数 如上所述,内核调用 init_module 来初始化一个刚刚加载的模块,并在模块即将卸载之前调用 cleanup_module 函数。然而,较新的内核中通常给这两个函数重新命名。从 2.3.13 内核版本开始, 25 第 2 章 构造和运行模块 增加了一项设施来显式命名初始化和清除函数,使用这项设施是一种更好的编程风格。 下面举个例子。如果我们将模块初始化函数命名为 my_init(而不是 init_module),将清除函数命名 为 my_cleanup,则可以使用下面两行来进行标记(通常在源文件末尾): module_init(my_init); module_exit(my_cleanup); 注意,要使用 module_init 和 module_exit,代码必须包含头文件 。 这样做的好处是内核中每个初始化和清除函数都有一个唯一的名字,因而给调试带来了方便;同时 也使那些既可以作为一个模块也可以直接嵌入内核的驱动程序更加容易编写。然而,如果我们仍然 使用老的初始化和清除函数的名字,则不需要使用函数 module_init 和 module_exit。实际上,这 两个函数对模块所做的唯一事情就是将 init_module 和 cleanup_module 定义为给定函数的名 字。 如果深入研究一下内核源码(版本 2.2 及其以后),我们会发现模块初始化函数和清除函数的原型 略有不同,如下所示: static int _ _init my_init(void) { .... } static void _ _exit my_cleanup(void) { .... } 象这样使用属性 _ _init,它会在初始化工作完成后,丢弃初始化函数并且回收它所占内存。然而, 它仅仅对直接嵌入内核的驱动程序有效,对驱动程序模块则没有作用。相反,当 _ _exit 用在直接 嵌入内核的驱动程序中时,它将忽略所标记的函数;而用在模块时则没有任何作用。 使用 _ _init(如果是数据条目,就是 _ _initdata)能够减少内核使用的内存。在模块初始化函数 前标记 _ _init 虽然在现在没有什么好处,但也没有任何坏处。而且尽管内核当前对模块初始化阶 段没有进行管理,但以后可能会在这方面加强。 2.5 使用资源 如果模块不使用内存、I/O 断口、I/O 内存以及中断线等系统资源,就不能完成自己的任务。如果 我们用的是一个老式的 DMA 控制器(例如 ISA 总线的 DMA 控制器),则还需要 DMA 通道。 作为程序员,我们可能已经习惯于管理内存分配;在这方面,编写内核代码并没有任何区别。我们 的程序需要使用 kmalloc 获得一块内存区,然后使用 kfree 释放。这两个函数和 malloc 以及 free 功能基本类似,不过 kmalloc 带有一个额外的参数 priority。通常使用 GFP_KERNEL 或者 GFP_USER 作为 priority 参数值,缩写 GFP 代表“get free page,获得空闲页面。”(第 7 章将 详述有关内存分配的问题。) 26 Linux 设备驱动程序 驱动程序新手刚开始可能对需要自己来分配 I/O 断口、I/O 内存*以及中断线等等感到奇怪。毕竟, 内核模块可以只访问这些资源而不用告诉操作系统。然而,尽管系统内存是匿名的并且可以从任何 地方开始分配,但 I/O 内存、端口以及中断等却都有各自特定的作用。例如,驱动程序可能需要 请求分配某个特定端口,而不是随便一个端口。然而驱动程序不能仅仅只是请求这些系统资源,它 应该首先弄清楚这些资源是否已在其它地方使用。 2.5.1 I/O 端口和 I/O 内存 多数情况下,一个典型驱动程序的任务是读写 I/O 端口和 I/O 内存。在初始化阶段和正常运行时, 驱动程序都可能访问 I/O 端口和 I/O 内存(统称 I/O 区域)。 然而,不幸地是,并不是所有的总线结构都能够清楚地区分每种设备所属的 I/O 区域。有些时候, 驱动程序需要猜测它的 I/O 区域在哪儿,甚至需要通过读写可能的地址范围来探测设备。ISA 总 线中就存在这个问题,并且它仍然使用在充满简单设备的个人计算机中,在工业领域它的 PC/104 实现也依然非常流行(参看第 15 章的 PC/104 和 PC/104+)。 尽管一些总线中存在这些的特性(或者缺少这些特性),设备驱动程序仍然必须保证它对 I/O 区域 的独占式访问,以防止其它驱动程序干扰。例如,如果一个正在探测硬件的驱动程序碰巧将数据写 到属于另一个设备的 I/O 端口,则肯定会发生问题。 为了避免这种不同设备之间的冲突,Linux 开发者实现了 I/O 区域的请求/释放机制。这种机制在 I/O 端口中早已使用,最近才推广到整个资源分配的管理。注意这种机制仅仅是一个帮助系统管理 资源的软件抽象,并不要求硬件支持。例如,如果非授权访问 I/O 端口,并不会产生类似于“段 错误“的错误状态,因为硬件并不强制要求对端口进行注册。 文件 /proc/ioports 和 /proc/iomem 以文本形式列出了已注册的资源。/proc/iomem 是在 2.3 版本 开发过程中才刚刚引入的。我们先讨论 2.4 版本,将可移植性问题留在本章末尾讨论。 端口 一台运行 2.4 版本内核的 PC 机上一个典型的 /proc/ioports 如下所示: 0000-001f : dma1 0020-003f : pic1 0040-005f : timer 0060-006f : keyboard 0080-008f : dma page reg 00a0-00bf : pic2 00c0-00df : dma2 00f0-00ff : fpu 0170-0177 : ide1 01f0-01f7 : ide0 02f8-02ff : serial(set) 0300-031f : NE2000 0376-0376 : ide1 03c0-03df : vga+ 03f6-03f6 : ide0 03f8-03ff : serial(set) * 位于外围设备上的内存区域通常称作 I/O 内存,以和系统 RAM 相区别。后者一般就叫做内存。 27 第 2 章 构造和运行模块 1000-103f : Intel Corporation 82371AB PIIX4 ACPI 1000-1003 : acpi 1004-1005 : acpi 1008-100b : acpi 100c-100f : acpi 1100-110f : Intel Corporation 82371AB PIIX4 IDE 1300-131f : pcnet_cs 1400-141f : Intel Corporation 82371AB PIIX4 ACPI 1800-18ff : PCI CardBus #02 1c00-1cff : PCI CardBus #04 5800-581f : Intel Corporation 82371AB PIIX4 USB d000-dfff : PCI Bus #01 d000-d0ff : ATI Technologies Inc 3D Rage LT Pro AGP-133 文件中的每一项表示(以 16 进制形式)一个被某个驱动程序锁定或者属于某个硬件设备的端口范 围。早期版本的内核中这个文件具有同样的格式,但是没有上面的由缩进表示的“分层”结构。 当系统增添一个新设备并且需要通过跳线来选择一个 I/O 范围时,这个文件可以用来避免端口冲 突:用户检查已经使用的端口,然后设置新设备使用一个空闲的 I/O 范围。尽管大部分现在的硬 件不再使用跳线,这种方法在处理定制设备或者工业部件时仍然非常有用。 隐藏在文件 ioports 后面的数据结构比文件本身更重要。当设备驱动程序初始化的时候,它能够知 道哪些端口范围已经使用。如果需要检测 I/O 端口来探测新设备,它能够避开检测那些被其它驱 动程序使用的端口。 ISA 探测实际上不是很安全。Linux 正式发行版本中的几个驱动程序在以模块的方式加载后,不再 执行探测任务,也不再检测那些可能被某个未知设备使用的端口,从而避免破坏系统运行。幸运的 是,新式总线结构(以及那些经过改良的老式总线结构)中不存在这些问题。 访问 I/O 注册表的编程接口由下面三个函数组成: int check_region(unsigned long start, unsigned long len); struct resource *request_region(unsigned long start, unsigned long len, char *name); void release_region(unsigned long start, unsigned long len); check_region 用来检查是否可以分配某个端口范围,如果不可以则返回一个负的错误编码(例如 -EBUSY 或者 -EINVAL)。request_region 完成真正的端口范围分配,分配成功则返回一个非空 指针。驱动程序并不需要使用或者保存这个返回的指针,我们只是检查它是否非空即可。* 如果代码仅仅在 2.4 内核中运行,则根本不需要调用 check_region。而且实际上这种情况下也最 好不要调用它,因为在调用 check_region 和 request_region 之间,情况可能发生改变。然而, 如 果 我 们 的 代 码 需 要 向 老 的 内 核 移 植 , 则 必 须 使 用 check_region , 因 为 在 2.4 版 本 之 前 request_region 返回 void。最后,驱动程序在完成任务之后还需要调用 release_region 释放分配 的端口。 这三个函数实际上都是宏,定义在 中。 下面的代码以及我们的驱动程序例子中都给出了注册端口的典型顺序。(因为包含与具体设备相关 * 只有一种情况下会使用这个实际指针,这就是当这个函数被内核资源管理子系统内部调用时。 28 Linux 设备驱动程序 的代码,这里没有给出函数 skull_probe_hw 的定义。) #include #include static int skull_detect(unsigned int port, unsigned int range) { int err; if ((err = check_region(port,range)) < 0) return err; /* busy */ if (skull_probe_hw(port,range) != 0) return -ENODEV; /* not found */ request_region(port,range,"skull"); /* "Can't fail" */ return 0; } 这段代码首先检查请求的端口范围是否可用,如果不能被分配,就没有必要继续探测硬件设备。实 际的端口分配是在检测到硬件之后发生的。对 request_region 的调用从来不会失败;因为内核一 次只装载一个模块,因此不可能出现其它的模块插进来取走已请求端口的情况。如果怀疑这一点, 我们可以编写代码进行测试,但是应当记住 2.4 内核之前 request_region 返回 void。 驱动程序分配的任何 I/O 端口最终都必须释放,skull 在函数 cleanup_module 内部完成这个任 务: static void skull_release(unsigned int port, unsigned int range) { release_region(port,range); } 资源的请求/释放方式同前面提到的设施的注册/注销的顺序非常相似,并且同样可以使用前面提到 的基于 goto 语句的出错处理机制。 内存 同 I/O 端口的情况类似,I/O 内存的信息可从文件 /proc/iomem 中获得。下面是一台个人微机上 这个文件的部分内容: 00000000-0009fbff : System RAM 0009fc00-0009ffff : reserved 000a0000-000bffff : Video RAM area 000c0000-000c7fff : Video ROM 000f0000-000fffff : System ROM 00100000-03feffff : System RAM 00100000-0022c557 : Kernel code 0022c558-0024455f : Kernel data 20000000-2fffffff : Intel Corporation 440BX/ZX - 82443BX/ZX Host bridge 68000000-68000fff : Texas Instruments PCI1225 68001000-68001fff : Texas Instruments PCI1225 (#2) e0000000-e3ffffff : PCI Bus #01 e4000000-e7ffffff : PCI Bus #01 e4000000-e4ffffff : ATI Technologies Inc 3D Rage LT Pro AGP-133 e6000000-e6000fff : ATI Technologies Inc 3D Rage LT Pro AGP-133 fffc0000-ffffffff : reserved 同样,这里列出的值是十六进制表示的范围,在冒号后的字符串是该 I/O 区域“所有者”的名字。 就驱动程序编程而言,I/O 内存的访问方式同 I/O 端口的访问方式相同,因为实际上它们都基于 29 第 2 章 构造和运行模块 同样的内部机制。 驱动程序使用下列函数调用来获得和释放对某个 I/O 内存区域的访问。 int check_mem_region(unsigned long start, unsigned long len); int request_mem_region(unsigned long start, unsigned long len, char *name); int release_mem_region(unsigned long start, unsigned long len); 典型的驱动程序通常事先知道它的 I/O 内存范围,因此相对于前面 I/O 端口的请求方法,对 I/O 内存的请求缩减为如下几行代码: if (check_mem_region(mem_addr, mem_size)) { printk("drivername: memory already in use\n"); return -EBUSY; } request_mem_region(mem_addr, mem_size, "drivername"); 2.5.2 Linux 2.4 中的资源分配 目前的资源分配机制是在 Linux 2.3.11 中引入的,它提供了一种灵活的控制系统资源的方法,本 节将简述这种机制。然而,基本的资源分配函数(request_region 及其它)在系统中仍然保留它 们的实现(以宏的方式)并且广泛使用,因为利用它们可保持和以前版本的向后兼容性。多数模块 开发人员无需知道幕后究竟发生了什么,但是需要开发复杂驱动程序的程序员还是需要了解一点。 Linux 的资源管理以一种分层结构管理任意的资源。全局性的资源(例如 I/O 端口范围)可以分 割为小一些的子集,比如是和某个具体总线插槽相关联的资源。驱动程序还可以根据需要进一步细 分属于自己的资源子集。 资源范围由在 中定义的资源结构来描述: struct resource { const char *name; unsigned long start, end; unsigned long flags; struct resource *parent, *sibling, *child; }; 顶层的(根)资源在系统启动时创建。例如下面的代码给出了一个描述 I/O 范围的资源结构的创 建过程: struct resource ioport_resource = { "PCI IO", 0x0000, IO_SPACE_LIMIT, IORESOURCE_IO }; 因此,我们知道资源名称是 PCI IO,包含从 0 到 IO_SPACE_LIMIT 的范围。根据底层硬件平台 的不同,IO_SPACE_LIMIT 可能是 oxffff(16 位的地址空间,例如在 x86、IA-64、M68k 和 MIPS 平台上)、oxffffffff(32 位:SPARC、PPC、SH)或者 oxffffffffffffffff(64 位:SPARC64)。 函数 allocate_resource 可以创建一个给定资源的子集。例如,在 PCI 初始化期间可以为实际分 配给物理设备的区域创建一个新的资源。当 PCI 代码读取这些端口或内存时,它就为那些区域创 建一个新资源,然后在 ioport_resource 或者 iomem_resource 中分配。 30 Linux 设备驱动程序 驱动程序因此可以请求某个资源的一个子集(实际上是一个全局资源的子集)并且调用 _ _request_region 将它标记为忙,_ _request_region 返回的指针指向描述所请求资源的资源数据 结构(出错情况下则返回空指针)。这个数据结构是全局资源树的一部分,因此驱动程序需要小心 使用。 感兴趣的读者可以浏览 kernel/resources.c 的源码或者查阅内核其它部分使用资源管理的机制来 获得细节信息。然而,对于大多数驱动程序编程人员来说,前面一节中讲到的 request_region 和 其它函数已经足够使用了。 这种分层机制带来了两个好处。其中之一 I/O 结构隐藏在内核的数据结构内部。例如,/proc/ioports 中显示的结果: e800-e8ff : Adaptec AHA-2940U2/W / 7890 e800-e8be : aic7xxx e800-e8ff 分配给了适配卡,它在 PCI 总线驱动程序中标识自己。接着 aic7xxx 驱动程序请求获 得那个 I/O 范围的大部分,即适配卡中端口实际对应的部分。 这种资源管理方式的另一个好处是,它将端口空间分割成与底层硬件对应的端口子集。因为资源分 配器不允许跨子集分配端口,因此它可以防止带有 bug 的驱动程序(或者一个正在探测系统中根 本不存在的硬件的驱动程序)获得属于不同范围的端口,即使这些端口范围此时尚未全部分配。 2.6 自动和手动配置 驱动程序需要了解的几个参数随着系统的不同而不同。例如,驱动程序必须知道硬件实际的 I/O 地 址或者内存范围(只有 ISA 设备是这样,对于设计良好的总线接口这可能不是问题)。有时候我们 还需要传递参数给驱动程序,帮助它找到对应的硬件或者激活/禁止某些特性。 根据设备的不同,除了 I/O 地址外可能还有其它参数影响驱动程序特性,例如设备牌号和发行版 本号等。驱动程序要想正确运行必须知道这些参数值。然而,给驱动程序正确设置参数值(即配置 驱动程序)是个比较困难的工作,需要在驱动程序初始化阶段完成。 总的说来,驱动程序有两种获得正确参数值的方法:一种方法是由用户显式指定,另一种方法是驱 动程序自动检测。自动检测毫无疑问是最好的驱动配置方法,然而用户手动配置却更容易实现。对 驱动程序开发人员来说,一种折中的方法是尽可能使用自动配置,然而允许用户手动配置并覆盖自 动检测的参数值。这样做的另外一个好处是我们在驱动程序开发初始阶段不必使用自动检测,而在 装入驱动程序模块的时候手动指定参数值;而在开发后期实现自动检测。 许多驱动程序还有一些配置选项来控制其操作的其它特征。例如,SCSI 适配器驱动程序通常会带 有选项来控制标签命令排队的使用,而 IDE 驱动程序允许用户控制 DMA 操作。因此,即使驱动 程序完全依靠自动检测来定位硬件,我们仍然需要给用户提供其它配置选项。 参数值可由 insmod 或者 modprobe 在装载模块时设置,后者还可以从配置文件(通常是 /etc/modules.conf )中获得参数赋值。这些命令能够在命令行中接受整型和字符串型赋值。因此, 如果模块需要获得一个叫做 skull_ival 的整型参数和一个叫做 skull_sval 的字符串型参数,我们 31 第 2 章 构造和运行模块 可以在模块装载时以下面的方式使用 insmod 命令设置参数: insmod skull skull_ival=666 skull_sval="the beast" 然而,在 insmod 能够改变模块参数之前,模块必须能够访问这些参数。参数由定义在 module.h 中的宏 MODULE_PARM 声明。MODULE_PARM 必须带两个参数:变量名和描述变量类型的字 符串。这个宏应位于任何函数之外,通常放在源文件的起始部分。上面提到的两个参数可用下面几 行来声明: int skull_ival=0; char *skull_sval; MODULE_PARM (skull_ival, "i"); MODULE_PARM (skull_sval, "s"); 目前模块参数只支持五种类型:b,字节(byte);h,短整型(short,两字节);i,整型(integer); l,长整型(long);s,字符串(string)。如果是字符串值,则需要声明一个指针变量。insmod 负 责为用户提供的参数分配内存并设置相应变量。类型前面的整数表明这是一个指定长度的数组,被 间隔符分开的两个数字指明了数组元素数目的最大最小值。如果我们想了解设计者对这个特征的详 细描述,可以参考头文件 。 作为一个例子,至少有两个元素、至多不超过 4 个元素的数组可定义为: int skull_array[4]; MODULE_PARM (skull_array, "2-4i"); 还有一个名为 MODULE_PARM_DESC 的宏,它让模块开发者为模块参数提供描述性文字。这段 描述存储在目标文件中,能够用类似 objdump 的工具查看,也可用自动的系统管理工具来显示。 例如: int base_port = 0x300; MODULE_PARM (base_port, "i"); MODULE_PARM_DESC (base_port, "The base I/O port (default 0x300)"); 所有模块参数都应该赋予一个默认值,用户可以使用 insmod 来显式改变。模块通过和默认值比 较来确定显式参数值。因此,可以这样设计模块自动配置:如果配置变量具有默认值则执行自动检 测,否则保持当前值。要想让这种方法生效,默认值应该是用户在装载模块时从来不会使用的参数 值。 下面的代码演示了 skull 如何自动检测设备的端口范围。这个例子中,使用自动检测探测多个设备, 而手动配置只限于单个设备。首先使用函数 skull_detect 探测“端口”,然后使用 skull_init_board 进行设备特有的初始化工作,这两个函数在这里没有给出。 /* * port ranges: the device can reside between * 0x280 and 0x300, in steps of 0x10. It uses 0x10 ports. */ #define SKULL_PORT_FLOOR 0x280 #define SKULL_PORT_CEIL 0x300 #define SKULL_PORT_RANGE 0x010 32 Linux 设备驱动程序 /* * the following function performs autodetection, unless a specific * value was assigned by insmod to "skull_port_base" */ static int skull_port_base=0; /* 0 forces autodetection */ MODULE_PARM (skull_port_base, "i"); MODULE_PARM_DESC (skull_port_base, "Base I/O port for skull"); static int skull_find_hw(void) /* returns the # of devices */ { /* base is either the load-time value or the first trial */ int base = skull_port_base ? skull_port_base : SKULL_PORT_FLOOR; int result = 0; /* loop one time if value assigned; try them all if autodetecting */ do { if (skull_detect(base, SKULL_PORT_RANGE) == 0) { skull_init_board(base); result++; } base += SKULL_PORT_RANGE; /* prepare for next trial */ } while (skull_port_base == 0 && base < SKULL_PORT_CEIL); return result; } 如果仅仅在驱动程序中使用配置变量(没有导出到内核符号表中),驱动编写者可以省略变量名前 的前缀(本例中是 skull_),因而方便模块用户的使用。除了要多敲几个字母外,前缀对用户通常 没有其它影响。 为了完整性起见,我们继续介绍其它三个在目标文件中增加说明文档的宏: MODULE_AUTHOR(name) 将模块作者名加入目标文件 MODULE_DESCRIPTION(desc) 在目标文件中增加模块描述文字 MODULE_SUPPORTED_EDVICE(dev) 描述模块所支持的设备。内核中的注释指明这个参数可能最终用来帮助模块自动装载,然而目前还 没有起到这种作用。 2.7 在用户空间编写驱动程序 首次接触内核的 Unix 程序员可能对编写模块比较紧张,然而编写用户空间程序来直接对设备端口 进行读写就容易多了。 相对于内核空间编程,用户空间编程具有自己的一些优点。有时候编写一个所谓的用户空间驱动程 序是替代内核空间驱动程序的一个不错的方法。 用户空间驱动程序的优点可以归纳如下: 33 第 2 章 构造和运行模块 „ 可以和整个 C 库连接。驱动程序不用借助外部程序(即前面提到的和驱动程序一起发行的用 于提供策略的用户程序)就可以完成许多非常规任务。 „ 可以使用通常的调试器调试驱动程序代码,而不用费力地调试运行内核。 „ 如果用户空间驱动程序挂起,则简单地杀掉它就行了。驱动程序带来的问题不会挂起整个系统, 除非驱动的硬件已经发生严重故障。 „ 和内核内存不同地是,用户内存可以换出。如果驱动程序很大但是不经常使用,则除了正在使 用的时候之外,不会占用内存。 „ 良好设计的驱动程序仍然支持对设备的并发访问。 X 服务器是用户空间驱动程序的一个例子。它十分清楚硬件可以做什么,不可以做什么,并且为 所有的 X 客户提供图形资源。然而,值得注意的是目前基于帧缓冲区(frame-buffer)的图形环境 正在慢慢成为发展趋势。这种环境下对于实际的图形操作,X 服务器仅仅是一个基于真正内核空 间驱动程序的服务器。 另外一个用户空间驱动程序的例子是 gpm 鼠标服务器。它在多个客户端之间分发鼠标事件,因此 和鼠标相关的应用可以同时跑在不同的虚拟控制台上。 然而,有些时候用户空间驱动程序将设备的访问权只授与一个程序,这就是 libsvga 库工作的原理。 它和应用程序连接,将一个 TTY 转变为一个图形显示设备,因此在不借助于中央控制服务(如服 务器)的情况下扩展了应用的功能。这种方法由于省去了通信开销,因而提供了较好的性能,然而 它要求应用必须以特权用户的身份运行(不过运行在内核空间的帧缓冲区驱动程序现在已经解决了 这个问题)。 除了具备上述优点外,用户空间驱动程序也有很多缺点,下面列出其中最重要的几点: „ 中断在用户空间中不可用。解决这一问题的唯一办法是使用 vm86 系统调用(在 x86 平台 上),然而它却带来了性能损失。* „ 只有通过 mmap 映射 /dev/mem 才能直接访问内存,但只有特权用户才可以执行这个操作。 „ 只有在调用 ioperm 或 iopl 后才可以访问 I/O 端口。然而并不是所有平台都支持这两个系统 调用,并且访问 /dev/port 可能非常慢,因而并非十分有效。同样只有特权用户才能引用这些 系统调用和访问设备文件。 „ 响应时间很慢。这是因为在客户端和硬件之间传递信息和动作需要上下文切换。 „ 更严重的是,如果驱动程序被换出到磁盘,响应时间将难以忍受。引用 mlock 系统调用或许 可以减轻这一问题,但由于用户空间程序一般需要连接多个库,因此通常需要占用多个内存页。 同样,mlock 也只有特权用户才能引用。 „ 用户空间中不能处理一些非常重要的设备,包括网络接口和块设备等。 如上所述,我们看到用户空间驱动程序毕竟做不了太多的工作。然而依然存在一些有意义的应用, 例如,对 SCSI 扫描设备(由包 SANE 实现)和 CD 刻录设备(由 cdrecord 和其它工具实现)的 支持。这两种情况下,用户空间驱动程序都依赖内核空间驱动程序“SCSI generic”,它导出底层 通用的 SCSI 功能到用户空间程序,然后再由用户空间驱动程序驱动自己的硬件。 * 本书的主题将局限于内核驱动程序,因此不准备讨论 vm86。而且,这个系统调用太依赖于平台,可能也很难引 起一般读者的兴趣。 34 Linux 设备驱动程序 要想编写用户空间驱动程序,了解硬件知识就足够了,没有必要了解内核的细微之处。本书中我们 将不再进一步讨论用户空间驱动,而将主要精力集中于内核代码。 有一种情况适合在用户空间处理,这就是当我们准备处理一种新的不常见的硬件时。在用户空间中 我们可以研究如何管理这个硬件而不用担心挂起整个系统。一旦完成,很容易就能将户空间驱动程 序封装到内核模块中。 2.8 向后兼容性 Linux 内核处于不断发展之中,许多内容逐渐改变,新的特色也在逐渐发展。本章中描述的接口都 属于 2.4 版本,我们还要做很多工作才能使基于这些接口的代码工作在老版本内核中。 这一节是本书中第一个关于“向后兼容性”的小节,本书后面还有很多关于这个主题的小节。每章 末尾我们将讨论从 2.0 版本以来内核发生的变化,以及为了移植代码我们需要做的工作。 作为开始,首先讲到的一个变化是在内核 2.1.90 中才第一次引入宏 KERNEL_VERSION。 头文 件 sysdep.h 中为那些需要使用这个宏的内核定义了一个替代实现。 2.8.1 资源管理的改变 如果想编写一个能够运行在 2.4 版本以前内核上的驱动程序,则新版本内核中新的资源管理方式 可能会带来移植方面的几个问题。本节我们讨论可能遇到的这些问题以及 sysdep.h 如何替用户隐 藏这一问题。 新的资源管理方式带来的最明显的改变是增加了 request_mem_region 和相关的一些函数。它们 的作用仅局限于访问 I/O 内存数据库,不会执行任何与硬件相关的操作。因此,在以前的内核上, 只 要 不 去 调 用 这 些 函 数 就 不 会 产 生 麻 烦 。 头 文 件 sysdep.h 为 我 们 做 到 这 一 点 , 它 将 request_mem_region 定义为宏,如果内核版本是 2.4 以前,这个宏就返回 0。 2.4 和以前内核版本的另一个区别是 request_region 以及相关函数的原型不同。 2.4 版本以前的内核将 request_region 和 release_region 的返回值定义为 void(因此必须事先 使用 check_region)。而新版本的函数实现更加精确,它返回一个指针,因此可以报告错误状态(所 以此时的 check_region 没有什么用处)。实际返回的指针除了用来测试是否非空外,几乎没有其 它用处。指针为空时则表示请求失败。 如果想在驱动程序代码中少写几行,并且不考虑向后兼容性,则可以在代码中使用这些函数的新版 本并避免使用 check_region。实际上,现在的 check_region 函数是基于 request_region 实现的, 它释放 I/O 区域并且在请求满足后返回成功信息,开销可以忽略不计,因为没有人会在在一个时 间关键的代码段中使用这些函数。 如果我们希望代码具有可移植性,则需要遵守前面提到的函数调用顺序并且忽略 request_region 和 release_region 的返回值。不管怎样,sysdep.h 将两个函数都定义为宏,并且调用成功后返回 35 第 2 章 构造和运行模块 0。因此我们的代码在具备可移植性的同时,仍然可以检查每个调用函数的返回值。 2.4 内核和以前内核在 I/O 注册方面的最后一点不同是参数 start 和 len 的数据类型。新版本内 核中总是使用 unsigned long,而老版本内核中使用短一些的整型。不过,这个变化对于驱动程序 移植性并没有太大的影响。 2.8.2 多处理器系统上的编译 内核 2.0 并没有使用 CONFIG_SMP 配置选项来构造 SMP 系统。代替地,我们在内核的主 makefile 里使用全局变量来指示构造 SMP 系统。值得注意地是,为 SMP 机器编译的内核在单 一处理器的机器上不能工作,反之亦然,因此我们必须做出正确的配置。 本书所带的示例代码在 makefile 中自动处理有关 SMP 的问题,因此前面提到的代码并不用拷贝 到每个模块中。然而,我们并不支持 2.0 版本内核以下的 SMP。这应该不是一个大问题,因为 2.0 中多处理器的支持并不可靠,我们通常使用 2.2 或者 2.4 版本内核运行 SMP 系统。本书之所 以还要讲 2.0 是因为它仍然是小型嵌入式系统选择的平台之一(特别是它没有 MMU 的实现),但 是这些系统都不带多处理器。 2.8.3 在 Linux 2.0 中导出符号 2.0 中的符号导出机制建立在函数 register_symtab 之上。2.0 中的模块需要建立一张表描述所有 需要导出的符号,接着在它的初始化函数中调用 register_symtab。只有那些在这张显式符号表中 列出的符号才会导出到内核。相反,如果没有调用这个函数,则导出所有的全局变量。 如果模块不需要导出任何符号,并且我们也不想将所有符号定义为静态,则可以在 init_module 中 增加下面一行来隐藏全局变量。函数 register_symtab 简单地用一张空的符号表来覆盖模块默认的 符号表。 register_symtab(NULL); 这实际上就是在为 2.0 内核编译模块时,sysdep.h 对 EXPORT_NO_SYMBOLS 所作的定义。 这也是为什么 EXPORT_NO_SYMBOLS 必须位于 init_module 函数内才能在 2.0 内核中正常工 作的原因。 如果我们确实需要从模块中导出符号,则需要创建一个描述这些符号的符号表数据结构。给一个 2.0 内核上的符号表数据结构赋值需要非常小心,不过内核开发者提供了头文件来简化操作。下面 的几行代码展示了如何利用 2.0 中头文件提供的设施,来定义和导出一个符号表: static struct symbol_table skull_syms = { #include X(skull_fn1), X(skull_fn2), X(skull_variable), #include }; register_symtab(&skull_syms); 36 Linux 设备驱动程序 驱动程序编写人员需要做很多工作,才能编写出能够控制符号可见性的可移植模块代码。特别是当 仅仅定义几个处理兼容性的宏不足以解决问题时,此时,保证可移植性需要大量的条件预处理语句。 不过原理比较简单,第一步是识别当前内核版本并定义一些相应的符号,我们在 sysdep.h 中就定 义了一个宏 REGISTER_SYMTAB(),它在 2.2 和以后的内核版本中扩展为空,在 2.0 版本中扩展 为 register_symtab 。 另 外 , 如 果 需 要 使 用 老 版 本 内 核 上 的 代 码 , 则 需 要 定 义 _ _USE_OLD_SYMTAB。 通过使用这些代码,导出符号的模块实现了可移植。示例代码中有一个叫做 misc-modules/export.c 的模块,它只有导出一个符号的功能。这个模块在 11 章“模块中的版本控制”中还会详细讲到, 它包含下面几行来导出符号并且保持可移植性: #ifdef _ _USE_OLD_SYMTAB_ _ static struct symbol_table export_syms = { #include X(export_function), #include }; #else EXPORT_SYMBOL(export_function); #endif int export_init(void) { REGISTER_SYMTAB(&export_syms); return 0; } 如果设置了 _ _USE_OLD_SYMTAB(即我们在处理 2.0 内核),则根据需要定义 symbol_table。 另 外 , EXPORT_SYMBOL 用 来 直 接 导 出 符 号 。 因 此 在 init_module 中 需 要 调 用 REGISTER_SYMTAB,在 2.0 以外的任何内核版本中,它将扩展为空。 2.8.4 模块配置参数 MODULE_PARM 在 2.1.18 内核中首次引入。在 2.0 内核中,没有显式的参数定义方法。因此, insmod 可以改变模块中的任何变量值。这种方法有一个很大的弊端就是用户可以访问他不应该访 问的变量,而且这种方法也没有参数类型检查。MODULE_PARM 使模块参数更加清楚安全,然而 也使 2.2 的模块和 2.0 内核不相兼容。 如果考虑与 2.0 的兼容性问题,则可以使用一个简单的预处理测试将各种 MODULE_ 宏定义为 空。示例代码中的头文件 sysdep.h 在需要的时候就这样处理这些宏。 2.9 快速参考 本节将总结这一章提到的内核函数、变量、宏以及 /proc 文件,可以作为对这些内容的一个参考。 从本章开始,以后每一章里都会有类似的一节来总结引入的新符号。 _KERNEL_ _ "MODULE" 预处理符号。在编译模块化内核代码时必须定义。 _SMP_ _ 37 第 2 章 构造和运行模块 预处理符号。为多处理器系统编译模块时必须定义。 int init_module(void); void cleanup_module(void); 模块入口点。在模块目标文件中必须定义。 #include module_init(init_function); module_exit(cleanup_function); 新版本内核中用来标记模块初始化和清除函数的新机制。 #include 必需的头文件。它必须包含在模块源代码中。 MOD_INC_USE_COUNT; MOD_DEC_USE_COUNT; MOD_IN_USE; 操作使用计数的宏。 /proc/modules 列出装入内核的模块列表。每个列表项包含模块名、模块占用内存大小以及使用计数等域,还有一 些附加字符串指明模块当前的活动选项。 EXPORT_SYMTAB; 预处理宏。在模块需要导出符号时定义。 EXPORT_NO_SYMBOLS; 指明模块不需要导出任何符号到内核。 EXPORT_SYMBOL (symbol); EXPORT_SYMBOL_NOVERS (symbol); 用来导出单个符号到内核的宏。第二个宏导出的符号不带版本控制信息。 int register_symtab(struct symbol_table *); 用来指定模块中公用符号集合的函数。仅用于 2.0 内核。 #include X(symbol), #include 2.0 内核中用于声明符号表的头文件和预处理器。 MODULE_PARM(variable, type); MODULE_PARM_DESC (variable, description); 将一个模块变量定义为参数的宏,用户随后可在装入模块时调整这个变量值。 MODULE_AUTHOR(author); MODULE_DESCRIPTION(description); MODULE_SUPPORTED_DEVICE(device); 在目标文件中添加关于模块的文档信息。 #include 必需的头文件。除非 _ _NO_VERSION_ _ (见下面)被定义,否则它被 包含。 38 Linux 设备驱动程序 LINUX_VERSION_CODE 整数宏,用在处理版本依赖的预处理条件语句中。 char kernel_version[] = UTS_RELEASE; 每个都模块必需的变量。除非已经定义 _ _NO_VERSION_ _(见下一项),否则 必须定义它。 _ _NO_VERSION_ _ 预处理器符号。用来防止在 中声明 kernel_version。 #include 最重要的头文件之一,包含驱动程序使用的大部分内核 API 的定义,包括睡眠函数以及各种变量 声明。 struct task_struct *current; 当前进程。 current->pid current->comm 当前进程的进程 ID 和命令名。 #include int printk(const char * fmt, ...); 函数 printf 的内核版。 #include void *kmalloc(unsigned int size, int priority);" "void kfree(void *obj); 函数 malloc 和 free 的内核版。使用 GFP_KERNEL 作为 priority 参数值。 #include int check_region(unsigned long from, unsigned long extent); struct resource *request_region(unsigned long from, unsigned long extent, const char *name); void release_region(unsigned long from, unsigned long extent); 注册和释放 I/O 端口的函数。 int check_mem_region (unsigned long start, unsigned long extent); struct resource *request_mem_region (unsigned long start, unsigned long extent, const char *name); void release_mem_region (unsigned long start, unsigned long extent); 注册和释放 I/O 内存区域的宏。 /proc/ksyms 公用内核符号表。 /proc/ioports 系统中安装的设备所占用的 I/O 端口列表。 /proc/iomem 已分配内存区域的列表。 39 第 3 章 字符设备驱动程序 第 3 章 字符设备驱动程序 本章的目标是编写一个完整的字符设备驱动程序。我们将开发一个字符设备驱动程序,此类驱动程 序适合于大多数简单的硬件设备,而且比起块设备或网络等驱动程序,字符设备驱动程序也较容易 理解。我们的最终目标是编写一个模块化的字符设备驱动程序,但本章我们不会讨论模块化的相关 问题。 贯穿全章,我们将介绍一些代码段,它们取自一个真正的设备驱动程序:scull,即 “Simple Character Utility for Loading Localities”的缩写。scull 是一个操作内存区域的字符设备驱动程序, 这片内存区域就当作一个设备。这种处理的副作用在于,只要涉及 scull,“设备”这个词就可与“scull 所使用的内存区域”互换使用。 scull 的优点在于它不和硬件相关,因为每台计算机都有内存。scull 只是操作某些内存,通过 kmalloc 进行分配。任何人都可以编译和运行 scull,而且 scull 可以移植到所有 Linux 支持的计 算机平台上。但另一方面,除了展示内核和字符设备驱动程序之间的接口,并让用户运行某些测试 例程外,scull 设备做不了任何“有用的”事情。 3.1 scull 的设计 编写驱动程序的第一步就是定义驱动程序为用户程序所提供的能力(机制)。由于我们的“设备” 是计算机内存的一部分,所以可以利用它随意地做我们想做的事情。它可以是顺序或随机存取设备, 也可以是一个或多个设备等。 为了让 scull 能够为编写真正的设备驱动程序提供一个样板,我们将讲解怎样在计算机内存之上实 现若干设备抽象,而且每个都具有各自的特点。 scull 的源代码实现了下列设备。我们将模块实现的每种设备称作一种“类型”: scull0 - scull3 四个设备各由一个全局和持久的内存区域组成。“全局”是指,如果设备被多次打开,则打开它的 所有文件描述符可共享该设备所包含的数据。“持久”是指,如果设备关闭后再打开,其中的数据 不丢失。可以使用常用命令来访问和测试这个设备,如 cp、cat 以及 shell 的 I/O 重定向等。在本 章我们将深入探讨它的内部结构。 40 Linux 设备驱动程序 scullpipe0 - scullpipe3 四个 FIFO(先入先出)设备,与管道类似。一个进程读取由另一个进程写入的数据。如果多个进 程读取同一个设备,它们会为数据发生竞争。scullpipe 的内部实现将说明阻塞式和非阻塞式读/写 如何实现,而无须借助于中断。虽然实际的驱动程序使用硬件中断与它们的设备保持同步,但阻塞 式和非阻塞式操作是一个重要内容,并且区别于中断处理(第 9 章将作介绍)。 scullsingle scullpriv sculluid scullwuid 这些设备与 scull0 相似,但在何时允许 open 操作方面受到某些限制。第一个(scullsingle)一次 只允许一个进程使用该驱动程序,而 scullpriv 对每个虚拟控制台(或 X 终端会话)是私有的,因 为每个控制台/终端将获取一块与其它控制台上进程不同的内存区。sculluid 和 scullwuid 可被多次 打开,但每次只能由一个用户打开;如果另一用户锁定了设备,sculluid 返回“设备忙”的错误, 而 scullwuid 则实现了阻塞式的 open。这些 scull 设备的变化类型相对“机制”而言,所增加的 更多是“策略。”总之,这类处理值得去了解的,因为某些设备需要不同类型的管理方式,就象 scull 各种类型的设备一样,它们就是这些管理机制的一部分。 每个 scull 设备都展示了驱动程序的不同功能,也提出了不同的难点。本章主要涉及 scull0-3 的内 部结构;更为复杂的设备将在第 5 章介绍:“样例实现:scullpipe”讲解 scullpipe,“设备文件的 访问控制”介绍其它设备。 3.2 主设备号和次设备号 访问字符设备是通过文件系统内的设备名称进行的。那些名称被称为特殊文件、设备文件,或者简 单称之为文件系统树的节点,它们通常位于 /dev 目录。字符设备驱动程序的设备文件可通过 ls -l 命令输出的第一列中的“c”来识别。块设备也在 /dev 下,但它们是由字符“b”标识的。本章内 容主要集中于字符设备,不过下面介绍的许多内容同样也适用于块设备。 如果执行 ls -l 命令,就可以在设备文件项的最后修改日期前看到两个数(用逗号分隔),这个位置 通常显示的是普通文件的长度,而这时这两个数就是相应设备的主设备号和次设备号。下面的列表 给出了典型系统中的一些设备。它们的主设备号是 1,4,7 和 10,而次设备号是 1,3,5,64, 65 和 129。 crw-rw-rw- 1 root root 1, 3 Feb 23 1999 null crw------- 1 root root 10, 1 Feb 23 1999 psaux crw------- 1 rubini tty 4, 1 Aug 16 22:22 tty1 crw-rw-rw- 1 root dialout 4, 64 Jun 30 11:19 ttyS0 crw-rw-rw- 1 root dialout 4, 65 Aug 16 00:00 ttyS1 crw------- 1 root sys 7, 1 Feb 23 1999 vcs1 crw------- 1 root sys 7, 129 Feb 23 1999 vcsa1 crw-rw-rw- 1 root root 1, 5 Feb 23 1999 zero 主设备号标识设备对应的驱动程序。例如,/dev/null 和 /dev/zero 由驱动程序 1 管理,而虚拟控 制台和串口终端由驱动程序 4 管理;类似地,vcsl 和 vcsal 设备都由驱动程序 7 管理。内核利用 41 第 3 章 字符设备驱动程序 主设备号在 open 操作中将设备与相应的驱动程序对应起来。 次设备号只是由那些主设备号已经确定的驱动程序使用,内核的其它部分不会用到它,而仅是把它 传递给驱动程序。一个驱动程序控制多个设备是常有的事情(如上面的例子所示),而次设备号为 驱动程序提供了一种区分不同设备的方法。 2.4 内核引入了一种新的(可选的)特征,也就是设备文件系统或 devfs,如果使用这种文件系统, 设备文件管理将被简化,而且也会大为不同。另一方面,这种新的文件系统带来了一些用户可见的 不兼容性,在本书编写阶段,它还没被系统发行商选作一个缺省的特征。因此,前面以及接下来的 内容中,关于增加新的驱动程序和设备文件的讲解,都假定 devfs 尚未引入。本章后面的“设备 文件系统”一节将讲述 devfs 的相关内容。 当未采用 devfs 时,向系统增加一个新的驱动程序意味着为其分配一个主设备号。这个分配工作在 驱动程序(模块)初始化时进行,由定义在 中的如下函数实现: int register_chrdev(unsigned int major, const char *name, struct file_operations *fops); 返回值提示操作成功还是失败。负的返回值提示错误;0 或正的返回值表明成功完成。major 参数 是被请求的主设备号,name 是设备的名称,该名称将出现在 /proc/devices 中,fops 是指向函 数指针数组的指针,这些函数是调用驱动程序的入口点,这些将在本章后面的“文件操作”一节进 行说明。 主设备号是用来索引字符设备静态数组的一个小整数,本章后面的“动态分配主设备号” 一节将 介绍怎样选择一个主设备号。2.0 内核支持 128 个设备; 2.2 和 2.4 内核则增加到 256 个(保留 了 0 和 255 这 两 个 值 以 备 将 来 之 需 )。 次 设 备 号 同 样 也 占 8 位 ; 它 们 不 必 传 递 给 函 数 register_chrdev,因为正如已经提到的,它们仅由驱动程序自己使用。目前有来自开发者群体的极 大压力,要求进一步增加内核可支持的设备数目;将设备号增加到至少 16 位将是 2.5 版本内核 开发的一个既定目标。 一旦设备注册到内核表中,它的操作就和指定的主设备号对应了起来。当我们在主设备号对应的字 符设备文件上进行某个操作时,内核将从 file_operations 结构中找到并调用正确的函数。出于这 个原因,传递给 register_chrdev 的指针应指向驱动程序中的一个全局性数据结构,而不是模块初 始化函数中的局部数据结构。 接下来的问题就是,如何给程序一个名字,通过这个名字,程序向设备驱动程序发出请求。这个名 字必须插入到 /dev 目录中,并与驱动程序的主设备号和次设备号相关联。 在文件系统上创建一个设备节点的命令是 mknod,只有超级用户才能创建设备。除了要创建的节 点名字外,该命令还带三个参数。例如,命令: mknod /dev/scull0 c 254 0 创建一个字符设备(c),主设备号是 254,次设备号是 0。次设备号应该在 0-255 范围内,这是因 42 Linux 设备驱动程序 为出于某些历史原因,它们存储在单个字节中。有很多原因要求扩展次设备号可用的范围,但就目 前来说,仍然限制在 8 位。 请注意,一旦通过 mknod 创建了设备文件,该文件将一直保留下来,除非明确地将其删除,这一 点与存储在磁盘上的其它信息是类似的。读者可以通过命令 rm /dev/scull0 将这个例子中创建的 设备删除。 3.2.1 动态分配主设备号 一部分主设备号已经静态地分配给了大部分常见设备。在内核源码树的 Documentation/device.txt 文件中可以找到这些设备的清单。由于许多数字已经分配了,为新设备选择一个独一无二的设备号 是很困难的--定制的驱动程序远比可用的主设备号多得多。不过可以使用为“实验或本地用途” *所保留的某个主设备号。 但是如果对多个“本地”驱动程序作实验,或者要为第三方提供驱动程序的话,就会再次面临选择 合适设备号的问题。 幸运的是(更恰当地说是感谢某些人的才智),我们现在可以动态分配主设备号。如果在调用 register_chrdev 时的 major 为零的话,这个函数就会选择一个空闲号码并做为返回值返回。返回 的主设备号总是正的,负的返回值则是错误码。请注意在两种情形下,行为稍有不同:如果调用者 是请求一个动态设备号的话,这个函数返回动态分配的数字,而当成功注册了一个预定义好的主设 备号时,它返回的是 0(并非主设备号)。 对于专有的驱动程序,我们强烈推荐读者不要随便选择一个当前未使用的设备号做为主设备号,而 应该使用动态分配机制获取主设备号。另一方面,如果驱动程序对大家有使用价值,并被包含进正 式的内核源码树的话,就需要申请分配一个独占使用的主设备号。 动态分配的缺点是,由于分配的主设备号不能保证始终一致,所以无法预先创建设备节点。这意味 着我们无法使用驱动程序的“按需载入”功能(第 11 章将介绍这一高级功能)。对于驱动程序的 一般用法,这倒不是什么问题,因为一旦分配了设备号,就可以从 /proc/devices 中读取得到。为 了加载一个使用动态主设备号的设备驱动程序,对 insmod 的调用可替换为一个简单的脚本,该脚 本在调用 insmod 之后,读取 /proc/devices 获得新分配的主设备号,以便创建对应的设备文件。 典型的 /proc/devices 文件一般如下所示: Character devices: 1 mem 2 pty 3 ttyp 4 ttyS 6 lp 7 vcs 10 misc 13 input 14 sound 21 sg * 位于 60 到 63,120 到 127,240 到 254 范围内的主设备号是为本地或实验用途所保留的,也就是说,不会 有实际设备采用这些主设备号。 43 第 3 章 字符设备驱动程序 180 usb Block devices: 2 fd 8 sd 11 sr 65 sd 66 sd 主设备号动态分配时,加载这类驱动程序模块的脚本,可以利用 awk 这类工具从 /proc/devices 中获取信息,并在 /dev 目录中创建文件。 下面名为 scull_load 的脚本,是 scull 发布内容的一部分。使用以模块形式发行驱动程序的用户, 可以在系统的 rc.local 文件中调用这个脚本,或是在需要模块时手工调用。 #!/bin/sh module="scull" device="scull" mode="664" # invoke insmod with all arguments we were passed # and use a pathname, as newer modutils don't look in . by default /sbin/insmod -f ./$module.o $* || exit 1 # remove stale nodes rm -f /dev/${device}[0-3] major=`awk "\\$2==\"$module\" {print \\$1}" /proc/devices` mknod /dev/${device}0 c $major 0 mknod /dev/${device}1 c $major 1 mknod /dev/${device}2 c $major 2 mknod /dev/${device}3 c $major 3 # give appropriate group/permissions, and change the group. # Not all distributions have staff; some have "wheel" instead. group="staff" grep '^staff:' /etc/group > /dev/null || group="wheel" chgrp $group /dev/${device}[0-3] chmod $mode /dev/${device}[0-3] 这个脚本同样可以适用于其它驱动程序,只要重新定义变量并调整 mknod 那几行就可以了。该脚 本创建了 4 个设备,因为 scull 的源码默认即创建 4 个设备。 脚本的最后几行看起来有点奇怪:为什么要改变设备的组和访问模式呢?原因在于这个脚本必须由 超级用户运行,所以新创建的设备文件自然属于 root 。缺省权限位只允许 root 对其有写访问权, 而其它用户只有读权限。正常情况下,设备节点需要不同的访问策略,因此有时需要修改访问权限。 我们的脚本默认地把访问权赋于一个用户组,而读者的需求可能有所不同。第 5 章“设备文件的 访问控制”一节中,sculluid 的代码将会展示设备驱动程序如何实现自己的设备访问授权。除 scull_load 外,还有一个 scull_unload 脚本用来清除 /dev 目录下的相关设备文件并卸载这个模 块。 除了使用这一对装载和卸载模块的脚本外,我们还可以编写一个 init 脚本,并将其保存在发行版 44 Linux 设备驱动程序 使用的 init 脚本目录中。* 作为 scull 源码的一部分,我们提供了相当详尽和可配置的 init 脚本范例,名为 scull.init。它接收 常用的参数 ―― “start”、“stop”或“restart”,而且可完成 scull_load 和 scull_unload 的双重 任务。 如果反复创建和删除 /dev 节点显得有些不必要的话,有一个解决的方法。如果只是装载和卸载单 个驱动程序,可在第一次创建设备文件之后,仅使用 rmmod 和 insmod 这两个命令:因为动态 设备号不是随机生成的,如果不受其它(动态)模块影响的话,可以预期获得到相同的动态主设备 号。在开发过程中避免脚本过长是有益的。但很明显,这个技巧不能适用于同时有多个驱动程序的 场合。 在我们看来,分配主设备号的最佳方式是,默认地采用动态分配,同时保留在加载时,甚至是编译 时,指定主设备号的余地。我们所建议的代码和用于端口自动探测的代码很相似。scull 设计中使 用了一个全局变量,scull_major,保存所选择的设备号。该变量的初始化值是 SCULL_MAJOR, 这个宏定义在 scull.h 中。在发行的源码中 SCULL_MAJOR 默认为 0,即“选择动态分配”。用 户可以使用这个默认值或选择某个特定的主设备号,既可以在编译前修改宏定义,也可以在 insmod 命令行中指定。最后,通过使用 scull_load 脚本,用户可以在 scull_load 的命令行中将 参数传递给 insmod。* 下面是 scull.c 中用来获取主设备号的代码: result = register_chrdev(scull_major, "scull", &scull_fops); if (result < 0) { printk(KERN_WARNING "scull: can't get major %d\n",scull_major); return result; } if (scull_major == 0) scull_major = result; /* dynamic */ 3.2.2 从系统中删除设备驱动程序 当从系统中卸载一个模块时,应该释放主设备号。这一操作可以在 cleanup_module 中调用如下函 数完成: int unregister_chrdev(unsigned int major, const char *name); 参数列表包括要释放的主设备号和相应的设备名。参数中的这个设备名,会被内核用来和设备号参 数所对应的已注册设备名进行比较,如果不同,返回 -ENINVAL。如果主设备号超出了所允许的范 围,内核同样返回 -EINVAL。 在 cleanup_module 中注销资源失败会有很不利的后果。下次读取 /proc/devices 时,由于其中一 个 name 字串仍然指向模块内存,而这块内存不再被映射,所以系统将出错。这种类型的出错称 * 不同的发行版把 init 脚本放在不同的位置,最常见的目录是 /etc/init.d、/etc/rc.d/init.d 和 /sbin/init.d 等等。另 外,如果脚本需要在引导阶段执行,则需要在对应该运行级别的目录(比如,.../rc3.d)中建立一个指向该脚本的 符号链接。 * init 脚本 scull.init 不能在命令行中接收驱动程序选项,但它支持一个配置文件,这是因为这个脚本是为启动和关 机时自动运行设计的。 45 第 3 章 字符设备驱动程序 为 oops*,因为这是内核访问无效地址时打印的消息。 如果在卸载驱动程序时没有注销主设备号,情况将是很难恢复的,因为 unregister_chrdev 中调用 的 strcmp 函数会使用指向最初模块的指针(即 name)。如果注销主设备号失败,就必须重新载 入相同的模块,以及另一个用于注销这个主设备号的模块。如果没有修改代码,运气好的话,这个 出问题的模块将获得相同的地址,而 name 字符串处于相同的位置。当然,更为安全的选择就是 重新启动系统。 除了卸载模块,还经常需要在卸载驱动程序时删除设备节点。这样的任务可以由与装载时使用的脚 本相配对的脚本来完成。对于我们的样例设备,脚本 scull_unload 完成了这个工作,另外通过调 用 scull.init stop 也可做到这一点。如果动态节点没有从 /dev 中删除,就有可能造成不可预期的 错误:如果两个驱动程序所使用的动态主设备号相同,开发者计算机上的某个空闲 /dev/framegrabber 就有可能在一个月之后才访问火警设备。试图打开/dev/framegrabber 时,如 果能得到“No such file or directory(没有这个文件或目录)”的回应,总比这个新设备可能导致的 后果要好得多。 3.2.3 dev_t 和 kdev_t 到目前为止,我们已经谈论了主设备号,接下来讨论次设备号,以及驱动程序是如何使用次设备号 来区分设备的。 每次在内核调用一个设备驱动程序时,它都会告诉驱动程序它正在操作哪个设备。主设备号和次设 备号合在一起构成单个数据类型并用来标识特定的设备。组合后的设备号(主设备号和次设备号合 在一起)保存于在稍后介绍的索引节点(inode)结构的 i_rdev 成员中。每个驱动程序接收一个 指向 inode 结构的指针作为第一个参数。假定该指针称作 inode(和大多数驱动程序开发人员), 则可以通过 inode->i_rdev 得出设备号。 历史上,Unix 使用 dev_t(设备类型)保存设备号。dev_t 通常是 中定义的一个 16 位整数。而现在有时需要超过 256 个次设备号,但是由于有许多应用程序都已“了解” dev_t 的 内部结构,一旦改变 dev_t 的结构就会造成这些应用程序无法运行,所以很难改变 dev_t 的定义。 因此,虽然已有的很多基础性工作是为了能够处理更大的设备号而准备的,但它们如今仍被视为 16 位整数。 不过,现在的 Linux 内核使用了一个新的类型,即 kdev_t。对于每一个内核函数来说,这个新类 型被设计为一个黑箱。用户程序完全不了解 kdev_t,而且内核函数也不知道 kdev_t 中究竟有些 什么。如果 kdev_t 一直保持隐藏,它就可以在内核的不同版本间任意变化,而不必修改每个人的 设备驱动程序。 kdev_t 的相关信息在 中定义,其中大部分是注释。如果读者对代码背后的推理 感兴趣的话,这个头文件是一段很有指导性的代码。因为 已经包含了这个头文件,所 以没有必要显式地包含这个文件。下列的这些宏和函数是可以对 kdev_t 执行的操作: MAJOR(kdev_t dev); * oops 的英文原意是“表示惊讶时所发出的喊声”,这个词被热衷于 Linux 的爱好者们既作名词用也作动词用。 46 Linux 设备驱动程序 从 kdev_t 结构中得出主设备号。 MINOR(kdev_t dev); 得出次设备号。 MKDEV(int ma, int mi); 通过主设备号和次设备号创建 kdev_t。 kdev_t_to_nr(kdev_t dev); 将 kdev_t 转换为一个整数(一个 dev_t)。 to_kdev_t(int dev); 将一个整数转换为 kdev_t。注意,内核模式中没有定义 dev_t,因此使用了 int。 只要在程序中采用这些操作去处理设备号,即便内部数据结构发生了变化,代码依然能正常工作。 3.3 文件操作 在接下来的几节中,我们将着眼于对所管理的设备,驱动程序能完成哪些不同的操作。打开的设备 在内核内部由 file 结构标识,内核使用 file_operations 结构访问驱动程序的函数。file_operations 结构是一个定义在 中的函数指针数组。每个文件都与它自己的函数集相关联(通过指 向 file_operations 结构的一个名为 f_op 的指针成员)。这些操作主要负责系统调用的实现,并因 此被命名为 open,read 等。我们可以认为文件是一个“对象”,操作它的函数是“方法”。如果 采用面向对象编程的术语来表达就是,对象声明的动作,将作用于其本身。这是我们在 Linux 内 核中看到的面向对象化编程的第一个例证,在后面的章节中还会看到更多。 按照惯例,file_operations 结构或指向这类结构的指针称为 fops(或者是与此相关的其它称法);我 们已经看到 register_chrdev 调用中有一个指针参数就是 fops。这个结构中的每一个成员都必须指 向驱动程序中实现特定操作的函数。对于不支持的操作,对应的成员可置为 NULL 值。对各个函 数而言,如果对应成员被赋为 NULL 指针,那么内核的具体处理行为是不尽相同的,本节后面的 列表会列出这些差异。 随着内核不断增加新的功能,file_operations 结构已逐渐变得越来越大。新增加的操作自然会对设 备驱动程序带来移植性的问题。每个驱动程序中该结构的实例都是用标准的 C 语法声明的,新的 操作一般添加在该结构的末尾,这样,对驱动程序简单地进行一次重新编译,这些操作都会被赋予 NULL,因此也就选择为缺省的行为,一般来说这正是期望的状态。 后来,内核开发人员又转而采用一种“标记化”的初始化格式,这种格式允许用名字对这类结构的 成员进行初始化,也就避免了因数据结构发生变化而带来的麻烦。这种标记化的初始化处理,并不 是标准 C 的规范,而是对 GUN 编译器的一种(有用的)特殊扩展。很快我们会看到一个标记化 的结构初始化范例。 下面列出了应用程序可在某个设备上调用的所有操作。为便于查询,我们尽量使之简洁,仅仅总结 了每个操作,以及使用 NULL 时的内核缺省行为。读者可以在初次阅读时跳过这张表,需要的时候 再来查阅。 47 第 3 章 字符设备驱动程序 介绍完另一个重要数据结构(也就是 file,它实际上包含了指向它拥有的 file_operations 结构的指 针)后,本章其余部分将讲解最重要的一些操作并给出一些技巧、警告和实际的代码样例。由于我 们尚未深入探讨内存管理、块操作和异步通知机制,其它更为复杂的操作将在以后的章节中介绍。 下面的表给出了 2.4 系列内核中 file_operations 结构所包括的操作。在这方面,2.4 内核和早期 版本之间的差别较小,本章后面将谈到这些差异,因此,我们首先专注 2.4 这个版本的情况。各 操作的返回值为 0 表示成功,为负的话则说明发生错误,除非另有所指。 loff_t (*llseek) (struct file *, loff_t, int); 方法 llseek 用来修改文件的当前读写位置,并将新位置做为(正的)返回值返回。参数 loff_t 一 个“长偏移量”,即使在 32 位平台上也至少占用 64 位的数据宽度。出错时返回一个负的返回值。 如果驱动程序没有设置这个函数,相对文件尾(EOF)的定位操作会失败,而其它的定位操作将修 改 file 结构(在本章后面的“file 结构”一节介绍)中的位置计数器并成功返回。 ssize_t (*read) (struct file *, char *, size_t, loff_t *); 用来从设备中读取数据。该函数指针被赋为 NULL 值时,将导致 read 系统调用出错并返回 -EINVAL(“Invalid argument,非法参数”)。函数返回非负值表示成功读取的字节数(返回值为 “signed size”数据类型,通常就是目标平台上的固有整数类型)。 ssize_t (*write) (struct file *, const char *, size_t, loff_t *); 向设备发送数据。如果没有这个函数,write 系统调用向调用程序返回一个 -EINVAL。如果返回值 非负,则表示成功写入的字节数。 int (*readdir) (struct file *, void *, filldir_t); 对于设备节点来说,这个成员应该为 NULL,它仅用于读取目录,只文件系统有用。 unsigned int (*poll) (struct file *, struct poll_table_struct *); poll 方法是 poll 和 select 这两个系统调用的后端实现。这两个系统调用可用来查询设备是否可 读或可写,或是否处于某种特殊状态。这两个系统调用是可阻塞的,直至设备变为可读或可写状态 为止。如果驱动程序没有定义它的 poll 方法,它驱动的设备就会被认为既可读也可写,并且不会 处于其它的特殊状态。返回值是一个描述设备状态的位掩码。 int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); 系统调用 ioctl 提供一种执行设备相关命令的方法(如格式化软盘的某个磁道,这既不是读操作也 不是写操作)。另外,内核还能识别一部分 ioctl 命令,而不必调用 fops 表中的 ioctl。如果设备 不提供 ioctl 入口点,则对于任何内核未预先定义的请求,ioctl 系统调用将返回错误(-ENOTTY, “No such ioctl for device,该设备无此 ioctl 命令”)。如果该设备方法返回一个非负值,那相同的 值会返回给调用程序提示调用成功。 int (*mmap) (struct file *, struct vm_area_struct *); mmap 用于请求将设备内存映射到进程地址空间。如果设备没有实现这个方法,mmap 系统调用 将返回 -ENODEV。 int (*open) (struct inode *, struct file *); 尽管这始终是对设备文件执行的第一个操作,然而却并不要求驱动程序一定要声明这个方法。如果 这个入口为 NULL,设备的打开操作永远成功,但系统不会通知驱动程序。 48 Linux 设备驱动程序 int (*flush) (struct file *); 对 flush 操作的调用发生在进程关闭设备文件描述符复本的时候,它应该完成(并等待)设备上尚 为完结的操作。请不要将它同用户程序使用的 fsync 操作相混淆。目前,flush 仅仅用于网络文件 系统(NFS)代码。如果 flush 被置为 NULL, 它只是简单地不被调用。 int (*release) (struct inode *, struct file *); 当 file 结构被释放时,将调用这个操作。与 open 相仿,也可以没有 release*。 int (*fsync) (struct inode *, struct dentry *, int); 该方法是 fsync 系统调用的后端实现,用户调用它来刷新待处理的数据。如果驱动程序没有实现 这一方法,fsync 系统调用返回 -EINVAL。 int (*fasync) (int, struct file *, int); 这个操作用来通知设备,它的 FASYNC 标志发生了变化。异步通知是比较高级的话题,将在第 5 章介绍。如果设备不支持异步通知,该成员可以是 NULL。 int (*lock) (struct file *, int, struct file_lock *); lock 方法用于实现文件锁定,锁定是常规文件不可缺少的特性,但设备驱动程序几乎从来不会实 现这个方法。 ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); 这些方法是在 2.3 版本周期的后期新增的,用来实现“分散/聚集”型的读写操作。应用程序有时 需要进行涉及多个内存区域的单次读或写操作,利用上面这些系统调用可完成这类工作,而不必强 加额外的数据拷贝操作。 struct module *owner; 这个成员并不象 file_operations 结构中的其它部分那样是个方法。取而代之的是,它是指向“拥 有”该结构的模块的指针,内核使用该指针维护模块的使用计数。scull 设备驱动程序所实现的只 是最重要的设备方法,并且采用标记化格式声明它的 file_operations 结构: struct file_operations scull_fops = { llseek: scull_llseek, read: scull_read, write: scull_write, ioctl: scull_ioctl, open: scull_open, release: scull_release, }; 这个声明采用了前面提到的标记化的结构初始化语法。这种语法是值得采用的,因为它使驱动程序 在结构的定义发生变化时更具可移植性,并且使得代码更加紧凑且易读。标记化的初始化方法允许 对结构成员进行重新排列。在某些场合下,将频繁访问的成员放在相同的硬件缓存线上,将大大提 高性能。 设置 file_operations 结构中的 owner 成员也是必要的。在有些内核代码中,常可以看到以下面这 样的标记语法初始化 owner 成员: * 注意,release 并不会在进程每次调用 close 时都会被调用。只要 file 结构被共享(如在 fork 或 dup 调用之 后),release 会等到所有的复本都关闭之后才会得到调用。如果希望在任意一个复本被关闭时,刷新那些待处理 的数据,则应实现 flush 方法。 49 第 3 章 字符设备驱动程序 owner: THIS_MODULE, 不 过 这 种 方 法 仅 在 2.4 内 核 上 才 会 起 作 用 。 更 具 可 移 植 性 的 方 法 是 使 用 宏 SET_MODULE_OWNER ,它定义在 中。scull 如下执行这种初始化工作: SET_MODULE_OWNER(&scull_fops); 这个宏对于任何含 owner 成员的结构均有效。我们将在本书其余部分多次遇到这个成员。 3.3.1 file 结构 中定义的 file 结构是设备驱动程序所使用的另一个最重要的数据结构。注意,file 结 构与用户程序中的 FILE 没有任何关联。FILE 定义在 C 库且不会出现在内核代码中;而 struct file 是一个内核结构,它不会出现在用户程序中。 file 结构代表一个打开的文件(它并不仅仅限定于设备驱动程序;系统中每个打开的文件在内核空 间都有一个对应的 file 结构)。它由内核在 open 时创建,并传递给在该文件上进行操作的所有函 数,直到最后的 close 函数。在文件的所有实例都被关闭之后,内核释放这个数据结构。注意, 一个打开的文件和磁盘文件不同,后者由 struct inode 表示。 在内核源码中,指向 struct file 的指针通常称为 file 或 filp (“文件指针”)。为了不致于和这个结 构本身相混淆,我们一致称该指针为 filp。这样的话,file 指的是结构本身,filp 则是指向该结构 的指针。 struct file 中最重要的成员罗列如下。与上节相似,这张表在首次阅读时可以略过。在下一节中, 将看到一些真正的 C 代码,我们会讨论其中的某些成员,到时读者可以反过来查阅这张表。 mode_t f_mode; 文件模式通过 FMODE_READ 和 FMODE_WRITE 位来标识文件是否可读或可写。读者可能会认 为要在自己的 ioctl 函数中查看这个成员来检查读/写许可,但由于内核在调用驱动程序的 read 和 write 前已经检查了许可,所以并不必为这两个方法检查许可。例如,一个未得到允许的写操作在 驱动程序还不知道的情况下就已经被内核拒绝了。 loff_t f_pos; 当前读/写位置。loff_t 是一个 64 位的数(用 gcc 的术语就是 long long)。如果驱动程序需要知 道文件中的当前位置,可以读取这个值,但不要去修改它(read/write 会使用它们接收到的最后那 个指针参数来更新这一位置,而不是直接对 filp->f_pos 操作)。 unsigned int f_flags; 文件标志,如 O_RDONLY、O_NONBLOCK 和 O_SYNC。驱动程序为了支持非阻塞型操作需要 检查这个标志,而其它标志很少用到。注意,检查读/写许可应该查看 f_mode 而不是 f_flags。所 有这些标志都定义在 中。 struct file_operations *f_op; 与文件相关的操作。内核在执行 open 操作时对这个指针赋值,以后需要处理这些操作时就读取 这个指针。filp->f_op 中的值决不会为方便引用而保存起来,也就是说,我们可以在任何需要的时 50 Linux 设备驱动程序 候修改文件的关联操作,在返回给调用者之后,新的操作方法就会立即生效。例如,对应于主设备 号 1(/dev/null、/dev/zero 等等)的 open 代码根据要打开的次设备号替换 filp->f_op 中的操作。 这种技巧允许相同主设备号下的设备实现多种操作行为,而不会增加系统调用的负担。这种替换文 件操作的能力在面向对象编程技术中称为“方法重载”。 void *private_data; open 系统调用在调用驱动程序的 open 方法前将这个指针置为 NULL。驱动程序可以将这个成员 用于任何目的或者忽略这个成员。驱动程序可以用这个成员指向已分配的数据,但是一定要在内核 销毁 file 结构前在 release 方法中释放内存。private_data 是跨系统调用时保存状态信息的非常 有用的资源,我们的大部分示例都使用了它。 struct dentry *f_dentry; 文件对应的目录项(dentry)结构。目录项是一种优化设计,在 2.1 系列版本内核中就已经引入了。 除了用 filp->f_dentry->d_inode 的方式来访问索引节点结构之外,设备驱动程序的开发者们一般 无需关心 dentry 结构。 实际的结构里还有其它一些成员,但它们对于设备驱动程序并没有多大用处。由于驱动程序从不填 写 file 结构,而只是对别处创建的这些结构进行访问,所以可以安全地忽略这些成员。 3.3.2 open 和 release 现在我们已经简单浏览了这些成员,下面将在实际的 scull 函数中使用这些成员。 open 方法 open 方法是为以后的操作完成初始化准备工作而提供给驱动程序的。此外,open 一般还会增加设 备的使用计数,防止在文件关闭前模块被卸载出内核。这个计数值在 release 方法中被递减,第 2 章的“使用计数”一节已经作了讲解。 在大部分驱动程序中,open 应完成如下工作: „ 增加使用计数。 „ 检查设备相关错误(诸如设备未就绪或类似的硬件问题)。 „ 如果设备是首次打开,则对其初始化。 „ 识别次设备号,如有必要更新 f_op 指针。 分配并填写置于 filp->private_data 里的数据结构。 在 scull 中,上面大部分预先的操作都取决于被打开设备的次设备号。因此,首先要做的就是识别 要操作的是哪个设备。可以通过查看 inode->i_rdev 做到这一点。 我们已经谈过内核是不使用次设备号的,因此驱动程序可以随意使用它们。事实上,不同的次设备 号用来访问不同的设备,或以不同的方式打开同一个设备。例如,/dev/st0(次设备号 0)和/dev/st1 (次设备号 1)涉及两个不同的 SCSI 磁带驱动,而/dev/nst0(次设备号 128)与/dev/st0 是相同 的物理设备,但它的操作行为不同(该设备在关闭时并不重绕磁带)。所有的磁带设备都有不同的 51 第 3 章 字符设备驱动程序 次设备号,这样驱动程序就能区分它们。 驱动程序实际上完全不知道被打开设备的名字,它仅仅知道设备号――用户可以根据这一点,为设 备取一个别名。如果创建了两个主/次设备号完全相同的设备文件,却只有一个相同设备的话,那 就没有方法区分它们。同样的效果可以采用符号链接或硬链接来获得,实现别名的推荐方法是创建 符号链接。 scull 驱动程序是这样使用次设备号的:字节的高 4 位标识设备的类型(personality,个性),低 4 位可以在某个类型的设备支持多个设备实例时,用于区分各个设备。因此,scull0 的高 4 位与 scullpipe0 不同,而 scull0 的低 4 位与 scull1 不同*。 源码中定义了两个宏(TYPE 和 NUM)从设备号中分解出这些位,如下所示: #define TYPE(dev) (MINOR(dev) >> 4) /* high nibble */ #define NUM(dev) (MINOR(dev) & 0xf) /* low nibble */ 对于每一设备类型,scull 定义了一个特定的 file_operations 结构,它在 open 操作时赋给 filp->f_op。下面的代码显示了多个 fops 是如何实现的: struct file_operations *scull_fop_array[]={ &scull_fops, /* type 0 */ &scull_priv_fops, /* type 1 */ &scull_pipe_fops, /* type 2 */ &scull_sngl_fops, /* type 3 */ &scull_user_fops, /* type 4 */ &scull_wusr_fops /* type 5 */ }; #define SCULL_MAX_TYPE 5 /* In scull_open, the fop_array is used according to TYPE(dev) */ int type = TYPE(inode->i_rdev); if (type > SCULL_MAX_TYPE) return -ENODEV; filp->f_op = scull_fop_array[type]; 内 核 根 据 主 设 备 号 调 用 open , scull 则 使 用 上 述 宏 分 解 出 的 次 设 备 号 。 TYPE 用 以 索 引 scull_fop_array 数组,从中取出正被打开的设备类型所对应的方法集。 在 scull 中, 根 据 次 设 备 号 判 断 设 备 的 类 型 , 并 赋 予 filp->f_op 由 设 备 类 型 所 决 定 的 对 应 file_operations 结构。然后新的 fops 中声明的 open 方法得到调用。通常,驱动程序并不调用自 己的 fops,这些 fops 是内核分发对应的驱动程序方法时调用的。不过,当 open 方法不得不处 理不同的设备类型时,则在修改了 fops 指针后,根据被打开设备的次设备号,或许就需要调用 fops->open 了。 scull_open 的实际代码如下。它使用了前面那段代码中定义的 TYPE 和 NUM 两个宏来得出次 设备号: int scull_open(struct inode *inode, struct file *filp) { Scull_Dev *dev; /* device information */ * 位切分是使用次设备号的一种典型方式。例如,IDE 驱动程序使用高 2 位表示磁盘号,低 6 位表示分区号。 52 Linux 设备驱动程序 int num = NUM(inode->i_rdev); int type = TYPE(inode->i_rdev); /* * If private data is not valid, we are not using devfs * so use the type (from minor nr.) to select a new f_op */ if (!filp->private_data && type) { if (type > SCULL_MAX_TYPE) return -ENODEV; filp->f_op = scull_fop_array[type]; return filp->f_op->open(inode, filp); /* dispatch to specific open */ } /* type 0, check the device number (unless private_data valid) */ dev = (Scull_Dev *)filp->private_data; if (!dev) { if (num >= scull_nr_devs) return -ENODEV; dev = &scull_devices[num]; filp->private_data = dev; /* for other methods */ } MOD_INC_USE_COUNT; /* Before we maybe sleep */ /* now trim to 0 the length of the device if open was write-only */ if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) { if (down_interruptible(&dev->sem)) { MOD_DEC_USE_COUNT; return -ERESTARTSYS; } scull_trim(dev); /* ignore errors */ up(&dev->sem); } return 0; /* success */ } 这里需要作一些解释。用于记录内存区域的数据结构是 Scull_Dev,这里简要介绍一下。全局变量 scull_nr_devs 和 scull_devices[](全部小写)分别是可用设备数和指向 Scull_Dev 的指针数组。 对 down_interruptible 和 up 的调用现在可以先忽略,我们很快会谈到它们。 这段代码看起来相当短小,是因为在调用 open 时它并没有做针对某个设备的任何处理。由于 scull0-3 设备被设计为全局和持久性的,这段代码无须做什么工作。特别是,由于我们并不维护 scull 的打开计数,也就是模块的使用计数,因此也就没有类似于“首次打开时初始化设备”的这 类动作。 既然说内核能通过 owner 成员维护模块的使用计数的话,那读者可能会奇怪为什么在这里却手工 地来增加计数。答案在于早期版本的内核要求模块自己去处理有关维护其使用计数的所有工作,那 时 owner 的机制还并不存在。为在老版本的内核上具有可移植性,scull 自行增加其使用计数。 这样的处理在 2.4 版本的系统中会引起使用计数值过高的情况,但那并不算是个问题,因为当模 块不再被使用时,计数值会降至零。 对设备唯一的实际操作是,当设备以写方式打开时,它的长度将被截为 0。出现这种特性的原因在 于,设计上,用更短的文件覆盖一个 scull 设备时,设备数据区应相应缩小。这与普通文件写打开 时长度截短为 0 的方式很相似。如果设备以读方式打开,则什么也不做。 稍后在浏览其它 scull 设备类型(personality)的代码时,将会看到真正的初始化工作是如何完成 53 第 3 章 字符设备驱动程序 的。 release 方法 release 方法的作用正好与 open 相反。有时读者会发现这个方法的实现被称为 device_close , 而不是 device_release。无论是哪种形式,这个设备方法应该完成下面的任务: „ 释放由 open 分配的,保存在 filp->private_data 中的所有内容 „ 在最后一次关闭操作时关闭设备 „ 使用计数减 1 scull 的基本模型无需执行关闭设备的动作,因此所需的代码量最少*: int scull_release(struct inode *inode, struct file *filp) { MOD_DEC_USE_COUNT; return 0; } 如果在 open 期间递增使用计数的话,则不应忘记对其递减,因为如果使用计数不归 0,内核无 法卸载模块。 如果某个时刻一个尚未被打开的文件被关闭了,计数将如何保证一致呢?要知道,dup 和 fork 都 会在不调用 open 的情况下,创建已打开文件的复本,但每一个都会在程序终止时关闭。例如, 大多数程序从来不打开它们的 stdin 文件(或设备),但它们都会在终止时关闭。 答案很简单:并不是每个 close 系统调用都会引起对 release 方法的调用。仅仅是那些实际释放 设备数据结构的那些 close 调用才会调用这个方法。内核维护一个 file 结构被使用多少次的计数 器。无论是 fork 还是 dup 都不创建新的数据结构(仅由 open 创建),它们只是增加已有结构 中的计数。 只有在 file 结构的计数归 0 时,close 系统调用才会执行 release 方法,这只在删除这个结构时 才会发生。release 方法与 close 系统调用间的关系保证了模块使用计数总是一致的。 注意:flush 方法在应用程序每次调用 close 时都会被调用。不过,很少有驱动程序去实现 flush, 因为在 close 时并没有什么事情需要去做,除非 release 被调用。正如读者猜想的那样,甚至在 应用程序未明确关闭它所打开的文件就中止时,以上的讨论同样也是适用的:内核在进程退出的时 候,通过内部使用 close 系统调用自动关闭相关的文件。 3.4 scull 的内存使用 在介绍读写操作以前,我们最好先看看 scull 如何并且为什么进行内存分配。为了全面理解代码, 我们需要知道“如何分配”,而“为什么分配”则表明了驱动程序编写者所需做出的选择,尽管 scull * 因为 scull_open 为每种设备都替换了不同的 filp->f_op,所以不同的设备由不同的函数关闭。我们会随后看到这 些内容。 54 Linux 设备驱动程序 作为设备来说肯定还不够代表性。 本节只讲解 scull 中的内存分配策略,而不会涉及编写实际驱动程序时需要的硬件管理技巧。这些 技巧将在第 8 章和第 9 章中介绍。因此,如果读者对针对内存操作的 scull 驱动程序的内部工作 原理不感兴趣的话,可以跳过这一节。 scull 使用的内存区域,这里也称为设备,其长度是可变的。写的越多,它就变得越长;用更短的 文件以覆盖方式写设备时则会变短。 scull 的实现方法并不是很巧妙。更为巧妙的实现,其代码读起来会较困难,而本节的目的只是讲 解 read 和 write,并非内存管理。这也就是为什么虽然分配整个页面会更有效,但代码只使用了 kmalloc 和 kfree,而没有采取分配整个页面的操作。 另一方面,从理论和实际角度考虑,我们不想限制“设备”的尺寸。理论角度上,对所管理的数据 项任意增加限制总是很糟糕的想法。实际角度上,为了在内存短缺的情况下进行测试,可利用 scull 暂时将系统的内存吃光,进行这样的测试有助于了解系统内部。我们可以使用命令 cp /dev/zero /dev/scull0 用光所有的系统 RAM,也可以用 dd 工具选择复制多少数据到 scull 设备中。 在 scull 中,每个设备都是一个指针链表,其中每个指针都指向一个 Scull_Dev 结构。默认情况 下,每一个这样的结构通过一个中间指针数组最多可引用 4,000,000 个字节。我们发布的源码使 用了一个有 1000 个指针的数组,每个指针指向一个 4000 字节的区域。我们把每一个内存区称 为一个量子,这个指针数组(或它的长度)称为量子集。scull 设备和它的内存区如图 3-1 所示。 图 3-1:scull 设备的布局 这样,向 scull 写一个字节就会消耗内存 8000 或 12000 个字节:每个量子占用 4000 个,量 子集占用 4000 或 8000 个字节(取决于目标平台上指针本身占用 32 位还是 64 位)。然而, 如果向 scull 写大量的数据,链表的开支并不会太大。每 4MB 数据只对应一个表项,而设备的最 大尺寸被计算机内存的大小所限制。 55 第 3 章 字符设备驱动程序 为量子和量子集选择合适的数值是一个策略问题,而非机制问题,而且最优数值依赖于如何使用设 备。因此 scull 设备的驱动程序不应对量子和量子集的尺寸强制使用某个特定的数值。在 scull 设 备中,用户可以采用几种方式来修改这些值:在编译时,可以修改 scull.h 中的 SCULL_QUANTUM 和 SCULL_QSET;而在模块加载时,可以设置 scull_quantum 和 scull_qset 的整数值;或者在 运行时,使用 ioctl 修改当前值和默认值。 使用宏和整数值同时允许在编译期间和加载阶段进行配置,这种方法和前面选择主设备号的方法类 似。对于驱动程序中任何不确定的,或与策略相关的数值,我们都可以使用这种技巧。 余下的唯一问题是如何选择缺省数值。在这个例子里,量子和量子集未充分填满会导致内存浪费, 而量子和量子集过小则会在进行内存分配、释放和指针链接等操作时增加系统开销,缺省数值的选 择问题就在于寻找这两者之间的最佳平衡点。 此外,还必须考虑 kmalloc 的内部设计,然而目前我们还无法涉及这一点。kmalloc 的内部结构将 在第 7 章 “The Real Story of kmalloc”一节中探讨。 缺省数值的选择基于这样的假设,在测试 scull 时,可能会有大块的数据写到其中,但大多数情况 下,对该设备的正常使用可能只传递几 K 的数据量。 用来保存设备信息的数据结构如下: typedef struct Scull_Dev { void **data; struct Scull_Dev *next; /* next list item */ int quantum; /* the current quantum size */ int qset; /* the current array size */ unsigned long size; devfs_handle_t handle; /* only used if devfs is there */ unsigned int access_key; /* used by sculluid and scullpriv */ struct semaphore sem; /* mutual exclusion semaphore */ } Scull_Dev; 下面的代码说明如何利用 Scull_Dev 保存数据。scull_trim 函数负责释放整个数据区,并且在文件 以写方式打开时由 scull_open 调用。它简单地遍历链表,释放所有找到的量子和量子集。 int scull_trim(Scull_Dev *dev) { Scull_Dev *next, *dptr; int qset = dev->qset; /* "dev" is not null */ int i; for (dptr = dev; dptr; dptr = next) { /* all the list items */ if (dptr->data) { for (i = 0; i < qset; i++) if (dptr->data[i]) kfree(dptr->data[i]); kfree(dptr->data); dptr->data=NULL; } next=dptr->next; if (dptr != dev) kfree(dptr); /* all of them but the first */ } dev->size = 0; dev->quantum = scull_quantum; dev->qset = scull_qset; 56 dev->next = NULL; return 0; } Linux 设备驱动程序 3.5 竞态的简介 读者已经了解到 scull 管理内存的方法,现在,我们考虑这样一种情形:两个进程,A 和 B,它们 都打开了一个相同的 scull 设备用于写操作。这两个进程试图同时添加数据到设备中。这种操作的 成功需要一个新的量子,所以每个进程都分配了所需的内存,并在量子集中保存指向这些新分配内 存的指针。 结果麻烦就来了。因为两个进程所见的是同一个 scull 设备,每个都会把各自新分配的内存指针存 放在量子集的相同位置。如果 A 先存储了它的指针,B 则会在随后同样的存储操作中覆盖原有的指 针。因此 A 分配的内存,以及已经写入这个区域的数据,都会丢失。 这种状况就是一种典型的竞态――结果取决于是 A 还是 B 首先执行存储操作,而且一般地来说 总会有一些无法预期的事情发生。在单处理器的 Linux 系统中,scull 的代码不会遇到这种问题, 因为运行内核代码的进程是非抢占式的。但在 SMP 系统中,情形则要复杂的多。进程 A 和 B 可 能运行在不同的处理器上,从而互相干扰。 Linux 内核提供了几种机制避免并处理竞态。这些机制的完整描述将放在第 9 章,不过在这里有 必要先作一个初期讨论。 信号量是用于资源访问控制的一般机制。在最为简单的形式里,信号量可用于互斥操作――使用互 斥模式的信号量,可以防止多个进程同时运行相同的代码或存取相同的数据。这种信号量通常被成 为 mutex,源于互斥(mutual exclusion)这个词。 Linux 信号量定义在 中,具有 struct semaphore 类型,驱动程序只能使用 给定的接口操作信号量。在 scull 中,每个设备都在 Scull_Dev 结构中分配了一个信号量。因为 设备是彼此独立的,所以没有必要在多个设备间进行互斥操作。 使用信号量之前,必须传递一个数值参数给 sema_init 函数进行初始化。对于互斥型的应用(比 如,避免多个线程同时存取相同的数据),信号量须初始化为 1,即信号量是可用的。作为设备初 始化设置的一部分,scull 模块初始化函数(scull_init)中接下来的代码说明了信号量是如何被初始 化的。 for (i=0; i < scull_nr_devs; i++) { scull_devices[i].quantum = scull_quantum; scull_devices[i].qset = scull_qset; sema_init(&scull_devices[i].sem, 1); } 如果一个进程想进入一段被信号量保护的代码的话,必须首先确保没有其它的进程对此进行访问。 经 典 计 算 机 科 学 把 获 取 信 号 量 的 函 数 功 能 称 为 P , 而 Linux 中 称 其 为 down 或 down_interruptible 。这些函数检查信号量的值,看它是否大于 0;如果是的话,它们将递减信号 量的值并返回。如果信号量的值为 0,那函数将进入睡眠,直到其它进程释放该信号量并将其唤醒 之后,再次进行尝试。down_interruptible 可以用一个信号来打断,但 down 则不允许有信号抵送 57 第 3 章 字符设备驱动程序 到进程。大多数情况都是希望信号起作用的;否则,就有可能建立一个无法杀掉的进程,以及其它 不可预期的结果。但是,允许信号中断将使得信号量的处理复杂化,因为我们总要去检查函数(这 里的 down_interruptible)是否已被中断。一般地,该函数返回 0 时表示成功,非 0 则出错。如果 这个处理过程被中途打断,它并不会获得信号量,因此,也就不能调用 up 函数了。因此,援引信 号量的典型调用通常是下面的这种形式: if (down_interruptible (&sem)) return -ERESTARTSYS; 返回值 -ERESTARTSYS 通知系统操作被信号打断。调用这个设备方法的内核函数或者重新尝试, 或者返回 -EINTR 至应用程序,这取决于应用程序是如何设置信号处理函数的。当然,如果是以 这种方式中断操作的话,代码应在返回前完成清理工作。 获取信号量的进程必须在随后的某个时候释放它。计算机科学称释放操作为 V,而 Linux 使用的 表达方式是 up。一个简单的调用形式就象下面这样: up (&sem); 它递增信号量的值,并唤醒正在等待信号量转为可用状态的的进程。 信号量必须小心使用。被信号量保护的数据必须是定义清晰的,并且存取这些数据的所有代码都必 须首先获得信号量。使用 down_interruptible 来获取信号量的代码不应调用其它也试图获得信号量 的函数,否则就会陷入死锁。如果驱动程序中的某段程序对其持有的信号量释放失败的话(可能就 是一次出错返回的结果),那么其它任何获取该信号量的尝试都将陷在那里。互斥操作一般来说是 很需要技巧的,一个定义良好和条理化的方法会带来很多好处。 在 scull 中,各设备的信号量用以保护对存储数据的存取。任何访问 Scull_Dev 结构之 data 成 员的代码都必须首先获得信号量。为避免死锁,仅仅是那些实现设备方法的函数才会去获取信号量。 而先前介绍过的 scull_trim 等内部程序,则假定信号量已经取得。只要保持这几个方面不变,访 问 Scull_Dev 结构时就可以避免竞态的发生。 3.6 read 和 write 读/写方法完成的任务是相似的,也就是,拷贝数据到应用程序空间,或反过来从应用程序空间拷 贝数据。因此,它们的原型相当相似,不妨同时介绍它们: ssize_t read(struct file *filp, char *buff, size_t count, loff_t *offp); ssize_t write(struct file *filp, const char *buff, size_t count, loff_t *offp); 对于这两个方法,参数 filp 是文件指针,参数 count 是请求传输的数据长度。参数 buff 是指向 用户空间的缓冲区,这个缓冲区或者保存将写入的数据,或者是一个存放新读入数据的空缓冲区。 最后的 offp 是一个指向“long offset type(长偏移量类型)”对象的指针,这个对象指明用户在文 件中存取操作的位置。返回值是“signed size type(有符号的尺寸类型)”,后面会谈到它的使用。 58 Linux 设备驱动程序 谈到数据传输,和两个设备方法的相关的主要问题是,需要在内核地址空间和用户地址空间之间传 输数据,不能用通常的办法利用指针或 memcpy 来完成这样的操作。出于许多原因,不能在内核 空间中直接使用用户空间地址。 内核空间地址与用户空间地址之间很大的一个差异,就是用户空间的内存是可被换出的。当内核访 问用户空间指针时,相对应的页面可能已不在内存中了,这样的话就会产生一个页面错。在本节和 第 5 章将介绍的一些函数使用了一些隐式技巧来正确地处理页面错,即使 CPU 正在内核空间执 行时。 这里,值得注意的是,Linux 2.0 在 x86 平台上,在用户空间和内核空间之间采用了一种完全不 同的内存映射方式。这样,用户空间的指针根本不能在内核空间中使用。如果目标设备不是 RAM 而是扩展卡,也有同样的问题,因为驱动程序必须在用户空间缓冲区和内核空间之间拷贝数据(也 可能是在内核空间和 I/O 内存之间)。 Linux 中此类跨空间的拷贝是由一些特定的函数完成的,它们定义在 中。这样 的拷贝或者是通过一般的(如 memcpy)函数完成,或者是通过为特定的数据大小(char, short, int, long)作了优化的函数来完成,它们大多数将在第 5 章的“使用 ioctl 参数”一节介绍。 scull 的 read 和 write 代码要做的工作,就是在用户地址空间和内核地址空间之间进行整段数据 的拷贝。这种能力是由下面的内核函数提供的,它们用于拷贝任意的一段字节序列,这也是每个 read 和 write 方法实现的核心部分。 unsigned long copy_to_user(void *to, const void *from, unsigned long count); unsigned long copy_from_user(void *to, const void *from, unsigned long count); 虽然这些函数的行为很象通常的 memcpy 函数,但当内核空间内运行的代码访问用户空间时,要 多加小心。被寻址的用户空间的页面可能当前并不在内存,于是页面错的对应处理程序会使访问进 程转入睡眠,直到该页面被传送至期望的位置。例如,当页面必须从交换空间取回时,这样的情况 就会发生。对于驱动程序编写人员,这带来的结果就是访问用户空间的任何函数都必须是可重入的, 并且必须能和其它驱动程序函数并发执行(也可参阅第 5 章的“编写重入代码”)。这就是我们使用 信号量来控制并发访问的原因。 这两个函数的作用并不限于在内核空间和用户空间之间拷贝数据,它们还检查用户空间的指针是否 有效。如果指针无效,就不会进行拷贝;另一方面,如果在拷贝过程中遇到无效地址,则仅仅会复 制部分数据。在这两种情况中,返回值是还未拷贝完的内存的数量值。scull 代码如果发现这样的 错误返回,就会在返回值不为 0 时,返回 -EFAULT 给用户。 关于用户空间访问和无效用户空间指针的内容是相对高级的话题,第 5 章的“使用 ioctl 参数”一 节会对它们进行进一步讨论。如果并不需要检查用户空间指针,那么建议你转而调用 _ _copy_to_user 和 _ _copy_from_user。例如,在知道这些参数已经过检查时,这还是很有用的。 至于谈到实际的设备方法,read 方法的任务是从设备拷贝数据到用户空间(使用 copy_to_user), 而 write 方法则是从用户空间拷贝数据到设备上(使用 copy_from_user)。每次 read 或 write 系 统调用都会请求一定数目的字节传输,不过驱动程序也并不限制小数据量的传输――读/写之间的 59 第 3 章 字符设备驱动程序 确切规则还是有些细微差异的,本章后面的内容会提到。 无论这些方法传输了多少数据,一般而言都应更新 *offp 所表示的文件位置,以便反映在新系统调 用成功完成之后当前的文件位置。大多数时候,offp 参数就是指向 filp->f_pos 的指针,但在对 pread 和 pwrite 系统调用的支持中,使用了一个不同的指针,pread/pwrite 是在单个原子操作 种完成 lseek 和 read/write 等价操作的两个系统调用。图 3-2 表明了一个典型的 read 实现是如 何使用其参数的。 图 3-2:read 的参数 出错时,read 和 write 方法都返回一个负值。大于等于 0 的返回值告诉调用程序成功传输了多 少字节。如果在正确传输部分数据之后发生错误,则返回值必须是成功传输的字节数,这个错误只 能在下一次函数调用时才会得到报告。 尽管内核函数通过返回负值来表示错误,而且该返回值表明了错误的类型(见第 2 章“init_module 中的错误处理”一节),但运行在用户空间的程序看到的始终是作为返回值的 -1。为了找到出错原 因,用户空间的程序必须访问 errno 变量。这种行为上的不同源于 POSIX 的系统调用标准,而 且还有一个好处,就是内核无须处理 errno。 3.6.1 read 方法 调用程序对 read 的返回值解释如下:如果返回值等于作为 count 参数传递给 read 系统调用的 值,所请求的字节数传输就成功完成了。这是最理想的情况。如果返回值是正的,但是比 count 小, 则只有部分数据成功传送。这种情况因设备的不同可能有许多原因。大部分情况下,程序会重新读 数据。例如,如果用 fread 函数读数据,这个库函数会不断调用系统调用,直至所请求的数据传 输完成。 如果返回值为 0,它表示已经到达了文件尾;负值意味着发生了错误,该值指明了发生了什么错误, 错误码在 中定义。比如这样的一些错误:-EINTR(系统调用被打断)或 -EFAULT (无效地址)。 60 Linux 设备驱动程序 上面的表格中遗漏了一种情况,就是“现在还没有数据,但以后会有”。在这种情况下,read 系统 调用应该阻塞。我们将在第 5 章的“阻塞型 I/O”一节中处理阻塞读入。 scull 代码利用了这些规则,特别地,它利用了部分读取的规则。每一次调用 scull_read 只处理一 个数据量子,而不必通过循环收集所有数据;这样一来代码就更短更易读了。如果读取数据的程序 确实需要更多的数据,它可以重新调用这个调用。如果用标准 I/O 库(如 fread 等)读取设备, 应用程序将不会注意到数据传送的量子化过程。 如果当前的读位置超出了设备大小,scull 的 read 方法就返回 0 告知程序这里已经没有数据了(换 句话说就是,我们已经到文件尾了)。如果进程 A 正在读设备,而此时进程 B 以写入模式打开这个 设备,于是设备被截断为长度 0,这种情况是有可能发生的。进程 A 突然发现自己超过了文件尾, 并且在下次调用 read 时返回 0。 下面是 read 的代码: ssize_t scull_read(struct file *filp, char *buf, size_t count, loff_t *f_pos) { Scull_Dev *dev = filp->private_data; /* the first list item */ Scull_Dev *dptr; int quantum = dev->quantum; int qset = dev->qset; int itemsize = quantum * qset; /* how many bytes in the list item */ int item, s_pos, q_pos, rest; ssize_t ret = 0; if (down_interruptible(&dev->sem)) return -ERESTARTSYS; if (*f_pos >= dev->size) goto out; if (*f_pos + count > dev->size) count = dev->size - *f_pos; /* find list item, qset index, and offset in the quantum */ item = (long)*f_pos / itemsize; rest = (long)*f_pos % itemsize; s_pos = rest / quantum; q_pos = rest % quantum; /* follow the list up to the right position (defined elsewhere) */ dptr = scull_follow(dev, item); if (!dptr->data) goto out; /* don't fill holes */ if (!dptr->data[s_pos]) goto out; /* read only up to the end of this quantum */ if (count > quantum - q_pos) count = quantum - q_pos; if (copy_to_user(buf, dptr->data[s_pos]+q_pos, count)) { ret = -EFAULT; goto out; } *f_pos += count; ret = count; out: up(&dev->sem); return ret; } 61 第 3 章 字符设备驱动程序 3.6.2 write 方法 与 read 类似,根据如下返回值规则,write 也能传输少于请求的数据量: 如果返回值等于 count,则完成了请求数目的字节传送。如果返回值是正的,但小于 count,只传 输了部分数据。程序很可能再次试图写入余下的数据。如果值为 0,意味着什么也没写入。这个结 果不是错误,而且也没有理由返回一个错误码。再次说明,标准库会重复调用 write。在第 5 章 介绍阻塞型 write 时,我们将详细说明这种情形。负值意味发生了错误,与 read 相同,有效的错 误码定义在中。 很不幸,有些错误程序只进行了部分传输就报错并异常退出。这种情况的发生是由于程序员习惯于 认定 write 调用要么失败要么就完全成功,在大多数时候的确是这样的,设备驱动也应对此进行支 持。这种局限性在 scull 的实现中可以弥补,但我们不想把代码搞的太复杂,能说明问题就行了, 所以,与 read 方法一样,scull 的 write 代码每次只处理一个量子: ssize_t scull_write(struct file *filp, const char *buf, size_t count, loff_t *f_pos) { Scull_Dev *dev = filp->private_data; Scull_Dev *dptr; int quantum = dev->quantum; int qset = dev->qset; int itemsize = quantum * qset; int item, s_pos, q_pos, rest; ssize_t ret = -ENOMEM; /* value used in "goto out" statements */ if (down_interruptible(&dev->sem)) return -ERESTARTSYS; /* find list item, qset index and offset in the quantum */ item = (long)*f_pos / itemsize; rest = (long)*f_pos % itemsize; s_pos = rest / quantum; q_pos = rest % quantum; /* follow the list up to the right position */ dptr = scull_follow(dev, item); if (!dptr->data) { dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL); if (!dptr->data) goto out; memset(dptr->data, 0, qset * sizeof(char *)); } if (!dptr->data[s_pos]) { dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL); if (!dptr->data[s_pos]) goto out; } /* write only up to the end of this quantum */ if (count > quantum - q_pos) count = quantum - q_pos; if (copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) { ret = -EFAULT; goto out; } *f_pos += count; ret = count; /* update the size */ if (dev->size < *f_pos) dev-> size = *f_pos; 62 Linux 设备驱动程序 out: up(&dev->sem); return ret; } 3.6.3 readv 和 writev Unix 系统很久以来就已支持两个可选的系统调用:readv 和 writev。这些“向量”型的函数具有一 个结构数组,每个结构包含一个指向缓冲区的指针和一个长度值。readv 调用可用于将指定数量的 数据依次读入每个缓冲。writev 则是把各个缓冲区的内容收集起来,并将它们在一次 write 操作中 进行输出。 然而直到 2.3.44 内核,Linux 始终是通过对 read/write 的多次调用来模拟 readv 和 writev 调 用的。如果驱动程序没有提供用于处理向量操作的方法,这类操作也就只好仍采用模拟方法来实现 了。不过,在很多情况下,直接在驱动程序中实现 readv 和 writev 可以获得更高的效率。 向量操作的函数原型如下: ssize_t (*readv) (struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos); ssize_t (*writev) (struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos); 在这里,filp 和 ppos 参数与在 read/write 中的用法是相同的。iovec 结构定义在 中, 形式如下: struct iovec { void *iov_base; _ _kernel_size_t iov_len; }; 每个 iovec 结构都描述了一个用于传输的数据块――这个数据块的起始位置在 iov_base(在用户 空间中),长度为 iov_len 个字节。函数中的 count 参数指明要操作多少个 iovec 结构。这些结构 由应用程序创建,而内核在调用驱动程序之前会把它们拷贝到内核空间。 向量化操作最简单的实现,可能就是只传递每个 iovec 结构的地址和长度给驱动程序的 read 或 write 函数。不过,正确而有效率的操作经常需要驱动程序做一些更为巧妙的事情。例如,磁带驱 动程序的 writev 就应在单个磁带录制过程里写入所有 iovec 结构的内容。 但是很多驱动程序并不期望通过自己实现这些方法来获益。所以,scull 忽略了它们。内核将会通 过 read/write 来模拟它们,而最终结果一样。 3.7 试试新设备 一旦准备好了刚才讲述的四个方法,驱动程序就可以编译和测试了,它保留写入的数据,直至用新 数据覆盖它们。这个设备有点象长度只受物理 RAM 容量限制的数据缓冲区。可以试试用 cp、dd, 或者输入/输出重定向等命令来测试这个驱动程序。 63 第 3 章 字符设备驱动程序 依据写入 scull 的数据量,用 free 命令可以看到空闲内存的缩减和扩增。 为了进一步证实每次读写一个量子,可以在驱动程序的适当位置加入 printk,通过可了解到程序读 /写大数据块时会发生什么事情。此外,还可以用工具 strace 来监视应用程序调用的系统调用以及 它们的返回值。跟踪 cp 或 ls -l > /dev/scull0 会显示出量子化的读写过程。下一章将会详细介绍 监视(或跟踪)技术。 3.8 设备文件系统 正如本章开始所提到的,Linux 内核的新近版本为设备入口点提供了一种特殊的文件系统。这个文 件系统曾一度以一个非正式的补丁形式提供给大家;而在 2.3.46 版本中,它已成为正式源代码树 的一部分。该文件系统对 2.2 版本也是支持的,虽然它并未被正式的 2.2 内核所包括。 尽管在本书编写的时候,这个特殊文件系统还没有被广泛使用,但它的新特点对设备驱动程序的编 写人员会有一定帮助。因此当 devfs 在目标系统上得到应用的时候,我们这个版本的 scull 也将 利用这一文件系统。模块在编译时通过内核配置信息来获知特定的特征是否被支持,在这个例子里, 我们依赖宏 CONFIG_DEVFS_FS 的定义与否来决定是否对这个文件系统进行支持。 devfs 的主要优势如下: „ 设备初始化时在 /dev 目录下创建设备入口点,移除设备时将被删除。 „ 设备驱动程序可以指定设备名、所有者和权限位,而用户空间程序仍可以修改所有者和权限位 (文件名则不能修改)。 „ 不再需要为设备驱动程序分配主设备号以及处理次设备号。 结果是,当模块装载和卸载时,不再需要运行一个脚本来创建设备文件,因为驱动程序会自主地管 理它自己的设备文件。 驱动程序应调用下面这些函数来处理设备的创建和删除工作。 #include devfs_handle_t devfs_mk_dir (devfs_handle_t dir, const char *name, void *info); devfs_handle_t devfs_register (devfs_handle_t dir, const char *name, unsigned int flags, unsigned int major, unsigned int minor, umode_t mode, void *ops, void *info); void devfs_unregister (devfs_handle_t de); devfs 还为内核编码提供了其它的一些函数,它们可以创建符号链接、访问内部数据结构并从索引 节点中获取 devfs_handle_t 项等等任务。这里并不涉及这部分函数,因为它们不太重要并且也不 太容易理解。感兴趣的读者可以参阅头文件以得到更多信息。 64 Linux 设备驱动程序 注册/注销函数中的各类参数如下: dir 新创建的设备文件所在目录。大多数驱动程序使用 NULL 值,把设备文件创建在目录 /dev 下。 如果需要创建一个自己的目录,驱动程序应该调用 devfs_mk_dir 。 name 设备的名称,前面无须加上 /dev/。如果想让设备处于子目录下的话,名字中可以包含斜线符,子 目录会在注册过程中被创建。此外,还可以指定一个指向目标子目录的 dir 指针。 flags devfs 标志的位掩码。DEVFS_FL_DEFAULT 就是个好的选择,DEVFS_FL_AUTO_DEVNUM 是 在需要自动进行主、次设备号分配时使用的标志。随后会说明这些标志。 major minor 设备的主/次设备号。如果在 flags 参数项指定了 DEVFS_FL_AUTO_DEVNUM ,这个参数就不再 有用了。 mode 新设备的访问模式。 ops 指向设备的文件操作数据结构的指针。 info 用于设置 filp->private_data 的缺省值。当设备打开时,文件系统将把 filp->private_data 指针初 始化为该值。传递给 devfs_mk_dir 的 info 指针并不是给 devfs 使用的,它作为“客户私有数据” 指针来使用。 de devfs_register 调用中获得的“devfs 入口”。 flags 参数用于为所创建的设备文件选择特定的功能特点。虽然在 中已 经对 flags 进行了简要而清楚的文档描述,但在这里仍有必要介绍其中的一部分。 DEVFS_FL_NONE DEVFS_FL_DEFAULT 前者只是简单地为 0 值,它是为提高代码的可读性而提出的。后面这个宏目前即定义为 DEVFS_FL_NONE,不过它对与这种文件系统将来的实现保持向前兼容性是个好的选择。 DEVFS_FL_AUTO_OWNER 这个标记使得设备似乎由最后打开它的 uid/gid 所拥有,而且在没有进程打开它的时候,可以被任 何人读取/写入。这个特点对 tty 设备文件非常有用,而且设备驱动程序也可以利用这个特点避免 对某个非共享设备的并发访问。我们将在第 5 章中讨论访问策略问题。 DEVFS_FL_SHOW_UNREG DEVFS_FL_HIDE DEVFS_FL_SHOW_UNREG 标 志 请 求 在 注 销 时 不 要 删 除 /dev 目 录 下 的 设 备 文 件 。 65 第 3 章 字符设备驱动程序 DEVFS_FL_HIDE 标志则请求将设备文件隐藏在 /dev 目录下。通常的设备一般并不需要这两个标 记。 DEVFS_FL_AUTO_DEVNUM 自动为设备分配设备号。即使 devfs 中对应入口文件可能已经被注销,这个设备号仍将保持与设 备名的关联,所以,如果在系统关机之前驱动程序被再次载入,则将获取相同的主/次设备号。 DEVFS_FL_NO_PERSISTENCE 当设备文件删除后,不再保留相关的信息。使用这个标志可以在模块卸载后节省一些系统内存,代 价是丢失了模块卸载/重新装载之间的设备特征的持久性记录。这些持久性特征包括访问模式、文 件所有者和主/次设备号等。 在运行时可以查询设备关联的标志并进行修改。下面的这两个函数完成这个任务: int devfs_get_flags (devfs_handle_t de, unsigned int *flags); int devfs_set_flags (devfs_handle_t de, unsigned int flags); 3.8.1 实际使用 devfs 因为涉及到设备名称,devfs 会带来严重的用户空间的不兼容性,所以并不是所有的系统都会使用 它。读者不太可能在近期编写一个仅支持 devfs 的驱动程序,这与新的特性如何被 Linux 使用者们 所接受并没有关联。因此,需要增加对“老”方法的支持,主要在用户空间中的文件创建和权限处 理,以及在内核空间使用主/次设备号等方面。 实现一个仅支持 devfs 的驱动所需要的代码是支持这两个环境所需代码的子集,所以我们只讲解双 模式下的初始化工作。我们并不去编写一个专门的驱动程序样例去试验 devfs,取而代之的是,为 现有的 scull 驱动程序增加对 devfs 的支持。如果向内核载入使用 devfs 的 scull 模块的话,需 要直接调用 insmod,而不是运行 scull_load 脚本。 我们选择创建一个目录来存放所有的 scull 设备文件,因为 devfs 的结构是高度层次化的,没有 理由不去遵循这个协定。而且,这样的话,还可以演示如何创建一个目录以及删除它。 在 scull_init 中,下面的代码用于设备的创建,其中 Scull_Dev 结构的 handle 成员用来保存被 注册了的设备: /* If we have devfs, create /dev/scull to put files in there */ scull_devfs_dir = devfs_mk_dir(NULL, "scull", NULL); if (!scull_devfs_dir) return -EBUSY; /* problem */ for (i=0; i < scull_nr_devs; i++) { sprintf(devname, "%i", i); devfs_register(scull_devfs_dir, devname, DEVFS_FL_AUTO_DEVNUM, 0, 0, S_IFCHR | S_IRUGO | S_IWUGO, &scull_fops, scull_devices+i); } 上面的代码与下面这段从 scull_cleanup 摘录代码的其中两行是相呼应的。 66 Linux 设备驱动程序 if (scull_devices) { for (i=0; if_ops 和 filp->private_data 的初始化工作。前一个指针只是简单地不作修改,因为 devfs_register 已经指 定了合适的文件操作。后一个指针也仅当值为 NULL 时通过 open 方法进行初始化,因为只有在 不使用 devfs 的情况下,才会是 NULL 值。 /* * If private data is not valid, we are not using devfs * so use the type (from minor nr.) to select a new f_op */ if (!filp->private_data && type) { if (type > SCULL_MAX_TYPE) return -ENODEV; filp->f_op = scull_fop_array[type]; return filp->f_op->open(inode, filp); /* dispatch to specific open */ } /* type 0, check the device number (unless private_data valid) */ dev = (Scull_Dev *)filp->private_data; if (!dev) { if (num >= scull_nr_devs) return -ENODEV; dev = &scull_devices[num]; filp->private_data = dev; /* for other methods */ } 一旦采用了上面的这些代码,scull 就能被载入到运行 devfs 的系统中。执行命令 ls -l /dev/scull, 我们可以看到下面的这些输出: crw-rw-rw- 1 root crw-rw-rw- 1 root crw-rw-rw- 1 root crw-rw-rw- 1 root crw-rw-rw- 1 root crw-rw-rw- 1 root crw-rw-rw- 1 root crw-rw-rw- 1 root crw-rw-rw- 1 root crw-rw-rw- 1 root crw-rw-rw- 1 root crw-rw-rw- 1 root root root root root root root root root root root root root 144, 1 Jan 1 1970 0 144, 2 Jan 1 1970 1 144, 3 Jan 1 1970 2 144, 4 Jan 1 1970 3 144, 5 Jan 1 1970 pipe0 144, 6 Jan 1 1970 pipe1 144, 7 Jan 1 1970 pipe2 144, 8 Jan 1 1970 pipe3 144, 12 Jan 1 1970 priv 144, 9 Jan 1 1970 single 144, 10 Jan 1 1970 user 144, 11 Jan 1 1970 wuser 上列各类文件的功能与“普通的” scull 模块是相同的,仅有的差别在设备路径上:原来是 /dev/scull0,而现在则是 /dev/scull/0。 67 第 3 章 字符设备驱动程序 3.8.2 可移植性问题和 devfs 出于能够在 2.0、2.2 和 2.4 这几个版本的 Linux 系统上编译并正常运行的需要,scull 的源码文 件显得有些复杂。这种可移植性的需求是借助基于宏 CONFIG_DEVFS_FS 的条件编译来实现的。 幸运的是,大多数开发人员在这一点上是有共识的,即 #ifdef 结构出现在函数定义部分时基本上 是个很糟糕的情况(如果是用于头文件的话则是可行的)。因此,增加对 devfs 的支持需要引入必 要的机制以便在代码中完全避免 #ifdef 。在 scull 中,我们仍使用条件编译,因为更老一点内核 版本的头文件还不能对上述方式提供支持。 如果代码只是用于 2.4 内核的话,就可以通过调用内核函数,用这两种方法初始化驱动程序,而 避免条件编译。因为事情都已安排好了,所以其中的一个初始化工作不必做任何事情,而只是成功 返回就行。下面给了一个例子,说明初始化是怎样进行的: #include int init_module() { /* request a major: does nothing if devfs is used */ result = devfs_register_chrdev(major, "name", &fops); if (result < 0) return result; /* register using devfs: does nothing if not in use */ devfs_register(NULL, "name", /* .... */ ); return 0; } 只要小心不致于对已经在内核头文件中作过定义的函数进行重定义的话,就可以在自己的头文件中 采取一些类似的技巧。去除条件编译是件好事情,因为这样可以提高代码的可读性,并借助编译器 分析整个输入文件,来减少可能的 bug 数目。只要是采用了条件编译,就有这样的风险,比如打 字或其它的错误可能被忽视――如果 C 的预处理过程因 #ifdef 正好把这些错误所处的部分丢弃 的话。 例如,下面就是 scull.h 如何在程序的 cleanup 部分避免条件编译的例子。这段代码对于所有的内 核版本都是可移植的,因为它并不依赖于头文件中所知的 devfs 结构。 #ifdef CONFIG_DEVFS_FS /* only if enabled, to avoid errors in 2.0 */ #include #else typedef void * devfs_handle_t; /* avoid #ifdef inside the structure */ #endif sysdep.h 中没有定义任何东西,因为实现这样的一般性代码是非常困难的。每个驱动程序都应根 据自己的需要合理进行安排,以避免在函数代码中出现过多的 #ifdef 语句。而且作为 scull 的一个 例外,我们选择不对 devfs 进行支持。我们希望这里的讨论足以帮助读者去利用 devfs,如果他 们想这样做的话;为了保持代码的简洁,对 devfs 的支持将从余下的示例程序中省略。 3.9 向后兼容性 到此为止,本章已经讲述了 2.4 版本 Linux 内核的编程接口。不幸的是,这些接口在内核发展过程 68 Linux 设备驱动程序 中已经发生了重大的变化。这些变化在实现方式上表现了不断的进步,但同样也对那些期望编写能 在多个内核版本间具有兼容性的驱动程序开发人员带来了挑战。 在本章涉及的范围内,2.4 和 2.2 版本之间的差异为数不多。但 2.2 版本修改了 2.0 当中许多 file_operations 方法的函数原型,而且对用户空间的访问也作了重大修改(更为简化了)。信号量 机制在 2.0 版本中也并不完善。最后要说明的是,2.1 开发系列引入了目录项(dentry)高速缓冲。 3.9.1 文件操作数据结构的变化 众多因素促使 file_operations 方法发生变化。长久以来的 2GB 文件尺寸的限制甚至在 Linux 的 2.0 版本中就带来了问题。其结果是,2.1 开发系列开始使用 loff_t 型的 64 位数据来表示文件位 置和长度。大尺寸文件的支持直到 2.4 版本的内核才被完全整合,但很多基础工作很早以前就已 经做了,并且也已被驱动开发人员所适应。 2.1 开发版本引入的另一个变化是对 read/write 方法增加了 f_pos 指针参数。这个变化用于支持 POSIX 标准的 pread 和 pwrite 系统调用,该参数显式地设置数据读/写的文件偏移量。没有这些 系统调用的话,以线程方式工作的程序在对文件的频繁处理中会产生竞态问题。 几乎所有 2.0 版本的 Linux 方法都显式地接收一个索引节点指针参数。2.1 系列将这个参数从部分 方法中删去,因为它很少被用到。如果需要索引节点指针的话,仍可以通过 filp 参数来获取它。 最终结果是,在 2.0 版本中,最常用的 file_operations 方法的原型就象下面列出的这样: int (*lseek) (struct inode *, struct file *, off_t, int); 注意,Linux 2.0 中调用的是 lseek,并非 llseek。名字的变化是用来区分现在的 seek 可以进行 64 位偏移量的操作。 int (*read) (struct inode *, struct file *, char *, int); "int (*write) (struct inode *, struct file *, const char *, int);" 正如已提到的,Linux 2.0 中这些函数具有索引节点指针参数,而没有位置参数。 void (*release) (struct inode *, struct file *); 在 2.0 版本的内核中,release 方法是不能失败的,因此返回的是 void。 file_operations 结构还有很多其它的变化;我们会在后面遇到它们的章节中谈到。同时,对于我们 所看到的这些变化,很值得花一点时间看看怎样编写能解决这些变化的可移植性代码。这些方法中 的变化非常大,还没有哪种简单精妙的办法可以将它们完全覆盖。 样例代码处理这些变化的办法是定义一组小的封装函数,将旧的 API“翻译”成新的。这些封装程 序只能在 2.0 版本的头文件下编译时才可使用,并且必须在 file_operations 结构中被替换为“真 正的”设备方法。下面这些是为 scull 驱动所设计的封装程序: /* * The following wrappers are meant to make things work with 2.0 kernels 69 第 3 章 字符设备驱动程序 */ #ifdef LINUX_20 int scull_lseek_20(struct inode *ino, struct file *f, off_t offset, int whence) { return (int)scull_llseek(f, offset, whence); } int scull_read_20(struct inode *ino, struct file *f, char *buf, int count) { return (int)scull_read(f, buf, count, &f->f_pos); } int scull_write_20(struct inode *ino, struct file *f, const char *b, int c) { return (int)scull_write(f, b, c, &f->f_pos); } void scull_release_20(struct inode *ino, struct file *f) { scull_release(ino, f); } /* Redefine "real" names to the 2.0 ones */ #define scull_llseek scull_lseek_20 #define scull_read scull_read_20 #define scull_write scull_write_20 #define scull_release scull_release_20 #define llseek lseek #endif /* LINUX_20 */ 用这种方式重新定义名字,也可以为结构成员解决因时间变迁而发生名称改变的问题(比如从 lseek 到 llseek 的变化)。 不必说,这种重定义名称的方法应该小心使用;这些代码行必须出现在 file_operations 结构定义 之前,但在这些名称使用之后????。 还有另外两个不兼容因素与 file_operations 结构相关。一个是 flush 方法在 2.1 版本的开发中被加 入。驱动开发人员几乎毫无必要去担心这个方法,但它在结构中的存在仍然可能带来问题。避免处 理 flush 方法的最好办法是采用标记化的初始化语法,正如我们在所有的样例源码文件中所做的那 样。 另外一个碍事的不同是,索引节点指针需要从 file 指针中获得。现代内核使用 dentry(目录项) 数据结构,但版本 2.0 中却没有这个结构。因此,sysdep.h 定义了一个宏,利用这个宏,可从 filp 中访问索引节点――它隐藏了版本间的差异。 #ifdef LINUX_20 # define INODE_FROM_F(filp) ((filp)->f_inode) #else # define INODE_FROM_F(filp) ((filp)->f_dentry->d_inode) #endif 70 Linux 设备驱动程序 3.9.2 模块使用计数 在 2.2 及更早期的内核中,Linux 内核对维护模块的使用计数并不提供任何帮助。模块只能自行完 成这样的工作。这种方法容易出错并且需要进行许多重复工作,而且也会造成竞态的发生,新的方 法因而明显改进了。 然而,为编写可移植代码,必须能处理早期版本的工作方式。也就是说,使用计数在新增对模块的 一个引用时仍必须递增,反之递减。可移植的代码也必须处理早期版本内核的 file_operations 结 构并不存在 owner 成员这一问题。解决这个问题最简单的办法是使用 SET_MODULE_OWNER, 而不是直接使用 owner 成员。在 sysdep.h 中,我们为不具有这一功能的内核提供了一个无效的 SET_FILE_OWNER 宏。 3.9.3 信号量支持的变化 信号量的支持在 2.0 版本内核的开发中考虑得很少,对 SMP 系统的支持在那时也比较原始。仅为 这个版本编写的驱动根本没有必要使用信号量,因为那时内核代码是运行在单 CPU 上的。尽管如 此,还是有对信号量的需求,这对后期内核版本所需要的完整保护并无损害。 本章涉及的大多数信号量函数在 2.0 内核中就已经存在了。所例外的是 sema_init,在 2.0 版本中, 程序员须手工初始信号量。sysdep.h 头文件通过定义另一个版本的 sema_init 来解决这个问题, 它必须在 2.0 内核下编译。 #ifdef LINUX_20 # ifdef MUTEX_LOCKED /* Only if semaphore.h included */ extern inline void sema_init (struct semaphore *sem, int val) { sem->count = val; sem->waking = sem->lock = 0; sem->wait = NULL; } # endif #endif /* LINUX_20 */ 3.9.4 用户空间访问的变化 最后,对用户空间的访问在 2.1 系列版本开发的一开始就完全改变了。新的接口有了更好的设计, 并能更好地利用硬件以保证对用户空间内存的安全访问。但,接口当然已不同了。2.0 版本的内存 访问函数如下: void memcpy_fromfs(void *to, const void *from, unsigned long count); void memcpy_tofs(void *to, const void *from, unsigned long count); 这些函数的名称来源于历史上对 i386 FS 段寄存器的使用。注意,这些函数没有返回值;如果用户 提供的是无效地址,数据拷贝会没有任何提示地失败。sysdep.h 隐藏了重命名的处理,并允许可 移植性地调用 copy_to_user 和 copy_from_user 。 71 第 3 章 字符设备驱动程序 3.10 快速索引 本章介绍了下列符号和头文件。file_operations 结构和 file 结构的成员清单并没有在这里给出。 #include “文件系统”头文件,它是编写设备驱动程序比许的头文件。所有重要的函数都在这里声明。 int register_chrdev(unsigned int major, const char *name, struct file_operations *fops); 注册字符设备驱动程序。如果主设备不为 0,则不加修改;如果主设备号为 0,系统动态给这个设 备分配一个新设备号。 int unregister_chrdev(unsigned int major, const char *name); 在卸载时注销驱动程序。major 和 name 字符串都必须存放与注册驱动程序时相同的值。 kdev_t inode->i_rdev; 当前设备的设备“号”,可从索引节点结构中获取。 int MAJOR(kdev_t dev); int MINOR(kdev_t dev); 这两个宏从设备项中分解出主/次设备号。 kdev_t MKDEV(int major, int minor); 这个宏由主/次设备号构造 kdev_t 数据项。 SET_MODULE_OWNER(struct file_operations *fops) 这个宏用来设置指定 file_operations 结构中的 owner 成员。 #include 定义信号量相关的函数和数据类型。 void sema_init (struct semaphore *sem, int val); 将信号量初始化为一个给定值。互斥信号量通常初始化为 1。 int down_interruptible (struct semaphore *sem); void up (struct semaphore *sem); 分别用于获取信号量(必要时转入睡眠)和释放信号量。 #include #include segment.h 在 2.0 及以上内核中定义跨地址空间拷贝的相关函数。2.1 版本系列将其名称改为 uaccess.h。 unsigned long _ _copy_from_user (void *to, const void *from, unsigned long count); unsigned long _ _copy_to_user (void *to, const void *from, unsigned long count); 在用户空间和内核空间之间拷贝数据。 void memcpy_fromfs(void *to, const void *from, unsigned long count); void memcpy_tofs(void *to, const void *from, unsigned long count); 72 Linux 设备驱动程序 这些函数在 2.0 版本内核上,用来从用户空间到内核空间拷贝字节数组,以及相反方向的拷贝。 #include devfs_handle_t devfs_mk_dir (devfs_handle_t dir, const char *name, void *info); devfs_handle_t devfs_register (devfs_handle_t dir, const char *name, unsigned int flags,unsigned int major, unsigned int minor, umode_t mode, void *ops, void *info); void devfs_unregister (devfs_handle_t de); 这些是用于在设备文件系统(devfs)中注册设备的基本函数。 73 第 4 章 调试技术 第 4 章 调试技术 对于任何一位内核代码的编写者来说,最急迫的问题之一就是如何完成调试。由于内核是一个不与 特定进程相关的功能集合,所以内核代码无法轻易地放在调试器中执行,而且也很难跟踪。同样, 要想复现内核代码中的错误也是相当困难的,因为这种错误可能导致整个系统崩溃,这样也就破坏 了可以用来跟踪它们的现场。 本章将介绍在这种令人痛苦的环境下监视内核代码并跟踪错误的技术。 4.1 通过打印调试 最普通的调试技术就是监视,即在应用程序编程中,在一些适当的地点调用 printf 显示监视信息。 调试内核代码的时候,则可以用 printk 来完成相同的工作。 4.1.1 printk 在前面的章节中,我们只是简单假设 printk 工作起来和 printf 很类似。现在则是介绍它们之间一 些不同点的时候了。 其中一个差别就是,通过附加不同日志级别(loglevel),或者说消息优先级,可让 printk 根据这些 级别所标示的严重程度,对消息进行分类。一般采用宏来指示日志级别,例如,KERN_INFO,我 们在前面已经看到它被添加在一些打印语句的前面,它就是一个可以使用的消息日志级别。日志级 别宏展开为一个字符串,在编译时由预处理器将它和消息文本拼接在一起;这也就是为什么下面的 例子中优先级和格式字串间没有逗号的原因。下面有两个 printk 的例子,一个是调试信息,一个 是临界信息: printk(KERN_DEBUG "Here I am: %s:%i\n", _ _FILE_ _, _ _LINE_ _); printk(KERN_CRIT "I'm trashed; giving up on %p\n", ptr); 在头文件 中定义了 8 种可用的日志级别字符串。 KERN_EMERG 用于紧急事件消息,它们一般是系统崩溃之前提示的消息。 KERN_ALERT 74 Linux 设备驱动程序 用于需要立即采取动作的情况。 KERN_CRIT 临界状态,通常涉及严重的硬件或软件操作失败。 KERN_ERR 用于报告错误状态;设备驱动程序会经常使用 KERN_ERR 来报告来自硬件的问题。 KERN_WARNING 对可能出现问题的情况进行警告,这类情况通常不会对系统造成严重问题。 KERN_NOTICE 有必要进行提示的正常情形。许多与安全相关的状况用这个级别进行汇报。 KERN_INFO 提示性信息。很多驱动程序在启动的时候,以这个级别打印出它们找到的硬件信息。 KERN_DEBUG 用于调试信息。 每个字符串(以宏的形式展开)代表一个尖括号中的整数。整数值的范围从 0 到 7,数值越小,优 先级就越高。 没有指定优先级的 printk 语句默认采用的级别是 DEFAULT_MESSAGE_LOGLEVEL,这个宏在 文件 kernel/printk.c 中指定为一个整数值。在 Linux 的开发过程中,这个默认的级别值已经有过 好几次变化,所以我们建议读者始终指定一个明确的级别。 根据日志级别,内核可能会把消息打印到当前控制台上,这个控制台可以是一个字符模式的终端、 一个串口打印机或是一个并口打印机。如果优先级小于 console_loglevel 这个整数值的话,消息 才能显示出来。如果系统同时运行了 klogd 和 syslogd,则无论 console_loglevel 为何值,内 核消息都将追加到 /var/log/messages 中(否则的话,除此之外的处理方式就依赖于对 syslogd 的 设置)。如果 klogd 没有运行,这些消息就不会传递到用户空间,这种情况下,就只好查看 /proc/kmsg 了。 变 量 console_loglevel 的 初 始 值 是 DEFAULT_CONSOLE_LOGLEVEL , 而 且 还 可 以 通 过 sys_syslog 系统调用进行修改。调用 klogd 时可以指定 -c 开关选项来修改这个变量, klogd 的 man 手册页对此有详细说明。注意,要修改它的当前值,必须先杀掉 klogd,再加 -c 选项重新启 动它。此外,还可以编写程序来改变控制台日志级别。读者可以在 O’Reilly 的 FTP 站点提供的 源文件 miscprogs/setlevel.c 里找到这样的一段程序。新优先级被指定为一个 1 到 8 之间的整数 值。如果值被设为 1,则只有级别为 0(KERN_EMERG) 的消息才能到达控制台;如果设为 8, 则包括调试信息在内的所有消息都能显示出来。 如果在控制台上工作,而且常常遇到内核错误(参见本章后面的“调试系统故障”一节)的话,就 有必要降低日志级别,因为出错处理代码会把 console_loglevel 增为它的最大数值,导致随后的 所有消息都显示在控制台上。如果需要查看调试信息,就有必要提高日志级别;这在远程调试内核, 并且在交互会话未使用文本控制台的情况下,是很有帮助的。 75 第 4 章 调试技术 从 2.1.31 这个版本起,可以通过文本文件 /proc/sys/kernel/printk 来读取和修改控制台的日志级 别。这个文件容纳了 4 个整数值。读者可能会对前面两个感兴趣:控制台的当前日志级别和默认 日志级别。例如,在最近的这些内核版本中,可以通过简单地输入下面的命令使所有的内核消息得 到显示: # echo 8 > /proc/sys/kernel/printk 不过,如果仍在 2.0 版本下的话,就需要使用 setlevel 这样的工具了。 现在大家应该清楚为什么在 hello.c 范例中使用 <1> 这些标记了,它们用来确保这些消息能在控 制台上显示出来。 对于控制台日志策略,Linux 考虑到了某些灵活性,也就是说,可以发送消息到一个指定的虚拟控 制台(假如控制台是文本屏幕的话)。默认情况下,“控制台”就是当前地虚拟终端。可以在任何一 个控制台设备上调用 ioctl(TIOCLINUX),来指定接收消息的虚拟终端。下面的 setconsole 程 序,可选择专门用来接收内核消息的控制台;这个程序必须由超级用户运行,在 misc-progs 目录 里可以找到它。下面是程序的代码: int main(int argc, char **argv) { char bytes[2] = {11,0}; /* 11 is the TIOCLINUX cmd number */ if (argc==2) bytes[1] = atoi(argv[1]); /* the chosen console */ else { fprintf(stderr, "%s: need a single arg\n",argv[0]); exit(1); } if (ioctl(STDIN_FILENO, TIOCLINUX, bytes)<0) { /* use stdin */ fprintf(stderr,"%s: ioctl(stdin, TIOCLINUX): %s\n", argv[0], strerror(errno)); exit(1); } exit(0); } setconsole 使用了特殊的 ioctl 命令:TIOCLINUX ,这个命令可以完成一些特定的 Linux 功能。 使用 TIOCLINUX 时,需要传给它一个指向字节数组的指针参数。数组的第一个字节指定所请求 子命令的数字,接下去的字节所具有的功能则由这个子命令决定。在 setconsole 中,使用的子命 令是 11,后面那个字节(存于 bytes[1]中)标识虚拟控制台。关于 TIOCLINUX 的详尽描述可以 在内核源码中的 drivers/char/tty_io.c 文件得到。 4.1.2 消息如何被记录 printk 函数将消息写到一个长度为 LOG_BUF_LEN(定义在 kernel/printk.c 中)字节的循环缓冲 区中,然后唤醒任何正在等待消息的进程,即那些睡眠在 syslog 系统调用上的进程,或者读取 /proc/kmesg 的进程。这两个访问日志引擎的接口几乎是等价的,不过请注意,对 /proc/kmesg 进 行读操作时,日志缓冲区中被读取的数据就不再保留,而 syslog 系统调用却能随意地返回日志数 据,并保留这些数据以便其它进程也能使用。一般而言,读 /proc 文件要容易些,这使它成为 klogd 的默认方法。 76 Linux 设备驱动程序 手工读取内核消息时,在停止 klogd 之后,可以发现 /proc 文件很象一个 FIFO,读进程会阻塞在 里面以等待更多的数据。显然,如果已经有 klogd 或其它的进程正在读取相同的数据,就不能采 用这种方法进行消息读取,因为会与这些进程发生竞争。 如果循环缓冲区填满了,printk 就绕回缓冲区的开始处填写新数据,覆盖最陈旧的数据,于是记录 进程就会丢失最早的数据。但与使用循环缓冲区所带来的好处相比,这个问题可以忽略不计。例如, 循环缓冲区可以使系统在没有记录进程的情况下照样运行,同时覆盖那些不再会有人去读的旧数 据,从而使内存的浪费减到最少。Linux 消息处理方法的另一个特点是,可以在任何地方调用 printk, 甚至在中断处理函数里也可以调用,而且对数据量的大小没有限制。而这个方法的唯一缺点就是可 能丢失某些数据。 klogd 运行时,会读取内核消息并将它们分发到 syslogd,syslogd 随后查看 /etc/syslog.conf , 找出处理这些数据的方法。syslogd 根据设施和优先级对消息进行区分;这两者的允许值均定义在 中。内核消息由 LOG_KERN 设施记录,并以 printk 中使用的优先级记录(例如, printk 中使用的 KERN_ERR 对应于 syslogd 中的 LOG_ERR)。如果没有运行 klogd,数据将保 留在循环缓冲区中,直到某个进程读取或缓冲区溢出为止。 如果想避免因为来自驱动程序的大量监视信息而扰乱系统日志,则可以为 klogd 指定 -f (file) 选 项,指示 klogd 将消息保存到某个特定的文件,或者修改 /etc/syslog.conf 来适应自己的需求。 另一种可能的办法是采取强硬措施:杀掉 klogd,而将消息详细地打印到空闲的虚拟终端上。* 或者在一个未使用的 xterm 上执行 cat /proc/kmesg 来显示消息。 4.1.3 开启及关闭消息 在驱动程序开发的初期阶段,printk 对于调试和测试新代码是相当有帮助的。不过,当正式发布驱 动程序时,就得删除这些打印语句,或至少让它们失效。不幸的是,你可能会发现这样的情况,在 删除了那些已被认为不再需要的提示消息后,又需要实现一个新的功能(或是有人发现了一个 bug),这时,又希望至少把一部分消息重新开启。这两个问题可以通过几个办法解决,以便全局 地开启或禁止消息,并能对个别消息进行开关控制。 我们在这里给出了一个编写 printk 调用的方法,可个别或全局地对它们进行开关;这个技巧是定 义一个宏,在需要时,这个宏展开为一个 printk(或 printf)调用。 可以通过在宏名字中删减或增加一个字母,打开或关闭每一条打印语句。 编译前修改 CFLAGS 变量,则可以一次关闭所有消息。 同样的打印语句既可以用在内核态也可以用在用户态,因此,关于这些额外的信息,驱动和测试程 序可以用同样的方法来进行管理。 * 例如,使用下面的命令可设置 10 号终端用于消息的显示: setlevel 8 setconsole 10 77 第 4 章 调试技术 下面这些来自 scull.h 的代码,就实现了这些功能。 #undef PDEBUG /* undef it, just in case */ #ifdef SCULL_DEBUG # ifdef _ _KERNEL_ _ /* This one if debugging is on, and kernel space */ # define PDEBUG(fmt, args...) printk( KERN_DEBUG "scull: " fmt, ## args) # else /* This one for user space */ # define PDEBUG(fmt, args...) fprintf(stderr, fmt, ## args) # endif #else # define PDEBUG(fmt, args...) /* not debugging: nothing */ #endif #undef PDEBUGG #define PDEBUGG(fmt, args...) /* nothing: it's a placeholder */ 符号 PDEBUG 依赖于是否定义了 SCULL_DEBUG,它能根据代码所运行的环境选择合适的方式 显示信息:内核态运行时使用 printk 系统调用;用户态下则使用 libc 调用 fprintf,向标准错误设备 进行输出。符号 PDEBUGG 则什么也不做;它可以用来将打印语句注释掉,而不必把它们完全删 除。 为了进一步简化这个过程,可以在 Makefile 加上下面几行: # Comment/uncomment the following line to disable/enable debugging DEBUG = y # Add your debugging flag (or not) to CFLAGS ifeq ($(DEBUG),y) DEBFLAGS = -O -g -DSCULL_DEBUG # "-O" is needed to expand inlines else DEBFLAGS = -O2 endif CFLAGS += $(DEBFLAGS) 本节所给出的宏依赖于 gcc 对 ANSI C 预编译器的扩展,这种扩展支持了带可变数目参数的宏。对 gcc 的这种依赖并不是什么问题,因为内核对 gcc 特性的依赖更强。此外,Makefile 依赖于 GNU 的 make 版本;基于同样的道理,这也不是什么问题。 如果读者熟悉 C 预编译器,可以将上面的定义进行扩展,实现“调试级别”的概念,这需要定义 一组不同的级别,并为每个级别赋一个整数(或位掩码),用以决定各个级别消息的详细程度。 但是每一个驱动程序都会有自身的功能和监视需求。良好的编程技术在于选择灵活性和效率的最佳 折衷点,对读者来说,我们无法预知最合适的点在哪里。记住,预处理程序的条件语句(以及代码 中的常量表达式)只在编译时执行,要再次打开或关闭消息必须重新编译。另一种方法就是使用 C 条件语句,它在运行时执行,因此可以在程序运行期间打开或关闭消息。这是个很好的功能,但每 次代码执行时系统都要进行额外的处理,甚至在消息关闭后仍然会影响性能。有时这种性能损失是 无法接受的。 在很多情况下,本节提到的这些宏都已被证实是很有用的,仅有的缺点是每次开启和关闭消息显示 时都要重新编译模块。 78 Linux 设备驱动程序 4.2 通过查询调试 上一节讲述了 printk 是如何工作的以及如何使用它,但还没谈到它的缺点。 由于 syslogd 会一直保持对其输出文件的同步刷新,每打印一行都会引起一次磁盘操作,因此大 量使用 printk 会严重降低系统性能。从 syslogd 的角度来看,这样的处理是正确的。它试图把每 件事情都记录到磁盘上,以防系统万一崩溃时,最后的记录信息能反应崩溃前的状况;然而,因处 理调试信息而使系统性能减慢,是大家所不希望的。这个问题可以通过在 /etc/syslogd.conf 中日 志文件的名字前面,前缀一个减号符解决。* 修改配置文件带来的问题在于,在完成调试之后改动将依旧保留;即使在一般的系统操作中,当希 望尽快把信息刷新到磁盘时,也是如此。如果不愿作这种持久性修改的话,另一个选择是运行一个 非 klogd 程序(如前面介绍的 cat /proc/kmesg),但这样并不能为通常的系统操作提供一个合适 的环境。 多数情况中,获取相关信息的最好方法是在需要的时候才去查询系统信息,而不是持续不断地产生 数据。实际上,每个 Unix 系统都提供了很多工具,用于获取系统信息,如:ps、netstat、vmstat 等等。 驱动程序开发人员对系统进行查询时,可以采用两种主要的技术:在 /proc 文件系统中创建文件, 或者使用驱动程序的 ioctl 方法。/proc 方式的另一个选择是使用 devfs,不过用于信息查找时, /proc 更为简单一些。 4.2.1 使用 /proc 文件系统 /proc 文件系统是一种特殊的、由程序创建的文件系统,内核使用它向外界输出信息。/proc 下面 的每个文件都绑定于一个内核函数,这个函数在文件被读取时,动态地生成文件的“内容”。我们 已经见到过这类文件的一些输出情况,例如,/proc/modules 列出的是当前载入模块的列表。 Linux 系统对/proc 的使用很频繁。现代 Linux 系统中的很多工具都是通过 /proc 来获取它们的信 息,例如 ps、top 和 uptime。有些设备驱动程序也通过 /proc 输出信息,你的驱动程序当然也可 以这么做。因为 /proc 文件系统是动态的,所以驱动程序模块可以在任何时候添加或删除其中的 文件项。 特征完全的 /proc 文件项相当复杂;在所有的这些特征当中,有一点要指出的是,这些 /proc 文 件不仅可以用于读出数据,也可以用于写入数据。不过,大多数时候,/proc 文件项是只读文件。 本节将只涉及简单的只读情形。如果有兴趣实现更为复杂的事情,读者可以先在这里了解基础知识, 然后参考内核源码来建立完整的认识。 所有使用 /proc 的模块必须包含 ,通过这个头文件定义正确的函数。 * 这个减号是个“特殊”标记,避免 syslogd 在每次出现新信息时都去刷新磁盘文件,这些内容记述在 syslog.conf(5) 中,这个手册页很值得一读。 79 第 4 章 调试技术 为创建一个只读 /proc 文件,驱动程序必须实现一个函数,用于在文件读取时生成数据。当某个 进程读这个文件时(使用 read 系统调用),请求会通过两个不同接口的其中之一发送到驱动程序 模块,使用哪个接口取决于注册情况。我们先把注册放到本节后面,先直接讲述读接口。 无论采用哪个接口,在这两种情况下,内核都会分配一页内存(也就是 PAGE_SIZE 个字节),驱 动程序向这片内存写入将返回给用户空间的数据。 推荐的接口是 read_proc,不过还有一个名为 get_info 的老一点的接口。 int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data); 参数表中的 page 指针指向将写入数据的缓冲区;start 被函数用来说明有意义的数据写在页面的 什么位置(对此后面还将进一步谈到);offset 和 count 这两个参数与在 read 实现中的用法相同。 eof 参数指向一个整型数,当没有数据可返回时,驱动程序必须设置这个参数;data 参数是一个 驱动程序特有的数据指针,可用于内部记录。* 这个函数可以在 2.4 内核中使用,如果使用我们的 sysdep.h 头文件,那么在 2.2 内核中也可以用 这个函数。 int (*get_info)(char *page, char **start, off_t offset, int count); get_info 是一个用来读取 /proc 文件的较老接口。所有的参数与 read_proc 中的对应参数用法相 同。缺少的是报告到达文件尾的指针和由 data 指针带来的面向对象风格。这个函数可以用在所有 我们感兴趣的内核版本中(尽管在它 2.0 版本的实现中有一个额外未用的参数)。 这两个函数的返回值都是实际放入页面缓冲区的数据的字节数,这一点与 read 函数对其它类型文 件的处理相同。另外还有 *eof 和 *start 这两个输出值。eof 只是一个简单的标记,而 start 的用 法就有点复杂了。 对于 /proc 文件系统的用户扩展,其最初实现中的主要问题在于,数据传输只使用单个内存页面。 这样就把用户文件的总体尺寸限制在了 4KB 以内(或者是适合于主机平台的其它值)。start 参数 在这里就是用来实现大数据文件的,不过该参数可以被忽略。 如果 proc_read 函数不对 *start 指针进行设置(它最初为 NULL),内核就会假定 offset 参数被 忽略,并且数据页包含了返回给用户空间的整个文件。反之,如果需要通过多个片段创建一个更大 的文件,则可以把 *start 赋值为页面指针,因此调用者也就知道了新数据放在缓冲区的开始位置。 当然,应该跳过前 offset 个字节的数据,因为这些数据已经在前面的调用中返回。 长久以来,关于 /proc 文件还有另一个主要问题,这也是 start 意图解决的一个问题。有时,在连 续的 read 调用之间,内核数据结构的 ASCII 表述会发生变化,以至于读进程发现前后两次调用 所获得的数据不一致。如果把 *start 设为一个小的整数值,调用程序可以利用它来增加 filp->f_pos 的值,而不依赖于返回的数据量,因此也就使 f_pos 成为 read_proc 或 get_info 程序中的一个 内部记录值。例如,如果 read_proc 函数从一个大的结构数组返回数据,并且这些结构的前 5 个 * 纵览全书,我们还会发现这样的一些指针;它们表示了这类处理中有关的“对象”,与 C++ 中的同类处理有些相 似。 80 Linux 设备驱动程序 已经在第一次调用中返回,那么可将 *start 设置为 5。下次调用中这个值将被作为偏移量;驱动 程序也就知道应该从数组的第六个结构开始返回数据。这种方法被它的作者称作“hack”,可以在 /fs/proc/generic.c 中看到。 现在我们来看个例子。下面是 scull 设备 read_proc 函数的简单实现: int scull_read_procmem(char *buf, char **start, off_t offset, int count, int *eof, void *data) { int i, j, len = 0; int limit = count - 80; /* Don't print more than this */ for (i = 0; i < scull_nr_devs && len <= limit; i++) { Scull_Dev *d = &scull_devices[i]; if (down_interruptible(&d->sem)) return -ERESTARTSYS; len += sprintf(buf+len,"\nDevice %i: qset %i, q %i, sz %li\n", i, d->qset, d->quantum, d->size); for (; d && len <= limit; d = d->next) { /* scan the list */ len += sprintf(buf+len, " item at %p, qset at %p\n", d, d->data); if (d->data && !d->next) /* dump only the last item - save space */ for (j = 0; j < d->qset; j++) { if (d->data[j]) len += sprintf(buf+len," % 4i: %8p\n", j,d->data[j]); } } up(&scull_devices[i].sem); } *eof = 1; return len; } 这是一个相当典型的 read_proc 实现。它假定决不会有这样的需求,即生成多于一页的数据,因 此忽略了 start 和 offset 值。但是,小心不要超出缓冲区,以防万一。 使用 get_info 接口的 /proc 函数与上面说明的 read_proc 非常相似,除了没有最后的那两个参 数。既然这样,则通过返回少于调用者预期的数据(也就是少于 count 参数),来提示已到达文件 尾。 一旦定义好了一个 read_proc 函数,就需要把它与一个 /proc 文件项连接起来。依赖于将要支持 的内核版本,有两种方法可以建立这样的连接。最容易的方法是简单地调用 create_proc_read_entry,但这只能用于 2.4 内核(如果使用我们的 sysdep.h 头文件,则也可用 于 2.2 内核)。下面就是 scull 使用的调用,以 /proc/scullmem 的形式来提供 /proc 功能。 create_proc_read_entry("scullmem", 0 /* default mode */, NULL /* parent dir */, scull_read_procmem, NULL /* client data */); 这个函数的参数表包括:/proc 文件项的名称、应用于该文件项的文件许可权限(0 是个特殊值, 会被转换为一个默认的、完全可读模式的掩码)、文件父目录的 proc_dir_entry 指针(我们使用 NULL 值 使 该 文 件 项 直 接 定 位 在 /proc 下 )、 指 向 read_proc 的 函 数 指 针 , 以 及 将 传 递 给 81 第 4 章 调试技术 read_proc 函数的数据指针。 目录项指针(proc_dir_entry)可用来在 /proc 下创建完整的目录层次结构。不过请注意,将文件 项置于 /proc 的子目录中有更为简单的方法,即把目录名称作为文件项名称的一部分――只要目 录本身已经存在。例如,有个新的约定,要求设备驱动程序对应的 /proc 文件项应转移到子目录 driver/ 中;scull 可以简单地指定它的文件项名称为 driver/scullmem,从而把它的 /proc 文件放 到这个子目录中。 当 然 , 在 模 块 卸 载 时 , /proc 中 的 文 件 项 也 应 被 删 除 。 remove_proc_entry 就 是 用 来 撤 消 create_proc_read_entry 所做工作的函数。 remove_proc_entry("scullmem", NULL /* parent dir */); 另一个创建 /proc 文件项的方法是,创建并初始化一个 proc_dir_entry 结构,并将该结构传递给 函数 proc_register_dynamic (2.0 版本)或 proc_register(2.2 版本,如果结构中的索引节点号为 0,该函数即认为是动态文件)。作为一个例子,当在 2.0 内核的头文件下进行编译时,考虑下面 scull 所使用的这些代码: static int scull_get_info(char *buf, char **start, off_t offset, int len, int unused) { int eof = 0; return scull_read_procmem (buf, start, offset, len, &eof, NULL); } struct proc_dir_entry scull_proc_entry = { namelen: 8, name: "scullmem", mode: S_IFREG | S_IRUGO, nlink: 1, get_info: scull_get_info, }; static void scull_create_proc() { proc_register_dynamic(&proc_root, &scull_proc_entry); } static void scull_remove_proc() { proc_unregister(&proc_root, scull_proc_entry.low_ino); } 代码声明了一个使用 get_info 接口的函数,并填写了一个 proc_dir_entry 结构,用于对文件系统 进行注册。 这段代码借助 sysdep.h 中宏定义的支持,提供了 2.0 和 2.4 内核之间的兼容性。因为 2.0 内核 不支持 read_proc,它使用了 get_info 接口。如果对 #ifdef 作一些更多的处理,可以使这段代码 在 2.2 内核中使用 read_proc,不过这样收益并不大。 82 Linux 设备驱动程序 4.2.2 ioctl 方法 ioctl 是作用于文件描述符之上的一个系统调用,我们会在下一章介绍它的用法;它接收一个“命令” 号,用以标识将执行的命令;以及另一个(可选的)参数,通常是个指针。 做为替代 /proc 文件系统的方法,可以为调试设计若干 ioctl 命令。这些命令从驱动程序复制相关 数据到用户空间,在用户空间中可以查看这些数据。 使用 ioctl 获取信息比起 /proc 来要困难一些,因为需要另一个程序调用 ioctl 并显示结果。这个 程序是必须编写并编译的,而且要和测试模块配合一致。从另一方面来说,相对实现 /proc 文件 所需的工作,驱动程序的编码则更为容易些。 有时 ioctl 是获取信息的最好方法,因为它比起读 /proc 要快得多。如果在数据写到屏幕之前要完 成某些处理工作,以二进制获取数据要比读取文本文件有效得多。此外,ioctl 并不要求把数据分 割成不超过一个内存页面的片断。 ioctl 方法的一个优点是,在结束调试之后,用来取得信息的这些命令仍可以保留在驱动程序中。 /proc 文件对任何查看这个目录的人都是可见的(很多人可能会纳闷“这些奇怪的文件是用来做什么 的”),然而与 /proc 文件不同,未公开的 ioctl 命令通常都不会被注意到。此外,万一驱动程序有 什么异常,这些命令仍然可以用来调试。唯一的缺点就是模块会稍微大一些。 4.3 通过监视调试 有时,通过监视用户空间中应用程序的运行情况,可以捕捉到一些小问题。监视程序同样也有助于 确认驱动程序工作是否正常。例如,看到 scull 的 read 实现如何响应不同数据量的 read 请求后, 我们就可以判断它是否工作正常。 有许多方法可监视用户空间程序的工作情况。可以用调试器一步步跟踪它的函数,插入打印语句, 或者在 strace 状态下运行程序。在检查内核代码时,最后一项技术最值得关注,我们将在此对它 进行讨论。 strace 命令是一个功能非常强大的工具,它可以显示程序所调用的所有系统调用。它不仅可以显 示调用,而且还能显示调用参数,以及用符号方式表示的返回值。当系统调用失败时,错误的符号 值(如 ENOMEM)和对应的字符串(如 Out of memory)都能被显示出来。strace 有许多命令行 选项;最为有用的是 -t,用来显示调用发生的时间;-T,显示调用所花费的时间; -e,限定被跟 踪的调用类型;-o,将输出重定向到一个文件中。默认情况下,strace 将跟踪信息打印到 stderr 上。 strace 从内核中接收信息。这意味着一个程序无论是否按调试方式编译(用 gcc 的 -g 选项)或 是被去掉了符号信息都可以被跟踪。与调试器可以连接到一个运行进程并控制它一样,strace 也 可以跟踪一个正在运行的进程。 跟踪信息通常用于生成错误报告,然后发给应用开发人员,但是它对内核编程人员来说也同样非常 有用。我们已经看到驱动程序是如何通过响应系统调用得到执行的;strace 允许我们检查每次调 用中输入和输出数据的一致性。 83 第 4 章 调试技术 例如,下面的屏幕信息显示了 strace ls /dev > /dev/scull0 命令的最后几行: [...] open("/dev", O_RDONLY|O_NONBLOCK) = 4 fcntl(4, F_SETFD, FD_CLOEXEC) =0 brk(0x8055000) = 0x8055000 lseek(4, 0, SEEK_CUR) =0 getdents(4, /* 70 entries */, 3933) = 1260 [...] getdents(4, /* 0 entries */, 3933) = 0 close(4) =0 fstat(1, {st_mode=S_IFCHR|0664, st_rdev=makedev(253, 0), ...}) = 0 ioctl(1, TCGETS, 0xbffffa5c) = -1 ENOTTY (Inappropriate ioctl for device) write(1, "MAKEDEV\natibm\naudio\naudio1\na"..., 4096) = 4000 write(1, "d2\nsdd3\nsdd4\nsdd5\nsdd6\nsdd7"..., 96) = 96 write(1, "4\nsde5\nsde6\nsde7\nsde8\nsde9\n"..., 3325) = 3325 close(1) =0 _exit(0) =? 很明显,ls 完成对目标目录的检索后,在首次对 write 的调用中,它试图写入 4KB 数据。很奇 怪(对于 ls 来说),实际只写了 4000 个字节,接着它重试这一操作。然而,我们知道 scull 的 write 实现每次最多只写一个量子(scull 中设置的量子大小为 4000 个字节),所以我们所预期的就是这 样的部分写入。经过几个步骤之后,每件工作都顺利通过,程序正常退出。 另一个例子,让我们来对 scull 设备进行读操作(使用 wc 命令): [...] open("/dev/scull0", O_RDONLY) =4 fstat(4, {st_mode=S_IFCHR|0664, st_rdev=makedev(253, 0), ...}) = 0 read(4, "MAKEDEV\natibm\naudio\naudio1\na"..., 16384) = 4000 read(4, "d2\nsdd3\nsdd4\nsdd5\nsdd6\nsdd7"..., 16384) = 3421 read(4, "", 16384) =0 fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(3, 7), ...}) = 0 ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0 write(1, " 7421 /dev/scull0\n", 20) = 20 close(4) =0 _exit(0) =? 正如所料,read 每次只能读取 4000 个字节,但数据总量与前面例子中写入的数量是相同的。与 上面的写跟踪相对比,请读者注意本例中重试工作是如何组织的。为了快速读取数据,wc 已被优 化了,因而它绕过了标准库,试图通过一次系统调用读取更多的数据。可以从跟踪的 read 行中看 到 wc 每次均试图读取 16KB 数据。 Linux 行家可以在 strace 的输出中发现很多有用信息。如果觉得这些符号过于拖累的话,则可以 仅限于监视文件方法(open,read 等)是如何工作的。 就个人观点而言,我们发现 strace 对于查找系统调用运行时的细微错误最为有用。通常应用或演 示程序中的 perror 调用在用于调试时信息还不够详细,而 strace 能够确切查明系统调用的哪个 参数引发了错误,这一点对调试是大有帮助的。 84 Linux 设备驱动程序 4.4 调试系统故障 即使采用了所有这些监视和调试技术,有时驱动程序中依然会有错误,这样的驱动程序在执行时就 会产生系统故障。在出现这种情况时,获取尽可能多的信息对解决问题是至关重要的。 注意,“故障”不意味着“panic”。Linux 代码非常健壮(用术语讲即为鲁棒,robust),可以很好 地响应大部分错误:故障通常会导致当前进程崩溃,而系统仍会继续运行。如果在进程上下文之外 发生故障,或是系统的重要组成被损害时,系统才有可能 panic。但如果问题出在驱动程序中时, 通常只会导致正在使用驱动程序的那个进程突然终止。唯一不可恢复的损失就是进程被终止时,为 进程上下文分配的一些内存可能会丢失;例如,由驱动程序通过 kmalloc 分配的动态链表可能丢 失。然而,由于内核在进程中止时会对已打开的设备调用 close 操作,驱动程序仍可以释放由 open 方法分配的资源。 我们已经说过,当内核行为异常时,会在控制台上打印出提示信息。下一节将解释如何解码并使用 这些消息。尽管它们对于初学者来说相当晦涩,不过处理器在出错时转储出的这些数据包含了许多 值得关注的信息,通常足以查明程序错误,而无需额外的测试。 4.4.1 oops 消息 大部分错误都在于 NULL 指针的使用或其他不正确的指针值的使用上。这些错误通常会导致一个 oops 消息。 由处理器使用的地址都是虚拟地址,而且通过一个复杂的称为页表(见第 13 章中的“页表”一节) 的结构映射为物理地址。当引用一个非法指针时,页面映射机制就不能将地址映射到物理地址,此 时处理器就会向操作系统发出一个“页面失效”的信号。如果地址非法,内核就无法“换页”到并 不存在的地址上;如果此时处理器处于超级用户模式,系统就会产生一个“oops”。 值得注意的是,2.0 版本之后引入的第一个增强是,当向用户空间移动数据或者移出时,无效地址 错误会被自动处理。Linus 选择了让硬件来捕捉错误的内存引用,所以正常情况(地址都正确时) 就可以更有效地得到处理。 oops 显示发生错误时处理器的状态,包括 CPU 寄存器的内容、页描述符表的位置,以及其它看 上去无法理解的信息。这些消息由失效处理函数(arch/*/kernel/traps.c)中的 printk 语句产生,就 象前面“printk”一节所介绍的那样分发出来。 让我们看看这样一个消息。当我们在一台运行 2.4 内核的 PC 机上使用一个 NULL 指针时,就 会导致下面这些信息显示出来。这里最为相关的信息就是指令指针(EIP),即出错指令的地址。 Unable to handle kernel NULL pointer dereference at virtual address \00000000 printing eip: c48370c3 *pde = 00000000 Oops: 0002 CPU: 0 EIP: 0010:[] EFLAGS: 00010286 eax: ffffffea ebx: c2281a20 esi: 4000c000 edi: 4000c000 ecx: c48370c0 ebp: c38adf8c edx: c2281a40 esp: c38adf8c 85 第 4 章 调试技术 ds: 0018 es: 0018 ss: 0018 Process ls (pid: 23171, stackpage=c38ad000) Stack: 0000010e c01356e6 c2281a20 4000c000 0000010e c2281a40 c38ac000 \ 0000010e 4000c000 bffffc1c 00000000 00000000 c38adfc4 c010b860 00000001 \ 4000c000 0000010e 0000010e 4000c000 bffffc1c 00000004 0000002b 0000002b \ 00000004 Call Trace: [] [] Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00 这个消息是通过对 faulty 模块的一个设备进行写操作而产生的,faulty 这个模块专为演示出错而 编写。faulty.c 中 write 方法的实现很简单: ssize_t faulty_write (struct file *filp, const char *buf, size_t count, loff_t *pos) { /* make a simple fault by dereferencing a NULL pointer */ *(int *)0 = 0; return 0; } 正如读者所见,我们这使用了一个 NULL 指针。因为 0 决不会是个合法的指针值,所以错误发生, 内核进入上面的 oops 消息状态。这个调用进程接着就被杀掉了。在 read 实现中,faulty 模块还 有更多有意思的错误状态。 char faulty_buf[1024]; ssize_t faulty_read (struct file *filp, char *buf, size_t count, loff_t *pos) { int ret, ret2; char stack_buf[4]; printk(KERN_DEBUG "read: buf %p, count %li\n", buf, (long)count); /* the next line oopses with 2.0, but not with 2.2 and later */ ret = copy_to_user(buf, faulty_buf, count); if (!ret) return count; /* we survived */ printk(KERN_DEBUG "didn't fail: retry\n"); /* For 2.2 and 2.4, let's try a buffer overflow */ sprintf(stack_buf, "1234567\n"); if (count > 8) count = 8; /* copy 8 bytes to the user */ ret2 = copy_to_user(buf, stack_buf, count); if (!ret2) return count; return ret2; } 这段程序首先从一个全局缓冲区读取数据,但并不检查数据的长度,然后通过对一个局部缓冲区进 行写入操作,制造一次缓冲区溢出。第一个操作仅在 2.0 内核会导致 oops 的发生,因为后期版 本能自动地处理用户拷贝函数。缓冲区溢出则会在所有版本的内核中造成 oops;然而,由于 return 指令把指令指针带到了不知道的地方,所以这种错误很难跟踪,所能获得的仅是如下的信息: EIP: 0010:[<00000000>] [...] Call Trace: [] Code: Bad EIP value. 86 Linux 设备驱动程序 用户处理 oops 消息的主要问题在于,我们很难从十六进制数值中看出什么内在的意义;为了使这 些数据对程序员更有意义,需要把它们解析为符号。有两个工具可用来为开发人员完成这样的解析: klogd 和 ksymoops。前者只要运行就会自行进行符号解码;后者则需要用户有目的地调用。下面 的讨论,使用了在我们第一个 oops 例子中通过使用 NULL 指针而产生的出错信息。 使用 klogd klogd 守护进程能在 oops 消息到达记录文件之前对它们解码。很多情况下,klogd 可以为开发者 提供所有必要的信息用于捕捉问题的所在,可是有时开发者必须给它一定的帮助。 当 faulty 的一个 oops 输出送达系统日志时,转储信息看上去会是下面的情况(注意 EIP 行和 stack 跟踪记录中已经解码的符号): Unable to handle kernel NULL pointer dereference at virtual address \ 00000000 printing eip: c48370c3 *pde = 00000000 Oops: 0002 CPU: 0 EIP: 0010:[faulty:faulty_write+3/576] EFLAGS: 00010286 eax: ffffffea ebx: c2c55ae0 ecx: c48370c0 edx: c2c55b00 esi: 0804d038 edi: 0804d038 ebp: c2337f8c esp: c2337f8c ds: 0018 es: 0018 ss: 0018 Process cat (pid: 23413, stackpage=c2337000) Stack: 00000001 c01356e6 c2c55ae0 0804d038 00000001 c2c55b00 c2336000 \ 00000001 0804d038 bffffbd4 00000000 00000000 bffffbd4 c010b860 00000001 \ 0804d038 00000001 00000001 0804d038 bffffbd4 00000004 0000002b 0000002b \ 00000004 Call Trace: [sys_write+214/256] [system_call+52/56] Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00 klogd 提供了大多数必要信息用于发现问题。在这个例子中,我们看到指令指针(EIP)正执行于 函数 faulty_write 中,因此我们就知道该从哪儿开始检查。字串 3/576 告诉我们处理器正处于函 数的第 3 个字节上,而函数整体长度为 576 个字节。注意这些数值都是十进制的,而非十六进制。 然而,当错误发生在可装载模块中时,为了获取错误相关的有用信息,开发者还必须注意一些情况。 klogd 在开始运行时装入所有可用符号,并随后使用这些符号。如果在 klogd 已经对自身初始化 之后(一般在系统启动时),装载某个模块,那 klogd 将不会有这个模块的符号信息。强制 klogd 取得这些信息的办法是,发送一个 SIGUSR1 信号给 klogd 进程,这种操作在时间顺序上,必须 是在模块已经装入(或重新装载)之后,而在进行任何可能引起 oops 的处理之前。 还可以在运行 klogd 时加上 -p 选项,这会使它在任何发现 oops 消息的时刻重新读入符号信息。 不过,klogd 的 man 手册不推荐这个方法,因为这使 klogd 在出问题之后再向内核查询信息。而 发生错误之后,所获得的信息可能是完全错误的了。 为了使 klogd 正确地工作,必须给它提供符号表文件 System.map 的一个当前复本。通常这个文 件在 /boot 中;如果从一个非标准的位置编译并安装了一个内核,就需要把 System.map 拷贝到 /boot,或告知 klogd 到什么位置查看。如果符号表与当前内核不匹配,klogd 就会拒绝解析符号。 87 第 4 章 调试技术 假如一个符号被解析在系统日志中,那么就有理由确信它已被正确解析了。 使用 ksymoops 有些时候,klogd 对于跟踪目的而言仍显不足。开发者经常既需要取得十六进制地址,又要获得对 应的符号,而且偏移量也常需要以十六进制的形式打印出来。除了地址解码之外,往往还需要更多 的信息。对 klogd 来说,在出错期间被杀掉,也是常用的事情。在这些情况下,可以调用一个更 为强大的 oops 分析器,ksymoops 就是这样的一个工具。 在 2.3 开发系列之前,ksymoops 是随内核源码一起发布的,位于 scripts 目录之下。它现在则 在自己的 FTP 站点上,对它的维护是与内核相独立的。即使读者所用的仍是较早期的内核,或许 还可以从 ftp://ftp.ocs.com.au/pub/ksymoops 站点上获取这个工具的升级版本。 为了取得最佳的工作状态,除错误消息之外,ksymoops 还需要很多信息;可以使用命令行选项告 诉它在什么地方能找到这些各个方面的内容。ksymoops 需要下列内容项: System.map 文件 模块列表 在 oops 发生时已定义好的 内核符号表 当前正运行的内核映像的复 本 已装载的任何内核模块的目 标文件位置 这个映射文件必须与 oops 发生时正在运行的内 核相一致。默认为 /usr/src/linux/System.map。 ksymoops 需要知道 oops 发生时都装入了哪些模 块,以便获得它们的符号信息。如果未提供这个列 表,ksymoops 会查看 /proc/modules。 默认从 /proc/ksyms 中取得该符号表。 注意,ksymoops 需要的是一个直接的内核映像, 而不是象 vmlinuz、zImage 或 bzImage 这样被大 多数系统所使用的压缩版本。默认是不使用内核映 像,因为大多数人都不会保存这样的一个内核。如 果手边就有这样一个符合要求的内核的话,就应该 采用 -v 选项告知 ksymoops 它的位置。 ksymoops 将在标准目录路径寻找这些模块,不过 在开发中,几乎总要采用 -o 选项告知 ksymoops 这些模块的存放位置。 虽然 ksymoops 会访问 /proc 中的文件来取得它所需的信息,但这样获得的结果是不可靠的。在 oops 发生和 ksymoops 运行的时间间隙中,系统几乎一定会重新启动,这样取自 /proc 的信息 就可能与故障发生时的实际状态不符合。只要有可能,最好在引起 oops 发生之前,保存 /proc/modules 和 /proc/ksyms 的复本。 我们强烈建议驱动程序开发人员阅读 ksymoops 的手册页,这是一个很好的资料文档。 这个工具命令行中的最后一个参数是 oops 消息的位置;如果缺少这个参数,ksymoops 会按 Unix 的惯例去读取标准输入设备。运气好的话,消息可以从系统日志中重新恢复;在发生很严重的崩溃 情况时,我们可能不得不将这些消息从屏幕上抄下来,然后再敲进去(除非用的是串口控制台,这 对内核开发人员来说,是非常棒的工具)。 注意,当 oops 消息已经被 klogd 处理过时,ksymoops 将会陷于混乱。如果 klogd 已经运行, 而且 oops 发生后系统仍在运行,那么经常可以通过调用 dmesg 命令来获得一个干净的 oops 88 Linux 设备驱动程序 消息。 如果没有明确地提供全部的上述信息,ksymoops 会发出警告。对于载入模块未作符号定义这类的 情况,它同样会发出警告。一个不作任何警告的 ksymoops 是很少见的。 ksymoops 的输出类似如下: >>EIP; c48370c3 <[faulty]faulty_write+3/20> <===== Trace; c01356e6 Trace; c010b860 Code; c48370c3 <[faulty]faulty_write+3/20> 00000000 <_EIP>: Code; c48370c3 <[faulty]faulty_write+3/20> <===== 0: c7 05 00 00 00 movl $0x0,0x0 <===== Code; c48370c8 <[faulty]faulty_write+8/20> 5: 00 00 00 00 00 Code; c48370cd <[faulty]faulty_write+d/20> a: 31 c0 xorl %eax,%eax Code; c48370cf <[faulty]faulty_write+f/20> c: 89 ec movl %ebp,%esp Code; c48370d1 <[faulty]faulty_write+11/20> e: 5d popl %ebp Code; c48370d2 <[faulty]faulty_write+12/20> f: c3 ret Code; c48370d3 <[faulty]faulty_write+13/20> 10: 8d b6 00 00 00 leal 0x0(%esi),%esi Code; c48370d8 <[faulty]faulty_write+18/20> 15: 00 正如上面所看到的,ksymoops 提供的 EIP 和内核堆栈信息与 klogd 所做的很相似,不过要更为 准确,而且是十六进制形式的。可以注意到,faulty_write 函数的长度被正确地报告为 0x20 个字 节。这是因为 ksymoops 读取了模块的目标文件,并从中获得了全部的有用信息。 而且在这个例子中,还可以得到错误发生处代码的汇编语言形式的转储输出。这些信息常被用于确 切地判断发生了些什么事情;这里很明显,错误在于一个向 0 地址写入数据 0 的指令。 ksymoops 的一个有趣特点是,它可以移植到几乎所有 Linux 可以运行的平台上,而且还利用了 bfd (二进制格式描述)库同时支持多种计算机结构。走出 PC 的世界,我们可以看到 SPARC64 平台上显示的 oops 消息是何等的相似(为了便于排版有几行被打断了): Unable to handle kernel NULL pointer dereference tsk->mm->context = 0000000000000734 tsk->mm->pgd = fffff80003499000 \/ ____ \/ "@'/ .. \`@" /_| \_ _/ |_\ \_ _U_/ ls(16740): Oops TSTATE: 0000004400009601 TPC: 0000000001000128 TNPC: 0000000000457fbc \ Y: 00800000 g0: 000000007002ea88 g1: 0000000000000004 g2: 0000000070029fb0 \ g3: 0000000000000018 g4: fffff80000000000 g5: 0000000000000001 g6: fffff8000119c000 \ g7: 0000000000000001 o0: 0000000000000000 o1: 000000007001a000 o2: 0000000000000178 \ o3: fffff8001224f168 o4: 0000000001000120 o5: 0000000000000000 sp: fffff8000119f621 \ ret_pc: 0000000000457fb4 89 第 4 章 调试技术 l0: fffff800122376c0 l1: ffffffffffffffea l2: 000000000002c400 \ l3: 000000000002c400 l4: 0000000000000000 l5: 0000000000000000 l6: 0000000000019c00 \ l7: 0000000070028cbc i0: fffff8001224f140 i1: 000000007001a000 i2: 0000000000000178 \ i3: 000000000002c400 i4: 000000000002c400 i5: 000000000002c000 i6: fffff8000119f6e1 \ i7: 0000000000410114 Caller[0000000000410114] Caller[000000007007cba4] Instruction DUMP: 01000000 90102000 81c3e008 \ 30680005 01000000 01000000 01000000 01000000 请注意,指令转储并不是从引起错误的那个指令开始,而是之前的三条指令:这是因为 RISC 平 台以并行的方式执行多条指令,这样可能产生延期的异常,因此必须能回溯最后的几条指令。 下面是当从 TSTATE 行开始输入数据时,ksymoops 所打印出的信息: >>TPC; 0000000001000128 <[faulty].text.start+88/a0> <===== >>O7; 0000000000457fb4 >>I7; 0000000000410114 Trace; 0000000000410114 Trace; 000000007007cba4 Code; 000000000100011c <[faulty].text.start+7c/a0> 0000000000000000 <_TPC>: Code; 000000000100011c <[faulty].text.start+7c/a0> 0: 01 00 00 00 nop Code; 0000000001000120 <[faulty].text.start+80/a0> 4: 90 10 20 00 clr %o0 ! 0 <_TPC> Code; 0000000001000124 <[faulty].text.start+84/a0> 8: 81 c3 e0 08 retl Code; 0000000001000128 <[faulty].text.start+88/a0> <===== c: c0 20 20 00 clr [ %g0 ] <===== Code; 000000000100012c <[faulty].text.start+8c/a0> 10: 30 68 00 05 b,a %xcc, 24 <_TPC+0x24> \ 0000000001000140 <[faulty]faulty_write+0/20> Code; 0000000001000130 <[faulty].text.start+90/a0> 14: 01 00 00 00 nop Code; 0000000001000134 <[faulty].text.start+94/a0> 18: 01 00 00 00 nop Code; 0000000001000138 <[faulty].text.start+98/a0> 1c: 01 00 00 00 nop Code; 000000000100013c <[faulty].text.start+9c/a0> 20: 01 00 00 00 nop 要打印出上面显示的反汇编代码,我们就必须告知 ksymoops 目标文件的格式和结构(之所以需要 这些信息,是因为 SPARC64 用户空间的本地结构是 32 位的)。本例中,使用选项 -t elf64-sparc -a sparc:v9 可进行这样的设置。 读者可能会抱怨对调用的跟踪并没带回什么值得注意的信息;然而,SPARC 处理器并不会把所有 的调用跟踪记录保存到堆栈中:07 和 I7 寄存器保存了最后调用的两个函数的指令指针,这就是 它们出现在调用跟踪记录边上的原因。在这个例子中,我们可以看到,故障指令位于一个由 sys_write 调用的函数中。 要注意的是,无论平台/结构是怎样的一种配合情况,用来显示反汇编代码的格式与 objdump 程序 所使用的格式是一样的。objdump 是个很强大的工具;如果想查看发生故障的完整函数,可以调 用命令: objdump -d faulty.o(再次重申,对于 SPARC64 平台,需要使用特殊选项:--target 90 Linux 设备驱动程序 elf64-sparc-architecture sparc:v9)。 关于 objdump 和它的命令行选项的更多信息,可以参阅这个命令的手册页帮助。 学习对 oops 消息进行解码,需要一定的实践经验,并且了解所使用的目标处理器,以及汇编语言 的表达习惯等。这样的准备是值得的,因为花费在学习上的时间很快会得到回报。即使之前读者已 经具备了非 Unix 操作系统中 PC 汇编语言的专门知识,仍有必要花些时间对此进行学习,因为 Unix 的语法与 Intel 的语法并不一样。(在 as 命令 infor 页的“i386-specific”一章中,对这种 差异进行了很好的描述。) 4.4.2 系统挂起 尽管内核代码中的大多数错误仅会导致一个 oops 消息,但有时它们则会将系统完全挂起。如果系 统挂起了,任何消息都无法打印。例如,如果代码进入一个死循环,内核就会停止进行调度,系统 不会再响应任何动作,包括 Ctrl-Alt-Del 组合键。处理系统挂起有两个选择――要么是防范于未然; 要么就是亡羊补牢,在发生挂起后调试代码。 通过在一些关键点上插入 schedule 调用可以防止死循环。schedule 函数(正如读者猜到的)会 调用调度器,并因此允许其他进程“偷取”当然进程的 CPU 时间。如果该进程因驱动程序的错误 而在内核空间陷入死循环,则可以在跟踪到这种情况之后,借助 schedule 调用杀掉这个进程。 当然,应该意识到任何对 schedule 的调用都可能给驱动程序带来代码重入的问题,因为 schedule 允许其他进程开始运行。假设驱动程序进行了合适的锁定,这种重入通常还并不致于带来问题。不 过,一定不要在驱动程序持有 spinlock 的任何时候调用 schedule。 如果驱动程序确实会挂起系统,而又不知该在什么位置插入 schedule 调用时,最好的方法是加入 一些打印信息,并把它们写入控制台(通过修改 console_loglevel 的数值)。 有时系统看起来象挂起了,但其实并没有。例如,如果键盘因某种奇怪的原因被锁住了就会发生这 种情况。运行专为探明此种情况而设计的程序,通过查看它的输出情况,可以发现这种假挂起。显 示器上的时钟或系统负荷表就是很好的状态监视器;只要它保持更新,就说明 scheduler 正在工 作。如果没有使用图形显示,则可以运行一个程序让键盘 LED 闪烁,或不时地开关软驱马达,或 不断触动扬声器(通常蜂鸣声是令人烦恼的,应尽量避免;可改为寻求 ioctl 命令 KDMKTONE ), 来 检 查 scheduler 是 否 工 作 正 常 。 O ’ Reilly FTP 站 点 上 可 以 找 到 一 个 例 子 (misc-progs/heartbeat.c),它会使键盘 LED 不断闪烁。 如果键盘不接收输入,最佳的处理方法是从网络登录到系统中,杀掉任何违例的进程,或是重新设 置键盘(用 kdb_mode -a)。然而,如果没有可用的网络用来帮助恢复的话,即使发现了系统挂起 是由键盘死锁造成的也没有用了。如果是这样的情况,就应该配置一种可替代的输入设备,以便至 少可以正常地重启系统。比起去按所谓的“大红钮”,在你的计算机上,通过替代的输入设备来关 机或重启系统要更为容易些,而且它可以免去 fsck 对磁盘的长时间扫描。 例如,这种替代输入设备可以是鼠标。1.10 或更新版本的 gpm 鼠标服务器可以通过命令行选项支 持类似的功能,不过仅限于文本模式。如果没有网络连接,并且以图形方式运行,则建议采用某些 自定义的解决方案,比如,设置一个与串口线 DCD 针脚相连的开关,并编写一个查询 DCD 信 91 第 4 章 调试技术 号状态变化的脚本,用于从外界干预键盘已被死锁的系统。 对于上述情形,一个不可缺少的工具是“magic SysRq key”,2.2 和后期版本内核中,在其它体系 结构上也可利用得到它。SysRq 魔法键是通过 PC 键盘上的 ALT 和 SysRq 组合键来激活的,在 SPARC 键盘上则是 ALT 和 Stop 组合键。连同这两个键一起按下的第三个键,会执行许多有用 动作中的其中一种,这些动作如下: r 在无法运行 kbd_mode 的情况中,关闭键盘的 raw 模式。 k 激活“留意安全键”(SAK)功能。SAK 将杀掉当前控制台上运行的所有进程,留下一个干净的终 端。 s 对所有磁盘进行紧急同步。 u 尝试以只读模式重新挂装所有磁盘。这个操作通常紧接着 s 动作之后被调用,它可以在系统处于 严重故障状态时节省很多检查文件系统的时间。 b 立即重启系统。注意先要同步并重新挂装磁盘。 p 打印当前的寄存器信息。 t 打印当前的任务列表。 m 打印内存信息。 还有其它的一些 SysRq 功能;要获得完整列表,可参阅内核源码 Documentation 目录下的 sysrq.txt 文件。注意,SysRq 功能必须明确地在内核配置中被开启,出于安全原因,大多数发行 系统并未开启它。不过,对于一个用于驱动程序开发的系统来说,为开启 SysRq 功能而带来的重 新编译新内核的麻烦是值得的。SysRq 必须在运行时通过下面的命令启动: echo 1 > /proc/sys/kernel/sysrq 在复现系统的挂起故障时,另一个要采取的预防措施是,把所有的磁盘都以只读的方式挂装在系统 上(或干脆就卸装它们)。如果磁盘是只读的或者并未挂装,就不会发生破坏文件系统或致使文件 系统处于不一致状态的危险。另一个可行方法是,使用通过 NFS (网络文件系统)将其所有文件系 统挂装入系统的计算机。这个方法要求内核具有“NFS-Root”的能力,而且在引导时还需传入一 些特定参数。如果采用这种方法,即使我们不借助于 SysRq,也能避免任何文件系统的崩溃,因 为 NFS 服务器管理文件系统的一致性,而它并不受驱动程序的影响。 92 Linux 设备驱动程序 4.5 调试器和相关工具 最后一种调试模块的方法就是使用调试器来一步步地跟踪代码,查看变量和计算机寄存器的值。这 种方法非常耗时,应该尽量避免。不过,某些情况下通过调试器对代码进行细粒度的分析是很有价 值的。 在内核中使用交互式调试器是一个很复杂的问题。出于对系统所有进程的整体利益考虑,内核在它 自己的地址空间中运行。其结果是,许多用户空间下的调试器所提供的常用功能很难用于内核之中, 比如断点和单步调试等。本节着眼于调试内核的几种方法;它们中的每一种都各有利弊。 4.5.1 使用 gdb gdb 在探究系统内部行为时非常有用。在我们这个层次上,熟练使用调试器,需要掌握 gdb 命令、 了解目标平台的汇编代码,还要具备对源代码和优化后的汇编码进行匹配的能力。 启动调试器时必须把内核看作是一个应用程序。除了指定未压缩的内核映像文件名外,还应该在命 令行中提供“core 文件”的名称。对于正运行的内核,所谓 core 文件就是这个内核在内存中的 核心映像,/proc/kcore。典型的 gdb 调用如下所示: gdb /usr/src/linux/vmlinux /proc/kcore 第一个参数是未经压缩的内核可执行文件的名字,而不是 zImage 或 bzImage 以及其他任何压缩 过的内核。 gdb 命令行的第二个参数是是 core 文件的名字。与其它 /proc 中的文件类似,/proc/kcore 也是在 被读取时产生的。当在 /proc 文件系统中执行 read 系统调用时,它会映射到一个用于数据生成而 不是数据读取的函数上;我们已在“使用 /proc 文件系统”一节中介绍了这个特性。kcore 用来按 照 core 文件的格式表示内核“可执行文件”;由于它要表示对应于所有物理内存的整个内核地址 空间,所以是一个非常巨大的文件。在 gdb 的使用中,可以通过标准 gdb 命令查看内核变量。例 如,p jiffies 可以打印从系统启动到当前时刻的时钟滴答数。 从 gdb 打印数据时,内核仍在运行,不同数据项的值会在不同时刻有所变化;然而,gdb 为了优 化对 core 文件的访问,会将已经读到的数据缓存起来。如果再次查看 jiffies 变量,仍会得到和上 次一样的值。对通常的 core 文件来说,对变量值进行缓存是正确的,这样可避免额外的磁盘访问。 但对“动态的”core 文件来说就不方便了。解决方法是在需要刷新 gdb 缓冲区的时候,执行命令 core-file /proc/kcore;调试器将使用新的 core 文件并丢弃所有的旧信息。不过,读新数据时并不 总是需要执行 core-file 命令;gdb 以几 KB 大小的小数据块形式读取 core 文件,缓存的仅是已 经引用的若干小块。 对内核进行调试时,gdb 通常能提供的许多功能都不可用。例如,gdb 不能修改内核数据;因为 在处理其内存映像之前,gdb 期望把待调试程序运行在自己的控制之下。同样,也不能设置断点 或观察点,或者单步跟踪内核函数。 如果用调试选项(-g)编译了内核,产生的 vmlinux 会比没有使用 -g 选项的更适合于 gdb。不过 要注意,用 -g 选项编译内核需要大量的磁盘空间(每个目标文件和内核自身都会比通常的大三倍 93 第 4 章 调试技术 甚至更多)。 在非 PC 类计算机上,情况则不尽相同。在 Alpha 上,make boot 会在生成可启动映像前将调试 信息去掉,所以最终会获得 vmlinux 和 vmlinux.gz 两个文件。gdb 可以使用前者,后者用来启 动。在 SPARC 上,默认情况则是不把内核(至少是 2.0 内核)调试信息去掉。 当用 -g 选项编译内核并且和 /proc/kcore 一起使用 vmlinux 运行调试器时,gdb 可以返回很多内 核内部信息。例如,可以使用下面的命令来转储结构数据,如 p *module_list、p *module_list->next 和 p *chrdevs[4]->fops 等。为了在使用 p 命令时取得最好效果,有必要保留一份内核映射表和 随手可及的源码。 利用 gdb 可在当前内核上执行的另一个有用任务是,通过 disassemble 命令(可缩写为 disass ) 或是“检查指令”(x/i)命令对函数进行反汇编。disassemble 命令的参数可以是函数名或是内存 范围;而 x/i 则使用一个内存地址做为参数,也可以是符号名称的形式。例如,可以用 x/20i 反汇 编 20 条指令。注意,不能反汇编一个模块的函数,因为调试器作用的是 vmlinux,它并不知道模 块的情况。如果试图通过地址反汇编模块代码,gdb 很有可能会返回“Cannot access memory at xxxx(不能访问 xxxx 处的内存)”这样的信息。基于同样的原因,也不能查看属于模块的数据项。 如果已知道变量的地址,可以从 /dev/mem 中读出它们的值,但要弄明白从系统内存中分解出的 原始数据的含义,难度是相当大的。 如果需要反汇编模块函数,最好对模块的目标文件用 objdump 工具进行处理。很不幸,该工具只 能对磁盘上的文件复本进行处理,而不能对运行中的模块进行处理;因此,由 objdump 给出的地 址都是未经重定位的地址,与模块的运行环境无关。对未经链接的目标文件进行反汇编的另一个不 利因素在于,其中的函数调用仍是未作解析的,所以就无法轻松地区分是对 printk 的调用呢,还 是对 kmalloc 的调用。 正如上面看到的,当目的在于查看内核的运行情况时,gdb 是一个有用的工具,但对于设备驱动程 序的调试,它还缺少一些至关重要的功能。 4.5.2 kdb 内核调试器 很多读者可能会奇怪这一点,即为什么不把一些更高级的调试功能直接编译进内核呢。答案很简单, 因为 Linus 不信任交互式的调试器。他担心这些调试器会导致一些不良的修改,也就是说,修补 的仅是一些表面现象,而没有发现问题的真正原因所在。因此,没有在内核中内建调试器。 然而,其他的内核开发人员偶尔也会用到一些交互式的调试工具。kdb 就是其中一种内建的内核调 试器,它在 oss.sgi.com 上以非正式的补丁形式提供。要使用 kdb,必须首先获得这个补丁(取 得的版本一定要和内核版本相匹配),然后对当前内核源码进行 patch 操作,再重新编译并安装这 个内核。注意,kdb 仅可用于 IA-32(x86) 系统(虽然用于 IA-64 的一个版本在主流内核源码中 短暂地出现过,但很快就被删去了)。 一旦运行的是支持 kdb 的内核,有几个方法可以进入 kdb 的调试状态。在控制台上按下 Pause (或 Break)键将启动调试。当内核发生 oops,或到达某个断点时,也会启动 kdb。无论是哪一 种情况,都看到下面这样的消息: 94 Linux 设备驱动程序 Entering kdb (0xc1278000) on processor 1 due to Keyboard Entry [1]kdb> 注意,当 kdb 运行时,内核所做的每一件事情都会停下来。当激活 kdb 调试时,系统不应运行 其他的任何东西;尤其是,不要开启网络――当然,除非是在调试网络驱动程序。一般来说,如果 要使用 kdb 的话,最好在启动时进入单用户模式。 作为一个例子,考虑下面这个快速的 scull 调试过程。假定驱动程序已被载入,可以象下面这样指 示 kdb 在 scull_read 函数中设置一个断点: [1]kdb> bp scull_read Instruction(i) BP #0 at 0xc8833514 (scull_read) [1]kdb> go is enabled on cpu 1 bp 命令指示 kdb 在内核下一次进入 scull_read 时停止运行。随后我们输入 go 继续执行。在把 一些东西放入 scull 的某个设备之后,我们可以在另一个终端的 shell 中运行 cat 命令尝试读取 这个设备,这样一来就会产生如下的状态: Entering kdb (0xc3108000) on processor 0 due to Breakpoint @ 0xc8833515 Instruction(i) breakpoint #0 at 0xc8833514 scull_read+0x1: movl %esp,%ebp [0]kdb> 我们现在正处于 scull_read 的开头位置。为了查明是怎样到达这个位置的,我们可以看看堆栈跟 踪记录: [0]kdb> bt EBP EIP 0xc3109c5c 0xc8833515 0xc3109fbc 0xfc458b10 0x1000, 0x804ad78) 0xbffffc88 0xc010bec0 [0]kdb> Function(args) scull_read+0x1 scull_read+0x33c255fc( 0x3, 0x803ad78, 0x1000, system_call kdb 试图打印出调用跟踪所记录的每个函数的参数列表。然而,它往往会被编译器所使用的优化技 巧弄糊涂。所以在这个例子中,虽然 scull_read 实际只有四个参数,kdb 却打印出了五个。 下面我们来看看如何查询数据。mds 命令是用来对数据进行处理的;我们可以用下面的命令查询 scull_devices 指针的值: [0]kdb> mds scull_devices 1 c8836104: c4c125c0 .... 在这里,我们请求查看的是从 scull_devices 指针位置开始的一个字大小(4 个字节)的数据;应 答告诉我们设备数据数组的起始地址位于 c4c125c0。要查看设备结构自身的数据值,我们需要用 到这个地址: [0]kdb> mds c4c125c0 c4c125c0: c3785000 .... c4c125c4: 00000000 .... c4c125c8: 00000fa0 .... c4c125cc: 000003e8 .... c4c125d0: 0000009a .... 95 第 4 章 调试技术 c4c125d4: 00000000 .... c4c125d8: 00000000 .... c4c125dc: 00000001 .... 上面的 8 行分别对应于 Scull_Dev 结构中的 8 个成员。因此,通过显示的这些数据,我们可以知 道,第一个设备的内存是从 0xc3785000 开始分配的,链表中没有下一个数据项,量子大小为 4000 (十六进制形式为 fa0)字节,量子集大小为 1000(十六进制形式为 3e8),这个设备中有 154 个 字节(十六进制形式为 9a)的数据,等等。 kdb 还可以修改数据。假设我们要从设备中削减一些数据: [0]kdb> mm c4c125d0 0x50 0xc4c125d0 = 0x50 接下来对设备的 cat 操作所返回的数据就会少于上次。 kdb 还有许多其他的功能,包括单步调试(根据指令,而不是 C 源代码行),在数据访问中设置断 点,反汇编代码,跟踪链表,访问寄存器数据等等。加上 kdb 补丁之后,在内核源码树的 Documentation/kdb 目录可以找到完整的手册页。 4.5.3 集成的内核调试器补丁 有很多内核开发人员为一个名为“集成的内核调试器”的非正式补丁作出过贡献,我们可将其简称 为 IKD(integrated kernel debugger)。IKD 提供了很多值得关注的内核调试工具。x86 是这个补 丁 的 主 要 平 台 , 不 过 它 也 可 以 用 于 其 它 的 结 构 体 系 之 上 。 IKD 补 丁 可 以 从 ftp://ftp.kernel.org/pub/linux/kernel/people/andrea/ikd 下 载 。 它 是 一 个 必 须 应 用 于 内 核 源 码 的 patch 补丁;因为这个 patch 是与版本相关的,所以要确保下载的补丁与正使用的内核版本相一 致。 IKD 补丁的功能之一是内核堆栈调试。如果开启这个功能,内核就会在每个函数调用时检查内核 堆栈的空闲空间的大小,如果过小的话就会强制产生一个 oops。如果内核中的某些事情引起堆栈 崩溃,这个工具就能用来帮助查找问题。这其实也就是一种“堆栈计量表”的功能,可以在任何特 定的时刻查看堆栈的填充程度。 IKD 补丁还包含了一些用于发现内核死锁的工具。如果某个内核过程持续时间过久而没有得到调 度的话,“软件死锁”探测器就会强制产生一个 oops。这是简单地通过对函数调用进行计数来实现 的,如果计数值超过了一个预定义的阈值,探测器就会动作,并中止一些工作。IKD 的另一个功 能是可以连续地把程序计数器打印到虚拟控制台上,这可以作为跟踪死锁的最后手段。“信号量死 锁”探测器则是在某个进程的 down 调用持续时间过久时强制产生 oops。 IKD 中的其它调试功能包括内核的跟踪功能,它可以记录内核代码的执行路径。还有一些内存调 试工具,包括一个内存泄漏探测器和一些称为“poisoner”的工具,它们在跟踪内存崩溃问题时非 常有用。 最后,IKD 也包含前一节讨论过的 kdb 调试器。不过,IKD 补丁中的 kdb 版本有些老。如果需 要 kdb 的话,我们推荐直接从 oss.sgi.com 获取当前的版本。 96 Linux 设备驱动程序 4.5.4 kgdb 补丁 kgdb 是一个在 Linux 内核上提供完整的 gdb 调试器功能的补丁,不过仅限于 x86 系统。它通过 串口连线以钩子的形式挂入目标调试系统进行工作,而在远端运行 gdb。使用 kgdb 时需要两个 系统――一个用于运行调试器,另一个用于运行待调试的内核。和 kdb 一样,kgdb 目前可从 oss.sgi.com 获得。 设置 kgdb 包括安装内核补丁并引导打过补丁之后的内核两个步骤。两个系统之间需要通过串口电 缆(或空调制解调器电缆)进行连接,在 gdb 这一侧,需要安装一些支持文件。kgdb 补丁把详 细的用法说明放在了文件 Documentation/i386/gdb-serial.txt 中;我们在这里就不再赘述。建议读 者阅读关于“调试模块”的说明:接近末尾的地方,有一些出于这个目的而编写的很好的 gdb 宏。 4.5.5 内核崩溃转储分析器 崩溃转储分析器使系统能把发生 oops 时的系统状态记录下来,以便在随后空闲的时候查看这些信 息。如果是对于一个异地用户的驱动程序进行支持,这些工具就会特别有用。用户可能不太愿意把 oops 复制下来,因此安装崩溃转储系统可以使技术支持人员不必依赖于用户的工作,也能获得用 于跟踪用户问题的必要信息。也正是出于这样的原因,可供利用的崩溃转储分析器都是由那些对用 户系统进行商业支持的公司开发的,这也就不足为奇了。 目前有两个崩溃转储分析器的补丁可以用于 Linux。在编写本节的时候,这两个工具都比较新,而 且都处在不断的变化之中。与其提供可能已经过时的详细信息,我们倒不如只是给出一个概观,并 指点读者在哪里可以找到更多的信息。 第一个分析器是 LKCD(Linux Kernel Crash Dumps,“Linux 内核崩溃转储”)。这个工具仍可以从 oss.sgi.com 上获得。当内核发生 oops 时,LKCD 会把当前系统状态(主要指内存)写入事先指 定好的转储设备中。这个转储设备必须是一个系统交换区。下次重启中(在存储交换功能开启之前) 系统会运行一个称为 LCRASH 的工具,来生成崩溃的概要记录,并可选择地把转储的复本保存在 一个普通文件中。LCRASH 可以交互方式地运行,提供了很多调试器风格的命令,用以查询系统 状态。 LKCD 目前只支持 Intel 32 位体系结构,并只能用在 SCSI 磁盘的交换分区上。 另一个崩溃转储设施可以从 www.missioncriticallinux.com 获得。这个崩溃转储子系统直接在目录 /var/dumps 中创建崩溃转储文件,而且并不使用交换区。这样就使某些事情变得更为容易,但也 意味着在知道问题已经出现在哪里的时候,文件系统已被系统修改。生成的崩溃转储的格式是标准 的 core 文件格式,所以可以利用 gdb 这类工具进行事后的分析。这个工具包也提供了另外的分 析器,可以从崩溃转储文件中解析出比 gdb 更丰富的信息。 4.5.6 用户模式的 Linux 虚拟机 用户模式 Linux 是一个很有意思的概念。它作为一个独立的可移植的 Linux 内核而构建,包含在 子目录 arch/um 中。然而,它并不是运行在某种新的硬件上,而是运行在基于 Linux 系统调用接 口所实现的虚拟机之上。因此,用户模式 Linux 可以使 Linux 内核成为一个运行在 Linux 系统 97 第 4 章 调试技术 之上单独的、用户模式的进程。 把一个内核的复本当作用户模式下的进程来运行可以带来很多好处。因为它运行在一个受约束的虚 拟处理器之上,所以有错误的内核不会破坏“真正的”系统。对软/硬件的不同配置可以在相同的 框架中轻易地进行尝试。并且,对于内核开发人员来说最值得注目的特点在于,可以很容易地利用 gdb 或其它调试器对用户模式 Linux 进行处理。归根结底,它只是一个进程。很明显,用户模式 Linux 有潜力加快内核的开发过程。 迄今为止,用户模式 Linux 虚拟机还未在主流内核中发布;要下载它,必须访问它的 web 站点 (http://user-mode-linux.sourceforge.net)。需要提醒的是,它仅可以集成到 2.4.0 之后的早期 2.4 内核版本中;当然等到本书出版的时候,版本支持方面可能会做得更好。 目前,用户模式 Linux 虚拟机也存在一些重大的限制,不过大部分可能很快就会得到解决。虚拟 处理器当前只能工作于单处理器模式;虽然虚拟机可以毫无问题地运行在 SMP 系统上,但它仍是 把主机模拟成单 CPU 模式。不过,对于驱动编写者来说,最大的麻烦在于,用户模式内核不能访 问主机系统上的硬件设备。因此,尽管用户模式 Linux 虚拟机对于本书中的大多数样例驱动程序的 调试非常有用,却无法用于调试那些处理实际硬件的驱动程序。最后一点,用户模式 Linux 虚拟机 仅能运行在 IA-32 体系结构之上。 因为对所有这些问题的修补工作正在进行之中,所以在不远的将来,对于 Linux 设备驱动程序的 开发人员,用户模式 Linux 虚拟机可能会成为一个不可或缺的工具。 4.5.7 Linux 跟踪工具包 Linux 跟踪工具包(LTT)是一个内核补丁,包含了一组可以用于内核事件跟踪的相关工具集。跟 踪内容包括时间信息,而且还能合理地建立在一段指定时间内所发生事件的完整图形化描述。因此, LTT 不仅能用于调试,还能用来捕捉性能方面的问题。 在 Web 站点 www.opersys.com/LTT 上,可以找到 LTT 以及大量的资料。 4.5.8 Dynamic Probes Dynamic Probes (或 DProbes )是 IBM 为基于 IA-32 结构的 Linux 发布的一种调试工具(遵 循 GPL 协议)。它可以在系统的几乎任何一个地方放置一个“探针”,既可以是用户空间也可以是 内核空间。这个探针由一些当控制到达指定地点即开始执行的代码(用一种特别设计的,面向堆栈 的语言编写)组成。这种代码能把信息传送回用户空间,修改寄存器,或者完成许多其它的工作。 DProbes 很有用的特点是,一旦内核编译进了这个功能,探针就可以插到一个运行系统的任一个 位置,而无需重建内核或重新启动。DProbes 也可以协同 LTT 工具在任意位置插入新的跟踪事件。 DProbes 工具可以从 IBM 的开放源码站点,即 oss.software.ibm.com 上下载。 98 第 5 章 增强的字符驱动程序操作 Linux 设备驱动程序 在第 3 章,我们已经构建了一个结构完整的可读写设备驱动程序,但一个实际可用的设备通常会 提供比同步 read 和 write 更多的功能。我们现在已经有了调试工具,即使出现了什么问题,我们 也可以继续实验下去,实现新的操作。 通常,除了读写设备之外,设备驱动程序还需要提供各种各样的硬件控制能力。这些控制操作一般 是通过 ioctl 方法来支持的,另一种方法是检查写入设备中的数据流,使用特殊序列做为控制命令。 后面这种方法应该尽量避免,因为它需要保留一些字符用于控制,在数据中就不能包含这些字符了, 另外,这种方法使用起来也比 ioctl 复杂。不过尽管如此,作为一种设备控制方法,有时它还是有 用的,如 tty 和其它一些设备就在使用这种方法。稍后我们会在本章的“非 ioctl 的设备控制”一 节中介绍这项技术。 正如我们在前一章中所阐述的,ioctl 系统调用为设备驱动程序执行“命令”提供了一个设备特有 的入口点。与 read 等方法不同,ioctl 是设备特有的,它允许应用程序访问被驱动硬件的特殊功 能,如配置设备、进入或退出某种操作模式等。这些控制操作通常无法通过 read/write 文件操作 完成。例如,向串口写入的所有东西都作为数据发送,因此无法通过写设备的方法来改变波特率。 这就是 ioctl 所要做的:控制 I/O 通道。 与 scull 不同,实际设备的另一个重要特点是,要读取或写入的数据需要同其他硬件交换而得,这 就需要某些同步机制。这就是阻塞型 I/O 和异步通知概念产生的基础,本章将通过改写 scull 设 备驱动程序介绍这两个概念,这个驱动程序利用不同进程间的交互产生异步事件。与最初的 scull 相同,你无需使用特定的硬件来测试驱动程序的工作情况。直到第 8 章“硬件管理”我们才会真 正与硬件打交道。 5.1 ioctl 在用户空间内调用的 ioctl 函数一般具有如下原型: int ioctl(int fd, int cmd, ...); 由于使用了一连串的“.”的缘故,这个原型在 Unix 系统调用中显得比较特别,通常这些点代表 可变数目的参数表。但是在实际系统中,系统调用不会真正使用可变数目的参数,而是必须有精确 99 第 5 章 增强的字符驱动程序操作 定义的参数个数,因为用户程序只能通过硬件“门”才能访问它们,这一点在第 2 章“用户空间 与内核空间”中已经指出过了。所以,原型中的这些点并不是数目不定的一串参数,而只是一个可 选参数,习惯上用 char *argp 定义,这里用点只是为了在编译时防止编译器进行类型检查。第 3 个参数的具体形式依赖于要完成的控制命令,也就是第 2 个参数。某些控制命令不需要参数,某 些需要一个整数参数,而某些则需要一个指针参数。使用指针可以向 ioctl 传递任意数据,这样设 备可以与用户空间交换任意数量的数据。 另一方面,设备驱动程序的 ioctl 方法,是按照如下原型获取其参数的: int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg); inode 和 filp 两个指针的值对应于应用程序传递的文件描述符 fd,传给 open 方法的也是同一参 数。参数 cmd 由用户空间不经修改地传递给驱动程序,可选的 arg 参数则无论用户程序使用的 是指针还是整数值,它都以 unsigned long 的形式传递给驱动程序。如果调用程序没有传递第 3 个参数,驱动程序所接收的 arg 没有任何意义。 由于对这个附加参数的类型检查被关闭了,如果传递给 ioctl 一个非法参数,编译器就无法报警, 这样,程序员就有可能漏过这个错误,而直到运行时才会察觉。这个类型检查的缺陷可以视为 ioctl 定义中的一个小问题,不过比起它提供的通用性,这也是必要的代价。 读者可能已经想到了,大多数 ioctl 的实现中都包括一个 switch 语句来根据 cmd 参数选择对应 的操作。不同的命令被赋予不同的数值,为了简化代码,通常会在代码中使用符号名代替数值,这 些符号名都是在预处理中赋值的。定制的设备驱动程序通常会在它们的头文件中声明这些符号,在 scull.h 中声明了 scull 所使用的符号。为了访问这些符号,用户程序自然也要包含这些头文件。 5.1.1 选择 ioctl 命令 在编写 ioctl 代码之前,需要选择对应不同命令的编号。遗憾的是,简单地从 1 开始选择号码是 不行的。 为了防止对错误的设备使用正确的命令,命令号应该在系统范围内唯一。这种错误匹配并不是不会 发生,程序可能发现自己正在试图对 FIFO 和 audio 等这类非串口设备输入流修改波特率。如果 每一个 ioctl 命令都是唯一的,应用程序进行这种操作就会得到一个 EINVAL 错误,而不是无意 间成功地完成了意想不到的操作。 为方便程序员创建唯一的 ioctl 命令号,每一个命令号被分为多个位字段。Linux 的第一版使用了 一个 16 位整数:高 8 位是与设备相关的“幻”数,低 8 位是一个序列号码,在设备内是唯一 的。当时采用这种方案是因为,用 Linus 的话说,他有点“无头绪”,后来才得到一个更好的位字 段分割方案。遗憾的是,相当多的驱动程序仍使用旧的约定,它们不得不这样:修改命令号会使很 多已有的程序无法运行。不过在我的源码中,使用了新的命令定义约定。 为 了 按 新 方 法 为 驱 动 程 序 选 择 ioctl 号 , 应 该 首 先 看 看 include/asm/ioctl.h 和 Documentation/ioctl-number.txt 这两个文件。头文件定义了位字段:类型(幻数)、基数、传送方 向、参数大小等等。ioctl-number.txt 文件中罗列了内核使用的幻数,这样,在选择自己的幻数时 100 Linux 设备驱动程序 就可以避免和内核冲突。这个文件也给出了为什么应该使用这个约定的原因。 现在已经不赞成使用的旧方法非常简单:选择一个 8 位幻数,比如“k”(十六进制为 0x6b),然 后加上一个基数,就象这样: #define SCULL_IOCTL1 0x6b01 #define SCULL_IOCTL2 0x6b02 /* .... */ 如果应用程序和驱动程序都约定使用这些号码,那么只要在驱动程序里实现 switch 语句就可以 了。但是,不应该再使用这种定义 ioctl 号码的传统 Unix 方法。这里介绍旧方法只是想给读者看 看一个 ioctl 号码大致是个什么样子。 定义号码的新方法使用了 4 个位字段,它们有如下意义。下面所介绍的新符号都定义在 中。 类型(type) 幻数。选择一个号码(记住先仔细阅读 ioctl-number.txt),并在整个驱动程序中使用这个号码。这 个字段有 8 位宽(_IOC_TYPEBITS)。 号码(number) 序(顺序的)数。它也是 8 位宽(_IOC_NRBITS)。 方向(direction) 如果该命令有数据传输,它定义数据传输的方向。可以使用的值有,_IOC_NONE(没有数据传输)、 _IOC_READ、_IOC_WRITE 和 _IOC_READ | _IOC_WRITE(双向传输数据)。数据传输是从应 用程序的角度看的;IOC_READ 意味着从设备中读数据,所以驱动程序必须向用户空间写数据。 注 意 , 该 字 段 是 一 个 位 掩 码 , 因 此 可 以 用 逻 辑 AND 操 作 从 中 分 解 出 _IOC_READ 和 _IOC_WRITE。 尺寸(size) 所涉及的用户数据大小。这个字段的宽度与体系结构有关,当前的范围从 8 位到 14 位不等。可 以在宏 _IOC_SIZEBITS 中找到某种体系结构的具体数值。不过,如果你想保持你的驱动程序的 可移植性,你最多只能使用 255,也就是 8 位。系统并不强制使用这个字段。如果需要更大尺度 的数据传输,则可以忽略这个字段。稍后我们将介绍如何使用这个字段。 包 含 在 之 中 的 头 文 件 定 义 了 可 以 用 于 构 造 命 令 号 的 宏 : _IO(type,nr)、_IOR(type,nr,dataitem)、_IOW(type,nr,dataitem) 和 IOWR(type,nr,dataitem)。每 一个宏都对应一种可能的数据传输方向。type 和 number 字段通过参数传递,size 字段的值使用 sizeof(dataitem) 来获得。头文件还定义了解码宏:_IOC_DIR(nr)、_IOC_TYPE(nr)、_IOC_NR(nr) 和 _IOC_SIZE(nr)。我不打算详细介绍这些宏,头文件里的定义已经足够清楚了,本节稍后也会 给出样例。 101 第 5 章 增强的字符驱动程序操作 下面是 scull 中的一些 ioctl 命令定义。需要特别指出的是,这些命令设置和获取驱动程序的配置 参数。 /* Use 'k' as magic number */ #define SCULL_IOC_MAGIC 'k' #define SCULL_IOCRESET _IO(SCULL_IOC_MAGIC, 0) /* * S means "Set" through a ptr * T means "Tell" directly with the argument value * G means "Get": reply by setting through a pointer * Q means "Query": response is on the return value * X means "eXchange": G and S atomically * H means "sHift": T and Q atomically */ #define SCULL_IOCSQUANTUM _IOW(SCULL_IOC_MAGIC, 1, scull_quantum) #define SCULL_IOCSQSET _IOW(SCULL_IOC_MAGIC, 2, scull_qset) #define SCULL_IOCTQUANTUM _IO(SCULL_IOC_MAGIC, 3) #define SCULL_IOCTQSET _IO(SCULL_IOC_MAGIC, 4) #define SCULL_IOCGQUANTUM _IOR(SCULL_IOC_MAGIC, 5, scull_quantum) #define SCULL_IOCGQSET _IOR(SCULL_IOC_MAGIC, 6, scull_qset) #define SCULL_IOCQQUANTUM _IO(SCULL_IOC_MAGIC, 7) #define SCULL_IOCQQSET _IO(SCULL_IOC_MAGIC, 8) #define SCULL_IOCXQUANTUM _IOWR(SCULL_IOC_MAGIC, 9, scull_quantum) #define SCULL_IOCXQSET _IOWR(SCULL_IOC_MAGIC,10, scull_qset) #define SCULL_IOCHQUANTUM _IO(SCULL_IOC_MAGIC, 11) #define SCULL_IOCHQSET _IO(SCULL_IOC_MAGIC, 12) #define SCULL_IOCHARDRESET _IO(SCULL_IOC_MAGIC, 15) /* debugging tool */ #define SCULL_IOC_MAXNR 15 最后一条命令,即 HARDRESET,用来将模块使用计数器复位为 0,这样就可以在因计数器而发 生错误时仍可以卸载模块。实际的源码还定义了从 IOCHQSET 到 HARDRESET 之间的所有命 令,但这里没有列出。 尽管根据已有的约定,ioctl 应该使用指针完成数据交换,但我们仍然选择用两种方法实现整数参 数传递――通过指针和显式数值。同样,这两种方法还用于返回整数:通过指针或设置返回值。如 果返回值是正的,就表示工作正常。从任何一个系统调用返回时,正的返回值是受保护的(如我们 在 read 和 write 所见到的),而负值则被认为是一个错误,并被用来设置用户空间中的 errno 变 量。 “exchange”和“shift”操作对 scull 设备来说并不特别有用。我们实现“exchange”操作是为 了示范在驱动程序中如何把分离的操作合并成一个原子操作,而“shift”操作则将“tell”和“query” 操作合并在一起。某些时候需要“测试兼设置”这类操作是原子操作――特别是当应用程序需要加 锁和解锁时*。 显式的命令序数没什么特别含义,仅仅用来区分命令。其实甚至可以在读命令和写命令中使用同一 序数,因为实际 ioctl 号中的“方向”位肯定不一样,不过最好还是不要这样做。除了在声明中用 到序数外,别的地方我们都不用它,这样就不必为它分配一个符号了。这也就是为什么前面给出的 定义中直接使用了数字的原因。例子示范了一种使用命令号的方法,也可以随意使用其它不同的方 法。 * 当一段程序代码总是被作为一条单一指令执行,而且执行期间不能被打断(如其它运行代码),就称这段代码是 原子的。 102 Linux 设备驱动程序 当前,内核并未使用 ioctl 的 cmd 参数的值,以后也不太可能使用。这样,如果想偷懒,可以不 使用上面那些复杂的声明,而直接显式地声明一组标量数字。由此带来的问题是,这样将无法从位 字段中受益了。头文件 就是这种旧分格的例子,它使用了 16 位的标量数值定义 ioctl 命令,这并非由于懒惰,而是那时只有这种方法,现在修改它会引起一大堆兼容性问题。 5.1.2 返回值 ioctl 的实现通常就是一个基于命令号的 switch 语句。但是如果命令号不能匹配任何合法操作时, 默认动作是什么?这问题颇有争议。有些内核函数会返回 -ENVAL(“Invalid argument”,非法参 数),表示命令参数不是合法参数。然而 POSIX 标准规定,如果使用了不合适的 ioctl 命令参数, 应该返回 -ENOTTY。在 libc5 及其以前的 C 库版本中,与这个值相对应的字符串一直都是“Not a typewriter”,只到 libc6 才把消息换成了“Inappropriate ioctl for device”,这看起来更贴切些。 因为绝大多数较新的 Linux 系统都基于 libc6,所以我们还是坚持标准,返回 -ENOTTY 吧。尽 管如此,对非法 ioctl 命令返回 -EINVAL 仍然是很普遍的做法。 5.1.3 预定义命令 尽管 ioctl 系统调用绝大部分用于操作设备,但还有一些命令是可以由内核识别的。要注意,当这 些命令用于设备时,它们会在自己的文件操作被调用之前处理。所以,如果为自己的 ioctl 命令选 用了与这些预定义命令相同的号码,就永远不会收到该命令的请求,而且由于 ioctl 号码冲突,应 用程序的行为将无法预测。 预定义命令分为三组: „ 可用于任何文件(普通、设备、FIFO 和套接字)的 „ 只用于普通文件的 „ 用于特定文件系统类型的 最后一组命令只能在宿主(hosting)文件系统上执行(见 chattr 命令)。设备驱动程序开发人员只对 第一组感兴趣,它们的幻数都是“T”。分析其它组的工作留给读者做练习。ext2_ioctl 是其中最有 意 思 的 函 数 ( 尽 管 比 你 想 的 容 易 的 多 ), 因 为 它 实 现 了 只 追 加 ( append-only ) 标 志 和 不 可 变 (immutable)标志。 下列 ioctl 命令对任何文件都是预定义的: FIOCLEX 设置执行时关闭标志(File IOctl CLose on EXec)。设置了这个标志后,当调用进程执行一个新程 序时文件描述符将被关闭。 FIONCLEX 清除执行时关闭标志。 FIOASYNC 设置或复位文件异步通知(稍后在本章“异步通知”一节中讨论)。注意直到 Linux 2.2.4 版本的 103 第 5 章 增强的字符驱动程序操作 内核都不正确地使用了这个命令来修改 O_SYNC 标志。因为这两个动作都可以通过其它方法完 成,所以实际上没有人使用 FIOASYNC 命令了,列在这只是为了完整。 FIONBIO 意指“File IOctl Non-Blocking I/O”,即“文件 ioctl 非阻塞型 I/O”(本章稍后在“阻塞型与非阻 塞型操作”一节中介绍)。该调用修改 filp->f_flags 中的 O_NONBLOCK 标志。传递给系统调用 的第 3 个参数指明了是设置还是清除该标志。本章稍后我们就可以看到它的作用。注意,fcntl 系 统调用也可以使用 F_SETFL 命令修改这个标志。 上面的最后一项中我们引入了一个新的系统调用,即 fcntl,看起来很象 ioctl。实际上 fnctl 调用 也要传递一个命令参数和一个附加的可选参数,在这点上它类似 ioctl。它和 ioctl 的不同主要是由 于历史原因造成的:当 Unix 的开发人员面对控制 I/O 操作的问题时,他们认为文件和设备是不 同的。那时,与 ioctl 实现相关的唯一设备就是终端,这也解释了为什么非法的 ioctl 命令的标准 返回值是 -ENOTTY。现在情况虽然不同了,但是 fcntl 还为了向后兼容而保留下来。 5.1.4 使用 ioctl 参数 在分析 scull 驱动程序的 ioctl 代码之前我们还有一点要讲解,就是怎样使用那个附加参数。如果 它是个整数,很简单,直接用就行了。如果是个指针,就要注意一些问题了。 当用一个指针指向用户空间时,必须确保指向的用户空间是合法的,而且对应的页面也已正确定位。 如果内核代码企图越界访问一个地址,处理器就会产生一个异常。在包括 Linux 2.0.x 的以前所有 内核版本代码中,这个异常都被转换为 oops 消息;2.1 及以后版本处理这个问题则温和许多。无 论如何,驱动程序应该负责对每个用到的用户空间地址做适当的检查,如果是非法地址则应该返回 一个错误。 内核 2.2.x 及以后版本的地址验证是通过函数 access_ok 实现的,它在中声 明: int access_ok(int type, const void *addr, unsigned long size); 第一个参数应该是 VERIFY_READ 或 VERIFY_WRITE,取决于要执行的动作是读还是写用户空 间内存区。addr 参数是一个用户空间地址,size 是字节数。例如,如果 ioctl 要从用户空间读一 个整数,size 就是 sizeof(int)。如果在指定地址处既要读又要写,则应该用 VERIFY_WRITE,它 是 VERIFY_READ 的超集。 与大多数函数不同,access_ok 返回一个布尔值:1 表示成功(访问成功),0 表示失败(访问不 成功)。如果返回失败,驱动程序通常要返回 -EFAULT 给调用者。 关于 access_ok 有两点有趣之处需要注意。第一,它并没有完成验证内存的全部工作,而只检查 了引用的内存是否位于进程有合适访问权限的区域内。特别是要确保访问地址没有指向内核空间内 存区。第二,大多数驱动程序代码中都不需要真正调用 access_ok 。因为后面要讲到的内存管理 程序会处理它。尽管如此,我们还是示范一下它的使用,既为了理解其过程,也是为了向后兼容的 原因。本章末尾还会深入讨论向后兼容问题。 104 Linux 设备驱动程序 scull 的源码在 switch 语句前,通过分析 ioctl 号码的位字段来检查参数: int err = 0, tmp; int ret = 0; /* * extract the type and number bitfields, and don't decode * wrong cmds: return ENOTTY (inappropriate ioctl) before access_ok() */ if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC) return -ENOTTY; if (_IOC_NR(cmd) > SCULL_IOC_MAXNR) return -ENOTTY; /* * the direction is a bitmask, and VERIFY_WRITE catches R/W * transfers. `Type' is user oriented, while * access_ok is kernel oriented, so the concept of "read" and * "write" is reversed */ if (_IOC_DIR(cmd) & _IOC_READ) err = !access_ok(VERIFY_WRITE, (void *)arg, _IOC_SIZE(cmd)); else if (_IOC_DIR(cmd) & _IOC_WRITE) err = !access_ok(VERIFY_READ, (void *)arg, _IOC_SIZE(cmd)); if (err) return -EFAULT; 在调用 access_ok 之后,驱动程序可以安全进行实际的数据传送了。除了 copy_from_user 和 copy_to_user 函数外,程序员还可以使用已经为最常用的数据尺寸(1、2 或 4 个字节,64 位平 台上的 8 字节)优化过的一组函数。这些函数定义在中,列在下面: put_user(datum, ptr) "_ _put_user(datum, ptr)" 这些宏把 datum 写到用户空间。它们相对比较快,当要传递单个数据时,应该用它而不是用 copy_to_user 。由于宏展开时不做类型检查,所以可以传递给 put_user 任意类型的指针,只要 是个用户空间地址就行。传递的数据尺寸依赖于 ptr 参数的类型,在编译时由特殊的 gcc 伪函数 确定,这里就没有必要介绍了。总而言之,如果 ptr 是一个字符指针,就传递一个字节,2、4、8 字节的情况类似。 put_user 进行检查以确保进程可以写入指定的内存地址。成功返回 0 ,出错返回 -EFAULT 。_ _put_user 做的检查少些(它不调用 access_ok),但对于某些错误地址仍会出现操作失败。因而, _ _put_user 应该在已经使用 access_ok 检验过内存区后再使用。 一般的情况是,实现一个 read 方法时,可以调用 _ _put_user 来节省几个时钟周期。或者在复 制几项数据之前,调用一次 access_ok。 get_user(local, ptr) "_ _get_user(local, ptr)" 这些宏用于从用户空间接收一个数据。除了传输方向相反,它们与 put_user 和 _ _put_user 差 不多。接收的数值保存在局部变量 local 中,返回值则指明了操作是否成功。同样,_ _get_user 应 该在操作地址已被 access_ok 检验后使用。 如果试图使用上面列出的函数传递尺寸不符合任意一个特定值的数值,结果通常是编译器会给出一 条奇怪的消息,比如“conversion to non-scalar type requested(需要转换为非标量类型)”。这种 情况下,必须使用 copy_to_user 或者 copy_from_user。 105 第 5 章 增强的字符驱动程序操作 5.1.5 权能与受限操作 对设备的访问由设备文件的许可控制,驱动程序通常不进行许可检查。不过也有这种情况,允许用 户对设备读/写而其它的操作被禁止。例如,不是所有的磁带驱动器使用者都可以设置它的默认块 大小,允许用户使用磁盘设备也并不意味着就可以格式化磁盘。在类似这种情况下,驱动程序必须 进行附加检查以确认用户是否有权进行请求的操作。 根据 Unix 系统传统,特权操作仅限于超级用户帐号。这种特权要么全有,要么全没――超级用户 几乎可以做任何事,所有其他用户则受到严格的限制。Linux 内核,如 2.2 版本,提供了一个更 为灵活的系统,称为权能(capabilities)。基于权能的系统抛弃了那种非有即无的特权分配方式, 而是把特权操作划分为独立的组。这样,某个特定的用户或程序可以被授权执行某一指定特权操作, 同时又没有执行其它不相关操作的能力。权能在用户空间还很少使用,而内核代码中几乎已经全部 使用这种方式了。 全部权能操作都可以在 中找到。对驱动程序开发者有意义的只是其中一部分, 罗列如下: CAP_DAC_OVERRIDE 越过文件或目录访问许可的能力 CAP_NET_ADMIN 执行网络管理任务的能力,包括那些能影响网络接口的任务。 CAP_SYS_MODULE 载入或卸除内核模块的能力 CAP_SYS_RAWIO 执行“裸”I/O 操作的能力。例如访问设备端口或直接与 USB 设备通信。 CAP_SYS_ADMIN 截获的能力,它提供了访问许多系统管理操作的途径。 CAP_SYS_TTY_CONFIG 执行 tty 配置任务的能力。 在执行一项特权操作之前,设备驱动程序应该检查调用进程是否有合适的权能,这是用 capable 函 数来实现的,它定义在 中: int capable(int capability); 在 scull 示例程序中,任何用户都允许查询 quantum 和 quantum 集的大小。但是只有授权用户 可以更改这些值,因为不恰当的值会降低系统性能。scull 的 ioctl 实现了在需要时检查用户的特 权级别: if (! capable (CAP_SYS_ADMIN)) return -EPERM; 106 Linux 设备驱动程序 因为缺少针对该任务的更好的权能定义,所以这里使用了 CAP_SYS_ADMIN。 5.1.6 ioctl 命令的实现 scull 的 ioctl 实现中只传递设备的可配置参数,很简单: switch(cmd) { #ifdef SCULL_DEBUG case SCULL_IOCHARDRESET: /* * reset the counter to 1, to allow unloading in case to allow unloading in case ecause the invoking * process has the device open. */ while (MOD_IN_USE) MOD_DEC_USE_COUNT; MOD_INC_USE_COUNT; /* don't break: fall through and reset things */ #endif /* SCULL_DEBUG */ case SCULL_IOCRESET: scull_quantum = SCULL_QUANTUM; scull_qset = SCULL_QSET; break; case SCULL_IOCSQUANTUM: /* Set: arg points to the value */ if (! capable (CAP_SYS_ADMIN)) return -EPERM; ret = _ _get_user(scull_quantum, (int *)arg); break; case SCULL_IOCTQUANTUM: /* Tell: arg is the value */ if (! capable (CAP_SYS_ADMIN)) return -EPERM; scull_quantum = arg; break; case SCULL_IOCGQUANTUM: /* Get: arg is pointer to result */ ret = _ _put_user(scull_quantum, (int *)arg); break; case SCULL_IOCQQUANTUM: /* Query: return it (it's positive) */ return scull_quantum; case SCULL_IOCXQUANTUM: /* eXchange: use arg as pointer */ if (! capable (CAP_SYS_ADMIN)) return -EPERM; tmp = scull_quantum; ret = _ _get_user(scull_quantum, (int *)arg); if (ret == 0) ret = _ _put_user(tmp, (int *)arg); break; case SCULL_IOCHQUANTUM: /* sHift: like Tell + Query */ if (! capable (CAP_SYS_ADMIN)) return -EPERM; tmp = scull_quantum; scull_quantum = arg; return tmp; default: /* redundant, as cmd was checked against MAXNR */ return -ENOTTY; 107 第 5 章 增强的字符驱动程序操作 } return ret; scull 中还包括 6 个操作 scull_qset 的入口,它们和 scull_quantum 的相应入口是一样的,这里 不再赘述。 从调用方的观点(例如从用户空间)看,传送和接收参数的 6 种途径如下: int quantum; ioctl(fd,SCULL_IOCSQUANTUM, &quantum); ioctl(fd,SCULL_IOCTQUANTUM, quantum); ioctl(fd,SCULL_IOCGQUANTUM, &quantum); quantum = ioctl(fd,SCULL_IOCQQUANTUM); ioctl(fd,SCULL_IOCXQUANTUM, &quantum); quantum = ioctl(fd,SCULL_IOCHQUANTUM, quantum); 当然,正常的驱动程序不会在一个地方就实现这么多调用方式。在这里只是为了示范各种不同的方 法。不过,通常情况下数据交换形式应该保持一致,要么都用指针(比较普遍),要么都用数值(用 的较少),尽量避免混用。 5.1.7 非 ioctl 的设备控制 有时通过向设备写入控制序列可以更好地控制设备。在控制台驱动程序中就使用了这一技术,它称 为“转义序列(escape sequence)”,用于控制移动光标,改变默认颜色,或其它配置任务。用这 种方法实现设备控制的好处是用户仅通过写数据就可以控制设备,无需使用(有时还得编写)配置 设备的程序。 例如,程序 setterm 通过打印转义序列来配置控制台(或某个终端)。这种方法的优点是可以对设 备进行远程控制。控制程序可以运行在非被控设备所在的计算机上,然后用一个简单的数据流重定 向就可以完成配置工作。这项技术已经在终端上使用,但它还可以更通用些。 通过打印序列进行控制的缺点是,它给设备增加了策略限制。例如,只有确认控制序列不会出现写 到设备的正常数据中时,才能使用这种技术。而只有部分的终端设备能满足这个要求。尽管文本显 示只需显示 ASCII 字符,但有时写数据流中也会出现控制字符,从而影响控制台的设置。例如, 对一个二进制文件使用 grep,找出的行可能什么都会包含,结果是经常造成控制台的字体错误*。 通过写入来控制的方式,非常适合于那种不传送数据而只响应命令的设备,如机器人。 例如,笔者编写过一个驱动程序,该驱动程序控制相机在两个轴上移动。在这个驱动程序里,“设 备”只是一对旧步进马达,不能读写。“发送数据流”的概念对步进马达来说没什么意义。这种情 况下,驱动程序将所写的数据解释为 ASCII 命令,并把请求转换为脉冲序列来操纵步进马达。这 种思路,与给调制解调器发送 AT 指令以设置通讯的方法基本类似,主要区别就是连接调制解调 器的串口还要发送真正的数据。直接设备控制的优点是使用 cat 就可以移动相机,而不必编写和 * CTRL-N 设置替换字体,它由图形字符组成,对你的 shell 输入来说是很不友好的;如果碰到这种问题,回显一 个 CTRL-O 字符可恢复主字体。 108 Linux 设备驱动程序 编译用于实现 ioctl 调用的代码。 当编写这种“面向命令的”驱动时,没什么必要实现 ioctl 方法。在解释器中加一条指令,实现和 使用都更简单些。 尽管如此,有时可能需要做相反的事情:不是用“写入”解释器来避免使用 ioctl,而是只使用 ioctl, 完全不使用“写入”。同时,驱动程序附带了一个特定的命令行工具,专门负责把命令送给驱动程 序。这种方法把内核空间的复杂性转移到了用户空间,这样处理起来可能会容易些,并且有助于减 少驱动程序的尺寸,然而,用户却无法再使用简单的命令如 cat 或 echo 来操作驱动程序。 5.2 阻塞型 I/O read 的一个问题是当尚无数据可读,而又没有到达文件尾时该怎么办。 默认的回答是“进入睡眠并等待数据”。这一节将介绍如何使进程睡眠,如何唤醒,以及应用程序 如何在不盲目执行 read 调用或阻塞的情况下查看是否有数据。同一概念也适用于 write。 和前面一样,我们在示范实际代码前先解释一些概念。 5.2.1 睡眠和唤醒 当进程等待一个事件(如数据到达或其他进程终止)时,它应该进入睡眠。睡眠使该进程暂时挂起, 腾出处理器给其他进程使用。在将来的某个时间,等待的事件发生了,进程被唤醒继续执行。这一 节讨论内核 2.4 中进程睡眠和唤醒的机制。以前版本稍后在本章“向后兼容性”一节中讲解。 Linux 中有几种处理睡眠和唤醒的方法,每种分别适合于不同的需求。不过所有方法都要处理同一 个基本数据类型:等待队列(wait_queue_head_t)。正确地说,一个“等待队列”其实是由正等 待事件发生的进程组成的一个队列。等待队列的声明和初始化部分如下: wait_queue_head_t my_queue; init_waitqueue_head (&my_queue); 如果一个等待队列被声明为静态的(比如不是某个过程的自动变量,或某个动态分配的数据结构的 一部分),它就可以在编译时初始化: DECLARE_WAIT_QUEUE_HEAD (my_queue); 忘记初始化等待队列是一个常见的错误(特别是以前的内核还不要求做这种初始化),如果没有初 始化,则可能导致无法预见的错误。 一旦声明了等待队列,完成了初始化,进程就可以使用它进入睡眠。基于睡眠的深度不同,可调用 sleep_on 的不同变体函数来完成睡眠。 sleep_on(wait_queue_head_t *queue); 把进程放入这个队列睡眠。sleep_on 有个缺点,就是不能被中断。其结果是,如果进程等待的事 109 第 5 章 增强的字符驱动程序操作 件永远不发生,进程就醒不来了(也杀不掉)。 interruptible_sleep_on(wait_queue_head_t *queue); 除了睡眠可以被信号中断外,这个变体做的事和 sleep_on 类似。在 wait_event_interruptible(稍 后介绍)出现以前,它也是设备驱动程序开发者一直使用的函数。 sleep_on_timeout(wait_queue_head_t *queue, long timeout); interruptible_sleep_on_timeout(wait_queue_head_t *queue, long timeout); 这两个函数和前两个类似,不同之处是到了指定时间后就不再睡眠。时间是用“jiffies”指定的, 第 6 章会讲到。 void wait_event(wait_queue_head_t queue, int condition); int wait_event_interruptible(wait_queue_head_t queue, int condition); 这两个宏是睡眠的首选方法。它们把等待事件和测试事件是否发生合并了起来,避免了竞态的发生。 它们会一直睡眠到 C 布尔表达式 condition 为真时为止。这两个宏扩展为 while 循环,condition 在循环期间不断重新求值――这区别于单个函数调用或简单的宏的行为,它们只会在调用时求一次 值。第二个宏实现为一个表达式,如果成功就得 0;如果循环被信号打断就得出 -ERESTARTSYS。 值得再重复一遍的是,驱动程序开发人员应该基本只用这些函数/宏中的“可中断的”形式。不可 中断的那些只在很少情况下使用,在这些情况下信号不能处理,例如,等待从交换空间取得一个数 据页面。大多数驱动程序都不会碰到这类特殊情况。 当然,睡眠只是问题的一半,进程总得在未来某个时刻被唤醒。如果一个驱动程序睡眠了,那么程 序中通常还有处理唤醒的部分,一旦等待的事件发生它们就会起作用。典型情况下驱动程序会在新 数据到达时的中断处理程序中唤醒睡眠者。当然其他实现方法也是有可能的。 既然有不止一种睡眠方法,当然也就有不止一种唤醒方法。内核提供的用来唤醒进程的高级函数有: wake_up(wait_queue_head_t *queue); 这个函数将唤醒在这个事件等待队列中的所有进程。 wake_up_interruptible(wait_queue_head_t *queue); wake_up_interruptible 只唤醒那些在“可中断睡眠”状态的进程。那些使用不可中断函数或宏而 睡眠在等待队列上的进程则继续睡眠。 wake_up_sync(wait_queue_head_t *queue); wake_up_interruptible_sync(wait_queue_head_t *queue); 通常,wake_up 调用会立即引发一次重新调度,这意味着在 wake_up 之前运行的其它进程都会 返回。这种“同步”的变体则不同,它让已醒来的进程继续运行,但不重新调度 CPU。在已知当 前进程就要进入睡眠因而要引发一次重新调度时,为了避免重调度就可以使用它。注意醒来的进程 可能立即会在一个不同的处理器上运行,所以不要指望这些函数提供了互斥性。 如果驱动程序使用的是 interruptible_sleep_on,那么用 wake_up 或是 wake_up_interruptible 几 乎没什么区别。不过使用后者是普遍约定,因为在两个调用间保持了一致性。 作为使用等待队列的一个例子,想象一下当进程读设备时进入睡眠而在其他人写设备时被唤醒的情 景。下列代码完成这件事: 110 Linux 设备驱动程序 DECLARE_WAIT_QUEUE_HEAD(wq); ssize_t sleepy_read (struct file *filp, char *buf, size_t count, loff_t *pos) { printk(KERN_DEBUG "process %i (%s) going to sleep\n", current->pid, current->comm); interruptible_sleep_on(&wq); printk(KERN_DEBUG "awoken %i (%s)\n", current->pid, current->comm); return 0; /* EOF */ } ssize_t sleepy_write (struct file *filp, const char *buf, size_t count, loff_t *pos) { printk(KERN_DEBUG "process %i (%s) awakening the readers...\n", current->pid, current->comm); wake_up_interruptible(&wq); return count; /* succeed, to avoid retrial */ } 这个设备的代码在我们的示例程序中称为 sleepy,和其它的一样,可以用 cat 或输入/输出重定向 来测试它。 关于等待队列,一个要牢记的重点是,被唤醒并不能保证等待的事件已经发生了,进程可能因其它 原因被唤醒,大部分的原因是收到了一个信号。从睡眠态返回后,任何睡眠的代码都应该在测试 condition 的循环中这么做,就象本章稍后要介绍的“实现示例:sucllpipe”中讲到的那样。 5.2.2 等待队列的深入分析 大多数驱动程序开发者完成任务时,知道前面所讨论的这些内容已经足够了。也可能还有些人想深 入了解一下,这一节可以满足这些人的好奇心,其他人可以直接跳到下一节,不会错过什么重要的 东西。 wait_queue_head_t 类型是一个相当简单的结构,在 中定义。它只包括一个锁变 量和一个正睡眠进程的链表。链表中的各个数据成员项的类型是 wait_queue_t,链表就是在 定义的通用链表,在第 10 章的“链表”一节还会讨论。通常,wait_queue_t 结构都 从堆栈中分配,这通过如 interruptible_sleep_on 之类的函数来完成。这些结构之所以在堆栈中, 是因为在相关函数中它们都定义为自动变量。一般情况下程序员不用处理它们。 但 是 在 一 些 高 级 应 用 中 , 可 能 会 要 求 直 接 处 理 wait_queue_t 变 量 。 为 此 我 们 浏 览 一 下 interruptible_sleep_on 这类函数的内部实现过程。下面是一个简化了的 interruptible_sleep_on 的 实现,它使进程进入睡眠。 void simplified_sleep_on(wait_queue_head_t *queue) { wait_queue_t wait; init_waitqueue_entry(&wait, current); current->state = TASK_INTERRUPTIBLE; add_wait_queue(queue, &wait); schedule(); 111 第 5 章 增强的字符驱动程序操作 remove_wait_queue (queue, &wait); } 这段代码创建和初始化一个 wait_queue_t 变量(即 wait,在堆栈中分配)。任务状态被置为 TASK_INTERRUPTIBLE,表示处于可中断睡眠中。这个等待队列成员随即被加入队列(由参数 wait_queue_head_t *queue 给出)。接着调用 schedule,使处理器可以被其它进程使用。只有其 它进程唤醒该睡眠进程后 schedule 才返回,并设置其进程状态为 TASK_RUNNING。接着,这 个队列成员从等待队列中删除,睡眠结束。 图 5-1 展示了等待队列数据结构的内部组成,以及如何被进程使用。 图 5-1:Linux 2.4 的等待队列 快速浏览一下内核代码,会发现非常多的程序都是用类似前面这个例子的方法来“手工”处理睡眠 的。其中大多数实现可以追溯到 2.2.3 之前的内核,那时 wait_event 还没有引入。前面已经说过, 现在处理等待事件的睡眠的首选方法是 wait_event,因为使用 interruptible_sleep_on 常会引起恼 人的竞态。至于为什么会这样,将在第 9 章“无竞争地进入睡眠”中详细阐述。简单地说,就是在 驱动程序将要睡眠和实际调用 interruptible_sleep_on 之间的时间里,情况可能已经发生了变化。 显式调用调度器的另一个原因是为了执行“排外”的等待。有可能发生下面这种情况,几个进程都 在等待同一事件,当 wake_up 被调用时,这些进程都试图重新执行。假如该事件表示到达了一个 原子性数据,那么只有一个进程可以读取它,所有其它进程仅仅是醒来后发现没有数据,然后接着 睡眠。 112 Linux 设备驱动程序 这种情况有时被称为“哄赶牧群”。在要求高性能的环境,该问题会浪费大量资源。创建许多的进 程运行,什么事也不做,还产生了很多上下文切换和处理器负荷,结果什么用也没有。如果这些进 程只是简单地继续睡眠下去,情况就好多了。 为此,在 2.3 内核开发系列中增加了“排他睡眠”的概念。如果进程用排外模式睡眠,内核一次只 会唤醒其中的一个,这样,可在某些情况下提升系统性能。 实现一个排他睡眠的代码和普通睡眠很类似: void simplified_sleep_exclusive(wait_queue_head_t *queue) { wait_queue_t wait; init_waitqueue_entry(&wait, current); current->state = TASK_INTERRUPTIBLE | TASK_EXCLUSIVE; add_wait_queue_exclusive(queue, &wait); schedule(); remove_wait_queue (queue, &wait); } 在 任 务 状 态 中 增 加 TASK_EXCLUSIVE 标 志 表 明 进 程 正 处 于 排 他 睡 眠 中 。 调 用 add_wait_queue_exclusive 也是必须的。该函数把进程加在等待队列的尾部,所有其它进程的后 面。目的是让非排他睡眠的进程排在前面,以使它们总是先被唤醒。一旦“唤醒”到了第一个处于 排他睡眠中的进程,它就可以停止睡眠了。 细心的读者可能已经注意到了“排外”地操作等待队列和调度器的另一个原因。尽管 sleep_on 这 样的函数只会在一个等待队列中阻塞进程,但是直接操作队列就意味着允许在多个队列中同时睡 眠。当然大多数驱动程序并不需要在多个队列中睡眠,如果有例外,那就需要使用与前面演示的类 似代码。 希望进一步了解等待队列的读者可以阅读 和 kernel/sched.c。 5.2.3 编写可重入代码 进程睡眠以后,驱动程序仍然是活动的,而且可以由另一个进程调用。考虑一个控制台驱动程序的 例子。当一个进程在 tty1 上等待键盘输入,用户切换到 tty2 ,启动一个新的 shell。现在两个 shell 都在控制台驱动程序中等待键盘输入,尽管它们睡眠在不同的等待队列