内存泄漏 资源泄漏
在软件工程中, 设计模式是解决软件设计中常见问题的指南。 尽管用于软件工程和开发的设计模式已被广泛使用,但是关于模式的专门用于软件调试的信息并不多。 Wiki页面中提到了一些内容,但是正如您所看到的那样,这些模式只是提供了有关如何在受控环境中重现错误的高级概述,而不是找到根本原因并缩小范围。
即使您没有读过著名的《四人行》或这里 , 这里或这里的封面,您也可能会了解,相比之下,调试设计模式并没有很好地提供。
但是,实际上,故事大相径庭。 一些大型公司,尤其是企业软件公司,拥有独立的团队,专门负责调试已出厂产品的错误。 这些被称为持续产品开发(CPD)或升级/维持工程团队。 产品交付后,开发团队基本上将所有权移交给该团队,该团队负责解决客户看到的任何问题
这篇文章对CPD工程师很有帮助,他们可能并不总是能够访问出现错误的环境。 我本人曾在CPD团队中度过了一段时间,然后将阐明一种在挑战性条件下解决特别讨厌的bug(称为资源泄漏)的模式。
繁琐的调试艺术
无论在软件工程的设计和开发阶段进行的工作质量如何,错误都是不可避免的。
在测试阶段,质量保证(QA)工程师会发现并归档错误,开发人员必须在产品交付之前解决这些错误。 通常是花在调试上的时间才能阻止产品发布,因此开发人员面临着巨大的压力,需要尽快修复这些错误。
开发团队通常拥有质量保证设置的豪华之处,在此阶段可以再现错误,从而使此过程的痛苦减轻一些。 通过此设置,开发人员可以更改产品代码以添加诊断信息或其他更改,这有助于查找缺陷的根本原因。 包含仅用于诊断所发现缺陷的更改的补丁称为“调试补丁”。
允许调试补丁程序自由发布,例如引起副作用,这些副作用可能会改变产品的正常可接受行为。 在内部/内部环境中,经常会出现调试补丁并让QA工程师在发现问题的地方进行尝试。 这样的补丁只能在质量检查工程师打算使用的系统/机器上运行,它们可以更改产品的行为,即使开发人员所做的更改对质量检查设置产生不利的副作用,可以随时重新创建它。 不用说,没有客户(尤其是企业客户)会同意运行未经质量检查团队测试和认可并带有警告的版本。
不管解决错误的重要性和不可避免性,一些开发人员都不会像设计和开发阶段那样期望开发过程的这一阶段。 原因可能很多,调试阶段很可能为学习新知识提供了最小的余地,此外,这意味着QA团队到目前为止发现的错误都令人感到尴尬。
在我的拙见中,与软件开发的其他阶段相比,调试从未得到足够的学术关注,因为从未在学校明确地进行过调试,这可能是调试如此乏味的主要原因之一。
错误的负担不会随产品交付给客户而结束。 质量检查团队甚至可能漏掉一些错误,并发现自己已经为购买了该软件的客户使用的已发布产品上。 有许多原因可能导致质量检查小组错过了客户看到的错误,例如:
- 客户环境中存在一些环境特质,导致该问题仅由客户而不是内部人员看到。 例如,客户看到的某些网络流量模式或硬件功能/故障可能导致代码以某种方式运行。
- 干扰我们产品运行的其他第三方软件。
- 在规划阶段可能完全没有用例。
有时,原因1和2可能如此严重,以至于即使知道问题的性质,环境细节和客户用例,也可能永远无法在内部复制错误。 这些错误将再次归还给开发团队,而他们也不了解发生问题的环境。
跟踪内部无法复制但仅在客户环境中可见的错误非常具有挑战性。 与内部环境不同,开发人员无法在客户环境中安装可能产生有害副作用的调试补丁。 相反,他们必须依靠产品支持包中已经收集的日志和其他诊断信息来解决该错误。
客户发现的错误通常称为升级。 与以前相比,在此阶段解决错误更为关键(因为现在必须等待客户)。 如果没有提供及时的解决方案,它们可能会影响您产品的竞争对手,这将影响产品的未来,并最终影响公司本身。 而且,花在升级上的任何时间都离开发当前/将来的发行版还有时间。
应当竭尽全力防止错误升级,但是一旦我们面对错误 ,快速解决问题对于产品和公司的声誉至关重要。 这就是为什么负责升级的团队必须配备解决问题的工具和技术的精良装备。
可怕的资源泄漏
在各种错误中,很少有像资源泄漏这样的问题引起软件工程师的恐惧。 内存和套接字泄漏是最常见的情况,但是我描述的技术可以广泛用于任何类型的泄漏。 通常, 资源泄漏被定义为程序分配的资源超出其实际需要时的错误情况。
从代码的角度来看,资源是通过系统调用(malloc / open)分配/获取的,并通过另一个(释放/关闭)释放回系统。 一旦资源达到其用途后就不释放分配的资源,则会导致泄漏。
如果不及时发现和修复,除了冻结导致泄漏的进程外,资源泄漏还会使运行该进程的整个系统陷入停顿。 这些后果显然是可怕的。 例如,考虑一个进程正在泄漏套接字,慢慢消耗所有可用的套接字而不关闭它们。 在某个时候,运行该进程的系统将用光套接字,并且其他进程在需要时将无法获取任何套接字。 很快,没人会从Secure Shell等访问系统。
这些bug可能需要一段时间才能体现出来,因此很容易会拖延QA周期,即使它们具有强大的耐久性测试计划也是如此。 有一些工具可以有效地对抗它们,但是可能无法在所有环境中使用; 例如,没有多少客户愿意在他们的生产环境中安装像valgrind这样的工具(一种非常强大的内存泄漏检测工具)。
我花了大量时间调试此类问题,尤其是内存泄漏。 为了帮助其他人解决资源泄漏,我将描述一种与语言/平台无关的通用步骤模式。
调试资源泄漏的模式
在代码级别,不泄漏资源的进程释放了已分配/获取的每个资源。 因此,如果一个进程正在泄漏内存,那么它执行的获取函数调用肯定比释放函数调用多。 为了找出泄漏的原因并加以解决,我们必须找出哪个获取功能没有相应的发布功能,然后找出为什么未调用发布功能。
请按照列出的顺序执行以下步骤。 它们与语言/平台无关,并且应适用于各种资源。
每个步骤都会回答有关泄漏/错误的问题,这有助于回答后续问题,并最终帮助我们找出有问题的代码路径。
每个问题/步骤都有助于从软件堆栈的底部(实际的获取或发布功能)到堆栈的顶部(导致创建的获取或发布功能的实际用户操作或用例)驱动解决方案泄漏)。 当看到泄漏的环境/设置是客户环境,客户可能不同意安装可以为开发人员提供帮助的工具时,可以使用该工具。
1.问题出在哪里?
调试资源泄漏的第一步是识别存在问题的层。 此处的目的是找出错误是在流程本身中还是在平台中。
就代码而言,要问的问题是:是否调用了发布功能? 例如,如果确实在特定的内存块上调用过free(),但仍然存在,则意味着问题出在操作系统级别,而不是应用程序。
以下是一些方法来弄清我们是否缺少对关闭/释放的调用:
- 如果泄漏在公司内部可用的设置中可重现,则我们可以记录/计数每个分配和释放功能。 否则,我们必须依靠日志包并在获取和释放功能周围寻找日志溢出,以作为指示。 例如,考虑以下伪代码:
function foo() …. log.message(“ABCD”) ... variablea = acquire_function() ... function bar() …. log.message(“WXYZ”) ... release_function(variablea)
如果我们在日志中看到的是ABCD而不是WXYZ,则很可能是变量a被泄漏,因为未调用release_function。
- 使用可在各种环境中应用的泄漏检测工具。 例如:; Valgrind , WinDbg等。不幸的是,这仅适用于内部环境。 即使在这里,某些功能强大的工具也会大大减慢流程速度,并可能使检查过程无效。 没有客户会允许将这样的工具安装在生产设置中。
- 查找操作系统可能提供的指示。 例如,某些操作系统提供有关正在运行的进程的信息,包括套接字打开的数量及其状态,这些信息将告诉您是否调用了close。
- 为了计数/审核获取和发布功能,开发自己的工具/技术。 解决此问题的最佳方法可能是开发工具,使其成为产品的一部分,并可以选择使产品以调试模式运行,从而提供所需的信息。 某些平台允许使用LD_PRELOAD,_malloc_hook_等此类构造。这些平台使您可以包装分配,释放函数以及注入跟踪所需内容的代码。
- 如果所有其他方法均失败,则您甚至必须向客户发布调试补丁。 如果该修补程序仅具有额外的日志记录,那将是理想的选择,但仍应进行基本的质量检查验证,以确保该修补程序在客户环境中不会产生不利的副作用。
2.哪些资源正在泄漏?
下一步是找出导致泄漏的资源实例。 就代码而言,要问的问题是:内存泄漏对象是X还是Y? 如果您已使用A,B,D或E部分来回答问题1,则直接回答该问题。
例如,1A直接告诉您问题出在应用程序中而不是平台上,并且变量A正在泄漏,但是,如果您使用C部分来回答问题1,或者如果您已经以某种方式知道该版本中没有错误,平台/操作系统,此答案尚不清楚,因此您将必须使用其他步骤来找到答案。
3.在哪里使用资源?
既然我们知道对象X正在泄漏,则可以在代码中的许多工作流/用例中使用它。 例如,在1A的伪代码段中,可以在多个地方调用foo,因此我们需要弄清楚其中哪些调用将对象留在后面,以及产品的哪个用例导致了这一点。 在代码级别,我们需要知道导致获取此特定对象的代码的调用跟踪。
在这里,如果很少有使用泄漏对象的情况,或者呼叫跟踪不是很深,则您将立即知道使用该对象的位置。 否则,您需要了解资源泄漏实例在产品本身中扮演的角色。 有时,了解其作用的最佳方法是进行良好的旧代码检查。 对代码的理解将使您处于很好的位置来回答这个问题。
如果在可用时间内证明不可行,则建立一个调用树,关联日志和代码,然后从那里开始。 回到我们在1A中的示例,我们知道foo调用已获取,并且我们需要查看从过程的入口点开始直接在哪里调用所有foo(例如C / C ++ / Java中的主要函数)。
罪魁祸首是foo的调用树中的分支,而bar的分支中没有相应的分支。
4.为什么资源泄漏?
一旦我们知道了泄漏资源实例的上下文,最后一步就是看看为什么不释放它。 在这种情况下,要么根本没有调用本应释放泄漏资源的代码路径,要么是在不调用本应释放资源的函数的情况下终止了代码路径。
例如,在1A中,我们需要找出为什么不调用调用bar的代码路径,或者如果我们知道该bar被调用,为什么不调用release_function(variable A)。 这是通过关联日志和代码中看到的内容来完成的。
确定泄漏后,修复程序将取决于其性质和原因,并且不在此模式的范围内。 不过,我会说,通过遵循此过程/模式,您将学到很多不错的课程。 例如,您至少将了解日志消息的位置和性质,这将帮助您更快地调试问题。 这些经验教训非常宝贵,必须加以利用,以实现有助于改善产品可诊断性的功能。 这样做将帮助必须解决这些问题的任何人。
最后忠告
如果上述步骤说起来容易做起来难,我不会感到惊讶,因为它们确实是。 就像我说的那样,调试是一项繁重的工作,它需要耐心,毅力和艰苦的工作。
有人说“最好的一拳是你不必扔的拳”(或类似的东西)。 同样,解决错误的最佳方法是首先避免它,或者在发布之前将其捕获在质量检查设置中。
投资单元,集成和预检测试是早期发现错误的好方法。 光靠强大的耐用性测试就无法帮助捕获内部的各种泄漏。 此外,您应该考虑编写一些方法来监视产品中各个流程的资源使用情况的工具,以通过例如定期收集资源利用率来指示泄漏。 如果它单调增长,我们就知道存在问题。
但是,就像我说的那样,升级是不可避免的,因此开发人员必须在设计产品本身时设计功能,以实现产品的可诊断性和可维护性。 至少,如果开发人员知道他们可能面临必须在他们可能无法访问的环境中寻求错误的情况,则必须在战略上放置有意义的日志消息。
在高层,与客户交付调试补丁的一些过程/安排也可能派上用场。 为了捕获客户环境中的泄漏,开发自己的工具并使它们成为产品的一部分是理想的。
当所有其他方法都失败了,并且最终他们会失败时 ,我希望这里描述的模式派上用场。 资源泄漏不容易解决,如果在内部看不到,尤其令人讨厌。
我不会说您必须遵循我的模式,但是像任何模式一样,这是一种建议,您可以根据自己的情况和环境采用它。 不过,我将告诉您,如果您想采用自底向上的方法调试资源泄漏,那么描述的模式中的问题顺序和问题本身最不可能改变。
我已经成功地使用了不止一次描述的步骤顺序,希望您也能这样做。
翻译自: https://www.javacodegeeks.com/2018/01/debugging-patterns-resource-leaks.html
内存泄漏 资源泄漏