分类 软件开发 下的文章

软件工具通常情况下会提供多个功能以供选择,但是如你所知的,不是所有的功能都能被每个人用到的。公正地讲,这并不是设计上的错误,因为每个用户都会有自己的需求,他们只在他们的领域内使用该工具。然而,深入了解你所使用的工具也是很有益处的,因为你永远不知道它的某个功能会在什么时候派上用场,从而节省下你宝贵的时间。

举一个例子:编译器。一个优秀的编程语言编译器总是会提供极多的选项,但是用户一般只知道和使用其中很有限的一部分功能。更具体点来说,比如你是 C 语言开发人员,并将 Linux 作为你的开发平台,那么你很有可能会用到 gcc 编译器,这个编译器提供了 (几乎) 数不清的命令行选项列表。

你知道,你可以让 gcc 保存每个编译阶段的输出吗?你知道用于生成警告的 -Wall 选项,它并不会包含一些特殊的警告吗?gcc 的很多命令行选项都不会经常用到,但是它们在某些特定的情况下会变得非常有用,例如,当你在调试代码的时候。

所以在本文中,我们会介绍这样的几个选项,提供所有必要的细节,并通过简单易懂的例子来解释它们。

但是在开始前,请注意本文中所有的例子所使用的环境:基于 Ubuntu 16.04 LTS 操作系统,gcc 版本为 5.4.0。

在每个编译阶段查看中间代码的输出

你知道在通过 gcc 编译 c 语言代码的时候大体上共分为四个阶段吗?分别为预处理 -> 编译 -> 汇编 -> 链接。在每个阶段之后,gcc 都会产生一个将移交给下一个阶段的临时输出文件。但是生成的都是临时文件,因此我们并不能看到它们——我们所看到的只是我们发起编译命令,然后它生成的我们可以直接运行的二进制文件或可执行文件。

但是比如说在预处理阶段,如果调试时需要查看代码是如何进行处理的,你要怎么做呢?好消息是 gcc 编译器提供了相应的命令行选项,你可以在标准编译命令中使用这些选项获得原本被编译器删除的中间文件。我们所说的选项就是-save-temps

以下是 gcc 手册中对该选项的介绍:

永久存储临时的中间文件,将它们放在当前的文件夹下并根据源文件名称为其命名。因此,用 -c -save-temps 命令编译 foo.c 文件时会生成 foo.i foo.s 和 foo.o 文件。即使现在编译器大多使用的是集成的预处理器,这命令也会生成预处理输出文件 foo.i。

当与 -x 命令行选项结合使用时,-save-temps 命令会避免覆写与中间文件有着相同扩展名的输入源文件。相应的中间文件可以通过在使用 -save-temps 命令之前重命名源文件获得。

以下是怎样使用这个选项的例子:

gcc -Wall -save-temps test.c -o test-exec

下图为该命令的执行结果,验证其确实产生了中间文件:

因此,在截图中你所看到的 test.i、test.s、 test.o 文件都是由 -save-temps 选项产生的。这些文件分别对应于预处理、编译和链接阶段。

让你的代码可调试和可分析

你可以使用专有的工具调试和分析代码。如 gdb 就是专用于调试的工具,而 gprof 则是热门的分析工具。但你知道 gcc 特定的命令行选项也可以让你的代码可调试和可分析吗?

让我们开始调试之路吧!为了能在代码调试中使用 gdb,你需要在编译代码的时候使用 gcc 编译器提供的 -g 选项。这个选项让 gcc 生成 gdb 需要的调试信息从而能成功地调试程序。

如果你想要使用此选项,建议您详细阅读 gcc 手册提供的有关此选项的详细信息——在某些情况下,其中的一些内容可能是至关重要的。 例如,以下是从手册页中摘录的内容:

GCC 允许在使用 -g 选项的时候配合使用 -O 选项。优化代码采用的便捷方式有时可能会产生意想不到的结果:某些你声明的变量可能不复存在;控制流可能会突然跳转到你未曾预期的位置;一些语句也许不会执行,因为它们已经把常量结果计算了或值已经被保存;一些语句可能会在不同地方执行,因为它们已经被移出循环。

然而优化的输出也是可以调试的。这就使得让优化器可以合理地优化或许有 bug 的代码。

不只是 gdb,使用 -g 选项编译代码,还可以开启使用 Valgrind 内存检测工具,从而完全发挥出该选项的潜力。或许还有一些人不知道,mencheck 工具被程序员们用来检测代码中是否存在内存泄露。你可以在这里参见这个工具的用法。

继续往下,为了能够在代码分析中使用 gprof 工具,你需要使用 -pg 命令行选项来编译代码。这会让 gcc 生成额外的代码来写入分析信息,gprof 工具需要这些信息来进行代码分析。gcc 手册 中提到:当编译你需要数据的源文件时,你必须使用这个选项,当然链接时也需要使用它。为了能了解 gprof 分析代码时具体是如何工作的,你可以转到我们的网站专用教程进行了解。

注意-g-pg 选项的用法类似于上一节中使用 -save-temps 选项的方式。

结论

我相信除了 gcc 的专业人士,都可以在这篇文章中得到了一些启发。尝试一下这些选项,然后观察它们是如何工作的。同时,请期待本教程系列的下一部分,我们将会讨论更多有趣和有用的 gcc 命令行选项。


via: https://www.howtoforge.com/tutorial/uncommon-but-useful-gcc-command-line-options/

作者:Ansh 译者:dongdongmian 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

通过这系列的前六篇文章,我们已经学会使用 Git 来对文本文件进行版本控制的管理。我们不禁要问,还有二进制文件呢,也可进行进行版本控制吗?答案是肯定的,Git 已经有了可以处理像多媒体文件这样的二进制大对象块(blob)的扩展。因此,今天我们会学习使用 Git 来管理所谓的二进制资产。

似乎大家都认可的事就是 Git 对于大的二进制对象文件支持得不好。要记住,二进制大对象与大文本文件是不同的。虽然 Git 对大型的文本文件版本控制毫无问题,但是对于不透明的二进制文件起不了多大作用,只能把它当作一个大的实体黑盒来提交。

设想这样的场景,有一个另人兴奋的第一人称解密游戏,您正在为它制作复杂的 3D 建模,源文件是以二进制格式保存的,最后生成一个 1GB 大小的的文件。您提交过一次,在 Git 源仓库历史中有一个 1GB 大小的新增提交。随后,您修改了下模型人物的头发造型,然后提交更新,因为 Git 并不能把头发从头部及模型中其余的部分离开来,所以您只能又提交 1GB 的量。接着,您改变了模型的眼睛颜色,提交这部分更新:又是 GB 级的提交量。对一个模型的一些微小修改,就会导致三个 GB 级的提交量。对于想对一个游戏所有资源进行版本控制这样的规模,这是个严重的问题。

不同的是如 .obj 这种格式的文本文件,和其它类型文件一样,都是一个提交就存储所有更新修改状态,不同的是 .obj 文件是一系列描述模型的纯文本行。如果您修改了该模型并保存回 .obj 文件,Git 可以逐行读取这两个文件,然后创建一个差异版本,得到一个相当小的提交。模型越精细,提交就越小,这就是标准的 Git 用例。虽然文件本身很大,但 Git 使用覆盖或稀疏存储的方法来构建当前数据使用状态的完整描述。

然而,不是所有的都是纯文本的,但都要使用 Git,所以需要解决方案,并且已经出现几个了。

OSTree 开始是作为 GNOME 项目出现的,旨在管理操作系统的二进制文件。它不适用于这里,所以我直接跳过。

Git 大文件存储(LFS) 是放在 GitHub 上的一个开源项目,是从 git-media 项目中分支出来的。git-mediagit-annex 是 Git 用于管理大文件的扩展。它们是对同一问题的两种不同的解决方案,各有优点。虽然它们都不是官方的项目,但在我看来,每个都有独到之处:

  • git-media 是集中模式,有一个公共资产的存储库。你可以告诉 git-media 大文件需要存储的位置,是在硬盘、服务器还是在云存储服务器,项目中的每个用户都将该位置视为大型文件的中心主存储位置。
  • git-annex 侧重于分布模式。用户各自创建存储库,每个存储库都有一个存储大文件的本地目录 .git/annex。这些 annex 会定期同步,只要有需要,每个用户都可以访问到所有的资源。除非通过 annex-cost 特别配置,否则 git-annex 优先使用本地存储,再使用外部存储。

对于这些,我已经在生产中使用了 git-media 和 git-annex,那么下面会向你们概述其工作原理。

git-media

git-media 是使用 Ruby 语言开发的,所以首先要安装 gem(LCTT 译注:Gem 是基于 Ruby 的一些开发工具包)。安装说明在其网站上。想使用 git-meida 的用户都需要安装它,因为 gem 是跨平台的工具,所以在各平台都适用。

安装完 git-media 后,你需要设置一些 Git 的配置选项。在每台机器上只需要配置一次。

$ git config filter.media.clean "git-media filter-clean"
$ git config filter.media.smudge "git-media filter-smudge"

在要使用 git-media 的每个存储库中,设置一个属性以将刚刚创建的过滤器结合到要您分类为“ 媒体 media ”的文件类型里。别被这种术语混淆。一个更好的术语是“资产”,因为“媒体”通常的意思是音频、视频和照片,但您也可以很容易地将 3D 模型,烘焙和纹理等归类为媒体。

例如:

$ echo "*.mp4 filter=media -crlf" >> .gitattributes
$ echo "*.mkv filter=media -crlf" >> .gitattributes
$ echo "*.wav filter=media -crlf" >> .gitattributes
$ echo "*.flac filter=media -crlf" >> .gitattributes
$ echo "*.kra filter=media -crlf" >> .gitattributes

当您要 暂存 stage 这些类型的文件时,文件会被复制到 .git/media 目录。

假设在服务器已经有了一个 Git 源仓库,最后一步就告诉源仓库“母舰”所在的位置,也就是,当媒体文件被推送给所有用户共享时,媒体文件将会存储的位置。这在仓库的 .git/config 文件中设置,请替换成您的用户名、主机和路径:

[git-media]
transport = scp
autodownload = false #默认为 true,拉取资源
scpuser = seth
scphost = example.com
scppath = /opt/jupiter.git

如果您的服务器上 SSH 设置比较复杂,例如使用了非标准端口或非默认 SSH 密钥文件的路径,请使用 .ssh/config 为主机设置默认配置。

git-media 的使用和普通文件一样,可以把普通文件和 blob 文件一样对待,一样进行 commit 操作。操作流程中唯一的不同就是,在某些时候,您应该将您的资产(或称媒体)同步到共享存储库中。

当要为团队发布资产或自己备份资料时,请使用如下命令:

$ git media sync

要用一个变更后的版本替换 git-media 中的文件时(例如,一个已经美声过的音频文件,或者一个已经完成的遮罩绘画,或者一个已经被颜色分级的视频文件),您必须明确的告诉 Git 更新该媒体。这将覆盖 git-media 不会复制远程已经存在的文件的默认设置:

$ git update-index --really-refresh

当您团队的其他成员(或是您本人,在其它机器上)克隆本仓库时,如果没有在 .git/config 中把 autodownload 选项设置为 true 的话,默认是不会下载资源的。但 git-media 的一个同步命令 git media sync 可解决所有问题。

git-annex

git-annex 的处理流程略微的有些不同,默认是使用本地仓库的,但基本的思想都一样。您可以从你的发行版的软件仓库中安装 git-annex,或者根据需要从该网站上下载安装。与 git-media 一样,任何使用 git-annex 的用户都必须在其机器上安装它。

其初始化设置比 git-media 都简单。运行如下命令,其中替换成您的路径,就可以在您的服务器上创建好裸存储库:

$ git init --bare --shared /opt/jupiter.git

然后克隆到本地计算机,把它标记为 git-annex 的初始路径:

$ git clone [email protected]:/opt/jupiter.clone
Cloning into 'jupiter.clone'... 
warning: You appear to have clonedan empty repository. 
Checking connectivity... done.
$ git annex init "seth workstation" 
init seth workstation ok

不要使用过滤器来区分媒体资源或大文件,您可以使用 git annex 命令来配置归类大文件:

$ git annex add bigblobfile.flac
add bigblobfile.flac
(checksum) ok
(Recording state in Git...)

跟普通文件一样进行提交操作:

$ git commit -m 'added flac source for sound fx'

但是推送操作是不同的,因为 git annex 使用自己的分支来跟踪资产。您首次推送可能需要 -u 选项,具体取决于您如何管理您的存储库:

$ git push -u origin master git-annex
To [email protected]:/opt/jupiter.git
* [new branch] master -> master
* [new branch] git-annex -> git-annex

和 git-media 一样,普通的 git push 命令是不会拷贝资料到服务器的,仅仅只是发送了相关的消息,要真正共享文件,需要运行同步命令:

$ git annex sync --content

如果别人已经提交了共享资源,您需要拉取它们,git annex sync 命令将提示您要在本地检出你本机没有,但在服务器上存在的资源。

git-media 和 git-annex 都非常灵活,都可以使用本地存储库来代替服务器,所以它们也常用于管理私有的本地项目。

Git 是一个非常强大和扩展性非常强的系统应用软件,我们应该毫不犹豫的使用它。现在就开始试试吧!


via: https://opensource.com/life/16/8/how-manage-binary-blobs-git-part-7

作者:Seth Kenlon 译者:runningwater 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这是 JavaScript 框架系列的第二章。在这一章里,我打算讲一下在浏览器里的异步代码不同执行方式。你将了解定时器和事件循环之间的不同差异,比如 setTimeout 和 Promises。

这个系列是关于一个开源的客户端框架,叫做 NX。在这个系列里,我主要解释一下写该框架不得不克服的主要困难。如果你对 NX 感兴趣可以参观我们的 主页

这个系列包含以下几个章节:

  1. 项目结构
  2. 定时执行 (当前章节)
  3. 沙箱代码评估
  4. 数据绑定介绍
  5. 数据绑定与 ES6 代理
  6. 自定义元素
  7. 客户端路由

异步代码执行

你可能比较熟悉 Promiseprocess.nextTick()setTimeout(),或许还有 requestAnimationFrame() 这些异步执行代码的方式。它们内部都使用了事件循环,但是它们在精确计时方面有一些不同。

在这一章里,我将解释它们之间的不同,然后给大家演示怎样在一个类似 NX 这样的先进框架里面实现一个定时系统。不用我们重新做一个,我们将使用原生的事件循环来达到我们的目的。

事件循环

事件循环甚至没有在 ES6 规范里提到。JavaScript 自身只有任务(Job)和任务队列(job queue)。更加复杂的事件循环是在 NodeJS 和 HTML5 规范里分别定义的,因为这篇是针对前端的,我会在详细说明后者。

事件循环可以被看做某个条件的循环。它不停的寻找新的任务来运行。这个循环中的一次迭代叫做一个滴答(tick)。在一次滴答期间执行的代码称为一次任务(task)。

while (eventLoop.waitForTask()) {  
  eventLoop.processNextTask()
}

任务是同步代码,它可以在循环中调度其它任务。一个简单的调用新任务的方式是 setTimeout(taskFn)。不管怎样, 任务可能有很多来源,比如用户事件、网络或者 DOM 操作。

任务队列

更复杂一些的是,事件循环可以有多个任务队列。这里有两个约束条件,相同任务源的事件必须在相同的队列,以及任务必须按插入的顺序进行处理。除此之外,浏览器可以做任何它想做的事情。例如,它可以决定接下来处理哪个任务队列。

while (eventLoop.waitForTask()) {  
  const taskQueue = eventLoop.selectTaskQueue()
  if (taskQueue.hasNextTask()) {
    taskQueue.processNextTask()
  }
}

用这个模型,我们不能精确的控制定时。如果用 setTimeout()浏览器可能决定先运行完其它几个队列才运行我们的队列。

微任务队列

幸运的是,事件循环还提供了一个叫做微任务(microtask)队列的单一队列。当前任务结束的时候,微任务队列会清空每个滴答里的任务。

while (eventLoop.waitForTask()) {  
  const taskQueue = eventLoop.selectTaskQueue()
  if (taskQueue.hasNextTask()) {
    taskQueue.processNextTask()
  }

  const microtaskQueue = eventLoop.microTaskQueue
  while (microtaskQueue.hasNextMicrotask()) {
    microtaskQueue.processNextMicrotask()
  }
}

最简单的调用微任务的方法是 Promise.resolve().then(microtaskFn)。微任务按照插入顺序进行处理,并且由于仅存在一个微任务队列,浏览器不会把时间弄乱了。

此外,微任务可以调度新的微任务,它将插入到同一个队列,并在同一个滴答内处理。

绘制 Rendering

最后是 绘制 Rendering 调度,不同于事件处理和分解,绘制并不是在单独的后台任务完成的。它是一个可以运行在每个循环滴答结束时的算法。

在这里浏览器又有了许多自由:它可能在每个任务以后绘制,但是它也可能在好几百个任务都执行了以后也不绘制。

幸运的是,我们有 requestAnimationFrame(),它在下一个绘制之前执行传递的函数。我们最终的事件模型像这样:

while (eventLoop.waitForTask()) {  
  const taskQueue = eventLoop.selectTaskQueue()
  if (taskQueue.hasNextTask()) {
    taskQueue.processNextTask()
  }

  const microtaskQueue = eventLoop.microTaskQueue
  while (microtaskQueue.hasNextMicrotask()) {
    microtaskQueue.processNextMicrotask()
  }

  if (shouldRender()) {
    applyScrollResizeAndCSS()
    runAnimationFrames()
    render()
  }
}

现在用我们所知道知识来创建定时系统!

利用事件循环

和大多数现代框架一样,NX 也是基于 DOM 操作和数据绑定的。批量操作和异步执行以取得更好的性能表现。基于以上理由我们用 PromisesMutationObserversrequestAnimationFrame()

我们所期望的定时器是这样的:

  1. 代码来自于开发者
  2. 数据绑定和 DOM 操作由 NX 来执行
  3. 开发者定义事件钩子
  4. 浏览器进行绘制

步骤 1

NX 寄存器对象基于 ES6 代理 以及 DOM 变动基于MutationObserver (变动观测器)同步运行(下一节详细介绍)。 它作为一个微任务延迟直到步骤 2 执行以后才做出反应。这个延迟已经在 Promise.resolve().then(reaction) 进行了对象转换,并且它将通过变动观测器自动运行。

步骤 2

来自开发者的代码(任务)运行完成。微任务由 NX 开始执行所注册。 因为它们是微任务,所以按序执行。注意,我们仍然在同一个滴答循环中。

步骤 3

开发者通过 requestAnimationFrame(hook) 通知 NX 运行钩子。这可能在滴答循环后发生。重要的是,钩子运行在下一次绘制之前和所有数据操作之后,并且 DOM 和 CSS 改变都已经完成。

步骤 4

浏览器绘制下一个视图。这也有可能发生在滴答循环之后,但是绝对不会发生在一个滴答的步骤 3 之前。

牢记在心里的事情

我们在原生的事件循环之上实现了一个简单而有效的定时系统。理论上讲它运行的很好,但是还是很脆弱,一个轻微的错误可能会导致很严重的 BUG。

在一个复杂的系统当中,最重要的就是建立一定的规则并在以后保持它们。在 NX 中有以下规则:

  1. 永远不用 setTimeout(fn, 0) 来进行内部操作
  2. 用相同的方法来注册微任务
  3. 微任务仅供内部操作
  4. 不要干预开发者钩子运行时间

规则 1 和 2

数据反射和 DOM 操作将按照操作顺序执行。这样只要不混合就可以很好的延迟它们的执行。混合执行会出现莫名其妙的问题。

setTimeout(fn, 0) 的行为完全不可预测。使用不同的方法注册微任务也会发生混乱。例如,下面的例子中 microtask2 不会正确地在 microtask1 之前运行。

Promise.resolve().then().then(microtask1)  
Promise.resolve().then(microtask2) 

规则 3 和 4

分离开发者的代码执行和内部操作的时间窗口是非常重要的。混合这两种行为会导致不可预测的事情发生,并且它会需要开发者了解框架内部。我想很多前台开发者已经有过类似经历。

结论

如果你对 NX 框架感兴趣,可以参观我们的主页。还可以在 GIT 上找到我们的源代码

在下一节我们再见,我们将讨论 沙盒化代码执行

你也可以给我们留言。


via: https://blog.risingstack.com/writing-a-javascript-framework-execution-timing-beyond-settimeout/

作者:Bertalan Miklos 译者:kokialoves 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

更新(2016/10/30):我写完这篇文章之后,我在这个基准测试中发了一个错误,会导致 Rollup 比它预期的看起来要好一些。不过,整体结果并没有明显的不同(Rollup 仍然击败了 Browserify 和 Webpack,虽然它并没有像 Closure 十分好),所以我只是更新了图表。该基准测试包括了 RequireJS 和 RequireJS Almond 打包器,所以文章中现在也包括了它们。要看原始帖子,可以查看历史版本

大约一年之前,我在将一个大型 JavaScript 代码库重构为更小的模块时发现了 Browserify 和 Webpack 中一个令人沮丧的事实:

“代码越模块化,代码体积就越大。:< ”

  • Nolan Lawson

过了一段时间,Sam Saccone 发布了一些关于 TumblrImgur 页面加载性能的出色的研究。其中指出:

“超过 400 ms 的时间单纯的花费在了遍历 Browserify 树上。”

  • Sam Saccone

在本篇文章中,我将演示小模块可能会根据你选择的 打包器 bundler 模块系统 module system 而出现高得惊人的性能开销。此外,我还将解释为什么这种方法不但影响你自己代码的模块,也会影响依赖项中的模块,这也正是第三方代码在性能开销上很少提及的方面。

网页性能

一个页面中包含的 JavaScript 脚本越多,页面加载也将越慢。庞大的 JavaScript 包会导致浏览器花费更多的时间去下载、解析和执行,这些都将加长载入时间。

即使当你使用如 Webpack code splitting、Browserify factor bundles 等工具将代码分解为多个包,该开销也仅仅是被延迟到页面生命周期的晚些时候。JavaScript 迟早都将有一笔开销。

此外,由于 JavaScript 是一门动态语言,同时流行的 CommonJS 模块也是动态的,所以这就使得在最终分发给用户的代码中剔除无用的代码变得异常困难。譬如你可能只使用到 jQuery 中的 $.ajax,但是通过载入 jQuery 包,你将付出整个包的代价。

JavaScript 社区对这个问题提出的解决办法是提倡 小模块 的使用。小模块不仅有许多 美好且实用的好处 如易于维护,易于理解,易于集成等,而且还可以通过鼓励包含小巧的功能而不是庞大的库来解决之前提到的 jQuery 的问题。

所以在小模块下,你将不需要这样:

var _ = require('lodash')
_.uniq([1,2,2,3])

而是可以如此:

var uniq = require('lodash.uniq')
uniq([1,2,2,3])

包与模块

需要强调的是这里我提到的“模块”并不同于 npm 中的“包”的概念。当你从 npm 安装一个包时,它会将该模块通过公用 API 展现出来,但是在这之下其实是一个许多模块的聚合物。

例如,我们来看一个包 is-array,它没有别的依赖,并且只包含 一个 JavaScript 文件,所以它只有一个模块。这算是足够简单的。

现在来看一个稍微复杂一点的包,如 once。它有一个依赖的包 wrappy 包都各自包含一个模块,所以总模块数为 2。至此,也还算好。

现在来一起看一个更为令人迷惑的例子:qs。因为它没有依赖的包,所以你可能就认为它只有一个模块,然而事实上,它有四个模块!

你可以用一个我写的工具 browserify-count-modules 来统计一个 Browserify 包的总模块数:

$ npm install qs
$ browserify node_modules/qs | browserify-count-modules
4

这说明了一个包可以包含一个或者多个模块。这些模块也可以依赖于其他的包,而这些包又将附带其自己所依赖的包与模块。由此可以确定的事就是任何一个包将包含至少一个模块。

模块膨胀

一个典型的网页应用中会包含多少个模块呢?我在一些流行的使用 Browserify 的网站上运行 browserify-count-moduleson 并且得到了以下结果:

顺带一提,我写过的最大的开源站点 Pokedex.org 包含了 4 个包,共 311 个模块。

让我们先暂时忽略这些 JavaScript 包的实际大小,我认为去探索一下一定数量的模块本身开销会是一件有意思的事。虽然 Sam Saccone 的文章 “2016 年 ES2015 转译的开销” 已经广为流传,但是我认为他的结论还未到达足够深度,所以让我们挖掘的稍微再深一点吧。

测试环节!

我构造了一个能导入 100、1000 和 5000 个其他小模块的测试模块,其中每个小模块仅仅导出一个数字。而父模块则将这些数字求和并记录结果:

// index.js
var total = 0
total += require('./module_0')
total += require('./module_1')
total += require('./module_2')
// etc.
console.log(total)


// module_1.js
module.exports = 1

我测试了五种打包方法:Browserify、带 bundle-collapser 插件的 Browserify、Webpack、Rollup 和 Closure Compiler。对于 Rollup 和 Closure Compiler 我使用了 ES6 模块,而对于 Browserify 和 Webpack 则用的是 CommonJS,目的是为了不涉及其各自缺点而导致测试的不公平(由于它们可能需要做一些转译工作,如 Babel 一样,而这些工作将会增加其自身的运行时间)。

为了更好地模拟一个生产环境,我对所有的包采用带 -mangle-compress 参数的 Uglify ,并且使用 gzip 压缩后通过 GitHub Pages 用 HTTPS 协议进行传输。对于每个包,我一共下载并执行 15 次,然后取其平均值,并使用 performance.now() 函数来记录载入时间(未使用缓存)与执行时间。

包大小

在我们查看测试结果之前,我们有必要先来看一眼我们要测试的包文件。以下是每个包最小处理后但并未使用 gzip 压缩时的体积大小(单位:Byte):

100 个模块1000 个模块5000 个模块
browserify798279987419985
browserify-collapsed578657991309982
webpack395439055203052
rollup671697138968
closure758795843955
100 个模块1000 个模块5000 个模块
browserify16491380064513
browserify-collapsed14641190356335
webpack693502726363
rollup300214511510
closure302214011789

Browserify 和 Webpack 的工作方式是隔离各个模块到各自的函数空间,然后声明一个全局载入器,并在每次 require() 函数调用时定位到正确的模块处。下面是我们的 Browserify 包的样子:

(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o

而 Rollup 和 Closure 包看上去则更像你亲手写的一个大模块。这是 Rollup 打包的包:

(function () {
        'use strict';
        var total = 0
        total += 0
        total += 1
        total += 2
// etc.

如果你清楚在 JavaScript 中使用嵌套函数与在关联数组查找一个值的固有开销, 那么你将很容易理解出现以下测试的结果的原因。

测试结果

我选择在搭载 Android 5.1.1 与 Chrome 52 的 Nexus 5(代表中低端设备)和运行 iOS 9 的第 6 代 iPod Touch(代表高端设备)上进行测试。

这是 Nexus 5 下的测试结果(查看表格):

Nexus 5 结果

这是 iPod Touch 下的测试结果(查看表格):

iPod Touch 结果

在 100 个模块时,各包的差异是微不足道的,但是一旦模块数量达到 1000 个甚至 5000 个时,差异将会变得非常巨大。iPod Touch 在不同包上的差异并不明显,而对于具有一定年代的 Nexus 5 来说,Browserify 和 Webpack 明显耗时更多。

与此同时,我发现有意思的是 Rollup 和 Closure 的运行开销对于 iPod 而言几乎可以忽略,并且与模块的数量关系也不大。而对于 Nexus 5 来说,运行的开销并非完全可以忽略,但 Rollup/Closure 仍比 Browserify/Webpack 低很多。后者若未在几百毫秒内完成加载则将会占用主线程的好几帧的时间,这就意味着用户界面将冻结并且等待直到模块载入完成。

值得注意的是前面这些测试都是在千兆网速下进行的,所以在网络情况来看,这只是一个最理想的状况。借助 Chrome 开发者工具,我们可以认为地将 Nexus 5 的网速限制到 3G 水平,然后来看一眼这对测试产生的影响(查看表格):

Nexus 5 3G 结果

一旦我们将网速考虑进来,Browserify/Webpack 和 Rollup/Closure 的差异将变得更为显著。在 1000 个模块规模(接近于 Reddit 1050 个模块的规模)时,Browserify 花费的时间比 Rollup 长大约 400 毫秒。然而 400 毫秒已经不是一个小数目了,正如 Google 和 Bing 指出的,亚秒级的延迟都会 对用户的参与产生明显的影响

还有一件事需要指出,那就是这个测试并非测量 100 个、1000 个或者 5000 个模块的每个模块的精确运行时间。因为这还与你对 require() 函数的使用有关。在这些包中,我采用的是对每个模块调用一次 require() 函数。但如果你每个模块调用了多次 require() 函数(这在代码库中非常常见)或者你多次动态调用 require() 函数(例如在子函数中调用 require() 函数),那么你将发现明显的性能退化。

Reddit 的移动站点就是一个很好的例子。虽然该站点有 1050 个模块,但是我测量了它们使用 Browserify 的实际执行时间后发现比“1000 个模块”的测试结果差好多。当使用那台运行 Chrome 的 Nexus 5 时,我测出 Reddit 的 Browserify require() 函数耗时 2.14 秒。而那个“1000 个模块”脚本中的等效函数只需要 197 毫秒(在搭载 i7 处理器的 Surface Book 上的桌面版 Chrome,我测出的结果分别为 559 毫秒与 37 毫秒,虽然给出桌面平台的结果有些令人惊讶)。

这结果提示我们有必要对每个模块使用多个 require() 函数的情况再进行一次测试。不过,我并不认为这对 Browserify 和 Webpack 会是一个公平的测试,因为 Rollup 和 Closure 都会将重复的 ES6 库导入处理为一个的顶级变量声明,同时也阻止了顶层空间以外的其他区域的导入。所以根本上来说,Rollup 和 Closure 中一个导入和多个导入的开销是相同的,而对于 Browserify 和 Webpack,运行开销随 require() 函数的数量线性增长。

为了我们这个分析的目的,我认为最好假设模块的数量是性能的短板。而事实上,“5000 个模块”也是一个比“5000 个 require() 函数调用”更好的度量标准。

结论

首先,bundle-collapser 对 Browserify 来说是一个非常有用的插件。如果你在产品中还没使用它,那么你的包将相对来说会略大且运行略慢(虽然我得承认这之间的差异非常小)。另一方面,你还可以转换到 Webpack 以获得更快的包而不需要额外的配置(其实我非常不愿意这么说,因为我是个顽固的 Browserify 粉)。

不管怎样,这些结果都明确地指出 Webpack 和 Browserify 相较 Rollup 和 Closure Compiler 而言表现都稍差,并且性能差异随着模块大小的增大而增大。不幸的是,我并不确定 Webpack 2 是否能解决这些问题,因为尽管他们将 从 Rollup 中借鉴一些想法,但是看起来他们的关注点更多在于 tree-shaking 方面 而不是在于 scope-hoisting 方面。(更新:一个更好的名字称为 内联 inlining ,并且 Webpack 团队 正在做这方面的工作。)

给出这些结果之后,我对 Closure Compiler 和 Rollup 在 JavaScript 社区并没有得到过多关注而感到惊讶。我猜测或许是因为(前者)需要依赖 Java,而(后者)仍然相当不成熟并且未能做到开箱即用(详见 Calvin’s Metcalf 的评论 中作的不错的总结)。

即使没有足够数量的 JavaScript 开发者加入到 Rollup 或 Closure 的队伍中,我认为 npm 包作者们也已准备好了去帮助解决这些问题。如果你使用 npm 安装 lodash,你将会发其现主要的导入是一个巨大的 JavaScript 模块,而不是你期望的 Lodash 的 超模块 hyper-modular 特性(require('lodash/uniq')require('lodash.uniq') 等等)。对于 PouchDB,我们做了一个类似的声明以 使用 Rollup 作为预发布步骤,这将产生对于用户而言尽可能小的包。

同时,我创建了 rollupify 来尝试将这过程变得更为简单一些,只需拖动到已存在的 Browserify 工程中即可。其基本思想是在你自己的项目中使用 导入 import 导出 export (可以使用 cjs-to-es6 来帮助迁移),然后使用 require() 函数来载入第三方包。这样一来,你依旧可以在你自己的代码库中享受所有模块化的优点,同时能导出一个适当大小的大模块来发布给你的用户。不幸的是,你依旧得为第三方库付出一些代价,但是我发现这是对于当前 npm 生态系统的一个很好的折中方案。

所以结论如下:一个大的 JavaScript 包比一百个小 JavaScript 模块要快。尽管这是事实,我依旧希望我们社区能最终发现我们所处的困境————提倡小模块的原则对开发者有利,但是对用户不利。同时希望能优化我们的工具,使得我们可以对两方面都有利。

福利时间!三款桌面浏览器

通常来说我喜欢在移动设备上运行性能测试,因为在这里我们能更清楚的看到差异。但是出于好奇,我也分别在一台搭载 i7 的 Surface Book 上的 Chrome 52、Edge 14 和 Firefox 48 上运行了测试。这分别是它们的测试结果:

Chrome 52 (查看表格)

Chrome 结果

Edge 14 (查看表格)

Edge 结果

Firefox 48 (查看表格)

Firefox 结果

我在这些结果中发现的有趣的地方如下:

  1. bundle-collapser 总是与 slam-dunk 完全不同。
  2. Rollup 和 Closure 的下载时间与运行时间之比总是非常高,它们的运行时间基本上微不足道。ChakraCore 和 SpiderMonkey 运行最快,V8 紧随其后。

如果你的 JavaScript 非常大并且是延迟加载,那么第二点将非常重要。因为如果你可以接受等待网络下载的时间,那么使用 Rollup 和 Closure 将会有避免界面线程冻结的优点。也就是说,它们将比 Browserify 和 Webpack 更少出现界面阻塞。

更新:在这篇文章的回应中,JDD 已经 给 Webpack 提交了一个 issue。还有 一个是给 Browserify 的

更新 2:Ryan Fitzer 慷慨地增加了 RequireJS 和包含 Almond 的 RequireJS 的测试结果,两者都是使用 AMD 而不是 CommonJS 或者 ES6。

测试结果表明 RequireJS 具有 最大的包大小 但是令人惊讶的是它的运行开销 与 Rollup 和 Closure 非常接近。这是在运行 Chrome 52 的 Nexus 5 下限制网速为 3G 的测试结果:

Nexus 5 (3G) RequireJS 结果

更新 3: 我写了一个 optimize-js ,它会减少一些函数内的函数的解析成本。


via: https://nolanlawson.com/2016/08/15/the-cost-of-small-modules/

作者:Nolan 译者:Yinr 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

名词缩写:

  • API 应用程序接口 Application Program Interface
  • ABI 应用系统二进制接口 Application Binary Interface

设备驱动是操作系统的一部分,它能够通过一些特定的编程接口便于硬件设备的使用,这样软件就可以控制并且运行那些设备了。因为每个驱动都对应不同的操作系统,所以你就需要不同的 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 请求数据包 IO Request Packets (IRP),IRP 可以被用来在驱动层识别请求并且传输数据。

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

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

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

1.2. Linux 驱动架构

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

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

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

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

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

2. 设备驱动 API

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

2.1. 初始化

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

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

2.2. 命名和声明设备

在 Windows 上注册设备

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

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

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

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

在 Linux 上注册设备

在 Linux 平台上,用户应用通过文件系统入口访问设备,它通常位于 /dev 目录。在模块初始化的时候,它通过调用内核函数 register_chrdev 创建了所有需要的入口。应用可以发起 open 系统调用来获取一个文件描述符来与设备进行交互。这个调用后来被发送到回调函数,这个调用(以及将来对该返回的文件描述符的进一步调用,例如 readwriteclose)会被分配到由该模块安装到 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 都支持在用户级应用程序和内核级驱动程序之间传输数据的三种方式:

  • 缓冲型输入输出 Buffered Input-Output 它使用由内核管理的缓冲区。对于写操作,内核从用户空间缓冲区中拷贝数据到内核分配的缓冲区,并且把它传送到设备驱动中。读操作也一样,由内核将数据从内核缓冲区中拷贝到应用提供的缓冲区中。
  • 直接型输入输出 Direct Input-Output 它不使用拷贝功能。代替它的是,内核在物理内存中钉死一块用户分配的缓冲区以便它可以一直留在那里,以便在数据传输过程中不被交换出去。
  • 内存映射 Memory mapping 它也能够由内核管理,这样内核和用户空间应用就能够通过不同的地址访问同样的内存页。
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_usercopy_to_userstrncpy_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 还支持通过 KDBKGDB 进行的交互式调试。调试支持可以内置到内核,并可在启动时启用。之后,可以直接通过物理键盘调试系统,或通过串行端口从另一台计算机连接到它。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 不提供稳定的二进制接口,因此有必要在每次内核更新时重新编译和更新所有必需的设备驱动程序。显然,内置在内核中的设备驱动程序会自动更新,但是树外模块会产生轻微的问题。 维护最新的模块二进制文件的任务通常用 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 设备驱动程序开发做的对比,有助于您理解它们,并在设备驱动程序开发过程的研究中,将此作为一个伟大的起点。


via: http://xmodulo.com/linux-vs-windows-device-driver-model.html

作者:Dennis Turpitka 译者:FrankXinqi &YangYang 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这篇教程提供了一个面向 C++ 程序员关于 protocol buffers 的基础介绍。通过创建一个简单的示例应用程序,它将向我们展示:

  • .proto 文件中定义消息格式
  • 使用 protocol buffer 编译器
  • 使用 C++ protocol buffer API 读写消息

这不是一个关于在 C++ 中使用 protocol buffers 的全面指南。要获取更详细的信息,请参考 Protocol Buffer Language GuideEncoding Reference

为什么使用 Protocol Buffers

我们接下来要使用的例子是一个非常简单的"地址簿"应用程序,它能从文件中读取联系人详细信息。地址簿中的每一个人都有一个名字、ID、邮件地址和联系电话。

如何序列化和获取结构化的数据?这里有几种解决方案:

  • 以二进制形式发送/接收原生的内存数据结构。通常,这是一种脆弱的方法,因为接收/读取代码必须基于完全相同的内存布局、大小端等环境进行编译。同时,当文件增加时,原始格式数据会随着与该格式相关的软件而迅速扩散,这将导致很难扩展文件格式。
  • 你可以创造一种 ad-hoc 方法,将数据项编码为一个字符串——比如将 4 个整数编码为 12:3:-23:67。虽然它需要编写一次性的编码和解码代码且解码需要耗费一点运行时成本,但这是一种简单灵活的方法。这最适合编码非常简单的数据。
  • 序列化数据为 XML。这种方法是非常吸引人的,因为 XML 是一种适合人阅读的格式,并且有为许多语言开发的库。如果你想与其他程序和项目共享数据,这可能是一种不错的选择。然而,众所周知,XML 是空间密集型的,且在编码和解码时,它对程序会造成巨大的性能损失。同时,使用 XML DOM 树被认为比操作一个类的简单字段更加复杂。

Protocol buffers 是针对这个问题的一种灵活、高效、自动化的解决方案。使用 Protocol buffers,你需要写一个 .proto 说明,用于描述你所希望存储的数据结构。利用 .proto 文件,protocol buffer 编译器可以创建一个类,用于实现对高效的二进制格式的 protocol buffer 数据的自动化编码和解码。产生的类提供了构造 protocol buffer 的字段的 getters 和 setters,并且作为一个单元来处理读写 protocol buffer 的细节。重要的是,protocol buffer 格式支持格式的扩展,代码仍然可以读取以旧格式编码的数据。

在哪可以找到示例代码

示例代码被包含于源代码包,位于“examples”文件夹。可在这里下载代码。

定义你的协议格式

为了创建自己的地址簿应用程序,你需要从 .proto 开始。.proto 文件中的定义很简单:为你所需要序列化的每个数据结构添加一个 消息 message ,然后为消息中的每一个字段指定一个名字和类型。这里是定义你消息的 .proto 文件 addressbook.proto

package tutorial;

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

message AddressBook {
  repeated Person person = 1;
}

如你所见,其语法类似于 C++ 或 Java。我们开始看看文件的每一部分内容做了什么。

.proto 文件以一个 package 声明开始,这可以避免不同项目的命名冲突。在 C++,你生成的类会被置于与 package 名字一样的命名空间。

下一步,你需要定义 消息 message 。消息只是一个包含一系列类型字段的集合。大多标准的简单数据类型是可以作为字段类型的,包括 boolint32floatdoublestring。你也可以通过使用其他消息类型作为字段类型,将更多的数据结构添加到你的消息中——在以上的示例,Person 消息包含了 PhoneNumber 消息,同时 AddressBook 消息包含 Person 消息。你甚至可以定义嵌套在其他消息内的消息类型——如你所见,PhoneNumber 类型定义于 Person 内部。如果你想要其中某一个字段的值是预定义值列表中的某个值,你也可以定义 enum 类型——这儿你可以指定一个电话号码是 MOBILEHOMEWORK 中的某一个。

每一个元素上的 = 1= 2 标记确定了用于二进制编码的唯一 “标签” tag 。标签数字 1-15 的编码比更大的数字少需要一个字节,因此作为一种优化,你可以将这些标签用于经常使用的元素或 repeated 元素,剩下 16 以及更高的标签用于非经常使用的元素或 optional 元素。每一个 repeated 字段的元素需要重新编码标签数字,因此 repeated 字段适合于使用这种优化手段。

每一个字段必须使用下面的修饰符加以标注:

  • required:必须提供该字段的值,否则消息会被认为是 “未初始化的” uninitialized 。如果 libprotobuf 以调试模式编译,序列化未初始化的消息将引起一个断言失败。以优化形式构建,将会跳过检查,并且无论如何都会写入该消息。然而,解析未初始化的消息总是会失败(通过 parse 方法返回 false)。除此之外,一个 required 字段的表现与 optional 字段完全一样。
  • optional:字段可能会被设置,也可能不会。如果一个 optional 字段没被设置,它将使用默认值。对于简单类型,你可以指定你自己的默认值,正如例子中我们对电话号码的 type 一样,否则使用系统默认值:数字类型为 0、字符串为空字符串、布尔值为 false。对于嵌套消息,默认值总为消息的“默认实例”或“原型”,它的所有字段都没被设置。调用 accessor 来获取一个没有显式设置的 optional(或 required) 字段的值总是返回字段的默认值。
  • repeated:字段可以重复任意次数(包括 0 次)。repeated 值的顺序会被保存于 protocol buffer。可以将 repeated 字段想象为动态大小的数组。

你可以查找关于编写 .proto 文件的完整指导——包括所有可能的字段类型——在 Protocol Buffer Language Guide 里面。不要在这里面查找与类继承相似的特性,因为 protocol buffers 不会做这些。

required 是永久性的

在把一个字段标识为 required 的时候,你应该特别小心。如果在某些情况下你不想写入或者发送一个 required 的字段,那么将该字段更改为 optional 可能会遇到问题——旧版本的读者(LCTT 译注:即读取、解析旧版本 Protocol Buffer 消息的一方)会认为不含该字段的消息是不完整的,从而有可能会拒绝解析。在这种情况下,你应该考虑编写特别针对于应用程序的、自定义的消息校验函数。Google 的一些工程师得出了一个结论:使用 required 弊多于利;他们更愿意使用 optionalrepeated 而不是 required。当然,这个观点并不具有普遍性。

编译你的 Protocol Buffers

既然你有了一个 .proto,那你需要做的下一件事就是生成一个将用于读写 AddressBook 消息的类(从而包括 PersonPhoneNumber)。为了做到这样,你需要在你的 .proto 上运行 protocol buffer 编译器 protoc

  1. 如果你没有安装编译器,请下载这个包,并按照 README 中的指令进行安装。
  2. 现在运行编译器,指定源目录(你的应用程序源代码位于哪里——如果你没有提供任何值,将使用当前目录)、目标目录(你想要生成的代码放在哪里;常与 $SRC_DIR 相同),以及你的 .proto 路径。在此示例中:
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto

因为你想要 C++ 的类,所以你使用了 --cpp_out 选项——也为其他支持的语言提供了类似选项。

在你指定的目标文件夹,将生成以下的文件:

  • addressbook.pb.h,声明你生成类的头文件。
  • addressbook.pb.cc,包含你的类的实现。

Protocol Buffer API

让我们看看生成的一些代码,了解一下编译器为你创建了什么类和函数。如果你查看 addressbook.pb.h,你可以看到有一个在 addressbook.proto 中指定所有消息的类。关注 Person 类,可以看到编译器为每个字段生成了 读写函数 accessors 。例如,对于 nameidemailphone 字段,有下面这些方法:(LCTT 译注:此处原文所指文件名有误,径该之。)

// name
inline bool has_name() const;
inline void clear_name();
inline const ::std::string& name() const;
inline void set_name(const ::std::string& value);
inline void set_name(const char* value);
inline ::std::string* mutable_name();

// id
inline bool has_id() const;
inline void clear_id();
inline int32_t id() const;
inline void set_id(int32_t value);

// email
inline bool has_email() const;
inline void clear_email();
inline const ::std::string& email() const;
inline void set_email(const ::std::string& value);
inline void set_email(const char* value);
inline ::std::string* mutable_email();

// phone
inline int phone_size() const;
inline void clear_phone();
inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phone() const;
inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phone();
inline const ::tutorial::Person_PhoneNumber& phone(int index) const;
inline ::tutorial::Person_PhoneNumber* mutable_phone(int index);
inline ::tutorial::Person_PhoneNumber* add_phone();

正如你所见到,getters 的名字与字段的小写名字完全一样,并且 setter 方法以 set_ 开头。同时每个 单一 singular requiredoptional)字段都有 has_ 方法,该方法在字段被设置了值的情况下返回 true。最后,所有字段都有一个 clear_ 方法,用以清除字段到 empty 状态。

数字型的 id 字段仅有上述的基本 读写函数 accessors 集合,而 nameemail 字段有两个额外的方法,因为它们是字符串——一个是可以获得字符串直接指针的mutable_ 的 getter ,另一个为额外的 setter。注意,尽管 email 还没被 设置 set ,你也可以调用 mutable_email;因为 email 会被自动地初始化为空字符串。在本例中,如果你有一个单一的(requiredoptional)消息字段,它会有一个 mutable_ 方法,而没有 set_ 方法。

repeated 字段也有一些特殊的方法——如果你看看 repeatedphone 字段的方法,你可以看到:

  • 检查 repeated 字段的 _size(也就是说,与 Person 相关的电话号码的个数)
  • 使用下标取得特定的电话号码
  • 更新特定下标的电话号码
  • 添加新的电话号码到消息中,之后你便可以编辑。(repeated 标量类型有一个 add_ 方法,用于传入新的值)

为了获取 protocol 编译器为所有字段定义生成的方法的信息,可以查看 C++ generated code reference

枚举和嵌套类

.proto 的枚举相对应,生成的代码包含了一个 PhoneType 枚举。你可以通过 Person::PhoneType 引用这个类型,通过 Person::MOBILEPerson::HOMEPerson::WORK 引用它的值。(实现细节有点复杂,但是你无须了解它们而可以直接使用)

编译器也生成了一个 Person::PhoneNumber 的嵌套类。如果你查看代码,你可以发现真正的类型为 Person_PhoneNumber,但它通过在 Person 内部使用 typedef 定义,使你可以把 Person_PhoneNumber 当成嵌套类。唯一产生影响的一个例子是,如果你想要在其他文件前置声明该类——在 C++ 中你不能前置声明嵌套类,但是你可以前置声明 Person_PhoneNumber

标准的消息方法

所有的消息方法都包含了许多别的方法,用于检查和操作整个消息,包括:

  • bool IsInitialized() const; :检查是否所有 required 字段已经被设置。
  • string DebugString() const; :返回人类可读的消息表示,对调试特别有用。
  • void CopyFrom(const Person& from);:使用给定的值重写消息。
  • void Clear();:清除所有元素为空的状态。

上面这些方法以及下一节要讲的 I/O 方法实现了被所有 C++ protocol buffer 类共享的 消息 Message 接口。为了获取更多信息,请查看 complete API documentation for Message

解析和序列化

最后,所有 protocol buffer 类都有读写你选定类型消息的方法,这些方法使用了特定的 protocol buffer 二进制格式。这些方法包括:

  • bool SerializeToString(string* output) const;:序列化消息并将消息字节数据存储在给定的字符串中。注意,字节数据是二进制格式的,而不是文本格式;我们只使用 string 类作为合适的容器。
  • bool ParseFromString(const string& data);:从给定的字符创解析消息。
  • bool SerializeToOstream(ostream* output) const;:将消息写到给定的 C++ ostream
  • bool ParseFromIstream(istream* input);:从给定的 C++ istream 解析消息。

这些只是两个用于解析和序列化的选择。再次说明,可以查看 Message API reference 完整的列表。

Protocol Buffers 和面向对象设计

Protocol buffer 类通常只是纯粹的数据存储器(像 C++ 中的结构体);它们在对象模型中并不是一等公民。如果你想向生成的 protocol buffer 类中添加更丰富的行为,最好的方法就是在应用程序中对它进行封装。如果你无权控制 .proto 文件的设计的话,封装 protocol buffers 也是一个好主意(例如,你从另一个项目中重用一个 .proto 文件)。在那种情况下,你可以用封装类来设计接口,以更好地适应你的应用程序的特定环境:隐藏一些数据和方法,暴露一些便于使用的函数,等等。但是你绝对不要通过继承生成的类来添加行为。这样做的话,会破坏其内部机制,并且不是一个好的面向对象的实践。

写消息

现在我们尝试使用 protocol buffer 类。你的地址簿程序想要做的第一件事是将个人详细信息写入到地址簿文件。为了做到这一点,你需要创建、填充 protocol buffer 类实例,并且将它们写入到一个 输出流 output stream

这里的程序可以从文件读取 AddressBook,根据用户输入,将新 Person 添加到 AddressBook,并且再次将新的 AddressBook 写回文件。这部分直接调用或引用 protocol buffer 类的代码会以“// pb”标出。

#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h" // pb
using namespace std;

// This function fills in a Person message based on user input.
void PromptForAddress(tutorial::Person* person) {
  cout << "Enter person ID number: ";
  int id;
  cin >> id;
  person->set_id(id);   // pb
  cin.ignore(256, '\n');

  cout << "Enter name: ";
  getline(cin, *person->mutable_name());    // pb

  cout << "Enter email address (blank for none): ";
  string email;
  getline(cin, email);
  if (!email.empty()) { // pb
    person->set_email(email);   // pb
  }

  while (true) {
    cout << "Enter a phone number (or leave blank to finish): ";
    string number;
    getline(cin, number);
    if (number.empty()) {
      break;
    }

    tutorial::Person::PhoneNumber* phone_number = person->add_phone();  //pb
    phone_number->set_number(number);   // pb

    cout << "Is this a mobile, home, or work phone? ";
    string type;
    getline(cin, type);
    if (type == "mobile") {
      phone_number->set_type(tutorial::Person::MOBILE); // pb
    } else if (type == "home") {
      phone_number->set_type(tutorial::Person::HOME);   // pb
    } else if (type == "work") {
      phone_number->set_type(tutorial::Person::WORK);   // pb
    } else {
      cout << "Unknown phone type.  Using default." << endl;
    }
  }
}

// Main function:  Reads the entire address book from a file,
//   adds one person based on user input, then writes it back out to the same
//   file.
int main(int argc, char* argv[]) {
  // Verify that the version of the library that we linked against is
  // compatible with the version of the headers we compiled against.
  GOOGLE_PROTOBUF_VERIFY_VERSION;   // pb

  if (argc != 2) {
    cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
    return -1;
  }

  tutorial::AddressBook address_book;   // pb

  {
    // Read the existing address book.
    fstream input(argv[1], ios::in | ios::binary);
    if (!input) {
      cout << argv[1] << ": File not found.  Creating a new file." << endl;
    } else if (!address_book.ParseFromIstream(&input)) {    // pb
      cerr << "Failed to parse address book." << endl;
      return -1;
    }
  }

  // Add an address.
  PromptForAddress(address_book.add_person());  // pb

  {
    // Write the new address book back to disk.
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!address_book.SerializeToOstream(&output)) {    // pb
      cerr << "Failed to write address book." << endl;
      return -1;
    }
  }

  // Optional:  Delete all global objects allocated by libprotobuf.
  google::protobuf::ShutdownProtobufLibrary();  // pb

  return 0;
}

注意 GOOGLE_PROTOBUF_VERIFY_VERSION 宏。它是一种好的实践——虽然不是严格必须的——在使用 C++ Protocol Buffer 库之前执行该宏。它可以保证避免不小心链接到一个与编译的头文件版本不兼容的库版本。如果被检查出来版本不匹配,程序将会终止。注意,每个 .pb.cc 文件在初始化时会自动调用这个宏。

同时注意在程序最后调用 ShutdownProtobufLibrary()。它用于释放 Protocol Buffer 库申请的所有全局对象。对大部分程序,这不是必须的,因为虽然程序只是简单退出,但是 OS 会处理释放程序的所有内存。然而,如果你使用了内存泄漏检测工具,工具要求全部对象都要释放,或者你正在写一个 Protocol Buffer 库,该库可能会被一个进程多次加载和卸载,那么你可能需要强制 Protocol Buffer 清除所有东西。

读取消息

当然,如果你无法从它获取任何信息,那么这个地址簿没多大用处!这个示例读取上面例子创建的文件,并打印文件里的所有内容。

#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h" // pb
using namespace std;

// Iterates though all people in the AddressBook and prints info about them.
void ListPeople(const tutorial::AddressBook& address_book) {    // pb
  for (int i = 0; i < address_book.person_size(); i++) {        // pb
    const tutorial::Person& person = address_book.person(i);    // pb

    cout << "Person ID: " << person.id() << endl;   // pb
    cout << "  Name: " << person.name() << endl;    // pb
    if (person.has_email()) {   // pb
      cout << "  E-mail address: " << person.email() << endl;   // pb
    }

    for (int j = 0; j < person.phone_size(); j++) { // pb
      const tutorial::Person::PhoneNumber& phone_number = person.phone(j);  // pb

      switch (phone_number.type()) {    // pb
        case tutorial::Person::MOBILE:  // pb
          cout << "  Mobile phone #: ";
          break;
        case tutorial::Person::HOME:    // pb
          cout << "  Home phone #: ";
          break;
        case tutorial::Person::WORK:    // pb
          cout << "  Work phone #: ";
          break;
      }
      cout << phone_number.number() << endl;    // ob
    }
  }
}

// Main function:  Reads the entire address book from a file and prints all
//   the information inside.
int main(int argc, char* argv[]) {
  // Verify that the version of the library that we linked against is
  // compatible with the version of the headers we compiled against.
  GOOGLE_PROTOBUF_VERIFY_VERSION;   // pb

  if (argc != 2) {
    cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
    return -1;
  }

  tutorial::AddressBook address_book;   // pb

  {
    // Read the existing address book.
    fstream input(argv[1], ios::in | ios::binary);
    if (!address_book.ParseFromIstream(&input)) {   // pb
      cerr << "Failed to parse address book." << endl;
      return -1;
    }
  }

  ListPeople(address_book);

  // Optional:  Delete all global objects allocated by libprotobuf.
  google::protobuf::ShutdownProtobufLibrary();  // pb

  return 0;
}

扩展 Protocol Buffer

或早或晚在你发布了使用 protocol buffer 的代码之后,毫无疑问,你会想要 "改善" protocol buffer 的定义。如果你想要新的 buffers 向后兼容,并且老的 buffers 向前兼容——几乎可以肯定你很渴望这个——这里有一些规则,你需要遵守。在新的 protocol buffer 版本:

  • 你绝不可以修改任何已存在字段的标签数字
  • 你绝不可以添加或删除任何 required 字段
  • 你可以删除 optionalrepeated 字段
  • 你可以添加新的 optionalrepeated 字段,但是你必须使用新的标签数字(也就是说,标签数字在 protocol buffer 中从未使用过,甚至不能是已删除字段的标签数字)。

(对于上面规则有一些例外情况,但它们很少用到。)

如果你能遵守这些规则,旧代码则可以欢快地读取新的消息,并且简单地忽略所有新的字段。对于旧代码来说,被删除的 optional 字段将会简单地赋予默认值,被删除的 repeated 字段会为空。新代码显然可以读取旧消息。然而,请记住新的 optional 字段不会呈现在旧消息中,因此你需要显式地使用 has_ 检查它们是否被设置或者在 .proto 文件在标签数字后使用 [default = value] 提供一个合理的默认值。如果一个 optional 元素没有指定默认值,它将会使用类型特定的默认值:对于字符串,默认值为空字符串;对于布尔值,默认值为 false;对于数字类型,默认类型为 0。注意,如果你添加一个新的 repeated 字段,新代码将无法辨别它被留空(被新代码)或者从没被设置(被旧代码),因为 repeated 字段没有 has_ 标志。

优化技巧

C++ Protocol Buffer 库已极度优化过了。但是,恰当的用法能够更多地提高性能。这里是一些技巧,可以帮你从库中挤压出最后一点速度:

  • 尽可能复用消息对象。即使它们被清除掉,消息也会尽量保存所有被分配来重用的内存。因此,如果我们正在处理许多相同类型或一系列相似结构的消息,一个好的办法是重用相同的消息对象,从而减少内存分配的负担。但是,随着时间的流逝,对象可能会膨胀变大,尤其是当你的消息尺寸(LCTT 译注:各消息内容不同,有些消息内容多一些,有些消息内容少一些)不同的时候,或者你偶尔创建了一个比平常大很多的消息的时候。你应该自己通过调用 SpaceUsed 方法监测消息对象的大小,并在它太大的时候删除它。
  • 对于在多线程中分配大量小对象的情况,你的操作系统内存分配器可能优化得不够好。你可以尝试使用 google 的 tcmalloc

高级用法

Protocol Buffers 绝不仅用于简单的数据存取以及序列化。请阅读 C++ API reference 来看看你还能用它来做什么。

protocol 消息类所提供的一个关键特性就是 反射 reflection 。你不需要针对一个特殊的消息类型编写代码,就可以遍历一个消息的字段并操作它们的值。一个使用反射的有用方法是 protocol 消息与其他编码互相转换,比如 XML 或 JSON。反射的一个更高级的用法可能就是可以找出两个相同类型的消息之间的区别,或者开发某种“协议消息的正则表达式”,利用正则表达式,你可以对某种消息内容进行匹配。只要你发挥你的想像力,就有可能将 Protocol Buffers 应用到一个更广泛的、你可能一开始就期望解决的问题范围上。

反射是由 Message::Reflection interface 提供的。


via: https://developers.google.com/protocol-buffers/docs/cpptutorial

作者:Google 译者:cposture 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出