《深入LINUX内核》第二章 内存寻址

本章主要讲述的是在x86架构中内存寻址的方式,以及Linux中是如何实现硬件寻址的。

0. 术语释义

0.1 术语源文

内存地址(Memory Address)
逻辑地址(Logical Address)
线性地址(Linear Address)
虚拟地址(Virtual Address)
物理地址(Physical Address)
分段单元(Segmentation Unit)
分页单元(Paging Unit)
段描述符(Segment Descriptor)
页全局目录(Page Global Directory)
页上级目录(Page Upper Directory)
页中间目录(Page Middle Directory)
页表(Page Table)
固定映射线性地址(Fix-mapped linear address)

0.2 缩略语

GDT(Global Descriptor Table)全局描述符表
LDT(Local Description Table)局部描述符表
MMU(Memory Manage Unit)内存控制单元
TI(Table Indicator)表指示位
RPL(Requested Privilege Level)请求特权等级
CPL(Current Privilege Level)当前特权等级
CS(Code Segment)代码段
DS(Date Segment)数据段
ES(Extra Segment)扩展段
SS(Stack Segment)栈段
FS(F-extra Segment)F扩展段
GS(G-extra Segment)G扩展段
TSS(Task State Segment)任务状态段
TSSD(Task State Segment Descriptor)任务状态段描述符
LDTD(Local Description Table Descriptor)局部描述符表描述符
PAE(Physical Address Extension)物理地址扩展
TLB(Translation Lookaside Buffer)转换后援缓冲器

1. 内存地址

内存地址是程序员访问内存内容的必由之路。
在x86架构中,内存地址中包含三个概念:

  • 逻辑地址
  • 线性地址
  • 物理地址

它们三者之间的转换顺序为:逻辑地址——>线性地址——>物理地址。
在内存控制单元(MMU)中,由分段单元完成逻辑地址到线性地址的转换。由分页单元完成线性地址到物理地址的转换。

1.1 逻辑地址

逻辑地址,顾名思义,就是我们在程序中用到的地址,是符合编程人员逻辑的地址。再深入一点就是处理器的指令中使用的地址。其组织方式就是:段选择符+段偏移量。但是在保护模式中,需要经过地址转换才能得到能够放到地址总线上使用,访问物理内存的地址。

其中段选择符有16位,段偏移量有32位。
由逻辑地址转换到线性地址的方式如下图所示:
逻辑地址到线性地址的转换示意图
其地址转换逻辑为:

  1. 首先看段选择符中断T1位是0还是1,从而判断是访问GDT中的段还是LDT中的段,并从相应的gdtr\ldtr寄存器中得到相应GDT\LDT表的地址和大小。该表其实就是一个数组,里面存放的是各个段的基地址。
  2. 其次再看段选择符中的高13位,这个就是上述数组的下标index值,根据index值在数组中找到目标段的基地址。
  3. 最后把基地址和段偏移组合就得到了线性地址。

1.2 线性地址

线性地址是逻辑地址和物理地址的中间层,是一个32位的无符号整形数,即表示可以寻址232=4G的内存。然后根据Linux中的分页机制,可将线性地址转换为硬件可用的物理地址。32位系统的线性地址分为三个域:

  • 页目录表(Directory):线性地址高10位(31~22)。
  • 页表(Tabel):线性地址中间10位(21~12)。
  • 偏移量(Offset):线性地址低12位(11~0)。
    下图是一个简单的二级页表转换示意图:
    线性地址转物理地址
    其地址转换逻辑为:
  1. 前提:内存管理单元已经建立好了每个进程的页目录表和页表。
  2. 首先把当前进程的页目录表的物理地址放入CR3寄存器中。
  3. 线性地址 0x80495b0 转换成二进制后是 0000 1000 0000 0100 1001 0101 1011 0000。
  4. 线性地址高10位为0000 1000 00,十进制为32,据此在页目录表的数据结构中找到对应的页表物理地址。
  5. 线性地址中间10位为00 0100 1001,十进制为73,据此在页表的数据结构中找到对应的物理页的物理地址。
  6. 线性地址低12位为0101 1011 0000,十进制为1456,据此在物理页中找到实际的目标物理地址。

1.3 物理地址

物理地址由32位或36位无符号整形表示。它是由CPU的地址总线发送到物理内存上的电信号。
物理内存是从0开始编址的,顺序递增,这个地址就是物理地址,反映了地址所在内存位于物理内存中的位置。

2. 硬件中的分段

从80286开始,x86架构的CPU有两种运行模式,实模式和保护模式。这两种模式中的都采用了分段寻址方式。在实模式中,逻辑地址=段基址+段内偏移(这里的逻辑地址其实就是物理地址)。而在保护模式中,逻辑地址=段选择符+段内偏移。

2.1 段选择符和段寄存器

段选择符

如前所述,逻辑地址由两部分组成:段选择符+段内偏移。段选择符是一个16位长的字符(段内偏移是一个32位的字符)。其结构组成为:
段选择符
其中高13位(15~3)是段描述符索引。这其实是段描述符表的序号。
第2位是TI位,该位表示索引表是全局段描述符表(0)还是局部段描述符(1)。
第1~0位是RPL,代表内核特权等级。在linux中只有0和3级,分别对应内核态和用户态。

段寄存器

段寄存器就是用来存放段选择符的,在32位处理器中有6个段寄存器:

  • CS——代码段:指向程序指令的段。
  • DS——数据段:指向静态数据或全局数据段。
  • ES——扩展段:通用段,可以指向任意段。
  • FS——F扩展段:同上。
  • GS——G扩展段:同上。
  • SS——栈段:指向当前程序栈的段。

其中ES、FS、GS都是扩展段,其中ES的全称为Extra Segment,而FS和GS与ES是一样性质的段。而名字没有特殊含义,只是顺着原先C\D\E的顺序继续排列而已。
其中,CS寄存器还有一个特殊的地方,就是CS寄存器的低两位(0~1)不叫RPL,而是叫CPL。代表当前CPU的特权等级,在linux中只有0和3级,分别对应内核态和用户态(除了RPL和CPL外,还有DPL)。

段描述符

如上所述,段选择符中的高13位是段描述符索引,用来在段描述符表中找到相应的段描述符。而段描述符是用来描述段的特征的。段描述符存放在段描述符表中。段描述符表分为全局描述符表(GPT)和局部描述符表(LPT)。
在一个系统中,只有一个GPT表,而在系统中的进程如果GPT不够用,就可以创建自己的LPT。
GPT和LPT表的地址和大小分别存放在gptr和lptr两个寄存器中。
段描述符的结构为
段描述符
其中:

  • Base: 段基地址,在段描述符中由三处合并而来。
  • G:粒度标识。为0时,段的单位是字节;置1时,段的单位是以4096字节为基础单位。
  • D/B:为0时,代表适配的是16位的段内偏移,即目标段是一个16位的段;置1时,代表适配的是32位的段内偏移,即目标段是32位的段。
  • AVL:可以任意设置,但是在linux中没有用到。
  • P:表示当前段是否存在于内存中。在linux中默认为1。
  • DPL:描述符特权级。在Linux中只有0和3级。分别对应于内核态和用户态。
  • S:为0时表示系统段;置1时表示数据段或数据段。
  • TYPE:根据S的值,描述段的类型。

段的类型有:

  • 代码段描述符:S=1,TYPE[11]=0
  • 数据段描述符:S=1,TYPE[11]=1
  • 任务状态描述符(任务状态段TSS):S=0,TYPE=11(运行)或9(未运行)
  • 局部描述符表描述符(LDTD):S=0,TYPE=2
快速访问段描述符

为了加速段描述符的访问,处理器提供了一种不可编程的寄存器,该不可编程寄存器可以存放8个字节(64位)的段描述符。
当一个段选择符装入到段寄存器中,对应的段描述符就会被放到这种不可编程寄存器中。此后,在地址转换过程中,如果需要用到段描述符中的内容,就可以直接访问寄存器中的内容,而不需要去内存中寻找。只有在切换段时,才会访问到内存中的GDT或LDT。
访问段描述符的逻辑为:

  1. 在gdtr或ldtr寄存器中取出GDT或LDT的物理地址A。
  2. 取出段选择符中的高13位的index值,
  3. 计算出目标段描述符的物理地址S=A+index*8。

段选择符中的段描述符索引值index共有13位,所以,应该可以寻址213=8192个段描述符。但是,一般情况下,GDT表中的第一项为0,即index值为0时,表示逻辑地址是无效的。所以,保存在GDT中的最大的段描述符数量为8191。
而段内偏移有32位,即每个段最大可以寻址232=4G。

分段单元

分段单元(Segmentation unit)的作用是将逻辑地址转换为线性地址。
转换逻辑在前面逻辑地址的地方已经讲过了。这里不再赘述。

3. Linux中的分段

linux设计的初衷就是可以移植到绝大多数流行的处理器架构,而有些处理器架构对分段支持度不够,所以,在linux中分段应用十分有限,只在x86架构中应用的有。
具体应用之处就是在用户态中的进程和内核态中的进程都使用分别使用一对段来对指令和数据进行寻址。这两对段就是 :

BaseGLimitSTypeDPLD/BP
用户代码段0x0000 000010xffff110311
用户数据段0x0000 000010xffff12311
内核代码段0x0000 000010xffff110011
内核代码段0x0000 000010xffff12011

在linux内核中,由__USER_CS,__USER_DS,__KERNEL_CS和__KERNEL_DS宏来定义的上述四个段的段选择符。需要对上述某个段寻址时,只需要将对应的宏定义写入到相应段寄存器中即可。

由上表可见,上述4个段的起始地址都是0x0,可知,无论内核态的进程还是用户态的进程,它们的逻辑地址是复用的。也就说不同的进程可以使用相同的逻辑地址(但是不用担心,它们之间会冲突,那还是因为逻辑会经过分段单元和分页单元,最后不通进程的逻辑地址最终会指向不同的物理地址)。另外,在linux中逻辑地址中的段内偏移量和线性地址的值是一样的。

段选择符需要与cs寄存器中的CPL字段保持一致,即当CPL=3时,ds寄存器中放置的应该是用户数据段的段选择符。当CPL=0时,ds寄存器中存放的应该是内核数据段的段选择符。

3.1 Linux GDT

单处理器系统只有一个GDT,而多处理器系统中每一个CPU都会对应一个GDT。
在GDT中, 包含有18个段描述符和14个空的、未使用的或保留的项。

3.2 LinuxLDT

Linux中不怎么使用LDT。

4. 硬件中的分页

硬件中的分页主要由分页单元(Paging Unit)负责。其作用是把线性地址转换为物理地址。此外还有一个重要的任务就是线性地址的访问权限进行检查,看是否为非法访问。如果是,则引起一个访问异常。

关于分页,有以下的几个概念要分清楚:

  • 页(Page):是指线性地址按固定长度分为若干组,其中每个组就是一个页。简单理解就是程序中所认为的内存空间中的固定大小的一份。
  • 页帧(Page Frame):也叫物理页,是物理内存按照固定长度分为若干组,每一个组就是一个页框或物理页。简单理解就是实际的物理内存中的固定大小的一份。

页帧和页是一一对应的,其大小应该是一样的,有分页单元负责为每一个页指定实际物理内存中的页帧。

  • 页表(Page Table):内核通过建立页表来实现线性地址和物理地址的映射。也就是内核通过页表把页和页帧一一对应。

在一个进程中,连续的页不一定会对应着连续的页帧,而是由页表来指定每个页对应的页帧。原因是从进程本身的角度来看,整个内核中就只有它自己,它可以随心所欲的使用全部的内存。但是在真实的系统中,会存在很多个进程,需要这些进程共用物理内存。所以,在进程中的连续页可能是由物理内存中不同地方的页帧组合而来的,其组合方式由页表决定。

从80386开始,所有的x86架构的处理器都支持分页,主要是由cr0寄存器中的PG标志位控制,该标志位置0时,分页功能就被启用了。

4.1 常规分页

从80386开始,x86架构处理器一般页的大小为4K。

如前所述,32位的线性地址分为三个部分:

  • Directory(31~22):页目录
  • Table(21~12):页表
  • Offset(11~0):页内偏移(这决定了页的大小,因为页内偏移有12位,所以,最大可寻址212=4096=4K大小的内存区域)

这是最典型的二级分页机制的实现方式,其示意图如下所示:
二级页表
使用多级分页的原因是:为了减少用来存储页表的内存的空间。如果使用单级页表,在4G空间中就需要有220个页表。而每个页表也是会占用内存空间,这么多的页表会浪费很大的内存空间的。

在最新的linux内核中,采用的是四级分页机制,其原理与二级分页机制一样,只是增加了两级目录,从而可以寻址更大的内存空间。
四级分页机制

4.2 扩展分页

Linux内核中允许使用扩展分页,所谓扩展分页就是每页的大小不是4K,而是4M。这样做的目的同样也是减少页表的数量。并且可以省去页表,只需要也目录即可了。

在扩展分页中,线性地址只有两部分:

  • 页目录(Directory)[31~22]
  • 页内偏移(Offset)[21~0]
    在这里插入图片描述
    扩展分页可以通过设置cr4寄存器中的PSE标志位与常规分页共存。

4.3 硬件保护方案

所谓硬件保护就是保护模式的意义所在。就是防止内存的越界访问。

具体措施为:
在页目录\页表结构中,User\Supervisor标志位为0时,只有CPL=0时,即在内核态时才能对本页寻址。User\Supervisor标志位为1时,无论CPL为何值,可对本页寻址。

4.4 常规分页举例

举例略

因为线性地址中前10位(31~22)是页目录,其可存放为210=1024个页表地址,若进程只用了其中一部分,那么其他剩余部分将别设置为0。同样在页表(21 ~12)也为10位,所以,同样可以存放210=1024个页帧地址,若只用了其中一部分,那么其他剩余部分将别设置为0。这些被设置0的地方,其页目录/页表中的present标志位就是0,而这位为0,就表示该页内存不存在,如果访问,就会产生#14缺页异常。

4.5 物理地址扩展(PAE)分页机制

前面所述的分页机制,最大支持4GB的内存寻址,但是在一些大型的服务器等运行场景,需要有更大的内存支持。

Intel在32位处理器上解决这个问题,把地址总线从32个管脚扩展到了36个。这样就可以寻址236=64G的内存空间。

但是仅仅是硬件上的改变还不足以完成物理地址的扩展,所以在linux中就有了PAE(Physical Address Extension)机制。

通过cr4寄存器中的PAE标志位控制PAE机制的开启和关闭。

机制(略),待后续有需要时补充。

需要注意的是,PAE机制只是扩展了物理内存的容量。而每个进程还是只能访问最大为4GB的内存空间。只是在PAE机制下,可以允许更多的进程共存于系统中。

4.6 64位系统中的分页

在64位系统中,线性地址有64位,一个标准页为4K,即需要页内偏移需要使用12位,那么还剩下64-12=52位,假使我们仅使用64位中的48位,那么依然还是剩下48-12=36位,如果这36我们平分给页目录和页表,在二级分页模式下,页目录和页表都可以容纳218=25600个项。那么从减少页表数量,节省页表所占内存空间的方面来说,这是不允许的。所以,需要使用更多级的分页机制。

linux为了适应不同的架构的分页机制,提供了一种通用的分页机制,可以完美兼容各个架构的分页机制。

4.7 硬件高速缓存

现代处理器的频率已经达到了GHz级别,但是内存却依然在MHz级别。 这就造成了当CPU访问内存时的资源和时间的浪费。并且更加尴尬的是,CPU还需要频繁的访问内存。这就极大地影响到了系统的运行效率。是可忍孰不可忍!于是缓存就应运而生了,由静态存储芯片(SRAM)组成,速度接近处理器的速度,全称就是硬件高速缓存(Hardware cache memory)。

硬件高速缓存位于分页单元和内存之间,由SRAM高速缓存高速缓存控制器组成。

高速缓存中分为很多行,每个行对应物理内存中的一个同样大小的区域,每个行里面存放的是最近最常调用的内存区域的内容。高速缓存控制器中存放的是一组表项,其中每个表项对应缓存中的一行。每个表项中有一个标签(tag)和一些状态标志位,这个标签是用来指示当前缓存行对应的内存行。

4.8 转换后援缓冲器(TLB)

TLB的作用是加速线性地址到物理地址的转换。当一个线性地址第一次被使用时,是通过内存中存放页表来计算解析出物理地址的。如果体用了TLB,就会把解析出的物理地址放入到一个TLB的表项中。这样以后需要解析该线性地址时,就可以快速的从TLB中得到了。

5. Linux中的分页

Linux中采用了可以兼容32位和64位处理器的分页机制。如前所述,二级分页机制对于32位处理器已经足够了,但是对于64位系统就力不从心了。所以Linux采用了四级分页(2.6.10版本采用了三级分页,但是很快就在2.6.11版本改为了四级分页)。

四级分页机制中,共有四种页目录/页表:

  • 页全局目录(Page Global Directory)
  • 页上级目录(Page Upper Directory)
  • 页中间目录(Page Middle Directory)
  • 页表(Page Table)

四级分页机制如下图所示:
四级分页机制
对于32位处理器,只需要两级分页即可,所以针对于这个情况,线性地址中的页上级目录和页中间目录不再分配bit位,全部给页全局目录。这样就相当于两级目录了,但是为了兼容32位和64位处理器,内核在指针序列中依然保留了页上级目录和页中间目录的位置,具体做法就是把它们两个的页目录项数设置为1,并把这两个页目录中各自唯一的一个页目录项映射到页全局目录中的一项。

5.1 线性地址字段

PAGE_SHIFT:指定线性地址中页内偏移(Offset)的位数,即会影响到页的大小。
PMD_SHIFT:指定线性地址中页内偏移(Offset)和页中间目录(Table)两个字段的总位数。
PUD_SHIFT:页上级目录项能映射内存区域的大小的对数值。
PGDIR_SHIFT:页全局目录项能映射内存区域的大小的对数值。
PETS_PER_PTE,PETS_PER_PMD,PETS_PER_PUD以及PTRS_PER_PGD:对应页表、页中间目录、页上级目录和页全局目录中表项的个数。

5.2 页表处理

5.3 物理内存布局

在这里插入图片描述
内核一般存放在0x100000的位置。即2M开始的地方。

5.4 进程页表

进程的线性地址空间被分为两个部分:

  • 0x0000 0000~0xBFFF FFFF(3G):用户态和内核态进程都可以访问。
  • 0xC000 0000~0xFFFF FFFF(1G):只有内核态进程才可访问。

其中二者的界限为0xC000 0000,由宏PAGE_OFFSET定义。

5.5 内核页表

如前所述,可知,每个进程都有一套页表(即分页机制中的所有页目录、页表等)。而内核作为一种特殊的程序,也拥有自己的一套页表。但是内核在初始化好这套页表后,却不会使用它,而是用它作为进程页表的模具。所有进程的页表都是通过这套页表复制出来的。

5.5.1 临时内核页表

当内核被解压到0x100000处后,需要启动内核的第一个程序swapper进程,就需要用到内存。这个就需要建立起一个临时内核页表,供临时使用。

假设内核的所使用的各种段、初始页表和一块用于存储动态数据的128K的区域,能够放到8M的内存区域内。所以,内核从实模式转换为保护模式时,就需要创建一个8M的可寻址的内存空间。内核把这块内存空间选在了内存开头的0~8M的位置,为了可以对这块内存区域寻址,就需要建立一个临时的页表。这个临时页表就叫做临时内核页表

临时内核页表需要寻址210244k=8M的地址空间。一个页表有1024个页表项,每个页表项可寻址4K的地址空间。所以,每个页表可以寻址10244K=4M的地址空间。所以映射8M的物理地址空间,需要有两个页表。而在页全局目录中,有1024个页目录项,对应1024个页表,可寻址10244M=4G的物理地址空间。如前所述,物理内存被分为两部分,内核空间(3G ~ 4G)和用户空间(0G ~ 3G)。所以,相应的在临时内核页表也要有两份,一份对应用户空间,一份用于内核空间。在页全局目录的1024个页目录项中,0 ~ 767项是对应用户空间(0 ~ 3G)的物理内存区域,768 ~ 1024项对应于内核空间(3G ~ 4G)。因此我们就把两组临时内核页表建立在对应的用户空间和内核空间的起始位置,各自对应0、1和768、769两组页全局目录项。而其他全局目录项都被初始化为0。

在这个临时内核页表中,内核把线性地址的地址空间中的[0x0,0x7F FFFF]和[0xC000 0000,0xC07F FFFF]两个区间都映射到物理内存的[0x0,0x7FFFFF]区域。
在这里插入图片描述
临时页全局目录的地址存放在swapper_pg_dir变量中。由临时页全局目录的地址可以找到两组临时页表的地址,就可以寻址8M临时内存。

5.5.2 当RAM小于896MB时的最终内核页表

为什么是896MB呢?这是因为在线性地址的4G内存地址空间中,在最后的128M空间是用来作为固定映射的区域。所以,其实,在内核空间(3G ~ 4G)这块区域中,有128M被分出去了,并没有1G,而仅有1G - 128M=896M。所以896M是一个特殊的值。

5.5.3 当RAM大于896MB小于4096MB时的最终内核页表
5.5.4 当RAM大于4096MB时的最终内核页表

5.6 固定映射的线性地址

在32位系统中的线性地址空间为4G,分为用户空间(0 ~ 3G)的3GB大小和内核空间(3 ~ 4G)的1GB大小两个部分。其中内核空间1GB又分为2个部分:线性区(3G ~ 3G+896M)和非线性区(3G+896M ~ MAX)。线性区会被线性映射到物理内存的(0 ~ 896M)空间,线性区的线性地址空间与物理地址的最前端的896M空间是一一对应的,这样可以加快内核对内存的读写速度。而非线性区不会提前进行地址映射,而是使用动态映射(即在需要使用的时候才会映射到物理内存中的[896M ~ MAX]内存区域中的任意页帧),用于非连续内存分配和固定映射。这样就使得内核可以访问所有的物理内存空间了。

非连续内存分配是线性地址映射到一连串非连续内存区域。是一对多的关系。
固定映射就是线性地址可以映射的高端内存(896M ~ Max)之间任意的物理地址。是一对一关系。

5.7 处理硬件高速缓存和TLB

所谓处理,就是使用一些技术来减少高速缓存和TLB的未命中次数。

5.7.1 处理硬件高速缓存

5.7.2 处理TLB

* 参考资料

  1. DPL,RPL,CPL 之间的联系和区别
  2. linux操作系统CPL、DPL、RPL说明
  3. 数据段描述符和代码段描述符(一)——《x86汇编语言:从实模式到保护模式》读书笔记10
  4. 临时内核页表的建立过程
  5. linux 用户空间与内核空间——高端内存详解
  6. 固定映射的线性地址
  7. linux 用户空间与内核空间——高端内存详解

# Q&A

#.1 在32位系统中,线性地址可以寻址232=4G的内存空间,那么在64位系统中呢?

#.2 线性映射和非线性映射??

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值