自制操作系统(0):开始与结束的序言
这篇序言本应该是最开始就写好的,但实际上它是最后一篇写好的,所以叫”开始与结束的序言”。
阅读下面这一系列的文章,你会看到我在实现这个操作系统过程中的一些想法。理想化地想,我想展示的是好的东西,包括看到一个庞然大物是如何从无到有的,而且每一步都有所见即所得的反馈;但现实总不是那么理想,这里面当然也有不好的东西,包括一时的纠结、挣扎,以及无奈妥协…
需要注意的是,这一系列文章可能难称得上是什么教程。 硬要说,这些只是我的一些思绪的记录和学习的笔记——在此之前,我没有哪怕一行的内核编程经验。记录这一切的目的是:我想跟大家分享开发操作系统的乐趣。当然乐趣并不意味着简单快捷,我觉得乐趣在于过程——学习的过程,挑战的过程。现在的AI很强大,我相信花上一天时间就能复制出我这几个月所做的一切(甚至比我做的要更好!)。但如果你能在阅读的过程中,哪怕只产生一条对你有用的启发,并能在日后的开发过程中想起它,那就再好不过了——你赋予了我记录这一切更深的意义。
如果你想知道我做了什么、没做什么,以及计划做什么,请阅读最后一篇“后记”文章。
有任何问题请在issue反馈,希望大家不要问太难的问题,不然我就很难回答了。
注:这个项目原本的名字叫lolios,后面改成moeos了,所以你会在后面看到许多lolios的标识。
Roadmap

一图胜千言。
上面是我用Nano Banana Pro制作的本次教程的Roadmap,我希望大家能直观地看到我们接下来将要实现的一些东西。
如果你不喜欢看图片,也可以看看我下面的讲解:
一开始,我们将会从OSDev Wiki的Bare Bone开始,制作一个内核级的Hello World,这就像新手村的训练场,让你能尝到点”战斗”的甜头,让你感觉内核编程并不是那么遥不可及的空中楼阁。后面为了项目的长稳发展,我们会把Bare Bone重构成Meaty Skeleton,因为它有更规范的项目结构;还引入了Makefile来管理构建,这样你就不需要每次都把所有的源码重新构建一遍;对于更长远的未来,它还为你做好了多平台支持的准备,这样当你后面不只想在i386指令集的CPU上运行你的操作系统时,就可以新建一个子文件夹,编写相应的适配代码…[^1] 有了Meaty Skeleton,我们就可以尝试切断与Multiboot的”脐带”,开始设置自己的GDT和IDT,走出新手村独立发展了。Interrupt Handler会是你的好帮手。
走出新手村后,我们会来到内存管理的领域——内存很大,但管理好它们不是易事。在管理内存之前,我们会先讨论如何遵循惯例,把内核迁移到高半区;再来是利用Buddy System这种巧妙的算法来管理物理内存;为了给后面寄宿在这个系统的进程一种独占内存空间的错觉,同时也是为了让它们互不干扰,我们会用Recursive Paging来以页的粒度管理虚拟化后的内存;最后,为了我们能以更小的粒度去申请内存,我们用可合并的链表+First Fit算法来实现内核堆管理器。至此,我们就能在内核层面驾驭内存了。
内存准备好后,我们会讨论另一种硬件资源的虚拟化:CPU。我们通过让进程轮流使用CPU来给它们独占CPU的错觉,并讨论一些基本的调度策略以确保尽可能的公平;同时,我们会讨论如何保存和恢复每个进程的上下文,这涉及到切换内核栈和寄存器。有了创建内核进程的基础后,我们会进一步讨论如何创建用户态进程——怎么用一段”蹦床”使我们从Ring 0(内核态)”回到”Ring 3(用户态),以及如何利用特殊的中断(系统调用)来使我们暂时陷入内核态,调用内核的代码。最后,我们会讨论怎么把用户态程序从内核代码中独立出来,封装成一个简单的指令流文件。
拥有了基本的进程基础设施后,我们会进入文件操作的领域。我们会讨论如何制作一层虚拟文件系统的抽象,来隐藏后面要适配的所有文件系统的实现细节,如何抽取出文件系统的共性特征并封装成接口。随后在VFS的基础上,我们会实现一个非常简单实用的文件系统:TARFS,并讨论如何用GRUB Module将其封装进镜像,在引导时加载进内存,以便让VFS挂载它。
有了一个基本可用的文件系统后,我们会回过头来补全进程运行的基础设施:实现用户态的malloc和free,利用它们来动态分配堆内存;再来是把用户态进程的封装进行”升级”,打包为ELF格式,并让我们的操作系统可以正确加载它;最后是结合VFS实现PipeFS,来实现管道这种进程间通信的形态。
现在,我们已经有了一个完成度不错的Hobby OS了,但我们不会停下脚步。我们会尝试实现一个TCP/IP栈:从PCI总线、RTL8139网卡驱动,到ARP、ICMP,再到鼎鼎大名的IP协议,最后是TCP协议的简单实现和UDP协议实现(剧透警告:由于时间有限,我们会做得尽量简单些,不会涉及拥塞控制和重传,发送甚至不是Send-and-Wait的,是”莽夫”式的直接发送)。利用这些基础设施,我们会实现用Telnet来访问我们的操作系统。
回到本地,我们会实现另一种进程间通信的形式:信号。这是一种很实用的机制——我们可以用中断信号来结束卡住的程序(不用再重启整个操作系统了),还可以用KILL信号来杀死别的进程或自己的进程。同时,我们不会满足于一个只读的文件系统,我们会讨论如何适配Ext2文件系统——这显然不是一件易事。之后我们会实现ProcFS,并基于它开发ps、netstat和kill。
在故事的最后,我们终于可以尝试移植一些软件了。由于前面对POSIX规范的忽视,在这里我们会吃到不少苦头,但最终,我们会把Kilo——这个小巧的文本编辑器——移植到我们的系统;最后,我们会把DOOM移植到我们的系统,并自豪地说出——It runs DOOM! 至此,我们的操作系统之旅就此暂告一段落。
就这样,翻开新的一页,我们开始吧。
[^1]: 实际上,”想长远一点”是很重要的事情。从现在我所处的位置回顾整个项目,我就吃了不少因为一时的急躁和短视埋下的坑带来的苦头(你会在阅读后面的文章时体会到这一点)!当然,这不是让你陷入完美主义的陷阱,你可以在原地进行迭代,但不要指望让未来的自己来完成这些——也就是人们常说的:不要相信程序员写下的每一个 // TODO。

参与讨论
(Participate in the discussion)
参与讨论
没有发现评论
暂无评论