关于操作系统:从零开始写-OS-内核-虚拟内存初探

83次阅读

共计 5556 个字符,预计需要花费 14 分钟才能阅读完成。

系列目录

  • 序篇
  • 筹备工作
  • BIOS 启动到实模式
  • GDT 与保护模式
  • 虚拟内存初探
  • 加载并进入 kernel
  • 显示与打印
  • GDT 和 IDT,中断解决
  • 虚拟内存欠缺
  • 实现堆和 malloc
  • 创立第一个内核线程
  • 多线程运行与切换
  • 锁与多线程同步
  • 过程的实现
  • 进入用户态
  • 一个简略的文件系统
  • 加载可执行程序
  • 零碎调用的实现
  • 键盘驱动
  • 运行 shell

kernel 虚拟内存概览

接上一篇 GDT 与保护模式,这一篇将是 loader 的重点。首先咱们须要建设 kernel 空间的虚拟内存。如果你对虚拟内存的原理还不相熟,请务必先自学,这里能够提供一个文档供参考。

到目前为止咱们始终在物理内存上操作,确切地说是在 1MB 的低地址空间内操作,这所有都很简略间接。然而接下来 loader 行将为加载 kernel 做筹备,咱们须要在更广大的 4GB 虚拟内存空间上布局数据和代码。

仿照 Linux 零碎,咱们将应用 3GB 以上的高地址空间作为内核空间来发展后续所有工作。例如最根本的,目前的物理低地址 1MB 会被映射到 virtual 地址 0 ~ 1MB 以及 3GB 以上空间 0xC0000000 ~ (0xC0000000 + 1MB) 处:

进入 kernel 当前,对低 1MB 空间的拜访将会应用 0xC0000000 ~ (0xC0000000 + 1MB) 虚拟地址,这里次要包含以后应用的 stack,以及显示器对应的内存映射:

所以 video 内存基地址将从 virtual 地址 0xC00B8000 开始,不过目前不用深究,后续将会在显示与打印一篇中详解。


除了最根本的低 1MB 内存空间,loader 还须要进一步在 0xC0000000 以上的 virtual 空间中开疆拓土,这次要包含两局部:

  • kernel 所应用的页目录(page directory)和页表(page table);
  • kernel 二进制镜像的读取,以及代码、数据的加载;

上面给出整个 loader 阶段将要搭建的 virtual-to-physical 内存映射关系图:

这张图是本篇最重要的全局图,其中第二行是第一行通过“扭曲”比例的图示,咱们将 3GB 以下的用户空间放大显示,以后重点只关注 3GB 以上的内核空间(粗框局部)。因为是 virtual 地址空间,咱们的空间划分能够比拟随便和“侈靡”,咱们以 4MB 为单位,从 0xC0000000 开始在 virtual 空间切割划分出以下几个区域:

  • 第一个 4MB 保留,其中低 1MB 空间映射到了 physical 地址的低 1MB,这是下面曾经解释过的;
  • 第二个 4MB(橙色)用来映射 kernel 的所有 page tables
  • 第三个 4MB(绿色),即从 0xC0800000 开始,作为加载、寄存 kernel 代码和数据的空间,也就是说 kernel 从该处开始编址;

这里要说一句,实现一个 OS 并没有固定的形式,以上只是我集体的实现形式。实际上对于内存的布局是很灵便的,就像这个我的项目的名字 scroll 一样,内存就是一幅画卷,CPU 则是画笔,在遵循肯定规定的前提下,能够做自由发挥。

上面咱们首先开始橙色局部,即内核 page directorypage tables 的建设。

建设 kernel 虚拟内存

在开始这一段之前,咱们还是回顾一下页目录(page directory)和页表(page table)的相干原理。

有一些要害数字须要记住:

  • 页(page)的大小为 4096;
  • 页目录项 pde (page directory entry) 和页表项 pte (page table entry),实质上是一样的构造,大小为 4 bytes
  • page direcotry 一共有 1024 项,指向总共 1024 张 page table,一共 4MB
  • 每个 page table 都有 1024 项,指向 1024 张 pages,治理着 1024 * 4KB = 4MB 的 virtual 空间;
  • 所以每个 pde 治理着 4MB 的 virtual 空间;

好了,上面咱们开始建设 kernel 空间的页表。依照常规给出代码链接:这一部分相干的代码从函数 setup_page 开始,供你参考。

从这里开始以下,依照术语常规,virtual 页我将用 page 表述,而 physical 页将用 frame 来表述。

建设 page directory

首先咱们须要拿出一个 frame,用来作为 page directory。回到 physical 内存散布的那张图,目前 1MB 以下的局部已被占用,咱们能够应用的局部就从 1MB 即 0x100000 开始。

我抉择的是 0x100000 + 4KB,即 0x100000 后的第 2 个 frame 作为 page directory,当然这齐全是集体抉择;0x100000 后的第 1 个 frame 我抉择将它作为第一个 page table

再次强调,这是我的集体抉择;frame 的抉择是十分自在的,只有是还没被占用的都能够应用,当然了你要记住本人用过了哪些 frames,正当紧凑并且尽量“好看”地布局应用。

映射 1MB 低内存空间

值得注意的是,第 0 和第 768 个 pde 都指向了同一个 page table,这个 page table 咱们将用它映射 0 ~ 1MB 低内存,即咱们目前所处的 1MB 内存空间。当然这个 page table 能够治理 4MB 的空间,咱们只映射了其中的 1MB,残余 3MB 的 virtual 空间就闲置了,不过这没有关系,闲置就闲置,反正这是 virtual 空间。

第 768 个 pde 治理的是 0xC0000000 即 3GB 开始的第一个 4MB 空间,回到本篇开始的第一张图,这里也将被映射到低 1MB 内存上:

映射 page directory 以及 page tables 自身

这里是本节的重点和难点。咱们晓得 page directorypage tables 所指向的都是 physical 页,而一旦关上了 paging 模式,咱们当前所有对内存的拜访将全副通过 virtual 地址,无奈再间接操作 phyical 地址。那么问题来了,咱们如何拜访并批改 page directorypage tables 自身呢?

一种办法当然是在须要时敞开 paging,间接拜访 physical 地址,之前举荐的教程 JamesM’s kernel development tutorials 在很多中央都是这么做的,不过这并不是一种好的做法,起因有以下几点:

  • 进入简单的 kernel 当前,代码的执行会大量波及到 stack 和 heap,以及其余全局变量等内存拜访,这些全部都是 kernel 空间的 virtual 地址,如果此时忽然敞开 paging,对它们的拜访将无奈进行。你必须十分小心地安顿你的代码对内存的拜访,否则将会呈现不可预知的结果,然而这其实十分难做到;
  • 一旦开启多线程,如果在敞开 paging 的状况下产生了中断,CPU 将进行一些主动的 stack 操作以及中断解决,全部都是对 virtual 地址的操作,显然其后果也是灾难性的;

一个更正当的做法是,咱们将 page directorypage tables 自身也映射到 virtual 空间,这样就能够像拜访其余失常内存一样拜访它们。从实质上说 page directorypage tables 无非也是一些 page,齐全能够和其它内存拜访厚此薄彼。问题就是,应该如何建设这种映射?来看下图:

咱们将 pde[769] 指向了 page directory 这个 frame 自身。这样 page direcotry 实际上同时也充当了一个 page table,它所治理的正好是 1024 张 page tables 自身,一共 4MB。这 1024 张 page tables,其中有一张就是 page direcotry 它本人。

是不是有点绕?换言之,因为 pde[769] 指向了 page directory 它本人,因而 0xC0400000 ~ 0xC0800000 这 4MB 的 virtual 空间,当初被映射到了 1024 张 page tables 上,而且更好的是,它们的 virtual 地址是齐全间断地,严密地排布在这 4MB 空间里。

由此,下面的问题曾经解决,page tables 对应的 virtual 地址空间为:

0xC0400000 ~ 0xC0800000

这是 4GB 空间中第 769 个 4MB 空间(总共 1024 个 4MB 空间,组成 4GB)。

并且咱们同时还失去了 page directory 它本人的 virtual 地址为:

0xC0701000

0xC0400000 ~ 0xC0800000 这 4MB 空间中的第 769 个 page,是不是很奇妙:)


这里的核心思想是,page directory 其实实质上是一个非凡的 page table,它和其它 page table 一样,都治理着 4MB 的空间。

如果感觉还是有点绕的话,你无妨反过来验证一下,从下面给出的 virtual 地址开始,推导理论指向的 physical 地址是哪里,我想很快就能理清这外面的逻辑。

如果你进一步思考的话,就会发现这并不是惟一的实现形式。你齐全能够不抉择 pde[769],而应用其它 virtual 空间来映射 page tables,例如用 pde[770] 也能够,这样所有 page tables 对应的 virtual 空间就变成了 0xC0800000 ~ 0xC0C00000。用 pde[769] 只是我集体的抉择,因为它是 0xC0000000 后的第二个 4MB 空间,这样的安顿,virtual 空间的应用能比拟紧凑参差一点。

映射 kernel 空间的其它区域

到目前为止,pde 768 和 769 曾经被应用,即 0xC0000000 ~ 0xC04000000xC0400000 ~ 0xC0800000 这两块 4MB 空间已被征用。剩下的 pde[770] ~ pde[1023] 对应的 254 个 page tables,咱们顺次为它们安顿上 frames。这样咱们最终征用了 256 个 pages & frames,总共 1MB 的内存(virtual & physical),来建设 kernel 空间(3GB ~ 4GB)的 page tables,治理这 1GB 的空间。

咱们将本章开始的那个 virtual-to-physical 内存映射关系图中的橙色局部抽出放大,展现 kernel 的 256 张 page tables 的内存散布:

留神到咱们只调配了 kernel 空间即 3GB 以上的 page tables,共 256 张,占地 1MB,它们映射的也是 0xC0400000 ~ 0xC0800000 空间的后 1/4 局部即 0xC0700000 ~ 0xC0800000;而 3GB 以下的用户空间此时并没有调配 page tables,因为目前咱们并没有应用到。

这 256 张 kernel 页表(其中有一张是 page directory 自身),是咱们编写 kernel 期间最外围的 page tables,并且在 page directory 里建设了 pde[768] ~ pde[1023] 这全副的 256 个表项,指向了这些 page tables。

其实除了前两个 page table,前面 254 个目前都是空的,没有被用到,咱们只是为它们安顿好了 frame 而已。这里用去了足足 1MB 的 physical 内存,这看上有点侈靡了,毕竟这个我的项目配置里 physical 内存总共只有 32 MB(见 bochsrc.txt,当然当初的计算机内存远不止 32 MB,这曾经不是个问题)。这样做有一个十分重要的起因,那就是这 256 张 kernel page tables 前面将被所有的过程(process)共享,也就是说对于用户 process 而言,3GB 以下的空间是隔离的,而 3GB 以上的 kernel 的空间是共享的,这也是天经地义的,否则就有多个 kernel 在内存中独立运行了。

每次 fork 出一个新的 process,它的 page directory 的 768 ~ 1023 项将会间接复制 kernel 的 page directory 的 768 ~ 1023 项,独特指向这 256 张 kernel page tables。所以咱们要求这 256 张 page tables 从一开始就固定下来,前面也不再变动,这样能力实现所有 process 共享的成果。

关上 paging

page tables 都准备就绪当前,就能够关上 paging 了:

enable_page:
  sgdt [gdt_ptr]

  ; move the video segment to > 0xC0000000
  mov ebx, [gdt_ptr + 2]
  or dword [ebx + 0x18 + 4], 0xC0000000

  ; move gdt to > 0xC0000000
  add dword [gdt_ptr + 2], 0xC0000000

  ; move stack to > 0xC0000000
  mov eax, [esp]
  add esp, 0xc0000000
  mov [esp], eax

  ; set page directory address to cr3 register 
  mov eax, PAGE_DIR_PHYISCAL_ADDR
  mov cr3, eax
  
  ; enable paging on cr0 register
  mov eax, cr0
  or eax, 0x80000000
  mov cr0, eax

这里最重要的就是设置 CR3 寄存器,使之指向 page directory 的 frame(留神是 physical 地址),而后关上 CR0 寄存器上的 paging 比特位开关。

总结

至此,loader 阶段对于 kernel 虚拟内存初始化的局部就完结了。这一段的代码并不长,外围仅仅是 setup_page 这一个函数,然而其背地的原理却是十分粗浅简单。在前面进入 kernel 之后,咱们将进一步欠缺虚拟内存相干的工作,这将包含缺页异样 (page fault) 的解决,过程 page directory 的复制等。虚拟内存的解决是贯通 kernel 实现和运行的底层外围工作,必须保障相对的正确和稳固。一旦出错,零碎会立即呈现各种难以预知的奇怪谬误甚至解体,并且 debug 十分艰难。

下一篇咱们将会加载真正的 kernel 到内存并且转到 kernel 开始执行代码,这将是进入 kernel 前的最初一道关卡。

正文完
 0