关注了就能看到更多这么棒的文章哦~

A QEMU case study in grappling with software complexity

October 12, 2021
This article was contributed by Kashyap Chamarthy
KVM Forum
DeepL assisted translation
https://lwn.net/Articles/872321/

要制作出具有长期可靠性以及可维护性的软件,有很多障碍。其中之一就是软件的复杂性。在最近结束的 2021 年 KVM 论坛上,Paolo Bonzini 以 open source 的 emulator 和 virtualizer (仿真器和虚拟化器) QEMU 为例,探讨了这个话题。根据他作为 QEMU 中几个子系统的维护者的经验,他针对如何抵御不该有的复杂性提出了一些具体建议。Bonzini 在整个演讲中使用 QEMU 作为实例,希望让未来的贡献者更容易对 QEMU 进行修改。然而,他所分享的经验也同样适用于其他许多项目。

为什么软件的复杂性会成为一个问题?首先,大家都知道它会导致各种错误,也会引入安全缺陷(security flaws)。对于复杂的软件来说,对代码进行 review 会更加困难;它也使得希望对项目进行贡献和维护的时候感到更加麻烦。显然,这些都是缺点。

Bonzini 希望能回答一个问题,那就是 "我们能在多大程度上消除复杂性?"。为此,他首先把复杂性分为了 "基本的(essential) "和 "偶然的(accidental)" 复杂性两种。这两类复杂性的概念源于 1987 年 Fred Brooks 的经典论文《No Silver Bullet》。Brooks 本人则是在借鉴了亚里士多德的 essence and accident 的定义。

正如 Bonzini 所说,essential complexity 是 "软件程序试图解决的问题本身固有的属性"。而 accidental complexity 则是 "正在解决手头问题的过程相关的一个特性"(即这不是由我们想要解决的问题本身所具有的困难)。为了进一步解释这些概念,他挑选了一些在 QEMU 中正在解决的问题,用来说明 QEMU 的 essential complexity。

Essence and accidents of QEMU

QEMU 在可移植性、可配置性、性能和安全性方面有许多要求。除了要模拟(emulate)guest device,以及提供保存和恢复 guest 状态的方法之外,它还有一个功能强大的 storage layer,并且还带有一些网络服务功能,如 VNC server。QEMU 还必须确保暴露给客户的 CPU 和 device 的模型保持稳定,哪怕曾经更新过所运行的硬件环境或 QEMU 本身。许多用户来说都希望在 QEMU 上直接使用发行版的内核,而不希望专门修改一个内核来使用。对于许多 QEMU 用户来说,能够启动非 Linux 的操作系统也是一个必备功能,这个也算是 essential complexity。

QEMU 提供了一个管理界面,通常称为 monitor。实际上是有两个,分别是 HMP(人类监控协议,human monitor protocol)和 QMP(QEMU monitor protocol),因为用户也需要一个简单的方法来与 monitor 互动,而不希望使用 QMP 提供出来的基于 JSON 的接口,外部程序则要 QMP 的接口来来管理 QEMU。因此,QEMU 包含了一个对象模型(object model)和一个代码生成器(code generator),它可以处理 C structure 的双向转换(marshaling and unmarshaling)。得益于这个代码生成器,可以简单地使用同样的代码来相应 JSON 或命令行参数的操作方式。

开发人员还看到了另一方面的复杂性,这是由于 build 过程中的相关工具所引入的。工具一般会使常见的任务变得更容易,但它们也使得调试工作在发生故障时变得更加困难。例如,QEMU 曾经有一个手动配置机制,需要用户逐一列举出它所要模拟的电路板上的所有设备。现在,只需要指定某个电路板,build 系统就会自动把它所支持的设备都启用起来。它还能确保不会去 build 那些不合理的配置,这个机制也非常有用。但是,开发者仍然必须要学会如何处理这类出错情况。

Sources of complexity

在演讲中,Bonzini 介绍了 accidental complexity 的两个主要来源。第一个是 "incomplete transitions"(受到一篇关于 GCC 维护的论文的启发),也就是当一种新的、更好的方法被引入的时候,这个新方法尚未在整个代码库中统一应用起来。这可能是由于多种原因造成的:开发人员可能没有时间或缺乏相关的专业知识,或者他们根本没有找到那些被遗漏的情况。

他列举了在 QEMU 中报错的两种截然不同的方式作为例子:一种是 propagation-based API,另一种是将 error 直接写入标准输出的具体函数(例如 error_report())。propagation-based API 是为了向 QMP 接口报告错误而引入的。它有两个优点:它将错误发生的位置与报告的位置分离开,并可以比较干净地进行 error recovery。另一个 incomplete transitions 的例子是,尽管现在 QEMU 的 build 系统主要使用 Meson,但仍有一些之前就存在的 build test 是用 Bourne shell 编写的,仍然是 QEMU 的配置脚本的一部分。

然而,QEMU 历史上也有一些彻底完成切换(transition)的例子。其中有几次是使用 Coccinelle 来完成的(这是一个模式匹配和源码转换工具,允许创建一个 "semantic patch,语义补丁",用来在整个代码库中修改所有相关点。例如,曾经使用 Coccinelle 来替换那些过时了的 API、 简化一些不必要的中间调用、甚至引入全新的 API(如 device 的创建和 "realization")。

accidental complexity 的第二个来源是重复逻辑以及缺乏抽象。在编写临时代码和设计可重复使用的数据结构和 API 之间,有一个平衡点需要权衡。Bonzini 举了命令行解析的例子,有些临时代码在使用 strtol() 或 scanf() 等函数,相应地正规代码使用的是 QEMU 专用 API(如 QemuOpts 或 keyval)。后者确保了命令行的一致性,有时还能协助打印一些 help 信息。

另一个例子是最近发生的,人们在试图将 QEMU 的更多部分组织成可以独立安装的 shared object。随着这类 module 的数量增加,我们就建立了一个新机制,可以将一个 module 所提供的功能和它的依赖关系列在实现相关功能的源文件代码中,而不是让它们散落在 QEMU 源代码各处。Bonzini 建议,一旦 reviewer 看到有过多的重复内容,或者某个功能散乱分布在许多文件中,他们就应该计划一下如何消除这种情况。

Complexity on the QEMU command line

讲座接着介绍了一个关于 QEMU accidental complexity 的案例研究,即命令行的处理代码。QEMU 有 117 个选项,用了大约 3000 行的代码来实现,这里有 "一些 essential complexity,但有太多 accidental complexity"。Bonzini 简要介绍了一些可以对这里进行简化处理的方法,也就是说在处理 QEMU 命令行解析代码时如何使其至少不会变得更糟。他首先问道:到底是什么导致了 QEMU 命令行选项的这些 accidental complexity?这么多的选项参数,具体的实现都有很大的差异,所以讲座将它们归为了六类,并按照 accidental complexity 的递增顺序进行了逐项介绍:flexible (灵活性)、command (命令)、combo (组合)、shortcut (快捷方式)、one-off (一次性),以及 legacy (遗留内容)。

flexible option 是最复杂的,因为它们用来满足广泛的需求。它引入了 QEMU 中大多数的 essential complexity,QEMU 的新功能通常是通过这些 option 来启用的。flexible option 的工作机制是将尽可能多的功能转化给通用的 QEMU APIs 来执行,因此启用新功能通常不需要对命令行解析代码进行任何新增或者修改。这就是为什么可以用一个单个选项 -object 来完成配置诸如加密密钥、TLS 证书、虚拟机与 host 上的 NUMA 节点的关联等的原因。而使用了三个选项,分别是 -cpu、-device 和 -machine 来配置了虚拟硬件的几乎所有特性。然而,这些 option 也不可避免地会有 accidental complexity:至少使用了四个 parser 来解析这些 option。这四个分别是 QemuOpts、keyval、一个 JSON 解析器,以及一个被 -cpu 选项所使用的专门定制的解析器。"四个解析器至少有两个是可以不需要的。"

command 选项是在 QEMU 命令行上指定的,但它通常也对应于在 runtime 调用的某个 QMP 命令。举例来说,guest 启动时可以不让 vCPU 运行(qemu-kvm -S 命令行命令,或者也可以在 runtime 停止),而后续需要时再启动 CPU(通过 QMP cont,表示 "继续")。另外的例子有 -loadvm,用来从一个保存有 guest 状态的文件中启动 QEMU。还有 trace,是用来启用 trace point 的(这要求 QEMU 在 build 的时候带上了相应的某个 tracing 后端支持)。这些 option 给 QEMU 的维护者带来的负担相对比较小,但 Bonzini 建议在添加新的命令行选项时保持一个较高的标准,这样才能今后从 QMP 接口设置这些 option 是更加容易。

加上了 combo 选项之后,"我们开始进入 accidental complexity 的地狱":这些选项在一个命令行选项中同时创建了 device 的前端和后端。例如,QEMU 的 -drive 选项会创建一个类似 virtio-blk 这样的 device,以及一个 guest 的磁盘镜像。这个选项还有一些更加细化的变体,它们对于普通用户来说已经很不方便了,所以 combo 选项确实是真正有用的,但维护这些选项给大家带来的负担也很重。相关的解析代码是很复杂的,而且这些选项也往往会对代码的其他部分产生影响,包括 backend 代码以及 virtual-chipset 的 creation 相关代码。这些选项使得 QEMU 的代码变得不那么模块化,也因为这个原因,如果不了解命令行的细节的话,就无法增加对新 board 的支持。

shortcut 是前三组选项的语法糖(syntactic sugar)。例如,-kernel path 是 -machine pc,kernel=path 的缩写。它们很方便(许多用户可能甚至没有意识到较长形式的存在)而且相应的维护负担很小,因为它们的实现完全可以依赖现有的命令行解析代码来完成。然而,考虑到已经存在这么多的选项了,所以最好也不要再增加。

然后是 one-off 选项。这些选项是必不可少的,但它们的实现往往是不够优化的。它们经常会向全局变量写入一个数值,或者调用一个无法在 runtime 通过 QEMU monitor 调用的函数。Bonzini 恳请开发人员务必避免创建再新的命令了,而是对现有的命令进行重构,改为 shortcut 或 command 选项,他在过去一年中一直在断断续续地做这件事。

最后,来到了 legacy 命令行选项,"我们跌到了谷底"。其中许多都是来自失败的实验(例如 -readconfig 和-writeconfig 选项),或者根本就不应该出现在 QEMU 中的东西。例如,-daemonize 在初始化后让 QEMU 进程变成守护进程,而 libvirt 等工具给用户提供的方案更加好。这些工具的未来,就是等待被废弃(deprecate)并最终删除掉。

Ways to fight back

QEMU 命令行带来了哪些教训,以及开发者可以从中学到哪些原则?他说:"不要从无到有地进行设计",要利用现有的 essential complexity。在着手添加一个新的命令行 flag 之前,问问自己是否有必要。也许可以跟 QEMU 中的 QEMU API 和 QMP 命令等集成起来。这样一来,就可以充分利用 QEMU 的子系统之间现有的互相配合的机制。

其次,Bonzini 强调了 patch reviewer 的责任:需要理解复杂性中的 essential 的这部分,不要把它误认为是 accidental。只有这样做了,才能真正发现 accidental complexity 是否在上升。而且不要让 accidental complexity 完全占据我们的项目。对于那些从事大型代码库重构的人,他鼓励先学习一下 Coccinelle。

incomplete transition 并不总是可怕的事情:从一个旧 API 过渡到一个新的、更好的 API 是软件改进中自然会发生的事情。就 QEMU 而言,有时一个新功能无论如何都需要一个 transition 阶段,因为它会影响到命令行或管理工具,因此就需要一个 deprecate 周期。在这种情况下,利用 incomplete transition,分阶段来完成工作。辨别出所有进行的工作中最小的各个任务,并为先做哪些后做哪些做好计划。

此外,需要确保这些新的、大家推荐的完成开发任务的方法、或使用某个功能的最佳方法要被得到相应的文档记录。"希望在做任何任务的时候都能有一个很明显正确的方法完成。如果没有这么明显的方法的话,那就要有一个记录下来的方法。" incomplete transition 或者分段过渡(piece-wise transition)都不应该阻碍人们对程序进行改进。需要对重复代码和添加更多抽象层之间进行权衡。有些情况可能需要重复代码,但当情况变差的时候,不要再继续让它继续恶化了。

Conclusion

要搭建 essentially-complex 并且可维护的(maintainable)的软件已经很困难了。如果这里讨论的 accidental complexity 的因素(incomplete transition、过度抽象、组件之间不明确的逻辑边界和工具的复杂性)没有得到控制,那么随着时间的推移就会出现越来越复杂的情况。从 QEMU 的经验中提炼出来的教训为其他面临类似麻烦的项目提供了不少借鉴。

[我想感谢 Paolo Bonzini 对本文早期草稿的深入 review。]

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

LWN:以QEMU为例解析软件复杂度!相关推荐

  1. 如何利用快解析软件搭建映射端口

    端口映射,就是将内网中主机的一个端口映射到外网主机的一个端口,提供相应的服务,当用户访问外网IP的这个端口时,服务器自动将请求映射到对应局域网内部的机器上.如何才能实现端口映射?今天小编给大家介绍两种 ...

  2. php函数查询sprintf,PHPsprintf函数用例解析

    代码如下: sprintf() 与 printf() 区别 语法格式一样,只是返回值不同 定义和用法 sprintf() 函数把格式化的字符串写写入一个变量中. 语法 sprintf(format,a ...

  3. 习题6 3.6.2 典型题例解析 3.6.3 自测训练

    //utf-8 <理论书>C语言程序设计教程 主编杨路明 习题6 选择题 ​ ABDA/A 填空题 ​ 函数原型 ​ 静态 ​ 局部 ​ 9 ​ 125 ​ 运行 ​ # ​ 11 写出下 ...

  4. 【实用软件】二维码批量解析软件v1.0

    软件介绍 二维码批量解析软件 软件属于在线解析的,所以必须联网使用 软件属于绿色软件,不需要安装,点击后直接运行 软件功能 二维码批量解析 当大家有一大堆二维码,又不想拿着手机一个一个扫的时候,这样的 ...

  5. 高等数学证明题500例解析-徐兵

    高等数学证明题500例解析-徐兵 链接: https://pan.baidu.com/s/11zPApdQ0xYeBAH3UhazDKw 提取码: edax 复制这段内容后打开百度网盘手机App,操作 ...

  6. 个人开发STDF文件解析软件 STDF BASE TOOL

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言 一.STDF文件是什么? 二.软件介绍 总结 前言 STDF格式文件在1985年由Teradyne公司发布,到目前为止 ...

  7. 领域驱动设计 软件核心复杂性应对之道_DDD - 领域驱动设计对软件复杂度的应对(上)...

    不管是因为规模与结构制造的理解力障碍,还是因为变化带来的预测能力问题,最终的决定因素还是因为需求.Eric Evans 认为"很多应用程序最主要的复杂性并不在技术上,而是来自领域本身.用户的 ...

  8. 阿里研究员谷朴:警惕软件复杂度困局

    作者 | 张瓅玶(谷朴)  阿里巴巴研究员 **导读:**对于大型的软件系统如互联网分布式应用或企业级软件,为何我们常常会陷入复杂度陷阱?如何识别复杂度增长的因素?在代码开发以及演进的过程中需要遵循哪 ...

  9. 阿里研究员:警惕软件复杂度困局

    简介:对于大型的软件系统如互联网分布式应用或企业级软件,为何我们常常会陷入复杂度陷阱?如何识别复杂度增长的因素?在代码开发以及演进的过程中需要遵循哪些原则?本文将分享阿里研究员谷朴关于软件复杂度的思考 ...

最新文章

  1. LabelMe图像数据集下载
  2. 防止stack buffer overflows攻击的方法 : Canary 漏洞缓解机制
  3. 启用Windows Server 2012的远程桌面
  4. 求一个正整数是哪几个正整数相加的和,这些数不能相同
  5. TCP文件下载器(Python)
  6. FF官宣新CFO推进融资和产品交付 贾跃亭激动发声
  7. 【clickhouse】clickhouse 的 数据类型
  8. MySQL学习-排序与分组函数
  9. 开源WinForms界面开发框架Management Studio 选项卡文档 插件 Office 2007蓝色风格 后台线程...
  10. 网页页面禁止用户复制源代码
  11. DRF总结(三)Serializer的使用
  12. 车牌号识别php+sdk,车牌识别SDK
  13. 如何快速上手制作高质量短视频?
  14. linux系统可以在移动硬盘,如何在移动硬盘上装LINUX系统?
  15. ChAMP包处理甲基化芯片数据
  16. 【arcgis10.8最新版安装】
  17. [指北针分类信息软件 v1.5.2.1] 全自动分类信息软件+高效稳定建立SEO外部链接
  18. read()函数:读文件函数
  19. element ui table封装组件,render 函数动态事件设置
  20. 用Python入门不明觉厉的马尔可夫链蒙特卡罗(附案例代码)

热门文章

  1. 补气健脾祛湿的食材有哪些?
  2. My bad! 对不起
  3. 电商模式的认识以及区别
  4. Android 中的各种 Drawable 你都知道了吗?
  5. 退出项目组时,退出所有群,删除所有联系方式
  6. 大数据基础平台实施及运维
  7. 图显系统DRM PLANE完全解析
  8. 国外期货程序化交易之报单流程讲解
  9. 深聊性能测试,从入门到放弃之:性能测试技术栈,看完这篇,保证刷新你对性能测试的认知~~
  10. burpsuite csrf攻击_CSRF攻击的BurpSuite实战