内存寻址(二):linux中的分段与分页机制

摘要:本文主要为你讲解linux中的分段和分页机制的实现原理,相关的宏定义和函数功能。


本文来源:内存寻址(二):linux中的分段与分页机制

1.linux中的分段


除了用来模拟80286的模式以外,段式基地址总是0(也就是说linux并没有真实地实现分段机制),所以线性地址和虚拟地址总是一样的。运行在所有用户态的linux进程都使用一对相同的段进行数据和指令的寻址,它们就是用户数据段和代码段;同理,存在内核数据段和代码段。这四个重要的段描述符的值是:


Segment Base G Limit S Type DPL D/B P
user code 0x00000000 1 0xfffff 1 10 3 1 1
user data 0x00000000 1 0xfffff 1 2 3 1 1
kernel code 0x00000000 1 0xfffff 1 10 0 1 1
kernel data 0x00000000 1 0xfffff 1 2 0 1 1


这样做的结果是,当对指令或者数据指针进行保存的时候,内核不需要为它设置逻辑地址的段选择符,因为cs寄存器含有当前的段选择符号。上面四个段的段选择符号,分别用宏定义表示:__USER_CS,__USER_DS,__KERNEL_CS,__KERNEL_DS。当发生内核切换或者相应的地址访问时,只需要把对应宏定义的数值装入cs中即可。


1.1 linux GDT

一个CPU对应一个GDT,多有的GDT都放在cpu_gdt_table数组之中,所有GDT的地址和它们的大小都存放在cpu_gdt_descr数组之中。


GDT的布局如下,其中每个GDT包含18个段描述符和14个空的(为了利用cache局部性原理)。

18个段描述符大致如下:

* 用户态和和心态代码和数据段共4个

* 任务状态段(TSS),每个处理器1个

* 等


1.2 linux LDT


大多数linux用户太程序不使用局部描述符表,内核定义了一个缺省的LDT,放在default_ldt数组中。


某些进行在有时候需要创建自己的局部描述符表,它通过系统调用modify_ldt系统调用来实现。当处理器开始执行拥有自定义ldt表的进程时,该cpuGDT副本中的LDT表项相应的就被修改了。


2.linux中的分页机制

最新linux分页模式(2.6kernel以后)

内存寻址(二):linux中的分段与分页机制

linux采用了一种同时适用于32位和64位的普通分页模型。2.6.10之前,linux一直采用三级分页模型,2.6.11以后,linux采用四级分页模型,如上图,其中每个部分的大小和具体的计算机体系结构有关。


对于没有启用PAE的系统,linux通过使得PUD和PMD的位都是0,虽然他们在地址中并不占有一定的位置,但是内核还是保留了它们的指针和表项。这样它们含有的项的数量为一,而且都映射到页全局目录的一个适当的目录项目。


PAE禁用时,offset是12b,table是10b,pgd是10b;PAE被**,table是9b,PMD是9b,PUD是0b,PGD是2b.


2.1线性地址字段

与页表处理相关的宏:


PAGE_SHIFT:

OFFSET 字段的位数,4k页对应12位,PAGE_MASK是oxfffff000,用于屏蔽相应字段

PMD_SHIFT:

offset+table的位数,PMD_SIZE用于计算页中间目录的一个单独表项所映射区域的大小,也就是一个页表的大小,PMD_MASK用于屏蔽PMD_SHIFT对应字段。注意在i386架构中,PAE启用与否对这个宏的值有影响,**以后它的值变为21.大型页往往不使用最后一级页表,所以大型页的LARGE_PAGE_SIZE=PMD_SIZE, LARGE_PAGE_MASK=PMD_MASK.

PUD_SHIFT:

页上级目录所能映射区域的大小的对数。PUD_SIZE用于计算页全局目录中一个单独表项所映射区域的大小,PUD_MASK用于屏蔽offset字段、table字段、middle字段和upper dir字段。因为upper dir恒为0位,所以在8086上,PUD_SHIFT总是等于PMD_SHIFT,而PUD_SIZE则等于4MB或者2MB。

PGDIR_SHIFT:

页全局目录中一个单独表项所能映射区域的大小。具体的机理可以参考物理地址扩展(PAE)分页机制,PAE的开启与否对这个宏的结果有影响,PAE开启的时候,它的值是12+9+9=30位,禁止的时候,它的值是12+10=22位。


PTRS_PER_PTE,PTRS_PER_PMD, PTRS_PER_PUD, PTRS_PER_PGD:

PAE禁止,分别为1024,1,1,1024;PAE**,分别为512,512,1,4。其中,PTRS_PER_PUD这一项的值恒是1.



2.2页表处理


内核定义了很多函数和宏来:描述页表项,修改页表项,读取页表项;读取页标志,设置页标志等;另外还有一些用来对页进行分配的函数和宏定义。


2.3物理内存布局


在初始化阶段,内核必须建立一个物理地址映射来指明哪些物理地址范围对于内核可用而哪些不可用。


内核将以下也况设置位保留:

* 不可用的物理地址范围内的页框

* 含有内核代码和已经初始化数据结构的页框


由于要给bios和其他程序预留地址空间,内核无法安装在内存的第一个MB(为了保持内核数据在内存在连续性),linux 2.6和前768个页框(3M)布局如下:

---------------------------------------------------------------------------------------

| **** | |********| | | | |

-----------------------------------------------------------------------------------------

0 1 0x9f 0x100 0x2ff

_text _etext _edata _end

其中,从左分别是:

不可用的页框

可用的页框

不可用的页框

内核代码

已经初始化的内核数据

没有初始化的内核数据

可用页框


2.4进程页表


注意:进程页表分为用户空间和系统空间,访问权限不同,使用0xc0000000作为分界线。


2.5内核页表


内核如何初始化自己的页表:创建有限的一共128K的地址空间+利用剩余的空间来建立页表。


1)临时内核页表


假设内核使用的段、临时页表和128KB的内存范围能够容纳在前8M空间,这需要2个页来映射。分页的第一阶段任务是在实模式和保护模式下能对这8M空间进行寻址。


2)RAM小于896M时候的最终内核页表


由内核页表所提供的最终映射必须把从0xc0000000开始的线性地址映射到从0开始的物理地址。其中宏__pa和__va分别进行相应区域的物理地址和线性地址之间的转换。


3)当RAM在896M和4096M之间的最终内核页表



4)当RAM大于4096MB时候的最终内核页表


此时,线性地址只有1G和RAM大于1G,此处的映射就可能涉及到PAE和高端内存,详细可以参考高端内存


2.6固定映射的线性地址


用于映射RAM的0~896M空间。


2.7cache和LTB


数据结构中最常使用的部分放在靠前的位置,同时尽量相邻,从而利用cache line。


cr3的更新往往也意味着TLB的刷新,下列情况除外:

1)两个共享页表集合的进程之间切换。

2)普通进程和内核进程之间切换。

另外,只要正在运行的进程页表集合发生变化,同样需要更新TLB。对于多处理器系统,还涉及到懒惰TLB策略。