江苏太平洋建设集团官方网站,白山seo,手机网站logo,深圳大公司一、系统简介
本操作系统是一个使用rust语言实现#xff0c;基于32位的x86CPU的分时操作系统。
项目地址#xff08;求star#xff09;#xff1a;GitHub - CaoGaorong/os-in-rust: 使用rust实现一个操作系统内核
详细文档#xff1a;自制操作系统 语雀
1. 项目特性 …一、系统简介
本操作系统是一个使用rust语言实现基于32位的x86CPU的分时操作系统。
项目地址求starGitHub - CaoGaorong/os-in-rust: 使用rust实现一个操作系统内核
详细文档自制操作系统 · 语雀
1. 项目特性
在本项目的实现上我认为该项目有一下特性 特性 说明 rust语言实现 市面上很多操作系统项目使用的C语言实现我这里使用的rust实现可以保证这个项目的代码由我手写实现。也体现了该项目的独特和新颖。 基于x86 CPU x86是市面上最常见的CPU基于这种通用大众化的CPU也能说明本项目用到的知识的通用。 拥有OS的基本功能 本项目在实现操作系统的功能上算得上是“麻雀虽小五脏俱全”。基本操作系统的核心功能都实现了 BIOS启动、Mbr引导、Loader加载内核 使用BIOS启动Mbr引导操作系统手写Loader 进入保护模式、加载段描述符、开启分页中断管理 实现各种中断CPU异常中断、键盘中断、硬盘中断等等常规中断内存管理 使用页表管理内存实现进程的虚拟内存进程独享4GB内存空间 *线程和进程管理 实现内核线程和用户进程其中包括PCB的属性的设计线程和进程的启动、执行任务的管理、调度。系统调用基于中断 在实现用户进程后用户进程处于3特权级下想调用内核高级功能就得通过系统调用。本系统实现了系统调用让用户进程跟内核交互。*文件系统 本系统实现了一个最小化并且全面的文件系统实现了文件、目录的层级结构支持在文件系统对文件以及目录的 增、删、改、查。对于文件和目录的常规操作都有支持。并且支持从硬盘加载程序并且执行。Shell交互 本系统提供了Shell作为人跟系统交互的桥梁在Shell读取命令然后执行。Shell实现了管道支持多个进程间通信。 这里只是对系统的基本介绍详细的介绍可以看下面更加详细的介绍。 mbr启动、Loader引导 本系统使用比较古老的BIOS启动手写Mbr启动并且手动实现Loader对系统进行引导。mbr和loader均为rust实现 没使用任何第三方crate 在市面上很多rust的系统引用了很多第三方的crate比如blog_os和rCore os。本系统不使用任何第三方crate所有功能都使用rust的core手动实现。 2. 项目结构介绍
如下图是该项目的文件结构 下面我挑几个重点的模块介绍 模块名称 模块类型 模块介绍 build makefile生成 make之后生成的文件 cat 用户程序独立进程 写入到文件系统然后shell可以加载成为一个进程运行 自制的cat程序把文件系统中的文本文件内容输出到控制台 echo 自制echo程序把echo命令跟着的字符串输出到控制台 grep 自制grep程序根据输入内容进行过滤然后输出到控制台 common 操作系统内核 源码 common包loader、loader2、kernel都会用到的常用工具 mbr mbr启动16位该模块就两个功能 实现mbr规范引导BIOS加载loader读取硬盘 loader loader启动16位 加载选择子、打开GDT进入保护模式加载loader2读取硬盘 loader2 loader2启动32位 由于loader生成的指令是16位的因此在进入保护模式后需要再进入到loader2执行加载kernel读取硬盘 kernel 进入内核的最终实现包括中断、内存管理、线程进程管理、文件系统等等操作系统核心都在这个模块 rrt Rust RunTime library 我自制简单的用户程序运行时库模仿了crt。为用户程序封装了_start入口和exit的调用。 target cargo生成 rust使用cargo生成的二进制文件。忽略 tests 单元测试 在这个项目实现中我写的简单的单元测试
3. 项目环境介绍
这里我介绍一下项目的开发环境和运行环境。 开发环境最终是生成一个32位的基于x86的操作系统二进制文件 名称 值 版本信息 操作系统 MacOS Ventura 13.0.1 CPU Apple M1 编译器 rustc rustc 1.82.0-nightly (5aea14073 2024-08-20)
binary: rustc
commit-hash: 5aea14073eee9e403c3bb857490cd6aa4a395531
commit-date: 2024-08-20
host: aarch64-apple-darwin
release: 1.82.0-nightly
LLVM version: 19.1.0 包管理工具 cargo cargo 1.82.0-nightly (ba8b39413 2024-08-16)
release: 1.82.0-nightly
commit-hash: ba8b39413c74d08494f94a7542fe79aa636e1661
commit-date: 2024-08-16
host: aarch64-apple-darwin
libgit2: 1.8.1 (sys:0.19.0 vendored)
libcurl: 7.84.0 (sys:0.4.74curl-8.9.0 system ssl:(SecureTransport) LibreSSL/3.3.6)
ssl: OpenSSL 1.1.1w 11 Sep 2023
os: Mac OS 13.0.1 [64-bit] 自动化工具 make GNU Make 3.81 elf文件二进制提取工具 x86_64-linux-gnu-objcopy GNU objcopy (GNU Binutils) 2.41 elf文件dump工具 x86_64-linux-gnu-objdump GNU objdump (GNU Binutils) 2.41 运行环境把这个x86的32位的操作系统镜像运行在虚拟机中 名称 值 环境版本信息 虚拟机 qemu-system-x86_64 QEMU emulator version 8.2.1 虚拟机2 bochs Bochs x86 Emulator 2.8 这里不得不说rust的跨平台开发能力太好了我在Mac M1上开发生成一个 x86 的 ELF文件。然后在启动一个qemu虚拟机运行。 4. 项目运行效果
这里给大家展示一下我的操作系统的基本功能。
下面我从这几个方面来展示
内置命令文件系统操作从文件系统加载进程Shell多进程通信管道 4.1. 内置命令
先看内置命令目前我的系统内置了如下命令排除文件系统操作和加载可执行文件 命令名称 命令用途 命令展示 pwd 展示当前工作目录 ps 查询当前的所有任务 clear 清屏 ctrl l快捷键 清屏 ctrl u快捷键 删除当前行的输入
这是一些基础并且跟功能无关的命令。下面请看我们使用操作系统中最常用的文件系统相关的命令。
4.2. 文件系统命令
我们使用一个操作系统最常用的一定是文件系统相关的命令比如「文件的增、删、改、查」、「目录的增、删、改、查」。我在本系统中基本都实现了如下表所示 命令名称 命令用途 命令展示 ls 展示当前目录下的所有文件。 这里的cat、grep是可执行文件 ls -l 展示当前目录下的文件细节 file_type中-表示普通文件d表示文件夹 cd 切换当前工作目录 mkdir 在当前工作目录下创建一个目录 rmdir 删除某个目录名称 touch 创建一个普通文件 创建了一个名为file的普通文件 看到这里可能会问怎么没有读写文件的命令我们一般使用cat命令读取文件并且我们会使用echo hello hello.txt来写入文件。 这个cat和echo命令是我写的用户程序这种程序是属于从硬盘加载后作为进程执行的。因此这个放到下一节。
4.3. 从文件系统加载进程
上面讲的都是内置的命令都比较简单而且固定。下面就是要从文件系统加载可执行程序执行了。
在上面讲项目结构中我提到我实现了3个用户程序作为小工具 这三个小工具分别是
cat把一个文件的内容输出到控制台echo把命令后面跟着的字符串输出到屏幕grep使用正泽匹配过滤输入然后把过滤后的结果输出 本小结我就展示cat和echo实现读、写一个文件。 命令名称 展示效果 echo 我这里先创建「hello.txt」文件然后把hello字符串写入到 hello.txt文件中 cat 我这里使用cat命令把hello.txt的文件内容输出 和输出到文件 这里使用echo 和 追加 数据到 hello.txt文件中
这里我简单展示了echo和cat的执行效果。
这里得注意echo和cat是两个在文件系统的文件这里体现出来的是 把 可执行文件 从文件系统加载到内存然后成为一个独立的用户进程执行的过程。
4.4. Shell多进程通信管道
另外我们在Shell常用的一个功能就是“管道”这个也是进程间通信的一种方式之一。
本系统设计好管道可以用于多个进程间通信。比如下面的命令 cat main.rs | grep main
main.rs是一个文本文件我输出到管道然后经过grep过滤后输出到屏幕 在本系统中运行效果是这样的 还可以多级管道如下图所示 这里管道的实现主要有一下核心点
阻塞队列进程间通信协调重定向文件描述符一切都是文件 关于更多的细节实现可以看下面的核心功能介绍。
二、*项目核心功能介绍
下面我来带大家过一下整个项目的整体功能不会太详细具体详细的内容可以见我写的文档。 本篇文章就作为一个目录带着过一下本系统的核心设计当然不够全面但是可以大概知道本系统实现了哪些功能。
如果想看最全面的讲解可以看我写的 合集文档
此处为语雀内容卡片点击链接查看自制操作系统 · 语雀 1. 物理结构
1.1. 硬盘结构
首先我们有两块硬盘
hd60M.imghd80M.img
hd60M.img是一个60MB大小的硬盘。它的作用是存放操作系统的这个硬盘没有分区是一个裸硬盘。
hd80M.img是一个80MB大小的硬盘。它的作用是给我们系统存放文件系统的这个硬盘经过了分区。 先看hd60M.img硬盘的结构如下图2-1所示 hd60M.img存放了操作系统的核心指令二进制文件mbr、loader、loader2、kernel。他们占用的扇区大小在图中也标记出来了。 下面我们再看看hd80M.img的结构我们的hd80M.img是一个有分区结构的硬盘如下图2-2所示 我们的文件系统只用其中的一个分区。 1.2. 模块的结构
再说到本系统的模块结构按照他们在硬盘的结构和位置我把本系统分为了如下几个核心模块
mbrloaderloader2kernel硬盘中的用户进程比如我自己写的cat、grep等小工具
这几个模块在硬盘的结构上是这样的一种结构如下图2-3所示 可以看到我们按照物理结构来划分把他们所在硬盘的位置以及如何加载到内存做了描述。 下面我们就从这个系统的设计和逻辑结构上来讲解这个系统。
2. 系统整体架构
先看这个系统的整体架构从全局的视角看一下这个项目的全景如下图2-1所示 要想一幅图画清楚全部的细节和交互是不可能的我这里只能挑选出最常用而且容易表现出来的部分尽量画在一张图里。 本系统把kernel模块的代码从硬盘加载到内存中放在低端1MB中实模式可以访问的1MB如下图2-5所示 为什么放在低端1MB呢因为低端1MB是开启分页后仍然可以访问的空间内核页表映射到了。这里的空间我们可以直接指定内存地址直接访问。
1MB以上的大部分空间我们都作为操作系统的堆空间实现内存管理后动态申请和释放是不能任意指定地址直接访问的。 下面我分具体的模块来把最核心的功能介绍一下。
3. 系统引导Mbr、Loader、Loader2
我们的核心在操作系统内核的设计因此对于加载内核前面的引导部分我们尽快过一下。
还是看这个硬盘的结构图 我们可以看到mbr、loader、loader2所在硬盘的位置最终目的就是把kernel从硬盘中加载到内存。
下面我简单说一说这几个引导程序的作用 模块 作用 mbr 在我看来有两个作用 标识当前硬盘或者分区有操作系统。固定在首个扇区并且以0x55AA结尾加载loader。把loader从硬盘中加载出来 loader 在我的系统设计中有两个作用 进入保护模式开启32位。加载loader2我特意设计的因为当前的loader是16位的在加载中存在一些问题 loader2 这里已经是给加载内核做准备了核心作用就是「打开分页」以及「加载内核」具体作用如下 构建内核的页目录表在本系统的设计中内核的页目录表固定放在0x100000也就是1MB以上加载页目录表加载到GDTR寄存器打开分页加载kernel
引导部分我们不说太详细在我看来引导程序最核心的就是要
进入保护模式、构建页目录表打开分页加载kernel
其中分页开启后最终是构建了一个内核页目录表如下图3-2所示 下面就开始介绍我们系统的核心设计。
4. 中断管理
我们进入内核后先要做的就是实现中断处理程序接收中断。当CPU抛出一些异常比如General Protection Exception、Page Fault的时候我们就可以通过中断处理程序的触发立马知道有中断发生。
关于中断的设计我不再细说可以看我本仓库写的文章
此处为语雀内容卡片点击链接查看一、中断介绍 · 语雀
此处为语雀内容卡片点击链接查看二、中断描述符 · 语雀
此处为语雀内容卡片点击链接查看三、中断发生时的栈操作 · 语雀 这里我们简单讲一下中断发生到执行中断处理程序的原理如下图4-1所示 下面我简单介绍一下我的这个系统实现了的中断处理程序 中断处理程序 中断类型 执行的操作 general protection CPU异常 关于异常可以看这里Exceptions - OSDev Wiki 通用保护异常。通常是低特权级下访问高级资源 double fault 一般是在中断处理程序里发生异常。所以异常“double”了 page fault 一般是访问一个页表没有映射的内存所以找不到页 invalid code 指令非法。遇到过一般是编译有点问题。 时钟中断 硬件中断 定时触发中断的频率可以设置 键盘中断 键盘按下生成扫描码 主ATA通道中断 硬盘信号中断 次ATA通道中断 硬盘信号中断 系统调用中断0x80 软件中断 代码指令中使用int指令发起中断
在实现中断处理程序中中断发生到跳转执行中断处理程序这个过程是根据中断描述符表来的我们不用过多关心。 我们的中断处理程序关键的就是在于要清楚如何保存上下文其中中断发生有的上下文是CPU给保存了的但是有的需要自行保存。这个中断上下文的保存如果使用rust实现就简单了很多因为rust专门有这样的声明
extern x86-interrupt fn handler(frame: InterruptStackFrame, error_code: u32) {}
这里把一个函数使用了extern x86-interrupt修饰了那么这个函数就会自动保存中断的上下文。 但是对于系统调用必须要清楚栈中的晚上的上下文数据因此就不能使用这个extern x86-interrupt来保存了而是只能手动保存上下文。这个等待后面系统调用再说。
但是这里我们要清除当中断发生时CPU自动保存了哪些上下文可以看这篇文章
此处为语雀内容卡片点击链接查看三、中断发生时的栈操作 · 语雀 另外中断的频率也是可以调整的我们可以通过操作「可编程中断控制器」来调整中断的频率。这里就不细说了。
5. 内存管理
在写应用层程序的时候经常要用到堆内存。比如C语言中的malloc来动态申请堆内存空间。我们本系统实现的「内存管理」模块就是要提供一个口子可以「动态申请」、「动态释放」内存空间。把内存操作的口子收拢统一管理这就是内存管理。
此处为语雀内容卡片点击链接查看一、内存结构回顾 · 语雀
此处为语雀内容卡片点击链接查看二、数据结构-位图 · 语雀
此处为语雀内容卡片点击链接查看三、位图的设计 · 语雀
此处为语雀内容卡片点击链接查看四、*管理内存 · 语雀
此处为语雀内容卡片点击链接查看五、细粒度管理内存 · 语雀 5.1. 内存空间的划分
而在我们在实现这个系统的过程中更要完全把控好内存的使用。首先我们在实现操作系统的时候操作内存分为两种内存操作 操作内存类型 说明 非管控的内存 首先内核代码本身加载到内存了需要一个内存空间存放这一块空间包括了 「内存管理模块」的元数据。比如内存管理的位图内核的代码指令内核页目录表、内核页表所在的空间这部分空间不参与内存管理的动态分配毕竟这是内存管理的元数据没法参与管理 内存管理的内存 剩下的就是被内存管理模块管控的内存提供口子统一在这里申请和释放内存
先看我们目前的内存结构如下图5-1所示 我们可以看到现在宏观上的内存结构可以分为这几个部分
低端1MB 实模式下低端1MB空间直接指定地址访问空间
内核页目录表、内核页表 固定内存地址访问毕竟分页功能是要经过这个页表的映射的
剩下可用内存 用作实现「内存管理」的空间 我们先看下低端1MB的结构如下图5-1所示 可以看到我们把kernel的指令部分都加载到低端1MB中在这低端1MB是不被内存管理管控的指定内存地址就可以访问得益于这一块空间的页表映射因此物理地址跟虚拟地址是一样的。 我们把内存确定好后空间的划分就成了这个样子如下图5-3所示 5.2. 内存管理的设计与实现
在上面图中我们说了哪些内存空间是可以被操作系统用来实现内存管理的现在我们就要对这部分空间进行统一管理要求做到 统一动态申请、统一动态释放。 其实这个实现原理也很简单就是使用位图。用位图来构建一个内存池位图的某一位就代表该池子中某一块内存使用与否。如下图5-4所示 而我们实际上可以把要管理的内存也分为了好几种
物理内存池 内核物理内存池。内核线程申请内存空间统一从这个池子里申请。用户物理内存池。用户进程申请内存空间从这个池子里申请。
虚拟地址池 每个进程有自己的页表有自己的虚拟地址范围因此每个进程需要一个虚拟地址池 在本系统中这些池子的细节我们可以看下表 内存池名称 类型 代表的地址范围 位图地址 内核物理内存池 物理内存池 [0x200000, 0x11000000) 总15MB 物理内存 [0x9A000, 0x9A1E0) 总480 bytes 用户物理内存池 [0x11000000, 0x2000000] 总15MB 物理内存 [0x9A1E0, 0x9A3C0) 总480 bytes 内核虚拟地址池 虚拟地址池 [0xC000 0000, 0xC0F0 0000) 总15MB逻辑上虚拟的 [0x9A3C0, 0x9A5A0) 总480 bytes 用户虚拟地址池 (每个用户进程都有1个) [0x8048000, 0xC000 0000) 约总2.88GB逻辑上虚拟的 动态申请的地址 总92KB23页 最终构建的内存池的效果如下图5-5所示 关于更多内存池的细节可以看我写的这篇文章
此处为语雀内容卡片点击链接查看四、*管理内存 · 语雀 5.3. 细粒度的内存管理实现依赖进程/线程的实现
上面我们讲了内存管理的底层数据结构就是用的「位图」我们位图的1个位就表示了4KB空间的使用与否。
这样粒度明显太大了在上层使用的的时候并不会这么大的粒度分配而更多的是按照字节分配。 因此把申请到的1页空间封装成一个Arena的结构然后“切碎”成很多小的MemBlock然后把没用完的MemBlock串起来放到申请内存的这个任务中。如下图5-6所示 然后把这些小块MemBlock组成链表串起来如下图5-7所示 最后把这些剩下的MemBlock按照规格的划分放到“申请内存的这个任务”的PCB里下次申请就直接从这个MemBlock队列里面取如下图5-8所示 这样下次该任务再申请内存空间的时候会先从自身任务拥有的MemBlock列表中去匹配合适规格的MemBlock直接从这里申请。而不再从内存管理中的位图申请。
6. 内核线程
我们把希望计算机做的一件事情抽象成“任务”那么每个“任务”都有自己的元信息这个就是TaskStruct。
我们还希望多个任务可以切换那么我们只需要把TaskStruct跟对应的上下文资源绑定并且在切换任务的时候保存和恢复上下文资源就可以做到让一个执行中的程序“暂停”和“继续”的效果。 在本节中我们讲一讲内核线程的实现用户进程我们放到下一讲。细节的可以看我写的这些内容
此处为语雀内容卡片点击链接查看一、如何理解进程/线程 · 语雀
此处为语雀内容卡片点击链接查看二、任务切换的原理 · 语雀
此处为语雀内容卡片点击链接查看三、任务队列 · 语雀
此处为语雀内容卡片点击链接查看四、创建内核线程 · 语雀
此处为语雀内容卡片点击链接查看五、线程切换 · 语雀 当我们理解了程序的执行原理以及用到的资源栈空间、寄存器资源那么我们也能理解只要把这些资源保存起来然后切换任务就可以做到“暂停”这个任务然后还可以恢复执行。 而我们线程切换就是保存上下文资源和恢复上下文资源的过程目前一个程序我们用到的就是寄存器资源栈地址也是寄存器中因此只要我们把寄存器资源保存到一块内存空间就好了我们把它保存到栈中。 比如我们要从任务A切换到任务B那么我们先保存任务A的寄存器资源如下图6-1所示 保存完毕后我们把当前使用的栈ESP寄存器切换到任务B的栈地址记录在任务B的TaskStruct中如下图6-2所示 然后再恢复任务B栈内的寄存器资源如下图6-3所示 这样以往我们就实现了保存和恢复某个任务的上下文资源。 但是在一个线程首次执行的时候则需要自己构建这个线程的上下文资源然后让esp指向这个构建好数据栈空间然后走「恢复上下文」的逻辑从而做到切换的效果。
这个就稍微复杂一点可以看我写的这篇文章
此处为语雀内容卡片点击链接查看四、创建内核线程 · 语雀
其实原理也简单就是利用恢复上下文切换线程的这个原理和流程我们创建好TaskStruct后自己填充这个任务的栈数据然后恢复上下文从而实现切换任务。
7. 用户进程
我们上面实现了内核线程后下面就要实现用户进程。用户进程的实现比内核线程要复杂一些。
关于用户进程跟内核进程的关系如下图7-1所示 关于实现用户进程和内核线程底层逻辑核心的区别有以下几点 内核线程 用户进程 运行时所处的特权级 0级 3级中断处理程序时是0级 任务PCB 申请1页内核空间 申请1页内核空间 虚拟地址池 所有内核线程共用一个虚拟地址池 总可用虚拟地址的范围是 [0xC001 0000, 0xFFFF FFFF] 每个进程有各自的虚拟地址池 每个地址池的地址范围是[0x804 8000, 0xC000 0000) 页表 所有内核线程共用一个页目录表 每个用户进程有各自的页目录表 栈空间分布 只有0级栈(跟PCB共用1页) 0级栈(PCB共用1页)3级栈(单独申请1页用户空间作为3级栈) 栈空间的使用 中断和非中断都使用1个栈被执行时需要把0级栈地址放到TSS 非中断执行时使用3级栈 中断发生时使用0级栈
以上对比了内核线程和用户进程在实现上的异同对比项可以分为两大类
用到的资源的不同CPU特权级的不同
资源上的差异还好说申请资源就是了关键在于CPU特权级我们如何让用户进程运行在3特权级下。 关于用户进程我也不细讲了详细的细节可以看我写的这几篇文章
此处为语雀内容卡片点击链接查看一、用户进程 · 语雀
此处为语雀内容卡片点击链接查看二、*任务切换的栈操作 · 语雀
此处为语雀内容卡片点击链接查看三、实现用户进程的常见问题 · 语雀
此处为语雀内容卡片点击链接查看四、进程拥有的资源 · 语雀 要理解用户进程的难点我认为要抓住用户进程的要点这个要点就是「特权级」。
我们设计出用户进程就是限制这个任务在操作上的安全性因此特权级就是实现用户进程最大的要点。
7.1. 中断和特权级
我们系统启动特权级处于0级下处于最高级。
我们前面在讲中断的时候就说到中断发生的时候CPU会保存上下文提升特权级然后中断退出就会恢复上下文降低特权级。我们要实现用户进程在低特权级下运行就要利用中断的退出。 因此要想利用好中断退出我们必须理解中断发生和结束时栈内数据的完整变化这个可以看我签名讲过的文章
此处为语雀内容卡片点击链接查看三、中断发生时的栈操作 · 语雀
当CPU发生中断会保存寄存器上下文那么栈中的数据就是这样的如下图7-2所示 当中断恢复的时候CPU也会自动从栈中的这个数据来恢复寄存器的值。其中就包括了eip的值也就是当上下文恢复eip的值就会实现跳转。
所以我们要利用好栈内上下文数据恢复时的跳转属性来达到跳转执行用户程序的目的。 我们创建好一个用户进程的TaskStruct通过填充栈信息如下图7-3所示 其中我们把栈的数据逻辑划分为了两个栈
中断栈 CPU发生中断时自动压栈和出栈。我们通过填充这个栈的数据然后中断退出从而达到跳转的目的。
线程栈 这是为了兼容中断处理程序发生后手动压栈的数据。内核线程切换和启动的时候就是利用的这个栈的数据因此我们用户进程也要兼容。 7.2. 用户进程的启动和切换
当我们构建好用户进程TaskStruct的栈数据中断栈和线程栈之后我们就可以来通过“恢复上下文”的方式来跳转并且利用iret中断退出的指令来实现特权级降低和指令跳转功能。 这个过程如下图7-4所示 随着栈内上下文数据的弹出
先弹出「线程栈」恢复了一些通用寄存器的上下文这些是中断处理程序会保存和恢复的上下文数据。然后利用iret指令CPU自动弹出「中断栈」恢复EIP寄存器的值后跳转以及降低特权级到3级 当我们能够在低特权级下执行某个任务那么也就是实现了用户进程了。 8. 系统调用
此处为语雀内容卡片点击链接查看一、什么是系统调用 · 语雀
此处为语雀内容卡片点击链接查看二、如何实现系统调用 · 语雀
此处为语雀内容卡片点击链接查看三、*系统调用的全链路分析 · 语雀
我们在低特权级下想要访问高特权级的资源比如执行某些特权指令那么就必须使用系统调用提升了特权级。如下图8-1所示 关于系统调用的实现我这里使用了中断来实现。具体系统调用的细节就不细说了发现本篇文章我写的内容太多了。
建议看我写的这篇精华文章很细节
此处为语雀内容卡片点击链接查看三、*系统调用的全链路分析 · 语雀 9. 文件系统
一个操作系统最直观的入口其实就是文件系统比如我们登录Linux一定要操作文件比如使用ls、cd等命令都是文件系统的命令。 但是其实文件系统是跟操作系统耦合性最小的在我看来操作系统更偏向底层 而文件系统其实偏向设计
比如上面讲的操作系统很多概念比如全局描述符表、页表、中断等等都跟硬件强相关。而文件系统硬件相关的只有读写硬盘并且这个完全可以封装成一个驱动层。剩下的就全是“文件”的设计比如“文件如何存储”、“文件和目录的关系”、“文件如何CRUD”等等。更加偏向应用层的设计
此处为语雀内容卡片点击链接查看一、硬盘和分区结构 · 语雀
此处为语雀内容卡片点击链接查看二、硬盘的操作(PIO模式) · 语雀
此处为语雀内容卡片点击链接查看三、什么是文件系统 · 语雀
此处为语雀内容卡片点击链接查看四、*文件系统的概要设计(精华) · 语雀
此处为语雀内容卡片点击链接查看五、文件系统的需求 · 语雀
此处为语雀内容卡片点击链接查看六、inode的详细设计 · 语雀
此处为语雀内容卡片点击链接查看七、目录项的设计 · 语雀
此处为语雀内容卡片点击链接查看八、文件的打开结构(文件描述符) · 语雀 9.1. 分区结构
在理解文件系统之前我们要先能够操作硬盘。那么我们就得理解分区。
分区就像给硬盘格式化让硬盘有一个基本的格式信息。“分区”这种结构是市面上大家都遵守一种结构。这里分区的结构也不细说了可以见如下图9-1所示 更多的可以看这里
此处为语雀内容卡片点击链接查看一、硬盘和分区结构 · 语雀
9.2. 硬盘操作PIO模式
在大学中我们学计算机组成原理都学过主机I/O的数据交换方式有三种
程序查询程序中断DMA
我们的系统要读取硬盘那么也就是这三种方式。在前面的MBR、Loader中读取硬盘使用的是程序查询也就是读取硬盘的寄存器如果数据没有准备好就轮询等待。 而现在我们的文件系统实现中我们有了中断的功能我们使用 程序中断的方式。这个其实就是 硬盘的PIO模式。
关于PIO模式的使用方法可以看这篇文档
https://wiki.osdev.org/ATA_PIO_Mode
这里就不细说了。
9.3. 文件系统的核心设计
文件系统有点像内存管理 文件管理。在我看来文件系统有两个东西要管理
管理硬盘的块或者说扇区使用LBA地址访问管理“文件”这是一种抽象的概念
对于“块”或者“扇区”的管理我们可以跟内存管理一样使用「块位图」。
而对于“文件”的管理我们就要引入新的数据结构和设计了。 另外我们的文件系统是要持久化保存的所以跟其他的结构设计还不一样只在内存里存在就行我们文件系统设计的数据结构得保存到硬盘里。
这里我不多说了直接放出文件系统设计后的结构如下图9-2所示 更加详细的讲解可以看这里
此处为语雀内容卡片点击链接查看四、*文件系统的概要设计(精华) · 语雀 9.4. 文件系统的目标
文件系统是一个复杂的系统在实现这个过程中我们一定要明确我们要实现哪些功能这样有目标地去做才能高效地实现。
因此我找了一些作为一个 常规文件系统有的功能然后列出要实现的需求
此处为语雀内容卡片点击链接查看五、文件系统的需求 · 语雀
这样我们更加有目的性地去思考和设计。在我看来我们的目标最核心的无非一下几点
文件操作实现文件的增、删、改、查目录操作实现目录的增、删、改、查目录和文件的层级关系
其中最需要考虑和设计的就是「目录和文件的层级关系」。要想实现得优雅、扩展性高是我们文件系统的亮点。
9.5. inode的设计
在理解文件系统的设计中只要在我看来需要理解三个核心模块
inode目录项文件和目录
一个文件必然要包含数据不然这个文件毫无意义。 其中inode描述的是一个文件的数据的元信息比如某个文件包含的数据分布在哪些地方。 因此inode结构中最核心功能就是 表明这个文件的数据内容 在哪些扇区也就是如下图9-3所示 这里inode的数据扇区i_sectors是一个数组长度是有限的要是文件的长度超过这个怎么办呢因此我们有了二级索引如下图9-4所示 我们通过设置i_secotors的最后一项的值指向的是间接块的LBA地址然后该块的全部数据又指向新的LBA块。 这样间接块的设计那么一个inode就可以包含多个数据块了如果不够甚至可以再增加一级间接块。关于inode的设计详细的可以看我写的这篇文章
此处为语雀内容卡片点击链接查看六、inode的详细设计 · 语雀 9.6. 文件、目录和目录项的设计
在实现文件系统中目录和文件是存在层级关系的但是目录和文件这两种类型我们都抽象理解为“文件”。 在设计上我们不区分「目录」跟「普通文件」他们都有自己的inode指向对应的数据区。那么怎么区分一个文件到底是「目录」还是「普通文件」呢我们使用「目录项」的结构。 目录项记录一个“文件”的名称、类型、inode号。而根据inode号就可以找到inode就可以找到当前“文件”包含的数据区。
这里的“文件”可以是普通文件也可以是目录inode的数据区如果是普通文件那么数据区里面是这个文件的二进制数据。如果是目录文件那么这个inode的数据区是很多的目录项。 因此有了目录项、inode、目录、普通文件的设计他们现在的关系如下图9-5所示 关于更多他们关系的讲解可以看我写的这篇文章
此处为语雀内容卡片点击链接查看七、目录项的设计 · 语雀
此处为语雀内容卡片点击链接查看为什么inode和目录项要分开存储 · 语雀
9.7. 文件描述符
每个进程都可以打开一个文件并且可以重复打开文件。并且打开之后同一个文件的两个打开结构互不干扰。比如vscode浏览器到这个文件的第100字节而edge浏览到这个文件的第300字节。两者互相独立。 那么关于打开一个文件我们可以得出以下结论
同一个文件可以被打开多次同一文件打开多次后可以处于不同的偏移量打开后可以进行的操作比如读、写权限 因此我们打开一个文件后应该需要一个结构来描述这个文件的“打开结构”相对于inode和目录项的结构这是一种动态结构同一个文件但是偏移量不同。 因此我们可以把这些 「打开的文件结构」统一放到一起组成一个「文件结构表」如下图9-6所示 我们可以看到
OpenedInode是描述的一个文件的静态属性比如文件所在的地方、文件数据的内容File文件结构描述的是文件的动态属性其中file_off描述了这个文件操作的偏移量如果多次打开一个文件那么OpenedInode是同一个但是会有不同的File文件结构。因为不同的打开偏移量不同 上面的「文件结构表」是整个系统全局的而我们每个进程应该拥有自己独立的操作的文件因此我们抽象出「文件描述符」的结构
每个任务的TaskStruct有个「文件描述符数组」这个数组的值就是「文件结构表」的下标。每个进程的「文件描述符」就是「文件描述符数组」的下标。 也就是「文件描述符」定位到「文件描述符数组」的元素再定位到「文件结构表」中的文件结构然后再定位到「具体的文件包括偏移、inode」这个过程如下图9-7所示 当我们完成文件的设计特别是目录项、目录、文件的层级关系的设计以及inode的理念我们就基本上已经完成了我们最终的文件系统了。 剩下的操作比如「对文件增、删、改、查」和「对目录的增、删、改、查」都只要按照这个设计来填充数据和查询数据就好了。
10. 系统交互
最后一步就是实现系统交互的我们的系统基本上实现得大差不差了我们Shell的作用只是锦上添花。关于系统交互上详细设计可以看下面我写的文章
此处为语雀内容卡片点击链接查看一、fork系统调用的实现 · 语雀
此处为语雀内容卡片点击链接查看二、Shell的基础实现 · 语雀
此处为语雀内容卡片点击链接查看三、实现从文件系统加载并且运行程序 · 语雀
此处为语雀内容卡片点击链接查看四、运行用户程序支持参数 · 语雀
此处为语雀内容卡片点击链接查看五、*Shell支持管道 · 语雀
此处为语雀内容卡片点击链接查看五、系统调用exit和wait · 语雀 我们在这一步实现中基本上把前面所学的都利用起来了在我看来本系统的系统交互有一下几个核心点
实现fork创建进程 这也是多进程的开始也是系统之间交互的基础支撑
从文件中加载用户程序 真正实现用户进程。把用户进程的二进制指令从硬盘中加载到内存然后跳转执行。乍一看很复杂其实理解了程序的原理后就很简单了。
管道和文件描述符的重定向 实现我们最常见的命令并且实现进程间通信
10.1. fork进程
实现进程的fork功能几乎是后面实现多进程以及管道的前提。也是理解我们在应用层编码中多线程的重要一环。
我在这里简单说一说fork的实现具体的可以看我写的这篇文章
此处为语雀内容卡片点击链接查看一、fork系统调用的实现 · 语雀 所谓fork其实就是把当前进程拥有的资源全部拷贝一遍深拷贝那么一个进程拥有哪些资源呢我们可以看如下图10-1所示 剩下的就是要拷贝进程拥有的资源了比如「TaskStruct」、「中断栈」、「文件描述符表」这些都好说关键复杂的在于「拷贝一个进程的堆内存空间」。
因为拷贝进程的堆内存空间也就意味着要完整拷贝的页表映射关系。 比如进程A fork出了进程B进程A的资源X复制了一份得到资源X。那么fork后进程A通过地址0x1234到资源X那么进程B也要通过地址0x1234访问到资源X。
也就是效果是如下图10-2所示的 根据上图10-2我们把进程的堆内存指向的某个物理空间进行了数据拷贝但是使用虚拟地址访问的时候虚拟地址要相同。 要实现这个效果我们就处理起来比较麻烦了这个拷贝流程如下图10-3所示 关于fork这个过程其实并不简单建议还是看看我写的文章
此处为语雀内容卡片点击链接查看一、fork系统调用的实现 · 语雀 10.2. 从文件系统加载程序运行
在上面进程管理中我们可以实现把一个内核函数封装成一个用户进程。
现在我们希望把一个程序从硬盘加载到内存然后再作为用户进程执行。其实这个原理是一样的不管程序是来自哪里都是跳转到入口开始执行。
因此关键点在于我们把一个程序的指令加载到内存然后根据程序的入口地址封装成一个用户进程即可。 在常规操作系统的实现中由于这个用户程序是其他操作系统生成的比如在Linux因此我们需要解析ELF文件然后提取出 二进制指令部分然后加载到内存的某个地址。
这里我使用了一个比较取巧的方式直接使用x86下32位的Linux的objcopy工具直接把一个32位的elf文件中的指令二进制给提取出来了然后再写入到我们自己操作系统的文件系统里这个过程如下图10-4所示 当我们的操作系统能够把用户程序的二进制加载到内存并且知道这个程序在内存的入口地址之后就可以根据这个封装成一个用户进程了这个过程如下图10-5所示 这个加载用户进程并且执行的过程其实本质上跟前面进程管理把一个函数封装成用户进程 没有区别因为我们只需要知道一个程序的在内存的入口地址就可以实现跳转执行指令了。 关于这个过程的更多细节可以看我写的这篇文章
此处为语雀内容卡片点击链接查看三、实现从文件系统加载并且运行程序 · 语雀 10.3. Shell支持管道
在使用操作系统时我们经常使用到管道并且管道是一个很好的进程间通信的方式。
因此本系统中也要支持管道并且要优雅的地支持管道功能。 在本系统中管道使用一个「阻塞队列」来实现阻塞队列如下图10-6所示 本系统中支持管道的实现并且管道也使用文件描述符来访问也就是现在文件描述符和管道的关系是这样的如下图10-7所示 也就是文件描述符有了类型既可以访问文件也可以访问管道还可以访问标准输入和标准输出。
后面就知道这种设计的好处了。一切皆是文件。 然后当我们的父进程创建管道然后再fork出子进程子进程就自动复制了父进程的文件描述符那么子进程就也可以访问到管道了。这个结构如下图10-8所示 那么只要两个进程可以访问到同一个管道那么这两个进程就可以通过管道来信息交互的一个进程往管道里写数据一个进程往管道里消费数据并且这个管道是一个阻塞队列可以保证生产者和消费者协调进行。 另外再搭配上 文件描述符的重定向就可以实现我们很常用的的命令
cat xxx.log | grep 2024 | grep main main.txt
这个命令的实现思路是这样的如下图10-9所示 而在实际的原理中我们做的其实是两件事情
父进程创建管道多个子进程共享管道。 这样看来一个管道就是一个生产者消费者模型从而实现数据交换的目的
重定向文件描述符。 在不改动子进程指令本身的情况下修改进程的文件描述符把进程的标准输入、标准输出文件描述符修改为管道或者文件的文件描述那么这个进程原本的标准输入和输出就变成了对管道或者文件的操作
当我们把这两件事做好后实现的效果就是如下图10-10所示 其实关于管道的实现和思考还有很多细节具体的可以看我这篇文章
此处为语雀内容卡片点击链接查看五、*Shell支持管道 · 语雀 10.4. 系统调用wait和exit
一个进程调用exit会退出这个进程的资源会被销毁。
父进程调用wait会等待子进程送子进程的“最后一程”。 这个过程有点像是fork的逆操作把fork中用到梳理的资源都给释放掉包括内存资源、文件资源、管道等等。这里就不细讲了可以看我写的这篇文章
此处为语雀内容卡片点击链接查看五、系统调用exit和wait · 语雀 11. 其他
其实还有很多东西没有讲我认为不是特别核心的设计就算了。
标准输出 如何实现在屏幕上打印字符我们在文本模式下往显存的区域按照规则写入ascii码即可。还有根据显卡控制器的规则控制光标。
标准输入如何读取键盘输入的键 这个涉及到键盘的扫描码读取键盘中断中的扫描码然后再根据键的组合通码、断码、shift键来生成对应的ascii码。本系统中把ascii码存到了一个全局的阻塞队列中。然后标准输入的话会从阻塞队列中读取数据如果没有数据就会阻塞。 还有很多其实我觉得特别有意思但是不是本系统的核心设计并且限于篇幅没有写的。 12. 参考资源
《操作系统真象还原》
Writing an OS in Rust
Preface - The Embedonomicon 四、展望
1. 我为什么要实现这个操作系统
这个项目从我第一次提交到最后的结束几乎耗时我5个月满打满算的5个月。这5个月来我几乎把所有的空余时间都花在了这个上面。每天除了上班就是在写这个项目。 首先本系统是我三年前写的一个操作系统的延续
GitHub - CaoGaorong/MyOS: To make an operating system from scratch
三年前我还在大学的时候就开始想着写一个操作系统了当时我是跟着书《操作系统真象还原》写的使用的C语言当时是大三我也是几乎把所有空余时间都花在了这个上面。
但是由于时间不够充裕只有一个学期的时间之后就要找工作了。所以只是跟着书上写并且并没有完全写完项目能跑但是文件系统没有实现。 在我上面三年前的仓库的readme中最后一段我曾经说要继续写操作系统并且补上文件系统而且当时我就曾想过要用rust来实现。没想到时隔三年的今天我都实现了完善了文件系统 rust实现。 今年我使用rust把这个操作系统重写、重新设计我是有三个期望
重新捡起我学过的操作系统毕竟三年了有点忘了更加深入完整学习操作系统之前只是跟着书上看并没有太多的调试经验并且没有实现文件系统学习Rust语言找个项目上手虽然学得不多但是确实对Rust更加了解了 现在过了5个月我把这个项目实现后再回头看还是很有成就感了。
2. 在这个过程中我遇到哪些问题
在五个月前我刚开始有用rust重写这个系统的想法的时候我是很犹豫的我最担心的就是这个项目流产因为当时的我对操作系统是半吊子rust更是只懂基础语法很多的偏门用法比如各种配置都不会。
所以当时我是很怕这个项目做不完特别怕这个项目做一半然后卡住了后面一直解决不了从而放弃。 一开始我害怕中途卡住而流产但是随着后面我遇到问题解决问题我渐渐地变得很有自信我开始越来越坚定地相信我可以完成。也许是我不断遇到难题并且不断解决的这个过程让我变得信心十足。 除了解决了信念不坚定的问题更多遇到的就是实打实操作系统和Rust相关的问题。非常多稀奇古怪的问题特别是操作系统这种底层软件没有应用层开发时的操作系统兜底也很容易出现一些少见、并且难以解决的问题。 并且这些问题在市面上很难找到答案比如操作系统相关的信息在网上就很少有资源再配合上Rust又是一个比较新的语言因此这两者在市面上都很少有相关的资料。
不过这些问题我最终还是都解决了我基本从以下三个方面解决了我的问题
外网的资料 不管是操作系统还是Rust外网的资料还是比较多的有很多的资料可以借鉴
其他相似的项目 自制操作系统的项目有很多可以参考设计。不过用Rust实现而且功能全的很少。
ChatGPT 这两年出了ChatGPT确实让我的编程变得方便了很多不管是遇到Rust还是操作系统的问题我都可以问ChatGPT虽然答案不一定可靠但是还是给了我很多帮助。 其中我遇到的更细节的问题更是有不少有一些我记录了下来
此处为语雀内容卡片点击链接查看记录一次实现fork栈溢出导致的问题 · 语雀
此处为语雀内容卡片点击链接查看最离奇的问题 · 语雀
此处为语雀内容卡片点击链接查看QEMU重复启动 · 语雀
此处为语雀内容卡片点击链接查看mbr无法打印问题 · 语雀
此处为语雀内容卡片点击链接查看release profile编译优化配置 · 语雀
此处为语雀内容卡片点击链接查看loader的opt-level设置问题 · 语雀
此处为语雀内容卡片点击链接查看loader的lto设置问题 · 语雀
此处为语雀内容卡片点击链接查看加载GDT问题 · 语雀
此处为语雀内容卡片点击链接查看构建页表途中卡住问题 · 语雀
此处为语雀内容卡片点击链接查看突然不能打印了 · 语雀
此处为语雀内容卡片点击链接查看构建IDT死循环问题 · 语雀
此处为语雀内容卡片点击链接查看静态变量初始化为0值不对的问题 · 语雀
此处为语雀内容卡片点击链接查看函数调用却自动内联 · 语雀 还有更多我遇到的问题没有记录下来在这个系统开发的早期经常遇到问题有的是关于Rust语言一些特性的问题随着我后面对Rust越来越熟练和了解在本系统的后续开发问题也变得越来越少。
3. 我后续会做哪些事情
这个项目确实一个大项目耗费我的时间比较长。但是如果说商用价值确实不大假如说我去学习一下Java的应用技术、各种中间件、大数据可能更好找工作工作能有更大的价值。 但是我做这个项目确实能给我更大的满足感就像锻炼完虽然很累但是又很爽的感觉。
昨晚这个项目我的收获确实非常大我花费五个月觉得非常值得最主要的是两点
学习了Rust。并且学习的是底层原理比如操作汇编、内存学习了操作系统。之前只是看但是这次我自己实现对操作系统、计算机底层的感悟更加深刻了 所以其实还有很多我可以做的包括我所在公司的部门就有IM的业务我想我如果我想深入学习IM我可以在我的这个系统上实现一套IP/TCP协议栈让我的操作系统实现网络的功能这样应该会对IM的理解更加深入。 另外还有很多可以做的比如对硬盘的操作我一直想学习使用一下DMA总是看到这个词但是没有底层实现过终归感觉不是完全了解。还有比如操作系统的图形界面GUI这个也比较炫酷。 总的来说可以做的还有很多我希望还能有机会像这次一样花费半年的时间来沉浸式地去做一件事情。