干货:软件运行机制及OS内存管理
内容选自:《许式伟的架构课》
更多极客时间:架构师技术专栏内容请参看:
操作系统是怎么获得执行权的?这是计算机主板 ROM 上的启动程序(BIOS)交给它的。
计算机加电启动后,中央处理器(CPU)会从一个固定的存储地址加载指令序列执行。通常,这个固定的存储地址指向计算机主板的 ROM 上的一段启动程序(BIOS)。这段启动程序通常包含以下这些内容。
引导区的引导程序有长度限制(关于这一点我在上一节已经介绍过),只能做非常少的事情。在常规情况下,它只是简单地跳转到真正的操作系统的启动程序,但有时计算机上安装了多个操作系统,此时引导程序会提供菜单让你选择要运行的操作系统。
这样,操作系统就开始干活了。
操作系统的需求演进
那么,操作系统是做什么的?前面我们说的“软件治理”是否可以涵盖它完整的目标?让我们从操作系统的发展历程说起。
最早期的计算机是大型机。这个时期的计算机笨重、昂贵,并且操作困难,主要使用人群是搞科研性质的科学家或其他高端人群。
虽然这个时期催生了 IBM 这样的硬件巨头,但大多数人根本就意识不到,这玩意儿对后世人们的生活能够产生如此翻天覆地的变化。
这个时期的计算机还是单任务的,以计算为主,软件为操作硬件服务。如果我们认为“软件治理”是操作系统的根源需求的话,那么可以认为这个时期还不存在操作系统。但的确会有一些辅助工具库来简化用户使用计算机的负担,我们可以把它看做操作系统的萌芽。
从这个意义来说,提供计算机的“基础编程接口”,降低软件开发的负担,是操作系统更为原始的需求。
此后,小型机和个人计算机(PC)的崛起,分别诞生了 UNIX 和 DOS 这两个影响深远的操作系统。UNIX 就不用说了,它几乎算得上今天所有现代操作系统的鼻祖。
DOS 的历史非常有趣。首先是 IBM 没把操作系统当回事儿,把这个活儿包给了微软。然后是微软只花了 5 万美元向西雅图公司购买了 86-DOS 操作系统的版权,更名为 MS-DOS。
那么 86-DOS 是怎么来的?西雅图公司的一个 24 岁小伙叫蒂姆·帕特森(Tim Paterson),单枪匹马花了 4 个月时间写出来的。
可以看到,这个时期人们对操作系统并没有太深刻的认知,多数人只把它看做硬件的附属品。IBM 不把它当回事,西雅图公司也没把它当回事,几万就把它卖了。只有微软认认真真地把它当做生意做了起来(在此之前微软的生意是卖 BASIC 语言的解析器起家,所以微软一直对 BASIC 语言情有独钟,直到很久以后微软搞出了 C# 语言后,情况才有所改变)。
等到 IBM 意识到操作系统是个金蛋,改由自己做 PC-DOS 操作系统的时候,微软已经通过推动 PC 兼容机的发展,让操作系统不再依赖特定的硬件设备,微软也就因此脱离 IBM 的臂膀,自己一飞冲天了。回到问题。要回答操作系统在做什么,我们可以从客户价值和商业价值两个维度来看。
客户价值来说,操作系统首先要解决的是软件治理的问题,大体可分为以下六个子系统:进程管理、存储管理、输入设备管理、输出设备管理、网络管理、安全管理等。
操作系统其次解决的是基础编程接口问题。这些编程接口一方面简化了软件开发,另一方面提供了多软件共同运行的环境,实现了软件治理。
商业价值来说,操作系统是基础的刚需软件。计算机离开了操作系统就是一堆废铜烂铁。随着个人计算机采购需求的急速增加,光靠软件 License 的费用就让操作系统厂商赚翻了。
虽然第一个广为人知的操作系统是 UNIX,但从商业上来说最成功的操作系统则是 DOS/Windows,成就了微软的霸主地位。
为什么是 DOS/Windows 赢得了市场,这无关技术优劣,关键在于两者的商业路线差异:UNIX 走的是企业市场,而 DOS/Windows 选择了更为巨大的市场:个人计算机(PC)市场。
操作系统也是核心的流量入口。占领了操作系统,就占有了用户,想推什么内容给用户都很容易。微软对这一点显然心知肚明。
这也是为什么当年网景推 Netscape 浏览器的时候,微软很紧张。因为浏览器是另一个软件治理的入口,本质上是操作系统之上的操作系统。如果软件都运行在浏览器上,那么本地操作系统就沦为和硬件一般无二的管道了。
虽然早期操作系统没有应用市场(AppStore),但是通过操作系统预装软件的方式向软件厂商收租,这是一直以来都有的盈利方式。国内盗版的番茄花园 Windows 发行版就是通过在 Windows 系统上预装软件来盈利。
当然预装软件只是一种可能性,流量变现的方式还有很多。苹果的 iOS 操作系统开启了新的玩法,它构建了新的商业闭环:账号(Account)、支付(Pay)、应用市场(AppStore)。
我们把这个商业模式叫收税模式。帐号(注意是互联网账号,不是过去用于权限管理的本地账号)是前提。没有帐号,就没有支付系统,也没有办法判断用户是否购买过某个软件。
应用市场实现了应用的分发,既解决了系统能力的无限扩展问题(客户价值),也解决了预装软件的软件个数总归有限的问题(商业价值)。支付则是收税模式的承载体,无论是下载应用收费,还是应用内购买内容收费,都可以通过这个关卡去收税。
无论是本地操作系统 iOS 和 Android,还是 Web 操作系统(浏览器)如微信小程序,都实现了“帐号 - 支付 - 应用市场”这样的商业闭环。这类操作系统,我们不妨把它叫做现代操作系统。
操作系统的边界在哪里?
架构的第一步是需求分析。上一节我提到了在架构设计过程中,需求分析至少应该花费三分之一的精力。通过这一节我们对操作系统演进过程的回顾,你可能更容易体会到这一点。
当我们说要做一个操作系统的时候,实际上我们自己对这句话的理解也是非常模糊的。尤其是我们正准备去做的事情是一个新生事物时,我们对其理解往往更加粗浅。
在本专栏开篇词中我也提过,架构也关乎用户需求,作为架构师我们不只是要知道当前的用户需求是什么,我们还要预测需求未来可能的变化,预判什么会发生,而什么一定不会发生。
我们可以问一下自己:我是否能够预料到,有一天支付(Pay)系统会成为操作系统的核心子系统?如果不能,那么怎么才能做到?
操作系统的边界到底在哪里?要回答这个问题,我们需要看清楚硬件、操作系统和浏览器三个角色的关系:
首先我们来看操作系统与硬件的关系。如果操作系统厂商不做硬件会怎样?我们知道个人计算机(PC)市场就是如此。微软虽然占据了 PC 操作系统(DOS/Windows)绝大部分江山,但是它自身并不生产硬件。这里面,PC 兼容机的发展对 DOS/Windows 的发展有着至关重要的支撑意义。它让操作系统厂商有了独立的生存空间。
到了移动时代,Google 收购 Android 后,通过免费策略占领移动操作系统的大半江山,一定程度上复制了微软的过程,但实际上并没有那么理想。
首先,Android 是免费的,Google 并没有从中收取软件 License 费用,而是借助 Android 的市场占有率来推动 Google 的服务(例如搜索、Gmail 等等),通过 Google 服务来获取商业回报。
其次,iOS 操作系统引入的 “账号 - 支付 - 应用市场” 的收税模式,受益方是硬件(手机)厂商,而非操作系统厂商。其中最关键的一点,几乎所有手机厂商都不接受把支付(Pay)这个核心系统交给 Google。
最后,不止支付系统,一旦手机厂商长大立足 ,Google 服务也会被逐步替换。所以 Google 和 Android 手机厂商之间的联盟并不可靠,养肥的手机厂商会不断试探 Google 的底线,而 Google 也会尝试去收紧政策,双方在博弈中达到平衡。
之所以会这样,我觉得原因有这么几个:
其一,历史是不可复制的,人们对操作系统的重要性认知已经非常充分。所以大部分手机厂商,都不会放弃操作系统的核心子系统的主控权。Android 系统的开源策略无法完全达到预期的目标,这也是 Google 最终还是免不了要自己做手机的原因。
其二,手机是个性化产品,硬件上并没有 PC 那么标准化。所以个人计算机有兼容机,而手机并没有所谓的标准化硬件。
分析完操作系统和硬件的关系,我们再来看它和浏览器的关系。在 PC 时期,操作系统和浏览器看起来至少需求上是有差异化的:操作系统,是以管理本地软件和内容为主(对内)。浏览器,是以管理互联网内容为主(对外)。但,这个边界必然会越来越模糊。
从商业价值来说,操作系统是刚性需求,核心的流量入口,兵家必争之地。所以,围绕它的核心能力,操作系统必然会不断演化出新的形态。
操作系统的边界到底在什么地方?我们通过分析硬件、操作系统、浏览器三者的关系,也做了定性的分析。这样的分析将有助于你对需求发展做出预判。
操作系统的核心职能是软件治理,而软件治理的一个很重要的部分,就是让多个软件可以共同合理使用计算机的资源,不至于出现争抢的局面。内存作为计算机最基础的硬件资源,有着非常特殊的位置。我们知道,CPU 可以直接访问的存储资源非常少,只有:寄存器、内存(RAM)、主板上的 ROM。寄存器的访问速度非常非常快,但是数量很少,大部分程序员不直接打交道,而是由编程语言的编译器根据需要自动选择寄存器来优化程序的运行性能。
主板上的 ROM 是非易失的只读的存储。所谓非易失,是计算机重新启动后它里面的数据仍然会存在。这不像内存(RAM),计算机重新启动后它上面的数据就丢失了。ROM 非易失和只读的特点,决定了它非常适合存储计算机的启动程序(BIOS)。所以你可以看到,内存的地位非常特殊,它是唯一的 CPU 内置支持,且和程序员直接会打交道的基础资源。
从图中可以看出,存储的作用有两个:一个是作为 “计算” 的操作对象,输入和输出数据存放的所在;另一个是存放 “计算” 本身,也就是程序员写的程序。这里说的存储,主要指的就是内存。
当然,这是从 CPU 角度看到的视图:对于 CPU 来说,“计算” 过程从计算机加电启动,执行 BIOS 程序的第一条指令开始,到最后计算机关机,整个就是一个完整的 “计算” 过程。这个过程有多少个“子的 ‘计算’过程”,它并不关心。但是从操作系统的视角来看,计算机从开机到关机,整个 “计算” 过程,由很多软件,也就是子 “计算” 过程,共同完成。从时序来说,计算机完整的 “计算” 过程如下:
整个 “计算” 过程的每个子过程都有其明确的考量。
首先,BIOS 程序没有固化在 CPU 中,而是独立放到主板的 ROM 上,是因为不同历史时期的计算机输入输出设备很不一样,有键盘 + 鼠标 + 显示器的,有触摸屏的,也有纯语音交互的,外置存储则有软盘,硬盘,闪存,这些变化我们通过调整 BIOS 程序就可以应对,而不需要修改 CPU。
引导区引导程序,则是程序从内置存储(ROM)转到外置存储的边界。引导区引导程序很短,BIOS 只需要把它加载到内存执行就可以,但是这样系统的控制权就很巧妙地转到外置存储了。
引导区引导程序不固化在 BIOS 中,而是写在外置存储的引导区,是为了避免 BIOS 程序需要经常性修改。毕竟 BIOS 还是硬件,而引导区引导程序已经属于软件范畴了,修改起来会方便很多。
OS 引导程序,则是外置存储接手计算机控制权的真正开始。这里 OS 是操作系统(Operating System)的缩写。操作系统从这里开始干活了。这个过程发生了很多很多事情,这里我们先略过。但是最终所有的初始化工作完成后,操作系统会把执行权交给 OSShell 程序。
OS Shell 程序负责操作系统与用户的交互。最早的时候,计算机的交互界面是字符界面,OS Shell 程序是一个命令行程序。DOS 中叫 command.com,而在 Linux 下则叫 sh 或者bash 之类。这里的 sh 就是 shell 的缩写。
这个时期启动一个软件的方式就是在 Shell 程序中输入一个命令行,Shell 负责解释命令行理解用户的意图,然后启动相应的软件。到了图形界面时期,在 Shell 中启动软件就变成点点鼠标,或者动动手指(触摸屏)就行了,交互范式简化了很多。
在了解了计算机从开机到关机的整个过程后,你可能很快会发现,这里面有一个很关键的细节没有交代:计算机是如何运行外置存储上的软件的?
这和内存管理有关。结合内存的作用,我们谈内存管理,只需要谈清楚两个问题: 如何分配内存(给运行中的软件,避免它们发生资源争抢); 如何运行外置存储(比如硬盘)上的软件?
在回答这两个问题之前,我们先了解一个背景知识:CPU 的实模式和保护模式。这两个模式 CPU 对内存的操作方式完全不同。在实模式下,CPU 直接通过物理地址访问内存。在保护模式下,CPU 通过一个地址映射表把虚拟的内存地址转为物理的内存地址,然后再去读取数据。相应的,工作在实模式下的操作系统,我们叫实模式操作系统;工作在保护模式下的操作系统,我们叫保护模式操作系统。
先看实模式操作系统。在实模式操作系统下,所有软件包括操作系统本身,都在同一个物理地址空间下。在 CPU看来,它们是同一个程序。操作系统如何分配内存?至少有两种可行的方法。
其一,把操作系统内存管理相关的函数地址,放到一个大家公认的地方(比如 0x10000处),每个软件要想申请内存就到这个地方取得内存管理函数并调用它。
其二,把内存管理功能设计为一个中断请求。所谓中断,是 CPU 响应硬件设备事件的一个机制。当某个输入输出设备发生了一件需要 CPU 来处理的事情,它就会触发一个中断。
但实模式有两个问题。其一是安全性。操作系统以及所有软件都运行在一起,相互之间可以随意修改对方的数据甚至程序指令,这样搞破坏就非常容易。其二是支持的软件复杂性低,同时可运行的软件数量少。
一方面,软件越复杂,它的程序代码量就越多,需要的存储空间越大,甚至可能出现单个软件的大小超过计算机的可用内存,这时在实模式下就没法执行它。另一方面,哪怕单个软件可运行,但是一旦我们同时运行的软件多几个,操作系统对内存的需求量就会急剧增加。相比这么多软件加起来的内存需求量,内存的存储空间往往仍然是不足的。
但是为什么平常我们可以毫无顾忌地不断打开新的软件,从来不曾担心过内存会不足呢?这就是保护模式的作用了。保护模式下,内存访问不再是直接通过物理内存,而是基于虚拟内存。虚拟内存模式下,整个内存空间被分成很多个连续的内存页。每个内存页大小是固定的,比如 64K。这样,每次 CPU 访问某个虚拟内存地址中的数据,它都会先计算出这是要访问哪个内存页,然后 CPU 再通过一个地址映射表,把虚拟的内存地址转为物理的内存地址,然后到这个物理内存地址去读取数据。地址映射表是一个数组,下标是内存页页号,值是该内存页对应的物理内存首地址。
当然,也有可能某一个内存页对应的物理内存地址还不存在,这种情况叫缺页,没法读取数据,这时 CPU 就会发起一个缺页的中断请求。
这个缺页的中断请求会被操作系统接管。发生缺页时,操作系统会为这个内存页分配物理的内存,并恢复这个内存页的数据。如果没有空闲的物理内存可以分配,它就会选择一个最久没有被访问的内存页进行淘汰。
当然,淘汰前会把这个内存页的数据保存起来,因为下次 CPU 访问这个被淘汰的内存页时一样会发生缺页中断请求,那时操作系统还要去恢复数据。通过这个虚拟内存的机制,操作系统并不需要一上来就把整个软件装进内存中,而是通过缺页中断按需加载对应的程序代码片段。多个软件同时运行的问题也解决了,内存不够用的时候,就把最久没有用过的内存页淘汰掉,腾出物理内存出来。
运行软件的问题解决了。那么,操作系统如何分配内存给运行中的软件?其实,内存分配的问题也解决了,并不需要任何额外的机制。反正内存地址空间是虚拟的,操作系统可以一上来就给要运行的软件分配超级大的内存,你想怎么用随你。软件如果不用某个内存页,什么都不发生。软件一旦用了某个内存页,通过缺页中断,操作系统就分配真正的物理内存给它。
通过引入虚拟内存及其缺页机制,CPU 很好地解决了操作系统和软件的配合关系。每个运行中的软件,我们把它叫进程,都有自己的地址映射表。也就是说,虚拟地址并不是全局的,而是每个进程有一个自己独立的虚拟地址空间。
在保护模式下,计算机的基础架构体系和操作系统共同在努力做的一件事情,就是让每个软件“感觉”自己在独占整个计算机的资源。独立的虚拟地址空间很好地伪装了这一点:看起来我独自在享用所有内存资源。在实模式下的浮动地址的问题也解决了,软件可以假设自己代码加载的绝对地址是什么,不需要在加载的时候重新调整 CPU 指令操作的地址。
这和实模式很不一样。在实模式下,所有进程都在同在物理内存的地址空间里,它们相互可以访问对方的数据,修改甚至破坏对方的数据,进而导致其他进程(包括操作系统本身的进程)崩溃。内存是进程运行的基础资源,保持进程基础资源的独立性,是软件治理的最基础的要求。这也是保护模式之所以叫“保护”模式的原因。
虚拟内存它本质上要解决这样两个很核心的需求。其一,软件越来越大,我们需要考虑在外置存储上执行指令,而不是完整加载到内存中。但是外置存储一方面它的数据 CPU 并不知道怎么读;另一方面就算知道怎么读,也不知道它的数据格式是什么样的,这依赖文件系统的设计。让 CPU 理解外置存储的实现细节?这并不是一个好的设计。
其二,要同时运行的软件越来越多,计算机内存的供给与软件运行的内存需求相比,捉襟见肘。怎么才能把有限的内存的使用效率最大化?一个很容易想到的思路是把不经常使用的内存数据交换到外置存储。但是问题仍然是,CPU 并不了解外置存储的实现细节,怎么才能把内存按需交换出去?
通过把虚拟内存地址分页,引入缺页中断,我们非常巧妙地解决了这个问题。缺页中断很像是 CPU 留给操作系统的回调函数,通过它对变化点实现了很好的开放性设计。
总结一下。我们今天先概要地阐述了计算机运行的全过程,并对每一步的核心意义做了简单的介绍。然后我们把话题转到我们这一节的重心:内存管理。谈内存管理,需要谈清楚两个核心问题:
如何分配内存(给运行中的软件,避免它们发生资源争抢);如何运行外置存储(比如硬盘)上的软件。我们分别就在实模式下和保护模式下的内存管理进行了讨论。
架构师专栏:
转载申明:转载本号文章请注明作者和来源,本号发布文章若存在版权等问题,请留言联系处理,谢谢。
推荐阅读
更多架构相关技术知识总结请参考“架构师全店铺技术资料打包”相关电子书(37本技术资料打包汇总详情可通过“阅读原文”获取)。
全店内容持续更新,现下单“全店铺技术资料打包(全)”,后续可享全店内容更新“免费”赠阅,价格仅收198元(原总价350元)。
温馨提示:
扫描二维码关注公众号,点击阅读原文链接获取“架构师技术全店资料打包汇总(全)”电子书资料详情。