在研究某一样东西的时候,其实最重要的一点就是了解清楚它的作用,它对自己有没有用,以前刚刚进入大学的时候总是听某某师兄师姐在讲某个课程没啥用,不用认真学,结果自己有时候也把某些人的某些话当真了,再结果我在学习某些课程的时候就没把它当回事,总认为以后没什么用,等到自己大四时才知道那些个所谓的师兄师姐们的脑袋都是被驴给踢过的,当发现最重的东西自己曾经视为无用时,为时晚也…所以在此建议读者以后搞任何东西前都先了解它的用处,对自己有没有用。如果真的是没有用的东西,又何必花费时间在这上面,还不如去当公务员呢。
一. 为什么我们有时会在内核级做driver?有时却在应用层做driver?
让我们一起来看《linux device driver》如何回答我们的。
一个第一次涉及内核问题的 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 功能给用户程序, 因此它们可以驱动它们自己的硬件.
一种在用户空间工作的情况可能是有意义的, 当你开始处理新的没有用过的硬件时. 这样你可以学习去管理你的硬件, 不必担心挂起整个系统. 一旦你完成了, 在一个内核模块中封装软件就会是一个简单操作了.
二. 从上面的经书中,我们应该可以了解清楚为什么我们要去关注应用层编写驱动程序了。OK,现在我们应该开始动手了。
1. 我们主要使用mmap函数将实际的硬件物理地址映射到用户空间的虚拟地址,这样,我们就可以在应用层对虚拟地址进行读写,实际将直接反映到物理地地址,至于mmap函数的原理,这里就不啰嗦了,请直接参考man手册。
2. 实例(这是本文的重点):
这里我主要实现两个函数,一个是映射函数,另一个则是映射的反过程,回收被映射的memory。
A. 地址映射:
一般我们将物理地址映射到某段虚拟地址可以采用两种策略,一种是一次性的把所要使用的物理地址全部映射到某段虚拟地址,让kernel自己给我们找一段可以映射的用户地址空间,并交给我返回,这种方式简单方便,当然也会浪费很多用户地址空间,因为可能会用的到地址并不是同时会用到,也并不是都会用到。另一种则是需要使用的时候再做个映射,返回一段被映射的地址空间,这种方式的好处就是不浪费地址空间,但是需要使用的时候再做映射。
前一种方式比较简单,这里我们就重点讲一下后一种方式,我也更倾向于采用后一种方式。
STEP(1):
构建一个链表,将我们做的地址映射的块串起来,一方面方便以后如果用到已被映射的地址空间时,则可以直接使用。另一方面,我们在做地址映射前检查地址空间是否经过了地址映射,避免重复映射导致异常。
STEP(2):
在进行一块地址映射前,我们需要先检索一下这个链表,看一下将要映射的地址空间是不是在这个区间,如果在,则可直接返回Start_Vir_addr,否则需要进行新的地址空间映射。Phy_addr是我们将要映射的物理地址,size则是要映射的大小。
STEP(3):
如果/dev/mem没有打开就打开。
STEP(4):
由于我们kernel的管理页表的最小单位是4K,则我们需要做4K的页面地址对齐操作。似乎现在并不用关心这点,因为Linux的mmap()的最后一 个形参offset并未强制要求页边界对齐,如果提供的值未对齐,系统自动向上舍入到页边界上。当然,我们做个对齐也无妨。
STEP(5):
地址映射,具体参数请参考man手册
STEP(6):
申请空间,添加新的地址空间到链表结点。
B. 回收射的地址空间。方法也是首先判断该地址是否在已映射的地址空间中,如果在,则将其引用次数减一,如果引用次数为0,则删除其结点并使用munmap回收被映射的地址。
三.使用
现在我们即可按照对应的地址偏移,使用虚拟地址进行IO口操作了。比如某个IO口地址是:0xFFFFF00f,则实际的虚拟地址可能是0x8424083c,则可以直接给0x8424083c赋值就可以达到对这个IO口置高或者置低。