首页 > 编程知识 正文

翻译架构,业务架构翻译

时间:2023-05-06 05:53:38 阅读:273575 作者:585

上次我检查时,外面是 2019 年,但我们仍在使用操作系统,其架构基本上可以追溯到 60 年代后期的 Multics 或 70 年代中期的 VAX VMS(以及 TBH,从 50’000 英尺开始,Multics 和VAX内核是不是真的是激烈的-最起码,这是很多 比任何人差小到什么-I-AM-去到提出)。这意味着我们正在使用的操作系统是在 40 到 50 年后构建的。 当然,自 Multics/VAX 以来,设计发生了很多变化,但某些基本前提(例如以任务/进程为中心,内核模式和用户模式编程之间的极端差异,纯粹依赖基于硬件的保护,甚至没有考虑到基于语言的,甚至使用与编译时相反的运行时间接)仍然存在,而且 – 仍然被认为是可能设计的唯一方式(tm)操作系统。

顺便说一句,Multics/VAX 和后来的 Unix/Windows 是他们那个时代非常好的系统,但毫无疑问,自 60 年代以来,IT 领域发生了相当多的重大发展,无论是在硬件还是软件方面,而且可能更重要的是——在我们在二十一世纪使用计算机的方式。

这可能意味着设计现有操作系统的某些前提不再适用——另一方面,可能会出现新的考虑因素。换句话说——重新审视操作系统设计(不要盲目接受现有操作系统的构建方式),看看当前的方法是否仍然是最佳的,这可能是个好主意。

当然,作为任何破坏当前思维方式的尝试,这篇文章都会受到攻击——这完全没问题;尽管如此,在攻击的同时,请尽量避免“嘿,你提出的与过去50年所做的不同,所以肯定是错误的”和“显然不可能实现的论点”你在说什么; 这就像提议永动机,所以它一定是错误的,而不必读取休息”(后者通常是因为思维在现有的OS设计,有许多这样的事情方面都确实是不可能的)。

1. 过去 50 年发生了什么变化?

如上所述,在过去的 50 年里,IT 领域发生了大量的变化;让我们讨论对我们的目的特别感兴趣的那些变化。实际上,发生了许多不同的事情,我必须将它们分为三个不同的类别,以便它们更有条理。

2. 我们使用计算机的方式的变化

第一个高级更改类别是关于我们使用计算机的方式:

2.1 从批处理到交互式编程的转变。

从历史上看,第一台计算机被视为计算某些东西的机器(例如Colossus在二战期间用来破解德国密码);这种趋势已经持续了一段时间(举个例子,当时无处不在的 OS/360 的第一个版本是严格的批处理系统[TanenbaumBos])。此外,Multics(我们今天使用的所有 *nix 系统的勤奋的睫毛)的理念是“使用小型专用计算机来处理实时中断,以便利用主系统进行处理”。以更悠闲的方式对信息进行主要处理。” [CorbatóVyssotsky]。反过来,这使得操作系统(以及一般的程序)全神贯注于任务(后来的线程):即使是现在,甚至在纯事件驱动程序的情况下,来自传入中断/事件的信息也会通过大约六个重量级的定时器可抢占 线程(导致相当多的非常昂贵的线程)进程中的上下文切换),然后再传递到应用程序级别的重量级 线程,该线程将调用相关事件处理程序。然而,自 Multics 时代以来,我们可以看到从执行批量计算到使用 CPU/MCU 来控制现实世界系统的逐渐转变(这相当于或多或少地实时对不断变化的输入做出反应)——更一般地说,互动系统, 已经发生了。事实上,在过去的 50 年里,这个部门的事情确实发生了巨大的变化——我们发现越来越多的计算机只是在等待某些事情发生——换句话说,它们对输入/处理传入事件做出反应 ,然后回去等待(反对长时间执行一些计算)。绝大多数网络服务器、游戏、桌面/手机应用程序和 MCU主要是处理输入而不是运行长期计算;当然,HPC 确实存在,但它肯定不再是现代计算机的唯一(甚至不是最流行的)用途。这种范式从计算事物转变为事件处理有一个极其重要的含义,它在很大程度上被现有的(~=“回到 60 年代”)操作系统设计所忽略:

对于此类交互系统(涵盖绝大多数现实世界的 MCU/CPU),处理传入事件(通常以未指定的顺序出现,就像在现实世界中发生的一样)成为一项极其重要的任务。此外,对于交互式程序而言,“执行线程”的整个概念(如“无限期运行的线程”)的重要性远不如类似 HPC 的纯计算任务(如果我们只有简短的事件处理程序——我们没有) t 真的需要长时间运行的“线程”,下面有更多内容)。举个例子——我知道的所有网络协议都是根据处理事件的状态机来描述的——而不是根据无限期运行的执行线程来描述的。

2.2 微处理器和单用途盒的出现。

在 1970 年代微处理器出现之前(我们可以争论是 i8008 改变了游戏规则,还是 i8080,但对我们来说这并不重要),每个人都坐在大型机终端前,只有几个现有的大型机(正如 NPL 负责人查尔斯·达尔文爵士所说的那样——“因为很有可能……一台机器就足以解决整个国家对它提出的所有问题”);结果,该大型机只需要共享。如今,我们的汽车和冰箱中都有 MCU/CPU,更不用说手机、手表等了。从大型机到微处理器的这种转变还有两个额外的重要影响,目前现有的操作系​​统设计在很大程度上忽略了这些影响:

在许多实际案例中,每个机器和每个 CPU 只运行一个应用程序(它一直发生在 MCU 世界和服务器端世界中)。反过来,这意味着在这些情况下,保护内核空间不受用户空间影响变得比在大型机和台式机上重要得多。举一个真实世界的例子:在我的专用数据库服务器上,如果我的数据库应用程序崩溃了,我不太关心操作系统是否幸存下来。在许多现实世界的案例中,抢占式调度(本质上是另一种防止行为不端的应用程序的保护形式)不再是真正的要求。注意:另一方面,多应用部署场景(例如在手机和桌面上)仍然被广泛使用,因此完全放弃内核保护是不可行的。
2.3 分布式系统的网络化和外观的普及。

如上所述,在我们设计现有操作系统的时代,存在的计算机很少——许多人共用一台计算机。相比之下,如今,整个计算机网络通常用于执行可见的单一任务(计算任务或交互任务)。

这允许完成很多工作——但令人惊讶的是,对此类分布式事物的操作系统级支持极其有限(并且分布式操作系统并没有真正在实践中使用)。

2.4 开源软件的激增。

到了 60 到 70 年代的 Multics/VAX 时代,公共领域软件(来自学术研究的软件)变得不足——所有软件都在变得越来越专有。然而,到 80 年代至 90 年代,已经开始了几个不同的开源计划——并且开发了许多高质量的开源软件。

从我们目前的角度来看,开源使​​得源代码级(API 级)兼容性对于许多用例来说是完全可以接受的(与闭源软件所需的二进制 ABI 级兼容性相反)。换句话说,出货源代码程序(或至少将它们发送到3次三方编译器)在许多情况下没有资格作为一个搅局者了。

3. 硬件的变化

现在,让我们来看看硬件发生的变化:

3.1 性能改进。

自 60 年代以来,CPU 速度和内存大小(以及更普遍的存储大小)发生了巨大的改进。这些改进意味着我们可以负担得起 1970 年无法负担的许多东西。举个例子,RAM 大小已经从 Multics 时代的 1M 增长(这是一个价值数百万美元的大型机(!)和当时被认为是巨大的)在我的桌面上超过 10 GB(RAM 大小增加了 10’000 倍,而价格又下降了 10’000 倍)。

实际上,这意味着如今,即使在 RAM 中,我们也可以轻松地将大多数桌面应用程序的所有输入存储几个小时。反过来,这会启用一些非常受欢迎的功能,例如记录/重放生产故障以启用事后分析以及容错(更多内容见下文)。此外,程序大小的重要性已大大降低。当然,关于程序代码不适合缓存的奇怪故事仍然存在——但它们很少而且相去甚远。另一方面,动态分支预测器(见下文)确实受益于在不同上下文中使用的相同代码,它们被编译到内存中的不同地址。 3.2 运行时间接寻址变得昂贵。

尽管自 60 年代以来计算机的所有组件都得到了极大的改进,但性能改进甚至不是针对不同的组件。作为一个非常重要的例子(有时称为“内存间隙”):早在 70-80 年代,典型的寄存器-寄存器操作是 1 到 4 个 CPU 周期,而 RAM 访问时间约为 2-4 个 CPU 周期[Knuth] [ i8080]; 总的来说,我们可以说在那个时候,RAM 访问成本与 RR ops 大致相当。在 2019 年,如果谈到台式机/服务器/电话(虽然不是关于 MCU),一个简单的 RR 操作需要 <1 个 CPU 周期(当然,平均而言),但非缓存 RAM 访问很容易需要 100+ 个 CPU 周期(请注意,它仍然比 50 年前的 RAM 访问速度更快,只是从那时起 RR 提高了很多)。换句话说,对于相当多的系统,RAM 访问变得比 RR 操作贵 100 倍——与 K&R 时间相比,从性能的角度来看,这是一个重大变化。这反过来又破坏了在对现有操作系统进行编程时广泛使用的某些编程范式;特别是,

在种类繁多的 CPU 上(~=“几乎所有东西都在台式机/服务器/手机上运行”),运行时间接访问变得非常昂贵。这,反过来,这意味着典型的C编码样式的一切是通过添加一个间接层来解决-与此间接是一个运行时间间接(如内存指针和/或函数指针) -成为低效的(尽管它使用的是在来自 K&R 时代的 CPU 上高效)。这可以通过用编译时间接替换运行时间接来改进——最流行的例子是 C 的 qsort() 和 C++ 的 std::sort() 之间的比较,但我们对运行时间接的一般观察远不止一个具体例子。为了解决这个性能问题,间接访问可以从运行时转移到编译时(通过使用宏或单态泛型),或者可以通过使用诸如数据展平和内联热/非内联冷代码之类的技术改进数据/代码空间局部性来缓解;然而,这些技术中的大多数并没有广泛用于现有操作系统的代码中。

3.3 线程上下文切换变得非常昂贵。

为了解决同样的“内存缺口”,高端 CPU 引入了多级缓存;如今,对于支持多 GB DRAM 的 CPU,通常具有三级缓存。反过来,这有一个极其重要的含义:线程上下文切换完成后重新填充缓存的成本。经常提到线程上下文切换的成本大约为 2000 个 CPU 周期——然而,这只是上下文切换的显式成本(保存寄存器等),并没有考虑重新填充的成本缓存。如果我们考虑到后面这些成本——我们会发现线程上下文切换的成本可能高达一百万(!)个 CPU 周期[LiEtAl]– 这是一个该死的地段™。通常,为了降低这些成本,操作系统将时间片保持在 100-200 毫秒——在@1GHz 时,100 万个 CPU 周期对应于 1 毫秒,在 100 毫秒的时间片中,我们所说的由于线程上下文切换导致的性能下降 <1%。然而,这些估计只有在线程同步引起的上下文切换不引起他们丑陋的头脑时才成立:竞争激烈的互斥锁很容易导致每秒数万次线程上下文切换,在这种情况下,大多数CPU时间可以花在重新填充缓存上。在实践中,这意味着:

无共享架构将被强烈地优选的共享内存的,无论是在应用程序和内核级别。在适用的情况下,协作多任务(和 Run-to-Completion aka RTC)比抢占更可取。实际上,如果我们仅在上下文不再存在时才切换上下文,则无需重新填充任何内容(当然,当前缓存仍将被丢弃——但无需将它们重新放入缓存并产生相关的成本)。 3.4 NUMA 的出现。

在 1990 年代至 2000 年代,NUMA 系统被引入,并迅速证明了 NUMA 相对于基于 FSB 的纯 SMP 架构的优越性。
这意味着 Shared-Nothing 系统往往比 Shared-Memory 系统具有固有的性能优势——甚至在我们考虑同步成本之前。例外确实存在,但很少而且相距甚远。

3.5 中断改进。

自 60 年代以来,基于中断的 CPU 变得无处不在(而且中断的成本 - 非常低)。虽然 I/O 中断的概念是在 60 年代初引入的,但在受 Multics 启发的设计中并没有认真考虑它(正如上面的引述清楚地表明,Multics 哲学是允许“以…悠闲的方式处理主要信息”,将中断视为由其他(非 Multics)系统处理的烦恼)。自 Multics 时代以来,中断变得越来越高效(中断合并和 DMA 可能是最重要的事情)——但它们仍然感觉像是整个操作系统设计中的飞快的高山(举个例子,在[TanenbaumBos] 中断将在第 5 章讨论——在进程、线程、内存管理,甚至(鼓声!)文件系统之后)。

3.6 更长的管道和分支预测器,包括动态分支预测器。

虽然从古代程序员时代就知道原始管道,但从大约 80 年代开始管道开始变得越来越复杂;虽然在 60 年代典型的流水线需要 3 个阶段,但目前更像是 8-14 个阶段。这增加了分支预测错误的成本——这反过来又导致了复杂分支预测器的开发,包括动态分支预测器。

动态分支预测的含义之一是,当在两个不同的上下文中使用时,最好将同一行代码编译成两条不同的指令,以便动态分支预测器可以为这些不同的指令收集不同的统计信息。在支持大大增加的程序内存大小的情况下,这往往会很好地发挥作用。

4. 软件的变化

最后但并非最不重要的 - 让我们来看看软件方面的变化:

4.1 静态分析。

在过去的50年,静态分析工具改善了大幅-目前允许证明真实世界的程序的某些安全问题; 示例包括 Rust、D1179 [Sutter]和[memory-safe-cpp]。

这为基于语言的保护系统的复兴打开了大门(作为当前基于硬件的保护的替代品——这使得 MCU 变得可行,或者作为基于硬件的保护的补充——提高安全性)。

4.2 公共加密/PKI。

在 70 年代,发明了公共密码学——它有很多好处,包括能够在一个盒子上签署可执行文件并在任意数量的其他盒子上验证签名。

反过来,这允许仅对一个框执行基于语言的检查——同时将签名的可执行文件传送到任意数量的其他框。

4.3 Shared-Nothing 消息传递应用程序级编程的激增。

自 2010 年左右以来,越来越多的声音反对使用共享内存互斥体大规模并行范式编写应用程序,支持无共享方法。正如[GoBlog] 中所说的那样:“不要通过共享内存进行通信;相反,通过通信共享内存。”; 虽然这个想法(又名消息传递程序)并不新鲜(尤其是 Erlang 以使用它多年而闻名),但从 2010 年左右开始,它在应用程序级程序员中变得越来越流行,有库/框架来支持它从 Python 的 Twisted/asyncio,到 node.js 和 Akka Actors。我个人的估计是,在 10 年内,大多数应用程序将以异步无共享程序(事件驱动或更通用的消息传递)的形式开发。

4.4 支持异步编程。

与前一项密切相关,并且可能是上面列出的所有项目中的最新发展,编程语言对异步编程的支持得到了极大的改进;特别是,自 2015 年以来,await运算符已使其成为各种不同的编程语言(从 JS 和 Python 到 C# 和 C++)。

这为交互式程序提供了相当直观的线程无关编程;顺便说一句,在[NoBugs]中有人认为基于await的编程是在操作未完成时必须处理干预事件时编写有状态程序的最佳方式(超越多线程编程和以前的异步编程形式)。

5 待续

正如我们所看到的,自从构想当前现有操作系统的设计以来,已经发生了很多事情。我们在这方面的下一个问题将是“好吧,我们确实有一些机会,但我们还有其他需求吗?”;换句话说,我们需要弄清楚我们是否希望改进现代操作系统。请继续关注第二部分。

参考

http://ithare.com/bringing-architecture-of-operating-systems-to-xxi-century-part-i-changes-in-it-since-over-last-50-years/

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。