下载本文的PDF版本 PDF

代码探险回顾

理解他人的代码是否变得更容易了?

George V. Neville-Neil,顾问

自从我第一次撰写关于代码探险的文章8以来已经五年了,尽管系统规模和范围持续增长,但我们用来理解这些系统的工具并没有以相同的速度增长。事实上,我认为我们正在稳步倒退。那么我们为什么要重蹈覆辙呢?这个主题是否重要到值得在五年内发表两篇文章?我认为是的。

经常被引用的关于计算机性能不断提升的摩尔定律实际上对代码探险者不利。计算机性能越强大,我们要求它们做的事情就越多,这增加了运行在其上的软件的复杂性。处理器速度提高了,这意味着在相同的时间内可以运行更多的代码行。可用内存变得更大,因此我们现在可以在内存中保存更多的状态或代码。磁盘变得更大并且需要的功耗更少(在闪存的情况下),突然间我们能够随身携带曾经被认为是海量的数据。20世纪70年代所谓的“软件危机”从未真正消退,因为每次软件工程师提出一种新的工作方式来降低复杂性时,行业都会向前发展并提出更高的要求。

复杂性在多个方向上增加,包括代码行数、模块数量以及系统和子系统数量。更复杂的系统需要更多的代码行来实现。随着系统规模的增长,软件团队经常集成来自外部资源的更多代码,这导致系统之间复杂的交互,而这些系统可能在设计时并未考虑到大规模集成。

这些数字对任何软件工程师来说都不应该感到惊讶,但它们是令人担忧的原因。尽管数字不太可能缩小,但除了一个数字外,所有数字都增长了50%以上,而且尽管代码行数可能呈线性增长,但这些数字所代表的新组件之间的交互并没有呈线性增长。如果我们假设系统中的所有模块都可以与其他所有模块自由交互,那么我们得到的系统中的潜在交互数量可以用 n(n-1)/2 表示,这个公式对于那些从事网络工作的人来说应该很熟悉,因为它代表一个完全连接的网络。如果一个系统从 100 个模块增长到 200 个模块,增长率为 100%,那么潜在连接的数量将从 4,950 增长到 19,900,增长率为 302%。

衡量系统接口数量的一个可靠指标是操作系统内核提供给用户程序的系统调用数量。自从我发表第一篇关于代码探险的文章以来,Linux 内核已从略低于 200 个系统调用增长到 313 个,增长了 50% 以上(见表 1)。6

两个新工具

我的第一篇关于代码探险的文章涵盖了几个工具,包括 global3、Cscope1、gprof4、ktrace7 和 truss11。我每天都在继续使用这些工具,但在过去的五年中,有两个新工具引起了我的注意:Doxygen2 和 DTrace9。虽然它们可能不是专门为代码探险而设计的,但两者都为该领域做出了重大贡献。在这里,我将讨论每个工具以及它们如何帮助我们理解大型代码库。

Doxygen。在 Doxygen 网页2的顶部是以下声明:“Doxygen 是一个用于 C++、C、Java、Objective-C、Python、IDL(Corba 和 Microsoft 风格)、Fortran、VHDL、PHP、C#,以及在某种程度上也支持 D 的文档系统。” 正如简介所说,Doxygen 的设计初衷是为了文档化源代码——它是一个非常好的文档化源代码的系统,其输出可用作手册页和手册——但它也有一些使其适用于代码探险的功能。

Doxygen 的作用是读取整个或部分源代码树,查找它可以提取并转换为格式精美的输出的文档标记,这些输出适用于文档化程序。它可以生成 Unix 手册页、LaTeX、HTML、RTF、PostScript 和 PDF。

对于代码探险者来说,最有趣的是 Doxygen 通过对有问题的代码运行预处理器来从任何源代码中提取信息的能力。Doxygen 是一种静态分析工具,因为它分析程序的源代码,但不查看程序运行时的程序状态。静态分析工具的优点在于它可以随时运行,并且不需要软件正在执行。在分析像操作系统这样的东西时,这非常有帮助。使 Doxygen 与我们的工作最相关的功能是那些与如何从源代码中提取数据相关的功能。当您一开始就打算使用 Doxygen 文档化自己的代码时,您已经在与系统一起工作,并且几乎不需要做额外的操作。如果您正在代码探险一个未知的代码库,那么您将需要更加积极地手动打开 Doxyfile 中的某些功能,Doxyfile 是 Doxygen 的配置文件。这些功能在此处列出

功能 含义
EXTRACT_ALL 从源代码中提取所有可以提取的内容
SOURCE_BROWSER 创建源代码的完整交叉引用
CLASS_DIAGRAMS 创建类图和继承图
HAVE_DOT 创建有用的代码探险图
CALL_GRAPH 制作一个跟踪所有函数调用的调用图
CALLER_GRAPH 输出调用者依赖关系图

HAVE_DOT 选项是最重要的选项,因为它允许 Doxygen 为代码探险者生成最有用的输出,包括类图、协作图、调用图和调用者图。我们将简要了解其中两种类型的图:调用图和调用者图。我们在本文中分析的代码是 FreeBSD 操作系统的 TCP/IP 协议栈。BSD TCP/IP 协议栈过去12已被研究,并且继续被研究 TCP/IP 套件的研究人员研究。

对于我们的示例,我们查看单个函数 ip_output(),该函数在网络协议栈的各个部分被调用,以便将 IP 数据报发送到网络。ip_output() 函数对于协议栈非常重要,因为所有正常的包传输都通过它。如果在这个函数中发现错误,或者如果由于某种原因需要更改 API,那么追踪该函数的所有当前用户(调用者)将非常重要。在图 1 中,我们看到了 Doxygen 为 ip_output() 生成的调用者图。

在图 1 中,不少于 16 个独立的例程,在几乎同样多的模块中,依赖于 ip_output() 函数。为了进行修复或更新 API,所有这些例程都需要进行调查。

调用者图的相反是调用图。调用图对于之前提到的工具(例如 Cscope 和 global)的用户来说很熟悉,这些工具允许用户交互式地浏览函数的调用图,在浏览源代码时跳入和跳出底层函数。Doxygen 为我们提供了与调用图交互的不同方式。图 2 显示了 ip_output() 函数的调用图。

与调用者图一样,调用图提供了函数如何融入整个系统的良好可视化概览。这两个图都充当了地图,我们可以从中得出关于软件结构方式的线索。一个相对容易看到的线索是包输出代码中的另一个热点,即 tcp_output(),它被七个不同的例程调用。

Doxygen 可以显示的信息种类是有代价的。生成此处显示的图表,需要分析包含 125,000 行代码的 136 个文件,在一台双核 2.5 GHz MacBook Pro 笔记本电脑上花费了 45 分钟。大部分时间用于生成调用图和调用者图,这对于代码探险者来说是迄今为止最有用的信息。5

DTrace。在过去几年中,最受关注的系统工具之一是 DTrace,这是一个来自 Sun Microsystems 的项目,根据 CDDL(通用开发和分发许可证)发布,并已移植到 FreeBSD 和 Mac OS X 操作系统。无论 DTrace 的设计者在编写他们的工具时是否专门针对代码探险,它显然是适用的。

DTrace 有几个组件:一个命令行程序、一种语言和一组探针,这些探针提供有关整个系统中发生的各种事件的信息。该系统的设计使其可以针对用户没有源代码的应用程序运行。

DTrace 是程序跟踪程序系列的下一个逻辑步骤,之前的程序包括 ktrace 和 truss。DTrace 为代码探险带来的是更丰富的原语集,无论是在其探针集还是 D 语言方面,这都使代码探险者更容易回答他们的问题。像 ktrace 这样的程序只显示程序运行时执行的系统调用,这些系统调用是应用程序与操作系统的所有交互。在典型的操作系统上,这些数字在数百个左右,尽管它们可以提供复杂软件正在做什么的线索,但它们并不是全部。Ktrace 无法跟踪操作系统本身,而现在可以使用 DTrace 完成此操作。

当人们讨论 DTrace 时,他们经常指出可用探针的数量很大,在 Mac OS X 上超过 23,000 个。这在某种程度上具有误导性。并非所有探针都可以立即使用,实际上,拥有如此丰富的资源使得为特定工作选择最有用的探针变得困难。探针是应用程序、库或操作系统中的一段代码,可以对其进行检测以代表用户记录信息。探针根据它们记录的内容分为几个类别。每个探针都由其提供程序 (Provider)、模块 (Module)、函数 (Function) 和名称 (Name) 划定。提供程序以系统命名,例如 io、lockstat、proc、profile、syscall、vminfo 和 dtrace 本身。Mac OS X 中提供了几个不同的提供程序,尽管天真地打印它们全部会向您显示每个进程都存在几个提供程序。每个进程的探针显示进程内部核心数据以及锁的信息。Mac OS X 上可用的一些提供程序如下所示

提供程序 用途
dtrace 与 dtrace 本身相关的探针
fbt 函数的入口点和出口点
io I/O 探针
lockstat 与锁定相关的探针
plockstat pthread 锁相关的探针
proc 进程特定信息
profile 性能分析和性能数据
syscall 系统调用信息
vminfo 虚拟内存探针

探针不仅与提供程序相关联,还与模块(您要检测的模块)以及函数(也是观察对象)相关联。跟踪点的名称指定单个探针。所有这些类别都放在一起,形成 DTrace 脚本或命令行,形成一种地址,指定工程师试图观察的内容。规范形式是 provider:module:function:name,空部分表示通配符。尽管 Sun 的两本手册是关于 DTrace 的优秀参考资料9, 10,但一个简单的示例将演示如何将其用于代码探险。

当面对一个新的和未知的系统进行探险时,首先要弄清楚的是程序使用了哪些服务。诸如 ktrace 和 truss 之类的程序可以显示此类信息,但 DTrace 大大扩展了这种能力。我们现在将找出 ls 程序执行所需的服务,以及哪些服务使用频率最高。

1: pid$target:::entry
2: {
3: @[probefunc] = count();
4: }

这里的脚本是用 D 语言编写的,对于任何熟悉 C 语言的人来说都应该相对容易理解。该脚本包含一个函数,该函数计算 ls 程序发出的任何调用的入口。C 程序员可能会找到函数名称和参数列表,但我们在这里看到的是所谓的谓词。谓词是一种选择 DTrace 将记录数据的探针的方法。第 1 行的谓词选择关联进程的任何调用的入口。当使用 dtrace 执行 calls.d 脚本时,其 pid$ 变量将替换为在 -c 命令行参数后给出的程序的进程 ID

> sudo dtrace -s calls.d -c ls
dtrace: script ‘calls.d’ matched 5906 probes
[output of ls command removed for brevity]
dtrace: pid 7008 has exited
strcoll 148
strcoll_l 148
__error 326
free 353
strcmp 381
wcwidth 614
_none_mbrtowc 662
mbrtowc 662
putchar 705
pthread_getspecific 1424
>

DTrace 还允许通过将 -c 替换为 -p 并将程序名称替换为实时进程 ID 来跟踪实时进程。这里我们展示了在 DTrace 下执行 ls 的缩写输出。仅显示最后几行,即入口计数较高的那些行。从这张快照中我们可以看到,ls 在字符串函数 strcoll 和 strcmp 上做了很多工作,如果我们试图优化程序,我们可能会首先查看这些函数在哪里被调用。

凭借数千个预定义的探针点,以及为用户进程动态创建探针的能力,很明显,DTrace 是过去十年中开发的最强大的代码探险工具。

持续的挑战

在回顾此处提到的工具(以及未提到的工具)时,一些挑战仍然显而易见。第一个挑战是一些开发人员从基于工具的方法转向一体化方法。

可以通过查看任何类 Unix 系统上可用的程序来最好地理解基于工具的方法。混合和匹配使用多个程序来完成任务具有其他文档充分记录的几个明显优势。当处理大型代码库时,一体化方法(例如 IDE)的缺点变得更加清晰。像 FreeBSD 内核这样的系统已经是数百兆字节的文本。使用 Cscope 和 global 等工具处理该代码库,以便使其更易于导航,会生成另外 175 MB 的数据。虽然 175 MB 的数据与平均桌面或笔记本电脑的内存(通常配备 2 GB 到 4 GB 的 RAM)相比可能很小,但在处理时将所有状态存储在内存中会导致所使用的任何工具的性能降低。数据的管道处理(保持内存数据较小)提高了所涉及工具的响应速度。将 FreeBSD 内核加载到 Eclipse 中花费了相当长的时间,然后占用了数百兆字节的 RAM。我在其他大型代码库上的其他 IDE 中也看到了类似的结果。

对于那些不仅处理大型代码库,而且还处理异构代码库的人来说,更大的挑战迫在眉睫。如今,大多数网站都是 PHP 或 Python 与 C 或 C++ 扩展的混合体,使用 MySQL 或 PostgreSQL 作为数据库后端,所有这些都基于用 C 编写的操作系统之上。通常,追踪特别棘手的问题需要多次跨越语言障碍——从 PHP 进入 C++,然后进入 SQL,然后可能回到 C 或 C++。到目前为止,我还没有看到任何工具能够理解如何分析这些跨语言交互。

最值得关注的领域是可视化。在所有审查的工具中,只有 Doxygen 生成了有趣且可用的可视化输出。其他工具的焦点非常狭窄,基于代码,用户通常只查看被调查系统的一小部分。

以这种方式工作有点像试图通过盯着纽约市的街道标志来了解美国。在没有精细细节的情况下查看底层系统的高级表示的能力可能是代码探险者最好的工具。能够将软件视为可以以不同方式导航的地图——例如,通过类关系和调用图——将使代码探险者的工作效率更高。

最后一个未涵盖的领域是网络。网络探险,即基于应用程序的网络流量来理解应用程序的能力,仍处于非常初级的状态,Wireshark 等工具是目前最先进的工具。许多应用程序已经在网上运行,能够在网络级别理解和使用它们非常重要。问

参考文献

  1. Cscope 手册页; http://cscope.sourceforge.net/cscope_man_page.html
  2. Doxygen 网站; http://www.stack.nl/~dimitri/doxygen/
  3. GNU global 源代码标签系统(2008 年 4 月 21 日)。Tama Communications Corporation; http://tamacom.com
  4. gprof; https://gnu.ac.cn/manual/gprof-2.9.1/gprof.html
  5. Graphviz 网站; https://graphviz.cpp.org.cn/
  6. Jones, T., Tauferner, A., Inglett T. 2007。HPC 系统调用使用趋势。Linux Clusters Institute; http://www.linuxclustersinstitute.org/conferences/archive/2007/PDF/jones_21421.pdf
  7. ktrace:开源操作系统上的标准工具。
  8. Neville-Neil, G.V. 2003。代码探险:探索洞穴般的代码库。《》1(6): 42-48。 http://doi.acm.org/10.1145/945131.945136
  9. Sun Microsystems。2005。《如何使用 DTrace》; http://www.sun.com/software/solaris/howtoguides/dtracehowto.jsp
  10. Sun Microsystems。2005。《Solaris 动态跟踪指南》; http://docs.sun.com/app/docs/doc/817-6223
  11. Truss 在 Solaris 上可用。
  12. Wright, G.R., Stevens, W.R. 1995。《TCP/IP Illustrated, Vol. 2: The Implementation》。波士顿,马萨诸塞州:Addison-Wesley Professional。

GEORGE V. NEVILLE-NEIL ([email protected]) 是《Communications of the 》和《》的专栏作家,以及《Queue》编辑委员会的成员。他从事网络和操作系统代码方面的工作,并教授各种与编程相关的学科的课程。

acmqueue

最初发表于 Queue vol. 6, no. 7
数字图书馆 中评论本文








© 保留所有权利。

© . All rights reserved.