zoukankan      html  css  js  c++  java
  • MIT6.828——Lab2内存管理准备知识

    保护模式内存管理机制

    MIT6.828——Lab1 PartA

    MIT6.828——Lab1 PartB

    分段机制的问题

    ​ 分段的主要问题,出现在内存不足或者内存碎片过多的情况下。对于一个程序而言,例如其代码段长度就和其代码的长度直接相关,各个段的大小是不固定的,不能拆分的,要装入内存便一次性将一个整段都装入,因此在内存紧张时,就会出现问题。可以想象到的解决方法有这样一种:将段换出到磁盘上,从而空出一部分的物理内存空间。但是同样的,如果段长度过长,内存过小,在频繁的换入换出也无济于事。

    ​ 问题的本质在于,分段机制下产生的连续线性地址,被认为在物理内存上也是连续的,线性地址就是物理地址。但是我们可用的物理地址并不是连续的,因此就会产生冲突。所以解决这个问题的关键在于,是否可以:线性地址连续,对应的物理地址不连续?为了解除这种一一映射的关系,便可以通过地址映射。

    ​ 即 线性地址(虚拟地址)——页表——> 物理地址

    ​ 为了效率的问题,这种映射关系写在页表里,页表在内存中,查表的工作由硬件完成。

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

    分段机制的作用:将线性地址转化为物理地址;用大小相等(4KB)的页代替大小不等的段。

    页表结构

    一级页表

    首先针对一级页表而言,寻址过程可以由下图表示

    可以看到,分页机制仍旧是基于分段基础上的。将分段形成的线性地址,进行划分,利用高20位作为在页表内寻址的偏移量,寻址页表项,其中页表项的基址(物理地址)放在CR3寄存器中。再利用低12位,结合页表项给出的基址,合成物理地址,送上地址总线,即可寻址物理内存单元。可以看到,因为划分了低12位为页内偏移,因此页表的大小也就是4KB,这是一个常用的页大小值。而高20位,则说明,页表中含1M项页表项,占内存位4MB。

    二级页表

    ​ 在一级页表的铺垫下,便有了另一个问题。页表的大小为4M,且必须提前建立好,每个进程都有自己的页表,如果进程数很多,页表的内存开销便很可观,因此是否可以动态的创建页表项呢?解决这个问题的答案就是二级页表。二级页表的思想是,将1M页平均放到1K个页表中,每个页表1K个页表项,占据内存位4KB,刚好为一页的大小。为了存储这些页表,引入了页目录。每个页表的物理地址,都在页目录中以页目录项的形式存储。因为最多1K页表,因此页目录大小也为4KB,一页的大小。

    ​ 二级页表下的寻址过程如下:

    ​ 现在需要了解一下,页表项和页目录项的详细信息了。这部分信息,可以在Intel系统开发手册上得到详细说明。

    这里直接截取部分说明了:

    开启分页机制

    开启分页机制,需要做三件事:

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

    编程实例

    在之前的mit6.828实验1中,已经看到了一个比较基本的实例,如何进入保护模式,并进行分页操作。为了对于lab2有一个更好的理解,这里截取一部分《操作系统真象还原》的代码进行解释说明。这里可以和lab1部分结合来看

    建立GDT进入保护模式

    GDT_BASE:   dd    0x00000000 
    			dd    0x00000000
    
    CODE_DESC:  dd    0x0000FFFF 
    			dd    DESC_CODE_HIGH4
    
    DATA_STACK_DESC:dd    0x0000FFFF
    				dd    DESC_DATA_HIGH4
    
    VIDEO_DESC: dd    0x80000007
    			dd    DESC_VIDEO_HIGH4  
    
    GDT_SIZE    equ   $ - GDT_BASE
    GDT_LIMIT   equ   GDT_SIZE -1
    
    gdt_ptr  dw  GDT_LIMIT 
    		 dd  GDT_BASE
    		 
    ;-----------------  打开A20  ----------------
    in al,0x92
    or al,0000_0010B
    out 0x92,al
    ;-----------------  加载GDT  ----------------
    lgdt [gdt_ptr]
    ;-----------------  cr0第0位置1  ----------------
    mov eax, cr0
    or eax, 0x00000001
    mov cr0, eax
    
    jmp dword SELECTOR_CODE:p_mode_start	     ; 刷新流水线
    [bits 32]
    p_mode_start:
       mov ax, SELECTOR_DATA
       mov ds, ax
       mov es, ax
       mov ss, ax
       mov esp,LOADER_STACK_TOP
       mov ax, SELECTOR_VIDEO
       mov gs, ax
    

    按约定,GDT的第一个段描述符为空,这里建立了三个段,都是按照段描述符的规格进行性质填充,对照相关位的含义既可以知道段的信息。为了装载GDT,使用命令lgdt即可。进入保护模式后,寻址就需要使用段选择子,这在之前的lab1中也说到了。

    开启分页

    首先规划内存的整体布局,可以先画出下面这张图

    人为规定的,将页目录放在了物理地址0x100000处,将第一个页表,放在了物理地址0x101000处。同时划分进程的虚拟地址空间位高端1GB内核空间和低端3GB用户控件。首先需要注意的是,在虚拟内存空间中,将高端1GB完全分给了内核。这对于每个进程都是一样的,为了实现所有进程的内核共享,这部分空间固定占据了页目录项的第0xc00项至第1023项。对于页目录而言,第0项存储了第0个页表的位置,最后一项存储了页目录自身在物理内存中的位置。

    值得注意的是,页表0和页表c00都映射到了物理内存的低端1MB。这么做的原因是,在内核加载到内存空间之前,运行的一直是loader程序,它运行在低端1MB。为了保证之前段机制下的地址和现在分页后的地址一致,内核的前1MB也需要映射到物理内存低端1MB空间。低端的1MB=256*4KB,因此占据了256页,需要256个页表项。

    在这部分说明之后,下面便是具体的实现:

    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
    
    ;开始创建页目录项(PDE)
    .create_pde:				     ; 创建Page Directory Entry
       mov eax, PAGE_DIR_TABLE_POS
       add eax, 0x1000 			     ; 此时eax为第一个页表的位置及属性
       mov ebx, eax				     ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。
    
    ;   下面将页目录项0和0xc00都存为第一个页表的地址,
    ;   一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,
    ;   这是为将地址映射为内核地址做准备
       or eax, PG_US_U | PG_RW_W | PG_P	         ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
       mov [PAGE_DIR_TABLE_POS + 0x0], eax       ; 第1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性
       mov [PAGE_DIR_TABLE_POS + 0xc00], eax     ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,
    					     				     ; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.
       sub eax, 0x1000
       mov [PAGE_DIR_TABLE_POS + 4092], eax	     ; 使最后一个目录项指向页目录表自己的地址
    
    ;下面创建页表项(PTE)
       mov ecx, 256				                 ; 1M低端内存 / 每页大小4k = 256
       mov esi, 0
       mov edx, PG_US_U | PG_RW_W | PG_P	     ; 属性为7,US=1,RW=1,P=1
    .create_pte:				                 ; 创建Page Table Entry
       mov [ebx+esi*4],edx			             ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址 
       add edx,4096
       inc esi
       loop .create_pte
    
    ;创建内核其它页表的PDE
       mov eax, PAGE_DIR_TABLE_POS
       add eax, 0x2000 		     ; 此时eax为第二个页表的位置
       or eax, PG_US_U | PG_RW_W | PG_P  ; 页目录项的属性RW和P位为1,US为0
       mov ebx, PAGE_DIR_TABLE_POS
       mov ecx, 254			     ; 范围为第769~1022的所有目录项数量
       mov esi, 769
    .create_kernel_pde:
       mov [ebx+esi*4], eax
       inc esi
       add eax, 0x1000
       loop .create_kernel_pde
       ret
    

    在建立好了虚拟内存的布局之后,就可以正式开启分页机制

    call setup_page
    
    ;要将描述符表地址及偏移量写入内存gdt_ptr,一会用新地址重新加载
    sgdt [gdt_ptr]	      ; 存储到原来gdt所有的位置
    
    ;将gdt描述符中视频段描述符中的段基址+0xc0000000
    mov ebx, [gdt_ptr + 2]  
    or dword [ebx + 0x18 + 4], 0xc0000000      ;视频段是第3个段描述符,每个描述符是8字节,故0x18。
    ;段描述符的高4字节的最高位是段基址的31~24位
    
    ;将gdt的基址加上0xc0000000使其成为内核所在的高地址
    add dword [gdt_ptr + 2], 0xc0000000
    
    add esp, 0xc0000000        ; 将栈指针同样映射到内核地址
    
    ; 把页目录地址赋给cr3
    mov eax, PAGE_DIR_TABLE_POS
    mov cr3, eax
    
    ; 打开cr0的pg位(第31位)
    mov eax, cr0
    or eax, 0x80000000
    mov cr0, eax
    
    ;在开启分页后,用gdt新的地址重新加载
    lgdt [gdt_ptr]             ; 重新加载
    
  • 相关阅读:
    【转】c#文件操作大全(一)
    Visual Assist安装、破解方法
    web socket多线程实时监听
    SFTP上传下载
    数据库分页代码
    JAVA H5微信分享
    Eclipse中activiti插件的安装
    HTTP请求报文和HTTP响应报文
    CodeVS 1013&1029
    Codeforces 805D/804B
  • 原文地址:https://www.cnblogs.com/oasisyang/p/15421981.html
Copyright © 2011-2022 走看看