继几个月前用于 iOS 端的 WireGuard 之后,WireGuard
现已可用于MacOS。但遗憾的是,在 Linux 方面,内核部分还没有实现主干化。

时间: 2019-09-17阅读: 152标签: 浏览器前言

澳门葡萄京官方网站 1

澳门葡萄京官方网站 2

看到标题,大家就能想起这个需求在很多项目上都能用到。我们部署在Web服务器上的前端应用,既可以用PC浏览器访问,也可以用手机浏览器访问,再加上现在智能设备的推广,我们甚至能在车载系统、穿戴设备和电视平台上访问。

WireGuard 的主要开发人员 Jason Donenfeld 在周六宣布了 MacOS 的 WireGuard
发布,并得到了其他开发人员的支持。这个 MacOS 端使用的是与 iOS
端相同的源代码构建,并集成到苹果的网络堆栈(Apple’s networking
stack)中。

名词缩写:

设备的多样化让用户无处不在,有时候我们需要根据不同的浏览器运行环境做出对应的处理。浏览器是JavaScript的承载体,我们可以从浏览器上获取相关的信息,来进一步处理我们的业务逻辑。

在有关 MacOS 的 WireGuard 的发布声明中,Donenfeld 评论说,Windows
客户端仍在发展中,但由于要为 Windows 7 和更高版本编写一个新的 Tun
驱动程序,所以需要额外一段时间。此新驱动程序应该比当前用于 Windows 的
OpenVPN Tun 驱动更安全、更快。

API 应用程序接口(Application Program Interface )

然而浏览器品牌众多,有些浏览器使用的标准也不太一样,造就了难以统一的判断。下面我大概罗列一下常用的浏览器品牌和在什么情况下使用浏览器运行环境判断。浏览器相关统计数据可以参考这里。

在 Linux 内核方面,对于人们期待已久的 WireGuard
内核的主干化,没有什么新的进展。看起来 WireGuard 不会被合并到 Linux
5.1中,因为代码还没有在 net-next
中发布。Donenfeld说,预计这些补丁的新版本很快就会被审核,而且这项工作正在幕后进行。

ABI 应用系统二进制接口(Application Binary Interface)

国际五大浏览器品牌:按照全球使用率降序排列

更多信息请访问 WireGuard.com 。

设备驱动是操作系统的一部分,它能够通过一些特定的编程接口便于硬件设备的使用,这样软件就可以控制并且运行那些设备了。因为每个驱动都对应不同的操作系统,所以你就需要不同的
Linux、Windows 或 Unix
设备驱动,以便能够在不同的计算机上使用你的设备。这就是为什么当你雇佣一个驱动开发者或者选择一个研发服务商提供者的时候,查看他们为各种操作系统平台开发驱动的经验是非常重要的。

Google Chrome:Windows、MacOS、Linux、Android、iOSApple
Safari
:MacOS、iOSMozilla
Firefox
:Windows、MacOS、Linux、Android、iOSASA
Opera
:Windows、MacOS、Linux、Android、iOSMicrosoft Internet
Explorer
Microsoft Edge:Windows

(文/开源中国)    

澳门葡萄京官方网站 3

国产常用浏览器品牌:按照国内使用率降序排列,普遍基于开源项目Chromium进行开发

驱动开发的第一步是理解每个操作系统处理它的驱动的不同方式、底层驱动模型、它使用的架构、以及可用的开发工具。例如,Linux
驱动程序模型就与 Windows 非常不同。虽然 Windows
提倡驱动程序开发和操作系统开发分别进行,并通过一组 ABI
调用来结合驱动程序和操作系统,但是 Linux
设备驱动程序开发不依赖任何稳定的 ABI 或
API,所以它的驱动代码并没有被纳入内核中。每一种模型都有自己的优点和缺点,但是如果你想为你的设备提供全面支持,那么重要的是要全面的了解它们。

微信浏览器QQ浏览器UC浏览器2345浏览器搜狗浏览器猎豹浏览器遨游浏览器百度浏览器:百度在2019年04月30日宣布停止服务其他浏览器:很多很多,数不清,我就不列出来了

在本文中,我们将比较 Windows 和 Linux
设备驱动程序,探索不同的架构,API,构建开发和分发,希望让您比较深入的理解如何开始为每一个操作系统编写设备驱动程序。

顺便吐槽一下这个不要脸的红芯浏览器,明明就是基于Chromium进行二次开发再套多一层外壳,还非得说自己开发的浏览器是世界第五大浏览器,偷吃不抹嘴,还是被眼尖的网友发现了。详情请戳one、two、three。。。。

1. 设备驱动架构

使用场景

Windows 设备驱动程序的体系结构和 Linux
中使用的不同,它们各有优缺点。差异主要受以下原因的影响:Windows
是闭源操作系统,而 Linux 是开源操作系统。比较 Linux 和 Windows
设备驱动程序架构将帮助我们理解 Windows 和 Linux 驱动程序背后的核心差异。

判断用户浏览器是桌面端还是移动端,显示对应的主题样式判断用户浏览器是Android端还是iOS端,跳转到对应的App下载链接判断用户浏览器是微信端还是H5端,调用微信分享或当前浏览器分享获取用户浏览器的内核和载体,用于统计用户设备平台分布区间获取用户浏览器的载体版本,用于提示更新信息其实还有很多使用场景,就不一一举例了原理

1.1. Windows 驱动架构

针对处理一个这样的使用场景,其实有一个比较专业的名字,叫做浏览器指纹。我们上面谈到的需求也只是浏览器指纹方案里面的一小部分,而我们需要使用到的浏览器指纹就是UserAgent。

虽然 Linux 内核分发时带着 Linux 驱动,而 Windows
内核则不包括设备驱动程序。与之不同的是,现代 Windows
设备驱动程序编写使用 Windows
驱动模型(WDM),这是一种完全支持即插即用和电源管理的模型,所以可以根据需要加载和卸载驱动程序。

这个UserAgent是何方神圣呢,中文翻译过来就是用户代理。引用百度的定义,就是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、CPU类型、浏览器载体及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。而这些信息也足够我们去判断浏览器运行环境了。

处理来自应用的请求,是由 Windows 内核的中被称为 I/O
管理器的部分来完成的。I/O 管理器的作用是是转换这些请求到 I/O
请求数据包(IO Request Packets)(IRP),IRP
可以被用来在驱动层识别请求并且传输数据。

准备

Windows 驱动模型 WDM 提供三种驱动, 它们形成了三个层:

目前网上很多解决方法都只是针对系统是否是桌面端还是移动端,Android端还是iOS端,部分浏览器载体的判断和获取等等,没有一个比较完美或者终极的解决方案。

  • 过滤(Filter)驱动提供关于 IRP 的可选附加处理。
  • 功能(Function)驱动是实现接口和每个设备通信的主要驱动。
  • 总线(Bus)驱动服务不同的配适器和不同的总线控制器,来实现主机模式控制设备。

因此我用了很多测试平台整理出一个比较全面的解决方案。这个方案包含浏览器系统及版本、浏览器平台、浏览器内核及版本、浏览器载体及版本、浏览器外壳及版本。

一个 IRP 通过这些层就像它们经过 I/O
管理器到达底层硬件那样。每个层能够独立的处理一个 IRP 并且把它们送回 I/O
管理器。在硬件底层中有硬件抽象层(HAL),它提供一个通用的接口到物理设备。

而此方案也是基于navigator.userAgent获取相关浏览器信息(如下),再通过系统、平台、内核、载体、外壳的特有字段进行归类统一,整理出一个完整的浏览器运行环境。

1.2. Linux 驱动架构

const ua = navigator.userAgent.toLowerCase();// 输出"mozilla/5.0 (iphone; cpu iphone os 11_0 like mac os x) applewebkit/604.1.38 (khtml, like gecko) version/11.0 mobile/15a372 safari/604.1"

相比于 Windows 设备驱动,Linux 设备驱动架构根本性的不同就是 Linux
没有一个标准的驱动模型也没有一个干净分隔的层。每一个设备驱动都被当做一个能够自动的从内核中加载和卸载的模块来实现。Linux
为即插即用设备和电源管理设备提供一些方式,以便那些驱动可以使用它们来正确地管理这些设备,但这并不是必须的。

浏览器信息:权重按照以下降序排列

模式输出那些它们提供的函数,并通过调用这些函数和传入随意定义的数据结构来沟通。请求来自文件系统或网络层的用户应用,并被转化为需要的数据结构。模块能够按层堆叠,在一个模块进行处理之后,另外一个再处理,有些模块提供了对一类设备的公共调用接口,例如
USB 设备。

浏览器系统:所运行的操作系统,包含Windows、MacOS、Linux、Android、iOS浏览器平台:所运行的设备平台,包含Desktop桌面端、Mobile移动端浏览器内核:浏览器渲染引擎,包含Webkit、Gecko、Presto、Trident浏览器载体:五大浏览器品牌,包含Chrome、Safari、Firefox、Opera、Iexplore/Edge浏览器外壳:基于五大浏览器品牌的内核进行开发,再套一层自研技术的外壳,如国内众多浏览器品牌

Linux 设备驱动程序支持三种设备:

获取UserAgent是否包含字段:判断是否包含系统、平台、内核、载体、外壳的特有字段

  • 实现一个字节流接口的字符(Character)设备。
  • 用于存放文件系统和处理多字节数据块 IO 的块(Block)设备。
  • 用于通过网络传输数据包的网络(Network)接口。
consttestUa =regexp=regexp.test(ua);

Linux 也有一个硬件抽象层(HAL),它实际扮演了物理硬件的设备驱动接口。

获取UserAgent对应字段的版本

2. 设备驱动 API

consttestVs =regexp=(ua.match(regexp) +"").replace(/[^0-9|_.]/ig,"").replace(/_/ig,".");

Linux 和 Windows 驱动 API
都属于事件驱动类型:只有当某些事件发生的时候,驱动代码才执行——当用户的应用程序希望从设备获取一些东西,或者当设备有某些请求需要告知操作系统。

方案

2.1. 初始化

上述准备工作完成后,我们就按照权重(系统 + 系统版本 平台 内核 + 载体 +
内核版本 + 载体版本 外壳 +
外壳版本
)根据系统、平台、内核、载体、外壳的特有字段来归类统一浏览器运行环境。

在 Windows 上,驱动被表示为 DriverObject 结构,它在 DriverEntry
函数的执行过程中被初始化。这些入口点也注册一些回调函数,用来响应设备的添加和移除、驱动卸载和处理新进入的
IRP。当一个设备连接的时候,Windows
创建一个设备对象,这个设备对象在设备驱动后面处理所有应用请求。

系统+系统版本

相比于 Windows,Linux 设备驱动生命周期由内核模块的 module_init 和
module_exit
函数负责管理,它们分别用于模块的加载和卸载。它们负责注册模块来通过使用内核接口来处理设备的请求。这个模块需要创建一个设备文件(或者一个网络接口),为其所希望管理的设备指定一个数字识别号,并注册一些当用户与设备文件交互的时候所使用的回调函数。

// 系统let system = "unknown";if (testUa(/windows|win32|win64|wow32|wow64/ig)) { system = "windows"; // window系统} else if (testUa(/macintosh|macintel/ig)) { system = "macos"; // macos系统} else if (testUa(/x11/ig)) { system = "linux"; // linux系统} else if (testUa(/android|adr/ig)) { system = "android"; // android系统} else if (testUa(/ios|iphone|ipad|ipod|iwatch/ig)) { system = "ios"; // ios系统}// 系统版本let systemVs = "unknown";if (system === "windows") { if (testUa(/windows nt 5.0|windows 2000/ig)) { systemVs = "2000"; } else if (testUa(/windows nt 5.1|windows xp/ig)) { systemVs = "xp"; } else if (testUa(/windows nt 5.2|windows 2003/ig)) { systemVs = "2003"; } else if (testUa(/windows nt 6.0|windows vista/ig)) { systemVs = "vista"; } else if (testUa(/windows nt 6.1|windows 7/ig)) { systemVs = "7"; } else if (testUa(/windows nt 6.2|windows 8/ig)) { systemVs = "8"; } else if (testUa(/windows nt 6.3|windows 8.1/ig)) { systemVs = "8.1"; } else if (testUa(/windows nt 10.0|windows 10/ig)) { systemVs = "10"; }} else if (system === "macos") { systemVs = testVs(/os x [d._]+/ig);} else if (system === "android") { systemVs = testVs(/android [d._]+/ig);} else if (system === "ios") { systemVs = testVs(/os [d._]+/ig);}

2.2. 命名和声明设备

平台

在 Windows 上注册设备

let platform = "unknow";if (system === "windows" || system === "macos" || system === "linux") { platform = "desktop"; // 桌面端} else if (system === "android" || system === "ios" || testUa(/mobile/ig)) { platform = "mobile"; // 移动端}

Windows 设备驱动在新连接设备时是由回调函数 AddDevice
通知的。它接下来就去创建一个设备对象(device
object),用于识别该设备的特定的驱动实例。取决于驱动的类型,设备对象可以是物理设备对象(Physical
Device Object)(PDO),功能设备对象(Function Device
Object)(FDO),或者过滤设备对象(Filter Device Object
)(FIDO)。设备对象能够堆叠,PDO 在底层。

内核+载体

设备对象在这个设备连接在计算机期间一直存在。DeviceExtension
结构能够被用于关联到一个设备对象的全局数据。

let engine = "unknow";let supporter = "unknow";if (testUa(/applewebkit/ig)  testUa(/safari/ig)) { engine = "webkit"; // webkit内核 if (testUa(/edge/ig)) { supporter = "edge"; // edge浏览器 } else if (testUa(/opr/ig)) { supporter = "opera"; // opera浏览器 } else if (testUa(/chrome/ig)) { supporter = "chrome"; // chrome浏览器 } else { supporter = "safari"; // safari浏览器 }} else if (testUa(/gecko/ig)  testUa(/firefox/ig)) { engine = "gecko"; // gecko内核 supporter = "firefox"; // firefox浏览器} else if (testUa(/presto/ig)) { engine = "presto"; // presto内核 supporter = "opera"; // opera浏览器} else if (testUa(/trident|compatible|msie/ig)) { engine = "trident"; // trident内核 supporter = "iexplore"; // iexplore浏览器}

设备对象可以有如下形式的名字
DeviceDeviceName,这被系统用来识别和定位它们。应用可以使用
CreateFile API
函数来打开一个有上述名字的文件,获得一个可以用于和设备交互的句柄。

内核版本+载体版本

然而,通常只有 PDO
有自己的名字。未命名的设备能够通过设备级接口来访问。设备驱动注册一个或多个接口,以
128 位全局唯一标识符(GUID)来标示它们。用户应用能够使用已知的 GUID
来获取一个设备的句柄。

// 内核版本let engineVs = "unknow";if (engine === "webkit") { engineVs = testVs(/applewebkit/[d.]+/ig);} else if (engine === "gecko") { engineVs = testVs(/gecko/[d.]+/ig);} else if (engine === "presto") { engineVs = testVs(/presto/[d.]+/ig);} else if (engine === "trident") { engineVs = testVs(/trident/[d.]+/ig);}// 载体版本let supporterVs = "unknow";if (supporter === "chrome") { supporterVs = testVs(/chrome/[d.]+/ig);} else if (supporter === "safari") { supporterVs = testVs(/version/[d.]+/ig);} else if (supporter === "firefox") { supporterVs = testVs(/firefox/[d.]+/ig);} else if (supporter === "opera") { supporterVs = testVs(/opr/[d.]+/ig);} else if (supporter === "iexplore") { supporterVs = testVs(/(msie [d.]+)|(rv:[d.]+)/ig);} else if (supporter === "edge") { supporterVs = testVs(/edge/[d.]+/ig);}

在 Linux 上注册设备

外壳+外壳版本

在 Linux 平台上,用户应用通过文件系统入口访问设备,它通常位于 /dev
目录。在模块初始化的时候,它通过调用内核函数 register_chrdev
创建了所有需要的入口。应用可以发起 open
系统调用来获取一个文件描述符来与设备进行交互。这个调用后来被发送到回调函数,这个调用(以及将来对该返回的文件描述符的进一步调用,例如
read、write 或close)会被分配到由该模块安装到 file_operations 或者
block_device_operations这样的数据结构中的回调函数。

let shell = "none";let shellVs = "unknow";if (testUa(/micromessenger/ig)) { shell = "wechat"; // 微信浏览器 shellVs = testVs(/micromessenger/[d.]+/ig);} else if (testUa(/qqbrowser/ig)) { shell = "qq"; // QQ浏览器 shellVs = testVs(/qqbrowser/[d.]+/ig);} else if (testUa(/ubrowser/ig)) { shell = "uc"; // UC浏览器 shellVs = testVs(/ubrowser/[d.]+/ig);} else if (testUa(/2345explorer/ig)) { shell = "2345"; // 2345浏览器 shellVs = testVs(/2345explorer/[d.]+/ig);} else if (testUa(/metasr/ig)) { shell = "sougou"; // 搜狗浏览器} else if (testUa(/lbbrowser/ig)) { shell = "liebao"; // 猎豹浏览器} else if (testUa(/maxthon/ig)) { shell = "maxthon"; // 遨游浏览器 shellVs = testVs(/maxthon/[d.]+/ig);} else if (testUa(/bidubrowser/ig)) { shell = "baidu"; // 百度浏览器 shellVs = testVs(/bidubrowser [d.]+/ig);}

设备驱动模块负责分配和保持任何需要用于操作的数据结构。传送进文件系统回调函数的
file 结构有一个 private_data
字段,它可以被用来存放指向具体驱动数据的指针。块设备和网络接口 API
也提供类似的字段。

终极合体

虽然应用使用文件系统的节点来定位设备,但是 Linux
在内部使用一个主设备号(major numbers)和次设备号(minor
numbers)的概念来识别设备及其驱动。主设备号被用来识别设备驱动,而次设备号由驱动使用来识别它所管理的设备。驱动为了去管理一个或多个固定的主设备号,必须首先注册自己或者让系统来分配未使用的设备号给它。

根据以上的条件判断获得的变量如下,我们可以把它们合并成一个对象输出。这样就可以输出一个清晰的浏览器运行环境,后面想干嘛就干嘛了,多方便。

目前,Linux 为主次设备对(major-minor pairs)使用一个 32 位的值,其中 12
位分配主设备号,并允许多达 4096
个不同的设备。主次设备对对于字符设备和块设备是不同的,所以一个字符设备和一个块设备能使用相同的设备对而不导致冲突。网络接口是通过像
eth0 的符号名来识别,这些又是区别于主次设备的字符设备和块设备的。

本文重点探究方案的可行性,没有过多考虑到代码的优化,所以条件判断使用得有些多,如果有什么方法能优化下代码,减少条件判断,可以在下方评论提个建议哟。

2.3. 交换数据

system:系统systemVs:系统版本platform:平台engine:内核engineVs:内核版本supporter:载体supporterVs:载体版本shell:外壳shellVs:外壳版本

Linux 和 Windows
都支持在用户级应用程序和内核级驱动程序之间传输数据的三种方式:

function BrowserType() { const ua = navigator.userAgent.toLowerCase(); const testUa = regexp = regexp.test(ua); const testVs = regexp = (ua.match(regexp) + "").replace(/[^0-9|_.]/ig, "").replace(/_/ig, "."); // 接上以上if...else条件判断 // ...... // 获取到system、systemVs、platform、engine、engineVs、supporter、supporterVs、shell、shellVs return Object.assign({ engine, // webkit gecko presto trident engineVs, platform, // desktop mobile supporter, // chrome safari firefox opera iexplore edge supporterVs, system, // windows macos linux android ios systemVs }, shell === "none" ? {} : { shell, // wechat qq uc 2345 sougou liebao maxthon baidu shellVs });}
  • 缓冲型输入输出(Buffered
    Input-Output)它使用由内核管理的缓冲区。对于写操作,内核从用户空间缓冲区中拷贝数据到内核分配的缓冲区,并且把它传送到设备驱动中。读操作也一样,由内核将数据从内核缓冲区中拷贝到应用提供的缓冲区中。
  • 直接型输入输出(Direct Input-Output)
    它不使用拷贝功能。代替它的是,内核在物理内存中钉死一块用户分配的缓冲区以便它可以一直留在那里,以便在数据传输过程中不被交换出去。
  • 内存映射(Memory mapping)
    它也能够由内核管理,这样内核和用户空间应用就能够通过不同的地址访问同样的内存页。

在控制台执行BrowserType(),该有的都出来了,哈哈!源码详情请戳这里,喜欢的可以点个赞支持下,谢谢。

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 的子系统,但是有几本书介绍了关于 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
授权的任何受信任的证书颁发机构获取软件出版商证书(Software Publisher
Certificate)。然后,此证书由 Microsoft
交叉签名,并且生成的交叉证书用于在发行之前签署驱动程序包。

Linux
内核也能配置为在内核模块被加载前校验签名,并禁止不可信的内核模块。被内核所信任的公钥集在构建时是固定的,并且是完全可配置的。由内核执行的检查,这个检查严格性在构建时也是可配置的,范围从简单地为不可信模块发出警告,到拒绝加载有效性可疑的任何东西。

5. 结论

如上所示,Windows 和 Linux 设备驱动程序基础设施有一些共同点,例如调用
API 的方法,但更多的细节是相当不同的。最突出的差异源于 Windows
是由商业公司开发的封闭源操作系统这个事实。这使得 Windows
上有好的、文档化的、稳定的驱动 ABI 和正式框架,而在 Linux
上,更多的是源代码做了一个有益的补充。文档支持也在 Windows
环境中更加发达,因为 Microsoft 具有维护它所需的资源。

另一方面,Linux
不会使用框架来限制设备驱动程序开发人员,并且内核和产品级设备驱动程序的源代码可以在需要的时候有所帮助。缺乏接口稳定性也有其作用,因为它意味着最新的设备驱动程序总是使用最新的接口,内核本身承载较小的后向兼容性负担,这带来了更干净的代码。

了解这些差异以及每个系统的具体情况是为您的设备提供有效的驱动程序开发和支持的关键的第一步。我们希望这篇文章对
Windows 和 Linux
设备驱动程序开发做的对比,有助于您理解它们,并在设备驱动程序开发过程的研究中,将此作为一个伟大的起点。

【编辑推荐】