【自制操作系统05】开启内存分页机制

通过前四章的努力,我们成功将控制权转交给了 loader.asm 这个程序,并且从实模式跨越到了保护模式。第四章讲保护模式的时候我说过,这是我们操作系统的第一个精彩之处。但其实这只是针对之前我们进行的只是无意义的输出,以及硬盘的加载等工作。但到了这一章,之前一步步的努力进入到了保护模式,也只能说是做了很多苦力,其实很多代码都是固定的,给我们发挥的空间也不大。

但是到了本章,可以说终于有能体现出我们设计能力的地方了。

一、实现分页要做哪些事

还是先直接简单说要做的事,再说为什么,实现分页要做以下三件事:

  1. 在内存某位置写好页表
  2. 页目录地址赋值给 cr3 寄存器
  3. 将 cr0 寄存器的 pg 位置 1

我们对比下进入保护模式中实现段描述符机制需要做的三件事:

  1. 在内存某位置写好段描述符表
  2. 段描述符表地址赋值给 gdtr 寄存器
  3. 将 cr0 寄存器的 pe 位置 1(这个其实是开启保护模式)

你看,是否是非常相似呢?都是内存某位置准备xxx,把起始地址赋值给一个特定的寄存器,然后将另一个特殊寄存器的某位置 1 表示开启。所以上一章我说过,cpu 与操作系体打配合,这种模式运用得非常多。我们写操作系统的人不用管 cpu 的具体实现,只需要按照指定步骤操作即可,之后硬件会帮我们完成所需要的功能。

二、为什么要分页

说实话我也想不明白为什么要分页,主要是我说不上来为什么不是其他方式,所以这块我也只能跟着官方说的去理解了。

如果只用段式管理的话,段大小不一致,且同一个程序逻辑地址和物理地址都是连续的。段大小不一致导致内存有大段有小段,也会留下一些内存碎片,过大的段查不进来,过小的段插进去又会产生更小的碎片。同一个段内所有的程序地址都是连续的,这也导致不灵活,我们希望能有一套机制使得程序所用的逻辑地址连续,但实际映射到的物理地址并不连续,增加这么一个层来解决这个问题。

我们本讲只是准备一些必要的页表,然后开启页表机制。等到后面多任务的时候才能真正体会到页表的用处以及好处,所以我们姑且先简单理解下,至于具体的好处,其实有好多细节的,等以后用到的时候慢慢体会。

三、页表长什么样以及虚拟地址到物理地址的转换

我们可以类比段的转化,我们最初给的地址是 段选择子:段内偏移值,在保护模式下,用段选择子去内存中的段描述符表中,找到段描述符,取出段基址,再+段内偏移地址,得到最终的物理地址。

页的转化也是类似的,上一步通过段描述符得到的“物理地址”,再开启分页后叫做逻辑地址。这个逻辑地址也是分成 前半部分:后半部分 这种形式,用前半部分的值在页表中寻找并换出一个页地址(也可以理解成基址这个概念),然后再拼接上后半部分的值,得到最终的物理地址。

只不过,现在的页表方案一般是二级页表,第一级叫页目录表(PDE),第二级叫页表(PTE)。然后这个逻辑地址就是被看成 高10位:中间10位:后12位。高10位负责再页目录表中找到一个页目录项,这个页目录项的值加上中间10位拼接后的地址去页表中去寻找一个页表项,这个页表项的值,再加上后12位,拼接后的地址就是最终的物理地址。

12位可以表示 4K,所以也就是一个页可表示的内存大小为 4KB。10位可以表示 1K,所以页目录表中最多有 1024 个页目录项,一个页表中最多有 1024 个页表项,那最大可表示的内存范围就是 1024 * 1024 * 4KB = 4G。其实这也是废话,你可以仔细想想看,不论你分成几级页表,只要是通过这种方式寻址的,只要是一个 32 位的地址,总是可以表示 4G 大小的。只不过通过你的不同分法,可能导致页大小,页目录项数目,页表数目,以及假如你定了 n 级页表后的 n 级页表的页表项数目不同而已。

页目录表和页表的数据结构

虚拟地址到物理地址的转换

四、页表设计

我们这样设计页表:

  • 页目录表的第 0 项和第 768 项,都对应紧接着的第一个页表,映射了低端 1M 的物理内存(0x00000-0x100000),也就是说逻辑地址的开端 1M 和 3G 以上的第一个 1M 地址,都对应这物理内存的地段 1M。
  • 页目录表的第 769~1022 项,分别往后对应 254 个页表,不过这些页表还没有写,先空着
  • 页目录表的第 1023 项,其地址指向该页目录表本身(也就是把页目录表当作页表去理解了),通过这种方式可以访问页目录表本身。(这块其实我也没理解为啥要这么搞,无非就是想用虚拟地址访问到这个页表本身嘛。

为什么这样设计呢?

因为我们分页之前的代码(loader)都在低端 1MB 范围内,所以开启分页之后的逻辑地址开始的 1M 也要一一对应上物理地址的开始 1M,所以有了第 0 个页目录项。第 768 个页目录项对应着逻辑地址 3G 以上的 4M( 0xc0000000~0xc03fffff 不过我们页表只写了 256 项也就是规划了 1M),这是因为我们决定将操作系统内核写在 3G 以上的 1M 空间里

我们规划,虚拟地址的 0~3G 是用户空间,3~4G 是内核空间,所以我们提前把页目录表的第 769~1022 项建好,至于为什么以后再说。

五、上代码

loader.asm

...
;创建页表并初始化(页目录和页表)
PAGE_DIR_TABLE_POS equ 0x100000
call setup_page

;重新加载 gdt,因为已经变成了虚拟地址方式
sgdt [lgdt_value]
mov ebx,[lgdt_value+2]
or dword [ebx+0x18+4],0xc0000000
add dword [lgdt_value+2],0xc0000000
add esp,0xc0000000

;页目录表起始地址存入 cr3 寄存器
mov eax,PAGE_DIR_TABLE_POS
mov cr3,eax

;开启分页
mov eax,cr0
or eax,0x80000000
mov cr0,eax

;重新加载 gdt
lgdt [lgdt_value]

mov byte [gs:0x1e0],'p'
mov byte [gs:0x1e2],'a'
mov byte [gs:0x1e4],'g'
mov byte [gs:0x1e6],'e'
mov byte [gs:0x1ea],'o'
mov byte [gs:0x1ec],'n'

jmp $

setup_page:
;先把页目录占用的空间逐字清零
    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:
    mov eax,PAGE_DIR_TABLE_POS
    add eax,0x1000; 此时eax为第一个页表的位置及属性
    mov ebx,eax
    or eax,111b
    mov [PAGE_DIR_TABLE_POS],eax
    mov [PAGE_DIR_TABLE_POS+0xc00],eax
    sub eax,0x1000
    mov [PAGE_DIR_TABLE_POS+4*1023],eax

;开始创建页表项(PTE)
    mov ecx,256
    mov esi,0
    mov edx,111b
.create_pte:
    mov [ebx+esi*4],edx
    add edx,4096
    inc esi
    loop .create_pte

;创建内核其他页表的页目录项(PDE)
    mov eax,PAGE_DIR_TABLE_POS
    add eax,0x2000
    or eax,111b
    mov ebx,PAGE_DIR_TABLE_POS
    mov ecx,254
    mov esi,769
.create_kernel_pde:
    mov [ebx+esi*4],eax
    inc esi
    add eax,0x1000
    loop .create_kernel_pde
    ret
...

六、运行

Makefile 仍然和上一章一样,所以直接执行 make brun

可以看到分页开启后,成功在屏幕输出了 pageon 字符串

七、学到这的一些感悟

我之前写过一篇文章 究竟什么是技术,还被好多人骂了。我文章里说的就是感觉现在做的事情(Java),以及好多好多所谓的技术,都只是应用而已,甚至觉得只有基础科学,只有研究质子中子电子,这些东西才算是真正的技术,其他的只是应用而已。

不过现在我知道自己的问题所在了,因为我研究操作系统就是想做点真正的技术。但现在看来,如果还延续当时的想法,像开启分页,进入保护模式,往显卡映射的内存写数据,这些都应该只叫做应用。因为这些的底层原理是 cpu 硬件电路的布线方式,我们的操作系统只是应用了它们,把一些操作封装起来再暴露给用户而已。

但如果真这样深入下去,其实是没完没了的,你的求知欲又会深入到物理层面,这其实跟计算机技术已经相差甚远了。所以我现在觉得,把底层细节当作已知,在这上面建立一套完善的体系,这本身就是这一层的技术了,每一层有每一层技术的复杂性,不能说越底层的才越接近技术,越接近真理。

所以,你可以不断深入探索底层的技术,但大可不必对自己所研究层次的知识妄自菲薄。

写在最后:开源项目和课程规划

如果你对自制一个操作系统感兴趣,不妨跟随这个系列课程看下去,甚至加入我们,一起来开发。

参考书籍

《操作系统真相还原》这本书真的赞!强烈推荐

项目开源

项目开源地址:https://gitee.com/sunym1993/flashos

当你看到该文章时,代码可能已经比文章中的又多写了一些部分了。你可以通过提交记录历史来查看历史的代码,我会慢慢梳理提交历史以及项目说明文档,争取给每一课都准备一个可执行的代码。当然文章中的代码也是全的,采用复制粘贴的方式也是完全可以的。

如果你有兴趣加入这个自制操作系统的大军,也可以在留言区留下您的联系方式,或者在 gitee 私信我您的联系方式。

课程规划

本课程打算出系列课程,我写到哪觉得可以写成一篇文章了就写出来分享给大家,最终会完成一个功能全面的操作系统,我觉得这是最好的学习操作系统的方式了。所以中间遇到的各种坎也会写进去,如果你能持续跟进,跟着我一块写,必然会有很好的收货。即使没有,交个朋友也是好的哈哈。

目前的系列包括

原文地址:https://www.cnblogs.com/flashsun/p/12234807.html

时间: 2024-10-17 11:17:42

【自制操作系统05】开启内存分页机制的相关文章

操作系统实现之内存分页机制.虚拟空间

内存虚拟存储主要是为了将一个进程分为不同页.存储到不同物理页中.然而不同进程的虚拟地址是可以相同的.因为MMU把进程的虚拟地址映射到各个不同的物理地址中. 以下操作系统采用二级分页.一开始CS:IP寄存器.将CS的基地址跟IP的偏移地址进行相加.得到线性地址.接着.线性地址的高10位用于当作页目录表的索引.页目录表保存的是页表的物理地址.接着.线性地址的低10位用于当作页表的索引.页表保存的是4k大小的页块.线性地址低10位那就是页块的偏移地址.由此虚拟地址->物理地址转换结束.当然操作系统在创

32机的内存分页机制

在实模式下寻址的时候,"段寄存器+偏移地址"进过转换计算以后得到的地址是"物理地址",也就是在物理内存中的实际地址,而在保护模式下,"段选择器+偏移地址"转换后的地址被称为"线性地址"而不是"物理地址",那么线性地址就是物理地址吗? 答案可能是,也可能不是,这取决于80386的内存分页机制是否被使用. 为什么有内存分页机制? 我们回顾一下,单任务的DOS系统中,一个应用程序可以使用所有的空闲内存,程序退出以

轻量级操作系统FreeRTOS的内存管理机制(三)

本文由嵌入式企鹅圈原创团队成员朱衡德(Hunter_Zhu)供稿. 轻量级操作系统FreeRTOS的内存管理机制(二)中讲到,heap2.c的内存管理机制会导致内存碎片的问题,系统运行久后会出现无法分配大块内存的情况,heap4.c中的管理机制提供了解决方法,它是在heap2.c的基础上添加了地址相邻空闲块间合并的功能,而heap5.c是对heap4.c的进一步扩展,它能够支持多块不连续分布的RAM空间作为堆使用,本篇将对heap4.c.heap5.c中的管理机制进行分析. 一.heap4.c

轻量级操作系统FreeRTOS的内存管理机制(一)

本文由嵌入式企鹅圈原创团队成员朱衡德(Hunter_Zhu) 近几年来,FreeRTOS在嵌入式操作系统排行榜中一直位居前列,作为开源的嵌入式操作系统之一,它支持许多不同架构的处理器以及多种编译工具链,具有轻量级.容易移植和使用的特点.本篇文章将会对FreeRTOS提供的几种内存分配策略进行介绍,FreeRTOS允许开发者根据自己的项目实际需要选择不同的内存分配策略或者自定义分配内存策略. 一.FreeRTOS内存分配源码 FreeRTOS在创建任务.队列.互斥量.信号量.软件定时以及事件组的时

轻量级操作系统FreeRTOS的内存管理机制(二)

本文由嵌入式企鹅圈原创团队成员朱衡德(Hunter_Zhu)供稿. 上一篇文章中介绍了FreeRTOS多种内存管理机制中最简单的一种:全局声明一个静态数组ucHeap,然后通过指针偏移记录空间的分配情况,在这种内存机制下无法对内存进行释放.同时也介绍了内存操作过程中字节对齐的细节,本篇文章将会对FreeRTOS源码中第二种内存管理机制heap2.c进行讲解,在heap2.c中同样使用一个全局静态数组ucHeap来表示内存,heap2.c内存管理机制较heap1.c而言增加了内存释放的功能,通过使

CPU内存管理和linux内存分页机制

一.基本概念 物理地址(physical address) 用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应. ——这个概念应该是这几个概念中最好理解的一个,但是值得一提的是,虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到 最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样.所以,说它是“与 地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与

【自制操作系统06】终于开始用 C 语言了,第一行内核代码!

一.整理下到目前为止的流程图 写到这,终于才把一些苦力活都干完了,也终于到了我们的内核代码部分,也终于开始第一次用 c 语言写代码了!为了这个阶段性的胜利,以及更好地进入内核部分,下图贴一张到目前为止的流程图.(其中黄色部分是今天准备做的事情) 二.先上代码 loader.asm ... ;加载kernel mov eax,0x9 ;kernel.bin所在的扇区号 0x9 mov ebx,0x70000 ;写入的内存地址 0x70000 mov ecx,200 ;读入的扇区数 call rd_

【自制操作系统07】深入浅出特权级

一.到目前为止的程序流程图 本讲我们不继续写任何代码,而是专门拿出一讲来说说特权级的事,为后续的工作做一个知识储备.这段内容太难啃了,也可能我恰好对这块不太感冒,反正我是恶心了好久才啃下来. 为了让大家清楚目前的程序进度,画了到目前为止的程序流程图,如下 二.什么时候处理器会进行特权级检查 为什么要进行特权级检查,我就不说太多了,简单理解,操作系统不希望用户进程访问内核数据,所以需要给指令呀还有数据呀都附上一个特权级的属性,让程序受限制. 特权级分为 0 1 2 3 四种,我们常说的 用户态 就

【自制操作系统11】中场休息之细节是魔鬼

如果你有幸看到这一章,那么恭喜你,你已经完成了整个操作系统的一多半了,而且如果你前面的东西都完全掌握了,那后面无非就是顺水推舟的事情了.本章不做继续的讲解,而是将之前的知识进行回顾,并把相似的知识点做对比.同时我也将到目前为止最大的感悟 细节是魔鬼 分享给大家. 一.到目前为止的程序流程图 为了让大家清楚目前的程序进度,画了到目前为止的程序流程图,如下.其实就是截至到内存管理这一块 二.回顾一下我们都做了些什么 这里列出整个系列到目前为止的目录,刚好也可以作为索引方便大家阅读了 [自制操作系统0