提交 aef179d6 编写于 作者: Y Yifan Wu

Deploy

上级 e7e0496c
......@@ -265,6 +265,9 @@ GDB 调试支持
运行 rCore-Tutorial-v3
------------------------------------------------------------
在 Qemu 平台上运行
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
如果是在 Qemu 平台上运行,只需在 ``os`` 目录下 ``make run`` 即可。在内核加载完毕之后,可以看到目前可以用的
应用程序。 ``usertests`` 打包了其中的很大一部分,所以我们可以运行它,只需输入在终端中输入它的名字即可。
......@@ -272,6 +275,9 @@ GDB 调试支持
之后,可以先按下 ``Ctrl+A`` ,再按下 ``X`` 来退出 Qemu。
在 K210 平台上运行
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
如果是在 K210 平台上运行则略显复杂。
首先,我们需要将 MicroSD 插入 PC 来将文件系统镜像拷贝上去。
......
......@@ -65,13 +65,7 @@
.. code-block::
[rustsbi] RustSBI version 0.1.1
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
<rustsbi-logo>
[rustsbi] Platform: QEMU (Version 0.1.0)
[rustsbi] misa: RV64ACDFIMSU
[rustsbi] mideleg: 0x222
......@@ -173,11 +167,11 @@
应用程序运行中,操作系统要支持应用程序的输出功能,并还能支持应用程序退出。这需要完成 ``sys_write`` 和 ``sys_exit`` 系统调用访问请求的实现。 具体实现涉及到内联汇编的编写,以及应用与操作系统内核之间系统调用的参数传递的约定。为了让应用在还没实现操作系统之前就能进行运行测试,我们采用了Linux on RISC-V64 的系统调用参数约定。具体实现可参看 :ref:`系统调用 <term-call-syscall>` 小节中的内容。 这样写完应用小例子后,就可以通过 ``qemu-riscv64`` 模拟器进行测试了。
写完应用程序后,还需实现支持多个应用程序轮流启动运行的操作系统。这里首先能把本来相对松散的应用程序执行代码和操作系统执行代码连接在一起,便于 ``qemu-system=riscv64`` 模拟器一次性地加载二者到内存中,并让操作系统能够找到应用程序的位置。为把二者连在一起,需要对生成的应用程序进行改造,首先是把应用程序执行文件从ELF执行文件格式变成Binary格式(通过 ``rust-objcopy`` 可以轻松完成);然后这些Binary格式的文件通过编译器辅助脚本 ``os/build.rs`` 转变变成 ``os/src/link_app.S`` 这个汇编文件的一部分,并生成各个Binary应用的辅助信息,便于操作系统能够找到应用的位置。编译器会把把操作系统的源码和 ``os/src/link_app.S`` 合在一起,编译出操作系统+Binary应用的ELF执行文件,并进一步转变成Binary格式。
写完应用程序后,还需实现支持多个应用程序轮流启动运行的操作系统。这里首先能把本来相对松散的应用程序执行代码和操作系统执行代码连接在一起,便于 ``qemu-system-riscv64`` 模拟器一次性地加载二者到内存中,并让操作系统能够找到应用程序的位置。为把二者连在一起,需要对生成的应用程序进行改造,首先是把应用程序执行文件从ELF执行文件格式变成Binary格式(通过 ``rust-objcopy`` 可以轻松完成);然后这些Binary格式的文件通过编译器辅助脚本 ``os/build.rs`` 转变变成 ``os/src/link_app.S`` 这个汇编文件的一部分,并生成各个Binary应用的辅助信息,便于操作系统能够找到应用的位置。编译器会把把操作系统的源码和 ``os/src/link_app.S`` 合在一起,编译出操作系统+Binary应用的ELF执行文件,并进一步转变成Binary格式。
操作系统本身需要完成对Binary应用的位置查找,找到后(通过 ``os/src/link_app.S`` 中的变量和标号信息完成),会把Binary应用拷贝到 ``user/src/linker.ld`` 指定的物理内存位置(OS的加载应用功能)。在一个应执行完毕后,还能加载另外一个应用,这主要是通过 ``AppManagerInner`` 数据结构和对应的函数 ``load_app`` 和 ``run_next_app`` 等来完成对应用的一系列管理功能。
这主要在 :ref:`实现批处理操作系统 <term-batchos>` 小节中讲解。
为了让Binary应用能够启动和运行,操作系统还需给Binary应用分配好执行环境所需一系列的资源。这主要包括设置好用户栈和内核栈(在应用在用户态和内核在内核态需要有各自的栈),实现Trap 上下文的保存与恢复(让应用能够在发出系统调用到内核态后,还能回到用户态继续执行),完成Trap 分发与处理等工作。由于涉及用户态与内核态之间的特权级切换细节的汇编代码,与硬件细节联系紧密,所以 :ref:`这部分内容 <term-trap-handle>` 是本章中理解比较困难的地方。如果要了解清楚,需要对涉及到的CSR寄存器的功能有清楚的认识。这就需要看看 :ref:`RISC-V手册 <http://crva.ict.ac.cn/documents/RISC-V-Reader-Chinese-v2p1.pdf>` 的第十章或更加详细的RISC-V的特权级规范文档了。有了上面的实现后,就剩下最后一步,实现 **执行应用程序** 的操作系统功能,其主要实现在 ``run_next_app`` 函数中 。
为了让Binary应用能够启动和运行,操作系统还需给Binary应用分配好执行环境所需一系列的资源。这主要包括设置好用户栈和内核栈(在应用在用户态和内核在内核态需要有各自的栈),实现Trap 上下文的保存与恢复(让应用能够在发出系统调用到内核态后,还能回到用户态继续执行),完成Trap 分发与处理等工作。由于涉及用户态与内核态之间的特权级切换细节的汇编代码,与硬件细节联系紧密,所以 :ref:`这部分内容 <term-trap-handle>` 是本章中理解比较困难的地方。如果要了解清楚,需要对涉及到的CSR寄存器的功能有清楚的认识。这就需要看看 `RISC-V手册 <http://crva.ict.ac.cn/documents/RISC-V-Reader-Chinese-v2p1.pdf>`_ 的第十章或更加详细的RISC-V的特权级规范文档了。有了上面的实现后,就剩下最后一步,实现 **执行应用程序** 的操作系统功能,其主要实现在 ``run_next_app`` 函数中 。
......@@ -227,13 +227,13 @@
本章涉及的代码量相对较多,且与进程执行相关的管理还有直接的关系。其实我们是参考经典的UNIX基于索引的文件系统,设计了一个简化的有一级目录并支持创建/打开/读写/关闭文件一系列操作的文件系统。这里简要介绍一下在内核中添加文件系统的大致开发过程。
第一步是能够写出与文件访问相关的应用。这里是参考并简化了Linux的创建/打开/读写/关闭文件的系统调用,在用户态设计并实现这些系统调用的接口。前面每章都或多或少地添加或改变各种系统调用,所以,在用户态实现面向文件的这几个系统调用接口是比较容易的
第一步是能够写出与文件访问相关的应用。这里是参考了Linux的创建/打开/读写/关闭文件的系统调用接口,力图实现一个 :ref:`简化版的文件系统模型 <fs-simplification>` 。在用户态我们只需要遵从相关系统调用的接口约定,在用户库里完成对应的封装即可。这一过程我们在前面的章节中已经重复过多次,读者应当对其比较熟悉。其中最为关键的是系统调用可以参考 :ref:`sys_open 语义介绍 <sys-open>` ,此外我们还给出了 :ref:`测例代码解读 <filetest-simple>`
第二步就是要实现 easyfs 文件系统了。由于Rust语言的特点,我们可以在用户态实现 easyfs 文件系统,并在用户态完成文件系统功能的基本测试后,就可以不修改就嵌入到操作系统内核中。我们按照自底向上方的执行流程来介绍easyfs文件系统的具体实现。当然,有了文件系统的具体实现,还需要对上一章的操作系统内核进行扩展,实现与 easyfs 文件系统对接的接口,这样才可以让操作系统拥有一个简单可用的文件系统。带有文件系统的操作系统就可以提高应用开发体验和程序执行与互操作的灵活性,让应用获得文件系统带了的各种便利
第二步就是要实现 easyfs 文件系统了。由于 Rust 语言的特点,我们可以在用户态实现 easyfs 文件系统,并在用户态完成文件系统功能的基本测试并基本验证其实现正确性之后,就可以放心的将该模块嵌入到操作系统内核中。当然,有了文件系统的具体实现,还需要对上一章的操作系统内核进行扩展,实现与 easyfs 文件系统对接的接口,这样才可以让操作系统拥有一个简单可用的文件系统。从而,内核可以支持允许文件读写功能的更复杂的应用,在命令行参数机制的加持下,可以进一步提升整个系统的灵活性,让应用的开发和调试变得更为轻松
easyfs文件系统的设计实现有五层。它的最底层就是对块设备的访问操作接口。为了实现easyfs文件系统,首先需要定义 ``BlockDevice`` trait,其成员函数定义 ``read_block`` 和 ``write_block`` 是操作系统内核中的块设备驱动需要实现的函数。这样就可以把内核中的块设备驱动与easyfs文件系统进行对接。完成对接后,easyfs文件系统可以通过这两个函数对块设备进行读写。
easyfs 文件系统的整体架构自下而上可分为五层。它的最底层就是对块设备的访问操作接口。在 ``easy-fs/src/block_dev.rs`` 中,可以看到 ``BlockDevice`` trait 代表了一个抽象块设备,该 trait 仅需求两个函数 ``read_block`` 和 ``write_block`` ,分别代表将数据从块设备读到内存中的缓冲区中,或者将数据从内存中的缓冲区写回到块设备中,数据需要以块为单位进行读写。easy-fs 库的使用者需要负责为它们看到的实际的块设备具体实现 ``BlockDevice`` trait 并提供给 easy-fs 库的上层,这样的话 easy-fs 库的最底层就与一个具体的执行环境对接起来了。至于为什么块设备层位于 easy-fs 的最底层,是因为文件系统仅仅是在块设备上存储的结构稍微复杂一点的数据,但无论它的操作变换如何复杂,从块设备的角度终究可以被分解成若干次块读写。
而具体使用这两个函数的是自底向上的第二层 -- 块缓存。块缓存是把应用要访问的数据放到一块内存区域中,减少磁盘读写的次数,提高系统性能。块缓存通过 ``read_block`` 和 ``write_block`` 函数接口来读写磁盘数据。这些磁盘数据会缓存在内存中。表示块缓存的数据结构是 ``BlockCache`` 。当我们创建一个 ``BlockCache`` 的时候,将触发一次 ``read_block`` 函数调用,将一个块上的数据从磁盘读到块缓冲区中。由于缓存磁盘块的内存有限,我们需要实现缓存的替换,这就需要实现类似与页替换算法的缓存替换算法。为了简单,我们实现的是FIFO缓存替换算法。具体替换过程是块缓存全局管理器 ``BlockCacheManager`` 中的成员函数 ``get_block_cache`` 来完成的
尽管在最底层我们就已经有了块读写的能力,但从编程方便性和性能的角度,仅有块读写这么基础的底层接口是不足以实现如此复杂的文件系统的,虽然它已经被我们大幅简化过了。比如,将一个块的内容读到内存的缓冲区,对缓冲区进行修改,并尚未写回的时候,如果由于编程上的不小心再次将该块的内容读到另一个缓冲区,而不是使用已有的缓冲区,这将会造成不一致问题。此外还有可能增加很多不必要的块读写次数,大幅降低文件系统的性能。因此,通过程序自动而非程序员手动对块的缓冲区进行统一管理也就势在必行了,该机制被我们抽象为 easy-fs 自底向上的第二层,即块缓存层。在 ``easy-fs/src/block_cache.rs`` 中, ``BlockCache`` 代表一个被我们管理起来的块的缓冲区,它带有缓冲区本体以及块的编号等信息。当它被创建的时候,将触发一次 ``read_block`` 将数据从块设备读到它的缓冲区中。接下来只要它驻留在内存中,便可保证对于同一个块的所有操作都会直接在它的缓冲区中进行而无需额外的 ``read_block`` 。块缓存管理器 ``BlockManager`` 在内存中管理有限个 ``BlockCache`` 并实现了类似 FIFO 的缓存替换算法,当一个块缓存被换出的时候视情况可能调用 ``write_block`` 将缓冲区数据写回块设备。总之,块缓存层对上提供 ``get_block_cache`` 接口来屏蔽掉相关细节,从而可以透明的读写一个块
有了块缓存,我们就可以在内存中方便地处理easyfs文件系统在磁盘上的各种数据了,这就是第三层文件系统的磁盘数据结构。easyfs文件系统中的所有需要持久保存的数据都会放到磁盘上,这包括了管理这个文件系统的 **超级块 (Super Block)**,管理空闲磁盘块的 **索引节点位图区** 和 **数据块位图区** ,以及管理文件的 **索引节点区** 和 放置文件数据的 **数据块区** 组成。
......
......@@ -130,6 +130,8 @@ Blocks 给出 ``os`` 目录也占用 8 个块进行存储。实际上目录也
打开与读写文件的系统调用
--------------------------------------------------
.. _sys-open:
文件打开
++++++++++++++++++++++++++++++++++++++++++++++++++
......@@ -194,6 +196,8 @@ Blocks 给出 ``os`` 目录也占用 8 个块进行存储。实际上目录也
在打开一个文件之后,我们就可以用之前的 ``sys_read/sys_write`` 两个系统调用来对它进行读写了。需要注意的是,常规文件的读写模式和之前介绍过的几种文件有所不同。标准输入输出和匿名管道都属于一种流式读写,而常规文件则是顺序读写和随机读写的结合。由于常规文件可以看成一段字节序列,我们应该能够随意读写它的任一段区间的数据,即随机读写。然而用户仅仅通过 ``sys_read/sys_write`` 两个系统调用不能做到这一点。
事实上,进程为每个它打开的常规文件维护了一个偏移量,在刚打开时初始值一般为 0 字节。当 ``sys_read/sys_write`` 的时候,将会从文件字节序列偏移量的位置开始 **顺序** 把数据读到应用缓冲区/从应用缓冲区写入数据。操作完成之后,偏移量向后移动读取/写入的实际字节数。这意味着,下次 ``sys_read/sys_write`` 将会从刚刚读取/写入之后的位置继续。如果仅使用 ``sys_read/sys_write`` 的话,则只能从头到尾顺序对文件进行读写。当我们需要从头开始重新写入或读取的话,只能通过 ``sys_close`` 关闭并重新打开文件来将偏移量重置为 0。为了解决这种问题,有另一个系统调用 ``sys_lseek`` 可以调整进程打开的一个常规文件的偏移量,这样便能对文件进行随机读写。在本教程中并未实现这个系统调用,因为顺序文件读写就已经足够了。顺带一提,在文件系统的底层实现中都是对文件进行随机读写的。
.. _filetest-simple:
下面我们从本章的测试用例 ``filetest_simple`` 来介绍文件系统接口的使用方法:
......
......@@ -4,7 +4,7 @@
本节导读
---------------------------------------
本节我们介绍一个简易文件系统实现 easy-fs。作为一个文件系统而言,它的磁盘布局(为了叙述方便,我们用磁盘来指代一系列持久存储设备)体现在磁盘上各扇区的内容上,而它解析磁盘布局得到的逻辑目录树结构则是通过内存上的数据结构来访问的,这意味着它要同时涉及到对磁盘和对内存的访问。它们的访问方式是不同的,对于内存直接通过一条指令即可直接读写内存相应的位置,而磁盘的话需要用软件的方式向磁盘发出请求来间接进行读写。此外,我们也要特别注意哪些数据结构是存储在磁盘上,哪些数据结构是存储在内存中的,这样在实现的时候才不会引起混乱。
本节我们介绍一个简易文件系统实现 easy-fs。作为一个文件系统而言,它的磁盘布局(为了叙述方便,我们用磁盘来指代一系列持久存储设备)体现在磁盘上各扇区的内容上,而它解析磁盘布局得到的逻辑目录树结构则是通过内存上的数据结构来访问的,这意味着它要同时涉及到对磁盘和对内存的访问。它们的访问方式是不同的,对于内存直接通过一条指令即可直接读写内存相应的位置,而磁盘的话需要用软件的方式向磁盘发出请求来间接进行读写。因此,我们也要特别注意哪些数据结构是存储在磁盘上,哪些数据结构是存储在内存中的,这样在实现的时候才不会引起混乱。
松耦合模块化设计思路
---------------------------------------
......
......@@ -134,7 +134,11 @@ commentsRunWhenDOMLoaded(addUtterances);
<li class="toctree-l3"><a class="reference internal" href="#qemu">Qemu 模拟器安装</a></li>
<li class="toctree-l3"><a class="reference internal" href="#k210">K210 真机串口通信</a></li>
<li class="toctree-l3"><a class="reference internal" href="#gdb">GDB 调试支持</a></li>
<li class="toctree-l3"><a class="reference internal" href="#rcore-tutorial-v3">运行 rCore-Tutorial-v3</a></li>
<li class="toctree-l3"><a class="reference internal" href="#rcore-tutorial-v3">运行 rCore-Tutorial-v3</a><ul>
<li class="toctree-l4"><a class="reference internal" href="#id6">在 Qemu 平台上运行</a></li>
<li class="toctree-l4"><a class="reference internal" href="#id7">在 K210 平台上运行</a></li>
</ul>
</li>
</ul>
</li>
<li class="toctree-l2"><a class="reference internal" href="6hardware.html">K210 开发板相关问题</a></li>
......@@ -456,10 +460,15 @@ sudo apt install python-serial
</div>
<div class="section" id="rcore-tutorial-v3">
<h2>运行 rCore-Tutorial-v3<a class="headerlink" href="#rcore-tutorial-v3" title="永久链接至标题"></a></h2>
<div class="section" id="id6">
<h3>在 Qemu 平台上运行<a class="headerlink" href="#id6" title="永久链接至标题"></a></h3>
<p>如果是在 Qemu 平台上运行,只需在 <code class="docutils literal notranslate"><span class="pre">os</span></code> 目录下 <code class="docutils literal notranslate"><span class="pre">make</span> <span class="pre">run</span></code> 即可。在内核加载完毕之后,可以看到目前可以用的
应用程序。 <code class="docutils literal notranslate"><span class="pre">usertests</span></code> 打包了其中的很大一部分,所以我们可以运行它,只需输入在终端中输入它的名字即可。</p>
<img alt="../_images/qemu-final.gif" src="../_images/qemu-final.gif" />
<p>之后,可以先按下 <code class="docutils literal notranslate"><span class="pre">Ctrl+A</span></code> ,再按下 <code class="docutils literal notranslate"><span class="pre">X</span></code> 来退出 Qemu。</p>
</div>
<div class="section" id="id7">
<h3>在 K210 平台上运行<a class="headerlink" href="#id7" title="永久链接至标题"></a></h3>
<p>如果是在 K210 平台上运行则略显复杂。</p>
<p>首先,我们需要将 MicroSD 插入 PC 来将文件系统镜像拷贝上去。</p>
<img alt="../_images/prepare-sd.gif" src="../_images/prepare-sd.gif" />
......@@ -474,6 +483,7 @@ sudo apt install python-serial
<p>之后,可以按下 <code class="docutils literal notranslate"><span class="pre">Ctrl+]</span></code> 来退出串口终端。</p>
<p>到这里,恭喜你完成了实验环境的配置,可以开始阅读教程的正文部分了!</p>
</div>
</div>
</div>
......
......@@ -279,7 +279,11 @@ commentsRunWhenDOMLoaded(addUtterances);
<li class="toctree-l2"><a class="reference internal" href="5setup-devel-env.html#qemu">Qemu 模拟器安装</a></li>
<li class="toctree-l2"><a class="reference internal" href="5setup-devel-env.html#k210">K210 真机串口通信</a></li>
<li class="toctree-l2"><a class="reference internal" href="5setup-devel-env.html#gdb">GDB 调试支持</a></li>
<li class="toctree-l2"><a class="reference internal" href="5setup-devel-env.html#rcore-tutorial-v3">运行 rCore-Tutorial-v3</a></li>
<li class="toctree-l2"><a class="reference internal" href="5setup-devel-env.html#rcore-tutorial-v3">运行 rCore-Tutorial-v3</a><ul>
<li class="toctree-l3"><a class="reference internal" href="5setup-devel-env.html#id6">在 Qemu 平台上运行</a></li>
<li class="toctree-l3"><a class="reference internal" href="5setup-devel-env.html#id7">在 K210 平台上运行</a></li>
</ul>
</li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="6hardware.html">K210 开发板相关问题</a><ul>
......
......@@ -269,13 +269,7 @@ commentsRunWhenDOMLoaded(addUtterances);
</div>
<p>如果顺利的话,我们可以看到批处理系统自动加载并运行所有的程序并且正确在程序出错的情况下保护了自身:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>[rustsbi] RustSBI version 0.1.1
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ &lt; | |
| |\ \----.| `--&#39; |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
&lt;rustsbi-logo&gt;
[rustsbi] Platform: QEMU (Version 0.1.0)
[rustsbi] misa: RV64ACDFIMSU
[rustsbi] mideleg: 0x222
......@@ -373,10 +367,10 @@ Assembly 2 Files 58 Lines
<p>相比于上一章的操作系统,本章操作系统有两个最大的不同之处,一个是支持应用程序在用户态运行,且能完成应用程序发出的系统调用;另一个是能够一个接一个地自动运行不同的应用程序。所以,我们需要对操作系统和应用程序进行修改,也需要对应用程序的编译生成过程进行修改。</p>
<p>首先改进应用程序,让它能够在用户态执行,并能发出系统调用。这其实就是上一章中 <a class="reference internal" href="../chapter1/3-1-mini-rt-usrland.html#term-print-userminienv"><span class="std std-ref">构建用户态执行环境</span></a> 小节介绍内容的进一步改进。具体而言,编写多个应用小程序,修改编译应用所需的 <code class="docutils literal notranslate"><span class="pre">linker.ld</span></code> 文件来 <a class="reference internal" href="2application.html#term-app-mem-layout"><span class="std std-ref">调整程序的内存布局</span></a> ,让操作系统能够把应用加载到指定内存地址后顺利启动并运行应用程序。</p>
<p>应用程序运行中,操作系统要支持应用程序的输出功能,并还能支持应用程序退出。这需要完成 <code class="docutils literal notranslate"><span class="pre">sys_write</span></code><code class="docutils literal notranslate"><span class="pre">sys_exit</span></code> 系统调用访问请求的实现。 具体实现涉及到内联汇编的编写,以及应用与操作系统内核之间系统调用的参数传递的约定。为了让应用在还没实现操作系统之前就能进行运行测试,我们采用了Linux on RISC-V64 的系统调用参数约定。具体实现可参看 <a class="reference internal" href="2application.html#term-call-syscall"><span class="std std-ref">系统调用</span></a> 小节中的内容。 这样写完应用小例子后,就可以通过 <code class="docutils literal notranslate"><span class="pre">qemu-riscv64</span></code> 模拟器进行测试了。</p>
<p>写完应用程序后,还需实现支持多个应用程序轮流启动运行的操作系统。这里首先能把本来相对松散的应用程序执行代码和操作系统执行代码连接在一起,便于 <code class="docutils literal notranslate"><span class="pre">qemu-system=riscv64</span></code> 模拟器一次性地加载二者到内存中,并让操作系统能够找到应用程序的位置。为把二者连在一起,需要对生成的应用程序进行改造,首先是把应用程序执行文件从ELF执行文件格式变成Binary格式(通过 <code class="docutils literal notranslate"><span class="pre">rust-objcopy</span></code> 可以轻松完成);然后这些Binary格式的文件通过编译器辅助脚本 <code class="docutils literal notranslate"><span class="pre">os/build.rs</span></code> 转变变成 <code class="docutils literal notranslate"><span class="pre">os/src/link_app.S</span></code> 这个汇编文件的一部分,并生成各个Binary应用的辅助信息,便于操作系统能够找到应用的位置。编译器会把把操作系统的源码和 <code class="docutils literal notranslate"><span class="pre">os/src/link_app.S</span></code> 合在一起,编译出操作系统+Binary应用的ELF执行文件,并进一步转变成Binary格式。</p>
<p>写完应用程序后,还需实现支持多个应用程序轮流启动运行的操作系统。这里首先能把本来相对松散的应用程序执行代码和操作系统执行代码连接在一起,便于 <code class="docutils literal notranslate"><span class="pre">qemu-system-riscv64</span></code> 模拟器一次性地加载二者到内存中,并让操作系统能够找到应用程序的位置。为把二者连在一起,需要对生成的应用程序进行改造,首先是把应用程序执行文件从ELF执行文件格式变成Binary格式(通过 <code class="docutils literal notranslate"><span class="pre">rust-objcopy</span></code> 可以轻松完成);然后这些Binary格式的文件通过编译器辅助脚本 <code class="docutils literal notranslate"><span class="pre">os/build.rs</span></code> 转变变成 <code class="docutils literal notranslate"><span class="pre">os/src/link_app.S</span></code> 这个汇编文件的一部分,并生成各个Binary应用的辅助信息,便于操作系统能够找到应用的位置。编译器会把把操作系统的源码和 <code class="docutils literal notranslate"><span class="pre">os/src/link_app.S</span></code> 合在一起,编译出操作系统+Binary应用的ELF执行文件,并进一步转变成Binary格式。</p>
<p>操作系统本身需要完成对Binary应用的位置查找,找到后(通过 <code class="docutils literal notranslate"><span class="pre">os/src/link_app.S</span></code> 中的变量和标号信息完成),会把Binary应用拷贝到 <code class="docutils literal notranslate"><span class="pre">user/src/linker.ld</span></code> 指定的物理内存位置(OS的加载应用功能)。在一个应执行完毕后,还能加载另外一个应用,这主要是通过 <code class="docutils literal notranslate"><span class="pre">AppManagerInner</span></code> 数据结构和对应的函数 <code class="docutils literal notranslate"><span class="pre">load_app</span></code><code class="docutils literal notranslate"><span class="pre">run_next_app</span></code> 等来完成对应用的一系列管理功能。</p>
<p>这主要在 <a class="reference internal" href="3batch-system.html#term-batchos"><span class="std std-ref">实现批处理操作系统</span></a> 小节中讲解。</p>
<p>为了让Binary应用能够启动和运行,操作系统还需给Binary应用分配好执行环境所需一系列的资源。这主要包括设置好用户栈和内核栈(在应用在用户态和内核在内核态需要有各自的栈),实现Trap 上下文的保存与恢复(让应用能够在发出系统调用到内核态后,还能回到用户态继续执行),完成Trap 分发与处理等工作。由于涉及用户态与内核态之间的特权级切换细节的汇编代码,与硬件细节联系紧密,所以 <a class="reference internal" href="4trap-handling.html#term-trap-handle"><span class="std std-ref">这部分内容</span></a> 是本章中理解比较困难的地方。如果要了解清楚,需要对涉及到的CSR寄存器的功能有清楚的认识。这就需要看看 <span class="xref std std-ref">RISC-V手册</span> 的第十章或更加详细的RISC-V的特权级规范文档了。有了上面的实现后,就剩下最后一步,实现 <strong>执行应用程序</strong> 的操作系统功能,其主要实现在 <code class="docutils literal notranslate"><span class="pre">run_next_app</span></code> 函数中 。</p>
<p>为了让Binary应用能够启动和运行,操作系统还需给Binary应用分配好执行环境所需一系列的资源。这主要包括设置好用户栈和内核栈(在应用在用户态和内核在内核态需要有各自的栈),实现Trap 上下文的保存与恢复(让应用能够在发出系统调用到内核态后,还能回到用户态继续执行),完成Trap 分发与处理等工作。由于涉及用户态与内核态之间的特权级切换细节的汇编代码,与硬件细节联系紧密,所以 <a class="reference internal" href="4trap-handling.html#term-trap-handle"><span class="std std-ref">这部分内容</span></a> 是本章中理解比较困难的地方。如果要了解清楚,需要对涉及到的CSR寄存器的功能有清楚的认识。这就需要看看 <a class="reference external" href="http://crva.ict.ac.cn/documents/RISC-V-Reader-Chinese-v2p1.pdf">RISC-V手册</a> 的第十章或更加详细的RISC-V的特权级规范文档了。有了上面的实现后,就剩下最后一步,实现 <strong>执行应用程序</strong> 的操作系统功能,其主要实现在 <code class="docutils literal notranslate"><span class="pre">run_next_app</span></code> 函数中 。</p>
</div>
</div>
......
......@@ -548,10 +548,10 @@ Rust 7 Files 908 Lines
<div class="section" id="id5">
<h2>本章代码导读<a class="headerlink" href="#id5" title="永久链接至标题"></a></h2>
<p>本章涉及的代码量相对较多,且与进程执行相关的管理还有直接的关系。其实我们是参考经典的UNIX基于索引的文件系统,设计了一个简化的有一级目录并支持创建/打开/读写/关闭文件一系列操作的文件系统。这里简要介绍一下在内核中添加文件系统的大致开发过程。</p>
<p>第一步是能够写出与文件访问相关的应用。这里是参考并简化了Linux的创建/打开/读写/关闭文件的系统调用,在用户态设计并实现这些系统调用的接口。前面每章都或多或少地添加或改变各种系统调用,所以,在用户态实现面向文件的这几个系统调用接口是比较容易的</p>
<p>第二步就是要实现 easyfs 文件系统了。由于Rust语言的特点,我们可以在用户态实现 easyfs 文件系统,并在用户态完成文件系统功能的基本测试后,就可以不修改就嵌入到操作系统内核中。我们按照自底向上方的执行流程来介绍easyfs文件系统的具体实现。当然,有了文件系统的具体实现,还需要对上一章的操作系统内核进行扩展,实现与 easyfs 文件系统对接的接口,这样才可以让操作系统拥有一个简单可用的文件系统。带有文件系统的操作系统就可以提高应用开发体验和程序执行与互操作的灵活性,让应用获得文件系统带了的各种便利</p>
<p>easyfs文件系统的设计实现有五层。它的最底层就是对块设备的访问操作接口。为了实现easyfs文件系统,首先需要定义 <code class="docutils literal notranslate"><span class="pre">BlockDevice</span></code> trait,其成员函数定义 <code class="docutils literal notranslate"><span class="pre">read_block</span></code><code class="docutils literal notranslate"><span class="pre">write_block</span></code> 是操作系统内核中的块设备驱动需要实现的函数。这样就可以把内核中的块设备驱动与easyfs文件系统进行对接。完成对接后,easyfs文件系统可以通过这两个函数对块设备进行读写。</p>
<p>而具体使用这两个函数的是自底向上的第二层 – 块缓存。块缓存是把应用要访问的数据放到一块内存区域中,减少磁盘读写的次数,提高系统性能。块缓存通过 <code class="docutils literal notranslate"><span class="pre">read_block</span></code><code class="docutils literal notranslate"><span class="pre">write_block</span></code> 函数接口来读写磁盘数据。这些磁盘数据会缓存在内存中。表示块缓存的数据结构是 <code class="docutils literal notranslate"><span class="pre">BlockCache</span></code> 。当我们创建一个 <code class="docutils literal notranslate"><span class="pre">BlockCache</span></code> 的时候,将触发一次 <code class="docutils literal notranslate"><span class="pre">read_block</span></code> 函数调用,将一个块上的数据从磁盘读到块缓冲区中。由于缓存磁盘块的内存有限,我们需要实现缓存的替换,这就需要实现类似与页替换算法的缓存替换算法。为了简单,我们实现的是FIFO缓存替换算法。具体替换过程是块缓存全局管理器 <code class="docutils literal notranslate"><span class="pre">BlockCacheManager</span></code> 中的成员函数 <code class="docutils literal notranslate"><span class="pre">get_block_cache</span></code> 来完成的</p>
<p>第一步是能够写出与文件访问相关的应用。这里是参考了Linux的创建/打开/读写/关闭文件的系统调用接口,力图实现一个 <a class="reference internal" href="1fs-interface.html#fs-simplification"><span class="std std-ref">简化版的文件系统模型</span></a> 。在用户态我们只需要遵从相关系统调用的接口约定,在用户库里完成对应的封装即可。这一过程我们在前面的章节中已经重复过多次,读者应当对其比较熟悉。其中最为关键的是系统调用可以参考 <a class="reference internal" href="1fs-interface.html#sys-open"><span class="std std-ref">sys_open 语义介绍</span></a> ,此外我们还给出了 <a class="reference internal" href="1fs-interface.html#filetest-simple"><span class="std std-ref">测例代码解读</span></a> </p>
<p>第二步就是要实现 easyfs 文件系统了。由于 Rust 语言的特点,我们可以在用户态实现 easyfs 文件系统,并在用户态完成文件系统功能的基本测试并基本验证其实现正确性之后,就可以放心的将该模块嵌入到操作系统内核中。当然,有了文件系统的具体实现,还需要对上一章的操作系统内核进行扩展,实现与 easyfs 文件系统对接的接口,这样才可以让操作系统拥有一个简单可用的文件系统。从而,内核可以支持允许文件读写功能的更复杂的应用,在命令行参数机制的加持下,可以进一步提升整个系统的灵活性,让应用的开发和调试变得更为轻松</p>
<p>easyfs 文件系统的整体架构自下而上可分为五层。它的最底层就是对块设备的访问操作接口。在 <code class="docutils literal notranslate"><span class="pre">easy-fs/src/block_dev.rs</span></code> 中,可以看到 <code class="docutils literal notranslate"><span class="pre">BlockDevice</span></code> trait 代表了一个抽象块设备,该 trait 仅需求两个函数 <code class="docutils literal notranslate"><span class="pre">read_block</span></code><code class="docutils literal notranslate"><span class="pre">write_block</span></code> ,分别代表将数据从块设备读到内存中的缓冲区中,或者将数据从内存中的缓冲区写回到块设备中,数据需要以块为单位进行读写。easy-fs 库的使用者需要负责为它们看到的实际的块设备具体实现 <code class="docutils literal notranslate"><span class="pre">BlockDevice</span></code> trait 并提供给 easy-fs 库的上层,这样的话 easy-fs 库的最底层就与一个具体的执行环境对接起来了。至于为什么块设备层位于 easy-fs 的最底层,是因为文件系统仅仅是在块设备上存储的结构稍微复杂一点的数据,但无论它的操作变换如何复杂,从块设备的角度终究可以被分解成若干次块读写。</p>
<p>尽管在最底层我们就已经有了块读写的能力,但从编程方便性和性能的角度,仅有块读写这么基础的底层接口是不足以实现如此复杂的文件系统的,虽然它已经被我们大幅简化过了。比如,将一个块的内容读到内存的缓冲区,对缓冲区进行修改,并尚未写回的时候,如果由于编程上的不小心再次将该块的内容读到另一个缓冲区,而不是使用已有的缓冲区,这将会造成不一致问题。此外还有可能增加很多不必要的块读写次数,大幅降低文件系统的性能。因此,通过程序自动而非程序员手动对块的缓冲区进行统一管理也就势在必行了,该机制被我们抽象为 easy-fs 自底向上的第二层,即块缓存层。在 <code class="docutils literal notranslate"><span class="pre">easy-fs/src/block_cache.rs</span></code> 中, <code class="docutils literal notranslate"><span class="pre">BlockCache</span></code> 代表一个被我们管理起来的块的缓冲区,它带有缓冲区本体以及块的编号等信息。当它被创建的时候,将触发一次 <code class="docutils literal notranslate"><span class="pre">read_block</span></code> 将数据从块设备读到它的缓冲区中。接下来只要它驻留在内存中,便可保证对于同一个块的所有操作都会直接在它的缓冲区中进行而无需额外的 <code class="docutils literal notranslate"><span class="pre">read_block</span></code> 。块缓存管理器 <code class="docutils literal notranslate"><span class="pre">BlockManager</span></code> 在内存中管理有限个 <code class="docutils literal notranslate"><span class="pre">BlockCache</span></code> 并实现了类似 FIFO 的缓存替换算法,当一个块缓存被换出的时候视情况可能调用 <code class="docutils literal notranslate"><span class="pre">write_block</span></code> 将缓冲区数据写回块设备。总之,块缓存层对上提供 <code class="docutils literal notranslate"><span class="pre">get_block_cache</span></code> 接口来屏蔽掉相关细节,从而可以透明的读写一个块</p>
<p>有了块缓存,我们就可以在内存中方便地处理easyfs文件系统在磁盘上的各种数据了,这就是第三层文件系统的磁盘数据结构。easyfs文件系统中的所有需要持久保存的数据都会放到磁盘上,这包括了管理这个文件系统的 <strong>超级块 (Super Block)</strong>,管理空闲磁盘块的 <strong>索引节点位图区</strong><strong>数据块位图区</strong> ,以及管理文件的 <strong>索引节点区</strong> 和 放置文件数据的 <strong>数据块区</strong> 组成。</p>
<p>easyfs文件系统中管理这些磁盘数据的控制逻辑主要集中在 <strong>磁盘块管理器</strong> 中,这是文件系统的第四层。对于文件系统管理而言,其核心是 <code class="docutils literal notranslate"><span class="pre">EasyFileSystem</span></code> 数据结构及其关键成员函数:</p>
<blockquote>
......
......@@ -141,7 +141,7 @@ commentsRunWhenDOMLoaded(addUtterances);
</li>
<li class="toctree-l3"><a class="reference internal" href="#fs-simplification">简易文件与目录抽象</a></li>
<li class="toctree-l3"><a class="reference internal" href="#id8">打开与读写文件的系统调用</a><ul>
<li class="toctree-l4"><a class="reference internal" href="#id9">文件打开</a></li>
<li class="toctree-l4"><a class="reference internal" href="#sys-open">文件打开</a></li>
<li class="toctree-l4"><a class="reference internal" href="#id10">文件的顺序读写</a></li>
</ul>
</li>
......@@ -348,8 +348,8 @@ commentsRunWhenDOMLoaded(addUtterances);
</div>
<div class="section" id="id8">
<h2>打开与读写文件的系统调用<a class="headerlink" href="#id8" title="永久链接至标题"></a></h2>
<div class="section" id="id9">
<h3>文件打开<a class="headerlink" href="#id9" title="永久链接至标题"></a></h3>
<div class="section" id="sys-open">
<span id="id9"></span><h3>文件打开<a class="headerlink" href="#sys-open" title="永久链接至标题"></a></h3>
<p>在读写一个常规文件之前,应用首先需要通过内核提供的 <code class="docutils literal notranslate"><span class="pre">sys_open</span></code> 系统调用让该文件在进程的文件描述符表中占一项,并得到操作系统的返回值–文件描述符,即文件关联的表项在文件描述表中的索引值:</p>
<div class="highlight-rust notranslate"><div class="highlight"><pre><span></span><span class="sd">/// 功能:打开一个常规文件,并返回可以访问它的文件描述符。</span>
<span class="sd">/// 参数:path 描述要打开的文件的文件名(简单起见,文件系统不需要支持目录,所有的文件都放在根目录 / 下),</span>
......@@ -402,7 +402,7 @@ commentsRunWhenDOMLoaded(addUtterances);
<h3>文件的顺序读写<a class="headerlink" href="#id10" title="永久链接至标题"></a></h3>
<p>在打开一个文件之后,我们就可以用之前的 <code class="docutils literal notranslate"><span class="pre">sys_read/sys_write</span></code> 两个系统调用来对它进行读写了。需要注意的是,常规文件的读写模式和之前介绍过的几种文件有所不同。标准输入输出和匿名管道都属于一种流式读写,而常规文件则是顺序读写和随机读写的结合。由于常规文件可以看成一段字节序列,我们应该能够随意读写它的任一段区间的数据,即随机读写。然而用户仅仅通过 <code class="docutils literal notranslate"><span class="pre">sys_read/sys_write</span></code> 两个系统调用不能做到这一点。</p>
<p>事实上,进程为每个它打开的常规文件维护了一个偏移量,在刚打开时初始值一般为 0 字节。当 <code class="docutils literal notranslate"><span class="pre">sys_read/sys_write</span></code> 的时候,将会从文件字节序列偏移量的位置开始 <strong>顺序</strong> 把数据读到应用缓冲区/从应用缓冲区写入数据。操作完成之后,偏移量向后移动读取/写入的实际字节数。这意味着,下次 <code class="docutils literal notranslate"><span class="pre">sys_read/sys_write</span></code> 将会从刚刚读取/写入之后的位置继续。如果仅使用 <code class="docutils literal notranslate"><span class="pre">sys_read/sys_write</span></code> 的话,则只能从头到尾顺序对文件进行读写。当我们需要从头开始重新写入或读取的话,只能通过 <code class="docutils literal notranslate"><span class="pre">sys_close</span></code> 关闭并重新打开文件来将偏移量重置为 0。为了解决这种问题,有另一个系统调用 <code class="docutils literal notranslate"><span class="pre">sys_lseek</span></code> 可以调整进程打开的一个常规文件的偏移量,这样便能对文件进行随机读写。在本教程中并未实现这个系统调用,因为顺序文件读写就已经足够了。顺带一提,在文件系统的底层实现中都是对文件进行随机读写的。</p>
<p>下面我们从本章的测试用例 <code class="docutils literal notranslate"><span class="pre">filetest_simple</span></code> 来介绍文件系统接口的使用方法:</p>
<p id="filetest-simple">下面我们从本章的测试用例 <code class="docutils literal notranslate"><span class="pre">filetest_simple</span></code> 来介绍文件系统接口的使用方法:</p>
<div class="highlight-rust notranslate"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span class="normal"> 1</span>
<span class="normal"> 2</span>
<span class="normal"> 3</span>
......
......@@ -267,7 +267,7 @@ commentsRunWhenDOMLoaded(addUtterances);
<h1>简易文件系统 easy-fs<a class="headerlink" href="#easy-fs" title="永久链接至标题"></a></h1>
<div class="section" id="id1">
<h2>本节导读<a class="headerlink" href="#id1" title="永久链接至标题"></a></h2>
<p>本节我们介绍一个简易文件系统实现 easy-fs。作为一个文件系统而言,它的磁盘布局(为了叙述方便,我们用磁盘来指代一系列持久存储设备)体现在磁盘上各扇区的内容上,而它解析磁盘布局得到的逻辑目录树结构则是通过内存上的数据结构来访问的,这意味着它要同时涉及到对磁盘和对内存的访问。它们的访问方式是不同的,对于内存直接通过一条指令即可直接读写内存相应的位置,而磁盘的话需要用软件的方式向磁盘发出请求来间接进行读写。此外,我们也要特别注意哪些数据结构是存储在磁盘上,哪些数据结构是存储在内存中的,这样在实现的时候才不会引起混乱。</p>
<p>本节我们介绍一个简易文件系统实现 easy-fs。作为一个文件系统而言,它的磁盘布局(为了叙述方便,我们用磁盘来指代一系列持久存储设备)体现在磁盘上各扇区的内容上,而它解析磁盘布局得到的逻辑目录树结构则是通过内存上的数据结构来访问的,这意味着它要同时涉及到对磁盘和对内存的访问。它们的访问方式是不同的,对于内存直接通过一条指令即可直接读写内存相应的位置,而磁盘的话需要用软件的方式向磁盘发出请求来间接进行读写。因此,我们也要特别注意哪些数据结构是存储在磁盘上,哪些数据结构是存储在内存中的,这样在实现的时候才不会引起混乱。</p>
</div>
<div class="section" id="id2">
<h2>松耦合模块化设计思路<a class="headerlink" href="#id2" title="永久链接至标题"></a></h2>
......
......@@ -244,7 +244,7 @@ commentsRunWhenDOMLoaded(addUtterances);
</li>
<li class="toctree-l2"><a class="reference internal" href="1fs-interface.html#fs-simplification">简易文件与目录抽象</a></li>
<li class="toctree-l2"><a class="reference internal" href="1fs-interface.html#id8">打开与读写文件的系统调用</a><ul>
<li class="toctree-l3"><a class="reference internal" href="1fs-interface.html#id9">文件打开</a></li>
<li class="toctree-l3"><a class="reference internal" href="1fs-interface.html#sys-open">文件打开</a></li>
<li class="toctree-l3"><a class="reference internal" href="1fs-interface.html#id10">文件的顺序读写</a></li>
</ul>
</li>
......
无法预览此类型文件
因为 它太大了无法显示 source diff 。你可以改为 查看blob
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册