1. 内存地址
以Intel的中央处理器为例,Linux 32位的系统中。物理内存的基本单位是字节(Byte),1个字节有8个二进制位。
每一个内存地址指向一个字节,内存地址加1后得到下一个字节的地址。这里用以表示物理内存实际位置的地址,就是通常所说的物理地址(Physical Address)。
CPU正在运行的进程代码、进程数据和栈区数据等。都暂时保存在物理内存中。
线性地址(Linear Address。亦即虚拟地址 Virtual Address)是出于下面考虑,而在物理地址和程序之间添加的中间层。
(1) 隔离不同进程使用的内存地址空间;(2) 提高内存的使用率;
(3) 确定程序运行时的地址。
(4) 扩展内存,即运行所需内层大于物理内存的程序
虚拟地址范围相应CPU的寻址能力,32位的CPU的虚拟地址范围为 0x00000000 ~ 0xFFFFFFFF,即最大虚拟内存为2^32 Bytes = 4GB。相应的64位CPU最大虚拟内存为 2^64 Bytes,然而实际上眼下大部分操作系统和应用程序都不须要这样大的虚拟地址空间,而且64位长的地址会添加系统的复杂性和地址转换成本,因此眼下的x86-64架构仅仅使用虚拟地址低位48位(0 ~ 47)作为虚拟地址,并用第47位的值填充48 ~ 63高位,因此64位CPU的最大虚拟内存为2^48 = 256TB。一般地,物理地址空间仅仅是虚拟地址空间的一个子集。
为了提高内存管理效率,发挥虚拟空间的作用。可设定CPU的 CR0 寄存器的最高位(PG,分页标志位)。启用分页机制将虚拟空间等分成若干页,然后按页帧管理和使用虚拟空间。物理内存规定的页大小有4096 Bytes。8192 Bytes,2MB, 4MB等,因为虚拟空间页中存储的内容实际上是要放到物理内存上的,所以虚拟空间也採用上述大小进行分页。普通的分页大小採用4KB的标准。
现代计算机系统中,一般不须要程序猿直接操作物理地址,而是由操作系统按页帧为进程分配运行用的虚拟地址。每一个页帧能够被映射到不论什么可用的物理内存页。CPU 在运行程序进程时,CPU发出对相应的虚拟地址进行读或写操作。硬件设备(MMU,内存管理单元,一般都集成在CPU芯片上)分析虚拟地址后查询页表并计算。将该虚拟地址映射为物理地址,然后通过北桥芯片(北桥芯片主要功能就是负责CPU和物理内存之间的通信)连接内存总线,从而CPU能够訪问到物理内存中进程代码和数据。
逻辑地址(Linear Address)指的是程序内部的地址偏移量。该地址以操作系统为程序分配的程序入口地址为基准,指定程序中操作数或指令的地址。逻辑地址是程序猿直接操纵的地址,比如在 C 语言编程中,定义一个 int 变量,然后使用取地址运算符(&var)得到的地址就是逻辑地址。
逻辑地址由两部分构成。各自是段选择符(Segment Selector)和段内偏移量(Offset),段选择符是一个16-bit (2字节)无符号数,段偏移量则是一个 32-bit 的无符号数。
段选择符的内容例如以下图所看到的。
Figure 1 Segment Selector Fields
2. 内存管理之分段机制
段(Segmentation)是在段式内存管理的概念下形成的术语。段式管理的基本思想是把程序内容或过程关系分成段。比如代码段、数据段等,操作系统按段为进程分配虚拟地址空间。这样不同进程段空间不同。实现了进程隔离和内存保护。每一个段都有自己的描写叙述符(Segment Descriptor, 8-byte long),这些描写叙述符被保存在全局描写叙述符表(Global Descriptor Table, GDT)或局部描写叙述符表(Local Descriptor Table, LDT)中。段描写叙述符的内容如图2所看到的。
Figure 2 Segment Descriptor
Base Address一共 32 bits,它指向当前段第一个字节的线性地址。Limit部分一共 20 bits。它指明本段虚拟空间最后一个字节相对第一个字节的偏移量。因此它也能表示段的长度。
与页不同(长度固定为4KB等),段的长度依据程序相应内容变化。另外,假设标志位 G设定为0,那么偏移量每添加 1,地址值添加 1 byte,那么这时段的最大长度为 1 byte * 2^20 = 1MB;假设标志位 G设定为1。那么偏移量加1,地址值添加 4 KB,相应的这时段的最大长度为 4KB * 2^20 = 4GB。
在进程的运行过程中,当遇到须要訪问内存的指令时,首先依据逻辑地址得到相应的线性地址。然后再依据线性地址得到物理地址。依据逻辑地址得到线性地址的过程如图3所看到的。
Figure 3 Translating a Logical Address
CPU提供CS 寄存器暂时保存正在运行的进程代码段的段选择符,DS 寄存器暂时保存进程数据段的段选择符。以及SS寄存器暂时保存栈区分段的段选择符。这样。在转换逻辑地址的时候。CPU依据当前保存在CS中的段选择器(參见图1)。当中 TI 标志确定段描写叙述符位于GDT还是LDT,Index部分确定段描写叙述符在表(GDT或LDT)中的位置,从而能够找到逻辑地址相应的段描写叙述符。依据段描写叙述符中的Base Address 找到段的起始线性地址,使用起始地址加上指令逻辑地址中的偏移量,就能得到指令所指向的实际线性地址。
因为分段机制和Intel处理器相关联,在其他的硬件系统上,可能并不支持分段式内存管理。因此在 Linux 中,操作系统倾向与使用分页的方式管理内存。
在用户模式(User Mode)下。所有的进程共用用户代码段和用户数据段。
用户模式下,所有进程使用代码段的段描写叙述符的Base Address部分都指向线性地址0x00000000,同一时候数据段的段描写叙述符的 Base Address部分也指向线性地址0x00000000;在内核模式(Kernel Mode)下,所有的进程共用内核代码段和内核数据段。
内核所有进程使用代码段的段描写叙述符的Base Address部分都指向线性地址0x00000000,同一时候数据段的段描写叙述符的 Base Address部分也指向线性地址0x00000000。上述的段描写叙述符的G位都设定为1,段相应的虚拟空间从0到2^32,相应整个32位CPU的最大虚拟空间。
上述办法攻克了其他硬件平台不支持段式管理的情况。大大简化了地址转换操作。可是因为理论上每一个进程的可用线性空间范围都是4G,即进程共用段表,使用段界限隔离进程内存的目的就不能实现了。
因此。在Linux中,为每一个进程分配独立的页表,纯粹依靠分页机制提供内存保护和进程隔离。接下来。针对分页机制进行具体的说明。
3. 内存管理之分页机制
分页机制将整个线性地址空间及整个物理内存看成由很多大小同样的存储块组成的,并把这些块作为页(虚拟空间分页后每一个单位称为页)或页帧(物理内存分页后每一个单位称为页帧)进行管理。
不考虑内存訪问权限时,线性地址空间的不论什么一页,理论上能够映射为物理地址空间中的不论什么一个页帧。
最常见的分页方式是以 4KB 单位划分页,而且保证页地址边界对齐,即每一页的起始地址都应被4K整除。在4KB的页单位下,32位机的整个虚拟空间就被划分成了 2^20 个页。
因为虚拟地址是按页所有被映射到同样大小的页帧,而且页面边界对齐,因此虚拟地址的后12位能够直接作为物理地址的低12位使用。
为了节省储存页表所需的内存空间(2^20 * 4B = 4M),32位操作系统常使用两级页表结构记载虚拟地址空间分页现状。因此每一个虚拟地址就由三部分组成。高10位是页文件夹(Page Directory)中内容的索引。中间10位是页表索引。低12位则作为相应物理地址在页帧中的偏移量。
Figure 4 Paging Mechanism
页文件夹保存在CR3寄存器中,能够直接訪问。訪问时以线性地址高10位作为索引,直接检索并得到相应索引的 32 位页文件夹项。32位页文件夹项的结构如图4中Page Directory部分所看到的,文件夹项的高20位用以给出该文件夹项相应的页表在内存中的物理地址的高 20 位,1024个文件夹项刚好能给出1024个页表的入口地址。
文件夹项的低12位是一些标志位。当中P标志指明当前文件夹项相应的页表是否在内存中;U标志指明当前文件夹项相应的页的訪问权限;S标志指明页的大小是4KB或4MB,等等。另外,因为每一个页文件夹项的长度为32位,即4个字节,页文件夹中共同拥有1024个页文件夹项。所以页文件夹的总大小为 4KB。
页表保存在内存中。
页表项的长度是32位,每一个页表中有1024个页表项,可得出每一个页表的大小是 4 KB。
页表在内存中存放时,与物理分页的大小(4KB)对齐,所以每一个页表所在的物理内存的起始物理地址的后12位都是0。
而该物理地址的高20位又由页表相应的页文件夹项中的高20位指定。这样就能够得到找到物理内存中的页表了。找到页表后,以线性地址的中间10位为索引。检索到该索引相应的32位页表项。和页文件夹项相似,页表项的高20用以给出其相应页帧的起始物理地址的高20位。页表项的低12位是关于页的标志位。
页帧相应物理内存。依据前面的两步找得到页帧的起始物理地址的高20位后,因为物理内存按4KB大小划分成页帧。所以页帧的起始物理地址的低12位都是0。这样高20位加低12位,得到页帧的起始物理地址。找到页帧后,使用线性地址的低10位作为偏移量。加上页帧的起始物理地址后能找到线性地址相应的物理地址了。须要注意的是。页帧和页表项的相应关系并非确定的,页表项指向的页首先是虚拟页,然后该虚拟页的内容被储存在不论什么合适的页帧中。
操作系统按页为每一个进程分配虚拟地址范围。理论上依据程序须要最大可使用4G的虚拟内存。
但因为操作系统须要保护内核进程内存,所以将内核进程虚拟内存和用户进程虚拟内存分离,前者可用空间为1G虚拟内存。后者为3G虚拟内存。进程运行时。操作系统为其分配的页的页文件夹会被载入到CR3寄存器,页表会被载入到物理内存。分页单元将线性地址转换为物理地址的过程中,会检查当前进程是否有訪问该分页的权限,以及线性地址相应的页数据是否在物理内存中,假设上述检查条件未被通过,分页单元将会生成页错误异常,进而中止进程或将相应分页数据载入到物理内存。
4. 物理地址扩展
物理地址扩展(Physical Address Extension)是Intel 32位CPU上独有的一种虚拟地址分页方式。理论上,32位CPU有32条内存寻址线,最多能訪问4G的物理内存;实际上在Linux系统中,用户模式程序须要线性地址空间。因此内核最多仅仅能直接訪问的物理内存为1G。
可是,随着计算机软件的发展,一台32位计算机上可能同一时候运行很多进程,而这些同一时候运行的进程所需内存量会大于4G,因此Intel为其32位CPU添加了4条内存寻址线。共36条。这样CPU支持的物理内存增大到2^36,即64GB。
扩展物理内存的同一时候。保持虚拟地址空间范围为4G不变。
从而使32位的应用程序继续使用32位的地址。每一个进程可使用的最大虚拟内存仍是4GB。
64GB的物理内存在4KB分页下,被分成2^24个页帧,每一个页帧的起始物理地址后12位仍然为0,但前24位则须要页表提供。而我们知道,常规分页中,页表项为32位,当中仅仅能提供20位作为其指向页帧的高20位物理地址,不能满足36位系统的寻址须要。能够同通过添加页表项的总长度来解决问题,为了保证以4KB的边界对齐。我们将页表项的长度添加为64位,8字节(而不能是刚好满足须要的36位)。页表大小保持4KB。那么一个页表中仅仅有512个页表项(2^12 / 8)。
相应地,页文件夹也要适应36位的物理内存寻址能力,每条页文件夹项长度也变成64位。页文件夹大小保持4KB。一个页文件夹中仅仅有512个页文件夹项。
这样一个页文件夹总共可检索 512 × 512=2^18个页,而虚拟地址空间共同拥有 2^20个页,所以总共须要4个页文件夹。
一个新的分层被添加到CR3控制器和页文件夹之间,这个新的分层是页文件夹指针表(Page Directory Pointer Table)。页文件夹指针表中有四个长度为64位的指针,分别指向前述的4个页文件夹。页文件夹指针表被载入到64GB内存的第一个4GB上(物理地址0x00000000 ~ 0xFFFFFFFF)。CR3中则保存的是该页文件夹指针表的起始物理地址。
开启物理扩展寻址方式后。将线性地址转换为物理地址的方式和之前有较大不同,具体过程如图5所看到的。Figure 5 Linear Address Translation with PAE
首先由CR3得到页面指针表的物理地址。然后以线性地址的30 ~ 31位作为索引得到页文件夹。接下来的21 ~39位(共9 bits。恰好提供所有512个页文件夹项的索引)能够帮助找到线性地址相应的页表。12 ~ 20(共9 bits,恰好提供所有512个页表项的物理地址)能够帮助找到线性地址相应的页帧的物理地址。
5. 64位操作系统的分页机制
64位机的寻址能力为 2^64 Bytes,但实际中用不到这么多的虚拟内存,使用64位寻址方式还会造成寻址时间添加、内存空间浪费等不利因素。因此在实际应用中。对64位机使用48位的寻址方式(最大支持256TB物理内存)。同样的,将物理内存分为4KB大小的页帧,那么就须要 48-12=36位物理地址高位来确定页帧位置。为了减小储存页表所需的物理内存,实现内存权限訪问,能够通过添加两个页文件夹层来分散页表。在Linux中,採用4层分页的方式来实现该目的。
Figure 6 Paging in 64-bit Linux
从64位线性地址(仅仅有48位作为地址用)转换位物理地址的过程相似32位线性地址的转换。
为了避免在多级页表解析过程中多次查表而导致性能下降的问题。Intel x86 处理器缓存了地址转译信息。即从虚拟地址到物理地址的映射关系。这样,当处理器反复訪问同一个地址时无须再进行转译。此缓存是一种位于处理器内部的关联存储单元阵列。也被称为地址转译快查缓冲区(TLB,TranslationLook-aside Buffer)。TLB 中包括了近期使用过的页面的内存映射信息,处理器提供了专门的电路来并发地读取并比較TLB中的页面映射项。因此。对于频繁使用的虚拟地址,它们非常可能在TLB中有相应的映射项,因而处理器能够绝对高速地将虚拟地址转译成物理地址。反之,假设一个虚拟地址没有出如今TLB中,那么处理器必须採用以上介绍的两次查表过程(意味着要两次訪问内存)才干完毕地址转译。在这样的情况下,这一次内存訪问会慢一些,可是,经过这次訪问以后,此虚拟页面与相应物理页面之间的映射关系将被记录到TLB中。所以。下次再訪问此虚拟页面时。处理器就能够从TLB 中实现高速转译,除非此映射项已经被 TLB 移除了。
研究表明,因为计算机程序的内存訪问有一定的局部性,因此,即使处理器仅仅维护一个相对较小的TLB。程序的运行也能获得较显著的性能提升。
6. 进程的建立和运行
运行程序时,操作系统会创建一个运行该程序的进程。然后装载程序或程序片段等,然后開始顺序运行代码段。在这个过程中。操作系统总的来说做三件事情:
(1) 为进程创建一个独立的虚拟地址空间(范围)
比如在32位系统常规分页状态下,操作系统发现待运行程序的指令和数据总和为32KB,那么操作系统会为进程分配8个页的虚拟内存空间,并分配页文件夹和页表。把页文件夹装入CR3,把进程用到的页表载入到内存。
但并不把指令和数据载入到内存。
(2) 读取程序可运行文件文件头,而且建立虚拟空间与可运行文件里的代码段、数据段的逻辑地址的映射关系这一步将程序指令和数据映射到虚拟内存空间中。
(3) 将 CPU 的指令寄存器设置成可运行文件的入口地址。启动运行
运行程序过程时,假设当前指令或数据之在虚拟地址空间中。而实际上并不在物理内存中(前两步都没有将指令或数据载入到物理内存),将发生页错误,这时操作系统再从物理内存分配一个空暇的物理页帧,并将虚拟地址页相应的数据从磁盘拷贝载入到物理页帧中。并建立页表项和页帧的映射关系。
随着进程的运行。页错误也会不断产生。操作系统也会响应每一个页错误并为进程分配物理内存页帧。但物理内存是有限的,为一个进程可分配的物理内存也有限。
所有可用物理内存都分配给进程后。假设进程继续抛出页错误请求很多其他物理内存,这时候操作系统依据自身的页置换操作算法,在保证进程正常运行的前提下,将先前为进程分配的物理内存页帧收回,又一次分给该进程。