Linux 与 Windows 的设备驱动模型对比:架构、API 和开发环境比较
Windows 设备驱动在新连接设备时是由回调函数AddDevice通知的。它接下来就去创建一个设备对象device object,用于识别该设备的特定的驱动实例。取决于驱动的类型,设
在 Windows 上注册设备 Windows 设备驱动在新连接设备时是由回调函数AddDevice通知的。它接下来就去创建一个设备对象device object,用于识别该设备的特定的驱动实例。取决于驱动的类型,设备对象可以是物理设备对象Physical Device Object(PDO),功能设备对象Function Device Object(FDO),或者过滤设备对象Filter Device Object(FIDO)。设备对象能够堆叠,PDO 在底层。 设备对象在这个设备连接在计算机期间一直存在。DeviceExtension结构能够被用于关联到一个设备对象的全局数据。 设备对象可以有如下形式的名字\Device\DeviceName,这被系统用来识别和定位它们。应用可以使用CreateFileAPI 函数来打开一个有上述名字的文件,获得一个可以用于和设备交互的句柄。 然而,通常只有 PDO 有自己的名字。未命名的设备能够通过设备级接口来访问。设备驱动注册一个或多个接口,以 128 位全局唯一标识符(GUID)来标示它们。用户应用能够使用已知的 GUID 来获取一个设备的句柄。 在 Linux 上注册设备 在 Linux 平台上,用户应用通过文件系统入口访问设备,它通常位于/dev目录。在模块初始化的时候,它通过调用内核函数register_chrdev创建了所有需要的入口。应用可以发起open系统调用来获取一个文件描述符来与设备进行交互。这个调用后来被发送到回调函数,这个调用(以及将来对该返回的文件描述符的进一步调用,例如read、write或close)会被分配到由该模块安装到file_operations或者block_device_operations这样的数据结构中的回调函数。 设备驱动模块负责分配和保持任何需要用于操作的数据结构。传送进文件系统回调函数的file结构有一个private_data字段,它可以被用来存放指向具体驱动数据的指针。块设备和网络接口 API 也提供类似的字段。 虽然应用使用文件系统的节点来定位设备,但是 Linux 在内部使用一个主设备号major numbers和次设备号minor numbers的概念来识别设备及其驱动。主设备号被用来识别设备驱动,而次设备号由驱动使用来识别它所管理的设备。驱动为了去管理一个或多个固定的主设备号,必须首先注册自己或者让系统来分配未使用的设备号给它。 目前,Linux 为主次设备对major-minor pairs使用一个 32 位的值,其中 12 位分配主设备号,并允许多达 4096 个不同的设备。主次设备对对于字符设备和块设备是不同的,所以一个字符设备和一个块设备能使用相同的设备对而不导致冲突。网络接口是通过像 eth0 的符号名来识别,这些又是区别于主次设备的字符设备和块设备的。 2.3. 交换数据 Linux 和 Windows 都支持在用户级应用程序和内核级驱动程序之间传输数据的三种方式: Windows 上的驱动程序 I/O 模式 支持缓冲型 I/O 是 WDM 的内置功能。缓冲区能够被设备驱动通过在 IRP 结构中的AssociatedIrp.SystemBuffer字段访问。当需要和用户空间通讯的时候,驱动只需从这个缓冲区中进行读写操作。 Windows 上的直接 I/O 由内存描述符列表memory descriptor lists(MDL)介导。这种半透明的结构是通过在 IRP 中的MdlAddress字段来访问的。它们被用来定位由用户应用程序分配的缓冲区的物理地址,并在 I/O 请求期间钉死不动。 在 Windows 上进行数据传输的第三个选项称为METHOD_NEITHER。 在这种情况下,内核需要传送用户空间的输入输出缓冲区的虚拟地址给驱动,而不需要确定它们有效或者保证它们映射到一个可以由设备驱动访问的物理内存地址。设备驱动负责处理这些数据传输的细节。 Linux 上的驱动程序 I/O 模式 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 Driver Model(WDM)为设备驱动定义了一个干净的接口框架。Windows 保持这些接口的源代码和二进制的兼容性。编译好的 WDM 驱动通常是前向兼容性:也就是说,一个较旧的驱动能够在没有重新编译的情况下在较新的系统上运行,但是它当然不能够访问系统提供的新功能。但是,驱动不保证后向兼容性。 Linux 源代码 和 Windows 相对比,Linux 是一个开源操作系统,因此 Linux 的整个源代码是用于驱动开发的 SDK。没有驱动设备的正式框架,但是 Linux 内核包含许多提供了如驱动注册这样的通用服务的子系统。这些子系统的接口在内核头文件中描述。 尽管 Linux 有定义接口,但这些接口在设计上并不稳定。Linux 不提供有关前向和后向兼容的任何保证。设备驱动对于不同的内核版本需要重新编译。没有稳定性的保证可以让 Linux 内核进行快速开发,因为开发人员不必去支持旧的接口,并且能够使用最好的方法解决手头的这些问题。 当为 Linux 写树内in-tree(指当前 Linux 内核开发主干)驱动程序时,这种不断变化的环境不会造成任何问题,因为它们作为内核源代码的一部分,与内核本身同步更新。然而,闭源驱动必须单独开发,并且在树外out-of-tree,必须维护它们以支持不同的内核版本。因此,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 的子系统,但是有几本书[1]介绍了关于 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:\Windows\INF目录中。这些文件由驱动供应商提供,并且定义哪些设备由该驱动程序服务,哪里可以找到驱动程序的二进制文件,和驱动程序的版本等。 当一个新设备插入计算机时,Windows 通过查看已经安装的驱动程序并且选择适当的一个加载。当设备被移除的时候,驱动会自动卸载它。 在 Linux 上,一些驱动被构建到内核中并且保持永久的加载。非必要的驱动被构建为内核模块,它们通常是存储在/lib/modules/kernel-version目录中。这个目录还包含各种配置文件,如modules.dep,用于描述内核模块之间的依赖关系。 虽然 Linux 内核可以在自身启动时加载一些模块,但通常模块加载由用户空间应用程序监督。例如,init进程可能在系统初始化期间加载一些模块,udev守护程序负责跟踪新插入的设备并为它们加载适当的模块。 4.2. 更新设备驱动 Windows 为设备驱动程序提供了稳定的二进制接口,因此在某些情况下,无需与系统一起更新驱动程序二进制文件。任何必要的更新由 Windows Update 服务处理,它负责定位、下载和安装适用于系统的最新版本的驱动程序。 然而,Linux 不提供稳定的二进制接口linux 驱动,因此有必要在每次内核更新时重新编译和更新所有必需的设备驱动程序。显然,内置在内核中的设备驱动程序会自动更新,但是树外模块会产生轻微的问题。 维护最新的模块二进制文件的任务通常用DKMS[2]来解决:这是一个当安装新的内核版本时自动重建所有注册的内核模块的服务。 4.3. 安全方面的考虑 所有 Windows 设备驱动程序在 Windows 加载它们之前必须被数字签名。在开发期间可以使用自签名证书,但是分发给终端用户的驱动程序包必须使用 Microsoft 信任的有效证书进行签名。供应商可以从 Microsoft 授权的任何受信任的证书颁发机构获取软件出版商证书Software Publisher Certificate。然后,此证书由 Microsoft 交叉签名,并且生成的交叉证书用于在发行之前签署驱动程序包。 Linux 内核也能配置为在内核模块被加载前校验签名,并禁止不可信的内核模块。被内核所信任的公钥集在构建时是固定的,并且是完全可配置的。由内核执行的检查,这个检查严格性在构建时也是可配置的,范围从简单地为不可信模块发出警告,到拒绝加载有效性可疑的任何东西。 5. 结论 如上所示,Windows 和 Linux 设备驱动程序基础设施有一些共同点,例如调用 API 的方法,但更多的细节是相当不同的。最突出的差异源于 Windows 是由商业公司开发的封闭源操作系统这个事实。这使得 Windows 上有好的、文档化的、稳定的驱动 ABI 和正式框架,而在 Linux 上,更多的是源代码做了一个有益的补充。文档支持也在 Windows 环境中更加发达,因为 Microsoft 具有维护它所需的资源。 另一方面,Linux 不会使用框架来限制设备驱动程序开发人员,并且内核和产品级设备驱动程序的源代码可以在需要的时候有所帮助。缺乏接口稳定性也有其作用,因为它意味着最新的设备驱动程序总是使用最新的接口,内核本身承载较小的后向兼容性负担,这带来了更干净的代码。 了解这些差异以及每个系统的具体情况是为您的设备提供有效的驱动程序开发和支持的关键的第一步。我们希望这篇文章对 Windows 和 Linux 设备驱动程序开发做的对比,有助于您理解它们,并在设备驱动程序开发过程的研究中,将此作为一个伟大的起点。 via: 作者:Dennis Turpitka[3]译者:FrankXinqi &YangYang校对:wxy 本文由LCTT[4]原创编译,Linux中国荣誉推出 (编辑:晋中站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |