diff --git a/.gitignore b/.gitignore index 6f31401f787928dc3bb3e2622578f889336d1d92..fa5724cfbaf60a67c63f923784152e2e20d3c693 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ build/ .vscode/ +.idea +source/_build/ diff --git a/Makefile b/Makefile index 2950f04450d8c72e39c4a2aa75270159ac7d66bb..3ca7d838b27abd62d5b6741a84ab0f573a30651a 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,15 @@ help: %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +view: + make html && firefox build/html/index.html + deploy: + @make clean + @make html @rm -rf docs @cp -r build/html docs @touch docs/.nojekyll + @git add -A + @git commit -m "Deploy" + @git push diff --git a/README.md b/README.md index 4651b644c4a368030dd56205f7957288fd9a6d9f..3648cab8ddf2720926aec81273d2e5cf5a2526c0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,12 @@ # rCore-Tutorial-Book-v3 Documentation of rCore-Tutorial version 3 in Chinese. + +Deployed version can be found [here](https://rcore-os.github.io/rCore-Tutorial-Book-v3/). + +If you cannot access `github.io` normally due to network problems, please visit the [synchronized version](http://wyfcyx.gitee.io/rcore-tutorial-book-v3) hosted on gitee. + +## Todo List + +- [x] code tree in introduction +- [ ] rust module system in chapter1 +- [x] update rustsbi to 0.1.1 diff --git a/all.sh b/all.sh new file mode 100755 index 0000000000000000000000000000000000000000..cf20d3be4a7c78d938106c19775640d589b40172 --- /dev/null +++ b/all.sh @@ -0,0 +1,2 @@ +make clean && make html && google-chrome build/html/index.html + diff --git a/docs/.buildinfo b/docs/.buildinfo deleted file mode 100644 index b9ff3771e4c3f5af45eb8c4aa3c99fdb35f45e41..0000000000000000000000000000000000000000 --- a/docs/.buildinfo +++ /dev/null @@ -1,4 +0,0 @@ -# Sphinx build info version 1 -# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 6e307cd3ba0daae86ac8cbb85a0d88aa -tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/.nojekyll b/docs/.nojekyll deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/docs/_images/CallStack.png b/docs/_images/CallStack.png deleted file mode 100644 index 8d606136311f5b53632374920786d6ecda17857e..0000000000000000000000000000000000000000 Binary files a/docs/_images/CallStack.png and /dev/null differ diff --git a/docs/_images/EnvironmentCallFlow.png b/docs/_images/EnvironmentCallFlow.png deleted file mode 100644 index 5897388521440946ddc55b2f2243be0d8afbe7f8..0000000000000000000000000000000000000000 Binary files a/docs/_images/EnvironmentCallFlow.png and /dev/null differ diff --git a/docs/_images/MemoryLayout.png b/docs/_images/MemoryLayout.png deleted file mode 100644 index b06ad5e045f7e959bf701acf836514198a132193..0000000000000000000000000000000000000000 Binary files a/docs/_images/MemoryLayout.png and /dev/null differ diff --git a/docs/_images/PrivilegeStack.png b/docs/_images/PrivilegeStack.png deleted file mode 100644 index be5f5e13a50390a5c6fd7b61fc22b5291577aadb..0000000000000000000000000000000000000000 Binary files a/docs/_images/PrivilegeStack.png and /dev/null differ diff --git a/docs/_images/StackFrame.png b/docs/_images/StackFrame.png deleted file mode 100644 index 524e3970803a688311817a4c6d4e5a779909528a..0000000000000000000000000000000000000000 Binary files a/docs/_images/StackFrame.png and /dev/null differ diff --git a/docs/_images/app-software-stack.png b/docs/_images/app-software-stack.png deleted file mode 100644 index 021f145b53b6a82f9a9075747502e7b4aa819c56..0000000000000000000000000000000000000000 Binary files a/docs/_images/app-software-stack.png and /dev/null differ diff --git a/docs/_sources/appendix-a/index.rst.txt b/docs/_sources/appendix-a/index.rst.txt deleted file mode 100644 index dd96e923645afc717e9cb5fd807da0f3dcf9bda5..0000000000000000000000000000000000000000 --- a/docs/_sources/appendix-a/index.rst.txt +++ /dev/null @@ -1,10 +0,0 @@ -Rust 快速入门 -============================= - -.. toctree:: - :hidden: - :maxdepth: 4 - -- `Stanford 新开的一门很值得学习的 Rust 入门课程 `_ -- `一份简单的 Rust 入门介绍 `_ -- `《RustOS Guide》中的 Rust 介绍部分 `_ \ No newline at end of file diff --git a/docs/_sources/appendix-b/index.rst.txt b/docs/_sources/appendix-b/index.rst.txt deleted file mode 100644 index 59335fddbea8eded73dceb44d3ab3297de4f9b45..0000000000000000000000000000000000000000 --- a/docs/_sources/appendix-b/index.rst.txt +++ /dev/null @@ -1,7 +0,0 @@ -常见工具的使用方法 -======================================== - -.. toctree:: - :hidden: - :maxdepth: 4 - \ No newline at end of file diff --git a/docs/_sources/chapter1/1app-ee-platform.rst.txt b/docs/_sources/chapter1/1app-ee-platform.rst.txt deleted file mode 100644 index 44b7cea8655c19605383ca7ec91797d5534f43a0..0000000000000000000000000000000000000000 --- a/docs/_sources/chapter1/1app-ee-platform.rst.txt +++ /dev/null @@ -1,209 +0,0 @@ -应用程序运行环境与平台支持 -================================================ - -.. toctree:: - :hidden: - :maxdepth: 5 - -作为一切的开始,让我们使用 Cargo 工具来创建一个 Rust 项目。它看上去没有任何特别之处: - -.. code-block:: console - - $ cargo new os --bin - -我们加上了 ``--bin`` 选项来告诉 Cargo 我们创建一个可执行项目而不是库项目。此时,项目的文件结构如下: - -.. code-block:: console - - $ tree os - os - ├── Cargo.toml - └── src - └── main.rs - - 1 directory, 2 files - -其中 ``Cargo.toml`` 中保存着项目的配置,包括作者的信息、联系方式以及库依赖等等。显而易见源代码保存在 ``src`` 目录下,目前为止只有 ``main.rs`` -一个文件,让我们看一下里面的内容: - -.. code-block:: rust - :linenos: - :caption: 最简单的 Rust 应用 - - fn main() { - println!("Hello, world!"); - } - -进入 os 项目根目录下,利用 Cargo 工具即可一条命令实现构建并运行项目: - -.. code-block:: console - - $ cargo run - Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os) - Finished dev [unoptimized + debuginfo] target(s) in 1.15s - Running `target/debug/os` - Hello, world! - -如我们预想的一样,我们在屏幕上看到了一行 ``Hello, world!`` 。但是,需要注意到我们所享受到的编程的方便并不是理所当然的,背后有着从硬件 -到软件的多种机制的支持。 - -应用程序运行环境 -------------------------------- - -如下图所示,应用程序的运行需要下面一套运行环境栈的支持: - -.. _app-software-stack: - -.. figure:: app-software-stack.png - :align: center - - 应用程序运行环境栈:图中的白色块自上而下(越往下则越靠近底层,下层作为上层的执行环境支持上层代码的运行)表示各级运行环境, - 黑色块则表示相邻两层运行环境之间的接口。 - -我们的应用位于最上层,它可以通过调用编程语言提供的标准库或者其他三方库对外提供的功能强大的函数接口,使得仅需少量的源代码就能完成复杂的 -功能。但是这些库的功能不仅限于此,事实上它们属于应用程序的 **执行环境** (Execution Environment),在我们通常不会注意到的地方,它 -们还会在执行应用之前完成一些初始化工作,并在应用程序执行的时候对它进行监控。我们在打印 ``Hello, world!`` 时使用的 ``println!`` -宏正是由 Rust 标准库 std 提供的。 - -从内核/操作系统的角度看来,它上面的一切都属于用户态,而它自身属于内核态。无论用户态应用如何编写,是手写汇编代码,还是基于某种编程语言利用 -其标准库或三方库,某些功能总要直接或间接的通过内核/操作系统提供的 **系统调用** (System Call) 来实现。因此系统调用充当了用户和内核之间 -的边界。内核作为用户态的运行环境,它不仅要提供系统调用接口,还需要对用户态应用的执行进行监控和管理。 - -.. note:: - - **Hello, world! 用到了哪些系统调用?** - - 从之前的 ``cargo run`` 的输出可以看出之前构建的可执行文件是在 target/debug 目录下的 os 。 - 在 Ubuntu 系统上,可以通过 ``strace`` 工具来运行一个程序并输出程序运行过程当中向内核请求的所有的系统调用及其返回值。 - 我们只需输入 ``strace target/debug/os`` 即可看到一长串的系统调用。 - - 其中,真正容易看出与 ``Hello, world!`` 相关的只有一个系统调用: - - .. code-block:: - - write(1, "Hello, world!\n", 14) = 14 - - 其参数的具体含义我们暂且不在这里进行解释。 - - 其余的系统调用基本上分别用于函数库和内核两层执行环境的初始化工作和对于上层的运行期监控和管理。之后,随着应用场景的复杂化,我们 - 需要更强的抽象能力,也会实现这里面的一些系统调用。 - -从硬件的角度来看,它上面的一切都属于软件。硬件可以分为三种: 处理器 (Processor) ——它更常见的名字是中央处理单元 (CPU, Central Processing Unit), -内存 (Memory) 还有 I/O 设备。其中处理器无疑是其中最复杂同时也最关键的一个。它与软件约定一套 **指令集体系结构** (ISA, Instruction Set Architecture), -使得软件可以通过 ISA 中提供的汇编指令来访问各种硬件资源。软件当然也需要知道处理器会如何执行这些指令:最简单的话就是一条一条执行位于内存 -中的指令。当然,实际的情况远比这个要复杂得多,为了适应现代应用程序的场景,处理器还需要提供很多额外的机制,而不仅仅是让数据在 CPU 寄存器、内存和 I/O 设备 -三者之间流动。 - -.. note:: - - **多层执行环境都是必需的吗?** - - 除了最上层的应用程序和最下层的硬件平台必须存在之外,作为中间层的函数库和内核并不是必须存在的:它们都是对下层资源进行了 **抽象** (Abstraction), - 并为上层提供了一套运行环境。抽象的优点在于它让上层以较小的代价获得所需的功能,并同时可以提供一些保护。但抽象同时也是一种限制,会丧失一些 - 应有的灵活性。比如,当你在考虑在项目中应该使用哪个函数库的时候,就常常需要这方面的权衡:过多的抽象和过少的抽象自然都是不合适的。 - - 实际上,我们通过应用程序的特征来判断它需要什么程度的抽象。 - - - 如果函数库和内核都不存在,那么我们就是在手写汇编代码,这种方式具有最高的灵活性,抽象能力则最低,基本等同于硬件。我们通常用这种方式来 - 实现一些架构相关且仅通过编程语言无法描述的小模块或者代码片段。 - - 如果仅存在函数库而不存在内核,意味着我们不需要内核提供的抽象。在嵌入式场景就常常会出现这种情况。嵌入式设备虽然也包含 CPU、内存和 I/O - 设备,但是它上面通常只会同时运行一个或几个功能非常简单的小应用程序,其定位就是那种功能单一的场景,比如人脸识别打卡系统等。我们常用的 - 操作系统如 Windows/Linux/macOS 等的抽象都支持同时运行很多应用程序,在嵌入式场景是过抽象的。因此,常见的解决方案是仅使用函数库构建 - 单独的应用程序或是用专为应用场景特别裁减过的轻量级内核管理少数应用程序。 - -平台与目标三元组 ---------------------------------------- - -对于一份用某种编程语言实现的源代码而言,编译器在将其通过编译、链接得到目标文件的时候需要知道程序要在哪个 **平台** (Platform) 上运行。 -从上面给出的 :ref:`应用程序运行环境栈 ` 可以看出: - -- 如果用户态基于的内核不同,会导致系统调用接口不同或者语义不一致; -- 如果底层硬件不同,对于硬件资源的访问方式会有差异。特别是 ISA 不同的话,对上提供的指令集和寄存器都不同。 - -它们都会导致最终生成的目标文件有很大不同。需要指出的是,某些编译器支持同一份源代码无需修改就可编译到多个不同的目标平台并在上面运行。这种 -情况下,源代码是 **跨平台** 的。而另一些编译器则已经预设好了一个固定的目标平台。 - -我们可以通过 **目标三元组** (Target Triplet) 来描述一个目标平台。它一般包括 CPU 架构、CPU 厂商和操作系统,它们确实都会控制目标文件的生成。 -比如,我们可以尝试看一下之前的 ``Hello, world!`` 的目标平台是什么。这可以通过打印编译器 rustc 的默认配置信息: - -.. code-block:: console - - $ rustc --version --verbose - rustc 1.48.0-nightly (73dc675b9 2020-09-06) - binary: rustc - commit-hash: 73dc675b9437c2a51a975a9f58cc66f05463c351 - commit-date: 2020-09-06 - host: x86_64-unknown-linux-gnu - release: 1.48.0-nightly - LLVM version: 11.0 - -从其中的 host 一项可以看出默认的目标平台是 ``x86_64-unknown-linux-gnu``,其中 CPU 架构是 x86_64,CPU 厂商是 unknown,操作系统是 linux-gnu。 -这种无论编译器还是其目标文件都在我们当前所处的平台运行是一种最简单也最普遍的情况。但是很快我们就将遇到另外一种情况。 - -讲了这么多,终于该介绍我们的主线任务了。我们希望能够在另一个平台上运行 ``Hello, world!``,而与之前的默认平台不同的地方在于,我们将 CPU 架构从 -x86_64 换成 RISC-V。 - -.. note:: - - 为何基于 RISC-V 架构而非 x86 系列架构? - - x86 架构为了在升级换代的同时保持对基于旧版架构应用程序/内核的兼容性,存在大量的历史包袱,也就是一些对于目前的应用场景没有任何意义,但又必须 - 花大量时间正确设置才能正常使用 CPU 的奇怪设定。为了建立并维护架构的应用生态,这确实是必不可少的,但站在教学的角度几乎完全是在浪费时间。而 - 新生的 RISC-V 架构十分简洁,架构文档需要阅读的核心部分不足百页,且这些功能已经足以用来构造一个具有相当抽象能力的内核了。 - -可以看一下目前 Rust 编译器支持哪些基于 RISC-V 的平台: - -.. code-block:: console - - $ rustc --print target-list | grep riscv - riscv32i-unknown-none-elf - riscv32imac-unknown-none-elf - riscv32imc-unknown-none-elf - riscv64gc-unknown-linux-gnu - riscv64gc-unknown-none-elf - riscv64imac-unknown-none-elf - -这里我们选择的是 ``riscv64gc-unknown-none-elf``,目标三元组中的操作系统是 none-elf,表明没有任何系统调用支持。这里我们之所以不选择有 -linux-gnu 系统调用支持的版本 ``riscv64gc-unknown-linux-gnu``,是因为我们只是想跑一个 ``Hello, world!``,没有必要使用操作系统所提供的 -那么高级的抽象。而且我们很清楚后续我们要开发的是一个内核,如果仅仅基于已有操作系统提供的系统调用的话,它自身的抽象能力会受到很大限制。所以它必须 -直面底层硬件来解锁更大的抽象能力上限。 - -.. note:: - - **RISC-V 指令集拓展** - - 由于基于 RISC-V 架构的处理器可能用于嵌入式场景或是通用计算场景,因此指令集规范将指令集划分为最基本的 RV32/64I 以及若干标准指令集拓展。 - 每款处理器只需按照其实际应用场景按需实现指令集拓展即可。 - - - RV32/64I:每款处理器都必须实现的基本整数指令集。在 RV32I 中,每个通用寄存器的位宽为 32 位;在 RV64I 中则为 64 位。它可以用来模拟 - 绝大多数标准指令集拓展中的指令,除了比较特殊的 A 拓展,因为它需要特别的硬件支持。 - - M 拓展:提供整数乘除法相关指令。 - - A 拓展:提供原子指令和一些相关的内存同步机制,这个后面会展开。 - - F/D 拓展:提供单/双精度浮点数运算支持。 - - C 拓展:提供压缩指令拓展。 - - G 拓展是基本整数指令集 I 再加上标准指令集拓展 MAFD 的总称,因此 riscv64gc 也就等同于 riscv64imafdc。我们剩下的内容都基于该处理器 - 架构完成。除此之外 RISC-V 架构还有很多标准指令集拓展,有一些还在持续更新中尚未稳定,有兴趣的读者可以浏览最新版的 RISC-V 指令集规范。 - -Rust 标准库与核心库 ----------------------------------- - -我们尝试一下将当前的 ``Hello, world!`` 程序的目标平台换成 riscv64gc-unknown-none-elf 看看会发生什么事情: - -.. code-block:: console - - $ cargo run --target riscv64gc-unknown-none-elf - Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os) - error[E0463]: can't find crate for `std` - | - = note: the `riscv64gc-unknown-none-elf` target may not be installed - -在之前的环境配置中,我们已经在 rustup 工具链中安装了这个目标平台支持,因此并不是该目标平台未安装的问题。因此只是单纯的在这个目标平台上找不到 -Rust 标准库 std。我们之前曾经提到过,编程语言的标准库或三方库的某些功能会直接或间接的用到操作系统提供的系统调用。但目前我们所选的目标平台不存在 -任何操作系统支持,于是 Rust 并没有为这个目标平台支持完整的标准库 std。类似这样的平台通常被我们称为 **裸机平台** (bare-metal)。 - -幸运的是,Rust 有一个对 std 裁剪过后的核心库 core,这个库是不需要任何操作系统支持的,相对的它的功能也比较受限,但是也包含了 Rust 语言 -相当一部分的核心机制,可以满足我们的大部分需求。在 Rust 语言生态中,有很多三方库也不依赖标准库 std 而仅仅依赖核心库 core,它们也可以很大 -程度上减轻我们的编程负担。它们是我们能够在裸机平台挣扎求生的最主要倚仗。 - -于是,我们知道在裸机平台上我们要将对于标准库 std 的引用换成核心库 core。但是做起来其实并没有那么容易。 \ No newline at end of file diff --git a/docs/_sources/chapter1/2remove-std.rst.txt b/docs/_sources/chapter1/2remove-std.rst.txt deleted file mode 100644 index f9cf314adf2bb5d2b309b43fe9e93e5bd954c54c..0000000000000000000000000000000000000000 --- a/docs/_sources/chapter1/2remove-std.rst.txt +++ /dev/null @@ -1,137 +0,0 @@ -移除标准库依赖 -========================== - -.. toctree:: - :hidden: - :maxdepth: 5 - -本节我们尝试移除之前的 ``Hello world!`` 程序对于标准库的依赖,使得它能够编译到裸机平台 RV64GC 上。 - -我们首先在 ``os`` 目录下新建 ``.cargo`` 目录,并在这个目录下创建 ``config`` 文件,并在里面输入如下内容: - -.. code-block:: - - // os/.cargo/config - [build] - target = "riscv64gc-unknown-none-elf" - -这会对于 Cargo 工具在 os 目录下的行为进行调整:现在默认会使用 riscv64gc 作为目标平台而不是原先的默认 x86_64-unknown-linux-gnu。 -事实上,这是一种编译器运行所在的平台与编译器生成可执行文件的目标平台不同(分别是后者和前者)的情况。这是一种 **交叉编译** (Cross Compile)。 - -当然,这只是使得我们之后在 ``cargo build`` 的时候不必再加上 ``--target`` 参数的一个小 trick。如果我们现在 ``cargo build`` ,还是会和 -上一小节一样出现找不到标准库 std 的错误。于是我们开始着手移除标准库。当然,这会产生一些副作用。 - -移除 println! 宏 ----------------------------------- - -我们在 ``main.rs`` 的开头加上一行 ``#![no_std]`` 来告诉 Rust 编译器不使用 Rust 标准库 std 转而使用核心库 core。编译器报出如下错误: - -.. error:: - - .. code-block:: console - - $ cargo build - Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os) - error: cannot find macro `println` in this scope - --> src/main.rs:4:5 - | - 4 | println!("Hello, world!"); - | ^^^^^^^ - -我们之前提到过, println! 宏是由标准库 std 提供的,且会使用到一个名为 write 的系统调用。现在我们的条件还不足以自己实现一个 println! 宏,由于 -使用了系统调用也不能在核心库 core 中找到它。我们目前先通过将它注释掉来绕过它。 - -提供语义项 panic_handler ----------------------------------------------------- - -.. error:: - - .. code-block:: console - - $ cargo build - Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os) - error: `#[panic_handler]` function required, but not found - -在使用 Rust 编写应用程序的时候,我们常常在遇到了一些无法恢复的致命错误导致程序无法继续向下运行的时候手动或自动调用 panic! 宏来并打印出错的 -位置让我们能够意识到它的存在,并进行一些后续处理。panic! 宏最典型的应用场景包括断言宏 assert! 失败或者对 ``Option::None/Result::Err`` -进行 ``unwrap`` 操作。 - -在标准库 std 中提供了 panic 的处理函数 ``#[panic_handler]``,其大致功能是打印出错位置和原因并杀死当前应用。可惜的是在核心库 core 中并没有提供, -因此我们需要自己实现 panic 处理函数。 - -.. note:: - - **Rust 语义项 lang_items** - - Rust 编译器内部的某些功能的实现并不是硬编码在语言内部的,而是以一种可插入的形式在库中提供。库只需要通过某种方式告诉编译器它的某个方法实现了 - 编译器内部的哪些功能,编译器就会采用库提供的方法来实现它内部对应的功能。通常只需要在库的方法前面加上一个标记即可。 - -我们开一个新的子模块 ``lang_items.rs`` 保存这些语义项,在里面提供 panic 处理函数的实现并通过标记通知编译器采用我们的实现: - -.. code-block:: rust - - // os/src/lang_items.rs - use core::panic::PanicInfo; - - #[panic_handler] - fn panic(_info: &PanicInfo) -> ! { - loop {} - } - -注意,panic 处理函数的函数签名需要一个 ``PanicInfo`` 的不可变借用作为输入参数,它在核心库中得以保留,这也是我们第一次与核心库打交道。之后我们 -会从 ``PanicInfo`` 解析出错位置并打印出来,然后杀死应用程序。但目前我们什么都不做只是在原地 loop。 - -移除 main 函数 ------------------------------ - -.. error:: - - .. code-block:: - - $ cargo build - Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os) - error: requires `start` lang_item - -编译器提醒我们缺少一个名为 ``start`` 的语义项。我们回忆一下,之前提到语言标准库和三方库作为应用程序的执行环境,需要负责在执行应用程序之前进行 -一些初始化工作,然后才跳转到应用程序的入口点(也就是跳转到我们编写的 ``main`` 函数)开始执行。事实上 ``start`` 语义项正代表着标准库 std 在 -执行应用程序之前需要进行的一些初始化工作。由于我们禁用了标准库,编译器也就找不到这项功能的实现了。 - -最简单的解决方案就是压根不让编译器使用这项功能。我们在 ``main.rs`` 的开头加入设置 ``#![no_main]`` 告诉编译器我们没有一般意义上的 ``main`` 函数, -并将原来的 ``main`` 函数删除。在失去了 ``main`` 函数的情况下,编译器也就不需要完成所谓的初始化工作了。 - -至此,我们成功移除了标准库的依赖并完成裸机平台上的构建。 - -.. code-block:: console - - $ cargo build - Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os) - Finished dev [unoptimized + debuginfo] target(s) in 0.06s - -目前的代码如下: - -.. code-block:: rust - - // os/src/main.rs - #![no_std] - #![no_main] - - mod lang_items; - - // os/src/lang_items.rs - use core::panic::PanicInfo; - - #[panic_handler] - fn panic(_info: &PanicInfo) -> ! { - loop {} - } - -本小节我们固然脱离了标准库,通过了编译器的检验,但也是伤筋动骨,将原有的很多功能弱化甚至直接删除,看起来距离在 RV64GC 平台上打印 -``Hello world!`` 相去甚远了(我们甚至连 println! 和 ``main`` 函数都删除了)。不要着急,接下来我们会以自己的方式来重塑这些 -功能,并最终完成我们的目标。 - -.. note:: - - **在 x86_64 平台上移除标准库依赖** - - 有兴趣的同学可以将目标平台换回之前默认的 ``x86_64-unknown-linux-gnu`` 并重复本小节所做的事情,比较两个平台从 ISA 到操作系统 - 的差异。可以参考 `BlogOS 的相关内容 `_ 。 \ No newline at end of file diff --git a/docs/_sources/chapter1/3minimal-rt.rst.txt b/docs/_sources/chapter1/3minimal-rt.rst.txt deleted file mode 100644 index b2acfe6540d48c7038b2b06ccf24906d15384d75..0000000000000000000000000000000000000000 --- a/docs/_sources/chapter1/3minimal-rt.rst.txt +++ /dev/null @@ -1,445 +0,0 @@ -重建最小化运行时 -================================= - -.. toctree:: - :hidden: - :maxdepth: 5 - -本节开始我们将着手自己来实现之前被我们移除的 ``Hello, world!`` 程序中的功能。在这一小节,我们介绍如何进行 **执行环境初始化** 。 - -我们在上一小节提到过,一个应用程序的运行离不开下面多层执行环境栈的支撑。以 ``Hello, world!`` 程序为例,在目前广泛使用的操作系统上, -它就至少需要经历以下层层递进的初始化过程: - -- 一段汇编代码对硬件进行初始化,让上层包括内核在内的软件得以运行; -- 要运行该程序的时候,内核分配相应资源,将程序代码和数据载入内存,并赋予 CPU 使用权,由此应用程序可以运行; -- 程序员编写的代码是应用程序的一部分,它需要标准库进行一些初始化工作后才能运行。 - -但在上一小节中,由于目标平台 ``riscv64gc-unknown-none-elf`` 没有任何操作系统支持,我们只能禁用标准库并移除默认的 main 函数 -入口。但是最终我们还是要将 main 恢复回来并且在里面输出 ``Hello, world!`` 的。因此,我们需要知道具体需要做哪些初始化工作才能支持 -main 的运行。 - -而这又需要明确两点:首先是系统在做这些初始化工作之前处于什么状态,在做完初始化工作也就是即将执行 main 函数之前又处于什么状态。比较二者 -即可得出答案。 - -让我们从 CPU 加电后第一条指令开始讲起。对于裸机平台 ``riscv64gc-unknown-none-elf`` 而言,它的 pc 寄存器会被设置为 ``0x80000000`` , -也就是说它会从这个 **物理地址** (Physical Address) 开始一条条取指并执行放置于 **物理内存** (Physical Memory) 中的指令。 - -.. note:: - - **物理内存与物理地址** - - 物理内存是计算机体系结构中一个重要的组成部分。在存储方面,CPU 唯一能够直接访问的只有物理内存中的数据,它可以通过访存指令来达到这一目的。 - 从 CPU 的视角看来,可以将物理内存看成一个大字节数组,而物理地址则对应于一个能够用来访问数组中某个元素的下标。与我们日常编程习惯不同的 - 是,该下标通常不以 0 开头,而通常以 ``0x80000000`` 开头。总结一下的话就是, CPU 可以通过物理地址来 *逐字节* 访问物理内存中保存的 - 数据。 - - 值得一提的是,当 CPU 以多个字节(比如 2/4/8 或更多)为单位访问物理内存(事实上并不局限于物理内存)中的数据时,就有可能会引入端序和 - 地址对齐的问题。由于这并不是重点,我们在这里不展开说明。 - -在该目标平台上,物理内存以物理地址 ``0x80000000`` 开头的部分放置着 **引导加载程序** (Bootloader) 的代码。它的任务是对硬件进行一些 -初始化工作,并跳转到一个固定的物理地址 ``0x80020000`` 。在本书正文中我们无需关心它的实现,而是当成一个黑盒使用即可,它的预编译版本 -可执行文件放在项目根目录的 ``bootloader`` 目录下。在这之后,控制权就会被移交到我们手中。因此,我们需要保证我们负责的初始化的代码 -出现在物理内存以物理地址 ``0x80020000`` 开头的地方。在我们的初始化任务完成之后,自然需要跳转到 main 函数进行执行里面的代码,这也是 -初始化任务的一个重要部分。 - -但实际上不止如此,我们还需要考虑栈的设置。 - -.. _function-call-and-stack: - -函数调用与栈 ----------------------------- - -从汇编指令的级别看待一段程序的执行,假如 CPU 依次执行的指令的物理地址序列为 :math:`\{a_n\}`,那么这个序列会符合怎样的模式呢? - -其中最简单的无疑就是 CPU 一条条连续向下执行指令,也即满足递推式 :math:`a_{n+1}=a_n+L`,这里我们假设该平台的指令是定长的且均为 -:math:`L` 字节(常见情况为 2/4 字节)。但是执行序列并不总是符合这种模式,当位于物理地址 :math:`a_n` 的指令是一条跳转指令的时候, -该模式就有可能被破坏。跳转指令对应于我们在程序中构造的 **控制流** (Control Flow) 的多种不同结构,比如分支结构(如 if/switch 语句) -和循环结构(如 for/while 语句)。用来实现上述两种结构的跳转指令,只需实现跳转功能,也就是将 pc 寄存器设置到一个指定的地址即可。 - -另一种控制流结构则显得更为复杂: **函数调用** (Function Call)。我们大概清楚调用函数整个过程中代码执行的顺序,如果是从源代码级的 -视角来看,我们会去执行被调用函数的代码,等到它返回之后,我们会回到调用函数对应语句的下一行继续执行。那么我们如何用汇编指令来实现 -这一过程?首先在调用的时候,需要有一条指令跳转到被调用函数的位置,这个看起来和其他控制结构没什么不同;但是在被调用函数返回的时候,我们 -却需要返回那条跳转过来的指令的下一条继续执行。这次用来返回的跳转究竟跳转到何处,在对应的函数调用发生之前是不知道的。比如,我们在两个不同的 -地方调用同一个函数,显然函数返回之后会回到不同的地址。这是一个很大的不同:其他控制流都只需要跳转到一个 *编译期固定下来* 的地址,而函数调用 -的返回跳转是跳转到一个 *运行时确定* (确切地说是在函数调用发生的时候)的地址。 - -对此,指令集必须给用于函数调用的跳转指令一些额外的能力,而不只是单纯的跳转。在 RISC-V 架构上,有两条指令即符合这样的特征: - -.. list-table:: RISC-V 函数调用跳转指令 - :widths: 20 30 - :header-rows: 1 - :align: center - - * - 指令 - - 指令功能 - * - :math:`\text{jal}\ \text{rd},\ \text{imm}[20:1]` - - :math:`\text{rd}\leftarrow\text{pc}+4` - - :math:`\text{pc}\leftarrow\text{pc}+\text{imm}` - * - :math:`\text{jalr}\ \text{rd},\ (\text{imm}[11:0])\text{rs}` - - :math:`\text{rd}\leftarrow\text{pc}+4` - - :math:`\text{pc}\leftarrow\text{rs}+\text{imm}` - -.. note:: - - **RISC-V 指令各部分含义** - - 在大多数只与通用寄存器打交道的指令中, rs 表示 **源寄存器** (Source Register), imm 表示 **立即数** (Immediate), - 是一个常数,二者构成了指令的输入部分;而 rd 表示 **目标寄存器** (Destination Register),它是指令的输出部分。rs 和 rd - 可以在 32 个通用寄存器 x0~x31 中选取。但是这三个部分都不是必须的,某些指令只有一种输入类型,另一些指令则没有输出部分。 - - -从中可以看出,这两条指令除了设置 pc 寄存器完成跳转功能之外,还将当前跳转指令的下一条指令地址保存在 rd 寄存器中。 -(这里假设所有指令的长度均为 4 字节,在不使用 C 标准指令集拓展的情况下成立) -在 RISC-V 架构中, -通常使用 ra(x1) 寄存器作为其中的 rd ,因此在函数返回的时候,只需跳转回 ra 所保存的地址即可。事实上在函数返回的时候我们常常使用一条 -**伪指令** (Pseudo Instruction) 跳转回调用之前的位置: ``ret`` 。它会被汇编器翻译为 ``jalr x0, 0(x1)``,含义为跳转到寄存器 -ra 保存的物理地址,由于 x0 是一个恒为 0 的寄存器,在 rd 中保存这一步被省略。总结一下,在进行函数调用的时候,我们通过 jalr 指令 -保存返回地址并实现跳转;而在函数即将返回的时候,则通过 ret 指令跳转之前的下一条指令继续执行。这两条指令实现了函数调用流程的核心机制。 - -由于我们是在 ra 寄存器中保存返回地址的,我们要保证它在函数执行的全程不发生变化,不然在 ret 之后就会跳转到错误的位置。事实上编译器 -除了函数调用的相关指令之外确实基本上不使用 ra 寄存器。也就是说,如果在函数中没有调用其他函数,那 ra 的值不会变化,函数调用流程 -能够正常工作。但遗憾的是,在实际编写代码的时候我们常常会遇到函数 **多层嵌套调用** 的情形。我们很容易想象,如果函数不支持嵌套调用,那么编程将会 -变得多么复杂。如果我们试图在一个函数 :math:`f` 中调用一个子函数,在跳转到子函数 :math:`g` 的同时,ra 会被覆盖成这条跳转指令的 -下一条的地址,而 ra 之前所保存的函数 :math:`f` 的返回地址将会 `永久丢失` 。 - -因此,若想正确实现嵌套函数调用的控制流,我们必须通过某种方式保证:在一个函数调用子函数的前后,ra 寄存器的值不能发生变化。但实际上, -这并不仅仅局限于 ra 一个寄存器,而是作用于所有的通用寄存器。这是因为,编译器是独立编译每个函数的,因此一个函数并不能知道它所调用的 -子函数修改了哪些寄存器。而站在一个函数的视角,在调用子函数的过程中某些寄存器的值被覆盖的确会对它接下来的执行产生影响。因此这是必要的。 -我们将在控制流转移前后需要保持不变的寄存器集合称之为 **上下文** (Context) 或称 **活动记录** (Activation Record),利用这一概念 -,则在函数调用前后需要保持不变的寄存器集合被称为函数调用上下文。 - -由于每个 CPU 只有一套寄存器,我们若想在子函数调用前后保持函数调用上下文不变,需要物理内存的帮助。确切的说,在调用子函数之前,我们需要在 -内存中的一个区域 **保存** (Save) 函数调用上下文中的寄存器;而之后我们会从内存中同样的区域读取并 **恢复** (Restore) 函数调用上下文 -中的寄存器。实际上,这一工作是由子函数的调用者和被调用者(也就是子函数自身)合作完成。函数调用上下文中的寄存器被分为如下两类: - -- **被调用者保存** (Callee-Saved) 寄存器,即被调用的函数保证调用它前后,这些寄存器保持不变; -- **调用者保存** (Caller-Saved) 寄存器,被调用的函数可能会覆盖这些寄存器。 - -从名字中可以看出,函数调用上下文由调用者和被调用者分别保存,其具体过程分别如下: - -- 调用者:首先保存不希望在函数调用过程中发生变化的调用者保存寄存器,然后通过 jal/jalr 指令调用子函数,返回回来之后恢复这些寄存器。 -- 被调用者:在函数开头保存函数执行过程中被用到的被调用者保存寄存器,然后执行函数,在退出之前恢复这些寄存器。 - -我们发现无论是调用者还是被调用者,都会因调用行为而需要两段匹配的保存和恢复寄存器的汇编代码,可以分别将其称为 **开场白** (Prologue) 和 -**收场白** (Epilogue),它们会由编译器帮我们自动插入。一个函数既有可能作为调用者调用其他函数,也有可能作为被调用者被其他函数调用。对于 -它而言,如果在执行的时候需要修改被调用者保存寄存器,而必须在函数开头的开场白和结尾的收场白处进行保存;对于调用者保存寄存器则可以没有任何 -顾虑的随便使用,因为它在约定中本就不需要承担保证调用者保存寄存器保持不变的义务。 - -.. note:: - - **寄存器保存与编译器优化** - - 这里值得说明的是,调用者和被调用者实际上只需分别按需保存调用者保存寄存器和被调用者保存寄存器的一个子集。对于调用者而言,那些内容 - 并不重要,即使在调用子函数的时候被覆盖也不影响函数执行的调用者保存寄存器不会被编译器保存;而对于被调用者而言,在其执行过程中没有 - 使用到的被调用者保存寄存器也无需保存。编译器作为寄存器的使用者自然知道在这两个场景中,分别有哪些值得保存的寄存器。 - 从这一角度也可以理解为何要将函数调用上下文分成两类:可以在尽可能早的时候优化掉一些无用的寄存器保存与恢复。 - -**调用规范** (Calling Convention) 约定在某个指令集架构上,某种编程语言的函数调用如何实现。它包括了以下内容: - -1. 函数的输入参数和返回值如何传递; -2. 函数调用上下文中调用者/被调用者保存寄存器的划分; -3. 其他的在函数调用流程中对于寄存器的使用方法。 - -调用规范是对于一种确定的编程语言来说的,因为一般意义上的函数调用只会在编程语言的内部进行。当一种语言想要调用用另一门编程语言编写的函数 -接口时,编译器就需要同时清楚两门语言的调用规范,并对寄存器的使用做出调整。 - -.. note:: - - **RISC-V 架构上的 C 语言调用规范** - - RISC-V 架构上的 C 语言调用规范可以在 `这里 `_ 找到。 - 它对通用寄存器的使用做出了如下约定: - - .. list-table:: RISC-V 寄存器功能分类 - :widths: 20 20 40 - :align: center - :header-rows: 1 - - * - 寄存器组 - - 保存者 - - 功能 - * - a0~a7 - - 调用者保存 - - 用来传递输入参数。特别的 a0 和 a1 用来保存返回值。 - * - t0~t6 - - 调用者保存 - - 作为临时寄存器使用,在函数中可以随意使用无需保存。 - * - s0~s11 - - 被调用者保存 - - 作为临时寄存器使用,保存后才能在函数中使用。 - - 剩下的 5 个通用寄存器情况如下: - - - zero(x0) 之前提到过,它恒为零,函数调用不会对它产生影响; - - ra(x1) 是调用者保存的,不过它并不会在每次调用子函数的时候都保存一次,而是在函数的开头和结尾保存/恢复即可,因为在执行期间即使被 - 覆盖也没有关系。看上去和被调用者保存寄存器保存的位置一样,但是它确实是调用者保存的。 - - sp(x2) 是被调用者保存的。这个之后就会提到。 - - gp(x3) 和 tp(x4) 在一个程序运行期间都不会变化,因此不必放在函数调用上下文中。它们的用途在后面的章节会提到。 - - 更加详细的内容可以参考 Cornell 的 `课件 `_ 。 - -之前我们讨论了函数调用上下文的保存/恢复时机以及寄存器的选择,但我们并没有详细说明这些寄存器保存在哪里,只是用“内存中的一块区域”草草带过。实际上, -它更确切的名字是 **栈** (Stack) 。 sp(x2) 常用来保存 **栈指针** (Stack Pointer),它是一个指向了内存中已经用过的位置的一个地址。在 -RISC-V 架构中,栈是从高地址到低地址增长的。在一个函数中,作为起始的开场白负责分配一块新的栈空间,其实它只需要知道需要空间的大小,然后将 sp -的值减小相应的字节数即可,于是物理地址区间 :math:`[\text{新sp},\text{旧sp})` 对应的物理内存便可以被这个函数用来函数调用上下文的保存/恢复 -以及其他工作,这块物理内存被称为这个函数的 **栈帧** (Stackframe)。同理,函数中作为结尾的收场白负责将开场白分配的栈帧回收,这也仅仅需要 -将 sp 的值增加相同的字节数回到分配之前的状态。这也可以解释为什么 sp 是一个被调用者保存寄存器。 - -.. figure:: CallStack.png - :align: center - - 函数调用与栈帧:如图所示,我们能够看到在程序依次调用 a、调用 b、调用 c、c 返回、b 返回整个过程中栈帧的分配/回收以及 sp 寄存器的变化。 - 图中标有 a/b/c 的块分别代表函数 a/b/c 的栈帧。 - -.. note:: - - **数据结构中的栈与实现函数调用所需要的栈** - - 从数据结构的角度来看,栈是一个 **后入先出** (Last In First Out, LIFO) 的线性表,支持向栈顶压入一个元素以及从栈顶弹出一个元素 - 两种操作,分别被称为 push 和 pop。从它提供的接口来看,它只支持访问栈顶附近的元素。因此在实现的时候需要维护一个指向栈顶 - 的指针来表示栈当前的状态。 - - 我们这里的栈与数据结构中的栈原理相同,在很多方面可以一一对应。栈指针 sp 可以对应到指向栈顶的指针,对于栈帧的分配/回收可以分别 - 对应到 push/pop 操作。如果将我们的栈看成一个内存分配器,它之所以可以这么简单,是因为它回收的内存一定是 *最近一次分配* 的内存, - 从而只需要类似 push/pop 的两种操作即可。 - -在合适的编译选项设置之下,一个函数的栈帧内容可能如下图所示: - -.. figure:: StackFrame.png - :align: center - - 函数栈帧中的内容 - -它的开头和结尾分别在 sp(x2) 和 fp(s0) 所指向的地址。按照地址从高到低分别有以下内容,它们都是通过 sp 加上一个偏移量来访问的: - -- ra 寄存器保存其返回之后的跳转地址,是一个调用者保存寄存器; -- 父亲栈帧的结束地址 fp,是一个被调用者保存寄存器; -- 其他被调用者保存寄存器 s1~s11; -- 函数所使用到的局部变量。 - -因此,栈上实际上保存了一条完整的函数调用链,通过适当的方式我们可以实现对它的跟踪。 - -至此,我们基本上说明了函数调用是如何基于栈来实现的。不过我们可以暂时先忽略掉这些细节,因为我们现在只是需要在初始化阶段完成栈的设置,也就是 -设置好栈指针 sp 寄存器,后面的函数调用相关机制编译器会帮我们自动完成。麻烦的是, sp 的值也不能随便设置。至少我们需要保证它仍在物理内存上, -而且不能与程序的其他代码、数据段相交,因为在函数调用的过程中,栈区域里面的内容会被修改。如何保证这一点呢?此外,之前我们还提到我们编写的 -初始化代码必须放在物理地址 ``0x80020000`` 开头的内存上,这又如何做到呢?事实上,这两点都需要我们接下来讲到的程序内存布局的知识。 - -程序内存布局 ----------------------------- - -在我们将源代码编译为可执行文件之后,它就会变成一个看似充满了杂乱无章的字节的一个文件。但我们知道这些字节至少可以分成代码和数据两部分,在 -程序运行起来的时候它们的功能并不相同:代码部分由一条条可以被 CPU 解码并执行的指令组成,而数据部分只是被 CPU 视作可用的存储空间。事实上 -我们还可以根据其功能进一步把两个部分划分为更小的单位: **段** (Section) 。不同的段会被编译器放置在内存不同的位置上,这构成了程序的 -**内存布局** (Memory Layout)。一种典型的程序相对内存布局如下: - -.. figure:: MemoryLayout.png - :align: center - - 一种典型的程序相对内存布局 - -代码部分只有代码段 ``.text`` 一个段,存放程序的所有汇编代码。 - -数据部分则还可以继续细化: - -- 已初始化数据段保存程序中那些已初始化的全局数据,分为 ``.rodata`` 和 ``.data`` 两部分。前者存放只读的全局数据,通常是一些常数或者是 - 常量字符串等;而后者存放可修改的全局数据。 -- 未初始化数据段 ``.bss`` 保存程序中那些未初始化的全局数据,通常由程序的加载者代为进行零初始化,也即将这块区域逐字节清零; -- **堆** (heap) 区域用来存放程序运行时动态分配的数据,如 C/C++ 中的 malloc/new 分配到的数据本体就放在堆区域,它向高地址增长; -- 栈区域 stack 不仅用作函数调用上下文的保存与恢复,每个函数作用域内的局部变量也被编译器放在它的栈帧内。它向低地址增长。 - -.. note:: - - **局部变量与全局变量** - - 在一个函数的视角中,它能够访问的变量包括以下几种: - - - 函数的输入参数和局部变量:保存在一些寄存器或是该函数的栈帧里面,如果是在栈帧里面的话是基于当前 sp 加上一个偏移量来访问的; - - 全局变量:保存在数据段 ``.data`` 和 ``.bss`` 中,某些情况下 gp(x3) 寄存器保存两个数据段中间的一个位置,于是全局变量是基于 - gp 加上一个偏移量来访问的。 - - 堆上的动态变量:本体被保存在堆上,大小在运行时才能确定。而我们只能 *直接* 访问栈上或者全局数据段中的 **编译期确定大小** 的变量。 - 因此我们需要通过一个运行时分配内存得到的一个指向堆上数据的指针来访问它,指针的位宽确实在编译期就能够确定。该指针即可以作为局部变量 - 放在栈帧里面,也可以作为全局变量放在全局数据段中。 - -我们可以将常说的编译流程细化为多个阶段(虽然输入一条命令便可将它们全部完成): - -1. **编译器** (Compiler) 将每个源文件从某门高级编程语言转化为汇编语言,注意此时源文件仍然是一个 ASCII 或其他编码的文本文件; -2. **汇编器** (Assembler) 将上一步的每个源文件中的文本格式的指令转化为机器码,得到一个二进制的 **目标文件** (Object File); -3. **链接器** (Linker) 将上一步得到的所有目标文件以及一些可能的外部目标文件链接在一起形成一个完整的可执行文件。 - -每个目标文件都有着自己局部的内存布局,里面含有若干个段。在链接的时候,链接器会将这些内存布局合并起来形成一个整体的内存布局。此外,每个目标文件 -都有一个符号表,里面记录着它需要从其他文件中寻找的外部符号和能够提供给其他文件的符号,通常是一些函数和全局变量等。在链接的时候汇编器会将 -外部符号替换为实际的地址。 - -我们可以通过 **链接脚本** (Linker Script) 调整链接器的行为,使得最终生成的可执行文件的内存布局符合我们的预期。 - -实现 ----------- - -我们自己编写运行时初始化的代码: - -.. code-block:: asm - :linenos: - - # os/src/entry.asm - .section .text.entry - .globl _start - _start: - la sp, boot_stack_top - call rust_main - - .section .bss.stack - .globl boot_stack - boot_stack: - .space 4096 * 16 - .globl boot_stack_top - boot_stack_top: - -在这段汇编代码中,我们从第 8 行开始预留了一块大小为 4096 * 16 字节也就是 :math:`64\text{KiB}` 的空间用作接下来要运行的程序的栈空间, -这块栈空间的栈顶地址被全局符号 ``boot_stack_top`` 标识,栈底则被全局符号 ``boot_stack`` 标识。同时,这块栈空间单独作为一个名为 -``.bss.stack`` 的段,之后我们会通过链接脚本来安排它的位置。 - -从第 2 行开始,我们通过汇编代码实现执行环境的初始化,它其实只有两条指令:第一条指令将 sp 设置为我们预留的栈空间的栈顶位置,于是之后在函数 -调用的时候,栈就可以从这里开始向低地址增长了。简单起见,我们目前暂时不考虑 sp 越过了栈底 ``boot_stack`` ,也就是栈溢出的情形,虽然这有 -可能导致严重的错误。第二条指令则是通过伪指令 ``call`` 函数调用 ``rust_main`` ,这里的 ``rust_main`` 是一个我们稍后自己编写的应用 -入口。因此初始化任务非常简单:正如上面所说的一样,只需要设置栈指针 sp,随后跳转到应用入口即可。这两条指令单独作为一个名为 -``.text.entry`` 的段,且全局符号 ``_start`` 给出了段内第一条指令的地址。 - -接着,我们在 ``main.rs`` 中嵌入这些汇编代码并声明应用入口 ``rust_main`` : - -.. code-block:: rust - :linenos: - :emphasize-lines: 4,8,10,11,12,13 - - // os/src/main.rs - #![no_std] - #![no_main] - #![feature(global_asm)] - - mod lang_items; - - global_asm!(include_str!("entry.asm")); - - #[no_mangle] - pub fn rust_main() -> ! { - loop {} - } - -背景高亮指出了 ``main.rs`` 中新增的代码。 - -第 4 行中,我们手动设置 ``global_asm`` 特性来支持在 Rust 代码中嵌入全局汇编代码。第 8 行,我们首先通过 -``include_str!`` 宏将同目录下的汇编代码 ``entry.asm`` 转化为字符串并通过 ``global_asm!`` 宏嵌入到代码中。 - -从第 10 行开始, -我们声明了应用的入口点 ``rust_main`` ,这里需要注意的是需要通过宏将 ``rust_main`` 标记为 ``#[no_mangle]`` 以避免编译器对它的 -名字进行混淆,不然的话在链接的时候, ``entry.asm`` 将找不到 ``main.rs`` 提供的外部符号 ``rust_main`` 从而导致链接失败。 - -我们修改 Cargo 的配置文件来使用我们自己的链接脚本 ``os/src/linker.ld`` 而非使用默认的内存布局: - -.. code-block:: - :linenos: - :emphasize-lines: 5,6,7,8 - - // os/.cargo/config - [build] - target = "riscv64gc-unknown-none-elf" - - [target.riscv64gc-unknown-none-elf] - rustflags = [ - "-Clink-arg=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes" - ] - -具体的链接脚本 ``os/src/linker.ld`` 如下: - -.. code-block:: - :linenos: - - OUTPUT_ARCH(riscv) - ENTRY(_start) - BASE_ADDRESS = 0x80020000; - - SECTIONS - { - . = BASE_ADDRESS; - skernel = .; - - stext = .; - .text : { - *(.text.entry) - *(.text .text.*) - } - - . = ALIGN(4K); - etext = .; - srodata = .; - .rodata : { - *(.rodata .rodata.*) - } - - . = ALIGN(4K); - erodata = .; - sdata = .; - .data : { - *(.data .data.*) - } - - . = ALIGN(4K); - edata = .; - .bss : { - *(.bss.stack) - sbss = .; - *(.bss .bss.*) - } - - . = ALIGN(4K); - ebss = .; - ekernel = .; - - /DISCARD/ : { - *(.eh_frame) - } - } - -第 1 行我们设置了目标平台为 riscv ;第 2 行我们设置了整个程序的入口点为之前定义的全局符号 ``_start``; -第 3 行定义了一个常量 ``BASE_ADDRESS`` 为 ``0x80020000`` ,也就是我们之前提到的期望我们自己实现的初始化代码被放在的地址; - -从第 5 行开始体现了链接过程中对输入的目标文件的段的合并。其中 ``.`` 表示当前地址,也就是链接器会从它指向的位置开始往下放置从输入的目标文件 -中收集来的段。我们可以对 ``.`` 进行赋值来调整接下来的段放在哪里,也可以创建一些全局符号赋值为 ``.`` 从而记录这一时刻的位置。我们还能够 -看到这样的格式: - -.. code-block:: - - .rodata : { - *(.rodata) - } - -冒号前面表示最终生成的可执行文件的一个段的名字,花括号内按照放置顺序描述将所有输入目标文件的哪些段放在这个段中,每一行格式为 -``(SectionName)``,表示目标文件 ``ObjectFile`` 的名为 ``SectionName`` 的段需要被放进去。我们也可以 -使用通配符来书写 ```` 和 ```` 分别表示可能的输入目标文件和段名。因此,最终的合并结果是,在最终可执行文件 -中各个常见的段 ``.text, .rodata .data, .bss`` 从低地址到高地址按顺序放置,每个段里面都包括了所有输入目标文件的同名段, -且每个段都有两个全局符号给出了它的开始和结束地址(比如 ``.text`` 段的开始和结束地址分别是 ``stext`` 和 ``etext`` )。 - -为了说明当前实现的正确性,我们需要讨论这样两个问题: - -1. 如何做到执行环境的初始化代码被放在内存上以 ``0x80020000`` 开头的区域上? - - 在链接脚本第 7 行,我们将当前地址设置为 ``BASE_ADDRESS`` 也即 ``0x80020000`` ,然后从这里开始往高地址放置各个段。第一个被放置的 - 是 ``.text`` ,而里面第一个被放置的又是来自 ``entry.asm`` 中的段 ``.text.entry``,这个段恰恰是含有两条指令的执行环境初始化代码, - 它在所有段中最早被放置在我们期望的 ``0x80020000`` 处。 - -2. 应用函数调用所需的栈放在哪里? - - 从链接脚本第 32 行开始,我们可以看出 ``entry.asm`` 中分配的栈空间对应的段 ``.bss.stack`` 被放入到可执行文件中的 - ``.bss`` 段中的低地址中。在后面虽然有一个通配符 ``.bss.*`` ,但是由于链接脚本的优先匹配规则它并不会被匹配到后面去。 - 这里需要注意的是地址区间 :math:`[\text{sbss},\text{ebss})` 并不包括栈空间,其原因后面再进行说明。 - -这样一来,我们就将运行时重建完毕了。在 ``os`` 目录下 ``cargo build --release`` 或者直接 ``make build`` 就能够看到 -最终生成的可执行文件 ``target/riscv64gc-unknown-none-elf/release/os`` 。 - -参考文献 ----------------- -- `RISC-V C 语言调用规范 `_ -- `Notes from Cornell CS3410 2019Spring `_ -- `Lecture from Berkeley CS61C 2018Spring `_ -- `Lecture from MIT 6.828 2020 `_ diff --git a/docs/_sources/chapter1/4load-manually.rst.txt b/docs/_sources/chapter1/4load-manually.rst.txt deleted file mode 100644 index 773cda7e94d155619460c7a2ce7fd63cf3e05e2d..0000000000000000000000000000000000000000 --- a/docs/_sources/chapter1/4load-manually.rst.txt +++ /dev/null @@ -1,320 +0,0 @@ -手动加载、运行应用程序 -================================== - -.. toctree:: - :hidden: - :maxdepth: 5 - -在上一节中我们自己实现了一套运行时来代替标准库,并完整的构建了最终的可执行文件。但是它现在只是放在磁盘上的一个文件,若想将它运行起来的话, -就需要将它加载到内存中,在大多数情况下这是操作系统的任务。 - -让我们先来看看最终可执行文件的格式: - -.. code-block:: console - - $ file os/target/riscv64gc-unknown-none-elf/release/os - os/target/riscv64gc-unknown-none-elf/release/os: ELF 64-bit LSB executable, - UCB RISC-V, version 1 (SYSV), statically linked, not stripped - -从中可以看出可执行文件的格式为 **可执行和链接格式** (Executable and Linkable Format, ELF),硬件平台是 RV64 。在 ELF 文件中, -除了程序必要的代码、数据段(它们本身都只是一些二进制的数据)之外,还有一些 **元数据** (Metadata) 描述这些段在地址空间中的位置和在 -文件中的位置以及一些权限控制信息,这些元数据只能放在代码、数据段的外面。 - -我们可以通过二进制工具 ``readelf`` 来看看 ELF 文件中究竟包含什么内容,输入命令: - -.. code-block:: console - - $ riscv64-unknown-elf-readelf os/target/riscv64gc-unknown-none-elf/release/os -a - -首先可以看到一个 ELF header,它位于 ELF 文件的开头: - -.. code-block:: objdump - :linenos: - :emphasize-lines: 2,11,12,13,17,19 - - ELF Header: - Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 - Class: ELF64 - Data: 2's complement, little endian - Version: 1 (current) - OS/ABI: UNIX - System V - ABI Version: 0 - Type: EXEC (Executable file) - Machine: RISC-V - Version: 0x1 - Entry point address: 0x80020000 - Start of program headers: 64 (bytes into file) - Start of section headers: 9016 (bytes into file) - Flags: 0x1, RVC, soft-float ABI - Size of this header: 64 (bytes) - Size of program headers: 56 (bytes) - Number of program headers: 3 - Size of section headers: 64 (bytes) - Number of section headers: 8 - Section header string table index: 6 - -- 第 2 行是一个称之为 **魔数** (Magic) 独特的常数,存放在 ELF header 的一个固定位置。当加载器将 ELF 文件加载到内存之前,通常会查看 - 该位置的值是否正确,来快速确认被加载的文件是不是一个 ELF 。 -- 第 11 行给出了可执行文件的入口点为 ``0x80020000`` ,这正是我们上一节所做的事情。 -- 从 12/13/17/19 行中,我们可以知道除了 ELF header 之外,还有另外两种不同的 header,分别称为 program header 和 section header, - 它们都有多个。ELF header 中给出了三种 header 的大小、在文件中的位置以及数目。 - -一共有 3 个不同的 program header,它们从文件的 64 字节开始,每个 56 字节: - -.. code-block:: objdump - - Program Headers: - Type Offset VirtAddr PhysAddr - FileSiz MemSiz Flags Align - LOAD 0x0000000000001000 0x0000000080020000 0x0000000080020000 - 0x000000000000001a 0x000000000000001a R E 0x1000 - LOAD 0x0000000000002000 0x0000000080021000 0x0000000080021000 - 0x0000000000000000 0x0000000000010000 RW 0x1000 - GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 - 0x0000000000000000 0x0000000000000000 RW 0x0 - -每个 program header 指向一个在加载的时候可以连续加载的区域。 - -一共有 8 个不同的 section header,它们从文件的 9016 字节开始,每个 64 字节: - -.. code-block:: objdump - - Section Headers: - [Nr] Name Type Address Offset - Size EntSize Flags Link Info Align - [ 0] NULL 0000000000000000 00000000 - 0000000000000000 0000000000000000 0 0 0 - [ 1] .text PROGBITS 0000000080020000 00001000 - 000000000000001a 0000000000000000 AX 0 0 2 - [ 2] .bss NOBITS 0000000080021000 00002000 - 0000000000010000 0000000000000000 WA 0 0 1 - [ 3] .riscv.attributes RISCV_ATTRIBUTE 0000000000000000 00002000 - 000000000000006a 0000000000000000 0 0 1 - [ 4] .comment PROGBITS 0000000000000000 0000206a - 0000000000000013 0000000000000001 MS 0 0 1 - [ 5] .symtab SYMTAB 0000000000000000 00002080 - 00000000000001c8 0000000000000018 7 4 8 - [ 6] .shstrtab STRTAB 0000000000000000 00002248 - 0000000000000041 0000000000000000 0 0 1 - [ 7] .strtab STRTAB 0000000000000000 00002289 - 00000000000000ab 0000000000000000 0 0 1 - Key to Flags: - W (write), A (alloc), X (execute), M (merge), S (strings), I (info), - L (link order), O (extra OS processing required), G (group), T (TLS), - C (compressed), x (unknown), o (OS specific), E (exclude), - p (processor specific) - - There are no section groups in this file. - -每个 section header 则描述一个段的元数据。 - -其中,我们看到了代码段 ``.text`` 被放在可执行文件的 4096 字节处,大小 0x1a=26 字节,需要被加载到地址 ``0x80020000``。 -它们分别由元数据的字段 Offset、 Size 和 Address 给出。同理,我们自己预留的应用程序函数调用栈在 ``.bss`` 段中,大小为 :math:`64\text{KiB}` -,需要被加载到地址 ``0x80021000`` 处。我们没有看到 ``.data/.rodata`` 等段,因为目前的 ``rust_main`` 里面没有任何东西。 - -我们还能够看到 ``.symtab`` 段中给出的符号表: - -.. code-block:: - - Symbol table '.symtab' contains 19 entries: - Num: Value Size Type Bind Vis Ndx Name - 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND - 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS os.78wp4f2l-cgu.0 - 2: 0000000000000000 0 FILE LOCAL DEFAULT ABS os.78wp4f2l-cgu.1 - 3: 0000000080020000 0 NOTYPE LOCAL DEFAULT 1 .Lpcrel_hi0 - 4: 0000000080020000 0 NOTYPE GLOBAL DEFAULT 1 _start - 5: 0000000080021000 0 NOTYPE GLOBAL DEFAULT 2 boot_stack - 6: 0000000080031000 0 NOTYPE GLOBAL DEFAULT 2 boot_stack_top - 7: 0000000080020010 10 FUNC GLOBAL DEFAULT 1 rust_main - 8: 0000000080020000 0 NOTYPE GLOBAL DEFAULT ABS BASE_ADDRESS - 9: 0000000080020000 0 NOTYPE GLOBAL DEFAULT 1 skernel - 10: 0000000080020000 0 NOTYPE GLOBAL DEFAULT 1 stext - 11: 0000000080021000 0 NOTYPE GLOBAL DEFAULT 1 etext - 12: 0000000080021000 0 NOTYPE GLOBAL DEFAULT 1 srodata - 13: 0000000080021000 0 NOTYPE GLOBAL DEFAULT 1 erodata - 14: 0000000080021000 0 NOTYPE GLOBAL DEFAULT 1 sdata - 15: 0000000080021000 0 NOTYPE GLOBAL DEFAULT 1 edata - 16: 0000000080031000 0 NOTYPE GLOBAL DEFAULT 2 sbss - 17: 0000000080031000 0 NOTYPE GLOBAL DEFAULT 2 ebss - 18: 0000000080031000 0 NOTYPE GLOBAL DEFAULT 2 ekernel - -里面包括了栈顶、栈底、rust_main 的地址以及我们在 ``linker.ld`` 中定义的各个段开始和结束地址。 - -因此,从 ELF header 中可以看出,ELF 中的内容按顺序应该是: - -- ELF header -- 若干个 program header -- 程序各个段的实际数据 -- 若干的 section header - -当将程序加载到内存的时候,对于每个 program header 所指向的区域,我们需要将对应的数据从文件复制到内存中。这就需要解析 ELF 的元数据 -才能知道数据在文件中的位置以及即将被加载到内存中的位置。但目前,我们不需要从 ELF 中解析元数据就知道程序的内存布局 -(这个内存布局是我们按照需求自己指定的),我们可以手动完成加载任务。 - -具体的做法是利用 ``rust-objcopy`` 工具删除掉 ELF 文件中的 -所有 header 只保留各个段的实际数据得到一个没有任何符号的纯二进制镜像文件,由于缺少了必要的元数据,我们的二进制工具也没有办法 -对它完成解析了。而后,我们直接将这个二进制镜像文件手动载入到内存中合适位置即可。在这里,我们知道在镜像文件中,仍然是代码段 ``.text`` -作为起始,因此我们要将这个代码段载入到 ``0x80020000`` 才能和上一级 bootloader 对接上。因此,我们只要把整个镜像文件手动载入到 -内存的地址 ``0x80020000`` 处即可。在不同的硬件平台上,手动加载的方式是不同的。 - -qemu 平台 -------------------------- - -首先我们还原一下可执行文件和二进制镜像的生成流程: - -.. code-block:: makefile - - # os/Makefile - TARGET := riscv64gc-unknown-none-elf - MODE := release - KERNEL_ELF := target/$(TARGET)/$(MODE)/os - KERNEL_BIN := $(KERNEL_ELF).bin - - $(KERNEL_BIN): kernel - @$(OBJCOPY) $(KERNEL_ELF) --strip-all -O binary $@ - - kernel: - @cargo build --release - -这里可以看出 ``KERNEL_ELF`` 保存最终可执行文件 ``os`` 的路径,而 ``KERNEL_BIN`` 保存只保留各个段数据的二进制镜像文件 ``os.bin`` -的路径。目标 ``kernel`` 直接通过 ``cargo build`` 以 release 模式最终可执行文件,目标 ``KERNEL_BIN`` 依赖于目标 ``kernel``,将 -可执行文件通过 ``rust-objcopy`` 工具加上适当的配置移除所有的 header 和符号得到二进制镜像。 - -我们可以通过 ``make run`` 直接在 qemu 上运行我们的应用程序,qemu 是一个虚拟机,它完整的模拟了一整套硬件平台,就像是一台真正的计算机 -一样,我们来看运行 qemu 的具体命令: - -.. code-block:: makefile - :linenos: - :emphasize-lines: 11,12,13,14,15 - - KERNEL_ENTRY_PA := 0x80020000 - - BOARD ?= qemu - SBI ?= rustsbi - BOOTLOADER := ../bootloader/$(SBI)-$(BOARD).bin - - run: run-inner - - run-inner: build - ifeq ($(BOARD),qemu) - @qemu-system-riscv64 \ - -machine virt \ - -nographic \ - -bios $(BOOTLOADER) \ - -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) - else - @cp $(BOOTLOADER) $(BOOTLOADER).copy - @dd if=$(KERNEL_BIN) of=$(BOOTLOADER).copy bs=128K seek=1 - @mv $(BOOTLOADER).copy $(KERNEL_BIN) - @sudo chmod 777 $(K210-SERIALPORT) - python3 $(K210-BURNER) -p $(K210-SERIALPORT) -b 1500000 $(KERNEL_BIN) - miniterm --eol LF --dtr 0 --rts 0 --filter direct $(K210-SERIALPORT) 115200 - endif - -注意其中高亮部分给出了传给 qemu 的参数。 - -- ``-machine`` 告诉 qemu 使用预设的硬件配置。在整个项目中我们将一直沿用该配置。 -- ``-bios`` 告诉 qemu 使用我们放在 ``bootloader`` 目录下的预编译版本作为 bootloader。 -- ``-device`` 则告诉 qemu 将二进制镜像加载到内存指定的位置。 - -可以先输入 Ctrl+A ,再输入 X 来退出 qemu 终端。 - -.. warning:: - - **FIXME: 使用 GDB 跟踪 qemu 的运行状态** - - -k210 平台 ------------------------- - -对于 k210 平台来说,只需要将 maix 系列开发板通过数据线连接到 PC,然后 ``make run BOARD=k210`` 即可。从 Makefile 中来看: - -.. code-block:: makefile - :linenos: - :emphasize-lines: 13,16,17 - - K210-SERIALPORT = /dev/ttyUSB0 - K210-BURNER = ../tools/kflash.py - - run-inner: build - ifeq ($(BOARD),qemu) - @qemu-system-riscv64 \ - -machine virt \ - -nographic \ - -bios $(BOOTLOADER) \ - -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) - else - @cp $(BOOTLOADER) $(BOOTLOADER).copy - @dd if=$(KERNEL_BIN) of=$(BOOTLOADER).copy bs=128K seek=1 - @mv $(BOOTLOADER).copy $(KERNEL_BIN) - @sudo chmod 777 $(K210-SERIALPORT) - python3 $(K210-BURNER) -p $(K210-SERIALPORT) -b 1500000 $(KERNEL_BIN) - miniterm --eol LF --dtr 0 --rts 0 --filter direct $(K210-SERIALPORT) 115200 - endif - -在构建目标 ``run-inner`` 的时候,根据平台 ``BOARD`` 的不同,启动运行的指令也不同。当我们传入命令行参数 ``BOARD=k210`` 时,就会进入下面 -的分支。 - -- 第 13 行我们使用 ``dd`` 工具将 bootloader 和二进制镜像拼接到一起,这是因为 k210 平台的写入工具每次只支持写入一个文件,所以我们只能 - 将二者合并到一起一并写入 k210 的内存上。这样的参数设置可以保证 bootloader 在合并后文件的开头,而二进制镜像在文件偏移量 0x20000 的 - 位置处。有兴趣的读者可以输入命令 ``man dd`` 查看关于工具 ``dd`` 的更多信息。 -- 第 16 行我们使用烧写工具 ``K210-BURNER`` 将合并后的镜像烧写到 k210 开发板的内存的 ``0x80000000`` 地址上。 - 参数 ``K210-SERIALPORT`` 表示当前 OS 识别到的 k210 开发板的串口设备名。在 Ubuntu 平台上一般为 ``/dev/ttyUSB0``。 -- 第 17 行我们打开串口终端和 k210 开发板进行通信,可以通过键盘向 k210 开发板发送字符并在屏幕上看到 k210 开发板的字符输出。 - -可以输入 Ctrl+] 退出 miniterm。 - -手动清空 .bss 段 ----------------------------------- - -由于 ``.bss`` 段需要在程序正式开始运行之前被固定初始化为零,因此在 ELF 文件中,为了节省磁盘空间,只会记录 ``.bss`` 段的位置而并不是 -有一块长度相等的全为零的数据。在内核将可执行文件加载到内存的时候,它需要负责将 ``.bss`` 所分配到的内存区域全部清零。而我们这里需要在 -应用程序 ``rust_main`` 中,在访问任何 ``.bss`` 段的全局数据之前手动将其清零。 - -.. code-block:: rust - :linenos: - - // os/src/main.rs - fn clear_bss() { - extern "C" { - fn sbss(); - fn ebss(); - } - (sbss as usize..ebss as usize).for_each(|a| { - unsafe { (a as *mut u8).write_volatile(0) } - }); - } - -在程序内自己进行清零的时候,我们就不用去解析 ELF(此时也没有 ELF 可供解析)了,而是通过链接脚本 ``linker.ld`` 中给出的全局符号 -``sbss`` 和 ``ebss`` 来确定 ``.bss`` 段的位置。 - -.. note:: - - **Rust 小知识:外部符号引用** - - extern "C" 可以引用一个外部的 C 函数接口(这意味着调用它的时候要遵从目标平台的 C 语言调用规范)。但我们这里只是引用位置标志 - 并将其转成 usize 获取它的地址。由此可以知道 ``.bss`` 段两端的地址。 - - **Rust 小知识:迭代器与闭包** - - 代码第 7 行用到了 Rust 的迭代器与闭包的语法,它们在很多情况下能够提高开发效率。如读者感兴趣的话也可以将其改写为等价的 for - 循环实现。 - -.. warning:: - - **Rust Unsafe** - - 代码第 8 行,我们将 ``.bss`` 段内的一个地址转化为一个 **裸指针** (Raw Pointer),并将它指向的值修改为 0。这在 C 语言中是 - 一种司空见惯的操作,但在 Rust 中我们需要将他包裹在 unsafe 块中。这是因为,Rust 认为对于裸指针的 **解引用** (Dereference) - 是一种 unsafe 行为。 - - 相比 C 语言,Rust 进行了更多的语义约束来保证安全性(内存安全/类型安全/并发安全),这在编译期和运行期都有所体现。但在某些时候, - 尤其是与底层硬件打交道的时候,在 Rust 的语义约束之内没法满足我们的需求,这个时候我们就需要将超出了 Rust 语义约束的行为包裹 - 在 unsafe 块中,告知编译器不需要对它进行完整的约束检查,而是由程序员自己负责保证它的安全性。当代码不能正常运行的时候,我们往往也是 - 最先去检查 unsafe 块中的代码,因为它没有受到编译器的保护,出错的概率更大。 - - C 语言中的指针相当于 Rust 中的裸指针,它无所不能但又太过于灵活,程序员对其不谨慎的使用常常会引起很多内存不安全问题,最常见的如 - 悬垂指针和多次回收的问题,Rust 编译器没法确认程序员对它的使用是否安全,因此将其划到 unsafe Rust 的领域。在 safe Rust 中,我们 - 有引用 ``&/&mut`` 以及各种功能各异的智能指针 ``Box/RefCell/Rc`` 可以使用,只要按照 Rust 的规则来使用它们便可借助 - 编译器在编译期就解决很多潜在的内存不安全问题。 - diff --git a/docs/_sources/chapter1/5sbi-print.rst.txt b/docs/_sources/chapter1/5sbi-print.rst.txt deleted file mode 100644 index aba658eb0cd5622688fb070a74ba6c0bf2646720..0000000000000000000000000000000000000000 --- a/docs/_sources/chapter1/5sbi-print.rst.txt +++ /dev/null @@ -1,111 +0,0 @@ -格式化输出 -===================== - -.. toctree:: - :hidden: - :maxdepth: 5 - -这一小节我们来自己实现 ``println!`` 的功能。 我们这里只是给出一些函数之间的调用关系,而不在这里进行一些实现细节上的展开。有兴趣的读者 -可以自行参考代码提供的注释。 - -在屏幕上打印一个字符是最基础的功能,它已经由 bootloader (也就是放在 ``bootloader`` 目录下的预编译版本)提供,具体的调用方法可以参考 -``sbi.rs`` 中的 ``console_putchar`` 函数。 - -随后我们在 ``console.rs`` 中利用 ``console_putchar`` 来实现 ``print!`` 和 ``println!`` 两个宏。有兴趣的读者可以去代码注释中 -参考有关 Rust ``core::fmt`` 库和宏编写的相关知识。在 ``main.rs`` 声明子模块 ``mod console`` 的时候加上 ``#[macro_use]`` 来让 -整个引用都可以使用到该模块里面定义的宏。 - -接着我们在 ``lang_items.rs`` 中修改 panic 时的行为: - -.. code-block:: rust - - // os/src/lang_items.rs - use crate::sbi::shutdown; - - #[panic_handler] - fn panic(info: &PanicInfo) -> ! { - if let Some(location) = info.location() { - println!("Panicked at {}:{} {}", location.file(), location.line(), info.message().unwrap()); - } else { - println!("Panicked: {}", info.message().unwrap()); - } - shutdown() - } - -我们尝试从传入的 ``PanicInfo`` 中解析 panic 发生的文件和行数。如果解析成功的话,就和 panic 的报错信息一起打印出来。我们需要在 -``main.rs`` 开头加上 ``#![feature(panic_info_message)]`` 才能通过 ``PanicInfo::message`` 获取报错信息。 - -.. note:: - - **Rust 小知识: 错误处理** - - Rust 中常利用 ``Option`` 和 ``Result`` 进行方便的错误处理。它们都属于枚举结构: - - - ``Option`` 既可以有值 ``Option::Some`` ,也有可能没有值 ``Option::None``; - - ``Result`` 既可以保存某个操作的返回值 ``Result::Ok`` ,也可以表明操作过程中出现了错误 ``Result::Err`` 。 - - 我们可以使用 ``Option/Result`` 来保存一个不能确定存在/不存在或是成功/失败的值。之后可以通过匹配 ``if let`` 或是在能够确定 - 的场合直接通过 ``unwrap`` 将里面的值取出。详细的内容可以参考 Rust 官方文档。 - - -此外,我们还使用 bootloader 中提供的另一个接口 ``shutdown`` 关闭机器。 - -最终我们的应用程序 ``rust_main`` 如下: - -.. code-block:: rust - - // os/src/main.rs - - #[no_mangle] - pub fn rust_main() -> ! { - extern "C" { - fn stext(); - fn etext(); - fn srodata(); - fn erodata(); - fn sdata(); - fn edata(); - fn sbss(); - fn ebss(); - fn boot_stack(); - fn boot_stack_top(); - }; - clear_bss(); - println!("Hello, world!"); - println!(".text [{:#x}, {:#x})", stext as usize, etext as usize); - println!(".rodata [{:#x}, {:#x})", srodata as usize, erodata as usize); - println!(".data [{:#x}, {:#x})", sdata as usize, edata as usize); - println!("boot_stack [{:#x}, {:#x})", boot_stack as usize, boot_stack_top as usize); - println!(".bss [{:#x}, {:#x})", sbss as usize, ebss as usize); - panic!("Shutdown machine!"); - } - -当我们在 qemu 平台上运行的时候能够看到如下的运行结果: - -.. code-block:: - :linenos: - - [rustsbi] Version 0.1.0 - .______ __ __ _______.___________. _______..______ __ - | _ \ | | | | / | | / || _ \ | | - | |_) | | | | | | (----`---| |----`| (----`| |_) || | - | / | | | | \ \ | | \ \ | _ < | | - | |\ \----.| `--' |.----) | | | .----) | | |_) || | - | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| - - [rustsbi] Platform: QEMU - [rustsbi] misa: RV64ACDFIMSU - [rustsbi] mideleg: 0x222 - [rustsbi] medeleg: 0xb109 - [rustsbi] Kernel entry: 0x80020000 - Hello, world! - .text [0x80020000, 0x80022000) - .rodata [0x80022000, 0x80023000) - .data [0x80023000, 0x80023000) - boot_stack [0x80023000, 0x80033000) - .bss [0x80033000, 0x80033000) - Panicked at src/main.rs:46 Shutdown machine! - - -其中前 13 行是 bootloader 的输出,剩下的部分是我们的应用程序的输出,打印了 ``Hello, world!``,输出了程序内部各个段的地址区间, -还展示了 panic 相关信息。 \ No newline at end of file diff --git a/docs/_sources/chapter1/6practice.rst.txt b/docs/_sources/chapter1/6practice.rst.txt deleted file mode 100644 index c2e71c4bd6d487d75f41ad48aaffec478b74cd86..0000000000000000000000000000000000000000 --- a/docs/_sources/chapter1/6practice.rst.txt +++ /dev/null @@ -1,17 +0,0 @@ -练习一 -============== - -.. toctree:: - :hidden: - :maxdepth: 5 - -编程练习一:backtrace ---------------------------------------------- - -仔细阅读 :ref:`函数调用与栈 ` 小节的内容,特别是函数栈帧中的 ra 寄存器和 prev fp 的位置以及它们的作用。 - -编程实现:在 ``rust_main`` 中多层嵌套调用函数,然后在最深层按照层数由深到浅打印函数调用链,也就是每一层函数栈帧中保存的 ra 寄存器的值, -由此我们可以依次知道每个函数的调用语句所在的地址,也就能跟踪一整条函数调用链。 - -拓展:寻找/改写 Rust 库或者自己实现,能够通过 ra 寄存器的值得到其所在的源文件/函数/行数,从而更直观的看到函数调用链。或者也可以通过 -addr2line 工具在运行结束之后手动去可执行文件中查找每个地址对应的信息。 \ No newline at end of file diff --git a/docs/_sources/chapter1/index.rst.txt b/docs/_sources/chapter1/index.rst.txt deleted file mode 100644 index 82266a9344ca1dddf31edfd4e579284ca7c7b5d9..0000000000000000000000000000000000000000 --- a/docs/_sources/chapter1/index.rst.txt +++ /dev/null @@ -1,73 +0,0 @@ -第一章:RV64 裸机应用 -============================================== - -.. toctree:: - :hidden: - :maxdepth: 4 - - 1app-ee-platform - 2remove-std - 3minimal-rt - 4load-manually - 5sbi-print - 6practice - -大多数程序员的第一行代码都从 ``Hello, world!`` 开始,当我们满怀着好奇心在编辑器内键入仅仅数个字节,再经过几行命令编译、运行,终于 -在黑洞洞的终端窗口中看到期望中的结果的时候,一扇通往编程世界的大门已经打开。时至今日,我们能够隐约意识到编程工作能够如此方便简洁并不是 -理所当然的,实际上有着多层硬件、软件隐藏在它背后,才让我们不必付出那么多努力就能够创造出功能强大的应用程序。 - -本章我们的目标仍然只是输出 ``Hello, world!`` ,但这一次,我们将离开舒适区,基于一个几乎空无一物的平台从零开始搭建我们自己的高楼大厦, -而不是仅仅通过一行语句就完成任务。 - -获取本章代码: - -.. code-block:: console - - $ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git - $ cd rCore-Tutorial-v3 - $ git checkout ch1 - -在 qemu 模拟器上运行本章代码: - -.. code-block:: console - - $ cd os - $ make run - -将 Maix 系列开发版连接到 PC,并在上面运行本章代码: - -.. code-block:: console - - $ cd os - $ make run BOARD=k210 - -.. warning:: - - **FIXME: 提供 wsl/macOS 等更多平台支持** - -如果顺利的话,以 qemu 平台为例,将输出: - -.. code-block:: - - [rustsbi] Version 0.1.0 - .______ __ __ _______.___________. _______..______ __ - | _ \ | | | | / | | / || _ \ | | - | |_) | | | | | | (----`---| |----`| (----`| |_) || | - | / | | | | \ \ | | \ \ | _ < | | - | |\ \----.| `--' |.----) | | | .----) | | |_) || | - | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| - - [rustsbi] Platform: QEMU - [rustsbi] misa: RV64ACDFIMSU - [rustsbi] mideleg: 0x222 - [rustsbi] medeleg: 0xb109 - [rustsbi] Kernel entry: 0x80020000 - Hello, world! - .text [0x80020000, 0x80022000) - .rodata [0x80022000, 0x80023000) - .data [0x80023000, 0x80023000) - boot_stack [0x80023000, 0x80033000) - .bss [0x80033000, 0x80033000) - Panicked at src/main.rs:46 Shutdown machine! - -除了 ``Hello, world!`` 之外还有一些额外的信息,最后关机。 \ No newline at end of file diff --git a/docs/_sources/chapter2/1rv-privilege.rst.txt b/docs/_sources/chapter2/1rv-privilege.rst.txt deleted file mode 100644 index 8c0c4d4186c371c894468dd134436a5cd0151061..0000000000000000000000000000000000000000 --- a/docs/_sources/chapter2/1rv-privilege.rst.txt +++ /dev/null @@ -1,156 +0,0 @@ -RISC-V 特权级架构 -===================================== - -.. toctree:: - :hidden: - :maxdepth: 5 - -为了保护我们的批处理系统不受到出错应用程序的影响并全程稳定工作,单凭软件实现是很难做到的,而是需要 CPU 提供一种特权级隔离机制,使得它在执行 -应用程序和内核代码的时候处于不同的特权级。特权级可以看成 CPU 随时间变化而处于的不同的工作模式。 - -RISC-V 架构中一共定义了 4 种特权级: - -.. list-table:: RISC-V 特权级 - :widths: 30 30 60 - :header-rows: 1 - :align: center - - * - 级别 - - 编码 - - 名称 - * - 0 - - 00 - - 机器模式 (M, Machine) - * - 1 - - 01 - - 监督模式 (S, Supervisor) - * - 2 - - 10 - - H, Hypervisor - * - 3 - - 11 - - 用户/应用模式 (U, User/Application) - -其中,级别的数值越小,特权级越高,掌控硬件的能力越强。从表中可以看出, M 模式处在最高的特权级,而 U 模式处于最低的特权级。 - -之前我们给出过支持应用程序运行的一套 :ref:`执行环境栈 ` ,现在我们站在特权级架构的角度去重新看待它: - -.. image:: PrivilegeStack.png - :align: center - :name: PrivilegeStack - -和之前一样,白色块表示一层执行环境,黑色块表示相邻两层执行环境之间的接口。这张图片给出了能够支持运行 Unix 这类复杂系统的软件栈。其中 -内核代码运行在 S 模式上;应用程序运行在 U 模式上。运行在 M 模式上的软件被称为 **监督模式执行环境** (SEE, Supervisor Execution Environment) -,这是站在运行在 S 模式上的软件的视角来看,它的下面也需要一层执行环境支撑,因此被命名为 SEE,它需要在相比 S 模式更高的特权级下运行, -一般情况下在 M 模式上运行。 - -.. note:: - - **按需实现 RISC-V 特权级** - - RISC-V 架构中,只有 M 模式是必须实现的,剩下的特权级则可以根据跑在 CPU 上应用的实际需求进行调整: - - - 简单的嵌入式应用只需要实现 M 模式; - - 带有一定保护能力的嵌入式系统需要实现 M/U 模式; - - 复杂的多任务系统则需要实现 M/S/U 模式。 - - -之前我们提到过,执行环境的其中一种功能是在执行它支持的上层软件之前进行一些初始化工作。我们之前提到的引导加载程序会在加电后对整个系统进行 -初始化,它实际上是 SEE 功能的一部分,也就是说在 RISC-V 架构上引导加载程序一般运行在 M 模式上。此外,编程语言的标准库也会在执行程序员 -编写的逻辑之前进行一些初始化工作,但是在这张图中我们并没有将其展开,而是统一归类到 U 模式软件,也就是应用程序中。 - -执行环境的另一种功能是对上层软件的执行进行监控管理。监控管理可以理解为,当上层软件执行的时候出现了一些情况导致需要用到执行环境中提供的功能, -因此需要暂停上层软件的执行,转而运行执行环境的代码。由于上层软件和执行环境被设计为运行在不同的特权级,这个过程也往往(而 **不一定** ) -伴随着 CPU 的 **特权级切换** 。当执行环境的代码运行结束后,我们需要回到上层软件暂停的位置继续执行。在 RISC-V 架构中,这种与常规控制流 -(顺序、循环、分支、函数调用)不同的 **异常控制流** (ECF, Exception Control Flow) 被称为 **陷入** (Trap) 。 - -触发 Trap 的原因总体上可以分为两种: **中断** (Interrupt) 和 **异常** (Exception) 。本章我们只会用到异常,因此暂且略过中断。异常 -就是指上层软件需要执行环境功能的原因确切的与上层软件的 **某一条指令的执行** 相关。下表中我们给出了 RISC-V 特权级定义的一些异常: - -.. list-table:: RISC-V 异常一览表 - :align: center - :header-rows: 1 - :widths: 30 30 60 - - * - Interrupt - - Exception Code - - Description - * - 0 - - 0 - - Instruction address misaligned - * - 0 - - 1 - - Instruction access fault - * - 0 - - 2 - - Illegal instruction - * - 0 - - 3 - - Breakpoint - * - 0 - - 4 - - Load address misaligned - * - 0 - - 5 - - Load access fault - * - 0 - - 6 - - Store/AMO address misaligned - * - 0 - - 7 - - Store/AMO access fault - * - 0 - - 8 - - Environment call from U-mode - * - 0 - - 9 - - Environment call from S-mode - * - 0 - - 11 - - Environment call from M-mode - * - 0 - - 12 - - Instruction page fault - * - 0 - - 13 - - Load page fault - * - 0 - - 14 - - Store/AMO page fault - -其中断点异常 (Breakpoint) 和执行环境调用 (Environment call) 两个异常是通过在上层软件中执行一条特定的指令触发的:当执行 ``ebreak`` -这条指令的之后就会触发断点异常;而执行 ``ecall`` 这条指令的时候则会随着 CPU 当前所处特权级而触发不同的异常。从表中可以看出,当 CPU 分别 -处于 M/S/U 三种特权级时执行 ``ecall`` 这条指令会触发三种异常。 - -在这里我们需要说明一下执行环境调用,这是一种很特殊的异常, :ref:`上图 ` 中相邻两特权级软件之间的接口正是基于这种异常 -机制实现的。M 模式软件 SEE 和 S 模式的内核之间的接口被称为 **监督模式二进制接口** (SBI, Supervisor Binary Interface),而内核和 -U 模式的应用程序之间的接口被称为 **应用程序二进制接口** (Application Binary Interface),当然它有一个更加通俗的名字—— **系统调用** -(syscall, System Call) 。而之所以叫做二进制接口,是因为它和在同一种编程语言内部调用接口不同,是汇编指令级的一种接口。事实上 M/S/U -三个特权级的软件可能分别由不同的编程语言实现,即使是用同一种编程语言实现的,其调用也并不是普通的函数调用执行流,而是陷入,在该过程中有可能 -切换 CPU 特权级。因此只有将接口下降到汇编指令级才能够满足其通用性。 - -可以看到,在这样的架构之下,每层特权级的软件都只能做高特权级软件允许它做的、且对于高特权级软件不会产生什么撼动的事情,一旦超出了能力范围, -就必须寻求高特权级软件的帮助。因此,在一条执行流中我们经常能够看到特权级切换。如下图所示: - -.. image:: EnvironmentCallFlow.png - :align: center - -其他的异常则一般是在执行某一条指令的时候发生了错误,需要将控制转交给高特权级软件:当错误可恢复的时候,则处理错误并重新回到上层软件的执行; -否则,一般会将上层软件杀死以避免破坏执行环境。 - -第一章只是一个简单的嵌入式应用,它全程运行在 M 模式下。而在后续的章节中,我们会用到 M/S/U 三种特权级:其中我们的内核运行在 S 模式下 -(在本章表现为一个简单的批处理系统),应用程序运行在 U 特权级下,第一章提到的预编译的 bootloader 实际上是运行在 M 模式下的 SEE。 -整个系统就由这三层运行在不同特权级下的不同软件组成。在特权级相关机制方面,本书正文中我们重点关心 S/U 特权级, M 特权级的机制细节则 -是作为可选内容在附录 :doc:`/appendix-c/index` 中讲解,有兴趣的读者可以参考。 - -.. - 随着特权级的逐渐降低,硬件的能力受到限制, - 从每一个特权级看来,比它特权级更低的部分都可以看成是它的应用。(这个好像没啥用?) - M 模式是每个 RISC-V CPU 都需要实现的模式,而剩下的模式都是可选的。常见的模式组合:普通嵌入式应用只需要在 M 模式上运行;追求安全的 - 嵌入式应用需要在 M/U 模式上运行;像 Unix 这样比较复杂的系统这需要 M/S/U 三种模式。 - RISC-V 特权级规范中给出了一些特权寄存器和特权指令... - 重要的是保护,也就是特权级的切换。当 CPU 处于低特权级的时候,如果发生了错误或者一些需要处理的情况,CPU 会切换到高特权级进行处理。这个 - 就是所谓的 Trap 机制。 - RISC-V 架构规范分为两部分: `RISC-V 无特权级规范 `_ - 和 `RISC-V 特权级规范 `_ 。 - RISC-V 无特权级规范中给出的指令和寄存器无论在 CPU 处于哪个特权级下都可以使用。 diff --git a/docs/_sources/chapter2/2application.rst.txt b/docs/_sources/chapter2/2application.rst.txt deleted file mode 100644 index 8125adfcb47ebf461d37e97db126d52452364e39..0000000000000000000000000000000000000000 --- a/docs/_sources/chapter2/2application.rst.txt +++ /dev/null @@ -1,218 +0,0 @@ -实现应用程序 -=========================== - -.. toctree:: - :hidden: - :maxdepth: 5 - -本节我们来实现被批处理系统逐个加载并运行的应用程序,它们是在认为自己会在 U 模式运行的前提下而设计、编写的,但实际上它们完全可能在其他特权级 -运行。事实上,保证应用程序的代码在 U 模式运行是我们接下来将实现的批处理系统的任务。 - -应用程序的实现放在项目根目录的 ``user`` 目录下,它和第一章的嵌入式应用不同之处在于以下几点。 - -项目结构 ------------------- - -我们看到 ``user/src`` 目录下面多出了一个 ``bin`` 目录。``bin`` 里面有多个文件,每个文件都是一个用户程序,目前里面有三个程序,分别是: - -- ``00hello_world``:在屏幕上打印一行 ``Hello, world!``; -- ``01store_fault``:访问一个非法的物理地址,测试批处理系统是否会被该错误影响; -- ``02power``:一个略微复杂的、行为不断在计算和打印字符串间切换的程序。 - -批处理系统会按照文件名开头的编号从小到大的顺序加载并运行它们。 - -打开其中任意一个文件,会看到里面只有一个 ``main`` 函数,因此这很像是我们日常利用高级语言编程,只需要在单个文件中给出主逻辑的实现即可。 - -我们还能够看到代码中尝试引入了外部库: - -.. code-block:: rust - - #[macro_use] - extern crate user_lib; - -这个外部库其实就是 ``user`` 目录下的 ``lib.rs`` 以及它引用的若干子模块中。至于这个外部库为何叫 ``user_lib`` 而不叫 ``lib.rs`` -所在的目录的名字 ``user`` ,是因为在 ``user/Cargo.toml`` 中我们对于库的名字进行了设置: ``name = "user_lib"`` 。它作为 -``bin`` 目录下的源程序所依赖的用户库,等价于其他编程语言提供的标准库。 - -在 ``lib.rs`` 中我们定义了用户库的入口点 ``_start`` : - -.. code-block:: rust - :linenos: - - #[no_mangle] - #[link_section = ".text.entry"] - pub extern "C" fn _start() -> ! { - clear_bss(); - syscall::sys_exit(main()); - panic!("unreachable after sys_exit!"); - } - -第 2 行使用 Rust 的宏将 ``_start`` 这段代码编译后的汇编代码中放在一个名为 ``.text.entry`` 的代码段中,方便我们在后续链接的时候 -调整它的位置使得它能够作为用户库的入口。 - -而从第 4 行开始我们能够看到进入用户库入口之后,首先和第一章一样手动清空需要被零初始化 ``.bss`` 段(很遗憾到目前为止底层的批处理系统还 -没有这个能力,所以我们只能在用户库中完成),然后是调用 ``main`` 函数得到一个类型为 ``i32`` 的返回值,最后是使用接下来会提到的系统调用 -退出应用程序并将这个返回值告知批处理系统。 - -我们还在 ``lib.rs`` 中看到了另一个 ``main`` : - -.. code-block:: rust - :linenos: - - #[linkage = "weak"] - #[no_mangle] - fn main() -> i32 { - panic!("Cannot find main!"); - } - -第 1 行,我们使用 Rust 的宏将其函数符号 ``main`` 标志为弱链接。这样在最后链接的时候,虽然在 ``lib.rs`` 和 ``bin`` 目录下的某个 -应用程序都有 ``main`` 符号,但由于 ``lib.rs`` 中的 ``main`` 符号是弱链接,链接器会使用 ``bin`` 目录下的应用主逻辑作为 ``main`` 。 -这里我们主要是进行某种程度上的保护,如果在 ``bin`` 目录下找不到任何 ``main`` ,那么编译也能够通过,并会在运行时报错。 - -为了上述这些链接操作,我们需要在 ``lib.rs`` 的开头加入: - -.. code-block:: rust - - #![feature(linkage)] - -内存布局 -------------------- - -在 ``user/.cargo/config`` 中,我们和第一章一样设置链接时使用链接脚本 ``user/src/linker.ld`` 。在其中我们做的重要的事情是: - -- 将程序的起始物理地址调整为 ``0x80040000`` ,三个应用程序都会被加载到这个物理地址上运行; -- 将 ``_start`` 所在的 ``.text.entry`` 放在整个程序的开头,也就是说批处理系统只要在加载之后跳转到 ``0x80040000`` 就已经进入了 - 用户库的入口点,并会在初始化之后跳转到应用程序主逻辑; -- 提供了最终生成可执行文件的 ``.bss`` 段的起始和终止地址,方便 ``clear_bss`` 函数使用。 - -其余的部分和第一章基本相同。 - -系统调用 ---------------------- - -在子模块 ``syscall`` 中我们作为应用程序来通过 ``ecall`` 调用批处理系统提供的接口,由于应用程序运行在 U 模式, ``ecall`` 指令会触发 -名为 ``Environment call from U-mode`` 的异常,并 Trap 进入 S 模式执行批处理系统针对这个异常特别提供的服务代码。由于这个接口处于 -S 模式的批处理系统和 U 模式的应用程序之间,从上一节我们可以知道,这个接口可以被称为 ABI 或者系统调用。现在我们不关心底层的批处理系统如何 -提供应用程序所需的功能,只是站在应用程序的角度去使用即可。 - -在本章中,应用程序和批处理系统之间约定如下两个系统调用: - -.. code-block:: rust - :caption: 系统调用一 - - /// 功能:将内存中缓冲区中的数据写入文件。 - /// 参数:`fd` 表示待写入文件的文件描述符; - /// `buf` 表示内存中缓冲区的起始地址; - /// `len` 表示内存中缓冲区的长度。 - /// 返回值:返回成功写入的长度。 - /// syscall ID:64 - fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize; - - /// 功能:退出应用程序并将返回值告知批处理系统。 - /// 参数:`xstate` 表示应用程序的返回值。 - /// 返回值:该系统调用不应该返回。 - /// syscall ID:93 - fn sys_exit(xstate: usize) -> !; - -我们知道系统调用实际上是汇编指令级的二进制接口,因此这里给出的只是使用 Rust 语言描述的版本。在实际调用的时候,我们需要按照 RISC-V 调用 -规范在合适的寄存器中放置系统调用的参数,然后执行 ``ecall`` 指令触发 Trap。在 Trap 回到 U 模式的应用程序代码之后,会从 ``ecall`` 的 -下一条指令继续执行,同时我们能够按照调用规范在合适的寄存器中读取返回值。 - -在 RISC-V 调用规范中,和函数调用的情形类似,约定寄存器 ``a0~a6`` 保存系统调用的参数, ``a0~a1`` 保存系统调用的返回值。有些许不同的是 -寄存器 ``a7`` 用来传递 syscall ID,这是因为所有的 syscall 都是通过 ``ecall`` 指令触发的,除了各输入参数之外我们还额外需要一个寄存器 -来保存要请求哪个系统调用。由于这超出了 Rust 语言的表达能力,我们需要在代码中使用内嵌汇编来完成参数/返回值绑定和 ``ecall`` 指令的插入: - -.. code-block:: rust - :linenos: - - fn syscall(id: usize, args: [usize; 3]) -> isize { - let mut ret: isize; - unsafe { - llvm_asm!("ecall" - : "={x10}" (ret) - : "{x10}" (args[0]), "{x11}" (args[1]), "{x12}" (args[2]), "{x17}" (id) - : "memory" - : "volatile" - ); - } - ret - } - -第 1 行,我们将所有的系统调用都封装成 ``syscall`` 函数,可以看到它支持传入 syscall ID 和 3 个参数。 - -第 4 行开始,我们使用 Rust 提供的 ``llvm_asm!`` 宏在代码中内嵌汇编,在本行也给出了具体要插入的汇编指令,也就是 ``ecall``,但这并不是 -全部,后面我们还需要进行一些相关设置。这个宏在 Rust 中还不稳定,因此我们需要在 ``lib.rs`` 开头加入 ``#![feature(llvm_asm)]`` 。 -此外,编译器无法判定插入汇编代码这个行为的安全性,所以我们需要将其包裹在 unsafe 块中自己来对它负责。 - -Rust 中的 ``llvm_asm!`` 宏的完整格式如下: - -.. code-block:: rust - - llvm_asm!(assembly template - : output operands - : input operands - : clobbers - : options - ); - -下面逐行进行说明。 - -第 5 行指定输出操作数。这里由于我们的系统调用返回值只有一个 ``isize`` ,根据调用规范它会被保存在 ``a0`` 寄存器中。在双引号内,我们 -可以对于使用的操作数进行限制,由于是输出部分,限制的开头必须是一个 ``=`` 。我们可以在限制内使用一对花括号再加上一个寄存器的名字告诉 -编译器汇编的输出结果会保存在这个寄存器中。我们将声明出来用来保存系统调用返回值的变量 ``ret`` 包在一对普通括号里面放在操作数限制的 -后面,这样可以把变量和寄存器建立联系。于是,在系统调用返回之后我们就能在变量 ``ret`` 中看到返回值了。注意,变量 ``ret`` 必须为可变 -绑定,否则无法通过编译,这也说明在 unsafe 块内编译器还是会进行力所能及的安全检查。 - -第 6 行指定输入操作数。由于是输入部分,限制的开头不用加上 ``=`` 。同时在限制中设置使用寄存器 ``a0~a2`` 来保存系统调用的参数,以及 -寄存器 ``a7`` 保存 syscall ID ,而它们分别 ``syscall`` 的参数变量 ``args`` 和 ``id`` 绑定。 - -第 7 行用于告知编译器插入的汇编代码会造成的一些影响以防止编译器在不知情的情况下误优化。常用的使用方法是告知编译器某个寄存器在执行嵌入 -的汇编代码中的过程中会发生变化。我们这里则是告诉编译器在执行嵌入汇编代码中的时候会修改内存。这能给编译器提供更多信息。 - -第 8 行用于告知编译器将我们在程序中给出的嵌入汇编代码保持原样放到最终构建的可执行文件中。如果不这样做的话,编译器可能会把它和其他代码 -一视同仁并放在一起进行一些我们期望之外的优化。为了保证语义的正确性,一些比较关键的汇编代码需要加上该选项。 - -第一章中的输出到屏幕的操作也同样是使用内联汇编调用 SEE 提供的 SBI 接口来实现的。有兴趣的读者可以回顾第一章的 ``console.rs`` 和 -``sbi.rs`` 。 - -.. note:: - - **Rust 中的内联汇编** - - 我们这里使用的 ``llvm_asm!`` 宏是将 Rust 底层 IR LLVM 中提供的内联汇编包装成的,更多信息可以参考 `llvm_asm 文档 `_ 。 - - 在未来的 Rust 版本推荐使用功能更加强大且方便易用的 ``asm!`` 宏,但是目前还未稳定,可以查看 `inline-asm RFC `_ 了解最新进展。 - -于是 ``sys_write`` 和 ``sys_exit`` 只需将 ``syscall`` 进行包装: - -.. code-block:: rust - :linenos: - - const SYSCALL_WRITE: usize = 64; - const SYSCALL_EXIT: usize = 93; - - pub fn sys_write(fd: usize, buffer: &[u8]) -> isize { - syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()]) - } - - pub fn sys_exit(xstate: i32) -> isize { - syscall(SYSCALL_EXIT, [xstate as usize, 0, 0]) - } - -注意 ``sys_write`` 使用一个 ``&[u8]`` 切片类型来描述缓冲区,这是一个 **胖指针** (Fat Pointer),里面既包含缓冲区的起始地址,还 -包含缓冲区的长度。我们可以分别通过 ``as_ptr`` 和 ``len`` 方法取出它们并独立的作为实际的系统调用参数。 - -我们把 ``console`` 子模块中 ``Stdout::write_str`` 改成基于 ``sys_write`` 的实现,且传入的 ``fd`` 参数设置为 1,它代表标准输出, -也就是输出到屏幕。目前我们不需要考虑其他的 ``fd`` 选取情况。这样,应用程序的 ``println!`` 宏借助系统调用变得可用了。 - -``sys_exit`` 则在用户库中的 ``_start`` 内使用,当应用程序主逻辑 ``main`` 返回之后,使用该系统调用退出应用程序并将返回值告知 -底层的批处理系统。 - -自动构建 ------------------------ - -这里简要介绍一下应用程序的自动构建。只需要在 ``user`` 目录下 ``make build`` 即可: - -1. 对于 ``src/bin`` 下的每个应用程序,在 ``target/riscv64gc-unknown-none-elf/release`` 目录下生成一个同名的 ELF 可执行文件; -2. 使用 objcopy 二进制工具将上一步中生成的 ELF 文件删除所有 ELF header 和符号得到 ``.bin`` 后缀的纯二进制镜像文件。它们将被链接 - 进内核并由内核在合适的时机加载到内存。 \ No newline at end of file diff --git a/docs/_sources/chapter2/3batch-system.rst.txt b/docs/_sources/chapter2/3batch-system.rst.txt deleted file mode 100644 index 16fb4430df9a882492993166ee37612512247b58..0000000000000000000000000000000000000000 --- a/docs/_sources/chapter2/3batch-system.rst.txt +++ /dev/null @@ -1,182 +0,0 @@ -实现批处理系统 -============================== - -.. toctree:: - :hidden: - :maxdepth: 5 - -将应用程序链接到内核 --------------------------------------------- - -在本章中,我们把应用程序的二进制镜像文件作为内核的数据段链接到内核里面,因此内核需要知道内含的应用程序的数量和它们的位置,这样才能够在运行时 -对它们进行管理并能够加载到物理内存。 - -在 ``os/src/main.rs`` 中能够找到这样一行: - -.. code-block:: rust - - global_asm!(include_str!("link_app.S")); - -这里我们引入了一段汇编代码 ``link_app.S`` ,它一开始并不存在,而是在构建的时候自动生成的。当我们使用 ``make run`` 让系统成功运行起来 -之后,我们可以先来看一看里面的内容: - -.. code-block:: asm - :linenos: - - # os/src/link_app.S - - .align 4 - .section .data - .global _num_app - _num_app: - .quad 3 - .quad app_0_start - .quad app_1_start - .quad app_2_start - .quad app_2_end - - .section .data - .global app_0_start - .global app_0_end - app_0_start: - .incbin "../user/target/riscv64gc-unknown-none-elf/release/00hello_world.bin" - app_0_end: - - .section .data - .global app_1_start - .global app_1_end - app_1_start: - .incbin "../user/target/riscv64gc-unknown-none-elf/release/01store_fault.bin" - app_1_end: - - .section .data - .global app_2_start - .global app_2_end - app_2_start: - .incbin "../user/target/riscv64gc-unknown-none-elf/release/02power.bin" - app_2_end: - -可以看到第 13 行开始的三个数据段分别插入了三个应用程序的二进制镜像,并且各自有一对全局符号 ``app_*_start, app_*_end`` 指示它们的 -开始和结束位置。而第 3 行开始的另一个数据段相当于一个 64 位整数数组。数组中的第一个元素表示应用程序的数量,后面则按照顺序放置每个应用 -程序的起始地址,最后一个元素放置最后一个应用程序的结束位置。这样每个应用程序的位置都能从该数组中相邻两个元素中得知。这个数组所在的位置 -同样也由全局符号 ``_num_app`` 所指示。 - -这个文件是在 ``cargo build`` 的时候,由脚本 ``os/build.rs`` 控制生成的。有兴趣的读者可以参考其代码。 - -应用管理器 --------------------------- - -我们在 ``os`` 的 ``batch`` 子模块中实现一个应用管理器,它的功能是:保存应用数量和各自的位置信息,以及当前执行到第几个应用了。结构体定义 -如下: - -.. code-block:: rust - - struct AppManager { - inner: RefCell, - } - struct AppManagerInner { - num_app: usize, - current_app: usize, - app_start: [usize; MAX_APP_NUM + 1], - } - -这里我们可以看出,上面提到的应用管理器需要保存和维护的信息都在 ``AppManagerInner`` 里面,而结构体 ``AppManager`` 里面只是保存了 -一个指向 ``AppManagerInner`` 的 ``RefCell`` 智能指针。这样设计的原因在于:我们希望将 ``AppManager`` 实例化为一个全局变量使得 -任何函数都可以直接访问,但是里面的 ``current_app`` 字段表示当前执行到了第几个应用,它会在系统运行期间发生变化。因此在声明全局变量 -的时候一种自然的方法是利用 ``static mut``。但是在 Rust 中,任何对于 ``static mut`` 变量的访问都是 unsafe 的,而我们要尽可能 -减少 unsafe 的使用来更多的让编译器负责安全性检查。 - -于是,我们利用 ``RefCell`` 来提供内部可变性,所谓的内部可变性就是指在我们只能拿到 ``AppManager`` 的不可变借用,意味着同样也只能 -拿到 ``AppManagerInner`` 的不可变借用的情况下依然可以修改 ``AppManagerInner`` 里面的字段。 -使用 ``RefCell::borrow/RefCell::borrow_mut`` 分别可以拿到 ``RefCell`` 里面内容的不可变借用/可变借用, -``RefCell`` 内部会运行时维护当前已有的借用状态并进行借用检查。于是 ``RefCell::borrow_mut`` 就是我们实现内部可变性的关键。 - -我们这样初始化 ``AppManager`` 的全局实例: - -.. code-block:: rust - - lazy_static! { - static ref APP_MANAGER: AppManager = AppManager { - inner: RefCell::new({ - extern "C" { fn _num_app(); } - let num_app_ptr = _num_app as usize as *const usize; - let num_app = unsafe { num_app_ptr.read_volatile() }; - let mut app_start: [usize; MAX_APP_NUM + 1] = [0; MAX_APP_NUM + 1]; - let app_start_raw: &[usize] = unsafe { - core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1) - }; - app_start[..=num_app].copy_from_slice(app_start_raw); - AppManagerInner { - num_app, - current_app: 0, - app_start, - } - }), - }; - } - -这里我们使用了外部库 ``lazy_static`` 提供的 ``lazy_static!`` 宏。要引入这个外部库,我们需要加入依赖: - -.. code-block:: toml - - # os/Cargo.toml - - [dependencies] - lazy_static = { version = "1.4.0", features = ["spin_no_std"] } - -``lazy_static!`` 宏提供了全局变量的运行时初始化功能。一般情况下,全局变量必须在编译期设置一个初始值,但是有些全局变量依赖于运行期间 -才能得到的数据作为初始值。这导致这些全局变量需要在运行时发生变化,也即重新设置初始值之后才能使用。如果我们手动实现的话有诸多不便之处, -比如又需要把这种全局变量声明为 ``static mut`` 并衍生出很多 unsafe 。这种情况下我们可以使用 ``lazy_static!`` 宏来帮助我们解决 -这个问题。这里我们借助 ``lazy_static!`` 声明了一个名为 ``APP_MANAGER`` 的 ``AppManager`` 全局实例,且只有在它第一次被使用到 -的时候才会实际进行初始化工作。 - -初始化的逻辑很简单,就是找到 ``link_app.S`` 中提供的符号 ``_num_app`` ,并从这里开始解析出应用数量以及各个应用的开头地址。注意其中 -对于切片类型的使用能够很大程度上简化编程。 - -因此,借助 Rust 核心库提供的 ``RefCell`` 和外部库 ``lazy_static!``,我们就能在避免 ``static mut`` 声明的情况下以更加 Rust 的 -方式使用全局变量。 - -``AppManagerInner`` 的方法中, ``print_app_info/get_current_app/move_to_next_app`` 都相当简单直接,需要说明的是 ``load_app``: - -.. code-block:: rust - :linenos: - - unsafe fn load_app(&self, app_id: usize) { - if app_id >= self.num_app { - panic!("All applications completed!"); - } - println!("[kernel] Loading app_{}", app_id); - // clear icache - llvm_asm!("fence.i" :::: "volatile"); - // clear app area - (APP_BASE_ADDRESS..APP_BASE_ADDRESS + APP_SIZE_LIMIT).for_each(|addr| { - (addr as *mut u8).write_volatile(0); - }); - let app_src = core::slice::from_raw_parts( - self.app_start[app_id] as *const u8, - self.app_start[app_id + 1] - self.app_start[app_id] - ); - let app_dst = core::slice::from_raw_parts_mut( - APP_BASE_ADDRESS as *mut u8, - app_src.len() - ); - app_dst.copy_from_slice(app_src); - } - -这个方法负责将参数 ``app_id`` 对应的应用程序的二进制镜像加载到物理内存以 ``0x80040000`` 开头的位置,这个位置是批处理系统和应用程序 -之间约定的常数,回忆上一小节中,我们也调整应用程序的内存布局以同一个地址开头。第 8 行开始,我们首先将一块内存清空,然后找到待加载应用 -二进制镜像的位置,并将它复制到正确的位置。它本质上是数据从一块内存复制到另一块内存,从批处理系统的角度来看是将它数据段的一部分复制到了它 -程序之外未知的地方。 - -注意第 7 行我们插入了一条奇怪的汇编指令 ``fence.i`` ,它是用来清理 i-cache 的。我们知道缓存是存储层级结构中提高访存速度的很重要一环。 -而 CPU 对物理内存所做的缓存又分成 **数据缓存** (d-cache) 和 **指令缓存** (i-cache) 两部分,分别在 CPU 访存和取指的时候使用。在取指 -的时候,对于一个指令地址, CPU 会先去 i-cache 里面看一下它是否在某个已缓存的缓存行内,如果在的话它就会直接从高速缓存中拿到指令而不是通过 -总线和内存通信。通常情况下, CPU 会认为程序的代码段不会发生变化,因此 i-cache 是一种只读缓存。但在这里,我们会修改会被 CPU 取指的内存 -区域,这会使得 i-cache 中含有与内存中不一致的内容。因此我们这里必须使用 ``fence.i`` 指令手动清空 i-cache ,让里面所有的内容全部失效, -才能够保证正确性。 - -``batch`` 子模块对外暴露出如下接口: - -- ``init`` :调用 ``print_app_info`` 的时候第一次用到了全局变量 ``APP_MANAGER`` ,它也是在这个时候完成初始化; -- ``run_next_app`` :批处理系统的核心操作,即加载并运行下一个应用程序。当批处理系统完成初始化或者一个应用程序运行结束或出错之后会调用 - 该函数。我们下节再介绍其具体实现。 \ No newline at end of file diff --git a/docs/_sources/chapter2/4trap-handling.rst.txt b/docs/_sources/chapter2/4trap-handling.rst.txt deleted file mode 100644 index dad2f03e6865f3b53a3a818383133c242196b5ec..0000000000000000000000000000000000000000 --- a/docs/_sources/chapter2/4trap-handling.rst.txt +++ /dev/null @@ -1,7 +0,0 @@ -处理 Trap -======================= - -.. toctree:: - :hidden: - :maxdepth: 5 - diff --git a/docs/_sources/chapter2/index.rst.txt b/docs/_sources/chapter2/index.rst.txt deleted file mode 100644 index 7b3d98403dc5cf36ccdeb85f0ecc0dcad9ebb255..0000000000000000000000000000000000000000 --- a/docs/_sources/chapter2/index.rst.txt +++ /dev/null @@ -1,95 +0,0 @@ -第二章:批处理系统 -============================================== - -.. toctree:: - :hidden: - :maxdepth: 4 - - 1rv-privilege - 2application - 3batch-system - 4trap-handling - -上一章,我们在 RV64 裸机平台上成功运行起来了 ``Hello, world!`` 。看起来这个过程非常顺利,只需要一条命令就能全部完成。但实际上,在那个 -计算机刚刚诞生的年代,很多事情并不像我们想象的那么简单。 当时,程序被记录在打孔的卡片上,使用汇编语言甚至机器语言来编写。而稀缺且昂贵的 -计算机由专业的管理员负责操作,就和我们在上一章所做的事情一样,他们手动将卡片输入计算机,等待程序运行结束或者终止程序的运行。最后,他们从 -计算机的输出端——也就是打印机中取出程序的输出并交给正在休息室等待的程序提交者。 - -实际上,这样做是一种对于珍贵的计算资源的浪费。因为当时的计算机和今天的个人计算机不同,它的体积极其庞大,能够占满一整个空调房间。管理员在 -房间的各个地方跑来跑去、或是等待打印机的输出的这些时间段,计算机都并没有在工作。于是,人们希望计算机能够不间断的工作且专注于计算任务本身。 - -**批处理系统** (Batch System) 应运而生。它的核心思想是:将多个程序打包到一起输入计算机。而当一个程序运行结束后,计算机会 *自动* 加载 -下一个程序到内存并开始执行。这便是最早的真正意义上的操作系统。 - -程序总是难免出现错误。但人们希望一个程序的错误不要影响到操作系统本身,它只需要终止出错的程序,转而运行执行序列中的下一个程序即可。如果后面的 -程序都无法运行就太糟糕了。这种 *保护* 操作系统不受有意或无意出错的程序破坏的机制被称为 **特权级** (Privilege) 机制,它实现了用户态和 -内核态的隔离,需要软件和硬件的共同努力。 - -本章我们的批处理系统将连续运行三个应用程序,放在 ``user/src/bin`` 目录下。 - -获取本章代码: - -.. code-block:: console - - $ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git - $ cd rCore-Tutorial-v3 - $ git checkout ch2 - -在 qemu 模拟器上运行本章代码: - -.. code-block:: console - - $ cd os - $ make run - -将 Maix 系列开发版连接到 PC,并在上面运行本章代码: - -.. code-block:: console - - $ cd os - $ make run BOARD=k210 - -如果顺利的话,我们可以看到批处理系统自动加载并运行所有的程序并且正确在程序出错的情况下保护了自身: - -.. code-block:: - - [rustsbi] Version 0.1.0 - .______ __ __ _______.___________. _______..______ __ - | _ \ | | | | / | | / || _ \ | | - | |_) | | | | | | (----`---| |----`| (----`| |_) || | - | / | | | | \ \ | | \ \ | _ < | | - | |\ \----.| `--' |.----) | | | .----) | | |_) || | - | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| - - [rustsbi] Platform: QEMU - [rustsbi] misa: RV64ACDFIMSU - [rustsbi] mideleg: 0x222 - [rustsbi] medeleg: 0xb1ab - [rustsbi] Kernel entry: 0x80020000 - [kernel] Hello, world! - [kernel] num_app = 3 - [kernel] app_0 [0x8002b028, 0x8002c328) - [kernel] app_1 [0x8002c328, 0x8002d6c0) - [kernel] app_2 [0x8002d6c0, 0x8002eb98) - [kernel] Loading app_0 - Hello, world! - [kernel] Application exited with code 0 - [kernel] Loading app_1 - Into Test store_fault, we will insert an invalid store operation... - Kernel should kill this application! - [kernel] PageFault in application, core dumped. - [kernel] Loading app_2 - 3^10000=5079 - 3^20000=8202 - 3^30000=8824 - 3^40000=5750 - 3^50000=3824 - 3^60000=8516 - 3^70000=2510 - 3^80000=9379 - 3^90000=2621 - 3^100000=2749 - Test power OK! - [kernel] Application exited with code 0 - [kernel] Panicked at src/batch.rs:61 All applications completed! - diff --git a/docs/_sources/collaboration.rst.txt b/docs/_sources/collaboration.rst.txt deleted file mode 100644 index 3cec6c0e8d7c87b81c9a8b8d21828e1e9aa19026..0000000000000000000000000000000000000000 --- a/docs/_sources/collaboration.rst.txt +++ /dev/null @@ -1,16 +0,0 @@ -项目协作 -================== - -.. toctree:: - :hidden: - :maxdepth: 4 - -1. 参考 `这里 `_ 安装 Sphinx。 -2. ``pip install sphinx_rtd_theme`` 安装 Read The Docs 主题。 -3. ``pip install jieba`` 安装中文分词。 -4. ``pip install sphinx-comments`` 安装 Sphinx 讨论区插件。 -5. :doc:`/rest-example` 是 ReST 的一些基本语法,也可以参考已完成的文档。 -6. 修改之后,在项目根目录下 ``make clean && make html`` 即可在 ``build/html/index.html`` 查看本地构建的主页。请注意在修改 - 章节目录结构之后需要 ``make clean`` 一下,不然可能无法正常更新。 -7. 确认修改无误之后,在项目根目录下 ``make deploy`` 然后即可 ``git add -A && git commit -m && git push`` 上传到远程仓库。 - 如果出现冲突的话,请删除掉 ``docs`` 目录再进行 merge。 diff --git a/docs/_sources/index.rst.txt b/docs/_sources/index.rst.txt deleted file mode 100644 index 70e1fccc8185e5ea9a7cf15611642a8804ccc86f..0000000000000000000000000000000000000000 --- a/docs/_sources/index.rst.txt +++ /dev/null @@ -1,47 +0,0 @@ -.. rCore-Tutorial-Book-v3 documentation master file, created by - sphinx-quickstart on Thu Oct 29 22:25:54 2020. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -rCore-Tutorial-Book 第三版 -================================================== - -.. toctree:: - :maxdepth: 2 - :caption: 正文 - :hidden: - - quickstart - chapter1/index - chapter2/index - -.. toctree:: - :maxdepth: 2 - :caption: 附录 - :hidden: - - appendix-a/index - appendix-b/index - appendix-c/index - -.. toctree:: - :maxdepth: 2 - :caption: 开发注记 - :hidden: - - rest-example - collaboration - - -欢迎来到 rCore-Tutorial-Book 第三版! - -读者须知 ---------------------- - -请先按照 :doc:`/quickstart` 中的说明完成环境配置,再从第一章开始阅读正文。 - -项目协作 ----------------------- - -请参考 :doc:`/collaboration` 了解如何进行项目协作。 - diff --git a/docs/_sources/quickstart.rst.txt b/docs/_sources/quickstart.rst.txt deleted file mode 100644 index a0ac0f2e348d73f4aa5f88ac8a0264fe6cd102a3..0000000000000000000000000000000000000000 --- a/docs/_sources/quickstart.rst.txt +++ /dev/null @@ -1,30 +0,0 @@ -快速上手 -============ - -.. toctree:: - :hidden: - :maxdepth: 4 - -本节我们将完成环境配置并成功运行 rCore-Tutorial。 - -首先,请参考 `环境部署 `_ 安装 qemu 模拟器 -和 rust。有一些小的变更如下: - -- 将 ``riscv64imac-unknown-none-elf`` 改成 ``riscv64gc-unknown-none-elf``; -- 在使用文档中提供的链接下载 qemu 源码的时候,点击下载之后需要将链接中的 ``localhost`` 替换为 ``42.194.184.212:5212``。若仍然 - 不行的话,可以在 `SiFive 官网 `_ 下载预编译的 qemu,比如 - `Ubuntu 版本 qemu `_ 。 - -此外: - -- 下载安装 `macOS 平台 `_ - 或 `Ubuntu 平台 `_ - 的预编译版本 ``riscv64-unknown-elf-*`` 工具链,并添加到环境变量。可以在提示找不到的情况下再进行下载。 -- 下载安装 `Linux 平台 `_ 预编译版本的 ``riscv64-linux-musl-*`` 工具链,并 - 添加到环境变量。可以在提示找不到的情况下再进行下载。 -- 如果想在 Maix 系列开发板上运行,需要安装 python 包 ``pyserial`` 和串口终端 miniterm 。 - -.. warning:: - - **FIXME: 提供一套开箱即用的 Docker 环境** - diff --git a/docs/_sources/rest-example.rst.txt b/docs/_sources/rest-example.rst.txt deleted file mode 100644 index ed7fcb21a6e5aa091141a978004dad6b628717f7..0000000000000000000000000000000000000000 --- a/docs/_sources/rest-example.rst.txt +++ /dev/null @@ -1,62 +0,0 @@ -reStructuredText 基本语法 -===================================================== - -.. toctree:: - :hidden: - :maxdepth: 4 - -.. note:: - 下面是一个注记。 - - `这里 `_ 给出了在 Sphinx 中 - 外部链接的引入方法。注意,链接的名字和用一对尖括号包裹起来的链接地址之间必须有一个空格。链接最后的下划线和片段的后续内容之间也需要 - 有一个空格。 - - 接下来是一个文档内部引用的例子。比如,戳 :doc:`/quickstart` 可以进入快速上手环节。 - -.. warning:: - - 下面是一个警告。 - - .. code-block:: rust - :linenos: - :caption: 一段示例 Rust 代码 - - // 我们甚至可以插入一段 Rust 代码! - fn add(a: i32, b: i32) -> i32 { a + b } - - 下面继续我们的警告。 - -.. error:: - - 下面是一个错误。 - - -这里是一行数学公式 :math:`\sin(\alpha+\beta)=\sin\alpha\cos\beta+\cos\alpha\sin\beta`。 - -基本的文本样式:这是 *斜体* ,这是 **加粗** ,接下来的则是行间公式 ``a0`` 。它们的前后都需要有一个空格隔开其他内容,这个让人挺不爽的... - -`这是 `_ 一个全面展示 -章节分布的例子,来自于 ReadTheDocs 的官方文档。事实上,现在我们也采用 ReadTheDocs 主题了,它非常美观大方。 - -下面是一个测试 gif。 - -.. image:: test.gif - -接下来是一个表格的例子。 - -.. list-table:: RISC-V 函数调用跳转指令 - :widths: 20 30 - :header-rows: 1 - :align: center - - * - 指令 - - 指令功能 - * - :math:`\text{jal}\ \text{rd},\ \text{imm}[20:1]` - - :math:`\text{rd}\leftarrow\text{pc}+4` - - :math:`\text{pc}\leftarrow\text{pc}+\text{imm}` - * - :math:`\text{jalr}\ \text{rd},\ (\text{imm}[11:0])\text{rs}` - - :math:`\text{rd}\leftarrow\text{pc}+4` - - :math:`\text{pc}\leftarrow\text{rs}+\text{imm}` \ No newline at end of file diff --git a/docs/_static/basic.css b/docs/_static/basic.css deleted file mode 100644 index 24a49f09b5a52dc32775ed946bcceca8156e7439..0000000000000000000000000000000000000000 --- a/docs/_static/basic.css +++ /dev/null @@ -1,856 +0,0 @@ -/* - * basic.css - * ~~~~~~~~~ - * - * Sphinx stylesheet -- basic theme. - * - * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -/* -- main layout ----------------------------------------------------------- */ - -div.clearer { - clear: both; -} - -div.section::after { - display: block; - content: ''; - clear: left; -} - -/* -- relbar ---------------------------------------------------------------- */ - -div.related { - width: 100%; - font-size: 90%; -} - -div.related h3 { - display: none; -} - -div.related ul { - margin: 0; - padding: 0 0 0 10px; - list-style: none; -} - -div.related li { - display: inline; -} - -div.related li.right { - float: right; - margin-right: 5px; -} - -/* -- sidebar --------------------------------------------------------------- */ - -div.sphinxsidebarwrapper { - padding: 10px 5px 0 10px; -} - -div.sphinxsidebar { - float: left; - width: 230px; - margin-left: -100%; - font-size: 90%; - word-wrap: break-word; - overflow-wrap : break-word; -} - -div.sphinxsidebar ul { - list-style: none; -} - -div.sphinxsidebar ul ul, -div.sphinxsidebar ul.want-points { - margin-left: 20px; - list-style: square; -} - -div.sphinxsidebar ul ul { - margin-top: 0; - margin-bottom: 0; -} - -div.sphinxsidebar form { - margin-top: 10px; -} - -div.sphinxsidebar input { - border: 1px solid #98dbcc; - font-family: sans-serif; - font-size: 1em; -} - -div.sphinxsidebar #searchbox form.search { - overflow: hidden; -} - -div.sphinxsidebar #searchbox input[type="text"] { - float: left; - width: 80%; - padding: 0.25em; - box-sizing: border-box; -} - -div.sphinxsidebar #searchbox input[type="submit"] { - float: left; - width: 20%; - border-left: none; - padding: 0.25em; - box-sizing: border-box; -} - - -img { - border: 0; - max-width: 100%; -} - -/* -- search page ----------------------------------------------------------- */ - -ul.search { - margin: 10px 0 0 20px; - padding: 0; -} - -ul.search li { - padding: 5px 0 5px 20px; - background-image: url(file.png); - background-repeat: no-repeat; - background-position: 0 7px; -} - -ul.search li a { - font-weight: bold; -} - -ul.search li div.context { - color: #888; - margin: 2px 0 0 30px; - text-align: left; -} - -ul.keywordmatches li.goodmatch a { - font-weight: bold; -} - -/* -- index page ------------------------------------------------------------ */ - -table.contentstable { - width: 90%; - margin-left: auto; - margin-right: auto; -} - -table.contentstable p.biglink { - line-height: 150%; -} - -a.biglink { - font-size: 1.3em; -} - -span.linkdescr { - font-style: italic; - padding-top: 5px; - font-size: 90%; -} - -/* -- general index --------------------------------------------------------- */ - -table.indextable { - width: 100%; -} - -table.indextable td { - text-align: left; - vertical-align: top; -} - -table.indextable ul { - margin-top: 0; - margin-bottom: 0; - list-style-type: none; -} - -table.indextable > tbody > tr > td > ul { - padding-left: 0em; -} - -table.indextable tr.pcap { - height: 10px; -} - -table.indextable tr.cap { - margin-top: 10px; - background-color: #f2f2f2; -} - -img.toggler { - margin-right: 3px; - margin-top: 3px; - cursor: pointer; -} - -div.modindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -div.genindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -/* -- domain module index --------------------------------------------------- */ - -table.modindextable td { - padding: 2px; - border-collapse: collapse; -} - -/* -- general body styles --------------------------------------------------- */ - -div.body { - min-width: 450px; - max-width: 800px; -} - -div.body p, div.body dd, div.body li, div.body blockquote { - -moz-hyphens: auto; - -ms-hyphens: auto; - -webkit-hyphens: auto; - hyphens: auto; -} - -a.headerlink { - visibility: hidden; -} - -a.brackets:before, -span.brackets > a:before{ - content: "["; -} - -a.brackets:after, -span.brackets > a:after { - content: "]"; -} - -h1:hover > a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink, -caption:hover > a.headerlink, -p.caption:hover > a.headerlink, -div.code-block-caption:hover > a.headerlink { - visibility: visible; -} - -div.body p.caption { - text-align: inherit; -} - -div.body td { - text-align: left; -} - -.first { - margin-top: 0 !important; -} - -p.rubric { - margin-top: 30px; - font-weight: bold; -} - -img.align-left, .figure.align-left, object.align-left { - clear: left; - float: left; - margin-right: 1em; -} - -img.align-right, .figure.align-right, object.align-right { - clear: right; - float: right; - margin-left: 1em; -} - -img.align-center, .figure.align-center, object.align-center { - display: block; - margin-left: auto; - margin-right: auto; -} - -img.align-default, .figure.align-default { - display: block; - margin-left: auto; - margin-right: auto; -} - -.align-left { - text-align: left; -} - -.align-center { - text-align: center; -} - -.align-default { - text-align: center; -} - -.align-right { - text-align: right; -} - -/* -- sidebars -------------------------------------------------------------- */ - -div.sidebar { - margin: 0 0 0.5em 1em; - border: 1px solid #ddb; - padding: 7px; - background-color: #ffe; - width: 40%; - float: right; - clear: right; - overflow-x: auto; -} - -p.sidebar-title { - font-weight: bold; -} - -div.admonition, div.topic, blockquote { - clear: left; -} - -/* -- topics ---------------------------------------------------------------- */ - -div.topic { - border: 1px solid #ccc; - padding: 7px; - margin: 10px 0 10px 0; -} - -p.topic-title { - font-size: 1.1em; - font-weight: bold; - margin-top: 10px; -} - -/* -- admonitions ----------------------------------------------------------- */ - -div.admonition { - margin-top: 10px; - margin-bottom: 10px; - padding: 7px; -} - -div.admonition dt { - font-weight: bold; -} - -p.admonition-title { - margin: 0px 10px 5px 0px; - font-weight: bold; -} - -div.body p.centered { - text-align: center; - margin-top: 25px; -} - -/* -- content of sidebars/topics/admonitions -------------------------------- */ - -div.sidebar > :last-child, -div.topic > :last-child, -div.admonition > :last-child { - margin-bottom: 0; -} - -div.sidebar::after, -div.topic::after, -div.admonition::after, -blockquote::after { - display: block; - content: ''; - clear: both; -} - -/* -- tables ---------------------------------------------------------------- */ - -table.docutils { - margin-top: 10px; - margin-bottom: 10px; - border: 0; - border-collapse: collapse; -} - -table.align-center { - margin-left: auto; - margin-right: auto; -} - -table.align-default { - margin-left: auto; - margin-right: auto; -} - -table caption span.caption-number { - font-style: italic; -} - -table caption span.caption-text { -} - -table.docutils td, table.docutils th { - padding: 1px 8px 1px 5px; - border-top: 0; - border-left: 0; - border-right: 0; - border-bottom: 1px solid #aaa; -} - -table.footnote td, table.footnote th { - border: 0 !important; -} - -th { - text-align: left; - padding-right: 5px; -} - -table.citation { - border-left: solid 1px gray; - margin-left: 1px; -} - -table.citation td { - border-bottom: none; -} - -th > :first-child, -td > :first-child { - margin-top: 0px; -} - -th > :last-child, -td > :last-child { - margin-bottom: 0px; -} - -/* -- figures --------------------------------------------------------------- */ - -div.figure { - margin: 0.5em; - padding: 0.5em; -} - -div.figure p.caption { - padding: 0.3em; -} - -div.figure p.caption span.caption-number { - font-style: italic; -} - -div.figure p.caption span.caption-text { -} - -/* -- field list styles ----------------------------------------------------- */ - -table.field-list td, table.field-list th { - border: 0 !important; -} - -.field-list ul { - margin: 0; - padding-left: 1em; -} - -.field-list p { - margin: 0; -} - -.field-name { - -moz-hyphens: manual; - -ms-hyphens: manual; - -webkit-hyphens: manual; - hyphens: manual; -} - -/* -- hlist styles ---------------------------------------------------------- */ - -table.hlist { - margin: 1em 0; -} - -table.hlist td { - vertical-align: top; -} - - -/* -- other body styles ----------------------------------------------------- */ - -ol.arabic { - list-style: decimal; -} - -ol.loweralpha { - list-style: lower-alpha; -} - -ol.upperalpha { - list-style: upper-alpha; -} - -ol.lowerroman { - list-style: lower-roman; -} - -ol.upperroman { - list-style: upper-roman; -} - -:not(li) > ol > li:first-child > :first-child, -:not(li) > ul > li:first-child > :first-child { - margin-top: 0px; -} - -:not(li) > ol > li:last-child > :last-child, -:not(li) > ul > li:last-child > :last-child { - margin-bottom: 0px; -} - -ol.simple ol p, -ol.simple ul p, -ul.simple ol p, -ul.simple ul p { - margin-top: 0; -} - -ol.simple > li:not(:first-child) > p, -ul.simple > li:not(:first-child) > p { - margin-top: 0; -} - -ol.simple p, -ul.simple p { - margin-bottom: 0; -} - -dl.footnote > dt, -dl.citation > dt { - float: left; - margin-right: 0.5em; -} - -dl.footnote > dd, -dl.citation > dd { - margin-bottom: 0em; -} - -dl.footnote > dd:after, -dl.citation > dd:after { - content: ""; - clear: both; -} - -dl.field-list { - display: grid; - grid-template-columns: fit-content(30%) auto; -} - -dl.field-list > dt { - font-weight: bold; - word-break: break-word; - padding-left: 0.5em; - padding-right: 5px; -} - -dl.field-list > dt:after { - content: ":"; -} - -dl.field-list > dd { - padding-left: 0.5em; - margin-top: 0em; - margin-left: 0em; - margin-bottom: 0em; -} - -dl { - margin-bottom: 15px; -} - -dd > :first-child { - margin-top: 0px; -} - -dd ul, dd table { - margin-bottom: 10px; -} - -dd { - margin-top: 3px; - margin-bottom: 10px; - margin-left: 30px; -} - -dl > dd:last-child, -dl > dd:last-child > :last-child { - margin-bottom: 0; -} - -dt:target, span.highlighted { - background-color: #fbe54e; -} - -rect.highlighted { - fill: #fbe54e; -} - -dl.glossary dt { - font-weight: bold; - font-size: 1.1em; -} - -.optional { - font-size: 1.3em; -} - -.sig-paren { - font-size: larger; -} - -.versionmodified { - font-style: italic; -} - -.system-message { - background-color: #fda; - padding: 5px; - border: 3px solid red; -} - -.footnote:target { - background-color: #ffa; -} - -.line-block { - display: block; - margin-top: 1em; - margin-bottom: 1em; -} - -.line-block .line-block { - margin-top: 0; - margin-bottom: 0; - margin-left: 1.5em; -} - -.guilabel, .menuselection { - font-family: sans-serif; -} - -.accelerator { - text-decoration: underline; -} - -.classifier { - font-style: oblique; -} - -.classifier:before { - font-style: normal; - margin: 0.5em; - content: ":"; -} - -abbr, acronym { - border-bottom: dotted 1px; - cursor: help; -} - -/* -- code displays --------------------------------------------------------- */ - -pre { - overflow: auto; - overflow-y: hidden; /* fixes display issues on Chrome browsers */ -} - -pre, div[class*="highlight-"] { - clear: both; -} - -span.pre { - -moz-hyphens: none; - -ms-hyphens: none; - -webkit-hyphens: none; - hyphens: none; -} - -div[class*="highlight-"] { - margin: 1em 0; -} - -td.linenos pre { - border: 0; - background-color: transparent; - color: #aaa; -} - -table.highlighttable { - display: block; -} - -table.highlighttable tbody { - display: block; -} - -table.highlighttable tr { - display: flex; -} - -table.highlighttable td { - margin: 0; - padding: 0; -} - -table.highlighttable td.linenos { - padding-right: 0.5em; -} - -table.highlighttable td.code { - flex: 1; - overflow: hidden; -} - -.highlight .hll { - display: block; -} - -div.highlight pre, -table.highlighttable pre { - margin: 0; -} - -div.code-block-caption + div { - margin-top: 0; -} - -div.code-block-caption { - margin-top: 1em; - padding: 2px 5px; - font-size: small; -} - -div.code-block-caption code { - background-color: transparent; -} - -table.highlighttable td.linenos, -span.linenos, -div.doctest > div.highlight span.gp { /* gp: Generic.Prompt */ - user-select: none; -} - -div.code-block-caption span.caption-number { - padding: 0.1em 0.3em; - font-style: italic; -} - -div.code-block-caption span.caption-text { -} - -div.literal-block-wrapper { - margin: 1em 0; -} - -code.descname { - background-color: transparent; - font-weight: bold; - font-size: 1.2em; -} - -code.descclassname { - background-color: transparent; -} - -code.xref, a code { - background-color: transparent; - font-weight: bold; -} - -h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { - background-color: transparent; -} - -.viewcode-link { - float: right; -} - -.viewcode-back { - float: right; - font-family: sans-serif; -} - -div.viewcode-block:target { - margin: -1px -10px; - padding: 0 10px; -} - -/* -- math display ---------------------------------------------------------- */ - -img.math { - vertical-align: middle; -} - -div.body div.math p { - text-align: center; -} - -span.eqno { - float: right; -} - -span.eqno a.headerlink { - position: absolute; - z-index: 1; -} - -div.math:hover a.headerlink { - visibility: visible; -} - -/* -- printout stylesheet --------------------------------------------------- */ - -@media print { - div.document, - div.documentwrapper, - div.bodywrapper { - margin: 0 !important; - width: 100%; - } - - div.sphinxsidebar, - div.related, - div.footer, - #top-link { - display: none; - } -} \ No newline at end of file diff --git a/docs/_static/css/badge_only.css b/docs/_static/css/badge_only.css deleted file mode 100644 index e380325bc6e273d9142c2369883d2a4b2a069cf1..0000000000000000000000000000000000000000 --- a/docs/_static/css/badge_only.css +++ /dev/null @@ -1 +0,0 @@ -.fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} \ No newline at end of file diff --git a/docs/_static/css/fonts/Roboto-Slab-Bold.woff b/docs/_static/css/fonts/Roboto-Slab-Bold.woff deleted file mode 100644 index 6cb60000181dbd348963953ac8ac54afb46c63d5..0000000000000000000000000000000000000000 Binary files a/docs/_static/css/fonts/Roboto-Slab-Bold.woff and /dev/null differ diff --git a/docs/_static/css/fonts/Roboto-Slab-Bold.woff2 b/docs/_static/css/fonts/Roboto-Slab-Bold.woff2 deleted file mode 100644 index 7059e23142aae3d8bad6067fc734a6cffec779c9..0000000000000000000000000000000000000000 Binary files a/docs/_static/css/fonts/Roboto-Slab-Bold.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/Roboto-Slab-Regular.woff b/docs/_static/css/fonts/Roboto-Slab-Regular.woff deleted file mode 100644 index f815f63f99da80ad2be69e4021023ec2981eaea0..0000000000000000000000000000000000000000 Binary files a/docs/_static/css/fonts/Roboto-Slab-Regular.woff and /dev/null differ diff --git a/docs/_static/css/fonts/Roboto-Slab-Regular.woff2 b/docs/_static/css/fonts/Roboto-Slab-Regular.woff2 deleted file mode 100644 index f2c76e5bda18a9842e24cd60d8787257da215ca7..0000000000000000000000000000000000000000 Binary files a/docs/_static/css/fonts/Roboto-Slab-Regular.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.eot b/docs/_static/css/fonts/fontawesome-webfont.eot deleted file mode 100644 index e9f60ca953f93e35eab4108bd414bc02ddcf3928..0000000000000000000000000000000000000000 Binary files a/docs/_static/css/fonts/fontawesome-webfont.eot and /dev/null differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.svg b/docs/_static/css/fonts/fontawesome-webfont.svg deleted file mode 100644 index 855c845e538b65548118279537a04eab2ec6ef0d..0000000000000000000000000000000000000000 --- a/docs/_static/css/fonts/fontawesome-webfont.svg +++ /dev/null @@ -1,2671 +0,0 @@ - - - - -Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 - By ,,, -Copyright Dave Gandy 2016. All rights reserved. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/_static/css/fonts/fontawesome-webfont.ttf b/docs/_static/css/fonts/fontawesome-webfont.ttf deleted file mode 100644 index 35acda2fa1196aad98c2adf4378a7611dd713aa3..0000000000000000000000000000000000000000 Binary files a/docs/_static/css/fonts/fontawesome-webfont.ttf and /dev/null differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.woff b/docs/_static/css/fonts/fontawesome-webfont.woff deleted file mode 100644 index 400014a4b06eee3d0c0d54402a47ab2601b2862b..0000000000000000000000000000000000000000 Binary files a/docs/_static/css/fonts/fontawesome-webfont.woff and /dev/null differ diff --git a/docs/_static/css/fonts/fontawesome-webfont.woff2 b/docs/_static/css/fonts/fontawesome-webfont.woff2 deleted file mode 100644 index 4d13fc60404b91e398a37200c4a77b645cfd9586..0000000000000000000000000000000000000000 Binary files a/docs/_static/css/fonts/fontawesome-webfont.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/lato-bold-italic.woff b/docs/_static/css/fonts/lato-bold-italic.woff deleted file mode 100644 index 88ad05b9ff413055b4d4e89dd3eec1c193fa20c6..0000000000000000000000000000000000000000 Binary files a/docs/_static/css/fonts/lato-bold-italic.woff and /dev/null differ diff --git a/docs/_static/css/fonts/lato-bold-italic.woff2 b/docs/_static/css/fonts/lato-bold-italic.woff2 deleted file mode 100644 index c4e3d804b57b625b16a36d767bfca6bbf63d414e..0000000000000000000000000000000000000000 Binary files a/docs/_static/css/fonts/lato-bold-italic.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/lato-bold.woff b/docs/_static/css/fonts/lato-bold.woff deleted file mode 100644 index c6dff51f063cc732fdb5fe786a8966de85f4ebec..0000000000000000000000000000000000000000 Binary files a/docs/_static/css/fonts/lato-bold.woff and /dev/null differ diff --git a/docs/_static/css/fonts/lato-bold.woff2 b/docs/_static/css/fonts/lato-bold.woff2 deleted file mode 100644 index bb195043cfc07fa52741c6144d7378b5ba8be4c5..0000000000000000000000000000000000000000 Binary files a/docs/_static/css/fonts/lato-bold.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/lato-normal-italic.woff b/docs/_static/css/fonts/lato-normal-italic.woff deleted file mode 100644 index 76114bc03362242c3325ecda6ce6d02bb737880f..0000000000000000000000000000000000000000 Binary files a/docs/_static/css/fonts/lato-normal-italic.woff and /dev/null differ diff --git a/docs/_static/css/fonts/lato-normal-italic.woff2 b/docs/_static/css/fonts/lato-normal-italic.woff2 deleted file mode 100644 index 3404f37e2e312757841abe20343588a7740768ca..0000000000000000000000000000000000000000 Binary files a/docs/_static/css/fonts/lato-normal-italic.woff2 and /dev/null differ diff --git a/docs/_static/css/fonts/lato-normal.woff b/docs/_static/css/fonts/lato-normal.woff deleted file mode 100644 index ae1307ff5f4c48678621c240f8972d5a6e20b22c..0000000000000000000000000000000000000000 Binary files a/docs/_static/css/fonts/lato-normal.woff and /dev/null differ diff --git a/docs/_static/css/fonts/lato-normal.woff2 b/docs/_static/css/fonts/lato-normal.woff2 deleted file mode 100644 index 3bf9843328a6359b6bd06e50010319c63da0d717..0000000000000000000000000000000000000000 Binary files a/docs/_static/css/fonts/lato-normal.woff2 and /dev/null differ diff --git a/docs/_static/css/theme.css b/docs/_static/css/theme.css deleted file mode 100644 index 8cd4f101a91d864f52b340128fddc1e6b39cddbb..0000000000000000000000000000000000000000 --- a/docs/_static/css/theme.css +++ /dev/null @@ -1,4 +0,0 @@ -html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a,.wy-menu-vertical li.current>a span.toctree-expand:before,.wy-menu-vertical li.on a,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li span.toctree-expand:before,.wy-nav-top a,.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! - * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li span.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p.caption .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a span.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a span.fa-pull-left.toctree-expand,.wy-menu-vertical li span.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p.caption .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a span.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a span.fa-pull-right.toctree-expand,.wy-menu-vertical li span.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p.caption .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a span.pull-left.toctree-expand,.wy-menu-vertical li.on a span.pull-left.toctree-expand,.wy-menu-vertical li span.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p.caption .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a span.pull-right.toctree-expand,.wy-menu-vertical li.on a span.pull-right.toctree-expand,.wy-menu-vertical li span.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li span.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li span.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li span.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li a span.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li span.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p.caption .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a span.toctree-expand,.btn .wy-menu-vertical li.on a span.toctree-expand,.btn .wy-menu-vertical li span.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p.caption .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a span.toctree-expand,.nav .wy-menu-vertical li.on a span.toctree-expand,.nav .wy-menu-vertical li span.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p.caption .btn .headerlink,.rst-content p.caption .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn span.toctree-expand,.wy-menu-vertical li.current>a .btn span.toctree-expand,.wy-menu-vertical li.current>a .nav span.toctree-expand,.wy-menu-vertical li .nav span.toctree-expand,.wy-menu-vertical li.on a .btn span.toctree-expand,.wy-menu-vertical li.on a .nav span.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p.caption .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li span.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p.caption .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li span.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p.caption .btn .fa-large.headerlink,.rst-content p.caption .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn span.fa-large.toctree-expand,.wy-menu-vertical li .nav span.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p.caption .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li span.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p.caption .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li span.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p.caption .btn .fa-spin.headerlink,.rst-content p.caption .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn span.fa-spin.toctree-expand,.wy-menu-vertical li .nav span.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p.caption .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li span.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p.caption .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li span.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p.caption .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li span.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p.caption .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini span.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol li,.rst-content ol.arabic li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content ol.arabic li p:last-child,.rst-content ol.arabic li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol li ul li,.rst-content ol.arabic li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs li{display:inline-block}.wy-breadcrumbs li.wy-breadcrumbs-aside{float:right}.wy-breadcrumbs li a{display:inline-block;padding:5px}.wy-breadcrumbs li a:first-child{padding-left:0}.rst-content .wy-breadcrumbs li tt,.wy-breadcrumbs li .rst-content tt,.wy-breadcrumbs li code{padding:5px;border:none;background:none}.rst-content .wy-breadcrumbs li tt.literal,.wy-breadcrumbs li .rst-content tt.literal,.wy-breadcrumbs li code.literal{color:#404040}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li span.toctree-expand{display:block;float:left;margin-left:-1.2em;font-size:.8em;line-height:1.6em;color:#4d4d4d}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover span.toctree-expand,.wy-menu-vertical li.on a:hover span.toctree-expand{color:grey}.wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand{display:block;font-size:.8em;line-height:1.6em;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover span.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover span.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover span.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover span.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover span.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover span.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover span.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 span.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 span.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover span.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active span.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search>a:hover{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p.caption .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p.caption .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version span.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content img{max-width:100%;height:auto}.rst-content div.figure{margin-bottom:24px}.rst-content div.figure p.caption{font-style:italic}.rst-content div.figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp{user-select:none;pointer-events:none}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink{visibility:hidden;font-size:14px}.rst-content .code-block-caption .headerlink:after,.rst-content .toctree-wrapper>p.caption .headerlink:after,.rst-content dl dt .headerlink:after,.rst-content h1 .headerlink:after,.rst-content h2 .headerlink:after,.rst-content h3 .headerlink:after,.rst-content h4 .headerlink:after,.rst-content h5 .headerlink:after,.rst-content h6 .headerlink:after,.rst-content p.caption .headerlink:after,.rst-content table>caption .headerlink:after{content:"\f0c1";font-family:FontAwesome}.rst-content .code-block-caption:hover .headerlink:after,.rst-content .toctree-wrapper>p.caption:hover .headerlink:after,.rst-content dl dt:hover .headerlink:after,.rst-content h1:hover .headerlink:after,.rst-content h2:hover .headerlink:after,.rst-content h3:hover .headerlink:after,.rst-content h4:hover .headerlink:after,.rst-content h5:hover .headerlink:after,.rst-content h6:hover .headerlink:after,.rst-content p.caption:hover .headerlink:after,.rst-content table>caption:hover .headerlink:after{visibility:visible}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .hlist{width:100%}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl dt span.classifier:before{content:" : "}html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.field-list>dt:after,html.writer-html5 .rst-content dl.footnote>dt:after{content:":"}html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.footnote>dt>span.brackets{margin-right:.5rem}html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{font-style:italic}html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.footnote>dd p,html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{font-size:inherit;line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code,html.writer-html4 .rst-content dl:not(.docutils) tt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} \ No newline at end of file diff --git a/docs/_static/doctools.js b/docs/_static/doctools.js deleted file mode 100644 index 7d88f807dc9ad9d1b5a320f2e27daa2f5196d875..0000000000000000000000000000000000000000 --- a/docs/_static/doctools.js +++ /dev/null @@ -1,316 +0,0 @@ -/* - * doctools.js - * ~~~~~~~~~~~ - * - * Sphinx JavaScript utilities for all documentation. - * - * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -/** - * select a different prefix for underscore - */ -$u = _.noConflict(); - -/** - * make the code below compatible with browsers without - * an installed firebug like debugger -if (!window.console || !console.firebug) { - var names = ["log", "debug", "info", "warn", "error", "assert", "dir", - "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", - "profile", "profileEnd"]; - window.console = {}; - for (var i = 0; i < names.length; ++i) - window.console[names[i]] = function() {}; -} - */ - -/** - * small helper function to urldecode strings - */ -jQuery.urldecode = function(x) { - return decodeURIComponent(x).replace(/\+/g, ' '); -}; - -/** - * small helper function to urlencode strings - */ -jQuery.urlencode = encodeURIComponent; - -/** - * This function returns the parsed url parameters of the - * current request. Multiple values per key are supported, - * it will always return arrays of strings for the value parts. - */ -jQuery.getQueryParameters = function(s) { - if (typeof s === 'undefined') - s = document.location.search; - var parts = s.substr(s.indexOf('?') + 1).split('&'); - var result = {}; - for (var i = 0; i < parts.length; i++) { - var tmp = parts[i].split('=', 2); - var key = jQuery.urldecode(tmp[0]); - var value = jQuery.urldecode(tmp[1]); - if (key in result) - result[key].push(value); - else - result[key] = [value]; - } - return result; -}; - -/** - * highlight a given string on a jquery object by wrapping it in - * span elements with the given class name. - */ -jQuery.fn.highlightText = function(text, className) { - function highlight(node, addItems) { - if (node.nodeType === 3) { - var val = node.nodeValue; - var pos = val.toLowerCase().indexOf(text); - if (pos >= 0 && - !jQuery(node.parentNode).hasClass(className) && - !jQuery(node.parentNode).hasClass("nohighlight")) { - var span; - var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); - if (isInSVG) { - span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); - } else { - span = document.createElement("span"); - span.className = className; - } - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - node.parentNode.insertBefore(span, node.parentNode.insertBefore( - document.createTextNode(val.substr(pos + text.length)), - node.nextSibling)); - node.nodeValue = val.substr(0, pos); - if (isInSVG) { - var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - var bbox = node.parentElement.getBBox(); - rect.x.baseVal.value = bbox.x; - rect.y.baseVal.value = bbox.y; - rect.width.baseVal.value = bbox.width; - rect.height.baseVal.value = bbox.height; - rect.setAttribute('class', className); - addItems.push({ - "parent": node.parentNode, - "target": rect}); - } - } - } - else if (!jQuery(node).is("button, select, textarea")) { - jQuery.each(node.childNodes, function() { - highlight(this, addItems); - }); - } - } - var addItems = []; - var result = this.each(function() { - highlight(this, addItems); - }); - for (var i = 0; i < addItems.length; ++i) { - jQuery(addItems[i].parent).before(addItems[i].target); - } - return result; -}; - -/* - * backward compatibility for jQuery.browser - * This will be supported until firefox bug is fixed. - */ -if (!jQuery.browser) { - jQuery.uaMatch = function(ua) { - ua = ua.toLowerCase(); - - var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || - /(webkit)[ \/]([\w.]+)/.exec(ua) || - /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || - /(msie) ([\w.]+)/.exec(ua) || - ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || - []; - - return { - browser: match[ 1 ] || "", - version: match[ 2 ] || "0" - }; - }; - jQuery.browser = {}; - jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; -} - -/** - * Small JavaScript module for the documentation. - */ -var Documentation = { - - init : function() { - this.fixFirefoxAnchorBug(); - this.highlightSearchWords(); - this.initIndexTable(); - if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) { - this.initOnKeyListeners(); - } - }, - - /** - * i18n support - */ - TRANSLATIONS : {}, - PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; }, - LOCALE : 'unknown', - - // gettext and ngettext don't access this so that the functions - // can safely bound to a different name (_ = Documentation.gettext) - gettext : function(string) { - var translated = Documentation.TRANSLATIONS[string]; - if (typeof translated === 'undefined') - return string; - return (typeof translated === 'string') ? translated : translated[0]; - }, - - ngettext : function(singular, plural, n) { - var translated = Documentation.TRANSLATIONS[singular]; - if (typeof translated === 'undefined') - return (n == 1) ? singular : plural; - return translated[Documentation.PLURALEXPR(n)]; - }, - - addTranslations : function(catalog) { - for (var key in catalog.messages) - this.TRANSLATIONS[key] = catalog.messages[key]; - this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); - this.LOCALE = catalog.locale; - }, - - /** - * add context elements like header anchor links - */ - addContextElements : function() { - $('div[id] > :header:first').each(function() { - $('\u00B6'). - attr('href', '#' + this.id). - attr('title', _('Permalink to this headline')). - appendTo(this); - }); - $('dt[id]').each(function() { - $('\u00B6'). - attr('href', '#' + this.id). - attr('title', _('Permalink to this definition')). - appendTo(this); - }); - }, - - /** - * workaround a firefox stupidity - * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075 - */ - fixFirefoxAnchorBug : function() { - if (document.location.hash && $.browser.mozilla) - window.setTimeout(function() { - document.location.href += ''; - }, 10); - }, - - /** - * highlight the search words provided in the url in the text - */ - highlightSearchWords : function() { - var params = $.getQueryParameters(); - var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; - if (terms.length) { - var body = $('div.body'); - if (!body.length) { - body = $('body'); - } - window.setTimeout(function() { - $.each(terms, function() { - body.highlightText(this.toLowerCase(), 'highlighted'); - }); - }, 10); - $('') - .appendTo($('#searchbox')); - } - }, - - /** - * init the domain index toggle buttons - */ - initIndexTable : function() { - var togglers = $('img.toggler').click(function() { - var src = $(this).attr('src'); - var idnum = $(this).attr('id').substr(7); - $('tr.cg-' + idnum).toggle(); - if (src.substr(-9) === 'minus.png') - $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); - else - $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); - }).css('display', ''); - if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { - togglers.click(); - } - }, - - /** - * helper function to hide the search marks again - */ - hideSearchWords : function() { - $('#searchbox .highlight-link').fadeOut(300); - $('span.highlighted').removeClass('highlighted'); - }, - - /** - * make the url absolute - */ - makeURL : function(relativeURL) { - return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; - }, - - /** - * get the current relative url - */ - getCurrentURL : function() { - var path = document.location.pathname; - var parts = path.split(/\//); - $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { - if (this === '..') - parts.pop(); - }); - var url = parts.join('/'); - return path.substring(url.lastIndexOf('/') + 1, path.length - 1); - }, - - initOnKeyListeners: function() { - $(document).keydown(function(event) { - var activeElementType = document.activeElement.tagName; - // don't navigate when in search box, textarea, dropdown or button - if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT' - && activeElementType !== 'BUTTON' && !event.altKey && !event.ctrlKey && !event.metaKey - && !event.shiftKey) { - switch (event.keyCode) { - case 37: // left - var prevHref = $('link[rel="prev"]').prop('href'); - if (prevHref) { - window.location.href = prevHref; - return false; - } - case 39: // right - var nextHref = $('link[rel="next"]').prop('href'); - if (nextHref) { - window.location.href = nextHref; - return false; - } - } - } - }); - } -}; - -// quick alias for translations -_ = Documentation.gettext; - -$(document).ready(function() { - Documentation.init(); -}); diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js deleted file mode 100644 index d634885e2c4564a9e5e17b2ed1cede5aa192f07d..0000000000000000000000000000000000000000 --- a/docs/_static/documentation_options.js +++ /dev/null @@ -1,12 +0,0 @@ -var DOCUMENTATION_OPTIONS = { - URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), - VERSION: '0.1', - LANGUAGE: 'zh_CN', - COLLAPSE_INDEX: false, - BUILDER: 'html', - FILE_SUFFIX: '.html', - LINK_SUFFIX: '.html', - HAS_SOURCE: true, - SOURCELINK_SUFFIX: '.txt', - NAVIGATION_WITH_KEYS: false -}; \ No newline at end of file diff --git a/docs/_static/file.png b/docs/_static/file.png deleted file mode 100644 index a858a410e4faa62ce324d814e4b816fff83a6fb3..0000000000000000000000000000000000000000 Binary files a/docs/_static/file.png and /dev/null differ diff --git a/docs/_static/fonts/FontAwesome.otf b/docs/_static/fonts/FontAwesome.otf deleted file mode 100644 index 401ec0f36e4f73b8efa40bd6f604fe80d286db70..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/FontAwesome.otf and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bold.eot b/docs/_static/fonts/Lato/lato-bold.eot deleted file mode 100644 index 3361183a419c188282a8545eaa8d8e298b8ffaab..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Lato/lato-bold.eot and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bold.ttf b/docs/_static/fonts/Lato/lato-bold.ttf deleted file mode 100644 index 29f691d5ed0c2d3d224423bb0288e6bd59292511..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Lato/lato-bold.ttf and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bold.woff b/docs/_static/fonts/Lato/lato-bold.woff deleted file mode 100644 index c6dff51f063cc732fdb5fe786a8966de85f4ebec..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Lato/lato-bold.woff and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bold.woff2 b/docs/_static/fonts/Lato/lato-bold.woff2 deleted file mode 100644 index bb195043cfc07fa52741c6144d7378b5ba8be4c5..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Lato/lato-bold.woff2 and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bolditalic.eot b/docs/_static/fonts/Lato/lato-bolditalic.eot deleted file mode 100644 index 3d4154936b42522fac84900c689a901ac12875c0..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Lato/lato-bolditalic.eot and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bolditalic.ttf b/docs/_static/fonts/Lato/lato-bolditalic.ttf deleted file mode 100644 index f402040b3e5360b90f3a12ca2afab2cd7244e16f..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Lato/lato-bolditalic.ttf and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bolditalic.woff b/docs/_static/fonts/Lato/lato-bolditalic.woff deleted file mode 100644 index 88ad05b9ff413055b4d4e89dd3eec1c193fa20c6..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Lato/lato-bolditalic.woff and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-bolditalic.woff2 b/docs/_static/fonts/Lato/lato-bolditalic.woff2 deleted file mode 100644 index c4e3d804b57b625b16a36d767bfca6bbf63d414e..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Lato/lato-bolditalic.woff2 and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-italic.eot b/docs/_static/fonts/Lato/lato-italic.eot deleted file mode 100644 index 3f826421a1d97b09797fad3d781a666a39eb45c9..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Lato/lato-italic.eot and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-italic.ttf b/docs/_static/fonts/Lato/lato-italic.ttf deleted file mode 100644 index b4bfc9b24aa993977662352c881c6e42f99f77e0..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Lato/lato-italic.ttf and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-italic.woff b/docs/_static/fonts/Lato/lato-italic.woff deleted file mode 100644 index 76114bc03362242c3325ecda6ce6d02bb737880f..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Lato/lato-italic.woff and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-italic.woff2 b/docs/_static/fonts/Lato/lato-italic.woff2 deleted file mode 100644 index 3404f37e2e312757841abe20343588a7740768ca..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Lato/lato-italic.woff2 and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-regular.eot b/docs/_static/fonts/Lato/lato-regular.eot deleted file mode 100644 index 11e3f2a5f0f9b8c7ef6affae8c543d20f7c112be..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Lato/lato-regular.eot and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-regular.ttf b/docs/_static/fonts/Lato/lato-regular.ttf deleted file mode 100644 index 74decd9ebb8d805201934266b3bda6a9d5831024..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Lato/lato-regular.ttf and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-regular.woff b/docs/_static/fonts/Lato/lato-regular.woff deleted file mode 100644 index ae1307ff5f4c48678621c240f8972d5a6e20b22c..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Lato/lato-regular.woff and /dev/null differ diff --git a/docs/_static/fonts/Lato/lato-regular.woff2 b/docs/_static/fonts/Lato/lato-regular.woff2 deleted file mode 100644 index 3bf9843328a6359b6bd06e50010319c63da0d717..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Lato/lato-regular.woff2 and /dev/null differ diff --git a/docs/_static/fonts/Roboto-Slab-Bold.woff b/docs/_static/fonts/Roboto-Slab-Bold.woff deleted file mode 100644 index 6cb60000181dbd348963953ac8ac54afb46c63d5..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Roboto-Slab-Bold.woff and /dev/null differ diff --git a/docs/_static/fonts/Roboto-Slab-Bold.woff2 b/docs/_static/fonts/Roboto-Slab-Bold.woff2 deleted file mode 100644 index 7059e23142aae3d8bad6067fc734a6cffec779c9..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Roboto-Slab-Bold.woff2 and /dev/null differ diff --git a/docs/_static/fonts/Roboto-Slab-Light.woff b/docs/_static/fonts/Roboto-Slab-Light.woff deleted file mode 100644 index 337d287116cd6540ec54f51b039a1c962dbcca7b..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Roboto-Slab-Light.woff and /dev/null differ diff --git a/docs/_static/fonts/Roboto-Slab-Light.woff2 b/docs/_static/fonts/Roboto-Slab-Light.woff2 deleted file mode 100644 index 20398aff31abb093abaafcc1d9bf5418ac437139..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Roboto-Slab-Light.woff2 and /dev/null differ diff --git a/docs/_static/fonts/Roboto-Slab-Regular.woff b/docs/_static/fonts/Roboto-Slab-Regular.woff deleted file mode 100644 index f815f63f99da80ad2be69e4021023ec2981eaea0..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Roboto-Slab-Regular.woff and /dev/null differ diff --git a/docs/_static/fonts/Roboto-Slab-Regular.woff2 b/docs/_static/fonts/Roboto-Slab-Regular.woff2 deleted file mode 100644 index f2c76e5bda18a9842e24cd60d8787257da215ca7..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Roboto-Slab-Regular.woff2 and /dev/null differ diff --git a/docs/_static/fonts/Roboto-Slab-Thin.woff b/docs/_static/fonts/Roboto-Slab-Thin.woff deleted file mode 100644 index 6b30ea630d5259514afaaa6cad000336226c2fd8..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Roboto-Slab-Thin.woff and /dev/null differ diff --git a/docs/_static/fonts/Roboto-Slab-Thin.woff2 b/docs/_static/fonts/Roboto-Slab-Thin.woff2 deleted file mode 100644 index 328f5bb042838089a40d0125728f9f47747d6c27..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/Roboto-Slab-Thin.woff2 and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot deleted file mode 100644 index 79dc8efed3447d6588baa2bb74122d56f3500038..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf deleted file mode 100644 index df5d1df2730433013f41bf2698cbe249b075aa02..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff deleted file mode 100644 index 6cb60000181dbd348963953ac8ac54afb46c63d5..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 deleted file mode 100644 index 7059e23142aae3d8bad6067fc734a6cffec779c9..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot deleted file mode 100644 index 2f7ca78a1eb34f0f98feb07ab1231d077b248940..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf deleted file mode 100644 index eb52a7907362cc3392eb74892883f5d9e260b638..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff deleted file mode 100644 index f815f63f99da80ad2be69e4021023ec2981eaea0..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff and /dev/null differ diff --git a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 b/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 deleted file mode 100644 index f2c76e5bda18a9842e24cd60d8787257da215ca7..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 and /dev/null differ diff --git a/docs/_static/fonts/fontawesome-webfont.eot b/docs/_static/fonts/fontawesome-webfont.eot deleted file mode 100644 index e9f60ca953f93e35eab4108bd414bc02ddcf3928..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/fontawesome-webfont.eot and /dev/null differ diff --git a/docs/_static/fonts/fontawesome-webfont.svg b/docs/_static/fonts/fontawesome-webfont.svg deleted file mode 100644 index 855c845e538b65548118279537a04eab2ec6ef0d..0000000000000000000000000000000000000000 --- a/docs/_static/fonts/fontawesome-webfont.svg +++ /dev/null @@ -1,2671 +0,0 @@ - - - - -Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 - By ,,, -Copyright Dave Gandy 2016. All rights reserved. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/_static/fonts/fontawesome-webfont.ttf b/docs/_static/fonts/fontawesome-webfont.ttf deleted file mode 100644 index 35acda2fa1196aad98c2adf4378a7611dd713aa3..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/fontawesome-webfont.ttf and /dev/null differ diff --git a/docs/_static/fonts/fontawesome-webfont.woff b/docs/_static/fonts/fontawesome-webfont.woff deleted file mode 100644 index 400014a4b06eee3d0c0d54402a47ab2601b2862b..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/fontawesome-webfont.woff and /dev/null differ diff --git a/docs/_static/fonts/fontawesome-webfont.woff2 b/docs/_static/fonts/fontawesome-webfont.woff2 deleted file mode 100644 index 4d13fc60404b91e398a37200c4a77b645cfd9586..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/fontawesome-webfont.woff2 and /dev/null differ diff --git a/docs/_static/fonts/lato-bold-italic.woff b/docs/_static/fonts/lato-bold-italic.woff deleted file mode 100644 index 88ad05b9ff413055b4d4e89dd3eec1c193fa20c6..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/lato-bold-italic.woff and /dev/null differ diff --git a/docs/_static/fonts/lato-bold-italic.woff2 b/docs/_static/fonts/lato-bold-italic.woff2 deleted file mode 100644 index c4e3d804b57b625b16a36d767bfca6bbf63d414e..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/lato-bold-italic.woff2 and /dev/null differ diff --git a/docs/_static/fonts/lato-bold.woff b/docs/_static/fonts/lato-bold.woff deleted file mode 100644 index c6dff51f063cc732fdb5fe786a8966de85f4ebec..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/lato-bold.woff and /dev/null differ diff --git a/docs/_static/fonts/lato-bold.woff2 b/docs/_static/fonts/lato-bold.woff2 deleted file mode 100644 index bb195043cfc07fa52741c6144d7378b5ba8be4c5..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/lato-bold.woff2 and /dev/null differ diff --git a/docs/_static/fonts/lato-normal-italic.woff b/docs/_static/fonts/lato-normal-italic.woff deleted file mode 100644 index 76114bc03362242c3325ecda6ce6d02bb737880f..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/lato-normal-italic.woff and /dev/null differ diff --git a/docs/_static/fonts/lato-normal-italic.woff2 b/docs/_static/fonts/lato-normal-italic.woff2 deleted file mode 100644 index 3404f37e2e312757841abe20343588a7740768ca..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/lato-normal-italic.woff2 and /dev/null differ diff --git a/docs/_static/fonts/lato-normal.woff b/docs/_static/fonts/lato-normal.woff deleted file mode 100644 index ae1307ff5f4c48678621c240f8972d5a6e20b22c..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/lato-normal.woff and /dev/null differ diff --git a/docs/_static/fonts/lato-normal.woff2 b/docs/_static/fonts/lato-normal.woff2 deleted file mode 100644 index 3bf9843328a6359b6bd06e50010319c63da0d717..0000000000000000000000000000000000000000 Binary files a/docs/_static/fonts/lato-normal.woff2 and /dev/null differ diff --git a/docs/_static/jquery-3.5.1.js b/docs/_static/jquery-3.5.1.js deleted file mode 100644 index 50937333b99a5e168ac9e8292b22edd7e96c3e6a..0000000000000000000000000000000000000000 --- a/docs/_static/jquery-3.5.1.js +++ /dev/null @@ -1,10872 +0,0 @@ -/*! - * jQuery JavaScript Library v3.5.1 - * https://jquery.com/ - * - * Includes Sizzle.js - * https://sizzlejs.com/ - * - * Copyright JS Foundation and other contributors - * Released under the MIT license - * https://jquery.org/license - * - * Date: 2020-05-04T22:49Z - */ -( function( global, factory ) { - - "use strict"; - - if ( typeof module === "object" && typeof module.exports === "object" ) { - - // For CommonJS and CommonJS-like environments where a proper `window` - // is present, execute the factory and get jQuery. - // For environments that do not have a `window` with a `document` - // (such as Node.js), expose a factory as module.exports. - // This accentuates the need for the creation of a real `window`. - // e.g. var jQuery = require("jquery")(window); - // See ticket #14549 for more info. - module.exports = global.document ? - factory( global, true ) : - function( w ) { - if ( !w.document ) { - throw new Error( "jQuery requires a window with a document" ); - } - return factory( w ); - }; - } else { - factory( global ); - } - -// Pass this if window is not defined yet -} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { - -// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 -// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode -// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common -// enough that all such attempts are guarded in a try block. -"use strict"; - -var arr = []; - -var getProto = Object.getPrototypeOf; - -var slice = arr.slice; - -var flat = arr.flat ? function( array ) { - return arr.flat.call( array ); -} : function( array ) { - return arr.concat.apply( [], array ); -}; - - -var push = arr.push; - -var indexOf = arr.indexOf; - -var class2type = {}; - -var toString = class2type.toString; - -var hasOwn = class2type.hasOwnProperty; - -var fnToString = hasOwn.toString; - -var ObjectFunctionString = fnToString.call( Object ); - -var support = {}; - -var isFunction = function isFunction( obj ) { - - // Support: Chrome <=57, Firefox <=52 - // In some browsers, typeof returns "function" for HTML elements - // (i.e., `typeof document.createElement( "object" ) === "function"`). - // We don't want to classify *any* DOM node as a function. - return typeof obj === "function" && typeof obj.nodeType !== "number"; - }; - - -var isWindow = function isWindow( obj ) { - return obj != null && obj === obj.window; - }; - - -var document = window.document; - - - - var preservedScriptAttributes = { - type: true, - src: true, - nonce: true, - noModule: true - }; - - function DOMEval( code, node, doc ) { - doc = doc || document; - - var i, val, - script = doc.createElement( "script" ); - - script.text = code; - if ( node ) { - for ( i in preservedScriptAttributes ) { - - // Support: Firefox 64+, Edge 18+ - // Some browsers don't support the "nonce" property on scripts. - // On the other hand, just using `getAttribute` is not enough as - // the `nonce` attribute is reset to an empty string whenever it - // becomes browsing-context connected. - // See https://github.com/whatwg/html/issues/2369 - // See https://html.spec.whatwg.org/#nonce-attributes - // The `node.getAttribute` check was added for the sake of - // `jQuery.globalEval` so that it can fake a nonce-containing node - // via an object. - val = node[ i ] || node.getAttribute && node.getAttribute( i ); - if ( val ) { - script.setAttribute( i, val ); - } - } - } - doc.head.appendChild( script ).parentNode.removeChild( script ); - } - - -function toType( obj ) { - if ( obj == null ) { - return obj + ""; - } - - // Support: Android <=2.3 only (functionish RegExp) - return typeof obj === "object" || typeof obj === "function" ? - class2type[ toString.call( obj ) ] || "object" : - typeof obj; -} -/* global Symbol */ -// Defining this global in .eslintrc.json would create a danger of using the global -// unguarded in another place, it seems safer to define global only for this module - - - -var - version = "3.5.1", - - // Define a local copy of jQuery - jQuery = function( selector, context ) { - - // The jQuery object is actually just the init constructor 'enhanced' - // Need init if jQuery is called (just allow error to be thrown if not included) - return new jQuery.fn.init( selector, context ); - }; - -jQuery.fn = jQuery.prototype = { - - // The current version of jQuery being used - jquery: version, - - constructor: jQuery, - - // The default length of a jQuery object is 0 - length: 0, - - toArray: function() { - return slice.call( this ); - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - - // Return all the elements in a clean array - if ( num == null ) { - return slice.call( this ); - } - - // Return just the one element from the set - return num < 0 ? this[ num + this.length ] : this[ num ]; - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems ) { - - // Build a new jQuery matched element set - var ret = jQuery.merge( this.constructor(), elems ); - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - - // Return the newly-formed element set - return ret; - }, - - // Execute a callback for every element in the matched set. - each: function( callback ) { - return jQuery.each( this, callback ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map( this, function( elem, i ) { - return callback.call( elem, i, elem ); - } ) ); - }, - - slice: function() { - return this.pushStack( slice.apply( this, arguments ) ); - }, - - first: function() { - return this.eq( 0 ); - }, - - last: function() { - return this.eq( -1 ); - }, - - even: function() { - return this.pushStack( jQuery.grep( this, function( _elem, i ) { - return ( i + 1 ) % 2; - } ) ); - }, - - odd: function() { - return this.pushStack( jQuery.grep( this, function( _elem, i ) { - return i % 2; - } ) ); - }, - - eq: function( i ) { - var len = this.length, - j = +i + ( i < 0 ? len : 0 ); - return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); - }, - - end: function() { - return this.prevObject || this.constructor(); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: push, - sort: arr.sort, - splice: arr.splice -}; - -jQuery.extend = jQuery.fn.extend = function() { - var options, name, src, copy, copyIsArray, clone, - target = arguments[ 0 ] || {}, - i = 1, - length = arguments.length, - deep = false; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - - // Skip the boolean and the target - target = arguments[ i ] || {}; - i++; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !isFunction( target ) ) { - target = {}; - } - - // Extend jQuery itself if only one argument is passed - if ( i === length ) { - target = this; - i--; - } - - for ( ; i < length; i++ ) { - - // Only deal with non-null/undefined values - if ( ( options = arguments[ i ] ) != null ) { - - // Extend the base object - for ( name in options ) { - copy = options[ name ]; - - // Prevent Object.prototype pollution - // Prevent never-ending loop - if ( name === "__proto__" || target === copy ) { - continue; - } - - // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( jQuery.isPlainObject( copy ) || - ( copyIsArray = Array.isArray( copy ) ) ) ) { - src = target[ name ]; - - // Ensure proper type for the source value - if ( copyIsArray && !Array.isArray( src ) ) { - clone = []; - } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { - clone = {}; - } else { - clone = src; - } - copyIsArray = false; - - // Never move original objects, clone them - target[ name ] = jQuery.extend( deep, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - - // Return the modified object - return target; -}; - -jQuery.extend( { - - // Unique for each copy of jQuery on the page - expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), - - // Assume jQuery is ready without the ready module - isReady: true, - - error: function( msg ) { - throw new Error( msg ); - }, - - noop: function() {}, - - isPlainObject: function( obj ) { - var proto, Ctor; - - // Detect obvious negatives - // Use toString instead of jQuery.type to catch host objects - if ( !obj || toString.call( obj ) !== "[object Object]" ) { - return false; - } - - proto = getProto( obj ); - - // Objects with no prototype (e.g., `Object.create( null )`) are plain - if ( !proto ) { - return true; - } - - // Objects with prototype are plain iff they were constructed by a global Object function - Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; - return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; - }, - - isEmptyObject: function( obj ) { - var name; - - for ( name in obj ) { - return false; - } - return true; - }, - - // Evaluates a script in a provided context; falls back to the global one - // if not specified. - globalEval: function( code, options, doc ) { - DOMEval( code, { nonce: options && options.nonce }, doc ); - }, - - each: function( obj, callback ) { - var length, i = 0; - - if ( isArrayLike( obj ) ) { - length = obj.length; - for ( ; i < length; i++ ) { - if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { - break; - } - } - } else { - for ( i in obj ) { - if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { - break; - } - } - } - - return obj; - }, - - // results is for internal usage only - makeArray: function( arr, results ) { - var ret = results || []; - - if ( arr != null ) { - if ( isArrayLike( Object( arr ) ) ) { - jQuery.merge( ret, - typeof arr === "string" ? - [ arr ] : arr - ); - } else { - push.call( ret, arr ); - } - } - - return ret; - }, - - inArray: function( elem, arr, i ) { - return arr == null ? -1 : indexOf.call( arr, elem, i ); - }, - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - merge: function( first, second ) { - var len = +second.length, - j = 0, - i = first.length; - - for ( ; j < len; j++ ) { - first[ i++ ] = second[ j ]; - } - - first.length = i; - - return first; - }, - - grep: function( elems, callback, invert ) { - var callbackInverse, - matches = [], - i = 0, - length = elems.length, - callbackExpect = !invert; - - // Go through the array, only saving the items - // that pass the validator function - for ( ; i < length; i++ ) { - callbackInverse = !callback( elems[ i ], i ); - if ( callbackInverse !== callbackExpect ) { - matches.push( elems[ i ] ); - } - } - - return matches; - }, - - // arg is for internal usage only - map: function( elems, callback, arg ) { - var length, value, - i = 0, - ret = []; - - // Go through the array, translating each of the items to their new values - if ( isArrayLike( elems ) ) { - length = elems.length; - for ( ; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret.push( value ); - } - } - - // Go through every key on the object, - } else { - for ( i in elems ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret.push( value ); - } - } - } - - // Flatten any nested arrays - return flat( ret ); - }, - - // A global GUID counter for objects - guid: 1, - - // jQuery.support is not used in Core but other projects attach their - // properties to it so it needs to exist. - support: support -} ); - -if ( typeof Symbol === "function" ) { - jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; -} - -// Populate the class2type map -jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), -function( _i, name ) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); -} ); - -function isArrayLike( obj ) { - - // Support: real iOS 8.2 only (not reproducible in simulator) - // `in` check used to prevent JIT error (gh-2145) - // hasOwn isn't used here due to false negatives - // regarding Nodelist length in IE - var length = !!obj && "length" in obj && obj.length, - type = toType( obj ); - - if ( isFunction( obj ) || isWindow( obj ) ) { - return false; - } - - return type === "array" || length === 0 || - typeof length === "number" && length > 0 && ( length - 1 ) in obj; -} -var Sizzle = -/*! - * Sizzle CSS Selector Engine v2.3.5 - * https://sizzlejs.com/ - * - * Copyright JS Foundation and other contributors - * Released under the MIT license - * https://js.foundation/ - * - * Date: 2020-03-14 - */ -( function( window ) { -var i, - support, - Expr, - getText, - isXML, - tokenize, - compile, - select, - outermostContext, - sortInput, - hasDuplicate, - - // Local document vars - setDocument, - document, - docElem, - documentIsHTML, - rbuggyQSA, - rbuggyMatches, - matches, - contains, - - // Instance-specific data - expando = "sizzle" + 1 * new Date(), - preferredDoc = window.document, - dirruns = 0, - done = 0, - classCache = createCache(), - tokenCache = createCache(), - compilerCache = createCache(), - nonnativeSelectorCache = createCache(), - sortOrder = function( a, b ) { - if ( a === b ) { - hasDuplicate = true; - } - return 0; - }, - - // Instance methods - hasOwn = ( {} ).hasOwnProperty, - arr = [], - pop = arr.pop, - pushNative = arr.push, - push = arr.push, - slice = arr.slice, - - // Use a stripped-down indexOf as it's faster than native - // https://jsperf.com/thor-indexof-vs-for/5 - indexOf = function( list, elem ) { - var i = 0, - len = list.length; - for ( ; i < len; i++ ) { - if ( list[ i ] === elem ) { - return i; - } - } - return -1; - }, - - booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|" + - "ismap|loop|multiple|open|readonly|required|scoped", - - // Regular expressions - - // http://www.w3.org/TR/css3-selectors/#whitespace - whitespace = "[\\x20\\t\\r\\n\\f]", - - // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram - identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + - "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", - - // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors - attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + - - // Operator (capture 2) - "*([*^$|!~]?=)" + whitespace + - - // "Attribute values must be CSS identifiers [capture 5] - // or strings [capture 3 or capture 4]" - "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + - whitespace + "*\\]", - - pseudos = ":(" + identifier + ")(?:\\((" + - - // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: - // 1. quoted (capture 3; capture 4 or capture 5) - "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + - - // 2. simple (capture 6) - "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + - - // 3. anything else (capture 2) - ".*" + - ")\\)|)", - - // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter - rwhitespace = new RegExp( whitespace + "+", "g" ), - rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + - whitespace + "+$", "g" ), - - rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), - rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + - "*" ), - rdescend = new RegExp( whitespace + "|>" ), - - rpseudo = new RegExp( pseudos ), - ridentifier = new RegExp( "^" + identifier + "$" ), - - matchExpr = { - "ID": new RegExp( "^#(" + identifier + ")" ), - "CLASS": new RegExp( "^\\.(" + identifier + ")" ), - "TAG": new RegExp( "^(" + identifier + "|[*])" ), - "ATTR": new RegExp( "^" + attributes ), - "PSEUDO": new RegExp( "^" + pseudos ), - "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + - whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + - whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), - "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), - - // For use in libraries implementing .is() - // We use this for POS matching in `select` - "needsContext": new RegExp( "^" + whitespace + - "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + - "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) - }, - - rhtml = /HTML$/i, - rinputs = /^(?:input|select|textarea|button)$/i, - rheader = /^h\d$/i, - - rnative = /^[^{]+\{\s*\[native \w/, - - // Easily-parseable/retrievable ID or TAG or CLASS selectors - rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - - rsibling = /[+~]/, - - // CSS escapes - // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters - runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + "?|\\\\([^\\r\\n\\f])", "g" ), - funescape = function( escape, nonHex ) { - var high = "0x" + escape.slice( 1 ) - 0x10000; - - return nonHex ? - - // Strip the backslash prefix from a non-hex escape sequence - nonHex : - - // Replace a hexadecimal escape sequence with the encoded Unicode code point - // Support: IE <=11+ - // For values outside the Basic Multilingual Plane (BMP), manually construct a - // surrogate pair - high < 0 ? - String.fromCharCode( high + 0x10000 ) : - String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); - }, - - // CSS string/identifier serialization - // https://drafts.csswg.org/cssom/#common-serializing-idioms - rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g, - fcssescape = function( ch, asCodePoint ) { - if ( asCodePoint ) { - - // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER - if ( ch === "\0" ) { - return "\uFFFD"; - } - - // Control characters and (dependent upon position) numbers get escaped as code points - return ch.slice( 0, -1 ) + "\\" + - ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; - } - - // Other potentially-special ASCII characters get backslash-escaped - return "\\" + ch; - }, - - // Used for iframes - // See setDocument() - // Removing the function wrapper causes a "Permission Denied" - // error in IE - unloadHandler = function() { - setDocument(); - }, - - inDisabledFieldset = addCombinator( - function( elem ) { - return elem.disabled === true && elem.nodeName.toLowerCase() === "fieldset"; - }, - { dir: "parentNode", next: "legend" } - ); - -// Optimize for push.apply( _, NodeList ) -try { - push.apply( - ( arr = slice.call( preferredDoc.childNodes ) ), - preferredDoc.childNodes - ); - - // Support: Android<4.0 - // Detect silently failing push.apply - // eslint-disable-next-line no-unused-expressions - arr[ preferredDoc.childNodes.length ].nodeType; -} catch ( e ) { - push = { apply: arr.length ? - - // Leverage slice if possible - function( target, els ) { - pushNative.apply( target, slice.call( els ) ); - } : - - // Support: IE<9 - // Otherwise append directly - function( target, els ) { - var j = target.length, - i = 0; - - // Can't trust NodeList.length - while ( ( target[ j++ ] = els[ i++ ] ) ) {} - target.length = j - 1; - } - }; -} - -function Sizzle( selector, context, results, seed ) { - var m, i, elem, nid, match, groups, newSelector, - newContext = context && context.ownerDocument, - - // nodeType defaults to 9, since context defaults to document - nodeType = context ? context.nodeType : 9; - - results = results || []; - - // Return early from calls with invalid selector or context - if ( typeof selector !== "string" || !selector || - nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { - - return results; - } - - // Try to shortcut find operations (as opposed to filters) in HTML documents - if ( !seed ) { - setDocument( context ); - context = context || document; - - if ( documentIsHTML ) { - - // If the selector is sufficiently simple, try using a "get*By*" DOM method - // (excepting DocumentFragment context, where the methods don't exist) - if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) { - - // ID selector - if ( ( m = match[ 1 ] ) ) { - - // Document context - if ( nodeType === 9 ) { - if ( ( elem = context.getElementById( m ) ) ) { - - // Support: IE, Opera, Webkit - // TODO: identify versions - // getElementById can match elements by name instead of ID - if ( elem.id === m ) { - results.push( elem ); - return results; - } - } else { - return results; - } - - // Element context - } else { - - // Support: IE, Opera, Webkit - // TODO: identify versions - // getElementById can match elements by name instead of ID - if ( newContext && ( elem = newContext.getElementById( m ) ) && - contains( context, elem ) && - elem.id === m ) { - - results.push( elem ); - return results; - } - } - - // Type selector - } else if ( match[ 2 ] ) { - push.apply( results, context.getElementsByTagName( selector ) ); - return results; - - // Class selector - } else if ( ( m = match[ 3 ] ) && support.getElementsByClassName && - context.getElementsByClassName ) { - - push.apply( results, context.getElementsByClassName( m ) ); - return results; - } - } - - // Take advantage of querySelectorAll - if ( support.qsa && - !nonnativeSelectorCache[ selector + " " ] && - ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) && - - // Support: IE 8 only - // Exclude object elements - ( nodeType !== 1 || context.nodeName.toLowerCase() !== "object" ) ) { - - newSelector = selector; - newContext = context; - - // qSA considers elements outside a scoping root when evaluating child or - // descendant combinators, which is not what we want. - // In such cases, we work around the behavior by prefixing every selector in the - // list with an ID selector referencing the scope context. - // The technique has to be used as well when a leading combinator is used - // as such selectors are not recognized by querySelectorAll. - // Thanks to Andrew Dupont for this technique. - if ( nodeType === 1 && - ( rdescend.test( selector ) || rcombinators.test( selector ) ) ) { - - // Expand context for sibling selectors - newContext = rsibling.test( selector ) && testContext( context.parentNode ) || - context; - - // We can use :scope instead of the ID hack if the browser - // supports it & if we're not changing the context. - if ( newContext !== context || !support.scope ) { - - // Capture the context ID, setting it first if necessary - if ( ( nid = context.getAttribute( "id" ) ) ) { - nid = nid.replace( rcssescape, fcssescape ); - } else { - context.setAttribute( "id", ( nid = expando ) ); - } - } - - // Prefix every selector in the list - groups = tokenize( selector ); - i = groups.length; - while ( i-- ) { - groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + - toSelector( groups[ i ] ); - } - newSelector = groups.join( "," ); - } - - try { - push.apply( results, - newContext.querySelectorAll( newSelector ) - ); - return results; - } catch ( qsaError ) { - nonnativeSelectorCache( selector, true ); - } finally { - if ( nid === expando ) { - context.removeAttribute( "id" ); - } - } - } - } - } - - // All others - return select( selector.replace( rtrim, "$1" ), context, results, seed ); -} - -/** - * Create key-value caches of limited size - * @returns {function(string, object)} Returns the Object data after storing it on itself with - * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) - * deleting the oldest entry - */ -function createCache() { - var keys = []; - - function cache( key, value ) { - - // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) - if ( keys.push( key + " " ) > Expr.cacheLength ) { - - // Only keep the most recent entries - delete cache[ keys.shift() ]; - } - return ( cache[ key + " " ] = value ); - } - return cache; -} - -/** - * Mark a function for special use by Sizzle - * @param {Function} fn The function to mark - */ -function markFunction( fn ) { - fn[ expando ] = true; - return fn; -} - -/** - * Support testing using an element - * @param {Function} fn Passed the created element and returns a boolean result - */ -function assert( fn ) { - var el = document.createElement( "fieldset" ); - - try { - return !!fn( el ); - } catch ( e ) { - return false; - } finally { - - // Remove from its parent by default - if ( el.parentNode ) { - el.parentNode.removeChild( el ); - } - - // release memory in IE - el = null; - } -} - -/** - * Adds the same handler for all of the specified attrs - * @param {String} attrs Pipe-separated list of attributes - * @param {Function} handler The method that will be applied - */ -function addHandle( attrs, handler ) { - var arr = attrs.split( "|" ), - i = arr.length; - - while ( i-- ) { - Expr.attrHandle[ arr[ i ] ] = handler; - } -} - -/** - * Checks document order of two siblings - * @param {Element} a - * @param {Element} b - * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b - */ -function siblingCheck( a, b ) { - var cur = b && a, - diff = cur && a.nodeType === 1 && b.nodeType === 1 && - a.sourceIndex - b.sourceIndex; - - // Use IE sourceIndex if available on both nodes - if ( diff ) { - return diff; - } - - // Check if b follows a - if ( cur ) { - while ( ( cur = cur.nextSibling ) ) { - if ( cur === b ) { - return -1; - } - } - } - - return a ? 1 : -1; -} - -/** - * Returns a function to use in pseudos for input types - * @param {String} type - */ -function createInputPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for buttons - * @param {String} type - */ -function createButtonPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return ( name === "input" || name === "button" ) && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for :enabled/:disabled - * @param {Boolean} disabled true for :disabled; false for :enabled - */ -function createDisabledPseudo( disabled ) { - - // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable - return function( elem ) { - - // Only certain elements can match :enabled or :disabled - // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled - // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled - if ( "form" in elem ) { - - // Check for inherited disabledness on relevant non-disabled elements: - // * listed form-associated elements in a disabled fieldset - // https://html.spec.whatwg.org/multipage/forms.html#category-listed - // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled - // * option elements in a disabled optgroup - // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled - // All such elements have a "form" property. - if ( elem.parentNode && elem.disabled === false ) { - - // Option elements defer to a parent optgroup if present - if ( "label" in elem ) { - if ( "label" in elem.parentNode ) { - return elem.parentNode.disabled === disabled; - } else { - return elem.disabled === disabled; - } - } - - // Support: IE 6 - 11 - // Use the isDisabled shortcut property to check for disabled fieldset ancestors - return elem.isDisabled === disabled || - - // Where there is no isDisabled, check manually - /* jshint -W018 */ - elem.isDisabled !== !disabled && - inDisabledFieldset( elem ) === disabled; - } - - return elem.disabled === disabled; - - // Try to winnow out elements that can't be disabled before trusting the disabled property. - // Some victims get caught in our net (label, legend, menu, track), but it shouldn't - // even exist on them, let alone have a boolean value. - } else if ( "label" in elem ) { - return elem.disabled === disabled; - } - - // Remaining elements are neither :enabled nor :disabled - return false; - }; -} - -/** - * Returns a function to use in pseudos for positionals - * @param {Function} fn - */ -function createPositionalPseudo( fn ) { - return markFunction( function( argument ) { - argument = +argument; - return markFunction( function( seed, matches ) { - var j, - matchIndexes = fn( [], seed.length, argument ), - i = matchIndexes.length; - - // Match elements found at the specified indexes - while ( i-- ) { - if ( seed[ ( j = matchIndexes[ i ] ) ] ) { - seed[ j ] = !( matches[ j ] = seed[ j ] ); - } - } - } ); - } ); -} - -/** - * Checks a node for validity as a Sizzle context - * @param {Element|Object=} context - * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value - */ -function testContext( context ) { - return context && typeof context.getElementsByTagName !== "undefined" && context; -} - -// Expose support vars for convenience -support = Sizzle.support = {}; - -/** - * Detects XML nodes - * @param {Element|Object} elem An element or a document - * @returns {Boolean} True iff elem is a non-HTML XML node - */ -isXML = Sizzle.isXML = function( elem ) { - var namespace = elem.namespaceURI, - docElem = ( elem.ownerDocument || elem ).documentElement; - - // Support: IE <=8 - // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes - // https://bugs.jquery.com/ticket/4833 - return !rhtml.test( namespace || docElem && docElem.nodeName || "HTML" ); -}; - -/** - * Sets document-related variables once based on the current document - * @param {Element|Object} [doc] An element or document object to use to set the document - * @returns {Object} Returns the current document - */ -setDocument = Sizzle.setDocument = function( node ) { - var hasCompare, subWindow, - doc = node ? node.ownerDocument || node : preferredDoc; - - // Return early if doc is invalid or already selected - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) { - return document; - } - - // Update global variables - document = doc; - docElem = document.documentElement; - documentIsHTML = !isXML( document ); - - // Support: IE 9 - 11+, Edge 12 - 18+ - // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( preferredDoc != document && - ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) { - - // Support: IE 11, Edge - if ( subWindow.addEventListener ) { - subWindow.addEventListener( "unload", unloadHandler, false ); - - // Support: IE 9 - 10 only - } else if ( subWindow.attachEvent ) { - subWindow.attachEvent( "onunload", unloadHandler ); - } - } - - // Support: IE 8 - 11+, Edge 12 - 18+, Chrome <=16 - 25 only, Firefox <=3.6 - 31 only, - // Safari 4 - 5 only, Opera <=11.6 - 12.x only - // IE/Edge & older browsers don't support the :scope pseudo-class. - // Support: Safari 6.0 only - // Safari 6.0 supports :scope but it's an alias of :root there. - support.scope = assert( function( el ) { - docElem.appendChild( el ).appendChild( document.createElement( "div" ) ); - return typeof el.querySelectorAll !== "undefined" && - !el.querySelectorAll( ":scope fieldset div" ).length; - } ); - - /* Attributes - ---------------------------------------------------------------------- */ - - // Support: IE<8 - // Verify that getAttribute really returns attributes and not properties - // (excepting IE8 booleans) - support.attributes = assert( function( el ) { - el.className = "i"; - return !el.getAttribute( "className" ); - } ); - - /* getElement(s)By* - ---------------------------------------------------------------------- */ - - // Check if getElementsByTagName("*") returns only elements - support.getElementsByTagName = assert( function( el ) { - el.appendChild( document.createComment( "" ) ); - return !el.getElementsByTagName( "*" ).length; - } ); - - // Support: IE<9 - support.getElementsByClassName = rnative.test( document.getElementsByClassName ); - - // Support: IE<10 - // Check if getElementById returns elements by name - // The broken getElementById methods don't pick up programmatically-set names, - // so use a roundabout getElementsByName test - support.getById = assert( function( el ) { - docElem.appendChild( el ).id = expando; - return !document.getElementsByName || !document.getElementsByName( expando ).length; - } ); - - // ID filter and find - if ( support.getById ) { - Expr.filter[ "ID" ] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - return elem.getAttribute( "id" ) === attrId; - }; - }; - Expr.find[ "ID" ] = function( id, context ) { - if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { - var elem = context.getElementById( id ); - return elem ? [ elem ] : []; - } - }; - } else { - Expr.filter[ "ID" ] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - var node = typeof elem.getAttributeNode !== "undefined" && - elem.getAttributeNode( "id" ); - return node && node.value === attrId; - }; - }; - - // Support: IE 6 - 7 only - // getElementById is not reliable as a find shortcut - Expr.find[ "ID" ] = function( id, context ) { - if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { - var node, i, elems, - elem = context.getElementById( id ); - - if ( elem ) { - - // Verify the id attribute - node = elem.getAttributeNode( "id" ); - if ( node && node.value === id ) { - return [ elem ]; - } - - // Fall back on getElementsByName - elems = context.getElementsByName( id ); - i = 0; - while ( ( elem = elems[ i++ ] ) ) { - node = elem.getAttributeNode( "id" ); - if ( node && node.value === id ) { - return [ elem ]; - } - } - } - - return []; - } - }; - } - - // Tag - Expr.find[ "TAG" ] = support.getElementsByTagName ? - function( tag, context ) { - if ( typeof context.getElementsByTagName !== "undefined" ) { - return context.getElementsByTagName( tag ); - - // DocumentFragment nodes don't have gEBTN - } else if ( support.qsa ) { - return context.querySelectorAll( tag ); - } - } : - - function( tag, context ) { - var elem, - tmp = [], - i = 0, - - // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too - results = context.getElementsByTagName( tag ); - - // Filter out possible comments - if ( tag === "*" ) { - while ( ( elem = results[ i++ ] ) ) { - if ( elem.nodeType === 1 ) { - tmp.push( elem ); - } - } - - return tmp; - } - return results; - }; - - // Class - Expr.find[ "CLASS" ] = support.getElementsByClassName && function( className, context ) { - if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { - return context.getElementsByClassName( className ); - } - }; - - /* QSA/matchesSelector - ---------------------------------------------------------------------- */ - - // QSA and matchesSelector support - - // matchesSelector(:active) reports false when true (IE9/Opera 11.5) - rbuggyMatches = []; - - // qSa(:focus) reports false when true (Chrome 21) - // We allow this because of a bug in IE8/9 that throws an error - // whenever `document.activeElement` is accessed on an iframe - // So, we allow :focus to pass through QSA all the time to avoid the IE error - // See https://bugs.jquery.com/ticket/13378 - rbuggyQSA = []; - - if ( ( support.qsa = rnative.test( document.querySelectorAll ) ) ) { - - // Build QSA regex - // Regex strategy adopted from Diego Perini - assert( function( el ) { - - var input; - - // Select is set to empty string on purpose - // This is to test IE's treatment of not explicitly - // setting a boolean content attribute, - // since its presence should be enough - // https://bugs.jquery.com/ticket/12359 - docElem.appendChild( el ).innerHTML = "" + - ""; - - // Support: IE8, Opera 11-12.16 - // Nothing should be selected when empty strings follow ^= or $= or *= - // The test attribute must be unknown in Opera but "safe" for WinRT - // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section - if ( el.querySelectorAll( "[msallowcapture^='']" ).length ) { - rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); - } - - // Support: IE8 - // Boolean attributes and "value" are not treated correctly - if ( !el.querySelectorAll( "[selected]" ).length ) { - rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); - } - - // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ - if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { - rbuggyQSA.push( "~=" ); - } - - // Support: IE 11+, Edge 15 - 18+ - // IE 11/Edge don't find elements on a `[name='']` query in some cases. - // Adding a temporary attribute to the document before the selection works - // around the issue. - // Interestingly, IE 10 & older don't seem to have the issue. - input = document.createElement( "input" ); - input.setAttribute( "name", "" ); - el.appendChild( input ); - if ( !el.querySelectorAll( "[name='']" ).length ) { - rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" + - whitespace + "*(?:''|\"\")" ); - } - - // Webkit/Opera - :checked should return selected option elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - // IE8 throws error here and will not see later tests - if ( !el.querySelectorAll( ":checked" ).length ) { - rbuggyQSA.push( ":checked" ); - } - - // Support: Safari 8+, iOS 8+ - // https://bugs.webkit.org/show_bug.cgi?id=136851 - // In-page `selector#id sibling-combinator selector` fails - if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { - rbuggyQSA.push( ".#.+[+~]" ); - } - - // Support: Firefox <=3.6 - 5 only - // Old Firefox doesn't throw on a badly-escaped identifier. - el.querySelectorAll( "\\\f" ); - rbuggyQSA.push( "[\\r\\n\\f]" ); - } ); - - assert( function( el ) { - el.innerHTML = "" + - ""; - - // Support: Windows 8 Native Apps - // The type and name attributes are restricted during .innerHTML assignment - var input = document.createElement( "input" ); - input.setAttribute( "type", "hidden" ); - el.appendChild( input ).setAttribute( "name", "D" ); - - // Support: IE8 - // Enforce case-sensitivity of name attribute - if ( el.querySelectorAll( "[name=d]" ).length ) { - rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); - } - - // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) - // IE8 throws error here and will not see later tests - if ( el.querySelectorAll( ":enabled" ).length !== 2 ) { - rbuggyQSA.push( ":enabled", ":disabled" ); - } - - // Support: IE9-11+ - // IE's :disabled selector does not pick up the children of disabled fieldsets - docElem.appendChild( el ).disabled = true; - if ( el.querySelectorAll( ":disabled" ).length !== 2 ) { - rbuggyQSA.push( ":enabled", ":disabled" ); - } - - // Support: Opera 10 - 11 only - // Opera 10-11 does not throw on post-comma invalid pseudos - el.querySelectorAll( "*,:x" ); - rbuggyQSA.push( ",.*:" ); - } ); - } - - if ( ( support.matchesSelector = rnative.test( ( matches = docElem.matches || - docElem.webkitMatchesSelector || - docElem.mozMatchesSelector || - docElem.oMatchesSelector || - docElem.msMatchesSelector ) ) ) ) { - - assert( function( el ) { - - // Check to see if it's possible to do matchesSelector - // on a disconnected node (IE 9) - support.disconnectedMatch = matches.call( el, "*" ); - - // This should fail with an exception - // Gecko does not error, returns false instead - matches.call( el, "[s!='']:x" ); - rbuggyMatches.push( "!=", pseudos ); - } ); - } - - rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) ); - rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join( "|" ) ); - - /* Contains - ---------------------------------------------------------------------- */ - hasCompare = rnative.test( docElem.compareDocumentPosition ); - - // Element contains another - // Purposefully self-exclusive - // As in, an element does not contain itself - contains = hasCompare || rnative.test( docElem.contains ) ? - function( a, b ) { - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - ) ); - } : - function( a, b ) { - if ( b ) { - while ( ( b = b.parentNode ) ) { - if ( b === a ) { - return true; - } - } - } - return false; - }; - - /* Sorting - ---------------------------------------------------------------------- */ - - // Document order sorting - sortOrder = hasCompare ? - function( a, b ) { - - // Flag for duplicate removal - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - // Sort on method existence if only one input has compareDocumentPosition - var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; - if ( compare ) { - return compare; - } - - // Calculate position if both inputs belong to the same document - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ? - a.compareDocumentPosition( b ) : - - // Otherwise we know they are disconnected - 1; - - // Disconnected nodes - if ( compare & 1 || - ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) { - - // Choose the first element that is related to our preferred document - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( a == document || a.ownerDocument == preferredDoc && - contains( preferredDoc, a ) ) { - return -1; - } - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( b == document || b.ownerDocument == preferredDoc && - contains( preferredDoc, b ) ) { - return 1; - } - - // Maintain original order - return sortInput ? - ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : - 0; - } - - return compare & 4 ? -1 : 1; - } : - function( a, b ) { - - // Exit early if the nodes are identical - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - var cur, - i = 0, - aup = a.parentNode, - bup = b.parentNode, - ap = [ a ], - bp = [ b ]; - - // Parentless nodes are either documents or disconnected - if ( !aup || !bup ) { - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - /* eslint-disable eqeqeq */ - return a == document ? -1 : - b == document ? 1 : - /* eslint-enable eqeqeq */ - aup ? -1 : - bup ? 1 : - sortInput ? - ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : - 0; - - // If the nodes are siblings, we can do a quick check - } else if ( aup === bup ) { - return siblingCheck( a, b ); - } - - // Otherwise we need full lists of their ancestors for comparison - cur = a; - while ( ( cur = cur.parentNode ) ) { - ap.unshift( cur ); - } - cur = b; - while ( ( cur = cur.parentNode ) ) { - bp.unshift( cur ); - } - - // Walk down the tree looking for a discrepancy - while ( ap[ i ] === bp[ i ] ) { - i++; - } - - return i ? - - // Do a sibling check if the nodes have a common ancestor - siblingCheck( ap[ i ], bp[ i ] ) : - - // Otherwise nodes in our document sort first - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - /* eslint-disable eqeqeq */ - ap[ i ] == preferredDoc ? -1 : - bp[ i ] == preferredDoc ? 1 : - /* eslint-enable eqeqeq */ - 0; - }; - - return document; -}; - -Sizzle.matches = function( expr, elements ) { - return Sizzle( expr, null, null, elements ); -}; - -Sizzle.matchesSelector = function( elem, expr ) { - setDocument( elem ); - - if ( support.matchesSelector && documentIsHTML && - !nonnativeSelectorCache[ expr + " " ] && - ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && - ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { - - try { - var ret = matches.call( elem, expr ); - - // IE 9's matchesSelector returns false on disconnected nodes - if ( ret || support.disconnectedMatch || - - // As well, disconnected nodes are said to be in a document - // fragment in IE 9 - elem.document && elem.document.nodeType !== 11 ) { - return ret; - } - } catch ( e ) { - nonnativeSelectorCache( expr, true ); - } - } - - return Sizzle( expr, document, null, [ elem ] ).length > 0; -}; - -Sizzle.contains = function( context, elem ) { - - // Set document vars if needed - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( ( context.ownerDocument || context ) != document ) { - setDocument( context ); - } - return contains( context, elem ); -}; - -Sizzle.attr = function( elem, name ) { - - // Set document vars if needed - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( ( elem.ownerDocument || elem ) != document ) { - setDocument( elem ); - } - - var fn = Expr.attrHandle[ name.toLowerCase() ], - - // Don't get fooled by Object.prototype properties (jQuery #13807) - val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? - fn( elem, name, !documentIsHTML ) : - undefined; - - return val !== undefined ? - val : - support.attributes || !documentIsHTML ? - elem.getAttribute( name ) : - ( val = elem.getAttributeNode( name ) ) && val.specified ? - val.value : - null; -}; - -Sizzle.escape = function( sel ) { - return ( sel + "" ).replace( rcssescape, fcssescape ); -}; - -Sizzle.error = function( msg ) { - throw new Error( "Syntax error, unrecognized expression: " + msg ); -}; - -/** - * Document sorting and removing duplicates - * @param {ArrayLike} results - */ -Sizzle.uniqueSort = function( results ) { - var elem, - duplicates = [], - j = 0, - i = 0; - - // Unless we *know* we can detect duplicates, assume their presence - hasDuplicate = !support.detectDuplicates; - sortInput = !support.sortStable && results.slice( 0 ); - results.sort( sortOrder ); - - if ( hasDuplicate ) { - while ( ( elem = results[ i++ ] ) ) { - if ( elem === results[ i ] ) { - j = duplicates.push( i ); - } - } - while ( j-- ) { - results.splice( duplicates[ j ], 1 ); - } - } - - // Clear input after sorting to release objects - // See https://github.com/jquery/sizzle/pull/225 - sortInput = null; - - return results; -}; - -/** - * Utility function for retrieving the text value of an array of DOM nodes - * @param {Array|Element} elem - */ -getText = Sizzle.getText = function( elem ) { - var node, - ret = "", - i = 0, - nodeType = elem.nodeType; - - if ( !nodeType ) { - - // If no nodeType, this is expected to be an array - while ( ( node = elem[ i++ ] ) ) { - - // Do not traverse comment nodes - ret += getText( node ); - } - } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { - - // Use textContent for elements - // innerText usage removed for consistency of new lines (jQuery #11153) - if ( typeof elem.textContent === "string" ) { - return elem.textContent; - } else { - - // Traverse its children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - ret += getText( elem ); - } - } - } else if ( nodeType === 3 || nodeType === 4 ) { - return elem.nodeValue; - } - - // Do not include comment or processing instruction nodes - - return ret; -}; - -Expr = Sizzle.selectors = { - - // Can be adjusted by the user - cacheLength: 50, - - createPseudo: markFunction, - - match: matchExpr, - - attrHandle: {}, - - find: {}, - - relative: { - ">": { dir: "parentNode", first: true }, - " ": { dir: "parentNode" }, - "+": { dir: "previousSibling", first: true }, - "~": { dir: "previousSibling" } - }, - - preFilter: { - "ATTR": function( match ) { - match[ 1 ] = match[ 1 ].replace( runescape, funescape ); - - // Move the given value to match[3] whether quoted or unquoted - match[ 3 ] = ( match[ 3 ] || match[ 4 ] || - match[ 5 ] || "" ).replace( runescape, funescape ); - - if ( match[ 2 ] === "~=" ) { - match[ 3 ] = " " + match[ 3 ] + " "; - } - - return match.slice( 0, 4 ); - }, - - "CHILD": function( match ) { - - /* matches from matchExpr["CHILD"] - 1 type (only|nth|...) - 2 what (child|of-type) - 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) - 4 xn-component of xn+y argument ([+-]?\d*n|) - 5 sign of xn-component - 6 x of xn-component - 7 sign of y-component - 8 y of y-component - */ - match[ 1 ] = match[ 1 ].toLowerCase(); - - if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { - - // nth-* requires argument - if ( !match[ 3 ] ) { - Sizzle.error( match[ 0 ] ); - } - - // numeric x and y parameters for Expr.filter.CHILD - // remember that false/true cast respectively to 0/1 - match[ 4 ] = +( match[ 4 ] ? - match[ 5 ] + ( match[ 6 ] || 1 ) : - 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) ); - match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); - - // other types prohibit arguments - } else if ( match[ 3 ] ) { - Sizzle.error( match[ 0 ] ); - } - - return match; - }, - - "PSEUDO": function( match ) { - var excess, - unquoted = !match[ 6 ] && match[ 2 ]; - - if ( matchExpr[ "CHILD" ].test( match[ 0 ] ) ) { - return null; - } - - // Accept quoted arguments as-is - if ( match[ 3 ] ) { - match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; - - // Strip excess characters from unquoted arguments - } else if ( unquoted && rpseudo.test( unquoted ) && - - // Get excess from tokenize (recursively) - ( excess = tokenize( unquoted, true ) ) && - - // advance to the next closing parenthesis - ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) { - - // excess is a negative index - match[ 0 ] = match[ 0 ].slice( 0, excess ); - match[ 2 ] = unquoted.slice( 0, excess ); - } - - // Return only captures needed by the pseudo filter method (type and argument) - return match.slice( 0, 3 ); - } - }, - - filter: { - - "TAG": function( nodeNameSelector ) { - var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); - return nodeNameSelector === "*" ? - function() { - return true; - } : - function( elem ) { - return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; - }; - }, - - "CLASS": function( className ) { - var pattern = classCache[ className + " " ]; - - return pattern || - ( pattern = new RegExp( "(^|" + whitespace + - ")" + className + "(" + whitespace + "|$)" ) ) && classCache( - className, function( elem ) { - return pattern.test( - typeof elem.className === "string" && elem.className || - typeof elem.getAttribute !== "undefined" && - elem.getAttribute( "class" ) || - "" - ); - } ); - }, - - "ATTR": function( name, operator, check ) { - return function( elem ) { - var result = Sizzle.attr( elem, name ); - - if ( result == null ) { - return operator === "!="; - } - if ( !operator ) { - return true; - } - - result += ""; - - /* eslint-disable max-len */ - - return operator === "=" ? result === check : - operator === "!=" ? result !== check : - operator === "^=" ? check && result.indexOf( check ) === 0 : - operator === "*=" ? check && result.indexOf( check ) > -1 : - operator === "$=" ? check && result.slice( -check.length ) === check : - operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : - operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : - false; - /* eslint-enable max-len */ - - }; - }, - - "CHILD": function( type, what, _argument, first, last ) { - var simple = type.slice( 0, 3 ) !== "nth", - forward = type.slice( -4 ) !== "last", - ofType = what === "of-type"; - - return first === 1 && last === 0 ? - - // Shortcut for :nth-*(n) - function( elem ) { - return !!elem.parentNode; - } : - - function( elem, _context, xml ) { - var cache, uniqueCache, outerCache, node, nodeIndex, start, - dir = simple !== forward ? "nextSibling" : "previousSibling", - parent = elem.parentNode, - name = ofType && elem.nodeName.toLowerCase(), - useCache = !xml && !ofType, - diff = false; - - if ( parent ) { - - // :(first|last|only)-(child|of-type) - if ( simple ) { - while ( dir ) { - node = elem; - while ( ( node = node[ dir ] ) ) { - if ( ofType ? - node.nodeName.toLowerCase() === name : - node.nodeType === 1 ) { - - return false; - } - } - - // Reverse direction for :only-* (if we haven't yet done so) - start = dir = type === "only" && !start && "nextSibling"; - } - return true; - } - - start = [ forward ? parent.firstChild : parent.lastChild ]; - - // non-xml :nth-child(...) stores cache data on `parent` - if ( forward && useCache ) { - - // Seek `elem` from a previously-cached index - - // ...in a gzip-friendly way - node = parent; - outerCache = node[ expando ] || ( node[ expando ] = {} ); - - // Support: IE <9 only - // Defend against cloned attroperties (jQuery gh-1709) - uniqueCache = outerCache[ node.uniqueID ] || - ( outerCache[ node.uniqueID ] = {} ); - - cache = uniqueCache[ type ] || []; - nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; - diff = nodeIndex && cache[ 2 ]; - node = nodeIndex && parent.childNodes[ nodeIndex ]; - - while ( ( node = ++nodeIndex && node && node[ dir ] || - - // Fallback to seeking `elem` from the start - ( diff = nodeIndex = 0 ) || start.pop() ) ) { - - // When found, cache indexes on `parent` and break - if ( node.nodeType === 1 && ++diff && node === elem ) { - uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; - break; - } - } - - } else { - - // Use previously-cached element index if available - if ( useCache ) { - - // ...in a gzip-friendly way - node = elem; - outerCache = node[ expando ] || ( node[ expando ] = {} ); - - // Support: IE <9 only - // Defend against cloned attroperties (jQuery gh-1709) - uniqueCache = outerCache[ node.uniqueID ] || - ( outerCache[ node.uniqueID ] = {} ); - - cache = uniqueCache[ type ] || []; - nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; - diff = nodeIndex; - } - - // xml :nth-child(...) - // or :nth-last-child(...) or :nth(-last)?-of-type(...) - if ( diff === false ) { - - // Use the same loop as above to seek `elem` from the start - while ( ( node = ++nodeIndex && node && node[ dir ] || - ( diff = nodeIndex = 0 ) || start.pop() ) ) { - - if ( ( ofType ? - node.nodeName.toLowerCase() === name : - node.nodeType === 1 ) && - ++diff ) { - - // Cache the index of each encountered element - if ( useCache ) { - outerCache = node[ expando ] || - ( node[ expando ] = {} ); - - // Support: IE <9 only - // Defend against cloned attroperties (jQuery gh-1709) - uniqueCache = outerCache[ node.uniqueID ] || - ( outerCache[ node.uniqueID ] = {} ); - - uniqueCache[ type ] = [ dirruns, diff ]; - } - - if ( node === elem ) { - break; - } - } - } - } - } - - // Incorporate the offset, then check against cycle size - diff -= last; - return diff === first || ( diff % first === 0 && diff / first >= 0 ); - } - }; - }, - - "PSEUDO": function( pseudo, argument ) { - - // pseudo-class names are case-insensitive - // http://www.w3.org/TR/selectors/#pseudo-classes - // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters - // Remember that setFilters inherits from pseudos - var args, - fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || - Sizzle.error( "unsupported pseudo: " + pseudo ); - - // The user may use createPseudo to indicate that - // arguments are needed to create the filter function - // just as Sizzle does - if ( fn[ expando ] ) { - return fn( argument ); - } - - // But maintain support for old signatures - if ( fn.length > 1 ) { - args = [ pseudo, pseudo, "", argument ]; - return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? - markFunction( function( seed, matches ) { - var idx, - matched = fn( seed, argument ), - i = matched.length; - while ( i-- ) { - idx = indexOf( seed, matched[ i ] ); - seed[ idx ] = !( matches[ idx ] = matched[ i ] ); - } - } ) : - function( elem ) { - return fn( elem, 0, args ); - }; - } - - return fn; - } - }, - - pseudos: { - - // Potentially complex pseudos - "not": markFunction( function( selector ) { - - // Trim the selector passed to compile - // to avoid treating leading and trailing - // spaces as combinators - var input = [], - results = [], - matcher = compile( selector.replace( rtrim, "$1" ) ); - - return matcher[ expando ] ? - markFunction( function( seed, matches, _context, xml ) { - var elem, - unmatched = matcher( seed, null, xml, [] ), - i = seed.length; - - // Match elements unmatched by `matcher` - while ( i-- ) { - if ( ( elem = unmatched[ i ] ) ) { - seed[ i ] = !( matches[ i ] = elem ); - } - } - } ) : - function( elem, _context, xml ) { - input[ 0 ] = elem; - matcher( input, null, xml, results ); - - // Don't keep the element (issue #299) - input[ 0 ] = null; - return !results.pop(); - }; - } ), - - "has": markFunction( function( selector ) { - return function( elem ) { - return Sizzle( selector, elem ).length > 0; - }; - } ), - - "contains": markFunction( function( text ) { - text = text.replace( runescape, funescape ); - return function( elem ) { - return ( elem.textContent || getText( elem ) ).indexOf( text ) > -1; - }; - } ), - - // "Whether an element is represented by a :lang() selector - // is based solely on the element's language value - // being equal to the identifier C, - // or beginning with the identifier C immediately followed by "-". - // The matching of C against the element's language value is performed case-insensitively. - // The identifier C does not have to be a valid language name." - // http://www.w3.org/TR/selectors/#lang-pseudo - "lang": markFunction( function( lang ) { - - // lang value must be a valid identifier - if ( !ridentifier.test( lang || "" ) ) { - Sizzle.error( "unsupported lang: " + lang ); - } - lang = lang.replace( runescape, funescape ).toLowerCase(); - return function( elem ) { - var elemLang; - do { - if ( ( elemLang = documentIsHTML ? - elem.lang : - elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) { - - elemLang = elemLang.toLowerCase(); - return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; - } - } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 ); - return false; - }; - } ), - - // Miscellaneous - "target": function( elem ) { - var hash = window.location && window.location.hash; - return hash && hash.slice( 1 ) === elem.id; - }, - - "root": function( elem ) { - return elem === docElem; - }, - - "focus": function( elem ) { - return elem === document.activeElement && - ( !document.hasFocus || document.hasFocus() ) && - !!( elem.type || elem.href || ~elem.tabIndex ); - }, - - // Boolean properties - "enabled": createDisabledPseudo( false ), - "disabled": createDisabledPseudo( true ), - - "checked": function( elem ) { - - // In CSS3, :checked should return both checked and selected elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - var nodeName = elem.nodeName.toLowerCase(); - return ( nodeName === "input" && !!elem.checked ) || - ( nodeName === "option" && !!elem.selected ); - }, - - "selected": function( elem ) { - - // Accessing this property makes selected-by-default - // options in Safari work properly - if ( elem.parentNode ) { - // eslint-disable-next-line no-unused-expressions - elem.parentNode.selectedIndex; - } - - return elem.selected === true; - }, - - // Contents - "empty": function( elem ) { - - // http://www.w3.org/TR/selectors/#empty-pseudo - // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), - // but not by others (comment: 8; processing instruction: 7; etc.) - // nodeType < 6 works because attributes (2) do not appear as children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - if ( elem.nodeType < 6 ) { - return false; - } - } - return true; - }, - - "parent": function( elem ) { - return !Expr.pseudos[ "empty" ]( elem ); - }, - - // Element/input types - "header": function( elem ) { - return rheader.test( elem.nodeName ); - }, - - "input": function( elem ) { - return rinputs.test( elem.nodeName ); - }, - - "button": function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === "button" || name === "button"; - }, - - "text": function( elem ) { - var attr; - return elem.nodeName.toLowerCase() === "input" && - elem.type === "text" && - - // Support: IE<8 - // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" - ( ( attr = elem.getAttribute( "type" ) ) == null || - attr.toLowerCase() === "text" ); - }, - - // Position-in-collection - "first": createPositionalPseudo( function() { - return [ 0 ]; - } ), - - "last": createPositionalPseudo( function( _matchIndexes, length ) { - return [ length - 1 ]; - } ), - - "eq": createPositionalPseudo( function( _matchIndexes, length, argument ) { - return [ argument < 0 ? argument + length : argument ]; - } ), - - "even": createPositionalPseudo( function( matchIndexes, length ) { - var i = 0; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ), - - "odd": createPositionalPseudo( function( matchIndexes, length ) { - var i = 1; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ), - - "lt": createPositionalPseudo( function( matchIndexes, length, argument ) { - var i = argument < 0 ? - argument + length : - argument > length ? - length : - argument; - for ( ; --i >= 0; ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ), - - "gt": createPositionalPseudo( function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; ++i < length; ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ) - } -}; - -Expr.pseudos[ "nth" ] = Expr.pseudos[ "eq" ]; - -// Add button/input type pseudos -for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { - Expr.pseudos[ i ] = createInputPseudo( i ); -} -for ( i in { submit: true, reset: true } ) { - Expr.pseudos[ i ] = createButtonPseudo( i ); -} - -// Easy API for creating new setFilters -function setFilters() {} -setFilters.prototype = Expr.filters = Expr.pseudos; -Expr.setFilters = new setFilters(); - -tokenize = Sizzle.tokenize = function( selector, parseOnly ) { - var matched, match, tokens, type, - soFar, groups, preFilters, - cached = tokenCache[ selector + " " ]; - - if ( cached ) { - return parseOnly ? 0 : cached.slice( 0 ); - } - - soFar = selector; - groups = []; - preFilters = Expr.preFilter; - - while ( soFar ) { - - // Comma and first run - if ( !matched || ( match = rcomma.exec( soFar ) ) ) { - if ( match ) { - - // Don't consume trailing commas as valid - soFar = soFar.slice( match[ 0 ].length ) || soFar; - } - groups.push( ( tokens = [] ) ); - } - - matched = false; - - // Combinators - if ( ( match = rcombinators.exec( soFar ) ) ) { - matched = match.shift(); - tokens.push( { - value: matched, - - // Cast descendant combinators to space - type: match[ 0 ].replace( rtrim, " " ) - } ); - soFar = soFar.slice( matched.length ); - } - - // Filters - for ( type in Expr.filter ) { - if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] || - ( match = preFilters[ type ]( match ) ) ) ) { - matched = match.shift(); - tokens.push( { - value: matched, - type: type, - matches: match - } ); - soFar = soFar.slice( matched.length ); - } - } - - if ( !matched ) { - break; - } - } - - // Return the length of the invalid excess - // if we're just parsing - // Otherwise, throw an error or return tokens - return parseOnly ? - soFar.length : - soFar ? - Sizzle.error( selector ) : - - // Cache the tokens - tokenCache( selector, groups ).slice( 0 ); -}; - -function toSelector( tokens ) { - var i = 0, - len = tokens.length, - selector = ""; - for ( ; i < len; i++ ) { - selector += tokens[ i ].value; - } - return selector; -} - -function addCombinator( matcher, combinator, base ) { - var dir = combinator.dir, - skip = combinator.next, - key = skip || dir, - checkNonElements = base && key === "parentNode", - doneName = done++; - - return combinator.first ? - - // Check against closest ancestor/preceding element - function( elem, context, xml ) { - while ( ( elem = elem[ dir ] ) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - return matcher( elem, context, xml ); - } - } - return false; - } : - - // Check against all ancestor/preceding elements - function( elem, context, xml ) { - var oldCache, uniqueCache, outerCache, - newCache = [ dirruns, doneName ]; - - // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching - if ( xml ) { - while ( ( elem = elem[ dir ] ) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - if ( matcher( elem, context, xml ) ) { - return true; - } - } - } - } else { - while ( ( elem = elem[ dir ] ) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - outerCache = elem[ expando ] || ( elem[ expando ] = {} ); - - // Support: IE <9 only - // Defend against cloned attroperties (jQuery gh-1709) - uniqueCache = outerCache[ elem.uniqueID ] || - ( outerCache[ elem.uniqueID ] = {} ); - - if ( skip && skip === elem.nodeName.toLowerCase() ) { - elem = elem[ dir ] || elem; - } else if ( ( oldCache = uniqueCache[ key ] ) && - oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { - - // Assign to newCache so results back-propagate to previous elements - return ( newCache[ 2 ] = oldCache[ 2 ] ); - } else { - - // Reuse newcache so results back-propagate to previous elements - uniqueCache[ key ] = newCache; - - // A match means we're done; a fail means we have to keep checking - if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) { - return true; - } - } - } - } - } - return false; - }; -} - -function elementMatcher( matchers ) { - return matchers.length > 1 ? - function( elem, context, xml ) { - var i = matchers.length; - while ( i-- ) { - if ( !matchers[ i ]( elem, context, xml ) ) { - return false; - } - } - return true; - } : - matchers[ 0 ]; -} - -function multipleContexts( selector, contexts, results ) { - var i = 0, - len = contexts.length; - for ( ; i < len; i++ ) { - Sizzle( selector, contexts[ i ], results ); - } - return results; -} - -function condense( unmatched, map, filter, context, xml ) { - var elem, - newUnmatched = [], - i = 0, - len = unmatched.length, - mapped = map != null; - - for ( ; i < len; i++ ) { - if ( ( elem = unmatched[ i ] ) ) { - if ( !filter || filter( elem, context, xml ) ) { - newUnmatched.push( elem ); - if ( mapped ) { - map.push( i ); - } - } - } - } - - return newUnmatched; -} - -function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { - if ( postFilter && !postFilter[ expando ] ) { - postFilter = setMatcher( postFilter ); - } - if ( postFinder && !postFinder[ expando ] ) { - postFinder = setMatcher( postFinder, postSelector ); - } - return markFunction( function( seed, results, context, xml ) { - var temp, i, elem, - preMap = [], - postMap = [], - preexisting = results.length, - - // Get initial elements from seed or context - elems = seed || multipleContexts( - selector || "*", - context.nodeType ? [ context ] : context, - [] - ), - - // Prefilter to get matcher input, preserving a map for seed-results synchronization - matcherIn = preFilter && ( seed || !selector ) ? - condense( elems, preMap, preFilter, context, xml ) : - elems, - - matcherOut = matcher ? - - // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, - postFinder || ( seed ? preFilter : preexisting || postFilter ) ? - - // ...intermediate processing is necessary - [] : - - // ...otherwise use results directly - results : - matcherIn; - - // Find primary matches - if ( matcher ) { - matcher( matcherIn, matcherOut, context, xml ); - } - - // Apply postFilter - if ( postFilter ) { - temp = condense( matcherOut, postMap ); - postFilter( temp, [], context, xml ); - - // Un-match failing elements by moving them back to matcherIn - i = temp.length; - while ( i-- ) { - if ( ( elem = temp[ i ] ) ) { - matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem ); - } - } - } - - if ( seed ) { - if ( postFinder || preFilter ) { - if ( postFinder ) { - - // Get the final matcherOut by condensing this intermediate into postFinder contexts - temp = []; - i = matcherOut.length; - while ( i-- ) { - if ( ( elem = matcherOut[ i ] ) ) { - - // Restore matcherIn since elem is not yet a final match - temp.push( ( matcherIn[ i ] = elem ) ); - } - } - postFinder( null, ( matcherOut = [] ), temp, xml ); - } - - // Move matched elements from seed to results to keep them synchronized - i = matcherOut.length; - while ( i-- ) { - if ( ( elem = matcherOut[ i ] ) && - ( temp = postFinder ? indexOf( seed, elem ) : preMap[ i ] ) > -1 ) { - - seed[ temp ] = !( results[ temp ] = elem ); - } - } - } - - // Add elements to results, through postFinder if defined - } else { - matcherOut = condense( - matcherOut === results ? - matcherOut.splice( preexisting, matcherOut.length ) : - matcherOut - ); - if ( postFinder ) { - postFinder( null, results, matcherOut, xml ); - } else { - push.apply( results, matcherOut ); - } - } - } ); -} - -function matcherFromTokens( tokens ) { - var checkContext, matcher, j, - len = tokens.length, - leadingRelative = Expr.relative[ tokens[ 0 ].type ], - implicitRelative = leadingRelative || Expr.relative[ " " ], - i = leadingRelative ? 1 : 0, - - // The foundational matcher ensures that elements are reachable from top-level context(s) - matchContext = addCombinator( function( elem ) { - return elem === checkContext; - }, implicitRelative, true ), - matchAnyContext = addCombinator( function( elem ) { - return indexOf( checkContext, elem ) > -1; - }, implicitRelative, true ), - matchers = [ function( elem, context, xml ) { - var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( - ( checkContext = context ).nodeType ? - matchContext( elem, context, xml ) : - matchAnyContext( elem, context, xml ) ); - - // Avoid hanging onto element (issue #299) - checkContext = null; - return ret; - } ]; - - for ( ; i < len; i++ ) { - if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) { - matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; - } else { - matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); - - // Return special upon seeing a positional matcher - if ( matcher[ expando ] ) { - - // Find the next relative operator (if any) for proper handling - j = ++i; - for ( ; j < len; j++ ) { - if ( Expr.relative[ tokens[ j ].type ] ) { - break; - } - } - return setMatcher( - i > 1 && elementMatcher( matchers ), - i > 1 && toSelector( - - // If the preceding token was a descendant combinator, insert an implicit any-element `*` - tokens - .slice( 0, i - 1 ) - .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } ) - ).replace( rtrim, "$1" ), - matcher, - i < j && matcherFromTokens( tokens.slice( i, j ) ), - j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ), - j < len && toSelector( tokens ) - ); - } - matchers.push( matcher ); - } - } - - return elementMatcher( matchers ); -} - -function matcherFromGroupMatchers( elementMatchers, setMatchers ) { - var bySet = setMatchers.length > 0, - byElement = elementMatchers.length > 0, - superMatcher = function( seed, context, xml, results, outermost ) { - var elem, j, matcher, - matchedCount = 0, - i = "0", - unmatched = seed && [], - setMatched = [], - contextBackup = outermostContext, - - // We must always have either seed elements or outermost context - elems = seed || byElement && Expr.find[ "TAG" ]( "*", outermost ), - - // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ), - len = elems.length; - - if ( outermost ) { - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - outermostContext = context == document || context || outermost; - } - - // Add elements passing elementMatchers directly to results - // Support: IE<9, Safari - // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id - for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) { - if ( byElement && elem ) { - j = 0; - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( !context && elem.ownerDocument != document ) { - setDocument( elem ); - xml = !documentIsHTML; - } - while ( ( matcher = elementMatchers[ j++ ] ) ) { - if ( matcher( elem, context || document, xml ) ) { - results.push( elem ); - break; - } - } - if ( outermost ) { - dirruns = dirrunsUnique; - } - } - - // Track unmatched elements for set filters - if ( bySet ) { - - // They will have gone through all possible matchers - if ( ( elem = !matcher && elem ) ) { - matchedCount--; - } - - // Lengthen the array for every element, matched or not - if ( seed ) { - unmatched.push( elem ); - } - } - } - - // `i` is now the count of elements visited above, and adding it to `matchedCount` - // makes the latter nonnegative. - matchedCount += i; - - // Apply set filters to unmatched elements - // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` - // equals `i`), unless we didn't visit _any_ elements in the above loop because we have - // no element matchers and no seed. - // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that - // case, which will result in a "00" `matchedCount` that differs from `i` but is also - // numerically zero. - if ( bySet && i !== matchedCount ) { - j = 0; - while ( ( matcher = setMatchers[ j++ ] ) ) { - matcher( unmatched, setMatched, context, xml ); - } - - if ( seed ) { - - // Reintegrate element matches to eliminate the need for sorting - if ( matchedCount > 0 ) { - while ( i-- ) { - if ( !( unmatched[ i ] || setMatched[ i ] ) ) { - setMatched[ i ] = pop.call( results ); - } - } - } - - // Discard index placeholder values to get only actual matches - setMatched = condense( setMatched ); - } - - // Add matches to results - push.apply( results, setMatched ); - - // Seedless set matches succeeding multiple successful matchers stipulate sorting - if ( outermost && !seed && setMatched.length > 0 && - ( matchedCount + setMatchers.length ) > 1 ) { - - Sizzle.uniqueSort( results ); - } - } - - // Override manipulation of globals by nested matchers - if ( outermost ) { - dirruns = dirrunsUnique; - outermostContext = contextBackup; - } - - return unmatched; - }; - - return bySet ? - markFunction( superMatcher ) : - superMatcher; -} - -compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { - var i, - setMatchers = [], - elementMatchers = [], - cached = compilerCache[ selector + " " ]; - - if ( !cached ) { - - // Generate a function of recursive functions that can be used to check each element - if ( !match ) { - match = tokenize( selector ); - } - i = match.length; - while ( i-- ) { - cached = matcherFromTokens( match[ i ] ); - if ( cached[ expando ] ) { - setMatchers.push( cached ); - } else { - elementMatchers.push( cached ); - } - } - - // Cache the compiled function - cached = compilerCache( - selector, - matcherFromGroupMatchers( elementMatchers, setMatchers ) - ); - - // Save selector and tokenization - cached.selector = selector; - } - return cached; -}; - -/** - * A low-level selection function that works with Sizzle's compiled - * selector functions - * @param {String|Function} selector A selector or a pre-compiled - * selector function built with Sizzle.compile - * @param {Element} context - * @param {Array} [results] - * @param {Array} [seed] A set of elements to match against - */ -select = Sizzle.select = function( selector, context, results, seed ) { - var i, tokens, token, type, find, - compiled = typeof selector === "function" && selector, - match = !seed && tokenize( ( selector = compiled.selector || selector ) ); - - results = results || []; - - // Try to minimize operations if there is only one selector in the list and no seed - // (the latter of which guarantees us context) - if ( match.length === 1 ) { - - // Reduce context if the leading compound selector is an ID - tokens = match[ 0 ] = match[ 0 ].slice( 0 ); - if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" && - context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) { - - context = ( Expr.find[ "ID" ]( token.matches[ 0 ] - .replace( runescape, funescape ), context ) || [] )[ 0 ]; - if ( !context ) { - return results; - - // Precompiled matchers will still verify ancestry, so step up a level - } else if ( compiled ) { - context = context.parentNode; - } - - selector = selector.slice( tokens.shift().value.length ); - } - - // Fetch a seed set for right-to-left matching - i = matchExpr[ "needsContext" ].test( selector ) ? 0 : tokens.length; - while ( i-- ) { - token = tokens[ i ]; - - // Abort if we hit a combinator - if ( Expr.relative[ ( type = token.type ) ] ) { - break; - } - if ( ( find = Expr.find[ type ] ) ) { - - // Search, expanding context for leading sibling combinators - if ( ( seed = find( - token.matches[ 0 ].replace( runescape, funescape ), - rsibling.test( tokens[ 0 ].type ) && testContext( context.parentNode ) || - context - ) ) ) { - - // If seed is empty or no tokens remain, we can return early - tokens.splice( i, 1 ); - selector = seed.length && toSelector( tokens ); - if ( !selector ) { - push.apply( results, seed ); - return results; - } - - break; - } - } - } - } - - // Compile and execute a filtering function if one is not provided - // Provide `match` to avoid retokenization if we modified the selector above - ( compiled || compile( selector, match ) )( - seed, - context, - !documentIsHTML, - results, - !context || rsibling.test( selector ) && testContext( context.parentNode ) || context - ); - return results; -}; - -// One-time assignments - -// Sort stability -support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando; - -// Support: Chrome 14-35+ -// Always assume duplicates if they aren't passed to the comparison function -support.detectDuplicates = !!hasDuplicate; - -// Initialize against the default document -setDocument(); - -// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) -// Detached nodes confoundingly follow *each other* -support.sortDetached = assert( function( el ) { - - // Should return 1, but returns 4 (following) - return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1; -} ); - -// Support: IE<8 -// Prevent attribute/property "interpolation" -// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx -if ( !assert( function( el ) { - el.innerHTML = ""; - return el.firstChild.getAttribute( "href" ) === "#"; -} ) ) { - addHandle( "type|href|height|width", function( elem, name, isXML ) { - if ( !isXML ) { - return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); - } - } ); -} - -// Support: IE<9 -// Use defaultValue in place of getAttribute("value") -if ( !support.attributes || !assert( function( el ) { - el.innerHTML = ""; - el.firstChild.setAttribute( "value", "" ); - return el.firstChild.getAttribute( "value" ) === ""; -} ) ) { - addHandle( "value", function( elem, _name, isXML ) { - if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { - return elem.defaultValue; - } - } ); -} - -// Support: IE<9 -// Use getAttributeNode to fetch booleans when getAttribute lies -if ( !assert( function( el ) { - return el.getAttribute( "disabled" ) == null; -} ) ) { - addHandle( booleans, function( elem, name, isXML ) { - var val; - if ( !isXML ) { - return elem[ name ] === true ? name.toLowerCase() : - ( val = elem.getAttributeNode( name ) ) && val.specified ? - val.value : - null; - } - } ); -} - -return Sizzle; - -} )( window ); - - - -jQuery.find = Sizzle; -jQuery.expr = Sizzle.selectors; - -// Deprecated -jQuery.expr[ ":" ] = jQuery.expr.pseudos; -jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort; -jQuery.text = Sizzle.getText; -jQuery.isXMLDoc = Sizzle.isXML; -jQuery.contains = Sizzle.contains; -jQuery.escapeSelector = Sizzle.escape; - - - - -var dir = function( elem, dir, until ) { - var matched = [], - truncate = until !== undefined; - - while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { - if ( elem.nodeType === 1 ) { - if ( truncate && jQuery( elem ).is( until ) ) { - break; - } - matched.push( elem ); - } - } - return matched; -}; - - -var siblings = function( n, elem ) { - var matched = []; - - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType === 1 && n !== elem ) { - matched.push( n ); - } - } - - return matched; -}; - - -var rneedsContext = jQuery.expr.match.needsContext; - - - -function nodeName( elem, name ) { - - return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); - -}; -var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); - - - -// Implement the identical functionality for filter and not -function winnow( elements, qualifier, not ) { - if ( isFunction( qualifier ) ) { - return jQuery.grep( elements, function( elem, i ) { - return !!qualifier.call( elem, i, elem ) !== not; - } ); - } - - // Single element - if ( qualifier.nodeType ) { - return jQuery.grep( elements, function( elem ) { - return ( elem === qualifier ) !== not; - } ); - } - - // Arraylike of elements (jQuery, arguments, Array) - if ( typeof qualifier !== "string" ) { - return jQuery.grep( elements, function( elem ) { - return ( indexOf.call( qualifier, elem ) > -1 ) !== not; - } ); - } - - // Filtered directly for both simple and complex selectors - return jQuery.filter( qualifier, elements, not ); -} - -jQuery.filter = function( expr, elems, not ) { - var elem = elems[ 0 ]; - - if ( not ) { - expr = ":not(" + expr + ")"; - } - - if ( elems.length === 1 && elem.nodeType === 1 ) { - return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : []; - } - - return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { - return elem.nodeType === 1; - } ) ); -}; - -jQuery.fn.extend( { - find: function( selector ) { - var i, ret, - len = this.length, - self = this; - - if ( typeof selector !== "string" ) { - return this.pushStack( jQuery( selector ).filter( function() { - for ( i = 0; i < len; i++ ) { - if ( jQuery.contains( self[ i ], this ) ) { - return true; - } - } - } ) ); - } - - ret = this.pushStack( [] ); - - for ( i = 0; i < len; i++ ) { - jQuery.find( selector, self[ i ], ret ); - } - - return len > 1 ? jQuery.uniqueSort( ret ) : ret; - }, - filter: function( selector ) { - return this.pushStack( winnow( this, selector || [], false ) ); - }, - not: function( selector ) { - return this.pushStack( winnow( this, selector || [], true ) ); - }, - is: function( selector ) { - return !!winnow( - this, - - // If this is a positional/relative selector, check membership in the returned set - // so $("p:first").is("p:last") won't return true for a doc with two "p". - typeof selector === "string" && rneedsContext.test( selector ) ? - jQuery( selector ) : - selector || [], - false - ).length; - } -} ); - - -// Initialize a jQuery object - - -// A central reference to the root jQuery(document) -var rootjQuery, - - // A simple way to check for HTML strings - // Prioritize #id over to avoid XSS via location.hash (#9521) - // Strict HTML recognition (#11290: must start with <) - // Shortcut simple #id case for speed - rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, - - init = jQuery.fn.init = function( selector, context, root ) { - var match, elem; - - // HANDLE: $(""), $(null), $(undefined), $(false) - if ( !selector ) { - return this; - } - - // Method init() accepts an alternate rootjQuery - // so migrate can support jQuery.sub (gh-2101) - root = root || rootjQuery; - - // Handle HTML strings - if ( typeof selector === "string" ) { - if ( selector[ 0 ] === "<" && - selector[ selector.length - 1 ] === ">" && - selector.length >= 3 ) { - - // Assume that strings that start and end with <> are HTML and skip the regex check - match = [ null, selector, null ]; - - } else { - match = rquickExpr.exec( selector ); - } - - // Match html or make sure no context is specified for #id - if ( match && ( match[ 1 ] || !context ) ) { - - // HANDLE: $(html) -> $(array) - if ( match[ 1 ] ) { - context = context instanceof jQuery ? context[ 0 ] : context; - - // Option to run scripts is true for back-compat - // Intentionally let the error be thrown if parseHTML is not present - jQuery.merge( this, jQuery.parseHTML( - match[ 1 ], - context && context.nodeType ? context.ownerDocument || context : document, - true - ) ); - - // HANDLE: $(html, props) - if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { - for ( match in context ) { - - // Properties of context are called as methods if possible - if ( isFunction( this[ match ] ) ) { - this[ match ]( context[ match ] ); - - // ...and otherwise set as attributes - } else { - this.attr( match, context[ match ] ); - } - } - } - - return this; - - // HANDLE: $(#id) - } else { - elem = document.getElementById( match[ 2 ] ); - - if ( elem ) { - - // Inject the element directly into the jQuery object - this[ 0 ] = elem; - this.length = 1; - } - return this; - } - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return ( context || root ).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor( context ).find( selector ); - } - - // HANDLE: $(DOMElement) - } else if ( selector.nodeType ) { - this[ 0 ] = selector; - this.length = 1; - return this; - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( isFunction( selector ) ) { - return root.ready !== undefined ? - root.ready( selector ) : - - // Execute immediately if ready is not present - selector( jQuery ); - } - - return jQuery.makeArray( selector, this ); - }; - -// Give the init function the jQuery prototype for later instantiation -init.prototype = jQuery.fn; - -// Initialize central reference -rootjQuery = jQuery( document ); - - -var rparentsprev = /^(?:parents|prev(?:Until|All))/, - - // Methods guaranteed to produce a unique set when starting from a unique set - guaranteedUnique = { - children: true, - contents: true, - next: true, - prev: true - }; - -jQuery.fn.extend( { - has: function( target ) { - var targets = jQuery( target, this ), - l = targets.length; - - return this.filter( function() { - var i = 0; - for ( ; i < l; i++ ) { - if ( jQuery.contains( this, targets[ i ] ) ) { - return true; - } - } - } ); - }, - - closest: function( selectors, context ) { - var cur, - i = 0, - l = this.length, - matched = [], - targets = typeof selectors !== "string" && jQuery( selectors ); - - // Positional selectors never match, since there's no _selection_ context - if ( !rneedsContext.test( selectors ) ) { - for ( ; i < l; i++ ) { - for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { - - // Always skip document fragments - if ( cur.nodeType < 11 && ( targets ? - targets.index( cur ) > -1 : - - // Don't pass non-elements to Sizzle - cur.nodeType === 1 && - jQuery.find.matchesSelector( cur, selectors ) ) ) { - - matched.push( cur ); - break; - } - } - } - } - - return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); - }, - - // Determine the position of an element within the set - index: function( elem ) { - - // No argument, return index in parent - if ( !elem ) { - return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; - } - - // Index in selector - if ( typeof elem === "string" ) { - return indexOf.call( jQuery( elem ), this[ 0 ] ); - } - - // Locate the position of the desired element - return indexOf.call( this, - - // If it receives a jQuery object, the first element is used - elem.jquery ? elem[ 0 ] : elem - ); - }, - - add: function( selector, context ) { - return this.pushStack( - jQuery.uniqueSort( - jQuery.merge( this.get(), jQuery( selector, context ) ) - ) - ); - }, - - addBack: function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter( selector ) - ); - } -} ); - -function sibling( cur, dir ) { - while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} - return cur; -} - -jQuery.each( { - parent: function( elem ) { - var parent = elem.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - parents: function( elem ) { - return dir( elem, "parentNode" ); - }, - parentsUntil: function( elem, _i, until ) { - return dir( elem, "parentNode", until ); - }, - next: function( elem ) { - return sibling( elem, "nextSibling" ); - }, - prev: function( elem ) { - return sibling( elem, "previousSibling" ); - }, - nextAll: function( elem ) { - return dir( elem, "nextSibling" ); - }, - prevAll: function( elem ) { - return dir( elem, "previousSibling" ); - }, - nextUntil: function( elem, _i, until ) { - return dir( elem, "nextSibling", until ); - }, - prevUntil: function( elem, _i, until ) { - return dir( elem, "previousSibling", until ); - }, - siblings: function( elem ) { - return siblings( ( elem.parentNode || {} ).firstChild, elem ); - }, - children: function( elem ) { - return siblings( elem.firstChild ); - }, - contents: function( elem ) { - if ( elem.contentDocument != null && - - // Support: IE 11+ - // elements with no `data` attribute has an object - // `contentDocument` with a `null` prototype. - getProto( elem.contentDocument ) ) { - - return elem.contentDocument; - } - - // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only - // Treat the template element as a regular one in browsers that - // don't support it. - if ( nodeName( elem, "template" ) ) { - elem = elem.content || elem; - } - - return jQuery.merge( [], elem.childNodes ); - } -}, function( name, fn ) { - jQuery.fn[ name ] = function( until, selector ) { - var matched = jQuery.map( this, fn, until ); - - if ( name.slice( -5 ) !== "Until" ) { - selector = until; - } - - if ( selector && typeof selector === "string" ) { - matched = jQuery.filter( selector, matched ); - } - - if ( this.length > 1 ) { - - // Remove duplicates - if ( !guaranteedUnique[ name ] ) { - jQuery.uniqueSort( matched ); - } - - // Reverse order for parents* and prev-derivatives - if ( rparentsprev.test( name ) ) { - matched.reverse(); - } - } - - return this.pushStack( matched ); - }; -} ); -var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g ); - - - -// Convert String-formatted options into Object-formatted ones -function createOptions( options ) { - var object = {}; - jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) { - object[ flag ] = true; - } ); - return object; -} - -/* - * Create a callback list using the following parameters: - * - * options: an optional list of space-separated options that will change how - * the callback list behaves or a more traditional option object - * - * By default a callback list will act like an event callback list and can be - * "fired" multiple times. - * - * Possible options: - * - * once: will ensure the callback list can only be fired once (like a Deferred) - * - * memory: will keep track of previous values and will call any callback added - * after the list has been fired right away with the latest "memorized" - * values (like a Deferred) - * - * unique: will ensure a callback can only be added once (no duplicate in the list) - * - * stopOnFalse: interrupt callings when a callback returns false - * - */ -jQuery.Callbacks = function( options ) { - - // Convert options from String-formatted to Object-formatted if needed - // (we check in cache first) - options = typeof options === "string" ? - createOptions( options ) : - jQuery.extend( {}, options ); - - var // Flag to know if list is currently firing - firing, - - // Last fire value for non-forgettable lists - memory, - - // Flag to know if list was already fired - fired, - - // Flag to prevent firing - locked, - - // Actual callback list - list = [], - - // Queue of execution data for repeatable lists - queue = [], - - // Index of currently firing callback (modified by add/remove as needed) - firingIndex = -1, - - // Fire callbacks - fire = function() { - - // Enforce single-firing - locked = locked || options.once; - - // Execute callbacks for all pending executions, - // respecting firingIndex overrides and runtime changes - fired = firing = true; - for ( ; queue.length; firingIndex = -1 ) { - memory = queue.shift(); - while ( ++firingIndex < list.length ) { - - // Run callback and check for early termination - if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && - options.stopOnFalse ) { - - // Jump to end and forget the data so .add doesn't re-fire - firingIndex = list.length; - memory = false; - } - } - } - - // Forget the data if we're done with it - if ( !options.memory ) { - memory = false; - } - - firing = false; - - // Clean up if we're done firing for good - if ( locked ) { - - // Keep an empty list if we have data for future add calls - if ( memory ) { - list = []; - - // Otherwise, this object is spent - } else { - list = ""; - } - } - }, - - // Actual Callbacks object - self = { - - // Add a callback or a collection of callbacks to the list - add: function() { - if ( list ) { - - // If we have memory from a past run, we should fire after adding - if ( memory && !firing ) { - firingIndex = list.length - 1; - queue.push( memory ); - } - - ( function add( args ) { - jQuery.each( args, function( _, arg ) { - if ( isFunction( arg ) ) { - if ( !options.unique || !self.has( arg ) ) { - list.push( arg ); - } - } else if ( arg && arg.length && toType( arg ) !== "string" ) { - - // Inspect recursively - add( arg ); - } - } ); - } )( arguments ); - - if ( memory && !firing ) { - fire(); - } - } - return this; - }, - - // Remove a callback from the list - remove: function() { - jQuery.each( arguments, function( _, arg ) { - var index; - while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { - list.splice( index, 1 ); - - // Handle firing indexes - if ( index <= firingIndex ) { - firingIndex--; - } - } - } ); - return this; - }, - - // Check if a given callback is in the list. - // If no argument is given, return whether or not list has callbacks attached. - has: function( fn ) { - return fn ? - jQuery.inArray( fn, list ) > -1 : - list.length > 0; - }, - - // Remove all callbacks from the list - empty: function() { - if ( list ) { - list = []; - } - return this; - }, - - // Disable .fire and .add - // Abort any current/pending executions - // Clear all callbacks and values - disable: function() { - locked = queue = []; - list = memory = ""; - return this; - }, - disabled: function() { - return !list; - }, - - // Disable .fire - // Also disable .add unless we have memory (since it would have no effect) - // Abort any pending executions - lock: function() { - locked = queue = []; - if ( !memory && !firing ) { - list = memory = ""; - } - return this; - }, - locked: function() { - return !!locked; - }, - - // Call all callbacks with the given context and arguments - fireWith: function( context, args ) { - if ( !locked ) { - args = args || []; - args = [ context, args.slice ? args.slice() : args ]; - queue.push( args ); - if ( !firing ) { - fire(); - } - } - return this; - }, - - // Call all the callbacks with the given arguments - fire: function() { - self.fireWith( this, arguments ); - return this; - }, - - // To know if the callbacks have already been called at least once - fired: function() { - return !!fired; - } - }; - - return self; -}; - - -function Identity( v ) { - return v; -} -function Thrower( ex ) { - throw ex; -} - -function adoptValue( value, resolve, reject, noValue ) { - var method; - - try { - - // Check for promise aspect first to privilege synchronous behavior - if ( value && isFunction( ( method = value.promise ) ) ) { - method.call( value ).done( resolve ).fail( reject ); - - // Other thenables - } else if ( value && isFunction( ( method = value.then ) ) ) { - method.call( value, resolve, reject ); - - // Other non-thenables - } else { - - // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer: - // * false: [ value ].slice( 0 ) => resolve( value ) - // * true: [ value ].slice( 1 ) => resolve() - resolve.apply( undefined, [ value ].slice( noValue ) ); - } - - // For Promises/A+, convert exceptions into rejections - // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in - // Deferred#then to conditionally suppress rejection. - } catch ( value ) { - - // Support: Android 4.0 only - // Strict mode functions invoked without .call/.apply get global-object context - reject.apply( undefined, [ value ] ); - } -} - -jQuery.extend( { - - Deferred: function( func ) { - var tuples = [ - - // action, add listener, callbacks, - // ... .then handlers, argument index, [final state] - [ "notify", "progress", jQuery.Callbacks( "memory" ), - jQuery.Callbacks( "memory" ), 2 ], - [ "resolve", "done", jQuery.Callbacks( "once memory" ), - jQuery.Callbacks( "once memory" ), 0, "resolved" ], - [ "reject", "fail", jQuery.Callbacks( "once memory" ), - jQuery.Callbacks( "once memory" ), 1, "rejected" ] - ], - state = "pending", - promise = { - state: function() { - return state; - }, - always: function() { - deferred.done( arguments ).fail( arguments ); - return this; - }, - "catch": function( fn ) { - return promise.then( null, fn ); - }, - - // Keep pipe for back-compat - pipe: function( /* fnDone, fnFail, fnProgress */ ) { - var fns = arguments; - - return jQuery.Deferred( function( newDefer ) { - jQuery.each( tuples, function( _i, tuple ) { - - // Map tuples (progress, done, fail) to arguments (done, fail, progress) - var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; - - // deferred.progress(function() { bind to newDefer or newDefer.notify }) - // deferred.done(function() { bind to newDefer or newDefer.resolve }) - // deferred.fail(function() { bind to newDefer or newDefer.reject }) - deferred[ tuple[ 1 ] ]( function() { - var returned = fn && fn.apply( this, arguments ); - if ( returned && isFunction( returned.promise ) ) { - returned.promise() - .progress( newDefer.notify ) - .done( newDefer.resolve ) - .fail( newDefer.reject ); - } else { - newDefer[ tuple[ 0 ] + "With" ]( - this, - fn ? [ returned ] : arguments - ); - } - } ); - } ); - fns = null; - } ).promise(); - }, - then: function( onFulfilled, onRejected, onProgress ) { - var maxDepth = 0; - function resolve( depth, deferred, handler, special ) { - return function() { - var that = this, - args = arguments, - mightThrow = function() { - var returned, then; - - // Support: Promises/A+ section 2.3.3.3.3 - // https://promisesaplus.com/#point-59 - // Ignore double-resolution attempts - if ( depth < maxDepth ) { - return; - } - - returned = handler.apply( that, args ); - - // Support: Promises/A+ section 2.3.1 - // https://promisesaplus.com/#point-48 - if ( returned === deferred.promise() ) { - throw new TypeError( "Thenable self-resolution" ); - } - - // Support: Promises/A+ sections 2.3.3.1, 3.5 - // https://promisesaplus.com/#point-54 - // https://promisesaplus.com/#point-75 - // Retrieve `then` only once - then = returned && - - // Support: Promises/A+ section 2.3.4 - // https://promisesaplus.com/#point-64 - // Only check objects and functions for thenability - ( typeof returned === "object" || - typeof returned === "function" ) && - returned.then; - - // Handle a returned thenable - if ( isFunction( then ) ) { - - // Special processors (notify) just wait for resolution - if ( special ) { - then.call( - returned, - resolve( maxDepth, deferred, Identity, special ), - resolve( maxDepth, deferred, Thrower, special ) - ); - - // Normal processors (resolve) also hook into progress - } else { - - // ...and disregard older resolution values - maxDepth++; - - then.call( - returned, - resolve( maxDepth, deferred, Identity, special ), - resolve( maxDepth, deferred, Thrower, special ), - resolve( maxDepth, deferred, Identity, - deferred.notifyWith ) - ); - } - - // Handle all other returned values - } else { - - // Only substitute handlers pass on context - // and multiple values (non-spec behavior) - if ( handler !== Identity ) { - that = undefined; - args = [ returned ]; - } - - // Process the value(s) - // Default process is resolve - ( special || deferred.resolveWith )( that, args ); - } - }, - - // Only normal processors (resolve) catch and reject exceptions - process = special ? - mightThrow : - function() { - try { - mightThrow(); - } catch ( e ) { - - if ( jQuery.Deferred.exceptionHook ) { - jQuery.Deferred.exceptionHook( e, - process.stackTrace ); - } - - // Support: Promises/A+ section 2.3.3.3.4.1 - // https://promisesaplus.com/#point-61 - // Ignore post-resolution exceptions - if ( depth + 1 >= maxDepth ) { - - // Only substitute handlers pass on context - // and multiple values (non-spec behavior) - if ( handler !== Thrower ) { - that = undefined; - args = [ e ]; - } - - deferred.rejectWith( that, args ); - } - } - }; - - // Support: Promises/A+ section 2.3.3.3.1 - // https://promisesaplus.com/#point-57 - // Re-resolve promises immediately to dodge false rejection from - // subsequent errors - if ( depth ) { - process(); - } else { - - // Call an optional hook to record the stack, in case of exception - // since it's otherwise lost when execution goes async - if ( jQuery.Deferred.getStackHook ) { - process.stackTrace = jQuery.Deferred.getStackHook(); - } - window.setTimeout( process ); - } - }; - } - - return jQuery.Deferred( function( newDefer ) { - - // progress_handlers.add( ... ) - tuples[ 0 ][ 3 ].add( - resolve( - 0, - newDefer, - isFunction( onProgress ) ? - onProgress : - Identity, - newDefer.notifyWith - ) - ); - - // fulfilled_handlers.add( ... ) - tuples[ 1 ][ 3 ].add( - resolve( - 0, - newDefer, - isFunction( onFulfilled ) ? - onFulfilled : - Identity - ) - ); - - // rejected_handlers.add( ... ) - tuples[ 2 ][ 3 ].add( - resolve( - 0, - newDefer, - isFunction( onRejected ) ? - onRejected : - Thrower - ) - ); - } ).promise(); - }, - - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj ) { - return obj != null ? jQuery.extend( obj, promise ) : promise; - } - }, - deferred = {}; - - // Add list-specific methods - jQuery.each( tuples, function( i, tuple ) { - var list = tuple[ 2 ], - stateString = tuple[ 5 ]; - - // promise.progress = list.add - // promise.done = list.add - // promise.fail = list.add - promise[ tuple[ 1 ] ] = list.add; - - // Handle state - if ( stateString ) { - list.add( - function() { - - // state = "resolved" (i.e., fulfilled) - // state = "rejected" - state = stateString; - }, - - // rejected_callbacks.disable - // fulfilled_callbacks.disable - tuples[ 3 - i ][ 2 ].disable, - - // rejected_handlers.disable - // fulfilled_handlers.disable - tuples[ 3 - i ][ 3 ].disable, - - // progress_callbacks.lock - tuples[ 0 ][ 2 ].lock, - - // progress_handlers.lock - tuples[ 0 ][ 3 ].lock - ); - } - - // progress_handlers.fire - // fulfilled_handlers.fire - // rejected_handlers.fire - list.add( tuple[ 3 ].fire ); - - // deferred.notify = function() { deferred.notifyWith(...) } - // deferred.resolve = function() { deferred.resolveWith(...) } - // deferred.reject = function() { deferred.rejectWith(...) } - deferred[ tuple[ 0 ] ] = function() { - deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments ); - return this; - }; - - // deferred.notifyWith = list.fireWith - // deferred.resolveWith = list.fireWith - // deferred.rejectWith = list.fireWith - deferred[ tuple[ 0 ] + "With" ] = list.fireWith; - } ); - - // Make the deferred a promise - promise.promise( deferred ); - - // Call given func if any - if ( func ) { - func.call( deferred, deferred ); - } - - // All done! - return deferred; - }, - - // Deferred helper - when: function( singleValue ) { - var - - // count of uncompleted subordinates - remaining = arguments.length, - - // count of unprocessed arguments - i = remaining, - - // subordinate fulfillment data - resolveContexts = Array( i ), - resolveValues = slice.call( arguments ), - - // the master Deferred - master = jQuery.Deferred(), - - // subordinate callback factory - updateFunc = function( i ) { - return function( value ) { - resolveContexts[ i ] = this; - resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; - if ( !( --remaining ) ) { - master.resolveWith( resolveContexts, resolveValues ); - } - }; - }; - - // Single- and empty arguments are adopted like Promise.resolve - if ( remaining <= 1 ) { - adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject, - !remaining ); - - // Use .then() to unwrap secondary thenables (cf. gh-3000) - if ( master.state() === "pending" || - isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { - - return master.then(); - } - } - - // Multiple arguments are aggregated like Promise.all array elements - while ( i-- ) { - adoptValue( resolveValues[ i ], updateFunc( i ), master.reject ); - } - - return master.promise(); - } -} ); - - -// These usually indicate a programmer mistake during development, -// warn about them ASAP rather than swallowing them by default. -var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; - -jQuery.Deferred.exceptionHook = function( error, stack ) { - - // Support: IE 8 - 9 only - // Console exists when dev tools are open, which can happen at any time - if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { - window.console.warn( "jQuery.Deferred exception: " + error.message, error.stack, stack ); - } -}; - - - - -jQuery.readyException = function( error ) { - window.setTimeout( function() { - throw error; - } ); -}; - - - - -// The deferred used on DOM ready -var readyList = jQuery.Deferred(); - -jQuery.fn.ready = function( fn ) { - - readyList - .then( fn ) - - // Wrap jQuery.readyException in a function so that the lookup - // happens at the time of error handling instead of callback - // registration. - .catch( function( error ) { - jQuery.readyException( error ); - } ); - - return this; -}; - -jQuery.extend( { - - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // A counter to track how many items to wait for before - // the ready event fires. See #6781 - readyWait: 1, - - // Handle when the DOM is ready - ready: function( wait ) { - - // Abort if there are pending holds or we're already ready - if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { - return; - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); - } -} ); - -jQuery.ready.then = readyList.then; - -// The ready event handler and self cleanup method -function completed() { - document.removeEventListener( "DOMContentLoaded", completed ); - window.removeEventListener( "load", completed ); - jQuery.ready(); -} - -// Catch cases where $(document).ready() is called -// after the browser event has already occurred. -// Support: IE <=9 - 10 only -// Older IE sometimes signals "interactive" too soon -if ( document.readyState === "complete" || - ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { - - // Handle it asynchronously to allow scripts the opportunity to delay ready - window.setTimeout( jQuery.ready ); - -} else { - - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", completed ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", completed ); -} - - - - -// Multifunctional method to get and set values of a collection -// The value/s can optionally be executed if it's a function -var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { - var i = 0, - len = elems.length, - bulk = key == null; - - // Sets many values - if ( toType( key ) === "object" ) { - chainable = true; - for ( i in key ) { - access( elems, fn, i, key[ i ], true, emptyGet, raw ); - } - - // Sets one value - } else if ( value !== undefined ) { - chainable = true; - - if ( !isFunction( value ) ) { - raw = true; - } - - if ( bulk ) { - - // Bulk operations run against the entire set - if ( raw ) { - fn.call( elems, value ); - fn = null; - - // ...except when executing function values - } else { - bulk = fn; - fn = function( elem, _key, value ) { - return bulk.call( jQuery( elem ), value ); - }; - } - } - - if ( fn ) { - for ( ; i < len; i++ ) { - fn( - elems[ i ], key, raw ? - value : - value.call( elems[ i ], i, fn( elems[ i ], key ) ) - ); - } - } - } - - if ( chainable ) { - return elems; - } - - // Gets - if ( bulk ) { - return fn.call( elems ); - } - - return len ? fn( elems[ 0 ], key ) : emptyGet; -}; - - -// Matches dashed string for camelizing -var rmsPrefix = /^-ms-/, - rdashAlpha = /-([a-z])/g; - -// Used by camelCase as callback to replace() -function fcamelCase( _all, letter ) { - return letter.toUpperCase(); -} - -// Convert dashed to camelCase; used by the css and data modules -// Support: IE <=9 - 11, Edge 12 - 15 -// Microsoft forgot to hump their vendor prefix (#9572) -function camelCase( string ) { - return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); -} -var acceptData = function( owner ) { - - // Accepts only: - // - Node - // - Node.ELEMENT_NODE - // - Node.DOCUMENT_NODE - // - Object - // - Any - return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); -}; - - - - -function Data() { - this.expando = jQuery.expando + Data.uid++; -} - -Data.uid = 1; - -Data.prototype = { - - cache: function( owner ) { - - // Check if the owner object already has a cache - var value = owner[ this.expando ]; - - // If not, create one - if ( !value ) { - value = {}; - - // We can accept data for non-element nodes in modern browsers, - // but we should not, see #8335. - // Always return an empty object. - if ( acceptData( owner ) ) { - - // If it is a node unlikely to be stringify-ed or looped over - // use plain assignment - if ( owner.nodeType ) { - owner[ this.expando ] = value; - - // Otherwise secure it in a non-enumerable property - // configurable must be true to allow the property to be - // deleted when data is removed - } else { - Object.defineProperty( owner, this.expando, { - value: value, - configurable: true - } ); - } - } - } - - return value; - }, - set: function( owner, data, value ) { - var prop, - cache = this.cache( owner ); - - // Handle: [ owner, key, value ] args - // Always use camelCase key (gh-2257) - if ( typeof data === "string" ) { - cache[ camelCase( data ) ] = value; - - // Handle: [ owner, { properties } ] args - } else { - - // Copy the properties one-by-one to the cache object - for ( prop in data ) { - cache[ camelCase( prop ) ] = data[ prop ]; - } - } - return cache; - }, - get: function( owner, key ) { - return key === undefined ? - this.cache( owner ) : - - // Always use camelCase key (gh-2257) - owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ]; - }, - access: function( owner, key, value ) { - - // In cases where either: - // - // 1. No key was specified - // 2. A string key was specified, but no value provided - // - // Take the "read" path and allow the get method to determine - // which value to return, respectively either: - // - // 1. The entire cache object - // 2. The data stored at the key - // - if ( key === undefined || - ( ( key && typeof key === "string" ) && value === undefined ) ) { - - return this.get( owner, key ); - } - - // When the key is not a string, or both a key and value - // are specified, set or extend (existing objects) with either: - // - // 1. An object of properties - // 2. A key and value - // - this.set( owner, key, value ); - - // Since the "set" path can have two possible entry points - // return the expected data based on which path was taken[*] - return value !== undefined ? value : key; - }, - remove: function( owner, key ) { - var i, - cache = owner[ this.expando ]; - - if ( cache === undefined ) { - return; - } - - if ( key !== undefined ) { - - // Support array or space separated string of keys - if ( Array.isArray( key ) ) { - - // If key is an array of keys... - // We always set camelCase keys, so remove that. - key = key.map( camelCase ); - } else { - key = camelCase( key ); - - // If a key with the spaces exists, use it. - // Otherwise, create an array by matching non-whitespace - key = key in cache ? - [ key ] : - ( key.match( rnothtmlwhite ) || [] ); - } - - i = key.length; - - while ( i-- ) { - delete cache[ key[ i ] ]; - } - } - - // Remove the expando if there's no more data - if ( key === undefined || jQuery.isEmptyObject( cache ) ) { - - // Support: Chrome <=35 - 45 - // Webkit & Blink performance suffers when deleting properties - // from DOM nodes, so set to undefined instead - // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted) - if ( owner.nodeType ) { - owner[ this.expando ] = undefined; - } else { - delete owner[ this.expando ]; - } - } - }, - hasData: function( owner ) { - var cache = owner[ this.expando ]; - return cache !== undefined && !jQuery.isEmptyObject( cache ); - } -}; -var dataPriv = new Data(); - -var dataUser = new Data(); - - - -// Implementation Summary -// -// 1. Enforce API surface and semantic compatibility with 1.9.x branch -// 2. Improve the module's maintainability by reducing the storage -// paths to a single mechanism. -// 3. Use the same single mechanism to support "private" and "user" data. -// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) -// 5. Avoid exposing implementation details on user objects (eg. expando properties) -// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 - -var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, - rmultiDash = /[A-Z]/g; - -function getData( data ) { - if ( data === "true" ) { - return true; - } - - if ( data === "false" ) { - return false; - } - - if ( data === "null" ) { - return null; - } - - // Only convert to a number if it doesn't change the string - if ( data === +data + "" ) { - return +data; - } - - if ( rbrace.test( data ) ) { - return JSON.parse( data ); - } - - return data; -} - -function dataAttr( elem, key, data ) { - var name; - - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { - name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); - data = elem.getAttribute( name ); - - if ( typeof data === "string" ) { - try { - data = getData( data ); - } catch ( e ) {} - - // Make sure we set the data so it isn't changed later - dataUser.set( elem, key, data ); - } else { - data = undefined; - } - } - return data; -} - -jQuery.extend( { - hasData: function( elem ) { - return dataUser.hasData( elem ) || dataPriv.hasData( elem ); - }, - - data: function( elem, name, data ) { - return dataUser.access( elem, name, data ); - }, - - removeData: function( elem, name ) { - dataUser.remove( elem, name ); - }, - - // TODO: Now that all calls to _data and _removeData have been replaced - // with direct calls to dataPriv methods, these can be deprecated. - _data: function( elem, name, data ) { - return dataPriv.access( elem, name, data ); - }, - - _removeData: function( elem, name ) { - dataPriv.remove( elem, name ); - } -} ); - -jQuery.fn.extend( { - data: function( key, value ) { - var i, name, data, - elem = this[ 0 ], - attrs = elem && elem.attributes; - - // Gets all values - if ( key === undefined ) { - if ( this.length ) { - data = dataUser.get( elem ); - - if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { - i = attrs.length; - while ( i-- ) { - - // Support: IE 11 only - // The attrs elements can be null (#14894) - if ( attrs[ i ] ) { - name = attrs[ i ].name; - if ( name.indexOf( "data-" ) === 0 ) { - name = camelCase( name.slice( 5 ) ); - dataAttr( elem, name, data[ name ] ); - } - } - } - dataPriv.set( elem, "hasDataAttrs", true ); - } - } - - return data; - } - - // Sets multiple values - if ( typeof key === "object" ) { - return this.each( function() { - dataUser.set( this, key ); - } ); - } - - return access( this, function( value ) { - var data; - - // The calling jQuery object (element matches) is not empty - // (and therefore has an element appears at this[ 0 ]) and the - // `value` parameter was not undefined. An empty jQuery object - // will result in `undefined` for elem = this[ 0 ] which will - // throw an exception if an attempt to read a data cache is made. - if ( elem && value === undefined ) { - - // Attempt to get data from the cache - // The key will always be camelCased in Data - data = dataUser.get( elem, key ); - if ( data !== undefined ) { - return data; - } - - // Attempt to "discover" the data in - // HTML5 custom data-* attrs - data = dataAttr( elem, key ); - if ( data !== undefined ) { - return data; - } - - // We tried really hard, but the data doesn't exist. - return; - } - - // Set the data... - this.each( function() { - - // We always store the camelCased key - dataUser.set( this, key, value ); - } ); - }, null, value, arguments.length > 1, null, true ); - }, - - removeData: function( key ) { - return this.each( function() { - dataUser.remove( this, key ); - } ); - } -} ); - - -jQuery.extend( { - queue: function( elem, type, data ) { - var queue; - - if ( elem ) { - type = ( type || "fx" ) + "queue"; - queue = dataPriv.get( elem, type ); - - // Speed up dequeue by getting out quickly if this is just a lookup - if ( data ) { - if ( !queue || Array.isArray( data ) ) { - queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); - } else { - queue.push( data ); - } - } - return queue || []; - } - }, - - dequeue: function( elem, type ) { - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), - startLength = queue.length, - fn = queue.shift(), - hooks = jQuery._queueHooks( elem, type ), - next = function() { - jQuery.dequeue( elem, type ); - }; - - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - startLength--; - } - - if ( fn ) { - - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift( "inprogress" ); - } - - // Clear up the last queue stop function - delete hooks.stop; - fn.call( elem, next, hooks ); - } - - if ( !startLength && hooks ) { - hooks.empty.fire(); - } - }, - - // Not public - generate a queueHooks object, or return the current one - _queueHooks: function( elem, type ) { - var key = type + "queueHooks"; - return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { - empty: jQuery.Callbacks( "once memory" ).add( function() { - dataPriv.remove( elem, [ type + "queue", key ] ); - } ) - } ); - } -} ); - -jQuery.fn.extend( { - queue: function( type, data ) { - var setter = 2; - - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - setter--; - } - - if ( arguments.length < setter ) { - return jQuery.queue( this[ 0 ], type ); - } - - return data === undefined ? - this : - this.each( function() { - var queue = jQuery.queue( this, type, data ); - - // Ensure a hooks for this queue - jQuery._queueHooks( this, type ); - - if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - } ); - }, - dequeue: function( type ) { - return this.each( function() { - jQuery.dequeue( this, type ); - } ); - }, - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - }, - - // Get a promise resolved when queues of a certain type - // are emptied (fx is the type by default) - promise: function( type, obj ) { - var tmp, - count = 1, - defer = jQuery.Deferred(), - elements = this, - i = this.length, - resolve = function() { - if ( !( --count ) ) { - defer.resolveWith( elements, [ elements ] ); - } - }; - - if ( typeof type !== "string" ) { - obj = type; - type = undefined; - } - type = type || "fx"; - - while ( i-- ) { - tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); - if ( tmp && tmp.empty ) { - count++; - tmp.empty.add( resolve ); - } - } - resolve(); - return defer.promise( obj ); - } -} ); -var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; - -var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); - - -var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; - -var documentElement = document.documentElement; - - - - var isAttached = function( elem ) { - return jQuery.contains( elem.ownerDocument, elem ); - }, - composed = { composed: true }; - - // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only - // Check attachment across shadow DOM boundaries when possible (gh-3504) - // Support: iOS 10.0-10.2 only - // Early iOS 10 versions support `attachShadow` but not `getRootNode`, - // leading to errors. We need to check for `getRootNode`. - if ( documentElement.getRootNode ) { - isAttached = function( elem ) { - return jQuery.contains( elem.ownerDocument, elem ) || - elem.getRootNode( composed ) === elem.ownerDocument; - }; - } -var isHiddenWithinTree = function( elem, el ) { - - // isHiddenWithinTree might be called from jQuery#filter function; - // in that case, element will be second argument - elem = el || elem; - - // Inline style trumps all - return elem.style.display === "none" || - elem.style.display === "" && - - // Otherwise, check computed style - // Support: Firefox <=43 - 45 - // Disconnected elements can have computed display: none, so first confirm that elem is - // in the document. - isAttached( elem ) && - - jQuery.css( elem, "display" ) === "none"; - }; - - - -function adjustCSS( elem, prop, valueParts, tween ) { - var adjusted, scale, - maxIterations = 20, - currentValue = tween ? - function() { - return tween.cur(); - } : - function() { - return jQuery.css( elem, prop, "" ); - }, - initial = currentValue(), - unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), - - // Starting value computation is required for potential unit mismatches - initialInUnit = elem.nodeType && - ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && - rcssNum.exec( jQuery.css( elem, prop ) ); - - if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { - - // Support: Firefox <=54 - // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144) - initial = initial / 2; - - // Trust units reported by jQuery.css - unit = unit || initialInUnit[ 3 ]; - - // Iteratively approximate from a nonzero starting point - initialInUnit = +initial || 1; - - while ( maxIterations-- ) { - - // Evaluate and update our best guess (doubling guesses that zero out). - // Finish if the scale equals or crosses 1 (making the old*new product non-positive). - jQuery.style( elem, prop, initialInUnit + unit ); - if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) { - maxIterations = 0; - } - initialInUnit = initialInUnit / scale; - - } - - initialInUnit = initialInUnit * 2; - jQuery.style( elem, prop, initialInUnit + unit ); - - // Make sure we update the tween properties later on - valueParts = valueParts || []; - } - - if ( valueParts ) { - initialInUnit = +initialInUnit || +initial || 0; - - // Apply relative offset (+=/-=) if specified - adjusted = valueParts[ 1 ] ? - initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : - +valueParts[ 2 ]; - if ( tween ) { - tween.unit = unit; - tween.start = initialInUnit; - tween.end = adjusted; - } - } - return adjusted; -} - - -var defaultDisplayMap = {}; - -function getDefaultDisplay( elem ) { - var temp, - doc = elem.ownerDocument, - nodeName = elem.nodeName, - display = defaultDisplayMap[ nodeName ]; - - if ( display ) { - return display; - } - - temp = doc.body.appendChild( doc.createElement( nodeName ) ); - display = jQuery.css( temp, "display" ); - - temp.parentNode.removeChild( temp ); - - if ( display === "none" ) { - display = "block"; - } - defaultDisplayMap[ nodeName ] = display; - - return display; -} - -function showHide( elements, show ) { - var display, elem, - values = [], - index = 0, - length = elements.length; - - // Determine new display value for elements that need to change - for ( ; index < length; index++ ) { - elem = elements[ index ]; - if ( !elem.style ) { - continue; - } - - display = elem.style.display; - if ( show ) { - - // Since we force visibility upon cascade-hidden elements, an immediate (and slow) - // check is required in this first loop unless we have a nonempty display value (either - // inline or about-to-be-restored) - if ( display === "none" ) { - values[ index ] = dataPriv.get( elem, "display" ) || null; - if ( !values[ index ] ) { - elem.style.display = ""; - } - } - if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) { - values[ index ] = getDefaultDisplay( elem ); - } - } else { - if ( display !== "none" ) { - values[ index ] = "none"; - - // Remember what we're overwriting - dataPriv.set( elem, "display", display ); - } - } - } - - // Set the display of the elements in a second loop to avoid constant reflow - for ( index = 0; index < length; index++ ) { - if ( values[ index ] != null ) { - elements[ index ].style.display = values[ index ]; - } - } - - return elements; -} - -jQuery.fn.extend( { - show: function() { - return showHide( this, true ); - }, - hide: function() { - return showHide( this ); - }, - toggle: function( state ) { - if ( typeof state === "boolean" ) { - return state ? this.show() : this.hide(); - } - - return this.each( function() { - if ( isHiddenWithinTree( this ) ) { - jQuery( this ).show(); - } else { - jQuery( this ).hide(); - } - } ); - } -} ); -var rcheckableType = ( /^(?:checkbox|radio)$/i ); - -var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i ); - -var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); - - - -( function() { - var fragment = document.createDocumentFragment(), - div = fragment.appendChild( document.createElement( "div" ) ), - input = document.createElement( "input" ); - - // Support: Android 4.0 - 4.3 only - // Check state lost if the name is set (#11217) - // Support: Windows Web Apps (WWA) - // `name` and `type` must use .setAttribute for WWA (#14901) - input.setAttribute( "type", "radio" ); - input.setAttribute( "checked", "checked" ); - input.setAttribute( "name", "t" ); - - div.appendChild( input ); - - // Support: Android <=4.1 only - // Older WebKit doesn't clone checked state correctly in fragments - support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; - - // Support: IE <=11 only - // Make sure textarea (and checkbox) defaultValue is properly cloned - div.innerHTML = ""; - support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; - - // Support: IE <=9 only - // IE <=9 replaces "; - support.option = !!div.lastChild; -} )(); - - -// We have to close these tags to support XHTML (#13200) -var wrapMap = { - - // XHTML parsers do not magically insert elements in the - // same way that tag soup parsers do. So we cannot shorten - // this by omitting or other required elements. - thead: [ 1, "", "
" ], - col: [ 2, "", "
" ], - tr: [ 2, "", "
" ], - td: [ 3, "", "
" ], - - _default: [ 0, "", "" ] -}; - -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - -// Support: IE <=9 only -if ( !support.option ) { - wrapMap.optgroup = wrapMap.option = [ 1, "" ]; -} - - -function getAll( context, tag ) { - - // Support: IE <=9 - 11 only - // Use typeof to avoid zero-argument method invocation on host objects (#15151) - var ret; - - if ( typeof context.getElementsByTagName !== "undefined" ) { - ret = context.getElementsByTagName( tag || "*" ); - - } else if ( typeof context.querySelectorAll !== "undefined" ) { - ret = context.querySelectorAll( tag || "*" ); - - } else { - ret = []; - } - - if ( tag === undefined || tag && nodeName( context, tag ) ) { - return jQuery.merge( [ context ], ret ); - } - - return ret; -} - - -// Mark scripts as having already been evaluated -function setGlobalEval( elems, refElements ) { - var i = 0, - l = elems.length; - - for ( ; i < l; i++ ) { - dataPriv.set( - elems[ i ], - "globalEval", - !refElements || dataPriv.get( refElements[ i ], "globalEval" ) - ); - } -} - - -var rhtml = /<|&#?\w+;/; - -function buildFragment( elems, context, scripts, selection, ignored ) { - var elem, tmp, tag, wrap, attached, j, - fragment = context.createDocumentFragment(), - nodes = [], - i = 0, - l = elems.length; - - for ( ; i < l; i++ ) { - elem = elems[ i ]; - - if ( elem || elem === 0 ) { - - // Add nodes directly - if ( toType( elem ) === "object" ) { - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); - - // Convert non-html into a text node - } else if ( !rhtml.test( elem ) ) { - nodes.push( context.createTextNode( elem ) ); - - // Convert html into DOM nodes - } else { - tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); - - // Deserialize a standard representation - tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); - wrap = wrapMap[ tag ] || wrapMap._default; - tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; - - // Descend through wrappers to the right content - j = wrap[ 0 ]; - while ( j-- ) { - tmp = tmp.lastChild; - } - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( nodes, tmp.childNodes ); - - // Remember the top-level container - tmp = fragment.firstChild; - - // Ensure the created nodes are orphaned (#12392) - tmp.textContent = ""; - } - } - } - - // Remove wrapper from fragment - fragment.textContent = ""; - - i = 0; - while ( ( elem = nodes[ i++ ] ) ) { - - // Skip elements already in the context collection (trac-4087) - if ( selection && jQuery.inArray( elem, selection ) > -1 ) { - if ( ignored ) { - ignored.push( elem ); - } - continue; - } - - attached = isAttached( elem ); - - // Append to fragment - tmp = getAll( fragment.appendChild( elem ), "script" ); - - // Preserve script evaluation history - if ( attached ) { - setGlobalEval( tmp ); - } - - // Capture executables - if ( scripts ) { - j = 0; - while ( ( elem = tmp[ j++ ] ) ) { - if ( rscriptType.test( elem.type || "" ) ) { - scripts.push( elem ); - } - } - } - } - - return fragment; -} - - -var - rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, - rtypenamespace = /^([^.]*)(?:\.(.+)|)/; - -function returnTrue() { - return true; -} - -function returnFalse() { - return false; -} - -// Support: IE <=9 - 11+ -// focus() and blur() are asynchronous, except when they are no-op. -// So expect focus to be synchronous when the element is already active, -// and blur to be synchronous when the element is not already active. -// (focus and blur are always synchronous in other supported browsers, -// this just defines when we can count on it). -function expectSync( elem, type ) { - return ( elem === safeActiveElement() ) === ( type === "focus" ); -} - -// Support: IE <=9 only -// Accessing document.activeElement can throw unexpectedly -// https://bugs.jquery.com/ticket/13393 -function safeActiveElement() { - try { - return document.activeElement; - } catch ( err ) { } -} - -function on( elem, types, selector, data, fn, one ) { - var origFn, type; - - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { - - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - on( elem, type, selector, data, types[ type ], one ); - } - return elem; - } - - if ( data == null && fn == null ) { - - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return elem; - } - - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return elem.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - } ); -} - -/* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ -jQuery.event = { - - global: {}, - - add: function( elem, types, handler, data, selector ) { - - var handleObjIn, eventHandle, tmp, - events, t, handleObj, - special, handlers, type, namespaces, origType, - elemData = dataPriv.get( elem ); - - // Only attach events to objects that accept data - if ( !acceptData( elem ) ) { - return; - } - - // Caller can pass in an object of custom data in lieu of the handler - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; - } - - // Ensure that invalid selectors throw exceptions at attach time - // Evaluate against documentElement in case elem is a non-element node (e.g., document) - if ( selector ) { - jQuery.find.matchesSelector( documentElement, selector ); - } - - // Make sure that the handler has a unique ID, used to find/remove it later - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure and main handler, if this is the first - if ( !( events = elemData.events ) ) { - events = elemData.events = Object.create( null ); - } - if ( !( eventHandle = elemData.handle ) ) { - eventHandle = elemData.handle = function( e ) { - - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? - jQuery.event.dispatch.apply( elem, arguments ) : undefined; - }; - } - - // Handle multiple events separated by a space - types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[ t ] ) || []; - type = origType = tmp[ 1 ]; - namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); - - // There *must* be a type, no attaching namespace-only handlers - if ( !type ) { - continue; - } - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[ type ] || {}; - - // If selector defined, determine special event api type, otherwise given type - type = ( selector ? special.delegateType : special.bindType ) || type; - - // Update special based on newly reset type - special = jQuery.event.special[ type ] || {}; - - // handleObj is passed to all event handlers - handleObj = jQuery.extend( { - type: type, - origType: origType, - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - needsContext: selector && jQuery.expr.match.needsContext.test( selector ), - namespace: namespaces.join( "." ) - }, handleObjIn ); - - // Init the event handler queue if we're the first - if ( !( handlers = events[ type ] ) ) { - handlers = events[ type ] = []; - handlers.delegateCount = 0; - - // Only use addEventListener if the special events handler returns false - if ( !special.setup || - special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add to the element's handler list, delegates in front - if ( selector ) { - handlers.splice( handlers.delegateCount++, 0, handleObj ); - } else { - handlers.push( handleObj ); - } - - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; - } - - }, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, selector, mappedTypes ) { - - var j, origCount, tmp, - events, t, handleObj, - special, handlers, type, namespaces, origType, - elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); - - if ( !elemData || !( events = elemData.events ) ) { - return; - } - - // Once for each type.namespace in types; type may be omitted - types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[ t ] ) || []; - type = origType = tmp[ 1 ]; - namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); - - // Unbind all events (on this namespace, if provided) for the element - if ( !type ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); - } - continue; - } - - special = jQuery.event.special[ type ] || {}; - type = ( selector ? special.delegateType : special.bindType ) || type; - handlers = events[ type ] || []; - tmp = tmp[ 2 ] && - new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); - - // Remove matching events - origCount = j = handlers.length; - while ( j-- ) { - handleObj = handlers[ j ]; - - if ( ( mappedTypes || origType === handleObj.origType ) && - ( !handler || handler.guid === handleObj.guid ) && - ( !tmp || tmp.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || - selector === "**" && handleObj.selector ) ) { - handlers.splice( j, 1 ); - - if ( handleObj.selector ) { - handlers.delegateCount--; - } - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - } - - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if ( origCount && !handlers.length ) { - if ( !special.teardown || - special.teardown.call( elem, namespaces, elemData.handle ) === false ) { - - jQuery.removeEvent( elem, type, elemData.handle ); - } - - delete events[ type ]; - } - } - - // Remove data and the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - dataPriv.remove( elem, "handle events" ); - } - }, - - dispatch: function( nativeEvent ) { - - var i, j, ret, matched, handleObj, handlerQueue, - args = new Array( arguments.length ), - - // Make a writable jQuery.Event from the native event object - event = jQuery.event.fix( nativeEvent ), - - handlers = ( - dataPriv.get( this, "events" ) || Object.create( null ) - )[ event.type ] || [], - special = jQuery.event.special[ event.type ] || {}; - - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[ 0 ] = event; - - for ( i = 1; i < arguments.length; i++ ) { - args[ i ] = arguments[ i ]; - } - - event.delegateTarget = this; - - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } - - // Determine handlers - handlerQueue = jQuery.event.handlers.call( this, event, handlers ); - - // Run delegates first; they may want to stop propagation beneath us - i = 0; - while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { - event.currentTarget = matched.elem; - - j = 0; - while ( ( handleObj = matched.handlers[ j++ ] ) && - !event.isImmediatePropagationStopped() ) { - - // If the event is namespaced, then each handler is only invoked if it is - // specially universal or its namespaces are a superset of the event's. - if ( !event.rnamespace || handleObj.namespace === false || - event.rnamespace.test( handleObj.namespace ) ) { - - event.handleObj = handleObj; - event.data = handleObj.data; - - ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || - handleObj.handler ).apply( matched.elem, args ); - - if ( ret !== undefined ) { - if ( ( event.result = ret ) === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } - - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } - - return event.result; - }, - - handlers: function( event, handlers ) { - var i, handleObj, sel, matchedHandlers, matchedSelectors, - handlerQueue = [], - delegateCount = handlers.delegateCount, - cur = event.target; - - // Find delegate handlers - if ( delegateCount && - - // Support: IE <=9 - // Black-hole SVG instance trees (trac-13180) - cur.nodeType && - - // Support: Firefox <=42 - // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861) - // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click - // Support: IE 11 only - // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343) - !( event.type === "click" && event.button >= 1 ) ) { - - for ( ; cur !== this; cur = cur.parentNode || this ) { - - // Don't check non-elements (#13208) - // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) - if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { - matchedHandlers = []; - matchedSelectors = {}; - for ( i = 0; i < delegateCount; i++ ) { - handleObj = handlers[ i ]; - - // Don't conflict with Object.prototype properties (#13203) - sel = handleObj.selector + " "; - - if ( matchedSelectors[ sel ] === undefined ) { - matchedSelectors[ sel ] = handleObj.needsContext ? - jQuery( sel, this ).index( cur ) > -1 : - jQuery.find( sel, this, null, [ cur ] ).length; - } - if ( matchedSelectors[ sel ] ) { - matchedHandlers.push( handleObj ); - } - } - if ( matchedHandlers.length ) { - handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); - } - } - } - } - - // Add the remaining (directly-bound) handlers - cur = this; - if ( delegateCount < handlers.length ) { - handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } ); - } - - return handlerQueue; - }, - - addProp: function( name, hook ) { - Object.defineProperty( jQuery.Event.prototype, name, { - enumerable: true, - configurable: true, - - get: isFunction( hook ) ? - function() { - if ( this.originalEvent ) { - return hook( this.originalEvent ); - } - } : - function() { - if ( this.originalEvent ) { - return this.originalEvent[ name ]; - } - }, - - set: function( value ) { - Object.defineProperty( this, name, { - enumerable: true, - configurable: true, - writable: true, - value: value - } ); - } - } ); - }, - - fix: function( originalEvent ) { - return originalEvent[ jQuery.expando ] ? - originalEvent : - new jQuery.Event( originalEvent ); - }, - - special: { - load: { - - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - click: { - - // Utilize native event to ensure correct state for checkable inputs - setup: function( data ) { - - // For mutual compressibility with _default, replace `this` access with a local var. - // `|| data` is dead code meant only to preserve the variable through minification. - var el = this || data; - - // Claim the first handler - if ( rcheckableType.test( el.type ) && - el.click && nodeName( el, "input" ) ) { - - // dataPriv.set( el, "click", ... ) - leverageNative( el, "click", returnTrue ); - } - - // Return false to allow normal processing in the caller - return false; - }, - trigger: function( data ) { - - // For mutual compressibility with _default, replace `this` access with a local var. - // `|| data` is dead code meant only to preserve the variable through minification. - var el = this || data; - - // Force setup before triggering a click - if ( rcheckableType.test( el.type ) && - el.click && nodeName( el, "input" ) ) { - - leverageNative( el, "click" ); - } - - // Return non-false to allow normal event-path propagation - return true; - }, - - // For cross-browser consistency, suppress native .click() on links - // Also prevent it if we're currently inside a leveraged native-event stack - _default: function( event ) { - var target = event.target; - return rcheckableType.test( target.type ) && - target.click && nodeName( target, "input" ) && - dataPriv.get( target, "click" ) || - nodeName( target, "a" ); - } - }, - - beforeunload: { - postDispatch: function( event ) { - - // Support: Firefox 20+ - // Firefox doesn't alert if the returnValue field is not set. - if ( event.result !== undefined && event.originalEvent ) { - event.originalEvent.returnValue = event.result; - } - } - } - } -}; - -// Ensure the presence of an event listener that handles manually-triggered -// synthetic events by interrupting progress until reinvoked in response to -// *native* events that it fires directly, ensuring that state changes have -// already occurred before other listeners are invoked. -function leverageNative( el, type, expectSync ) { - - // Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add - if ( !expectSync ) { - if ( dataPriv.get( el, type ) === undefined ) { - jQuery.event.add( el, type, returnTrue ); - } - return; - } - - // Register the controller as a special universal handler for all event namespaces - dataPriv.set( el, type, false ); - jQuery.event.add( el, type, { - namespace: false, - handler: function( event ) { - var notAsync, result, - saved = dataPriv.get( this, type ); - - if ( ( event.isTrigger & 1 ) && this[ type ] ) { - - // Interrupt processing of the outer synthetic .trigger()ed event - // Saved data should be false in such cases, but might be a leftover capture object - // from an async native handler (gh-4350) - if ( !saved.length ) { - - // Store arguments for use when handling the inner native event - // There will always be at least one argument (an event object), so this array - // will not be confused with a leftover capture object. - saved = slice.call( arguments ); - dataPriv.set( this, type, saved ); - - // Trigger the native event and capture its result - // Support: IE <=9 - 11+ - // focus() and blur() are asynchronous - notAsync = expectSync( this, type ); - this[ type ](); - result = dataPriv.get( this, type ); - if ( saved !== result || notAsync ) { - dataPriv.set( this, type, false ); - } else { - result = {}; - } - if ( saved !== result ) { - - // Cancel the outer synthetic event - event.stopImmediatePropagation(); - event.preventDefault(); - return result.value; - } - - // If this is an inner synthetic event for an event with a bubbling surrogate - // (focus or blur), assume that the surrogate already propagated from triggering the - // native event and prevent that from happening again here. - // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the - // bubbling surrogate propagates *after* the non-bubbling base), but that seems - // less bad than duplication. - } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { - event.stopPropagation(); - } - - // If this is a native event triggered above, everything is now in order - // Fire an inner synthetic event with the original arguments - } else if ( saved.length ) { - - // ...and capture the result - dataPriv.set( this, type, { - value: jQuery.event.trigger( - - // Support: IE <=9 - 11+ - // Extend with the prototype to reset the above stopImmediatePropagation() - jQuery.extend( saved[ 0 ], jQuery.Event.prototype ), - saved.slice( 1 ), - this - ) - } ); - - // Abort handling of the native event - event.stopImmediatePropagation(); - } - } - } ); -} - -jQuery.removeEvent = function( elem, type, handle ) { - - // This "if" is needed for plain objects - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle ); - } -}; - -jQuery.Event = function( src, props ) { - - // Allow instantiation without the 'new' keyword - if ( !( this instanceof jQuery.Event ) ) { - return new jQuery.Event( src, props ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = src.defaultPrevented || - src.defaultPrevented === undefined && - - // Support: Android <=2.3 only - src.returnValue === false ? - returnTrue : - returnFalse; - - // Create target properties - // Support: Safari <=6 - 7 only - // Target should not be a text node (#504, #13143) - this.target = ( src.target && src.target.nodeType === 3 ) ? - src.target.parentNode : - src.target; - - this.currentTarget = src.currentTarget; - this.relatedTarget = src.relatedTarget; - - // Event type - } else { - this.type = src; - } - - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); - } - - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || Date.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - constructor: jQuery.Event, - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse, - isSimulated: false, - - preventDefault: function() { - var e = this.originalEvent; - - this.isDefaultPrevented = returnTrue; - - if ( e && !this.isSimulated ) { - e.preventDefault(); - } - }, - stopPropagation: function() { - var e = this.originalEvent; - - this.isPropagationStopped = returnTrue; - - if ( e && !this.isSimulated ) { - e.stopPropagation(); - } - }, - stopImmediatePropagation: function() { - var e = this.originalEvent; - - this.isImmediatePropagationStopped = returnTrue; - - if ( e && !this.isSimulated ) { - e.stopImmediatePropagation(); - } - - this.stopPropagation(); - } -}; - -// Includes all common event props including KeyEvent and MouseEvent specific props -jQuery.each( { - altKey: true, - bubbles: true, - cancelable: true, - changedTouches: true, - ctrlKey: true, - detail: true, - eventPhase: true, - metaKey: true, - pageX: true, - pageY: true, - shiftKey: true, - view: true, - "char": true, - code: true, - charCode: true, - key: true, - keyCode: true, - button: true, - buttons: true, - clientX: true, - clientY: true, - offsetX: true, - offsetY: true, - pointerId: true, - pointerType: true, - screenX: true, - screenY: true, - targetTouches: true, - toElement: true, - touches: true, - - which: function( event ) { - var button = event.button; - - // Add which for key events - if ( event.which == null && rkeyEvent.test( event.type ) ) { - return event.charCode != null ? event.charCode : event.keyCode; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) { - if ( button & 1 ) { - return 1; - } - - if ( button & 2 ) { - return 3; - } - - if ( button & 4 ) { - return 2; - } - - return 0; - } - - return event.which; - } -}, jQuery.event.addProp ); - -jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { - jQuery.event.special[ type ] = { - - // Utilize native event if possible so blur/focus sequence is correct - setup: function() { - - // Claim the first handler - // dataPriv.set( this, "focus", ... ) - // dataPriv.set( this, "blur", ... ) - leverageNative( this, type, expectSync ); - - // Return false to allow normal processing in the caller - return false; - }, - trigger: function() { - - // Force setup before trigger - leverageNative( this, type ); - - // Return non-false to allow normal event-path propagation - return true; - }, - - delegateType: delegateType - }; -} ); - -// Create mouseenter/leave events using mouseover/out and event-time checks -// so that event delegation works in jQuery. -// Do the same for pointerenter/pointerleave and pointerover/pointerout -// -// Support: Safari 7 only -// Safari sends mouseenter too often; see: -// https://bugs.chromium.org/p/chromium/issues/detail?id=470258 -// for the description of the bug (it existed in older Chrome versions as well). -jQuery.each( { - mouseenter: "mouseover", - mouseleave: "mouseout", - pointerenter: "pointerover", - pointerleave: "pointerout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - delegateType: fix, - bindType: fix, - - handle: function( event ) { - var ret, - target = this, - related = event.relatedTarget, - handleObj = event.handleObj; - - // For mouseenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { - event.type = handleObj.origType; - ret = handleObj.handler.apply( this, arguments ); - event.type = fix; - } - return ret; - } - }; -} ); - -jQuery.fn.extend( { - - on: function( types, selector, data, fn ) { - return on( this, types, selector, data, fn ); - }, - one: function( types, selector, data, fn ) { - return on( this, types, selector, data, fn, 1 ); - }, - off: function( types, selector, fn ) { - var handleObj, type; - if ( types && types.preventDefault && types.handleObj ) { - - // ( event ) dispatched jQuery.Event - handleObj = types.handleObj; - jQuery( types.delegateTarget ).off( - handleObj.namespace ? - handleObj.origType + "." + handleObj.namespace : - handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if ( typeof types === "object" ) { - - // ( types-object [, selector] ) - for ( type in types ) { - this.off( type, selector, types[ type ] ); - } - return this; - } - if ( selector === false || typeof selector === "function" ) { - - // ( types [, fn] ) - fn = selector; - selector = undefined; - } - if ( fn === false ) { - fn = returnFalse; - } - return this.each( function() { - jQuery.event.remove( this, types, fn, selector ); - } ); - } -} ); - - -var - - // Support: IE <=10 - 11, Edge 12 - 13 only - // In IE/Edge using regex groups here causes severe slowdowns. - // See https://connect.microsoft.com/IE/feedback/details/1736512/ - rnoInnerhtml = /\s*$/g; - -// Prefer a tbody over its parent table for containing new rows -function manipulationTarget( elem, content ) { - if ( nodeName( elem, "table" ) && - nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { - - return jQuery( elem ).children( "tbody" )[ 0 ] || elem; - } - - return elem; -} - -// Replace/restore the type attribute of script elements for safe DOM manipulation -function disableScript( elem ) { - elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; - return elem; -} -function restoreScript( elem ) { - if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) { - elem.type = elem.type.slice( 5 ); - } else { - elem.removeAttribute( "type" ); - } - - return elem; -} - -function cloneCopyEvent( src, dest ) { - var i, l, type, pdataOld, udataOld, udataCur, events; - - if ( dest.nodeType !== 1 ) { - return; - } - - // 1. Copy private data: events, handlers, etc. - if ( dataPriv.hasData( src ) ) { - pdataOld = dataPriv.get( src ); - events = pdataOld.events; - - if ( events ) { - dataPriv.remove( dest, "handle events" ); - - for ( type in events ) { - for ( i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( dest, type, events[ type ][ i ] ); - } - } - } - } - - // 2. Copy user data - if ( dataUser.hasData( src ) ) { - udataOld = dataUser.access( src ); - udataCur = jQuery.extend( {}, udataOld ); - - dataUser.set( dest, udataCur ); - } -} - -// Fix IE bugs, see support tests -function fixInput( src, dest ) { - var nodeName = dest.nodeName.toLowerCase(); - - // Fails to persist the checked state of a cloned checkbox or radio button. - if ( nodeName === "input" && rcheckableType.test( src.type ) ) { - dest.checked = src.checked; - - // Fails to return the selected option to the default selected state when cloning options - } else if ( nodeName === "input" || nodeName === "textarea" ) { - dest.defaultValue = src.defaultValue; - } -} - -function domManip( collection, args, callback, ignored ) { - - // Flatten any nested arrays - args = flat( args ); - - var fragment, first, scripts, hasScripts, node, doc, - i = 0, - l = collection.length, - iNoClone = l - 1, - value = args[ 0 ], - valueIsFunction = isFunction( value ); - - // We can't cloneNode fragments that contain checked, in WebKit - if ( valueIsFunction || - ( l > 1 && typeof value === "string" && - !support.checkClone && rchecked.test( value ) ) ) { - return collection.each( function( index ) { - var self = collection.eq( index ); - if ( valueIsFunction ) { - args[ 0 ] = value.call( this, index, self.html() ); - } - domManip( self, args, callback, ignored ); - } ); - } - - if ( l ) { - fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); - first = fragment.firstChild; - - if ( fragment.childNodes.length === 1 ) { - fragment = first; - } - - // Require either new content or an interest in ignored elements to invoke the callback - if ( first || ignored ) { - scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); - hasScripts = scripts.length; - - // Use the original fragment for the last item - // instead of the first because it can end up - // being emptied incorrectly in certain situations (#8070). - for ( ; i < l; i++ ) { - node = fragment; - - if ( i !== iNoClone ) { - node = jQuery.clone( node, true, true ); - - // Keep references to cloned scripts for later restoration - if ( hasScripts ) { - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( scripts, getAll( node, "script" ) ); - } - } - - callback.call( collection[ i ], node, i ); - } - - if ( hasScripts ) { - doc = scripts[ scripts.length - 1 ].ownerDocument; - - // Reenable scripts - jQuery.map( scripts, restoreScript ); - - // Evaluate executable scripts on first document insertion - for ( i = 0; i < hasScripts; i++ ) { - node = scripts[ i ]; - if ( rscriptType.test( node.type || "" ) && - !dataPriv.access( node, "globalEval" ) && - jQuery.contains( doc, node ) ) { - - if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { - - // Optional AJAX dependency, but won't run scripts if not present - if ( jQuery._evalUrl && !node.noModule ) { - jQuery._evalUrl( node.src, { - nonce: node.nonce || node.getAttribute( "nonce" ) - }, doc ); - } - } else { - DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); - } - } - } - } - } - } - - return collection; -} - -function remove( elem, selector, keepData ) { - var node, - nodes = selector ? jQuery.filter( selector, elem ) : elem, - i = 0; - - for ( ; ( node = nodes[ i ] ) != null; i++ ) { - if ( !keepData && node.nodeType === 1 ) { - jQuery.cleanData( getAll( node ) ); - } - - if ( node.parentNode ) { - if ( keepData && isAttached( node ) ) { - setGlobalEval( getAll( node, "script" ) ); - } - node.parentNode.removeChild( node ); - } - } - - return elem; -} - -jQuery.extend( { - htmlPrefilter: function( html ) { - return html; - }, - - clone: function( elem, dataAndEvents, deepDataAndEvents ) { - var i, l, srcElements, destElements, - clone = elem.cloneNode( true ), - inPage = isAttached( elem ); - - // Fix IE cloning issues - if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && - !jQuery.isXMLDoc( elem ) ) { - - // We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2 - destElements = getAll( clone ); - srcElements = getAll( elem ); - - for ( i = 0, l = srcElements.length; i < l; i++ ) { - fixInput( srcElements[ i ], destElements[ i ] ); - } - } - - // Copy the events from the original to the clone - if ( dataAndEvents ) { - if ( deepDataAndEvents ) { - srcElements = srcElements || getAll( elem ); - destElements = destElements || getAll( clone ); - - for ( i = 0, l = srcElements.length; i < l; i++ ) { - cloneCopyEvent( srcElements[ i ], destElements[ i ] ); - } - } else { - cloneCopyEvent( elem, clone ); - } - } - - // Preserve script evaluation history - destElements = getAll( clone, "script" ); - if ( destElements.length > 0 ) { - setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); - } - - // Return the cloned set - return clone; - }, - - cleanData: function( elems ) { - var data, elem, type, - special = jQuery.event.special, - i = 0; - - for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { - if ( acceptData( elem ) ) { - if ( ( data = elem[ dataPriv.expando ] ) ) { - if ( data.events ) { - for ( type in data.events ) { - if ( special[ type ] ) { - jQuery.event.remove( elem, type ); - - // This is a shortcut to avoid jQuery.event.remove's overhead - } else { - jQuery.removeEvent( elem, type, data.handle ); - } - } - } - - // Support: Chrome <=35 - 45+ - // Assign undefined instead of using delete, see Data#remove - elem[ dataPriv.expando ] = undefined; - } - if ( elem[ dataUser.expando ] ) { - - // Support: Chrome <=35 - 45+ - // Assign undefined instead of using delete, see Data#remove - elem[ dataUser.expando ] = undefined; - } - } - } - } -} ); - -jQuery.fn.extend( { - detach: function( selector ) { - return remove( this, selector, true ); - }, - - remove: function( selector ) { - return remove( this, selector ); - }, - - text: function( value ) { - return access( this, function( value ) { - return value === undefined ? - jQuery.text( this ) : - this.empty().each( function() { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - this.textContent = value; - } - } ); - }, null, value, arguments.length ); - }, - - append: function() { - return domManip( this, arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.appendChild( elem ); - } - } ); - }, - - prepend: function() { - return domManip( this, arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.insertBefore( elem, target.firstChild ); - } - } ); - }, - - before: function() { - return domManip( this, arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this ); - } - } ); - }, - - after: function() { - return domManip( this, arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this.nextSibling ); - } - } ); - }, - - empty: function() { - var elem, - i = 0; - - for ( ; ( elem = this[ i ] ) != null; i++ ) { - if ( elem.nodeType === 1 ) { - - // Prevent memory leaks - jQuery.cleanData( getAll( elem, false ) ); - - // Remove any remaining nodes - elem.textContent = ""; - } - } - - return this; - }, - - clone: function( dataAndEvents, deepDataAndEvents ) { - dataAndEvents = dataAndEvents == null ? false : dataAndEvents; - deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; - - return this.map( function() { - return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); - } ); - }, - - html: function( value ) { - return access( this, function( value ) { - var elem = this[ 0 ] || {}, - i = 0, - l = this.length; - - if ( value === undefined && elem.nodeType === 1 ) { - return elem.innerHTML; - } - - // See if we can take a shortcut and just use innerHTML - if ( typeof value === "string" && !rnoInnerhtml.test( value ) && - !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { - - value = jQuery.htmlPrefilter( value ); - - try { - for ( ; i < l; i++ ) { - elem = this[ i ] || {}; - - // Remove element nodes and prevent memory leaks - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - elem.innerHTML = value; - } - } - - elem = 0; - - // If using innerHTML throws an exception, use the fallback method - } catch ( e ) {} - } - - if ( elem ) { - this.empty().append( value ); - } - }, null, value, arguments.length ); - }, - - replaceWith: function() { - var ignored = []; - - // Make the changes, replacing each non-ignored context element with the new content - return domManip( this, arguments, function( elem ) { - var parent = this.parentNode; - - if ( jQuery.inArray( this, ignored ) < 0 ) { - jQuery.cleanData( getAll( this ) ); - if ( parent ) { - parent.replaceChild( elem, this ); - } - } - - // Force callback invocation - }, ignored ); - } -} ); - -jQuery.each( { - appendTo: "append", - prependTo: "prepend", - insertBefore: "before", - insertAfter: "after", - replaceAll: "replaceWith" -}, function( name, original ) { - jQuery.fn[ name ] = function( selector ) { - var elems, - ret = [], - insert = jQuery( selector ), - last = insert.length - 1, - i = 0; - - for ( ; i <= last; i++ ) { - elems = i === last ? this : this.clone( true ); - jQuery( insert[ i ] )[ original ]( elems ); - - // Support: Android <=4.0 only, PhantomJS 1 only - // .get() because push.apply(_, arraylike) throws on ancient WebKit - push.apply( ret, elems.get() ); - } - - return this.pushStack( ret ); - }; -} ); -var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); - -var getStyles = function( elem ) { - - // Support: IE <=11 only, Firefox <=30 (#15098, #14150) - // IE throws on elements created in popups - // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" - var view = elem.ownerDocument.defaultView; - - if ( !view || !view.opener ) { - view = window; - } - - return view.getComputedStyle( elem ); - }; - -var swap = function( elem, options, callback ) { - var ret, name, - old = {}; - - // Remember the old values, and insert the new ones - for ( name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } - - ret = callback.call( elem ); - - // Revert the old values - for ( name in options ) { - elem.style[ name ] = old[ name ]; - } - - return ret; -}; - - -var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); - - - -( function() { - - // Executing both pixelPosition & boxSizingReliable tests require only one layout - // so they're executed at the same time to save the second computation. - function computeStyleTests() { - - // This is a singleton, we need to execute it only once - if ( !div ) { - return; - } - - container.style.cssText = "position:absolute;left:-11111px;width:60px;" + - "margin-top:1px;padding:0;border:0"; - div.style.cssText = - "position:relative;display:block;box-sizing:border-box;overflow:scroll;" + - "margin:auto;border:1px;padding:1px;" + - "width:60%;top:1%"; - documentElement.appendChild( container ).appendChild( div ); - - var divStyle = window.getComputedStyle( div ); - pixelPositionVal = divStyle.top !== "1%"; - - // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44 - reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12; - - // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3 - // Some styles come back with percentage values, even though they shouldn't - div.style.right = "60%"; - pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36; - - // Support: IE 9 - 11 only - // Detect misreporting of content dimensions for box-sizing:border-box elements - boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36; - - // Support: IE 9 only - // Detect overflow:scroll screwiness (gh-3699) - // Support: Chrome <=64 - // Don't get tricked when zoom affects offsetWidth (gh-4029) - div.style.position = "absolute"; - scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12; - - documentElement.removeChild( container ); - - // Nullify the div so it wouldn't be stored in the memory and - // it will also be a sign that checks already performed - div = null; - } - - function roundPixelMeasures( measure ) { - return Math.round( parseFloat( measure ) ); - } - - var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, - reliableTrDimensionsVal, reliableMarginLeftVal, - container = document.createElement( "div" ), - div = document.createElement( "div" ); - - // Finish early in limited (non-browser) environments - if ( !div.style ) { - return; - } - - // Support: IE <=9 - 11 only - // Style of cloned element affects source element cloned (#8908) - div.style.backgroundClip = "content-box"; - div.cloneNode( true ).style.backgroundClip = ""; - support.clearCloneStyle = div.style.backgroundClip === "content-box"; - - jQuery.extend( support, { - boxSizingReliable: function() { - computeStyleTests(); - return boxSizingReliableVal; - }, - pixelBoxStyles: function() { - computeStyleTests(); - return pixelBoxStylesVal; - }, - pixelPosition: function() { - computeStyleTests(); - return pixelPositionVal; - }, - reliableMarginLeft: function() { - computeStyleTests(); - return reliableMarginLeftVal; - }, - scrollboxSize: function() { - computeStyleTests(); - return scrollboxSizeVal; - }, - - // Support: IE 9 - 11+, Edge 15 - 18+ - // IE/Edge misreport `getComputedStyle` of table rows with width/height - // set in CSS while `offset*` properties report correct values. - // Behavior in IE 9 is more subtle than in newer versions & it passes - // some versions of this test; make sure not to make it pass there! - reliableTrDimensions: function() { - var table, tr, trChild, trStyle; - if ( reliableTrDimensionsVal == null ) { - table = document.createElement( "table" ); - tr = document.createElement( "tr" ); - trChild = document.createElement( "div" ); - - table.style.cssText = "position:absolute;left:-11111px"; - tr.style.height = "1px"; - trChild.style.height = "9px"; - - documentElement - .appendChild( table ) - .appendChild( tr ) - .appendChild( trChild ); - - trStyle = window.getComputedStyle( tr ); - reliableTrDimensionsVal = parseInt( trStyle.height ) > 3; - - documentElement.removeChild( table ); - } - return reliableTrDimensionsVal; - } - } ); -} )(); - - -function curCSS( elem, name, computed ) { - var width, minWidth, maxWidth, ret, - - // Support: Firefox 51+ - // Retrieving style before computed somehow - // fixes an issue with getting wrong values - // on detached elements - style = elem.style; - - computed = computed || getStyles( elem ); - - // getPropertyValue is needed for: - // .css('filter') (IE 9 only, #12537) - // .css('--customProperty) (#3144) - if ( computed ) { - ret = computed.getPropertyValue( name ) || computed[ name ]; - - if ( ret === "" && !isAttached( elem ) ) { - ret = jQuery.style( elem, name ); - } - - // A tribute to the "awesome hack by Dean Edwards" - // Android Browser returns percentage for some values, - // but width seems to be reliably pixels. - // This is against the CSSOM draft spec: - // https://drafts.csswg.org/cssom/#resolved-values - if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { - - // Remember the original values - width = style.width; - minWidth = style.minWidth; - maxWidth = style.maxWidth; - - // Put in the new values to get a computed value out - style.minWidth = style.maxWidth = style.width = ret; - ret = computed.width; - - // Revert the changed values - style.width = width; - style.minWidth = minWidth; - style.maxWidth = maxWidth; - } - } - - return ret !== undefined ? - - // Support: IE <=9 - 11 only - // IE returns zIndex value as an integer. - ret + "" : - ret; -} - - -function addGetHookIf( conditionFn, hookFn ) { - - // Define the hook, we'll check on the first run if it's really needed. - return { - get: function() { - if ( conditionFn() ) { - - // Hook not needed (or it's not possible to use it due - // to missing dependency), remove it. - delete this.get; - return; - } - - // Hook needed; redefine it so that the support test is not executed again. - return ( this.get = hookFn ).apply( this, arguments ); - } - }; -} - - -var cssPrefixes = [ "Webkit", "Moz", "ms" ], - emptyStyle = document.createElement( "div" ).style, - vendorProps = {}; - -// Return a vendor-prefixed property or undefined -function vendorPropName( name ) { - - // Check for vendor prefixed names - var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), - i = cssPrefixes.length; - - while ( i-- ) { - name = cssPrefixes[ i ] + capName; - if ( name in emptyStyle ) { - return name; - } - } -} - -// Return a potentially-mapped jQuery.cssProps or vendor prefixed property -function finalPropName( name ) { - var final = jQuery.cssProps[ name ] || vendorProps[ name ]; - - if ( final ) { - return final; - } - if ( name in emptyStyle ) { - return name; - } - return vendorProps[ name ] = vendorPropName( name ) || name; -} - - -var - - // Swappable if display is none or starts with table - // except "table", "table-cell", or "table-caption" - // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display - rdisplayswap = /^(none|table(?!-c[ea]).+)/, - rcustomProp = /^--/, - cssShow = { position: "absolute", visibility: "hidden", display: "block" }, - cssNormalTransform = { - letterSpacing: "0", - fontWeight: "400" - }; - -function setPositiveNumber( _elem, value, subtract ) { - - // Any relative (+/-) values have already been - // normalized at this point - var matches = rcssNum.exec( value ); - return matches ? - - // Guard against undefined "subtract", e.g., when used as in cssHooks - Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : - value; -} - -function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { - var i = dimension === "width" ? 1 : 0, - extra = 0, - delta = 0; - - // Adjustment may not be necessary - if ( box === ( isBorderBox ? "border" : "content" ) ) { - return 0; - } - - for ( ; i < 4; i += 2 ) { - - // Both box models exclude margin - if ( box === "margin" ) { - delta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); - } - - // If we get here with a content-box, we're seeking "padding" or "border" or "margin" - if ( !isBorderBox ) { - - // Add padding - delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - - // For "border" or "margin", add border - if ( box !== "padding" ) { - delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - - // But still keep track of it otherwise - } else { - extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - - // If we get here with a border-box (content + padding + border), we're seeking "content" or - // "padding" or "margin" - } else { - - // For "content", subtract padding - if ( box === "content" ) { - delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - } - - // For "content" or "padding", subtract border - if ( box !== "margin" ) { - delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - } - } - - // Account for positive content-box scroll gutter when requested by providing computedVal - if ( !isBorderBox && computedVal >= 0 ) { - - // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border - // Assuming integer scroll gutter, subtract the rest and round down - delta += Math.max( 0, Math.ceil( - elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - - computedVal - - delta - - extra - - 0.5 - - // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter - // Use an explicit zero to avoid NaN (gh-3964) - ) ) || 0; - } - - return delta; -} - -function getWidthOrHeight( elem, dimension, extra ) { - - // Start with computed style - var styles = getStyles( elem ), - - // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322). - // Fake content-box until we know it's needed to know the true value. - boxSizingNeeded = !support.boxSizingReliable() || extra, - isBorderBox = boxSizingNeeded && - jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - valueIsBorderBox = isBorderBox, - - val = curCSS( elem, dimension, styles ), - offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); - - // Support: Firefox <=54 - // Return a confounding non-pixel value or feign ignorance, as appropriate. - if ( rnumnonpx.test( val ) ) { - if ( !extra ) { - return val; - } - val = "auto"; - } - - - // Support: IE 9 - 11 only - // Use offsetWidth/offsetHeight for when box sizing is unreliable. - // In those cases, the computed value can be trusted to be border-box. - if ( ( !support.boxSizingReliable() && isBorderBox || - - // Support: IE 10 - 11+, Edge 15 - 18+ - // IE/Edge misreport `getComputedStyle` of table rows with width/height - // set in CSS while `offset*` properties report correct values. - // Interestingly, in some cases IE 9 doesn't suffer from this issue. - !support.reliableTrDimensions() && nodeName( elem, "tr" ) || - - // Fall back to offsetWidth/offsetHeight when value is "auto" - // This happens for inline elements with no explicit setting (gh-3571) - val === "auto" || - - // Support: Android <=4.1 - 4.3 only - // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) - !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && - - // Make sure the element is visible & connected - elem.getClientRects().length ) { - - isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; - - // Where available, offsetWidth/offsetHeight approximate border box dimensions. - // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the - // retrieved value as a content box dimension. - valueIsBorderBox = offsetProp in elem; - if ( valueIsBorderBox ) { - val = elem[ offsetProp ]; - } - } - - // Normalize "" and auto - val = parseFloat( val ) || 0; - - // Adjust for the element's box model - return ( val + - boxModelAdjustment( - elem, - dimension, - extra || ( isBorderBox ? "border" : "content" ), - valueIsBorderBox, - styles, - - // Provide the current computed size to request scroll gutter calculation (gh-3589) - val - ) - ) + "px"; -} - -jQuery.extend( { - - // Add in style property hooks for overriding the default - // behavior of getting and setting a style property - cssHooks: { - opacity: { - get: function( elem, computed ) { - if ( computed ) { - - // We should always get a number back from opacity - var ret = curCSS( elem, "opacity" ); - return ret === "" ? "1" : ret; - } - } - } - }, - - // Don't automatically add "px" to these possibly-unitless properties - cssNumber: { - "animationIterationCount": true, - "columnCount": true, - "fillOpacity": true, - "flexGrow": true, - "flexShrink": true, - "fontWeight": true, - "gridArea": true, - "gridColumn": true, - "gridColumnEnd": true, - "gridColumnStart": true, - "gridRow": true, - "gridRowEnd": true, - "gridRowStart": true, - "lineHeight": true, - "opacity": true, - "order": true, - "orphans": true, - "widows": true, - "zIndex": true, - "zoom": true - }, - - // Add in properties whose names you wish to fix before - // setting or getting the value - cssProps: {}, - - // Get and set the style property on a DOM Node - style: function( elem, name, value, extra ) { - - // Don't set styles on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { - return; - } - - // Make sure that we're working with the right name - var ret, type, hooks, - origName = camelCase( name ), - isCustomProp = rcustomProp.test( name ), - style = elem.style; - - // Make sure that we're working with the right name. We don't - // want to query the value if it is a CSS custom property - // since they are user-defined. - if ( !isCustomProp ) { - name = finalPropName( origName ); - } - - // Gets hook for the prefixed version, then unprefixed version - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // Check if we're setting a value - if ( value !== undefined ) { - type = typeof value; - - // Convert "+=" or "-=" to relative numbers (#7345) - if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { - value = adjustCSS( elem, name, ret ); - - // Fixes bug #9237 - type = "number"; - } - - // Make sure that null and NaN values aren't set (#7116) - if ( value == null || value !== value ) { - return; - } - - // If a number was passed in, add the unit (except for certain CSS properties) - // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append - // "px" to a few hardcoded values. - if ( type === "number" && !isCustomProp ) { - value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); - } - - // background-* props affect original clone's values - if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { - style[ name ] = "inherit"; - } - - // If a hook was provided, use that value, otherwise just set the specified value - if ( !hooks || !( "set" in hooks ) || - ( value = hooks.set( elem, value, extra ) ) !== undefined ) { - - if ( isCustomProp ) { - style.setProperty( name, value ); - } else { - style[ name ] = value; - } - } - - } else { - - // If a hook was provided get the non-computed value from there - if ( hooks && "get" in hooks && - ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { - - return ret; - } - - // Otherwise just get the value from the style object - return style[ name ]; - } - }, - - css: function( elem, name, extra, styles ) { - var val, num, hooks, - origName = camelCase( name ), - isCustomProp = rcustomProp.test( name ); - - // Make sure that we're working with the right name. We don't - // want to modify the value if it is a CSS custom property - // since they are user-defined. - if ( !isCustomProp ) { - name = finalPropName( origName ); - } - - // Try prefixed name followed by the unprefixed name - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // If a hook was provided get the computed value from there - if ( hooks && "get" in hooks ) { - val = hooks.get( elem, true, extra ); - } - - // Otherwise, if a way to get the computed value exists, use that - if ( val === undefined ) { - val = curCSS( elem, name, styles ); - } - - // Convert "normal" to computed value - if ( val === "normal" && name in cssNormalTransform ) { - val = cssNormalTransform[ name ]; - } - - // Make numeric if forced or a qualifier was provided and val looks numeric - if ( extra === "" || extra ) { - num = parseFloat( val ); - return extra === true || isFinite( num ) ? num || 0 : val; - } - - return val; - } -} ); - -jQuery.each( [ "height", "width" ], function( _i, dimension ) { - jQuery.cssHooks[ dimension ] = { - get: function( elem, computed, extra ) { - if ( computed ) { - - // Certain elements can have dimension info if we invisibly show them - // but it must have a current display style that would benefit - return rdisplayswap.test( jQuery.css( elem, "display" ) ) && - - // Support: Safari 8+ - // Table columns in Safari have non-zero offsetWidth & zero - // getBoundingClientRect().width unless display is changed. - // Support: IE <=11 only - // Running getBoundingClientRect on a disconnected node - // in IE throws an error. - ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? - swap( elem, cssShow, function() { - return getWidthOrHeight( elem, dimension, extra ); - } ) : - getWidthOrHeight( elem, dimension, extra ); - } - }, - - set: function( elem, value, extra ) { - var matches, - styles = getStyles( elem ), - - // Only read styles.position if the test has a chance to fail - // to avoid forcing a reflow. - scrollboxSizeBuggy = !support.scrollboxSize() && - styles.position === "absolute", - - // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991) - boxSizingNeeded = scrollboxSizeBuggy || extra, - isBorderBox = boxSizingNeeded && - jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - subtract = extra ? - boxModelAdjustment( - elem, - dimension, - extra, - isBorderBox, - styles - ) : - 0; - - // Account for unreliable border-box dimensions by comparing offset* to computed and - // faking a content-box to get border and padding (gh-3699) - if ( isBorderBox && scrollboxSizeBuggy ) { - subtract -= Math.ceil( - elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - - parseFloat( styles[ dimension ] ) - - boxModelAdjustment( elem, dimension, "border", false, styles ) - - 0.5 - ); - } - - // Convert to pixels if value adjustment is needed - if ( subtract && ( matches = rcssNum.exec( value ) ) && - ( matches[ 3 ] || "px" ) !== "px" ) { - - elem.style[ dimension ] = value; - value = jQuery.css( elem, dimension ); - } - - return setPositiveNumber( elem, value, subtract ); - } - }; -} ); - -jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, - function( elem, computed ) { - if ( computed ) { - return ( parseFloat( curCSS( elem, "marginLeft" ) ) || - elem.getBoundingClientRect().left - - swap( elem, { marginLeft: 0 }, function() { - return elem.getBoundingClientRect().left; - } ) - ) + "px"; - } - } -); - -// These hooks are used by animate to expand properties -jQuery.each( { - margin: "", - padding: "", - border: "Width" -}, function( prefix, suffix ) { - jQuery.cssHooks[ prefix + suffix ] = { - expand: function( value ) { - var i = 0, - expanded = {}, - - // Assumes a single number if not a string - parts = typeof value === "string" ? value.split( " " ) : [ value ]; - - for ( ; i < 4; i++ ) { - expanded[ prefix + cssExpand[ i ] + suffix ] = - parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; - } - - return expanded; - } - }; - - if ( prefix !== "margin" ) { - jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; - } -} ); - -jQuery.fn.extend( { - css: function( name, value ) { - return access( this, function( elem, name, value ) { - var styles, len, - map = {}, - i = 0; - - if ( Array.isArray( name ) ) { - styles = getStyles( elem ); - len = name.length; - - for ( ; i < len; i++ ) { - map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); - } - - return map; - } - - return value !== undefined ? - jQuery.style( elem, name, value ) : - jQuery.css( elem, name ); - }, name, value, arguments.length > 1 ); - } -} ); - - -function Tween( elem, options, prop, end, easing ) { - return new Tween.prototype.init( elem, options, prop, end, easing ); -} -jQuery.Tween = Tween; - -Tween.prototype = { - constructor: Tween, - init: function( elem, options, prop, end, easing, unit ) { - this.elem = elem; - this.prop = prop; - this.easing = easing || jQuery.easing._default; - this.options = options; - this.start = this.now = this.cur(); - this.end = end; - this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); - }, - cur: function() { - var hooks = Tween.propHooks[ this.prop ]; - - return hooks && hooks.get ? - hooks.get( this ) : - Tween.propHooks._default.get( this ); - }, - run: function( percent ) { - var eased, - hooks = Tween.propHooks[ this.prop ]; - - if ( this.options.duration ) { - this.pos = eased = jQuery.easing[ this.easing ]( - percent, this.options.duration * percent, 0, 1, this.options.duration - ); - } else { - this.pos = eased = percent; - } - this.now = ( this.end - this.start ) * eased + this.start; - - if ( this.options.step ) { - this.options.step.call( this.elem, this.now, this ); - } - - if ( hooks && hooks.set ) { - hooks.set( this ); - } else { - Tween.propHooks._default.set( this ); - } - return this; - } -}; - -Tween.prototype.init.prototype = Tween.prototype; - -Tween.propHooks = { - _default: { - get: function( tween ) { - var result; - - // Use a property on the element directly when it is not a DOM element, - // or when there is no matching style property that exists. - if ( tween.elem.nodeType !== 1 || - tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) { - return tween.elem[ tween.prop ]; - } - - // Passing an empty string as a 3rd parameter to .css will automatically - // attempt a parseFloat and fallback to a string if the parse fails. - // Simple values such as "10px" are parsed to Float; - // complex values such as "rotate(1rad)" are returned as-is. - result = jQuery.css( tween.elem, tween.prop, "" ); - - // Empty strings, null, undefined and "auto" are converted to 0. - return !result || result === "auto" ? 0 : result; - }, - set: function( tween ) { - - // Use step hook for back compat. - // Use cssHook if its there. - // Use .style if available and use plain properties where available. - if ( jQuery.fx.step[ tween.prop ] ) { - jQuery.fx.step[ tween.prop ]( tween ); - } else if ( tween.elem.nodeType === 1 && ( - jQuery.cssHooks[ tween.prop ] || - tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { - jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); - } else { - tween.elem[ tween.prop ] = tween.now; - } - } - } -}; - -// Support: IE <=9 only -// Panic based approach to setting things on disconnected nodes -Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { - set: function( tween ) { - if ( tween.elem.nodeType && tween.elem.parentNode ) { - tween.elem[ tween.prop ] = tween.now; - } - } -}; - -jQuery.easing = { - linear: function( p ) { - return p; - }, - swing: function( p ) { - return 0.5 - Math.cos( p * Math.PI ) / 2; - }, - _default: "swing" -}; - -jQuery.fx = Tween.prototype.init; - -// Back compat <1.8 extension point -jQuery.fx.step = {}; - - - - -var - fxNow, inProgress, - rfxtypes = /^(?:toggle|show|hide)$/, - rrun = /queueHooks$/; - -function schedule() { - if ( inProgress ) { - if ( document.hidden === false && window.requestAnimationFrame ) { - window.requestAnimationFrame( schedule ); - } else { - window.setTimeout( schedule, jQuery.fx.interval ); - } - - jQuery.fx.tick(); - } -} - -// Animations created synchronously will run synchronously -function createFxNow() { - window.setTimeout( function() { - fxNow = undefined; - } ); - return ( fxNow = Date.now() ); -} - -// Generate parameters to create a standard animation -function genFx( type, includeWidth ) { - var which, - i = 0, - attrs = { height: type }; - - // If we include width, step value is 1 to do all cssExpand values, - // otherwise step value is 2 to skip over Left and Right - includeWidth = includeWidth ? 1 : 0; - for ( ; i < 4; i += 2 - includeWidth ) { - which = cssExpand[ i ]; - attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; - } - - if ( includeWidth ) { - attrs.opacity = attrs.width = type; - } - - return attrs; -} - -function createTween( value, prop, animation ) { - var tween, - collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ), - index = 0, - length = collection.length; - for ( ; index < length; index++ ) { - if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) { - - // We're done with this property - return tween; - } - } -} - -function defaultPrefilter( elem, props, opts ) { - var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display, - isBox = "width" in props || "height" in props, - anim = this, - orig = {}, - style = elem.style, - hidden = elem.nodeType && isHiddenWithinTree( elem ), - dataShow = dataPriv.get( elem, "fxshow" ); - - // Queue-skipping animations hijack the fx hooks - if ( !opts.queue ) { - hooks = jQuery._queueHooks( elem, "fx" ); - if ( hooks.unqueued == null ) { - hooks.unqueued = 0; - oldfire = hooks.empty.fire; - hooks.empty.fire = function() { - if ( !hooks.unqueued ) { - oldfire(); - } - }; - } - hooks.unqueued++; - - anim.always( function() { - - // Ensure the complete handler is called before this completes - anim.always( function() { - hooks.unqueued--; - if ( !jQuery.queue( elem, "fx" ).length ) { - hooks.empty.fire(); - } - } ); - } ); - } - - // Detect show/hide animations - for ( prop in props ) { - value = props[ prop ]; - if ( rfxtypes.test( value ) ) { - delete props[ prop ]; - toggle = toggle || value === "toggle"; - if ( value === ( hidden ? "hide" : "show" ) ) { - - // Pretend to be hidden if this is a "show" and - // there is still data from a stopped show/hide - if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { - hidden = true; - - // Ignore all other no-op show/hide data - } else { - continue; - } - } - orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); - } - } - - // Bail out if this is a no-op like .hide().hide() - propTween = !jQuery.isEmptyObject( props ); - if ( !propTween && jQuery.isEmptyObject( orig ) ) { - return; - } - - // Restrict "overflow" and "display" styles during box animations - if ( isBox && elem.nodeType === 1 ) { - - // Support: IE <=9 - 11, Edge 12 - 15 - // Record all 3 overflow attributes because IE does not infer the shorthand - // from identically-valued overflowX and overflowY and Edge just mirrors - // the overflowX value there. - opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; - - // Identify a display type, preferring old show/hide data over the CSS cascade - restoreDisplay = dataShow && dataShow.display; - if ( restoreDisplay == null ) { - restoreDisplay = dataPriv.get( elem, "display" ); - } - display = jQuery.css( elem, "display" ); - if ( display === "none" ) { - if ( restoreDisplay ) { - display = restoreDisplay; - } else { - - // Get nonempty value(s) by temporarily forcing visibility - showHide( [ elem ], true ); - restoreDisplay = elem.style.display || restoreDisplay; - display = jQuery.css( elem, "display" ); - showHide( [ elem ] ); - } - } - - // Animate inline elements as inline-block - if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) { - if ( jQuery.css( elem, "float" ) === "none" ) { - - // Restore the original display value at the end of pure show/hide animations - if ( !propTween ) { - anim.done( function() { - style.display = restoreDisplay; - } ); - if ( restoreDisplay == null ) { - display = style.display; - restoreDisplay = display === "none" ? "" : display; - } - } - style.display = "inline-block"; - } - } - } - - if ( opts.overflow ) { - style.overflow = "hidden"; - anim.always( function() { - style.overflow = opts.overflow[ 0 ]; - style.overflowX = opts.overflow[ 1 ]; - style.overflowY = opts.overflow[ 2 ]; - } ); - } - - // Implement show/hide animations - propTween = false; - for ( prop in orig ) { - - // General show/hide setup for this element animation - if ( !propTween ) { - if ( dataShow ) { - if ( "hidden" in dataShow ) { - hidden = dataShow.hidden; - } - } else { - dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } ); - } - - // Store hidden/visible for toggle so `.stop().toggle()` "reverses" - if ( toggle ) { - dataShow.hidden = !hidden; - } - - // Show elements before animating them - if ( hidden ) { - showHide( [ elem ], true ); - } - - /* eslint-disable no-loop-func */ - - anim.done( function() { - - /* eslint-enable no-loop-func */ - - // The final step of a "hide" animation is actually hiding the element - if ( !hidden ) { - showHide( [ elem ] ); - } - dataPriv.remove( elem, "fxshow" ); - for ( prop in orig ) { - jQuery.style( elem, prop, orig[ prop ] ); - } - } ); - } - - // Per-property setup - propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); - if ( !( prop in dataShow ) ) { - dataShow[ prop ] = propTween.start; - if ( hidden ) { - propTween.end = propTween.start; - propTween.start = 0; - } - } - } -} - -function propFilter( props, specialEasing ) { - var index, name, easing, value, hooks; - - // camelCase, specialEasing and expand cssHook pass - for ( index in props ) { - name = camelCase( index ); - easing = specialEasing[ name ]; - value = props[ index ]; - if ( Array.isArray( value ) ) { - easing = value[ 1 ]; - value = props[ index ] = value[ 0 ]; - } - - if ( index !== name ) { - props[ name ] = value; - delete props[ index ]; - } - - hooks = jQuery.cssHooks[ name ]; - if ( hooks && "expand" in hooks ) { - value = hooks.expand( value ); - delete props[ name ]; - - // Not quite $.extend, this won't overwrite existing keys. - // Reusing 'index' because we have the correct "name" - for ( index in value ) { - if ( !( index in props ) ) { - props[ index ] = value[ index ]; - specialEasing[ index ] = easing; - } - } - } else { - specialEasing[ name ] = easing; - } - } -} - -function Animation( elem, properties, options ) { - var result, - stopped, - index = 0, - length = Animation.prefilters.length, - deferred = jQuery.Deferred().always( function() { - - // Don't match elem in the :animated selector - delete tick.elem; - } ), - tick = function() { - if ( stopped ) { - return false; - } - var currentTime = fxNow || createFxNow(), - remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), - - // Support: Android 2.3 only - // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497) - temp = remaining / animation.duration || 0, - percent = 1 - temp, - index = 0, - length = animation.tweens.length; - - for ( ; index < length; index++ ) { - animation.tweens[ index ].run( percent ); - } - - deferred.notifyWith( elem, [ animation, percent, remaining ] ); - - // If there's more to do, yield - if ( percent < 1 && length ) { - return remaining; - } - - // If this was an empty animation, synthesize a final progress notification - if ( !length ) { - deferred.notifyWith( elem, [ animation, 1, 0 ] ); - } - - // Resolve the animation and report its conclusion - deferred.resolveWith( elem, [ animation ] ); - return false; - }, - animation = deferred.promise( { - elem: elem, - props: jQuery.extend( {}, properties ), - opts: jQuery.extend( true, { - specialEasing: {}, - easing: jQuery.easing._default - }, options ), - originalProperties: properties, - originalOptions: options, - startTime: fxNow || createFxNow(), - duration: options.duration, - tweens: [], - createTween: function( prop, end ) { - var tween = jQuery.Tween( elem, animation.opts, prop, end, - animation.opts.specialEasing[ prop ] || animation.opts.easing ); - animation.tweens.push( tween ); - return tween; - }, - stop: function( gotoEnd ) { - var index = 0, - - // If we are going to the end, we want to run all the tweens - // otherwise we skip this part - length = gotoEnd ? animation.tweens.length : 0; - if ( stopped ) { - return this; - } - stopped = true; - for ( ; index < length; index++ ) { - animation.tweens[ index ].run( 1 ); - } - - // Resolve when we played the last frame; otherwise, reject - if ( gotoEnd ) { - deferred.notifyWith( elem, [ animation, 1, 0 ] ); - deferred.resolveWith( elem, [ animation, gotoEnd ] ); - } else { - deferred.rejectWith( elem, [ animation, gotoEnd ] ); - } - return this; - } - } ), - props = animation.props; - - propFilter( props, animation.opts.specialEasing ); - - for ( ; index < length; index++ ) { - result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); - if ( result ) { - if ( isFunction( result.stop ) ) { - jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = - result.stop.bind( result ); - } - return result; - } - } - - jQuery.map( props, createTween, animation ); - - if ( isFunction( animation.opts.start ) ) { - animation.opts.start.call( elem, animation ); - } - - // Attach callbacks from options - animation - .progress( animation.opts.progress ) - .done( animation.opts.done, animation.opts.complete ) - .fail( animation.opts.fail ) - .always( animation.opts.always ); - - jQuery.fx.timer( - jQuery.extend( tick, { - elem: elem, - anim: animation, - queue: animation.opts.queue - } ) - ); - - return animation; -} - -jQuery.Animation = jQuery.extend( Animation, { - - tweeners: { - "*": [ function( prop, value ) { - var tween = this.createTween( prop, value ); - adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween ); - return tween; - } ] - }, - - tweener: function( props, callback ) { - if ( isFunction( props ) ) { - callback = props; - props = [ "*" ]; - } else { - props = props.match( rnothtmlwhite ); - } - - var prop, - index = 0, - length = props.length; - - for ( ; index < length; index++ ) { - prop = props[ index ]; - Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || []; - Animation.tweeners[ prop ].unshift( callback ); - } - }, - - prefilters: [ defaultPrefilter ], - - prefilter: function( callback, prepend ) { - if ( prepend ) { - Animation.prefilters.unshift( callback ); - } else { - Animation.prefilters.push( callback ); - } - } -} ); - -jQuery.speed = function( speed, easing, fn ) { - var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { - complete: fn || !fn && easing || - isFunction( speed ) && speed, - duration: speed, - easing: fn && easing || easing && !isFunction( easing ) && easing - }; - - // Go to the end state if fx are off - if ( jQuery.fx.off ) { - opt.duration = 0; - - } else { - if ( typeof opt.duration !== "number" ) { - if ( opt.duration in jQuery.fx.speeds ) { - opt.duration = jQuery.fx.speeds[ opt.duration ]; - - } else { - opt.duration = jQuery.fx.speeds._default; - } - } - } - - // Normalize opt.queue - true/undefined/null -> "fx" - if ( opt.queue == null || opt.queue === true ) { - opt.queue = "fx"; - } - - // Queueing - opt.old = opt.complete; - - opt.complete = function() { - if ( isFunction( opt.old ) ) { - opt.old.call( this ); - } - - if ( opt.queue ) { - jQuery.dequeue( this, opt.queue ); - } - }; - - return opt; -}; - -jQuery.fn.extend( { - fadeTo: function( speed, to, easing, callback ) { - - // Show any hidden elements after setting opacity to 0 - return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show() - - // Animate to the value specified - .end().animate( { opacity: to }, speed, easing, callback ); - }, - animate: function( prop, speed, easing, callback ) { - var empty = jQuery.isEmptyObject( prop ), - optall = jQuery.speed( speed, easing, callback ), - doAnimation = function() { - - // Operate on a copy of prop so per-property easing won't be lost - var anim = Animation( this, jQuery.extend( {}, prop ), optall ); - - // Empty animations, or finishing resolves immediately - if ( empty || dataPriv.get( this, "finish" ) ) { - anim.stop( true ); - } - }; - doAnimation.finish = doAnimation; - - return empty || optall.queue === false ? - this.each( doAnimation ) : - this.queue( optall.queue, doAnimation ); - }, - stop: function( type, clearQueue, gotoEnd ) { - var stopQueue = function( hooks ) { - var stop = hooks.stop; - delete hooks.stop; - stop( gotoEnd ); - }; - - if ( typeof type !== "string" ) { - gotoEnd = clearQueue; - clearQueue = type; - type = undefined; - } - if ( clearQueue ) { - this.queue( type || "fx", [] ); - } - - return this.each( function() { - var dequeue = true, - index = type != null && type + "queueHooks", - timers = jQuery.timers, - data = dataPriv.get( this ); - - if ( index ) { - if ( data[ index ] && data[ index ].stop ) { - stopQueue( data[ index ] ); - } - } else { - for ( index in data ) { - if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { - stopQueue( data[ index ] ); - } - } - } - - for ( index = timers.length; index--; ) { - if ( timers[ index ].elem === this && - ( type == null || timers[ index ].queue === type ) ) { - - timers[ index ].anim.stop( gotoEnd ); - dequeue = false; - timers.splice( index, 1 ); - } - } - - // Start the next in the queue if the last step wasn't forced. - // Timers currently will call their complete callbacks, which - // will dequeue but only if they were gotoEnd. - if ( dequeue || !gotoEnd ) { - jQuery.dequeue( this, type ); - } - } ); - }, - finish: function( type ) { - if ( type !== false ) { - type = type || "fx"; - } - return this.each( function() { - var index, - data = dataPriv.get( this ), - queue = data[ type + "queue" ], - hooks = data[ type + "queueHooks" ], - timers = jQuery.timers, - length = queue ? queue.length : 0; - - // Enable finishing flag on private data - data.finish = true; - - // Empty the queue first - jQuery.queue( this, type, [] ); - - if ( hooks && hooks.stop ) { - hooks.stop.call( this, true ); - } - - // Look for any active animations, and finish them - for ( index = timers.length; index--; ) { - if ( timers[ index ].elem === this && timers[ index ].queue === type ) { - timers[ index ].anim.stop( true ); - timers.splice( index, 1 ); - } - } - - // Look for any animations in the old queue and finish them - for ( index = 0; index < length; index++ ) { - if ( queue[ index ] && queue[ index ].finish ) { - queue[ index ].finish.call( this ); - } - } - - // Turn off finishing flag - delete data.finish; - } ); - } -} ); - -jQuery.each( [ "toggle", "show", "hide" ], function( _i, name ) { - var cssFn = jQuery.fn[ name ]; - jQuery.fn[ name ] = function( speed, easing, callback ) { - return speed == null || typeof speed === "boolean" ? - cssFn.apply( this, arguments ) : - this.animate( genFx( name, true ), speed, easing, callback ); - }; -} ); - -// Generate shortcuts for custom animations -jQuery.each( { - slideDown: genFx( "show" ), - slideUp: genFx( "hide" ), - slideToggle: genFx( "toggle" ), - fadeIn: { opacity: "show" }, - fadeOut: { opacity: "hide" }, - fadeToggle: { opacity: "toggle" } -}, function( name, props ) { - jQuery.fn[ name ] = function( speed, easing, callback ) { - return this.animate( props, speed, easing, callback ); - }; -} ); - -jQuery.timers = []; -jQuery.fx.tick = function() { - var timer, - i = 0, - timers = jQuery.timers; - - fxNow = Date.now(); - - for ( ; i < timers.length; i++ ) { - timer = timers[ i ]; - - // Run the timer and safely remove it when done (allowing for external removal) - if ( !timer() && timers[ i ] === timer ) { - timers.splice( i--, 1 ); - } - } - - if ( !timers.length ) { - jQuery.fx.stop(); - } - fxNow = undefined; -}; - -jQuery.fx.timer = function( timer ) { - jQuery.timers.push( timer ); - jQuery.fx.start(); -}; - -jQuery.fx.interval = 13; -jQuery.fx.start = function() { - if ( inProgress ) { - return; - } - - inProgress = true; - schedule(); -}; - -jQuery.fx.stop = function() { - inProgress = null; -}; - -jQuery.fx.speeds = { - slow: 600, - fast: 200, - - // Default speed - _default: 400 -}; - - -// Based off of the plugin by Clint Helfers, with permission. -// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/ -jQuery.fn.delay = function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; - - return this.queue( type, function( next, hooks ) { - var timeout = window.setTimeout( next, time ); - hooks.stop = function() { - window.clearTimeout( timeout ); - }; - } ); -}; - - -( function() { - var input = document.createElement( "input" ), - select = document.createElement( "select" ), - opt = select.appendChild( document.createElement( "option" ) ); - - input.type = "checkbox"; - - // Support: Android <=4.3 only - // Default value for a checkbox should be "on" - support.checkOn = input.value !== ""; - - // Support: IE <=11 only - // Must access selectedIndex to make default options select - support.optSelected = opt.selected; - - // Support: IE <=11 only - // An input loses its value after becoming a radio - input = document.createElement( "input" ); - input.value = "t"; - input.type = "radio"; - support.radioValue = input.value === "t"; -} )(); - - -var boolHook, - attrHandle = jQuery.expr.attrHandle; - -jQuery.fn.extend( { - attr: function( name, value ) { - return access( this, jQuery.attr, name, value, arguments.length > 1 ); - }, - - removeAttr: function( name ) { - return this.each( function() { - jQuery.removeAttr( this, name ); - } ); - } -} ); - -jQuery.extend( { - attr: function( elem, name, value ) { - var ret, hooks, - nType = elem.nodeType; - - // Don't get/set attributes on text, comment and attribute nodes - if ( nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - // Fallback to prop when attributes are not supported - if ( typeof elem.getAttribute === "undefined" ) { - return jQuery.prop( elem, name, value ); - } - - // Attribute hooks are determined by the lowercase version - // Grab necessary hook if one is defined - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - hooks = jQuery.attrHooks[ name.toLowerCase() ] || - ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); - } - - if ( value !== undefined ) { - if ( value === null ) { - jQuery.removeAttr( elem, name ); - return; - } - - if ( hooks && "set" in hooks && - ( ret = hooks.set( elem, value, name ) ) !== undefined ) { - return ret; - } - - elem.setAttribute( name, value + "" ); - return value; - } - - if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { - return ret; - } - - ret = jQuery.find.attr( elem, name ); - - // Non-existent attributes return null, we normalize to undefined - return ret == null ? undefined : ret; - }, - - attrHooks: { - type: { - set: function( elem, value ) { - if ( !support.radioValue && value === "radio" && - nodeName( elem, "input" ) ) { - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; - } - return value; - } - } - } - }, - - removeAttr: function( elem, value ) { - var name, - i = 0, - - // Attribute names can contain non-HTML whitespace characters - // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 - attrNames = value && value.match( rnothtmlwhite ); - - if ( attrNames && elem.nodeType === 1 ) { - while ( ( name = attrNames[ i++ ] ) ) { - elem.removeAttribute( name ); - } - } - } -} ); - -// Hooks for boolean attributes -boolHook = { - set: function( elem, value, name ) { - if ( value === false ) { - - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); - } else { - elem.setAttribute( name, name ); - } - return name; - } -}; - -jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) { - var getter = attrHandle[ name ] || jQuery.find.attr; - - attrHandle[ name ] = function( elem, name, isXML ) { - var ret, handle, - lowercaseName = name.toLowerCase(); - - if ( !isXML ) { - - // Avoid an infinite loop by temporarily removing this function from the getter - handle = attrHandle[ lowercaseName ]; - attrHandle[ lowercaseName ] = ret; - ret = getter( elem, name, isXML ) != null ? - lowercaseName : - null; - attrHandle[ lowercaseName ] = handle; - } - return ret; - }; -} ); - - - - -var rfocusable = /^(?:input|select|textarea|button)$/i, - rclickable = /^(?:a|area)$/i; - -jQuery.fn.extend( { - prop: function( name, value ) { - return access( this, jQuery.prop, name, value, arguments.length > 1 ); - }, - - removeProp: function( name ) { - return this.each( function() { - delete this[ jQuery.propFix[ name ] || name ]; - } ); - } -} ); - -jQuery.extend( { - prop: function( elem, name, value ) { - var ret, hooks, - nType = elem.nodeType; - - // Don't get/set properties on text, comment and attribute nodes - if ( nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; - } - - if ( value !== undefined ) { - if ( hooks && "set" in hooks && - ( ret = hooks.set( elem, value, name ) ) !== undefined ) { - return ret; - } - - return ( elem[ name ] = value ); - } - - if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { - return ret; - } - - return elem[ name ]; - }, - - propHooks: { - tabIndex: { - get: function( elem ) { - - // Support: IE <=9 - 11 only - // elem.tabIndex doesn't always return the - // correct value when it hasn't been explicitly set - // https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - // Use proper attribute retrieval(#12072) - var tabindex = jQuery.find.attr( elem, "tabindex" ); - - if ( tabindex ) { - return parseInt( tabindex, 10 ); - } - - if ( - rfocusable.test( elem.nodeName ) || - rclickable.test( elem.nodeName ) && - elem.href - ) { - return 0; - } - - return -1; - } - } - }, - - propFix: { - "for": "htmlFor", - "class": "className" - } -} ); - -// Support: IE <=11 only -// Accessing the selectedIndex property -// forces the browser to respect setting selected -// on the option -// The getter ensures a default option is selected -// when in an optgroup -// eslint rule "no-unused-expressions" is disabled for this code -// since it considers such accessions noop -if ( !support.optSelected ) { - jQuery.propHooks.selected = { - get: function( elem ) { - - /* eslint no-unused-expressions: "off" */ - - var parent = elem.parentNode; - if ( parent && parent.parentNode ) { - parent.parentNode.selectedIndex; - } - return null; - }, - set: function( elem ) { - - /* eslint no-unused-expressions: "off" */ - - var parent = elem.parentNode; - if ( parent ) { - parent.selectedIndex; - - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - } - }; -} - -jQuery.each( [ - "tabIndex", - "readOnly", - "maxLength", - "cellSpacing", - "cellPadding", - "rowSpan", - "colSpan", - "useMap", - "frameBorder", - "contentEditable" -], function() { - jQuery.propFix[ this.toLowerCase() ] = this; -} ); - - - - - // Strip and collapse whitespace according to HTML spec - // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace - function stripAndCollapse( value ) { - var tokens = value.match( rnothtmlwhite ) || []; - return tokens.join( " " ); - } - - -function getClass( elem ) { - return elem.getAttribute && elem.getAttribute( "class" ) || ""; -} - -function classesToArray( value ) { - if ( Array.isArray( value ) ) { - return value; - } - if ( typeof value === "string" ) { - return value.match( rnothtmlwhite ) || []; - } - return []; -} - -jQuery.fn.extend( { - addClass: function( value ) { - var classes, elem, cur, curValue, clazz, j, finalValue, - i = 0; - - if ( isFunction( value ) ) { - return this.each( function( j ) { - jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); - } ); - } - - classes = classesToArray( value ); - - if ( classes.length ) { - while ( ( elem = this[ i++ ] ) ) { - curValue = getClass( elem ); - cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); - - if ( cur ) { - j = 0; - while ( ( clazz = classes[ j++ ] ) ) { - if ( cur.indexOf( " " + clazz + " " ) < 0 ) { - cur += clazz + " "; - } - } - - // Only assign if different to avoid unneeded rendering. - finalValue = stripAndCollapse( cur ); - if ( curValue !== finalValue ) { - elem.setAttribute( "class", finalValue ); - } - } - } - } - - return this; - }, - - removeClass: function( value ) { - var classes, elem, cur, curValue, clazz, j, finalValue, - i = 0; - - if ( isFunction( value ) ) { - return this.each( function( j ) { - jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); - } ); - } - - if ( !arguments.length ) { - return this.attr( "class", "" ); - } - - classes = classesToArray( value ); - - if ( classes.length ) { - while ( ( elem = this[ i++ ] ) ) { - curValue = getClass( elem ); - - // This expression is here for better compressibility (see addClass) - cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); - - if ( cur ) { - j = 0; - while ( ( clazz = classes[ j++ ] ) ) { - - // Remove *all* instances - while ( cur.indexOf( " " + clazz + " " ) > -1 ) { - cur = cur.replace( " " + clazz + " ", " " ); - } - } - - // Only assign if different to avoid unneeded rendering. - finalValue = stripAndCollapse( cur ); - if ( curValue !== finalValue ) { - elem.setAttribute( "class", finalValue ); - } - } - } - } - - return this; - }, - - toggleClass: function( value, stateVal ) { - var type = typeof value, - isValidValue = type === "string" || Array.isArray( value ); - - if ( typeof stateVal === "boolean" && isValidValue ) { - return stateVal ? this.addClass( value ) : this.removeClass( value ); - } - - if ( isFunction( value ) ) { - return this.each( function( i ) { - jQuery( this ).toggleClass( - value.call( this, i, getClass( this ), stateVal ), - stateVal - ); - } ); - } - - return this.each( function() { - var className, i, self, classNames; - - if ( isValidValue ) { - - // Toggle individual class names - i = 0; - self = jQuery( this ); - classNames = classesToArray( value ); - - while ( ( className = classNames[ i++ ] ) ) { - - // Check each className given, space separated list - if ( self.hasClass( className ) ) { - self.removeClass( className ); - } else { - self.addClass( className ); - } - } - - // Toggle whole class name - } else if ( value === undefined || type === "boolean" ) { - className = getClass( this ); - if ( className ) { - - // Store className if set - dataPriv.set( this, "__className__", className ); - } - - // If the element has a class name or if we're passed `false`, - // then remove the whole classname (if there was one, the above saved it). - // Otherwise bring back whatever was previously saved (if anything), - // falling back to the empty string if nothing was stored. - if ( this.setAttribute ) { - this.setAttribute( "class", - className || value === false ? - "" : - dataPriv.get( this, "__className__" ) || "" - ); - } - } - } ); - }, - - hasClass: function( selector ) { - var className, elem, - i = 0; - - className = " " + selector + " "; - while ( ( elem = this[ i++ ] ) ) { - if ( elem.nodeType === 1 && - ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { - return true; - } - } - - return false; - } -} ); - - - - -var rreturn = /\r/g; - -jQuery.fn.extend( { - val: function( value ) { - var hooks, ret, valueIsFunction, - elem = this[ 0 ]; - - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.type ] || - jQuery.valHooks[ elem.nodeName.toLowerCase() ]; - - if ( hooks && - "get" in hooks && - ( ret = hooks.get( elem, "value" ) ) !== undefined - ) { - return ret; - } - - ret = elem.value; - - // Handle most common string cases - if ( typeof ret === "string" ) { - return ret.replace( rreturn, "" ); - } - - // Handle cases where value is null/undef or number - return ret == null ? "" : ret; - } - - return; - } - - valueIsFunction = isFunction( value ); - - return this.each( function( i ) { - var val; - - if ( this.nodeType !== 1 ) { - return; - } - - if ( valueIsFunction ) { - val = value.call( this, i, jQuery( this ).val() ); - } else { - val = value; - } - - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - - } else if ( typeof val === "number" ) { - val += ""; - - } else if ( Array.isArray( val ) ) { - val = jQuery.map( val, function( value ) { - return value == null ? "" : value + ""; - } ); - } - - hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; - - // If set returns undefined, fall back to normal setting - if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; - } - } ); - } -} ); - -jQuery.extend( { - valHooks: { - option: { - get: function( elem ) { - - var val = jQuery.find.attr( elem, "value" ); - return val != null ? - val : - - // Support: IE <=10 - 11 only - // option.text throws exceptions (#14686, #14858) - // Strip and collapse whitespace - // https://html.spec.whatwg.org/#strip-and-collapse-whitespace - stripAndCollapse( jQuery.text( elem ) ); - } - }, - select: { - get: function( elem ) { - var value, option, i, - options = elem.options, - index = elem.selectedIndex, - one = elem.type === "select-one", - values = one ? null : [], - max = one ? index + 1 : options.length; - - if ( index < 0 ) { - i = max; - - } else { - i = one ? index : 0; - } - - // Loop through all the selected options - for ( ; i < max; i++ ) { - option = options[ i ]; - - // Support: IE <=9 only - // IE8-9 doesn't update selected after form reset (#2551) - if ( ( option.selected || i === index ) && - - // Don't return options that are disabled or in a disabled optgroup - !option.disabled && - ( !option.parentNode.disabled || - !nodeName( option.parentNode, "optgroup" ) ) ) { - - // Get the specific value for the option - value = jQuery( option ).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - }, - - set: function( elem, value ) { - var optionSet, option, - options = elem.options, - values = jQuery.makeArray( value ), - i = options.length; - - while ( i-- ) { - option = options[ i ]; - - /* eslint-disable no-cond-assign */ - - if ( option.selected = - jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 - ) { - optionSet = true; - } - - /* eslint-enable no-cond-assign */ - } - - // Force browsers to behave consistently when non-matching value is set - if ( !optionSet ) { - elem.selectedIndex = -1; - } - return values; - } - } - } -} ); - -// Radios and checkboxes getter/setter -jQuery.each( [ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - set: function( elem, value ) { - if ( Array.isArray( value ) ) { - return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); - } - } - }; - if ( !support.checkOn ) { - jQuery.valHooks[ this ].get = function( elem ) { - return elem.getAttribute( "value" ) === null ? "on" : elem.value; - }; - } -} ); - - - - -// Return jQuery for attributes-only inclusion - - -support.focusin = "onfocusin" in window; - - -var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - stopPropagationCallback = function( e ) { - e.stopPropagation(); - }; - -jQuery.extend( jQuery.event, { - - trigger: function( event, data, elem, onlyHandlers ) { - - var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, - eventPath = [ elem || document ], - type = hasOwn.call( event, "type" ) ? event.type : event, - namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; - - cur = lastElement = tmp = elem = elem || document; - - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } - - if ( type.indexOf( "." ) > -1 ) { - - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split( "." ); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf( ":" ) < 0 && "on" + type; - - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[ jQuery.expando ] ? - event : - new jQuery.Event( type, typeof event === "object" && event ); - - // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) - event.isTrigger = onlyHandlers ? 2 : 3; - event.namespace = namespaces.join( "." ); - event.rnamespace = event.namespace ? - new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : - null; - - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [ event ] : - jQuery.makeArray( data, [ event ] ); - - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } - - // Determine event propagation path in advance, per W3C events spec (#9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { - - bubbleType = special.delegateType || type; - if ( !rfocusMorph.test( bubbleType + type ) ) { - cur = cur.parentNode; - } - for ( ; cur; cur = cur.parentNode ) { - eventPath.push( cur ); - tmp = cur; - } - - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( tmp === ( elem.ownerDocument || document ) ) { - eventPath.push( tmp.defaultView || tmp.parentWindow || window ); - } - } - - // Fire handlers on the event path - i = 0; - while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { - lastElement = cur; - event.type = i > 1 ? - bubbleType : - special.bindType || type; - - // jQuery handler - handle = ( - dataPriv.get( cur, "events" ) || Object.create( null ) - )[ event.type ] && - dataPriv.get( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } - - // Native handler - handle = ontype && cur[ ontype ]; - if ( handle && handle.apply && acceptData( cur ) ) { - event.result = handle.apply( cur, data ); - if ( event.result === false ) { - event.preventDefault(); - } - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { - - if ( ( !special._default || - special._default.apply( eventPath.pop(), data ) === false ) && - acceptData( elem ) ) { - - // Call a native DOM method on the target with the same name as the event. - // Don't do default actions on window, that's where global variables be (#6170) - if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { - - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ ontype ]; - - if ( tmp ) { - elem[ ontype ] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - - if ( event.isPropagationStopped() ) { - lastElement.addEventListener( type, stopPropagationCallback ); - } - - elem[ type ](); - - if ( event.isPropagationStopped() ) { - lastElement.removeEventListener( type, stopPropagationCallback ); - } - - jQuery.event.triggered = undefined; - - if ( tmp ) { - elem[ ontype ] = tmp; - } - } - } - } - - return event.result; - }, - - // Piggyback on a donor event to simulate a different one - // Used only for `focus(in | out)` events - simulate: function( type, elem, event ) { - var e = jQuery.extend( - new jQuery.Event(), - event, - { - type: type, - isSimulated: true - } - ); - - jQuery.event.trigger( e, null, elem ); - } - -} ); - -jQuery.fn.extend( { - - trigger: function( type, data ) { - return this.each( function() { - jQuery.event.trigger( type, data, this ); - } ); - }, - triggerHandler: function( type, data ) { - var elem = this[ 0 ]; - if ( elem ) { - return jQuery.event.trigger( type, data, elem, true ); - } - } -} ); - - -// Support: Firefox <=44 -// Firefox doesn't have focus(in | out) events -// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 -// -// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1 -// focus(in | out) events fire after focus & blur events, -// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order -// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857 -if ( !support.focusin ) { - jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) { - - // Attach a single capturing handler on the document while someone wants focusin/focusout - var handler = function( event ) { - jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) ); - }; - - jQuery.event.special[ fix ] = { - setup: function() { - - // Handle: regular nodes (via `this.ownerDocument`), window - // (via `this.document`) & document (via `this`). - var doc = this.ownerDocument || this.document || this, - attaches = dataPriv.access( doc, fix ); - - if ( !attaches ) { - doc.addEventListener( orig, handler, true ); - } - dataPriv.access( doc, fix, ( attaches || 0 ) + 1 ); - }, - teardown: function() { - var doc = this.ownerDocument || this.document || this, - attaches = dataPriv.access( doc, fix ) - 1; - - if ( !attaches ) { - doc.removeEventListener( orig, handler, true ); - dataPriv.remove( doc, fix ); - - } else { - dataPriv.access( doc, fix, attaches ); - } - } - }; - } ); -} -var location = window.location; - -var nonce = { guid: Date.now() }; - -var rquery = ( /\?/ ); - - - -// Cross-browser xml parsing -jQuery.parseXML = function( data ) { - var xml; - if ( !data || typeof data !== "string" ) { - return null; - } - - // Support: IE 9 - 11 only - // IE throws on parseFromString with invalid input. - try { - xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); - } catch ( e ) { - xml = undefined; - } - - if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) { - jQuery.error( "Invalid XML: " + data ); - } - return xml; -}; - - -var - rbracket = /\[\]$/, - rCRLF = /\r?\n/g, - rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, - rsubmittable = /^(?:input|select|textarea|keygen)/i; - -function buildParams( prefix, obj, traditional, add ) { - var name; - - if ( Array.isArray( obj ) ) { - - // Serialize array item. - jQuery.each( obj, function( i, v ) { - if ( traditional || rbracket.test( prefix ) ) { - - // Treat each array item as a scalar. - add( prefix, v ); - - } else { - - // Item is non-scalar (array or object), encode its numeric index. - buildParams( - prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", - v, - traditional, - add - ); - } - } ); - - } else if ( !traditional && toType( obj ) === "object" ) { - - // Serialize object item. - for ( name in obj ) { - buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); - } - - } else { - - // Serialize scalar item. - add( prefix, obj ); - } -} - -// Serialize an array of form elements or a set of -// key/values into a query string -jQuery.param = function( a, traditional ) { - var prefix, - s = [], - add = function( key, valueOrFunction ) { - - // If value is a function, invoke it and use its return value - var value = isFunction( valueOrFunction ) ? - valueOrFunction() : - valueOrFunction; - - s[ s.length ] = encodeURIComponent( key ) + "=" + - encodeURIComponent( value == null ? "" : value ); - }; - - if ( a == null ) { - return ""; - } - - // If an array was passed in, assume that it is an array of form elements. - if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { - - // Serialize the form elements - jQuery.each( a, function() { - add( this.name, this.value ); - } ); - - } else { - - // If traditional, encode the "old" way (the way 1.3.2 or older - // did it), otherwise encode params recursively. - for ( prefix in a ) { - buildParams( prefix, a[ prefix ], traditional, add ); - } - } - - // Return the resulting serialization - return s.join( "&" ); -}; - -jQuery.fn.extend( { - serialize: function() { - return jQuery.param( this.serializeArray() ); - }, - serializeArray: function() { - return this.map( function() { - - // Can add propHook for "elements" to filter or add form elements - var elements = jQuery.prop( this, "elements" ); - return elements ? jQuery.makeArray( elements ) : this; - } ) - .filter( function() { - var type = this.type; - - // Use .is( ":disabled" ) so that fieldset[disabled] works - return this.name && !jQuery( this ).is( ":disabled" ) && - rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && - ( this.checked || !rcheckableType.test( type ) ); - } ) - .map( function( _i, elem ) { - var val = jQuery( this ).val(); - - if ( val == null ) { - return null; - } - - if ( Array.isArray( val ) ) { - return jQuery.map( val, function( val ) { - return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - } ); - } - - return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - } ).get(); - } -} ); - - -var - r20 = /%20/g, - rhash = /#.*$/, - rantiCache = /([?&])_=[^&]*/, - rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, - - // #7653, #8125, #8152: local protocol detection - rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, - rnoContent = /^(?:GET|HEAD)$/, - rprotocol = /^\/\//, - - /* Prefilters - * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) - * 2) These are called: - * - BEFORE asking for a transport - * - AFTER param serialization (s.data is a string if s.processData is true) - * 3) key is the dataType - * 4) the catchall symbol "*" can be used - * 5) execution will start with transport dataType and THEN continue down to "*" if needed - */ - prefilters = {}, - - /* Transports bindings - * 1) key is the dataType - * 2) the catchall symbol "*" can be used - * 3) selection will start with transport dataType and THEN go to "*" if needed - */ - transports = {}, - - // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression - allTypes = "*/".concat( "*" ), - - // Anchor tag for parsing the document origin - originAnchor = document.createElement( "a" ); - originAnchor.href = location.href; - -// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport -function addToPrefiltersOrTransports( structure ) { - - // dataTypeExpression is optional and defaults to "*" - return function( dataTypeExpression, func ) { - - if ( typeof dataTypeExpression !== "string" ) { - func = dataTypeExpression; - dataTypeExpression = "*"; - } - - var dataType, - i = 0, - dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || []; - - if ( isFunction( func ) ) { - - // For each dataType in the dataTypeExpression - while ( ( dataType = dataTypes[ i++ ] ) ) { - - // Prepend if requested - if ( dataType[ 0 ] === "+" ) { - dataType = dataType.slice( 1 ) || "*"; - ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func ); - - // Otherwise append - } else { - ( structure[ dataType ] = structure[ dataType ] || [] ).push( func ); - } - } - } - }; -} - -// Base inspection function for prefilters and transports -function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { - - var inspected = {}, - seekingTransport = ( structure === transports ); - - function inspect( dataType ) { - var selected; - inspected[ dataType ] = true; - jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { - var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); - if ( typeof dataTypeOrTransport === "string" && - !seekingTransport && !inspected[ dataTypeOrTransport ] ) { - - options.dataTypes.unshift( dataTypeOrTransport ); - inspect( dataTypeOrTransport ); - return false; - } else if ( seekingTransport ) { - return !( selected = dataTypeOrTransport ); - } - } ); - return selected; - } - - return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); -} - -// A special extend for ajax options -// that takes "flat" options (not to be deep extended) -// Fixes #9887 -function ajaxExtend( target, src ) { - var key, deep, - flatOptions = jQuery.ajaxSettings.flatOptions || {}; - - for ( key in src ) { - if ( src[ key ] !== undefined ) { - ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; - } - } - if ( deep ) { - jQuery.extend( true, target, deep ); - } - - return target; -} - -/* Handles responses to an ajax request: - * - finds the right dataType (mediates between content-type and expected dataType) - * - returns the corresponding response - */ -function ajaxHandleResponses( s, jqXHR, responses ) { - - var ct, type, finalDataType, firstDataType, - contents = s.contents, - dataTypes = s.dataTypes; - - // Remove auto dataType and get content-type in the process - while ( dataTypes[ 0 ] === "*" ) { - dataTypes.shift(); - if ( ct === undefined ) { - ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" ); - } - } - - // Check if we're dealing with a known content-type - if ( ct ) { - for ( type in contents ) { - if ( contents[ type ] && contents[ type ].test( ct ) ) { - dataTypes.unshift( type ); - break; - } - } - } - - // Check to see if we have a response for the expected dataType - if ( dataTypes[ 0 ] in responses ) { - finalDataType = dataTypes[ 0 ]; - } else { - - // Try convertible dataTypes - for ( type in responses ) { - if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) { - finalDataType = type; - break; - } - if ( !firstDataType ) { - firstDataType = type; - } - } - - // Or just use first one - finalDataType = finalDataType || firstDataType; - } - - // If we found a dataType - // We add the dataType to the list if needed - // and return the corresponding response - if ( finalDataType ) { - if ( finalDataType !== dataTypes[ 0 ] ) { - dataTypes.unshift( finalDataType ); - } - return responses[ finalDataType ]; - } -} - -/* Chain conversions given the request and the original response - * Also sets the responseXXX fields on the jqXHR instance - */ -function ajaxConvert( s, response, jqXHR, isSuccess ) { - var conv2, current, conv, tmp, prev, - converters = {}, - - // Work with a copy of dataTypes in case we need to modify it for conversion - dataTypes = s.dataTypes.slice(); - - // Create converters map with lowercased keys - if ( dataTypes[ 1 ] ) { - for ( conv in s.converters ) { - converters[ conv.toLowerCase() ] = s.converters[ conv ]; - } - } - - current = dataTypes.shift(); - - // Convert to each sequential dataType - while ( current ) { - - if ( s.responseFields[ current ] ) { - jqXHR[ s.responseFields[ current ] ] = response; - } - - // Apply the dataFilter if provided - if ( !prev && isSuccess && s.dataFilter ) { - response = s.dataFilter( response, s.dataType ); - } - - prev = current; - current = dataTypes.shift(); - - if ( current ) { - - // There's only work to do if current dataType is non-auto - if ( current === "*" ) { - - current = prev; - - // Convert response if prev dataType is non-auto and differs from current - } else if ( prev !== "*" && prev !== current ) { - - // Seek a direct converter - conv = converters[ prev + " " + current ] || converters[ "* " + current ]; - - // If none found, seek a pair - if ( !conv ) { - for ( conv2 in converters ) { - - // If conv2 outputs current - tmp = conv2.split( " " ); - if ( tmp[ 1 ] === current ) { - - // If prev can be converted to accepted input - conv = converters[ prev + " " + tmp[ 0 ] ] || - converters[ "* " + tmp[ 0 ] ]; - if ( conv ) { - - // Condense equivalence converters - if ( conv === true ) { - conv = converters[ conv2 ]; - - // Otherwise, insert the intermediate dataType - } else if ( converters[ conv2 ] !== true ) { - current = tmp[ 0 ]; - dataTypes.unshift( tmp[ 1 ] ); - } - break; - } - } - } - } - - // Apply converter (if not an equivalence) - if ( conv !== true ) { - - // Unless errors are allowed to bubble, catch and return them - if ( conv && s.throws ) { - response = conv( response ); - } else { - try { - response = conv( response ); - } catch ( e ) { - return { - state: "parsererror", - error: conv ? e : "No conversion from " + prev + " to " + current - }; - } - } - } - } - } - } - - return { state: "success", data: response }; -} - -jQuery.extend( { - - // Counter for holding the number of active queries - active: 0, - - // Last-Modified header cache for next request - lastModified: {}, - etag: {}, - - ajaxSettings: { - url: location.href, - type: "GET", - isLocal: rlocalProtocol.test( location.protocol ), - global: true, - processData: true, - async: true, - contentType: "application/x-www-form-urlencoded; charset=UTF-8", - - /* - timeout: 0, - data: null, - dataType: null, - username: null, - password: null, - cache: null, - throws: false, - traditional: false, - headers: {}, - */ - - accepts: { - "*": allTypes, - text: "text/plain", - html: "text/html", - xml: "application/xml, text/xml", - json: "application/json, text/javascript" - }, - - contents: { - xml: /\bxml\b/, - html: /\bhtml/, - json: /\bjson\b/ - }, - - responseFields: { - xml: "responseXML", - text: "responseText", - json: "responseJSON" - }, - - // Data converters - // Keys separate source (or catchall "*") and destination types with a single space - converters: { - - // Convert anything to text - "* text": String, - - // Text to html (true = no transformation) - "text html": true, - - // Evaluate text as a json expression - "text json": JSON.parse, - - // Parse text as xml - "text xml": jQuery.parseXML - }, - - // For options that shouldn't be deep extended: - // you can add your own custom options here if - // and when you create one that shouldn't be - // deep extended (see ajaxExtend) - flatOptions: { - url: true, - context: true - } - }, - - // Creates a full fledged settings object into target - // with both ajaxSettings and settings fields. - // If target is omitted, writes into ajaxSettings. - ajaxSetup: function( target, settings ) { - return settings ? - - // Building a settings object - ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : - - // Extending ajaxSettings - ajaxExtend( jQuery.ajaxSettings, target ); - }, - - ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), - ajaxTransport: addToPrefiltersOrTransports( transports ), - - // Main method - ajax: function( url, options ) { - - // If url is an object, simulate pre-1.5 signature - if ( typeof url === "object" ) { - options = url; - url = undefined; - } - - // Force options to be an object - options = options || {}; - - var transport, - - // URL without anti-cache param - cacheURL, - - // Response headers - responseHeadersString, - responseHeaders, - - // timeout handle - timeoutTimer, - - // Url cleanup var - urlAnchor, - - // Request state (becomes false upon send and true upon completion) - completed, - - // To know if global events are to be dispatched - fireGlobals, - - // Loop variable - i, - - // uncached part of the url - uncached, - - // Create the final options object - s = jQuery.ajaxSetup( {}, options ), - - // Callbacks context - callbackContext = s.context || s, - - // Context for global events is callbackContext if it is a DOM node or jQuery collection - globalEventContext = s.context && - ( callbackContext.nodeType || callbackContext.jquery ) ? - jQuery( callbackContext ) : - jQuery.event, - - // Deferreds - deferred = jQuery.Deferred(), - completeDeferred = jQuery.Callbacks( "once memory" ), - - // Status-dependent callbacks - statusCode = s.statusCode || {}, - - // Headers (they are sent all at once) - requestHeaders = {}, - requestHeadersNames = {}, - - // Default abort message - strAbort = "canceled", - - // Fake xhr - jqXHR = { - readyState: 0, - - // Builds headers hashtable if needed - getResponseHeader: function( key ) { - var match; - if ( completed ) { - if ( !responseHeaders ) { - responseHeaders = {}; - while ( ( match = rheaders.exec( responseHeadersString ) ) ) { - responseHeaders[ match[ 1 ].toLowerCase() + " " ] = - ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] ) - .concat( match[ 2 ] ); - } - } - match = responseHeaders[ key.toLowerCase() + " " ]; - } - return match == null ? null : match.join( ", " ); - }, - - // Raw string - getAllResponseHeaders: function() { - return completed ? responseHeadersString : null; - }, - - // Caches the header - setRequestHeader: function( name, value ) { - if ( completed == null ) { - name = requestHeadersNames[ name.toLowerCase() ] = - requestHeadersNames[ name.toLowerCase() ] || name; - requestHeaders[ name ] = value; - } - return this; - }, - - // Overrides response content-type header - overrideMimeType: function( type ) { - if ( completed == null ) { - s.mimeType = type; - } - return this; - }, - - // Status-dependent callbacks - statusCode: function( map ) { - var code; - if ( map ) { - if ( completed ) { - - // Execute the appropriate callbacks - jqXHR.always( map[ jqXHR.status ] ); - } else { - - // Lazy-add the new callbacks in a way that preserves old ones - for ( code in map ) { - statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; - } - } - } - return this; - }, - - // Cancel the request - abort: function( statusText ) { - var finalText = statusText || strAbort; - if ( transport ) { - transport.abort( finalText ); - } - done( 0, finalText ); - return this; - } - }; - - // Attach deferreds - deferred.promise( jqXHR ); - - // Add protocol if not provided (prefilters might expect it) - // Handle falsy url in the settings object (#10093: consistency with old signature) - // We also use the url parameter if available - s.url = ( ( url || s.url || location.href ) + "" ) - .replace( rprotocol, location.protocol + "//" ); - - // Alias method option to type as per ticket #12004 - s.type = options.method || options.type || s.method || s.type; - - // Extract dataTypes list - s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ]; - - // A cross-domain request is in order when the origin doesn't match the current origin. - if ( s.crossDomain == null ) { - urlAnchor = document.createElement( "a" ); - - // Support: IE <=8 - 11, Edge 12 - 15 - // IE throws exception on accessing the href property if url is malformed, - // e.g. http://example.com:80x/ - try { - urlAnchor.href = s.url; - - // Support: IE <=8 - 11 only - // Anchor's host property isn't correctly set when s.url is relative - urlAnchor.href = urlAnchor.href; - s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !== - urlAnchor.protocol + "//" + urlAnchor.host; - } catch ( e ) { - - // If there is an error parsing the URL, assume it is crossDomain, - // it can be rejected by the transport if it is invalid - s.crossDomain = true; - } - } - - // Convert data if not already a string - if ( s.data && s.processData && typeof s.data !== "string" ) { - s.data = jQuery.param( s.data, s.traditional ); - } - - // Apply prefilters - inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); - - // If request was aborted inside a prefilter, stop there - if ( completed ) { - return jqXHR; - } - - // We can fire global events as of now if asked to - // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118) - fireGlobals = jQuery.event && s.global; - - // Watch for a new set of requests - if ( fireGlobals && jQuery.active++ === 0 ) { - jQuery.event.trigger( "ajaxStart" ); - } - - // Uppercase the type - s.type = s.type.toUpperCase(); - - // Determine if request has content - s.hasContent = !rnoContent.test( s.type ); - - // Save the URL in case we're toying with the If-Modified-Since - // and/or If-None-Match header later on - // Remove hash to simplify url manipulation - cacheURL = s.url.replace( rhash, "" ); - - // More options handling for requests with no content - if ( !s.hasContent ) { - - // Remember the hash so we can put it back - uncached = s.url.slice( cacheURL.length ); - - // If data is available and should be processed, append data to url - if ( s.data && ( s.processData || typeof s.data === "string" ) ) { - cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; - - // #9682: remove data so that it's not used in an eventual retry - delete s.data; - } - - // Add or update anti-cache param if needed - if ( s.cache === false ) { - cacheURL = cacheURL.replace( rantiCache, "$1" ); - uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce.guid++ ) + - uncached; - } - - // Put hash and anti-cache on the URL that will be requested (gh-1732) - s.url = cacheURL + uncached; - - // Change '%20' to '+' if this is encoded form body content (gh-2658) - } else if ( s.data && s.processData && - ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) { - s.data = s.data.replace( r20, "+" ); - } - - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - if ( jQuery.lastModified[ cacheURL ] ) { - jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); - } - if ( jQuery.etag[ cacheURL ] ) { - jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); - } - } - - // Set the correct header, if data is being sent - if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { - jqXHR.setRequestHeader( "Content-Type", s.contentType ); - } - - // Set the Accepts header for the server, depending on the dataType - jqXHR.setRequestHeader( - "Accept", - s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? - s.accepts[ s.dataTypes[ 0 ] ] + - ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : - s.accepts[ "*" ] - ); - - // Check for headers option - for ( i in s.headers ) { - jqXHR.setRequestHeader( i, s.headers[ i ] ); - } - - // Allow custom headers/mimetypes and early abort - if ( s.beforeSend && - ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) { - - // Abort if not done already and return - return jqXHR.abort(); - } - - // Aborting is no longer a cancellation - strAbort = "abort"; - - // Install callbacks on deferreds - completeDeferred.add( s.complete ); - jqXHR.done( s.success ); - jqXHR.fail( s.error ); - - // Get transport - transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); - - // If no transport, we auto-abort - if ( !transport ) { - done( -1, "No Transport" ); - } else { - jqXHR.readyState = 1; - - // Send global event - if ( fireGlobals ) { - globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); - } - - // If request was aborted inside ajaxSend, stop there - if ( completed ) { - return jqXHR; - } - - // Timeout - if ( s.async && s.timeout > 0 ) { - timeoutTimer = window.setTimeout( function() { - jqXHR.abort( "timeout" ); - }, s.timeout ); - } - - try { - completed = false; - transport.send( requestHeaders, done ); - } catch ( e ) { - - // Rethrow post-completion exceptions - if ( completed ) { - throw e; - } - - // Propagate others as results - done( -1, e ); - } - } - - // Callback for when everything is done - function done( status, nativeStatusText, responses, headers ) { - var isSuccess, success, error, response, modified, - statusText = nativeStatusText; - - // Ignore repeat invocations - if ( completed ) { - return; - } - - completed = true; - - // Clear timeout if it exists - if ( timeoutTimer ) { - window.clearTimeout( timeoutTimer ); - } - - // Dereference transport for early garbage collection - // (no matter how long the jqXHR object will be used) - transport = undefined; - - // Cache response headers - responseHeadersString = headers || ""; - - // Set readyState - jqXHR.readyState = status > 0 ? 4 : 0; - - // Determine if successful - isSuccess = status >= 200 && status < 300 || status === 304; - - // Get response data - if ( responses ) { - response = ajaxHandleResponses( s, jqXHR, responses ); - } - - // Use a noop converter for missing script - if ( !isSuccess && jQuery.inArray( "script", s.dataTypes ) > -1 ) { - s.converters[ "text script" ] = function() {}; - } - - // Convert no matter what (that way responseXXX fields are always set) - response = ajaxConvert( s, response, jqXHR, isSuccess ); - - // If successful, handle type chaining - if ( isSuccess ) { - - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - modified = jqXHR.getResponseHeader( "Last-Modified" ); - if ( modified ) { - jQuery.lastModified[ cacheURL ] = modified; - } - modified = jqXHR.getResponseHeader( "etag" ); - if ( modified ) { - jQuery.etag[ cacheURL ] = modified; - } - } - - // if no content - if ( status === 204 || s.type === "HEAD" ) { - statusText = "nocontent"; - - // if not modified - } else if ( status === 304 ) { - statusText = "notmodified"; - - // If we have data, let's convert it - } else { - statusText = response.state; - success = response.data; - error = response.error; - isSuccess = !error; - } - } else { - - // Extract error from statusText and normalize for non-aborts - error = statusText; - if ( status || !statusText ) { - statusText = "error"; - if ( status < 0 ) { - status = 0; - } - } - } - - // Set data for the fake xhr object - jqXHR.status = status; - jqXHR.statusText = ( nativeStatusText || statusText ) + ""; - - // Success/Error - if ( isSuccess ) { - deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); - } else { - deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); - } - - // Status-dependent callbacks - jqXHR.statusCode( statusCode ); - statusCode = undefined; - - if ( fireGlobals ) { - globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", - [ jqXHR, s, isSuccess ? success : error ] ); - } - - // Complete - completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); - - if ( fireGlobals ) { - globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); - - // Handle the global AJAX counter - if ( !( --jQuery.active ) ) { - jQuery.event.trigger( "ajaxStop" ); - } - } - } - - return jqXHR; - }, - - getJSON: function( url, data, callback ) { - return jQuery.get( url, data, callback, "json" ); - }, - - getScript: function( url, callback ) { - return jQuery.get( url, undefined, callback, "script" ); - } -} ); - -jQuery.each( [ "get", "post" ], function( _i, method ) { - jQuery[ method ] = function( url, data, callback, type ) { - - // Shift arguments if data argument was omitted - if ( isFunction( data ) ) { - type = type || callback; - callback = data; - data = undefined; - } - - // The url can be an options object (which then must have .url) - return jQuery.ajax( jQuery.extend( { - url: url, - type: method, - dataType: type, - data: data, - success: callback - }, jQuery.isPlainObject( url ) && url ) ); - }; -} ); - -jQuery.ajaxPrefilter( function( s ) { - var i; - for ( i in s.headers ) { - if ( i.toLowerCase() === "content-type" ) { - s.contentType = s.headers[ i ] || ""; - } - } -} ); - - -jQuery._evalUrl = function( url, options, doc ) { - return jQuery.ajax( { - url: url, - - // Make this explicit, since user can override this through ajaxSetup (#11264) - type: "GET", - dataType: "script", - cache: true, - async: false, - global: false, - - // Only evaluate the response if it is successful (gh-4126) - // dataFilter is not invoked for failure responses, so using it instead - // of the default converter is kludgy but it works. - converters: { - "text script": function() {} - }, - dataFilter: function( response ) { - jQuery.globalEval( response, options, doc ); - } - } ); -}; - - -jQuery.fn.extend( { - wrapAll: function( html ) { - var wrap; - - if ( this[ 0 ] ) { - if ( isFunction( html ) ) { - html = html.call( this[ 0 ] ); - } - - // The elements to wrap the target around - wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); - - if ( this[ 0 ].parentNode ) { - wrap.insertBefore( this[ 0 ] ); - } - - wrap.map( function() { - var elem = this; - - while ( elem.firstElementChild ) { - elem = elem.firstElementChild; - } - - return elem; - } ).append( this ); - } - - return this; - }, - - wrapInner: function( html ) { - if ( isFunction( html ) ) { - return this.each( function( i ) { - jQuery( this ).wrapInner( html.call( this, i ) ); - } ); - } - - return this.each( function() { - var self = jQuery( this ), - contents = self.contents(); - - if ( contents.length ) { - contents.wrapAll( html ); - - } else { - self.append( html ); - } - } ); - }, - - wrap: function( html ) { - var htmlIsFunction = isFunction( html ); - - return this.each( function( i ) { - jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); - } ); - }, - - unwrap: function( selector ) { - this.parent( selector ).not( "body" ).each( function() { - jQuery( this ).replaceWith( this.childNodes ); - } ); - return this; - } -} ); - - -jQuery.expr.pseudos.hidden = function( elem ) { - return !jQuery.expr.pseudos.visible( elem ); -}; -jQuery.expr.pseudos.visible = function( elem ) { - return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); -}; - - - - -jQuery.ajaxSettings.xhr = function() { - try { - return new window.XMLHttpRequest(); - } catch ( e ) {} -}; - -var xhrSuccessStatus = { - - // File protocol always yields status code 0, assume 200 - 0: 200, - - // Support: IE <=9 only - // #1450: sometimes IE returns 1223 when it should be 204 - 1223: 204 - }, - xhrSupported = jQuery.ajaxSettings.xhr(); - -support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); -support.ajax = xhrSupported = !!xhrSupported; - -jQuery.ajaxTransport( function( options ) { - var callback, errorCallback; - - // Cross domain only allowed if supported through XMLHttpRequest - if ( support.cors || xhrSupported && !options.crossDomain ) { - return { - send: function( headers, complete ) { - var i, - xhr = options.xhr(); - - xhr.open( - options.type, - options.url, - options.async, - options.username, - options.password - ); - - // Apply custom fields if provided - if ( options.xhrFields ) { - for ( i in options.xhrFields ) { - xhr[ i ] = options.xhrFields[ i ]; - } - } - - // Override mime type if needed - if ( options.mimeType && xhr.overrideMimeType ) { - xhr.overrideMimeType( options.mimeType ); - } - - // X-Requested-With header - // For cross-domain requests, seeing as conditions for a preflight are - // akin to a jigsaw puzzle, we simply never set it to be sure. - // (it can always be set on a per-request basis or even using ajaxSetup) - // For same-domain requests, won't change header if already provided. - if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) { - headers[ "X-Requested-With" ] = "XMLHttpRequest"; - } - - // Set headers - for ( i in headers ) { - xhr.setRequestHeader( i, headers[ i ] ); - } - - // Callback - callback = function( type ) { - return function() { - if ( callback ) { - callback = errorCallback = xhr.onload = - xhr.onerror = xhr.onabort = xhr.ontimeout = - xhr.onreadystatechange = null; - - if ( type === "abort" ) { - xhr.abort(); - } else if ( type === "error" ) { - - // Support: IE <=9 only - // On a manual native abort, IE9 throws - // errors on any property access that is not readyState - if ( typeof xhr.status !== "number" ) { - complete( 0, "error" ); - } else { - complete( - - // File: protocol always yields status 0; see #8605, #14207 - xhr.status, - xhr.statusText - ); - } - } else { - complete( - xhrSuccessStatus[ xhr.status ] || xhr.status, - xhr.statusText, - - // Support: IE <=9 only - // IE9 has no XHR2 but throws on binary (trac-11426) - // For XHR2 non-text, let the caller handle it (gh-2498) - ( xhr.responseType || "text" ) !== "text" || - typeof xhr.responseText !== "string" ? - { binary: xhr.response } : - { text: xhr.responseText }, - xhr.getAllResponseHeaders() - ); - } - } - }; - }; - - // Listen to events - xhr.onload = callback(); - errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" ); - - // Support: IE 9 only - // Use onreadystatechange to replace onabort - // to handle uncaught aborts - if ( xhr.onabort !== undefined ) { - xhr.onabort = errorCallback; - } else { - xhr.onreadystatechange = function() { - - // Check readyState before timeout as it changes - if ( xhr.readyState === 4 ) { - - // Allow onerror to be called first, - // but that will not handle a native abort - // Also, save errorCallback to a variable - // as xhr.onerror cannot be accessed - window.setTimeout( function() { - if ( callback ) { - errorCallback(); - } - } ); - } - }; - } - - // Create the abort callback - callback = callback( "abort" ); - - try { - - // Do send the request (this may raise an exception) - xhr.send( options.hasContent && options.data || null ); - } catch ( e ) { - - // #14683: Only rethrow if this hasn't been notified as an error yet - if ( callback ) { - throw e; - } - } - }, - - abort: function() { - if ( callback ) { - callback(); - } - } - }; - } -} ); - - - - -// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432) -jQuery.ajaxPrefilter( function( s ) { - if ( s.crossDomain ) { - s.contents.script = false; - } -} ); - -// Install script dataType -jQuery.ajaxSetup( { - accepts: { - script: "text/javascript, application/javascript, " + - "application/ecmascript, application/x-ecmascript" - }, - contents: { - script: /\b(?:java|ecma)script\b/ - }, - converters: { - "text script": function( text ) { - jQuery.globalEval( text ); - return text; - } - } -} ); - -// Handle cache's special case and crossDomain -jQuery.ajaxPrefilter( "script", function( s ) { - if ( s.cache === undefined ) { - s.cache = false; - } - if ( s.crossDomain ) { - s.type = "GET"; - } -} ); - -// Bind script tag hack transport -jQuery.ajaxTransport( "script", function( s ) { - - // This transport only deals with cross domain or forced-by-attrs requests - if ( s.crossDomain || s.scriptAttrs ) { - var script, callback; - return { - send: function( _, complete ) { - script = jQuery( " - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
- - - -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/appendix-b/index.html b/docs/appendix-b/index.html deleted file mode 100644 index 6e1a660b6ea85d5f56bdd7e9f061c85c9dbea699..0000000000000000000000000000000000000000 --- a/docs/appendix-b/index.html +++ /dev/null @@ -1,266 +0,0 @@ - - - - - - - - - - 常见工具的使用方法 — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

常见工具的使用方法

-
-
-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/appendix-c/index.html b/docs/appendix-c/index.html deleted file mode 100644 index eaf0e95acc5514e04c87696bc7698ea1a77fb16e..0000000000000000000000000000000000000000 --- a/docs/appendix-c/index.html +++ /dev/null @@ -1,266 +0,0 @@ - - - - - - - - - - 深入机器模式:RustSBI — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

深入机器模式:RustSBI

-
-
-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/chapter1/1app-ee-platform.html b/docs/chapter1/1app-ee-platform.html deleted file mode 100644 index 48607f0c187e15cd229bb895c123f058972f1461..0000000000000000000000000000000000000000 --- a/docs/chapter1/1app-ee-platform.html +++ /dev/null @@ -1,448 +0,0 @@ - - - - - - - - - - 应用程序运行环境与平台支持 — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

应用程序运行环境与平台支持

-
-
-

作为一切的开始,让我们使用 Cargo 工具来创建一个 Rust 项目。它看上去没有任何特别之处:

-
$ cargo new os --bin
-
-
-

我们加上了 --bin 选项来告诉 Cargo 我们创建一个可执行项目而不是库项目。此时,项目的文件结构如下:

-
$ tree os
-os
-├── Cargo.toml
-└── src
-    └── main.rs
-
-1 directory, 2 files
-
-
-

其中 Cargo.toml 中保存着项目的配置,包括作者的信息、联系方式以及库依赖等等。显而易见源代码保存在 src 目录下,目前为止只有 main.rs -一个文件,让我们看一下里面的内容:

-
-
最简单的 Rust 应用
-
1
-2
-3
fn main() {
-    println!("Hello, world!");
-}
-
-
-
-

进入 os 项目根目录下,利用 Cargo 工具即可一条命令实现构建并运行项目:

-
$ cargo run
-   Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
-    Finished dev [unoptimized + debuginfo] target(s) in 1.15s
-     Running `target/debug/os`
-Hello, world!
-
-
-

如我们预想的一样,我们在屏幕上看到了一行 Hello, world! 。但是,需要注意到我们所享受到的编程的方便并不是理所当然的,背后有着从硬件 -到软件的多种机制的支持。

-
-

应用程序运行环境

-

如下图所示,应用程序的运行需要下面一套运行环境栈的支持:

-
-../_images/app-software-stack.png -

应用程序运行环境栈:图中的白色块自上而下(越往下则越靠近底层,下层作为上层的执行环境支持上层代码的运行)表示各级运行环境, -黑色块则表示相邻两层运行环境之间的接口。

-
-

我们的应用位于最上层,它可以通过调用编程语言提供的标准库或者其他三方库对外提供的功能强大的函数接口,使得仅需少量的源代码就能完成复杂的 -功能。但是这些库的功能不仅限于此,事实上它们属于应用程序的 执行环境 (Execution Environment),在我们通常不会注意到的地方,它 -们还会在执行应用之前完成一些初始化工作,并在应用程序执行的时候对它进行监控。我们在打印 Hello, world! 时使用的 println! -宏正是由 Rust 标准库 std 提供的。

-

从内核/操作系统的角度看来,它上面的一切都属于用户态,而它自身属于内核态。无论用户态应用如何编写,是手写汇编代码,还是基于某种编程语言利用 -其标准库或三方库,某些功能总要直接或间接的通过内核/操作系统提供的 系统调用 (System Call) 来实现。因此系统调用充当了用户和内核之间 -的边界。内核作为用户态的运行环境,它不仅要提供系统调用接口,还需要对用户态应用的执行进行监控和管理。

-
-

注解

-

Hello, world! 用到了哪些系统调用?

-

从之前的 cargo run 的输出可以看出之前构建的可执行文件是在 target/debug 目录下的 os 。 -在 Ubuntu 系统上,可以通过 strace 工具来运行一个程序并输出程序运行过程当中向内核请求的所有的系统调用及其返回值。 -我们只需输入 strace target/debug/os 即可看到一长串的系统调用。

-

其中,真正容易看出与 Hello, world! 相关的只有一个系统调用:

-
write(1, "Hello, world!\n", 14)         = 14
-
-
-

其参数的具体含义我们暂且不在这里进行解释。

-

其余的系统调用基本上分别用于函数库和内核两层执行环境的初始化工作和对于上层的运行期监控和管理。之后,随着应用场景的复杂化,我们 -需要更强的抽象能力,也会实现这里面的一些系统调用。

-
-

从硬件的角度来看,它上面的一切都属于软件。硬件可以分为三种: 处理器 (Processor) ——它更常见的名字是中央处理单元 (CPU, Central Processing Unit), -内存 (Memory) 还有 I/O 设备。其中处理器无疑是其中最复杂同时也最关键的一个。它与软件约定一套 指令集体系结构 (ISA, Instruction Set Architecture), -使得软件可以通过 ISA 中提供的汇编指令来访问各种硬件资源。软件当然也需要知道处理器会如何执行这些指令:最简单的话就是一条一条执行位于内存 -中的指令。当然,实际的情况远比这个要复杂得多,为了适应现代应用程序的场景,处理器还需要提供很多额外的机制,而不仅仅是让数据在 CPU 寄存器、内存和 I/O 设备 -三者之间流动。

-
-

注解

-

多层执行环境都是必需的吗?

-

除了最上层的应用程序和最下层的硬件平台必须存在之外,作为中间层的函数库和内核并不是必须存在的:它们都是对下层资源进行了 抽象 (Abstraction), -并为上层提供了一套运行环境。抽象的优点在于它让上层以较小的代价获得所需的功能,并同时可以提供一些保护。但抽象同时也是一种限制,会丧失一些 -应有的灵活性。比如,当你在考虑在项目中应该使用哪个函数库的时候,就常常需要这方面的权衡:过多的抽象和过少的抽象自然都是不合适的。

-

实际上,我们通过应用程序的特征来判断它需要什么程度的抽象。

-
    -
  • 如果函数库和内核都不存在,那么我们就是在手写汇编代码,这种方式具有最高的灵活性,抽象能力则最低,基本等同于硬件。我们通常用这种方式来 -实现一些架构相关且仅通过编程语言无法描述的小模块或者代码片段。

  • -
  • 如果仅存在函数库而不存在内核,意味着我们不需要内核提供的抽象。在嵌入式场景就常常会出现这种情况。嵌入式设备虽然也包含 CPU、内存和 I/O -设备,但是它上面通常只会同时运行一个或几个功能非常简单的小应用程序,其定位就是那种功能单一的场景,比如人脸识别打卡系统等。我们常用的 -操作系统如 Windows/Linux/macOS 等的抽象都支持同时运行很多应用程序,在嵌入式场景是过抽象的。因此,常见的解决方案是仅使用函数库构建 -单独的应用程序或是用专为应用场景特别裁减过的轻量级内核管理少数应用程序。

  • -
-
-
-
-

平台与目标三元组

-

对于一份用某种编程语言实现的源代码而言,编译器在将其通过编译、链接得到目标文件的时候需要知道程序要在哪个 平台 (Platform) 上运行。 -从上面给出的 应用程序运行环境栈 可以看出:

-
    -
  • 如果用户态基于的内核不同,会导致系统调用接口不同或者语义不一致;

  • -
  • 如果底层硬件不同,对于硬件资源的访问方式会有差异。特别是 ISA 不同的话,对上提供的指令集和寄存器都不同。

  • -
-

它们都会导致最终生成的目标文件有很大不同。需要指出的是,某些编译器支持同一份源代码无需修改就可编译到多个不同的目标平台并在上面运行。这种 -情况下,源代码是 跨平台 的。而另一些编译器则已经预设好了一个固定的目标平台。

-

我们可以通过 目标三元组 (Target Triplet) 来描述一个目标平台。它一般包括 CPU 架构、CPU 厂商和操作系统,它们确实都会控制目标文件的生成。 -比如,我们可以尝试看一下之前的 Hello, world! 的目标平台是什么。这可以通过打印编译器 rustc 的默认配置信息:

-
$ rustc --version --verbose
-rustc 1.48.0-nightly (73dc675b9 2020-09-06)
-binary: rustc
-commit-hash: 73dc675b9437c2a51a975a9f58cc66f05463c351
-commit-date: 2020-09-06
-host: x86_64-unknown-linux-gnu
-release: 1.48.0-nightly
-LLVM version: 11.0
-
-
-

从其中的 host 一项可以看出默认的目标平台是 x86_64-unknown-linux-gnu,其中 CPU 架构是 x86_64,CPU 厂商是 unknown,操作系统是 linux-gnu。 -这种无论编译器还是其目标文件都在我们当前所处的平台运行是一种最简单也最普遍的情况。但是很快我们就将遇到另外一种情况。

-

讲了这么多,终于该介绍我们的主线任务了。我们希望能够在另一个平台上运行 Hello, world!,而与之前的默认平台不同的地方在于,我们将 CPU 架构从 -x86_64 换成 RISC-V。

-
-

注解

-

为何基于 RISC-V 架构而非 x86 系列架构?

-

x86 架构为了在升级换代的同时保持对基于旧版架构应用程序/内核的兼容性,存在大量的历史包袱,也就是一些对于目前的应用场景没有任何意义,但又必须 -花大量时间正确设置才能正常使用 CPU 的奇怪设定。为了建立并维护架构的应用生态,这确实是必不可少的,但站在教学的角度几乎完全是在浪费时间。而 -新生的 RISC-V 架构十分简洁,架构文档需要阅读的核心部分不足百页,且这些功能已经足以用来构造一个具有相当抽象能力的内核了。

-
-

可以看一下目前 Rust 编译器支持哪些基于 RISC-V 的平台:

-
$ rustc --print target-list | grep riscv
-riscv32i-unknown-none-elf
-riscv32imac-unknown-none-elf
-riscv32imc-unknown-none-elf
-riscv64gc-unknown-linux-gnu
-riscv64gc-unknown-none-elf
-riscv64imac-unknown-none-elf
-
-
-

这里我们选择的是 riscv64gc-unknown-none-elf,目标三元组中的操作系统是 none-elf,表明没有任何系统调用支持。这里我们之所以不选择有 -linux-gnu 系统调用支持的版本 riscv64gc-unknown-linux-gnu,是因为我们只是想跑一个 Hello, world!,没有必要使用操作系统所提供的 -那么高级的抽象。而且我们很清楚后续我们要开发的是一个内核,如果仅仅基于已有操作系统提供的系统调用的话,它自身的抽象能力会受到很大限制。所以它必须 -直面底层硬件来解锁更大的抽象能力上限。

-
-

注解

-

RISC-V 指令集拓展

-

由于基于 RISC-V 架构的处理器可能用于嵌入式场景或是通用计算场景,因此指令集规范将指令集划分为最基本的 RV32/64I 以及若干标准指令集拓展。 -每款处理器只需按照其实际应用场景按需实现指令集拓展即可。

-
    -
  • RV32/64I:每款处理器都必须实现的基本整数指令集。在 RV32I 中,每个通用寄存器的位宽为 32 位;在 RV64I 中则为 64 位。它可以用来模拟 -绝大多数标准指令集拓展中的指令,除了比较特殊的 A 拓展,因为它需要特别的硬件支持。

  • -
  • M 拓展:提供整数乘除法相关指令。

  • -
  • A 拓展:提供原子指令和一些相关的内存同步机制,这个后面会展开。

  • -
  • F/D 拓展:提供单/双精度浮点数运算支持。

  • -
  • C 拓展:提供压缩指令拓展。

  • -
-

G 拓展是基本整数指令集 I 再加上标准指令集拓展 MAFD 的总称,因此 riscv64gc 也就等同于 riscv64imafdc。我们剩下的内容都基于该处理器 -架构完成。除此之外 RISC-V 架构还有很多标准指令集拓展,有一些还在持续更新中尚未稳定,有兴趣的读者可以浏览最新版的 RISC-V 指令集规范。

-
-
-
-

Rust 标准库与核心库

-

我们尝试一下将当前的 Hello, world! 程序的目标平台换成 riscv64gc-unknown-none-elf 看看会发生什么事情:

-
$ cargo run --target riscv64gc-unknown-none-elf
-   Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
-error[E0463]: can't find crate for `std`
-  |
-  = note: the `riscv64gc-unknown-none-elf` target may not be installed
-
-
-

在之前的环境配置中,我们已经在 rustup 工具链中安装了这个目标平台支持,因此并不是该目标平台未安装的问题。因此只是单纯的在这个目标平台上找不到 -Rust 标准库 std。我们之前曾经提到过,编程语言的标准库或三方库的某些功能会直接或间接的用到操作系统提供的系统调用。但目前我们所选的目标平台不存在 -任何操作系统支持,于是 Rust 并没有为这个目标平台支持完整的标准库 std。类似这样的平台通常被我们称为 裸机平台 (bare-metal)。

-

幸运的是,Rust 有一个对 std 裁剪过后的核心库 core,这个库是不需要任何操作系统支持的,相对的它的功能也比较受限,但是也包含了 Rust 语言 -相当一部分的核心机制,可以满足我们的大部分需求。在 Rust 语言生态中,有很多三方库也不依赖标准库 std 而仅仅依赖核心库 core,它们也可以很大 -程度上减轻我们的编程负担。它们是我们能够在裸机平台挣扎求生的最主要倚仗。

-

于是,我们知道在裸机平台上我们要将对于标准库 std 的引用换成核心库 core。但是做起来其实并没有那么容易。

-
-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/chapter1/2remove-std.html b/docs/chapter1/2remove-std.html deleted file mode 100644 index eb32cc4d8746056209098ab11404be13796f0149..0000000000000000000000000000000000000000 --- a/docs/chapter1/2remove-std.html +++ /dev/null @@ -1,389 +0,0 @@ - - - - - - - - - - 移除标准库依赖 — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

移除标准库依赖

-
-
-

本节我们尝试移除之前的 Hello world! 程序对于标准库的依赖,使得它能够编译到裸机平台 RV64GC 上。

-

我们首先在 os 目录下新建 .cargo 目录,并在这个目录下创建 config 文件,并在里面输入如下内容:

-
// os/.cargo/config
-[build]
-target = "riscv64gc-unknown-none-elf"
-
-
-

这会对于 Cargo 工具在 os 目录下的行为进行调整:现在默认会使用 riscv64gc 作为目标平台而不是原先的默认 x86_64-unknown-linux-gnu。 -事实上,这是一种编译器运行所在的平台与编译器生成可执行文件的目标平台不同(分别是后者和前者)的情况。这是一种 交叉编译 (Cross Compile)。

-

当然,这只是使得我们之后在 cargo build 的时候不必再加上 --target 参数的一个小 trick。如果我们现在 cargo build ,还是会和 -上一小节一样出现找不到标准库 std 的错误。于是我们开始着手移除标准库。当然,这会产生一些副作用。

-
-

移除 println! 宏

-

我们在 main.rs 的开头加上一行 #![no_std] 来告诉 Rust 编译器不使用 Rust 标准库 std 转而使用核心库 core。编译器报出如下错误:

-
-

错误

-
$ cargo build
-   Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
-error: cannot find macro `println` in this scope
---> src/main.rs:4:5
-  |
-4 |     println!("Hello, world!");
-  |     ^^^^^^^
-
-
-
-

我们之前提到过, println! 宏是由标准库 std 提供的,且会使用到一个名为 write 的系统调用。现在我们的条件还不足以自己实现一个 println! 宏,由于 -使用了系统调用也不能在核心库 core 中找到它。我们目前先通过将它注释掉来绕过它。

-
-
-

提供语义项 panic_handler

-
-

错误

-
$ cargo build
-   Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
-error: `#[panic_handler]` function required, but not found
-
-
-
-

在使用 Rust 编写应用程序的时候,我们常常在遇到了一些无法恢复的致命错误导致程序无法继续向下运行的时候手动或自动调用 panic! 宏来并打印出错的 -位置让我们能够意识到它的存在,并进行一些后续处理。panic! 宏最典型的应用场景包括断言宏 assert! 失败或者对 Option::None/Result::Err -进行 unwrap 操作。

-

在标准库 std 中提供了 panic 的处理函数 #[panic_handler],其大致功能是打印出错位置和原因并杀死当前应用。可惜的是在核心库 core 中并没有提供, -因此我们需要自己实现 panic 处理函数。

-
-

注解

-

Rust 语义项 lang_items

-

Rust 编译器内部的某些功能的实现并不是硬编码在语言内部的,而是以一种可插入的形式在库中提供。库只需要通过某种方式告诉编译器它的某个方法实现了 -编译器内部的哪些功能,编译器就会采用库提供的方法来实现它内部对应的功能。通常只需要在库的方法前面加上一个标记即可。

-
-

我们开一个新的子模块 lang_items.rs 保存这些语义项,在里面提供 panic 处理函数的实现并通过标记通知编译器采用我们的实现:

-
// os/src/lang_items.rs
-use core::panic::PanicInfo;
-
-#[panic_handler]
-fn panic(_info: &PanicInfo) -> ! {
-    loop {}
-}
-
-
-

注意,panic 处理函数的函数签名需要一个 PanicInfo 的不可变借用作为输入参数,它在核心库中得以保留,这也是我们第一次与核心库打交道。之后我们 -会从 PanicInfo 解析出错位置并打印出来,然后杀死应用程序。但目前我们什么都不做只是在原地 loop。

-
-
-

移除 main 函数

-
-

错误

-
$ cargo build
-   Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
-error: requires `start` lang_item
-
-
-
-

编译器提醒我们缺少一个名为 start 的语义项。我们回忆一下,之前提到语言标准库和三方库作为应用程序的执行环境,需要负责在执行应用程序之前进行 -一些初始化工作,然后才跳转到应用程序的入口点(也就是跳转到我们编写的 main 函数)开始执行。事实上 start 语义项正代表着标准库 std 在 -执行应用程序之前需要进行的一些初始化工作。由于我们禁用了标准库,编译器也就找不到这项功能的实现了。

-

最简单的解决方案就是压根不让编译器使用这项功能。我们在 main.rs 的开头加入设置 #![no_main] 告诉编译器我们没有一般意义上的 main 函数, -并将原来的 main 函数删除。在失去了 main 函数的情况下,编译器也就不需要完成所谓的初始化工作了。

-

至此,我们成功移除了标准库的依赖并完成裸机平台上的构建。

-
$ cargo build
-   Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
-    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
-
-
-

目前的代码如下:

-
// os/src/main.rs
-#![no_std]
-#![no_main]
-
-mod lang_items;
-
-// os/src/lang_items.rs
-use core::panic::PanicInfo;
-
-#[panic_handler]
-fn panic(_info: &PanicInfo) -> ! {
-    loop {}
-}
-
-
-

本小节我们固然脱离了标准库,通过了编译器的检验,但也是伤筋动骨,将原有的很多功能弱化甚至直接删除,看起来距离在 RV64GC 平台上打印 -Hello world! 相去甚远了(我们甚至连 println! 和 main 函数都删除了)。不要着急,接下来我们会以自己的方式来重塑这些 -功能,并最终完成我们的目标。

-
-

注解

-

在 x86_64 平台上移除标准库依赖

-

有兴趣的同学可以将目标平台换回之前默认的 x86_64-unknown-linux-gnu 并重复本小节所做的事情,比较两个平台从 ISA 到操作系统 -的差异。可以参考 BlogOS 的相关内容

-
-
-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/chapter1/3minimal-rt.html b/docs/chapter1/3minimal-rt.html deleted file mode 100644 index 8ba341c7d5b9798fa5ec02042e58af45e964bc08..0000000000000000000000000000000000000000 --- a/docs/chapter1/3minimal-rt.html +++ /dev/null @@ -1,756 +0,0 @@ - - - - - - - - - - 重建最小化运行时 — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

重建最小化运行时

-
-
-

本节开始我们将着手自己来实现之前被我们移除的 Hello, world! 程序中的功能。在这一小节,我们介绍如何进行 执行环境初始化

-

我们在上一小节提到过,一个应用程序的运行离不开下面多层执行环境栈的支撑。以 Hello, world! 程序为例,在目前广泛使用的操作系统上, -它就至少需要经历以下层层递进的初始化过程:

-
    -
  • 一段汇编代码对硬件进行初始化,让上层包括内核在内的软件得以运行;

  • -
  • 要运行该程序的时候,内核分配相应资源,将程序代码和数据载入内存,并赋予 CPU 使用权,由此应用程序可以运行;

  • -
  • 程序员编写的代码是应用程序的一部分,它需要标准库进行一些初始化工作后才能运行。

  • -
-

但在上一小节中,由于目标平台 riscv64gc-unknown-none-elf 没有任何操作系统支持,我们只能禁用标准库并移除默认的 main 函数 -入口。但是最终我们还是要将 main 恢复回来并且在里面输出 Hello, world! 的。因此,我们需要知道具体需要做哪些初始化工作才能支持 -main 的运行。

-

而这又需要明确两点:首先是系统在做这些初始化工作之前处于什么状态,在做完初始化工作也就是即将执行 main 函数之前又处于什么状态。比较二者 -即可得出答案。

-

让我们从 CPU 加电后第一条指令开始讲起。对于裸机平台 riscv64gc-unknown-none-elf 而言,它的 pc 寄存器会被设置为 0x80000000 , -也就是说它会从这个 物理地址 (Physical Address) 开始一条条取指并执行放置于 物理内存 (Physical Memory) 中的指令。

-
-

注解

-

物理内存与物理地址

-

物理内存是计算机体系结构中一个重要的组成部分。在存储方面,CPU 唯一能够直接访问的只有物理内存中的数据,它可以通过访存指令来达到这一目的。 -从 CPU 的视角看来,可以将物理内存看成一个大字节数组,而物理地址则对应于一个能够用来访问数组中某个元素的下标。与我们日常编程习惯不同的 -是,该下标通常不以 0 开头,而通常以 0x80000000 开头。总结一下的话就是, CPU 可以通过物理地址来 逐字节 访问物理内存中保存的 -数据。

-

值得一提的是,当 CPU 以多个字节(比如 2/4/8 或更多)为单位访问物理内存(事实上并不局限于物理内存)中的数据时,就有可能会引入端序和 -地址对齐的问题。由于这并不是重点,我们在这里不展开说明。

-
-

在该目标平台上,物理内存以物理地址 0x80000000 开头的部分放置着 引导加载程序 (Bootloader) 的代码。它的任务是对硬件进行一些 -初始化工作,并跳转到一个固定的物理地址 0x80020000 。在本书正文中我们无需关心它的实现,而是当成一个黑盒使用即可,它的预编译版本 -可执行文件放在项目根目录的 bootloader 目录下。在这之后,控制权就会被移交到我们手中。因此,我们需要保证我们负责的初始化的代码 -出现在物理内存以物理地址 0x80020000 开头的地方。在我们的初始化任务完成之后,自然需要跳转到 main 函数进行执行里面的代码,这也是 -初始化任务的一个重要部分。

-

但实际上不止如此,我们还需要考虑栈的设置。

-
-

函数调用与栈

-

从汇编指令的级别看待一段程序的执行,假如 CPU 依次执行的指令的物理地址序列为 \(\{a_n\}\),那么这个序列会符合怎样的模式呢?

-

其中最简单的无疑就是 CPU 一条条连续向下执行指令,也即满足递推式 \(a_{n+1}=a_n+L\),这里我们假设该平台的指令是定长的且均为 -\(L\) 字节(常见情况为 2/4 字节)。但是执行序列并不总是符合这种模式,当位于物理地址 \(a_n\) 的指令是一条跳转指令的时候, -该模式就有可能被破坏。跳转指令对应于我们在程序中构造的 控制流 (Control Flow) 的多种不同结构,比如分支结构(如 if/switch 语句) -和循环结构(如 for/while 语句)。用来实现上述两种结构的跳转指令,只需实现跳转功能,也就是将 pc 寄存器设置到一个指定的地址即可。

-

另一种控制流结构则显得更为复杂: 函数调用 (Function Call)。我们大概清楚调用函数整个过程中代码执行的顺序,如果是从源代码级的 -视角来看,我们会去执行被调用函数的代码,等到它返回之后,我们会回到调用函数对应语句的下一行继续执行。那么我们如何用汇编指令来实现 -这一过程?首先在调用的时候,需要有一条指令跳转到被调用函数的位置,这个看起来和其他控制结构没什么不同;但是在被调用函数返回的时候,我们 -却需要返回那条跳转过来的指令的下一条继续执行。这次用来返回的跳转究竟跳转到何处,在对应的函数调用发生之前是不知道的。比如,我们在两个不同的 -地方调用同一个函数,显然函数返回之后会回到不同的地址。这是一个很大的不同:其他控制流都只需要跳转到一个 编译期固定下来 的地址,而函数调用 -的返回跳转是跳转到一个 运行时确定 (确切地说是在函数调用发生的时候)的地址。

-

对此,指令集必须给用于函数调用的跳转指令一些额外的能力,而不只是单纯的跳转。在 RISC-V 架构上,有两条指令即符合这样的特征:

- - ---- - - - - - - - - - - - - - -
RISC-V 函数调用跳转指令

指令

指令功能

\(\text{jal}\ \text{rd},\ \text{imm}[20:1]\)

\(\text{rd}\leftarrow\text{pc}+4\)

-

\(\text{pc}\leftarrow\text{pc}+\text{imm}\)

-

\(\text{jalr}\ \text{rd},\ (\text{imm}[11:0])\text{rs}\)

\(\text{rd}\leftarrow\text{pc}+4\)

-

\(\text{pc}\leftarrow\text{rs}+\text{imm}\)

-
-
-

注解

-

RISC-V 指令各部分含义

-

在大多数只与通用寄存器打交道的指令中, rs 表示 源寄存器 (Source Register), imm 表示 立即数 (Immediate), -是一个常数,二者构成了指令的输入部分;而 rd 表示 目标寄存器 (Destination Register),它是指令的输出部分。rs 和 rd -可以在 32 个通用寄存器 x0~x31 中选取。但是这三个部分都不是必须的,某些指令只有一种输入类型,另一些指令则没有输出部分。

-
-

从中可以看出,这两条指令除了设置 pc 寄存器完成跳转功能之外,还将当前跳转指令的下一条指令地址保存在 rd 寄存器中。 -(这里假设所有指令的长度均为 4 字节,在不使用 C 标准指令集拓展的情况下成立) -在 RISC-V 架构中, -通常使用 ra(x1) 寄存器作为其中的 rd ,因此在函数返回的时候,只需跳转回 ra 所保存的地址即可。事实上在函数返回的时候我们常常使用一条 -伪指令 (Pseudo Instruction) 跳转回调用之前的位置: ret 。它会被汇编器翻译为 jalr x0, 0(x1),含义为跳转到寄存器 -ra 保存的物理地址,由于 x0 是一个恒为 0 的寄存器,在 rd 中保存这一步被省略。总结一下,在进行函数调用的时候,我们通过 jalr 指令 -保存返回地址并实现跳转;而在函数即将返回的时候,则通过 ret 指令跳转之前的下一条指令继续执行。这两条指令实现了函数调用流程的核心机制。

-

由于我们是在 ra 寄存器中保存返回地址的,我们要保证它在函数执行的全程不发生变化,不然在 ret 之后就会跳转到错误的位置。事实上编译器 -除了函数调用的相关指令之外确实基本上不使用 ra 寄存器。也就是说,如果在函数中没有调用其他函数,那 ra 的值不会变化,函数调用流程 -能够正常工作。但遗憾的是,在实际编写代码的时候我们常常会遇到函数 多层嵌套调用 的情形。我们很容易想象,如果函数不支持嵌套调用,那么编程将会 -变得多么复杂。如果我们试图在一个函数 \(f\) 中调用一个子函数,在跳转到子函数 \(g\) 的同时,ra 会被覆盖成这条跳转指令的 -下一条的地址,而 ra 之前所保存的函数 \(f\) 的返回地址将会 永久丢失

-

因此,若想正确实现嵌套函数调用的控制流,我们必须通过某种方式保证:在一个函数调用子函数的前后,ra 寄存器的值不能发生变化。但实际上, -这并不仅仅局限于 ra 一个寄存器,而是作用于所有的通用寄存器。这是因为,编译器是独立编译每个函数的,因此一个函数并不能知道它所调用的 -子函数修改了哪些寄存器。而站在一个函数的视角,在调用子函数的过程中某些寄存器的值被覆盖的确会对它接下来的执行产生影响。因此这是必要的。 -我们将在控制流转移前后需要保持不变的寄存器集合称之为 上下文 (Context) 或称 活动记录 (Activation Record),利用这一概念 -,则在函数调用前后需要保持不变的寄存器集合被称为函数调用上下文。

-

由于每个 CPU 只有一套寄存器,我们若想在子函数调用前后保持函数调用上下文不变,需要物理内存的帮助。确切的说,在调用子函数之前,我们需要在 -内存中的一个区域 保存 (Save) 函数调用上下文中的寄存器;而之后我们会从内存中同样的区域读取并 恢复 (Restore) 函数调用上下文 -中的寄存器。实际上,这一工作是由子函数的调用者和被调用者(也就是子函数自身)合作完成。函数调用上下文中的寄存器被分为如下两类:

-
    -
  • 被调用者保存 (Callee-Saved) 寄存器,即被调用的函数保证调用它前后,这些寄存器保持不变;

  • -
  • 调用者保存 (Caller-Saved) 寄存器,被调用的函数可能会覆盖这些寄存器。

  • -
-

从名字中可以看出,函数调用上下文由调用者和被调用者分别保存,其具体过程分别如下:

-
    -
  • 调用者:首先保存不希望在函数调用过程中发生变化的调用者保存寄存器,然后通过 jal/jalr 指令调用子函数,返回回来之后恢复这些寄存器。

  • -
  • 被调用者:在函数开头保存函数执行过程中被用到的被调用者保存寄存器,然后执行函数,在退出之前恢复这些寄存器。

  • -
-

我们发现无论是调用者还是被调用者,都会因调用行为而需要两段匹配的保存和恢复寄存器的汇编代码,可以分别将其称为 开场白 (Prologue) 和 -收场白 (Epilogue),它们会由编译器帮我们自动插入。一个函数既有可能作为调用者调用其他函数,也有可能作为被调用者被其他函数调用。对于 -它而言,如果在执行的时候需要修改被调用者保存寄存器,而必须在函数开头的开场白和结尾的收场白处进行保存;对于调用者保存寄存器则可以没有任何 -顾虑的随便使用,因为它在约定中本就不需要承担保证调用者保存寄存器保持不变的义务。

-
-

注解

-

寄存器保存与编译器优化

-

这里值得说明的是,调用者和被调用者实际上只需分别按需保存调用者保存寄存器和被调用者保存寄存器的一个子集。对于调用者而言,那些内容 -并不重要,即使在调用子函数的时候被覆盖也不影响函数执行的调用者保存寄存器不会被编译器保存;而对于被调用者而言,在其执行过程中没有 -使用到的被调用者保存寄存器也无需保存。编译器作为寄存器的使用者自然知道在这两个场景中,分别有哪些值得保存的寄存器。 -从这一角度也可以理解为何要将函数调用上下文分成两类:可以在尽可能早的时候优化掉一些无用的寄存器保存与恢复。

-
-

调用规范 (Calling Convention) 约定在某个指令集架构上,某种编程语言的函数调用如何实现。它包括了以下内容:

-
    -
  1. 函数的输入参数和返回值如何传递;

  2. -
  3. 函数调用上下文中调用者/被调用者保存寄存器的划分;

  4. -
  5. 其他的在函数调用流程中对于寄存器的使用方法。

  6. -
-

调用规范是对于一种确定的编程语言来说的,因为一般意义上的函数调用只会在编程语言的内部进行。当一种语言想要调用用另一门编程语言编写的函数 -接口时,编译器就需要同时清楚两门语言的调用规范,并对寄存器的使用做出调整。

-
-

注解

-

RISC-V 架构上的 C 语言调用规范

-

RISC-V 架构上的 C 语言调用规范可以在 这里 找到。 -它对通用寄存器的使用做出了如下约定:

- - ----- - - - - - - - - - - - - - - - - - - - - -
RISC-V 寄存器功能分类

寄存器组

保存者

功能

a0~a7

调用者保存

用来传递输入参数。特别的 a0 和 a1 用来保存返回值。

t0~t6

调用者保存

作为临时寄存器使用,在函数中可以随意使用无需保存。

s0~s11

被调用者保存

作为临时寄存器使用,保存后才能在函数中使用。

-

剩下的 5 个通用寄存器情况如下:

-
    -
  • zero(x0) 之前提到过,它恒为零,函数调用不会对它产生影响;

  • -
  • ra(x1) 是调用者保存的,不过它并不会在每次调用子函数的时候都保存一次,而是在函数的开头和结尾保存/恢复即可,因为在执行期间即使被 -覆盖也没有关系。看上去和被调用者保存寄存器保存的位置一样,但是它确实是调用者保存的。

  • -
  • sp(x2) 是被调用者保存的。这个之后就会提到。

  • -
  • gp(x3) 和 tp(x4) 在一个程序运行期间都不会变化,因此不必放在函数调用上下文中。它们的用途在后面的章节会提到。

  • -
-

更加详细的内容可以参考 Cornell 的 课件

-
-

之前我们讨论了函数调用上下文的保存/恢复时机以及寄存器的选择,但我们并没有详细说明这些寄存器保存在哪里,只是用“内存中的一块区域”草草带过。实际上, -它更确切的名字是 (Stack) 。 sp(x2) 常用来保存 栈指针 (Stack Pointer),它是一个指向了内存中已经用过的位置的一个地址。在 -RISC-V 架构中,栈是从高地址到低地址增长的。在一个函数中,作为起始的开场白负责分配一块新的栈空间,其实它只需要知道需要空间的大小,然后将 sp -的值减小相应的字节数即可,于是物理地址区间 \([\text{新sp},\text{旧sp})\) 对应的物理内存便可以被这个函数用来函数调用上下文的保存/恢复 -以及其他工作,这块物理内存被称为这个函数的 栈帧 (Stackframe)。同理,函数中作为结尾的收场白负责将开场白分配的栈帧回收,这也仅仅需要 -将 sp 的值增加相同的字节数回到分配之前的状态。这也可以解释为什么 sp 是一个被调用者保存寄存器。

-
-../_images/CallStack.png -

函数调用与栈帧:如图所示,我们能够看到在程序依次调用 a、调用 b、调用 c、c 返回、b 返回整个过程中栈帧的分配/回收以及 sp 寄存器的变化。 -图中标有 a/b/c 的块分别代表函数 a/b/c 的栈帧。

-
-
-

注解

-

数据结构中的栈与实现函数调用所需要的栈

-

从数据结构的角度来看,栈是一个 后入先出 (Last In First Out, LIFO) 的线性表,支持向栈顶压入一个元素以及从栈顶弹出一个元素 -两种操作,分别被称为 push 和 pop。从它提供的接口来看,它只支持访问栈顶附近的元素。因此在实现的时候需要维护一个指向栈顶 -的指针来表示栈当前的状态。

-

我们这里的栈与数据结构中的栈原理相同,在很多方面可以一一对应。栈指针 sp 可以对应到指向栈顶的指针,对于栈帧的分配/回收可以分别 -对应到 push/pop 操作。如果将我们的栈看成一个内存分配器,它之所以可以这么简单,是因为它回收的内存一定是 最近一次分配 的内存, -从而只需要类似 push/pop 的两种操作即可。

-
-

在合适的编译选项设置之下,一个函数的栈帧内容可能如下图所示:

-
-../_images/StackFrame.png -

函数栈帧中的内容

-
-

它的开头和结尾分别在 sp(x2) 和 fp(s0) 所指向的地址。按照地址从高到低分别有以下内容,它们都是通过 sp 加上一个偏移量来访问的:

-
    -
  • ra 寄存器保存其返回之后的跳转地址,是一个调用者保存寄存器;

  • -
  • 父亲栈帧的结束地址 fp,是一个被调用者保存寄存器;

  • -
  • 其他被调用者保存寄存器 s1~s11;

  • -
  • 函数所使用到的局部变量。

  • -
-

因此,栈上实际上保存了一条完整的函数调用链,通过适当的方式我们可以实现对它的跟踪。

-

至此,我们基本上说明了函数调用是如何基于栈来实现的。不过我们可以暂时先忽略掉这些细节,因为我们现在只是需要在初始化阶段完成栈的设置,也就是 -设置好栈指针 sp 寄存器,后面的函数调用相关机制编译器会帮我们自动完成。麻烦的是, sp 的值也不能随便设置。至少我们需要保证它仍在物理内存上, -而且不能与程序的其他代码、数据段相交,因为在函数调用的过程中,栈区域里面的内容会被修改。如何保证这一点呢?此外,之前我们还提到我们编写的 -初始化代码必须放在物理地址 0x80020000 开头的内存上,这又如何做到呢?事实上,这两点都需要我们接下来讲到的程序内存布局的知识。

-
-
-

程序内存布局

-

在我们将源代码编译为可执行文件之后,它就会变成一个看似充满了杂乱无章的字节的一个文件。但我们知道这些字节至少可以分成代码和数据两部分,在 -程序运行起来的时候它们的功能并不相同:代码部分由一条条可以被 CPU 解码并执行的指令组成,而数据部分只是被 CPU 视作可用的存储空间。事实上 -我们还可以根据其功能进一步把两个部分划分为更小的单位: (Section) 。不同的段会被编译器放置在内存不同的位置上,这构成了程序的 -内存布局 (Memory Layout)。一种典型的程序相对内存布局如下:

-
-../_images/MemoryLayout.png -

一种典型的程序相对内存布局

-
-

代码部分只有代码段 .text 一个段,存放程序的所有汇编代码。

-

数据部分则还可以继续细化:

-
    -
  • 已初始化数据段保存程序中那些已初始化的全局数据,分为 .rodata.data 两部分。前者存放只读的全局数据,通常是一些常数或者是 -常量字符串等;而后者存放可修改的全局数据。

  • -
  • 未初始化数据段 .bss 保存程序中那些未初始化的全局数据,通常由程序的加载者代为进行零初始化,也即将这块区域逐字节清零;

  • -
  • (heap) 区域用来存放程序运行时动态分配的数据,如 C/C++ 中的 malloc/new 分配到的数据本体就放在堆区域,它向高地址增长;

  • -
  • 栈区域 stack 不仅用作函数调用上下文的保存与恢复,每个函数作用域内的局部变量也被编译器放在它的栈帧内。它向低地址增长。

  • -
-
-

注解

-

局部变量与全局变量

-

在一个函数的视角中,它能够访问的变量包括以下几种:

-
    -
  • 函数的输入参数和局部变量:保存在一些寄存器或是该函数的栈帧里面,如果是在栈帧里面的话是基于当前 sp 加上一个偏移量来访问的;

  • -
  • 全局变量:保存在数据段 .data.bss 中,某些情况下 gp(x3) 寄存器保存两个数据段中间的一个位置,于是全局变量是基于 -gp 加上一个偏移量来访问的。

  • -
  • 堆上的动态变量:本体被保存在堆上,大小在运行时才能确定。而我们只能 直接 访问栈上或者全局数据段中的 编译期确定大小 的变量。 -因此我们需要通过一个运行时分配内存得到的一个指向堆上数据的指针来访问它,指针的位宽确实在编译期就能够确定。该指针即可以作为局部变量 -放在栈帧里面,也可以作为全局变量放在全局数据段中。

  • -
-
-

我们可以将常说的编译流程细化为多个阶段(虽然输入一条命令便可将它们全部完成):

-
    -
  1. 编译器 (Compiler) 将每个源文件从某门高级编程语言转化为汇编语言,注意此时源文件仍然是一个 ASCII 或其他编码的文本文件;

  2. -
  3. 汇编器 (Assembler) 将上一步的每个源文件中的文本格式的指令转化为机器码,得到一个二进制的 目标文件 (Object File);

  4. -
  5. 链接器 (Linker) 将上一步得到的所有目标文件以及一些可能的外部目标文件链接在一起形成一个完整的可执行文件。

  6. -
-

每个目标文件都有着自己局部的内存布局,里面含有若干个段。在链接的时候,链接器会将这些内存布局合并起来形成一个整体的内存布局。此外,每个目标文件 -都有一个符号表,里面记录着它需要从其他文件中寻找的外部符号和能够提供给其他文件的符号,通常是一些函数和全局变量等。在链接的时候汇编器会将 -外部符号替换为实际的地址。

-

我们可以通过 链接脚本 (Linker Script) 调整链接器的行为,使得最终生成的可执行文件的内存布局符合我们的预期。

-
-
-

实现

-

我们自己编写运行时初始化的代码:

-
 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
# os/src/entry.asm
-    .section .text.entry
-    .globl _start
-_start:
-    la sp, boot_stack_top
-    call rust_main
-
-    .section .bss.stack
-    .globl boot_stack
-boot_stack:
-    .space 4096 * 16
-    .globl boot_stack_top
-boot_stack_top:
-
-
-

在这段汇编代码中,我们从第 8 行开始预留了一块大小为 4096 * 16 字节也就是 \(64\text{KiB}\) 的空间用作接下来要运行的程序的栈空间, -这块栈空间的栈顶地址被全局符号 boot_stack_top 标识,栈底则被全局符号 boot_stack 标识。同时,这块栈空间单独作为一个名为 -.bss.stack 的段,之后我们会通过链接脚本来安排它的位置。

-

从第 2 行开始,我们通过汇编代码实现执行环境的初始化,它其实只有两条指令:第一条指令将 sp 设置为我们预留的栈空间的栈顶位置,于是之后在函数 -调用的时候,栈就可以从这里开始向低地址增长了。简单起见,我们目前暂时不考虑 sp 越过了栈底 boot_stack ,也就是栈溢出的情形,虽然这有 -可能导致严重的错误。第二条指令则是通过伪指令 call 函数调用 rust_main ,这里的 rust_main 是一个我们稍后自己编写的应用 -入口。因此初始化任务非常简单:正如上面所说的一样,只需要设置栈指针 sp,随后跳转到应用入口即可。这两条指令单独作为一个名为 -.text.entry 的段,且全局符号 _start 给出了段内第一条指令的地址。

-

接着,我们在 main.rs 中嵌入这些汇编代码并声明应用入口 rust_main

-
 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
// os/src/main.rs
-#![no_std]
-#![no_main]
-#![feature(global_asm)]
-
-mod lang_items;
-
-global_asm!(include_str!("entry.asm"));
-
-#[no_mangle]
-pub fn rust_main() -> ! {
-    loop {}
-}
-
-
-

背景高亮指出了 main.rs 中新增的代码。

-

第 4 行中,我们手动设置 global_asm 特性来支持在 Rust 代码中嵌入全局汇编代码。第 8 行,我们首先通过 -include_str! 宏将同目录下的汇编代码 entry.asm 转化为字符串并通过 global_asm! 宏嵌入到代码中。

-

从第 10 行开始, -我们声明了应用的入口点 rust_main ,这里需要注意的是需要通过宏将 rust_main 标记为 #[no_mangle] 以避免编译器对它的 -名字进行混淆,不然的话在链接的时候, entry.asm 将找不到 main.rs 提供的外部符号 rust_main 从而导致链接失败。

-

我们修改 Cargo 的配置文件来使用我们自己的链接脚本 os/src/linker.ld 而非使用默认的内存布局:

-
1
-2
-3
-4
-5
-6
-7
-8
// os/.cargo/config
-[build]
-target = "riscv64gc-unknown-none-elf"
-
-[target.riscv64gc-unknown-none-elf]
-rustflags = [
-    "-Clink-arg=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes"
-]
-
-
-

具体的链接脚本 os/src/linker.ld 如下:

-
 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
-21
-22
-23
-24
-25
-26
-27
-28
-29
-30
-31
-32
-33
-34
-35
-36
-37
-38
-39
-40
-41
-42
-43
-44
-45
OUTPUT_ARCH(riscv)
-ENTRY(_start)
-BASE_ADDRESS = 0x80020000;
-
-SECTIONS
-{
-    . = BASE_ADDRESS;
-    skernel = .;
-
-    stext = .;
-    .text : {
-        *(.text.entry)
-        *(.text .text.*)
-    }
-
-    . = ALIGN(4K);
-    etext = .;
-    srodata = .;
-    .rodata : {
-        *(.rodata .rodata.*)
-    }
-
-    . = ALIGN(4K);
-    erodata = .;
-    sdata = .;
-    .data : {
-        *(.data .data.*)
-    }
-
-    . = ALIGN(4K);
-    edata = .;
-    .bss : {
-        *(.bss.stack)
-        sbss = .;
-        *(.bss .bss.*)
-    }
-
-    . = ALIGN(4K);
-    ebss = .;
-    ekernel = .;
-
-    /DISCARD/ : {
-        *(.eh_frame)
-    }
-}
-
-
-

第 1 行我们设置了目标平台为 riscv ;第 2 行我们设置了整个程序的入口点为之前定义的全局符号 _start; -第 3 行定义了一个常量 BASE_ADDRESS0x80020000 ,也就是我们之前提到的期望我们自己实现的初始化代码被放在的地址;

-

从第 5 行开始体现了链接过程中对输入的目标文件的段的合并。其中 . 表示当前地址,也就是链接器会从它指向的位置开始往下放置从输入的目标文件 -中收集来的段。我们可以对 . 进行赋值来调整接下来的段放在哪里,也可以创建一些全局符号赋值为 . 从而记录这一时刻的位置。我们还能够 -看到这样的格式:

-
.rodata : {
-    *(.rodata)
-}
-
-
-

冒号前面表示最终生成的可执行文件的一个段的名字,花括号内按照放置顺序描述将所有输入目标文件的哪些段放在这个段中,每一行格式为 -<ObjectFile>(SectionName),表示目标文件 ObjectFile 的名为 SectionName 的段需要被放进去。我们也可以 -使用通配符来书写 <ObjectFile><SectionName> 分别表示可能的输入目标文件和段名。因此,最终的合并结果是,在最终可执行文件 -中各个常见的段 .text, .rodata .data, .bss 从低地址到高地址按顺序放置,每个段里面都包括了所有输入目标文件的同名段, -且每个段都有两个全局符号给出了它的开始和结束地址(比如 .text 段的开始和结束地址分别是 stextetext )。

-

为了说明当前实现的正确性,我们需要讨论这样两个问题:

-
    -
  1. 如何做到执行环境的初始化代码被放在内存上以 0x80020000 开头的区域上?

    -
    -

    在链接脚本第 7 行,我们将当前地址设置为 BASE_ADDRESS 也即 0x80020000 ,然后从这里开始往高地址放置各个段。第一个被放置的 -是 .text ,而里面第一个被放置的又是来自 entry.asm 中的段 .text.entry,这个段恰恰是含有两条指令的执行环境初始化代码, -它在所有段中最早被放置在我们期望的 0x80020000 处。

    -
    -
  2. -
  3. 应用函数调用所需的栈放在哪里?

    -
    -

    从链接脚本第 32 行开始,我们可以看出 entry.asm 中分配的栈空间对应的段 .bss.stack 被放入到可执行文件中的 -.bss 段中的低地址中。在后面虽然有一个通配符 .bss.* ,但是由于链接脚本的优先匹配规则它并不会被匹配到后面去。 -这里需要注意的是地址区间 \([\text{sbss},\text{ebss})\) 并不包括栈空间,其原因后面再进行说明。

    -
    -
  4. -
-

这样一来,我们就将运行时重建完毕了。在 os 目录下 cargo build --release 或者直接 make build 就能够看到 -最终生成的可执行文件 target/riscv64gc-unknown-none-elf/release/os

-
- -
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/chapter1/4load-manually.html b/docs/chapter1/4load-manually.html deleted file mode 100644 index 6cebcc12dbc7229e6320127709c196db08a35284..0000000000000000000000000000000000000000 --- a/docs/chapter1/4load-manually.html +++ /dev/null @@ -1,614 +0,0 @@ - - - - - - - - - - 手动加载、运行应用程序 — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

手动加载、运行应用程序

-
-
-

在上一节中我们自己实现了一套运行时来代替标准库,并完整的构建了最终的可执行文件。但是它现在只是放在磁盘上的一个文件,若想将它运行起来的话, -就需要将它加载到内存中,在大多数情况下这是操作系统的任务。

-

让我们先来看看最终可执行文件的格式:

-
$ file os/target/riscv64gc-unknown-none-elf/release/os
-os/target/riscv64gc-unknown-none-elf/release/os: ELF 64-bit LSB executable,
-UCB RISC-V, version 1 (SYSV), statically linked, not stripped
-
-
-

从中可以看出可执行文件的格式为 可执行和链接格式 (Executable and Linkable Format, ELF),硬件平台是 RV64 。在 ELF 文件中, -除了程序必要的代码、数据段(它们本身都只是一些二进制的数据)之外,还有一些 元数据 (Metadata) 描述这些段在地址空间中的位置和在 -文件中的位置以及一些权限控制信息,这些元数据只能放在代码、数据段的外面。

-

我们可以通过二进制工具 readelf 来看看 ELF 文件中究竟包含什么内容,输入命令:

-
$ riscv64-unknown-elf-readelf os/target/riscv64gc-unknown-none-elf/release/os -a
-
-
-

首先可以看到一个 ELF header,它位于 ELF 文件的开头:

-
 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
ELF Header:
-Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
-Class:                             ELF64
-Data:                              2's complement, little endian
-Version:                           1 (current)
-OS/ABI:                            UNIX - System V
-ABI Version:                       0
-Type:                              EXEC (Executable file)
-Machine:                           RISC-V
-Version:                           0x1
-Entry point address:               0x80020000
-Start of program headers:          64 (bytes into file)
-Start of section headers:          9016 (bytes into file)
-Flags:                             0x1, RVC, soft-float ABI
-Size of this header:               64 (bytes)
-Size of program headers:           56 (bytes)
-Number of program headers:         3
-Size of section headers:           64 (bytes)
-Number of section headers:         8
-Section header string table index: 6
-
-
-
    -
  • 第 2 行是一个称之为 魔数 (Magic) 独特的常数,存放在 ELF header 的一个固定位置。当加载器将 ELF 文件加载到内存之前,通常会查看 -该位置的值是否正确,来快速确认被加载的文件是不是一个 ELF 。

  • -
  • 第 11 行给出了可执行文件的入口点为 0x80020000 ,这正是我们上一节所做的事情。

  • -
  • 从 12/13/17/19 行中,我们可以知道除了 ELF header 之外,还有另外两种不同的 header,分别称为 program header 和 section header, -它们都有多个。ELF header 中给出了三种 header 的大小、在文件中的位置以及数目。

  • -
-

一共有 3 个不同的 program header,它们从文件的 64 字节开始,每个 56 字节:

-
Program Headers:
-Type           Offset             VirtAddr           PhysAddr
-               FileSiz            MemSiz              Flags  Align
-LOAD           0x0000000000001000 0x0000000080020000 0x0000000080020000
-               0x000000000000001a 0x000000000000001a  R E    0x1000
-LOAD           0x0000000000002000 0x0000000080021000 0x0000000080021000
-               0x0000000000000000 0x0000000000010000  RW     0x1000
-GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
-               0x0000000000000000 0x0000000000000000  RW     0x0
-
-
-

每个 program header 指向一个在加载的时候可以连续加载的区域。

-

一共有 8 个不同的 section header,它们从文件的 9016 字节开始,每个 64 字节:

-
Section Headers:
-[Nr] Name              Type             Address           Offset
-      Size              EntSize          Flags  Link  Info  Align
-[ 0]                   NULL             0000000000000000  00000000
-      0000000000000000  0000000000000000           0     0     0
-[ 1] .text             PROGBITS         0000000080020000  00001000
-      000000000000001a  0000000000000000  AX       0     0     2
-[ 2] .bss              NOBITS           0000000080021000  00002000
-      0000000000010000  0000000000000000  WA       0     0     1
-[ 3] .riscv.attributes RISCV_ATTRIBUTE  0000000000000000  00002000
-      000000000000006a  0000000000000000           0     0     1
-[ 4] .comment          PROGBITS         0000000000000000  0000206a
-      0000000000000013  0000000000000001  MS       0     0     1
-[ 5] .symtab           SYMTAB           0000000000000000  00002080
-      00000000000001c8  0000000000000018           7     4     8
-[ 6] .shstrtab         STRTAB           0000000000000000  00002248
-      0000000000000041  0000000000000000           0     0     1
-[ 7] .strtab           STRTAB           0000000000000000  00002289
-      00000000000000ab  0000000000000000           0     0     1
-Key to Flags:
-W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
-L (link order), O (extra OS processing required), G (group), T (TLS),
-C (compressed), x (unknown), o (OS specific), E (exclude),
-p (processor specific)
-
-There are no section groups in this file.
-
-
-

每个 section header 则描述一个段的元数据。

-

其中,我们看到了代码段 .text 被放在可执行文件的 4096 字节处,大小 0x1a=26 字节,需要被加载到地址 0x80020000。 -它们分别由元数据的字段 Offset、 Size 和 Address 给出。同理,我们自己预留的应用程序函数调用栈在 .bss 段中,大小为 \(64\text{KiB}\) -,需要被加载到地址 0x80021000 处。我们没有看到 .data/.rodata 等段,因为目前的 rust_main 里面没有任何东西。

-

我们还能够看到 .symtab 段中给出的符号表:

-
Symbol table '.symtab' contains 19 entries:
-   Num:    Value          Size Type    Bind   Vis      Ndx Name
-   0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
-   1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS os.78wp4f2l-cgu.0
-   2: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS os.78wp4f2l-cgu.1
-   3: 0000000080020000     0 NOTYPE  LOCAL  DEFAULT    1 .Lpcrel_hi0
-   4: 0000000080020000     0 NOTYPE  GLOBAL DEFAULT    1 _start
-   5: 0000000080021000     0 NOTYPE  GLOBAL DEFAULT    2 boot_stack
-   6: 0000000080031000     0 NOTYPE  GLOBAL DEFAULT    2 boot_stack_top
-   7: 0000000080020010    10 FUNC    GLOBAL DEFAULT    1 rust_main
-   8: 0000000080020000     0 NOTYPE  GLOBAL DEFAULT  ABS BASE_ADDRESS
-   9: 0000000080020000     0 NOTYPE  GLOBAL DEFAULT    1 skernel
-   10: 0000000080020000     0 NOTYPE  GLOBAL DEFAULT    1 stext
-   11: 0000000080021000     0 NOTYPE  GLOBAL DEFAULT    1 etext
-   12: 0000000080021000     0 NOTYPE  GLOBAL DEFAULT    1 srodata
-   13: 0000000080021000     0 NOTYPE  GLOBAL DEFAULT    1 erodata
-   14: 0000000080021000     0 NOTYPE  GLOBAL DEFAULT    1 sdata
-   15: 0000000080021000     0 NOTYPE  GLOBAL DEFAULT    1 edata
-   16: 0000000080031000     0 NOTYPE  GLOBAL DEFAULT    2 sbss
-   17: 0000000080031000     0 NOTYPE  GLOBAL DEFAULT    2 ebss
-   18: 0000000080031000     0 NOTYPE  GLOBAL DEFAULT    2 ekernel
-
-
-

里面包括了栈顶、栈底、rust_main 的地址以及我们在 linker.ld 中定义的各个段开始和结束地址。

-

因此,从 ELF header 中可以看出,ELF 中的内容按顺序应该是:

-
    -
  • ELF header

  • -
  • 若干个 program header

  • -
  • 程序各个段的实际数据

  • -
  • 若干的 section header

  • -
-

当将程序加载到内存的时候,对于每个 program header 所指向的区域,我们需要将对应的数据从文件复制到内存中。这就需要解析 ELF 的元数据 -才能知道数据在文件中的位置以及即将被加载到内存中的位置。但目前,我们不需要从 ELF 中解析元数据就知道程序的内存布局 -(这个内存布局是我们按照需求自己指定的),我们可以手动完成加载任务。

-

具体的做法是利用 rust-objcopy 工具删除掉 ELF 文件中的 -所有 header 只保留各个段的实际数据得到一个没有任何符号的纯二进制镜像文件,由于缺少了必要的元数据,我们的二进制工具也没有办法 -对它完成解析了。而后,我们直接将这个二进制镜像文件手动载入到内存中合适位置即可。在这里,我们知道在镜像文件中,仍然是代码段 .text -作为起始,因此我们要将这个代码段载入到 0x80020000 才能和上一级 bootloader 对接上。因此,我们只要把整个镜像文件手动载入到 -内存的地址 0x80020000 处即可。在不同的硬件平台上,手动加载的方式是不同的。

-
-

qemu 平台

-

首先我们还原一下可执行文件和二进制镜像的生成流程:

-
# os/Makefile
-TARGET := riscv64gc-unknown-none-elf
-MODE := release
-KERNEL_ELF := target/$(TARGET)/$(MODE)/os
-KERNEL_BIN := $(KERNEL_ELF).bin
-
-$(KERNEL_BIN): kernel
-   @$(OBJCOPY) $(KERNEL_ELF) --strip-all -O binary $@
-
-kernel:
-   @cargo build --release
-
-
-

这里可以看出 KERNEL_ELF 保存最终可执行文件 os 的路径,而 KERNEL_BIN 保存只保留各个段数据的二进制镜像文件 os.bin -的路径。目标 kernel 直接通过 cargo build 以 release 模式最终可执行文件,目标 KERNEL_BIN 依赖于目标 kernel,将 -可执行文件通过 rust-objcopy 工具加上适当的配置移除所有的 header 和符号得到二进制镜像。

-

我们可以通过 make run 直接在 qemu 上运行我们的应用程序,qemu 是一个虚拟机,它完整的模拟了一整套硬件平台,就像是一台真正的计算机 -一样,我们来看运行 qemu 的具体命令:

-
 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
-21
-22
-23
KERNEL_ENTRY_PA := 0x80020000
-
-BOARD                ?= qemu
-SBI                  ?= rustsbi
-BOOTLOADER   := ../bootloader/$(SBI)-$(BOARD).bin
-
-run: run-inner
-
-run-inner: build
-ifeq ($(BOARD),qemu)
-   @qemu-system-riscv64 \
-      -machine virt \
-      -nographic \
-      -bios $(BOOTLOADER) \
-      -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)
-else
-   @cp $(BOOTLOADER) $(BOOTLOADER).copy
-   @dd if=$(KERNEL_BIN) of=$(BOOTLOADER).copy bs=128K seek=1
-   @mv $(BOOTLOADER).copy $(KERNEL_BIN)
-   @sudo chmod 777 $(K210-SERIALPORT)
-   python3 $(K210-BURNER) -p $(K210-SERIALPORT) -b 1500000 $(KERNEL_BIN)
-   miniterm --eol LF --dtr 0 --rts 0 --filter direct $(K210-SERIALPORT) 115200
-endif
-
-
-

注意其中高亮部分给出了传给 qemu 的参数。

-
    -
  • -machine 告诉 qemu 使用预设的硬件配置。在整个项目中我们将一直沿用该配置。

  • -
  • -bios 告诉 qemu 使用我们放在 bootloader 目录下的预编译版本作为 bootloader。

  • -
  • -device 则告诉 qemu 将二进制镜像加载到内存指定的位置。

  • -
-

可以先输入 Ctrl+A ,再输入 X 来退出 qemu 终端。

-
-

警告

-

FIXME: 使用 GDB 跟踪 qemu 的运行状态

-
-
-
-

k210 平台

-

对于 k210 平台来说,只需要将 maix 系列开发板通过数据线连接到 PC,然后 make run BOARD=k210 即可。从 Makefile 中来看:

-
 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
-14
-15
-16
-17
-18
K210-SERIALPORT      = /dev/ttyUSB0
-K210-BURNER          = ../tools/kflash.py
-
-run-inner: build
-ifeq ($(BOARD),qemu)
-   @qemu-system-riscv64 \
-      -machine virt \
-      -nographic \
-      -bios $(BOOTLOADER) \
-      -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)
-else
-   @cp $(BOOTLOADER) $(BOOTLOADER).copy
-   @dd if=$(KERNEL_BIN) of=$(BOOTLOADER).copy bs=128K seek=1
-   @mv $(BOOTLOADER).copy $(KERNEL_BIN)
-   @sudo chmod 777 $(K210-SERIALPORT)
-   python3 $(K210-BURNER) -p $(K210-SERIALPORT) -b 1500000 $(KERNEL_BIN)
-   miniterm --eol LF --dtr 0 --rts 0 --filter direct $(K210-SERIALPORT) 115200
-endif
-
-
-

在构建目标 run-inner 的时候,根据平台 BOARD 的不同,启动运行的指令也不同。当我们传入命令行参数 BOARD=k210 时,就会进入下面 -的分支。

-
    -
  • 第 13 行我们使用 dd 工具将 bootloader 和二进制镜像拼接到一起,这是因为 k210 平台的写入工具每次只支持写入一个文件,所以我们只能 -将二者合并到一起一并写入 k210 的内存上。这样的参数设置可以保证 bootloader 在合并后文件的开头,而二进制镜像在文件偏移量 0x20000 的 -位置处。有兴趣的读者可以输入命令 man dd 查看关于工具 dd 的更多信息。

  • -
  • 第 16 行我们使用烧写工具 K210-BURNER 将合并后的镜像烧写到 k210 开发板的内存的 0x80000000 地址上。 -参数 K210-SERIALPORT 表示当前 OS 识别到的 k210 开发板的串口设备名。在 Ubuntu 平台上一般为 /dev/ttyUSB0

  • -
  • 第 17 行我们打开串口终端和 k210 开发板进行通信,可以通过键盘向 k210 开发板发送字符并在屏幕上看到 k210 开发板的字符输出。

  • -
-

可以输入 Ctrl+] 退出 miniterm。

-
-
-

手动清空 .bss 段

-

由于 .bss 段需要在程序正式开始运行之前被固定初始化为零,因此在 ELF 文件中,为了节省磁盘空间,只会记录 .bss 段的位置而并不是 -有一块长度相等的全为零的数据。在内核将可执行文件加载到内存的时候,它需要负责将 .bss 所分配到的内存区域全部清零。而我们这里需要在 -应用程序 rust_main 中,在访问任何 .bss 段的全局数据之前手动将其清零。

-
 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
// os/src/main.rs
-fn clear_bss() {
-    extern "C" {
-        fn sbss();
-        fn ebss();
-    }
-    (sbss as usize..ebss as usize).for_each(|a| {
-        unsafe { (a as *mut u8).write_volatile(0) }
-    });
-}
-
-
-

在程序内自己进行清零的时候,我们就不用去解析 ELF(此时也没有 ELF 可供解析)了,而是通过链接脚本 linker.ld 中给出的全局符号 -sbssebss 来确定 .bss 段的位置。

-
-

注解

-

Rust 小知识:外部符号引用

-

extern “C” 可以引用一个外部的 C 函数接口(这意味着调用它的时候要遵从目标平台的 C 语言调用规范)。但我们这里只是引用位置标志 -并将其转成 usize 获取它的地址。由此可以知道 .bss 段两端的地址。

-

Rust 小知识:迭代器与闭包

-

代码第 7 行用到了 Rust 的迭代器与闭包的语法,它们在很多情况下能够提高开发效率。如读者感兴趣的话也可以将其改写为等价的 for -循环实现。

-
-
-

警告

-

Rust Unsafe

-

代码第 8 行,我们将 .bss 段内的一个地址转化为一个 裸指针 (Raw Pointer),并将它指向的值修改为 0。这在 C 语言中是 -一种司空见惯的操作,但在 Rust 中我们需要将他包裹在 unsafe 块中。这是因为,Rust 认为对于裸指针的 解引用 (Dereference) -是一种 unsafe 行为。

-

相比 C 语言,Rust 进行了更多的语义约束来保证安全性(内存安全/类型安全/并发安全),这在编译期和运行期都有所体现。但在某些时候, -尤其是与底层硬件打交道的时候,在 Rust 的语义约束之内没法满足我们的需求,这个时候我们就需要将超出了 Rust 语义约束的行为包裹 -在 unsafe 块中,告知编译器不需要对它进行完整的约束检查,而是由程序员自己负责保证它的安全性。当代码不能正常运行的时候,我们往往也是 -最先去检查 unsafe 块中的代码,因为它没有受到编译器的保护,出错的概率更大。

-

C 语言中的指针相当于 Rust 中的裸指针,它无所不能但又太过于灵活,程序员对其不谨慎的使用常常会引起很多内存不安全问题,最常见的如 -悬垂指针和多次回收的问题,Rust 编译器没法确认程序员对它的使用是否安全,因此将其划到 unsafe Rust 的领域。在 safe Rust 中,我们 -有引用 &/&mut 以及各种功能各异的智能指针 Box<T>/RefCell<T>/Rc<T> 可以使用,只要按照 Rust 的规则来使用它们便可借助 -编译器在编译期就解决很多潜在的内存不安全问题。

-
-
-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/chapter1/5sbi-print.html b/docs/chapter1/5sbi-print.html deleted file mode 100644 index 9180df3a6edbec5d2fce90d026c5ccd64ad1b590..0000000000000000000000000000000000000000 --- a/docs/chapter1/5sbi-print.html +++ /dev/null @@ -1,384 +0,0 @@ - - - - - - - - - - 格式化输出 — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

格式化输出

-
-
-

这一小节我们来自己实现 println! 的功能。 我们这里只是给出一些函数之间的调用关系,而不在这里进行一些实现细节上的展开。有兴趣的读者 -可以自行参考代码提供的注释。

-

在屏幕上打印一个字符是最基础的功能,它已经由 bootloader (也就是放在 bootloader 目录下的预编译版本)提供,具体的调用方法可以参考 -sbi.rs 中的 console_putchar 函数。

-

随后我们在 console.rs 中利用 console_putchar 来实现 print!println! 两个宏。有兴趣的读者可以去代码注释中 -参考有关 Rust core::fmt 库和宏编写的相关知识。在 main.rs 声明子模块 mod console 的时候加上 #[macro_use] 来让 -整个引用都可以使用到该模块里面定义的宏。

-

接着我们在 lang_items.rs 中修改 panic 时的行为:

-
// os/src/lang_items.rs
-use crate::sbi::shutdown;
-
-#[panic_handler]
-fn panic(info: &PanicInfo) -> ! {
-    if let Some(location) = info.location() {
-        println!("Panicked at {}:{} {}", location.file(), location.line(), info.message().unwrap());
-    } else {
-        println!("Panicked: {}", info.message().unwrap());
-    }
-    shutdown()
-}
-
-
-

我们尝试从传入的 PanicInfo 中解析 panic 发生的文件和行数。如果解析成功的话,就和 panic 的报错信息一起打印出来。我们需要在 -main.rs 开头加上 #![feature(panic_info_message)] 才能通过 PanicInfo::message 获取报错信息。

-
-

注解

-

Rust 小知识: 错误处理

-

Rust 中常利用 Option<T>Result<T, E> 进行方便的错误处理。它们都属于枚举结构:

-
    -
  • Option<T> 既可以有值 Option::Some<T> ,也有可能没有值 Option::None

  • -
  • Result<T, E> 既可以保存某个操作的返回值 Result::Ok<T> ,也可以表明操作过程中出现了错误 Result::Err<E>

  • -
-

我们可以使用 Option/Result 来保存一个不能确定存在/不存在或是成功/失败的值。之后可以通过匹配 if let 或是在能够确定 -的场合直接通过 unwrap 将里面的值取出。详细的内容可以参考 Rust 官方文档。

-
-

此外,我们还使用 bootloader 中提供的另一个接口 shutdown 关闭机器。

-

最终我们的应用程序 rust_main 如下:

-
// os/src/main.rs
-
-#[no_mangle]
-pub fn rust_main() -> ! {
-    extern "C" {
-        fn stext();
-        fn etext();
-        fn srodata();
-        fn erodata();
-        fn sdata();
-        fn edata();
-        fn sbss();
-        fn ebss();
-        fn boot_stack();
-        fn boot_stack_top();
-    };
-    clear_bss();
-    println!("Hello, world!");
-    println!(".text [{:#x}, {:#x})", stext as usize, etext as usize);
-    println!(".rodata [{:#x}, {:#x})", srodata as usize, erodata as usize);
-    println!(".data [{:#x}, {:#x})", sdata as usize, edata as usize);
-    println!("boot_stack [{:#x}, {:#x})", boot_stack as usize, boot_stack_top as usize);
-    println!(".bss [{:#x}, {:#x})", sbss as usize, ebss as usize);
-    panic!("Shutdown machine!");
-}
-
-
-

当我们在 qemu 平台上运行的时候能够看到如下的运行结果:

-
 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
[rustsbi] Version 0.1.0
-.______       __    __      _______.___________.  _______..______   __
-|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
-|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
-|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
-|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
-| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|
-
-[rustsbi] Platform: QEMU
-[rustsbi] misa: RV64ACDFIMSU
-[rustsbi] mideleg: 0x222
-[rustsbi] medeleg: 0xb109
-[rustsbi] Kernel entry: 0x80020000
-Hello, world!
-.text [0x80020000, 0x80022000)
-.rodata [0x80022000, 0x80023000)
-.data [0x80023000, 0x80023000)
-boot_stack [0x80023000, 0x80033000)
-.bss [0x80033000, 0x80033000)
-Panicked at src/main.rs:46 Shutdown machine!
-
-
-

其中前 13 行是 bootloader 的输出,剩下的部分是我们的应用程序的输出,打印了 Hello, world!,输出了程序内部各个段的地址区间, -还展示了 panic 相关信息。

-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/chapter1/6practice.html b/docs/chapter1/6practice.html deleted file mode 100644 index 1a5a3177e4d76aa89be056b41b490d978e4ae1b5..0000000000000000000000000000000000000000 --- a/docs/chapter1/6practice.html +++ /dev/null @@ -1,285 +0,0 @@ - - - - - - - - - - 练习一 — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

练习一

-
-
-
-

编程练习一:backtrace

-

仔细阅读 函数调用与栈 小节的内容,特别是函数栈帧中的 ra 寄存器和 prev fp 的位置以及它们的作用。

-

编程实现:在 rust_main 中多层嵌套调用函数,然后在最深层按照层数由深到浅打印函数调用链,也就是每一层函数栈帧中保存的 ra 寄存器的值, -由此我们可以依次知道每个函数的调用语句所在的地址,也就能跟踪一整条函数调用链。

-

拓展:寻找/改写 Rust 库或者自己实现,能够通过 ra 寄存器的值得到其所在的源文件/函数/行数,从而更直观的看到函数调用链。或者也可以通过 -addr2line 工具在运行结束之后手动去可执行文件中查找每个地址对应的信息。

-
-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/chapter1/index.html b/docs/chapter1/index.html deleted file mode 100644 index a58942b9e5b0835462d969039ad62e9cfc7dcc76..0000000000000000000000000000000000000000 --- a/docs/chapter1/index.html +++ /dev/null @@ -1,321 +0,0 @@ - - - - - - - - - - 第一章:RV64 裸机应用 — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

第一章:RV64 裸机应用

-
-
-

大多数程序员的第一行代码都从 Hello, world! 开始,当我们满怀着好奇心在编辑器内键入仅仅数个字节,再经过几行命令编译、运行,终于 -在黑洞洞的终端窗口中看到期望中的结果的时候,一扇通往编程世界的大门已经打开。时至今日,我们能够隐约意识到编程工作能够如此方便简洁并不是 -理所当然的,实际上有着多层硬件、软件隐藏在它背后,才让我们不必付出那么多努力就能够创造出功能强大的应用程序。

-

本章我们的目标仍然只是输出 Hello, world! ,但这一次,我们将离开舒适区,基于一个几乎空无一物的平台从零开始搭建我们自己的高楼大厦, -而不是仅仅通过一行语句就完成任务。

-

获取本章代码:

-
$ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
-$ cd rCore-Tutorial-v3
-$ git checkout ch1
-
-
-

在 qemu 模拟器上运行本章代码:

-
$ cd os
-$ make run
-
-
-

将 Maix 系列开发版连接到 PC,并在上面运行本章代码:

-
$ cd os
-$ make run BOARD=k210
-
-
-
-

警告

-

FIXME: 提供 wsl/macOS 等更多平台支持

-
-

如果顺利的话,以 qemu 平台为例,将输出:

-
[rustsbi] Version 0.1.0
-.______       __    __      _______.___________.  _______..______   __
-|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
-|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
-|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
-|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
-| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|
-
-[rustsbi] Platform: QEMU
-[rustsbi] misa: RV64ACDFIMSU
-[rustsbi] mideleg: 0x222
-[rustsbi] medeleg: 0xb109
-[rustsbi] Kernel entry: 0x80020000
-Hello, world!
-.text [0x80020000, 0x80022000)
-.rodata [0x80022000, 0x80023000)
-.data [0x80023000, 0x80023000)
-boot_stack [0x80023000, 0x80033000)
-.bss [0x80033000, 0x80033000)
-Panicked at src/main.rs:46 Shutdown machine!
-
-
-

除了 Hello, world! 之外还有一些额外的信息,最后关机。

-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/chapter2/1rv-privilege.html b/docs/chapter2/1rv-privilege.html deleted file mode 100644 index 8e26d86bea525e3a60e8989befa240794a432d6c..0000000000000000000000000000000000000000 --- a/docs/chapter2/1rv-privilege.html +++ /dev/null @@ -1,425 +0,0 @@ - - - - - - - - - - RISC-V 特权级架构 — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

RISC-V 特权级架构

-
-
-

为了保护我们的批处理系统不受到出错应用程序的影响并全程稳定工作,单凭软件实现是很难做到的,而是需要 CPU 提供一种特权级隔离机制,使得它在执行 -应用程序和内核代码的时候处于不同的特权级。特权级可以看成 CPU 随时间变化而处于的不同的工作模式。

-

RISC-V 架构中一共定义了 4 种特权级:

- - ----- - - - - - - - - - - - - - - - - - - - - - - - - -
RISC-V 特权级

级别

编码

名称

0

00

机器模式 (M, Machine)

1

01

监督模式 (S, Supervisor)

2

10

H, Hypervisor

3

11

用户/应用模式 (U, User/Application)

-

其中,级别的数值越小,特权级越高,掌控硬件的能力越强。从表中可以看出, M 模式处在最高的特权级,而 U 模式处于最低的特权级。

-

之前我们给出过支持应用程序运行的一套 执行环境栈 ,现在我们站在特权级架构的角度去重新看待它:

-../_images/PrivilegeStack.png -

和之前一样,白色块表示一层执行环境,黑色块表示相邻两层执行环境之间的接口。这张图片给出了能够支持运行 Unix 这类复杂系统的软件栈。其中 -内核代码运行在 S 模式上;应用程序运行在 U 模式上。运行在 M 模式上的软件被称为 监督模式执行环境 (SEE, Supervisor Execution Environment) -,这是站在运行在 S 模式上的软件的视角来看,它的下面也需要一层执行环境支撑,因此被命名为 SEE,它需要在相比 S 模式更高的特权级下运行, -一般情况下在 M 模式上运行。

-
-

注解

-

按需实现 RISC-V 特权级

-

RISC-V 架构中,只有 M 模式是必须实现的,剩下的特权级则可以根据跑在 CPU 上应用的实际需求进行调整:

-
    -
  • 简单的嵌入式应用只需要实现 M 模式;

  • -
  • 带有一定保护能力的嵌入式系统需要实现 M/U 模式;

  • -
  • 复杂的多任务系统则需要实现 M/S/U 模式。

  • -
-
-

之前我们提到过,执行环境的其中一种功能是在执行它支持的上层软件之前进行一些初始化工作。我们之前提到的引导加载程序会在加电后对整个系统进行 -初始化,它实际上是 SEE 功能的一部分,也就是说在 RISC-V 架构上引导加载程序一般运行在 M 模式上。此外,编程语言的标准库也会在执行程序员 -编写的逻辑之前进行一些初始化工作,但是在这张图中我们并没有将其展开,而是统一归类到 U 模式软件,也就是应用程序中。

-

执行环境的另一种功能是对上层软件的执行进行监控管理。监控管理可以理解为,当上层软件执行的时候出现了一些情况导致需要用到执行环境中提供的功能, -因此需要暂停上层软件的执行,转而运行执行环境的代码。由于上层软件和执行环境被设计为运行在不同的特权级,这个过程也往往(而 不一定 ) -伴随着 CPU 的 特权级切换 。当执行环境的代码运行结束后,我们需要回到上层软件暂停的位置继续执行。在 RISC-V 架构中,这种与常规控制流 -(顺序、循环、分支、函数调用)不同的 异常控制流 (ECF, Exception Control Flow) 被称为 陷入 (Trap) 。

-

触发 Trap 的原因总体上可以分为两种: 中断 (Interrupt) 和 异常 (Exception) 。本章我们只会用到异常,因此暂且略过中断。异常 -就是指上层软件需要执行环境功能的原因确切的与上层软件的 某一条指令的执行 相关。下表中我们给出了 RISC-V 特权级定义的一些异常:

- - ----- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
RISC-V 异常一览表

Interrupt

Exception Code

Description

0

0

Instruction address misaligned

0

1

Instruction access fault

0

2

Illegal instruction

0

3

Breakpoint

0

4

Load address misaligned

0

5

Load access fault

0

6

Store/AMO address misaligned

0

7

Store/AMO access fault

0

8

Environment call from U-mode

0

9

Environment call from S-mode

0

11

Environment call from M-mode

0

12

Instruction page fault

0

13

Load page fault

0

14

Store/AMO page fault

-

其中断点异常 (Breakpoint) 和执行环境调用 (Environment call) 两个异常是通过在上层软件中执行一条特定的指令触发的:当执行 ebreak -这条指令的之后就会触发断点异常;而执行 ecall 这条指令的时候则会随着 CPU 当前所处特权级而触发不同的异常。从表中可以看出,当 CPU 分别 -处于 M/S/U 三种特权级时执行 ecall 这条指令会触发三种异常。

-

在这里我们需要说明一下执行环境调用,这是一种很特殊的异常, 上图 中相邻两特权级软件之间的接口正是基于这种异常 -机制实现的。M 模式软件 SEE 和 S 模式的内核之间的接口被称为 监督模式二进制接口 (SBI, Supervisor Binary Interface),而内核和 -U 模式的应用程序之间的接口被称为 应用程序二进制接口 (Application Binary Interface),当然它有一个更加通俗的名字—— 系统调用 -(syscall, System Call) 。而之所以叫做二进制接口,是因为它和在同一种编程语言内部调用接口不同,是汇编指令级的一种接口。事实上 M/S/U -三个特权级的软件可能分别由不同的编程语言实现,即使是用同一种编程语言实现的,其调用也并不是普通的函数调用执行流,而是陷入,在该过程中有可能 -切换 CPU 特权级。因此只有将接口下降到汇编指令级才能够满足其通用性。

-

可以看到,在这样的架构之下,每层特权级的软件都只能做高特权级软件允许它做的、且对于高特权级软件不会产生什么撼动的事情,一旦超出了能力范围, -就必须寻求高特权级软件的帮助。因此,在一条执行流中我们经常能够看到特权级切换。如下图所示:

-../_images/EnvironmentCallFlow.png -

其他的异常则一般是在执行某一条指令的时候发生了错误,需要将控制转交给高特权级软件:当错误可恢复的时候,则处理错误并重新回到上层软件的执行; -否则,一般会将上层软件杀死以避免破坏执行环境。

-

第一章只是一个简单的嵌入式应用,它全程运行在 M 模式下。而在后续的章节中,我们会用到 M/S/U 三种特权级:其中我们的内核运行在 S 模式下 -(在本章表现为一个简单的批处理系统),应用程序运行在 U 特权级下,第一章提到的预编译的 bootloader 实际上是运行在 M 模式下的 SEE。 -整个系统就由这三层运行在不同特权级下的不同软件组成。在特权级相关机制方面,本书正文中我们重点关心 S/U 特权级, M 特权级的机制细节则 -是作为可选内容在附录 深入机器模式:RustSBI 中讲解,有兴趣的读者可以参考。

-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/chapter2/2application.html b/docs/chapter2/2application.html deleted file mode 100644 index 7ee34ad2b253ac31cbd23628acb90a3721d21ed8..0000000000000000000000000000000000000000 --- a/docs/chapter2/2application.html +++ /dev/null @@ -1,478 +0,0 @@ - - - - - - - - - - 实现应用程序 — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

实现应用程序

-
-
-

本节我们来实现被批处理系统逐个加载并运行的应用程序,它们是在认为自己会在 U 模式运行的前提下而设计、编写的,但实际上它们完全可能在其他特权级 -运行。事实上,保证应用程序的代码在 U 模式运行是我们接下来将实现的批处理系统的任务。

-

应用程序的实现放在项目根目录的 user 目录下,它和第一章的嵌入式应用不同之处在于以下几点。

-
-

项目结构

-

我们看到 user/src 目录下面多出了一个 bin 目录。bin 里面有多个文件,每个文件都是一个用户程序,目前里面有三个程序,分别是:

-
    -
  • 00hello_world:在屏幕上打印一行 Hello, world!

  • -
  • 01store_fault:访问一个非法的物理地址,测试批处理系统是否会被该错误影响;

  • -
  • 02power:一个略微复杂的、行为不断在计算和打印字符串间切换的程序。

  • -
-

批处理系统会按照文件名开头的编号从小到大的顺序加载并运行它们。

-

打开其中任意一个文件,会看到里面只有一个 main 函数,因此这很像是我们日常利用高级语言编程,只需要在单个文件中给出主逻辑的实现即可。

-

我们还能够看到代码中尝试引入了外部库:

-
#[macro_use]
-extern crate user_lib;
-
-
-

这个外部库其实就是 user 目录下的 lib.rs 以及它引用的若干子模块中。至于这个外部库为何叫 user_lib 而不叫 lib.rs -所在的目录的名字 user ,是因为在 user/Cargo.toml 中我们对于库的名字进行了设置: name =  "user_lib" 。它作为 -bin 目录下的源程序所依赖的用户库,等价于其他编程语言提供的标准库。

-

lib.rs 中我们定义了用户库的入口点 _start

-
1
-2
-3
-4
-5
-6
-7
#[no_mangle]
-#[link_section = ".text.entry"]
-pub extern "C" fn _start() -> ! {
-    clear_bss();
-    syscall::sys_exit(main());
-    panic!("unreachable after sys_exit!");
-}
-
-
-

第 2 行使用 Rust 的宏将 _start 这段代码编译后的汇编代码中放在一个名为 .text.entry 的代码段中,方便我们在后续链接的时候 -调整它的位置使得它能够作为用户库的入口。

-

而从第 4 行开始我们能够看到进入用户库入口之后,首先和第一章一样手动清空需要被零初始化 .bss 段(很遗憾到目前为止底层的批处理系统还 -没有这个能力,所以我们只能在用户库中完成),然后是调用 main 函数得到一个类型为 i32 的返回值,最后是使用接下来会提到的系统调用 -退出应用程序并将这个返回值告知批处理系统。

-

我们还在 lib.rs 中看到了另一个 main

-
1
-2
-3
-4
-5
#[linkage = "weak"]
-#[no_mangle]
-fn main() -> i32 {
-    panic!("Cannot find main!");
-}
-
-
-

第 1 行,我们使用 Rust 的宏将其函数符号 main 标志为弱链接。这样在最后链接的时候,虽然在 lib.rsbin 目录下的某个 -应用程序都有 main 符号,但由于 lib.rs 中的 main 符号是弱链接,链接器会使用 bin 目录下的应用主逻辑作为 main 。 -这里我们主要是进行某种程度上的保护,如果在 bin 目录下找不到任何 main ,那么编译也能够通过,并会在运行时报错。

-

为了上述这些链接操作,我们需要在 lib.rs 的开头加入:

-
#![feature(linkage)]
-
-
-
-
-

内存布局

-

user/.cargo/config 中,我们和第一章一样设置链接时使用链接脚本 user/src/linker.ld 。在其中我们做的重要的事情是:

-
    -
  • 将程序的起始物理地址调整为 0x80040000 ,三个应用程序都会被加载到这个物理地址上运行;

  • -
  • _start 所在的 .text.entry 放在整个程序的开头,也就是说批处理系统只要在加载之后跳转到 0x80040000 就已经进入了 -用户库的入口点,并会在初始化之后跳转到应用程序主逻辑;

  • -
  • 提供了最终生成可执行文件的 .bss 段的起始和终止地址,方便 clear_bss 函数使用。

  • -
-

其余的部分和第一章基本相同。

-
-
-

系统调用

-

在子模块 syscall 中我们作为应用程序来通过 ecall 调用批处理系统提供的接口,由于应用程序运行在 U 模式, ecall 指令会触发 -名为 Environment call from U-mode 的异常,并 Trap 进入 S 模式执行批处理系统针对这个异常特别提供的服务代码。由于这个接口处于 -S 模式的批处理系统和 U 模式的应用程序之间,从上一节我们可以知道,这个接口可以被称为 ABI 或者系统调用。现在我们不关心底层的批处理系统如何 -提供应用程序所需的功能,只是站在应用程序的角度去使用即可。

-

在本章中,应用程序和批处理系统之间约定如下两个系统调用:

-
-
系统调用一
-
/// 功能:将内存中缓冲区中的数据写入文件。
-/// 参数:`fd` 表示待写入文件的文件描述符;
-///      `buf` 表示内存中缓冲区的起始地址;
-///      `len` 表示内存中缓冲区的长度。
-/// 返回值:返回成功写入的长度。
-/// syscall ID:64
-fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize;
-
-/// 功能:退出应用程序并将返回值告知批处理系统。
-/// 参数:`xstate` 表示应用程序的返回值。
-/// 返回值:该系统调用不应该返回。
-/// syscall ID:93
-fn sys_exit(xstate: usize) -> !;
-
-
-
-

我们知道系统调用实际上是汇编指令级的二进制接口,因此这里给出的只是使用 Rust 语言描述的版本。在实际调用的时候,我们需要按照 RISC-V 调用 -规范在合适的寄存器中放置系统调用的参数,然后执行 ecall 指令触发 Trap。在 Trap 回到 U 模式的应用程序代码之后,会从 ecall 的 -下一条指令继续执行,同时我们能够按照调用规范在合适的寄存器中读取返回值。

-

在 RISC-V 调用规范中,和函数调用的情形类似,约定寄存器 a0~a6 保存系统调用的参数, a0~a1 保存系统调用的返回值。有些许不同的是 -寄存器 a7 用来传递 syscall ID,这是因为所有的 syscall 都是通过 ecall 指令触发的,除了各输入参数之外我们还额外需要一个寄存器 -来保存要请求哪个系统调用。由于这超出了 Rust 语言的表达能力,我们需要在代码中使用内嵌汇编来完成参数/返回值绑定和 ecall 指令的插入:

-
 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
fn syscall(id: usize, args: [usize; 3]) -> isize {
-    let mut ret: isize;
-    unsafe {
-        llvm_asm!("ecall"
-            : "={x10}" (ret)
-            : "{x10}" (args[0]), "{x11}" (args[1]), "{x12}" (args[2]), "{x17}" (id)
-            : "memory"
-            : "volatile"
-        );
-    }
-    ret
-}
-
-
-

第 1 行,我们将所有的系统调用都封装成 syscall 函数,可以看到它支持传入 syscall ID 和 3 个参数。

-

第 4 行开始,我们使用 Rust 提供的 llvm_asm! 宏在代码中内嵌汇编,在本行也给出了具体要插入的汇编指令,也就是 ecall,但这并不是 -全部,后面我们还需要进行一些相关设置。这个宏在 Rust 中还不稳定,因此我们需要在 lib.rs 开头加入 #![feature(llvm_asm)] 。 -此外,编译器无法判定插入汇编代码这个行为的安全性,所以我们需要将其包裹在 unsafe 块中自己来对它负责。

-

Rust 中的 llvm_asm! 宏的完整格式如下:

-
llvm_asm!(assembly template
-   : output operands
-   : input operands
-   : clobbers
-   : options
-);
-
-
-

下面逐行进行说明。

-

第 5 行指定输出操作数。这里由于我们的系统调用返回值只有一个 isize ,根据调用规范它会被保存在 a0 寄存器中。在双引号内,我们 -可以对于使用的操作数进行限制,由于是输出部分,限制的开头必须是一个 = 。我们可以在限制内使用一对花括号再加上一个寄存器的名字告诉 -编译器汇编的输出结果会保存在这个寄存器中。我们将声明出来用来保存系统调用返回值的变量 ret 包在一对普通括号里面放在操作数限制的 -后面,这样可以把变量和寄存器建立联系。于是,在系统调用返回之后我们就能在变量 ret 中看到返回值了。注意,变量 ret 必须为可变 -绑定,否则无法通过编译,这也说明在 unsafe 块内编译器还是会进行力所能及的安全检查。

-

第 6 行指定输入操作数。由于是输入部分,限制的开头不用加上 = 。同时在限制中设置使用寄存器 a0~a2 来保存系统调用的参数,以及 -寄存器 a7 保存 syscall ID ,而它们分别 syscall 的参数变量 argsid 绑定。

-

第 7 行用于告知编译器插入的汇编代码会造成的一些影响以防止编译器在不知情的情况下误优化。常用的使用方法是告知编译器某个寄存器在执行嵌入 -的汇编代码中的过程中会发生变化。我们这里则是告诉编译器在执行嵌入汇编代码中的时候会修改内存。这能给编译器提供更多信息。

-

第 8 行用于告知编译器将我们在程序中给出的嵌入汇编代码保持原样放到最终构建的可执行文件中。如果不这样做的话,编译器可能会把它和其他代码 -一视同仁并放在一起进行一些我们期望之外的优化。为了保证语义的正确性,一些比较关键的汇编代码需要加上该选项。

-

第一章中的输出到屏幕的操作也同样是使用内联汇编调用 SEE 提供的 SBI 接口来实现的。有兴趣的读者可以回顾第一章的 console.rs 和 -sbi.rs

-
-

注解

-

Rust 中的内联汇编

-

我们这里使用的 llvm_asm! 宏是将 Rust 底层 IR LLVM 中提供的内联汇编包装成的,更多信息可以参考 llvm_asm 文档

-

在未来的 Rust 版本推荐使用功能更加强大且方便易用的 asm! 宏,但是目前还未稳定,可以查看 inline-asm RFC 了解最新进展。

-
-

于是 sys_writesys_exit 只需将 syscall 进行包装:

-
 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
const SYSCALL_WRITE: usize = 64;
-const SYSCALL_EXIT: usize = 93;
-
-pub fn sys_write(fd: usize, buffer: &[u8]) -> isize {
-    syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()])
-}
-
-pub fn sys_exit(xstate: i32) -> isize {
-    syscall(SYSCALL_EXIT, [xstate as usize, 0, 0])
-}
-
-
-

注意 sys_write 使用一个 &[u8] 切片类型来描述缓冲区,这是一个 胖指针 (Fat Pointer),里面既包含缓冲区的起始地址,还 -包含缓冲区的长度。我们可以分别通过 as_ptrlen 方法取出它们并独立的作为实际的系统调用参数。

-

我们把 console 子模块中 Stdout::write_str 改成基于 sys_write 的实现,且传入的 fd 参数设置为 1,它代表标准输出, -也就是输出到屏幕。目前我们不需要考虑其他的 fd 选取情况。这样,应用程序的 println! 宏借助系统调用变得可用了。

-

sys_exit 则在用户库中的 _start 内使用,当应用程序主逻辑 main 返回之后,使用该系统调用退出应用程序并将返回值告知 -底层的批处理系统。

-
-
-

自动构建

-

这里简要介绍一下应用程序的自动构建。只需要在 user 目录下 make build 即可:

-
    -
  1. 对于 src/bin 下的每个应用程序,在 target/riscv64gc-unknown-none-elf/release 目录下生成一个同名的 ELF 可执行文件;

  2. -
  3. 使用 objcopy 二进制工具将上一步中生成的 ELF 文件删除所有 ELF header 和符号得到 .bin 后缀的纯二进制镜像文件。它们将被链接 -进内核并由内核在合适的时机加载到内存。

  4. -
-
-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/chapter2/3batch-system.html b/docs/chapter2/3batch-system.html deleted file mode 100644 index b975c064e7a4b906e7f5d009b1d96421aca21254..0000000000000000000000000000000000000000 --- a/docs/chapter2/3batch-system.html +++ /dev/null @@ -1,479 +0,0 @@ - - - - - - - - - - 实现批处理系统 — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

实现批处理系统

-
-
-
-

将应用程序链接到内核

-

在本章中,我们把应用程序的二进制镜像文件作为内核的数据段链接到内核里面,因此内核需要知道内含的应用程序的数量和它们的位置,这样才能够在运行时 -对它们进行管理并能够加载到物理内存。

-

os/src/main.rs 中能够找到这样一行:

-
global_asm!(include_str!("link_app.S"));
-
-
-

这里我们引入了一段汇编代码 link_app.S ,它一开始并不存在,而是在构建的时候自动生成的。当我们使用 make run 让系统成功运行起来 -之后,我们可以先来看一看里面的内容:

-
 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
-21
-22
-23
-24
-25
-26
-27
-28
-29
-30
-31
-32
# os/src/link_app.S
-
-    .align 4
-    .section .data
-    .global _num_app
-_num_app:
-    .quad 3
-    .quad app_0_start
-    .quad app_1_start
-    .quad app_2_start
-    .quad app_2_end
-
-    .section .data
-    .global app_0_start
-    .global app_0_end
-app_0_start:
-    .incbin "../user/target/riscv64gc-unknown-none-elf/release/00hello_world.bin"
-app_0_end:
-
-    .section .data
-    .global app_1_start
-    .global app_1_end
-app_1_start:
-    .incbin "../user/target/riscv64gc-unknown-none-elf/release/01store_fault.bin"
-app_1_end:
-
-    .section .data
-    .global app_2_start
-    .global app_2_end
-app_2_start:
-    .incbin "../user/target/riscv64gc-unknown-none-elf/release/02power.bin"
-app_2_end:
-
-
-

可以看到第 13 行开始的三个数据段分别插入了三个应用程序的二进制镜像,并且各自有一对全局符号 app_*_start, app_*_end 指示它们的 -开始和结束位置。而第 3 行开始的另一个数据段相当于一个 64 位整数数组。数组中的第一个元素表示应用程序的数量,后面则按照顺序放置每个应用 -程序的起始地址,最后一个元素放置最后一个应用程序的结束位置。这样每个应用程序的位置都能从该数组中相邻两个元素中得知。这个数组所在的位置 -同样也由全局符号 _num_app 所指示。

-

这个文件是在 cargo build 的时候,由脚本 os/build.rs 控制生成的。有兴趣的读者可以参考其代码。

-
-
-

应用管理器

-

我们在 osbatch 子模块中实现一个应用管理器,它的功能是:保存应用数量和各自的位置信息,以及当前执行到第几个应用了。结构体定义 -如下:

-
struct AppManager {
-    inner: RefCell<AppManagerInner>,
-}
-struct AppManagerInner {
-    num_app: usize,
-    current_app: usize,
-    app_start: [usize; MAX_APP_NUM + 1],
-}
-
-
-

这里我们可以看出,上面提到的应用管理器需要保存和维护的信息都在 AppManagerInner 里面,而结构体 AppManager 里面只是保存了 -一个指向 AppManagerInnerRefCell 智能指针。这样设计的原因在于:我们希望将 AppManager 实例化为一个全局变量使得 -任何函数都可以直接访问,但是里面的 current_app 字段表示当前执行到了第几个应用,它会在系统运行期间发生变化。因此在声明全局变量 -的时候一种自然的方法是利用 static mut。但是在 Rust 中,任何对于 static mut 变量的访问都是 unsafe 的,而我们要尽可能 -减少 unsafe 的使用来更多的让编译器负责安全性检查。

-

于是,我们利用 RefCell 来提供内部可变性,所谓的内部可变性就是指在我们只能拿到 AppManager 的不可变借用,意味着同样也只能 -拿到 AppManagerInner 的不可变借用的情况下依然可以修改 AppManagerInner 里面的字段。 -使用 RefCell::borrow/RefCell::borrow_mut 分别可以拿到 RefCell 里面内容的不可变借用/可变借用, -RefCell 内部会运行时维护当前已有的借用状态并进行借用检查。于是 RefCell::borrow_mut 就是我们实现内部可变性的关键。

-

我们这样初始化 AppManager 的全局实例:

-
lazy_static! {
-    static ref APP_MANAGER: AppManager = AppManager {
-        inner: RefCell::new({
-            extern "C" { fn _num_app(); }
-            let num_app_ptr = _num_app as usize as *const usize;
-            let num_app = unsafe { num_app_ptr.read_volatile() };
-            let mut app_start: [usize; MAX_APP_NUM + 1] = [0; MAX_APP_NUM + 1];
-            let app_start_raw: &[usize] = unsafe {
-                core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1)
-            };
-            app_start[..=num_app].copy_from_slice(app_start_raw);
-            AppManagerInner {
-                num_app,
-                current_app: 0,
-                app_start,
-            }
-        }),
-    };
-}
-
-
-

这里我们使用了外部库 lazy_static 提供的 lazy_static! 宏。要引入这个外部库,我们需要加入依赖:

-
# os/Cargo.toml
-
-[dependencies]
-lazy_static = { version = "1.4.0", features = ["spin_no_std"] }
-
-
-

lazy_static! 宏提供了全局变量的运行时初始化功能。一般情况下,全局变量必须在编译期设置一个初始值,但是有些全局变量依赖于运行期间 -才能得到的数据作为初始值。这导致这些全局变量需要在运行时发生变化,也即重新设置初始值之后才能使用。如果我们手动实现的话有诸多不便之处, -比如又需要把这种全局变量声明为 static mut 并衍生出很多 unsafe 。这种情况下我们可以使用 lazy_static! 宏来帮助我们解决 -这个问题。这里我们借助 lazy_static! 声明了一个名为 APP_MANAGERAppManager 全局实例,且只有在它第一次被使用到 -的时候才会实际进行初始化工作。

-

初始化的逻辑很简单,就是找到 link_app.S 中提供的符号 _num_app ,并从这里开始解析出应用数量以及各个应用的开头地址。注意其中 -对于切片类型的使用能够很大程度上简化编程。

-

因此,借助 Rust 核心库提供的 RefCell 和外部库 lazy_static!,我们就能在避免 static mut 声明的情况下以更加 Rust 的 -方式使用全局变量。

-

AppManagerInner 的方法中, print_app_info/get_current_app/move_to_next_app 都相当简单直接,需要说明的是 load_app

-
 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
-21
unsafe fn load_app(&self, app_id: usize) {
-    if app_id >= self.num_app {
-        panic!("All applications completed!");
-    }
-    println!("[kernel] Loading app_{}", app_id);
-    // clear icache
-    llvm_asm!("fence.i" :::: "volatile");
-    // clear app area
-    (APP_BASE_ADDRESS..APP_BASE_ADDRESS + APP_SIZE_LIMIT).for_each(|addr| {
-        (addr as *mut u8).write_volatile(0);
-    });
-    let app_src = core::slice::from_raw_parts(
-        self.app_start[app_id] as *const u8,
-        self.app_start[app_id + 1] - self.app_start[app_id]
-    );
-    let app_dst = core::slice::from_raw_parts_mut(
-        APP_BASE_ADDRESS as *mut u8,
-        app_src.len()
-    );
-    app_dst.copy_from_slice(app_src);
-}
-
-
-

这个方法负责将参数 app_id 对应的应用程序的二进制镜像加载到物理内存以 0x80040000 开头的位置,这个位置是批处理系统和应用程序 -之间约定的常数,回忆上一小节中,我们也调整应用程序的内存布局以同一个地址开头。第 8 行开始,我们首先将一块内存清空,然后找到待加载应用 -二进制镜像的位置,并将它复制到正确的位置。它本质上是数据从一块内存复制到另一块内存,从批处理系统的角度来看是将它数据段的一部分复制到了它 -程序之外未知的地方。

-

注意第 7 行我们插入了一条奇怪的汇编指令 fence.i ,它是用来清理 i-cache 的。我们知道缓存是存储层级结构中提高访存速度的很重要一环。 -而 CPU 对物理内存所做的缓存又分成 数据缓存 (d-cache) 和 指令缓存 (i-cache) 两部分,分别在 CPU 访存和取指的时候使用。在取指 -的时候,对于一个指令地址, CPU 会先去 i-cache 里面看一下它是否在某个已缓存的缓存行内,如果在的话它就会直接从高速缓存中拿到指令而不是通过 -总线和内存通信。通常情况下, CPU 会认为程序的代码段不会发生变化,因此 i-cache 是一种只读缓存。但在这里,我们会修改会被 CPU 取指的内存 -区域,这会使得 i-cache 中含有与内存中不一致的内容。因此我们这里必须使用 fence.i 指令手动清空 i-cache ,让里面所有的内容全部失效, -才能够保证正确性。

-

batch 子模块对外暴露出如下接口:

-
    -
  • init :调用 print_app_info 的时候第一次用到了全局变量 APP_MANAGER ,它也是在这个时候完成初始化;

  • -
  • run_next_app :批处理系统的核心操作,即加载并运行下一个应用程序。当批处理系统完成初始化或者一个应用程序运行结束或出错之后会调用 -该函数。我们下节再介绍其具体实现。

  • -
-
-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/chapter2/4trap-handling.html b/docs/chapter2/4trap-handling.html deleted file mode 100644 index 6911a221a664a2af833a0c7a1ee00fbe48350e44..0000000000000000000000000000000000000000 --- a/docs/chapter2/4trap-handling.html +++ /dev/null @@ -1,274 +0,0 @@ - - - - - - - - - - 处理 Trap — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

处理 Trap

-
-
-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/chapter2/index.html b/docs/chapter2/index.html deleted file mode 100644 index a541fad8ba003afff6785d71eb99fdc5ed405108..0000000000000000000000000000000000000000 --- a/docs/chapter2/index.html +++ /dev/null @@ -1,340 +0,0 @@ - - - - - - - - - - 第二章:批处理系统 — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

第二章:批处理系统

-
-
-

上一章,我们在 RV64 裸机平台上成功运行起来了 Hello, world! 。看起来这个过程非常顺利,只需要一条命令就能全部完成。但实际上,在那个 -计算机刚刚诞生的年代,很多事情并不像我们想象的那么简单。 当时,程序被记录在打孔的卡片上,使用汇编语言甚至机器语言来编写。而稀缺且昂贵的 -计算机由专业的管理员负责操作,就和我们在上一章所做的事情一样,他们手动将卡片输入计算机,等待程序运行结束或者终止程序的运行。最后,他们从 -计算机的输出端——也就是打印机中取出程序的输出并交给正在休息室等待的程序提交者。

-

实际上,这样做是一种对于珍贵的计算资源的浪费。因为当时的计算机和今天的个人计算机不同,它的体积极其庞大,能够占满一整个空调房间。管理员在 -房间的各个地方跑来跑去、或是等待打印机的输出的这些时间段,计算机都并没有在工作。于是,人们希望计算机能够不间断的工作且专注于计算任务本身。

-

批处理系统 (Batch System) 应运而生。它的核心思想是:将多个程序打包到一起输入计算机。而当一个程序运行结束后,计算机会 自动 加载 -下一个程序到内存并开始执行。这便是最早的真正意义上的操作系统。

-

程序总是难免出现错误。但人们希望一个程序的错误不要影响到操作系统本身,它只需要终止出错的程序,转而运行执行序列中的下一个程序即可。如果后面的 -程序都无法运行就太糟糕了。这种 保护 操作系统不受有意或无意出错的程序破坏的机制被称为 特权级 (Privilege) 机制,它实现了用户态和 -内核态的隔离,需要软件和硬件的共同努力。

-

本章我们的批处理系统将连续运行三个应用程序,放在 user/src/bin 目录下。

-

获取本章代码:

-
$ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
-$ cd rCore-Tutorial-v3
-$ git checkout ch2
-
-
-

在 qemu 模拟器上运行本章代码:

-
$ cd os
-$ make run
-
-
-

将 Maix 系列开发版连接到 PC,并在上面运行本章代码:

-
$ cd os
-$ make run BOARD=k210
-
-
-

如果顺利的话,我们可以看到批处理系统自动加载并运行所有的程序并且正确在程序出错的情况下保护了自身:

-
[rustsbi] Version 0.1.0
-.______       __    __      _______.___________.  _______..______   __
-|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
-|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
-|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
-|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
-| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|
-
-[rustsbi] Platform: QEMU
-[rustsbi] misa: RV64ACDFIMSU
-[rustsbi] mideleg: 0x222
-[rustsbi] medeleg: 0xb1ab
-[rustsbi] Kernel entry: 0x80020000
-[kernel] Hello, world!
-[kernel] num_app = 3
-[kernel] app_0 [0x8002b028, 0x8002c328)
-[kernel] app_1 [0x8002c328, 0x8002d6c0)
-[kernel] app_2 [0x8002d6c0, 0x8002eb98)
-[kernel] Loading app_0
-Hello, world!
-[kernel] Application exited with code 0
-[kernel] Loading app_1
-Into Test store_fault, we will insert an invalid store operation...
-Kernel should kill this application!
-[kernel] PageFault in application, core dumped.
-[kernel] Loading app_2
-3^10000=5079
-3^20000=8202
-3^30000=8824
-3^40000=5750
-3^50000=3824
-3^60000=8516
-3^70000=2510
-3^80000=9379
-3^90000=2621
-3^100000=2749
-Test power OK!
-[kernel] Application exited with code 0
-[kernel] Panicked at src/batch.rs:61 All applications completed!
-
-
-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/collaboration.html b/docs/collaboration.html deleted file mode 100644 index 13fbf4a5a1af9e725f1066d695ce0f55b6c861bb..0000000000000000000000000000000000000000 --- a/docs/collaboration.html +++ /dev/null @@ -1,274 +0,0 @@ - - - - - - - - - - 项目协作 — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

项目协作

-
-
-
    -
  1. 参考 这里 安装 Sphinx。

  2. -
  3. pip install sphinx_rtd_theme 安装 Read The Docs 主题。

  4. -
  5. pip install jieba 安装中文分词。

  6. -
  7. pip install sphinx-comments 安装 Sphinx 讨论区插件。

  8. -
  9. reStructuredText 基本语法 是 ReST 的一些基本语法,也可以参考已完成的文档。

  10. -
  11. 修改之后,在项目根目录下 make clean && make html 即可在 build/html/index.html 查看本地构建的主页。请注意在修改 -章节目录结构之后需要 make clean 一下,不然可能无法正常更新。

  12. -
  13. 确认修改无误之后,在项目根目录下 make deploy 然后即可 git add -A && git commit -m && git push 上传到远程仓库。 -如果出现冲突的话,请删除掉 docs 目录再进行 merge。

  14. -
-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/genindex.html b/docs/genindex.html deleted file mode 100644 index 466e0099b328b9f707220da457a4b52b5392cb41..0000000000000000000000000000000000000000 --- a/docs/genindex.html +++ /dev/null @@ -1,252 +0,0 @@ - - - - - - - - - - 索引 — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- -
    - -
  • »
  • - -
  • 索引
  • - - -
  • - - - -
  • - -
- - -
-
-
-
- - -

索引

- -
- -
- - -
- -
-
- - -
- -
-

- - © 版权所有 2020, Yifan Wu - -

-
- - - - Built with Sphinx using a - - theme - - provided by Read the Docs. - -
- -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index e08672ec0e5065efe741371fbbcbd8170d3a9a75..0000000000000000000000000000000000000000 --- a/docs/index.html +++ /dev/null @@ -1,274 +0,0 @@ - - - - - - - - - - rCore-Tutorial-Book 第三版 — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

rCore-Tutorial-Book 第三版

-
-
-
-
-
-
-

欢迎来到 rCore-Tutorial-Book 第三版!

-
-

读者须知

-

请先按照 快速上手 中的说明完成环境配置,再从第一章开始阅读正文。

-
-
-

项目协作

-

请参考 项目协作 了解如何进行项目协作。

-
-
- - -
- -
-
- - - - -
- -
-

- - © 版权所有 2020, Yifan Wu - -

-
- - - - Built with Sphinx using a - - theme - - provided by Read the Docs. - -
- -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/objects.inv b/docs/objects.inv deleted file mode 100644 index 73d11c5c2821400bba7724391ada7ed25bb2757f..0000000000000000000000000000000000000000 Binary files a/docs/objects.inv and /dev/null differ diff --git a/docs/quickstart.html b/docs/quickstart.html deleted file mode 100644 index 28849f5433a1fdd764a0854d00b229e49d1f4673..0000000000000000000000000000000000000000 --- a/docs/quickstart.html +++ /dev/null @@ -1,288 +0,0 @@ - - - - - - - - - - 快速上手 — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

快速上手

-
-
-

本节我们将完成环境配置并成功运行 rCore-Tutorial。

-

首先,请参考 环境部署 安装 qemu 模拟器 -和 rust。有一些小的变更如下:

-
    -
  • riscv64imac-unknown-none-elf 改成 riscv64gc-unknown-none-elf

  • -
  • 在使用文档中提供的链接下载 qemu 源码的时候,点击下载之后需要将链接中的 localhost 替换为 42.194.184.212:5212。若仍然 -不行的话,可以在 SiFive 官网 下载预编译的 qemu,比如 -Ubuntu 版本 qemu

  • -
-

此外:

-
    -
  • 下载安装 macOS 平台 -或 Ubuntu 平台 -的预编译版本 riscv64-unknown-elf-* 工具链,并添加到环境变量。可以在提示找不到的情况下再进行下载。

  • -
  • 下载安装 Linux 平台 预编译版本的 riscv64-linux-musl-* 工具链,并 -添加到环境变量。可以在提示找不到的情况下再进行下载。

  • -
  • 如果想在 Maix 系列开发板上运行,需要安装 python 包 pyserial 和串口终端 miniterm 。

  • -
-
-

警告

-

FIXME: 提供一套开箱即用的 Docker 环境

-
-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/rest-example.html b/docs/rest-example.html deleted file mode 100644 index a4c3a61e5cb4ac511396a269d0199d465b219aff..0000000000000000000000000000000000000000 --- a/docs/rest-example.html +++ /dev/null @@ -1,322 +0,0 @@ - - - - - - - - - - reStructuredText 基本语法 — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- - - - -
-
-
-
- -
-

reStructuredText 基本语法

-
-
-
-

注解

-

下面是一个注记。

-

这里 给出了在 Sphinx 中 -外部链接的引入方法。注意,链接的名字和用一对尖括号包裹起来的链接地址之间必须有一个空格。链接最后的下划线和片段的后续内容之间也需要 -有一个空格。

-

接下来是一个文档内部引用的例子。比如,戳 快速上手 可以进入快速上手环节。

-
-
-

警告

-

下面是一个警告。

-
-
一段示例 Rust 代码
-
1
-2
// 我们甚至可以插入一段 Rust 代码!
-fn add(a: i32, b: i32) -> i32 { a + b }
-
-
-
-

下面继续我们的警告。

-
-
-

错误

-

下面是一个错误。

-
-

这里是一行数学公式 \(\sin(\alpha+\beta)=\sin\alpha\cos\beta+\cos\alpha\sin\beta\)

-

基本的文本样式:这是 斜体 ,这是 加粗 ,接下来的则是行间公式 a0 。它们的前后都需要有一个空格隔开其他内容,这个让人挺不爽的…

-

这是 一个全面展示 -章节分布的例子,来自于 ReadTheDocs 的官方文档。事实上,现在我们也采用 ReadTheDocs 主题了,它非常美观大方。

-

下面是一个测试 gif。

-_images/test.gif -

接下来是一个表格的例子。

- - ---- - - - - - - - - - - - - - -
RISC-V 函数调用跳转指令

指令

指令功能

\(\text{jal}\ \text{rd},\ \text{imm}[20:1]\)

\(\text{rd}\leftarrow\text{pc}+4\)

-

\(\text{pc}\leftarrow\text{pc}+\text{imm}\)

-

\(\text{jalr}\ \text{rd},\ (\text{imm}[11:0])\text{rs}\)

\(\text{rd}\leftarrow\text{pc}+4\)

-

\(\text{pc}\leftarrow\text{rs}+\text{imm}\)

-
-
- - -
- -
- - -
-
- -
- -
- - - - - - - - - - - \ No newline at end of file diff --git a/docs/search.html b/docs/search.html deleted file mode 100644 index 15a1e11b5a3bffb2251708ac4fef9f6f345cc36e..0000000000000000000000000000000000000000 --- a/docs/search.html +++ /dev/null @@ -1,266 +0,0 @@ - - - - - - - - - - 搜索 — rCore-Tutorial-Book-v3 0.1 文档 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- -
- - - - - - - - - - - - - - - - - -
- -
    - -
  • »
  • - -
  • 搜索
  • - - -
  • - - - -
  • - -
- - -
-
-
-
- - - - -
- -
- -
- -
-
- - -
- -
-

- - © 版权所有 2020, Yifan Wu - -

-
- - - - Built with Sphinx using a - - theme - - provided by Read the Docs. - -
- -
-
- -
- -
- - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/searchindex.js b/docs/searchindex.js deleted file mode 100644 index 77fb263f7a3978988941d0924661df2334a353e8..0000000000000000000000000000000000000000 --- a/docs/searchindex.js +++ /dev/null @@ -1 +0,0 @@ -Search.setIndex({docnames:["appendix-a/index","appendix-b/index","appendix-c/index","chapter1/1app-ee-platform","chapter1/2remove-std","chapter1/3minimal-rt","chapter1/4load-manually","chapter1/5sbi-print","chapter1/6practice","chapter1/index","chapter2/1rv-privilege","chapter2/2application","chapter2/3batch-system","chapter2/4trap-handling","chapter2/index","collaboration","index","quickstart","rest-example"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":3,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":2,"sphinx.domains.rst":2,"sphinx.domains.std":1,sphinx:56},filenames:["appendix-a/index.rst","appendix-b/index.rst","appendix-c/index.rst","chapter1/1app-ee-platform.rst","chapter1/2remove-std.rst","chapter1/3minimal-rt.rst","chapter1/4load-manually.rst","chapter1/5sbi-print.rst","chapter1/6practice.rst","chapter1/index.rst","chapter2/1rv-privilege.rst","chapter2/2application.rst","chapter2/3batch-system.rst","chapter2/4trap-handling.rst","chapter2/index.rst","collaboration.rst","index.rst","quickstart.rst","rest-example.rst"],objects:{},objnames:{},objtypes:{},terms:{"&&":15,"--":[3,4,5,6,7,9,14],"---":[7,9,14],"----":[7,9,14],"----.":[7,9,14],".----":[7,9,14],"..":[6,12],"...":14,"._____":[7,9,14],".______":[7,9,14],"0.06":4,"0.1":[7,9,14],"00":[6,10],"00000000":6,"0000000000000000":6,"0000000000000001":6,"0000000000000013":6,"0000000000000018":6,"000000000000001a":6,"0000000000000041":6,"000000000000006a":6,"00000000000000ab":6,"00000000000001c8":6,"0000000000010000":6,"0000000080020000":6,"0000000080020010":6,"0000000080021000":6,"0000000080031000":6,"00001000":6,"00002000":6,"0000206a":6,"00002080":6,"00002248":6,"00002289":6,"00hello":[11,12],"00hello_world":[11,12],"01":[6,10],"01store":[11,12],"01store_fault":[11,12],"02":6,"02power":[11,12],"06":3,"06s":4,"09":3,"0x0":6,"0x0000000000000000":6,"0x000000000000001a":6,"0x0000000000001000":6,"0x0000000000002000":6,"0x0000000000010000":6,"0x0000000080020000":6,"0x0000000080021000":6,"0x1":6,"0x1000":6,"0x1a":6,"0x20000":6,"0x222":[7,9,14],"0x80000000":[5,6],"0x80020000":[5,6,7,9,14],"0x80021000":6,"0x80022000":[7,9],"0x80023000":[7,9],"0x8002b028":14,"0x8002c328":14,"0x8002d6c0":14,"0x8002eb98":14,"0x80033000":[7,9],"0x80040000":[11,12],"0xb109":[7,9],"0xb1ab":14,"1.15":3,"1.4":12,"1.48":3,"10":[5,6,10],"10000":14,"100000":14,"11":[3,5,6,10,18],"11.0":3,"115200":6,"12":[6,10],"128k":6,"13":[6,7,10,12],"14":[3,6,10],"15":6,"1500000":6,"15s":3,"16":[5,6],"17":6,"18":6,"184":17,"184.212":17,"19":6,"194":17,"20":[5,18],"20000":14,"2018spring":5,"2019spring":5,"2020":[3,5],"212":17,"2510":14,"26":6,"2621":14,"2749":14,"30000":14,"32":[3,5],"3824":14,"40000":14,"4096":[5,6],"42":17,"42.194":17,"45":6,"46":[6,7,9],"48":3,"4c":6,"4k":5,"50000":14,"5079":14,"5212":17,"56":6,"5750":14,"6.828":5,"60000":14,"61":14,"64":[3,4,5,6,11,12],"64i":3,"70000":14,"73dc675b9":3,"73dc675b9437c2a51a975a9f58cc66f05463c351":3,"777":6,"78wp4f2l":6,"7f":6,"80000":14,"8202":14,"828":5,"8516":14,"8824":14,"90000":14,"9016":6,"93":11,"9379":14,"\u4e00\u4e00":5,"\u4e00\u4e00\u5bf9":5,"\u4e00\u4e00\u5bf9\u5e94":5,"\u4e00\u4e0b":[3,4,5,6,10,11,12,15],"\u4e00\u4e2a":[3,4,5,6,7,9,10,11,12,14,18],"\u4e00\u4e9b":[3,4,5,6,7,9,10,11,15,17],"\u4e00\u4efd":[0,3],"\u4e00\u5171":[6,10],"\u4e00\u5207":3,"\u4e00\u53f0":6,"\u4e00\u5757":[5,6,12],"\u4e00\u5957":[3,5,6,10,17],"\u4e00\u5b9a":[5,10],"\u4e00\u5bf9":[5,11,12,18],"\u4e00\u5c42":[8,10],"\u4e00\u5e76":6,"\u4e00\u6247":9,"\u4e00\u6574":6,"\u4e00\u6574\u5957":6,"\u4e00\u65e6":10,"\u4e00\u6761":[3,5,10,11,12,14],"\u4e00\u6761\u6761":5,"\u4e00\u6765":5,"\u4e00\u6837":[3,4,5,6,10,11,14],"\u4e00\u6b21":[4,5,9,12],"\u4e00\u6b65":[5,11],"\u4e00\u6bb5":[5,12,18],"\u4e00\u70b9":5,"\u4e00\u7269":9,"\u4e00\u76f4":6,"\u4e00\u770b":12,"\u4e00\u79cd":[3,4,5,6,10,12,14],"\u4e00\u7ae0":[10,11,14,16],"\u4e00\u7ea7":6,"\u4e00\u81f4":[3,12],"\u4e00\u822c":[3,4,5,6,10,12],"\u4e00\u8282":[6,11],"\u4e00\u884c":[3,4,5,9,11,12,18],"\u4e00\u89c6\u540c\u4ec1":11,"\u4e00\u8d77":[5,6,7,11,14],"\u4e00\u90e8":[3,5,10,12],"\u4e00\u90e8\u5206":[3,5,10,12],"\u4e00\u957f\u4e32":3,"\u4e00\u95e8":[0,5],"\u4e00\u9879":3,"\u4e09\u4e2a":[5,10,11,12,14],"\u4e09\u5c42":10,"\u4e09\u65b9":[3,4],"\u4e09\u79cd":[3,6,10],"\u4e09\u8005":3,"\u4e0a\u4e0b":5,"\u4e0a\u4e0b\u6587":5,"\u4e0a\u4ee5":5,"\u4e0a\u53bb":[3,5],"\u4e0a\u5c42":[3,5,10],"\u4e0a\u79fb":4,"\u4e0a\u8ff0":[5,11],"\u4e0a\u9650":3,"\u4e0a\u9762":[3,5,9,12,14],"\u4e0b\u4ee5":12,"\u4e0b\u5212":18,"\u4e0b\u5212\u7ebf":18,"\u4e0b\u5c42":3,"\u4e0b\u6587":5,"\u4e0b\u6765":[4,5,11,18],"\u4e0b\u6807":5,"\u4e0b\u8282":12,"\u4e0b\u8868\u4e2d":10,"\u4e0b\u8bef":11,"\u4e0b\u8f7d":17,"\u4e0b\u8f7d\u5b89\u88c5":17,"\u4e0b\u964d":10,"\u4e0b\u9762":[3,5,6,10,11,18],"\u4e0d\u4ec5":[3,5],"\u4e0d\u4ec5\u4ec5":[3,5],"\u4e0d\u4f1a":[3,5,10,12],"\u4e0d\u4fbf":12,"\u4e0d\u5230":[3,4,5,11,17],"\u4e0d\u53d8":5,"\u4e0d\u53ef":[3,4,12],"\u4e0d\u540c":[3,4,5,6,10,11,14],"\u4e0d\u540c\u4e4b\u5904":11,"\u4e0d\u5f00":5,"\u4e0d\u5fc5":[4,5,9],"\u4e0d\u65ad":11,"\u4e0d\u662f":[3,4,5,6,9,10,11,12],"\u4e0d\u6b62":5,"\u4e0d\u7136":[5,15],"\u4e0d\u7136\u7684\u8bdd":5,"\u4e0d\u723d":18,"\u4e0d\u7528":[6,11],"\u4e0d\u77e5":11,"\u4e0d\u77e5\u60c5":11,"\u4e0d\u80fd":[4,5,6,7],"\u4e0d\u884c":17,"\u4e0d\u8981":[4,14],"\u4e0d\u8ba9":4,"\u4e0d\u8db3":[3,4],"\u4e0d\u8db3\u4ee5":4,"\u4e0d\u8fc7":5,"\u4e0d\u95f4\u65ad":14,"\u4e13\u4e1a":14,"\u4e13\u4e3a":3,"\u4e13\u6ce8":14,"\u4e14\u4f1a":4,"\u4e16\u754c":9,"\u4e1c\u897f":6,"\u4e22\u5931":5,"\u4e24\u4e2a":[4,5,7,10,11,12],"\u4e24\u5c42":[3,10],"\u4e24\u6761":5,"\u4e24\u6bb5":5,"\u4e24\u70b9":5,"\u4e24\u79cd":[5,6,10],"\u4e24\u7aef":6,"\u4e24\u7c7b":5,"\u4e24\u95e8":5,"\u4e25\u91cd":5,"\u4e27\u5931":3,"\u4e2a\u4eba":14,"\u4e2d\u5219":3,"\u4e2d\u592e":3,"\u4e2d\u5e38":7,"\u4e2d\u6587":15,"\u4e2d\u65ad":10,"\u4e2d\u662f":6,"\u4e2d\u6709":10,"\u4e2d\u672c":5,"\u4e2d\u6807":5,"\u4e2d\u6808":5,"\u4e2d\u95f4":[3,5],"\u4e2d\u95f4\u5c42":3,"\u4e32\u53e3":[6,17],"\u4e34\u65f6":5,"\u4e3a\u4e86":[3,5,6,10,11],"\u4e3a\u4ec0\u4e48":5,"\u4e3a\u4f55":[3,5,11],"\u4e3a\u4f8b":[5,9],"\u4e3a\u5f31":11,"\u4e3a\u6b62":[3,11],"\u4e3b\u7ebf":3,"\u4e3b\u8981":[3,11],"\u4e3b\u9875":15,"\u4e3b\u9898":[15,18],"\u4e49\u52a1":5,"\u4e4b\u4e0b":[5,10],"\u4e4b\u4e3a":[5,6],"\u4e4b\u5185":6,"\u4e4b\u524d":[3,4,5,6,10],"\u4e4b\u540e":[3,4,5,7,8,10,11,12,15,17],"\u4e4b\u5904":[3,11],"\u4e4b\u5916":[3,5,6,9,11,12],"\u4e4b\u6240\u4ee5":[3,5,10],"\u4e4b\u95f4":[3,7,10,11,12,18],"\u4e58\u9664":3,"\u4e5f\u5c31\u662f\u8bf4":[5,10,11],"\u4e60\u60ef":5,"\u4e66\u5199":5,"\u4e86\u89e3":[11,16],"\u4e8b\u5b9e":[3,4,5,10,11,18],"\u4e8b\u5b9e\u4e0a":[3,4,5,10,11,18],"\u4e8b\u60c5":[3,4,6,10,11,14],"\u4e8c\u6761":5,"\u4e8c\u8005":[5,6],"\u4e8c\u8fdb\u5236":[5,6,10,11,12],"\u4e8e\u662f":[3,4,5,11,12,14],"\u4e9b\u8bb8":11,"\u4ea4\u53c9":4,"\u4ea4\u7ed9":[10,14],"\u4ea4\u9053":[4,5,6],"\u4ea7\u751f":[4,5,10],"\u4eab\u53d7":3,"\u4eba\u4eec":14,"\u4eba\u8138":3,"\u4eba\u8138\u8bc6\u522b":3,"\u4ec0\u4e48":[3,4,5,6,10],"\u4ec5\u4ec5":[3,5,9],"\u4eca\u5929":14,"\u4eca\u65e5":9,"\u4ecb\u7ecd":[0,3,5,11,12],"\u4ecd\u7136":[5,6,9,17],"\u4ece\u4e2d":[5,6],"\u4ece\u5c0f":11,"\u4ece\u5c0f\u5230\u5927":11,"\u4ece\u6587\u4ef6":6,"\u4ece\u6808":5,"\u4ece\u800c":[5,8],"\u4ece\u8868\u4e2d":10,"\u4ece\u96f6\u5f00\u59cb":9,"\u4ed3\u5e93":15,"\u4ed4\u7ec6":8,"\u4ed4\u7ec6\u9605\u8bfb":8,"\u4ed6\u4eec":14,"\u4ed8\u51fa":9,"\u4ee3\u4e3a":5,"\u4ee3\u4ef7":3,"\u4ee3\u66ff":6,"\u4ee3\u7801":[3,4,5,6,7,9,10,11,12,14,18],"\u4ee3\u7801\u6267\u884c":5,"\u4ee3\u7801\u6bb5":[5,6,11,12],"\u4ee3\u7801\u8fd0\u884c":10,"\u4ee3\u8868":[4,5,11],"\u4ee5\u4e0b":[5,11],"\u4ee5\u4e0b\u5185\u5bb9":5,"\u4ee5\u4e0b\u51e0\u70b9":11,"\u4ee5\u53ca":[3,5,6,8,11,12],"\u4ee5\u8f83":3,"\u4efb\u4f55":[3,5,6,11,12],"\u4efb\u52a1":[3,5,6,9,10,11,14],"\u4efb\u610f":11,"\u4f11\u606f":14,"\u4f11\u606f\u5ba4":14,"\u4f18\u5148":5,"\u4f18\u5316":[5,11],"\u4f18\u70b9":3,"\u4f1a\u4ee5":4,"\u4f1a\u5148":12,"\u4f1a\u5e2e":5,"\u4f20\u5165":[6,7,11],"\u4f20\u5230":15,"\u4f20\u7ed9":6,"\u4f20\u9012":[5,11],"\u4f24\u7b4b\u52a8\u9aa8":4,"\u4f2a\u6307\u4ee4":5,"\u4f34\u968f":10,"\u4f46\u662f":[3,5,6,10,11,12],"\u4f4d\u4e8e":[3,5,6],"\u4f4d\u7f6e":[4,5,6,8,10,11,12],"\u4f53\u73b0":[5,6],"\u4f53\u79ef":14,"\u4f53\u7cfb":[3,5],"\u4f53\u7cfb\u7ed3\u6784":[3,5],"\u4f55\u5904":5,"\u4f5c\u4e3a":[3,4,5,6,10,11,12],"\u4f5c\u6570":11,"\u4f5c\u7528":[4,5,8],"\u4f5c\u7528\u57df":5,"\u4f5c\u8005":3,"\u4f5c\u8fc7":7,"\u4f7f\u5f97":[3,4,5,10,11,12],"\u4f7f\u7528":[3,4,5,6,7,11,12,14,17],"\u4f7f\u7528\u6743":5,"\u4f7f\u7528\u8005":5,"\u4f8b\u5b50":18,"\u4f9d\u6b21":[5,8],"\u4f9d\u7136":12,"\u4f9d\u8d56":[3,6,11,12],"\u4f9d\u8d56\u4e8e":[6,12],"\u4fbf\u662f":14,"\u4fdd\u5b58":[3,4,5,6,7,8,11,12],"\u4fdd\u62a4":[3,6,10,11,14],"\u4fdd\u6301":[3,5,11],"\u4fdd\u7559":[4,6],"\u4fdd\u8bc1":[5,6,11,12],"\u4fe1\u606f":[3,6,7,8,9,11,12],"\u4fee\u6539":[3,5,6,7,11,12,15],"\u501a\u4ed7":3,"\u501f\u52a9":[6,11,12],"\u501f\u7528":[4,12],"\u503c\u5f97":[0,5,8],"\u503c\u5f97\u4e00\u63d0\u7684\u662f":5,"\u5047\u5982":5,"\u5047\u8bbe":5,"\u504f\u79fb":[5,6],"\u504f\u79fb\u91cf":[5,6],"\u505a\u51fa":5,"\u505a\u5230":[5,10],"\u505a\u6cd5":6,"\u50cf\u662f":[6,11],"\u5141\u8bb8":10,"\u5143\u7d20":[5,12],"\u5145\u5f53":3,"\u5145\u6ee1":5,"\u5148\u51fa":5,"\u5165\u53e3":[4,5,6,11],"\u5168\u4e3a":6,"\u5168\u5c40":[5,6,12],"\u5168\u5c40\u53d8\u91cf":[5,12],"\u5168\u7a0b":[5,10],"\u5168\u90e8":[5,6,11,12,14],"\u5168\u9762":18,"\u516c\u5f0f":18,"\u5171\u540c":14,"\u5171\u540c\u52aa\u529b":14,"\u5173\u4e8e":6,"\u5173\u5fc3":[5,10,11],"\u5173\u673a":9,"\u5173\u7cfb":[5,7],"\u5173\u952e":[3,11,12],"\u5173\u95ed":7,"\u5174\u8da3":[3,4,6,7,10,11,12],"\u5176\u4e2d":[3,5,6,7,10,11,12],"\u5176\u4ed6":[3,5,10,11,18],"\u5176\u4f59":[3,11],"\u5176\u5212":6,"\u5176\u5b9e":[3,5,11],"\u5177\u4f53":[3,5,6,7,11,12],"\u5177\u6709":3,"\u5178\u578b":[4,5],"\u517c\u5bb9":3,"\u517c\u5bb9\u6027":3,"\u5185\u542b":12,"\u5185\u5b58":[3,6,12,14],"\u5185\u5bb9":[3,4,5,6,7,8,10,12,18],"\u5185\u5d4c":11,"\u5185\u6838":[3,5,6,10,11,14],"\u5185\u8054":11,"\u5185\u90e8":[4,5,7,10,12,18],"\u5192\u53f7":5,"\u5199\u5165":[6,11],"\u51b2\u7a81":15,"\u51cf\u5c0f":5,"\u51cf\u5c11":12,"\u51cf\u8f7b":3,"\u51e0\u4e2a":[3,12],"\u51e0\u4e4e":[3,9],"\u51e0\u70b9":11,"\u51e0\u79cd":5,"\u51e0\u884c":9,"\u51fa\u6765":[4,7,11],"\u51fa\u73b0":[3,4,5,7,10,14,15],"\u51fa\u9519":[4,6,10,12,14],"\u51fd\u6570":[3,6,7,8,10,11,12],"\u51fd\u6570\u5e93":3,"\u51fd\u6570\u8c03\u7528":[6,8,10,11],"\u5206\u4e3a":[3,5,10],"\u5206\u522b":[3,4,5,6,10,11,12],"\u5206\u5e03":18,"\u5206\u6210":[5,12],"\u5206\u652f":[5,6,10],"\u5206\u8bcd":15,"\u5206\u914d":[5,6],"\u5206\u914d\u5185\u5b58":5,"\u5206\u914d\u5668":5,"\u5207\u6362":[10,11],"\u5207\u7247":[11,12],"\u5212\u5206":[3,5],"\u5212\u7ebf":18,"\u521a\u521a":14,"\u521b\u5efa":[3,4,5],"\u521b\u9020":9,"\u521d\u59cb":[3,4,5,6,10,11,12],"\u521d\u59cb\u503c":12,"\u521d\u59cb\u5316":[3,4,5,6,10,11,12],"\u5220\u9664":[4,6,11,15],"\u5224\u5b9a":11,"\u5224\u65ad":3,"\u5229\u7528":[3,5,6,7,11,12],"\u524d\u4e3a":[3,11],"\u524d\u540e":[5,18],"\u524d\u63d0":11,"\u524d\u8005":[4,5],"\u524d\u9762":[4,5],"\u5269\u4e0b":[3,5,7,10],"\u526f\u4f5c\u7528":4,"\u529b\u6240\u80fd\u53ca":11,"\u529e\u6cd5":6,"\u529f\u80fd":[3,4,6,7,9,10,11,12,18],"\u529f\u80fd\u5f3a\u5927":[3,9],"\u52a0\u4e0a":[3,4,5,6,7,11],"\u52a0\u5165":[4,11,12],"\u52a0\u7535\u540e":[5,10],"\u52a0\u7c97":18,"\u52a0\u8f7d":[5,10,11,12,14],"\u52a8\u6001":5,"\u52a8\u6001\u5206\u914d":5,"\u52aa\u529b":[9,14],"\u5305\u542b":[3,6,11],"\u5305\u62ec":[3,4,5,6],"\u5305\u88b1":3,"\u5305\u88c5":11,"\u5305\u88f9":[6,11,18],"\u5316\u4e3a":12,"\u5339\u914d":[5,7],"\u533a\u57df":[5,6,12],"\u533a\u95f4":[5,7],"\u5341\u5206":3,"\u5347\u7ea7":3,"\u5347\u7ea7\u6362\u4ee3":3,"\u5355\u4e00":3,"\u5355\u4e2a":11,"\u5355\u4f4d":5,"\u5355\u5143":3,"\u5355\u51ed":10,"\u5355\u72ec":[3,5],"\u5355\u7eaf":[3,5],"\u5360\u6ee1":14,"\u5361\u7247":14,"\u5370\u673a":14,"\u5373\u4f7f":[5,10],"\u5373\u53ef":[3,4,5,6,11,14,15],"\u5373\u5c06":[5,6],"\u5373\u7528":17,"\u5382\u5546":3,"\u5386\u53f2":3,"\u538b\u5165":5,"\u538b\u6839":4,"\u538b\u7f29":3,"\u539f\u5148":4,"\u539f\u56e0":[4,5,10,12],"\u539f\u5730":4,"\u539f\u5b50":3,"\u539f\u6709":4,"\u539f\u6765":4,"\u539f\u6837":11,"\u539f\u7406":5,"\u53c2\u6570":[3,4,5,6,11,12],"\u53c2\u6570\u8bbe\u7f6e":[6,11],"\u53c2\u8003":[4,7,10,11,12,15,16,17],"\u53ca\u5176":3,"\u53cc\u5f15\u53f7":11,"\u53d1\u73b0":5,"\u53d1\u751f":[3,5,7,10,11,12],"\u53d1\u751f\u53d8\u5316":[5,11,12],"\u53d1\u9001":6,"\u53d6\u51fa":[7,11,14],"\u53d6\u6307":[5,12],"\u53d7\u5230":[3,6,10],"\u53d7\u9650":3,"\u53d8\u5316":[5,10,11,12],"\u53d8\u5f97":[5,11],"\u53d8\u6027":12,"\u53d8\u6210":5,"\u53d8\u66f4":17,"\u53d8\u91cf":[5,11,12,17],"\u53e6\u5916":[3,6],"\u53ea\u4f1a":[3,5,6,10],"\u53ea\u662f":[3,4,5,6,7,9,10,11,12],"\u53ea\u6709":[3,5,10,11,12],"\u53ea\u80fd":[5,6,10,11,12],"\u53ea\u8981":[6,11],"\u53ea\u8bfb":[5,12],"\u53eb\u505a":10,"\u53ef\u4ee5":[3,4,5,6,7,8,10,11,12,14,15,17,18],"\u53ef\u53d8":[11,12],"\u53ef\u53d8\u6027":12,"\u53ef\u60dc":4,"\u53ef\u6267\u884c\u6587\u4ef6":[3,4,5,6,8,11],"\u53ef\u7528":[5,11],"\u53ef\u80fd":[3,5,7,10,11,12,15],"\u53f8\u7a7a":6,"\u53f8\u7a7a\u89c1\u60ef":6,"\u5404\u4e2a":[5,6,7,12,14],"\u5404\u5f02":6,"\u5404\u79cd":[3,6],"\u5404\u7ea7":3,"\u5404\u81ea":12,"\u5408\u4f5c":5,"\u5408\u5e76":[5,6],"\u5408\u9002":[3,5,6,11],"\u540c\u4e00":[5,12],"\u540c\u4e00\u4e2a":[5,12],"\u540c\u4e8e":3,"\u540c\u4ec1":11,"\u540c\u540d":[5,11],"\u540c\u5b66":4,"\u540c\u65f6":[3,5,11],"\u540c\u6837":[5,11,12],"\u540c\u6b65":3,"\u540c\u7406":[5,6],"\u540d\u4e3a":[4,5,11,12],"\u540d\u5b57":[3,5,10,11,18],"\u540d\u79f0":10,"\u540e\u5165":5,"\u540e\u7eed":[3,4,10,11,18],"\u540e\u7f00":11,"\u540e\u8005":[4,5],"\u540e\u9762":[3,5,11,12,14],"\u5411\u4e0b":[4,5],"\u5426\u5219":[10,11],"\u542b\u4e49":[3,5],"\u542b\u6709":[5,12],"\u542f\u52a8":6,"\u544a\u77e5":[6,11],"\u544a\u8bc9":[3,4,6,11],"\u547d\u4ee4":[3,5,6,9,14],"\u547d\u4ee4\u884c":6,"\u547d\u540d":10,"\u54ea\u4e2a":[3,11],"\u54ea\u4e9b":[3,4,5],"\u54ea\u91cc":5,"\u552f\u4e00":5,"\u5668\u4f1a":[5,11],"\u56de\u5230":[5,10,11],"\u56de\u5fc6":[4,12],"\u56de\u6536":[5,6],"\u56de\u6765":5,"\u56de\u987e":11,"\u56e0\u4e3a":[3,5,6,10,11,14],"\u56e0\u6b64":[3,4,5,6,10,11,12],"\u56fa\u5b9a":[3,5,6],"\u56fa\u7136":4,"\u56fe\u4e2d":3,"\u56fe\u7247":10,"\u5728\u4e8e":[3,11,12],"\u5728\u5185":5,"\u5728\u5b50":[5,11],"\u5730\u5740":[5,6,7,8,11,12,18],"\u5730\u65b9":[3,5,12,14],"\u573a\u5408":7,"\u573a\u666f":[3,4,5],"\u57fa\u4e8e":[3,5,9,10,11],"\u57fa\u672c":[3,5,11,15],"\u57fa\u672c\u4e0a":[3,5],"\u57fa\u672c\u76f8\u540c":11,"\u57fa\u7840":7,"\u5806\u4e0a":5,"\u589e\u52a0":5,"\u589e\u957f":5,"\u58f0\u660e":[5,7,11,12],"\u5904\u4e8e":[5,10,11],"\u5904\u5728":10,"\u5904\u7406":[3,4,7,10,11],"\u5904\u7406\u51fd\u6570":4,"\u5904\u7406\u5355\u5143":3,"\u5904\u7406\u5668":3,"\u5904\u7406\u9519\u8bef":10,"\u590d\u5236":[6,12],"\u590d\u5236\u5230":[6,12],"\u590d\u672c":4,"\u590d\u6742":[3,5,10,11],"\u590d\u6742\u5316":3,"\u5916\u90e8":[5,6,11,12,18],"\u5916\u9762":6,"\u591a\u4e2a":[3,5,6,11,14],"\u591a\u4e48":5,"\u591a\u4efb\u52a1":10,"\u591a\u51fa":11,"\u591a\u5c42":[3,5,8,9],"\u591a\u6570":[3,5,6,9],"\u591a\u6b21":6,"\u591a\u79cd":[3,5],"\u591a\u79cd\u4e0d\u540c":5,"\u5927\u53a6":9,"\u5927\u591a":[3,5,6,9],"\u5927\u591a\u6570":[3,5,6,9],"\u5927\u5c0f":[5,6],"\u5927\u65b9":18,"\u5927\u6982":5,"\u5927\u81f4":4,"\u5927\u90e8":3,"\u5927\u90e8\u5206":3,"\u5927\u91cf":3,"\u5927\u95e8":9,"\u5931\u53bb":4,"\u5931\u6548":12,"\u5931\u8d25":[4,5,7],"\u5947\u602a":[3,12],"\u597d\u5947":9,"\u597d\u5947\u5fc3":9,"\u597d\u6808":5,"\u5982\u4e0b":[3,4,5,7,10,11,12,17],"\u5982\u4f55":[3,5,11,16],"\u5982\u56fe\u6240\u793a":5,"\u5982\u679c":[3,4,5,7,9,11,12,14,15,17],"\u5982\u6b64":[5,9],"\u5b50\u51fd\u6570":5,"\u5b50\u96c6":5,"\u5b57\u7b26":[5,6,7,11],"\u5b57\u7b26\u4e32":[5,11],"\u5b57\u8282":[5,6,9],"\u5b57\u8282\u6570":5,"\u5b58\u50a8":[5,12],"\u5b58\u50a8\u7a7a\u95f4":5,"\u5b58\u5728":[3,4,7,12],"\u5b58\u653e":[5,6],"\u5b58\u653e\u7a0b\u5e8f":5,"\u5b66\u4e60":0,"\u5b83\u4eec":[3,5,6,7,8,11,12,18],"\u5b83\u4f1a":[5,11,12],"\u5b83\u6052\u4e3a":5,"\u5b89\u5168":[6,11,12],"\u5b89\u5168\u6027":[6,11,12],"\u5b89\u5168\u68c0\u67e5":11,"\u5b89\u6392":5,"\u5b89\u88c5":[3,15,17],"\u5b8c\u5168":[3,11],"\u5b8c\u6210":[3,4,5,6,9,11,12,14,15,16,17],"\u5b8c\u6574":[3,5,6,11],"\u5b8c\u6bd5":5,"\u5b8f\u5728":11,"\u5b8f\u5c06":5,"\u5b8f\u662f":[4,11],"\u5b8f\u6765":[4,12],"\u5b98\u65b9":[7,18],"\u5b98\u7f51":17,"\u5b9a\u4e49":[5,6,7,10,11,12],"\u5b9a\u4f4d":3,"\u5b9a\u957f":5,"\u5b9e\u4f8b":12,"\u5b9e\u73b0":[3,4,6,7,8,10,14],"\u5b9e\u9645":[3,5,6,9,10,11,12,14],"\u5b9e\u9645\u4e0a":[3,5,9,10,11,14],"\u5bb9\u6027":3,"\u5bb9\u6613":[3,5],"\u5bc4\u5b58":[3,8,11],"\u5bc4\u5b58\u5668":[3,8,11],"\u5bf9\u4e8e":[3,4,5,6,10,11,12,14],"\u5bf9\u5916":[3,12],"\u5bf9\u5e94":[4,5,6,8,12],"\u5bf9\u6b64":5,"\u5bf9\u9f50":5,"\u5bfb\u627e":[5,8],"\u5bfb\u6c42":10,"\u5bfc\u81f4":[3,4,5,10,12],"\u5bfc\u81f4\u7cfb\u7edf":3,"\u5c01\u88c5":11,"\u5c06\u4f1a":5,"\u5c06\u5e38":5,"\u5c0f\u8282":[4,5,7,8,12],"\u5c11\u6570":3,"\u5c11\u91cf":3,"\u5c16\u62ec\u53f7":18,"\u5c1a\u672a":3,"\u5c1d\u8bd5":[3,4,7,11],"\u5c24\u5176":6,"\u5c31\u662f":[3,4,5,7,8,10,11,12,14],"\u5c31\u662f\u6307":[10,12],"\u5c31\u662f\u8bf4":[5,10,11],"\u5c3d\u53ef":[5,12],"\u5c3d\u53ef\u80fd":[5,12],"\u5c40\u90e8":5,"\u5c40\u90e8\u53d8\u91cf":5,"\u5c40\u9650":5,"\u5c40\u9650\u4e8e":5,"\u5c42\u5c42":5,"\u5c42\u6570":8,"\u5c42\u7ea7":12,"\u5c4f\u5e55":[3,6,7,11],"\u5c55\u5f00":[3,5,7,10],"\u5c55\u793a":[7,18],"\u5c5e\u4e8e":[3,7],"\u5d4c\u5165":[3,5,10,11],"\u5d4c\u5165\u5f0f":[3,10,11],"\u5d4c\u5165\u5f0f\u5e94\u7528":[10,11],"\u5d4c\u5957":[5,8],"\u5de5\u4f5c":[3,4,5,9,10,12,14],"\u5de5\u5177":[3,4,6,8,11,17],"\u5dee\u5f02":[3,4],"\u5df2\u6709":[3,12],"\u5df2\u7ecf":[3,5,7,9,11],"\u5e03\u5c40":[6,12],"\u5e0c\u671b":[3,5,12,14],"\u5e26\u6709":10,"\u5e2e\u52a9":[5,10,12],"\u5e38\u5e38":[3,4,5,6],"\u5e38\u6570":[5,6,12],"\u5e38\u7528":[3,5,11],"\u5e38\u89c1":[3,5,6],"\u5e38\u89c4":10,"\u5e38\u91cf":5,"\u5e73\u53f0":[4,5,7,9,14,17],"\u5e74\u4ee3":14,"\u5e76\u4e0d\u76f8\u540c":5,"\u5e76\u4e14":[5,12,14],"\u5e76\u4f1a":11,"\u5e76\u53d1":6,"\u5e76\u6253\u5370":4,"\u5e78\u8fd0":3,"\u5e7f\u6cdb":5,"\u5e8f\u5217":[5,14],"\u5e93\u4e2d":4,"\u5e93\u548c\u5b8f":7,"\u5e93\u662f":3,"\u5e94\u6709":3,"\u5e94\u7528":[4,5,7,10,14],"\u5e94\u7528\u7a0b\u5e8f":[3,4,5,7,9,10,14],"\u5e94\u8be5":[3,6,11],"\u5e94\u8fd0":14,"\u5e94\u8fd0\u800c\u751f":14,"\u5e95\u5c42":[3,6,11],"\u5e9e\u5927":14,"\u5efa\u7acb":[3,11],"\u5efa\u7acb\u8054\u7cfb":11,"\u5f00\u53d1":[3,6,9,14,17],"\u5f00\u53d1\u677f":[6,17],"\u5f00\u573a":5,"\u5f00\u573a\u767d":5,"\u5f00\u5934":[4,5,6,7,11,12],"\u5f00\u59cb":[3,4,5,6,9,11,12,14,16],"\u5f00\u59cb\u8fd0\u884c":6,"\u5f00\u7bb1":17,"\u5f02\u5e38":11,"\u5f15\u5165":[5,11,12,18],"\u5f15\u53f7":11,"\u5f15\u5bfc":[5,10],"\u5f15\u7528":[3,6,7,11,18],"\u5f15\u8d77":6,"\u5f20\u56fe":10,"\u5f31\u5316":4,"\u5f3a\u5927":[3,9,11],"\u5f52\u7c7b":10,"\u5f53\u4e2d":3,"\u5f53\u4e8e":[6,12],"\u5f53\u524d":[3,4,5,6,10,12],"\u5f53\u6210":5,"\u5f53\u65f6":14,"\u5f53\u7136":[3,4,9,10],"\u5f62\u5f0f":4,"\u5f62\u6210":5,"\u5f71\u54cd":[5,10,11,14],"\u5f80\u5f80":[6,10],"\u5f80\u9ad8":5,"\u5f88\u591a":[3,4,5,6,12,14],"\u5f88\u5927":[3,5,12],"\u5f88\u5feb":3,"\u5f97\u4ee5":[4,5],"\u5f97\u51fa":5,"\u5f97\u5230":[3,5,6,11,12],"\u5f97\u77e5":12,"\u5faa\u73af":[5,6,10],"\u5fc3\u601d":14,"\u5fc5\u4e0d\u53ef\u5c11":3,"\u5fc5\u8981":[3,5,6],"\u5fc5\u9700":3,"\u5fc5\u987b":[3,5,10,11,12,18],"\u5feb\u901f":[6,16,18],"\u5ffd\u7565":5,"\u600e\u6837":5,"\u601d\u60f3":14,"\u603b\u4f53":10,"\u603b\u662f":[5,14],"\u603b\u79f0":3,"\u603b\u7ebf":12,"\u603b\u7ed3":5,"\u6052\u4e3a":5,"\u6062\u590d":[4,5,10],"\u6070\u6070":5,"\u60ac\u5782":6,"\u60c5\u51b5":[3,4,5,6,10,11,12,14,17],"\u60c5\u5f62":[5,11],"\u60f3\u8981":5,"\u60f3\u8c61":[5,14],"\u610f\u4e49":[3,4,5,14],"\u610f\u5473":[3,6,12],"\u610f\u5473\u7740":[3,6,12],"\u610f\u8bc6":[4,9],"\u611f\u5174":6,"\u611f\u5174\u8da3":6,"\u6210\u529f":[4,7,11,12,14,17],"\u6210\u7acb":5,"\u6211\u4eec":[3,4,5,6,7,8,9,10,11,12,14,17,18],"\u6216\u662f":[3,5,7,14],"\u6216\u79f0":5,"\u6216\u8005":[3,4,5,8,11,12,14],"\u623f\u95f4":14,"\u6240\u4ee5":[3,5,6,10,11],"\u6240\u5728":[4,8,11,12],"\u6240\u5904":3,"\u6240\u6709":[3,5,6,11,12,14],"\u6240\u793a":[3,5,10],"\u6240\u80fd":11,"\u6240\u8bf4":5,"\u6240\u8c13":[4,12],"\u6240\u9009":3,"\u624b\u4e2d":5,"\u624b\u5199":3,"\u624b\u52a8":[4,5,8,11,12,14],"\u624d\u80fd":[3,5,6,7,12],"\u6253\u4ea4\u9053":[4,5,6],"\u6253\u5305":14,"\u6253\u5361":3,"\u6253\u5370":[3,4,7,8,11,14],"\u6253\u5370\u673a":14,"\u6253\u5b54":14,"\u6253\u5f00":[6,9,11],"\u6267\u884c":[3,4,5,6,8,10,11,12,14],"\u6279\u5904\u7406":[10,11],"\u627e\u5230":[4,5,12],"\u627f\u62c5":5,"\u62a5\u51fa":4,"\u62a5\u9519":7,"\u62bd\u8c61":3,"\u62d3\u5c55":[3,5,8],"\u62ec\u53f7":[5,11,18],"\u62fc\u63a5":6,"\u62ff\u5230":12,"\u6301\u7eed":3,"\u6307\u4ee4":[3,6,10,11,12],"\u6307\u4ee4\u96c6":[3,5],"\u6307\u51fa":[3,5],"\u6307\u5411":[5,6,12],"\u6307\u5b9a":[5,6,11],"\u6307\u793a":12,"\u6307\u9488":[5,6,11,12],"\u6309\u7167":[3,5,6,8,11,12,16],"\u6323\u624e":3,"\u6362\u4ee3":3,"\u6362\u56de":4,"\u6362\u6210":3,"\u638c\u63a7":10,"\u63a5\u4e0a":6,"\u63a5\u4e0b":[4,5,11,18],"\u63a5\u4e0b\u6765":[4,5,11,18],"\u63a5\u53e3":[3,5,6,7,10,11,12],"\u63a5\u7740":[5,7],"\u63a7\u5236":[3,5,6,10,12],"\u63a7\u5236\u6743":5,"\u63a7\u5236\u6d41":[5,10],"\u63a7\u5236\u76ee\u6807":3,"\u63a7\u5236\u7ed3\u6784":5,"\u63a8\u8350":11,"\u63cf\u8ff0":[3,5,6,11],"\u63cf\u8ff0\u7b26":11,"\u63d0\u4ea4":14,"\u63d0\u4ea4\u8005":14,"\u63d0\u4f9b":[3,5,7,9,10,11,12,17],"\u63d0\u5230":[3,4,5,10,11,12],"\u63d0\u793a":17,"\u63d0\u9192":4,"\u63d0\u9ad8":[6,12],"\u63d2\u4ef6":15,"\u63d2\u5165":[4,5,11,12,18],"\u642d\u5efa":9,"\u64bc\u52a8":10,"\u64cd\u4f5c":[3,4,5,6,7,11,12,14],"\u64cd\u4f5c\u6570":11,"\u64cd\u4f5c\u7cfb\u7edf":[3,4,5,6,14],"\u64cd\u4f5c\u8fc7\u7a0b":7,"\u652f\u6301":[5,6,9,10,11],"\u652f\u6491":[5,10],"\u6536\u573a":5,"\u6536\u96c6":5,"\u6539\u5199":[6,8],"\u6539\u6210":[11,17],"\u653e\u5165":5,"\u653e\u5230":11,"\u653e\u5728":[5,6,7,11,14],"\u653e\u7a0b\u5e8f":5,"\u653e\u7f6e":[5,11,12],"\u653e\u8fdb":5,"\u653e\u8fdb\u53bb":5,"\u6548\u7387":6,"\u6559\u5b66":3,"\u6570\u4e2a":9,"\u6570\u503c":10,"\u6570\u5b66":18,"\u6570\u5b66\u516c\u5f0f":18,"\u6570\u636e":[3,5,6,11,12],"\u6570\u636e\u7ebf":6,"\u6570\u636e\u7ed3\u6784":5,"\u6570\u76ee":6,"\u6570\u7ec4":[5,12],"\u6570\u91cf":12,"\u6574\u4e2a":[5,6,7,10,11,14],"\u6574\u4f53":5,"\u6574\u5957":6,"\u6574\u6570":[3,12],"\u6574\u6761":8,"\u6587\u4ef6":[3,4,5,6,7,8,11,12],"\u6587\u4ef6\u540d":11,"\u6587\u672c":[5,18],"\u6587\u672c\u6587\u4ef6":5,"\u6587\u672c\u683c\u5f0f":5,"\u6587\u6863":[3,7,11,15,17,18],"\u659c\u4f53":18,"\u65ad\u70b9":10,"\u65ad\u8a00":4,"\u65b0\u589e":5,"\u65b0\u5efa":4,"\u65b0\u5f00":0,"\u65b0\u7248":3,"\u65b0\u751f":3,"\u65b0\u8fdb\u5c55":11,"\u65b9\u4fbf":[3,7,9,11],"\u65b9\u5f0f":[3,4,5,6,12],"\u65b9\u6848":[3,4],"\u65b9\u6cd5":[4,5,7,11,12,18],"\u65b9\u9762":[3,5,10],"\u65e0\u4e00\u7269":9,"\u65e0\u610f":14,"\u65e0\u6240":6,"\u65e0\u6240\u4e0d\u80fd":6,"\u65e0\u6cd5":[3,4,11,14,15],"\u65e0\u7528":5,"\u65e0\u7591":[3,5],"\u65e0\u8bba":[3,5],"\u65e0\u8bba\u662f":5,"\u65e0\u8bef":15,"\u65e0\u9700":[3,5],"\u65e2\u6709":5,"\u65e5\u5e38":[5,11],"\u65e7\u7248":3,"\u65f6\u5019":[3,4,5,6,7,9,10,11,12,17],"\u65f6\u523b":5,"\u65f6\u62a5":11,"\u65f6\u673a":[5,11],"\u65f6\u6765":6,"\u65f6\u81f3\u4eca\u65e5":9,"\u65f6\u95f4":[3,10,14],"\u65f6\u95f4\u6bb5":14,"\u6602\u8d35":14,"\u660e\u786e":5,"\u6613\u7528":11,"\u6613\u89c1":3,"\u662f\u4e0d\u662f":6,"\u662f\u4ece":5,"\u662f\u5426":[6,11,12],"\u662f\u56e0\u4e3a":[3,5,6,10,11],"\u663e\u5f97":5,"\u663e\u7136":5,"\u663e\u800c":3,"\u663e\u800c\u6613\u89c1":3,"\u666e\u901a":[10,11],"\u666e\u904d":3,"\u667a\u80fd":[6,12],"\u6682\u4e14":[3,10],"\u6682\u505c":10,"\u6682\u65f6":5,"\u66b4\u9732":12,"\u66b4\u9732\u51fa":12,"\u66f4\u4e3a":5,"\u66f4\u52a0":[5,10,11,12],"\u66f4\u5927":[3,6],"\u66f4\u5f3a":3,"\u66f4\u65b0":[3,15],"\u66fe\u7ecf":3,"\u66ff\u6362":[5,17],"\u6700\u4e0a":3,"\u6700\u4e0a\u5c42":3,"\u6700\u4f4e":[3,10],"\u6700\u5148":6,"\u6700\u540e":[9,11,12,14,18],"\u6700\u65b0":[3,11],"\u6700\u65b0\u7248":3,"\u6700\u65b0\u8fdb\u5c55":11,"\u6700\u65e9":[5,14],"\u6700\u7ec8":[3,4,5,6,7,11],"\u6700\u8fd1":5,"\u6700\u9ad8":[3,10],"\u6709\u4e9b":12,"\u6709\u503c":7,"\u6709\u5173":7,"\u6709\u610f":14,"\u6709\u6240":6,"\u6709\u7740":[3,5,9],"\u670d\u52a1":11,"\u671f\u671b":[5,9,11],"\u671f\u95f4":[5,12],"\u672a\u6765":11,"\u672a\u77e5":12,"\u672c\u4e66":[5,10],"\u672c\u4f53":5,"\u672c\u5730":15,"\u672c\u6587":5,"\u672c\u76f8":11,"\u672c\u7ae0":[9,10,11,12,14],"\u672c\u884c":11,"\u672c\u8d28":12,"\u672c\u8eab":[6,14],"\u673a\u5236":[3,5,10,14],"\u673a\u5668":[5,7,10,14],"\u673a\u5668\u7801":5,"\u673a\u5668\u8bed\u8a00":14,"\u6740\u6b7b":[4,10],"\u6742\u4e71":5,"\u6742\u4e71\u65e0\u7ae0":5,"\u6743\u8861":3,"\u6743\u9650":6,"\u6761\u4ef6":4,"\u6761\u6761":5,"\u6765\u5230":16,"\u6765\u770b":[3,5,6,10,12],"\u6765\u81ea":[5,18],"\u6765\u8bf4":[5,6],"\u6781\u5176":14,"\u6784\u5efa":[3,4,6,12,15],"\u6784\u6210":5,"\u6784\u9020":[3,5],"\u679a\u4e3e":7,"\u67b6\u6784":[3,5],"\u67d0\u4e2a":[4,5,7,11,12],"\u67d0\u4e9b":[3,4,5,6],"\u67d0\u79cd":[3,4,5,11],"\u67d0\u79cd\u7a0b\u5ea6":11,"\u67d0\u95e8":5,"\u67e5\u627e":8,"\u67e5\u770b":[6,11,15],"\u6807\u51c6":[5,6,10,11],"\u6807\u5fd7":[6,11],"\u6807\u8bb0":[4,5],"\u6807\u8bc6":5,"\u6808\u4e0a":5,"\u6808\u5e27":[5,8],"\u6808\u5e95":[5,6],"\u6808\u662f":5,"\u6808\u6765":5,"\u6808\u9876":[5,6],"\u6837\u5f0f":18,"\u6838\u5fc3":[4,5,12,14],"\u6838\u5fc3\u601d\u60f3":14,"\u6839\u636e":[5,6,10,11],"\u6839\u76ee\u5f55":[3,5,11,15],"\u683c\u5f0f":[5,6,11],"\u68c0\u67e5":[6,11,12],"\u68c0\u9a8c":4,"\u6982\u5ff5":5,"\u6982\u7387":6,"\u6a21\u5757":[3,4,7,11,12],"\u6a21\u5f0f":[5,6,10,11],"\u6a21\u62df":[3,6,9,14,17],"\u6a21\u62df\u5668":[9,14,17],"\u6b22\u8fce":16,"\u6b63\u5728":14,"\u6b63\u5982":5,"\u6b63\u5e38":[3,5,6,15],"\u6b63\u5f0f":6,"\u6b63\u6587":[5,10,16],"\u6b63\u662f":[3,6,10],"\u6b63\u786e":[3,5,6,11,12,14],"\u6b63\u786e\u6027":[5,11,12],"\u6b64\u5916":[5,7,10,11,17],"\u6b64\u65f6":[3,5,6],"\u6bcf\u4e2a":[3,5,6,8,11,12],"\u6bcf\u5c42":10,"\u6bcf\u6b21":[5,6],"\u6bcf\u6b3e":3,"\u6bd4\u5982":[3,5,12,17,18],"\u6bd4\u8f83":[3,4,5,11],"\u6c38\u4e45":5,"\u6c42\u751f":3,"\u6c47\u7f16":[3,5,10,11,12,14],"\u6c47\u7f16\u5668":5,"\u6c47\u7f16\u8bed\u8a00":[5,14],"\u6ca1\u4ec0\u4e48":5,"\u6ca1\u6709":[3,4,5,6,7,10,11,14],"\u6ca1\u6cd5":6,"\u6cbf\u7528":6,"\u6ce8\u610f":[3,4,5,6,11,12,15,18],"\u6ce8\u8bb0":18,"\u6ce8\u91ca":[4,7],"\u6d1e\u6d1e":9,"\u6d3b\u52a8":5,"\u6d3b\u6027":3,"\u6d41\u4e2d":10,"\u6d41\u52a8":3,"\u6d41\u7a0b":[5,6],"\u6d4b\u8bd5":[11,18],"\u6d4f\u89c8":3,"\u6d6a\u8d39":[3,14],"\u6d6a\u8d39\u65f6\u95f4":3,"\u6d6e\u70b9":3,"\u6d6e\u70b9\u6570":3,"\u6df1\u5165":10,"\u6df1\u5230":8,"\u6df1\u5c42":8,"\u6df7\u6dc6":5,"\u6dfb\u52a0":17,"\u6e05\u695a":[3,5],"\u6e05\u7406":12,"\u6e05\u7a7a":[11,12],"\u6e05\u96f6":[5,6],"\u6e90\u4ee3\u7801":[3,5],"\u6e90\u6587\u4ef6":[5,8],"\u6e90\u7801":17,"\u6e90\u7a0b\u5e8f":11,"\u6ea2\u51fa":5,"\u6ee1\u6000":9,"\u6ee1\u8db3":[3,5,6,10],"\u6f5c\u5728":6,"\u7075\u6d3b":[3,6],"\u7075\u6d3b\u6027":3,"\u70b9\u4e3a":[5,6],"\u70b9\u51fb":17,"\u70b9\u6570":3,"\u70e7\u5199":6,"\u7136\u540e":[4,5,6,8,11,12,15],"\u7236\u4eb2":5,"\u7247\u6bb5":[3,18],"\u7248\u672c":[3,5,6,7,11,17],"\u7269\u7406":[5,11,12],"\u7269\u7406\u5730\u5740":[5,11],"\u7279\u522b":[3,5,8,11],"\u7279\u522b\u4e4b\u5904":3,"\u7279\u5b9a":10,"\u7279\u5f81":[3,5],"\u7279\u6027":5,"\u7279\u6743":[11,14],"\u7279\u6b8a":[3,10],"\u72b6\u6001":[5,6,12],"\u72ec\u7279":6,"\u72ec\u7acb":[5,11],"\u73af\u5883":[4,5,10,16,17],"\u73af\u5883\u53d8":17,"\u73af\u5883\u53d8\u91cf":17,"\u73af\u8282":18,"\u73b0\u4ee3":3,"\u73b0\u5728":[4,5,6,10,11,18],"\u73cd\u8d35":14,"\u7406\u5668":3,"\u7406\u6240\u5f53\u7136":[3,9],"\u7406\u89e3":[5,10],"\u751a\u81f3":[4,14,18],"\u751a\u8fdc":4,"\u751f\u53d8":[5,11,12],"\u751f\u6001":3,"\u751f\u6210":[3,4,5,6,11,12],"\u7528\u4e8e":[3,5,11],"\u7528\u4f5c":5,"\u7528\u5230":[3,5,6,10,12],"\u7528\u6237":[3,10,11,14],"\u7528\u6237\u5e93":11,"\u7528\u6237\u7a0b\u5e8f":11,"\u7528\u6743":5,"\u7528\u6765":[3,5,11,12],"\u7528\u8005":5,"\u7528\u8fc7":5,"\u7528\u9014":5,"\u7531\u4e8e":[3,4,5,6,10,11],"\u7531\u5143":6,"\u7531\u6b64":[5,6,8],"\u7565\u5fae":11,"\u7565\u8fc7":10,"\u767d\u5904":5,"\u767d\u8272":[3,10],"\u767e\u9875":3,"\u7684\u786e":5,"\u7684\u8bdd":[3,5,6,7,9,11,12,14,15,17],"\u76d1\u63a7":[3,10],"\u76d1\u7763":10,"\u76ee\u524d":[3,4,5,6,11],"\u76ee\u524d\u4e3a\u6b62":[3,11],"\u76ee\u5f55":[3,4,5,6,7,11,14,15],"\u76ee\u6807":[4,5,6,9],"\u76ee\u7684":5,"\u76f4\u63a5":[3,4,5,6,7,12],"\u76f4\u89c2":8,"\u76f4\u9762":3,"\u76f8\u4ea4":5,"\u76f8\u5173":[3,4,5,7,10,11],"\u76f8\u53bb":4,"\u76f8\u53bb\u751a\u8fdc":4,"\u76f8\u540c":[5,11],"\u76f8\u5bf9":[3,5],"\u76f8\u5e94":5,"\u76f8\u5f53":[3,6,12],"\u76f8\u5f53\u4e8e":[6,12],"\u76f8\u6bd4":[6,10],"\u76f8\u7b49":6,"\u76f8\u90bb":[3,10,12],"\u7701\u7565":5,"\u770b\u4e0a":[3,5],"\u770b\u4e0a\u53bb":[3,5],"\u770b\u4f3c":5,"\u770b\u51fa":[3,5,6,10,12],"\u770b\u5230":[3,5,6,7,8,9,10,11,12,14],"\u770b\u5f85":[5,10],"\u770b\u6210":[5,10],"\u770b\u6765":[3,5],"\u770b\u770b":[3,6],"\u770b\u8d77":[4,5,14],"\u770b\u8d77\u6765":[4,5,14],"\u771f\u6b63":[3,6,14],"\u7740\u6025":4,"\u7740\u624b":[4,5],"\u77e5\u60c5":11,"\u77e5\u8bc6":[5,6,7],"\u77e5\u9053":[3,5,6,8,11,12],"\u7834\u574f":[5,10,14],"\u786c\u4ef6":[3,5,6,9,10,14],"\u786c\u4ef6\u5e73\u53f0":[3,6],"\u786c\u4ef6\u8d44\u6e90":3,"\u786e\u5207":[5,10],"\u786e\u5207\u7684\u8bf4":5,"\u786e\u5b9a":[5,6,7],"\u786e\u5b9e":[3,5],"\u786e\u8ba4":[6,15],"\u78c1\u76d8":6,"\u78c1\u76d8\u7a7a\u95f4":6,"\u793a\u4f8b":18,"\u7981\u7528":[4,5],"\u79bb\u4e0d\u5f00":5,"\u79bb\u5f00":9,"\u79f0\u4e3a":[3,5,6,10,11,14],"\u79f0\u4e4b\u4e3a":[5,6],"\u79fb\u4ea4":5,"\u79fb\u9664":[5,6],"\u7a00\u7f3a":14,"\u7a0b\u5e8f":[4,7,9,10,14],"\u7a0b\u5e8f\u4ee3\u7801":[5,11],"\u7a0b\u5e8f\u5458":[5,6,9,10],"\u7a0b\u5e8f\u8fd0\u884c":[5,10,11,12,14],"\u7a0b\u5ea6":[3,11,12],"\u7a0d\u540e":5,"\u7a33\u5b9a":[3,10,11],"\u7a76\u7adf":[5,6],"\u7a7a\u65e0\u4e00\u7269":9,"\u7a7a\u683c":18,"\u7a7a\u89c1":6,"\u7a7a\u8c03":14,"\u7a7a\u95f4":[5,6],"\u7a97\u53e3":9,"\u7acb\u5373":5,"\u7ae0\u8282":[5,10,15,18],"\u7aef\u5e8f":5,"\u7b26\u53f7":[5,6,11,12],"\u7b26\u53f7\u8868":[5,6],"\u7b26\u5408":5,"\u7b2c\u4e00":[4,5,10,11,12,16],"\u7b2c\u4e00\u4e2a":[5,12],"\u7b2c\u4e00\u6761":5,"\u7b2c\u4e00\u6b21":[4,12],"\u7b2c\u4e00\u7ae0":[10,11,16],"\u7b2c\u4e00\u884c":9,"\u7b2c\u4e8c":5,"\u7b2c\u4e8c\u6761":5,"\u7b2c\u51e0":12,"\u7b2c\u51e0\u4e2a":12,"\u7b49\u4ef7":[6,11],"\u7b49\u5230":5,"\u7b49\u540c":3,"\u7b49\u540c\u4e8e":3,"\u7b49\u5f85":14,"\u7b49\u7b49":3,"\u7b54\u6848":5,"\u7b7e\u540d":4,"\u7b80\u5316":12,"\u7b80\u5355":[0,3,4,5,10,12,14],"\u7b80\u6d01":[3,9],"\u7b80\u8981":11,"\u7b97\u673a":[5,6,14],"\u7ba1\u7406":[3,10,14],"\u7ba1\u7406\u5458":14,"\u7c7b\u4f3c":[3,5,11],"\u7c7b\u578b":[5,6,11,12],"\u7cbe\u5ea6":3,"\u7cdf\u7cd5":14,"\u7cfb\u5217":[3,6,9,14,17],"\u7cfb\u7edf":[3,4,5,6,10],"\u7ea6\u5b9a":[3,5,11,12],"\u7ea6\u675f":6,"\u7ea7\u522b":[5,10],"\u7ebf\u6027":5,"\u7ebf\u6027\u8868":5,"\u7ec4\u6210":[5,10],"\u7ec4\u6210\u90e8\u5206":5,"\u7ec6\u5316":5,"\u7ec6\u8282":[5,7,10],"\u7ec8\u4e8e":[3,9],"\u7ec8\u6b62":[11,14],"\u7ec8\u7aef":[6,9,17],"\u7ecf\u5386":5,"\u7ecf\u5e38":10,"\u7ecf\u8fc7":9,"\u7ed1\u5b9a":11,"\u7ed3\u5c3e":5,"\u7ed3\u675f":[5,6,8,10,12,14],"\u7ed3\u6784":[3,5,7,12,15],"\u7ed3\u679c":[5,7,9,11],"\u7ed5\u8fc7":4,"\u7ed9\u51fa":[3,5,6,7,10,11,18],"\u7edd\u5927":3,"\u7edd\u5927\u591a\u6570":3,"\u7edf\u4e00":10,"\u7ee7\u7eed":[4,5,10,11,18],"\u7ee7\u7eed\u6267\u884c":[5,10,11],"\u7ef4\u62a4":[3,5,12],"\u7f13\u51b2":11,"\u7f13\u51b2\u533a":11,"\u7f13\u5b58":12,"\u7f16\u5199":[3,4,5,7,10,11,14],"\u7f16\u53f7":11,"\u7f16\u7801":[4,5,10],"\u7f16\u7a0b":[3,5,9,10,11,12],"\u7f16\u7a0b\u8bed\u8a00":[3,5,10,11],"\u7f16\u8bd1":[3,4,5,6,7,9,10,11,12,17],"\u7f16\u8bd1\u5668":[3,4,5,6,11,12],"\u7f16\u8f91":9,"\u7f16\u8f91\u5668":9,"\u7f3a\u5c11":[4,6],"\u7f8e\u89c2":18,"\u7f8e\u89c2\u5927\u65b9":18,"\u7ffb\u8bd1":5,"\u8003\u8651":[3,5,11],"\u800c\u4e14":[3,5],"\u800c\u540e":6,"\u800c\u662f":[4,5,6,10,12],"\u800c\u8a00":[3,5],"\u8054\u7cfb":[3,11],"\u8054\u7cfb\u65b9\u5f0f":3,"\u80cc\u540e":[3,9],"\u80cc\u666f":5,"\u80fd\u529b":[3,5,10,11],"\u80fd\u53ca":11,"\u80fd\u591f":[3,4,5,6,7,8,9,10,11,12,14],"\u811a\u672c":[5,6,11,12],"\u8131\u79bb":4,"\u81ea\u4e0a\u800c\u4e0b":3,"\u81ea\u52a8":[4,5,12,14],"\u81ea\u5df1":[4,5,6,7,8,9,11],"\u81ea\u7136":[3,5,12],"\u81ea\u884c":7,"\u81ea\u8eab":[3,5,14],"\u81f3\u4e8e":11,"\u81f3\u4eca":9,"\u81f3\u5c11":5,"\u81f3\u6b64":[4,5],"\u81f4\u547d":4,"\u8212\u9002":9,"\u8282\u6570":5,"\u8282\u7701":6,"\u82e5\u5e72":[3,5,6,11],"\u82e5\u5e72\u4e2a":[5,6],"\u82e5\u60f3":[5,6],"\u8303\u56f4":10,"\u8349\u8349":5,"\u83b7\u53d6":[6,7,9,14],"\u83b7\u5f97":3,"\u865a\u62df":6,"\u865a\u62df\u673a":6,"\u867d\u7136":[3,5,11],"\u884c\u4e2d":[5,6],"\u884c\u4e3a":[4,5,6,7,11],"\u884c\u5185":12,"\u884c\u6570":7,"\u884c\u6587":[3,4,5,6,8,11],"\u884c\u662f":[6,7],"\u884c\u95f4":18,"\u884d\u751f":12,"\u8868\u660e":[3,7],"\u8868\u683c":18,"\u8868\u73b0":10,"\u8868\u793a":[3,5,6,10,11,12],"\u8868\u8fbe":11,"\u8868\u8fbe\u80fd\u529b":11,"\u88c1\u51cf":3,"\u88c1\u526a":3,"\u88f8\u673a":[3,4,5,14],"\u8986\u76d6":5,"\u89c1\u60ef":6,"\u89c4\u5219":[5,6],"\u89c4\u8303":[3,5,6,11],"\u89c6\u4f5c":5,"\u89c6\u540c":11,"\u89c6\u89d2":[5,10],"\u89d2\u5ea6":[3,5,10,11,12],"\u89e3\u51b3":[3,4,6,12],"\u89e3\u51b3\u65b9\u6848":[3,4],"\u89e3\u6790":[4,6,7,12],"\u89e3\u7801":5,"\u89e3\u91ca":[3,5],"\u89e3\u9501":3,"\u89e6\u53d1":[10,11],"\u8b66\u544a":18,"\u8ba1\u7b97":[3,5,6,11,14],"\u8ba1\u7b97\u673a":[5,6,14],"\u8ba1\u7b97\u8d44\u6e90":14,"\u8ba4\u4e3a":[6,11,12],"\u8ba8\u8bba":[5,15],"\u8ba8\u8bba\u533a":15,"\u8bb0\u5f55":[5,6,14],"\u8bb2\u5230":5,"\u8bb2\u89e3":10,"\u8bbe\u5907":[3,6],"\u8bbe\u5b9a":3,"\u8bbe\u7f6e":[3,4,5,6,11,12],"\u8bbe\u8ba1":[10,11,12],"\u8bbf\u5b58":[5,12],"\u8bbf\u95ee":[3,5,6,11,12],"\u8bc6\u522b":[3,6],"\u8bd5\u56fe":5,"\u8bde\u751f":14,"\u8be6\u7ec6":[5,7],"\u8bed\u4e49":[3,6,11],"\u8bed\u53e5":[5,8,9],"\u8bed\u6cd5":[6,15],"\u8bed\u8a00":[3,4,5,6,10,11,14],"\u8bf4\u660e":[5,10,11,12,16],"\u8bf7\u6c42":[3,11],"\u8bf8\u591a":12,"\u8bf8\u591a\u4e0d\u4fbf":12,"\u8bfb\u53d6":[5,11],"\u8bfb\u8005":[3,6,7,10,11,12],"\u8bfe\u4ef6":5,"\u8bfe\u7a0b":0,"\u8c03\u6574":[4,5,10,11,12],"\u8c03\u7528":[3,4,6,7,8,10,12],"\u8c03\u7528\u51fd\u6570":[5,8],"\u8c03\u7528\u8005":5,"\u8c28\u614e":6,"\u8d1f\u62c5":3,"\u8d1f\u8d23":[4,5,6,11,12,14],"\u8d39\u65f6":3,"\u8d39\u65f6\u95f4":3,"\u8d44\u6e90":[3,5,14],"\u8d4b\u4e88":5,"\u8d4b\u503c":5,"\u8d56\u4e8e":[6,12],"\u8d77\u59cb":[5,6,11,12],"\u8d77\u6765":[3,4,5,6,12,14,18],"\u8d85\u51fa":[6,10,11],"\u8d8a\u5c0f":10,"\u8d8a\u5f3a":10,"\u8d8a\u5f80":3,"\u8d8a\u8fc7":5,"\u8db3\u4ee5":[3,4],"\u8dd1\u6765\u8dd1\u53bb":14,"\u8ddd\u79bb":4,"\u8ddf\u8e2a":[5,6,8],"\u8de8\u5e73\u53f0":3,"\u8def\u5f84":6,"\u8df3\u8f6c":[4,11],"\u8f6c\u5316":[5,6],"\u8f6c\u56de":5,"\u8f6c\u6210":6,"\u8f6c\u79fb":5,"\u8f6c\u800c":[4,10,14],"\u8f6c\u8fc7":5,"\u8f6f\u4ef6":[3,5,9,10,14],"\u8f7b\u91cf":3,"\u8f7b\u91cf\u7ea7":3,"\u8f7d\u5165":[5,6],"\u8f93\u5165":[3,4,5,6,11,14],"\u8f93\u51fa":[3,5,6,9,11,14],"\u8fb9\u754c":3,"\u8fbe\u5230":5,"\u8fc7\u4e8e":6,"\u8fc7\u540e":3,"\u8fc7\u591a":3,"\u8fc7\u5c11":3,"\u8fc7\u7a0b":[3,5,7,10,11,14],"\u8fd0\u7b97":3,"\u8fd0\u884c":[4,7,8,9,10,11,12,14,17],"\u8fd4\u56de":[3,5,7,11],"\u8fd4\u56de\u503c":[3,5,7,11],"\u8fd8\u539f":6,"\u8fd8\u662f":[3,4,5,11],"\u8fd8\u6709":[3,6,9],"\u8fd9\u4e00":[5,7],"\u8fd9\u4e24\u70b9":5,"\u8fd9\u4e2a":[3,4,5,6,10,11,12,14,18],"\u8fd9\u4e48":[3,5],"\u8fd9\u4e9b":[3,4,5,6,11,12,14],"\u8fd9\u4f1a":[4,12],"\u8fd9\u5757":5,"\u8fd9\u5f20":10,"\u8fd9\u65b9":3,"\u8fd9\u65b9\u9762":3,"\u8fd9\u662f":[4,5,10,11,18],"\u8fd9\u6709":5,"\u8fd9\u6761":5,"\u8fd9\u6837":[3,5,6,10,11,12,14],"\u8fd9\u6837\u4e00\u6765":5,"\u8fd9\u6b21":5,"\u8fd9\u6bb5":5,"\u8fd9\u79cd":[3,5,10,12,14],"\u8fd9\u80fd":11,"\u8fd9\u91cc":[3,5,6,7,10,11,12,15,18],"\u8fd9\u9879":4,"\u8fdb\u4e00\u6b65":5,"\u8fdb\u5165":[3,6,11,18],"\u8fdb\u5236":[5,6,10,11,12],"\u8fdb\u53bb":5,"\u8fdb\u5c55":11,"\u8fdb\u884c":[3,4,5,6,7,10,11,12,15,16,17],"\u8fdc\u6bd4":3,"\u8fdc\u7a0b":15,"\u8fde\u63a5":[6,9,14],"\u8fde\u7eed":[5,6,14],"\u8fed\u4ee3":6,"\u9000\u51fa":[5,6,11],"\u9002\u5e94":3,"\u9002\u5f53":[5,6],"\u9009\u53d6":[5,11],"\u9009\u62e9":[3,5],"\u9009\u9879":[3,5,11],"\u9010\u4e2a":11,"\u9010\u5b57":5,"\u9010\u884c":11,"\u9012\u63a8":5,"\u9012\u8fdb":5,"\u901a\u4fd7":10,"\u901a\u4fe1":[6,12],"\u901a\u5e38":[3,4,5,6,12],"\u901a\u5f80":9,"\u901a\u7528":[3,5,10],"\u901a\u7528\u5bc4\u5b58\u5668":[3,5],"\u901a\u7528\u6027":10,"\u901a\u77e5":4,"\u901a\u8fc7":[3,4,5,6,7,8,9,10,11,12],"\u901a\u914d\u7b26":5,"\u901f\u5ea6":12,"\u9020\u6210":11,"\u903b\u8f91":[10,11,12],"\u9047\u5230":[3,4,5],"\u9057\u61be":[5,11],"\u9075\u4ece":6,"\u907f\u514d":[5,10,12],"\u90a3\u4e2a":14,"\u90a3\u4e48":[3,5,9,11,14],"\u90a3\u4e9b":5,"\u90a3\u6761":5,"\u90a3\u79cd":3,"\u90e8\u5206":[0,3,5,6,7,10,11,12],"\u90e8\u7f72":17,"\u914d\u5668":5,"\u914d\u7f6e":[3,5,6,16,17],"\u914d\u7f6e\u6587\u4ef6":5,"\u91c7\u7528":[4,18],"\u91cc\u9762":[3,4,5,6,7,11,12],"\u91cd\u5851":4,"\u91cd\u590d":4,"\u91cd\u590d\u672c":4,"\u91cd\u65b0":[10,12],"\u91cd\u70b9":[5,10],"\u91cd\u8981":[5,11,12],"\u91cd\u8981\u4e00\u73af":12,"\u91cf\u7ea7":3,"\u9488\u5bf9":11,"\u94fe\u4e2d":3,"\u94fe\u63a5":[3,5,6,11,17,18],"\u9519\u8bef":[4,5,7,10,11,14,18],"\u9519\u8bef\u5904\u7406":7,"\u952e\u5165":9,"\u952e\u76d8":6,"\u955c\u50cf":[6,11,12],"\u955c\u50cf\u6587\u4ef6":[6,11,12],"\u957f\u4e32":3,"\u957f\u5ea6":[5,6,11],"\u95ed\u5305":6,"\u95ee\u9898":[3,5,6,12],"\u95f4\u5c42":3,"\u95f4\u63a5":3,"\u95f4\u65ad":14,"\u9605\u8bfb":[3,8,16],"\u9632\u6b62":11,"\u9636\u6bb5":5,"\u9644\u5f55":10,"\u9644\u8fd1":5,"\u9650\u4e8e":[3,5],"\u9650\u5236":[3,11],"\u9664\u4e86":[3,4,5,6,9,11],"\u9664\u6b64":3,"\u9664\u6b64\u4e4b\u5916":3,"\u9677\u5165":10,"\u968f\u4fbf":5,"\u968f\u540e":[5,7],"\u968f\u610f":5,"\u968f\u7740":[3,10],"\u9690\u7ea6":9,"\u9690\u85cf":9,"\u9694\u5f00":18,"\u9694\u79bb":[10,14],"\u96be\u514d":14,"\u96c6\u5408":5,"\u9700\u6c42":[3,6,10],"\u9700\u8981":[3,4,5,6,7,10,11,12,14,15,17,18],"\u9732\u51fa":12,"\u975e\u5e38":[3,5,14,18],"\u975e\u5e38\u7b80\u5355":[3,5],"\u975e\u6cd5":11,"\u9760\u8fd1":3,"\u9876\u5f39":5,"\u9879\u6b63":4,"\u9879\u76ee":[3,5,6],"\u987a\u5229":[9,14],"\u987a\u5e8f":[5,6,10,11,12],"\u987e\u8651":5,"\u9884\u60f3":3,"\u9884\u671f":5,"\u9884\u7559":[5,6],"\u9884\u8bbe":[3,6],"\u9886\u57df":6,"\u989d\u5916":[3,5,9,11],"\u9996\u5148":[4,5,6,11,12,17],"\u9ad8\u4eae":[5,6],"\u9ad8\u5230":5,"\u9ad8\u697c":9,"\u9ad8\u697c\u5927\u53a6":9,"\u9ad8\u7ea7":[3,5,11],"\u9ad8\u901f":12,"\u9ad8\u901f\u7f13\u5b58":12,"\u9b54\u6570":6,"\u9ebb\u70e6":5,"\u9ed1\u6d1e":9,"\u9ed1\u6d1e\u6d1e":9,"\u9ed1\u76d2":5,"\u9ed1\u8272":[3,10],"\u9ed8\u8ba4":[3,4,5],"_______..______":[7,9,14],"_______.___________.":[7,9,14],"abstract":3,"byte":6,"c++":5,"cgu.0":6,"cgu.1":6,"class":6,"const":[11,12],"default":6,"float":6,"for":[3,5,6,12],"function":[4,5],"if":[5,6,7,12],"in":[3,4,5,6,14],"new":[3,5,12],"null":6,"os.78":6,"static":[6,12],"switch":5,"v0.1":[3,4],"while":5,"with":14,__:[7,9,14],_____:[7,9,14],______:[7,9,14],_______:[7,9,14],___________:[7,9,14],_end:12,_info:4,_num_app:12,_start:[5,6,11,12],a0:[5,11,18],a1:[5,11],a2:11,a6:11,a7:[5,11],a_:5,a_n:5,abi:[6,11],abs:6,access:10,activ:5,add:[12,15,18],addr2lin:8,addr:[6,12],address:[5,6,10,12],after:11,align:[5,6,12],all:[6,12,14],alloc:6,alpha:18,amo:10,an:14,and:6,app:[12,14],app_0:14,app_0_end:12,app_0_start:12,app_1:14,app_1_end:12,app_1_start:12,app_2:14,app_2_end:12,app_2_start:12,app_:12,app_base_address:12,app_dst:12,app_id:12,app_manag:12,app_size_limit:12,app_src:12,app_start:12,app_start_raw:12,applic:[10,12,14],appmanag:12,appmanagerinn:12,arch:5,architectur:3,are:6,area:12,arg:[5,11],as:[6,7,11,12],as_ptr:11,ascii:5,asm:[5,11,12],assembl:[5,11],assert:4,at:[7,9,14],attribut:6,ax:6,bare:3,base:[5,6,12],base_address:[5,6],batch:[12,14],be:3,berkelei:5,beta:18,bin:[3,6,11,12,14],binari:[3,6,10],bind:6,bio:6,bit:6,blogo:4,board:[6,9,14],boot:[5,6,7,9],boot_stack:[5,6,7,9],boot_stack_top:[5,6,7],bootload:[5,6,7,10],borrow:12,borrow_mut:12,box:6,breakpoint:10,bs:6,bss:[5,7,9,11],buf:11,buffer:11,build:[4,5,6,11,12,15],burner:6,but:4,cach:12,call:[3,5,10,11],calle:5,caller:5,can:3,cannot:[4,11],cargo:[3,4,5,6,11,12],cd:[9,14],central:3,cforc:5,cgu:6,ch1:9,ch2:14,checkout:[9,14],chmod:6,clean:15,clear:[6,7,11,12],clear_bss:[6,7,11],clink:5,clobber:11,clone:[9,14],code:[10,14],com:[9,14],comment:[6,15],commit:[3,15],compil:[3,4,5],complement:6,complet:[12,14],compress:6,config:[4,5,11],consol:[7,11],console_putchar:7,contain:6,context:5,control:[5,10],convent:5,copi:[6,12],copy_from_slic:12,core:[3,4,7,12,14],cornel:5,cos:18,cp:6,cpu:[3,5,10,12],crate:[3,7,11],cross:4,cs3410:5,cs61c:5,ctrl:6,current:[6,12],current_app:12,data:[5,6,7,9,12],date:3,dd:6,debug:3,debuginfo:[3,4],depend:12,deploi:15,derefer:6,descript:10,destin:5,dev:[3,4,6],devic:6,direct:6,directori:3,discard:5,doc:15,docker:17,dst:12,dtr:6,dump:14,e0463:3,each:[6,12],ebreak:10,ebss:[5,6,7],ecal:[10,11],ecf:10,edata:[5,6,7],eh:5,eh_fram:5,ekernel:[5,6],elf64:6,elf:[3,4,5,6,11,12,17],els:[6,7],end:12,endian:6,endif:6,entri:[5,6,7,9,11,14],entsiz:6,environ:[3,10,11],eol:6,epilogu:5,erodata:[5,6,7],err:[4,7],error:[3,4],etext:[5,6,7],except:10,exclud:6,exec:6,execut:[3,6,10],exit:[11,14],extern:[6,7,11,12],extra:6,fat:11,fault:[10,11,12,14],fd:11,featur:[5,7,11,12],fenc:12,file:[3,5,6,7],filesiz:6,filter:6,find:[3,4,11],finish:[3,4],first:5,fixm:[6,9,17],flag:6,flow:[5,10],fmt:7,fn:[3,4,5,6,7,11,12,18],for_each:[6,12],format:6,found:4,fp:[5,8],frame:5,from:[5,10,11,12],from_raw_part:12,from_raw_parts_mut:12,func:6,gdb:6,get:12,get_current_app:12,gif:18,git:[9,14,15],github:[9,14],global:[5,6,12],global_asm:[5,12],globl:5,gnu:[3,4,6],gnu_stack:6,gp:5,grep:3,group:6,guid:0,handler:7,hash:3,header:[6,11],heap:5,hello:[3,4,5,7,9,11,14],hi0:6,home:[3,4],host:3,html:15,http:[9,14],hypervisor:10,i32:[11,18],icach:12,id:[11,12],ifeq:6,illeg:10,imm:[5,18],immedi:5,incbin:12,includ:[5,12],include_str:[5,12],index:[6,15],info:[4,6,7,12],init:12,inlin:11,inner:[6,12],input:11,insert:14,instal:[3,15],instruct:[3,5,10],interfac:10,interrupt:10,into:[6,14],invalid:14,ir:11,isa:[3,4],isiz:11,item:[4,5,7],jal:[5,18],jalr:[5,18],jieba:15,k210:[9,14],kei:6,kernel:[6,7,9,12,14],kernel_bin:6,kernel_elf:6,kernel_entry_pa:6,kflash:6,kib:[5,6],kill:14,la:5,lang:[4,5,7],lang_item:[4,5,7],last:5,layout:5,lazi:12,lazy_stat:12,ld:[5,6,11],lectur:5,leftarrow:[5,18],len:[11,12],let:[7,11,12],lf:6,lib:11,lifo:5,limit:12,line:7,link:[6,11,12],link_app:12,link_sect:11,linkabl:6,linkag:11,linker:[5,6,11],linux:[3,4,17],list:3,littl:6,llvm:[3,11,12],llvm_asm:[11,12],load:[6,10,12,14],load_app:12,loader:6,local:6,localhost:17,locat:7,loop:[4,5],lpcrel:6,lpcrel_hi0:6,lsb:6,machin:[6,7,9,10],maco:[3,9,17],macro:[4,7,11],macro_us:[7,11],mafd:3,magic:6,mai:3,main:[3,5,6,7,8,9,11,12],maix:[6,9,14,17],make:[5,6,9,11,12,14,15],makefil:6,malloc:5,man:6,manag:12,mangl:[5,7,11],max:12,max_app_num:12,medeleg:[7,9,14],memori:[3,5,11],memsiz:6,merg:[6,15],messag:7,metadata:6,metal:3,mideleg:[7,9,14],miniterm:[6,17],misa:[7,9,14],misalign:10,mit:5,mod:[4,5,7],mode:[6,10,11],move:12,move_to_next_app:12,ms:6,musl:17,mut:[6,11,12],mv:6,name:[6,11],ndx:6,next:12,nightli:3,no:[4,5,6,7,11,12],no_main:[4,5],no_mangl:[5,7,11],no_std:[4,5],nobit:6,nograph:6,none:[3,4,5,6,7,11,12,17],not:[3,4,6],note:[3,5],notyp:6,nr:6,num:[6,12,14],num_app:[12,14],num_app_ptr:12,number:6,objcopi:[6,11],object:5,objectfil:5,of:6,offset:6,ok:[7,14],oper:14,operand:11,option:[4,7,11],order:6,os:[3,4,5,6,7,9,12,14],out:5,output:[5,11],output_arch:5,pa:6,page:10,pagefault:14,panic:[7,11,12],panic_handl:7,panic_info_messag:7,panicinfo:[4,7],panick:[7,9,14],part:12,pc:[5,6,9,14,18],physaddr:6,physic:5,pip:15,platform:[3,7,9,14],point:6,pointer:[5,6,11],pop:5,power:14,prev:8,print:[3,7,12],print_app_info:12,println:[3,7,11,12],privileg:14,process:[3,6],processor:[3,6],progbit:6,program:6,prologu:5,pseudo:5,ptr:[11,12],pub:[5,7,11],push:[5,15],putchar:7,py:6,pyseri:17,python3:6,python:17,qemu:[7,9,14,17],quad:12,ra:[5,8],raw:[6,12],rc:6,rcore:[3,4,9,14,17],rd:[5,18],read:[12,15],read_volatil:12,readelf:6,readthedoc:18,record:5,ref:12,refcel:[6,12],regist:5,releas:[3,5,6,11,12],requir:[4,6],rest:15,restor:5,restructuredtext:15,result:[4,7],ret:[5,11],rfc:11,risc:[3,6,11],riscv32i:3,riscv32imac:3,riscv32imc:3,riscv64:[6,17],riscv64gc:[3,4,5,6,11,12,17],riscv64imac:[3,17],riscv64imafdc:3,riscv:[3,5,6],riscv_attribut:6,rodata:[5,6,7,9],rs:[3,4,5,6,7,9,11,12,14,18],rtd:15,rts:6,run:[3,6,9,12,14],run_next_app:12,rust:[4,5,6,7,8,11,12,17,18],rust_main:[5,6,7,8],rustc:3,rustflag:5,rusto:0,rustsbi:[6,7,9,10,14],rustup:3,rv32:3,rv32i:3,rv64:[6,14],rv64acdfimsu:[7,9,14],rv64gc:4,rv64i:3,rvc:6,rw:6,s0:5,s11:5,s1:5,safe:6,save:5,sbi:[6,7,10,11],sbss:[5,6,7],scope:4,script:5,sdata:[5,6,7],section:[5,6,11,12],sectionnam:5,see:[10,11],seek:6,self:12,serialport:6,set:3,shinbokuow:[3,4],should:14,shstrtab:6,shutdown:[7,9],sifiv:17,sin:18,size:[6,12],skernel:[5,6],slice:12,soft:6,some:7,sourc:5,sp:5,space:5,specif:6,sphinx:[15,18],sphinx_rtd_them:15,spin:12,spin_no_std:12,src:[3,4,5,6,7,9,11,12,14],srodata:[5,6,7],stack:[5,6,7,9],stackfram:5,stanford:0,start:[4,5,6,11,12],std:[3,4,5,12],stdout:11,stext:[5,6,7],store:[10,14],store_fault:14,str:[5,11,12],strace:3,string:6,strip:6,strtab:6,struct:12,sudo:6,supervisor:10,sy:11,symbol:6,symtab:6,sys_exit:11,sys_writ:11,syscal:[10,11],syscall_exit:11,syscall_writ:11,system:[3,6,10,14],sysv:6,t0:5,t6:5,tabl:6,target:[3,4,5,6,11,12],templat:11,test:14,text:[5,6,7,9,11,18],the:[3,15],theme:15,there:6,thi:[4,6,14],tls:6,to:[6,12],toml:[3,11,12],tool:6,top:[5,6,7],tp:5,trap:[10,11],tree:3,trick:4,triplet:3,tsrc:5,ttyusb0:6,tutori:[3,4,9,14,17],type:6,u8:[6,11,12],ubuntu:[3,6,17],ucb:6,und:6,unit:3,unix:[6,10],unknown:[3,4,5,6,11,12,17],unoptim:[3,4],unreach:11,unsaf:[6,11,12],unwrap:[4,7],use:[4,7,11],user:[10,11,12,14],user_lib:11,usiz:[6,7,11,12],v0:[3,4],v3:[3,4,9,14],valu:6,verbos:3,version:[3,6,7,9,12,14],virt:6,virtaddr:6,vis:6,volatil:[6,11,12],wa:6,we:14,weak:11,will:14,window:3,workspac:[3,4],world:[3,4,5,7,9,11,12,14],wp4f2l:6,write:[3,4,6,11,12],write_str:11,write_volatil:[6,12],wsl:9,x0:5,x10:11,x11:11,x12:11,x17:11,x1:5,x2:5,x31:5,x3:5,x4:5,x86:[3,4],x86_64:[3,4],xstate:11,yes:5,zero:5},titles:["Rust \u5feb\u901f\u5165\u95e8","\u5e38\u89c1\u5de5\u5177\u7684\u4f7f\u7528\u65b9\u6cd5","\u6df1\u5165\u673a\u5668\u6a21\u5f0f\uff1aRustSBI","\u5e94\u7528\u7a0b\u5e8f\u8fd0\u884c\u73af\u5883\u4e0e\u5e73\u53f0\u652f\u6301","\u79fb\u9664\u6807\u51c6\u5e93\u4f9d\u8d56","\u91cd\u5efa\u6700\u5c0f\u5316\u8fd0\u884c\u65f6","\u624b\u52a8\u52a0\u8f7d\u3001\u8fd0\u884c\u5e94\u7528\u7a0b\u5e8f","\u683c\u5f0f\u5316\u8f93\u51fa","\u7ec3\u4e60\u4e00","\u7b2c\u4e00\u7ae0\uff1aRV64 \u88f8\u673a\u5e94\u7528","RISC-V \u7279\u6743\u7ea7\u67b6\u6784","\u5b9e\u73b0\u5e94\u7528\u7a0b\u5e8f","\u5b9e\u73b0\u6279\u5904\u7406\u7cfb\u7edf","\u5904\u7406 Trap","\u7b2c\u4e8c\u7ae0\uff1a\u6279\u5904\u7406\u7cfb\u7edf","\u9879\u76ee\u534f\u4f5c","rCore-Tutorial-Book \u7b2c\u4e09\u7248","\u5feb\u901f\u4e0a\u624b","reStructuredText \u57fa\u672c\u8bed\u6cd5"],titleterms:{"\u4e00\u7ae0":9,"\u4e00\u89c8":10,"\u4e00\u89c8\u8868":10,"\u4e09\u5143":3,"\u4e09\u5143\u7ec4":3,"\u4e8c\u7ae0":14,"\u4f7f\u7528":1,"\u4f9d\u8d56":4,"\u5143\u7ec4":3,"\u5165\u95e8":0,"\u5185\u5b58":[5,11],"\u5185\u6838":12,"\u51fd\u6570":[4,5,18],"\u51fd\u6570\u8c03\u7528":[5,18],"\u5206\u7c7b":5,"\u529f\u80fd":5,"\u52a0\u8f7d":6,"\u534f\u4f5c":[15,16],"\u53c2\u8003":5,"\u53c2\u8003\u6587\u732e":5,"\u57fa\u672c":18,"\u5904\u7406":[12,13,14],"\u5b9e\u73b0":[5,11,12],"\u5bc4\u5b58":5,"\u5bc4\u5b58\u5668":5,"\u5de5\u5177":1,"\u5e03\u5c40":[5,11],"\u5e38\u89c1":1,"\u5e73\u53f0":[3,6],"\u5e94\u7528":[3,6,9,11,12],"\u5e94\u7528\u7a0b\u5e8f":[6,11,12],"\u5f02\u5e38":10,"\u5feb\u901f":[0,17],"\u624b\u52a8":6,"\u6279\u5904\u7406":[12,14],"\u6307\u4ee4":[5,18],"\u63d0\u4f9b":4,"\u652f\u6301":3,"\u6587\u732e":5,"\u65b9\u6cd5":1,"\u6700\u5c0f":5,"\u6700\u5c0f\u5316":5,"\u673a\u5668":2,"\u6784\u5efa":11,"\u67b6\u6784":10,"\u6807\u51c6":[3,4],"\u6838\u5fc3":3,"\u683c\u5f0f":7,"\u683c\u5f0f\u5316":7,"\u6a21\u5f0f":2,"\u6df1\u5165":2,"\u6e05\u7a7a":6,"\u7279\u6743":10,"\u73af\u5883":3,"\u7406\u5668":12,"\u76ee\u6807":3,"\u79fb\u9664":4,"\u7a0b\u5e8f":[3,5,6,11,12],"\u7a0b\u5e8f\u8fd0\u884c":3,"\u7b2c\u4e00":9,"\u7b2c\u4e00\u7ae0":9,"\u7b2c\u4e09":16,"\u7b2c\u4e09\u7248":16,"\u7b2c\u4e8c":14,"\u7b2c\u4e8c\u7ae0":14,"\u7ba1\u7406":12,"\u7ba1\u7406\u5668":12,"\u7cfb\u7edf":[11,12,14],"\u7ec3\u4e60":8,"\u7ed3\u6784":11,"\u7f16\u7a0b":8,"\u81ea\u52a8":11,"\u88f8\u673a":9,"\u8bed\u4e49":4,"\u8bed\u6cd5":18,"\u8bfb\u8005":16,"\u8c03\u7528":[5,11,18],"\u8df3\u8f6c":[5,18],"\u8f93\u51fa":7,"\u8fd0\u884c":[3,5,6],"\u91cd\u5efa":5,"\u94fe\u63a5":12,"\u9879\u76ee":[11,15,16],"\u987b\u77e5":16,backtrac:8,book:16,bss:6,handler:4,k210:6,main:4,panic:4,panic_handl:4,println:4,qemu:6,rcore:16,restructuredtext:18,risc:[5,10,18],rust:[0,3],rustsbi:2,rv64:9,trap:13,tutori:16}}) \ No newline at end of file diff --git a/outline.md b/outline.md new file mode 100644 index 0000000000000000000000000000000000000000..790970cb8346b132f614551bd5a9b542c7e28f0a --- /dev/null +++ b/outline.md @@ -0,0 +1,425 @@ +# 更新记录 +### 2021-01-04更新: + +Chaptter4 添加实现的过程描述:改进内存隔离的好处; + +### 2020-12-20更新: + +将文件描述符从 Chapter7 移动到 Chapter6。 + +### 2020-12-02更新: + +根据讨论更新了 Chapter1-Chapter7 到分割线之前的内容作为 Tutorial 的第一部分,即让系统能够将所有的资源都利用起来。第二部分则讨论如何做的更好。在 12 月 26 日之前尽可能按照大纲完成多个不同版本的 demo。 + +[https://shimo.im/sheets/wV3VVxl04EieK3y1/MODOC](https://shimo.im/sheets/wV3VVxl04EieK3y1/MODOC)是目前的系统调用一览表,预计只需要实现 14 个系统调用就能初步满足要求。 + +### 2020-11-30更新: + +更新了Chapter2。 + +合并了Chapter3/Chapter4为Chapter3,目前覆盖范围为Chapter1-Chapter5。 + +### lab 设计:2020-11-01 + +#### 可能的章节与代码风格 + +* 新 OS 实验的目的是:“**强化学生对 OS 的整体观念**”。OS的目的是满足应用需求,为此需要一定的硬件支持和自身逐步增强的功能。鼓励学生自己从头写(有参考实现)、强化整体观、step-by-step。 +* **整个文档的风格是应用**导向的,每个 step 的任务一定不是凭空而来、而是**应用**的需求。每一章都是为了解决一个应用具体需求而要求OS要完成的功能,这个功能需要一定的硬件支持。 +* 每个章节给出完整可运行且带有完整注释(可以通过 rustdoc 工具生成 html 版)的代码。 +* 文档中给出重要的代码片段(照顾到纸质版的读者,事实上在网页版给出代码的链接即可)而并不需要完整的代码,但是需要有完整的执行流程叙述,对于边界条件有足够的讨论。在文档中插入的代码不带有注释,而是将解释放到文档的文字部分。 +* 类似xv6,每一章的小节描述一项小功能是如何实现的,不同小节之间可能有一定的先后关系,也有可能是并列的。 +* 尽可能讲清楚设计背后的思想与优缺点。 +* 在讲解OS设计方面,尽量做到与语言无关。在讲解例子的时候,应该有对应的C和rust版本。 +* 在某些具体例子中,最好能体现rust比c强 +* 2020-10-28:前几章 Chapter1-4 需要等具体实现出来之后再规划章节。 + +# 章节大纲 +## Chapter0 Hello world! 之旅(偏概述) + +### 主要动机: + +参考 csapp 第一章,站在一个相对宏观的视角解释一个非常简单的 hello world! 程序是在哪些硬件/软件的支持下得以编译/运行起来的。 + +helloworld.c 如何被编译器编译成执行程序,且如何被操作系统执行的。 + +gcc + +strace + +## Chapter1 裸机应用(优先级1) + +### 主要动机 + +支持应用进行计算与结果输出。 + +在裸机上输出 Hello world,就像在其他 OS 上一样。 + +app列表: + +* hello_world:输出字符串 +* count_sum:累加一维数组的和,并输出结果 + +备注:不需要输入功能 + +### 内核应完成功能 + +内存地址空间: + +知道自己在内存的哪个位置。理解编译器生成的代码。 + +init:基本初始化 + +主要是硬件加电后的硬件初始化,以前是OS做,后面给BIOS, bootloader等完成初步初始化。OS需要知道内存大小,IO分布。 + +write函数:输出字符串 + +驱动串口的初始化,能够通过串口输出。 + +exit函数:表明程序结束 + +其它(不是主要的): + +在 qemu/k210 平台上基于 RustSBI 跳转到内核,打印调试信息,支持内核堆内存分配。 + +### 章节分布 + +基本上和第二版/第三版一致。注意需要考虑上面的应用和功能。 + +## Chapter2批处理系统(优先级1) + +### 主要动机 + +内核不会被应用程序破坏 + +### 用户程序 + +支持应用进行计算与结果输出。在裸机上输出 Hello world,就像在其他 OS 上一样。但应用程序无法破坏内核,但能得到内核的服务。 + +app列表: + +* hello_world:输出字符串。 +* count_sum:累加一维数组的和,并输出结果。 +### 内核应完成功能 + +设置好内核和用户运行的栈,内核初始化完成后通过 sret 跳转到用户程序进行执行,然后在用户程序系统调用的时候完成特权级切换、上下文保存/恢复及栈的切换 + +按顺序加载运行多个应用程序。当应用程序出错(非法指令基于 RustSBI 不容易完成,比如访问非法的物理地址)之后直接杀死应用程序并切换到下一个。 + +### 新增系统调用 + +* sys_write:向串口写 +* sys_exit: 表明任务结束。 +### 实现备注 + +将编译之后的用户镜像和内核打包到一起放到内存上 + +分离用户和内核特权级,保护OS,用户需要请求内核提供的服务 + +## +## Chapter3 分时多任务系统之一非抢占式调度(优先级1) + +### 主要动机 + +提高整个应用的CPU利用率 + +多任务,因此需要实现任务切换,可采用如下方法: + +* 批处理:在内存中放多个程序,执行完一个再执行下一个。当执行IO操作时,采用的是忙等的方式,效率差。 +* 非抢占切换:CPU和I/O设备之间速度不匹配矛盾,程序之间的公平性。当一个程序主动要求暂停或退出时,换另外一个程序执行CPU计算。 + +*>> 这时,可能需要引入中断(但中断不是本章主要的内容,如果不引入更好)。* + +### 用户程序 + +两个程序放置在一个不同的固定的物理地址上(这样不需要页表机制等虚存能力),完成的功能为:一个程序完成一些计算&输出,主动暂停,OS切换到另外一个程序执行,交替运行。 + +* count_multiplication:一维数组的乘法,并输出结果 +* count_sum:累加一维数组的和,并输出结果 +* [wyf 的具体实现]三个输出小程序,详见[here](https://github.com/rcore-os/rCore-Tutorial-v3/tree/ch3-coop/user/src/bin) +### 内核应完成功能 + +实现通过 sys_yield 交出当前任务的 CPU 所有权,通过 sys_exit 表明任务结束。需要为每个任务分配一个用户栈和内核栈,且需要实现类似 switch 用来任务切换的函数。 + +* sys_yield:让出CPU +* sys_exit:退出当前任务并让出 CPU +### 实现备注 + +重点是实现switch + +当所有任务运行结束后退出内核 + +## Chapter3 分时多任务系统之二 抢占式调度(优先级1) + +### 主要动机 + +进一步提高整个应用的CPU利用率/交互性与任务之间的公平性 + +因此需要实现强制任务切换,并引入中断,可采用如下方法: + +* 时钟中断:基于时间片进行调度 +* (不在这里引入)串口中断:在发出输出请求后,不是轮询忙等,而是中断方式响应 +### 用户程序 + +* [wyf 的具体实现]三个计算质数幂次的小程序,外加一个 sleep 的程序。[here](https://github.com/rcore-os/rCore-Tutorial-v3/tree/ch3/user/src/bin) +### 内核应完成功能 + +实现时钟/串口中断处理,以及基于中断的基本时间片轮转调度 + +### 新增系统调用 + +* sys_get_time:返回当前的 CPU 时钟周期数 +## Chapter4 内存隔离安全性:地址空间(优先级1) + +### 主要动机 + +* 更好地支持应用(包括内核)的动态内存需求。首先:在内核态实现动态内存分配(这是物理内存),这样引入了堆的概念 +* 更好地支持在内核中对非法地址的访问的检查。在内核态实现页表机制,这样内核访问异常地址也能及时报警。 +* 提高应用间的安全性(通过页机制实现隔离) +* 附带好处:应用程序地址空间可以相同,便于应用程序的开发 +### 用户程序 + +应用程序与上一章基本相同,只不过应用程序的地址空间起始位置应该相同。而且这一章需要将 ELF 链接进内核而不是二进制镜像。 + +特别的,可以设置访问其他应用程序地址空间或是访问内核地址空间的应用程序,内核会将其杀死。 + +在用户库使用 sbrk 申请动态分配空间而不是放在数据段中。 + +### 内核应完成功能 + +* 内核动态内存分配器(对于 Rust 而言,对于 C 仍可以考虑静态分配) +* 物理页帧分配器 +* 页表机制,特别是用户和内核地址空间的隔离(参考 xv6) +* ELF 解析和加载(在内核初始化的时候完成全部的地址空间创建和加载即可) +### 新增系统调用 + +* sys_sbrk:拓展或缩减当前应用程序的堆空间大小 +### 建议实现过程: + +1. 在Chapter1的基础上实现基本的物理内存管理机制,即连续内存的动态分配。 +2. 在Chapter1的基础上实现基本的页表机制。 +3. 然后再合并到Chapter3上。 +## Chapter5 进程及重要系统调用(优先级1) + +### 主要动机 + +应用以进程的方式进行运行,简化了应用开发的负担,OS也更好管理 + +引入重要的进程概念,整合Chapt1~4的内容抽象出进程,实现一系列相关机制及 syscall + +### 用户程序 + +用户终端 user_shell以及一些相应的测试 + +### 内核应完成功能 + +实现完整的子进程机制,初始化第一个用户进程 initproc。 + +### 新增系统调用 + +* sys_fork +* sys_wait(轮询版) +* sys_exec +* sys_getpid +* sys_yield更新 +* sys_exit 更新 +* sys_read:终端需要从串口读取命令 +## Chapter6 文件系统与进程间通信(优先级1) + +### 主要动机 + +进程之间需要进行一些协作。本章主要是通过管道进行通信。 + +同时,需要引入文件系统,并通过文件描述符来访问对应类型的 Unix 资源。 + +### 用户程序 + +简单的通过 fork 和子进程共享管道的测试; + +【可选】强化用户终端的功能,支持使用 | 进行管道连接。 + +### 内核应完成功能 + +实现管道。 + +将字符设备(标准输入/输出)和管道封装为通过文件描述符访问的文件。 + +### 新增系统调用 + +* sys_pipe:目前对于管道的 read/write 只需实现轮询版本。 +* sys_close:作用是关闭管道 +## Chapter7 数据持久化(优先级1) + +### 主要动机 + +实现数据持久化存储。 + +### 用户程序 + +多种不同大小的文件读写。 + +### 内核应完成功能 + +实现另一种在块设备上持久化存储的文件。 + +文件系统不需要实现目录。 + +### 新增系统调用 + +* sys_open:创建或打开一个文件 +# ----------------------------分割线------------------------------------------------- + +## Chapter6 单核同步互斥(优先级1,需要划分为单核/多核两部分) + +### 主要动机: + +应用之间需要在操作系统的帮助下有序共享资源(如串口,内存等)。 + +解释内核中已有的同步互斥问题,并实现阻塞机制。 + +### 内核应完成功能: + +实现死锁检测机制,并基于阻塞机制实现 sys_sleep 和 sys_wait 以及 sys_kill + +### 新增系统调用: + +sys_sleep 以及 sys_wait/sys_kill 的更新 + +### 章节分布: + +#### 基于原子指令实现自旋锁 + +* 讨论并发冲突的来源(单核/多核) +* 关中断/自旋/自旋关中断锁各自什么情况下能起作用,在课上还讲到一种获取锁失败直接 yield 的锁 +* 原子指令与内存一致性模型简介 +* 具体实现 +* 需要说明的是,课上的锁是针对于同一时刻只能有一个进程处于临界区之内。但是 Rust 风格的锁,也就是 Mutex 更加类似于一个管程(尽管 Rust 语言并没有这个概念),它用来保护一个数据结构,保证同一时间只有一个进程对于这个数据结构进行操作,自然保证了一致性。而 xv6 里面的锁只能保护临界区,相对而言对于数据结构一致性的保护就需要更加复杂的讨论。 +#### 死锁检测 + +#### 阻塞的同步原语:条件变量 + +简单讨论一下其他的同步原语。 + +* 课上提到的信号量和互斥量(后者是前者的特例)保护的都是某一个临界区 +#### 基于条件变量实现 sys_sleep + +#### 基于条件变量重新实现 sys_wait + +#### 更新 sys_kill 使得支持 kill 掉正在阻塞的进程 + +## ChapterX IPC(优先级1) + +### 主要动机: + +应用之间需要交换信息 + +### 内核应完成功能: + +* pipe +* shared mem +### 新增系统调用: + +## Chapter8 设备驱动(优先级2) + +### 主要动机: + +应用可以把I/O 设备用起来。 + +### 内核应完成功能: + +实现块设备驱动和串口驱动,理解同步/异步两种驱动实现方式 + +#### 背景知识:设备驱动、设备寄存器、轮询、中断 + +#### 设备树(可选) + +#### 实现 virtio_disk 块设备的块读写(同步+轮询风格) + +#### 实现 virtio_disk 块设备的块读写(异步+中断风格) + +#### 实现串口设备的异步输入和同步输出 + +* 参考 xv6,可以在内核里面维护一个 FIFO,这样即使串口本身没有 FIFO 也可以 +## Chapter9 Unix 资源:文件(优先级1) + +### 主要动机: + +应用可以通过单一接口(文件)访问磁盘来保存信息和访问其他外设 + +Unix 万物皆文件,将文件作为进程可以访问的内核资源单位 + +### 内核应完成功能: + +支持三种不同的 Unix 资源:字符设备(串口)、块设备(文件系统)、管道 + +### 新增系统调用: + +sys_open/sys_close + +### 背景知识:Unix 万物皆文件/进程对于文件的访问方式 + +#### file 抽象接口 + +* 支持 read/write 两种操作,表示 file 到地址空间中一块缓冲区的读写操作 +#### 字符设备路线 + +* 直接将串口设备驱动封装一下即可。 +#### 文件系统路线 + +* 分成多个子章节,等实现出来之后才知道怎么写 +#### 管道路线 + +* 一个非常经典的读者/写者问题。 +### ChapterX 虚存管理(优先级2) + +### 主要动机: + +提高应用执行的效率(侧重内存) + +- 支持物理内存不够的情况 + +- copy on write + +### 内核应完成功能: + +### 新增系统调用: + + + +### Chapter10 多核(可选) + +### 主要动机: + +提高应用执行的并行执行效率(侧重多处理器) + +### 内核应完成功能: + +### 新增系统调用: + +#### 多核启动与 IPI + +#### 多核调度 + +### Chapter11多核下的同步互斥(可选) + +### 主要动机: + +提高应用并行执行下的正确性(侧重多处理器) + +### 内核应完成功能: + +### 新增系统调用: + +#### 多核启动与 IPI + +#### 多核调度 + +## Appendix A Rust 语言快速入门与练习题 + +## Appendix B 常见构建工具的使用方法 + +比如 Makefile\ld 等。 + +## Appendix C RustSBI 与 Kendryte K210 兼容性设计 + +## 其他附录… + diff --git a/show.sh b/show.sh new file mode 100755 index 0000000000000000000000000000000000000000..64db054096d1928395eebea0d371364c5c1aacde --- /dev/null +++ b/show.sh @@ -0,0 +1,2 @@ +make html && google-chrome build/html/index.html + diff --git a/source/_static/dracula.css b/source/_static/dracula.css new file mode 100644 index 0000000000000000000000000000000000000000..30def141da24ebcdae0b47135ec7098916835234 --- /dev/null +++ b/source/_static/dracula.css @@ -0,0 +1,91 @@ +/* Dracula Theme v1.2.5 + * + * https://github.com/zenorocha/dracula-theme + * + * Copyright 2016, All rights reserved + * + * Code licensed under the MIT license + * http://zenorocha.mit-license.org + * + * @author Rob G + * @author Chris Bracco + * @author Zeno Rocha + */ + + .highlight .hll { background-color: #111110 } + .highlight { background: #282a36; color: #f8f8f2 } + .highlight .c { color: #6272a4 } /* Comment */ + .highlight .err { color: #f8f8f2 } /* Error */ + .highlight .g { color: #f8f8f2 } /* Generic */ + .highlight .k { color: #ff79c6 } /* Keyword */ + .highlight .l { color: #f8f8f2 } /* Literal */ + .highlight .n { color: #f8f8f2 } /* Name */ + .highlight .o { color: #ff79c6 } /* Operator */ + .highlight .x { color: #f8f8f2 } /* Other */ + .highlight .p { color: #f8f8f2 } /* Punctuation */ + .highlight .ch { color: #6272a4 } /* Comment.Hashbang */ + .highlight .cm { color: #6272a4 } /* Comment.Multiline */ + .highlight .cp { color: #ff79c6 } /* Comment.Preproc */ + .highlight .cpf { color: #6272a4 } /* Comment.PreprocFile */ + .highlight .c1 { color: #6272a4 } /* Comment.Single */ + .highlight .cs { color: #6272a4 } /* Comment.Special */ + .highlight .gd { color: #962e2f } /* Generic.Deleted */ + .highlight .ge { color: #f8f8f2; text-decoration: underline } /* Generic.Emph */ + .highlight .gr { color: #f8f8f2 } /* Generic.Error */ + .highlight .gh { color: #f8f8f2; font-weight: bold } /* Generic.Heading */ + .highlight .gi { color: #f8f8f2; font-weight: bold } /* Generic.Inserted */ + .highlight .go { color: #44475a } /* Generic.Output */ + .highlight .gp { color: #f8f8f2 } /* Generic.Prompt */ + .highlight .gs { color: #f8f8f2 } /* Generic.Strong */ + .highlight .gu { color: #f8f8f2; font-weight: bold } /* Generic.Subheading */ + .highlight .gt { color: #f8f8f2 } /* Generic.Traceback */ + .highlight .kc { color: #ff79c6 } /* Keyword.Constant */ + .highlight .kd { color: #8be9fd; font-style: italic } /* Keyword.Declaration */ + .highlight .kn { color: #ff79c6 } /* Keyword.Namespace */ + .highlight .kp { color: #ff79c6 } /* Keyword.Pseudo */ + .highlight .kr { color: #ff79c6 } /* Keyword.Reserved */ + .highlight .kt { color: #8be9fd } /* Keyword.Type */ + .highlight .ld { color: #f8f8f2 } /* Literal.Date */ + .highlight .m { color: #bd93f9 } /* Literal.Number */ + .highlight .s { color: #f1fa8c } /* Literal.String */ + .highlight .na { color: #50fa7b } /* Name.Attribute */ + .highlight .nb { color: #8be9fd; font-style: italic } /* Name.Builtin */ + .highlight .nc { color: #50fa7b } /* Name.Class */ + .highlight .no { color: #f8f8f2 } /* Name.Constant */ + .highlight .nd { color: #f8f8f2 } /* Name.Decorator */ + .highlight .ni { color: #f8f8f2 } /* Name.Entity */ + .highlight .ne { color: #f8f8f2 } /* Name.Exception */ + .highlight .nf { color: #50fa7b } /* Name.Function */ + .highlight .nl { color: #8be9fd; font-style: italic } /* Name.Label */ + .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ + .highlight .nx { color: #f8f8f2 } /* Name.Other */ + .highlight .py { color: #f8f8f2 } /* Name.Property */ + .highlight .nt { color: #ff79c6 } /* Name.Tag */ + .highlight .nv { color: #8be9fd; font-style: italic } /* Name.Variable */ + .highlight .ow { color: #ff79c6 } /* Operator.Word */ + .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ + .highlight .mb { color: #bd93f9 } /* Literal.Number.Bin */ + .highlight .mf { color: #bd93f9 } /* Literal.Number.Float */ + .highlight .mh { color: #bd93f9 } /* Literal.Number.Hex */ + .highlight .mi { color: #bd93f9 } /* Literal.Number.Integer */ + .highlight .mo { color: #bd93f9 } /* Literal.Number.Oct */ + .highlight .sa { color: #f1fa8c } /* Literal.String.Affix */ + .highlight .sb { color: #f1fa8c } /* Literal.String.Backtick */ + .highlight .sc { color: #f1fa8c } /* Literal.String.Char */ + .highlight .dl { color: #f1fa8c } /* Literal.String.Delimiter */ + .highlight .sd { color: #f1fa8c } /* Literal.String.Doc */ + .highlight .s2 { color: #f1fa8c } /* Literal.String.Double */ + .highlight .se { color: #f1fa8c } /* Literal.String.Escape */ + .highlight .sh { color: #f1fa8c } /* Literal.String.Heredoc */ + .highlight .si { color: #f1fa8c } /* Literal.String.Interpol */ + .highlight .sx { color: #f1fa8c } /* Literal.String.Other */ + .highlight .sr { color: #f1fa8c } /* Literal.String.Regex */ + .highlight .s1 { color: #f1fa8c } /* Literal.String.Single */ + .highlight .ss { color: #f1fa8c } /* Literal.String.Symbol */ + .highlight .bp { color: #f8f8f2; font-style: italic } /* Name.Builtin.Pseudo */ + .highlight .fm { color: #50fa7b } /* Name.Function.Magic */ + .highlight .vc { color: #8be9fd; font-style: italic } /* Name.Variable.Class */ + .highlight .vg { color: #8be9fd; font-style: italic } /* Name.Variable.Global */ + .highlight .vi { color: #8be9fd; font-style: italic } /* Name.Variable.Instance */ + .highlight .vm { color: #8be9fd; font-style: italic } /* Name.Variable.Magic */ + .highlight .il { color: #bd93f9 } /* Literal.Number.Integer.Long */ diff --git a/source/_static/my_style.css b/source/_static/my_style.css new file mode 100644 index 0000000000000000000000000000000000000000..8aa6c288f61f3d5df3dbc4feeefb21eba4ecda31 --- /dev/null +++ b/source/_static/my_style.css @@ -0,0 +1,3 @@ +.wy-nav-content { + max-width: 1200px !important; +} diff --git a/source/appendix-a/index.rst b/source/appendix-a/index.rst index dd96e923645afc717e9cb5fd807da0f3dcf9bda5..00ad67491d49052e43ad87e5c0b3a7fe1649ae25 100644 --- a/source/appendix-a/index.rst +++ b/source/appendix-a/index.rst @@ -1,10 +1,45 @@ -Rust 快速入门 +附录 A:Rust 系统编程入门 ============================= .. toctree:: :hidden: :maxdepth: 4 + +.. .. note:: + +.. **Rust 语法卡片:外部符号引用** + +.. extern "C" 可以引用一个外部的 C 函数接口(这意味着调用它的时候要遵从目标平台的 C 语言调用规范)。但我们这里只是引用位置标志 +.. 并将其转成 usize 获取它的地址。由此可以知道 ``.bss`` 段两端的地址。 + +.. **Rust 语法卡片:迭代器与闭包** + +.. 代码第 7 行用到了 Rust 的迭代器与闭包的语法,它们在很多情况下能够提高开发效率。如读者感兴趣的话也可以将其改写为等价的 for +.. 循环实现。 + +.. .. _term-raw-pointer: +.. .. _term-dereference: +.. .. warning:: + +.. **Rust 语法卡片:Unsafe** + +.. 代码第 8 行,我们将 ``.bss`` 段内的一个地址转化为一个 **裸指针** (Raw Pointer),并将它指向的值修改为 0。这在 C 语言中是 +.. 一种司空见惯的操作,但在 Rust 中我们需要将他包裹在 unsafe 块中。这是因为,Rust 认为对于裸指针的 **解引用** (Dereference) +.. 是一种 unsafe 行为。 + +.. 相比 C 语言,Rust 进行了更多的语义约束来保证安全性(内存安全/类型安全/并发安全),这在编译期和运行期都有所体现。但在某些时候, +.. 尤其是与底层硬件打交道的时候,在 Rust 的语义约束之内没法满足我们的需求,这个时候我们就需要将超出了 Rust 语义约束的行为包裹 +.. 在 unsafe 块中,告知编译器不需要对它进行完整的约束检查,而是由程序员自己负责保证它的安全性。当代码不能正常运行的时候,我们往往也是 +.. 最先去检查 unsafe 块中的代码,因为它没有受到编译器的保护,出错的概率更大。 + +.. C 语言中的指针相当于 Rust 中的裸指针,它无所不能但又太过于灵活,程序员对其不谨慎的使用常常会引起很多内存不安全问题,最常见的如 +.. 悬垂指针和多次回收的问题,Rust 编译器没法确认程序员对它的使用是否安全,因此将其划到 unsafe Rust 的领域。在 safe Rust 中,我们 +.. 有引用 ``&/&mut`` 以及各种功能各异的智能指针 ``Box/RefCell/Rc`` 可以使用,只要按照 Rust 的规则来使用它们便可借助 +.. 编译器在编译期就解决很多潜在的内存不安全问题。 + +- `OS Tutorial Summer of Code 2020:Rust系统编程入门指导 `_ - `Stanford 新开的一门很值得学习的 Rust 入门课程 `_ - `一份简单的 Rust 入门介绍 `_ -- `《RustOS Guide》中的 Rust 介绍部分 `_ \ No newline at end of file +- `《RustOS Guide》中的 Rust 介绍部分 `_ +- `一份简单的Rust宏编程新手指南 `_ \ No newline at end of file diff --git a/source/appendix-b/index.rst b/source/appendix-b/index.rst index 59335fddbea8eded73dceb44d3ab3297de4f9b45..0fe7f5105d0c88187d94c70647767d92064a36ad 100644 --- a/source/appendix-b/index.rst +++ b/source/appendix-b/index.rst @@ -1,7 +1,368 @@ -常见工具的使用方法 +附录 B:常见工具的使用方法 ======================================== .. toctree:: :hidden: :maxdepth: 4 - \ No newline at end of file + + + +分析可执行文件 +------------------------ + +对于Rust编译器生成的执行程序,可通过各种有效工具进行分析。如果掌握了对这些工具的使用,那么在后续的开发工作中,对碰到的各种奇怪问题就进行灵活处理和解决了。 +我们以Rust编译生成的一个简单的“Hello, world”应用执行程序为分析对象,看看如何进行分析。 + +让我们先来通过 ``file`` 工具看看最终生成的可执行文件的格式: + +.. code-block:: console + + $ cargo new os + $ cd os; cargo build + Compiling os v0.1.0 (/tmp/os) + Finished dev [unoptimized + debuginfo] target(s) in 0.26s + + $ file target/debug/os + target/debug/os: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, + interpreter /lib64/ld-linux-x86-64.so.2, ...... + + $ + +.. _term-elf: +.. _term-metadata: + +从中可以看出可执行文件的格式为 **可执行和链接格式** (Executable and Linkable Format, ELF),硬件平台是 x86-64。在 ELF 文件中, +除了程序必要的代码、数据段(它们本身都只是一些二进制的数据)之外,还有一些 **元数据** (Metadata) 描述这些段在地址空间中的位置和在 +文件中的位置以及一些权限控制信息,这些元数据只能放在代码、数据段的外面。 + +rust-readobj +^^^^^^^^^^^^^^^^^^^^^^^ + +我们可以通过二进制工具 ``rust-readobj`` 来看看 ELF 文件中究竟包含什么内容,输入命令: + +.. code-block:: console + + $ rust-readobj -all target/debug/os + +首先可以看到一个 ELF header,它位于 ELF 文件的开头: + +.. code-block:: objdump + :linenos: + :emphasize-lines: 8,19,20,21,24,25,26,27 + + File: target/debug/os + Format: elf64-x86-64 + Arch: x86_64 + AddressSize: 64bit + LoadName: + ElfHeader { + Ident { + Magic: (7F 45 4C 46) + Class: 64-bit (0x2) + DataEncoding: LittleEndian (0x1) + FileVersion: 1 + OS/ABI: SystemV (0x0) + ABIVersion: 0 + Unused: (00 00 00 00 00 00 00) + } + Type: SharedObject (0x3) + Machine: EM_X86_64 (0x3E) + Version: 1 + Entry: 0x5070 + ProgramHeaderOffset: 0x40 + SectionHeaderOffset: 0x32D8D0 + Flags [ (0x0) + ] + HeaderSize: 64 + ProgramHeaderEntrySize: 56 + ProgramHeaderCount: 12 + SectionHeaderEntrySize: 64 + SectionHeaderCount: 42 + StringTableSectionIndex: 41 + } + ...... + +.. _term-magic: + +- 第 8 行是一个称之为 **魔数** (Magic) 独特的常数,存放在 ELF header 的一个固定位置。当加载器将 ELF 文件加载到内存之前,通常会查看 + 该位置的值是否正确,来快速确认被加载的文件是不是一个 ELF 。 +- 第 19 行给出了可执行文件的入口点为 ``0x5070`` 。 +- 从 20-21 行中,我们可以知道除了 ELF header 之外,还有另外两种不同的 header,分别称为 program header 和 section header, + 它们都有多个。ELF header 中给出了其他两种header 的大小、在文件中的位置以及数目。 +- 从 24-27 行中,可以看到有 12 个不同的 program header,它们从文件的 0x40 字节偏移处开始,每个 56 字节; + 有64个section header,它们从文件的 0x2D8D0 字节偏移处开始,每个 64 字节; + + +有多个不同的 section header,下面是个具体的例子: + +.. code-block:: objdump + + ...... + Section { + Index: 14 + Name: .text (157) + Type: SHT_PROGBITS (0x1) + Flags [ (0x6) + SHF_ALLOC (0x2) + SHF_EXECINSTR (0x4) + ] + Address: 0x5070 + Offset: 0x5070 + Size: 208067 + Link: 0 + Info: 0 + AddressAlignment: 16 + EntrySize: 0 + } + + +每个 section header 则描述一个段的元数据。 + +其中,我们看到了代码段 ``.text`` 需要被加载到地址 ``0x5070`` ,大小 208067 字节,。 +它们分别由元数据的字段 Offset、 Size 和 Address 给出。。 + +我们还能够看到程序中的符号表: + +.. code-block:: + + Symbol { + Name: _start (37994) + Value: 0x5070 + Size: 47 + Binding: Global (0x1) + Type: Function (0x2) + Other: 0 + Section: .text (0xE) + } + Symbol { + Name: main (38021) + Value: 0x51A0 + Size: 47 + Binding: Global (0x1) + Type: Function (0x2) + Other: 0 + Section: .text (0xE) + } + +里面包括了我们写的 ``main`` 函数的地址以及用户态执行环境的起始地址 ``_start`` 函数的地址。 + +因此,从 ELF header 中可以看出,ELF 中的内容按顺序应该是: + +- ELF header +- 若干个 program header +- 程序各个段的实际数据 +- 若干的 section header + + +rust-objdump +^^^^^^^^^^^^^^^^^^^^^^^ + +如果想了解正常的ELF文件的具体指令内容,可以通过 ``rust-objdump`` 工具反汇编ELF文件得到: + +.. code-block:: console + + $ rust-objdump -all target/debug/os + +具体结果如下: + +.. code-block:: objdump + + 505b: e9 c0 ff ff ff jmp 0x5020 <.plt> + + Disassembly of section .plt.got: + + 0000000000005060 <.plt.got>: + 5060: ff 25 5a 3f 04 00 jmpq *278362(%rip) # 48fc0 <_GLOBAL_OFFSET_TABLE_+0x628> + 5066: 66 90 nop + + Disassembly of section .text: + + 0000000000005070 <_start>: + 5070: f3 0f 1e fa endbr64 + 5074: 31 ed xorl %ebp, %ebp + 5076: 49 89 d1 movq %rdx, %r9 + 5079: 5e popq %rsi + 507a: 48 89 e2 movq %rsp, %rdx + 507d: 48 83 e4 f0 andq $-16, %rsp + 5081: 50 pushq %rax + 5082: 54 pushq %rsp + 5083: 4c 8d 05 86 2c 03 00 leaq 208006(%rip), %r8 # 37d10 <__libc_csu_fini> + 508a: 48 8d 0d 0f 2c 03 00 leaq 207887(%rip), %rcx # 37ca0 <__libc_csu_init> + 5091: 48 8d 3d 08 01 00 00 leaq 264(%rip), %rdi # 51a0
+ 5098: ff 15 d2 3b 04 00 callq *277458(%rip) # 48c70 <_GLOBAL_OFFSET_TABLE_+0x2d8> + ...... + 00000000000051a0
: + 51a0: 48 83 ec 18 subq $24, %rsp + 51a4: 8a 05 db 7a 03 00 movb 228059(%rip), %al # 3cc85 <__rustc_debug_gdb_scripts_section__> + 51aa: 48 63 cf movslq %edi, %rcx + 51ad: 48 8d 3d ac ff ff ff leaq -84(%rip), %rdi # 5160 <_ZN2os4main17h717a6a6e05a70248E> + 51b4: 48 89 74 24 10 movq %rsi, 16(%rsp) + 51b9: 48 89 ce movq %rcx, %rsi + 51bc: 48 8b 54 24 10 movq 16(%rsp), %rdx + 51c1: 88 44 24 0f movb %al, 15(%rsp) + 51c5: e8 f6 00 00 00 callq 0x52c0 <_ZN3std2rt10lang_start17hc258028f546a93a1E> + 51ca: 48 83 c4 18 addq $24, %rsp + 51ce: c3 retq + 51cf: 90 nop + ...... + +从上面的反汇编结果,我们可以看到用户态执行环境的入口函数 ``_start`` 以及应用程序的主函数 ``main`` 的地址和具体汇编代码内容。 + + +rust-objcopy +^^^^^^^^^^^^^^^^^^^^^^^ + +当前的ELF执行程序有许多与执行无直接关系的信息(如调试信息等),可以通过 ``rust-objcopy`` 工具来清除。 + +.. code-block:: console + + $ rust-objcopy --strip-all target/debug/os target/debug/os.bin + $ ls -l target/debug/os* + -rwxrwxr-x 2 chyyuu chyyuu 3334992 1月 19 22:26 target/debug/os + -rwxrwxr-x 1 chyyuu chyyuu 297200 1月 19 22:59 target/debug/os.bin + + $ ./target/debug/os.bin + Hello, world! + +可以看到,经过处理的ELF文件 ``os.bin`` 在文件长度上大大减少了,但也能正常执行。 + +另外,当将程序加载到内存的时候,对于每个 program header 所指向的区域,我们需要将对应的数据从文件复制到内存中。这就需要解析 ELF 的元数据 +才能知道数据在文件中的位置以及即将被加载到内存中的位置。但如果我们不需要从 ELF 中解析元数据就知道程序的内存布局 +(这个内存布局是我们按照需求自己指定的),我们可以手动完成加载任务。 + +具体的做法是利用 ``rust-objcopy`` 工具删除掉 ELF 文件中的 +所有 header 只保留各个段的实际数据得到一个没有任何符号的纯二进制镜像文件: + +.. code-block:: console + + $ rust-objcopy --strip-all target/debug/os -O binary target/debug/os.bin + + + +这样就生成了一个没有任何符号的纯二进制镜像文件。由于缺少了必要的元数据,我们的 ``file`` 工具也没有办法 +对它完成解析了。而后,我们可直接将这个二进制镜像文件手动载入到内存中合适位置即可。 + + + +qemu 平台上可执行文件和二进制镜像的生成流程 +---------------------------------------------- + + + +make & Makefile +^^^^^^^^^^^^^^^^^^^^^^^ + +首先我们还原一下可执行文件和二进制镜像的生成流程: + +.. code-block:: makefile + + # os/Makefile + TARGET := riscv64gc-unknown-none-elf + MODE := release + KERNEL_ELF := target/$(TARGET)/$(MODE)/os + KERNEL_BIN := $(KERNEL_ELF).bin + + $(KERNEL_BIN): kernel + @$(OBJCOPY) $(KERNEL_ELF) --strip-all -O binary $@ + + kernel: + @cargo build --release + +这里可以看出 ``KERNEL_ELF`` 保存最终可执行文件 ``os`` 的路径,而 ``KERNEL_BIN`` 保存只保留各个段数据的二进制镜像文件 ``os.bin`` +的路径。目标 ``kernel`` 直接通过 ``cargo build`` 以 release 模式最终可执行文件,目标 ``KERNEL_BIN`` 依赖于目标 ``kernel``,将 +可执行文件通过 ``rust-objcopy`` 工具加上适当的配置移除所有的 header 和符号得到二进制镜像。 + +我们可以通过 ``make run`` 直接在 qemu 上运行我们的应用程序,qemu 是一个虚拟机,它完整的模拟了一整套硬件平台,就像是一台真正的计算机 +一样,我们来看运行 qemu 的具体命令: + +.. code-block:: makefile + :linenos: + :emphasize-lines: 11,12,13,14,15 + + KERNEL_ENTRY_PA := 0x80020000 + + BOARD ?= qemu + SBI ?= rustsbi + BOOTLOADER := ../bootloader/$(SBI)-$(BOARD).bin + + run: run-inner + + run-inner: build + ifeq ($(BOARD),qemu) + @qemu-system-riscv64 \ + -machine virt \ + -nographic \ + -bios $(BOOTLOADER) \ + -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) + else + @cp $(BOOTLOADER) $(BOOTLOADER).copy + @dd if=$(KERNEL_BIN) of=$(BOOTLOADER).copy bs=128K seek=1 + @mv $(BOOTLOADER).copy $(KERNEL_BIN) + @sudo chmod 777 $(K210-SERIALPORT) + python3 $(K210-BURNER) -p $(K210-SERIALPORT) -b 1500000 $(KERNEL_BIN) + miniterm --eol LF --dtr 0 --rts 0 --filter direct $(K210-SERIALPORT) 115200 + endif + + +qemu +^^^^^^^^^^^^^^^^^^^^^^^ + +注意其中高亮部分给出了传给 qemu 的参数。 + +- ``-machine`` 告诉 qemu 使用预设的硬件配置。在整个项目中我们将一直沿用该配置。 +- ``-bios`` 告诉 qemu 使用我们放在 ``bootloader`` 目录下的预编译版本作为 bootloader。 +- ``-device`` 则告诉 qemu 将二进制镜像加载到内存指定的位置。 + +可以先输入 Ctrl+A ,再输入 X 来退出 qemu 终端。 + +.. warning:: + + **FIXME: 使用 GDB 跟踪 qemu 的运行状态** + + +k210 平台上可执行文件和二进制镜像的生成流程 +------------------------------------------------------- + +对于 k210 平台来说,只需要将 maix 系列开发板通过数据线连接到 PC,然后 ``make run BOARD=k210`` 即可。从 Makefile 中来看: + +.. code-block:: makefile + :linenos: + :emphasize-lines: 13,16,17 + + K210-SERIALPORT = /dev/ttyUSB0 + K210-BURNER = ../tools/kflash.py + + run-inner: build + ifeq ($(BOARD),qemu) + @qemu-system-riscv64 \ + -machine virt \ + -nographic \ + -bios $(BOOTLOADER) \ + -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) + else + @cp $(BOOTLOADER) $(BOOTLOADER).copy + @dd if=$(KERNEL_BIN) of=$(BOOTLOADER).copy bs=128K seek=1 + @mv $(BOOTLOADER).copy $(KERNEL_BIN) + @sudo chmod 777 $(K210-SERIALPORT) + python3 $(K210-BURNER) -p $(K210-SERIALPORT) -b 1500000 $(KERNEL_BIN) + miniterm --eol LF --dtr 0 --rts 0 --filter direct $(K210-SERIALPORT) 115200 + endif + +在构建目标 ``run-inner`` 的时候,根据平台 ``BOARD`` 的不同,启动运行的指令也不同。当我们传入命令行参数 ``BOARD=k210`` 时,就会进入下面 +的分支。 + +- 第 13 行我们使用 ``dd`` 工具将 bootloader 和二进制镜像拼接到一起,这是因为 k210 平台的写入工具每次只支持写入一个文件,所以我们只能 + 将二者合并到一起一并写入 k210 的内存上。这样的参数设置可以保证 bootloader 在合并后文件的开头,而二进制镜像在文件偏移量 0x20000 的 + 位置处。有兴趣的读者可以输入命令 ``man dd`` 查看关于工具 ``dd`` 的更多信息。 +- 第 16 行我们使用烧写工具 ``K210-BURNER`` 将合并后的镜像烧写到 k210 开发板的内存的 ``0x80000000`` 地址上。 + 参数 ``K210-SERIALPORT`` 表示当前 OS 识别到的 k210 开发板的串口设备名。在 Ubuntu 平台上一般为 ``/dev/ttyUSB0``。 +- 第 17 行我们打开串口终端和 k210 开发板进行通信,可以通过键盘向 k210 开发板发送字符并在屏幕上看到 k210 开发板的字符输出。 + +可以输入 Ctrl+] 退出 miniterm。 + + +其他工具和文件格式说明的参考 +------------------------------------------------------- + +- `链接脚本(Linker Scripts)语法和规则解析(翻译自官方手册) `_ +- `Make 命令教程 `_ diff --git a/source/appendix-c/index.rst b/source/appendix-c/index.rst index b54a61b8e3aa00bf296f87dfe7d2e91eb2e597e7..4a5d3ee92d03a1de066de8183198a108cd2c709c 100644 --- a/source/appendix-c/index.rst +++ b/source/appendix-c/index.rst @@ -1,6 +1,18 @@ -深入机器模式:RustSBI +附录 C:深入机器模式:RustSBI ================================================= .. toctree:: :hidden: - :maxdepth: 4 \ No newline at end of file + :maxdepth: 4 + +RISC-V指令集的SBI标准规定了类Unix操作系统之下的运行环境规范。这个规范拥有多种实现,RustSBI是它的一种实现。 + +RISC-V架构中,存在着定义于操作系统之下的运行环境。这个运行环境不仅将引导启动RISC-V下的操作系统, 还将常驻后台,为操作系统提供一系列二进制接口,以便其获取和操作硬件信息。 RISC-V给出了此类环境和二进制接口的规范,称为“操作系统二进制接口”,即“SBI”。 + +SBI的实现是在M模式下运行的特定于平台的固件,它将管理S、U等特权上的程序或通用的操作系统。 + +RustSBI项目发起于鹏城实验室的“rCore代码之夏-2020”活动,它是完全由Rust语言开发的SBI实现。 现在它能够在支持的RISC-V设备上运行rCore教程和其它操作系统内核。 + +RustSBI项目的目标是,制作一个从固件启动的最小Rust语言SBI实现,为可能的复杂实现提供参考和支持。 RustSBI也可以作为一个库使用,帮助更多的SBI开发者适配自己的平台,以支持更多处理器核和片上系统。 + +当前项目实现源码:https://github.com/luojia65/rustsbi \ No newline at end of file diff --git a/source/appendix-d/1asm.rst b/source/appendix-d/1asm.rst new file mode 100644 index 0000000000000000000000000000000000000000..c20c8c82e1b7c9d8d7d9badbaaed0dc4761ba2d0 --- /dev/null +++ b/source/appendix-d/1asm.rst @@ -0,0 +1,7 @@ +RISCV汇编相关 +========================= + +- `RISC-V Assembly Programmer's Manual `_ +- `RISC-V Low-level Test Suits `_ +- `CoreMark®-PRO comprehensive, advanced processor benchmark `_ +- `riscv-tests的使用 `_ \ No newline at end of file diff --git a/source/appendix-d/2rv.rst b/source/appendix-d/2rv.rst new file mode 100644 index 0000000000000000000000000000000000000000..a8e86377aa06a61a26a3ddb81ae883b949258ad8 --- /dev/null +++ b/source/appendix-d/2rv.rst @@ -0,0 +1,22 @@ +RISCV硬件相关 +========================= + +Quick Reference +------------------- +- `Registers & ABI `_ +- `Interrupt `_ +- `ISA & Extensions `_ +- `Toolchain `_ +- `Control and Status Registers (CSRs) `_ +- `Accessing CSRs `_ +- `Assembler & Instructions `_ + +ISA +------------------------ + +- `User-Level ISA, Version 1.12 `_ +- `4 Supervisor-Level ISA, Version 1.12 `_ +- `Vector Extension `_ +- `RISC-V Bitmanip Extension `_ +- `External Debug `_ +- `ISA Resources `_ \ No newline at end of file diff --git a/docs/_sources/appendix-c/index.rst.txt b/source/appendix-d/index.rst similarity index 55% rename from docs/_sources/appendix-c/index.rst.txt rename to source/appendix-d/index.rst index b54a61b8e3aa00bf296f87dfe7d2e91eb2e597e7..717d1f3263dd7b6ba01a6653bcb69d6735a8bef9 100644 --- a/docs/_sources/appendix-c/index.rst.txt +++ b/source/appendix-d/index.rst @@ -1,6 +1,9 @@ -深入机器模式:RustSBI +附录 D:RISC-V相关信息 ================================================= .. toctree:: :hidden: - :maxdepth: 4 \ No newline at end of file + :maxdepth: 4 + + 1asm + 2rv \ No newline at end of file diff --git a/source/chapter0/0intro.rst b/source/chapter0/0intro.rst new file mode 100644 index 0000000000000000000000000000000000000000..cc8c0353523f6e206243a8cebdbe3cef3f694af8 --- /dev/null +++ b/source/chapter0/0intro.rst @@ -0,0 +1,46 @@ +为何要写这本操作系统书 +================================================== + +现在国内外已有一系列优秀的操作系统教材,例如 William Stallings 的《Operating Systems Internals and Design Principles》,Avi Silberschatz 、 Peter Baer Galvin 和 Greg Gagne 的《Operating System Concepts》, +Remzi H. Arpaci-Dusseau 和 Andrea C. Arpaci-Dusseau 的《Operating Systems: Three Easy Pieces》等。 +然而,从我们自 2000 年以来的教学实践来看,某些经典教材对操作系统的概念和原理很重视,但还有如下一些问题有待改进: + +- 原理与实践脱节:缺乏在操作系统的概念/原理与操作系统的设计/实现之间建立联系的桥梁,导致学生发现操作系统实现相关的实验 + 与操作系统的概念相比,有较大的鸿沟。 +- 缺少历史发展的脉络:操作系统的概念和原理是从实际操作系统设计与实现过程中,从无到有逐步演进而产生的,有其发展的历史渊源 + 和规律。但目前的大部分教材只提及当前主流操作系统的概念和原理,有“凭空出现”的感觉,学生并不知道这些内容出现的前因后果。 +- 忽视硬件细节或用复杂硬件:很多教材忽视或抽象硬件细节,使得操作系统概念难以落地。部分教材把 x86 作为操作系统实验的硬件 + 参考平台,缺乏对当前快速发展的 RISC-V 等体系结构的实验支持,使得学生在操作系统实验中可能需要花较大代价了解相对繁杂的 x86 硬件细节,影响操作系统实验的效果。 + +这些问题增加了学生学习和掌握操作系统的难度。我们想通过尝试解决上面三个问题,来缓解学生学习操作系统的压力,提升他们的兴趣,让他们能够在一个学期内比较好地掌握操作系统。为应对“原理与实践脱节”的问题,我们强调了实践先行,实践引领原理的理念。MIT 教授 Frans Kaashoek 等师生设计实现了基于 UNIX v6 的 xv6 教学操作系统用于每年的本科操作系统课的实验中,并在课程讲解中把原理和实验结合起来,在国际上得到了广泛的认可。这些都给了我们很好的启发,经过十多年的实践,对一个计算机专业的本科生而言,设计实现一个操作系统(包括CPU)有挑战但可行,前提是实际操作系统要小巧并能体现操作系统的核心思想。这样就能够让学生加深对操作系统原理和概念的理解,能让操作系统原理和概念落地。 + +为应对“缺少历史发展的脉络”的问题,我们重新设计操作系统实验和教学内容,按照操作系统的历史发展过程来建立多个相对独立的小实验,每个实验体现了操作系统的一个微缩的历史,并从中归纳总结出操作系统相关的概念与原理,并在教学中引导学生理解这些概念和原理是如何一步一步演进的。 + +为应对“忽视硬件细节或用复杂硬件”的问题,我们在硬件(x86, ARM, MIPS, RISC-V 等)和编程语言(C, C++, Go, Rust 等)选择方面进行了多年尝试。在 2017 年引入了 RISC-V 架构作为操作系统实验的硬件环境,在 2018 年引入 Rust 编程语言作为开发操作系统的编程语言,使得学生以相对较小的开发和调试代价能够用 Rust 语言编写运行在 RISC-V 上的操作系统。我们简化了形象化、可视化操作系统的概念和原理的过程,目的是让学生可以把操作系统的概念和原理直接对应到程序代码、硬件规范和操作系统的实际执行中,加强学生对操作系统内涵的实际体验和感受。 + +所以本书的目标是以简洁的 RISC-V 架构为底层硬件基础,根据上层应用从小到大的需求,按 OS 发展的历史脉络,逐步讲解如何设计并实现满足这些需求的“从小到大”的多个“小”操作系统,并在设计实现操作系统的过程中,逐步解析操作系统各种概念与原理的知识点,对应的做到有“理”可循和有“码”可查,最终让读者通过主动的操作系统设计与实现来深入地掌握操作系统的概念与原理。 + +在具体撰写过程中,第零章是对操作系统的一个概述,让读者对操作系统的历史、定义、特征等概念上有一个大致的了解。后面的每个章节体现了操作系统的一个微缩的历史发展过程,即从对应用由简到繁的支持的角度出发,每章会讲解如何设计一个可运行应用的操作系统,满足应用的阶段性需求。从而读者可以通过对应配套的操作系统设计实验,了解如何从一个微不足道的“小”操作系统,根据应用需求,添加或增强操作系统功能,逐步形成一个类似 UNIX 的相对完善的“小”操作系统。每一步都小到足以让人感觉到易于掌控,而在每一步结束时,你都有一个可以工作的“小”操作系统。另外,通过足够详尽的测试程序,可以随时验证读者实现的操作系统在每次更新后是否正常工作。由于实验的代码规模和实现复杂度在一个逐步递增的可控范围内,读者可以结合对应于操作系统设计实验的进一步的原理讲解,来建立操作系统概念原理和实际实现的对应关系,从而能够通过操作系统实验的实践过程来加强对理论概念的理解,通过理论概念来进一步指导操作系统实验的实现与改进。 + +在你开始阅读与实践本书讲解的内容之前,你需要决定用什么编程语言来完成操作系统实验。你可以用任何你喜欢的编程语言和你喜欢的CPU上来实现操作系统。我们推荐的编程语言是 Rust ,我们推荐的架构是 RISC-V。 + +.. + chyyuu:有一个比较大的ascii图,画出我们做出的各种OSes。 + + +.. note:: + + **目前常见的操作系统内核都是基于 C 语言的,为何要推荐 Rust 语言?** + + - 事实上, C 语言就是为写 UNIX 而诞生的。Dennis Ritchie 和 KenThompson 没有期望设计一种新语言能帮助高效简洁地开发复杂的应用业务逻辑,只是希望用一种简洁的方式来代替难以使用的汇编语言抽象出计算机的行为,便于编写控制计算机硬件的操作系统。 + - C 语言的指针既是天使又是魔鬼。它灵活且易于使用,但语言本身几乎不保证安全性,且缺少有效的并发支持。这导致内存和并发漏洞成为当前基于 C 开发的主流操作系统的噩梦。 + - Rust 语言具有与 C 一样的硬件控制能力,且大大强化了安全编程。从某种角度上看,新出现的 Rust 语言的核心目标是解决 C 的短板,取代 C 。所以用 Rust 写 OS 具有很好的开发和运行的体验。 + - 用 Rust 写 OS 的代价仅仅是学会用 Rust 编程。 + + **目前常见的指令集架构是 x86 和 ARM ,为何要推荐 RISC-V ?** + + - 目前为止最常见的架构是 x86 和 ARM ,他们已广泛应用在服务器,台式机,移动终端和很多嵌入式系统中。它们需要支持非常多的软件系统和应用需求,导致它们越来越复杂。 + - x86 后向兼容的策略确保了它的江湖地位,但导致其丢不掉很多已经比较过时的硬件设计,让操作系统疲于适配这些硬件特征。 + - x86 和 ARM 在商业上都很成功,其广泛使用使得其 CPU 硬件逻辑越来越复杂,且不够开放,不能改变,不是开源的,提高了操作系统开发者的学习难度。 + - 从某种角度上看,新出现的 RISC-V 的核心目标是灵活适应未来的 AIoT 场景,保证基本功能,提供可配置的扩展功能。其开源特征使得学生都可以方便地设计一个 RISC-V CPU。 + - 写面向 RISC-V 的 OS 的代价仅仅是你了解 RISC-V 的 Supervisor 特权模式,知道 OS 在 Supervisor 特权模式下的控制能力。 diff --git a/source/chapter0/1what-is-os.rst b/source/chapter0/1what-is-os.rst index b596cf927d268b88bf4fc0e358eef31bab4a5cb2..73b00bd3ed724c86c7e58c66749db3c88b45fd31 100644 --- a/source/chapter0/1what-is-os.rst +++ b/source/chapter0/1what-is-os.rst @@ -8,61 +8,111 @@ 站在一万米的代码空间维度看 ---------------------------------- -现在的操作系统是一个复杂的系统软件,比如Linux操作系统达到了千万行的C源代码量级。在学习操作系统的初期,我们没有必要去分析了解这样一个规模的软件。但这样的软件也是有其特有的一些特征。如果我们站在一万米的高空来看操作系统,可以发现操作系统这个软件干的事主要有两件:一是向下管理计算机硬件和各种外设,而是向上给应用软件提供各种服务帮助。这样描述还太简单了一些,我们可对其进一步描述:操作系统是一个可以管理CPU、内存和各种外设,并管理和服务应用软件的软件。为了完成这些工作,操作系统需要知道如何与硬件打交道,如何更好地面向应用软件做好服务,这就有一系列操作系统的理论,抽象,设计等来支持如何做和做得好的需求。 +现在的通用操作系统是一个复杂的系统软件,比如 Linux 操作系统达到了千万行的 C 源代码量级。在学习操作系统的初期,我们没有必要去分析了解这样大规模的软件。但这样的软件也是有其特有的一些特征。首先,它称为系统软件,简单理解它就是在一个计算机系统范围内使用的软件,管的是整个计算机系统。如果这样来看,一个编辑软件,如 Vi Emacs 就不能算了。 +而在计算机中安装的 Rust 标准库(类似的有 C 标准库 libc 等)可以算是一个。 +如果我们站在一万米的高空来看 :ref:`操作系统 ` ,可以发现操作系统这个软件干的事主要有两件:一是向下管理计算机硬件和各种外设,二是向上给应用软件提供各种服务帮助。我们可对其进一步描述:操作系统是一个可以管理CPU、内存和各种外设,并管理和服务应用软件的软件。这样的描述也是大多数操作系统教材上对操作系统的一个比较概括的定义。为了完成这些工作,操作系统需要知道如何与硬件打交道,如何更好地面向应用软件做好服务,这就有一系列操作系统相关的理论、抽象、设计等来支持如何做和做得好的需求。 + +.. image:: computer-hw-sw.png + :align: center + :scale: 50 % + :name: computer-hw-sw + +如果看看我们的身边, Android 应用运行在 ARM 处理器上 Android 操作系统的执行环境中,微软的 Office 应用运行在 x86-64 处理器上 Windows 操作系统的执行环境中,Web Server应用运行在 x86-64 处理器上 Linux 操作系统的执行环境中, Web app 应用运行在 x86-64 或 ARM 处理器上 Chrome OS 操作系统的执行环境中。而在一些嵌入式环境中,操作系统以运行时库的形式与应用程序紧密结合在一起,形成一个可在嵌入式硬件上单一执行的嵌入式应用。所以,在不同的应用场景下,操作系统的边界也是不同的,我们可以把运行时库、图形界面支持库等这些可支持不同应用的系统软件 (System Software) 也看成是操作系统的一部分。 + +站在应用程序的角度来看,我们可以发现常见的应用程序其实是运行在由硬件、操作系统、运行时库、图形界面支持库等所包起来的一个 :ref:`执行环境 (Execution Environment) ` 中,应用程序只需根据与系统软件约定好的应用程序二进制接口 (ABI, Application Binary Interface) 来请求执行环境提供的各种服务或功能,从而完成应用程序自己的功能。基于这样的观察,我们可以把操作系统再简单一点地定义为: **应用程序的软件执行环境** 。从这个角度出发,操作系统可以包括运行时库、图形界面支持库等系统软件,也能适应在操作系统发展的不同历史时期对操作系统的概括性描述和定义。 + +.. image:: EE.png + :align: center + :name: exec-env 站在计算机发展的百年时间尺度看 ---------------------------------- -虽然电子计算机的出现距今才仅仅七十年左右,但计算机技术和操作系统已经发生了巨大的变化。从计算机发展的短暂的历史角度看,操作系统也是从无到有地逐步发展起来的。操作系统主要完成对硬件控制和对应用程序的服务所必需的功能,操作系统的历史与计算机的发展史密不可分。操作系统的内涵和功能随着历史的发展也在一直变化,改进中,在今天,在二十一世纪初期的大众眼中,操作系统就是他们的手机/终端上的软件系统,包括各种应用程序集合。在用户的眼里,如果一个操作系统没有图形界面、网络浏览器,那它就称为一个操作系统了。 +虽然电子计算机的出现距今才仅仅七十年左右,但计算机技术和操作系统已经发生了巨大的变化。从计算机发展的短暂的历史角度看,操作系统也是从无到有地逐步发展起来的。操作系统主要完成控制硬件控制和为应用程序提供服务这些必不可少的功能,它的历史与计算机的发展史密不可分。操作系统的内涵和功能随着历史的发展也在一直变化、改进中。如今在二十一世纪初期的大众眼中,操作系统就是他们的手机/终端上的软件系统,包括各种应用程序集合,图形界面和网络浏览器是其中重要的组成部分。 + +其实,操作系统的内涵和外延随着历史的发展也一直在变化,并没有类似于“1+1=2”这样的明确定义。参考生物的进化史,我们也给操作系统的进化历史做一个简单的概述,从中可以看到操作系统在各个时间段上包含什么,具有什么样的特征。但无论操作系统的内在实现和具体目标如何变化,其管理计算机硬件,给应用提供服务的核心定位没有变化。 + +寒武纪生物大爆发时代 +~~~~~~~~~~~~~~~~~~~~~~ -其实,操作系统的内涵和外延随着历史的发展,也一直在变化,并没有类似于“1+1=2”这样的明确定义。参考生物的进化史,我们也给操作系统的进化历史做一个简单的概述,从中可以看到操作系统在各个时间段上包含什么,具有什么样的特征。 +电子计算机在 1946 年最开始出现的时候是没有操作系统 (Operating System) 的,只有操作员 (Operator) 。启动,扳开关,装卡片/纸带等比较辛苦的工作都是计算机操作员或者用户自己完成。操作员/用户带着记录有程序和数据的卡片 (Punch Card) 或打孔纸带去操作机器。装好卡片/纸带后,启动卡片/纸带阅读器,把程序和数据读入计算机内存中之后,计算机就开始工作,并把结果也输出到卡片/纸带或显示屏上,最后程序停止。 -三叶虫时代 -~~~~~~~~~~~~~~ +由于过低的人工操作效率浪费了计算机的宝贵机时,所以就引入监控程序 (Monitor) 辅助完成输入、输出、加载、运行程序等工作,这是现代操作系统的起源,类似寒武纪生物大爆发中的“三叶虫”。一般情况下,计算机每次只能执行一个任务, CPU 大部分时间都在等待人的缓慢操作。这个初级的“辅助操作”过程一直持续到 20 世纪 50 年代。 -电子计算机在1946年最开始出现的时候是没有操作系统(Operating System)的,只有操作员(Operator)。启动,扳开关,装卡片/纸带等比较辛苦的工作都是计算机操作员或者用户自己完成。操作员/用户带着记录有程序和数据的卡片\(punch card\)或打孔纸带去操作机器。装好卡片/纸带后,启动卡片/纸带阅读器,让计算机把程序和数据读入计算机机的内存中后,计算机就开始工作,并把结果也输出到卡片/纸带或显示屏上,最后程序停止。 +.. note:: -由于人的操作效率太低,计算机的机时宝贵,所以就引入监控程序(Monitor)辅助完成输入,输出,加载,运行程序等工作,这是现代操作系统的起源。一般情况下,计算机每次只能执行一个任务,CPU大部分时间都在等待人的缓慢操作。这个“手工操作”过程一直持续到20世纪50年代。 + 可以在 :ref:`本书第一章 ` 看到初级的“三叶虫”操作系统其实就是一个给应用提供各种服务(比如输出字符串)的库,方便了单一应用程序的开发与运行。 -恐龙时代 -~~~~~~~~~~~~~~ +泥盆纪鱼类时代和二叠纪两栖动物时代 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -在20世纪50年代~60年代,计算机发展到大型机阶段,而所对应的早期操作系统非常多样化,专用化,生产商生产出针对各自硬件的专用操作系统,大部分用汇编语言编写,这导致操作系统的进化比较缓慢,但进化再持续,从“手工操作”进化到了“批处理”阶段和“多道程序”阶段。在1964年,IBM公司开发了面向System/360系列机器的统一可兼容的操作系统——OS/360。OS/360是一种批处理操作系统。为了能充分地利用计算机系统,应尽量使该系统连续运行,减少空闲时间,所以批处理操作系统把一批作业(古老的术语,可理解为现在的程序)以脱机方式输入到磁带上,并使这批作业能一个接一个地连续处理:1)将磁带上的一个作业装入内存;2)并把运行控制权交给该作业;3)当该作业处理完成后,把控制权交还给操作系统;4)重复1-3的步骤。 +在 20 世纪 50~60 年代,计算机发展到大型机阶段,而所对应的早期操作系统非常多样化、专用化,生产商生产出针对各自硬件的专用操作系统,大部分用汇编语言编写,这导致操作系统的进化比较缓慢,但进化在持续进行,从“手工操作”进化到了“批处理”阶段和“多道程序”阶段。在 1964 年, IBM 公司开发了面向 System/360 系列机器的统一可兼容的操作系统—— OS/360 ,它是一种批处理操作系统。为了能充分地利用计算机系统,应尽量使该系统连续运行,减少空闲时间,所以批处理操作系统把一批作业(古老的术语,可理解为现在的程序)以脱机方式输入到磁带上,并使这批作业能一个接一个地连续处理,流程如下: + +1. 将磁带上的一个作业装入内存; +2. 操作系统把运行控制权交给该作业; +3. 当该作业处理完成后,控制权被交还给操作系统; +4. 重复1-3的步骤处理下一个作业直到所有作业处理完毕。 批处理操作系统分为单道批处理系统和多道批处理系统。单道批处理操作系统只能管理内存中的一个(道)作业,无法充分利用计算机系统中的所有资源,致使系统整体性能较差。多道批处理操作系统能管理内存中的多个(道)作业,可比较充分地利用计算机系统中的所有资源,提升系统整体性能。 -多道批处理操作系统为此采用了多道程序设计技术,就是指允许同时把多个程序放入内存,并允许它们交替在CPU中运行,它们共享系统中的各种硬、软件资源。当一道程序因I/O请求而暂停运行时,CPU便立即转去运行另一道程序。 +多道批处理操作系统为此采用了多道程序设计技术,就是指允许同时把多个程序放入内存,并允许它们交替在 CPU 中运行,它们共享系统中的各种硬、软件资源。当一道程序因 I/O 请求而暂停运行时, CPU 便立即转去运行另一道程序。 虽然批处理操作系统提高了系统的执行效率,但其缺点是人机交互性差。如果程序员的代码出现错误,必须重新编码,上传内存,再执行。这需要花费以小时和天为单位的时间开销,使得程序员修改和调试程序很不方便。 -爬行动物时代 -~~~~~~~~~~~~~~ +.. note:: + + 可以在 :ref:`本书第二章 ` 看到批处理操作系统的设计实现,以及支持一个一个地执行应用程序的运行过程。而在 :ref:`本书第三章 ` 的前三节可以看到支持协作式多道程序的操作系统的设计实现,以及支持应用程序主动放弃 CPU 以提高系统整体执行效率的过程。 + +侏罗纪与白垩纪的爬行动物时代 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +20 世纪 60 年代末,提高人机交互方式的分时操作系统越来越崭露头角。分时是指多个用户和多个程序以很小的时间间隔来共享使用同一台计算机上的 CPU 和其他硬件/软件资源。1964 年由贝尔实验室、麻省理工学院及美国通用电气公司所共同参与研发目标远大的MULTICS (MULTiplexed Information and Computing System) 操作系统是一套安装在大型主机上多人多任务的操作系统。 MULTICS 以兼容分时系统 (CTSS) 做基础,建置在美国通用电力公司的大型机 GE-645 ,目标是连接 1000 部终端机,支持 300 位用户同时上线。因 MULTICS 项目的工作进度过于缓慢,1969 年 AT&T 的 Bell 实验室从 MULTICS 研发中撤出。但贝尔实验室的两位软件工程师 Thompson 与 Ritchie 借鉴了一些重要的 MULTICS 理念,以 C 语言为基础发展出 UNIX 操作系统。UNIX 操作系统的早期版本是完全免费的,可以轻易获得并随意修改,所以它得到了广泛的接受。后来,它成为开发小型机操作系统的起点。由于早期的广泛应用,它已经成为分时操作系统的典范。 + +.. note:: + + 可以在 :ref:`本书第三章 ` 的第四节可以看到分时操作系统的设计实现,以及操作系统可强制让应用程序被动放弃 CPU 使得应用可以公平共享系统中的软硬件资源。并且 UNIX 还有虚存、文件、进程等当前操作系统的关键特性,这些内容也在本书的第四章~第七章中有详细的设计描述。 + +古近纪哺乳动物时代 +~~~~~~~~~~~~~~~~~~~~~~~ + +20 世纪 70 年代,微型处理器的发展使计算机的应用普及至中小企及个人爱好者,推动了个人计算机 (PC, Personal Computer) 的发展,也进一步推动了面向个人使用的操作系统的出现。其代表是由微软公司在 20 世纪 80 年代为个人计算机开发的 DOS/Windows 操作系统,其特点是简单易用,特别是基于 Windows 操作系统的 GUI 界面,极大地简化了一般用户使用计算机的难度,使得计算机得到了快速的普及。这里需要注意的是,第一个带 GUI 界面的个人计算机原型起源于伟大却又让人扼腕叹息的施乐帕洛阿图研究中心 (PARC, Palo Alto Research Center) ,PARC 研发出的带有图标、弹出式菜单和重叠窗口的图形交互界面 (GUI, Graphical User Interface),可利用鼠标的点击动作来进行操控,这是当今我们所使用的 GUI 系统的基础。支持便捷的图形交互界面也成为自 20 世纪 70 年代以来操作系统的主要特征之一。 + +.. note:: + + 目前支持 GUI 交互接口的操作系统设计实现在本书中还没有对应的章节。但其操作系统的内核其实与分时操作系统的设计实现思路基本是一致的。如果在本书设计的简单分时操作系统的基础上,添加一个图形外设的驱动和一个简单的 GUI 窗口系统,也许是一个有趣的实验内容。 + +第四纪智人时代 +~~~~~~~~~~~~~~~~~~~~~ + +21 世纪以来, Internet 和移动互联网的迅猛发展,使得在服务器领域和个人终端的应用与需求大增。iOS 和 Android 操作系统是21世纪个人终端操作系统的代表,Linux 在巨型机到数据中心服务器操作系统中占据了统治地位。以 Android 系统为例,Android 操作系统是一个包括 Linux 操作系统内核、基于 Java 的中间件、用户界面和关键应用软件的移动设备软件栈集合。这里介绍一下广泛用在服务器领域和个人终端中的操作系统内核--Linux 操作系统内核。1991 年 8 月,芬兰学生 Linus Torvalds \(林纳斯·托瓦兹\) 在 comp.os.minix 新闻组贴上了以下这段话:  + + + "你好,所有使用 minix 的人 -我正在为 386 ( 486 ) AT 做一个免费的操作系统 ( 只是为了爱好 )...″ + -20世纪60年代末,提高人机交互方式的分时操作系统越来越展露头角。分时是指多个用户和多个程序以很小的时间间隔来共享使用同一台计算机上的CPU和其他硬件/软件资源。1964年由贝尔实验室、麻省理工学院及美国通用电气公司所共同参与研发目标远大的MULTICS\(MULTiplexed Information and Computing System\)操作系统,MULTICS是一套安装在大型主机上多人多任务的操作系统。 MULTICS以兼容分时系统(CTSS)做基础,建置在美国通用电力公司的大型机GE-645,目标是连接1000部终端机,支持300的用户同时上线。因MULTICS项目的工作进度过于缓慢,1969年AT&T的 Bell 实验室从MULTICS 研发中撤出。但贝尔实验室的两位软件工程师 Thompson 与 Ritchie借鉴了一些重要的Multics理念,以C语言为基础,发展出UNIX操作操作系统。UNIX操作系统的早期版本是完全免费的,可以轻易获得并随意修改,所以它得到了广泛的接受。后来,它成为开发小型机操作系统的起点。由于早期的广泛应用,它已经成为的分时操作系统的典范。 +而他所说的“爱好″成为了大家都知道的 Linux 操作系统内核。这个时代的操作系统的特征是联网,发挥网络的吞吐量和低延迟是这个时代的网络操作系统追求的目标。  -哺乳动物时代 -~~~~~~~~~~~~~~ +.. note:: -20世纪70年代,微型处理器的发展使计算机的应用普及至中小企及个人爱好者,推动了个人计算机\(Personal Computer\)的发展,也进一步推动了面向个人使用的操作系统的出现。其代表是由微软公司中在20世纪80年代为个人计算机开发的DOS/Windows操作系统,其特点是简单易用,特别是基于Windows操作系统的GUI界面,极大地简化了一般用户使用计算机的难度,使得计算机得到了快速的普及。这里需要注意的是,第一个带GUI界面的个人计算机原型起源于伟大却又让人扼腕叹息的施乐帕洛阿图研究中心PARC(Palo Alto Research Center),PARC研发出的带有图标、弹出式菜单和重叠窗口的GUI(Graphical User Interface),可利用鼠标的点击动作来进行操控,这是当今我们所使用的GUI系统的基础。支持便捷的图形交互界面也成为从20世纪70年代~21世纪初的操作系统的主要特征之一。 + 目前支持联网的操作系统设计实现在本书中还没有对应的章节。但其操作系统的内核其实与分时操作系统的设计实现思路基本是一致的。如果在本书设计的简单分时操作系统的基础上,添加一个网卡外设的驱动和一个简单的网络协议栈,也许是另一个有趣的实验内容。 -智人时代 -~~~~~~~~~~~~~~ +二十一世纪神人时代 +~~~~~~~~~~~~~~~~~~~~~~~~~ -21世纪以来,Internt和移动互联网的迅猛发展,使得在服务器领域和个人终端的应用与需求大增。iOS和Android操作系统是21世纪个人终端操作系统的代表,Linux在巨型机到数据中心服务器操作系统中占据了统治地位。以Android系统为例,Android一词英文本义指“机器人”,它是由Google公司于2007年11月推出的基于Linux Kernel的开源手机操作系统,目前在移动终端中占有最大的份额。Android操作系统是一个包括Linux操作系统内核、基于Java的中间件、用户界面和关键应用软件的移动设备软件栈集合。这里介绍一下广泛用在服务器领域和个人终端中的操作系统内核--Linux操作系统内核。1991年8 月,芬兰学生 Linus Torvalds\(林纳斯·托瓦兹\)在 comp.os.minix 新闻组贴上了以下这段话:  +当前,大数据、人工智能、机器学习、高速移动互联网络、AR/VR 对操作系统等系统软件带来了新的挑战。如何有效支持和利用这些技术是未来操作系统的方向。 -``` -  "你好,所有使用 minix 的人 -我正在为 386 ( 486 ) AT 做一个免费的操作系统 ( 只是为了爱好 ),不会像 GNU 那样很大很专业。″ -``` +在 2020 年,我们看到了华为逐步推出的鸿蒙系统;小米也推出了物联网软件平台小米 Vela ;阿里推出了 AliOS Thing;腾讯推出了Tencent OS;苹果公司接连推出 A14、M1 等基于 ARM 的 CPU,逐步开始淘汰 X86CPU;微软推出 Windows 10 IoT,Google 推出 Fuchsia OS,也都在做着各种云、边、端的技术调整和创新。 -而他所说的"爱好″就变成我们今天知道的 Linux操作系统内核。 Linus通过Internet首次发表 Linux kernel的源代码,并且选用GPL版权协议来发行。GPL版权协议允许任何人以任何形式发布 Linux 的源代码,在Internet的日渐盛行以及 Linux 开放自由的GPL版权之下,吸引了无数计算机Hacker和公司投入开发、改善 Linux kernel,使得 Linux kernel的功能日见强大。这个时代的操作系统的特征是联网,发挥网络的吞吐量和低延迟是这个时代的网络操作系统追求的目标。  +大家好像都意识到,不仅仅是人工智能和机器学习,下一个具有分布式特征的操作系统的新突破即将到来,并试图通过这种具有分布式特征的操作系统带来的连贯用户体验,打通从数据中心、服务器、桌面、移动端、边缘设备等的整个 AI 和物联网 (IoT, Internet of Things) 的生态。也许这个时代的未来操作系统与之前的操作系统相比,其最大的不同是跳出了单个设备节点,通过高速的无线网络从多种维度来管理多个设备,形成分布式操作系统。 -神人时代 -~~~~~~~~~~~~~~ +.. note:: -当前,大数据、人工智能、机器学习、高速移动互联网络、AR/VR对操作系统等系统软件带来了新的挑战。如何有效支持和利用这些技术是未来操作系统的方向。 + 目前支持AIoT的操作系统设计实现在本书中还没有对应的章节,不过我们的同学也设计了 + `zCore操作系统 `_ , + 欢迎看完本书的同学能够尝试参与或独立设计面向未来的操作系统。 -在2020年,我们看到了华为逐步推出的鸿蒙系统;小米也推出了物联网软件平台小米Vela;阿里推出了AliOS Thing;腾讯推出了Tencent OS;苹果公司接连推出A14、M1等基于ARM的CPU,逐步开始淘汰X86CPU;微软推出Windows 10 IoT,Google推出Fuchsia OS,也都在做着各种云、边、端的技术调整和创新。 -大家好像都意识到,不仅仅是人工智能和机器学习,下一个具有分布式特征的操作系统的新突破即将到来,并试图通过这种具有分布式特征的操作系统带来的连贯用户体验,打通从数据中心、服务器、桌面、移动端、边缘设备等的整个AI+IoT(Internet of Things)的生态。也许这个时代的未来操作系统与之前的操作系统相比,其最大的不同是跳出了单个设备节点,而是通过高速的无线网络从多种维度来管理多个设备,形成分布式操作系统。 +.. note:: + 本节内容部分参考了尤瓦尔·赫拉利所著的“人类简史”、“未来简史” 。 \ No newline at end of file diff --git a/source/chapter0/2os-interface.rst b/source/chapter0/2os-interface.rst index 04e927e092fada989ecfea708eff664f5f8d0e5d..a5e127bb5be084a72ed0aa27ba6f17a4778d7942 100644 --- a/source/chapter0/2os-interface.rst +++ b/source/chapter0/2os-interface.rst @@ -5,20 +5,40 @@ :hidden: :maxdepth: 5 -操作系统的接口 ----------------------------------- +站在使用操作系统的角度会比较容易对操作系统的功能产生初步的认识。操作系统内核是一个需要提供各种服务的软件,其服务对象是应用程序,而用户(这里可以理解为一般使用计算机的人)是通过应用程序的服务间接获得操作系统的服务的,因此操作系统内核藏在一般用户看不到的地方。但应用程序需要访问操作系统获得操作系统的服务,这就需要通过操作系统的接口才能完成。操作系统的接口的形式就是上一节提到的应用程序二进制接口 (ABI, Application Binary Interface)。但操作系统不是简单的一个函数库的编程接口 (API, Application Programming Interface) ,它的接口需要考虑安全因素,使得应用软件不能直接读写操作系统内部函数的地址空间,为此,操作系统设计了一套安全可靠的接口,我们称为系统调用接口 (System Call Interface),应用程序可以通过系统调用接口请求获得操作系统的服务,但不能直接调用操作系统的函数和全局变量;操作系统提供完服务后,返回应用程序继续执行。 + + +.. note:: + + **API 与 ABI 的区别** + + 应用程序二进制接口 ABI 是不同二进制代码片段的连接纽带。ABI 定义了二进制机器代码级别的规则,主要包括基本数据类型,通用寄存器的使用,参数的传递规则,以及堆栈的使用等等。ABI 是用来约束链接器 (Linker) 和汇编器 (Assembler) 的。基于不同高级语言编写的应用程序、库和操作系统,如果遵循同样的 ABI 定义,那么它们就能正确链接和执行。 + + 应用程序编程接口 API 是不同源代码片段的连接纽带。API 定义了一个源码级(如 C 语言)函数的参数,参数的类型,函数的返回值等。因此 API 是用来约束编译器 (Compiler) 的:一个 API 是给编译器的一些指令,它规定了源代码可以做以及不可以做哪些事。API 与编程语言相关,如 LibC 是基于 C 语言编写的标准库,那么基于 C 的应用程序就可以通过编译器建立与 LibC 的联系,并能在运行中正确访问 LibC 中的函数。 + + +对于实际操作系统而言,具有大量的服务接口,比如目前 Linux 有三百个系统调用接口。下面列出了一些相对比较重要的操作系统接口或抽象: + +* 进程(即程序运行过程)管理:复制创建进程 fork 、退出进程 exit 、执行进程 exec 等。 +* 同步互斥的并发控制:信号量 semaphore 、管程 monitor 、条件变量 condition variable 等。 +* 进程间通信:管道 pipe 、信号 signal 、事件 event 等。 +* 虚存管理:内存空间映射 mmap 、改变数据段地址空间大小 sbrk 、共享内存 shm 等。 +* 文件I/O操作:读 read 、写 write 、打开 open 、关闭 close 等。 +* 外设I/O操作:外设包括键盘、显示器、串口、磁盘、时钟 ...,但接口均采用了文件 I/O 操作的通用系统调用接口。 + +.. note:: + + 上述表述在某种程度上说明了操作系统对计算机硬件重要组成的抽象和虚拟化,使得应用程序只需基于对简单的抽象概念的访问来到达对计算机系统资源的使用: -读者可站在使用操作系统的角度来看操作系统,这样会比较容易对操作系统有个初步的进一步了解。操作系统内核是一个需要提供各种服务的软件,其服务对象是应用程序,而用户(这里可以理解为一般使用计算机的人)是通过应用程序的服务间接获得操作系统的服务的),所以操作系统内核藏在一般用户看不到的地方。但应用程序需要访问操作系统,获得操作系统的服务,这就需要通过操作系统的接口才能完成。如果把操作系统看成是一个函数库,那么其接口就是函数名称和它的参数。但操作系统不是简单的一个函数库,它的接口需要考虑安全因素,使得应用软件不能直接读写操作系统内部函数的地址地址空间,为此,操作系统设计了一个安全可靠的接口,我们称为系统调用接口(System Call Interface),应用程序可以通过系统调用接口请求获得操作系统的服务,但不能直接调用操作系统的函数和全局变量;操作系统提供完服务后,返回应用程序继续执行。 + * 文件 (File) 是外设的一种抽象和虚拟化。特别对于存储外设而言,文件是持久存储的抽象。 + * 地址空间 (Address Space) 是对内存的抽象和虚拟化。 + * 进程 (Process) 是对计算机资源的抽象和虚拟化。而其中最核心的部分是对CPU的抽象与虚拟化。 -对于实际操作系统而言,具有大量的服务接口,比如目前Linux有三百个系统调用接口。下面列出了一些相对比较重要的操作系统接口: -* 进程(即程序运行过程)管理:复制创建进程--fork、退出进程--exit、执行进程--exec、... -* 同步互斥的并发控制:信号量--semaphore、管程--monitor、条件变量--condition variable 、... -* 进程间通信:管道--pipe、信号--signal、事件--event、共享内存--shared mem、... -* 文件I/O操作:读--read、写--write、打开--open、关闭--close、... -* 外设I/O操作:外设包括键盘、显示器、串口、磁盘、时钟、...,但接口是直接采用了文件I/O操作的系统调用接口 -> 这在某种程度上说明了文件是外设的一种抽象。在UNIX中,大部分外设都可以以文件的形式来访问 +.. image:: run-app.png + :align: center + :name: run-app -有了这些接口,简单的应用程序就不用考虑底层硬件细节,可以在操作系统的服务支持和管理下简洁地完成其应用功能了。在现阶段,也许大家对进程、文件、同步互斥等概念还没有清晰的了解,在接下来的章节会对这些概念有进一步的介绍。 +有了这些接口,简单的应用程序就不用考虑底层硬件细节,可以在操作系统的服务支持和管理下简洁地完成其应用功能了。在现阶段,也许大家对进程、文件、地址空间等抽象概念还不了解,在接下来的章节会对这些概念有进一步的介绍。 diff --git a/source/chapter0/3os-hw-abstract.rst b/source/chapter0/3os-hw-abstract.rst index 3896e30110ff9ac051ba56f75aa55c2d0a754c66..6b49f47158eae1c81951bda9d0259dd50b2e9892 100644 --- a/source/chapter0/3os-hw-abstract.rst +++ b/source/chapter0/3os-hw-abstract.rst @@ -1,33 +1,199 @@ -操作系统的硬件抽象 +操作系统抽象 ================================================ .. toctree:: :hidden: :maxdepth: 5 -操作系统抽象 ----------------------------------- -接下来读者可站在操作系统实现的角度来看操作系统。操作系统为了能够更好地管理计算机系统并对应用程序提供便捷的服务,在操作系统的发展过程中,计算机科学家提出了如下四个个抽象概念,奠定了操作系统内核设计与实现的基础。操作系统原理中的其他基本概念基本上都基于上述这四个操作系统抽象。 +.. + chyyuu:我觉得需要给出执行环境(EE),Task,...,上下文(函数,trap,task,进程...),执行流等的描述。 + 并且有一个图,展示这些概念的关系。这些概念能够有链接,指向进一步实际定义或使用的地方。 -中断(Interrupt) ----------------------------------- +接下来读者可站在操作系统实现的角度来看操作系统。操作系统为了能够更好地管理计算机系统并为应用程序提供便捷的服务,在计算机和操作系统的技术研究和发展的过程中,形成了一系列的核心概念,奠定了操作系统内核设计与实现的基础。 + +.. note:: + + 在本书中,下面的抽象表示不会仅仅就是一个文字的描述,还会在后续章节对具体操作系统设计与运行的讲述中,以具体化的静态数据结构,动态执行对物理/虚拟资源的变化来展示。从而让读者能够建立操作系统抽象概念与操作系统具体实验之间的内在联系。 + +执行环境 +---------------------------------------- + +**执行环境** (Execution Environment) 是一个内涵很丰富且有一定变化的一个术语,它主要负责给在其上执行的软件提供相应的功能与资源,并可在计算机系统中形成多层次的执行环境。对于现在直接运行在裸机硬件 (Bare-Metal) 上的操作系统,其执行环境是 *计算机的硬件* 。 +在寒武纪时期的计算机系统中,还没有操作系统,所以对于直接运行在裸机硬件上的应用程序而言,其执行环境也是 *计算机的硬件* 。 +随着计算机技术的发展,应用程序下面形成了一层比较通用的函数库,这使得应用程序不需要直接访问硬件了,它所需要的功能(比如显示字符串)和资源(比如一块内存)都可以通过函数库的函数来帮助完成。在第二个阶段,应用程序的执行环境就变成了 *函数库* -> *计算机硬件* ,而这时函数库的执行环境就是计算机的硬件。 + +.. image:: basic-EE.png + :align: center + :name: basic-ee + +再进一步,操作系统取代了函数库来访问硬件,函数库通过访问操作系统的系统服务来进一步给应用程序 +提供丰富的功能和资源。在第三个阶段,应用程序的执行环境就变成了 *函数库* -> *操作系统* -> *计算机硬件* 。 +在后面又出现了基于 Java 语言的应用程序,在函数库和操作系统之间,多了一层 Java 虚拟机,此时 Java 应用 +程序的执行环境就变成了 *函数库* -> *Java 虚拟机* -> *操作系统* -> *计算机硬件* 。在云计算时代,在传统操作系统与 +计算机硬件之间多了一层 Hypervisor/VMM ,此时应用程序的执行环境变成了 *函数库* -> *Java 虚拟机* -> *操作系统* -> *Hypervisor/VMM* -> *计算机硬件* 。 + +.. _term-ee-switch: + +另外,CPU在执行过程中,可以在不同层次的执行环境之间可以切换,这称为 **执行环境切换** 。这主要是通过特定的 API 或 ABI 来完成的,这样不同执行环境的软件就能实现数据交换与互操作,而且还保证了彼此之间有清晰的隔离。 + +.. image:: complex-EE.png + :align: center + :name: complex-ee + +对于应用程序的执行环境而言,其具体的内容是多变的,但应用程序只能看到执行环境直接提供给它的接口(API 或 ABI),这使得应用程序所能得到的服务取决于执行环境提供给它这套接口。当然执行环境中的内在功能(如对于应用程序的资源调度与管理等)也会对应用程序的执行效率,可靠性等提供间接的支持。所以,操作系统是属于或等于应用程序执行环境的软件部分,其形态可以是一个库,也可以是一个虚拟机等,或者它们的某种组合形式。 + +基于上面的介绍,我们可以给应用程序的执行环境一个基本的定义:执行环境是一个概念,一种机制,用来完成应用程序在运行时的数据与资源管理、应用程序的生存期等方面的处理,它定义了应用程序有权访问的其他数据或资源,并决定了应用程序的行为限制范围。 + +.. _term-ccf: + +普通控制流 +---------------------- + +回顾一下编译原理课上的知识,程序的控制流 (Flow of Control or Control Flow) 是指以一个程序的指令、语句或基本块为单位的执行序列。再回顾一下计算机组成原理课上的知识,处理器的控制流是指处理器中程序计数器的控制转移序列。最简单的一种控制流(没有异常或中断产生的前提下)是一个“平滑的”序列,其中每个要执行的指令地址在内存中都是相邻的。如果站在程序员的角度来看控制流,会发现控制流是程序员编写的程序的执行序列,这些序列是程序员预设好的。程序运行时能以多种简单的控制流(顺序、分支、循环结构和多层嵌套函数调用)组合的方式,来一行一行的执行源代码(以编程语言级的视角),也是一条一条的执行汇编指令(以汇编语言级的视角)。对于上述的不同描述,我们可以统称其为普通控制流 (CCF,Common Control Flow,简称 控制流) 。在应用程序视角下,它只能接触到它所在的执行环境,不会跳到其他执行环境,所以应用程序执行基本上是以普通控制流的形式完成整个运行的过程。 + +.. _term-ecf: + +异常控制流 +-------------------------------------- + +应用程序在执行过程中,如果出现外设中断或 CPU 异常,处理器执行的前一条指令和后一条指令会位于两个完全不同的位置,即不同的执行环境 。比如,前一条指令还在应用程序的代码段中,后一条指令就跑到操作系统的代码段中去了,这就是一种控制流的“突变”,即控制流脱离了其所在的执行环境,并产生 :ref:`执行环境的切换 `。 + +应用程序 *感知* 不到这种异常的控制流情况,这主要是由于操作系统把这种情况 *透明* 地进行了执行环境的切换和对各种异常情况的处理,让应用程序从始至终地 *认为* 没有这些异常控制流的产生。 + +简单地说,异常控制流 (ECF, Exceptional Control Flow) 是处理器在执行过程中的突变,其主要作用是通过硬件和操作系统的协同工作来响应处理器状态中的特殊变化。比如当应用程序正在执行时,产生了时钟外设中断,导致操作系统打断当前应用程序的执行,转而进入 **操作系统** 所在的执行环境去处理时钟外设中断。处理完毕后,再回到应用程序的执行环境中被打断的地方继续执行。 + +.. note:: + + 本书是从操作系统的角度来给出的异常控制流的定义。 + + 在“深入理解计算机系统”(CSAPP)一书中,对异常控制流也给出了相关定义: + 系统必须能对系统状态的变化做出反应,这些系统状态不是被内部程序变量捕获,也不一定和程序的执行相关。现代系统通过使控制流发生突变对这些情况做出反应。我们称这种突变为异常控制流( Exceptional Control Flow,ECF) + + 我们这里的异常控制流不涉及C++/Java等编程语言级的exception机制。 + + +.. _term-context: +.. _term-ees: + +上下文或执行环境的状态 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +站在硬件的角度来看普通控制流或异常控制流的具体执行过程,我们会发现从控制流起始的某条指令开始记录,指令可访问的所有物理资源,包括自带的所有通用寄存器、特权级相关特殊寄存器、以及指令访问的内存等,会随着指令的执行而逐渐发生变化。 + +这里我们把控制流在执行完某指令时的物理资源内容,即确保下一时刻能继续 *正确* 执行控制流指令的物理资源内容称为控制流的 **上下文** (Context) ,也可称为控制流所在执行环境的状态。 + +这里需要理解控制流的上下文对控制流的 *正确* 执行的影响。如果在某时刻,由于某种有意或无意的原因,控制流的上下文发生了不是由于控制流本身的指令产生的变化,并使得控制流执行接下来的指令序列时出现了偏差,并最终导致执行过程或执行结果不符合预期,这种情形称为没有正确执行。 所以,我们这里说的控制流的上下文是指仅会影响控制流正确执行的有限的物理资源内容。 + +如果一个控制流是属于某个函数,那么这个控制流的上下文简称为函数调用上下文。如果一个控制流是属于某个应用程序的,那么这个控制流的上下文简称为应用程序上下文。如果把某 :ref:`进程 ` 看做是运行的应用程序,那么这个属于某个应用程序的控制流可简称为某进程上下文。如果一个控制流是属于操作系统,那么这个控制流的上下文简称为操作系统上下文。如果一个控制流是属于操作系统中处理中断/异常/陷入的那段代码,那么这个控制流的上下文简称为中断/异常/陷入的上下文。 + +那么随着CPU的执行,各种前缀的上下文(执行环境的状态)会在不断的变化。 +如果出现了处理器在执行过程中的突变(即异常控制流)或转移(如多层函数调用),需要由维持执行环境的软硬件协同起来,保存发生突变或转移前的当前的执行环境的状态(比如突变或函数调用前一刻的指令寄存器,栈寄存器和其他一些通用寄存器等内容),并在完成突变处理或被调用函数后,恢复突变或转移前的执行环境的状态。这是由于完成与突变相关的执行会破坏突变前的执行环境状态(比如上述各种寄存器的内容),导致如果不保存状态,就无法恢复到突变前执行环境,继续正常的普通控制流的执行。 -简单地说,中断是处理器在执行过程中的突变,用来响应处理器状态中的特殊变化。比如当应用程序正在执行时,产生了时钟外设中断,导致操作系统打断当前应用程序的执行,转而去处理时钟外设中断,处理完毕后,再回到应用程序被打断的地方继续执行。在操作系统中,有三类中断:外设中断(Device Interrupt)、陷阱中断(Trap Interrupt)和故障中断(Fault Interrupt,也称为exception,异常)。外设中断由外部设备引起的外部I/O事件如时钟中断、控制台中断等。外设中断是异步产生的,与处理器的执行无关。故障中断是在处理器执行指令期间检测到不正常的或非法的内部事件(如除零错、地址访问越界)。陷阱中断是在程序中使用请求操作系统服务的系统调用而引发的有意事件。在后面的叙述中,如果没有特别指出,我们将用简称中断、陷阱、故障来区分这三种特殊的中断事件,在不需要区分的地方,统一用中断表示。 +对于异常控制流的上下文保存与恢复,主要是通过 CPU 和操作系统(手动编写在栈上保存与恢复寄存器的指令)来协同完成;对于函数转移控制流的上下文保存与恢复,主要是通过编译器(自动生成在栈上保存与恢复寄存器的指令)来帮助完成的。 -进程(Process) +在操作系统中,需要处理三类异常控制流:外设中断 (Device Interrupt) 、陷入 (Trap) 和异常 (Exception,也称Fault Interrupt)。 + + +.. _term-execution-flow: + +执行流或执行历史 +------------------------ + +无论是操作系统还是应用程序,它在某一段时间上的执行过程会让处理器执行一系列程序的指令,并对计算机的物理资源的内容(即上下文)进行了改变。如果结合上面的抽象概念更加细致地表述一下,可以认为在它从开始到结束的整个执行过程中,截取其中一个时间段,在这个时间段中,它所执行的指令流形成了这个时间段的控制流,而控制流中的每条执行的指令和它执行后的上下文,形成由二元组<指令指针,上下文>()构成的有序序列,我们用 **执行流** (Execution Flow) 或 **执行历史** (Execution History) 来表示这个二元组有序序列。它完整描述了操作系统或应用程序在一段时间内执行的指令流以及计算机物理资源的变化过程。 + + +中断 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +外设 **中断** (Interrupt) 由外部设备引起的外部 I/O 事件如时钟中断、控制台中断等。外设中断是异步产生的,与处理器的执行无关。 + +.. image:: interrupt.png + :align: center + :name: interrupt + +异常 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**异常** (Exception) 是在处理器执行指令期间检测到不正常的或非法的内部事件(如除零错、地址访问越界)。 + + +.. image:: exception.png + :align: center + :name: exception + +陷入 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**陷入** (Trap) 是在程序中使用请求操作系统服务的系统调用而引发的有意事件。 + +.. image:: syscall.png + :align: center + :name: syscall + +在后面的叙述中,如果没有特别指出,我们将用简称中断、陷入、异常来区分这三种异常控制流。 + +.. note:: + + 本书是从操作系统的角度来给出的中断 (Interrupt) 、陷入 (Trap) 和异常 (Exception)的定义。 + + 在不同的书籍中,对于中断 、陷入和异常的定义会有一些差别。有的书籍把中断、陷入和异常都统一为一种中断,表示程序的当前控制流被打断了,要去执行不属于这个控制流的另外一个没有程序逻辑先后关系的控制流;也有书籍把这三者 + 统一为一种异常,表示相对于程序的正常控制流而言,出现了的一种没有程序逻辑先后关系的异常控制流。甚至也有书籍把这三者统一为一种陷入,表示相对于程序的正常控制流而言,CPU会陷入到 + 操作系统内核中去执行。 + + 在RISC-V的特权级规范文档中,“陷入” 包含中断和异常,而原来意义上的陷入(trap,系统调用)只是exception中的一种情况。另外还有一种 “软件中断” ,它是指软件可以通过写特定寄存器(mip/sip)的特定位(MSIP/SSIP/USIP)来产生的中断。而异常和中断有严格的区分,在记录产生的异常或中断类型的特定寄存器(mcause/scause)中,寄存器最高位为 ``0`` 表示异常,最高位为 ``1`` 表示中断。进一步的详细信息可以可参考RISC-V的特权级规范文档和后面的章节。 + + 这些都是从不同的视角来阐释中断、陷入和异常,并没有一个唯一精确的解释。对于读者而言,重点是了解这些术语在后续章节的操作系统设计实现中所表示的具体含义和特征。 + +.. _term_process: + +进程 ---------------------------------- +进程 (Process) 的一个经典定义是一个正在运行的程序的实例。在计算机系统中,我们可以“同时”运行多个程序,这个“同时”,其实是操作系统给用户造成的一个“幻觉”。在操作系统上运行一个程序时,我们会得到一个“幻觉”,就好像我们执行的一个程序是整个计算机系统中当前运行的唯一的程序,能够独占使用处理器、内存和外设。而且程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通过进程的概念提供给我们的。 + +.. image:: prog-illusion.png + :align: center + :name: prog-illusion + +计算机系统中运行的每个程序都是运行在某个进程的上下文中。这里的上下文是指程序在运行中的状态。运行的状态包括:内存中的代码和数据,栈、堆、当前执行的指令位置(程序计数器的内容)、当前执行时刻的各个通用寄存器中的值,各种正在访问的资源的集合。进程上下文如下图所示: + +.. image:: context-of-process.png + :align: center + :name: context-of-process + -简单地说,进程是一个正在运行的程序。在计算机系统中,我们可以“同时”运行多个程序,这个“同时”,其实是操作系统给用户造成的一个“幻觉”。大家知道,处理器是计算机系统中的硬件资源。为了提高处理器的利用率,操作系统采用了多道程序技术。如果一个程序因某个事件而不能运行下去时,就把处理器占用权转交给另一个可运行程序。为了刻画多道程序的并发执行的过程,就要引入进程的概念。从操作系统原理上看,一个进程是一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。操作系统中的进程管理需要协调多道程序之间的关系,解决对处理器分配调度策略、分配实施和回收等问题,从而使得处理器资源得到最充分的利用。 +我们知道,处理器是计算机系统中的硬件资源。为了提高处理器的利用率,操作系统需要让处理器足够忙,即让不同的程序轮流占用处理器来运行。如果一个程序因某个事件而不能运行下去时,就通过进程上下文切换把处理器占用权转交给另一个可运行程序。进程上下文切换如下图所示: -虚存(Virtual Memory) +.. image:: context-switch.png + :align: center + :name: context-switch + +基于上面的介绍,我们可以给进程一个更加准确的定义:一个进程是一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。 +操作系统中的进程管理需要采用某种调度策略将处理器资源分配给程序并在适当的时候回收,并且要尽可能充分利用处理器的硬件资源。 + +地址空间 ---------------------------------- -简单地说,虚存就是操作系统通过处理器中的MMU硬件的支持而给应用程序和用户提供一个大的(超过计算机中的内存条容量)、一致的(连续的地址空间)、私有的(其他应用程序无法破坏)的存储空间。这需要操作系统将内存和硬盘结合起来管理,为用户提供一个容量比实际内存大得多的虚拟存储器,并且需要操作系统为应用程序分配内存空间,使用户存放在内存中的程序和数据彼此隔离、互不侵扰。操作系统中的虚存管理与处理器的MMU密切相关。 +**地址空间** (Address Space) 是对物理内存的虚拟化和抽象,也称虚存 (Virtual Memory)。它就是操作系统通过处理器中的内存管理单元 (MMU, Memory Management Unit) 硬件的支持而给应用程序和用户提供一个大的(可能超过计算机中的内存条容量)、一致的(连续的地址空间)、私有的(其他应用程序无法破坏)的存储空间。这需要操作系统将内存和硬盘结合起来管理,为用户提供一个容量比实际内存大得多的虚拟存储器,并且需要操作系统为应用程序分配内存空间,使用户存放在内存中的程序和数据彼此隔离、互不侵扰。操作系统中的虚存管理与处理器的 MMU 密切相关,在启动虚存机制后,软件通过 CPU 访问的每个虚拟地址都需要通过 CPU 中的 MMU 转换为一个物理地址来进行访问。下面是虚拟的地址空间与物理内存和物理磁盘映射的图示: + + +.. image:: address-space.png + :align: center + :name: address-space -文件(File) +文件 ---------------------------------- -简单地说,文件就是存放在持久存储介质(比如硬盘、光盘、U盘等)上,方便应用程序和用户读写的数据。当处理器需要访问文件中的数据时,可通过操作系统把它们装入内存。放在硬盘上的程序也是一种文件。文件管理的任务是有效地支持文件的存储、检索和修改等操作。 \ No newline at end of file +**文件** (File) 主要用于对持久存储的抽象,并进一步扩展到为外设的抽象。具体而言,文件可理解为存放在持久存储介 +质(比如硬盘、光盘、U盘等)上,方便应用程序和用户读写的数据。以磁盘为代表的持久存储介质的数据访问单位是一个扇区或一个块,而在内存中的数据访问单位是一个字节或一个字。这就需要操作系统通过文件来屏蔽磁盘与内存差异,尽量以内存的读写方式来处理持久存储的数据。当处理器需要访问文件中的数据时,可通过操作系统把它们装入内存。文件管理的任务是有效地支持文件的存储、 +检索和修改等操作。 + +下面是文件对磁盘的抽象映射图示: + +.. image:: file-disk.png + :align: center + :name: file-disk + + +从一个更高和更广泛的层次上看,各种外设虽然差异很大,但也有基本的读写操作,可以通过文件来进行统一的抽象,并在操作系统内部实现中来隐藏对外设的具体访问过程,从而让用户可以以统一的文件操作来访问各种外设。这样就可以把文件看成是对外设的一种统一抽象,应用程序通过基本的读写操作来完成对外设的访问。 diff --git a/source/chapter0/4os-features.rst b/source/chapter0/4os-features.rst index 05ad9acd4fd9a823f09b43a6a12d2aff0e56d4e7..0e82cb0dbcac3bb8f2d4c20a34a088b0bd15d629 100644 --- a/source/chapter0/4os-features.rst +++ b/source/chapter0/4os-features.rst @@ -6,7 +6,7 @@ :maxdepth: 5 -基于操作系统的四个抽象,我们可以看出,从总体上看,操作系统具有五个方面的特征:虚拟性(Virtualization)、并发性(concurrency)、异步性、共享性和持久性(persistency)。在虚拟性方面,可以从操作系统对内存,CPU的抽象和处理上有更好的理解;对于并发性和共享性方面,可以从操作系统支持多个应用程序“同时”运行的情况来理解;对于异步性,可以从操作系统调度,中断处理对应用程序执行造成的影响等几个放马来理解;对于持久性方面,可以从操作系统中的文件系统支持把数据方便地从磁盘等存储介质上存入和取出来理解。 +基于操作系统的四个抽象,我们可以看出,从总体上看,操作系统具有五个方面的特征:虚拟化 (Virtualization)、并发性 (Concurrency)、异步性、共享性和持久性 (Persistency)。操作系统的虚拟化可以理解为它对内存、CPU 的抽象和处理;并发性和共享性可以理解为操作系统支持多个应用程序“同时”运行;异步性可以从操作系统调度、中断处理对应用程序执行造成的影响等几个方面来理解;持久性则可以从操作系统中的文件系统支持把数据方便地从磁盘等存储介质上存入和取出来理解。 虚拟性 ---------------------------------- @@ -15,29 +15,32 @@ 内存虚拟化 ~~~~~~~~~~~~~~ -首先来看看内存的虚拟化。程序员在写应用程序的时候,不用考虑其程序的起始内存地址要放到计算机内存的具体某个位置,而是用字符串符号定义了各种变量和函数,直接在代码中便捷地使用这些符号就行了。这是由于操作系统建立了一个**地址固定**,**空间巨大**的虚拟内存给应用程序来运行,这是**空间虚拟化**。这里的每个符号在运行时是要对应到具体的内存地址的。这些内存地址的具体数值是什么?程序员不用关心。为什么?因为编译器会自动帮我们吧这些符号翻译成地址,形成可执行程序。程序使用的内存是否占得太大了?在一般情况下,程序员也不用关心。 +首先来看看内存的虚拟化。程序员在写应用程序的时候,不用考虑其程序的起始内存地址要放到计算机内存的具体某个位置,而是用字符串符号定义了各种变量和函数,直接在代码中便捷地使用这些符号就行了。这是由于操作系统建立了一个 *地址固定* , *空间巨大* 的虚拟内存给应用程序来运行,这是 **空间虚拟化** 。这里的每个符号在运行时是要对应到具体的内存地址的。这些内存地址的具体数值是什么?程序员不用关心。为什么?因为编译器会自动帮我们把这些符号翻译成地址,形成可执行程序。程序使用的内存是否占得太大了?在一般情况下,程序员也不用关心。 -> 还记得虚拟地址(逻辑地址)的描述吗? -> 但编译器\(compiler,比如gcc\)和链接器(linker,比如ld)也不知道程序每个符号对应的地址应该放在未来程序运行时的哪个物理内存地址中。所以,编译器的一个简单处理办法就是,设定一个固定地址(比如 0x10000)作为起始地址,开始存放代码,代码之后是数据,所有变量和函数的符号都在这个起始地址之后的某个固定偏移位置。假定程序每次运行都是位于一个不会变化的起始地址。 -> 这里的变量指的是全局变量,其地址在编译链接后会确定不变。但局部变量是放在堆栈中的,会随着堆栈大小的动态变化而变化。 -> 这里编译器产生的地址就是虚拟地址。 -> 这里,编译器和链接器图省事,找了一个适合它们的解决办法。当程序要运行的时候,这个符号到机器物理内存的映射必须要解决了,这自然就推到了操作系统身上。操作系统会把编译器和链接器生成的执行代码和数据放到物理内存中的空闲区域中,并建立虚拟地址到物理地址的映射关系。由于物理内存中的空闲区域是动态变化的,这也导致虚拟地址到物理地址的映射关系是动态变化的,需要操作系统来维护好可变的映射关系,确保编译器“固定起始地址”的假设成立。只有操作系统维护好了这个映射关系,才能让程序员只需写一些易于人理解的字符串符号来代表一个内存空间地址,且编译器只需确定一个固定地址作为程序的起始地址就可以生成一个不用考虑将来这个程序要在哪里运行的问题,从而实现了**空间虚拟化**。 +.. note:: -应用程序在运行时不用考虑当前物理内存是否够用。如果应用程序需要一定空间的内存,但由于在某些情况下,物理内存的空闲空间可能不多了,这时操作系统通过把物理内存中最近没使用的空间(不是空闲的,只是最近用得少)换出(就是“挪地”)到硬盘上暂时缓存起来,这样空闲空间就大了,就可以满足应用程序的运行时内存需求了,从而实现了**空间大小虚拟化**。 + 还记得虚拟地址(逻辑地址)的描述吗? + + 实际上,编译器 (Compiler,比如 gcc) 和链接器 (linker,比如 ld) 也不知道程序每个符号对应的地址应该放在未来程序运行时的哪个物理内存地址中。所以,编译器的一个简单处理办法就是,设定一个固定地址(比如 0x10000)作为起始地址开始存放代码,代码之后是数据,所有变量和函数的符号都在这个起始地址之后的某个固定偏移位置。假定程序每次运行都是位于一个不会变化的起始地址。这里的变量指的是全局变量,其地址在编译链接后会确定不变。但局部变量是放在堆栈中的,会随着堆栈大小的动态变化而变化。这里编译器产生的地址就是虚拟地址。 -CPU虚拟化 + 这里,编译器和链接器图省事,找了一个适合它们的解决办法。当程序要运行的时候,这个符号到机器物理内存的映射必须要解决了,这自然就推到了操作系统身上。操作系统会把编译器和链接器生成的执行代码和数据放到物理内存中的空闲区域中,并建立虚拟地址到物理地址的映射关系。由于物理内存中的空闲区域是动态变化的,这也导致虚拟地址到物理地址的映射关系是动态变化的,需要操作系统来维护好可变的映射关系,确保编译器“固定起始地址”的假设成立。只有操作系统维护好了这个映射关系,才能让程序员只需写一些易于人理解的字符串符号来代表一个内存空间地址,且编译器只需确定一个固定地址作为程序的起始地址就可以不用考虑将来这个程序要在哪里运行的问题,从而实现了 **空间虚拟化** 。 + +应用程序在运行时不用考虑当前物理内存是否够用。如果应用程序需要一定空间的内存,但由于在某些情况下,物理内存的空闲空间可能不多了,这时操作系统通过把物理内存中最近没使用的空间(不是空闲的,只是最近用得少)换出(就是“挪地”)到硬盘上暂时缓存起来,这样空闲空间就大了,就可以满足应用程序的运行时内存需求了,从而实现了 **空间大小虚拟化** 。 + +CPU 虚拟化 ~~~~~~~~~~~~~~ -再来看CPU虚拟化。不同的应用程序可以在内存中并发运行,相同的应用程序也可有多个拷贝在内存中并发运行。而每个程序都“认为”自己完全独占了CPU在运行,这是”时间虚拟化“。这其实也是操作系统给了运行的应用程序一个虚拟幻象。其实是操作系统把时间分成小段,每个应用程序占用其中一小段时间片运行,用完这一时间片后,操作系统会切换到另外一个应用程序,让它运行。由于时间片很短,操作系统的切换开销也很小,人眼基本上是看不出的,反而感觉到多个程序各自在独立”并行“执行,从而实现了**时间虚拟化**。 +再来看 CPU 虚拟化。不同的应用程序可以在内存中并发运行,相同的应用程序也可有多个拷贝在内存中并发运行。而每个程序都“认为”自己完全独占了 CPU 在运行,这是”时间虚拟化“。这其实也是操作系统给了运行的应用程序一个幻象。其实是操作系统把时间分成小段,每个应用程序占用其中一小段时间片运行,用完这一时间片后,操作系统会切换到另外一个应用程序,让它运行。由于时间片很短,操作系统的切换开销也很小,人眼基本上是看不出的,反而感觉到多个程序各自在独立”并行“执行,从而实现了 **时间虚拟化** 。 -> 并行(Parallel)是指两个或者多个事件在同一时刻发生;而并发(Concurrent)是指两个或多个事件在同一时间间隔内发生。 -> 对于单CPU的计算机而言,各个”同时“运行的程序其实是串行分时复用一个CPU,任一个时刻点上只有一个程序在CPU上运行。 -> 这些虚拟性的特征给应用程序的开发和执行提供了非常方便的环境,但也给操作系统的设计与实现提出了很多挑战。 +.. note:: + 并行 (Parallel) 是指两个或者多个事件在同一时刻发生;而并发 (Concurrent) 是指两个或多个事件在同一时间间隔内发生。 + 对于单 CPU 的计算机而言,各个”同时“运行的程序其实是串行分时复用一个 CPU ,任一个时刻点上只有一个程序在 CPU 上运行。 + 这些虚拟性的特征给应用程序的开发和执行提供了非常方便的环境,但也给操作系统的设计与实现提出了很多挑战。 并发性 ---------------------------------- -操作系统为了能够让CPU充分地忙起来并充分利用各种资源,就需要给很多任务给它去完成。这些任务是分时完成的,有操作系统来完成各个应用在运行时的任务切换。并发性虽然能有效改善系统资源的利用率,但并发性也带来了对共享资源的争夺问题,即同步互斥问题;执行时间的不确定性问题,即并发程序在执行中是走走停停,断续推进的。并发性对操作系统的设计也带来了很多挑战,一不小心就会出现程序执行结果不确定,程序死锁等很难调试和重现的问题。 +操作系统为了能够让 CPU 充分地忙起来并充分利用各种资源,就需要给很多任务给它去完成。这些任务是分时完成的,由操作系统来完成各个应用在运行时的任务切换。并发性虽然能有效改善系统资源的利用率,但也带来了对共享资源的争夺问题,即同步互斥问题;执行时间的不确定性问题,即并发程序在执行中是走走停停,断续推进的。并发性对操作系统的设计也带来了很多挑战,一不小心就会出现程序执行结果不确定,程序死锁等很难调试和重现的问题。 异步性 ---------------------------------- @@ -47,15 +50,17 @@ CPU虚拟化 共享性 ---------------------------------- -共享是指多个应用并发运行时,宏观上体现出它们可同时访问同一个资源,即这个资源可被共享。但其实在微观上,操作系统在硬件等的支持下要确保应用程序互斥或交替访问这个共享的资源。比如两个应用同时写访问同一个内存单元,从宏观的应用层面上看,二者都能正确地读出同一个内存单元的内容。而在微观上,操作系统会调度应用程序的先后执行顺序,在数据总线上任何一个时刻,只有一个应用去访问存储单元。 +共享是指多个应用并发运行时,宏观上体现出它们可同时访问同一个资源,即这个资源可被共享。但其实在微观上,操作系统在硬件等的支持下要确保应用程序互斥或交替访问这个共享的资源。比如两个应用同时访问同一个内存单元,从宏观的应用层面上看,二者都能正确地读出同一个内存单元的内容。而在微观上,操作系统会调度应用程序的先后执行顺序,在数据总线上任何一个时刻,只有一个应用去访问存储单元。 持久性 ---------------------------------- -操作系统提供了文件系统来从可持久保存的存储介质(硬盘,SSD等,以后以硬盘来代表)中取数据和代码到内存中,并可以把内存中的数据写回到硬盘上。硬盘在这里是外设,具有持久性,以文件系统的形式呈现给应用程序。 +操作系统提供了文件系统来从可持久保存的存储介质(磁盘, SSD 等,以后以硬盘来代表)中取数据和代码到内存中,并可以把内存中的数据写回到硬盘上。硬盘在这里是外设,具有持久性,以文件系统的形式呈现给应用程序。 -> 文件系统也可看成是操作系统对硬盘的虚拟化 -> 这种持久性的特征进一步带来了共享属性,即在文件系统中的文件可以被多个运行的程序所访问,从而给应用程序之间实现数据共享提供了方便。即使掉电,磁盘上的数据还不会丢失,可以在下一次机器加电后提供给运行的程序使用。持久性对操作系统的执行效率提出了挑战,如何让数据在高速的内存和慢速的硬盘间高效流动是需要操作系统考虑的问题。 +.. note:: + + 文件系统也可看成是操作系统对存储外设(如硬盘、SSD 等)的虚拟化。 + 这种持久性的特征进一步带来了共享属性,即在文件系统中的文件可以被多个运行的程序所访问,从而给应用程序之间实现数据共享提供了方便。即使掉电,存储外设上的数据还不会丢失,可以在下一次机器加电后提供给运行的程序使用。持久性对操作系统的执行效率提出了挑战,如何让数据在高速的内存和慢速的硬盘间高效流动是需要操作系统考虑的问题。 diff --git a/source/chapter0/5extend-reading.rst b/source/chapter0/5extend-reading.rst deleted file mode 100644 index 1dc5822c9513ca2539da34e88d249efb8254d627..0000000000000000000000000000000000000000 --- a/source/chapter0/5extend-reading.rst +++ /dev/null @@ -1,8 +0,0 @@ -扩展阅读 -================================================ - -.. toctree:: - :hidden: - :maxdepth: 5 - -扩展阅读部分 \ No newline at end of file diff --git a/source/chapter0/5setup-devel-env.rst b/source/chapter0/5setup-devel-env.rst new file mode 100644 index 0000000000000000000000000000000000000000..3ec7374c722490bb94b8abdb62cf6d3a33f04873 --- /dev/null +++ b/source/chapter0/5setup-devel-env.rst @@ -0,0 +1,293 @@ +实验环境配置 +============ + +.. toctree:: + :hidden: + :maxdepth: 4 + +本节我们将完成环境配置并成功运行 rCore-Tutorial-v3 。整个流程分为下面几个部分: + +- 系统环境配置 +- Rust 开发环境配置 +- Qemu 模拟器安装 +- 其他工具安装 +- 运行 rCore-Tutorial-v3 + +系统环境配置 +------------------------------- + +目前实验仅支持 Ubuntu18.04 + 操作系统。对于 Windows10 和 macOS 上的用户,可以使用 VMware 或 +VirtualBox 安装一台 Ubuntu18.04 虚拟机并在上面进行实验。 + +特别的,Windows10 的用户可以通过系统内置的 WSL2 虚拟机(请不要使用 WSL1)来安装 Ubuntu 18.04 / 20.04 。 +步骤如下: + +- 升级 Windows 10 到最新版(Windows 10 版本 18917 或以后的内部版本)。注意,如果 + 不是 Windows 10 专业版,可能需要手动更新,在微软官网上下载。升级之后, + 可以在 PowerShell 中输入 ``winver`` 命令来查看内部版本号。 +- 「Windows 设置 > 更新和安全 > Windows 预览体验计划」处选择加入 “Dev 开发者模式”。 +- 以管理员身份打开 PowerShell 终端并输入以下命令: + + .. code-block:: + + # 启用 Windows 功能:“适用于 Linux 的 Windows 子系统” + >> dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart + + # 启用 Windows 功能:“已安装的虚拟机平台” + >> dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart + + # 改为对应从微软应用商店安装的 Linux 版本名,比如:`wsl --set-version Ubuntu 2` + # 如果你没有提前从微软应用商店安装任何 Linux 版本,请跳过此步骤 + >> wsl --set-version 2 + + # 设置默认为 WSL 2,如果 Windows 版本不够,这条命令会出错 + >> wsl --set-default-version 2 + +- `下载 Linux 内核安装包 `_ +- 在微软商店(Microsoft Store)中搜索并安装 Ubuntu18.04 / 20.04。 + +如果你打算使用 VMware 安装虚拟机的话,我们已经配置好了一个能直接运行 rCore-Tutorial-v3 的 +Ubuntu18.04 镜像,它是一个 ``vmdk`` 格式的虚拟磁盘文件,只需要在 VMware 中新建一台虚拟机, +在设置虚拟磁盘的时候选择它即可。`百度网盘链接 `_ (提取码 x5mf ) +或者 `清华云盘链接 `_ 。 +已经创建好用户 oslab ,密码为一个空格。它已经安装了中文输入法和 Markdown 编辑器 Typora 还有作为 Rust 集成开发环境的 +Visual Studio Code,能够更容易完成实验并撰写实验报告。 + +.. _link-docker-env: + +.. note:: + + **Docker 开发环境** + + 感谢 dinghao188 和张汉东老师帮忙配置好的 Docker 开发环境,进入 Docker 开发环境之后不需要任何软件工具链的安装和配置,可以直接将 tutorial 运行起来,目前应该仅支持将 tutorial 运行在 Qemu 模拟器上。 + + 使用方法如下(以 Ubuntu18.04 为例): + + 1. 通过 ``su`` 切换到管理员账户 ``root`` ; + 2. 在 ``rCore-Tutorial-v3`` 根目录下 ``make docker`` 进入到 Docker 环境; + 3. 进入 Docker 之后,会发现当前处于根目录 ``/`` ,我们通过 ``cd mnt`` 将当前工作路径切换到 ``/mnt`` 目录; + 4. 通过 ``ls`` 可以发现 ``/mnt`` 目录下的内容和 ``rCore-Tutorial-v3`` 目录下的内容完全相同,接下来就可以在这个环境下运行 tutorial 了。例如 ``cd os && make run`` 。 + + +你也可以在 Windows10 或 macOS 原生系统或者其他 Linux 发行版上进行实验,基本上不会出现太大的问题。不过由于 +时间问题我们只在 Ubuntu18.04 上进行了测试,后面的配置也都是基于它的。如果遇到了问题的话,请在本节的讨论区 +中留言,我们会尽量帮助解决。 + +Rust 开发环境配置 +------------------------------------------- + +首先安装 Rust 版本管理器 rustup 和 Rust 包管理器 cargo,这里我们用官方的安装脚本来安装: + +.. code-block:: bash + + curl https://sh.rustup.rs -sSf | sh + +如果通过官方的脚本下载失败了,可以在浏览器的地址栏中输入 ``_ 来下载脚本,在本地运行即可。 + +如果官方的脚本在运行时出现了网络速度较慢的问题,可选地可以通过修改 rustup 的镜像地址 +(修改为中国科学技术大学的镜像服务器)来加速: + +.. code-block:: bash + + export RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static + export RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup + curl https://sh.rustup.rs -sSf | sh + +或者使用tuna源来加速 `参见 rustup 帮助 `_: + +.. code-block:: bash + + export RUSTUP_DIST_SERVER=https://mirrors.tuna.edu.cn/rustup + export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.edu.cn/rustup/rustup + curl https://sh.rustup.rs -sSf | sh + +或者也可以通过在运行前设置命令行中的科学上网代理来实现: + +.. code-block:: bash + + # e.g. Shadowsocks 代理,请根据自身配置灵活调整下面的链接 + export https_proxy=http://127.0.0.1:1080 + export http_proxy=http://127.0.0.1:1080 + export ftp_proxy=http://127.0.0.1:1080 + +安装完成后,我们可以重新打开一个终端来让之前设置的环境变量生效。我们也可以手动将环境变量设置应用到当前终端, +只需要输入以下命令: + +.. code-block:: bash + + source $HOME/.cargo/env + +接下来,我们可以确认一下我们正确安装了 Rust 工具链: + +.. code-block:: bash + + rustc --version + +可以看到当前安装的工具链的版本。 + +.. code-block:: bash + + rustc 1.46.0-nightly (7750c3d46 2020-06-26) + +.. warning:: + 目前用于操作系统实验开发的rustc编译器的版本不局限在1.46.0这样的数字上,你可以选择更新的rustc编译器。但注意只能用rustc的nightly版本。 + + +可通过如下命令安装rustc的nightly版本,并把该版本设置为rustc的缺省版本。 + +.. code-block:: bash + + rustup install nightly + rustup default nightly + + +我们最好把软件包管理器 cargo 所用的软件包镜像地址 crates.io 也换成中国科学技术大学的镜像服务器来加速三方库的下载。 +我们打开(如果没有就新建) ``~/.cargo/config`` 文件,并把内容修改为: + +.. code-block:: toml + + [source.crates-io] + registry = "https://github.com/rust-lang/crates.io-index" + replace-with = 'ustc' + [source.ustc] + registry = "git://mirrors.ustc.edu.cn/crates.io-index" + +同样,也可以使用tuna源 `参见 crates.io 帮助 `_: + +.. code-block:: toml + + [source.crates-io] + replace-with = 'tuna' + + [source.tuna] + registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git" + +接下来安装一些Rust相关的软件包 + +.. code-block:: bash + + rustup target add riscv64gc-unknown-none-elf + cargo install cargo-binutils + rustup component add llvm-tools-preview + rustup component add rust-src + +.. warning:: + 如果你换了另外一个rustc编译器(必须是nightly版的),需要重新安装上述rustc所需软件包。 + rCore-Tutorial 仓库中的 ``Makefile`` 包含了这些工具的安装,如果你使用 ``make run`` 也可以不手动安装。 + +至于 Rust 开发环境,推荐 JetBrains Clion + Rust插件 或者 Visual Studio Code 搭配 rust-analyzer 和 RISC-V Support 插件。 + +.. note:: + + * JetBrains Clion是付费商业软件,但对于学生和教师,只要在 JetBrains 网站注册账号,可以享受一定期限(半年左右)的免费使用的福利。 + * Visual Studio Code 是开源软件,不用付费就可使用。 + * 当然,采用 VIM,Emacs 等传统的编辑器也是没有问题的。 + +Qemu 模拟器安装 +---------------------------------------- + +我们需要使用 Qemu 5.0.0 版本进行实验,而很多 Linux 发行版的软件包管理器默认软件源中的 Qemu 版本过低,因此 +我们需要从源码手动编译安装 Qemu 模拟器。 + +首先我们安装依赖包,获取 Qemu 源代码并手动编译: + +.. code-block:: bash + + # 安装编译所需的依赖包 + sudo apt install autoconf automake autotools-dev curl libmpc-dev libmpfr-dev libgmp-dev \ + gawk build-essential bison flex texinfo gperf libtool patchutils bc \ + zlib1g-dev libexpat-dev pkg-config libglib2.0-dev libpixman-1-dev git tmux python3 + # 下载源码包 + # 如果下载速度过慢可以使用我们提供的百度网盘链接:https://pan.baidu.com/s/1z-iWIPjxjxbdFS2Qf-NKxQ + # 提取码 8woe + wget https://download.qemu.org/qemu-5.0.0.tar.xz + # 解压 + tar xvJf qemu-5.0.0.tar.xz + # 编译安装并配置 RISC-V 支持 + cd qemu-5.0.0 + ./configure --target-list=riscv64-softmmu,riscv64-linux-user + make -j$(nproc) + +.. note:: + + 注意,上面的依赖包可能并不完全,比如在 Ubuntu 18.04 上: + + - 出现 ``ERROR: pkg-config binary 'pkg-config' not found`` 时,可以安装 ``pkg-config`` 包; + - 出现 ``ERROR: glib-2.48 gthread-2.0 is required to compile QEMU`` 时,可以安装 + ``libglib2.0-dev`` 包; + - 出现 ``ERROR: pixman >= 0.21.8 not present`` 时,可以安装 ``libpixman-1-dev`` 包。 + + 另外一些 Linux 发行版编译 Qemu 的依赖包可以从 `这里 `_ + 找到。 + +之后我们可以在同目录下 ``sudo make install`` 将 Qemu 安装到 ``/usr/local/bin`` 目录下,但这样经常会引起 +冲突。个人来说更习惯的做法是,编辑 ``~/.bashrc`` 文件(如果使用的是默认的 ``bash`` 终端),在文件的末尾加入 +几行: + +.. code-block:: bash + + # 请注意,qemu-5.0.0 的父目录可以随着你的实际安装位置灵活调整 + export PATH=$PATH:/home/shinbokuow/Downloads/built/qemu-5.0.0 + export PATH=$PATH:/home/shinbokuow/Downloads/built/qemu-5.0.0/riscv64-softmmu + export PATH=$PATH:/home/shinbokuow/Downloads/built/qemu-5.0.0/riscv64-linux-user + +随后即可在当前终端 ``source ~/.bashrc`` 更新系统路径,或者直接重启一个新的终端。 + +此时我们可以确认 Qemu 的版本: + +.. code-block:: bash + + qemu-system-riscv64 --version + qemu-riscv64 --version + +K210 真机串口通信 +------------------------------ + +为了能在 K210 真机上运行 Tutorial,我们还需要安装基于 Python 的串口通信库和简易的串口终端。 + +.. code-block:: bash + + pip3 install pyserial + sudo apt install python-serial + +GDB 调试支持 +------------------------------ + +在 ``os`` 目录下 ``make debug`` 可以调试我们的内核,这需要安装终端复用工具 ``tmux`` ,还需要基于 riscv64 平台的 gdb 调试器 ``riscv64-unknown-elf-gdb`` 。该调试器包含在 riscv64 gcc 工具链中,工具链的预编译版本可以在如下链接处下载: + +- `Ubuntu 平台 `_ +- `macOS 平台 `_ +- `Windows 平台 `_ +- `CentOS 平台 `_ + +解压后在 ``bin`` 目录下即可找到 ``riscv64-unknown-elf-gdb`` 以及另外一些常用工具 ``objcopy/objdump/readelf`` 等。 + +运行 rCore-Tutorial-v3 +------------------------------------------------------------ + +如果是在 Qemu 平台上运行,只需在 ``os`` 目录下 ``make run`` 即可。在内核加载完毕之后,可以看到目前可以用的 +应用程序。 ``usertests`` 打包了其中的很大一部分,所以我们可以运行它,只需输入在终端中输入它的名字即可。 + +.. image:: qemu-final.gif + +之后,可以先按下 ``Ctrl+A`` ,再按下 ``X`` 来退出 Qemu。 + +如果是在 K210 平台上运行则略显复杂。 + +首先,我们需要将 MicroSD 插入 PC 来将文件系统镜像拷贝上去。 + +.. image:: prepare-sd.gif + +.. warning:: + + 在 ``os/Makefile`` 中我们默认设置 MicroSD 在当前操作系统中可以用设备 ``SDCARD=/dev/sdb`` 访问。你可以使用 ``df -hT`` 命令来确认在你的环境中 MicroSD 是哪个设备, + 并在 ``make sdcard`` 之前对 ``os/Makefile`` 的 ``SDCARD`` 配置做出适当的修改。不然,这有可能导致 **设备 /dev/sdb 上数据丢失**! + +随后,我们将 MicroSD 插入 K210 开发板,将 K210 开发板连接到 PC ,然后进入 ``os`` 目录 ``make run BOARD=k210`` +在 K210 开发板上跑 Tutorial 。 + +.. image:: k210-final.gif + +之后,可以按下 ``Ctrl+]`` 来退出串口终端。 + +到这里,恭喜你完成了实验环境的配置,可以开始阅读教程的正文部分了! diff --git a/source/chapter0/6hardware.rst b/source/chapter0/6hardware.rst new file mode 100644 index 0000000000000000000000000000000000000000..5c7a6e85df1517c8da85feefe756f79c0b59e66b --- /dev/null +++ b/source/chapter0/6hardware.rst @@ -0,0 +1,72 @@ +K210 开发板相关问题 +===================================================== + +我们采用的真实硬件平台 Kendryte K210 在设计的时候基于 RISC-V 特权级架构 1.9.1 版本,它发布于 2016 年,目前已经不被 +主流工具链所支持了。麻烦的是, 1.9.1 版本和后续版本确实有很多不同。为此,RustSBI 做了很多兼容性工作,使得基于新版规范 +的软件几乎可以被不加修改的运行在 Kendryte K210 上。在这里我们先简单介绍一些开发板相关的问题。 + +K210 相关 Demo 和文档 +-------------------------------------------- + +- `K210 datasheet `_ +- `K210 官方 SDK `_ +- `K210 官方 SDK 文档 `_ +- `K210 官方 SDK Demo `_ +- `K210 Demo in Rust `_ + +K210 相关工具 +-------------------------------------------- + +JTAG 调试 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- `一篇 Blog `_ +- `Sipeed 工程师提供的详细配置文档 `_ +- `MaixDock OpenOCD 调试配置 `_ + +烧写 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- `kflash.py `_ +- `kflash_gui `_ + + +K210 可用内存大小 +-------------------------------------------- + +K210 的内存是由 CPU 和 KPU 共享使用的,如果想要 CPU 能够使用全部的 :math:`8\text{MiB}` 需要满足三个条件: + +- KPU 不能处于工作状态; +- PLL1 必须被使能; +- PLL1 的 clock gate 必须处于打开状态。 + +否则, CPU 仅能够使用 :math:`6\text{MiB}` 内存。 + +我们进行如下操作即可让 CPU 使用全部 :math:`8\text{MiB}` 内存(基于官方 SDK): + +.. code-block:: c + + sysctl_pll_enable(SYSCTL_PLL1); + syscyl_clock_enable(SYSCTL_CLOCK_PLL1); + +K210 的频率 +-------------------------------------------- +默认情况下,K210 的 CPU 频率为 403000000 ,约 :math:`400\text{MHz}` 。而计数器 ``mtime`` CSR 增长的频率为 +CPU 频率的 1/62 ,约 :math:`6.5\text{MHz}` 。 + + +K210 的 MMU 支持 +-------------------------------------------- + +K210 有完善的 SV39 多级页表机制,然而它是基于 1.9.1 版本特权级架构的,和我们目前使用的有一些不同。不过在 RustSBI +的帮助下,本项目中完全看不出 Qemu 和 K210 两个平台在这方面的区别。详情请参考 +`RustSBI 的设计与实现 `_ +的 P11 。 + +K210 的外部中断支持 +-------------------------------------------- + +K210 的 S 特权级外部中断不存在(被硬件置为零),因此任何软件/硬件代理均无法工作。为此,RustSBI 专门提供了一个新的 SBI +call ,让 S 模式软件可以编写 S 特权级外部中断的 handler 并注册到 RustSBI 中,在中断触发的时候由 RustSBI 调用该 +handler 处理中断。详情请参考 `RustSBI 的设计与实现 `_ +的 P12 。 \ No newline at end of file diff --git a/source/chapter0/EE.png b/source/chapter0/EE.png new file mode 100644 index 0000000000000000000000000000000000000000..8fdb4add3d1ccbe585e858ac4b9cc390fb52be9c Binary files /dev/null and b/source/chapter0/EE.png differ diff --git a/source/chapter0/address-space.png b/source/chapter0/address-space.png new file mode 100644 index 0000000000000000000000000000000000000000..3195d2ac33e35c226cc1bba22436a0e790b90476 Binary files /dev/null and b/source/chapter0/address-space.png differ diff --git a/source/chapter0/basic-EE.png b/source/chapter0/basic-EE.png new file mode 100644 index 0000000000000000000000000000000000000000..a7ef96b8e77e07a0a79668227b68a584594ce10f Binary files /dev/null and b/source/chapter0/basic-EE.png differ diff --git a/source/chapter0/complex-EE.png b/source/chapter0/complex-EE.png new file mode 100644 index 0000000000000000000000000000000000000000..2098b325c587e61a3a741420352bf1ff674482d3 Binary files /dev/null and b/source/chapter0/complex-EE.png differ diff --git a/source/chapter0/computer-hw-sw.png b/source/chapter0/computer-hw-sw.png new file mode 100644 index 0000000000000000000000000000000000000000..4ff6393f1f44f5e01eb5b46e8075340e19989288 Binary files /dev/null and b/source/chapter0/computer-hw-sw.png differ diff --git a/source/chapter0/context-of-process.png b/source/chapter0/context-of-process.png new file mode 100644 index 0000000000000000000000000000000000000000..b6db9a1db479c4dc2fdbdcfb9d3f7e933c6787b3 Binary files /dev/null and b/source/chapter0/context-of-process.png differ diff --git a/source/chapter0/context-switch.png b/source/chapter0/context-switch.png new file mode 100644 index 0000000000000000000000000000000000000000..35718ffc92081b539bf3c98294b39ef80bb0b8a0 Binary files /dev/null and b/source/chapter0/context-switch.png differ diff --git a/source/chapter0/exception.png b/source/chapter0/exception.png new file mode 100644 index 0000000000000000000000000000000000000000..39e369f50d712ca9c2ff737ea87d4bd50088129a Binary files /dev/null and b/source/chapter0/exception.png differ diff --git a/source/chapter0/file-disk.png b/source/chapter0/file-disk.png new file mode 100644 index 0000000000000000000000000000000000000000..148d9432510b65f5f2d863f1436cad9a635208d0 Binary files /dev/null and b/source/chapter0/file-disk.png differ diff --git a/source/chapter0/index.rst b/source/chapter0/index.rst index 277298ebf5c6cbd8664334762678a58f48181b27..0befd8482cb2f54c85c3aa8ea3157f381e15be73 100644 --- a/source/chapter0/index.rst +++ b/source/chapter0/index.rst @@ -1,29 +1,16 @@ -第零章: 操作系统概述 +.. _link-chapter0: + +第零章:操作系统概述 ============================================== .. toctree:: - :hidden: :maxdepth: 4 + 0intro 1what-is-os 2os-interface 3os-hw-abstract 4os-features - 5extend-reading - - -为何要写这本操作系统书 ------------------------ - -现在国内外已有一系列优秀的操作系统教材,例如William Stallings的《Operating Systems Internals and Design Principles》,Avi Silberschatz、Peter Baer Galvin 和 Greg Gagne 的《Operating System Concepts》,Remzi H. Arpaci-Dusseau 和 Andrea C. Arpaci-Dusseau 的《Operating Systems: Three Easy Pieces》等。然而,从我们从2000年以来的教学实践来看,某些经典教材对操作系统的概念和原理很重视,但缺乏对操作系统的概念/原理与操作系统的实现之间建立一个联系的桥梁,导致学生发现操作系统实现相关的实验与操作系统的概念相比,有较大的代沟(gap)。此外,部分教材把 x86 作为的操作系统实验的硬件参考平台,缺乏对当前快速发展的RISC-V等体系结构的实验支持,使得学生在操作系统实验中可能需要花较大代价了解相对繁杂的x86硬件细节,影响操作系统实验的效果。还有部分教材也基本以 Linux/Unix 等实际操作系统为主,难以让学生在一个学期内掌握期中的核心设计。 - -对于在校的学生和已经参加工作的工程师而言,能否以较小的时间和精力比较全面地了解操作系统呢?陆游老夫子说过“纸上得来终觉浅,绝知此事要躬行”,也许在了解基本的操作系统概念和原理基础上,通过实际动手来一步一步分析、设计和实现一个操作系统,会发现操作系统原来如此,概念原理和实际实现之间有紧密的联系和巨大的差异。 - -也许学生有疑问,在本科期间自己能通过设计实现一个操作系统吗?这一点其实已经是一个实际存在的现实情况了。MIT教授 Frans Kaashoek等师生设计实现了基于UNIX v6的xv6教学操作系统用于每年的本科操作系统课的实验中,而且在2019年,他们进一步改进了xv6,让其运行在RISC-V CPU上。RISC-V CPU同样来源于高校,是Berkeley教授David Patterson等师生设计实现的第五代RISC CPU(现在简称RISC-V),用于计算机组成原理和计算机体系机构课程的教学和科研中。 - - -这些都给了我们很大的启发:对一个计算机专业的本科生而言,设计实现一个操作系统(包括CPU)有挑战但可行! 所以本书的目标是以简洁的RISC-V CPU为底层硬件基础,根据上层应用从小到大的需求,逐步讲解如何设计并实现满足这些需求的“从小到大”的多个操作系统。并在实现操作系统的过程中,逐步解析操作系统各种概念与原理的知识点,对应的做到有“理”可循和有“码”可查,最终让读者了解和掌握操作系统的原理、设计与实现。 - -在具体撰写过程中,第零章是对操作系统的一个概述,让读者对操作系统的历史、定义、特征等概念上有一个大致的了解。后面的每个章节体现了操作系统的一个微缩的历史发展过程,即从对应用由简到繁的支持的角度出发,每章会讲解如何设计一个可运行应用的操作系统,满足应用的阶段性需求。从而读者可以通过对应配套的操作系统设计实验,了解如何从一个微不足道的小操作系统,根据应用需求,添加或增强操作系统功能,逐步形成一个类似UNIX的相对完善的操作系统。每一步都小到足以让人感觉到易于掌控,而在每一步结束时,你都有一个可以工作的操作系统。另外,通过足够详尽的`测试程序 https://github.com/rcore-os/rcore-tutorial-tests`_ ,可以随时验证读者实现的操作系统在每次更新后是否正常工作。由于实验的代码规模和实现复杂度在一个逐步递增的可控范围内,读者可以结合对应于操作系统设计实验的进一步的原理讲解,来建立操作系统概念原理和实际实现的对应关系,从而能够通过操作系统实验的实践过程来加强对理论概念的理解,通过理论概念来进一步指导操作系统实验的实现与改进。 + 5setup-devel-env + 6hardware -在你开始阅读与实践本书讲解的内容之前,你需要决定用什么编程语言来完成操作系统实验。你可以用任何你喜欢的编程语言来实现操作系统。我们推荐的编程语言是Rust语言或者C语言。 diff --git a/source/chapter0/interrupt.png b/source/chapter0/interrupt.png new file mode 100644 index 0000000000000000000000000000000000000000..e26b40031fbdbc1e1d36a03ab5348c45566675df Binary files /dev/null and b/source/chapter0/interrupt.png differ diff --git a/source/chapter0/k210-final.gif b/source/chapter0/k210-final.gif new file mode 100755 index 0000000000000000000000000000000000000000..d887a483fcb61354c9f154cdd3b714e8429de99b Binary files /dev/null and b/source/chapter0/k210-final.gif differ diff --git a/source/chapter0/prepare-sd.gif b/source/chapter0/prepare-sd.gif new file mode 100644 index 0000000000000000000000000000000000000000..f0a59a3c259ae665f55c17049e10a0d57e2b857c Binary files /dev/null and b/source/chapter0/prepare-sd.gif differ diff --git a/source/chapter0/prog-illusion.png b/source/chapter0/prog-illusion.png new file mode 100644 index 0000000000000000000000000000000000000000..55b6ac807a43379dd93bea90a4141be6b3d5cbd9 Binary files /dev/null and b/source/chapter0/prog-illusion.png differ diff --git a/source/chapter0/qemu-final.gif b/source/chapter0/qemu-final.gif new file mode 100644 index 0000000000000000000000000000000000000000..3c14ac72500da6d13eb348e2c8abee1898e3eac4 Binary files /dev/null and b/source/chapter0/qemu-final.gif differ diff --git a/source/chapter0/run-app.png b/source/chapter0/run-app.png new file mode 100644 index 0000000000000000000000000000000000000000..c3d7fbbccf7468b21e47591818e47d31518344fc Binary files /dev/null and b/source/chapter0/run-app.png differ diff --git a/source/chapter0/syscall.png b/source/chapter0/syscall.png new file mode 100644 index 0000000000000000000000000000000000000000..5305a4719bb1ef5a8d911218e56711437fa202a1 Binary files /dev/null and b/source/chapter0/syscall.png differ diff --git a/source/chapter1/0intro.rst b/source/chapter1/0intro.rst new file mode 100644 index 0000000000000000000000000000000000000000..7f0793ddb9d08a809071d0061444d24b3ecff102 --- /dev/null +++ b/source/chapter1/0intro.rst @@ -0,0 +1,118 @@ +引言 +===================== + +本章导读 +-------------------------- + +.. + 这是注释:我觉得需要给出执行环境(EE),Task,...等的描述。 + 并且有一个图,展示这些概念的关系。 + +本章展现了操作系统一个功能:让应用与硬件隔离,简化了应用访问硬件的难度和复杂性。 + +大多数程序员的第一行代码都从 ``Hello, world!`` 开始,当我们满怀着好奇心在编辑器内键入仅仅数个字节,再经过几行命令编译(靠的是编译器)、运行(靠的是操作系统),终于在黑洞洞的终端窗口中看到期望中的结果的时候,一扇通往编程世界的大门已经打开。在本章第一节 :doc:`1app-ee-platform` 中,可以看到用Rust语言编写的非常简单的“Hello, world”应用程序。 + +不过我们能够隐约意识到编程工作能够如此方便简洁并不是理所当然的,实际上有着多层硬件和软件工具和支撑环境隐藏在它背后,才让我们不必付出那么多努力就能够创造出功能强大的应用程序。生成应用程序二进制执行代码所依赖的是以 **编译器** 为主的开发环境;运行应用程序执行码所依赖的是以 **操作系统** 为主的执行环境。 + +本章主要是设计和实现建立在裸机上的执行环境,从中对应用程序和它所依赖的执行环境有一个全面和深入的理解。 + +本章我们的目标仍然只是输出 ``Hello, world!`` ,但这一次,我们将离开舒适区,基于一个几乎空无一物的平台从零开始搭建我们自己的高楼大厦,而不是仅仅通过一行语句就完成任务。所以,在接下来的内容中,我们将描述如何让 ``Hello, world!`` 应用程序逐步脱离对编译器、运行时和操作系统的现有复杂依赖,最终以最小的依赖需求能在裸机上运行。这时,我们也可把这个能在裸机上运行的 ``Hello, world!`` 应用程序称为一种支持输出字符串的非常初级的寒武纪“三叶虫”操作系统,它其实就是一个给应用提供各种服务(比如输出字符串)的库,方便了单一应用程序在裸机上的开发与运行。输出字符串功能好比是三叶虫的眼睛,有了它,我们就有了最基本的调试功能,即通过在代码中的不同位置插入特定内容的输出语句来实现对程序运行的调试。 + +.. note:: + + 在操作系统发展历史上,在1956年就诞生了操作系统GM-NAA I/O,并且被实际投入使用,它的一个主要任务就是"自动加载运行一个接一个的程序"。 + +实践体验 +--------------------------- + +本章设计实现了一个支持显示字符串应用的简单操作系统--“三叶虫”操作系统。 + +获取本章代码: + +.. code-block:: console + + $ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git + $ cd rCore-Tutorial-v3 + $ git checkout ch1 + +在 qemu 模拟器上运行本章代码,看看一个小应用程序是如何在QEMU模拟的计算机上运行的: + +.. code-block:: console + + $ cd os + $ make run + +将 Maix 系列开发板连接到 PC,并在上面运行本章代码,看看一个小应用程序是如何在真实计算机上运行的: + +.. code-block:: console + + $ cd os + $ make run BOARD=k210 + +.. warning:: + + **FIXME: 提供 wsl/macOS 等更多平台支持** + +如果顺利的话,以 qemu 平台为例,将输出: + +.. code-block:: + + [rustsbi] RustSBI version 0.1.1 + .______ __ __ _______.___________. _______..______ __ + | _ \ | | | | / | | / || _ \ | | + | |_) | | | | | | (----`---| |----`| (----`| |_) || | + | / | | | | \ \ | | \ \ | _ < | | + | |\ \----.| `--' |.----) | | | .----) | | |_) || | + | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| + + [rustsbi] Platform: QEMU (Version 0.1.0) + [rustsbi] misa: RV64ACDFIMSU + [rustsbi] mideleg: 0x222 + [rustsbi] medeleg: 0xb1ab + [rustsbi-dtb] Hart count: cluster0 with 1 cores + [rustsbi] Kernel entry: 0x80200000 + Hello, world! + .text [0x80200000, 0x80202000) + .rodata [0x80202000, 0x80203000) + .data [0x80203000, 0x80203000) + boot_stack [0x80203000, 0x80213000) + .bss [0x80213000, 0x80213000) + Panicked at src/main.rs:46 Shutdown machine! + +除了 ``Hello, world!`` 之外还有一些额外的信息,最后关机。 + + +.. note:: + + RustSBI是啥? + + 戳 :doc:`../appendix-c/index` 可以进一步了解RustSBI。 + +本章代码树 +------------------------------------------------ + +.. code-block:: + + ├── bootloader(内核依赖的运行在 M 特权级的 SBI 实现,本项目中我们使用 RustSBI) + │   ├── rustsbi-k210.bin(可运行在 k210 真实硬件平台上的预编译二进制版本) + │   └── rustsbi-qemu.bin(可运行在 qemu 虚拟机上的预编译二进制版本) + ├── LICENSE + ├── os(我们的内核实现放在 os 目录下) + │   ├── Cargo.toml(内核实现的一些配置文件) + │   ├── Makefile + │   └── src(所有内核的源代码放在 os/src 目录下) + │   ├── console.rs(将打印字符的 SBI 接口进一步封装实现更加强大的格式化输出) + │   ├── entry.asm(设置内核执行环境的的一段汇编代码) + │   ├── lang_items.rs(需要我们提供给 Rust 编译器的一些语义项,目前包含内核 panic 时的处理逻辑) + │   ├── linker-k210.ld(控制内核内存布局的链接脚本以使内核运行在 k210 真实硬件平台上) + │   ├── linker-qemu.ld(控制内核内存布局的链接脚本以使内核运行在 qemu 虚拟机上) + │   ├── main.rs(内核主函数) + │   └── sbi.rs(调用底层 SBI 实现提供的 SBI 接口) + ├── README.md + ├── rust-toolchain(控制整个项目的工具链版本) + └── tools(自动下载的将内核烧写到 k210 开发板上的工具) + ├── kflash.py + ├── LICENSE + ├── package.json + ├── README.rst + └── setup.py diff --git a/source/chapter1/1app-ee-platform.rst b/source/chapter1/1app-ee-platform.rst index 44b7cea8655c19605383ca7ec91797d5534f43a0..9e685865bac178c9ac3f5f96823e2d01bb235e73 100644 --- a/source/chapter1/1app-ee-platform.rst +++ b/source/chapter1/1app-ee-platform.rst @@ -1,11 +1,20 @@ -应用程序运行环境与平台支持 +应用程序执行环境与平台支持 ================================================ .. toctree:: :hidden: :maxdepth: 5 -作为一切的开始,让我们使用 Cargo 工具来创建一个 Rust 项目。它看上去没有任何特别之处: + +本节导读 +------------------------------- + +本节介绍了如何设计实现一个提供显示字符服务的用户态执行环境和裸机执行环境,以支持一个应用程序显示字符串。显示字符服务的裸机执行环境和用户态执行环境向下直接或间接与硬件关联,向上可通过函数库给应用提供 **显示字符** 的服务。这也说明了不管执行环境是简单还是复杂,设计实现上是否容易,它都体现了不同操作系统的共性特征--给应用需求提供服务。在某种程度上看,执行环境的软件主体就可称为是一种操作系统。 + +执行应用程序 +------------------------------- + +我们先在Linux上开发并运行一个简单的“Hello, world”应用程序,看看一个简单应用程序从开发到执行的全过程。作为一切的开始,让我们使用 Cargo 工具来创建一个 Rust 项目。它看上去没有任何特别之处: .. code-block:: console @@ -23,8 +32,7 @@ 1 directory, 2 files -其中 ``Cargo.toml`` 中保存着项目的配置,包括作者的信息、联系方式以及库依赖等等。显而易见源代码保存在 ``src`` 目录下,目前为止只有 ``main.rs`` -一个文件,让我们看一下里面的内容: +其中 ``Cargo.toml`` 中保存着项目的配置,包括作者的信息、联系方式以及库依赖等等。显而易见源代码保存在 ``src`` 目录下,目前为止只有 ``main.rs`` 一个文件,让我们看一下里面的内容: .. code-block:: rust :linenos: @@ -44,118 +52,136 @@ Running `target/debug/os` Hello, world! -如我们预想的一样,我们在屏幕上看到了一行 ``Hello, world!`` 。但是,需要注意到我们所享受到的编程的方便并不是理所当然的,背后有着从硬件 -到软件的多种机制的支持。 +如我们预想的一样,我们在屏幕上看到了一行 ``Hello, world!`` 。但是,需要注意到我们所享受到的编程和执行程序的方便性并不是理所当然的,背后有着从硬件到软件的多种机制的支持。特别是对于应用程序的运行,是需要有一个强大的执行环境来帮助。接下来,我们就要看看有操作系统加持的强大的执行环境。 -应用程序运行环境 +应用程序执行环境 ------------------------------- -如下图所示,应用程序的运行需要下面一套运行环境栈的支持: +如下图所示,现在通用操作系统(如 Linux 等)上的应用程序运行需要下面一套多层次的执行环境栈的支持: .. _app-software-stack: .. figure:: app-software-stack.png :align: center - 应用程序运行环境栈:图中的白色块自上而下(越往下则越靠近底层,下层作为上层的执行环境支持上层代码的运行)表示各级运行环境, - 黑色块则表示相邻两层运行环境之间的接口。 + 应用程序执行环境栈:图中的白色块自上而下(越往下则越靠近底层,下层作为上层的执行环境支持上层代码的运行)表示各级执行环境,黑色块则表示相邻两层执行环境之间的接口。 + +.. _term-execution-environment: + +我们的应用位于最上层,它可以通过调用编程语言提供的标准库或者其他三方库对外提供的功能强大的函数接口,使得仅需少量的源代码就能完成复杂的功能。但是这些库的功能不仅限于此,事实上它们属于应用程序的 **执行环境** (Execution Environment),在我们通常不会注意到的地方,它们还会在执行应用之前完成一些初始化工作,并在应用程序执行的时候对它进行监控。我们在打印 ``Hello, world!`` 时使用的 ``println!`` 宏正是由 Rust 标准库 std 和 GNU Libc 库等提供的。 -我们的应用位于最上层,它可以通过调用编程语言提供的标准库或者其他三方库对外提供的功能强大的函数接口,使得仅需少量的源代码就能完成复杂的 -功能。但是这些库的功能不仅限于此,事实上它们属于应用程序的 **执行环境** (Execution Environment),在我们通常不会注意到的地方,它 -们还会在执行应用之前完成一些初始化工作,并在应用程序执行的时候对它进行监控。我们在打印 ``Hello, world!`` 时使用的 ``println!`` -宏正是由 Rust 标准库 std 提供的。 +.. _term-system-call: -从内核/操作系统的角度看来,它上面的一切都属于用户态,而它自身属于内核态。无论用户态应用如何编写,是手写汇编代码,还是基于某种编程语言利用 -其标准库或三方库,某些功能总要直接或间接的通过内核/操作系统提供的 **系统调用** (System Call) 来实现。因此系统调用充当了用户和内核之间 -的边界。内核作为用户态的运行环境,它不仅要提供系统调用接口,还需要对用户态应用的执行进行监控和管理。 +从内核/操作系统的角度看来,它上面的一切都属于用户态,而它自身属于内核态。无论用户态应用如何编写,是手写汇编代码,还是基于某种编程语言利用其标准库或三方库,某些功能总要直接或间接的通过内核/操作系统提供的 **系统调用** (System Call) 来实现。因此系统调用充当了用户和内核之间的边界。内核作为用户态的执行环境,它不仅要提供系统调用接口,还需要对用户态应用的执行进行监控和管理。 .. note:: **Hello, world! 用到了哪些系统调用?** - 从之前的 ``cargo run`` 的输出可以看出之前构建的可执行文件是在 target/debug 目录下的 os 。 - 在 Ubuntu 系统上,可以通过 ``strace`` 工具来运行一个程序并输出程序运行过程当中向内核请求的所有的系统调用及其返回值。 - 我们只需输入 ``strace target/debug/os`` 即可看到一长串的系统调用。 + 从之前的 ``cargo run`` 的输出可以看出之前构建的可执行文件是在 target/debug 目录下的 os 。在 Ubuntu 系统上,可以通过 ``strace`` 工具来运行一个程序并输出程序运行过程当中向内核请求的所有的系统调用及其返回值。我们只需输入 ``strace target/debug/os`` 即可看到一长串的各种系统调用。 - 其中,真正容易看出与 ``Hello, world!`` 相关的只有一个系统调用: + 其中,容易看出与 ``Hello, world!`` 应用实际执行相关的只有两个系统调用: .. code-block:: - + + [输出字符串] write(1, "Hello, world!\n", 14) = 14 + [程序退出执行] + exit_group(0) 其参数的具体含义我们暂且不在这里进行解释。 - 其余的系统调用基本上分别用于函数库和内核两层执行环境的初始化工作和对于上层的运行期监控和管理。之后,随着应用场景的复杂化,我们 - 需要更强的抽象能力,也会实现这里面的一些系统调用。 + 其余的系统调用基本上分别用于函数库和内核两层执行环境的初始化工作和对于上层的运行期监控和管理。之后,随着应用场景的复杂化,我们需要更强的抽象能力,也会实现这里面的一些系统调用。 + +.. _term-isa: + +从硬件的角度来看,它上面的一切都属于软件。硬件可以分为三种: 处理器 (Processor) ——它更常见的名字是中央处理单元 (CPU, Central Processing Unit),内存 (Memory) 还有 I/O 设备。其中处理器无疑是其中最复杂同时也最关键的一个。它与软件约定一套 **指令集体系结构** (ISA, Instruction Set Architecture),使得软件可以通过 ISA 中提供的汇编指令来访问各种硬件资源。软件当然也需要知道处理器会如何执行这些指令:最简单的话就是一条一条执行位于内存中的指令。当然,实际的情况远比这个要复杂得多,为了适应现代应用程序的场景,处理器还需要提供很多额外的机制,而不仅仅是让数据在 CPU 寄存器、内存和 I/O 设备三者之间流动。 -从硬件的角度来看,它上面的一切都属于软件。硬件可以分为三种: 处理器 (Processor) ——它更常见的名字是中央处理单元 (CPU, Central Processing Unit), -内存 (Memory) 还有 I/O 设备。其中处理器无疑是其中最复杂同时也最关键的一个。它与软件约定一套 **指令集体系结构** (ISA, Instruction Set Architecture), -使得软件可以通过 ISA 中提供的汇编指令来访问各种硬件资源。软件当然也需要知道处理器会如何执行这些指令:最简单的话就是一条一条执行位于内存 -中的指令。当然,实际的情况远比这个要复杂得多,为了适应现代应用程序的场景,处理器还需要提供很多额外的机制,而不仅仅是让数据在 CPU 寄存器、内存和 I/O 设备 -三者之间流动。 +.. _term-abstraction: .. note:: **多层执行环境都是必需的吗?** - 除了最上层的应用程序和最下层的硬件平台必须存在之外,作为中间层的函数库和内核并不是必须存在的:它们都是对下层资源进行了 **抽象** (Abstraction), - 并为上层提供了一套运行环境。抽象的优点在于它让上层以较小的代价获得所需的功能,并同时可以提供一些保护。但抽象同时也是一种限制,会丧失一些 - 应有的灵活性。比如,当你在考虑在项目中应该使用哪个函数库的时候,就常常需要这方面的权衡:过多的抽象和过少的抽象自然都是不合适的。 + 除了最上层的应用程序和最下层的硬件平台必须存在之外,作为中间层的函数库和操作系统内核并不是必须存在的: + 它们都是对下层资源进行了 **抽象** (Abstraction),并为上层提供了一套执行环境(也可理解为一些服务功能)。抽象的优点在于它让上层以较小的代价获得所需的功能,并同时可以提供一些保护。但抽象同时也是一种限制,会丧失一些应有的灵活性。比如,当你在考虑在项目中应该使用哪个函数库的时候,就常常需要这方面的权衡:过多的抽象和过少的抽象自然都是不合适的。理解应用的需求也很重要。一个能合理满足应用需求的操作系统设计是操作系统设计者需要深入考虑的问题。这也是一种权衡,过多的服务功能和过少的服务功能自然都是不合适的。 - 实际上,我们通过应用程序的特征来判断它需要什么程度的抽象。 + 实际上,我们通过应用程序的特征和需求来判断操作系统需要什么程度的抽象和功能。 + + - 如果函数库和内核都不存在,那么我们就是在手写汇编代码,这种方式具有最高的灵活性,抽象能力则最低,基本等同于硬件。我们通常用这种方式来实现一些架构相关且仅通过编程语言无法描述的小模块或者代码片段。 + - 如果仅存在函数库而不存在内核,意味着我们不需要内核提供的抽象。在嵌入式场景就常常会出现这种情况。嵌入式设备虽然也包含 CPU、内存和 I/O 设备,但是它上面通常只会同时运行一个或几个功能非常简单的小应用程序,其定位就是那种功能单一的场景,比如人脸识别打卡系统等。我们常用的操作系统如 Windows/Linux/macOS 等的抽象都支持同时运行很多应用程序,在嵌入式场景是过抽象或功能太多,用力过猛的。因此,常见的解决方案是仅使用函数库构建单独的应用程序或是用专为应用场景特别裁减过的轻量级内核管理少数应用程序。 + +.. note:: + + **“用力过猛”的现代操作系统** + + 对于如下更简单的小应用程序,我们可以看到“用力过猛”的现代操作系统提供的执行环境支持: + + .. code-block:: rust + + //ch1/donothing.rs + fn main() { + //do nothing + } + + 它只是想显示一下几乎感知不到的存在感。在编译后再运行,可以看到的情况是: + + .. code-block:: console + + $ rustc donothing.rs + $ ./donothing + $ (无输出) + $ strace ./donothing + (多达 93 行的输出,表明 donothing 向 Linux 操作系统内核发出了93次各种各样的系统调用) + execve("./donothing", ["./donothing"], 0x7ffe02c9ca10 /* 67 vars */) = 0 + brk(NULL) = 0x563ba0532000 + arch_prctl(0x3001 /* ARCH_??? */, 0x7fff2da54360) = -1 EINVAL (无效的参数) + ...... - - 如果函数库和内核都不存在,那么我们就是在手写汇编代码,这种方式具有最高的灵活性,抽象能力则最低,基本等同于硬件。我们通常用这种方式来 - 实现一些架构相关且仅通过编程语言无法描述的小模块或者代码片段。 - - 如果仅存在函数库而不存在内核,意味着我们不需要内核提供的抽象。在嵌入式场景就常常会出现这种情况。嵌入式设备虽然也包含 CPU、内存和 I/O - 设备,但是它上面通常只会同时运行一个或几个功能非常简单的小应用程序,其定位就是那种功能单一的场景,比如人脸识别打卡系统等。我们常用的 - 操作系统如 Windows/Linux/macOS 等的抽象都支持同时运行很多应用程序,在嵌入式场景是过抽象的。因此,常见的解决方案是仅使用函数库构建 - 单独的应用程序或是用专为应用场景特别裁减过的轻量级内核管理少数应用程序。 平台与目标三元组 --------------------------------------- -对于一份用某种编程语言实现的源代码而言,编译器在将其通过编译、链接得到目标文件的时候需要知道程序要在哪个 **平台** (Platform) 上运行。 -从上面给出的 :ref:`应用程序运行环境栈 ` 可以看出: +.. _term-platform: + +对于一份用某种编程语言实现的应用程序源代码而言,编译器在将其通过编译、链接得到可执行文件的时候需要知道程序要在哪个 **平台** (Platform) 上运行。这里 **平台** 主要是指CPU类型、操作系统类型和标准运行时库的组合。从上面给出的 :ref:`应用程序执行环境栈 ` 可以看出: - 如果用户态基于的内核不同,会导致系统调用接口不同或者语义不一致; - 如果底层硬件不同,对于硬件资源的访问方式会有差异。特别是 ISA 不同的话,对上提供的指令集和寄存器都不同。 -它们都会导致最终生成的目标文件有很大不同。需要指出的是,某些编译器支持同一份源代码无需修改就可编译到多个不同的目标平台并在上面运行。这种 -情况下,源代码是 **跨平台** 的。而另一些编译器则已经预设好了一个固定的目标平台。 +它们都会导致最终生成的可执行文件有很大不同。需要指出的是,某些编译器支持同一份源代码无需修改就可编译到多个不同的目标平台并在上面运行。这种情况下,源代码是 **跨平台** 的。而另一些编译器则已经预设好了一个固定的目标平台。 + +.. _term-target-triplet: -我们可以通过 **目标三元组** (Target Triplet) 来描述一个目标平台。它一般包括 CPU 架构、CPU 厂商和操作系统,它们确实都会控制目标文件的生成。 -比如,我们可以尝试看一下之前的 ``Hello, world!`` 的目标平台是什么。这可以通过打印编译器 rustc 的默认配置信息: +我们可以通过 **目标三元组** (Target Triplet) 来描述一个目标平台。它一般包括 CPU 架构、CPU 厂商、操作系统和运行时库,它们确实都会控制可执行文件的生成。比如,我们可以尝试看一下之前的 ``Hello, world!`` 的目标平台是什么。这可以通过打印编译器 rustc 的默认配置信息: .. code-block:: console $ rustc --version --verbose - rustc 1.48.0-nightly (73dc675b9 2020-09-06) - binary: rustc - commit-hash: 73dc675b9437c2a51a975a9f58cc66f05463c351 - commit-date: 2020-09-06 - host: x86_64-unknown-linux-gnu - release: 1.48.0-nightly - LLVM version: 11.0 + rustc 1.51.0-nightly (d1aed50ab 2021-01-26) + binary: rustc + commit-hash: d1aed50ab81df3140977c610c5a7d00f36dc519f + commit-date: 2021-01-26 + host: x86_64-unknown-linux-gnu + release: 1.51.0-nightly + LLVM version: 11.0.1 -从其中的 host 一项可以看出默认的目标平台是 ``x86_64-unknown-linux-gnu``,其中 CPU 架构是 x86_64,CPU 厂商是 unknown,操作系统是 linux-gnu。 -这种无论编译器还是其目标文件都在我们当前所处的平台运行是一种最简单也最普遍的情况。但是很快我们就将遇到另外一种情况。 +从其中的 host 一项可以看出默认的目标平台是 ``x86_64-unknown-linux-gnu``,其中 CPU 架构是 x86_64,CPU 厂商是 unknown,操作系统是 linux,运行时库是gnu libc(封装了Linux系统调用,并提供POSIX接口为主的函数库)。这种无论编译器还是其生成的可执行文件都在我们当前所处的平台运行是一种最简单也最普遍的情况。但是很快我们就将遇到另外一种情况。 -讲了这么多,终于该介绍我们的主线任务了。我们希望能够在另一个平台上运行 ``Hello, world!``,而与之前的默认平台不同的地方在于,我们将 CPU 架构从 -x86_64 换成 RISC-V。 +讲了这么多,终于该介绍我们的主线任务了。我们希望能够在另一个硬件平台上运行 ``Hello, world!``,而与之前的默认平台不同的地方在于,我们将 CPU 架构从 x86_64 换成 RISC-V。 .. note:: - 为何基于 RISC-V 架构而非 x86 系列架构? + **为何基于 RISC-V 架构而非 x86 系列架构?** - x86 架构为了在升级换代的同时保持对基于旧版架构应用程序/内核的兼容性,存在大量的历史包袱,也就是一些对于目前的应用场景没有任何意义,但又必须 - 花大量时间正确设置才能正常使用 CPU 的奇怪设定。为了建立并维护架构的应用生态,这确实是必不可少的,但站在教学的角度几乎完全是在浪费时间。而 - 新生的 RISC-V 架构十分简洁,架构文档需要阅读的核心部分不足百页,且这些功能已经足以用来构造一个具有相当抽象能力的内核了。 + x86 架构为了在升级换代的同时保持对基于旧版架构应用程序/内核的兼容性,存在大量的历史包袱,也就是一些对于目前的应用场景没有任何意义,但又必须花大量时间正确设置才能正常使用 CPU 的奇怪设定。为了建立并维护架构的应用生态,这确实是必不可少的,但站在教学的角度几乎完全是在浪费时间。而新生的 RISC-V 架构十分简洁,架构文档需要阅读的核心部分不足百页,且这些功能已经足以用来构造一个具有相当抽象能力的内核了。 可以看一下目前 Rust 编译器支持哪些基于 RISC-V 的平台: .. code-block:: console $ rustc --print target-list | grep riscv + riscv32gc-unknown-linux-gnu riscv32i-unknown-none-elf riscv32imac-unknown-none-elf riscv32imc-unknown-none-elf @@ -163,27 +189,22 @@ x86_64 换成 RISC-V。 riscv64gc-unknown-none-elf riscv64imac-unknown-none-elf -这里我们选择的是 ``riscv64gc-unknown-none-elf``,目标三元组中的操作系统是 none-elf,表明没有任何系统调用支持。这里我们之所以不选择有 -linux-gnu 系统调用支持的版本 ``riscv64gc-unknown-linux-gnu``,是因为我们只是想跑一个 ``Hello, world!``,没有必要使用操作系统所提供的 -那么高级的抽象。而且我们很清楚后续我们要开发的是一个内核,如果仅仅基于已有操作系统提供的系统调用的话,它自身的抽象能力会受到很大限制。所以它必须 -直面底层硬件来解锁更大的抽象能力上限。 +这里我们选择的是 ``riscv64gc-unknown-none-elf``,目标三元组中的 CPU 架构是 riscv64gc,厂商是 unknown,操作系统是 none,elf表示没有标准的运行时库(表明没有任何系统调用的封装支持),但可以生成ELF格式的执行程序。这里我们之所以不选择有 +linux-gnu 系统调用支持的版本 ``riscv64gc-unknown-linux-gnu``,是因为我们只是想跑一个 ``Hello, world!``,没有必要使用现在通用操作系统所提供的那么高级的抽象和多余的操作系统服务。而且我们很清楚后续我们要开发的是一个操作系统内核,它必须直面底层物理硬件(bare-metal)来提供更大的操作系统服务功能,已有操作系统(如Linux)提供的系统调用服务对这个内核而言是多余的。 .. note:: **RISC-V 指令集拓展** - 由于基于 RISC-V 架构的处理器可能用于嵌入式场景或是通用计算场景,因此指令集规范将指令集划分为最基本的 RV32/64I 以及若干标准指令集拓展。 - 每款处理器只需按照其实际应用场景按需实现指令集拓展即可。 + 由于基于 RISC-V 架构的处理器可能用于嵌入式场景或是通用计算场景,因此指令集规范将指令集划分为最基本的 RV32/64I 以及若干标准指令集拓展。每款处理器只需按照其实际应用场景按需实现指令集拓展即可。 - - RV32/64I:每款处理器都必须实现的基本整数指令集。在 RV32I 中,每个通用寄存器的位宽为 32 位;在 RV64I 中则为 64 位。它可以用来模拟 - 绝大多数标准指令集拓展中的指令,除了比较特殊的 A 拓展,因为它需要特别的硬件支持。 + - RV32/64I:每款处理器都必须实现的基本整数指令集。在 RV32I 中,每个通用寄存器的位宽为 32 位;在 RV64I 中则为 64 位。它可以用来模拟绝大多数标准指令集拓展中的指令,除了比较特殊的 A 拓展,因为它需要特别的硬件支持。 - M 拓展:提供整数乘除法相关指令。 - A 拓展:提供原子指令和一些相关的内存同步机制,这个后面会展开。 - F/D 拓展:提供单/双精度浮点数运算支持。 - C 拓展:提供压缩指令拓展。 - G 拓展是基本整数指令集 I 再加上标准指令集拓展 MAFD 的总称,因此 riscv64gc 也就等同于 riscv64imafdc。我们剩下的内容都基于该处理器 - 架构完成。除此之外 RISC-V 架构还有很多标准指令集拓展,有一些还在持续更新中尚未稳定,有兴趣的读者可以浏览最新版的 RISC-V 指令集规范。 + G 拓展是基本整数指令集 I 再加上标准指令集拓展 MAFD 的总称,因此 riscv64gc 也就等同于 riscv64imafdc。我们剩下的内容都基于该处理器架构完成。除此之外 RISC-V 架构还有很多标准指令集拓展,有一些还在持续更新中尚未稳定,有兴趣的读者可以浏览最新版的 RISC-V 指令集规范。 Rust 标准库与核心库 ---------------------------------- @@ -198,12 +219,16 @@ Rust 标准库与核心库 | = note: the `riscv64gc-unknown-none-elf` target may not be installed -在之前的环境配置中,我们已经在 rustup 工具链中安装了这个目标平台支持,因此并不是该目标平台未安装的问题。因此只是单纯的在这个目标平台上找不到 -Rust 标准库 std。我们之前曾经提到过,编程语言的标准库或三方库的某些功能会直接或间接的用到操作系统提供的系统调用。但目前我们所选的目标平台不存在 -任何操作系统支持,于是 Rust 并没有为这个目标平台支持完整的标准库 std。类似这样的平台通常被我们称为 **裸机平台** (bare-metal)。 +.. _term-bare-metal: + +在之前的开发环境配置中,我们已经在 rustup 工具链中安装了这个目标平台支持,因此并不是该目标平台未安装的问题。这个问题只是单纯的表示在这个目标平台上找不到Rust 标准库 std。我们之前曾经提到过,编程语言的标准库或三方库的某些功能会直接或间接的用到操作系统提供的系统调用。但目前我们所选的目标平台不存在任何操作系统支持,于是 Rust 并没有为这个目标平台支持完整的标准库 std。类似这样的平台通常被我们称为 **裸机平台** (bare-metal)。 + +.. note:: + + **Rust语言标准库** + + Rust 语言标准库是让 Rust 语言开发的软件具备可移植性的基础,类似于 C 语言的 LibC 标准库。它是一组最小的、经过实战检验的共享抽象,适用于更广泛的 Rust 生态系统开发。它提供了核心类型,如 Vec 和 Option、类库定义的语言原语操作、标准宏、I/O 和多线程等。默认情况下,所有 Rust crate 都可以使用 std 来支持 Rust 应用程序的开发。但 Rust 语言标准库的一个限制是,它需要有操作系统的支持。所以,如果你要实现的软件是运行在裸机上的操作系统,就不能直接用 Rust 语言标准库了。 -幸运的是,Rust 有一个对 std 裁剪过后的核心库 core,这个库是不需要任何操作系统支持的,相对的它的功能也比较受限,但是也包含了 Rust 语言 -相当一部分的核心机制,可以满足我们的大部分需求。在 Rust 语言生态中,有很多三方库也不依赖标准库 std 而仅仅依赖核心库 core,它们也可以很大 -程度上减轻我们的编程负担。它们是我们能够在裸机平台挣扎求生的最主要倚仗。 +幸运的是,Rust 有一个对 std 裁剪过后的核心库 core,这个库是不需要任何操作系统支持的,相对的它的功能也比较受限,但是也包含了 Rust 语言相当一部分的核心机制,可以满足我们的大部分需求。Rust 语言是一种面向系统(包括操作系统)开发的语言,所以在 Rust 语言生态中,有很多三方库也不依赖标准库 std 而仅仅依赖核心库 core。对它们的使用可以很大程度上减轻我们的编程负担。它们是我们能够在裸机平台挣扎求生的最主要倚仗,也是大部分运行在没有操作系统支持的 Rust 嵌入式软件的必备。 -于是,我们知道在裸机平台上我们要将对于标准库 std 的引用换成核心库 core。但是做起来其实并没有那么容易。 \ No newline at end of file +于是,我们知道在裸机平台上我们要将对于标准库 std 的引用换成核心库 core。但是做起来其实还要有一些琐碎的事情需要解决。 \ No newline at end of file diff --git a/source/chapter1/2remove-std.rst b/source/chapter1/2remove-std.rst index f9cf314adf2bb5d2b309b43fe9e93e5bd954c54c..73b598e06c0effbd4d30caa6bf61368338c1c2ac 100644 --- a/source/chapter1/2remove-std.rst +++ b/source/chapter1/2remove-std.rst @@ -5,24 +5,30 @@ :hidden: :maxdepth: 5 -本节我们尝试移除之前的 ``Hello world!`` 程序对于标准库的依赖,使得它能够编译到裸机平台 RV64GC 上。 +本节导读 +------------------------------- + +为了很好地理解一个简单应用所需的服务如何体现,本节将尝试开始构造一个小的执行环境,可建立在 Linux 之上,也可直接建立在裸机之上,我们称为“三叶虫”操作系统。作为第一步,本节将尝试移除之前的 ``Hello world!`` 程序对于 Rust std 标准库的依赖,使得它能够编译到裸机平台 RV64GC 或 Linux-RV64 上。 + +移除 println! 宏 +---------------------------------- 我们首先在 ``os`` 目录下新建 ``.cargo`` 目录,并在这个目录下创建 ``config`` 文件,并在里面输入如下内容: -.. code-block:: +.. code-block:: toml - // os/.cargo/config + # os/.cargo/config [build] target = "riscv64gc-unknown-none-elf" -这会对于 Cargo 工具在 os 目录下的行为进行调整:现在默认会使用 riscv64gc 作为目标平台而不是原先的默认 x86_64-unknown-linux-gnu。 -事实上,这是一种编译器运行所在的平台与编译器生成可执行文件的目标平台不同(分别是后者和前者)的情况。这是一种 **交叉编译** (Cross Compile)。 +.. _term-cross-compile: -当然,这只是使得我们之后在 ``cargo build`` 的时候不必再加上 ``--target`` 参数的一个小 trick。如果我们现在 ``cargo build`` ,还是会和 -上一小节一样出现找不到标准库 std 的错误。于是我们开始着手移除标准库。当然,这会产生一些副作用。 +这会对于 Cargo 工具在 os 目录下的行为进行调整:现在默认会使用 riscv64gc 作为目标平台而不是原先的默认 x86_64-unknown-linux-gnu。事实上,这是一种编译器运行所在的平台与编译器生成可执行文件的目标平台不同(分别是后者和前者)的情况。这是一种 **交叉编译** (Cross Compile)。 -移除 println! 宏 ----------------------------------- +.. + chyyuu:解释一下交叉编译??? + +当然,这只是使得我们之后在 ``cargo build`` 的时候不必再加上 ``--target`` 参数的一个小 trick。如果我们现在 ``cargo build`` ,还是会和上一小节一样出现找不到标准库 std 的错误。于是我们开始着手移除标准库,当然,这会产生一些副作用。 我们在 ``main.rs`` 的开头加上一行 ``#![no_std]`` 来告诉 Rust 编译器不使用 Rust 标准库 std 转而使用核心库 core。编译器报出如下错误: @@ -38,8 +44,7 @@ 4 | println!("Hello, world!"); | ^^^^^^^ -我们之前提到过, println! 宏是由标准库 std 提供的,且会使用到一个名为 write 的系统调用。现在我们的条件还不足以自己实现一个 println! 宏,由于 -使用了系统调用也不能在核心库 core 中找到它。我们目前先通过将它注释掉来绕过它。 +我们之前提到过, println! 宏是由标准库 std 提供的,且会使用到一个名为 write 的系统调用。现在我们的代码功能还不足以自己实现一个 println! 宏。由于使用了系统调用也不能在核心库 core 中找到它,所以我们目前先通过将它注释掉来绕过它。 提供语义项 panic_handler ---------------------------------------------------- @@ -52,19 +57,15 @@ Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os) error: `#[panic_handler]` function required, but not found -在使用 Rust 编写应用程序的时候,我们常常在遇到了一些无法恢复的致命错误导致程序无法继续向下运行的时候手动或自动调用 panic! 宏来并打印出错的 -位置让我们能够意识到它的存在,并进行一些后续处理。panic! 宏最典型的应用场景包括断言宏 assert! 失败或者对 ``Option::None/Result::Err`` -进行 ``unwrap`` 操作。 +在使用 Rust 编写应用程序的时候,我们常常在遇到了一些无法恢复的致命错误导致程序无法继续向下运行的时候手动或自动调用 panic! 宏来并打印出错的位置让我们能够意识到它的存在,并进行一些后续处理。panic! 宏最典型的应用场景包括断言宏 assert! 失败或者对 ``Option::None/Result::Err`` 进行 ``unwrap`` 操作。 -在标准库 std 中提供了 panic 的处理函数 ``#[panic_handler]``,其大致功能是打印出错位置和原因并杀死当前应用。可惜的是在核心库 core 中并没有提供, -因此我们需要自己实现 panic 处理函数。 +在标准库 std 中提供了 panic 的处理函数 ``#[panic_handler]``,其大致功能是打印出错位置和原因并杀死当前应用。可惜的是在核心库 core 中并没有提供,因此我们需要自己实现 panic 处理函数。 .. note:: - **Rust 语义项 lang_items** + **Rust 语法卡片:语义项 lang_items** - Rust 编译器内部的某些功能的实现并不是硬编码在语言内部的,而是以一种可插入的形式在库中提供。库只需要通过某种方式告诉编译器它的某个方法实现了 - 编译器内部的哪些功能,编译器就会采用库提供的方法来实现它内部对应的功能。通常只需要在库的方法前面加上一个标记即可。 + Rust 编译器内部的某些功能的实现并不是硬编码在语言内部的,而是以一种可插入的形式在库中提供。库只需要通过某种方式告诉编译器它的某个方法实现了编译器内部的哪些功能,编译器就会采用库提供的方法来实现它内部对应的功能。通常只需要在库的方法前面加上一个标记即可。 我们开一个新的子模块 ``lang_items.rs`` 保存这些语义项,在里面提供 panic 处理函数的实现并通过标记通知编译器采用我们的实现: @@ -78,8 +79,7 @@ loop {} } -注意,panic 处理函数的函数签名需要一个 ``PanicInfo`` 的不可变借用作为输入参数,它在核心库中得以保留,这也是我们第一次与核心库打交道。之后我们 -会从 ``PanicInfo`` 解析出错位置并打印出来,然后杀死应用程序。但目前我们什么都不做只是在原地 loop。 +注意,panic 处理函数的函数签名需要一个 ``PanicInfo`` 的不可变借用作为输入参数,它在核心库中得以保留,这也是我们第一次与核心库打交道。之后我们会从 ``PanicInfo`` 解析出错位置并打印出来,然后杀死应用程序。但目前我们什么都不做只是在原地 ``loop`` 。 移除 main 函数 ----------------------------- @@ -92,12 +92,9 @@ Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os) error: requires `start` lang_item -编译器提醒我们缺少一个名为 ``start`` 的语义项。我们回忆一下,之前提到语言标准库和三方库作为应用程序的执行环境,需要负责在执行应用程序之前进行 -一些初始化工作,然后才跳转到应用程序的入口点(也就是跳转到我们编写的 ``main`` 函数)开始执行。事实上 ``start`` 语义项正代表着标准库 std 在 -执行应用程序之前需要进行的一些初始化工作。由于我们禁用了标准库,编译器也就找不到这项功能的实现了。 +编译器提醒我们缺少一个名为 ``start`` 的语义项。我们回忆一下,之前提到语言标准库和三方库作为应用程序的执行环境,需要负责在执行应用程序之前进行一些初始化工作,然后才跳转到应用程序的入口点(也就是跳转到我们编写的 ``main`` 函数)开始执行。事实上 ``start`` 语义项正代表着标准库 std 在执行应用程序之前需要进行的一些初始化工作。由于我们禁用了标准库,编译器也就找不到这项功能的实现了。 -最简单的解决方案就是压根不让编译器使用这项功能。我们在 ``main.rs`` 的开头加入设置 ``#![no_main]`` 告诉编译器我们没有一般意义上的 ``main`` 函数, -并将原来的 ``main`` 函数删除。在失去了 ``main`` 函数的情况下,编译器也就不需要完成所谓的初始化工作了。 +最简单的解决方案就是压根不让编译器使用这项功能。我们在 ``main.rs`` 的开头加入设置 ``#![no_main]`` 告诉编译器我们没有一般意义上的 ``main`` 函数,并将原来的 ``main`` 函数删除。在失去了 ``main`` 函数的情况下,编译器也就不需要完成所谓的初始化工作了。 至此,我们成功移除了标准库的依赖并完成裸机平台上的构建。 @@ -125,13 +122,51 @@ loop {} } -本小节我们固然脱离了标准库,通过了编译器的检验,但也是伤筋动骨,将原有的很多功能弱化甚至直接删除,看起来距离在 RV64GC 平台上打印 -``Hello world!`` 相去甚远了(我们甚至连 println! 和 ``main`` 函数都删除了)。不要着急,接下来我们会以自己的方式来重塑这些 -功能,并最终完成我们的目标。 +本小节我们固然脱离了标准库,通过了编译器的检验,但也是伤筋动骨,将原有的很多功能弱化甚至直接删除,看起来距离在 RV64GC 平台上打印 ``Hello world!`` 相去甚远了(我们甚至连 println! 和 ``main`` 函数都删除了)。不要着急,接下来我们会以自己的方式来重塑这些基本功能,并最终完成我们的目标。 + + +分析被移除标准库的程序 +----------------------------- + +对于上面这个被移除标准库的应用程序,通过了编译器的检查和编译,形成了二进制代码。但这个二进制代码是怎样的,它能否被正常执行呢?我们可以通过一些工具来分析一下。 + +.. code-block:: console + + [文件格式] + $ file target/riscv64gc-unknown-none-elf/debug/os + target/riscv64gc-unknown-none-elf/debug/os: ELF 64-bit LSB executable, UCB RISC-V, ...... + + [文件头信息] + $ rust-readobj -h target/riscv64gc-unknown-none-elf/debug/os + File: target/riscv64gc-unknown-none-elf/debug/os + Format: elf64-littleriscv + Arch: riscv64 + AddressSize: 64bit + ...... + Type: Executable (0x2) + Machine: EM_RISCV (0xF3) + Version: 1 + Entry: 0x0 + ...... + } + + [反汇编导出汇编程序] + $ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os + target/riscv64gc-unknown-none-elf/debug/os: file format elf64-littleriscv + + + +通过 ``file`` 工具对二进制程序 ``os`` 的分析可以看到它好像是一个合法的 RV64 执行程序,但通过 ``rust-readobj`` 工具进一步分析,发现它的入口地址 Entry 是 ``0`` ,这就比较奇怪了,地址从 0 执行,好像不对。再通过 ``rust-objdump`` 工具把它反汇编,可以看到没有生成汇编代码。所以,我们可以断定,这个二进制程序虽然合法,但它是一个空程序。这不是我们希望的,我们希望有具体内容的执行程序。为什么会这样呢?原因是我们缺少了编译器需要找到的入口函数 ``_start`` 。 + +在下面几节,我们将建立有支持显示字符串的最小执行环境。 .. note:: **在 x86_64 平台上移除标准库依赖** 有兴趣的同学可以将目标平台换回之前默认的 ``x86_64-unknown-linux-gnu`` 并重复本小节所做的事情,比较两个平台从 ISA 到操作系统 - 的差异。可以参考 `BlogOS 的相关内容 `_ 。 \ No newline at end of file + 的差异。可以参考 `BlogOS 的相关内容 `_ 。 + +.. note:: + + 本节内容部分参考自 `BlogOS 的相关章节 `_ 。 \ No newline at end of file diff --git a/source/chapter1/3-1-mini-rt-usrland.rst b/source/chapter1/3-1-mini-rt-usrland.rst new file mode 100644 index 0000000000000000000000000000000000000000..abdda3796a5c71810353db435deb34f25f2cb46b --- /dev/null +++ b/source/chapter1/3-1-mini-rt-usrland.rst @@ -0,0 +1,297 @@ +构建用户态执行环境 +================================= + +.. toctree:: + :hidden: + :maxdepth: 5 + +本节导读 +------------------------------- + +本节开始我们将着手自己来实现之前被我们移除的 ``Hello, world!`` 程序中执行环境的功能。 +在这一小节,我们介绍如何进行 **执行环境初始化** 。 + +在这里,我们先设计实现一个最小执行环境以支持最简单的用户态 ``Hello, world!`` 程序,再改进这个最小执行环境,支持对裸机应用程序。这样设计实现的原因是, +它能帮助我们理解这两个不同的执行环境在支持同样一个应用程序时的的相同和不同之处,这将加深对执行环境的理解,并对后续写自己的OS和运行在OS上的应用程序都有帮助。 +所以,本节将先建立一个用户态的最小执行环境,即 **恐龙虾** 操作系统。 + +用户态最小化执行环境 +---------------------------- + +在上一节,我们构造的二进制程序是一个空程序,其原因是 Rust 编译器找不到执行环境的入口函数,于是就没有生产后续的代码。所以,我们首先要把入口函数 +找到。通过查找资料,发现Rust编译器要找的入口函数是 ``_start()`` ,于是我们可以在 ``main.rs`` 中添加如下内容: + + +.. code-block:: rust + + // os/src/main.rs + #[no_mangle] + extern "C" fn _start() { + loop{}; + } + + +对上述代码重新编译,再用分析工具分析,可以看到: + + +.. code-block:: console + + $ cargo build + Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os) + Finished dev [unoptimized + debuginfo] target(s) in 0.06s + + [文件格式] + $ file target/riscv64gc-unknown-none-elf/debug/os + target/riscv64gc-unknown-none-elf/debug/os: ELF 64-bit LSB executable, UCB RISC-V, ...... + + [文件头信息] + $ rust-readobj -h target/riscv64gc-unknown-none-elf/debug/os + File: target/riscv64gc-unknown-none-elf/debug/os + Format: elf64-littleriscv + Arch: riscv64 + AddressSize: 64bit + ...... + Type: Executable (0x2) + Machine: EM_RISCV (0xF3) + Version: 1 + Entry: 0x11120 + ...... + } + + [反汇编导出汇编程序] + $ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os + target/riscv64gc-unknown-none-elf/debug/os: file format elf64-littleriscv + + Disassembly of section .text: + + 0000000000011120 <_start>: + ; loop {} + 11120: 09 a0 j 2 <_start+0x2> + 11122: 01 a0 j 0 <_start+0x2> + + +通过 ``file`` 工具对二进制程序 ``os`` 的分析可以看到它依然是一个合法的 RV64 执行程序,但通过 ``rust-readobj`` 工具进一步分析,发现它的入口地址 Entry 是 ``0x11120`` ,这好像是一个合法的地址。再通过 ``rust-objdump`` 工具把它反汇编,可以看到生成汇编代码! + +所以,我们可以断定,这个二进制程序虽然合法,但它是一个空程序。这不是我们希望的,我们希望有具体内容的执行程序。为什么会这样呢? + +仔细读读这两条指令,发现就是一个死循环的汇编代码,且其第一条指令的地址与入口地址 Entry 的值一致。这已经是一个合理的程序了。如果我们用 ``qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os`` 执行这个程序,可以看到好像就是在执行死循环。 + +我们能让程序正常退出吗?我们把 ``_start()`` 函数中的循环语句注释掉,重新编译并分析,看到其汇编代码是: + + +.. code-block:: console + + $ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os + + target/riscv64gc-unknown-none-elf/debug/os: file format elf64-littleriscv + + + Disassembly of section .text: + + 0000000000011120 <_start>: + ; } + 11120: 82 80 ret + +看起来是有内容(具有 ``ret`` 函数返回汇编指令)且合法的执行程序。但如果我们执行它,就发现有问题了: + +.. code-block:: console + + $ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os + 段错误 (核心已转储) + +*段错误 (核心已转储)* 是常见的一种应用程序出错,而我们这个非常简单的应用程序导致了 Linux 环境模拟程序 ``qemu-riscv64`` 崩溃了!为什么会这样? + +.. _term-qemu-riscv64: + +.. note:: + + QEMU有两种运行模式: ``User mode`` 模式,即用户态模拟,如 ``qemu-riscv64`` 程序,能够模拟不同处理器的用户态指令的执行,并可以直接解析ELF可执行文件,加载运行那些为不同处理器编译的用户级Linux应用程序(ELF可执行文件);在翻译并执行不同应用程序中的不同处理器的指令时,如果碰到是系统调用相关的汇编指令,它会把不同处理器(如RISC-V)的Linux系统调用转换为本机处理器(如x86-64)上的Linux系统调用,这样就可以让本机Linux完成系统调用,并返回结果(再转换成RISC-V能识别的数据)给这些应用。 ``System mode`` 模式,即系统态模式,如 ``qemu-system-riscv64`` 程序,能够模拟一个完整的基于不同CPU的硬件系统,包括处理器、内存及其他外部设备,支持运行完整的操作系统。 + +回顾一下最开始的输出 ``Hello, world!`` 的简单应用程序,其入口函数名字是 ``main`` ,编译时用的是标准库 std 。它可以正常执行。再仔细想想,当一个应用程序出错的时候,最上层为操作系统的执行环境会把它给杀死。但如果一个应用的入口函数正常返回,执行环境应该优雅地让它退出才对。没错!目前的执行环境还缺了一个退出机制。 + +先了解一下,操作系统会提供一个退出的系统调用服务接口,但应用程序调用这个接口,那这个程序就退出了。这里先给出代码: + +.. _term-llvm-syscall: + +.. code-block:: rust + + // os/src/main.rs + #![feature(llvm_asm)] + + const SYSCALL_EXIT: usize = 93; + + fn syscall(id: usize, args: [usize; 3]) -> isize { + let mut ret: isize; + unsafe { + llvm_asm!("ecall" + : "={x10}" (ret) + : "{x10}" (args[0]), "{x11}" (args[1]), "{x12}" (args[2]), "{x17}" (id) + : "memory" + : "volatile" + ); + } + ret + } + + pub fn sys_exit(xstate: i32) -> isize { + syscall(SYSCALL_EXIT, [xstate as usize, 0, 0]) + } + + #[no_mangle] + extern "C" fn _start() { + sys_exit(9); + } + +``main.rs`` 增加的内容不多,但还是有点与一般的应用程序有所不同,因为它引入了汇编和系统调用。如果你看不懂上面内容的细节,没关系,在第二章的第二节 :doc:`/chapter2/2application` 会有详细的介绍。这里只需知道 ``_start`` 函数调用了一个 ``sys_exit`` 函数,来向操作系统发出一个退出服务的系统调用请求,并传递给OS的退出码为 ``9`` 。 + +我们编译执行以下修改后的程序: + +.. code-block:: console + + $ cargo build --target riscv64gc-unknown-none-elf + Compiling os v0.1.0 (/media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/os_kernel_lab/os) + Finished dev [unoptimized + debuginfo] target(s) in 0.26s + + [$?表示执行程序的退出码,它会被告知 OS] + $ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os; echo $? + 9 + +可以看到,返回的结果确实是 ``9`` 。这样,我们在没有任何显示功能的情况下,勉强完成了一个简陋的用户态最小化执行环境。 + +上面实现的最小化执行环境貌似能够在 Linux 操作系统上支持只调用一个 ``SYSCALL_EXIT`` 系统调用服务的程序,但这也说明了 +在操作系统的支持下,实现一个基本的用户态执行环境还是比较容易的。其中的原因是,操作系统帮助用户态执行环境完成了程序加载、程序退出、资源分配、资源回收等各种琐事。如果没有操作系统,那么实现一个支持在裸机上运行应用程序的执行环境,就要考虑更多的事情了,或者干脆简化一切可以不必干的事情(比如对于单个应用,不需要调度功能等)。 + +在裸机上的执行环境,其实就是之前提到的“三叶虫”操作系统。 + + +有显示支持的用户态执行环境 +---------------------------- + +没有显示功能,终究觉得缺了点啥。在没有通常开发应用程序时常用的动态调试工具的情况下,其实能显示字符串,就已经能够满足绝大多数情况下的调试需求了。 + +Rust 的 core 库内建了以一系列帮助实现显示字符的基本 Trait 和数据结构,函数等,我们可以对其中的关键部分进行扩展,就可以实现定制的 ``println!`` 功能。 + + +实现输出字符串的相关函数 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +首先封装一下对 ``SYSCALL_WRITE`` 系统调用。这个是 Linux 操作系统内核提供的系统调用,其 ``ID`` 就是 ``SYSCALL_WRITE``。 + +.. code-block:: rust + + const SYSCALL_WRITE: usize = 64; + + pub fn sys_write(fd: usize, buffer: &[u8]) -> isize { + syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()]) + } + +然后实现基于 ``Write`` Trait 的数据结构,并完成 ``Write`` Trait 所需要的 ``write_str`` 函数,并用 ``print`` 函数进行包装。 + + +.. code-block:: rust + + struct Stdout; + + impl Write for Stdout { + fn write_str(&mut self, s: &str) -> fmt::Result { + sys_write(1, s.as_bytes()); + Ok(()) + } + } + + pub fn print(args: fmt::Arguments) { + Stdout.write_fmt(args).unwrap(); + } + +最后,实现基于 ``print`` 函数,实现Rust语言 **格式化宏** ( `formatting macros `_ )。 + + +.. code-block:: rust + + #[macro_export] + macro_rules! print { + ($fmt: literal $(, $($arg: tt)+)?) => { + $crate::console::print(format_args!($fmt $(, $($arg)+)?)); + } + } + + #[macro_export] + macro_rules! println { + ($fmt: literal $(, $($arg: tt)+)?) => { + print(format_args!(concat!($fmt, "\n") $(, $($arg)+)?)); + } + } + +上面的代码没有读懂?没关系,你只要了解到应用程序发出的宏调用 ``println!`` 就是通过上面的实现,一步一步地调用,最终通过操作系统提供的 ``SYSCALL_WRITE`` 系统调用服务,帮助我们完成了字符串显示输出。这就完成了有显示支持的用户态执行环境。 + +接下来,我们调整一下应用程序,让它发出显示字符串和退出的请求: + +.. code-block:: rust + + #[no_mangle] + extern "C" fn _start() { + println!("Hello, world!"); + sys_exit(9); + } + +整体工作完成!当然,我们实现的很简陋,用户态执行环境和应用程序都放在一个文件里面,以后会通过我们学习的软件工程的知识,进行软件重构,让代码更清晰和模块化。 + +现在,我们编译并执行一下,可以看到正确的字符串输出,且程序也能正确结束! + + +.. code-block:: console + + $ cargo build --target riscv64gc-unknown-none-elf + Compiling os v0.1.0 (/media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/os_kernel_lab/os) + Finished dev [unoptimized + debuginfo] target(s) in 0.61s + + $ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os; echo $? + Hello, world! + 9 + + +.. 下面出错的情况是会在采用 linker.ld,加入了 .cargo/config +.. 的内容后会出错: +.. .. [build] +.. .. target = "riscv64gc-unknown-none-elf" +.. .. [target.riscv64gc-unknown-none-elf] +.. .. rustflags = [ +.. .. "-Clink-arg=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes" +.. .. ] + +.. 重新定义了栈和地址空间布局后才会出错 + +.. 段错误 (核心已转储) + +.. 系统崩溃了!借助以往的操作系统内核编程经验和与下一节调试kernel的成果经验,我们直接定位为是 **栈** (Stack) 没有设置的问题。我们需要添加建立栈的代码逻辑。 + +.. .. code-block:: asm + +.. # entry.asm + +.. .section .text.entry +.. .globl _start +.. _start: +.. la sp, boot_stack_top +.. call rust_main + +.. .section .bss.stack +.. .globl boot_stack +.. boot_stack: +.. .space 4096 * 16 +.. .globl boot_stack_top +.. boot_stack_top: + +.. 然后把汇编代码嵌入到 ``main.rs`` 中,并进行微调。 + +.. .. code-block:: rust + +.. #![feature(global_asm)] + +.. global_asm!(include_str!("entry.asm")); + +.. #[no_mangle] +.. #[link_section=".text.entry"] +.. extern "C" fn rust_main() { + +.. 再次编译执行,可以看到正确的字符串输出,且程序也能正确结束! diff --git a/source/chapter1/3-2-mini-rt-baremetal.rst b/source/chapter1/3-2-mini-rt-baremetal.rst new file mode 100644 index 0000000000000000000000000000000000000000..66db107c3948922562b66463f6aab083efd0c3a2 --- /dev/null +++ b/source/chapter1/3-2-mini-rt-baremetal.rst @@ -0,0 +1,559 @@ +构建裸机运行时执行环境 +================================= + +.. toctree:: + :hidden: + :maxdepth: 5 + +本节导读 +------------------------------- + +本节开始我们将着手自己来实现裸机上的最小执行环境,即我们的“三叶虫”操作系统,并能在裸机上运行 ``Hello, world!`` 程序。 +有了上一节实现的用户态的最小执行环境,我们可以稍加改造,就可以完成裸机上的最小执行环境了。与上节不同,需要关注地方主要是: + +- 物理内存的 DRAM 位置(放应用程序的地方)和应用程序的内存布局(如何在 DRAM 中放置应用程序的各个部分) +- SBI 的字符输出接口(执行环境提供的输出字符服务,可以被应用程序使用) +- 应用程序的初始化(起始的指令位置,对 ``栈 stack`` 和 ``bss`` 的初始化) + + +了解硬件组成和裸机启动过程 +---------------------------- + +在这一小节,我们介绍如何进行 **执行环境初始化** 。我们在上一小节提到过,一个应用程序的运行离不开下面多层执行环境栈的支撑。 +以 ``Hello, world!`` 程序为例,在目前广泛使用的操作系统上,它就至少需要经历以下层层递进的初始化过程: + +- 启动OS:硬件启动后,会有一段代码(一般统称为bootloader)对硬件进行初始化,让包括内核在内的系统软件得以运行; +- OS准备好应用程序执行的环境:要运行该应用程序的时候,内核分配相应资源,将程序代码和数据载入内存,并赋予 CPU 使用权,由此应用程序可以运行; +- 应用程序开始执行:程序员编写的代码是应用程序的一部分,它需要标准库/核心库进行一些初始化工作后才能运行。 + +不过我们的目标是实现在裸机上执行的应用。由于目标平台 ``riscv64gc-unknown-none-elf`` 没有任何操作系统支持,我们只能禁用标准库并移除默认的 main 函数 +入口。但是最终我们还是要将 main 函数恢复回来并且输出 ``Hello, world!`` 的。因此,我们需要知道具体需要做哪些初始化工作才能支持 +应用程序在裸机上的运行。 + +而这又需要明确三点:首先,应用程序的裸机硬件系统是啥样子的?其次,系统在做这些初始化工作之前处于什么状态;最后,在做完初始化工作也就是即将执行 main 函数之前又处于什么状态。比较二者 +即可得出答案。 + +硬件组成 +^^^^^^^^^^^^^^^^^^^^^^ + +我们采用的是QEMU软件 ``qemu-system-riscv64`` 来模拟一台RISC-V 64计算机,具体的硬件规格是: + - 外设:16550A UART,virtio-net/block/console/gpu等和设备树 + - 硬件特权级:priv v1.10, user v2.2 + - 中断控制器:可参数化的CLINT(核心本地中断器)、可参数化的PLIC(平台级中断控制器) + - 可参数化的RAM内存 + - 可配置的多核 RV64GC M/S/U mode CPU + +这里列出的硬件功能很多还用不上,不过在后面的章节中会逐步用到上面的硬件功能,以支持更加强大的操作系统能力。 + +在QEMU模拟的硬件中,物理内存和外设都是通过对内存读写的方式来进行访问,下面列出了QEMU模拟的物理内存空间。 + +.. code-block:: c + + // qemu/hw/riscv/virt.c + static const struct MemmapEntry { + hwaddr base; + hwaddr size; + } virt_memmap[] = { + [VIRT_DEBUG] = { 0x0, 0x100 }, + [VIRT_MROM] = { 0x1000, 0xf000 }, + [VIRT_TEST] = { 0x100000, 0x1000 }, + [VIRT_RTC] = { 0x101000, 0x1000 }, + [VIRT_CLINT] = { 0x2000000, 0x10000 }, + [VIRT_PCIE_PIO] = { 0x3000000, 0x10000 }, + [VIRT_PLIC] = { 0xc000000, VIRT_PLIC_SIZE(VIRT_CPUS_MAX * 2) }, + [VIRT_UART0] = { 0x10000000, 0x100 }, + [VIRT_VIRTIO] = { 0x10001000, 0x1000 }, + [VIRT_FLASH] = { 0x20000000, 0x4000000 }, + [VIRT_PCIE_ECAM] = { 0x30000000, 0x10000000 }, + [VIRT_PCIE_MMIO] = { 0x40000000, 0x40000000 }, + [VIRT_DRAM] = { 0x80000000, 0x0 }, + }; + + +到现在为止,其中比较重要的两个是: + - VIRT_DRAM:DRAM的内存起始地址是 ``0x80000000`` ,缺省大小为128MB。在本书中一般限制为8MB。 + - VIRT_UART0:串口相关的寄存器起始地址是 ``0x10000000`` ,范围是 ``0x100`` ,我们通过访问这段特殊的区域来实现字符输入输出的管理与控制。 + +.. _term-bootloader: + + +裸机启动过程 +^^^^^^^^^^^^^^^^^^ + +.. note:: + + **QEMU 模拟 CPU 加电的执行过程** + + CPU加电后的执行细节与具体硬件相关,我们这里以QEMU模拟器为具体例子简单介绍一下。 + + 这需要从 CPU 加电后如何初始化,如何执行第一条指令开始讲起。对于我们采用的QEMU模拟器而言,它模拟了一台标准的RISC-V64计算机。我们启动QEMU时,可设置一些参数,在RISC-V64计算机启动执行前,先在其模拟的内存中放置好BootLoader程序和操作系统的二进制代码。这可以通过查看 ``os/Makefile`` 文件中包含 ``qemu-system-riscv64`` 的相关内容来了解。 + + - ``-bios $(BOOTLOADER)`` 这个参数意味着硬件内存中的固定位置 ``0x80000000`` 处放置了一个BootLoader程序--RustSBI(戳 :doc:`../appendix-c/index` 可以进一步了解RustSBI。)。 + - ``-device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)`` 这个参数表示硬件内存中的特定位置 ``$(KERNEL_ENTRY_PA)`` 放置了操作系统的二进制代码 ``$(KERNEL_BIN)`` 。 ``$(KERNEL_ENTRY_PA)`` 的值是 ``0x80200000`` 。 + + 当我们执行包含上次参数的qemu-system-riscv64软件,就意味给这台虚拟的RISC-V64计算机加电了。此时,CPU的其它通用寄存器清零, + 而PC寄存器会指向 ``0x1000`` 的位置。 + 这个 ``0x1000`` 位置上是CPU加电后执行的第一条指令(固化在硬件中的一小段引导代码),它会很快跳转到 ``0x80000000`` 处, + 即RustSBI的第一条指令。RustSBI完成基本的硬件初始化后, + 会跳转操作系统的二进制代码 ``$(KERNEL_BIN)`` 所在内存位置 ``0x80200000`` ,执行操作系统的第一条指令。 + 这时我们的编写的操作系统才开始正式工作。 + + 为啥在 ``0x80000000`` 放置 ``Bootloader`` ?因为这是QEMU的硬件模拟代码中设定好的 ``Bootloader`` 的起始地址。 + + 为啥在 ``0x80200000`` 放置 ``os`` ?因为这是 ``Bootloader--RustSBI`` 的代码中设定好的 ``os`` 的起始地址。 + + +.. note:: + + **操作系统与SBI之间是啥关系?** + + SBI是RISC-V的一种底层规范,操作系统内核与实现SBI规范的RustSBI的关系有点象应用与操作系统内核的关系,后者向前者提供一定的服务。只是SBI提供的服务很少, + 能帮助操作系统内核完成的功能有限,但这些功能很底层,很重要,比如关机,显示字符串等。通过操作系统内核也能直接实现,但比较繁琐,如果RustSBI提供了服务, + 那么操作系统内核直接调用就好了。 + + +.. warning:: + + **FIXME: 提供一下分析展示** + +实现关机功能 +---------------------------- + +如果在裸机上的应用程序执行完毕并通知操作系统后,那么“三叶虫”操作系统就没事干了,实现正常关机是一个合理的选择。所以我们要让“三叶虫”操作系统能够正常关机,这是需要调用SBI提供的关机功能 ``SBI_SHUTDOWN`` ,这与上一节的 ``SYSCALL_EXIT`` 类似, +只是在具体参数上有所不同。在上一节完成的没有显示功能的用户态最小化执行环境基础上,修改后的代码如下: + +.. _term-llvm-sbicall: + +.. code-block:: rust + + // bootloader/rustsbi-qemu.bin 直接添加的SBI规范实现的二进制代码,给操作系统提供基本支持服务 + + // os/src/sbi.rs + fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize { + let mut ret; + unsafe { + llvm_asm!("ecall" + : "={x10}" (ret) + : "{x10}" (arg0), "{x11}" (arg1), "{x12}" (arg2), "{x17}" (which) + ... + + // os/src/main.rs + const SBI_SHUTDOWN: usize = 8; + + pub fn shutdown() -> ! { + sbi_call(SBI_SHUTDOWN, [0, 0, 0]); + panic!("It should shutdown!"); + } + + #[no_mangle] + extern "C" fn _start() { + shutdown(); + } + + + + +也许有同学比较迷惑,应用程序访问操作系统提供的系统调用的指令是 ``ecall`` ,操作系统访问 +RustSBI提供的SBI服务的SBI调用的指令也是 ``ecall`` 。 +这其实是没有问题的,虽然指令一样,但它们所在的特权级和特权级转换是不一样的。简单地说,应用程序位于最弱的用户特权级(User Mode),操作系统位于 +很强大的内核特权级(Supervisor Mode),RustSBI位于完全掌控机器的机器特权级(Machine Mode),通过 ``ecall`` 指令,可以完成从弱的特权级 +到强的特权级的转换。具体细节,可以看下一章的进一步描述。在这里,只要知道如果“三叶虫”操作系统正确地向RustSBI发出了停机的SBI服务请求, +那么RustSBI能够通知QEMU模拟的RISC-V计算机停机(即 ``qemu-system-riscv64`` 软件能正常退出)就行了。 + +下面是编译执行,结果如下: + + +.. code-block:: console + + # 编译生成ELF格式的执行文件 + $ cargo build --release + Compiling os v0.1.0 (/media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/os_kernel_lab/os) + Finished release [optimized] target(s) in 0.15s + # 把ELF执行文件转成bianary文件 + $ rust-objcopy --binary-architecture=riscv64 target/riscv64gc-unknown-none-elf/release/os --strip-all -O binary target/riscv64gc-unknown-none-elf/release/os.bin + + #加载运行 + $ qemu-system-riscv64 -machine virt -nographic -bios ../bootloader/rustsbi-qemu.bin -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000 + # 无法退出,风扇狂转,感觉碰到死循环 + +这样的结果是我们不期望的。问题在哪?仔细查看和思考,操作系统的入口地址不对!对 ``os`` ELF执行程序,通过rust-readobj分析,看到的入口地址不是 +RustSBIS约定的 ``0x80200000`` 。我们需要修改 ``os`` ELF执行程序的内存布局。 + + +设置正确的程序内存布局 +---------------------------- + +.. _term-linker-script: + +我们可以通过 **链接脚本** (Linker Script) 调整链接器的行为,使得最终生成的可执行文件的内存布局符合我们的预期。 +我们修改 Cargo 的配置文件来使用我们自己的链接脚本 ``os/src/linker.ld`` 而非使用默认的内存布局: + +.. code-block:: + :linenos: + :emphasize-lines: 5,6,7,8 + + // os/.cargo/config + [build] + target = "riscv64gc-unknown-none-elf" + + [target.riscv64gc-unknown-none-elf] + rustflags = [ + "-Clink-arg=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes" + ] + +具体的链接脚本 ``os/src/linker.ld`` 如下: + +.. code-block:: + :linenos: + + OUTPUT_ARCH(riscv) + ENTRY(_start) + BASE_ADDRESS = 0x80200000; + + SECTIONS + { + . = BASE_ADDRESS; + skernel = .; + + stext = .; + .text : { + *(.text.entry) + *(.text .text.*) + } + + . = ALIGN(4K); + etext = .; + srodata = .; + .rodata : { + *(.rodata .rodata.*) + } + + . = ALIGN(4K); + erodata = .; + sdata = .; + .data : { + *(.data .data.*) + } + + . = ALIGN(4K); + edata = .; + .bss : { + *(.bss.stack) + sbss = .; + *(.bss .bss.*) + } + + . = ALIGN(4K); + ebss = .; + ekernel = .; + + /DISCARD/ : { + *(.eh_frame) + } + } + +第 1 行我们设置了目标平台为 riscv ;第 2 行我们设置了整个程序的入口点为之前定义的全局符号 ``_start``; +第 3 行定义了一个常量 ``BASE_ADDRESS`` 为 ``0x80200000`` ,也就是我们之前提到的期望我们自己实现的初始化代码被放在的地址; + +从第 5 行开始体现了链接过程中对输入的目标文件的段的合并。其中 ``.`` 表示当前地址,也就是链接器会从它指向的位置开始往下放置从输入的目标文件 +中收集来的段。我们可以对 ``.`` 进行赋值来调整接下来的段放在哪里,也可以创建一些全局符号赋值为 ``.`` 从而记录这一时刻的位置。我们还能够 +看到这样的格式: + +.. code-block:: + + .rodata : { + *(.rodata) + } + +冒号前面表示最终生成的可执行文件的一个段的名字,花括号内按照放置顺序描述将所有输入目标文件的哪些段放在这个段中,每一行格式为 +``(SectionName)``,表示目标文件 ``ObjectFile`` 的名为 ``SectionName`` 的段需要被放进去。我们也可以 +使用通配符来书写 ```` 和 ```` 分别表示可能的输入目标文件和段名。因此,最终的合并结果是,在最终可执行文件 +中各个常见的段 ``.text, .rodata .data, .bss`` 从低地址到高地址按顺序放置,每个段里面都包括了所有输入目标文件的同名段, +且每个段都有两个全局符号给出了它的开始和结束地址(比如 ``.text`` 段的开始和结束地址分别是 ``stext`` 和 ``etext`` )。 + + + + +为了说明当前实现的正确性,我们需要讨论这样一个问题: + +1. 如何做到执行环境的初始化代码被放在内存上以 ``0x80200000`` 开头的区域上? + + 在链接脚本第 7 行,我们将当前地址设置为 ``BASE_ADDRESS`` 也即 ``0x80200000`` ,然后从这里开始往高地址放置各个段。第一个被放置的 + 是 ``.text`` ,而里面第一个被放置的又是来自 ``entry.asm`` 中的段 ``.text.entry``,这个段恰恰是含有两条指令的执行环境初始化代码, + 它在所有段中最早被放置在我们期望的 ``0x80200000`` 处。 + + +这样一来,我们就将运行时重建完毕了。在 ``os`` 目录下 ``cargo build --release`` 或者直接 ``make build`` 就能够看到 +最终生成的可执行文件 ``target/riscv64gc-unknown-none-elf/release/os`` 。 +通过分析,我们看到 ``0x80200000`` 处的代码是我们预期的 ``_start()`` 函数的内容。我们采用刚才的编译运行方式进行试验,发现还是同样的错误结果。 +问题出在哪里?这时需要用上 ``debug`` 大法了。 + + +.. code-block:: console + + # 在一个终端执行如下命令: + $ qemu-system-riscv64 -machine virt -nographic -bios ../bootloader/rustsbi-qemu.bin -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000 -S -s + + # 在另外一个终端执行如下命令: + $ rust-gdb target/riscv64gc-unknown-none-elf/release/os + (gdb) target remote :1234 + (gdb) break *0x80200000 + (gdb) x /16i 0x80200000 + (gdb) si + +结果发现刚执行一条指令,整个系统就飞了( ``pc`` 寄存器等已经变成为 ``0`` 了)。再一看, ``sp`` 寄存器是一个非常大的值 ``0xffffff...`` 。这就很清楚是 +**栈 stack** 出现了问题。我们没有设置好 **栈 stack** ! 好吧,我们需要考虑如何合理设置 **栈 stack** 。 + + +正确配置栈空间布局 +---------------------------- + +为了说明如何实现正确的栈,我们需要讨论这样一个问题:应用函数调用所需的栈放在哪里? + + 需要有一段代码来分配并栈空间,并把 ``sp`` 寄存器指向栈空间的起始位置(注意:栈空间是从上向下 ``push`` 数据的)。 + 所以,我们要写一小段汇编代码 ``entry.asm`` 来帮助建立好栈空间。 + 从链接脚本第 32 行开始,我们可以看出 ``entry.asm`` 中分配的栈空间对应的段 ``.bss.stack`` 被放入到可执行文件中的 + ``.bss`` 段中的低地址中。在后面虽然有一个通配符 ``.bss.*`` ,但是由于链接脚本的优先匹配规则它并不会被匹配到后面去。 + 这里需要注意的是地址区间 :math:`[\text{sbss},\text{ebss})` 并不包括栈空间,其原因后面再进行说明。 + +我们自己编写运行时初始化的代码: + +.. code-block:: asm + :linenos: + + # os/src/entry.asm + .section .text.entry + .globl _start + _start: + la sp, boot_stack_top + call rust_main + + .section .bss.stack + .globl boot_stack + boot_stack: + .space 4096 * 16 + .globl boot_stack_top + boot_stack_top: + +在这段汇编代码中,我们从第 8 行开始预留了一块大小为 4096 * 16 字节也就是 :math:`64\text{KiB}` 的空间用作接下来要运行的程序的栈空间, +这块栈空间的栈顶地址被全局符号 ``boot_stack_top`` 标识,栈底则被全局符号 ``boot_stack`` 标识。同时,这块栈空间单独作为一个名为 +``.bss.stack`` 的段,之后我们会通过链接脚本来安排它的位置。 + +从第 2 行开始,我们通过汇编代码实现执行环境的初始化,它其实只有两条指令:第一条指令将 sp 设置为我们预留的栈空间的栈顶位置,于是之后在函数 +调用的时候,栈就可以从这里开始向低地址增长了。简单起见,我们目前暂时不考虑 sp 越过了栈底 ``boot_stack`` ,也就是栈溢出的情形,虽然这有 +可能导致严重的错误。第二条指令则是通过伪指令 ``call`` 函数调用 ``rust_main`` ,这里的 ``rust_main`` 是一个我们稍后自己编写的应用 +入口。因此初始化任务非常简单:正如上面所说的一样,只需要设置栈指针 sp,随后跳转到应用入口即可。这两条指令单独作为一个名为 +``.text.entry`` 的段,且全局符号 ``_start`` 给出了段内第一条指令的地址。 + +接着,我们在 ``main.rs`` 中嵌入这些汇编代码并声明应用入口 ``rust_main`` : + +.. code-block:: rust + :linenos: + :emphasize-lines: 4,8,10,11,12,13 + + // os/src/main.rs + #![no_std] + #![no_main] + #![feature(global_asm)] + + mod lang_items; + + global_asm!(include_str!("entry.asm")); + + #[no_mangle] + pub fn rust_main() -> ! { + loop {} + } + +背景高亮指出了 ``main.rs`` 中新增的代码。 + +第 4 行中,我们手动设置 ``global_asm`` 特性来支持在 Rust 代码中嵌入全局汇编代码。第 8 行,我们首先通过 +``include_str!`` 宏将同目录下的汇编代码 ``entry.asm`` 转化为字符串并通过 ``global_asm!`` 宏嵌入到代码中。 + +从第 10 行开始, +我们声明了应用的入口点 ``rust_main`` ,这里需要注意的是需要通过宏将 ``rust_main`` 标记为 ``#[no_mangle]`` 以避免编译器对它的 +名字进行混淆,不然的话在链接的时候, ``entry.asm`` 将找不到 ``main.rs`` 提供的外部符号 ``rust_main`` 从而导致链接失败。 + + +这样一来,我们就将“三叶虫”操作系统编写完毕了。再次使用上节中的编译,生成和运行操作,我们看到QEMU模拟的RISC-V 64计算机 **优雅** 地退出了! + + + +.. code-block:: console + + $ qemu-system-riscv64 \ + > -machine virt \ + > -nographic \ + > -bios ../bootloader/rustsbi-qemu.bin \ + > -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000 + [rustsbi] Version 0.1.0 + .______ __ __ _______.___________. _______..______ __ + | _ \ | | | | / | | / || _ \ | | + | |_) | | | | | | (----`---| |----`| (----`| |_) || | + | / | | | | \ \ | | \ \ | _ < | | + | |\ \----.| `--' |.----) | | | .----) | | |_) || | + | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| + + [rustsbi] Platform: QEMU + [rustsbi] misa: RV64ACDFIMSU + [rustsbi] mideleg: 0x222 + [rustsbi] medeleg: 0xb1ab + [rustsbi] Kernel entry: 0x80200000 + # “优雅”地退出了。 + + + + + +清空 .bss 段 +---------------------------------- + +与内存相关的部分太容易出错了。所以,我们再仔细检查代码后,发现在嵌入式系统中常见的 **清零 .bss段** 的工作并没有完成。 + +由于一般应用程序的 ``.bss`` 段在程序正式开始运行之前会被执环境(系统库或操作系统内核)固定初始化为零,因此在 ELF 文件中,为了节省磁盘空间,只会记录 ``.bss`` 段的位置,且应用程序的假定在它执行前,其 ``.bss段`` 的数据内容都已是 ``全0`` 。 +如果这块区域不是全零,且执行环境也没提前清零,那么会与应用的假定矛盾,导致程序出错。对于在裸机上执行的应用程序,其执行环境(就是QEMU模拟硬件+“三叶虫”操作系统内核)将可执行文件加载到内存的时候,并负责将 ``.bss`` 所分配到的内存区域全部清零。 + +落实到我们正在实现的“三叶虫”操作系统内核,我们需要提供清零的 ``clear_bss()`` 函数。此函数属于执行环境,并在执行环境调用 +应用程序的 ``rust_main`` 主函数前,把 ``.bss`` 段的全局数据清零。 + +.. code-block:: rust + :linenos: + + // os/src/main.rs + fn clear_bss() { + extern "C" { + fn sbss(); + fn ebss(); + } + (sbss as usize..ebss as usize).for_each(|a| { + unsafe { (a as *mut u8).write_volatile(0) } + }); + } + +在程序内自己进行清零的时候,我们就不用去解析 ELF(此时也没有 ELF 可供解析)了,而是通过链接脚本 ``linker.ld`` 中给出的全局符号 +``sbss`` 和 ``ebss`` 来确定 ``.bss`` 段的位置。 + + + +我们可以松一口气了。接下来,我们要让“三叶虫”操作系统要实现“Hello, world”输出! + + +添加裸机打印相关函数 +---------------------------------- + + + +与上一节为输出字符实现的代码片段相比,裸机应用的执行环境支持字符输出的代码改动会很小。 +下面的代码基于上节有打印能力的执行环境的基础上做的变动。 + +.. code-block:: rust + + const SBI_CONSOLE_PUTCHAR: usize = 1; + + pub fn console_putchar(c: usize) { + syscall(SBI_CONSOLE_PUTCHAR, [c, 0, 0]); + } + + impl Write for Stdout { + fn write_str(&mut self, s: &str) -> fmt::Result { + //sys_write(STDOUT, s.as_bytes()); + for c in s.chars() { + console_putchar(c as usize); + } + Ok(()) + } + } + + +可以看到主要就只是把之前的操作系统系统调用改为了SBI调用。然后我们再编译运行试试, + +.. code-block:: console + + $ cargo build + $ rust-objcopy --binary-architecture=riscv64 target/riscv64gc-unknown-none-elf/debug/os --strip-all -O binary target/riscv64gc-unknown-none-elf/debug/os.bin + $ qemu-system-riscv64 -machine virt -nographic -bios ../bootloader/rustsbi-qemu.bin -device loader,file=target/riscv64gc-unknown-none-elf/debug/os.bin,addr=0x80200000 + + [rustsbi] Version 0.1.0 + .______       __    __      _______.___________.  _______..______   __ + |   _  \     |  |  |  |    /       |           | /       ||   _  \ |  | + |  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  | + |      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  | + |  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  | + | _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__| + + [rustsbi] Platform: QEMU + [rustsbi] misa: RV64ACDFIMSU + [rustsbi] mideleg: 0x222 + [rustsbi] medeleg: 0xb1ab + [rustsbi] Kernel entry: 0x80200000 + Hello, world! + +可以看到,在裸机上输出了 ``Hello, world!`` ,而且qemu正常退出,表示RISC-V计算机也正常关机了。 + + +接着我们可提高“三叶虫”操作系统处理异常的能力,即给异常处理函数 ``panic`` 增加显示字符串能力。主要修改内容如下: + +.. code-block:: rust + + // os/src/main.rs + #![feature(panic_info_message)] + + #[panic_handler] + fn panic(info: &PanicInfo) -> ! { + if let Some(location) = info.location() { + println!("Panicked at {}:{} {}", location.file(), location.line(), info.message().unwrap()); + } else { + println!("Panicked: {}", info.message().unwrap()); + } + shutdown() + } + +我们尝试从传入的 ``PanicInfo`` 中解析 panic 发生的文件和行数。如果解析成功的话,就和 panic 的报错信息一起打印出来。我们需要在 +``main.rs`` 开头加上 ``#![feature(panic_info_message)]`` 才能通过 ``PanicInfo::message`` 获取报错信息。 + +但我们在 ``main.rs`` 的 ``rust_main`` 函数中调用 ``panic!("It should shutdown!");`` 宏时,整个模拟执行的结果是: + +.. code-block:: console + + $ cargo build --release + $ rust-objcopy --binary-architecture=riscv64 target/riscv64gc-unknown-none-elf/release/os \ + --strip-all -O binary target/riscv64gc-unknown-none-elf/release/os.bin + $ qemu-system-riscv64 \ + -machine virt \ + -nographic \ + -bios ../bootloader/rustsbi-qemu.bin \ + -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000 + + [rustsbi] Version 0.1.0 + .______ __ __ _______.___________. _______..______ __ + | _ \ | | | | / | | / || _ \ | | + | |_) | | | | | | (----`---| |----`| (----`| |_) || | + | / | | | | \ \ | | \ \ | _ < | | + | |\ \----.| `--' |.----) | | | .----) | | |_) || | + | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| + + [rustsbi] Platform: QEMU + [rustsbi] misa: RV64ACDFIMSU + [rustsbi] mideleg: 0x222 + [rustsbi] medeleg: 0xb1ab + [rustsbi] Kernel entry: 0x80200000 + Hello, world! + Panicked at src/main.rs:95 It should shutdown! + +可以看到产生panic的地点在 ``main.rs`` 的第95行,与源码中的实际位置一致!到这里,我们基本上算是完成了第一章的实验内容, +实现了支持应用程序在裸机上显示字符串的“三叶虫”操作系统。但也能看出,这个操作系统很脆弱,只能支持一个简单的易用,在本质上 +是一个提供方便服务接口的库。“三叶虫”操作系统还需进化,提升能力。 +在下一章,我们将进入“敏迷龙”操作系统的设计与实现。 + + +.. note:: + + **Rust 小知识: 错误处理** + + Rust 中常利用 ``Option`` 和 ``Result`` 进行方便的错误处理。它们都属于枚举结构: + + - ``Option`` 既可以有值 ``Option::Some`` ,也有可能没有值 ``Option::None``; + - ``Result`` 既可以保存某个操作的返回值 ``Result::Ok`` ,也可以表明操作过程中出现了错误 ``Result::Err`` 。 + + 我们可以使用 ``Option/Result`` 来保存一个不能确定存在/不存在或是成功/失败的值。之后可以通过匹配 ``if let`` 或是在能够确定 + 的场合直接通过 ``unwrap`` 将里面的值取出。详细的内容可以参考 Rust 官方文档。 diff --git a/source/chapter1/4load-manually.rst b/source/chapter1/4load-manually.rst deleted file mode 100644 index 773cda7e94d155619460c7a2ce7fd63cf3e05e2d..0000000000000000000000000000000000000000 --- a/source/chapter1/4load-manually.rst +++ /dev/null @@ -1,320 +0,0 @@ -手动加载、运行应用程序 -================================== - -.. toctree:: - :hidden: - :maxdepth: 5 - -在上一节中我们自己实现了一套运行时来代替标准库,并完整的构建了最终的可执行文件。但是它现在只是放在磁盘上的一个文件,若想将它运行起来的话, -就需要将它加载到内存中,在大多数情况下这是操作系统的任务。 - -让我们先来看看最终可执行文件的格式: - -.. code-block:: console - - $ file os/target/riscv64gc-unknown-none-elf/release/os - os/target/riscv64gc-unknown-none-elf/release/os: ELF 64-bit LSB executable, - UCB RISC-V, version 1 (SYSV), statically linked, not stripped - -从中可以看出可执行文件的格式为 **可执行和链接格式** (Executable and Linkable Format, ELF),硬件平台是 RV64 。在 ELF 文件中, -除了程序必要的代码、数据段(它们本身都只是一些二进制的数据)之外,还有一些 **元数据** (Metadata) 描述这些段在地址空间中的位置和在 -文件中的位置以及一些权限控制信息,这些元数据只能放在代码、数据段的外面。 - -我们可以通过二进制工具 ``readelf`` 来看看 ELF 文件中究竟包含什么内容,输入命令: - -.. code-block:: console - - $ riscv64-unknown-elf-readelf os/target/riscv64gc-unknown-none-elf/release/os -a - -首先可以看到一个 ELF header,它位于 ELF 文件的开头: - -.. code-block:: objdump - :linenos: - :emphasize-lines: 2,11,12,13,17,19 - - ELF Header: - Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 - Class: ELF64 - Data: 2's complement, little endian - Version: 1 (current) - OS/ABI: UNIX - System V - ABI Version: 0 - Type: EXEC (Executable file) - Machine: RISC-V - Version: 0x1 - Entry point address: 0x80020000 - Start of program headers: 64 (bytes into file) - Start of section headers: 9016 (bytes into file) - Flags: 0x1, RVC, soft-float ABI - Size of this header: 64 (bytes) - Size of program headers: 56 (bytes) - Number of program headers: 3 - Size of section headers: 64 (bytes) - Number of section headers: 8 - Section header string table index: 6 - -- 第 2 行是一个称之为 **魔数** (Magic) 独特的常数,存放在 ELF header 的一个固定位置。当加载器将 ELF 文件加载到内存之前,通常会查看 - 该位置的值是否正确,来快速确认被加载的文件是不是一个 ELF 。 -- 第 11 行给出了可执行文件的入口点为 ``0x80020000`` ,这正是我们上一节所做的事情。 -- 从 12/13/17/19 行中,我们可以知道除了 ELF header 之外,还有另外两种不同的 header,分别称为 program header 和 section header, - 它们都有多个。ELF header 中给出了三种 header 的大小、在文件中的位置以及数目。 - -一共有 3 个不同的 program header,它们从文件的 64 字节开始,每个 56 字节: - -.. code-block:: objdump - - Program Headers: - Type Offset VirtAddr PhysAddr - FileSiz MemSiz Flags Align - LOAD 0x0000000000001000 0x0000000080020000 0x0000000080020000 - 0x000000000000001a 0x000000000000001a R E 0x1000 - LOAD 0x0000000000002000 0x0000000080021000 0x0000000080021000 - 0x0000000000000000 0x0000000000010000 RW 0x1000 - GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 - 0x0000000000000000 0x0000000000000000 RW 0x0 - -每个 program header 指向一个在加载的时候可以连续加载的区域。 - -一共有 8 个不同的 section header,它们从文件的 9016 字节开始,每个 64 字节: - -.. code-block:: objdump - - Section Headers: - [Nr] Name Type Address Offset - Size EntSize Flags Link Info Align - [ 0] NULL 0000000000000000 00000000 - 0000000000000000 0000000000000000 0 0 0 - [ 1] .text PROGBITS 0000000080020000 00001000 - 000000000000001a 0000000000000000 AX 0 0 2 - [ 2] .bss NOBITS 0000000080021000 00002000 - 0000000000010000 0000000000000000 WA 0 0 1 - [ 3] .riscv.attributes RISCV_ATTRIBUTE 0000000000000000 00002000 - 000000000000006a 0000000000000000 0 0 1 - [ 4] .comment PROGBITS 0000000000000000 0000206a - 0000000000000013 0000000000000001 MS 0 0 1 - [ 5] .symtab SYMTAB 0000000000000000 00002080 - 00000000000001c8 0000000000000018 7 4 8 - [ 6] .shstrtab STRTAB 0000000000000000 00002248 - 0000000000000041 0000000000000000 0 0 1 - [ 7] .strtab STRTAB 0000000000000000 00002289 - 00000000000000ab 0000000000000000 0 0 1 - Key to Flags: - W (write), A (alloc), X (execute), M (merge), S (strings), I (info), - L (link order), O (extra OS processing required), G (group), T (TLS), - C (compressed), x (unknown), o (OS specific), E (exclude), - p (processor specific) - - There are no section groups in this file. - -每个 section header 则描述一个段的元数据。 - -其中,我们看到了代码段 ``.text`` 被放在可执行文件的 4096 字节处,大小 0x1a=26 字节,需要被加载到地址 ``0x80020000``。 -它们分别由元数据的字段 Offset、 Size 和 Address 给出。同理,我们自己预留的应用程序函数调用栈在 ``.bss`` 段中,大小为 :math:`64\text{KiB}` -,需要被加载到地址 ``0x80021000`` 处。我们没有看到 ``.data/.rodata`` 等段,因为目前的 ``rust_main`` 里面没有任何东西。 - -我们还能够看到 ``.symtab`` 段中给出的符号表: - -.. code-block:: - - Symbol table '.symtab' contains 19 entries: - Num: Value Size Type Bind Vis Ndx Name - 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND - 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS os.78wp4f2l-cgu.0 - 2: 0000000000000000 0 FILE LOCAL DEFAULT ABS os.78wp4f2l-cgu.1 - 3: 0000000080020000 0 NOTYPE LOCAL DEFAULT 1 .Lpcrel_hi0 - 4: 0000000080020000 0 NOTYPE GLOBAL DEFAULT 1 _start - 5: 0000000080021000 0 NOTYPE GLOBAL DEFAULT 2 boot_stack - 6: 0000000080031000 0 NOTYPE GLOBAL DEFAULT 2 boot_stack_top - 7: 0000000080020010 10 FUNC GLOBAL DEFAULT 1 rust_main - 8: 0000000080020000 0 NOTYPE GLOBAL DEFAULT ABS BASE_ADDRESS - 9: 0000000080020000 0 NOTYPE GLOBAL DEFAULT 1 skernel - 10: 0000000080020000 0 NOTYPE GLOBAL DEFAULT 1 stext - 11: 0000000080021000 0 NOTYPE GLOBAL DEFAULT 1 etext - 12: 0000000080021000 0 NOTYPE GLOBAL DEFAULT 1 srodata - 13: 0000000080021000 0 NOTYPE GLOBAL DEFAULT 1 erodata - 14: 0000000080021000 0 NOTYPE GLOBAL DEFAULT 1 sdata - 15: 0000000080021000 0 NOTYPE GLOBAL DEFAULT 1 edata - 16: 0000000080031000 0 NOTYPE GLOBAL DEFAULT 2 sbss - 17: 0000000080031000 0 NOTYPE GLOBAL DEFAULT 2 ebss - 18: 0000000080031000 0 NOTYPE GLOBAL DEFAULT 2 ekernel - -里面包括了栈顶、栈底、rust_main 的地址以及我们在 ``linker.ld`` 中定义的各个段开始和结束地址。 - -因此,从 ELF header 中可以看出,ELF 中的内容按顺序应该是: - -- ELF header -- 若干个 program header -- 程序各个段的实际数据 -- 若干的 section header - -当将程序加载到内存的时候,对于每个 program header 所指向的区域,我们需要将对应的数据从文件复制到内存中。这就需要解析 ELF 的元数据 -才能知道数据在文件中的位置以及即将被加载到内存中的位置。但目前,我们不需要从 ELF 中解析元数据就知道程序的内存布局 -(这个内存布局是我们按照需求自己指定的),我们可以手动完成加载任务。 - -具体的做法是利用 ``rust-objcopy`` 工具删除掉 ELF 文件中的 -所有 header 只保留各个段的实际数据得到一个没有任何符号的纯二进制镜像文件,由于缺少了必要的元数据,我们的二进制工具也没有办法 -对它完成解析了。而后,我们直接将这个二进制镜像文件手动载入到内存中合适位置即可。在这里,我们知道在镜像文件中,仍然是代码段 ``.text`` -作为起始,因此我们要将这个代码段载入到 ``0x80020000`` 才能和上一级 bootloader 对接上。因此,我们只要把整个镜像文件手动载入到 -内存的地址 ``0x80020000`` 处即可。在不同的硬件平台上,手动加载的方式是不同的。 - -qemu 平台 -------------------------- - -首先我们还原一下可执行文件和二进制镜像的生成流程: - -.. code-block:: makefile - - # os/Makefile - TARGET := riscv64gc-unknown-none-elf - MODE := release - KERNEL_ELF := target/$(TARGET)/$(MODE)/os - KERNEL_BIN := $(KERNEL_ELF).bin - - $(KERNEL_BIN): kernel - @$(OBJCOPY) $(KERNEL_ELF) --strip-all -O binary $@ - - kernel: - @cargo build --release - -这里可以看出 ``KERNEL_ELF`` 保存最终可执行文件 ``os`` 的路径,而 ``KERNEL_BIN`` 保存只保留各个段数据的二进制镜像文件 ``os.bin`` -的路径。目标 ``kernel`` 直接通过 ``cargo build`` 以 release 模式最终可执行文件,目标 ``KERNEL_BIN`` 依赖于目标 ``kernel``,将 -可执行文件通过 ``rust-objcopy`` 工具加上适当的配置移除所有的 header 和符号得到二进制镜像。 - -我们可以通过 ``make run`` 直接在 qemu 上运行我们的应用程序,qemu 是一个虚拟机,它完整的模拟了一整套硬件平台,就像是一台真正的计算机 -一样,我们来看运行 qemu 的具体命令: - -.. code-block:: makefile - :linenos: - :emphasize-lines: 11,12,13,14,15 - - KERNEL_ENTRY_PA := 0x80020000 - - BOARD ?= qemu - SBI ?= rustsbi - BOOTLOADER := ../bootloader/$(SBI)-$(BOARD).bin - - run: run-inner - - run-inner: build - ifeq ($(BOARD),qemu) - @qemu-system-riscv64 \ - -machine virt \ - -nographic \ - -bios $(BOOTLOADER) \ - -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) - else - @cp $(BOOTLOADER) $(BOOTLOADER).copy - @dd if=$(KERNEL_BIN) of=$(BOOTLOADER).copy bs=128K seek=1 - @mv $(BOOTLOADER).copy $(KERNEL_BIN) - @sudo chmod 777 $(K210-SERIALPORT) - python3 $(K210-BURNER) -p $(K210-SERIALPORT) -b 1500000 $(KERNEL_BIN) - miniterm --eol LF --dtr 0 --rts 0 --filter direct $(K210-SERIALPORT) 115200 - endif - -注意其中高亮部分给出了传给 qemu 的参数。 - -- ``-machine`` 告诉 qemu 使用预设的硬件配置。在整个项目中我们将一直沿用该配置。 -- ``-bios`` 告诉 qemu 使用我们放在 ``bootloader`` 目录下的预编译版本作为 bootloader。 -- ``-device`` 则告诉 qemu 将二进制镜像加载到内存指定的位置。 - -可以先输入 Ctrl+A ,再输入 X 来退出 qemu 终端。 - -.. warning:: - - **FIXME: 使用 GDB 跟踪 qemu 的运行状态** - - -k210 平台 ------------------------- - -对于 k210 平台来说,只需要将 maix 系列开发板通过数据线连接到 PC,然后 ``make run BOARD=k210`` 即可。从 Makefile 中来看: - -.. code-block:: makefile - :linenos: - :emphasize-lines: 13,16,17 - - K210-SERIALPORT = /dev/ttyUSB0 - K210-BURNER = ../tools/kflash.py - - run-inner: build - ifeq ($(BOARD),qemu) - @qemu-system-riscv64 \ - -machine virt \ - -nographic \ - -bios $(BOOTLOADER) \ - -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) - else - @cp $(BOOTLOADER) $(BOOTLOADER).copy - @dd if=$(KERNEL_BIN) of=$(BOOTLOADER).copy bs=128K seek=1 - @mv $(BOOTLOADER).copy $(KERNEL_BIN) - @sudo chmod 777 $(K210-SERIALPORT) - python3 $(K210-BURNER) -p $(K210-SERIALPORT) -b 1500000 $(KERNEL_BIN) - miniterm --eol LF --dtr 0 --rts 0 --filter direct $(K210-SERIALPORT) 115200 - endif - -在构建目标 ``run-inner`` 的时候,根据平台 ``BOARD`` 的不同,启动运行的指令也不同。当我们传入命令行参数 ``BOARD=k210`` 时,就会进入下面 -的分支。 - -- 第 13 行我们使用 ``dd`` 工具将 bootloader 和二进制镜像拼接到一起,这是因为 k210 平台的写入工具每次只支持写入一个文件,所以我们只能 - 将二者合并到一起一并写入 k210 的内存上。这样的参数设置可以保证 bootloader 在合并后文件的开头,而二进制镜像在文件偏移量 0x20000 的 - 位置处。有兴趣的读者可以输入命令 ``man dd`` 查看关于工具 ``dd`` 的更多信息。 -- 第 16 行我们使用烧写工具 ``K210-BURNER`` 将合并后的镜像烧写到 k210 开发板的内存的 ``0x80000000`` 地址上。 - 参数 ``K210-SERIALPORT`` 表示当前 OS 识别到的 k210 开发板的串口设备名。在 Ubuntu 平台上一般为 ``/dev/ttyUSB0``。 -- 第 17 行我们打开串口终端和 k210 开发板进行通信,可以通过键盘向 k210 开发板发送字符并在屏幕上看到 k210 开发板的字符输出。 - -可以输入 Ctrl+] 退出 miniterm。 - -手动清空 .bss 段 ----------------------------------- - -由于 ``.bss`` 段需要在程序正式开始运行之前被固定初始化为零,因此在 ELF 文件中,为了节省磁盘空间,只会记录 ``.bss`` 段的位置而并不是 -有一块长度相等的全为零的数据。在内核将可执行文件加载到内存的时候,它需要负责将 ``.bss`` 所分配到的内存区域全部清零。而我们这里需要在 -应用程序 ``rust_main`` 中,在访问任何 ``.bss`` 段的全局数据之前手动将其清零。 - -.. code-block:: rust - :linenos: - - // os/src/main.rs - fn clear_bss() { - extern "C" { - fn sbss(); - fn ebss(); - } - (sbss as usize..ebss as usize).for_each(|a| { - unsafe { (a as *mut u8).write_volatile(0) } - }); - } - -在程序内自己进行清零的时候,我们就不用去解析 ELF(此时也没有 ELF 可供解析)了,而是通过链接脚本 ``linker.ld`` 中给出的全局符号 -``sbss`` 和 ``ebss`` 来确定 ``.bss`` 段的位置。 - -.. note:: - - **Rust 小知识:外部符号引用** - - extern "C" 可以引用一个外部的 C 函数接口(这意味着调用它的时候要遵从目标平台的 C 语言调用规范)。但我们这里只是引用位置标志 - 并将其转成 usize 获取它的地址。由此可以知道 ``.bss`` 段两端的地址。 - - **Rust 小知识:迭代器与闭包** - - 代码第 7 行用到了 Rust 的迭代器与闭包的语法,它们在很多情况下能够提高开发效率。如读者感兴趣的话也可以将其改写为等价的 for - 循环实现。 - -.. warning:: - - **Rust Unsafe** - - 代码第 8 行,我们将 ``.bss`` 段内的一个地址转化为一个 **裸指针** (Raw Pointer),并将它指向的值修改为 0。这在 C 语言中是 - 一种司空见惯的操作,但在 Rust 中我们需要将他包裹在 unsafe 块中。这是因为,Rust 认为对于裸指针的 **解引用** (Dereference) - 是一种 unsafe 行为。 - - 相比 C 语言,Rust 进行了更多的语义约束来保证安全性(内存安全/类型安全/并发安全),这在编译期和运行期都有所体现。但在某些时候, - 尤其是与底层硬件打交道的时候,在 Rust 的语义约束之内没法满足我们的需求,这个时候我们就需要将超出了 Rust 语义约束的行为包裹 - 在 unsafe 块中,告知编译器不需要对它进行完整的约束检查,而是由程序员自己负责保证它的安全性。当代码不能正常运行的时候,我们往往也是 - 最先去检查 unsafe 块中的代码,因为它没有受到编译器的保护,出错的概率更大。 - - C 语言中的指针相当于 Rust 中的裸指针,它无所不能但又太过于灵活,程序员对其不谨慎的使用常常会引起很多内存不安全问题,最常见的如 - 悬垂指针和多次回收的问题,Rust 编译器没法确认程序员对它的使用是否安全,因此将其划到 unsafe Rust 的领域。在 safe Rust 中,我们 - 有引用 ``&/&mut`` 以及各种功能各异的智能指针 ``Box/RefCell/Rc`` 可以使用,只要按照 Rust 的规则来使用它们便可借助 - 编译器在编译期就解决很多潜在的内存不安全问题。 - diff --git a/source/chapter1/3minimal-rt.rst b/source/chapter1/4understand-prog.rst similarity index 62% rename from source/chapter1/3minimal-rt.rst rename to source/chapter1/4understand-prog.rst index b2acfe6540d48c7038b2b06ccf24906d15384d75..927d4c11e2af74ce7ebbfcdf274fb5ef8270d9ed 100644 --- a/source/chapter1/3minimal-rt.rst +++ b/source/chapter1/4understand-prog.rst @@ -1,48 +1,40 @@ -重建最小化运行时 -================================= +理解应用程序和执行环境 +================================== .. toctree:: :hidden: :maxdepth: 5 -本节开始我们将着手自己来实现之前被我们移除的 ``Hello, world!`` 程序中的功能。在这一小节,我们介绍如何进行 **执行环境初始化** 。 -我们在上一小节提到过,一个应用程序的运行离不开下面多层执行环境栈的支撑。以 ``Hello, world!`` 程序为例,在目前广泛使用的操作系统上, -它就至少需要经历以下层层递进的初始化过程: -- 一段汇编代码对硬件进行初始化,让上层包括内核在内的软件得以运行; -- 要运行该程序的时候,内核分配相应资源,将程序代码和数据载入内存,并赋予 CPU 使用权,由此应用程序可以运行; -- 程序员编写的代码是应用程序的一部分,它需要标准库进行一些初始化工作后才能运行。 +本节导读 +------------------------------- -但在上一小节中,由于目标平台 ``riscv64gc-unknown-none-elf`` 没有任何操作系统支持,我们只能禁用标准库并移除默认的 main 函数 -入口。但是最终我们还是要将 main 恢复回来并且在里面输出 ``Hello, world!`` 的。因此,我们需要知道具体需要做哪些初始化工作才能支持 -main 的运行。 +在前面几节,我们进行了大量的实验。接下来是要消化总结和归纳理解的时候了。 +本节主要会进一步归纳总结执行程序和执行环境相关的基础知识: -而这又需要明确两点:首先是系统在做这些初始化工作之前处于什么状态,在做完初始化工作也就是即将执行 main 函数之前又处于什么状态。比较二者 -即可得出答案。 + - 物理内存与物理地址 + - 函数调用与栈 + - 调用规范 + - 程序内存布局 + - 执行环境 -让我们从 CPU 加电后第一条指令开始讲起。对于裸机平台 ``riscv64gc-unknown-none-elf`` 而言,它的 pc 寄存器会被设置为 ``0x80000000`` , -也就是说它会从这个 **物理地址** (Physical Address) 开始一条条取指并执行放置于 **物理内存** (Physical Memory) 中的指令。 +如果读者已经了解,可直接跳过,进入下一节。 -.. note:: - - **物理内存与物理地址** +.. _term-physical-address: +.. _term-physical-memory: - 物理内存是计算机体系结构中一个重要的组成部分。在存储方面,CPU 唯一能够直接访问的只有物理内存中的数据,它可以通过访存指令来达到这一目的。 - 从 CPU 的视角看来,可以将物理内存看成一个大字节数组,而物理地址则对应于一个能够用来访问数组中某个元素的下标。与我们日常编程习惯不同的 - 是,该下标通常不以 0 开头,而通常以 ``0x80000000`` 开头。总结一下的话就是, CPU 可以通过物理地址来 *逐字节* 访问物理内存中保存的 - 数据。 - 值得一提的是,当 CPU 以多个字节(比如 2/4/8 或更多)为单位访问物理内存(事实上并不局限于物理内存)中的数据时,就有可能会引入端序和 - 地址对齐的问题。由于这并不是重点,我们在这里不展开说明。 +物理内存与物理地址 +---------------------------- +物理内存是计算机体系结构中一个重要的组成部分。在存储方面,CPU 唯一能够直接访问的只有物理内存中的数据,它可以通过访存指令来达到这一目的。 +从 CPU 的视角看来,可以将物理内存看成一个大字节数组,而物理地址则对应于一个能够用来访问数组中某个元素的下标。与我们日常编程习惯不同的 +是,该下标通常不以 0 开头,而通常以 ``0x80000000`` 开头。总结一下的话就是, CPU 可以通过物理地址来 *逐字节* 访问物理内存中保存的 +数据。 -在该目标平台上,物理内存以物理地址 ``0x80000000`` 开头的部分放置着 **引导加载程序** (Bootloader) 的代码。它的任务是对硬件进行一些 -初始化工作,并跳转到一个固定的物理地址 ``0x80020000`` 。在本书正文中我们无需关心它的实现,而是当成一个黑盒使用即可,它的预编译版本 -可执行文件放在项目根目录的 ``bootloader`` 目录下。在这之后,控制权就会被移交到我们手中。因此,我们需要保证我们负责的初始化的代码 -出现在物理内存以物理地址 ``0x80020000`` 开头的地方。在我们的初始化任务完成之后,自然需要跳转到 main 函数进行执行里面的代码,这也是 -初始化任务的一个重要部分。 +值得一提的是,当 CPU 以多个字节(比如 2/4/8 或更多)为单位访问物理内存(事实上并不局限于物理内存)中的数据时,就有可能会引入端序和 +地址对齐的问题。由于这并不是重点,我们在这里不展开说明。 -但实际上不止如此,我们还需要考虑栈的设置。 .. _function-call-and-stack: @@ -51,11 +43,15 @@ main 的运行。 从汇编指令的级别看待一段程序的执行,假如 CPU 依次执行的指令的物理地址序列为 :math:`\{a_n\}`,那么这个序列会符合怎样的模式呢? +.. _term-control-flow: + 其中最简单的无疑就是 CPU 一条条连续向下执行指令,也即满足递推式 :math:`a_{n+1}=a_n+L`,这里我们假设该平台的指令是定长的且均为 :math:`L` 字节(常见情况为 2/4 字节)。但是执行序列并不总是符合这种模式,当位于物理地址 :math:`a_n` 的指令是一条跳转指令的时候, 该模式就有可能被破坏。跳转指令对应于我们在程序中构造的 **控制流** (Control Flow) 的多种不同结构,比如分支结构(如 if/switch 语句) 和循环结构(如 for/while 语句)。用来实现上述两种结构的跳转指令,只需实现跳转功能,也就是将 pc 寄存器设置到一个指定的地址即可。 +.. _term-function-call: + 另一种控制流结构则显得更为复杂: **函数调用** (Function Call)。我们大概清楚调用函数整个过程中代码执行的顺序,如果是从源代码级的 视角来看,我们会去执行被调用函数的代码,等到它返回之后,我们会回到调用函数对应语句的下一行继续执行。那么我们如何用汇编指令来实现 这一过程?首先在调用的时候,需要有一条指令跳转到被调用函数的位置,这个看起来和其他控制结构没什么不同;但是在被调用函数返回的时候,我们 @@ -63,6 +59,12 @@ main 的运行。 地方调用同一个函数,显然函数返回之后会回到不同的地址。这是一个很大的不同:其他控制流都只需要跳转到一个 *编译期固定下来* 的地址,而函数调用 的返回跳转是跳转到一个 *运行时确定* (确切地说是在函数调用发生的时候)的地址。 + +.. image:: function-call.png + :align: center + :name: function-call + + 对此,指令集必须给用于函数调用的跳转指令一些额外的能力,而不只是单纯的跳转。在 RISC-V 架构上,有两条指令即符合这样的特征: .. list-table:: RISC-V 函数调用跳转指令 @@ -81,6 +83,10 @@ main 的运行。 :math:`\text{pc}\leftarrow\text{rs}+\text{imm}` +.. _term-source-register: +.. _term-immediate: +.. _term-destination-register: + .. note:: **RISC-V 指令各部分含义** @@ -90,12 +96,16 @@ main 的运行。 可以在 32 个通用寄存器 x0~x31 中选取。但是这三个部分都不是必须的,某些指令只有一种输入类型,另一些指令则没有输出部分。 +.. _term-pseudo-instruction: + 从中可以看出,这两条指令除了设置 pc 寄存器完成跳转功能之外,还将当前跳转指令的下一条指令地址保存在 rd 寄存器中。 (这里假设所有指令的长度均为 4 字节,在不使用 C 标准指令集拓展的情况下成立) 在 RISC-V 架构中, 通常使用 ra(x1) 寄存器作为其中的 rd ,因此在函数返回的时候,只需跳转回 ra 所保存的地址即可。事实上在函数返回的时候我们常常使用一条 **伪指令** (Pseudo Instruction) 跳转回调用之前的位置: ``ret`` 。它会被汇编器翻译为 ``jalr x0, 0(x1)``,含义为跳转到寄存器 -ra 保存的物理地址,由于 x0 是一个恒为 0 的寄存器,在 rd 中保存这一步被省略。总结一下,在进行函数调用的时候,我们通过 jalr 指令 +ra 保存的物理地址,由于 x0 是一个恒为 0 的寄存器,在 rd 中保存这一步被省略。 + +总结一下,在进行函数调用的时候,我们通过 jalr 指令 保存返回地址并实现跳转;而在函数即将返回的时候,则通过 ret 指令跳转之前的下一条指令继续执行。这两条指令实现了函数调用流程的核心机制。 由于我们是在 ra 寄存器中保存返回地址的,我们要保证它在函数执行的全程不发生变化,不然在 ret 之后就会跳转到错误的位置。事实上编译器 @@ -104,16 +114,24 @@ ra 保存的物理地址,由于 x0 是一个恒为 0 的寄存器,在 rd 中 变得多么复杂。如果我们试图在一个函数 :math:`f` 中调用一个子函数,在跳转到子函数 :math:`g` 的同时,ra 会被覆盖成这条跳转指令的 下一条的地址,而 ra 之前所保存的函数 :math:`f` 的返回地址将会 `永久丢失` 。 +.. _term-function-context: +.. _term-activation-record: + 因此,若想正确实现嵌套函数调用的控制流,我们必须通过某种方式保证:在一个函数调用子函数的前后,ra 寄存器的值不能发生变化。但实际上, 这并不仅仅局限于 ra 一个寄存器,而是作用于所有的通用寄存器。这是因为,编译器是独立编译每个函数的,因此一个函数并不能知道它所调用的 子函数修改了哪些寄存器。而站在一个函数的视角,在调用子函数的过程中某些寄存器的值被覆盖的确会对它接下来的执行产生影响。因此这是必要的。 -我们将在控制流转移前后需要保持不变的寄存器集合称之为 **上下文** (Context) 或称 **活动记录** (Activation Record),利用这一概念 +我们将由于函数调用,在控制流转移前后需要保持不变的寄存器集合称之为 **函数调用上下文** (Context) 或称 **活动记录** (Activation Record),利用这一概念 ,则在函数调用前后需要保持不变的寄存器集合被称为函数调用上下文。 +.. _term-save-restore: + 由于每个 CPU 只有一套寄存器,我们若想在子函数调用前后保持函数调用上下文不变,需要物理内存的帮助。确切的说,在调用子函数之前,我们需要在 内存中的一个区域 **保存** (Save) 函数调用上下文中的寄存器;而之后我们会从内存中同样的区域读取并 **恢复** (Restore) 函数调用上下文 中的寄存器。实际上,这一工作是由子函数的调用者和被调用者(也就是子函数自身)合作完成。函数调用上下文中的寄存器被分为如下两类: +.. _term-callee-saved: +.. _term-caller-saved: + - **被调用者保存** (Callee-Saved) 寄存器,即被调用的函数保证调用它前后,这些寄存器保持不变; - **调用者保存** (Caller-Saved) 寄存器,被调用的函数可能会覆盖这些寄存器。 @@ -122,6 +140,9 @@ ra 保存的物理地址,由于 x0 是一个恒为 0 的寄存器,在 rd 中 - 调用者:首先保存不希望在函数调用过程中发生变化的调用者保存寄存器,然后通过 jal/jalr 指令调用子函数,返回回来之后恢复这些寄存器。 - 被调用者:在函数开头保存函数执行过程中被用到的被调用者保存寄存器,然后执行函数,在退出之前恢复这些寄存器。 +.. _term-prologue: +.. _term-epilogue: + 我们发现无论是调用者还是被调用者,都会因调用行为而需要两段匹配的保存和恢复寄存器的汇编代码,可以分别将其称为 **开场白** (Prologue) 和 **收场白** (Epilogue),它们会由编译器帮我们自动插入。一个函数既有可能作为调用者调用其他函数,也有可能作为被调用者被其他函数调用。对于 它而言,如果在执行的时候需要修改被调用者保存寄存器,而必须在函数开头的开场白和结尾的收场白处进行保存;对于调用者保存寄存器则可以没有任何 @@ -136,6 +157,13 @@ ra 保存的物理地址,由于 x0 是一个恒为 0 的寄存器,在 rd 中 使用到的被调用者保存寄存器也无需保存。编译器作为寄存器的使用者自然知道在这两个场景中,分别有哪些值得保存的寄存器。 从这一角度也可以理解为何要将函数调用上下文分成两类:可以在尽可能早的时候优化掉一些无用的寄存器保存与恢复。 +.. _term-calling-convention: + + +调用规范 +---------------- + + **调用规范** (Calling Convention) 约定在某个指令集架构上,某种编程语言的函数调用如何实现。它包括了以下内容: 1. 函数的输入参数和返回值如何传递; @@ -180,6 +208,10 @@ ra 保存的物理地址,由于 x0 是一个恒为 0 的寄存器,在 rd 中 更加详细的内容可以参考 Cornell 的 `课件 `_ 。 +.. _term-stack: +.. _term-stack-pointer: +.. _term-stack-frame: + 之前我们讨论了函数调用上下文的保存/恢复时机以及寄存器的选择,但我们并没有详细说明这些寄存器保存在哪里,只是用“内存中的一块区域”草草带过。实际上, 它更确切的名字是 **栈** (Stack) 。 sp(x2) 常用来保存 **栈指针** (Stack Pointer),它是一个指向了内存中已经用过的位置的一个地址。在 RISC-V 架构中,栈是从高地址到低地址增长的。在一个函数中,作为起始的开场白负责分配一块新的栈空间,其实它只需要知道需要空间的大小,然后将 sp @@ -193,6 +225,8 @@ RISC-V 架构中,栈是从高地址到低地址增长的。在一个函数中 函数调用与栈帧:如图所示,我们能够看到在程序依次调用 a、调用 b、调用 c、c 返回、b 返回整个过程中栈帧的分配/回收以及 sp 寄存器的变化。 图中标有 a/b/c 的块分别代表函数 a/b/c 的栈帧。 +.. _term-lifo: + .. note:: **数据结构中的栈与实现函数调用所需要的栈** @@ -229,6 +263,9 @@ RISC-V 架构中,栈是从高地址到低地址增长的。在一个函数中 程序内存布局 ---------------------------- +.. _term-section: +.. _term-memory-layout: + 在我们将源代码编译为可执行文件之后,它就会变成一个看似充满了杂乱无章的字节的一个文件。但我们知道这些字节至少可以分成代码和数据两部分,在 程序运行起来的时候它们的功能并不相同:代码部分由一条条可以被 CPU 解码并执行的指令组成,而数据部分只是被 CPU 视作可用的存储空间。事实上 我们还可以根据其功能进一步把两个部分划分为更小的单位: **段** (Section) 。不同的段会被编译器放置在内存不同的位置上,这构成了程序的 @@ -243,6 +280,8 @@ RISC-V 架构中,栈是从高地址到低地址增长的。在一个函数中 数据部分则还可以继续细化: +.. _term-heap: + - 已初始化数据段保存程序中那些已初始化的全局数据,分为 ``.rodata`` 和 ``.data`` 两部分。前者存放只读的全局数据,通常是一些常数或者是 常量字符串等;而后者存放可修改的全局数据。 - 未初始化数据段 ``.bss`` 保存程序中那些未初始化的全局数据,通常由程序的加载者代为进行零初始化,也即将这块区域逐字节清零; @@ -264,6 +303,11 @@ RISC-V 架构中,栈是从高地址到低地址增长的。在一个函数中 我们可以将常说的编译流程细化为多个阶段(虽然输入一条命令便可将它们全部完成): +.. _term-compiler: +.. _term-assembler: +.. _term-linker: +.. _term-object-file: + 1. **编译器** (Compiler) 将每个源文件从某门高级编程语言转化为汇编语言,注意此时源文件仍然是一个 ASCII 或其他编码的文本文件; 2. **汇编器** (Assembler) 将上一步的每个源文件中的文本格式的指令转化为机器码,得到一个二进制的 **目标文件** (Object File); 3. **链接器** (Linker) 将上一步得到的所有目标文件以及一些可能的外部目标文件链接在一起形成一个完整的可执行文件。 @@ -272,174 +316,12 @@ RISC-V 架构中,栈是从高地址到低地址增长的。在一个函数中 都有一个符号表,里面记录着它需要从其他文件中寻找的外部符号和能够提供给其他文件的符号,通常是一些函数和全局变量等。在链接的时候汇编器会将 外部符号替换为实际的地址。 -我们可以通过 **链接脚本** (Linker Script) 调整链接器的行为,使得最终生成的可执行文件的内存布局符合我们的预期。 - -实现 ----------- - -我们自己编写运行时初始化的代码: - -.. code-block:: asm - :linenos: - - # os/src/entry.asm - .section .text.entry - .globl _start - _start: - la sp, boot_stack_top - call rust_main - - .section .bss.stack - .globl boot_stack - boot_stack: - .space 4096 * 16 - .globl boot_stack_top - boot_stack_top: - -在这段汇编代码中,我们从第 8 行开始预留了一块大小为 4096 * 16 字节也就是 :math:`64\text{KiB}` 的空间用作接下来要运行的程序的栈空间, -这块栈空间的栈顶地址被全局符号 ``boot_stack_top`` 标识,栈底则被全局符号 ``boot_stack`` 标识。同时,这块栈空间单独作为一个名为 -``.bss.stack`` 的段,之后我们会通过链接脚本来安排它的位置。 - -从第 2 行开始,我们通过汇编代码实现执行环境的初始化,它其实只有两条指令:第一条指令将 sp 设置为我们预留的栈空间的栈顶位置,于是之后在函数 -调用的时候,栈就可以从这里开始向低地址增长了。简单起见,我们目前暂时不考虑 sp 越过了栈底 ``boot_stack`` ,也就是栈溢出的情形,虽然这有 -可能导致严重的错误。第二条指令则是通过伪指令 ``call`` 函数调用 ``rust_main`` ,这里的 ``rust_main`` 是一个我们稍后自己编写的应用 -入口。因此初始化任务非常简单:正如上面所说的一样,只需要设置栈指针 sp,随后跳转到应用入口即可。这两条指令单独作为一个名为 -``.text.entry`` 的段,且全局符号 ``_start`` 给出了段内第一条指令的地址。 - -接着,我们在 ``main.rs`` 中嵌入这些汇编代码并声明应用入口 ``rust_main`` : -.. code-block:: rust - :linenos: - :emphasize-lines: 4,8,10,11,12,13 - - // os/src/main.rs - #![no_std] - #![no_main] - #![feature(global_asm)] - - mod lang_items; - - global_asm!(include_str!("entry.asm")); - - #[no_mangle] - pub fn rust_main() -> ! { - loop {} - } - -背景高亮指出了 ``main.rs`` 中新增的代码。 - -第 4 行中,我们手动设置 ``global_asm`` 特性来支持在 Rust 代码中嵌入全局汇编代码。第 8 行,我们首先通过 -``include_str!`` 宏将同目录下的汇编代码 ``entry.asm`` 转化为字符串并通过 ``global_asm!`` 宏嵌入到代码中。 - -从第 10 行开始, -我们声明了应用的入口点 ``rust_main`` ,这里需要注意的是需要通过宏将 ``rust_main`` 标记为 ``#[no_mangle]`` 以避免编译器对它的 -名字进行混淆,不然的话在链接的时候, ``entry.asm`` 将找不到 ``main.rs`` 提供的外部符号 ``rust_main`` 从而导致链接失败。 - -我们修改 Cargo 的配置文件来使用我们自己的链接脚本 ``os/src/linker.ld`` 而非使用默认的内存布局: - -.. code-block:: - :linenos: - :emphasize-lines: 5,6,7,8 - - // os/.cargo/config - [build] - target = "riscv64gc-unknown-none-elf" - - [target.riscv64gc-unknown-none-elf] - rustflags = [ - "-Clink-arg=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes" - ] - -具体的链接脚本 ``os/src/linker.ld`` 如下: - -.. code-block:: - :linenos: - - OUTPUT_ARCH(riscv) - ENTRY(_start) - BASE_ADDRESS = 0x80020000; - - SECTIONS - { - . = BASE_ADDRESS; - skernel = .; - - stext = .; - .text : { - *(.text.entry) - *(.text .text.*) - } - - . = ALIGN(4K); - etext = .; - srodata = .; - .rodata : { - *(.rodata .rodata.*) - } - - . = ALIGN(4K); - erodata = .; - sdata = .; - .data : { - *(.data .data.*) - } - - . = ALIGN(4K); - edata = .; - .bss : { - *(.bss.stack) - sbss = .; - *(.bss .bss.*) - } - - . = ALIGN(4K); - ebss = .; - ekernel = .; - - /DISCARD/ : { - *(.eh_frame) - } - } - -第 1 行我们设置了目标平台为 riscv ;第 2 行我们设置了整个程序的入口点为之前定义的全局符号 ``_start``; -第 3 行定义了一个常量 ``BASE_ADDRESS`` 为 ``0x80020000`` ,也就是我们之前提到的期望我们自己实现的初始化代码被放在的地址; - -从第 5 行开始体现了链接过程中对输入的目标文件的段的合并。其中 ``.`` 表示当前地址,也就是链接器会从它指向的位置开始往下放置从输入的目标文件 -中收集来的段。我们可以对 ``.`` 进行赋值来调整接下来的段放在哪里,也可以创建一些全局符号赋值为 ``.`` 从而记录这一时刻的位置。我们还能够 -看到这样的格式: - -.. code-block:: - - .rodata : { - *(.rodata) - } - -冒号前面表示最终生成的可执行文件的一个段的名字,花括号内按照放置顺序描述将所有输入目标文件的哪些段放在这个段中,每一行格式为 -``(SectionName)``,表示目标文件 ``ObjectFile`` 的名为 ``SectionName`` 的段需要被放进去。我们也可以 -使用通配符来书写 ```` 和 ```` 分别表示可能的输入目标文件和段名。因此,最终的合并结果是,在最终可执行文件 -中各个常见的段 ``.text, .rodata .data, .bss`` 从低地址到高地址按顺序放置,每个段里面都包括了所有输入目标文件的同名段, -且每个段都有两个全局符号给出了它的开始和结束地址(比如 ``.text`` 段的开始和结束地址分别是 ``stext`` 和 ``etext`` )。 - -为了说明当前实现的正确性,我们需要讨论这样两个问题: - -1. 如何做到执行环境的初始化代码被放在内存上以 ``0x80020000`` 开头的区域上? - - 在链接脚本第 7 行,我们将当前地址设置为 ``BASE_ADDRESS`` 也即 ``0x80020000`` ,然后从这里开始往高地址放置各个段。第一个被放置的 - 是 ``.text`` ,而里面第一个被放置的又是来自 ``entry.asm`` 中的段 ``.text.entry``,这个段恰恰是含有两条指令的执行环境初始化代码, - 它在所有段中最早被放置在我们期望的 ``0x80020000`` 处。 - -2. 应用函数调用所需的栈放在哪里? - - 从链接脚本第 32 行开始,我们可以看出 ``entry.asm`` 中分配的栈空间对应的段 ``.bss.stack`` 被放入到可执行文件中的 - ``.bss`` 段中的低地址中。在后面虽然有一个通配符 ``.bss.*`` ,但是由于链接脚本的优先匹配规则它并不会被匹配到后面去。 - 这里需要注意的是地址区间 :math:`[\text{sbss},\text{ebss})` 并不包括栈空间,其原因后面再进行说明。 +.. note:: -这样一来,我们就将运行时重建完毕了。在 ``os`` 目录下 ``cargo build --release`` 或者直接 ``make build`` 就能够看到 -最终生成的可执行文件 ``target/riscv64gc-unknown-none-elf/release/os`` 。 + 本节内容部分参考自: -参考文献 ----------------- -- `RISC-V C 语言调用规范 `_ -- `Notes from Cornell CS3410 2019Spring `_ -- `Lecture from Berkeley CS61C 2018Spring `_ -- `Lecture from MIT 6.828 2020 `_ + - `RISC-V C 语言调用规范 `_ + - `Notes from Cornell CS3410 2019Spring `_ + - `Lecture from Berkeley CS61C 2018Spring `_ + - `Lecture from MIT 6.828 2020 `_ diff --git a/source/chapter1/5exercise.rst b/source/chapter1/5exercise.rst new file mode 100644 index 0000000000000000000000000000000000000000..38f297fac3fa3218451b82ca4f829d44278ca806 --- /dev/null +++ b/source/chapter1/5exercise.rst @@ -0,0 +1,153 @@ +chapter1练习 +===================================================== + +.. toctree:: + :hidden: + :maxdepth: 4 + +- 本节难度: **低** + +编程作业 +------------------------------- + +彩色化 LOG ++++++++++++++++++++++++++++++++ + +lab1 的工作使得我们从硬件世界跳入了软件世界,当看到自己的小 os 可以在裸机硬件上输出 ``hello world`` 是不是很高兴呢?但是为了后续的一步开发,更好的调试环境也是必不可少的,第一章的练习要求大家实现更加炫酷的彩色log。 + +详细的原理不多说,感兴趣的同学可以参考 `ANSI转义序列 `_ ,现在执行如下这条命令试试 + +.. code-block:: console + + $ echo -e "\x1b[31mhello world\x1b[0m" + +如果你明白了我们是如何利用串口实现输出,那么要实现彩色输出就十分容易了,只需要用需要输出的字符串替换上一条命令中的 ``hello world``,用期望颜色替换 ``31(代表红色)`` 即可。 + +.. warning:: + + 以下内容仅为推荐实现,不是练习要求,有时间和兴趣的同学可以尝试。 + +我们推荐实现如下几个等级的输出,输出优先级依次降低: + +.. list-table:: log 等级推荐 + :header-rows: 1 + :align: center + + * - 名称 + - 颜色 + - 用途 + * - ERROR + - 红色(31) + - 表示发生严重错误,很可能或者已经导致程序崩溃 + * - WARN + - 黄色(93) + - 表示发生不常见情况,但是并不一定导致系统错误 + * - INFO + - 蓝色(34) + - 比较中庸的选项,输出比较重要的信息,比较常用 + * - DEBUG + - 绿色(32) + - 输出信息较多,在 debug 时使用 + * - TRACE + - 灰色(90) + - 最详细的输出,跟踪了每一步关键路径的执行 + +我们可以输出比设定输出等级以及更高输出等级的信息,如设置 ``LOG = INFO``,则输出 ``ERROR``、``WARN``、``INFO`` 等级的信息。简单 demo 如下,输出等级为 INFO: + +.. image:: color-demo.png + +为了方便使用彩色输出,我们要求同学们实现彩色输出的宏或者函数,用以代替 print 完成输出内核信息的功能,它们有着和 prinf 十分相似的使用格式,要求支持可变参数解析,形如: + +.. code-block:: rust + + // 这段代码输出了 os 内存空间布局,这到这些信息对于编写 os 十分重要 +   + info!(".text [{:#x}, {:#x})", s_text as usize, e_text as usize); + debug!(".rodata [{:#x}, {:#x})", s_rodata as usize, e_rodata as usize); + error!(".data [{:#x}, {:#x})", s_data as usize, e_data as usize); + +.. code-block:: c + + info("load range : [%d, %d] start = %d\n", s, e, start); + +在以后,我们还可以在 log 信息中增加线程、CPU等信息(只是一个推荐,不做要求),这些信息将极大的方便你的代码调试。 + + +实验要求 ++++++++++++++++++++++++++++++++ + +- 实现分支:ch1。 +- 完成实验指导书中的内容,在裸机上实现 ``hello world`` 输出。 +- 实现彩色输出宏(只要求可以彩色输出,不要求 log 等级控制,不要求多种颜色)。 +- 隐形要求:可以关闭内核所有输出。从 lab2 开始要求关闭内核所有输出(如果实现了 log 等级控制,那么这一点自然就实现了)。 +- 利用彩色输出宏输出 os 内存空间布局,即:输出 ``.text``、``.data``、``.rodata``、``.bss`` 各段位置,输出等级为 ``INFO``。 + +challenge: 支持多核,实现多个核的 boot。 + +实验检查 ++++++++++++++++++++++++++++++++ + +- 实验目录要求(Rust) + +.. code-block:: + + ├── os(内核实现) + │   ├── Cargo.toml(配置文件) + │   ├── Makefile (要求 make run LOG=xxx 可以正确执行,可以不实现对 LOG 这一属性的支持,设置默认输出等级为 INFO) + │   └── src(所有内核的源代码放在 os/src 目录下) + │   ├── main.rs(内核主函数) + │   └── ... + ├── reports + │   ├── lab1.md/pdf + │   └── ... + ├── README.md(其他必要的说明) + ├── ... + +报告命名 labx.md/pdf,统一放在 reports 目录下。每个实验新增一个报告,为了方便修改,检查报告是以最新分支的所有报告为准。 + +- 检查 + +.. code-block:: console + + $ cd os + $ git checkout ch1 + $ make run LOG=INFO + +可以正确执行(可以不支持LOG参数,只有要彩色输出就好),可以看到正确的内存布局输出,根据实现不同数值可能有差异,但应该位于 ``linker.ld`` 中指示 ``BASE_ADDRESS`` 后一段内存,输出之后关机。 + +tips ++++++++++++++++++++++++++++++++ + +- 对于 Rust, 可以使用 crate `log `_ ,推荐参考 `rCore `_ +- 对于 C,可以实现不同的函数(注意不推荐多层可变参数解析,有时会出现不稳定情况),也可以参考 `linux printk `_ 使用宏实现代码重用。 +- 两种语言都可以使用 ``extern`` 关键字获得在其他文件中定义的符号。 + +问答作业 +------------------------------- + +1. 为了方便 os 处理,M态软件会将 S 态异常/中断委托给 S 态软件,请指出有哪些寄存器记录了委托信息,rustsbi 委托了哪些异常/中断?(也可以直接给出寄存器的值) + +2. 请学习 gdb 调试工具的使用(这对后续调试很重要),并通过 gdb 简单跟踪从机器加电到跳转到 0x80200000 的简单过程。只需要描述重要的跳转即可,只需要描述在 qemu 上的情况。 + +3. tips: + + - 事实上进入 rustsbi 之后就不需要使用 gdb 调试了。可以直接阅读代码。`rustsbi起始代码 `_ 。 + - 可以使用示例代码 Makefile 中的 ``make debug`` 指令。 + - 一些可能用到的 gdb 指令: + - ``x/10i 0x80000000`` : 显示 0x80000000 处的10条汇编指令。 + - ``x/10i $pc`` : 显示即将执行的10条汇编指令。 + - ``x/10xw 0x80000000`` : 显示 0x80000000 处的10条数据,格式为16进制32bit。 + - ``info register``: 显示当前所有寄存器信息。 + - ``info r t0``: 显示 t0 寄存器的值。 + - ``break funcname``: 在目标函数第一条指令处设置断点。 + - ``break *0x80200000``: 在 0x80200000 出设置断点。 + - ``continue``: 执行直到碰到断点。 + - ``si``: 单步执行一条汇编指令。 + +报告要求 +------------------------------- + +- 简单总结本次实验你编程的内容。(控制在5行以内,不要贴代码) +- 由于彩色输出不好自动测试,请附正确运行后的截图。 +- 完成问答问题。 +- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 diff --git a/source/chapter1/5sbi-print.rst b/source/chapter1/5sbi-print.rst deleted file mode 100644 index aba658eb0cd5622688fb070a74ba6c0bf2646720..0000000000000000000000000000000000000000 --- a/source/chapter1/5sbi-print.rst +++ /dev/null @@ -1,111 +0,0 @@ -格式化输出 -===================== - -.. toctree:: - :hidden: - :maxdepth: 5 - -这一小节我们来自己实现 ``println!`` 的功能。 我们这里只是给出一些函数之间的调用关系,而不在这里进行一些实现细节上的展开。有兴趣的读者 -可以自行参考代码提供的注释。 - -在屏幕上打印一个字符是最基础的功能,它已经由 bootloader (也就是放在 ``bootloader`` 目录下的预编译版本)提供,具体的调用方法可以参考 -``sbi.rs`` 中的 ``console_putchar`` 函数。 - -随后我们在 ``console.rs`` 中利用 ``console_putchar`` 来实现 ``print!`` 和 ``println!`` 两个宏。有兴趣的读者可以去代码注释中 -参考有关 Rust ``core::fmt`` 库和宏编写的相关知识。在 ``main.rs`` 声明子模块 ``mod console`` 的时候加上 ``#[macro_use]`` 来让 -整个引用都可以使用到该模块里面定义的宏。 - -接着我们在 ``lang_items.rs`` 中修改 panic 时的行为: - -.. code-block:: rust - - // os/src/lang_items.rs - use crate::sbi::shutdown; - - #[panic_handler] - fn panic(info: &PanicInfo) -> ! { - if let Some(location) = info.location() { - println!("Panicked at {}:{} {}", location.file(), location.line(), info.message().unwrap()); - } else { - println!("Panicked: {}", info.message().unwrap()); - } - shutdown() - } - -我们尝试从传入的 ``PanicInfo`` 中解析 panic 发生的文件和行数。如果解析成功的话,就和 panic 的报错信息一起打印出来。我们需要在 -``main.rs`` 开头加上 ``#![feature(panic_info_message)]`` 才能通过 ``PanicInfo::message`` 获取报错信息。 - -.. note:: - - **Rust 小知识: 错误处理** - - Rust 中常利用 ``Option`` 和 ``Result`` 进行方便的错误处理。它们都属于枚举结构: - - - ``Option`` 既可以有值 ``Option::Some`` ,也有可能没有值 ``Option::None``; - - ``Result`` 既可以保存某个操作的返回值 ``Result::Ok`` ,也可以表明操作过程中出现了错误 ``Result::Err`` 。 - - 我们可以使用 ``Option/Result`` 来保存一个不能确定存在/不存在或是成功/失败的值。之后可以通过匹配 ``if let`` 或是在能够确定 - 的场合直接通过 ``unwrap`` 将里面的值取出。详细的内容可以参考 Rust 官方文档。 - - -此外,我们还使用 bootloader 中提供的另一个接口 ``shutdown`` 关闭机器。 - -最终我们的应用程序 ``rust_main`` 如下: - -.. code-block:: rust - - // os/src/main.rs - - #[no_mangle] - pub fn rust_main() -> ! { - extern "C" { - fn stext(); - fn etext(); - fn srodata(); - fn erodata(); - fn sdata(); - fn edata(); - fn sbss(); - fn ebss(); - fn boot_stack(); - fn boot_stack_top(); - }; - clear_bss(); - println!("Hello, world!"); - println!(".text [{:#x}, {:#x})", stext as usize, etext as usize); - println!(".rodata [{:#x}, {:#x})", srodata as usize, erodata as usize); - println!(".data [{:#x}, {:#x})", sdata as usize, edata as usize); - println!("boot_stack [{:#x}, {:#x})", boot_stack as usize, boot_stack_top as usize); - println!(".bss [{:#x}, {:#x})", sbss as usize, ebss as usize); - panic!("Shutdown machine!"); - } - -当我们在 qemu 平台上运行的时候能够看到如下的运行结果: - -.. code-block:: - :linenos: - - [rustsbi] Version 0.1.0 - .______ __ __ _______.___________. _______..______ __ - | _ \ | | | | / | | / || _ \ | | - | |_) | | | | | | (----`---| |----`| (----`| |_) || | - | / | | | | \ \ | | \ \ | _ < | | - | |\ \----.| `--' |.----) | | | .----) | | |_) || | - | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| - - [rustsbi] Platform: QEMU - [rustsbi] misa: RV64ACDFIMSU - [rustsbi] mideleg: 0x222 - [rustsbi] medeleg: 0xb109 - [rustsbi] Kernel entry: 0x80020000 - Hello, world! - .text [0x80020000, 0x80022000) - .rodata [0x80022000, 0x80023000) - .data [0x80023000, 0x80023000) - boot_stack [0x80023000, 0x80033000) - .bss [0x80033000, 0x80033000) - Panicked at src/main.rs:46 Shutdown machine! - - -其中前 13 行是 bootloader 的输出,剩下的部分是我们的应用程序的输出,打印了 ``Hello, world!``,输出了程序内部各个段的地址区间, -还展示了 panic 相关信息。 \ No newline at end of file diff --git a/source/chapter1/6practice.rst b/source/chapter1/6practice.rst deleted file mode 100644 index c2e71c4bd6d487d75f41ad48aaffec478b74cd86..0000000000000000000000000000000000000000 --- a/source/chapter1/6practice.rst +++ /dev/null @@ -1,17 +0,0 @@ -练习一 -============== - -.. toctree:: - :hidden: - :maxdepth: 5 - -编程练习一:backtrace ---------------------------------------------- - -仔细阅读 :ref:`函数调用与栈 ` 小节的内容,特别是函数栈帧中的 ra 寄存器和 prev fp 的位置以及它们的作用。 - -编程实现:在 ``rust_main`` 中多层嵌套调用函数,然后在最深层按照层数由深到浅打印函数调用链,也就是每一层函数栈帧中保存的 ra 寄存器的值, -由此我们可以依次知道每个函数的调用语句所在的地址,也就能跟踪一整条函数调用链。 - -拓展:寻找/改写 Rust 库或者自己实现,能够通过 ra 寄存器的值得到其所在的源文件/函数/行数,从而更直观的看到函数调用链。或者也可以通过 -addr2line 工具在运行结束之后手动去可执行文件中查找每个地址对应的信息。 \ No newline at end of file diff --git a/source/chapter1/ch1.py b/source/chapter1/ch1.py new file mode 100644 index 0000000000000000000000000000000000000000..4cf8c7b617467939693d6c90c389e805d561a31f --- /dev/null +++ b/source/chapter1/ch1.py @@ -0,0 +1,230 @@ +from manimlib.imports import * + +class Test(Scene): + CONFIG = { + "camera_config": { + "background_color": WHITE, + }, + } + def construct(self): + left_line = Line(start = np.array([-1, -4, 0]), end = np.array([-1, 4, 0])) + left_line.set_color(BLACK) + self.add(left_line) + right_line = Line(start = np.array([1, -4, 0]), end = np.array([1, 4, 0])) + right_line.set_color(BLACK) + self.add(right_line) + STACKFRAME_HEIGHT = 1.0 + STACKFRAME_WIDTH = 2.0 + for i in range(0, 4): + stack_frame = Rectangle(height=1.0, width=2.0, stroke_color=BLACK, stroke_width=5, stroke_opacity=0.1) + stack_frame.set_y(i * STACKFRAME_HEIGHT - 1) + self.add(stack_frame) + left_text = TextMobject("sp + %d" % (8*i,)) + left_text.next_to(stack_frame, LEFT) + left_text.set_color(BLACK) + self.add(left_text) + + high_address = TextMobject("High Address", color=BLUE) + #high_address.to_corner(UL, buff=0.5) + high_address.to_edge(TOP, buff=0).shift(RIGHT*(STACKFRAME_WIDTH+1)) + self.add(high_address) + self.add(DashedLine(color=BLACK).next_to(high_address, LEFT, buff=0)) + low_address = TextMobject("Low Address", color=BLUE) + low_address.to_edge(BOTTOM, buff=0).shift(RIGHT*(STACKFRAME_WIDTH+1)) + self.add(low_address) + self.add(DashedLine(color=BLACK).next_to(low_address, LEFT, buff=0)) + +class CallStack(Scene): + CONFIG = { + "camera_config": { + "background_color": WHITE, + }, + } + def construct(self): + # constants + BLOCK_WIDTH = 1 + BLOCK_TOP = 2 + HORIZONTAL_GAP = 1 + ADDR_ARROW_LENGTH = 1.5 + + left_line = Line(np.array([-0.5, -3, 0]), np.array([-0.5, 3, 0]), color=BLACK) + right_line = Line(np.array([0.5, -3, 0]), np.array([0.5, 3, 0]), color=BLACK) + + high_addr = TextMobject("High Address", color=BLACK).scale(0.7).move_to(np.array([5, 3, 0]), LEFT) + low_addr = TextMobject("Low Address", color=BLACK).scale(0.7).move_to(np.array([5, -3, 0]), LEFT) + addr_arrow = Arrow(color=BLACK, stroke_width=3) \ + .rotate(angle=90 * DEGREES, axis=IN).move_to(np.array([5.5, 0, 0])) \ + .scale(3.5) + addr_arrow.add( + TextMobject("grow", color=BLACK) + .next_to(addr_arrow.get_center(), RIGHT, buff=0.1) + .scale(0.7) + ) + self.add(high_addr, low_addr, addr_arrow) + + stack_frame_a = Rectangle(width=BLOCK_WIDTH, height=1.5, stroke_color=BLACK) + stack_frame_a.set_fill(color=BLUE, opacity=0.8) + stack_frame_a.add(TextMobject("a", color=BLACK)) + stack_frame_a.shift(UP*BLOCK_TOP) + stack_frame_b = Rectangle(width=BLOCK_WIDTH, height=1, stroke_color=BLACK) + stack_frame_b.set_fill(color=RED, opacity=0.8) + stack_frame_b.add(TextMobject("b", color=BLACK)) + stack_frame_b.next_to(stack_frame_a, DOWN, buff=0) + stack_frame_c = Rectangle(width=BLOCK_WIDTH, height=2, stroke_color=BLACK) + stack_frame_c.set_fill(color=GREEN, opacity=0.8) + stack_frame_c.add(TextMobject("c", color=BLACK)) + stack_frame_c.next_to(stack_frame_b, DOWN, buff=0) + vgroup_a = VGroup(left_line, right_line, stack_frame_a) + vgroup_ab = vgroup_a.deepcopy().add(stack_frame_b) + vgroup_abc = vgroup_ab.deepcopy().add(stack_frame_c) + horizontal_group = [vgroup_a, vgroup_ab, vgroup_abc, vgroup_ab.deepcopy(), vgroup_a.deepcopy()] + labels = [ + "Call a", + "Call b", + "Call c", + "c returned", + "b returned", + ] + for i in range(0, 5): + # 0->2, 1->3, 2->4, 3->3, 4->2 + m = {0: 2, 1: 3, 2: 4, 3: 3, 4: 2} + arrow = Arrow(color=BLACK).scale(0.25) + arrow.next_to(horizontal_group[i][m[i]].get_corner(DL), LEFT, buff=0) + arrow.add(TextMobject("sp", color=BLACK).next_to(arrow.get_left(), LEFT, buff=0).scale(0.7)) + label = TextMobject(labels[i], color=BLACK).scale(0.7).to_edge(TOP, buff=0.1) + horizontal_group[i].add(arrow, label) + + horizontal_group[i].shift((i - 2) * (BLOCK_WIDTH + HORIZONTAL_GAP) * RIGHT) + self.add(horizontal_group[i]) + +class StackFrame(Scene): + CONFIG = { + "camera_config": { + "background_color": WHITE, + }, + } + def construct(self): + # constants + STACK_HEIGHT_HALF = 3.5 + left_line = Line(np.array([-1, -STACK_HEIGHT_HALF, 0]), np.array([-1, STACK_HEIGHT_HALF, 0]), color=BLACK) + right_line = Line(np.array([1, -STACK_HEIGHT_HALF, 0]), np.array([1, STACK_HEIGHT_HALF, 0]), color=BLACK) + self.add(left_line, right_line) + father_stack_frame = Rectangle(width=2, height=1.5, fill_color=BLUE, fill_opacity=1.0).set_y(2.3) + father_stack_frame.set_stroke(color=BLACK) + + father_stack_frame.add(TextMobject("Father", color=BLACK).scale(0.5).next_to(father_stack_frame.get_center(), UP, buff=0.1)) + father_stack_frame.add(TextMobject("StackFrame", color=BLACK).scale(0.5)\ + .next_to(father_stack_frame[1], DOWN, buff=0.2)) + + ra = Rectangle(width=2, height=0.7, fill_color=YELLOW_E, fill_opacity=1.0).next_to(father_stack_frame, DOWN, buff=0) + ra.set_stroke(color=BLACK) + ra.add(TextMobject("ra", color=BLACK).scale(0.5).move_to(ra)) + + fp = Rectangle(width=2, height=0.7, fill_color=TEAL_E, fill_opacity=1.0).next_to(ra, DOWN, buff=0) + fp.set_stroke(color=BLACK) + fp.add(TextMobject("prev fp", color=BLACK).scale(0.5).move_to(fp)) + + callee_saved = Rectangle(width=2, height=1.3, fill_color=ORANGE, fill_opacity=1.0).next_to(fp, DOWN, buff=0) + callee_saved.set_stroke(color=BLACK) + callee_saved.add(TextMobject("Callee-saved", color=BLACK).scale(0.5).move_to(callee_saved)) + + local_var = Rectangle(width=2, height=1.6, fill_color=MAROON_E, fill_opacity=0.7).next_to(callee_saved, DOWN, buff=0) + local_var.set_stroke(color=BLACK) + local_var.add(TextMobject("Local Variables", color=BLACK).scale(0.5).move_to(local_var)) + + current_sp = Arrow(color=BLACK).next_to(local_var.get_corner(DL), LEFT, buff=0).scale(0.25, about_edge=RIGHT) + current_sp.add(TextMobject("sp", color=BLACK).scale(0.5).next_to(current_sp.get_left(), LEFT, buff=0.1)) + + current_fp = Arrow(color=BLACK).next_to(father_stack_frame.get_corner(DL), LEFT, buff=0).scale(.25, about_edge=RIGHT) + current_fp.add(TextMobject("fp", color=BLACK).scale(0.5).next_to(current_fp.get_left(), LEFT, buff=0.1)) + + upper_bound = Arrow(color=BLACK)\ + .rotate(90*DEGREES, IN)\ + .next_to(ra.get_corner(UR), DOWN, buff=0)\ + .shift(0.3*RIGHT)\ + .scale(1.2, about_edge=UP)\ + .set_stroke(width=3) + upper_bound.tip.scale(0.4, about_edge=UP) + lower_bound = Arrow(color=BLACK)\ + .rotate(90*DEGREES, OUT)\ + .next_to(local_var.get_corner(DR), UP, buff=0)\ + .shift(0.3*RIGHT)\ + .scale(1.2, about_edge=DOWN)\ + .set_stroke(width=3) + lower_bound.tip.scale(0.4, about_edge=DOWN) + current_stack_frame = TextMobject("Current StackFrame", color=BLACK).scale(0.5)\ + .next_to((upper_bound.get_center()+lower_bound.get_center())*.5, RIGHT, buff=0.1) + upper_dash = DashedLine(color=BLACK).next_to(ra.get_corner(UR), RIGHT, buff=0)\ + .scale(0.7, about_edge=LEFT) + lower_dash = DashedLine(color=BLACK).next_to(local_var.get_corner(DR), RIGHT, buff=0)\ + .scale(0.7, about_edge=LEFT) + + prev_fp_p1 = DashedLine(fp[1].get_right() + np.array([0.1, 0, 0]), fp[1].get_right() + np.array([1.2, 0, 0]), color=RED) + delta_y = father_stack_frame.get_top()[1] - prev_fp_p1.get_right()[1] + prev_fp_p2 = DashedLine(prev_fp_p1.get_right(), prev_fp_p1.get_right()+delta_y*UP, color=RED) + prev_fp_p3 = DashedLine(prev_fp_p2.get_end(), father_stack_frame.get_corner(UR), color=RED) + prev_fp_p3.add_tip(0.1) + + self.add(father_stack_frame, ra, fp, callee_saved, local_var, current_sp, current_fp) + self.add(upper_bound, lower_bound, current_stack_frame, upper_dash, lower_dash) + self.add(prev_fp_p1, prev_fp_p2, prev_fp_p3) + +class MemoryLayout(Scene): + CONFIG = { + "camera_config": { + "background_color": WHITE, + }, + } + def construct(self): + # constants + STACK_HEIGHT_HALF = 4 + left_line = Line(np.array([-1, -STACK_HEIGHT_HALF, 0]), np.array([-1, STACK_HEIGHT_HALF, 0]), color=BLACK) + right_line = Line(np.array([1, -STACK_HEIGHT_HALF, 0]), np.array([1, STACK_HEIGHT_HALF, 0]), color=BLACK) + self.add(left_line, right_line) + + text = Rectangle(width=2, height=1.5, stroke_color=BLACK).set_y(-3) + #text.set_fill(color=GREEN, opacity=1.0) + text.add(TextMobject(".text", color=BLACK).scale(0.7).move_to(text)) + rodata = Rectangle(width=2, height=.75, stroke_color=BLACK).next_to(text, UP, buff=0) + rodata.add(TextMobject(".rodata", color=BLACK).scale(.7).move_to(rodata)) + data = Rectangle(width=2, height=.75, stroke_color=BLACK).next_to(rodata, UP, buff=0) + #data.set_fill(color=TEAL_E, opacity=1.0) + data.add(TextMobject(".data", color=BLACK).scale(0.7).move_to(data)) + bss = Rectangle(width=2, height=.75, stroke_color=BLACK).next_to(data, UP, buff=0) + #bss.set_fill(color=MAROON_E, opacity=1.0) + bss.add(TextMobject(".bss", color=BLACK).scale(0.7).move_to(bss)) + heap = Rectangle(width=2, height=1, stroke_color=BLACK).next_to(bss, UP, buff=0) + #heap.set_fill(color=GRAY, opacity=1.0) + heap.add(TextMobject("heap", color=BLACK).scale(0.7).move_to(heap)) + stack = Rectangle(width=2, height=1, stroke_color=BLACK).set_y(3) + #stack.set_fill(color=BLUE_E, opacity=0.8) + stack.add(TextMobject("stack", color=BLACK).scale(0.7).move_to(stack)) + stack_down = Arrow(color=BLACK).rotate(90*DEGREES, IN).next_to(stack, DOWN, buff=0)\ + .scale(0.35, about_edge=UP) + stack_down.tip.scale(0.5, about_edge=UP) + heap_up = Arrow(color=BLACK).rotate(90*DEGREES, OUT).next_to(heap, UP, buff=0)\ + .scale(0.35, about_edge=DOWN) + heap_up.tip.scale(0.5, about_edge=DOWN) + low_addr = TextMobject("Low Address", color=BLACK).to_edge(BOTTOM, buff=0.05).shift(3*RIGHT) + high_addr = TextMobject("High Address", color=BLACK).to_edge(TOP, buff=.05).shift(3*RIGHT) + + division = DashedLine(color=BLACK).next_to(text.get_corner(UL), LEFT, buff=0).scale(1.5) + data_mem_division = DashedLine(color=BLACK).next_to(stack.get_corner(UL), LEFT, buff=0).scale(1.5) + code_mem_division = DashedLine(color=BLACK).next_to(text.get_corner(DL), LEFT, buff=0).scale(1.5) + + data_mem = TextMobject("Data Memory", color=BLACK).move_to((division.get_center()+data_mem_division.get_center())*.5)\ + .scale(.8)\ + .shift(LEFT*.3) + code_mem = TextMobject("Code Memory", color=BLACK).move_to((division.get_center()+code_mem_division.get_center())*.5)\ + .scale(.8)\ + .shift(LEFT*.3) + + self.add(text, rodata, data, bss, heap, stack) + self.add(stack_down, heap_up) + self.add(low_addr, high_addr) + self.add(division, data_mem_division, code_mem_division) + self.add(data_mem, code_mem) + + + + diff --git a/source/chapter1/color-demo.png b/source/chapter1/color-demo.png new file mode 100644 index 0000000000000000000000000000000000000000..63b275883897c78d1cba1f949ac9b4d68d71ff0d Binary files /dev/null and b/source/chapter1/color-demo.png differ diff --git a/source/chapter1/function-call.png b/source/chapter1/function-call.png new file mode 100644 index 0000000000000000000000000000000000000000..3f45c749affac9a7acc08069230666a9b0185d4f Binary files /dev/null and b/source/chapter1/function-call.png differ diff --git a/source/chapter1/index.rst b/source/chapter1/index.rst index 82266a9344ca1dddf31edfd4e579284ca7c7b5d9..e89ad2aae8335d66ce33653e7792ae69bef51761 100644 --- a/source/chapter1/index.rst +++ b/source/chapter1/index.rst @@ -1,73 +1,17 @@ +.. _link-chapter1: + 第一章:RV64 裸机应用 ============================================== .. toctree:: - :hidden: :maxdepth: 4 + 0intro 1app-ee-platform 2remove-std - 3minimal-rt - 4load-manually - 5sbi-print - 6practice - -大多数程序员的第一行代码都从 ``Hello, world!`` 开始,当我们满怀着好奇心在编辑器内键入仅仅数个字节,再经过几行命令编译、运行,终于 -在黑洞洞的终端窗口中看到期望中的结果的时候,一扇通往编程世界的大门已经打开。时至今日,我们能够隐约意识到编程工作能够如此方便简洁并不是 -理所当然的,实际上有着多层硬件、软件隐藏在它背后,才让我们不必付出那么多努力就能够创造出功能强大的应用程序。 - -本章我们的目标仍然只是输出 ``Hello, world!`` ,但这一次,我们将离开舒适区,基于一个几乎空无一物的平台从零开始搭建我们自己的高楼大厦, -而不是仅仅通过一行语句就完成任务。 - -获取本章代码: - -.. code-block:: console - - $ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git - $ cd rCore-Tutorial-v3 - $ git checkout ch1 - -在 qemu 模拟器上运行本章代码: - -.. code-block:: console - - $ cd os - $ make run - -将 Maix 系列开发版连接到 PC,并在上面运行本章代码: - -.. code-block:: console - - $ cd os - $ make run BOARD=k210 - -.. warning:: - - **FIXME: 提供 wsl/macOS 等更多平台支持** - -如果顺利的话,以 qemu 平台为例,将输出: - -.. code-block:: - - [rustsbi] Version 0.1.0 - .______ __ __ _______.___________. _______..______ __ - | _ \ | | | | / | | / || _ \ | | - | |_) | | | | | | (----`---| |----`| (----`| |_) || | - | / | | | | \ \ | | \ \ | _ < | | - | |\ \----.| `--' |.----) | | | .----) | | |_) || | - | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| + 3-1-mini-rt-usrland + 3-2-mini-rt-baremetal + 4understand-prog + 5exercise - [rustsbi] Platform: QEMU - [rustsbi] misa: RV64ACDFIMSU - [rustsbi] mideleg: 0x222 - [rustsbi] medeleg: 0xb109 - [rustsbi] Kernel entry: 0x80020000 - Hello, world! - .text [0x80020000, 0x80022000) - .rodata [0x80022000, 0x80023000) - .data [0x80023000, 0x80023000) - boot_stack [0x80023000, 0x80033000) - .bss [0x80033000, 0x80033000) - Panicked at src/main.rs:46 Shutdown machine! -除了 ``Hello, world!`` 之外还有一些额外的信息,最后关机。 \ No newline at end of file diff --git a/source/chapter2/0intro.rst b/source/chapter2/0intro.rst new file mode 100644 index 0000000000000000000000000000000000000000..2e1a42510aaeb534f64b85ba3dc027c4d003be34 --- /dev/null +++ b/source/chapter2/0intro.rst @@ -0,0 +1,160 @@ +引言 +================================ + +本章导读 +--------------------------------- + +.. + chyyuu:有一个ascii图,画出我们做的OS。 + +本章展现了操作系统一系列功能: + +- 通过批处理支持多个程序的自动加载和运行 +- 操作系统利用硬件特权级机制,实现对操作系统自身的保护 + +上一章,我们在 RV64 裸机平台上成功运行起来了 ``Hello, world!`` 。看起来这个过程非常顺利,只需要一条命令就能全部完成。但实际上,在那个计算机刚刚诞生的年代,很多事情并不像我们想象的那么简单。 当时,程序被记录在打孔的卡片上,使用汇编语言甚至机器语言来编写。而稀缺且昂贵的计算机由专业的管理员负责操作,就和我们在上一章所做的事情一样,他们手动将卡片输入计算机,等待程序运行结束或者终止程序的运行。最后,他们从计算机的输出端——也就是打印机中取出程序的输出并交给正在休息室等待的程序提交者。 + +实际上,这样做是一种对于珍贵的计算资源的浪费。因为当时的计算机和今天的个人计算机不同,它的体积极其庞大,能够占满一整个空调房间,像巨大的史前生物。管理员在房间的各个地方跑来跑去、或是等待打印机的输出的这些时间段,计算机都并没有在工作。于是,人们希望计算机能够不间断的工作且专注于计算任务本身。 + +.. _term-batch-system: + +**批处理系统** (Batch System) 应运而生。它的核心思想是:将多个程序打包到一起输入计算机。而当一个程序运行结束后,计算机会 *自动* 加载下一个程序到内存并开始执行。这便是最早的真正意义上的操作系统。 + +.. _term-privilege: + +程序总是难免出现错误。但人们希望一个程序的错误不要影响到操作系统本身,它只需要终止出错的程序,转而运行执行序列中的下一个程序即可。如果后面的程序都无法运行就太糟糕了。这种 *保护* 操作系统不受有意或无意出错的程序破坏的机制被称为 **特权级** (Privilege) 机制,它实现了用户态和内核态的隔离,需要软件和硬件的共同努力。 + + +本章主要是设计和实现建立支持批处理系统的泥盆纪“邓式鱼”操作系统,从而对可支持运行一批应用程序的执行环境有一个全面和深入的理解。 + +本章我们的目标让泥盆纪“邓式鱼”操作系统能够感知多个应用程序的存在,并一个接一个地运行这些应用程序,当一个应用程序执行完毕后,会启动下一个应用程序,直到所有的应用程序都执行完毕。 + +.. image:: deng-fish.png + :align: center + :name: fish-os + +实践体验 +--------------------------- + +本章我们的批处理系统将连续运行三个应用程序,放在 ``user/src/bin`` 目录下。 + +获取本章代码: + +.. code-block:: console + + $ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git + $ cd rCore-Tutorial-v3 + $ git checkout ch2 + +在 qemu 模拟器上运行本章代码: + +.. code-block:: console + + $ cd os + $ make run + +将 Maix 系列开发板连接到 PC,并在上面运行本章代码: + +.. code-block:: console + + $ cd os + $ make run BOARD=k210 + +如果顺利的话,我们可以看到批处理系统自动加载并运行所有的程序并且正确在程序出错的情况下保护了自身: + +.. code-block:: + + [rustsbi] RustSBI version 0.1.1 + .______ __ __ _______.___________. _______..______ __ + | _ \ | | | | / | | / || _ \ | | + | |_) | | | | | | (----`---| |----`| (----`| |_) || | + | / | | | | \ \ | | \ \ | _ < | | + | |\ \----.| `--' |.----) | | | .----) | | |_) || | + | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| + + [rustsbi] Platform: QEMU (Version 0.1.0) + [rustsbi] misa: RV64ACDFIMSU + [rustsbi] mideleg: 0x222 + [rustsbi] medeleg: 0xb1ab + [rustsbi-dtb] Hart count: cluster0 with 1 cores + [rustsbi] Kernel entry: 0x80200000 + [kernel] Hello, world! + [kernel] num_app = 3 + [kernel] app_0 [0x8020b028, 0x8020c048) + [kernel] app_1 [0x8020c048, 0x8020d100) + [kernel] app_2 [0x8020d100, 0x8020e4b8) + [kernel] Loading app_0 + Hello, world! + [kernel] Application exited with code 0 + [kernel] Loading app_1 + Into Test store_fault, we will insert an invalid store operation... + Kernel should kill this application! + [kernel] PageFault in application, core dumped. + [kernel] Loading app_2 + 3^10000=5079 + 3^20000=8202 + 3^30000=8824 + 3^40000=5750 + 3^50000=3824 + 3^60000=8516 + 3^70000=2510 + 3^80000=9379 + 3^90000=2621 + 3^100000=2749 + Test power OK! + [kernel] Application exited with code 0 + [kernel] Panicked at src/batch.rs:61 All applications completed! + +本章代码树 +------------------------------------------------- + +.. code-block:: + + ├── bootloader + │   ├── rustsbi-k210.bin + │   └── rustsbi-qemu.bin + ├── LICENSE + ├── os + │   ├── build.rs(新增:生成 link_app.S 将应用作为一个数据段链接到内核) + │   ├── Cargo.toml + │   ├── Makefile(修改:构建内核之前先构建应用) + │   └── src + │   ├── batch.rs(新增:实现了一个简单的批处理系统) + │   ├── console.rs + │   ├── entry.asm + │   ├── lang_items.rs + │   ├── link_app.S(构建产物,由 os/build.rs 输出) + │   ├── linker-k210.ld + │   ├── linker-qemu.ld + │   ├── main.rs(修改:主函数中需要初始化 Trap 处理并加载和执行应用) + │   ├── sbi.rs + │   ├── syscall(新增:系统调用子模块 syscall) + │   │   ├── fs.rs(包含文件 I/O 相关的 syscall) + │   │   ├── mod.rs(提供 syscall 方法根据 syscall ID 进行分发处理) + │   │   └── process.rs(包含任务处理相关的 syscall) + │   └── trap(新增:Trap 相关子模块 trap) + │   ├── context.rs(包含 Trap 上下文 TrapContext) + │   ├── mod.rs(包含 Trap 处理入口 trap_handler) + │   └── trap.S(包含 Trap 上下文保存与恢复的汇编代码) + ├── README.md + ├── rust-toolchain + ├── tools + │   ├── kflash.py + │   ├── LICENSE + │   ├── package.json + │   ├── README.rst + │   └── setup.py + └── user(新增:应用测例保存在 user 目录下) + ├── Cargo.toml + ├── Makefile + └── src + ├── bin(基于用户库 user_lib 开发的应用,每个应用放在一个源文件中) + │   ├── 00hello_world.rs + │   ├── 01store_fault.rs + │   └── 02power.rs + ├── console.rs + ├── lang_items.rs + ├── lib.rs(用户库 user_lib) + ├── linker.ld(应用的链接脚本) + └── syscall.rs(包含 syscall 方法生成实际用于系统调用的汇编指令, + 各个具体的 syscall 都是通过 syscall 来实现的) \ No newline at end of file diff --git a/source/chapter2/1rv-privilege.rst b/source/chapter2/1rv-privilege.rst index 8c0c4d4186c371c894468dd134436a5cd0151061..22a5a0c40d461b2de75375209cfc9d2ea1b60df7 100644 --- a/source/chapter2/1rv-privilege.rst +++ b/source/chapter2/1rv-privilege.rst @@ -1,12 +1,43 @@ -RISC-V 特权级架构 +特权级机制 ===================================== .. toctree:: :hidden: :maxdepth: 5 -为了保护我们的批处理系统不受到出错应用程序的影响并全程稳定工作,单凭软件实现是很难做到的,而是需要 CPU 提供一种特权级隔离机制,使得它在执行 -应用程序和内核代码的时候处于不同的特权级。特权级可以看成 CPU 随时间变化而处于的不同的工作模式。 +本节导读 +------------------------------- + +为了保护我们的批处理操作系统不受到出错应用程序的影响并全程稳定工作,单凭软件实现是很难做到的,而是需要 CPU 提供一种特权级隔离机制,使CPU在执行应用程序和操作系统内核的指令时处于不同的特权级。本节主要介绍了特权级机制的软硬件设计思路,以及RISC-V的特权级架构,包括特权指令的描述。 + +特权级的软硬件协同设计 +------------------------------------------ + +实现特权级机制的根本原因是应用程序运行的安全性不可充分信任。在上一节里,操作系统和应用紧密连接在一起,形成一个应用程序来执行。随着应用需求的增加,操作系统也越来越大,会以库的形式存在;同时应用自身也会越来越复杂。由于操作系统会给多个应用提供服务,所以它可能的错误会比较快地被发现,但应用自身的错误可能就不会很快发现。由于二者通过编译器形成一个应用程序来执行,即使是应用本身的问题,也会导致操作系统受到连累,从而可能导致整个计算机系统都不可用了。 +所以,计算机专家就想到一个方法,能否让相对安全可靠的操作系统不受到应用程序的破坏,运行在一个安全的执行环境中,而让应用程序运行在一个无法破坏操作系统的执行环境中? + +为确保操作系统的安全,对应用程序而言,需要限制的主要有两个方面: +- 应用程序不能访问任意的地址空间(这个在第四章会进一步讲解,本章不会讲解) +- 应用程序不能执行某些可能破会计算机系统的指令(本章的重点) + +假设有了这样的限制,我们还需要确保应用程序能够得到操作系统的服务,即应用程序和操作系统还需要有交互的手段。使得低特权级软件都只能做高特权级软件允许它做的,且低特权级软件的超出其能力的要求必须寻求高特权级软件的帮助。在这里的高特权级软件就是低特权级软件的软件执行环境。 + +为了完成这样的特权级需求,需要进行软硬件协同设计。一个比较简洁的方法就是,处理器设置两个不同安全等级的执行环境:用户态特权级的执行环境和内核态特权级的执行环境。且明确指出可能破会计算机系统的内核态特权级指令子集,规定内核态特权级指令子集中的指令只能在内核态特权级的执行环境中执行,如果在用户态特权级的执行环境中执行这些指令,会产生异常。处理器在执行不同特权级的执行环境下的指令前进行特权级安全检查。 + +为了让应用程序获得操作系统的函数服务,采用传统的函数调用方式(即通常的 ``call`` 和 ``ret`` 指令或指令组合)将会直接绕过硬件的特权级保护检查。所以要设计新的指令:执行环境调用(Execution Environment Call,简称 ``ecall`` )和执行环境返回(Execution Environment Return,简称 ``eret`` )): + +- ``ecall`` :具有用户态到内核态的执行环境切换能力的函数调用指令(RISC-V中就有这条指令) +- ``eret`` :具有内核态到用户态的执行环境切换能力的函数返回指令(RISC-V中有类似的 ``sret`` 指令) + +但硬件具有了这样的机制后,还需要操作系统的配合才能最终完成对操作系统自己的保护。首先,操作系统需要提供相应的控制流,能在执行 ``eret`` 前准备和恢复用户态执行应用程序的上下文。其次,在应用程序调用 ``ecall`` 指令后,能够保存用户态执行应用程序的上下文,便于后续的恢复;且还要坚持应用程序发出的服务请求是安全的。 + +.. note:: + + 在实际的CPU,如x86、RISC-V等,设计了多达4种特权级。对于一般的操作系统而言,其实只要两种特权级就够了。 + + +RISC-V 特权级架构 +------------------------------------------ RISC-V 架构中一共定义了 4 种特权级: @@ -20,7 +51,7 @@ RISC-V 架构中一共定义了 4 种特权级: - 名称 * - 0 - 00 - - 机器模式 (M, Machine) + - 用户/应用模式 (U, User/Application) * - 1 - 01 - 监督模式 (S, Supervisor) @@ -29,9 +60,9 @@ RISC-V 架构中一共定义了 4 种特权级: - H, Hypervisor * - 3 - 11 - - 用户/应用模式 (U, User/Application) + - 机器模式 (M, Machine) -其中,级别的数值越小,特权级越高,掌控硬件的能力越强。从表中可以看出, M 模式处在最高的特权级,而 U 模式处于最低的特权级。 +其中,级别的数值越大,特权级越高,掌控硬件的能力越强。从表中可以看出, M 模式处在最高的特权级,而 U 模式处于最低的特权级。 之前我们给出过支持应用程序运行的一套 :ref:`执行环境栈 ` ,现在我们站在特权级架构的角度去重新看待它: @@ -39,6 +70,8 @@ RISC-V 架构中一共定义了 4 种特权级: :align: center :name: PrivilegeStack +.. _term-see: + 和之前一样,白色块表示一层执行环境,黑色块表示相邻两层执行环境之间的接口。这张图片给出了能够支持运行 Unix 这类复杂系统的软件栈。其中 内核代码运行在 S 模式上;应用程序运行在 U 模式上。运行在 M 模式上的软件被称为 **监督模式执行环境** (SEE, Supervisor Execution Environment) ,这是站在运行在 S 模式上的软件的视角来看,它的下面也需要一层执行环境支撑,因此被命名为 SEE,它需要在相比 S 模式更高的特权级下运行, @@ -53,19 +86,29 @@ RISC-V 架构中一共定义了 4 种特权级: - 简单的嵌入式应用只需要实现 M 模式; - 带有一定保护能力的嵌入式系统需要实现 M/U 模式; - 复杂的多任务系统则需要实现 M/S/U 模式。 + - 到目前为止,(Hypervisor, H)模式的特权规范还没完全制定好。所以本书不会涉及。 之前我们提到过,执行环境的其中一种功能是在执行它支持的上层软件之前进行一些初始化工作。我们之前提到的引导加载程序会在加电后对整个系统进行 初始化,它实际上是 SEE 功能的一部分,也就是说在 RISC-V 架构上引导加载程序一般运行在 M 模式上。此外,编程语言的标准库也会在执行程序员 编写的逻辑之前进行一些初始化工作,但是在这张图中我们并没有将其展开,而是统一归类到 U 模式软件,也就是应用程序中。 +回顾第一章,当时只是实现了简单的支持单个裸机应用的库级别的“三叶虫”操作系统,它和应用程序全程运行在 S 模式下,应用程序很容易破坏没有任何保护的执行环境--操作系统。而在后续的章节中,我们会涉及到RISC-V的 M/S/U 三种特权级:其中应用程序和用户态支持库运行在 U 模式的最低特权级;操作系统内核运行在 S 模式特权级(在本章表现为一个简单的批处理系统),形成支撑应用程序和用户态支持库的执行环境;而第一章提到的预编译的 bootloader -- ``RustSBI`` 实际上是运行在更底层的 M 模式特权级下的软件,是操作系统内核的执行环境。整个软件系统就由这三层运行在不同特权级下的不同软件组成。 + +在特权级相关机制方面,本书正文中我们重点关心RISC-V的 S/U 特权级, M 特权级的机制细节则是作为可选内容在 :doc:`/appendix-c/index` 中讲解,有兴趣的读者可以参考。 + +.. _term-ecf: +.. _term-trap: + 执行环境的另一种功能是对上层软件的执行进行监控管理。监控管理可以理解为,当上层软件执行的时候出现了一些情况导致需要用到执行环境中提供的功能, 因此需要暂停上层软件的执行,转而运行执行环境的代码。由于上层软件和执行环境被设计为运行在不同的特权级,这个过程也往往(而 **不一定** ) 伴随着 CPU 的 **特权级切换** 。当执行环境的代码运行结束后,我们需要回到上层软件暂停的位置继续执行。在 RISC-V 架构中,这种与常规控制流 -(顺序、循环、分支、函数调用)不同的 **异常控制流** (ECF, Exception Control Flow) 被称为 **陷入** (Trap) 。 +(顺序、循环、分支、函数调用)不同的 **异常控制流** (ECF, Exception Control Flow) 被称为 **异常(Exception)** 。 -触发 Trap 的原因总体上可以分为两种: **中断** (Interrupt) 和 **异常** (Exception) 。本章我们只会用到异常,因此暂且略过中断。异常 -就是指上层软件需要执行环境功能的原因确切的与上层软件的 **某一条指令的执行** 相关。下表中我们给出了 RISC-V 特权级定义的一些异常: +.. _term-exception: + +用户态应用直接触发从用户态到内核态的 **异常控制流** 的原因总体上可以分为两种:执行 ``Trap类异常`` 指令和执行了会产生 ``Fault类异常`` 的指令 。``Trap类异常`` 指令 +就是指用户态软件为获得内核态操作系统的服务功能而发出的特殊指令。 ``Fault类`` 的指令是指用户态软件执行了在内核态操作系统看来是非法操作的指令。下表中我们给出了 RISC-V 特权级定义的会导致从低特权级到高特权级的各种 **异常**: .. list-table:: RISC-V 异常一览表 :align: center @@ -115,35 +158,72 @@ RISC-V 架构中一共定义了 4 种特权级: - 13 - Load page fault * - 0 - - 14 + - 15 - Store/AMO page fault -其中断点异常 (Breakpoint) 和执行环境调用 (Environment call) 两个异常是通过在上层软件中执行一条特定的指令触发的:当执行 ``ebreak`` -这条指令的之后就会触发断点异常;而执行 ``ecall`` 这条指令的时候则会随着 CPU 当前所处特权级而触发不同的异常。从表中可以看出,当 CPU 分别 -处于 M/S/U 三种特权级时执行 ``ecall`` 这条指令会触发三种异常。 +.. _term-environment-call: + +其中断点(Breakpoint) 和 **执行环境调用** (Environment call) 两个异常(为了与其他非有意为之的异常区分,会把这种有意为之的指令称为 ``陷入`` 或 +``trap`` 类指令)是通过在上层软件中执行一条特定的指令触发的:当执行 ``ebreak`` +这条指令的之后就会触发断点陷入异常;而执行 ``ecall`` 这条指令的时候则会随着 CPU 当前所处特权级而触发不同的 ``陷入`` 情况。从表中可以看出,当 CPU 分别 +处于 M/S/U 三种特权级时执行 ``ecall`` 这条指令会触发三种陷入。 + +.. _term-sbi: +.. _term-abi: -在这里我们需要说明一下执行环境调用,这是一种很特殊的异常, :ref:`上图 ` 中相邻两特权级软件之间的接口正是基于这种异常 -机制实现的。M 模式软件 SEE 和 S 模式的内核之间的接口被称为 **监督模式二进制接口** (SBI, Supervisor Binary Interface),而内核和 -U 模式的应用程序之间的接口被称为 **应用程序二进制接口** (Application Binary Interface),当然它有一个更加通俗的名字—— **系统调用** +在这里我们需要说明一下执行环境调用 ``ecall`` ,这是一种很特殊的会产生 ``陷入`` 的指令, :ref:`上图 ` 中相邻两特权级软件之间的接口正是基于这种陷入 +机制实现的。M 模式软件 SEE 和 S 模式的内核之间的接口被称为 **监督模式二进制接口** (Supervisor Binary Interface, SBI),而内核和 +U 模式的应用程序之间的接口被称为 **应用程序二进制接口** (Application Binary Interface, ABI),当然它有一个更加通俗的名字—— **系统调用** (syscall, System Call) 。而之所以叫做二进制接口,是因为它和在同一种编程语言内部调用接口不同,是汇编指令级的一种接口。事实上 M/S/U -三个特权级的软件可能分别由不同的编程语言实现,即使是用同一种编程语言实现的,其调用也并不是普通的函数调用执行流,而是陷入,在该过程中有可能 -切换 CPU 特权级。因此只有将接口下降到汇编指令级才能够满足其通用性。 +三个特权级的软件可能分别由不同的编程语言实现,即使是用同一种编程语言实现的,其调用也并不是普通的函数调用执行流,而是**陷入异常控制流** ,在该过程中会 +切换 CPU 特权级。因此只有将接口下降到汇编指令级才能够满足其通用性和灵活性。 -可以看到,在这样的架构之下,每层特权级的软件都只能做高特权级软件允许它做的、且对于高特权级软件不会产生什么撼动的事情,一旦超出了能力范围, +可以看到,在这样的架构之下,每层特权级的软件都只能做高特权级软件允许它做的、且不会产生什么撼动高特权级软件的事情,一旦低特权级软件的要求超出了其能力范围, 就必须寻求高特权级软件的帮助。因此,在一条执行流中我们经常能够看到特权级切换。如下图所示: .. image:: EnvironmentCallFlow.png :align: center + :name: environment-call-flow + +.. _term-csr: + +其他的异常则一般是在执行某一条指令的时候发生了某种错误(如除零、无效地址访问、无效指令等),或处理器认为处于当前特权级下执行当前指令是高特权级指令或会访问不应该访问的高特权级的资源(可能危害系统)。碰到这些情况,就需要需要将控制转交给高特权级的软件(如操作系统)来处理。当处理错误恢复后,则可重新回到低优先级软件去执行;如果不能回复错误,那高特权级软件可以杀死和清除低特权级软件,免破坏整个执行环境。 + +.. _term-csr-instr: + +RISC-V的特权指令 +^^^^^^^^^^^^^^^^^^^^^^^^^ +与特权级无关的一般的指令和通用寄存器 ``x0~x31`` 在任何特权级都可以任意执行。而每个特权级都对应一些特殊指令和 **控制状态寄存器** (CSR, Control and Status Register) ,来控制该特权级的某些行为并描述其状态。当然特权指令不只是具有有读写 CSR 的指令,还有其他功能的特权指令。 + +如果低优先级下的处理器执行了高优先级的指令,会产生非法指令错误的异常,于是位于高特权级的执行环境能够得知低优先级的软件出现了该错误,这个错误一般是不可恢复的,此时一般它会将上层的低特权级软件终止。这在某种程度上体现了特权级保护机制的作用。 + +在RISC-V中,会有两类低优先级U模式下运行高优先级S模式的指令: + +- 指令本身属于高特权级的指令,如 ``sret`` 指令(表示从S模式返回到U模式)。 +- 指令访问了 :ref:`S模式特权级下才能访问的寄存器 ` 或内存,如表示S模式系统状态的 **控制状态寄存器** ``sstatus`` 等。 + +.. list-table:: RISC-V S模式特权指令 + :align: center + :header-rows: 1 + :widths: 30 60 + + * - 指令 + - 含义 + * - sret + - 从S模式返回U模式。在U模式下执行会产生非法指令异常 + * - wfi + - 处理器在空闲时进入低功耗状态等待中断。在U模式下执行会尝试非法指令异常 + * - sfence.vma + - 刷新TLB缓存。在U模式下执行会尝试非法指令异常 + * - 访问S模式CSR的指令 + - 通过访问 :ref:`sepc/stvec/scause/sscartch/stval/sstatus/satp等CSR ` 来改变系统状态。在U模式下执行会尝试非法指令异常 -其他的异常则一般是在执行某一条指令的时候发生了错误,需要将控制转交给高特权级软件:当错误可恢复的时候,则处理错误并重新回到上层软件的执行; -否则,一般会将上层软件杀死以避免破坏执行环境。 +在下一节中,我们将看到 :ref:`在U模式下的用户态应用程序 ` ,如果执行上述S模式特权指令指令,将会产生非法指令异常,从而看出RISC-V的特权模式设计在一定程度上提供了对操作系统的保护。 -第一章只是一个简单的嵌入式应用,它全程运行在 M 模式下。而在后续的章节中,我们会用到 M/S/U 三种特权级:其中我们的内核运行在 S 模式下 -(在本章表现为一个简单的批处理系统),应用程序运行在 U 特权级下,第一章提到的预编译的 bootloader 实际上是运行在 M 模式下的 SEE。 -整个系统就由这三层运行在不同特权级下的不同软件组成。在特权级相关机制方面,本书正文中我们重点关心 S/U 特权级, M 特权级的机制细节则 -是作为可选内容在附录 :doc:`/appendix-c/index` 中讲解,有兴趣的读者可以参考。 .. + * - mret + - 从M模式返回S/U模式。在S/U模式下执行会产生非法指令异常 随着特权级的逐渐降低,硬件的能力受到限制, 从每一个特权级看来,比它特权级更低的部分都可以看成是它的应用。(这个好像没啥用?) M 模式是每个 RISC-V CPU 都需要实现的模式,而剩下的模式都是可选的。常见的模式组合:普通嵌入式应用只需要在 M 模式上运行;追求安全的 diff --git a/source/chapter2/2application.rst b/source/chapter2/2application.rst index 8125adfcb47ebf461d37e97db126d52452364e39..50c85bad91c291b1d75a9aee9ca1ddcbbddafc5b 100644 --- a/source/chapter2/2application.rst +++ b/source/chapter2/2application.rst @@ -5,13 +5,23 @@ :hidden: :maxdepth: 5 -本节我们来实现被批处理系统逐个加载并运行的应用程序,它们是在认为自己会在 U 模式运行的前提下而设计、编写的,但实际上它们完全可能在其他特权级 -运行。事实上,保证应用程序的代码在 U 模式运行是我们接下来将实现的批处理系统的任务。 +本节导读 +------------------------------- -应用程序的实现放在项目根目录的 ``user`` 目录下,它和第一章的嵌入式应用不同之处在于以下几点。 +本节主要讲解如何设计实现被批处理系统逐个加载并运行的应用程序。它们是假定在 U 特权级模式运行的前提下而设计、编写的。实际上,如果应用程序的代码都符合它要运行的某特权级的约束,那它完全可能在某特权级中运行。保证应用程序的代码在 U 模式运行是我们接下来将实现的批处理系统的任务。其涉及的设计实现要点是: + +- 应用程序的内存布局 +- 应用程序发出的系统调用 + +从某种程度上讲,这里设计的应用程序与第一章中的最小用户态执行环境有很多相同的地方。即设计一个应用程序,能够在用户态通过操作系统提供的服务完成自身的功能。 + +应用程序设计 +----------------------------- + +应用程序的实现放在项目根目录的 ``user`` 目录下,它和第一章的裸机应用不同之处在于以下几点。 项目结构 ------------------- +^^^^^^^^^^^^^^^^^^^^^^ 我们看到 ``user/src`` 目录下面多出了一个 ``bin`` 目录。``bin`` 里面有多个文件,每个文件都是一个用户程序,目前里面有三个程序,分别是: @@ -43,7 +53,7 @@ #[link_section = ".text.entry"] pub extern "C" fn _start() -> ! { clear_bss(); - syscall::sys_exit(main()); + exit(main()); panic!("unreachable after sys_exit!"); } @@ -51,8 +61,9 @@ 调整它的位置使得它能够作为用户库的入口。 而从第 4 行开始我们能够看到进入用户库入口之后,首先和第一章一样手动清空需要被零初始化 ``.bss`` 段(很遗憾到目前为止底层的批处理系统还 -没有这个能力,所以我们只能在用户库中完成),然后是调用 ``main`` 函数得到一个类型为 ``i32`` 的返回值,最后是使用接下来会提到的系统调用 -退出应用程序并将这个返回值告知批处理系统。 +没有这个能力,所以我们只能在用户库中完成),然后是调用 ``main`` 函数得到一个类型为 ``i32`` 的返回值。 + +第 5 行我们调用后面会提到的用户库提供的 ``exit`` 接口退出应用程序并将这个返回值告知批处理系统。 我们还在 ``lib.rs`` 中看到了另一个 ``main`` : @@ -76,19 +87,19 @@ #![feature(linkage)] 内存布局 -------------------- +^^^^^^^^^^^^^^^^^^^^^^ 在 ``user/.cargo/config`` 中,我们和第一章一样设置链接时使用链接脚本 ``user/src/linker.ld`` 。在其中我们做的重要的事情是: -- 将程序的起始物理地址调整为 ``0x80040000`` ,三个应用程序都会被加载到这个物理地址上运行; -- 将 ``_start`` 所在的 ``.text.entry`` 放在整个程序的开头,也就是说批处理系统只要在加载之后跳转到 ``0x80040000`` 就已经进入了 +- 将程序的起始物理地址调整为 ``0x80400000`` ,三个应用程序都会被加载到这个物理地址上运行; +- 将 ``_start`` 所在的 ``.text.entry`` 放在整个程序的开头,也就是说批处理系统只要在加载之后跳转到 ``0x80400000`` 就已经进入了 用户库的入口点,并会在初始化之后跳转到应用程序主逻辑; - 提供了最终生成可执行文件的 ``.bss`` 段的起始和终止地址,方便 ``clear_bss`` 函数使用。 其余的部分和第一章基本相同。 系统调用 ---------------------- +^^^^^^^^^^^^^^^^^^^^^^ 在子模块 ``syscall`` 中我们作为应用程序来通过 ``ecall`` 调用批处理系统提供的接口,由于应用程序运行在 U 模式, ``ecall`` 指令会触发 名为 ``Environment call from U-mode`` 的异常,并 Trap 进入 S 模式执行批处理系统针对这个异常特别提供的服务代码。由于这个接口处于 @@ -98,7 +109,7 @@ S 模式的批处理系统和 U 模式的应用程序之间,从上一节我们 在本章中,应用程序和批处理系统之间约定如下两个系统调用: .. code-block:: rust - :caption: 系统调用一 + :caption: 第二章新增系统调用 /// 功能:将内存中缓冲区中的数据写入文件。 /// 参数:`fd` 表示待写入文件的文件描述符; @@ -125,6 +136,8 @@ S 模式的批处理系统和 U 模式的应用程序之间,从上一节我们 .. code-block:: rust :linenos: + // user/src/syscall.rs + fn syscall(id: usize, args: [usize; 3]) -> isize { let mut ret: isize; unsafe { @@ -138,9 +151,9 @@ S 模式的批处理系统和 U 模式的应用程序之间,从上一节我们 ret } -第 1 行,我们将所有的系统调用都封装成 ``syscall`` 函数,可以看到它支持传入 syscall ID 和 3 个参数。 +第 3 行,我们将所有的系统调用都封装成 ``syscall`` 函数,可以看到它支持传入 syscall ID 和 3 个参数。 -第 4 行开始,我们使用 Rust 提供的 ``llvm_asm!`` 宏在代码中内嵌汇编,在本行也给出了具体要插入的汇编指令,也就是 ``ecall``,但这并不是 +第 6 行开始,我们使用 Rust 提供的 ``llvm_asm!`` 宏在代码中内嵌汇编,在本行也给出了具体要插入的汇编指令,也就是 ``ecall``,但这并不是 全部,后面我们还需要进行一些相关设置。这个宏在 Rust 中还不稳定,因此我们需要在 ``lib.rs`` 开头加入 ``#![feature(llvm_asm)]`` 。 此外,编译器无法判定插入汇编代码这个行为的安全性,所以我们需要将其包裹在 unsafe 块中自己来对它负责。 @@ -157,27 +170,26 @@ Rust 中的 ``llvm_asm!`` 宏的完整格式如下: 下面逐行进行说明。 -第 5 行指定输出操作数。这里由于我们的系统调用返回值只有一个 ``isize`` ,根据调用规范它会被保存在 ``a0`` 寄存器中。在双引号内,我们 +第 7 行指定输出操作数。这里由于我们的系统调用返回值只有一个 ``isize`` ,根据调用规范它会被保存在 ``a0`` 寄存器中。在双引号内,我们 可以对于使用的操作数进行限制,由于是输出部分,限制的开头必须是一个 ``=`` 。我们可以在限制内使用一对花括号再加上一个寄存器的名字告诉 编译器汇编的输出结果会保存在这个寄存器中。我们将声明出来用来保存系统调用返回值的变量 ``ret`` 包在一对普通括号里面放在操作数限制的 后面,这样可以把变量和寄存器建立联系。于是,在系统调用返回之后我们就能在变量 ``ret`` 中看到返回值了。注意,变量 ``ret`` 必须为可变 绑定,否则无法通过编译,这也说明在 unsafe 块内编译器还是会进行力所能及的安全检查。 -第 6 行指定输入操作数。由于是输入部分,限制的开头不用加上 ``=`` 。同时在限制中设置使用寄存器 ``a0~a2`` 来保存系统调用的参数,以及 +第 8 行指定输入操作数。由于是输入部分,限制的开头不用加上 ``=`` 。同时在限制中设置使用寄存器 ``a0~a2`` 来保存系统调用的参数,以及 寄存器 ``a7`` 保存 syscall ID ,而它们分别 ``syscall`` 的参数变量 ``args`` 和 ``id`` 绑定。 -第 7 行用于告知编译器插入的汇编代码会造成的一些影响以防止编译器在不知情的情况下误优化。常用的使用方法是告知编译器某个寄存器在执行嵌入 +第 9 行用于告知编译器插入的汇编代码会造成的一些影响以防止编译器在不知情的情况下误优化。常用的使用方法是告知编译器某个寄存器在执行嵌入 的汇编代码中的过程中会发生变化。我们这里则是告诉编译器在执行嵌入汇编代码中的时候会修改内存。这能给编译器提供更多信息。 -第 8 行用于告知编译器将我们在程序中给出的嵌入汇编代码保持原样放到最终构建的可执行文件中。如果不这样做的话,编译器可能会把它和其他代码 +第 10 行用于告知编译器将我们在程序中给出的嵌入汇编代码保持原样放到最终构建的可执行文件中。如果不这样做的话,编译器可能会把它和其他代码 一视同仁并放在一起进行一些我们期望之外的优化。为了保证语义的正确性,一些比较关键的汇编代码需要加上该选项。 -第一章中的输出到屏幕的操作也同样是使用内联汇编调用 SEE 提供的 SBI 接口来实现的。有兴趣的读者可以回顾第一章的 ``console.rs`` 和 -``sbi.rs`` 。 +上面这一段汇编代码的含义和内容与第一章中的 :ref:`第一章中U-Mode应用程序中的系统调用汇编代码 ` 的是一致的。与 :ref:`第一章中的RustSBI输出到屏幕的SBI调用汇编代码 ` 涉及的汇编指令一样,但传递参数的寄存器的含义是不同的。有兴趣的读者可以回顾第一章的 ``console.rs`` 和 ``sbi.rs`` 。 .. note:: - **Rust 中的内联汇编** + **Rust 语法卡片:内联汇编** 我们这里使用的 ``llvm_asm!`` 宏是将 Rust 底层 IR LLVM 中提供的内联汇编包装成的,更多信息可以参考 `llvm_asm 文档 `_ 。 @@ -188,6 +200,8 @@ Rust 中的 ``llvm_asm!`` 宏的完整格式如下: .. code-block:: rust :linenos: + // user/src/syscall.rs + const SYSCALL_WRITE: usize = 64; const SYSCALL_EXIT: usize = 93; @@ -199,20 +213,156 @@ Rust 中的 ``llvm_asm!`` 宏的完整格式如下: syscall(SYSCALL_EXIT, [xstate as usize, 0, 0]) } +.. _term-fat-pointer: + 注意 ``sys_write`` 使用一个 ``&[u8]`` 切片类型来描述缓冲区,这是一个 **胖指针** (Fat Pointer),里面既包含缓冲区的起始地址,还 包含缓冲区的长度。我们可以分别通过 ``as_ptr`` 和 ``len`` 方法取出它们并独立的作为实际的系统调用参数。 -我们把 ``console`` 子模块中 ``Stdout::write_str`` 改成基于 ``sys_write`` 的实现,且传入的 ``fd`` 参数设置为 1,它代表标准输出, +我们将上述两个系统调用在用户库 ``user_lib`` 中进一步封装,从而更加接近在 Linux 等平台的实际体验: + +.. code-block:: rust + :linenos: + + // user/src/lib.rs + use syscall::*; + + pub fn write(fd: usize, buf: &[u8]) -> isize { sys_write(fd, buf) } + pub fn exit(exit_code: i32) -> isize { sys_exit(exit_code) } + +我们把 ``console`` 子模块中 ``Stdout::write_str`` 改成基于 ``write`` 的实现,且传入的 ``fd`` 参数设置为 1,它代表标准输出, 也就是输出到屏幕。目前我们不需要考虑其他的 ``fd`` 选取情况。这样,应用程序的 ``println!`` 宏借助系统调用变得可用了。 +参考下面的代码片段: + +.. code-block:: rust + :linenos: + + // user/src/console.rs + const STDOUT: usize = 1; + + impl Write for Stdout { + fn write_str(&mut self, s: &str) -> fmt::Result { + write(STDOUT, s.as_bytes()); + Ok(()) + } + } -``sys_exit`` 则在用户库中的 ``_start`` 内使用,当应用程序主逻辑 ``main`` 返回之后,使用该系统调用退出应用程序并将返回值告知 +``exit`` 接口则在用户库中的 ``_start`` 内使用,当应用程序主逻辑 ``main`` 返回之后,使用它退出应用程序并将返回值告知 底层的批处理系统。 -自动构建 ------------------------ + + +编译生成应用程序二进制码 +------------------------------- 这里简要介绍一下应用程序的自动构建。只需要在 ``user`` 目录下 ``make build`` 即可: 1. 对于 ``src/bin`` 下的每个应用程序,在 ``target/riscv64gc-unknown-none-elf/release`` 目录下生成一个同名的 ELF 可执行文件; 2. 使用 objcopy 二进制工具将上一步中生成的 ELF 文件删除所有 ELF header 和符号得到 ``.bin`` 后缀的纯二进制镜像文件。它们将被链接 - 进内核并由内核在合适的时机加载到内存。 \ No newline at end of file + 进内核并由内核在合适的时机加载到内存。 + +实现操作系统前执行应用程序 +----------------------------------- + +我们还没有实现操作系统,能提前执行或测试应用程序吗?可以! 这是因为我们除了一个能模拟一台RISC-V 64 计算机的全系统模拟器 ``qemu-system-riscv64`` 外,还有一个 :ref:`直接支持运行RISC-V64 用户程序的半系统模拟器qemu-riscv64 ` 。 + +.. note:: + + 如果想让用户态应用程序在Linux和在我们自己写的OS上执行效果一样,需要做到二者的系统调用的接口是一样的(包括系统调用编号,参数约定的具体的寄存器和栈等)。 + + +.. _term-csr-instr-app: + +假定我们已经完成了编译并生成了ELF 可执行文件格式的应用程序,我们就可以来试试。首先看看应用程序执行 :ref:`RV64的S模式特权指令 ` 会出现什么情况。 + +.. note:: + + 下载编译特权指令的应用需要获取 + + .. code-block:: console + + $ git clone -b v4-illegal-priv-code-csr-in-u-mode-app https://github.com/chyyuu/os_kernel_lab.git + $ cd os_kernel_lab/user + $ make build + +我们先看看代码: + +.. code-block:: rust + :linenos: + + // usr/src/bin/03priv_intr.rs + ... + println!("Hello, world!"); + unsafe { + llvm_asm!("sret" + : : : : + ); + } + ... + +在上述代码中,在显示 ``Hello, world`` 字符串后,会执行 ``sret`` 特权指令。 + +.. code-block:: rust + :linenos: + + // usr/src/bin/04priv_intr.rs + ... + println!("Hello, world!"); + let mut sstatus = sstatus::read(); + sstatus.set_spp(SPP::User); + ... + +在上述代码中,在显示 ``Hello, world`` 字符串后,会读写 ``sstatus`` 特权CSR。 + +.. code-block:: console + + $ cd user + $ cd target/riscv64gc-unknown-none-elf/release/ + $ ls + 00hello_world 01store_fault 02power + 03priv_intr 04priv_csr + ... + # 上面的文件就是ELF格式的应用程序 + $ qemu-riscv64 ./03priv_intr + Hello, world! + 非法指令 (核心已转储) + # 执行特权指令出错 + $ qemu-riscv64 ./04priv_csr + Hello, world! + 非法指令 (核心已转储) + # 执行访问特权级CSR的指令出错 + +看来RV64的特权级机制确实有用。那对于一般的应用程序,在 ``qemu-riscv64`` 模拟器下能正确执行吗? + +.. code-block:: console + + $ cd user + $ cd target/riscv64gc-unknown-none-elf/release/ + $ ls + 00hello_world 01store_fault 02power + 03priv_intr 04priv_csr + ... + # 上面的文件就是ELF格式的应用程序 + $ qemu-riscv64 ./00hello_world + Hello, world! + # 正确显示了字符串 + $ qemu-riscv64 01store_fault + qemu-riscv64 01store_fault + Into Test store_fault, we will insert an invalid store operation... + Kernel should kill this application! + 段错误 (核心已转储) + # 故意访问了一个非法地址,导致应用和qemu-riscv64被Linux内核杀死 + $ qemu-riscv64 02power + 3^10000=5079 + 3^20000=8202 + 3^30000=8824 + 3^40000=5750 + 3^50000=3824 + 3^60000=8516 + 3^70000=2510 + 3^80000=9379 + 3^90000=2621 + 3^100000=2749 + Test power OK! + # 正确地完成了计算 + +三个应用都能够执行并顺利结束!是由于得到了本机操作系统Linux的支持。我们期望我们在下一节开始实现的泥盆纪“邓式鱼”操作系统也能够正确上面的应用程序。 diff --git a/source/chapter2/3batch-system.rst b/source/chapter2/3batch-system.rst index 16fb4430df9a882492993166ee37612512247b58..f75f9c9ebf58314904cfec89bc384a2735705484 100644 --- a/source/chapter2/3batch-system.rst +++ b/source/chapter2/3batch-system.rst @@ -1,10 +1,20 @@ -实现批处理系统 +实现批处理操作系统 ============================== .. toctree:: :hidden: :maxdepth: 5 +本节导读 +------------------------------- + +目前本章设计的批处理操作系统--泥盆纪“邓式鱼”操作系统,还没有文件/文件系统的机制与设计实现,所以还缺少一种类似文件系统那样的松耦合灵活放置应用程序和加载执行应用程序的机制。这就需要设计一种简洁的程序放置和加载方式,能够在批处理操作系统与应用程序之间建立联系的纽带。这主要包括两个方面: + +- 静态编码:通过一定的编程技巧,把应用程序代码和批处理操作系统代码“绑定”在一起。 +- 动态加载:基于静态编码留下的“绑定”信息,操作系统可以找到应用程序文件二进制代码的起始地址和长度,并能加载到内存中运行。 + +这里与硬件相关且比较困难的地方是如何让在内核态的批处理操作系统启动应用程序,且能让应用程序在用户态正常执行。本节会讲大致过程,而具体细节将放到下一节具体讲解。 + 将应用程序链接到内核 -------------------------------------------- @@ -25,7 +35,7 @@ # os/src/link_app.S - .align 4 + .align 3 .section .data .global _num_app _num_app: @@ -63,10 +73,15 @@ 这个文件是在 ``cargo build`` 的时候,由脚本 ``os/build.rs`` 控制生成的。有兴趣的读者可以参考其代码。 -应用管理器 --------------------------- +找到并加载应用程序二进制码 +----------------------------------------------- + +能够找到并加载应用程序二进制码的应用管理器 ``AppManager`` 是“邓式鱼”操作系统的核心组件。我们在 ``os`` 的 ``batch`` 子模块中实现一个应用管理器,它的主要功能是: -我们在 ``os`` 的 ``batch`` 子模块中实现一个应用管理器,它的功能是:保存应用数量和各自的位置信息,以及当前执行到第几个应用了。结构体定义 +- 保存应用数量和各自的位置信息,以及当前执行到第几个应用了。 +- 根据应用程序位置信息,初始化好应用所需内存空间,并加载应用执行。 + +应用管理器 ``AppManager`` 结构体定义 如下: .. code-block:: rust @@ -79,6 +94,7 @@ current_app: usize, app_start: [usize; MAX_APP_NUM + 1], } + unsafe impl Sync for AppManager {} 这里我们可以看出,上面提到的应用管理器需要保存和维护的信息都在 ``AppManagerInner`` 里面,而结构体 ``AppManager`` 里面只是保存了 一个指向 ``AppManagerInner`` 的 ``RefCell`` 智能指针。这样设计的原因在于:我们希望将 ``AppManager`` 实例化为一个全局变量使得 @@ -86,10 +102,23 @@ 的时候一种自然的方法是利用 ``static mut``。但是在 Rust 中,任何对于 ``static mut`` 变量的访问都是 unsafe 的,而我们要尽可能 减少 unsafe 的使用来更多的让编译器负责安全性检查。 -于是,我们利用 ``RefCell`` 来提供内部可变性,所谓的内部可变性就是指在我们只能拿到 ``AppManager`` 的不可变借用,意味着同样也只能 +此外,为了让 ``AppManager`` 能被直接全局实例化,我们需要将其标记为 ``Sync`` 。 + +.. note:: + + **为什么对于 static mut 的访问是 unsafe 的** + + **为什么要将 AppManager 标记为 Sync** + + 可以参考附录A:Rust 快速入门的并发章节。 + +.. _term-interior-mutability: + +于是,我们利用 ``RefCell`` 来提供 **内部可变性** (Interior Mutability), +所谓的内部可变性就是指在我们只能拿到 ``AppManager`` 的不可变借用,意味着同样也只能 拿到 ``AppManagerInner`` 的不可变借用的情况下依然可以修改 ``AppManagerInner`` 里面的字段。 使用 ``RefCell::borrow/RefCell::borrow_mut`` 分别可以拿到 ``RefCell`` 里面内容的不可变借用/可变借用, -``RefCell`` 内部会运行时维护当前已有的借用状态并进行借用检查。于是 ``RefCell::borrow_mut`` 就是我们实现内部可变性的关键。 +``RefCell`` 会在运行时维护当前它管理的对象的已有借用状态,并在访问对象时进行借用检查。于是 ``RefCell::borrow_mut`` 就是我们实现内部可变性的关键。 我们这样初始化 ``AppManager`` 的全局实例: @@ -115,6 +144,8 @@ }; } +初始化的逻辑很简单,就是找到 ``link_app.S`` 中提供的符号 ``_num_app`` ,并从这里开始解析出应用数量以及各个应用的开头地址。注意其中对于切片类型的使用能够很大程度上简化编程。 + 这里我们使用了外部库 ``lazy_static`` 提供的 ``lazy_static!`` 宏。要引入这个外部库,我们需要加入依赖: .. code-block:: toml @@ -126,15 +157,11 @@ ``lazy_static!`` 宏提供了全局变量的运行时初始化功能。一般情况下,全局变量必须在编译期设置一个初始值,但是有些全局变量依赖于运行期间 才能得到的数据作为初始值。这导致这些全局变量需要在运行时发生变化,也即重新设置初始值之后才能使用。如果我们手动实现的话有诸多不便之处, -比如又需要把这种全局变量声明为 ``static mut`` 并衍生出很多 unsafe 。这种情况下我们可以使用 ``lazy_static!`` 宏来帮助我们解决 -这个问题。这里我们借助 ``lazy_static!`` 声明了一个名为 ``APP_MANAGER`` 的 ``AppManager`` 全局实例,且只有在它第一次被使用到 -的时候才会实际进行初始化工作。 - -初始化的逻辑很简单,就是找到 ``link_app.S`` 中提供的符号 ``_num_app`` ,并从这里开始解析出应用数量以及各个应用的开头地址。注意其中 -对于切片类型的使用能够很大程度上简化编程。 +比如需要把这种全局变量声明为 ``static mut`` 并衍生出很多 unsafe code。这种情况下我们可以使用 ``lazy_static!`` 宏来帮助我们解决 +这个问题。这里我们借助 ``lazy_static!`` 声明了一个 ``AppManager`` 结构的名为 ``APP_MANAGER`` 的全局实例,且只有在它第一次被使用到 +的时候才会进行实际的初始化工作。 -因此,借助 Rust 核心库提供的 ``RefCell`` 和外部库 ``lazy_static!``,我们就能在避免 ``static mut`` 声明的情况下以更加 Rust 的 -方式使用全局变量。 +因此,借助 Rust 核心库提供的 ``RefCell`` 和外部库 ``lazy_static!``,我们就能在避免 ``static mut`` 声明的情况下以更加优雅的Rust风格使用全局变量。 ``AppManagerInner`` 的方法中, ``print_app_info/get_current_app/move_to_next_app`` 都相当简单直接,需要说明的是 ``load_app``: @@ -163,10 +190,13 @@ app_dst.copy_from_slice(app_src); } -这个方法负责将参数 ``app_id`` 对应的应用程序的二进制镜像加载到物理内存以 ``0x80040000`` 开头的位置,这个位置是批处理系统和应用程序 -之间约定的常数,回忆上一小节中,我们也调整应用程序的内存布局以同一个地址开头。第 8 行开始,我们首先将一块内存清空,然后找到待加载应用 -二进制镜像的位置,并将它复制到正确的位置。它本质上是数据从一块内存复制到另一块内存,从批处理系统的角度来看是将它数据段的一部分复制到了它 -程序之外未知的地方。 +这个方法负责将参数 ``app_id`` 对应的应用程序的二进制镜像加载到物理内存以 ``0x80400000`` 开头的位置,这个位置是批处理操作系统和应用程序 +之间约定的常数地址,回忆上一小节中,我们也调整应用程序的内存布局以同一个地址开头。第 8 行开始,我们首先将一块内存清空,然后找到待加载应用 +二进制镜像的位置,并将它复制到正确的位置。它本质上是把数据从一块内存复制到另一块内存,从批处理操作系统的角度来看是将它数据段的一部分复制到了它 +程序之外未知的地方。在这一点上也体现了冯诺依曼计算机的 ``代码即数据`` 的特征。 + +.. _term-dcache: +.. _term-icache: 注意第 7 行我们插入了一条奇怪的汇编指令 ``fence.i`` ,它是用来清理 i-cache 的。我们知道缓存是存储层级结构中提高访存速度的很重要一环。 而 CPU 对物理内存所做的缓存又分成 **数据缓存** (d-cache) 和 **指令缓存** (i-cache) 两部分,分别在 CPU 访存和取指的时候使用。在取指 @@ -175,8 +205,15 @@ 区域,这会使得 i-cache 中含有与内存中不一致的内容。因此我们这里必须使用 ``fence.i`` 指令手动清空 i-cache ,让里面所有的内容全部失效, 才能够保证正确性。 +.. warning:: + + **模拟器与真机的不同之处** + + 至少在 Qemu 模拟器的默认配置下,各类缓存如 i-cache/d-cache/TLB 都处于机制不完全甚至完全不存在的状态。目前在 Qemu 平台上,即使我们 + 不加上刷新 i-cache 的指令,大概率也是能够正常运行的。但在 K210 真机上就会看到错误。 + ``batch`` 子模块对外暴露出如下接口: - ``init`` :调用 ``print_app_info`` 的时候第一次用到了全局变量 ``APP_MANAGER`` ,它也是在这个时候完成初始化; -- ``run_next_app`` :批处理系统的核心操作,即加载并运行下一个应用程序。当批处理系统完成初始化或者一个应用程序运行结束或出错之后会调用 +- ``run_next_app`` :批处理操作系统的核心操作,即加载并运行下一个应用程序。当批处理操作系统完成初始化或者一个应用程序运行结束或出错之后会调用 该函数。我们下节再介绍其具体实现。 \ No newline at end of file diff --git a/source/chapter2/4trap-handling.rst b/source/chapter2/4trap-handling.rst index dad2f03e6865f3b53a3a818383133c242196b5ec..5c57c408c63546ba47044926148cefb7e765d549 100644 --- a/source/chapter2/4trap-handling.rst +++ b/source/chapter2/4trap-handling.rst @@ -1,7 +1,624 @@ -处理 Trap -======================= +实现特权级的切换 +=========================== .. toctree:: :hidden: :maxdepth: 5 +本节导读 +------------------------------- + +由于有特权级机制的存在,应用程序在用户态特权级运行时,是无法直接通过函数调用访问处于内核态特权级的批处理操作系统内核中的函数的。所以会通过某种机制进行特权级之间的切换,使得用户态应用程序可以得到内核态操作系统函数的服务。本节将讲解在RISC-V 64处理器提供的U/S特权级下,批处理操作系统和应用程序如何相互配合,完成特权级切换的。 + +RISC-V特权级切换 +--------------------------------------- + +特权级切换的起因 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +我们知道,批处理操作系统被设计为运行在 S 模式,这是由RustSBI提供的 SEE(Supervisor Execution Environment) 所保证的;而应用程序被设计为运行在 U 模式,这个则是由我们的批处理操作系统提供的 AEE(Application Execution Environment) +所保证的。批处理操作系统为了建立好应用程序的执行环境,需要在执行应用程序之前进行一些初始化工作,并监控应用程序的执行,具体体现在: + +- 当启动应用程序的时候,需要初始化应用程序的用户态上下文,并能切换到用户态执行应用程序; +- 当应用程序发起系统调用(即发出Trap )之后,需要到批处理操作系统中进行处理; +- 当应用程序执行出错的时候,需要到批处理操作系统中杀死该应用并加载运行下一个应用; +- 当应用程序执行结束的时候,需要到批处理操作系统中加载运行下一个应用(实际上也是通过系统调用 ``sys_exit`` 来实现的)。 + +这些处理都涉及到特权级切换,因此都需要硬件和操作系统协同提供的特权级切换机制。 + + +特权级切换相关的控制状态寄存器 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +当从一般意义上讨论 RISC-V 架构的 Trap 机制时,通常需要注意两点: + +- 在触发 Trap 之前 CPU 运行在哪个特权级; +- 以及 CPU 需要切换到哪个特权级来处理该 Trap 并在处理完成之后返回原特权级。 + +但本章中我们仅考虑当 CPU 在 U 特权级运行用户程序的时候触发 Trap,并切换到 S 特权级的批处理操作系统的对应服务代码来进行处理。 + + +.. _term-s-mod-csr: + +在 RISC-V 架构中,关于 Trap 有一条重要的规则:在 Trap 前的特权级不会高于Trap后的特权级。因此如果触发 Trap 之后切换到 S 特权级(下称 Trap 到 S), +说明 Trap 发生之前 CPU 只能运行在 S/U 特权级。但无论如何,只要是 Trap 到 S 特权级,操作系统就会使用 S 特权级中与 Trap 相关的 **控制状态寄存器** (CSR, Control and Status Register) 来辅助 Trap +处理。我们在编写运行在 S 特权级的批处理操作系统中的 Trap 处理相关代码的时候,就需要使用如下所示的S模式的CSR寄存器。 + +.. list-table:: 进入 S 特权级 Trap 的相关 CSR + :header-rows: 1 + :align: center + :widths: 30 100 + + * - CSR 名 + - 该 CSR 与 Trap 相关的功能 + * - sstatus + - ``SPP`` 等字段给出 Trap 发生之前 CPU 处在哪个特权级(S/U)等信息 + * - sepc + - 当 Trap 是一个异常的时候,记录 Trap 发生之前执行的最后一条指令的地址 + * - scause + - 描述 Trap 的原因 + * - stval + - 给出 Trap 附加信息 + * - stvec + - 控制 Trap 处理代码的入口地址 + +.. note:: + + **S模式下最重要的 sstatus 寄存器** + + 注意 ``sstatus`` 是 S 特权级最重要的 CSR,可以从很多方面控制 S 特权级的CPU行为和执行状态。 + +.. chy + 我们在这里先给出它在 Trap 处理过程中的作用。 + +特权级切换 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +大多数的 Trap (陷入) 发生的场景都是在执行某条指令(如 ``ecall`` )之后,CPU 发现触发了一个 Trap并需要进行特殊处理,并涉及到 :ref:`执行环境切换 ` 。具体而言,用户态执行环境中的应用程序通过 ``ecall`` 指令 +向内核态执行环境在的操作系统请求某项服务功能,那么处理器和操作系统会完成到内核态执行环境的切换,并在操作系统完成服务后,再次切换到用户态执行环境,然后应用程序会紧接着``ecall`` 指令的后一条指令位置处继续执行,参考 :ref:`图示 ` 。 + +.. chy ???: 这条触发 Trap 的指令和进入 Trap 之前执行的最后一条指令不一定是同一条。 + + + +.. chy??? 下面的内容合并到第零章的os 抽象一节中, 执行环境切换, context等 + _term-execution-of-thread: + 回顾第一章的 :ref:`函数调用与栈 ` ,我们知道在一个固定的 CPU 上,只要有一个栈作为存储空间,我们就能以多种 + 普通控制流(顺序、分支、循环结构和多层嵌套函数调用)组合的方式,来一行一行的执行源代码(以编程语言级的视角),也是一条一条的执行汇编指令 + (以汇编语言级的视角)。只考虑普通控制流,那么从某条指令开始记录,该 CPU 可用的所有资源,包括自带的所有通用寄存器(包括虚拟的描述当前执行 + 指令地址的寄存器 pc )和当前特权级可用的 CSR 以及位于内存中的一块栈空间,它们会随着指令的执行而逐渐发生变化。这种局限在普通控制流(相对于 :ref:`异常控制流 ` 而言)之内的 + 连续指令执行和与之同步的对相关资源的改变我们用一个新名词 **执行流** (Execution Flow) 来命名。执行流的状态是一个由它衍生出来的 + 概念,表示截止到某条指令执行完毕所有相关资源(包括寄存器、栈)的状态集合,它完整描述了自记录起始之后该执行流的指令执行历史。 + .. note:: + 实际上 CPU 还有其他资源可用: + - 内存除了与执行流绑定的栈之外的其他存储空间,比如程序中的数据段; + - 外围 I/O 设备。 + 它们也会在执行期间动态发生变化。但它们可能由多条执行流共享,难以清晰的从中单独区分出某一条执行流的状态变化。因此在执行流概念中, + 我们不将其纳入考虑。 + +.. chy??? 内容与第一段有重复 + 让我们通过从 U 特权级 Trap 到 S 特权级的切换过程(这是一个特例,实际上在 Trap 的前后,特权级也可以不变)来分析一下在 Trap 前后发生了哪些事情。首先假设CPU 正处在 U 特权级跑着应用程序的代码。在执行完某一条指令之后, CPU 发现一个 + 中断/异常被触发,于是必须将应用执行流暂停,先 Trap 到更高的 S 特权级去执行批处理操作系统提供的相应服务代码,等待执行 + 完了之后再回过头来恢复到并继续执行应用程序的执行流。 +.. chy??? 内容与第一段有重复 + 我们可以将 CPU 在 S 特权级执行的那一段指令序列也看成一个控制流,因为它全程只是以普通控制流的模式在 S 特权级执行。这个控制流的意义就在于 + 处理 Trap ,我们可以将其称之为 **Trap 专用** 控制流,它在 Trap 触发的时候开始,并于 Trap 处理完毕之后结束。于是我们可以从执行环境切换和控制流的角度来看待 + 操作系统对Trap 的整个处理过程: CPU 从用户态执行环境中的应用程序的普通控制流转到内核态执行环境中的 Trap 执行流,然后再切换回去继续运行应用程序。站在应用程序的角度, 由操作系统和CPU协同完成的Trap 机制对它是完全透明的,无论应用程序在它的哪一条指令执行结束后进入 Trap ,它总是相信在 Trap 结束之后 CPU 能够在与被打断的时候"相同"的执行环境中继续正确的运行应用程序的指令。 +.. chy??? + .. note:: +.. chy??? 内容与第一段有重复 + 这里所说的相同并不是绝对相同,但是其变化是完全能够被应用程序预知到的。比如应用程序通过 ``ecall`` 指令请求底层高特权级软件的功能, + 由调用规范它知道 Trap 之后 ``a0~a1`` 两个寄存器会被用来保存返回值,所以会发生变化。这个信息是应用程序明确知晓的,但某种程度上 + 确实也体现了执行流的变化。 + +应用程序被切换回来之后需要从暂停的位置恢复并继续执行,这需要在切换前后维持应用程序的上下文保持不变。应用程序的上下文可以分为通用寄存器和栈两部分。 +由于每个 CPU 在不同特权级下共享一套通用寄存器,所以在运行操作系统的 Trap +处理过程中,操作系统也会用到这些寄存器,这将应用程序的上下文。因此,就和函数调用需要保存函数调用上下文/活动记录一样,在执行操作系统的 Trap 处理过程的最开始, +即修改这些寄存器之前,我们需要在某个地方(就是某内存块或内核的栈)保存这些寄存器并在后续恢复这些寄存器。 + +除了通用寄存器之外还有一些可能在处理 Trap 过程中会被修改的 +CSR,比如 CPU 所在的特权级。我们要保证它们的变化在我们的预期之内,比如对于特权级而言应该是 Trap 之前在 U 特权级,处理 Trap 的时候在 S +特权级,返回之后又需要回到 U 特权级。而对于栈问题则相对简单,只要两个执行流用来记录执行历史的栈所对应的内存区域不相交,就不会产生令我们 +头痛的覆盖问题,也就无需进行保存/恢复。 + +执行流切换的相关机制一部分由硬件帮我们完成,另一部分则需要由操作系统来实现。 + +.. _trap-hw-mechanism: + +特权级切换的硬件控制机制 +------------------------------------- + +当 CPU 执行完一条指令并准备从用户特权级 Trap 到 S 特权级的时候,硬件会自动帮我们做这些事情: + +- ``sstatus`` 的 ``SPP`` 字段会被修改为 CPU 当前的特权级(U/S)。 +- ``sepc`` 会被修改为 Trap 回来之后默认会执行的下一条指令的地址。当 Trap 是一个异常的时候,它实际会被修改成 Trap 之前执行的最后一条 + 指令的地址。 +- ``scause/stval`` 分别会被修改成这次 Trap 的原因以及相关的附加信息。 +- CPU 会跳转到 ``stvec`` 所设置的 Trap 处理入口地址,并将当前特权级设置为 S ,然后开始向下执行。 + +.. note:: + + **stvec 相关细节** + + 在 RV64 中, ``stvec`` 是一个 64 位的 CSR,在中断使能的情况下,保存了中断处理的入口地址。它有两个字段: + + - MODE 位于 [1:0],长度为 2 bits; + - BASE 位于 [63:2],长度为 62 bits。 + + 当 MODE 字段为 0 的时候, ``stvec`` 被设置为 Direct 模式,此时进入 S 模式的 Trap 无论原因如何,处理 Trap 的入口地址都是 ``BASE<<2`` + , CPU 会跳转到这个地方进行异常处理。本书中我们只会将 ``stvec`` 设置为 Direct 模式。而 ``stvec`` 还可以被设置为 Vectored 模式, + 有兴趣的读者可以自行参考 RISC-V 指令集特权级规范。 + +而当 CPU 完成 Trap 处理准备返回的时候,需要通过一条 S 特权级的特权指令 ``sret`` 来完成,这一条指令具体完成以下功能: + +- CPU 会将当前的特权级按照 ``sstatus`` 的 ``SPP`` 字段设置为 U 或者 S ; +- CPU 会跳转到 ``sepc`` 寄存器指向的那条指令,然后开始向下执行。 + +从上面可以看出硬件主要负责特权级切换、跳转到异常处理入口地址(要在使能异常/中断前设置好)以及在 CSR 中保存一些只有硬件才方便探测到的硬件内的 Trap +相关信息。这基本上都是硬件不得不完成的事情,剩下的工作都交给软件,让软件能有更大的灵活性。 + +用户栈与内核栈 +-------------------------------- + +在 Trap 触发的一瞬间, CPU 就会切换到 S 特权级并跳转到 ``stvec`` 所指示的位置。但是在正式进入 S 特权级的 Trap 处理之前,上面 +提到过我们必须保存原执行流的寄存器状态,这一般通过栈来完成。但我们需要用专门为操作系统准备的内核栈,而不是应用程序运行时用到的用户栈。 + +.. + chy:我们在一个作为用户栈的特别留出的内存区域上保存应用程序的栈信息,而 Trap 执行流则使用另一个内核栈。 + +使用两个不同的栈是为了安全性:如果两个执行流使用同一个栈,在返回之后应用程序就有能力看到 Trap 执行流的 +历史信息,比如内核一些函数的地址,这样会带来安全隐患。于是,我们要做的是,在批处理操作系统中加入一段汇编代码中,实现从用户栈切换到内核栈, +并在内核栈上保存应用程序执行流的寄存器状态。 + +我们声明两个类型 ``KernelStack`` 和 ``UserStack`` 分别表示用户栈和内核栈,它们都只是字节数组的简单包装: + +.. code-block:: rust + :linenos: + + // os/src/batch.rs + + const USER_STACK_SIZE: usize = 4096 * 2; + const KERNEL_STACK_SIZE: usize = 4096 * 2; + + #[repr(align(4096))] + struct KernelStack { + data: [u8; KERNEL_STACK_SIZE], + } + + #[repr(align(4096))] + struct UserStack { + data: [u8; USER_STACK_SIZE], + } + + static KERNEL_STACK: KernelStack = KernelStack { data: [0; KERNEL_STACK_SIZE] }; + static USER_STACK: UserStack = UserStack { data: [0; USER_STACK_SIZE] }; + +常数 ``USER_STACK_SIZE`` 和 ``KERNEL_STACK_SIZE`` 指出内核栈和用户栈的大小分别为 :math:`8\text{KiB}` 。两个类型是以全局变量 +的形式实例化在批处理操作系统的 ``.bss`` 段中的。 + +我们为两个类型实现了 ``get_sp`` 方法来获取栈顶地址。由于在 RISC-V 中栈是向下增长的,我们只需返回包裹的数组的终止地址,以用户栈 +类型 ``UserStack`` 为例: + +.. code-block:: rust + :linenos: + + impl UserStack { + fn get_sp(&self) -> usize { + self.data.as_ptr() as usize + USER_STACK_SIZE + } + } + +于是换栈是非常简单的,只需将 ``sp`` 寄存器的值修改为 ``get_sp`` 的返回值即可。 + +.. _term-trap-context: + +接下来是Trap上下文(即数据结构 ``TrapContext`` ),类似前面提到的函数调用上下文,即在 Trap 发生时需要保存的物理资源内容,并将其一起放在一个名为 +``TrapContext`` 的类型中,定义如下: + +.. code-block:: rust + :linenos: + + // os/src/trap/context.rs + + #[repr(C)] + pub struct TrapContext { + pub x: [usize; 32], + pub sstatus: Sstatus, + pub sepc: usize, + } + +可以看到里面包含所有的通用寄存器 ``x0~x31`` ,还有 ``sstatus`` 和 ``sepc`` 。那么为什么需要保存它们呢? + +- 对于通用寄存器而言,两条执行流运行在不同的特权级,所属的软件也可能由不同的编程语言编写,虽然在 Trap 控制流中只是会执行 Trap 处理 + 相关的代码,但依然可能直接或间接调用很多模块,因此很难甚至不可能找出哪些寄存器无需保存。既然如此我们就只能全部保存了。但这里也有一些例外, + 如 ``x0`` 被硬编码为 0 ,它自然不会有变化;还有 ``tp(x4)`` 除非我们手动出于一些特殊用途使用它,否则一般也不会被用到。它们无需保存, + 但我们仍然在 ``TrapContext`` 中为它们预留空间,主要是为了后续的实现方便。 +- 对于 CSR 而言,我们知道进入 Trap 的时候,硬件会立即覆盖掉 ``scause/stval/sstatus/sepc`` 的全部或是其中一部分。``scause/stval`` + 的情况是:它总是在 Trap 处理的第一时间就被使用或者是在其他地方保存下来了,因此它没有被修改并造成不良影响的风险。 + 而对于 ``sstatus/sepc`` 而言,它们会在 Trap 处理的全程有意义(在 Trap 执行流最后 ``sret`` 的时候还用到了它们),而且确实会出现 + Trap 嵌套的情况使得它们的值被覆盖掉。所以我们需要将它们也一起保存下来,并在 ``sret`` 之前恢复原样。 + + +Trap 管理 +------------------------------- + +特权级切换的核心是对Trap的管理。这主要涉及到如下一下内容: + +- 应用程序通过 ``ecall`` 进入到内核状态时,操作系统保存被打断的应用程序的Trap 上下文。 +- 操作系统根据与Trap相关的CSR寄存器内容,完成系统调用服务的分发与处理。 +- 操作系统完成系统调用服务后,需要恢复被打断的应用程序的Trap 上下文,并通 ``sret`` 让应用程序继续执行。 + +接下来我们具体介绍上述内容。 + +Trap 上下文的保存与恢复 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +首先是具体实现 Trap 上下文保存和恢复的汇编代码。 + +.. _trap-context-save-restore: + + +在批处理操作系统初始化的时候,我们需要修改 ``stvec`` 寄存器来指向正确的 Trap 处理入口点。 + +.. code-block:: rust + :linenos: + + // os/src/trap/mod.rs + + global_asm!(include_str!("trap.S")); + + pub fn init() { + extern "C" { fn __alltraps(); } + unsafe { + stvec::write(__alltraps as usize, TrapMode::Direct); + } + } + +这里我们引入了一个外部符号 ``__alltraps`` ,并将 ``stvec`` 设置为 Direct 模式指向它的地址。我们在 ``os/src/trap/trap.S`` +中实现 Trap 上下文保存/恢复的汇编代码,分别用外部符号 ``__alltraps`` 和 ``__restore`` 标记,并将这段汇编代码中插入进来。 + +Trap 处理的总体流程如下:首先通过 ``__alltraps`` 将 Trap 上下文保存在内核栈上,然后跳转到使用 Rust 编写的 ``trap_handler`` 函数 +完成 Trap 分发及处理。当 ``trap_handler`` 返回之后,使用 ``__restore`` 从保存在内核栈上的 Trap 上下文恢复寄存器。最后通过一条 +``sret`` 指令回到应用程序执行。 + +首先是保存 Trap 上下文的 ``__alltraps`` 的实现: + +.. code-block:: riscv + :linenos: + + # os/src/trap/trap.S + + .macro SAVE_GP n + sd x\n, \n*8(sp) + .endm + + .align 2 + __alltraps: + csrrw sp, sscratch, sp + # now sp->kernel stack, sscratch->user stack + # allocate a TrapContext on kernel stack + addi sp, sp, -34*8 + # save general-purpose registers + sd x1, 1*8(sp) + # skip sp(x2), we will save it later + sd x3, 3*8(sp) + # skip tp(x4), application does not use it + # save x5~x31 + .set n, 5 + .rept 27 + SAVE_GP %n + .set n, n+1 + .endr + # we can use t0/t1/t2 freely, because they were saved on kernel stack + csrr t0, sstatus + csrr t1, sepc + sd t0, 32*8(sp) + sd t1, 33*8(sp) + # read user stack from sscratch and save it on the kernel stack + csrr t2, sscratch + sd t2, 2*8(sp) + # set input argument of trap_handler(cx: &mut TrapContext) + mv a0, sp + call trap_handler + +- 第 7 行我们使用 ``.align`` 将 ``__alltraps`` 的地址 4 字节对齐,这是 RISC-V 特权级规范的要求; +- 第 8 行的 ``csrrw`` 原型是 :math:`\text{csrrw rd, csr, rs}` 可以将 CSR 当前的值读到通用寄存器 :math:`\text{rd}` 中,然后将 + 通用寄存器 :math:`\text{rs}` 的值写入该 CSR 。因此这里起到的是交换 sscratch 和 sp 的效果。在这一行之前 sp 指向用户栈, sscratch + 指向内核栈(原因稍后说明),现在 sp 指向内核栈, sscratch 指向用户栈。 +- 第 12 行,我们准备在内核栈上保存 Trap 上下文,于是预先分配 :math:`34\times 8` 字节的栈帧,这里改动的是 sp ,说明确实是在内核栈上。 +- 第 13~24 行,保存 Trap 上下文的通用寄存器 x0~x31,跳过 x0 和 tp(x4),原因之前已经说明。我们在这里也不保存 sp(x2),因为我们要基于 + 它来找到每个寄存器应该被保存到的正确的位置。实际上,在栈帧分配之后,我们可用于保存 Trap 上下文的地址区间为 :math:`[\text{sp},\text{sp}+8\times34)` , + + 按照 ``TrapContext`` 结构体的内存布局,它从低地址到高地址分别按顺序放置 x0~x31,最后是 sstatus 和 sepc 。因此通用寄存器 xn + 应该被保存在地址区间 :math:`[\text{sp}+8n,\text{sp}+8(n+1))` 。 在这里我们正是这样基于 sp 来保存这些通用寄存器的。 + + 为了简化代码,x5~x31 这 27 个通用寄存器我们通过类似循环的 ``.rept`` 每次使用 ``SAVE_GP`` 宏来保存,其实质是相同的。注意我们需要在 + ``Trap.S`` 开头加上 ``.altmacro`` 才能正常使用 ``.rept`` 命令。 +- 第 25~28 行,我们将 CSR sstatus 和 sepc 的值分别读到寄存器 t0 和 t1 中然后保存到内核栈对应的位置上。指令 + :math:`\text{csrr rd, csr}` 的功能就是将 CSR 的值读到寄存器 :math:`\text{rd}` 中。这里我们不用担心 t0 和 t1 被覆盖, + 因为它们刚刚已经被保存了。 +- 第 30~31 行专门处理 sp 的问题。首先将 sscratch 的值读到寄存器 t2 并保存到内核栈上,注意它里面是进入 Trap 之前的 sp 的值,指向 + 用户栈。而现在的 sp 则指向内核栈。 +- 第 33 行令 :math:`\text{a}_0\leftarrow\text{sp}`,让寄存器 a0 指向内核栈的栈指针也就是我们刚刚保存的 Trap 上下文的地址, + 这是由于我们接下来要调用 ``trap_handler`` 进行 Trap 处理,它的第一个参数 ``cx`` 由调用规范要从 a0 中获取。而 Trap 处理函数 + ``trap_handler`` 需要 Trap 上下文的原因在于:它需要知道其中某些寄存器的值,比如在系统调用的时候应用程序传过来的 syscall ID 和 + 对应参数。我们不能直接使用这些寄存器现在的值,因为它们可能已经被修改了,因此要去内核栈上找已经被保存下来的值。 + + +.. _term-atomic-instruction: + +.. note:: + + **CSR 相关原子指令** + + RISC-V 中读写 CSR 的指令通常都能只需一条指令就能完成多项功能。这样的指令被称为 **原子指令** (Atomic Instruction)。这里 + 的原子的含义是“不可分割的最小个体”,也就是说指令的多项功能要么都不完成,要么全部完成,而不会处于某种中间状态。 + +当 ``trap_handler`` 返回之后会从调用 ``trap_handler`` 的下一条指令开始执行,也就是从栈上的 Trap 上下文恢复的 ``__restore`` : + +.. _code-restore: + +.. code-block:: riscv + :linenos: + + .macro LOAD_GP n + ld x\n, \n*8(sp) + .endm + + __restore: + # case1: start running app by __restore + # case2: back to U after handling trap + mv sp, a0 + # now sp->kernel stack(after allocated), sscratch->user stack + # restore sstatus/sepc + ld t0, 32*8(sp) + ld t1, 33*8(sp) + ld t2, 2*8(sp) + csrw sstatus, t0 + csrw sepc, t1 + csrw sscratch, t2 + # restore general-purpuse registers except sp/tp + ld x1, 1*8(sp) + ld x3, 3*8(sp) + .set n, 5 + .rept 27 + LOAD_GP %n + .set n, n+1 + .endr + # release TrapContext on kernel stack + addi sp, sp, 34*8 + # now sp->kernel stack, sscratch->user stack + csrrw sp, sscratch, sp + sret + +- 第 8 行比较奇怪我们暂且不管,假设它从未发生,那么 sp 仍然指向内核栈的栈顶。 +- 第 11~24 行负责从内核栈顶的 Trap 上下文恢复通用寄存器和 CSR 。注意我们要先恢复 CSR 再恢复通用寄存器,这样我们使用的三个临时寄存器 + 才能被正确恢复。 +- 在第 26 行之前,sp 指向保存了 Trap 上下文之后的内核栈栈顶, sscratch 指向用户栈栈顶。我们在第 26 行在内核栈上回收 Trap 上下文所 + 占用的内存,回归进入 Trap 之前的内核栈栈顶。第 27 行,再次交换 sscratch 和 sp,现在 sp 重新指向用户栈栈顶,sscratch 也依然保存 + 进入 Trap 之前的状态并指向内核栈栈顶。 +- 在应用程序执行流状态被还原之后,第 28 行我们使用 ``sret`` 指令回到 U 特权级继续运行应用程序执行流。 + +.. note:: + + **sscratch CSR 的用途** + + 在特权级切换的时候,我们需要将 Trap 上下文保存在内核栈上,因此需要一个寄存器暂存内核栈地址,并以它作为基地址来依次保存 Trap 上下文 + 的内容。但是所有的通用寄存器都不能够用来暂存,因为它们都需要被保存,如果覆盖掉它们会影响应用执行流的执行。 + + 事实上我们缺少了一个重要的中转寄存器,而 ``sscratch`` CSR 正是为此而生。从上面的汇编代码中可以看出,在保存 Trap 上下文的时候,它 + 起到了两个作用:首先是保存了内核栈的地址,其次它作为一个中转站让 sp 目前指向的用户栈的地址可以暂时保存下来。于是,我们仅需一条 + ``csrrw`` 指令就完成了从用户栈到内核栈的切换,这是一种极其精巧的实现。 + +Trap 分发与处理 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Trap 在使用 Rust 实现的 ``trap_handler`` 函数中完成分发和处理: + +.. code-block:: rust + :linenos: + + // os/src/trap/mod.rs + + #[no_mangle] + pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext { + let scause = scause::read(); + let stval = stval::read(); + match scause.cause() { + Trap::Exception(Exception::UserEnvCall) => { + cx.sepc += 4; + cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize; + } + Trap::Exception(Exception::StoreFault) | + Trap::Exception(Exception::StorePageFault) => { + println!("[kernel] PageFault in application, core dumped."); + run_next_app(); + } + Trap::Exception(Exception::IllegalInstruction) => { + println!("[kernel] IllegalInstruction in application, core dumped."); + run_next_app(); + } + _ => { + panic!("Unsupported trap {:?}, stval = {:#x}!", scause.cause(), stval); + } + } + cx + } + +- 第 4 行声明返回值为 ``&mut TrapContext`` 并在第 25 行实际将传入的 ``cx`` 原样返回,因此在 ``__restore`` 的时候 a0 在调用 + ``trap_handler`` 前后并没有发生变化,仍然指向分配 Trap 上下文之后的内核栈栈顶,和此时 sp 的值相同,我们 :math:`\text{sp}\leftarrow\text{a}_0` + 并不会有问题; +- 第 7 行根据 scause 寄存器所保存的 Trap 的原因进行分发处理。这里我们无需手动操作这些 CSR ,而是使用 Rust 的 riscv 库来更加方便的 + 做这些事情。要引入 riscv 库,我们需要: + + .. code-block:: toml + + # os/Cargo.toml + + [dependencies] + riscv = { git = "https://github.com/rcore-os/riscv", features = ["inline-asm"] } + +- 第 8~11 行,发现 Trap 的原因是来自 U 特权级的 Environment Call,也就是系统调用。这里我们首先修改保存在内核栈上的 Trap 上下文里面 + sepc,让其增加 4。这是因为我们知道这是一个由 ``ecall`` 指令触发的系统调用,在进入 Trap 的时候,硬件会将 sepc 设置为这条 ``ecall`` + 指令所在的地址(因为它是进入 Trap 之前最后一条执行的指令)。而在 Trap 返回之后,我们希望应用程序执行流从 ``ecall`` 的下一条指令 + 开始执行。因此我们只需修改 Trap 上下文里面的 sepc,让它增加 ``ecall`` 指令的码长,也即 4 字节。这样在 ``__restore`` 的时候 sepc + 在恢复之后就会指向 ``ecall`` 的下一条指令,并在 ``sret`` 之后从那里开始执行。这属于我们之前提到过的——用户程序能够预知到的执行流 + 状态所发生的变化。 + + 用来保存系统调用返回值的 a0 寄存器也会同样发生变化。我们从 Trap 上下文取出作为 syscall ID 的 a7 和系统调用的三个参数 a0~a2 传给 + ``syscall`` 函数并获取返回值。 ``syscall`` 函数是在 ``syscall`` 子模块中实现的。 +- 第 12~20 行,分别处理应用程序出现访存错误和非法指令错误的情形。此时需要打印错误信息并调用 ``run_next_app`` 直接切换并运行下一个 + 应用程序。 +- 第 21 行开始,当遇到目前还不支持的 Trap 类型的时候,我们的批处理操作系统整个 panic 报错退出。 + +对于系统调用而言, ``syscall`` 函数并不会实际处理系统调用而只是会根据 syscall ID 分发到具体的处理函数: + +.. code-block:: rust + :linenos: + + // os/src/syscall/mod.rs + + pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize { + match syscall_id { + SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]), + SYSCALL_EXIT => sys_exit(args[0] as i32), + _ => panic!("Unsupported syscall_id: {}", syscall_id), + } + } + +这里我们会将传进来的参数 ``args`` 转化成能够被具体的系统调用处理函数接受的类型。它们的实现都非常简单: + +.. code-block:: rust + :linenos: + + // os/src/syscall/fs.rs + + const FD_STDOUT: usize = 1; + + pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize { + match fd { + FD_STDOUT => { + let slice = unsafe { core::slice::from_raw_parts(buf, len) }; + let str = core::str::from_utf8(slice).unwrap(); + print!("{}", str); + len as isize + }, + _ => { + panic!("Unsupported fd in sys_write!"); + } + } + } + + // os/src/syscall/process.rs + + pub fn sys_exit(xstate: i32) -> ! { + println!("[kernel] Application exited with code {}", xstate); + run_next_app() + } + +- ``sys_write`` 我们将传入的位于应用程序内的缓冲区的开始地址和长度转化为一个字符串 ``&str`` ,然后使用批处理操作系统已经实现的 ``print!`` + 宏打印出来。注意这里我们并没有检查传入参数的安全性,即使会在出错严重的时候 panic,还是会存在安全隐患。这里我们出于实现方便暂且不做修补。 +- ``sys_exit`` 打印退出的应用程序的返回值并同样调用 ``run_next_app`` 切换到下一个应用程序。 + +.. _ch2-app-execution: + +执行应用程序 +------------------------------------- + +当批处理操作系统初始化完成,或者是某个应用程序运行结束或出错的时候,我们要调用 ``run_next_app`` 函数切换到下一个应用程序。此时 CPU 运行在 +S 特权级,而它希望能够切换到 U 特权级。在 RISC-V 架构中,唯一一种能够使得 CPU 特权级下降的方法就是通过 Trap 返回系列指令,比如 +``sret`` 。事实上,在运行应用程序之前要完成如下这些工作: + +- 跳转到应用程序入口点 ``0x80400000``。 +- 将使用的栈切换到用户栈。 +- 在 ``__alltraps`` 时我们要求 ``sscratch`` 指向内核栈,这个也需要在此时完成。 +- 从 S 特权级切换到 U 特权级。 + +它们可以通过复用 ``__restore`` 的代码更容易的实现。我们只需要在内核栈上压入一个相应构造的 Trap 上下文,再通过 ``__restore`` ,就能 +让这些寄存器到达我们希望的状态。 + +.. code-block:: rust + :linenos: + + // os/src/trap/context.rs + + impl TrapContext { + pub fn set_sp(&mut self, sp: usize) { self.x[2] = sp; } + pub fn app_init_context(entry: usize, sp: usize) -> Self { + let mut sstatus = sstatus::read(); + sstatus.set_spp(SPP::User); + let mut cx = Self { + x: [0; 32], + sstatus, + sepc: entry, + }; + cx.set_sp(sp); + cx + } + } + +为 ``TrapContext`` 实现 ``app_init_context`` 方法,修改其中的 sepc 寄存器为应用程序入口点 ``entry``, sp 寄存器为我们设定的 +一个栈指针,并将 sstatus 寄存器的 ``SPP`` 字段设置为 User 。 + +在 ``run_next_app`` 函数中我们能够看到: + +.. code-block:: rust + :linenos: + :emphasize-lines: 10,11,12,13,14 + + // os/src/batch.rs + + pub fn run_next_app() -> ! { + let current_app = APP_MANAGER.inner.borrow().get_current_app(); + unsafe { + APP_MANAGER.inner.borrow().load_app(current_app); + } + APP_MANAGER.inner.borrow_mut().move_to_next_app(); + extern "C" { fn __restore(cx_addr: usize); } + unsafe { + __restore(KERNEL_STACK.push_context( + TrapContext::app_init_context(APP_BASE_ADDRESS, USER_STACK.get_sp()) + ) as *const _ as usize); + } + panic!("Unreachable in batch::run_current_app!"); + } + +在高亮行所做的事情是在内核栈上压入一个 Trap 上下文,其 sepc 是应用程序入口地址 ``0x80400000`` ,其 sp 寄存器指向用户栈,其 sstatus +的 ``SPP`` 字段被设置为 User 。``push_context`` 的返回值是内核栈压入 Trap 上下文之后的栈顶,它会被作为 ``__restore`` 的参数( +回看 :ref:`__restore 代码 ` ,这时我们可以理解为何 ``__restore`` 的开头会做 +:math:`\text{sp}\leftarrow\text{a}_0` )使得在 ``__restore`` 中 sp 仍然可以指向内核栈的栈顶。这之后,就和一次普通的 +``__restore`` 一样了。 + +.. note:: + + 有兴趣的读者可以思考: sscratch 是何时被设置为内核栈顶的? + + + +.. + 马老师发生甚么事了? + -- + 这里要说明目前只考虑从 U Trap 到 S ,而实际上 Trap 的要素就有:Trap 之前在哪个特权级,Trap 在哪个特权级处理。这个对于中断和异常 + 都是如此,只不过中断可能跟特权级的关系稍微更紧密一点。毕竟中断的类型都是跟特权级挂钩的。但是对于 Trap 而言有一点是共同的,也就是触发 + Trap 不会导致优先级下降。从中断/异常的代理就可以看出从定义上就不允许代理到更低的优先级。而且代理只能逐级代理,目前我们能操作的只有从 + M 代理到 S,其他代理都基本只出现在指令集拓展或者硬件还不支持。中断的情况是,如果是属于某个特权级的中断,不能在更低的优先级处理。事实上 + 这个中断只可能在 CPU 处于不会更高的优先级上收到(否则会被屏蔽),而 Trap 之后优先级不会下降(Trap 代理机制决定),这样就自洽了。 + -- + 之前提到异常是说需要执行环境功能的原因与某条指令的执行有关。而 Trap 的定义更加广泛一些,就是在执行某条指令之后发现需要执行环境的功能, + 如果是中断的话 Trap 回来之后默认直接执行下一条指令,如果是异常的话硬件会将 sepc 设置为 Trap 发生之前最后执行的那条指令,而异常发生 + 的原因不一定和这条指令的执行有关。应该指出的是,在大多数情况下都是和最后这条指令的执行有关。但在缓存的作用下也会出现那种特别极端的情况。 + -- + 然后是 Trap 到 S,就有 S 模式的一些相关 CSR,以及从 U Trap 到 S,硬件会做哪些事情(包括触发异常的一瞬间,以及处理完成调用 sret + 之后)。然后指出从用户的视角来看,如果是 ecall 的话, Trap 回来之后应该从 ecall 的下一条指令开始执行,且执行现场不能发生变化。 + 所以就需要将应用执行环境保存在内核栈上(还需要换栈!)。栈存在的原因可能是 Trap handler 是一条新的运行在 S 特权级的执行流,所以 + 这个可以理解成跨特权级的执行流切换,确实就复杂一点,要保存的内容也相对多一点。而下一章多任务的任务切换是全程发生在 S 特权级的执行流 + 切换,所以会简单一点,保存的通用寄存器大概率更少(少在调用者保存寄存器),从各种意义上都很像函数调用。从不同特权级的角度来解释换栈 + 是出于安全性,应用不应该看到 Trap 执行流的栈,这样做完之后,虽然理论上可以访问,但应用不知道内核栈的位置应该也有点麻烦。 + -- + 然后是 rust_trap 的处理,尤其是奇妙的参数传递,内部处理逻辑倒是非常简单。 + -- + 最后是如何利用 __restore 初始化应用的执行环境,包括如何设置入口点、用户栈以及保证在 U 特权级执行。 + + + + + diff --git a/source/chapter2/5exercise.rst b/source/chapter2/5exercise.rst new file mode 100644 index 0000000000000000000000000000000000000000..080cdde47050dcf2d9e1197f9f33889441a2797a --- /dev/null +++ b/source/chapter2/5exercise.rst @@ -0,0 +1,135 @@ +chapter2练习 +===================================================== + +.. toctree:: + :hidden: + :maxdepth: 4 + +- 本节难度: **低** + +编程练习 +------------------------------- + +简单安全检查 ++++++++++++++++++++++++++++++++ + +lab2 中,我们实现了第一个系统调用 ``sys_write``,这使得我们可以在用户态输出信息。但是 os 在提供服务的同时,还有保护 os 本身以及其他用户程序不受错误或者恶意程序破坏的功能。 + +由于还没有实现虚拟内存,我们可以在用户程序中指定一个属于其他程序字符串,并将它输出,这显然是不合理的,因此我们要对 sys_write 做检查: + +- sys_write 仅能输出位于程序本身内存空间内的数据,否则报错。 + +实验要求 ++++++++++++++++++++++++++++++++ +- 实现分支: ch2。 +- 完成实验指导书中的内容,能运行用户态程序并执行 sys_write,sys_exit 系统调用。 +- 为 sys_write 增加安全性检查,并通过 `Rust测例 `_ 中 chapter2 对应的所有测例,测例详情见对应仓库。 + +challenge: 支持多核,实现多个核运行用户程序。 + +.. _inherit-last-ch-changes: + +.. note:: + + **如何快速继承上一章练习题的修改** + + 从这一章开始,在完成本章习题之前,首先要做的就是将上一章框架的修改继承到本章的框架代码。出于各种原因,实际上通过 ``git merge`` 并不是很方便,这里给出一种打 patch 的方法,希望能够有所帮助。 + + 1. 切换到上一章的分支,通过 ``git log`` 找到你在此分支上的第一次 commit 的前一个 commit 的 ID ,复制其前 8 位,记作 ``base-commit`` 。假设分支上最新的一次 commit ID 是 ``last-commit`` 。 + 2. 确保你位于项目根目录 ``rCore-Tutorial-v3`` 下。通过 ``git diff > `` 即可在 ``patch-path`` 路径位置(比如 ``~/Desktop/chx.patch`` )生成一个描述你对于上一章分支进行的全部修改的一个补丁文件。打开看一下,它给出了每个被修改的文件中涉及了哪些块的修改,还附加了块前后的若干行代码。如果想更加灵活进行合并的话,可以通过 ``git format-patch `` 命令在当前目录下生成一组补丁,它会对于 ``base-commit`` 后面的每一次 commit 均按照顺序生成一个补丁。 + 3. 切换到本章分支,通过 ``git apply --reject `` 来将一个补丁打到当前章节上。它的大概原理是对于补丁中的每个被修改文件中的每个修改块,尝试通过块的前后若干行代码来定位它在当前分支上的位置并进行替换。有一些块可能无法匹配,此时会生成与这些块所在的文件同名的 ``*.rej`` 文件,描述了哪些块替换失败了。在项目根目录 ``rCore-Tutorial-v3`` 下,可以通过 ``find . -name *.rej`` 来找到所有相关的 ``*.rej`` 文件并手动完成替换。 + 4. 在处理完所有 ``*.rej`` 之后,将它们删除并 commit 一下。现在就可以开始本章的实验了。 + +实验检查 +++++++++++++++++++++++++++++++ + +- 实验目录要求(Rust) + +.. code-block:: + + ├── os(内核实现) + │   ├── build.rs (在这里实现用户程序的打包) + │   ├── Cargo.toml(配置文件) + │   ├── Makefile (要求 make run 可以正确执行,尽量不输出调试信息) + │ ├── build.rs (在这里实现用户程序的打包) + │   ├── src(所有内核的源代码放在 os/src 目录下) + │   ├── main.rs(内核主函数) + │   ├── ... + ├── reports + │   ├── lab2.md/pdf + │   └── ... + ├── README.md(其他必要的说明) + ├── ... + +参考示例目录结构。目标用户目录 ``../user/build/bin``。 + +- 检查 + +.. code-block:: console + + $ git checkout ch2 + $ cd os + $ make run + +可以正确执行正确执行目标用户测例,并得到预期输出(详见测例注释)。 + +注意:如果设置默认 log 等级,从 lab2 开始关闭所有 log 输出。 + +简答题 +------------------------------- + +1. 正确进入 U 态后,程序的特征还应有:使用 S 态特权指令,访问 S 态寄存器后会报错。目前由于一些其他原因,这些问题不太好测试,请同学们可以自行测试这些内容(参考 `前三个测例 `_ ),描述程序出错行为,同时注意注明你使用的 sbi 及其版本。 + +2. 请结合用例理解 `trap.S `_ 中两个函数 ``__alltraps`` 和 ``__restore`` 的作用,并回答如下几个问题: + + 1. L40: 刚进入 ``__restore`` 时,``a0`` 代表了什么值。请指出 ``__restore`` 的两种使用情景。 + + 2. L46-L51: 这几行汇编代码特殊处理了哪些寄存器?这些寄存器的的值对于进入用户态有何意义?请分别解释。 + + .. code-block:: riscv + + ld t0, 32*8(sp) + ld t1, 33*8(sp) + ld t2, 2*8(sp) + csrw sstatus, t0 + csrw sepc, t1 + csrw sscratch, t2 + + 3. L53-L59: 为何跳过了 ``x2`` 和 ``x4``? + + .. code-block:: riscv + + ld x1, 1*8(sp) + ld x3, 3*8(sp) + .set n, 5 + .rept 27 + LOAD_GP %n + .set n, n+1 + .endr + + 4. L63: 该指令之后,``sp`` 和 ``sscratch`` 中的值分别有什么意义? + + .. code-block:: riscv + + csrrw sp, sscratch, sp + + 5. ``__restore``:中发生状态切换在哪一条指令?为何该指令执行之后会进入用户态? + + 6. L13: 该指令之后,``sp`` 和 ``sscratch`` 中的值分别有什么意义? + + .. code-block:: riscv + + csrrw sp, sscratch, sp + + 7. 从 U 态进入 S 态是哪一条指令发生的? + +3. 程序陷入内核的原因有中断和异常(系统调用),请问 riscv64 支持哪些中断 / 异常?如何判断进入内核是由于中断还是异常?描述陷入内核时的几个重要寄存器及其值。 + +4. 对于任何中断, ``__alltraps`` 中都需要保存所有寄存器吗?你有没有想到一些加速 ``__alltraps`` 的方法?简单描述你的想法。 + +报告要求 +------------------------------- + +- 简单总结与上次实验相比本次实验你增加的东西(控制在5行以内,不要贴代码)。 +- 完成问答问题。 +- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 diff --git a/source/chapter2/ch2.py b/source/chapter2/ch2.py new file mode 100644 index 0000000000000000000000000000000000000000..8f8966f3fa2528d60975131868235d648157e698 --- /dev/null +++ b/source/chapter2/ch2.py @@ -0,0 +1,133 @@ +from manimlib.imports import * + +class EnvironmentCallFlow(Scene): + CONFIG = { + "camera_config": { + "background_color": WHITE, + }, + } + def construct(self): + os = Rectangle(height=FRAME_HEIGHT*.8, width=2.0, color=BLACK, fill_color=WHITE, fill_opacity=1.0) + app = Rectangle(height=FRAME_HEIGHT*.8, width=2.0, color=BLACK, fill_color=WHITE, fill_opacity=1.0) + app.shift(np.array([-4, 0, 0])) + see = Rectangle(height=FRAME_HEIGHT*.8, width=2.0, color=BLACK, fill_color=WHITE, fill_opacity=1.0) + see.shift(np.array([4, 0, 0])) + self.add(os, app, see) + os_text = TextMobject("OS", color=BLACK).next_to(os, UP, buff=0.2) + app_text = TextMobject("Application", color=BLACK).next_to(app, UP, buff=0.2) + see_text = TextMobject("SEE", color=BLACK).next_to(see, UP, buff=.2) + self.add(os_text, app_text, see_text) + app_ecall = Rectangle(height=0.5, width=2.0, color=BLACK, fill_color=BLUE, fill_opacity=1.0) + app_ecall.move_to(app) + app_ecall_text = TextMobject("ecall", color=BLACK).move_to(app_ecall) + self.add(app_ecall, app_ecall_text) + app_code1 = Rectangle(width=2.0, height=2.0, color=BLACK, fill_color=GREEN, fill_opacity=1.0) + app_code1.next_to(app_ecall, UP, buff=0) + self.add(app_code1) + app_code2 = Rectangle(width=2.0, height=2.5, color=BLACK, fill_color=GREEN, fill_opacity=1.0) + app_code2.next_to(app_ecall, DOWN, buff=0) + self.add(app_code2) + app_code1_text = TextMobject("U Code", color=BLACK).move_to(app_code1).shift(np.array([-.15, 0, 0])) + app_code2_text = TextMobject("U Code", color=BLACK).move_to(app_code2).shift(np.array([-.15, 0, 0])) + self.add(app_code1_text, app_code2_text) + os_ecall = Rectangle(height=.5, width=2.0, color=BLACK, fill_color=BLUE, fill_opacity=1.0) + os_ecall.move_to(os) + os_ecall_text = TextMobject("ecall", color=BLACK).move_to(os_ecall) + self.add(os_ecall, os_ecall_text) + os_code1 = Rectangle(width=2.0, height=2.0, color=BLACK, fill_color=PURPLE, fill_opacity=1.0).next_to(os_ecall, UP, buff=0) + os_code1_text = TextMobject("S Code", color=BLACK).move_to(os_code1).shift(np.array([-.15, 0, 0])) + os_code2 = Rectangle(width=2.0, height=2.5, color=BLACK, fill_color=PURPLE, fill_opacity=1.0).next_to(os_ecall, DOWN, buff=0) + os_code2_text = TextMobject("S Code", color=BLACK).move_to(os_code2).shift(np.array([-.15, 0, 0])) + self.add(os_code1, os_code2, os_code1_text, os_code2_text) + app_ecall_anchor = app_ecall.get_center() + np.array([0.8, 0, 0]) + app_front = Line(start=app_ecall_anchor+np.array([0, 2, 0]), end=app_ecall_anchor, color=RED) + app_front.add_tip(tip_length=0.2) + self.add(app_front) + os_ecall_anchor = os_ecall.get_center() + np.array([0.8, 0, 0]) + os_front = Line(start=os_ecall_anchor+np.array([0, 2, 0]), end=os_ecall_anchor, color=RED) + os_front.add_tip(tip_length=.2) + self.add(os_front) + trap_to_os = DashedLine(start=app_ecall_anchor, end=os_ecall_anchor+np.array([0, 2, 0]), color=RED) + trap_to_os.add_tip(tip_length=.2) + self.add(trap_to_os) + see_entry = see.get_center()+np.array([0.8, 2, 0]) + see_exit = see_entry+np.array([0, -4, 0]) + see_code = Rectangle(width=2.0, height=see_entry[1]-see_exit[1], color=BLACK, fill_color=GRAY, fill_opacity=1.0).move_to(see) + self.add(see_code) + see_text = TextMobject("M Code", color=BLACK).move_to(see_code).shift(np.array([-.15, 0, 0])) + self.add(see_text) + see_front = Line(start=see_entry, end=see_exit, color=RED).add_tip(tip_length=.2) + self.add(see_front) + trap_to_see = DashedLine(start=os_ecall_anchor, end=see_entry, color=RED).add_tip(tip_length=.2) + self.add(trap_to_see) + os_back_anchor = os_ecall_anchor+np.array([0, -.5, 0]) + trap_back_to_os = DashedLine(start=see_exit, end=os_back_anchor, color=RED).add_tip(tip_length=.2) + self.add(trap_back_to_os) + os_exit = os_back_anchor+np.array([0, -2, 0]) + os_front2 = Line(start=trap_back_to_os, end=os_exit, color=RED).add_tip(tip_length=.2) + self.add(os_front2) + app_back_anchor = app_ecall_anchor+np.array([0, -.5, 0]) + trap_back_to_app = DashedLine(start=os_exit, end=app_back_anchor, color=RED).add_tip(tip_length=.2) + self.add(trap_back_to_app) + app_front2 = Line(start=app_back_anchor, end=app_back_anchor+np.array([0, -2, 0]), color=RED) + app_front2.add_tip(tip_length=.2) + self.add(app_front2) + u_into_s = TextMobject("U into S", color=BLACK).next_to(app_ecall, RIGHT, buff=0).shift(np.array([0, .5, 0])).scale(0.5) + s_back_u = TextMobject("S back to U", color=BLACK).next_to(app_ecall, RIGHT, buff=0).shift(np.array([-.3, -1, 0])).scale(0.5) + s_into_m = TextMobject("S into M", color=BLACK).next_to(os_ecall, RIGHT, buff=0).shift(np.array([0, .5, 0])).scale(.5) + m_back_s = TextMobject("M back to S", color=BLACK).next_to(os_ecall, RIGHT, buff=0).shift(np.array([-.3, -1, 0])).scale(.5) + self.add(u_into_s, s_back_u, s_into_m, m_back_s) + + + +class PrivilegeStack(Scene): + CONFIG = { + "camera_config": { + "background_color": WHITE, + }, + } + def construct(self): + os = Rectangle(width=4.0, height=1.0, color=BLACK, fill_color=WHITE, fill_opacity=1.0) + os_text = TextMobject("OS", color=BLACK).move_to(os) + self.add(os, os_text) + + sbi = Rectangle(width=4.0, height=1.0, color=BLACK, fill_color=BLACK, fill_opacity=1.0) + sbi.next_to(os, DOWN, buff=0) + sbi_text = TextMobject("SBI", color=WHITE).move_to(sbi) + self.add(sbi, sbi_text) + + see = Rectangle(width=4.0, height=1.0, color=BLACK, fill_color=WHITE, fill_opacity=1.0) + see.next_to(sbi, DOWN, buff=0) + see_text = TextMobject("SEE", color=BLACK).move_to(see) + self.add(see, see_text) + + abi0 = Rectangle(height=1.0, width=1.8, color=BLACK, fill_color=BLACK, fill_opacity=1.0) + abi0.next_to(os, UP, buff=0).align_to(os, LEFT) + abi0_text = TextMobject("ABI", color=WHITE).move_to(abi0) + self.add(abi0, abi0_text) + + abi1 = Rectangle(height=1.0, width=1.8, color=BLACK, fill_color=BLACK, fill_opacity=1.0) + abi1.next_to(os, UP, buff=0).align_to(os, RIGHT) + abi1_text = TextMobject("ABI", color=WHITE).move_to(abi1) + self.add(abi1, abi1_text) + + app0 = Rectangle(height=1.0, width=1.8, color=BLACK, fill_color=WHITE, fill_opacity=1.0) + app0.next_to(abi0, UP, buff=0) + app0_text = TextMobject("App", color=BLACK).move_to(app0) + self.add(app0, app0_text) + + app1 = Rectangle(height=1.0, width=1.8, color=BLACK, fill_color=WHITE, fill_opacity=1.0) + app1.next_to(abi1, UP, buff=0) + app1_text = TextMobject("App", color=BLACK).move_to(app1) + self.add(app1, app1_text) + + line0 = DashedLine(sbi.get_right(), sbi.get_right() + np.array([3, 0, 0]), color=BLACK) + self.add(line0) + line1 = DashedLine(abi1.get_right(), abi1.get_right() + np.array([3, 0, 0]), color=BLACK) + self.add(line1) + + machine = TextMobject("Machine", color=BLACK).next_to(see, RIGHT, buff=.8) + supervisor = TextMobject("Supervisor", color=BLACK).next_to(os, RIGHT, buff=.8) + user = TextMobject("User", color=BLACK).next_to(app1, RIGHT, buff=.8) + self.add(machine, supervisor, user) + diff --git a/source/chapter2/deng-fish.png b/source/chapter2/deng-fish.png new file mode 100644 index 0000000000000000000000000000000000000000..94f35e3a398200189a2f734b2246829c165946db Binary files /dev/null and b/source/chapter2/deng-fish.png differ diff --git a/source/chapter2/index.rst b/source/chapter2/index.rst index 7b3d98403dc5cf36ccdeb85f0ecc0dcad9ebb255..f4aa5bcd4f9c17464d6c3601be37203e9bb3d8ee 100644 --- a/source/chapter2/index.rst +++ b/source/chapter2/index.rst @@ -1,95 +1,15 @@ +.. _link-chapter2: + 第二章:批处理系统 ============================================== .. toctree:: - :hidden: :maxdepth: 4 + 0intro 1rv-privilege 2application 3batch-system 4trap-handling - -上一章,我们在 RV64 裸机平台上成功运行起来了 ``Hello, world!`` 。看起来这个过程非常顺利,只需要一条命令就能全部完成。但实际上,在那个 -计算机刚刚诞生的年代,很多事情并不像我们想象的那么简单。 当时,程序被记录在打孔的卡片上,使用汇编语言甚至机器语言来编写。而稀缺且昂贵的 -计算机由专业的管理员负责操作,就和我们在上一章所做的事情一样,他们手动将卡片输入计算机,等待程序运行结束或者终止程序的运行。最后,他们从 -计算机的输出端——也就是打印机中取出程序的输出并交给正在休息室等待的程序提交者。 - -实际上,这样做是一种对于珍贵的计算资源的浪费。因为当时的计算机和今天的个人计算机不同,它的体积极其庞大,能够占满一整个空调房间。管理员在 -房间的各个地方跑来跑去、或是等待打印机的输出的这些时间段,计算机都并没有在工作。于是,人们希望计算机能够不间断的工作且专注于计算任务本身。 - -**批处理系统** (Batch System) 应运而生。它的核心思想是:将多个程序打包到一起输入计算机。而当一个程序运行结束后,计算机会 *自动* 加载 -下一个程序到内存并开始执行。这便是最早的真正意义上的操作系统。 - -程序总是难免出现错误。但人们希望一个程序的错误不要影响到操作系统本身,它只需要终止出错的程序,转而运行执行序列中的下一个程序即可。如果后面的 -程序都无法运行就太糟糕了。这种 *保护* 操作系统不受有意或无意出错的程序破坏的机制被称为 **特权级** (Privilege) 机制,它实现了用户态和 -内核态的隔离,需要软件和硬件的共同努力。 - -本章我们的批处理系统将连续运行三个应用程序,放在 ``user/src/bin`` 目录下。 - -获取本章代码: - -.. code-block:: console - - $ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git - $ cd rCore-Tutorial-v3 - $ git checkout ch2 - -在 qemu 模拟器上运行本章代码: - -.. code-block:: console - - $ cd os - $ make run - -将 Maix 系列开发版连接到 PC,并在上面运行本章代码: - -.. code-block:: console - - $ cd os - $ make run BOARD=k210 - -如果顺利的话,我们可以看到批处理系统自动加载并运行所有的程序并且正确在程序出错的情况下保护了自身: - -.. code-block:: - - [rustsbi] Version 0.1.0 - .______ __ __ _______.___________. _______..______ __ - | _ \ | | | | / | | / || _ \ | | - | |_) | | | | | | (----`---| |----`| (----`| |_) || | - | / | | | | \ \ | | \ \ | _ < | | - | |\ \----.| `--' |.----) | | | .----) | | |_) || | - | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| - - [rustsbi] Platform: QEMU - [rustsbi] misa: RV64ACDFIMSU - [rustsbi] mideleg: 0x222 - [rustsbi] medeleg: 0xb1ab - [rustsbi] Kernel entry: 0x80020000 - [kernel] Hello, world! - [kernel] num_app = 3 - [kernel] app_0 [0x8002b028, 0x8002c328) - [kernel] app_1 [0x8002c328, 0x8002d6c0) - [kernel] app_2 [0x8002d6c0, 0x8002eb98) - [kernel] Loading app_0 - Hello, world! - [kernel] Application exited with code 0 - [kernel] Loading app_1 - Into Test store_fault, we will insert an invalid store operation... - Kernel should kill this application! - [kernel] PageFault in application, core dumped. - [kernel] Loading app_2 - 3^10000=5079 - 3^20000=8202 - 3^30000=8824 - 3^40000=5750 - 3^50000=3824 - 3^60000=8516 - 3^70000=2510 - 3^80000=9379 - 3^90000=2621 - 3^100000=2749 - Test power OK! - [kernel] Application exited with code 0 - [kernel] Panicked at src/batch.rs:61 All applications completed! + 5exercise diff --git a/source/chapter3/0intro.rst b/source/chapter3/0intro.rst new file mode 100644 index 0000000000000000000000000000000000000000..f718987848346edec27107e63fbb5d95314d4982 --- /dev/null +++ b/source/chapter3/0intro.rst @@ -0,0 +1,283 @@ +引言 +======================================== + +本章导读 +-------------------------- + + +.. + chyyuu:有一个ascii图,画出我们做的OS。 + + +本章展现了操作系统一系列功能: + +- 通过提前加载应用程序到内存,减少应用程序切换开销 +- 通过协作机制支持程序主动放弃处理器,提高系统执行效率 +- 通抢占机制支持程序被动放弃处理器,提高不同程序对处理器资源使用的公平性,也进一步提高了应用对I/O事件的响应效率 + + +上一章,我们实现了一个简单的批处理系统。首先,它能够自动按照顺序加载并运行序列中的每一个应用,当一个应用运行结束之后无需操作员的手动替换;另一方面,在硬件提供的特权级机制的帮助下,运行在更高特权级的它不会受到有意或者无意出错的应用的影响,可以全方位监控运行在用户态特权级的应用的执行,一旦应用越过了硬件所设置特权级界限或主动申请获得操作系统的服务,就会触发 Trap 并进入到批处理系统中进行处理。无论原因是应用出错或是应用声明自己执行完毕,批处理系统都只需要加载序列中的下一个应用并进入执行。可以看到批处理系统的特性是:在内存中同一时间最多只需驻留一个应用。这是因为只有当一个应用出错或退出之后,批处理系统才会去将另一个应用加载到相同的一块内存区域。 + +而计算机硬件在快速发展,内存容量在逐渐增大,处理器的速度也在增加,外设IO性能方面的进展不大。这就使得以往内存只能放下一个程序的情况得到很大改善,但处理器的空闲程度加大了。于是科学家就开始考虑在内存中尽量同时驻留多个应用,这样处理器的利用率就会提高。但只有一个程序执行完毕后或主动放弃执行,处理器才能执行另外一个程序。这种运行方式称为 **多道程序** 。 + + +协作式操作系统 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +早期的计算机系统大部分是单处理器计算机系统。当处理器进一步发展后,它与IO的速度差距也进一步拉大。这时计算机科学家发现,在 **多道程序** 运行方式下,一个程序如果不让出处理器,其他程序是无法执行的。如果一个应用由于IO操作让处理器空闲下来或让处理器忙等,那其他需要处理器资源进行计算的应用还是没法使用空闲的处理器资源。于是就想到,让应用在执行IO操作时,可以主动 **释放处理器** ,让其他应用继续执行。当然执行 **放弃处理器** 的操作算是一种对处理器资源的直接管理,所以应用程序可以发出这样的系统调用,让操作系统来具体完成。这样的操作系统就是支持 **多道程序** 协作式操作系统。 + +抢占式操作系统 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +计算机科学家很快发现,编写应用程序的科学家(简称应用程序员)来自不同的领域,他们不一定有友好互助的意识,也不了解其他程序的执行情况,很难(也没必要)有提高整个系统利用率上的大局观。在他们的脑海里,整个计算机就应该是为他们自己的应用准备的,不用考虑其他程序的运行。这导致应用程序员在编写程序时,无法做到在程序的合适位置放置 **放弃处理器的系统调用请求** ,这样系统的整体利用率还是无法提高。 + +所以,站在系统的层面,还是需要有一种办法能强制打断应用程序的执行,来提高整个系统的效率,让在整个系统中执行的多个程序之间占用计算机资源的情况相对公平一些。根据计算机系统的硬件设计,为提高I/O效率,外设可以通过硬件中断机制来与处理机进行I/O交互操作。这种硬件中断机制·可随时打断应用程序的执行,并让操作系统来完成对外设的I/O响应。 + +而操作系统可进一步利用某种以固定时长为时间间隔的外设中断(比如时钟中断)来强制打断一个程序的执行,这样一个程序只能运行一段时间(可以简称为一个时间片, Time Slice)就一定会让出处理器,且操作系统可以在处理外设的I/O响应后,让不同应用程序分时占用处理器执行,并可通过程序占用处理器的总执行时间来评估运行的程序对处理器资源的消耗。 + +.. _term-task: + +我们可以把一个程序在一个时间片上占用处理器执行的过程称为一个 **任务** (Task),让操作系统对不同程序的 **任务** 进行管理。通过平衡各个程序在整个时间段上的任务数,就达到一定程度的系统公平和高效的系统效率。在一个包含多个时间片的时间段上,会有属于不同程序的多个任务在轮流占用处理器执行,这样的操作系统就是支持 **分时多任务** 的抢占式操作系统。 + + +本章所介绍的多道程序和分时多任务系统都有一些共同的特点:在内存中同一时间可以驻留多个应用。所有的应用都是在系统启动的时候分别加载到内存的不同区域中。由于目前计算机系统中只有一个处理器,则同一时间最多只有一个应用在执行,剩下的应用则处于就绪状态,需要内核将处理器分配给它们才能开始执行。一旦应用开始执行,它就处于运行状态了。 + + +本章主要是设计和实现建立支持 **多道程序** 的二叠纪“锯齿螈”初级操作系统、支持多道程序的三叠纪“始初龙”协作式操作系统和支持 **分时多任务** 的三叠纪“腔骨龙”抢占式操作系统,从而对可支持运行一批应用程序的多种执行环境有一个全面和深入的理解,并可归纳抽象出 **任务** , **任务切换** 等操作系统的概念。 + + +.. note:: + + 读者也许会有疑问:由于只有一个 处理器,即使这样做,同一时间最多还是只能运行一个应用,还浪费了更多的内存来把所有 + 的应用都加载进来。那么这样做有什么意义呢? + + 读者可以带着这个问题继续看下去。后面我们会介绍这样做到底能够解决什么问题。 + +实践体验 +------------------------------------- + +.. _term-multiprogramming: +.. _term-time-sharing-multitasking: + +**多道程序** (Multiprogramming) 和 **分时多任务** (Time-Sharing Multitasking) 对于应用的要求是不同的,因此我们分别为它们编写了不同的应用,代码也被放在两个不同的分支上。对于它们更加深入的讲解请参考本章正文,我们在引言中仅给出运行代码的方法。 + +获取多道程序的代码: + +.. code-block:: console + + $ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git + $ cd rCore-Tutorial-v3 + $ git checkout ch3-coop + +获取分时多任务系统的代码: + +.. code-block:: console + + $ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git + $ cd rCore-Tutorial-v3 + $ git checkout ch3 + +在 qemu 模拟器上运行本章代码: + +.. code-block:: console + + $ cd os + $ make run + +将 Maix 系列开发板连接到 PC,并在上面运行本章代码: + +.. code-block:: console + + $ cd os + $ make run BOARD=k210 + +多道程序的应用分别会输出一个不同的字母矩阵。当他们交替执行的时候,以 k210 平台为例,我们将看到字母行的交错输出: + +.. code-block:: + + [rustsbi] Version 0.1.0 + .______ __ __ _______.___________. _______..______ __ + | _ \ | | | | / | | / || _ \ | | + | |_) | | | | | | (----`---| |----`| (----`| |_) || | + | / | | | | \ \ | | \ \ | _ < | | + | |\ \----.| `--' |.----) | | | .----) | | |_) || | + | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| + + [rustsbi] Platform: K210 + [rustsbi] misa: RV64ACDFIMSU + [rustsbi] mideleg: 0x222 + [rustsbi] medeleg: 0x1ab + [rustsbi] Kernel entry: 0x80020000 + [kernel] Hello, world! + AAAAAAAAAA [1/5] + BBBBBBBBBB [1/2] + CCCCCCCCCC [1/3] + AAAAAAAAAA [2/5] + BBBBBBBBBB [2/2] + CCCCCCCCCC [2/3] + AAAAAAAAAA [3/5] + Test write_b OK! + [kernel] Application exited with code 0 + CCCCCCCCCC [3/3] + AAAAAAAAAA [4/5] + Test write_c OK! + [kernel] Application exited with code 0 + AAAAAAAAAA [5/5] + Test write_a OK! + [kernel] Application exited with code 0 + [kernel] Panicked at src/task/mod.rs:97 All applications completed! + [rustsbi] reset triggered! todo: shutdown all harts on k210; program halt + +分时多任务系统应用分为两种。编号为 00/01/02 的应用分别会计算质数 3/5/7 的幂次对一个大质数取模的余数,并会将结果阶段性输出。编号为 03 的 +应用则会等待三秒钟之后再退出。以 k210 平台为例,我们将会看到 00/01/02 三个应用分段完成它们的计算任务,而应用 03 由于等待时间过长总是 +最后一个结束执行。 + +.. code-block:: + + [rustsbi] RustSBI version 0.1.1 + .______ __ __ _______.___________. _______..______ __ + | _ \ | | | | / | | / || _ \ | | + | |_) | | | | | | (----`---| |----`| (----`| |_) || | + | / | | | | \ \ | | \ \ | _ < | | + | |\ \----.| `--' |.----) | | | .----) | | |_) || | + | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| + + [rustsbi] Platform: K210 (Version 0.1.0) + [rustsbi] misa: RV64ACDFIMSU + [rustsbi] mideleg: 0x22 + [rustsbi] medeleg: 0x1ab + [rustsbi] Kernel entry: 0x80020000 + [kernel] Hello, world! + power_3 [10000/200000] + power_3 [20000/200000] + power_3 [30000/200000power_5 [10000/140000] + power_5 [20000/140000] + power_5 [30000/140000power_7 [10000/160000] + power_7 [20000/160000] + power_7 [30000/160000] + ] + power_3 [40000/200000] + power_3 [50000/200000] + power_3 [60000/200000] + power_5 [40000/140000] + power_5 [50000/140000] + power_5 [60000/140000power_7 [40000/160000] + power_7 [50000/160000] + power_7 [60000/160000] + ] + power_3 [70000/200000] + power_3 [80000/200000] + power_3 [90000/200000] + power_5 [70000/140000] + power_5 [80000/140000] + power_5 [90000/140000power_7 [70000/160000] + power_7 [80000/160000] + power_7 [90000/160000] + ] + power_3 [100000/200000] + power_3 [110000/200000] + power_3 [120000/] + power_5 [100000/140000] + power_5 [110000/140000] + power_5 [120000/power_7 [100000/160000] + power_7 [110000/160000] + power_7 [120000/160000200000] + power_3 [130000/200000] + power_3 [140000/200000] + power_3 [150000140000] + power_5 [130000/140000] + power_5 [140000/140000] + 5^140000 = 386471875] + power_7 [130000/160000] + power_7 [140000/160000] + power_7 [150000/160000/200000] + power_3 [160000/200000] + power_3 [170000/200000] + power_3 [ + Test power_5 OK! + [kernel] Application exited with code 0 + ] + power_7 [160000/160000] + 7180000/200000] + power_3 [190000/200000] + power_3 [200000/200000] + 3^200000 = 871008973^160000 = 667897727 + Test power_7 OK! + [kernel] Application exited with code 0 + + Test power_3 OK! + [kernel] Application exited with code 0 + Test sleep OK! + [kernel] Application exited with code 0 + [kernel] Panicked at src/task/mod.rs:98 All applications completed! + [rustsbi] reset triggered! todo: shutdown all harts on k210; program halt. Type: 0, reason: 0 + +输出结果看上去有一些混乱,原因是用户程序的每个 ``println!`` 往往会被拆分成多个 ``sys_write`` 系统调用提交给内核。有兴趣的同学可以参考 ``println!`` 宏的实现。 + +另外需要说明的是一点是:与上一章不同,应用的编号不再决定其被加载运行的先后顺序,而仅仅能够改变应用被加载到内存中的位置。 + +本章代码树 +--------------------------------------------- + +.. code-block:: + :linenos: + :emphasize-lines: 10 + + ├── bootloader + │   ├── rustsbi-k210.bin + │   └── rustsbi-qemu.bin + ├── LICENSE + ├── os + │   ├── build.rs + │   ├── Cargo.toml + │   ├── Makefile + │   └── src + │   ├── batch.rs(移除:功能分别拆分到 loader 和 task 两个子模块) + │   ├── config.rs(新增:保存内核的一些配置) + │   ├── console.rs + │   ├── entry.asm + │   ├── lang_items.rs + │   ├── link_app.S + │   ├── linker-k210.ld + │   ├── linker-qemu.ld + │   ├── loader.rs(新增:将应用加载到内存并进行管理) + │   ├── main.rs(修改:主函数进行了修改) + │   ├── sbi.rs(修改:引入新的 sbi call set_timer) + │   ├── syscall(修改:新增若干 syscall) + │   │   ├── fs.rs + │   │   ├── mod.rs + │   │   └── process.rs + │   ├── task(新增:task 子模块,主要负责任务管理) + │   │   ├── context.rs(引入 Task 上下文 TaskContext) + │   │   ├── mod.rs(全局任务管理器和提供给其他模块的接口) + │   │   ├── switch.rs(将任务切换的汇编代码解释为 Rust 接口 __switch) + │   │   ├── switch.S(任务切换的汇编代码) + │   │   └── task.rs(任务控制块 TaskControlBlock 和任务状态 TaskStatus 的定义) + │   ├── timer.rs(新增:计时器相关) + │   └── trap + │   ├── context.rs + │   ├── mod.rs(修改:时钟中断相应处理) + │   └── trap.S + ├── README.md + ├── rust-toolchain + ├── tools + │   ├── kflash.py + │   ├── LICENSE + │   ├── package.json + │   ├── README.rst + │   └── setup.py + └── user + ├── build.py(新增:使用 build.py 构建应用使得它们占用的物理地址区间不相交) + ├── Cargo.toml + ├── Makefile(修改:使用 build.py 构建应用) + └── src + ├── bin(修改:换成第三章测例) + │   ├── 00power_3.rs + │   ├── 01power_5.rs + │   ├── 02power_7.rs + │   └── 03sleep.rs + ├── console.rs + ├── lang_items.rs + ├── lib.rs + ├── linker.ld + └── syscall.rs \ No newline at end of file diff --git a/source/chapter3/1multi-loader.rst b/source/chapter3/1multi-loader.rst new file mode 100644 index 0000000000000000000000000000000000000000..d10a991fd3f414328c7cc51f70ba6a658e1e8519 --- /dev/null +++ b/source/chapter3/1multi-loader.rst @@ -0,0 +1,141 @@ +多道程序放置与加载 +===================================== + +本节导读 +-------------------------- + +在本章的引言中我们提到每个应用都需要按照它的编号被分别放置并加载到内存中不同的位置。本节我们就来介绍它是如何实现的。通过具体实现,可以看到多个应用程序被一次性地加载到内存中,这样在切换到另外一个应用程序执行会很快,不像前一章介绍的操作系统,还要有清空前一个应用,然后加载当前应用的过程与开销。 + +但我们也会了解到,每个应用程序需要知道自己运行时在内存中的不同位置,这对应用程序的编写带来了一定的麻烦。而且操作系统也要知道每个应用程序运行时的位置,不能任意移动应用程序所在的内存空间,即不能在运行时根据内存空间的动态空闲情况,把应用程序调整到合适的空闲空间中。 + +.. + chyyuu:有一个ascii图,画出我们做的OS在本节的部分。 + +多道程序放置 +---------------------------- + +与第二章相同,所有应用的 ELF 都经过 strip 丢掉所有 ELF header 和符号变为二进制镜像文件,随后以同样的格式通过 ``link_user.S`` 在编译的时候直接链接到内核的数据段中。不同的是,我们对相关模块进行了调整:在第二章中应用的加载和进度控制都交给 ``batch`` 子模块,而在第三章中我们将应用的加载这部分功能分离出来在 ``loader`` 子模块中实现,应用的执行和切换则交给 ``task`` 子模块。 + +注意,我们需要调整每个应用被构建时候使用的链接脚本 ``linker.ld`` 中的起始地址 ``BASE_ADDRESS`` 为它实际会被内核加载并运行的地址。也就是要做到:应用知道自己会被加载到某个地址运行,而内核也确实能做到将它加载到那个地址。这算是应用和内核在某种意义上达成的一种协议。之所以要有这么苛刻的条件,是因为应用和内核的能力都很弱,通用性很低。事实上,目前应用程序的编址方式是基于绝对位置的而并没做到与位置无关,内核也没有提供相应的重定位机制。 + +.. note:: + + 对于编址方式,需要再回顾一下编译原理课讲解的后端代码生成技术,以及计算机组成原理课的指令寻址方式的内容。可以在 `这里 `_ 找到更多有关 + 位置无关和重定位的说明。 + +由于每个应用被加载到的位置都不同,也就导致它们的链接脚本 ``linker.ld`` 中的 ``BASE_ADDRESS`` 都是不同的。实际上, +我们写了一个脚本 ``build.py`` 而不是直接用 ``cargo build`` 构建应用的链接脚本: + +.. code-block:: python + :linenos: + + # user/build.py + + import os + + base_address = 0x80400000 + step = 0x20000 + linker = 'src/linker.ld' + + app_id = 0 + apps = os.listdir('src/bin') + apps.sort() + for app in apps: + app = app[:app.find('.')] + lines = [] + lines_before = [] + with open(linker, 'r') as f: + for line in f.readlines(): + lines_before.append(line) + line = line.replace(hex(base_address), hex(base_address+step*app_id)) + lines.append(line) + with open(linker, 'w+') as f: + f.writelines(lines) + os.system('cargo build --bin %s --release' % app) + print('[build.py] application %s start with address %s' %(app, hex(base_address+step*app_id))) + with open(linker, 'w+') as f: + f.writelines(lines_before) + app_id = app_id + 1 + +它的思路很简单,在遍历 ``app`` 的大循环里面只做了这样几件事情: + +- 第 16~22 行,找到 ``src/linker.ld`` 中的 ``BASE_ADDRESS = 0x80400000;`` 这一行,并将后面的地址 + 替换为和当前应用对应的一个地址; +- 第 23 行,使用 ``cargo build`` 构建当前的应用,注意我们可以使用 ``--bin`` 参数来只构建某一个应用; +- 第 25~26 行,将 ``src/linker.ld`` 还原。 + + +多道程序加载 +---------------------------- + +应用的加载方式也和上一章的有所不同。上一章中讲解的加载方法是让所有应用都共享同一个固定的加载物理地址。也是因为这个原因,内存中同时最多只能驻留一个应用,当它运行完毕或者出错退出的时候由操作系统的 ``batch`` 子模块加载一个新的应用来替换掉它。本章中,所有的应用在内核初始化的时候就一并被加载到内存中。为了避免覆盖,它们自然需要被加载到不同的物理地址。这是通过调用 ``loader`` 子模块的 ``load_apps`` 函数实现的: + +.. code-block:: rust + :linenos: + + // os/src/loader.rs + + pub fn load_apps() { + extern "C" { fn _num_app(); } + let num_app_ptr = _num_app as usize as *const usize; + let num_app = get_num_app(); + let app_start = unsafe { + core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1) + }; + // clear i-cache first + unsafe { llvm_asm!("fence.i" :::: "volatile"); } + // load apps + for i in 0..num_app { + let base_i = get_base_i(i); + // clear region + (base_i..base_i + APP_SIZE_LIMIT).for_each(|addr| unsafe { + (addr as *mut u8).write_volatile(0) + }); + // load app from data section to memory + let src = unsafe { + core::slice::from_raw_parts( + app_start[i] as *const u8, + app_start[i + 1] - app_start[i] + ) + }; + let dst = unsafe { + core::slice::from_raw_parts_mut(base_i as *mut u8, src.len()) + }; + dst.copy_from_slice(src); + } + } + +可以看出,第 :math:`i` 个应用被加载到以物理地址 ``base_i`` 开头的一段物理内存上,而 ``base_i`` 的计算方式如下: + +.. code-block:: rust + :linenos: + + // os/src/loader.rs + + fn get_base_i(app_id: usize) -> usize { + APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT + } + +我们可以在 ``config`` 子模块中找到这两个常数。从这一章开始, ``config`` 子模块用来存放内核中所有的常数。看到 ``APP_BASE_ADDRESS`` 被设置为 ``0x80400000`` ,而 ``APP_SIZE_LIMIT`` 和上一章一样被设置为 ``0x20000`` ,也就是每个应用二进制镜像的大小限制。因此,应用的内存布局就很明朗了——就是从 ``APP_BASE_ADDRESS`` 开始依次为每个应用预留一段空间。 + + +这样,我们就说明了多个应用是如何被构建和加载的。 + + +执行应用程序 +---------------------------- + +当多道程序的初始化放置工作完成,或者是某个应用程序运行结束或出错的时候,我们要调用 run_next_app 函数切换到下一个应用程序。此时 CPU 运行在 S 特权级的操作系统中,而操作系统希望能够切换到 U 特权级去运行应用程序。这一过程与上章的 :ref:`执行应用程序 ` 一节的描述类似。相对不同的是,操作系统知道每个应用程序预先加载在内存中的位置,这就需要设置应用程序返回的不同 Trap 上下文(Trap上下文中保存了 放置程序起始地址的``epc`` 寄存器内容): + +- 跳转到应用程序(编号 :math:`i` )的入口点 :math:`\text{entry}_i` +- 将使用的栈切换到用户栈 :math:`\text{stack}_i` + + + +二叠纪“锯齿螈”操作系统 +------------------------ + +这样,我们的二叠纪“锯齿螈”操作系统就算是实现完毕了。 + +.. + chyyuu:有一个ascii图,画出我们做的OS。 \ No newline at end of file diff --git a/source/chapter3/2task-switching.rst b/source/chapter3/2task-switching.rst new file mode 100644 index 0000000000000000000000000000000000000000..363f13947b688ad30186d8fdd1be4f6ea8c6f8aa --- /dev/null +++ b/source/chapter3/2task-switching.rst @@ -0,0 +1,173 @@ +任务切换 +================================ + +本节导读 +-------------------------- + +在上一节实现的二叠纪“锯齿螈”操作系统还是比较原始,一个应用会独占 CPU 直到它出错或主动退出。操作系统还是以程序的一次执行过程(从开始到结束)作为处理器切换程序的时间段。为了提高效率,我们需要引入新的操作系统概念 **任务** 、 **任务切换** 、**任务上下文** 。 + +如果把应用程序执行的整个过程进行进一步分析,可以看到,如果程序访问 IO 或睡眠等待时,其实是不需要占用处理器的,于是我们可以把应用程序的不同时间段的执行过程分为两类,占用处理器执行有效任务的计算阶段和不必占用处理器的等待阶段。这些按时间流连接在一起的不同类型的多个阶段形成了一个我们熟悉的“暂停-继续...”组合的 :ref:`执行流或执行历史 ` 。从开始到结束的整个执行流就是应用程序的整个执行过程。 + +本节的重点是操作系统的核心机制—— **任务切换** 。 任务切换支持的场景是:一个应用在运行途中便会主动交出 CPU 的使用权,此时它只能暂停执行,等到内核重新给它分配处理器资源之后才能恢复并继续执行。 + +任务的概念形成 +--------------------------------- + +.. + chyyuu:程序执行过程的图示。 + +如果操作系统能够在某个应用程序处于等待阶段的时候,把处理器转给另外一个处于计算阶段的应用程序,那么只要转换的开销不大,那么处理器的执行效率就会大大提高。当然,这需要应用程序在运行途中能主动交出 CPU 的使用权,此时它处于等待阶段,等到操作系统让它再次执行后,那它就可以继续执行了。 + +.. _term-task: +.. _term-task-switch: + +到这里,我们就把应用程序的一个计算阶段的执行过程(也是一段执行流)称为一个 **任务** ,所有的任务都完成后,应用程序也就完成了。从一个程序的任务切换到另外一个程序的任务称为 **任务切换** 。为了确保切换后的任务能够正确继续执行,操作系统需要支持让任务的执行“暂停”和“继续”。 + +.. _term-task-context: + +我们又看到了熟悉的“暂停-继续”组合。一旦一条执行流需要支持“暂停-继续”,就需要提供一种执行流切换的机制,而且需要保证执行流被切换出去之前和切换回来之后,它的状态,也就是在执行过程中同步变化的资源(如寄存器、栈等)需要保持不变,或者变化在它的预期之内。而不是所有的资源都需要被保存,事实上只有那些对于执行流接下来的进行仍然有用,且在它被切换出去的时候有被覆盖的风险的那些资源才有被保存的价值。这些物理资源被称为 **任务上下文 (Task Context)** 。 + + +这里,大家开始在具体的操作系统中接触到了一些抽象的概念,其实这些概念都是具体代码的结构和代码动态执行过程的文字表述而已。 + +不同类型的上下文与切换 +--------------------------------- + +在执行流切换过程中,我们需要结合硬件机制和软件实现来保存和恢复任务上下文。任务的一次切换涉及到被换出和即将被换入的两条执行流(分属两个任务),通常它们都需要共同遵循某些约定来合作完成这一过程。在前两章,我们已经看到了两种上下文保存/恢复的实例。让我们再来回顾一下它们: + +- 第一章《RV64 裸机应用》中,我们介绍了 :ref:`函数调用与栈 ` 。当时提到过,为了支持嵌套函数调用,不仅需要硬件平台提供特殊的跳转指令,还需要保存和恢复 :ref:`函数调用上下文 ` 。注意在 *我们* 的定义中,函数调用包含在普通控制流(与异常控制流相对)之内,且始终用一个固定的栈来保存执行的历史记录,因此函数调用并不涉及执行流的切换。但是我们依然可以将其看成调用者和被调用者两个执行过程的“切换”,二者的协作体现在它们都遵循调用规范,分别保存一部分通用寄存器,这样的好处是编译器能够有足够的信息来尽可能减少需要保存的寄存器的数目。虽然当时用了很大的篇幅来说明,但其实整个过程都是编译器负责完成的,我们只需设置好栈就行了。 +- 第二章《批处理系统》中第一次涉及到了某种异常(Trap)控制流,即两条执行流的切换,需要保存和恢复 :ref:`系统调用(Trap)上下文 ` 。当时,为了让内核能够 *完全掌控* 应用的执行,且不会被应用破坏整个系统,我们必须利用硬件 + 提供的特权级机制,让应用和内核运行在不同的特权级。应用运行在 U 特权级,它所被允许的操作进一步受限,处处被内核监督管理;而内核运行在 S 特权级,有能力处理应用执行过程中提出的请求或遇到的状况。 + + 应用程序与操作系统打交道的核心在于硬件提供的 Trap 机制,也就是在 U 特权级运行的应用执行流和在 S 特权级运行的 Trap 执行流(操作系统的陷入处理部分)之间的切换。Trap 执行流是在 Trap 触发的一瞬间生成的,它和原应用执行流有着很密切的联系,因为它唯一的目标就是处理 Trap 并恢复到原应用执行流。而且,由于 Trap 机制对于应用来说几乎是透明的,所以基本上都是 Trap 执行流在“负重前行”。Trap 执行流需要把 **Trap 上下文** 保存在自己的 + 内核栈上,里面包含几乎所有的通用寄存器,因为在 Trap 处理过程中它们都可能被用到。如果有需要的话,可以回看 + :ref:`Trap 上下文保存与恢复 ` 小节。 + +任务切换的设计与实现 +--------------------------------- + +本节的任务切换的执行过程是第二章的 Trap 之后的另一种异常控制流,都是描述两条执行流之间的切换,如果将它和 Trap 切换进行比较,会有如下异同: + +- 与 Trap 切换不同,它不涉及特权级切换; +- 与 Trap 切换不同,它的一部分是由编译器帮忙完成的; +- 与 Trap 切换相同,它对应用是透明的。 + +事实上,它是来自两个不同应用的 Trap 执行流之间的切换。当一个应用 Trap 到 S 模式的操作系统中进行进一步处理(即进入了操作系统的Trap执行流)的时候,其 Trap 执行流可以调用一个特殊的 ``__switch`` 函数。这个函数表面上就是一个普通的函数调用:在 ``__switch`` 返回之后,将继续从调用该函数的位置继续向下执行。但是其间却隐藏着复杂的执行流切换过程。具体来说,调用 ``__switch`` 之后直到它返回前的这段时间,原 Trap 执行流会先被暂停并被切换出去, CPU 转而运行另一个应用的 Trap 执行流。之后在时机合适的时候,原 Trap 执行流才会从某一条 Trap 执行流(很有可能不是它之前切换到的那一条)切换回来继续执行并最终返回。不过,从实现的角度讲, ``__switch`` 和一个普通的函数之间的差别仅仅是它会换栈。 + +.. image:: task_context.png + +当 Trap 执行流准备调用 ``__switch`` 函数并进入暂停状态的时候,让我们考察一下它内核栈上的情况。如上图所示,在准备调用 ``__switch`` 函数之前,内核栈上从栈底到栈顶分别是保存了应用执行状态的 Trap 上下文以及内核在对 Trap 处理的过程中留下的调用栈信息。由于之后还要恢复回来执行,我们必须保存 CPU 当前的某些寄存器,我们称它们为 **任务上下文** (Task Context)。我们会在稍后介绍里面需要包含哪些寄存器。至于保存的位置,我们将任务上下文直接压入内核栈的栈顶,从这一点上来说它和函数调用一样。 + +这样需要保存的信息就已经确实的保存在内核栈上了,而恢复的时候我们要从任务上下文的位置——也就是这一时刻内核栈栈顶的位置找到被保存的寄存器快照进行恢复,这个位置也需要被保存下来。对于每一条被暂停的 Trap 执行流,我们都用一个名为 ``task_cx_ptr`` 的变量来保存它栈顶的任务上下文的地址。利用 C 语言的引用来描述的话就是: + +.. code-block:: C + + TaskContext *task_cx_ptr = &task_cx; + +由于我们要用 ``task_cx_ptr`` 这个变量来进行保存任务上下文的地址,自然也要对任务上下文的地址进行修改。于是我们还需要指向 ``task_cx_ptr`` 这个变量的指针 ``task_cx_ptr2`` : + +.. code-block:: C + + TaskContext **task_cx_ptr2 = &task_cx_ptr; + +接下来我们同样从栈上内容的角度来看 ``__switch`` 的整体流程: + +.. image:: switch-1.png + +.. image:: switch-2.png + +Trap 执行流在调用 ``__switch`` 之前就需要明确知道即将切换到哪一条目前正处于暂停状态的 Trap 执行流,因此 ``__switch`` 有两个参数,第一个参数代表它自己,第二个参数则代表即将切换到的那条 Trap 执行流。这里我们用上面提到过的 ``task_cx_ptr2`` 作为代表。在上图中我们假设某次 ``__switch`` 调用要从 Trap 执行流 A 切换到 B,一共可以分为五个阶段,在每个阶段中我们都给出了 A 和 B 内核栈上的内容。 + +- 阶段 [1]:在 Trap 执行流 A 调用 ``__switch`` 之前,A 的内核栈上只有 Trap 上下文和 Trap 处理的调用栈信息,而 B 是之前被切换出去的,它的栈顶还有额外的一个任务上下文; +- 阶段 [2]:A 在自身的内核栈上分配一块任务上下文的空间在里面保存 CPU 当前的寄存器快照。随后,我们更新 A 的 ``task_cx_ptr``,只需写入指向它的指针 ``task_cx_ptr2`` 指向的内存即可; +- 阶段 [3]:这一步极为关键。这里读取 B 的 ``task_cx_ptr`` 或者说 ``task_cx_ptr2`` 指向的那块内存获取到 B 的内核栈栈顶位置,并复制给 ``sp`` 寄存器来换到 B 的内核栈。由于内核栈保存着它迄今为止的执行历史记录,可以说 **换栈也就实现了执行流的切换** 。正是因为这一步, ``__switch`` 才能做到一个函数跨两条执行流执行。 +- 阶段 [4]:CPU 从 B 的内核栈栈顶取出任务上下文并恢复寄存器状态,在这之后还要进行退栈操作。 +- 阶段 [5]:对于 B 而言, ``__switch`` 函数返回,可以从调用 ``__switch`` 的位置继续向下执行。 + +从结果来看,我们看到 A 执行流 和 B 执行流的状态发生了互换, A 在保存任务上下文之后进入暂停状态,而 B 则恢复了上下文并在 CPU 上执行。 + +下面我们给出 ``__switch`` 的实现: + +.. code-block:: riscv + :linenos: + + # os/src/task/switch.S + + .altmacro + .macro SAVE_SN n + sd s\n, (\n+1)*8(sp) + .endm + .macro LOAD_SN n + ld s\n, (\n+1)*8(sp) + .endm + .section .text + .globl __switch + __switch: + # __switch( + # current_task_cx_ptr2: &*const TaskContext, + # next_task_cx_ptr2: &*const TaskContext + # ) + # push TaskContext to current sp and save its address to where a0 points to + addi sp, sp, -13*8 + sd sp, 0(a0) + # fill TaskContext with ra & s0-s11 + sd ra, 0(sp) + .set n, 0 + .rept 12 + SAVE_SN %n + .set n, n + 1 + .endr + # ready for loading TaskContext a1 points to + ld sp, 0(a1) + # load registers in the TaskContext + ld ra, 0(sp) + .set n, 0 + .rept 12 + LOAD_SN %n + .set n, n + 1 + .endr + # pop TaskContext + addi sp, sp, 13*8 + ret + +我们手写汇编代码来实现 ``__switch`` 。可以看到它的函数原型中的两个参数分别是当前 Trap 执行流和即将被切换到的 Trap 执行流的 ``task_cx_ptr2`` ,从 :ref:`RISC-V 调用规范 ` 可以知道它们分别通过寄存器 ``a0/a1`` 传入。 + +阶段 [2] 体现在第 18~26 行。第 18 行在 A 的内核栈上预留任务上下文的空间,然后将当前的栈顶位置保存下来。接下来就是逐个对寄存器进行保存,从中我们也能够看出 ``TaskContext`` 里面究竟包含哪些寄存器: + +.. code-block:: rust + :linenos: + + // os/src/task/context.rs + + #[repr(C)] + pub struct TaskContext { + ra: usize, + s: [usize; 12], + } + +这里面只保存了 ``ra`` 和被调用者保存的 ``s0~s11`` 。``ra`` 的保存很重要,它记录了 ``__switch`` 返回之后应该到哪里继续执行,从而在切换回来并 ``ret`` 之后能到正确的位置。而保存调用者保存的寄存器是因为,调用者保存的寄存器可以由编译器帮我们自动保存。我们会将这段汇编代码中的全局符号 ``__switch`` 解释为一个 Rust 函数: + +.. code-block:: rust + :linenos: + + // os/src/task/switch.rs + + global_asm!(include_str!("switch.S")); + + extern "C" { + pub fn __switch( + current_task_cx_ptr2: *const usize, + next_task_cx_ptr2: *const usize + ); + } + +我们会调用该函数来完成切换功能而不是直接跳转到符号 ``__switch`` 的地址。因此在调用前后 Rust 编译器会自动帮助我们插入保存/恢复调用者保存寄存器的汇编代码。 + +仔细观察的话可以发现 ``TaskContext`` 很像一个普通函数栈帧中的内容。正如之前所说, ``__switch`` 的实现除了换栈之外几乎就是一个普通函数,也能在这里得到体现。尽管如此,二者的内涵却有着很大的不同。 + +剩下的汇编代码就比较简单了。读者可以自行对照注释看看图示中的后面几个阶段各是如何实现的。另外,后面会出现传给 ``__switch`` 的两个参数相同,也就是某个 Trap 执行流自己切换到自己的情形,请读者对照图示思考目前的实现能否对它进行正确处理。 + +.. + chyyuu:有一个内核态切换的例子。 + + \ No newline at end of file diff --git a/source/chapter3/3multiprogramming.rst b/source/chapter3/3multiprogramming.rst new file mode 100644 index 0000000000000000000000000000000000000000..f0f8b6ee1a3c551e57c3eb768d076b258cc3c551 --- /dev/null +++ b/source/chapter3/3multiprogramming.rst @@ -0,0 +1,443 @@ +多道程序与协作式调度 +========================================= + + +本节导读 +-------------------------- + + +上一节我们已经介绍了任务切换是如何实现的,最终我们将其封装为一个函数 ``__switch`` 。但是在实际使用的时候,我们需要知道何时调用该函数,以及如何确定传入函数的两个参数——分别代表正待换出和即将被换入的两条 Trap 执行流。本节我们就来介绍任务切换的第一种实际应用场景:多道程序。 + +本节的一个重点是展示进一步增强的操作系统管理能力的和对处理器资源的相对高效利用。为此,对 **任务** 的概念进行进一步扩展和延伸:形成了 + +- 任务运行状态:任务从开始到结束执行过程中所处的不同运行状态:未初始化、准备执行、正在执行、已退出 +- 任务控制块:管理程序的执行过程的任务上下文,控制程序的执行与暂停 +- 任务相关系统调用:应用程序和操作系统直接的接口,用于程序主动暂停 ``sys_yield`` 和主动退出 ``sys_exit`` + +本节的代码可以在 ``ch3-coop`` 分支上找到。 + +多道程序背景与 yield 系统调用 +------------------------------------------------------------------------- + +还记得第二章中介绍的批处理系统的设计初衷吗?它是注意到 CPU 并没有一直在执行应用程序,在一个应用程序运行结束直到下一个应用程序开始运行的这段时间,可能需要操作员取出上一个程序的执行结果并手动进行程序卡片的替换,这段空档期对于宝贵的 CPU 计算资源是一种巨大的浪费。于是批处理系统横空出世,它可以自动连续完成应用的加载和运行,并将一些本不需要 CPU 完成的简单任务交给廉价的外围设备,从而让 CPU 能够更加专注于计算任务本身,大大提高了 CPU 的利用率。 + +.. _term-input-output: + +尽管 CPU 一直在跑应用了,但是其利用率仍有上升的空间。随着应用需求的不断复杂,有的时候会在内核的监督下访问一些外设,它们也是计算机系统的另一个非常重要的组成部分,即 **输入/输出** (I/O, Input/Output) 。CPU 会将请求和一些附加的参数写入外设,待外设处理完毕之后, CPU 便可以从外设读到请求的处理结果。比如在从作为外部存储的磁盘上读取数据的时候,CPU 将要读取的扇区的编号以及读到的数据放到的物理地址传给磁盘,在磁盘对请求进行调度并完成数据拷贝之后,就能在物理内存中看到要读取的数据。 + +在一个应用对外设发出了请求之后,它不能立即向下执行,而是要等待外设将请求处理完毕并拿到完整的处理结果之后才能继续。那么如何知道外设是否已经完成了请求呢?通常外设会提供一个可读的寄存器记录它目前的工作状态,于是 CPU 需要不断原地循环读取它直到它的结果显示设备已经将请求处理完毕了,才能向下执行。然而,外设的计算速度和 CPU 相比可能慢了几个数量级,这就导致 CPU 有大量时间浪费在等待外设这件事情上,这段时间它几乎没有做任何事情,也在一定程度上造成了 CPU 的利用率不够理想。 + +我们暂时考虑 CPU 只能 *单方面* 通过读取外设提供的寄存器来获取外设请求处理的状态。多道程序的思想在于:内核同时管理多个应用。如果外设处理的时间足够长,那我们可以先进行任务切换去执行其他应用,在某次切换回来之后,应用再次读取设备寄存器,发现请求已经处理完毕了,那么就可以用拿到的完整的数据继续向下执行了。这样的话,只要同时存在的应用足够多,就能保证 CPU 不必浪费时间在等待外设上,而是几乎一直在进行计算。这种任务切换,是通过应用进行一个名为 ``sys_yield`` 的系统调用来实现的,这意味着它主动交出 CPU 的使用权给其他应用。 + +这正是本节标题的后半部分“协作式”的含义。一个应用会持续运行下去,直到它主动调用 ``sys_yield`` 来交出 CPU 使用权。内核将很大的权力下放到应用,让所有的应用互相协作来最终达成最大化 CPU 利用率,充分利用计算资源这一终极目标。在计算机发展的早期,由于应用基本上都是一些简单的计算任务,且程序员都比较遵守规则,因此内核可以信赖应用,这样协作式的制度是没有问题的。 + +.. image:: multiprogramming.png + +上图描述了一种多道程序执行的典型情况。其中横轴为时间线,纵轴为正在执行的实体。开始时,某个应用(蓝色)向外设提交了一个请求,随即可以看到对应的外设(紫色)开始工作。但是它要工作相当长的一段时间,因此应用(蓝色)不会去等待它结束而是会调用 ``sys_yield`` 主动交出 CPU 使用权来切换到另一个应用(绿色)。另一个应用(绿色)在执行了一段时间之后调用了 ``sys_yield`` ,此时内核决定让应用(蓝色)继续执行。它检查了一下外设的工作状态,发现请求尚未处理完,于是再次调用 ``sys_yield`` 。然后另一个应用(绿色)执行了一段时间之后 ``sys_yield`` 再次切换回这个应用(蓝色),这次的不同是它发现外设已经处理完请求了,于是它终于可以向下执行了。 + +上面我们是通过“避免无谓的外设等待来提高 CPU 利用率”这一切入点来引入 ``sys_yield`` 。但其实调用 ``sys_yield`` 不一定与外设有关。随着内核功能的逐渐复杂,我们还会遇到很多其他类型的需要等待其完成才能继续向下执行的事件,我们都可以立即调用 ``sys_yield`` 来避免等待过程造成的浪费。 + +.. note:: + + **sys_yield 的缺点** + + 请读者思考一下, ``sys_yield`` 存在哪些缺点? + + 当应用调用它主动交出 CPU 使用权之后,它下一次再被允许使用 CPU 的时间点与内核的调度策略与当前的总体应用执行情况有关,很有可能远远迟于该应用等待的事件(如外设处理完请求)达成的时间点。这就会造成该应用的响应延迟不稳定,有可能极高。比如,设想一下,敲击键盘之后隔了数分钟之后才能在屏幕上看到字符,这已经超出了人类所能忍受的范畴。 + + 但也请不要担心,我们后面会有更加优雅的解决方案。 + +我们给出 ``sys_yield`` 的标准接口: + +.. code-block:: rust + :caption: 第三章新增系统调用(一) + + /// 功能:应用主动交出 CPU 所有权并切换到其他应用。 + /// 返回值:总是返回 0。 + /// syscall ID:124 + fn sys_yield() -> isize; + +然后是用户库对应的实现和封装: + +.. code-block:: rust + + // user/src/syscall.rs + + pub fn sys_yield() -> isize { + syscall(SYSCALL_YIELD, [0, 0, 0]) + } + + // user/src/lib.rs + + pub fn yield_() -> isize { sys_yield() } + +注意 ``yield`` 是 Rust 的关键字,因此我们只能将应用直接调用的接口命名为 ``yield_`` 。 + +接下来我们介绍内核应如何实现该系统调用。 + +任务控制块与任务运行状态 +--------------------------------------------------------- + +在第二章批处理系统中我们只需知道目前执行到第几个应用就行了,因为同一时间内核只管理一个应用,当它出错或退出之后内核会 +将其替换为另一个。然而,一旦引入了任务切换机制就没有那么简单了,同一时间内核需要管理多个未完成的应用,而且我们不能对 +应用完成的顺序做任何假定,并不是先加入的应用就一定会先完成。这种情况下,我们必须在内核中对每个应用分别维护它的运行 +状态,目前有如下几种: + +.. code-block:: rust + :linenos: + + // os/src/task/task.rs + + #[derive(Copy, Clone, PartialEq)] + pub enum TaskStatus { + UnInit, // 未初始化 + Ready, // 准备运行 + Running, // 正在运行 + Exited, // 已退出 + } + +.. note:: + + **Rust 语法卡片:#[derive]** + + 通过 ``#[derive(...)]`` 可以让编译器为你的类型提供一些 Trait 的默认实现。 + + - 实现了 ``Clone`` Trait 之后就可以调用 ``clone`` 函数完成拷贝; + - 实现了 ``PartialEq`` Trait 之后就可以使用 ``==`` 运算符比较该类型的两个实例,从逻辑上说只有 + 两个相等的应用执行状态才会被判为相等,而事实上也确实如此。 + - ``Copy`` 是一个标记 Trait,决定该类型在按值传参/赋值的时候取移动语义还是复制语义。 + + +.. _term-task-control-block: + +仅仅有这个是不够的,内核还需要保存一个应用的更多信息,我们将它们都保存在一个名为 **任务控制块** (Task Control Block) 的数据结构中: + +.. code-block:: rust + :linenos: + + // os/src/task/task.rs + + pub struct TaskControlBlock { + pub task_cx_ptr: usize, + pub task_status: TaskStatus, + } + + impl TaskControlBlock { + pub fn get_task_cx_ptr2(&self) -> *const usize { + &self.task_cx_ptr as *const usize + } + } + +可以看到我们还在 ``task_cx_ptr`` 字段中维护了一个上一小节中提到的指向应用被切换出去的时候,它内核栈栈顶的任务上下文的指针。而在任务切换函数 ``__switch`` 中我们需要用这个 ``task_cx_ptr`` 的指针作为参数并代表这个应用,于是 ``TaskControlBlock`` 还提供了获取这个指针的指针 ``task_cx_ptr2`` 的方法 ``get_task_cx_ptr2`` 。 + +任务控制块非常重要。在内核中,它就是应用的管理单位。在后面的章节我们还会不断向里面添加更多内容。 + +任务管理器 +-------------------------------------- + +我们还需要一个全局的任务管理器来管理这些用任务控制块描述的应用: + +.. code-block:: rust + + // os/src/task/mod.rs + + pub struct TaskManager { + num_app: usize, + inner: RefCell, + } + + struct TaskManagerInner { + tasks: [TaskControlBlock; MAX_APP_NUM], + current_task: usize, + } + + unsafe impl Sync for TaskManager {} + +其中仍然使用到了变量与常量分离的编程风格:字段 ``num_app`` 仍然表示任务管理器管理的应用的数目,它在 ``TaskManager`` 初始化之后就不会发生变化;而包裹在 ``TaskManagerInner`` 内的任务控制块数组 ``tasks`` 以及表示 CPU 正在执行的应用编号 ``current_task`` 会在执行应用的过程中发生变化: 每个应用的运行状态都会发生变化,而 CPU 执行的应用也在不断切换。 + +再次强调,这里的 ``current_task`` 与第二章批处理系统中的含义不同。在批处理系统中,它表示一个既定的应用序列中的执行进度,隐含着在该应用之前的都已经执行完毕,之后都没有执行;而在这里我们只能通过它知道 CPU 正在执行哪个应用,而不能获得其他应用的任何信息。 + +我们在使用之前初始化 ``TaskManager`` 的全局实例 ``TASK_MANAGER`` (为此也需要将 ``TaskManager`` 标记为 ``Sync``): + +.. code-block:: rust + :linenos: + + // os/src/task/mod.rs + + lazy_static! { + pub static ref TASK_MANAGER: TaskManager = { + let num_app = get_num_app(); + let mut tasks = [ + TaskControlBlock { task_cx_ptr: 0, task_status: TaskStatus::UnInit }; + MAX_APP_NUM + ]; + for i in 0..num_app { + tasks[i].task_cx_ptr = init_app_cx(i) as * const _ as usize; + tasks[i].task_status = TaskStatus::Ready; + } + TaskManager { + num_app, + inner: RefCell::new(TaskManagerInner { + tasks, + current_task: 0, + }), + } + }; + } + +- 第 5 行:调用 ``loader`` 子模块提供的 ``get_num_app`` 接口获取链接到内核的应用总数,后面会用到; +- 第 6~9 行:创建一个初始化的 ``tasks`` 数组,其中的每个任务控制块的运行状态都是 ``UnInit`` 代表尚未初始化; +- 第 10~12 行:依次对每个任务控制块进行初始化,将其运行状态设置为 ``Ready`` ,并在它的内核栈栈顶压入一些初始化 + 的上下文,然后更新它的 ``task_cx_ptr`` 。一些细节我们会稍后介绍。 +- 从第 14 行开始:创建 ``TaskManager`` 实例并返回。 + +实现 sys_yield 和 sys_exit +---------------------------------------------------------------------------- + +``sys_yield`` 的实现用到了 ``task`` 子模块提供的 ``suspend_current_and_run_next`` 接口: + +.. code-block:: rust + + // os/src/syscall/process.rs + + use crate::task::suspend_current_and_run_next; + + pub fn sys_yield() -> isize { + suspend_current_and_run_next(); + 0 + } + +这个接口如字面含义,就是暂停当前的应用并切换到下个应用。 + +同样, ``sys_exit`` 也改成基于 ``task`` 子模块提供的 ``exit_current_and_run_next`` 接口: + +.. code-block:: rust + + // os/src/syscall/process.rs + + use crate::task::exit_current_and_run_next; + + pub fn sys_exit(exit_code: i32) -> ! { + println!("[kernel] Application exited with code {}", exit_code); + exit_current_and_run_next(); + panic!("Unreachable in sys_exit!"); + } + +它的含义是退出当前的应用并切换到下个应用。在调用它之前我们打印应用的退出信息并输出它的退出码。如果是应用出错也应该调用该接口,不过我们这里并没有实现,有兴趣的读者可以尝试。 + +那么 ``suspend_current_and_run_next`` 和 ``exit_current_and_run_next`` 各是如何实现的呢? + +.. code-block:: rust + + // os/src/task/mod.rs + + pub fn suspend_current_and_run_next() { + mark_current_suspended(); + run_next_task(); + } + + pub fn exit_current_and_run_next() { + mark_current_exited(); + run_next_task(); + } + +它们都是先修改当前应用的运行状态,然后尝试切换到下一个应用。修改运行状态比较简单,实现如下: + +.. code-block:: rust + :linenos: + + // os/src/task/mod.rs + + fn mark_current_suspended() { + TASK_MANAGER.mark_current_suspended(); + } + + fn mark_current_exited() { + TASK_MANAGER.mark_current_exited(); + } + + impl TaskManager { + fn mark_current_suspended(&self) { + let mut inner = self.inner.borrow_mut(); + let current = inner.current_task; + inner.tasks[current].task_status = TaskStatus::Ready; + } + + fn mark_current_exited(&self) { + let mut inner = self.inner.borrow_mut(); + let current = inner.current_task; + inner.tasks[current].task_status = TaskStatus::Exited; + } + } + +以 ``mark_current_suspended`` 为例。它调用了全局任务管理器 ``TASK_MANAGER`` 的 ``mark_current_suspended`` 方法。其中,首先获得里层 ``TaskManagerInner`` 的可变引用,然后根据其中记录的当前正在执行的应用 ID 对应在任务控制块数组 ``tasks`` 中修改状态。 + +接下来看看 ``run_next_task`` 的实现: + +.. code-block:: rust + :linenos: + + // os/src/task/mod.rs + + fn run_next_task() { + TASK_MANAGER.run_next_task(); + } + + impl TaskManager { + fn run_next_task(&self) { + if let Some(next) = self.find_next_task() { + let mut inner = self.inner.borrow_mut(); + let current = inner.current_task; + inner.tasks[next].task_status = TaskStatus::Running; + inner.current_task = next; + let current_task_cx_ptr2 = inner.tasks[current].get_task_cx_ptr2(); + let next_task_cx_ptr2 = inner.tasks[next].get_task_cx_ptr2(); + core::mem::drop(inner); + unsafe { + __switch( + current_task_cx_ptr2, + next_task_cx_ptr2, + ); + } + } else { + panic!("All applications completed!"); + } + } + } + +``run_next_task`` 使用任务管理器的全局实例 ``TASK_MANAGER`` 的 ``run_next_task`` 方法。它会调用 ``find_next_task`` 方法尝试寻找一个运行状态为 ``Ready`` 的应用并返回其 ID 。注意到其返回的类型是 ``Option`` ,也就是说不一定能够找到,当所有的应用都退出并将自身状态修改为 ``Exited`` 就会出现这种情况,此时 ``find_next_task`` 应该返回 ``None`` 。如果能够找到下一个可运行的应用的话,我们就可以分别拿到当前应用 ``current`` 和即将被切换到的应用 ``next`` 的 ``task_cx_ptr2`` ,然后调用 ``__switch`` 接口进行切换。如果找不到的话,说明所有的应用都运行完毕了,我们可以直接 panic 退出内核。 + +注意在实际切换之前我们需要手动 drop 掉我们获取到的 ``TaskManagerInner`` 的可变引用。因为一般情况下它是在函数退出之后才会被自动释放,从而 ``TASK_MANAGER`` 的 ``inner`` 字段得以回归到未被借用的状态,之后可以再借用。如果不手动 drop 的话,编译器会在 ``__switch`` 返回,也就是当前应用被切换回来的时候才 drop,这期间我们都不能修改 ``TaskManagerInner`` ,甚至不能读(因为之前是可变借用)。正因如此,我们需要在 ``__switch`` 前提早手动 drop 掉 ``inner`` 。 + +于是 ``find_next_task`` 又是如何实现的呢? + +.. code-block:: rust + :linenos: + + // os/src/task/mod.rs + + impl TaskManager { + fn find_next_task(&self) -> Option { + let inner = self.inner.borrow(); + let current = inner.current_task; + (current + 1..current + self.num_app + 1) + .map(|id| id % self.num_app) + .find(|id| { + inner.tasks[*id].task_status == TaskStatus::Ready + }) + } + } + +``TaskManagerInner`` 的 ``tasks`` 是一个固定的任务控制块组成的表,长度为 ``num_app`` ,可以用下标 ``0~num_app-1`` 来访问得到每个应用的控制状态。我们的任务就是找到 ``current_task`` 后面第一个状态为 ``Ready`` 的应用。因此从 ``current_task + 1`` 开始循环一圈,需要首先对 ``num_app`` 取模得到实际的下标,然后检查它的运行状态。 + +.. note:: + + **Rust 语法卡片:迭代器** + + ``a..b`` 实际上表示左闭右开区间 :math:`[a,b)` ,在 Rust 中,它会被表示为类型 ``core::ops::Range`` ,标准库中为它实现好了 ``Iterator`` trait,因此它也是一个迭代器。 + + 关于迭代器的使用方法如 ``map/find`` 等,请参考 Rust 官方文档。 + +我们可以总结一下应用的运行状态变化图: + +.. image:: fsm-coop.png + +第一次进入用户态 +------------------------------------------ + +在应用真正跑起来之前,需要 CPU 第一次从内核态进入用户态。我们在第二章批处理系统中也介绍过实现方法,只需在内核栈上压入构造好的 Trap 上下文,然后 ``__restore`` 即可。本章的思路大致相同,但是有一些变化。 + +当一个应用即将被运行的时候,它会被 ``__switch`` 过来。如果它是之前被切换出去的话,那么此时它的内核栈上应该有 Trap 上下文和任务上下文,切换机制可以正常工作。但是如果它是第一次被执行怎么办呢?这就需要它的内核栈上也有类似结构的内容。我们是在创建 ``TaskManager`` 的全局实例 ``TASK_MANAGER`` 的时候来进行这个初始化的。 + +.. code-block:: rust + + // os/src/task/mod.rs + + for i in 0..num_app { + tasks[i].task_cx_ptr = init_app_cx(i) as * const _ as usize; + tasks[i].task_status = TaskStatus::Ready; + } + +当时我们进行了这样的操作。 ``init_app_cx`` 是在 ``loader`` 子模块中定义的: + +.. code-block:: rust + + // os/src/loader.rs + + pub fn init_app_cx(app_id: usize) -> &'static TaskContext { + KERNEL_STACK[app_id].push_context( + TrapContext::app_init_context(get_base_i(app_id), USER_STACK[app_id].get_sp()), + TaskContext::goto_restore(), + ) + } + + impl KernelStack { + fn get_sp(&self) -> usize { + self.data.as_ptr() as usize + KERNEL_STACK_SIZE + } + pub fn push_context(&self, trap_cx: TrapContext, task_cx: TaskContext) -> &'static mut TaskContext { + unsafe { + let trap_cx_ptr = (self.get_sp() - core::mem::size_of::()) as *mut TrapContext; + *trap_cx_ptr = trap_cx; + let task_cx_ptr = (trap_cx_ptr as usize - core::mem::size_of::()) as *mut TaskContext; + *task_cx_ptr = task_cx; + task_cx_ptr.as_mut().unwrap() + } + } + } + +这里 ``KernelStack`` 的 ``push_context`` 方法先压入一个和之前相同的 Trap 上下文,再在它上面压入一个任务上下文,然后返回任务上下文的地址。这个任务上下文是我们通过 ``TaskContext::goto_restore`` 构造的: + +.. code-block:: rust + + // os/src/task/context.rs + + impl TaskContext { + pub fn goto_restore() -> Self { + extern "C" { fn __restore(); } + Self { + ra: __restore as usize, + s: [0; 12], + } + } + } + +它只是将任务上下文的 ``ra`` 寄存器设置为 ``__restore`` 的入口地址。这样,在 ``__switch`` 从它上面恢复并返回之后就会直接跳转到 ``__restore`` ,此时栈顶是一个我们构造出来第一次进入用户态执行的 Trap 上下文,就和第二章的情况一样了。 + +需要注意的是, ``__restore`` 的实现需要做出变化:它 **不再需要** 在开头 ``mv sp, a0`` 了。因为在 ``__switch`` 之后,``sp`` 就已经正确指向了我们需要的 Trap 上下文地址。 + + +在 ``rust_main`` 中我们调用 ``task::run_first_task`` 来开始应用的执行: + +.. code-block:: rust + :linenos: + + // os/src/task/mod.rs + + impl TaskManager { + fn run_first_task(&self) { + self.inner.borrow_mut().tasks[0].task_status = TaskStatus::Running; + let next_task_cx_ptr2 = self.inner.borrow().tasks[0].get_task_cx_ptr2(); + let _unused: usize = 0; + unsafe { + __switch( + &_unused as *const _, + next_task_cx_ptr2, + ); + } + } + } + + pub fn run_first_task() { + TASK_MANAGER.run_first_task(); + } + +这里我们取出即将最先执行的编号为 0 的应用的 ``task_cx_ptr2`` 并希望能够切换过去。注意 ``__switch`` 有两个参数分别表示当前应用和即将切换到的应用的 ``task_cx_ptr2`` ,其第一个参数存在的意义是记录当前应用的任务上下文被保存在哪里,也就是当前应用内核栈的栈顶,这样之后才能继续执行该应用。但在 ``run_first_task`` 的时候,我们并没有执行任何应用, ``__switch`` 前半部分的保存仅仅是在启动栈上保存了一些之后不会用到的数据,自然也无需记录启动栈栈顶的位置。 + +因此,我们显式声明了一个 ``_unused`` 变量,并将它的地址作为第一个参数传给 ``__switch`` ,这样保存一些寄存器之后的启动栈栈顶的位置将会保存在此变量中。然而无论是此变量还是启动栈我们之后均不会涉及到,一旦应用开始运行,我们就开始在应用的用户栈和内核栈之间开始切换了。这里声明此变量的意义仅仅是为了避免覆盖到其他数据。 + + +三叠纪“始初龙”协作式操作系统 +--------------------------------- + +简介与画图!!! \ No newline at end of file diff --git a/source/chapter3/4time-sharing-system.rst b/source/chapter3/4time-sharing-system.rst new file mode 100644 index 0000000000000000000000000000000000000000..7a17eaf0823e34f59bed35c61fbc2135cdc1f478 --- /dev/null +++ b/source/chapter3/4time-sharing-system.rst @@ -0,0 +1,301 @@ +分时多任务系统与抢占式调度 +=========================================================== + +本节导读 +-------------------------- + +本节的重点是操作系统对中断的处理和对应用程序的抢占。为此,对 **任务** 的概念进行进一步扩展和延伸: + +- 分时多任务:操作系统管理每个应用程序,以时间片为单位来分时占用处理器运行应用。 +- 时间片轮转调度:操作系统在一个程序用完其时间片后,就抢占当前程序并调用下一个程序执行,周而复始,形成对应用程序在任务级别上的时间片轮转调度。 + + +分时多任务系统的背景 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. _term-throughput: + +上一节我们介绍了多道程序,它是一种允许应用在等待外设时主动切换到其他应用来达到总体 CPU 利用率最高的设计。那个时候,应用是不太注重自身的运行情况的,即使它 yield 交出 CPU 资源之后需要很久才能再拿到,使得它真正在 CPU 执行的相邻两时间段距离都很远,应用也是无所谓的。因为它们的目标是总体 CPU 利用率最高,可以换成一个等价的指标: **吞吐量** (Throughput) 。大概可以理解为在某个时间点将一组应用放进去,要求在一段固定的时间之内执行完毕的应用最多,或者是总进度百分比最大。因此,所有的应用和编写应用的程序员都有这样的共识:只要 CPU 一直在做实际的工作就好。 + +.. _term-background-application: +.. _term-interactive-application: +.. _term-latency: + +从现在的眼光来看,当时的应用更多是一种 **后台应用** (Background Application) ,在将它加入执行队列之后我们只需定期确认它的运行状态。而随着技术的发展,涌现了越来越多的 **交互式应用** (Interactive Application) ,它们要达成的一个重要目标就是提高用户操作的响应速度,这样才能优化应用的使用体验。对于这些应用而言,即使需要等待外设或某些事件,它们也不会倾向于主动 yield 交出 CPU 使用权,因为这样可能会带来无法接受的延迟。也就是说,应用之间相比合作更多的是互相竞争宝贵的硬件资源。 + +.. _term-cooperative-scheduling: +.. _term-preemptive-scheduling: + +如果应用自己很少 yield ,内核就要开始收回之前下放的权力,由它自己对 CPU 资源进行集中管理并合理分配给各应用,这就是内核需要提供的任务调度能力。我们可以将多道程序的调度机制分类成 **协作式调度** (Cooperative Scheduling) ,因为它的特征是:只要一个应用不主动 yield 交出 CPU 使用权,它就会一直执行下去。与之相对, **抢占式调度** (Preemptive Scheduling) 则是应用 *随时* 都有被内核切换出去的可能。 + +.. _term-time-slice: +.. _term-fairness: + +现代的任务调度算法基本都是抢占式的,它要求每个应用只能连续执行一段时间,然后内核就会将它强制性切换出去。一般将 **时间片** (Time Slice) 作为应用连续执行时长的度量单位,每个时间片可能在毫秒量级。调度算法需要考虑:每次在换出之前给一个应用多少时间片去执行,以及要换入哪个应用。可以从性能和 **公平性** (Fairness) 两个维度来评价调度算法,后者要求多个应用分到的时间片占比不应差距过大。 + + + +时间片轮转调度 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. _term-round-robin: + +简单起见,本书中我们使用 **时间片轮转算法** (RR, Round-Robin) 来对应用进行调度,只要对它进行少许拓展就能完全满足我们续的需求。本章中我们仅需要最原始的 RR 算法,用文字描述的话就是维护一个任务队列,每次从队头取出一个应用执行一个时间片,然后把它丢到队尾,再继续从队头取出一个应用,以此类推直到所有的应用执行完毕。 + + +本节的代码可以在 ``ch3`` 分支上找到。 + + +RISC-V 架构中的中断 +----------------------------------- + +.. _term-interrupt: +.. _term-sync: +.. _term-async: + + +时间片轮转调度的核心机制就在于计时。操作系统的计时功能是依靠硬件提供的时钟中断来实现的。在介绍时钟中断之前,我们先简单介绍一下中断。 + +**中断** (Interrupt) 和我们第二章中介绍的 用于系统调用的 **陷入** ``Trap`` 一样都是异常 ,但是它们被触发的原因确是不同的。对于某个处理器核而言, **陷入** 与发起 **陷入** 的指令执行是 **同步** (Synchronous) 的, **陷入** 被触发的原因一定能够追溯到某条指令的执行;而中断则 **异步** (Asynchronous) 于当前正在进行的指令,也就是说中断来自于哪个外设以及中断如何触发完全与处理器正在执行的当前指令无关。 + +.. _term-parallel: + +.. note:: + + **从底层硬件的角度区分同步和异步** + + 从底层硬件的角度可能更容易理解这里所提到的同步和异步。以一个处理器传统的五级流水线设计而言,里面含有取指、译码、算术、 + 访存、寄存器等单元,都属于执行指令所需的硬件资源。那么假如某条指令的执行出现了问题,一定能被其中某个单元看到并反馈给流水线控制单元,从而它会在执行预定的下一条指令之前先进入异常处理流程。也就是说,异常在这些单元内部即可被发现并解决。 + + 而对于中断,可以想象为想发起中断的是一套完全不同的电路(从时钟中断来看就是简单的计数和比较器),这套电路仅通过一根导线接入进来,当想要触发中断的时候则输入一个高电平或正边沿,处理器会在每执行完一条指令之后检查一下这根线,看情况决定是继续执行接下来的指令还是进入中断处理流程。也就是说,大多数情况下,指令执行的相关硬件单元和可能发起中断的电路是完全独立 **并行** (Parallel) 运行的,它们中间只有一根导线相连,除此之外指令执行的那些单元就完全不知道对方处于什么状态了。 + +在不考虑指令集拓展的情况下,RISC-V 架构中定义了如下中断: + +.. list-table:: RISC-V 中断一览表 + :align: center + :header-rows: 1 + :widths: 30 30 60 + + * - Interrupt + - Exception Code + - Description + * - 1 + - 1 + - Supervisor software interrupt + * - 1 + - 3 + - Machine software interrupt + * - 1 + - 5 + - Supervisor timer interrupt + * - 1 + - 7 + - Machine timer interrupt + * - 1 + - 9 + - Supervisor external interrupt + * - 1 + - 11 + - Machine external interrupt + +RISC-V 的中断可以分成三类: + +.. _term-software-interrupt: +.. _term-timer-interrupt: +.. _term-external-interrupt: + +- **软件中断** (Software Interrupt) +- **时钟中断** (Timer Interrupt) +- **外部中断** (External Interrupt) + +另外,相比异常,中断和特权级之间的联系更为紧密,可以看到这三种中断每一个都有 M/S 特权级两个版本。中断的特权级可以决定该中断是否会被屏蔽,以及需要 Trap 到 CPU 的哪个特权级进行处理。 + +在判断中断是否会被屏蔽的时候,有以下规则: + +- 如果中断的特权级低于 CPU 当前的特权级,则该中断会被屏蔽,不会被处理; +- 如果中断的特权级高于与 CPU 当前的特权级或相同,则需要通过相应的 CSR 判断该中断是否会被屏蔽。 + +以内核所在的 S 特权级为例,中断屏蔽相应的 CSR 有 ``sstatus`` 和 ``sie`` 。``sstatus`` 的 ``sie`` 为 S 特权级的中断使能,能够同时控制三种中断,如果将其清零则会将它们全部屏蔽。即使 ``sstatus.sie`` 置 1 ,还要看 ``sie`` 这个 CSR,它的三个字段 ``ssie/stie/seie`` 分别控制 S 特权级的软件中断、时钟中断和外部中断的中断使能。比如对于 S 态时钟中断来说,如果 CPU 不高于 S 特权级,需要 ``sstatus.sie`` 和 ``sie.stie`` 均为 1 该中断才不会被屏蔽;如果 CPU 当前特权级高于 S 特权级,则该中断一定会被屏蔽。 + +如果中断没有被屏蔽,那么接下来就需要 Trap 进行处理,而具体 Trap 到哪个特权级与一些中断代理 CSR 的设置有关。默认情况下,所有的中断都需要 Trap 到 M 特权级处理。而设置这些代理 CSR 之后,就可以 Trap 到低特权级处理,但是 Trap 到的特权级不能低于中断的特权级。事实上所有的异常默认也都是 Trap 到 M 特权级处理的,它们也有一套对应的异常代理 CSR ,设置之后也可以 Trap 到低优先级来处理异常。 + +我们会在 :doc:`/appendix-c/index` 中再深入介绍中断/异常代理。在正文中我们只需要了解: + +- 包括系统调用(即来自 U 特权级的环境调用)在内的所有异常都会 Trap 到 S 特权级处理; +- 只需考虑 S 特权级的时钟/软件/外部中断,且它们都会被 Trap 到 S 特权级处理。 + +默认情况下,当 Trap 进入某个特权级之后,在 Trap 处理的过程中同特权级的中断都会被屏蔽。这里我们还需要对第二章介绍的 Trap 发生时的硬件机制做一下补充,同样以 Trap 到 S 特权级为例: + +- 当 Trap 发生时,``sstatus.sie`` 会被保存在 ``sstatus.spie`` 字段中,同时 ``sstatus.sie`` 置零,这也就在 Trap 处理的过程中屏蔽了所有 S 特权级的中断; +- 当 Trap 处理完毕 ``sret`` 的时候, ``sstatus.sie`` 会恢复到 ``sstatus.spie`` 内的值。 + +.. _term-nested-interrupt: + +也就是说,如果不去手动设置 ``sstatus`` CSR ,在只考虑 S 特权级中断的情况下,是不会出现 **嵌套中断** (Nested Interrupt) 的。嵌套中断是指在处理一个中断的过程中再一次触发了中断从而通过 Trap 来处理。由于默认情况下一旦进入 Trap 硬件就自动禁用所有同特权级中断,自然也就不会再次触发中断导致嵌套中断了。 + +.. note:: + + **嵌套中断与嵌套 Trap** + + 嵌套中断可以分为两部分:在处理一个中断的过程中又被同特权级/高特权级中断所打断。默认情况下硬件会避免前一部分,也可以通过手动设置来允许前一部分的存在;而从上面介绍的规则可以知道,后一部分则是无论如何设置都不可避免的。 + + 嵌套 Trap 则是指处理一个 Trap 过程中又再次发生 Trap ,嵌套中断算是嵌套 Trap 的一部分。 + +.. note:: + + **RISC-V 架构的 U 特权级中断** + + 目前,RISC-V 用户态中断作为代号 N 的一个指令集拓展而存在。有兴趣的读者可以阅读最新版的 RISC-V 特权级架构规范一探究竟。 + + +时钟中断与计时器 +------------------------------------------------------------------ + +由于需要一种计时机制,RISC-V 架构要求处理器要有一个内置时钟,其频率一般低于 CPU 主频。此外,还有一个计数器统计处理器自上电以来经过了多少个内置时钟的时钟周期。在 RV64 架构上,该计数器保存在一个 64 位的 CSR ``mtime`` 中,我们无需担心它的溢出问题,在内核运行全程可以认为它是一直递增的。 + +另外一个 64 位的 CSR ``mtimecmp`` 的作用是:一旦计数器 ``mtime`` 的值超过了 ``mtimecmp``,就会触发一次时钟中断。这使得我们可以方便的通过设置 ``mtimecmp`` 的值来决定下一次时钟中断何时触发。 + +可惜的是,它们都是 M 特权级的 CSR ,而我们的内核处在 S 特权级,是不被硬件允许直接访问它们的。好在运行在 M 特权级的 SEE 已经预留了相应的接口,我们可以调用它们来间接实现计时器的控制: + +.. code-block:: rust + + // os/src/timer.rs + + use riscv::register::time; + + pub fn get_time() -> usize { + time::read() + } + +``timer`` 子模块的 ``get_time`` 函数可以取得当前 ``mtime`` 计数器的值; + +.. code-block:: rust + :linenos: + + // os/src/sbi.rs + + const SBI_SET_TIMER: usize = 0; + + pub fn set_timer(timer: usize) { + sbi_call(SBI_SET_TIMER, timer, 0, 0); + } + + // os/src/timer.rs + + use crate::config::CLOCK_FREQ; + const TICKS_PER_SEC: usize = 100; + + pub fn set_next_trigger() { + set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC); + } + +- 代码片段第 5 行, ``sbi`` 子模块有一个 ``set_timer`` 调用,是一个由 SEE 提供的标准 SBI 接口函数,它可以用来设置 ``mtimecmp`` 的值。 +- 代码片段第 14 行, ``timer`` 子模块的 ``set_next_trigger`` 函数对 ``set_timer`` 进行了封装,它首先读取当前 ``mtime`` 的值,然后计算出 10ms 之内计数器的增量,再将 ``mtimecmp`` 设置为二者的和。这样,10ms 之后一个 S 特权级时钟中断就会被触发。 + + 至于增量的计算方式, ``CLOCK_FREQ`` 是一个预先获取到的各平台不同的时钟频率,单位为赫兹,也就是一秒钟之内计数器的增量。它可以在 ``config`` 子模块中找到。10ms 的话只需除以常数 ``TICKS_PER_SEC`` 也就是 100 即可。 + +后面可能还有一些计时的操作,比如统计一个应用的运行时长的需求,我们再设计一个函数: + +.. code-block:: rust + + // os/src/timer.rs + + const MSEC_PER_SEC: usize = 1000; + + pub fn get_time_ms() -> usize { + time::read() / (CLOCK_FREQ / MSEC_PER_SEC) + } + +``timer`` 子模块的 ``get_time_ms`` 可以以毫秒为单位返回当前计数器的值,这让我们终于能对时间有一个具体概念了。实现原理就不再赘述。 + +我们也新增一个系统调用方便应用获取当前的时间,以毫秒为单位: + +.. code-block:: rust + :caption: 第三章新增系统调用(二) + + /// 功能:获取当前的时间,以毫秒为单位。 + /// 返回值:返回当前的时间,以毫秒为单位。 + /// syscall ID:169 + fn sys_get_time() -> isize; + +它在内核中的实现只需调用 ``get_time_ms`` 函数即可。 + + +抢占式调度 +----------------------------------- + +有了时钟中断和计时器,抢占式调度就很容易实现了: + +.. code-block:: rust + + // os/src/trap/mod.rs + + match scause.cause() { + Trap::Interrupt(Interrupt::SupervisorTimer) => { + set_next_trigger(); + suspend_current_and_run_next(); + } + } + +我们只需在 ``trap_handler`` 函数下新增一个分支,当发现触发了一个 S 特权级时钟中断的时候,首先重新设置一个 10ms 的计时器,然后调用上一小节提到的 ``suspend_current_and_run_next`` 函数暂停当前应用并切换到下一个。 + +为了避免 S 特权级时钟中断被屏蔽,我们需要在执行第一个应用之前进行一些初始化设置: + +.. code-block:: rust + :linenos: + :emphasize-lines: 9,10 + + // os/src/main.rs + + #[no_mangle] + pub fn rust_main() -> ! { + clear_bss(); + println!("[kernel] Hello, world!"); + trap::init(); + loader::load_apps(); + trap::enable_timer_interrupt(); + timer::set_next_trigger(); + task::run_first_task(); + panic!("Unreachable in rust_main!"); + } + + // os/src/trap/mod.rs + + use riscv::register::sie; + + pub fn enable_timer_interrupt() { + unsafe { sie::set_stimer(); } + } + +- 第 9 行设置了 ``sie.stie`` 使得 S 特权级时钟中断不会被屏蔽; +- 第 10 行则是设置第一个 10ms 的计时器。 + +这样,当一个应用运行了 10ms 之后,一个 S 特权级时钟中断就会被触发。由于应用运行在 U 特权级,且 ``sie`` 寄存器被正确设置,该中断不会被屏蔽,而是 Trap 到 S 特权级内的我们的 ``trap_handler`` 里面进行处理,并顺利切换到下一个应用。这便是我们所期望的抢占式调度机制。从应用运行的结果也可以看出,三个 ``power`` 系列应用并没有进行 yield ,而是由内核负责公平分配它们执行的时间片。 + +目前在等待某些事件的时候仍然需要 yield ,其中一个原因是为了节约 CPU 计算资源,另一个原因是当事件依赖于其他的应用的时候,由于只有一个 CPU ,当前应用的等待可能永远不会结束。这种情况下需要先将它切换出去,使得其他的应用到达它所期待的状态并满足事件的生成条件,再切换回来。 + +.. _term-busy-loop: + +这里我们先通过 yield 来优化 **轮询** (Busy Loop) 过程带来的 CPU 资源浪费。在 ``03sleep`` 这个应用中: + +.. code-block:: rust + + // user/src/bin/03sleep.rs + + #[no_mangle] + fn main() -> i32 { + let current_timer = get_time(); + let wait_for = current_timer + 3000; + while get_time() < wait_for { + yield_(); + } + println!("Test sleep OK!"); + 0 + } + +它的功能是等待 3000ms 然后退出。可以看出,我们会在循环里面 ``yield_`` 来主动交出 CPU 而不是无意义的忙等。尽管我们不这样做,已有的抢占式调度还是会在它循环 10ms 之后切换到其他应用,但是这样能让内核给其他应用分配更多的 CPU 资源并让它们更早运行结束。 + +三叠纪“腔骨龙”抢占式操作系统 +--------------------------------- + +简介与画图!!! \ No newline at end of file diff --git a/source/chapter3/5exercise.rst b/source/chapter3/5exercise.rst new file mode 100644 index 0000000000000000000000000000000000000000..292f91b04283b646826b514f3630e14d9fc41948 --- /dev/null +++ b/source/chapter3/5exercise.rst @@ -0,0 +1,176 @@ +chapter3练习 +======================================= + +- 本节难度: **并不那么简单了!早点动手** + +编程作业 +-------------------------------------- + +stride 调度算法 ++++++++++++++++++++++++++++++++++++++++++ + +lab3中我们引入了任务调度的概念,可以在不同任务之间切换,目前我们实现的调度算法十分简单,存在一些问题且不存在优先级。现在我们要为我们的 os 实现一种带优先级的调度算法:stide 调度算法。 + +算法描述如下: + +(1) 为每个进程设置一个当前 stride,表示该进程当前已经运行的“长度”。另外设置其对应的 pass 值(只与进程的优先权有关系),表示对应进程在调度后,stride 需要进行的累加值。 + +(2) 每次需要调度时,从当前 runnable 态的进程中选择 stride 最小的进程调度。对于获得调度的进程 P,将对应的 stride 加上其对应的步长 pass。 + +(3) 一个时间片后,回到上一步骤,重新调度当前 stride 最小的进程。 + +可以证明,如果令 P.pass = BigStride / P.priority 其中 P.priority 表示进程的优先权(大于 1),而 BigStride 表示一个预先定义的大常数,则该调度方案为每个进程分配的时间将与其优先级成正比。证明过程我们在这里略去,有兴趣的同学可以在网上查找相关资料。 + +其他实验细节: + +- stride 调度要求进程优先级 :math:`\geq 2`,所以设定进程优先级 :math:`\leq 1` 会导致错误。 +- 进程初始 stride 设置为 0 即可。 +- 进程初始优先级设置为 16。 + +tips: 可以使用优先级队列比较方便的实现 stride 算法。如使用:`BinaryHeap `_ 。 + +实验要求 ++++++++++++++++++++++++++++++++++++++++++ + +- 完成分支: ch3。 + +- 完成实验指导书中的内容,实现 sys_yield,实现协作式和抢占式的调度。 + +- 实现 stride 调度算法,实现 sys_gettime, sys_set_priority 两个系统调用并通过 `Rust测例 `_ 中 chapter3 对应的所有测例,测例详情见对应仓库,系统调用具体要求参考 `guide.md `_ 。 + +.. _gettime-semantic-diff: + +.. note:: + + **sys_gettime 在测例和教程正文中语义的不同** + + 为了更加贴近 POSIX 标准系统调用接口,在测例中 ``sys_gettime`` 需要将当前时间保存在一个 ``TimeVal`` 中,但是在用户库 ``user_lib`` 中的 ``get_time`` 函数仍然是以毫秒为单位,它的实现方式是将 ``TimeVal`` 中的秒数 ``sec`` 和微秒数 ``usec`` 转化为合计的毫秒数。因此,如果基于实验框架来做的话, ``sys_gettime`` 在内核中的实现需要发生变化。 + + 另外需要注意的是,在修改之后, ``sys_gettime`` 和 POSIX 标准接口也仅仅做到了格式相同。在 POSIX 标准接口中 ``sys_gettime`` 统计当前相对 1970-01-01 00:00:00 +0000 (UTC) 过去的时间,而我们并没有用到任何 RTC 外设,只能做到统计自开机之后过去的时间。 + +需要说明的是 lab3 有3类测例,``ch3_0_*`` 用来检查基本 syscall 的实现,``ch3_1_*`` 基于 yield 来检测基本的调度,``ch3_2_*`` 基于时钟中断来测试 stride 调度算法实现的正确性。测试时可以分别测试 3 组测例,使得输出更加可控、更加清晰。 + +特别的,我们有一个死循环测例 ``ch3t_deadloop`` 用来保证大家真的实现了时钟中断。这一章中我们人为限制一个程序执行的最大时间(必须很大),超过就杀死,这样,我们的程序更不容易被恶意程序伤害。这一规定可以在实验4开始删除,仅仅为通过 lab3 测例设置。 + +challenge: 实现多核,可以并行调度。 + +实验检查 +++++++++++++++++++++++++++++++++++++++++ + +- 实验目录要求 + + 目录要求不变(参考lab1目录或者示例代码目录结构)。同样在 os 目录下 `make run` 之后可以正确加载用户程序并执行。 + + 目标用户目录 ``../user/build/bin``。 + +- 检查 + + 可以正确 `make run` 执行,可以正确执行目标用户测例,并得到预期输出(详见测例注释)。 + +简答作业 +-------------------------------------------- + +(1) 简要描述这一章的进程调度策略。何时进行进程切换?如何选择下一个运行的进程?如何处理新加入的进程? + +(2) 在 C 版代码中,同样实现了类似 RR 的调度算法,但是由于没有 VecDeque 这样直接可用的数据结构(Rust很棒对不对),C 版代码的实现严格来讲存在一定问题。大致情况如下:C版代码使用一个进程池(也就是一个 struct proc 的数组)管理进程调度,当一个时间片用尽后,选择下一个进程逻辑在 `chapter3相关代码 `_ ,也就是当第 i 号进程结束后,会以 i -> max_num -> 0 -> i 的顺序遍历进程池,直到找到下一个就绪进程。C 版代码新进程在调度池中的位置选择见 `chapter5相关代码 `_ ,也就是从头到尾遍历进程池,找到第一个空位。 + + (2-1) 在目前这一章(chapter3)两种调度策略有实质不同吗?考虑在一个完整的 os 中,随时可能有新进程产生,这两种策略是否实质相同? + + (2-2) 其实 C 版调度策略在公平性上存在比较大的问题,请找到一个进程产生和结束的时间序列,使得在该调度算法下发生:先创建的进程后执行的现象。你需要给出类似下面例子的信息(有更详细的分析描述更好,但尽量精简)。同时指出该序列在你实现的 stride 调度算法下顺序是怎样的? + + .. list-table:: 调度顺序举例 + :header-rows: 1 + :align: center + + * - 时间点 + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + * - 运行进程 + - + - p1 + - p2 + - p3 + - p4 + - p1 + - p3 + - + * - 事件 + - p1、p2、p3产生 + - + - p2 结束 + - p4 产生 + - p4 结束 + - p1 结束 + - p3 结束 + - + + 产生顺序:p1、p2、p3、p4。第一次执行顺序: p1、p2、p3、p4。没有违反公平性。 + + 其他细节:允许进程在其他进程执行时产生(也就是被当前进程创建)/结束(也就是被当前进程杀死)。 + +(3) stride 算法深入 + + stride算法原理非常简单,但是有一个比较大的问题。例如两个 pass = 10 的进程,使用 8bit 无符号整形储存 stride, p1.stride = 255, p2.stride = 250,在 p2 执行一个时间片后,理论上下一次应该 p1 执行。 + + - 实际情况是轮到 p1 执行吗?为什么? + + 我们之前要求进程优先级 >= 2 其实就是为了解决这个问题。可以证明,**在不考虑溢出的情况下**, 在进程优先级全部 >= 2 的情况下,如果严格按照算法执行,那么 STRIDE_MAX – STRIDE_MIN <= BigStride / 2。 + + - 为什么?尝试简单说明(传达思想即可,不要求严格证明)。 + + 已知以上结论,**考虑溢出的情况下**,我们可以通过设计 Stride 的比较接口,结合 BinaryHeap 的 pop 接口可以很容易的找到真正最小的 Stride。 + + - 请补全如下 ``partial_cmp`` 函数(假设永远不会相等)。 + + .. code-block:: rust + + use core::cmp::Ordering; + + struct Stride(u64); + + impl PartialOrd for Stride { + fn partial_cmp(&self, other: &Self) -> Option { + // ... + } + } + + impl PartialEq for Person { + fn eq(&self, other: &Self) -> bool { + false + } + } + + 例如使用 8 bits 存储 stride, BigStride = 255, 则: + + - (125 < 255) == false + - (129 < 255) == true + + +报告要求 +------------------------------- + +- 简单总结与上次实验相比本次实验你增加的东西(控制在5行以内,不要贴代码)。 +- 完成问答问题。 +- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 + +参考信息 +------------------------------- +如果有兴趣进一步了解 stride 调度相关内容,可以尝试看看: + +- `作者 Carl A. Waldspurger 写这个调度算法的原论文 `_ +- `作者 Carl A. Waldspurger 的博士生答辩slide `_ +- `南开大学实验指导中对Stride算法的部分介绍 `_ +- `NYU OS课关于Stride Scheduling的Slide `_ + +如果有兴趣进一步了解用户态线程实现的相关内容,可以尝试看看: + +- `user-multitask in rv64 `_ +- `绿色线程 in x86 `_ +- `x86版绿色线程的设计实现 `_ +- `用户级多线程的切换原理 `_ diff --git a/source/chapter3/ch3.pptx b/source/chapter3/ch3.pptx new file mode 100644 index 0000000000000000000000000000000000000000..03bba87f9d2c62ceac6fa368928e689d86988199 Binary files /dev/null and b/source/chapter3/ch3.pptx differ diff --git a/source/chapter3/fsm-coop.png b/source/chapter3/fsm-coop.png new file mode 100644 index 0000000000000000000000000000000000000000..bc633bcd2b59f693f14c5a9ab9f90c62b0652f45 Binary files /dev/null and b/source/chapter3/fsm-coop.png differ diff --git a/source/chapter3/index.rst b/source/chapter3/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..d71d5a5bdae1f78fa9b124721104e3026210ebbe --- /dev/null +++ b/source/chapter3/index.rst @@ -0,0 +1,14 @@ +.. _link-chapter3: + +第三章:多道程序与分时多任务 +============================================== + +.. toctree:: + :maxdepth: 4 + + 0intro + 1multi-loader + 2task-switching + 3multiprogramming + 4time-sharing-system + 5exercise diff --git a/source/chapter3/multiprogramming.png b/source/chapter3/multiprogramming.png new file mode 100644 index 0000000000000000000000000000000000000000..7e4df49003f5a1bf229db490ffa6f1c42aa42085 Binary files /dev/null and b/source/chapter3/multiprogramming.png differ diff --git a/source/chapter3/switch-1.png b/source/chapter3/switch-1.png new file mode 100644 index 0000000000000000000000000000000000000000..9f539d2f8fa0229360d1d6b0b5152b7926baff92 Binary files /dev/null and b/source/chapter3/switch-1.png differ diff --git a/source/chapter3/switch-2.png b/source/chapter3/switch-2.png new file mode 100644 index 0000000000000000000000000000000000000000..72e1407f942097103323bfef258f0224b77d4944 Binary files /dev/null and b/source/chapter3/switch-2.png differ diff --git a/source/chapter3/task_context.png b/source/chapter3/task_context.png new file mode 100644 index 0000000000000000000000000000000000000000..3857dad5776518145af2dafd7a5f4136585b92c5 Binary files /dev/null and b/source/chapter3/task_context.png differ diff --git a/source/chapter4/0intro.rst b/source/chapter4/0intro.rst new file mode 100644 index 0000000000000000000000000000000000000000..97d8025b8e4af9f644e3de33a5ce0d1e573ed29d --- /dev/null +++ b/source/chapter4/0intro.rst @@ -0,0 +1,245 @@ +引言 +============================== + +本章导读 +------------------------------- + +.. + chyyuu:有一个ascii图,画出我们做的OS。 + + +本章展现了操作系统一系列功能: + +- 通过动态内存分配,提高了应用程序对内存的动态使用效率 +- 通过页表的虚实内存映射机制,简化了编译器对应用的地址空间设置 +- 通过页表的虚实内存映射机制,加强了应用之间,应用与内核之间的内存隔离,增强了系统安全 +- 通过页表的虚实内存映射机制,可以实现空分复用(提出,但没有实现) + +.. _term-illusion: +.. _term-time-division-multiplexing: +.. _term-transparent: + +上一章,我们分别实现了多道程序和分时多任务系统,它们的核心机制都是任务切换。由于多道程序和分时多任务系统的设计初衷不同,它们在任务切换的时机和策略也不同。有趣的一点是,任务切换机制对于应用是完全 **透明** (Transparent) 的,应用可以不对内核实现该机制的策略做任何假定(除非要进行某些针对性优化),甚至可以完全不知道这机制的存在。 + +在大多数应用(也就是应用开发者)的视角中,它们会独占一整个 CPU 和特定(连续或不连续)的内存空间。当然,通过上一章的学习,我们知道在现代操作系统中,出于公平性的考虑,我们极少会让独占CPU这种情况发生。所以应用自认为的独占CPU只是内核想让应用看到的一种 **幻象** (Illusion) ,而 CPU 计算资源被 **时分复用** (TDM, Time-Division Multiplexing) 的实质被内核通过恰当的抽象隐藏了起来,对应用不可见。 + +与之相对,我们目前还没有对内存管理功能进行有效的管理,仅仅是把程序放到某处的物理内存中。在内存访问方面,所有的应用都直接通过物理地址访问物理内存,这使得应用开发者需要了解繁琐的物理地址空间布局,访问内存也很不方便。在上一章中,出于任务切换的需要,所有的应用都在初始化阶段被加载到内存中并同时驻留下去直到它们全部运行结束。而且,所有的应用都直接通过物理地址访问物理内存。这会带来以下问题: + +- 首先,内核提供给应用的内存访问接口不够透明,也不好用。由于应用直接访问物理内存,这需要它在构建的时候就需要规划自己需要被加载到哪个地址运行。为了避免冲突可能还需要应用的开发者们对此进行协商,这显然是一件在今天看来不可理喻且极端麻烦的事情。 +- 其次,内核并没有对应用的访存行为进行任何保护措施,每个应用都有整块物理内存的读写权力。即使应用被限制在 U 特权级下运行,它还是能够造成很多麻烦:比如它可以读写其他应用的数据来窃取信息或者破坏它的正常运行;甚至它还可以修改内核的代码段来替换掉原本的 ``trap_handler`` 来挟持内核执行恶意代码。总之,这造成系统既不安全、也不稳定。 +- 再次,目前应用的内存使用空间在其运行前已经限定死了,内核不能灵活地给应用程序提供的运行时动态可用内存空间。比如一个应用结束后,这个应用所占的空间就被释放了,但这块空间无法动态地给其它还在运行的应用使用。 + +因此,为了防止应用胡作非为,本章将更好的管理物理内存,并提供给应用一个抽象出来的更加透明易用、也更加安全的访存接口,这就是基于分页机制的虚拟内存。站在应用程序运行的角度看,就是存在一个从“0”地址开始的非常大的可读/可写/可执行的地址空间(Address Space)。 + +实现地址空间的第一步就是实现分页机制,建立好虚拟内存和物理内存的页映射关系。此过程涉及硬件细节,不同的地址映射关系组合,相对比较复杂。总体而言,我们需要思考如下问题: + +- 硬件中物理内存的范围是什么? +- 哪些物理内存空间需要建立页映射关系? +- 如何建立页表使能分页机制? +- 如何确保OS能够在分页机制使能前后的不同时间段中都能正常寻址和执行代码? +- 页目录表(一级)的起始地址设置在哪里? +- 二级/三级等页表的起始地址设置在哪里,需要多大空间? +- 如何设置页目录表项的内容? +- 如何设置其它页表项的内容? +- 如果要让每个任务有自己的地址空间,那每个任务是否要有自己的页表? +- 代表应用程序的任务和操作系统需要有各自的页表吗? +- 在有了页表之后,任务和操作系统之间应该如何传递数据? + +如果能解决上述问题,我们就能设计实现具有超强防护能力的侏罗纪“头甲龙”操作系统。并可更好地理解地址空间,虚拟地址等操作系统的抽象概念与操作系统的虚存具体实现之间的联系。 + +.. + chyyuu:在哪里讲解虚存的设计与实现??? + + +实践体验 +----------------------- + +本章的应用和上一章相同,只不过由于内核提供给应用的访存接口被替换,应用的构建方式发生了变化,这方面在下面会深入介绍。 +因此应用运行起来的效果与上一章是一致的。 + +获取本章代码: + +.. code-block:: console + + $ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git + $ cd rCore-Tutorial-v3 + $ git checkout ch4 + +在 qemu 模拟器上运行本章代码: + +.. code-block:: console + + $ cd os + $ make run + +将 Maix 系列开发板连接到 PC,并在上面运行本章代码: + +.. code-block:: console + + $ cd os + $ make run BOARD=k210 + +如果顺利的话,我们将看到和上一章相同的运行结果(以 K210 平台为例): + +.. code-block:: + + [rustsbi] RustSBI version 0.1.1 + .______ __ __ _______.___________. _______..______ __ + | _ \ | | | | / | | / || _ \ | | + | |_) | | | | | | (----`---| |----`| (----`| |_) || | + | / | | | | \ \ | | \ \ | _ < | | + | |\ \----.| `--' |.----) | | | .----) | | |_) || | + | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| + + [rustsbi] Platform: K210 (Version 0.1.0) + [rustsbi] misa: RV64ACDFIMSU + [rustsbi] mideleg: 0x22 + [rustsbi] medeleg: 0x1ab + [rustsbi] Kernel entry: 0x80020000 + [kernel] Hello, world! + .text [0x80020000, 0x8002b000) + .rodata [0x8002b000, 0x8002e000) + .data [0x8002e000, 0x8004c000) + .bss [0x8004c000, 0x8035d000) + mapping .text section + mapping .rodata section + mapping .data section + mapping .bss section + mapping physical memory + [kernel] back to world! + remap_test passed! + init TASK_MANAGER + num_app = 4 + power_3 [10000/300000power_5 [10000/210000] + power_5 [20000/210000] + power_5 [30000/210000] + + ... + + (mod 998244353) + Test power_7 OK! + [kernel] Application exited with code 0 + power_3 [290000/300000] + power_3 [300000/300000] + 3^300000 = 612461288(mod 998244353) + Test power_3 OK! + [kernel] Application exited with code 0 + Test sleep OK! + [kernel] Application exited with code 0 + [kernel] Panicked at src/task/mod.rs:112 All applications completed! + [rustsbi] reset triggered! todo: shutdown all harts on k210; program halt. Type: 0, reason: 0 + +本章代码树 +----------------------------------------------------- + +.. code-block:: + :linenos: + :emphasize-lines: 52 + + ├── bootloader + │   ├── rustsbi-k210.bin + │   └── rustsbi-qemu.bin + ├── LICENSE + ├── os + │   ├── build.rs + │   ├── Cargo.lock + │   ├── Cargo.toml + │   ├── Makefile + │   └── src + │   ├── config.rs(修改:新增一些内存管理的相关配置) + │   ├── console.rs + │   ├── entry.asm + │   ├── lang_items.rs + │   ├── link_app.S + │   ├── linker-k210.ld(修改:将跳板页引入内存布局) + │   ├── linker-qemu.ld(修改:将跳板页引入内存布局) + │   ├── loader.rs(修改:仅保留获取应用数量和数据的功能) + │   ├── main.rs(修改) + │   ├── mm(新增:内存管理的 mm 子模块) + │   │   ├── address.rs(物理/虚拟 地址/页号的 Rust 抽象) + │   │   ├── frame_allocator.rs(物理页帧分配器) + │   │   ├── heap_allocator.rs(内核动态内存分配器) + │   │   ├── memory_set.rs(引入地址空间 MemorySet 及逻辑段 MemoryArea 等) + │   │   ├── mod.rs(定义了 mm 模块初始化方法 init) + │   │   └── page_table.rs(多级页表抽象 PageTable 以及其他内容) + │   ├── sbi.rs + │   ├── syscall + │   │   ├── fs.rs(修改:基于地址空间的 sys_write 实现) + │   │   ├── mod.rs + │   │   └── process.rs + │   ├── task + │   │   ├── context.rs(修改:构造一个跳转到不同位置的初始任务上下文) + │   │   ├── mod.rs(修改,详见文档) + │   │   ├── switch.rs + │   │   ├── switch.S + │   │   └── task.rs(修改,详见文档) + │   ├── timer.rs + │   └── trap + │   ├── context.rs(修改:在 Trap 上下文中加入了更多内容) + │   ├── mod.rs(修改:基于地址空间修改了 Trap 机制,详见文档) + │   └── trap.S(修改:基于地址空间修改了 Trap 上下文保存与恢复汇编代码) + ├── README.md + ├── rust-toolchain + ├── tools + │   ├── kflash.py + │   ├── LICENSE + │   ├── package.json + │   ├── README.rst + │   └── setup.py + └── user + ├── build.py(移除) + ├── Cargo.toml + ├── Makefile + └── src + ├── bin + │   ├── 00power_3.rs + │   ├── 01power_5.rs + │   ├── 02power_7.rs + │   └── 03sleep.rs + ├── console.rs + ├── lang_items.rs + ├── lib.rs + ├── linker.ld(修改:将所有应用放在各自地址空间中固定的位置) + └── syscall.rs + + + +实现流程概述 +----------------------------------------------------- + +本章涉及的代码量相对多了起来,也许同学们不知如何从哪里看起或从哪里开始尝试实验。这里简要介绍一下“头甲龙”操作系统的大致开发过程。 + +我们先从简单的地方入手,那当然就是先改进应用程序了。具体而言,主要就是把 ``linker.ld`` 中应用程序的其实地址都改为 ``0x0`` ,这是假定我们操作系统能够通过分页机制把不同应用的相同虚地址映射到不同的物理地址中。这样我们写应用就不用考虑物理地址布局的问题,写起来更简单了。 + +为了能够在内核中动态分配内存,我们的第二步需要在内核增加连续内存分配的功能,具体实现主要集中在 ``os/src/mm/heap_allocator.rs`` 中。完成这一步后,我们就可以在内核中用到Rust的堆数据结构了,如 ``Vec`` 、 ``Box`` 等,这样内核编程就更加灵活了。 + +操作系统如果要建立页表,首先要能管理整个系统的物理内存,这就需要知道哪些物理内存放了内核代码,哪些物理内存是空闲的等各种事情。所以需要了解整个系统的物理内存空间的范围,并用以页为单位的页帧来进行物理内存分配的管理,具体实现主要集中在 ``os/src/mm/frame_allocator.rs`` 中。 + + +页表中的页表项的索引其实是虚拟地址中的虚拟页号,页表项的重要内容是物理地址的物理页帧号。为了能够灵活地在虚拟地址、物理地址、虚拟页号、物理页号之间进行各种转换,在 ``os/src/mm/address.rs`` 中实现了各种转换函数。 + + +完成上述工作后,基本上就做好了建立页表的前期准备。我们就可以开始建立页表,这主要涉及到页表项的数据结构表示,以及多级页表的起始物理页帧位置和整个所占用的物理页帧的记录。具体实现主要集中在 ``os/src/mm/page_table.rs`` 中。 + +一旦使能分页机制,那么内核中也将基于虚地址进行虚存访问,所以在给应用添加虚拟地址空间前,内核自己也会建立一个页表,把整个物理地址空间通过简单的一一映射对应到一个虚拟地址空间中。后续的应用在执行前,也需要建立一个虚拟地址空间,这意味着第三章的 ``task`` 将进化到第五章的拥有独立页表的 ``进程`` 。虚拟地址空间需要有一个数据结构管理起来,这就是 ``MemorySet`` ,即地址空间这个抽象概念所对应的具象体现。在一个虚拟地址空间中,有代码段,数据段等不同属性且不一定连续的子空间,这也有一个重要的数据结构 ``MapArea`` 来表示和管理。围绕 ``MemorySet`` 等一系列的数据结构和相关操作的实现,主要集中在 ``os/src/mm/memory_set.rs`` 中。比如内核的页表和虚拟空间的建立在如下代码中: + +.. code-block:: rust + :linenos: + + // os/src/mm/memory_set.rs + + lazy_static! { + pub static ref KERNEL_SPACE: Arc> = Arc::new(Mutex::new( + MemorySet::new_kernel() + )); + } + +完成到这里,我们就可以使能分页机制了。且我们应该有更加方便的机制来给支持应用运行。在本章之前,都是把应用程序转换成 ``binary`` 格式来执行,这其实把编译器生成的 ``ELF`` 执行文件中大量有用的信息给去掉了,比如各种属性的代码段,数据段,程序的入口地址等。既然有了给应用运行提供虚拟地址空间的能力,我们就可以利用 ``ELF`` 执行文件中的各种信息来灵活构建应用运行所需要的虚拟地址空间。在 ``os/src/loader.rs`` 中可以看到如何获取一个应用的 ELF 执行文件,而在 ``os/src/mm/memory_set`` 中的 ``MemorySet::from_elf`` 可以看到如何通过解析 ELF 来创建一个应用地址空间。 + +对于有了虚拟地址空间的 ``任务`` ,我们可以把它叫做 ``进程`` 了。操作系统为此需要扩展任务控制块 ``TaskControlBlock`` 的管理范围,使得操作系统能管理拥有独立页表和虚拟地址空间的应用程序的运行。相关主要的改动集中在 ``os/src/task/task.rs`` 中。 + +由于代表应用程序运行的进程和管理应用的操作系统各自有独立的页表和虚拟地址空间,所以这就出现了两个比较挑战的事情。一个是由于系统调用、中断或异常导致的应用程序和操作系统之间的 ``Trap`` 上下文切换不像以前那么简单了,因为需要切换页表,这需要看看 ``os/src/trap/trap.S`` ;还有就是对内核态产生的中断和用户态产生的中断需要分别处理,这需要看看 ``os/src/trap/mod.rs`` 和 :ref:`跳板的实现 ` 中的讲解。 + +另外一个挑战是,操作系统需要跨不同的虚拟地址空间读写数据,在跨页访问时,一个不小心,就可能导致错误的数据读写。对这部分的处理,需要看看 ``os/src/syscall/fs.rs``、 ``os/src/mm/page_table.rs`` 中的 ``translated_byte_buffer`` 函数的实现。 + +实现到这,应该就可以给应用程序运行提供一个方便且安全的虚拟地址空间了。 \ No newline at end of file diff --git a/source/chapter4/1rust-dynamic-allocation.rst b/source/chapter4/1rust-dynamic-allocation.rst new file mode 100644 index 0000000000000000000000000000000000000000..039bdc9dd2654ee10ba37219ef6c03f81c955fd2 --- /dev/null +++ b/source/chapter4/1rust-dynamic-allocation.rst @@ -0,0 +1,309 @@ +Rust 中的动态内存分配 +======================================================== + + +本节导读 +-------------------------- + + +到目前为止,如果将我们的内核也看成一个应用,那么其中所有的变量都是被静态分配在内存中的,这样在对空闲内存的使用方面缺少灵活性。我们希望能在操作系统中提供动态申请和释放内存的能力,这样就可以加强操作系统对各种以内存为基础的资源分配与管理。 + +在应用程序的视角中,动态内存分配中的内存,其实就是操作系统管理的“堆 (Heap)”。但现在要实现操作系统,那么就需要操作系统自身能提供动态内存分配的能力。如果要实现动态内存分配的能力,需要操作系统需要有如下功能: + +- 初始时能提供一块大内存空间作为初始的“堆”。在没有分页机制情况下,这块空间是物理内存空间,否则就是虚拟内存空间。 +- 提供在堆上分配一块内存的函数接口。这样函数调用方就能够得到一块地址连续的空闲内存块进行读写。 +- 提供释放内存的函数接口。能够回收内存,以备后续的内存分配请求。 +- 提供空闲空间管理的连续内存分配算法。能够有效地管理空闲快,这样就能够动态地维护一系列空闲和已分配的内存块。 +- (可选)提供建立在堆上的数据结构和操作。有了上述基本的内存分配与释放函数接口,就可以实现类似动态数组,动态字典等空间灵活可变的堆数据结构,提高编程的灵活性。 + +考虑到我们是用Rust来编程的,为了在接下来的一些操作系统的实现功能中进一步释放 Rust 语言的强表达能力来减轻我们的编码负担,本节我们尝试在内核中支持动态内存分配以可以使用各种需要动态内存支持的Rust功能,如Vec、HashMap等。 + +静态与动态内存分配 +---------------------------------------------- + + +静态分配 +^^^^^^^^^^^^^^^^^^^^^^^^^ + + +若在某一时间点观察一个应用的地址空间,可以看到若干块连续内存,每一块都对应于一个生命周期尚未结束的变量。这个变量可能 +是一个局部变量,它来自于当前正在执行的函数或者当前函数调用栈上某个正在等待调用返回的函数的栈帧,也即它是被分配在 +栈上;这个变量也可能是一个全局变量,它被分配在数据段中。它们有一个共同点:在编译的时候编译器已经知道它们类型的字节大小, +于是给它们分配一块等大的内存将它们存储其中,这块内存在变量所属函数的栈帧/数据段中的位置也已经被固定了下来。 + +.. _term-static-allocation: + +这些变量是被 **静态分配** (Static Allocation) 的,这一过程来源于我们在程序中对变量的声明,在编译期由编译器完成。 +如果应用仅使用静态分配,它也许可以应付绝大部分的需求,但是某些情况则不够灵活。比如,需要将一个文件读到内存进行处理, +而且必须将文件一次性完整读进来处理才能正确。此时,可以选择声明一个栈上的局部变量或者数据段中的全局变量作为缓冲区来暂存 +文件的内容。但在编程的时候我们并不知道待处理的文件的大小,只能根据经验将缓冲区的大小设置为某一固定常数。在代码真正运行 +的时候,如果待处理的文件很小,那么缓冲区多出的部分是被浪费掉的,也拉高了应用的内存占用;如果待处理的文件很大,应用则 +无法正常运行。就像缓冲区的大小设置一样,还有很多其他的问题来源于某些数据结构需求的内存大小取决于应用的实际运行情况。 + + +动态分配 +^^^^^^^^^^^^^^^^^^^^^^^^^ + + +.. _term-dynamic-allocation: + +此时,使用 **动态分配** (Dynamic Allocation) 则可以解决这个问题。动态分配就是指应用不仅在自己的地址空间放置那些 +自编译期开始就大小固定、用于静态内存分配的逻辑段(如全局数据段、栈段),还另外放置一个大小可以随着应用的运行动态增减 +的逻辑段,它的名字叫做堆。同时,应用还要能够将这个段真正管理起来,即支持在运行的时候从里面分配一块空间来存放变量,而 +在变量的生命周期结束之后,这块空间需要被回收以待后面的使用。如果堆的大小固定,那么这其实就是一个连续内存分配问题, +我们课上所介绍到的那些算法都可以随意使用。取决于应用的实际运行状况,每次分配的空间大小可能会有不同,因此也会产生外碎片。 +如果在某次分配的时候发现堆空间不足,我们并不会像上一小节介绍的那样移动变量的存放位置让它们紧凑起来从而释放间隙用来分配 +(事实上它很难做到这一点), +一般情况下应用会直接通过系统调用(如类 Unix 内核提供的 ``sbrk`` 系统调用)来向内核请求增加它地址空间内堆的大小,之后 +就可以正常分配了。当然,这一类系统调用也能缩减堆的大小。 + +鉴于动态分配是一项非常基础的功能,很多高级语言的标准库中都实现了它。以 C 语言为例,C 标准库中提供了如下两个动态分配 +的接口函数: + +.. code-block:: c + + void* malloc (size_t size); + void free (void* ptr); + +其中,``malloc`` 的作用是从堆中分配一块大小为 ``size`` 字节的空间,并返回一个指向它的指针。而后续不用的时候,将这个 +指针传给 ``free`` 即可在堆中回收这块空间。我们通过返回的指针变量来间接访问堆上的空间,而无法直接进行 +访问。事实上,我们在程序中能够 *直接* 看到的变量都是被静态分配在栈或者全局数据段上的,它们大小在编译期已知,比如这里 +一个指针类型的大小就可以等于计算机可寻址空间的位宽。这样的它们却可以作为背后一块大小在编译期无法确定的空间的代表,这是一件非常有趣的 +事情。 + +除了可以灵活利用内存之外,动态分配还允许我们以尽可能小的代价灵活调整变量的生命周期。一个局部变量被静态分配在它所在函数 +的栈帧中,一旦函数返回,这个局部变量的生命周期也就结束了;而静态分配在数据段中的全局变量则是在应用的整个运行期间均存在。 +动态分配允许我们构造另一种并不一直存在也不绑定于函数调用的变量生命周期:以 C 语言为例,可以说自 ``malloc`` 拿到指向 +一个变量的指针到 ``free`` 将它回收之前的这段时间,这个变量在堆上存在。由于需要跨越函数调用,我们需要作为堆上数据代表 +的变量在函数间以参数或返回值的形式进行传递,而这些变量一般都很小(如一个指针),其拷贝开销可以忽略。 + +而动态内存分配的缺点在于:它背后运行着连续内存分配算法,相比静态分配会带来一些额外的开销。如果动态分配非常频繁,可能会产生很多无法使用的空闲空间碎片,甚至可能会成为应用的性能瓶颈。 + +.. _rust-heap-data-structures: + +Rust 中的堆数据结构 +------------------------------------------------ + +Rust 的标准库中提供了很多开箱即用的堆数据结构,利用它们能够大大提升我们的开发效率。 + +.. _term-smart-pointer: + +首先是一类 **智能指针** (Smart Pointer) 。智能指针和 Rust 中的其他两类指针也即裸指针 ``*const T/*mut T`` +以及引用 ``&T/&mut T`` 一样,都指向地址空间中的另一个区域并包含它的位置信息。但不同在于,它们携带的信息数量不等, +需要经过编译器不同等级的安全检查,可靠性和灵活程度也不同。 + +.. _term-borrow-check: + +- 裸指针 ``*const T/*mut T`` 基本等价于 C/C++ 里面的普通指针 ``T*`` ,它自身的内容仅仅是一个地址。它最为灵活, + 但是也最不安全。编译器只能对它进行最基本的可变性检查, :ref:`第一章 ` 曾经提到,对于裸指针 + 解引用访问它指向的那块数据是 unsafe 行为,需要被包裹在 unsafe 块中。 +- 引用 ``&T/&mut T`` 自身的内容也仅仅是一个地址,但是 Rust 编译器会在编译的时候进行比较严格的 **借用检查** + (Borrow Check) ,要求引用的生命周期必须在被借用的变量的生命周期之内,同时可变借用和不可变借用不能共存,一个 + 变量可以同时存在多个不可变借用,而可变借用同时最多只能存在一个。这能在编译期就解决掉很多内存不安全问题。 +- 智能指针不仅包含它指向的区域的地址,还含有一些额外的信息,因此这个类型的字节大小大于平台的位宽,属于一种胖指针。 + 从用途上看,它不仅可以作为一个媒介来访问它指向的数据,还能在这个过程中起到一些管理和控制的功能。 + +在 Rust 中,与动态内存分配相关的智能指针有如下这些: + +- ``Box`` 在创建时会在堆上分配一个类型为 ``T`` 的变量,它自身也只保存在堆上的那个变量的位置。而和裸指针或引用 + 不同的是,当 ``Box`` 被回收的时候,它指向的——也就是在堆上被动态分配的那个变量也会被回收。 +- ``Rc`` 是一个单线程上使用的引用计数类型, ``Arc`` 与其功能相同,只是它可以在多线程上使用。它提供了 + 多所有权,也即地址空间中同时可以存在指向同一个堆上变量的 ``Rc`` ,它们都可以拿到指向变量的不可变引用来 + 访问这同一个变量。而它同时也是一个引用计数,事实上在堆上的另一个位置维护了堆上这个变量目前被引用了多少次, + 也就是存在多少个 ``Rc`` 。这个计数会随着 ``Rc`` 的创建或复制而增加,并当 ``Rc`` 生命周期结束 + 被回收时减少。当这个计数变为零之后,这个计数变量本身以及被引用的变量都会从堆上被回收。 +- ``Mutex`` 是一个互斥锁,在多线程中使用,它可以保护里层被动态分配到堆上的变量同一时间只有一个线程能对它 + 进行操作,从而避免数据竞争,这是并发安全的问题,会在后面详细说明。同时,它能够提供 + :ref:`内部可变性 ` 。``Mutex`` 时常和 ``Arc`` 配套使用,因为它是用来 + 保护多个线程可能同时访问的数据,其前提就是多个线程都拿到指向同一块堆上数据的 ``Mutex`` 。于是,要么就是 + 这个 ``Mutex`` 作为全局变量被分配到数据段上,要么就是我们需要将 ``Mutex`` 包裹上一层多所有权变成 + ``Arc>`` ,让它可以在线程间进行传递。请记住 ``Arc>`` 这个经典组合,我们后面会经常用到。 + + 之前我们通过 ``RefCell`` 来获得内部可变性。可以将 ``Mutex`` 看成 ``RefCell`` 的多线程版本, + 因为 ``RefCell`` 是只能在单线程上使用的。而且 ``RefCell`` 并不会在堆上分配内存,它仅用到静态内存 + 分配。 + +这和 C++ 很像, ``Box`` 可以对标 C++ 的 ``std::unique_ptr`` ;而 ``Arc`` 则类似于 C++ 的 +``std::shared_ptr`` 。 + +.. _term-collection: +.. _term-container: + +随后,是一些 **集合** (Collection) 或称 **容器** (Container) 类型,它们负责管理一组数目可变的元素,这些元素 +的类型相同或是有着一些同样的特征。在 C++/Python/Java 等高级语言中我们已经对它们的使用方法非常熟悉了,对于 +Rust 而言,我们则可以直接使用以下容器: + +- 向量 ``Vec`` 类似于 C++ 中的 ``std::vector`` ; +- 键值对容器 ``BTreeMap`` 类似于 C++ 中的 ``std::map`` ; +- 有序集合 ``BTreeSet`` 类似于 C++ 中的 ``std::set`` ; +- 链表 ``LinkedList`` 类似于 C++ 中的 ``std::list`` ; +- 双端队列 ``VecDeque`` 类似于 C++ 中的 ``std::deque`` 。 +- 变长字符串 ``String`` 类似于 C++ 中的 ``std::string`` 。 + +下面是一张 Rust 智能指针/容器及其他类型的内存布局的经典图示,来自 +`这里 `_ 。 + +.. image:: rust-containers.png + +可以发现,在动态内存分配方面 Rust 和 C++ 很像,事实上 Rust 有意从 C++ 借鉴了这部分优秀特性。让我们先来看其他一些语言 +使用动态内存的方式: + +.. _term-reference-counting: +.. _term-garbage-collection: + +- C 语言仅支持 ``malloc/free`` 这一对操作,它们必须恰好成对使用,否则就会出现错误。比如分配了之后没有回收,则会导致 + 内存溢出;回收之后再次 free 相同的指针,则会造成 Double-Free 问题;又如回收之后再尝试通过指针访问它指向的区域,这 + 属于 Use-After-Free 问题。总之,这样的内存安全问题层出不穷,毕竟人总是会犯错的。 +- Python/Java 通过 **引用计数** (Reference Counting) 对所有的对象进行运行时的动态管理,一套 **垃圾回收** + (GC, Garbage Collection) 机制会被自动定期触发,每次都会检查所有的对象,如果其引用计数为零则可以将该对象占用的内存 + 从堆上回收以待后续其他的对象使用。这样做完全杜绝了内存安全问题,但是性能开销则很大,而且 GC 触发的时机和每次 GC 的 + 耗时都是无法预测的,还使得性能不够稳定。 + +.. _term-raii: + +C++ 的 **资源获取即初始化** (RAII, Resource Acquisition Is Initialization) 风格则致力于解决上述问题。 +RAII 的含义是说,将一个使用前必须获取的资源的生命周期绑定到一个变量上。以 ``Box`` 为例,在它被 +创建的时候,会在堆上分配一块空间保存它指向的数据;而在 ``Box`` 生命周期结束被回收的时候,堆上的那块空间也会 +立即被一并回收。这也就是说,我们无需手动回收资源,它会和绑定到的变量同步由编译器自动回收,我们既不用担心忘记回收更不 +可能回收多次;同时,由于我们很清楚一个变量的生命周期,则该资源何时被回收也是完全可预测的,我们也明确知道这次回收 +操作的开销。在 Rust 中,不限于堆内存,将某种资源的生命周期与一个变量绑定的这种 RAII 的思想无处不见,甚至这种资源 +可能只是另外一种类型的变量。 + + +在内核中支持动态内存分配 +-------------------------------------------------------- + +如果要在操作系统内核中支持动态内存分配,则需要实现在本节开始介绍的一系列功能:初始化堆、分配/释放内存块的函数接口、连续内存分配算法。相对于C语言而言,如果用Rust语言实现,它在 ``alloc`` crate中设定了一套简洁规范的接口,只要实现了这套接口,内核就可以很方便地支持动态内存分配了。 + +上边介绍的那些与堆相关的智能指针或容器都可以在 Rust 自带的 ``alloc`` crate 中找到。当我们使用 Rust 标准库 +``std`` 的时候可以不用关心这个 crate ,因为标准库内已经已经实现了一套堆管理算法,并将 ``alloc`` 的内容包含在 +``std`` 名字空间之下让开发者可以直接使用。然而我们的内核是在禁用了标准库(即 ``no_std`` )的裸机平台,核心库 +``core`` 也并没有动态内存分配的功能,这个时候就要考虑利用 ``alloc`` 库了。 + +``alloc`` 库需要我们提供给它一个 ``全局的动态内存分配器`` ,它会利用该分配器来管理堆空间,从而使得它提供的堆数据结构可以正常 +工作。具体而言,我们的动态内存分配器需要实现它提供的 ``GlobalAlloc`` Trait,这个 Trait 有两个必须实现的抽象接口: + +.. code-block:: rust + + // alloc::alloc::GlobalAlloc + + pub unsafe fn alloc(&self, layout: Layout) -> *mut u8; + pub unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout); + +可以看到,它们类似 C 语言中的 ``malloc/free`` ,分别代表堆空间的分配和回收,也同样使用一个裸指针(也就是地址) +作为分配的返回值和回收的参数。两个接口中都有一个 ``alloc::alloc::Layout`` 类型的参数, +它指出了分配的需求,分为两部分,分别是所需空间的大小 ``size`` ,以及返回地址的对齐要求 ``align`` 。这个对齐要求 +必须是一个 2 的幂次,单位为字节数,限制返回的地址必须是 ``align`` 的倍数。 + +.. note:: + + **为何 C 语言 malloc 的时候不需要提供对齐需求?** + + 在 C 语言中,所有对齐要求的最大值是一个平台有关的很小的常数(比如8 bytes),消耗少量内存即可使得每一次分配都符合这个最大 + 的对齐要求。因此也就不需要区分不同分配的对齐要求了。而在 Rust 中,某些分配的对齐要求可能很大,就只能采用更 + 加复杂的方法。 + +之后,只需将我们的动态内存分配器类型实例化为一个全局变量,并使用 ``#[global_allocator]`` 语义项标记即可。由于该 +分配器的实现比较复杂,我们这里直接使用一个已有的伙伴分配器实现。首先添加 crate 依赖: + +.. code-block:: toml + + # os/Cargo.toml + + buddy_system_allocator = "0.6" + +接着,需要引入 ``alloc`` 库的依赖,由于它算是 Rust 内置的 crate ,我们并不是在 ``Cargo.toml`` 中进行引入,而是在 +``main.rs`` 中声明即可: + +.. code-block:: rust + + // os/src/main.rs + + extern crate alloc; + +然后,根据 ``alloc`` 留好的接口提供全局动态内存分配器: + +.. code-block:: rust + :linenos: + + // os/src/mm/heap_allocator.rs + + use buddy_system_allocator::LockedHeap; + use crate::config::KERNEL_HEAP_SIZE; + + #[global_allocator] + static HEAP_ALLOCATOR: LockedHeap = LockedHeap::empty(); + + static mut HEAP_SPACE: [u8; KERNEL_HEAP_SIZE] = [0; KERNEL_HEAP_SIZE]; + + pub fn init_heap() { + unsafe { + HEAP_ALLOCATOR + .lock() + .init(HEAP_SPACE.as_ptr() as usize, KERNEL_HEAP_SIZE); + } + } + +- 第 7 行,我们直接将 ``buddy_system_allocator`` 中提供的 ``LockedHeap`` 实例化成一个全局变量,并使用 + ``alloc`` 要求的 ``#[global_allocator]`` 语义项进行标记。注意 ``LockedHeap`` 已经实现了 ``GlobalAlloc`` + 要求的抽象接口了。 +- 第 11 行,在使用任何 ``alloc`` 中提供的堆数据结构之前,我们需要先调用 ``init_heap`` 函数来给我们的全局分配器 + 一块内存用于分配。在第 9 行可以看到,这块内存是一个 ``static mut`` 且被零初始化的字节数组,位于内核的 + ``.bss`` 段中。 ``LockedHeap`` 也是一个被互斥锁保护的类型,在对它任何进行任何操作之前都要先获取锁以避免其他 + 线程同时对它进行操作导致数据竞争。然后,调用 ``init`` 方法告知它能够用来分配的空间的起始地址和大小即可。 + +我们还需要处理动态内存分配失败的情形,在这种情况下我们直接 panic : + +.. code-block:: rust + + // os/src/main.rs + + #![feature(alloc_error_handler)] + + // os/src/mm/heap_allocator.rs + + #[alloc_error_handler] + pub fn handle_alloc_error(layout: core::alloc::Layout) -> ! { + panic!("Heap allocation error, layout = {:?}", layout); + } + +最后,让我们尝试一下动态内存分配吧! + +.. chyyuu 如何尝试??? + +.. code-block:: rust + :linenos: + + // os/src/mm/heap_allocator.rs + + #[allow(unused)] + pub fn heap_test() { + use alloc::boxed::Box; + use alloc::vec::Vec; + extern "C" { + fn sbss(); + fn ebss(); + } + let bss_range = sbss as usize..ebss as usize; + let a = Box::new(5); + assert_eq!(*a, 5); + assert!(bss_range.contains(&(a.as_ref() as *const _ as usize))); + drop(a); + let mut v: Vec = Vec::new(); + for i in 0..500 { + v.push(i); + } + for i in 0..500 { + assert_eq!(v[i], i); + } + assert!(bss_range.contains(&(v.as_ptr() as usize))); + drop(v); + println!("heap_test passed!"); + } + +其中分别使用智能指针 ``Box`` 和向量 ``Vec`` 在堆上分配数据并管理它们,通过 ``as_ref`` 和 ``as_ptr`` +方法可以分别看到它们指向的数据的位置,能够确认它们的确在 ``.bss`` 段的堆上。 + +.. note:: + + 本节部分内容参考自 `BlogOS 的相关章节 `_ 。 \ No newline at end of file diff --git a/source/chapter4/2address-space.rst b/source/chapter4/2address-space.rst new file mode 100644 index 0000000000000000000000000000000000000000..01f7fd148d8990b004d423fcf3f71801d1be0ea3 --- /dev/null +++ b/source/chapter4/2address-space.rst @@ -0,0 +1,214 @@ +地址空间 +===================================== + + +本节导读 +-------------------------- + + +直到现在,我们的操作系统给应用看到的是一个非常原始的物理内存空间,可以简单地理解为一个可以随便访问的大数组。为了限制应用访问内存空间的范围并给操作系统提供内存管理的灵活性,计算机硬件引入了各种内存保护/映射硬件机制,如RISC-V的基址-边界翻译和保护机制、x86的分段机制、RISC-V/x86/ARM都有的分页机制。它们的共同之处在于CPU访问的数据和指令内存地址是虚地址,需要进行转换形成合法的物理地址或产生非法的异常。为了用好这种硬件机制,操作系统需要升级自己的能力。 + +操作系统为了更好地管理这两种形式的内存,并给应用程序提供统一的访问接口,即应用程序不需要了解虚拟内存和物理内存的区别的,操作系统提出了 ``地址空间 Address Space`` 抽象,并在内核中建立虚实地址空间的映射机制,给应用程序提供一个虚拟的内存环境。 + +本节将结合操作系统的发展历程回顾来介绍 ``地址空间 Address Space`` 抽象的实现策略 +是如何变化的。 + +虚拟地址与地址空间 +------------------------------- + +地址虚拟化出现之前 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +我们之前介绍过,在最早整套硬件资源只用来执行单个裸机应用的时候,并不存在真正意义上的操作系统,而只能算是一种应用 +函数库。那个时候,物理内存的一部分用来保存函数库的代码和数据,余下的部分都交给应用来使用。从功能上可以将应用 +占据的内存分成几个段:代码段、全局数据段、堆和栈等。当然,由于就只有这一个应用,它想如何调整布局都是它自己的 +事情。从内存使用的角度来看,批处理系统和裸机应用很相似:批处理系统的每个应用也都是独占内核之外的全部内存空间, +只不过当一个应用出错或退出之后,它所占据的内存区域会被清空,而序列中的下一个应用将自己的代码和数据放置进来。 +这个时期,内核提供给应用的访存视角是一致的,因为它们确实会在运行过程中始终独占一块固定的内存区域,每个应用开发者 +都基于这一认知来规划程序的内存布局。 + +后来,为了降低等待 I/O 带来的无意义的 CPU 资源损耗,多道程序出现了。而为了提升用户的交互式体验,提高生产力,分时 +多任务系统诞生了。它们的特点在于:应用开始多出了一种“暂停”状态,这可能来源于它主动 yield 交出 CPU 资源,或是在 +执行了足够长时间之后被内核强制性换出。当应用处于暂停状态的时候,它驻留在内存中的代码、数据该何去何从呢?曾经有一种 +做法是每个应用仍然和在批处理系统中一样独占内核之外的整块内存,当暂停的时候,内核负责将它的代码、数据保存在磁盘或 +硬盘中,然后把即将换入的应用保存在磁盘上的代码、数据恢复到内存,这些都做完之后才能开始执行新的应用。 + +不过,由于这种做法需要大量读写内存和外部存储设备,而它们的速度都比 CPU 慢上几个数量级,这导致任务切换的开销过大, +甚至完全不能接受。既然如此,就只能像我们在第三章中的做法一样,限制每个应用的最大可用内存空间小于物理内存的容量,这样 +就可以同时把多个应用的数据驻留在内存中。在任务切换的时候只需完成任务上下文保存与恢复即可,这只是在内存的帮助下保存、 +恢复少量通用寄存器,甚至无需访问外存,这从很大程度上降低了任务切换的开销。 + +在本章的引言中介绍过第三章中操作系统的做法对应用程序开发带了一定的困难。从应用开发的角度看,需要应用程序决定自己会被加载到哪个物理地址运行,需要直接访问真实的 +物理内存。这就要求应用开发者对于硬件的特性和使用方法有更多了解,产生额外的学习成本,也会为应用的开发和调试带来不便。从 +内核的角度来看,将直接访问物理内存的权力下放到应用会使得它难以对应用程序的访存行为进行有效管理,已有的特权级机制亦无法 +阻止很多来自应用程序的恶意行为。 + +加一层抽象加强内存管理 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +为了解决这种困境,抽象仍然是最重要的指导思想。在这里,抽象意味着内核要负责将物理内存管理起来,并为上面的应用提供 +一层抽象接口,从之前的失败经验学习,这层抽象需要达成下面的设计目标: + +- *透明* :应用开发者可以不必了解底层真实物理内存的硬件细节,且在非必要时也不必关心内核的实现策略, + 最小化他们的心智负担; +- *高效* :这层抽象至少在大多数情况下不应带来过大的额外开销; +- *安全* :这层抽象应该有效检测并阻止应用读写其他应用或内核的代码、数据等一系列恶意行为。 + +.. _term-address-space: +.. _term-virtual-address: + +最终,到目前为止仍被操作系统内核广泛使用的抽象被称为 **地址空间** (Address Space) 。某种程度上讲,可以将它看成一块 +巨大但并不一定真实存在的内存。在每个应用程序的视角里,操作系统分配给应用程序一个范围有限(但其实很大),独占的连续地址空间(其中有些地方被操作系统限制不能访问,如内核本身占用的虚地址空间等),因此应用程序可以在划分给它的地址空间中随意规划内存布局,它的 +各个段也就可以分别放置在地址空间中它希望的位置(当然是操作系统允许应用访问的地址)。应用同样可以使用一个地址作为索引来读写自己地址空间的数据,就像用物理地址 +作为索引来读写物理内存上的数据一样。这种地址被称为 **虚拟地址** (Virtual Address) 。当然,操作系统要达到 **地址空间** 抽象的设计目标,需要有计算机硬件的支持,这就是计算机组成原理课上讲到的 ``MMU`` 和 ``TLB`` 等硬件机制。 + +从此,应用能够直接看到并访问的内存就只有操作系统提供的地址空间,且它的任何一次访存使用的地址都是虚拟地址,无论取指令来执行还是读写 +栈、堆或是全局数据段都是如此。事实上,特权级机制被拓展,使得应用不再具有通过物理地址直接访问物理内存的能力。应用所处的执行环境在安全方面被进一步强化,形成了用户态特权级和地址空间的二维安全措施。 + +由于每个应用独占一个地址空间,里面只含有自己的各个段,于是它可以随意规划 +各个段的分布而无需考虑和其他应用冲突;同时,它完全无法窃取或者破坏其他应用的数据,毕竟那些段在其他应用的地址空间 +内,鉴于应用只能通过虚拟地址读写它自己的地址空间,这是它没有能力去访问的。这是 **地址空间** 抽象对应用程序执行的安全性和稳定性的一种保障。 + +.. image:: address-translation.png + +.. _term-mmu: +.. _term-address-translation: + + +我们知道应用的数据终归还是存在物理内存中的,那么虚拟地址如何形成地址空间,虚拟地址空间如何转换为物理内存呢?操作系统可以设计巧妙的数据结构来表示地址空间。但如果完全由操作系统来完成转换每次处理器地址访问所需的虚实地址转换,那开销就太大了。这就需要扩展硬件功能来加速地址转换过程(回忆 *计算机组成原理* 课上讲的 ``MMU`` 和 ``TLB`` )。 + + +增加硬件加速虚实地址转换 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +开始回顾一下 **计算机组成原理** 课。如上图所示,当应用取指或者执行 +一条访存指令的时候,它都是在以虚拟地址为索引读写自己的地址空间。此时,CPU 中的 **内存管理单元** +(MMU, Memory Management Unit) 自动将这个虚拟地址进行 **地址转换** (Address Translation) 变为一个物理地址, +也就是物理内存上这个应用的数据真实被存放的位置。也就是说,在 MMU 的帮助下,应用对自己地址空间的读写才能被实际转化为 +对于物理内存的访问。 + +事实上,每个应用的地址空间都可以看成一个从虚拟地址到物理地址的映射。可以想象对于不同的应用来说,该映射可能是不同的, +即 MMU 可能会将来自不同两个应用地址空间的相同虚拟地址翻译成不同的物理地址。要做到这一点,就需要硬件提供一些寄存器 +,软件可以对它进行设置来控制 MMU 按照哪个应用的地址空间进行地址转换。于是,将应用的数据放到物理内存并进行管理,而 +在任务切换的时候需要将控制 MMU 选用哪个应用的地址空间进行映射的那些寄存器也一并进行切换,则是作为软件部分的内核需 +要完成的工作。 + +回过头来,在介绍内核对于 CPU 资源的抽象——时分复用的时候,我们曾经提到它为应用制造了一种每个应用独占整个 CPU 的 +幻象,而隐藏了多个应用分时共享 CPU 的实质。而地址空间也是如此,应用只需、也只能看到它独占整个地址空间的幻象,而 +藏在背后的实质仍然是多个应用共享物理内存,它们的数据分别存放在内存的不同位置。 + +地址空间只是一层抽象接口,它有很多种具体的实现策略。对于不同的实现策略来说,操作系统内核如何规划应用数据放在物理内存的位置, +而 MMU 又如何进行地址转换也都是不同的。下面我们简要介绍几种曾经被使用的策略,并探讨它们的优劣。 + +分段内存管理 +------------------------------------- + +.. image:: simple-base-bound.png + +.. _term-slot: + +曾经的一种做法如上图所示:每个应用的地址空间大小限制为一个固定的常数 ``bound`` ,也即每个应用的可用虚拟地址区间 +均为 :math:`[0,\text{bound})` 。随后,就可以以这个大小为单位,将物理内存除了内核预留空间之外的部分划分为若干 +个大小相同的 **插槽** (Slot) ,每个应用的所有数据都被内核放置在其中一个插槽中,对应于物理内存上的一段连续物理地址 +区间,假设其起始物理地址为 :math:`\text{base}` ,则由于二者大小相同,这个区间实际为 +:math:`[\text{base},\text{base}+\text{bound})` 。因此地址转换很容易完成,只需检查一下虚拟地址不超过地址空间 +的大小限制(此时需要借助特权级机制通过异常来进行处理),然后做一个线性映射,将虚拟地址加上 :math:`\text{base}` +就得到了数据实际所在的物理地址。 + +.. _term-bitmap: + +可以看出,这种实现极其简单:MMU 只需要 :math:`\text{base,bound}` 两个寄存器,在地址转换进行比较或加法运算即可; +而内核只需要在任务切换的同时切换 :math:`\text{base}` 寄存器(由于 :math:`\text{bound}` 是一个常数),内存 +管理方面它只需考虑一组插槽的占用状态,可以用一个 **位图** (Bitmap) 来表示,随着应用的新增和退出对应置位或清空。 + +.. _term-internal-fragment: + +然而,它的问题在于:浪费的内存资源过多。注意到应用地址空间预留了一部分,它是用来让栈得以向低地址增长,同时允许堆 +往高地址增长(支持应用运行时进行动态内存分配)。每个应用的情况都不同,内核只能按照在它能力范围之内的消耗内存最多 +的应用的情况来统一指定地址空间的大小,而其他内存需求较低的应用根本无法充分利用内核给他们分配的这部分空间。 +但这部分空间又是一个完整的插槽的一部分,也不能再交给其他应用使用。这种在地址空间内部无法被充分利用的空间被称为 +**内碎片** (Internal Fragment) ,它限制了系统同时共存的应用数目。如果应用的需求足够多样化,那么内核无论如何设置 +应用地址空间的大小限制也不能得到满意的结果。这就是固定参数的弊端:虽然实现简单,但不够灵活。 + +为了解决这个问题,一种分段管理的策略开始被使用,如下图所示: + +.. image:: segmentation.png + +注意到内核开始以更细的粒度,也就是应用地址空间中的一个逻辑段作为单位来安排应用的数据在物理内存中的布局。对于每个 +段来说,从它在某个应用地址空间中的虚拟地址到它被实际存放在内存中的物理地址中间都要经过一个不同的线性映射,于是 +MMU 需要用一对不同的 :math:`\text{base/bound}` 进行区分。这里由于每个段的大小都是不同的,我们也不再能仅仅 +使用一个 :math:`\text{bound}` 进行简化。当任务切换的时候,这些对寄存器也需要被切换。 + +简单起见,我们这里忽略一些不必要的细节。比如应用在以虚拟地址为索引访问地址空间的时候,它如何知道该地址属于哪个段, +从而硬件可以使用正确的一对 :math:`\text{base/bound}` 寄存器进行合法性检查和完成实际的地址转换。这里只关注 +分段管理是否解决了内碎片带来的内存浪费问题。注意到每个段都只会在内存中占据一块与它实际所用到的大小相等的空间。堆 +的情况可能比较特殊,它的大小可能会在运行时增长,但是那需要应用通过系统调用向内核请求。也就是说这是一种按需分配,而 +不再是内核在开始时就给每个应用分配一大块很可能用不完的内存。由此,不再有内碎片了。 + +.. _term-external-fragment: + +尽管内碎片被消除了,但内存浪费问题并没有完全解决。这是因为每个段的大小都是不同的(它们可能来自不同的应用,功能 +也不同),内核就需要使用更加通用、也更加复杂的连续内存分配算法来进行内存管理,而不能像之前的插槽那样以一个比特 +为单位。顾名思义,连续内存分配算法就是每次需要分配一块连续内存来存放一个段的数据。 +随着一段时间的分配和回收,物理内存还剩下一些相互不连续的较小的可用连续块,其中有一些只是两个已分配内存块之间的很小的间隙,它们自己可能由于空间较小,已经无法被 +用于分配,被称为 **外碎片** (External Fragment) 。 + +如果这时再想分配一个比较大的块, +就需要将这些不连续的外碎片“拼起来”,形成一个大的连续块。然而这是一件开销很大的事情,涉及到极大的内存读写开销。具体而言,这需要移动和调整一些已分配内存块在物理内存上的位置,才能让那些小的外碎片能够合在一起,形成一个大的空闲块。如果连续内存分配算法 +选取得当,可以尽可能减少这种操作。课上所讲到的那些算法,包括 first-fit/worst-fit/best-fit 或是 buddy +system,其具体表现取决于实际的应用需求,各有优劣。 + +那么,分段内存管理带来的外碎片和连续内存分配算法比较复杂的 +问题可否被解决呢? + +分页内存管理 +-------------------------------------- + +仔细分析一下可以发现,段的大小不一是外碎片产生的根本原因。之前我们把应用的整个地址空间连续放置在物理内存中,在 +每个应用的地址空间大小均相同的情况下,只需利用类似位图的数据结构维护一组插槽的占用状态,从逻辑上分配和回收都是 +以一个固定的比特为单位,自然也就不会存在外碎片了。但是这样粒度过大,不够灵活,又在地址空间内部产生了内碎片。 + +若要结合二者的优点的话,就需要内核始终以一个同样大小的单位来在物理内存上放置应用地址空间中的数据,这样内核就可以 +使用简单的插槽式内存管理,使得内存分配算法比较简单且不会产生外碎片;同时,这个单位的大小要足够小,从而其内部没有 +被用到的内碎片的大小也足够小,尽可能提高内存利用率。这便是我们将要介绍的分页内存管理。 + +.. image:: page-table.png + +.. _term-page: +.. _term-frame: + +如上图所示,内核以页为单位进行物理内存管理。每个应用的地址空间可以被分成若干个(虚拟) **页面** (Page) ,而 +可用的物理内存也同样可以被分成若干个(物理) **页帧** (Frame) ,虚拟页面和物理页帧的大小相同。每个虚拟页面 +中的数据实际上都存储在某个物理页帧上。相比分段内存管理,分页内存管理的粒度更小,应用地址空间中的每个逻辑段都 +由多个虚拟页面组成,而每个虚拟页面在地址转换的过程中都使用一个不同的线性映射,而不是在分段内存管理中每个逻辑段 +都使用一个相同的线性映射。 + +.. _term-virtual-page-number: +.. _term-physical-page-number: +.. _term-page-table: + +为了方便实现虚拟页面到物理页帧的地址转换,我们给每个虚拟页面和物理页帧一个编号,分别称为 **虚拟页号** +(VPN, Virtual Page Number) 和 **物理页号** (PPN, Physical Page Number) 。每个应用都有一个不同的 +**页表** (Page Table) ,里面记录了该应用地址空间中的每个虚拟页面映射到物理内存中的哪个物理页帧,即数据实际 +被内核放在哪里。我们可以用页号来代表二者,因此如果将页表看成一个键值对,其键的类型为虚拟页号,值的类型则为物理 +页号。当 MMU 进行地址转换的时候,它首先找到给定的虚拟地址所在的虚拟页面的页号,然后查当前应用的页表根据虚拟页号 +找到物理页号,最后按照虚拟地址在它所在的虚拟页面中的相对位置相应给物理页号对应的物理页帧的起始地址加上一个偏移量, +这就得到了实际访问的物理地址。 + +在页表中通过虚拟页号不仅能查到物理页号,还能得到一组保护位,它限制了应用对转换得到的物理地址对应的内存的使用方式。 +最典型的如 ``rwx`` , ``r`` 表示当前应用可以读该内存; ``w`` 表示当前应用可以写该内存; ``x`` 则表示当前应用 +可以从该内存取指令用来执行。一旦违反了这种限制则会触发异常被内核捕获到。通过适当的设置,可以检查一些应用明显的 +错误:比如应用修改自己本应该只读的代码段,或者从数据段取指令来执行。 + +当一个应用的地址空间比较大的时候,页表里面的项数会很多(事实上每个虚拟页面都应该对应页表中的一项,上图中我们已经 +省略掉了那些未被使用的虚拟页面),导致它的容量极速膨胀,已经不再是像之前那样数个寄存器便可存下来的了,CPU 内也没有 +足够的硬件资源能够将它存下来。因此它只能作为一种被内核管理的数据结构放在内存中,但是 CPU 也会直接访问它来查页表, +这也就需要内核和硬件之间关于页表的内存布局达成一致。 + +由于分页内存管理既简单又灵活,它逐渐成为了主流,RISC-V 架构也使用了这种策略。后面我们会基于这种机制,自己来动手从物理内存抽象出应用的地址空间来。 + +.. note:: + + 本节部分内容参考自 `Operating Systems: Three Easy Pieces `_ + 教材的 13~16 小节。 + diff --git a/source/chapter4/3sv39-implementation-1.rst b/source/chapter4/3sv39-implementation-1.rst new file mode 100644 index 0000000000000000000000000000000000000000..9062168382dc34502b323d42bd6e0cbe5a45032d --- /dev/null +++ b/source/chapter4/3sv39-implementation-1.rst @@ -0,0 +1,455 @@ +实现 SV39 多级页表机制(上) +======================================================== + + +本节导读 +-------------------------- + + +在上一小节中我们已经简单介绍了分页的内存管理策略,现在我们尝试在 RV64 架构提供的 SV39 分页机制的基础上完成内核中的软件对应实现。由于内容过多,我们将分成两个小节进行讲解。本节主要讲解在RV64架构下的虚拟地址与物理地址的访问属性(可读,可写,可执行等),组成结构(页号,帧号,偏移量等),访问的空间范围等;以及如何用Rust语言来设计有类型的页表项。 + + +虚拟地址和物理地址 +------------------------------------------------------ + +内存控制相关的CSR寄存器 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +默认情况下 MMU 未被使能,此时无论 CPU 位于哪个特权级,访存的地址都会作为一个物理地址交给对应的内存控制单元来直接 +访问物理内存。我们可以通过修改 S 特权级的一个名为 ``satp`` 的 CSR 来启用分页模式,在这之后 S 和 U 特权级的访存 +地址会被视为一个虚拟地址,它需要经过 MMU 的地址转换变为一个物理地址,再通过它来访问物理内存;而 M 特权级的访存地址,我们可设定是内存的物理地址。 + + +.. note:: + + M 特权级的访存地址被视为一个物理地址还是一个需要经历和 S/U 特权级相同的地址转换的虚拟地址取决于硬件配置,在这里我们不会进一步探讨。 + +.. chyyuu M模式下,应该访问的是物理地址??? + +.. image:: satp.png + :name: satp-layout + +上图是 RV64 架构下 ``satp`` 的字段分布。当 ``MODE`` 设置为 0 的时候,代表所有访存都被视为物理地址;而设置为 8 +的时候,SV39 分页机制被启用,所有 S/U 特权级的访存被视为一个 39 位的虚拟地址,它们需要先经过 MMU 的地址转换流程, +如果顺利的话,则会变成一个 56 位的物理地址来访问物理内存;否则则会触发异常,这体现了该机制的内存保护能力。 + +虚拟地址和物理地址都是字节地址,39 位的虚拟地址可以用来访问理论上最大 :math:`512\text{GiB}` 的地址空间, +而 56 位的物理地址在理论上甚至可以访问一块大小比这个地址空间的还高出几个数量级的物理内存。但是实际上无论是 +虚拟地址还是物理地址,真正有意义、能够通过 MMU 的地址转换或是 CPU 内存控制单元的检查的地址仅占其中的很小 +一部分,因此它们的理论容量上限在目前都没有实际意义。 + + +地址格式与组成 +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: sv39-va-pa.png + +.. _term-page-offset: + +我们采用分页管理,单个页面的大小设置为 :math:`4\text{KiB}` ,每个虚拟页面和物理页帧都对齐到这个页面大小,也就是说 +虚拟/物理地址区间 :math:`[0,4\text{KiB})` 为第 :math:`0` 个虚拟页面/物理页帧,而 +:math:`[4\text{KiB},8\text{KiB})` 为第 :math:`1` 个,以此类推。 :math:`4\text{KiB}` 需要用 12 位字节地址 +来表示,因此虚拟地址和物理地址都被分成两部分:它们的低 12 位,即 :math:`[11:0]` 被称为 **页内偏移** +(Page Offset) ,它描述一个地址指向的字节在它所在页面中的相对位置。而虚拟地址的高 27 位,即 :math:`[38:12]` 为 +它的虚拟页号 VPN,同理物理地址的高 44 位,即 :math:`[55:12]` 为它的物理页号 PPN,页号可以用来定位一个虚拟/物理地址 +属于哪一个虚拟页面/物理页帧。 + +地址转换是以页为单位进行的,在地址转换的前后地址的页内偏移部分不变。可以认为 MMU 只是从虚拟地址中取出 27 位虚拟页号, +在页表中查到其对应的物理页号(如果存在的话),最后将得到的44位的物理页号与虚拟地址的12位页内偏移依序拼接到一起就变成了56位的物理地址。 + +.. _high-and-low-256gib: + +.. note:: + + **RV64 架构中虚拟地址为何只有 39 位?** + + 在 64 位架构上虚拟地址长度确实应该和位宽一致为 64 位,但是在启用 SV39 分页模式下,只有低 39 位是真正有意义的。 + SV39 分页模式规定 64 位虚拟地址的 :math:`[63:39]` 这 25 位必须和第 38 位相同,否则 MMU 会直接认定它是一个 + 不合法的虚拟地址。通过这个检查之后 MMU 再取出低 39 位尝试将其转化为一个 56 位的物理地址。 + + 也就是说,所有 :math:`2^{64}` 个虚拟地址中,只有最低的 :math:`256\text{GiB}` (当第 38 位为 0 时) + 以及最高的 :math:`256\text{GiB}` (当第 38 位为 1 时)是可能通过 MMU 检查的。当我们写软件代码的时候,一个 + 地址的位宽毋庸置疑就是 64 位,我们要清楚可用的只有最高和最低这两部分,尽管它们已经巨大的超乎想象了;而本节中 + 我们专注于介绍 MMU 的机制,强调 MMU 看到的真正用来地址转换的虚拟地址只有 39 位。 + + + +地址相关的数据结构抽象与类型定义 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +正如本章第一小节所说,在分页内存管理中,地址转换的核心任务在于如何维护虚拟页号到物理页号的映射——也就是页表。不过在具体 +实现它之前,我们先将地址和页号的概念抽象为 Rust 中的类型,借助 Rust 的类型安全特性来确保它们被正确实现。 + +首先是这些类型的定义: + +.. code-block:: rust + + // os/src/mm/address.rs + + #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] + pub struct PhysAddr(pub usize); + + #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] + pub struct VirtAddr(pub usize); + + #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] + pub struct PhysPageNum(pub usize); + + #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] + pub struct VirtPageNum(pub usize); + +.. _term-type-convertion: + +上面分别给出了物理地址、虚拟地址、物理页号、虚拟页号的 Rust 类型声明,它们都是 Rust 的元组式结构体,可以看成 +usize 的一种简单包装。我们刻意将它们各自抽象出来而不是都使用 usize 保存,就是为了在 Rust 编译器的帮助下进行 +多种方便且安全的 **类型转换** (Type Convertion) 。 + +首先,这些类型本身可以和 usize 之间互相转换,以物理地址 ``PhysAddr`` 为例,我们需要: + +.. code-block:: rust + + // os/src/mm/address.rs + + impl From for PhysAddr { + fn from(v: usize) -> Self { Self(v) } + } + + impl From for usize { + fn from(v: PhysAddr) -> Self { v.0 } + } + +前者允许我们从一个 ``usize`` 来生成 ``PhysAddr`` ,即 ``PhysAddr::from(_: usize)`` 将得到一个 ``PhysAddr`` +;反之亦然。其实由于我们在声明结构体的时候将字段公开了出来,从物理地址变量 ``pa`` 得到它的 usize 表示的更简便方法 +是直接 ``pa.0`` 。 + +.. note:: + + **Rust 语法卡片:类型转换之 From 和 Into** + + 一般而言,当我们为类型 ``U`` 实现了 ``From`` Trait 之后,可以使用 ``U::from(_: T)`` 来从一个 ``T`` + 类型的实例来构造一个 ``U`` 类型的实例;而当我们为类型 ``U`` 实现了 ``Into`` Trait 之后,对于一个 ``U`` + 类型的实例 ``u`` ,可以使用 ``u.into()`` 来将其转化为一个类型为 ``T`` 的实例。 + + 当我们为 ``U`` 实现了 ``From`` 之后,Rust 会自动为 ``T`` 实现 ``Into`` Trait, + 因为它们两个本来就是在做相同的事情。因此我们只需相互实现 ``From`` 就可以相互 ``From/Into`` 了。 + + 需要注意的是,当我们使用 ``From`` Trait 的 ``from`` 方法来构造一个转换后类型的实例的时候,``from`` 的参数 + 已经指明了转换前的类型,因而 Rust 编译器知道该使用哪个实现;而使用 ``Into`` Trait 的 ``into`` 方法来将当前 + 类型转化为另一种类型的时候,它并没有参数,因而函数签名中并没有指出要转化为哪一个类型,则我们必须在其他地方 *显式* + 指出目标类型。比如,当我们要将 ``u.into()`` 绑定到一个新变量 ``t`` 的时候,必须通过 ``let t: T`` 显式声明 + ``t`` 的类型;又或是将 ``u.into()`` 的结果作为参数传给某一个函数,那么这个函数的函数签名中一定指出了传入位置 + 的参数的类型,Rust 编译器也就明确知道转换的类型。 + + 请注意,解引用 ``Deref`` Trait 是 Rust 编译器唯一允许的一种隐式类型转换,而对于其他的类型转换,我们必须手动 + 调用类型转化方法或者是显式给出转换前后的类型。这体现了 Rust 的类型安全特性,在 C/C++ 中并不是如此,比如两个 + 不同的整数/浮点数类型进行二元运算的时候,编译器经常要先进行隐式类型转换使两个操作数类型相同,而后再进行运算,导致 + 了很多数值溢出或精度损失问题。Rust 不会进行这种隐式类型转换,它会在编译期直接报错,提示两个操作数类型不匹配。 + +其次,地址和页号之间可以相互转换。我们这里仍以物理地址和物理页号之间的转换为例: + +.. code-block:: rust + :linenos: + + // os/src/mm/address.rs + + impl PhysAddr { + pub fn page_offset(&self) -> usize { self.0 & (PAGE_SIZE - 1) } + } + + impl From for PhysPageNum { + fn from(v: PhysAddr) -> Self { + assert_eq!(v.page_offset(), 0); + v.floor() + } + } + + impl From for PhysAddr { + fn from(v: PhysPageNum) -> Self { Self(v.0 << PAGE_SIZE_BITS) } + } + +其中 ``PAGE_SIZE`` 为 :math:`4096` , ``PAGE_SIZE_BITS`` 为 :math:`12` ,它们均定义在 ``config`` 子模块 +中,分别表示每个页面的大小和页内偏移的位宽。从物理页号到物理地址的转换只需左移 :math:`12` 位即可,但是物理地址需要 +保证它与页面大小对齐才能通过右移转换为物理页号。 + +对于不对齐的情况,物理地址不能通过 ``From/Into`` 转换为物理页号,而是需要通过它自己的 ``floor`` 或 ``ceil`` 方法来 +进行下取整或上取整的转换。 + +.. code-block:: rust + + // os/src/mm/address.rs + + impl PhysAddr { + pub fn floor(&self) -> PhysPageNum { PhysPageNum(self.0 / PAGE_SIZE) } + pub fn ceil(&self) -> PhysPageNum { PhysPageNum((self.0 + PAGE_SIZE - 1) / PAGE_SIZE) } + } + +我们暂时先介绍这两种最简单的类型转换。 + +页表项的数据结构抽象与类型定义 +----------------------------------------- + +第一小节中我们提到,在页表中以虚拟页号作为索引不仅能够查到物理页号,还能查到一组保护位,它控制了应用对地址空间每个 +虚拟页面的访问权限。但实际上还有更多的标志位,物理页号和全部的标志位以某种固定的格式保存在一个结构体中,它被称为 +**页表项** (PTE, Page Table Entry) ,是利用虚拟页号在页表中查到的结果。 + +.. image:: sv39-pte.png + +上图为 SV39 分页模式下的页表项,其中 :math:`[53:10]` 这 :math:`44` 位是物理页号,最低的 :math:`8` 位 +:math:`[7:0]` 则是标志位,它们的含义如下(请注意,为方便说明,下文我们用 *页表项的对应虚拟页面* 来表示索引到 +一个页表项的虚拟页号对应的虚拟页面): + +- 仅当 V(Valid) 位为 1 时,页表项才是合法的; +- R/W/X 分别控制索引到这个页表项的对应虚拟页面是否允许读/写/取指; +- U 控制索引到这个页表项的对应虚拟页面是否在 CPU 处于 U 特权级的情况下是否被允许访问; +- G 我们暂且不理会; +- A(Accessed) 记录自从页表项上的这一位被清零之后,页表项的对应虚拟页面是否被访问过; +- D(Dirty) 则记录自从页表项上的这一位被清零之后,页表项的对应虚拟页表是否被修改过。 + +让我们先来实现页表项中的标志位 ``PTEFlags`` : + +.. code-block:: rust + + // os/src/main.rs + + #[macro_use] + extern crate bitflags; + + // os/src/mm/page_table.rs + + use bitflags::*; + + bitflags! { + pub struct PTEFlags: u8 { + const V = 1 << 0; + const R = 1 << 1; + const W = 1 << 2; + const X = 1 << 3; + const U = 1 << 4; + const G = 1 << 5; + const A = 1 << 6; + const D = 1 << 7; + } + } + +`bitflags `_ 是一个 Rust 中常用来比特标志位的 crate 。它提供了 +一个 ``bitflags!`` 宏,如上面的代码段所展示的那样,可以将一个 ``u8`` 封装成一个标志位的集合类型,支持一些常见的集合 +运算。它的一些使用细节这里不展开,请读者自行参考它的官方文档。注意,在使用之前我们需要引入该 crate 的依赖: + +.. code-block:: toml + + # os/Cargo.toml + + [dependencies] + bitflags = "1.2.1" + +接下来我们实现页表项 ``PageTableEntry`` : + +.. code-block:: rust + :linenos: + + // os/src/mm/page_table.rs + + #[derive(Copy, Clone)] + #[repr(C)] + pub struct PageTableEntry { + pub bits: usize, + } + + impl PageTableEntry { + pub fn new(ppn: PhysPageNum, flags: PTEFlags) -> Self { + PageTableEntry { + bits: ppn.0 << 10 | flags.bits as usize, + } + } + pub fn empty() -> Self { + PageTableEntry { + bits: 0, + } + } + pub fn ppn(&self) -> PhysPageNum { + (self.bits >> 10 & ((1usize << 44) - 1)).into() + } + pub fn flags(&self) -> PTEFlags { + PTEFlags::from_bits(self.bits as u8).unwrap() + } + } + +- 第 3 行我们让编译器自动为 ``PageTableEntry`` 实现 ``Copy/Clone`` Trait,来让这个类型以值语义赋值/传参的时候 + 不会发生所有权转移,而是拷贝一份新的副本。从这一点来说 ``PageTableEntry`` 就和 usize 一样,因为它也只是后者的 + 一层简单包装,解释了 usize 各个比特段的含义。 +- 第 10 行使得我们可以从一个物理页号 ``PhysPageNum`` 和一个页表项标志位 ``PTEFlags`` 生成一个页表项 + ``PageTableEntry`` 实例;而第 20 行和第 23 行则分别可以从一个页表项将它们两个取出。 +- 第 15 行中,我们也可以通过 ``empty`` 方法生成一个全零的页表项,注意这隐含着该页表项的 V 标志位为 0 , + 因此它是不合法的。 + +后面我们还为 ``PageTableEntry`` 实现了一些辅助函数(Helper Function),可以快速判断一个页表项的 V/R/W/X 标志位是否为 1,以 V +标志位的判断为例: + +.. code-block:: rust + + // os/src/mm/page_table.rs + + impl PageTableEntry { + pub fn is_valid(&self) -> bool { + (self.flags() & PTEFlags::V) != PTEFlags::empty() + } + } + +这里相当于判断两个集合的交集是否为空集,部分说明了 ``bitflags`` crate 的使用方法。 + +多级页表原理 +------------------------------- + +页表的一种最简单的实现是线性表,也就是按照地址从低到高、输入的虚拟页号从 :math:`0` 开始递增的顺序依次在内存中 +(我们之前提到过页表的容量过大无法保存在 CPU 中)放置每个虚拟页号对应的页表项。由于每个页表项的大小是 :math:`8` +字节,我们只要知道第一个页表项(对应虚拟页号 :math:`0` )被放在的物理地址 :math:`\text{base_addr}` ,就能 +直接计算出每个输入的虚拟页号对应的页表项所在的位置。如下图所示: + +.. image:: linear-table.png + :height: 400 + :align: center + +事实上,对于虚拟页号 :math:`i` ,如果页表(每个应用都有一个页表,这里指其中某一个)的起始地址为 +:math:`\text{base_addr}` ,则这个虚拟页号对应的页表项可以在物理地址 :math:`\text{base_addr}+8i` 处找到。 +这使得 MMU 的实现和内核的软件控制都变得非常简单。然而遗憾的是,这远远超出了我们的物理内存限制。由于虚拟页号有 +:math:`2^{27}` 种,每个虚拟页号对应一个 :math:`8` 字节的页表项,则每个页表都需要消耗掉 :math:`1\text{GiB}` +内存!应用的数据还需要保存在内存的其他位置,这就使得每个应用要吃掉 :math:`1\text{GiB}` 以上的内存。作为对比, +我们的 K210 开发板目前只有 :math:`8\text{MiB}` 的内存,因此从空间占用角度来说,这种线性表实现是完全不可行的。 + +线性表的问题在于:它保存了所有虚拟页号对应的页表项,但是高达 :math:`512\text{GiB}` 的地址空间中真正会被应用 +使用到的只是其中极小的一个子集(本教程中的应用内存使用量约在数十~数百 :math:`\text{KiB}` 量级),也就导致 +有意义并能在页表中查到实际的物理页号的虚拟页号在 :math:`2^{27}` 中也只是很小的一部分。由此线性表的绝大部分空间 +其实都是被浪费掉的。 + +那么如何进行优化呢?核心思想就在于 **按需分配** ,也就是说:有多少合法的虚拟页号,我们就维护一个多大的映射,并为此使用 +多大的内存用来保存映射。这是因为,每个应用的地址空间最开始都是空的,或者说所有的虚拟页号均不合法,那么这样的页表 +自然不需要占用任何内存, MMU 在地址转换的时候无需关心页表的内容而是将所有的虚拟页号均判为不合法即可。而在后面, +内核已经决定好了一个应用的各逻辑段存放位置之后,它就需要负责从零开始以虚拟页面为单位来让该应用的地址空间的某些部分 +变得合法,反映在该应用的页表上也就是一对对映射顺次被插入进来,自然页表所占据的内存大小也就逐渐增加。 + +这种思想在计算机科学中得到了广泛应用:为了方便接下来的说明,我们可以举一道数据结构的题目作为例子。设想我们要维护 +一个字符串的多重集,集合中所有的字符串的字符集均为 :math:`\alpha=\{a,b,c\}` ,长度均为一个给定的常数 +:math:`n` 。该字符串集合一开始为空集。我们要支持两种操作,第一种是将一个字符串插入集合,第二种是查询一个字符串在当前 +的集合中出现了多少次。 + +.. _term-trie: + +简单起见,假设 :math:`n=3` 。那么我们可能会建立这样一颗 **字典树** (Trie) : + +.. image:: trie.png + +字典树由若干个节点(图中用椭圆形来表示)组成,从逻辑上而言每个节点代表一个可能的字符串前缀。每个节点的存储内容 +都只有三个指针,对于蓝色的非叶节点来说,它的三个指针各自指向一个子节点;而对于绿色的叶子节点来说,它的三个指针不再指向 +任何节点,而是具体保存一种可能的长度为 :math:`n` 的字符串的计数。这样,对于题目要求的两种操作,我们只需根据输入的 +字符串中的每个字符在字典树上自上而下对应走出一步,最终就能够找到字典树中维护的它的计数。之后我们可以将其直接返回或者 +加一。 + +注意到如果某些字符串自始至终没有被插入,那么一些节点没有存在的必要。反过来说一些节点是由于我们插入了一个以它对应的字符串 +为前缀的字符串才被分配出来的。如下图所示: + +.. image:: trie-1.png + +一开始仅存在一个根节点。在我们插入字符串 ``acb`` 的过程中,我们只需要分配 ``a`` 和 ``ac`` 两个节点。 +注意 ``ac`` 是一个叶节点,它的 ``b`` 指针不再指向另外一个节点而是保存字符串 ``acb`` 的计数。 +此时我们无法访问到其他未分配的节点,如根节点的 ``b/c`` 或是 ``a`` 节点的 ``a/b`` 均为空指针。 +如果后续再插入一个字符串,那么 **至多分配两个新节点** ,因为如果走的路径上有节点已经存在,就无需重复分配了。 +这可以说明,字典树中节点的数目(或者说字典树消耗的内存)是随着插入字符串的数目逐渐线性增加的。 + +读者可能很好奇,为何在这里要用相当一部分篇幅来介绍字典树呢?事实上 SV39 分页机制等价于一颗字典树。 :math:`27` 位的 +虚拟页号可以看成一个长度 :math:`n=3` 的字符串,字符集为 :math:`\alpha=\{0,1,2,...,511\}` ,因为每一位字符都 +由 :math:`9` 个比特组成。而我们也不再维护所谓字符串的计数,而是要找到字符串(虚拟页号)对应的页表项。 +因此,每个叶节点都需要保存 :math:`512` 个 :math:`8` 字节的页表项,一共正好 :math:`4\text{KiB}` , +可以直接放在一个物理页帧内。而对于非叶节点来说,从功能上它只需要保存 :math:`512` 个指向下级节点的指针即可, +不过我们就像叶节点那样也保存 :math:`512` 个页表项,这样所有的节点都可以被放在一个物理页帧内,它们的位置可以用一个 +物理页号来代替。当想从一个非叶节点向下走时,只需找到当前字符对应的页表项的物理页号字段,它就指向了下一级节点的位置, +这样非叶节点中转的功能也就实现了。每个节点的内部是一个线性表,也就是将这个节点起始物理地址加上字符对应的偏移量就找到了 +指向下一级节点的页表项(对于非叶节点)或是能够直接用来地址转换的页表项(对于叶节点)。 + +.. _term-multi-level-page-table: +.. _term-page-index: + +这种页表实现被称为 **多级页表** (Multi-Level Page-Table) 。由于 SV39 中虚拟页号被分为三级 **页索引** +(Page Index) ,因此这是一种三级页表。 + +非叶节点的页表项标志位含义和叶节点相比有一些不同: + +- 当 V 为 0 的时候,代表当前指针是一个空指针,无法走向下一级节点,即该页表项对应的虚拟地址范围是无效的; +- 只有当V 为1 且 R/W/X 均为 0 时,表示是一个合法的页目录表项,其包含的指针会指向下一级的页表。 +- 注意: 当V 为1 且 R/W/X 不全为 0 时,表示是一个合法的页表项,其包含了虚地址对应的物理页号。 + +在这里我们给出 SV39 中的 R/W/X 组合的含义: + + .. image:: pte-rwx.png + :align: center + :height: 250 + +.. _term-huge-page: + +.. note:: + + **大页** (Huge Page) + + 本教程中并没有用到大页的知识,这里只是作为拓展,不感兴趣的读者可以跳过。 + + 事实上正确的说法应该是:只要 R/W/X 不全为 0 就会停下来,直接从当前的页表项中取出物理页号进行最终的地址转换。 + 如果这一过程并没有发生在多级页表的最深层,那么在地址转换的时候并不是直接将物理页号和虚拟地址中的页内偏移接 + 在一起得到物理地址,这样做会有问题:由于有若干级页索引并没有被使用到,即使两个虚拟地址的这些级页索引不同, + 还是会最终得到一个相同的物理地址,导致冲突。 + + 我们需要重新理解将物理页号和页内偏移“接起来”这一行为,它的本质是将物理页号对应的物理页帧的起始物理地址和 + 页内偏移进行求和,前者是将物理页号左移上页内偏移的位数得到,因此看上去恰好就是将物理页号和页内偏移接在一起。 + 但是如果在从多级页表往下走的中途停止,未用到的页索引会和虚拟地址的 :math:`12` 位页内偏移一起形成一个 + 位数更多的页内偏移,也就对应于一个大页,在转换物理地址的时候,其算法仍是上述二者求和,但那时便不再是简单的 + 拼接操作。 + + 在 SV39 中,如果使用了一级页索引就停下来,则它可以涵盖虚拟页号的前 :math:`9` 位为某一固定值的所有虚拟地址, + 对应于一个 :math:`1\text{GiB}` 的大页;如果使用了二级页索引就停下来,则它可以涵盖虚拟页号的前 + :math:`18` 位为某一固定值的所有虚拟地址,对应于一个 :math:`2\text{MiB}` 的大页。以同样的视角,如果使用了 + 所有三级页索引才停下来,它可以涵盖虚拟页号为某一个固定值的所有虚拟地址,自然也就对应于一个大小为 + :math:`4\text{KiB}` 的虚拟页面。 + + 使用大页的优点在于,当地址空间的大块连续区域的访问权限均相同的时候,可以直接映射一个大页,从时间上避免了大量 + 页表项的索引和修改,从空间上降低了所需节点的数目。但是,从内存分配算法的角度,这需要内核支持从物理内存上分配 + 三种不同大小的连续区域( :math:`4\text{KiB}` 或是另外两种大页),便不能使用更为简单的插槽式管理。权衡利弊 + 之后,本书全程只会以 :math:`4\text{KiB}` 为单位进行页表映射而不会使用大页特性。 + +那么 SV39 多级页表相比线性表到底能节省多少内存呢?这里直接给出结论:设某个应用地址空间实际用到的区域总大小为 +:math:`S` 字节,则地址空间对应的多级页表消耗内存为 :math:`\frac{S}{512}` 左右。下面给出了详细分析,对此 +不感兴趣的读者可以直接跳过。 + +.. note:: + + **分析 SV39 多级页表的内存占用** + + 我们知道,多级页表的总内存消耗取决于节点的数目,每个节点 + 则需要一个大小为 :math:`4\text{KiB}` 物理页帧存放。不妨设某个应用地址空间中的实际用到的总空间大小为 :math:`S` + 字节,则多级页表所需的内存至少有这样两个上界: + + - 每映射一个 :math:`4\text{KiB}` 的虚拟页面,最多需要新分配两个物理页帧来保存新的节点,加上初始就有一个根节点, + 因此消耗内存不超过 + :math:`4\text{KiB}\times(1+2\frac{S}{4\text{KiB}})=4\text{KiB}+2S` ; + - 考虑已经映射了很多虚拟页面,使得根节点的 :math:`512` 个孩子节点都已经被分配的情况,此时最坏的情况是每次映射 + 都需要分配一个不同的最深层节点,加上根节点的所有孩子节点并不一定都被分配,从这个角度来讲消耗内存不超过 + :math:`4\text{KiB}\times(1+512+\frac{S}{4\text{KiB}})=4\text{KiB}+2\text{MiB}+S` 。 + + 虽然这两个上限都可以通过刻意构造一种地址空间的使用来达到,但是它们看起来很不合理,因为它们均大于 :math:`S` ,也就是 + 元数据比数据还大。其实,真实环境中一般不会有如此极端的使用方式,更加贴近 + 实际的是下面一种上限:即除了根节点的一个物理页帧之外,地址空间中的每个实际用到的大小为 :math:`T` 字节的 *连续* 区间 + 会让多级页表额外消耗不超过 :math:`4\text{KiB}\times(\lceil\frac{T}{2\text{MiB}}\rceil+\lceil\frac{T}{1\text{GiB}}\rceil)` + 的内存。这是因为,括号中的两项分别对应为了映射这段连续区间所需要新分配的最深层和次深层节点的数目,前者每连续映射 + :math:`2\text{MiB}` 才会新分配一个,而后者每连续映射 :math:`1\text{GiB}` 才会新分配一个。由于后者远小于前者, + 可以将后者忽略,最后得到的结果近似于 :math:`\frac{T}{512}` 。而一般情况下我们对于地址空间的使用方法都是在其中 + 放置少数几个连续的逻辑段,因此当一个地址空间实际使用的区域大小总和为 :math:`S` 字节的时候,我们可以认为为此多级页表 + 消耗的内存在 :math:`\frac{S}{512}` 左右。相比线性表固定消耗 :math:`1\text{GiB}` 的内存,这已经相当可以 + 接受了。 + +上面主要是对一个固定应用的多级页表进行了介绍。在一个多任务系统中,可能同时存在多个任务处于运行/就绪状态,它们的多级页表 +在内存中共存,那么 MMU 应该如何知道当前做地址转换的时候要查哪一个页表呢?回到 :ref:`satp CSR 的布局 ` , +其中的 PPN 字段指的就是多级页表根节点所在的物理页号。因此,每个应用的地址空间就可以用包含了它多级页表根节点所在物理页号 +的 ``satp`` CSR 代表。在我们切换任务的时候, ``satp`` 也必须被同时切换。 + +最后的最后,我们给出 SV39 地址转换的全过程图示来结束多级页表原理的介绍: + +.. image:: sv39-full.png + :height: 600 + :align: center diff --git a/source/chapter4/4sv39-implementation-2.rst b/source/chapter4/4sv39-implementation-2.rst new file mode 100644 index 0000000000000000000000000000000000000000..b0f44c98cfddc6fdf08b77099d1c251f4af6880c --- /dev/null +++ b/source/chapter4/4sv39-implementation-2.rst @@ -0,0 +1,618 @@ +实现 SV39 多级页表机制(下) +======================================================== + + +本节导读 +-------------------------- + + +本节我们继续来实现 SV39 多级页表机制。这还需进一步了解和管理当前已经使用是或空闲的物理页帧,这样操作系统才能给应用程序动态分配或回收物理地址空间。有了有效的物理内存空间的管理,操作系统就能够在物理内存空间中建立多级页表(页表占用物理内存),为应用程序和操作系统自身建立虚实地址映射关系,从而实现虚拟内存空间,即给应用“看到”的地址空间。 + +物理页帧管理 +----------------------------------- + +从前面的介绍可以看出物理页帧的重要性:它既可以用来实际存放应用的数据,也能够用来存储某个应用多级页表中的一个节点。 +目前的物理内存上已经有一部分用于放置内核的代码和数据,我们需要将剩下可用的部分以单个物理页帧为单位管理起来, +当需要存放应用数据或是应用的多级页表需要一个新节点的时候分配一个物理页帧,并在应用出错或退出的时候回收它占有 +的所有物理页帧。 + +可用物理页的分配与回收 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +首先,我们需要知道物理内存的哪一部分是可用的。在 ``os/src/linker.ld`` 中,我们用符号 ``ekernel`` 指明了 +内核数据的终止物理地址,在它之后的物理内存都是可用的。而在 ``config`` 子模块中: + +.. code-block:: rust + + // os/src/config.rs + + pub const MEMORY_END: usize = 0x80800000; + +我们硬编码整块物理内存的终止物理地址为 ``0x80800000`` 。 而 :ref:`之前 ` 提到过物理内存的 +起始物理地址为 ``0x80000000`` ,这意味着我们将可用内存大小设置为 :math:`8\text{MiB}` 。 +实际上在 Qemu 模拟器上可以通过设置使用更大的物理内存,但这里我们希望 +它和真实硬件 K210 的配置保持一致,因此设置为仅使用 :math:`8\text{MiB}` 。我们用一个左闭右开的物理页号区间来表示 +可用的物理内存,则: + +- 区间的左端点应该是 ``ekernel`` 的物理地址以上取整方式转化成的物理页号; +- 区间的右端点应该是 ``MEMORY_END`` 以下取整方式转化成的物理页号。 + +这个区间将被传给我们后面实现的物理页帧管理器用于初始化。 + +我们声明一个 ``FrameAllocator`` Trait 来描述一个物理页帧管理器需要提供哪些功能: + +.. code-block:: rust + + // os/src/mm/frame_allocator.rs + + trait FrameAllocator { + fn new() -> Self; + fn alloc(&mut self) -> Option; + fn dealloc(&mut self, ppn: PhysPageNum); + } + +即创建一个实例,还有以物理页号为单位进行物理页帧的分配和回收。 + +我们实现一种最简单的栈式物理页帧管理策略 ``StackFrameAllocator`` : + +.. code-block:: rust + + // os/src/mm/frame_allocator.rs + + pub struct StackFrameAllocator { + current: usize, + end: usize, + recycled: Vec, + } + +其中各字段的含义是:物理页号区间 :math:`[\text{current},\text{end})` 此前均 *从未* 被分配出去过,而向量 +``recycled`` 以后入先出的方式保存了被回收的物理页号(注意我们已经自然的将内核堆用起来了)。 + +初始化非常简单。在通过 ``FrameAllocator`` 的 ``new`` 方法创建实例的时候,只需将区间两端均设为 :math:`0` , +然后创建一个新的向量;而在它真正被使用起来之前,需要调用 ``init`` 方法将自身的 :math:`[\text{current},\text{end})` +初始化为可用物理页号区间: + +.. code-block:: rust + + // os/src/mm/frame_allocator.rs + + impl FrameAllocator for StackFrameAllocator { + fn new() -> Self { + Self { + current: 0, + end: 0, + recycled: Vec::new(), + } + } + } + + impl StackFrameAllocator { + pub fn init(&mut self, l: PhysPageNum, r: PhysPageNum) { + self.current = l.0; + self.end = r.0; + } + } + +接下来我们来看核心的物理页帧分配和回收如何实现: + +.. code-block:: rust + + // os/src/mm/frame_allocator.rs + + impl FrameAllocator for StackFrameAllocator { + fn alloc(&mut self) -> Option { + if let Some(ppn) = self.recycled.pop() { + Some(ppn.into()) + } else { + if self.current == self.end { + None + } else { + self.current += 1; + Some((self.current - 1).into()) + } + } + } + fn dealloc(&mut self, ppn: PhysPageNum) { + let ppn = ppn.0; + // validity check + if ppn >= self.current || self.recycled + .iter() + .find(|&v| {*v == ppn}) + .is_some() { + panic!("Frame ppn={:#x} has not been allocated!", ppn); + } + // recycle + self.recycled.push(ppn); + } + } + +- 在分配 ``alloc`` 的时候,首先会检查栈 ``recycled`` 内有没有之前回收的物理页号,如果有的话直接弹出栈顶并返回; + 否则的话我们只能从之前从未分配过的物理页号区间 :math:`[\text{current},\text{end})` 上进行分配,我们分配它的 + 左端点 ``current`` ,同时将管理器内部维护的 ``current`` 加一代表 ``current`` 此前已经被分配过了。在即将返回 + 的时候,我们使用 ``into`` 方法将 usize 转换成了物理页号 ``PhysPageNum`` 。 + + 注意极端情况下可能出现内存耗尽分配失败的情况:即 ``recycled`` 为空且 :math:`\text{current}==\text{end}` 。 + 为了涵盖这种情况, ``alloc`` 的返回值被 ``Option`` 包裹,我们返回 ``None`` 即可。 +- 在回收 ``dealloc`` 的时候,我们需要检查回收页面的合法性,然后将其压入 ``recycled`` 栈中。回收页面合法有两个 + 条件: + + - 该页面之前一定被分配出去过,因此它的物理页号一定 :math:`<\text{current}` ; + - 该页面没有正处在回收状态,即它的物理页号不能在栈 ``recycled`` 中找到。 + + 我们通过 ``recycled.iter()`` 获取栈上内容的迭代器,然后通过迭代器的 ``find`` 方法试图 + 寻找一个与输入物理页号相同的元素。其返回值是一个 ``Option`` ,如果找到了就会是一个 ``Option::Some`` , + 这种情况说明我们内核其他部分实现有误,直接报错退出。 + +下面我们来创建 ``StackFrameAllocator`` 的全局实例 ``FRAME_ALLOCATOR`` : + +.. code-block:: rust + + // os/src/mm/frame_allocator.rs + + use spin::Mutex; + + type FrameAllocatorImpl = StackFrameAllocator; + + lazy_static! { + pub static ref FRAME_ALLOCATOR: Mutex = + Mutex::new(FrameAllocatorImpl::new()); + } + +这里我们使用互斥锁 ``Mutex`` 来包裹栈式物理页帧分配器。每次对该分配器进行操作之前,我们都需要先通过 +``FRAME_ALLOCATOR.lock()`` 拿到分配器的可变借用。注意 ``alloc`` 中并没有提供 ``Mutex`` ,它 +来自于一个我们在 ``no_std`` 的裸机环境下经常使用的名为 ``spin`` 的 crate ,它仅依赖 Rust 核心库 +``core`` 提供一些可跨平台使用的同步原语,如互斥锁 ``Mutex`` 和读写锁 ``RwLock`` 等。 + +.. note:: + + **Rust 语法卡片:在单核环境下使用 Mutex 的原因** + + 在编写一个多线程的应用时,加锁的目的是为了避免数据竞争,使得里层的共享数据结构同一时间只有一个线程 + 在对它进行访问。然而,目前我们的内核运行在单 CPU 上,且 Trap 进入内核之后并没有手动打开中断,这也就 + 使得同一时间最多只有一条 Trap 执行流并发访问内核的各数据结构,此时应该是并没有任何数据竞争风险的。那么 + 加锁的原因其实有两点: + + 1. 在不触及 ``unsafe`` 的情况下实现 ``static mut`` 语义。如果读者还有印象, + :ref:`前面章节 ` 我们使用 ``RefCell`` 提供了内部可变性去掉了 + 声明中的 ``mut`` ,然而麻烦的在于 ``static`` ,在 Rust 中一个类型想被实例化为一个全局变量,则 + 该类型必须先告知编译器自己某种意义上是线程安全的,这个过程本身是 ``unsafe`` 的。 + + 因此我们直接使用 ``Mutex`` ,它既通过 ``lock`` 方法提供了内部可变性,又已经在模块内部告知了 + 编译器它的线程安全性。这样 ``unsafe`` 就被隐藏在了 ``spin`` crate 之内,我们无需关心。这种风格 + 是 Rust 所推荐的。 + 2. 方便后续拓展到真正存在数据竞争风险的多核环境下运行。 + + 这里引入了一些新概念,比如什么是线程,又如何定义线程安全?读者可以先不必深究,暂时有一个初步的概念即可。 + +我们需要添加该 crate 的依赖: + +.. code-block:: toml + + # os/Cargo.toml + + [dependencies] + spin = "0.7.0" + +在正式分配物理页帧之前,我们需要将物理页帧全局管理器 ``FRAME_ALLOCATOR`` 初始化: + +.. code-block:: rust + + // os/src/mm/frame_allocator.rs + + pub fn init_frame_allocator() { + extern "C" { + fn ekernel(); + } + FRAME_ALLOCATOR + .lock() + .init(PhysAddr::from(ekernel as usize).ceil(), PhysAddr::from(MEMORY_END).floor()); + } + +这里我们调用物理地址 ``PhysAddr`` 的 ``floor/ceil`` 方法分别下/上取整获得可用的物理页号区间。 + + +分配/回收物理页帧的接口 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +然后是真正公开给其他子模块调用的分配/回收物理页帧的接口: + +.. code-block:: rust + + // os/src/mm/frame_allocator.rs + + pub fn frame_alloc() -> Option { + FRAME_ALLOCATOR + .lock() + .alloc() + .map(|ppn| FrameTracker::new(ppn)) + } + + fn frame_dealloc(ppn: PhysPageNum) { + FRAME_ALLOCATOR + .lock() + .dealloc(ppn); + } + +可以发现, ``frame_alloc`` 的返回值类型并不是 ``FrameAllocator`` 要求的物理页号 ``PhysPageNum`` ,而是将其 +进一步包装为一个 ``FrameTracker`` 。这里借用了 RAII 的思想,将一个物理页帧的生命周期绑定到一个 ``FrameTracker`` +变量上,当一个 ``FrameTracker`` 被创建的时候,我们需要从 ``FRAME_ALLOCATOR`` 中分配一个物理页帧: + +.. code-block:: rust + + // os/src/mm/frame_allocator.rs + + pub struct FrameTracker { + pub ppn: PhysPageNum, + } + + impl FrameTracker { + pub fn new(ppn: PhysPageNum) -> Self { + // page cleaning + let bytes_array = ppn.get_bytes_array(); + for i in bytes_array { + *i = 0; + } + Self { ppn } + } + } + +我们将分配来的物理页帧的物理页号作为参数传给 ``FrameTracker`` 的 ``new`` 方法来创建一个 ``FrameTracker`` +实例。由于这个物理页帧之前可能被分配过并用做其他用途,我们在这里直接将这个物理页帧上的所有字节清零。这一过程并不 +那么显然,我们后面再详细介绍。 + +当一个 ``FrameTracker`` 生命周期结束被编译器回收的时候,我们需要将它控制的物理页帧回收掉 ``FRAME_ALLOCATOR`` 中: + +.. code-block:: rust + + // os/src/mm/frame_allocator.rs + + impl Drop for FrameTracker { + fn drop(&mut self) { + frame_dealloc(self.ppn); + } + } + +这里我们只需为 ``FrameTracker`` 实现 ``Drop`` Trait 即可。当一个 ``FrameTracker`` 实例被回收的时候,它的 +``drop`` 方法会自动被编译器调用,通过之前实现的 ``frame_dealloc`` 我们就将它控制的物理页帧回收以供后续使用了。 + +.. note:: + + **Rust 语法卡片:Drop Trait** + + Rust 中的 ``Drop`` Trait 是它的 RAII 内存管理风格可以被有效实践的关键。之前介绍的多种在堆上分配的 Rust + 数据结构便都是通过实现 ``Drop`` Trait 来进行被绑定资源的自动回收的。例如: + + - ``Box`` 的 ``drop`` 方法会回收它控制的分配在堆上的那个变量; + - ``Rc`` 的 ``drop`` 方法会减少分配在堆上的那个引用计数,一旦变为零则分配在堆上的那个被计数的变量自身 + 也会被回收; + - ``Mutex`` 的 ``lock`` 方法会获取互斥锁并返回一个 ``MutexGuard<'a, T>`` ,它可以被当做一个 ``&mut T`` + 来使用;而 ``MutexGuard<'a, T>`` 的 ``drop`` 方法会将锁释放,从而允许其他线程获取锁并开始访问里层的 + 数据结构。锁的实现原理我们先不介绍。 + + ``FrameTracker`` 的设计也是基于同样的思想,有了它之后我们就不必手动回收物理页帧了,这在编译期就解决了很多 + 潜在的问题。 + +最后做一个小结:从其他模块的视角看来,物理页帧分配的接口是调用 ``frame_alloc`` 函数得到一个 ``FrameTracker`` +(如果物理内存还有剩余),它就代表了一个物理页帧,当它的生命周期结束之后它所控制的物理页帧将被自动回收。下面是 +一段演示该接口使用方法的测试程序: + +.. code-block:: rust + :linenos: + :emphasize-lines: 9 + + // os/src/mm/frame_allocator.rs + + #[allow(unused)] + pub fn frame_allocator_test() { + let mut v: Vec = Vec::new(); + for i in 0..5 { + let frame = frame_alloc().unwrap(); + println!("{:?}", frame); + v.push(frame); + } + v.clear(); + for i in 0..5 { + let frame = frame_alloc().unwrap(); + println!("{:?}", frame); + v.push(frame); + } + drop(v); + println!("frame_allocator_test passed!"); + } + +如果我们将第 9 行删去,则第一轮分配的 5 个物理页帧都是分配之后在循环末尾就被立即回收,因为循环作用域的临时变量 +``frame`` 的生命周期在那时结束了。然而,如果我们将它们 move 到一个向量中,它们的生命周期便被延长了——直到第 11 行 +向量被清空的时候,这些 ``FrameTracker`` 的生命周期才结束,它们控制的 5 个物理页帧才被回收。这种思想我们立即 +就会用到。 + +多级页表实现 +----------------------------------- + + +页表基本数据结构与访问接口 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +我们知道,SV39 多级页表是以节点为单位进行管理的。每个节点恰好存储在一个物理页帧中,它的位置可以用一个物理页号来 +表示。 + +.. code-block:: rust + :linenos: + + // os/src/mm/page_table.rs + + pub struct PageTable { + root_ppn: PhysPageNum, + frames: Vec, + } + + impl PageTable { + pub fn new() -> Self { + let frame = frame_alloc().unwrap(); + PageTable { + root_ppn: frame.ppn, + frames: vec![frame], + } + } + } + +每个应用的地址空间都对应一个不同的多级页表,这也就意味这不同页表的起始地址(即页表根节点的地址)是不一样的。因此 ``PageTable`` 要保存它根节点的物理页号 ``root_ppn`` 作为页表唯一的区分标志。此外, +向量 ``frames`` 以 ``FrameTracker`` 的形式保存了页表所有的节点(包括根节点)所在的物理页帧。这与物理页帧管理模块 +的测试程序是一个思路,即将这些 ``FrameTracker`` 的生命周期进一步绑定到 ``PageTable`` 下面。当 ``PageTable`` +生命周期结束后,向量 ``frames`` 里面的那些 ``FrameTracker`` 也会被回收,也就意味着存放多级页表节点的那些物理页帧 +被回收了。 + +当我们通过 ``new`` 方法新建一个 ``PageTable`` 的时候,它只需有一个根节点。为此我们需要分配一个物理页帧 +``FrameTracker`` 并挂在向量 ``frames`` 下,然后更新根节点的物理页号 ``root_ppn`` 。 + +多级页表并不是被创建出来之后就不再变化的,为了 MMU 能够通过地址转换正确找到应用地址空间中的数据实际被内核放在内存中 +位置,操作系统需要动态维护一个虚拟页号到页表项的映射,支持插入/删除键值对,其方法签名如下: + +.. code-block:: rust + + // os/src/mm/page_table.rs + + impl PageTable { + pub fn map(&mut self, vpn: VirtPageNum, ppn: PhysPageNum, flags: PTEFlags); + pub fn unmap(&mut self, vpn: VirtPageNum); + } + +- 我们通过 ``map`` 方法来在多级页表中插入一个键值对,注意这里我们将物理页号 ``ppn`` 和页表项标志位 ``flags`` 作为 + 不同的参数传入而不是整合为一个页表项; +- 相对的,我们通过 ``unmap`` 方法来删除一个键值对,在调用时仅需给出作为索引的虚拟页号即可。 + +.. _modify-page-table: + +在这些操作的过程中我们自然需要访问或修改多级页表节点的内容。每个节点都被保存在一个物理页帧中,在多级页表的架构中我们是以 +一个节点被存放在的物理页帧的物理页号作为指针指向该节点,这意味着,对于每个节点来说,一旦我们知道了指向它的物理页号,我们 +就需要能够修改这个节点的内容。前面我们在使用 ``frame_alloc`` 分配一个物理页帧之后便立即将它上面的数据清零其实也是一样 +的需求。总结一下也就是说,至少在操作某个多级页表或是管理物理页帧的时候,我们要能够自由的读写与一个给定的物理页号对应的 +物理页帧上的数据。 + +在尚未启用分页模式之前,内核和应用的代码都可以通过物理地址直接访问内存。而在打开分页模式之后,分别运行在 S 特权级 +和 U 特权级的内核和应用的访存行为都会受到影响,它们的访存地址会被视为一个当前地址空间( ``satp`` CSR 给出当前 +多级页表根节点的物理页号)中的一个虚拟地址,需要 MMU +查相应的多级页表完成地址转换变为物理地址,也就是地址空间中虚拟地址指向的数据真正被内核放在的物理内存中的位置,然后 +才能访问相应的数据。此时,如果想要访问一个特定的物理地址 ``pa`` 所指向的内存上的数据,就需要对应 **构造** 一个虚拟地址 +``va`` ,使得当前地址空间的页表存在映射 :math:`\text{va}\rightarrow\text{pa}` ,且页表项中的保护位允许这种 +访问方式。于是,在代码中我们只需访问地址 ``va`` ,它便会被 MMU 通过地址转换变成 ``pa`` ,这样我们就做到了在启用 +分页模式的情况下也能从某种意义上直接访问内存。 + +.. _term-identical-mapping: + +这就需要我们提前扩充多级页表维护的映射,使得对于每一个对应于某一特定物理页帧的物理页号 ``ppn`` ,均存在一个虚拟页号 +``vpn`` 能够映射到它,而且要能够较为简单的针对一个 ``ppn`` 找到某一个能映射到它的 ``vpn`` 。这里我们采用一种最 +简单的 **恒等映射** (Identical Mapping) ,也就是说对于物理内存上的每个物理页帧,我们都在多级页表中用一个与其 +物理页号相等的虚拟页号映射到它。当我们想针对物理页号构造一个能映射到它的虚拟页号的时候,也只需使用一个和该物理页号 +相等的虚拟页号即可。 + +.. _term-recursive-mapping: + +.. note:: + + **其他的映射方式** + + 为了达到这一目的还存在其他不同的映射方式,例如比较著名的 **页表自映射** (Recursive Mapping) 等。有兴趣的同学 + 可以进一步参考 `BlogOS 中的相关介绍 `_ 。 + +这里需要说明的是,在下一节中我们可以看到,应用和内核的地址空间是隔离的。而直接访问物理页帧的操作只会在内核中进行, +应用无法看到物理页帧管理器和多级页表等内核数据结构。因此,上述的恒等映射只需被附加到内核地址空间即可。 + + +内核中访问物理页帧的方法 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. _access-frame-in-kernel-as: + + +于是,我们来看看在内核中应如何访问一个特定的物理页帧: + +.. code-block:: rust + + // os/src/mm/address.rs + + impl PhysPageNum { + pub fn get_pte_array(&self) -> &'static mut [PageTableEntry] { + let pa: PhysAddr = self.clone().into(); + unsafe { + core::slice::from_raw_parts_mut(pa.0 as *mut PageTableEntry, 512) + } + } + pub fn get_bytes_array(&self) -> &'static mut [u8] { + let pa: PhysAddr = self.clone().into(); + unsafe { + core::slice::from_raw_parts_mut(pa.0 as *mut u8, 4096) + } + } + pub fn get_mut(&self) -> &'static mut T { + let pa: PhysAddr = self.clone().into(); + unsafe { + (pa.0 as *mut T).as_mut().unwrap() + } + } + } + +我们构造可变引用来直接访问一个物理页号 ``PhysPageNum`` 对应的物理页帧,不同的引用类型对应于物理页帧上的一种不同的 +内存布局,如 ``get_pte_array`` 返回的是一个页表项定长数组的可变引用,可以用来修改多级页表中的一个节点;而 +``get_bytes_array`` 返回的是一个字节数组的可变引用,可以以字节为粒度对物理页帧上的数据进行访问,前面进行数据清零 +就用到了这个方法; ``get_mut`` 是个泛型函数,可以获取一个恰好放在一个物理页帧开头的类型为 ``T`` 的数据的可变引用。 + +在实现方面,都是先把物理页号转为物理地址 ``PhysAddr`` ,然后再转成 usize 形式的物理地址。接着,我们直接将它 +转为裸指针用来访问物理地址指向的物理内存。在分页机制开启前,这样做自然成立;而开启之后,虽然裸指针被视为一个虚拟地址, +但是上面已经提到这种情况下虚拟地址会映射到一个相同的物理地址,因此在这种情况下也成立。注意,我们在返回值类型上附加了 +静态生命周期泛型 ``'static`` ,这是为了绕过 Rust 编译器的借用检查,实质上可以将返回的类型也看成一个裸指针,因为 +它也只是标识数据存放的位置以及类型。但与裸指针不同的是,无需通过 ``unsafe`` 的解引用访问它指向的数据,而是可以像一个 +正常的可变引用一样直接访问。 + +.. note:: + + **unsafe 真的就是“不安全”吗?** + + 下面是笔者关于 ``unsafe`` 一点可能不太正确的理解,不感兴趣的读者可以跳过。 + + 当我们在 Rust 中使用 unsafe 的时候,并不仅仅是为了绕过编译器检查,更是为了告知编译器和其他看到这段代码的程序员: + “ **我保证这样做是安全的** ” 。尽管,严格的 Rust 编译器暂时还不能确信这一点。从规范 Rust 代码编写的角度, + 我们需要尽可能绕过 unsafe ,因为如果 Rust 编译器或者一些已有的接口就可以提供安全性,我们当然倾向于利用它们让我们 + 实现的功能仍然是安全的,可以避免一些无谓的心智负担;反之,就只能使用 unsafe ,同时最好说明如何保证这项功能是安全的。 + + 这里简要从内存安全的角度来分析一下 ``PhysPageNum`` 的 ``get_*`` 系列方法的实现中 ``unsafe`` 的使用。为了方便 + 解释,我们可以将 ``PhysPageNum`` 也看成一种 RAII 的风格,即它控制着一个物理页帧资源的访问。首先,这不会导致 + use-after-free 的问题,因为在内核运行全期整块物理内存都是可以访问的,它不存在被释放后无法访问的可能性;其次, + 也不会导致并发冲突。注意这不是在 ``PhysPageNum`` 这一层解决的,而是 ``PhysPageNum`` 的使用层要保证任意两个线程 + 不会同时对一个 ``PhysPageNum`` 进行操作。读者也应该可以感觉出这并不能算是一种好的设计,因为这种约束从代码层面是很 + 难直接保证的,而是需要系统内部的某种一致性。虽然如此,它对于我们这个极简的内核而言算是很合适了。 + +.. chyyuu 上面一段提到了线程??? + +建立和拆除虚实地址映射关系 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +接下来介绍建立和拆除虚实地址映射关系的 ``map`` 和 ``unmap`` 方法是如何实现的。它们都依赖于一个很重要的过程,也即在多级页表中找到一个虚拟地址对应的页表项。 +找到之后,只要修改页表项的内容即可完成键值对的插入和删除。在寻找页表项的时候,可能出现页表的中间级节点还未被创建的情况, +这个时候我们需要手动分配一个物理页帧来存放这个节点,并将这个节点接入到当前的多级页表的某级中。 + + +.. code-block:: rust + :linenos: + + // os/src/mm/address.rs + + impl VirtPageNum { + pub fn indexes(&self) -> [usize; 3] { + let mut vpn = self.0; + let mut idx = [0usize; 3]; + for i in (0..3).rev() { + idx[i] = vpn & 511; + vpn >>= 9; + } + idx + } + } + + // os/src/mm/page_table.rs + + impl PageTable { + fn find_pte_create(&mut self, vpn: VirtPageNum) -> Option<&mut PageTableEntry> { + let idxs = vpn.indexes(); + let mut ppn = self.root_ppn; + let mut result: Option<&mut PageTableEntry> = None; + for i in 0..3 { + let pte = &mut ppn.get_pte_array()[idxs[i]]; + if i == 2 { + result = Some(pte); + break; + } + if !pte.is_valid() { + let frame = frame_alloc().unwrap(); + *pte = PageTableEntry::new(frame.ppn, PTEFlags::V); + self.frames.push(frame); + } + ppn = pte.ppn(); + } + result + } + } + +- ``VirtPageNum`` 的 ``indexes`` 可以取出虚拟页号的三级页索引,并按照从高到低的顺序返回。注意它里面包裹的 + usize 可能有 :math:`27` 位,也有可能有 :math:`64-12=52` 位,但这里我们是用来在多级页表上进行遍历,因此 + 只取出低 :math:`27` 位。 +- ``PageTable::find_pte_create`` 在多级页表找到一个虚拟页号对应的页表项的可变引用方便后续的读写。如果在 + 遍历的过程中发现有节点尚未创建则会新建一个节点。 + + 变量 ``ppn`` 表示当前节点的物理页号,最开始指向多级页表的根节点。随后每次循环通过 ``get_pte_array`` 将 + 取出当前节点的页表项数组,并根据当前级页索引找到对应的页表项。如果当前节点是一个叶节点,那么直接返回这个页表项 + 的可变引用;否则尝试向下走。走不下去的话就新建一个节点,更新作为下级节点指针的页表项,并将新分配的物理页帧移动到 + 向量 ``frames`` 中方便后续的自动回收。注意在更新页表项的时候,不仅要更新物理页号,还要将标志位 V 置 1, + 不然硬件在查多级页表的时候,会认为这个页表项不合法,从而触发 Page Fault 而不能向下走。 + +于是, ``map/unmap`` 就非常容易实现了: + +.. code-block:: rust + + // os/src/mm/page_table.rs + + impl PageTable { + pub fn map(&mut self, vpn: VirtPageNum, ppn: PhysPageNum, flags: PTEFlags) { + let pte = self.find_pte_create(vpn).unwrap(); + assert!(!pte.is_valid(), "vpn {:?} is mapped before mapping", vpn); + *pte = PageTableEntry::new(ppn, flags | PTEFlags::V); + } + pub fn unmap(&mut self, vpn: VirtPageNum) { + let pte = self.find_pte_create(vpn).unwrap(); + assert!(pte.is_valid(), "vpn {:?} is invalid before unmapping", vpn); + *pte = PageTableEntry::empty(); + } + } + +只需根据虚拟页号找到页表项,然后修改或者直接清空其内容即可。 + +.. warning:: + + 目前的实现方式并不打算对物理页帧耗尽的情形做任何处理而是直接 ``panic`` 退出。因此在前面的代码中能够看到 + 很多 ``unwrap`` ,这种使用方式并不为 Rust 所推荐,只是由于简单起见暂且这样做。 + +为了方便后面的实现,我们还需要 ``PageTable`` 提供一种不经过 MMU 而是手动查页表的方法: + +.. code-block:: rust + :linenos: + + // os/src/mm/page_table.rs + + impl PageTable { + /// Temporarily used to get arguments from user space. + pub fn from_token(satp: usize) -> Self { + Self { + root_ppn: PhysPageNum::from(satp & ((1usize << 44) - 1)), + frames: Vec::new(), + } + } + fn find_pte(&self, vpn: VirtPageNum) -> Option<&PageTableEntry> { + let idxs = vpn.indexes(); + let mut ppn = self.root_ppn; + let mut result: Option<&PageTableEntry> = None; + for i in 0..3 { + let pte = &ppn.get_pte_array()[idxs[i]]; + if i == 2 { + result = Some(pte); + break; + } + if !pte.is_valid() { + return None; + } + ppn = pte.ppn(); + } + result + } + pub fn translate(&self, vpn: VirtPageNum) -> Option { + self.find_pte(vpn) + .map(|pte| {pte.clone()}) + } + } + +- 第 5 行的 ``from_token`` 可以临时创建一个专用来手动查页表的 ``PageTable`` ,它仅有一个从传入的 ``satp`` token + 中得到的多级页表根节点的物理页号,它的 ``frames`` 字段为空,也即不实际控制任何资源; +- 第 11 行的 ``find_pte`` 和之前的 ``find_pte_create`` 不同之处在于它不会试图分配物理页帧。一旦在多级页表上遍历 + 遇到空指针它就会直接返回 ``None`` 表示无法正确找到传入的虚拟页号对应的页表项; +- 第 28 行的 ``translate`` 调用 ``find_pte`` 来实现,如果能够找到页表项,那么它会将页表项拷贝一份并返回,否则就 + 返回一个 ``None`` 。 + +.. chyyuu 没有提到from_token的作用??? \ No newline at end of file diff --git a/source/chapter4/5kernel-app-spaces.rst b/source/chapter4/5kernel-app-spaces.rst new file mode 100644 index 0000000000000000000000000000000000000000..47dca2f7a1bab80adb8ed49636b340239c03c24a --- /dev/null +++ b/source/chapter4/5kernel-app-spaces.rst @@ -0,0 +1,589 @@ +内核与应用的地址空间 +================================================ + + +本节导读 +-------------------------- + + + + +页表 ``PageTable`` 只能以页为单位帮助我们维护一个虚拟内存到物理内存的地址转换关系,它本身对于计算机系统的整个虚拟/物理内存空间并没有一个全局的描述和掌控。操作系统通过不同页表的管理,来完成对不同应用和操作系统自身所在的虚拟内存,以及虚拟内存与物理内存映射关系的全面管理。这种管理是建立在地址空间的抽象上的。本节 +我们就在内核中通过基于页表的各种数据结构实现地址空间的抽象,并介绍内核和应用的虚拟和物理地址空间中各需要包含哪些内容。 + +实现地址空间抽象 +------------------------------------------ + + +逻辑段:一段连续地址的虚拟内存 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +我们以逻辑段 ``MapArea`` 为单位描述一段连续地址的虚拟内存。所谓逻辑段,就是指地址区间中的一段实际可用(即 MMU 通过查多级页表 +可以正确完成地址转换)的地址连续的虚拟地址区间,该区间内包含的所有虚拟页面都以一种相同的方式映射到物理页帧,具有可读/可写/可执行等属性。 + +.. code-block:: rust + + // os/src/mm/memory_set.rs + + pub struct MapArea { + vpn_range: VPNRange, + data_frames: BTreeMap, + map_type: MapType, + map_perm: MapPermission, + } + +其中 ``VPNRange`` 描述一段虚拟页号的连续区间,表示该逻辑段在地址区间中的位置和长度。它是一个迭代器,可以使用 Rust +的语法糖 for-loop 进行迭代。有兴趣的读者可以参考 ``os/src/mm/address.rs`` 中它的实现。 + +.. note:: + + **Rust 语法卡片:迭代器 Iterator** + + Rust编程的迭代器模式允许你对一个序列的项进行某些处理。迭代器(iterator)是负责遍历序列中的每一项和决定序列何时结束的控制逻辑。对于如何使用迭代器处理元素序列和如何实现 Iterator trait 来创建自定义迭代器的内容,可以参考 `Rust 程序设计语言-中文版第十三章第二节 `_ + +``MapType`` 描述该逻辑段内的所有虚拟页面映射到物理页帧的同一种方式,它是一个枚举类型,在内核当前的实现中支持两种方式: + +.. code-block:: rust + + // os/src/mm/memory_set.rs + + #[derive(Copy, Clone, PartialEq, Debug)] + pub enum MapType { + Identical, + Framed, + } + +其中 ``Identical`` 表示之前也有提到的恒等映射,用于在启用多级页表之后仍能够访问一个特定的物理地址指向的物理内存;而 +``Framed`` 则表示对于每个虚拟页面都需要映射到一个新分配的物理页帧。 + +当逻辑段采用 ``MapType::Framed`` 方式映射到物理内存的时候, ``data_frames`` 是一个保存了该逻辑段内的每个虚拟页面 +和它被映射到的物理页帧 ``FrameTracker`` 的一个键值对容器 ``BTreeMap`` 中,这些物理页帧被用来存放实际内存数据而不是 +作为多级页表中的中间节点。和之前的 ``PageTable`` 一样,这也用到了 RAII 的思想,将这些物理页帧的生命周期绑定到它所在的逻辑段 +``MapArea`` 下,当逻辑段被回收之后这些之前分配的物理页帧也会自动地同时被回收。 + +``MapPermission`` 表示控制该逻辑段的访问方式,它是页表项标志位 ``PTEFlags`` 的一个子集,仅保留 U/R/W/X +四个标志位,因为其他的标志位仅与硬件的地址转换机制细节相关,这样的设计能避免引入错误的标志位。 + +.. code-block:: rust + + // os/src/mm/memory_set.rs + + bitflags! { + pub struct MapPermission: u8 { + const R = 1 << 1; + const W = 1 << 2; + const X = 1 << 3; + const U = 1 << 4; + } + } + + + +地址空间:一系列有关联的逻辑段 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +地址空间是一系列有关联的逻辑段,这种关联一般是指这些逻辑段属于一个运行的程序(目前把一个运行的程序称为任务,后续会称为进程)。用来表明正在运行的应用所在执行环境中的可访问内存空间,在这个内存空间中,包含了一系列的不一定连续的逻辑段。这样我们就有任务的地址空间,内核的地址空间等说法了。地址空间使用 ``MemorySet`` 类型来表示: + +.. code-block:: rust + + // os/src/mm/memory_set.rs + + pub struct MemorySet { + page_table: PageTable, + areas: Vec, + } + +它包含了该地址空间的多级页表 ``page_table`` 和一个逻辑段 ``MapArea`` 的向量 ``areas`` 。注意 ``PageTable`` 下 +挂着所有多级页表的节点所在的物理页帧,而每个 ``MapArea`` 下则挂着对应逻辑段中的数据所在的物理页帧,这两部分 +合在一起构成了一个地址空间所需的所有物理页帧。这同样是一种 RAII 风格,当一个地址空间 ``MemorySet`` 生命周期结束后, +这些物理页帧都会被回收。 + +地址空间 ``MemorySet`` 的方法如下: + +.. code-block:: rust + :linenos: + + // os/src/mm/memory_set.rs + + impl MemorySet { + pub fn new_bare() -> Self { + Self { + page_table: PageTable::new(), + areas: Vec::new(), + } + } + fn push(&mut self, mut map_area: MapArea, data: Option<&[u8]>) { + map_area.map(&mut self.page_table); + if let Some(data) = data { + map_area.copy_data(&mut self.page_table, data); + } + self.areas.push(map_area); + } + /// Assume that no conflicts. + pub fn insert_framed_area( + &mut self, + start_va: VirtAddr, end_va: VirtAddr, permission: MapPermission + ) { + self.push(MapArea::new( + start_va, + end_va, + MapType::Framed, + permission, + ), None); + } + pub fn new_kernel() -> Self; + /// Include sections in elf and trampoline and TrapContext and user stack, + /// also returns user_sp and entry point. + pub fn from_elf(elf_data: &[u8]) -> (Self, usize, usize); + } + +- 第 4 行, ``new_bare`` 方法可以新建一个空的地址空间; +- 第 10 行, ``push`` 方法可以在当前地址空间插入一个新的逻辑段 ``map_area`` ,如果它是以 ``Framed`` 方式映射到 + 物理内存,还可以可选地在那些被映射到的物理页帧上写入一些初始化数据 ``data`` ; +- 第 18 行, ``insert_framed_area`` 方法调用 ``push`` ,可以在当前地址空间插入一个 ``Framed`` 方式映射到 + 物理内存的逻辑段。注意该方法的调用者要保证同一地址空间内的任意两个逻辑段不能存在交集,从后面即将分别介绍的内核和 + 应用的地址空间布局可以看出这一要求得到了保证; +- 第 29 行, ``new_kernel`` 可以生成内核的地址空间,而第 32 行的 ``from_elf`` 则可以应用的 ELF 格式可执行文件 + 解析出各数据段并对应生成应用的地址空间。它们的实现我们将在后面讨论。 + +在实现 ``push`` 方法在地址空间中插入一个逻辑段 ``MapArea`` 的时候,需要同时维护地址空间的多级页表 ``page_table`` +记录的虚拟页号到页表项的映射关系,也需要用到这个映射关系来找到向哪些物理页帧上拷贝初始数据。这用到了 ``MapArea`` +提供的另外几个方法: + +.. code-block:: rust + :linenos: + + // os/src/mm/memory_set.rs + + impl MapArea { + pub fn new( + start_va: VirtAddr, + end_va: VirtAddr, + map_type: MapType, + map_perm: MapPermission + ) -> Self { + let start_vpn: VirtPageNum = start_va.floor(); + let end_vpn: VirtPageNum = end_va.ceil(); + Self { + vpn_range: VPNRange::new(start_vpn, end_vpn), + data_frames: BTreeMap::new(), + map_type, + map_perm, + } + } + pub fn map(&mut self, page_table: &mut PageTable) { + for vpn in self.vpn_range { + self.map_one(page_table, vpn); + } + } + pub fn unmap(&mut self, page_table: &mut PageTable) { + for vpn in self.vpn_range { + self.unmap_one(page_table, vpn); + } + } + /// data: start-aligned but maybe with shorter length + /// assume that all frames were cleared before + pub fn copy_data(&mut self, page_table: &mut PageTable, data: &[u8]) { + assert_eq!(self.map_type, MapType::Framed); + let mut start: usize = 0; + let mut current_vpn = self.vpn_range.get_start(); + let len = data.len(); + loop { + let src = &data[start..len.min(start + PAGE_SIZE)]; + let dst = &mut page_table + .translate(current_vpn) + .unwrap() + .ppn() + .get_bytes_array()[..src.len()]; + dst.copy_from_slice(src); + start += PAGE_SIZE; + if start >= len { + break; + } + current_vpn.step(); + } + } + } + +- 第 4 行的 ``new`` 方法可以新建一个逻辑段结构体,注意传入的起始/终止虚拟地址会分别被下取整/上取整为虚拟页号并传入 + 迭代器 ``vpn_range`` 中; +- 第 19 行的 ``map`` 和第 24 行的 ``unmap`` 可以将当前逻辑段到物理内存的映射从传入的该逻辑段所属的地址空间的 + 多级页表中加入或删除。可以看到它们的实现是遍历逻辑段中的所有虚拟页面,并以每个虚拟页面为单位依次在多级页表中进行 + 键值对的插入或删除,分别对应 ``MapArea`` 的 ``map_one`` 和 ``unmap_one`` 方法,我们后面将介绍它们的实现; +- 第 31 行的 ``copy_data`` 方法将切片 ``data`` 中的数据拷贝到当前逻辑段实际被内核放置在的各物理页帧上,从而 + 在地址空间中通过该逻辑段就能访问这些数据。调用它的时候需要满足:切片 ``data`` 中的数据大小不超过当前逻辑段的 + 总大小,且切片中的数据会被对齐到逻辑段的开头,然后逐页拷贝到实际的物理页帧。 + + 从第 36 行开始的循环会遍历每一个需要拷贝数据的虚拟页面,在数据拷贝完成后会在第 48 行通过调用 ``step`` 方法,该 + 方法来自于 ``os/src/mm/address.rs`` 中为 ``VirtPageNum`` 实现的 ``StepOne`` Trait,感兴趣的读者可以阅读 + 代码确认其实现。 + + 每个页面的数据拷贝需要确定源 ``src`` 和目标 ``dst`` 两个切片并直接使用 ``copy_from_slice`` 完成复制。当确定 + 目标切片 ``dst`` 的时候,第 ``39`` 行从传入的当前逻辑段所属的地址空间的多级页表中手动查找迭代到的虚拟页号被映射 + 到的物理页帧,并通过 ``get_bytes_array`` 方法获取能够真正改写该物理页帧上内容的字节数组型可变引用,最后再获取它 + 的切片用于数据拷贝。 + +接下来介绍对逻辑段中的单个虚拟页面进行映射/解映射的方法 ``map_one`` 和 ``unmap_one`` 。显然它们的实现取决于当前 +逻辑段被映射到物理内存的方式: + +.. code-block:: rust + :linenos: + + // os/src/mm/memory_set.rs + + impl MemoryArea { + pub fn map_one(&mut self, page_table: &mut PageTable, vpn: VirtPageNum) { + let ppn: PhysPageNum; + match self.map_type { + MapType::Identical => { + ppn = PhysPageNum(vpn.0); + } + MapType::Framed => { + let frame = frame_alloc().unwrap(); + ppn = frame.ppn; + self.data_frames.insert(vpn, frame); + } + } + let pte_flags = PTEFlags::from_bits(self.map_perm.bits).unwrap(); + page_table.map(vpn, ppn, pte_flags); + } + pub fn unmap_one(&mut self, page_table: &mut PageTable, vpn: VirtPageNum) { + match self.map_type { + MapType::Framed => { + self.data_frames.remove(&vpn); + } + _ => {} + } + page_table.unmap(vpn); + } + } + +- 对于第 4 行的 ``map_one`` 来说,在虚拟页号 ``vpn`` 已经确定的情况下,它需要知道要将一个怎么样的页表项插入多级页表。 + 页表项的标志位来源于当前逻辑段的类型为 ``MapPermission`` 的统一配置,只需将其转换为 ``PTEFlags`` ;而页表项的 + 物理页号则取决于当前逻辑段映射到物理内存的方式: + + - 当以恒等映射 ``Identical`` 方式映射的时候,物理页号就等于虚拟页号; + - 当以 ``Framed`` 方式映射的时候,需要分配一个物理页帧让当前的虚拟页面可以映射过去,此时页表项中的物理页号自然就是 + 这个被分配的物理页帧的物理页号。此时还需要将这个物理页帧挂在逻辑段的 ``data_frames`` 字段下。 + + 当确定了页表项的标志位和物理页号之后,即可调用多级页表 ``PageTable`` 的 ``map`` 接口来插入键值对。 +- 对于第 19 行的 ``unmap_one`` 来说,基本上就是调用 ``PageTable`` 的 ``unmap`` 接口删除以传入的虚拟页号为键的 + 键值对即可。然而,当以 ``Framed`` 映射的时候,不要忘记同时将虚拟页面被映射到的物理页帧 ``FrameTracker`` 从 + ``data_frames`` 中移除,这样这个物理页帧才能立即被回收以备后续分配。 + +内核地址空间 +------------------------------------------ + +.. _term-isolation: + +在本章之前,内核和应用代码的访存地址都被视为一个物理地址直接访问物理内存,而在分页模式开启之后,它们都需要通过 MMU 的 +地址转换变成物理地址再交给 CPU 的访存单元去访问物理内存。地址空间抽象的重要意义在于 **隔离** (Isolation) ,当我们 +在执行每个应用的代码的时候,内核需要控制 MMU 使用这个应用地址空间的多级页表进行地址转换。由于每个应用地址空间在创建 +的时候也顺带设置好了多级页表使得只有那些存放了它的数据的物理页帧能够通过该多级页表被映射到,这样它就只能访问自己的数据 +而无法触及其他应用或是内核的数据。 + +.. _term-trampoline: + +启用分页模式下,内核代码的访存地址也会被视为一个虚拟地址并需要经过 MMU 的地址转换,因此我们也需要为内核对应构造一个 +地址空间,它除了仍然需要允许内核的各数据段能够被正常访问之后,还需要包含所有应用的内核栈以及一个 +**跳板** (Trampoline) 。我们会在本章的最后一节再深入介绍跳板的机制。 + +下图是软件看到的 64 位地址空间在 SV39 分页模式下实际可能通过 MMU 检查的最高 :math:`256\text{GiB}` (之前在 +:ref:`这里 ` 中解释过最高和最低 :math:`256\text{GiB}` 的问题): + +.. image:: kernel-as-high.png + :name: kernel-as-high + :align: center + :height: 400 + +可以看到,跳板放在最高的一个虚拟页面中。接下来则是从高到低放置每个应用的内核栈,内核栈的大小由 ``config`` 子模块的 +``KERNEL_STACK_SIZE`` 给出。它们的映射方式为 ``MapPermission`` 中的 rw 两个标志位,意味着这个逻辑段仅允许 +CPU 处于内核态访问,且只能读或写。 + +.. _term-guard-page: + +注意相邻两个内核栈之间会预留一个 **保护页面** (Guard Page) ,它是内核地址空间中的空洞,多级页表中并不存在与它相关的映射。 +它的意义在于当内核栈空间不足(如调用层数过多或死递归)的时候,代码会尝试访问 +空洞区域内的虚拟地址,然而它无法在多级页表中找到映射,便会触发异常,此时控制权会交给 trap handler 对这种情况进行 +处理。由于编译器会对访存顺序和局部变量在栈帧中的位置进行优化,我们难以确定一个已经溢出的栈帧中的哪些位置会先被访问, +但总的来说,空洞区域被设置的越大,我们就能越早捕获到这一错误并避免它覆盖其他重要数据。由于我们的内核非常简单且内核栈 +的大小设置比较宽裕,在当前的设计中我们仅将空洞区域的大小设置为单个页面。 + +下面则给出了内核地址空间的低 :math:`256\text{GiB}` 的布局: + +.. image:: kernel-as-low.png + :align: center + :height: 400 + +四个逻辑段 ``.text/.rodata/.data/.bss`` 被恒等映射到物理内存,这使得我们在无需调整内核内存布局 ``os/src/linker.ld`` +的情况下就仍能和启用页表机制之前那样访问内核的各数据段。注意我们借用页表机制对这些逻辑段的访问方式做出了限制,这都是为了 +在硬件的帮助下能够尽可能发现内核中的 bug ,在这里: + +- 四个逻辑段的 U 标志位均未被设置,使得 CPU 只能在处于 S 特权级(或以上)时访问它们; +- 代码段 ``.text`` 不允许被修改; +- 只读数据段 ``.rodata`` 不允许被修改,也不允许从它上面取指; +- ``.data/.bss`` 均允许被读写,但是不允许从它上面取指。 + +此外, :ref:`之前 ` 提到过内核地址空间中需要存在一个恒等映射到内核数据段之外的可用物理 +页帧的逻辑段,这样才能在启用页表机制之后,内核仍能以纯软件的方式读写这些物理页帧。它们的标志位仅包含 rw ,意味着该 +逻辑段只能在 S 特权级以上访问,并且只能读写。 + +下面我们给出创建内核地址空间的方法 ``new_kernel`` : + +.. code-block:: rust + :linenos: + + // os/src/mm/memory_set.rs + + extern "C" { + fn stext(); + fn etext(); + fn srodata(); + fn erodata(); + fn sdata(); + fn edata(); + fn sbss_with_stack(); + fn ebss(); + fn ekernel(); + fn strampoline(); + } + + impl MemorySet { + /// Without kernel stacks. + pub fn new_kernel() -> Self { + let mut memory_set = Self::new_bare(); + // map trampoline + memory_set.map_trampoline(); + // map kernel sections + println!(".text [{:#x}, {:#x})", stext as usize, etext as usize); + println!(".rodata [{:#x}, {:#x})", srodata as usize, erodata as usize); + println!(".data [{:#x}, {:#x})", sdata as usize, edata as usize); + println!(".bss [{:#x}, {:#x})", sbss_with_stack as usize, ebss as usize); + println!("mapping .text section"); + memory_set.push(MapArea::new( + (stext as usize).into(), + (etext as usize).into(), + MapType::Identical, + MapPermission::R | MapPermission::X, + ), None); + println!("mapping .rodata section"); + memory_set.push(MapArea::new( + (srodata as usize).into(), + (erodata as usize).into(), + MapType::Identical, + MapPermission::R, + ), None); + println!("mapping .data section"); + memory_set.push(MapArea::new( + (sdata as usize).into(), + (edata as usize).into(), + MapType::Identical, + MapPermission::R | MapPermission::W, + ), None); + println!("mapping .bss section"); + memory_set.push(MapArea::new( + (sbss_with_stack as usize).into(), + (ebss as usize).into(), + MapType::Identical, + MapPermission::R | MapPermission::W, + ), None); + println!("mapping physical memory"); + memory_set.push(MapArea::new( + (ekernel as usize).into(), + MEMORY_END.into(), + MapType::Identical, + MapPermission::R | MapPermission::W, + ), None); + memory_set + } + } + +``new_kernel`` 将映射跳板和地址空间中最低 :math:`256\text{GiB}` 中的所有的逻辑段。第 3 行开始,我们从 +``os/src/linker.ld`` 中引用了很多表示了各个段位置的符号,而后在 ``new_kernel`` 中,我们从低地址到高地址 +依次创建 5 个逻辑段并通过 ``push`` 方法将它们插入到内核地址空间中,上面我们已经详细介绍过这 5 个逻辑段。跳板 +是通过 ``map_trampoline`` 方法来映射的,我们也将在本章最后一节进行讲解。 + +应用地址空间 +------------------------------------------ + +现在我们来介绍如何创建应用的地址空间。在前面的章节中,我们直接将丢弃所有符号的应用二进制镜像链接到内核,在初始化的时候 +内核仅需将他们加载到正确的初始物理地址就能使它们正确执行。但本章中,我们希望效仿内核地址空间的设计,同样借助页表机制 +使得应用地址空间的各个逻辑段也可以有不同的访问方式限制,这样可以提早检测出应用的错误并及时将其终止以最小化它对系统带来的 +恶劣影响。 + +在第三章中,每个应用链接脚本中的起始地址被要求是不同的,这样它们的代码和数据存放的位置才不会产生冲突。但是这是一种对于应用开发者 +极其不友好的设计。现在,借助地址空间的抽象,我们终于可以让所有应用程序都使用同样的起始地址,这也意味着所有应用可以使用同一个链接脚本了: + +.. code-block:: + :linenos: + + /* user/src/linker.ld */ + + OUTPUT_ARCH(riscv) + ENTRY(_start) + + BASE_ADDRESS = 0x0; + + SECTIONS + { + . = BASE_ADDRESS; + .text : { + *(.text.entry) + *(.text .text.*) + } + . = ALIGN(4K); + .rodata : { + *(.rodata .rodata.*) + } + . = ALIGN(4K); + .data : { + *(.data .data.*) + } + .bss : { + *(.bss .bss.*) + } + /DISCARD/ : { + *(.eh_frame) + *(.debug*) + } + } + +我们将起始地址 ``BASE_ADDRESS`` 设置为 :math:`\text{0x0}` ,显然它只能是一个地址空间中的虚拟地址而非物理地址。 +事实上由于我们将入口汇编代码段放在最低的地方,这也是整个应用的入口点。 +我们只需清楚这一事实即可,而无需像之前一样将其硬编码到代码中。此外,在 ``.text`` 和 ``.rodata`` 中间以及 ``.rodata`` 和 +``.data`` 中间我们进行了页面对齐,因为前后两个逻辑段的访问方式限制是不同的,由于我们只能以页为单位对这个限制进行设置, +因此就只能将下一个逻辑段对齐到下一个页面开始放置。相对的, ``.data`` 和 ``.bss`` 两个逻辑段由于限制相同,它们中间 +则无需进行页面对齐。 + +下图展示了应用地址空间的布局: + +.. image:: app-as-full.png + :align: center + :height: 400 + +左侧给出了应用地址空间最低 :math:`256\text{GiB}` 的布局:从 :math:`\text{0x0}` 开始向高地址放置应用内存布局中的 +各个逻辑段,最后放置带有一个保护页面的用户栈。这些逻辑段都是以 ``Framed`` 方式映射到物理内存的,从访问方式上来说都加上 +了 U 标志位代表 CPU 可以在 U 特权级也就是执行应用代码的时候访问它们。右侧则给出了最高的 :math:`256\text{GiB}` , +可以看出它只是和内核地址空间一样将跳板放置在最高页,还将 Trap 上下文放置在次高页中。这两个虚拟页面虽然位于应用地址空间, +但是它们并不包含 U 标志位,事实上它们在地址空间切换的时候才会发挥作用,请同样参考本章的最后一节。 + +在 ``os/src/build.rs`` 中,我们不再将丢弃了所有符号的应用二进制镜像链接进内核,而是直接使用 ELF 格式的可执行文件, +因为在前者中内存布局中各个逻辑段的位置和访问限制等信息都被裁剪掉了。而 ``loader`` 子模块也变得极其精简: + +.. code-block:: rust + + // os/src/loader.rs + + pub fn get_num_app() -> usize { + extern "C" { fn _num_app(); } + unsafe { (_num_app as usize as *const usize).read_volatile() } + } + + pub fn get_app_data(app_id: usize) -> &'static [u8] { + extern "C" { fn _num_app(); } + let num_app_ptr = _num_app as usize as *const usize; + let num_app = get_num_app(); + let app_start = unsafe { + core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1) + }; + assert!(app_id < num_app); + unsafe { + core::slice::from_raw_parts( + app_start[app_id] as *const u8, + app_start[app_id + 1] - app_start[app_id] + ) + } + } + +它仅需要提供两个函数: ``get_num_app`` 获取链接到内核内的应用的数目,而 ``get_app_data`` 则根据传入的应用编号 +取出对应应用的 ELF 格式可执行文件数据。它们和之前一样仍是基于 ``build.rs`` 生成的 ``link_app.S`` 给出的符号来 +确定其位置,并实际放在内核的数据段中。 +``loader`` 模块中原有的内核和用户栈则分别作为逻辑段放在内核和用户地址空间中,我们无需再去专门为其定义一种类型。 + +在创建应用地址空间的时候,我们需要对 ``get_app_data`` 得到的 ELF 格式数据进行解析,找到各个逻辑段所在位置和访问 +限制并插入进来,最终得到一个完整的应用地址空间: + +.. code-block:: rust + :linenos: + + // os/src/mm/memory_set.rs + + impl MemorySet { + /// Include sections in elf and trampoline and TrapContext and user stack, + /// also returns user_sp and entry point. + pub fn from_elf(elf_data: &[u8]) -> (Self, usize, usize) { + let mut memory_set = Self::new_bare(); + // map trampoline + memory_set.map_trampoline(); + // map program headers of elf, with U flag + let elf = xmas_elf::ElfFile::new(elf_data).unwrap(); + let elf_header = elf.header; + let magic = elf_header.pt1.magic; + assert_eq!(magic, [0x7f, 0x45, 0x4c, 0x46], "invalid elf!"); + let ph_count = elf_header.pt2.ph_count(); + let mut max_end_vpn = VirtPageNum(0); + for i in 0..ph_count { + let ph = elf.program_header(i).unwrap(); + if ph.get_type().unwrap() == xmas_elf::program::Type::Load { + let start_va: VirtAddr = (ph.virtual_addr() as usize).into(); + let end_va: VirtAddr = ((ph.virtual_addr() + ph.mem_size()) as usize).into(); + let mut map_perm = MapPermission::U; + let ph_flags = ph.flags(); + if ph_flags.is_read() { map_perm |= MapPermission::R; } + if ph_flags.is_write() { map_perm |= MapPermission::W; } + if ph_flags.is_execute() { map_perm |= MapPermission::X; } + let map_area = MapArea::new( + start_va, + end_va, + MapType::Framed, + map_perm, + ); + max_end_vpn = map_area.vpn_range.get_end(); + memory_set.push( + map_area, + Some(&elf.input[ph.offset() as usize..(ph.offset() + ph.file_size()) as usize]) + ); + } + } + // map user stack with U flags + let max_end_va: VirtAddr = max_end_vpn.into(); + let mut user_stack_bottom: usize = max_end_va.into(); + // guard page + user_stack_bottom += PAGE_SIZE; + let user_stack_top = user_stack_bottom + USER_STACK_SIZE; + memory_set.push(MapArea::new( + user_stack_bottom.into(), + user_stack_top.into(), + MapType::Framed, + MapPermission::R | MapPermission::W | MapPermission::U, + ), None); + // map TrapContext + memory_set.push(MapArea::new( + TRAP_CONTEXT.into(), + TRAMPOLINE.into(), + MapType::Framed, + MapPermission::R | MapPermission::W, + ), None); + (memory_set, user_stack_top, elf.header.pt2.entry_point() as usize) + } + } + +- 第 9 行,我们将跳板插入到应用地址空间; +- 第 11 行,我们使用外部 crate ``xmas_elf`` 来解析传入的应用 ELF 数据并可以轻松取出各个部分。 + :ref:`此前 ` 我们简要介绍过 ELF 格式的布局。第 14 行,我们取出 ELF 的魔数来判断 + 它是不是一个合法的 ELF 。 + + 第 15 行,我们可以直接得到 program header 的数目,然后遍历所有的 program header 并将合适的区域加入 + 到应用地址空间中。这一过程的主体在第 17~39 行之间。第 19 行我们确认 program header 的类型是 ``LOAD`` , + 这表明它有被内核加载的必要,此时不必理会其他类型的 program header 。接着通过 ``ph.virtual_addr()`` 和 + ``ph.mem_size()`` 来计算这一区域在应用地址空间中的位置,通过 ``ph.flags()`` 来确认这一区域访问方式的 + 限制并将其转换为 ``MapPermission`` 类型(注意它默认包含 U 标志位)。最后我们在第 27 行创建逻辑段 + ``map_area`` 并在第 34 行 ``push`` 到应用地址空间。在 ``push`` 的时候我们需要完成数据拷贝,当前 + program header 数据被存放的位置可以通过 ``ph.offset()`` 和 ``ph.file_size()`` 来找到。 注意当 + 存在一部分零初始化的时候, ``ph.file_size()`` 将会小于 ``ph.mem_size()`` ,因为这些零出于缩减可执行 + 文件大小的原因不应该实际出现在 ELF 数据中。 +- 我们从第 40 行开始处理用户栈。注意在前面加载各个 program header 的时候,我们就已经维护了 ``max_end_vpn`` + 记录目前涉及到的最大的虚拟页号,只需紧接着在它上面再放置一个保护页面和用户栈即可。 +- 第 53 行则在应用地址空间中映射次高页面来存放 Trap 上下文。 +- 第 59 行返回的时候,我们不仅返回应用地址空间 ``memory_set`` ,也同时返回用户栈虚拟地址 ``user_stack_top`` + 以及从解析 ELF 得到的该应用入口点地址,它们将被我们用来创建应用的任务控制块。 \ No newline at end of file diff --git a/source/chapter4/6multitasking-based-on-as.rst b/source/chapter4/6multitasking-based-on-as.rst new file mode 100644 index 0000000000000000000000000000000000000000..55cbd78192ad9731a54156cc88efea843a4fdbcc --- /dev/null +++ b/source/chapter4/6multitasking-based-on-as.rst @@ -0,0 +1,796 @@ +基于地址空间的分时多任务 +============================================================== + + +本节导读 +-------------------------- + + + + +本节我们介绍如何基于地址空间抽象而不是对于物理内存的直接访问来实现第三章的分时多任务系统。这样,我们的应用编写会更加方便,与操作系统的关联也松耦合一些,操作系统自身的安全性也得到了加强。 + +建立并开启基于分页模式的虚拟地址空间 +-------------------------------------------- + +当 SBI 实现(本项目中基于 RustSBI)初始化完成后, CPU 将跳转到内核入口点并在 S 特权级上执行,此时还并没有开启分页模式 +,内核的每一次访存仍被视为一个物理地址直接访问物理内存。而在开启分页模式之后,内核的代码在访存的时候只能看到内核地址空间, +此时每次访存将被视为一个虚拟地址且需要通过 MMU 基于内核地址空间的多级页表的地址转换。这两种模式之间的过渡在内核初始化期间 +完成。 + +创建内核地址空间 +^^^^^^^^^^^^^^^^^^^^^^^^ + + +我们创建内核地址空间的全局实例: + +.. code-block:: rust + + // os/src/mm/memory_set.rs + + lazy_static! { + pub static ref KERNEL_SPACE: Arc> = Arc::new(Mutex::new( + MemorySet::new_kernel() + )); + } + +从之前对于 ``lazy_static!`` 宏的介绍可知, ``KERNEL_SPACE`` 在运行期间它第一次被用到时才会实际进行初始化,而它所 +占据的空间则是编译期被放在全局数据段中。这里使用经典的 ``Arc>`` 组合是因为我们既需要 ``Arc`` 提供的共享 +引用,也需要 ``Mutex`` 提供的互斥访问。在多核环境下才能体现出它的全部能力,目前在单核环境下主要是为了通过编译器检查。 + +在 ``rust_main`` 函数中,我们首先调用 ``mm::init`` 进行内存管理子系统的初始化: + +.. code-block:: rust + + // os/src/mm/mod.rs + + pub use memory_set::KERNEL_SPACE; + + pub fn init() { + heap_allocator::init_heap(); + frame_allocator::init_frame_allocator(); + KERNEL_SPACE.lock().activate(); + } + +可以看到,我们最先进行了全局动态内存分配器的初始化,因为接下来马上就要用到 Rust 的堆数据结构。接下来我们初始化物理页帧 +管理器(内含堆数据结构 ``Vec`` )使能可用物理页帧的分配和回收能力。最后我们创建内核地址空间并让 CPU 开启分页模式, +MMU 在地址转换的时候使用内核的多级页表,这一切均在一行之内做到: + +- 首先,我们引用 ``KERNEL_SPACE`` ,这是它第一次被使用,就在此时它会被初始化,调用 ``MemorySet::new_kernel`` + 创建一个内核地址空间并使用 ``Arc>`` 包裹起来; +- 接着使用 ``.lock()`` 获取一个可变引用 ``&mut MemorySet`` 。需要注意的是这里发生了两次隐式类型转换: + + 1. 我们知道 + ``lock`` 是 ``Mutex`` 的方法而不是 ``Arc`` 的方法,由于 ``Arc`` 实现了 ``Deref`` Trait ,当 + ``lock`` 需要一个 ``&Mutex`` 类型的参数的时候,编译器会自动将传入的 ``&Arc>`` 转换为 + ``&Mutex`` 这样就实现了类型匹配; + 2. 事实上 ``Mutex::lock`` 返回的是一个 ``MutexGuard<'a, T>`` ,这同样是 + RAII 的思想,当这个类型生命周期结束后互斥锁就会被释放。而该类型实现了 ``DerefMut`` Trait,因此当一个函数接受类型 + 为 ``&mut T`` 的参数却被传入一个类型为 ``&mut MutexGuard<'a, T>`` 的参数的时候,编译器会自动进行类型转换使 + 参数匹配。 +- 最后,我们调用 ``MemorySet::activate`` : + + .. code-block:: rust + :linenos: + + // os/src/mm/page_table.rs + + pub fn token(&self) -> usize { + 8usize << 60 | self.root_ppn.0 + } + + // os/src/mm/memory_set.rs + + impl MemorySet { + pub fn activate(&self) { + let satp = self.page_table.token(); + unsafe { + satp::write(satp); + llvm_asm!("sfence.vma" :::: "volatile"); + } + } + } + + ``PageTable::token`` 会按照 :ref:`satp CSR 格式要求 ` 构造一个无符号 64 位无符号整数,使得其 + 分页模式为 SV39 ,且将当前多级页表的根节点所在的物理页号填充进去。在 ``activate`` 中,我们将这个值写入当前 CPU 的 + satp CSR ,从这一刻开始 SV39 分页模式就被启用了,而且 MMU 会使用内核地址空间的多级页表进行地址转换。 + + 我们必须注意切换 satp CSR 是否是一个 *平滑* 的过渡:其含义是指,切换 satp 的指令及其下一条指令这两条相邻的指令的 + 虚拟地址是相邻的(由于切换 satp 的指令并不是一条跳转指令, pc 只是简单的自增当前指令的字长), + 而它们所在的物理地址一般情况下也是相邻的,但是它们所经过的地址转换流程却是不同的——切换 satp 导致 MMU 查的多级页表 + 是不同的。这就要求前后两个地址空间在切换 satp 的指令 *附近* 的映射满足某种意义上的连续性。 + + 幸运的是,我们做到了这一点。这条写入 satp 的指令及其下一条指令都在内核内存布局的代码段中,在切换之后是一个恒等映射, + 而在切换之前是视为物理地址直接取指,也可以将其看成一个恒等映射。这完全符合我们的期待:即使切换了地址空间,指令仍应该 + 能够被连续的执行。 + +注意到在 ``activate`` 的最后,我们插入了一条汇编指令 ``sfence.vma`` ,它又起到什么作用呢? + +让我们再来回顾一下多级页表:它相比线性表虽然大量节约了内存占用,但是却需要 MMU 进行更多的隐式访存。如果是一个线性表, +MMU 仅需单次访存就能找到页表项并完成地址转换,而多级页表(以 SV39 为例,不考虑大页)最顺利的情况下也需要三次访存。这些 +额外的访存和真正访问数据的那些访存在空间上并不相邻,加大了多级缓存的压力,一旦缓存缺失将带来巨大的性能惩罚。如果采用 +多级页表实现,这个问题会变得更为严重,使得地址空间抽象的性能开销过大。 + +.. _term-tlb: + +为了解决性能问题,一种常见的做法是在 CPU 中利用部分硬件资源额外加入一个 **快表** +(TLB, Translation Lookaside Buffer) , 它维护了部分虚拟页号到页表项的键值对。当 MMU 进行地址转换的时候,首先 +会到快表中看看是否匹配,如果匹配的话直接取出页表项完成地址转换而无需访存;否则再去查页表并将键值对保存在快表中。一旦 +我们修改了 satp 切换了地址空间,快表中的键值对就会失效,因为它还表示着上个地址空间的映射关系。为了 MMU 的地址转换 +能够及时与 satp 的修改同步,我们可以选择立即使用 ``sfence.vma`` 指令将快表清空,这样 MMU 就不会看到快表中已经 +过期的键值对了。 + +.. note:: + + **sfence.vma 是一个屏障** + + 对于一种仅含有快表的 RISC-V CPU 实现来说,我们可以认为 ``sfence.vma`` 的作用就是清空快表。事实上它在特权级 + 规范中被定义为一种含义更加丰富的内存屏障,具体来说: ``sfence.vma`` 可以使得所有发生在它后面的地址转换都能够 + 看到所有排在它前面的写入操作,在不同的平台上这条指令要做的事情也都是不同的。这条指令还可以被精细配置来减少同步开销, + 详情请参考 RISC-V 特权级规范。 + + +检查内核地址空间的多级页表设置 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +调用 ``mm::init`` 之后我们就使能了内核动态内存分配、物理页帧管理,还启用了分页模式进入了内核地址空间。之后我们可以 +通过 ``mm::remap_test`` 来检查内核地址空间的多级页表是否被正确设置: + +.. code-block:: rust + + // os/src/mm/memory_set.rs + + pub fn remap_test() { + let mut kernel_space = KERNEL_SPACE.lock(); + let mid_text: VirtAddr = ((stext as usize + etext as usize) / 2).into(); + let mid_rodata: VirtAddr = ((srodata as usize + erodata as usize) / 2).into(); + let mid_data: VirtAddr = ((sdata as usize + edata as usize) / 2).into(); + assert_eq!( + kernel_space.page_table.translate(mid_text.floor()).unwrap().writable(), + false + ); + assert_eq!( + kernel_space.page_table.translate(mid_rodata.floor()).unwrap().writable(), + false, + ); + assert_eq!( + kernel_space.page_table.translate(mid_data.floor()).unwrap().executable(), + false, + ); + println!("remap_test passed!"); + } + +其中分别通过手动查内核多级页表的方式验证代码段和只读数据段不允许被写入,同时不允许从数据段上取指。 + +.. _term-trampoline: + +跳板的实现 +------------------------------------ + +上一小节我们看到无论是内核还是应用的地址空间,最高的虚拟页面都是一个跳板。同时应用地址空间的次高虚拟页面还被设置为用来 +存放应用的 Trap 上下文。那么跳板究竟起什么作用呢?为何不直接把 Trap 上下文仍放到应用的内核栈中呢? + +回忆曾在第二章介绍过的 :ref:`Trap 上下文保存与恢复 ` 。当一个应用 Trap 到内核的时候, +``sscratch`` 已经指出了该应用内核栈的栈顶,我们用一条指令即可从用户栈切换到内核栈,然后直接将 Trap 上下文压入内核栈 +栈顶。当 Trap 处理完毕返回用户态的时候,将 Trap 上下文中的内容恢复到寄存器上,最后将保存着应用用户栈顶的 ``sscratch`` +与 sp 进行交换,也就从内核栈切换回了用户栈。在这个过程中, ``sscratch`` 起到了非常关键的作用,它使得我们可以在不破坏 +任何通用寄存器的情况下完成用户栈和内核栈顶的 Trap 上下文这两个工作区域之间的切换。 + +然而,一旦使能了分页机制,一切就并没有这么简单了,我们必须在这个过程中同时完成地址空间的切换。 +具体来说,当 ``__alltraps`` 保存 Trap 上下文的时候,我们必须通过修改 satp 从应用地址空间切换到内核地址空间, +因为 trap handler 只有在内核地址空间中才能访问; +同理,在 ``__restore`` 恢复 Trap 上下文的时候,我们也必须从内核地址空间切换回应用地址空间,因为应用的代码和 +数据只能在它自己的地址空间中才能访问,内核地址空间是看不到的。 +进而,地址空间的切换不能影响指令的连续执行,这就要求应用和内核地址空间在切换地址空间指令附近是平滑的。 + +.. _term-meltdown: + +.. note:: + + **内核与应用地址空间的隔离** + + 目前我们的设计是有一个唯一的内核地址空间存放内核的代码、数据,同时对于每个应用维护一个它们自己的地址空间,因此在 + Trap 的时候就需要进行地址空间切换,而在任务切换的时候无需进行(因为这个过程全程在内核内完成)。而教程前两版以及 + :math:`\mu` core 中的设计是每个应用都有一个地址空间,可以将其中的逻辑段分为内核和用户两部分,分别映射到内核和 + 用户的数据和代码,且分别在 CPU 处于 S/U 特权级时访问。此设计中并不存在一个单独的内核地址空间。 + + 之前设计方式的优点在于: Trap 的时候无需切换地址空间,而在任务切换的时候才需要切换地址空间。由于后者比前者更容易 + 实现,这降低了实现的复杂度。而且在应用高频进行系统调用的时候能够避免地址空间切换的开销,这通常源于快表或 cache + 的失效问题。但是这种设计方式也有缺点:即内核的逻辑段需要在每个应用的地址空间内都映射一次,这会带来一些无法忽略的 + 内存占用开销,并显著限制了嵌入式平台(如我们所采用的 K210 )的任务并发数。此外,这种做法无法应对处理器的 `熔断 + (Meltdown) 漏洞 `_ ,使得恶意应用能够以某种方式看到它本来无权访问的地址空间中内核部分的数据。将内核与地址空间隔离 + 便是修复此漏洞的一种方法。 + + 经过权衡,在本教程中我们参考 MIT 的教学 OS `xv6 `_ , + 采用内核和应用地址空间隔离的设计。 + +我们为何将应用的 Trap 上下文放到应用地址空间的次高页面而不是内核地址空间中的内核栈中呢?原因在于,假如我们将其放在内核栈 +中,在保存 Trap 上下文之前我们必须先切换到内核地址空间,这就需要我们将内核地址空间的 token 写入 satp 寄存器,之后我们 +还需要有一个通用寄存器保存内核栈栈顶的位置,这样才能以它为基址保存 Trap 上下文。在保存 Trap 上下文之前我们必须完成这 +两项工作。然而,我们无法在不破坏任何一个通用寄存器的情况下做到这一点。因为事实上我们需要用到内核的两条信息:内核地址空间 +的 token 还有应用内核栈顶的位置,硬件却只提供一个 ``sscratch`` 可以用来进行周转。所以,我们不得不将 Trap 上下文保存在 +应用地址空间的一个虚拟页面中以避免切换到内核地址空间才能保存。 + +为了方便实现,我们在 Trap 上下文中包含更多内容(和我们关于上下文的定义有些不同,它们在初始化之后便只会被读取而不会被写入 +,并不是每次都需要保存/恢复): + +.. code-block:: rust + :linenos: + :emphasize-lines: 8,9,10 + + // os/src/trap/context.rs + + #[repr(C)] + pub struct TrapContext { + pub x: [usize; 32], + pub sstatus: Sstatus, + pub sepc: usize, + pub kernel_satp: usize, + pub kernel_sp: usize, + pub trap_handler: usize, + } + +在多出的三个字段中: + +- ``kernel_satp`` 表示内核地址空间的 token ; +- ``kernel_sp`` 表示当前应用在内核地址空间中的内核栈栈顶的虚拟地址; +- ``trap_handler`` 表示内核中 trap handler 入口点的虚拟地址。 + +它们在应用初始化的时候由内核写入应用地址空间中的 TrapContext 的相应位置,此后就不再被修改。 + +让我们来看一下现在的 ``__alltraps`` 和 ``__restore`` 各是如何在保存和恢复 Trap 上下文的同时也切换地址空间的: + +.. code-block:: riscv + :linenos: + + # os/src/trap/trap.S + + .section .text.trampoline + .globl __alltraps + .globl __restore + .align 2 + __alltraps: + csrrw sp, sscratch, sp + # now sp->*TrapContext in user space, sscratch->user stack + # save other general purpose registers + sd x1, 1*8(sp) + # skip sp(x2), we will save it later + sd x3, 3*8(sp) + # skip tp(x4), application does not use it + # save x5~x31 + .set n, 5 + .rept 27 + SAVE_GP %n + .set n, n+1 + .endr + # we can use t0/t1/t2 freely, because they have been saved in TrapContext + csrr t0, sstatus + csrr t1, sepc + sd t0, 32*8(sp) + sd t1, 33*8(sp) + # read user stack from sscratch and save it in TrapContext + csrr t2, sscratch + sd t2, 2*8(sp) + # load kernel_satp into t0 + ld t0, 34*8(sp) + # load trap_handler into t1 + ld t1, 36*8(sp) + # move to kernel_sp + ld sp, 35*8(sp) + # switch to kernel space + csrw satp, t0 + sfence.vma + # jump to trap_handler + jr t1 + + __restore: + # a0: *TrapContext in user space(Constant); a1: user space token + # switch to user space + csrw satp, a1 + sfence.vma + csrw sscratch, a0 + mv sp, a0 + # now sp points to TrapContext in user space, start restoring based on it + # restore sstatus/sepc + ld t0, 32*8(sp) + ld t1, 33*8(sp) + csrw sstatus, t0 + csrw sepc, t1 + # restore general purpose registers except x0/sp/tp + ld x1, 1*8(sp) + ld x3, 3*8(sp) + .set n, 5 + .rept 27 + LOAD_GP %n + .set n, n+1 + .endr + # back to user stack + ld sp, 2*8(sp) + sret + +- 当应用 Trap 进入内核的时候,硬件会设置一些 CSR 并在 S 特权级下跳转到 ``__alltraps`` 保存 Trap 上下文。此时 + sp 寄存器仍指向用户栈,但 ``sscratch`` 则被设置为指向应用地址空间中存放 Trap 上下文的位置,实际在次高页面。 + 随后,就像之前一样,我们 ``csrrw`` 交换 sp 和 ``sscratch`` ,并基于指向 Trap 上下文位置的 sp 开始保存通用 + 寄存器和一些 CSR ,这个过程在第 28 行结束。到这里,我们就全程在内核地址空间中完成了保存 Trap 上下文的工作。 + +- 接下来该考虑切换到内核地址空间并跳转到 trap handler 了。第 30 行我们将内核地址空间的 token 载入到 t0 寄存器中, + 第 32 行我们将 trap handler 入口点的虚拟地址载入到 t1 寄存器中,第 34 行我们直接将 sp 修改为应用内核栈顶的地址。 + 这三条信息均是内核在初始化该应用的时候就已经设置好的。第 36~37 行我们将 satp 修改为内核地址空间的 token 并使用 + ``sfence.vma`` 刷新快表,这就切换到了内核地址空间。最后在第 39 行我们通过 ``jr`` 指令跳转到 t1 寄存器所保存的 + trap handler 入口点的地址。注意这里我们不能像之前的章节那样直接 ``call trap_handler`` ,原因稍后解释。 +- 当内核将 Trap 处理完毕准备返回用户态的时候会 *调用* ``__restore`` ,它有两个参数:第一个是 Trap 上下文在应用 + 地址空间中的位置,这个对于所有的应用来说都是相同的,由调用规范在 a0 寄存器中传递;第二个则是即将回到的应用的地址空间 + 的 token ,在 a1 寄存器中传递。由于 Trap 上下文是保存在应用地址空间中的,第 44~45 行我们先切换回应用地址空间。第 + 46 行我们将传入的 Trap 上下文位置保存在 ``sscratch`` 寄存器中,这样 ``__alltraps`` 中才能基于它将 Trap 上下文 + 保存到正确的位置。第 47 行我们将 sp 修改为 Trap 上下文的位置,后面基于它恢复各通用寄存器和 CSR。最后在第 64 行, + 我们通过 ``sret`` 指令返回用户态。 + +接下来还需要考虑切换地址空间前后指令能否仍能连续执行。可以看到我们将 ``trap.S`` 中的整段汇编代码放置在 +``.text.trampoline`` 段,并在调整内存布局的时候将它对齐到代码段的一个页面中: + +.. code-block:: diff + :linenos: + + # os/src/linker.ld + + stext = .; + .text : { + *(.text.entry) + + . = ALIGN(4K); + + strampoline = .; + + *(.text.trampoline); + + . = ALIGN(4K); + *(.text .text.*) + } + +这样,这段汇编代码放在一个物理页帧中,且 ``__alltraps`` 恰好位于这个物理页帧的开头,其物理地址被外部符号 +``strampoline`` 标记。在开启分页模式之后,内核和应用代码都只能看到各自的虚拟地址空间,而在它们的视角中,这段汇编代码 +被放在它们地址空间的最高虚拟页面上,由于这段汇编代码在执行的时候涉及到地址空间切换,故而被称为跳板页面。 + +那么在产生trap前后的一小段时间内会有一个比较 **极端** 的情况,即刚产生trap时,CPU已经进入了内核态(即Supervisor Mode),但此时执行代码和访问数据还是在应用程序所处的用户态虚拟地址空间中,而不是我们通常理解的内核虚拟地址空间。在这段特殊的时间内,CPU指令 +为什么能够被连续执行呢?这里需要注意:无论是内核还是应用的地址空间,跳板的虚拟页均位于同样位置,且它们也将会映射到同一个实际存放这段 +汇编代码的物理页帧。也就是说,在执行 ``__alltraps`` 或 ``__restore`` 函数进行地址空间切换的时候,应用的用户态虚拟地址空间和操作系统内核的内核态虚拟地址空间对切换地址空间的指令所在页的映射方式均是相同的,这就说明了这段切换地址空间的指令控制流仍是可以连续执行的。 + +现在可以说明我们在创建用户/内核地址空间中用到的 ``map_trampoline`` 是如何实现的了: + +.. code-block:: rust + :linenos: + + // os/src/config.rs + + pub const TRAMPOLINE: usize = usize::MAX - PAGE_SIZE + 1; + + // os/src/mm/memory_set.rs + + impl MemorySet { + /// Mention that trampoline is not collected by areas. + fn map_trampoline(&mut self) { + self.page_table.map( + VirtAddr::from(TRAMPOLINE).into(), + PhysAddr::from(strampoline as usize).into(), + PTEFlags::R | PTEFlags::X, + ); + } + } + +这里我们为了实现方便并没有新增逻辑段 ``MemoryArea`` 而是直接在多级页表中插入一个从地址空间的最高虚拟页面映射到 +跳板汇编代码所在的物理页帧的键值对,访问方式限制与代码段相同,即 RX 。 + +最后可以解释为何我们在 ``__alltraps`` 中需要借助寄存器 ``jr`` 而不能直接 ``call trap_handler`` 了。因为在 +内存布局中,这条 ``.text.trampoline`` 段中的跳转指令和 ``trap_handler`` 都在代码段之内,汇编器(Assembler)和链接器(Linker)会根据 ``linker-qemu/k210.ld`` 的地址布局描述,设定电子指令的地址,并计算二者地址偏移量 +并让跳转指令的实际效果为当前 pc 自增这个偏移量。但实际上我们知道由于我们设计的缘故,这条跳转指令在被执行的时候, +它的虚拟地址被操作系统内核设置在地址空间中的最高页面之内,加上这个偏移量并不能正确的得到 ``trap_handler`` 的入口地址。 + +**问题的本质可以概括为:跳转指令实际被执行时的虚拟地址和在编译器/汇编器/链接器进行后端代码生成和链接形成最终机器码时设置此指令的地址是不同的。** + +加载和执行应用程序 +------------------------------------ + +扩展任务控制块 +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +为了让应用在运行时有一个安全隔离且符合编译器给应用设定的地址空间布局的虚拟地址空间,操作系统需要对任务进行更多的管理,所以任务控制块相比第三章也包含了更多内容: + +.. code-block:: rust + :linenos: + :emphasize-lines: 6,7,8 + + // os/src/task/task.rs + + pub struct TaskControlBlock { + pub task_cx_ptr: usize, + pub task_status: TaskStatus, + pub memory_set: MemorySet, + pub trap_cx_ppn: PhysPageNum, + pub base_size: usize, + } + +除了应用的地址空间 ``memory_set`` 之外,还有位于应用地址空间次高页的 Trap 上下文被实际存放在物理页帧的物理页号 +``trap_cx_ppn`` ,它能够方便我们对于 Trap 上下文进行访问。此外, ``base_size`` 统计了应用数据的大小,也就是 +在应用地址空间中从 :math:`\text{0x0}` 开始到用户栈结束一共包含多少字节。它后续还应该包含用于应用动态内存分配的 +堆空间的大小,但我们暂不支持。 + + + +更新对任务控制块的管理 +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +下面是任务控制块的创建: + +.. code-block:: rust + :linenos: + + // os/src/config.rs + + /// Return (bottom, top) of a kernel stack in kernel space. + pub fn kernel_stack_position(app_id: usize) -> (usize, usize) { + let top = TRAMPOLINE - app_id * (KERNEL_STACK_SIZE + PAGE_SIZE); + let bottom = top - KERNEL_STACK_SIZE; + (bottom, top) + } + + // os/src/task/task.rs + + impl TaskControlBlock { + pub fn new(elf_data: &[u8], app_id: usize) -> Self { + // memory_set with elf program headers/trampoline/trap context/user stack + let (memory_set, user_sp, entry_point) = MemorySet::from_elf(elf_data); + let trap_cx_ppn = memory_set + .translate(VirtAddr::from(TRAP_CONTEXT).into()) + .unwrap() + .ppn(); + let task_status = TaskStatus::Ready; + // map a kernel-stack in kernel space + let (kernel_stack_bottom, kernel_stack_top) = kernel_stack_position(app_id); + KERNEL_SPACE + .lock() + .insert_framed_area( + kernel_stack_bottom.into(), + kernel_stack_top.into(), + MapPermission::R | MapPermission::W, + ); + let task_cx_ptr = (kernel_stack_top - core::mem::size_of::()) + as *mut TaskContext; + unsafe { *task_cx_ptr = TaskContext::goto_trap_return(); } + let task_control_block = Self { + task_cx_ptr: task_cx_ptr as usize, + task_status, + memory_set, + trap_cx_ppn, + base_size: user_sp, + }; + // prepare TrapContext in user space + let trap_cx = task_control_block.get_trap_cx(); + *trap_cx = TrapContext::app_init_context( + entry_point, + user_sp, + KERNEL_SPACE.lock().token(), + kernel_stack_top, + trap_handler as usize, + ); + task_control_block + } + } + +- 第 15 行,我们解析传入的 ELF 格式数据构造应用的地址空间 ``memory_set`` 并获得其他信息; +- 第 16 行,我们从地址空间 ``memory_set`` 中查多级页表找到应用地址空间中的 Trap 上下文实际被放在哪个物理页帧; +- 第 22 行,我们根据传入的应用 ID ``app_id`` 调用在 ``config`` 子模块中定义的 ``kernel_stack_position`` 找到 + 应用的内核栈预计放在内核地址空间 ``KERNEL_SPACE`` 中的哪个位置,并通过 ``insert_framed_area`` 实际将这个逻辑段 + 加入到内核地址空间中; + +.. _trap-return-intro: + +- 第 30~32 行,我们在应用的内核栈顶压入一个跳转到 ``trap_return`` 而不是 ``__restore`` 的任务上下文,这主要是为了能够支持对该应用的启动并顺利切换到用户地址空间执行。在构造方式上,只是将 ra 寄存器的值设置为 ``trap_return`` 的地址。 ``trap_return`` 是我们后面要介绍的 + 新版的 Trap 处理的一部分。 + + 这里我们对裸指针解引用成立的原因在于:我们之前已经进入了内核地址空间,而我们要操作的内核栈也是在内核地址空间中的; +- 第 33 行开始我们用上面的信息来创建任务控制块实例 ``task_control_block``; +- 第 41 行我们需要初始化该应用的 Trap 上下文,由于它是在应用地址空间而不是在内核地址空间中,我们只能手动查页表找到 + Trap 上下文实际被放在的物理页帧,然后通过之前介绍的 :ref:`在内核地址空间读写特定物理页帧的能力 ` + 获得在用户空间的 Trap 上下文的可变引用用于初始化: + + .. code-block:: rust + + // os/src/task/task.rs + + impl TaskControlBlock { + pub fn get_trap_cx(&self) -> &'static mut TrapContext { + self.trap_cx_ppn.get_mut() + } + } + + 此处需要说明的是,返回 ``'static`` 的可变引用和之前一样可以看成一个绕过 unsafe 的裸指针;而 ``PhysPageNum::get_mut`` + 是一个泛型函数,由于我们已经声明了总体返回 ``TrapContext`` 的可变引用,则Rust编译器会给 ``get_mut`` 泛型函数针对具体类型 ``TrapContext`` + 的情况生成一个特定版本的 ``get_mut`` 函数实现。在 ``get_trap_cx`` 函数中则会静态调用``get_mut`` 泛型函数的特定版本实现。 +- 第 42 行我们正式通过 Trap 上下文的可变引用来对其进行初始化: + + .. code-block:: rust + :linenos: + :emphasize-lines: 8,9,10,18,19,20 + + // os/src/trap/context.rs + + impl TrapContext { + pub fn set_sp(&mut self, sp: usize) { self.x[2] = sp; } + pub fn app_init_context( + entry: usize, + sp: usize, + kernel_satp: usize, + kernel_sp: usize, + trap_handler: usize, + ) -> Self { + let mut sstatus = sstatus::read(); + sstatus.set_spp(SPP::User); + let mut cx = Self { + x: [0; 32], + sstatus, + sepc: entry, + kernel_satp, + kernel_sp, + trap_handler, + }; + cx.set_sp(sp); + cx + } + } + + 和之前相比 ``TrapContext::app_init_context`` 需要补充上让应用在 ``__alltraps`` 能够顺利进入到内核地址空间 + 并跳转到 trap handler 入口点的相关信息。 + +在内核初始化的时候,需要将所有的应用加载到全局应用管理器中: + +.. code-block:: rust + :linenos: + + // os/src/task/mod.rs + + struct TaskManagerInner { + tasks: Vec, + current_task: usize, + } + + lazy_static! { + pub static ref TASK_MANAGER: TaskManager = { + println!("init TASK_MANAGER"); + let num_app = get_num_app(); + println!("num_app = {}", num_app); + let mut tasks: Vec = Vec::new(); + for i in 0..num_app { + tasks.push(TaskControlBlock::new( + get_app_data(i), + i, + )); + } + TaskManager { + num_app, + inner: RefCell::new(TaskManagerInner { + tasks, + current_task: 0, + }), + } + }; + } + +可以看到,在 ``TaskManagerInner`` 中我们使用向量 ``Vec`` 来保存任务控制块。在全局任务管理器 ``TASK_MANAGER`` +初始化的时候,只需使用 ``loader`` 子模块提供的 ``get_num_app`` 和 ``get_app_data`` 分别获取链接到内核的应用 +数量和每个应用的 ELF 文件格式的数据,然后依次给每个应用创建任务控制块并加入到向量中即可。我们还将 ``current_task`` 设置 +为 0 ,于是将从第 0 个应用开始执行。 + +回过头来介绍一下应用构建器 ``os/build.rs`` 的改动: + +- 首先,我们在 ``.incbin`` 中不再插入清除全部符号的应用二进制镜像 ``*.bin`` ,而是将构建得到的 ELF 格式文件直接链接进来; +- 其次,在链接每个 ELF 格式文件之前我们都加入一行 ``.align 3`` 来确保它们对齐到 8 字节,这是由于如果不这样做, ``xmas-elf`` crate 可能会在解析 ELF 的时候进行不对齐的内存读写,例如使用 ``ld`` 指令从内存的一个没有对齐到 8 字节的地址加载一个 64 位的值到一个通用寄存器。而在 k210 平台上,由于其硬件限制,这会触发一个内存读写不对齐的异常,导致解析无法正常完成。 + +为了方便后续的实现,全局任务管理器还需要提供关于当前应用与地址空间有关的一些信息: + +.. code-block:: rust + :linenos: + + // os/src/task/mod.rs + + impl TaskManager { + fn get_current_token(&self) -> usize { + let inner = self.inner.borrow(); + let current = inner.current_task; + inner.tasks[current].get_user_token() + } + + fn get_current_trap_cx(&self) -> &mut TrapContext { + let inner = self.inner.borrow(); + let current = inner.current_task; + inner.tasks[current].get_trap_cx() + } + } + + pub fn current_user_token() -> usize { + TASK_MANAGER.get_current_token() + } + + pub fn current_trap_cx() -> &'static mut TrapContext { + TASK_MANAGER.get_current_trap_cx() + } + +通过 ``current_user_token`` 和 ``current_trap_cx`` 分别可以获得当前正在执行的应用的地址空间的 token 和可以在 +内核地址空间中修改位于该应用地址空间中的 Trap 上下文的可变引用。 + +改进 Trap 处理的实现 +------------------------------------ + +为了能够支持地址空间,让我们来看现在 ``trap_handler`` 的改进实现: + +.. code-block:: rust + :linenos: + + // os/src/trap/mod.rs + + fn set_kernel_trap_entry() { + unsafe { + stvec::write(trap_from_kernel as usize, TrapMode::Direct); + } + } + + #[no_mangle] + pub fn trap_from_kernel() -> ! { + panic!("a trap from kernel!"); + } + + #[no_mangle] + pub fn trap_handler() -> ! { + set_kernel_trap_entry(); + let cx = current_trap_cx(); + let scause = scause::read(); + let stval = stval::read(); + match scause.cause() { + ... + } + trap_return(); + } + +由于应用的 Trap 上下文不在内核地址空间,因此我们调用 ``current_trap_cx`` 来获取当前应用的 Trap 上下文的可变引用 +而不是像之前那样作为参数传入 ``trap_handler`` 。至于 Trap 处理的过程则没有发生什么变化。 + +注意到,在 ``trap_handler`` 的开头还调用 ``set_kernel_trap_entry`` 将 ``stvec`` 修改为同模块下另一个函数 +``trap_from_kernel`` 的地址。这就是说,一旦进入内核后再次触发到 S 的 Trap,则会在硬件设置一些 CSR 之后跳过寄存器 +的保存过程直接跳转到 ``trap_from_kernel`` 函数,在这里我们直接 ``panic`` 退出。这是因为内核和应用的地址空间分离 +之后,从 U 还是从 S Trap 到 S 的 Trap 上下文保存与恢复实现方式和 Trap 处理逻辑有很大差别,我们不得不实现两遍而 +不太可能将二者整合起来。这里简单起见我们弱化了从 S 到 S 的 Trap ,省略了 Trap 上下文保存过程而直接 ``panic`` 。 + +在 ``trap_handler`` 完成 Trap 处理之后,我们需要调用 ``trap_return`` 返回用户态: + +.. code-block:: rust + :linenos: + + // os/src/trap/mod.rs + + fn set_user_trap_entry() { + unsafe { + stvec::write(TRAMPOLINE as usize, TrapMode::Direct); + } + } + + #[no_mangle] + pub fn trap_return() -> ! { + set_user_trap_entry(); + let trap_cx_ptr = TRAP_CONTEXT; + let user_satp = current_user_token(); + extern "C" { + fn __alltraps(); + fn __restore(); + } + let restore_va = __restore as usize - __alltraps as usize + TRAMPOLINE; + unsafe { + llvm_asm!("fence.i" :::: "volatile"); + llvm_asm!("jr $0" + :: "r"(restore_va), "{a0}"(trap_cx_ptr), "{a1}"(user_satp) + :: "volatile" + ); + } + panic!("Unreachable in back_to_user!"); + } + +- 第 11 行,在 ``trap_return`` 的开头我们调用 ``set_user_trap_entry`` 来让应用 Trap 到 S 的时候可以跳转到 + ``__alltraps`` 。注意我们把 ``stvec`` 设置为内核和应用地址空间共享的跳板页面的起始地址 ``TRAMPOLINE`` 而不是 + 编译器在链接时看到的 ``__alltraps`` 的地址,因为启用分页模式之后我们只能通过跳板页面上的虚拟地址来实际取得 + ``__alltraps`` 和 ``__restore`` 的汇编代码。 +- 之前介绍的时候提到过 ``__restore`` 需要两个参数:分别是 Trap 上下文在应用地址空间中的虚拟地址和要继续执行的应用 + 地址空间的 token 。第 12 和第 13 行则分别准备好这两个参数。 +- 最后我们需要跳转到 ``__restore`` 切换到应用地址空间从 Trap 上下文中恢复通用寄存器并 ``sret`` 继续执行应用。它的 + 关键在于如何找到 ``__restore`` 在内核/应用地址空间中共同的虚拟地址。第 18 行我们展示了计算它的过程:由于 + ``__alltraps`` 是对齐到地址空间跳板页面的起始地址 ``TRAMPOLINE`` 上的, 则 ``__restore`` 的虚拟地址只需在 + ``TRAMPOLINE`` 基础上加上 ``__restore`` 相对于 ``__alltraps`` 的偏移量即可。这里 ``__alltraps`` 和 + ``__restore`` 都是指编译器在链接时看到的内核内存布局中的地址。在第 21 行我们使用 ``jr`` 指令完成了跳转的任务。 +- 在开始执行应用之前,第 20 行我们需要使用 ``fence.i`` 指令清空指令缓存 i-cache 。这是因为,在内核中进行的一些操作 + 可能导致一些原先存放某个应用代码的物理页帧如今用来存放数据或者是其他应用的代码,i-cache 中可能还保存着该物理页帧的 + 错误快照。因此我们直接将整个 i-cache 清空避免错误。 + +当每个应用第一次获得 CPU 使用权即将进入用户态执行的时候,它的内核栈顶放置着我们在 +:ref:`内核加载应用的时候 ` 构造的一个任务上下文: + +.. code-block:: rust + + // os/src/task/context.rs + + impl TaskContext { + pub fn goto_trap_return() -> Self { + Self { + ra: trap_return as usize, + s: [0; 12], + } + } + } + +在 ``__switch`` 切换到它的时候,这将会跳转到 ``trap_return`` 并第一次返回用户态。 + +改进 sys_write 的实现 +------------------------------------ + +同样由于内核和应用地址空间的隔离, ``sys_write`` 不再能够直接访问位于应用空间中的数据,而需要手动查页表才能知道那些 +数据被放置在哪些物理页帧上并进行访问。 + +为此,页表模块 ``page_table`` 提供了将应用地址空间中一个缓冲区转化为在内核空间中能够直接访问的形式的辅助函数: + +.. code-block:: rust + :linenos: + + // os/src/mm/page_table.rs + + pub fn translated_byte_buffer( + token: usize, + ptr: *const u8, + len: usize + ) -> Vec<&'static [u8]> { + let page_table = PageTable::from_token(token); + let mut start = ptr as usize; + let end = start + len; + let mut v = Vec::new(); + while start < end { + let start_va = VirtAddr::from(start); + let mut vpn = start_va.floor(); + let ppn = page_table + .translate(vpn) + .unwrap() + .ppn(); + vpn.step(); + let mut end_va: VirtAddr = vpn.into(); + end_va = end_va.min(VirtAddr::from(end)); + v.push(&ppn.get_bytes_array()[start_va.page_offset()..end_va.page_offset()]); + start = end_va.into(); + } + v + } + +参数中的 ``token`` 是某个应用地址空间的 token , ``ptr`` 和 ``len`` 则分别表示该地址空间中的一段缓冲区的起始地址 +和长度。 ``translated_byte_buffer`` 会以向量的形式返回一组可以在内核空间中直接访问的字节数组切片,具体实现在这里 +不再赘述。 + +进而,我们完成对 ``sys_write`` 系统调用的改造: + +.. code-block:: rust + + // os/src/syscall/fs.rs + + pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize { + match fd { + FD_STDOUT => { + let buffers = translated_byte_buffer(current_user_token(), buf, len); + for buffer in buffers { + print!("{}", core::str::from_utf8(buffer).unwrap()); + } + len as isize + }, + _ => { + panic!("Unsupported fd in sys_write!"); + } + } + } + +我们尝试将每个字节数组切片转化为字符串 ``&str`` 然后输出即可。 + + + +小结 +------------------------------------- + +这一章内容很多,讲解了 **地址空间** 这一抽象概念是如何在一个具体的“头甲龙”操作系统中实现的。这里面的核心内容是如何建立基于页表机制的虚拟地址空间。为此,操作系统需要知道并管理整个系统中的物理内存;需要建立虚拟地址到物理地址映射关系的页表;并基于页表给操作系统自身和每个应用提供一个虚拟地址空间;并需要对管理应用的任务控制块进行扩展,确保能对应用的地址空间进行管理;由于应用和内核的地址空间是隔离的,需要有一个跳板来帮助完成应用与内核之间的切换执行;并导致了对异常、中断、系统调用的相应更改。这一系列的改进,最终的效果是编写应用更加简单了,且应用的执行或错误不会影响到内核和其他应用的正常工作。为了得到这些好处,我们需要比较费劲地进化我们的操作系统。如果同学结合阅读代码,编译并运行应用+内核,读懂了上面的文档,那完成本章的实验就有了一个坚实的基础。 + +如果同学能想明白如何插入/删除页表;如何在 ``trap_handler`` 下处理 ``LoadPageFault`` ;以及 ``sys_get_time`` 在使能页机制下如何实现,那就会发现下一节的实验练习也许 **就和lab1一样** 。 diff --git a/source/chapter4/7exercise.rst b/source/chapter4/7exercise.rst new file mode 100644 index 0000000000000000000000000000000000000000..6659d1e316148c7cf7dc2070689bd39e9af9b452 --- /dev/null +++ b/source/chapter4/7exercise.rst @@ -0,0 +1,115 @@ +chapter4练习 +============================================ + +- 本节难度: **看懂代码就和lab1一样** + +编程作业 +--------------------------------------------- + +申请内存 +++++++++++++++++++++++++++++++++++++++++++++ + +你有没有想过,当你在 C 语言中写下的 ``new int[100];`` 执行时可能会发生哪些事情?你可能已经发现,目前我们给用户程序的内存都是固定的并没有增长的能力,这些程序是不能执行 ``new`` 这类导致内存使用增加的操作。libc 中通过 `sbrk `_ 系统调用增加进程可使用的堆空间,这也是本来的题目设计,但是一位热心的往年助教J学长表示:这一点也不酷!他推荐了另一个申请内存的系统调用。 + +`mmap `_ 本身主要使用来在内存中映射文件的,这里我们简化它的功能,仅仅用来提供申请内存的功能。 + +mmap 系统调用新定义: + +- syscall ID:222 +- C接口: ``int mmap(void* start, unsigned long long len, int port)`` +- Rust接口: ``fn mmap(start: usize, len: usize, port: usize) -> i32`` +- 功能:申请长度为 len 字节的物理内存(不要求实际物理内存位置,可以随便找一块),并映射到 addr 开始的虚存,内存页属性为 port。 +- 参数: + - start:需要映射的虚存起始地址。 + - len:映射字节长度,可以为 0 (如果是则直接返回),不可过大(上限 1GiB )。 + - port:第 0 位表示是否可读,第 1 位表示是否可写,第 2 位表示是否可执行。其他位无效(必须为 0 )。 +- 说明: + - 正确时返回实际 map size(为 4096 的倍数),错误返回 -1 。 + - 为了简单,addr 要求按页对齐(否则报错),len 可直接按页上取整。 + - 为了简单,不考虑分配失败时的页回收(也就是内存泄漏)。 +- 错误: + - [addr, addr + len) 存在已经被映射的页。 + - 物理内存不足。 + - port & !0x7 != 0 (port 其余位必须为0)。 + - port & 0x7 = 0 (这样的内存无意义)。 + +munmap 系统调用新定义: + +- syscall ID:215 +- C接口: ``int munmap(void* start, unsigned long long len)`` +- Rust接口: ``fn munmap(start: usize, len: usize) -> i32`` +- 功能:取消一块虚存的映射。 +- 参数:同 mmap +- 说明: + - 为了简单,参数错误时不考虑内存的恢复和回收。 +- 错误: + - [start, start + len) 中存在未被映射的虚存。 + +实验要求 +++++++++++++++++++++++++++++++++++++++++++ + +- 实现分支:ch4。 +- 完成实验指导书中的内容,实现虚拟内存,可以运行过去几个lab的程序。 +- 更新 sys_write 的范围检查,改为基于页表的检查方法。 +- 实现 mmap 和 munmap 两个自定义系统调用,并通过 `Rust测例 `_ 中 chapter4 对应的所有测例,测例详情见对应仓库,系统调用具体要求参考 `guide.md `_ 中chapter4对应的所有测例。 + +注意:记得删除 lab3 关于程序时间片上界的规定。 + +challenge: 支持多核。 + +实验检查 ++++++++++++++++++++++++++++++++++++++++++++++ + +- 实验目录要求 + + 目录要求不变(参考lab1目录或者示例代码目录结构)。同样在 os 目录下 `make run` 之后可以正确加载用户程序并执行。 + + 加载的用户测例位置: ``../user/build/bin`` 。 + +- 检查 + + 可以正确 `make run` 执行,可以正确执行目标用户测例,并得到预期输出(详见测例注释)。 + +问答作业 +------------------------------------------------- + +1. 请列举 SV39 页表页表项的组成,结合课堂内容,描述其中的标志位有何作用/潜在作用? + +2. 缺页 + + 这次的实验没有涉及到缺页有点遗憾,主要是缺页难以测试,而且更多的是一种优化,不符合这次实验的核心理念,所以这里补两道小题。 + + 缺页指的是进程访问页面时页面不在页表中或在页表中无效的现象,此时 MMU 将会返回一个中断,告知 os 进程内存访问出了问题。os 选择填补页表并重新执行异常指令或者杀死进程。 + + - 请问哪些异常可能是缺页导致的? + - 发生缺页时,描述相关的重要寄存器的值(lab2中描述过的可以简单点)。 + + 缺页有两个常见的原因,其一是 Lazy 策略,也就是直到内存页面被访问才实际进行页表操作。比如,一个程序被执行时,进程的代码段理论上需要从磁盘加载到内存。但是 os 并不会马上这样做,而是会保存 .text 段在磁盘的位置信息,在这些代码第一次被执行时才完成从磁盘的加载操作。 + + - 这样做有哪些好处? + + 此外 COW(Copy On Write) 也是常见的容易导致缺页的 Lazy 策略,这个之后再说。其实,我们的 mmap 也可以采取 Lazy 策略,比如:一个用户进程先后申请了 10G 的内存空间,然后用了其中 1M 就直接退出了。按照现在的做法,我们显然亏大了,进行了很多没有意义的页表操作。 + + - 请问处理 10G 连续的内存页面,需要操作的页表实际大致占用多少内存(给出数量级即可)? + - 请简单思考如何才能在现有框架基础上实现 Lazy 策略,缺页时又如何处理?描述合理即可,不需要考虑实现。 + + 缺页的另一个常见原因是 swap 策略,也就是内存页面可能被换到磁盘上了,导致对应页面失效。 + + - 此时页面失效如何表现在页表项(PTE)上? + +3. 双页表与单页表 + + 为了防范侧信道攻击,我们的 os 使用了双页表。但是传统的设计一直是单页表的,也就是说,用户线程和对应的内核线程共用同一张页表,只不过内核对应的地址只允许在内核态访问。 + + - 如何更换页表? + - 单页表情况下,如何控制用户态无法访问内核页面?(tips:看看上一题最后一问) + - 单页表有何优势?(回答合理即可) + - 双页表实现下,何时需要更换页表?假设你写一个单页表操作系统,你会选择何时更换页表(回答合理即可)? + +报告要求 +-------------------------------------------------------- + +* 简单总结本次实验与上个实验相比你增加的东西。(控制在5行以内,不要贴代码) +* 完成问答问题。 +* (optional) 你对本次实验设计及难度的看法。 + \ No newline at end of file diff --git a/source/chapter4/address-translation.png b/source/chapter4/address-translation.png new file mode 100644 index 0000000000000000000000000000000000000000..949120a5e05aaacaa5032085bcd735dd69cb35a1 Binary files /dev/null and b/source/chapter4/address-translation.png differ diff --git a/source/chapter4/app-as-full.png b/source/chapter4/app-as-full.png new file mode 100644 index 0000000000000000000000000000000000000000..796c23e751c721b86c3e4ad58eb4372c6ee46113 Binary files /dev/null and b/source/chapter4/app-as-full.png differ diff --git a/source/chapter4/index.rst b/source/chapter4/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..6e07555cbe743b701ea233fa5c3197c5f4d821a6 --- /dev/null +++ b/source/chapter4/index.rst @@ -0,0 +1,14 @@ +第四章:地址空间 +============================================== + +.. toctree:: + :maxdepth: 4 + + 0intro + 1rust-dynamic-allocation + 2address-space + 3sv39-implementation-1 + 4sv39-implementation-2 + 5kernel-app-spaces + 6multitasking-based-on-as + 7exercise diff --git a/source/chapter4/kernel-as-high.png b/source/chapter4/kernel-as-high.png new file mode 100644 index 0000000000000000000000000000000000000000..344d94d92d9bbfbba00084dce256b12be6f06221 Binary files /dev/null and b/source/chapter4/kernel-as-high.png differ diff --git a/source/chapter4/kernel-as-low.png b/source/chapter4/kernel-as-low.png new file mode 100644 index 0000000000000000000000000000000000000000..8cf78a470e3dc17e243ce6d370cd3b92108a36a4 Binary files /dev/null and b/source/chapter4/kernel-as-low.png differ diff --git a/source/chapter4/linear-table.png b/source/chapter4/linear-table.png new file mode 100644 index 0000000000000000000000000000000000000000..ce3d95f3e42ec37414262b9e984d36ccdfee1402 Binary files /dev/null and b/source/chapter4/linear-table.png differ diff --git a/source/chapter4/page-table.png b/source/chapter4/page-table.png new file mode 100644 index 0000000000000000000000000000000000000000..0d3715dbf54bcca5bef0f3fb9e70493f965ec85d Binary files /dev/null and b/source/chapter4/page-table.png differ diff --git a/source/chapter4/pte-rwx.png b/source/chapter4/pte-rwx.png new file mode 100644 index 0000000000000000000000000000000000000000..e2c20df53c4233b111d6066411f8aeaa2d057649 Binary files /dev/null and b/source/chapter4/pte-rwx.png differ diff --git a/source/chapter4/rust-containers.png b/source/chapter4/rust-containers.png new file mode 100644 index 0000000000000000000000000000000000000000..239ecba10220f43b1da57f1c5f01d31362f36787 Binary files /dev/null and b/source/chapter4/rust-containers.png differ diff --git a/source/chapter4/satp.png b/source/chapter4/satp.png new file mode 100644 index 0000000000000000000000000000000000000000..33357b737a7fe4be045d3b6a085e9ee2323339f7 Binary files /dev/null and b/source/chapter4/satp.png differ diff --git a/source/chapter4/segmentation.png b/source/chapter4/segmentation.png new file mode 100644 index 0000000000000000000000000000000000000000..46bb1e6fd1d8e7d42687bc7ea99f7888a0ff91cb Binary files /dev/null and b/source/chapter4/segmentation.png differ diff --git a/source/chapter4/simple-base-bound.png b/source/chapter4/simple-base-bound.png new file mode 100755 index 0000000000000000000000000000000000000000..55cb02179a955d62819d81ce83ae97c5a274c95d Binary files /dev/null and b/source/chapter4/simple-base-bound.png differ diff --git a/source/chapter4/sv39-full.png b/source/chapter4/sv39-full.png new file mode 100644 index 0000000000000000000000000000000000000000..5678542eb3eefe7f421d84f8f5ba2ecf1e7496ec Binary files /dev/null and b/source/chapter4/sv39-full.png differ diff --git a/source/chapter4/sv39-pte.png b/source/chapter4/sv39-pte.png new file mode 100644 index 0000000000000000000000000000000000000000..7f693907429c67ad64a0f1e731fa76c7aa0348eb Binary files /dev/null and b/source/chapter4/sv39-pte.png differ diff --git a/source/chapter4/sv39-va-pa.png b/source/chapter4/sv39-va-pa.png new file mode 100644 index 0000000000000000000000000000000000000000..daf34be5435ee5a65f4ba3cd911c3a3881974498 Binary files /dev/null and b/source/chapter4/sv39-va-pa.png differ diff --git a/source/chapter4/trie-1.png b/source/chapter4/trie-1.png new file mode 100644 index 0000000000000000000000000000000000000000..29df0c12011646651ffd5da0258c4472deaf7462 Binary files /dev/null and b/source/chapter4/trie-1.png differ diff --git a/source/chapter4/trie.png b/source/chapter4/trie.png new file mode 100644 index 0000000000000000000000000000000000000000..0a6e3cf9be4031917cf918cde1ebb48954d29734 Binary files /dev/null and b/source/chapter4/trie.png differ diff --git a/source/chapter5/0intro.rst b/source/chapter5/0intro.rst new file mode 100644 index 0000000000000000000000000000000000000000..08d4f575a263d1bda6a1359b7c616ff4bbfd7129 --- /dev/null +++ b/source/chapter5/0intro.rst @@ -0,0 +1,219 @@ +引言 +=========================================== + +本章导读 +------------------------------------------- + +在正式开始这一章的介绍之前,我们很高兴告诉读者:在前面的章节中基本涵盖了一个功能相对完善的内核抽象所需的所有硬件机制,而从本章开始我们所做的主要是一些软件上的工作,这会略微轻松一些。 + +在前面的章节中,随着应用的需求逐渐变得复杂,作为其执行环境的内核也需要在硬件提供的相关机制的支持之下努力为应用提供更多强大、易用且安全的抽象。让我们先来简单回顾一下: + +- 第一章《RV64 裸机应用》中,由于我们从始至终只需运行一个应用,这时我们的内核看起来只是一个 **函数库** ,它会对应用的执行环境进行初始化,包括设置函数调用栈的位置使得应用能够正确使用内存。此外,它还将 SBI 接口函数进行了封装使得应用更容易使用这些功能。 +- 第二章《批处理系统》中,我们需要自动加载并执行一个固定序列内的多个应用,当一个应用出错或者正常退出之后则切换到下一个。为了让这个流程能够稳定进行而不至于被某个应用的错误所破坏,内核需要借助硬件提供的 **特权级机制** 将应用代码放在 U 特权级执行,并对它的行为进行限制。一旦应用出现错误或者请求一些只有内核才能提供的服务时,控制权会移交给内核并对该 **Trap** 进行处理。 +- 第三章《多道程序与分时多任务》中,出于一些对于总体性能或者交互性的要求,从 CPU 的角度看,它在执行一个应用一段时间之后,会暂停这个应用并切换出去,等到之后切换回来的时候再继续执行。其核心机制就是 **任务切换** 。对于每个应用来说,它会认为自己始终独占一个 CPU ,不过这只是内核对 CPU 资源的恰当抽象给它带来的一种幻象。 +- 第四章《地址空间》中,我们利用一种经典的抽象—— **地址空间** 来代替先前对于物理内存的直接访问。这样做使得每个应用独占一个访存空间并与其他应用隔离起来,并由内核和硬件机制保证不同应用的数据被实际存放在物理内存上的位置也不相交。于是开发者在开发应用的时候无需顾及其他应用,整个系统的安全性也得到了一定保证。 + +事实上,由于我们还没有充分发掘这些抽象的能力,应用的开发和使用仍然比较受限,且用户在应用运行过程中的灵活性和交互性不够强,这尤其体现在交互能力上。目前为止,所有的应用都是在内核初始化阶段被一并加载到内存中的,之后也无法对应用进行动态增删,从用户的角度来看这和第二章的批处理系统似乎并没有什么不同。 + +.. _term-terminal: +.. _term-command-line: + +于是,本章我们会开发一个用户 **终端** (Terminal) 或称 **命令行** 应用(Command Line Application, 俗称 **Shell** ) ,形成用户与操作系统进行交互的命令行界面(Command Line Interface),它就和我们今天常用的 OS 中的命令行应用(如 Linux中的bash,Windows中的CMD等)没有什么不同:只需在其中输入命令即可启动或杀死应用,或者监控系统的运行状况。这自然是现代 OS 中不可缺少的一部分,并大大增加了系统的 **可交互性** ,使得用户可以更加灵活地控制系统。 + +为了在用户态就可以借助操作系统的服务动态灵活地管理和控制应用的执行,我们需要在已有的 **任务** 抽象的基础上进一步扩展,形成新的抽象: **进程** ,并实现若干基于 **进程** 的强大系统调用。 + +- 创建(create):操作系统需要提供一些创建新进程的服务。用户在shell中键入命令或用鼠标双击应用程序图标(这需要GUI界面,目前我们还没有实现)时,会调用操作系统服务来创建新进程,运行指定的程序。 +- 销毁(destroy):操作系统还需提供退出并销毁进程的服务。进程会在运行完成后可自行退出,但还需要其他进程(如创建这些进程的父进程)来回收这些进程最后的资源并销毁这些进程。 +- 等待(wait):操作系统提供等待进程停止运行是很有用的,比如上面提到的退出信息的收集。 +- 信息(info):操作系统也可提供有关进程的身份和状态等进程信息,例如进程的ID,进程的运行状态,进程的优先级等。 +- 其他控制:操作系统还可有其他的进程控制服务。例如,让一个进程能够杀死另外一个进程,暂停进程(停止运行一段时间),恢复进程(继续运行)等。 + + +.. note:: + + **任务和进程的关系与区别** + + 第三章提到的 **任务** 和这里提到的 **进程** 有何关系和区别? 这需要从二者对资源的占用和执行的过程这两个方面来进行分析。 + + 任务和进程都是一个程序的执行过程,或表示了一个运行的程序;都是能够被操作系统打断并通过切换来分时占用CPU资源;都需要 **地址空间** 来放置代码和数据;都有从开始运行到结束运行这样的生命周期。 + + 第三章提到的 **任务** 是这里提到的 **进程** 的初级阶段,还没进化到拥有更强大的动态变化的功能:进程可以在运行的过程中,创建 **子进程** 、 用新的 **程序** 内容覆盖已有的 **程序** 内容、可管理更多的 物理或虚拟的 **资源** 。 + + + +实践体验 +------------------------------------------- + +获取本章代码: + +.. code-block:: console + + $ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git + $ cd rCore-Tutorial-v3 + $ git checkout ch5 + +在 qemu 模拟器上运行本章代码: + +.. code-block:: console + + $ cd os + $ make run + +将 Maix 系列开发板连接到 PC,并在上面运行本章代码: + +.. code-block:: console + + $ cd os + $ make run BOARD=k210 + +待内核初始化完毕之后,将在屏幕上打印可用的应用列表并进入用户终端(以 K210 平台为例): + +.. code-block:: + + [rustsbi] RustSBI version 0.1.1 + .______ __ __ _______.___________. _______..______ __ + | _ \ | | | | / | | / || _ \ | | + | |_) | | | | | | (----`---| |----`| (----`| |_) || | + | / | | | | \ \ | | \ \ | _ < | | + | |\ \----.| `--' |.----) | | | .----) | | |_) || | + | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| + + [rustsbi] Platform: K210 (Version 0.1.0) + [rustsbi] misa: RV64ACDFIMSU + [rustsbi] mideleg: 0x22 + [rustsbi] medeleg: 0x1ab + [rustsbi] Kernel entry: 0x80020000 + [kernel] Hello, world! + last 808 Physical Frames. + .text [0x80020000, 0x8002e000) + .rodata [0x8002e000, 0x80032000) + .data [0x80032000, 0x800c7000) + .bss [0x800c7000, 0x802d8000) + mapping .text section + mapping .rodata section + mapping .data section + mapping .bss section + mapping physical memory + remap_test passed! + after initproc! + /**** APPS **** + exit + fantastic_text + forktest + forktest2 + forktest_simple + forktree + hello_world + initproc + matrix + sleep + sleep_simple + stack_overflow + user_shell + usertests + yield + **************/ + Rust user shell + >> + +其中 ``usertests`` 打包了很多应用,只要执行它就能够自动执行一系列应用。 + +只需输入应用的名称并回车即可在系统中执行该应用。如果输入错误的话可以使用退格键 (Backspace) 。以应用 ``exit`` 为例: + +.. code-block:: + + >> exit + I am the parent. Forking the child... + I am the child. + I am parent, fork a child pid 3 + I am the parent, waiting now.. + waitpid 3 ok. + exit pass. + Shell: Process 2 exited with code 0 + >> + +当应用执行完毕后,将继续回到用户终端的命令输入模式。 + +本章代码树 +-------------------------------------- + +.. code-block:: + :linenos: + + ├── bootloader + │   ├── rustsbi-k210.bin + │   └── rustsbi-qemu.bin + ├── LICENSE + ├── os + │   ├── build.rs(修改:基于应用名的应用构建器) + │   ├── Cargo.toml + │   ├── Makefile + │   └── src + │   ├── config.rs + │   ├── console.rs + │   ├── entry.asm + │   ├── lang_items.rs + │   ├── link_app.S + │   ├── linker-k210.ld + │   ├── linker-qemu.ld + │   ├── loader.rs(修改:基于应用名的应用加载器) + │   ├── main.rs(修改) + │   ├── mm(修改:为了支持本章的系统调用对此模块做若干增强) + │   │   ├── address.rs + │   │   ├── frame_allocator.rs + │   │   ├── heap_allocator.rs + │   │   ├── memory_set.rs + │   │   ├── mod.rs + │   │   └── page_table.rs + │   ├── sbi.rs + │   ├── syscall + │   │   ├── fs.rs(修改:新增 sys_read) + │   │   ├── mod.rs(修改:新的系统调用的分发处理) + │   │   └── process.rs(修改:新增 sys_getpid/fork/exec/waitpid) + │   ├── task + │   │   ├── context.rs + │   │   ├── manager.rs(新增:任务管理器,为上一章任务管理器功能的一部分) + │   │   ├── mod.rs(修改:调整原来的接口实现以支持进程) + │   │   ├── pid.rs(新增:进程标识符和内核栈的 Rust 抽象) + │   │   ├── processor.rs(新增:处理器监视器,为上一章任务管理器功能的一部分) + │   │   ├── switch.rs + │   │   ├── switch.S + │   │   └── task.rs(修改:支持进程机制的任务控制块) + │   ├── timer.rs + │   └── trap + │   ├── context.rs + │   ├── mod.rs(修改:对于系统调用的实现进行修改以支持进程系统调用) + │   └── trap.S + ├── README.md + ├── rust-toolchain + ├── tools + │   ├── kflash.py + │   ├── LICENSE + │   ├── package.json + │   ├── README.rst + │   └── setup.py + └── user(对于用户库 user_lib 进行修改,替换了一套新的测例) + ├── Cargo.toml + ├── Makefile + └── src + ├── bin + │   ├── exit.rs + │   ├── fantastic_text.rs + │   ├── forktest2.rs + │   ├── forktest.rs + │   ├── forktest_simple.rs + │   ├── forktree.rs + │   ├── hello_world.rs + │   ├── initproc.rs + │   ├── matrix.rs + │   ├── sleep.rs + │   ├── sleep_simple.rs + │   ├── stack_overflow.rs + │   ├── user_shell.rs + │   ├── usertests.rs + │   └── yield.rs + ├── console.rs + ├── lang_items.rs + ├── lib.rs + ├── linker.ld + └── syscall.rs \ No newline at end of file diff --git a/source/chapter5/1process.rst b/source/chapter5/1process.rst new file mode 100644 index 0000000000000000000000000000000000000000..0ca690cc7d4b808096ac871012645fd4eb41a091 --- /dev/null +++ b/source/chapter5/1process.rst @@ -0,0 +1,390 @@ +进程概念及重要系统调用 +================================================ + +本节导读 +------------------------- + +本节的内容有: + +- 介绍进程的概念以及它和一些其他相近的概念的比较; +- 从应用开发者或是用户的角度介绍我们的实现中一种简单的类 Unix 进程模型; +- 介绍三个最重要的进程相关的系统调用并给出一些用例。 + +进程概念 +------------------------- + +.. _term-process: + +在本章的引言中,出于方便应用开发和使得应用功能更加强大的目标,我们引入了进程的概念。所谓 **进程** (Process) ,就是指 **正在执行的程序** 。尽管说起来很容易,但事实上进程是一个内涵相当丰富且深刻、难以从单个角度解释清楚的抽象概念。我们可以先试着从动态和静态的角度来进行初步的思考。我们知道,当一个应用被成功构建之后,它会从源代码变为某种格式的可执行文件,如果将其展开的话可以在它的内存布局中看到若干个功能迥异的逻辑段。但仅是如此的话,它也就只是某种格式特殊的、被 **静态** 归档到存储器上的一个文件而已。 + +然而,可执行文件与其他类型文件的决定性的不同就在于它可以被内核加载并执行。这一过程自然是不能凭空进行的,而是需要占据某些真实的硬件资源。例如,可执行文件一定需要被加载到物理内存的某些区域中才能执行,另外还可能需要预留一些可执行文件内存布局中未规划的区域(比如栈),这就会消耗掉部分内存空间;在执行的时候需要占据一个 CPU 的全部硬件资源,我们之前介绍过的有通用寄存器(其中程序计数器 pc 和栈指针 sp 两个意义尤其重大)、CSR 、各级 cache 、TLB 等。 + +打一个比方,可执行文件本身可以看成一张编译器解析源代码之后总结出的一张记载如何利用各种硬件资源进行一轮生产流程的 **蓝图** 。而内核的一大功能便是作为一个硬件资源管理器,它可以随时启动一轮生产流程(即执行任意一个应用),这需要选中一张蓝图(此时确定执行哪个可执行文件),接下来就需要内核按照蓝图上所记载的对资源的需求来对应的将各类资源分配给它让这轮生产流程得以顺利进行。当按照蓝图上的记载生产流程完成(应用退出)之后,内核还需要将对应的硬件资源回收以便后续的重复利用。 + +因此,进程就是选取某个可执行文件并对其进行一次动态执行的过程。相比可执行文件,它的动态性主要体现在: + +1. 它是一个过程,从时间上来看有开始也有结束; +2. 在该过程中对于可执行文件中给出的需求要相应对 **硬件资源** 进行 **动态绑定** 。 + +这里需要指出的是,两个进程可以选择同一个可执行文件执行,然而它们却是截然不同的进程:它们的启动时间、占据的硬件资源、输入数据均有可能是不同的,这些条件均会导致它们是不一样的执行过程。在某些情况下,我们可以看到它们的输出是不同的——这是其中一种可能的直观表象。 + +在内核中,需要有一个进程管理器,在其中记录每个进程对资源的占用情况,这是内核作为一个硬件资源管理器所必须要做到的。进程管理器通常需要管理多个进程,因为如果同一时间只有一个进程的话,就可以简单的将所有的硬件资源都交给该进程,同时内核也会像第一章《RV64 裸机应用》那样退化成一个函数库。 + +本节接下来主要站在应用开发者和用户的角度来介绍如何理解进程概念并基于它编写应用程序。 + +.. note:: + + **为何要在这里才引入进程** + + 根据我们多年来的OS课程经验,学生对 ``进程`` 的简单定义“ **正在执行的程序** ”比较容易理解。但对于多个运行的程序之间如何切换,会带来哪些并发问题,进程创建与虚拟内存的关系是啥等问题很难一下子理解清楚,也不清楚试图解决这些问题的原因。 + + 这是由于在 ``进程`` 这个定义背后,有特权级切换、异常处理,程序执行的上下文切换、地址映射、地址空间、虚存管理等一系列的知识的支撑,才能理解清楚操作系统对进程的整个管理过程。所以,我们在前面几章对上述知识进行了铺垫。并以此为基础,更加全面地来分析操作系统是如何管理进程的。 + +.. note:: + + **进程,线程和协程** + + 进程,线程和协程是操作系统中经常出现的名词,它们都是操作系统中的抽象概念,有联系和共同的地方,但也有区别。计算机的核心是CPU,它承担了基本上所有的计算任务;而操作系统是计算机的管理者,它可以以进程,线程和协程为基本的管理和调度单位来使用CPU执行具体的程序逻辑。 + + 从历史角度上看,它们依次出现的顺序是进程、线程和协程。在还没有进程抽象的早期操作系统中,计算机科学家把程序在计算机上的一次执行过程称为一个任务(task)或一个工作(job),其特点是任务和工作在其整个的执行过程中,不会被切换。这样其他任务必须等待一个任务结束后,才能执行,这样系统的效率会比较低。 + + 在引入面向CPU的分时切换机制和面向内存的虚拟内存机制后,进程的概念就被提出了,进程成为CPU(也称处理器)调度(scheduling)和分派(switch)的对象,各个进程间以时间片为单位轮流使用CPU,且每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。这时,操作系统通过进程这个抽象来完成对应用程序在CPU和内存使用上的管理。 + + 随着计算机的发展,对计算机系统性能的要求越来越高,而进程之间的切换开销相对较大,于是计算机科学家就提出了线程。线程是程序执行中一个单一的顺序控制流程,线程是进程的一部分,一个进程可以包含一个或多个线程。各个线程之间共享进程的地址空间,但线程要有自己独立的栈(用于函数访问,局部变量等)和独立的控制流。且线程是处理器调度和分派的基本单位。对于线程的调度和管理,可以在操作系统层面完成,也可以在用户态的线程库中完成。用户态线程也称为绿色线程(GreenThread)。如果是在用户态的线程库中完成,操作系统是“看不到”这样的线程的,也就谈不上对这样线程的管理了。 + + 协程(coroutines,也称纤程(Fiber)),也是程序执行中一个单一的顺序控制流程,建立在线程之上(即一个线程上可以有多个协程),但又比线程更加轻量级的处理器调度对象。协程一般是由用户态的协程管理库来进行管理和调度,这样操作系统是看不到协程的。而且多个协程共享同一线程的栈,这样协程在时间和空间的管理开销上,相对于线程又有很大的改善。在具体实现上,协程可以在用户态运行时库这一层面通过函数调用来实现;也可在语言级支持协程,比如Rust语言引入的 ``async`` 、 ``wait`` 关键字等,通过编译器和运行时库二者配合来简化程序员编程的负担并提高整体的性能。 + +进程模型与重要系统调用 +------------------------------------------------------------ + +目前,我们只介绍一种我们的内核实现中所采用的一种非常简单的进程模型。这个进程模型有三个运行状态:就绪态、运行态和等待态;有基于独立的页表的地址空间;可被操作系统调度来分时占用CPU执行;可以动态创建和退出;可通过系统调用获得操作系统的服务。 +前面我们并没有给出进程需要使用哪些类型的资源,这其实取决于内核提供给应用的系统调用接口以及内核的具体实现。我们实现的进程模型建立在地址空间抽象之上:每个进程都需要一个地址空间,它涵盖了它选择的可执行文件的内存布局,还包含一些其他的逻辑段。且进程模型需要操作系统支持一些重要的系统调用:创建进程、执行新程序、等待进程结束等,来达到应用程序执行的动态灵活性。接下来会介绍这些系统调用的基本功能和设计思路。 + +fork 系统调用 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. _term-pid: +.. _term-initial-process: + +系统中同一时间存在的每个进程都被一个不同的 **进程标识符** (PID, Process Identifier) 所标识。在内核初始化完毕之后会创建一个进程——即 **用户初始进程** (Initial Process) ,它是目前在内核中以硬编码方式创建的唯一一个进程。其他所有的进程都是通过一个名为 ``fork`` 的系统调用来创建的。 + +.. code-block:: rust + + /// 功能:当前进程 fork 出来一个子进程。 + /// 返回值:对于子进程返回 0,对于当前进程则返回子进程的 PID 。 + /// syscall ID:220 + pub fn sys_fork() -> isize; + +进程A调用 ``fork`` 系统调用之后,内核会创建一个新进程B,这个进程B和调用 ``fork`` 的进程A在返回用户态那一瞬间几乎处于相同的状态:这意味着它们包含的用户态的代码段、堆栈段及其他数据段的内容完全相同,但是它们是被放在两个独立的地址空间中的。因此新进程的地址空间需要从原有进程的地址空间完整拷贝一份。两个进程通用寄存器也几乎完全相同。例如, pc 相同意味着两个进程会从同一位置的一条相同指令(我们知道其上一条指令一定是用于系统调用的 ecall 指令)开始向下执行, sp 相同则意味着两个进程的用户栈在各自的地址空间中的位置相同。其余的寄存器相同则确保了二者回到了相同的执行流状态。 + +.. _term-child-process: +.. _term-parent-process: + +但是唯有用来保存 ``fork`` 系统调用返回值的 a0 寄存器(这是 RV64 函数调用规范规定的函数返回值所用的寄存器)的值是不同的。这区分了两个进程:原进程的返回值为新创建进程的 PID ,而新创建进程的返回值为 0 。由于新的进程是原进程主动调用 ``fork`` 衍生出来的,我们称新进程为原进程的 **子进程** (Child Process) ,相对的原进程则被称为新进程的 **父进程** (Parent Process) 。这样二者就建立了一种父子关系。注意到每个进程可能有多个子进程,但最多只能有一个父进程,于是所有进程可以被组织成一颗有根树,其根节点正是代表用户初始程序-initproc的第一个用户态的初始进程。 + +相比创建一个进程, ``fork`` 更重要的功能是建立一对新的父子关系。在我们的进程模型中,父进程和子进程之间的联系更为紧密,它们更容易进行合作或通信,而且一些重要的机制也需要在它们之间才能展开。 + +waitpid 系统调用 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. _term-zombie-process: + +当一个进程通过 ``exit`` 系统调用退出之后,它所占用的资源并不能够立即全部回收。比如该进程的内核栈目前就正用来进行系统调用处理,如果将放置它的物理页帧回收的话,可能会导致系统调用不能正常处理。对于这种问题,一种典型的做法是当进程退出的时候内核立即回收一部分资源并将该进程标记为 **僵尸进程** (Zombie Process) 。之后,由该进程的父进程通过一个名为 ``waitpid`` 的系统调用来收集该进程的返回状态并回收掉它所占据的全部资源,这样这个进程才被彻底销毁。系统调用 ``waitpid`` 的原型如下: + +.. code-block:: rust + + /// 功能:当前进程等待一个子进程变为僵尸进程,回收其全部资源并收集其返回值。 + /// 参数:pid 表示要等待的子进程的进程 ID,如果为 -1 的话表示等待任意一个子进程; + /// exit_code 表示保存子进程返回值的地址,如果这个地址为 0 的话表示不必保存。 + /// 返回值:如果要等待的子进程不存在则返回 -1;否则如果要等待的子进程均未结束则返回 -2; + /// 否则返回结束的子进程的进程 ID。 + /// syscall ID:260 + pub fn sys_waitpid(pid: isize, exit_code: *mut i32) -> isize; + +一般情况下一个进程要负责通过 ``waitpid`` 系统调用来等待所有它 ``fork`` 出来的子进程结束并回收掉它们占据的资源,这也是父子进程间的一种同步手段。但这并不是必须的:如果一个进程先于它的子进程结束,在它退出的时候,它的所有子进程将成为进程树的根节点——用户初始进程的子进程,同时这些子进程的父进程也会变成用户初始进程。这之后,这些子进程的资源就由用户初始进程负责回收了,这也是用户初始进程很重要的一个用途。后面我们会介绍用户初始进程是如何实现的。 + +exec 系统调用 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +如果仅有 ``fork`` 的话,那么所有的进程都只能和用户初始进程一样执行同样的代码段,这显然是远远不够的。于是我们还需要引入 ``exec`` 系统调用来执行不同的可执行文件: + +.. code-block:: rust + + /// 功能:将当前进程的地址空间清空并加载一个特定的可执行文件,返回用户态后开始它的执行。 + /// 参数:path 给出了要加载的可执行文件的名字; + /// 返回值:如果出错的话(如找不到名字相符的可执行文件)则返回 -1,否则不应该返回。 + /// syscall ID:221 + pub fn sys_exec(path: &str) -> isize; + +注意,我们知道 ``path`` 作为 ``&str`` 类型是一个胖指针,既有起始地址又包含长度信息。在实际进行系统调用的时候,我们只会将起始地址传给内核(对标 C 语言仅会传入一个 ``char*`` )。这就需要应用负责在传入的字符串的末尾加上一个 ``\0`` ,这样内核才能知道字符串的长度。下面给出了用户库 ``user_lib`` 中的调用方式: + +.. code-block:: rust + + // user/src/exec.rs + + pub fn sys_exec(path: &str) -> isize { + syscall(SYSCALL_EXEC, [path.as_ptr() as usize, 0, 0]) + } + +这样,利用 ``fork`` 和 ``exec`` 的组合,我们很容易在一个进程内 ``fork`` 出一个子进程并执行一个特定的可执行文件。 + +.. _term-redirection: + +.. note:: + + **为何创建进程要通过两个系统调用而不是一个?** + + 读者可能会有疑问,对于要达成执行不同应用的目标,我们为什么不设计一个系统调用接口同时实现创建一个新进程并加载给定的可执行文件两种功能? + 因为如果使用 ``fork`` 和 ``exec`` 的组合,那么 ``fork`` 出来的进程仅仅是为了 ``exec`` 一个新应用提供空间。而执行 ``fork`` 中对父进程的地址空间拷贝没有用处,还浪费了时间,且在后续清空地址空间的时候还会产生一些资源回收的额外开销。 + 然而这样做是经过实践考验的——事实上 ``fork`` 和 ``exec`` 是一种灵活的系统调用组合。上述的这些开销能够通过一些技术方法(如 ``copy on write`` 等)大幅降低,且拆分为两个系统调用后,可以灵活地支持 **重定向** (Redirection) 等功能。 + 上述方法是UNIX类操作系统的典型做法,这一点与Windows操作系统不一样。在Windows中, ``CreateProcess`` 函数用来创建一个新的进程和它的主线程,通过这个新进程运行指定的可执行文件。虽然是一个函数,但这个函数的参数十个之多,使得这个函数很复杂,且没有 ``fork`` 和 ``exec`` 的组合的灵活性。 + + +应用程序示例 +----------------------------------------------- + +我们刚刚介绍了 ``fork/waitpid/exec`` 三个重要系统调用,借助它们我们可以开发功能更为强大的应用程序。下面我们通过描述两个重要的应用程序: **用户初始程序-init** 和 **外壳程序-user_shell** 的开发过程,来展示这些重要系统调用的使用方法。 + +系统调用封装 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +读者可以在 ``user/src/syscall.rs`` 中看到以 ``sys_*`` 开头的系统调用的函数原型,它们后续还会在 ``user/src/lib.rs`` 中被封装成方便应用程序使用的形式。如 ``sys_fork`` 被封装成 ``fork`` ,而 ``sys_exec`` 被封装成 ``exec`` 。这里值得一提的是 ``sys_waitpid`` 被封装成两个不同的 API : + +.. code-block:: rust + :linenos: + + // user/src/lib.rs + + pub fn wait(exit_code: &mut i32) -> isize { + loop { + match sys_waitpid(-1, exit_code as *mut _) { + -2 => { yield_(); } + // -1 or a real pid + exit_pid => return exit_pid, + } + } + } + + pub fn waitpid(pid: usize, exit_code: &mut i32) -> isize { + loop { + match sys_waitpid(pid as isize, exit_code as *mut _) { + -2 => { yield_(); } + // -1 or a real pid + exit_pid => return exit_pid, + } + } + } + +其中 ``wait`` 表示等待任意一个子进程结束,根据 ``sys_waitpid`` 的约定它需要传的 pid 参数为 ``-1`` ;而 ``waitpid`` 则等待一个 PID 固定的子进程结束。在具体实现方面,我们看到当 ``sys_waitpid`` 返回值为 ``-2`` ,即要等待的子进程存在但它却尚未退出的时候,我们调用 ``yield_`` 主动交出 CPU 使用权,待下次 CPU 使用权被内核交还给它的时候再次调用 ``sys_waitpid`` 查看要等待的子进程是否退出。这样做可以减小 CPU 资源的浪费。 + +目前的实现风格是尽可能简化内核,因此 ``sys_waitpid`` 是立即返回的,即它的返回值只能给出返回这一时刻的状态。如果这一时刻要等待的子进程还尚未结束,那么也只能如实向应用报告这一结果。于是用户库 ``user_lib`` 就需要负责对返回状态进行持续的监控,因此它里面便需要进行循环检查。在后面的实现中,我们会将 ``sys_waitpid`` 的内核实现设计为 **阻塞** 的,也即直到得到一个确切的结果位置都停在内核内,也就意味着内核返回给应用的结果可以直接使用。那是 ``wait`` 和 ``waitpid`` 两个 API 的实现便会更加简单。 + +用户初始程序-initproc +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +我们首先来看用户初始程序-initproc是如何实现的: + +.. code-block:: rust + :linenos: + + // user/src/bin/initproc.rs + + #![no_std] + #![no_main] + + #[macro_use] + extern crate user_lib; + + use user_lib::{ + fork, + wait, + exec, + yield_, + }; + + #[no_mangle] + fn main() -> i32 { + if fork() == 0 { + exec("user_shell\0"); + } else { + loop { + let mut exit_code: i32 = 0; + let pid = wait(&mut exit_code); + if pid == -1 { + yield_(); + continue; + } + println!( + "[initproc] Released a zombie process, pid={}, exit_code={}", + pid, + exit_code, + ); + } + } + 0 + } + +- 第 19 行为 ``fork`` 返回值为 0 的分支,表示子进程,此行直接通过 ``exec`` 执行外壳程序 ``user_shell`` ,注意我们需要在字符串末尾手动加入 ``\0`` ,因为 Rust 在将这些字符串连接到只读数据段的时候不会插入 ``\0`` 。 +- 第 21 行开始则为返回值不为 0 的分支,表示调用 ``fork`` 的用户初始程序-initproc自身。可以看到它在不断循环调用 ``wait`` 来等待那些被移交到它下面的子进程并回收它们占据的资源。如果回收成功的话则会打印一条报告信息给出被回收子进程的 PID 和返回值;否则就 ``yield_`` 交出 CPU 资源并在下次轮到它执行的时候再回收看看。这也可以看出,用户初始程序-initproc对于资源的回收并不算及时,但是对于已经退出的僵尸进程,用户初始程序-initproc最终总能够成功回收它们的资源。 + + +外壳程序-user_shell +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +由于外壳程序-user_shell需要捕获我们的输入并进行解析处理,我们需要加入一个新的用于输入的系统调用: + +.. code-block:: rust + + /// 功能:从文件中读取一段内容到缓冲区。 + /// 参数:fd 是待读取文件的文件描述符,切片 buffer 则给出缓冲区。 + /// 返回值:如果出现了错误则返回 -1,否则返回实际读到的字节数。 + /// syscall ID:63 + pub fn sys_read(fd: usize, buffer: &mut [u8]) -> isize; + +在实际调用的时候我们必须要同时向内核提供缓冲区的起始地址及长度: + +.. code-block:: rust + + // user/src/syscall.rs + + pub fn sys_read(fd: usize, buffer: &mut [u8]) -> isize { + syscall(SYSCALL_READ, [fd, buffer.as_mut_ptr() as usize, buffer.len()]) + } + +我们在用户库中将其进一步封装成每次能够从 **标准输入** 中获取一个字符的 ``getchar`` 函数: + +.. code-block:: rust + + // user/src/lib.rs + + pub fn read(fd: usize, buf: &mut [u8]) -> isize { sys_read(fd, buf) } + + // user/src/console.rs + + const STDIN: usize = 0; + + pub fn getchar() -> u8 { + let mut c = [0u8; 1]; + read(STDIN, &mut c); + c[0] + } + +其中,我们每次临时声明一个长度为 1 的缓冲区。 + +接下来就可以介绍外壳程序- ``user_shell`` 是如何实现的了: + +.. code-block:: rust + :linenos: + :emphasize-lines: 28,53,61 + + // user/src/bin/user_shell.rs + + #![no_std] + #![no_main] + + extern crate alloc; + + #[macro_use] + extern crate user_lib; + + const LF: u8 = 0x0au8; + const CR: u8 = 0x0du8; + const DL: u8 = 0x7fu8; + const BS: u8 = 0x08u8; + + use alloc::string::String; + use user_lib::{fork, exec, waitpid, yield_}; + use user_lib::console::getchar; + + #[no_mangle] + pub fn main() -> i32 { + println!("Rust user shell"); + let mut line: String = String::new(); + print!(">> "); + loop { + let c = getchar(); + match c { + LF | CR => { + println!(""); + if !line.is_empty() { + line.push('\0'); + let pid = fork(); + if pid == 0 { + // child process + if exec(line.as_str()) == -1 { + println!("Error when executing!"); + return -4; + } + unreachable!(); + } else { + let mut exit_code: i32 = 0; + let exit_pid = waitpid(pid as usize, &mut exit_code); + assert_eq!(pid, exit_pid); + println!( + "Shell: Process {} exited with code {}", + pid, exit_code + ); + } + line.clear(); + } + print!(">> "); + } + BS | DL => { + if !line.is_empty() { + print!("{}", BS as char); + print!(" "); + print!("{}", BS as char); + line.pop(); + } + } + _ => { + print!("{}", c as char); + line.push(c as char); + } + } + } + } + +可以看到,在以第 25 行开头的主循环中,每次都是调用 ``getchar`` 获取一个用户输入的字符,并根据它相应进行一些动作。第 23 行声明的字符串 ``line`` 则维护着用户当前输入的命令内容,它也在不断发生变化。 + +.. note:: + + **在应用中使能动态内存分配** + + 我们知道,在 Rust 中可变长字符串类型 ``String`` 是基于动态内存分配的。因此本章我们还要在用户库 ``user_lib`` 中支持动态内存分配,与第四章的做法相同,只需加入以下内容即可: + + .. code-block:: rust + + use buddy_system_allocator::LockedHeap; + + const USER_HEAP_SIZE: usize = 16384; + + static mut HEAP_SPACE: [u8; USER_HEAP_SIZE] = [0; USER_HEAP_SIZE]; + + #[global_allocator] + static HEAP: LockedHeap = LockedHeap::empty(); + + #[alloc_error_handler] + pub fn handle_alloc_error(layout: core::alloc::Layout) -> ! { + panic!("Heap allocation error, layout = {:?}", layout); + } + + #[no_mangle] + #[link_section = ".text.entry"] + pub extern "C" fn _start() -> ! { + unsafe { + HEAP.lock() + .init(HEAP_SPACE.as_ptr() as usize, USER_HEAP_SIZE); + } + exit(main()); + } + +- 如果用户输入回车键(第 28 行),那么user_shell 会 fork 出一个子进程(第 34 行开始)并试图通过 ``exec`` 系统调用执行一个应用,应用的名字在字符串 ``line`` 中给出。这里我们需要注意的是由于子进程是从user_shell 进程中 fork 出来的,它们除了 fork 的返回值不同之外均相同,自然也可以看到一个和user_shell 进程维护的版本相同的字符串 ``line`` 。第 35 行对 ``exec`` 的返回值进行了判断,如果返回值为 -1 的话目前说明在应用管理器中找不到名字相同的应用,此时子进程就直接打印错误信息并退出;反之 ``exec`` 则根本不会返回,而是开始执行目标应用。 + + fork 之后的user_shell 进程自己的逻辑可以在第 41 行找到。可以看出它只是在等待 fork 出来的子进程结束并回收掉它的资源,还会顺带收集子进程的退出状态并打印出来。 +- 如果用户输入退格键(第 53 行),首先我们需要将屏幕上当前行的最后一个字符用空格替换掉,这可以通过输入一个特殊的退格字节 ``BS`` 来实现。其次,user_shell 进程内维护的 ``line`` 也需要弹出最后一个字符。 +- 如果用户输入了一个其他字符(第 61 行),它将会被视为用户的正常输入,我们直接将它打印在屏幕上并加入到 ``line`` 中。 + +当内核初始化完毕之后,它会从可执行文件 ``initproc`` 中加载并执行用户初始程序-initproc,而用户初始程序-initproc中又会 ``fork`` 并 ``exec`` 来运行外壳程序- ``user_shell`` 。这两个应用虽然都是在 CPU 的 U 特权级执行的,但是相比其他应用,它们要更加基础。原则上应该将它们作为一个组件打包在操作系统中。但这里为了实现更加简单,我们并不将它们和其他应用进行区分。 + +除此之外,我们还从 :math:`\mu\text{core}` 中借鉴了很多应用测例。它们可以做到同一时间 **并发** 多个进程并能够有效检验我们内核实现的正确性。感兴趣的读者可以参考 ``matrix`` 和 ``forktree`` 等应用。 \ No newline at end of file diff --git a/source/chapter5/2core-data-structures.rst b/source/chapter5/2core-data-structures.rst new file mode 100644 index 0000000000000000000000000000000000000000..cd63700ea35c51bcb3076f6d12b3dac197c729ad --- /dev/null +++ b/source/chapter5/2core-data-structures.rst @@ -0,0 +1,548 @@ +进程管理的核心数据结构 +=================================== + +本节导读 +----------------------------------- + +为了更好实现进程管理,同时也使得操作系统整体架构更加灵活,能够满足后续的一些需求,我们需要重新设计一些数据结构包含的内容及接口。本节将按照如下顺序来进行介绍: + +- 基于应用名的应用链接/加载器 +- 进程标识符 ``PidHandle`` 以及内核栈 ``KernelStack`` +- 任务控制块 ``TaskControlBlock`` +- 任务管理器 ``TaskManager`` +- 处理器监视器 ``Processor`` + +基于应用名的应用链接/加载器 +------------------------------------------------------------------------ + +在实现 ``exec`` 系统调用的时候,我们需要根据应用的名字而不仅仅是一个编号来获取应用的 ELF 格式数据。因此原有的链接和加载接口需要做出如下变更: + +在链接器 ``os/build.rs`` 中,我们需要按顺序保存链接进来的每个应用的名字: + +.. code-block:: + :linenos: + :emphasize-lines: 8-13 + + // os/build.rs + + for i in 0..apps.len() { + writeln!(f, r#" .quad app_{}_start"#, i)?; + } + writeln!(f, r#" .quad app_{}_end"#, apps.len() - 1)?; + + writeln!(f, r#" + .global _app_names + _app_names:"#)?; + for app in apps.iter() { + writeln!(f, r#" .string "{}""#, app)?; + } + + for (idx, app) in apps.iter().enumerate() { + ... + } + +第 8~13 行,我们按照顺序将各个应用的名字通过 ``.string`` 伪指令放到数据段中,注意链接器会自动在每个字符串的结尾加入分隔符 ``\0`` ,它们的位置则由全局符号 ``_app_names`` 指出。 + +而在加载器 ``loader.rs`` 中,我们用一个全局可见的 *只读* 向量 ``APP_NAMES`` 来按照顺序将所有应用的名字保存在内存中: + +.. code-block:: Rust + + // os/src/loader.rs + + lazy_static! { + static ref APP_NAMES: Vec<&'static str> = { + let num_app = get_num_app(); + extern "C" { fn _app_names(); } + let mut start = _app_names as usize as *const u8; + let mut v = Vec::new(); + unsafe { + for _ in 0..num_app { + let mut end = start; + while end.read_volatile() != '\0' as u8 { + end = end.add(1); + } + let slice = core::slice::from_raw_parts(start, end as usize - start as usize); + let str = core::str::from_utf8(slice).unwrap(); + v.push(str); + start = end.add(1); + } + } + v + }; + } + +使用 ``get_app_data_by_name`` 可以按照应用的名字来查找获得应用的 ELF 数据,而 ``list_apps`` 在内核初始化时被调用,它可以打印出所有可用的应用的名字。 + +.. code-block:: rust + + // os/src/loader.rs + + pub fn get_app_data_by_name(name: &str) -> Option<&'static [u8]> { + let num_app = get_num_app(); + (0..num_app) + .find(|&i| APP_NAMES[i] == name) + .map(|i| get_app_data(i)) + } + + pub fn list_apps() { + println!("/**** APPS ****"); + for app in APP_NAMES.iter() { + println!("{}", app); + } + println!("**************/") + } + + +进程标识符和内核栈 +------------------------------------------------------------------------ + +进程标识符 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +同一时间存在的所有进程都有一个自己的进程标识符,它们是互不相同的整数。这里我们使用 RAII 的思想,将其抽象为一个 ``PidHandle`` 类型,当它的生命周期结束后对应的整数会被编译器自动回收: + +.. code-block:: rust + + // os/src/task/pid.rs + + pub struct PidHandle(pub usize); + +类似之前的物理页帧分配器 ``FrameAllocator`` ,我们实现一个同样使用简单栈式分配策略的进程标识符分配器 ``PidAllocator`` ,并将其全局实例化为 ``PID_ALLOCATOR`` : + +.. code-block:: rust + + // os/src/task/pid.rs + + struct PidAllocator { + current: usize, + recycled: Vec, + } + + impl PidAllocator { + pub fn new() -> Self { + PidAllocator { + current: 0, + recycled: Vec::new(), + } + } + pub fn alloc(&mut self) -> PidHandle { + if let Some(pid) = self.recycled.pop() { + PidHandle(pid) + } else { + self.current += 1; + PidHandle(self.current - 1) + } + } + pub fn dealloc(&mut self, pid: usize) { + assert!(pid < self.current); + assert!( + self.recycled.iter().find(|ppid| **ppid == pid).is_none(), + "pid {} has been deallocated!", pid + ); + self.recycled.push(pid); + } + } + + lazy_static! { + static ref PID_ALLOCATOR : Mutex = Mutex::new(PidAllocator::new()); + } + +``PidAllocator::alloc`` 将会分配出去一个将 ``usize`` 包装之后的 ``PidHandle`` 。我们将其包装为一个全局分配进程标识符的接口 ``pid_alloc`` 提供给内核的其他子模块: + +.. code-block:: rust + + // os/src/task/pid.rs + + pub fn pid_alloc() -> PidHandle { + PID_ALLOCATOR.lock().alloc() + } + +同时我们也需要为 ``PidHandle`` 实现 ``Drop`` Trait 来允许编译器进行自动的资源回收: + +.. code-block:: rust + + // os/src/task/pid.rs + + impl Drop for PidHandle { + fn drop(&mut self) { + PID_ALLOCATOR.lock().dealloc(self.0); + } + } + +内核栈 +~~~~~~~~~~~~~~~~~~~~~~ + +在前面的章节中我们介绍过 :ref:`内核地址空间布局 ` ,当时我们将每个应用的内核栈按照应用编号从小到大的顺序将它们作为逻辑段从高地址到低地址放在内核地址空间中,且两两之间保留一个守护页面使得我们能够尽可能早的发现内核栈溢出问题。从本章开始,我们将应用编号替换为进程标识符来决定每个进程内核栈在地址空间中的位置。 + +因此,在内核栈 ``KernelStack`` 中保存着它所属进程的 PID : + +.. code-block:: rust + + // os/src/task/pid.rs + + pub struct KernelStack { + pid: usize, + } + +它提供以下方法: + +.. code-block:: rust + :linenos: + + // os/src/task/pid.rs + + /// Return (bottom, top) of a kernel stack in kernel space. + pub fn kernel_stack_position(app_id: usize) -> (usize, usize) { + let top = TRAMPOLINE - app_id * (KERNEL_STACK_SIZE + PAGE_SIZE); + let bottom = top - KERNEL_STACK_SIZE; + (bottom, top) + } + + impl KernelStack { + pub fn new(pid_handle: &PidHandle) -> Self { + let pid = pid_handle.0; + let (kernel_stack_bottom, kernel_stack_top) = kernel_stack_position(pid); + KERNEL_SPACE + .lock() + .insert_framed_area( + kernel_stack_bottom.into(), + kernel_stack_top.into(), + MapPermission::R | MapPermission::W, + ); + KernelStack { + pid: pid_handle.0, + } + } + pub fn push_on_top(&self, value: T) -> *mut T where + T: Sized, { + let kernel_stack_top = self.get_top(); + let ptr_mut = (kernel_stack_top - core::mem::size_of::()) as *mut T; + unsafe { *ptr_mut = value; } + ptr_mut + } + pub fn get_top(&self) -> usize { + let (_, kernel_stack_top) = kernel_stack_position(self.pid); + kernel_stack_top + } + } + +- 第 11 行, ``new`` 方法可以从一个 ``PidHandle`` ,也就是一个已分配的进程标识符中对应生成一个内核栈 ``KernelStack`` 。它调用了第 4 行声明的 ``kernel_stack_position`` 函数来根据进程标识符计算内核栈在内核地址空间中的位置,随即在第 14 行将一个逻辑段插入内核地址空间 ``KERNEL_SPACE`` 中。 +- 第 25 行的 ``push_on_top`` 方法可以将一个类型为 ``T`` 的变量压入内核栈顶并返回其裸指针,这也是一个泛型函数。它在实现的时候用到了第 32 行的 ``get_top`` 方法来获取当前内核栈顶在内核地址空间中的地址。 + +内核栈 ``KernelStack`` 也用到了 RAII 的思想,具体来说,实际保存它的物理页帧的生命周期被绑定到它下面,当 ``KernelStack`` 生命周期结束后,这些物理页帧也将会被编译器自动回收: + +.. code-block:: rust + + // os/src/task/pid.rs + + impl Drop for KernelStack { + fn drop(&mut self) { + let (kernel_stack_bottom, _) = kernel_stack_position(self.pid); + let kernel_stack_bottom_va: VirtAddr = kernel_stack_bottom.into(); + KERNEL_SPACE + .lock() + .remove_area_with_start_vpn(kernel_stack_bottom_va.into()); + } + } + +这仅需要为 ``KernelStack`` 实现 ``Drop`` Trait,一旦它的生命周期结束则在内核地址空间中将对应的逻辑段删除(为此在 ``MemorySet`` 中新增了一个名为 ``remove_area_with_start_vpn`` 的方法,感兴趣的读者可以参考其实现),由前面章节的介绍我们知道这也就意味着那些物理页帧被同时回收掉了。 + +进程控制块 +------------------------------------------------------------------------ + +在内核中,每个进程的执行状态、资源控制等元数据均保存在一个被称为 **进程控制块** (PCB, Process Control Block) 的结构中,它是内核对进程进行管理的单位,故而是一种极其关键的内核数据结构。在内核看来,它就等价于一个进程。 + +承接前面的章节,我们仅需对任务控制块 ``TaskControlBlock`` 进行若干改动并让它直接承担进程控制块的功能: + +.. code-block:: rust + :linenos: + + // os/src/task/task.rs + + pub struct TaskControlBlock { + // immutable + pub pid: PidHandle, + pub kernel_stack: KernelStack, + // mutable + inner: Mutex, + } + + pub struct TaskControlBlockInner { + pub trap_cx_ppn: PhysPageNum, + pub base_size: usize, + pub task_cx_ptr: usize, + pub task_status: TaskStatus, + pub memory_set: MemorySet, + pub parent: Option>, + pub children: Vec>, + pub exit_code: i32, + } + +任务控制块中包含两部分: + +- 在初始化之后就不再变化的作为一个字段直接放在任务控制块中。这里将进程标识符 ``PidHandle`` 和内核栈 ``KernelStack`` 放在其中; +- 在运行过程中可能发生变化的则放在 ``TaskControlBlockInner`` 中,将它再包裹上一层互斥锁 ``Mutex`` 放在任务控制块中。这是因为在我们的设计中外层只能获取任务控制块的不可变引用,若想修改里面的部分内容的话这需要 ``Mutex`` 所提供的内部可变性。另外,当后续真正可能有多核同时修改同一个任务控制块中的内容时, ``Mutex`` 可以提供互斥从而避免数据竞争。 + +``TaskControlBlockInner`` 中则包含下面这些内容: + +- ``trap_cx_ppn`` 指出了应用地址空间中的 Trap 上下文(详见第四章)被放在的物理页帧的物理页号。 +- ``base_size`` 的含义是:应用数据仅有可能出现在应用地址空间低于 ``base_size`` 字节的区域中。借助它我们可以清楚的知道应用有多少数据驻留在内存中。 +- ``task_cx_ptr`` 指出一个暂停的任务的任务上下文在内核地址空间(更确切的说是在自身内核栈)中的位置,用于任务切换。 +- ``task_status`` 维护当前进程的执行状态。 +- ``memory_set`` 表示应用地址空间。 +- ``parent`` 指向当前进程的父进程(如果存在的话)。注意我们使用 ``Weak`` 而非 ``Arc`` 来包裹另一个任务控制块,因此这个智能指针将不会影响父进程的引用计数。 +- ``children`` 则将当前进程的所有子进程的任务控制块以 ``Arc`` 智能指针的形式保存在一个向量中,这样才能够更方便的找到它们。 +- 当进程调用 exit 系统调用主动退出或者执行出错由内核终止的时候,它的退出码 ``exit_code`` 会被内核保存在它的任务控制块中,并等待它的父进程通过 waitpid 回收它的资源的同时也收集它的 PID 以及退出码。 + +注意我们在维护父子进程关系的时候大量用到了引用计数 ``Arc/Weak`` 。子进程的进程控制块并不会被直接放到父进程控制块下面,因为子进程完全有可能在父进程退出后仍然存在。因此进程控制块的本体是被放到内核堆上面的,对于它的一切访问都是通过智能指针 ``Arc/Weak`` 来进行的。当且仅当它的引用计数变为 0 的时候,进程控制块以及被绑定到它上面的各类资源才会被回收。 + +``TaskControlBlockInner`` 提供的方法主要是对于它内部的字段的快捷访问: + +.. code-block:: rust + + // os/src/task/task.rs + + impl TaskControlBlockInner { + pub fn get_task_cx_ptr2(&self) -> *const usize { + &self.task_cx_ptr as *const usize + } + pub fn get_trap_cx(&self) -> &'static mut TrapContext { + self.trap_cx_ppn.get_mut() + } + pub fn get_user_token(&self) -> usize { + self.memory_set.token() + } + fn get_status(&self) -> TaskStatus { + self.task_status + } + pub fn is_zombie(&self) -> bool { + self.get_status() == TaskStatus::Zombie + } + } + +而任务控制块 ``TaskControlBlock`` 目前提供以下方法: + +.. code-block:: rust + + // os/src/task/task.rs + + impl TaskControlBlock { + pub fn acquire_inner_lock(&self) -> MutexGuard { + self.inner.lock() + } + pub fn getpid(&self) -> usize { + self.pid.0 + } + pub fn new(elf_data: &[u8]) -> Self {...} + pub fn exec(&self, elf_data: &[u8]) {...} + pub fn fork(self: &Arc) -> Arc {...} + } + +- ``acquire_inner_lock`` 尝试获取互斥锁来得到一个 ``MutexGuard`` ,它可以被看成一个内层 ``TaskControlBlockInner`` 的可变引用并可以对它指向的内容进行修改。之所以要包装为一个方法而不是直接通过 ``self.inner.lock`` 是由于这样接口的定义更加清晰明确。 +- ``getpid`` 以 ``usize`` 的形式返回当前进程的进程标识符。 +- ``new`` 用来创建一个新的进程,目前仅用于内核中手动创建唯一一个初始进程 ``initproc`` 。 +- ``exec`` 用来实现 ``exec`` 系统调用,即当前进程加载并执行另一个 ELF 格式可执行文件。 +- ``fork`` 用来实现 ``fork`` 系统调用,即当前进程 fork 出来一个与之几乎相同的子进程。 + +``new/exec/fork`` 的实现我们将在下一小节再介绍。 + +任务管理器 +------------------------------------------------------------------------ + +在前面的章节中,任务管理器 ``TaskManager`` 不仅负责管理所有的任务,还维护着 CPU 当前在执行哪个任务。由于这种设计不够灵活,不能拓展到后续的多核环境,我们需要将任务管理器对于 CPU 的监控职能拆分到下面即将介绍的处理器监视器 ``Processor`` 中去,任务管理器自身仅负责管理所有任务。在这里,任务指的就是进程。 + +.. code-block:: rust + :linenos: + + // os/src/task/manager.rs + + pub struct TaskManager { + ready_queue: VecDeque>, + } + + /// A simple FIFO scheduler. + impl TaskManager { + pub fn new() -> Self { + Self { ready_queue: VecDeque::new(), } + } + pub fn add(&mut self, task: Arc) { + self.ready_queue.push_back(task); + } + pub fn fetch(&mut self) -> Option> { + self.ready_queue.pop_front() + } + } + + lazy_static! { + pub static ref TASK_MANAGER: Mutex = Mutex::new(TaskManager::new()); + } + + pub fn add_task(task: Arc) { + TASK_MANAGER.lock().add(task); + } + + pub fn fetch_task() -> Option> { + TASK_MANAGER.lock().fetch() + } + +``TaskManager`` 将所有的任务控制块用引用计数 ``Arc`` 智能指针包裹后放在一个双端队列 ``VecDeque`` 中。正如之前介绍的那样,我们并不直接将任务控制块放到 ``TaskManager`` 里面,而是将它们放在内核堆上,在任务管理器中仅存放他们的引用计数智能指针,这也是任务管理器的操作单位。这样做的原因在于,任务控制块经常需要被放入/取出,如果直接移动任务控制块自身将会带来大量的数据拷贝开销,而对于智能指针进行移动则没有多少开销。其次,允许任务控制块的共享引用在某些情况下能够让我们的实现更加方便。 + +``TaskManager`` 提供 ``add/fetch`` 两个操作,前者表示将一个任务加入队尾,后者则表示从队头中取出一个任务来执行。从调度算法来看,这里用到的就是最简单的 RR 算法。全局实例 ``TASK_MANAGER`` 则提供给内核的其他子模块 ``add_task/fetch_task`` 两个函数。 + +在我们的设计中,即使在多核情况下,我们也只有单个任务管理器共享给所有的核来使用。然而在其他设计中,每个核可能都有一个自己独立的任务管理器来管理仅可以在自己上面运行的任务。 + +处理器监视器 +------------------------------------------------------------------------ + +处理器监视器 ``Processor`` 负责从任务管理器 ``TaskManager`` 分离出去的那部分维护 CPU 状态的职责: + +.. code-block:: rust + + // os/src/task/processor.rs + + pub struct Processor { + inner: RefCell, + } + + unsafe impl Sync for Processor {} + + struct ProcessorInner { + current: Option>, + idle_task_cx_ptr: usize, + } + + impl Processor { + pub fn new() -> Self { + Self { + inner: RefCell::new(ProcessorInner { + current: None, + idle_task_cx_ptr: 0, + }), + } + } + } + +在 ``Processor`` 中仅有一个被 ``RefCell`` 包裹起来的 ``ProcessorInner`` 结构体,存放所有在运行过程中可能变化的内容,目前包括: + +- ``current`` 表示在当前处理器上正在执行的任务; +- ``idle_task_cx_ptr`` 表示当前处理器上的 idle 执行流的任务上下文的地址。 + +``Processor`` 是一种 per-CPU 的数据结构,即每个核都有一份专属的 ``Processor`` 结构体,只有这个核自己会访问它,它很容易被拓展到多核环境下使用。因此无论是单核还是多核环境,在访问 ``Processor`` 的时候都不会带来任何隐含的数据竞争风险,这样我们就可以将 ``Processor`` 标记为 ``Sync`` 并全局实例化。但是由于在运行时我们还需要对里面的内容进行修改,故而我们使用一个 ``RefCell`` 将可能被修改的内容包裹起来以提供内部可变性。 + +在单核环境下,我们仅创建单个 ``Processor`` 的全局实例 ``PROCESSOR`` : + +.. code-block:: rust + + // os/src/task/processor.rs + + lazy_static! { + pub static ref PROCESSOR: Processor = Processor::new(); + } + +正在执行的任务 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +在抢占式调度模型中,在一个处理器上执行的任务常常被换入或换出,因此我们需要维护在一个处理器上正在执行的任务,可以查看它的信息或是对它进行替换: + +.. code-block:: rust + :linenos: + + // os/src/task/processor.rs + + impl Processor { + pub fn take_current(&self) -> Option> { + self.inner.borrow_mut().current.take() + } + pub fn current(&self) -> Option> { + self.inner.borrow().current.as_ref().map(|task| Arc::clone(task)) + } + } + + pub fn take_current_task() -> Option> { + PROCESSOR.take_current() + } + + pub fn current_task() -> Option> { + PROCESSOR.current() + } + + pub fn current_user_token() -> usize { + let task = current_task().unwrap(); + let token = task.acquire_inner_lock().get_user_token(); + token + } + + pub fn current_trap_cx() -> &'static mut TrapContext { + current_task().unwrap().acquire_inner_lock().get_trap_cx() + } + +- 第 4 行的 ``Processor::take_current`` 可以取出当前正在执行的任务。注意首先需要通过 ``inner.borrow_mut`` 来获得里层 ``ProcessorInner`` 的可变引用,而后通过 ``Option::take`` 来将正在执行的任务取出并返回,这意味着 ``ProcessorInner`` 里面的 ``current`` 字段也变为 ``None`` 。 +- 第 7 行的 ``Processor::current`` 返回当前执行的任务的一份拷贝,这并不会影响到 ``ProcessorInner`` 里面的 ``current`` 字段,因此只需通过 ``borrow`` 来获取 ``ProcessorInner`` 的不可变引用。 +- 第 12 行的 ``take_current_task`` 以及第 16 行的 ``current_task`` 是对 ``Processor::take_current/current`` 进行封装并提供给内核其他子模块的接口。 +- 第 20 行的 ``current_user_token`` 和第 26 行的 ``current_trap_cx`` 基于 ``current_task`` 实现,可以提供当前正在执行的任务的更多信息。 + + +任务调度的 idle 执行流 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +每个 ``Processor`` 都有一个不同的 idle 执行流,它们运行在每个核各自的启动栈上,功能是尝试从任务管理器中选出一个任务来在当前核上执行。在内核初始化完毕之后,每个核都会通过调用 ``run_tasks`` 函数来进入 idle 执行流: + +.. code-block:: rust + :linenos: + + // os/src/task/processor.rs + + impl Processor { + fn get_idle_task_cx_ptr2(&self) -> *const usize { + let inner = self.inner.borrow(); + &inner.idle_task_cx_ptr as *const usize + } + pub fn run(&self) { + loop { + if let Some(task) = fetch_task() { + let idle_task_cx_ptr2 = self.get_idle_task_cx_ptr2(); + // acquire + let mut task_inner = task.acquire_inner_lock(); + let next_task_cx_ptr2 = task_inner.get_task_cx_ptr2(); + task_inner.task_status = TaskStatus::Running; + drop(task_inner); + // release + self.inner.borrow_mut().current = Some(task); + unsafe { + __switch( + idle_task_cx_ptr2, + next_task_cx_ptr2, + ); + } + } + } + } + } + + pub fn run_tasks() { + PROCESSOR.run(); + } + +可以看到,调度功能的主体在第 8 行的 ``Processor::run`` 中实现。它循环调用 ``fetch_task`` 直到顺利从任务管理器中取出一个任务,随后便准备通过任务切换的方式来执行: + +- 第 11 行得到 ``__switch`` 的第一个参数,也就是当前 idle 执行流的 task_cx_ptr2,这调用了第 4 行的 ``get_idle_task_cx_ptr2`` 方法。 +- 第 13~16 行需要先获取从任务管理器中取出的任务的互斥锁再对对应的任务控制块进行操作,因为在多核环境下有可能会产生并发冲突。在里面我们获取任务的 task_cx_ptr2 作为 ``__switch`` 的第二个参数并修改任务的状态。第 16 行我们需要手动释放互斥锁,这样才能划分出更加精确的临界区。如果依赖编译器在循环的末尾自动释放的话,相当于扩大了临界区,有可能会导致死锁。 +- 第 18 行我们修改当前 ``Processor`` 正在执行的任务为我们取出的任务。注意这里相当于 ``Arc`` 形式的任务从任务管理器流动到了处理器监视器中。也就是说,在稳定的情况下,每个尚未结束的进程的任务控制块都只能被引用一次,要么在任务管理器中,要么则是在某个处理器的 ``Processor`` 中。 +- 第 20 行我们调用 ``__switch`` 来从当前的 idle 执行流切换到接下来要执行的任务。 + +上面介绍了从 idle 执行流通过任务调度切换到某个任务开始执行的过程。而反过来,当一个应用用尽了内核本轮分配给它的时间片或者它主动调用 ``yield`` 系统调用交出 CPU 使用权之后,进入内核后它会调用 ``schedule`` 函数来切换到 idle 执行流并开启新一轮的任务调度。 + +.. code-block:: rust + + // os/src/task/processor.rs + + pub fn schedule(switched_task_cx_ptr2: *const usize) { + let idle_task_cx_ptr2 = PROCESSOR.get_idle_task_cx_ptr2(); + unsafe { + __switch( + switched_task_cx_ptr2, + idle_task_cx_ptr2, + ); + } + } + +这里,我们需要传入即将被切换出去的任务的 task_cx_ptr2 来在合适的位置保存任务上下文,之后就可以通过 ``__switch`` 来切换到 idle 执行流。切换回去之后,从源代码级来看,我们将跳转到 ``Processor::run`` 中 ``__switch`` 返回之后的位置,也即开启了下一轮循环。 \ No newline at end of file diff --git a/source/chapter5/3implement-process-mechanism.rst b/source/chapter5/3implement-process-mechanism.rst new file mode 100644 index 0000000000000000000000000000000000000000..8ced07832b65b78764304b98f0d670d3ad9d3715 --- /dev/null +++ b/source/chapter5/3implement-process-mechanism.rst @@ -0,0 +1,647 @@ +进程管理机制的设计实现 +============================================ + +本节导读 +-------------------------------------------- + +本节将从如下四个方面介绍如何基于上一节设计的内核数据结构来实现进程管理: + +- 初始进程 ``initproc`` 的创建; +- 进程调度机制:当进程主动调用 ``sys_yield`` 交出 CPU 使用权或者内核本轮分配的时间片用尽之后如何切换到下一个进程; +- 进程生成机制:介绍进程相关的两个重要系统调用 ``sys_fork/sys_exec`` 的实现; +- 字符输入机制:为了支对外壳程序-user_shell获得字符输入,介绍 ``sys_read`` 系统调用的实现; +- 进程资源回收机制:当进程调用 ``sys_exit`` 正常退出或者出错被内核终止之后如何保存其退出码,其父进程又是如何通过 ``sys_waitpid`` 系统调用收集该进程的信息并回收其资源。 + +初始进程的创建 +-------------------------------------------- + +内核初始化完毕之后即会调用 ``task`` 子模块提供的 ``add_initproc`` 函数来将初始进程 ``initproc`` 加入任务管理器,但在这之前我们需要初始化初始进程的进程控制块 ``INITPROC`` ,这个过程基于 ``lazy_static`` 在运行时完成。 + +.. code-block:: rust + + // os/src/task/mod.rs + + use crate::loader::get_app_data_by_name; + use manager::add_task; + + lazy_static! { + pub static ref INITPROC: Arc = Arc::new( + TaskControlBlock::new(get_app_data_by_name("initproc").unwrap()) + ); + } + + pub fn add_initproc() { + add_task(INITPROC.clone()); + } + +我们调用 ``TaskControlBlock::new`` 来创建一个进程控制块,它需要传入 ELF 可执行文件的数据切片作为参数,这可以通过加载器 ``loader`` 子模块提供的 ``get_app_data_by_name`` 接口查找 ``initproc`` 的 ELF 数据来获得。在初始化 ``INITPROC`` 之后,则在 ``add_initproc`` 中可以调用 ``task`` 的任务管理器 ``manager`` 子模块提供的 ``add_task`` 接口将其加入到任务管理器。 + +接下来介绍 ``TaskControlBlock::new`` 是如何实现的: + +.. code-block:: rust + :linenos: + + // os/src/task/task.rs + + use super::{PidHandle, pid_alloc, KernelStack}; + use super::TaskContext; + use crate::config::TRAP_CONTEXT; + use crate::trap::TrapContext; + + // impl TaskControlBlock + pub fn new(elf_data: &[u8]) -> Self { + // memory_set with elf program headers/trampoline/trap context/user stack + let (memory_set, user_sp, entry_point) = MemorySet::from_elf(elf_data); + let trap_cx_ppn = memory_set + .translate(VirtAddr::from(TRAP_CONTEXT).into()) + .unwrap() + .ppn(); + // alloc a pid and a kernel stack in kernel space + let pid_handle = pid_alloc(); + let kernel_stack = KernelStack::new(&pid_handle); + let kernel_stack_top = kernel_stack.get_top(); + // push a task context which goes to trap_return to the top of kernel stack + let task_cx_ptr = kernel_stack.push_on_top(TaskContext::goto_trap_return()); + let task_control_block = Self { + pid: pid_handle, + kernel_stack, + inner: Mutex::new(TaskControlBlockInner { + trap_cx_ppn, + base_size: user_sp, + task_cx_ptr: task_cx_ptr as usize, + task_status: TaskStatus::Ready, + memory_set, + parent: None, + children: Vec::new(), + exit_code: 0, + }), + }; + // prepare TrapContext in user space + let trap_cx = task_control_block.acquire_inner_lock().get_trap_cx(); + *trap_cx = TrapContext::app_init_context( + entry_point, + user_sp, + KERNEL_SPACE.lock().token(), + kernel_stack_top, + trap_handler as usize, + ); + task_control_block + } + +- 第 10 行我们解析 ELF 得到应用地址空间 ``memory_set`` ,用户栈在应用地址空间中的位置 ``user_sp`` 以及应用的入口点 ``entry_point`` 。 +- 第 11 行我们手动查页表找到应用地址空间中的 Trap 上下文被实际放在哪个物理页帧上,用来做后续的初始化。 +- 第 16~18 行我们为该进程分配 PID 以及内核栈,并记录下内核栈在内核地址空间的位置 ``kernel_stack_top`` 。 +- 第 20 行我们在该进程的内核栈上压入初始化的任务上下文,使得第一次任务切换到它的时候可以跳转到 ``trap_return`` 并进入用户态开始执行。 +- 第 21 行我们整合之前的部分信息创建进程控制块 ``task_control_block`` 。 +- 第 39 行我们初始化位于该进程应用地址空间中的 Trap 上下文,使得第一次进入用户态的时候时候能正确跳转到应用入口点并设置好用户栈,同时也保证在 Trap 的时候用户态能正确进入内核态。 +- 第 46 行将 ``task_control_block`` 返回。 + +进程调度机制 +-------------------------------------------- + +通过调用 ``task`` 子模块提供的 ``suspend_current_and_run_next`` 函数可以暂停当前任务并切换到下一个任务,当应用调用 ``sys_yield`` 主动交出使用权、本轮时间片用尽或者由于某些原因内核中的处理无法继续的时候,就会在内核中调用此函数触发调度机制并进行任务切换。下面给出了两种典型的使用情况: + +.. code-block:: rust + :emphasize-lines: 4,18 + + // os/src/syscall/process.rs + + pub fn sys_yield() -> isize { + suspend_current_and_run_next(); + 0 + } + + // os/src/trap/mod.rs + + #[no_mangle] + pub fn trap_handler() -> ! { + set_kernel_trap_entry(); + let scause = scause::read(); + let stval = stval::read(); + match scause.cause() { + Trap::Interrupt(Interrupt::SupervisorTimer) => { + set_next_trigger(); + suspend_current_and_run_next(); + } + ... + } + trap_return(); + } + +随着进程概念的引入, ``suspend_current_and_run_next`` 的实现也需要发生变化: + +.. code-block:: rust + :linenos: + + // os/src/task/mod.rs + + use processor::{task_current_task, schedule}; + use manager::add_task; + + pub fn suspend_current_and_run_next() { + // There must be an application running. + let task = take_current_task().unwrap(); + + // ---- hold current PCB lock + let mut task_inner = task.acquire_inner_lock(); + let task_cx_ptr2 = task_inner.get_task_cx_ptr2(); + // Change status to Ready + task_inner.task_status = TaskStatus::Ready; + drop(task_inner); + // ---- release current PCB lock + + // push back to ready queue. + add_task(task); + // jump to scheduling cycle + schedule(task_cx_ptr2); + } + +首先通过 ``take_current_task`` 来取出当前正在执行的任务,修改其进程控制块内的状态,随后将这个任务放入任务管理器的队尾。接着调用 ``schedule`` 函数来触发调度并切换任务。注意,当仅有一个任务的时候, ``suspend_current_and_run_next`` 的效果是会继续执行这个任务。 + +进程的生成机制 +-------------------------------------------- + +在内核中手动生成的进程只有初始进程 ``initproc`` ,余下所有的进程都是它直接或间接 fork 出来的。当一个子进程被 fork 出来之后,它可以调用 ``exec`` 系统调用来加载并执行另一个可执行文件。因此, ``fork/exec`` 两个系统调用提供了进程的生成机制。下面我们分别来介绍二者的实现。 + +fork 系统调用的实现 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +在实现 fork 的时候,最为关键且困难的是为子进程创建一个和父进程几乎完全相同的应用地址空间。我们的实现如下: + +.. code-block:: rust + :linenos: + + // os/src/mm/memory_set.rs + + impl MapArea { + pub fn from_another(another: &MapArea) -> Self { + Self { + vpn_range: VPNRange::new( + another.vpn_range.get_start(), + another.vpn_range.get_end() + ), + data_frames: BTreeMap::new(), + map_type: another.map_type, + map_perm: another.map_perm, + } + } + } + + impl MemorySet { + pub fn from_existed_user(user_space: &MemorySet) -> MemorySet { + let mut memory_set = Self::new_bare(); + // map trampoline + memory_set.map_trampoline(); + // copy data sections/trap_context/user_stack + for area in user_space.areas.iter() { + let new_area = MapArea::from_another(area); + memory_set.push(new_area, None); + // copy data from another space + for vpn in area.vpn_range { + let src_ppn = user_space.translate(vpn).unwrap().ppn(); + let dst_ppn = memory_set.translate(vpn).unwrap().ppn(); + dst_ppn.get_bytes_array().copy_from_slice(src_ppn.get_bytes_array()); + } + } + memory_set + } + } + +这需要对内存管理子模块 ``mm`` 做一些拓展: + +- 第 4 行的 ``MapArea::from_another`` 可以从一个逻辑段复制得到一个虚拟地址区间、映射方式和权限控制均相同的逻辑段,不同的是由于它还没有真正被映射到物理页帧上,所以 ``data_frames`` 字段为空。 +- 第 18 行的 ``MemorySet::from_existed_user`` 可以复制一个完全相同的地址空间。首先在第 19 行,我们通过 ``new_bare`` 新创建一个空的地址空间,并在第 21 行通过 ``map_trampoline`` 为这个地址空间映射上跳板页面,这是因为我们解析 ELF 创建地址空间的时候,并没有将跳板页作为一个单独的逻辑段插入到地址空间的逻辑段向量 ``areas`` 中,所以这里需要单独映射上。 + + 剩下的逻辑段都包含在 ``areas`` 中。我们遍历原地址空间中的所有逻辑段,将复制之后的逻辑段插入新的地址空间,在插入的时候就已经实际分配了物理页帧了。接着我们遍历逻辑段中的每个虚拟页面,对应完成数据复制,这只需要找出两个地址空间中的虚拟页面各被映射到哪个物理页帧,就可转化为将数据从物理内存中的一个位置复制到另一个位置,使用 ``copy_from_slice`` 即可轻松实现。 + +接着,我们实现 ``TaskControlBlock::fork`` 来从父进程的进程控制块创建一份子进程的控制块: + +.. code-block:: rust + :linenos: + + // os/src/task/task.rs + + impl TaskControlBlock { + pub fn fork(self: &Arc) -> Arc { + // ---- hold parent PCB lock + let mut parent_inner = self.acquire_inner_lock(); + // copy user space(include trap context) + let memory_set = MemorySet::from_existed_user( + &parent_inner.memory_set + ); + let trap_cx_ppn = memory_set + .translate(VirtAddr::from(TRAP_CONTEXT).into()) + .unwrap() + .ppn(); + // alloc a pid and a kernel stack in kernel space + let pid_handle = pid_alloc(); + let kernel_stack = KernelStack::new(&pid_handle); + let kernel_stack_top = kernel_stack.get_top(); + // push a goto_trap_return task_cx on the top of kernel stack + let task_cx_ptr = kernel_stack.push_on_top(TaskContext::goto_trap_return()); + let task_control_block = Arc::new(TaskControlBlock { + pid: pid_handle, + kernel_stack, + inner: Mutex::new(TaskControlBlockInner { + trap_cx_ppn, + base_size: parent_inner.base_size, + task_cx_ptr: task_cx_ptr as usize, + task_status: TaskStatus::Ready, + memory_set, + parent: Some(Arc::downgrade(self)), + children: Vec::new(), + exit_code: 0, + }), + }); + // add child + parent_inner.children.push(task_control_block.clone()); + // modify kernel_sp in trap_cx + // **** acquire child PCB lock + let trap_cx = task_control_block.acquire_inner_lock().get_trap_cx(); + // **** release child PCB lock + trap_cx.kernel_sp = kernel_stack_top; + // return + task_control_block + // ---- release parent PCB lock + } + } + +它基本上和新建进程控制块的 ``TaskControlBlock::new`` 是相同的,但要注意以下几点: + +- 子进程的地址空间不是通过解析 ELF 而是通过在第 8 行调用 ``MemorySet::from_existed_user`` 复制父进程地址空间得到的; +- 第 26 行,我们让子进程和父进程的 ``base_size`` ,也即应用数据的大小保持一致; +- 在 fork 的时候需要注意父子进程关系的维护。第 30 行我们将父进程的弱引用计数放到子进程的进程控制块中,而在第 36 行我们将子进程插入到父进程的孩子向量 ``children`` 中。 + +我们在子进程内核栈上压入一个初始化的任务上下文,使得内核一旦通过任务切换到该进程,就会跳转到 ``trap_return`` 来进入用户态。而在复制地址空间的时候,子进程的 Trap 上下文也是完全从父进程复制过来的,这可以保证子进程进入用户态和其父进程回到用户态的那一瞬间 CPU 的状态是完全相同的(后面我们会让它们有一点不同从而区分两个进程)。而两个进程的应用数据由于地址空间复制的原因也是完全相同的,这是 fork 语义要求做到的。 + +在具体实现 ``sys_fork`` 的时候,我们需要特别注意如何体现父子进程的差异: + +.. code-block:: rust + :linenos: + + // os/src/syscall/process.rs + + pub fn sys_fork() -> isize { + let current_task = current_task().unwrap(); + let new_task = current_task.fork(); + let new_pid = new_task.pid.0; + // modify trap context of new_task, because it returns immediately after switching + let trap_cx = new_task.acquire_inner_lock().get_trap_cx(); + // we do not have to move to next instruction since we have done it before + // for child process, fork returns 0 + trap_cx.x[10] = 0; + // add new task to scheduler + add_task(new_task); + new_pid as isize + } + +在调用 ``syscall`` 进行系统调用分发并具体调用 ``sys_fork`` 之前,我们已经将当前进程 Trap 上下文中的 sepc 向后移动了 4 字节使得它回到用户态之后会从 ecall 的下一条指令开始执行。之后当我们复制地址空间的时候,子进程地址空间 Trap 上下文的 sepc 也是移动之后的值,我们无需再进行修改。 + +父子进程回到用户态的瞬间都处于刚刚从一次系统调用返回的状态,但二者的返回值不同。第 8~11 行我们将子进程的 Trap 上下文用来存放系统调用返回值的 a0 寄存器修改为 0 ,而父进程系统调用的返回值会在 ``trap_handler`` 中 ``syscall`` 返回之后再设置为 ``sys_fork`` 的返回值,这里我们返回子进程的 PID 。这就做到了父进程 ``fork`` 的返回值为子进程的 PID ,而子进程的返回值则为 0 。通过返回值是否为 0 可以区分父子进程。 + +另外,不要忘记在第 13 行,我们将生成的子进程通过 ``add_task`` 加入到任务管理器中。 + +exec 系统调用的实现 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``exec`` 系统调用使得一个进程能够加载一个新的 ELF 可执行文件替换原有的应用地址空间并开始执行。我们先从进程控制块的层面进行修改: + +.. code-block:: rust + :linenos: + + // os/src/task/task.rs + + impl TaskControlBlock { + pub fn exec(&self, elf_data: &[u8]) { + // memory_set with elf program headers/trampoline/trap context/user stack + let (memory_set, user_sp, entry_point) = MemorySet::from_elf(elf_data); + let trap_cx_ppn = memory_set + .translate(VirtAddr::from(TRAP_CONTEXT).into()) + .unwrap() + .ppn(); + + // **** hold current PCB lock + let mut inner = self.acquire_inner_lock(); + // substitute memory_set + inner.memory_set = memory_set; + // update trap_cx ppn + inner.trap_cx_ppn = trap_cx_ppn; + // initialize trap_cx + let trap_cx = inner.get_trap_cx(); + *trap_cx = TrapContext::app_init_context( + entry_point, + user_sp, + KERNEL_SPACE.lock().token(), + self.kernel_stack.get_top(), + trap_handler as usize, + ); + // **** release current PCB lock + } + } + +它在解析传入的 ELF 格式数据之后只做了两件事情: + +- 首先是从 ELF 生成一个全新的地址空间并直接替换进来(第 15 行),这将导致原有的地址空间生命周期结束,里面包含的全部物理页帧都会被回收; +- 然后是修改新的地址空间中的 Trap 上下文,将解析得到的应用入口点、用户栈位置以及一些内核的信息进行初始化,这样才能正常实现 Trap 机制。 + +这里无需对任务上下文进行处理,因为这个进程本身已经在执行了,而只有被暂停的应用才需要在内核栈上保留一个任务上下文。 + +借助它 ``sys_exec`` 就很容易实现了: + +.. code-block:: rust + :linenos: + + // os/src/mm/page_table.rs + + pub fn translated_str(token: usize, ptr: *const u8) -> String { + let page_table = PageTable::from_token(token); + let mut string = String::new(); + let mut va = ptr as usize; + loop { + let ch: u8 = *(page_table.translate_va(VirtAddr::from(va)).unwrap().get_mut()); + if ch == 0 { + break; + } else { + string.push(ch as char); + va += 1; + } + } + string + } + + // os/src/syscall/process.rs + + pub fn sys_exec(path: *const u8) -> isize { + let token = current_user_token(); + let path = translated_str(token, path); + if let Some(data) = get_app_data_by_name(path.as_str()) { + let task = current_task().unwrap(); + task.exec(data); + 0 + } else { + -1 + } + } + +应用在 ``sys_exec`` 系统调用中传递给内核的只有一个要执行的应用名字符串在当前应用地址空间中的起始地址,如果想在内核中具体获得字符串的话就需要手动查页表。第 3 行的 ``translated_str`` 便可以从内核地址空间之外的某个地址空间中拿到一个字符串,其原理就是逐字节查页表直到发现一个 ``\0`` 为止。 + +回到 ``sys_exec`` 的实现,它调用 ``translated_str`` 找到要执行的应用名并试图在应用加载器提供的 ``get_app_data_by_name`` 接口中找到对应的 ELF 数据。如果找到的话就调用 ``TaskControlBlock::exec`` 替换掉地址空间并返回 0。这个返回值其实并没有意义,因为我们在替换地址空间的时候本来就对 Trap 上下文重新进行了初始化。如果没有找到的话就不做任何事情并返回 -1,在外壳程序-user_shell中我们也正是通过这个返回值来判断要执行的应用是否存在。 + +系统调用后重新获取 Trap 上下文 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +原来在 ``trap_handler`` 中我们是这样处理系统调用的: + +.. code-block:: rust + + // os/src/trap/mod.rs + + #[no_mangle] + pub fn trap_handler() -> ! { + set_kernel_trap_entry(); + let cx = current_trap_cx(); + let scause = scause::read(); + let stval = stval::read(); + match scause.cause() { + Trap::Exception(Exception::UserEnvCall) => { + cx.sepc += 4; + cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize; + } + ... + } + trap_return(); + } + +这里的 ``cx`` 是当前应用的 Trap 上下文的可变引用,我们需要通过查页表找到它具体被放在哪个物理页帧上,并构造相同的虚拟地址来在内核中访问它。对于系统调用 ``sys_exec`` 来说,一旦调用它之后,我们会发现 ``trap_handler`` 原来上下文中的 ``cx`` 失效了——因为它是用来访问之前地址空间中 Trap 上下文被保存在的那个物理页帧的,而现在它已经被回收掉了。因此,为了能够处理类似的这种情况,我们在 ``syscall`` 分发函数返回之后需要重新获取 ``cx`` ,目前的实现如下: + +.. code-block:: rust + + // os/src/trap/mod.rs + + #[no_mangle] + pub fn trap_handler() -> ! { + set_kernel_trap_entry(); + let scause = scause::read(); + let stval = stval::read(); + match scause.cause() { + Trap::Exception(Exception::UserEnvCall) => { + // jump to next instruction anyway + let mut cx = current_trap_cx(); + cx.sepc += 4; + // get system call return value + let result = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]); + // cx is changed during sys_exec, so we have to call it again + cx = current_trap_cx(); + cx.x[10] = result as usize; + } + ... + } + trap_return(); + } + + +外壳程序-user_shell的输入机制 +-------------------------------------------- + +为了实现外壳程序-user_shell的输入机制,我们需要实现 ``sys_read`` 系统调用使得应用能够取得用户的键盘输入。 + +.. code-block:: rust + + // os/src/syscall/fs.rs + + use crate::sbi::console_getchar; + + const FD_STDIN: usize = 0; + + pub fn sys_read(fd: usize, buf: *const u8, len: usize) -> isize { + match fd { + FD_STDIN => { + assert_eq!(len, 1, "Only support len = 1 in sys_read!"); + let mut c: usize; + loop { + c = console_getchar(); + if c == 0 { + suspend_current_and_run_next(); + continue; + } else { + break; + } + } + let ch = c as u8; + let mut buffers = translated_byte_buffer(current_user_token(), buf, len); + unsafe { buffers[0].as_mut_ptr().write_volatile(ch); } + 1 + } + _ => { + panic!("Unsupported fd in sys_read!"); + } + } + } + +目前我们仅支持从标准输入 ``FD_STDIN`` 即文件描述符 0 读入,且单次读入的长度限制为 1,即每次只能读入一个字符。我们调用 ``sbi`` 子模块提供的从键盘获取输入的接口 ``console_getchar`` ,如果返回 0 的话说明还没有输入,我们调用 ``suspend_current_and_run_next`` 暂时切换到其他进程,等下次切换回来的时候再看看是否有输入了。获取到输入之后,我们退出循环并手动查页表将输入的字符正确的写入到应用地址空间。 + +进程资源回收机制 +-------------------------------------------- + +进程的退出 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +当应用调用 ``sys_exit`` 系统调用主动退出或者出错由内核终止之后,会在内核中调用 ``exit_current_and_run_next`` 函数退出当前任务并切换到下一个。使用方法如下: + +.. code-block:: rust + :linenos: + :emphasize-lines: 4,29,34 + + // os/src/syscall/process.rs + + pub fn sys_exit(exit_code: i32) -> ! { + exit_current_and_run_next(exit_code); + panic!("Unreachable in sys_exit!"); + } + + // os/src/trap/mod.rs + + #[no_mangle] + pub fn trap_handler() -> ! { + set_kernel_trap_entry(); + let scause = scause::read(); + let stval = stval::read(); + match scause.cause() { + Trap::Exception(Exception::StoreFault) | + Trap::Exception(Exception::StorePageFault) | + Trap::Exception(Exception::InstructionFault) | + Trap::Exception(Exception::InstructionPageFault) | + Trap::Exception(Exception::LoadFault) | + Trap::Exception(Exception::LoadPageFault) => { + println!( + "[kernel] {:?} in application, bad addr = {:#x}, bad instruction = {:#x}, core dumped.", + scause.cause(), + stval, + current_trap_cx().sepc, + ); + // page fault exit code + exit_current_and_run_next(-2); + } + Trap::Exception(Exception::IllegalInstruction) => { + println!("[kernel] IllegalInstruction in application, core dumped."); + // illegal instruction exit code + exit_current_and_run_next(-3); + } + ... + } + trap_return(); + } + +相比前面的章节, ``exit_current_and_run_next`` 带有一个退出码作为参数。当在 ``sys_exit`` 正常退出的时候,退出码由应用传到内核中;而出错退出的情况(如第 29 行的访存错误或第 34 行的非法指令异常)则是由内核指定一个特定的退出码。这个退出码会在 ``exit_current_and_run_next`` 写入当前进程的进程控制块中: + +.. code-block:: rust + :linenos: + + // os/src/mm/memory_set.rs + + impl MemorySet { + pub fn recycle_data_pages(&mut self) { + self.areas.clear(); + } + } + + // os/src/task/mod.rs + + pub fn exit_current_and_run_next(exit_code: i32) { + // take from Processor + let task = take_current_task().unwrap(); + // **** hold current PCB lock + let mut inner = task.acquire_inner_lock(); + // Change status to Zombie + inner.task_status = TaskStatus::Zombie; + // Record exit code + inner.exit_code = exit_code; + // do not move to its parent but under initproc + + // ++++++ hold initproc PCB lock here + { + let mut initproc_inner = INITPROC.acquire_inner_lock(); + for child in inner.children.iter() { + child.acquire_inner_lock().parent = Some(Arc::downgrade(&INITPROC)); + initproc_inner.children.push(child.clone()); + } + } + // ++++++ release parent PCB lock here + + inner.children.clear(); + // deallocate user space + inner.memory_set.recycle_data_pages(); + drop(inner); + // **** release current PCB lock + // drop task manually to maintain rc correctly + drop(task); + // we do not have to save task context + let _unused: usize = 0; + schedule(&_unused as *const _); + } + +- 第 13 行我们调用 ``take_current_task`` 来将当前进程控制块从处理器监控 ``PROCESSOR`` 中取出而不是得到一份拷贝,这是为了正确维护进程控制块的引用计数; +- 第 17 行我们将进程控制块中的状态修改为 ``TaskStatus::Zombie`` 即僵尸进程,这样它后续才能被父进程在 ``waitpid`` 系统调用的时候回收; +- 第 19 行我们将传入的退出码 ``exit_code`` 写入进程控制块中,后续父进程在 ``waitpid`` 的时候可以收集; +- 第 24~26 行所做的事情是将当前进程的所有子进程挂在初始进程 ``initproc`` 下面,其做法是遍历每个子进程,修改其父进程为初始进程,并加入初始进程的孩子向量中。第 32 行将当前进程的孩子向量清空。 +- 第 34 行对于当前进程占用的资源进行早期回收。在第 4 行可以看出, ``MemorySet::recycle_data_pages`` 只是将地址空间中的逻辑段列表 ``areas`` 清空,这将导致应用地址空间的所有数据被存放在的物理页帧被回收,而用来存放页表的那些物理页帧此时则不会被回收。 +- 最后在第 41 行我们调用 ``schedule`` 触发调度及任务切换,由于我们再也不会回到该进程的执行过程中,因此无需关心任务上下文的保存。 + +父进程回收子进程资源 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +父进程通过 ``sys_waitpid`` 系统调用来回收子进程的资源并收集它的一些信息: + +.. code-block:: rust + :linenos: + + // os/src/syscall/process.rs + + /// If there is not a child process whose pid is same as given, return -1. + /// Else if there is a child process but it is still running, return -2. + pub fn sys_waitpid(pid: isize, exit_code_ptr: *mut i32) -> isize { + let task = current_task().unwrap(); + // find a child process + + // ---- hold current PCB lock + let mut inner = task.acquire_inner_lock(); + if inner.children + .iter() + .find(|p| {pid == -1 || pid as usize == p.getpid()}) + .is_none() { + return -1; + // ---- release current PCB lock + } + let pair = inner.children + .iter() + .enumerate() + .find(|(_, p)| { + // ++++ temporarily hold child PCB lock + p.acquire_inner_lock().is_zombie() && + (pid == -1 || pid as usize == p.getpid()) + // ++++ release child PCB lock + }); + if let Some((idx, _)) = pair { + let child = inner.children.remove(idx); + // confirm that child will be deallocated after removing from children list + assert_eq!(Arc::strong_count(&child), 1); + let found_pid = child.getpid(); + // ++++ temporarily hold child lock + let exit_code = child.acquire_inner_lock().exit_code; + // ++++ release child PCB lock + *translated_refmut(inner.memory_set.token(), exit_code_ptr) = exit_code; + found_pid as isize + } else { + -2 + } + // ---- release current PCB lock automatically + } + +``sys_waitpid`` 是一个立即返回的系统调用,它的返回值语义是:如果当前的进程不存在一个符合要求的子进程,则返回 -1;如果至少存在一个,但是其中没有僵尸进程(也即仍未退出)则返回 -2;如果都不是的话则可以正常回收并返回回收子进程的 pid 。但在编写应用的开发者看来, ``wait/waitpid`` 两个辅助函数都必定能够返回一个有意义的结果,要么是 -1,要么是一个正数 PID ,是不存在 -2 这种通过等待即可消除的中间结果的。这等待的过程正是在用户库 ``user_lib`` 中完成。 + +第 11~17 行判断 ``sys_waitpid`` 是否会返回 -1 ,这取决于当前进程是否有一个符合要求的子进程。当传入的 ``pid`` 为 -1 的时候,任何一个子进程都算是符合要求;但 ``pid`` 不为 -1 的时候,则只有 PID 恰好与 ``pid`` 相同的子进程才算符合条件。我们简单通过迭代器即可完成判断。 + +第 18~26 行判断符合要求的子进程中是否有僵尸进程,如果有的话还需要同时找出它在当前进程控制块子进程向量中的下标。如果找不到的话直接返回 ``-2`` ,否则进入第 28~36 行的处理: + +- 第 28 行我们将子进程从向量中移除并置于当前上下文中,此时可以确认这是对于该子进程控制块的唯一一次强引用,即它不会出现在某个进程的子进程向量中,更不会出现在处理器监控器或者任务管理器中。当它所在的代码块结束,这次引用变量的生命周期结束,将导致该子进程进程控制块的引用计数变为 0 ,彻底回收掉它占用的所有资源,包括:内核栈和它的 PID 还有它的应用地址空间存放页表的那些物理页帧等等。 +- 剩下主要是将收集的子进程信息返回回去。第 31 行得到了子进程的 PID 并会在最终返回;第 33 行得到了子进程的退出码并于第 35 行写入到当前进程的应用地址空间中。由于应用传递给内核的仅仅是一个指向应用地址空间中保存子进程返回值的内存区域的指针,我们还需要在 ``translated_refmut`` 中手动查页表找到应该写入到物理内存中的哪个位置。其实现可以在 ``os/src/mm/page_table.rs`` 中找到,比较简单,在这里不再赘述。 \ No newline at end of file diff --git a/source/chapter5/4exercise.rst b/source/chapter5/4exercise.rst new file mode 100644 index 0000000000000000000000000000000000000000..a59889554998dfa1d3e96d366b43d2fa5156a013 --- /dev/null +++ b/source/chapter5/4exercise.rst @@ -0,0 +1,74 @@ +chapter5 练习 +============================================== + +- 本节难度: **一定比lab4简单** + +编程作业 +--------------------------------------------- + +进程创建 ++++++++++++++++++++++++++++++++++++++++++++++ + +大家一定好奇过为啥进程创建要用 fork + execve 这么一个奇怪的系统调用,就不能直接搞一个新进程吗?思而不学则殆,我们就来试一试!这章的编程练习请大家实现一个完全 DIY 的系统调用 spawn,用以创建一个新进程。 + +spawn 系统调用定义( `标准spawn看这里 `_ ): + +- syscall ID: 400 +- C 接口: ``int spawn(char *filename)`` +- Rust 接口: ``fn spawn(file: *const u8) -> isize`` +- 功能:相当于 fork + exec,新建子进程并执行目标程序。 +- 说明:成功返回子进程id,否则返回 -1。 +- 可能的错误: + - 无效的文件名。 + - 进程池满/内存不足等资源错误。 + +实验要求 ++++++++++++++++++++++++++++++++++++++++++++++ +- 实现分支:ch5。 +- 完成实验指导书中的内容,实现进程控制,可以运行 usershell。 +- 实现自定义系统调用 spawn,并通过 并通过 `Rust测例 `_ 中chapter5对应的所有测例。 + +challenge: 支持多核。 + +实验检查 ++++++++++++++++++++++++++++++++++++++++++++++ + +- 实验目录要求 + + 目录要求不变(参考lab1目录或者示例代码目录结构)。同样在 os 目录下 ``make run`` 之后可以正确加载用户程序并执行。 + 加载的用户测例位置: ``../user/build/bin``。 + +- 检查 + + 可以正确 ``make run`` 执行,可以正确执行目标用户测例,并得到预期输出(详见测例注释)。 + +问答作业 +-------------------------------------------- + +(1) fork + exec 的一个比较大的问题是 fork 之后的内存页/文件等资源完全没有使用就废弃了,针对这一点,有什么改进策略? + +(2) 其实使用了题(1)的策略之后,fork + exec 所带来的无效资源的问题已经基本被解决了,但是今年来 fork 还是在被不断的批判,那么到底是什么正在"杀死"fork?可以参考 `论文 `_ ,**注意**:回答无明显错误就给满分,出这题只是想引发大家的思考,完全不要求看论文,球球了,别卷了。 + +(3) fork 当年被设计并称道肯定是有其好处的。请使用 **带初始参数** 的 spawn 重写如下 fork 程序,然后描述 fork 有那些好处。注意:使用"伪代码"传达意思即可,spawn 接口可以自定义。可以写多个文件。 + + .. code-block:: rust + + fn main() { + let a = get_a(); + if fork() == 0 { + let b = get_b(); + println!("a + b = {}", a + b); + exit(0); + } + println!("a = {}", a); + 0 + } + +4. 描述进程执行的几种状态,以及 fork/exec/wait/exit 对于状态的影响。 + +报告要求 +------------------------------------------------------------ + +* 简单总结本次实验与上个实验相比你增加的东西。(控制在5行以内,不要贴代码) +* 完成问答问题 +* (optional) 你对本次实验设计及难度的看法。 \ No newline at end of file diff --git a/source/chapter5/index.rst b/source/chapter5/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..45e8b60e9b7429a465f76f41336e8c1d8a6ebf50 --- /dev/null +++ b/source/chapter5/index.rst @@ -0,0 +1,14 @@ +第五章:进程及进程管理 +============================================== + +.. toctree:: + :maxdepth: 4 + + 0intro + 1process + 2core-data-structures + 3implement-process-mechanism + 4exercise + +MULTICS操作系统是侏罗纪的“霸王龙”操作系统。 +UNIX操作系统是小巧聪明的“伤齿龙”操作系统。 \ No newline at end of file diff --git a/source/chapter6/0intro.rst b/source/chapter6/0intro.rst new file mode 100644 index 0000000000000000000000000000000000000000..d1da625c78234baec58d8d038d138db605767933 --- /dev/null +++ b/source/chapter6/0intro.rst @@ -0,0 +1,148 @@ +引言 +========================================= + +本章导读 +----------------------------------------- + +在上一章中,我们引入了非常重要的进程的概念。截止到目前为止,进程能够进行交互的 I/O 资源还非常有限。它只能接受用户在键盘上的输入,并将字符输出到屏幕上。我们一般将它们分别称为标准输入和标准输出。 + +本章我们会引入操作系统中的另一个概念——文件描述符。每个进程都在它自己的文件描述符表中保存着多个文件描述符,而进程通过每个文件描述符均可对一个它已经请求内核打开的 I/O 资源(也即文件)进行读写。文件描述符可以描述包括标准输入/标准输出在内的多种不同的 I/O 资源。 + +本章我们首先将标准输入/标准输出的访问改造为基于文件描述符,然后同样基于文件描述符实现一种父子进程之间的通信机制——管道。 + +实践体验 +----------------------------------------- + +获取本章代码: + +.. code-block:: console + + $ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git + $ cd rCore-Tutorial-v3 + $ git checkout ch6 + +在 qemu 模拟器上运行本章代码: + +.. code-block:: console + + $ cd os + $ make run + +将 Maix 系列开发板连接到 PC,并在上面运行本章代码: + +.. code-block:: console + + $ cd os + $ make run BOARD=k210 + +进入用户终端后,可以运行管道机制的简单测例 ``pipetest`` 和比较复杂的测例 ``pipe_large_test`` 。 ``pipetest`` 需要保证父进程通过管道传输给子进程的字符串不会发生变化;而 ``pipe_large_test`` 中,父进程将一个长随机字符串传给子进程,随后父子进程同时计算该字符串的某种 Hash 值(逐字节求和),子进程会将计算后的 Hash 值传回父进程,而父进程接受到之后,需要验证两个 Hash 值相同,才算通过测试。 + +运行两个测例的输出可能如下: + +.. code-block:: + + >> pipetest + Read OK, child process exited! + pipetest passed! + Shell: Process 2 exited with code 0 + >> pipe_large_test + sum = 369114(parent) + sum = 369114(child) + Child process exited! + pipe_large_test passed! + Shell: Process 2 exited with code 0 + >> + + + +本章代码树 +----------------------------------------- + +.. code-block:: + + ├── bootloader + │   ├── rustsbi-k210.bin + │   └── rustsbi-qemu.bin + ├── LICENSE + ├── os + │   ├── build.rs + │   ├── Cargo.lock + │   ├── Cargo.toml + │   ├── Makefile + │   └── src + │   ├── config.rs + │   ├── console.rs + │   ├── entry.asm + │   ├── fs(新增:文件系统子模块 fs) + │   │   ├── mod.rs(包含已经打开且可以被进程读写的文件的抽象 File Trait) + │   │   ├── pipe.rs(实现了 File Trait 的第一个分支——可用来进程间通信的管道) + │   │   └── stdio.rs(实现了 File Trait 的第二个分支——标准输入/输出) + │   ├── lang_items.rs + │   ├── link_app.S + │   ├── linker-k210.ld + │   ├── linker-qemu.ld + │   ├── loader.rs + │   ├── main.rs + │   ├── mm + │   │   ├── address.rs + │   │   ├── frame_allocator.rs + │   │   ├── heap_allocator.rs + │   │   ├── memory_set.rs + │   │   ├── mod.rs + │   │   └── page_table.rs(新增:应用地址空间的缓冲区抽象 UserBuffer 及其迭代器实现) + │   ├── sbi.rs + │   ├── syscall + │   │   ├── fs.rs(修改:调整 sys_read/write 的实现,新增 sys_close/pipe) + │   │   ├── mod.rs(修改:调整 syscall 分发) + │   │   └── process.rs + │   ├── task + │   │   ├── context.rs + │   │   ├── manager.rs + │   │   ├── mod.rs + │   │   ├── pid.rs + │   │   ├── processor.rs + │   │   ├── switch.rs + │   │   ├── switch.S + │   │   └── task.rs(修改:在任务控制块中加入文件描述符表相关机制) + │   ├── timer.rs + │   └── trap + │   ├── context.rs + │   ├── mod.rs + │   └── trap.S + ├── README.md + ├── rust-toolchain + ├── tools + │   ├── kflash.py + │   ├── LICENSE + │   ├── package.json + │   ├── README.rst + │   └── setup.py + └── user + ├── Cargo.lock + ├── Cargo.toml + ├── Makefile + └── src + ├── bin + │   ├── exit.rs + │   ├── fantastic_text.rs + │   ├── forktest2.rs + │   ├── forktest.rs + │   ├── forktest_simple.rs + │   ├── forktree.rs + │   ├── hello_world.rs + │   ├── initproc.rs + │   ├── matrix.rs + │   ├── pipe_large_test.rs(新增) + │   ├── pipetest.rs(新增) + │   ├── run_pipe_test.rs(新增) + │   ├── sleep.rs + │   ├── sleep_simple.rs + │   ├── stack_overflow.rs + │   ├── user_shell.rs + │   ├── usertests.rs + │   └── yield.rs + ├── console.rs + ├── lang_items.rs + ├── lib.rs(新增两个系统调用:sys_close/sys_pipe) + ├── linker.ld + └── syscall.rs(新增两个系统调用:sys_close/sys_pipe) diff --git a/source/chapter6/1file-descriptor.rst b/source/chapter6/1file-descriptor.rst new file mode 100644 index 0000000000000000000000000000000000000000..123b5ce06b157cffda120be20f887ff9617d1be4 --- /dev/null +++ b/source/chapter6/1file-descriptor.rst @@ -0,0 +1,300 @@ +文件与文件描述符 +=========================================== + +本节导读 +------------------------------------------- + +本节我们介绍文件和文件描述符概念,并在进程中加入文件描述符表,同时将进程对于标准输入输出的访问的修改为基于文件抽象接口的实现。 + +文件 +------------------------------------------- + +.. chyyuu 可以简单介绍一下文件的起源??? + +在类 Unix 操作系统中,”**一切皆文件**“ (Everything is a file) 是一种重要的设计哲学。在这里,所谓的 **文件** (File) 就是指由内核管理并分配给进程让它可以与之交互的部分 I/O 资源,它大致可以分成以下几种: + +- **普通文件** (Regular File) 指的是储存在磁盘/硬盘等存储介质上的文件系统中的一般意义上的文件,可以被看成一个固定的字节序列; +- **目录** (Directory) 是用来建立树形目录结构的文件系统并可以根据路径索引文件的一种特殊的文件; +- **符号链接** (Symbolic Link) 一种能够指向其他文件的特殊文件; +- **命名管道** (Named Pipe) 与我们本章将介绍的 **匿名管道** (Pipe) 有一些不同,但它们均用于单向的进程间通信; +- **套接字** (Socket) 可以支持双向的进程间通信; +- **设备文件** (Device File) 可以与系统中的 I/O 设备进行交互。这些 I/O 设备可以分成两类: **块设备** (Block Device) 和 **字符设备** (Character Device) 。块设备只能以 **块** (Block) 为单位进行读写,并支持随机访问,最典型的如一些磁盘/硬盘等;而字符设备允许发送者和接收者传输一条字节流,接收者只能按照发送者发送的顺序接收字符,最典型的如键盘/串口等。 + +文件虽然代表了很多种不同的软件/硬件 I/O 资源,但是在进程看来所有文件的访问都可以通过一个统一的抽象接口 ``File`` 来进行: + +.. code-block:: rust + + // os/src/fs/mod.rs + + pub trait File : Send + Sync { + fn read(&self, buf: UserBuffer) -> usize; + fn write(&self, buf: UserBuffer) -> usize; + } + +其中 ``UserBuffer`` 是我们在 ``mm`` 子模块中定义的应用地址空间中的一段缓冲区的抽象。它本质上其实只是一个 ``&[u8]`` ,但是它位于应用地址空间中,在内核中我们无法直接通过这种方式来访问,因此需要进行封装。然而,在理解抽象接口 ``File`` 的各方法时,我们仍可以将 ``UserBuffer`` 看成一个 ``&[u8]`` 切片,它是同时给出了缓冲区的起始地址及长度的一个胖指针。 + +``read`` 指的是从文件中读取数据放到缓冲区中,最多将缓冲区填满(即读取缓冲区的长度那么多字节),并返回实际读取的字节数;而 ``write`` 指的是将缓冲区中的数据写入文件,最多将缓冲区中的数据全部写入,并返回直接写入的字节数。至于 ``read`` 和 ``write`` 的实现则与文件具体是哪种类型有关,它决定了数据如何被读取和写入。 + +回过头来再看一下用户缓冲区的抽象 ``UserBuffer`` ,它的声明如下: + +.. code-block:: rust + + // os/src/mm/page_table.rs + + pub fn translated_byte_buffer( + token: usize, + ptr: *const u8, + len: usize + ) -> Vec<&'static mut [u8]>; + + pub struct UserBuffer { + pub buffers: Vec<&'static mut [u8]>, + } + + impl UserBuffer { + pub fn new(buffers: Vec<&'static mut [u8]>) -> Self { + Self { buffers } + } + pub fn len(&self) -> usize { + let mut total: usize = 0; + for b in self.buffers.iter() { + total += b.len(); + } + total + } + } + +它只是将我们调用 ``translated_byte_buffer`` 获得的包含多个切片的 ``Vec`` 进一步包装起来,通过 ``len`` 方法可以得到缓冲区的长度。此外,我们还让它作为一个迭代器可以逐字节进行读写。有兴趣的读者可以参考类型 ``UserBufferIterator`` 还有 ``IntoIterator`` 和 ``Iterator`` 两个 Trait 的使用方法。 + +标准输入和标准输出 +-------------------------------------------- + +我们为标准输入和标准输出实现 ``File`` Trait,使得进程可以与它们交互: + +.. code-block:: rust + :linenos: + + // os/src/fs/stdio.rs + + pub struct Stdin; + + pub struct Stdout; + + impl File for Stdin { + fn read(&self, mut user_buf: UserBuffer) -> usize { + assert_eq!(user_buf.len(), 1); + // busy loop + let mut c: usize; + loop { + c = console_getchar(); + if c == 0 { + suspend_current_and_run_next(); + continue; + } else { + break; + } + } + let ch = c as u8; + unsafe { user_buf.buffers[0].as_mut_ptr().write_volatile(ch); } + 1 + } + fn write(&self, _user_buf: UserBuffer) -> usize { + panic!("Cannot write to stdin!"); + } + } + + impl File for Stdout { + fn read(&self, _user_buf: UserBuffer) -> usize{ + panic!("Cannot read from stdout!"); + } + fn write(&self, user_buf: UserBuffer) -> usize { + for buffer in user_buf.buffers.iter() { + print!("{}", core::str::from_utf8(*buffer).unwrap()); + } + user_buf.len() + } + } + +可以看到,标准输入 ``Stdin`` 只允许进程通过 ``read`` 从里面读入,目前每次仅支持读入一个字符,其实现与之前的 ``sys_read`` 基本相同,只是需要通过 ``UserBuffer`` 来获取具体将字节写入的位置。相反,标准输出 ``Stdout`` 只允许进程通过 ``write`` 写入到里面,实现方法是遍历每个切片,将其转化为字符串通过 ``print!`` 宏来输出。值得注意的是,如果有多核同时使用 ``print!`` 宏,将会导致两个不同的输出交错到一起造成输出混乱,后续我们还会对它做一些改进。 + +文件描述符与文件描述符表 +-------------------------------------------- + +.. chyyuu 可以解释一下文件描述符的起因??? + +每个进程都带有一个线性的 **文件描述符表** (File Descriptor Table) 记录所有它请求内核打开并可以读写的那些文件。而 **文件描述符** (File Descriptor) 则是一个非负整数,表示文件描述符表中一个打开的文件所处的位置。通过文件描述符,进程可以在自身的文件描述符表中找到对应的文件,并进行读写。当打开一个文件的时候,如果顺利,内核会返回给应用刚刚打开的文件的文件描述符;而当应用想关闭一个文件的时候,也需要向内核提供对应的文件描述符。 + +当一个进程被创建的时候,内核会默认为其打开三个文件: + +- 文件描述符为 0 的标准输入; +- 文件描述符为 1 的标准输出; +- 文件描述符为 2 的标准错误输出。 + +在我们的实现中并不区分标准输出和标准错误输出,而是会将文件描述符 1 和 2 均对应到标准输出。 + +这里隐含着有关文件描述符的一条重要的规则:即进程打开一个文件的时候,内核总是会将文件分配到该进程文件描述符表中 **最小的** 空闲位置。比如,当一个进程被创建以后立即打开一个文件,则内核总是会返回文件描述符 3 。当我们关闭一个打开的文件之后,它对应的文件描述符将会变得空闲并在后面可以被分配出去。 + +我们需要在进程控制块中加入文件描述符表的相应字段: + +.. code-block:: rust + :linenos: + :emphasize-lines: 12 + + // os/src/task/task.rs + + pub struct TaskControlBlockInner { + pub trap_cx_ppn: PhysPageNum, + pub base_size: usize, + pub task_cx_ptr: usize, + pub task_status: TaskStatus, + pub memory_set: MemorySet, + pub parent: Option>, + pub children: Vec>, + pub exit_code: i32, + pub fd_table: Vec>>, + } + +可以看到 ``fd_table`` 的类型包含多层嵌套,我们从外到里分别说明: + +- ``Vec`` 的动态长度特性使得我们无需设置一个固定的文件描述符数量上限,我们可以更加灵活的使用内存,而不必操心内存管理问题; +- ``Option`` 使得我们可以区分一个文件描述符当前是否空闲,当它是 ``None`` 的时候是空闲的,而 ``Some`` 则代表它已被占用; +- ``Arc`` 首先提供了共享引用能力。后面我们会提到,可能会有多个进程共享同一个文件对它进行读写。此外被它包裹的内容会被放到内核堆而不是栈上,于是它便不需要在编译期有着确定的大小; +- ``dyn`` 关键字表明 ``Arc`` 里面的类型实现了 ``File/Send/Sync`` 三个 Trait ,但是编译期无法知道它具体是哪个类型(可能是任何实现了 ``File`` Trait 的类型如 ``Stdin/Stdout`` ,故而它所占的空间大小自然也无法确定),需要等到运行时才能知道它的具体类型,对于一些抽象方法的调用也是在那个时候才能找到该类型实现的版本的地址并跳转过去。 + +.. note:: + + **Rust 语法卡片:Rust 中的多态** + + 在编程语言中, **多态** (Polymorphism) 指的是在同一段代码中可以隐含多种不同类型的特征。在 Rust 中主要通过泛型和 Trait 来实现多态。 + + 泛型是一种 **编译期多态** (Static Polymorphism),在编译一个泛型函数的时候,编译器会对于所有可能用到的类型进行实例化并对应生成一个版本的汇编代码,在编译期就能知道选取哪个版本并确定函数地址,这可能会导致生成的二进制文件体积较大;而 Trait 对象(也即上面提到的 ``dyn`` 语法)是一种 **运行时多态** (Dynamic Polymorphism),需要在运行时查一种类似于 C++ 中的 **虚表** (Virtual Table) 才能找到实际类型对于抽象接口实现的函数地址并进行调用,这样会带来一定的运行时开销,但是更为灵活。 + +当新建一个进程的时候,我们需要按照先前的说明为进程打开标准输入输出: + +.. code-block:: rust + :linenos: + :emphasize-lines: 18-25 + + // os/src/task/task.rs + + impl TaskControlBlock { + pub fn new(elf_data: &[u8]) -> Self { + ... + let task_control_block = Self { + pid: pid_handle, + kernel_stack, + inner: Mutex::new(TaskControlBlockInner { + trap_cx_ppn, + base_size: user_sp, + task_cx_ptr: task_cx_ptr as usize, + task_status: TaskStatus::Ready, + memory_set, + parent: None, + children: Vec::new(), + exit_code: 0, + fd_table: vec![ + // 0 -> stdin + Some(Arc::new(Stdin)), + // 1 -> stdout + Some(Arc::new(Stdout)), + // 2 -> stderr + Some(Arc::new(Stdout)), + ], + }), + }; + ... + } + } + +此外,在 fork 的时候,子进程需要完全继承父进程的文件描述符表来和父进程共享所有文件: + +.. code-block:: rust + :linenos: + :emphasize-lines: 8-16,29 + + // os/src/task/task.rs + + impl TaskControlBlock { + pub fn fork(self: &Arc) -> Arc { + ... + // push a goto_trap_return task_cx on the top of kernel stack + let task_cx_ptr = kernel_stack.push_on_top(TaskContext::goto_trap_return()); + // copy fd table + let mut new_fd_table: Vec>> = Vec::new(); + for fd in parent_inner.fd_table.iter() { + if let Some(file) = fd { + new_fd_table.push(Some(file.clone())); + } else { + new_fd_table.push(None); + } + } + let task_control_block = Arc::new(TaskControlBlock { + pid: pid_handle, + kernel_stack, + inner: Mutex::new(TaskControlBlockInner { + trap_cx_ppn, + base_size: parent_inner.base_size, + task_cx_ptr: task_cx_ptr as usize, + task_status: TaskStatus::Ready, + memory_set, + parent: Some(Arc::downgrade(self)), + children: Vec::new(), + exit_code: 0, + fd_table: new_fd_table, + }), + }); + // add child + ... + } + } + +这样,即使我们仅手动为初始进程 ``initproc`` 打开了标准输入输出,所有进程也都可以访问它们。 + +文件读写系统调用 +--------------------------------------------------- + +基于文件抽象接口和文件描述符表,我们终于可以让文件读写系统调用 ``sys_read/write`` 变得更加具有普适性,不仅仅局限于标准输入输出: + +.. code-block:: rust + + // os/src/syscall/fs.rs + + pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize { + let token = current_user_token(); + let task = current_task().unwrap(); + let inner = task.acquire_inner_lock(); + if fd >= inner.fd_table.len() { + return -1; + } + if let Some(file) = &inner.fd_table[fd] { + let file = file.clone(); + // release Task lock manually to avoid deadlock + drop(inner); + file.write( + UserBuffer::new(translated_byte_buffer(token, buf, len)) + ) as isize + } else { + -1 + } + } + + pub fn sys_read(fd: usize, buf: *const u8, len: usize) -> isize { + let token = current_user_token(); + let task = current_task().unwrap(); + let inner = task.acquire_inner_lock(); + if fd >= inner.fd_table.len() { + return -1; + } + if let Some(file) = &inner.fd_table[fd] { + let file = file.clone(); + // release Task lock manually to avoid deadlock + drop(inner); + file.read( + UserBuffer::new(translated_byte_buffer(token, buf, len)) + ) as isize + } else { + -1 + } + } + +我们都是在当前进程的文件描述符表中通过文件描述符找到某个文件,无需关心文件具体的类型,只要知道它一定实现了 ``File`` Trait 的 ``read/write`` 方法即可。Trait 对象提供的运行时多态能力会在运行的时候帮助我们定位到 ``read/write`` 的符合实际类型的实现。 \ No newline at end of file diff --git a/source/chapter6/2pipe.rst b/source/chapter6/2pipe.rst new file mode 100644 index 0000000000000000000000000000000000000000..2b38c101078a0c3fad181d2622b62aab9f19e352 --- /dev/null +++ b/source/chapter6/2pipe.rst @@ -0,0 +1,381 @@ +管道 +============================================ + +本节导读 +-------------------------------------------- + +本节我们基于上一节介绍的抽象文件接口 ``File`` 实现一种父子进程间的单向进程间通信机制——管道,同时实现两个新的系统调用 ``sys_pipe`` 和 ``sys_close`` 。 + +管道的系统调用原型及使用方法 +-------------------------------------------- + +首先来介绍什么是 **管道** (Pipe) 。我们可以将管道看成一个有一定缓冲区大小的字节队列,它分为读和写两端,需要通过不同的文件描述符来访问。读端只能用来从管道中读取,而写端只能用来将数据写入管道。由于管道是一个队列,读取的时候会从队头读取并弹出,而写入的时候则会写入到队列的队尾。同时,管道的缓冲区大小是有限的,一旦整个缓冲区都被填满就不能再继续写入,需要等到读端读取并从队列中弹出一些字符之后才能继续写入。当缓冲区为空的时候自然也不能继续从里面读取,需要等到写端写入了一些数据之后才能继续读取。 + +.. chyyuu 进一步介绍一下pipe的历史??? + +我们新增一个系统调用来为当前进程打开一个管道: + +.. code-block:: rust + + /// 功能:为当前进程打开一个管道。 + /// 参数:pipe 表示应用地址空间中的一个长度为 2 的 usize 数组的起始地址,内核需要按顺序将管道读端 + /// 和写端的文件描述符写入到数组中。 + /// 返回值:如果出现了错误则返回 -1,否则返回 0 。可能的错误原因是:传入的地址不合法。 + /// syscall ID:59 + pub fn sys_pipe(pipe: *mut usize) -> isize; + +在用户库中会将其包装为 ``pipe`` 函数: + +.. code-block:: rust + + // user/src/syscall.rs + + const SYSCALL_PIPE: usize = 59; + + pub fn sys_pipe(pipe: &mut [usize]) -> isize { + syscall(SYSCALL_PIPE, [pipe.as_mut_ptr() as usize, 0, 0]) + } + + // user/src/lib.rs + + pub fn pipe(pipe_fd: &mut [usize]) -> isize { sys_pipe(pipe_fd) } + +只有当一个管道的所有读端/写端都被关闭之后,管道占用的资源才会被回收,因此我们需要通过关闭文件的系统调用 ``sys_close`` 来尽可能早的关闭之后不再用到的读端和写端。 + +.. code-block:: rust + + /// 功能:当前进程关闭一个文件。 + /// 参数:fd 表示要关闭的文件的文件描述符。 + /// 返回值:如果成功关闭则返回 0 ,否则返回 -1 。可能的出错原因:传入的文件描述符并不对应一个打开的文件。 + /// syscall ID:57 + pub fn sys_close(fd: usize) -> isize; + +它会在用户库中被包装为 ``close`` 函数。 + +我们来从简单的管道测例 ``pipetest`` 中介绍管道的使用方法: + +.. code-block:: rust + :linenos: + + // user/src/bin/pipetest.rs + + #![no_std] + #![no_main] + + #[macro_use] + extern crate user_lib; + + use user_lib::{fork, close, pipe, read, write, wait}; + + static STR: &str = "Hello, world!"; + + #[no_mangle] + pub fn main() -> i32 { + // create pipe + let mut pipe_fd = [0usize; 2]; + pipe(&mut pipe_fd); + // read end + assert_eq!(pipe_fd[0], 3); + // write end + assert_eq!(pipe_fd[1], 4); + if fork() == 0 { + // child process, read from parent + // close write_end + close(pipe_fd[1]); + let mut buffer = [0u8; 32]; + let len_read = read(pipe_fd[0], &mut buffer) as usize; + // close read_end + close(pipe_fd[0]); + assert_eq!(core::str::from_utf8(&buffer[..len_read]).unwrap(), STR); + println!("Read OK, child process exited!"); + 0 + } else { + // parent process, write to child + // close read end + close(pipe_fd[0]); + assert_eq!(write(pipe_fd[1], STR.as_bytes()), STR.len() as isize); + // close write end + close(pipe_fd[1]); + let mut child_exit_code: i32 = 0; + wait(&mut child_exit_code); + assert_eq!(child_exit_code, 0); + println!("pipetest passed!"); + 0 + } + } + +在父进程中,我们通过 ``pipe`` 打开一个管道,于是 ``pipe_fd[0]`` 保存了管道读端的文件描述符,而 ``pipe_fd[1]`` 保存了管道写端的文件描述符。在 ``fork`` 之后,子进程会完全继承父进程的文件描述符表,于是子进程也可以通过同样的文件描述符来访问同一个管道的读端和写端。之前提到过管道是单向的,在这个测例中我们希望管道中的数据从父进程流向子进程,也即父进程仅通过管道的写端写入数据,而子进程仅通过管道的读端读取数据。 + +因此,我们分别在第 25 和第 34 行第一时间在子进程中关闭管道的写端和在父进程中关闭管道的读端。父进程在第 35 行将字符串 ``STR`` 写入管道的写端,随后在第 37 行关闭管道的写端;子进程在第 27 行从管道的读端读取字符串,并在第 29 行关闭。 + +如果想在父子进程之间实现双向通信,我们就必须创建两个管道。有兴趣的读者可以参考测例 ``pipe_large_test`` 。 + +通过 sys_close 关闭文件 +-------------------------------------------- + +关闭文件的系统调用 ``sys_close`` 实现非常简单,我们只需将进程控制块中的文件描述符表对应的一项改为 ``None`` 代表它已经空闲即可,同时这也会导致内层的引用计数类型 ``Arc`` 被销毁,会减少一个文件的引用计数,当引用计数减少到 0 之后文件所占用的资源就会被自动回收。 + +.. code-block:: rust + + // os/src/syscall/fs.rs + + pub fn sys_close(fd: usize) -> isize { + let task = current_task().unwrap(); + let mut inner = task.acquire_inner_lock(); + if fd >= inner.fd_table.len() { + return -1; + } + if inner.fd_table[fd].is_none() { + return -1; + } + inner.fd_table[fd].take(); + 0 + } + +管道的实现 +-------------------------------------------- + +我们将管道的一端(读端或写端)抽象为 ``Pipe`` 类型: + +.. code-block:: rust + + // os/src/fs/pipe.rs + + pub struct Pipe { + readable: bool, + writable: bool, + buffer: Arc>, + } + +``readable`` 和 ``writable`` 分别指出该管道端可否支持读取/写入,通过 ``buffer`` 字段还可以找到该管道端所在的管道自身。后续我们将为它实现 ``File`` Trait ,之后它便可以通过文件描述符来访问。 + +而管道自身,也就是那个带有一定大小缓冲区的字节队列,我们抽象为 ``PipeRingBuffer`` 类型: + +.. code-block:: rust + + // os/src/fs/pipe.rs + + const RING_BUFFER_SIZE: usize = 32; + + #[derive(Copy, Clone, PartialEq)] + enum RingBufferStatus { + FULL, + EMPTY, + NORMAL, + } + + pub struct PipeRingBuffer { + arr: [u8; RING_BUFFER_SIZE], + head: usize, + tail: usize, + status: RingBufferStatus, + write_end: Option>, + } + +- ``RingBufferStatus`` 记录了缓冲区目前的状态:``FULL`` 表示缓冲区已满不能再继续写入; ``EMPTY`` 表示缓冲区为空无法从里面读取;而 ``NORMAL`` 则表示除了 ``FULL`` 和 ``EMPTY`` 之外的其他状态。 +- ``PipeRingBuffer`` 的 ``arr/head/tail`` 三个字段用来维护一个循环队列,其中 ``arr`` 为存放数据的数组, ``head`` 为循环队列队头的下标, ``tail`` 为循环队列队尾的下标。 +- ``PipeRingBuffer`` 的 ``write_end`` 字段还保存了它的写端的一个弱引用计数,这是由于在某些情况下需要确认该管道所有的写端是否都已经被关闭了,通过这个字段很容易确认这一点。 + +从内存管理的角度,每个读端或写端中都保存着所属管道自身的强引用计数,且我们确保这些引用计数只会出现在管道端口 ``Pipe`` 结构体中。于是,一旦一个管道所有的读端和写端均被关闭,便会导致它们所属管道的引用计数变为 0 ,循环队列缓冲区所占用的资源被自动回收。虽然 ``PipeRingBuffer`` 中保存了一个指向写端的引用计数,但是它是一个弱引用,也就不会出现循环引用的情况导致内存泄露。 + +.. chyyuu 介绍弱引用??? + +管道创建 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +通过 ``PipeRingBuffer::new`` 可以创建一个新的管道: + +.. code-block:: rust + + // os/src/fs/pipe.rs + + impl PipeRingBuffer { + pub fn new() -> Self { + Self { + arr: [0; RING_BUFFER_SIZE], + head: 0, + tail: 0, + status: RingBufferStatus::EMPTY, + write_end: None, + } + } + } + +``Pipe`` 的 ``read/write_end_with_buffer`` 方法可以分别从一个已有的管道创建它的读端和写端: + +.. code-block:: rust + + // os/src/fs/pipe.rs + + impl Pipe { + pub fn read_end_with_buffer(buffer: Arc>) -> Self { + Self { + readable: true, + writable: false, + buffer, + } + } + pub fn write_end_with_buffer(buffer: Arc>) -> Self { + Self { + readable: false, + writable: true, + buffer, + } + } + } + +可以看到,读端和写端的访问权限进行了相应设置:不允许向读端写入,也不允许从写端读取。 + +通过 ``make_pipe`` 方法可以创建一个管道并返回它的读端和写端: + +.. code-block:: rust + + // os/src/fs/pipe.rs + + impl PipeRingBuffer { + pub fn set_write_end(&mut self, write_end: &Arc) { + self.write_end = Some(Arc::downgrade(write_end)); + } + } + + /// Return (read_end, write_end) + pub fn make_pipe() -> (Arc, Arc) { + let buffer = Arc::new(Mutex::new(PipeRingBuffer::new())); + let read_end = Arc::new( + Pipe::read_end_with_buffer(buffer.clone()) + ); + let write_end = Arc::new( + Pipe::write_end_with_buffer(buffer.clone()) + ); + buffer.lock().set_write_end(&write_end); + (read_end, write_end) + } + +注意,我们调用 ``PipeRingBuffer::set_write_end`` 在管道中保留它的写端的弱引用计数。 + +现在来实现创建管道的系统调用 ``sys_pipe`` : + +.. code-block:: rust + :linenos: + + // os/src/task/task.rs + + impl TaskControlBlockInner { + pub fn alloc_fd(&mut self) -> usize { + if let Some(fd) = (0..self.fd_table.len()) + .find(|fd| self.fd_table[*fd].is_none()) { + fd + } else { + self.fd_table.push(None); + self.fd_table.len() - 1 + } + } + } + + // os/src/syscall/fs.rs + + pub fn sys_pipe(pipe: *mut usize) -> isize { + let task = current_task().unwrap(); + let token = current_user_token(); + let mut inner = task.acquire_inner_lock(); + let (pipe_read, pipe_write) = make_pipe(); + let read_fd = inner.alloc_fd(); + inner.fd_table[read_fd] = Some(pipe_read); + let write_fd = inner.alloc_fd(); + inner.fd_table[write_fd] = Some(pipe_write); + *translated_refmut(token, pipe) = read_fd; + *translated_refmut(token, unsafe { pipe.add(1) }) = write_fd; + 0 + } + +``TaskControlBlockInner::alloc_fd`` 可以在进程控制块中分配一个最小的空闲文件描述符来访问一个新打开的文件。它先从小到大遍历所有曾经被分配过的文件描述符尝试找到一个空闲的,如果没有的话就需要拓展文件描述符表的长度并新分配一个。 + +在 ``sys_pipe`` 中,第 21 行我们调用 ``make_pipe`` 创建一个管道并获取其读端和写端,第 22~25 行我们分别为读端和写端分配文件描述符并将它们放置在文件描述符表中的相应位置中。第 26~27 行我们则是将读端和写端的文件描述符写回到应用地址空间。 + +管道读写 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +首先来看如何为 ``Pipe`` 实现 ``File`` Trait 的 ``read`` 方法,即从管道的读端读取数据。在此之前,我们需要对于管道循环队列进行封装来让它更易于使用: + +.. code-block:: rust + :linenos: + + // os/src/fs/pipe.rs + + impl PipeRingBuffer { + pub fn read_byte(&mut self) -> u8 { + self.status = RingBufferStatus::NORMAL; + let c = self.arr[self.head]; + self.head = (self.head + 1) % RING_BUFFER_SIZE; + if self.head == self.tail { + self.status = RingBufferStatus::EMPTY; + } + c + } + pub fn available_read(&self) -> usize { + if self.status == RingBufferStatus::EMPTY { + 0 + } else { + if self.tail > self.head { + self.tail - self.head + } else { + self.tail + RING_BUFFER_SIZE - self.head + } + } + } + pub fn all_write_ends_closed(&self) -> bool { + self.write_end.as_ref().unwrap().upgrade().is_none() + } + } + +``PipeRingBuffer::read_byte`` 方法可以从管道中读取一个字节,注意在调用它之前需要确保管道缓冲区中不是空的。它会更新循环队列队头的位置,并比较队头和队尾是否相同,如果相同的话则说明管道的状态变为空 ``EMPTY`` 。仅仅通过比较队头和队尾是否相同不能确定循环队列是否为空,因为它既有可能表示队列为空,也有可能表示队列已满。因此我们需要在 ``read_byte`` 的同时进行状态更新。 + +``PipeRingBuffer::available_read`` 可以计算管道中还有多少个字符可以读取。我们首先需要需要判断队列是否为空,因为队头和队尾相等可能表示队列为空或队列已满,两种情况 ``available_read`` 的返回值截然不同。如果队列为空的话直接返回 0,否则根据队头和队尾的相对位置进行计算。 + +``PipeRingBuffer::all_write_ends_closed`` 可以判断管道的所有写端是否都被关闭了,这是通过尝试将管道中保存的写端的弱引用计数升级为强引用计数来实现的。如果升级失败的话,说明管道写端的强引用计数为 0 ,也就意味着管道所有写端都被关闭了,从而管道中的数据不会再得到补充,待管道中仅剩的数据被读取完毕之后,管道就可以被销毁了。 + +下面是 ``Pipe`` 的 ``read`` 方法的实现: + +.. code-block:: rust + :linenos: + + // os/src/fs/pipe.rs + + impl File for Pipe { + fn read(&self, buf: UserBuffer) -> usize { + assert_eq!(self.readable, true); + let mut buf_iter = buf.into_iter(); + let mut read_size = 0usize; + loop { + let mut ring_buffer = self.buffer.lock(); + let loop_read = ring_buffer.available_read(); + if loop_read == 0 { + if ring_buffer.all_write_ends_closed() { + return read_size; + } + drop(ring_buffer); + suspend_current_and_run_next(); + continue; + } + // read at most loop_read bytes + for _ in 0..loop_read { + if let Some(byte_ref) = buf_iter.next() { + unsafe { *byte_ref = ring_buffer.read_byte(); } + read_size += 1; + } else { + return read_size; + } + } + } + } + } + +- 第 6 行的 ``buf_iter`` 将传入的应用缓冲区 ``buf`` 转化为一个能够逐字节对于缓冲区进行访问的迭代器,每次调用 ``buf_iter.next()`` 即可按顺序取出用于访问缓冲区中一个字节的裸指针。 +- 第 7 行的 ``read_size`` 用来维护实际有多少字节从管道读入应用的缓冲区。 +- ``File::read`` 的语义是要从文件中最多读取应用缓冲区大小那么多字符。这可能超出了循环队列的大小,或者由于尚未有进程从管道的写端写入足够的字符,因此我们需要将整个读取的过程放在一个循环中,当循环队列中不存在足够字符的时候暂时进行任务切换,等待循环队列中的字符得到补充之后再继续读取。 + + 这个循环从第 8 行开始,第 10 行我们用 ``loop_read`` 来保存循环这一轮次中可以从管道循环队列中读取多少字符。如果管道为空则会检查管道的所有写端是否都已经被关闭,如果是的话,说明我们已经没有任何字符可以读取了,这时可以直接返回;否则我们需要等管道的字符得到填充之后再继续读取,因此我们调用 ``suspend_current_and_run_next`` 切换到其他任务,等到切换回来之后回到循环开头再看一下管道中是否有字符了。在调用之前我们需要手动释放管道自身的锁,因为切换任务时候的 ``__switch`` 并不是一个正常的函数调用。 + + 如果 ``loop_read`` 不为 0 ,在这一轮次中管道中就有 ``loop_read`` 个字节可以读取。我们可以迭代应用缓冲区中的每个字节指针并调用 ``PipeRingBuffer::read_byte`` 方法来从管道中进行读取。如果这 ``loop_read`` 个字节均被读取之后还没有填满应用缓冲区就需要进入循环的下一个轮次,否则就可以直接返回了。 + +``Pipe`` 的 ``write`` 方法——即通过管道的写端向管道中写入数据的实现和 ``read`` 的原理类似,篇幅所限在这里不再赘述,感兴趣的读者可自行参考其实现。 \ No newline at end of file diff --git a/source/chapter6/3exercise.rst b/source/chapter6/3exercise.rst new file mode 100644 index 0000000000000000000000000000000000000000..c23bb4898911a9389f2ad4338d5a49c18d8ba751 --- /dev/null +++ b/source/chapter6/3exercise.rst @@ -0,0 +1,89 @@ +chapter6 练习 +=========================================== + +- 本节难度: **也就和lab3一样吧** + +编程作业 +------------------------------------------- + +进程通信:邮件 ++++++++++++++++++++++++++++++++++++++++++++ + +这一章我们实现了基于 pipe 的进程间通信,但是看测例就知道了,管道不太自由,我们来实现一套乍一看更靠谱的通信 syscall吧!本节要求实现邮箱机制,以及对应的 syscall。 + +- 邮箱说明:每个进程拥有唯一一个邮箱,基于“数据报”收发字节信息,利用环形buffer存储,读写顺序为 FIFO,不记录来源进程。每次读写单位必须为一个报文,如果用于接收的缓冲区长度不够,舍弃超出的部分(截断报文)。为了简单,邮箱中最多拥有16条报文,每条报文最大长度256字节。当邮箱满时,发送邮件(也就是写邮箱会失败)。不考虑读写邮箱的权限,也就是所有进程都能够随意给其他进程的邮箱发报。 + +**mailread**: + + * syscall ID:401 + * C接口: ``int mailread(void* buf, int len)`` + * Rust接口: ``fn mailread(buf: *mut u8, len: usize)`` + * 功能:读取一个报文,如果成功返回报文长度. + * 参数: + * buf: 缓冲区头。 + * len:缓冲区长度。 + * 说明: + * len > 256 按 256 处理,len < 队首报文长度且不为0,则截断报文。 + * len = 0,则不进行读取,如果没有报文读取,返回-1,否则返回0,这是用来测试是否有报文可读。 + * 可能的错误: + * 邮箱空。 + * buf 无效。 + +**mailwrite**: + + * syscall ID:402 + * C接口: ``int mailwrite(int pid, void* buf, int len)`` + * Rust接口: ``fn mailwrite(pid: usize, buf: *mut u8, len: usize)`` + * 功能:向对应进程邮箱插入一条报文. + * 参数: + * pid: 目标进程id。 + * buf: 缓冲区头。 + * len:缓冲区长度。 + * 说明: + * len > 256 按 256 处理, + * len = 0,则不进行写入,如果邮箱满,返回-1,否则返回0,这是用来测试是否可以发报。 + * 可以向自己的邮箱写入报文。 + * 可能的错误: + * 邮箱满。 + * buf 无效。 + +实验要求 ++++++++++++++++++++++++++++++++++++++++++++++ + +- 实现分支:ch6。 +- 完成实验指导书中的内容,实现进程控制,可以基于 pipe 进行进程通信。 +- 实现邮箱机制及系统调用,并通过 `Rust测例 `_ 中 chapter6 对应的所有测例。 + +challenge: 支持多核。 + +实验检查 +++++++++++++++++++++++++++++++++++++++++++++++ + +- 实验目录要求 + + 目录要求不变(参考lab1目录或者示例代码目录结构)。同样在 os 目录下 ``make run`` 之后可以正确加载用户程序并执行。 + + 加载的用户测例位置: ``../user/build/bin``。 + +- 检查 + + 可以正确 ``make run`` 执行,可以正确执行目标用户测例,并得到预期输出(详见测例注释)。 + +问答作业 +------------------------------------------- + +(1) 举出使用 pipe 的一个实际应用的例子。 + +(2) 假设我们的邮箱现在有了更加强大的功能,容量大幅增加而且记录邮件来源,可以实现“回信”。考虑一个多核场景,有 m 个核为消费者,n 个为生产者,消费者通过邮箱向生产者提出订单,生产者通过邮箱回信给出产品。 + + - 假设你的邮箱实现没有使用锁等机制进行保护,在多核情景下可能会发生哪些问题?单核一定不会发生问题吗?为什么? + - 请结合你在课堂上学到的内容,描述读者写者问题的经典解决方案,必要时提供伪代码。 + - 由于读写是基于报文的,不是随机读写,你有什么点子来优化邮箱的实现吗? + + +报告要求 +--------------------------------------- + +* 简单总结本次实验与上个实验相比你增加的东西。(控制在5行以内,不要贴代码) +* 完成问答问题 +* (optional) 你对本次实验设计及难度的看法。 \ No newline at end of file diff --git a/source/chapter6/index.rst b/source/chapter6/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..9cef196b0e8027fb93d953c97247b43dab6920e6 --- /dev/null +++ b/source/chapter6/index.rst @@ -0,0 +1,12 @@ +第六章:文件描述符与进程间通信 +============================================== + +.. toctree:: + :maxdepth: 4 + + 0intro + 1file-descriptor + 2pipe + 3exercise + +有团队协作能力的“迅猛龙”操作系统。 \ No newline at end of file diff --git a/source/chapter7/0intro.rst b/source/chapter7/0intro.rst new file mode 100644 index 0000000000000000000000000000000000000000..c9995e67919f55ce5096a39c6bdf41cda1b8bbb7 --- /dev/null +++ b/source/chapter7/0intro.rst @@ -0,0 +1,209 @@ +引言 +========================================= + +本章导读 +----------------------------------------- + +在第六章中,我们为进程引入了文件的抽象,使得进程能够通过一个统一的接口来读写内核管理的多种不同的 I/O 资源。作为例子,我们实现了匿名管道,并通过它进行了简单的父子进程间的单向通信。 + +本章我们对一种非常重要的外设—— **持久存储设备** (Persistent Storage) 进行管理。事实上,大名鼎鼎的 MS-DOS 是 Disk Operating System 的缩写,在那个时候硬盘就已经成为操作系统管理的主要系统组件之一。在此之前我们仅使用一种存储,也就是内存(或称 RAM)。相比内存,持久存储设备的读写速度较慢,容量较大,且掉电之后并不会丢失数据。如果没有持久存储设备的话,我们在内核上的任何活动记录全部只能保存在内存上,一旦掉电这些数据将会全部丢失。因此,将需要持久保存的数据从内存写入到持久存储设备,或是从持久存储读入到内存是应用必不可少的一种需求。常见的持久存储设备有硬盘、磁盘以及固态硬盘 (SSD, Solid-State Drive) 等。 + +对于应用访问持久存储设备的需求,内核需要新增两种文件:标准文件和目录,它们均以文件系统的形式被组织并保存在持久存储设备上。 + +实践体验 +----------------------------------------- + +获取本章代码: + +.. code-block:: console + + $ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git + $ cd rCore-Tutorial-v3 + $ git checkout ch7 + +在 qemu 模拟器上运行本章代码: + +.. code-block:: console + + $ cd os + $ make run + +若要在 k210 平台上运行,首先需要将 microSD 通过读卡器插入 PC ,然后将打包应用 ELF 的文件系统镜像烧写到 microSD 中: + +.. code-block:: console + + $ cd os + $ make sdcard + Are you sure write to /dev/sdb ? [y/N] + y + 16+0 records in + 16+0 records out + 16777216 bytes (17 MB, 16 MiB) copied, 1.76044 s, 9.5 MB/s + 8192+0 records in + 8192+0 records out + 4194304 bytes (4.2 MB, 4.0 MiB) copied, 3.44472 s, 1.2 MB/s + +途中需要输入 ``y`` 确认将文件系统烧写到默认的 microSD 所在位置 ``/dev/sdb`` 中。这个位置可以在 ``os/Makefile`` 中的 ``SDCARD`` 处进行修改,在烧写之前请确认它被正确配置为 microSD 的实际位置,否则可能会造成数据损失。 + +烧写之后,将 microSD 插入到 Maix 系列开发板并连接到 PC,然后在开发板上运行本章代码: + +.. code-block:: console + + $ cd os + $ make run BOARD=k210 + +内核初始化完成之后就会进入用户终端,在这里我们运行一下本章的测例 ``filetest_simple`` : + +.. code-block:: + + >> filetest_simple + file_test passed! + Shell: Process 2 exited with code 0 + >> + +它会将 ``Hello, world!`` 输出到另一个文件 ``filea`` ,并读取里面的内容确认输出正确。我们也可以通过命令行工具 ``cat`` 来更直观的查看 ``filea`` 中的内容: + +.. code-block:: + + >> cat filea + Hello, world! + Shell: Process 2 exited with code 0 + >> + +此外,在本章我们为用户终端支持了输入/输出重定向功能,可以将一个应用的输出保存到一个指定的文件。例如,下面的命令可以将 ``yield`` 应用的输出保存在文件 ``fileb`` 当中,并在应用执行完毕之后确认它的输出: + +.. code-block:: + + >> yield > fileb + Shell: Process 2 exited with code 0 + >> cat fileb + Hello, I am process 2. + Back in process 2, iteration 0. + Back in process 2, iteration 1. + Back in process 2, iteration 2. + Back in process 2, iteration 3. + Back in process 2, iteration 4. + yield pass. + + Shell: Process 2 exited with code 0 + >> + +本章代码树 +----------------------------------------- + +.. code-block:: + :linenos: + :emphasize-lines: 45 + + ├── bootloader + │   ├── rustsbi-k210.bin + │   └── rustsbi-qemu.bin + ├── Dockerfile + ├── easy-fs(新增:从内核中独立出来的一个简单的文件系统 EasyFileSystem 的实现) + │   ├── Cargo.toml + │   └── src + │   ├── bitmap.rs(位图抽象) + │   ├── block_cache.rs(块缓存层,将块设备中的部分块缓存在内存中) + │   ├── block_dev.rs(声明块设备抽象接口 BlockDevice,需要库的使用者提供其实现) + │   ├── efs.rs(实现整个 EasyFileSystem 的磁盘布局) + │   ├── layout.rs(一些保存在磁盘上的数据结构的内存布局) + │   ├── lib.rs + │   └── vfs.rs(提供虚拟文件系统的核心抽象,即索引节点 Inode) + ├── easy-fs-fuse(新增:将当前 OS 上的应用可执行文件按照 easy-fs 的格式进行打包) + │   ├── Cargo.toml + │   └── src + │   └── main.rs + ├── LICENSE + ├── Makefile + ├── os + │   ├── build.rs + │   ├── Cargo.toml(修改:新增 Qemu 和 K210 两个平台的块设备驱动依赖 crate) + │   ├── Makefile(修改:新增文件系统的构建流程) + │   └── src + │   ├── config.rs(修改:新增访问块设备所需的一些 MMIO 配置) + │   ├── console.rs + │   ├── drivers(修改:新增 Qemu 和 K210 两个平台的块设备驱动) + │   │   ├── block + │   │   │   ├── mod.rs(将不同平台上的块设备全局实例化为 BLOCK_DEVICE 提供给其他模块使用) + │   │   │   ├── sdcard.rs(K210 平台上的 microSD 块设备) + │   │   │   └── virtio_blk.rs(Qemu 平台的 virtio-blk 块设备) + │   │   └── mod.rs + │   ├── entry.asm + │   ├── fs(修改:在文件系统中新增标准文件的支持) + │   │   ├── inode.rs(新增:将 easy-fs 提供的 Inode 抽象封装为内核看到的 OSInode + │   │   │ 并实现 fs 子模块的 File Trait) + │   │   ├── mod.rs + │   │   ├── pipe.rs + │   │   └── stdio.rs + │   ├── lang_items.rs + │   ├── link_app.S + │   ├── linker-k210.ld + │   ├── linker-qemu.ld + │   ├── loader.rs(移除:应用加载器 loader 子模块,本章开始从文件系统中加载应用) + │   ├── main.rs + │   ├── mm + │   │   ├── address.rs + │   │   ├── frame_allocator.rs + │   │   ├── heap_allocator.rs + │   │   ├── memory_set.rs(修改:在创建地址空间的时候插入 MMIO 虚拟页面) + │   │   ├── mod.rs + │   │   └── page_table.rs + │   ├── sbi.rs + │   ├── syscall + │   │   ├── fs.rs(修改:新增 sys_open/sys_dup) + │   │   ├── mod.rs + │   │   └── process.rs(修改:sys_exec 改为从文件系统中加载 ELF,并支持命令行参数) + │   ├── task + │   │   ├── context.rs + │   │   ├── manager.rs + │   │   ├── mod.rs(修改初始进程 INITPROC 的初始化) + │   │   ├── pid.rs + │   │   ├── processor.rs + │   │   ├── switch.rs + │   │   ├── switch.S + │   │   └── task.rs + │   ├── timer.rs + │   └── trap + │   ├── context.rs + │   ├── mod.rs + │   └── trap.S + ├── README.md + ├── rust-toolchain + ├── tools + │   ├── kflash.py + │   ├── LICENSE + │   ├── package.json + │   ├── README.rst + │   └── setup.py + └── user + ├── Cargo.lock + ├── Cargo.toml + ├── Makefile + └── src + ├── bin + │   ├── cat.rs(新增) + │   ├── cmdline_args.rs(新增) + │   ├── exit.rs + │   ├── fantastic_text.rs + │   ├── filetest_simple.rs(新增:简单文件系统测例) + │   ├── forktest2.rs + │   ├── forktest.rs + │   ├── forktest_simple.rs + │   ├── forktree.rs + │   ├── hello_world.rs + │   ├── initproc.rs + │   ├── matrix.rs + │   ├── pipe_large_test.rs + │   ├── pipetest.rs + │   ├── run_pipe_test.rs + │   ├── sleep.rs + │   ├── sleep_simple.rs + │   ├── stack_overflow.rs + │   ├── user_shell.rs(修改:支持命令行参数解析和输入/输出重定向) + │   ├── usertests.rs + │   └── yield.rs + ├── console.rs + ├── lang_items.rs + ├── lib.rs(修改:支持命令行参数解析) + ├── linker.ld + └── syscall.rs(修改:新增 sys_open 和 sys_dup) \ No newline at end of file diff --git a/source/chapter7/1fs-interface.rst b/source/chapter7/1fs-interface.rst new file mode 100644 index 0000000000000000000000000000000000000000..ff5601f52b1beb9a0bed405a19277e6fbe5de4a3 --- /dev/null +++ b/source/chapter7/1fs-interface.rst @@ -0,0 +1,246 @@ +文件系统接口 +================================================= + +本节导读 +------------------------------------------------- + +本节我们首先介绍 Linux 上的标准文件和目录对用户来说值得注意的地方及使用方法。由于 Linux 上的文件系统模型还是比较复杂,在我们的内核实现中对它进行了很大程度的简化,我们会对简化的具体情形进行介绍。最后,我们介绍我们内核上应用的开发者应该如何使用我们简化后的文件系统和一些相关知识。 + +Linux 标准文件和目录 +------------------------------------------------- + +标准文件 ++++++++++++++++++++++++++++++++++++++++++++++++++ + +在操作系统的用户看来,标准文件是保存在持久存储设备上的一个字节序列,每个标准文件都有一个 **文件名** (Filename) ,用户需要通过它来区分不同的标准文件。方便起见,在下面的描述中,“文件”有可能指的是包括标准文件在内的若干种进程可以读写的 I/O 资源,也有可能指的是标准文件本身,请读者自行根据上下文判断取哪种含义。 + +在 Linux 系统上, ``stat`` 工具可以获取文件的一些信息。下面以我们项目中的一个源代码文件 ``os/src/main.rs`` 为例: + +.. code-block:: console + + $ cd os/src/ + $ stat main.rs + File: main.rs + Size: 940 Blocks: 8 IO Block: 4096 regular file + Device: 801h/2049d Inode: 4975 Links: 1 + Access: (0644/-rw-r--r--) Uid: ( 1000/ oslab) Gid: ( 1000/ oslab) + Access: 2021-02-28 23:32:50.289925450 +0800 + Modify: 2021-02-28 23:32:50.133927136 +0800 + Change: 2021-02-28 23:32:50.133927136 +0800 + Birth: - + +``stat`` 工具展示了 ``main.rs`` 的如下信息: + +- File 表明它的文件名为 ``main.rs`` 。 +- Size 表明它的字节大小为 940 字节。 +- Blocks 表明它占据 8 个 **块** (Block) 来存储。在文件系统中,文件的数据以块为单位进行存储,在 IO Block 可以看出在 Ubuntu 系统中每个块的大小为 4096 字节。 +- regular file 表明这个文件是一个标准文件。事实上,其他类型的文件也可以通过文件名来进行访问。 +- 当文件是一个特殊文件(如块设备文件或者字符设备文件的时候),Device 将指出该特殊文件的 major/minor ID 。对于一个标准文件,我们无需关心它。 +- Inode 表示文件的底层编号。在文件系统的底层实现中,并不是直接通过文件名来索引文件,而是首先需要将文件名转化为文件的底层编号,再根据这个编号去索引文件。然而,用户无需关心这一信息。 +- Links 给出文件的硬链接数。有关软硬链接的相关知识我们在这里不进行介绍,有兴趣的读者可以自行搜索了解。 +- Uid 给出该文件的所属的用户 ID , Gid 给出该文件所属的用户组 ID 。Access 的其中一种表示是一个长度为 10 的字符串(这里是 ``-rw-r--r--`` ),其中第 1 位给出该文件的类型,这个文件是一个标准文件,因此这第 1 位为 ``-`` 。后面的 9 位可以分为三组,分别表示该文件的所有者/在该文件所属的用户组内的其他用户以及剩下的所有用户能够读取/写入/将该文件作为一个可执行文件来执行。 +- Access/Modify 分别给出该文件的最近一次访问/最近一次修改时间。 + +如果我们使用 ``stat`` 工具查看我们构建的一个能在我们的内核上执行的应用 ELF 可执行文件: + +.. code-block:: console + + $ cd user/target/riscv64gc-unknown-none-elf/release/ + $ stat user_shell + File: user_shell + Size: 85712 Blocks: 168 IO Block: 4096 regular file + Device: 801h/2049d Inode: 1460936 Links: 2 + Access: (0755/-rwxr-xr-x) Uid: ( 1000/ oslab) Gid: ( 1000/ oslab) + Access: 2021-03-01 11:21:34.785309066 +0800 + Modify: 2021-03-01 11:21:32.829332116 +0800 + Change: 2021-03-01 11:21:32.833332069 +0800 + Birth: - + +从中可以看出我们构建的应用体积大概在数十 KiB 量级。它的 Access 指出所有用户均可将其作为一个可执行文件在当前 OS 中加载并执行。然而这仅仅是能够通过权限检查而已,这个应用只有在我们自己的内核上才能真正被加载运行。 + +用户常常通过文件的 **拓展名** (Filename extension) 来推断该文件的用途,如 ``main.rs`` 的拓展名是 ``.rs`` ,我们由此知道它是一个 Rust 源代码文件。但从内核的角度来看,它会将所有文件无差别的看成一个字节序列,文件内容的结构和含义则是交给对应的应用进行解析。 + +目录 ++++++++++++++++++++++++++++++++++++++++++++++++++ + +最早的文件系统仅仅通过文件名来区分文件,但是这会造成一些归档和管理上的困难。如今我们的使用习惯是将文件根据功能、属性的不同分类归档到不同层级的目录之下。这样我们就很容易逐级找到想要的文件。结合用户和用户组的概念,目录的存在也使得权限控制更加容易,只需要对于目录进行设置就可以间接设置用户/用户组对该目录下所有文件的访问权限,这使得操作系统能够更加安全的支持多用户。 + +同样可以通过 ``stat`` 工具获取目录的一些信息: + +.. code-block:: console + + $ stat os + File: os + Size: 4096 Blocks: 8 IO Block: 4096 directory + Device: 801h/2049d Inode: 4982 Links: 5 + Access: (0755/drwxr-xr-x) Uid: ( 1000/ oslab) Gid: ( 1000/ oslab) + Access: 2021-02-28 23:32:50.133927136 +0800 + Modify: 2021-02-28 23:32:50.129927180 +0800 + Change: 2021-02-28 23:32:50.129927180 +0800 + Birth: - + +directory 表明 ``os`` 是一个目录,从 Access 字符串的首位 ``d`` 也可以看出这一点。对于目录而言, Access 的 ``rwx`` 含义有所不同: + +- ``r`` 表示是否允许获取该目录下有哪些文件和子目录; +- ``w`` 表示是否允许在该目录下创建/删除文件和子目录; +- ``x`` 表示是否允许“通过”该目录。 + +Blocks 给出 ``os`` 目录也占用 8 个块进行存储。实际上目录也可以看作一种标准文件,它也有属于自己的底层编号,它的内容中保存着若干 **目录项** (Dirent, Directory Entry) ,可以看成一组映射,根据它下面的文件或子目录的文件名或目录名能够查到文件和子目录在文件系统中的底层编号,即 Inode 编号。但是与标准文件不同的是,用户无法 **直接** 修改目录的内容,只能通过创建/删除它下面的文件或子目录才能间接做到这一点。 + +有了目录之后,我们就可以将所有的文件和目录组织为一种被称为 **目录树** (Directory Tree) 的有根树结构(不考虑软链接)。树中的每个节点都是一个文件或目录,一个目录下面的所有的文件和子目录都是它的孩子。可以看出所有的文件都是目录树的叶子节点。目录树的根节点也是一个目录,它被称为 **根目录** (Root Directory)。目录树中的每个目录和文件都可以用它的 **绝对路径** (Absolute Path) 来进行索引,该绝对路径是目录树上的根节点到待索引的目录和文件所在的节点之间自上而下的路径上的所有节点的文件或目录名两两之间加上路径分隔符拼接得到的。例如,在 Linux 上,根目录的绝对路径是 ``/`` ,路径分隔符也是 ``/`` ,因此: + +- ``main.rs`` 的绝对路径是 ``/home/oslab/workspace/v3/rCore-Tutorial-v3/os/src/main.rs`` ; +- ``os`` 目录的绝对路径则是 ``/home/oslab/workspace/v3/rCore-Tutorial-v3/os/`` 。 + +上面的绝对路径因具体环境而异。 + +一般情况下,绝对路径都很长,用起来颇为不便。而且,在日常使用中,我们通常固定在一个工作目录下而不会频繁切换目录。因此更为常用的是 **相对路径** (Relative Path) 而非绝对路径。每个进程都会记录自己当前所在的工作目录,当它在索引文件或目录的时候,如果传给它的路径并未以 ``/`` 开头则会被内核认为是一个相对于进程当前工作目录的相对路径,这个路径会被拼接在进程当前路径的后面组成一个绝对路径,实际索引的是这个绝对路径对应的文件或目录。其中, ``./`` 表示当前目录,而 ``../`` 表示当前目录的父目录,这在通过相对路径进行索引的时候非常实用。在使用终端的时候, ``pwd`` 工具可以打印终端进程当前所在的目录,而通过 ``cd`` 可以切换终端进程的工作目录。 + +一旦引入目录之后,我们就不再单纯的通过文件名来索引文件,而是通过路径(绝对或相对)进行索引。在文件系统的底层实现中,也是对应的先将路径转化为一个文件或目录的底层编号,然后再通过这个编号具体索引文件或目录。将路径转化为底层编号的过程是逐级进行的,对于绝对路径的情况,需要从根目录出发,每次根据当前目录底层编号获取到它的内容,根据下一级子目录的目录名查到该子目录的底层编号,然后从该子目录继续向下遍历,依此类推。在这个过程目录的权限控制位将会起到保护作用,阻止无权限用户进行访问。 + +.. note:: + + **目录是否有必要存在** + + 基于路径的索引难以并行或分布式化,因为我们总是需要查到一级目录的底层编号才能查到下一级,这是一个天然串行的过程。在一些性能需求极高的环境中,可以考虑弱化目录的权限控制职能,将目录树结构扁平化,将文件系统的磁盘布局变为类键值对存储。 + +文件系统 ++++++++++++++++++++++++++++++++++++++++++++++++++ + +标准文件和目录都是实际保存在持久存储设备中的。持久存储设备仅支持以扇区为单位的随机读写,这和上面介绍的通过路径即可索引到文件并进行读写的用户视角有很大的不同。负责中间转换的便是 **文件系统** (File System) 。具体而言,文件系统负责将逻辑上的目录树结构(包括其中每个文件或目录的数据和其他信息)映射到持久存储设备上,决定设备上的每个扇区各应存储哪些内容。反过来,文件系统也可以从持久存储设备还原出逻辑上的目录树结构。 + +文件系统有很多种不同的实现,每一种都能将同一个逻辑上目录树结构转化为一个不同的持久存储设备上的扇区布局。最著名的文件系统有 Windows 上的 FAT/NTFS 和 Linux 上的 ext3/ext4 。 + +在一个复杂的计算机系统中,可以同时包含多个持久存储设备,它们上面的数据可能是以不同文件系统格式存储的。为了能够对它们进行统一管理,在内核中有一层 **虚拟文件系统** (VFS, Virtual File System) ,它规定了逻辑上目录树结构的通用格式及相关操作的抽象接口,只要不同的底层文件系统均实现虚拟文件系统要求的那些抽象接口,再加上 **挂载** (Mount) 等方式,这些持久存储设备上的不同文件系统便可以用一个统一的逻辑目录树结构一并进行管理。 + +.. _fs-simplification: + +简易文件与目录抽象 +------------------------------------------------- + + +我们的内核实现对于目录树结构进行了很大程度上的简化,这样做的目的是为了能够完整的展示文件系统的工作原理,但代码量又不至于太多。我们进行的简化如下: + +- 扁平化:仅存在根目录 ``/`` 一个目录,剩下所有的文件都放在根目录内。在索引一个文件的时候,我们直接使用文件的文件名而不是它含有 ``/`` 的绝对路径。 +- 权限控制:我们不设置用户和用户组概念,全程只有单用户。同时根目录和其他文件也都没有权限控制位,即完全不限制文件的访问方式,不会区分文件是否可执行。 +- 不记录文件访问/修改的任何时间戳。 +- 不支持软硬链接。 +- 除了文档中即将介绍的系统调用之外,其他的很多文件系统相关系统调用均未实现。 + +文件打开与读写 +-------------------------------------------------- + +文件打开 +++++++++++++++++++++++++++++++++++++++++++++++++++ + +在读写一个标准文件之前,应用首先需要通过内核提供的 ``sys_open`` 系统调用将该文件打开在进程的文件描述符表中: + +.. code-block:: rust + + /// 功能:打开一个标准文件,并返回可以访问它的文件描述符。 + /// 参数:path 描述要打开的文件的文件名(简单起见,文件系统不需要支持目录,所有的文件都放在根目录 / 下), + /// flags 描述打开文件的标志,具体含义下面给出。 + /// 返回值:如果出现了错误则返回 -1,否则返回打开标准文件的文件描述符。可能的错误原因是:文件不存在。 + /// syscall ID:56 + pub fn sys_open(path: *const u8, flags: u32) -> isize; + +目前我们的内核支持以下几种标志(多种不同标志可能共存): + +- 如果 ``flags`` 为 0,则表示以只读模式 *RDONLY* 打开; +- 如果 ``flags`` 第 0 位被设置(0x001),表示以只写模式 *WRONLY* 打开; +- 如果 ``flags`` 第 1 位被设置(0x002),表示既可读又可写 *RDWR* ; +- 如果 ``flags`` 第 9 位被设置(0x200),表示允许创建文件 *CREATE* ,在找不到该文件的时候应创建文件;如果该文件已经存在则应该将该文件的大小归零; +- 如果 ``flags`` 第 10 位被设置(0x400),则在打开文件的时候应该清空文件的内容并将该文件的大小归零,也即 *TRUNC* 。 + +注意 ``flags`` 里面的权限设置只能控制进程对本次打开的文件的访问。一般情况下,在打开文件的时候首先需要经过文件系统的权限检查,比如一个文件自身不允许写入,那么进程自然也就不能以 *WRONLY* 或 *RDWR* 标志打开文件。但在我们简化版的文件系统中文件不进行权限设置,这一步就可以绕过。 + +在用户库 ``user_lib`` 中,我们将该系统调用封装为 ``open`` 接口: + +.. code-block:: rust + + // user/src/lib.rs + + bitflags! { + pub struct OpenFlags: u32 { + const RDONLY = 0; + const WRONLY = 1 << 0; + const RDWR = 1 << 1; + const CREATE = 1 << 9; + const TRUNC = 1 << 10; + } + } + + pub fn open(path: &str, flags: OpenFlags) -> isize { + sys_open(path, flags.bits) + } + +借助 ``bitflags!`` 宏我们将一个 ``u32`` 的 flags 包装为一个 ``OpenFlags`` 结构体更易使用,它的 ``bits`` 字段可以将自身转回 ``u32`` ,它也会被传给 ``sys_open`` : + +.. code-block:: rust + + // user/src/syscall.rs + + const SYSCALL_OPEN: usize = 56; + + pub fn sys_open(path: &str, flags: u32) -> isize { + syscall(SYSCALL_OPEN, [path.as_ptr() as usize, flags as usize, 0]) + } + +我们在 ``sys_open`` 传给内核的两个参数只有待打开文件的文件名字符串的起始地址(和之前一样,我们需要保证该字符串以 ``\0`` 结尾)还有标志位。由于每个通用寄存器为 64 位,我们需要先将 ``u32`` 的 ``flags`` 转换为 ``usize`` 。 + +文件的顺序读写 +++++++++++++++++++++++++++++++++++++++++++++++++++ + +在打开一个文件之后,我们就可以用之前的 ``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`` 来介绍文件系统接口的使用方法: + +.. code-block:: rust + :linenos: + + // user/src/bin/filetest_simple.rs + + #![no_std] + #![no_main] + + #[macro_use] + extern crate user_lib; + + use user_lib::{ + open, + close, + read, + write, + OpenFlags, + }; + + #[no_mangle] + pub fn main() -> i32 { + let test_str = "Hello, world!"; + let filea = "filea\0"; + let fd = open(filea, OpenFlags::CREATE | OpenFlags::WRONLY); + assert!(fd > 0); + let fd = fd as usize; + write(fd, test_str.as_bytes()); + close(fd); + + let fd = open(filea, OpenFlags::RDONLY); + assert!(fd > 0); + let fd = fd as usize; + let mut buffer = [0u8; 100]; + let read_len = read(fd, &mut buffer) as usize; + close(fd); + + assert_eq!( + test_str, + core::str::from_utf8(&buffer[..read_len]).unwrap(), + ); + println!("file_test passed!"); + 0 + } + +- 第 20~25 行,我们打开文件 ``filea`` ,向其中写入字符串 ``Hello, world!`` 而后关闭文件。这里需要注意的是我们需要为字符串字面量手动加上 ``\0`` 作为结尾。在打开文件时 *CREATE* 标志使得如果 ``filea`` 原本不存在,文件系统会自动创建一个同名文件,如果已经存在的话则会清空它的内容。而 *WRONLY* 使得此次只能写入该文件而不能读取。 +- 第 27~32 行,我们以只读 *RDONLY* 的方式将文件 ``filea`` 的内容读取到缓冲区 ``buffer`` 中。注意我们很清楚 ``filea`` 的总大小不超过缓冲区的大小,因此通过单次 ``read`` 即可将 ``filea`` 的内容全部读取出来。而更常见的情况是需要进行多次 ``read`` 直到它的返回值为 0 才能确认文件的内容已被读取完毕了。 +- 最后的第 34~38 行我们确认从 ``filea`` 读取到的内容和之前写入的一致,则测试通过。 diff --git a/source/chapter7/2fs-implementation.rst b/source/chapter7/2fs-implementation.rst new file mode 100644 index 0000000000000000000000000000000000000000..9d0375a7cbf548a4468b30df577ce1ccb40fa6cc --- /dev/null +++ b/source/chapter7/2fs-implementation.rst @@ -0,0 +1,1516 @@ +简易文件系统 easy-fs +======================================= + +本节导读 +--------------------------------------- + +本节我们介绍一个简易文件系统实现 easy-fs。作为一个文件系统而言,它的磁盘布局(为了叙述方便,我们用磁盘来指代一系列持久存储设备)体现在磁盘上各扇区的内容上,而它解析磁盘布局得到的逻辑目录树结构则是通过内存上的数据结构来访问的,这意味着它要同时涉及到对磁盘和对内存的访问。它们的访问方式是不同的,对于内存直接通过一条指令即可直接读写内存相应的位置,而磁盘的话需要用软件的方式向磁盘发出请求来间接进行读写。此外,我们也要特别注意哪些数据结构是存储在磁盘上,哪些数据结构是存储在内存中的,这样在实现的时候才不会引起混乱。 + +easy-fs 被从内核中分离出来,它的实现分成两个不同的 crate : + +- ``easy-fs`` 为简易文件系统的本体,它是一个库形式 crate,实现一种我们设计的简单磁盘布局; +- ``easy-fs-fuse`` 是一个能在开发环境(如 Ubuntu)中运行的应用程序,它可以对 ``easy-fs`` 进行测试,或者将为我们内核开发的应用打包为一个 easy-fs 格式的文件系统镜像。 + +``easy-fs`` crate 自下而上大致可以分成五个不同的层次: + +1. 块设备接口层 +2. 块缓存层 +3. 磁盘数据结构层 +4. 磁盘块管理器层 +5. 索引节点层 + +块设备接口层 +--------------------------------------- + +本层的代码在 ``block_dev.rs`` 中。 + +在 ``easy-fs`` 库的最底层声明了一个块设备的抽象接口 ``BlockDevice`` : + +.. code-block:: rust + + // easy-fs/src/block_dev.rs + + pub trait BlockDevice : Send + Sync + Any { + fn read_block(&self, block_id: usize, buf: &mut [u8]); + fn write_block(&self, block_id: usize, buf: &[u8]); + } + +它需要实现两个抽象方法: + +- ``read_block`` 可以将编号为 ``block_id`` 的块从磁盘读入内存中的缓冲区 ``buf`` ; +- ``write_block`` 可以内存中的缓冲区 ``buf`` 中的数据写入磁盘编号为 ``block_id`` 的块。 + +这是因为,之前提到过,块设备仅支持以块为单位进行随机读写,由此才有了这两个抽象方法。在 ``easy-fs`` 中并没有一个实现了 ``BlockDevice`` Trait 的具体类型,实际上这是需要由库的使用者提供并接入到 ``easy-fs`` 库的。这也体现了 ``easy-fs`` 的泛用性:它可以用于管理任何实现了 ``BlockDevice`` Trait 的块设备。 + +.. note:: + + **块与扇区** + + 实际上,块和扇区是两个不同的概念。 **扇区** (Sector) 是块设备随机读写的大小单位,通常每个扇区为 512 字节。而块是文件系统存储文件时的大小单位,每个块的大小等同于一个或多个扇区。之前提到过 Linux 默认文件系统的单个块大小为 4096 字节。在我们的 easy-fs 实现中一个块的大小和扇区相同为 512 字节,因此在后面的讲解中我们不再区分扇区和块的概念。 + +块缓存层 +--------------------------------------- + +本层的代码在 ``block_cache.rs`` 中。 + +由于 CPU 不能直接读写磁盘块,因此常见的手段是先通过 ``read_block`` 将一个块上的数据从磁盘读到内存中的一个缓冲区中,这个缓冲区中的内容是可以直接读写的。如果对于缓冲区中的内容进行了修改,那么后续需要通过 ``write_block`` 将缓冲区中的内容写回到磁盘块中。 + +事实上,无论站在代码实现鲁棒性还是性能的角度,将这些缓冲区合理的管理起来都是很有必要的。一种完全不进行任何管理的模式可能是:每当要对一个磁盘块进行读写的时候,都通过 ``read_block`` 将块数据读取到一个 *临时* 创建的缓冲区,并在进行一些操作之后(可选地)将缓冲区的内容写回到磁盘块。从性能上考虑,我们需要尽可能降低真正块读写(即 ``read/write_block`` )的次数,因为每一次调用它们都会产生大量开销。要做到这一点,关键就在于对于块读写操作进行 **合并** 。例如,如果一个块已经被读到缓冲区中了,那么我们就没有必要再读一遍,直接用已有的缓冲区就行了;同时,对于同一个块的缓冲区的多次修改没有必要每次都写回磁盘,只需等所有的修改都结束之后统一写回磁盘即可。 + +但是,当磁盘上的数据结构比较复杂的时候,在编程的时候我们很难手动正确的规划块读取/写入的时机。这不仅可能涉及到复杂的参数传递,稍有不慎还有可能引入同步性问题:即对于一个块缓冲区的修改在对于同一个块进行后续操作的时候不可见。它很致命但又难以调试。 + +因此,我们的做法是将缓冲区统一管理起来。当我们要读写一个块的时候,首先就是去全局管理器中查看这个块是否已被缓存到内存中的缓冲区中。这样,在一段连续时间内对于一个块进行的所有操作均是在同一个固定的缓冲区中进行的,这解决了同步性问题。此外,通过 ``read/write_block`` 真正进行块读写的时机完全交给全局管理器处理,我们在编程时无需操心。全局管理器仅会在必要的时机分别发起一次真正的块读写,尽可能将更多的块操作合并起来。 + +块缓存 ++++++++++++++++++++++++++++++++++++++++++ + +块缓存 ``BlockCache`` 的声明如下: + +.. code-block:: rust + + // easy-fs/src/lib.rs + + pub const BLOCK_SZ: usize = 512; + + // easy-fs/src/block_cache.rs + + pub struct BlockCache { + cache: [u8; BLOCK_SZ], + block_id: usize, + block_device: Arc, + modified: bool, + } + +其中: + +- ``cache`` 是一个 512 字节的数组,表示位于内存中的缓冲区; +- ``block_id`` 记录了这个块缓存来自于磁盘中的块的编号; +- ``block_device`` 保留一个底层块设备的引用使得可以和它打交道; +- ``modified`` 记录自从这个块缓存从磁盘载入内存之后,它有没有被修改过。 + +当我们创建一个 ``BlockCache`` 的时候,这将触发一次 ``read_block`` 将一个块上的数据从磁盘读到缓冲区 ``cache`` : + +.. code-block:: rust + + // easy-fs/src/block_cache.rs + + impl BlockCache { + /// Load a new BlockCache from disk. + pub fn new( + block_id: usize, + block_device: Arc + ) -> Self { + let mut cache = [0u8; BLOCK_SZ]; + block_device.read_block(block_id, &mut cache); + Self { + cache, + block_id, + block_device, + modified: false, + } + } + } + +一旦缓冲区已经存在于内存中,CPU 就可以直接访问存储在它上面的磁盘数据结构: + +.. code-block:: rust + :linenos: + + // easy-fs/src/block_cache.rs + + impl BlockCache { + fn addr_of_offset(&self, offset: usize) -> usize { + &self.cache[offset] as *const _ as usize + } + + pub fn get_ref(&self, offset: usize) -> &T where T: Sized { + let type_size = core::mem::size_of::(); + assert!(offset + type_size <= BLOCK_SZ); + let addr = self.addr_of_offset(offset); + unsafe { &*(addr as *const T) } + } + + pub fn get_mut(&mut self, offset: usize) -> &mut T where T: Sized { + let type_size = core::mem::size_of::(); + assert!(offset + type_size <= BLOCK_SZ); + self.modified = true; + let addr = self.addr_of_offset(offset); + unsafe { &mut *(addr as *mut T) } + } + } + +- ``addr_of_offset`` 可以得到一个 ``BlockCache`` 内部的缓冲区一个指定偏移量 ``offset`` 的字节地址; +- ``get_ref`` 是一个泛型方法,它可以获取缓冲区中的位于偏移量 ``offset`` 的一个类型为 ``T`` 的磁盘上数据结构的不可变引用。该泛型方法的 Trait Bound 限制类型 ``T`` 必须是一个编译时已知大小的类型,我们通过 ``core::mem::size_of::()`` 在编译时获取类型 ``T`` 的大小并确认该数据结构被整个包含在磁盘块及其缓冲区之内。这里编译器会自动进行生命周期标注,约束返回的引用的生命周期不超过 ``BlockCache`` 自身,在使用的时候我们会保证这一点。 +- ``get_mut`` 与 ``get_ref`` 的不同之处在于它会获取磁盘上数据结构的可变引用,由此可以对数据结构进行修改。由于这些数据结构目前位于内存中的缓冲区中,我们需要将 ``BlockCache`` 的 ``modified`` 标记为 true 表示该缓冲区已经被修改,之后需要将数据写回磁盘块才能真正将修改同步到磁盘。 + +``BlockCache`` 的设计也体现了 RAII 思想, 它管理着一个缓冲区的生命周期。当 ``BlockCache`` 的生命周期结束之后缓冲区也会被从内存中回收,这个时候 ``modified`` 标记将会决定数据是否需要写回磁盘: + +.. code-block:: rust + + // easy-fs/src/block_cache.rs + + impl BlockCache { + pub fn sync(&mut self) { + if self.modified { + self.modified = false; + self.block_device.write_block(self.block_id, &self.cache); + } + } + } + + impl Drop for BlockCache { + fn drop(&mut self) { + self.sync() + } + } + +在 ``BlockCache`` 被 ``drop`` 的时候,它会首先调用 ``sync`` 方法,如果自身确实被修改过的话才会将缓冲区的内容写回磁盘。事实上, ``sync`` 并不是只有在 ``drop`` 的时候才会被调用。在 Linux 中,通常有一个后台进程负责定期将内存中缓冲区的内容写回磁盘。另外有一个 ``sys_fsync`` 系统调用可以手动通知内核将一个文件的修改同步回磁盘。由于我们的实现比较简单, ``sync`` 仅会在 ``BlockCache`` 被 ``drop`` 时才会被调用。 + +我们可以将 ``get_ref/get_mut`` 进一步封装为更为易用的形式: + +.. code-block:: rust + + // easy-fs/src/block_cache.rs + + impl BlockCache { + pub fn read(&self, offset: usize, f: impl FnOnce(&T) -> V) -> V { + f(self.get_ref(offset)) + } + + pub fn modify(&mut self, offset:usize, f: impl FnOnce(&mut T) -> V) -> V { + f(self.get_mut(offset)) + } + } + +它们的含义是:在 ``BlockCache`` 缓冲区偏移量为 ``offset`` 的位置获取一个类型为 ``T`` 的磁盘上数据结构的不可变/可变引用(分别对应 ``read/modify`` ),并让它进行传入的闭包 ``f`` 中所定义的操作。注意 ``read/modify`` 的返回值是和传入闭包的返回值相同的,因此相当于 ``read/modify`` 构成了传入闭包 ``f`` 的一层执行环境,让它能够真正绑定到一个缓冲区开始执行。 + +这里我们传入闭包的类型为 ``FnOnce`` ,这是因为闭包里面的变量被捕获的方式涵盖了不可变引用/可变引用/和 move 三种可能性,故而我们需要选取范围最广的 ``FnOnce`` 。参数中的 ``impl`` 关键字体现了一种类似泛型的静态分发功能。 + +我们很快将展示 ``read/modify`` 接口如何在后续的开发中提供便利。 + +块缓存全局管理器 ++++++++++++++++++++++++++++++++++++++++++ + +为了避免在块缓存上浪费过多内存,我们希望内存中同时只能驻留有限个磁盘块的缓冲区: + +.. code-block:: rust + + // easy-fs/src/block_cache.rs + + const BLOCK_CACHE_SIZE: usize = 16; + +块缓存全局管理器的功能是:当我们要对一个磁盘块进行读写从而需要获取它的缓冲区的时候,首先看它是否已经被载入到内存中了,如果已经被载入的话则直接返回,否则需要读取磁盘块的数据到内存中。此时,如果内存中驻留的磁盘块缓冲区的数量已满,则需要遵循某种缓存替换算法将某个块的缓冲区从内存中移除,再将刚刚请求的块的缓冲区加入到内存中。我们这里使用一种类 FIFO 的简单缓存替换算法,因此在管理器中只需维护一个队列: + +.. code-block:: rust + + // easy-fs/src/block_cache.rs + + use alloc::collections::VecDeque; + + pub struct BlockCacheManager { + queue: VecDeque<(usize, Arc>)>, + } + + impl BlockCacheManager { + pub fn new() -> Self { + Self { queue: VecDeque::new() } + } + } + +队列 ``queue`` 中管理的是块编号和块缓存的二元组。块编号的类型为 ``usize`` ,而块缓存的类型则是一个 ``Arc>`` 。这是一个此前频频提及到的 Rust 中的经典组合,它可以同时提供共享引用和互斥访问。这里的共享引用意义在于块缓存既需要在管理器 ``BlockCacheManager`` 保留一个引用,还需要以引用的形式返回给块缓存的请求者让它可以对块缓存进行访问。而互斥访问在单核上的意义在于提供内部可变性通过编译,在多核环境下则可以帮助我们避免可能的并发冲突。事实上,一般情况下我们需要在更上层提供保护措施避免两个线程同时对一个块缓存进行读写,因此这里只是比较谨慎的留下一层保险。 + +``get_block_cache`` 方法尝试从块缓存管理器中获取一个编号为 ``block_id`` 的块的块缓存,如果找不到的话会从磁盘读取到内存中,还有可能会发生缓存替换: + +.. code-block:: rust + :linenos: + + // easy-fs/src/block_cache.rs + + impl BlockCacheManager { + pub fn get_block_cache( + &mut self, + block_id: usize, + block_device: Arc, + ) -> Arc> { + if let Some(pair) = self.queue + .iter() + .find(|pair| pair.0 == block_id) { + Arc::clone(&pair.1) + } else { + // substitute + if self.queue.len() == BLOCK_CACHE_SIZE { + // from front to tail + if let Some((idx, _)) = self.queue + .iter() + .enumerate() + .find(|(_, pair)| Arc::strong_count(&pair.1) == 1) { + self.queue.drain(idx..=idx); + } else { + panic!("Run out of BlockCache!"); + } + } + // load block into mem and push back + let block_cache = Arc::new(Mutex::new( + BlockCache::new(block_id, Arc::clone(&block_device)) + )); + self.queue.push_back((block_id, Arc::clone(&block_cache))); + block_cache + } + } + } + +- 第 9 行会遍历整个队列试图找到一个编号相同的块缓存,如果找到了话会将块缓存管理器中保存的块缓存的引用复制一份并返回; +- 第 13 行对应找不到的情况,此时必须将块从磁盘读入内存中的缓冲区。在实际读取之前需要判断管理器保存的块缓存数量是否已经达到了上限。如果达到了上限(第 15 行)才需要执行缓存替换算法丢掉某个块的缓存空出一个空位。这里使用一种类 FIFO 算法,如果是 FIFO 算法的话,每次加入一个缓存的时候需要从队尾加入,需要替换的时候则从队头弹出。但是此时队头对应的块缓存可能仍在使用:判断的标志是其强引用计数 :math:`\geq 2` ,即除了块缓存管理器保留的一份副本之外,在外面还有若干份副本正在使用。因此,我们的做法是从队头遍历到队尾找到第一个强引用计数恰好为 1 的块缓存并将其替换出去。 + + 那么是否有可能出现队列已满且其中所有的块缓存都正在使用的情形呢?事实上,只要我们的上限 ``BLOCK_CACHE_SIZE`` 设置的足够大,超过所有线程同时访问的块总数上限,那么这种情况永远不会发生。但是,如果我们的上限设置不足,这里我们就只能 panic 。 +- 第 27 行开始我们创建一个新的块缓存(会触发 ``read_block`` 进行块读取)并加入到队尾,最后返回给请求者。 + +接下来需要创建 ``BlockCacheManager`` 的全局实例: + +.. code-block:: rust + + // easy-fs/src/block_cache.rs + + lazy_static! { + pub static ref BLOCK_CACHE_MANAGER: Mutex = Mutex::new( + BlockCacheManager::new() + ); + } + + pub fn get_block_cache( + block_id: usize, + block_device: Arc + ) -> Arc> { + BLOCK_CACHE_MANAGER.lock().get_block_cache(block_id, block_device) + } + +之后,对于其他模块而言就可以直接通过 ``get_block_cache`` 方法来请求块缓存了。这里需要指出的是,它返回的是一个 ``Arc>`` ,调用者需要通过 ``.lock()`` 获取里层互斥锁 ``Mutex`` 才能对最里面的 ``BlockCache`` 进行操作,比如通过 ``read/modify`` 访问缓冲区里面的磁盘数据结构。 + +磁盘布局及磁盘上数据结构 +--------------------------------------- + +本层的代码在 ``layout.rs`` 和 ``bitmap.rs`` 中。 + +对于一个文件系统而言,最重要的功能是如何将一个逻辑上的目录树结构映射到磁盘上,决定磁盘上的每个块应该存储哪些数据。为了更容易进行管理和更新,我们需要将磁盘上的数据组织为若干种不同的磁盘上数据结构,并合理安排它们在磁盘中的位置。 + +easy-fs 磁盘布局概述 ++++++++++++++++++++++++++++++++++++++++ + +在 easy-fs 磁盘布局中,按照块编号从小到大可以分成 5 个连续区域: + +- 最开始的区域长度为一个块,其内容是 easy-fs **超级块** (Super Block),超级块内以魔数的形式提供了文件系统合法性检查功能,同时还可以定位其他连续区域的位置。 +- 接下来的一个区域是一个索引节点位图,长度为若干个块。它记录了后面的索引节点区域中有哪些索引节点已经被分配出去使用了,而哪些还尚未被分配出去。 +- 接下来的一个区域是索引节点区域,长度为若干个块。其中的每个块都存储了若干个索引节点。 +- 接下来的一个区域是一个数据块位图,长度为若干个块。它记录了后面的数据块区域中有哪些数据块已经被分配出去使用了,而哪些还尚未被分配出去。 +- 最后的一个区域则是数据块区域,顾名思义,其中的每一个块的职能都是作为一个数据块实际保存文件或目录中的数据。 + +**索引节点** (Inode, Index Node) 是文件系统中的一种重要数据结构。逻辑目录树结构中的每个文件和目录都对应一个 inode ,我们前面提到的在文件系统实现中文件/目录的底层编号实际上就是指 inode 编号。在 inode 中不仅包含了我们通过 ``stat`` 工具能够看到的文件/目录的元数据(大小/访问权限/类型等信息),还包含它到那些实际保存文件/目录数据的数据块(位于最后的数据块区域中)的索引信息,从而能够找到文件/目录的数据被保存在哪里。从索引方式上看,同时支持直接索引和间接索引。 + +每个区域中均存储着不同的磁盘数据结构,它们能够对磁盘中的数据进行解释并将其结构化。下面我们分别对它们进行介绍。 + +easy-fs 超级块 ++++++++++++++++++++++++++++++++++++++++ + +超级块 ``SuperBlock`` 的内容如下: + +.. code-block:: rust + + // easy-fs/src/layout.rs + + #[repr(C)] + pub struct SuperBlock { + magic: u32, + pub total_blocks: u32, + pub inode_bitmap_blocks: u32, + pub inode_area_blocks: u32, + pub data_bitmap_blocks: u32, + pub data_area_blocks: u32, + } + +其中, ``magic`` 是一个用于文件系统合法性验证的魔数, ``total_block`` 给出文件系统的总块数。注意这并不等同于所在磁盘的总块数,因为文件系统很可能并没有占据整个磁盘。后面的四个字段则分别给出 easy-fs 布局中后四个连续区域的长度各为多少个块。 + +下面是它实现的方法: + +.. code-block:: rust + + // easy-fs/src/layout.rs + + impl SuperBlock { + pub fn initialize( + &mut self, + total_blocks: u32, + inode_bitmap_blocks: u32, + inode_area_blocks: u32, + data_bitmap_blocks: u32, + data_area_blocks: u32, + ) { + *self = Self { + magic: EFS_MAGIC, + total_blocks, + inode_bitmap_blocks, + inode_area_blocks, + data_bitmap_blocks, + data_area_blocks, + } + } + pub fn is_valid(&self) -> bool { + self.magic == EFS_MAGIC + } + } + +- ``initialize`` 可以在创建一个 easy-fs 的时候对超级块进行初始化,注意各个区域的块数是以参数的形式传入进来的,它们的划分是更上层的磁盘块管理器需要完成的工作。 +- ``is_valid`` 则可以通过魔数判断超级块所在的文件系统是否合法。 + +``SuperBlock`` 是一个磁盘上数据结构,它就存放在磁盘上编号为 0 的块的开头。 + +位图 ++++++++++++++++++++++++++++++++++++++++ + +在 easy-fs 布局中存在两个不同的位图,分别对于索引节点和数据块进行管理。每个位图都由若干个块组成,每个块大小为 512 字节,即 4096 个比特。每个比特都代表一个索引节点/数据块的分配状态, 0 意味着未分配,而 1 则意味着已经分配出去。位图所要做的事情是通过比特位的分配(寻找一个为 0 的比特位设置为 1)和回收(将比特位清零)来进行索引节点/数据块的分配和回收。 + +.. code-block:: rust + + // easy-fs/src/bitmap.rs + + pub struct Bitmap { + start_block_id: usize, + blocks: usize, + } + + impl Bitmap { + pub fn new(start_block_id: usize, blocks: usize) -> Self { + Self { + start_block_id, + blocks, + } + } + } + +位图 ``Bitmap`` 中仅保存了它所在区域的起始块编号以及区域的长度为多少个块。通过 ``new`` 方法可以新建一个位图。注意 ``Bitmap`` 自身是驻留在内存中的,但是它能够控制它所在区域的那些磁盘块。磁盘块上的数据则是要以磁盘数据结构 ``BitmapBlock`` 的格式进行操作: + +.. code-block:: rust + + // easy-fs/src/bitmap.rs + + type BitmapBlock = [u64; 64]; + +``BitmapBlock`` 是一个磁盘数据结构,它将位图区域中的一个磁盘块解释为长度为 64 的一个 ``u64`` 数组, 每个 ``u64`` 打包了一组 64 个比特,于是整个数组包含 :math:`64\times 64=4096` 个比特,且可以以组为单位进行操作。 + +首先来看 ``Bitmap`` 如何分配一个比特: + +.. code-block:: rust + :linenos: + + // easy-fs/src/bitmap.rs + + const BLOCK_BITS: usize = BLOCK_SZ * 8; + + impl Bitmap { + pub fn alloc(&self, block_device: &Arc) -> Option { + for block_id in 0..self.blocks { + let pos = get_block_cache( + block_id + self.start_block_id as usize, + Arc::clone(block_device), + ) + .lock() + .modify(0, |bitmap_block: &mut BitmapBlock| { + if let Some((bits64_pos, inner_pos)) = bitmap_block + .iter() + .enumerate() + .find(|(_, bits64)| **bits64 != u64::MAX) + .map(|(bits64_pos, bits64)| { + (bits64_pos, bits64.trailing_ones() as usize) + }) { + // modify cache + bitmap_block[bits64_pos] |= 1u64 << inner_pos; + Some(block_id * BLOCK_BITS + bits64_pos * 64 + inner_pos as usize) + } else { + None + } + }); + if pos.is_some() { + return pos; + } + } + None + } + } + +其主要思路是遍历区域中的每个块,再在每个块中以比特组(每组 64 比特)为单位进行遍历,找到一个尚未被全部分配出去的组,最后在里面分配一个比特。它将会返回分配的比特所在的位置,等同于索引节点/数据块的编号。如果所有比特均已经被分配出去了,则返回 ``None`` 。 + +第 7 行枚举区域中的每个块(编号为 ``block_id`` ),在循环内部我们需要读写这个块,在块内尝试找到一个空闲的比特并置 1 。一旦涉及到块的读写,就需要用到块缓存层提供的接口: + +- 第 8 行我们调用 ``get_block_cache`` 获取块缓存,注意我们传入的块编号是区域起始块编号 ``start_block_id`` 加上区域内的块编号 ``block_id`` 得到的块设备上的块编号。 +- 第 12 行我们通过 ``.lock()`` 获取块缓存的互斥锁从而可以对块缓存进行访问。 +- 第 13 行我们使用到了 ``BlockCache::modify`` 接口。它传入的偏移量 ``offset`` 为 0,这是因为整个块上只有一个 ``BitmapBlock`` ,它的大小恰好为 512 字节。因此我们需要从块的开头开始才能访问到完整的 ``BitmapBlock`` 。同时,传给它的闭包需要显式声明参数类型为 ``&mut BitmapBlock`` ,不然的话, ``BlockCache`` 的泛型方法 ``modify/get_mut`` 无法得知应该用哪个类型来解析块上的数据。在声明之后,编译器才能在这里将两个方法中的泛型 ``T`` 实例化为具体类型 ``BitmapBlock`` 。 + + 总结一下,这里 ``modify`` 的含义就是:从缓冲区偏移量为 0 的位置开始将一段连续的数据(数据的长度随具体类型而定)解析为一个 ``BitmapBlock`` 并要对该数据结构进行修改。在闭包内部,我们可以使用这个 ``BitmapBlock`` 的可变引用 ``bitmap_block`` 对它进行访问。 ``read/get_ref`` 的用法完全相同,后面将不再赘述。 +- 闭包的主体位于第 14~26 行。它尝试在 ``bitmap_block`` 中找到一个空闲的比特并返回其位置,如果不存在的话则返回 ``None`` 。它的思路是,遍历每 64 个比特构成的组(一个 ``u64`` ),如果它并没有达到 ``u64::MAX`` (即 :math:`2^{64}-1` ),则通过 ``u64::trailing_ones`` 找到最低的一个 0 并置为 1 。如果能够找到的话,比特组的编号将保存在变量 ``bits64_pos`` 中,而分配的比特在组内的位置将保存在变量 ``inner_pos`` 中。在返回分配的比特编号的时候,它的计算方式是 ``block_id*BLOCK_BITS+bits64_pos*64+inner_pos`` 。注意闭包中的 ``block_id`` 并不在闭包的参数列表中,因此它是从外部环境(即自增 ``block_id`` 的循环)中捕获到的。 + +我们一旦在某个块中找到一个空闲的比特并成功分配,就不再考虑后续的块。第 28 行体现了提前返回的思路。 + +.. warning:: + + **Rust 语法卡片:闭包** + + FIXME + +接下来看 ``Bitmap`` 如何回收一个比特: + +.. code-block:: rust + + // easy-fs/src/bitmap.rs + + /// Return (block_pos, bits64_pos, inner_pos) + fn decomposition(mut bit: usize) -> (usize, usize, usize) { + let block_pos = bit / BLOCK_BITS; + bit = bit % BLOCK_BITS; + (block_pos, bit / 64, bit % 64) + } + + impl Bitmap { + pub fn dealloc(&self, block_device: &Arc, bit: usize) { + let (block_pos, bits64_pos, inner_pos) = decomposition(bit); + get_block_cache( + block_pos + self.start_block_id, + Arc::clone(block_device) + ).lock().modify(0, |bitmap_block: &mut BitmapBlock| { + assert!(bitmap_block[bits64_pos] & (1u64 << inner_pos) > 0); + bitmap_block[bits64_pos] -= 1u64 << inner_pos; + }); + } + } + +``dealloc`` 方法首先调用 ``decomposition`` 函数将比特编号 ``bit`` 分解为区域中的块编号 ``block_pos`` 、块内的组编号 ``bits64_pos`` 以及组内编号 ``inner_pos`` 的三元组,这样就能精确定位待回收的比特,随后将其清零即可。 + +磁盘上索引节点 ++++++++++++++++++++++++++++++++++++++++ + +在磁盘上的索引节点区域,每个块上都保存着若干个索引节点 ``DiskInode`` : + +.. code-block:: rust + + // easy-fs/src/layout.rs + + const INODE_DIRECT_COUNT: usize = 28; + + #[repr(C)] + pub struct DiskInode { + pub size: u32, + pub direct: [u32; INODE_DIRECT_COUNT], + pub indirect1: u32, + pub indirect2: u32, + type_: DiskInodeType, + } + + #[derive(PartialEq)] + pub enum DiskInodeType { + File, + Directory, + } + +每个文件/目录在磁盘上均以一个 ``DiskInode`` 的形式存储。其中首先包含文件/目录的元数据:它的 ``size`` 表示文件/目录内容的字节数, ``type_`` 表示索引节点的类型 ``DiskInodeType`` ,目前仅支持文件 ``File`` 和目录 ``Directory`` 两种类型。其余的 ``direct/indirect1/indirect2`` 都是到存储文件/目录内容的数据块的索引,这也是索引节点名字的由来。 + +为了尽可能节约空间,在进行索引的时候,块的编号用一个 ``u32`` 存储。索引方式分成直接索引和间接索引两种: + +- 当文件很小的时候,只需用到直接索引, ``direct`` 数组中最多可以指向 ``INODE_DIRECT_COUNT`` 个数据块,当取值为 28 的时候,通过直接索引可以找到 14KiB 的内容。 +- 当文件比较大的时候,不仅直接索引的 ``direct`` 数组装满,还需要用到一级间接索引 ``indirect1`` 。它指向一个一级索引块,这个块也位于磁盘布局的数据块区域中。这个一级索引块中的每个 ``u32`` 都用来指向数据块区域中一个保存该文件内容的数据块,因此,最多能够索引 :math:`\frac{512}{4}=128` 个数据块,对应 64KiB 的内容。 +- 当文件大小超过直接索引和一级索引支持的容量上限 78KiB 的时候,就需要用到二级间接索引 ``indirect2`` 。它指向一个位于数据块区域中的二级索引块。二级索引块中的每个 ``u32`` 指向一个不同的一级索引块,这些一级索引块也位于数据块区域中。因此,通过二级间接索引最多能够索引 :math:`128\times 64\text{KiB}=8\text{MiB}` 的内容。 + +为了充分利用空间,我们将 ``DiskInode`` 的大小设置为 128 字节,每个块正好能够容纳 4 个 ``DiskInode`` 。在后续需要支持更多类型的元数据的时候,可以适当缩减直接索引 ``direct`` 的块数,并将节约出来的空间用来存放其他元数据,仍可保证 ``DiskInode`` 的总大小为 128 字节。 + +通过 ``initialize`` 方法可以初始化一个 ``DiskInode`` 为一个文件或目录: + +.. code-block:: rust + + // easy-fs/src/layout.rs + + impl DiskInode { + /// indirect1 and indirect2 block are allocated only when they are needed. + pub fn initialize(&mut self, type_: DiskInodeType) { + self.size = 0; + self.direct.iter_mut().for_each(|v| *v = 0); + self.indirect1 = 0; + self.indirect2 = 0; + self.type_ = type_; + } + } + +需要注意的是, ``indirect1/2`` 均被初始化为 0 。因为最开始文件内容的大小为 0 字节,并不会用到一级/二级索引。为了节约空间,我们会完全按需分配一级/二级索引块。此外,直接索引 ``direct`` 也被清零。 + +``is_file`` 和 ``is_dir`` 两个方法可以用来确认 ``DiskInode`` 的类型为文件还是目录: + +.. code-block:: rust + + // easy-fs/src/layout.rs + + impl DiskInode { + pub fn is_dir(&self) -> bool { + self.type_ == DiskInodeType::Directory + } + pub fn is_file(&self) -> bool { + self.type_ == DiskInodeType::File + } + } + +``get_block_id`` 方法体现了 ``DiskInode`` 最重要的数据块索引功能,它可以从索引中查到它自身用于保存文件内容的第 ``block_id`` 个数据块的块编号,这样后续才能对这个数据块进行访问: + +.. code-block:: rust + :linenos: + :emphasize-lines: 10,12,18 + + // easy-fs/src/layout.rs + + const INODE_INDIRECT1_COUNT: usize = BLOCK_SZ / 4; + const INDIRECT1_BOUND: usize = DIRECT_BOUND + INODE_INDIRECT1_COUNT; + type IndirectBlock = [u32; BLOCK_SZ / 4]; + + impl DiskInode { + pub fn get_block_id(&self, inner_id: u32, block_device: &Arc) -> u32 { + let inner_id = inner_id as usize; + if inner_id < INODE_DIRECT_COUNT { + self.direct[inner_id] + } else if inner_id < INDIRECT1_BOUND { + get_block_cache(self.indirect1 as usize, Arc::clone(block_device)) + .lock() + .read(0, |indirect_block: &IndirectBlock| { + indirect_block[inner_id - INODE_DIRECT_COUNT] + }) + } else { + let last = inner_id - INDIRECT1_BOUND; + let indirect1 = get_block_cache( + self.indirect2 as usize, + Arc::clone(block_device) + ) + .lock() + .read(0, |indirect2: &IndirectBlock| { + indirect2[last / INODE_INDIRECT1_COUNT] + }); + get_block_cache( + indirect1 as usize, + Arc::clone(block_device) + ) + .lock() + .read(0, |indirect1: &IndirectBlock| { + indirect1[last % INODE_INDIRECT1_COUNT] + }) + } + } + } + +这里需要说明的是: + +- 第 10/12/18 行分别利用直接索引/一级索引和二级索引,具体选用哪种索引方式取决于 ``block_id`` 所在的区间。 +- 在对一个索引块进行操作的时候,我们将其解析为磁盘数据结构 ``IndirectBlock`` ,实质上就是一个 ``u32`` 数组,每个都指向一个下一级索引块或者数据块。 +- 对于二级索引的情况,需要先查二级索引块找到挂在它下面的一级索引块,再通过一级索引块找到数据块。 + +在初始化之后文件/目录的 ``size`` 均为 0 ,此时并不会索引到任何数据块。它需要通过 ``increase_size`` 方法逐步扩充容量。在扩充的时候,自然需要一些新的数据块来作为索引块或是保存内容的数据块。我们需要先编写一些辅助方法来确定在容量扩充的时候额外需要多少块: + +.. code-block:: rust + + // easy-fs/src/layout.rs + + impl DiskInode { + /// Return block number correspond to size. + pub fn data_blocks(&self) -> u32 { + Self::_data_blocks(self.size) + } + fn _data_blocks(size: u32) -> u32 { + (size + BLOCK_SZ as u32 - 1) / BLOCK_SZ as u32 + } + /// Return number of blocks needed include indirect1/2. + pub fn total_blocks(size: u32) -> u32 { + let data_blocks = Self::_data_blocks(size) as usize; + let mut total = data_blocks as usize; + // indirect1 + if data_blocks > INODE_DIRECT_COUNT { + total += 1; + } + // indirect2 + if data_blocks > INDIRECT1_BOUND { + total += 1; + // sub indirect1 + total += (data_blocks - INDIRECT1_BOUND + INODE_INDIRECT1_COUNT - 1) / INODE_INDIRECT1_COUNT; + } + total as u32 + } + pub fn blocks_num_needed(&self, new_size: u32) -> u32 { + assert!(new_size >= self.size); + Self::total_blocks(new_size) - Self::total_blocks(self.size) + } + } + +``data_blocks`` 方法可以计算为了容纳自身 ``size`` 字节的内容需要多少个数据块。计算的过程只需用 ``size`` 除以每个块的大小 ``BLOCK_SZ`` 并向上取整。而 ``total_blocks`` 不仅包含数据块,还需要统计索引块。计算的方法也很简单,先调用 ``data_blocks`` 得到需要多少数据块,再根据数据块数目所处的区间统计索引块即可。 ``blocks_num_needed`` 可以计算将一个 ``DiskInode`` 的 ``size`` 扩容到 ``new_size`` 需要额外多少个数据和索引块。这只需要调用两次 ``total_blocks`` 作差即可。 + +下面给出 ``increase_size`` 方法的接口: + +.. code-block:: rust + + // easy-fs/src/layout.rs + + impl DiskInode { + pub fn increase_size( + &mut self, + new_size: u32, + new_blocks: Vec, + block_device: &Arc, + ); + } + +其中 ``new_size`` 表示容量扩充之后的文件大小; ``new_blocks`` 是一个保存了本次容量扩充所需块编号的向量,这些块都是由上层的磁盘块管理器负责分配的。 ``increase_size`` 的实现有些复杂,在这里不详细介绍。大致的思路是按照直接索引、一级索引再到二级索引的顺序进行扩充。 + +有些时候我们还需要清空文件的内容并回收所有数据和索引块。这是通过 ``clear_size`` 方法来实现的: + +.. code-block:: rust + + // easy-fs/src/layout.rs + + impl DiskInode { + /// Clear size to zero and return blocks that should be deallocated. + /// + /// We will clear the block contents to zero later. + pub fn clear_size(&mut self, block_device: &Arc) -> Vec; + } + +它会将回收的所有块的编号保存在一个向量中返回给磁盘块管理器。它的实现原理和 ``increase_size`` 一样也分为多个阶段,在这里不展开。 + +接下来需要考虑通过 ``DiskInode`` 来读写它索引的那些数据块中的数据。这些数据可以被视为一个字节序列,而每次我们都是选取其中的一段连续区间进行操作,以 ``read_at`` 为例: + +.. code-block:: rust + :linenos: + + // easy-fs/src/layout.rs + + type DataBlock = [u8; BLOCK_SZ]; + + impl DiskInode { + pub fn read_at( + &self, + offset: usize, + buf: &mut [u8], + block_device: &Arc, + ) -> usize { + let mut start = offset; + let end = (offset + buf.len()).min(self.size as usize); + if start >= end { + return 0; + } + let mut start_block = start / BLOCK_SZ; + let mut read_size = 0usize; + loop { + // calculate end of current block + let mut end_current_block = (start / BLOCK_SZ + 1) * BLOCK_SZ; + end_current_block = end_current_block.min(end); + // read and update read size + let block_read_size = end_current_block - start; + let dst = &mut buf[read_size..read_size + block_read_size]; + get_block_cache( + self.get_block_id(start_block as u32, block_device) as usize, + Arc::clone(block_device), + ) + .lock() + .read(0, |data_block: &DataBlock| { + let src = &data_block[start % BLOCK_SZ..start % BLOCK_SZ + block_read_size]; + dst.copy_from_slice(src); + }); + read_size += block_read_size; + // move to next block + if end_current_block == end { break; } + start_block += 1; + start = end_current_block; + } + read_size + } + } + +它的含义是:将文件内容从 ``offset`` 字节开始的部分读到内存中的缓冲区 ``buf`` 中,并返回实际读到的字节数。如果文件剩下的内容还足够多,那么缓冲区会被填满;不然的话文件剩下的全部内容都会被读到缓冲区中。具体实现上有很多细节,但大致的思路是遍历位于字节区间 ``start,end`` 中间的那些块,将它们视为一个 ``DataBlock`` (也就是一个字节数组),并将其中的部分内容复制到缓冲区 ``buf`` 中适当的区域。 ``start_block`` 维护着目前是文件内部第多少个数据块,需要首先调用 ``get_block_id`` 从索引中查到这个数据块在块设备中的块编号,随后才能传入 ``get_block_cache`` 中将正确的数据块缓存到内存中进行访问。 + +在第 14 行进行了简单的边界条件判断,如果要读取的内容超出了文件的范围那么直接返回 0 表示读取不到任何内容。 + +``write_at`` 的实现思路基本上和 ``read_at`` 完全相同。但不同的是 ``write_at`` 不会出现失败的情况,传入的整个缓冲区的数据都必定会被写入到文件中。当从 ``offset`` 开始的区间超出了文件范围的时候,就需要调用者在调用 ``write_at`` 之前提前调用 ``increase_size`` 将文件大小扩充到区间的右端保证写入的完整性。 + +数据块与目录项 ++++++++++++++++++++++++++++++++++++++++ + +作为一个文件而言,它的内容在文件系统或内核看来没有任何既定的格式,都只是一个字节序列。因此每个保存内容的数据块都只是一个字节数组: + +.. code-block:: rust + + // easy-fs/src/layout.rs + + type DataBlock = [u8; BLOCK_SZ]; + +然而,目录的内容却需要遵从一种特殊的格式。在我们的实现中,它可以看成一个目录项的序列,每个目录项都是一个二元组,二元组的首个元素是目录下面的一个文件(或子目录)的文件名(或目录名),另一个元素则是文件(或子目录)所在的索引节点编号。目录项相当于目录树结构上的孩子指针,我们需要通过它来一级一级的找到实际要访问的文件或目录。目录项 ``DirEntry`` 的定义如下: + +.. code-block:: rust + + // easy-fs/src/layout.rs + + const NAME_LENGTH_LIMIT: usize = 27; + + #[repr(C)] + pub struct DirEntry { + name: [u8; NAME_LENGTH_LIMIT + 1], + inode_number: u32, + } + + pub const DIRENT_SZ: usize = 32; + +目录项 ``Dirent`` 最大允许保存长度为 27 的文件/目录名(数组 ``name`` 中最末的一个字节留给 ``\0`` ),且它自身占据空间 32 字节,每个数据块可以存储 16 个目录项。我们可以通过 ``empty`` 和 ``new`` 分别生成一个空的目录项或是一个合法的目录项: + +.. code-block:: rust + + // easy-fs/src/layout.rs + + impl DirEntry { + pub fn empty() -> Self { + Self { + name: [0u8; NAME_LENGTH_LIMIT + 1], + inode_number: 0, + } + } + pub fn new(name: &str, inode_number: u32) -> Self { + let mut bytes = [0u8; NAME_LENGTH_LIMIT + 1]; + &mut bytes[..name.len()].copy_from_slice(name.as_bytes()); + Self { + name: bytes, + inode_number, + } + } + } + +在从目录的内容中读取目录项或者是将目录项写入目录的时候,我们需要将目录项转化为缓冲区(即字节切片)的形式来符合 ``read/write_at`` 接口的要求: + +.. code-block:: rust + + // easy-fs/src/layout.rs + + impl DirEntry { + pub fn as_bytes(&self) -> &[u8] { + unsafe { + core::slice::from_raw_parts( + self as *const _ as usize as *const u8, + DIRENT_SZ, + ) + } + } + pub fn as_bytes_mut(&mut self) -> &mut [u8] { + unsafe { + core::slice::from_raw_parts_mut( + self as *mut _ as usize as *mut u8, + DIRENT_SZ, + ) + } + } + } + +此外,通过 ``name`` 和 ``inode_number`` 方法可以取出目录项中的内容: + +.. code-block:: rust + + // easy-fs/src/layout.rs + + impl DirEntry { + pub fn name(&self) -> &str { + let len = (0usize..).find(|i| self.name[*i] == 0).unwrap(); + core::str::from_utf8(&self.name[..len]).unwrap() + } + pub fn inode_number(&self) -> u32 { + self.inode_number + } + } + +磁盘块管理器 +--------------------------------------- + +本层的代码在 ``efs.rs`` 中。 + +上面介绍了 easy-fs 的磁盘布局设计以及数据的组织方式——即各类磁盘数据结构。但是它们都是以比较零散的形式分开介绍的,也并没有体现出磁盘布局上各个区域是如何划分的。实现 easy-fs 的整体磁盘布局,将各段区域及上面的磁盘数据结构结构整合起来就是简易文件系统 ``EasyFileSystem`` 的职责。它知道每个布局区域所在的位置,磁盘块的分配和回收也需要经过它才能完成,因此某种意义上讲它还可以看成一个磁盘块管理器。 + +注意从这一层开始,所有的数据结构就都放在内存上了。 + +.. code-block:: rust + + // easy-fs/src/efs.rs + + pub struct EasyFileSystem { + pub block_device: Arc, + pub inode_bitmap: Bitmap, + pub data_bitmap: Bitmap, + inode_area_start_block: u32, + data_area_start_block: u32, + } + +``EasyFileSystem`` 包含索引节点和数据块的两个位图 ``inode_bitmap`` 和 ``data_bitmap`` ,还记录下索引节点区域和数据块区域起始块编号方便确定每个索引节点和数据块在磁盘上的具体位置。我们还要在其中保留块设备的一个指针 ``block_device`` ,在进行后续操作的时候,该指针会被拷贝并传递给下层的数据结构,让它们也能够直接访问块设备。 + +通过 ``create`` 方法可以在块设备上创建并初始化一个 easy-fs 文件系统: + +.. code-block:: rust + :linenos: + + // easy-fs/src/efs.rs + + impl EasyFileSystem { + pub fn create( + block_device: Arc, + total_blocks: u32, + inode_bitmap_blocks: u32, + ) -> Arc> { + // calculate block size of areas & create bitmaps + let inode_bitmap = Bitmap::new(1, inode_bitmap_blocks as usize); + let inode_num = inode_bitmap.maximum(); + let inode_area_blocks = + ((inode_num * core::mem::size_of::() + BLOCK_SZ - 1) / BLOCK_SZ) as u32; + let inode_total_blocks = inode_bitmap_blocks + inode_area_blocks; + let data_total_blocks = total_blocks - 1 - inode_total_blocks; + let data_bitmap_blocks = (data_total_blocks + 4096) / 4097; + let data_area_blocks = data_total_blocks - data_bitmap_blocks; + let data_bitmap = Bitmap::new( + (1 + inode_bitmap_blocks + inode_area_blocks) as usize, + data_bitmap_blocks as usize, + ); + let mut efs = Self { + block_device: Arc::clone(&block_device), + inode_bitmap, + data_bitmap, + inode_area_start_block: 1 + inode_bitmap_blocks, + data_area_start_block: 1 + inode_total_blocks + data_bitmap_blocks, + }; + // clear all blocks + for i in 0..total_blocks { + get_block_cache( + i as usize, + Arc::clone(&block_device) + ) + .lock() + .modify(0, |data_block: &mut DataBlock| { + for byte in data_block.iter_mut() { *byte = 0; } + }); + } + // initialize SuperBlock + get_block_cache(0, Arc::clone(&block_device)) + .lock() + .modify(0, |super_block: &mut SuperBlock| { + super_block.initialize( + total_blocks, + inode_bitmap_blocks, + inode_area_blocks, + data_bitmap_blocks, + data_area_blocks, + ); + }); + // write back immediately + // create a inode for root node "/" + assert_eq!(efs.alloc_inode(), 0); + let (root_inode_block_id, root_inode_offset) = efs.get_disk_inode_pos(0); + get_block_cache( + root_inode_block_id as usize, + Arc::clone(&block_device) + ) + .lock() + .modify(root_inode_offset, |disk_inode: &mut DiskInode| { + disk_inode.initialize(DiskInodeType::Directory); + }); + Arc::new(Mutex::new(efs)) + } + } + +- 第 10~21 行根据传入的参数计算每个区域各应该包含多少块。根据 inode 位图的大小计算 inode 区域至少需要多少个块才能够使得 inode 位图中的每个比特都能够有一个实际的 inode 可以对应,这样就确定了 inode 位图区域和 inode 区域的大小。剩下的块都分配给数据块位图区域和数据块区域。我们希望数据块位图中的每个比特仍然能够对应到一个数据块,但是数据块位图又不能过小,不然会造成某些数据块永远不会被使用。因此数据块位图区域最合理的大小是剩余的块数除以 4097 再上取整,因为位图中的每个块能够对应 4096 个数据块。其余的块就都作为数据块使用。 +- 第 22 行创建我们的 ``EasyFileSystem`` 实例 ``efs`` 。 +- 第 30 行首先将块设备的前 ``total_blocks`` 个块清零,因为我们的 easy-fs 要用到它们,这也是为初始化做准备。 +- 第 41 行将位于块设备编号为 0 块上的超级块进行初始化,只需传入之前计算得到的每个区域的块数就行了。 +- 第 54~63 行我们要做的事情是创建根目录 ``/`` 。首先需要调用 ``alloc_inode`` 在 inode 位图中分配一个 inode ,由于这是第一次分配,它的编号固定是 0 。接下来需要将分配到的 inode 初始化为 easy-fs 中的唯一一个目录,我们需要调用 ``get_disk_inode_pos`` 来根据 inode 编号获取该 inode 所在的块的编号以及块内偏移,之后就可以将它们传给 ``get_block_cache`` 和 ``modify`` 了。 + +通过 ``open`` 方法可以从一个已写入了 easy-fs 镜像的块设备上打开我们的 easy-fs : + +.. code-block:: rust + + // easy-fs/src/efs.rs + + impl EasyFileSystem { + pub fn open(block_device: Arc) -> Arc> { + // read SuperBlock + get_block_cache(0, Arc::clone(&block_device)) + .lock() + .read(0, |super_block: &SuperBlock| { + assert!(super_block.is_valid(), "Error loading EFS!"); + let inode_total_blocks = + super_block.inode_bitmap_blocks + super_block.inode_area_blocks; + let efs = Self { + block_device, + inode_bitmap: Bitmap::new( + 1, + super_block.inode_bitmap_blocks as usize + ), + data_bitmap: Bitmap::new( + (1 + inode_total_blocks) as usize, + super_block.data_bitmap_blocks as usize, + ), + inode_area_start_block: 1 + super_block.inode_bitmap_blocks, + data_area_start_block: 1 + inode_total_blocks + super_block.data_bitmap_blocks, + }; + Arc::new(Mutex::new(efs)) + }) + } + } + +它只需将块设备编号为 0 的块作为超级块读取进来,就可以从中知道 easy-fs 的磁盘布局,由此可以构造 ``efs`` 实例。 + +``EasyFileSystem`` 知道整个磁盘布局,可以从 inode 或数据块从位图上分配的从零开始编号知道它们在磁盘上的实际位置。 + +.. code-block:: rust + + // easy-fs/src/efs.rs + + impl EasyFileSystem { + pub fn get_disk_inode_pos(&self, inode_id: u32) -> (u32, usize) { + let inode_size = core::mem::size_of::(); + let inodes_per_block = (BLOCK_SZ / inode_size) as u32; + let block_id = self.inode_area_start_block + inode_id / inodes_per_block; + (block_id, (inode_id % inodes_per_block) as usize * inode_size) + } + + pub fn get_data_block_id(&self, data_block_id: u32) -> u32 { + self.data_area_start_block + data_block_id + } + } + +inode 和数据块的分配/回收也由它负责: + +.. code-block:: rust + + // easy-fs/src/efs.rs + + impl EasyFileSystem { + pub fn alloc_inode(&mut self) -> u32 { + self.inode_bitmap.alloc(&self.block_device).unwrap() as u32 + } + + /// Return a block ID not ID in the data area. + pub fn alloc_data(&mut self) -> u32 { + self.data_bitmap.alloc(&self.block_device).unwrap() as u32 + self.data_area_start_block + } + + pub fn dealloc_data(&mut self, block_id: u32) { + get_block_cache( + block_id as usize, + Arc::clone(&self.block_device) + ) + .lock() + .modify(0, |data_block: &mut DataBlock| { + data_block.iter_mut().for_each(|p| { *p = 0; }) + }); + self.data_bitmap.dealloc( + &self.block_device, + (block_id - self.data_area_start_block) as usize + ) + } + } + +注意: + +- ``alloc_data`` 和 ``dealloc_data`` 分配/回收数据块传入/返回的参数都表示数据块在块设备上的编号,而不是在数据块位图中分配的比特编号; +- ``dealloc_inode`` 未实现,因为现在还不支持文件删除。 + +索引节点 +--------------------------------------- + +本层的代码在 ``vfs.rs`` 中。 + +``EasyFileSystem`` 实现了我们设计的磁盘布局并能够将所有块有效的管理起来。但是对于库的使用者而言更希望能够直接看到目录树结构中逻辑上的文件和目录,他们往往不关心磁盘布局是如何实现的。为此我们设计索引节点 ``Inode`` 暴露给库的使用者,让他们能够直接对文件和目录进行操作。 ``Inode`` 和 ``DiskInode`` 的区别从它们的名字中就可以看出: ``DiskInode`` 放在磁盘块中比较固定的位置,而 ``Inode`` 是放在内存中的。 + +.. code-block:: rust + + // easy-fs/src/vfs.rs + + pub struct Inode { + block_id: usize, + block_offset: usize, + fs: Arc>, + block_device: Arc, + } + +``block_id`` 和 ``block_offset`` 记录该 ``Inode`` 对应的 ``DiskInode`` 保存在磁盘上的具体位置方便我们后续对它进行访问。 ``fs`` 是指向 ``EasyFileSystem`` 的一个指针,因为 ``Inode`` 的种种操作实际上都是要通过底层的文件系统来完成。 + +仿照 ``BlockCache::read/modify`` ,我们可以设计两个方法来简化对于 ``Inode`` 对应的磁盘上的 ``DiskInode`` 的访问流程,而不是每次都需要 ``get_block_cache.lock.read/modify`` : + +.. code-block:: rust + + // easy-fs/src/vfs.rs + + impl Inode { + fn read_disk_inode(&self, f: impl FnOnce(&DiskInode) -> V) -> V { + get_block_cache( + self.block_id, + Arc::clone(&self.block_device) + ).lock().read(self.block_offset, f) + } + + fn modify_disk_inode(&self, f: impl FnOnce(&mut DiskInode) -> V) -> V { + get_block_cache( + self.block_id, + Arc::clone(&self.block_device) + ).lock().modify(self.block_offset, f) + } + } + +下面我们分别介绍库的使用者对于文件系统的一些常用操作: + +获取根目录 inode ++++++++++++++++++++++++++++++++++++++++ + +库的使用者在通过 ``EasyFileSystem::open`` 从装载了 easy-fs 镜像的块设备上打开 easy-fs 之后,要做的第一件事情就是获取根目录的 ``Inode`` 。因为我们目前仅支持绝对路径,对于任何文件/目录的索引都必须从根目录开始向下逐级进行。等到索引完成之后,我们才能对文件/目录进行操作。事实上 ``EasyFileSystem`` 提供了另一个名为 ``root_inode`` 的方法来获取根目录的 ``Inode`` : + +.. code-block:: rust + + // easy-fs/src/efs.rs + + impl EasyFileSystem { + pub fn root_inode(efs: &Arc>) -> Inode { + let block_device = Arc::clone(&efs.lock().block_device); + Inode::new( + 0, + Arc::clone(efs), + block_device, + ) + } + } + + // easy-fs/src/vfs.rs + + impl Inode { + pub fn new( + inode_id: u32, + fs: Arc>, + block_device: Arc, + ) -> Self { + let (block_id, block_offset) = fs.lock().get_disk_inode_pos(inode_id); + Self { + block_id: block_id as usize, + block_offset, + fs, + block_device, + } + } + } + +在 ``root_inode`` 中,主要是在 ``Inode::new`` 的时候将传入的 ``inode_id`` 设置为 0 ,因为根目录对应于文件系统中第一个分配的 inode ,因此它的 ``inode_id`` 总会是 0 。 + +文件索引 ++++++++++++++++++++++++++++++++++++++++ + +:ref:`前面 ` 提到过,为了尽可能简化我们的实现,我们所实现的是一个扁平化的文件系统,即在目录树上仅有一个目录——那就是作为根节点的根目录。所有的文件都在根目录下面。于是,我们不必实现目录索引,而文件索引也非常简单,仅需在根目录的目录项中根据文件名找到文件的 inode 编号即可。由于没有子目录的存在,这个过程只会进行一次。 + +.. code-block:: rust + + // easy-fs/src/vfs.rs + + impl Inode { + pub fn find(&self, name: &str) -> Option> { + let _ = self.fs.lock(); + self.read_disk_inode(|disk_inode| { + self.find_inode_id(name, disk_inode) + .map(|inode_id| { + Arc::new(Self::new( + inode_id, + self.fs.clone(), + self.block_device.clone(), + )) + }) + }) + } + + fn find_inode_id( + &self, + name: &str, + disk_inode: &DiskInode, + ) -> Option { + // assert it is a directory + assert!(disk_inode.is_dir()); + let file_count = (disk_inode.size as usize) / DIRENT_SZ; + let mut dirent = DirEntry::empty(); + for i in 0..file_count { + assert_eq!( + disk_inode.read_at( + DIRENT_SZ * i, + dirent.as_bytes_mut(), + &self.block_device, + ), + DIRENT_SZ, + ); + if dirent.name() == name { + return Some(dirent.inode_number() as u32); + } + } + None + } + } + +``find`` 方法只会被根目录 ``Inode`` 调用,文件系统中其他文件的 ``Inode`` 不会调用这个方法。它首先调用 ``find_inode_id`` 方法尝试从根目录的 ``DiskInode`` 上找到要索引的文件名对应的 inode 编号。这就需要将根目录内容中的所有目录项都读到内存进行逐个比对。如果能够找到的话, ``find`` 方法会根据查到 inode 编号对应生成一个 ``Inode`` 用于后续对文件的访问。 + +这里需要注意的是,包括 ``find`` 在内所有暴露给库使用者的文件系统操作(还包括接下来将要介绍的几种),全程均需持有 ``EasyFileSystem`` 的互斥锁。这能够保证在多核情况下,同时最多只能有一个核在进行文件系统相关操作。这样也许会带来一些不必要的性能损失,但我们目前暂时先这样做。如果我们在这里加锁的话,其实就能够保证块缓存的互斥访问了。 + +文件列举 ++++++++++++++++++++++++++++++++++++++++ + +``ls`` 方法可以收集根目录下的所有文件的文件名并以向量的形式返回回来,这个方法只有根目录的 ``Inode`` 才会调用: + +.. code-block:: rust + + // easy-fs/src/vfs.rs + + impl Inode { + pub fn ls(&self) -> Vec { + let _ = self.fs.lock(); + self.read_disk_inode(|disk_inode| { + let file_count = (disk_inode.size as usize) / DIRENT_SZ; + let mut v: Vec = Vec::new(); + for i in 0..file_count { + let mut dirent = DirEntry::empty(); + assert_eq!( + disk_inode.read_at( + i * DIRENT_SZ, + dirent.as_bytes_mut(), + &self.block_device, + ), + DIRENT_SZ, + ); + v.push(String::from(dirent.name())); + } + v + }) + } + } + +文件创建 ++++++++++++++++++++++++++++++++++++++++ + +``create`` 方法可以在根目录下创建一个文件,该方法只有根目录的 ``Inode`` 会调用: + +.. code-block:: rust + :linenos: + + // easy-fs/src/vfs.rs + + impl Inode { + pub fn create(&self, name: &str) -> Option> { + let mut fs = self.fs.lock(); + if self.modify_disk_inode(|root_inode| { + // assert it is a directory + assert!(root_inode.is_dir()); + // has the file been created? + self.find_inode_id(name, root_inode) + }).is_some() { + return None; + } + // create a new file + // alloc a inode with an indirect block + let new_inode_id = fs.alloc_inode(); + // initialize inode + let (new_inode_block_id, new_inode_block_offset) + = fs.get_disk_inode_pos(new_inode_id); + get_block_cache( + new_inode_block_id as usize, + Arc::clone(&self.block_device) + ).lock().modify(new_inode_block_offset, |new_inode: &mut DiskInode| { + new_inode.initialize(DiskInodeType::File); + }); + self.modify_disk_inode(|root_inode| { + // append file in the dirent + let file_count = (root_inode.size as usize) / DIRENT_SZ; + let new_size = (file_count + 1) * DIRENT_SZ; + // increase size + self.increase_size(new_size as u32, root_inode, &mut fs); + // write dirent + let dirent = DirEntry::new(name, new_inode_id); + root_inode.write_at( + file_count * DIRENT_SZ, + dirent.as_bytes(), + &self.block_device, + ); + }); + // release efs lock manually because we will acquire it again in Inode::new + drop(fs); + // return inode + Some(Arc::new(Self::new( + new_inode_id, + self.fs.clone(), + self.block_device.clone(), + ))) + } + } + +- 第 6~13 行,检查文件是否已经在根目录下,如果找到的话返回 ``None`` ; +- 第 14~25 行,为待创建文件分配一个新的 inode 并进行初始化; +- 第 26~39 行,将待创建文件的目录项插入到根目录的内容中使得之后可以索引过来。 + +文件清空 ++++++++++++++++++++++++++++++++++++++++ + +在以某些标志位打开文件(例如带有 *CREATE* 标志打开一个已经存在的文件)的时候,需要首先将文件清空。在索引到文件的 ``Inode`` 之后可以调用 ``clear`` 方法: + +.. code-block:: rust + + // easy-fs/src/vfs.rs + + impl Inode { + pub fn clear(&self) { + let mut fs = self.fs.lock(); + self.modify_disk_inode(|disk_inode| { + let size = disk_inode.size; + let data_blocks_dealloc = disk_inode.clear_size(&self.block_device); + assert!(data_blocks_dealloc.len() == DiskInode::total_blocks(size) as usize); + for data_block in data_blocks_dealloc.into_iter() { + fs.dealloc_data(data_block); + } + }); + } + } + +这会将之前该文件占据的索引块和数据块在 ``EasyFileSystem`` 中回收。 + +文件读写 ++++++++++++++++++++++++++++++++++++++++ + +从根目录索引到一个文件之后可以对它进行读写,注意,和 ``DiskInode`` 一样,这里的读写作用在字节序列的一段区间上: + +.. code-block:: rust + + // easy-fs/src/vfs.rs + + impl Inode { + pub fn read_at(&self, offset: usize, buf: &mut [u8]) -> usize { + let _ = self.fs.lock(); + self.read_disk_inode(|disk_inode| { + disk_inode.read_at(offset, buf, &self.block_device) + }) + } + + pub fn write_at(&self, offset: usize, buf: &[u8]) -> usize { + let mut fs = self.fs.lock(); + self.modify_disk_inode(|disk_inode| { + self.increase_size((offset + buf.len()) as u32, disk_inode, &mut fs); + disk_inode.write_at(offset, buf, &self.block_device) + }) + } + } + +实现比较简单,需要注意在 ``DiskInode::write_at`` 之前先调用 ``increase_size`` 对自身进行扩容: + +.. code-block:: rust + + // easy-fs/src/vfs.rs + + impl Inode { + fn increase_size( + &self, + new_size: u32, + disk_inode: &mut DiskInode, + fs: &mut MutexGuard, + ) { + if new_size < disk_inode.size { + return; + } + let blocks_needed = disk_inode.blocks_num_needed(new_size); + let mut v: Vec = Vec::new(); + for _ in 0..blocks_needed { + v.push(fs.alloc_data()); + } + disk_inode.increase_size(new_size, v, &self.block_device); + } + } + +这里会从 ``EasyFileSystem`` 中分配一些用于扩容的数据块并传给 ``DiskInode::increase_size`` 。 + +测试 easy-fs +--------------------------------------- + +``easy-fs`` 架构设计的一个优点在于它可以在我们的开发环境(Windows/macOS/Ubuntu)上进行测试,不必过早的放到内核中。众所周知,内核运行在裸机环境上,在上面是很难调试的。而在我们的开发环境上对于调试的支持更为完善,从基于命令行的 GDB 到 IDE 提供的图形化调试界面都能给我们带来很大帮助。另外一点是,由于需要放到在裸机上运行的内核中, ``easy-fs`` 只能使用 ``no_std`` 模式,因此无法使用 ``println!`` 等宏来打印调试信息。但是在我们的开发环境上作为一个应用运行的时候,我们可以暂时让它使用标准库 ``std`` ,这也会带来一些方便。 + +``easy-fs`` 的测试放在另一个名为 ``easy-fs-fuse`` 的 crate 中,不同于 ``easy-fs`` ,它是一个支持 ``std`` 的二进制 crate ,能够在开发环境上运行并很容易调试。 + +在开发环境中模拟块设备 ++++++++++++++++++++++++++++++++++++++++ + +从库使用者的角度来看,它仅需要提供一个实现了 ``BlockDevice`` Trait 的块设备用来装载文件系统,之后就可以使用 ``Inode`` 来方便的进行文件系统操作了。但是在开发环境上,我们如何来提供这样一个块设备呢?答案是用 Host OS 上的一个文件进行模拟。 + +.. code-block:: rust + + // easy-fs-fuse/src/main.rs + + use std::fs::File; + use easy-fs::BlockDevice; + + const BLOCK_SZ: usize = 512; + + struct BlockFile(Mutex); + + impl BlockDevice for BlockFile { + fn read_block(&self, block_id: usize, buf: &mut [u8]) { + let mut file = self.0.lock().unwrap(); + file.seek(SeekFrom::Start((block_id * BLOCK_SZ) as u64)) + .expect("Error when seeking!"); + assert_eq!(file.read(buf).unwrap(), BLOCK_SZ, "Not a complete block!"); + } + + fn write_block(&self, block_id: usize, buf: &[u8]) { + let mut file = self.0.lock().unwrap(); + file.seek(SeekFrom::Start((block_id * BLOCK_SZ) as u64)) + .expect("Error when seeking!"); + assert_eq!(file.write(buf).unwrap(), BLOCK_SZ, "Not a complete block!"); + } + } + +``std::file::File`` 由 Rust 标准库 std 提供,可以访问 Host OS 上的一个文件。我们将它包装成 ``BlockFile`` 类型来模拟一块磁盘,为它实现 ``BlockDevice`` 接口。注意 ``File`` 本身仅通过 ``read/write`` 接口是不能实现随机读写的,在访问一个特定的块的时候,我们必须先 ``seek`` 到这个块的开头位置。 + +测试主函数为 ``easy-fs-fuse/src/main.rs`` 中的 ``efs_test`` 函数中,我们只需在 ``easy-fs-fuse`` 目录下 ``cargo test`` 即可执行该测试: + +.. code-block:: + + running 1 test + test efs_test ... ok + + test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.27s + +看到上面的内容就说明测试通过了。 + +``efs_test`` 展示了 ``easy-fs`` 库的使用方法,大致分成以下几个步骤: + +打开块设备 ++++++++++++++++++++++++++++++++++++++++ + +.. code-block:: rust + + let block_file = Arc::new(BlockFile(Mutex::new({ + let f = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open("target/fs.img")?; + f.set_len(8192 * 512).unwrap(); + f + }))); + EasyFileSystem::create( + block_file.clone(), + 4096, + 1, + ); + +第一步我们需要打开块设备。这里我们在 HostOS 创建文件 ``easy-fs-fuse/target/fs.img`` 来新建一个块设备,并将它的容量设置为 8192 个块即 4MiB 。在创建的时候需要将它的访问权限设置为可读可写。 + +由于我们在进行测试,需要初始化测试环境,因此我们在块设备 ``block_file`` 上初始化 easy-fs 文件系统,这会将 ``block_file`` 用于放置 easy-fs 镜像的前 4096 个块上的数据覆盖,然后变成仅有一个根目录的初始文件系统。如果块设备上已经放置了一个合法的 easy-fs 镜像,则我们不必这样做。 + +从块设备上打开文件系统 ++++++++++++++++++++++++++++++++++++++++ + +.. code-block:: rust + + let efs = EasyFileSystem::open(block_file.clone()); + +这是通常进行的第二个步骤。 + +获取根目录的 Inode ++++++++++++++++++++++++++++++++++++++++ + +.. code-block:: rust + + let root_inode = EasyFileSystem::root_inode(&efs); + +这是通常进行的第三个步骤。 + +进行文件系统操作 ++++++++++++++++++++++++++++++++++++++++ + +拿到根目录 ``root_inode`` 之后,可以通过它进行文件系统操作,目前支持以下几种: + +- 通过 ``create`` 创建文件。 +- 通过 ``ls`` 列举根目录下的文件。 +- 通过 ``find`` 根据文件名索引文件。 + +当通过索引获取根目录下的一个文件的 inode 之后则可以进行如下操作: + +- 通过 ``clear`` 将文件内容清空。 +- 通过 ``read/write_at`` 读写文件,注意我们需要将读写在文件中开始的位置 ``offset`` 作为一个参数传递进去。 + +测试方法在这里不详细介绍,大概是每次清空文件 ``filea`` 的内容,向其中写入一个不同长度的随机数字字符串,然后再全部读取出来,验证和写入的内容一致。其中有一个细节是:用来生成随机字符串的 ``rand`` crate 并不支持 ``no_std`` ,因此只有在用户态我们才能更容易进行测试。 + +将应用打包为 easy-fs 镜像 +--------------------------------------- + +在第六章中我们需要将所有的应用都链接到内核中,随后在应用管理器中通过应用名进行索引来找到应用的 ELF 数据。这样做有一个缺点,就是会造成内核体积过度膨胀。在 k210 平台上可以很明显的感觉到从第五章开始随着应用数量的增加,向开发板上烧写内核镜像的耗时显著增长。同时这也会浪费内存资源,因为未被执行的应用也占据了内存空间。在实现了我们自己的文件系统之后,终于可以将这些应用打包到 easy-fs 镜像中放到磁盘中,当我们要执行应用的时候只需从文件系统中取出应用 ELF 并加载到内存中执行即可,这样就避免了上面的那些问题。 + +``easy-fs-fuse`` 的主体 ``easy-fs-pack`` 函数就实现了这个功能: + +.. code-block:: rust + :linenos: + + // easy-fs-fuse/src/main.rs + + use clap::{Arg, App}; + + fn easy_fs_pack() -> std::io::Result<()> { + let matches = App::new("EasyFileSystem packer") + .arg(Arg::with_name("source") + .short("s") + .long("source") + .takes_value(true) + .help("Executable source dir(with backslash)") + ) + .arg(Arg::with_name("target") + .short("t") + .long("target") + .takes_value(true) + .help("Executable target dir(with backslash)") + ) + .get_matches(); + let src_path = matches.value_of("source").unwrap(); + let target_path = matches.value_of("target").unwrap(); + println!("src_path = {}\ntarget_path = {}", src_path, target_path); + let block_file = Arc::new(BlockFile(Mutex::new({ + let f = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(format!("{}{}", target_path, "fs.img"))?; + f.set_len(8192 * 512).unwrap(); + f + }))); + // 4MiB, at most 4095 files + let efs = EasyFileSystem::create( + block_file.clone(), + 8192, + 1, + ); + let root_inode = Arc::new(EasyFileSystem::root_inode(&efs)); + let apps: Vec<_> = read_dir(src_path) + .unwrap() + .into_iter() + .map(|dir_entry| { + let mut name_with_ext = dir_entry.unwrap().file_name().into_string().unwrap(); + name_with_ext.drain(name_with_ext.find('.').unwrap()..name_with_ext.len()); + name_with_ext + }) + .collect(); + for app in apps { + // load app data from host file system + let mut host_file = File::open(format!("{}{}", target_path, app)).unwrap(); + let mut all_data: Vec = Vec::new(); + host_file.read_to_end(&mut all_data).unwrap(); + // create a file in easy-fs + let inode = root_inode.create(app.as_str()).unwrap(); + // write data to easy-fs + inode.write_at(0, all_data.as_slice()); + } + // list apps + for app in root_inode.ls() { + println!("{}", app); + } + Ok(()) + } + +- 为了实现 ``easy-fs-fuse`` 和 ``os/user`` 的解藕,第 6~21 行使用 ``clap`` crate 进行命令行参数解析,需要通过 ``-s`` 和 ``-t`` 分别指定应用的源代码目录和保存应用 ELF 的目录而不是在 ``easy-fs-fuse`` 中硬编码。如果解析成功的话它们会分别被保存在变量 ``src_path`` 和 ``target_path`` 中。 +- 第 23~38 行依次完成:创建 4MiB 的 easy-fs 镜像文件、进行 easy-fs 初始化、获取根目录 inode 。 +- 第 39 行获取源码目录中的每个应用的源代码文件并去掉后缀名,收集到向量 ``apps`` 中。 +- 第 48 行开始,枚举 ``apps`` 中的每个应用,从应用 ELF 目录中找到对应应用的 ELF 文件(这是一个 HostOS 上的文件)并将数据读入内存。接着需要在我们的 easy-fs 中创建一个同名文件并将 ELF 数据写入到这个文件中。这个过程相当于将 HostOS 上的文件系统中的一个文件复制到我们的 easy-fs 中。 + +尽管没有进行任何同步回磁盘的操作,我们也不用担心块缓存中的修改没有写回磁盘。因为在 ``easy-fs-fuse`` 这个应用正常退出的过程中,块缓存因生命周期结束会被回收,届时如果 ``modified`` 标志为 true 就会将修改写回磁盘。 \ No newline at end of file diff --git a/source/chapter7/3using-easy-fs-in-kernel.rst b/source/chapter7/3using-easy-fs-in-kernel.rst new file mode 100644 index 0000000000000000000000000000000000000000..75f3d598d06cdc97d7a64339ed37ac52589c2e04 --- /dev/null +++ b/source/chapter7/3using-easy-fs-in-kernel.rst @@ -0,0 +1,531 @@ +在内核中使用 easy-fs +=============================================== + +本节导读 +----------------------------------------------- + +本节我们介绍如何将 ``easy-fs`` 文件系统接入内核中从而在内核中支持标准文件和目录。这自下而上可以分成这样几个层次: + +- 块设备驱动层。针对内核所要运行在的 qemu 或 k210 平台,我们需要将平台上的块设备驱动起来并实现 ``easy-fs`` 所需的 ``BlockDevice`` Trait ,这样 ``easy-fs`` 才能将该块设备用作 easy-fs 镜像的载体。 +- ``easy-fs`` 层。我们在上一节已经介绍了它内部的层次划分。这里是站在内核的角度,只需知道它接受一个块设备 ``BlockDevice`` ,并可以在上面打开文件系统 ``EasyFileSystem`` ,进而获取 ``Inode`` 进行各种文件系统操作即可。 +- 内核索引节点层。在内核中需要将 ``easy-fs`` 提供的 ``Inode`` 进一步封装成 ``OSInode`` 表示进程中一个打开的标准文件。由于有很多种不同的打开方式,因此在 ``OSInode`` 中要维护一些额外的信息。 +- 文件描述符层。标准文件对应的 ``OSInode`` 也是一种文件,因此也需要为它实现 ``File`` Trait 从而能够可以将它放入到进程文件描述符表中并通过 ``sys_read/write`` 系统调用进行读写。 +- 系统调用层。针对标准文件这种新的文件类型的加入,一些系统调用以及相关的内核机制需要进行修改。 + +块设备驱动层 +----------------------------------------------- + +在 ``drivers`` 子模块中的 ``block/mod.rs`` 中,我们可以找到内核访问的块设备实例 ``BLOCK_DEVICE`` : + +.. code-block:: rust + + // os/drivers/block/mod.rs + + #[cfg(feature = "board_qemu")] + type BlockDeviceImpl = virtio_blk::VirtIOBlock; + + #[cfg(feature = "board_k210")] + type BlockDeviceImpl = sdcard::SDCardWrapper; + + lazy_static! { + pub static ref BLOCK_DEVICE: Arc = Arc::new(BlockDeviceImpl::new()); + } + +qemu 和 k210 平台上的块设备是不同的。在 qemu 上,我们使用 ``VirtIOBlock`` 访问 VirtIO 块设备;而在 k210 上,我们使用 ``SDCardWrapper`` 来访问插入 k210 开发板上真实的 microSD 卡,它们都实现了 ``easy-fs`` 要求的 ``BlockDevice`` Trait 。通过 ``#[cfg(feature)]`` 可以在编译的时候根据编译参数调整 ``BlockDeviceImpl`` 具体为哪个块设备,之后将它全局实例化为 ``BLOCK_DEVICE`` 使得内核的其他模块可以访问。 + +Qemu 模拟器平台 ++++++++++++++++++++++++++++++++++++++++++++++++ + +在启动 Qemu 模拟器的时候,我们可以配置参数来添加一块 VirtIO 块设备: + +.. code-block:: makefile + :linenos: + :emphasize-lines: 12-13 + + # os/Makefile + + FS_IMG := ../user/target/$(TARGET)/$(MODE)/fs.img + + run-inner: build + ifeq ($(BOARD),qemu) + @qemu-system-riscv64 \ + -machine virt \ + -nographic \ + -bios $(BOOTLOADER) \ + -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) \ + -drive file=$(FS_IMG),if=none,format=raw,id=x0 \ + -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 + +- 第 12 行,我们为虚拟机添加一块虚拟硬盘,内容为我们之前通过 ``easy-fs-fuse`` 工具打包的包含应用 ELF 的 easy-fs 镜像,并命名为 ``x0`` 。 +- 第 13 行,我们将硬盘 ``x0`` 作为一个 VirtIO 总线中的一个块设备接入到虚拟机系统中。 ``virtio-mmio-bus.0`` 表示 VirtIO 总线通过 MMIO 进行控制,且该块设备在总线中的编号为 0 。 + +**内存映射 I/O** (MMIO, Memory-Mapped I/O) 指的是外设的设备寄存器可以通过物理地址来访问,每个外设的设备寄存器都分布在一个或数个物理地址区间中,外设两两之间不会产生冲突,且这些物理地址区间也不会和物理内存所在的区间存在交集。从 RV64 平台 Qemu 的 `源码 `_ 中可以找到 VirtIO 总线的 MMIO 物理地址区间为从 0x10001000 开头的 4KiB 。为了能够在内核中访问 VirtIO 总线,我们就必须在内核地址空间中提前进行映射: + +.. code-block:: rust + + // os/src/config.rs + + #[cfg(feature = "board_qemu")] + pub const MMIO: &[(usize, usize)] = &[ + (0x10001000, 0x1000), + ]; + +如上面一段代码所示,在 ``config`` 子模块中我们硬编码 Qemu 上仅有一段访问 VirtIO 总线的 MMIO 区间。在创建内核地址空间的时候需要进行映射: + +.. code-block:: rust + + // os/src/mm/memory_set.rs + + use crate::config::MMIO; + + impl MemorySet { + /// Without kernel stacks. + pub fn new_kernel() -> Self { + ... + println!("mapping memory-mapped registers"); + for pair in MMIO { + memory_set.push(MapArea::new( + (*pair).0.into(), + ((*pair).0 + (*pair).1).into(), + MapType::Identical, + MapPermission::R | MapPermission::W, + ), None); + } + memory_set + } + } + +这里我们进行的是透明的恒等映射从而让内核可以兼容于直接访问物理地址的设备驱动库。 + +由于设备驱动的开发过程比较琐碎且与 OS 课程没有很大关系,我们这里直接使用已有的 `virtio-drivers `_ crate ,它已经支持 VirtIO 总线架构下的块设备、网络设备、GPU 等设备。 + +.. code-block:: rust + + // os/src/drivers/block/virtio_blk.rs + + use virtio_drivers::{VirtIOBlk, VirtIOHeader}; + const VIRTIO0: usize = 0x10001000; + + pub struct VirtIOBlock(Mutex>); + + impl VirtIOBlock { + pub fn new() -> Self { + Self(Mutex::new(VirtIOBlk::new( + unsafe { &mut *(VIRTIO0 as *mut VirtIOHeader) } + ).unwrap())) + } + } + + impl BlockDevice for VirtIOBlock { + fn read_block(&self, block_id: usize, buf: &mut [u8]) { + self.0.lock().read_block(block_id, buf).expect("Error when reading VirtIOBlk"); + } + fn write_block(&self, block_id: usize, buf: &[u8]) { + self.0.lock().write_block(block_id, buf).expect("Error when writing VirtIOBlk"); + } + } + +上面的代码中,我们将 ``virtio-drivers`` crate 提供的 VirtIO 块设备抽象 ``VirtIOBlk`` 包装为我们自己的 ``VirtIOBlock`` ,实质上只是加上了一层互斥锁,生成一个新的类型来实现 ``easy-fs`` 需要的 ``BlockDevice`` Trait 。注意在 ``VirtIOBlk::new`` 的时候需要传入一个 ``&mut VirtIOHeader`` 的参数, ``VirtIOHeader`` 实际上就代表以 MMIO 方式访问 VirtIO 设备所需的一组设备寄存器。因此我们从 ``qemu-system-riscv64`` 平台上的 Virtio MMIO 区间左端 ``VIRTIO0`` 开始转化为一个 ``&mut VirtIOHeader`` 就可以在该平台上访问这些设备寄存器了。 + +很容易为 ``VirtIOBlock`` 实现 ``BlockDevice`` Trait ,因为它内部来自 ``virtio-drivers`` crate 的 ``VirtIOBlk`` 类型已经实现了 ``read/write_block`` 方法,我们进行转发即可。 + +VirtIO 设备需要占用部分内存作为一个公共区域从而更好的和 CPU 进行合作。这就像 MMU 需要在内存中保存多级页表才能和 CPU 共同实现分页机制一样。在 VirtIO 架构下,需要在公共区域中放置一种叫做 VirtQueue 的环形队列,CPU 可以向此环形队列中向 VirtIO 设备提交请求,也可以从队列中取得请求的结果,详情可以参考 `virtio 文档 `_ 。对于 VirtQueue 的使用涉及到物理内存的分配和回收,但这并不在 VirtIO 驱动 ``virtio-drivers`` 的职责范围之内,因此它声明了数个相关的接口,需要库的使用者自己来实现: + +.. code-block:: rust + + // https://github.com/rcore-os/virtio-drivers/blob/master/src/hal.rs#L57 + + extern "C" { + fn virtio_dma_alloc(pages: usize) -> PhysAddr; + fn virtio_dma_dealloc(paddr: PhysAddr, pages: usize) -> i32; + fn virtio_phys_to_virt(paddr: PhysAddr) -> VirtAddr; + fn virtio_virt_to_phys(vaddr: VirtAddr) -> PhysAddr; + } + +由于我们已经实现了基于分页内存管理的地址空间,实现这些功能自然不在话下: + +.. code-block:: rust + + // os/src/drivers/block/virtio_blk.rs + + lazy_static! { + static ref QUEUE_FRAMES: Mutex> = Mutex::new(Vec::new()); + } + + #[no_mangle] + pub extern "C" fn virtio_dma_alloc(pages: usize) -> PhysAddr { + let mut ppn_base = PhysPageNum(0); + for i in 0..pages { + let frame = frame_alloc().unwrap(); + if i == 0 { ppn_base = frame.ppn; } + assert_eq!(frame.ppn.0, ppn_base.0 + i); + QUEUE_FRAMES.lock().push(frame); + } + ppn_base.into() + } + + #[no_mangle] + pub extern "C" fn virtio_dma_dealloc(pa: PhysAddr, pages: usize) -> i32 { + let mut ppn_base: PhysPageNum = pa.into(); + for _ in 0..pages { + frame_dealloc(ppn_base); + ppn_base.step(); + } + 0 + } + + #[no_mangle] + pub extern "C" fn virtio_phys_to_virt(paddr: PhysAddr) -> VirtAddr { + VirtAddr(paddr.0) + } + + #[no_mangle] + pub extern "C" fn virtio_virt_to_phys(vaddr: VirtAddr) -> PhysAddr { + PageTable::from_token(kernel_token()).translate_va(vaddr).unwrap() + } + +这里有一些细节需要注意: + +- ``virtio_dma_alloc/dealloc`` 需要分配/回收数个 *连续* 的物理页帧,而我们的 ``frame_alloc`` 是逐个分配,严格来说并不保证分配的连续性。幸运的是,这个过程只会发生在内核初始化阶段,因此能够保证连续性。 +- 在 ``virtio_dma_alloc`` 中通过 ``frame_alloc`` 得到的那些物理页帧 ``FrameTracker`` 都会被保存在全局的向量 ``QUEUE_FRAMES`` 以延长它们的生命周期,避免提前被回收。 + + +K210 真实硬件平台 ++++++++++++++++++++++++++++++++++++++++++++++++ + +在 K210 开发板上,我们可以插入 microSD 卡并将其作为块设备。相比 VirtIO 块设备来说,想要将 microSD 驱动起来是一件相当困难的事情。SD 自身的通信规范就已经非常复杂了,在 K210 上它还是挂在 **串行外设接口** (SPI, Serial Peripheral Interface) 总线下。此外还需要正确设置 GPIO 的管脚映射并调整各锁相环的频率。实际上,在一块小小的芯片中除了 K210 CPU 之外,还集成了很多不同种类的外设和控制模块,它们内在的关联比较紧密,不能像 VirtIO 设备那样从系统中独立出来。 + +好在目前 Rust 嵌入式的生态正高速发展,针对 K210 平台也有比较成熟的封装了各类外设接口的库可以用来开发上层应用。但是其功能往往分散为多个 crate ,在使用的时候需要开发者根据需求自行进行组装。这属于 Rust 的特点之一,和 C 语言提供一个一站式的板级开发包风格有很大的不同。在开发的时候,笔者就从社区中选择了一些 crate 并进行了微量修改最终变成 ``k210-hal/k210-pac/k210-soc`` 三个能够运行在 S 特权级(它们的原身仅支持运行在 M 特权级)的 crate ,它们可以更加便捷的实现 microSD 的驱动。关于 microSD 的驱动 ``SDCardWrapper`` 的实现,有兴趣的读者可以参考 ``os/src/drivers/block/sdcard.rs`` 。 + +.. note:: + + **感谢相关 crate 的原身** + + - `k210-hal `_ + - `k210-pac `_ + - `k210-sdk-stuff `_ + +要在 K210 上启用 microSD ,执行的时候无需任何改动,只需在 ``make run`` 之前将 microSD 插入 PC 再通过 ``make sdcard`` 将 easy-fs 镜像烧写进去即可。而后,将 microSD 插入 K210 开发板,连接到 PC 再 ``make run`` 。 + +在对 microSD 进行操作的时候,基本上要涉及到 K210 内置的所有外设,正所谓”牵一发而动全身“。因此 K210 平台上的 MMIO 包含很多区间: + +.. code-block:: rust + + // os/src/config.rs + + #[cfg(feature = "board_k210")] + pub const MMIO: &[(usize, usize)] = &[ + // we don't need clint in S priv when running + // we only need claim/complete for target0 after initializing + (0x0C00_0000, 0x3000), /* PLIC */ + (0x0C20_0000, 0x1000), /* PLIC */ + (0x3800_0000, 0x1000), /* UARTHS */ + (0x3800_1000, 0x1000), /* GPIOHS */ + (0x5020_0000, 0x1000), /* GPIO */ + (0x5024_0000, 0x1000), /* SPI_SLAVE */ + (0x502B_0000, 0x1000), /* FPIOA */ + (0x502D_0000, 0x1000), /* TIMER0 */ + (0x502E_0000, 0x1000), /* TIMER1 */ + (0x502F_0000, 0x1000), /* TIMER2 */ + (0x5044_0000, 0x1000), /* SYSCTL */ + (0x5200_0000, 0x1000), /* SPI0 */ + (0x5300_0000, 0x1000), /* SPI1 */ + (0x5400_0000, 0x1000), /* SPI2 */ + ]; + +内核索引节点层 +----------------------------------------------- + +在本章的第一小节我们介绍过,站在用户的角度看来,在一个进程中可以使用多种不同的标志来打开一个文件,这会影响到打开的这个文件可以用何种方式被访问。此外,在连续调用 ``sys_read/write`` 读写一个文件的时候,我们知道在某些地方存在着一个偏移量也在被不断更新,使得我们总是在顺序读写文件。这些用户视角中的文件系统抽象特征需要内核来实现,因为可以看出 ``easy-fs`` 是没有涉及到它们的。因此,我们需要将 ``easy-fs`` 提供的 ``Inode`` 进一步封装为 OS 中的索引节点 ``OSInode`` : + +.. code-block:: rust + + // os/src/fs/inode.rs + + pub struct OSInode { + readable: bool, + writable: bool, + inner: Mutex, + } + + pub struct OSInodeInner { + offset: usize, + inode: Arc, + } + + impl OSInode { + pub fn new( + readable: bool, + writable: bool, + inode: Arc, + ) -> Self { + Self { + readable, + writable, + inner: Mutex::new(OSInodeInner { + offset: 0, + inode, + }), + } + } + } + +``OSInode`` 就表示进程中一个被打开的标准文件或目录。 ``readable/writable`` 分别表明该文件是否允许通过 ``sys_read/write`` 进行读写。至于在 ``sys_read/write`` 期间被维护偏移量 ``offset`` 和它在 ``easy-fs`` 中的 ``Inode`` 则加上一把互斥锁丢到 ``OSInodeInner`` 中。这在提供内部可变性的同时,也可以简单应对多个进程同时读写一个文件的情况。 + + +文件描述符层 +----------------------------------------------- + +因为 ``OSInode`` 也是要一种要丢到进程文件描述符表并通过 ``sys_read/write`` 系统调用进行读写的文件,因此我们也需要为它实现 ``File`` Trait : + +.. code-block:: rust + + // os/src/fs/inode.rs + + impl File for OSInode { + fn readable(&self) -> bool { self.readable } + fn writable(&self) -> bool { self.writable } + fn read(&self, mut buf: UserBuffer) -> usize { + let mut inner = self.inner.lock(); + let mut total_read_size = 0usize; + for slice in buf.buffers.iter_mut() { + let read_size = inner.inode.read_at(inner.offset, *slice); + if read_size == 0 { + break; + } + inner.offset += read_size; + total_read_size += read_size; + } + total_read_size + } + fn write(&self, buf: UserBuffer) -> usize { + let mut inner = self.inner.lock(); + let mut total_write_size = 0usize; + for slice in buf.buffers.iter() { + let write_size = inner.inode.write_at(inner.offset, *slice); + assert_eq!(write_size, slice.len()); + inner.offset += write_size; + total_write_size += write_size; + } + total_write_size + } + } + +本章我们为 ``File`` Trait 新增了 ``readable/writable`` 两个抽象接口从而在 ``sys_read/sys_write`` 的时候进行简单的访问权限检查。 ``read/write`` 的实现也比较简单,只需遍历 ``UserBuffer`` 中的每个缓冲区片段,调用 ``Inode`` 写好的 ``read/write_at`` 接口就好了。注意 ``read/write_at`` 的起始位置是在 ``OSInode`` 中维护的 ``offset`` ,这个 ``offset`` 也随着遍历的进行被持续更新。在 ``read/write`` 的全程需要获取 ``OSInode`` 的互斥锁,保证两个进程无法同时访问同个文件。 + +文件系统相关内核机制实现 +----------------------------------------------- + +文件系统初始化 ++++++++++++++++++++++++++++++++++++++++++++++++ + +在上一小节我们介绍过,为了使用 ``easy-fs`` 提供的抽象,我们需要进行一些初始化操作才能成功将 ``easy-fs`` 接入到我们的内核中。按照前面总结的步骤: + +1. 打开块设备。从本节前面可以看出,我们已经打开并可以访问装载有 easy-fs 文件系统镜像的块设备 ``BLOCK_DEVICE`` 。 +2. 从块设备 ``BLOCK_DEVICE`` 上打开文件系统。 +3. 从文件系统中获取根目录的 inode 。 + +2-3 步我们在这里完成: + +.. code-block:: rust + + // os/src/fs/inode.rs + + lazy_static! { + pub static ref ROOT_INODE: Arc = { + let efs = EasyFileSystem::open(BLOCK_DEVICE.clone()); + Arc::new(EasyFileSystem::root_inode(&efs)) + }; + } + +这之后就可以使用根目录的 inode ``ROOT_INODE`` 来在内核内操作我们的 easy-fs 了。例如,在文件系统初始化完毕之后,在内核主函数 ``rust_main`` 中调用 ``list_apps`` 函数来列举文件系统中可用的应用的文件名: + +.. code-block:: rust + + // os/src/fs/inode.rs + + pub fn list_apps() { + println!("/**** APPS ****"); + for app in ROOT_INODE.ls() { + println!("{}", app); + } + println!("**************/") + } + + +通过 sys_open 打开文件 ++++++++++++++++++++++++++++++++++++++++++++++++ + +我们需要在内核中也定义一份打开文件的标志 ``OpenFlags`` : + +.. code-block:: rust + + // os/src/fs/inode.rs + + bitflags! { + pub struct OpenFlags: u32 { + const RDONLY = 0; + const WRONLY = 1 << 0; + const RDWR = 1 << 1; + const CREATE = 1 << 9; + const TRUNC = 1 << 10; + } + } + + impl OpenFlags { + /// Do not check validity for simplicity + /// Return (readable, writable) + pub fn read_write(&self) -> (bool, bool) { + if self.is_empty() { + (true, false) + } else if self.contains(Self::WRONLY) { + (false, true) + } else { + (true, true) + } + } + } + +它的 ``read_write`` 方法可以根据标志的情况返回要打开的文件是否允许读写。简单起见,这里假设标志自身一定合法。 + +接着,我们实现 ``open_file`` 在 *内核* 中根据文件名打开一个根目录下的文件: + +.. code-block:: rust + + // os/src/fs/inode.rs + + pub fn open_file(name: &str, flags: OpenFlags) -> Option> { + let (readable, writable) = flags.read_write(); + if flags.contains(OpenFlags::CREATE) { + if let Some(inode) = ROOT_INODE.find(name) { + // clear size + inode.clear(); + Some(Arc::new(OSInode::new( + readable, + writable, + inode, + ))) + } else { + // create file + ROOT_INODE.create(name) + .map(|inode| { + Arc::new(OSInode::new( + readable, + writable, + inode, + )) + }) + } + } else { + ROOT_INODE.find(name) + .map(|inode| { + if flags.contains(OpenFlags::TRUNC) { + inode.clear(); + } + Arc::new(OSInode::new( + readable, + writable, + inode + )) + }) + } + } + +这里主要是实现了 ``OpenFlags`` 各标志位的语义。例如只有包含 `CREATE` 才允许创建文件,如果文件已经存在则清空文件的内容。另外我们将从 ``OpenFlags`` 解析得到的读写相关权限传入 ``OSInode`` 的创建过程中。 + +在其基础上, ``sys_open`` 也就很容易实现了: + +.. code-block:: rust + + // os/src/syscall/fs.rs + + pub fn sys_open(path: *const u8, flags: u32) -> isize { + let task = current_task().unwrap(); + let token = current_user_token(); + let path = translated_str(token, path); + if let Some(inode) = open_file( + path.as_str(), + OpenFlags::from_bits(flags).unwrap() + ) { + let mut inner = task.acquire_inner_lock(); + let fd = inner.alloc_fd(); + inner.fd_table[fd] = Some(inode); + fd as isize + } else { + -1 + } + } + +通过 sys_exec 加载并执行应用 ++++++++++++++++++++++++++++++++++++++++++++++++ + +在有了文件系统支持之后,我们在 ``sys_exec`` 所需的应用 ELF 数据就不再需要通过应用加载器从内核的数据段获取,而是从文件系统中获取即可: + +.. code-block:: rust + :linenos: + :emphasize-lines: 15-24 + + // os/src/syscall/process.rs + + pub fn sys_exec(path: *const u8, mut args: *const usize) -> isize { + let token = current_user_token(); + let path = translated_str(token, path); + let mut args_vec: Vec = Vec::new(); + loop { + let arg_str_ptr = *translated_ref(token, args); + if arg_str_ptr == 0 { + break; + } + args_vec.push(translated_str(token, arg_str_ptr as *const u8)); + unsafe { args = args.add(1); } + } + if let Some(app_inode) = open_file(path.as_str(), OpenFlags::RDONLY) { + let all_data = app_inode.read_all(); + let task = current_task().unwrap(); + let argc = args_vec.len(); + task.exec(all_data.as_slice(), args_vec); + // return argc because cx.x[10] will be covered with it later + argc as isize + } else { + -1 + } + } + +注意上面代码片段中的高亮部分。当需要获取应用的 ELF 数据的时候,首先调用 ``open_file`` 以只读的方式在内核中打开应用可执行文件并获取它的对应的 ``OSInode`` 。接下来可以通过 ``OSInode::read_all`` 将该文件的数据全部读到一个向量 ``all_data`` 中: + +.. code-block:: rust + + // os/src/fs/inode.rs + + impl OSInode { + pub fn read_all(&self) -> Vec { + let mut inner = self.inner.lock(); + let mut buffer = [0u8; 512]; + let mut v: Vec = Vec::new(); + loop { + let len = inner.inode.read_at(inner.offset, &mut buffer); + if len == 0 { + break; + } + inner.offset += len; + v.extend_from_slice(&buffer[..len]); + } + v + } + } + +之后,就可以从向量 ``all_data`` 中拿到应用的 ELF 数据,当解析完毕并创建完应用地址空间后该向量将会被回收。 + +同样的,我们在内核中创建初始进程 ``initproc`` 也需要替换为基于文件系统的实现: + +.. code-block:: rust + + // os/src/task/mod.rs + + lazy_static! { + pub static ref INITPROC: Arc = Arc::new({ + let inode = open_file("initproc", OpenFlags::RDONLY).unwrap(); + let v = inode.read_all(); + TaskControlBlock::new(v.as_slice()) + }); + } \ No newline at end of file diff --git a/source/chapter7/4cmdargs-and-redirection.rst b/source/chapter7/4cmdargs-and-redirection.rst new file mode 100644 index 0000000000000000000000000000000000000000..2d1754ca662c7034dd9317978c82dcf47e618332 --- /dev/null +++ b/source/chapter7/4cmdargs-and-redirection.rst @@ -0,0 +1,481 @@ +命令行参数与标准 I/O 重定向 +================================================= + +本节导读 +------------------------------------------------- + +之前我们已经支持从文件系统中加载应用,还实现了文件的创建和读写。但是目前我们在应用中只能硬编码要操作的文件,这就使得应用的功能大大受限,用户终端对于文件的交互访问能力也很弱。为了解决这些问题,我们需要在用户终端和内核中支持命令行参数的解析和传递还有标准 I/O 重定向功能。之后,我们便可以在用户终端执行应用的时候通过调整命令手动将应用的输入和输出从标准输入输出替换为某个特定文件,还实现了命令行工具 ``cat`` 来查看一个文件的内容。 + +命令行参数 +------------------------------------------------- + +在使用 C/C++ 语言开发 Linux 应用的时候,我们可以使用标准库提供的 ``argc/argv`` 来获取命令行参数,它们是直接被作为参数传给 ``main`` 函数的。下面来看一个打印命令行参数的例子: + +.. code-block:: c + + // a.c + + #include + + int main(int argc, char* argv[]) { + printf("argc = %d\n", argc); + for (int i = 0; i < argc; i++) { + printf("argv[%d] = %s\n", i, argv[i]); + } + return 0; + } + +其中 ``argc`` 表示命令行参数的个数,而 ``argv`` 是一个长度为 ``argc`` 的字符串数组,数组中的每个字符串都是一个命令行参数。我们可以在 Linux 系统上运行这个程序: + +.. code-block:: console + + $ gcc a.c -oa -g -Wall + $ ./a aa bb 11 22 cc + argc = 6 + argv[0] = ./a + argv[1] = aa + argv[2] = bb + argv[3] = 11 + argv[4] = 22 + argv[5] = cc + +为了支持后续的一些功能,我们希望在我们自己的内核和用户终端上支持这个功能。为了对实现正确性进行测试,在本章中我们编写了一个名为 ``cmdline_args`` 的应用,它是用 Rust 编写的,并只能在我们的内核上执行,但是它的功能是和 ``a.c`` 保持一致的。我们可以在我们的内核上运行该应用来看看效果: + +.. code-block:: + + Rust user shell + >> cmdline_args aa bb 11 22 cc + argc = 6 + argv[0] = cmdline_args + argv[1] = aa + argv[2] = bb + argv[3] = 11 + argv[4] = 22 + argv[5] = cc + Shell: Process 2 exited with code 0 + >> + +可以看到二者的输出是基本相同的。 + +但是,要实现这个看似简单的功能,需要内核和用户态的共同努力。为了支持命令行参数, ``sys_exec`` 的系统调用接口需要发生变化: + +.. code-block:: rust + + // user/src/syscall.rs + + pub fn sys_exec(path: &str, args: &[*const u8]) -> isize; + +可以看到,它的参数多出了一个 ``args`` 数组,数组中的每个元素都是一个命令行参数字符串的起始地址。由于我们是以引用的形式传递这个数组,实际传递给内核的实际上是这个数组的起始地址: + +.. code-block:: rust + + // user/src/syscall.rs + + pub fn sys_exec(path: &str, args: &[*const u8]) -> isize { + syscall(SYSCALL_EXEC, [path.as_ptr() as usize, args.as_ptr() as usize, 0]) + } + + // user/src/lib.rs + + pub fn exec(path: &str, args: &[*const u8]) -> isize { sys_exec(path, args) } + +接下来我们分析一下,一行带有命令行参数的命令从输入到它的命令行参数被打印出来中间经历了哪些过程。 + +用户终端的命令行参数分割 ++++++++++++++++++++++++++++++++++++++++++++++++++ + +回忆一下,之前在用户终端 ``user_shell`` 中,一旦接收到一个回车,我们就会将当前行的内容 ``line`` 作为一个名字并试图去执行同名的应用。但是现在 ``line`` 还可能包含一些命令行参数,只有最开头的一个才是要执行的应用名。因此我们要做的第一件事情就是将 ``line`` 用空格进行分割: + +.. code-block:: rust + + // user/src/bin/user_shell.rs + + let args: Vec<_> = line.as_str().split(' ').collect(); + let mut args_copy: Vec = args + .iter() + .map(|&arg| { + let mut string = String::new(); + string.push_str(arg); + string + }) + .collect(); + + args_copy + .iter_mut() + .for_each(|string| { + string.push('\0'); + }); + +经过分割, ``args`` 中的 ``&str`` 都是 ``line`` 中的一段子区间,它们的结尾并没有包含 ``\0`` ,因为 ``line`` 是我们输入得到的,中间本来就没有 ``\0`` 。由于在向内核传入字符串的时候,我们必须保证其结尾为 ``\0`` ,因此我们需要用 ``args_copy`` 将 ``args`` 中的字符串拷贝一份到堆上并在末尾手动加入 ``\0`` 。这样就可以安心的 ``args_copy`` 中的字符串传入内核了。但是在传入内核的时候,我们只能传入这些字符串的起始地址: + +.. code-block:: rust + + // user/src/bin/user_shell.rs + + let mut args_addr: Vec<*const u8> = args_copy + .iter() + .map(|arg| arg.as_ptr()) + .collect(); + args_addr.push(0 as *const u8); + +向量 ``args_addr`` 中的每个元素都代表一个命令行参数字符串的起始地址。由于我们要传递给内核的是这个向量的起始地址,为了让内核能够获取到命令行参数的个数,我们需要在 ``args_addr`` 的末尾放入一个 0 ,这样内核看到它的时候就能知道命令行参数已经获取完毕了。 + +在 ``fork`` 出来的子进程里面我们需要这样执行应用: + +.. code-block:: rust + + // user/src/bin/user_shell.rs + + // child process + if exec(args_copy[0].as_str(), args_addr.as_slice()) == -1 { + println!("Error when executing!"); + return -4; + } + +sys_exec 将命令行参数压入用户栈 ++++++++++++++++++++++++++++++++++++++++++++++++++ + +在 ``sys_exec`` 中,首先需要将应用传进来的命令行参数取出来: + +.. code-block:: rust + :linenos: + :emphasize-lines: 6-14,19 + + // os/src/syscall/process.rs + + pub fn sys_exec(path: *const u8, mut args: *const usize) -> isize { + let token = current_user_token(); + let path = translated_str(token, path); + let mut args_vec: Vec = Vec::new(); + loop { + let arg_str_ptr = *translated_ref(token, args); + if arg_str_ptr == 0 { + break; + } + args_vec.push(translated_str(token, arg_str_ptr as *const u8)); + unsafe { args = args.add(1); } + } + if let Some(app_inode) = open_file(path.as_str(), OpenFlags::RDONLY) { + let all_data = app_inode.read_all(); + let task = current_task().unwrap(); + let argc = args_vec.len(); + task.exec(all_data.as_slice(), args_vec); + // return argc because cx.x[10] will be covered with it later + argc as isize + } else { + -1 + } + } + +这里的 ``args`` 指向命令行参数字符串起始地址数组中的一个位置,每次我们都可以从一个起始地址通过 ``translated_str`` 拿到一个字符串,直到 ``args`` 为 0 就说明没有更多命令行参数了。在第 19 行调用 ``TaskControlBlock::exec`` 的时候,我们需要将获取到的 ``args_vec`` 传入进去并将里面的字符串压入到用户栈上。 + +.. code-block:: rust + :linenos: + :emphasize-lines: 11-34,45,50,51 + + // os/src/task/task.rs + + impl TaskControlBlock { + pub fn exec(&self, elf_data: &[u8], args: Vec) { + // memory_set with elf program headers/trampoline/trap context/user stack + let (memory_set, mut user_sp, entry_point) = MemorySet::from_elf(elf_data); + let trap_cx_ppn = memory_set + .translate(VirtAddr::from(TRAP_CONTEXT).into()) + .unwrap() + .ppn(); + // push arguments on user stack + user_sp -= (args.len() + 1) * core::mem::size_of::(); + let argv_base = user_sp; + let mut argv: Vec<_> = (0..=args.len()) + .map(|arg| { + translated_refmut( + memory_set.token(), + (argv_base + arg * core::mem::size_of::()) as *mut usize + ) + }) + .collect(); + *argv[args.len()] = 0; + for i in 0..args.len() { + user_sp -= args[i].len() + 1; + *argv[i] = user_sp; + let mut p = user_sp; + for c in args[i].as_bytes() { + *translated_refmut(memory_set.token(), p as *mut u8) = *c; + p += 1; + } + *translated_refmut(memory_set.token(), p as *mut u8) = 0; + } + // make the user_sp aligned to 8B for k210 platform + user_sp -= user_sp % core::mem::size_of::(); + + // **** hold current PCB lock + let mut inner = self.acquire_inner_lock(); + // substitute memory_set + inner.memory_set = memory_set; + // update trap_cx ppn + inner.trap_cx_ppn = trap_cx_ppn; + // initialize trap_cx + let mut trap_cx = TrapContext::app_init_context( + entry_point, + user_sp, + KERNEL_SPACE.lock().token(), + self.kernel_stack.get_top(), + trap_handler as usize, + ); + trap_cx.x[10] = args.len(); + trap_cx.x[11] = argv_base; + *inner.get_trap_cx() = trap_cx; + // **** release current PCB lock + } + } + +第 11-34 行所做的主要工作是将命令行参数以某种格式压入用户栈。具体的格式可以参考下图(比如应用传入了两个命令行参数 ``aa`` 和 ``bb`` ): + +.. image:: user-stack-cmdargs.png + :align: center + +- 首先需要在用户栈上分配一个字符串指针数组,也就是蓝色区域。数组中的每个元素都指向一个用户栈更低处的命令行参数字符串的起始地址。在第 12~24 行可以看到,最开始我们只是分配空间,具体的值要等到字符串被放到用户栈上之后才能确定更新。 +- 第 23~32 行,我们逐个将传入的 ``args`` 中的字符串压入到用户栈中,对应于图中的橙色区域。为了实现方便,我们在用户栈上预留空间之后逐字节进行复制。注意 ``args`` 中的字符串是通过 ``translated_str`` 从应用地址空间取出的,它的末尾不包含 ``\0`` 。为了应用能知道每个字符串的长度,我们需要手动在末尾加入 ``\0`` 。 +- 第 34 行将 ``user_sp`` 以 8 字节对齐,即图中的绿色区域。这是因为命令行参数的长度不一,很有可能压入之后 ``user_sp`` 没有对齐到 8 字节,那么在 K210 平台上在访问用户栈的时候就会触发访存不对齐的异常。在 Qemu 平台上则并不存在这个问题。 + +我们还需要对应修改 Trap 上下文。首先是第 45 行,我们的 ``user_sp`` 相比之前已经发生了变化,它上面已经压入了命令行参数。同时,我们还需要修改 Trap 上下文中的 ``a0/a1`` 寄存器,让 ``a0`` 表示命令行参数的个数,而 ``a1`` 则表示图中 ``argv_base`` 即蓝色区域的起始地址。这两个参数在第一次进入对应应用的用户态的时候会被接收并用于还原命令行参数。 + +用户库从用户栈上还原命令行参数 ++++++++++++++++++++++++++++++++++++++++++++++++++ + +在第一次进入用户态的时候,我们放在 Trap 上下文 a0/a1 两个寄存器中的内容可以被用户库中的入口函数以参数的形式接收: + +.. code-block:: rust + :linenos: + :emphasize-lines: 10-24 + + // user/src/lib.rs + + #[no_mangle] + #[link_section = ".text.entry"] + pub extern "C" fn _start(argc: usize, argv: usize) -> ! { + unsafe { + HEAP.lock() + .init(HEAP_SPACE.as_ptr() as usize, USER_HEAP_SIZE); + } + let mut v: Vec<&'static str> = Vec::new(); + for i in 0..argc { + let str_start = unsafe { + ((argv + i * core::mem::size_of::()) as *const usize).read_volatile() + }; + let len = (0usize..).find(|i| unsafe { + ((str_start + *i) as *const u8).read_volatile() == 0 + }).unwrap(); + v.push( + core::str::from_utf8(unsafe { + core::slice::from_raw_parts(str_start as *const u8, len) + }).unwrap() + ); + } + exit(main(argc, v.as_slice())); + } + +可以看到,在入口 ``_start`` 中我们就接收到了命令行参数个数 ``argc`` 和字符串数组的起始地址 ``argv`` 。但是这个起始地址不太好用,我们希望能够将其转化为编写应用的时候看到的 ``&[&str]`` 的形式。转化的主体在第 10~23 行,就是分别取出 ``argc`` 个字符串的起始地址(基于字符串数组的 base 地址 ``argv`` ),从它向后找到第一个 ``\0`` 就可以得到一个完整的 ``&str`` 格式的命令行参数字符串并加入到向量 ``v`` 中。最后通过 ``v.as_slice`` 就得到了我们在 ``main`` 主函数中看到的 ``&[&str]`` 。 + +通过命令行工具 cat 输出文件内容 ++++++++++++++++++++++++++++++++++++++++++++++++++ + +有了之前的命令行参数支持,我们就可以编写命令行工具 ``cat`` 来输出指定文件的内容了。它的使用方法如下: + +.. code-block:: + + >> filetest_simple + file_test passed! + Shell: Process 2 exited with code 0 + >> cat filea + Hello, world! + Shell: Process 2 exited with code 0 + >> + +``filetest_simple`` 会将 ``Hello, world!`` 输出到文件 ``filea`` 中。之后我们就可以通过 ``cat filea`` 来打印文件 ``filea`` 中的内容。 + +``cat`` 本身也是一个应用,且很容易实现: + +.. code-block:: rust + + // user/src/bin/cat.rs + + #![no_std] + #![no_main] + + #[macro_use] + extern crate user_lib; + extern crate alloc; + + use user_lib::{ + open, + OpenFlags, + close, + read, + }; + use alloc::string::String; + + #[no_mangle] + pub fn main(argc: usize, argv: &[&str]) -> i32 { + assert!(argc == 2); + let fd = open(argv[1], OpenFlags::RDONLY); + if fd == -1 { + panic!("Error occured when opening file"); + } + let fd = fd as usize; + let mut buf = [0u8; 16]; + let mut s = String::new(); + loop { + let size = read(fd, &mut buf) as usize; + if size == 0 { break; } + s.push_str(core::str::from_utf8(&buf[..size]).unwrap()); + } + println!("{}", s); + close(fd); + 0 + } + + +标准输入输出重定向 +------------------------------------------------- + +为了进一步增强用户终端使用文件系统时的灵活性,我们需要新增标准输入输出重定向功能。这个功能在我们使用 Linux 内核的时候很常用,我们在自己的内核中举个例子: + +.. code-block:: + + >> yield > fileb + Shell: Process 2 exited with code 0 + >> cat fileb + Hello, I am process 2. + Back in process 2, iteration 0. + Back in process 2, iteration 1. + Back in process 2, iteration 2. + Back in process 2, iteration 3. + Back in process 2, iteration 4. + yield pass. + + Shell: Process 2 exited with code 0 + >> + +通过 ``>`` 我们可以将应用 ``yield`` 的输出重定向到文件 ``fileb`` 中。我们也可以注意到在屏幕上暂时看不到 ``yield`` 的输出了。在应用 ``yield`` 退出之后,我们可以使用 ``cat`` 工具来查看文件 ``fileb`` 的内容,可以看到里面的确是 ``yield`` 的输出。同理,通过 ``<`` 则可以将一个应用的输入重定向到某个指定文件而不是从键盘输入。 + +注意重定向功能对于应用来说是透明的。在应用中除非明确指出了某些内容要从指定的文件输入或者输出到指定的文件,剩下的内容默认都是输入自进程文件描述表位置 0 (即 fd=0)处的标准输入,并输出到进程文件描述符表位置 1 (即 fd=1)处的标准输出。因此,在应用执行之前,我们就要对应用进程的文件描述符表进行某种替换。以输出为例,我们需要提前打开文件并用这个文件来替换掉应用文件描述符表位置 1 处的标准输出,这就完成了所谓的重定向。在重定向之后,应用认为自己输出到 fd=1 的标准输出,但实际上是输出到我们指定的文件中。我们能够做到这一点还是得益于文件的抽象,因为在进程看来无论是标准输出还是标准文件都是一种文件,可以通过同样的接口来读写。 + +为了实现重定向功能,我们需要引入一个新的系统调用 ``sys_dup`` : + +.. code-block:: rust + + // user/src/syscall.rs + + /// 功能:将进程中一个已经打开的文件复制一份并分配到一个新的文件描述符中。 + /// 参数:fd 表示进程中一个已经打开的文件的文件描述符。 + /// 返回值:如果出现了错误则返回 -1,否则能够访问已打开文件的新文件描述符。 + /// 可能的错误原因是:传入的 fd 并不对应一个合法的已打开文件。 + /// syscall ID:24 + pub fn sys_dup(fd: usize) -> isize; + +这个系统调用的实现非常简单: + +.. code-block:: rust + + // os/src/syscall/fs.rs + + pub fn sys_dup(fd: usize) -> isize { + let task = current_task().unwrap(); + let mut inner = task.acquire_inner_lock(); + if fd >= inner.fd_table.len() { + return -1; + } + if inner.fd_table[fd].is_none() { + return -1; + } + let new_fd = inner.alloc_fd(); + inner.fd_table[new_fd] = Some(Arc::clone(inner.fd_table[fd].as_ref().unwrap())); + new_fd as isize + } + +首先检查传入 ``fd`` 的合法性。然后在文件描述符表中分配一个新的文件描述符,并保存 ``fd`` 指向的已打开文件的一份拷贝即可。 + +那么我们应该在什么时候进行替换,又应该如何利用 ``sys_dup`` 进行替换呢? + +答案是在用户终端 ``user_shell`` 中进行处理。在分割命令行参数的时候,我们要检查是否存在通过 ```` 进行输入输出重定向的情况,如果存在的话则需要将它们从命令行参数中移除,并记录匹配到的输入/输出文件名到字符串 ``input/output`` 中。注意,为了实现方便,我们这里假设输入用户终端的命令一定合法:即 ```` 最多只会出现一次,且后面总是会有一个参数作为重定向到的文件。 + +.. code-block:: rust + + // user/src/bin/user_shell.rs + + // redirect input + let mut input = String::new(); + if let Some((idx, _)) = args_copy + .iter() + .enumerate() + .find(|(_, arg)| arg.as_str() == "<\0") { + input = args_copy[idx + 1].clone(); + args_copy.drain(idx..=idx + 1); + } + + // redirect output + let mut output = String::new(); + if let Some((idx, _)) = args_copy + .iter() + .enumerate() + .find(|(_, arg)| arg.as_str() == ">\0") { + output = args_copy[idx + 1].clone(); + args_copy.drain(idx..=idx + 1); + } + +打开文件和替换的过程则发生在 ``fork`` 之后的子进程分支中: + +.. code-block:: rust + :linenos: + + // user/src/bin/user_shell.rs + + let pid = fork(); + if pid == 0 { + // input redirection + if !input.is_empty() { + let input_fd = open(input.as_str(), OpenFlags::RDONLY); + if input_fd == -1 { + println!("Error when opening file {}", input); + return -4; + } + let input_fd = input_fd as usize; + close(0); + assert_eq!(dup(input_fd), 0); + close(input_fd); + } + // output redirection + if !output.is_empty() { + let output_fd = open( + output.as_str(), + OpenFlags::CREATE | OpenFlags::WRONLY + ); + if output_fd == -1 { + println!("Error when opening file {}", output); + return -4; + } + let output_fd = output_fd as usize; + close(1); + assert_eq!(dup(output_fd), 1); + close(output_fd); + } + // child process + if exec(args_copy[0].as_str(), args_addr.as_slice()) == -1 { + println!("Error when executing!"); + return -4; + } + unreachable!(); + } else { + let mut exit_code: i32 = 0; + let exit_pid = waitpid(pid as usize, &mut exit_code); + assert_eq!(pid, exit_pid); + println!("Shell: Process {} exited with code {}", pid, exit_code); + } + +- 输入重定向发生在第 6~16 行。我们尝试打开输入文件 ``input`` 到 ``input_fd`` 中。之后,首先通过 ``close`` 关闭标准输入所在的文件描述符 0 。之后通过 ``dup`` 来分配一个新的文件描述符来访问 ``input_fd`` 对应的输入文件。这里用到了文件描述符分配的重要性质:即必定分配可用描述符中编号最小的一个。由于我们刚刚关闭了描述符 0 ,那么在 ``dup`` 的时候一定会将它分配出去,于是现在应用进程的文件描述符 0 就对应到输入文件了。最后,因为不会用到输入文件原来的描述符 ``input_fd`` ,我们将其关掉。 +- 输出重定向则发生在 18~31 行。它的原理和输入重定向几乎完全一致,只是通过 ``open`` 打开文件的标志不太相同。 + +之后,就可以通过 ``exec`` 来执行应用了。 + +虽然 ``fork/exec/waitpid`` 三个经典的系统调用自它们于古老的 Unix 时代诞生以来已经过去了太长时间,从某种程度上来讲已经不太适合新的内核环境了。人们也已经提出了若干种替代品并已经在进行实践,比如 ``spawn`` 或者 Linux 上的 ``clone`` 系统调用。但是它们迄今为止仍然存在就证明在它们的设计中还能够找到可取之处。从本节介绍的重定向就可以看出它们的灵活性以及强大的功能性:我们能够进行重定向恰恰是因为执行应用分为 ``fork`` 和 ``exec`` 两个调用,那么在这两个调用之间我们就能够进行一些类似重定向的处理。在实现的过程中,我们还用到了 ``fork`` 出来的子进程会和父进程共享文件描述符表的性质。 \ No newline at end of file diff --git a/source/chapter7/5exercise.rst b/source/chapter7/5exercise.rst new file mode 100644 index 0000000000000000000000000000000000000000..cc3cc064ff48e1bd9925cf953198ccbc0fb008e4 --- /dev/null +++ b/source/chapter7/5exercise.rst @@ -0,0 +1,121 @@ +lab7 实验要求 +================================================ + +- 本节难度: **理解文件系统比较费事,编程难度适中** + +编程作业 +------------------------------------------------- + +硬链接 +++++++++++++++++++++++++++++++++++++++++++++++++++ + +你的电脑桌面是咋样的?是放满了图标吗?反正我的 windows 是这样的。显然很少人会真的把可执行文件放到桌面上,桌面图标其实都是一些快捷方式。或者用 unix 的术语来说:软链接。为了减少工作量,我们今天来实现软链接的兄弟:[硬链接](https://en.wikipedia.org/wiki/Hard_link)。 + +硬链接要求两个不同的目录项指向同一个文件,在我们的文件系统中也就是两个不同名称目录项指向同一个磁盘块。本节要求实现三个系统调用 sys_linkat、sys_unlinkat、sys_stat。 + +**linkat**: + + * syscall ID: 37 + * 功能:创建一个文件的一个硬链接, `linkat标准接口 `_ 。 + * C接口: ``int linkat(int olddirfd, char* oldpath, int newdirfd, char* newpath, unsigned int flags)`` + * Rust 接口: ``fn linkat(olddirfd: i32, oldpath: *const u8, newdirfd: i32, newpath: *const u8, flags: u32) -> i32`` + * 参数: + * olddirfd,newdirfd: 仅为了兼容性考虑,本次实验中始终为 AT_FDCWD (-100),可以忽略。 + * flags: 仅为了兼容性考虑,本次实验中始终为 0,可以忽略。 + * oldpath:原有文件路径 + * newpath: 新的链接文件路径。 + * 说明: + * 为了方便,不考虑新文件路径已经存在的情况(属于未定义行为),除非链接同名文件。 + * 返回值:如果出现了错误则返回 -1,否则返回 0。 + * 可能的错误 + * 链接同名文件。 + +**unlinkat**: + + * syscall ID: 35 + * 功能:取消一个文件路径到文件的链接, `unlinkat标准接口 `_ 。 + * C接口: ``int unlinkat(int dirfd, char* path, unsigned int flags)`` + * Rust 接口: ``fn unlinkat(dirfd: i32, path: *const u8, flags: u32) -> i32`` + * 参数: + * dirfd: 仅为了兼容性考虑,本次实验中始终为 AT_FDCWD (-100),可以忽略。 + * flags: 仅为了兼容性考虑,本次实验中始终为 0,可以忽略。 + * path:文件路径。 + * 说明: + * 为了方便,不考虑使用 unlink 彻底删除文件的情况。 + * 返回值:如果出现了错误则返回 -1,否则返回 0。 + * 可能的错误 + * 文件不存在。 + +**fstat**: + + * syscall ID: 80 + * 功能:获取文件状态。 + * C接口: ``int fstat(int fd, struct Stat* st)`` + * Rust 接口: ``fn fstat(fd: i32, st: *mut Stat) -> i32`` + * 参数: + * fd: 文件描述符 + * st: 文件状态结构体 + + .. code-block:: rust + + #[repr(C)] + #[derive(Debug)] + pub struct Stat { + /// 文件所在磁盘驱动器号 + pub dev: u64, + /// inode 文件所在 inode 编号 + pub ino: u64, + /// 文件类型 + pub mode: StatMode, + /// 硬链接数量,初始为1 + pub nlink: u32, + /// 无需考虑,为了兼容性设计 + pad: [u64; 7], + } + + /// StatMode 定义: + bitflags! { + pub struct StatMode: u32 { + const NULL = 0; + /// directory + const DIR = 0o040000; + /// ordinary regular file + const FILE = 0o100000; + } + } + + +实验要求 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +- 实现分支:ch7。 +- 完成实验指导书中的内容,实现基本的文件操作。 +- 实现硬链接及相关系统调用,并通过 `Rust测例 `_ 中 chapter7 对应的所有测例。 + +challenge: 支持多核。 + +实验检查 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +- 实验目录要求 + + 目录要求不变(参考lab1目录或者示例代码目录结构)。同样在 os 目录下 `make run` 之后可以正确加载用户程序并执行。 + + 加载的用户测例位置: `../user/build/bin`。 + +- 检查 + + 可以正确 `make run` 执行,可以正确执行目标用户测例,并得到预期输出(详见测例注释)。 + +问答作业 +---------------------------------------------------------- + +1. 目前的文件系统只有单级目录,假设想要支持多级文件目录,请描述你设想的实现方式,描述合理即可。 + +2. 在有了多级目录之后,我们就也可以为一个目录增加硬链接了。在这种情况下,文件树中是否可能出现环路?你认为应该如何解决?请在你喜欢的系统上实现一个环路,描述你的实现方式以及系统提示、实际测试结果。 + +报告要求 +----------------------------------------------------------- +* 简单总结本次实验与上个实验相比你增加的东西。(控制在5行以内,不要贴代码) +* 完成问答问题 +* (optional) 你对本次实验设计及难度的看法。 \ No newline at end of file diff --git a/source/chapter7/ch7.pptx b/source/chapter7/ch7.pptx new file mode 100644 index 0000000000000000000000000000000000000000..103bab33ff47786a6d2f947938df19f596385c09 Binary files /dev/null and b/source/chapter7/ch7.pptx differ diff --git a/source/chapter7/index.rst b/source/chapter7/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..459a267e228a9fb430ae820437c17c9d5dbe58a2 --- /dev/null +++ b/source/chapter7/index.rst @@ -0,0 +1,14 @@ +第七章:数据持久化存储 +============================================== + +.. toctree:: + :maxdepth: 4 + + 0intro + 1fs-interface + 2fs-implementation + 3using-easy-fs-in-kernel + 4cmdargs-and-redirection + 5exercise + +最晚灭绝的“霸王龙”操作系统 \ No newline at end of file diff --git a/source/chapter7/user-stack-cmdargs.png b/source/chapter7/user-stack-cmdargs.png new file mode 100644 index 0000000000000000000000000000000000000000..28b28812dc45f2cc54cd92b312b3e7ddf1b2df75 Binary files /dev/null and b/source/chapter7/user-stack-cmdargs.png differ diff --git a/source/chapter8/index.rst b/source/chapter8/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..93cc9917b9009a87d98be6895eeb82e98d0a68f8 --- /dev/null +++ b/source/chapter8/index.rst @@ -0,0 +1,6 @@ +第八章:阻塞(暂定) +============================================== + +.. toctree:: + :hidden: + :maxdepth: 4 \ No newline at end of file diff --git a/source/conf.py b/source/conf.py index 830fce737d7aa4e1ab9720bdb7a627b6ef17e9e5..b2d1df5a9477a2bf1c618657d2d7536f8fc1d010 100644 --- a/source/conf.py +++ b/source/conf.py @@ -67,3 +67,53 @@ html_theme = 'sphinx_rtd_theme' # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] +html_css_files = [ + 'my_style.css', + #'dracula.css', +] + +from pygments.lexer import RegexLexer +from pygments import token +from sphinx.highlighting import lexers + +class RVLexer(RegexLexer): + name = 'riscv' + tokens = { + 'root': [ + # Comment + (r'#.*\n', token.Comment), + # General Registers + (r'\b(?:x[1-2]?[0-9]|x30|x31|zero|ra|sp|gp|tp|fp|t[0-6]|s[0-9]|s1[0-1]|a[0-7]|pc)\b', token.Name.Attribute), + # CSRs + (r'\bs(?:status|tvec|ip|ie|counteren|scratch|epc|cause|tval|atp|)\b', token.Name.Constant), + (r'\bm(?:isa|vendorid|archid|hardid|status|tvec|ideleg|ip|ie|counteren|scratch|epc|cause|tval)\b', token.Name.Constant), + # Instructions + (r'\b(?:(addi?w?)|(slti?u?)|(?:and|or|xor)i?|(?:sll|srl|sra)i?w?|lui|auipc|subw?|jal|jalr|beq|bne|bltu?|bgeu?|s[bhwd]|(l[bhw]u?)|ld)\b', token.Name.Decorator), + (r'\b(?:csrr?[rws]i?)\b', token.Name.Decorator), + (r'\b(?:ecall|ebreak|[msu]ret|wfi|sfence.vma)\b', token.Name.Decorator), + (r'\b(?:nop|li|la|mv|not|neg|negw|sext.w|seqz|snez|sltz|sgtz|f(?:mv|abs|neg).(?:s|d)|b(?:eq|ne|le|ge|lt)z|bgt|ble|bgtu|bleu|j|jr|ret|call)\b', token.Name.Decorator), + (r'(?:%hi|%lo|%pcrel_hi|%pcrel_lo|%tprel_(?:hi|lo|add))', token.Name.Decorator), + # Directives + (r'(?:.2byte|.4byte|.8byte|.quad|.half|.word|.dword|.byte|.dtpreldword|.dtprelword|.sleb128|.uleb128|.asciz|.string|.incbin|.zero)', token.Name.Function), + (r'(?:.align|.balign|.p2align)', token.Name.Function), + (r'(?:.globl|.local|.equ)', token.Name.Function), + (r'(?:.text|.data|.rodata|.bss|.comm|.common|.section)', token.Name.Function), + (r'(?:.option|.macro|.endm|.file|.ident|.size|.type)', token.Name.Function), + (r'(?:.set|.rept|.endr|.macro|.endm|.altmacro)', token.Name.Function), + # Number + (r'\b(?:(?:0x|)[\da-f]+|(?:0o|)[0-7]+|\d+)\b', token.Number), + # Labels + (r'\S+:', token.Name.Builtin), + # Whitespace + (r'\s', token.Whitespace), + # Other operators + (r'[,\+\*\-\(\)\\%]', token.Text), + # Hacks + (r'(?:SAVE_GP|trap_handler|__switch|LOAD_GP|SAVE_SN|LOAD_SN|__alltraps|__restore)', token.Name.Builtin), + (r'(?:.trampoline)', token.Name.Function), + (r'(?:n)', token.Name.Entity), + (r'(?:x)', token.Text), + ], + } + +lexers['riscv'] = RVLexer() diff --git a/source/index.rst b/source/index.rst index 3c225b305a50d4cda525c91195abbfd648c25076..fac081f807120d63b70c46bb123e407c52c97490 100644 --- a/source/index.rst +++ b/source/index.rst @@ -8,13 +8,24 @@ rCore-Tutorial-Book 第三版 .. toctree:: :maxdepth: 2 - :caption: 正文 + :caption: Part1 - Just do it! :hidden: - quickstart chapter0/index chapter1/index chapter2/index + chapter3/index + chapter4/index + chapter5/index + chapter6/index + chapter7/index + +.. toctree:: + :maxdepth: 2 + :caption: Part2 - Do it better! + :hidden: + + chapter8/index .. toctree:: :maxdepth: 2 @@ -24,25 +35,63 @@ rCore-Tutorial-Book 第三版 appendix-a/index appendix-b/index appendix-c/index + appendix-d/index + terminology .. toctree:: :maxdepth: 2 :caption: 开发注记 :hidden: + setup-sphinx rest-example - collaboration - + log 欢迎来到 rCore-Tutorial-Book 第三版! -读者须知 +.. note:: + + :doc:`/log` + + 项目/文档于 2021-03-15 最后一次更新,情况如下: + + - 增加了在做实验的时候打补丁继承上一章节修改的 :ref:`教程 ` 。 + + +项目简介 +--------------------- + +这本教程旨在一步一步展示如何 **从零开始** 用 **Rust** 语言写一个基于 **RISC-V** 架构的 **类 Unix 内核** 。值得注意的是,本项目不仅支持模拟器环境(如 Qemu/terminus 等),还支持在真实硬件平台 Kendryte K210 上运行。 + + +导读 --------------------- -请先按照 :doc:`/quickstart` 中的说明完成环境配置,再从第一章开始阅读正文。 +请大家先阅读 :ref:`第零章 ` ,对于项目的开发背景和操作系统的概念有一个整体把控。 + +在正式进行实验之前,请先按照第零章章末的 :doc:`/chapter0/5setup-devel-env` 中的说明完成环境配置,再从第一章开始阅读正文。 + +如果已经对 RISC-V 架构、Rust 语言和内核的知识有较多了解,第零章章末的 :doc:`/chapter0/6hardware` 提供了我们采用的真实硬件平台 Kendryte K210 的一些信息。 项目协作 ---------------------- -请参考 :doc:`/collaboration` 了解如何进行项目协作。 +- :doc:`/setup-sphinx` 介绍了如何基于 Sphinx 框架配置文档开发环境,之后可以本地构建并渲染 html 或其他格式的文档; +- :doc:`/rest-example` 给出了目前编写文档才用的 ReStructuredText 标记语言的一些基础语法及用例; +- `项目的源代码仓库 `_ && `文档仓库 `_ +- 时间仓促,本项目还有很多不完善之处,欢迎大家积极在每一个章节的评论区留言,或者提交 Issues 或 Pull Requests,让我们 + 一起努力让这本书变得更好! + +项目进度 +----------------------- +- 2020-11-03:环境搭建完成,开始着手编写文档。 +- 2020-11-13:第一章完成。 +- 2020-11-27:第二章完成。 +- 2020-12-20:前七章代码完成。 +- 2021-01-10:第三章完成。 +- 2021-01-18:加入第零章。 +- 2021-01-30:第四章完成。 +- 2021-02-16:第五章完成。 +- 2021-02-20:第六章完成。 +- 2021-03-06:第七章完成。到这里为止第一版初稿就已经完成了。 \ No newline at end of file diff --git a/source/log.rst b/source/log.rst new file mode 100644 index 0000000000000000000000000000000000000000..f5198df3cc1f47228d41e49d8f5f036ffa8e9ad3 --- /dev/null +++ b/source/log.rst @@ -0,0 +1,98 @@ +更新日志 +=============================== + +2021-03-15 +------------------------------- + +- 增加了在做实验的时候打补丁继承上一章节修改的 :ref:`教程 ` 。 + +2021-03-09 +------------------------------- + +- 将所有分支的 RustSBI 版本更新为 [81d53d8] 的 0.2.0-alpha.1 ,主要是在 Qemu 平台上支持非法指令的转发,目前可以正确处理带有非法指令的应用程序了。参考 ch2 分支上的测例 ``00hello_world.rs`` 。 + + +2021-03-07 +------------------------------- + +- 在各章分支的链接脚本中加入了 ``.srodata/.sbss/.sdata`` 。 + +2021-03-06 +------------------------------- + +- 文档第一版初稿(全七章)完成! +- 修复了框架中基于 Qemu 平台运行却仍需要下载 kflash.py 工具的问题。 + +2021-03-05 +------------------------------- + +- 第三章练习中增加了对于 ``sys_gettime`` 语义在教程和测例中差异的相关说明, :ref:`详情 ` 。 +- 修正了第四章练习中 mmap 系统调用语义中的一处错误。 + + +2021-03-03 +------------------------------- + +- 更新了第四章练习题。 +- 为方便调试,提供了 riscv64 gcc 工具链的下载链接。 +- 将文档渲染改为宽屏模式。 + +2021-02-28 +------------------------------- + +修复了 ch3-coop 分支在 Rust 版本更新后无法成功运行的问题。 + +2021-02-27 +------------------------------- + +完善了 ``easy-fs`` : + +- 订正了 ``easy-fs`` 块缓存层的实现,移除了 ``dirty`` 子模块。 +- 支持二级间接块索引,使得支持的单个文件最大容量从 :math:`94\text{KiB}` 变为超过 :math:`8\text{MiB}` 。调整了单个 ``DiskInode`` 大小为 128 字节。 +- 在新建一个索引节点的时候不再直接分配一二级间接索引块,而是完全按需分配。 +- 将 ``easy-fs`` 的测试和应用程序打包的函数分离到另一个名为 ``easy-fs-fuse`` 的 crate 中。 + +从 ch7 开始: + +- 出于后续的一些需求, ``sys_exec`` 需要支持命令行参数,为此用户终端 ``user_shell`` 中需要相应增加一些解析功能,内核中 ``sys_exec`` 的实现也需要进行修改。新增了应用 ``cmdline_args`` 来打印传入的命令行参数。 +- 新增了应用 cat 工具可以读取一个文件的全部内容。 +- 在用户终端中支持通过 ``<`` 和 ``>`` 进行简单的输入/输出重定向,为此在内核中新增了一个 ``sys_dup`` 系统调用。 + +另外,在所有章节分支新增了 docker 支持来尽可能降低环境配置的时间成本,详见 :ref:`使用 Docker 环境 ` 。 + +2021-02-20 +------------------------------- + +第六章文档完成。 + +2021-02-16 +------------------------------- + +第五章文档完成。 + +2021-02-13 +------------------------------- + +将 ch2-ch6 的 build.rs 中的对齐需求修改为刚好合适。 + +2021-02-09 +------------------------------- + +在每一章的引言处加入了本章的代码树改动概况。 + +2021-02-08 +------------------------------- + +将 K210 开发板的烧写工具 ``kflash.py`` 从项目中移除。 + +2021-02-07 +------------------------------- + +将所有分支的 RustSBI 更新为最新的 0.1.1 版本[3257d899], **不加任何改动** 直接放在项目中。这导致 qemu 和 k210 两个平台的内核入口点变得不同,目前根据 RustSBI 的默认配置,qemu 平台上的内核入口点为 ``0x80200000`` ,而 k210 平台上为了提高烧写速度则为 ``0x80020000`` 。 + +前几个章节应用放置在内存中的位置也需要对应进行修改: + +- 第二章应用的起始地址变为 ``0x80400000`` ; +- 第三章应用的起始地址变为 ``0x80400000`` 。 + +文档稍后更新。 \ No newline at end of file diff --git a/source/pygments-coloring.txt b/source/pygments-coloring.txt new file mode 100644 index 0000000000000000000000000000000000000000..09c2cb548a6296c2563178f8fa54b42ce95ae099 --- /dev/null +++ b/source/pygments-coloring.txt @@ -0,0 +1,32 @@ +Pygments 默认配色: +Keyword.Constant 深绿加粗 +Keyword.Declaration 深绿加粗 +Keyword.Namespace 深绿加粗 +Keyword.Pseudo 浅绿 +Keyword.Reserved 深绿加粗 +Keyword.Type 樱桃红 +Name.Attribute 棕黄 +Name.Builtin 浅绿 +Name.Builtin.Pseudo 浅绿 +Name.Class 深蓝加粗 +Name.Constant 棕红 +Name.Decorator 浅紫 +Name.Entity 灰色 +Name.Exception 深红 +Name.Function 深蓝 +Name.Function.Magic 深蓝 +Name.Label 棕黄 +Name.Namespace 深蓝加粗 +Name.Other 默认黑色 +Name.Tag 深绿加粗 +Name.Variable 蓝黑 + + +通用寄存器 -> 棕黄 Name.Attribute +CSR -> 棕红 Name.Constant +指令 -> 浅紫 Name.Decorator +伪指令 -> 樱桃红 Keyword.Type +Directives -> 深蓝 Name.Function +标签/剩余字面量 -> 浅绿 Name.Builtin +数字 -> Number + diff --git a/source/quickstart.rst b/source/quickstart.rst deleted file mode 100644 index a0ac0f2e348d73f4aa5f88ac8a0264fe6cd102a3..0000000000000000000000000000000000000000 --- a/source/quickstart.rst +++ /dev/null @@ -1,30 +0,0 @@ -快速上手 -============ - -.. toctree:: - :hidden: - :maxdepth: 4 - -本节我们将完成环境配置并成功运行 rCore-Tutorial。 - -首先,请参考 `环境部署 `_ 安装 qemu 模拟器 -和 rust。有一些小的变更如下: - -- 将 ``riscv64imac-unknown-none-elf`` 改成 ``riscv64gc-unknown-none-elf``; -- 在使用文档中提供的链接下载 qemu 源码的时候,点击下载之后需要将链接中的 ``localhost`` 替换为 ``42.194.184.212:5212``。若仍然 - 不行的话,可以在 `SiFive 官网 `_ 下载预编译的 qemu,比如 - `Ubuntu 版本 qemu `_ 。 - -此外: - -- 下载安装 `macOS 平台 `_ - 或 `Ubuntu 平台 `_ - 的预编译版本 ``riscv64-unknown-elf-*`` 工具链,并添加到环境变量。可以在提示找不到的情况下再进行下载。 -- 下载安装 `Linux 平台 `_ 预编译版本的 ``riscv64-linux-musl-*`` 工具链,并 - 添加到环境变量。可以在提示找不到的情况下再进行下载。 -- 如果想在 Maix 系列开发板上运行,需要安装 python 包 ``pyserial`` 和串口终端 miniterm 。 - -.. warning:: - - **FIXME: 提供一套开箱即用的 Docker 环境** - diff --git a/docs/_images/test.gif b/source/resources/test.gif similarity index 100% rename from docs/_images/test.gif rename to source/resources/test.gif diff --git a/source/rest-example.rst b/source/rest-example.rst index ed7fcb21a6e5aa091141a978004dad6b628717f7..8321b46810eeb1ab07c7749d1150585ea120ed57 100644 --- a/source/rest-example.rst +++ b/source/rest-example.rst @@ -12,7 +12,7 @@ reStructuredText 基本语法 外部链接的引入方法。注意,链接的名字和用一对尖括号包裹起来的链接地址之间必须有一个空格。链接最后的下划线和片段的后续内容之间也需要 有一个空格。 - 接下来是一个文档内部引用的例子。比如,戳 :doc:`/quickstart` 可以进入快速上手环节。 + 接下来是一个文档内部引用的例子。比如,戳 :doc:`chapter0/5setup-devel-env` 可以进入快速上手环节。 .. warning:: @@ -41,7 +41,7 @@ reStructuredText 基本语法 下面是一个测试 gif。 -.. image:: test.gif +.. image:: resources/test.gif 接下来是一个表格的例子。 diff --git a/source/collaboration.rst b/source/setup-sphinx.rst similarity index 53% rename from source/collaboration.rst rename to source/setup-sphinx.rst index 3cec6c0e8d7c87b81c9a8b8d21828e1e9aa19026..369e80f9ee24d24b0c562fa42639dc6d605ff8cb 100644 --- a/source/collaboration.rst +++ b/source/setup-sphinx.rst @@ -1,5 +1,5 @@ -项目协作 -================== +修改和构建本项目 +==================================== .. toctree:: :hidden: @@ -10,7 +10,6 @@ 3. ``pip install jieba`` 安装中文分词。 4. ``pip install sphinx-comments`` 安装 Sphinx 讨论区插件。 5. :doc:`/rest-example` 是 ReST 的一些基本语法,也可以参考已完成的文档。 -6. 修改之后,在项目根目录下 ``make clean && make html`` 即可在 ``build/html/index.html`` 查看本地构建的主页。请注意在修改 - 章节目录结构之后需要 ``make clean`` 一下,不然可能无法正常更新。 -7. 确认修改无误之后,在项目根目录下 ``make deploy`` 然后即可 ``git add -A && git commit -m && git push`` 上传到远程仓库。 - 如果出现冲突的话,请删除掉 ``docs`` 目录再进行 merge。 +6. 修改之后,在项目根目录下 ``make clean && make html`` 即可在 ``build/html/index.html`` 查看本地构建的主页。请注意在修改章节目录结构之后需要 ``make clean`` 一下,不然可能无法正常更新。 +7. 确认修改无误之后,将 ``main`` 主分支上的修改 merge 到 ``deploy`` 分支,在项目根目录下 ``make deploy`` 即可将更新后的文档部署到用于部署的 ``deploy`` 分支上。 + 如果与其他人的提交冲突的话,请删除掉 ``docs`` 目录再进行 merge。 diff --git a/source/terminology.rst b/source/terminology.rst new file mode 100644 index 0000000000000000000000000000000000000000..d67d8e18ee765979cfa8d16e713a64133b8fa462 --- /dev/null +++ b/source/terminology.rst @@ -0,0 +1,396 @@ +术语中英文对照表 +========================= + +.. toctree:: + :hidden: + :maxdepth: 4 + +第一章:RV64 裸机应用 +---------------------------------- + +.. list-table:: + :align: center + :header-rows: 1 + :widths: 40 60 30 + + * - 中文 + - 英文 + - 出现章节 + * - 执行环境 + - Execution Environment + - :ref:`应用程序运行环境与平台支持 ` + * - 系统调用 + - System Call + - :ref:`应用程序运行环境与平台支持 ` + * - 指令集体系结构 + - ISA, Instruction Set Architecture + - :ref:`应用程序运行环境与平台支持 ` + * - 抽象 + - Abstraction + - :ref:`应用程序运行环境与平台支持 ` + * - 平台 + - Platform + - :ref:`应用程序运行环境与平台支持 ` + * - 目标三元组 + - Target Triplet + - :ref:`应用程序运行环境与平台支持 ` + * - 裸机平台 + - Bare-Metal + - :ref:`应用程序运行环境与平台支持 ` + * - 交叉编译 + - Cross Compile + - :ref:`移除标准库依赖 ` + * - 物理地址 + - Physical Address + - :ref:`重建最小化运行时 ` + * - 物理内存 + - Physical Memory + - :ref:`重建最小化运行时 ` + * - 引导加载程序 + - Bootloader + - :ref:`重建最小化运行时 ` + * - 控制流 + - Control Flow + - :ref:`重建最小化运行时 ` + * - 函数调用 + - Function Call + - :ref:`重建最小化运行时 ` + * - 源寄存器 + - Source Register + - :ref:`重建最小化运行时 ` + * - 立即数 + - Immediate + - :ref:`重建最小化运行时 ` + * - 目标寄存器 + - Destination Register + - :ref:`重建最小化运行时 ` + * - 伪指令 + - Pseudo Instruction + - :ref:`重建最小化运行时 ` + * - 上下文 + - Context + - :ref:`重建最小化运行时 ` + * - 活动记录 + - Activation Record + - :ref:`重建最小化运行时 ` + * - 保存/恢复 + - Save/Restore + - :ref:`重建最小化运行时 ` + * - 被调用者保存 + - Callee-Saved + - :ref:`重建最小化运行时 ` + * - 调用者保存 + - Caller-Saved + - :ref:`重建最小化运行时 ` + * - 开场白 + - Prologue + - :ref:`重建最小化运行时 ` + * - 收场白 + - Epilogue + - :ref:`重建最小化运行时 ` + * - 调用规范 + - Calling Convention + - :ref:`重建最小化运行时 ` + * - 栈/栈指针/栈帧 + - Stack/Stack Pointer/Stackframe + - :ref:`重建最小化运行时 ` + * - 后入先出 + - LIFO, Last In First Out + - :ref:`重建最小化运行时 ` + * - 段 + - Section + - :ref:`重建最小化运行时 ` + * - 内存布局 + - Memory Layout + - :ref:`重建最小化运行时 ` + * - 堆 + - Heap + - :ref:`重建最小化运行时 ` + * - 编译器 + - Compiler + - :ref:`重建最小化运行时 ` + * - 汇编器 + - Assembler + - :ref:`重建最小化运行时 ` + * - 链接器 + - Linker + - :ref:`重建最小化运行时 ` + * - 目标文件 + - Object File + - :ref:`重建最小化运行时 ` + * - 链接脚本 + - Linker Script + - :ref:`重建最小化运行时 ` + * - 可执行和链接格式 + - ELF, Executable and Linkable Format + - :ref:`手动加载、运行应用程序 ` + * - 元数据 + - Metadata + - :ref:`手动加载、运行应用程序 ` + * - 魔数 + - Magic + - :ref:`手动加载、运行应用程序 ` + * - 裸指针 + - Raw Pointer + - :ref:`手动加载、运行应用程序 ` + * - 解引用 + - Dereference + - :ref:`手动加载、运行应用程序 ` + +第二章:批处理系统 +------------------------- + +.. list-table:: + :align: center + :header-rows: 1 + :widths: 40 60 30 + + * - 中文 + - 英文 + - 出现章节 + * - 批处理系统 + - Batch System + - :ref:`引言 ` + * - 特权级 + - Privilege + - :ref:`引言 ` + * - 监督模式执行环境 + - SEE, Supervisor Execution Environment + - :ref:`RISC-V 特权级架构 ` + * - 异常控制流 + - ECF, Exception Control Flow + - :ref:`RISC-V 特权级架构 ` + * - 陷入 + - Trap + - :ref:`RISC-V 特权级架构 ` + * - 异常 + - Exception + - :ref:`RISC-V 特权级架构 ` + * - 执行环境调用 + - Environment Call + - :ref:`RISC-V 特权级架构 ` + * - 监督模式二进制接口 + - SBI, Supervisor Binary Interface + - :ref:`RISC-V 特权级架构 ` + * - 应用程序二进制接口 + - ABI, Application Binary Interface + - :ref:`RISC-V 特权级架构 ` + * - 控制状态寄存器 + - CSR, Control and Status Register + - :ref:`RISC-V 特权级架构 ` + * - 胖指针 + - Fat Pointer + - :ref:`实现应用程序 ` + * - 内部可变性 + - Interior Mutability + - :ref:`实现应用程序 ` + * - 指令缓存 + - i-cache, Instruction Cache + - :ref:`实现批处理系统 ` + * - 数据缓存 + - d-cache, Data Cache + - :ref:`实现批处理系统 ` + * - 执行流 + - Execution of Thread + - :ref:`处理 Trap ` + * - 原子指令 + - Atomic Instruction + - :ref:`处理 Trap ` + +第三章:多道程序与分时多任务 +---------------------------------------------------------------------------- + +.. list-table:: + :align: center + :header-rows: 1 + :widths: 40 60 30 + + * - 中文 + - 英文 + - 出现章节 + * - 多道程序 + - Multiprogramming + - :ref:`引言 ` + * - 分时多任务系统 + - Time-Sharing Multitasking + - :ref:`引言 ` + * - 任务上下文 + - Task Context + - :ref:`任务切换 ` + * - 输入/输出 + - I/O, Input/Output + - :ref:`多道程序与协作式调度 ` + * - 任务控制块 + - Task Control Block + - :ref:`多道程序与协作式调度 ` + * - 吞吐量 + - Throughput + - :ref:`分时多任务系统与抢占式调度 ` + * - 后台应用 + - Background Application + - :ref:`分时多任务系统与抢占式调度 ` + * - 交互式应用 + - Interactive Application + - :ref:`分时多任务系统与抢占式调度 ` + * - 协作式调度 + - Cooperative Scheduling + - :ref:`分时多任务系统与抢占式调度 ` + * - 时间片 + - Time Slice + - :ref:`分时多任务系统与抢占式调度 ` + * - 公平性 + - Fairness + - :ref:`分时多任务系统与抢占式调度 ` + * - 时间片轮转算法 + - RR, Round-Robin + - :ref:`分时多任务系统与抢占式调度 ` + * - 中断 + - Interrupt + - :ref:`分时多任务系统与抢占式调度 ` + * - 同步 + - Synchronous + - :ref:`分时多任务系统与抢占式调度 ` + * - 异步 + - Asynchronous + - :ref:`分时多任务系统与抢占式调度 ` + * - 并行 + - Parallel + - :ref:`分时多任务系统与抢占式调度 ` + * - 软件中断 + - Software Interrupt + - :ref:`分时多任务系统与抢占式调度 ` + * - 时钟中断 + - Timer Interrupt + - :ref:`分时多任务系统与抢占式调度 ` + * - 外部中断 + - External Interrupt + - :ref:`分时多任务系统与抢占式调度 ` + * - 嵌套中断 + - Nested Interrupt + - :ref:`分时多任务系统与抢占式调度 ` + * - 轮询 + - Busy Loop + - :ref:`分时多任务系统与抢占式调度 ` + +第四章:地址空间 +------------------------------------------- + +.. list-table:: + :align: center + :header-rows: 1 + :widths: 40 60 30 + + * - 中文 + - 英文 + - 出现章节 + * - 幻象 + - Illusion + - :ref:`引言 ` + * - 时分复用 + - TDM, Time-Division Multiplexing + - :ref:`引言 ` + * - 地址空间 + - Address Space + - :ref:`地址空间 ` + * - 虚拟地址 + - Virtual Address + - :ref:`地址空间 ` + * - 内存管理单元 + - MMU, Memory Management Unit + - :ref:`地址空间 ` + * - 地址转换 + - Address Translation + - :ref:`地址空间 ` + * - 插槽 + - Slot + - :ref:`地址空间 ` + * - 位图 + - Bitmap + - :ref:`地址空间 ` + * - 内碎片 + - Internal Fragment + - :ref:`地址空间 ` + * - 外碎片 + - External Fragment + - :ref:`地址空间 ` + * - 页面 + - Page + - :ref:`地址空间 ` + * - 虚拟页号 + - VPN, Virtual Page Number + - :ref:`地址空间 ` + * - 物理页号 + - PPN, Physical Page Number + - :ref:`地址空间 ` + * - 页表 + - Page Table + - :ref:`地址空间 ` + * - 静态分配 + - Static Allocation + - :ref:`Rust 中的动态内存分配 ` + * - 动态分配 + - Dynamic Allocation + - :ref:`Rust 中的动态内存分配 ` + * - 智能指针 + - Smart Pointer + - :ref:`Rust 中的动态内存分配 ` + * - 集合 + - Collection + - :ref:`Rust 中的动态内存分配 ` + * - 容器 + - Container + - :ref:`Rust 中的动态内存分配 ` + * - 借用检查 + - Borrow Check + - :ref:`Rust 中的动态内存分配 ` + * - 引用计数 + - Reference Counting + - :ref:`Rust 中的动态内存分配 ` + * - 垃圾回收 + - GC, Garbage Collection + - :ref:`Rust 中的动态内存分配 ` + * - 资源获取即初始化 + - RAII, Resource Acquisition Is Initialization + - :ref:`Rust 中的动态内存分配 ` + * - 页内偏移 + - Page Offset + - :ref:`实现 SV39 多级页表机制(上) ` + * - 类型转换 + - Type Convertion + - :ref:`实现 SV39 多级页表机制(上) ` + * - 字典树 + - Trie + - :ref:`实现 SV39 多级页表机制(上) ` + * - 多级页表 + - Multi-Level Page Table + - :ref:`实现 SV39 多级页表机制(上) ` + * - 页索引 + - Page Index + - :ref:`实现 SV39 多级页表机制(上) ` + * - 大页 + - Huge Page + - :ref:`实现 SV39 多级页表机制(上) ` + * - 恒等映射 + - Identical Mapping + - :ref:`实现 SV39 多级页表机制(下) ` + * - 页表自映射 + - Recursive Mapping + - :ref:`实现 SV39 多级页表机制(下) ` + * - 跳板 + - Trampoline + - :ref:`内核与应用的地址空间 ` + * - 隔离 + - Isolation + - :ref:`内核与应用的地址空间 ` + * - 保护页面 + - Guard Page + - :ref:`内核与应用的地址空间 ` + * - 快表 + - Translation Lookaside Buffer + - :ref:`基于地址空间的分时多任务 ` + * - 熔断 + - Meltdown + - :ref:`基于地址空间的分时多任务 ` + + + \ No newline at end of file diff --git a/source/test.gif b/source/test.gif deleted file mode 100644 index 8a687dd9119cbcc4ba1aac7d03975ffee7a806cc..0000000000000000000000000000000000000000 Binary files a/source/test.gif and /dev/null differ