wenmo8 发布的文章

无论是否有异常处理,用任何语言编写良好的错误处理代码都是困难的。当我考虑在一个给定的程序中需要实现什么样的异常处理时,我首先将可能捕获的每个异常分类到四个bucket中的一个,我将其标记为致命的、硬骨头般突出的、烦人的、外部的。

致命的异常不是你的错,你不能阻止它们,你也不能理智地清除它们。它们几乎总是发生,因为这一进程病入膏肓,即将摆脱痛苦。内存不足、线程中止等。捕捉这些是毫无意义的,因为你的用户代码所能做的一切都不能解决问题。就让你的“finally”块运行,并希望最好的。(或者,如果你真的很担心,快速失败和不让;在这一点上“finally”块运行,它们可能只会让事情变得更糟。但这是另一话题。)

硬骨头般突出的异常是您自己的该死的错误,您可以阻止它们,因此它们是代码中的错误。你不应该捕获它们;这样做是在你的代码中隐藏了一个错误。相反,您应该改写您的代码,这样就不可能在第一时间发生异常,因此不需要捕获异常。这个参数是空的,类型转换是坏的,索引超出范围,你试图除以零-这些都是你本来可以很容易地避免的问题,所以首先要防止混乱,而不是试图捕获它。

令人烦恼的异常是不幸的设计决策的结果。恼人的异常是在完全非异常的情况下抛出的,因此必须一直捕获和处理。典型的异常例子是Int32.Parse,如果给它一个不能被解析为整数的字符串,它就会抛出。但是这个方法99%的用例是转换用户输入的字符串,这可能是任何旧的东西,因此解析失败也不例外。更糟糕的是,如果不实现整个方法本身,调用者就无法提前确定其参数是否糟糕,在这种情况下,他们不需要首先调用它。这个不幸的设计决策非常令人恼火,当然,框架团队随后不久就实现了TryParse,这是正确的做法。你必须抓住令人恼火的异常,但这样做是令人恼火的。试着永远不要自己写一个抛出令人烦恼的例外的库。

最后,外部异常看起来有点像恼人的异常,只是它们不是不幸的设计选择的结果。相反,它们是凌乱的外部现实影响到你美丽、清晰的程序逻辑的结果。考虑这个伪C#代码,例如:

try
{
using ( File f = OpenFile(filename, ForReading) )
{
// Blah blah blah
}
}
catch (FileNotFoundException)
{
// Handle filename not found
}

使用WinDgb调试的时候,我们需要和各种结构体等符号打交道。包括系统的符号等等。有时候符号太多了,我们根本记不住或者只有模糊的印象,比如只记得其中的2个字母,怎么办?或者知道符号名,但不知道在哪个模块,特别是使用stl库的时候。这时候dt搜索就可以帮上忙了。
使用如下通配符命令即可列出所有的符号

dt  *!*XXX*  xxx为我们知道的仅有符号名字符

例如:

0:006> dt  *!*filesystem_error*
          xxx!filesystem_error
          xxx!std::tr2::sys::basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >
          xxx!std::tr2::sys::basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >
          MSVCP120!filesystem_error
          MSVCP120!std::tr2::sys::basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >
          MSVCP120!std::tr2::sys::basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >
          YYY!filesystem_error
          YYY!std::tr2::sys::basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >
          YYY!std::tr2::sys::basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >
105139e0  xxx!std::tr2::sys::basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >::basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >
10513950  xxx!std::tr2::sys::basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >::basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >
10514130  xxx!std::tr2::sys::basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >::~basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >
10514d10  xxx!std::tr2::sys::basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >::`scalar deleting destructor'
0f9c4140  YYY!std::tr2::sys::basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >::basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >
0f9c05e0  YYY!std::tr2::sys::basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >::basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >
0f9c4190  YYY!std::tr2::sys::basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >::~basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >
0f9c0690  YYY!std::tr2::sys::basic_filesystem_error<std::tr2::sys::basic_path<std::basic_string<char,std::char_traits<char>,std::allocator<char> >,std::tr2::sys::path_traits> >::`scalar deleting destructor'

 

今天在调试分析一个dmp文件,要分析clr的栈,于是,输入命令".loadby sos clrjit",结果出现如下错误提示:

0:000> .loadby sos clrjit
The call to LoadLibrary(C:\Windows\Microsoft.NET\Framework\v4.0.30319\sos) failed, Win32 error 0n193
    "%1 不是有效的 Win32 应用程序。"
Please check your debugger configuration and/or network access.
很是吃惊。从来没有遇到过,仔细看提示,是加载sos扩展库失败,原因是"不是有效的win32应用程序",难道是sos.ll被破坏了,还是下载不全,最后都排除了。

实在是没办法了,感到很沮丧。最后无意看到Windbg标题栏显示的是64位版本的

而我要用的是32位版本,于是,我关掉64位,开启32位windbg,加载dmp,分析,输入输入命令".loadby sos clrjit",没有错误提示了,相应的扩展命令也能正常使用。

看来就是因为使用64位版本windbg导致的。以后要注意了

 

错误模型试图回答的基本问题是:如何将“错误”传达给程序员和系统用户?
在回答这个问题时,最大的挑战之一就是定义错误的实际含义。大多数语言将Bug和可恢复的错误归为同一类,并使用相同的工具来处理它们。例如空引用或越界数组访问的处理方式与网络连接问题或解析错误相同。乍一看,这种一致性似乎不错,但它有根深蒂固的问题。尤其是,它具有误导性,并且经常导致不可靠的代码。
我们的总体解决方案是提供一个双管齐下的错误模型。一方面,由于编程错误,产生了fail-fast 异常,我们称之为快速终止。另一方面,您已经静态地检查了可恢复错误的异常。两者在编程模型和背后的机制上都非常不同。快速终止在一瞬间毫无歉意地破坏了整个过程,拒绝运行任何用户代码。)当然,异常有助于恢复,但是有深层类型的系统支持来帮助检查和验证。

这趟旅程漫长而曲折。为了讲述这个故事,我把这篇文章分成六个主要方面:

  • 基础知识
  • Bug是不可恢复的错误!
  • 可靠性、容错性和隔离性
  • Bug:终止、断言和契约
  • 可恢复错误:类型定向异常
  • 回顾与结论

事后看来,某些结果似乎显而易见。尤其是考虑到现代系统语言如Go和Rust。但有些结果让我们吃惊。我会尽我所能直截了当地去追索,但我会一路上给你充分的回馈。我们尝试了很多没用的东西,我想这比尘埃落定时的结局更有趣。

Bug是不可恢复的错误

我们早期所做的一个重要区别是可恢复错误和错误之间的区别:

  • 可恢复的错误通常是编程数据验证的结果。一些法典审查了世界状况,认为这种情况不可接受,无法取得进展。可能是一些正在解析的标记文本、来自网站的用户输入,或者是暂时的网络连接失败。在这些情况下,程序有望恢复。编写这段代码的开发人员必须考虑在失败时应该做什么,因为无论您做什么,它都会发生在构造良好的程序中。响应可能是将情况告知最终用户、重试或完全放弃操作,但是这是一种可预测的、经常是计划好的情况,尽管被称为“错误”
  • Bug是一种程序员没想到的错误。输入没有正确验证,逻辑写错了,或者出现了许多问题。这样的问题通常都不会被及时发现;需要一段时间才能间接观察到“二次效应”,这时可能会对程序的状态造成重大损害。因为开发商没想到会发生这种事,所以所有的赌注都没有了。此代码可以访问的所有数据结构现在都是可疑的。因为这些问题不一定能及时发现,事实上,更多的问题值得怀疑。依赖于语言的隔离保证,也许整个过程都被污染了。

这种区别是最重要的。令人惊讶的是,大多数系统并不能产生一个,至少在原则上不是这样的!如上所述,Java、C#和动态语言只对所有内容使用异常;C和Go使用返回代码。C++使用的是一个取决于观众的混合,但通常的故事是一个项目选择一个单独的并且到处使用它。但是,您通常听不到语言建议使用两种不同的错误处理技术。
考虑到bug本质上是不可恢复的,我们没有尝试。在运行时检测到的所有错误都会导致所谓的终止,也就是所谓的“快速失败”。
上述每个系统都提供了类似于放弃的机制。C#有环境.FailFast,C++有std::terminate;等等。每一个都突然而迅速地撕开周围的背景。这个上下文的范围取决于系统——例如,C和C++终止进程。

尽管我们确实以一种比普通人更规范、更普遍的方式使用放弃,但我们肯定不是第一个认识到这种模式的人。哈斯克尔的这篇文章很好地阐明了这一区别:
我参与了一个用C++编写的图书馆的开发。一位开发人员告诉我,开发人员分为喜欢异常的和喜欢返回代码的两类。在我看来,返回码的朋友赢了。然而,我得到的印象是,他们争论了错误的一点:异常和返回代码同样具有表现力,但是它们不应该用来描述错误。实际上,返回代码包含数组索引超出范围等定义。但是我想知道:当我的函数从一个子例程获取这个返回代码时,它应该如何反应?它要给它的程序员发一封信吗?它可以依次将此代码返回给调用方,但也不知道如何处理它。更糟糕的是,由于我无法对函数的实现进行假设,我不得不期望每个子例程都有一个超出范围的数组索引。我的结论是数组索引超出范围是一个(编程)错误。它不能在运行时处理或修复,只能由开发人员修复。因此不应该有相应的返回代码,而是应该有断言。

放弃细粒度的可变共享内存作用域是可疑的,比如Goroutines或threads之类的,除非您的系统以某种方式保证所造成的潜在损害的作用域。不过,这些机制是伟大的,我们有使用!这意味着在这些语言中使用废弃规则确实是可能的。

然而,这种方法要在规模上取得成功,有一些架构元素是必要的。我敢肯定你在想“如果我每次在我的C#程序中出现空引用时都抛出整个过程,我会有一些非常恼火的客户”;同样地,“那根本就不可靠!”!“事实证明,可靠性可能不是你想的那样。

可靠性、容错性和隔离性

在我们进一步讨论之前,我们需要陈述一个中心信念:史无前例。

建立一个可靠的系统

普遍的看法是,你通过系统地保证失败永远不会发生来建立一个可靠的系统。直觉上,这很有道理。有一个问题:在极限,这是不可能的。如果你能像许多任务关键型实时系统一样,仅在这处房产上就花费数百万美元,那么你就可以取得重大进展。或许可以使用SPARK这样的语言(Ada的一组基于契约的扩展)来正式证明所写每一行的正确性。然而,经验表明,即使是这种方法也不是万无一失的。
我们没有反抗生活的现实,而是拥抱它。显然,你会尽可能地消除失败。错误模型必须使它们透明且易于处理。但更重要的是,你设计你的系统,这样即使个别部分出现故障,整个系统仍能正常工作,然后教你的系统优雅地恢复那些故障部分。这在分布式系统中是众所周知的。那为什么是小说呢?
最重要的是,操作系统只是一个由协作进程组成的分布式网络,就像一个由微服务或互联网本身组成的分布式集群。主要的区别包括延迟、可以建立的信任级别和容易程度,以及关于位置、标识等的各种假设,但是在高度异步、分布式和I/O密集型系统中,失败是必然发生的。我的印象是,在很大程度上,由于单片内核的持续成功,整个世界还没有实现“操作系统作为分布式系统”的飞跃。然而,一旦你这样做了,很多设计原则就会变得显而易见。
与大多数分布式系统一样,我们的体系结构假定过程失败是不可避免的。我们花了大量的时间来防止层叠故障,定期记录日志,并实现程序和服务的可重启性。
当你假设这一点的时候,你会以不同的方式构建事物。
特别是,隔离是至关重要的。系统的流程模型鼓励轻量级细粒度隔离。因此,程序和现代操作系统中通常的“线程”是独立的独立实体。防止一个这样的连接失败比在地址空间中共享可变状态要容易得多。

孤立也鼓励简单。巴特勒·兰普森(Butler Lampson)的《计算机系统设计经典提示》(Hinks on Computer System Design)探讨了这个话题。我一直很喜欢霍尔的这句话: