Linux 内存寻址
内存地址分类
逻辑地址:机器语言指令中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段(segment)和偏移量(offset或displacement)组成,偏移量指明了从段开始的地方到实际地址之间的距离。
线性地址(或 虚拟地址):一个32位(或64位)无符号整数,在32位系统中可以用来表示高达4GB(0x0000 0000 —— 0xffff ffff)的地址,也就是高达 4 * 1024 * 1024 * 1024个内存单元(字节)。
物理地址(physical address):芯片级内存单元寻址。与从微处理器的地址引脚发送到内存总线上的电信号相对应。物理地址由32位或36位(开启PAE)无符号整数表示。
内存管理单元(MMU)通过分段单元(segmentation unit)把逻辑地址转换成线性地址;然后,通过分页单元(paging unit)把线性地址转换成物理地址。分段单元和分页单元都是一种硬件电路。
硬件中的分段
段选择符和段寄存器
逻辑地址由两部分组成:段选择符和指定段内相对地址的偏移量。段选择符(Segment Selector)是一个16位长的字段,而偏移量是一个32位长的字段。
字段名 | 描述 |
---|---|
索引 | 指定了放在GDT或LDT中的相应段描述符 |
TI | TI(Table Indicator)标志,指明段描述符是在GDT中(TI=0)或在LDT中(TI=1) |
RPL | 请求者特权级,当相应的段选择符装入到cs寄存器中时指示出CPU当前的特权级,它还可以用于在访问数据段时有选择地削弱处理器的特权级 |
处理器提供段寄存器来存放段选择符以保证查找段选择符的效率。这些段寄存器称为cs, ss, ds, es, fs和gs。程序可以把同一个段寄存器用于不同的目的:先将其值保存在内存中,用完后再恢复。6个段寄存器中3个有专门的用途:
-
cs 代码段寄存器,指向包含程序指令的段。
-
ss 栈段寄存器,指向包含当前程序栈的段。
-
ds 数据段寄存器,指向包含静态数据或者全局数据段(初始化数据)。
其他3个段寄存器作一般用途,可以指向任意的数据段。cs寄存器还有一个很重要的功能:它含有一个 两位的字段,用以指明CPU的 当前特权级(Current Privilege Level, CPL)。0代表最高优先级——内核态,而3代表最低优先级——用户态。
段描述符
每个段由一个 8字节(64 bit) 的段描述符(Segment Descriptor)表示,它描述了段的特征。段描述符放在全局描述符表(Global Descriptor Table, GDT)或局部描述符表(Local Descriptor Table, LDT)中。GDT在主存中的地址和大小存放在gdtr控制寄存器中,当前正被使用的LDT地址和大小放在ldtr控制寄存器中。
字段名 | 描述 |
---|---|
基地址(Base) | 包含段的首字节的线性地址 (32 bit) |
G | 粒度标志;置0,则段大小以字节为单位,否则以4096字节的倍数计 |
Limit | 最大段偏移量,段的长度(20 bit)。如果G被置为0,则一个段的大小在1个字节到1MB之间变化;否则,将在4KB到4GB之间变化 |
S | 系统标志;置0,系统段,存储诸如LDT这种关键的数据结构,否则它是一个普通的代码段或数据段 |
Type | 描述了段的类型特征和它的存取权限 |
DPL | 描述符特权级(Descriptor Privilege Level)字段;用于限制对这个段的存取。表示访问这个段要求的CPU最小的优先级 |
P | Segment-Present标志;为0表示段当前不在主存中。Linux总是把这个标志(第47位)设为1,因为它从来不把整个段交换到磁盘上去 |
D或B | 取决于是代码段还是数据段 |
AVL | 操作系统使用,但被Linux忽略 |
为加速逻辑地址到线性地址的转换,80x86处理器提供一种附加的非编程的寄存器(不能被编程者设置的寄存器),供6个可编程的段寄存器使用。每一个非编程的寄存器含有8个字节的段描述符,由相应的段寄存器中的段选择符来指定。每当一个段选择符被装入段寄存器时,相应的段描述符就由内存装入到对应的非编程CPU寄存器。之后,针对那个段的逻辑地址转换就可以不访问主存中的GDT或LDT,处理器只需直接引用存放段描述符的CPU寄存器即可。仅当段寄存器的内容改变时,才有必要访问GDT或LDT。
分段单元
下图显示一个逻辑地址转换的详细过程,分段单元(segmentation unit)执行以下操作:
-
先检查段选择符的TI字段,以决定段描述符保存在哪一个描述符表中。GDT中,分段单元从gdtr寄存器得到GDT的线性基地址;LDT中,分段单元从ldtr寄存器得到LDT的线性基地址。
-
从段选择符的index字段计算段描述符的地址,index字段的值乘以8(一个段描述符的大小),这个结果与gdtr或ldtr寄存器中的内容相加。
-
把逻辑地址的偏移量与段描述符Base字段的值相加就得到了线性地址。
有了与段寄存器相关的不可编程寄存器,只有当段寄存器的内容被改变时才需要执行前两个操作。
Linux中的分段
2.6版的Linux只有在x86结构下才需要分段。
运行在用户态的所有Linux进程都使用一对相同的段来对指令和数据寻址。这两个段就是所谓的用户代码段和用户数据段。类似地,运行在内核态的所有Linux进程都使用一对相同的段对指令和数据寻址:内核代码段和内核数据段。
下表显示了这4个重要段的段描述符字段的值:
段 | Base | G | Limit | S | Type | DPL | D/B | p |
---|---|---|---|---|---|---|---|---|
用户代码段 | 0x0000 0000 | 1 | 0xfffff | 1 | 10 | 3 | 1 | 1 |
用户数据段 | 0x0000 0000 | 1 | 0xfffff | 1 | 2 | 3 | 1 | 1 |
内核代码段 | 0x0000 0000 | 1 | 0xfffff | 1 | 10 | 0 | 1 | 1 |
内核数据段 | 0x0000 0000 | 1 | 0xfffff | 1 | 2 | 0 | 1 | 1 |
G为1,粒度为4KB,Limit为 0xfffff,则空间为 4GB
相应的段选择符由宏定义。
__USER_CS、__USER_DS、__KERNEL_CS、__KERNEL_DS
为了对内核代码段寻址,内核只需把__KERNEL_CS宏产生的值装进cs段寄存器即可。
注意,与段相关的线性地址从0开始,达到2^23 - 1的寻址限长。这就意味着在用户态或内核态下的所有进程可以使用相同的逻辑地址。
所有段都从0x0000 0000 开始,那么,在Linux下逻辑地址与线性地址是一致的,即逻辑地址的偏移量字段的值与相应的线性地址的值总是一致的。
当对指向指令或者数据结构的指针进行保存时,内核不需要为其设置逻辑地址的段选择符,因为cs寄存器就含有当前的段选择符。例如,当内核调用一个函数时,它执行一条call汇编语言指令,该指令仅指定其逻辑地址的偏移量部分,而段选择符不用设置,它已经隐含在cs寄存器中了。因为“在内核态执行”的段只有一种,叫做代码段,由宏__KERNEL_CS定义,所以只要当CPU切换到内核态时将__KERNEL_CS装载进cs就足够了。同样的道理也适用于指向内核数据结构的指针(隐含地使用ds寄存器)以及指向用户数据结构的指针(内核显式地使用es寄存器)。
Linux GDT
在单处理器系统中只有一个GDT,而在多处理器系统中每个CPU对应一个GDT。所有的GDT都存放在cpu_gdt_table数组中,而所有GDT的地址和它们的大小(当初始化gdtr寄存器时使用)被存放在cpu_gdt_descr数组中。这些符号都在文件arch/i386/kernel/head.S中被定义。
下图是GDT的布局示意图。每个GDT包含18个段描述符和14个空的,未使用的,或保留的项。插入未使用的项的目的是为了使经常一起访问的描述符能够处于同一个32字节的硬件高速缓存行中。
每一个GDT中包含的18个段描述符指同下列的段:
-
用户态和内核态下的代码段和数据段,共4个。
-
任务状态段(TSS),每个处理器有1个。每个TSS相应的线性地址空间都是内核数据段相应线性地址空间的一个小子集。所有的任务状态段都顺序地存放在init_tss数组中,值得特别说明的是,第n个CPU的TSS描述符的Base字段指向init_tss数组的第n个元素。G(粒度)标志被清0,而Limit字段置为0xeb, 因为TSS段是236字节长。Type字段置为9或11(可用的32位TSS),且DPL置 为0,因为不允许用户态下的进程访问TSS段。
-
1个包括缺省局部描述符表的段,这个段通常被所有进程共享。
-
3个局部线程存储(Thread-Local Storage, TLS)段:这种机制允许多线程应用程序使用最多3个局部于线程的数据段。系统使用set_thread_area()和get_thread_area()分别为正在执行的进程创建和撤销一个TLS段。
-
与高级电源管理(APM)相关的3个段:由于BIOS代码使用段,所以当Linux APM驱动程序调用BIOS函数来获取或者设置APM设备的状态时,就可以使用自定义的代码段和数据段。
-
与支持即插即用(PnP)功能的BIOS服务程序相关的5个段。
-
被内核用来处理“双重错误”异常(处理一个异常时可能会引发另一个异常)的特殊TSS段。
系统中每个处理器都有一个GDT副本。除少数几种情况外,所有GDT的副本都存放相同的表项:
-
每个处理器都有它自己的TSS段。
-
GDT中只有少数项可能依赖于CPU正在执行的进程(LDT和TLS段描述符)。
-
在某些情况下,处理器可能临时修改GDT副本里的某个项,例如,当调用APM的BIOS例程时就会发生这种情况。
Linux LDT
大多数用户态下的Linux程序不使用局部描述符表,因此内核就定义了一个缺省的LDT供大多数进程共享。缺省的局部描述符表存放在default_ldt数组中。它包含5个项,但内核仅仅有效地使用了其中的两个项:用于iBCS执行文件的调用门和Solaris/x86可执行文件的调用门。调用门是80x86微处理器提供的一种机制,用于在调用预定义函数时改变CPU的特权级(参考Intel文档以获取更多详情)。
硬件中的分页
分页单元(paging unit)把线性地址转换成物理地址。其中的一个关键任务是把所请求的访问类型与线性地址的访问权限相比较,如果这次内存访问是无效的,就产生一个缺页异常。
为了效率起见,线性地址被分成以固定长度为单位的组,称为页(page)。页内部连续的线性地址被映射到连续的物理地址中。这样,内核可以指定一个页的物理地址和其存取权限,而不用指定页所包含的全部线性地址的存取权限。我们遵循通常习惯,使用术语“页”既指一组线性地址,又指包含在这组地址中的数据。
分页单元把所有的RAM分成固定长度的叶框(page frame)(也叫做物理页)。每一个叶框包含一个页,也就是说叶框的长度与一个页的长度一致。页框是主存的一部分,因此也是一个存储区域。区分一页和一个页框是很重要的,前者只是一个数据块,可以存放在任何页框或磁盘中。
把线性地址映射到物理地址的数据结构称为页表(page table )。页表存放在主存中,并在启用分页单元之前必须由内核对页表进行适当的初始化。
从80386开始,所有的80x86处理器都支持分页,它通过设置cr0寄存器的PG标志启用。当PG=0时,线性地址就被解释成物理地址。<需要了解控制寄存器(cr0~cr3)的结构及作用>
常规分页
从80386起,Intel处理器的分页单元处理4KB的页。32位的线性地址被分成3个域:
-
Directory(目录):最高10位
-
Table(页表):中间10位
-
Offset(偏移量):最低12位
线性地址的转换分两步完成,每一步都基于一种转换表,第一种转换表称为页目录表(page directory),第二种转换表称为页表(page table )。
页目录 及 页表都分别存放在1个页中(4KB),其中每个表项也都是4个字节。
使用这种二级模式的目的在于减少每个进程页表所需RAM的数量。如果使用简单的一级页表,那将需要高达2^20个表项(4GB/4KB = 2^20 ,也就是,在每项4个字节时,需要4MB RAM)来表示每个进程的页表(如果进程使用全部4GB线性地址空间),即使一个进程并不使用那个范围内的所有地址。二级模式通过只为进程实际使用的那些“虚拟内存区”请求页表来减少内存容量。
每个活动进程必须有一个分配给它的页目录。不过,没有必要马上为进程的所有页表都分配RAM。只有在进程实际需要一个页表时才给该页表分配RAM会更为有效率。
正在使用的页目录的物理地址存放在控制寄存器cr3中。
页目录项和页表项有相同的结构,每项都包含下面的字段:
字段 | 描述 |
---|---|
Present标志 | 置为1,所指的页(或页表)就在主存中;为0,则这一页不在主存,此时这个表项剩余的位可由操作系统用于自己的目的。如果只需一个地址转换所需的页表项或页目录项中Present标志被清0,那么分页单元就把该线性地址存放在控制寄存器cr2中,并产生14号异常:缺页异常。 |
包含页框物理地址最高20位的字段 | 由于每一个页框有4KB的容量,它的物理地址必须是4096的倍数,因此物理地址的最低12位总是为0。若这个字段指向一个页目录,相应的页框就含有一个页表,若指向一个页表,相应的页框就含有一页数据。 |
Accessed标志 | 每当分页单元对相应页框进行寻址时就设置这个标志。当选中的页被交换出去时,这一标志由操作系统使用。分页单元从来不重置这个标志,而是必须由操作系统去做。 |
Dirty标志 | 只应用于页表项中。每当对一个页框进行写操作时就设置这个标志。与Accessed标志一样,“当选中…………系统去做”。 |
Read/Write标志 | 含有页或页表的存取权限。 |
User/Supervisor标志 | 含有访问页或页表所需的特权级。 |
PCD和PWT标志 | 控制硬件高速缓存处理页或页表的方式。 |
Page Size标志 | 只应用于页目录项。置为1,则页目录指的是2MB或4MB的页框。 |
Global标志 | 只应用于页表项。这个标志是在Pentium Pro中引入的,用来防止常用页从TLB(俗称“快表”)高速缓存中刷新出去。只有在cr4寄存器的页全局启用(Page Global Enable, PGE)标志置位时这个标志才起作用。 |
扩展分页
从Pentium模型开始,80x86微处理器引入了扩展分页(extended paging),它允许页框大小为4MB而不是4KB。扩展分页用于把大段连续的线性地址转换成相应的物理地址,在这些情况下,内核可以不用中间页表进行地址转换,从而节省内存并保留TLB项。
通过设置页目录项的Page Size标志启用扩展分页功能。分页单元吧32位线性地址分成两个字段:
-
Directory:最高10位
-
Offset:其余22位
扩展分页和正常分页的目录项基本相同,除了:
-
Page Size标志必须被设置。
-
32位物理地址字段只有最高10位是有意义的。这是因为每一个物理地址都是在以4MB为边界的地方开始的,故这个地址的最低22位为0。
通过设置cr4处理器寄存器的PSE标志能使扩展分页与常规分页共存。
硬件保护方案
分页单元和分段单元的保护方案不同。尽管x86处理器允许一个段使用4种可能的特权级别,但与页和页表相关的特权级只有两个,因为特权由User/Supervisor标志所控制。若这个标志为0,只有当CPL小于3(这意味着对于Linux而言,处理器处于内核态)时才能对页寻址。若该标志为1,则总能对页寻址。
此外,与段的3种存取权限(读、写、执行)不同的是,页的存取权限只有两种(度、写)。如果页目录项或页表项的Read/Write标志等于0,说明相应的页表或页是只读的,否则是可读写的。
物理地址扩展(PAE)分页机制
处理器所支持的RAM容量受连接到地址总线上的地址管脚数限制。早期Intel处理器从80386到Pentium使用32位物理地址。从理论上讲,这样的系统上可以安装高达4GB的RAM;而实际上,由于用户进程线性地址空间的需要,内核不能直接对1GB以上的RAM进行寻址。
然而,大型服务器需要大于4GB的RAM来同时运行数以千计的进程,所以必须扩展32位x86结构所支持的RAM容量。Intel通过在它的处理器上把管脚数从32增加到36已经满足了这些需求。寻址能力可达到2^36 = 64GB。不过,只有引入一种新的分页机制把32位线性地址转换为36位物理地址才能使用所增加的物理地址。
从Pentium Pro处理器开始,Intel引入一种叫做 物理地址扩展(Physical Address Extension, PAE)的机制。另外一种叫做页大小扩展[Page Size Extension (PSE-36)]的机制在Pentium 3处理器中引入,但是Linux并没有采用这种机制。
通过设置cr4控制寄存器中的物理地址扩展(PAE)标志激活PAE。页目录项中的页大小标志PS启用大尺寸页(在PAE启用时为2MB)。
Intel为了支持PAE改变了分页机制:
-
64GB的RAM被分为2^24个页框(4KB),页表项的物理地址字段从20位扩展到了24位。因为PAE页表项必须包含12个标志位(在前面已描述)和24个物理地址位,总数之和为36,页表项大小从32位变为64位增加了一倍。结果,一个4KB的页表包含512个表项而不是1024个表项。
-
引入一个叫做页目录指针表(Page Directory Pointer Table, PDPT)的页表新级别,它由4个64位表项组成。
-
cr3控制寄存器包含一个27位的页目录指针表(PDPT)基地址字段。因为PDPT存放在RAM的前4GB中,并在32字节(25)的倍数上对齐,因此27位足以表示这种表的基地址。
-
当把线性地址映射到4KB的页时(页目录项中的PS标志清0), 32位线性地址按下列方式解释:
-
cr3:指向一个PDPT
-
位31-30:指向PDPT中4个项中的一个
-
位29-21:指向页目录中512个项目中的一个
-
位20-12:指向页表中512项中的一个
-
位11-0:4KB页中的偏移量
-
-
当把线性地址映射到2MB的页时(页目录项中的PS标志置为1), 32位线性地址按下列方式解释:
-
cr3:指向一个PDPT
-
位31-30:指向PDPT中4个项中的一个
-
位29-21:指向页目录中512个项中的一个
-
位20-0:2MB页中的偏移量
-
总之,一旦cr3被设置,就可能寻址高达4GB RAM。如果我们希望对更多的RAM寻址,就必须在cr3中放置一个新值,或改变PDPT的内容。然而,使用PAE的主要问题是线性地址仍然是32位长。这就迫使内核编程人员用同一线性地址映射不同的RAM区。很明显,PAE并没有扩大进程的线性地址空间,因为它只处理物理地址。此外,只有内核能够修改进程的页表,所以在用户态下运行的进程不能使用大于4GB的物理地址空间。另一方面,PAE允许内核使用容量高达64GB的RAM,从而显著增加了系统中的进程数量。
64位系统中的分页
平台名称 | 页大小 | 寻址使用的位数 | 分页级别数 | 线性地址分级 |
---|---|---|---|---|
alpha | 8KB | 43 | 3 | 10+10+10+13 |
ia64 | 4KB | 39 | 3 | 9+9+9+12 |
ppc64 | 4KB | 41 | 3 | 10+10+9+12 |
sh64 | 4KB | 41 | 3 | 10+10+9+12 |
x86_64 | 4KB | 48 | 4 | 9+9+9+9+12 |
转换后援缓冲器(TLB)
x86处理器包含了一个称为转换后援缓冲器或TLB(Translation Lookaside Buffer)的高速缓存用于加快线性地址的转换。当一个线性地址被第一次使用时,通过慢速访问RAM中的页表计算出相应的物理地址。同时,物理地址被存放在一个TLB表项(TLB entry)中,以便以后对同一个线性地址的引用可以快速地得到转换。
在多处理系统中,每个CPU都有自己的TLB,叫做该CPU的本地TLB。
当CPU的cr3控制寄存器被修改时,硬件自动使本地TLB中的所有项都无效,这是因为新的一组页表被启用而TLB指向的是旧数据。
Linux中的分页
Linux采用了一种同时适用于32位和64位系统的普通分页模型。从2.6.11版本开始,采用了四级分页模型。下图中展示的4种页表分别被为:
-
页全局目录(Page Global Directory )
-
页上级目录(Page Upper Directory )
-
页中级目录(Page Middle Directory )
-
页表(Page Table)
对于没有启用物理地址扩展的32位系统,两级页表已经足够了。Linux通过使“页上级目录”位和“页中间目录”位全为0,从根本上取消了页上级目录和页中间目录字段。不过,页上级目录和页中间目录在指针序列中的位置被保留,以便同样的代码在32位系统和64位系统下都能使用。内核为页上级目录和页中间目录保留了一个位置,这是通过把它们的页目录项数设置为1,并把这两个目录项映射到页全局目录的一个适当的目录项而实现的。
启用了物理地址扩展(PAE)的32位系统使用了三级页表。Linux的页全局目录对应x86的页目录指针表(PDPT),取消了页上级目录,页中间目录对应x86的页目录,Linux的页表对应x86的页表。
最后,64位系统使用二级还是四级分页取决于硬件对线性地址的位的划分。
Linux的进程处理很大程度上依赖于分页。事实上,线性地址到物理地址的自动转换使下面的设计目标变得可行:
-
给每一个进程分配一块不同的物理地址空间,这确保了可以有效地防止寻址错误。
-
区别页(即一组数据)和页框(即主存中的物理地址)之不同。这就允许存放在某个页框中的一个页,然后保存到磁盘上,以后重新装入这同一页时又可以被装在不同的页框中。这就是虚拟内存机制的基本要素。
每个进程有它自己的页全局目录和自己的页表集。当发生进程切换时,Linux把cr3控制寄存器的内存保存在前一个执行进程的描述符中,然后把下一个要执行进程的描述符的值装入cr3寄存器中。因此,当新进程重新开始在CPU上执行时,分页单元指向一组正确的页表。
物理内存布局
可参考 地址空间布局
在初始化阶段,内核必须建立一个物理地址映射来指定哪些物理地址范围对内核可用而哪些不可用。
内核将下列页框记为保留:
-
在不可用的物理地址范围内的页框。
-
含有内核代码和已初始化的数据结构的页框。
保留页框中的页绝不能被动态分配或交换到磁盘上。
一般来说,Linux内核安装在RAM中从物理地址0x00100000开始的地方,也就是说,从第二个MB开始。所需页框总数依赖干内核的配置方案:典型的配置所得到的内核可以被安装在小于3MB的RAM中。
为什么内核没有安装在RAM第一个MB开始的地方?因为PC体系结构有几个独特的地方必须考虑到。例如:
-
页框0由BIOS使用,存放加电自检(Power-On Self-Test, POST)期间检查到的系统硬件配置。
-
物理地址从0x000a0000到0x000fffff的范围通常留给BIOS例程,并且映射ISA图形卡上的内部内存。这个区域就是所有IBM兼容PC上从640KB到1MB之间著名的洞:物理地址存在但被保留,对应的页框不能由操作系统使用。
-
第一个MB内的其他页框可能由特定计算机模型保留。例如,IBM Thinkpnd把0xa0页框映射到0x9f页框。
在启动过程的早期阶段,内核询问BIOS并了解物理内存的大小。在新近的计算机中,内核也调用BIOS过程建立一组物理地址范围和其对应的内存类型。
随后,内核执行machine_specific_memory_setup()函数,该函数建立物理地址映射。当然,如果这张表是可获取的,那是内核在BIOS列表的基础上构建的。否则,内核按保守的缺省设置构建这张表:从0x9f000(LOWMEMSIZE())到0x100000(HIGH_MEMORY)号的所有页框都标记为保留。
开始 | 结束 | 类型 |
---|---|---|
0x0000 0000 | 0x0009 ffff | Usable |
0x000f 0000 | 0x000f ffff | Reserved |
0x0010 0000 | 0x07fe ffff | Usable |
0x07ff 0000 | 0x07ff 2ffff | ACPI data |
0x07ff 3000 | 0x07ff ffff | ACPI NVS |
0xffff 0000 | 0xffff ffff | Reserved |
上表显示了具有128MB(0x0800 0000) RAM计算机的典型配置。从0x07ff 0000到0x07ff 2fff 的物理地址范围中存有加电自检(POST)阶段由BIOS写入的系统硬件设备信息。在初始化阶段,内核把这些信息拷贝到一个合适的内核数据结构中,然后认为这些页框是可用的。相反,从0x07ff3000到0x07ff ffff的物理地址范围被映射到硬件设备的ROM芯片。从0xffff 0000开始的物理地址范围标记为保留,因为它由硬件映射到BIOS的ROM芯片。注意BIOS也许并不提供一些物理地址范围的信息(在上述表中,范围是0x000a 0000到0x000e ffff)。为安全可靠起见,Linux假定这样的范围是不可用的。
内核可能不会见到BIOS报告的所有物理内存:例如,如果未使用PAE支持来编译,即使有更大的物理内存可供使用,内核也只能寻址4GB大小的RAM。setup_memory()函数在machine_specific_memory_setup()执行后被调用:它分析物理内存区域表并初始化一些变量来描述内核的物理内存布局。
为了避免把内核装入一组不连续的页框里,Linux更愿跳过RAM的第一个MB。明确地说,Linux用PC体系结构未保留的页框来动态存放所分配的页。下图显示了Linux怎样填充前3MB的RAM:
符号_text对应于物理地址0x0010 0000 (16MB),表示内核代码第一个字节的地址。内核代码的结束位代由另外一个类似的符号_etext表示。内核数据分为两组:初始化过的数据的和没有初始化的数据。初始化过的数据在_etext后开始,在_edata处结束。紧接着是未初始化的数据并以_end结束。
图中出现的符号并没有在Linux源代码中定义,它们是编译内核时产生的(可以在System.map文件中找到这些符号,System.map是编译内核以后所创建的)。
进程页表
进程的线性地址空间分成两部分:
-
从0x0000 0000——0xbfff ffff的线性地址,无论进程运行在用户态还是内核态都可以寻址(0—3GB)。
-
从0xc000 0000——0xffff ffff的线性地址,只有内核的进程才能寻址。
进程运行在用户态时,所产生的线性地址小于0xc000 0000,而运行在内核态时,执行内核代码,所产生的地址大于等于0xc000 0000。但是,在某些情况下,内核为了检索或存放数据必须访问用户态线性地址空间。
宏PAGE_OFFSET产生的值是0xc000 0000,这就是进程在线性地址空间中的偏移量,也是内核生存空间的开始之处。
内核页表
内核维持着一组自己使用的页表,驻留在所谓的主内核页全局目录(master kernel Page Global Directory)中。系统初始化后,这组页表还从未被任何进程或任何内核线程直接使用;更确切地说,主内核页全局目录的最高目录项部分作为参考模型,为系统中每个普通进程对应的页全局目录项提供参考模型。
内核初始化自己的页表,这个过程分为两个阶段。事实上,内核映像刚刚被装入内存后,CPU仍然运行于实模式,所以分页功能没有被启用。
第一个阶段,内核创建一个有限的地址空间,包括内核的代码段和数据段、初始页表和用于存放动态数据结构的共128KB大小的空间。这个最小限度的地址空间仅够将内核装入RAM和对其初始化的核心数据结构。
第二个阶段,内核充分利用剩余的RAM并适当地建立分页表。下一节解释这个方案是怎样实施的。
临时内核页表
临时页全局目录是在内核编译过程中静态地初始化的,而临时页表是由startup_32()汇编语言函数(定义于arch/i386/kernel/head.S)初始化的。不再过多提及页上级目录和页中间目录,因为它们相当于页全局目录项。在这个阶段PAE支持并未激活。
临时页全局目录放在swapper_pg_dir变量中。临时页表在pg0变量处开始存放,紧接在内核未初始化的数据段(_end符号)后面。为简单起见,我们假设内核使用的段、临时页表和128KB的内存范围能容纳于RAM前8MB空间里。为了映射RAM前8MB的空间,需要用到两个页表。
分页第一个阶段的目标是允许在实模式下和保护模式下都能很容易地对这8MB寻址。因此,内核必须创建一个映射,把从0x0000 0000到0x007f ffff的线性地址和从0xc000 0000到0xc07f ffff的线性地址映射到从0x0000 0000到0x007f ffff的物理地址。换句话说,内核在初始化的第一阶段,可以通过与物理地址相同的线性地址或者通过从0xc000 0000开始的8MB线性地址对RAM的前8MB进行寻址。
内核通过把swapper_pg_dir所有项都填充为0来创建期望的映射,不过,0、1、0x300(十进制768)和0x301(十进制769)这四项除外。后两项包含了从0xc000 0000到0xc07f ffff间的所有线性地址。0、1、0x300和0x301按以下方式初始化:
-
0项和0x300项的地址字段置为pg0的物理地址,而1项和0x301项的地址字段 置为紧随pg0后的页框的物理地址。
-
把这四个项中的Present、Read/Write和User/Supervisor标志置位。
-
把这四个项中的Accessed、Dirty、PCD、PWD和Page Size标志清0。
汇编语言函数startup_32()也启用分页单元,通过向cr3控制寄存器装入swapper_pg_dir的地址及设置cr0控制寄存器的PG标志来达到这一目的。下面是等价的代码片段:
movl $swapper_pg_dir-0xc0000000,%eax movl %eax,%cr3 /*设置页表指针*/ movl %cr0,%eax orl $0x80000000,%eax movl %eax,%cr0 /*设置分页(PG)位“/