块设备驱动程序:优化

作者:Michael K. Johnson

上个月,当我要开始长篇讨论优化时,我这个专栏的空间用完了。为了实用性,在继续讨论初始化之前,我将仅在此处提及最有用的优化。

我不会提供任何优化的示例代码实现,因为这些是复杂的问题,需要以特定于设备的方式处理,并且正如我在讨论中断驱动驱动程序时代码比介绍基本的初始设备驱动程序时更加模糊一样,如果我尝试在此处编写一些代码,那么它要么会大到占据整个杂志,要么会模糊到完全没用。我希望即使没有代码,我的解释对您也有用。

此外,我应该警告您,我谈论的优化虽然代表了常见的优化,但不一定代表您在 Linux 源代码中会找到的任何内容,除非我明确声明。我正在稍微更理论的层面上写作——关于可以做什么,而不是已经做了什么。在概念上与我写的内容相似的事情已经完成,但细节是我自己的,可能不是处理事情的最佳方式。像往常一样,使用此专栏作为内核源代码的介绍,并阅读实际源代码以获得比我在此处可以给您的更多的见解。

如果您渴望代码并且不关心优化,请直接跳到本文的第二部分,我在其中讨论初始化。

合并

一种常见的优化是合并相邻请求。这意味着当驱动程序被通知或注意到请求已添加到请求队列时,它会查看请求列表,看看是否有针对下一个块(以及可能超出该块的更多块)的请求。如果是这样,它会向硬件发送一个请求,用一个命令读取多个块,当数据从硬件传入时(大概是在中断服务例程中),它会在对任一请求调用 end_request(1)(实际上,是专为该驱动程序设计的类似函数)之前填充这两个请求。在满足(或未能满足)请求后,将为每个请求调用等效于 end_request() 的函数,但在中断得到满足之前,不会唤醒等待任一请求的进程。

这将要求您编写自己的 end_request() 版本。虽然这听起来可能令人生畏,但它并不像听起来那么难,因为您可以几乎原样使用它。例如,您可以逐字复制它,除了在末尾不执行 wake_up(&wait_for_request) 之外,您可以将 wait_for_request 添加到事件列表中,以便在您准备好时唤醒。然后,您将在完成处理每个请求后立即调用这个新的 almost_end_request() 函数。当您完成处理整个中断并准备好唤醒进程时,迭代事件列表,依次对每个事件调用 wake_up(),从第一个满足到最后一个满足。

请注意,wake_up() 不会直接导致上下文切换。驱动程序在对正在唤醒的进程运行 wake_up() 时不会放弃控制。相反,wake_up() 使所有正在唤醒的进程“可运行”,并设置 need_resched 标志。此标志表示调度程序应该在下一个方便的时间被调用,例如从“慢速”中断处理例程(包括时钟处理例程)返回时或从系统调用返回时。这意味着驱动程序不会因调用 wake_up() 而被抢占,因此它将能够唤醒所有必要的进程而不会被抢占。

这可能需要多次尝试才能正确。我能提供的唯一帮助是“确保您有备份。真的。”

我注意到的 Linux 内核中唯一执行类似操作的驱动程序是软盘驱动程序;磁道缓冲区以类似的方式工作,其中可以通过发送到硬件的单个读取命令满足多个请求。如果您有兴趣调查其工作原理,请阅读 drivers/block/floppy.c 并搜索 floppy_track_buffer 并阅读整个函数 make_raw_rw_request()

分散-聚集

听起来像个“浪费公帑的项目”,不是吗?分散-聚集在概念上可能与合并相邻请求有点相似,但与更智能的硬件一起使用,并且可能更容易实现。“分散”部分意味着当有多个块要写入磁盘各处(例如)时,会发出一个命令来启动写入所有这些不同扇区,从而将协商中涉及的开销从 O(n) 减少到 O(1),其中 n 是要写入的块或扇区数。

类似地,“聚集”部分意味着当有多个块要读取时,会发出一个命令来启动读取所有块,并且当磁盘传入每个块时,相应的请求会被标记为已满足,使用 end_request(1) 或等效的特定于设备的代码。只有当每个读取或写入的块都导致生成单独的中断时,您才能轻松地将 end_request() 未修改地与分散-聚集一起使用,甚至可能并非如此。SCSI 驱动程序有自己的做法,这可能是最好的方法。

如果您想提高吞吐量,但略微牺牲响应时间,您可以使用计时器来帮助:当您的 request() 被通知有请求,并看到只有一个未完成的请求时,它可以设置一个计时器很快到期(也许是十分之一或二十分之一秒),假设在等待期间,会有更多请求涌入以进行处理,并且当达到一定数量的请求,或者计时器到期时,以先到者为准,分散-聚集将应用于这些块。如果调用 request() 例程并注意到已累积“足够”数量(无论多少...)的请求,它将卸载计时器并处理这些请求。如果计时器到期,则将处理所有请求。

请注意,使用的计时器不应与用于硬件超时的静态计时器相同。相反,它应该是动态分配的计时器。有关动态计时器例程的详细信息,请参阅 <linux/timer.h>

我将重复我的标准免责声明:这是简化的(至少,我正在尝试简化它...),如果您想要更详细和正确的信息,请研究真实的驱动程序。SCSI 高级驱动程序(scsi.c、sd.c、sr.c)绝对是开始的地方。(我没有提及 st.c 和 sg.c,因为它们是字符设备,而不是块设备。)

错误

优化几乎很容易写,听起来也很迷人,但错误处理才是真正考验实力的地方。我甚至不打算在本专栏中真正涵盖错误处理。这是一个非常复杂的主题,并且对于每个硬件都不同。如果您已经读到这里,您应该已经掌握了开始阅读内核中块设备驱动程序所需的基本知识。通过阅读那里的所有错误处理,您将开始了解如何漂亮地进行错误处理。考虑按以下顺序阅读驱动程序:ramdisk.c、hd.c 和 floppy.c。

当您编写驱动程序时,您可能会从非常简单的错误处理开始。当您使用您的驱动程序时,您可能会发现更多要处理的错误情况。有时您会发现您需要重新设计驱动程序的某些组件以使错误处理正确。例如,直到最近,软盘驱动程序中的错误处理还不是很好。您可以以读写方式挂载写保护软盘,然后在尝试写入软盘时导致严重问题。此外,如果您尝试(例如)在写保护软盘上写入 tar 存档,您会收到一连串错误,因为驱动程序重置软盘驱动器并一直假设错误会消失。软盘驱动程序经过重大重写才正确解决了该问题。

初始化

这实际上并非特定于块设备驱动程序,但肯定有必要了解。在第 9 期中,我给出了一个示例初始化函数,但它主要是伪代码,并没有涵盖您需要执行的许多与真实设备一起工作的事情。例如,它没有解释如何获取 DMA 通道,也没有解释如何获取 IRQ 线。

处理 IRQ 和 DMA 最简单的方法是在初始化设备时分配 IRQ 线和 DMA 通道。这不是最好的方法,但它是您在编写设备驱动程序时开始的最简单方法。当您想弄清楚何时分配和释放它们时,您可以阅读其他设备驱动程序。例如,软盘设备驱动程序具有函数 floppy_grab_irq_and_dma()floppy_release_irq_and_dma(),它们的功能正如其名称所示,不仅在初始化代码中使用,而且在驱动程序的其余部分也使用。

floppy_grab_irq_and_dma() 函数是开始学习如何分配 IRQ 线和 DMA 通道的好地方。根据 <asm/dma.h>,IRQ 线应首先分配,最后释放。

我们先来看看 IRQ。request_irq() 接受四个参数。request_irq() 的第一个参数是要分配的 IRQ 线号。第二个是收到中断时要调用的中断服务例程。第三个是一个标志,可以设置为 SA_INTERRUPT 或其他值(大概是 0),它决定传递给中断服务例程的参数是指向寄存器结构的指针 (0) 还是中断号 (SA_INTERRUPT),以及中断是“快速”中断处理程序 (SA_INTERRUPT) 还是“慢速”中断处理程序 (0)。“慢速”中断处理程序是指中断处理程序返回时会完成更多处理的程序,包括可能运行调度程序以选择一个新进程成为活动进程。“快速”中断处理程序尽可能少做。第四个参数是设备驱动程序的名称。

request_dma() 更简单。它接受两个参数,第一个参数是 DMA 通道,第二个参数是设备驱动程序的名称。

相应的释放函数甚至更简单。free_dma() 仅接受 DMA 通道号,而 free_irq() 仅接受 IRQ 号。

当然,使用 IRQ,尤其是使用 DMA,不仅仅是分配和释放线路和通道,但这只是一个开始。吹毛求疵地说,这只是一个开始。阅读 <asm/dma.h>、kernel/dma.c、<asm/irq.h> 和 kernel/irq.c 以了解详细信息;它们非常易读,并且有很多有用的注释。

邮件

K. D. Nguyen 在阅读了 1 月份的 Linux Journal 后给我发了一些电子邮件,回应了我也从其他人那里听到的愿望。

自上一期以来,我一直在阅读两本关于 Unix 设备驱动程序的书,KHG 和 Kernel Korner 文章。我觉得我可以编写一些设备驱动程序。但不幸的是,关于 Unix 设备驱动程序的所有书籍和文章似乎都缺少一些东西。那就是缺乏实践环境。我们这些设备驱动程序初学者只能阅读和查看 Linux 下的一些设备驱动程序代码,并尝试理解它们是如何工作的。如果有一些硬件或设备套件让我们真正就我们刚刚阅读的关于编写 Unix 设备驱动程序的内容进行一些练习(而不是购买新的彩色打印机,然后恳求制造商提供规范来编写新的具有挑战性的设备驱动程序),那将会更有趣。当然,目前,我会继续阅读。

没有真正的实践环境。开始学习的最简单方法是编写 ramdisk 驱动程序。除此之外,许多真实设备实际上相当简单。深入研究吧!当您为单内核操作系统编写内核代码时,很难在水边涉足,无论您是为简单的 ramdisk 编写代码还是为玩具设备套件或真实设备编写代码。学习曲线非常陡峭,但这意味着在短时间内进行艰苦的学习,您真的可以掌握编写基本设备驱动程序所需的大部分知识。

此外,实践设备将支持哪些功能,它会做什么?这是一个难以回答的问题,我也不打算尝试回答——而且我认为制造商也不会。由于您为实践设备编写驱动程序与为真实设备编写驱动程序一样有可能搞砸您的整个系统,因此您不妨在真实设备上工作。确实 有像任何玩具设备一样简单的真实设备。

其他资源

Michael K. JohnsonLinux Journal 的编辑,也是 Linux 内核黑客指南 (KHG) 的作者。他正在使用此专栏来开发和扩展 KHG。

加载 Disqus 评论