OS 升级迭代与向前向后兼容问题
内容分析:
1. OS更新的兼容性问题
2. 内核API向后兼容
3. 内核API向前兼容
4. 内核API的修改
01.OS更新的兼容性问题
今天,我主要想和大家探讨方德在系统金融兼容方面的方案及遇到的问题,并分享我们的应对策略。首先,系统更新会面临一些问题。目前,国产系统生态中存在大量私有驱动,与社区开源驱动不同,私有驱动的版本更新往往跟不上系统更新。系统更新后,用户可能会发现驱动模块无法加载,或虽有源码却无法编译。有些用户会购买第三方商业系统,更新后可能会发现系统无法正常运行。购买的第三方业务系统通常不会频繁更新新内容或基础库版本。此外,用户的常用软件和业务系统也可能出现无法运行的问题。比较简单的问题可能是配置文件出错,例如更新后系统无法运行,我们可能会建议用户删除旧配置文件或进行其他操作以解决问题。有的可能是基础库版本不匹配,更严重的问题可能是内核更新后整体不再支持。
因此,在系统更新时,需注意两种兼容性:一是向后兼容,即在系统层面,底层库需保持兼容性。在这方面,我们基本只增加不删除,以确保多个版本共存。另外,对于核心组件如窗口管理器和自营系统组件,以及关键配置文件,也要保持兼容性,通常只增加不删除,以免下一版本改动过大导致问题。在分析旧配置文件时,可能会遇到各种问题。
向前兼容意味着系统需保持兼容性,因此我们无法大规模跟进开源社区更新。但新版本常引入性能和功能需求,需跟进。若不跟进,时间一长,用户更新软件时,我们的系统可能无法继续支持。因此,我们面临向前兼容问题,当然, BUG 补丁需及时修复。系统内核默认会启用我们的补丁。平时,若遇到影响力大或严重的漏洞和 BUG 修复,可能直接通过 CVE 推送。这是一种临时解决方案。
02.内核API向后兼容
我简单介绍一下向后兼容的问题。内核包含一个 ABI 二进制接口,主要通过系统调用 syscall 以及 / dev 、 / sys 、/ proc 文件系统提供接口。内核本身有一套机制来保证 ABI 兼容性,其文档的 ABI 目录中也有相关说明。我们在研发时也严格遵循这一机制。所有二进制接口遵循一个原则:只增加不删除。如果因 BUG 或其他原因需要修正时,要全面修正,而不仅仅是增加,以保持兼容性的稳定性。
接下来,我们重点讨论 API 的变化。通常情况下,我们很少遇到需要修改内核 ABI/API 的需求,这种情况较为罕见。一般来说,这种情况会导致一些混乱或变化。主要是我们的工程师在修改内核配置时可能没有足够注意,他们可能只想修改某一配置,但在提交时可能会无意中修改了多个配置。另外,内核版本更新也会导致 ABI/API 的变化。在这方面,我们需要找到方法来检测这些变化,并在检测到后进行兼容性处理。
目前,在编译时我们会进行 ABI 检查。即在发布第一个版本的内核时,会编译生成一个符号表,每个版本的符号表都不相同。发布时,我们会将第一个版本的符号表作为白名单,这是我们的完整白名单。我们还有一个最小支持子集,这是从适配的硬件以及客户提供的第三方硬件软件的内核模块中提取的内核符号依赖,我们会将其导出作为重要支持子集。重要支持子集坚决不允许修改,因为一旦发生变化,客户更新后,业务可能会无法运行。
因此,对于配置,尤其是第一个版本的配置,我们需要进行严格审核。发布后,这些配置基本不能再随意关闭。新功能可以开启,但已开启的配置不要关闭,以保持兼容性。这可能会导致一些 API 变化。因为在开发过程中,可能错误地打开了某个配置,导致性能下降或安全问题。此时,如果需要关闭配置并引起 API 变化,我们会经过公司内部专家组评审,确定是否可以变更。如果不能变更,我们只能在内核中进行动态修正。因为关闭某些内核配置后,相关功能可能无法正常工作。但如果通过代码或程序修正,可以动态达到目的。最小子集是不可协商的,如果其中的符号涉及 Bug 或其他问题需要修改,只能在代码层面进行修改,不能禁用 ABI/API 。
编译时,一旦检测到 API 变化,编译将终止,开发者的编译无法通过。然后,我们会检查引起变化的符号。如果是完整符号表中的符号,原则上不允许修改。如果确实需要修改,我们会通过专家组评审,评审通过后才会进行修改。如果不能修改,只能在代码层面实现动态兼容。最小子集中的符号不能修改。
除了动态检查工具,我们还开发了一个静态检查工具。目前,该工具已开源,并被电子四院采纳,用于制定国产操作系统的开放标准。他们可能有需求,需要检测方德等各厂家的操作系统是否支持特定版本的内核标准。目前,他们正在使用我们公司的工具进行检测。该工具是一款静态检测工具,与编译时不同,它可以频繁扫描 OS 的 ISO 文件,扫描后会生成报告,告知你操作系统是否支持某一版本的标准。如果不支持,它会打印出一些初步的头文件信息。扫描完成后, ISO
是否支持我们的API版本将更加清晰。
03.内核API向前兼容
接下来,我简单介绍一下我们向前兼容的做法。向前兼容主要是为了让旧内核具备更多功能。一个典型的例子是牛肉系统,它目前已基本成为主流。主流应用中,有些新应用的后端其实不支持 AIO 。如果旧内核不支持 AIO ,客户在使用你的系统时,可能会遇到问题。例如,当他们需要运行新的业务系统并使用 LUN 时,系统可能无法正常运行。是的,尽管我们系统的版本号可能较老,但它包含大量新内核的性能和功能特性,我们能够将其引入。此外,设备支持和新功能更新是我们经常需要做的工作。我们的服务器和网卡设备经常从开源社区获取更新,以便我们提供支持。
向前移植并非简单地应用补丁,代码中存在众多路径,你可能一次性编译通过并运行成功,但这并不代表功能调节已成功移植。移植后,我们需要做以下工作:第一步是对补丁进行分析研究,评估其影响。我们有静态和动态检查工具,会进行一些检查。最重要的是构建测试案例,确保补丁功能正常。因为我们上游的许多特性或修复都基于上游内核的某些配置。
向前移植和向前兼容会产生类似的影响。接下来,我们讨论的是每月一次的补丁,可能代码量较少,内容不多。但它可能依赖于某个模块,从而成为一个新特性。这个新特性可能是在某个版本中引入的,之前尚未使用。这个补丁也需要对应这个新特性。新特性可能又依赖于其他模块或子系统的一些新内容,需要逐套进行分析和评估。
现在,我来介绍一下我们公司关于 io-uring 的做法。首先, io-uring是在 Linux 4.19 版本中引入的。但我们已知的是 Linux 5.4的某个版本,移植时,我们直接在上游查找相关提交,发现提交数量众多,且依赖于其他模块。最主要的是 block 模块和文件系统模块。在我们的系统中,内存模块也受到影响。如果它们像姐妹一样在一起,内存模块中有些部分看似无关,但实际存在关联,这比较麻烦。
当时我们的想法是,在遗失东西时,主要有两种方法:一种是自顶向下的方法,即直接找到目标版本。如果目标版本中的文件是新的,而4.19版本中没有,那么我就忽略中间过程,直接将其拿来。如果中间涉及一些变动,我就直接通过一些特定命令追踪所有改动,然后一次性将整个改动导入内核。接着进行一些修正,修正后将其移植过来。这种方法的优点是可以一步到位,只要这一步成功,中间过程可以忽略。缺点是,随着版本不断更新,涉及的内容越来越多,因素反应会涉及到的模块或补丁特别多,这会导致工作不可控,可能花费很长时间。最后,在某个阶段,当大家清楚地了解某个模块时,会发现4.19.38.14版本可能很困难,最终可能得出结论:这个版本不合适,那么就需要再找另一个版本。
另一种方法是自底向上。就像自己在开发功能一样,从第一个补丁开始,然后是第二个补丁,不断向上推进。只要第一个补丁能够一次性成功应用,就说明这个功能在我这边是支持的,至少可以有一个初始版本。然后继续向上推进,一个版本接一个版本地进行。这样做的问题是,中间涉及的补丁数量实在太多,反复这样做的话,工作量特别大。
最终,我们选择将这两种方法结合起来。因为我们发现内核中有一个IO-U ,在向阻塞位移动时会打一个 Tag ,相当于版本标识。每次到达合同窗口期时,它才会更新版本。我查看了 Merge Tag ,数量不多,大约十个。我们以 Merge Tag 为目标,已知第一个初始版本为Merge Tag 1,第二个版本为 Merge Tag 2。如果目标版本中间只有十个 Merge Tag ,我们就在每个 Merge Tag 中使用自顶向下的方法。每个 Merge Tag 都有许多提交,我们将所有提交一次性集成到现有的4.19内核中,然后在每一步完成后进行测试验证。
测试验证主要包括黑盒测试和白盒测试, Io-Uring 自身就提供了测试程序。我们还基于 Syscall 接口编写 Demo ,运行其所有路径。每次 Tag 都要进行测试,这样我们能更好地控制整个过程。但我们清楚每一步的状态。 Io-Uring 有很多特性,可能在移植到某一步时,有些特性还不支持。
04.内核API的修改
最后,我来谈谈内核 API 的修改。其实前面我们已经提到,正常情况下,我们没有需求去修改 ABI 或 API 。在内核中,结构体或函数通常包含一个 Flag 成员变量,这是内核 API留给我们的扩展空间。一般情况下,引入新功能时,尽量利用内核提供的 Flag ,无论是在结构体还是函数中。使用Flag进行扩展,不要增加参数,也不要盲目更改数据类型。更不要随意复制 API 并添加 V 1、V 2 等版本构成自己的版本。问题是,将来漏洞修复可能会修复原内核函数,但无法修复复制的版本。结构体也是如此,不要随意更改系统结构体中的数据类型,更不要更改成员变量,否则其定义等都会出现问题。增加后,尽量检查该结构在内核中常用的部分是否受到影响。