当前位置: 首页 > 科技观察

Linux和Windows设备驱动模型比较:架构、API和开发环境的比较

时间:2023-03-12 20:46:19 科技观察

名词缩写:API应用程序接口(ApplicationProgramInterface)ABI它可以通过一些特定的编程接口方便硬件设备的使用,从而使软件可以控制和运行这些设备。由于每个驱动程序对应不同的操作系统,您将需要不同的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内核随Linux驱动程序一起分发,但Windows内核不包括设备驱动程序。相比之下,现代Windows设备驱动程序是使用Windows驱动程序模型(WDM)编写的,该模型完全支持即插即用和电源管理,因此可以根据需要加载和卸载驱动程序。处理来自应用程序的请求是由Windows内核中称为I/O管理器的部分完成的。I/O管理器的作用是将这些请求转换成I/O请求包(IORequestPackets)(IRP),在驱动层可以用来识别请求和传递数据。Windows驱动模型WDM提供了三种驱动,它们形成了三层:过滤器(Filter)驱动提供了可选的对IRP的附加处理。功能(Function)驱动程序是实现接口并与各个设备通信的主要驱动程序。总线(Bus)驱动程序服务于不同的适配器和不同的总线控制器来实现主机模式控制设备。IRP通过这些层,就像它们通过I/O管理器传递到底层硬件一样。每一层都可以独立处理一个IRP,并将它们发送回I/O管理器。在硬件的底部有硬件抽象层(HAL),它为物理设备提供通用接口。1.2.Linux驱动程序架构与Windows设备驱动程序相比,Linux设备驱动程序架构的根本区别在于Linux没有标准的驱动程序模型,也没有完全分离的层。每个设备驱动程序都实现为一个模块,可以自动从内核加载和卸载。Linux为即插即用设备和电源管理设备提供了一些手段,以便那些驱动程序可以使用它们来正确地管理这些设备,但这不是必需的。模式导出它们提供的功能,并通过调用这些功能并传入任意定义的数据结构来进行通信。请求来自文件系统或网络层的用户应用程序,并被转换为所需的数据结构。模块可以分层堆叠。处理完一个模块后,再处理另一个。有些模块为一类设备提供了通用的调用接口,比如USB设备。Linux设备驱动程序支持三种类型的设备:实现字节流接口的字符设备。用于存储文件系统和处理多字节数据块IO的块设备。用于通过网络传输数据包的网络接口。Linux还有一个硬件抽象层(HAL),它实际上充当物理硬件的设备驱动程序接口。2.设备驱动APILinux和Windows驱动API都是事件驱动的:只有当某些事件发生时,驱动程序代码才会被执行——当用户的应用程序想要从设备获取某些东西时,或者当设备有某些请求时。通知操作系统。2.1.初始化在Windows上,驱动程序表示为DriverObject结构,它在DriverEntry函数的执行期间被初始化。这些入口点还注册回调函数以响应设备添加和删除、驱动程序卸载和处理新的传入IRP。连接设备时,Windows会创建一个设备对象来处理设备驱动程序背后的所有应用程序请求。与Windows相比,Linux设备驱动生命周期由内核模块的module_init和module_exit函数管理,分别用于模块加载和卸载。它们负责注册模块以使用内核接口处理设备请求。该模块需要创建一个设备文件(或网络接口),为它希望管理的设备分配一个数字标识符,并注册一些回调函数以供用户与设备文件交互时使用。2.2.命名和声明设备在Windows上注册设备当连接新设备时,Windows设备驱动程序会通过回调函数AddDevice得到通知。然后它去创建一个设备对象(deviceobject),这个对象用来标识设备的具体驱动实例。根据驱动程序的类型,设备对象可以是物理设备对象(PDO)、功能设备对象(FDO)或过滤设备对象(FIDO)。设备对象可以堆叠,PDO位于底部。只要设备连接到计算机,设备对象就会存在。DeviceExtension结构可用于将全局数据与设备对象相关联。设备对象的名称可以是\Device\DeviceName形式,系统使用这些名称来识别和定位它们。应用程序可以使用CreateFileAPI函数打开具有上述名称的文件并获取可用于与设备交互的句柄。但是,通常只有PDO有自己的名称。可以通过设备级接口访问未命名的设备。设备驱动程序注册一个或多个接口,使用128位全局唯一标识符(GUID)标识它们。用户应用程序可以使用已知的GUID来获取设备的句柄。在Linux上注册设备在Linux平台上,用户应用程序通过文件系统条目访问设备,这些条目通常位于/dev目录中。当模块初始化时,它通过调用内核函数register_chrdev创建所有需要的条目。应用程序可以发出open系统调用来获取文件描述符以与设备交互。这个调用稍后被发送到回调函数,这个调用(以及将来对返回的文件描述符的进一步调用,例如读、写或关闭)将被分配给这个模块在file_operations或block_device_operations中安装的数据结构打回来。设备驱动程序模块负责分配和维护操作所需的任何数据结构。传入文件系统回调函数的文件结构有一个private_data字段,可用于存储指向特定驱动程序数据的指针。块设备和网络接口API也提供类似的字段。当应用程序使用文件系统节点来定位设备时,Linux在内部使用主要和次要编号的概念来识别设备及其驱动程序。主编号用于标识设备驱动程序,次编号用于驱动程序标识其管理的设备。为了让驱动程序管理一个或多个固定的主设备号,它必须首先注册自己或让系统为其分配未使用的设备号。目前,Linux使用32位值作为主要-次要对,其中12位分配给主要编号并允许多达4096个不同的设备。字符设备和块设备的主次设备对不同,因此字符设备和块设备可以使用相同的设备对而不会引起冲突。网络接口由诸如eth0之类的符号名称来标识,而这些名称又不同于字符设备和块设备、主要设备和次要设备。2.3.交换数据Linux和Windows都支持三种在用户级应用程序和内核级驱动程序之间传输数据的方式:缓冲输入输出(BufferedInput-Output),它使用由内核管理的缓冲区。对于写操作,内核将数据从用户空间缓冲区复制到内核分配的缓冲区,并将其传输到设备驱动程序。读操作也是如此,内核将数据从内核缓冲区复制到应用程序提供的缓冲区中。直接输入输出(DirectInput-Output)不使用复制功能。相反,内核将用户分配的缓冲区固定在物理内存中,以便它可以留在那里并且不会在数据传输期间被换出。内存映射(Memorymapping)也可以由内核来管理,这样内核和用户空间的应用程序就可以通过不同的地址访问同一个内存页。Windows上的驱动程序I/O模式支持缓冲I/O作为WDM的内置功能。设备驱动程序可以通过IRP结构中的AssociatedIrp.SystemBuffer字段访问缓冲区。当它需要与用户空间通信时,驱动只需要从这个缓冲区中读写即可。Windows上的直接I/O由内存描述符列表(MDL)调解。通过IRP中的MdlAddress字段访问此半透明结构。它们用于定位用户应用程序分配的缓冲区的物理地址,并在I/O请求期间固定。Windows上数据传输的第三个选项称为METHOD_NEITHER。在这种情况下,内核需要将用户空间I/O缓冲区的虚拟地址传递给驱动程序,而无需使它们有效或确保它们映射到设备驱动程序可访问的物理内存地址。设备驱动程序处理这些数据传输的细节。Linux上的驱动程序I/O模式Linux提供了许多函数,例如clear_user、copy_to_user、strncpy_from_user和一些其他函数,用于内核和用户内存之间的缓冲区数据传输。这些函数保证指向数据缓冲区的指针的有效性,并通过在内存区域之间安全地复制数据缓冲区来处理数据传输的所有细节。然而,块设备驱动程序对已知大小的整个数据块进行操作,这些数据可以在内核和用户地址区域之间快速移动而无需复制它们。这种情况由Linux内核为所有块设备驱动程序自动处理。块请求队列处理无冗余副本的数据块传输,Linux系统调用接口将文件系统请求转换为块请求。最后,设备驱动程序可以从内核地址区分配一些内存页(不可交换),并使用remap_pfn_range函数直接将这些页映射到用户进程的地址空间中。然后应用程序可以获得这些缓冲区的虚拟地址,并使用它与设备驱动程序进行通信。3.设备驱动开发环境3.1.设备驱动框架WindowsDriverKitWindows是一个闭源操作系统。Microsoft提供WindowsDriverKit以方便非Microsoft供应商开发Windows设备驱动程序。该工具包包含开发、调试、验证和打包Windows设备驱动程序所需的一切。Windows驱动程序模型(WDM)为设备驱动程序定义了一个干净的接口框架。Windows维护这些接口的源代码和二进制兼容性。编译后的WDM驱动程序通常是向前兼容的:也就是说,较旧的驱动程序无需重新编译即可在较新的系统上运行,但当然它无法访问系统提供的较新功能。但是,驱动程序不保证向后兼容性。Linux源码与Windows相比,Linux是开源操作系统,所以Linux的全部源码就是驱动开发的SDK。驱动设备没有正式的框架,但Linux内核包含许多提供驱动程序注册等公共服务的子系统。这些子系统的接口在内核头文件中进行了描述。尽管Linux定义了接口,但这些接口在设计上并不稳定。Linux不提供任何关于向前和向后兼容性的保证。需要为不同的内核版本重新编译设备驱动程序。没有稳定性的保证允许Linux内核的快速开发,因为开发人员不必支持旧的接口并且可以使用最新的方法来解决手头的问题。在为Linux编写树内(指当前的Linux内核开发主干)驱动程序时,这种不断变化的环境不会造成任何问题,因为它们作为内核源代码的一部分与内核本身同步更新。但是闭源驱动必须单独开发,out-of-tree,必须维护它们以支持不同的内核版本。因此,Linux鼓励设备驱动程序开发人员在树中维护他们的驱动程序。3.2.为设备驱动程序构建系统WindowsDriverKit为MicrosoftVisualStudio添加了驱动程序开发支持,并包括用于构建驱动程序代码的编译器。开发Windows设备驱动程序与在IDE中开发用户空间应用程序没有太大区别。Microsoft提供了一个企业级Windows驱动程序包,它提供了类似于Linux命令行的构建环境。Linux使用Makefile作为树内和树外系统设备驱动程序的构建系统。Linux构建系统非常发达,通常设备驱动程序只需要几行代码就可以生成可运行的二进制文件。开发人员可以使用任何IDE,只要它可以处理Linux源代码存储库并运行make,他们还可以轻松地从终端手动编译驱动程序。3.3.文档支持Windows为驱动程序开发提供了良好的文档支持。WindowsDriverKit包括文档和示例驱动程序代码,有关内核接口的大量信息可通过MSDN获得,并且有大量有关驱动程序开发和Windows底层的参考和指南。Linux文档不是描述性的,但是让驱动程序开发人员可以使用整个Linux源代码可以缓解这个问题。源代码树中的文档目录描述了一些Linux子系统,但是有几本关于Linux设备驱动程序开发和Linux内核概述的书籍更详细。Linux不提供指定的设备驱动程序示例,但可以提供现有生产级驱动程序的源代码,可以作为开发新设备驱动程序的参考。3.4.调试支持Linux和Windows都具有可用于跟踪调试驱动程序代码的日志记录机制。在Windows上,您将使用DbgPrint函数,而在Linux上,使用的函数称为printk。但是,并非所有问题都可以仅使用日志记录和源代码来解决。有时断点更有用,因为它们允许检查驱动代码的动态行为。交互式调试对于调查崩溃原因也是必不可少的。Windows通过其内核级调试器WinDbg支持交互式调试。这就需要通过串口连接两台机器:一台运行被调试的内核,另一台运行调试器并控制被调试的操作系统。WindowsDriverKit包括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为设备驱动程序提供了一个稳定的二进制接口,因此在某些情况下,无需随系统更新驱动程序二进制文件。任何必要的更新都由WindowsUpdate服务处理,该服务负责为系统查找、下载和安装最新版本的驱动程序。但是,Linux不提供稳定的二进制接口,因此必须在每次内核更新时重新编译和更新所有必需的设备驱动程序。显然,内核中内置的设备驱动程序会自动更新,但树外模块会产生一些小问题。维护最新模块二进制文件的任务通常使用DKMS来解决:这是一种在安装新内核版本时自动重建所有已注册内核模块的服务。4.3.安全注意事项所有Windows设备驱动程序都必须在Windows加载它们之前进行数字签名。开发过程中可以使用自签名证书,但分发给最终用户的驱动程序包必须使用微软信任的有效证书进行签名。供应商可以从Microsoft授权的任何受信任的证书颁发机构获得软件发行商证书。Microsoft然后对该证书进行交叉签名,生成的交叉证书用于在分发之前对驱动程序包进行签名。Linux内核还可以配置为在加载内核模块之前验证内核模块的签名,并禁用不受信任的内核模块。内核信任的公钥集在构建时是固定的,并且是完全可配置的。由内核执行的检查,其严格性也可以在构建时配置,范围从简单地对不受信任的模块发出警告,到拒绝加载任何有效性可疑的东西。5.结论如上所示,Windows和Linux的设备驱动程序基础结构有一些共同点,例如调用API的方法,但更多细节却大不相同。最显着的区别在于Windows是一个由商业公司开发的闭源操作系统。这有助于在Windows上形成良好的、有记录的、稳定的驱动程序ABI和正式框架,而Linux上的更多源代码是一个有用的补充。文档支持在Windows环境下也更加发达,因为微软拥有维护它所需的资源。另一方面,Linux不使用框架限制设备驱动程序开发人员,内核和生产级设备驱动程序的源代码在需要时可以提供帮助。接口稳定性的缺乏也有其影响,因为这意味着最新的设备驱动程序始终使用最新的接口,内核本身承担的向后兼容性负担更小,从而导致代码更清晰。了解这些差异和每个系统的细节是为您的设备提供有效驱动程序开发和支持的关键的第一步。我们希望本文对Windows和Linux设备驱动程序开发的比较可以帮助您理解它们,并作为您研究设备驱动程序开发过程的一个很好的起点。