分类: LINUX
2013-07-14 23:03:24
原文地址:linux内存寻址解析 作者:wangbaolin719
1.内存地址
1.逻辑地址:每一个逻辑地址都有一个段和偏移量组成。
2.线性地址:也叫虚拟地址,是一个32位无符号整数,可以用来表示高达4GB的地址,值得范围从0x00000000到0xffffffff。
3.物理地址:用于内存芯片级内存单元寻址
内存控制单元(MMU)将逻辑地址转化成线性地址,再转化成物理地址。
1.段选择符(段寄存器 )
?包括:索引、TI、RPL
?存放段描述符在段表中的索引号(段编号),TI标志用于说明是是GDT还是LDT表,RPL标志权限。
2.段描述符:段描述符表中的一项,表示一段的信息:
?段的基地址(Base Address):在线性地址空间中段的起始地址。
?段的界限(Limit):即段大小,在虚拟地址空间中,段内可以使用的最大偏移量。
?段的保护属性(Attribute): 表示段的特性。例如,该段是否可被读出或写入,或者该段是否作为一个程序来执行,以及段的特权级等等。
3.段描述符放在全局描述符表GDT,或局部描述符表LDT中。能够保存在GDT中的段描述符的最大数目是8191个。
4.段寻址过程(得到线性地址)
(1.判断指令类型,确定使用哪个段寄存器
(2.读段寄存器的内容,找到存放段描述信息的数据结构。(GDTR、LDTR)
①通过TI标志,判断本次操作所用段是到全局段描述表中找,还是到局部段描述表中找
②读GDTR或LDTR寄存器中存放的地址,找到描述表的首地址
③根据段寄存器中记录的索引号从描述表首址处偏移,找到第n个描述符,既是要找的段信息。
(3.得到基地址
(4.指令地址做偏移,判断是否长度越界
(5.根据指令性质及段描述符中的访问权限判断是否越权
(6.将基地址与指令中的偏移地址相加得到实际的内存地址,完成地址映射。
5.保护的表现
(1.界限保护
?越界判断:段长参数
?特权指令:新增的GDTR或LDTR寄存器不存在与旧指令兼容问题,访问他们的指令设定为特权指令,段寄存器的访问属性仍然同以往一样无特权,既保持了兼容,又保证了程序无法故意修改段描述进行越界。
(2.权限保护
?根据段的保护属性判断是否具有访问权限。如,只读段中不允许写入。
?系统态、用户态分离。段寄存器中的后两位RPL表示请求者操作要求的特权级。GDTR或LDTR中的dpl字段设定了段的访问权限。指令段rpl标志要求的权限应不低于dpl规定的级别。特权指令只能在系统态执行。
1.linux中的四个段
运行在用户态的所有linux进程使用一对相同的段来对指令和数据寻址,即用户代码段和用户数据段;运行在内核态的所有linux进程也使用一对相同的段来对指令和数据寻址,即内核代码段和内核数据段。
段 |
Base |
G |
Limit |
S |
Type |
DPL |
D/B |
P |
用户代码段 |
0x00000000 |
1 |
0xfffff |
1 |
10 |
3 |
1 |
1 |
用户数据段 |
0x00000000 |
1 |
0xfffff |
1 |
2 |
3 |
1 |
1 |
内核代码段 |
0x00000000 |
1 |
0xfffff |
1 |
10 |
0 |
1 |
1 |
内核数据段 |
0x00000000 |
1 |
0xfffff |
1 |
2 |
0 |
1 |
1 |
2.linux段机制
他们都是从0开始,即意味着用户态和内核态下的所有进程使用相同的逻辑地址。因为段机制就是将逻辑地址转化为线性地址,也就是说在linux中分段机制并没有起到实际的作用,只是走一下过程,而linux下的逻辑地址就等于线性地址。
3.GDT和LDT
每个cpu对应一个GDT,所有的GDT都存放在cpu_gdt_table数组中,而所有GDT的地址和他们的大小被存放在cpu_gdt_descr数组中。每个GDT包含18个段描述符和14个空的。
Linux中基本不使用LDT。
为了效率起见,线性地址分成以固定长度为单位的组,称为页。
分页单元把所有的RAM分成固定长度的页框(也叫物理页),每个页框包含一个页,也就是说一个页框的长度与一个页的长度一致。页框是主存的一部分,也是一个存储区域。页只是一个数据块,可以存放在任何页框或磁盘中。分页单元把线性地址转化成物理地址。
把线性地址映射到物理地址的数据结构称为页表。页表放在主存中,并在启用分页单元之前必须由内核对页表进行适当的初始化。
1.常规分页:
分页单元处理4KB的页。32位的线性地址分成3个域:目录(最高10位)、页表(中间10位)、偏移量(最低12位)。
每个活动的进程必须有一个分配给它的页目录。但没有必要马上为进程所有的页表都分配RAM。正在使用的页目录的物理地址存在寄存器cr3中。线性地址中的目录字段决定页目录中的目录项,而目录项指向适当的页表。地址的页表字段又决定页表中的表项,而表项含有页所在页框的物理地址。偏移量字段决定页框内的相对位置。每一页含有4096个字节。
页目录和页表都可以多大1024项,所以一个页目录可以寻址高达1024*1024*4096=2^32.这和32位地址所期望的一样。
或者下图:
2.扩展分页:
它允许页框大小为4MB。目录字段10位,偏移量字段为22位。
3.物理地址扩展(PAE)分页机制:
32位的物理地址,由于用户进程线性地址空间的需要,内核不能直接对1GB以上的RAM进行寻址。而许多服务器需要大于4GB的RAM来运行数以千计的进程。所以Intel将管教数增加到36,现在处理器的寻址能力为2^36=64GB.
4.硬件高速缓存:
高速缓存单元插在分页单元和内存之间。
在linux设置中,对于所有的页框都启用高速缓存,对于写操作总是采用回写策略。
5.转换后援缓冲器TLB:
除硬件缓存外,还采用TLB的高速缓存来用于加快线性地址的转换。
Linux分页机制的作用:分页机制是在段机制之后进行的,它进一步将线性地址转换为物理地址。我们先来看看硬件构造:
80386使用4K字节大小的页,且每页的起始地址都被4K整除。因此,早期80386把4GB字节线性地址空间划分为1M个页面,采用了两级表结构。
两级表的第一级表称为页目录,存储在一个4K字节的页中,页目录表共有1K个表项,每个表项为4个字节,线性地址最高的10位(22-31)用来产生第一级表索引,由该索引得到的表项中的内容定位了二级表中的一个表的地址,即下级页表所在的内存块号。
第二级表称为页表,存储在一个4K字节页中,它包含了1K字节的表项,每个表项包含了一个页的物理地址。二级页表由线性地址的中间10位(12-21)位进行索引,定位页表表项,获得页的物理地址。页物理地址的高20位与线性地址的低12位形成最后的物理地址。利用两级页表转换地址图:
80x86的分页机制由CR0中的PG位启用。如PG=1,启用分页机制,并使用本节要描述的机制,把线性地址转换为物理地址。如PG=0,禁用分页机制,直接把前面段机制产生的线性地址当作物理地址使用。
80386使用4K字节大小的页。每一页都有4K字节长,并在4K字节的边界上对齐,即每一页的起始地址都能被4K整除(物理地址最低12位为0)。因此,80386把4G字节的线性地址空间,划分为1G个页面,每页有4K字节大小。
分页机制通过把线性地址空间中的页,重新定位到物理地址空间来进行管理,因为每个页面的整个4K字节作为一个单位进行映射,并且每个页面都对齐4K字节的边界,因此,线性地址的低12位经过分页机制直接地作为物理地址的低12位使用。
线性/物理地址的转换,可将其意义扩展为允许将一个线性地址标记为无效,而不是实际地产生一个物理地址。有两种情况可能使页被标记为无效:其一是线性地址是操作系统不支持的地址;其二是在虚拟存储器系统中,线性地址对应的页存储在磁盘上,而不是存储在RAM存储器中。在前一种情况下,程序因产生了无效地址而必须被终止。
对于后一种情况,该无效的地址实际上是请求操作系统的虚拟存储管理系统,把存放在磁盘上的页传送到物理存储器中,使该页能被程序所访问。由于无效页通常是与虚拟存储系统相联系的,这样的无效页通常称为未驻留页,并且用页表属性位中叫做存在位的属性位进行标识。未驻留页是程序可访问的页,但它不在主存储器中。对这样的页进行访问,形式上是发生异常,实际上是通过异常进行缺页处理。
5.1 页全局目录
页全局目录表,最多可包含1024个页目录项,每个页目录项为4个字节,算起来正好一个页面,结构如图所示:
·第0位是存在位,Present标志:如果被置为1,所指的页(或页表)就在主存中;如果该标志为0,则这一页不在主存中,此时这个表项剩余的位可由操作系统用于自己的目的。如果执行一个地址转换所需的页表项或页目录项中Present标志被清0,那么分页单元就把该线性地址存放在控制寄存器cr2中,并产生14号异常:缺页异常。(我们将在后面的一系列博客中重点讨论Linux如何使用这个字段)。
·第1位是读/写位,第2位是用户/管理员位,Read/Write标志:含有页或页表的存取权限(Read/Write或Read);User/Supervisor标志:含有访问页或页表所需的特权级。这两位为页目录项提供硬件保护。当特权级为3的进程要想访问页面时,需要通过页保护检查,而特权级为0的进程就可以绕过页保护,如图所示:
·第3位是PWT(Page Write-Through)位,表示是否采用写透方式,写透方式就是既写内存(RAM)也写高速缓存,该位为1表示采用写透方式
·第4位是PCD(Page Cache Disable)位,表示是否启用高速缓存,该位为1表示启用高速缓存。
·第5位是访问位,Accessed标志:当对页目录项进行访问时,A位=1。每当分页单元对相应页框进行寻址时就设置这个标志。当选中的页被交换出去时,这一标志就可以由操作系统使用。分页单元从不重置这个标志;而是必须由操作系统去做。
·第6位Dirty标志,对于页全局目录项,其始终为1。
·第7位是Page Size标志,只适用于页目录项。如果置为1,页目录项指的是4MB的页面,请看后面的扩展分页。
·第8位是Global 标志:只应用于页表项。这个标志是在Pentium Pro引入的,用来防止常用页从TLB高速缓存中刷新出去。只有在cr4寄存器的页全局启用(Page GlobalEnable ,PGE)标志置位时这个标志才起作用。
·第9~11位由操作系统专用,Linux也没有做特殊之用
5.2 页表
80386的每个页目录项指向一个页表,页表最多含有1024个页面项,每项4个字节,包含页面的起始地址和有关该页面的信息。页面的起始地址也是4K的整数倍,所以页面的低12位也留作它用,如图所示。
第31~12位是20位物理页面地址,除第6位外第0~5位及9~11位的用途和页目录项一样,第6位是页面项独有的,当对涉及的页面进行写操作时,D位被置1。
4GB的存储器只有一个页目录,它最多有1024个页目录项,每个页目录项又含有1024个页面项,因此,存储器一共可以分成1024×1024=1M个页面。由于每个页面为4K个字节,所以,存储器的大小正好最多为4GB。
5.3 线性地址到物理地址的转换
当访问一个操作单元时,如何由分段结构确定的32位线性地址通过分页操作转化成32位物理地址呢?过程如图所示。
第一步,CR3包含着页目录的起始地址,用32位线性地址的最高10位A31~A22作为页目录的页目录项的索引,将它乘以4,与CR3中的页目录的起始地址相加,形成相应页表的地址。
第二步,从指定的地址中取出32位页目录项,它的低12位为0,这32位是页表的起始地址。用32位线性地址中的A21~A12位作为页表中的页面的索引,将它乘以4,与页表的起始地址相加,形成32位页面地址。
第三步,将A11~A0作为相对于页面地址的偏移量,与32位页面地址相加,形成32位物理地址。
下面,我们就通过一个实例来介绍一下常规分页是如何工作的。我们假定内核已给一个正在运行的进程分配的线性地址空间范围是0x20000000 到 0x2003ffff(3GB线性地址空间是一个上限,用户态进程只是引用其中的一个子集)。这个空间正好由64页面组成。其实我们并不必关心包含这些页的页框的物理地址,为什么呢?事实上,其中的一些页甚至可能不在主存中。我们只关注页表项中剩余的字段。
进程的线性地址的最高10位开始。这两个地址都以2开头后面跟着0,因此高10位有相同的值,即0x080或十进制的128。因此,这两个地址的页目录(Directory字段)都指向进程页目录的第129项。相应的目录项中必须包含分配给该进程的页表的物理地址。如果没有给这个进程分配其它的线性地址,页目录的其余1023项都填为0。
位的值(即Table字段的值)范围从0到0x03f,或十进制的从0到63。因而只有页表的前64个表项是有意义的,其余960表项都填0。
需要读线性地址0x20021406中的字节。这个地址由分页单元按下面的方法处理:
1. Directory字段的0x80用于选择页目录的第0x80目录项,此目录项指向和该进程的页相关的页表。
2. Table字段0x21用于选择页表的第0x21表项,此表项指向包含所需页的页框。
3. 最后,Offet字段0x406用于在目标页框中读偏移量为0x406中的字节。
如果页表第0x21表项的Present标志为0,则此页就不在主存中;在这种情况下,分页单元在线性地址转换的同时产生一个缺页异常。无论何时,当进程试图访问限定在0x20000000到0x2003ffff范围之外的线性地址时,都将产生一个缺页异常,因为这些页表项都填充了0,尤其是它们的Present标志都被清0。
当今,Linux采用了一种同时适用于32位和64位系统的普通分页模型。前面我们看到,两级页表对32位系统来说已经足够了,但64位系统需要更多数量的分页级别。直到2.6.10版本,Linux采用三级分页的模型。从2.6.11版本开始,采用了四级分页模型:
如上图所示:
图中展示的4种页表分别被称作:
? 页全局目录(Page Global Directory)
? 页上级目录(Page Upper Directory)
? 页中间目录(Page Middle Directory)
? 页表(Page Table)
页全局目录包含若干页上级目录的地址,页上级目录又依次包含若干页中间目录的地址,而页中间目录又包含若干页表的地址。每一个页表项指向一个页框。线性地址因此被分成五个部分。图中没有显示位数,因为每一部分的大小与具体的计算机体系结构有关。
对于没有启用物理地址扩展的32位系统,两级页表已经足够了。从本质上说Linux通过使“页上级目录”位和“页中间目录”位全为0,彻底取消了页上级目录和页中间目录字段。不过,页上级目录和页中间目录在指针序列中的位置被保留,以便同样的代码在32位系统和64位系统下都能使用。内核为页上级目录和页中间目录保留了一个位置,这是通过把它们的页目录项数设置为1,并把这两个目录项映射到页全局目录的一个合适的目录项而实现的。
启用了物理地址扩展的32 位系统使用了三级页表。Linux 的页全局目录对应80x86 的页目录指针表(PDPT),取消了页上级目录,页中间目录对应80x86的页目录,Linux的页表对应80x86的页表。
最终,64位系统使用三级还是四级分页取决于硬件对线性地址的位的划分。那么,为什么Linux是如此地热衷使用分页技术而对分段机制表现得那么地冷淡呢,因为Linux的进程处理很大程度上依赖于分页。事实上,线性地址到物理地址的自动转换使下面的设计目标变得可行:
? 给每一个进程分配一块不同的物理地址空间,这确保了可以有效地防止寻址错误。
? 区别页(即一组数据)和页框(即主存中的物理地址)之不同。这就允许存放在某个页框中的一个页,然后保存到磁盘上,以后重新装入这同一页时又被装在不同的页框中。这就是虚拟内存机制的基本要素。
每一个进程有它自己的页全局目录和自己的页表集。当发生进程切换时,Linux把cr3控制寄存器的内容保存在前一个执行进程的描述符中,然后把下一个要执行进程的描述符的值装入cr3寄存器中。因此,当新进程重新开始在CPU上执行时,分页单元指向一组正确的页表。
把线性地址映射到物理地址虽然有点复杂,但现在已经成了一种机械式的任务。本章下面的几节中列举了一些比较单调乏味的函数和宏,它们检索内核为了查找地址和管理叶表所需的信息;其中大多数函数只有一两行。也许现在你就想跳过这部分,但是知道这些函数和宏的功能是非常有用的,因为在以后章节的讨论中你会经常看到它们。
5.4 线性地址字段处理
下列宏简化了页表处理:
PAGE_SHIFT
指定Offset字段的位数;当用于80x86处理器时,它返回的值为12。由于页内所有地址都必须放在Offset字段, 因此80x86系统的页的大小是212 =4096字节。 PAGE_MASK宏产生的值为0xfffff000,用以屏蔽Offset字段的所有位。
PMD_SHIFT
指定线性地址的Offset和Table字段的总位数;换句话说,是页中间目录项可以映射的区域大小的对数。PMD_SIZE 宏用于计算由页中间目录的一个单独表项所映射的区域大小,也就是一个页表的大小。PMD_MASK宏用于屏蔽Offset字段与Table字段的所有位。当PAE 被禁用时,PMD_SHIFT 产生的值为22(来自Offset 的12 位加上来自Table 的10 位),PMD_SIZE 产生的值为222 或 4 MB, PMD_MASK产生的值为 0xffc00000。相反,当PAE被激活时,PMD_SHIFT 产生的值为21 (来自Offset的12位加上来自Table的9位), PMD_SIZE 产生的值为221 或2 MB以及PMD_MASK产生的值为 0xffe00000。大型页不使用最后一级页表,所以产生大型页尺寸的LARGE_PAGE_SIZE 宏等于PMD_SIZE(2PMD_SHIFT),而在大型页地址中用于屏蔽Offset字段和Table字段的所有位的LARGE_PAGE_MASK宏,就等于PMD_MASK。
PUD_SHIFT
确定页上级目录项能映射的区域大小的对数。PUD_SIZE宏用于计算页全局目录中的一个单独表项所能映射的区域大小。PUD_MASK宏用于屏蔽Offset字段,Table字段,Middle Air字段和Upper Air字段的所有位。在80x86处理器上,PUD_SHIFT总是等价于PMD_SHIFT,而PUD_SIZE则等于4MB或2MB。
PGDIR_SHIFT
确定页全局页目录项能映射的区域大小的对数。 PGDIR_SIZE宏用于计算页全局目录中一个单独表项所能映射区域的大小。PGDIR_MASK宏用于屏蔽Offset, Table,Middle Air及Upper Air的所有位。当PAE 被禁止时,PGDIR_SHIFT 产生的值为22(与PMD_SHIFT 和PUD_SHIFT 产生的值相同),PGDIR_SIZE 产生的值为 222 或 4 MB,以及 PGDIR_MASK 产生的值为 0xffc00000。相反,当PAE被激活时,PGDIR_SHIFT 产生的值为30 (12 位Offset 加 9 位Table再加 9位 Middle Air), PGDIR_SIZE 产生的值为230 或 1 GB以及PGDIR_MASK产生的值为0xc0000000。
PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD以及PTRS_PER_PGD
用于计算页表、页中间目录、页上级目录和页全局目录表中表项的个数。当PAE被禁止时,它们产生的值分别为1024,1,1和1024。当PAE被激活时,产生的值分别为512,512,1和4。
5.5 页表处理
pte_t、pmd_t、pud_t和 pgd_t分别描述页表项、页中间目录项、页上级目录和页全局目录项的类型格式。当PAE被激活时它们都是64位的数据类型,否则都是32位数据类型。pgprot_t是另一个64位(PAE激活时)或32位(PAE禁用时)的数据类型,它表示与一个单独表项相关的保护标志。
五个类型转换宏(__ pte、__ pmd、__ pud、__ pgd和__
pgprot)把一个无符号整数转换成所需的类型。另外的五个类型转换宏(pte_val,pmd_val, pud_val, pgd_val和pgprot_val)执行相反的转换,即把上面提到的四种特殊的类型转换成一个无符号整数。
内核还提供了许多宏和函数用于读或修改页表表项:
? 如果相应的表项值为0,那么,宏pte_none、pmd_none、pud_none和 pgd_none产生的值为1,否则产生的值为0。
? 宏pte_clear、pmd_clear、pud_clear和 pgd_clear清除相应页表的一个表项,由此禁止进程使用由该页表项映射的线性地址。ptep_get_and_clear( )函数清除一个页表项并返回前一个值。
? set_pte,set_pmd,set_pud和set_pgd向一个页表项中写入指定的值。set_pte_atomic与set_pte作用相同,但是当PAE被激活时它同样能保证64位的值能被原子地写入。
? 如果a和b两个页表项指向同一页并且指定相同访问优先级,pte_same(a,b)返回1,否则返回0。
? 如果页中间目录项指向一个大型页(2MB或4MB),pmd_large(e)返回1,否则返回0。
宏pmd_bad由函数使用并通过输入参数传递来检查页中间目录项。如果目录项指向一个不能使用的页表,也就是说,如果至少出现以下条件中的一个,则这个宏产生的值为1:
? 页不在主存中(Present标志被清除)。
? 页只允许读访问(Read/Write标志被清除)。
? Acessed或者Dirty位被清除(对于每个现有的页表,Linux总是强制设置这些标志)。
pud_bad宏和pgd_bad宏总是产生0。没有定义pte_bad宏,因为页表项引用一个不在主存中的页,一个不可写的页或一个根本无法访问的页都是合法的。
如果一个页表项的Present标志或者Page Size标志等于1,则pte_present宏产生的值为1,否则为0。前面讲过页表项的Page Size标志对微处理器的分页部件来讲没有意义,然而,对于当前在主存中却又没有读、写或执行权限的页,内核将其Present和Page Size分别标记为0和1。这样,任何试图对此类页的访问都会引起一个缺页异常,因为页的Present标志被清0,而内核可以通过检查Page Size的值来检测到产生异常并不是因为缺页。
如果相应表项的Present标志等于1,也就是说,如果对应的页或页表被装载入主存,pmd_present宏产生的值为1。pud_present宏和pgd_present宏产生的值总是1。
下表中列出的函数用来查询页表项中任意一个标志的当前值;除了pte_file()外,其他函数只有在pte_present返回1的时候,才能正常返回页表项中任意一个标志。
函数名称 |
说明 |
pte_user( ) |
读 User/Supervisor 标志。 |
pte_read( ) |
读 User/Supervisor 标志(表示 80x86 处理器上的页不受读的保护)。 |
pte_write( ) |
读 Read/Write 标志。 |
pte_exec( ) |
读 User/Supervisor 标志( 80x86 处理器上的页不受代码执行的保护)。 |
pte_dirty( ) |
读 Dirty 标志。 |
pte_young( ) |
读 Accessed 标志。 |
pte_file( ) |
读 Dirty 标志(当 Present 标志被清除而 Dirty 标志被设置时,页属于一个非线性磁盘文件映射)。 |
下表列出的另一组函数用于设置页表项中各标志的值:
函数名称 |
说明 |
mk_pte_huge( ) |
设置页表项中的 Page Size 和 Present 标志。 |
pte_wrprotect( ) |
清除 Read/Write 标志。 |
pte_rdprotect( ) |
清除 User/Supervisor 标志。 |
pte_exprotect( ) |
清除 User/Supervisor 标志。 |
pte_mkwrite( ) |
设置 Read/Write 标志。 |
pte_mkread( ) |
设置 User/Supervisor 标志。 |
pte_mkexec( ) |
设置 User/Supervisor 标志。 |
pte_mkclean( ) |
清除 Dirty 标志。 |
pte_mkdirty( ) |
设置 Dirty 标志。 |
pte_mkold( ) |
清除 Accessed 标志(把此页标记为未访问)。 |
pte_mkyoung( ) |
设置 Accessed 标志(把此页标记为访问过)。 |
pte_modify(p,v) |
把页表项 p 的所有访问权限设置为指定的值 v 。 |
ptep_set_wrprotect() |
与 pte_wrprotect( ) 类似,但作用于指向页表项的指针。 |
ptep_set_access_flags( ) |
如果 Dirty 标志被设置为 1 则将页的访问权设置为指定的值,并调用 flush_tlb_page() 函数。 |
ptep_mkdirty( ) |
与 pte_mkdirty( ) 类似,但作用于指向页表项的指针。 |
ptep_test_and_clear_dirty( ) |
与 pte_mkclean( ) 类似,但作用于指向页表项的指针并返回 Dirty 标志的旧值。 |
ptep_test_and_clear_young( ) |
与 pte_mkold( ) 类似,但作用于指向页表项的指针并返回 Accessed 标志的旧值。 |
现在,我们来讨论下表中列出的宏,它们把一个页地址和一组保护标志组合成页表项,或者执行相反的操作,从一个页表项中提取出页地址。请注意这其中的一些宏对页的引用是通过 “页描述符”的线性地址,而不是通过该页本身的线性地址。
宏名称 |
说明 |
pgd_index(addr) |
找到线性地址 addr 对应的的目录项在页全局目录中的索引(相对位置)。 |
pgd_offset(mm, addr) |
接收内存描述符地址 mm 和线性地址 addr 作为参数。这个宏产生地址 addr 在页全局目录中相应表项的线性地址;通过内存描述符 mm 内的一个指针可以找到这个页全局目录。 |
pgd_offset_k(addr) |
产生主内核页全局目录中的某个项的线性地址,该项对应于地址 addr 。 |
pgd_page(pgd) |
通过页全局目录项 pgd 产生页上级目录所在页框的页描述符地址。在两级或三级分页系统中,该宏等价于 pud_page() ,后者应用于页上级目录项。 |
pud_offset(pgd, addr) |
参数为指向页全局目录项的指针 pgd 和线性地址 addr 。这个宏产生页上级目录中目录项 addr 对应的线性地址。在两级或三级分页系统中,该宏产生 pgd ,即一个页全局目录项的地址。 |
pud_page(pud) |
通过页上级目录项 pud 产生相应的页中间目录的线性地址。在两级分页系统中,该宏等价于 pmd_page() ,后者应用于页中间目录项。 |
pmd_index(addr) |
产生线性地址 addr 在页中间目录中所对应目录项的索引(相对位置)。 |
pmd_offset(pud, addr) |
接收指向页上级目录项的指针 pud 和线性地址 addr 作为参数。这个宏产生目录项 addr 在页中间目录中的偏移地址。在两级或三级分页系统中,它产生 pud ,即页全局目录项的地址。 |
pmd_page(pmd) |
通过页中间目录项 pmd 产生相应页表的页描述符地址。在两级或三级分页系统中, pmd 实际上是页全局目录中的一项。 |
mk_pte(p,prot) |
接收页描述符地址 p 和一组访问权限 prot 作为参数,并创建相应的页表项。 |
pte_index(addr) |
产生线性地址 addr 对应的表项在页表中的索引(相对位置)。 |
pte_offset_kernel(dir,addr) |
线性地址 addr 在页中间目录 dir 中有一个对应的项,该宏就产生这个对应项,即页表的线性地址。另外,该宏只在主内核页表上使用。 |
pte_offset_map(dir, addr) |
接收指向一个页中间目录项的指针 dir 和线性地址 addr 作为参数,它产生与线性地址 addr 相对应的页表项的线性地址。如果页表被保存在高端存储器中,那么内核建立一个临时内核映射,并用 pte_unmap 对它进行释放。 pte_offset_map_nested 宏和 pte_unmap_nested 宏是相同的,但它们使用不同的临时内核映射。 |
pte_page( x ) |
返回页表项 x 所引用页的描述符地址。 |
pte_to_pgoff( pte ) |
从一个页表项的 pte 字段内容中提取出文件偏移量,这个偏移量对应着一个非线性文件内存映射所在的页。 |
pgoff_to_pte(offset ) |
为非线性文件内存映射所在的页创建对应页表项的内容。 |
下面我们罗列最后一组函数来简化页表项的创建和撤消。当使用两级页表时,创建或删除一个页中间目录项是不重要的。如本节前部分所述,页中间目录仅含有一个指向下属页表的目录项。所以,页中间目录项只是页全局目录中的一项而已。然而当处理页表时,创建一个页表项可能很复杂,因为包含页表项的那个页表可能就不存在。在这样的情况下,有必要分配一个新页框,把它填写为 0 ,并把这个表项加入。
如果 PAE 被激活,内核使用三级页表。当内核创建一个新的页全局目录时,同时也分配四个相应的页中间目录;只有当父页全局目录被释放时,这四个页中间目录才得以释放。当使用两级或三级分页时,页上级目录项总是被映射为页全局目录中的一个单独项。与以往一样,下表中列出的函数描述是针对 80x86 构架的。
函数名称 |
说明 |
pgd_alloc( mm ) |
分配一个新的页全局目录。如果 PAE 被激活,它还分配三个对应用户态线性地址的子页中间目录。参数 mm( 内存描述符的地址 ) 在 80x86 构架上被忽略。 |
pgd_free( pgd) |
释放页全局目录中地址为 pgd 的项。如果 PAE 被激活,它还将释放用户态线性地址对应的三个页中间目录。 |
pud_alloc(mm, pgd, addr) |
在两级或三级分页系统下,这个函数什么也不做:它仅仅返回页全局目录项 pgd 的线性地址。 |
pud_free(x) |
在两级或三级分页系统下,这个宏什么也不做。 |
pmd_alloc(mm, pud, addr) |
定义这个函数以使普通三级分页系统可以为线性地址 addr 分配一个新的页中间目录。如果 PAE 未被激活,这个函数只是返回输入参数 pud 的值,也就是说,返回页全局目录中目录项的地址。如果 PAE 被激活,该函数返回线性地址 addr 对应的页中间目录项的线性地址。参数 mm 被忽略。 |
pmd_free(x) |
该函数什么也不做,因为页中间目录的分配和释放是随同它们的父全局目录一同进行的。 |
pte_alloc_map(mm, pmd, addr) |
接收页中间目录项的地址 pmd 和线性地址 addr 作为参数,并返回与 addr 对应的页表项的地址。如果页中间目录项为空,该函数通过调用函数 pte_alloc_one( ) 分配一个新页表。如果分配了一个新页表, addr 对应的项就被创建,同时 User/Supervisor 标志被设置为 1 。如果页表被保存在高端内存,则内核建立一个临时内核映射,并用 pte_unmap 对它进行释放。 |
pte_alloc_kernel(mm, pmd, addr) |
如果与地址 addr 相关的页中间目录项 pmd 为空,该函数分配一个新页表。然后返回与 addr 相关的页表项的线性地址。该函数仅被主内核页表使用。 |
pte_free(pte) |
释放与页描述符指针 pte 相关的页表。 |
pte_free_kernel(pte) |
等价于 pte_free( ) ,但由主内核页表使用。 |
clear_page_range(mmu, start,end) |
从线性地址 start 到 end 通过反复释放页表和清除页中间目录项来清除进程页表的内容。 |
上图反映了如下信息:
1、 进程的4G 线性空间被划分成三个部分:进程空间(0-3G)、内核直接映射空间(3G – high_memory)、内核动态映射空间(VMALLOC_START - VMALLOC_END)
2、 三个空间使用同一张页目录表,通过 CR3 可找到此页目录表。但不同的空间在页目录表中页对应不同的项,因此互相不冲突
3、内核初始化以后,根据实际物理内存的大小,计算出 high_memory、VMALLOC_START、VMALLOC_END 的值。并为“内核直接映射”空间建立好映射关系,所有的物理内存都可以通过此空间进行访问。
4、“进程空间”和“内核动态映射空间”的映射关系是动态建立的(通过缺页异常)
假设在有三个线性地址 addr1, addr2, addr3 ,分别属于三个线性空间,但是最终都映射到物理页面1:
1、 三个地址对应不同的页表和页表项
2、 但是页表项的高 20bit 肯定是1,表示物理页面的索引号是1
3、 同时,根据高 20 bit,可以从 mem_map[] 中找到对应的 struct page 结构,struct page 用于管理实际的物理页面(红线)
4、 从线性地址,根据页目录表,页表,可以找到物理地址
5、 和物理地址之间很容易互相转换
6、 从物理地址,可以很容易的反推出在内核直接映射空间的线性地址(蓝线)。内核空间的虚拟地址和物理地址相差3G,而要想得到在进程空间或者内核动态映射空间的对应的线性地址,则需要遍历相应的“虚存区间”链表。
关于页目录表:
1、 每个进程有一个属于自己的页目录表,可通过 CR3 寄存器找到
2、 而内核也有一个独立于其它进程的页目录表,保存在 swapper_pg_dir[] 数组中
3、 当进程切换的时候,只需要将新进程的页目录把地址加载到 CR3 寄存器中即可
4、 创建一个新进程的时候,需要为它分配一个 page,作为页目录表,并将 swapper_pg_dir[] 的高 256 项拷贝过来,低 768 项则清0
用户空间不是进程共享的,而是进程隔离的。每个进程最大都可以有3GB的用户空间。一个进程对其中一个地址的访问,与其它进程对于同一地址的访问绝不冲突。比如,一个进程从其用户空间的地址0x1234ABCD处可以读出整数8,而另外一个进程从其用户空间的地址0x1234ABCD处可以读出整数20,这取决于进程自身的逻辑。
任意一个时刻,在一个CPU上只有一个进程在运行。所以对于此CPU来讲,在这一时刻,整个系统只存在一个4GB的虚拟地址空间,这个虚拟地址空间是面向此进程的。当进程发生切换的时候,虚拟地址空间也随着切换。由此可以看出,每个进程都有自己的虚拟地址空间,只有此进程运行的时候,其虚拟地址空间才被运行它的CPU所知。在其它时刻,其虚拟地址空间对于CPU来说,是不可知的。所以尽管每个进程都可以有4 GB的虚拟地址空间,但在CPU眼中,只有一个虚拟地址空间存在。虚拟地址空间的变化,随着进程切换而变化。
从上面我们知道,一个程序编译连接后形成的地址空间是一个虚拟地址空间,但是程序最终还是要运行在物理内存中。因此,应用程序所给出的任何虚地址最终必须被转化为物理地址,所以,虚拟地址空间必须被映射到物理内存空间中,这个映射关系需要通过硬件体系结构所规定的数据结构来建立。这就是我们所说的段描述符表和页表,Linux主要通过页表来进行映射。
于是,我们得出一个结论,如果给出的页表不同,那么CPU将某一虚拟地址空间中的地址转化成的物理地址就会不同。所以我们为每一个进程都建立其页表,将每个进程的虚拟地址空间根据自己的需要映射到物理地址空间上。既然某一时刻在某一CPU上只能有一个进程在运行,那么当进程发生切换的时候,将页表也更换为相应进程的页表,这就可以实现每个进程都有自己的虚拟地址空间而互不影响。所以,在任意时刻,对于一个CPU来说,只需要有当前进程的页表,就可以实现其虚拟地址到物理地址的转化。
内核空间对所有的进程都是共享的,其中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据,不管是内核程序还是用户程序,它们被编译和连接以后,所形成的指令和符号地址都是虚地址,而不是物理内存中的物理地址。
虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000)开始的,之所以这么规定,是为了在内核空间与物理内存之间建立简单的线性映射关系。其中,3GB(0xC0000000)就是物理地址与虚拟地址之间的位移量,在Linux代码中就叫做PAGE_OFFSET。
我们来看一下在include/asm/i386/page.h头文件中对内核空间中地址映射的说明及定义:
#define
__PAGE_OFFSET
(0xC0000000)
……
#define
PAGE_OFFSET
((unsigned long)__PAGE_OFFSET)
#define
__pa(x)
((unsigned long)(x)-PAGE_OFFSET)
#define
__va(x)
((void *)((unsigned long)(x)+PAGE_OFFSET))
对于内核空间而言,给定一个虚地址x,其物理地址为“x-
PAGE_OFFSET”,给定一个物理地址x,其虚地址为“x+ PAGE_OFFSET”。
这里再次说明,宏__pa()仅仅把一个内核空间的虚地址映射到物理地址,而决不适用于用户空间,用户空间的地址映射要复杂得多,它通过分页机制完成。