尊敬的 KV:
我最近对学习操作系统和系统软件的工作原理产生了不健康的兴趣,因为我结束了一次应用程序调试会话,该会话似乎指向的错误不在应用程序中,而是在它调用的代码中,该代码位于操作系统中。幸运的是,我正在使用的操作系统是开源的,所以我希望能够继续调试我的问题,因为多年前当我还是本科生时,有人告诉我操作系统只是另一个程序,尽管它具有特殊的功能。当我尝试调试问题时,我发现,与我在应用程序开发中习惯使用的工具不同,用于调试操作系统的工具充其量是原始的。与我的 IDE 及其工具相比,我手头用于继续调试的工具更像是石刀和熊皮,而不是现代软件。由于我从您的简介中得知您从事操作系统工作,所以我想写信问一下,“这就是全部吗?” 或者,也许编写操作系统的人在软件方面非常出色,以至于他们不觉得他们缺乏用于工作的优秀工具。我感觉自己像是一个没有鞋子的鞋匠的孩子。
尊敬的 Cobb:
一位风险投资家曾经告诉我:“工具不赚钱。” поскольку这个人很擅长投资公司,我愿意相信他的话。
如果你看看软件工具领域,你会发现大多数开发者要么使用开源工具(LLVM 和 gcc 用于编译器,gdb 用于调试器,vi/vim 或 Emacs 用于编辑器);要么使用最近改革的专有软件之家微软的工具,微软已经意识到其 Visual Studio Code 系统是将人们引入其平台的有效方法;或者最后是苹果,其工具仅适用于其平台。在专业市场,例如深度嵌入式、军事和航空航天领域,存在专有工具,这些工具通常比其开源同类产品更糟糕,因为此类工具的市场很小但利润丰厚。
让我们首先驳斥你在信中提出的一个神话,即那些编写操作系统的人在某种程度上比那些编写应用程序或任何其他类型软件的人更优秀的开发者。在困难的环境中(例如直接在硬件之上)编写代码肯定可以提高你的编码技能。它肯定会让你更加小心,因为代码中的失败可能会产生可怕的副作用,例如导致整个计算机崩溃。以这种方式学习小心谨慎并不会让你比任何其他试图理解和扩展大型软件语料库的尝试更像软件天才。
操作系统编程的困难主要来自两个方面:(1)硬件不允许某些类型的错觉;(2)缺乏优秀的工具,正如你所指出的那样。
许多应用程序编程可用的便利性都归功于操作系统在硬件之上创建的软件错觉。考虑一下当你的应用程序程序遇到故障时会发生什么:它崩溃了,但它不会使系统上的任何其他东西崩溃,并且它通常会以核心转储的形式留下出错记录。程序无法使系统上的其他程序崩溃,这归功于虚拟内存系统提供的错觉,即每个程序都拥有所有内存,并且不会影响其他程序拥有的内存。操作系统可以以这种方式运行,而且,事实上,在研究中足够常见的微内核操作系统设计可以利用此功能使操作系统中更多的代码可重启。但是,此功能会带来操作系统设计者一直不愿意付出的开销成本,因此操作系统仍然是“一个大型程序”,当出现错误时,就会崩溃。
硬件限制不是改进操作系统工具的主要障碍,因为毕竟,应用程序编写者仅使用软件就获得了大量的便利。事实上,操作系统的主要目的是成为一个软件库,以帮助编写应用程序,因为除了那些从事操作系统工作的人之外,没有人真正关心操作系统。
可以构建系统,使其更适合优秀的工具,并且可以构建更好的工具,如果我们愿意停下来思考一下这可能意味着什么。在系统软件中,压力始终在于“让机器工作起来!” 这意味着破解硬件驱动程序和其他位,使盒子工作——甚至不是工作得很好——只是工作起来。人们很高兴操作系统能够工作并且应用程序不会崩溃,以至于他们从不回头考虑他们正在使用的系统的设计是否适合应用程序或硬件。使系统工作并不意味着设计是正确的设计,只是你实际上使机器运行起来而没有魔法烟雾逸出。
在我更具哲学意味的日子里(在漫长的调试会话后喝更多酒的日子里),我将操作系统软件视为像厄休拉·勒古恩短篇小说《离开奥梅拉斯的人》中的孩子。故事中的一个孩子被锁在地下室里,几乎没有食物,并且遭受巨大的痛苦,但是这个孩子的存在确保了镇上其他人的幸福生活。读者被告知,如果这个孩子被放出来并得到妥善对待,镇上的每个人都会受苦。这个孩子的存在是为了让其他人可以过上幸福的生活,就像操作系统的存在是为了让应用程序可以拥有幸福的生活/运行时。
如果我们退后一步,思考如何使系统软件更好,我们可能会在设计此类系统时提出一些原则。它们可能类似于操作系统中的所有大型软件都应该被设计为(1)可扩展的,(2)可测量的,和(3)可调试的,这些原则将与工具如何与整个系统交互有关。
当系统围绕一组众所周知且文档完善的模式构建时,扩展系统是最容易实现的;我需要一个看起来像 X 的东西,所以我将创建一个 Y,它看起来很像 X,但进行了一些更改。任何此类模式唯一存在的地方通常是在操作系统的设备驱动程序中,即使在那里,硬件通常也会决定形式,并且驱动程序必须扭曲自身以正确的形式和格式为系统的其余部分提供数据。
计算行业花费了无数的资金试图解决应用程序的这个问题,从最初引入软件库到数十年来使用面向对象的语言和工具。在过去的 50 年中,没有一项此类工作进入主要的操作系统。用于构建操作系统的代码只有最原始的数据类型(列表、哈希、偶尔的树),而应用程序中使用的库是现代数据类型的丰富宝库。最初反对操作系统中使用复杂数据类型的论点是大小,但是在 16GB 内存是手表或手机的起点,更不用说现代服务器的世界中,这个论点站不住脚。
扩展复杂且系统固有的东西(例如内存的处理方式或调度程序)的想法几乎是令人厌恶的,因为接口文档不完善且脆弱。
因此,设计系统软件时要遵循的首要原则是可扩展性。组成操作系统本身的每个子系统都必须默认设计为可替换,并具有清晰的文档化 API、单元测试以及应用程序软件要求的其他所有属性。
第二个原则,软件的测量,在过去的 20 年中通过 DTrace 及其子系统 bpftrace 等系统得到了改进,现在可用于应用程序和系统代码,但 DTrace 并非为测量而设计。当前的测量工具是在它们旨在测量的软件之后很久才创建的,它们扭曲自身以解开底层系统,并提供了一种有用的(如果原始的)方法来查看系统正在做什么。为测量而设计的系统将已经内置了跟踪点,这些跟踪点会调用系统状态中的重要转换,以便工具——或者更糟糕的是,你卑微的程序员——不必四处寻找以弄清楚系统中发生了什么。大多数软件,不仅仅是操作系统,都是在没有测量概念的情况下创建的,测量概念只是在人们说“系统运行缓慢”时才引入,这是一个常见且令人恼火的错误报告消息。
最后但并非最不重要的是,我们来到了你可能需要的工具,也是你写信的初衷:调试器。为了使应用程序能够被调试,需要做很多工作,其中最重要的是操作系统通过特殊的系统调用对调试的支持。操作系统设计者知道,无法调试应用程序是不可接受的,但出于某种原因,他们仍然经常认为使用打印语句调试操作系统本身是完全可以接受的(printk()
或 printf();
任你选择)。
在新硬件上启动系统时,也许“这就是你所拥有的一切”,但是一个正确设计的系统应该从调试钩子开始,而不是从像可变参数调用到复杂控制台输出系统这样的复杂东西开始。事实上,要使用调试器做一些聪明的事情,只需要一个小型监控程序,该程序公开直接内存读取和写入(gdb 通过 gdb stubs 支持此功能)。然而,通常,当有人在新硬件上启动系统软件时,竞赛是为了使 printf()
工作,因为这很熟悉,而且人类似乎喜欢熟悉的东西,即使它会导致更糟糕的结果。
如果系统在设计时考虑了这些问题(我如何扩展它?我如何测量它?我如何调试它?),那么构建更好的工具也会更容易。工具将有一些可以依靠的东西,而不是猜测内存中某些随机字节可能意味着什么。那是一个缓冲区吗?它是一个重要的缓冲区吗?谁知道呢,一切都是内存!
我希望有一天能拥有像应用程序软件那样优秀的系统软件工具,但首先我们都必须离开奥梅拉斯。
KV
George V. Neville-Neil 为乐趣和利润而从事网络和操作系统代码工作。他还教授有关编程相关主题的课程。他的兴趣领域是计算机安全、操作系统、网络、时间协议以及大型代码库的维护和管理。他是The Kollected Kode Vicious的作者,并与 Marshall Kirk McKusick 和 Robert N. M. Watson 合着了The Design and Implementation of the FreeBSD Operating System。近 20 年来,他一直是更广为人知的 Kode Vicious 专栏作家。自 2014 年以来,他一直是剑桥大学的工业访问学者,在那里他参与了多个与计算机安全相关的项目。他获得了马萨诸塞州波士顿东北大学的计算机科学学士学位,并且是 、Usenix 协会和 IEEE 的成员。他的软件不仅在地球上运行,而且还作为 VxWorks 的一部分部署在 NASA 的火星任务中。他是一位狂热的自行车爱好者和旅行家,目前居住在纽约市。
版权所有 © 2023 归所有者/作者所有。出版权已授权给 。
最初发表于 Queue 第 21 卷,第 3 期—
在 数字图书馆 中评论本文
Charisma Chan, Beth Cooper - 调试 Google 分布式系统中的事件
本文介绍了 2019 年对 Google 工程师如何调试生产问题进行的研究结果,包括工程师以不同组合有效调试所使用的工具类型、高级策略和低级任务。它考察了用于捕获数据的研究方法,总结了生产调查的常见工程流程,并分享了专家如何调试复杂分布式系统的示例。最后,本文将这项研究的 Google 特性扩展到提供一些您可以在组织中应用的实用策略。
Devon H. O'Dell - 调试心态
软件开发人员花费 35-50% 的时间来验证和调试软件。调试、测试和验证的成本估计占软件开发项目总预算的 50-75%,每年超过 1000 亿美元。虽然工具、语言和环境减少了花在单个调试任务上的时间,但它们并没有显着减少花在调试上的总时间,也没有减少调试的成本。因此,过度关注开发期间消除错误是适得其反的;程序员应该将调试视为一种解决问题的练习。
Peter Phillips - 使用跟踪增强调试
创建用于运行旧程序的模拟器是一项艰巨的任务。您需要彻底了解目标硬件以及模拟器要执行的原始程序的正确功能。除了功能正确之外,模拟器还必须达到以原始实时速度运行程序的性能目标。实现这些目标不可避免地需要大量的调试。错误通常是模拟器本身中的细微错误,但也可能是对目标硬件的误解或原始程序中的实际已知错误。(原始程序的二进制数据也可能已变得细微损坏或不是预期的版本。)
Queue Readers - 又一天,又一个 Bug
作为本期关于程序员工具的一部分,我们在 Queue 决定就调试主题进行一项非正式的网络投票。我们请您告诉我们您使用的工具以及您如何使用它们。我们还收集了关于那些难以追踪的 bug 的故事,这些 bug 有时会让我们考虑从事其他职业。