Linux 设备驱动程序 第三版

第  1  章  第一章 设备驱动简介 

目录

1.1.  驱动程序的角色(见 [标题编号.])

1.2.  划分内核(见 [标题编号.])

1.2.1.  可加载模块(见 [标题编号.])

1.3.  设备和模块的分类(见 [标题编号.])

1.4.  安全问题(见 [标题编号.])

1.5.  版本编号(见 [标题编号.])

1.6.  版权条款(见 [标题编号.])

1.7.  加入内核开发社团(见 [标题编号.])

1.8.  本书的内容(见 [标题编号.])

 

以 Linux 为代表的自由操作系统的很多优点之一, 是它们的内部是开放给所有 人看的. 操作系统, 曾经是一个隐藏的神秘的地方, 它的代码只局限于少数的程 序员, 现在已准备好让任何具备必要技能的人来检查, 理解以及修改. Linux 已 经帮助使操作系统民主化. Linux 内核保留有大量的复杂的代码, 但是, 那些想 要成为内核 hacker 的人需要一个入口点, 这样他们可以进入代码中, 不会被代 码的复杂性压倒. 通常, 设备驱动提供了这样的门路.

驱动程序在 Linux 内核里扮演着特殊的角色. 它们是截然不同的"黑盒子", 使 硬件的特殊的一部分响应定义好的内部编程接口. 它们完全隐藏了设备工作的细 节. 用户的活动通过一套标准化的调用来进行, 这些调用与特别的驱动是独立的; 设备驱动的角色就是将这些调用映射到作用于实际硬件的和设备相关的操作上. 这个编程接口是这样, 驱动可以与内核的其他部分分开建立, 并在需要的时候在运行时"插入". 这种模块化使得 Linux 驱动易写, 以致于目前有几百个驱动可用.

编写 Linux 设备驱动有许多理由让人感兴趣. 可用的新硬件出现的速率(以及陈 旧的速率)就确保了驱动编写者在可见的将来内是忙碌的. 个别人可能需要了解

驱动以便存取一个他们感兴趣的特殊设备. 硬件供应商, 通过为他们的产品开发 Linux 驱动, 可以给他们的潜在市场增加大量的正在扩张的 Linux 用户基数. 还有 Linux 系统的开放源码性质意味着如果驱动编写者愿意, 驱动源码能够快

速地散布到几百万用户.

本书指导你如何编写你自己的驱动, 以及如何利用内核相关的部分. 我们采用一 种设备-独立的方法; 编程技术和接口, 在任何可能的时候, 不会捆绑到任何特 定的设备. 每一个驱动都是不同的; 作为一个驱动编写者, 你需要深入理解你的 特定设备. 但是大部分的原则和基本技术对所有驱动都是一样的. 本书无法教你 关于你的设备的东西, 但是它给予你所需要的使你的设备运行起来的背景知识的 指导.

在你学习编写驱动时, 你通常会发现大量有关 Linux 内核的东西. 这也许会帮 助你理解你的机器是如何工作的, 以及为什么事情不是如你所愿的快, 或者不是 如你所要的进行. 我们会逐步介绍新概念, 由非常简单的驱动开始并建立它们; 每一个新概念都伴有例子代码, 这样的代码不需要特别的硬件来测试.

本章不会真正进入编写代码. 但是, 我们介绍一些 Linux 内核的背景概念, 这 样在以后我们动手编程时, 你会感到乐于知道这些.

1.1.  驱动程序的角色

作为一个程序员, 你能够对你的驱动作出你自己的选择, 并且在所需的编程时间 和结果的灵活性之间, 选择一个可接受的平衡. 尽管说一个驱动是"灵活"的, 听 起来有些奇怪, 但是我们喜欢这个字眼, 因为它强调了一个驱动程序的角色是提供机制, 而不是策略. 

机制和策略的区分是其中一个在 Unix 设计背后的最好观念. 大部分的编程问题 其实可以划分为 2 部分:" 提供什么能力"(机制) 和 "如何使用这些能力"(策 略). 如果这两方面由程序的不同部分来表达, 或者甚至由不同的程序共同表达, 软件包是非常容易开发和适应特殊的需求.

例如, 图形显示的 Unix 管理划分为 X服务器, 它理解硬件以及提供了统一的 接口给用户程序, 还有窗口和会话管理器, 它实现了一个特别的策略, 而对硬件 一无所知. 人们可以在不同的硬件上使用相同的窗口管理器, 而且不同的用户可 以在同一台工作站上运行不同的配置. 甚至完全不同的桌面环境, 例如KDE 和 GNOME, 可以在同一系统中共存. 另一个例子是 TCP/IP 网络的分层结构: 操作 系统提供 socket 抽象层, 它对要传送的数据而言不实现策略, 而不同的服务器 负责各种服务( 以及它们的相关策略). 而且, 一个服务器, 例如 ftpd 提供文 件传输机制, 同时用户可以使用任何他们喜欢的客户端; 无论命令行还是图形客 户端都存在, 并且任何人都能编写一个新的用户接口来传输文件.

在驱动相关的地方, 机制和策略之间的同样的区分都适用. 软驱驱动是不含策略 的--它的角色仅仅是将磁盘表现为一个数据块的连续阵列. 系统的更高级部分提

供了策略, 例如谁可以存取软驱驱动, 这个软驱是直接存取还是要通过一个文件系统, 以及用户是否可以加载文件系统到这个软驱. 因为不同的环境常常需要不 同的使用硬件的方式, 尽可能对策略透明是非常重要的.

在编写驱动时, 程序员应当特别注意这个基础的概念: 编写内核代码来存取硬件, 但是不能强加特别的策略给用户, 因为不同的用户有不同的需求. 驱动应当做到 使硬件可用, 将所有关于如何使用硬件的事情留给应用程序. 一个驱动, 这样, 就是灵活的, 如果它提供了对硬件能力的存取, 没有增加约束. 然而, 有时必须 作出一些策略的决定. 例如, 一个数字 I/O 驱动也许只提供对硬件的字符存取, 以便避免额外的代码处理单个位.

你也可以从不同的角度看你的驱动: 它是一个存在于应用程序和实际设备间的软件层. 驱动的这种特权的角色允许驱动程序员y 严密地选择设备应该如何表现: 不同的驱动可以提供不同的能力, 甚至是同一个设备. 实际的驱动设计应当是在 许多不同考虑中的平衡. 例如, 一个单个设备可能由不同的程序并发使用, 驱动 程序员有完全的自由来决定如何处理并发性. 你能在设备上实现内存映射而不依 赖它的硬件能力, 或者你能提供一个用户库来帮助应用程序员在可用的原语之上 实现新策略, 等等. 一个主要的考虑是在展现给用户尽可能多的选项, 和你不得 不花费的编写驱动的时间之间做出平衡, 还有需要保持事情简单以避免错误潜 入.

对策略透明的驱动有一些典型的特征. 这些包括支持同步和异步操作, 可以多次 打开的能力, 利用硬件全部能力, 没有软件层来"简化事情"或者提供策略相关的 操作. 这样的驱动不但对他们的最终用户好用, 而且证明也是易写易维护的. 成 为策略透明的实际是一个共同的目标, 对软件设计者来说.

许多设备驱动, 确实, 是与用户程序一起发行的, 以便帮助配置和存取目标设备. 这些程序包括简单的工具到完全的图形应用. 例子包括 tunelp 程序, 它调整并 口打印机驱动如何操作, 还有图形的 cardctl 工具, 它是PCMCIA 驱动包的一 部分. 经常会提供一个客户库, 它提供了不需要驱动自身实现的功能.

本书的范围是内核, 因此我们尽力不涉及策略问题, 应用程序, 以及支持库. 有 时我们谈论不同的策略以及如何支持他们, 但是我们不会进入太多有关使用设备的程序的细节, 或者是他们强加的策略的细节. 但是, 你应当理解, 用户程序是一个软件包的构成部分, 并且就算是对策略透明的软件包在发行时也会带有配置文件, 来对底层的机制应用缺省的动作.

2.1 1.1. 驱动程序的角色

2.2 1.2. 划分内核

1.2.  划分内核

在 Unix 系统中, 几个并发的进程专注于不同的任务. 每个进程请求系统资源, 象计算能力, 内存, 网络连接, 或者一些别的资源. 内核是个大块的可执行文件, 负责处理所有这样的请求. 尽管不同内核任务间的区别常常不是能清楚划分, 内

核的角色可以划分(如同图内核的划分(见 [标题编号.]))成下列几个部分:

 

进程管理

内核负责创建和销毁进程, 并处理它们与外部世界的联系(输入和输出). 不同进程间通讯(通过信号, 管道, 或者进程间通讯原语)对整个系统功能 来说是基本的, 也由内核处理. 另外, 调度器, 控制进程如何共享 CPU, 是进程管理的一部分. 更通常地, 内核的进程管理活动实现了多个进程在 一个单个或者几个 CPU 之上的抽象.

内存管理

计算机的内存是主要的资源, 处理它所用的策略对系统性能是至关重要的. 内核为所有进程的每一个都在有限的可用资源上建立了一个虚拟地址空间. 内核的不同部分与内存管理子系统通过一套函数调用交互, 从简单的 malloc/free 对到更多更复杂的功能.

文件系统 

Unix 在很大程度上基于文件系统的概念; 几乎 Unix 中的任何东西都可 看作一个文件. 内核在非结构化的硬件之上建立了一个结构化的文件系统, 结果是文件的抽象非常多地在整个系统中应用. 另外, Linux 支持多个文 件系统类型, 就是说, 物理介质上不同的数据组织方式. 例如, 磁盘可被 格式化成标准 Linux 的 ext3 文件系统, 普遍使用的FAT 文件系统, 或者其他几个文件系统.

设备控制  

几乎每个系统操作最终都映射到一个物理设备上. 除了处理器, 内存和非 常少的别的实体之外, 全部中的任何设备控制操作都由特定于要寻址的设 备相关的代码来进行. 这些代码称为设备驱动. 内核中必须嵌入系统中出现的每个外设的驱动, 从硬盘驱动到键盘和磁带驱动器. 内核功能的这个 方面是本书中的我们主要感兴趣的地方.

网络

网络必须由操作系统来管理, 因为大部分网络操作不是特定于某一个进程: 进入系统的报文是异步事件. 报文在某一个进程接手之前必须被收集, 识 别, 分发. 系统负责在程序和网络接口之间递送数据报文, 它必须根据程 序的网络活动来控制程序的执行. 另外, 所有的路由和地址解析问题都在 内核中实现.

1.2.1.  可加载模块

Linux 的众多优良特性之一就是可以在运行时扩展由内核提供的特性的能力. 这 意味着你可以在系统正在运行着的时候增加内核的功能( 也可以去除 ). 

每块可以在运行时添加到内核的代码, 被称为一个模块. Linux 内核提供了对许 多模块类型的支持, 包括但不限于, 设备驱动. 每个模块由目标代码组成( 没有 连接成一个完整可执行文件 ), 可以动态连接到运行中的内核中, 通过 insmod 程序, 以及通过rmmod 程序去连接.

内核的划分(见 [标题编号.]) 表示了负责特定任务的不同类别的模块, 一个 模块是根据它提供的功能来说它属于一个特别类别的. 图内核的划分(见 [标题 编号.]) 中模块的安排涵盖了最重要的类别, 但是远未完整, 因为在 Linux 中 越来越多的功能被模块化了.

图  1.1.  内核的划分

 

 

2.3 1.3.  设备和模块的分类

1.3.  设备和模块的分类 

以 LInux 的方式看待设备可区分为 3 种基本设备类型. 每个模块常常实现 3 种类型中的 1种, 因此可分类成字符模块, 块模块, 或者一个网络模块. 这种 将模块分成不同类型或类别的方法并非是固定不变的; 程序员可以选择建立在一 个大块代码中实现了不同驱动的巨大模块. 但是, 好的程序员, 常常创建一个不 同的模块给每个它们实现的新功能, 因为分解是可伸缩性和可扩张性的关键因素.

3 类驱动如下:

既然不是一个面向流的设备, 一个网络接口就不象 /dev/tty1 那么容易映射到 文件系统的一个结点上. Unix 的提供对接口的存取的方式仍然是通过分配一个名子给它们( 例如 eth0 ), 但是这个名子在文件系统中没有对应的入口. 内核与网 络设备驱动间的通讯与字符和块设备驱动所用的完全不同. 不用 read 和 write, 内核调用和报文传递相关的函数.

字符设备

一个字符( char ) 设备是一种可以当作一个字节流来存取的设备( 如同一 个文件 ); 一个字符驱动负责实现这种行为. 这样的驱动常常至少实现 open, close, read, 和 write 系统调用. 文本控制台(/dev/console ) 和串口(/dev/ttyS0 及其友 )是字符设备的例子,因为它们很好地展现了 流的抽象. 字符设备通过文件系统结点来存取, 例如 /dev/tty1 和

/dev/lp0. 在一个字符设备和一个普通文件之间唯一有关的不同就是, 你 经常可以在普通文件中移来移去, 但是大部分字符设备仅仅是数据通道, 你只能顺序存取.然而, 存在看起来象数据区的字符设备, 你可以在里面 移来移去. 例如, frame grabber 经常这样, 应用程序可以使用 mmap 或 者 lseek 存取整个要求的图像.

块设备

如同字符设备, 块设备通过位于 /dev 目录的文件系统结点来存取. 一个 块设备(例如一个磁盘)应该是可以驻有一个文件系统的. 在大部分的

Unix 系统, 一个块设备只能处理这样的 I/O 操作, 传送一个或多个长度 经常是 512 字节( 或一个更大的 2 的幂的数 )的整块. Linux, 相反, 允 许应用程序读写一个块设备象一个字符设备一样 -- 它允许一次传送任意数目的字节. 结果就是, 块和字符设备的区别仅仅在内核在内部管理数据的方式上, 并且因此在内核/驱动的软件接口上不同. 如同一个字符设备, 每个块设备都通过一个文件系统结点被存取的, 它们之间的区别对用户是 透明的. 块驱动和字符驱动相比, 与内核的接口完全不同. 

网络接口

任何网络事务都通过一个接口来进行, 就是说, 一个能够与其他主机交换数据的设备. 通常, 一个接口是一个硬件设备, 但是它也可能是一个纯粹 的软件设备, 比如环回接口. 一个网络接口负责发送和接收数据报文, 在 内核网络子系统的驱动下, 不必知道单个事务是如何映射到实际的被发送的报文上的. 很多网络连接( 特别那些使用 TCP 的)是面向流的, 但是网 络设备却常常设计成处理报文的发送和接收. 一个网络驱动对单个连接一 无所知; 它只处理报文.

有其他的划分驱动模块的方式, 与上面的设备类型是正交的. 通常, 某些类型的 驱动与给定类型设备的其他层的内核支持函数一起工作. 例如, 你可以说 USB 模块, 串口模块, SCSI 模块, 等等. 每个 USB 设备由一个 USB 模块驱动, 与 USB 子系统一起工作, 但是设备自身在系统中表现为一个字符设备( 比如一个 USB 串口 ), 一个块设备( 一个 USB 内存读卡器 ), 或者一个网络设备( 一个 USB 以太网接口 ).

另外的设备驱动类别近来已经添加到内核中, 包括 FireWire 驱动和 I2O 驱动.以它们处理 USB 和 SCSI 驱动相同的方式, 内核开发者集合了类别范围内的特 性, 并把它们输出给驱动实现者, 以避免重复工作和 bug, 因此简化和加强了编 写类似驱动的过程.

在设备驱动之外, 别的功能, 不论硬件和软件, 在内核中都是模块化的. 一个普 通的例子是文件系统. 一个文件系统类型决定了在块设备上信息是如何组织的, 以便能表示一棵目录与文件的树. 这样的实体不是设备驱动, 因为没有明确的设 备与信息摆放方式相联系; 文件系统类型却是一种软件驱动, 因为它将低级数据 结构映射为高级的数据结构. 文件系统决定一个文件名多长, 以及在一个目录入 口中存储每个文件的什么信息. 文件系统模块必须实现最低级的系统调用, 来存 取目录和文件, 通过映射文件名和路径( 以及其他信息, 例如存取模式 )到保存 在数据块中的数据结构. 这样的一个接口是完全与数据被传送来去磁盘( 或其他 介质 )相互独立, 这个传送是由一个块设备驱动完成的.

如果你考虑一个 Unix 系统是多么依赖下面的文件系统, 你会认识到这样的一个 软件概念对系统操作是至关重要的. 解码文件系统信息的能力处于内核层级中最 低级, 并且是最重要的; 甚至如果你为你的新 CD-ROM 编写块驱动, 如果你对上 面的数据不能运行 ls 或者 cp 就毫无用处.Linux 支持一个文件系统模块的概 念, 其软件接口声明了不同操作, 可以在一个文件系统节点, 目录, 文件和超级 块上进行操作. 对一个程序员来说, 居然需要编写一个文件系统模块是非常不常见的, 因为官方内核已经包含了大部分重要的文件系统类型的代码.

2.4 1.4. 安全问题

1.4.  安全问题 

安全是当今重要性不断增长的关注点. 我们将讨论安全相关的问题, 在它们在本 书中出现时. 有几个通用的概念, 却值得现在提一下.

系统中任何安全检查都由内核代码强加上去. 如果内核有安全漏洞, 系统作为一个整体就有漏洞. 在官方的内核发布里, 只有一个有授权的用户可以加载模块; 系统调用 init_module 检查调用进程是否是有权加载模块到内核里. 因此, 当 运行一个官方内核时, 只有超级用户[1]或者一个成功获得特权的入侵者, 才可以 利用特权代码的能力.

在可能时, 驱动编写者应当避免将安全策略编到他们的代码中. 安全是一个策略 问题, 最好在内核高层来处理, 在系统管理员的控制下. 但是, 常有例外.

作为一个设备驱动编写者, 你应当知道在什么情形下, 某些类型的设备存取可能 反面地影响系统作为一个整体, 并且应当提供足够地控制. 例如, 会影响全局资源的设备操作( 例如设置一条中断线 ), 可能会损坏硬件( 例如, 加载固件 ), 或者它可能会影响其他用户( 例如设置一个磁带驱动的缺省的块大小 ), 常常是 只对有足够授权的用户, 并且这种检查必须由驱动自身进行.

驱动编写者也必须要小心, 当然, 来避免引入安全 bug. C编程语言使得易于犯 下几类的错误. 例如, 许多现今的安全问题是由于缓冲区覆盖引起, 它是由于程序员忘记检查有多少数据写入缓冲区, 数据在缓冲区结尾之外结束, 因此覆盖了 无关的数据. 这样的错误可能会危及整个系统的安全, 必须避免. 幸运的是, 在 设备驱动上下文中避免这样的错误经常是相对容易的, 这里对用户的接口经过精 细定义并被高度地控制. 

一些其他的通用的安全观念也值得牢记. 任何从用户进程接收的输入应当以极大 的怀疑态度来对待; 除非你能核实它, 否则不要信任它. 小心对待未初始化的内存; 从内核获取的任何内存应当清零或者在其对用户进程或设备可用之前进行初 始化. 否则, 可能发生信息泄漏( 数据, 密码的暴露等等 ). 如果你的设备解析 发送给它的数据, 要确保用户不能发送任何能危及系统的东西. 最后, 考虑一下 设备操作的可能后果; 如果有特定的操作( 例如, 加载一个适配卡的固件或者格 式化一个磁盘 ), 能影响到系统的, 这些操作应该完全确定地要限制在授权的用 户中.

也要小心, 当从第三方接收软件时, 特别是与内核有关: 因为每个人都可以接触 到源码, 每个人都可以分拆和重组东西. 尽管你能够信任在你的发布中的预编译 的内核, 你应当避免运行一个由不能信任的朋友编译的内核 -- 如果你不能作为 root 运行预编译的二进制文件, 那么你最好不要运行一个预编译的内核. 例如, 一个经过了恶意修改的内核可能会允许任何人加载模块, 这样就通过 init_module 开启了一个不想要的后门.

 注意, Linux 内核可以编译成不支持任何属于模块的东西, 因此关闭了任何模块 相关的安全漏洞. 在这种情况下, 当然, 所有需要的驱动必须直接建立到内核自 身内部. 在 2.2 和以后的内核, 也可以在系统启动之后, 通过capability 机 制来禁止内核模块的加载.

[1] 从技术上讲, 只有具有 CAP_SYS_MODULE 权利的人才可以进行这个操作. 我们 第 6 章讨论 capabilities .

2.5 1.5. 版本编号

1.5.  版本编号

在深入编程之前, 我们应当对 Linux 使用的版本编号方法和本书涉及的版本做 些说明.

首先, 注意的是在 Linux 系统中使用的每一个软件包有自己的发行版本号, 它 们之间存在相互依赖性: 你需要一个包的特别的版本来运行另外一个包的特别版本. Linux 发布的创建者常常要处理匹配软件包的繁琐问题, 这样用户从一个已 打包好的发布中安装就不需要处理版本号的问题了. 另外, 那些替换和更新系统 软件的人, 就要自己处理这个问题了. 幸运的是, 几乎所有的现代发布支持单个 软件包的更新, 通过检查软件包之间的依赖性; 发布的软件包管理器通常不允许 更新, 直到满足了依赖性.

为了运行我们在讨论过程中介绍的例子, 你除了 2.6 内核要求的之外不需要任 何工具的特别版本; 任何近期的 Linux 发布都可以用来运行我们的例子. 我们 不详述特别的要求, 因为你内核源码中的文件 Document/Changes 是这种信息的 最好的来源, 如果你遇到任何问题.

至于说内核, 偶数的内核版本( 就是说, 2.6.x )是稳定的, 用来做通用的发布. 奇数版本( 例如 2.7.x ), 相反, 是开发快照并且是非常短暂的; 它们的最新版 本代表了开发的当前状态, 但是会在几天内就过时了. 

本书涵盖内核 2.6 版本. 我们的目标是为设备驱动编写者展示 2.6.10 内核的 所有可用的特性, 这是我们在编写本书时的内核版本. 本书的这一版不涉及内核 的其他版本. 你们有人感兴趣的话, 本书第 2 版详细涵盖 2.0 到 2.4 版本. 那个版本依然在 htt://lwn.net/Kernel/LDD2 在线获取到.

内核程序员应当明白到 2.6 内核的开发过程的改变. 2.6 系列现在接受之前可能 认为对一个"稳定"的内核太大的更改. 在其他的方面, 这意味着内核内部编程接 口可能改变, 因此潜在地会使本书某些部分过时; 基于这个原因, 伴随着文本的 例子代码已知可以在 2.6.10 上运行, 但是某些模块没有在之前的版本上编译. 想紧跟内核编程变化的程序员最好加入邮件列表, 并且利用列在参考书目中的网 站. 也有一个网页在http://lwn.net/Articls/2.6-kernel-api 上维护, 它包 含自本书出版以来的 API 改变的信息.

本文不特别地谈论奇数内核版本. 普通用户不会有理由运行开发中的内核. 试验 新特性的开发者, 但是, 想运行最新的开发版本. 他们常常不停更新到最新的版 本, 来收集 bug 的修正和新的特性实现. 但是注意, 试验中的内核没有任何保

障[2], 如果你由于一个非当前的奇数版本内核的一个 bug 而引起的问题, 没人可 以帮你. 那些运行奇数版本内核的人常常是足够熟练的深入到代码中, 不需要一 本教科书, 这也是我们为什么不谈论开发中的内核的另一个原因.

Linux 的另一个特性是它是平台独立的操作系统, 并非仅仅是" PC 克隆体的一种 Unix 克隆 ", 更多的: 它当前支持大约 20 种体系. 本书是尽可能地平台独立, 所有的代码例子至少是在 x86 和 x86-64 平台上测试过.因为代码已经在

32-bit 和 64-bit 处理器上测试过, 它应当能够在所有其他平台上编译和运行.

如同你可能期望地, 依赖特殊硬件的代码例子不会在所有支持的平台上运行, 但 是这个通常在源码里说明了.

 [2] 注意, 对于偶数版本的内核也不存在保证, 除非你依靠一个同意提供它自己的 担保的商业供应商.

 

2.6 1.6.  版权条款

 1.6.  版权条款

 Linux 是以 GNU 通用公共版权( GPL )的版本 2 作为许可的, 它来自自由软件基 金的 GNU 项目. GPL 允许任何人重发布, 甚至是销售, GPL 涵盖的产品, 只要接 收方对源码能存取并且能够行使同样的权力. 另外, 任何源自使用 GPL 产品的 软件产品, 如果它是完全的重新发布, 必须置于 GPL 之下发行.

 这样一个许可的主要目的是允许知识的增长, 通过同意每个人去任意修改程序; 同时, 销售软件给公众的人仍然可以做他们的工作. 尽管这是一个简单的目标, 关于 GPL 和它的使用存在着从未结束的讨论. 如果你想阅读这个许可证, 你能 够在你的系统中几个地方发现它, 包括你的内核源码树的目录中的 COPYING 文 件.

 供应商常常询问他们是否可以只发布二进制形式的内核模块. 对这个问题的答案 已是有意让它模糊不清. 二进制模块的发布 -- 只要它们依附预已公布的内核接 口 -- 至今已是被接受了. 但是内核的版权由许多开发者持有, 并且他们不是全 都同意内核模块不是衍生产品. 如果你或者你的雇主想在非自由的许可下发布内 核模块, 你真正需要的是和你的法律顾问讨论. 请注意内核开发者不会对于在内 核发行之间破坏二进制模块有任何疑虑, 甚至在一个稳定的内核系列之间. 如果 它根本上是可能的, 你和你的用户最好以自由软件的方式发行你的模块.

如果你想你的代码进入主流内核, 或者如果你的代码需要对内核的补丁, 你在发 行代码时, 必须立刻使用一个 GPL 兼容的许可.尽管个人使用你的改变不需要 强加 GPL, 如果你发布你的代码, 你必须包含你的代码到发布里面 -- 要求你的

软件包的人必须被允许任意重建二进制的内容.

 至于本书, 大部分的代码是可自由地重新发布, 要么是源码形式, 要么是二进制 形式,我们和 O' Reilly都不保留任何权利对任何的衍生的工作. 所有的程序都 可从ftp://ftp.ora.com/pub/examples/linux/drivers/ 得到, 详尽的版权条款在相同目录中的 LICENSE 文件里阐述.

 2.7 1.7. 加入内核开发社团

 1.7.  加入内核开发社团

 在你开始为 Linux 内核编写模块时, 你就成为一个开发者大社团的一部分. 在 这个社团中, 你不仅会发现有人忙碌于类似工作, 还有一群特别投入的工程师努 力使 Linux 成为更好的系统. 这些人可以是帮助, 理念, 以及关键的审查的来源, 以及他们将是你愿意求助的第一类人, 当你在寻找一个新驱动的测试者.

 对于 Linux 内核开发者, 中心的汇聚点是 Linux 内核邮件列表. 所有主要的内 核开发者, 从Linus Torvalds 到其他人, 都订阅这个列表. 请注意这个列表不适合心力衰弱的人: 每天或者几天内的书写流量可能多至 200 条消息. 但是, 随这个列表之后的是对那些感兴趣于内核开发的人重要的东西; 它也是一个最高 品质的资源, 对那些需要内核开发帮助的人.

 为加入 Linux 内核列表, 遵照在 Linux 内核邮件列表FAQ:http://www.tux.org/lkml 中的指示. 阅读这个 FAQ 的剩余部分, 当你熟悉它时; 那里有大量的有用的信息. Linux 内核开发者都是忙碌的人, 他们更多地愿 意帮助那些已经清楚地首先完成了属于自己的那部分工作的人.

 2.8 1.8. 本书的内容 

1.8.  本书的内容

从这里开始, 我们进入内核编程的世界. 第 2 章介绍了模块化, 解释了内部的秘密以及展示了运行模块的代码. 第 2 章谈论字符驱动以及展示一个基于内存 的设备驱动的代码, 出于乐趣对它读写. 使用内存作为设备的硬件基础使得任何 人可以不用要求特殊的硬件来运行代码. 

调试技术对程序员是必备的工具, 第 4 章介绍它. 对那些想分析当前内核的人 同样重要的是并发的管理和竞争情况. 第 5 章关注的是由于并发存取资源而导致的问题, 并且介绍控制并发的 Linux 机制.

在具备了调试和并发管理的能力下, 我们转向字符驱动的高级特性, 例如阻塞操 作, selet 的使用,以及重要的 ioctl 调用; 这是第 6 章的主题.

在处理硬件管理之前, 我们研究多一点内核软件接口: 第 7 章展示了内核中是 如何管理时间的, 第 8 章讲解了内存分配.

 接下来我们集中到硬件. 第 9 章描述了 I/O 口的管理和设备上的内存缓存; 随 后是中断处理, 在 第 10 章. 不幸的是, 不是每个人都能运行这些章节中的例 子代码, 因为确实需要某些硬件来测试软件接口中断. 我们尽力保持需要的硬件 支持到最小程度, 但是你仍然需要某些硬件, 例如标准并口, 来使用这些章节的 例子代码.

 第 11 章涉及内核数据类型的使用, 以及编写可移植代码.

 本书的第 2 半专注于更高级的主题. 我们从深入硬件内部开始, 特别的, 是特 殊外设总线功能. 第 12 章涉及编写 PCI 设备驱动, 第 13 章检验使用 USB 设备的 API.

 具有了对外设总线的理解, 我们详细看一下 Linux 设备模型, 这是内核使用的 抽象层来描述它管理的硬件和软件资源. 第 14 章是一个自底向上的设备模型框 架的考察, 从 kobject 类型开始以及从那里进一步进行. 它涉及设备模型与真 实设备的集成; 接下来是利用这些知识来接触如热插拔设备和电源管理等主题.

 在第 15 章, 我们转移到 Linux 的内存管理. 这一章显示如何映射系统内存到 用户空间( mmap 系统调用 ), 映射用户内存到内核空间( 使用

get_user_pages ), 以及如何映射任何一种内存到设备空间( 进行 直接内存存取

[DMA] 操作 ).

 我们对内存的理解将对下面两章是有用的, 它们涉及到其他主要的驱动类型. 第

16 章介绍了块驱动, 并展示了与我们到现在为止已遇到过的字符驱动的区别.

第 17 章进入网络驱动的编写. 我们最后是讨论串行驱动(第 18 章)和一个参考 书目.


第  2  章  建立和运行模块

 目录

 2.1.  设置你的测试系统(见 [标题编号.])

2.2. Hello World  模块(见 [标题编号.])

2.3.  内核模块相比于应用程序(见 [标题编号.])

2.3.1.  用户空间和内核空间(见 [标题编号.])

2.3.2. 内核的并发(见 [标题编号.])

2.3.3.  当前进程(见 [标题编号.])

2.3.4.  几个别的细节(见 [标题编号.])

2.4.  编译和加载(见 [标题编号.])

2.4.1.  编译模块(见 [标题编号.])

2.4.2.  加载和卸载模块(见 [标题编号.])

2.4.3.  版本依赖(见 [标题编号.])

2.4.4.  平台依赖性(见 [标题编号.])

2.5.  内核符号表(见 [标题编号.])

2.6.  预备知识(见 [标题编号.])

2.7.  初始化和关停(见 [标题编号.])

2.7.1.  清理函数(见 [标题编号.])

2.7.2.  初始化中的错误处理(见 [标题编号.])

2.7.3.  模块加载竞争(见 [标题编号.])

2.8.  模块参数(见 [标题编号.])

2.9.  在用户空间做(见 [标题编号.])

2.10.  快速参考(见 [标题编号.])

 

时间差不多该开始编程了. 本章介绍所有的关于模块和内核编程的关键概念. 在 这几页里, 我们建立并运行一个完整(但是相对地没有什么用处)的模块, 并且查 看一些被所有模块共用的基本代码. 开发这样的专门技术对任何类型的模块化的 驱动都是重要的基础. 为避免一次抛出太多的概念, 本章只论及模块, 不涉及任 何特别的设备类型.

 在这里介绍的所有的内核项 ( 函数, 变量, 头文件, 和宏 )在本章的结尾的参 考一节里有说明.

 2.1.  设置你的测试系统

 在本章开始, 我们提供例子模块来演示编程概念. ( 所有的例子都可从 O' Reilly' s 的 FTP 网站上得到, 如第 1 章解释的那样 )建立, 加载, 和修改这 些例子, 是提高你对驱动如何工作以及如何与内核交互的理解的好方法.

 例子模块应该可以在大部分的 2.6.x 内核上运行, 包括那些由发布供应商提供 的. 但是, 我们建议你获得一个主流内核, 直接从 kernel.org 的镜像网络, 并 把它安装到你的系统中. 供应商的内核可能是主流内核被重重地打了补丁并且和 主流内核有分歧; 偶尔, 供应商的补丁可能改变了设备驱动可见的内核 API. 如 果你在编写一个必须在特别的发布上运行的驱动, 你当然要在相应的内核上建立 和测试. 但是, 处于学习驱动编写的目的, 一个标准内核是最好的.

 不管你的内核来源, 建立 2.6.x 的模块需要你有一个配置好并建立好的内核树 在你的系统中. 这个要求是从之前内核版本的改变, 之前只要有一套当前版本的 头文件就足够了. 2.6 模块针对内核源码树里找到的目标文件连接; 结果是一个 更加健壮的模块加载器, 还要求那些目标文件也是可用的. 因此你的第一个商业

订单是具备一个内核源码树( 或者从 krenel.org 网络或者你的发布者的内核源 码包), 建立一个新内核, 并且安装到你的系统. 因为我们稍后会见到的原因, 生活通常是最容易的如果当你建立模块时真正运行目标内核, 尽管这不是需要的.

 注意

你应当也考虑一下在哪里进行你的模块试验, 开发和测试. 我们已经尽力使 我们的例子模块安全和正确, 但是 bug 的可能性是经常会有的.内核代码 中的错误可能会引起一个用户进程的死亡, 或者偶尔, 瘫痪整个系统. 它们 正常地不会导致更严重地后果, 例如磁盘损伤. 然而, 还是建议你进行你的 内核试验在一个没有包含你负担不起丢失的数据的系统, 并且没有进行重要 的服务. 内核开发者典型地会保留一台"牺牲"系统来测试新的代码.

 因此, 如果你还没有一个合适的系统, 带有一个配置好并建立好的源码树在磁盘上, 现在是时候建立了. 我们将等待. 一旦这个任务完成, 你就准备好开始摆布内核模块了.

 

3.1 2.1.  设置你的测试系统

3.2 2.2. HelloWorld  模块

 2.2.  Hello World 模块

 许多编程书籍从一个 "hello world" 例子开始,作为一个展示可能的最简单的程 序的方法. 本书涉及的是内核模块而不是程序; 因此, 对无耐心的读者, 下面的 代码是一个完整的 "hello world"模块:

 #include <linux/init.h>

#include <linux/module.h> MODULE_LICENSE("Dual BSD/GPL"); 

static int hello_init(void)

{

printk(KERN_ALERT"Hello, world\n");

return 0;

}

static voidhello_exit(void)

{

printk(KERN_ALERT"Goodbye, cruel world\n");

}

 module_init(hello_init);

module_exit(hello_exit);

这个模块定义了两个函数, 一个在模块加载到内核时被调用( hello_init )以及 一个在模块被去除时被调用( hello_exit ). moudle_init 和module_exit 这几 行使用了特别的内核宏来指出这两个函数的角色. 另一个特别的宏 (MODULE_LICENSE) 是用来告知内核, 该模块带有一个自由的许可证; 没有这样 的说明, 在模块加载时内核会抱怨. 

printk 函数在 Linux 内核中定义并且对模块可用; 它与标准 C 库函数 printf 的行为相似. 内核需要它自己的打印函数, 因为它靠自己运行, 没有 C 库的帮助. 模块能够调用 printk 是因为, 在 insmod 加载了它之后, 模块被连接到内核并且可存取内核的公用符号 (函数和变量, 下一节详述). 字串 KERN_ALERT 是消息的优先级. [3]

我们在此模块中指定了一个高优先级, 因为使用缺省优先级的消息可能不会在任 何有用的地方显示, 这依赖于你运行的内核版本, klogd 守护进程的版本, 以及 你的配置. 现在你可以忽略这个因素; 我们在第 4 章讲解它.

你可以用 insmod 和 rmmod 工具来测试这个模块. 注意只有超级用户可以加载 和卸载模块.

% make

make[1]: Entering directory `/usr/src/linux-2.6.10' CC [M] /home/ldd3/src/misc-modules/hello.o

Building modules, stage 2. MODPOST

CC /home/ldd3/src/misc-modules/hello.mod.oLD [M] /home/ldd3/src/misc-modules/hello.komake[1]: Leaving directory `/usr/src/linux-2.6.10'

% su

root# insmod./hello.ko

Hello, world

root# rmmodhello

Goodbye cruel world root#

请再一次注意, 为使上面的操作命令顺序工作, 你必须在某个地方有正确配置和 建立的内核树, 在那里可以找到 makefile (/usr/src/linux-2.6.10, 在展示的例子里面 ). 我们在 "编译和加载" 这一节深入模块建立的细节.

依据你的系统用来递交消息行的机制, 你的输出可能不同. 特别地, 前面的屏幕 输出是来自一个字符控制台; 如果你从一个终端模拟器或者在窗口系统中运

insmod 和 rmmod, 你不会在你的屏幕上看到任何东西. 消息进入了其中一个系 统日志文件中, 例如 /var/log/messages (实际文件名子随 Linux 发布而变化). 内核递交消息的机制在第 4 章描述.

如你能见到的, 编写一个模块不是如你想象的困难 -- 至少, 在模块没有要求做 任何有用的事情时. 困难的部分是理解你的设备, 以及如何获得最高性能. 通过 本章我们深入模块化内部并且将设备相关的问题留到后续章节.

 [3] 优先级只是一个字串, 例如 <1>, 前缀于 printk 格式串之前. 注意在 KERN_ALERT 之后缺少一个逗号; 添加一个逗号在那里是一个普通的讨厌的错误 ( 幸运的是, 编译器会捕捉到 ).

 3.3 2.3. 内核模块相比于应用程序

 2.3.  内核模块相比于应用程序

 在我们深入之前, 有必要强调一下内核模块和应用程序之间的各种不同.

 不同于大部分的小的和中型的应用程序从头至尾处理一个单个任务, 每个内核模 块只注册自己以便来服务将来的请求, 并且它的初始化函数立刻终止. 换句话说, 模块初始化函数的任务是为以后调用模块的函数做准备; 好像是模块说, " 我在 这里, 这是我能做的."模块的退出函数( 例子里是 hello_exit )就在模块被卸载 时调用. 它好像告诉内核, "我不再在那里了, 不要要求我做任何事了."这种编程 的方法类似于事件驱动的编程, 但是虽然不是所有的应用程序都是事件驱动的, 每个内核模块都是. 另外一个主要的不同, 在事件驱动的应用程序和内核代码之 间, 是退出函数: 一个终止的应用程序可以在释放资源方面懒惰, 或者完全不做 清理工作, 但是模块的退出函数必须小心恢复每个由初始化函数建立的东西, 否 则会保留一些东西直到系统重启.

 偶然地, 卸载模块的能力是你将最欣赏的模块化的其中一个特色, 因为它有助于 减少开发时间; 你可测试你的新驱动的连续的版本, 而不用每次经历漫长的关机

/重启周期.

 作为一个程序员, 你知道一个应用程序可以调用它没有定义的函数: 连接阶段使 用合适的函数库解决了外部引用. printf 是一个这种可调用的函数并且在 libc 里面定义. 一个模块, 在另一方面, 只连接到内核, 它能够调用的唯一的函数是 内核输出的那些; 没有库来连接.在 hello.c 中使用的printk 函数, 例如, 是 在内核中定义的 printf 版本并且输出给模块. 它表现类似于原始的函数, 只有 几个小的不同, 首要的一个是缺乏浮点的支持.

图 连接一个模块到内核(见 [标题编号.]) 展示了函数调用和函数指针在模块中 如何使用来增加新功能到一个运行中的内核. 

图  2.1.  连接一个模块到内核


因为没有库连接到模块中, 源文件不应当包含通常的头文件, <stdarg.h>和非常特殊的情况是仅有的例外. 只有实际上是内核的一部分的函数才可以在内核模块里使用. 内核相关的任何东西都在头文件里声明, 这些头文件在你已建立和配置 的内核源码树里; 大部分相关的头文件位于 include/linux 和 include/asm, 但是别的 include 的子目录已经添加到关联特定内核子系统的材料里了.

单个内核头文件的作用在书中需要它们的时候进行介绍.

另外一个在内核编程和应用程序编程之间的重要不同是每一个环境是如何处理错 误: 在应用程序开发中段错误是无害的, 一个调试器常常用来追踪错误到源码中 的问题, 而一个内核错误至少会杀掉当前进程, 如果不终止整个系统. 我们会在 第 4 章看到如何跟踪内核错误.

 2.3.1.  用户空间和内核空间

A module runs in kernel space, whereas applications run in user space. This concept is at the base of operating systems theory. 一个模块在内核空间运行, 而应用程序在用户空间运行. 这个概 念是操作系统理论的基础.

 操作系统的角色, 实际上, 是给程序提供一个一致的计算机硬件的视角. 另外, 操作系统必须承担程序的独立操作和保护对于非授权的资源存取. 这一不平凡的 任务只有 CPU 增强系统软件对应用程序的保护才有可能.

每种现代处理器都能够加强这种行为. 选中的方法是 CPU 自己实现不同的操作 形态(或者级别). 这些级别有不同的角色, 一些操作在低些级别中不允许; 程序 代码只能通过有限的几个门从一种级别切换到另一个. Unix 系统设计成利用了这

种硬件特性, 使用了两个这样的级别. 所有当今的处理器至少有两个保护级别, 并且某些, 例如 x86 家族, 有更多级别; 当几个级别存在时, 使用最高和最低级别. 在 Unix 下, 内核在最高级运行( 也称之为超级模式 ), 这里任何事情都 允许, 而应用程序在最低级运行(所谓的用户模式), 这里处理器控制了对硬件的 直接存取以及对内存的非法存取.

 我们常常提到运行模式作为内核空间和用户空间. 这些术语不仅包含存在于这两 个模式中不同特权级别, 还包含有这样的事实, 即每个模式有它自己的内存映射

-- 它自己的地址空间.

 Unix 从用户空间转换执行到内核空间, 无论何时一个应用程序发出一个系统调用或者被硬件中断挂起时. 执行系统调用的内核代码在进程的上下文中工作 -- 它代表调用进程并且可以存取该进程的地址空间. 换句话说, 处理中断的代码对 进程来说是异步的, 不和任何特别的进程有关.

 模块的角色是扩展内核的功能; 模块化的代码在内核空间运行. 经常地一个驱动 进行之前提到的两种任务: 模块中一些的函数作为系统调用的一部分执行, 一些 负责中断处理.

 2.3.2.  内核的并发

 内核编程与传统应用程序编程方式很大不同的是并发问题. 大部分应用程序, 多 线程的应用程序是一个明显的例外, 典型地是顺序运行的, 从头至尾, 不必要担 心其他事情会发生而改变它们的环境. 内核代码没有运行在这样的简单世界中, 即便最简单的内核模块必须在这样的概念下编写, 很多事情可能马上发生.

 内核编程中有几个并发的来源. 自然的, Linux系统运行多个进程, 在同一时间, 不止一个进程能够试图使用你的驱动. 大部分设备能够中断处理器; 中断处理异 步运行, 并且可能在你的驱动试图做其他事情的同一时间被调用. 几个软件抽象

( 例如内核定时器, 第 7 章介绍 )也异步运行. 而且, 当然, Linux 可以在对称 多处理器系统( SMP )上运行, 结果是你的驱动可能在多个 CPU 上并发执行. 最 后, 在 2.6, 内核代码已经是可抢占的了; 这个变化使得即便是单处理器会有许

多与多处理器系统同样的并发问题.

 结果, Linux 内核代码, 包括驱动代码, 必须是可重入的 --它必须能够同时在 多个上下文中运行. 数据结构必须小心设计以保持多个执行线程分开, 并且代码 必须小心存取共享数据, 避免数据的破坏. 编写处理并发和避免竞争情况( 一个 不幸的执行顺序导致不希望的行为的情形 )的代码需要仔细考虑并可能是微妙的. 正确的并发管理在编写正确的内核代码时是必须的; 由于这个理由, 本书的每一 个例子驱动都是考虑了并发下编写的. 用到的技术在我们遇到它们时再讲解; 第

5 章也专门讲述这个问题, 以及并发管理的可用的内核原语.

驱动程序员的一个通常的错误是假定并发不是一个问题, 只要一段特别的代码没 有进入睡眠( 或者 "阻塞" ). 即便在之前的内核( 不可抢占), 这种假设在多处 理器系统中也不成立. 在 2.6, 内核代码不能(极少)假定它能在一段给定代码上

持有处理器. 如果你不考虑并发来编写你的代码, 就极有可能导致严重失效, 以 至于非常难于调试.

 2.3.3.  当前进程

 尽管内核模块不象应用程序一样顺序执行, 内核做的大部分动作是代表一个特定进程的. 内核代码可以引用当前进程, 通过存取全局项 current, 它在

<asm/current.h>中定义, 它产生一个指针指向结构 task_struct, 在

<linux/sched.h> 定义. current 指针指向当前在运行的进程. 在一个系统调用 执行期间, 例如 open 或者 read, 当前进程是发出调用的进程. 内核代码可以

通过使用 current 来使用进程特定的信息, 如果它需要这样. 这种技术的一个 例子在第 6 章展示.

 实际上, current 不真正地是一个全局变量. 支持 SMP 系统的需要强迫内核开发 者去开发一种机制, 在相关的 CPU 上来找到当前进程. 这种机制也必须快速, 因为对 current 的引用非常频繁地发生. 结果就是一个依赖体系的机制, 常常, 隐藏了一个指向task_struct 的指针在内核堆栈内. 实现的细节对别的内核子 系统保持隐藏, 一个设备驱动可以只包含 <linux/sched.h> 并且引用当前进程. 例如, 下面的语句打印了当前进程的进程 ID 和命令名称, 通过存取结构 task_struct 中的某些字段.

printk(KERN_INFO "The process is \"%s\" (pid %i)\n",current->comm, current->pid);

 存于 current->comm 的命令名称是由当前进程执行的程序文件的基本名称( 截 短到 15 个字符, 如果需要 ).

 2.3.4.  几个别的细节

 内核编程与用户空间编程在许多方面不同. 我们将在本书的过程中指出它们, 但 是有几个基础性的问题, 尽管没有保证它们自己有一节内容, 也值得一提. 因此, 当你深入内核时, 下面的事项应当牢记.

应用程序存在于虚拟内存中, 有一个非常大的堆栈区. 堆栈, 当然, 是用来保存函数调用历史以及所有的由当前活跃的函数创建的自动变量. 内核, 相反, 有一 个非常小的堆栈; 它可能小到一个, 4096 字节的页. 你的函数必须与这个内核空 间调用链共享这个堆栈. 因此, 声明一个巨大的自动变量从来就不是一个好主意; 如果你需要大的结构, 你应当在调用时间内动态分配.

常常, 当你查看内核 API 时, 你会遇到以双下划线(  )开始的函数名. 这样标 志的函数名通常是一个低层的接口组件, 应当小心使用. 本质上讲, 双下划线告 诉程序员:"如果你调用这个函数, 确信你知道你在做什么."

内核代码不能做浮点算术. 使能浮点将要求内核在每次进出内核空间的时候保存 和恢复浮点处理器的状态 -- 至少, 在某些体系上. 在这种情况下, 内核代码真的没有必要包含浮点, 额外的负担不值得.

 3.4 2.4. 编译和加载

 2.4.  编译和加载

 本章开头的 "hello world" 例子包含了一个简短的建立并加载模块到系统中去的演示. 当然, 整个过程比我们目前看到的多. 本节提供了更多细节关于一个模块 作者如何将源码转换成内核中的运行的子系统.

 2.4.1.  编译模块

第一步, 我们需要看一下模块如何必须被建立. 模块的建立过程与用户空间的应 用程序的建立过程有显著不同; 内核是一个大的, 独立的程序, 对于它的各个部 分如何组合在一起有详细的明确的要求. 建立过程也与以前版本的内核的过程不同; 新的建立系统用起来更简单并且产生更正确的结果, 但是它看起来与以前非 常不同. 内核建立系统是一头负责的野兽, 我们就看它一小部分. 在内核源码的 Document/kbuild 目录下发现的文件, 任何想理解表面之下的真实情况的人都要阅读一下.

 有几个前提, 你必须在能建立内核模块前解决. 第一个是保证你有版本足够新的 编译器, 模块工具, 以及其他必要工具. 在内核文档目录下的文件 Documentation/Changes 一直列出了需要的工具版本; 你应当在向前走之前参考一下它. 试图建立一个内核(包括它的模块), 用错误的工具版本, 可能导致不尽的奇怪的难题. 注意, 偶尔地, 编译器的版本太新可能会引起和太老的版本引起 的一样的问题. 内核源码对于编译器做了很大的假设, 新的发行版本有时会一时 地破坏东西.

 如果你仍然没有一个内核树在手边, 或者还没有配置和建立内核, 现在是时间去 做了. 没有源码树在你的文件系统上, 你无法为 2.6 内核建立可加载的模块. 实际运行为其而建立的内核也是有帮助的( 尽管不是必要的 ).

 一旦你已建立起所有东西, 给你的模块创建一个 makefile 就是直截了当的. 实 际上, 对于本章前面展示的" hello world" 例子, 单行就够了:

obj-m := hello.o

 熟悉 make , 但是对 2.6 内核建立系统不熟悉的读者, 可能奇怪这个 makefile 如何工作. 毕竟上面的这一行不是一个传统的 makefile 的样子. 答案, 当然, 是内核建立系统处理了余下的工作. 上面的安排( 它利用了由 GNU make 提供的 扩展语法 )表明有一个模块要从目标文件 hello.o 建立. 在从目标文件建立后 结果模块命名为 hello.ko.

 反之, 如果你有一个模块名为 module.ko, 是来自 2 个源文件( 姑且称之为, file1.c 和 file2.c ), 正确的书写应当是:

 obj-m := module.o

module-objs :=file1.o file2.o

 对于一个象上面展示的要工作的 makefile, 它必须在更大的内核建立系统的上 下文被调用. 如果你的内核源码数位于, 假设, 你的 ~/kernel-2.6 目录,用来 建立你的模块的 make 命令( 在包含模块源码和 makefile 的目录下键入 )会 是:

 make -C ~/kernel-2.6M=`pwd` modules

这个命令开始是改变它的目录到用 -C 选项提供的目录下( 就是说, 你的内核源码目录 ). 它在那里会发现内核的顶层 makefile. 这个 M= 选项使 makefile 在试图建立模块目标前, 回到你的模块源码目录. 这个目标, 依次地, 是指在 obj-m 变量中发现的模块列表, 在我们的例子里设成了 module.o.

 键入前面的 make 命令一会儿之后就会感觉烦, 所以内核开发者就开发了一种 makefile 方式, 使得生活容易些对于那些在内核树之外建立模块的人. 这个窍 门是如下书写你的 makefile:

# If KERNELRELEASE isdefined, we've been invoked from the

# kernel build system and can use its language. ifneq ($(KERNELRELEASE),)

 obj-m := hello.o

# Otherwise wewere called directly from the command

# line; invoke the kernel build system. else

 KERNELDIR ?=/lib/modules/$(shell uname -r)/build

PWD := $(shellpwd)

default:

$(MAKE) -C$(KERNELDIR) M=$(PWD) modules

 endif

 再一次, 我们看到了扩展的 GNU make 语法在起作用. 这个 makefile 在一次典 型的建立中要被读 2 次. 当从命令行中调用这个makefile , 它注意到 KERNELRELEASE 变量没有设置. 它利用这样一个事实来定位内核源码目录, 即已 安装模块目录中的符号连接指回内核建立树. 如果你实际上没有运行你在为其而 建立的内核, 你可以在命令行提供一个 KERNELDIR= 选项, 设置 KERNELDIR 环 境变量,或者重写 makefile 中设置 KERNELDIR 的那一行. 一旦发现内核源码 树, makefile 调用 default: 目标, 来运行第 2个 make 命令( 在 makefile 里参数化成 $(MAKE))象前面描述过的一样来调用内核建立系统.在第 2 次读, makefile 设置 obj-m, 并且内核的 makefile 文件完成实际的建立模块工作.

 这种建立模块的机制你可能感觉笨拙模糊. 一旦你习惯了它, 但是, 你很可能会 欣赏这种已经编排进内核建立系统的能力. 注意, 上面的不是一个完整的 makefile; 一个真正的 makefile 包含通常的目标类型来清除不要的文件, 安装 模块等等. 一个完整的例子可以参考例子代码目录的 makefile.

 2.4.2.  加载和卸载模块

 模块建立之后, 下一步是加载到内核. 如我们已指出的,insmod 为你完成这个工 作. 这个程序加载模块的代码段和数据段到内核, 接着, 执行一个类似 ld 的函 数, 它连接模块中任何未解决的符号连接到内核的符号表上. 但是不象连接器, 内核不修改模块的磁盘文件, 而是内存内的拷贝. insmod 接收许多命令行选项 (详情见 manpage), 它能够安排值给你模块中的参数, 在连接到当前内核之前. 因此, 如果一个模块正确设计了, 它能够在加载时配置; 加载时配置比编译时配 置给了用户更多的灵活性, 有时仍然在用. 加载时配置在本章后面的 "模块参数 " 一节讲解.

 感兴趣的读者可能想看看内核如何支持 insmod: 它依赖一个在 kernel/module.c 中定义的系统调用. 函数 sys_init_module 分配内核内存来 存放模块 ( 这个内存用 vmalloc 分配; 看第 8 章的 "vmalloc 和其友" ); 它 接着拷贝模块的代码段到这块内存区, 借助内核符号表解决模块中的内核引用, 并且调用模块的初始化函数来启动所有东西.

 如果你真正看了内核代码, 你会发现系统调用的名子以 sys_ 为前缀. 这对所有 系统调用都是成立的, 并且没有别的函数. 记住这个有助于在源码中查找系统调 用.

 modprobe 工具值得快速提及一下. modprobe, 如同 insmod, 加载一个模块到内 核. 它的不同在于它会查看要加载的模块, 看是否它引用了当前内核没有定义的 符号. 如果发现有, modprobe 在定义相关符号的当前模块搜索路径中寻找其他模块. 当modprobe 找到这些模块( 要加载模块需要的 ), 它也把它们加载到内核.

如果你在这种情况下代替以使用 insmod , 命令会失败, 在系统日志文件中留下 一条 " unresolved symbols "消息.

 如前面提到, 模块可以用 rmmod 工具从内核去除. 注意, 如果内核认为模块还 在用( 就是说, 一个程序仍然有一个打开文件对应模块输出的设备 ), 或者内核 被配置成不允许模块去除, 模块去除会失败. 可以配置内核允许"强行"去除模块, 甚至在它们看来是忙的. 如果你到了需要这选项的地步, 但是, 事情可能已经错 的太严重以至于最好的动作就是重启了.

 lsmod 程序生成一个内核中当前加载的模块的列表. 一些其他信息, 例如使用了 一个特定模块的其他模块, 也提供了.lsmod 通过读取 /proc/modules 虚拟文件 工作. 当前加载的模块的信息也可在位于 /sys/module 的 sysfs 虚拟文件系统 找到.

 2.4.3.  版本依赖

 记住, 你的模块代码一定要为每个它要连接的内核版本重新编译 -- 至少, 在缺 乏modversions 时, 这里不涉及因为它们更多的是给内核发布制作者, 而不是 开发者. 模块是紧密结合到一个特殊内核版本的数据结构和函数原型上的; 模块 见到的接口可能一个内核版本与另一个有很大差别. 当然, 在开发中的内核更加 是这样.

 内核不只是认为一个给定模块是针对一个正确的内核版本建立的. 建立过程的其 中一步是对一个当前内核树中的文件(称为 vermagic.o)连接你的模块; 这个东 东含有相当多的有关要为其建立模块的内核的信息, 包括目标内核版本, 编译器 版本, 以及许多重要配置变量的设置. 当尝试加载一个模块, 这些信息被检查与 运行内核的兼容性. 如果不匹配, 模块不会加载; 代之的是你见到如下内容:

 # insmod hello.ko

Error inserting'./hello.ko': -1 Invalid module format

看一下系统日志文件(/var/log/message 或者任何你的系统被配置来用的)将发 现导致模块无法加载特定的问题.

 如果你需要编译一个模块给一个特定的内核版本, 你将需要使用这个特定版本的 建立系统和源码树. 前面展示过的在例子 makefile 中简单修改 KERNELDIR 变 量, 就完成这个动作.

内核接口在各个发行之间常常变化. 如果你编写一个模块想用来在多个内核版本上工作(特别地是如果它必须跨大的发行版本), 你可能只能使用宏定义和

#ifdef 来使你的代码正确建立. 本书的这个版本只关心内核的一个主要版本,

因此不会在我们的例子代码中经常见到版本检查. 但是这种需要确实有时会有.

在这样情况下, 你要利用在 linux/version.h 中发现的定义. 这个头文件, 自 动包含在 linux/module.h, 定义了下面的宏定义:

 UTS_RELEASE

 这个宏定义扩展成字符串, 描述了这个内核树的版本. 例如, "2.6.10".

 LINUX_VERSION_CODE

 这个宏定义扩展成内核版本的二进制形式, 版本号发行号的每个部分用一 个字节表示. 例如, 2.6.10 的编码是 132618 ( 就是, 0x02060a ). [4]有 了这个信息, 你可以(几乎是)容易地决定你在处理的内核版本.

 KERNEL_VERSION(major,minor,release)

这个宏定义用来建立一个整型版本编码, 从组成一个版本号的单个数字. 例如,KERNEL_VERSION(2.6.10) 扩展成 132618. 这个宏定义非常有用, 当你需要比较当前版本和一个已知的检查点.

 大部分的基于内核版本的依赖性可以使用预处理器条件解决, 通过利用

KERNEL_VERSION 和 LINUX_VERSION_VODE. 版本依赖不应当, 但是, 用繁多的

#ifdef 条件来搞乱驱动的代码; 处理不兼容的最好的方式是把它们限制到特定的头文件. 作为一个通用的原则, 明显版本(或者平台)依赖的代码应当隐藏在一个低级的宏定义或者函数后面. 高层的代码就可以只调用这些函数, 而不必关心

低层的细节. 这样书写的代码易读并且更健壮.

 2.4.4.  平台依赖性

 每个电脑平台有其自己的特点, 内核设计者可以自由使用所有的特性来获得更好的性能. in the target object file ???

 不象应用程序开发者, 他们必须和预编译的库一起连接他们的代码, 依附在参数 传递的规定上, 内核开发者可以专用某些处理器寄存器给特别的用途, 他们确实 这样做了. 更多的, 内核代码可以为一个 CPU族里的特定处理器优化, 以最好地利用目标平台; 不象应用程序那样常常以二进制格式发布, 一个定制的内核编 译可以为一个特定的计算机系列优化.

 例如, IA32 (x86) 结构分为几个不同的处理器类型. 老式的 80386 处理器仍然 被支持( 到现在 ), 尽管它的指令集, 以现代的标准看,非常有限. 这个体系中 更加现代的处理器已经引入了许多新特性, 包括进入内核的快速指令, 处理器间 的加锁, 拷贝数据, 等等. 更新的处理器也可采用 36 位( 或者更大 )的物理地址, 当在适当的模式下, 以允许他们寻址超过 4 GB 的物理内存. 其他的处理器 家族也有类似的改进. 内核, 依赖不同的配置选项, 可以被建立来使用这些附加的特性.

清楚地, 如果一个模块与一个给定内核工作, 它必须以与内核相同的对目标处理 器的理解来建立. 再一次, vermagic.o 目标文件登场. 当加载一个模块, 内核为 模块检查特定处理器的配置选项, 确认它们匹配运行的内核. 如果模块用不同选

项编译, 它不会加载.

 如果你计划为通用的发布编写驱动, 你可能很奇怪你怎么可能支持所有这些不同的变体. 最好的答案, 当然, 是发行你的驱动在 GPL 兼容的许可之下, 并且贡 献它给主流内核. 如果没有那样, 以源码形式和一套脚本发布你的驱动, 以便在 用户系统上编译可能是最好的答案. 一些供应商已发行了工具来简化这个工作. 如果你必须发布你的驱动以二进制形式, 你需要查看由你的目标发布所提供的不 同的内核, 并且为每个提供一个模块版本. 要确认考虑到了任何在产生发布后可能发行的勘误内核. 接着, 要考虑许可权的问题, 如同我们在第 1 章的" 许可 条款" 一节中讨论的. 作为一个通用的规则, 以源码形式发布东西是你行于世的 易途.

[4] 这允许在稳定版本之间多达 256 个开发版本.

 3.5 2.5.  内核符号表

 2.5.  内核符号表

 我们已经看到 insmod 如何对应共用的内核符号来解决未定义的符号. 表中包含 了全局内核项的地址 -- 函数和变量 -- 需要来完成模块化的驱动. 当加载一个 模块, 如何由模块输出的符号成为内核符号表的一部分. 通常情况下, 一个模块 完成它自己的功能不需要输出如何符号. 你需要输出符号, 但是, 在任何别的模 块能得益于使用它们的时候.

新的模块可以用你的模块输出的符号, 你可以堆叠新的模块在其他模块之上. 模 块堆叠在主流内核源码中也实现了: msdos 文件系统依赖 fat 模块输出的符号, 某一个输入 USB 设备模块堆叠在usbcore 和输入模块之上.

 模块堆叠在复杂的工程中有用处. 如果一个新的抽象以驱动程序的形式实现, 它 可能提供一个特定硬件实现的插入点. 例如, video-for-linux 系列驱动分成一 个通用模块, 输出了由特定硬件的低层设备驱动使用的符号. 根据你的设置, 你 加载通用的视频模块和你的已安装硬件对应的特定模块. 对并口的支持和众多可 连接设备以同样的方式处理, 如同 USB 内核子系统. 在并口子系统的堆叠在图 并口驱动模块的堆叠(见 [标题编号.]) 中显示; 箭头显示了模块和内核编程接 口间的通讯.

 图  2.2.  并口驱动模块的堆叠

当使用堆叠的模块时, 熟悉 modprobe 工具是有帮助的. 如我们前面讲的, modprobe 函数很多地方与 insmod 相同, 但是它也加载任何你要加载的模块需

要的其他模块. 所以, 一个 modprobe 命令有时可能代替几次使用 insmod( 尽 管你从当前目录下加载你自己模块仍将需要 insmod, 因为 modprobe 只查找标 准的已安装模块目录 ).

 使用堆叠来划分模块成不同层, 这有助于通过简化每一层来缩短开发时间. 这同 我们在第 1 章讨论的区分机制和策略是类似的.

 linux 内核头文件提供了方便来管理你的符号的可见性, 因此减少了命名空间的 污染( 将与在内核别处已定义的符号冲突的名子填入命名空间), 并促使了正确 的信息隐藏. 如果你的模块需要输出符号给其他模块使用, 应当使用下面的宏定 义:

EXPORT_SYMBOL(name); EXPORT_SYMBOL_GPL(name);

 上面宏定义的任一个使得给定的符号在模块外可用. _GPL 版本的宏定义只能使符 号对 GPL 许可的模块可用. 符号必须在模块文件的全局部分输出, 在任何函数 之外, 因为宏定义扩展成一个特殊用途的并被期望是全局存取的变量的声明. 这 个变量存储于模块的一个特殊的可执行部分( 一个 "ELF 段" ), 内核用这个部分 在加载时找到模块输出的变量. ( 感兴趣的读者可以看<linux/module.h> 获知 详情, 尽管并不需要这些细节使东西动起来. )

 3.6 2.6. 预备知识

 2.6.  预备知识

我们正在接近去看一些实际的模块代码. 但是首先, 我们需要看一些需要出现在你的模块源码文件中的东西. 内核是一个独特的环境, 它将它的要求强加于要和 它接口的代码上.

大部分内核代码包含了许多数量的头文件来获得函数, 数据结构和变量的定义. 我们将在碰到它们时检查这些文件, 但是有几个文件对模块是特殊的, 必须出现 在每一个可加载模块中. 因此, 几乎所有模块代码都有下面内容:

 #include<linux/module.h>

#include<linux/init.h>

moudle.h 包含了大量加载模块需要的函数和符号的定义. 你需要 init.h 来指 定你的初始化和清理函数, 如我们在上面的 "hello world" 例子里见到的, 这个 我们在下一节中再讲. 大部分模块还包含 moudleparam.h, 使得可以在模块加载 时传递参数给模块. 我们将很快遇到.

 不是严格要求的, 但是你的模块确实应当指定它的代码使用哪个许可. 做到这一 点只需包含一行 MODULE_LICENSE:

 MODULE_LICENSE("GPL");

内核认识的特定许可有, "GPL"( 适用 GNU 通用公共许可的任何版本 ), "GPL v2"( 只适用 GPL 版本 2 ), "GPL and additionalrights", "Dual BSD/GPL", "Dual MPL/GPL", 和 "Proprietary". 除非你的模块明确标识是在内核认识的一 个自由许可下, 否则就假定它是私有的, 内核在模块加载时被"弄污浊"了. 象我 们在第 1 章"许可条款"中提到的, 内核开发者不会热心帮助在加载了私有模块 后遇到问题的用户.

 可以在模块中包含的其他描述性定义有 MODULE_AUTHOR ( 声明谁编写了模块 ),MODULE_DESCRIPION( 一个人可读的关于模块做什么的声明 ), MODULE_VERSION

( 一个代码修订版本号; 看 <linux/module.h> 的注释以便知道创建版本字串使 用的惯例),MODULE_ALIAS ( 模块为人所知的另一个名子 ), 以及 MODULE_DEVICE_TABLE ( 来告知用户空间, 模块支持那些设备 ). 我们会讨论 MODULE_ALIAS 在第 11 章以及 MUDULE_DEVICE_TABLE 在第 12 章.

 各种 MODULE_ 声明可以出现在你的源码文件的任何函数之外的地方. 但是, 一 个内核代码中相对近期的惯例是把这些声明放在文件末尾.

 3.7 2.7. 初始化和关停

 2.7.  初始化和关停

 如已提到的, 模块初始化函数注册模块提供的任何功能. 这些功能, 我们指的是 新功能, 可以由应用程序存取的或者一整个驱动或者一个新软件抽象. 实际的初 始化函数定义常常如:

static int   init initialization_function(void)

{

/* Initializationcode here */

}

module_init(initialization_function);

初始化函数应当声明成静态的, 因为它们不会在特定文件之外可见; 没有硬性规 定这个, 然而, 因为没有函数能输出给内核其他部分, 除非明确请求. 声明中的

  init 标志可能看起来有点怪; 它是一个给内核的暗示, 给定的函数只是在初 始化使用. 模块加载者在模块加载后会丢掉这个初始化函数, 使它的内存可做其

他用途. 一个类似的标签 (  initdata) 给只在初始化时用的数据. 使用

  init 和   initdata是可选的, 但是它带来的麻烦是值得的. 只是要确认不要用在那些在初始化完成后还使用的函数(或者数据结构)上. 你可能还会遇到

  devinit 和   devinitdata在内核源码里; 这些只在内核没有配置支持 hotplug 设备时转换成  init 和 _initdata. 我们会在 14 章谈论 hotplug 支持.

 使用 moudle_init 是强制的. 这个宏定义增加了特别的段到模块目标代码中, 表明在哪里找到模块的初始化函数. 没有这个定义, 你的初始化函数不会被调 用.

 模块可以注册许多的不同设施, 包括不同类型的设备, 文件系统, 加密转换, 以 及更多. 对每一个设施, 有一个特定的内核函数来完成这个注册. 传给内核注册 函数的参数常常是一些数据结构的指针, 描述新设施以及要注册的新设施的名子. 数据结构常常包含模块函数指针, 模块中的函数就是这样被调用的.

 能够注册的项目远远超出第 1 章中提到的设备类型列表. 它们包括,其他的, 串口, 多样设备, sysfs 入口, /proc 文件,执行域, 链路规程. 这些可注册项 的大部分都支持不直接和硬件相关的函数, 但是处于"软件抽象"区域里. 这些项 可以注册, 是因为它们以各种方式(例如象 /proc 文件和链路规程)集成在驱动的功能中.

 对某些驱动有其他的设施可以注册作为补充, 但它们的使用太特别, 所以不值得 讨论它们. 它们使用堆叠技术, 在"内核符号表"一节中讲过. 如果你想深入探求, 你可以在内核源码里查找 EXPORT_SYMBOL , 找到由不同驱动提供的入口点. 大部 分注册函数以 register_ 做前缀, 因此找到它们的另外一个方法是在内核源码 里查找 register_ .

 2.7.1.  清理函数

每个非试验性的模块也要求有一个清理函数, 它注销接口, 在模块被去除之前返 回所有资源给系统. 这个函数定义为:

static void   exit cleanup_function(void)

{

/* Cleanup codehere */

}

 module_exit(cleanup_function);

 清理函数没有返回值, 因此它被声明为 void.   exit 修饰符标识这个代码是只 用于模块卸载( 通过使编译器把它放在特殊的 ELF 段). 如果你的模块直接建立 在内核里, 或者如果你的内核配置成不允许模块卸载, 标识为   exit的函数被 简单地丢弃. 因为这个原因, 一个标识   exit 的函数只在模块卸载或者系统停止时调用; 任何别的使用是错的. 再一次, moudle_exit 声明对于使得内核能够 找到你的清理函数是必要的.

如果你的模块没有定义一个清理函数, 内核不会允许它被卸载.

2.7.2.  初始化中的错误处理

 你必须记住一件事, 在注册内核设施时, 注册可能失败. 即便最简单的动作常常 需要内存分配, 分配的内存可能不可用. 因此模块代码必须一直检查返回值, 并 且确认要求的操作实际上已经成功.

如果在你注册工具时发生任何错误, 首先第一的事情是决定模块是否能够无论如 何继续初始化它自己. 常常, 在一个注册失败后模块可以继续操作, 如果需要可 以功能降级. 在任何可能的时候, 你的模块应当尽力向前, 并提供事情失败后具 备的能力.

 如果证实你的模块在一个特别类型的失败后完全不能加载, 你必须取消任何在失 败前注册的动作. 内核不保留已经注册的设施的每模块注册, 因此如果初始化在 某个点失败, 模块必须能自己退回所有东西. 如果你无法注销你获取的东西, 内 核就被置于一个不稳定状态; 它包含了不存在的代码的内部指针. 这种情况下, 经常地, 唯一的方法就是重启系统. 在初始化错误发生时, 你确实要小心地将事 情做正确.

 错误恢复有时用 goto 语句处理是最好的. 我们通常不愿使用 goto, 但是在我 们的观念里, 这是一个它有用的地方. 在错误情形下小心使用 goto 可以去掉大 量的复杂, 过度对齐的, "结构形" 的逻辑. 因此, 在内核里, goto 是处理错误 经常用到, 如这里显示的.

 下面例子代码( 使用设施注册和注销函数)在初始化在任何点失败时做得正确:

 int   initmy_init_function(void)

{

int err;

/* registrationtakes 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 */

}

 这段代码试图注册 3 个(虚构的)设施. goto 语句在失败情况下使用, 在事情变 坏之前只对之前已经成功注册的设施进行注销.

另一个选项, 不需要繁多的 goto 语句, 是跟踪已经成功注册的, 并且在任何出 错情况下调用你的模块的清理函数. 清理函数只回卷那些已经成功完成的步骤. 然而这种选择, 需要更多代码和更多CPU 时间, 因此在快速途径下, 你仍然依 赖于 goto 作为最好的错误恢复工具.

 my_init_function 的返回值, err, 是一个错误码. 在 Linux 内核里, 错误码是 负数, 属于定义于<linux/errno.h> 的集合. 如果你需要产生你自己的错误码 代替你从其他函数得到的返回值, 你应当包含 <linux/errno.h> 以便使用符号 式的返回值, 例如-ENODEV, -ENOMEM, 等等. 返回适当的错误码总是一个好做法, 因为用户程序能够把它们转变为有意义的字串, 使用 perror 或者类似的方法.

 显然, 模块清理函数必须撤销任何由初始化函数进行的注册, 并且惯例(但常常 不是要求的)是按照注册时相反的顺序注销设施.

void   exit my_cleanup_function(void)

{

unregister_those(ptr3, "skull"); unregister_that(ptr2,"skull"); unregister_this(ptr1, "skull");

return ;

}

 如果你的初始化和清理比处理几项复杂, goto方法可能变得难于管理, 因为所有的清理代码必须在初始化函数里重复, 包括几个混合的标号. 有时, 因此, 一种 不同的代码排布证明更成功.

使代码重复最小和所有东西流线化, 你应当做的是无论何时发生错误都从初始化 里调用清理函数. 清理函数接着必须在撤销它的注册前检查每一项的状态. 以最 简单的形式, 代码看起来象这样:

struct something *item1; struct somethingelse *item2; int stuff_ok;

 void my_cleanup(void)

{

if (item1)

release_thing(item1);

if (item2)

release_thing2(item2);

if (stuff_ok)

unregister_stuff();

return;

}

int   init my_init(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;

el

goto fail;

return 0; /*success */

fail:

my_cleanup();

return err;

}

如这段代码所示, 你也许需要, 也许不要外部的标志来标识初始化步骤的成功,

要依赖你调用的注册/分配函数的语义. 不管要不要标志, 这种初始化会变得包 含大量的项, 常常比之前展示的技术要好. 注意, 但是, 清理函数当由非退出代码调用时不能标志为   exit, 如同前面的例子.

2.7.3.  模块加载竞争

到目前, 我们的讨论已来到一个模块加载的重要方面: 竞争情况. 如果你在如何 编写你的初始化函数上不小心, 你可能造成威胁到整个系统的稳定的情形. 我们 将在本书稍后讨论竞争情况; 现在, 快速提几点就足够了:

首先时你应该一直记住, 内核的某些别的部分会在注册完成之后马上使用任何你 注册的设施. 这是完全可能的, 换句话说, 内核将调用进你的模块, 在你的初始 化函数仍然在运行时. 所以你的代码必须准备好被调用, 一旦它完成了它的第一 个注册. 不要注册任何设施, 直到所有的需要支持那个设施的你的内部初始化已经完成.

你也必须考虑到如果你的初始化函数决定失败会发生什么, 但是内核的一部分已 经在使用你的模块已注册的设施. 如果这种情况对你的模块是可能的, 你应当认 真考虑根本不要使初始化失败. 毕竟, 模块已清楚地成功输出一些有用的东西. 如果初始化必须失败, 必须小心地处理任何可能的在内核别处发生的操作, 直到 这些操作已完成.

3.8 2.8. 模块参数

2.8.  模块参数

驱动需要知道的几个参数因不同的系统而不同. 从使用的设备号( 如我们在下一 章见到的 )到驱动应当任何操作的几个方面. 例如, SCSI 适配器的驱动常常有选项控制标记命令队列的使用, IDE 驱动允许用户控制 DMA操作. 如果你的驱动控 制老的硬件, 还需要被明确告知哪里去找硬件的 I/O 端口或者 I/O 内存地址. 内核通过在加载驱动的模块时指定可变参数的值, 支持这些要求.

这些参数的值可由 insmod 或者 modprobe 在加载时指定; 后者也可以从它的配 置文件(/etc/modprobe.conf)读取参数的值. 这些命令在命令行里接受几类规格 的值. 作为演示这种能力的一种方法, 想象一个特别需要的对本章开始的"hello world"模块(称为 hellop)的改进. 我们增加 2 个参数: 一个整型值, 称为

howmany, 一个字符串称为 whom. 我们的特别多功能的模块就在加载时, 欢迎

whom 不止一次, 而是howmany 次. 这样一个模块可以用这样的命令行加载:

insmod hellophowmany=10 whom="Mom"

一旦以那样的方式加载, hellop 会说 "hello, Mom" 10 次.

但是, 在 insmod 可以修改模块参数前, 模块必须使它们可用. 参数用

moudle_param 宏定义来声明, 它定义在 moduleparam.h. module_param 使用了

3 个参数: 变量名, 它的类型, 以及一个权限掩码用来做一个辅助的 sysfs 入 口. 这个宏定义应当放在任何函数之外, 典型地是出现在源文件的前面. 因此 hellop 将声明它的参数, 并如下使得对 insmod 可用:

static char *whom = "world"; static int howmany = 1;module_param(howmany, int, S_IRUGO); module_param(whom, charp, S_IRUGO); 

模块参数支持许多类型: 

bool invbool

一个布尔型( true 或者 false)值(相关的变量应当是 int 类型). invbool 类型颠倒了值, 所以真值变成 false, 反之亦然.

charp

一个字符指针值. 内存为用户提供的字串分配, 指针因此设置.

intlong short uintulong ushort

基本的变长整型值. 以 u 开头的是无符号值.

 

数组参数, 用逗号间隔的列表提供的值, 模块加载者也支持. 声明一个数组参数,

使用: 

module_param_array(name,type,num,perm);

这里 name 是你的数组的名子(也是参数名), type 是数组元素的类型, num 是一 个整型变量, perm 是通常的权限值. 如果数组参数在加载时设置, num 被设置成 提供的数的个数. 模块加载者拒绝比数组能放下的多的值.

如果你确实需要一个没有出现在上面列表中的类型, 在模块代码里有钩子会允许 你来定义它们; 任何使用它们的细节见 moduleparam.h. 所有的模块参数应当给 定一个缺省值; insmod 只在用户明确告知它的时候才改变这些值. 模块可检查明 显的参数, 通过对应它们的缺省值检查这些参数.

最后的 module_param 字段是一个权限值; 你应当使用 <linux/stat.h> 中定义 的值. 这个值控制谁可以存取这些模块参数在 sysfs 中的表示. 如果 perm被 设为 0, 就根本没有 sysfs 项. 否则, 它出现在/sys/module[5]  下面, 带有给 定的权限. 使用S_IRUGO 作为参数可以被所有人读取, 但是不能改变; S_IRUGO|S_IWUSR 允许 root 来改变参数. 注意, 如果一个参数被 sysfs 修改, 你的模块看到的参数值也改变了, 但是你的模块没有任何其他的通知. 你应当不 要使模块参数可写, 除非你准备好检测这个改变并且因而作出反应.

 [5] 然而, 在本书写作时, 有讨论将参数移出 sysfs.

 3.9 2.9.  在用户空间做 

2.9.  在用户空间做

一个第一次涉及内核问题的 Unix 程序员, 可能会紧张写一个模块. 编写一个用 户程序来直接读写设备端口可能容易些.

 确实, 有几个论据倾向于用户空间编程, 有时编写一个所谓的用户空间设备驱动对比钻研内核是一个明智的选择. 在本节, 我们讨论几个理由, 为什么你可能在 用户空间编写驱动. 本书是关于内核空间驱动的, 但是, 所以我们不超越这个介绍性的讨论.

用户空间驱动的好处在于:

·       完整的 C 库可以连接. 驱动可以进行许多奇怪的任务, 不用依靠外面的 程序(实现使用策略的工具程序, 常常随着驱动自身发布).

·       程序员可以在驱动代码上运行常用的调试器, 而不必走调试一个运行中的 内核的弯路.

·       如果一个用户空间驱动挂起了, 你可简单地杀掉它. 驱动的问题不可能挂起整个系统, 除非被控制的硬件真的疯掉了.

·       用户内存是可交换的, 不象内核内存. 一个不常使用的却有很大一个驱动 的设备不会占据别的程序可以用到的 RAM, 除了在它实际在用时.

·       一个精心设计的驱动程序仍然可以, 如同内核空间驱动, 允许对设备的并 行存取.

·       如果你必须编写一个封闭源码的驱动, 用户空间的选项使你容易避免不明 朗的许可的情况和改变的内核接口带来的问题.

例如, USB 驱动能够在用户空间编写; 看(仍然年幼) libusb 项目, 在 libusb.sourceforge.net 和 "gadgetfs" 在内核源码里. 另一个例子是 X 服务 器: 它确切地知道它能处理哪些硬件, 哪些不能, 并且它提供图形资源给所有的 X 客户.注意, 然而, 有一个缓慢但是固定的漂移向着基于 frame-buffer 的图 形环境, X 服务器只是作为一个服务器, 基于一个内核空间的真实的设备驱动, 这个驱动负责真正的图形操作.

常常, 用户空间驱动的编写者完成一个服务器进程, 从内核接管作为单个代理的 负责硬件控制的任务. 客户应用程序就可以连接到服务器来进行实际的操作; 因 此, 一个聪明的驱动经常可以允许对设备的并行存取. 这就是 X 服务器如何工 作的.

但是用户空间的设备驱动的方法有几个缺点. 最重要的是:

·       中断在用户空间无法用. 在某些平台上有对这个限制的解决方法, 例如在

IA32 体系上的 vm86 系统调用.

·       只可能通过内存映射 /dev/mem 来使用 DMA, 而且只有特权用户可以这样 做.

·    存取I/O 端口只能在调用 ioperm 或者 iopl 之后. 此外, 不是所有的 平台支持这些系统调用, 而存取/dev/port 可能太慢而无效率. 这些系统 调用和设备文件都要求特权用户.

·       响应时间慢, 因为需要上下文切换在客户和硬件之间传递信息或动作.

·       更不好的是, 如果驱动已被交换到硬盘, 响应时间会长到不可接受. 使用 mlock 系统调用可能会有帮助, 但是常常的你将需要锁住许多内存页, 因 为一个用户空间程序依赖大量的库代码. mlock, 也, 限制在授权用户上.

·       最重要的设备不能在用户空间处理,包括但不限于, 网络接口和块设备.

如你所见, 用户空间驱动不能做的事情毕竟太多. 感兴趣的应用程序还是存在: 例如, 对 SCSI 扫描器设备的支持( 由 SANE 包实现 )和 CD 刻录器 ( 由 cdrecord 和别的工具实现 ). 在两种情况下, 用户级别的设备情况依赖 "SCSI gneric" 内核驱动, 它输出了低层的 SCSI 功能给用户程序, 因此它们可以驱动 它们自己的硬件

一种在用户空间工作的情况可能是有意义的, 当你开始处理新的没有用过的硬件时. 这样你可以学习去管理你的硬件, 不必担心挂起整个系统. 一旦你完成了, 在一个内核模块中封装软件就会是一个简单操作了.

3.10 2.10.  快速参考

2.10.  快速参考

本节总结了我们在本章接触到的内核函数, 变量, 宏定义, 和 /proc 文件. 它 的用意是作为一个参考. 每一项列都在相关头文件的后面, 如果有. 从这里开始, 在几乎每章的结尾会有类似一节, 总结一章中介绍的新符号. 本节中的项通常以 在本章中出现的顺序排列:

insmodmodprobermmod

用户空间工具, 加载模块到运行中的内核以及去除它们.

#include <linux/init.h>module_init(init_function);module_exit(cleanup_function);

指定模块的初始化和清理函数的宏定义.

    init

    initdata

    exit

    exitdata

函数(   init 和   exit )和数据 (  initdata 和   exitdata)的标记, 只用在模块初始化或者清理时间. 为初始化所标识的项可能会在初始化完成后丢弃; 退出的项可能被丢弃如果内核没有配置模块卸载. 这些标记通 过使相关的目标在可执行文件的特定的 ELF 节里被替换来工作.

#include <linux/sched.h> 

最重要的头文件中的一个. 这个文件包含很多驱动使用的内核 API 的定 义, 包括睡眠函数和许多变量声明.

struct task_struct *current;

当前进程. 

current->pidcurrent->comm

进程 ID 和 当前进程的命令名.

obj-m

一个 makefile 符号, 内核建立系统用来决定当前目录下的哪个模块应当 被建立.

/sys/module

/proc/modules

/sys/module 是一个 sysfs 目录层次, 包含当前加载模块的信息.

/proc/moudles 是旧式的,那种信息的单个文件版本. 其中的条目包含了 模块名, 每个模块占用的内存数量, 以及使用计数. 另外的字串追加到每 行的末尾来指定标志, 对这个模块当前是活动的.

vermagic. 

来自内核源码目录的目标文件, 描述一个模块为之建立的环境.

#include <linux/module.h>

必需的头文件. 它必须在一个模块源码中包含.

#include <linux/version.h>

头文件, 包含在建立的内核版本信息 

LINUX_VERSION_CODE

整型宏定义, 对 #ifdef 版本依赖有用. 

EXPORT_SYMBOL (symbol); EXPORT_SYMBOL_GPL (symbol);

宏定义, 用来输出一个符号给内核. 第 2 种形式输出没有版本信息, 第

3 种限制输出给 GPL 许可的模块.

MODULE_AUTHOR(author); MODULE_DESCRIPTION(description);MODULE_VERSION(version_string);MODULE_DEVICE_TABLE(table_info);MODULE_ALIAS(alternate_name);

放置文档在目标文件的模块中.

module_init(init_function);

module_exit(exit_function);

宏定义, 声明一个模块的初始化和清理函数.

#include <linux/moduleparam.h>

module_param(variable, type, perm);

宏定义, 创建模块参数, 可以被用户在模块加载时调整( 或者在启动时间, 对于内嵌代码). 类型可以是 bool, charp, int, invbool,short, ushort, uint, ulong, 或者 intarray.

#include <linux/kernel.h>

int printk(const char* fmt, ...);

内核代码的 printf 类似物

第  3  章  字符驱动

目录

3.1. scull  的设计(见 [标题编号.])

3.2.  主次编号(见 [标题编号.])

3.2.1.  设备编号的内部表示(见 [标题编号.])

3.2.2.  分配和释放设备编号(见 [标题编号.])

3.2.3.  主编号的动态分配(见 [标题编号.])

3.3.  一些重要数据结构(见 [标题编号.])

3.3.1.  文件操作(见 [标题编号.])

3.3.2.  文件结构(见 [标题编号.])

3.3.3. inode  结构(见 [标题编号.])

3.4.  字符设备注册(见 [标题编号.])

3.4.1. scull  中的设备注册(见 [标题编号.])

3.4.2.  老方法(见 [标题编号.])

3.5. open  和 release(见 [标题编号.])

3.5.1. open  方法(见 [标题编号.])

3.5.2. release 方法(见 [标题编号.])

3.6. scull  的内存使用(见 [标题编号.])

3.7.  读和写(见 [标题编号.])

3.7.1. read  方法(见 [标题编号.])

3.7.2. write  方法(见 [标题编号.])

3.7.3. readv 和 writev(见 [标题编号.])

3.8.  使用新设备(见 [标题编号.])

3.9.  快速参考(见 [标题编号.])

本章的目的是编写一个完整的字符设备驱动. 我们开发一个字符驱动是因为这一 类适合大部分简单硬件设备. 字符驱动也比块驱动易于理解(我们在后续章节接 触). 我们的最终目的是编写一个模块化的字符驱动, 但是我们不会在本章讨论

模块化的事情.

贯串本章, 我们展示从一个真实设备驱动提取的代码片段: scull( Simple Character Utility for LoadingLocalities). scull 是一个字符驱动, 操作一 块内存区域好像它是一个设备. 在本章, 因为 scull 的这个怪特性, 我们可互换地使用设备这个词和"scull 使用的内存区".

scull 的优势在于它不依赖硬件. scull 只是操作一些从内核分配的内存. 任何 人都可以编译和运行 scull, 并且 scull 在Linux 运行的体系结构中可移植. 另一方面, 这个设备除了演示内核和字符驱动的接口和允许用户运行一些测试之 外, 不做任何有用的事情.

3.1.  scull 的设计

编写驱动的第一步是定义驱动将要提供给用户程序的能力(机制).因为我们的"设 备"是计算机内存的一部分, 我们可自由做我们想做的事情. 它可以是一个顺序 的或者随机存取的设备, 一个或多个设备, 等等.

为使 scull 作为一个模板来编写真实设备的真实驱动, 我们将展示给你如何在 计算机内存上实现几个设备抽象, 每个有不同的个性.

scull 源码实现下面的设备. 模块实现的每种设备都被引用做一种类型.

scull0  到 scull3

4 个设备,每个由一个全局永久的内存区组成. 全局意味着如果设备被多 次打开, 设备中含有的数据由所有打开它的文件描述符共享. 永久意味着 如果设备关闭又重新打开, 数据不会丢失. 这个设备用起来有意思, 因为 它可以用惯常的命令来存取和测试, 例如 cp, cat, 以及 I/O重定向.

scullpipe0 到 scullpipe3

4 个 FIFO (先入先出) 设备, 行为象管道. 一个进程读的内容来自另一个 进程所写的. 如果多个进程读同一个设备, 它们竞争数据. scullpipe的 内部将展示阻塞读写和非阻塞读写如何实现, 而不必采取中断. 尽管真实 的驱动使用硬件中断来同步它们的设备, 阻塞和非阻塞操作的主题是重要 的并且与中断处理是分开的.(在第 10 章涉及).

scullsinglescullpriv sculluid scullwuid

这些设备与 scull0 相似, 但是在什么时候允许打开上有一些限制. 第一 个( snullsingle) 只允许一次一个进程使用驱动, 而 scullpriv 对每个 虚拟终端(或者 X 终端会话)是私有的, 因为每个控制台/终端上的进程有

不同的内存区. sculluid 和 scullwuid 可以多次打开, 但是一次只能是 一个用户; 前者返回一个"设备忙"错误, 如果另一个用户锁着设备, 而后 者实现阻塞打开. 这些 scull 的变体可能看来混淆了策略和机制, 但是 它们值得看看, 因为一些实际设备需要这类管理.

每个 scull 设备演示了驱动的不同特色, 并且呈现了不同的难度. 本章涉及scull0 到 scull3 的内部; 更高级的设备在第 6 章涉及. scullpipe 在"一个阻 塞 I/O 例子"一节中描述, 其他的在"设备文件上的存取控制"中描述.

4.1 3.1. scull  的设备 

4.2 3.2. 主次编号

3.2.  主次编号

字符设备通过文件系统中的名子来存取. 那些名子称为文件系统的特殊文件, 或 者设备文件, 或者文件系统的简单结点; 惯例上它们位于 /dev 目录. 字符驱动 的特殊文件由使用 ls -l 的输出的第一列的"c"标识. 块设备也出现在 /dev 中, 但是它们由"b"标识. 本章集中在字符设备, 但是下面的很多信息也适用于块设 备.

如果你发出 ls -l 命令, 你会看到在设备文件项中有 2 个数(由一个逗号分隔) 在最后修改日期前面, 这里通常是文件长度出现的地方. 这些数字是给特殊设备的主次设备编号. 下面的列表显示了一个典型系统上出现的几个设备. 它们的主 编号是 1, 4, 7, 和 10, 而次编号是 1, 3, 5, 64, 65, 和 129.

crw-rw-rw-

1

root

root

1,  3

Apr

11

2002

null

crw-------

1

root

root

10, 1

Apr

11

2002

psaux

crw-------

1

root

root

4,  1

Oct

28

03:04

tty1

crw-rw-rw-

1

root

tty

4, 64

Apr

11

2002

ttys0

crw-rw----

1

root

uucp

4, 65

Apr

11

2002

ttyS1

crw--w----

1

vcsa

tty

7,  1

Apr

11

2002

vcs1

crw--w----

1

vcsa

tty

7,129

Apr

11

2002

vcsa1

crw-rw-rw-

1

root

root

1,  5

Apr

11

2002

zero 

传统上, 主编号标识设备相连的驱动. 例如, /dev/null 和 /dev/zero 都由驱动

1 来管理, 而虚拟控制台和串口终端都由驱动 4 管理; 同样, vcs1 和 vcsa1 设

备都由驱动 7 管理. 现代 Linux 内核允许多个驱动共享主编号, 但是你看到的 大部分设备仍然按照一个主编号一个驱动的原则来组织.

次编号被内核用来决定引用哪个设备. 依据你的驱动是如何编写的(如同我们下 面见到的), 你可以从内核得到一个你的设备的直接指针, 或者可以自己使用次 编号作为本地设备数组的索引. 不论哪个方法, 内核自己几乎不知道次编号的任 何事情, 除了它们指向你的驱动实现的设备.

3.2.1.  设备编号的内部表示

在内核中, dev_t 类型(在 <linux/types.h>中定义)用来持有设备编号 -- 主次 部分都包括. 对于 2.6.0 内核, dev_t 是 32 位的量, 12 位用作主编号, 20 位 用作次编号. 你的代码应当, 当然, 对于设备编号的内部组织从不做任何假设; 相反, 应当利用在<linux/kdev_t.h>中的一套宏定义. 为获得一个 dev_t 的主 或者次编号, 使用:

MAJOR(dev_t dev); MINOR(dev_t dev);

相反, 如果你有主次编号, 需要将其转换为一个 dev_t, 使用:

MKDEV(int major, int minor);

 

注意, 2.6 内核能容纳有大量设备, 而以前的内核版本限制在 255 个主编号和

255 个次编号. 有人认为这么宽的范围在很长时间内是足够的, 但是计算领域被 这个特性的错误假设搞乱了. 因此你应当希望 dev_t 的格式将来可能再次改变; 但是, 如果你仔细编写你的驱动, 这些变化不会是一个问题.

3.2.2.  分配和释放设备编号

在建立一个字符驱动时你的驱动需要做的第一件事是获取一个或多个设备编号来 使用. 为此目的的必要的函数是 register_chrdev_region, 在 <linux/fs.h>中声明:

int register_chrdev_region(dev_tfirst, unsigned int count, char *name);

这里, first 是你要分配的起始设备编号. first 的次编号部分常常是 0, 但是 没有要求是那个效果. count 是你请求的连续设备编号的总数. 注意, 如果 count 太大, 你要求的范围可能溢出到下一个次编号; 但是只要你要求的编号范围可用, 一切都仍然会正确工作. 最后, name是应当连接到这个编号范围的设备 的名子; 它会出现在/proc/devices 和 sysfs 中如同大部分内核函数, 如果分配成功进行,register_chrdev_region 的返回值是0. 出错的情况下, 返回一个负的错误码, 你不能存取请求的区域.

如果你确实事先知道你需要哪个设备编号, register_chrdev_region 工作得好. 然而, 你常常不会知道你的设备使用哪个主编号; 在 Linux 内核开发社团中一 直努力使用动态分配设备编号. 内核会乐于动态为你分配一个主编号, 但是你必 须使用一个不同的函数来请求这个分配.

int alloc_chrdev_region(dev_t *dev,unsigned intfirstminor, unsignedint count, char *name);

使用这个函数, dev 是一个只输出的参数, 它在函数成功完成时持有你的分配范 围的第一个数. fisetminor 应当是请求的第一个要用的次编号; 它常常是 0. count 和 name 参数如同给 request_chrdev_region 的一样.

不管你任何分配你的设备编号, 你应当在不再使用它们时释放它. 设备编号的释 放使用:

voidunregister_chrdev_region(dev_t first, unsigned int count);

调用 unregister_chrdev_region 的地方常常是你的模块的 cleanup 函数.

上面的函数分配设备编号给你的驱动使用, 但是它们不告诉内核你实际上会对这些编号做什么. 在用户空间程序能够存取这些设备号中一个之前, 你的驱动需要 连接它们到它的实现设备操作的内部函数上. 我们将描述如何简短完成这个连接, 但首先顾及一些必要的枝节问题.

3.2.3.  主编号的动态分配

一些主设备编号是静态分派给最普通的设备的. 一个这些设备的列表在内核源码 树的 Documentation/devices.txt 中. 分配给你的新驱动使用一个已经分配的静态编号的机会很小, 但是, 并且新编号没在分配. 因此, 作为一个驱动编写者, 你有一个选择: 你可以简单地捡一个看来没有用的编号, 或者你以动态方式分配 主编号. 只要你是你的驱动的唯一用户就可以捡一个编号用; 一旦你的驱动更广 泛的被使用了, 一个随机捡来的主编号将导致冲突和麻烦.

因此, 对于新驱动, 我们强烈建议你使用动态分配来获取你的主设备编号, 而不 是随机选取一个当前空闲的编号. 换句话说, 你的驱动应当几乎肯定地使用 alloc_chrdev_region, 不是 register_chrdev_region.

动态分配的缺点是你无法提前创建设备节点, 因为分配给你的模块的主编号会变 化. 对于驱动的正常使用, 这不是问题, 因为一旦编号分配了, 你可从

/proc/devices中读取它.[6]

为使用动态主编号来加载一个驱动, 因此, 可使用一个简单的脚本来代替调用

insmod,在调用 insmod 后, 读取 /proc/devices 来创建特殊文件.

一个典型的 /proc/devices 文件看来如下:

Character devices:

1 mem

2 pty

3 ttyp

4 ttyS

6 lp

7 vcs

10 misc

13input

14sound

21 sg

180 usb

Block devices:

2 fd

8 sd

11 sr

65 sd

66 sd 

因此加载一个已经安排了一个动态编号的模块的脚本, 可以使用一个工具来编写,

如 awk , 来从 /proc/devices 获取信息以创建 /dev 中的文件.

下面的脚本, snull_load, 是 scull 发布的一部分. 以模块发布的驱动的用户可 以从系统的 rc.local 文件中调用这样一个脚本, 或者在需要模块时手工调用

它.

#!/bin/sh module="scull"device="scull" mode="664"

# invoke insmod withall arguments we got

# anduse a pathname, as newer modutils don't look in . by default

/sbin/insmod./$module.ko $* || exit 1

# remove stale nodes

rm -f/dev/${device}[0-3]

major=$(awk"\\$2==\"$module\"{print \\$1}" /proc/devices)

mknod /dev/${device}0c $major 0 mknod /dev/${device}1 c $major 1 mknod /dev/${device}2 c $major 2mknod /dev/${device}3 c $major 3

# give appropriategroup/permissions, and change the group.

# Not all distributions have staff, some have"wheel" instead. group="staff"

grep -q'^staff:' /etc/group || group="wheel"

chgrp $group/dev/${device}[0-3]

chmod$mode /dev/${device}[0-3]

这个脚本可以通过重定义变量和调整 mknod 行来适用于另外的驱动. 这个脚本 仅仅展示了创建 4 个设备, 因为 4 是 scull 源码中缺省的.

脚本的最后几行可能有些模糊:为什么改变设备的组和模式? 理由是这个脚本必 须由超级用户运行, 因此新建的特殊文件由 root 拥有. 许可位缺省的是只有root 有写权限, 而任何人可以读. 通常, 一个设备节点需要一个不同的存取策略, 因此在某些方面别人的存取权限必须改变. 我们的脚本缺省是给一个用户组 存取, 但是你的需求可能不同. 在第 6 章的"设备文件的存取控制"一节中, sculluid 的代码演示了驱动如何能够强制它自己的对设备存取的授权.

还有一个 scull_unload 脚本来清理 /dev 目录并去除模块.

作为对使用一对脚本来加载和卸载的另外选择, 你可以编写一个 init 脚本, 准 备好放在你的发布使用这些脚本的目录中. [7]作为 scull 源码的一部分, 我们提 供了一个相当完整和可配置的 init 脚本例子, 称为 scull.init; 它接受传统 的参数 -- start, stop, 和 restart -- 并且完成scull_load 和 scull_unload 的角色.

如果反复创建和销毁 /dev 节点, 听来过分了, 有一个有用的办法. 如果你在加载和卸载单个驱动, 你可以在你第一次使用你的脚本创建特殊文件之后, 只使用 rmmod 和 insmod: 这样动态编号不是随机的. [8]并且你每次都可以使用所选的同一个编号, 如果你不加载任何别的动态模块. 在开发中避免长脚本是有用的. 但 是这个技巧, 显然不能扩展到一次多于一个驱动.

安排主编号最好的方式, 我们认为, 是缺省使用动态分配, 而留给自己在加载时 指定主编号的选项权, 或者甚至在编译时. scull 实现以这种方式工作; 它使用

一个全局变量, scull_major, 来持有选定的编号(还有一个 scull_minor 给次编 号). 这个变量初始化为 SCULL_MAJOR, 定义在 scull.h. 发布的源码中的 SCULL_MAJOR 的缺省值是 0, 意思是"使用动态分配". 用户可以接受缺省值或者

选择一个特殊主编号, 或者在编译前修改宏定义或者在 insmod 命令行指定一个 值给 scull_major. 最后, 通过使用 scull_load 脚本,用户可以在

scull_load 的命令行传递参数给 insmod.[9]

这是我们用在 scull 的源码中获取主编号的代码:

if (scull_major) {

dev =MKDEV(scull_major, scull_minor);

result= register_chrdev_region(dev, scull_nr_devs, "scull");

} else{

result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");

scull_major= MAJOR(dev);

}

if(result < 0) {

printk(KERN_WARNING"scull: can't get major %d\n",scull_major);

returnresult;

}

本书使用的几乎所有例子驱动使用类似的代码来分配它们的主编号.

[6] 从 sysfs 中能获取更好的设备信息, 在基于 2.6 的系统通常加载于 /sys. 但是使 scull 通过 sysfs 输出信息超出了本章的范围; 我们在 14 章中回到这 个主题.

[7] Linux Standard Base 指出init 脚本应当放在 /etc/init.d, 但是一些发布 仍然放在别处. 另外, 如果你的脚本在启动时运行, 你需要从合适的运行级别目 录做一个连接给它(也就是, .../rc3.d).

[8] 尽管某些内核开发者已警告说将来就会这样做.

[9] init 脚本 scull.init 不在命令行中接受驱动选项, 但是它支持一个配置文 件, 因为它被设计来在启动和关机时自动使用.

4.3 3.3. 一些重要数据结构

3.3.  一些重要数据结构

如同你想象的, 注册设备编号仅仅是驱动代码必须进行的诸多任务中的第一个. 我们将很快看到其他重要的驱动组件, 但首先需要涉及一个别的. 大部分的基础 性的驱动操作包括 3 个重要的内核数据结构, 称为file_operations, file, 和

inode. 需要对这些结构的基本了解才能够做大量感兴趣的事情, 因此我们现在 在进入如何实现基础性驱动操作的细节之前, 会快速查看每一个.

3.3.1.  文件操作

到现在, 我们已经保留了一些设备编号给我们使用, 但是我们还没有连接任何我 们设备操作到这些编号上. file_operation 结构是一个字符驱动如何建立这个连接. 这个结构, 定义在<linux/fs.h>, 是一个函数指针的集合. 每个打开文件 (内部用一个 file 结构来代表, 稍后我们会查看)与它自身的函数集合相关连

( 通过包含一个称为 f_op 的成员, 它指向一个file_operations 结构). 这些 操作大部分负责实现系统调用, 因此, 命名为 open, read, 等等. 我们可以认为 文件是一个"对象"并且其上的函数操作称为它的"方法", 使用面向对象编程的术 语来表示一个对象声明的用来操作对象的动作. 这是我们在 Linux 内核中看到 的第一个面向对象编程的现象, 后续章中我们会看到更多.

传统上, 一个 file_operation 结构或者其一个指针称为 fops( 或者它的一些变体). 结构中的每个成员必须指向驱动中的函数, 这些函数实现一个特别的操 作, 或者对于不支持的操作留置为 NULL. 当指定为 NULL 指针时内核的确切的 行为是每个函数不同的, 如同本节后面的列表所示.

下面的列表介绍了一个应用程序能够在设备上调用的所有操作. 我们已经试图保 持列表简短, 这样它可作为一个参考, 只是总结每个操作和在 NULL 指针使用时 的缺省内核行为.

在你通读 file_operations 方法的列表时, 你会注意到不少参数包含字串

  user. 这种注解是一种文档形式, 注意, 一个指针是一个不能被直接解引用的 用户空间地址. 对于正常的编译,  user 没有效果, 但是它可被外部检查软件使用来找出对用户空间地址的错误使用. 

本章剩下的部分, 在描述一些其他重要数据结构后, 解释了最重要操作的角色并 且给了提示, 告诫和真实代码例子. 我们推迟讨论更复杂的操作到后面章节, 因 为我们还不准备深入如内存管理, 阻塞操作, 和异步通知.

struct module *owner

第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE, 一个在

<linux/module.h>中定义的宏.

loff_t (*llseek) (struct file *,loff_t, int);

llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返 回值. loff_t 参数是一个"longoffset", 并且就算在 32 位平台上也至少

64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调 用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在"file 结构" 一节中描述).

ssize_t (*read) (struct file *, char __user *,size_t, loff_t *);

用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以

-EINVAL("Invalidargument") 失败. 一个非负返回值代表了成功读取的 字节数( 返回值是一个 "signed size"类型, 常常是目标平台本地的整数 类型).

ssize_t (*aio_read)(struct kiocb *, char __user *, size_t, loff_t);

初始化一个异步读 -- 可能在函数返回前不结束的读操作. 如果这个方法 是 NULL, 所有的操作会由 read 代替进行(同步地).

ssize_t (*write) (struct file *, const char __user *, size_t,loff_t *);

发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程 序. 如果非负, 返回值代表成功写的字节数.

ssize_t (*aio_write)(struct kiocb *,const char __user *, size_t,loff_t *);

初始化设备上的一个异步写.

int (*readdir) (struct file *, void *,filldir_t);

对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对文件系 统有用.

unsigned int (*poll) (struct file*, struct poll_table_struct *);

poll 方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查 询对一个或多个文件描述符的读或写是否会阻塞. poll方法应当返回一个 位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到 I/O 变为可能. 如果一个驱动的 poll 方法 为 NULL, 设备假定为不阻塞地可读可写.

int (*ioctl)(struct inode *, struct file *, unsigned int,unsigned long);

ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个 磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表. 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求

(-ENOTTY, "设备无这样的 ioctl"), 系统调用返回一个错误.

int (*mmap) (struct file *,struct vm_area_struct *);

mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.

int (*open) (struct inode *, struct file *);

尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的 方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到 通知. 

int (*flush) (struct file *);

flush 操作在进程关闭它的设备文件描述符的拷贝时调用; 它应当执行 (并且等待)设备的任何未完成的操作. 这个必须不要和用户查询请求的 fsync 操作混淆了. 当前, flush在很少驱动中使用; SCSI磁带驱动使用 它, 例如, 为确保所有写的数据在设备关闭前写到磁带上. 如果 flush

为 NULL, 内核简单地忽略用户应用程序的请求.

int (*release) (struct inode *,struct file *);

在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL.

int (*fsync) (struct file *, struct dentry *,int);

这个方法是 fsync 系统调用的后端, 用户调用来刷新任何挂着的数据.

如果这个指针是 NULL, 系统调用返回 -EINVAL.

int (*aio_fsync)(struct kiocb *, int);

这是 fsync 方法的异步版本.

int (*fasync) (int, struct file *,int);

这个操作用来通知设备它的 FASYNC 标志的改变. 异步通知是一个高级的 主题, 在第 6 章中描述. 这个成员可以是 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 *);

这些方法实现发散/汇聚读和写操作. 应用程序偶尔需要做一个包含多个 内存区的单个读或写操作; 这些系统调用允许它们这样做而不必对数据进 行额外拷贝. 如果这些函数指针为 NULL, read 和 write 方法被调用( 可能多于一次 ).

ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t,void *);

这个方法实现 sendfile 系统调用的读, 使用最少的拷贝从一个文件描述 符搬移数据到另一个. 例如, 它被一个需要发送文件内容到一个网络连接 的 web 服务器使用. 设备驱动常常使 sendfile 为NULL.

ssize_t (*sendpage) (struct file*, struct page *,int,size_t,loff_t *, int);

sendpage 是 sendfile 的另一半; 它由内核调用来发送数据, 一次一页,

到对应的文件. 设备驱动实际上不实现 sendpage.

unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long,unsigned long);

这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备 上的内存段中. 这个任务通常由内存管理代码进行; 这个方法存在为了使 驱动能强制特殊设备可能有的任何的对齐请求. 大部分驱动可以置这个方 法为 NULL.[10]

int (*check_flags)(int)

这个方法允许模块检查传递给 fnctl(F_SETFL...) 调用的标志.

int (*dir_notify)(struct file *, unsigned long);

这个方法在应用程序使用 fcntl 来请求目录改变通知时调用. 只对文件 系统有用; 驱动不需要实现 dir_notify.

scull 设备驱动只实现最重要的设备方法. 它的 file_operations 结构是如下 初始化的:

structfile_operations scull_fops = {

.owner=  THIS_MODULE,

.llseek=  scull_llseek,

.read =  scull_read,

.write=  scull_write,

.ioctl=  scull_ioctl,

.open =  scull_open,

.release=  scull_release,

};

这个声明使用标准的 C 标记式结构初始化语法. 这个语法是首选的, 因为它使 驱动在结构定义的改变之间更加可移植, 并且, 有争议地, 使代码更加紧凑和可读. 标记式初始化允许结构成员重新排序; 在某种情况下, 真实的性能提高已经实现, 通过安放经常使用的成员的指针在相同硬件高速存储行中.

3.3.2.  文件结构

struct file, 定义于 <linux/fs.h>, 是设备驱动中第二个最重要的数据结构. 注意 file 与用户空间程序的 FILE 指针没有任何关系. 一个 FILE 定义在 C 库中, 从不出现在内核代码中. 一个 structfile, 另一方面, 是一个内核结构, 从不出现在用户程序中.

文件结构代表一个打开的文件. (它不特定给设备驱动; 系统中每个打开的文件有 一个关联的 struct file 在内核空间). 它由内核在 open 时创建, 并传递给在文件上操作的任何函数, 直到最后的关闭. 在文件的所有实例都关闭后, 内核释 放这个数据结构.

在内核源码中, struct file 的指针常常称为 file或者 filp("file pointer").我们将一直称这个指针为 filp 以避免和结构自身混淆. 因此, file 指的是结构, 而 filp 是结构指针.

struct file 的最重要成员在这展示. 如同在前一节, 第一次阅读可以跳过这个 列表. 但是,在本章后面, 当我们面对一些真实 C 代码时, 我们将更详细讨论 这些成员.

mode_t f_mode;

文件模式确定文件是可读的或者是可写的(或者都是), 通过位

FMODE_READ 和FMODE_WRITE. 你可能想在你的 open 或者ioctl 函数中 检查这个成员的读写许可, 但是你不需要检查读写许可, 因为内核在调用 你的方法之前检查. 当文件还没有为那种存取而打开时读或写的企图被拒绝, 驱动甚至不知道这个情况. 

loff_t f_pos;

当前读写位置. loff_t 在所有平台都是 64 位( 在 gcc 术语里是long long ). 驱动可以读这个值, 如果它需要知道文件中的当前位置, 但是正 常地不应该改变它; 读和写应当使用它们作为最后参数而收到的指针来更 新一个位置, 代替直接作用于 filp->f_pos. 这个规则的一个例外是在 llseek 方法中, 它的目的就是改变文件位置.

unsigned int f_flags;

这些是文件标志, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驱动应当检查 O_NONBLOCK 标志来看是否是请求非阻塞操作( 我们在第一章的"阻塞 和非阻塞操作"一节中讨论非阻塞 I/O ); 其他标志很少使用. 特别地, 应

当检查读/写许可, 使用 f_mode 而不是 f_flags. 所有的标志在头文件

<linux/fcntl.h>中定义.

struct file_operations *f_op;

和文件关联的操作. 内核安排指针作为它的 open 实现的一部分, 接着读 取它当它需要分派任何的操作时. filp->f_op 中的值从不由内核保存为后面的引用; 这意味着你可改变你的文件关联的文件操作, 在你返回调用者 之后新方法会起作用. 例如, 关联到主编号 1 (/dev/null, /dev/zero, 等等)的 open 代码根据打开的次编号来替代 filp->f_op 中的操作. 这 个做法允许实现几种行为, 在同一个主编号下而不必在每个系统调用中引 入开销. 替换文件操作的能力是面向对象编程的"方法重载"的内核对等体.

void *private_data;

open 系统调用设置这个指针为 NULL, 在为驱动调用 open 方法之前. 你 可自由使用这个成员或者忽略它; 你可以使用这个成员来指向分配的数据, 但是接着你必须记住在内核销毁文件结构之前, 在 release 方法中释放 那个内存. private_data 是一个有用的资源, 在系统调用间保留状态信息, 我们大部分例子模块都使用它.

struct dentry *f_dentry;

关联到文件的目录入口( dentry )结构. 设备驱动编写者正常地不需要关 心 dentry 结构, 除了作为 filp->f_dentry->d_inode 存取 inode 结构.

真实结构有多几个成员, 但是它们对设备驱动没有用处. 我们可以安全地忽略这 些成员, 因为驱动从不创建文件结构; 它们真实存取别处创建的结构.

3.3.3.  inode 结构

inode 结构由内核在内部用来表示文件. 因此, 它和代表打开文件描述符的文件 结构是不同的. 可能有代表单个文件的多个打开描述符的许多文件结构, 但是它 们都指向一个单个 inode 结构.inode 结构包含大量关于文件的信息. 作为一个通用的规则, 这个结构只有 2个成员对于编写驱动代码有用:

dev_t i_rdev; 

对于代表设备文件的节点, 这个成员包含实际的设备编号.

struct cdev *i_cdev;

struct cdev 是内核的内部结构, 代表字符设备; 这个成员包含一个指针,

指向这个结构, 当节点指的是一个字符设备文件时.

i_rdev 类型在2.5 开发系列中改变了, 破坏了大量的驱动. 作为一个鼓励更可移植编程的方法, 内核开发者已经增加了 2 个宏, 可用来从一个 inode 中获取 主次编号:

unsigned int iminor(struct inode *inode);

unsigned int imajor(struct inode *inode);

为了不要被下一次改动抓住, 应当使用这些宏代替直接操作 i_rdev.

[10] 注意, release 不是每次进程调用 close 时都被调用. 无论何时共享一个文 件结构(例如, 在一个 fork 或 dup 之后),release 不会调用直到所有的拷贝都 关闭了. 如果你需要在任一拷贝关闭时刷新挂着的数据, 你应当实现 flush 方 法.

4.4 3.4. 字符设备注册

3.4.  字符设备注册

如我们提过的, 内核在内部使用类型 struct cdev 的结构来代表字符设备. 在内 核调用你的设备操作前, 你编写分配并注册一个或几个这些结构. [11]为此, 你的 代码应当包含<linux/cdev.h>, 这个结构和它的关联帮助函数定义在这里.有 2 种方法来分配和初始化一个这些结构. 如果你想在运行时获得一个独立的cdev 结构, 你可以为此使用这样的代码:

struct cdev *my_cdev= cdev_alloc();

my_cdev->ops= &my_fops; 

但是, 偶尔你会想将 cdev 结构嵌入一个你自己的设备特定的结构; scull 这样 做了. 在这种情况下, 你应当初始化你已经分配的结构, 使用:

voidcdev_init(struct cdev *cdev, struct file_operations *fops);

任一方法, 有一个其他的 struct cdev 成员你需要初始化. 象 file_operations 结构, struct cdev 有一个拥有者成员, 应当设置为 THIS_MODULE. 一旦 cdev 结构建立, 最后的步骤是把它告诉内核, 调用:

int cdev_add(structcdev *dev, dev_t num, unsigned int count);

这里, dev 是 cdev 结构, num是这个设备响应的第一个设备号, count 是应当 关联到设备的设备号的数目. 常常 count 是 1, 但是有多个设备号对应于一个 特定的设备的情形. 例如, 设想 SCSI 磁带驱动, 它允许用户空间来选择操作模 式(例如密度), 通过安排多个次编号给每一个物理设备.

在使用 cdev_add 是有几个重要事情要记住. 第一个是这个调用可能失败. 如果 它返回一个负的错误码, 你的设备没有增加到系统中. 它几乎会一直成功, 但是, 并且带起了其他的点: cdev_add 一返回, 你的设备就是"活的"并且内核可以调用它的操作. 除非你的驱动完全准备好处理设备上的操作, 你不应当调用

cdev_add.

为从系统去除一个字符设备, 调用:

void cdev_del(structcdev *dev);

显然, 你不应当在传递给 cdev_del 后存取 cdev 结构.

3.4.1.  scull 中的设备注册

在内部, scull 使用一个 struct scull_dev 类型的结构表示每个设备. 这个结 构定义为:

struct scull_dev {

structscull_qset *data;  /* Pointer to firstquantum set */

intquantum;  /* the current quantum size */

intqset;  /* the current array size */

unsignedlong size;  /* amount of data stored here*/

unsignedint access_key;  /* used by sculluid andscullpriv */

struct semaphore sem;  /* mutual exclusion semaphore  */

struct cdev cdev; /*Char device structure */

};

我们在遇到它们时讨论结构中的各个成员, 但是现在, 我们关注于 cdev, 我们 的设备与内核接口的 struct cdev. 这个结构必须初始化并且如上所述添加到系 统中; 处理这个任务的 scull 代码是:

static voidscull_setup_cdev(struct scull_dev *dev, int index)

{

int err, devno =MKDEV(scull_major, scull_minor + index);

cdev_init(&dev->cdev,&scull_fops);

dev->cdev.owner= THIS_MODULE;

dev->cdev.ops= &scull_fops;

err = cdev_add(&dev->cdev, devno, 1);

/* Failgracefully if need be */

if (err)

printk(KERN_NOTICE"Error %d adding scull%d", err,index);

}

因为 cdev 结构嵌在 struct scull_dev 里面, cdev_init 必须调用来进行那个 结构的初始化.

3.4.2.  老方法

如果你深入浏览 2.6 内核的大量驱动代码, 你可能注意到有许多字符驱动不使 用我们刚刚描述过的 cdev 接口. 你见到的是还没有更新到 2.6 内核接口的老 代码. 因为那个代码实际上能用, 这个更新可能很长时间不会发生. 为完整, 我 们描述老的字符设备注册接口, 但是新代码不应当使用它; 这个机制在将来内核 中可能会消失.

注册一个字符设备的经典方法是使用:

int register_chrdev(unsigned int major, const char *name, structfile_operations *fops);

这里, major 是感兴趣的主编号, name 是驱动的名子(出现在/proc/devices), fops 是缺省的 file_operations 结构. 一个对 register_chrdev 的调用为给 定的主编号注册 0 - 255 的次编号, 并且为每一个建立一个缺省的 cdev 结构. 使用这个接口的驱动必须准备好处理对所有 256 个次编号的 open 调用( 不管 它们是否对应真实设备 ), 它们不能使用大于 255 的主或次编号.

如果你使用 register_chrdev, 从系统中去除你的设备的正确的函数是:

intunregister_chrdev(unsigned int major, const char *name);

major 和name 必须和传递给 register_chrdev 的相同,否则调用会失败.

[11] 有一个早些的机制以避免使用 cdev结构(我们在"老方法"一节中讨论).但是,

新代码应当使用新技术.

4.5 3.5. open  release

3.5.  open 和 release

到此我们已经快速浏览了这些成员, 我们开始在真实的 scull 函数中使用它们.

3.5.1.  open方法

open 方法提供给驱动来做任何的初始化来准备后续的操作. 在大部分驱动中, open 应当进行下面的工作:

·       检查设备特定的错误(例如设备没准备好, 或者类似的硬件错误

·       如果它第一次打开, 初始化设备

·       如果需要, 更新 f_op 指针.

·       分配并填充要放进 filp->private_data 的任何数据结构

但是, 事情的第一步常常是确定打开哪个设备. 记住 open 方法的原型是:

int (*open)(structinode *inode, struct file *filp);

inode参数有我们需要的信息,以它的 i_cdev 成员的形式, 里面包含我们之前 建立的 cdev 结构. 唯一的问题是通常我们不想要 cdev 结构本身, 我们需要的 是包含 cdev 结构的 scull_dev 结构. C 语言使程序员玩弄各种技巧来做这种转换; 但是, 这种技巧编程是易出错的, 并且导致别人难于阅读和理解代码. 幸运 的是, 在这种情况下, 内核 hacker 已经为我们实现了这个技巧, 以 container_of 宏的形式, 在 <linux/kernel.h> 中定义: 

container_of(pointer,container_type, container_field); 

这个宏使用一个指向 container_field类型的成员的指针, 它在一个 container_type 类型的结构中, 并且返回一个指针指向包含结构. 在 scull_open, 这个宏用来找到适当的设备结构:

struct scull_dev*dev; /* device information */

dev =container_of(inode->i_cdev, structscull_dev, cdev);

filp->private_data= dev; /* for other methods */

一旦它找到 scull_dev 结构, scull 在文件结构的 private_data 成员中存储一个它的指针, 为以后更易存取.

识别打开的设备的另外的方法是查看存储在 inode 结构的次编号. 如果你使用register_chrdev 注册你的设备, 你必须使用这个技术. 确认使用iminor 从 inode 结构中获取次编号, 并且确定它对应一个你的驱动真正准备好处理的设 备.

scull_open 的代码(稍微简化过)是:

int scull_open(structinode *inode, struct file *filp)

{

structscull_dev *dev; /* device information */

dev =container_of(inode->i_cdev, structscull_dev, cdev);

filp->private_data= dev; /* for other methods */

/* now trim to 0 the length of the device if open was write-only

*/

if ((filp->f_flags & O_ACCMODE) ==O_WRONLY)

{

scull_trim(dev);/* ignore errors */

}

return0; /* success */

}

代码看来相当稀疏, 因为在调用 open 时它没有做任何特别的设备处理. 它不需 要, 因为 scull 设备设计为全局的和永久的. 特别地, 没有如"在第一次打开时

初始化设备"等动作, 因为我们不为 scull 保持打开计数.

唯一在设备上的真实操作是当设备为写而打开时将它截取为长度为 0. 这样做是 因为, 在设计上, 用一个短的文件覆盖一个 scull 设备导致一个短的设备数据区. 这类似于为写而打开一个常规文件, 将其截短为 0. 如果设备为读而打开, 这个操作什么都不做.

在我们查看其他 scull 特性的代码时将看到一个真实的初始化如何起作用的.

3.5.2.  release 方法

release方法的角色是 open 的反面. 有时你会发现方法的实现称为 device_close, 而不是 device_release. 任一方式, 设备方法应 释放open 分配在 filp->private_data 中的任何东西在最后的 close 关闭设备

 scull 的基本形式没有硬件去关闭, 因此需要的代码是最少的:[12]

intscull_release(struct inode *inode, struct file *filp)

{

return0;

}

你可能想知道当一个设备文件关闭次数超过它被打开的次数会发生什么. 毕竟, dup 和 fork 系统调用不调用open 来创建打开文件的拷贝; 每个拷贝接着在程 序终止时被关闭. 例如, 大部分程序不打开它们的 stdin 文件(或设备), 但是 它们都以关闭它结束. 当一个打开的设备文件已经真正被关闭时驱动如何知道?

答案简单: 不是每个 close 系统调用引起调用 release 方法. 只有真正释放设 备数据结构的调用会调用这个方法 -- 因此得名. 内核维持一个文件结构被使用 多少次的计数. fork 和 dup 都不创建新文件(只有 open这样); 它们只递增正 存在的结构中的计数. close系统调用仅在文件结构计数掉到 0 时执行 release 方法, 这在结构被销毁时发生. release 方法和 close 系统调用之间的这种关系 保证了你的驱动一次 open 只看到一次 release.

注意, flush 方法在每次应用程序调用 close 时都被调用. 但是, 很少驱动实现

flush, 因为常常在 close 时没有什么要做, 除非调用 release.

如你会想到的, 前面的讨论即便是应用程序没有明显地关闭它打开的文件也适用:

内核在进程 exit 时自动关闭了任何文件, 通过在内部使用 close 系统调用.

[12] 其他风味的设备由不同的函数关闭, 因为 scull_open 为每个设备替换了不同 的 filp->f_op. 我们在介绍每种风味时再讨论它们.

4.6 3.6. scull  的内存使用

3.6.  scull 的内存使用

在介绍读写操作前, 我们最好看看如何以及为什么 scull 进行内存分配. "如何" 是需要全面理解代码, "为什么"演示了驱动编写者需要做的选择, 尽管 scull 明 确地不是典型设备.

本节只处理 scull 中的内存分配策略, 不展示给你编写真正驱动需要的硬件管理技能. 这些技能在第 9 章和第 10 章介绍. 因此, 你可跳过本章, 如果你不 感兴趣于理解面向内存的 scull 驱动的内部工作.

scull 使用的内存区, 也称为一个设备, 长度可变. 你写的越多, 它增长越多;

通过使用一个短文件覆盖设备来进行修整.

scull 驱动引入2 个核心函数来管理 Linux 内核中的内存. 这些函数, 定义在

<linux/slab.h>, 是:

void *kmalloc(size_t size, int flags);

void kfree(void *ptr);

对 kmalloc 的调用试图分配 size 字节的内存; 返回值是指向那个内存的指针 或者如果分配失败为NULL. flags 参数用来描述内存应当如何分配; 我们在第 8 章详细查看这些标志. 对于现在, 我们一直使用GFP_KERNEL. 分配的内存应当 用 kfree 来释放.你应当从不传递任何不是从 kmalloc 获得的东西给kfree. 但是, 传递一个 NULL 指针给 kfree 是合法的.

kmalloc 不是最有效的分配大内存区的方法(见第 8 章), 所以挑选给 scull 的实现不是一个特别巧妙的. 一个巧妙的源码实现可能更难阅读, 而本节的目标是 展示读和写, 不是内存管理. 这是为什么代码只是使用 kmalloc 和 kfree 而不 依靠整页的分配, 尽管这个方法会更有效.

在 flip 一边, 我们不想限制"设备"区的大小, 由于理论上的和实践上的理由. 理论上, 给在被管理的数据项施加武断的限制总是个坏想法. 实践上, scull 可 用来暂时地吃光你系统中的内存, 以便运行在低内存条件下的测试. 运行这样的 测试可能会帮助你理解系统的内部. 你可以使用命令 cp /dev/zero /dev/scull0 来用scull 吃掉所有的真实 RAM, 并且你可以使用 dd 工具来选择贝多少数据给 scull 设备.

在 scull, 每个设备是一个指针链表, 每个都指向一个 scull_dev 结构. 每个 这样的结构, 缺省地, 指向最多 4 兆字节, 通过一个中间指针数组. 发行代码 使用一个 1000 个指针的数组指向每个 4000 字节的区域. 我们称每个内存区域为一个量子, 数组(或者它的长度) 为一个量子集. 一个 scull 设备和它的内存 区如图一个 scull 设备的布局(见 [标题编号.])所示.

 

图  3.1.  一个scull 设备的布局

 


选定的数字是这样, 在 scull 中写单个一个字节消耗 8000 或 12,000 KB 内存:

4000 是量子, 4000 或者 8000 是量子集(根据指针在目标平台上是用 32 位还是64 位表示).相反, 如果你写入大量数据, 链表的开销不是太坏. 每 4 MB 数据 只有一个链表元素,设备的最大尺寸受限于计算机的内存大小.

为量子和量子集选择合适的值是一个策略问题, 而不是机制, 并且优化的值依赖 于设备如何使用. 因此, scull 驱动不应当强制给量子和量子集使用任何特别的值. 在 scull 中, 用户可以掌管改变这些值, 有几个途径:编译时间通过改变 scull.h 中的宏 SCULL_QUANTUM 和 SCULL_QSET, 在模块加载时设定整数值 scull_quantum 和 scull_qset, 或者使用 ioctl 在运行时改变当前值和缺省 值. 

使用宏定义和一个整数值来进行编译时和加载时配置, 是对于如何选择主编号的 回忆. 我们在驱动中任何与策略相关或专断的值上运用这个技术.

余下的唯一问题是如果选择缺省值. 在这个特殊情况下, 问题是找到最好的平衡, 由填充了一半的量子和量子集导致内存浪费, 如果量子和量子集小的情况下分配 释放和指针连接引起开销. 另外, kmalloc 的内部设计应当考虑进去. (现在我们 不追求这点, 不过;kmalloc 的内部在第 8 章探索.) 缺省值的选择来自假设测试时可能有大量数据写进 scull, 尽管设备的正常使用最可能只传送几 KB数据.

我们已经见过内部代表我们设备的 scull_dev 结构. 结构的quantum 和 qset 分别代表设备的量子和量子集大小. 实际数据, 但是, 是由一个不同的结构跟踪, 我们称为 struct scull_qset:

struct scull_qset {

void**data;

structscull_qset *next;

};

下一个代码片段展示了实际中 struct scull_dev 和struct scull_qset 是如何 被用来持有数据的. sucll_trim 函数负责释放整个数据区, 由 scull_open 在文 件为写而打开时调用. 它简单地遍历列表并且释放它发现的任何量子和量子集.

int scull_trim(struct scull_dev *dev)

{

structscull_qset *next, *dptr;

intqset = dev->qset; /*"dev" is not-null */

int i;

for(dptr = dev->data; dptr; dptr =next)

{ /*all the list items */

if (dptr->data) {

for (i= 0; i < qset; i++)

kfree(dptr->data[i]);

kfree(dptr->data);

dptr->data= NULL;

}

next =dptr->next;

kfree(dptr);

}

dev->size= 0;

dev->quantum= scull_quantum;

dev->qset= scull_qset;

dev->data= NULL;

return0;

}

scull_trim也用在模块清理函数中, 来归还 scull 使用的内存给系统.

4.7 3.7. 读和写

3.7.  读和写

读和写方法都进行类似的任务, 就是, 从和到应用程序代码拷贝数据. 因此, 它 们的原型相当相似, 可以同时介绍它们:

ssize_t read(struct file *filp, char  user *buff,size_t count, loff_t*offp);

ssize_t write(struct file *filp, constchar  user *buff,size_t count, loff_t *offp);

对于 2 个方法, filp 是文件指针, count 是请求的传输数据大小. buff 参数指 向持有被写入数据的缓存, 或者放入新数据的空缓存. 最后, offp是一个指针指 向一个"long offset type"对象, 它指出用户正在存取的文件位置. 返回值是一 个"signed size type"; 它的使用在后面讨论.

让我们重复一下, read 和 write 方法的 buff 参数是用户空间指针. 因此, 它 不能被内核代码直接解引用. 这个限制有几个理由:

·       依赖于你的驱动运行的体系, 以及内核被如何配置的, 用户空间指针当运 行于内核模式可能根本是无效的. 可能没有那个地址的映射, 或者它可能 指向一些其他的随机数据.

·       就算这个指针在内核空间是同样的东西, 用户空间内存是分页的, 在做系 统调用时这个内存可能没有在 RAM中. 试图直接引用用户空间内存可能 产生一个页面错, 这是内核代码不允许做的事情. 结果可能是一个"oops", 导致进行系统调用的进程死亡.

·       置疑中的指针由一个用户程序提供, 它可能是错误的或者恶意的. 如果你 的驱动盲目地解引用一个用户提供的指针, 它提供了一个打开的门路使用 户空间程序存取或覆盖系统任何地方的内存. 如果你不想负责你的用户的

系统的安全危险, 你就不能直接解引用用户空间指针.

显然, 你的驱动必须能够存取用户空间缓存以完成它的工作. 但是, 为安全起见 这个存取必须使用特殊的, 内核提供的函数. 我们介绍几个这样的函数(定义于

<asm/uaccess.h>), 剩下的在第一章"使用 ioctl 参数"一节中. 它们使用一些特殊的, 依赖体系的技巧来确保内核和用户空间的数据传输安全和正确.

scull 中的读写代码需要拷贝一整段数据到或者从用户地址空间. 这个能力由下 列内核函数提供, 它们拷贝一个任意的字节数组, 并且位于大部分读写实现的核 心中.

unsigned longcopy_to_user(void  user *to,const void*from,unsigned long count);

unsigned longcopy_from_user(void *to,const void   user*from,unsigned long count);

尽管这些函数表现象正常的 memcpy 函数, 必须加一点小心在从内核代码中存取 用户空间. 寻址的用户也当前可能不在内存, 虚拟内存子系统会使进程睡眠在这 个页被传送到位时. 例如, 这发生在必须从交换空间获取页的时候. 对于驱动编 写者来说, 最终结果是任何存取用户空间的函数必须是可重入的, 必须能够和其

他驱动函数并行执行, 并且, 特别的, 必须在一个它能够合法地睡眠的位置. 我 们在第 5 章再回到这个主题.

这 2 个函数的角色不限于拷贝数据到和从用户空间: 它们还检查用户空间指针 是否有效. 如果指针无效, 不进行拷贝; 如果在拷贝中遇到一个无效地址, 另一 方面, 只拷贝部分数据. 在 2 种情况下, 返回值是还要拷贝的数据量. scull 代 码查看这个错误返回, 并且如果它不是 0 就返回-EFAULT 给用户.

用户空间存取和无效用户空间指针的主题有些高级, 在第 6 章讨论. 然而, 值得注意的是如果你不需要检查用户空间指针,你可以调用   copy_to_user 和

  copy_from_user 来代替. 这是有用处的, 例如, 如果你知道你已经检查了这 些参数. 但是, 要小心; 事实上, 如果你不检查你传递给这些函数的用户空间指针, 那么你可能造成内核崩溃和/或安全漏洞.

至于实际的设备方法, read 方法的任务是从设备拷贝数据到用户空间(使用 copy_to_user), 而 write 方法必须从用户空间拷贝数据到设备(使用 copy_from_user). 每个 read 或write 系统调用请求一个特定数目字节的传送, 但是驱动可自由传送较少数据 -- 对读和写这确切的规则稍微不同, 在本章后面 描述.

不管这些方法传送多少数据, 它们通常应当更新 *offp 中的文件位置来表示在 系统调用成功完成后当前的文件位置. 内核接着在适当时候传播文件位置的改变 到文件结构. pread 和 pwrite 系统调用有不同的语义; 它们从一个给定的文件 偏移操作, 并且不改变其他的系统调用看到的文件位置. 这些调用传递一个指向 用户提供的位置的指针, 并且放弃你的驱动所做的改变.

给read 的参数(见 [标题编号.])表示了一个典型读实现是如何使用它的参数.

图  3.2.  给 read 的参数

read 和 write 方法都在发生错误时返回一个负值. 相反, 大于或等于 0 的返 回值告知调用程序有多少字节已经成功传送. 如果一些数据成功传送接着发生错

误, 返回值必须是成功传送的字节数, 错误不报告直到函数下一次调用. 实现这 个传统, 当然, 要求你的驱动记住错误已经发生, 以便它们可以在以后返回错误 状态.

尽管内核函数返回一个负数指示一个错误, 这个数的值指出所发生的错误类型

( 如第 2 章介绍 ), 用户空间运行的程序常常看到 -1 作为错误返回值. 它们 需要存取 errno 变量来找出发生了什么. 用户空间的行为由 POSIX 标准来规定, 但是这个标准没有规定内核内部如何操作.

3.7.1.  read 方法

read 的返回值由调用的应用程序解释:

·       如果这个值等于传递给 read 系统调用的 count 参数, 请求的字节数已 经被传送. 这是最好的情况.

·       如果是正数, 但是小于 count, 只有部分数据被传送. 这可能由于几个原

因, 依赖于设备. 常常, 应用程序重新试着读取. 例如, 如果你使用

fread 函数来读取, 库函数重新发出系统调用直到请求的数据传送完成.

·       如果值为 0, 到达了文件末尾(没有读取数据).

·       一个负值表示有一个错误. 这个值指出了什么错误, 根据

<linux/errno.h>.出错的典型返回值包括 -EINTR( 被打断的系统调用)

或者-EFAULT( 坏地址 ).

前面列表中漏掉的是这种情况"没有数据, 但是可能后来到达". 在这种情况下, read 系统调用应当阻塞. 我们将在第 6 章涉及阻塞.

scull 代码利用了这些规则. 特别地, 它利用了部分读规则. 每个 scull_read 调用只处理单个数据量子, 不实现一个循环来收集所有的数据; 这使得代码更短 更易读. 如果读程序确实需要更多数据, 它重新调用. 如果标准 I/O 库(例如,fread)用来读取设备, 应用程序甚至不会注意到数据传送的量子化.

如果当前读取位置大于设备大小, scull 的 read 方法返回 0 来表示没有可用的 数据(换句话说,我们在文件尾). 这个情况发生在如果进程 A 在读设备, 同时 进程 B 打开它写, 这样将设备截短为 0. 进程 A 突然发现自己过了文件尾, 下 一个读调用返回 0.

这是 read 的代码( 忽略对down_interruptible 的调用并且现在为 up; 我们 在下一章中讨论它们):

ssize_t scull_read(struct file *filp, char   user *buf, size_t count, loff_t *f_pos)

{

structscull_dev *dev =filp->private_data;

structscull_qset *dptr; /* the first listitem */

intquantum = dev->quantum, qset =dev->qset;

int itemsize= quantum * qset; /* how many bytes in the listitem

*/

intitem, s_pos, q_pos, rest;

ssize_tretval = 0;

 

if(down_interruptible(&dev->sem))

return-ERESTARTSYS;

if(*f_pos >= dev->size)

gotoout;

if(*f_pos + count > dev->size)

count =dev->size - *f_pos;

 

/* find listitem,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;

 

/* followthe list up to the right position (definedelsewhere)

*/

dptr =scull_follow(dev, item);

if(dptr == NULL || !dptr->data || !dptr->data[s_pos])

gotoout; /* don't fill holes */

 

/* read only up tothe 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))

{

retval= -EFAULT;

gotoout;

}

*f_pos+= count;

retval= count;

out: 

}

up(&dev->sem);

return retval;

3.7.2.  write 方法

write, 象 read, 可以传送少于要求的数据, 根据返回值的下列规则:

·       如果值等于 count, 要求的字节数已被传送.

·       如果正值, 但是小于 count, 只有部分数据被传送. 程序最可能重试写入 剩下的数据.

·       如果值为 0, 什么没有写. 这个结果不是一个错误, 没有理由返回一个错 误码. 再一次, 标准库重试写调用. 我们将在第 6 章查看这种情况的确切含义, 那里介绍了阻塞.

·       一个负值表示发生一个错误; 如同对于读, 有效的错误值是定义于<linux/errno.h>中. 

不幸的是, 仍然可能有发出错误消息的不当行为程序, 它在进行了部分传送时终 止. 这是因为一些程序员习惯看写调用要么完全失败要么完全成功, 这实际上是 大部分时间的情况, 应当也被设备支持. scull 实现的这个限制可以修改, 但是 我们不想使代码不必要地复杂.

write 的 scull 代码一次处理单个量子, 如 read 方法做的:

ssize_t scull_write(struct file *filp, const char  user *buf, size_t count, loff_t *f_pos)

{

structscull_dev *dev = filp->private_data;

structscull_qset *dptr;

intquantum = dev->quantum, qset =dev->qset;

intitemsize = quantum * qset;

int item, s_pos, q_pos, rest;

ssize_t retval = -ENOMEM; /* value used in "goto out" statements

*/

if (down_interruptible(&dev->sem))

return-ERESTARTSYS;

 

/* find listitem, 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 == NULL)

gotoout;

if (!dptr->data)

{

dptr->data= kmalloc(qset * sizeof(char *),GFP_KERNEL);

if(!dptr->data)

gotoout;

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])

 gotoout;

}

/*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))

{

retval= -EFAULT;

gotoout;

}

*f_pos+= count;

retval= count;

/* update the size */

if(dev->size < *f_pos)

dev->size = *f_pos;

out:

up(&dev->sem);

return retval;

}

3.7.3.  readv 和 writev

Unix系统已经长时间支持名为 readv 和 writev 的 2 个系统调用. 这些 read 和 write 的"矢量"版本使用一个结构数组, 每个包含一个缓存的指针和一个长 度值. 一个 readv 调用被期望来轮流读取指示的数量到每个缓存. 相反, writev 要收集每个缓存的内容到一起并且作为单个写操作送出它们.

如果你的驱动不提供方法来处理矢量操作, readv 和 writev 由多次调用你的 read 和 write 方法来实现. 在许多情况, 但是, 直接实现 readv 和 writev 能获得更大的效率.

矢量操作的原型是:

ssize_t (*readv) (structfile *filp, conststruct iovec *iov,unsigned long count, loff_t *ppos);

ssize_t (*writev) (struct file *filp, const structiovec *iov, unsignedlong count, loff_t *ppos);

这里, filp 和 ppos 参数与 read 和write 的相同. iovec 结构, 定义于

<linux/uio.h>,如同:

struct iovec

{

void   user *iov_base;   kernel_size_t iov_len;

};

每个 iovec 描述了一块要传送的数据; 它开始于 iov_base (在用户空间)并且有

iov_len 字节长. count 参数告诉有多少 iovec 结构. 这些结构由应用程序创建,

但是内核在调用驱动之前拷贝它们到内核空间.

矢量操作的最简单实现是一个直接的循环, 只是传递出去每个 iovec 的地址和长度给驱动的 read 和 write 函数. 然而, 有效的和正确的行为常常需要驱动 更聪明. 例如, 一个磁带驱动上的writev 应当将全部 iovec 结构中的内容作 为磁带上的单个记录.

很多驱动, 但是, 没有从自己实现这些方法中获益. 因此, scull 省略它们. 内 核使用 read 和 write来模拟它们, 最终结果是相同的.

4.8 3.8. 使用新设备

3.8.  使用新设备

一旦你装备好刚刚描述的 4 个方法, 驱动可以编译并测试了; 它保留了你写给它的任何数据, 直到你用新数据覆盖它. 这个设备表现如一个数据缓存器, 它的 长度仅仅受限于可用的真实 RAM 的数量. 你可试着使用 cp, dd, 以及 输入/输 出重定向来测试这个驱动.free 命令可用来看空闲内存的数量如何缩短和扩张的, 依据有多少数据写入scull.为对一次读写一个量子有更多信心, 你可增加一个 printk 在驱动的适当位置, 并且观察当应用程序读写大块数据中发生了什么. 可选地, 使用 strace 工具来监视程序发出的系统调用以及它们的返回值.跟踪一个 cp 或者一个 ls -l >/dev/scull0 展示了量子化的读和写. 监视(以及调试)技术在第 4 章详细介绍.

4.9 3.9. 快速参考

#include <linux/types.h>

dev_t

dev_t 是用来在内核里代表设备号的类型.

int MAJOR(dev_t dev);

int MINOR(dev_t dev);

从设备编号中抽取主次编号的宏.

dev_t MKDEV(unsigned int major, unsigned int minor);

从主次编号来建立 dev_t 数据项的宏定义.

#include <linux/fs.h>

"文件系统"头文件是编写设备驱动需要的头文件. 许多重要的函数和数据 结构在此定义.

int register_chrdev_region(dev_t first, unsigned int count,char *name)

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,unsigned int count,char *name)

void unregister_chrdev_region(dev_t first,unsigned int count);

允许驱动分配和释放设备编号的范围的函数. register_chrdev_region 应 当用在事先知道需要的主编号时; 对于动态分配, 使用 alloc_chrdev_region 代替.

int register_chrdev(unsigned int major,const char *name, struct file_operations *fops);

老的( 2.6 之前) 字符设备注册函数. 它在 2.6 内核中被模拟, 但是不应 当给新代码使用. 如果主编号不是 0, 可以不变地用它; 否则一个动态编 号被分配给这个设备.

int unregister_chrdev(unsignedint major,const char *name);

恢复一个由 register_chrdev 所作的注册的函数. major 和 name 字符串 必须包含之前用来注册设备时同样的值.

struct file_operations;

struct file;

struct inode;

大部分设备驱动使用的 3 个重要数据结构. file_operations 结构持有一个字符驱动的方法; struct file 代表一个打开的文件, struct inode代 表磁盘上的一个文件.

#include <linux/cdev.h>

struct cdev *cdev_alloc(void);

voidcdev_init(struct cdev *dev, struct file_operations *fops);intcdev_add(struct cdev *dev, dev_t num, unsigned int count);void cdev_del(struct cdev *dev);

cdev 结构管理的函数, 它代表内核中的字符设备.

#include <linux/kernel.h>

container_of(pointer, type, field);

 一个传统宏定义, 可用来获取一个结构指针, 从它里面包含的某个其他结 构的指针.

 #include <asm/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, unsignedlong count);



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值