Chinaunix首页 | 论坛 | 博客
  • 博客访问: 229566
  • 博文数量: 77
  • 博客积分: 80
  • 博客等级: 民兵
  • 技术积分: 420
  • 用 户 组: 普通用户
  • 注册时间: 2012-01-06 17:15
文章分类
文章存档

2013年(4)

2012年(73)

分类:

2012-12-10 14:59:49

这两天一直看到uc论坛上关于系统中几种地址概念的讨论,加上这一块自己一直没有理解,索性就在网上多找了些资料,下面是自己的一些浅薄的理解。
设计平台为x86,如果是其它平台,不保证能一一对号入座,但应该也是可以举一反三的吧。
 
概念
 
   在硬件工程师和普通用户看来,内存就是插在或固化在主板上的内存条,他们有一定的容量,比如512MB。但应用程序员并不过度关心内存容量,而是关心可以使用的内存空间。比如,他们可以开发一个占用1GB内存的程序,让其在操作系统下运行,哪怕实际内存不足1G。操作系统开发者则是介于二者之间,即需要知道物理内存的地址,也需要提供一套机制,为应用程序提供另一个内存空间,这个空间的大小可以与实际内存大小没有直接关系。
   这里将主板上的物理内存条所提供的内存空间定义为物理内存空间,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址
将应用程序员看到的内存定义为虚拟地址,它是相对于物理内存来讲的,可以直接理解成“不直实的”,“假的”内存,例如,一个0x08000000内存地址,它并不对就物理地址上那个大数组中0x08000000 - 1那个地址元素;之所以是这样,是因为现代操作系统都提供了一种内存管理的抽像,即虚拟内存(virtual memory)。进程使用虚拟内存中的地址,由操作系统协助相关硬件,把它“转换”成真正的物理地址。这个“转换”,是所有问题讨论的关键。
   Intel为了兼容,将远古时代的段式内存管理方式保留了下来。逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。以上例,我们说的连接器为A分配的0x08111111这个地址就是逻辑地址。——不过不好意思,这样说,好像又违背了Intel中段式管理中,对逻辑地址要求,“一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量],也就是说,上例中那个0x08111111,应该表示为[A的代码段标识符: 0x08111111],这样,才完整一些”。
线性地址空间是指一段连续的、不分段的、范围为0~4GB的地址空间。一个线性地址就是线性地址空间的一个绝对地址。
   x86平台下的系统采用分段机制与分页机制对地址进行转换,其中分段机制把一个虚拟地址转换成线性地址;接着,分页机制把一个线性地址转换成物理地址。
 
分段机制
 
   段是虚拟地址空间的基本单位,分段机制必须把虚拟地址空间的一个地址转换为线性地址空间的一个线性地址。为了实现这种映射,仅仅用段寄存器来确定一个基址是不够的,至少还有描述段的长度,所以这里需要一个数据结构,这个结构包括以下内容:1)段的基地址;2)段的界限;3)段的保护属性。此时对于虚拟地址的每一个索引,都对应着索引:基地址:界限:属性的对应关系,这种对应关系组成一个表就叫做段描述符表,其中的表项叫做段描述符,而此时在段寄存器中存放的内容就是段描述表中的索引,由索引可以确定段描述符在描述符表中的位置,因此,段寄存器也叫选择符,结构如下:
   此时对于每一个虚拟地址“选择符:偏移量”,就能够先通过段选择符找到对应的段描述符中的基地址,加上偏移量即可得其在系统中的线性地址,这个对应关系如下图所示:
Snap3.jpg
 
 Linux中的段
    在IA32上任意地址都是一个虚拟地址,即任意地址都是以“选择符:偏移量”的方式给出的,这是分段机制内存访问模式的基本特点。但是,由于绝大多数硬件平台都不支持分段机制,只支持分页机制,所以为了让Linux有更好的可移植性,需要去掉分段机制。
    不幸的是,IA32规定分段机制是不可禁止的,因此,Linux的开发人员干脆让段的基地址为0,而段的界限为4GB,这是对于任意偏移量,则等式为“0+偏移量=线性地址”,即偏移量等于线性地址。另外由于分段机制规定偏移量<4GB,所以偏移量的范围为0H~FFFFFFFFH,这恰好是线性地址空间范围,也就是说虚拟地址直接映射到了线性地址,所以在Linux下虚拟逻辑地址与线性地址是一致的(而不是相同),即虚拟地址中的逻辑偏移量总是等于线性地址的。
    因此,Linux在系统中分别为内核和用户程序创建了基地址为0,段界限为4GB的代码段描述符与数据段描述符。
 
分页机制
 
   CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。从管理和效率的角度出发,线性地址被分为以固定长度为单位的组,称为页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page[2^20]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址——对应的页的地址。

   另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。

   这里注意到,这个total_page数组有2^20个成员,每个成员是一个地址(32位机,一个地址也就是4字节),那么要单单要表示这么一个数组,就要占去4MB的内存空间。为了节省空间,引入了一个二级管理模式的机器来组织分页单元。文字描述太累,看图直观一些:
Snap1.jpg

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

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

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

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

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

整个转换依据硬件转换原理,只是多了二次数组的索引罢了,如下图:
Snap2.jpg 那么,对于使用二级架构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,就是最终的物理地址了。


总结
   综上所述,也就是在Linux系统中,只需要考虑分页机制,而对于系统中使用的地址来说,则只需要区分逻辑地址与物理地址即可~~~
阅读(4176) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~