Linux 与 Windows 设备驱动程序模型:架构、API 和构建环境比较
设备驱动程序是操作系统的一部分,它通过某些编程接口促进硬件设备的使用,以便软件应用程序可以控制和操作设备。由于每个驱动程序都特定于特定操作系统,因此您需要单独的 Linux、Windows 或 Unix 设备驱动程序才能在不同计算机上使用您的设备。这就是为什么在雇用驱动程序开发人员或选择研发服务提供商时,重要的是查看他们为各种操作系统平台开发驱动程序的经验。

驱动程序开发的第一步是了解每个操作系统处理其驱动程序的方式、其使用的底层驱动程序模型和体系结构以及可用的开发工具的差异。例如,Linux 驱动程序模型与 Windows 驱动程序模型有很大不同。 Windows 促进了驱动程序开发和操作系统开发的分离,并通过一组 ABI 调用将驱动程序和操作系统结合在一起,而 Linux 设备驱动程序开发不依赖于任何稳定的 ABI 或 API,而是将驱动程序代码合并到内核中。这些模型都有自己的优点和缺点,但如果您想为您的设备提供全面的支持,了解所有这些模型非常重要。
在本文中,我们将比较 Windows 和 Linux 设备驱动程序,并探讨它们在架构、API、构建开发和分发方面的差异,希望能让您深入了解如何开始为每个操作系统编写设备驱动程序。
1. 设备驱动架构
Windows 设备驱动程序体系结构与 Linux 驱动程序中使用的体系结构不同,两者都有自己的优缺点。差异主要是由于 Windows 是闭源操作系统而 Linux 是开源操作系统。 Linux 和 Windows 设备驱动程序架构的比较将帮助我们了解 Windows 和 Linux 驱动程序背后的核心差异。
1.1. Windows驱动程序架构
Linux 内核本身附带了驱动程序,而 Windows 内核不包含设备驱动程序。相反,现代 Windows 设备驱动程序是使用 Windows 驱动程序模型 (WDM) 编写的,它完全支持即插即用和电源管理,以便可以根据需要加载和卸载驱动程序。
来自应用程序的请求由 Windows 内核中称为 IO 管理器的部分处理,该部分将它们转换为 IO 请求数据包 (IRP),用于识别请求并在驱动程序层之间传送数据。
WDM提供三种驱动程序,形成三层:
过滤器驱动程序提供可选的 IRP 附加处理。
功能驱动程序是实现各个设备接口的主要驱动程序。
总线驱动程序为托管设备的各种适配器和总线控制器提供服务。
IRP 在从 IO 管理器向下传输到硬件时会通过这些层。每层都可以自己处理 IRP 并将其发送回 IO 管理器。底层是硬件抽象层(HAL),它为物理设备提供通用接口。
1.2. Linux驱动架构
Linux 设备驱动程序体系结构与 Windows 设备驱动程序体系结构的核心区别在于 Linux 没有标准的驱动程序模型或清晰的层分离。每个设备驱动程序通常被实现为一个可以动态加载和卸载到内核中的模块。 Linux 提供了即插即用支持和电源管理的方法,以便驱动程序可以使用它们来正确管理设备,但这不是必需的。
模块导出它们提供的函数,并通过调用这些函数并传递任意数据结构来进行通信。来自用户应用程序的请求来自文件系统或网络级别,并根据需要转换为数据结构。模块可以堆叠成层,依次处理请求,其中一些模块为 USB 设备等设备系列提供通用接口。
Linux 设备驱动程序支持三种设备:
实现字节流接口的字符设备。
托管文件系统并使用多字节数据块执行 IO 的块设备。
网络接口,用于通过网络传输数据包。
Linux 还有一个硬件抽象层,充当设备驱动程序实际硬件的接口。
2. 设备驱动API
Linux 和 Windows 驱动程序 API 都是事件驱动的:驱动程序代码仅在发生某些事件时执行:或者当用户应用程序想要从设备中获取某些内容时,或者当设备需要向操作系统告知某些内容时。
2.1.初始化
在 Windows 上,驱动程序由 DriverObject
结构表示,该结构在执行 DriverEntry
函数期间初始化。该入口点还注册了许多回调,以对设备添加和删除、驱动程序卸载以及处理传入的 IRP 做出反应。当设备连接时,Windows 会创建一个设备对象,该设备对象代表设备驱动程序处理所有应用程序请求。
与 Windows 相比,Linux 设备驱动程序的生命周期由内核模块的 module
_init
和 module
_exit
函数管理,这些函数在模块加载或卸载时调用。它们负责注册模块以使用内部内核接口处理设备请求。该模块必须创建一个设备文件(或网络接口),指定它希望管理的设备的数字标识符,并注册用户与设备文件交互时要调用的多个回调。
2.2.命名和声明设备
在 Windows 上注册设备
Windows 设备驱动程序在其 AddDevice
回调中收到有关新连接设备的通知。然后,它继续创建一个设备对象,用于标识该设备的该特定驱动程序实例。根据驱动程序类型,设备对象可以是物理设备对象 (PDO)、功能设备对象 (FDO) 或筛选设备对象 (FIDO)。设备对象可以堆叠起来,底层有一个PDO。
设备对象在设备连接到计算机的整个过程中都存在。 DeviceExtension
结构可用于将全局数据与设备对象相关联。
设备对象可以具有 DeviceDeviceName 形式的名称,系统使用这些名称来识别和定位它们。应用程序使用 CreateFile API 函数打开具有此类名称的文件,获取句柄,然后可使用该句柄与设备进行交互。
然而,通常只有 PDO 具有不同的名称。可以通过设备类接口访问未命名的设备。设备驱动程序注册一个或多个由 128 位全局唯一标识符 (GUID) 标识的接口。然后,用户应用程序可以使用已知的 GUID 获取此类设备的句柄。
在 Linux 上注册设备
在 Linux 上,用户应用程序通过文件系统条目访问设备,通常位于 /dev
目录中。该模块在模块初始化期间通过调用 register
_chrdev
等内核函数创建所有必需的条目。应用程序发出开放系统调用来获取文件描述符,然后使用该文件描述符与设备进行交互。然后,此调用(以及带有返回描述符的进一步系统调用,例如 read
、write
或 close
)被分派到由模块安装到 file_operations
或 block_device_operations
等结构中的回调函数。
设备驱动程序模块负责分配和维护其操作所需的任何数据结构。传递到文件系统回调的 file
结构有一个 private_data
字段,可用于存储指向驱动程序特定数据的指针。块设备和网络接口 API 也提供类似的字段。
应用程序使用文件系统节点来定位设备,而 Linux 使用主编号和次编号的概念在内部识别设备及其驱动程序。 主编号用于标识设备驱动程序,而次编号由驱动程序用于标识由其管理的设备。驱动程序必须注册自己才能管理一个或多个固定的主号码,或者要求系统为其分配一些未使用的号码。
目前,Linux 对主次对使用 32 位值,为主次对分配 12 位,允许最多 4096 个不同的驱动程序。字符设备和块设备的主次对是不同的,因此字符设备和块设备可以使用同一对而不会发生冲突。网络接口由诸如 eth0 之类的符号名称来标识,这些名称又与字符设备和块设备的主次编号不同。
2.3.交换数据
Linux 和 Windows 都支持三种在用户级应用程序和内核级驱动程序之间传输数据的方式:
缓冲输入输出使用由内核管理的缓冲区。对于写操作,内核将数据从用户空间缓冲区复制到内核分配的缓冲区中,并将其传递给设备驱动程序。读取是相同的,内核将数据从内核缓冲区复制到应用程序提供的缓冲区中。
直接输入输出,不涉及复制。相反,内核将用户分配的缓冲区固定在物理内存中,以便在数据传输过程中它保留在那里而不会被换出。
内存映射也可以由内核安排,以便内核和用户空间应用程序可以使用不同的地址访问相同的内存页。
Windows 上的驱动程序 IO 模式
对缓冲 IO 的支持是 WDM 的内置功能。设备驱动程序可以通过 IRP 结构的 AssociatedIrp.SystemBuffer 字段访问该缓冲区。当驱动程序需要与用户空间通信时,只需读取或写入该缓冲区即可。
Windows 上的直接 IO 由内存描述符列表 (MDL) 介导。这些是半透明结构,可通过 IRP 的 MdlAddress 字段访问。它们用于定位由用户应用程序分配并在 IO 请求期间固定的缓冲区的物理地址。
Windows 上数据传输的第三个选项称为 METHOD_NEITHER
。在这种情况下,内核只是将用户空间输入和输出缓冲区的虚拟地址传递给驱动程序,而不验证它们或确保它们映射到设备驱动程序可访问的物理内存中。设备驱动程序负责处理数据传输的细节。
Linux 上的驱动程序 IO 模式
Linux 提供了许多函数,例如 clear_user
、copy_to_user
、strncpy_from_user
以及其他一些函数来执行内核和用户内存之间的缓冲数据传输。这些函数验证指向数据缓冲区的指针,并通过在内存区域之间安全地复制数据缓冲区来处理数据传输的所有细节。
然而,块设备的驱动程序对已知大小的整个数据块进行操作,这些数据块可以简单地在内核和用户地址空间之间移动,而无需复制它们。对于所有块设备驱动程序,Linux 内核都会自动处理这种情况。块请求队列负责传输数据块而不进行过多的复制,Linux系统调用接口负责将文件系统请求转换为块请求。
最后,设备驱动程序可以从内核地址空间(不可交换)分配一些内存页面,然后使用remap_pfn_range函数将这些页面直接映射到用户进程的地址空间。然后应用程序可以获得该缓冲区的虚拟地址并使用它与设备驱动程序进行通信。
3.设备驱动开发环境
3.1.设备驱动框架
Windows 驱动程序工具包
Windows 是一个闭源操作系统。 Microsoft 提供了Windows 驱动程序工具包,以方便非 Microsoft 供应商开发 Windows 设备驱动程序。该套件包含构建、调试、验证和打包 Windows 设备驱动程序所需的所有内容。
Windows 驱动程序模型为设备驱动程序定义了一个干净的接口框架。 Windows 维护这些接口的源代码和二进制兼容性。编译的 WDM 驱动程序通常是向前兼容的:也就是说,较旧的驱动程序可以按原样在较新的系统上运行,而无需重新编译,但它当然无法访问操作系统提供的新功能。但是,不保证驱动程序向后兼容。
Linux源代码
与Windows相比,Linux是一个开源操作系统,因此Linux的整个源代码都是用于驱动开发的SDK。设备驱动程序没有正式的框架,但 Linux 内核包含许多提供驱动程序注册等通用服务的子系统。这些子系统的接口在内核头文件中描述。
虽然 Linux 确实有定义的接口,但这些接口的设计并不稳定。 Linux 不提供任何有关向前或向后兼容性的保证。设备驱动程序需要重新编译才能与不同的内核版本配合使用。没有稳定性保证可以让 Linux 内核快速开发,因为开发人员不必支持旧的接口,并且可以使用最佳方法来解决手头的问题。
在为 Linux 编写树内驱动程序时,这种不断变化的环境不会造成任何问题,因为它们是内核源代码的一部分,因为它们与内核本身一起更新。然而,闭源驱动程序必须单独开发,树外,并且必须维护它们以支持不同的内核版本。因此,Linux 鼓励设备驱动程序开发人员在树中维护他们的驱动程序。
3.2.构建设备驱动程序系统
Windows 驱动程序工具包添加了对 Microsoft Visual Studio 的驱动程序开发支持,并包含用于构建驱动程序代码的编译器。开发 Windows 设备驱动程序与在 IDE 中开发用户空间应用程序没有太大区别。 Microsoft 还提供了一个企业 Windows 驱动程序工具包,它支持类似于 Linux 的命令行构建环境。
Linux 使用 Makefile 作为树内和树外设备驱动程序的构建系统。 Linux 构建系统非常发达,通常设备驱动程序只需要几行代码即可生成工作二进制文件。开发人员可以使用任何 IDE,只要它可以处理 Linux 源代码库并运行 make
,或者他们可以轻松地从终端手动编译驱动程序。
3.3.文档支持
Windows 为驱动程序开发提供了出色的文档支持。 Windows 驱动程序工具包包括文档和示例驱动程序代码,可通过 MSDN 获得有关内核接口的丰富信息,并且存在大量有关驱动程序开发和 Windows 内部结构的参考和指南书籍。
Linux 文档没有那么描述性,但是随着 Linux 的整个源代码可供驱动程序开发人员使用,这种情况得到了缓解。源代码树中的 Documentation 目录记录了一些 Linux 子系统的文档,但是还有多本有关 Linux 设备驱动程序开发和 Linux 内核概述的书籍,这些书籍更加详细。
Linux不提供指定的设备驱动程序示例,但现有生产驱动程序的源代码是可用的,可以作为开发新设备驱动程序的参考。
3.4.调试支持
Linux 和 Windows 都有可用于跟踪调试驱动程序代码的日志记录工具。在 Windows 上,可以使用 DbgPrint 函数来实现此目的,而在 Linux 上,该函数称为 printk。然而,并不是所有问题都可以仅使用日志记录和源代码来解决。有时断点更有用,因为它们允许检查驱动程序代码的动态行为。交互式调试对于研究崩溃原因也至关重要。
Windows 通过其内核级调试器 WinDbg
支持交互式调试。这需要通过串口连接两台机器:一台计算机运行被调试的内核,另一台计算机运行调试器并控制正在调试的操作系统。 Windows 驱动程序工具包包含 Windows 内核的调试符号,因此 Windows 数据结构将在调试器中部分可见。
Linux 还支持通过 KDB
和 KGDB
进行交互式调试。调试支持可以内置到内核中并在启动时启用。之后,人们可以直接通过物理键盘调试系统,或者通过串行端口从另一台机器连接到系统。 KDB 提供了一个简单的命令行界面,它是在同一台机器上调试内核的唯一方法。然而,KDB 缺乏源代码级调试支持。 KGDB 通过串行端口提供更复杂的接口。它允许使用 GDB 等标准应用程序调试器来调试 Linux 内核,就像任何其他用户空间应用程序一样。
4. 分发设备驱动程序
4.1.安装设备驱动程序
在 Windows 上安装的驱动程序由称为 INF 文件的文本文件描述,这些文件通常存储在 C:WindowsINF 目录中。这些文件由驱动程序供应商提供,定义驱动程序为哪些设备提供服务、在哪里可以找到驱动程序二进制文件、驱动程序的版本等。
当新设备插入计算机时,Windows 看起来
在 Linux 上,一些驱动程序内置于内核中并永久加载。非必需的构建为内核模块,通常存储在 /lib/modules/kernel-version 目录中。该目录还包含各种配置文件,例如描述内核模块之间依赖关系的modules.dep。
虽然 Linux 内核可以在启动时加载一些模块,但通常模块加载是由用户空间应用程序监督的。例如,init进程可能会在系统初始化期间加载一些模块,而udev守护进程负责跟踪新插入的设备并为它们加载适当的模块。
4.2.更新设备驱动程序
Windows 为设备驱动程序提供了稳定的二进制接口,因此在某些情况下无需随系统一起更新驱动程序二进制文件。任何必要的更新均由 Windows Update 服务处理,该服务负责查找、下载和安装适合系统的最新版本的驱动程序。
然而,Linux 不提供稳定的二进制接口,因此每次内核更新时都需要重新编译和更新所有必需的设备驱动程序。显然,内置于内核中的设备驱动程序会自动更新,但树外模块会带来一些小问题。维护最新模块二进制文件的任务通常通过 DKMS 来解决:一种在安装新内核版本时自动重建所有已注册内核模块的服务。
4.3.安全考虑
所有 Windows 设备驱动程序都必须在 Windows 加载之前进行数字签名。在开发过程中可以使用自签名证书,但分发给最终用户的驱动程序包必须使用 Microsoft 信任的有效证书进行签名。供应商可以从 Microsoft 授权的任何受信任的证书颁发机构获取软件发行商证书。然后,Microsoft 对该证书进行交叉签名,生成的交叉证书用于在发布之前对驱动程序包进行签名。
Linux 内核还可以配置为验证正在加载的内核模块的签名并禁止不受信任的模块。内核信任的公钥集在构建时是固定的,并且是完全可配置的。内核执行的检查的严格程度也可以在构建时进行配置,范围从简单地对不受信任的模块发出警告到拒绝加载任何有效性可疑的内容。
5. 结论
如上所示,Windows 和 Linux 设备驱动程序基础结构有一些共同点,例如 API 方法,但更多细节却相当不同。最显着的差异源于这样一个事实:Windows 是由商业公司开发的闭源操作系统。这就是 Windows 需要良好的、有记录的、稳定的驱动程序 ABI 和正式框架的原因,而在 Linux 上,这将是对源代码的一个很好的补充。文档支持在 Windows 环境中也更加发达,因为 Microsoft 拥有维护它所需的资源。
另一方面,Linux 不会用框架来限制设备驱动程序开发人员,并且内核和生产设备驱动程序的源代码在合适的人手中也同样有帮助。缺乏接口稳定性也会产生影响,因为这意味着最新的设备驱动程序始终使用最新的接口,并且内核本身承担的向后兼容性负担更轻,从而产生更干净的代码。
了解这些差异以及每个系统的具体情况是为您的设备提供有效的驱动程序开发和支持的关键的第一步。我们希望 Windows 和 Linux 设备驱动程序开发比较有助于理解它们,并将作为您研究设备驱动程序开发过程的一个很好的起点。