欢迎各位兄弟 发布技术文章

这里的技术是共享的

You are here

LINUX 逻辑地址、线性地址、虚拟地址和物理地址 有大用

1、概念解释

物理地址:
  用于内存芯片级的单元寻址,与地址总线相对应。这个概念应该是这几个概念中最好理解的一个,但是值得一提的是,虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽像。

虚拟内存:
  这是对整个内存(不要与机器上插那条对上号)的抽像描述。它是相对于物理内存来讲的,可以直接理解成“不直实的”,“假的”内存,例如,一个0x08000000内存地址,它并不对就物理地址上那个大数组中0x08000000 - 1那个地址元素;有了这样的抽像,一个程序,就可以使用比真实物理地址大得多的地址空间。(拆东墙,补西墙,银行也是这样子做的),甚至多个进程可以使用相同的地址。不奇怪,因为转换后的物理地址并非相同的。
  ——可以把连接后的程序反编译看一下,发现连接器已经为程序分配了一个地址,例如,要调用某个函数A,代码不是call A,而是call 0x0811111111 ,也就是说,函数A的地址已经被定下来了。没有这样的“转换”,没有虚拟地址的概念,这样做是根本行不通的。
打住了,这个问题再说下去,就收不住了。

逻辑地址:
  可以认为是cpu执行程序过程中的一种中间地址。Intel为了兼容,将远古时代的段式内存的管理方式保留了下来,至于为什么会产生段式内存的管理方式,参见[2]。一个逻辑地址,是由一个段标识符加上一个指定段内的相对地址的偏移量,表示为[段标识符:段内偏移量],也就是说上面例子中的那个0x08111111应该表示为 [A的代码的段标识符:0x08111111] 这样才完整一些。

线性地址:
  线性地址,也即虚拟地址,如果逻辑地址对应的是硬件平台段式管理转换前的地址的话,那么线性地址则对应了硬件页式内存的转换前的地址。

2、初步理解

对上面的各种地址的阶段性总结如下:
  CPU将一个虚拟地址空间的地址转换为物理地址,需要进行两步:首先将给定的逻辑地址,即[段标识符:段内偏移量]这样的形式,利用段式管理单元,转化为线性地址,然后利用页式内存管理单元,转化为最终的物理地址。图形表示如下(下图中的左半部分):
这里写图片描述

3、深层次的理解

更进一步的对于段式内存管理和页式内存管理的解释如下:
 - 段标识符也即段选择符,它用来从段描述符表中选择一个具体的段,某个段描述符表项的base字段描述了一个段的开始位置的线性地址。
 - gdt是全局段描述符表,
 - ldt是局部段描述符表,
 - 段选择符中的TI = 0表示用GDT,TI=1表示用LDT
 - gdt在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而ldt则在ldtr寄存器中。

3.1、段式管理单元

有了上面的这些概念,对于上图的右半部分的段式管理单元就好理解了:
  程序过来一个逻辑地址,使用其段标识符(也即段选择符)的Index字段去索引段描述符表,若TI=0,索引全局段描述符表,TI=1,索引局部段描述符表,表的地址在相应的寄存器中。通过Index字段和段描述符表的位置能找到某项具体的段描述符。将段描述符中的base字段和逻辑地址中的offset字段合并即得到了线性地址。
  按照Intel的本意,全局的用GDT,每个进程自己的用LDT——不过Linux则对所有的进程都使用了相同的段来对指令和数据寻址。即用户数据段,用户代码段,对应的,内核中的是内核数据段和内核代码段。[1]中有介绍,四个段的基地址全为0。这样,给定一个段内偏移地址,按照前面转换公式,0 + 段内偏移,转换为线性地址,可以得出重要的结论,“在Linux下,逻辑地址与线性地址总是一致(是一致,不是有些人说的相同)的,即逻辑地址的偏移量字段的值与线性地址的值总是相同的。!!!”所以如果做linux下内核开发,对于上述的x86的段式管理可以完全不用理会,我们可以认为linux根本没有用intel弄出来的这个段式管理,而是以页式管理完成了所有的内存管理工作。

3.2、页式管理单元:

  CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。从管理和效率的角度出发,线性地址被分为以固定长度为单位的组,称为页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page[2^20]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址——对应的页的地址。
  另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。
  这里注意到,这个total_page数组有2^20个成员,每个成员是一个地址(32位机,一个地址也就是4字节),那么要单单要表示这么一个数组,就要占去4MB的内存空间。为了节省空间,引入了一个二级管理模式的机器来组织分页单元。如上图中的页式管理单元部分,我们单独拿出来看:
这里写图片描述
1、分页单元中,页目录是唯一的,它的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。万里长征就从此长始了。
2、每一个活动的进程,因为都有其独立的对应的虚似内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录地址放到cr3寄存器中,将别个的保存下来。
3、每一个32位的线性地址被划分为三部份,面目录索引(10位):页表索引(10位):偏移(12位)
4、依据以下步骤进行转换:
 ① 从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
 ② 根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
 ③ 根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
 ④ 将页的起始地址与线性地址中最后12位相加,得到最终我们想要的葫芦;

参考:
[1] LINUX 逻辑地址、线性地址、物理地址和虚拟地址
 (原文地址:http://bbs.chinaunix.net/thread-2083672-1-1.html,并且评论的第4页有错误修正)
[2] 为什么会有段式内存管理机制

疑问解答:
1、我理解的所谓的内存映射,不过是将虚拟地址空间 和 可执行文件建立映射关系,这种映射关系的建立,可能是做一张表,来记录虚拟地址空间中的地址和可执行文件在磁盘上的地址的对应关系。这样当程序执行第一条指令时,会发生缺页中断,根据之前建立的映射关系从磁盘中拿到需要的东西搬移到物理内存中,并且将物理内存的地址和此时的第一条指令的虚拟地址这一对映射关系写到页表中。
  该异常是虚拟内存机制赖以存在的基本保证——它会告诉内核去真正为进程分配物理页,并建立对应的页表,这之后虚拟地址才实实在在地映射到了系统的物理内存上。——《Linux内存管理(最透彻的一篇)
2、程序的内存映像从低地址到高地址依次是:
· txt段
· data段: 已初始化的全局变量和已初始化的static变量;
· bss段: 未初始化的全局变量和未初始化的static变量以及初始化为零的全局变量和静态变量(参考这里这里)(未初始化的全局变量和static变量,系统自动赋值为零。这个段在编译成 .exe可执行文件时,只是标记了一下这个段的大小,并没有实际的分配全为零的页框。
  例如:一个程序的txt段的大小是8kB,初始化数据段的大小是8kB,未初始化的数据段(BSS)的大小是4kB,那么可执行文件的大小是 16kB(代码+初始化的数据)加上一个很短的头部来告诉系统在初始化的数据后再另外分配4KB,同时在程序启动之后把他们初始化零为0。这个技巧巧妙的避免了在可执行文件中中存储4kB的0.
  更进一步,为了避免分配一个全是0的物理页框,在初始化的时候,linux就分配了一个静态零页面,即一个全为零的写保护页面。当加载程序的时候,未初始化的数据区域被设置为指向该零页面。当一个进程真正要写这个区域的时候,写时复制机制就开始起作用,一个实际的页框就被分配给该进程。——《现代操作系统P428》
· 堆: 通常情况下堆也是请求二进制零的页面** ,往上生长——《深入理解计算机系统P585, P587》
· 栈: 通常情况下往下生长
· 共享库的内存映射区域:在用户堆和栈之间存在一个共享库的内存映射区域,比如标准C库的libc.so,这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。

  不管怎么样,在进程切换的时候,要将进程的 页表基地址 装入到 页表基址寄存器中,这个页表基址寄存器在X86架构中是cr3寄存器,在arm架构中是TTB寄存器,详细见:ARM协处理器CP15
  并且,为了在切换进程时,不完全的刷新TLB,还需要将进程id记录到TLB中,只有这样才能知道TLB中当前的表项是否是是当前进程的(因为此时TLB的寻址是使用的虚拟寻址,两个不同的进程可能使用同一个虚拟地址来寻址),在X86架构中实现这种功能的一个结构叫做PCID(进程上下文标识符),在ARM结构中叫做ASID,详见:什么是TLB和PCID?为什么要有PCID?为什么Linux现在才开始使用它?swapper进程修改页表项 vs kvm中guest页表的写保护

来自 https://blog.csdn.net/baidu_35679960/article/details/80463445


普通分类: