一次操作系统之旅
前言
题目取的有些诗意了,实际上这篇文章是我读完于渊的《一个操作系统的实现》后对该书的总结和评价。本人非科班出身(通信专业),对操作系统底层的工作原理一直有种执念,偶然的机会看到了这本书,花了两个月时间从头到尾读了一遍,我把书中的源码读完理解之后重写了一遍。如果你也想实现一个小的操作系统却又不知从何下手,这本书确实是一个不错的选择。
这本书完全是从零开始搭建了一个可以通过软盘/硬盘引导的微内核操作系统(类似于minix),但由于篇幅限制(只有不到500页)作者在实现很多功能时选择了最简陋的方式,如进程调度算法/文件系统/进程间通信/内存管理等等,还有很多可以优化的空间,代码中存在一些BUG,我在重写时已经对其中的一些BUG进行了修正,并且添加了一些中文注释(文后附有github地址,可以直接运行)。读这本书之前最好先了解一些8086汇编相关知识,书中和系统引导加载有关的代码均是用汇编编写。推荐在linux环境下完成操作系统代码的编译和调试工作。附录是我对书中一些关键概念的理解,看完再读这本书可能会更好理解。
上篇
第一二章的节奏比较慢,主要介绍如何安装和使用调试环境bochs(虚拟机,类似vmware,它仿真了x86的硬件环境及其外围设备),并用汇编语言编写了一个软盘的扇区引导程序,通过bochs完成对该程序的加载。
第三章信息量比较大,介绍了如何从实模式跳转到保护模式,建立分段机制,建立gdt和ldt,建立分页机制,建立实模式和保护模式下中断向量表,以及8259a中断控制器的初始化。
第四章介绍软盘引导扇区,fat12文件系统,以及如何使用512字节的引导扇区找到加载器(loader)并装入内存,将权力移交给加载器(loader)。
第五章主要介绍loader部分的工作,包括将kernel加载到内存,内核文件(elf格式)的读取和加载,实模式到保护模式的切换,分页机制的建立,建立中断向量表,建立异常中断处理(interrupt/fault/trap),并将控制权交给内核,此时主要编程语言由汇编转为c语言。
第六章首先介绍进程的切换,特权级ring0和ring1相互切换,进程被中断时,堆栈的切换情况是:任务堆栈->进程表->内核堆栈,中断重入时不再进行堆栈切换。最后介绍了一个简单的进程调度算法,通过设置任务tick数确定运行优先级。
第七章介绍键盘输入的控制(键盘驱动),创建一个终端任务(tty)用于处理键盘输入,对键盘上的特殊按键处理并输出,介绍系统调用的添加过程,系统调用分为用户部分和内核部分,用户部分传入系统调用号,内核部分完成相应的操作。
下篇
第八章模仿minix的微内核架构和其对应的消息机制(同步IPC)建立了进程通信机制, 并对系统调用get_ticks进行改写。
第九章内容比较丰富, 建立了一个简单的文件系统,添加硬盘驱动任务(设备打开关闭/读/写/io控制)和文件系统驱动任务(文件打开/关闭/删除等),创建一个硬盘映像,利用fdisk对其进行分区, 实现了硬盘驱动相关处理。完成文件系统的静态管理(在硬盘上的数据组织形式),完成文件系统动态管理(文件创建/打开,读,写,关闭),在硬盘中开辟一块区域用于存放运行log,便于后续分析,完成文件删除功能,将tty纳入文件系统。
第十章介绍mm驱动任务,完成init进程与fork系统调用,完成应用程序执行功能,实现一个简单的shell,可从命令行读取输入字符解析并调用相应的应用程序。
第十一章对系统进行完善并实现硬盘引导 (没太细看,有大篇幅的汇编程序,看着头疼)。
总结
我个人感觉在这本书上投入的时间是值得的,操作系统本身内容很丰富,这本书只能起到抛砖引玉的作用,但它让我觉得操作系统不再遥不可及,也让我对计算机的基本知识有了更深刻的理解。
干货
有关CPL/RPL/DPL介绍:https://www.jianshu.com/p/972f1bd497be
我的github链接:https://github.com/bazingaaaa/BazingaOS
把repo下载到本地之后,linux环境下在项目目录下执行make和make buildimg,然后执行bochs -f bochsrc.txt,系统就加载起来了,F1/F2/F3切换终端界面,在F2和F3的界面上运行了一个简陋的shell,可以通过命令行执行应用程序echo,例如输入echo hello world!。
书中源码github链接:https://github.com/wlmnzf/oranges
后面几章源码运行有一些问题,具体原因我还没有深究。
附录
1. 微内核/宏内核
两种内核设计思想的不同之处在于内核职责的划分,微内核中内核的功能最简化,只负责主要的进程调度/通信等过程,其他工作交给专门的进程来完成,而宏内核负责完成用户所有的系统调用请求。微内核可以用一个专门的系统进程用来处理用户进程请求。
2.实模式/保护模式
系统启动时处于实模式,在进入真正的操作系统之前需要切换到保护模式,区别主要有以下几点
i) 寻址方式不同,虽然都是通过段寄存器和段内偏移进行寻址,但是实模式寻址空间只有1Mb,计算公式:段寄存器 x 16 + 段内偏移,而保护模式(32位)有4Gb的寻址空间,计算公式:段基地址 + 段内偏移。保护模式下还可以启用内存分页机制。
ii) 在保护模式下,cpu会根据dpl/rpl/dpl对数据的访问或执行的合法性进行检验,非法访问时cpu会抛出异常,也就是说用户可以通过设置段的属性达到“保护”的目的。
iii) 中断向量表位置不同,实模式下中断向量表存放在内存起始位置,而保护模式下需要建立中断向量表,并加载中断向量表寄存器,中断到来时,cpu通过idtr找到中断向量表的位置。
3.逻辑地址/线性地址/物理地址
我们在c程序中的获取到的变量地址或者是在汇编程序中获取到的地址偏移均是逻辑地址,逻辑地址 + 段选择子 + 段描述符表 可以转换为线性地址,线性地址 + 页目录表 + 页表 可以转换为物理地址。 每个进程拥有各自的局部描述符表(ldt),对应着不同的虚拟地址空间。
4. 内核态/用户态
内核态和用户态指的是程序执行的特权级,intel的cpu提供了ring0~ring3四种特权级,这本书的描述的操作系统用到了ring0(中断执行时的特权级),ring1(系统任务特权级),ring3(用户特权级),特权级决定了程序可以访问或者执行的数据。在不同的特权级下程序使用的堆栈也不同。用户在进行系统调用时就完成了用户态-》内核态-》用户态的切换。
5.伪指令org
书中的汇编代码经常出现这个指令,比较难理解,这地方解释一下。例如org 07c00h,这条指令的作用是告诉汇编器这条指令之后的程序会加载到偏移07c00h处。注意这条指令并不能决定计算机加载程序的位置,实际上我们是事先知道计算机会把程序加载到07c00h处,通过这条指令告诉汇编器对内存地址的引用都要加上07c00h。