彻底理解操作系统:CPU与实模式
The following article is from 码农的荒岛求生 Author 陆小风
点击关注公众号,一周多次包邮送书
对于人类来说,我们不喜欢拐弯抹角,喜欢更直接的东西,“有话直说”、“没有中间商赚差价”、“简洁的设计”等等,然而对于计算机,尤其是对内存管理来说则恰恰相反,在这里"简洁"的设计往往不是好的设计,这到底是什么意思呢?我们在很早的文章中就提到过,内存从本质上将非常简单,你可以将其想像成一个个的小盒子组成,每个小盒子要么能存储1要么存储0,每8个小盒子组成一个字节(8比特),每个字节都有一个唯一的地址,通过这个地址我们就能从相应的一组小盒子取出这个比特。其它没了。看到了吧,内存本身其实是非常简单的,然而程序员以及程序使用内存的方式又让这个问题变得复杂起来,分析任何复杂问题都要抓住重点、抓住核心问题,那么这里的重点以及核心是什么呢?不卖关子,这里的核心在于两个字:寻址,Addressing。一切都是围绕寻址展开的。
寻址,最重要的就是寻址
什么是寻址 Addressing?所谓寻址就是找到内存中某个我们需要的数据的方式。哪怕以我们平时去储物柜取东西都有很多“寻址”方式:直接告诉我们一个编号,我们拿到这个编号后按个去找,就像下面这张图,我们需要找到东西在第15号储物柜中,那么我们根据15这个地址就能找到第15号储物柜。 当然我们也可以将储物柜划分区域,还是以刚才的储物柜为例我们可以划分为3个区域,当我们需要找东西时告诉我们其在储物柜的哪个区域,以及在该区域中的"偏移"是多少。
以下图为例我们需要的东西在第二个区域,区域内的偏移为6(该区域中的第6个储物柜)。
死板 vs 灵活
我们知道程序以及程序使用的数据编译好后存放在磁盘上,运行时要加载到内存中,因此这里同样存在寻址问题:我们需要根据内存地址找到机器指令以及数据,接下来假设有一个只有8字节大小的内存和一个只有2字节机器指令的程序(无需关心实际意义):绝对路径与相对路径
想一想绝对地址有什么问题?这个问题就好比你在程序中读取一个绝对地址时:/user/xiaofeng/doc/a.c
如果是你自己的计算机那么没有问题,但如果这个程序在其它人的计算机上运行就不一定了,因为其它人的计算机中不一定有这个路径,这时该怎么办呢?聪明的你一定知道,那就不要使用绝对路径,而是使用相对路径就可了:./a.c
很简单吧,这样不管这段程序被加载到了哪个内存区域,只要我们知道起始地址那么总能计算出真实的物理内存地址,重定位问题就可以这样解决。实际上你会发现,这个储物柜的第二种寻址方式也没有什么区别。物理地址 = 起始地址 + 相对地址
分段式内存管理
我们知道,程序的内存从内容上可以分为存放机器指令的代码区域、存放全局变量的数据区域、保存函数运行时信息的栈区等,显然我们可以将程序按照这种划分进行分段管理,段内使用相对地址,这样无论这些段被加载到内存的哪个区域我们都能方便的计算出正确的物理内存地址。保存机器指令的区域,这个区域就是我们所说的代码段(Code Segment),因此我们可以使用一个寄存器来专门指向代码段,这就是CS寄存器的作用,CS也是Code Segment的缩写。
同样的道理,程序运行起来后还有专门的区域用来保存数据,因此必须要专门的寄存器指向数据段(Data Segment),这就是DS寄存器的作用,DS是Data Segment的缩写。
程序运行起来后还有运行时栈(Stack Segment),因此可以使用SS寄存器来指向程序员运行时栈,SS是Stack Segment的缩写。
此外还有ES寄存器,Extra Segment,其用作临时段寄存器。
没有内存保护会怎样?
至今,在多线程编程中这个问题依然困扰着程序员,因为同一个进程中的线程共享同一个地址空间,这也就意味着你的线程可以修改地址空间中任何可写的区域,包括栈区以及堆区,当然这也就意味着其它线程可以修改你的线程使用的数据,这是多线程中一大类bug的来源,关于这一部分的内容你可以参考《线程间到底共享了哪些进程资源?》。而这个问题在内存地址没有任何保护情况下更加严重,因为这时不是一个进程而是多有进程包括操作系统都共享同一个物理内存地址,任何一个进程都可以修改内存中任何位置,你的进程可以破坏其他进程使用的内存,可以破坏操作系统使用的内存,破坏其它进程大不了重新启动这个进程,但是如果破坏了操作系统那么没有办法,此时你只能重新启动计算机,如果CPU没有提供内存保护机制,那么操作系统连自己都保护不了更何况去保护其它进程。没想到吧,看似简单直接的内存读写竟然会有这么多问题。实模式
好啦,到目前为止让我们暂且总结一下。绝对的内存地址不好用,这样的地址必须将程序加载到内存的特定位置上,为解决这个问题使用相对地址,x86中为每个程序的区域都配备有专用的寄存器用来存放该段在内存中的起始地址,这样就可以根据基址加偏移计算出物理内存地址,注意,这里计算出来的是真实的物理内存地址。 内存读写没有任何保护,程序可以读写内存的任何区域。
此时给定一个段寄存器再给出一个偏移我们就能直接在内存中找到需要的数据:16 ∗ selector + offset
寻址空间有限,只有1MB 利用 selector:offset的方式利用两个16位寄存器来寻址1MB内存 没有内存保护机制,当然,没有内存保护机制的一大优点就在于内存读写速度要更快,原因就在于不需要经过虚拟内存地址到物理内存地址的转换,也不需要进行任何检查(这可能是实模式下仅有的优点)
实模式与操作系统
实模式是x86系列处理器最早期的内存管理模式,这一时期的操作系统别无选择,只能运行在这种模式下,早期的DOS系统以及早期的Microsoft Windows操作系统就运行在实模式下。虽然实模式理解起来很简单,但这种模式最主要的问题在于:把物理内存暴露给程序
没有内存保护机制
总结
实模式是一种非常古老的内存管理方式,在这种方法下程序员直面物理内存,且处理器没有提供内存读写机制,程序员可读写任何内存区域。实际上实模式对于现代操作系统来几乎没什么用处,只不过如果你针对x86 CPU编写操作系统那么实模式是必须要了解的,但对于其它CPU来说则没有这样的历史包袱,因此有很多操作系统教材开始基于非X86平台来讲解,这样能更快速的讲解操作系统而不是在一上来就在各种内存模式中打转。注意,本文提到的实模式仅仅针对x86系列处理器而言,对于上层应用的大部分程序员来说根本就不需要关心实模式,然而技术就和生物一样也在不断演变进化,了解过去才能更好的理解当下以及未来。推荐阅读
• 被diss性能差,Dan连夜优化React新文档• 这一定是你经常用,但是不知道名字的设计模式• 助你成为 CSS 大师的18个 GitHub 仓库• 炸锅了,Java多线程批量操作,居然有人不做事务控制?• 我又发现了超赞的软硬件项目,全部开源• React如何原生实现防抖?• 竟然有一半的人不知道 for 与 foreach 的区别???
👇更多内容请点击👇