“Going Further on x86”的版本间差异
(创建页面,内容为“{{Rating|2}} {{Template:Kernel designs}} 你已经为 x86 完成了 Bare Bones。 现在呢,你可能想知道下一步。 欢迎来到操作系统开发世界! 以下指南假设你正在按照从上到下的顺序进行下面讨论的事情。 在开始实施之前,建议你阅读完整内容以获得更广阔的视野。 == 准备真实 == 在继续之前: * 你应该获取英特尔手册的副本。下面讨论的大多数特定于处理器…”) |
小 (→多进程) |
||
(未显示同一用户的3个中间版本) | |||
第1行: | 第1行: | ||
{{Rating|2}} {{Template:Kernel designs}} | {{Rating|2}} {{Template:Kernel designs}} | ||
你已经为 [[x86]] 完成了 [[Bare Bones]]。 | 你已经为 [[x86]] 完成了 [[Bare Bones]]。 现在呢,你可能开始会有一点困惑。 欢迎来到操作系统开发世界! | ||
以下指南假设你正在按照从上到下的顺序推进着下面讨论的工作。 在开始实施之前,建议你先阅读完整内容以获得更广阔的视野。 | |||
== | == 准备面对实战 == | ||
在继续之前: | 在继续之前: | ||
* | * 你应该获取英特尔手册的副本。 下面讨论的大多数特定于处理器的内容在英特尔手册中都有最好的介绍。 | ||
* 你应该确保自己有足够的耐心和时间。操作系统开发是最耗时的项目之一。 | * 你应该确保自己有足够的耐心和时间。操作系统开发是最耗时的项目之一。 | ||
== 设计注意事项 == | == 设计注意事项 == | ||
将操作系统为一个整体设计,而又一部分一部分的完成,是一项棘手但重要的任务。 | |||
<!-- | <!-- | ||
=== 内核模型 === | === 内核模型 === | ||
你应该决定你将实现哪个内核模型。 会是一个 [[Monolithic Kernel| | 你应该决定你将实现哪个内核模型。 会是一个 [[Monolithic Kernel|单内核]]? 会是一个 [[Microkernel|微内核]]? 会是一个 [[Kernel#Subset Kernel Models|其它设计]]? 或者一些全新的东西?由你决定! | ||
--> | --> | ||
=== 代码结构化 === | === 代码结构化 === | ||
你应该决定你的代码应该如何结构化。 考虑到你最终会将操作系统移植到不同的体系结构,使用不同的汇编指令,不同的初始化顺序,不同的硬件,不同的内存结构等。 | 你应该决定你的代码应该如何结构化。 考虑到你最终会将操作系统移植到不同的体系结构,使用不同的汇编指令,不同的初始化顺序,不同的硬件,不同的内存结构等。 你必须确保不会将一种架构中的文件与另一种架构中的文件混合在一起。 [[Meaty Skeleton]]提供一个如何构建代码的最小示例。 | ||
=== | === 适用于未来 === | ||
考虑到你最终将希望在整个内核接口范围内添加新功能。 因此,你必须确保在不破坏依赖接口的情况下很容易重构它们。 | 考虑到你最终将希望在整个内核接口范围内添加新功能。 因此,你必须确保在不破坏依赖接口的情况下很容易重构它们。 | ||
第27行: | 第27行: | ||
=== 多线程 === | === 多线程 === | ||
意识到,从现在开始的一段时间内,你将不会在单个线程中运行代码,而是'''将'''与其他线程和其他处理器并行运行,这意味着它最终将在关键操作期间被抢占,并且其他线程将能够破坏保存的状态。 | |||
=== 硬件抽象 === | === 硬件抽象 === | ||
意识到并不是每一件硬件都存在于每台计算机中,所以你可能想要通过 [[硬件抽象层]] 来抽象它。 例如,[[PIT]] 和 [[HPET]] | 意识到并不是每一件硬件都存在于每台计算机中,所以你可能想要通过 [[Hardware Abstraction Layer|硬件抽象层]] 来抽象它。 例如,[[PIT]] 和 [[HPET]] 是两个定时器,你可能希望将其抽象为一个定时接口。 | ||
=== 算法 === | === 算法 === | ||
对于每个任务,找到你认为重要的各个方面 (例如,简单性,速度,内存使用等) 得分最多的算法。 | |||
= = 分页 = = | == 分页 == | ||
[[Paging]] 很好,因为它可以让你根据需要映射内存,并且可以让进程看到完整的地址空间。 | [[Paging|分页]]很好,因为它可以让你根据需要映射内存,并且可以让进程看到完整的地址空间。 它还提供了高级的保护能力。 您可能希望在<tt>boot.s</tt>中尽早启用它。 | ||
=== Higher Half | === Higher Half === | ||
你应该继续使用 [[Higher Half Kernel|higher half | 你应该继续使用 [[Higher Half Kernel|higher half 内核]],因此用户空间程序可以在4 MiB (或更低,如果你愿意) 加载,而不会与内核二进制文件冲突。 要做出的一个重要决定是在哪里精确映射内核。 | ||
许多人更喜欢将内核映射到0x80000000,留下2 GiB用于内核数据,2 GiB用于进程。 | 许多人更喜欢将内核映射到0x80000000,留下2 GiB用于内核数据,2 GiB用于进程。 这可以允许单内核缓存大文件或文件系统结构。 | ||
其他人则更喜欢将其映射到0xC0000000,为内核数据留出1 GiB,为进程留出3 GiB。 他们的主要论点是,它与 [[PAE]] 更好地集成,因为整个内核空间正好适合一个页面目录。 | 其他人则更喜欢将其映射到0xC0000000,为内核数据留出1 GiB,为进程留出3 GiB。 他们的主要论点是,它与 [[PAE]] 更好地集成,因为整个内核空间正好适合一个页面目录。 | ||
其他一些人 (包括本页的原始作者) | 其他一些人 (包括本页的原始作者) 走极端,将内核映射为0xE0000000,留下了内核数据的狭窄512 MiB空间和进程的3.5 GiB。 他们的主要论点是用户空间应该能够使用尽可能多的内存。 | ||
=== 权限 === | === 权限 === | ||
[[Bare Bones]] 教你为内核使用 [[ELF]] 二进制文件。 你知道 <tt>.text</tt> 、 <tt>.data</tt> 、 <tt>.rodata</tt> 、 <tt>.bss</tt> 是什么意思吗? | [[Bare Bones]] 教你为内核使用 [[ELF]] 二进制文件。 你知道 <tt>.text</tt>、<tt>.data</tt>、 <tt>.rodata</tt>、<tt>.bss</tt> 是什么意思吗? 对,它们是可执行文件的节(Section)。 在 <tt>.text</tt> 中存储了处理器的指令,在 <tt>.data</tt> 中有数据,在 <tt>.rodata</tt> 中有只读数据,在 <tt>.bss</tt> 中,存在未初始化的数据。 可能会有更多的节,但现在让我们集中讨论这些。 | ||
这些节中的每一个都需要应用不同的权限,例如 <tt>.text</tt> 应该是只读的,<tt>.data</tt> 应该是读写的,<tt>.rodata</tt> 应该是只读的,而 <tt>.bss</tt> 应该是可读写的。 为了使这些变化在内核空间中生效,你必须在适当的寄存器中设置WP(写保护-Write Protect)位。 确保在 [[x86-64]] 和 [[PAE]] 模式下禁用非 <tt>.text</tt> 部分的执行。 | |||
为了便于在节上设置权限,你可以执行以下操作: | 为了便于在节上设置权限,你可以执行以下操作: | ||
第61行: | 第61行: | ||
* 告诉链接器插入指示特定部分的开始和结束地址的符号,以便你可以从映射代码中访问它们。 | * 告诉链接器插入指示特定部分的开始和结束地址的符号,以便你可以从映射代码中访问它们。 | ||
= = | == 更多x86特定的东西 == | ||
操作系统应尽可能自立自主。 引导加载程序(bootloader)可能使环境处于 “工作” 状态,但从长远来看并不方便。 | |||
=== 段 === | === 段 === | ||
在第一段(segment)更改之前创建一个 [[GDT]],因为 [[GRUB]] 已经设置的那个已不再有效 (这些条目-entry只是简单地仍缓存在处理器中,这就是为什么它 “起作用” 的原因)。 | |||
你至少需要这些条目: | 你至少需要这些条目: 空段条目(null segment entry)、内核代码段条目(kernel code segment entry)、内核数据段条目(kernel data segment entry)、用户代码段条目(user code segment entry)、用户数据段条目(user data segment entry)、 [[Task State Segment|任务状态段(Task State Segment)]]条目。 | ||
=== 中断 === | === 中断 === | ||
每个实际操作系统都需要处理 [[Exceptions|异常]] (例如 [[Exceptions# 页面故障|页面故障]]),并且当外围设备数据准备好时从中读取数据(而不是轮询)。 | |||
创建一个 [[IDT]]。写入中断处理程序。 启用中断控制器 (例如 [[PIC]] 或 [[APIC]])。 | 创建一个 [[IDT]]。写入中断处理程序。 启用中断控制器 (例如 [[PIC]] 或 [[APIC]])。 | ||
确保在中断处理程序开始时保存所有寄存器,并在中断处理程序结束时恢复它们。 | 确保在中断处理程序开始时保存所有寄存器,并在中断处理程序结束时恢复它们。 还请记住,某些异常会导致错误编码被推送到堆栈,而另一些则不会。 | ||
=== | === 定时器 === | ||
初始化定时器,以便能够跟踪时间。 考虑一下你想首先支持哪个定时器 (大多数初学者都使用 [[PIT]],尽管它很古老),以及你想如何设置它 (大多数时候以方便的tick间隔设置它,例如1 ms或10 ms)。 但是,请确保抽象接口,以便更容易添加对更多计时器的支持。 | |||
=== 获取键盘输入 === | === 获取键盘输入 === | ||
第89行: | 第89行: | ||
重要的是允许用户能够与操作系统进行交互。 可以使用IO端口读取键盘,但是你需要设置中断以获得适当的键盘支持。 | 重要的是允许用户能够与操作系统进行交互。 可以使用IO端口读取键盘,但是你需要设置中断以获得适当的键盘支持。 | ||
= = 内存管理 = = | == 内存管理 == | ||
很快,你将需要分配一些在编译时不知道大小的东西。 这就是 [[ | 很快,你将需要分配一些在编译时不知道大小的东西。 这就是 [[Memory Management| 内存管理器]] 的目标。 | ||
=== 获取内存映射 === | === 获取内存映射 === | ||
你首先需要 [[ | 你首先需要 [[Detecting_Memory_(x86)#通过GRUB内存映射| 获取内存映射]],这样你就知道哪些物理区域是空闲的。 然后,你在此基础上继续完善。 | ||
=== 物理内存管理器 === | === 物理内存管理器 === | ||
当然,你还需要一个空闲物理页面的列表,因此你知道接下来要分配哪些物理页面帧。 | |||
一种常见的方法是创建一个链接列表,即将下一个空闲页面的物理地址存储在上一个空闲页面的开头,而仅有空闲内存才记录这样一个头。 但是,你已启用分页,因此你不能任意写入内存的每个部分。 相反,你可以一次映射一个页面框架,并向它写入下一个空闲页面的地址。 或者,你可以在higher half中针对所有物理内存有一个单独的映射: 它在64位内核中尤其常见,因为它简化了设计,几乎没有缺点。 | |||
=== 虚拟内存分配器 === | === 虚拟内存分配器 === | ||
你还将需要一种分配虚拟页面以映射物理内存的方法,而不是硬编码的值。 | 你还将需要一种分配虚拟页面以映射物理内存的方法,而不是硬编码的值。 拥有一种方法来跟踪地址空间的哪些部分被使用,哪些不被使用。 | ||
有多种方法可以跟踪地址空间。像Linux和Windows这样的现代操作系统使用AVL树,但是你也可以使用任何你喜欢的数据结构。 | 有多种方法可以跟踪地址空间。像Linux和Windows这样的现代操作系统使用AVL树,但是你也可以使用任何你喜欢的数据结构。 | ||
第111行: | 第111行: | ||
=== 堆分配器 === | === 堆分配器 === | ||
你肯定也想要实现一个[[Heap|堆(heap)]],难道你想一次以4KB的粒度继续分配内存?首先实现一个非常简单 (但缓慢) 的链表堆。 然后,你可以进行更复杂的设计,例如不同bucket的单独块大小等。 你还应该记住,最终你的堆将内存不足,所以你需要实现堆扩展。 | |||
或者你可以选择另一个设计,涉及[https://en.wikipedia.org/wiki/Slab_allocation Slab Allocator]. | |||
== 调度器 == | == 调度器 == | ||
如果无法调度任务,则不算是一个真正的操作系统。 每个现代桌面操作系统都应允许浏览web,同时渲染3D场景,同时在电子表格中对数据进行排序,同时将大文件写入磁盘。 这由 [[Scheduler|调度器]] 负责的。 | |||
=== | === 多处理(Multiprocessing) === | ||
做好 [[Multiprocessing| | 做好 [[Multiprocessing|多处理]] 的准备。 尚未准备好进行多处理的调度器可能以后会被完全重写。 | ||
=== 优先级别 === | === 优先级别 === | ||
第129行: | 第129行: | ||
=== 线程列表 === | === 线程列表 === | ||
通常建议每个状态和优先级都有不同的线程列表。 | 通常建议每个状态和优先级都有不同的线程列表。 这样,调度代码不必遍历每个线程找到高优先级线程,然后万一找不到,再次迭代线程列表以找到优先级较低的线程,然后可能再次失败,等等。 分多个列表意味着调度程序代码的运行速度更快,因为可以立即检测到缺少特定优先级别的线程,同时也不会遍历非活动线程。 | ||
<!- | <!-- | ||
=== | === Algorithm === | ||
For the scheduling algorithm you would probably like having: | For the scheduling algorithm you would probably like having: | ||
第146行: | 第146行: | ||
== 结论 == | == 结论 == | ||
操作系统开发不能说容易,也不能说难。 而可以说是无比艰巨。 要知道与成熟操作系统所涉及的复杂性相比,上述(不完整)列表还算不了什么。 | |||
[[Category:Tutorials]] | [[Category:Tutorials]] |
2022年3月17日 (四) 02:14的最新版本
难度等级 |
---|
![]() 中等 |
内核设计 |
---|
模型 |
其它概念 |
你已经为 x86 完成了 Bare Bones。 现在呢,你可能开始会有一点困惑。 欢迎来到操作系统开发世界!
以下指南假设你正在按照从上到下的顺序推进着下面讨论的工作。 在开始实施之前,建议你先阅读完整内容以获得更广阔的视野。
准备面对实战
在继续之前:
- 你应该获取英特尔手册的副本。 下面讨论的大多数特定于处理器的内容在英特尔手册中都有最好的介绍。
- 你应该确保自己有足够的耐心和时间。操作系统开发是最耗时的项目之一。
设计注意事项
将操作系统为一个整体设计,而又一部分一部分的完成,是一项棘手但重要的任务。
代码结构化
你应该决定你的代码应该如何结构化。 考虑到你最终会将操作系统移植到不同的体系结构,使用不同的汇编指令,不同的初始化顺序,不同的硬件,不同的内存结构等。 你必须确保不会将一种架构中的文件与另一种架构中的文件混合在一起。 Meaty Skeleton提供一个如何构建代码的最小示例。
适用于未来
考虑到你最终将希望在整个内核接口范围内添加新功能。 因此,你必须确保在不破坏依赖接口的情况下很容易重构它们。
多线程
意识到,从现在开始的一段时间内,你将不会在单个线程中运行代码,而是将与其他线程和其他处理器并行运行,这意味着它最终将在关键操作期间被抢占,并且其他线程将能够破坏保存的状态。
硬件抽象
意识到并不是每一件硬件都存在于每台计算机中,所以你可能想要通过 硬件抽象层 来抽象它。 例如,PIT 和 HPET 是两个定时器,你可能希望将其抽象为一个定时接口。
算法
对于每个任务,找到你认为重要的各个方面 (例如,简单性,速度,内存使用等) 得分最多的算法。
分页
分页很好,因为它可以让你根据需要映射内存,并且可以让进程看到完整的地址空间。 它还提供了高级的保护能力。 您可能希望在boot.s中尽早启用它。
Higher Half
你应该继续使用 higher half 内核,因此用户空间程序可以在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 是什么意思吗? 对,它们是可执行文件的节(Section)。 在 .text 中存储了处理器的指令,在 .data 中有数据,在 .rodata 中有只读数据,在 .bss 中,存在未初始化的数据。 可能会有更多的节,但现在让我们集中讨论这些。
这些节中的每一个都需要应用不同的权限,例如 .text 应该是只读的,.data 应该是读写的,.rodata 应该是只读的,而 .bss 应该是可读写的。 为了使这些变化在内核空间中生效,你必须在适当的寄存器中设置WP(写保护-Write Protect)位。 确保在 x86-64 和 PAE 模式下禁用非 .text 部分的执行。
为了便于在节上设置权限,你可以执行以下操作:
- 告诉链接器在4个KiB边界处对齐它们,因此各节占据整个页面。
- 告诉链接器插入指示特定部分的开始和结束地址的符号,以便你可以从映射代码中访问它们。
更多x86特定的东西
操作系统应尽可能自立自主。 引导加载程序(bootloader)可能使环境处于 “工作” 状态,但从长远来看并不方便。
段
在第一段(segment)更改之前创建一个 GDT,因为 GRUB 已经设置的那个已不再有效 (这些条目-entry只是简单地仍缓存在处理器中,这就是为什么它 “起作用” 的原因)。
你至少需要这些条目: 空段条目(null segment entry)、内核代码段条目(kernel code segment entry)、内核数据段条目(kernel data segment entry)、用户代码段条目(user code segment entry)、用户数据段条目(user data segment entry)、 任务状态段(Task State Segment)条目。
中断
每个实际操作系统都需要处理 异常 (例如 页面故障),并且当外围设备数据准备好时从中读取数据(而不是轮询)。
创建一个 IDT。写入中断处理程序。 启用中断控制器 (例如 PIC 或 APIC)。
确保在中断处理程序开始时保存所有寄存器,并在中断处理程序结束时恢复它们。 还请记住,某些异常会导致错误编码被推送到堆栈,而另一些则不会。
定时器
初始化定时器,以便能够跟踪时间。 考虑一下你想首先支持哪个定时器 (大多数初学者都使用 PIT,尽管它很古老),以及你想如何设置它 (大多数时候以方便的tick间隔设置它,例如1 ms或10 ms)。 但是,请确保抽象接口,以便更容易添加对更多计时器的支持。
获取键盘输入
- 正文: PS/2 Keyboard
重要的是允许用户能够与操作系统进行交互。 可以使用IO端口读取键盘,但是你需要设置中断以获得适当的键盘支持。
内存管理
很快,你将需要分配一些在编译时不知道大小的东西。 这就是 内存管理器 的目标。
获取内存映射
你首先需要 获取内存映射,这样你就知道哪些物理区域是空闲的。 然后,你在此基础上继续完善。
物理内存管理器
当然,你还需要一个空闲物理页面的列表,因此你知道接下来要分配哪些物理页面帧。
一种常见的方法是创建一个链接列表,即将下一个空闲页面的物理地址存储在上一个空闲页面的开头,而仅有空闲内存才记录这样一个头。 但是,你已启用分页,因此你不能任意写入内存的每个部分。 相反,你可以一次映射一个页面框架,并向它写入下一个空闲页面的地址。 或者,你可以在higher half中针对所有物理内存有一个单独的映射: 它在64位内核中尤其常见,因为它简化了设计,几乎没有缺点。
虚拟内存分配器
你还将需要一种分配虚拟页面以映射物理内存的方法,而不是硬编码的值。 拥有一种方法来跟踪地址空间的哪些部分被使用,哪些不被使用。
有多种方法可以跟踪地址空间。像Linux和Windows这样的现代操作系统使用AVL树,但是你也可以使用任何你喜欢的数据结构。
堆分配器
你肯定也想要实现一个堆(heap),难道你想一次以4KB的粒度继续分配内存?首先实现一个非常简单 (但缓慢) 的链表堆。 然后,你可以进行更复杂的设计,例如不同bucket的单独块大小等。 你还应该记住,最终你的堆将内存不足,所以你需要实现堆扩展。
或者你可以选择另一个设计,涉及Slab Allocator.
调度器
如果无法调度任务,则不算是一个真正的操作系统。 每个现代桌面操作系统都应允许浏览web,同时渲染3D场景,同时在电子表格中对数据进行排序,同时将大文件写入磁盘。 这由 调度器 负责的。
多处理(Multiprocessing)
做好 多处理 的准备。 尚未准备好进行多处理的调度器可能以后会被完全重写。
优先级别
以某种方式设计调度程序,因此线程可以具有不同的优先级。
线程列表
通常建议每个状态和优先级都有不同的线程列表。 这样,调度代码不必遍历每个线程找到高优先级线程,然后万一找不到,再次迭代线程列表以找到优先级较低的线程,然后可能再次失败,等等。 分多个列表意味着调度程序代码的运行速度更快,因为可以立即检测到缺少特定优先级别的线程,同时也不会遍历非活动线程。
结论
操作系统开发不能说容易,也不能说难。 而可以说是无比艰巨。 要知道与成熟操作系统所涉及的复杂性相比,上述(不完整)列表还算不了什么。