Chinaunix

标题: 我理解的逻辑地址、线性地址、物理地址和虚拟地址(补充完整了) [打印本页]

作者: 独孤九贱    时间: 2008-01-15 16:32
标题: 我理解的逻辑地址、线性地址、物理地址和虚拟地址(补充完整了)
要过年了,发个年终总结贴,只是个人理解,不包正确哈。

本贴涉及的硬件平台是X86,如果是其它平台,嘻嘻,不保证能一一对号入座,但是举一反三,我想是完全可行的。

一、概念

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

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

逻辑地址(logical address)
Intel为了兼容,将远古时代的段式内存管理方式保留了下来。逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。以上例,我们说的连接器为A分配的0x08111111这个地址就是逻辑地址。
——不过不好意思,这样说,好像又违背了Intel中段式管理中,对逻辑地址要求,“一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量],也就是说,上例中那个0x08111111,应该表示为[A的代码段标识符: 0x08111111],这样,才完整一些”

线性地址(linear address)或也叫虚拟地址(virtual address)
跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。

-------------------------------------------------------------
CPU将一个虚拟内存空间中的地址转换为物理地址,需要进行两步:首先将给定一个逻辑地址(其实是段内偏移量,这个一定要理解!!!),CPU要利用其段式内存管理单元,先将为个逻辑地址转换成一个线程地址,再利用其页式内存管理单元,转换为最终物理地址。

这样做两次转换,的确是非常麻烦而且没有必要的,因为直接可以把线性地址抽像给进程。之所以这样冗余,Intel完全是为了兼容而已。

2、CPU段式内存管理,逻辑地址如何转换为线性地址
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,如图:

最后两位涉及权限检查,本贴中不包含。

索引号,或者直接理解成数组下标——那它总要对应一个数组吧,它又是什么东东的索引呢?这个东东就是“段描述符(segment descriptor)”,呵呵,段描述符具体地址描述了一个段(对于“段”这个字眼的理解,我是把它想像成,拿了一把刀,把虚拟内存,砍成若干的截——段)。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,我刚才对段的抽像不太准确,因为看看描述符里面究竟有什么东东——也就是它究竟是如何描述的,就理解段究竟有什么东东了,每一个段描述符由8个字节组成,如下图:

这些东东很复杂,虽然可以利用一个数据结构来定义它,不过,我这里只关心一样,就是Base字段,它描述了一个段的开始位置的线性地址。

Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT。

GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

好多概念,像绕口令一样。这张图看起来要直观些:

首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],
1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
3、把Base + offset,就是要转换的线性地址了。

还是挺简单的,对于软件来讲,原则上就需要把硬件转换所需的信息准备好,就可以让硬件来完成这个转换了。OK,来看看Linux怎么做的。

3、Linux的段式管理
Intel要求两次转换,这样虽说是兼容了,但是却是很冗余,呵呵,没办法,硬件要求这样做了,软件就只能照办,怎么着也得形式主义一样。
另一方面,其它某些硬件平台,没有二次转换的概念,Linux也需要提供一个高层抽像,来提供一个统一的界面。所以,Linux的段式管理,事实上只是“哄骗”了一下硬件而已。

按照Intel的本意,全局的用GDT,每个进程自己的用LDT——不过Linux则对所有的进程都使用了相同的段来对指令和数据寻址。即用户数据段,用户代码段,对应的,内核中的是内核数据段和内核代码段。这样做没有什么奇怪的,本来就是走形式嘛,像我们写年终总结一样。
include/asm-i386/segment.h
  1. #define GDT_ENTRY_DEFAULT_USER_CS        14
  2. #define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3)

  3. #define GDT_ENTRY_DEFAULT_USER_DS        15
  4. #define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS * 8 + 3)

  5. #define GDT_ENTRY_KERNEL_BASE        12

  6. #define GDT_ENTRY_KERNEL_CS                (GDT_ENTRY_KERNEL_BASE + 0)
  7. #define __KERNEL_CS (GDT_ENTRY_KERNEL_CS * 8)

  8. #define GDT_ENTRY_KERNEL_DS                (GDT_ENTRY_KERNEL_BASE + 1)
  9. #define __KERNEL_DS (GDT_ENTRY_KERNEL_DS * 8)
复制代码

把其中的宏替换成数值,则为:
  1. #define __USER_CS 115        [00000000 1110  0  11]
  2. #define __USER_DS 123        [00000000 1111  0  11]
  3. #define __KERNEL_CS 96      [00000000 1100  0  00]
  4. #define __KERNEL_DS 104    [00000000 1101  0  00]
复制代码


方括号后是这四个段选择符的16位二制表示,它们的索引号和T1字段值也可以算出来了
  1. __USER_CS              index= 14   T1=0
  2. __USER_DS               index= 15   T1=0
  3. __KERNEL_CS           index=  12  T1=0
  4. __KERNEL_DS           index= 13   T1=0
复制代码


T1均为0,则表示都使用了GDT,再来看初始化GDT的内容中相应的12-15项(arch/i386/head.S):
  1.         .quad 0x00cf9a000000ffff        /* 0x60 kernel 4GB code at 0x00000000 */
  2.         .quad 0x00cf92000000ffff        /* 0x68 kernel 4GB data at 0x00000000 */
  3.         .quad 0x00cffa000000ffff        /* 0x73 user 4GB code at 0x00000000 */
  4.         .quad 0x00cff2000000ffff        /* 0x7b user 4GB data at 0x00000000 */
复制代码


按照前面段描述符表中的描述,可以把它们展开,发现其16-31位全为0,即四个段的基地址全为0。

这样,给定一个段内偏移地址,按照前面转换公式,0 + 段内偏移,转换为线性地址,可以得出重要的结论,“在Linux下,逻辑地址与线性地址总是一致(是一致,不是有些人说的相同)的,即逻辑地址的偏移量字段的值与线性地址的值总是相同的。!!!”

忽略了太多的细节,例如段的权限检查。呵呵。

Linux中,绝大部份进程并不例用LDT,除非使用Wine ,仿真Windows程序的时候。

4.CPU的页式内存管理

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位)
依据以下步骤进行转换:
1、从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
2、根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
3、根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
4、将页的起始地址与线性地址中最后12位相加,得到最终我们想要的葫芦;

这个转换过程,应该说还是非常简单地。全部由硬件完成,虽然多了一道手续,但是节约了大量的内存,还是值得的。那么再简单地验证一下:
1、这样的二级模式是否仍能够表示4G的地址;
页目录共有:2^10项,也就是说有这么多个页表
每个目表对应了:2^10页;
每个页中可寻址:2^12个字节。
还是2^32 = 4GB

2、这样的二级模式是否真的节约了空间;
也就是算一下页目录项和页表项共占空间 (2^10 * 4 + 2 ^10 *4) = 8KB。哎,……怎么说呢!!!

红色错误,标注一下,后文贴中有此讨论。。。。。。
按<深入理解计算机系统>中的解释,二级模式空间的节约是从两个方面实现的:
A、如果一级页表中的一个页表条目为空,那么那所指的二级页表就根本不会存在。这表现出一种巨大的潜在节约,因为对于一个典型的程序,4GB虚拟地址空间的大部份都会是未分配的;
B、只有一级页表才需要总是在主存中。虚拟存储器系统可以在需要时创建,并页面调入或调出二级页表,这就减少了主存的压力。只有最经常使用的二级页表才需要缓存在主存中。——不过Linux并没有完全享受这种福利,它的页表目录和与已分配页面相关的页表都是常驻内存的。

值得一提的是,虽然页目录和页表中的项,都是4个字节,32位,但是它们都只用高20位,低12位屏蔽为0——把页表的低12屏蔽为0,是很好理解的,因为这样,它刚好和一个页面大小对应起来,大家都成整数增加。计算起来就方便多了。但是,为什么同时也要把页目录低12位屏蔽掉呢?因为按同样的道理,只要屏蔽其低10位就可以了,不过我想,因为12>10,这样,可以让页目录和页表使用相同的数据结构,方便。

本贴只介绍一般性转换的原理,扩展分页、页的保护机制、PAE模式的分页这些麻烦点的东东就不啰嗦了……可以参考其它专业书籍。

5.Linux的页式内存管理
原理上来讲,Linux只需要为每个进程分配好所需数据结构,放到内存中,然后在调度进程的时候,切换寄存器cr3,剩下的就交给硬件来完成了(呵呵,事实上要复杂得多,不过偶只分析最基本的流程)。

前面说了i386的二级页管理架构,不过有些CPU,还有三级,甚至四级架构,Linux为了在更高层次提供抽像,为每个CPU提供统一的界面。提供了一个四层页管理架构,来兼容这些二级、三级、四级管理架构的CPU。这四级分别为:

页全局目录PGD(对应刚才的页目录)
页上级目录PUD(新引进的)
页中间目录PMD(也就新引进的)
页表PT(对应刚才的页表)。

整个转换依据硬件转换原理,只是多了二次数组的索引罢了,如下图:

那么,对于使用二级管理架构32位的硬件,现在又是四级转换了,它们怎么能够协调地工作起来呢?嗯,来看这种情况下,怎么来划分线性地址吧!
从硬件的角度,32位地址被分成了三部份——也就是说,不管理软件怎么做,最终落实到硬件,也只认识这三位老大。
从软件的角度,由于多引入了两部份,,也就是说,共有五部份。——要让二层架构的硬件认识五部份也很容易,在地址划分的时候,将页上级目录和页中间目录的长度设置为0就可以了。
这样,操作系统见到的是五部份,硬件还是按它死板的三部份划分,也不会出错,也就是说大家共建了和谐计算机系统。

这样,虽说是多此一举,但是考虑到64位地址,使用四层转换架构的CPU,我们就不再把中间两个设为0了,这样,软件与硬件再次和谐——抽像就是强大呀!!!

例如,一个逻辑地址已经被转换成了线性地址,0x08147258,换成二制进,也就是:
0000100000 0101000111 001001011000
内核对这个地址进行划分
PGD = 0000100000
PUD = 0
PMD = 0
PT = 0101000111
offset = 001001011000

现在来理解Linux针对硬件的花招,因为硬件根本看不到所谓PUD,PMD,所以,本质上要求PGD索引,直接就对应了PT的地址。而不是再到PUD和PMD中去查数组(虽然它们两个在线性地址中,长度为0,2^0 =1,也就是说,它们都是有一个数组元素的数组),那么,内核如何合理安排地址呢?
从软件的角度上来讲,因为它的项只有一个,32位,刚好可以存放与PGD中长度一样的地址指针。那么所谓先到PUD,到到PMD中做映射转换,就变成了保持原值不变,一一转手就可以了。这样,就实现了“逻辑上指向一个PUD,再指向一个PDM,但在物理上是直接指向相应的PT的这个抽像,因为硬件根本不知道有PUD、PMD这个东西”。

然后交给硬件,硬件对这个地址进行划分,看到的是:
页目录 = 0000100000
PT = 0101000111
offset = 001001011000
嗯,先根据0000100000(32),在页目录数组中索引,找到其元素中的地址,取其高20位,找到页表的地址,页表的地址是由内核动态分配的,接着,再加一个offset,就是最终的物理地址了。

[ 本帖最后由 独孤九贱 于 2009-9-22 20:36 编辑 ]
作者: ghosTM55    时间: 2008-01-15 16:37
标题: 学习了

作者: netentsec    时间: 2008-01-15 16:48
深入浅出,讲的好,期待下文
作者: sisi8408    时间: 2008-01-15 20:07
赞一个。。。。。。。。。
作者: duanius    时间: 2008-01-15 21:14
说的不错   比ulk第二章更直白 人性化

[ 本帖最后由 duanius 于 2008-1-15 21:15 编辑 ]
作者: flw2    时间: 2008-01-16 09:34
支持一个,LZ超级高手
作者: achlice    时间: 2008-01-16 10:14
见了好帖子然后回复 ,是一种美德, 比如像我,一直都 是这样做的~~



看了内核源代码 情景 分析 ,从 8086 讲到80286,  80386,  终于明白,这是怎么回事儿了~~

[ 本帖最后由 achlice 于 2008-1-28 15:53 编辑 ]
作者: qps104    时间: 2008-01-16 11:16
好文,加深了我对3种地址的理解
作者: yj1804    时间: 2008-01-16 17:20
好文,赞一个
PS.推荐大家看毛德操的"LINUX内核源代码情景分析"一书, 里面讲解也比较详细
作者: duanius    时间: 2008-01-16 18:51
好文好文  隐隐中透着一种和谐美
作者: liuweni    时间: 2008-01-17 08:48
mark一下,好贴。
作者: guohua219    时间: 2008-01-17 09:00
不错,很能让人理解.........
作者: Aryang    时间: 2008-01-17 11:16
还没仔细看,大家说好那一定真的好
作者: 浮云一梦    时间: 2008-01-17 11:24
不错不错

作者: CUDev    时间: 2008-01-17 11:33
很好,很强大!
赞一个!
作者: xqwwqiao    时间: 2008-01-17 14:05
对我等新手来说,太深奥了!
作者: cindylzh    时间: 2008-01-17 14:27
lz理解真是透彻。仰望。。。。
作者: hinux    时间: 2008-01-17 17:31
不回不行啦!
作者: fwl    时间: 2008-01-17 18:14
不错不错  内存管理的原理在os的教材上就是这么说的
不过和实际联系起来的话就这个写得最好了
作者: neoedmund    时间: 2008-01-17 23:46
你这图是不是抄的深入理解linux内核看着眼熟
作者: wqch    时间: 2008-01-18 00:21
大致看了一下,图文并茂,讲得不错
作者: snhanwei    时间: 2008-01-18 09:06
如此好贴,当然要顶!
作者: foxwolf_ym    时间: 2008-01-18 09:09
不错,接着加深映像!!谢谢楼主!
作者: brvman    时间: 2008-01-18 09:29
好文,我頂!
作者: pl_piaoling    时间: 2008-01-18 12:31
看样,还需要继续学习~
作者: yamir    时间: 2008-01-18 14:44
原帖由 独孤九贱 于 2008-1-15 16:32 发表

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



相当不错的文章,如果在物理地址这里能再说清楚点就完美了
把物理地址与物理内存一一对应在刚开始学的时候是可以接受,不过再更深入到x86的体系架构中,这样理解,容易会产生很多混淆了,还是应该把北桥、IO端口和PCI一起拿出来理解,建议楼主可以结合Intel的北桥的datasheet里的那张System Address Ranges来补充说明一下。
作者: sanbiangongzi    时间: 2008-01-18 16:32
最后一部分没有看懂,那位大侠再讨论一下,唉,技不如人
作者: drunkedfish    时间: 2008-01-18 21:18
很强大  明天我估计要用它  感动啊
作者: 独孤九贱    时间: 2008-01-19 21:26
原帖由 yamir 于 2008-1-18 14:44 发表


相当不错的文章,如果在物理地址这里能再说清楚点就完美了
把物理地址与物理内存一一对应在刚开始学的时候是可以接受,不过再更深入到x86的体系架构中,这样理解,容易会产生很多混淆了,还是应该把北桥 ...


这涉及到DRAM的物理访问方式了,本贴只是介绍偶对这些入门概念的理解而已,不想扯远了……或许以后会单独另一贴吧。

另有朋友说偶的图的抄袭问题——不是抄袭,直接原版拷贝的,哈哈,图画得挺好,让偶画,肯定没有这么简单明了……
作者: wxs40305    时间: 2008-01-19 21:31
标题: 回复 #29 独孤九贱 的帖子
独孤九贱兄,能否帮我 解决 下我发的 "局部描述符 表 中有多少个描述符"贴?
谢谢了
作者: free_mind    时间: 2008-01-19 21:39
乖乖,吓到我了,我还以为物理地址=线形地址,虚拟地址=逻辑地址
作者: 留下可爱心情    时间: 2008-01-19 21:59
很牛的说~!
作者: youyu_buzai    时间: 2008-01-20 15:14
不错,,,,,是个高手
作者: tanbao2000    时间: 2008-01-20 15:30
学习了,顶一个!!!!
作者: mageguoshi    时间: 2008-01-20 16:26
顶一个啊!是好帖子就要留名!
作者: vestige    时间: 2008-01-20 21:18
标题: 回复 #1 独孤九贱 的帖子
感谢楼主分享!不过我对于这句话有点疑问:

2、这样的二级模式是否真的节约了空间;
也就是算一下页目录项和页表项共占空间 (2^10 * 4 + 2 ^10 *4) = 8KB。哎,……怎么说呢!!!

这样计算的结果应该是所有页目录的空间+一个页目录对应的页表的空间,Linux进程内存管理应该可以允许进程不只一个页目录被装入内存吧?
还是说就是只能放1024个页表?这样就说的过去了。

[ 本帖最后由 vestige 于 2008-1-20 21:40 编辑 ]
作者: chenyq83    时间: 2008-01-20 22:33
谢谢楼主分享!
作者: xdolt    时间: 2008-01-20 23:36
这个社会终于“和谐”了!
作者: 独孤九贱    时间: 2008-01-21 09:08
原帖由 vestige 于 2008-1-20 21:18 发表
感谢楼主分享!不过我对于这句话有点疑问:

2、这样的二级模式是否真的节约了空间;
也就是算一下页目录项和页表项共占空间 (2^10 * 4 + 2 ^10 *4) = 8KB。哎,……怎么说呢!!!

这样计算的结果应该是 ...

是的,这样计算的确是错误的,呵呵,谢谢指正,估计是当时行文的时候,没有仔细考虑……

二级模式能够节约内存容量的原因,当一个进程被调度运行的时候,内核会分配给它一个对应的目录项,但是由于采用虚拟内存管理方式,这个目录中的所有页表并非都会被分配空间,只有进程实际需要的时候,才会进行页表的空间分配。
作者: 独孤九贱    时间: 2008-01-21 09:11
原帖由 wxs40305 于 2008-1-19 21:31 发表
独孤九贱兄,能否帮我 解决 下我发的 "局部描述符 表 中有多少个描述符"贴?
谢谢了


没有看到你的贴,不知你指的是不是这个:
  1. struct desc_struct default_ldt[] = { { 0, 0 }, { 0, 0 }, { 0, 0 },
  2.                 { 0, 0 }, { 0, 0 } };
复制代码

arch/i386/kernel/traps.c
作者: ziyiu123    时间: 2008-01-21 11:22
与君共勉,呵呵,学习了。
作者: jonry    时间: 2008-01-21 17:34
很不错,学习了~~~
作者: 想飞的蜗牛    时间: 2008-01-21 20:39
写得很好 又容易看懂 呵呵 赞一个
lz是高手
作者: softchinacom    时间: 2008-01-22 14:19
标题: 你的理解错误很多,居然将硬件的进步说成是冗余,可惜了。
从硬件设计的角度来看!
物理地址: 内存单元的编号,逻辑上是将将内存单元按照一定的度量单位划分,如字节,字等,80x86  可以按字节也可以按字编址。所谓的总线只是一个存取通路,插座相当于一个设备开关。地址空间是数据总线的宽度。
逻辑地址:也称相对地址,是与绝对地址(物理地址)相对的一个概念,是相对与某一位置开始编址。她存在的意义是记住程序和数据的相对位置。一般定义为CPU通过地址总线发出的地址。 程序装入到内存后就通过相关的硬件,主要是地址的加法器与一个相对位置相加得到绝对地址。相对的位置是存在段表和页表中的,通过相联查找获得。地址空间由用户定义
虚拟内存:说白了就是构造一个满足程序和用户需要的空间。程序员不用担心内存不够!也不用担心地址问题。只要关心程序的逻辑顺序,即逻辑地址。
虚拟地址:和逻辑地址是抽象和具体的关系,是逻辑地址的一个实例。
虚拟地址到物理地址变换:对于X86  目前有三种方式,页式,段式,段页式
页式:
地址映像:
   第一步 将用户程序按页的长度划分,长度固定。  划分了之后,就有了页的起始地址
      第二步 操作系统创建进程时,先查全局的页表,找了几页空闲的内存,也就有了物理的页的起始地址
      第三步 操作系统创建页表,一般有这么几项,物理页号, 用户页号 (o-n) 使用位等。 这样就建立了虚拟地址到物理地址的映射关系了。
地址变换:硬件根据cpu发出的逻辑地址(这里是虚拟地址)查表,得到物理的页号(高位地址),将物理页号加逻辑地址的地位(页内地址)就是物理地址了。一次查表,一次加法计算。比直接用物理地址编程效率要低很多。
段式:
虚拟地址划分  用户号+段号+段内偏移
地址映像:
   第一步 将用户程序分段,长度不固定。  划分了之后,就有了段的起始地址,和段的长度。
      第二步 操作系统,根据段长申请内存空间,也就有了段起始地址。
      第三步 操作系统创建段表,一般有这么几项,两个段起始地址, 段长。 这样就建立了逻辑段到物理段的映射关系了。
地址变换:INTEL X86是这样的, 有段寄存器堆,即段选择子,描述符是一致的。 先将段表基地址装入 LDT,(这里讨论的是一般用户程序),根据CPU 发出的逻辑地址得到段号, 加上LDT 当中的段表基地址得到一个逻辑段到物理段的映射项,读出物理段基地址,再加上段的偏移得到 物理地址 ,总共需要做两次加法,一次查表, 效率更低。
段页式: 即将程序中一段再分页,需要一个段表,一个页表,先查段表得到页表基地址,然后再页式变换。
对于这种三种变换方式的优点: 简化 编程,简化系统设计,太多了,
总结:  对内核编程人员的建议:  先对程序划分,再定义映射,设置寄存器,地址变换由硬件完成。
作者: polter    时间: 2008-01-22 15:46
好文,标记一下,赞一个
作者: mik    时间: 2008-01-23 01:17
原帖由 softchinacom 于 2008-1-22 14:19 发表
从硬件设计的角度来看!
物理地址: 内存单元的编号,逻辑上是将将内存单元按照一定的度量单位划分,如字节,字等,80x86  可以按字节也可以按字编址。所谓的总线只是一个存取通路,插座相当于一个设备开关。地址空 ...


没细看,但 LZ 写得还是不错滴,何以说错误很多呀,你说得也一踏糊涂
作者: 独孤九贱    时间: 2008-01-23 09:24
原帖由 softchinacom 于 2008-1-22 14:19 发表
从硬件设计的角度来看!
物理地址: 内存单元的编号,逻辑上是将将内存单元按照一定的度量单位划分,如字节,字等,80x86  可以按字节也可以按字编址。所谓的总线只是一个存取通路,插座相当于一个设备开关。地址空 ...


1、如果你认为段式+页式不是Intel的冗余,不是为了向上兼容而保留,而是一种硬件进步,我虽然不赞成你的看法,但是我想听听你的理由——不过你的贴子中,并没有并于“进步”的原因解释,只是一大堆的名词解释。呵呵。而且,内容基本与偶是一致的,没有质的分歧;

2、Intel在后续CPU中,保留了段式映射结构,连他自己都说这只是一个兼容而不得已的原因(这个不权威哈,出处忘了,好像在N年前的一篇电脑杂志上看到的)

3、很多优秀的CPU,本身就没有,或者说是“有限支持”段式映射;

4、如果真要说是“进步”,那也是Intel一家的进步,从段式发展到支持页式——不过如果它不考虑兼容,完全可以舍弃段式。所以还是说不上“进步”。

呵呵……一家之言,仅供参考。
作者: df00171    时间: 2008-01-24 00:17
lz写的好啊
作者: jzlinux    时间: 2008-01-24 09:57
标题: 回复 #1 独孤九贱 的帖子
好文,mark一下,感谢lz
作者: jiaxi    时间: 2008-01-24 10:26
好文,转载了
谢谢
作者: DeathAngle    时间: 2008-01-24 10:40
标题: 回复 #1 独孤九贱 的帖子
good,转载下,帮 lz再宣传下
作者: flytiger23    时间: 2008-01-24 11:57
楼主描述得很精彩,赞一个,顶一个,呵呵
作者: caong    时间: 2008-01-24 15:57
好帖,希望更多人能看到
作者: dreampro    时间: 2008-01-26 08:25
提示: 作者被禁止或删除 内容自动屏蔽
作者: gbh5526335    时间: 2008-01-27 22:34
感觉讲的不错,可是我看不懂,基础太薄弱了!
作者: gothicane    时间: 2008-01-28 10:14
好贴~~收藏了~~~~~
作者: 恋夏寒    时间: 2008-01-28 17:54
mark 一下, 留個腳印
作者: 雪夜狂飘    时间: 2008-01-29 10:57
的确值得学习,有时间应该研究研究
作者: www1862    时间: 2008-01-29 11:55
不错,都是楼主自己的理解。图抄一抄没什么的,哈哈。注明一下就行了。
作者: mageguoshi    时间: 2008-02-01 14:17
好帖留名!一定要顶啊!
作者: mouse2000    时间: 2008-02-15 20:51
好文是一定要顶的~!
作者: chameleon110    时间: 2008-02-18 02:18
好贴,深入浅出,顶,坚决要顶
作者: william.zhang    时间: 2008-02-18 11:18
决定顶下:em11:
作者: 大块头    时间: 2008-02-19 13:36
据我所知,九见好像是圈里的,哈哈
作者: 独孤九贱    时间: 2008-02-19 16:23
标题: 回复 #64 大块头 的帖子
什么圈(quan or juan)?
作者: maxxfire    时间: 2008-02-19 17:01
好文哪,期待楼主的下一篇~
楼主,能不能讲讲linux内存3个zone:DMA、NORMAL、HIGHMEM的工作原理啊。。
作者: arshur    时间: 2008-02-21 13:59
标题: 强人啊!!!!
强人啊!!!!!!!!!!!!!!
作者: fdshenjia    时间: 2008-02-21 19:41
写的太好了!不想说太多的赞美,但是发自内心的。
作者: pasoolee    时间: 2008-02-22 12:29
楼主应该结合 Linux下的 IA32汇编语言,讲一下,或结合PE文件的特性,在深入探讨一下,真正的可执行文件是如何编译的,如何加载的,如:PE文件的入口地址如何映射的问题,嘻嘻。(当然,我可不知道编译器的内部原理哦,只是想说一下如何对静态代码进行分析,不包括对静态代码加密的哦)
作者: potti_chu    时间: 2008-02-22 15:31
很好,很强大. 来学习.
作者: renxiaojun_6635    时间: 2008-02-22 21:44
看过,不错! 学到了!
作者: _LoveLinux    时间: 2008-02-23 11:44
好文,LZ很厉害!!!

[ 本帖最后由 _LoveLinux 于 2008-2-23 11:55 编辑 ]

61254_080216010548.gif (14.25 KB, 下载次数: 78)

61254_080216010548.gif

作者: aaaaal    时间: 2008-02-26 09:38
佩服
光从排版的精神来说都值得称赞。
作者: wangjl_sdu    时间: 2008-02-27 09:54
看了,不错,收下了。
作者: Godbach    时间: 2008-03-06 23:18
感谢LZ的辛苦劳动
作者: bigbear1113    时间: 2008-03-07 22:57
好文章,很值得学习!!
作者: wwling2001    时间: 2008-03-09 19:26
标题: 回复 #1 独孤九贱 的帖子
好帖啊,顶一下
作者: davidwrk    时间: 2008-03-10 13:22
学习了~学习了~学习了~学习了~
作者: vestige    时间: 2008-03-29 12:42
标题: 回复 #1 独孤九贱 的帖子
我对这里还有一点疑问:
从软件的角度上来讲,因为它的项只有一个,32位,刚好可以存放与PGD中长度一样的地址指针。那么所谓先到PUD,到到PMD中做映射转换,就变成了保持原值不变,一一转手就可以了。这样,就实现了“逻辑上指向一个PUD,再指向一个PDM,但在物理上是直接指向相应的PT的这个抽像,因为硬件根本不知道有PUD、PMD这个东西”。

我对“转手”这个过程不太了解,是不是一个32位的线性地址,先从头10位的PGD中取得PT的入口项,然后由于要兼容4级分页,需要把这个32位的线性地址拷贝到唯一的PUD入口,再拷贝到PMD入口,然后再得到PT的地址?这样做的话岂非效率很低?
作者: zx_wing    时间: 2008-03-29 12:58
原帖由 vestige 于 2008-3-29 12:42 发表
我对这里还有一点疑问:
从软件的角度上来讲,因为它的项只有一个,32位,刚好可以存放与PGD中长度一样的地址指针。那么所谓先到PUD,到到PMD中做映射转换,就变成了保持原值不变,一一转手就可以了。这样,就 ...

不是,对页表的操作,是逐级获得指向下一级的指针,对于PUD的操作,实际上仍然是返回指向PGD的指针。
这里有段代码,虽然不是x86页表操作的代码,但性质是一样的,可以解释这个问题。
/*
代码中所有和pud有关的部分都是假的,被实现成空函数,例如pud_alloc_one()、pgd_cmpxchg_rel()等等
*/
pgd = pgd_offset(mm, mpaddr);
again_pgd:
    if (unlikely(pgd_none(*pgd))) { // acquire semantics

        pud_t *old_pud = NULL;
        pud = pud_alloc_one(mm, mpaddr);
        if (unlikely(!pgd_cmpxchg_rel(mm, pgd, old_pud, pud))) {
            pud_free(pud);
            goto again_pgd;
        }
    }

    pud = pud_offset(pgd, mpaddr); //这里仍然返回指向PGD的指针
again_pud:
    if (unlikely(pud_none(*pud))) { // acquire semantics

        pmd_t* old_pmd = NULL;
        pmd = pmd_alloc_one(mm, mpaddr);
        if (unlikely(!pud_cmpxchg_rel(mm, pud, old_pmd, pmd))) {
            pmd_free(pmd);
            goto again_pud;
        }
    }

    pmd = pmd_offset(pud, mpaddr);
again_pmd:
    if (unlikely(pmd_none(*pmd))) { // acquire semantics

        pte_t* old_pte = NULL;
        pte_t* pte = pte_alloc_one_kernel(mm, mpaddr);
        if (unlikely(!pmd_cmpxchg_kernel_rel(mm, pmd, old_pte, pte))) {
            pte_free_kernel(pte);
            goto again_pmd;
        }
    }


作者: vestige    时间: 2008-03-29 15:49
标题: 回复 #80 zx_wing 的帖子
Thanks 楼上 a lot!

我看ULK里讲到:
The kernel keeps a position for the Page Upper Directory and the Page Middle Directory by setting the number of entries in them to 1 and mapping these two entries into the proper entry of the Page Global Direcotry.

不知道这里说的proper entry指的什么?
是不是为PUD和PMD在PGD中分配的两个entry,然后这两个entry又都指回了PGD?
作者: zx_wing    时间: 2008-03-29 18:50
原帖由 vestige 于 2008-3-29 15:49 发表
Thanks 楼上 a lot!

我看ULK里讲到:
The kernel keeps a position for the Page Upper Directory and the Page Middle Directory by setting the number of entries in them to 1 and mapping these ...

我也不清楚这里的2级paging是什么意思。
我对x86的这部分不是很懂,但刚才去看了一下,确实分配了pmd的,应该是三级level。我看的代码是2.6.20,不知道这个版本还支持2级level不。不太懂,不好意思
作者: vestige    时间: 2008-03-29 19:33
还是感谢zx_wing了,呵呵,我去琢磨琢磨,搞明白了整理个东西出来
作者: hansom6    时间: 2008-03-29 20:24
学习了,
作者: new_learner    时间: 2008-04-13 02:29
楼主描述得不错,生动形象,呵呵
作者: capable    时间: 2008-04-13 12:26
very good!
作者: pro_mise    时间: 2008-04-13 16:29
呵呵,九贱兄幸苦了啊。
to 44:
您提到总线地址是逻辑地址?在x86平台,对应PCI总线,总线地址应该是物理地址吧?
作者: szjrabbit    时间: 2008-05-31 23:21
深入浅出,确实是好文,期待作者有更好的文章,新手需要这样的文章。
作者: minifish    时间: 2008-06-01 10:08
原帖由 pro_mise 于 2008-4-13 16:29 发表
呵呵,九贱兄幸苦了啊。
to 44:
您提到总线地址是逻辑地址?在x86平台,对应PCI总线,总线地址应该是物理地址吧?


这要看44楼提到的地址总线是哪一个级别的?如果是CPU to MMU的话,倒也不能说错。
如果是FSB或者PCI总线上的地址,那么肯定是物理地址。
嗯,严格的说,FSB上发出的物理地址不一定完全是内存的物理地址
可以去看看Intel MCH的规范,里面讲的比较详细。
作者: zmhu    时间: 2008-06-03 22:44
very good. study !!!
作者: qingfengjianke    时间: 2008-06-28 09:57
:wink: 55555
作者: 怪怪虎    时间: 2008-06-30 09:38
标题: 回复 #1 独孤九贱 的帖子
好帖啊,顶起来~~~~
作者: james_li85    时间: 2008-07-08 16:58
正在看内核
强烈顶LZ
作者: hansion3406    时间: 2008-07-11 10:44
这个竟然是我不懂的。。
今天我一定看明白它再下班。。
作者: 冰伊罗    时间: 2008-07-11 18:18
强,本人新手,学习ing
作者: 黑胡子    时间: 2008-11-20 09:06
看懂了3分之1  后面就:em12:
作者: linuxfun    时间: 2008-12-23 10:57
提示: 作者被禁止或删除 内容自动屏蔽
作者: vincentdpan    时间: 2008-12-26 15:08
有啥好说的,加精啊!
作者: whoisliang    时间: 2008-12-28 08:22
学习了,感动!
作者: 随风之幻    时间: 2009-01-04 14:14
嗯,先根据0000100000(32),在页目录数组中索引,找到其元素中的地址,取其高20位,找到页表的地址,页表的地址是由内核动态分配的,接着,再加一个offset,就是最终的物理地址了。


页表的地址是由内核动态分配的?这句话有问题吧。因为在linux内核代码head.s里面,页目录和页表早就把整个内存划分好的。不存在动态分配一说。

再说页目录的地址是在物理地址0处,跟在页目录后面的就是页表,我实在是无法理解LZ说的页表的地址由内核动态分配的意思。

[ 本帖最后由 随风之幻 于 2009-1-4 14:17 编辑 ]




欢迎光临 Chinaunix (http://bbs.chinaunix.net/) Powered by Discuz! X3.2