0%

逻辑地址,线性地址,物理地址

。。。

首先,我们有1块物理内存,比如4G的DDR3,如果CPU想要找这个内存里边的一块区域存储的东西(0、1),需要知道这块区域在哪里,最简单的就是把物理内存的一个个存储单元都编号,比如位置在最开头的就编号为0x00,接下来一个单元就编号0x01,…,这里每个单元都是1位,也就是只能存1个0或1个1,就像这样:

但是这样每一位都进行编号,整个地址编号就太多了,如果我们约定每8位编一个号,那整个地址就是原先的1/8,就像这样:

为了方便,我们把每8位的一个单元叫做1个字节,0x00指向的就是物理内存中的第1个存储单元,也就是第1个字节,字节是物理内存的最小存储单元。如果想拿到第1个字节第3位中的数字1(标红的位置),那就需要把整个字节都先拿到,再去找里边的数字。

这里给物理内存编的1个号码就是1个物理地址,所有给物理内存编的号码就叫物理地址的地址空间。

CPU如果想拿到某个物理内存中的数字,需要发送一组电信号给物理内存芯片,芯片根据这组电信号查找相应地址,然后返回给CPU,这组电信号编码成数字就是物理地址了,就像这样:

其中CPU到内存控制芯片的这几根线叫地址线,比如常说的某个架构有20根地址线或者32根、36根地址线,就表明CPU可以寻址到的地址空间,比如32根地址线就可以最高寻址到2^32,也就是4G。我们这里画了4根,就可以寻址到2^4,也即是16个字节。

我们需要编写程序控制CPU进行逻辑处理、计算、存储等动作,当需要读写内存中的数据的时候,最简单的当然是用物理地址去指示CPU问内存控制芯片要哪个地址的数据了,比如:

我们想要把0x00中的数据放到寄存器ax中,就把0x00这个地址给CPU,这就可以了,多简单。当然如果真的这么简单,大家都轻松了🙃。事实上,CPU为了能处理多任务,同时在一个任务中区分代码、数据、栈,就需要对内存进行划分,分别存放不同的内容,这样一是为了区分,二是为了保护(比如代码段不允许修改,某些数据只读)。先来想象一下可能的解决办法,我们的目的是想把一段处理逻辑控制在内存0x00~0x07这个段之间,让CPU在运行的时候不能超出这个内存范围去获取代码或数据,超过就要报错。一个简单的方法是设置一个标志位,该标志位表明指令中的地址都需要在0x00~0x07之间,不能超过。在指示0x080x15之间的内存的时候,再设置另一个标志位,起到同样的作用。这样就可以将整个内存分为2个段:0x00\0x07,0x08~0x15。每个段的长度是8个字节。每个段都设置一个标志位的方法有点儿笨,我们用每个段的首地址来表示这个段:0x00表示第1个段,0x08表示第2个段,再用跟段的首地址的偏移量来确认段内某个字节的地址。现在我们确定第1个段中第2个字节的地址就是0x00:1(第1个字节的地址是0)。如下图:

现在我们指令中用到的0x00:1就是逻辑地址,因为它在逻辑上将内存划分为了多个不同的段。我们要做的只是输入0x00:1这个地址,CPU会帮我们把逻辑地址翻译成0x01的物理地址🙂。

除了使用分段的方式划分内存外,还可以将物理内存直接划分为多个不同的页,称为页帧,一个物理页一般是4K大小,这样控制一页内的内存属性相同,多个不同的页可以使用不同的属性,就跟分段类似了,如下图:

这时候会出现个新问题:CPU已经使用逻辑地址来划分段了,如何去实现分页呢?其实肯定也想到了,在逻辑地址和物理地址之间加一个抽象层接口,也就是我们要说的最后一个地址:线性地址,也叫虚拟地址。线性地址的位置在逻辑地址和物理地址之间,比如在讲linux内核的书里边常见有这样的图:

CPU将逻辑地址按照分段逻辑转换成线性地址,再按分页逻辑转换成物理地址。这里的转换都是依靠CPU电路完成的,但是怎样分段,怎样分页是需要内核程序先规定好才行。CPU怎样进行分段和分页的就不细说了,这里讲的真好,可以去看看。
我们来对比一下分段和分页:

分段 分页
目的 将内存按照不同的属性分隔,实现隔离和保护 将内存按照不同的属性分隔,实现隔离和保护
对象 对线性地址进行分隔 对物理页帧进行分隔
方法 使用逻辑地址(段基址:偏移)分隔不同的段 将线性地址分隔成不同的部分,一步步去寻找页对应的物理页帧,进而找到对应的物理地址
程序需要事先做好的准备 准备好GDT,LDT,IDT,也就是个个不同的段 准备好页目录,页表,也就是各个不同的页
大小 段大小可变,程序控制 页大小固定的几个值,4K,4M,2M,CPU规定好的
好了,现在我们的CPU中地址关系如下:
看上去很复杂吧,其实理解了就没那么多东西,我画画的水平有限😅。

有个问题很多人都想知道,我一开始看内存这部分的时候也很着急:linux用户程序里边用的是什么地址?我先说答案:是逻辑地址。但很多书里边讲linux虚拟内存的分布,跟程序里用到的地址是一模一样的啊?其实linux用户程序中的地址是逻辑地址中的偏移,只不过linux把所有段的基址都设为0,这样段内偏移就跟线性地址一样了(因为段基址都是0啊,段偏移加上0还是段偏移)。为什么linux要把所有段基址都设为0呢?其实分段和分页都是对内存的分隔和保护,从用途上来说是重复的,分页更简单些,所有linux就只用了分页来实现内存保护。

还有另一个问题:linux中用户进程看到的内存都是一样的,0~4G,这是如何实现的?答案是基于分页,linux给每个进程都维护一个分页映射规则,每个分页规则对应一种线性地址到物理地址的映射,这样每个进程看到的都是同样的线性地址,但是后边对应的物理地址各不相同。

内存管理是linux内核中最复杂的系统之一,除了逻辑地址到线性地址再到物理地址的映射外,还有很多其他的特性辅助操作系统对物理内存实现管理,比如分段逻辑实现时用到的GDT、LDT、IDT、TSS、6个段寄存器,分页逻辑实现时用到的页目录、页表、页帧、CR3、PAE,CPU和物理内存之间的高速缓存等。骨架已经理清楚了,其他的理解就慢慢往里加吧。


参考:

  1. http://ilinuxkernel.com/?p=1276,讲linux内核的大牛,思路清晰,文笔优美。
  2. 《Linux内核设计与实现》Robert Love,极好的内核入门书。
  3. 《深入Linux内核架构》(德)Wolfgang.Mauerer,深入细节1。
  4. 《Understanding the Linux Kernel 》3rd_Edition,Daniel P. Bovet and Marco Cesati,深入细节2。