Linux 内核驱动接口¶
(你的所有问题都得到了解答,甚至更多)
Greg Kroah-Hartman <greg@kroah.com>
本文旨在解释为什么 Linux 没有二进制内核接口,也没有稳定的内核接口。
注意
请注意,本文描述的是内核内部接口,而不是内核到用户空间的接口。
内核到用户空间的接口是应用程序使用的接口,即系统调用接口。该接口随时间推移非常稳定,不会被破坏。我有一些在 0.9 版本以前的内核上构建的旧程序,它们在最新的 2.6 内核版本上仍然运行良好。用户和应用程序程序员可以信赖这个接口的稳定性。
执行摘要¶
你认为你想要一个稳定的内核接口,但实际上你并不想要,你甚至都不知道。你想要的是一个稳定运行的驱动程序,只有当你的驱动程序在主内核树中时才能实现。如果你的驱动程序在主内核树中,你还会获得许多其他好处,所有这些都使得 Linux 成为一个强大、稳定和成熟的操作系统,这也是你最初使用它的原因。
引言¶
只有那些想要编写内核驱动程序的人才需要担心内核内部接口的变化。对于世界上绝大多数人来说,他们既看不到这个接口,也根本不关心它。
首先,我不会讨论任何关于闭源、隐藏源代码、二进制大文件(binary blobs)、源代码封装或任何其他描述其源代码未在 GPL 下发布的内核驱动程序的法律问题。如果您有任何法律问题,请咨询律师,我是一名程序员,因此,我只在这里描述技术问题(并非要轻视法律问题,它们是真实存在的,您确实需要时刻注意它们)。
所以,这里有两个主要议题:二进制内核接口和稳定内核源代码接口。它们彼此依赖,但我们先讨论二进制部分,以将其解决。
二进制内核接口¶
假设我们有一个稳定的内核源代码接口,那么一个二进制接口也会自然而然地出现,对吗?错了。请考虑以下关于 Linux 内核的事实
根据您使用的 C 编译器版本,不同的内核数据结构将包含不同的结构对齐方式,并且可能以不同方式包含不同的函数(将函数内联或不内联)。单个函数组织并不那么重要,但不同的数据结构填充非常重要。
根据您选择的内核构建选项,内核可能会做出各种不同的假设
不同的结构可能包含不同的字段
某些功能可能根本未实现(例如,对于非 SMP 构建,某些锁会编译为空)。
内核中的内存可以以不同的方式对齐,具体取决于构建选项。
Linux 运行在各种不同的处理器架构上。来自一种架构的二进制驱动程序无法在另一种架构上正常运行。
现在,可以通过简单地为精确特定的内核配置编译您的模块,并使用构建内核的完全相同的 C 编译器来解决其中一些问题。如果您想为特定 Linux 发行版的特定发布版本提供模块,这已经足够了。但是,将单个构建乘以不同 Linux 发行版的数量以及 Linux 发行版支持的不同发布版本的数量,您很快就会遇到不同发布版本上不同构建选项的噩梦。还要意识到,每个 Linux 发行版发布都包含许多不同的内核,所有这些内核都针对不同的硬件类型(不同的处理器类型和不同的选项)进行了调整,因此即使对于单个发布版本,您也需要创建模块的多个版本。
相信我,如果你试图支持这种发布方式,久而久之你会发疯的,我很久以前就以这种艰难的方式学到了这一点……
稳定内核源代码接口¶
如果你与那些试图长期维护不在主内核树中的 Linux 内核驱动程序的人交流,这是一个更“不稳定”的话题。
Linux 内核开发是持续且高速进行的,从未放慢脚步。因此,内核开发者会发现当前接口中的错误,或找出更好的方法来处理问题。如果他们这样做,他们就会修复当前接口以使其更好地工作。当他们这样做时,函数名称可能会改变,结构可能会增大或缩小,函数参数可能会被重新调整。如果发生这种情况,内核中所有使用此接口的实例都会同时得到修复,确保一切继续正常工作。
作为具体示例,内核内部的 USB 接口在该子系统的生命周期内至少经历了三次不同的重构。这些重构是为了解决许多不同的问题
从数据流的同步模型更改为异步模型。这降低了许多驱动程序的复杂性,并提高了所有 USB 驱动程序的吞吐量,使我们现在几乎所有 USB 设备都能以其可能的最大速度运行。
在 USB 驱动程序从 USB 核心分配数据包的方式上进行了更改,这样所有驱动程序现在都需要向 USB 核心提供更多信息,以修复一些已知的死锁问题。
这与一些闭源操作系统形成鲜明对比,这些操作系统不得不长期维护其旧的 USB 接口。这使得新开发者有可能意外使用旧接口并以不正确的方式行事,从而导致操作系统的稳定性下降。
在这两种情况下,所有开发者都同意这些都是需要进行的重要更改,并且它们是在相对较小的痛苦下完成的。如果 Linux 必须确保它将保留一个稳定的源代码接口,那么就会创建一个新接口,而旧的、损坏的接口将不得不长期维护,从而给 USB 开发者带来额外的工作。由于所有 Linux USB 开发者都在自己的时间内工作,要求程序员免费做额外的工作而没有收益是不可能的。
安全问题对 Linux 来说也非常重要。当发现安全问题时,它会在很短的时间内得到修复。很多次,这导致了内部内核接口的重构,以防止安全问题的发生。当这种情况发生时,所有使用这些接口的驱动程序也会同时得到修复,确保安全问题得到解决,并且将来不会意外地再次出现。如果内部接口不允许更改,那么修复此类安全问题并确保其不再发生将是不可能的。
内核接口会随着时间的推移进行清理。如果当前没有人在使用某个接口,它就会被删除。这确保了内核尽可能小,并且所有潜在的接口都得到了尽可能好的测试(未使用的接口几乎不可能测试其有效性)。
该怎么办¶
那么,如果你有一个不在主内核树中的 Linux 内核驱动程序,你作为一个开发者该怎么办?为每个不同发行版的每个不同内核版本发布二进制驱动程序是一个噩梦,而且试图跟上不断变化的内核接口也是一项艰巨的工作。
很简单,将你的内核驱动程序纳入主内核树(请记住,我们这里谈论的是在 GPL 兼容许可下发布的驱动程序,如果你的代码不属于这一类,祝你好运,你自己搞定吧,你这个寄生虫)。如果你的驱动程序在主内核树中,并且内核接口发生了变化,那么最初进行内核更改的人会负责修复它。这确保了你的驱动程序始终可构建,并且随着时间的推移正常工作,而你只需付出很少的努力。
将您的驱动程序纳入主内核树的非常好的副作用是:
驱动程序的质量将提高,因为维护成本(对原始开发者而言)将降低。
其他开发者会为您的驱动程序添加功能。
其他人会发现并修复您驱动程序中的错误。
其他人会在您的驱动程序中找到优化机会。
当外部接口需要更改时,其他人会为您更新驱动程序。
驱动程序会自动随所有 Linux 发行版一起发布,而无需请求发行版添加它。
由于 Linux 支持“开箱即用”的设备数量比任何其他操作系统都多,并且它在比任何其他操作系统更多种处理器架构上支持这些设备,这种经过验证的开发模式一定做对了什么 :)
感谢 Randy Dunlap、Andrew Morton、David Brownell、Hanna Linder、Robert Love 和 Nishanth Aravamudan 对本文早期草稿的审阅和评论。