内核角
内核所需的大部分抽象功能已存在于 Linux 内核中。 Linux 拥有类 Unix 操作系统中设计最佳的缓冲区缓存之一。 内存管理和网络层还有一些工作需要完成,但随着越来越多的开发工作完成,似乎(而且合乎情理地)能够由几乎不具备操作系统知识和经验的人完成的项目越来越少。 那些知道足够多的知识来了解内存管理需要做什么的人,不需要阅读关于内核如何工作的文档; 他们中的许多人发现直接阅读 Linux 源代码更快。
这并不是说 Linux 社区中初学者可以做的项目很少; 实际上有很多,但这些项目大多不在内核本身之内。
相对初学者可以完成的一个项目,而且永远不会过时,是为新硬件编写设备驱动程序。 仍然有一些硬件 Linux 不支持,制造商一直在发布新硬件,Linux 用户购买硬件,然后希望将其与 Linux 一起使用。
幸运的是,用于编写设备驱动程序的接口相对简单明了。 我所说的“明了”是指规则没有太多例外,也不需要耍花招才能使事情正常运行。 在接下来的几栏中(“几栏”是相对的,我可能会花上几年时间...),我将介绍您需要了解的编写各种设备驱动程序的信息。
其中一些信息已在 Linux 内核黑客指南 中,但我将在此处扩展这些信息。 当我在这里写一些新的东西时,它最终会进入 内核黑客指南 中; 这是 Linux Journal 支持 Linux 文档项目的方式之一。
几乎以一种矛盾的方式,我将以描述如何(以及何时!)将设备驱动程序作为用户程序来实现来启动这个 内核角 专栏。
向 Linux 内核添加代码的首要规则是不要添加。 内核中的代码无法交换,因此会占用宝贵的内存,无论当时是否正在使用。 许多硬件设备可以由用户空间程序驱动,这些程序在设备不使用时会被很好地放在一边(交换出去或根本不运行)。 以这种方式实现的设备的一个主要示例是视频卡。
虽然 Linux 启动代码具有初始化许多不同类型显卡的视频模式的选项,但 Linux 内核本身中对视频卡的实际设备支持非常有限,仅包括在单色(hercules 风格)或彩色(CGA、EGA、VGA 及更高版本)显卡上显示文本的支持。 不包括对图形的支持。
XFree86 在 X 服务器中为许多图形卡提供用户级驱动程序。 这些驱动程序仅在 X 服务器运行时加载,并且当前未使用的部分可以在必要时交换出去。 此外,通过不使设备使用系统调用接口进行写入,这些驱动程序速度更快,因为它们是在用户空间中实现的。
当然,有些驱动程序无法编写为用户空间驱动程序:最常见的是需要可以处理中断的驱动程序的硬件。 我们将在以后的文章中讨论这些。
设备驱动程序与硬件通信的最常见方式(至少在 PC 架构上)可能是通过 I/O 总线。 这条总线与内存总线完全分离,并且通过特殊的机器指令进行访问。 举一个具体的例子,让我们使用并行端口。 并行端口驱动程序位于内核中有三个原因:它可以是中断驱动的,它可以管理争用,并且它在历史上一直是内核的一部分。 它也相当小巧且非常常见,因此不会使内核过度膨胀。 但是,可以从用户空间驱动并行端口。 让我们看看如何做到这一点。
并行端口在 I/O 总线上有三个地址,它们由一个基地址和两个偏移量指定。 这对于设备来说很常见; 许多设备有多个基地址可供选择,并且使用的任何其他端口都指定为相对于基地址的偏移量。 并行端口的三个基地址在 linux/lp.h 中给出,并且是(十六进制)Ox3bc、Ox378 和 Ox278。 状态端口是其上方的下一个端口,控制端口在状态端口上方。 因此,如果写入字符的基 I/O 端口是 Ox378,则状态端口是 Ox379,控制端口是 Ox380。
设备驱动程序与硬件通信的最常见方式……是通过 1/0 总线。
您需要足够的设备文档才能知道如何与它通信。 8255 芯片是并行端口所基于的芯片,并且该芯片和并行端口接口的文档描述了这三个端口。
状态端口在读取时可以报告几种情况
位 | 条件 |
Ox80 | 如果为 0,则打印机忙 |
Ox40 | 如果为 0,则打印机已确认接收到发送的字符 |
Ox20 | 如果为 1,则打印机缺纸 |
OxlO | 如果为 1,则打印机在线 |
Ox08 | 如果为 0,则打印机处于错误状态 |
控制端口在写入时控制几个方面
位 | 条件 |
OxlO | 设置为 1 以启用在打印机准备就绪时发送中断 |
Ox08 | 设置为 1 以告诉打印机准备好通话 |
Ox04 | 设置为 0 以告诉打印机初始化自身 |
OxO1 | 设置为 1 以准备向打印机发送另一个字节 |
不幸的是,并非所有打印机都对可以发送的所有信号达成一致,因此必须使用最小公分母。 这意味着我不会使用您在表格中看到的所有位。 此外,我显然不会使用中断使能位,因为中断不能从用户级程序中使用。
我也不会进行任何认真的错误检测; 我想展示编写一个(或多或少)工作的简单驱动程序可以有多么简单。 如果您想了解如何处理错误检测,只需阅读 Linux 内核源代码中的 include/linux/lp.h 和 drivers/char/lp.c。
程序 userlp.c(参见侧边栏)需要通过启用优化进行编译,并以 root 用户身份(或 setuid root)运行才能工作。 它从标准输入中获取文件,并将其打印到命令行上指定的打印机:O、1 或 2,分别对应于 lpO、lpi 和 lp2。
与标准内核驱动程序不同,这仅在一台打印机上进行了轻微测试,因此我不能说它会在您的打印机上工作。 这没关系,因为这只是一个例子。 请注意,我可以将相同的驱动程序编写为使用 /dev/port 和 dd 的 /bin/sh 脚本,并且可能在更短的时间内完成,但您更有可能用 C 而不是 /bin/sh 编写设备驱动程序。
为了使用 inb_p() 和 ou tb_b(),我不仅必须通过启用优化进行编译并使用 ioperm() 来允许访问这些端口,还必须使用 ioperm() 来允许访问端口 Ox80。 这是因为 *b_p () 函数使用端口 Ox80 来减慢端口访问速度。
我也很幸运,因为我的所有端口都小于 Ox3ff。 要访问高于 Ox3ff 的端口,您要么需要使用 /dev/port(如下所述),要么为了最快的访问速度,使用 iopl() 函数将您的 I/O 保护级别设置为“ring 3”,这与内核相同。 这很不幸(尽管有充分的理由;如果您关心,请阅读 kernel/ioport.h),因为这意味着您可以访问任何端口,并且如果您由于某些编程错误访问了错误的端口,您可能会更容易地搞砸整个机器。 想象一下,如果您的程序意外地将“随机”值写入控制硬盘驱动器的 I/O 端口之一会发生什么。 在“ring 3”中,代码几乎与内核一样强大,因此用户级驱动程序的一个优势消失了。
如果您要执行像使用 iopl() 将您的代码放入 ring 3 这样危险的操作,您可能应该知道如何阅读内核源代码,因此我将简单地将您推荐给 kernel/ioport.h 以了解详细信息。 系统调用在内核中称为 sys_name,因此请查找 sys iopl()。
请注意,我使用了 ioperm() 函数来使用 inb_p() 和 outb_b() 函数直接从端口读取和写入端口,并且此函数要求代码以 root 用户身份运行。 另一种选择是从 /dev/port 读取和写入。 这有点慢,但具有代码不需要 root 权限即可运行的优点; 只需对 /dev/port 具有读取和写入权限即可。 只需使用 lseek() 寻址到您要读取或写入的端口地址,然后 read() 或 write() 单个字节到文件即可。 如果您想再次读取或写入,则需要再次使用 lseek()。 如果您创建一个名为 port 的组,并使 /dev/port 可由组 port 读取和写入,那么组 port 中的任何用户都可以使用以这种方式编写的用户空间设备驱动程序,而无需程序是 setuid root。
访问 /dev/port 的另一种方法是使用 mmap() 将其映射到某些内存空间。 然后,您可以直接在将端口映射到的内存地址处写入端口。 请参阅下面的关于内存映射的部分,了解如何映射文件; 详细信息(文件名除外)是相同的。 由于 perl 可以使用 mmap() 调用,因此可以使用 perl 脚本编写访问 /dev/port 和 /dev/mem 的设备驱动程序。
其他设备可能需要在物理内存中的某个位置进行访问。 前 3GB 的物理内存(如果您有超过该内存并且不知道如何访问第 4 个 GB,我不会同情您...)可以通过 /dev/mem 访问。 侧边栏(在第 20 页左侧)给出了来自 svgalib 的 mmap() 代码的粗略版本,与 XFree86 一样,它是用于视频卡的用户空间设备驱动程序
代码首先打开 /dev/mem,然后分配足够的内存以映射到它想要的 /dev/mem 部分,然后将 /dev/mem 映射到已分配的内存之上。 一旦成功完成此操作,无论该进程何时写入或读取该内存,它都是在写入或读取映射到 /dev/mem 的地址处的物理内存。
由于 perl 可以使用 mmap() 调用,因此可以使用 perl 脚本编写访问 /dev/port 和 /dev/mem 的设备驱动程序。 如果您尚未使用 perl,则可能不值得这样做,但如果您确实使用 perl,您可能会觉得这个想法很有趣。 如果您尝试这样做,我想知道它对您有何作用,如果您有任何提示,我可能会将其传递给本专栏的读者。 同样,从技术上讲,可以使用 shell 脚本编写设备驱动程序(尽管在实践中“过于聪明”且相当慢),方法是使用 dd 读取和写入端口。 为了与众不同,我研究了这样一个驱动程序,发现主要问题是缺乏二进制按位运算和缺乏真正的二进制数据。 我不分发这个 shell 脚本; 任何认真对待以这种方式玩耍的人都可以根据本专栏中介绍的 userlp.c 文件自行编写。 如果您使其可靠地工作,请通知我,我可能会在未来的“内核角”中打印您的版本。
Michael K. Johnson 是 Linux Journal 的编辑,也是 Linux Kernel Hackers' Guide 的作者。 欢迎您的评论。