linux内存管理图解 -- 线性地址转物理地址

chumojing 贡献于2013-01-01

作者 雨林木风  创建于2011-04-21 06:26:00   修改者雨林木风  修改于2011-04-21 07:12:00字数2420

文档摘要:一、逻辑地址转线性地址   机器语言指令中出现的内存地址,都是逻辑地址,需要转换成线性地址,再经过MMU(CPU中的内存管理单元)转换成物理地址才能够被访问到。我们写个最简单的helloworld程序,用gccs编译,再反编译后会看到以下指令:mov   0x80495b0,%eax这里的内存地址0x80495b0就是一个逻辑地址,必须加上隐含的DS数据段的基地址,才能构成线性地址。
关键词:

一、逻辑地址转线性地址     机器语言指令中出现的内存地址,都是逻辑地址,需要转换成线性地址,再经过 MMU(CPU 中的内存管理单元 ) 转换成物理地址才能够被访问到。 我们写个最简单的 hello world 程序,用 gccs 编译,再反编译后会看到以下指令: mov    0x80495b0, %eax 这里的内存地址 0x80495b0 就是一个逻辑地址,必须加上隐含的 DS 数据段的基地址,才能构成线性地址。也就是说 0x80495b0 是当前任务的 DS 数据段内的偏移。           在 x86 保护模式下,段的信息(段基线性地址、长度、权限等)即段描述符 占 8 个字节,段信息无法直接存放在段寄存器中(段寄存器只有 2 字节)。 Intel 的设计是段描述符集中存放在 GDT 或 LDT 中,而段寄存器存放的是段描述符在 GDT 或 LDT 内的索引值 (index) 。 Linux 中逻辑地址等于线性地址 。为什么这么说呢?因为 Linux 所有的段(用户代码段、用户数据段、内核代码段、内核数据段)的线性地址都是从 0x00000000 开始,长度 4G ,这样 线性地址 = 逻辑地址 + 0x00000000 ,也就是说逻辑地址等于线性地址了。 这样的情况下 Linux 只用到了 GDT ,不论是用户任务还是内核任务,都没有用到 LDT 。 GDT 的第 12 和 13 项段描述符是 __KERNEL_CS 和 __KERNEL_DS ,第 14 和 15 项段描述符是 __USER_CS 和 __USER_DS 。内核任务使用 __KERNEL_CS 和 __KERNEL_DS ,所有的用户任务共用 __USER_CS 和 __USER_DS ,也就是说不需要给每个任务再单独分配段描述符。内核段描述符和用户段描述符虽然起始线性地址和长度都一样,但 DPL( 描述符特权级 ) 是不一样的。 __KERNEL_CS 和 __KERNEL_DS 的 DPL 值为 0 (最高特权), __USER_CS 和 __USER_DS 的 DPL 值为 3 。 用 gdb 调试程序的时候,用 info reg 显示当前寄存器的值: cs             0x73     115 ss             0x7b     123 ds             0x7b     123 es             0x7b     123 可以看到 ds 值为 0x7b, 转换成二进制为 00000000 01111011 , TI 字段值为 0, 表示使用 GDT , GDT 索引值为 01111 ,即十进制 15 ,对应的就是 GDT 内的 __USER_DATA 用户数据段描述符。 从上面可以看到, Linux 在 x86 的分段机制上运行,却通过一个巧妙的方式绕开了分段。 Linux 主要以分页的方式实现内存管理。 二、线性地址转物理地址 前面说了Linux中逻辑地址等于线性地址,那么线性地址怎么对应到物理地址呢?这个大家都知道,那就是通过分页机制,具体的说,就是通过页表查找来对应 物理地址。 准确的说分页是CPU提供的一种机制,Linux只是根据这种机制的规则,利用它实现了内存管理。 在保护模式下,控制寄存器CR0的最高位PG位控制着分页管理机制是否生效,如果PG=1,分页机制生效,需通过页表查找才能把线性地址转换物理地址。如 果PG=0,则分页机制无效,线性地址就直接做为物理地址。 分页的基本原理是把内存划分成大小固定的若干单元,每个单元称为一页(page),每页包含4k字节的地址空间(为简化分析,我们不考虑扩展分页的情 况)。这样每一页的起始地址都是4k字节对齐的。为了能转换成物理地址,我们需要给CPU提供当前任务的线性地址转物理地址的查找表,即页表(page table)。注意,为了实现每个任务的平坦的虚拟内存,每个任务都有自己的页目录表和页表。 为了节约页表占用的内存空间,x86将线性地址通过页目录表和页表两级查找转换成物理地址。 32位的线性地址被分成3个部分: 最高10位 Directory 页目录表偏移量,中间10位 Table是页表偏移量,最低12位Offset是物理页内的字节偏移量。 页目录表的大小为4k(刚好是一个页的大小),包含1024项,每个项4字节(32位),项目里存储的内容就是页表的物理地址。如果页目录表中的页表尚未 分配,则物理地址填0。 页表的大小也是4k,同样包含1024项,每个项4字节,内容为最终物理页的物理内存起始地址。 每个活动的任务,必须要先分配给它一个页目录表,并把页目录表的物理地址存入cr3寄存器。页表可以提前分配好,也可以在用到的时候再分配。 还是以 mov 0x80495b0, %eax 中的地址为例分析一下线性地址转物理地址的过程。 前面说到Linux中逻辑地址等于线性地址,那么我们要转换的线性地址就是0x80495b0。 转换的过程是由CPU自动完成的,Linux所要做的就是 准备好转换所需的页目录表和页表(假设已经准备好,给页目录表和页表分配物理内存的过程很复杂,后面再分析)。 内核先将当前任务的页目录表的物理地址填入cr3寄存器。 线性地址 0x80495b0 转换成二进制后是 0000 1000 0000 0100 1001 0101 1011 0000,最高10位0000 1000 00的十进制是32,CPU查看页目录表第32项,里面存放的是页表的物理地址。线性地址中间10位00 0100 1001 的十进制是73,页表的第73项存储的是最终物理页的物理起始地址。物理页基地址加上线性地址中最低12位的偏移量,CPU就找到了线性地址最终对应的物 理内存单元。 我们知道Linux中用户进程线性地址能寻址的范围是0 - 3G,那么是不是需要提前先把这3G虚拟内存的页表都建立好呢?一般情况下,物理内存是远远小于3G的,加上同时有很多进程都在运行,根本无法给每个进程 提前建立3G的线性地址页表。Linux利用CPU的一个机制解决了这个问题。进程创建后我们可以给页目录表的表项值都填0,CPU在查找页表时,如果表 项的内容为0,则会引发一个缺页异常,进程暂停执行,Linux内核这时候可以通过一系列复杂的算法给分配一个物理页,并把物理页的地址填入表项中,进程 再恢复执行。当然进程在这个过程中是被蒙蔽的,它自己的感觉还是正常访问到了物理内存。

下载文档到电脑,查找使用更方便

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 3 金币 [ 分享文档获得金币 ] 0 人已下载

下载文档