本文讲述linux在X86平台上的分段和分页机制
MMU(内存控制单元):硬件电路。 逻辑——分段单元——线性——分页单元——物理
逻辑地址:程序代码里的地址。段+偏移量
线性地址:逻辑地址经过分段转换为线性地址。如果是32位CPU,可表达4G的线性地址。0x00000000-0xffffffff
物理地址:用于物理内存芯片的内存单元寻址。它的范围与地址总线的位数相关。
X86的硬件分段:
一个逻辑地址分为两部分组成:段标识符和指定段内相对地址的偏移量。
处理器提供6个段寄存器,CS,SS,DS,ES,FS,GS;
段描述符:8个字节;用来存放段起始地址,段大小,存储权限等。存储在GDT或LDT中。
段选择符(段标识符):16位,用来寻找段描述符,存放于段寄存器中。
分段流程:将逻辑地址指定的段标识符装载在段寄存器中,由段标识符指定段描述符的起始地址,在段描述中找到相应段的段起始地址,将段起始地址左移N(N=地址总线位数-CPU位数),然后与逻辑地址指定的段内偏移量相加,得到线性地址。
linux的分段:
linux以非常有限的方式使用分段,只有在X86机构下才使用分段(一些体系结构对分段支持很有限,尤其是RISC)。
运行在用户态的linux进程都使用一对相同的段来对指令和数据寻址:用户代码段和用户数据段;
运行在内核态的所有linux进程都使用一对相同的段对指令和数据寻址:内核代码段和内核数据段。
他们的段起始地址都是0X0,则在linux下逻辑地址和线性地址是一致的,即逻辑地址的偏移量字段的值和相应的线性地址的值总是一致的。
X86的硬件分页:
页:一组线性地址和地址中的数据。 分页单元将线性地址转换成物理地址。
分页单元把所有的RAM分成固定长度的页框。线性地址按照页框大小分组,称为页。页框长度=页长度。页内部的线性地址被映射到连续的物理地址中。
把线性地址映射到物理地址的数据结构称为页表。页表存放在主存中,并在启用分页单元之前必须有内核对页表进行适当的初始化。
常规分页:
32位的线性地址被分成3个域:Directory(目录)最高10位:Table(页表)中间10位:Offset(偏移量)最低12位(4KB页);线性地址的转换分两步完成,每一步都基于一种转换表,第一种转换表称为页目录表,第二种表称为页表;使用二级模式的目的在于减少每个进程页表所需的RAM数量。每个活动进程都有一个分配给它的页目录,在实际需要时分配页表,其页目录的物理地址存放在控制寄存器的cr3中,cr3与线性地址的dirctory字段决定页目录中的目录项,再通过线性地址的TABLE字段决定页表的表项,再配合OFFSET字段找到页框内物理地址。
扩展分页:
从pentiun开始,X86引入扩展分页,页框大小为4MB,扩展分页把大段连续的线性地址转换为相应的物理地址。内核则可以不用对中间页表转换。DIRECTIORY(10位):OFFSET(22位)(4MB页);
物理地址扩展分页机制:
处理器所支持的RAM容量受到连接到地址总线上的地址管脚数量限制。大型服务器需要大于4GB的RAM时,intel在它的处理器上把管脚从32增加到36位,引入物理地址扩展的分页机制将32位线性地址转换为36位物理地址。
64位系统的分页:
32位的系统采用2级分页机制,出于效率和RAM容量的考虑,64位系统采用额外的分页机制。使用的级别的数量取决于处理器的类型。
硬件高速缓存:减少cpu等待动态RAM的时间。 行:几十个连续字节。 实现慢速DRAM和高速静态SRAM之间传送
linux 的分页:
linux采用了一种同时适用于32位和64位熊的普通分页模型,目前采用四级分页模型:页全局目录:页上级目录:页中间目录:页表。
页全局目录包含若干页上级目录的地址,页上级目录包含若干页中间级目录的地址,页中间级目录又包含若干页表的地址。每个页表项又指向夜歌页框。线性地址因此被分成了5个部分。
没有启动物理地址扩展的32位系统,linux采用2级模型:页全局目录和页表,页上级目录和页中间级目录全置0;
启用了物理地址扩展的32位系统使用3级页表。
64位系统使用三级还是四级分页取决于硬件对线性地址的划分。
物理内存布局:
物理地址映射:保留出不能用的物理地址
进程页表:0x00000000到0xbfffffff线性地址。用户态,内核态可用
0xc0000000到0xffffffff线性地址。 内核态可用
总结:
用户空间不是进程共享的,而是进程隔离的。每个进程最大都可以有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”。