什么是软件发行版?
你可能觉得自己非常熟悉“软件发行版”这个词,但也不妨仔细思考一下,从宏观角度重新看待这个主题。
这个词往往会让我们想到成千上万的 Linux 发行版,但不止 Linux 和 BSD(Berkeley Software Distribution,伯克利软件发行版)的名字里有发行版,Android 和 iOS 也都是软件发行版。
实际上,它太常见了,所以已经没什么人对这个概念感兴趣,也很难找到公认的定义。
软件发行版当然是包括“分发”这个含义的,商业、开源软件都需要分发。而要进一步理解的话,就要先看看软件发行版都解决了哪些问题。
软件发行版这个概念诞生前,软件是不会同他人分享的。当人们想要分享软件时,大家就发现软件需要适当地打包、配置,让各部分协同工作,可能还需要一些胶水来粘合,又要给软件包找到合适的分发介质,安全地从一端传送到另一端,正确安装,然后才开始运行。所以,软件发行需要的是构建可交付软件组合的机制和社区。
操作系统或内核往往是这类机制和社区提供的一种典型的发行版。在它们身后的是发行版维护者或程序包维护者。他们的任务千差万别,例如编写存储所有软件包的软件(称为存储库)、维护包管理器及其格式、维护完整的操作系统安装程序、打包并上传他们构建的软件或在特定时间框架/生命周期内构建的其他软件、确保存储库拒绝所有恶意代码、跟踪最新的安全问题和错误报告、帮助第三方软件适配版本配置,而最重要的是测试、计划和确保一切事物协同运作。这些维护者是发行版的真相来源,他们对此负责。
实现方法多种多样
软件世界,尤其是开源产业正在蓬勃发展。总会有人开辟新的分支,形成树状的软件族谱,对技术和理念选择产生长期影响。我们现在拥有一个充满活力的生态系统,某棵树上的一片绿叶可能会影响另一棵树上完全无关的一片叶子。
目标和专长
不同的软件发行版之间可能会有很大的不同,那为什么不做一个统一的平台呢?一个原因是对专业化和差异化的需求。每个发行版都针对不同的受众,有着自己的社区理念。举一些例子:
一个发行版可以支持特定的硬件组合,比如特殊的 CPU 指令集和外部设备;
发行版可以针对某些环境做优化:桌面、便携式移动设备、服务器、小型机、嵌入式设备、虚拟化环境等;
发行版可以具备或不具备商业支持;
可以为某个领域中的不同专业级别设计针对性的发行版,如安全研究、科学计算、音乐制作、多媒体终端、汽车 HUD、移动设备接口等;
发行版可以遵循专业环境中的某些必要标准,如安全和强化标准等;
发行版可以具备单一商业用途,如防火墙、计算机集群、路由器等。
维护者根据发行版的设计目标来做决策,决定配置、安全性、可移植性和功能的细节。例如,如果某个发行版要支持免费软件,就会严格检查自带软件和存储库中的许可证,并安排程序来检查核心许可的一致性。
如果某个发行版针对的是桌面受众,就会优先考虑国际化、易用性、用户友好程度并带来丰富的软件包。而如果目标平台是实时嵌入式设备,就会精简内核、配置并优化,对软件包也要精挑细选。如果一个发行版针对的是喜欢控制设备的高级用户,维护人员将让用户来做出大多数决定,提供尽可能多的最新版本软件包,提供宽松的安装途径,并带来众多库和软件开发工具。
每个发行版都会按照自己的理念来设计,并提供一层组件,也就是一个软件栈。
分层
发行版维护者在构建发行版时手头往往有很多构建块来选择,他们从中选出一些自认为最关键的来组成发行版。有时他们还会做出小巧松散的内核,并提供胶水软件来让用户轻松地按需(安装、运行时、维护模式等)选择和切换模块。
具体来说,这些相互依存的组件包括:
第一部分是安装方法,一切的起点。
第二部分是内核,是当今所有操作系统的真正核心。但某些发行版可能会提供针对不同事物的多个内核,或者根本不提供内核。
第三部分是文件系统和文件层次结构,这一组件管理物理或虚拟硬件上文件的分发位置和方式。这一部分可以混合搭配,比如文件系统树的各部分存储在不同的文件系统上。
第四部分是初始化系统,PID 1。PID 1 是系统上所有其他进程的母进程。它的作用以及应该包含的功能是一个争议性的主题。
第五部分由 Shell 实用程序组成,有时也称为用户区或用户空间。它是第一个用户可以直接与之交互以控制操作系统(进程运行的地方)的组件层。在基于 Unix 的系统上,用户区实现一般遵循 POSIX 标准,这里也有很多争议。
第六部分由服务及其管理部分组成。守护程序(长时间运行的进程)负责让系统保持秩序。管理功能是否应该成为初始系统的一部分,也是有争议的主题。
第七部分是文档。它经常被遗忘,但却非常重要。
最后一部分是剩下的内容,包括所有用户界面、实用程序和它们在系统上的管理途径。
稳定发布与滚动发布
发行版想要做到让软件的各个部分保持更新状态,就需要选择一套发布机制。这往往针对的是外部的第三方开源软件。
具体来说,这种机制指的是软件各个部分与对应最新版本的差异程度,版本越新,对整套系统带来的破坏性风险越大。一种方法是每个部分出新版时就更新软件,叫做滚动发布;另一种则是在各个部分的版本经过严谨测试后再加入整个系统,这叫做稳定发布。
注意:这是从维护人员打包软件版本的角度来区分的机制。维护者的上游更改可以直接影响所有用户,也可以让用户选择多个上游版本来源。
第一种情况极端化来说,就是让用户直接从软件供应商/创建者的源代码存储库下载更新,或者相反,让软件源直接向发行版推送更新。这很容易造成系统冲突或导致安全漏洞,但使用容器化环境可避免这种后果。
发行版往往分为长期支持稳定版本(长期与必要的安全更新和错误修复保持同步)和非稳定最新版本(快速测试最新内容和特性)。用户可以在一段时间内跳转至发行版的最新版本,其中包含很多重大更改。
一些发行版可能会在主要版本更新上带来内核的 ABI 或 API 重大更新,这意味着系统中的所有内容都需要重建和重新安装。发布周期和更新速率实际上是一个范围。
在两种情况下,软件版本更新时,维护者都必须决定如何与用户沟通交流,告诉他们发生了什么事情。无论是通过官方渠道、日志记录、邮件还是其他方式,交流都必须是双向的。用户要报告错误,维护人员要发布他们的决策并判断是否需要用户参与,从而创造围绕发行版的社区。
滚动发布需要软件包维护者付出巨大的努力,因为他们必须持续跟上开发人员的步伐。考虑到新库的数量越来越多,涉及的语言也越来越新,这项任务也会更加困难。
各类用户都希望能从一种系统中获得准确的信息。企业环境和关键任务更喜欢稳定的发行版,软件开发人员或普通的最终用户可能更偏爱最新版本。
跨发行版标准
考虑到这些需求,我们当然会需要跨发行版的标准。
在用户级别,不同标准下的差异不会很明显,因为 Unix 系统中各种事物总能工作起来。各个发行版或多或少会遵循 POSIX 标准。
在 Linux 生态系统中,自由标准组织试图使用通用的 Linux ABI、文件系统层次结构、命名约定等来改善软件的互操作性。但涉及到跨内部发行版的共通特性时,这些努力只是冰山一角。
此外,发行版的各个层级都可以说有自己的标准:桌面互操作性标准、文件系统标准、网络标准、安全性标准等。这方面最大的推动者是自由桌面组织,它试图为 Linux 发行版创建一个强制的跨发行版标准。
但又有一个大问题出现了:我们是否真的需要这样的跨发行版标准呢?
包管理和打包
这一部分谈的是软件包,包括存储和安全访问各个包的机制,如何搜索、下载、安装、删除它们以及本地管理、版本控制和配置等事宜。
分发方式
指的是分发和共享软件的方式,以及这个过程的前端部分。
首先是存储方式。物理介质是一种历史悠久的选项,但随着今天软件的飞速发展,物理介质就显得不够灵活了。通过 FTP、HTTP、HTTPS、公开的 svn 或 gitrepo、中心化枢纽(如 Github)或应用程序商店(如苹果和谷歌提供的应用程序商店)这些网络途径分发会更便利。
这里的要求是存储系统和通信系统应安全、可靠,不会出现故障,并且可以从任何地方访问。于是人们往往使用镜像系统来提升可靠性,并通过边缘网络来加快分发速度和均衡负载。存储库维护者来决定存储的方式和格式。通常,这会是一个带有软件 API 的文件系统,用户可以通过互联网与之交互。有两种主要的格式策略:基于源代码的存储库和二进制存储库。
接下来的问题是谁可以上载和管理主机包,谁有权复制存储库。
作为用户的一个真实来源,重要的是确保存储库在接受一个软件包之前已对软件包做了验证和安全处理。许多发行版都只有维护者才能做到这一点。他们会有密钥以对软件包进行签名并验证。
还有一些发行版允许用户自己构建软件包,然后将其发送到中心枢纽以进行自动或手动验证,再上传到存储库。每个用户都有自己的签名验证密钥。
还有第三种选择是介于两者之间的,发行版的核心由官方发行维护者管理,其他部分由用户社区管理。
最后一个问题是包如何到达用户。用户如何在本地和远程与存储库交互取决于包管理的选择。比如用户是否缓存远程存储库的版本,就像 BSD 端口树系统常见的那样?跟踪更新、锁定软件版本、允许降级的灵活性如何?用户可以从其他来源下载吗?用户能否在其计算机上拥有同一软件的多个版本?
格式
如前所述,软件分享格式主要有两种理念:源代码端口样式和预构建的二进制程序包。
在用户端进行管理的软件称为“程序包管理器”,它是与存储库的连接桥梁。许多发行版会创建自己的包管理器或使用流行的选项。它负责搜索、下载、安装、更新和删除本地软件。
这里的规则是,不通过包管理器安装的内容相当于不存在。请注意,发行版可以有很多包管理器。
每个包管理器都依赖于特定的格式和元数据来管理软件。这种格式可以由一组文件或具有特定信息段的单个二进制文件组成。
以下是包管理器需要的常见信息列表:
包名称
版本号
说明
对其他软件包及其版本的依赖
需要为包创建的目录布局
以及所需的配置文件和是否应覆盖它们
所有文件的完整性或 ECC
验证,通过加密签名之类的机制确保包来自受信任的来源
确认包是一个组、一个元数据包还是一个普通包
对某些事件采取的操作:安装前、安装后、删除前和删除后
安装时是否有特定的配置标志或参数要传递给包管理器
因此,预编译的包会减轻包维护者的负担。这里的优点之一是预编译的软件包很方便,下载和运行起来更容易。
另一方面,专有软件往往会用编译好的二进制包来分发。二进制格式还可以节省空间,因为代码会以压缩的存档格式存储。一些包管理器还可以选择将压缩的选择权留给用户,并从其配置文件动态识别如何解压缩软件包。另外还有“Alien”之类的工具,可以将一种二进制程序包格式转换为另一种二进制程序包格式来简化包维护人员的工作。
冲突解决方案和依赖管理
解决依赖性
包管理器最难的工作之一就是处理依赖关系。
包管理器必须保留系统上当前安装的所有包和版本的列表及其依赖项。当用户想要安装软件包时,它必须以该软件包的依赖关系列表作为输入,将其与已有软件包的依赖关系进行比较,并以满足所有依赖关系的顺序输出需要安装内容的列表。
在构建自动化实用程序(例如 make)的软件开发领域中,这是一个常见的问题。该工具创建一个有向无环图(DAG),并使用图论和无环依赖原则(ADP)来尝试找到正确的顺序。如果找不到解决方案,或者图中有冲突或循环,则应中止操作。
反之,在删除包时也是如此。我们必须做出决定,是否要删除依赖这个包的其他内容。确实,这是一个难题。
版本控制
如果我们允许在系统上安装同一软件的多个版本,就会遇到版本控制的复杂性。如果我们不这样做,但允许用户从一个版本切换到另一个版本,也得决定是否要切换版本依赖的所有软件包。
发行版也需要版本控制。许多发行版会附加特定的包,于是不同的版本可能有不同的存储库。
命名约定也是一个关键部分,它应该向用户传达命名的含义,告诉用户是否发生了更改。包维护者应遵循软件开发人员的命名约定,或者使用自己的约定。如果两个软件的名称相互冲突,存储库中就需要添加一些额外的信息。
我们要决定是依靠语义控制版本,比如主要、次要、补丁,还是像许多发行版一样依赖名称来区分版本,还是靠发布日期或者仅仅是增量数字来做区分。
静态与动态链接
有一件事可能不适用于基于源代码的发行版,那就是在构建包时决定是静态还是动态链接到库。
动态链接指的是程序选择不将其依赖的库包括在可执行文件中,而是仅对其引用,然后在运行时由动态链接器解析,该链接器将在使用时将共享库加载到内存中。相反,静态链接意味着将库直接存储在已编译的可执行程序内部。
当大量软件依赖相同的库时,动态链接很有用,一次只需要一个实例即可。可执行文件也较小,并且在更新时所有依赖它的程序都会从中受益(只要接口相同)。
动态链接环境中的包管理器必须考虑已安装库的版本以及依赖它们的包。如果不同的包依赖于不同的版本,则可能会产生问题。由于这个原因,至少在与核心系统无关的事情上,一些发行版社区选择完全放弃动态链接并依赖静态链接。
静态链接的另一个附带好处是,它不必操心动态链接器的依赖关系,从而提升了速度。
静态构建简化了包管理流程。不需要复杂的 DAG,因为一切都是独立的。此外,这可以允许同时安装同一软件的多个版本,而不会发生冲突。更新和回滚不会让静态链接陷入混乱。
这会带来更加容器化的软件,继续发展就会产生市场平台(例如 Android 和 iOS),在平台上软件开发人员可以自己分发,完全跳过中间人,并为越来越不耐烦的用户提供了适用于其当前操作系统的最新版本。一切都是自包装的。
但这在很大程度上依赖于存储库/市场的信任程度。需要有很多安全机制来防止恶意软件被上载。
这对用户和软件开发人员(从特定角度而言)也非常重要,因为他们可以直接分发预构建的程序包,尤其是在基本系统具有稳定的 ABI 的情况下。
另一个角度来说,静态链接会让软件包占用更多空间,浪费更多资源(存储、内存、能源)。
此外,由于它是软件开发人员直接向用户推送的模式,因此不再有发行维护人员对发行版的审核,证书的不确定性也会增加。
此外,软件开发人员应确保其代码中没有漏洞或错误。从开发人员角度来说,他们需要自己下载各个依赖库的源代码并编译构建,整个软件就成了基于源代码的发行版。
可再现性
由于包管理在过去几年中变得越来越杂乱,因此出现了一种新趋势来恢复各方面的秩序,这种趋势就是可再现性。它受到了函数式编程和容器的启发。尊重可再现性的包管理器将其每个版本都断言为始终产生相同的输出(功能层面)。
它们允许不同版本的软件包彼此并排安装,每个版本都位于自己的树中,并且允许普通用户安装只有他们可以访问的软件包。因此,不同用户可以有不同的软件包。
它们可以用作通用包管理器,可以与其他任何包管理器一起安装而不会发生冲突。
最突出的例子是 Nix 和 Guix,它们使用纯函数式部署模型,将软件安装到通过加密哈希生成的唯一目录中。每个哈希中都包含来自每种软件的依赖关系,从而解决了依赖地狱的问题。这种包管理方法有望生成更可靠、可再现和可移植的包装。
有状态和可验证的系统
关于信任,可移植性和可再现性的讨论也可以应用于整个系统本身。
当我们谈论软件市场时,由于软件开发人员直接将发行版推向市场,而用户可以立即访问最新版本,这就必须采取额外的安全措施。
措施之一是容器化,将每个软件沙箱化。使每个软件都在各自的空间中运行,而不会影响其余的系统资源。这消除了审核和验证每个软件的沉重负担。有许多实现这种沙箱的解决方案,例如 docker、chroot、jails、firejail、selinux、cgroups 等。
我们还可以隔离用户的主目录,让它们自打包,从不安装或修改可全局访问的位置。这使我们拥有了可验证的系统核心,因为它没有更改,保持原始状态,所以很安全。将发行版的用户部分设为原子的、可移动的、容器化的,而让其余部分可重现的理念正在改变游戏规则。
有了容器、虚拟化以及特定和通用的包管理器,发行版还重要吗?
如今,在容器、虚拟化以及特定及通用的包管理器中,发行版是否还扮演着重要角色呢?
对容器来说,它们仍然非常重要,因为它们通常是其他组件的基础。发行版由共同构建和发行软件并确保其正常工作的人员来创建。这不是容器管理者的任务,而且会给容器管理者的工作带来很大便利。
另一方面,容器隐藏了漏洞,而发行版维护者则可以起到沟通和跟进安全漏洞及其他错误的作用。社区可以解决每个人都面临的艰巨问题。
构建容器的系统管理员可能不具备管理和构建数百个软件和库,并确保它们协同工作的知识。
如果包是自包含的
如果包是自包含的,发行版是否重要呢?
在某种程度上,它们是这种通用的自包含软件包的提供者/发行者。正如我们所言,保持分发的哲学并提供经过测试的工具箱是非常重要的。
我们很可能会走到拥有多个包管理器的世界,每个包管理器都因其特定的领域和目的而受信任。每个人都有不同的哲学和技术真相来源。
编程语言包管理
旧的打包方法几乎无法跟上今天软件的构建速度和粒度。旧的软件发布生命周期已被淘汰。因此,人们开发了特定于语言的工具,不仅用于库的安装,还包括软件的安装。现在,我们可以将发行版提供的包管理器称为系统级,其他包管理器称为应用程序级或特定管理器。
因此,系统内的复杂性和冲突激增了,发行版包管理者发现管理和维护可以通过这些工具安装的内容是毫无意义的。反之亦然,特定的工具制造商也不会对将他们提供的内容包含在发行版系统级包管理器中感兴趣。
尊重可再现性的包管理器(例如我们提到的 Nix)基于本地化思想,因此可以更清晰地处理此类情况,把所有内容放在不由系统级包管理器维护的目录树里。
同样,这里我们还要面对用途不同的多种包管理器。
无发行版
在容器世界中一个流行的话题是“无发行版”。它指的是替换发行版中提供的所有内容,删除其自定义内容或从头开始构建映像,并且可能不依赖通用软件包管理器。
这种容器的优点是它们很小并且针对单一目的。这使系统管理员可以完全控制容器上发生的事情。
但请记住,就像我们前面提到的那样,控制所有内容需要付出巨大的代价。这将使系统管理员(而不是分发维护者)承担管理和跟上错误和安全更新的负担。
总结
我希望大家通过本文对发行版提供的内容及其在当前时代的地位有一个更清晰的了解。
你对这一主题有何看法?你喜欢多样性吗?你将使用哪个堆栈来构建发行版?你如何看待静态版本,让用户将自己的软件上传到存储库?你有解决信任问题的方法吗?你如何看待这种变化?欢迎留言。
原文链接:
https://venam.nixers.net/blog/unix/2020/03/29/distro-pkgs.html
评论