- A+
保护模式内存管理机制
分段机制的问题
分段的主要问题,出现在内存不足或者内存碎片过多的情况下。对于一个程序而言,例如其代码段长度就和其代码的长度直接相关,各个段的大小是不固定的,不能拆分的,要装入内存便一次性将一个整段都装入,因此在内存紧张时,就会出现问题。可以想象到的解决方法有这样一种:将段换出到磁盘上,从而空出一部分的物理内存空间。但是同样的,如果段长度过长,内存过小,在频繁的换入换出也无济于事。
问题的本质在于,分段机制下产生的连续线性地址,被认为在物理内存上也是连续的,线性地址就是物理地址。但是我们可用的物理地址并不是连续的,因此就会产生冲突。所以解决这个问题的关键在于,是否可以:线性地址连续,对应的物理地址不连续?为了解除这种一一映射的关系,便可以通过地址映射。
即 线性地址(虚拟地址)——页表——> 物理地址。
为了效率的问题,这种映射关系写在页表里,页表在内存中,查表的工作由硬件完成。
分页机制的思想:通过映射,可以使得连续的线性地址与任意的物理地址相关联,逻辑上连续的线性地址对应的物理地址可以不连续。
分段机制的作用:将线性地址转化为物理地址;用大小相等(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] ; 重新加载