图片 16

段寄存器的值对应于20位地址总线的中的高16位,需要运行进程D

在上一篇文章Linux内存寻址之分段机制中,我们了解逻辑地址通过分段机制转换为线性地址的过程。下面,我们就来看看更加重要和复杂的分页机制。

前言

本文涉及的硬件平台是X86,如果是其他平台的话,如ARM,是会使用到MMU,但是没有使用到分段机制;
最近在学习Linux内核,读到《深入理解Linux内核》的内存寻址一章。原本以为自己对分段分页机制已经理解了,结果发现其实是一知半解。于是,查找了很多资料,最终理顺了内存寻址的知识。现在把我的理解记录下来,希望对内核学习者有一定帮助,也希望大家指出错误之处。

  • 内存地址 
    1. 三种内存地址:1)逻辑地址(机器指令中操作数或指令的地址) 分段单元
      2)线性地址(虚拟地址)分页单元
      3)物理地址(用于内存芯片级内存单元寻址
    2. 多CPU时,共享同一内存,RAM芯片由独立的CPU并发访问;
      由内存仲裁器保证RAM的读写的串行执行

         

  • Linux中的分段 
    1. 80X86才使用分段(把程序划分为逻辑相关的实体),Linux更喜欢使用分页(当所有进程使用相同的段Register值时,它们共享同样的一组线性地址,这样内存管理简单;
      RISC对分段的支持有限.).
    2. 两者都划分进程的物理地址空间:分段可以给每一个进程分配不同的线性地址空间,而分页可以把同一线性地址空间映射到不同的物理空间.
    3. 段选择符由宏__USER/KERNEL_CS/DS定义.对内核代码寻址,吧__KERNEL_CS宏产生的值装入cs寄存器即可.这样执行指令时,只需指定逻辑地址的偏移部分,段选择符已经隐含在寄存器内.
    4. 所有段(内核/用户的数据/代码段)的Base=oX0000000,即逻辑地址(的偏移量字段值)=线性地址(的值).所有进程使用相同的逻辑地址.
    5. 每个CPU一个GDT,会插入未使用的项使得经常一起访问的描述符能够处于同一32字节的硬件Cache中;
      大多数用户态APP不适用局部描述符表,所以定义了一个缺省的LDT供进程共享,同时进程可以创建自己的LDT.

         

  • 硬件中的分页 
    1. 分页单元的一个关键任务就是把所请求的访问类型与现行地址的访问权限相比较,如果访问无效,产生一个缺页异常.
    2. 页:
      线性地址被分为固定长为单位的页,页内连续的线性地址被映射到连续的物理地址中,内核仅需为页指定物理地址和存取权限,提高了效率.
    3. 页框:
      相应地,把RAM也分为固定长度的页框(物理页),一个页框包含一个页(长度一致).
    4. 页表: 把线性地址映射到物理地址的数据结构.存放于主存中
    5. CPU通过设置cr0寄存器的PG=0标志启动线性地址到物理地址的翻译
    6. 常规分页 
      1. 页Size=4KB.从而32位的线性地址= Directory(10b:目录)+
        Table(10b:页表)+ Offset(低12b:偏移量)
      2. 转换分为两步,每一步基于一种转换表.使用二级模式为了减少每个进程页所需的RAM的数量.二级模式只为进程实际使用的那些虚拟内存区请求页表来减少内存容量.
      3. 每个活动进程都有一个页目录,但是仅在进程实际需要一个页表时才给该页表分配RAM.
    7. 扩展分页:
      页框Size=4MB.用于把大段连续的线性地址转换成相应的物理地址.不适用中间页表.32b线性地址=Directory(10)+Offset(22)
    8. 64位系统中的分页:
      使用了额外的分页基本.不然的话每个进程的页目录和页表含有的表项太多.
    9. 物理地址扩展(PAE)分页机制 
      1. CPU所支持的RAM容量受地址总线上的管脚数限制,将该数从32改为36即可为2(36)=64GB.
      2. PAE机制:1)64GB的RAM分为2(24)个页框,页表项的物理地址字段从20变为24位.所以PAE页表项=12个标志位+24个物理地址位=36,页表项Size=64(>32).所以一个4KB的页表包含512个页表而非1024个.
      3. 2)引入页目录指针表(PDPT)的级别,由4个64位表项组成.
      4. 3)cr3控制寄存器含有一个27位的PDPT的基地址,因PDPT在RAM的前4GB中,并在32字节的倍数上对其,所以27可以表示其基地址
      5. 解释32位线性地址(4KB/2MB的页):1)cr3指向PDPT;
        2)31-30指向PDPT中4个项之一;
        3)29-21指向页目录中512个项中的一个;
        4)20-12指向页表512项之一+11-0为4KB页内Offset/20-0为2MB页中Offset.
      6. PAE的主要问题是线性地址仍是32位.并没有扩大进程的线性地址空间,而只处理物理地址.内核编程人员必须用同一线性地址映射不同的RAM区.
      7. 由于只有内核才能修改进程的页表,所以用户态下的进程不能使用>4GB的物理地址;
        但内核能够使用更多的物理地址从而增加了系统中的进程数量.
    10. 硬件Cache 
      1. 由行(几十个连续的字节)组成.其位于分页单元和主内存之间.由硬件Cache内存(真正的行)+Cache控制器(表项数据.一个表项(标签(辨别行映射的内存单元)+标志)对应一个行)组成
      2. 类型:
        直接映射/充分关联/N-路组关联,来将主存中的一个行,关联到Cache中的一个行.
      3. 写策略:通写(同时写RAM和行,一般为了效率,关闭Cache;
        回写:只写Cache的行,当CPU没有命中时,Cache的行被写回到内存中.
      4. 一个CPU一个Cache,需要使用高速缓存侦听(snooping)来同步他们的内容.
      5. Pentium
        CPU可以为每一个页框指定Cache策略,Linux忽略之,使得所有的页框都启用Cache,且回写策略.
    11. 转换后援缓冲器(TLB):
      加速线性地址的转化.物理地址经过一次转化后放于TLB表项内.其每个CPU都有一个,但是不必同步,因为运行在现有CPU上的进程可以使同一线性地址于不同的物理地址关联.

         

  • Linux中的分页 
    1. 组成:页全局目录+页上级目录+页中间目录+页表.
      并在32位系统中将页中间目录和页上级目标级别的位设置为0来对应.
    2. 物理内存布局:
      Linux内核安装于RAM的第二个MB开始.原因:页框0由BIOS使用;
      640KB~1MB为洞(物理地址被保留但不能被使用).布局=内核代码+已初始化过的Data+未初始化过的Data.
    3. 进程页表:线性地址空间,用户态 < oXc000000, 内核态 >
      oXc0000000.
    4. 内核页表
为什么需要分页?

图片 1

分段式进程运行.png

最开始进程A,进程B,进程C已经被加载到了内存中。然后这时,进程B运行完毕,腾出20M可用内存,需要运行进程D,却发现,内存中没有足够大的内存空间可以加载进程D。这时,有
2 种方案:

  • 等待进程C运行完毕,这样就有足够连续的内存加载进程D。
  • 将进程A 的段 A3 或进程 C 的段 C1 换出到硬盘上,腾出一部分空间。

第一个方案比较简单直接,但是需要等待。
第二个方案虽然解决了内存不足的问题,但也有缺陷,假如,极端情况下,物理内存特别小,无法容纳任何一个进程段,就没法运行进程,也没法做到段的换入换出的问题。

所以问题的本质是什么?
在目前只分段的情况下,CPU认为线性地址等于物理地址,而线性地址是由编译器编译出来的,他本身是连续的,所以物理地址也必须要连续才可以。
我们需要做的就是打破线性地址和物理地址一一对应的关系,然后将它们建立一种映射关系,可以将线性地址映射到任意物理地址。

分页机制在段机制之后进行,以完成线性—物理地址的转换过程。段机制把逻辑地址转换为线性地址,分页机制进一步把该线性地址再转换为物理地址。

分段到底是怎么回事

相信学过操作系统课程的人都知道分段分页,但是奇怪的是书上基本没提分段分页是怎么产生的,这就导致我们知其然不知其所以然。下面我们先扒一下分段机制产生的历史。

一级页表

分页机制其实是建立在分段机制上的。这是因为内存分段是属于Intel
IA32架构骨子里的东西。

尽管在保护模式中段寄存器中的内容已经是选择子了
,但选择子最终就是为了找到段基址,其内存访问的核心仍是”段基址:段内偏移地址”,这2个地址相加后之后得到的是绝对地址,此地址在分段机制下被CPU认为是物理地址,是可以直接拿去地址总线上用。

图片 2

内存分段机制下的内存访问.png

但是如果CPU打开了分页机制,段部件输出的线性地址就不再等同于物理地址了,将其称之为虚拟地址。此虚拟地址对应的物理地址需要在页表中查找,这项工作是由页部件完成的。

为了搞清页部件的工作原理,我们需要明白:

  • 分页机制的思想
    分页机制的思想是:通过映射,可以使连续的线性地址与任意物理地址相关联,逻辑上连续的线性地址其对应的物理地址可以是不连续的。

图片 3

image.png

  • 页表的结构

    在内存中,最简单的映射是逐字节映射,即一个虚拟地址对应一个物理地址,若线性地址0x1对应0x10,我们需要一个结构来存储这种关系,这个就是页表,页表中的每一项称为页表项

    如果采取逐字节映射,显然是不可能的。4GB内存最大地址32位,则每一个页表项需要4字节,一共4G,则需要4G个页表项,那么页表就需要4G*4Byte
    = 16G,显然是不可能的。

    所以我们需要减少页表项的数目,增大内存块尺寸。

    图片 4

    页尺寸.png

所以现在,假设内存块尺寸为4K,则4G/4K = 2^20,

图片 5

页表和页表项.png

所以现在我们需要解决的是第2个问题,因为任一地址最终都会落到某一个物理页中,但我们还需要一个偏移地址才能访问到一个物理页的每一个字节。所以如”图页尺寸”,右边12位就可以拿来作偏移量。

段部件出来的32位地址,现在怎么从中得出足够信息将其翻译。

分页机制打开前,要将页表地址加载到控制寄存器CR3上。

一个页表对应一个页,所以虚拟地址的高20位作为页表项的索引,每个页表项占4位。我们将其乘以4就是该页表项相对于页表物理地址的偏移量。

这个偏移量加上页表地址,就得到了我们想要的物理页地址,再用虚拟地址的低12位作为偏移量加上物理页地址,就得到了真实的物理地址。

图片 6

一级页表地址翻译.png

硬件中的分页

分页机制由CR0中的PG位启用。如PG=1,启用分页机制,并使用本节要描述的机制,把线性地址转换为物理地址。如PG=0,禁用分页机制,直接把段机制产生的线性地址当作物理地址使用。分页机制管理的对象是固定大小的存储块,称之为页(page)。分页机制把整个线性地址空间及整个物理地址空间都看成由页组成,在线性地址空间中的任何一页,可以映射为物理地址空间中的任何一页(我们把物理空间中的一页叫做一个页面或页框(page
frame))

图片 7

80386使用4K字节大小的页。每一页都有4K字节长,并在4K字节的边界上对齐,即每一页的起始地址都能被4K整除。因此,80386把4G字节的线性地址空间,划分为1G个页面,每页有4K字节大小。分页机制通过把线性地址空间中的页,重新定位到物理地址空间来进行管理,因为每个页面的整个4K字节作为一个单位进行映射,并且每个页面都对齐4K字节的边界,因此,线性地址的低12位经过分页机制直接地作为物理地址的低12位使用。

实模式的诞生(16位处理器及寻址)

在8086处理器诞生之前,内存寻址方式就是直接访问物理地址。8086处理器为了寻址1M的内存空间,把地址总线扩展到了20位。但是,一个尴尬的问题出现了,ALU的宽度只有16位,也就是说,ALU不能计算20位的地址。为了解决这个问题,分段机制被引入,登上了历史舞台。
为了支持分段,8086处理器设置了四个段寄存器:CS, DS, SS,
ES.每个段寄存器都是16位的,同时访问内存的指令中的地址也是16位的。但是,在送入地址总线之前,CPU先把它与某个段寄存器内的值相加。这里要注意:段寄存器的值对应于20位地址总线的中的高16位,所以相加时实际上是内存总线中的高12位与段寄存器中的16位相加,而低4位保留不变,这样就形成一个20位的实际地址,也就实现了从16位内存地址到20位实际地址的转换,或者叫“映射”。

二级页表

现代操作系统都是2级页表,那为什么需要二级页表呢?

  • 一级页表最多可以容纳个1M(4G/4K)个页表项,每个页表项4字节,如果全部占满的话需要4M。
  • 一级页表中所有页表项必须提前建好,原因是操作系统要占用4GB虚拟地址空间中的高1GB,用户进程占用低3GB。
  • 每个进程都有自己的页表,进程一多,页表占用的空间就很大了。

归根结底,我们需要:不要一次性的将全部页表项建好,需要时动态创建页表项。

二级页表则很好的解决了这个问题。

无论几级页表,标准页尺寸都是4KB,所以4G内存最多有1M个页标准页,一级页表是将标准页放置到一张页表中。

二级页表是将1M个标准页平均放置到1024个页表中,每个页表包含1024个页表项,一个页表占用大小1024*4=4K,刚好是一个标准页的大小。

然后再用一个页目录表存储这些页表。每个页表的物理地址以页目录项(Page
Directory
Entry,PDE)的形式存储。1024个目录项,每个4字节,刚好也是4K,即一个标准页的大小。

图片 8

二级页表.png

前面一级页表的方法是将虚拟地址拆分成2部分,高20位虚拟地址用于定位一个物理页起始地址,低12位用于表示偏移地址。

在二级页表中,现在它们是这样的:
定位一个物理页,需要先找到其所在的页表,页目录中有1024个页目录表项,只需要10位二进制位就够了。然后再从页表中定位物理页,因为一个页表有1024个页表项,同样只需要10位二进制就够了。余下的12位仍然表示物理页内的偏移。

所以我们规定,32位虚拟地址中,我们用高10位(31-22)用于在页目录中定位页表,中间10位(21-12)用于在也表中定位物理页,低12位(11-0)用于表示物理页内偏移。

图片 9

二级页表地址翻译.png

为什么使用两级页表

假设每个进程都占用了4G的线性地址空间,页表共含1M个表项,每个表项占4个字节,那么每个进程的页表要占据4M的内存空间。为了节省页表占用的空间,我们使用两级页表。每个进程都会被分配一个页目录,但是只有被实际使用页表才会被分配到内存里面。一级页表需要一次分配所有页表空间,两级页表则可以在需要的时候再分配页表空间。

保护模式的诞生(32位处理器及寻址)

  • 80286处理器的地址总线为24位,寻址空间达16M,同时引入了保护模式(内存段的访问受到限制)
  • 80386处理器是一个32位处理器,ALU和地址总线都是32位的,寻址空间达
    4G。也就是说它可以不通过分段机制,直接访问4G的内存空间。虽然它是新时代的小王子,超越它的无数前辈,然而,它需要背负家族的使命–兼容前代的处理器。也就是说,它必须支持实模式和保护模式。所以,80386在段寄存器的基础上构筑保护模式,并且保留16位的段寄存器。
  • 从80386之后的处理器,架构基本相似,统称为IA32(32 Bit Intel
    Architecture)。
页表项 和 页目录项

图片 10

image.png

  • 页表物理地址
    32位地址应该用32位来表示,为什么这里会用20位?因为标准物理页大小是4K,所以最低12位总是0,所以用20位就够了。

  • AVL

  • D 位
    Dirty,意为脏页位,当 CPU
    对一个页面执行写操作时,就会设置对应页表项的 D 位为1

  • A 位
    Accessed,意为访问位,为1表示该页已经被CPU访问过了。

  • PCD 位
    Page-level Cache
    Disable,页级告诉缓存禁止位,若为1表示该页启用高速缓存。为0
    表示禁用高速缓存。

  • PWD 位

  • US 位
    User/Supervisor
    ,若为1表示处于User级,0,1,2,3特权级的程序都可以访问该页。若为0表示处于Supervisor级,特权级为3不允许访问该页。

  • G 位

  • RW 位
    1表示可读可写,为0表示可读不可写。

  • P 位
    P,Present,存在位,为1表示该页存在于物理内存中。

两级页表结构

两级表结构的第一级称为页目录,存储在一个4K字节的页面中。页目录表共有1K个表项,每个表项为4个字节,并指向第二级表。线性地址的最高10位(即位31~位32)用来产生第一级的索引,由索引得到的表项中,指定并选择了1K个二级表中的一个表。

两级表结构的第二级称为页表,也刚好存储在一个4K字节的页面中,包含1K个字节的表项,每个表项包含一个页的物理基地址。第二级页表由线性地址的中间10位(即位21~位12)进行索引,以获得包含页的物理地址的页表项,这个物理地址的高20位与线性地址的低12位形成了最后的物理地址,也就是页转化过程输出的物理地址。

图片 11

IA32的内存寻址机制

启用分页机制

启用分页机制,我们需要做3件事:

  • 准备好页目录表和页表
  • 将页表地址写入控制寄存器CR3
  • 寄存器CR0的PG位置1

在规划页目录表和页表时,我们得首先明白操作系统和用户进程的关系,用户进程运行在低特权级,用户进程需要访问硬件相关资源时,必须要向操作系统申请,之后将结果返回给操作系统,用户进程有很多,操作系统1个,怎么实现把操作系统共享给用户进程

图片 12

每个进程都有自己的虚拟空间.png

这个很简单,把操作系统属于用户进程的虚拟地址空间就可以了。把虚拟地址空间的高1G部分划分给操作系统,
0 – 3GB是用户进程自己虚拟地址空间。

; 准备好页目录表和页表
call setup_page
......
; 把页目录地址赋值给cr3
    mov eax, PAGE_DIR_TABLE_POS
    mov cr3, eax

; 打开CR0的PG位
    mov eax, cr0
    or eax, 0x80000000
    mov cr0, eax
.....
;----------------------------------------------------------
; setup_page
; 功能: 创建页目录和页表
; 参数: 无
;----------------------------------------------------------
setup_page:
    ; 先把页目录占用的空间逐字节清0
    mov ecx, 4096
    mov esi, 0
    .clear_page_dir:
        mov byte [PAGE_DIR_TABLE_POS + esi], 0
        inc esi
        loop .clear_page_dir

    ; 开始创建页目录项
    .create_pde:
    mov eax, PAGE_DIR_TABLE_POS
    add eax, 0x1000     ; eax = 0x100000 + 0x1000 = 0x00101000 
    mov ebx, eax        ; ebx = 0x00101000

    or eax, PG_US_U | PG_RW_W | PG_P     ; eax = 0x00101007
    mov [PAGE_DIR_TABLE_POS + 0x0], eax  ;       
    mov [PAGE_DIR_TABLE_POS + 0xc00], eax

    sub eax, 0x1000
    mov [PAGE_DIR_TABLE_POS + 4092], eax

    ; 下面创建页表项
    mov ecx, 256        ; 1M/4K=256
    mov esi, 0
    mov edx, PG_US_U | PG_RW_W | PG_P   ; edx=0x00000007

    .create_pte:
        mov [ebx + esi*4], edx

        add edx, 4096
        inc esi
        loop .create_pte

    ; 创建内核(高1G, 768-1023)页目录项
    mov eax, PAGE_DIR_TABLE_POS
    add eax, 0x2000     ; eax = 0x00100000 + 0x2000 = 0x00102000
    or eax, PG_US_U | PG_RW_W | PG_P       ; eax = 0x00102007

    mov ebx, PAGE_DIR_TABLE_POS     ; ebx = 0x00100000
    mov ecx, 254
    mov esi, 769
    .create_kernel_pde:
        mov [ebx+esi*4], eax
        inc esi, 
        add eax, 0x1000
        loop .create_kernel_pde

    ret

图片 13

页目录和页表.png

页目录项

图片 14

  • 第31~12位是20位页表地址,由于页表地址的低12位总为0,所以用高20位指出32位页表地址就可以了。因此,一个页目录最多包含1024个页表地址。
  • 第0位是存在位,如果P=1,表示页表地址指向的该页在内存中,如果P=0,表示不在内存中。
  • 第1位是读/写位,第2位是用户/管理员位,这两位为页目录项提供硬件保护。当特权级为3的进程要想访问页面时,需要通过页保护检查,而特权级为0的进程就可以绕过页保护。
  • 第3位是PWT(Page
    Write-Through)位,表示是否采用写透方式,写透方式就是既写内存(RAM)也写高速缓存,该位为1表示采用写透方式
  • 第4位是PCD(Page Cache
    Disable)位,表示是否启用高速缓存,该位为1表示启用高速缓存。
  • 第5位是访问位,当对页目录项进行访问时,A位=1。
  • 第7位是Page
    Size标志,只适用于页目录项。如果置为1,页目录项指的是4MB的页面,请看后面的扩展分页。
  • 第9~11位由操作系统专用,Linux也没有做特殊之用。

寻址硬件

在 8086
的实模式下,把某一段寄存器左移4位,然后与地址ADDR相加后被直接送到内存总线上,这个相加后的地址就是内存单元的物理地址,而程序中的这个地址就叫逻辑地址(或叫虚地址)。在IA32的保护模式下,这个逻辑地址不是被直接送到内存总线而是被送到内存管理单元(MMU)。MMU由一个或一组芯片组成,其功能是把逻辑地址映射为物理地址,即进行地址转换,如图所示。
图片 15
MMU

页面项

图片 16

80386的每个页目录项指向一个页表,页表最多含有1024个页面项,每项4个字节,包含页面的起始地址和有关该页面的信息。页面的起始地址也是4K的整数倍,所以页面的低12位也留作它用。

第31~12位是20位物理页面地址,除第6位外第0~5位及9~11位的用途和页目录项一样,第6位是页面项独有的,当对涉及的页面进行写操作时,D位被置1。

4GB的内存只有一个页目录,它最多有1024个页目录项,每个页目录项又含有1024个页面项,因此,内存一共可以分成1024×1024=1M个页面。由于每个页面为4K个字节,所以,存储器的大小正好最多为4GB。

IA32的三种地址

  • 逻辑地址:
    机器语言指令仍用这种地址指定一个操作数的地址或一条指令的地址。
    这种寻址方式在Intel的分段结构中表现得尤为具体,它使得MS-DOS或Windows程序员把程序分为若干段。每个逻辑地址都由一个段和偏移量组成。
  • 线性地址:
    线性地址是一个32位的无符号整数,可以表达高达232(4GB)的地址。通常用16进制表示线性地址,其取值范围为0x00000000~0xffffffff。
  • 物理地址:
    也就是内存单元的实际地址,用于芯片级内存单元寻址。
    物理地址也由32位无符号整数表示。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

相关文章