Going Further on x86

来自osdev
Zhang3讨论 | 贡献2022年1月17日 (一) 05:44的版本 (创建页面,内容为“{{Rating|2}} {{Template:Kernel designs}} 你已经为 x86 完成了 Bare Bones。 现在呢,你可能想知道下一步。 欢迎来到操作系统开发世界! 以下指南假设你正在按照从上到下的顺序进行下面讨论的事情。 在开始实施之前,建议你阅读完整内容以获得更广阔的视野。 == 准备真实 == 在继续之前: * 你应该获取英特尔手册的副本。下面讨论的大多数特定于处理器…”)
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳到导航 跳到搜索
难度等级
Difficulty 2.png
中等
内核设计
模型
其它概念

你已经为 x86 完成了 Bare Bones。 现在呢,你可能想知道下一步。 欢迎来到操作系统开发世界!

以下指南假设你正在按照从上到下的顺序进行下面讨论的事情。 在开始实施之前,建议你阅读完整内容以获得更广阔的视野。

准备真实

在继续之前:

  • 你应该获取英特尔手册的副本。下面讨论的大多数特定于处理器的事情在英特尔手册中得到最好的描述。
  • 你应该确保自己有足够的耐心和时间。操作系统开发是最耗时的项目之一。

设计注意事项

将操作系统设计为一个整体,并且部分地设计是一项棘手但重要的任务。

代码结构化

你应该决定你的代码应该如何结构化。 考虑到你最终会将操作系统移植到不同的体系结构,使用不同的汇编指令,不同的初始化顺序,不同的硬件,不同的内存结构等。 你必须确保不会将一种体系结构中的文件与另一种体系结构中的文件混合在一起。 Meaty Skeleton 是如何构建代码的最小示例。

未来打样

考虑到你最终将希望在整个内核接口范围内添加新功能。 因此,你必须确保在不破坏依赖接口的情况下很容易重构它们。

多线程

意识到,从现在开始的一段时间内,你将不会在单个线程中运行代码,但是它将与其他线程和其他处理器并行运行,这意味着它最终将在关键操作期间被抢占,并且其他线程将能够破坏保存的状态。

硬件抽象

意识到并不是每一件硬件都存在于每台计算机中,所以你可能想要通过 硬件抽象层 来抽象它。 例如,PITHPET 是两个计时器,你可能希望将其抽象为一个定时接口。

算法

对于每个任务,找到从对你重要的各个方面 (例如,简单性,速度,内存使用等) 获得最多积分的算法。

= 分页 =

Paging 很好,因为它可以让你根据需要映射内存,并且可以让进程看到完整的地址空间。 它还提供了先进的保护能力。 你可能希望在 引导早期启用它。s.

Higher Half 较高的一半

你应该继续使用 higher half kernel,因此用户空间程序可以在4 MiB (或更低,如果你愿意) 加载,而不会与内核二进制文件冲突。 要做出的一个重要决定是在哪里精确映射内核。

许多人更喜欢将内核映射到0x80000000,留下2 GiB用于内核数据,2 GiB用于进程。 这可以允许单片内核缓存大文件或文件系统结构。

其他人则更喜欢将其映射到0xC0000000,为内核数据留出1 GiB,为进程留出3 GiB。 他们的主要论点是,它与 PAE 更好地集成,因为整个内核空间正好适合一个页面目录。

其他一些人 (包括本页的原始作者) 走极端,将内核映射为0xE0000000,留下了内核数据的512 MiB和进程的3.5 GiB的狭窄空间。 他们的主要论点是用户空间应该能够使用尽可能多的内存。

权限

Bare Bones 教你为内核使用 ELF 二进制文件。 你知道 .text.data.rodata.bss 是什么意思吗? 对,它们是可执行文件的部分。 在 .text 中存储了处理器的指令,在 .data 中有数据,在 .rodata 中有只读数据,在 .bss 中,存在未初始化的数据。 可能会有更多的部分,但现在让我们集中讨论这些。

这些部分中的每一个都应应用不同的权限,例如 .text 应该是只读的,.data 应该是读写的,.rodata 应该是只读的,并且 .bss 应该是读写的。 为了使这些更改在内核空间中生效,你必须在适当的寄存器中设置WP位。 确保在 x86-64PAE 模式下禁用非 .text 部分的执行。

为了便于在节上设置权限,你可以执行以下操作:

  • 告诉链接器在4个KiB边界处对齐它们,因此各节占据整个页面。
  • 告诉链接器插入指示特定部分的开始和结束地址的符号,以便你可以从映射代码中访问它们。

= 更多x86具体的东西 =

操作系统应尽可能自力更生。 引导加载程序可能使环境处于 “工作” 状态,但从长远来看并不方便。

在第一段更改之前创建一个 GDT,因为 GRUB 已经设置的那个不再有效 (条目仍然只是缓存在处理器中,这就是它 “工作” 的原因)。

你至少需要这些条目: 空段条目、内核代码段条目、内核数据段条目、用户代码段条目、用户数据段条目、 任务状态段 条目。

中断

每个实际操作系统都处理 异常 (例如 页面故障),并且仅在接收到数据时 (而不是轮询) 从外围设备读取。

创建一个 IDT。写入中断处理程序。 启用中断控制器 (例如 PICAPIC)。

确保在中断处理程序开始时保存所有寄存器,并在中断处理程序结束时恢复它们。 还请记住,某些异常会导致错误代码被推送到堆栈,而另一些则不会。

计时器

初始化计时器,以便能够跟踪时间。 考虑一下你想首先支持哪个计时器 (大多数初学者都使用 PIT,尽管它很古老),以及你想如何设置它 (大多数设置它以方便的间隔打勾,例如1 ms或10 ms)。 但是,请确保抽象界面,以便更容易添加对更多计时器的支持。

获取键盘输入

正文: PS/2 Keyboard

重要的是允许用户能够与操作系统进行交互。 可以使用IO端口读取键盘,但是你需要设置中断以获得适当的键盘支持。

= 内存管理 =

很快,你将需要分配一些在编译时不知道大小的东西。 这就是 内存管理器 的来源。

获取内存映射

你首先需要 获取内存映射,这样你就知道哪些物理区域是空闲的。 然后,你在此基础上再接再厉。

物理内存管理器

当然,你还需要一个免费的物理页面列表,因此你知道接下来要分配哪些物理页面框架。

一种常见的方法是创建一个链接列表,即将下一个空闲页面的物理地址存储在上一个空闲页面的开头,因此仅使用空闲内存来存储它。 但是,你已启用分页,因此你不能任意写入内存的每个部分。 相反,你可以一次映射一个页面框架,并向它写入下一个空闲页面的地址。 或者,你可以在较高的一半中针对所有物理内存有一个单独的映射: 它在64位内核中尤其常见,因为它简化了设计,几乎没有缺点。

虚拟内存分配器

你还将需要一种分配虚拟页面以映射物理内存的方法,而不是硬编码的值。 获取一种方法来跟踪地址空间的哪些部分被使用,哪些不被使用。

有多种方法可以跟踪地址空间。像Linux和Windows这样的现代操作系统使用AVL树,但是你也可以使用任何你喜欢的数据结构。

堆分配器

你肯定也想要一个 heap,或者你想一次以4KB的粒度继续分配?首先实现一个非常简单 (但缓慢) 的链表堆。 然后,你可以进行更复杂的设计,例如不同桶的单独块大小等。 你还应该记住,最终你的堆将失去内存,所以你需要实现堆扩展。

或者你可以选择另一个涉及 Slab Allocator.

调度器

如果无法安排任务,则没有操作系统是真实的。 每个现代桌面操作系统都应允许浏览web,同时渲染3D场景,同时在电子表格中对数据进行排序,同时将大文件写入磁盘。 这由 scheduler 负责。

多重处理

做好 multiprocessing 的准备。 尚未准备好进行多处理的调度程序可能会被完全重写。

优先级别

以某种方式设计调度程序,因此线程可以具有不同的优先级。

线程列表

通常建议每个状态和优先级都有不同的线程列表。 这样,调度代码不必浏览每个线程的高优先级线程,然后可能找不到一个,然后再次迭代线程列表以找到优先级较低的线程,然后可能再次失败,等等。 这意味着调度程序代码的运行速度更快,因为可以立即检测到缺少特定优先级别的线程,同时也不会遍历非活动线程。 <!-

算法

For the scheduling algorithm you would probably like having:

  • A thread state for each thread (saying if it's "running", "ready to run", or blocked for some reason).
  • A "switch_to(thread)" function that does nothing if the thread being switched to is currently running or, if the thread is not currently running:
    • Saves the currently running thread's register contents, etc.
    • Checks if the currently running thread's state is "running" and if it is, puts the currently running thread back into the "ready to run" state, and puts it on whatever data structure the scheduler uses to keep track of "ready to run" threads.
    • Sets the state of the thread being switched to to "running", and loads its register contents, etc.
  • A "find_thread_to_switch_to()" function that chooses a thread to switch to (using whatever data structure the scheduler uses to keep track of "ready to run" threads), removes the selected thread from the scheduler's data structure, and then calls the "switch_to(thread)" function.
  • A "block_thread(reason)" function which sets the currently running thread's state to whatever the reason is, then calls the "find_thread_to_switch_to()" function.
  • An "unblock_thread(thread)" function which sets a thread's state to "read to run", then decides if the thread being unblocked should preempt the currently running thread. If the thread being unblocked shouldn't preempt then it puts the thread on whatever data structure the scheduler uses to keep track of "ready to run" threads; and if the thread being unblocked should preempt it calls the "switch_to(thread)" function instead (to cause an immediate task switch, without bothering with the unnecessary overhead of the "find_thread_to_switch_to()" function).

-->

结论

操作系统开发不容易,也不难。 它之所以很难。因为与成熟的操作系统所涉及的复杂性相比,上述 (不完整) 列表还算不了什么。