读一本书:LINUX设备驱动程序(1)设备驱动简介

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

1.1驱动程序的角色

作为一个程序员, 你能够对你的驱动作出你自己的选择, 并且在所需的编程时间和结果的
灵活性之间, 选择一个可接受的平衡. 尽管说一个驱动是"灵活"的, 听起来有些奇怪, 但
是我们喜欢这个字眼, 因为它强调了一个驱动程序的角色是提供机制, 而不是策略.
机制和策略的区分是其中一个在 Unix 设计背后的最好观念. 大部分的编程问题其实可以
划分为 2 部分:" 提供什么能力"(机制) 和 "如何使用这些能力"(策略). 如果这两方面
由程序的不同部分来表达, 或者甚至由不同的程序共同表达, 软件包是非常容易开发和适
应特殊的需求.


例如, 图形显示的 Unix 管理划分为 X 服务器, 它理解硬件以及提供了统一的接口给用
户程序, 还有窗口和会话管理器, 它实现了一个特别的策略, 而对硬件一无所知. 人们可
以在不同的硬件上使用相同的窗口管理器, 而且不同的用户可以在同一台工作站上运行不
同的配置. 甚至完全不同的桌面环境, 例如 KDE 和 GNOME, 可以在同一系统中共存. 另
一个例子是 TCP/IP 网络的分层结构: 操作系统提供 socket 抽象层, 它对要传送的数据
而言不实现策略, 而不同的服务器负责各种服务( 以及它们的相关策略). 而且, 一个服
务器, 例如 ftpd 提供文件传输机制, 同时用户可以使用任何他们喜欢的客户端; 无论命
令行还是图形客户端都存在, 并且任何人都能编写一个新的用户接口来传输文件.
在驱动相关的地方, 机制和策略之间的同样的区分都适用. 软驱驱动是不含策略的--它的
角色仅仅是将磁盘表现为一个数据块的连续阵列. 系统的更高级部分提供了策略, 例如谁
可以存取软驱驱动, 这个软驱是直接存取还是要通过一个文件系统, 以及用户是否可以加
载文件系统到这个软驱. 因为不同的环境常常需要不同的使用硬件的方式, 尽可能对策略
透明是非常重要的.


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


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


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


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

1.2.内核划分 

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


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


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


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


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

1.2.1 可加载模块

Linux 的众多优良特性之一就是可以在运行时扩展由内核提供的特性的能力. 这意味着你
可以在系统正在运行着的时候增加内核的功能( 也可以去除 ).
每块可以在运行时添加到内核的代码, 被称为一个模块. Linux 内核提供了对许多模块类
型的支持, 包括但不限于, 设备驱动. 每个模块由目标代码组成( 没有连接成一个完整可
执行文件 ), 可以动态连接到运行中的内核中, 通过 insmod 程序, 以及通过 rmmod 程
序去连接
 

1.3. 设备和模块的分类

以 Linux 的方式看待设备可区分为 3 种基本设备类型. 每个模块常常实现 3 种类型中
的 1 种, 因此可分类成字符模块, 块模块, 或者一个网络模块.

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

1.4 安全问题

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


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


在可能时, 驱动编写者应当避免将安全策略编到他们的代码中. 安全是一个策略问题, 最
好在内核高层来处理, 在系统管理员的控制下. 但是, 常有例外.
作为一个设备驱动编写者, 你应当知道在什么情形下, 某些类型的设备存取可能反面地影
响系统作为一个整体, 并且应当提供足够地控制. 例如, 会影响全局资源的设备操作( 例
如设置一条中断线 ), 可能会损坏硬件( 例如, 加载固件 ), 或者它可能会影响其他用户
( 例如设置一个磁带驱动的缺省的块大小 ), 常常是只对有足够授权的用户, 并且这种检
查必须由驱动自身进行.


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


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


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


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


 


 

  

 
参考文档:LINUX设备驱动程序(第三版)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值