您当前的位置:首页 > 计算机 > 软件应用 > 开发工具(IDE)

调试 Emacs 我是如何学会停止焦虑并爱上 DTrace

时间:12-14来源:作者:点击数:
CDSY,CDSY.XYZ

有一段时间 Elfeed(link:https://github.com/skeeto/elfeed) 出现了一个奇怪的、虚假的失败。用户在更新 feed 时经常 看到一个错误(link:https://github.com/skeeto/elfeed/issues/248)(剧透警告)error in process sentinel: Search failed.

如果你使用 Elfeed,可能自己也遇到过。表面上看很明显是 curl(其任务是 负责下载 feed 数据(link:http://nullprogram.com/blog/2016/06/16/))运行成功但输出并不完整。由于运行是成功的,因此 Elfeed 假设数据已经全部在 curl 的输出缓冲区中了,但是结果不是这样,所以出现了严重的错误。

不幸的是,这个问题无法重现。在 Emacs 之外手动运行curl不会发现任何问题。 让Elfeed重新获取feed也完全没问题的。只有当 Elfeed 在压力下同时获取多个feed时,这个问题才会随机出现。

而且在报错误之前,curl 进程就已经退出了,重要的调试信息已经丢失了。这看起来像是 Emacs 本身的一个 bug,并没有可靠的方法能从 Emacs Lisp 中捕获必要的调试信息。

而且,的确,后来被证明事实就是如此。

一种快速而粗糙的变通方法是使用 condition-case 捕获并吞下抛出的错误。

当出现这一奇怪的问题时,Elfeed 不会在用户面前显示为严重的错误,而是会尝试吞下这个错误(如果它可以可靠地被检测到的话),并将其视为简单的失败。

这个变通方案让我很不舒服。 Elfeed 已经对错误进行了详尽的检查。肯定有那个地方部队,总有一天我会当场找出原因。我只需要在自己机器上见证这个 bug 就行了。Elfeed 是我日常生活的一部分,所以我有一天肯定也会遇到这个问题。我的计划是,如果那一天到来了的话,就运行一个修改过的 Elfeed,用来捕获额外的数据。我还会在 GDB 下定期运行 Emacs,以便更深入地检查故障。

现在我只能等到 时机成熟(link:https://www.youtube.com/watch?v=fE2KDzZaxvE)。

Bryan Cantrill, DTrace 和 FreeBSD

Besides what I've already linked in this article, here are a couple more great presentations:

在假期间,我重新发现了 Bryan Cantrill(link:https://en.wikipedia.org/wiki/Bryan_Cantrill),他是一名系统软件工程师,曾在1996年至2010年间为Sun工作,其最出名的作品就是 DTrace(link:http://dtrace.org/blogs/about/)。

我第一次见到他是在2015年的一个 BSD Now访谈上(link:https://www.youtube.com/watch?v=l6XQUciI-Sc)年。我重看了那次访谈,觉得自己还有很多东西要向他学习。他成了我心目中的英雄。所以我在网上搜索了 更多他的写作和演讲(link:http://dtrace.org/blogs/bmc/2018/02/03/talks/)。

除了本文提到的,这里还有一些很棒的演讲:

  • 软件工程中的口头传统(link:https://www.youtube.com/watch?v=4PaWFYm0kEw)
  • Fork Yeah! illumos的兴起与发展(link:https://www.youtube.com/watch?v=-zRN7XLCRhc)

你还可以 在他的DTrace博客中(link:http://dtrace.org/blogs/bmc/) 找到一些文章。

在Sun的最后15年左右的时间里,一些有趣的操作系统技术诞生了——最著名的就是DTrace和ZFS——Bryan对此充满激情。

幸运的是,由于Sun在最后一刻以开放源码的形式发布了这些技术,使得大部分技术都在甲骨文的收购中幸存了下来。否则这些技术将会永远地失去了。

四散的前Sun员工们仍然对之前在Sun的工作充满热情,他们与一些老客户一起,收集了这些技术碎片,并组建了 illumos(link:https://illumos.org/)社区,就像一个开源舰队一样。

自然,我想亲自尝试一下。真的像他们说的那么好吗? 通常我坚持使用Linux,但它(通常)没有这些Sun技术。主要原因是许可证不兼容。Sun发布的代码是 CDDL(link:https://opensource.org/licenses/CDDL-1.0),与GPL不兼容。

Ubuntu / 确实 / 以包含ZFS而臭名昭著(link:https://insights.ubuntu.com/2016/02/18/zfs-licensingandlinux/),但其他发行版不愿意冒这个风险。移植 DTrace 是一项艰巨的任务,因为它涉及整个内核,这也使得许可问题变得更加复杂。

Linux 以 Not Invented Here (NIH)综合症而闻名,许可问题肯定是造成这种情况的原因之一。

它们不采纳 ZFS 和 DTrace,而是从零开始重新设计:用 btrfs 代替 ZFS,用 大量其他工具(link:http://www.brendangregg.com/blog/2015-07-08/choosing-a-linuxtracer.html) 代替 DTrace。

通常,我对系统调用跟踪最感兴趣,我对标的是 strace(link:https://en.wikipedia.org/wiki/Strace),当然它有其局限性——比如在 Emacs 下调试 curl 的这种情况。

NIH 的另一个著名的例子是 Linux 的 epoll(2)(link:http://man7.org/linux/man-pages/man7/epoll.7.html),它是 BSD kqueue(2)(link:https://www.freebsd.org/cgi/man.cgi?query%60kqueue&sektion%602) 的 简陋(link:https://idea.popcount.org/2017-02-20-epoll-is-fundamentally-broken-12/) 版本(link:https://idea.popcount.org/2017-03-20-epoll-is-fundamentally-broken-22/)

所以,如果我想自己尝试这些技术,就需要安装一个不同的操作系统。我已经入手了 OmniOS(link:https://omnios.omniti.com/),一个建立在 illumos 上的操作系统.我用它在虚拟机中搭建了一个陌生的环境来测试一些自己的软件(例如 enchive(link:https://file+.vscode-resource.vscode-cdn.net/i:/%E4%B8%8B%E8%BD%BD/emacs-document-master/blog/2017/03/12))。

OmniOS 有一种哲学叫做 Keep Your Software To Yourself(link:https://omnios.omniti.com/wiki.php/KYSTY)(KYSTY),实际上就是只编码不打包。

说实话,你不能怪他们,因为 他们是一个小社区(link:https://utcc.utoronto.ca/~cks/space/blog/solaris/IllumosSupportLimits)。

最好的解决方案可能就是使用 pkgsrc(link:https://www.pkgsrc.org/),它本质上是一个通用的打包系统。否则 你就得靠自己了(link:http://nullprogram.com/blog/2017/06/19/)。

还有 openindiana(link:https://www.openindiana.org/),这是一个更友好的面向桌面的 illumos 发行版。

总之,当事情不顺利的时候,你只能靠自己。

这种情况就像几十年前运行 Linux 一样,那时跑 Linux 十分困难。

如果您有兴趣尝试 DTrace,那么目前最简单的方法恐怕就是 FreeBSD(link:https://www.freebsd.org/)了。它有一个庞大的、活跃的社区、完整的文档和大量的包选择。它的许可证(BSD 许可证)与 CDDL 兼容,因此 ZFS 和 DTrace 都已移植到 FreeBSD 了。

DTrace 是啥?

讲了这么多,但是还没有说 DTrace到底是什么(link:https://wiki.freebsd.org/DTrace/Tutorial)。我不会再写一份教程,但会提供足够的信息来源供你学习。

DTrace 是一个用于 /实时/ 调试生产系统内核和应用程序的跟踪框架。这里的 生产系统 意味着它非常稳定和安全的——使用 DTrace 不会将您的系统置于崩溃或损坏数据的风险中。“实时”意味着它对性能的影响很小。

您可以在实时、活动的系统上使用 DTrace,而且对其影响很小。这两个核心设计原则对于解决那些只在生产中出现的棘手 bug 非常重要。DTrace 的 /探针/ 分散在整个系统中: 在系统调用中、调度器事件中、网络事件中、进程事件中、信号中、虚拟内存事件等。

它使用一种称为D的专门语言(与通用编程语言D无关),您可以在这些指令点动态地添加行为。

这些行为通常用来捕获信息,但是它也可以操作正在跟踪的事件。每个探针由冒号分隔的4元标识组成:提供者、模块、函数和探测名称。空元素表示通配符。例如:syscall::open:entry 是位于 open(2) 入口的探针(即entry),syscall:::entry 则匹配所有系统调用的入口探测。

与 Linux 上监视特定进程的 strace 不同,DTrace 应用于整个系统。

要在 Emacs 的 strace 下运行 curl,就必须修改 Emacs 的行为。而用 DTrace,我可以测量每个 curl 进程,不需要对 Emacs 做任何更改,且对 Emacs 的影响可以忽略不计。这很重要。

因此,就这个 Elfeed 问题,更适合在 FreeBSD 中调试这个问题。 我所要做的就是当场抓住它。然而,距离那个 bug 报告已经过去几个月了,我还不明所以。我只希望最终能找到一个可以应用 DTrace 的有趣问题。

树莓 Pi 2 上的 FreeBSD

因此我选择了在 FreeBSD 运行这些技术,我要做的就是决定在哪里运行 FreeBSD 而已。我可以在虚拟机中跑,但是在真正的硬件上尝试总是更有趣。

FreeBSD支持树莓派2(link:https://wiki.freebsd.org/FreeBSD/arm/Raspberry%20Pi),我有一个树莓派2在那做灰,所以我把他用起来了。

我把镜像写到 SD 卡上,这几天来我一直在折腾这个新系统。我克隆了几十个自己的git仓库,对其进行构建和测试,并掌握了一些门道。

我第一次试用了 ports 系统,主要是为了确定低功耗的 Raspberry Pi 2 需要几天时间来构建那些我想要尝试的包。我 这些天主要用Vim编程(link:http://nullprogram.com/blog/2017/04/01/),所以前几天我并没有我配置 Emacs。最后,我确实构建了 Emacs,克隆了我的配置,启动它,并给尝试了一下 Elfeed。

这时 搜索失败 的 bug 就来了!不是一次,而是几十次。完美! 这个低功耗的平台简直就是转为这个 bug 而生的,它总是会触发这个 bug。考虑到我已经有了 DTrace,它真是调试这个 BUG 的完美场所。有些东西在对 Elfeed 撒谎,DTrace 将扮演法官。

在开始之前,我觉得有三种可能性:

  1. curl 运行成功,但是截断了输出。
  2. Emacs 悄悄地截断了 curl 的输出。
  3. Emacs 搞错了 curl 的退出状态。

使用 Dtrace,我可以观察每个 curl 进程向 Emacs 写入的内容,还可以重新检查 curl 的退出状态。我使用了以下(新手)DTrace 脚本:

syscall::write:entry
/execname == "curl"/
  {
    printf("%d WRITE %d "%s"n",
          pid, arg2, stringof(copyin(arg1, arg2)));
  }

syscall::exit:entry
/execname == "curl"/
  {
    printf("%d EXIT %dn", pid, arg0);
  }

/execname == "curl"/ 是一个判断条件,它的作用是(显然)只触发curl进程的行为。第一个探针为curl中的每个 write(2) 打印一行信息。arg0arg1arg2 对应的 write(2): 的 fd、buf、count 参数。

它记录写入的进程 ID (pid)、写入的长度和实际写入的内容。请记住,这些curl进程是由Emacs并行运行的,因此进程 id 可以让我将独立的写和退出状态关联起来。

第二个探针输出 pid 和退出状态(exit(2) 的第一个参数)。

我也想比较一下,当 curl 退出时,究竟送了什么到 Elfeed,所以我修改 process sentinel(link:http://www.gnu.org/software/emacs/manual/html_node/elisp/Sentinels.html) ------子进程退出时的回调函数——在退出前调用 write-file,我可以将这些缓冲区转储与 DTrace 生成的日志进行比较。结果有两个重要的发现。

首先,当 搜索失败 bug 发生时,缓冲区完全是空的(95% 的情况下),或者 HTTP 头文件末尾的空白行被截断(5% 的情况下)。DTrace 表明 curl 已经充分完成了工作,所以 Emacs 才是说谎者。它并没有将 curl 的所有数据传递给 Elfeed。这很麻烦。其次, curl对行进行了缓冲,每一行都是独立的 write(2),我肯定 没 想到会这样。

通常,C 库只在输出为终端时进行行缓冲。这是因为它猜测用户可能正在观看,期望一次输出一行。

下面是它在日志中的样子:

88188 WRITE 32 "Server: Apache/2.4.18 (Ubuntu)
"
88188 WRITE 46 "Location: https://blog.plover.com/index.atom
"
88188 WRITE 21 "Content-Length: 299
"
88188 WRITE 45 "Content-Type: text/html; charset=iso-8859-1
"
88188 WRITE 2 "
"

curl 为什么会认为 Emacs 是终端呢?

哦 对了。 这就是我四年前写EmacSQL时遇到的问题(link:http://nullprogram.com/blog/2014/02/06/)。

默认情况下,Emacs 通过一个伪终端(pty)连接到子进程。我当时认为这是 Emacs 中的一个错误,现在我仍然坚持这个说法。pty 会导致一些奇怪的、烦人的问题,而且意义不大:

  • 它会解释控制字符。希望你没有传输二进制数据!
  • 子进程通常会进行行缓冲。这使它们变慢,尽管在某些情况你可能就想这样。
  • Stdout 和 stderr 混合在一起。(至 Emacs 25 之后,该特性变成可选的了。)
  • Emacs 中有一个 bug,当大量并行使用 ptys 时会导致截断。

仅仅通过观察 DTrace 日志,我就知道该怎么做了:将 pty 转储到管道中。这是由 process-connection-type 变量控制的,并 只用一行代码就修复了它(link:https://github.com/skeeto/elfeed/commit/945765a57d2f27996b643bc62e803dc167d1547)。

这不仅完全解决了截断问题,而且 Elfeed 在所有机器上获取 feed 的速度也明显更快。它不再一次一行地接收大量XML,而是像用吸管吸布丁一样。现在它甚至在我的树莓派2上也很顺畅,以前从未有过这种情况(再没有 搜索失败 的bug)。即使您从未受到此 bug 的影响,您也将从这一修复中获益。

我还没有正式将其报告为 Emacs bug,因为可重现性仍然是一个问题。上报 BUG 需要比 用树莓派在互联网上并行地发出一堆 HTTP 请求 更好的内容。这个解决方案让我想起了 老锅炉工的故事(link:https://www.buzzmaven.com/old-engineer-hammer-2/):挥起锤子就要收一大笔钱。一旦问题出现, DTrace 就迅速帮助确定用锤子攻击 Emacs 的位置。

最后,非常感谢 alphapapa 几个月前花时间报告这个 bug。

CDSY,CDSY.XYZ
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐