From 4bd101c6e60e8f4421bc026a6a1f8688517eaea4 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Wed, 29 Jun 2022 12:24:12 +0000 Subject: [PATCH] Initialize gh-pages --- .buildinfo | 4 + .nojekyll | 0 0setup-devel-env.html | 636 + _images/app-as-full.png | Bin 0 -> 34166 bytes _images/app-software-stack.png | Bin 0 -> 11052 bytes _images/chap1-intro.png | Bin 0 -> 9217 bytes _images/color-demo.png | Bin 0 -> 71559 bytes _images/easy-fs-demo.png | Bin 0 -> 20644 bytes _images/fsm-coop.png | Bin 0 -> 24175 bytes _images/kernel-as-high.png | Bin 0 -> 34754 bytes _images/kernel-as-low.png | Bin 0 -> 20344 bytes _images/multiprogramming.png | Bin 0 -> 20854 bytes _images/satp.png | Bin 0 -> 23634 bytes _images/sv39-pte.png | Bin 0 -> 12849 bytes _images/sv39-va-pa.png | Bin 0 -> 16067 bytes _images/user-stack-cmdargs.png | Bin 0 -> 16759 bytes _sources/0setup-devel-env.rst.txt | 281 + _sources/appendix-a/index.rst.txt | 56 + _sources/appendix-b/index.rst.txt | 318 + _sources/appendix-c/index.rst.txt | 18 + _sources/appendix-d/index.rst.txt | 32 + _sources/chapter1/0intro.rst.txt | 115 + _sources/chapter1/1app-ee-platform.rst.txt | 120 + _sources/chapter1/2remove-std.rst.txt | 158 + _sources/chapter1/3mini-rt-usrland.rst.txt | 282 + _sources/chapter1/4mini-rt-baremetal.rst.txt | 328 + _sources/chapter1/5exercise.rst.txt | 143 + _sources/chapter1/index.rst.txt | 13 + _sources/chapter2/0intro.rst.txt | 163 + _sources/chapter2/2application.rst.txt | 214 + _sources/chapter2/3batch-system.rst.txt | 168 + _sources/chapter2/4trap-handling.rst.txt | 519 + _sources/chapter2/5exercise.rst.txt | 139 + _sources/chapter2/index.rst.txt | 13 + _sources/chapter3/0intro.rst.txt | 224 + _sources/chapter3/1multi-loader.rst.txt | 71 + _sources/chapter3/2task-switching.rst.txt | 104 + _sources/chapter3/3multiprogramming.rst.txt | 317 + .../chapter3/4time-sharing-system.rst.txt | 161 + _sources/chapter3/5exercise.rst.txt | 133 + _sources/chapter3/index.rst.txt | 14 + _sources/chapter4/0intro.rst.txt | 139 + .../chapter4/3sv39-implementation-1.rst.txt | 216 + .../chapter4/4sv39-implementation-2.rst.txt | 447 + _sources/chapter4/5kernel-app-spaces.rst.txt | 586 + .../6multitasking-based-on-as.rst.txt | 684 + _sources/chapter4/7exercise.rst.txt | 113 + _sources/chapter4/index.rst.txt | 12 + _sources/chapter5/0intro.rst.txt | 187 + _sources/chapter5/1process.rst.txt | 230 + .../chapter5/2core-data-structures.rst.txt | 540 + .../3implement-process-mechanism.rst.txt | 665 + _sources/chapter5/4exercise.rst.txt | 137 + _sources/chapter5/index.rst.txt | 12 + _sources/chapter6/0intro.rst.txt | 177 + _sources/chapter6/1file-descriptor.rst.txt | 243 + _sources/chapter6/1fs-interface.rst.txt | 112 + .../chapter6/2fs-implementation-1.rst.txt | 674 + .../chapter6/2fs-implementation-2.rst.txt | 591 + .../chapter6/3using-easy-fs-in-kernel.rst.txt | 313 + _sources/chapter6/4exercise.rst.txt | 114 + _sources/chapter6/index.rst.txt | 13 + _sources/chapter7/0intro.rst.txt | 120 + _sources/chapter7/1pipe.rst.txt | 364 + .../chapter7/2cmdargs-and-redirection.rst.txt | 337 + _sources/chapter7/3exercise.rst.txt | 20 + _sources/chapter7/index.rst.txt | 10 + _sources/chapter8/0intro.rst.txt | 254 + _sources/chapter8/1thread-kernel.rst.txt | 485 + _sources/chapter8/2lock.rst.txt | 379 + _sources/chapter8/3semaphore.rst.txt | 275 + _sources/chapter8/4condition-variable.rst.txt | 300 + _sources/chapter8/5exercise.rst.txt | 132 + _sources/chapter8/index.rst.txt | 15 + _sources/index.rst.txt | 85 + _sources/rest-example.rst.txt | 76 + _sources/setup-sphinx.rst.txt | 16 + _static/basic.css | 904 ++ _static/doctools.js | 323 + _static/documentation_options.js | 12 + _static/dracula.css | 91 + _static/file.png | Bin 0 -> 286 bytes _static/jquery-3.5.1.js | 10872 ++++++++++++++++ _static/jquery.js | 2 + _static/language_data.js | 297 + _static/minus.png | Bin 0 -> 90 bytes _static/my_style.css | 3 + _static/plus.png | Bin 0 -> 90 bytes _static/pygments.css | 243 + _static/scripts/main.js | 3 + _static/scripts/main.js.map | 1 + _static/searchtools.js | 522 + _static/styles/furo-extensions.css | 2 + _static/styles/furo-extensions.css.map | 1 + _static/styles/furo.css | 2 + _static/styles/furo.css.map | 1 + _static/tabs.css | 53 + _static/tabs.js | 145 + _static/translations.js | 63 + _static/underscore-1.13.1.js | 2042 +++ _static/underscore.js | 6 + appendix-a/index.html | 424 + appendix-b/index.html | 674 + appendix-c/index.html | 392 + appendix-d/index.html | 435 + chapter1/0intro.html | 497 + chapter1/1app-ee-platform.html | 497 + chapter1/2remove-std.html | 532 + chapter1/3mini-rt-usrland.html | 596 + chapter1/4mini-rt-baremetal.html | 658 + chapter1/5exercise.html | 420 + chapter1/index.html | 429 + chapter2/0intro.html | 552 + chapter2/2application.html | 586 + chapter2/3batch-system.html | 543 + chapter2/4trap-handling.html | 829 ++ chapter2/5exercise.html | 421 + chapter2/index.html | 425 + chapter3/0intro.html | 593 + chapter3/1multi-loader.html | 463 + chapter3/2task-switching.html | 488 + chapter3/3multiprogramming.html | 673 + chapter3/4time-sharing-system.html | 538 + chapter3/5exercise.html | 549 + chapter3/index.html | 427 + chapter4/0intro.html | 491 + chapter4/3sv39-implementation-1.html | 590 + chapter4/4sv39-implementation-2.html | 805 ++ chapter4/5kernel-app-spaces.html | 931 ++ chapter4/6multitasking-based-on-as.html | 1015 ++ chapter4/7exercise.html | 546 + chapter4/index.html | 454 + chapter5/0intro.html | 549 + chapter5/1process.html | 619 + chapter5/2core-data-structures.html | 899 ++ chapter5/3implement-process-mechanism.html | 1025 ++ chapter5/4exercise.html | 540 + chapter5/index.html | 454 + chapter6/0intro.html | 521 + chapter6/1file-descriptor.html | 620 + chapter6/1fs-interface.html | 510 + chapter6/2fs-implementation-1.html | 1014 ++ chapter6/2fs-implementation-2.html | 959 ++ chapter6/3using-easy-fs-in-kernel.html | 682 + chapter6/4exercise.html | 559 + chapter6/index.html | 465 + chapter7/0intro.html | 495 + chapter7/1pipe.html | 726 ++ chapter7/2cmdargs-and-redirection.html | 704 + chapter7/3exercise.html | 423 + chapter7/index.html | 420 + chapter8/0intro.html | 614 + chapter8/1thread-kernel.html | 853 ++ chapter8/2lock.html | 804 ++ chapter8/3semaphore.html | 643 + chapter8/4condition-variable.html | 676 + chapter8/5exercise.html | 546 + chapter8/index.html | 470 + genindex.html | 363 + index.html | 440 + objects.inv | Bin 0 -> 2295 bytes rest-example.html | 458 + search.html | 370 + searchindex.js | 1 + setup-sphinx.html | 396 + 165 files changed, 65696 insertions(+) create mode 100644 .buildinfo create mode 100644 .nojekyll create mode 100644 0setup-devel-env.html create mode 100644 _images/app-as-full.png create mode 100644 _images/app-software-stack.png create mode 100644 _images/chap1-intro.png create mode 100644 _images/color-demo.png create mode 100644 _images/easy-fs-demo.png create mode 100644 _images/fsm-coop.png create mode 100644 _images/kernel-as-high.png create mode 100644 _images/kernel-as-low.png create mode 100644 _images/multiprogramming.png create mode 100644 _images/satp.png create mode 100644 _images/sv39-pte.png create mode 100644 _images/sv39-va-pa.png create mode 100644 _images/user-stack-cmdargs.png create mode 100644 _sources/0setup-devel-env.rst.txt create mode 100644 _sources/appendix-a/index.rst.txt create mode 100644 _sources/appendix-b/index.rst.txt create mode 100644 _sources/appendix-c/index.rst.txt create mode 100644 _sources/appendix-d/index.rst.txt create mode 100644 _sources/chapter1/0intro.rst.txt create mode 100644 _sources/chapter1/1app-ee-platform.rst.txt create mode 100644 _sources/chapter1/2remove-std.rst.txt create mode 100644 _sources/chapter1/3mini-rt-usrland.rst.txt create mode 100644 _sources/chapter1/4mini-rt-baremetal.rst.txt create mode 100644 _sources/chapter1/5exercise.rst.txt create mode 100644 _sources/chapter1/index.rst.txt create mode 100644 _sources/chapter2/0intro.rst.txt create mode 100644 _sources/chapter2/2application.rst.txt create mode 100644 _sources/chapter2/3batch-system.rst.txt create mode 100644 _sources/chapter2/4trap-handling.rst.txt create mode 100644 _sources/chapter2/5exercise.rst.txt create mode 100644 _sources/chapter2/index.rst.txt create mode 100644 _sources/chapter3/0intro.rst.txt create mode 100644 _sources/chapter3/1multi-loader.rst.txt create mode 100644 _sources/chapter3/2task-switching.rst.txt create mode 100644 _sources/chapter3/3multiprogramming.rst.txt create mode 100644 _sources/chapter3/4time-sharing-system.rst.txt create mode 100644 _sources/chapter3/5exercise.rst.txt create mode 100644 _sources/chapter3/index.rst.txt create mode 100644 _sources/chapter4/0intro.rst.txt create mode 100644 _sources/chapter4/3sv39-implementation-1.rst.txt create mode 100644 _sources/chapter4/4sv39-implementation-2.rst.txt create mode 100644 _sources/chapter4/5kernel-app-spaces.rst.txt create mode 100644 _sources/chapter4/6multitasking-based-on-as.rst.txt create mode 100644 _sources/chapter4/7exercise.rst.txt create mode 100644 _sources/chapter4/index.rst.txt create mode 100644 _sources/chapter5/0intro.rst.txt create mode 100644 _sources/chapter5/1process.rst.txt create mode 100644 _sources/chapter5/2core-data-structures.rst.txt create mode 100644 _sources/chapter5/3implement-process-mechanism.rst.txt create mode 100644 _sources/chapter5/4exercise.rst.txt create mode 100644 _sources/chapter5/index.rst.txt create mode 100644 _sources/chapter6/0intro.rst.txt create mode 100644 _sources/chapter6/1file-descriptor.rst.txt create mode 100644 _sources/chapter6/1fs-interface.rst.txt create mode 100644 _sources/chapter6/2fs-implementation-1.rst.txt create mode 100644 _sources/chapter6/2fs-implementation-2.rst.txt create mode 100644 _sources/chapter6/3using-easy-fs-in-kernel.rst.txt create mode 100644 _sources/chapter6/4exercise.rst.txt create mode 100644 _sources/chapter6/index.rst.txt create mode 100644 _sources/chapter7/0intro.rst.txt create mode 100644 _sources/chapter7/1pipe.rst.txt create mode 100644 _sources/chapter7/2cmdargs-and-redirection.rst.txt create mode 100644 _sources/chapter7/3exercise.rst.txt create mode 100644 _sources/chapter7/index.rst.txt create mode 100644 _sources/chapter8/0intro.rst.txt create mode 100644 _sources/chapter8/1thread-kernel.rst.txt create mode 100644 _sources/chapter8/2lock.rst.txt create mode 100644 _sources/chapter8/3semaphore.rst.txt create mode 100644 _sources/chapter8/4condition-variable.rst.txt create mode 100644 _sources/chapter8/5exercise.rst.txt create mode 100644 _sources/chapter8/index.rst.txt create mode 100644 _sources/index.rst.txt create mode 100644 _sources/rest-example.rst.txt create mode 100644 _sources/setup-sphinx.rst.txt create mode 100644 _static/basic.css create mode 100644 _static/doctools.js create mode 100644 _static/documentation_options.js create mode 100644 _static/dracula.css create mode 100644 _static/file.png create mode 100644 _static/jquery-3.5.1.js create mode 100644 _static/jquery.js create mode 100644 _static/language_data.js create mode 100644 _static/minus.png create mode 100644 _static/my_style.css create mode 100644 _static/plus.png create mode 100644 _static/pygments.css create mode 100644 _static/scripts/main.js create mode 100644 _static/scripts/main.js.map create mode 100644 _static/searchtools.js create mode 100644 _static/styles/furo-extensions.css create mode 100644 _static/styles/furo-extensions.css.map create mode 100644 _static/styles/furo.css create mode 100644 _static/styles/furo.css.map create mode 100644 _static/tabs.css create mode 100644 _static/tabs.js create mode 100644 _static/translations.js create mode 100644 _static/underscore-1.13.1.js create mode 100644 _static/underscore.js create mode 100644 appendix-a/index.html create mode 100644 appendix-b/index.html create mode 100644 appendix-c/index.html create mode 100644 appendix-d/index.html create mode 100644 chapter1/0intro.html create mode 100644 chapter1/1app-ee-platform.html create mode 100644 chapter1/2remove-std.html create mode 100644 chapter1/3mini-rt-usrland.html create mode 100644 chapter1/4mini-rt-baremetal.html create mode 100644 chapter1/5exercise.html create mode 100644 chapter1/index.html create mode 100644 chapter2/0intro.html create mode 100644 chapter2/2application.html create mode 100644 chapter2/3batch-system.html create mode 100644 chapter2/4trap-handling.html create mode 100644 chapter2/5exercise.html create mode 100644 chapter2/index.html create mode 100644 chapter3/0intro.html create mode 100644 chapter3/1multi-loader.html create mode 100644 chapter3/2task-switching.html create mode 100644 chapter3/3multiprogramming.html create mode 100644 chapter3/4time-sharing-system.html create mode 100644 chapter3/5exercise.html create mode 100644 chapter3/index.html create mode 100644 chapter4/0intro.html create mode 100644 chapter4/3sv39-implementation-1.html create mode 100644 chapter4/4sv39-implementation-2.html create mode 100644 chapter4/5kernel-app-spaces.html create mode 100644 chapter4/6multitasking-based-on-as.html create mode 100644 chapter4/7exercise.html create mode 100644 chapter4/index.html create mode 100644 chapter5/0intro.html create mode 100644 chapter5/1process.html create mode 100644 chapter5/2core-data-structures.html create mode 100644 chapter5/3implement-process-mechanism.html create mode 100644 chapter5/4exercise.html create mode 100644 chapter5/index.html create mode 100644 chapter6/0intro.html create mode 100644 chapter6/1file-descriptor.html create mode 100644 chapter6/1fs-interface.html create mode 100644 chapter6/2fs-implementation-1.html create mode 100644 chapter6/2fs-implementation-2.html create mode 100644 chapter6/3using-easy-fs-in-kernel.html create mode 100644 chapter6/4exercise.html create mode 100644 chapter6/index.html create mode 100644 chapter7/0intro.html create mode 100644 chapter7/1pipe.html create mode 100644 chapter7/2cmdargs-and-redirection.html create mode 100644 chapter7/3exercise.html create mode 100644 chapter7/index.html create mode 100644 chapter8/0intro.html create mode 100644 chapter8/1thread-kernel.html create mode 100644 chapter8/2lock.html create mode 100644 chapter8/3semaphore.html create mode 100644 chapter8/4condition-variable.html create mode 100644 chapter8/5exercise.html create mode 100644 chapter8/index.html create mode 100644 genindex.html create mode 100644 index.html create mode 100644 objects.inv create mode 100644 rest-example.html create mode 100644 search.html create mode 100644 searchindex.js create mode 100644 setup-sphinx.html diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 0000000..faeaf1c --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# 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: fa19155d8d3818690b607bbf52d47471 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/0setup-devel-env.html b/0setup-devel-env.html new file mode 100644 index 0000000..abf434d --- /dev/null +++ b/0setup-devel-env.html @@ -0,0 +1,636 @@ + + + + + + + + 第零章:实验环境配置 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

第零章:实验环境配置

+
+
+

本节我们将完成环境配置并成功运行 rCore-Tutorial 。整个流程分为下面几个部分:

+
    +
  • OS 环境配置

  • +
  • Rust 开发环境配置

  • +
  • Qemu 模拟器安装

  • +
  • 其他工具安装

  • +
  • 试运行 rCore-Tutorial

  • +
+

如果你在环境配置中遇到了无法解决的问题,请在本节讨论区留言,我们会尽力提供帮助。

+
+

OS 环境配置

+

目前,实验主要支持 Ubuntu18.04/20.04 操作系统。使用 Windows10 和 macOS 的读者,可以安装一台 Ubuntu18.04 虚拟机或 Docker +进行实验。

+

Windows10 用户可以通过系统内置的 WSL2 虚拟机(请不要使用 WSL1)来安装 Ubuntu 18.04 / 20.04 。读者请自行在互联网上搜索相关安装教程,或 适用于 Linux 的 Windows 子系统安装指南 (Windows 10)

+
+

注解

+

基于github classroom的开发方式

+

基于github classroom,可方便建立开发用的git repository,并可基于github的 codespace (在线版ubuntu +vscode)在线开发使用。整个开发环境仅仅需要一个网络浏览器。

+
    +
  1. 在网络浏览器中用自己的 github id 登录 github.com

  2. +
  3. 接收 第一个实验练习 setup-env-run-os1 的github classroom在线邀请 ,根据提示一路选择OK即可。

  4. +
  5. 完成第二步后,你的第一个实验练习 setup-env-run-os1 的 github repository 会被自动建立好,点击此github repository的链接,就可看到你要完成的第一个实验了。

  6. +
  7. 在你的第一个实验练习的网页的中上部可以看到一个醒目的 code 绿色按钮,点击后,可以进一步看到 codespace 标签和醒目的 create codesapce on main 绿色按钮。请点击这个绿色按钮,就可以进入到在线的ubuntu +vscode环境中

  8. +
  9. 再按照下面的环境安装提示在vscode的 console 中安装配置开发环境:rustc,qemu等工具。

  10. +
  11. 重要: 在vscode的 console 中执行 make setupclassroom_testX (该命令仅执行一次,X的范围为 1-8)配置githubclassroom 自动评分功能。

  12. +
  13. 然后就可以基于在线vscode进行开发、运行、提交等完整的实验过程了。

  14. +
+

上述的3,4,5步不是必须的,你也可以线下本地开发。

+
+
+

注解

+

Docker 开发环境

+

感谢 dinghao188 和张汉东老师帮忙配置好的 Docker 开发环境,进入 Docker 开发环境之后不需要任何软件工具链的安装和配置,可以直接将 tutorial 运行起来,目前应该仅支持将 tutorial 运行在 Qemu 模拟器上。

+

使用方法如下(以 Ubuntu18.04 为例):

+
    +
  1. 通过 su 切换到管理员账户 root

  2. +
  3. rCore-Tutorial 根目录下 make docker 进入到 Docker 环境;

  4. +
  5. 进入 Docker 之后,会发现当前处于根目录 / ,我们通过 cd mnt 将当前工作路径切换到 /mnt 目录;

  6. +
  7. 通过 ls 可以发现 /mnt 目录下的内容和 rCore-Tutorial-v3 目录下的内容完全相同,接下来就可以在这个环境下运行 tutorial 了。例如 cd os && make run

  8. +
+
+

使用 macOS 进行实验理论上也是可行的,但本章节仅介绍 Ubuntu 下的环境配置方案。

+
+

注解

+

经初步测试,使用 M1 芯片的 macOS 也可以运行本实验的框架,即我们的实验对平台的要求不是很高。但我们仍建议同学配置 Ubuntu 环境,以避免未知的环境问题。

+
+
+
+

Rust 开发环境配置

+

首先安装 Rust 版本管理器 rustup 和 Rust 包管理器 cargo,可以使用官方安装脚本:

+
curl https://sh.rustup.rs -sSf | sh
+
+
+

如果因网络问题通过命令行下载脚本失败了,可以在浏览器地址栏中输入 https://sh.rustup.rs 将脚本下载到本地运行。或者使用字节跳动提供的镜像源。

+

建议将 rustup 的镜像地址修改为中科大的镜像服务器,以加速安装:

+
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 帮助

+
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
+
+
+

也可以设置科学上网代理:

+
# 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
+
+
+

安装中全程选择默认选项即可。

+

安装完成后,我们可以重新打开一个终端来让新设置的环境变量生效,也可以手动将环境变量设置应用到当前终端, +只需输入以下命令:

+
source $HOME/.cargo/env
+
+
+

确认一下我们正确安装了 Rust 工具链:

+
rustc --version
+
+
+

最好把 Rust 包管理器 cargo 镜像地址 crates.io 也替换成中国科学技术大学的镜像服务器,来加速三方库的下载。 +打开或新建 ~/.cargo/config 文件,并把内容修改为:

+
[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 帮助

+
[source.crates-io]
+replace-with = 'tuna'
+
+[source.tuna]
+registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"
+
+
+

推荐 JetBrains Clion + Rust插件 或者 Visual Studio Code 搭配 rust-analyzer 和 RISC-V Support 插件 进行代码阅读和开发。

+
+

注解

+
    +
  • JetBrains Clion是付费商业软件,但对于学生和教师,只要在 JetBrains 网站注册账号,可以享受一定期限(半年左右)的免费使用的福利。

  • +
  • Visual Studio Code 是开源软件。

  • +
  • 当然,采用 VIM,Emacs 等传统的编辑器也是没有问题的。

  • +
+
+
+
+

Qemu 模拟器安装

+

我们需要使用 Qemu 7.0.0 以上版本进行实验,为此,从源码手动编译安装 Qemu 模拟器:

+
# 安装编译所需的依赖包
+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 ninja-build
+# 下载源码包
+# 如果下载速度过慢可以使用我们提供的百度网盘链接:https://pan.baidu.com/s/1z-iWIPjxjxbdFS2Qf-NKxQ
+# 提取码 8woe
+wget https://download.qemu.org/qemu-7.0.0.tar.xz
+# 解压
+tar xvJf qemu-7.0.0.tar.xz
+# 编译安装并配置 RISC-V 支持
+cd qemu-7.0.0
+./configure --target-list=riscv64-softmmu,riscv64-linux-user
+make -j$(nproc)
+
+
+
+

注解

+

注意,上面的依赖包可能并不完全,比如在 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 的依赖包可以从 这里 +找到。请自行选择合适的编译器版本正常编译 Qemu。

+
+

之后我们可以在同目录下 sudo make install 将 Qemu 安装到 /usr/local/bin 目录下,但这样经常会引起 +冲突。个人来说更习惯的做法是,编辑 ~/.bashrc 文件(如果使用的是默认的 bash 终端),在文件的末尾加入 +几行:

+
# 请注意,qemu-7.0.0 的父目录可以随着你的实际安装位置灵活调整
+export PATH=$PATH:/home/shinbokuow/Downloads/built/qemu-7.0.0
+export PATH=$PATH:/home/shinbokuow/Downloads/built/qemu-7.0.0/riscv64-softmmu
+export PATH=$PATH:/home/shinbokuow/Downloads/built/qemu-7.0.0/riscv64-linux-user
+
+
+

随后即可在当前终端 source ~/.bashrc 更新系统路径,或者直接重启一个新的终端。

+

确认 Qemu 的版本:

+
qemu-system-riscv64 --version
+qemu-riscv64 --version
+
+
+
+
+

试运行 rCore-Tutorial

+
$ git clone https://github.com/LearningOS/rust-based-os-comp2022.git
+$ cd rust-based-os-comp2022
+$ make setupclassroom  //注意:这一步很重要,是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次,以后一般就不用执行了,除非 .github/workflows/classroom.yml发生了变化。
+
+
+

我们先运行不需要处理用户代码的裸机操作系统 os1

+
cd os1
+LOG=DEBUG make run
+
+
+

如果你的环境配置正确,你应当会看到如下输出:

+
[rustsbi] RustSBI version 0.2.2, adapting to RISC-V SBI v1.0.0
+.______       __    __      _______.___________.  _______..______   __
+|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
+|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
+|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
+|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
+| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|
+[rustsbi] Implementation     : RustSBI-QEMU Version 0.1.1
+[rustsbi] Platform Name      : riscv-virtio,qemu
+[rustsbi] Platform SMP       : 1
+[rustsbi] Platform Memory    : 0x80000000..0x88000000
+[rustsbi] Boot HART          : 0
+[rustsbi] Device Tree Region : 0x87000000..0x87000ef2
+[rustsbi] Firmware Address   : 0x80000000
+[rustsbi] Supervisor Address : 0x80200000
+[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
+[rustsbi] pmp02: 0x80000000..0x80200000 (---)
+[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
+Hello, world!
+[DEBUG] .rodata [0x80203000, 0x80205000)
+[ INFO] .data [0x80205000, 0x80206000)
+[ WARN] boot_stack [0x80206000, 0x80216000)
+[ERROR] .bss [0x80216000, 0x80217000)
+Panicked at src/main.rs:48 Shutdown machine!
+
+
+

通常 rCore 会自动关闭 Qemu 。如果在某些情况下需要强制结束,可以先按下 Ctrl+A ,再按下 X 来退出 Qemu。

+
+

注意

+

请务必执行 make run,这将为你安装一些上文没有提及的 Rust 包依赖。

+

如果卡在了

+
Updating git repository `https://github.com/rcore-os/riscv`
+
+
+

请通过更换 hosts 等方式解决科学上网问题,或者将 riscv 项目下载到本地,并修改 os/Cargo.toml 中的 riscv 包依赖路径

+
[dependencies]
+riscv = { path = "YOUR riscv PATH", features = ["inline-asm"] }
+
+
+
+

恭喜你完成了实验环境的配置,可以开始阅读教程的正文部分了!

+
+
+

GDB 调试支持*

+
+

注意

+

使用 GDB debug 并不是必须的,你可以暂时跳过本小节。

+
+

os 目录下 make debug 可以调试我们的内核,这需要安装终端复用工具 tmux ,还需要基于 riscv64 平台的 gdb 调试器 riscv64-unknown-elf-gdb 。该调试器包含在 riscv64 gcc 工具链中,工具链的预编译版本可以在如下链接处下载:

+ +

解压后在 bin 目录下即可找到 riscv64-unknown-elf-gdb 以及另外一些常用工具 objcopy/objdump/readelf 等。

+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/_images/app-as-full.png b/_images/app-as-full.png new file mode 100644 index 0000000000000000000000000000000000000000..796c23e751c721b86c3e4ad58eb4372c6ee46113 GIT binary patch literal 34166 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 KP@;G8+J6DbJy41O literal 0 HcmV?d00001 diff --git a/_images/app-software-stack.png b/_images/app-software-stack.png new file mode 100644 index 0000000000000000000000000000000000000000..021f145b53b6a82f9a9075747502e7b4aa819c56 GIT binary patch literal 11052 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 literal 0 HcmV?d00001 diff --git a/_images/fsm-coop.png b/_images/fsm-coop.png new file mode 100644 index 0000000000000000000000000000000000000000..bc633bcd2b59f693f14c5a9ab9f90c62b0652f45 GIT binary patch literal 24175 zcmc$`c|4Tw_dh;}ibx?vNLnnRvX3kgLiUNWrm}}HLv|_@p_1L$cZQ0wjIB+C7|WQl zR<>C}vM0Y{sxMw>%x);w)$XW4Hzco^^%ESAkRY0NZB$4OW zp^kLXloW9`_K%0TE={mplj2Q&x96I;IXC-Z4?Ru936H~6R##EoBU+bY3$!_VMIU}M zPh;Gr$DX8pVSKfEhV*Lo>sV^0>L_jJD(d2D*V2;rbmL-|QZO!^0gwbccJiSi^sVl? z2TTSA3mf2s({GdrBN?F!rkbQ((06_$elPTWnLURK`hLo%aT@wQCinkO2ie(isvFMz z7%K&z&vL{sCt>&yh>)=R$B~SC7$6$6v%!=!)B*X~Hx5ZzA7*A}j^mLqPvJ7Ua4wCQ zzawqi_(gS*jqvVqp!?RjTI>0uwwh7jVdrL}ELF$3k@}VEn4HyLtD5t2t^>u+JI~9n zSDgy59{$l%*thK$5!zMVGS{@EO3=uxxjsA{XMqjw4H)tH5kHRXH|V{UwiWE%>1qJO zOARQ(VBAlH;QN&(pH@DM&4=}0At7#f@2nP?GaF$}yU@~}uV_7A88%3??HBdy5}(~E zNVav&T4#>XYzpmL#9k2NYzQ{F;ag4fP)Y38$Lxg-Meywg6ZOh5%enNnow>YWzIk&o zJ2!jM?G(}#9bjbQ>@BP-8H)KbqI*RXS}$oX2@U^e;O+)-ytB=!faZNo}<}ZMv6-kee2tPY>I9B zFRoS&h5*re7)I|D!Db^>+$sbJ{>X$7$tgaPxYD}+Zh)cXuyN11nBCsB7aj+m3Avzk zaiGN!v2zMF9xL*9S;Ov^9D>{E+gwnd%au|C-6c!+s+G?+G9{l{Sg=QC zInb8LfpIH2nZK@(c5yO70?s@U)f_&hF=7EQtNt+VE*ZxLnZn z2=cp3h=8MoEEk-=AlR=GBTq6un!}~O2ij2FS1dG&sLgyr1gyk^AJylP=4=Lu;EKl4 z{Uy2I%4djNUtC>IAy2ud*IYLx#%;%%>baHnA2cr!IwQ`hwTq6_oh^OJwO?~? zec=YVC3NeRc72jzQ%l2qQH|y>)h~6~WpmVRYn8Yy+oyL;AB?P=ImE$x0c+!dv~>5$ zXi*EOaBf;~&>h)yu}XM%23E}k_DR@%vL1y;B+@ms)ZJ=JQNkEE(`F?{-HYFzb=xZ8$>E1lwCI}*#|P&b&e*Njjo&h#T5??O`a~*8tj-4Nv#SF%_xTM zl%c8!x3-lCmq@$thW}HmWJ_9?*M|Ck z4q{|t4oRM`a_W&cE4=c!aIr^i{K-+JKBTX&c0a@Jqc?oVgZ=t@dk+|Fv?P&V*}sp} z>Om`d-K>sVUz{11V}jxCL(0)xpTR5#&#cdTYPs@$UXs`a%EA#D$4fngd}EyVR0nA} zZ7Ec;{}yXnK!xoLqie0lXznuZtR5$G*v0{9G9IGG#5r+ii_P_DcHd3kn!YvZ(S2~g zWXx*5U=_i6wX9g9c#IeJ^a(W9{X)(Xb+Y9-o}~f4BO3UAUr{7(&=~trJcSX~4Cy@B zTa&U}HXkYHQhhf|ObX`PyH_;??0r|&Nq3eo-(x_zhaD5m5W9x+sG@vKbM*UKUBZKq z%-UrPbI^gBrZ%u8S3Bl&}Rc9V76t|jf zQZ0Q5iHBj)bkc_Og!Sz^NAKsCi?q15@ZfkVp9bEsLmZmuu8@(3GJ!kR5Eyz;m0|J- zL`8MRcw&-Ogse_m5B6aH@*#CWIurHlE2^vU*d&jJ+;8Oj7hsdj^oJk1jSk#UbP#w} zBY?a^qpC6-L(&P7WfqPwh!Dp0)$IPxrEW_XDzg!}C~MCY_IIbl@bo^Di?#Y=uXJM5 zzlsDg?%{ztl)dtRxkOn~q(3t1*WP2Y^u9QWX!E?1Bt&(~YEp&Xc&pe1UF*g_Uxfb0Y?{65gbS&IOgWS`w!j)rTO- z&ORP-(DGxn?hIna979AVvphXTXU&JOi&NQ)KUw&5Px7ntK~H{@;J$SDY_T;r{0RBb z-%I*?GNz_j_$ee(w!U0g5@glGdJHlY&#`78tIjAy7$q_p0D?aQvk42^6VBy@%S?V7 z36qV1q-;+{afi^IJup7{2A>3fMs@5a1~QW9i*vG&^WwKlB3o1yvNv0{AjAW`=tG9G z52R}Us)UCl?k0OOoDbP}FbrrwI^s6BqTF#Y8G5G;t^=))-EgBxM8bwaWO zQ%UAX7zf~R*mGk%;+XNTU48lZ5~>Q11_Rx4?B!JK^$_<#qtVEEX1Z-BA!iF2PctDWxL^l`5{HUyqr!@+a3{mwC*}~atbe;Z$`mUYVe=@vc z0pVZ3uV`#d@A^Y5q7!5CRDl(}Pj(kK7evDSyCMW{&5+4N%NfFGQg~$8O^E%*oKFxj zxi>&}*XZ5BLOVdqi2Fg;gq|DjX>}q*dT{0fjk@SyK;zgs}Y_3x~#$HB9CU z)Ui;g-Ql$l$7g-%OY^JTYc!-|PrlnG0f`X<(=D)gCTC-FC5V2bUH zmlM&6%O`NPABISLQ_`w!mqs>cL&na{lw>k)-DSG{02=JgNYfsd0*17S+JNpXwQ(tC zJO?x)V}bN19Q&?SpQ7f+z;OC4HP9EK1+?rJu4} zs)P?hdy;bx5tB5m1pAsx;BuCPW)z-nCCQoenCoBH;iP?_D~m-x%=o{ zby@=S4GoQayYk4fR@Ji}IYyia$hNZ9a zqx(RZM_t3WatK_-LJ+r}kbL(c%b0)1f?3PcJ55mu^c~R90IhchC||#R)h#=140*g^ z@?bdQD-{GYkx|Aq#(!P0-Z0S>F)bsQNoKGGPmCBg2ddJAJ8?G+tU~z zkEc9-4UvjJ1%~YSG#a1Wqt+1(&5<9EITRLh59pCc8luLhAby!@*IL5ja9>MRjbT$O z;-YL2bz)}8a2$c*7r929&-szL(F;0d+$B~ROF6)%V}_Vaif`p(b0cm@i-)}sb*6OY z-#-mQ{P3vE9W=^*1W}lO$?h0x^bl;|-R#vrmc#TL3tNtrWSI6^twqkZx(P)0<%>>E zP6G}c!2E_s8lQkjvbVN&w`a$@h|Mcl=e`~6 zm3Xoeu-rat-=lP>cwDt1#%exiwJ(s;^h^+%xn%wUQG#5Y+iO^LbVxg3l;{Uc@?eFH zTV~L2U7y9z3hA$IwWZyPQ~LPxRh!^6)ouqz{#sSEKCRH=5(lS z98tQeUHZG~ye7IH9T4g2)e&-M*)O=|uW-LNFi+W3!nZthDffD=)ZT)zuE$GvtTR zCf!M3=a;`^uvt46;5HCHLL17Hw%K@HAbouU71Wh-&ap3Fce-ZSLuPUM^QNf+BklL^ zK`YNxzwuxN%PgzPX{>eprp=zE0E`Z`KL8uj6hBs}6EE1?GQ{B?U!z~?LuG~up{}{J zrn!cvsD(D$Fv7@y^iwo^fdgxy%h>6)%1~Na{Tz;(>vj?)SrDRlfm&YG)s-rfSAo$g zYOxw3R`sYV53OQq$KV@p5p9>+SnQ4=3oqHFR5bq9lL=b8+h6NxSM}8~J@vtU)h`*| z9b%5ZCqMN{pWL|rf>5~}VAs?F&Ioz4eJd`?tHYix&J9>vyvj;SFmu@X91gdP<~5%0 z0TQtF+yI@tkyEirm_bA4epkudSj)P*_5(~)E!)(V+(Ffdu1=|jC*k+D)dZLX*aWx) z(mjKzR=uzK&Pgb1IK^DpUYnRMEvtwQA@^#vGwtShB4wN@=azIGhb##;S9K1=j`G?C zc@4zdZ06DWwq4X1xc8&3`3&GbsVFhr*0S-Cu$eDOG9(y%PLXknQ<``plPh@2RfV?R z6Dr95@&P3?fKo!bMouArAs?n(OFEEK6@xT*R&mah3s<3Qnk*gbUBDJkthDpYy1H*j zXf|1!vp>Y6ek!Rq#BB$46gCM4qf%b_BJG*N0z`u-rn?BbA!`HYBl@%(w|;k;+!%7N z9nG~$EIzqRu&~3%=bBJF%5N20)sj*uUnqx@nRBWbDqFS#kU76uj=wd%Cs9zaplwLtV={uO2E2j z&;);PJSv7)x+XsN>e5759479S$Jg8iHssHkC;02{DTK1m10`k#K+P>pO%9kD$*qou zwghwMPUKAFPUKDGPZUhB919$<$I6*Skz<%fT^Szg3HL`B5oz+|4MX;TJcePU+tXX| zDuY|wt1Y$h$J|#(e2kD?sGmSOZN4x7Wy@2Xwc%2PK4g>91UAqmn=0eoT0m3mZGM`oH-KgJ7<_c27_4+{jM znb2%#E_C{FVdO{WFSLsfSW<&mM-^~lXC!@X@+Jx=iYAIDN+wv31rOL`OpF8yUy?)p znZn9}d7zv^ZCZ2|S}O9$kLLT^aNLD7rqI^6-M!!BOR3vH1cu~}xxsl`)>@@=ho6&= z@=O^XeAvBXwEC$3?nmXHFEs}mZ3ayW`f|Ylr4wZn%we3x z=`C#O8y2V+8pRYbzrA5|p(8duF}wN#)^|ldg+4_- z#XhMz1^duK;EbLs>+&4Z^GdX({?8JXjW>lZDwFO}>!@pX!OMi+b(pal<>eiyvzSi))JYxzxe|aN4UU+509K5REP&xI0h<3_J{iH7i?f2ta zx*tTw4Ei$|^P{dQ7Qcr*yi)u93s~~pxth?O&GA{&Ni@sRKBZBg9+fetD3H$`M`0RD4~$Lbzqxr zC+9jNJ>nIAJR$`cp%?Kysz^#&I_Oq`53R4zpjC$qL1rSG>&#i#_MxtY7=sz5X%`Ss z+eF(yjLrSre$spR4A?fFv(L}+z>#BKZ32$THooU(GJN9v=bKi4nm)WTqdTKNGrKbE zS*suJoy*ZFL^%t)jwe-9=Ei}6UR>%o8ZHoJL;IjNv$mItvf>d((=;LtX}voO@u5cr zf^a;O{UY%f4x(EU4B?*|ceXcu#f&)y?ALV!Sh8)9Rz)^`qXfvr`tI$ZS--py{?)UE*7fDpt=WT;2m54M(L+=btl7vn>> z-b*p4=AHK%FwxO+IKdsU2Mvsr2d{oTseTLLi-<%dy?)w>FR>3(7)6=^F9jaT_Foz_2R&5N-TGOJec9k~1Y&uvT*#McbFl{O^r5`h&%~GNnEhEhBSt!HtSK~bgj|`DGxxz z*0++YKc>mKbv-_vc^K0`co1(LKrZMFnCV8lC0CFCXpPBp$EzL^KplQ=ECoApy*bDq zNm~VW*{xxhGkc;~qGaML-%kdlAaX(Sl}g*DHMzr&5mE?MgyHMgvo*uefk4YlVicgi zS(UYk;pX-|7>gWf3D$HE<=43GY=PqU#s+)9#(H_w_lrH`;??@7yOrEwKdOlDE|BKU zFL)_()bA4gc$LBgaP-()7WsOc0eOdp@Tsp!) z#ST;b`R+W`Xqn|Y_OSL&cX<&%(ar{T@H{_!)oG^oZN?>Y$$Y0eJj{ z-|Zwp8FfkV)8f(&1Bv*r;;bnirs^IikVy=v!<4Jj@O^Kbkow%CJ*z;kz0wau7C$K| z$4J>@o)iN;@cjL2AcaS!?7}b5f$}zP$cry(spmr$+S2KT;dXshCbhc~T2(ddZ^@2n< z*?Kp1?1hry@)kpdlQxSfOO0!jKEvOQhBEb*GPhC5uAc@LB~THlwtP#{64Pt7lO;>m z3MbtglIjmx7z}N@+9dRA2t-ymJAqAmlwZL^cDGQ_*;P;N;gYVBpC$VWPkzC#1MQM7 z;5X6>$+_=dW3@#7M4`ky+4=)+a^$p2%o%$2oe?U$kz2<3Fuwg>9!+W1>h4-3q{yJF z_pq@T0IaiGJ_iIp56~#$e>G^}f!6)egGx|7wcu{S_RK0v`E=USmU;ZP6+7y1T5ko| z8j1r=+rV~XebfU>eirzP8NbX~e8^?^jZaioYszWu*LPp3V5w{|&Xc{~DizV-k8+kHyYIazzwn&uTvtwY0PXaQw?-n56DCdE%zv=Ufg=32YSy)6-c0#pzUFLx;N=zM5V~q~5`P(rJ}VD9db=SZAbt~{$)_v(;)9<@Iav~Q6F-}?iX-EW$b7W+{&dop z!&x9vVpRH^ffgH@uu?R%Qin2!bLcJw86?)h%EerM9WNSuRnBd06#?Jp;sV(qTAcEn zB6JoURywv%JV87i!+nKh4ZLIExx04_Vch`tP>2ddGoriiIf}IO;Cj>d(xJcxnn`D_ z(30%E_`t0ES+95t3S;^QjP&D`RvS$|Aw|({0jAK0H@r4WHc^GpSgz1mO-p`y#58XYUh@?C$PH zc)S^Z5+D_9vUVl##F}qzK@~jFB#~g=egQdlZz9$)D|qFVPy6#ttd0TMXQmsgAEPs6 zFva1_3@G789e(P`cVx-UZZpk)hqelQtvY-OXW{(A?aBBAPN6sZTMN#Yz8;D`kR-Do z4Ny7R{vxAVpYL~t&IIFsY`wkqTyS2zw%gS#t>qnZDj0djI%1&1KCwTaPNhaM>Y5xO z^!9*majnEEQ`dAPRH z|9b@QPiw`IsM0GrW}CUI&iMuXP1Gf5;)OxmV+WF<)q zX&S|_F7MKJVo_~pQO#HCC_$qLvC%yfDs8wSb#1mG*Q#QM}dL zaGq#t1p>N3rPbqcfwppF3oc6_!|W+=BC?QXbgO zdqM|eE~@_1jykFNfrr1dB@!3p|%~ z)O4y-+W6MA0w8S8Q?p>By3bE_C(x_U@2y@XSFLrA$z7kt?H5a<#Bp;PCb~sCp;_&; zS7oeG;>xgBj~mjepf7M^xMDf-vcvKdneLZ9HpwoepkHryf5zu%&RSoi6P9Ss&>Jus zcp^AV^I7uj6SI1=b*8if-U8;aJ8f_&PrxVN!Hw6(vIF+&A;6}2P48^a?*KlR(%d;C z6fI^pRP49rTXn~o%YrSCoy(cq`*XGfORnNW)fQ^6#!EDkQ6Vu!h5554XOZ)HpJ<=U z0N1RX@`g)S7lm5Lh3UDo(NPW=TjsP-m1kL#e#Igo7eWq=Pd?<3APhHy5GsC#|M0ePfMTTknM*voGRTlklC8*&- ztU_UDyMJLCmFK# zeR`oy2#9R)Qtey9+luZdYIDST-B6iQnt@m6^6Cjy`A0$>KSJ$BYEG#X`md%8n&=nM z;(VxgkXE$$Dd@DVua*a+2>OH)HS%7Uo*yp1qB#R$QUGrGhwnV0BhqyXc4tQrJ-ivN zLU_g13r&U3rb9y2ewblbuLOh?lnmB5X-yeU?RVC02b%)B{l>g3bZK}0{`7?s%XXif zb1r@GiO?r#z2!8!wvVf=P1;lg*6MAc4NH5aOVCWnX+OcK;%j%rdzyQ({7C{~d-YTP zd@y0Vux?5>G=6K@e4`m7_BgP-Z(J$M$^tWEZsnvhT-u_qE8ZltaGd%zfPME z9pC!CKlG)KeTg}YB6e-VLg-4cSf9^Q<&KslS(DuFMhUJ6%pB{-!kqh()mE?d@k$5p zMfSw1wG0JrRC;Ze=~k`^^>go>6y+B86Ax9FTb${s-O)b;NtvC|$ptE7%NTygMrE4l znYN{#%Kbx}z_Ts=Mt7KNK55dHx6*8)a#_=%FWy7yg=@p&BcKr~1$@5$+<&b(hxk~$rNA*v{lnST28^z**dPTQQ=qLnUZ8j1`bIhfe+oPuH~8bdiDqGNy9LQjJ&^}bVIz$o=OMOE$IhOh-?HVHAS zg5}7f=&oMF;1m(ivY_V2MuRrUMf6?$U_9)5l1u5pQ)BApE$n=>M|~#OOr^qh-EhUd z(fR*__&XzY=l*CH3JbBad83rRPmGP;_PzL^VqSK`t!b`aSLlWv7G*v>sJK&)9qV_w zt*n{r-NsHciD5%~L`U^NL~jL@_5s85L%3(B@pQ@Zy^{pWfm%!=<;p59zfA)BWy2n8 znmmRF2h8;X^E`0IA$CWIlr&!zAXD@SCAzM`q2=an4O0Pz87*UL6Q8%-AC+OH+AuO0 zcLPh$J*>Tb^|Y1YbV8ZMaQ#b!1Z;MV)A z(l$b?9TJt0alk3sW)0%BZI^4x+!C7EN;50}Ee^IWRsyRyz4%R6Os5z1C#A;Qz$sE= z*7g{IRb5$imt|$3N8j!*GnqtE&Byf~%-?lK@!TE${9zzbcU5;^JxAF`f?2F@&hIQU zw`eoiQsrj3Y%BFZZDSd#G|*_ZZEW6>y1Td03pgQLy_d6vL!7)(L5-@*(wO398yPZR zz)r)>t=$H-je`Lb@fJhoZA}$@6U~-X?-4lv=wN^}moCJ-a{-u;0`^yd;s0hFR{Ydz zqjs%acc=$u70LIt`zc;8EZ_tdDSpRw9fzXcuXtaoTUtaRYfRZ%^h_=iZQP=XA|C!0 zR76Xh>9?hwHx{f!e{}(t$>cfzd(a8G{_Y8y``wW@?i<3=-7S%-+m;XT2yb)pIGBD@fvGzC z$7KNe_u^InyhN_Qa%MR|#V(=DN6tmHV(1p3`MvqDxmRV3T1CrVkKvI?jaS>TYSTN4 z{@JrD)hW|u1}6nJ{j=5Wtr;~Sm1_~gQnC9lW&pyi zB)aNnP*a-U7PG9VDroH*nG<~LCL-~S1ku~4BL0K)nWh7CG;y0AQ;c9qH6||5rl74g zG<0<p>})!JzZ3NNbwT&%UXWh^N=oN{At?fk`;lPp_=GG zn=P+U^zxPM@e{S>z3x1-4N04r<~e_YEiYr@)x@uk4t#LU<~FpQ=ntWTJH!mx=3|dp zZ3|W(`Sj?Emt$3TsnWOiLPIHGGmW!&M8K!(oG-`a8t2~J4=>5w3##bS#ibO=8%4Dj zr-H|>D!8H-tX|ZumJQse2pU;>HWA;Qn2nk6tWAGjBVttiK5WbK<&BF^)%fahb*8}- zVw-L#(~q}$3GW=S^I3ELu^~|yEM^Y1&_+WI%B*jaljLw1EOHq&d;Z3ouwuGKRl50M zkBras6=mK-=zA7>wx)g6?w$%3qf(;OM*^L_slEBD$j*civ|msEgckv9xn!%#Q^2;Y z4j@XdzG*S@?flf895J zb+uMEbXX$~c&LBvvO}_sBZOkjyfee3hc&T^~%ES_A0 z%`EBLzKmUds7E)-AJ-Np9#p&+_saU3--7~jUAnpj${*gmvAo};-noDaFptf__f0i=N6 zCjcMlj`$>WPa~mOk=z**YX7QoC|Svisv9t(V(wnLf-Bc3YBr{%oCrR^p4frz5W+vX zFkr7c;?&YNgEQw9CxkBaG!9-Z(RuiRx|deA+Kbx?5GT`?ED3f}b+|!OugWrWoyWtc zn>-Id&Ss%d?9%&_9q(yPdTFjS_Ri=I=dzG*U9vpoz>Ve0XL45fvoHYa{8l?AzIF&; zIZr*Iql|sXF3sSrr{q&RIs=>~PfE*cG^Pzqf;U&>q#J+TM>F*m(LMWj+ne)70*C4v zIqsS7g^oes&}1chLxA&m;%5jn?Us{X#4CAEwjZ0n`tXOTzgaz#WjOpy(A>ejv|Q_5 zfN$qWLq8Y0m3z7w>`lJjnX2PSjp_#H?q~egd6<>4!1-d}<*N%ElHUpFT`R`1NP1UKFz+jz1gxeJQA`w>&wwT2)yb8iJ#2bjw3t3+1=STc=@XG z?&{2r1c_Hmpyn_7311MRSuvV zgD$tqCdXvhY{BbV;F@bYoMV6c&=6#pJ*l63uK)8W9xiSnZl?~KJUs-au?E62o`x#R z2taOfM`p(j%loWo&&GvFa&O-5mfzV05JZD$4x?iofH)=Kz4RUh2N1`NC9>~cyr8&` zc%b+Lm%ZZ1Xpgbrm9I|2O43|iu)L&G zU-qkQy|iH*Fq+by(wW-R4o=}|7X?^utpA=W1X;k-#FlNW4P$j=$BT}nj%y9O_~8zN z<<|>6$D60~;#FVkhYcZY5gv$;urnr6IT8SvR+MgD%O#IC)YdN#S^+FaNnBstOx$Xg z8%g3`mQh{ol9@efoZg||uu;T>HIB+L8L)>1@!23ZXKU#Ze;Y<*YzN+V*gg3Zsr_w~ zq0;Ymk$QPnl!=mwKZ6E>6>#6JyHyOd9SMo=2lt`Vr=z$&>%!0 zPup0I(SEo~++^MOUkxF&U7HvnagUU137&b9&Mll^5D(tjZOH@ts9 z?J!O-B|Z>MSiZNtK9lFxOjdaiV32ZFSX?G_z0>aJ^E;ELkR4m=iw3K6qj{kWQQqN= z+2zOgPBE&B4DjD1;d2}(+n@RFeR{>=l7j=G^s29}uQv}|tA*u1LH$9INoLgHAixJ4 z#Yt^5Rf$rZ*QK_-u)kr~+LMll9g!WEMZJ094&50_j{OD-Q}M`8&r7JqP7m^FP7(Out71Bb}m*POW^!gBr9%?PDmZe0%T?K$E7^+$vt_8zVuK%pKY73DjD{i7WN{(Z9b( z32A={!DnBbo=Uc>(Kk5(uY|Y2d*I{h&y1sZgn%3C3psh~W2?`V)5>E3;*t_*O|(AR3~iMP@?@7hNvyo^Dcb6|!f7k&8_0(yVG?v$F&@ag^@A7V(lKn@}|lD8Nfv;~sXpuXfi$1W^FQC>>; zIrkyi2JW1V@g3SY$(js0Y?ZXEZa;eW4TG=Lod^!}Zz$@U$GkE(I^30^)Gwv==ub=& zIR-P1ngVt)tnnU5-s$@F{e2#s3PRBl>oaOQE?41n9wk|mGDsr>xgNEA2+{p|TyQkCqM`d|b-!Wn%VqtfGq3cYF$;vE zccb^Cd0|+SsE7w(NldnuD?{b-bmHZQ?^MMqt$exl8N}*u30)it0ZXgXX5D5xqdt>s z%g008TI{Lyv6rwRy@*%a{$dSc(uGbvnbRQDs!-YZJ34sRZ1x0ZqGCeLr7xcp1Sr8Tk)2J zBuOhrA+0>*Pvkz-8PpZjopf3QMqk;nC)0iA9SGHv8-W^h1R(-P5CM3URy)&`3MMK+ zx<{#_tWgxH_n4doC@+yfaf&Jv9yn!qY0Lh4s;z(Z21q6;`k7aJsqJ!`UfWN#?FqHq zTVFa_digG0ez13nAxCB!xhAvk$;M;34ohcqY68)~6#d$OqQ$ z>7DQIB6|1V9p4zN%^lW617kT2WKza$u>4hzs5D5Gi2+eF3Aj958*UDFbiu2@7tgP8 zPJU(u`VcP#i$2b#8;7d|B|-_MadV2DuM;rlMtg zBa&qoMVyA|8-ID>7;-S%uA#^f%_nE)3EaHt|CU&yMP=n=p}f4kA%c<8<$Gw~CgAsk z#Rrdl@_?LPJXo>V zoSmcQ)6-m-j)CowHG0{q<(-7{3Wd;l=T+;W=|>`Q*?Abd2mR~L9qqo3L++Vdmot_Rt#@#`S>Ep6Gg8upI zpNv9oO5sSk#bp%0O^5BZHnlVeR(HzuKZP@TjjL&1_{i@Ftmrh*h%!4H#Fkv$9pZVF z!WmM1K&)*U%Gt)UXb`iC4G@$G*^ZUBGPlW(V6pIa&abc@omv z8>QXy%{pkI)e(3#j^w_AmFw%-vjku2`(he?+qtCzM=5m%Ia)?XC@+O}*4j+I4G6h_ z8i$IAlC0xF7^k-+VZ`6qpmdS#if$^3t2R!+MgJ3$-m8?S5xxo+S3 zjnw3zl(qUF(GqkGY&Wnhthn~=LEhY*gwsZG8xN@#$smPh$O4-@W-(tmi(heW-~eXV zSsB|jhEF|M&#p%Q#pZ!n z_291Y04jzg==I-I12^*h=Eqb5?A)VjbcPFv^~;Q5m6uT|AudGe`3+ORfXWn)>R3}e(#DHNeVJLE`lFLK?6@hq=;_4%eV2lh1(>%j znPeg!LRrOZ#XJMF-<8t0H`GS&_CA5i^E5e>K7gc?wl1_XQHps;-@bqOS2mlw;?gar zOnSERxrone*z6z^%{-(}K%$TV?qfaA7HY#xhU!PXLy$kdFND{Ug@rZ7QW0hQn?6bh za&aUTd1Efr9nz5}xp9_|(vlvcizV6FRS60x#pj6jTiZ^0Q+eG?+(I)t@nIIQf?_ z_m9q9vNuDSWU>^B$q)li8PT)S)$LY(5W^`k%)fONVc ztGfIBU60+25vwnOq@o?m$AdBIA-1F$1$f%!g`4zPB-=G<@3^Y%MXx^}DAna13hEta zMZdoQRTOX@_OnS8Qm?NO54DC}VSU1vXdW+icmsD3j+!kBtX#{V3TS+;%{T!H3pWy% z8;H2j1E(zIVp>N<;D4wyB!-{oG}#t4reu_8;fn@!c)z%gvbHkgTyqvba2Y+x%sWP9 zENmE!PO@NoupDWK{YddoTMDUQCV2LqL`I$-D=oo26sr7fq;`p7JxyoJ(OzEm2*cpV zlW_b?F&IvLx>g=nfn7M8mY<0yk7;7^Rn3h(vld7xLu1WfRGiT{# zK&+QBN3D0Ksi@r+uaoP;n(skBGQ%Uv3RTCSs9ULS2%F5PctXp_eUupP*vY<>H&?y< z@UopqPL;$S^m@RmVpb$iAj$F4bmNz2tB_tE?kcWLU5o1tpG`#UWd*8!fz$`?9kW_EXV+{v)l1>vU&P^qW%}(HYHfrHHn`e#^zKZkv(@S*^px!U*wB+-9 zQ0s=Ho%>Me#JZ=~-Ap_4@?MgnhSu_LTzmX3pWj|VwN*2p?Uini&rU9t`^<7)yrmpj zHAWA(Yi(m<5GB8O<~Gg+Gni+$Q+d(GU^}iZJ~hXj#Na`p z;G{s;T-%NC4Uvh~(iJob3Z4`Juy?4>%t9|3g&v;2s?PtljhWM_F1UHY&!YkSfzCs-<8t@v}> zG-uGz@}dcHmqB$5fP5_eRE$~GnHwvZ7B%O~9GCk2DEsl5;$}o@rwEkN@2niGL`+6v zLpCtk8^`xQ1b53z8p%-&7wF|uU7%zxM<&G1xEU{^AHwT(|J3WGnK5CAd7LlqNfbAe zsKj8)i_hc}RcNqW>D3K}O9>Kzy9{m{pl;gkcBGemW!IY~{WoZ=h!$eCu&tOv9MQG}|uQMzlQjMKm zr@WcC$ol-4oYo(;x5ce-QdRq4509H%H(&3TfcPNF6Y(@(s-9KM(lEL6<@!vI=cg05 zbK3_5tck)i^m@1#nHpD2-HPu&Y5v(OslZ>s(~su&GdMNsU?RV^@pZ;{qwWMV%$r-|iepkGW64r}PE^t7+H4X1Y#aw+k z;AK)gaL(&{ANFv6z}|tv9>dJgN08El<*VCq+W47(n#p@&PjDW%3KSwCGdX#%DSuE8 z0JJwtbnTvEyO{N7`M)0|!3q5S+gp(R0d^t6gb1f-q@QX@L@b6V5*Fp^!zlMN28&Ai zcugh!5&|nuH4PeA7O_2(x{S*%KTF=N*CJ8QmN+C;E9t*|bKk?uS(1dk{!kqjY|oQ- z=^6z>hCGaNVn6Gjmi{V|&P(HFvC%J8Tet63u}EM`kW$BSl6QZ)S9j8waki)@TXp_V zQ&^ZfS?3t-+Y2XX6+u@o0)&cdH8Y`A2X*V`u9CR_ON-m!>{Mt~`{R<_-<$cD|J~jH{C%lb51l3Vy;yo zH@!L*^p$h*;XrH6<-ZKw!+yNlV-&7zvG4CXy~)*z>4M6NXQH99H}GM=V>19eQ03El z5fTbm7>wI%((*53VFMjXfh%{EEsp+eAEsSlQ}11Xmf-j^kK0#3ZsO$7aY!ct;yxlh zK@yRnvIV_d4+d+$X0rRh!M~5*0iay3q**jwU4jBN2}(N_DiQ=^KH)+AYs|<)l9snQ z6u-fgEiU{Swf!lMP#WlN@>jYvwFK*dnFQ@%Pjim%KmL~!?Z(gkJyt&H!JfY?Ie-4v z$9*#1lb4k(uKexq5o6JPr)Ph=6QNithi(V``|cYMH@yNf3egf$e<@d&%m7uNz7=(U zM-3MTl*6<~__StX*cJ=>@GYm^&1LbDkVJ z|92_V$pMC=c~$%V^on{y8UC+R>*`ag>1BZWx5*@~v9{uxRe)*GHJ+Hr< zIFBlm{GYd~s;ZFxzMK1cOuY54C`ALgy&1{Q?>N%|Rd<4qnCZ7OZp42?P0yM*bpGFu z>kYgsx%jYyUeygA?R7UrHoQ3hckitDS4TgmO8B>`7r{U&r-7S}rX6p&%R;$Ze@arPuYt^c+8@$(dxE`C0g3ra&&d zGsxU`&RSqrv9P(doS`q>G$2KLBIrZKQ-dvAtZLOxHbtFKQE*`mw^S+u(lcWWl^Fw< z-~h8$?D2GNk}P?nQ!!SRD!5wt39*N8KEf13v>3qaN;pl>=db>F1mwh<+%q#qq;NBT zG|AHrZoVLRtQF-?XvzIWzMpMnBsW(NUWy?-IfsQXQm~BGKE=ELjPlGiyc?Bh!1@f= zQb=_y8#J!XyjMG^}nl*{RSV-SJ~AzmxWp!v}(230}DO62kML9Q>~!&~{~htjVA! zQ{^)hh{XBL3)0H`C5aOOnVFSD^0H<8V9~NGEkLL#YmE2u4@MhoiRG6?yGl+=>D9Ex zNJH>W0Yh23CB!rG%ilTMb7Aob6cYN(%xirka^I1gzGMC~qKe{Y#YHKWFVEfjn4+6- zDis_`KX;`o^qjf&@>sy=^OO2rzo$AsgAxY^2=R6f2D#`&kRv`rUJpRqRxUdSTUHKM zuJ&#&gIxO!i{YHvUSBfGqe-;VQKP708HveA`;qt6e8M)N4PP=>O))Q?b^x3NDi=f&N2f@>{5${gPJlfzkRWbXT_~>~pkv`lF(s+#7 zQXT)-K~%|d!_Z*==Rr&ZIHOu3u5wWKR=fN-B=(uUudjj9KO9tG+69XL+@aTjpmfnH zfi>3~d@;e=O3tC!@u5(G-2(dscm;$evotcsRpeF4A!x5eOF7zL&%G36r<;N9*78Pz zhO;u+M_s~c5!xWx1Ll>s?u7u3jnyj&Ljg=WZF9b5T$QzV0q@nj(CgK=(C4KYBBoeM zEp=-`h8tqgUaSdoO1i7_1JtE!7oOsX18L$WG3716X+uZgJhJ;0zlDk3%T$HB zEb^6RtNU}MUvK`bG8$;&#*!7u_goOR0xb~wtjfq8gBvJylT4$lhk{(IsC|60?LWaw zg-rDK|7X=6cZ5f*M(xlbWKF`}^#@C}f$amTS$6w?ph%jM&-51^JS&8ZL&b9NF?HKs zOwBTVjxSXKPFEb0p?yn0C7{}WeJXiYyD|a3NKY`F=o=7#rxldo|mH5Vr z|F0_NeuwqC_S20=g!d?ln2Pot`t;=Rxp!x-zy2VlbqKkI5^P?G^}IC0yeIX3p|k!Be$j^j-5r1;FnsR6>0#>wj39oJ46>Ln&FtOttV4A4Z^!k8R2ERWWZmE3=Lu!c z0C%@pJtTase)=lG9nI zlZVq26oZie%ujIn_vQOuApdK?wwV2W{&3*f65R1os#OEznwYyhzFo7;O*@xmdL#E} zHCA$IjU|ocjeBCH&QUl%YL;HpXe~Fml^Qu03}l#sda&?BwS+5stZ=MoY*1ZA>_tD8JeoikGu;E

v4IOy6ePIk!P;6e+Z+gS7iplHX*IpRNCzHZkx#U@;CKBP6Yd zSAIA6xd%2_bR*UcihHhoQB6fJt!=4d?$^-(QB*H+YfwW}jzai{t3V{=2s3;#h2nX8 z3Z;mxnygC}@;LORwgwJ7y#ii%A3%8q-CGQ15q@BO#p3wTT(aZcby#N9)-2x2y64ZL z#nd&yN#)>%x*F~>6NotB?|+6zCr*AeWF2 zJeT94gPLHZOU3@1H8FpjqZDzUXrr#8C8PDvoLpH0j7FwUcIa;zYiFhztyM{pXbNjy zZJtQhM8}wDq6He`3K)2Rz7bx$Wsmm~&8y)p|6hid+T7~5bH+k9_yhM%OuV3 z%B#ARIO3i@rri^i33E>Pzy=nocOxq%2)-l$O1j7<+w;HU7iB*N!<5R;>IX0U9##Oo?Bqb*#HEnfdcgc$@2hmr@lFl_iSV@Hb zdR1|WczM(#S#)rPXk+$^c%TCcm{5)j@;GI?*Jyc)$|-mQIli>5oD)^kCuOu=<7Uxq z3aPHTy1E07c8)yj5v!jHG|tHtEJ_(iZUp108G)w%i}nYP!?j+cWiqNw52kuI;6DP$ zJIH<=-F0iljNE4tNgTTo9t2VKjCeZUjegVc8SZP1Z>|g)ebe-N$ENcezDta70gaj# zm;`ZrUYWsZp-q$4Uh*n;lspRL_ODuUL~pdFDJ|FQ>FtSJ=A__-r*MVtF}Qzo2)%uS z)=4`#tyQ{wql;Ah;50XzUiw+L4pLE5MV0tTU5B7CEZ2Ks}BbXO$JNUSm0XdSG&Tg>&Nkb+O#0VWXSdAmYv^5W!Bn+v=A}XGQUg+O0>j zj>$HNTEbJB72)X@6=b$k+8?l~MDAEye8=qF)USD2Bwf$KyRC2MroTFO@J=A-kulpR zS9pF(nWm<^q80CT^Pi)69O4PUFX8yAK$Eh;?BQJe!J(g)U8tE+)zeg z{!i(?K$^8oysn>N=ZDf^dHL9)M%BEkq<;ZDAi75gY(L=#P*>{ClFOL=i=*|8+(T&m z88Jgyd(o@NV^+D=Cp@pbr6I37dq04FS;E1rEB&dLZRWJy*=G9*Pu;~#QHdhUH1j;l z7%1~~@zC;{DyNK!(>4n{{p8m#O>yELm=71O*M)Qy(<5YA?X@cmWz(?+YLgq+Pl@2Q z)^bK&;~5;8PM(%^b%WV2^zuV3w|i`@!kkPhTx06E=;XIA#>~ai(CRh($(~uTX+NHH zn8?YWr0(8Td8^`{t;Y#l?LUm;tD2Pj9mapRv#fn8cq0gCI@0yyL9QP+RPd5oJnFFH z?eOa!hd8PIF=Gn0BYwqJ#Nuu#O8Sq7fdIXmakQBQ=|Fx86pmZ}gpvg1EZ&4zsz=NO zsk=Oo0Qh%}t|vvBsz2{fU-{wVrky=TynQxJ*4v(Vg#MCRyp69(_osvuCAY0-ex)B}vDw?xVcv~Pn3mJx`pz)Q z3Z+vr{+w|5hvyyB`q@Ft(`{je_tr+H%oVc-7rvwlYGVM7b>3LXD482iLc2eh_g+mM zdMAVDDsKtq)eYRVv+}!PQ5L6 zQ|sDMC(5N8M}9rkvS$zA4EVEOFHw?X_i*6?OV6By7UqRtu`X#NMb3=&CO9zu=_{J~ zyB_-|w=XIwbrW+&IaGxFYEXm?v73yseNa8Vy!4NS3*ICs8Mum;xjjP_nZXb|@-JJa z$7!xx`!O5CIK!w<0f7!INE+xB*6#%CxBxMtoEQa-w|_QDSm3=r?P9ihBGaqv`NNw| z)g9yo)C2DizBhk=a~+x1U#8@kwzw4Qu+oO>yS{(2CDHM_zNn6)m6ji=l|HU=GV)jD zH2jlgs~W@#v849GK|>q&hFk|StpPmtxxy@;Y+-iME#wyNgr11S^+(zbR5M@!^8#kb zp1h-(DB~x?<}k=)4n(=Mx~oVFh@HrWYv~1= zR_Se*b%~-7FZLkXod0D}1_Cr<6^Ef>QLyEycPG8TByOn0AOO#kvRhfFqp|B!e0{DE zf^bH7^AX!~qqFCRr6es!Bx11|#IlLRt(^xjq0BsLD*r{^Ox}h{o5TJ|$2#rTXz`7R zXK~#~4VaxBC`xAxs4a*i@KV{SqOGzo!-nG2$**deayn^}evQHT^yO9vumVIQCq*2a z*;k%3lWc^wJiO;Y{z(3OHB9*R3vjdj<^uw#1qSac0J3r={tHxq6w&G@AT4WC@U>!=;drmf7#|s+<@TTSC!cYG% zMUqB*xn46Xbf_|5am=Ba08vy0|o%g4bBcn3VU}{d(epQ_K$Dq(Bd$fCS~ONC+#l{v!fUc_+Sjhm*FB_nR8t z##rSw zBxjO4=@j8KdimJaj635t>a=D>410PORX)6V{7#pCv?;HGKgrkW2ck~Q0Bbkp>@UeR zb(lTNQN3kgbDrrVvq$ESD369A=BV^%f&s3+Dr<6{QBri<;Shxh5lbB`K<|3NaZvuV zkti|AkL7opwn@qYH!W%$(lL_d85Q@#`u4osMIw=UuUQ@5h2mK~JBv6?$_odJCmg1W z-?Bx37~wLSfC*m(fv>&+vEwjyNtYjXL1K(A*p5xP=Kn9e)w~2fO9Rrz$Qf_n_wlyO zbmQ|(WY31$K=a>_QOJiQ8L0h{jdt0 zCIhB^w`EZJ$TRPM_c*Qr@TU_v%FX+&>2$$F>vaDtht##vtJ82HNzN;`%(lC5uoLc1 zP*lZtT@BU-V>zLpAeKn3xVL1TS`Fl+b)#gBDO;}1d8p$*ytM9nB?|Kdqk2#ajq;Lj z???0efePJ)LfTG2clxE77eW%r7`5opxZYmpF9X#82rl76h{}iGKF5?r1>mECtZ!7W zRFQ@J+%zo^S+^$WeAiY0cepgFa10-VsU)mcwajED=o0r+YBaUu~ u8GI_2|F^TlVYl~RPA~uB7i@VrvoFSb*PXMi+%xDb!OZ0F{vxAqqyGo_7B^P_ literal 0 HcmV?d00001 diff --git a/_images/kernel-as-high.png b/_images/kernel-as-high.png new file mode 100644 index 0000000000000000000000000000000000000000..344d94d92d9bbfbba00084dce256b12be6f06221 GIT binary patch literal 34754 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 UBIb3p02A;rI%A@TJ7pXEZ$f14Y5)KL literal 0 HcmV?d00001 diff --git a/_images/kernel-as-low.png b/_images/kernel-as-low.png new file mode 100644 index 0000000000000000000000000000000000000000..8cf78a470e3dc17e243ce6d370cd3b92108a36a4 GIT binary patch literal 20344 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 JlG|3#{|DV!7lQx* literal 0 HcmV?d00001 diff --git a/_images/multiprogramming.png b/_images/multiprogramming.png new file mode 100644 index 0000000000000000000000000000000000000000..7e4df49003f5a1bf229db490ffa6f1c42aa42085 GIT binary patch literal 20854 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 CrjdF8 literal 0 HcmV?d00001 diff --git a/_images/satp.png b/_images/satp.png new file mode 100644 index 0000000000000000000000000000000000000000..33357b737a7fe4be045d3b6a085e9ee2323339f7 GIT binary patch literal 23634 zcmeFZWmuHm`!x(oC@InsB1k%bgmj34G)PH@5`uI$DBN^O4Gl^P(%m?84M-!MgLLN* z?;h{_7sva5-{;fwe0%0N)Inyh+1K9ZKF@Wowf2O(R+7fYd4Pk4hK4UIBdLmphS3L} zcih1Of6K8r!@xi2j;hix(257B*TD}M?8e28c15rX=u5dkyX ztNv66EWAV_LhuvCT_%#(M5G;ByU3!VDQl00_d@WkovqzNk*gn0r$;AFnwlCn4Fw)d z4D|ncK>bYr^^`vH|G;ylF@MvR>x=L1`yJ$pEk0NMSEuu~;?k7-HmP-+#SQ&qnugZ2 zK4)(a4+q5?80q6mBqg)!ea=_O9j7ev>XdVGv};mTr&XIIB*8EENbFnE zG&TH92gPq1f7!L+!*3Ic7&V_Q;UcqY<~%HQ&+&zp!+H8U$PKOL+}CIh7lR%q$0`Z+ zK&mI-A@cLm9V~-MI4v$ru5~PnOtA>4&F6fO)+uj+0A$ZJrfj^~3UfHGw}(*p*Mz^Q7`qX(_n$KtFdMgfEfY>G#*5WPR7!G4K0C z`H@5nuM(xm^?6m$An^Ltc5C@#EFvPJ=&9E$S1tw`ccI=lSEna;If-C&e)qsC&f}5( zO0jMJg~ehnNL=~LEalg^ZPPI{g+Vo(cm41$Vx^~j>@}&#QKPNNGCpsy!+stcC3ny5 zssyB=1;#K`>)qYceApKW|NefSL9487dit@wpZv0k43;rH-kF{DIvSEjoGnLj zQlZIL*a+rS=Xm%B2M5bovmE#cmlR<1z0tFm5~d|qL}1}j7+oKi_$ECmX&ff#Ou@Ka zT(?LDv%7TTfBAXb)O z-04RSFO{GBkv$D>X-~lmB%t{q=Uj;R26xVMQn62j|xMV!|i4lMG z%j*z6OHCBrdxl7{RXE+5oy{AT_|CftjWyFP;0MzZT&X8DZ@Z?kZ}dyk(A(5^FV%6t zBI5{a7HmJW;yMKPZp%$`%E(6??pZcBo<)v^t#|WsGURyC^f|H_yG^I*`lml`6IN!l zQMh^J=Rh4Ydb4sQYd!DzpYSaB+Fj4O&RgGb*t+8`1)%jCg1GrJm@hrZwG7`Kv3LR< zR081rR{~gNy8ufRirts1l=i{*=`u&ayxxx#Z0%091qU}rI`~fJ92rZDM)HyDcT6#! z-@b7HMv5o)!*l(RzE)Pfvp;zZ?8P#J->ip$BvCPx{5&_lJI*X+?QM969lU@#I zD_^`mbg!9psmN$UB)zAt*s0&FSI&thlC1i+f}eVcuM-h4BzlO-iSN^#q3*|HnsBg| zmJ_QL!7_|qAiW|=+Xx&p9!O#q4HxPv5&e4rAT%59XizTrHXV;wC?Ze6NmY zsf|x2jR`A|I}HuW_*jIrRukJZbrpZ_j|%+;muQ&2@7qI%kB@~wj?)$SOSb}_im%^h zt({oe$#H2GtiFqWr#3NBK1Qc{wjLtjF)V~>5AVl@_@Uh+kRGys{d;46LvGWnGitp` z;9{!@CnP-r@iO&r=DQ{*GKv3Yzum3*9ASC%=7-PK#V_Y$ggC3t@X_5)5?!mZ4y&-r zFZ^?EZN+_5?6C{%v}B!NADa(?o=|!eNs%IbKXnfxfXUp0srGbPqXH@~`+ldX9 zIlf$ecq22@`UDK*ubO{$={Cy-YAQgULY0^hMrf&ix1W;0BF0FwHuxi$`0we6Esn*z5I5<*$DgOPJJ)0#`+6=rsolC}*372m%H=elz!qUr z>T}Bo*8f#GLyRx+bXcCVPSDx5HG9y-?WZqM^oL2O5k>3fcC}^))as1>vt#q|rDko( zzp}x&!)ZmQTttPDTNT?8*Pz^d7}tJJBTS%n?{@J+uRRzdPED!ImN`3&@^<6?12<>> zSUK5bvHiU^!s#QoLDAF6)yjLz4a=Poj+T$xPAPof|Ni;aY~jbJ6Q)wn(;c(kI98ho zAy9v+`gWR54S3$Q-*LP?Uz-kp*PqB`gHIH_A#imxqL{#;-+v1iVx;Tnyfrzr)p$5y zHdSe16M+S?Oy0l8OasQercq@?nX8;(bNhD9j6)br3af~L+PzO`Y)!YZ@SbwET$Z07 zPZ-YLkM5O;6|0jlPNTAI9P-H3RNLr=45U zq(PnjH!OiC{cpl0O2tOMl*O-1PG%gH6_a@_K-9!C{nwM^ZLmiCb>?r}>- zQ}#)QD3Ny@^}p#)WkceJm%C zgTB|s1foZ;8)!G5(zvfgBQkC-^V0kR2_M=_nAXkHW^|$OkhvAe;Cv`hk|bcCnL=!P zakQQbV$n1Y)bKey=P`{xSZE|y(Q)1;e47RY`HAb9_w_`}wP~c-g}LL|V$ifVvw#M{ zW*%iXCn2~A^IodVs`g6>5(shh_{&vSW3~pVY*tpSlN+D2MQk3IEiE3K8G%D^{rWoi z1olHOBdDQ|`{8PWVZC5y2Z)QxD}|4p zC1Al06UDqQ5^*^je-^j*p_#BVMgYdXUXK)JcFMipoq8xgkK zTeu@0n?;&8`q#T_JYVL}v8j#5^&Ax&tUh6qm6Bo^kP(s9G_?RcaC~Und~R0bB7%d^ zc%$7B{@__JJf9k~uV5FT8cuCiybCbet$LW(BU#h3d=}f_0ji((|5-Nd`1vZe8uI8V z6=E1S_inZIbTUAzGpP=}tTr~c(~}|fzcXFxFAw@=^^7^{R_=qTO}Pl4P`26(q_VNI zq;v#DVtSXOyqlg!LBw=1cUhfYs@^qxbJlZz7A+?F zp7yb(B?Q-eLDx)arVBkzrZz8o}(`N9BisJ=;OSD2A7Q! zdR5^M^je+7@o>KB{<-!6FicRCn(gJo&sSoVx4gK=L2@4tr{HQG0HtzbTLg#i7AeUC zP>QkycWRfn#L8(-$Vogz(BEDSqzR`OGubs{vCD`F20J2V+fSjUQ0>kc`!1RtHB%O?H~JXk-agMU z*?a%GOT6)5GqtPo}zB?Uhw);!JaXRMX-dAPNd=;GabMXV$M>wh+(X9m?a<3jGXw4_B{ zUm;V#)#hzB0T@uYT999LwI17+%61u5;P02`Y#JqVsobkQ9@j;MV#5}9Mdt&{$py{Q z5|8<2Z)-EEW2Zo7VF;K6W8#gjwds?X%AL#{Et2|Mdl>vP2jRuID-7BBMx~JOPzrtA zC6gZcnAdZkA-2!udmc){xQS$$3wEx+El=AQcOSO-ujT1yU|Ym-IXU7H4B4IhdOvM# z`t_ip`tNvR(|U#eN;4M)L*PZ84lgG9?MlO8Ts>AB$Pab}p#eOcU5Qcb-wpC;A2HNm&y-voQz0AT3o7@B9Kef(J| zU*KRg5&CU=s>)H`?C7-RX0{N(c(B8N3n?bEJVR)Y8&NJD#5lz9KOUs|se16IL=1;~ z_f%6F-bf)clCYRqGw?@yK-jq3c&NnB?^&v_>bm{+V(XQLb73M9jR}x7Ztq%05~3NT z`Ea1;#jo)eXih+D|I3uO>qKsL;lL@#w2_^Or2vg%j6NroIm2uCI*qgWtQ8*8^)aEa zul)=cW=~!UGW0Z=)3w0UfF~Fra4WRUzXtR#!2D-Z%?ZUXORvQAbz!zA(~vO?5f<4` z7h4sB<0WmicXQH%Nmaf$^vj6DdS4HbK)%zmxn>7_FKM1~Xo%;Au*uDGr zP}XcJmO6zyQdPZqarG{=W0i3g1b_O!gFjnsL*kj~AwX~2Vq>xmVy6%AmhFB@VE-c1 zG#EEZ>fN3CJ{=~r{jJW3lE3WQVz6q$z*0AAkC%xWvN>LqV(mvAupiJJ>>nVJPo?*> z>N`MOJ8Efm=>3TpR?F$0JNAylb=Mc0+iJy+S)9(jn_s&xe8Qf&Z*1VP&WIchIfPNj z`Oq*Wn|V^d#A3@4^k7vds3v`!94Acmm1za(7#$t4Opxax&9Jv#zQ zX1|(S@!!=+RKEBv!_SE0Gr`KAImzx_&Ux-u-9*)sx!*;bAh1Z-@2oXwB^_2h!05Xd zpAeVT_{e?kBQ!=l7$B?|CB40{pfQm<({F=lW>SSLCQf1HAUu1aIoI3q+Ww|&EtVxV zob**$2tQM!pNcwWuYa6#p|!%ep(=M?p~wcSSJc#v6*Kr8Nqo!X7%JwfXnQG16eMxh zlFvev{90%xL?NNR?7V8FmTZazgd%&-Pc1rO&)2aKT)*9Bc8@8-Q<-^TUv4im{G5f` z9)>%J-_weHQtzy-j_Y%x9g9Z>3`JnrVMQ zh-LdiOzRcD)W+RwcI1EGFq6M&;{!JsCV8c@*l)f7u&Zw|acI4gkbR{PxD($1%h_A9 z#mE?aZC8XVcf}HK`Ckp2i*)&qvUKNA(_5nMF;_GSZl_u!4m0_t@V+- zX}_4~4$r)?UxXY&5sKZ-^1{uR^}D`oJAHfY>LByANxIouxMKf8ED3FX%!`?JX{aP2;jg% zTIX$}LSB52Q^jOsDJ2Ir^upQCmMtQt@_EiXm~B)W74cm&jZi<+vBj=OD_-2RSj1gv zUXRZBzN-QVZ{ojY6~uvIe}L7O`9D(G*UOc+6s8bj!p7^vjGM%}#7wo%%;H?Gj7s~X z0_5t_pK0YDG@SgM!nw7u($g_AQvdQdbt-#Y5mB##V8!qAgG1$K7aWKcpH!hx0X(0W z0J_@oW7)E#sTw5jfM!lD6up8gK5rso4q#KhVN{-Q7)Sj#N?!BNx(_1k$+Tl!)P9c;7izALq_kI8{e zUz#3(25O_?(w9~Y~l?&;>F5473I=(n*Z*8!ndd06Y?|E+e2`x$|< zgzGO;^3(h>5mb0Fe$OFRW!&FD->HBG!tnIvxjfnv`Tvxj$|)o53^8vj044Yu zEsN_qIT`@woJl>S(o3dOli9~$nBi|$N8iFNHTtAW057C`33Fvv3}t4yWY29nnd*aE zL(VF{&FH?nyynSOqmINbnNcc-KgMK3?o2F3h+pFekX(jilT%ak%6^i=3|h_6xx4>h zlXs1U9bk2T913^ky$3lOtmiN6KI+~3O|LKMjJ;=@SijebJDb{Bj@2jkKP$_&5oMdu zJ>E-@x6o(P=eWM zcj|{UGm77!STp@o0KaPVxwSLwF@M)6e~)9Yo^0(GLvB^)^yu0)nNDCTYn#JbKPFp= z&#YzSOGnYVcv^5<8eH|e`uRS&YrqaaO?2xsOho}>AJO`_d37q zFLyQM;JaB8kOt2+c~01(sIF>##~S;li^? zbpsZytZM0}y@PA6qJUg-=@)8AMI5U9cIY}#jSp-DVNi%-zntKcq~UMfx@cT$89T8C zWn=Yr^-l3V<35BieC|A>zKu;#X|bId#=&hpRavG_D>F}POl<&w(eVV|=F_LeH~n-C z*{<1e`54u?*Fv^m4rprNh}6HUm_qH2Vv=GV3G?;xqV|zG5RU)jAU7o@qZIDJ|?zpt6T8Hozdx>>&HqF zMp^*Iq~_|lEnpDPfgf<;dl!o#EiV!2Bmubo9&>KX5jINPGb{(9HUfT_>J-b2Dt8y) zzAA3X?e<13?%ur0t>$+tU|3YO&mP?KT3{E&VAYB;?r8Bc6GOkFb5bf=4VyQmsqTwq zM3g~9a%VI&^-bRxxSM)j-f=)N3}mCcxbJ88xRki{LGNj^$&(od`4p|%1>wZ(V+(sO zWYLqc>9Rfb>rgfz(rI}uHWx6W&yX}aUx(PgydO;GX=F+h2|OYzR)_*Ur_X!|dAiJL zbA#(t<73uU{2Z{~{%NdDTAgjLAC7sp%QuJ_XJGA8`YneOxqVGt`s$_gZwO#k|JF>3 zNyMz50)>@+QCX(*p1+}k4^h5Ped{vk{{ECzu#-EH+cXqO62;6>8E3I`$M1gjLTW&R z7HI$%)l#|wsT8an5mdQag07S616XxFU-O-bfP+Axv2Lo@5r1#}DbTAa6^)|$ipmQk zZTUmc?oKlE&is&H^#YsbgmwB)^QayOND-1%{4uqn|GZB~-6c#$2M1d9+=)mO)Zkg> zd<-l@aR63l@_GRa5}MukRj45TPtXhteY-3ciJzs90dg4oJ%4>}fwRw#Ud~Xn4g<&t z2ZE1=_<|=Rs}_?K2srn#{y)8e1&z9oCbZXvsOJT&oGcVwxWQ>%Rot$Y#CWMaq?fZmKI_zuVTP7HRXK>MUOtyJgw3pfyBzao_kF> zHt_fL_0@gIl}&4cO>&HqWQbJ(bMrX;Zz^n<1W+IrF8~1)+^z=xA3Ty@0L~0WT7jma zoW@86Fk6zaAe#{{JbuXDABMtUZ_r(6W_GU?2`R z@`0C2p!m3EfcIxl4Uf-iOpp`h6JYjzK&c3D0^zL%F+goSecOrw$TsDQv!$>G!JJu_ zX=^h;b@HP4@4Bt>zJ3X88k2ON3yT_caG?T5>sftgZ$M?fE-efc0ZVR{n`EYn&NeJZ zZTAD+4A4G2(zNc`+V4Ub`{~0VOHfH9=!^Rw1yR345!@LHo;`(MlCxa%NAy*;k-N~%0AJsH%33NEr-&C_V?zfBD6F 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 literal 0 HcmV?d00001 diff --git a/_images/sv39-pte.png b/_images/sv39-pte.png new file mode 100644 index 0000000000000000000000000000000000000000..7f693907429c67ad64a0f1e731fa76c7aa0348eb GIT binary patch literal 12849 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 JiJVEm{{Zb|GRpt} literal 0 HcmV?d00001 diff --git a/_images/sv39-va-pa.png b/_images/sv39-va-pa.png new file mode 100644 index 0000000000000000000000000000000000000000..daf34be5435ee5a65f4ba3cd911c3a3881974498 GIT binary patch literal 16067 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 literal 0 HcmV?d00001 diff --git a/_images/user-stack-cmdargs.png b/_images/user-stack-cmdargs.png new file mode 100644 index 0000000000000000000000000000000000000000..b707600a66d8a6dc5840d82806367f7e94a11553 GIT binary patch literal 16759 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 。 + + +.. note:: + + **基于github classroom的开发方式** + + 基于github classroom,可方便建立开发用的git repository,并可基于github的 codespace (在线版ubuntu +vscode)在线开发使用。整个开发环境仅仅需要一个网络浏览器。 + + 1. 在网络浏览器中用自己的 github id 登录 github.com + 2. 接收 `第一个实验练习 setup-env-run-os1 的github classroom在线邀请 `_ ,根据提示一路选择OK即可。 + 3. 完成第二步后,你的第一个实验练习 setup-env-run-os1 的 github repository 会被自动建立好,点击此github repository的链接,就可看到你要完成的第一个实验了。 + 4. 在你的第一个实验练习的网页的中上部可以看到一个醒目的 `code` 绿色按钮,点击后,可以进一步看到 `codespace` 标签和醒目的 `create codesapce on main` 绿色按钮。请点击这个绿色按钮,就可以进入到在线的ubuntu +vscode环境中 + 5. 再按照下面的环境安装提示在vscode的 `console` 中安装配置开发环境:rustc,qemu等工具。 + 6. **重要:** 在vscode的 `console` 中执行 `make setupclassroom_testX` (该命令仅执行一次,X的范围为 1-8)配置githubclassroom 自动评分功能。 + 7. 然后就可以基于在线vscode进行开发、运行、提交等完整的实验过程了。 + + 上述的3,4,5步不是必须的,你也可以线下本地开发。 + + +.. note:: + + **Docker 开发环境** + + 感谢 dinghao188 和张汉东老师帮忙配置好的 Docker 开发环境,进入 Docker 开发环境之后不需要任何软件工具链的安装和配置,可以直接将 tutorial 运行起来,目前应该仅支持将 tutorial 运行在 Qemu 模拟器上。 + + 使用方法如下(以 Ubuntu18.04 为例): + + 1. 通过 ``su`` 切换到管理员账户 ``root`` ; + 2. 在 ``rCore-Tutorial`` 根目录下 ``make docker`` 进入到 Docker 环境; + 3. 进入 Docker 之后,会发现当前处于根目录 ``/`` ,我们通过 ``cd mnt`` 将当前工作路径切换到 ``/mnt`` 目录; + 4. 通过 ``ls`` 可以发现 ``/mnt`` 目录下的内容和 ``rCore-Tutorial-v3`` 目录下的内容完全相同,接下来就可以在这个环境下运行 tutorial 了。例如 ``cd os && make run`` 。 + +使用 macOS 进行实验理论上也是可行的,但本章节仅介绍 Ubuntu 下的环境配置方案。 + +.. note:: + + 经初步测试,使用 M1 芯片的 macOS 也可以运行本实验的框架,即我们的实验对平台的要求不是很高。但我们仍建议同学配置 Ubuntu 环境,以避免未知的环境问题。 + +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 + +最好把 Rust 包管理器 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" + + +推荐 JetBrains Clion + Rust插件 或者 Visual Studio Code 搭配 rust-analyzer 和 RISC-V Support 插件 进行代码阅读和开发。 + +.. note:: + + * JetBrains Clion是付费商业软件,但对于学生和教师,只要在 JetBrains 网站注册账号,可以享受一定期限(半年左右)的免费使用的福利。 + * Visual Studio Code 是开源软件。 + * 当然,采用 VIM,Emacs 等传统的编辑器也是没有问题的。 + +Qemu 模拟器安装 +---------------------------------------- + +我们需要使用 Qemu 7.0.0 以上版本进行实验,为此,从源码手动编译安装 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 ninja-build + # 下载源码包 + # 如果下载速度过慢可以使用我们提供的百度网盘链接:https://pan.baidu.com/s/1z-iWIPjxjxbdFS2Qf-NKxQ + # 提取码 8woe + wget https://download.qemu.org/qemu-7.0.0.tar.xz + # 解压 + tar xvJf qemu-7.0.0.tar.xz + # 编译安装并配置 RISC-V 支持 + cd qemu-7.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 的依赖包可以从 `这里 `_ + 找到。请自行选择合适的编译器版本正常编译 Qemu。 + +之后我们可以在同目录下 ``sudo make install`` 将 Qemu 安装到 ``/usr/local/bin`` 目录下,但这样经常会引起 +冲突。个人来说更习惯的做法是,编辑 ``~/.bashrc`` 文件(如果使用的是默认的 ``bash`` 终端),在文件的末尾加入 +几行: + +.. code-block:: bash + + # 请注意,qemu-7.0.0 的父目录可以随着你的实际安装位置灵活调整 + export PATH=$PATH:/home/shinbokuow/Downloads/built/qemu-7.0.0 + export PATH=$PATH:/home/shinbokuow/Downloads/built/qemu-7.0.0/riscv64-softmmu + export PATH=$PATH:/home/shinbokuow/Downloads/built/qemu-7.0.0/riscv64-linux-user + +随后即可在当前终端 ``source ~/.bashrc`` 更新系统路径,或者直接重启一个新的终端。 + +确认 Qemu 的版本: + +.. code-block:: bash + + qemu-system-riscv64 --version + qemu-riscv64 --version + +试运行 rCore-Tutorial +------------------------------------------------------------ + +.. code-block:: bash + + $ git clone https://github.com/LearningOS/rust-based-os-comp2022.git + $ cd rust-based-os-comp2022 + $ make setupclassroom //注意:这一步很重要,是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次,以后一般就不用执行了,除非 .github/workflows/classroom.yml发生了变化。 + +我们先运行不需要处理用户代码的裸机操作系统 ``os1`` : + +.. code-block:: bash + + cd os1 + LOG=DEBUG make run + +如果你的环境配置正确,你应当会看到如下输出: + +.. code-block:: bash + + [rustsbi] RustSBI version 0.2.2, adapting to RISC-V SBI v1.0.0 + .______ __ __ _______.___________. _______..______ __ + | _ \ | | | | / | | / || _ \ | | + | |_) | | | | | | (----`---| |----`| (----`| |_) || | + | / | | | | \ \ | | \ \ | _ < | | + | |\ \----.| `--' |.----) | | | .----) | | |_) || | + | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| + [rustsbi] Implementation : RustSBI-QEMU Version 0.1.1 + [rustsbi] Platform Name : riscv-virtio,qemu + [rustsbi] Platform SMP : 1 + [rustsbi] Platform Memory : 0x80000000..0x88000000 + [rustsbi] Boot HART : 0 + [rustsbi] Device Tree Region : 0x87000000..0x87000ef2 + [rustsbi] Firmware Address : 0x80000000 + [rustsbi] Supervisor Address : 0x80200000 + [rustsbi] pmp01: 0x00000000..0x80000000 (-wr) + [rustsbi] pmp02: 0x80000000..0x80200000 (---) + [rustsbi] pmp03: 0x80200000..0x88000000 (xwr) + Hello, world! + [DEBUG] .rodata [0x80203000, 0x80205000) + [ INFO] .data [0x80205000, 0x80206000) + [ WARN] boot_stack [0x80206000, 0x80216000) + [ERROR] .bss [0x80216000, 0x80217000) + Panicked at src/main.rs:48 Shutdown machine! + +通常 rCore 会自动关闭 Qemu 。如果在某些情况下需要强制结束,可以先按下 ``Ctrl+A`` ,再按下 ``X`` 来退出 Qemu。 + +.. attention:: + + 请务必执行 ``make run``,这将为你安装一些上文没有提及的 Rust 包依赖。 + + 如果卡在了 + + .. code-block:: + + Updating git repository `https://github.com/rcore-os/riscv` + + 请通过更换 hosts 等方式解决科学上网问题,或者将 riscv 项目下载到本地,并修改 os/Cargo.toml 中的 riscv 包依赖路径 + + .. code-block:: + + [dependencies] + riscv = { path = "YOUR riscv PATH", features = ["inline-asm"] } + +恭喜你完成了实验环境的配置,可以开始阅读教程的正文部分了! + +GDB 调试支持* +------------------------------ + +.. attention:: + + 使用 GDB debug 并不是必须的,你可以暂时跳过本小节。 + + + +在 ``os`` 目录下 ``make debug`` 可以调试我们的内核,这需要安装终端复用工具 ``tmux`` ,还需要基于 riscv64 平台的 gdb 调试器 ``riscv64-unknown-elf-gdb`` 。该调试器包含在 riscv64 gcc 工具链中,工具链的预编译版本可以在如下链接处下载: + +- `Ubuntu 平台 `_ +- `macOS 平台 `_ +- `Windows 平台 `_ +- `CentOS 平台 `_ + +解压后在 ``bin`` 目录下即可找到 ``riscv64-unknown-elf-gdb`` 以及另外一些常用工具 ``objcopy/objdump/readelf`` 等。 diff --git a/_sources/appendix-a/index.rst.txt b/_sources/appendix-a/index.rst.txt new file mode 100644 index 0000000..23d27b2 --- /dev/null +++ b/_sources/appendix-a/index.rst.txt @@ -0,0 +1,56 @@ +附录 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 的规则来使用它们便可借助 +.. 编译器在编译期就解决很多潜在的内存不安全问题。 + +Rust编程相关 +-------------------------------- + +- `OS Tutorial Summer of Code 2020:Rust系统编程入门指导 `_ +- `Stanford 新开的一门很值得学习的 Rust 入门课程 `_ +- `一份简单的 Rust 入门介绍 `_ +- `《RustOS Guide》中的 Rust 介绍部分 `_ +- `一份简单的Rust宏编程新手指南 `_ + + +Rust系统编程pattern +--------------------------------- + +- `Arc> in Rust `_ +- `Understanding Closures in Rust `_ +- `Closures in Rust `_ \ No newline at end of file diff --git a/_sources/appendix-b/index.rst.txt b/_sources/appendix-b/index.rst.txt new file mode 100644 index 0000000..22d9e13 --- /dev/null +++ b/_sources/appendix-b/index.rst.txt @@ -0,0 +1,318 @@ +附录 B:常见工具的使用方法 +======================================== + +.. toctree:: + :hidden: + :maxdepth: 4 + + + +分析可执行文件 +------------------------ + +对于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 + @qemu-system-riscv64 \ + -machine virt \ + -nographic \ + -bios $(BOOTLOADER) \ + -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) + + + +qemu +^^^^^^^^^^^^^^^^^^^^^^^ + +注意其中高亮部分给出了传给 qemu 的参数。 + +- ``-machine`` 告诉 qemu 使用预设的硬件配置。在整个项目中我们将一直沿用该配置。 +- ``-bios`` 告诉 qemu 使用我们放在 ``bootloader`` 目录下的预编译版本作为 bootloader。 +- ``-device`` 则告诉 qemu 将二进制镜像加载到内存指定的位置。 + +可以先输入 Ctrl+A ,再输入 X 来退出 qemu 终端。 + +.. warning:: + + **FIXME:使用 GDB 跟踪 qemu 的运行状态** + +其他工具和文件格式说明的参考 +------------------------------------------------------- + +- `链接脚本(Linker Scripts)语法和规则解析(翻译自官方手册) `_ +- `Make 命令教程 `_ diff --git a/_sources/appendix-c/index.rst.txt b/_sources/appendix-c/index.rst.txt new file mode 100644 index 0000000..4a5d3ee --- /dev/null +++ b/_sources/appendix-c/index.rst.txt @@ -0,0 +1,18 @@ +附录 C:深入机器模式:RustSBI +================================================= + +.. toctree:: + :hidden: + :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/_sources/appendix-d/index.rst.txt b/_sources/appendix-d/index.rst.txt new file mode 100644 index 0000000..135613d --- /dev/null +++ b/_sources/appendix-d/index.rst.txt @@ -0,0 +1,32 @@ +附录 D:RISC-V相关信息 +================================================= + +RISCV汇编相关 +----------------------------------------------- + +- `RISC-V Assembly Programmer's Manual `_ +- `RISC-V Low-level Test Suits `_ +- `CoreMark®-PRO comprehensive, advanced processor benchmark `_ +- `riscv-tests的使用 `_ + +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/_sources/chapter1/0intro.rst.txt b/_sources/chapter1/0intro.rst.txt new file mode 100644 index 0000000..4298f90 --- /dev/null +++ b/_sources/chapter1/0intro.rst.txt @@ -0,0 +1,115 @@ +引言 +===================== + +本章导读 +-------------------------- + +大多数程序员的职业生涯都从 ``Hello, world!`` 开始。 + +.. code-block:: + + printf("Hello world!\n"); + cout << "Hello world!\n"; + print("Hello world!") + System.out.println("Hello world!"); + echo "Hello world!" + println!("Hello world!"); + +然而,要用几行代码向世界问好,并不像表面上那么简单。 +``Hello, world!`` 程序能够编译运行,靠的是以 **编译器** 为主的开发环境和以 **操作系统** 为主的执行环境。 + +在本章中,我们将抽丝剥茧,一步步让 ``Hello, world!`` 程序脱离其依赖的执行环境, +编写一个能打印 ``Hello, world!`` 的 OS。这趟旅途将让我们对应用程序及其执行环境有更深入的理解。 + +.. attention:: + 实验指导书存在的目的是帮助读者理解框架代码。 + + 为便于测试,完成编程实验时,请以框架代码为基础,不必跟着文档从零开始编写内核。 + +为了做到这一步,首先需要让程序不依赖于标准库, +并通过编译。 + +接下来要让脱离了标准库的程序能输出(即支持 ``println!``),这对程序的开发和调试至关重要。 +我们先在用户态下实现该功能,在 `此处 `_ 获取相关代码。 + +最后把程序移植到内核态,构建在裸机上支持输出的最小运行时环境。 + +实践体验 +--------------------------- + +.. note:: + + 基于github classroom的开发方式 + + 基于github classroom,可方便建立开发用的git repository,并可基于github的 codespace (在线版ubuntu +vscode)在线开发使用。整个开发环境仅仅需要一个网络浏览器。 + + 1. 在网络浏览器中用自己的 github id 登录 github.com + 2. 接收 `第一个实验练习 setup-env-run-os1 的github classroom在线邀请 `_ ,根据提示一路选择OK即可。 + 3. 完成第二步后,你的第一个实验练习 setup-env-run-os1 的 github repository 会被自动建立好,点击此github repository的链接,就可看到你要完成的第一个实验了。 + 4. 在你的第一个实验练习的网页的中上部可以看到一个醒目的 `code` 绿色按钮,点击后,可以进一步看到 `codespace` 标签和醒目的 `create codesapce on main` 绿色按钮。请点击这个绿色按钮,就可以进入到在线的ubuntu +vscode环境中 + 5. 再按照下面的环境安装提示在vscode的 `console` 中安装配置开发环境:rustc,qemu等工具。 + 6. 在vscode的 `console` 中执行 `make setupclassroom_test1` (该命令仅执行一次)配置githubclassroom 自动评分功能。 + 7. 然后就可以基于在线vscode进行开发、运行、提交等完整的实验过程了。 + + 上述的3,4,5步不是必须的,你也可以线下本地开发。 + + +本章一步步实现了支持打印字符串的简单操作系统。 + +获取本章代码: + +.. code-block:: console + + $ git clone https://github.com/LearningOS/rust-based-os-comp2022.git + $ cd rust-based-os-comp2022/ + $ make setupclassroom_test1 //注意:这一步很重要,是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次,以后一般就不用执行了,除非 .github/workflows/classroom.yml发生了变化。 + + + +运行本章代码,并设置日志级别为 ``TRACE``: + +.. code-block:: console + + $ cd os1 + $ make run LOG=TRACE + + +预期输出: + +.. figure:: color-demo.png + :align: center + +除了 ``Hello, world!`` 之外还有一些额外的信息,最后关机。 + +本章代码树 +------------------------------------------------ + + +.. code-block:: + + ├── bootloader (内核依赖的运行在 M 特权级的 SBI 实现,本项目中我们使用 RustSBI) + │   └── rustsbi-qemu.bin + ├── os + │   ├── Cargo.toml (cargo 项目配置文件) + │   ├── Makefile + │   └── src + │   ├── console.rs (将打印字符的 SBI 接口进一步封装实现更加强大的格式化输出) + │   ├── entry.asm (设置内核执行环境的的一段汇编代码) + │   ├── lang_items.rs (需要我们提供给 Rust 编译器的一些语义项,目前包含内核 panic 时的处理逻辑) + │   ├── linker.ld (控制内核内存布局的链接脚本以使内核运行在 qemu 虚拟机上) + │   ├── logging.rs (为本项目实现了日志功能) + │   ├── main.rs (内核主函数) + │   └── sbi.rs (封装底层 SBI 实现提供的 SBI 接口) + └── rust-toolchain (整个项目的工具链版本) + + cloc os + ------------------------------------------------------------------------------- + Language files blank comment code + ------------------------------------------------------------------------------- + Rust 5 25 6 155 + make 1 11 4 34 + Assembly 1 1 0 11 + TOML 1 2 1 7 + ------------------------------------------------------------------------------- + SUM: 8 39 11 207 + ------------------------------------------------------------------------------- \ No newline at end of file diff --git a/_sources/chapter1/1app-ee-platform.rst.txt b/_sources/chapter1/1app-ee-platform.rst.txt new file mode 100644 index 0000000..4ccd02b --- /dev/null +++ b/_sources/chapter1/1app-ee-platform.rst.txt @@ -0,0 +1,120 @@ +应用程序执行环境与平台支持 +================================================ + +.. toctree:: + :hidden: + :maxdepth: 5 + + +执行应用程序 +------------------------------- + +我们先从最简单的 Rust ``Hello, world`` 程序开始,用 Cargo 工具创建 Rust 项目。 + +.. code-block:: console + + $ cargo new os + +此时,项目的文件结构如下: + +.. code-block:: console + + $ tree os + os + ├── Cargo.toml + └── src + └── main.rs + + 1 directory, 2 files + +其中 ``Cargo.toml`` 中保存了项目的库依赖、作者信息等。 + +cargo 为我们准备好了 ``Hello world!`` 源代码: + +.. code-block:: rust + :linenos: + :caption: 最简单的 Rust 应用 + + fn main() { + println!("Hello, world!"); + } + +输入 ``cargo run`` 构建并运行项目: + +.. code-block:: console + + 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!`` ,但为了打印出 ``Hello, world!``,我们需要的不止几行源代码。 + +理解应用程序执行环境 +------------------------------- + +在现代通用操作系统(如 Linux)上运行应用程序,需要多层次的执行环境栈支持: + + +.. figure:: app-software-stack.png + :align: center + + 应用程序执行环境栈:图中的白色块自上而下表示各级执行环境,黑色块则表示相邻两层执行环境之间的接口。 + 下层作为上层的执行环境,支持上层代码运行。 + +我们的应用程序通过调用标准库或第三方库提供的接口,仅需少量源代码就能完成复杂的功能; +``Hello, world!`` 程序调用的 ``println!`` 宏就是由 Rust 标准库 std 和 GNU Libc 等提供的。 +这些库属于应用程序的 **执行环境** (Execution Environment),而它们的实现又依赖于操作系统提供的系统调用。 + +平台与目标三元组 +--------------------------------------- + +编译器在编译、链接得到可执行文件时需要知道,程序要在哪个 **平台** (Platform) 上运行, +**目标三元组** (Target Triplet) 描述了目标平台的 CPU 指令集、操作系统类型和标准运行时库。 + +我们研究一下现在 ``Hello, world!`` 程序的目标三元组是什么: + +.. code-block:: console + + $ rustc --version --verbose + rustc 1.61.0-nightly (68369a041 2022-02-22) + binary: rustc + commit-hash: 68369a041cea809a87e5bd80701da90e0e0a4799 + commit-date: 2022-02-22 + host: x86_64-unknown-linux-gnu + release: 1.61.0-nightly + LLVM version: 14.0.0 + +其中 host 一项表明默认目标平台是 ``x86_64-unknown-linux-gnu``, +CPU 架构是 x86_64,CPU 厂商是 unknown,操作系统是 linux,运行时库是 gnu libc。 + +接下来,我们希望把 ``Hello, world!`` 移植到 RICV 目标平台 ``riscv64gc-unknown-none-elf`` 上运行。 + +.. note:: + + ``riscv64gc-unknown-none-elf`` 的 CPU 架构是 riscv64gc,厂商是 unknown,操作系统是 none, + elf 表示没有标准的运行时库。没有任何系统调用的封装支持,但可以生成 ELF 格式的执行程序。 + 我们不选择有 linux-gnu 支持的 ``riscv64gc-unknown-linux-gnu``,是因为我们的目标是开发操作系统内核,而非在 linux 系统上运行的应用程序。 + +修改目标平台 +---------------------------------- + +将程序的目标平台换成 ``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 + + +报错的原因是目标平台上确实没有 Rust 标准库 std,也不存在任何受 OS 支持的系统调用。 +这样的平台被我们称为 **裸机平台** (bare-metal)。 + +幸运的是,除了 std 之外,Rust 还有一个不需要任何操作系统支持的核心库 core, +它包含了 Rust 语言相当一部分核心机制,可以满足本门课程的需求。 +有很多第三方库也不依赖标准库 std,而仅仅依赖核心库 core。 + +为了以裸机平台为目标编译程序,我们要将对标准库 std 的引用换成核心库 core。 \ No newline at end of file diff --git a/_sources/chapter1/2remove-std.rst.txt b/_sources/chapter1/2remove-std.rst.txt new file mode 100644 index 0000000..f70f4d8 --- /dev/null +++ b/_sources/chapter1/2remove-std.rst.txt @@ -0,0 +1,158 @@ +.. _term-remove-std: + +移除标准库依赖 +========================== + +.. toctree:: + :hidden: + :maxdepth: 5 + + +首先在 ``os`` 目录下新建 ``.cargo`` 目录,并在这个目录下创建 ``config`` 文件,输入如下内容: + +.. code-block:: toml + + # os/.cargo/config + [build] + target = "riscv64gc-unknown-none-elf" + + +这将使 cargo 工具在 os 目录下默认会使用 riscv64gc-unknown-none-elf 作为目标平台。 +这种编译器运行的平台(x86_64)与可执行文件运行的目标平台不同的情况,称为 **交叉编译** (Cross Compile)。 + +移除 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 的系统调用。 +无论如何,我们先将这行代码注释掉。 + + +提供语义项 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 + +标准库 std 提供了 Rust 错误处理函数 ``#[panic_handler]``,其大致功能是打印出错位置和原因并杀死当前应用。 +但核心库 core 并没有提供这项功能,得靠我们自己实现。 + +新建一个子模块 ``lang_items.rs``,在里面编写 panic 处理函数,通过标记 ``#[panic_handler]`` 告知编译器采用我们的实现: + +.. code-block:: rust + + // os/src/lang_items.rs + use core::panic::PanicInfo; + + #[panic_handler] + fn panic(_info: &PanicInfo) -> ! { + loop {} + } + +目前我们遇到错误什么都不做,只在原地 ``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`` 的语义项。 +``start`` 语义项代表了标准库 std 在执行应用程序之前需要进行的一些初始化工作。由于我们禁用了标准库,编译器也就找不到这项功能的实现了。 + +在 ``main.rs`` 的开头加入设置 ``#![no_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 {} + } + + +分析被移除标准库的程序 +----------------------------- + +我们可以通过一些工具来分析目前的程序: + +.. 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``。 +再通过 ``rust-objdump`` 工具把它反汇编,没有生成任何汇编代码。 +可见,这个二进制程序虽然合法,但它是一个空程序,原因是缺少了编译器规定的入口函数 ``_start`` 。 + +从下一节开始,我们将着手实现本节移除的、由用户态执行环境提供的功能。 + +.. note:: + + 本节内容部分参考自 `BlogOS 的相关章节 `_ 。 + diff --git a/_sources/chapter1/3mini-rt-usrland.rst.txt b/_sources/chapter1/3mini-rt-usrland.rst.txt new file mode 100644 index 0000000..b466f9d --- /dev/null +++ b/_sources/chapter1/3mini-rt-usrland.rst.txt @@ -0,0 +1,282 @@ +.. _term-print-userminienv: + +构建用户态执行环境 +================================= + +.. toctree:: + :hidden: + :maxdepth: 5 + +.. note:: + + 前三小节的用户态程序案例代码在 `此处 `_ 获取。 + + +用户态最小化执行环境 +---------------------------- + +执行环境初始化 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +首先我们要给 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 + + [反汇编导出汇编程序] + $ 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> + + +反汇编出的两条指令就是一个死循环, +这说明编译器生成的已经是一个合理的程序了。 +用 ``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 + +看起来是合法的执行程序。但如果我们执行它,会引发问题: + +.. code-block:: console + + $ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os + 段错误 (核心已转储) + +这个简单的程序导致 ``qemu-riscv64`` 崩溃了!为什么会这样? + +.. note:: + + QEMU有两种运行模式: + + ``User mode`` 模式,即用户态模拟,如 ``qemu-riscv64`` 程序, + 能够模拟不同处理器的用户态指令的执行,并可以直接解析ELF可执行文件, + 加载运行那些为不同处理器编译的用户级Linux应用程序。 + + ``System mode`` 模式,即系统态模式,如 ``qemu-system-riscv64`` 程序, + 能够模拟一个完整的基于不同CPU的硬件系统,包括处理器、内存及其他外部设备,支持运行完整的操作系统。 + + +目前的执行环境还缺了一个退出机制,我们需要操作系统提供的 ``exit`` 系统调用来退出程序。这里先给出代码: + +.. code-block:: rust + + // os/src/main.rs + + const SYSCALL_EXIT: usize = 93; + + fn syscall(id: usize, args: [usize; 3]) -> isize { + let mut ret; + unsafe { + core::arch::asm!( + "ecall", + inlateout("x10") args[0] => ret, + in("x11") args[1], + in("x12") args[2], + in("x17") id, + ); + } + 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`` 函数, +向操作系统发出了退出的系统调用请求,退出码为 ``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 + + [打印程序的返回值] + $ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os; echo $? + 9 + +可以看到,返回的结果确实是 ``9`` 。这样,我们勉强完成了一个简陋的用户态最小化执行环境。 + + +有显示支持的用户态执行环境 +---------------------------- + +没有 ``println`` 输出信息,终究觉得缺了点啥。 + +Rust 的 core 库内建了以一系列帮助实现显示字符的基本 Trait 和数据结构,函数等,我们可以对其中的关键部分进行扩展,就可以实现定制的 ``println!`` 功能。 + + +实现输出字符串的相关函数 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. attention:: + + 如果你觉得理解 Rust 宏有困难,把它当成黑盒就好! + + +首先封装一下对 ``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)+)?)); + } + } + +接下来,我们调整一下应用程序,让它发出显示字符串和退出的请求: + +.. 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/_sources/chapter1/4mini-rt-baremetal.rst.txt b/_sources/chapter1/4mini-rt-baremetal.rst.txt new file mode 100644 index 0000000..7cba2f9 --- /dev/null +++ b/_sources/chapter1/4mini-rt-baremetal.rst.txt @@ -0,0 +1,328 @@ +.. _term-print-kernelminienv: + +构建裸机执行环境 +================================= + +.. toctree:: + :hidden: + :maxdepth: 5 + +有了上一节实现的用户态的最小执行环境,稍加改造,就可以完成裸机上的最小执行环境了。 +本节中,我们将把 ``Hello world!`` 应用程序从用户态搬到内核态。 + + +裸机启动过程 +---------------------------- + +用 QEMU 软件 ``qemu-system-riscv64`` 来模拟 RISC-V 64 计算机。加载内核程序的命令如下: + +.. code-block:: bash + + qemu-system-riscv64 \ + -machine virt \ + -nographic \ + -bios $(BOOTLOADER) \ + -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) + + +- ``-bios $(BOOTLOADER)`` 意味着硬件加载了一个 BootLoader 程序,即 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`` 的位置,这里有固化在硬件中的一小段引导代码, +它会很快跳转到 ``0x80000000`` 的 RustSBI 处。 +RustSBI完成硬件初始化后,会跳转到 ``$(KERNEL_BIN)`` 所在内存位置 ``0x80200000`` 处, +执行操作系统的第一条指令。 + +.. figure:: chap1-intro.png + :align: center + +.. note:: + + **RustSBI 是什么?** + + SBI 是 RISC-V 的一种底层规范,RustSBI 是它的一种实现。 + 操作系统内核与 RustSBI 的关系有点像应用与操作系统内核的关系,后者向前者提供一定的服务。只是SBI提供的服务很少, + 比如关机,显示字符串等。 + +实现关机功能 +---------------------------- + +对上一节实现的代码稍作调整,通过 ``ecall`` 调用 RustSBI 实现关机功能: + +.. _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 { + core::arch::asm!( + "ecall", + ... + + const SBI_SHUTDOWN: usize = 8; + + pub fn shutdown() -> ! { + sbi_call(SBI_SHUTDOWN, 0, 0, 0); + panic!("It should shutdown!"); + } + + // os/src/main.rs + #[no_mangle] + extern "C" fn _start() { + shutdown(); + } + + +应用程序访问操作系统提供的系统调用的指令是 ``ecall`` ,操作系统访问 +RustSBI提供的SBI调用的指令也是 ``ecall`` , +虽然指令一样,但它们所在的特权级是不一样的。 +简单地说,应用程序位于最弱的用户特权级(User Mode), +操作系统位于内核特权级(Supervisor Mode), +RustSBI位于机器特权级(Machine Mode)。 +下一章会进一步阐释具体细节。 + +编译执行,结果如下: + +.. code-block:: bash + + # 编译生成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 + # 无法退出,风扇狂转,感觉碰到死循环 + +问题在哪?通过 rust-readobj 分析 ``os`` 可执行程序,发现其入口地址不是 +RustSBI 约定的 ``0x80200000`` 。我们需要修改程序的内存布局并设置好栈空间。 + + +设置正确的程序内存布局 +---------------------------- + +可以通过 **链接脚本** (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`` ,RustSBI 期望的 OS 起始地址; + +.. attention:: + + linker 脚本的语法不做要求,感兴趣的同学可以自行查阅相关资料。 + +从 ``BASE_ADDRESS`` 开始,代码段 ``.text``, 只读数据段 ``.rodata``,数据段 ``.data``, bss 段 ``.bss`` 由低到高依次放置, +且每个段都有两个全局变量给出其起始和结束地址(比如 ``.text`` 段的开始和结束地址分别是 ``stext`` 和 ``etext`` )。 + + +正确配置栈空间布局 +---------------------------- + +用另一段汇编代码初始化栈空间: + +.. 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`` ,链接脚本里有它的位置。 + +``_start`` 作为操作系统的入口地址,将依据链接脚本被放在 ``BASE_ADDRESS`` 处。 +``la sp, boot_stack_top`` 作为 OS 的第一条指令, +将 sp 设置为栈空间的栈顶。 +简单起见,我们目前不考虑 sp 越过栈底 ``boot_stack`` ,也就是栈溢出的情形。 +第二条指令则是函数调用 ``rust_main`` ,这里的 ``rust_main`` 是我们稍后自己编写的应用入口。 + +接着,我们在 ``main.rs`` 中嵌入这些汇编代码并声明应用入口 ``rust_main`` : + +.. code-block:: rust + :linenos: + :emphasize-lines: 7,9,10,11,12 + + // os/src/main.rs + #![no_std] + #![no_main] + + mod lang_items; + + core::arch::global_asm!(include_str!("entry.asm")); + + #[no_mangle] + pub fn rust_main() -> ! { + shutdown(); + } + +背景高亮指出了 ``main.rs`` 中新增的代码。 + +第 7 行,我们使用 ``global_asm`` 宏,将同目录下的汇编文件 ``entry.asm`` 嵌入到代码中。 + +从第 9 行开始, +我们声明了应用的入口点 ``rust_main`` ,需要注意的是,这里通过宏将 ``rust_main`` +标记为 ``#[no_mangle]`` 以避免编译器对它的名字进行混淆,不然在链接时, +``entry.asm`` 将找不到 ``main.rs`` 提供的外部符号 ``rust_main``,导致链接失败。 + +再次使用上节中的编译,生成和运行操作,我们看到QEMU模拟的RISC-V 64计算机 **优雅** 地退出了! + +.. code-block:: console + # 教程使用的 RustSBI 版本比代码框架稍旧,输出有所不同 + $ 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 段** 的工作我们还没有完成。 + +.. 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) } + }); + } + + pub fn rust_main() -> ! { + clear_bss(); + shutdown(); + } + +链接脚本 ``linker.ld`` 中给出的全局符号 ``sbss`` 和 ``ebss`` 让我们能轻松确定 ``.bss`` 段的位置。 + + +添加裸机打印相关函数 +---------------------------------- + +在上一节中我们为用户态程序实现的 ``println`` 宏,略作修改即可用于本节的内核态操作系统。 +详见 ``os/src/console.rs``。 + +利用 ``println`` 宏,我们重写异常处理函数 ``panic``,使其在 panic 时能打印错误发生的位置。 +相关代码位于 ``os/src/lang_items.rs`` 中。 + +我们还使用第三方库 ``log`` 为你实现了日志模块,相关代码位于 ``os/src/logging.rs`` 中。 + +.. note:: + + 在 cargo 项目中引入外部库 log,需要修改 ``Cargo.toml`` 加入相应的依赖信息。 + +现在,让我们重复一遍本章开头的试验,``make run LOG=TRACE``! + +.. figure:: color-demo.png + :align: center + +产生 panic 的地点与源码中的实际位置一致!至此,我们完成了第一章的实验内容, + + +.. note:: + + 背景知识:`理解应用程序和执行环境 `_ \ No newline at end of file diff --git a/_sources/chapter1/5exercise.rst.txt b/_sources/chapter1/5exercise.rst.txt new file mode 100644 index 0000000..268ccc9 --- /dev/null +++ b/_sources/chapter1/5exercise.rst.txt @@ -0,0 +1,143 @@ +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``。 + +实验检查 ++++++++++++++++++++++++++++++++ + +.. - 实验目录要求(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`` 后一段内存,输出之后关机。 + +问答作业 +------------------------------- + +.. 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``: 单步执行一条汇编指令。 + +报告要求 +------------------------------- + +- 简单总结你实现的功能(200字以内,不要贴代码)。 +- 完成问答题。 +- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 \ No newline at end of file diff --git a/_sources/chapter1/index.rst.txt b/_sources/chapter1/index.rst.txt new file mode 100644 index 0000000..356ff69 --- /dev/null +++ b/_sources/chapter1/index.rst.txt @@ -0,0 +1,13 @@ +.. _link-chapter1: + +第一章:应用程序与基本执行环境 +============================================== + +.. toctree:: + :maxdepth: 4 + + 0intro + 1app-ee-platform + 2remove-std + 3mini-rt-usrland + 4mini-rt-baremetal diff --git a/_sources/chapter2/0intro.rst.txt b/_sources/chapter2/0intro.rst.txt new file mode 100644 index 0000000..e7df06f --- /dev/null +++ b/_sources/chapter2/0intro.rst.txt @@ -0,0 +1,163 @@ +引言 +================================ + +本章导读 +--------------------------------- + + +**批处理系统** (Batch System) 出现于计算资源匮乏的年代,其核心思想是: +将多个程序打包到一起输入计算机;当一个程序运行结束后,计算机会 *自动* 执行下一个程序。 + +应用程序难免会出错,如果一个程序的错误导致整个操作系统都无法运行,那就太糟糕了。 +*保护* 操作系统不受出错程序破坏的机制被称为 **特权级** (Privilege) 机制, +它实现了用户态和内核态的隔离。 + +本章在上一章的基础上,让我们的 OS 内核能以批处理的形式一次运行多个应用程序,同时利用特权级机制, +令 OS 不因出错的用户态程序而崩溃。 + +本章首先为批处理操作系统设计用户程序,再阐述如何将这些应用程序链接到内核中,最后介绍如何利用特权级机制处理 Trap. + +实践体验 +--------------------------- + +.. note:: + + 基于github classroom的开发方式 + + 基于github classroom,可方便建立开发用的git repository,并可基于github的 codespace (在线版ubuntu +vscode)在线开发使用。整个开发环境仅仅需要一个网络浏览器。 + + 1. 在网络浏览器中用自己的 github id 登录 github.com + 2. 接收 `第二个实验练习的github classroom在线邀请 `_ ,根据提示一路选择OK即可。 + 3. 完成第二步后,你的第二个实验练习的 github repository 会被自动建立好,点击此github repository的链接,就可看到你要完成的第一个实验了。 + 4. 在你的第二个实验练习的网页的中上部可以看到一个醒目的 `code` 绿色按钮,点击后,可以进一步看到 `codespace` 标签和醒目的 `create codesapce on main` 绿色按钮。请点击这个绿色按钮,就可以进入到在线的ubuntu +vscode环境中 + 5. 再按照下面的环境安装提示在vscode的 `console` 中安装配置开发环境:rustc,qemu等工具。 + 6. 在vscode的 `console` 中执行 `make setupclassroom_test2` (该命令仅执行一次)配置githubclassroom 自动评分功能。 + 7. 然后就可以基于在线vscode进行开发、运行、提交等完整的实验过程了。 + + 上述的3,4,5步不是必须的,你也可以线下本地开发。 + + +本章我们引入了用户程序。 + +.. code-block:: console + + $ git clone https://github.com/LearningOS/rust-based-os-comp2022.git + $ cd rust-based-os-comp2022 + $ make setupclassroom_test2 //注意:这一步很重要,是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次,以后一般就不用执行了,除非 .github/workflows/classroom.yml发生了变化。 + +在 qemu 模拟器上运行本章代码: + +.. code-block:: console + + $ cd os2 + $ make run LOG=INFO + +批处理系统自动加载并运行了所有的用户程序,尽管某些程序出错了: + +.. code-block:: + + [rustsbi] RustSBI version 0.2.0-alpha.4 + .______ __ __ _______.___________. _______..______ __ + | _ \ | | | | / | | / || _ \ | | + | |_) | | | | | | (----`---| |----`| (----`| |_) || | + | / | | | | \ \ | | \ \ | _ < | | + | |\ \----.| `--' |.----) | | | .----) | | |_) || | + | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| + + [rustsbi] Implementation: RustSBI-QEMU Version 0.0.1 + [rustsbi-dtb] Hart count: cluster0 with 1 cores + [rustsbi] misa: RV64ACDFIMSU + [rustsbi] mideleg: ssoft, stimer, sext (0x222) + [rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab) + [rustsbi] pmp0: 0x80000000 ..= 0x800fffff (rwx) + [rustsbi] pmp1: 0x80000000 ..= 0x807fffff (rwx) + [rustsbi] pmp2: 0x0 ..= 0xffffffffffffff (---) + [rustsbi] enter supervisor 0x80200000 + [kernel] Hello, world! + [ INFO] [kernel] num_app = 6 + [ INFO] [kernel] app_0 [0x8020b040, 0x8020f868) + [ INFO] [kernel] app_1 [0x8020f868, 0x80214090) + [ INFO] [kernel] app_2 [0x80214090, 0x80218988) + [ INFO] [kernel] app_3 [0x80218988, 0x8021d160) + [ INFO] [kernel] app_4 [0x8021d160, 0x80221a68) + [ INFO] [kernel] app_5 [0x80221a68, 0x80226538) + [ INFO] [kernel] Loading app_0 + [ERROR] [kernel] PageFault in application, core dumped. + [ INFO] [kernel] Loading app_1 + [ERROR] [kernel] IllegalInstruction in application, core dumped. + [ INFO] [kernel] Loading app_2 + [ERROR] [kernel] IllegalInstruction in application, core dumped. + [ INFO] [kernel] Loading app_3 + [ INFO] [kernel] Application exited with code 1234 + [ INFO] [kernel] Loading app_4 + Hello, world from user mode program! + [ INFO] [kernel] Application exited with code 0 + [ INFO] [kernel] Loading app_5 + 3^10000=5079(MOD 10007) + 3^20000=8202(MOD 10007) + 3^30000=8824(MOD 10007) + 3^40000=5750(MOD 10007) + 3^50000=3824(MOD 10007) + 3^60000=8516(MOD 10007) + 3^70000=2510(MOD 10007) + 3^80000=9379(MOD 10007) + 3^90000=2621(MOD 10007) + 3^100000=2749(MOD 10007) + Test power OK! + [ INFO] [kernel] Application exited with code 0 + Panicked at src/batch.rs:68 All applications completed! + +本章代码树 +------------------------------------------------- + +.. code-block:: + + ── os2 + │   ├── Cargo.toml + │   ├── Makefile (修改:构建内核之前先构建应用) + │   ├── build.rs (新增:生成 link_app.S 将应用作为一个数据段链接到内核) + │   └── src + │   ├── batch.rs(新增:实现了一个简单的批处理系统) + │   ├── console.rs + │   ├── entry.asm + │   ├── lang_items.rs + │   ├── link_app.S(构建产物,由 os/build.rs 输出) + │   ├── linker.ld + │   ├── logging.rs + │   ├── main.rs(修改:主函数中需要初始化 Trap 处理并加载和执行应用) + │   ├── sbi.rs + │   ├── sync(新增:包装了RefCell,暂时不用关心) + │   │   ├── mod.rs + │   │   └── up.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 上下文保存与恢复的汇编代码) + └── user(新增:应用测例保存在 user 目录下) + ├── Cargo.toml + ├── Makefile + └── src + ├── bin(基于用户库 user_lib 开发的应用,每个应用放在一个源文件中) + │   ├── ... + ├── console.rs + ├── lang_items.rs + ├── lib.rs(用户库 user_lib) + ├── linker.ld(应用的链接脚本) + └── syscall.rs(包含 syscall 方法生成实际用于系统调用的汇编指令, + 各个具体的 syscall 都是通过 syscall 来实现的) + + cloc os + ------------------------------------------------------------------------------- + Language files blank comment code + ------------------------------------------------------------------------------- + Rust 14 62 21 435 + Assembly 3 9 16 106 + make 1 12 4 36 + TOML 1 2 1 9 + ------------------------------------------------------------------------------- + SUM: 19 85 42 586 + ------------------------------------------------------------------------------- diff --git a/_sources/chapter2/2application.rst.txt b/_sources/chapter2/2application.rst.txt new file mode 100644 index 0000000..99dbed7 --- /dev/null +++ b/_sources/chapter2/2application.rst.txt @@ -0,0 +1,214 @@ +实现应用程序 +=========================== + +.. toctree:: + :hidden: + :maxdepth: 5 + +.. note:: + + 拓展阅读:`RISC-V 特权级机制 `_ + + +应用程序设计 +----------------------------- + +.. attention:: + + 用户库看起来很复杂,它预留了直到 ch7 内核才能实现的系统调用接口,console 模块还实现了输出缓存区。它们不是为本章准备的,你只需关注本节提到的部分即可。 + + +应用程序、用户库(包括入口函数、初始化函数、I/O函数和系统调用接口等多个rs文件组成)放在项目根目录的 ``user`` 目录下: + +- user/src/bin/*.rs:各个应用程序 +- user/src/*.rs:用户库(包括入口函数、初始化函数、I/O函数和系统调用接口等) +- user/src/linker.ld:应用程序的内存布局说明 + +项目结构 +^^^^^^^^^^^^^^^^^^^^^^ + +``user/src/bin`` 里面有多个文件,其中三个是: + +- ``hello_world``:在屏幕上打印一行 ``Hello, world!`` +- ``bad_address``:访问一个非法的物理地址,测试批处理系统是否会被该错误影响 +- ``power``:不断在计算操作和打印字符串操作之间切换 + +批处理系统会按照文件名顺序加载并运行它们。 + +每个应用程序的实现都在对应的单个文件中。打开 ``hello_world.rs``,能看到一个 ``main`` 函数,还有外部库引用: + +.. code-block:: rust + + #[macro_use] + extern crate user_lib; + +这个外部库其实就是 ``user`` 目录下的 ``lib.rs`` 以及它引用的若干子模块。 +在 ``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(); + exit(main()); + } + +第 2 行使用 ``link_section`` 宏将 ``_start`` 函数编译后的汇编代码放在名为 ``.text.entry`` 的代码段中, +方便用户库链接脚本将它作为用户程序的入口。 + +而从第 4 行开始,我们手动清零 ``.bss`` 段,然后调用 ``main`` 函数得到一个类型为 ``i32`` 的返回值, +最后,调用用户库提供的 ``exit`` 接口退出,并将返回值告知批处理系统。 + +我们在 ``lib.rs`` 中看到了另一个 ``main`` : + +.. code-block:: rust + :linenos: + + #![feature(linkage)] // 启用弱链接特性 + + #[linkage = "weak"] + #[no_mangle] + fn main() -> i32 { + panic!("Cannot find main!"); + } + +我们使用 Rust 宏将其标志为弱链接。这样在最后链接的时候, +虽然 ``lib.rs`` 和 ``bin`` 目录下的某个应用程序中都有 ``main`` 符号, +但由于 ``lib.rs`` 中的 ``main`` 符号是弱链接, +链接器会使用 ``bin`` 目录下的函数作为 ``main`` 。 +如果在 ``bin`` 目录下找不到任何 ``main`` ,那么编译也能通过,但会在运行时报错。 + +内存布局 +^^^^^^^^^^^^^^^^^^^^^^ + +我们使用链接脚本 ``user/src/linker.ld`` 规定用户程序的内存布局: + +- 将程序的起始物理地址调整为 ``0x80400000`` ,三个应用程序都会被加载到这个物理地址上运行; +- 将 ``_start`` 所在的 ``.text.entry`` 放在整个程序的开头 ``0x80400000``; + 批处理系统在加载应用后,跳转到 ``0x80400000``,就进入了用户库的 ``_start`` 函数; +- 提供了最终生成可执行文件的 ``.bss`` 段的起始和终止地址,方便 ``clear_bss`` 函数使用。 + +其余的部分和第一章基本相同。 + +系统调用 +^^^^^^^^^^^^^^^^^^^^^^ + +在子模块 ``syscall`` 中我们来通过 ``ecall`` 调用批处理系统提供的接口, +由于应用程序运行在用户态(即 U 模式), ``ecall`` 指令会触发名为 ``Environment call from U-mode`` 的异常, +并 Trap 进入 S 模式执行批处理系统针对这个异常特别提供的服务程序。 +这个接口被称为 ABI 或者系统调用。 +现在我们不关心 S 态的批处理系统如何提供应用程序所需的功能,只考虑如何使用它。 + +在本章中,应用程序和批处理系统约定如下两个系统调用: + +.. 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) -> !; + +实际调用时,我们要按照 RISC-V 调用规范,在合适的寄存器中放置参数, +然后执行 ``ecall`` 指令触发 Trap。当 Trap 结束,回到 U 模式后, +用户程序会从 ``ecall`` 的下一条指令继续执行,同时在合适的寄存器中读取返回值。 + +.. note:: + + RISC-V 寄存器编号从 ``0~31`` ,表示为 ``x0~x31`` 。 其中: + - ``x10~x17`` : 对应 ``a0~a7`` + - ``x1`` :对应 ``ra`` + +约定寄存器 ``a0~a6`` 保存系统调用的参数, ``a0`` 保存系统调用的返回值, +寄存器 ``a7`` 用来传递 syscall ID。 +这超出了 Rust 语言的表达能力,我们需要内嵌汇编来完成参数/返回值绑定和 ``ecall`` 指令的插入: + +.. code-block:: rust + :linenos: + + // user/src/syscall.rs + + fn syscall(id: usize, args: [usize; 3]) -> isize { + let mut ret: isize; + unsafe { + core::arch::asm!( + "ecall", + inlateout("x10") args[0] => ret, + in("x11") args[1], + in("x12") args[2], + in("x17") id + ); + } + ret + } + +第 3 行,我们将所有的系统调用都封装成 ``syscall`` 函数,可以看到它支持传入 syscall ID 和 3 个参数。 + +第 6 行开始,我们使用 Rust 提供的 ``asm!`` 宏在代码中内嵌汇编。 +Rust 编译器无法判定汇编代码的安全性,所以我们需要将其包裹在 unsafe 块中。 + +简而言之,这条汇编代码的执行结果是以寄存器 ``a0~a2`` 来保存系统调用的参数,以及寄存器 ``a7`` 保存 syscall ID, +返回值通过寄存器 ``a0`` 传递给局部变量 ``ret``。 + +这段汇编代码与第一章中出现过的内嵌汇编很像,读者可以查看 ``os/src/sbi.rs`` 。 + +.. note:: + + 可以查看 `Inline assembly `_ 了解 ``asm`` 宏。 + +于是 ``sys_write`` 和 ``sys_exit`` 只需将 ``syscall`` 进行包装: + +.. code-block:: rust + :linenos: + + // user/src/syscall.rs + + 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]) + } + +我们将上述两个系统调用在用户库 ``user_lib`` 中进一步封装,像标准库一样: + +.. 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`` 子模块中,借助 ``write``,我们为应用程序实现了 ``println!`` 宏。 +传入到 ``write`` 的 ``fd`` 参数设置为 1,代表标准输出 STDOUT,暂时不用考虑其他的 ``fd`` 选取情况。 + + +编译生成应用程序二进制码 +------------------------------- + +简要介绍一下应用程序的构建,在 ``user`` 目录下 ``make build``: + +1. 对于 ``src/bin`` 下的每个应用程序, + 在 ``target/riscv64gc-unknown-none-elf/release`` 目录下生成一个同名的 ELF 可执行文件; +2. 使用 objcopy 二进制工具删除所有 ELF header 和符号,得到 ``.bin`` 后缀的纯二进制镜像文件。 + 它们将被链接进内核,并由内核在合适的时机加载到内存。 diff --git a/_sources/chapter2/3batch-system.rst.txt b/_sources/chapter2/3batch-system.rst.txt new file mode 100644 index 0000000..ce2786b --- /dev/null +++ b/_sources/chapter2/3batch-system.rst.txt @@ -0,0 +1,168 @@ + +.. _term-batchos: + +实现批处理操作系统 +============================== + +.. toctree:: + :hidden: + :maxdepth: 5 + +将应用程序链接到内核 +-------------------------------------------- + +在本章中,我们要把应用程序的二进制镜像文件作为数据段链接到内核里, +内核需要知道应用程序的数量和它们的位置。 + +在 ``os/src/main.rs`` 中能够找到这样一行: + +.. code-block:: rust + + core::arch::global_asm!(include_str!("link_app.S")); + +这里我们引入了一段汇编代码 ``link_app.S`` ,它是在 ``make run`` 构建操作系统时自动生成的,里面的内容大致如下: + +.. code-block:: asm + :linenos: + + # os/src/link_app.S + + .align 3 + .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/hello_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/bad_address.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/power.bin" + app_2_end: + +第 13 行开始的三个数据段分别插入了三个应用程序的二进制镜像, +并且各自有一对全局符号 ``app_*_start, app_*_end`` 指示它们的开始和结束位置。 +而第 3 行开始的另一个数据段相当于一个 64 位整数数组。 +数组中的第一个元素表示应用程序的数量,后面则按照顺序放置每个应用程序的起始地址, +最后一个元素放置最后一个应用程序的结束位置。这样数组中相邻两个元素记录了每个应用程序的始末位置, +这个数组所在的位置由全局符号 ``_num_app`` 所指示。 + +这个文件是在 ``cargo build`` 时,由脚本 ``os/build.rs`` 控制生成的。 + +找到并加载应用程序二进制码 +----------------------------------------------- + +我们在 ``os`` 的 ``batch`` 子模块中实现一个应用管理器 ``AppManager`` ,结构体定义如下: + +.. code-block:: rust + + struct AppManager { + num_app: usize, + current_app: usize, + app_start: [usize; MAX_APP_NUM + 1], + } + +初始化 ``AppManager`` 的全局实例: + +.. code-block:: rust + + lazy_static! { + static ref APP_MANAGER: UPSafeCell = unsafe { + UPSafeCell::new({ + extern "C" { + fn _num_app(); + } + let num_app_ptr = _num_app as usize as *const usize; + let num_app = num_app_ptr.read_volatile(); + let mut app_start: [usize; MAX_APP_NUM + 1] = [0; MAX_APP_NUM + 1]; + let app_start_raw: &[usize] = + core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1); + app_start[..=num_app].copy_from_slice(app_start_raw); + AppManager { + num_app, + current_app: 0, + app_start, + } + }) + }; + } + +初始化的逻辑很简单,就是找到 ``link_app.S`` 中提供的符号 ``_num_app`` ,并从这里开始解析出应用数量以及各个应用的开头地址。 +用容器 ``UPSafeCell`` 包裹 ``AppManager`` 是为了防止全局对象 ``APP_MANAGER`` 被重复获取。 + +.. note:: + + ``UPSafeCell`` 实现在 ``sync`` 模块中,调用 ``exclusive_access`` 方法能获取其内部对象的可变引用, + 如果程序运行中同时存在多个这样的引用,会触发 ``already borrowed: BorrowMutError``。 + + ``UPSafeCell`` 既提供了内部可变性,又在单核情境下防止了内部对象被重复借用,我们将在后文中多次见到它。 + +这里使用了外部库 ``lazy_static`` 提供的 ``lazy_static!`` 宏。 + +``lazy_static!`` 宏提供了全局变量的运行时初始化功能。一般情况下,全局变量必须在编译期设置初始值, +但是有些全局变量的初始化依赖于运行期间才能得到的数据。 +如这里我们借助 ``lazy_static!`` 声明了一个 ``AppManager`` 结构的名为 ``APP_MANAGER`` 的全局实例, +只有在它第一次被使用到的时候才会进行实际的初始化工作。 + +``AppManager`` 的方法中, ``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!"); + } + info!("[kernel] Loading app_{}", app_id); + // clear icache + core::arch::asm!("fence.i"); + // clear app area + core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, APP_SIZE_LIMIT).fill(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`` 对应的应用程序的二进制镜像加载到物理内存以 ``0x80400000`` 起始的位置, +这个位置是批处理操作系统和应用程序之间约定的常数地址。 +我们将从这里开始的一块内存清空,然后找到待加载应用二进制镜像的位置,并将它复制到正确的位置。 + +清空内存前,我们插入了一条奇怪的汇编指令 ``fence.i`` ,它是用来清理 i-cache 的。 +我们知道, 缓存又分成 **数据缓存** (d-cache) 和 **指令缓存** (i-cache) 两部分,分别在 CPU 访存和取指的时候使用。 +通常情况下, CPU 会认为程序的代码段不会发生变化,因此 i-cache 是一种只读缓存。 +但在这里,我们会修改会被 CPU 取指的内存区域,使得 i-cache 中含有与内存不一致的内容, +必须使用 ``fence.i`` 指令手动清空 i-cache ,让里面所有的内容全部失效, +才能够保证程序执行正确性。 + +.. warning:: + + **模拟器与真机的不同之处** + + 在 Qemu 模拟器上,即使不加刷新 i-cache 的指令,大概率也能正常运行,但在物理计算机上不是这样。 + +``batch`` 子模块对外暴露出如下接口: + +- ``init`` :调用 ``print_app_info`` 的时第一次用到了全局变量 ``APP_MANAGER`` ,它在这时完成初始化; +- ``run_next_app`` :批处理操作系统的核心操作,即加载并运行下一个应用程序。 + 批处理操作系统完成初始化,或者应用程序运行结束/出错后会调用该函数。下节再介绍其具体实现。 \ No newline at end of file diff --git a/_sources/chapter2/4trap-handling.rst.txt b/_sources/chapter2/4trap-handling.rst.txt new file mode 100644 index 0000000..89de48b --- /dev/null +++ b/_sources/chapter2/4trap-handling.rst.txt @@ -0,0 +1,519 @@ +.. _term-trap-handle: + +实现特权级的切换 +=========================== + +.. toctree:: + :hidden: + :maxdepth: 5 + +RISC-V特权级切换 +--------------------------------------- + +特权级切换的起因 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +批处理操作系统为了建立好应用程序的执行环境,需要在执行应用程序前进行一些初始化工作, +并监控应用程序的执行,具体体现在: + +- 启动应用程序时,需要初始化应用程序的用户态上下文,并能切换到用户态执行应用程序; +- 应用程序发起系统调用后,需要切换到批处理操作系统中进行处理; +- 应用程序执行出错时,批处理操作系统要杀死该应用并加载运行下一个应用; +- 应用程序执行结束时,批处理操作系统要加载运行下一个应用。 + +这些处理都涉及到特权级切换,因此都需要硬件和操作系统协同提供的特权级切换机制。 + + +特权级切换相关的控制状态寄存器 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +本章中我们仅考虑当 CPU 在 U 特权级运行用户程序的时候触发 Trap, +并切换到 S 特权级的批处理操作系统进行处理。 + +.. 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 处理代码的入口地址 + +特权级切换的具体过程一部分由硬件直接完成,另一部分则需要由操作系统来实现。 + +.. _trap-hw-mechanism: + +特权级切换的硬件控制机制 +------------------------------------- + +当 CPU 执行完一条指令并准备从用户特权级 陷入( ``Trap`` )到 S 特权级的时候,硬件会自动完成如下这些事情: + +- ``sstatus`` 的 ``SPP`` 字段会被修改为 CPU 当前的特权级(U/S)。 +- ``sepc`` 会被修改为 Trap 处理完成后默认会执行的下一条指令的地址。 +- ``scause/stval`` 分别会被修改成这次 Trap 的原因以及相关的附加信息。 +- CPU 会跳转到 ``stvec`` 所设置的 Trap 处理入口地址,并将当前特权级设置为 S ,然后从Trap 处理入口地址处开始执行。 + +.. 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 模式, + +而当 CPU 完成 Trap 处理准备返回的时候,需要通过一条 S 特权级的特权指令 ``sret`` 来完成,这一条指令具体完成以下功能: + +- CPU 会将当前的特权级按照 ``sstatus`` 的 ``SPP`` 字段设置为 U 或者 S ; +- CPU 会跳转到 ``sepc`` 寄存器指向的那条指令,然后继续执行。 + +用户栈与内核栈 +-------------------------------- + +在 Trap 触发的一瞬间, CPU 会切换到 S 特权级并跳转到 ``stvec`` 所指示的位置。 +但是在正式进入 S 特权级的 Trap 处理之前,我们必须保存原控制流的寄存器状态,这一般通过栈来完成。 +但我们需要用专门为操作系统准备的内核栈,而不是应用程序运行时用到的用户栈。 + +我们声明两个类型 ``KernelStack`` 和 ``UserStack`` 分别表示用户栈和内核栈,它们都只是字节数组的简单包装: + +.. code-block:: rust + :linenos: + + // os/src/batch.rs + + #[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], + }; + + +两个栈以全局变量的形式实例化在批处理操作系统的 ``.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 上下文,即在 Trap 发生时需要保存的物理资源内容,定义如下: + +.. 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 上下文的保存与恢复 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +首先是具体实现 Trap 上下文保存和恢复的汇编代码。 + +.. _trap-context-save-restore: + + +在批处理操作系统初始化时,我们需要修改 ``stvec`` 寄存器来指向正确的 Trap 处理入口点。 + +.. code-block:: rust + :linenos: + + // os/src/trap/mod.rs + + core::arch::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`` 标记为函数,并通过 ``global_asm!`` 宏将 ``trap.S`` 这段汇编代码插入进来。 + +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 特权级规范的要求; +- 第 9 行的 ``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),因为它在第 9 行 + 后指向的是内核栈。用户栈的栈指针保存在 sscratch 中,必须通过 ``csrr`` 指令读到通用寄存器中后才能使用,因此我们先考虑保存其它通用寄存器,腾出空间。 + + 我们要基于 sp 来找到每个寄存器应该被保存到的正确的位置。实际上,在栈帧分配之后,我们可用于保存 Trap 上下文的地址区间为 :math:`[\text{sp},\text{sp}+8\times34)` , + 按照 ``TrapContext`` 结构体的内存布局,基于内核栈的位置(sp所指地址)来从低地址到高地址分别按顺序放置 x0~x31这些通用寄存器,最后是 sstatus 和 sepc 。因此通用寄存器 xn + 应该被保存在地址区间 :math:`[\text{sp}+8n,\text{sp}+8(n+1))` 。 + + 为了简化代码,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 并保存到内核栈上,注意: sscratch 的值是进入 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 特权级继续运行应用程序控制流。 + +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 。 +- 第 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!`` + 宏打印出来。这里我们并没有检查传入参数的安全性,存在安全隐患。 +- ``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: + + // os/src/batch.rs + + pub fn run_next_app() -> ! { + let mut app_manager = APP_MANAGER.exclusive_access(); + let current_app = app_manager.get_current_app(); + unsafe { + app_manager.load_app(current_app); + } + app_manager.move_to_next_app(); + drop(app_manager); + // before this we have to drop local variables related to resources manually + // and release the resources + 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!"); + } + + +``__restore`` 所做的事情是在内核栈上压入一个 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/_sources/chapter2/5exercise.rst.txt b/_sources/chapter2/5exercise.rst.txt new file mode 100644 index 0000000..fbeaa2c --- /dev/null +++ b/_sources/chapter2/5exercise.rst.txt @@ -0,0 +1,139 @@ +chapter2练习(已废弃) +===================================================== + +.. toctree:: + :hidden: + :maxdepth: 4 + +编程练习 +------------------------------- + +简单安全检查 ++++++++++++++++++++++++++++++++ + +.. lab2 中,我们实现了第一个系统调用 ``sys_write``,这使得我们可以在用户态输出信息。但是 os 在提供服务的同时,还有保护 os 本身以及其他用户程序不受错误或者恶意程序破坏的功能。 + +.. 由于还没有实现虚拟内存,我们可以在用户程序中指定一个属于其他程序字符串,并将它输出,这显然是不合理的,因此我们要对 sys_write 做检查: + +.. - sys_write 仅能输出位于程序本身内存空间内的数据,否则报错。 + +实验要求 ++++++++++++++++++++++++++++++++ +.. - 实现分支: ch2。 +.. - 完成实验指导书中的内容,能运行用户态程序并执行 sys_write,sys_exit 系统调用。 +.. - 为 sys_write 增加安全性检查,并通过 `Rust测例 `_ 中 chapter2 对应的所有测例,测例详情见对应仓库。 + +.. challenge: 支持多核,实现多个核运行用户程序。 + +实验约定 +++++++++++++++++++++++++++++++ + +.. 在第二章的测试中,我们对于内核有如下仅仅为了测试方便的要求,请调整你的内核代码来符合这些要求。 + +.. - 用户栈大小必须为 4096,且按照 4096 字节对齐。这一规定可以在实验4开始删除,仅仅为通过 lab2/3 测例设置。 + +.. .. _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 可以正确执行,尽量不输出调试信息) +.. │   ├── src(所有内核的源代码放在 os/src 目录下) +.. │   ├── main.rs(内核主函数) +.. │   ├── ... +.. ├── reports +.. │   ├── lab2.md/pdf +.. │   └── ... +.. ├── README.md(其他必要的说明) +.. ├── ... + +.. 参考示例目录结构。目标用户目录 ``../user/build/bin``。 + +.. - 检查 + +.. .. code-block:: console + +.. $ git checkout ch2 +.. $ cd os +.. $ make run + +.. 可以正确执行正确执行目标用户测例,并得到预期输出(详见测例注释)。 + + +简答题 +------------------------------- + +.. 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`` 的方法?简单描述你的想法。 + +报告要求 +------------------------------- + +- 简单总结你实现的功能(200字以内,不要贴代码)。 +- 完成问答题。 +- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 diff --git a/_sources/chapter2/index.rst.txt b/_sources/chapter2/index.rst.txt new file mode 100644 index 0000000..5f1083d --- /dev/null +++ b/_sources/chapter2/index.rst.txt @@ -0,0 +1,13 @@ +.. _link-chapter2: + +第二章:批处理系统 +============================================== + +.. toctree:: + :maxdepth: 4 + + 0intro + 2application + 3batch-system + 4trap-handling + diff --git a/_sources/chapter3/0intro.rst.txt b/_sources/chapter3/0intro.rst.txt new file mode 100644 index 0000000..5b9b663 --- /dev/null +++ b/_sources/chapter3/0intro.rst.txt @@ -0,0 +1,224 @@ +引言 +======================================== + +本章导读 +-------------------------- + + +本章的目标是实现分时多任务系统,它能并发地执行多个用户程序,并调度这些程序。为此需要实现 + +- 一次性加载所有用户程序,减少任务切换开销; +- 支持任务切换机制,保存切换前后程序上下文; +- 支持程序主动放弃处理器,实现 yield 系统调用; +- 以时间片轮转算法调度用户程序,实现资源的时分复用。 + + +实践体验 +------------------------------------- + +.. note:: + + 基于github classroom的开发方式 + + 基于github classroom,可方便建立开发用的git repository,并可基于github的 codespace (在线版ubuntu +vscode)在线开发使用。整个开发环境仅仅需要一个网络浏览器。 + + 1. 在网络浏览器中用自己的 github id 登录 github.com + 2. 接收 `第一个实验(os3)的github classroom在线邀请 `_ ,根据提示一路选择OK即可。 + 3. 完成第二步后,你的第一个实验的 github repository 会被自动建立好,点击此github repository的链接,就可看到你要完成的第一个实验了。 + 4. 在你的第一个实验的网页的中上部可以看到一个醒目的 `code` 绿色按钮,点击后,可以进一步看到 `codespace` 标签和醒目的 `create codesapce on main` 绿色按钮。请点击这个绿色按钮,就可以进入到在线的ubuntu +vscode环境中 + 5. 再按照下面的环境安装提示在vscode的 `console` 中安装配置开发环境:rustc,qemu等工具。 + 6. 在vscode的 `console` 中执行 `make setupclassroom_test3` (该命令仅执行一次)配置githubclassroom 自动评分功能。 + 7. 然后就可以基于在线vscode进行开发、运行、提交等完整的实验过程了。 + + 上述的3,4,5步不是必须的,你也可以线下本地开发。 + +.. code-block:: console + + $ git clone https://github.com/LearningOS/rust-based-os-comp2022.git + $ cd rust-based-os-comp2022/ + $ make setupclassroom_test3 //注意:这一步很重要,是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次,以后一般就不用执行了,除非 .github/workflows/classroom.yml发生了变化。 + +在 qemu 模拟器上运行 `lab1(os3)参考框架: `_ : + +.. code-block:: console + + $ cd os3-ref + $ make run + +运行代码,看到用户程序交替输出信息: + +.. code-block:: + + [rustsbi] RustSBI version 0.2.0-alpha.4 + .______ __ __ _______.___________. _______..______ __ + | _ \ | | | | / | | / || _ \ | | + | |_) | | | | | | (----`---| |----`| (----`| |_) || | + | / | | | | \ \ | | \ \ | _ < | | + | |\ \----.| `--' |.----) | | | .----) | | |_) || | + | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| + + [rustsbi] Implementation: RustSBI-QEMU Version 0.0.1 + [rustsbi-dtb] Hart count: cluster0 with 1 cores + [rustsbi] misa: RV64ACDFIMSU + [rustsbi] mideleg: ssoft, stimer, sext (0x222) + [rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab) + [rustsbi] pmp0: 0x80000000 ..= 0x800fffff (rwx) + [rustsbi] pmp1: 0x80000000 ..= 0x807fffff (rwx) + [rustsbi] pmp2: 0x0 ..= 0xffffffffffffff (---) + [rustsbi] enter supervisor 0x80200000 + [kernel] Hello, world! + power_3 [10000/200000] + power_3 [20000/200000] + power_3 [30000/200000] + power_3 [40000/200000] + power_3 [50000/200000] + power_3 [60000/200000] + power_3 [70000/200000] + power_3 [80000/200000] + power_3 [90000/200000] + power_3 [100000/200000] + power_3 [110000/200000] + power_3 [120000/200000] + power_3 [130000/200000] + power_3 [140000/200000] + power_3 [150000/200000] + power_3 [160000/200000] + power_3 [170000/200000] + power_3 [180000/200000] + power_3 [190000/200000] + power_3 [200000/200000] + 3^200000 = 871008973(MOD 998244353) + Test power_3 OK! + power_5 [10000/140000] + power_5 [20000/140000] + power_5 [30000/140000] + power_5 [40000/140000] + power_5 [50000/140000] + power_5 [60000/140000] + power_7 [10000/160000] + power_7 [20000/160000] + power_7 [30000/160000] + power_7 [40000/160000] + power_7 [50000/160000] + power_7 [60000/160000] + power_7 [70000/160000] + power_7 [80000/160000] + power_7 [90000/160000] + power_7 [100000/160000] + power_7 [110000/160000] + power_7 [120000/160000] + power_7 [130000/160000] + power_7 [140000/160000] + power_7 [150000/160000] + power_7 [160000/160000] + 7^160000 = 667897727(MOD 998244353) + Test power_7 OK! + get_time OK! 42 + current time_msec = 42 + AAAAAAAAAA [1/5] + BBBBBBBBBB [1/5] + CCCCCCCCCC [1/5] + power_5 [70000/140000] + AAAAAAAAAA [2/5] + BBBBBBBBBB [2/5] + CCCCCCCCCC [2/5] + power_5 [80000/140000] + power_5 [90000/140000] + power_5 [100000/140000] + power_5 [110000/140000] + power_5 [120000/140000] + power_5 [130000/140000] + power_5 [140000/140000] + 5^140000 = 386471875(MOD 998244353) + Test power_5 OK! + AAAAAAAAAA [3/5] + BBBBBBBBBB [3/5] + CCCCCCCCCC [3/5] + AAAAAAAAAA [4/5] + BBBBBBBBBB [4/5] + CCCCCCCCCC [4/5] + AAAAAAAAAA [5/5] + BBBBBBBBBB [5/5] + CCCCCCCCCC [5/5] + Test write A OK! + Test write B OK! + Test write C OK! + time_msec = 143 after sleeping 100 ticks, delta = 101ms! + Test sleep1 passed! + Test sleep OK! + Panicked at src/task/mod.rs:98 All applications completed! + + +`lab1(os3)参考框架: `_ +-------------------------------------------------------------------------------------------------------------------- + +.. code-block:: + + ── os3-ref +    ├── build.rs +    ├── Cargo.toml +    ├── Makefile +    └── src +    ├── batch.rs(移除:功能分别拆分到 loader 和 task 两个子模块) +   ├── config.rs(新增:保存内核的一些配置) +    ├── console.rs + ├── logging.rs + ├── sync +    ├── entry.asm +    ├── lang_items.rs +    ├── link_app.S +    ├── linker.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 + + cloc os + ------------------------------------------------------------------------------- + Language files blank comment code + ------------------------------------------------------------------------------- + Rust 21 87 20 627 + Assembly 4 12 22 144 + make 1 11 4 36 + TOML 1 2 1 10 + ------------------------------------------------------------------------------- + SUM: 27 112 47 817 + ------------------------------------------------------------------------------- + + +.. 本章代码导读 +.. ----------------------------------------------------- + +.. 本章的重点是实现对应用之间的协作式和抢占式任务切换的操作系统支持。与上一章的操作系统实现相比,有如下一些不同的情况导致实现上也有差异: + +.. - 多个应用同时放在内存中,所以他们的起始地址是不同的,且地址范围不能重叠 +.. - 应用在整个执行过程中会暂停或被抢占,即会有主动或被动的任务切换 + +.. 这些实现上差异主要集中在对应用程序执行过程的管理、支持应用程序暂停的系统调用和主动切换应用程序所需的时钟中断机制的管理。 + +.. 对于第一个不同情况,需要对应用程序的地址空间布局进行调整,每个应用的地址空间都不相同,且不能重叠。这并不要修改应用程序本身,而是通过一个脚本 ``build.py`` 来针对每个应用程序修改链接脚本 ``linker.ld`` 中的 ``BASE_ADDRESS`` ,让编译器在编译不同应用时用到的 ``BASE_ADDRESS`` 都不同,且有足够大的地址间隔。这样就可以让每个应用所在的内存空间是不同的。 + +.. 对于第二个不同情况,需要实现任务切换,这就需要在上一章的 ``trap`` 上下文切换的基础上,再加上一个 ``task`` 上下文切换,才能完成完整的任务切换。这里面的关键数据结构是表示应用执行上下文的 ``TaskContext`` 数据结构和具体完成上下文切换的汇编语言编写的 ``__switch`` 函数。一个应用的执行需要被操作系统管理起来,这是通过 ``TaskControlBlock`` 数据结构来表示应用执行上下文的动态过程和动态状态(运行态、就绪态等)。而为了做好应用程序第一次执行的前期初始化准备, ``TaskManager`` 数据结构的全局变量实例 ``TASK_MANAGER`` 描述了应用程序初始化所需的数据, 而 ``TASK_MANAGER`` 的初始化赋值过程是实现这个准备的关键步骤。 + +.. 应用程序可以在用户态执行后,还需要有新的系统调用 ``sys_yield`` 的实现来支持应用自己的主动暂停;还要添加对时钟中断的处理,来支持抢占应用执行的抢占式切换。有了时钟中断,就可以在一定时间内打断应用的执行,并主动切换到另外一个应用,这部分主要是通过对 ``trap_handler`` 函数中进行扩展,来完成在时钟中断产生时可能进行的任务切换。 ``TaskManager`` 数据结构的成员函数 ``run_next_task`` 来实现基于任务控制块的切换,并会具体调用 ``__switch`` 函数完成硬件相关部分的任务上下文切换。 + +.. 如果理解了上面的数据结构和相关函数的关系和相互调用的情况,那么就比较容易理解本章改进后的操作系统了。 + + +.. .. [#prionosuchus] 锯齿螈身长可达9米,是迄今出现过的最大的两栖动物,是二叠纪时期江河湖泊和沼泽中的顶级掠食者。 +.. .. [#eoraptor] 始初龙(也称始盗龙)是后三叠纪时期的两足食肉动物,也是目前所知最早的恐龙,它们只有一米长,却代表着恐龙的黎明。 +.. .. [#coelophysis] 腔骨龙(也称虚形龙)最早出现于三叠纪晚期,它体形纤细,善于奔跑,以小型动物为食。 diff --git a/_sources/chapter3/1multi-loader.rst.txt b/_sources/chapter3/1multi-loader.rst.txt new file mode 100644 index 0000000..7ebe569 --- /dev/null +++ b/_sources/chapter3/1multi-loader.rst.txt @@ -0,0 +1,71 @@ +多道程序放置与加载 +===================================== + +多道程序放置 +---------------------------- + + +在第二章中,内核让所有应用都共享同一个固定的起始地址。 +正因如此,内存中同时最多只能驻留一个应用, + +要一次加载运行多个程序,就要求每个用户程序被内核加载到内存中的起始地址都不同。 +为此,我们编写脚本 ``user/build.py`` 为每个应用定制各自的起始地址。 +它的思路很简单,对于每一个应用程序,使用 ``cargo rustc`` 单独编译, +用 ``-Clink-args=-Ttext=xxxx`` 选项指定链接时 .text 段的地址为 ``0x80400000 + app_id * 0x20000`` 。 + +.. note:: + + qemu 预留的内存空间是有限的,如果加载的程序过多,程序地址超出内存空间,可能出现 ``core dumped``. + +多道程序加载 +---------------------------- + +在第二章中负责应用加载和执行的子模块 ``batch`` 被拆分为 ``loader`` 和 ``task`` , +前者负责启动时加载应用程序,后者负责切换和调度。 + +其中, ``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 { + core::arch::asm!("fence.i"); + } + // 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`` 子模块中找到这两个常数, ``APP_BASE_ADDRESS`` 被设置为 ``0x80400000`` , +而 ``APP_SIZE_LIMIT`` 和上一章一样被设置为 ``0x20000`` 。这种放置方式与 ``user/build.py`` 的实现一致。 diff --git a/_sources/chapter3/2task-switching.rst.txt b/_sources/chapter3/2task-switching.rst.txt new file mode 100644 index 0000000..b32046b --- /dev/null +++ b/_sources/chapter3/2task-switching.rst.txt @@ -0,0 +1,104 @@ +任务切换 +================================ + + +本节我们将见识操作系统的核心机制—— **任务切换** , +即应用在运行中主动或被动地交出 CPU 的使用权,内核可以选择另一个程序继续执行。 +内核需要保证用户程序两次运行期间,任务上下文(如寄存器、栈等)保持一致。 + +任务切换的设计与实现 +--------------------------------- + +任务切换与上一章提及的 Trap 控制流切换相比,有如下异同: + +- 与 Trap 切换不同,它不涉及特权级切换,部分由编译器完成; +- 与 Trap 切换相同,它对应用是透明的。 + +事实上,任务切换是来自两个不同应用在内核中的 Trap 控制流之间的切换。 +当一个应用 Trap 到 S 态 OS 内核中进行进一步处理时, +其 Trap 控制流可以调用一个特殊的 ``__switch`` 函数。 +在 ``__switch`` 返回之后,Trap 控制流将继续从调用该函数的位置继续向下执行。 +而在调用 ``__switch`` 之后到返回前的这段时间里, +原 Trap 控制流 ``A`` 会先被暂停并被切换出去, CPU 转而运行另一个应用的 Trap 控制流 ``B`` 。 +``__switch`` 返回之后,原 Trap 控制流 ``A`` 才会从某一条 Trap 控制流 ``C`` 切换回来继续执行。 + +我们需要在 ``__switch`` 中保存 CPU 的某些寄存器,它们就是 **任务上下文** (Task Context)。 + +下面我们给出 ``__switch`` 的实现: + +.. code-block:: riscv + :linenos: + + # os/src/task/switch.S + + .altmacro + .macro SAVE_SN n + sd s\n, (\n+2)*8(a0) + .endm + .macro LOAD_SN n + ld s\n, (\n+2)*8(a1) + .endm + .section .text + .globl __switch + __switch: + # __switch( + # current_task_cx_ptr: *mut TaskContext, + # next_task_cx_ptr: *const TaskContext + # ) + # save kernel stack of current task + sd sp, 8(a0) + # save ra & s0~s11 of current execution + sd ra, 0(a0) + .set n, 0 + .rept 12 + SAVE_SN %n + .set n, n + 1 + .endr + # restore ra & s0~s11 of next execution + ld ra, 0(a1) + .set n, 0 + .rept 12 + LOAD_SN %n + .set n, n + 1 + .endr + # restore kernel stack of next task + ld sp, 8(a1) + ret + +它的两个参数分别是当前和即将被切换到的 Trap 控制流的 ``task_cx_ptr`` ,从 RISC-V 调用规范可知,它们分别通过寄存器 ``a0/a1`` 传入。 + +内核先把 ``current_task_cx_ptr`` 中包含的寄存器值逐个保存,再把 ``next_task_cx_ptr`` 中包含的寄存器值逐个恢复。 + +``TaskContext`` 里包含的寄存器有: + +.. code-block:: rust + :linenos: + + // os/src/task/context.rs + #[repr(C)] + pub struct TaskContext { + ra: usize, + sp: usize, + s: [usize; 12], + } + +``s0~s11`` 是被调用者保存寄存器, ``__switch`` 是用汇编编写的,编译器不会帮我们处理这些寄存器。 +保存 ``ra`` 很重要,它记录了 ``__switch`` 函数返回之后应该跳转到哪里继续执行。 + +我们将这段汇编代码 ``__switch`` 解释为一个 Rust 函数: + +.. code-block:: rust + :linenos: + + // os/src/task/switch.rs + + core::arch::global_asm!(include_str!("switch.S")); + + extern "C" { + pub fn __switch( + current_task_cx_ptr: *mut TaskContext, + next_task_cx_ptr: *const TaskContext); + } + +我们会调用该函数来完成切换功能,而不是直接跳转到符号 ``__switch`` 的地址。 +因此在调用前后,编译器会帮我们保存和恢复调用者保存寄存器。 diff --git a/_sources/chapter3/3multiprogramming.rst.txt b/_sources/chapter3/3multiprogramming.rst.txt new file mode 100644 index 0000000..5132c3a --- /dev/null +++ b/_sources/chapter3/3multiprogramming.rst.txt @@ -0,0 +1,317 @@ +管理多道程序 +========================================= + + +而内核为了管理任务,需要维护任务信息,相关内容包括: + +- 任务运行状态:未初始化、准备执行、正在执行、已退出 +- 任务控制块:维护任务状态和任务上下文 +- 任务相关系统调用:程序主动暂停 ``sys_yield`` 和主动退出 ``sys_exit`` + +yield 系统调用 +------------------------------------------------------------------------- + + +.. image:: multiprogramming.png + +上图描述了一种多道程序执行的典型情况。其中横轴为时间线,纵轴为正在执行的实体。 +开始时,蓝色应用向外设提交了一个请求,外设随即开始工作, +但是它要一段时间后才能返回结果。蓝色应用于是调用 ``sys_yield`` 交出 CPU 使用权, +内核让绿色应用继续执行。一段时间后 CPU 切换回蓝色应用,发现外设仍未返回结果, +于是再次 ``sys_yield`` 。直到第二次切换回蓝色应用,外设才处理完请求,于是蓝色应用终于可以向下执行了。 + +我们还会遇到很多其他需要等待其完成才能继续向下执行的事件,调用 ``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 + // yield 是 Rust 的关键字 + pub fn yield_() -> isize { sys_yield() } + +下文介绍内核应如何实现该系统调用。 + +任务控制块与任务运行状态 +--------------------------------------------------------- + +任务运行状态暂包括如下几种: + +.. code-block:: rust + :linenos: + + // os/src/task/task.rs + + #[derive(Copy, Clone, PartialEq)] + pub enum TaskStatus { + UnInit, // 未初始化 + Ready, // 准备运行 + Running, // 正在运行 + Exited, // 已退出 + } + +任务状态外和任务上下文一并保存在名为 **任务控制块** (Task Control Block) 的数据结构中: + +.. code-block:: rust + :linenos: + + // os/src/task/task.rs + + #[derive(Copy, Clone)] + pub struct TaskControlBlock { + pub task_status: TaskStatus, + pub task_cx: TaskContext, + } + + +任务控制块非常重要。在内核中,它就是应用的管理单位。后面的章节我们还会不断向里面添加更多内容。 + +任务管理器 +-------------------------------------- + +内核需要一个全局的任务管理器来管理这些任务控制块: + +.. code-block:: rust + + // os/src/task/mod.rs + + pub struct TaskManager { + num_app: usize, + inner: UPSafeCell, + } + + struct TaskManagerInner { + tasks: [TaskControlBlock; MAX_APP_NUM], + current_task: usize, + } + +这里用到了变量与常量分离的编程风格:字段 ``num_app`` 表示应用数目,它在 ``TaskManager`` 初始化后将保持不变; +而包裹在 ``TaskManagerInner`` 内的任务控制块数组 ``tasks``,以及正在执行的应用编号 ``current_task`` 会在执行过程中变化。 + +初始化 ``TaskManager`` 的全局实例 ``TASK_MANAGER``: + +.. 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: TaskContext::zero_init(), + task_status: TaskStatus::UnInit, + }; MAX_APP_NUM]; + for (i, t) in tasks.iter_mut().enumerate().take(num_app) { + t.task_cx = TaskContext::goto_restore(init_app_cx(i)); + t.task_status = TaskStatus::Ready; + } + TaskManager { + num_app, + inner: unsafe { + UPSafeCell::new(TaskManagerInner { + tasks, + current_task: 0, + }) + }, + } + }; + } + +- 第 5 行:调用 ``loader`` 子模块提供的 ``get_num_app`` 接口获取链接到内核的应用总数; +- 第 10~12 行:依次对每个任务控制块进行初始化,将其运行状态设置为 ``Ready`` ,并在它的内核栈栈顶压入一些初始化 + 上下文,然后更新它的 ``task_cx`` 。一些细节我们会稍后介绍。 +- 从第 14 行开始:创建 ``TaskManager`` 实例并返回。 + +.. note:: + + 关于 Rust 迭代器语法如 ``iter_mut/(a..b)`` ,及其方法如 ``enumerate/map/find/take``,请参考 Rust 官方文档。 + +实现 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() { + TASK_MANAGER.mark_current_suspended(); + TASK_MANAGER.run_next_task(); + } + + pub fn exit_current_and_run_next() { + TASK_MANAGER.mark_current_exited(); + TASK_MANAGER.run_next_task(); + } + + +它们都是先修改当前应用的运行状态,然后尝试切换到下一个应用。修改运行状态比较简单,实现如下: + +.. code-block:: rust + :linenos: + + // os/src/task/mod.rs + + impl TaskManager { + fn mark_current_suspended(&self) { + let mut inner = self.inner.exclusive_access(); + let current = inner.current_task; + inner.tasks[current].task_status = TaskStatus::Ready; + } + } + +以 ``mark_current_suspended`` 为例。首先获得里层 ``TaskManagerInner`` 的可变引用,然后修改任务控制块数组 ``tasks`` 中当前任务的状态。 + +再看 ``run_next_task`` 的实现: + +.. code-block:: rust + :linenos: + + // os/src/task/mod.rs + + impl TaskManager { + fn run_next_task(&self) { + if let Some(next) = self.find_next_task() { + let mut inner = self.inner.exclusive_access(); + let current = inner.current_task; + inner.tasks[next].task_status = TaskStatus::Running; + inner.current_task = next; + let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext; + let next_task_cx_ptr = &inner.tasks[next].task_cx as *const TaskContext; + drop(inner); + // before this, we should drop local variables that must be dropped manually + unsafe { + __switch(current_task_cx_ptr, next_task_cx_ptr); + } + // go back to user mode + } else { + panic!("All applications completed!"); + } + } + + fn find_next_task(&self) -> Option { + let inner = self.inner.exclusive_access(); + 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) + } + } + +``run_next_task`` 会调用 ``find_next_task`` 方法尝试寻找一个运行状态为 ``Ready`` 的应用并获得其 ID 。 +如果找不到, 说明所有应用都执行完了, ``find_next_task`` 将返回 ``None`` ,内核 panic 退出。 +如果能够找到下一个可运行应用,我们就调用 ``__switch`` 切换任务。 + +切换任务之前,我们要手动 drop 掉我们获取到的 ``TaskManagerInner`` 可变引用。 +因为函数还没有返回, ``inner`` 不会自动销毁。我们只有令 ``TASK_MANAGER`` 的 ``inner`` 字段回到未被借用的状态,下次任务切换时才能再借用。 + +我们可以总结一下应用的运行状态变化图: + +.. image:: fsm-coop.png + +第一次进入用户态 +------------------------------------------ + +我们在第二章中介绍过 CPU 第一次从内核态进入用户态的方法,只需在内核栈上压入构造好的 Trap 上下文, +然后 ``__restore`` 即可。本章要在此基础上做一些扩展。 + +在初始化任务控制块时,我们是这样做的: + +.. code-block:: rust + + // os/src/task/mod.rs + + for (i, t) in tasks.iter_mut().enumerate().take(num_app) { + t.task_cx = TaskContext::goto_restore(init_app_cx(i)); + t.task_status = TaskStatus::Ready; + } + +``init_app_cx`` 在 ``loader`` 子模块中定义,它向内核栈压入了一个 Trap 上下文,并返回压入 Trap 上下文后 ``sp`` 的值。 +这个 Trap 上下文的构造方式与第二章相同。 + +``goto_restore`` 保存传入的 ``sp``,并将 ``ra`` 设置为 ``__restore`` 的入口地址,构造任务上下文后返回。这样,任务管理器中各个应用的任务上下文就得到了初始化。 + +.. code-block:: rust + + // os/src/task/context.rs + + impl TaskContext { + pub fn goto_restore(kstack_ptr: usize) -> Self { + extern "C" { fn __restore(); } + Self { + ra: __restore as usize, + sp: kstack_ptr, + s: [0; 12], + } + } + } + +在 ``rust_main`` 中我们调用 ``task::run_first_task`` 来执行第一个应用: + +.. code-block:: rust + :linenos: + + // os/src/task/mod.rs + + fn run_first_task(&self) -> ! { + let mut inner = self.inner.exclusive_access(); + let task0 = &mut inner.tasks[0]; + task0.task_status = TaskStatus::Running; + let next_task_cx_ptr = &task0.task_cx as *const TaskContext; + drop(inner); + let mut _unused = TaskContext::zero_init(); + // before this, we should drop local variables that must be dropped manually + unsafe { + __switch(&mut _unused as *mut TaskContext, next_task_cx_ptr); + } + panic!("unreachable in run_first_task!"); + } + +我们显式声明了一个 ``_unused`` 变量,并将它的地址作为第一个参数传给 ``__switch`` , +声明此变量的意义仅仅是为了避免其他数据被覆盖。 + +在 ``__switch`` 中恢复 ``sp`` 后, ``sp`` 将指向 ``init_app_cx`` 构造的 Trap 上下文,后面就回到第二章的情况了。 +此外, ``__restore`` 的实现需要做出变化:它 **不再需要** 在开头 ``mv sp, a0`` 了。因为在 ``__switch`` 之后,``sp`` 就已经正确指向了我们需要的 Trap 上下文地址。 \ No newline at end of file diff --git a/_sources/chapter3/4time-sharing-system.rst.txt b/_sources/chapter3/4time-sharing-system.rst.txt new file mode 100644 index 0000000..9038da2 --- /dev/null +++ b/_sources/chapter3/4time-sharing-system.rst.txt @@ -0,0 +1,161 @@ +分时多任务系统 +=========================================================== + + +现代的任务调度算法基本都是抢占式的,它要求每个应用只能连续执行一段时间,然后内核就会将它强制性切换出去。 +一般将 **时间片** (Time Slice) 作为应用连续执行时长的度量单位,每个时间片可能在毫秒量级。 +简单起见,我们使用 **时间片轮转算法** (RR, Round-Robin) 来对应用进行调度。 + + +时钟中断与计时器 +------------------------------------------------------------------ + +实现调度算法需要计时。RISC-V 要求处理器维护时钟计数器 ``mtime``,还有另外一个 CSR ``mtimecmp`` 。 +一旦计数器 ``mtime`` 的值超过了 ``mtimecmp``,就会触发一次时钟中断。 + +运行在 M 特权级的 SEE 已经预留了相应的接口,基于此编写的 ``get_time`` 函数可以取得当前 ``mtime`` 计数器的值; + +.. code-block:: rust + + // os/src/timer.rs + + use riscv::register::time; + + pub fn get_time() -> usize { + time::read() + } + +在 10 ms 后设置时钟中断的代码如下: + +.. 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`` 调用,用来设置 ``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 MICRO_PER_SEC: usize = 1_000_000; + + pub fn get_time_us() -> usize { + time::read() / (CLOCK_FREQ / MICRO_PER_SEC) + } + + +``timer`` 子模块的 ``get_time_us`` 可以以微秒为单位返回当前计数器的值。 + +新增一个系统调用,使应用能获取当前的时间: + +.. code-block:: rust + :caption: 第三章新增系统调用(二) + + /// 功能:获取当前的时间,保存在 TimeVal 结构体 ts 中,_tz 在我们的实现中忽略 + /// 返回值:返回是否执行成功,成功则返回 0 + /// syscall ID:169 + fn sys_get_time(ts: *mut TimeVal, _tz: usize) -> isize; + +结构体 ``TimeVal`` 的定义如下,内核只需调用 ``get_time_us`` 即可实现该系统调用。 + +.. code-block:: rust + + // os/src/syscall/process.rs + + #[repr(C)] + pub struct TimeVal { + pub sec: usize, + pub usec: usize, + } + +RISC-V 架构中的嵌套中断问题 +----------------------------------- + +默认情况下,当 Trap 进入某个特权级之后,在 Trap 处理的过程中同特权级的中断都会被屏蔽。 + +- 当 Trap 发生时,``sstatus.sie`` 会被保存在 ``sstatus.spie`` 字段中,同时 ``sstatus.sie`` 置零, + 这也就在 Trap 处理的过程中屏蔽了所有 S 特权级的中断; +- 当 Trap 处理完毕 ``sret`` 的时候, ``sstatus.sie`` 会恢复到 ``sstatus.spie`` 内的值。 + +也就是说,如果不去手动设置 ``sstatus`` CSR ,在只考虑 S 特权级中断的情况下,是不会出现 **嵌套中断** (Nested Interrupt) 的。 + +.. note:: + + **嵌套中断与嵌套 Trap** + + 嵌套中断可以分为两部分:在处理一个中断的过程中又被同特权级/高特权级中断所打断。默认情况下硬件会避免前一部分, + 也可以通过手动设置来允许前一部分的存在;而从上面介绍的规则可以知道,后一部分则是无论如何设置都不可避免的。 + + 嵌套 Trap 则是指处理一个 Trap 过程中又再次发生 Trap ,嵌套中断算是嵌套 Trap 的一种。 + + +抢占式调度 +----------------------------------- + +有了时钟中断和计时器,抢占式调度就很容易实现了: + +.. 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 特权级时钟中断时,重新设置计时器, +调用 ``suspend_current_and_run_next`` 函数暂停当前应用并切换到下一个。 + +为了避免 S 特权级时钟中断被屏蔽,我们需要在执行第一个应用前调用 ``enable_timer_interrupt()`` 设置 ``sie.stie``, +使得 S 特权级时钟中断不会被屏蔽;再设置第一个 10ms 的计时器。 + +.. code-block:: rust + :linenos: + + // os/src/main.rs + + #[no_mangle] + pub fn rust_main() -> ! { + // ... + trap::enable_timer_interrupt(); + timer::set_next_trigger(); + // ... + } + + // os/src/trap/mod.rs + + use riscv::register::sie; + + pub fn enable_timer_interrupt() { + unsafe { sie::set_stimer(); } + } + +就这样,我们实现了时间片轮转任务调度算法。 ``power`` 系列用户程序可以验证我们取得的成果:这些应用并没有主动 yield, +内核仍能公平地把时间片分配给它们。 + diff --git a/_sources/chapter3/5exercise.rst.txt b/_sources/chapter3/5exercise.rst.txt new file mode 100644 index 0000000..5d126d8 --- /dev/null +++ b/_sources/chapter3/5exercise.rst.txt @@ -0,0 +1,133 @@ +chapter3练习 +======================================= + +Lab1 编程作业 +-------------------------------------- + +获取任务信息 +++++++++++++++++++++++++++ + +ch3 中,我们的系统已经能够支持多个任务分时轮流运行,我们希望引入一个新的系统调用 ``sys_task_info`` 以获取当前任务的信息,定义如下: + +.. code-block:: rust + + fn sys_task_info(ti: *mut TaskInfo) -> isize + +- syscall ID: 410 +- 查询当前正在执行的任务信息,任务信息包括任务控制块相关信息(任务状态)、任务使用的系统调用及调用次数、任务总运行时长(单位ms)。 + +.. code-block:: rust + + struct TaskInfo { + status: TaskStatus, + syscall_times: [u32; MAX_SYSCALL_NUM], + time: usize + } + +- 参数: + - ti: 待查询任务信息 +- 返回值:执行成功返回0,错误返回-1 +- 说明: + - 相关结构已在框架中给出,只需添加逻辑实现功能需求即可。 + - 在我们的实验中,系统调用号一定小于 500,所以直接使用一个长为 ``MAX_SYSCALL_NUM=500`` 的数组做桶计数。 + - 运行时间 time 返回系统调用时刻距离任务第一次被调度时刻的时长,也就是说这个时长可能包含该任务被其他任务抢占后的等待重新调度的时间。 + - 由于查询的是当前任务的状态,因此 TaskStatus 一定是 Running。(助教起初想设计根据任务 id 查询,但是既不好定义任务 id 也不好写测例,遂放弃 QAQ) + - 调用 ``sys_task_info`` 也会对本次调用计数。 +- 提示: + - 大胆修改已有框架!除了配置文件,你几乎可以随意修改已有框架的内容。 + - 程序运行时间可以通过调用 ``get_time()`` 获取,注意任务运行总时长的单位是 ms。 + - 系统调用次数可以考虑在进入内核态系统调用异常处理函数之后,进入具体系统调用函数之前维护。 + - 阅读 TaskManager 的实现,思考如何维护内核控制块信息(可以在控制块可变部分加入需要的信息)。 + + +实验要求 ++++++++++++++++++++++++++++++++++++++++++ + +- `lab1(os3)参考框架: `_ + +- 实验目录要求 + +.. code-block:: + + ├── os3(内核实现) + │   ├── Cargo.toml(配置文件) + │   └── src(所有内核的源代码放在 os/src 目录下) + │   ├── main.rs(内核主函数) + │   └── ... + ├── reports (不是 report) + │   ├── lab1.md/pdf + │   └── ... + ├── ... + + +- 通过所有测例: + + CI 使用的测例与本地相同,测试中,user 文件夹及其它与构建相关的文件将被替换,请不要试图依靠硬编码通过测试。 + + 在 ``os3`` 目录下,默认情况下,makefile 仅编译基础测例 (``BASE=1``),即无需修改框架即可正常运行的测例。 + 你需要在编译时指定 ``BASE=0`` 控制框架仅编译实验测例(在 os 目录执行 ``make run BASE=0``), + 或指定 ``BASE=2`` 控制框架同时编译基础测例和实验测例。 + +.. note:: + + 你的实现只需且必须通过测例,建议读者感到困惑时先检查测例。 + + +简答作业 +-------------------------------------------- + +1. 正确进入 U 态后,程序的特征还应有:使用 S 态特权指令,访问 S 态寄存器后会报错。 + 请同学们可以自行测试这些内容 (运行 `Rust 三个 bad 测例 (ch2b_bad_*.rs) `_ , + 注意在编译时至少需要指定 ``LOG=ERROR`` 才能观察到内核的报错信息) , + 描述程序出错行为,同时注意注明你使用的 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 态是哪一条指令发生的? + +报告要求 +------------------------------- + +- 简单总结你实现的功能(200字以内,不要贴代码)。 +- 完成问答题。 +- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 diff --git a/_sources/chapter3/index.rst.txt b/_sources/chapter3/index.rst.txt new file mode 100644 index 0000000..d71d5a5 --- /dev/null +++ b/_sources/chapter3/index.rst.txt @@ -0,0 +1,14 @@ +.. _link-chapter3: + +第三章:多道程序与分时多任务 +============================================== + +.. toctree:: + :maxdepth: 4 + + 0intro + 1multi-loader + 2task-switching + 3multiprogramming + 4time-sharing-system + 5exercise diff --git a/_sources/chapter4/0intro.rst.txt b/_sources/chapter4/0intro.rst.txt new file mode 100644 index 0000000..0f445de --- /dev/null +++ b/_sources/chapter4/0intro.rst.txt @@ -0,0 +1,139 @@ +引言 +============================== + +本章导读 +------------------------------- + +本章中内核将实现虚拟内存机制,这注定是一趟艰难的旅程。 + + +实践体验 +----------------------- + +.. note:: + + 基于github classroom的开发方式 + + 基于github classroom,可方便建立开发用的git repository,并可基于github的 codespace (在线版ubuntu +vscode)在线开发使用。整个开发环境仅仅需要一个网络浏览器。 + + 1. 在网络浏览器中用自己的 github id 登录 github.com + 2. 接收 `第二个实验(os4)的github classroom在线邀请 `_ ,根据提示一路选择OK即可。 + 3. 完成第二步后,你的第二个实验的 github repository 会被自动建立好,点击此github repository的链接,就可看到你要完成的第一个实验了。 + 4. 在你的第二个实验的网页的中上部可以看到一个醒目的 `code` 绿色按钮,点击后,可以进一步看到 `codespace` 标签和醒目的 `create codesapce on main` 绿色按钮。请点击这个绿色按钮,就可以进入到在线的ubuntu +vscode环境中 + 5. 再按照下面的环境安装提示在vscode的 `console` 中安装配置开发环境:rustc,qemu等工具。 + 6. 在vscode的 `console` 中执行 `make setupclassroom_test4` (该命令仅执行一次)配置githubclassroom 自动评分功能。 + 7. 然后就可以基于在线vscode进行开发、运行、提交等完整的实验过程了。 + + 上述的3,4,5步不是必须的,你也可以线下本地开发。 + +本章应用运行起来效果与上一章基本一致。 + +获取本章代码: + +.. code-block:: console + + $ git clone https://github.com/LearningOS/rust-based-os-comp2022.git + $ cd rust-based-os-comp2022/ + $ make setupclassroom_test4 //注意:这一步很重要,是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次,以后一般就不用执行了,除非 .github/workflows/classroom.yml发生了变化。 + + +在 qemu 模拟器上运行 `lab2(os4)参考框架: `_ : + +.. code-block:: console + + $ cd os4-ref + $ make run + + +`lab2(os4)参考框架: `_ +-------------------------------------------------------------------------------------------------------------------- + +.. code-block:: + :linenos: + + ├── os4-ref + │   ├── ... + │   └── src + │   ├── ... + │   ├── config.rs(修改:新增一些内存管理的相关配置) + │   ├── linker.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 以及其他内容) + │   ├── syscall + │   │   ├── fs.rs(修改:基于地址空间的 sys_write 实现) + │   │   ├── mod.rs + │   │   └── process.rs + │   ├── task + │   │   ├── context.rs(修改:构造一个跳转到不同位置的初始任务上下文) + │   │   ├── mod.rs(修改,详见文档) + │   │   ├── switch.rs + │   │   ├── switch.S + │   │   └── task.rs(修改,详见文档) + │   └── trap + │   ├── context.rs(修改:在 Trap 上下文中加入了更多内容) + │   ├── mod.rs(修改:基于地址空间修改了 Trap 机制,详见文档) + │   └── trap.S(修改:基于地址空间修改了 Trap 上下文保存与恢复汇编代码) + └── user + ├── build.py(编译时不再使用) + ├── ... + └── src + ├── linker.ld(修改:将所有应用放在各自地址空间中固定的位置) + └── ... + + cloc os4-ref + ------------------------------------------------------------------------------- + Language files blank comment code + ------------------------------------------------------------------------------- + Rust 26 138 56 1526 + Assembly 3 3 26 86 + make 1 11 4 36 + TOML 1 2 1 13 + ------------------------------------------------------------------------------- + SUM: 31 154 87 1661 + ------------------------------------------------------------------------------- + + +.. 本章代码导读 +.. ----------------------------------------------------- + +.. 本章涉及的代码量相对多了起来,也许同学们不知如何从哪里看起或从哪里开始尝试实验。这里简要介绍一下“头甲龙”操作系统的大致开发过程。 + +.. 我们先从简单的地方入手,那当然就是先改进应用程序了。具体而言,主要就是把 ``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() +.. )); +.. } + +.. 完成到这里,我们就可以使能分页机制了。且我们应该有更加方便的机制来给支持应用运行。在本章之前,都是把应用程序的所有元数据丢弃从而转换成二进制格式来执行,这其实把编译器生成的 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`` ;还有就是需要对来自用户态和内核态的 Trap 分别进行处理,这需要看看 ``os/src/trap/mod.rs`` 和 :ref:`跳板的实现 ` 中的讲解。 + +.. 另外一个挑战是,在内核地址空间中执行的内核代码常常需要读写应用地址空间的数据,这无法简单的通过一次访存交给 MMU 来解决,而是需要手动查应用地址空间的页表。在访问应用地址空间中的一块跨多个页数据的时候还需要注意处理边界条件。可以参考 ``os/src/syscall/fs.rs``、 ``os/src/mm/page_table.rs`` 中的 ``translated_byte_buffer`` 函数的实现。 + +.. 实现到这,应该就可以给应用程序运行提供一个方便且安全的虚拟地址空间了。 \ No newline at end of file diff --git a/_sources/chapter4/3sv39-implementation-1.rst.txt b/_sources/chapter4/3sv39-implementation-1.rst.txt new file mode 100644 index 0000000..79f6965 --- /dev/null +++ b/_sources/chapter4/3sv39-implementation-1.rst.txt @@ -0,0 +1,216 @@ +实现 SV39 多级页表机制(上) +======================================================== + +.. note:: + + 背景知识: `地址空间 `_ + + 背景知识: `SV39 多级页表原理 `_ + + +我们将在内核实现 RV64 架构 SV39 分页机制。由于内容过多,分成两个小节。 + +虚拟地址和物理地址 +------------------------------------------------------ + +内存控制相关的CSR寄存器 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +默认情况下 MMU 未被使能,此时无论 CPU 处于哪个特权级,访存的地址都将直接被视作物理地址。 +可以通过修改 S 特权级的 ``satp`` CSR 来启用分页模式,此后 S 和 U 特权级的访存地址会被视为虚拟地址,经过 MMU 的地址转换获得对应物理地址,再通过它来访问物理内存。 + +.. image:: satp.png + :name: satp-layout + +上图是 RV64 架构下 ``satp`` 的字段分布。当 ``MODE`` 设置为 0 的时候,所有访存都被视为物理地址;而设置为 8 +时,SV39 分页机制被启用,所有 S/U 特权级的访存被视为一个 39 位的虚拟地址,MMU 会将其转换成 56 位的物理地址;如果转换失败,则会触发异常。 + + +地址格式与组成 +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: sv39-va-pa.png + +我们采用分页管理,单个页面的大小设置为 :math:`4\text{KiB}` ,每个虚拟页面和物理页帧都按 4 KB 对齐。 +:math:`4\text{KiB}` 需要用 12 位字节地址来表示,因此虚拟地址和物理地址都被分成两部分: +它们的低 12 位被称为 **页内偏移** (Page Offset) 。虚拟地址的高 27 位,即 :math:`[38:12]` 为它的虚拟页号 VPN; +物理地址的高 44 位,即 :math:`[55:12]` 为它的物理页号 PPN。页号可以用来定位一个虚拟/物理地址属于哪一个虚拟页面/物理页帧。 + +地址转换是以页为单位进行的,转换前后地址页内偏移部分不变。MMU 只是从虚拟地址中取出 27 位虚拟页号, +在页表中查到其对应的物理页号,如果找到,就将得到的 44 位的物理页号与 12 位页内偏移拼接到一起,形成 56 位物理地址。 + +.. note:: + + **RV64 架构中虚拟地址为何只有 39 位?** + + 虚拟地址长度确实应该和位宽一致为 64 位,但是在启用 SV39 分页模式下,只有低 39 位是真正有意义的。 + SV39 分页模式规定 64 位虚拟地址的 :math:`[63:39]` 这 25 位必须和第 38 位相同,否则 MMU 会直接认定它是一个 + 不合法的虚拟地址。。 + + 也就是说,所有 :math:`2^{64}` 个虚拟地址中,只有最低的 :math:`256\text{GiB}` (当第 38 位为 0 时) + 以及最高的 :math:`256\text{GiB}` (当第 38 位为 1 时)是可能通过 MMU 检查的。 + +地址相关的数据结构抽象与类型定义 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +实现页表之前,先将地址和页号的概念抽象为 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 类型声明,它们都是 usize 的一种简单包装。 +将它们各自抽象出来而不是直接使用 usize,是为了在 Rust 编译器的帮助下进行多种方便且安全的 **类型转换** (Type Convertion) 。 + +这些类型本身可以和 usize 之间互相转换,地址和页号之间也可以相互转换。以物理地址和物理页号之间的转换为例: + +.. 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) } + } + +页表项的数据结构抽象与类型定义 +----------------------------------------- + +.. 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`` 封装成一个标志位的集合类型,支持一些常见的集合 +运算。 + +接下来我们实现页表项 ``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,来让这个类型以值语义赋值/传参的时候 + 不会发生所有权转移,而是拷贝一份新的副本。 +- 第 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() + } + } + +这里相当于判断两个集合的交集是否为空。 diff --git a/_sources/chapter4/4sv39-implementation-2.rst.txt b/_sources/chapter4/4sv39-implementation-2.rst.txt new file mode 100644 index 0000000..ce6df02 --- /dev/null +++ b/_sources/chapter4/4sv39-implementation-2.rst.txt @@ -0,0 +1,447 @@ +实现 SV39 多级页表机制(下) +======================================================== + +物理页帧管理 +----------------------------------- + +可用物理页的分配与回收 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +首先,我们需要知道物理内存的哪一部分是可用的。在 ``os/src/linker.ld`` 中,我们用符号 ``ekernel`` 指明了 +内核数据的终止物理地址,在它之后的物理内存都是可用的。而在 ``config`` 子模块中: + +.. code-block:: rust + + // os/src/config.rs + + pub const MEMORY_END: usize = 0x80800000; + +我们硬编码整块物理内存的终止物理地址为 ``0x80800000`` 。 而物理内存的起始物理地址为 ``0x80000000`` , +意味着我们将可用内存大小设置为 :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``,并在正式分配物理页帧之前将 ``FRAME_ALLOCATOR`` 初始化,见 ``os/src/mm/frame_allocator.rs``。 + +分配/回收物理页帧的接口 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +公开给其他子模块调用的分配/回收物理页帧的接口: + +.. code-block:: rust + + // os/src/mm/frame_allocator.rs + + pub fn frame_alloc() -> Option { + FRAME_ALLOCATOR + .exclusive_access() + .alloc() + .map(FrameTracker::new) + } + + fn frame_dealloc(ppn: PhysPageNum) { + FRAME_ALLOCATOR.exclusive_access().dealloc(ppn); + } + + +可以发现, ``frame_alloc`` 的返回值类型并不是 ``FrameAllocator`` 要求的物理页号 ``PhysPageNum`` ,而是将其 +进一步包装为一个 ``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`` 我们就将它控制的物理页帧回收以供后续使用了。 + +最后做一个小结:从其他模块的视角看来,物理页帧分配的接口是调用 ``frame_alloc`` 函数得到一个 ``FrameTracker`` +(如果物理内存还有剩余),它就代表了一个物理页帧,当它的生命周期结束之后它所控制的物理页帧将被自动回收。 + +多级页表实现 +----------------------------------- + + +页表基本数据结构与访问接口 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +我们知道,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: + +在这些操作的过程中,我们自然需要访问或修改多级页表节点的内容。每个节点都被保存在一个物理页帧中,在多级页表的架构中,我们以 +一个节点被存放在的物理页帧的物理页号作为指针指向该节点,这意味着,对于每个节点来说,一旦我们知道了指向它的物理页号,我们 +就能够修改这个节点的内容。 + +.. _term-identical-mapping: + +这就需要我们提前扩充多级页表维护的映射,使得对于每一个对应于某一特定物理页帧的物理页号 ``ppn`` ,均存在一个虚拟页号 +``vpn`` 能够映射到它,而且要能够较为简单的针对一个 ``ppn`` 找到某一个能映射到它的 ``vpn`` 。这里我们采用一种最 +简单的 **恒等映射** (Identical Mapping) ,也就是说对于物理内存上的每个物理页帧,我们都在多级页表中用一个与其 +物理页号相等的虚拟页号映射到它。当我们想针对物理页号构造一个能映射到它的虚拟页号的时候,也只需使用一个和该物理页号 +相等的虚拟页号即可。 + + +内核中访问物理页帧的方法 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. _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`` 的解引用访问它指向的数据,而是可以像一个 +正常的可变引用一样直接访问。 + + +建立和拆除虚实地址映射关系 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +接下来介绍建立和拆除虚实地址映射关系的 ``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/_sources/chapter4/5kernel-app-spaces.rst.txt b/_sources/chapter4/5kernel-app-spaces.rst.txt new file mode 100644 index 0000000..3eb3da0 --- /dev/null +++ b/_sources/chapter4/5kernel-app-spaces.rst.txt @@ -0,0 +1,586 @@ +内核与应用的地址空间 +================================================ + + +本节我们就在内核中通过基于页表的各种数据结构实现地址空间的抽象。 + +实现地址空间抽象 +------------------------------------------ + + +逻辑段:一段连续地址的虚拟内存 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +我们以逻辑段 ``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/_sources/chapter4/6multitasking-based-on-as.rst.txt b/_sources/chapter4/6multitasking-based-on-as.rst.txt new file mode 100644 index 0000000..24ec9e7 --- /dev/null +++ b/_sources/chapter4/6multitasking-based-on-as.rst.txt @@ -0,0 +1,684 @@ +基于地址空间的分时多任务 +============================================================== + +本节我们介绍如何基于地址空间抽象来实现第三章的分时多任务系统。 + +建立并开启基于分页模式的虚拟地址空间 +-------------------------------------------- + +当 SBI 实现(本项目中基于 RustSBI)初始化完成后, CPU 将跳转到内核入口点并在 S 特权级上执行,此时还并没有开启分页模式 +,内核的每一次访存仍被视为一个物理地址直接访问物理内存。而在开启分页模式之后,内核的代码在访存的时候只能看到内核地址空间, +此时每次访存将被视为一个虚拟地址且需要通过 MMU 基于内核地址空间的多级页表的地址转换。这两种模式之间的过渡在内核初始化期间 +完成。 + +创建内核地址空间 +^^^^^^^^^^^^^^^^^^^^^^^^ + + +我们创建内核地址空间的全局实例: + +.. code-block:: rust + + // os/src/mm/memory_set.rs + + lazy_static! { + pub static ref KERNEL_SPACE: Arc> = Arc::new(unsafe { + UPSafeCell::new(MemorySet::new_kernel() + )}); + } + +从之前对于 ``lazy_static!`` 宏的介绍可知, ``KERNEL_SPACE`` 在运行期间它第一次被用到时才会实际进行初始化,而它所 +占据的空间则是编译期被放在全局数据段中。 ``Arc>`` 同时带来 ``Arc`` 提供的共享 +引用,和 ``UPSafeCell`` 提供的互斥访问。 + +在 ``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.exclusive_access().activate(); + } + +可以看到,我们最先进行了全局动态内存分配器的初始化,因为接下来马上就要用到 Rust 的堆数据结构。接下来我们初始化物理页帧 +管理器(内含堆数据结构 ``Vec`` )使能可用物理页帧的分配和回收能力。最后我们创建内核地址空间并让 CPU 开启分页模式, +MMU 在地址转换的时候使用内核的多级页表,这一切均在一行之内做到: + +- 首先,我们引用 ``KERNEL_SPACE`` ,这是它第一次被使用,就在此时它会被初始化,调用 ``MemorySet::new_kernel`` + 创建一个内核地址空间并使用 ``Arc>`` 包裹起来; + +- 最然后,我们调用 ``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); + core::arch::asm!("sfence.vma"); + } + } + } + + ``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 就不会看到快表中已经 +过期的键值对了。 + +.. _term-trampoline: + +跳板的实现 +------------------------------------ + +上一小节我们看到无论是内核还是应用的地址空间,最高的虚拟页面都是一个跳板。同时应用地址空间的次高虚拟页面还被设置为用来 +存放应用的 Trap 上下文。那么跳板究竟起什么作用呢?为何不直接把 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 + 的失效问题。但是这种设计方式也有缺点:即内核的逻辑段需要在每个应用的地址空间内都映射一次,这会带来一些无法忽略的 + 内存占用开销,并显著限制了嵌入式平台的任务并发数。此外,这种做法无法应对处理器的 `熔断 + (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.ld`` 的地址布局描述,设定电子指令的地址,并计算二者地址偏移量 +并让跳转指令的实际效果为当前 pc 自增这个偏移量。但实际上我们知道由于我们设计的缘故,这条跳转指令在被执行的时候, +它的虚拟地址被操作系统内核设置在地址空间中的最高页面之内,加上这个偏移量并不能正确的得到 ``trap_handler`` 的入口地址。 + +**问题的本质可以概括为:跳转指令实际被执行时的虚拟地址和在编译器/汇编器/链接器进行后端代码生成和链接形成最终机器码时设置此指令的地址是不同的。** + +加载和执行应用程序 +------------------------------------ + +扩展任务控制块 +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +为了让应用在运行时有一个安全隔离且符合编译器给应用设定的地址空间布局的虚拟地址空间,操作系统需要对任务进行更多的管理,所以任务控制块相比第三章也包含了更多内容: + +.. code-block:: rust + :linenos: + :emphasize-lines: 6,7,8 + + // os/src/task/task.rs + + pub struct TaskControlBlock { + pub task_status: TaskStatus, + pub task_cx: TaskContext, + 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 + .exclusive_access() + .insert_framed_area( + kernel_stack_bottom.into(), + kernel_stack_top.into(), + MapPermission::R | MapPermission::W, + ); + let task_control_block = Self { + task_status, + task_cx: TaskContext::goto_trap_return(kernel_stack_top), + 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.exclusive_access().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: + +- 我们在应用的内核栈顶压入一个跳转到 ``trap_return`` 而不是 ``__restore`` 的任务上下文, + 这主要是为了能够支持对该应用的启动并顺利切换到用户地址空间执行。在构造方式上,只是将 ra 寄存器的值设置为 + ``trap_return`` 的地址。 ``trap_return`` 是我们后面要介绍的新版的 Trap 处理的一部分。 +- 初始化该应用的 Trap 上下文,由于它是在应用地址空间而不是在内核地址空间中,我们只能手动查页表找到 + Trap 上下文实际被放在的物理页帧,再获得在用户空间的 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`` 泛型函数的特定版本实现。 + + .. 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 = { + info!("init TASK_MANAGER"); + let num_app = get_num_app(); + info!("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: unsafe { + UPSafeCell::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 位的值到一个通用寄存器。 + +为了方便后续的实现,全局任务管理器还需要提供关于当前应用与地址空间有关的一些信息。通过 ``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 { + core::arch::asm!( + "fence.i", + "jr {restore_va}", + restore_va = in(reg) restore_va, + in("a0") trap_cx_ptr, + in("a1") user_satp, + options(noreturn) + ); + } + 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`` 都是指编译器在链接时看到的内核内存布局中的地址。我们使用 ``jr`` 指令完成了跳转的任务。 +- 在开始执行应用之前,我们需要使用 ``fence.i`` 指令清空指令缓存 i-cache 。这是因为,在内核中进行的一些操作 + 可能导致一些原先存放某个应用代码的物理页帧如今用来存放数据或者是其他应用的代码,i-cache 中可能还保存着该物理页帧的 + 错误快照。因此我们直接将整个 i-cache 清空避免错误。 + +改进 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`` 然后输出即可。 + diff --git a/_sources/chapter4/7exercise.rst.txt b/_sources/chapter4/7exercise.rst.txt new file mode 100644 index 0000000..2bfe8e6 --- /dev/null +++ b/_sources/chapter4/7exercise.rst.txt @@ -0,0 +1,113 @@ +chapter4练习 +============================================ + +Lab2 编程作业 +--------------------------------------------- + +重写 sys_get_time 和 sys_task_info +++++++++++++++++++++++++++++++++++++++++++++ + +引入虚存机制后,原来内核的 sys_get_time 和 sys_task_info 函数实现就无效了。请你重写这个函数,恢复其正常功能。 + +mmap 和 munmap 匿名映射 +++++++++++++++++++++++++++++++++++++++++++++ + +`mmap `_ 在 Linux 中主要用于在内存中映射文件, +本次实验简化它的功能,仅用于申请内存。 + +请实现 mmap 和 munmap 系统调用,mmap 定义如下: + + +.. code-block:: rust + + fn sys_mmap(start: usize, len: usize, port: usize) -> isize + +- syscall ID:222 +- 申请长度为 len 字节的物理内存(不要求实际物理内存位置,可以随便找一块),将其映射到 start 开始的虚存,内存页属性为 port +- 参数: + - start 需要映射的虚存起始地址,要求按页对齐 + - len 映射字节长度,可以为 0 + - port:第 0 位表示是否可读,第 1 位表示是否可写,第 2 位表示是否可执行。其他位无效且必须为 0 +- 返回值:执行成功则返回 0,错误返回 -1 +- 说明: + - 为了简单,目标虚存区间要求按页对齐,len 可直接按页向上取整,不考虑分配失败时的页回收。 +- 可能的错误: + - start 没有按页大小对齐 + - port & !0x7 != 0 (port 其余位必须为0) + - port & 0x7 = 0 (这样的内存无意义) + - [start, start + len) 中存在已经被映射的页 + - 物理内存不足 + +munmap 定义如下: + +.. code-block:: rust + + fn sys_munmap(start: usize, len: usize) -> isize + +- syscall ID:215 +- 取消到 [start, start + len) 虚存的映射 +- 参数和返回值请参考 mmap +- 说明: + - 为了简单,参数错误时不考虑内存的恢复和回收。 +- 可能的错误: + - [start, start + len) 中存在未被映射的虚存。 + +tips: + +- 一定要注意 mmap 是的页表项,注意 riscv 页表项的格式与 port 的区别。 +- 你增加 PTE_U 了吗? + +实验要求 +++++++++++++++++++++++++++++++++++++++++++ + +- `lab2(os4)参考框架: `_ +- 在 ``os4`` 目录下,实现 mmap 和 munmap 两个系统调用,通过所有测例。 +- 报告命名 lab2.md,位于 ``reports`` 目录下 + +TIPS:注意 port 参数的语义,它与内核定义的 MapPermission 有明显不同! + +问答作业 +------------------------------------------------- + +1. 请列举 SV39 页表页表项的组成,描述其中的标志位有何作用? + +2. 缺页 + 缺页指的是进程访问页面时页面不在页表中或在页表中无效的现象,此时 MMU 将会返回一个中断, + 告知 os 进程内存访问出了问题。os 选择填补页表并重新执行异常指令或者杀死进程。 + + - 请问哪些异常可能是缺页导致的? + - 发生缺页时,描述相关重要寄存器的值,上次实验描述过的可以简略。 + + 缺页有两个常见的原因,其一是 Lazy 策略,也就是直到内存页面被访问才实际进行页表操作。 + 比如,一个程序被执行时,进程的代码段理论上需要从磁盘加载到内存。但是 os 并不会马上这样做, + 而是会保存 .text 段在磁盘的位置信息,在这些代码第一次被执行时才完成从磁盘的加载操作。 + + - 这样做有哪些好处? + + 其实,我们的 mmap 也可以采取 Lazy 策略,比如:一个用户进程先后申请了 10G 的内存空间, + 然后用了其中 1M 就直接退出了。按照现在的做法,我们显然亏大了,进行了很多没有意义的页表操作。 + + - 处理 10G 连续的内存页面,对应的 SV39 页表大致占用多少内存 (估算数量级即可)? + - 请简单思考如何才能实现 Lazy 策略,缺页时又如何处理?描述合理即可,不需要考虑实现。 + + 缺页的另一个常见原因是 swap 策略,也就是内存页面可能被换到磁盘上了,导致对应页面失效。 + + - 此时页面失效如何表现在页表项(PTE)上? + +3. 双页表与单页表 + + 为了防范侧信道攻击,我们的 os 使用了双页表。但是传统的设计一直是单页表的,也就是说, + 用户线程和对应的内核线程共用同一张页表,只不过内核对应的地址只允许在内核态访问。 + (备注:这里的单/双的说法仅为自创的通俗说法,并无这个名词概念,详情见 `KPTI `_ ) + + - 在单页表情况下,如何更换页表? + - 单页表情况下,如何控制用户态无法访问内核页面?(tips:看看上一题最后一问) + - 单页表有何优势?(回答合理即可) + - 双页表实现下,何时需要更换页表?假设你写一个单页表操作系统,你会选择何时更换页表(回答合理即可)? + +报告要求 +-------------------------------------------------------- + +- 简单总结你实现的功能(200字以内,不要贴代码)。 +- 完成问答题。 +- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 diff --git a/_sources/chapter4/index.rst.txt b/_sources/chapter4/index.rst.txt new file mode 100644 index 0000000..715e8e2 --- /dev/null +++ b/_sources/chapter4/index.rst.txt @@ -0,0 +1,12 @@ +第四章:地址空间 +============================================== + +.. toctree:: + :maxdepth: 4 + + 0intro + 3sv39-implementation-1 + 4sv39-implementation-2 + 5kernel-app-spaces + 6multitasking-based-on-as + 7exercise diff --git a/_sources/chapter5/0intro.rst.txt b/_sources/chapter5/0intro.rst.txt new file mode 100644 index 0000000..84027be --- /dev/null +++ b/_sources/chapter5/0intro.rst.txt @@ -0,0 +1,187 @@ +引言 +=========================================== + +本章导读 +------------------------------------------- + +.. note:: + + 基于github classroom的开发方式 + + 基于github classroom,可方便建立开发用的git repository,并可基于github的 codespace (在线版ubuntu +vscode)在线开发使用。整个开发环境仅仅需要一个网络浏览器。 + + 1. 在网络浏览器中用自己的 github id 登录 github.com + 2. 接收 `第三个实验(os5)的github classroom在线邀请 `_ ,根据提示一路选择OK即可。 + 3. 完成第二步后,你的第三个实验的 github repository 会被自动建立好,点击此github repository的链接,就可看到你要完成的第一个实验了。 + 4. 在你的第三个实验的网页的中上部可以看到一个醒目的 `code` 绿色按钮,点击后,可以进一步看到 `codespace` 标签和醒目的 `create codesapce on main` 绿色按钮。请点击这个绿色按钮,就可以进入到在线的ubuntu +vscode环境中 + 5. 再按照下面的环境安装提示在vscode的 `console` 中安装配置开发环境:rustc,qemu等工具。 + 6. 在vscode的 `console` 中执行 `make setupclassroom_test5` (该命令仅执行一次)配置githubclassroom 自动评分功能。 + 7. 然后就可以基于在线vscode进行开发、运行、提交等完整的实验过程了。 + + 上述的3,4,5步不是必须的,你也可以线下本地开发。 + +我们将开发一个用户 **终端** (Terminal) 或 **命令行** (Command Line Application, 俗称 **Shell** ) , +形成用户与操作系统进行交互的命令行界面 (Command Line Interface)。 + +为此,我们要对任务建立新的抽象: **进程** ,并实现若干基于 **进程** 的强大系统调用。 + +.. note:: + + **任务和进程的关系与区别** + + 第三章提到的 **任务** 是这里提到的 **进程** 的初级阶段,与任务相比,进程能在运行中创建 **子进程** 、 + 用新的 **程序** 内容覆盖已有的 **程序** 内容、可管理更多物理或虚拟 **资源** 。 + +实践体验 +------------------------------------------- + +获取本章代码: + +.. code-block:: console + + $ git clone https://github.com/LearningOS/rust-based-os-comp2022.git + $ cd rust-based-os-comp2022/ + $ make setupclassroom_test5 //注意:这一步很重要,是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次,以后一般就不用执行了,除非 .github/workflows/classroom.yml发生了变化。 + + +在 qemu 模拟器上运行`lab3(os5)参考框架: `_ : + +.. code-block:: console + + $ cd os5-ref + $ make run + +待内核初始化完毕之后,将在屏幕上打印可用的应用列表并进入shell程序: + +.. code-block:: + + [rustsbi] RustSBI version 0.2.0-alpha.4 + .______ __ __ _______.___________. _______..______ __ + | _ \ | | | | / | | / || _ \ | | + | |_) | | | | | | (----`---| |----`| (----`| |_) || | + | / | | | | \ \ | | \ \ | _ < | | + | |\ \----.| `--' |.----) | | | .----) | | |_) || | + | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| + + [rustsbi] Implementation: RustSBI-QEMU Version 0.0.1 + [rustsbi-dtb] Hart count: cluster0 with 1 cores + [rustsbi] misa: RV64ACDFIMSU + [rustsbi] mideleg: ssoft, stimer, sext (0x222) + [rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab) + [rustsbi] pmp0: 0x80000000 ..= 0x800fffff (rwx) + [rustsbi] pmp1: 0x80000000 ..= 0x807fffff (rwx) + [rustsbi] pmp2: 0x0 ..= 0xffffffffffffff (---) + [rustsbi] enter supervisor 0x80200000 + [kernel] Hello, world! + /**** APPS **** + ch2b_bad_address + ch2b_bad_instructions + ch2b_bad_register + ch2b_hello_world + ch2b_power_3 + ch2b_power_5 + ch2b_power_7 + ch3b_sleep + ch3b_sleep1 + ch3b_yield0 + ch3b_yield1 + ch3b_yield2 + ch5b_exit + ch5b_forktest + ch5b_forktest2 + ch5b_forktest_simple + ch5b_forktree + ch5b_initproc + ch5b_user_shell + **************/ + Rust user shell + >> + +可以通过输入ch5b开头的应用来测试ch5实现的fork等功能: + +.. code-block:: + + >> ch5b_forktest_simple + + sys_wait without child process test passed! + parent start, pid = 2! + ready waiting on parent process! + hello child process! + child process pid = 3, exit code = 100 + Shell: Process 2 exited with code 0 + +`lab3(os5)参考框架: `_ +---------------------------------------------------------------------------------------------------------------------- + +.. code-block:: + :linenos: + + ├── os5-ref +    ├── build.rs(修改:基于应用名的应用构建器) +    ├── ... +    └── src +    ├── ... +    ├── loader.rs(修改:基于应用名的应用加载器) +    ├── main.rs(修改) +    ├── mm(修改:为了支持本章的系统调用对此模块做若干增强) +    │   ├── address.rs +    │   ├── frame_allocator.rs +    │   ├── heap_allocator.rs +    │   ├── memory_set.rs +    │   ├── mod.rs +    │   └── page_table.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(新增:处理器管理结构 ``Processor`` ,为上一章任务管理器功能的一部分) +    │   ├── switch.rs +    │   ├── switch.S +    │   └── task.rs(修改:支持进程机制的任务控制块) +    └── trap +    ├── context.rs +    ├── mod.rs(修改:对于系统调用的实现进行修改以支持进程系统调用) +    └── trap.S + + cloc os + ------------------------------------------------------------------------------- + Language files blank comment code + ------------------------------------------------------------------------------- + Rust 29 180 138 2049 + Assembly 4 20 26 229 + make 1 11 4 36 + TOML 1 2 1 13 + ------------------------------------------------------------------------------- + SUM: 35 213 169 2327 + ------------------------------------------------------------------------------- + + +.. 本章代码导读 +.. ----------------------------------------------------- + +.. 本章的第一小节 :doc:`/chapter5/1process` 介绍了操作系统中经典的进程概念,并描述我们将要实现的参考自 Unix 系内核并经过简化的精简版进程模型。在该模型下,若想对进程进行管理,实现创建、退出等操作,核心就在于 ``fork/exec/waitpid`` 三个系统调用。 + +.. 首先我们修改运行在应用态的应用软件,它们均放置在 ``user`` 目录下。在新增系统调用的时候,需要在 ``user/src/lib.rs`` 中新增一个 ``sys_*`` 的函数,它的作用是将对应的系统调用按照与内核约定的 ABI 在 ``syscall`` 中转化为一条用于触发系统调用的 ``ecall`` 的指令;还需要在用户库 ``user_lib`` 将 ``sys_*`` 进一步封装成一个应用可以直接调用的与系统调用同名的函数。通过这种方式我们新增三个进程模型中核心的系统调用 ``fork/exec/waitpid`` ,一个查看进程 PID 的系统调用 ``getpid`` ,还有一个允许应用程序获取用户键盘输入的 ``read`` 系统调用。 + +.. 基于进程模型,我们在 ``user/src/bin`` 目录下重新实现了一组应用程序。其中有两个特殊的应用程序:用户初始程序 ``initproc.rs`` 和 shell 程序 ``user_shell.rs`` ,可以认为它们位于内核和其他应用程序之间的中间层提供一些基础功能,但是它们仍处于应用层。前者会被内核唯一自动加载、也是最早加载并执行,后者则负责从键盘接收用户输入的应用名并执行对应的应用。剩下的应用从不同层面测试了我们内核实现的正确性,读者可以自行参考。值得一提的是, ``usertests`` 可以按照顺序执行绝大部分应用,会在测试的时候为我们提供很多方便。 + +.. 接下来就需要在内核中实现简化版的进程机制并支持新增的系统调用。在本章第二小节 :doc:`/chapter5/2core-data-structures` 中我们对一些进程机制相关的数据结构进行了重构或者修改: + +.. - 为了支持基于应用名而不是应用 ID 来查找应用 ELF 可执行文件,从而实现灵活的应用加载,在 ``os/build.rs`` 以及 ``os/src/loader.rs`` 中更新了 ``link_app.S`` 的格式使得它包含每个应用的名字,另外提供 ``get_app_data_by_name`` 接口获取应用的 ELF 数据。 +.. - 在本章之前,任务管理器 ``TaskManager`` 不仅负责管理所有的任务状态,还维护着我们的 CPU 当前正在执行哪个任务。这种设计耦合度较高,我们将后一个功能分离到 ``os/src/task/processor.rs`` 中的处理器管理结构 ``Processor`` 中,它负责管理 CPU 上执行的任务和一些其他信息;而 ``os/src/task/manager.rs`` 中的任务管理器 ``TaskManager`` 仅负责管理所有任务。 +.. - 针对新的进程模型,我们复用前面章节的任务控制块 ``TaskControlBlock`` 作为进程控制块来保存进程的一些信息,相比前面章节还要新增 PID、内核栈、应用数据大小、父子进程、退出码等信息。它声明在 ``os/src/task/task.rs`` 中。 +.. - 从本章开始,内核栈在内核地址空间中的位置由所在进程的 PID 决定,我们需要在二者之间建立联系并提供一些相应的资源自动回收机制。可以参考 ``os/src/task/pid.rs`` 。 + +.. 有了这些数据结构的支撑,我们在本章第三小节 :doc:`/chapter5/3implement-process-mechanism` 实现进程机制。它可以分成如下几个方面: + +.. - 初始进程的自动创建。在内核初始化的时候需要调用 ``os/src/task/mod.rs`` 中的 ``add_initproc`` 函数,它会调用 ``TaskControlBlock::new`` 读取并解析初始应用 ``initproc`` 的 ELF 文件数据并创建初始进程 ``INITPROC`` ,随后会将它加入到全局任务管理器 ``TASK_MANAGER`` 中参与调度。 +.. - 进程切换机制。当一个进程退出或者是主动/被动交出 CPU 使用权之后需要由内核将 CPU 使用权交给其他进程。在本章中我们沿用 ``os/src/task/mod.rs`` 中的 ``suspend_current_and_run_next`` 和 ``exit_current_and_run_next`` 两个接口来实现进程切换功能,但是需要适当调整它们的实现。我们需要调用 ``os/src/task/task.rs`` 中的 ``schedule`` 函数进行进程切换,它会首先切换到处理器的 idle 控制流(即 ``os/src/task/processor`` 的 ``Processor::run`` 方法),然后在里面选取要切换到的进程并切换过去。 +.. - 进程调度机制。在进程切换的时候我们需要选取一个进程切换过去。选取进程逻辑可以参考 ``os/src/task/manager.rs`` 中的 ``TaskManager::fetch_task`` 方法。 +.. - 进程生成机制。这主要是指 ``fork/exec`` 两个系统调用。它们的实现分别可以在 ``os/src/syscall/process.rs`` 中找到,分别基于 ``os/src/process/task.rs`` 中的 ``TaskControlBlock::fork/exec`` 。 +.. - 进程资源回收机制。当一个进程主动退出或出错退出的时候,在 ``exit_current_and_run_next`` 中会立即回收一部分资源并在进程控制块中保存退出码;而需要等到它的父进程通过 ``waitpid`` 系统调用(与 ``fork/exec`` 两个系统调用放在相同位置)捕获到它的退出码之后,它的进程控制块才会被回收,从而所有资源都被回收。 +.. - 为了支持用户终端 ``user_shell`` 读取用户键盘输入的功能,还需要实现 ``read`` 系统调用,它可以在 ``os/src/syscall/fs.rs`` 中找到。 \ No newline at end of file diff --git a/_sources/chapter5/1process.rst.txt b/_sources/chapter5/1process.rst.txt new file mode 100644 index 0000000..2024aa3 --- /dev/null +++ b/_sources/chapter5/1process.rst.txt @@ -0,0 +1,230 @@ +与进程有关的重要系统调用 +================================================ + +重要系统调用 +------------------------------------------------------------ + +fork 系统调用 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: rust + + /// 功能:由当前进程 fork 出一个子进程。 + /// 返回值:对于子进程返回 0,对于当前进程则返回子进程的 PID 。 + /// syscall ID:220 + pub fn sys_fork() -> isize; + +exec 系统调用 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: rust + + /// 功能:将当前进程的地址空间清空并加载一个特定的可执行文件,返回用户态后开始它的执行。 + /// 参数:字符串 path 给出了要加载的可执行文件的名字; + /// 返回值:如果出错的话(如找不到名字相符的可执行文件)则返回 -1,否则不应该返回。 + /// 注意:path 必须以 "\0" 结尾,否则内核将无法确定其长度 + /// syscall ID:221 + pub fn sys_exec(path: &str) -> isize; + +利用 ``fork`` 和 ``exec`` 的组合,我们能让创建一个子进程,并令其执行特定的可执行文件。 + +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; + + +``sys_waitpid`` 在用户库中被封装成两个不同的 API, ``wait(exit_code: &mut i32)`` 和 ``waitpid(pid: usize, exit_code: &mut i32)``, +前者用于等待任意一个子进程,后者用于等待特定子进程。它们实现的策略是如果子进程还未结束,就以 yield 让出时间片: + +.. 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 => { sys_yield(); } + n => { return n; } + } + } + } + + +应用程序示例 +----------------------------------------------- + +借助这三个重要系统调用,我们可以开发功能更强大的应用。下面是两个案例: **用户初始程序-init** 和 **shell程序-user_shell** 。 + +用户初始程序-initproc +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +在内核初始化完毕后创建的第一个进程,是 **用户初始进程** (Initial Process) ,它将通过 +``fork+exec`` 创建 ``user_shell`` 子进程,并将被用于回收僵尸进程。 + +.. code-block:: rust + :linenos: + + // user/src/bin/ch5b_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("ch5b_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`` 出的子进程分支,通过 ``exec`` 启动shell程序 ``user_shell`` , + 注意我们需要在字符串末尾手动加入 ``\0`` 。 +- 第 21 行开始则为父进程分支,表示用户初始程序-initproc自身。它不断循环调用 ``wait`` 来等待并回收系统中的僵尸进程占据的资源。 + 如果回收成功的话则会打印一条报告信息给出被回收子进程的 PID 和返回值;否则就 ``yield_`` 交出 CPU 资源并在下次轮到它执行的时候再回收看看。 + + +shell程序-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`` 函数。 + +shell程序 ``user_shell`` 实现如下: + +.. code-block:: rust + :linenos: + :emphasize-lines: 28,53,61 + + // user/src/bin/ch5b_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`` 则维护着用户当前输入的命令内容,它也在不断发生变化。 + +- 如果用户输入回车键(第 28 行),那么user_shell 会 fork 出一个子进程(第 34 行开始)并试图通过 + ``exec`` 系统调用执行一个应用,应用的名字在字符串 ``line`` 中给出。如果 exec 的返回值为 -1 , + 说明在应用管理器中找不到对应名字的应用,此时子进程就直接打印错误信息并退出;否则子进程将开始执行目标应用。 + + fork 之后的 user_shell 进程自己的逻辑可以在第 41 行找到。它在等待 fork 出来的子进程结束并回收掉它的资源,还会顺带收集子进程的退出状态并打印出来。 +- 如果用户输入退格键(第 53 行),首先我们需要将屏幕上当前行的最后一个字符用空格替换掉, + 这可以通过输入一个特殊的退格字节 ``BS`` 来实现。其次,user_shell 进程内维护的 ``line`` 也需要弹出最后一个字符。 +- 如果用户输入了一个其他字符(第 61 行),就接将它打印在屏幕上,并加入到 ``line`` 中。 +- 按键 ``Ctrl+A`` 再输入 ``X`` 来退出qemu模拟器。 \ No newline at end of file diff --git a/_sources/chapter5/2core-data-structures.rst.txt b/_sources/chapter5/2core-data-structures.rst.txt new file mode 100644 index 0000000..b43388a --- /dev/null +++ b/_sources/chapter5/2core-data-structures.rst.txt @@ -0,0 +1,540 @@ +进程管理的核心数据结构 +=================================== + +本节导读 +----------------------------------- + +为了更好实现进程管理,我们需要设计和调整内核中的一些数据结构,包括: + +- 基于应用名的应用链接/加载器 +- 进程标识符 ``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!("**************/") + } + + +进程标识符和内核栈 +------------------------------------------------------------------------ + +进程标识符 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +同一时间存在的所有进程都有一个自己的进程标识符,它们是互不相同的整数。这里将其抽象为一个 ``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: UPSafeCell = + unsafe { UPSafeCell::new(PidAllocator::new()) }; + } + +``PidAllocator::alloc`` 将会分配出去一个将 ``usize`` 包装之后的 ``PidHandle`` 。 +我们将其包装为一个全局分配进程标识符的接口 ``pid_alloc``: + +.. code-block:: rust + + // os/src/task/pid.rs + + pub fn pid_alloc() -> PidHandle { + PID_ALLOCATOR.exclusive_access().alloc() + } + +同时我们也需要为 ``PidHandle`` 实现 ``Drop`` Trait 来允许编译器进行自动的资源回收: + +.. code-block:: rust + + // os/src/task/pid.rs + + impl Drop for PidHandle { + fn drop(&mut self) { + //println!("drop pid {}", self.0); + PID_ALLOCATOR.exclusive_access().dealloc(self.0); + } + } + +内核栈 +~~~~~~~~~~~~~~~~~~~~~~ + +从本章开始,我们将应用编号替换为进程标识符来决定每个进程内核栈在地址空间中的位置。 + +在内核栈 ``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.exclusive_access().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 + .exclusive_access() + .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: UPSafeCell, + } + + pub struct TaskControlBlockInner { + pub trap_cx_ppn: PhysPageNum, + pub base_size: usize, + pub task_cx: TaskContext, + pub task_status: TaskStatus, + pub memory_set: MemorySet, + pub parent: Option>, + pub children: Vec>, + pub exit_code: i32, + } + + +任务控制块中包含两部分: + +- 在初始化之后就不再变化的作为一个字段直接放在任务控制块中。这里将进程标识符 ``PidHandle`` 和内核栈 ``KernelStack`` 放在其中; +- 在运行过程中可能发生变化的则放在 ``TaskControlBlockInner`` 中,将它再包裹上一层 ``UPSafeCell`` 放在任务控制块中。 + 在此使用 ``UPSafeCell`` 可以提供互斥从而避免数据竞争。 + +``TaskControlBlockInner`` 中包含下面这些内容: + +- ``trap_cx_ppn`` 指出了应用地址空间中的 Trap 上下文被放在的物理页帧的物理页号。 +- ``base_size`` 的含义是:应用数据仅有可能出现在应用地址空间低于 ``base_size`` 字节的区域中。借助它我们可以清楚的知道应用有多少数据驻留在内存中。 +- ``task_cx`` 保存任务上下文,用于任务切换。 +- ``task_status`` 维护当前进程的执行状态。 +- ``memory_set`` 表示应用地址空间。 +- ``parent`` 指向当前进程的父进程(如果存在的话)。注意我们使用 ``Weak`` 而非 ``Arc`` + 来包裹另一个任务控制块,因此这个智能指针将不会影响父进程的引用计数。 +- ``children`` 则将当前进程的所有子进程的任务控制块以 ``Arc`` 智能指针的形式保存在一个向量中,这样才能够更方便的找到它们。 +- 当进程调用 exit 系统调用主动退出或者执行出错由内核终止的时候,它的退出码 ``exit_code`` + 会被内核保存在它的任务控制块中,并等待它的父进程通过 waitpid 回收它的资源的同时也收集它的 PID 以及退出码。 + +注意我们在维护父子进程关系的时候大量用到了智能指针 ``Arc/Weak`` ,当且仅当它的引用计数变为 0 的时候,进程控制块以及被绑定到它上面的各类资源才会被回收。 + +``TaskControlBlockInner`` 提供的方法主要是对于它内部字段的快捷访问: + +.. code-block:: rust + + // os/src/task/task.rs + + impl TaskControlBlockInner { + 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 inner_exclusive_access(&self) -> RefMut<'_, TaskControlBlockInner> { + self.inner.exclusive_access() + } + 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 {...} + } + +- ``inner_exclusive_access`` 尝试获取互斥锁来得到 ``TaskControlBlockInner`` 的可变引用。 +- ``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: UPSafeCell = + unsafe { UPSafeCell::new(TaskManager::new()) }; + } + + pub fn add_task(task: Arc) { + TASK_MANAGER.exclusive_access().add(task); + } + + pub fn fetch_task() -> Option> { + TASK_MANAGER.exclusive_access().fetch() + } + +``TaskManager`` 将所有的任务控制块用引用计数 ``Arc`` 智能指针包裹后放在一个双端队列 ``VecDeque`` 中。 +使用智能指针的原因在于,任务控制块经常需要被放入/取出,如果直接移动任务控制块自身将会带来大量的数据拷贝开销, +而对于智能指针进行移动则没有多少开销。其次,允许任务控制块的共享引用在某些情况下能够让我们的实现更加方便。 + +``TaskManager`` 提供 ``add/fetch`` 两个操作,前者表示将一个任务加入队尾,后者则表示从队头中取出一个任务来执行。 +从调度算法来看,这里用到的就是最简单的 RR 算法。全局实例 ``TASK_MANAGER`` 则提供给内核的其他子模块 ``add_task/fetch_task`` 两个函数。 + +处理器管理结构 +------------------------------------------------------------------------ + +处理器管理结构 ``Processor`` 负责维护从任务管理器 ``TaskManager`` 分离出去的那部分 CPU 状态: + +.. code-block:: rust + + // os/src/task/processor.rs + + pub struct Processor { + current: Option>, + idle_task_cx: TaskContext, + } + +包括: + +- ``current`` 表示在当前处理器上正在执行的任务; +- ``idle_task_cx_ptr`` 表示当前处理器上的 idle 控制流的任务上下文的地址。 + +在单核环境下,我们仅创建单个 ``Processor`` 的全局实例 ``PROCESSOR`` : + +.. code-block:: rust + + // os/src/task/processor.rs + + lazy_static! { + pub static ref PROCESSOR: UPSafeCell = unsafe { UPSafeCell::new(Processor::new()) }; + } + +正在执行的任务 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: rust + :linenos: + + // os/src/task/processor.rs + + impl Processor { + pub fn take_current(&mut self) -> Option> { + self.current.take() + } + pub fn current(&self) -> Option> { + self.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.inner_exclusive_access().get_user_token(); + token + } + + pub fn current_trap_cx() -> &'static mut TrapContext { + current_task() + .unwrap() + .inner_exclusive_access() + .get_trap_cx() + } + + +- 第 4 行的 ``Processor::take_current`` 可以取出当前正在执行的任务。 ``Option::take`` 意味着 ``current`` 字段也变为 ``None`` 。 +- 第 7 行的 ``Processor::current`` 返回当前执行的任务的一份拷贝。。 +- ``current_user_token`` 和 ``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_ptr(&mut self) -> *mut TaskContext { + &mut self.idle_task_cx as *mut _ + } + } + + pub fn run_tasks() { + loop { + let mut processor = PROCESSOR.exclusive_access(); + if let Some(task) = fetch_task() { + let idle_task_cx_ptr = processor.get_idle_task_cx_ptr(); + // access coming task TCB exclusively + let mut task_inner = task.inner_exclusive_access(); + let next_task_cx_ptr = &task_inner.task_cx as *const TaskContext; + task_inner.task_status = TaskStatus::Running; + drop(task_inner); + // release coming task TCB manually + processor.current = Some(task); + // release processor manually + drop(processor); + unsafe { + __switch(idle_task_cx_ptr, next_task_cx_ptr); + } + } + } + } + +调度功能的主体在 ``run_tasks`` 中实现。它循环调用 ``fetch_task`` 直到顺利从任务管理器中取出一个任务,然后获得 +``__switch`` 两个参数进行任务切换。注意在整个过程中要严格控制临界区。 + +当一个应用交出 CPU 使用权时,进入内核后它会调用 ``schedule`` 函数来切换到 idle 控制流并开启新一轮的任务调度。 + +.. code-block:: rust + + // os/src/task/processor.rs + + pub fn schedule(switched_task_cx_ptr: *mut TaskContext) { + let mut processor = PROCESSOR.exclusive_access(); + let idle_task_cx_ptr = processor.get_idle_task_cx_ptr(); + drop(processor); + unsafe { + __switch(switched_task_cx_ptr, idle_task_cx_ptr); + } + } + +切换回去之后,我们将跳转到 ``Processor::run`` 中 ``__switch`` 返回之后的位置,也即开启了下一轮循环。 diff --git a/_sources/chapter5/3implement-process-mechanism.rst.txt b/_sources/chapter5/3implement-process-mechanism.rst.txt new file mode 100644 index 0000000..1de6985 --- /dev/null +++ b/_sources/chapter5/3implement-process-mechanism.rst.txt @@ -0,0 +1,665 @@ +进程管理机制的设计实现 +============================================ + +本节导读 +-------------------------------------------- + +本节将介绍如何基于上一节设计的内核数据结构来实现进程管理: + +- 初始进程 ``initproc`` 的创建; +- 进程调度机制:当进程主动调用 ``sys_yield`` 交出 CPU 使用权,或者内核本轮分配的时间片用尽之后如何切换到下一个进程; +- 进程生成机制:介绍进程相关的两个重要系统调用 ``sys_fork/sys_exec`` 的实现; +- 字符输入机制:介绍 ``sys_read`` 系统调用的实现; +- 进程资源回收机制:当进程调用 ``sys_exit`` 正常退出或者出错被内核终止后,如何保存其退出码,其父进程又是如何通过 + ``sys_waitpid`` 收集该进程的信息并回收其资源。 + +初始进程的创建 +-------------------------------------------- + +内核初始化完毕之后,即会调用 ``task`` 子模块提供的 ``add_initproc`` 函数来将初始进程 ``initproc`` +加入任务管理器,但在这之前,我们需要初始进程的进程控制块 ``INITPROC`` ,这基于 ``lazy_static`` 在运行时完成。 + +.. code-block:: rust + + // os/src/task/mod.rs + + 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: unsafe { UPSafeCell::new(TaskControlBlockInner { + trap_cx_ppn, + base_size: user_sp, + task_cx: TaskContext::goto_trap_return(kernel_stack_top), + 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.inner_exclusive_access().get_trap_cx(); + *trap_cx = TrapContext::app_init_context( + entry_point, + user_sp, + KERNEL_SPACE.exclusive_access().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 的时候用户态能正确进入内核态。 + +进程调度机制 +-------------------------------------------- + +调用 ``task`` 子模块提供的 ``suspend_current_and_run_next`` 函数可以暂停当前任务,并切换到下一个任务,下面给出了两种典型的使用场景: + +.. 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(); + + // ---- access current TCB exclusively + let mut task_inner = task.inner_exclusive_access(); + let task_cx_ptr = &mut task_inner.task_cx as *mut TaskContext; + // Change status to Ready + task_inner.task_status = TaskStatus::Ready; + drop(task_inner); + // ---- release current PCB + + // push back to ready queue. + add_task(task); + // jump to scheduling cycle + schedule(task_cx_ptr); + } + +首先通过 ``take_current_task`` 来取出当前正在执行的任务,修改其进程控制块内的状态,随后将这个任务放入任务管理器的队尾。接着调用 +``schedule`` 函数来触发调度并切换任务。当仅有一个任务的时候, ``suspend_current_and_run_next`` 的效果是会继续执行这个任务。 + +进程的生成机制 +-------------------------------------------- + +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 { + // ---- access parent PCB exclusively + let mut parent_inner = self.inner_exclusive_access(); + // 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(); + let task_control_block = Arc::new(TaskControlBlock { + pid: pid_handle, + kernel_stack, + inner: unsafe { + UPSafeCell::new(TaskControlBlockInner { + trap_cx_ppn, + base_size: parent_inner.base_size, + task_cx: TaskContext::goto_trap_return(kernel_stack_top), + 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 + // **** access children PCB exclusively + let trap_cx = task_control_block.inner_exclusive_access().get_trap_cx(); + trap_cx.kernel_sp = kernel_stack_top; + // return + task_control_block + // ---- release parent PCB automatically + // **** release children PCB automatically + } + } + +它基本上和新建进程控制块的 ``TaskControlBlock::new`` 是相同的,但要注意以下几点: + +- 子进程的地址空间不是通过解析 ELF,而是通过在第 8 行调用 ``MemorySet::from_existed_user`` 复制父进程地址空间得到的; +- 在 fork 的时候需要注意父子进程关系的维护。既要将父进程的弱引用计数放到子进程的进程控制块中,又要将子进程插入到父进程的孩子向量 ``children`` 中。 + +实现 ``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.inner_exclusive_access().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 + } + +在调用 ``sys_fork`` 之前,我们已经将当前进程 Trap 上下文中的 sepc 向后移动了 4 字节,使得它回到用户态之后会从 ecall +的下一条指令开始执行。之后,当我们复制地址空间时,子进程地址空间 Trap 上下文的 sepc 也是移动之后的值,我们无需再进行修改。 + +父子进程回到用户态的瞬间都处于刚刚从一次系统调用返回的状态,但二者返回值不同。第 8~11 行我们将子进程的 Trap +上下文中用来存放系统调用返回值的 a0 寄存器修改为 0 ,而父进程系统调用的返回值会在 ``syscall`` 返回之后再设置为 ``sys_fork`` +的返回值。这就做到了父进程 ``fork`` 的返回值为子进程的 PID ,而子进程的返回值为 0。 + +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(); + + // **** access inner exclusively + let mut inner = self.inner_exclusive_access(); + // 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.exclusive_access().token(), + self.kernel_stack.get_top(), + trap_handler as usize, + ); + // **** release inner automatically + } + } + +它在解析传入的 ELF 格式数据之后只做了两件事情: + +- 首先从 ELF 生成一个全新的地址空间并直接替换进来(第 15 行),这将导致原有地址空间生命周期结束,里面包含的全部物理页帧都会被回收; +- 然后修改新的地址空间中的 Trap 上下文,将解析得到的应用入口点、用户栈位置以及一些内核的信息进行初始化,这样才能正常实现 Trap 机制。 + +``sys_exec`` 的实现如下,它调用 ``translated_str`` 找到要执行的应用名,并试图从应用加载器提供的 ``get_app_data_by_name`` +接口中获取对应的 ELF 数据,如果找到的话就调用 ``TaskControlBlock::exec`` 替换地址空间。 + + + +.. code-block:: rust + + // 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`` 系统调用中传递给内核的只有一个应用名字符串在用户地址空间中的首地址,内核必限手动查页表来获得字符串的值。 + +``translated_str`` 用来从用户地址空间中查找字符串,其原理就是逐字节查页表直到发现一个 ``\0`` 为止。为什么要逐字节查页表? +因为内核不知道字符串的长度,且字符串可能是跨物理页的。 + +.. code-block:: rust + + // 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 + } + +系统调用后重新获取 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`` 失效了,因为它是就原来的地址空间而言的。为了能够处理类似的这种情况,我们在 ``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(); + } + + +sys_read 获取输入 +-------------------------------------------- + +我们需要实现 ``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 读入,且每次只能读入一个字符,这是利用 ``sbi`` +提供的接口 ``console_getchar`` 实现的。如果还没有输入,我们就切换到其他进程,等下次切换回来时再看看是否有输入了。 +获取到输入后就退出循环,并手动查页表将输入字符正确写入到应用地址空间。 + +进程资源回收机制 +-------------------------------------------- + +进程的退出 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +当应用调用 ``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`` 带有一个退出码作为参数,这个退出码会在 +``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(); + // **** access current TCB exclusively + let mut inner = task.inner_exclusive_access(); + // 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 + + // ++++++ access initproc TCB exclusively + { + let mut initproc_inner = INITPROC.inner_exclusive_access(); + for child in inner.children.iter() { + child.inner_exclusive_access().parent = Some(Arc::downgrade(&INITPROC)); + initproc_inner.children.push(child.clone()); + } + } + // ++++++ release parent PCB + + inner.children.clear(); + // deallocate user space + inner.memory_set.recycle_data_pages(); + drop(inner); + // **** release current PCB + // drop task manually to maintain rc correctly + drop(task); + // we do not have to save task context + let mut _unused = TaskContext::zero_init(); + schedule(&mut _unused as *mut _); + } + + +- 第 13 行,调用 ``take_current_task`` 来将当前进程控制块从处理器监控 ``PROCESSOR`` + 中取出,而不只是得到一份拷贝,这是为了正确维护进程控制块的引用计数; +- 第 17 行将进程控制块中的状态修改为 ``TaskStatus::Zombie`` 即僵尸进程; +- 第 19 行将传入的退出码 ``exit_code`` 写入进程控制块中,后续父进程在 ``waitpid`` 的时候可以收集; +- 第 24~26 行所做的事情是,将当前进程的所有子进程挂在初始进程 ``initproc`` 下面。第 32 行将当前进程的孩子向量清空。 +- 第 34 行,对于当前进程占用的资源进行早期回收。 ``MemorySet::recycle_data_pages`` 只是将地址空间中的逻辑段列表 + ``areas`` 清空,这将导致应用地址空间的所有数据被存放在的物理页帧被回收,而用来存放页表的那些物理页帧此时则不会被回收。 +- 最后在第 41 行我们调用 ``schedule`` 触发调度及任务切换,我们再也不会回到该进程的执行过程,因此无需关心任务上下文的保存。 + +父进程回收子进程资源 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. 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 + + // ---- access current TCB exclusively + let mut inner = task.inner_exclusive_access(); + if !inner + .children + .iter() + .any(|p| pid == -1 || pid as usize == p.getpid()) + { + return -1; + // ---- release current PCB + } + let pair = inner.children.iter().enumerate().find(|(_, p)| { + // ++++ temporarily access child PCB lock exclusively + p.inner_exclusive_access().is_zombie() && (pid == -1 || pid as usize == p.getpid()) + // ++++ release child PCB + }); + 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 access child TCB exclusively + let exit_code = child.inner_exclusive_access().exit_code; + // ++++ release child PCB + *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`` 完成。 + +首先判断 ``sys_waitpid`` 是否会返回 -1 ,这取决于当前进程是否有一个符合要求的子进程。当传入的 ``pid`` 为 -1 +的时候,任何一个子进程都算是符合要求;但 ``pid`` 不为 -1 的时候,则只有 PID 恰好与 ``pid`` +相同的子进程才算符合条件。我们简单通过迭代器即可完成判断。 + +再判断符合要求的子进程中是否有僵尸进程。如果找不到的话直接返回 ``-2`` ,否则进行下一步处理: + +我们将子进程从向量中移除并置于当前上下文中,当它所在的代码块结束,这次引用变量的生命周期结束,子进程进程控制块的引用计数将变为 +0 ,内核将彻底回收掉它占用的所有资源,包括内核栈、它的 PID 、存放页表的那些物理页帧等等。 + +获得子进程退出码后,考虑到应用传入的指针指向应用地址空间,我们还需要手动查页表找到对应物理内存中的位置。 +``translated_refmut`` 的实现可以在 ``os/src/mm/page_table.rs`` 中找到。 \ No newline at end of file diff --git a/_sources/chapter5/4exercise.rst.txt b/_sources/chapter5/4exercise.rst.txt new file mode 100644 index 0000000..50a0cd1 --- /dev/null +++ b/_sources/chapter5/4exercise.rst.txt @@ -0,0 +1,137 @@ +chapter5练习 +============================================== + +Lab3 编程作业 +--------------------------------------------- + +进程创建 ++++++++++++++++++++++++++++++++++++++++++++++ + +大家一定好奇过为啥进程创建要用 fork + exec 这么一个奇怪的系统调用,就不能直接搞一个新进程吗? +思而不学则殆,我们就来试一试!这章的编程练习请大家实现一个完全 DIY 的系统调用 spawn,用以创建一个新进程。 + +spawn 系统调用定义( `标准spawn看这里 `_ ): + +.. code-block:: rust + + fn sys_spawn(path: *const u8) -> isize + +- syscall ID: 400 +- 功能:新建子进程,使其执行目标程序。 +- 说明:成功返回子进程id,否则返回 -1。 +- 可能的错误: + - 无效的文件名。 + - 进程池满/内存不足等资源错误。 + +TIPS:虽然测例很简单,但提醒读者 spawn **不必** 像 fork 一样复制父进程的地址空间。 + +stride 调度算法 ++++++++++++++++++++++++++++++++++++++++++ + +ch3 中我们实现的调度算法十分简单。现在我们要为我们的 os 实现一种带优先级的调度算法:stride 调度算法。 + +算法描述如下: + +(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。 + +为了实现该调度算法,内核还要增加 set_prio 系统调用 + +.. code-block:: rust + + // syscall ID:140 + // 设置当前进程优先级为 prio + // 参数:prio 进程优先级,要求 prio >= 2 + // 返回值:如果输入合法则返回 prio,否则返回 -1 + fn sys_set_priority(prio: isize) -> isize; + +实现 tips: + +- 你可以在TCB加入新的字段来支持优先级等。 +- 为了减少整数除的误差,BIG_STRIDE 一般需要很大,但为了不至于发生反转现象(详见问答作业),或许选择一个适中的数即可,当然能进行溢出处理就更好了。 +- stride 算法要找到 stride 最小的进程,使用优先级队列是效率不错的办法,但是我们的实验测例很简单,所以效率完全不是问题。事实上,很推荐使用暴力扫一遍的办法找最小值。 +- 注意设置进程的初始优先级。 + +.. attention:: + + 为了让大家能在本编程作业中使用 ``Vec`` 等数据结构,我们利用第三方库 ``buddy_system_allocator`` + 为大家实现了堆内存分配器,相关代码位于 ``mm/heap_allocator`` 模块。 + + 背景知识: `Rust 中的动态内存分配 `_ + +实验要求 ++++++++++++++++++++++++++++++++++++++++++++++ +- `lab3(os5)参考框架: `_ +- 实验目录在 ``os5`` 。注意在reports中放入lab1-3的所有报告。 +- 通过所有测例。 + + 在 os5 目录下 ``make run BASE=2`` 加载所有测例, ``ch5_usertest`` 打包了所有你需要通过的测例, + 你也可以通过修改这个文件调整本地测试的内容, 或者单独运行某测例来纠正特定的错误。 ``ch5_stride`` + 检查 stride 调度算法是否满足公平性要求,六个子程序运行的次数应该大致与其优先级呈正比,测试通过标准是 + :math:`\max{\frac{runtimes}{prio}}/ \min{\frac{runtimes}{prio}} < 1.5`. + + CI 的原理是用 ``ch5_usertest`` 替代 ``ch5b_initproc`` ,使内核在所有测例执行完后直接退出。 + + 从本章开始,你的内核必须前向兼容,能通过前一章的所有测例。 + +.. note:: + + 利用 ``git cherry-pick`` 系列指令,能方便地将前一章分支 commit 移植到本章分支。 + +问答作业 +-------------------------------------------- + +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`` 函数,假设两个 Stride 永远不会相等。 + + .. code-block:: rust + + use core::cmp::Ordering; + + struct Stride(u64); + + impl PartialOrd for Stride { + fn partial_cmp(&self, other: &Self) -> Option { + // ... + } + } + + impl PartialEq for Stride { + fn eq(&self, other: &Self) -> bool { + false + } + } + + TIPS: 使用 8 bits 存储 stride, BigStride = 255, 则: ``(125 < 255) == false``, ``(129 < 255) == true``. + +报告要求 +------------------------------------------------------------ + +- 简单总结你实现的功能(200字以内,不要贴代码)。 +- 完成问答题。 +- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 diff --git a/_sources/chapter5/index.rst.txt b/_sources/chapter5/index.rst.txt new file mode 100644 index 0000000..7aaf8e1 --- /dev/null +++ b/_sources/chapter5/index.rst.txt @@ -0,0 +1,12 @@ +第五章:进程及进程管理 +============================================== + +.. toctree:: + :maxdepth: 4 + + 0intro + 1process + 2core-data-structures + 3implement-process-mechanism + 4exercise + \ No newline at end of file diff --git a/_sources/chapter6/0intro.rst.txt b/_sources/chapter6/0intro.rst.txt new file mode 100644 index 0000000..aabc249 --- /dev/null +++ b/_sources/chapter6/0intro.rst.txt @@ -0,0 +1,177 @@ +引言 +========================================= + +本章导读 +----------------------------------------- + +本章我们将实现一个简单的文件系统 -- easyfs,能够对 **持久存储设备** (Persistent Storage) I/O 资源进行管理;将设计两种文件:常规文件和目录文件,它们均以文件系统所维护的 **磁盘文件** 形式被组织并保存在持久存储设备上。 + +实践体验 +----------------------------------------- + +.. note:: + + 基于github classroom的开发方式 + + 基于github classroom,可方便建立开发用的git repository,并可基于github的 codespace (在线版ubuntu +vscode)在线开发使用。整个开发环境仅仅需要一个网络浏览器。 + + 1. 在网络浏览器中用自己的 github id 登录 github.com + 2. 接收 `第四个实验(os6)的github classroom在线邀请 `_ ,根据提示一路选择OK即可。 + 3. 完成第二步后,你的第四个实验的 github repository 会被自动建立好,点击此github repository的链接,就可看到你要完成的第一个实验了。 + 4. 在你的第四个实验的网页的中上部可以看到一个醒目的 `code` 绿色按钮,点击后,可以进一步看到 `codespace` 标签和醒目的 `create codesapce on main` 绿色按钮。请点击这个绿色按钮,就可以进入到在线的ubuntu +vscode环境中 + 5. 再按照下面的环境安装提示在vscode的 `console` 中安装配置开发环境:rustc,qemu等工具。 + 6. 在vscode的 `console` 中执行 `make setupclassroom_test6` (该命令仅执行一次)配置githubclassroom 自动评分功能。 + 7. 然后就可以基于在线vscode进行开发、运行、提交等完整的实验过程了。 + + 上述的3,4,5步不是必须的,你也可以线下本地开发。 + + +获取本章代码: + +.. code-block:: console + + $ git clone https://github.com/LearningOS/rust-based-os-comp2022.git + $ cd rust-based-os-comp2022/ + $ make setupclassroom_test6 //注意:这一步很重要,是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次,以后一般就不用执行了,除非 .github/workflows/classroom.yml发生了变化。 + + +在 qemu 模拟器上运行本章代码参考框架: + +.. code-block:: console + + $ cd os6-ref + $ make run + +内核初始化完成之后就会进入shell程序,在这里我们运行一下本章的测例 ``ch6b_filetest_simple`` : + +.. code-block:: + + >> ch6b_filetest_simple + file_test passed! + Shell: Process 2 exited with code 0 + >> + +它会将 ``Hello, world!`` 输出到另一个文件 ``filea`` ,并读取里面的内容确认输出正确。我们也可以通过命令行工具 ``ch6b_cat`` 来查看 ``filea`` 中的内容: + +.. code-block:: + + >> ch6b_cat + Hello, world! + Shell: Process 2 exited with code 0 + >> + +easy-fs和 `lab4(os6)参考框架: `_ +------------------------------------------------------------------------------------------------------------------- + +.. code-block:: + :linenos: + + ├── 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 + ├── os +    ├── build.rs(修改:不再需要将用户态程序链接到内核中) +    ├── Cargo.toml(修改:新增 Qemu 的块设备驱动依赖 crate) +    ├── Makefile(修改:新增文件系统的构建流程) +    └── src +    ├── config.rs(修改:新增访问块设备所需的一些 MMIO 配置) +    ├── ... +    ├── drivers(新增:Qemu 平台的块设备驱动) +    │   ├── block +    │   │   ├── mod.rs(将不同平台上的块设备全局实例化为 BLOCK_DEVICE 提供给其他模块使用) +    │   │   └── virtio_blk.rs(Qemu 平台的 virtio-blk 块设备) +    │   └── mod.rs +    ├── fs(新增:对文件系统及文件抽象) +    │   ├── inode.rs(新增:将 easy-fs 提供的 Inode 抽象封装为内核看到的 OSInode +    │   │ 并实现 fs 子模块的 File Trait) +    │   ├── mod.rs +    │   └── stdio.rs(新增:将标准输入输出也抽象为文件) +    ├── loader.rs(移除:应用加载器 loader 子模块,本章开始从文件系统中加载应用) +    ├── mm +    │   ├── address.rs +    │   ├── frame_allocator.rs +    │   ├── heap_allocator.rs +    │   ├── memory_set.rs(修改:在创建地址空间的时候插入 MMIO 虚拟页面) +    │   ├── mod.rs +    │   └── page_table.rs(新增:应用地址空间的缓冲区抽象 UserBuffer 及其迭代器实现) +    ├── syscall +    │   ├── fs.rs(修改:新增 sys_open,修改sys_read、sys_write) +    │   ├── 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(修改:在任务控制块中加入文件描述符表的相关机制) + + cloc easy-fs os + ------------------------------------------------------------------------------- + Language files blank comment code + ------------------------------------------------------------------------------- + Rust 41 306 418 3349 + Assembly 4 53 26 526 + make 1 13 4 48 + TOML 2 4 2 23 + ------------------------------------------------------------------------------- + SUM: 48 376 450 3946 + ------------------------------------------------------------------------------- + +.. 本章代码导读 +.. ----------------------------------------------------- + +.. 本章涉及的代码量相对较多,且与进程执行相关的管理还有直接的关系。其实我们是参考经典的UNIX基于索引的文件系统,设计了一个简化的有一级目录并支持创建/打开/读写/关闭文件一系列操作的文件系统。这里简要介绍一下在内核中添加文件系统的大致开发过程。 + +.. 第一步是能够写出与文件访问相关的应用。这里是参考了Linux的创建/打开/读写/关闭文件的系统调用接口,力图实现一个 :ref:`简化版的文件系统模型 ` 。在用户态我们只需要遵从相关系统调用的接口约定,在用户库里完成对应的封装即可。这一过程我们在前面的章节中已经重复过多次,读者应当对其比较熟悉。其中最为关键的是系统调用可以参考 :ref:`sys_open 语义介绍 ` ,此外我们还给出了 :ref:`测例代码解读 ` 。 + +.. 第二步就是要实现 easyfs 文件系统了。由于 Rust 语言的特点,我们可以在用户态实现 easyfs 文件系统,并在用户态完成文件系统功能的基本测试并基本验证其实现正确性之后,就可以放心的将该模块嵌入到操作系统内核中。当然,有了文件系统的具体实现,还需要对上一章的操作系统内核进行扩展,实现与 easyfs 文件系统对接的接口,这样才可以让操作系统拥有一个简单可用的文件系统。从而,内核可以支持允许文件读写功能的更复杂的应用,在命令行参数机制的加持下,可以进一步提升整个系统的灵活性,让应用的开发和调试变得更为轻松。 + +.. easyfs 文件系统的整体架构自下而上可分为五层。它的最底层就是对块设备的访问操作接口。在 ``easy-fs/src/block_dev.rs`` 中,可以看到 ``BlockDevice`` trait 代表了一个抽象块设备,该 trait 仅需求两个函数 ``read_block`` 和 ``write_block`` ,分别代表将数据从块设备读到内存中的缓冲区中,或者将数据从内存中的缓冲区写回到块设备中,数据需要以块为单位进行读写。easy-fs 库的使用者需要负责为它们看到的实际的块设备具体实现 ``BlockDevice`` trait 并提供给 easy-fs 库的上层,这样的话 easy-fs 库的最底层就与一个具体的执行环境对接起来了。至于为什么块设备层位于 easy-fs 的最底层,是因为文件系统仅仅是在块设备上存储的结构稍微复杂一点的数据,但无论它的操作变换如何复杂,从块设备的角度终究可以被分解成若干次块读写。 + +.. 尽管在最底层我们就已经有了块读写的能力,但从编程方便性和性能的角度,仅有块读写这么基础的底层接口是不足以实现如此复杂的文件系统的,虽然它已经被我们大幅简化过了。比如,将一个块的内容读到内存的缓冲区,对缓冲区进行修改,并尚未写回的时候,如果由于编程上的不小心再次将该块的内容读到另一个缓冲区,而不是使用已有的缓冲区,这将会造成不一致问题。此外还有可能增加很多不必要的块读写次数,大幅降低文件系统的性能。因此,通过程序自动而非程序员手动对块的缓冲区进行统一管理也就势在必行了,该机制被我们抽象为 easy-fs 自底向上的第二层,即块缓存层。在 ``easy-fs/src/block_cache.rs`` 中, ``BlockCache`` 代表一个被我们管理起来的块的缓冲区,它带有缓冲区本体以及块的编号等信息。当它被创建的时候,将触发一次 ``read_block`` 将数据从块设备读到它的缓冲区中。接下来只要它驻留在内存中,便可保证对于同一个块的所有操作都会直接在它的缓冲区中进行而无需额外的 ``read_block`` 。块缓存管理器 ``BlockManager`` 在内存中管理有限个 ``BlockCache`` 并实现了类似 FIFO 的缓存替换算法,当一个块缓存被换出的时候视情况可能调用 ``write_block`` 将缓冲区数据写回块设备。总之,块缓存层对上提供 ``get_block_cache`` 接口来屏蔽掉相关细节,从而可以透明的读写一个块。 + +.. 有了块缓存,我们就可以在内存中方便地处理easyfs文件系统在磁盘上的各种数据了,这就是第三层文件系统的磁盘数据结构。easyfs文件系统中的所有需要持久保存的数据都会放到磁盘上,这包括了管理这个文件系统的 **超级块 (Super Block)**,管理空闲磁盘块的 **索引节点位图区** 和 **数据块位图区** ,以及管理文件的 **索引节点区** 和 放置文件数据的 **数据块区** 组成。 + +.. easyfs文件系统中管理这些磁盘数据的控制逻辑主要集中在 **磁盘块管理器** 中,这是文件系统的第四层。对于文件系统管理而言,其核心是 ``EasyFileSystem`` 数据结构及其关键成员函数: + +.. - EasyFileSystem.create:创建文件系统 +.. - EasyFileSystem.open:打开文件系统 +.. - EasyFileSystem.alloc_inode:分配inode (dealloc_inode未实现,所以还不能删除文件) +.. - EasyFileSystem.alloc_data:分配数据块 +.. - EasyFileSystem.dealloc_data:回收数据块 + +.. 对于单个文件的管理和读写的控制逻辑主要是 **索引节点** 来完成,这是文件系统的第五层,其核心是 ``Inode`` 数据结构及其关键成员函数: + +.. - Inode.new:在磁盘上的文件系统中创建一个inode +.. - Inode.find:根据文件名查找对应的磁盘上的inode +.. - Inode.create:在根目录下创建一个文件 +.. - Inode.read_at:根据inode找到文件数据所在的磁盘数据块,并读到内存中 +.. - Inode.write_at:根据inode找到文件数据所在的磁盘数据块,把内存中数据写入到磁盘数据块中 + +.. 上述五层就构成了easyfs文件系统的整个内容。我们可以把easyfs文件系统看成是一个库,被应用程序调用。而 ``easy-fs-fuse`` 这个应用就通过调用easyfs文件系统库中各种函数,并用Linux上的文件模拟了一个块设备,就可以在这个模拟的块设备上创建了一个easyfs文件系统。 + +.. 第三步,我们需要把easyfs文件系统加入到我们的操作系统内核中。这还需要做两件事情,第一件是在Qemu模拟的 ``virtio`` 块设备上实现块设备驱动程序 ``os/src/drivers/block/virtio_blk.rs`` 。由于我们可以直接使用 ``virtio-drivers`` crate中的块设备驱动,所以只要提供这个块设备驱动所需要的内存申请与释放以及虚实地址转换的4个函数就可以了。而我们之前操作系统中的虚存管理实现中,以及有这些函数,导致块设备驱动程序很简单,具体实现细节都被 ``virtio-drivers`` crate封装好了。 + +.. 第二件事情是把文件访问相关的系统调用与easyfs文件系统连接起来。在easfs文件系统中是没有进程的概念的。而进程是程序运行过程中访问资源的管理实体,这就要对 ``easy-fs`` crate 提供的 ``Inode`` 结构进一步封装,形成 ``OSInode`` 结构,以表示进程中一个打开的常规文件。对于应用程序而言,它理解的磁盘数据是常规的文件和目录,不是 ``OSInode`` 这样相对复杂的结构。其实常规文件对应的 OSInode 是文件在操作系统内核中的内部表示,因此需要为它实现 File Trait 从而能够可以将它放入到进程文件描述符表中,并通过 sys_read/write 系统调用进行读写。这样就建立了文件与 ``OSInode`` 的对应关系,并通过上面描述的三个步骤完成了包含文件系统的操作系统内核,并能给应用提供基于文件的系统调用服务。 + +.. 完成包含文件系统的操作系统内核后,我们在shell程序和内核中支持命令行参数的解析和传递,这样可以让应用根据灵活地通过命令行参数来动态地表示要操作的文件。这需要扩展对应的系统调用 ``sys_exec`` ,主要的改动就是在创建新进程时,把命令行参数压入用户栈中,这样应用程序在执行时就可以从用户栈中获取到命令行的参数值了。 + +.. 在上一章,我们提到了把标准输出设备在文件描述符表中的文件描述符的值规定为 1 ,用 Stdin 表示;把标准输入设备在文件描述符表中的文件描述符的值规定为 0,用 stdout 表示 。另外,还有一条文件描述符相关的重要规则:即进程打开一个文件的时候,内核总是会将文件分配到该进程文件描述符表中编号 最小的 空闲位置。利用这些约定,只实现新的系统调用 ``sys_dup`` 完成对文件描述符的复制,就可以巧妙地实现标准 I/O 重定向功能了。 + +.. 具体思路是,在某应用进程执行之前,父进程(比如 user_shell进程)要对子应用进程的文件描述符表进行某种替换。以输出为例,父进程在创建子进程前,提前打开一个常规文件 A,然后 ``fork`` 子进程,在子进程的最初执行中,通过 ``sys_close`` 关闭 Stdout 文件描述符,用 ``sys_dup`` 复制常规文件 A 的文件描述符,这样 Stdout 文件描述符实际上指向的就是常规文件A了,这时再通过 ``sys_close`` 关闭常规文件 A 的文件描述符。至此,常规文件 A 替换掉了应用文件描述符表位置 1 处的标准输出文件,这就完成了所谓的 **重定向** ,即完成了执行新应用前的准备工作。 + +.. 接下来是子进程调用 ``sys_exec`` 系统调用,创建并开始执行新子应用进程。在重定向之后,新的子应用进程认为自己输出到 fd=1 的标准输出文件,但实际上是输出到父进程(比如 user_shell进程)指定的文件A中。文件这一抽象概念透明化了文件、I/O设备之间的差异,因为在进程看来无论是标准输出还是常规文件都是一种文件,可以通过同样的接口来读写。这就是文件的强大之处。 diff --git a/_sources/chapter6/1file-descriptor.rst.txt b/_sources/chapter6/1file-descriptor.rst.txt new file mode 100644 index 0000000..8fe5b58 --- /dev/null +++ b/_sources/chapter6/1file-descriptor.rst.txt @@ -0,0 +1,243 @@ +文件与文件描述符 +=========================================== + +文件简介 +------------------------------------------- + +文件可代表很多种不同类型的I/O 资源,但是在进程看来,所有文件的访问都可以通过一个简洁统一的抽象接口 ``File`` 进行: + +.. code-block:: rust + + // os/src/fs/mod.rs + + pub trait File : Send + Sync { + fn readable(&self) -> bool; + fn writable(&self) -> bool; + fn read(&self, buf: UserBuffer) -> usize; + fn write(&self, buf: UserBuffer) -> usize; + } + + +这个接口在内存和I/O资源之间建立了数据交换的通道。其中 ``UserBuffer`` 是我们在 ``mm`` 子模块中定义的应用地址空间中的一段缓冲区,我们可以将它看成一个 ``&[u8]`` 切片。 + +``read`` 指的是从文件(即I/O资源)中读取数据放到缓冲区中,最多将缓冲区填满(即读取缓冲区的长度那么多字节),并返回实际读取的字节数;而 ``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 的使用方法。 + +标准输入和标准输出 +-------------------------------------------- + +其实我们在第二章就对应用程序引入了基于 **文件** 的标准输出接口 ``sys_write`` ,在第五章引入标准输入接口 ``sys_read`` 。我们提前把标准输出设备在文件描述符表中的文件描述符的值规定为 ``1`` ,用 ``Stdout`` 表示;把标准输入设备文件描述符规定为 ``0``,用 ``Stdin`` 表示 。现在,我们重写这些系统调用,先为标准输入和标准输出实现 ``File`` Trait: + +.. code-block:: rust + :linenos: + + // os/src/fs/stdio.rs + + pub struct Stdin; + + pub struct Stdout; + + impl File for Stdin { + fn readable(&self) -> bool { true } + fn writable(&self) -> bool { false } + 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 readable(&self) -> bool { false } + fn writable(&self) -> bool { true } + 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!`` 宏来输出。 + +文件描述符与文件描述符表 +-------------------------------------------- + +为简化操作系统设计实现,可以让每个进程都带有一个线性的 **文件描述符表** ,记录所有它请求内核打开并可以读写的那些文件集合。而 **文件描述符** (File Descriptor) 则是一个非负整数,表示文件描述符表中一个打开的 **文件描述符** 所处的位置(可理解为数组下标)。进程通过文件描述符,可以在自身的文件描述符表中找到对应的文件记录信息,从而也就找到了对应的文件,并对文件进行读写。当打开( ``open`` )或创建( ``create`` ) 一个文件的时候,如果顺利,内核会返回给应用刚刚打开或创建的文件对应的文件描述符;而当应用想关闭( ``close`` )一个文件的时候,也需要向内核提供对应的文件描述符。 + + +文件I/O操作 +------------------------------------------- + +在进程控制块中加入文件描述符表的相应字段: + +.. 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: TaskContext, + 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: 19-26 + + // 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: unsafe { + UPSafeCell::new(TaskControlBlockInner { + trap_cx_ppn, + base_size: user_sp, + task_cx: TaskContext::goto_trap_return(kernel_stack_top), + 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 时,子进程需要完全继承父进程的文件描述符表来和父进程共享所有文件。这样,即使我们仅手动为初始进程 ``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`` 的符合实际类型的实现。 diff --git a/_sources/chapter6/1fs-interface.rst.txt b/_sources/chapter6/1fs-interface.rst.txt new file mode 100644 index 0000000..9bc27d0 --- /dev/null +++ b/_sources/chapter6/1fs-interface.rst.txt @@ -0,0 +1,112 @@ +文件系统接口 +================================================= + +简易文件与目录抽象 +------------------------------------------------- + +与课堂所学相比,我们实现的文件系统进行了很大的简化: + +- 扁平化:仅存在根目录 ``/`` 一个目录,所有的文件都放在根目录内。直接以文件名索引文件。 +- 不设置用户和用户组概念,不记录文件访问/修改的任何时间戳,不支持软硬链接。 +- 只实现了最基本的文件系统相关系统调用。 + +打开与读写文件的系统调用 +-------------------------------------------------- + +打开文件 +++++++++++++++++++++++++++++++++++++++++++++++++++ + +.. code-block:: rust + + /// 功能:打开一个常规文件,并返回可以访问它的文件描述符。 + /// 参数:path 描述要打开的文件的文件名(简单起见,文件系统不需要支持目录,所有的文件都放在根目录 / 下), + /// flags 描述打开文件的标志,具体含义下面给出。 + /// dirfd 和 mode 仅用于保证兼容性,忽略 + /// 返回值:如果出现了错误则返回 -1,否则返回打开常规文件的文件描述符。可能的错误原因是:文件不存在。 + /// syscall ID:56 + fn sys_openat(dirfd: usize, path: &str, flags: u32, mode: u32) -> isize + +目前我们的内核支持以下几种标志(多种不同标志可能共存): + +- 如果 ``flags`` 为 0,则表示以只读模式 *RDONLY* 打开; +- 如果 ``flags`` 第 0 位被设置(0x001),表示以只写模式 *WRONLY* 打开; +- 如果 ``flags`` 第 1 位被设置(0x002),表示既可读又可写 *RDWR* ; +- 如果 ``flags`` 第 9 位被设置(0x200),表示允许创建文件 *CREATE* ,在找不到该文件的时候应创建文件;如果该文件已经存在则应该将该文件的大小归零; +- 如果 ``flags`` 第 10 位被设置(0x400),则在打开文件的时候应该清空文件的内容并将该文件的大小归零,也即 *TRUNC* 。 + +在用户库 ``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_openat(AT_FDCWD as usize, path, flags.bits, OpenFlags::RDWR.bits) + } + +借助 ``bitflags!`` 宏我们将一个 ``u32`` 的 flags 包装为一个 ``OpenFlags`` 结构体,可以从它的 ``bits`` 字段获得 ``u32`` 表示。 + + +顺序读写文件 +++++++++++++++++++++++++++++++++++++++++++++++++++ + +在打开一个文件之后,我们就可以用之前的 ``sys_read/sys_write`` 两个系统调用来对它进行读写了。本教程只实现文件的顺序读写,而不考虑随机读写。 + +以本章的测试用例 ``ch6b_filetest_simple`` 来介绍文件系统接口的使用方法: + +.. code-block:: rust + :linenos: + + // user/src/bin/ch6b_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!`` 而后关闭文件。 +- 第 27~32 行,我们以只读 的方式将文件 ``filea`` 的内容读取到缓冲区 ``buffer`` 中。 ``filea`` 的总大小不超过缓冲区的大小,因此通过单次 ``read`` 即可将内容全部读出来而更常见的情况是需要进行多次 ``read`` ,直到返回值为 0 才能确认文件已被读取完毕。 diff --git a/_sources/chapter6/2fs-implementation-1.rst.txt b/_sources/chapter6/2fs-implementation-1.rst.txt new file mode 100644 index 0000000..b79fa3d --- /dev/null +++ b/_sources/chapter6/2fs-implementation-1.rst.txt @@ -0,0 +1,674 @@ +简易文件系统 easy-fs (上) +======================================= + +松耦合模块化设计思路 +--------------------------------------- + +内核的功能越来越多,代码量也越来越大。出于解耦合考虑,文件系统 easy-fs 被从内核中分离出来,分成两个不同的 crate : + +- ``easy-fs`` 是简易文件系统的本体; +- ``easy-fs-fuse`` 是能在开发环境(如 Ubuntu)中运行的应用程序,用于将应用打包为 easy-fs 格式的文件系统镜像,也可以用来对 ``easy-fs`` 进行测试。 + +easy-fs与底层设备驱动之间通过抽象接口 ``BlockDevice`` 来连接,采用轮询方式访问 ``virtio_blk`` 虚拟磁盘设备,避免调用外设中断的相关内核函数。easy-fs 避免了直接访问进程相关的数据和函数,从而能独立于内核开发。 + +``easy-fs`` crate 以层次化思路设计,自下而上可以分成五个层次: + +1. 磁盘块设备接口层:以块为单位对磁盘块设备进行读写的 trait 接口 +2. 块缓存层:在内存中缓存磁盘块的数据,避免频繁读写磁盘 +3. 磁盘数据结构层:磁盘上的超级块、位图、索引节点、数据块、目录项等核心数据结构和相关处理 +4. 磁盘块管理器层:合并了上述核心数据结构和磁盘布局所形成的磁盘文件系统数据结构 +5. 索引节点层:管理索引节点,实现了文件创建/文件打开/文件读写等成员函数 + +本节将介绍前三层,下一节将介绍后两层。 + +.. image:: easy-fs-demo.png + :align: center + +块设备接口层 +--------------------------------------- + +在 ``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`` 的使用者将负责提供抽象方法的实现。 + +块缓存层 +--------------------------------------- + +为了加速 IO,内存可以作为磁盘的缓存。实现磁盘块缓存功能的代码在 ``block_cache.rs`` 。 + +块缓存 ++++++++++++++++++++++++++++++++++++++++++ + +块缓存 ``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`` 时,将一个块从磁盘读到缓冲区 ``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, + } + } + } + +``BlockCache`` 向上提供以下方法: + +.. 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 表示该缓冲区已经被修改,之后需要将数据写回磁盘块才能真正将修改同步到磁盘。 + +我们可以将 ``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`` 不可变/可变引用,将闭包 ``f`` 作用于这个引用,返回 ``f`` 的返回值。 中所定义的操作。 + +这里我们传入闭包的类型为 ``FnOnce`` ,这是因为闭包里面的变量被捕获的方式涵盖了不可变引用/可变引用/和 move 三种可能性,故而我们需要选取范围最广的 ``FnOnce`` 。参数中的 ``impl`` 关键字体现了一种类似泛型的静态分发功能。 + +.. warning:: + + **Rust 语法卡片:闭包** + + 闭包是持有外部环境变量的函数。所谓外部环境, 就是指创建闭包时所在的词法作用域。Rust中定义的闭包,按照对外部环境变量的使用方式(借用、复制、转移所有权),分为三个类型: Fn、FnMut、FnOnce。Fn类型的闭包会在闭包内部以共享借用的方式使用环境变量;FnMut类型的闭包会在闭包内部以独占借用的方式使用环境变量;而FnOnce类型的闭包会在闭包内部以所有者的身份使用环境变量。由此可见,根据闭包内使用环境变量的方式,即可判断创建出来的闭包的类型。 + + +当 ``BlockCache`` 的生命周期结束后,缓冲区也会被回收, ``modified`` 标记将会决定数据是否需要写回磁盘: + +.. code-block:: rust + + // easy-fs/src/block_cache.rs + + impl Drop for BlockCache { + fn drop(&mut self) { + if self.modified { + self.modified = false; + self.block_device.write_block(self.block_id, &self.cache); + } + } + } + +块缓存全局管理器 ++++++++++++++++++++++++++++++++++++++++++ + +内存只能同时缓存有限个磁盘块。当我们要对一个磁盘块进行读写时,块缓存全局管理器检查它是否已经被载入内存中,如果是则直接返回,否则就读取磁盘块到内存。如果内存中驻留的磁盘块缓冲区的数量已满,则需要进行缓存替换。这里使用一种类 FIFO 的缓存替换算法,在管理器中只需维护一个队列: + +.. code-block:: rust + + // easy-fs/src/block_cache.rs + + use alloc::collections::VecDeque; + + pub struct BlockCacheManager { + queue: VecDeque<(usize, Arc>)>, + } + +队列 ``queue`` 维护块编号和块缓存的二元组。块缓存的类型是一个 ``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 行对应找不到的情况,此时必须将块从磁盘读入内存中的缓冲区。读取前需要判断已保存的块数量是否达到了上限。是,则执行缓存替换算法,替换的标准是其强引用计数 :math:`\eq 1` ,即除了块缓存管理器保留的一份副本之外,在外面没有副本正在使用。 +- 第 27 行开始,创建一个新的块缓存(会触发 ``read_block`` 进行块读取)并加入到队尾,最后返回给请求者。 + +磁盘布局及磁盘上数据结构 +--------------------------------------- + +磁盘数据结构层的代码在 ``layout.rs`` 和 ``bitmap.rs`` 中。 + +easy-fs 磁盘布局概述 ++++++++++++++++++++++++++++++++++++++++ + +easy-fs 磁盘按照块编号从小到大顺序分成 5 个连续区域: + +- 第一个区域只包括一个块,它是 **超级块** (Super Block),用于定位其他连续区域的位置,检查文件系统合法性。 +- 第二个区域是一个索引节点位图,长度为若干个块。它记录了索引节点区域中有哪些索引节点已经被分配出去使用了。 +- 第三个区域是索引节点区域,长度为若干个块。其中的每个块都存储了若干个索引节点。 +- 第四个区域是一个数据块位图,长度为若干个块。它记录了后面的数据块区域中有哪些已经被分配出去使用了。 +- 最后的区域则是数据块区域,其中的每个被分配出去的块保存了文件或目录的具体内容。 + +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, + ); + + pub fn is_valid(&self) -> bool { + self.magic == EFS_MAGIC + } + } + +- ``initialize`` 用于在创建一个 easy-fs 的时候初始化超级块,各个区域的块数由更上层的磁盘块管理器传入。 +- ``is_valid`` 则可以通过魔数判断超级块所在的文件系统是否合法。 + +位图 ++++++++++++++++++++++++++++++++++++++++ + +在 easy-fs 布局中存在两类不同的位图,分别对索引节点和数据块进行管理。每个位图都由若干个块组成,每个块大小 4096 bits。每个 bit 都代表一个索引节点/数据块的分配状态。 + +.. code-block:: rust + + // easy-fs/src/bitmap.rs + + pub struct Bitmap { + start_block_id: usize, + blocks: usize, + } + + type BitmapBlock = [u64; 64]; + + +``Bitmap`` 是位图区域的管理器,它保存了位图区域的起始块编号和块数。而 ``BitmapBlock`` 将位图区域中的一个磁盘块解释为长度为 64 的一个 ``u64`` 数组。 + +首先来看 ``Bitmap`` 如何分配一个bit: + +.. 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 + } + } + +其主要思路是遍历区域中的每个块,再在每个块中以bit组(每组 64 bits)为单位进行遍历,找到一个尚未被全部分配出去的组,最后在里面分配一个bit。它将会返回分配的bit所在的位置,等同于索引节点/数据块的编号。如果所有bit均已经被分配出去了,则返回 ``None`` 。 + +第 7 行枚举区域中的每个块(编号为 ``block_id`` ),在循环内部我们需要读写这个块,在块内尝试找到一个空闲的bit并置 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`` 中找到一个空闲的bit并返回其位置,如果不存在的话则返回 ``None`` 。它的思路是,遍历每 64 bits构成的组(一个 ``u64`` ),如果它并没有达到 ``u64::MAX`` (即 :math:`2^{64}-1` ),则通过 ``u64::trailing_ones`` 找到最低的一个 0 并置为 1 。如果能够找到的话,bit组的编号将保存在变量 ``bits64_pos`` 中,而分配的bit在组内的位置将保存在变量 ``inner_pos`` 中。在返回分配的bit编号的时候,它的计算方式是 ``block_id*BLOCK_BITS+bits64_pos*64+inner_pos`` 。注意闭包中的 ``block_id`` 并不在闭包的参数列表中,因此它是从外部环境(即自增 ``block_id`` 的循环)中捕获到的。 + +我们一旦在某个块中找到一个空闲的bit并成功分配,就不再考虑后续的块。第 28 行体现了提前返回的思路。 + +回收 bit 的方法类似,感兴趣的读者可自行阅读源代码。 + +磁盘上索引节点 ++++++++++++++++++++++++++++++++++++++++ + +在磁盘上的索引节点区域,每个块上都保存着若干个索引节点 ``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`` 将文件大小扩充到区间的右端保证写入的完整性。 + +目录项 ++++++++++++++++++++++++++++++++++++++++ + +对于文件而言,它的内容在文件系统或内核看来没有任何既定的格式,只是一个字节序列。目录的内容却需要遵从一种特殊的格式,它可以看成一个目录项的序列,每个目录项都是一个二元组,包括目录下文件的文件名和索引节点编号。目录项 ``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。目录项自身长 32 字节,每个数据块可以存储 16 个目录项。可以通过 ``empty`` 和 ``new`` 方法生成目录项,通过 ``name`` 和 ``inode_number`` 方法取出目录项中的内容: + +.. code-block:: rust + + // easy-fs/src/layout.rs + + impl DirEntry { + pub fn empty() -> Self; + pub fn new(name: &str, inode_number: u32) -> Self; + pub fn name(&self) -> &str; + pub fn inode_number(&self) -> u32 + } + +在从目录中读取目录项,或将目录项写入目录时,需要将目录项转化为缓冲区(即字节切片)的形式来符合 ``read_at OR 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, + ) + } + } + } diff --git a/_sources/chapter6/2fs-implementation-2.rst.txt b/_sources/chapter6/2fs-implementation-2.rst.txt new file mode 100644 index 0000000..d8f0a57 --- /dev/null +++ b/_sources/chapter6/2fs-implementation-2.rst.txt @@ -0,0 +1,591 @@ +简易文件系统 easy-fs (下) +======================================= + + +磁盘块管理器 +--------------------------------------- + +本层的代码在 ``efs.rs`` 中。 + +.. 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 位图中的每个bit都能够有一个实际的 inode 可以对应,这样就确定了 inode 位图区域和 inode 区域的大小。剩下的块都分配给数据块位图区域和数据块区域。我们希望数据块位图中的每个bit仍然能够对应到一个数据块,但是数据块位图又不能过小,不然会造成某些数据块永远不会被使用。因此数据块位图区域最合理的大小是剩余的块数除以 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位图 或数据块位图上分配的 bit 编号,来算出各个存储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`` 分配/回收数据块传入/返回的参数都表示数据块在块设备上的编号,而不是在数据块位图中分配的bit编号; +- ``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); + // acquire efs lock temporarily + let (block_id, block_offset) = efs.lock().get_disk_inode_pos(0); + // release efs lock + Inode::new( + block_id, + block_offset, + Arc::clone(efs), + block_device, + ) + } + } + + // easy-fs/src/vfs.rs + + impl Inode { + /// We should not acquire efs lock here. + pub fn new( + block_id: u32, + block_offset: usize, + fs: Arc>, + block_device: Arc, + ) -> Self { + Self { + block_id: block_id as usize, + block_offset, + fs, + block_device, + } + } + } + +在 ``root_inode`` 中,主要是在 ``Inode::new`` 的时候将传入的 ``inode_id`` 设置为 0 ,因为根目录对应于文件系统中第一个分配的 inode ,因此它的 ``inode_id`` 总会是 0 。同时在设计上,我们不会在 ``Inode::new`` 中尝试获取整个 ``EasyFileSystem`` 的锁来查询 inode 在块设备中的位置,而是在调用它之前预先查询并作为参数传过去。 + +文件索引 ++++++++++++++++++++++++++++++++++++++++ + +为了尽可能简化我们的实现,所有的文件都在根目录下面。于是,我们不必实现目录索引。文件索引的查找比较简单,仅需在根目录的目录项中根据文件名找到文件的 inode 编号即可。由于没有子目录的存在,这个过程只会进行一次。 + +.. code-block:: rust + + // easy-fs/src/vfs.rs + + impl Inode { + pub fn find(&self, name: &str) -> Option> { + let fs = self.fs.lock(); + self.read_disk_inode(|disk_inode| { + self.find_inode_id(name, disk_inode) + .map(|inode_id| { + let (block_id, block_offset) = fs.get_disk_inode_pos(inode_id); + Arc::new(Self::new( + block_id, + block_offset, + 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`` 的互斥锁(相对的,文件系统内部的操作如之前的 ``Inode::new`` 或是上面的 ``find_inode_id`` 都是假定在已持有 efs 锁的情况下才被调用的,因此它们不应尝试获取锁)。这能够保证在多核情况下,同时最多只能有一个核在进行文件系统相关操作。这样也许会带来一些不必要的性能损失,但我们目前暂时先这样做。如果我们在这里加锁的话,其实就能够保证块缓存的互斥访问了。 + +文件列举 ++++++++++++++++++++++++++++++++++++++++ + +``ls`` 方法可以收集根目录下的所有文件的文件名并以向量的形式返回,这个方法只有根目录的 ``Inode`` 才会调用: + +.. code-block:: rust + + // easy-fs/src/vfs.rs + + impl Inode { + pub fn ls(&self) -> Vec { + let _fs = 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, + ); + }); + + let (block_id, block_offset) = fs.get_disk_inode_pos(new_inode_id); + // return inode + Some(Arc::new(Self::new( + block_id, + block_offset, + self.fs.clone(), + self.block_device.clone(), + ))) + // release efs lock automatically by compiler + } + } + +- 第 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 _fs = 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 镜像 +--------------------------------------- + +在第六章中我们需要将所有的应用都链接到内核中,随后在应用管理器中通过应用名进行索引来找到应用的 ELF 数据。这样做有一个缺点,就是会造成内核体积过度膨胀。同时这也会浪费内存资源,因为未被执行的应用也占据了内存空间。在实现了我们自己的文件系统之后,终于可以将这些应用打包到 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 文件(这是一个 HostOS 上的文件)并将数据读入内存。接着需要在我们的 easy-fs 中创建一个同名文件并将 ELF 数据写入到这个文件中。这个过程相当于将 HostOS 上的文件系统中的一个文件复制到我们的 easy-fs 中。 + +尽管没有进行任何同步写回磁盘的操作,我们也不用担心块缓存中的修改没有写回磁盘。因为在 ``easy-fs-fuse`` 这个应用正常退出的过程中,块缓存因生命周期结束会被回收,届时如果 ``modified`` 标志为 true 就会将修改写回磁盘。 \ No newline at end of file diff --git a/_sources/chapter6/3using-easy-fs-in-kernel.rst.txt b/_sources/chapter6/3using-easy-fs-in-kernel.rst.txt new file mode 100644 index 0000000..b4c60d7 --- /dev/null +++ b/_sources/chapter6/3using-easy-fs-in-kernel.rst.txt @@ -0,0 +1,313 @@ +在内核中使用 easy-fs +=============================================== + +块设备驱动层 +----------------------------------------------- + +在 ``drivers`` 子模块中的 ``block/mod.rs`` 中,我们可以找到内核访问的块设备实例 ``BLOCK_DEVICE`` : + +.. code-block:: rust + + // os/src/drivers/block/mod.rs + + type BlockDeviceImpl = virtio_blk::VirtIOBlock; + + lazy_static! { + pub static ref BLOCK_DEVICE: Arc = Arc::new(BlockDeviceImpl::new()); + } + +在 qemu 上,我们使用 ``VirtIOBlock`` 访问 VirtIO 块设备,并将它全局实例化为 ``BLOCK_DEVICE`` ,使内核的其他模块可以访问。 + +在启动 Qemu 模拟器的时候,我们可以配置参数来添加一块 VirtIO 块设备: + +.. code-block:: makefile + :linenos: + :emphasize-lines: 11-12 + + # os/Makefile + + FS_IMG := ../user/target/$(TARGET)/$(MODE)/fs.img + + run: build + @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 + +- 第 11 行,我们为虚拟机添加一块虚拟硬盘,内容为我们之前通过 ``easy-fs-fuse`` 工具打包的包含应用 ELF 的 easy-fs 镜像,并命名为 ``x0`` 。 +- 第 12 行,我们将硬盘 ``x0`` 作为一个 VirtIO 总线中的一个块设备接入到虚拟机系统中。 ``virtio-mmio-bus.0`` 表示 VirtIO 总线通过 MMIO 进行控制,且该块设备在总线中的编号为 0 。 + +**内存映射 I/O** (MMIO, Memory-Mapped I/O) 指通过特定的物理内存地址来访问外设的设备寄存器。查阅资料,可知 VirtIO 总线的 MMIO 物理地址区间为从 0x10001000 开头的 4KiB 。 + +在 ``config`` 子模块中我们硬编码 Qemu 上的 VirtIO 总线的 MMIO 地址区间(起始地址,长度)。在创建内核地址空间的时候需要建立页表映射: + +.. code-block:: rust + + // os/src/config.rs + + pub const MMIO: &[(usize, usize)] = &[ + (0x10001000, 0x1000), + ]; + + // 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 + } + } + +这里我们进行的是透明的恒等映射,让内核可以兼容于直接访问物理地址的设备驱动库。 + +由于设备驱动的开发过程比较琐碎,我们这里直接使用已有的 `virtio-drivers `_ crate,感兴趣的同学可以自行了解。 + + +内核索引节点层 +----------------------------------------------- + +内核将 ``easy-fs`` 提供的 ``Inode`` 进一步封装为 OS 中的索引节点 ``OSInode`` 。 + +.. code-block:: rust + + // os/src/fs/inode.rs + + pub struct OSInode { + readable: bool, + writable: bool, + inner: UPSafeCell, + } + + pub struct OSInodeInner { + offset: usize, + inode: Arc, + } + +``OSInode`` 就表示进程中一个被打开的常规文件或目录。 ``readable/writable`` 分别表明该文件是否允许通过 ``sys_read/write`` 进行读写,读写过程中的偏移量 ``offset`` 和 ``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 + } + } + +``read/write`` 的实现也比较简单,只需遍历 ``UserBuffer`` 中的每个缓冲区片段,调用 ``Inode`` 写好的 ``read/write_at`` 接口就好了。注意 ``read/write_at`` 的起始位置是在 ``OSInode`` 中维护的 ``offset`` ,这个 ``offset`` 也随着遍历的进行被持续更新。在 ``read/write`` 的全程需要获取 ``OSInode`` 的互斥锁,保证两个进程无法同时访问同个文件。 + +本章我们为 ``File`` Trait 新增了 ``readable/writable`` 两个抽象接口,从而在 ``sys_read/sys_write`` 的时候进行简单的访问权限检查。 + +文件系统相关内核机制实现 +----------------------------------------------- + +文件系统初始化 ++++++++++++++++++++++++++++++++++++++++++++++++ + +为了使用 ``easy-fs`` 提供的抽象,内核需要进行一些初始化操作。我们需要从块设备 ``BLOCK_DEVICE`` 上打开文件系统,并从文件系统中获取根目录的 inode 。 + + +.. 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`` 的相关接口了。例如,在文件系统初始化完毕之后,调用 ``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`` 各标志位的语义。例如只有 ``flags`` 参数包含 `CREATE` 标志位才允许创建文件;而如果文件已经存在,则清空文件的内容。 + +在其基础上, ``sys_open`` 也就很容易实现了。 + +通过 sys_exec 加载并执行应用 ++++++++++++++++++++++++++++++++++++++++++++++++ + +有了文件系统支持后, ``sys_exec`` 所需的表示应用 ELF 格式数据改为从文件系统中获取: + +.. code-block:: rust + :linenos: + :emphasize-lines: 17-25 + + // 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); + argc as isize + } else { + -1 + } + +注意上面代码片段中的高亮部分。当执行获取应用的 ELF 数据的操作时,首先调用 ``open_file`` 函数,以只读的方式在内核中打开应用文件并获取它对应的 ``OSInode`` 。接下来可以通过 ``OSInode::read_all`` 将该文件的数据全部读到一个向量 ``all_data`` 中: + +之后,就可以从向量 ``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("ch6b_initproc", OpenFlags::RDONLY).unwrap(); + let v = inode.read_all(); + TaskControlBlock::new(v.as_slice()) + }); + } diff --git a/_sources/chapter6/4exercise.rst.txt b/_sources/chapter6/4exercise.rst.txt new file mode 100644 index 0000000..31f6f9a --- /dev/null +++ b/_sources/chapter6/4exercise.rst.txt @@ -0,0 +1,114 @@ +chapter6练习 +================================================ + +Lab4 编程作业 +------------------------------------------------- + +硬链接 +++++++++++++++++++++++++++++++++++++++++++++++++++ + +硬链接要求两个不同的目录项指向同一个文件,在我们的文件系统中也就是两个不同名称目录项指向同一个磁盘块。 + +本节要求实现三个系统调用 ``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 彻底删除文件的情况,此时需要回收inode以及它对应的数据块。 + * 返回值:如果出现了错误则返回 -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 { + /// 文件所在磁盘驱动器号,该实验中写死为 0 即可 + 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; + } + } + + +实验要求 ++++++++++++++++++++++++++++++++++++++++++++++ +- `lab4(os6)参考框架: `_ +- 实验目录要求不变。 +- 通过所有测例。 + + 在 ``os6`` 目录下 ``make run BASE=2`` 加载所有测例, ``ch6_usertest`` 打包了所有你需要通过的测例,你也可以通过修改这个文件调整本地测试的内容。 + + 你的内核必须前向兼容,能通过前一章的所有测例。 + +.. note:: + + **如何调试 easy-fs** + + 如果你在第一章练习题中已经借助 ``log`` crate 实现了日志功能,那么你可以直接在 ``easy-fs`` 中引入 ``log`` crate,通过 ``log::info!/debug!`` 等宏即可进行调试并在内核中看到日志输出。具体来说,在 ``easy-fs`` 中的修改是:在 ``easy-fs/Cargo.toml`` 的依赖中加入一行 ``log = "0.4.0"``,然后在 ``easy-fs/src/lib.rs`` 中加入一行 ``extern crate log`` 。 + + 你也可以完全在用户态进行调试。仿照 ``easy-fs-fuse`` 建立一个在当前操作系统中运行的应用程序,将测试逻辑写在 ``main`` 函数中。这个时候就可以将它引用的 ``easy-fs`` 的 ``no_std`` 去掉并使用 ``println!`` 进行调试。 + + +问答作业 +---------------------------------------------------------- + +1. 在我们的easy-fs中,root inode起着什么作用?如果root inode中的内容损坏了,会发生什么? + +报告要求 +----------------------------------------------------------- +- 简单总结你实现的功能(200字以内,不要贴代码)。 +- 完成问答题。 +- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 diff --git a/_sources/chapter6/index.rst.txt b/_sources/chapter6/index.rst.txt new file mode 100644 index 0000000..92a5d7d --- /dev/null +++ b/_sources/chapter6/index.rst.txt @@ -0,0 +1,13 @@ +第六章:文件系统与I/O重定向 +============================================== + +.. toctree:: + :maxdepth: 4 + + 0intro + 1file-descriptor.rst + 1fs-interface + 2fs-implementation-1 + 2fs-implementation-2 + 3using-easy-fs-in-kernel + 4exercise diff --git a/_sources/chapter7/0intro.rst.txt b/_sources/chapter7/0intro.rst.txt new file mode 100644 index 0000000..0791ba1 --- /dev/null +++ b/_sources/chapter7/0intro.rst.txt @@ -0,0 +1,120 @@ +引言 +========================================= + +本章导读 +----------------------------------------- + +本章将基于文件描述符实现父子进程之间的通信机制——管道。 +我们还将扩展 ``exec`` 系统调用,使之能传递运行参数,并进一步改进 shell 程序,使其支持重定向符号 ``>`` 和 ``<`` 。 + +实践体验 +----------------------------------------- + + +获取本章代码: + +.. code-block:: console + + $ git clone https://github.com/LearningOS/rust-based-os-comp2022.git + $ cd rust-based-os-comp2022/ + //$ make setupclassroom //注意:在本章不需要做这一步,因为这不是一个作业。(这一步很重要,是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次,以后一般就不用执行了,除非 .github/workflows/classroom.yml发生了变化。) + + +在 qemu 模拟器上运行 `os7参考框架: `_ : + +.. code-block:: console + + $ cd os7-ref + $ make run + +进入shell程序后,可以运行管道机制的简单测例 ``ch7b_pipetest``, ``ch7b_pipetest`` 需要保证父进程通过管道传输给子进程的字符串不会发生变化。 + +测例输出大致如下: + +.. code-block:: + + >> ch7b_pipetest + Read OK, child process exited! + pipetest passed! + Shell: Process 2 exited with code 0 + >> + +同样的,也可以运行较为复杂的测例 ``ch7b_pipe_large_test``,体验通过两个管道实现双向通信。 + +此外,在本章我们为shell程序支持了输入/输出重定向功能,可以将一个应用的输出保存到一个指定的文件。例如,下面的命令可以将 ``ch7b_yield`` 应用的输出保存在文件 ``fileb`` 当中,并在应用执行完毕之后确认它的输出: + +.. code-block:: + + >> ch7b_yield > fileb + Shell: Process 2 exited with code 0 + >> ch7b_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 + >> + +`os7参考框架: `_ +----------------------------------------------------------------------------------------------------------------- + +.. code-block:: + + ── os7-ref +    └── src +    ├── ... +    ├── fs +    │   ├── inode.rs +    │   ├── mod.rs +    │   ├── pipe.rs(新增:实现了 File Trait 的第三个实现——可用来进程间通信的管道) +    │   └── stdio.rs +    ├── mm +    │   ├── address.rs +    │   ├── frame_allocator.rs +    │   ├── heap_allocator.rs +    │   ├── memory_set.rs +    │   ├── mod.rs +    │   └── page_table.rs +    ├── syscall +    │   ├── fs.rs(修改:添加了sys_pipe和sys_dup) +    │   ├── mod.rs +    │   └── process.rs(修改:sys_exec添加了对参数的支持) +    ├── task +       ├── context.rs +       ├── manager.rs +       ├── mod.rs +       ├── pid.rs +       ├── processor.rs +       ├── switch.rs +       ├── switch.S +       └── task.rs(修改:在exec中将参数压入用户栈中) + + cloc easy-fs os + ------------------------------------------------------------------------------- + Language files blank comment code + ------------------------------------------------------------------------------- + Rust 42 317 434 3574 + Assembly 4 53 26 526 + make 1 13 4 48 + TOML 2 4 2 23 + ------------------------------------------------------------------------------- + SUM: 49 387 466 4171 + ------------------------------------------------------------------------------- + + +.. 本章代码导读 +.. ----------------------------------------------------- + +.. 在本章第一节 :doc:`/chapter6/1file-descriptor` 中,我们引入了文件的概念,用它来代表进程可以读写的多种被内核管理的硬件/软件资源。进程必须通过系统调用打开一个文件,将文件加入到自身的文件描述符表中,才能通过文件描述符(也就是某个特定文件在自身文件描述符表中的下标)来读写该文件。 + +.. 文件的抽象 Trait ``File`` 声明在 ``os/src/fs/mod.rs`` 中,它提供了 ``read/write`` 两个接口,可以将数据写入应用缓冲区抽象 ``UserBuffer`` ,或者从应用缓冲区读取数据。应用缓冲区抽象类型 ``UserBuffer`` 来自 ``os/src/mm/page_table.rs`` 中,它将 ``translated_byte_buffer`` 得到的 ``Vec<&'static mut [u8]>`` 进一步包装,不仅保留了原有的分段读写能力,还可以将其转化为一个迭代器逐字节进行读写,这在读写一些流式设备的时候特别有用。 + +.. 在进程控制块 ``TaskControlBlock`` 中需要加入文件描述符表字段 ``fd_table`` ,可以看到它是一个向量,里面保存了若干实现了 ``File`` Trait 的文件,由于采用动态分发,文件的类型可能各不相同。 ``os/src/syscall/fs.rs`` 的 ``sys_read/write`` 两个读写文件的系统调用需要访问当前进程的文件描述符表,用应用传入内核的文件描述符来索引对应的已打开文件,并调用 ``File`` Trait 的 ``read/write`` 接口; ``sys_close`` 这可以关闭一个文件。调用 ``TaskControlBlock`` 的 ``alloc_fd`` 方法可以在文件描述符表中分配一个文件描述符。进程控制块的其他操作也需要考虑到新增的文件描述符表字段的影响,如 ``TaskControlBlock::new`` 的时候需要对 ``fd_table`` 进行初始化, ``TaskControlBlock::fork`` 中则需要将父进程的 ``fd_table`` 复制一份给子进程。 + +.. 到本章为止我们支持两种文件:标准输入输出和管道。不同于前面章节,我们将标准输入输出分别抽象成 ``Stdin`` 和 ``Stdout`` 两个类型,并为他们实现 ``File`` Trait 。在 ``TaskControlBlock::new`` 创建初始进程的时候,就默认打开了标准输入输出,并分别绑定到文件描述符 0 和 1 上面。 + +.. 管道 ``Pipe`` 是另一种文件,它可以用于父子进程间的单向进程间通信。我们也需要为它实现 ``File`` Trait 。 ``os/src/syscall/fs.rs`` 中的系统调用 ``sys_pipe`` 可以用来打开一个管道并返回读端/写端两个文件的文件描述符。管道的具体实现在 ``os/src/fs/pipe.rs`` 中,本章第二节 :doc:`/chapter6/2pipe` 中给出了详细的讲解。管道机制的测试用例可以参考 ``user/src/bin`` 目录下的 ``pipetest.rs`` 和 ``pipe_large_test.rs`` 两个文件。 diff --git a/_sources/chapter7/1pipe.rst.txt b/_sources/chapter7/1pipe.rst.txt new file mode 100644 index 0000000..29ab917 --- /dev/null +++ b/_sources/chapter7/1pipe.rst.txt @@ -0,0 +1,364 @@ +管道 +============================================ + +管道的系统调用原型及使用方法 +-------------------------------------------- + +新增为当前进程打开一个管道(包含一个只读文件,一个只写文件)的系统调用: + +.. 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/lib.rs + + pub fn pipe(pipe_fd: &mut [usize]) -> isize { sys_pipe(pipe_fd) } + +只有当一个管道的所有读端文件/写端文件都被关闭之后,管道占用的资源才会被回收。 + +.. code-block:: rust + + /// 功能:当前进程关闭一个文件。 + /// 参数:fd 表示要关闭的文件的文件描述符。 + /// 返回值:如果成功关闭则返回 0 ,否则返回 -1 。可能的出错原因:传入的文件描述符并不对应一个打开的文件。 + /// syscall ID:57 + pub fn sys_close(fd: usize) -> isize; + +它会在用户库中被包装为 ``close`` 函数。 + +我们从测例 ``ch7b_pipetest`` 中理解管道的使用方法: + +.. code-block:: rust + :linenos: + + // user/src/bin/ch7b_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 行关闭。 + +如果想在父子进程之间实现双向通信,我们就必须创建两个管道。有兴趣的读者可以参考测例 ``ch7b_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`` 的原理类似,篇幅所限在这里不再赘述,感兴趣的读者可自行查阅。 diff --git a/_sources/chapter7/2cmdargs-and-redirection.rst.txt b/_sources/chapter7/2cmdargs-and-redirection.rst.txt new file mode 100644 index 0000000..87ab705 --- /dev/null +++ b/_sources/chapter7/2cmdargs-and-redirection.rst.txt @@ -0,0 +1,337 @@ +命令行参数与标准 I/O 重定向 +================================================= + + +命令行参数 +------------------------------------------------- + +使用 C 语言开发 Linux 应用时,可以使用标准库提供的 ``argc/argv`` 来获取命令行参数,我们希望在我们自己的内核和shell程序上支持这个功能。为了支持命令行参数, ``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) } + + +shell程序的命令行参数分割 ++++++++++++++++++++++++++++++++++++++++++++++++++ + +回忆一下,在shell程序 ``user_shell`` 中,一旦接收到一个回车,我们就会将当前行的内容 ``line`` 作为一个名字并试图去执行同名的应用。但是现在 ``line`` 还可能包含一些命令行参数,只有最开头的一个才是要执行的应用名。因此我们要做的第一件事情就是将 ``line`` 用空格分割: + +.. code-block:: rust + + // user/src/bin/ch6b_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`` 中的字符串传入内核了。我们用 ``args_addr`` 来收集这些字符串的起始地址: + +.. code-block:: rust + + // user/src/bin/ch6b_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`` 出来的子进程中,我们调用 ``exec`` 传入命令行参数。 + +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 + } + } + +每次我们都可以从一个起始地址通过 ``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 + user_sp -= user_sp % core::mem::size_of::(); + + // **** access current TCB exclusively + let mut inner = self.inner_exclusive_access(); + // 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.exclusive_access().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 + } + } + +第 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 字节对齐,在 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]`` 。 + +有了命令行参数支持,我们就可以编写命令行工具 ``ch6b_cat`` 来输出指定文件的内容了。读者可以自行参阅其实现。 + +标准输入输出重定向 +------------------------------------------------- + +为了增强 shell 程序使用文件系统时的灵活性,我们需要新增标准输入输出重定向功能。 + +重定向功能对于应用来说是透明的。在应用中除非明确指出了数据要从指定的文件输入或者输出到指定的文件,否则数据默认都是输入自进程文件描述表位置 0 处的标准输入,并输出到进程文件描述符表位置 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 + } + +在 ``sys_dup`` 函数中,首先检查传入 ``fd`` 的合法性。然后在文件描述符表中分配一个新的文件描述符,并保存 ``fd`` 指向的已打开文件的一份拷贝即可。 + +在shell程序 ``user_shell`` 分割命令行参数的时候,我们要检查是否存在通过 ``<`` 或 ``>`` 进行输入输出重定向的情况,如果存在的话则需要将它们从命令行参数中移除,并记录匹配到的输入文件名或输出文件名到字符串 ``input`` 或 ``output`` 中。注意,为了实现方便,我们这里假设输入shell程序的命令一定合法:即 ``<`` 或 ``>`` 最多只会出现一次,且后面总是会有一个参数作为重定向到的文件。 + +.. code-block:: rust + + // user/src/bin/ch6b_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`` 打开文件的标志不太相同 diff --git a/_sources/chapter7/3exercise.rst.txt b/_sources/chapter7/3exercise.rst.txt new file mode 100644 index 0000000..fc6ff3f --- /dev/null +++ b/_sources/chapter7/3exercise.rst.txt @@ -0,0 +1,20 @@ +chapter7练习 +=========================================== + +编程作业 +------------------------------------------- + +本章无编程作业 + +问答作业 +------------------------------------------- + +(1) 举出使用 pipe 的一个实际应用的例子。 + +(2) 如果需要在多个进程间互相通信,则需要为每一对进程建立一个管道,非常繁琐,请设计一个更易用的多进程通信机制。 + +报告要求 +--------------------------------------- + +- 完成问答题。 +- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 diff --git a/_sources/chapter7/index.rst.txt b/_sources/chapter7/index.rst.txt new file mode 100644 index 0000000..692cf6d --- /dev/null +++ b/_sources/chapter7/index.rst.txt @@ -0,0 +1,10 @@ +第七章:进程间通信 +============================================== + +.. toctree:: + :maxdepth: 4 + + 0intro + 1pipe + 2cmdargs-and-redirection + 3exercise diff --git a/_sources/chapter8/0intro.rst.txt b/_sources/chapter8/0intro.rst.txt new file mode 100644 index 0000000..458ce44 --- /dev/null +++ b/_sources/chapter8/0intro.rst.txt @@ -0,0 +1,254 @@ +引言 +========================================= + +本章导读 +----------------------------------------- + +到本章开始之前,我们好像已经完成了组成应用程序执行环境的操作系统的三个重要抽象:进程、地址空间和文件, +让应用程序开发、运行和存储数据越来越方便和灵活。有了进程以后,可以让操作系统从宏观层面实现多个应用的并发执行, +而并发是通过操作系统基于处理器的时间片不断地切换进程来达到的。到目前为止的并发,仅仅是进程间的并发, +对于一个进程内部还没有并发性的体现。而这就是线程(Thread)出现的起因:提高一个进程内的并发性。 + +.. chyyuu + https://en.wikipedia.org/wiki/Per_Brinch_Hansen 关于操作系统并发 Binch Hansen 和 Hoare ??? + https://en.wikipedia.org/wiki/Thread_(computing) 关于线程 + http://www.serpentine.com/blog/threads-faq/the-history-of-threads/ The history of threads + https://en.wikipedia.org/wiki/Core_War 我喜欢的一种早期游戏 + [Dijkstra, 65] Dijkstra, E. W., Cooperating sequential processes, in Programming Languages, Genuys, F. (ed.), Academic Press, 1965. + [Saltzer, 66] Saltzer, J. H., Traffic control in a multiplexed computer system, MAC-TR-30 (Sc.D. Thesis), July, 1966. + https://en.wikipedia.org/wiki/THE_multiprogramming_system + http://www.cs.utexas.edu/users/EWD/ewd01xx/EWD196.PDF + https://en.wikipedia.org/wiki/Edsger_W._Dijkstra + https://en.wikipedia.org/wiki/Per_Brinch_Hansen + https://en.wikipedia.org/wiki/Tony_Hoare + https://en.wikipedia.org/wiki/Mutual_exclusion + https://en.wikipedia.org/wiki/Semaphore_(programming) + https://en.wikipedia.org/wiki/Monitor_(synchronization) + Dijkstra, Edsger W. The structure of the 'THE'-multiprogramming system (EWD-196) (PDF). E.W. Dijkstra Archive. Center for American History, University of Texas at Austin. (transcription) (Jun 14, 1965) + + +有了进程以后,为什么还会出现线程呢?考虑如下情况,对于很多应用(以单一进程的形式运行)而言, +逻辑上存在多个可并行执行的任务,如果其中一个任务被阻塞,将会引起不依赖该任务的其他任务也被阻塞。 +举个具体的例子,我们平常用编辑器来编辑文本内容的时候,都会有一个定时自动保存的功能, +这个功能的作用是在系统或应用本身出现故障的情况前,已有的文档内容会被提前保存。 +假设编辑器自动保存时由于磁盘性能导致写入较慢,导致整个进程被操作系统挂起,这就会影响到用户编辑文档的人机交互体验: +即软件的及时响应能力不足,用户只有等到磁盘写入完成后,操作系统重新调度该进程运行后,用户才可编辑。 +如果我们把一个进程内的多个可并行执行任务通过一种更细粒度的方式让操作系统进行调度, +那么就可以通过处理器时间片切换实现这种细粒度的并发执行。这种细粒度的调度对象就是线程。 + + +.. _term-thread-define: + +线程定义 +~~~~~~~~~~~~~~~~~~~~ + +简单地说,线程是进程的组成部分,进程可包含1 -- n个线程,属于同一个进程的线程共享进程的资源, +比如地址空间、打开的文件等。基本的线程由线程ID、执行状态、当前指令指针 (PC)、寄存器集合和栈组成。 +线程是可以被操作系统或用户态调度器独立调度(Scheduling)和分派(Dispatch)的基本单位。 + +在本章之前,进程是程序的基本执行实体,是程序关于某数据集合上的一次运行活动,是系统进行资源(处理器、 +地址空间和文件等)分配和调度的基本单位。在有了线程后,对进程的定义也要调整了,进程是线程的资源容器, +线程成为了程序的基本执行实体。 + + +同步互斥 +~~~~~~~~~~~~~~~~~~~~~~ + +在上面提到了同步互斥和数据一致性,它们的含义是什么呢?当多个线程共享同一进程的地址空间时, +每个线程都可以访问属于这个进程的数据(全局变量)。如果每个线程使用到的变量都是其他线程不会读取或者修改的话, +那么就不存在一致性问题。如果变量是只读的,多个线程读取该变量也不会有一致性问题。但是,当一个线程修改变量时, +其他线程在读取这个变量时,可能会看到一个不一致的值,这就是数据不一致性的问题。 + +.. note:: + + **并发相关术语** + + - 共享资源(shared resource):不同的线程/进程都能访问的变量或数据结构。 + - 临界区(critical section):访问共享资源的一段代码。 + - 竞态条件(race condition):多个线程/进程都进入临界区时,都试图更新共享的数据结构,导致产生了不期望的结果。 + - 不确定性(indeterminate): 多个线程/进程在执行过程中出现了竞态条件,导致执行结果取决于哪些线程在何时运行, + 即执行结果不确定,而开发者期望得到的是确定的结果。 + - 互斥(mutual exclusion):一种操作原语,能保证只有一个线程进入临界区,从而避免出现竞态,并产生确定的执行结果。 + - 原子性(atomic):一系列操作要么全部完成,要么一个都没执行,不会看到中间状态。在数据库领域, + 具有原子性的一系列操作称为事务(transaction)。 + - 同步(synchronization):多个并发执行的进程/线程在一些关键点上需要互相等待,这种相互制约的等待称为进程/线程同步。 + - 死锁(dead lock):一个线程/进程集合里面的每个线程/进程都在等待只能由这个集合中的其他一个线程/进程 + (包括他自身)才能引发的事件,这种情况就是死锁。 + - 饥饿(hungry):指一个可运行的线程/进程尽管能继续执行,但由于操作系统的调度而被无限期地忽视,导致不能执行的情况。 + +在后续的章节中,会大量使用上述术语,如果现在还不够理解,没关系,随着后续的一步一步的分析和实验, +相信大家能够掌握上述术语的实际含义。 + + + +实践体验 +----------------------------------------- + +.. note:: + + 基于github classroom的开发方式 + + 基于github classroom,可方便建立开发用的git repository,并可基于github的 codespace (在线版ubuntu +vscode)在线开发使用。整个开发环境仅仅需要一个网络浏览器。 + + 1. 在网络浏览器中用自己的 github id 登录 github.com + 2. 接收 `第五个实验(os8)的github classroom在线邀请 `_ ,根据提示一路选择OK即可。 + 3. 完成第二步后,你的第五个实验的 github repository 会被自动建立好,点击此github repository的链接,就可看到你要完成的第一个实验了。 + 4. 在你的第五个实验的网页的中上部可以看到一个醒目的 `code` 绿色按钮,点击后,可以进一步看到 `codespace` 标签和醒目的 `create codesapce on main` 绿色按钮。请点击这个绿色按钮,就可以进入到在线的ubuntu +vscode环境中 + 5. 再按照下面的环境安装提示在vscode的 `console` 中安装配置开发环境:rustc,qemu等工具。 + 6. 在vscode的 `console` 中执行 `make setupclassroom_test8` (该命令仅执行一次)配置githubclassroom 自动评分功能。 + 7. 然后就可以基于在线vscode进行开发、运行、提交等完整的实验过程了。 + + 上述的3,4,5步不是必须的,你也可以线下本地开发。 + + +获取本章代码: + +.. code-block:: console + + $ git clone https://github.com/LearningOS/rust-based-os-comp2022.git + $ cd rust-based-os-comp2022/ + $ make setupclassroom_test8 //注意:这一步很重要,是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次,以后一般就不用执行了,除非 .github/workflows/classroom.yml发生了变化。 + + +在 qemu 模拟器上运行本章代码 `lab5(os8)参考框架: `_ : + +.. code-block:: console + + $ cd os8-ref + $ make run + +内核初始化完成之后就会进入 shell 程序,我们可以体会一下线程的创建和执行过程。在这里我们运行一下本章的测例 ``ch8b_threads`` : + +.. code-block:: + + >> ch8b_threads + aaa....bbb...ccc... + thread#1 exited with code 1 + thread#2 exited with code 2 + thread#3 exited with code 3 + main thread exited. + Shell: Process 2 exited with code 0 + >> + +它会有4个线程在执行,等前3个线程执行完毕后,主线程退出,导致整个进程退出。 + +此外,在本章的操作系统支持通过互斥来执行“哲学家就餐问题”这个应用程序: + +.. code-block:: + + >> ch8b_phil_din_mutex + Here comes 5 philosophers! + time cost = 720 + '-' -> THINKING; 'x' -> EATING; ' ' -> WAITING + #0: ------- xxxxxxxx---------- xxxx----- xxxxxx--xxx + #1: ---xxxxxx-- xxxxxxx---------- x---xxxxxx + #2: ----- xx---------xx----xxxxxx------------ xxxx + #3: -----xxxxxxxxxx------xxxxx-------- xxxxxx-- xxxxxxxxx + #4: ------ x------ xxxxxx-- xxxxx------ xx + #0: ------- xxxxxxxx---------- xxxx----- xxxxxx--xxx + Shell: Process 2 exited with code 0 + >> + +我们可以看到5个代表“哲学家”的线程通过操作系统的 **信号量** 互斥机制在进行 “THINKING”、“EATING”、“WAITING” 的日常生活。 +没有哲学家由于拿不到筷子而饥饿,也没有两个哲学家同时拿到一个筷子。 + +.. note:: + + **哲学家就餐问题** + + 计算机科学家 Dijkstra 提出并解决的哲学家就餐问题是经典的进程同步互斥问题。哲学家就餐问题描述如下: + + 有5个哲学家共用一张圆桌,分别坐在周围的5张椅子上,在圆桌上有5个碗和5只筷子,他们的生活方式是交替地进行思考和进餐。 + 平时,每个哲学家进行思考,饥饿时便试图拿起其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐完毕,放下筷子继续思考。 + + +本章的 `lab5(os8)参考框架: `_ 代码树 +----------------------------------------- + +.. code-block:: + :linenos: + + . + ├── bootloader + │ └── rustsbi-qemu.bin + ├── Dockerfile + ├── easy-fs + │ ├── Cargo.lock + │ ├── Cargo.toml + │ └── src + │ ├── bitmap.rs + │ ├── block_cache.rs + │ ├── block_dev.rs + │ ├── efs.rs + │ ├── layout.rs + │ ├── lib.rs + │ └── vfs.rs + ├── easy-fs-fuse + │ ├── Cargo.lock + │ ├── Cargo.toml + │ └── src + │ └── main.rs + ├── LICENSE + ├── Makefile + ├── os + │ ├── build.rs + │ ├── Cargo.lock + │ ├── Cargo.toml + │ ├── Makefile + │ └── src + │ ├── config.rs (修改:扩大了内核堆空间) + │ ├── console.rs + │ ├── drivers + │ │ ├── block + │ │ │ ├── mod.rs + │ │ │ └── virtio_blk.rs + │ │ └── mod.rs + │ ├── entry.asm + │ ├── fs + │ │ ├── inode.rs + │ │ ├── mod.rs + │ │ ├── pipe.rs + │ │ └── stdio.rs + │ ├── lang_items.rs + │ ├── linker.ld + │ ├── logging.rs + │ ├── main.rs + │ ├── mm + │ │ ├── address.rs + │ │ ├── frame_allocator.rs + │ │ ├── heap_allocator.rs + │ │ ├── memory_set.rs (修改:去除了构建进程地址空间时分配用户栈和映射陷入上下文的逻辑) + │ │ ├── mod.rs + │ │ └── page_table.rs + │ ├── sbi.rs + │ ├── sync (新增:互斥锁、信号量和条件变量三种同步互斥机制的实现) + │ │ ├── condvar.rs + │ │ ├── mod.rs + │ │ ├── mutex.rs + │ │ ├── semaphore.rs + │ │ └── up.rs + │ ├── syscall + │ │ ├── fs.rs (修改:将原先对 task 的调用改为对 process 的调用) + │ │ ├── mod.rs + │ │ ├── process.rs (修改:将原先对 task 的调用改为对 process 的调用) + │ │ ├── sync.rs (新增:三种同步互斥机制相关的系统调用,以及基于定时器条件变量的 sleep 调用) + │ │ └── thread.rs (新增:线程相关系统调用) + │ ├── task + │ │ ├── context.rs (修改:将任务上下文的成员变量改为 pub 类型) + │ │ ├── id.rs (新增:由 pid.rs 修改而来,提供 pid/tid 、 kstack/ustack 的分配和回收机制) + │ │ ├── kthread.rs (新增:完全在内核态运行的线程,仅供参考,在实验中未使用) + │ │ ├── manager.rs + │ │ ├── mod.rs (修改:增加阻塞线程的功能,将 exit 扩展到多线程,并在主线程退出时一并退出进程) + │ │ ├── processor.rs (修改:增加获取当前线程的中断上下文虚拟地址及获取当前进程的功能) + │ │ ├── process.rs (新增:将原先 Task 中的地址空间、文件等机制拆分为进程) + │ │ ├── stackless_coroutine.rs (新增:完全在内核态运行的无栈协程,仅供参考,在实验中未使用) + │ │ ├── switch.rs + │ │ ├── switch.S + │ │ └── task.rs (修改:将进程相关的功能移至 process.rs 中) + │ ├── timer.rs (修改:增加定时器条件变量的实现) + │ └── trap + │ ├── context.rs + │ ├── mod.rs (修改:使用线程对应的中断上下文地址而非固定的 TRAP_CONTEXT) + │ └── trap.S + ├── README.md + └── rust-toolchain diff --git a/_sources/chapter8/1thread-kernel.rst.txt b/_sources/chapter8/1thread-kernel.rst.txt new file mode 100644 index 0000000..1e5c130 --- /dev/null +++ b/_sources/chapter8/1thread-kernel.rst.txt @@ -0,0 +1,485 @@ +内核态的线程管理 +========================================= + +线程概念 +--------------------------------------------- + +这里会结合与进程的比较来说明线程的概念。到本章之前,我们看到了进程这一抽象,操作系统让进程拥有相互隔离的虚拟的地址空间, +让进程感到在独占一个虚拟的处理器。其实这只是操作系统通过时分复用和空分复用技术来让每个进程复用有限的物理内存和物理CPU。 +而线程是在进程内中的一个新的抽象。在没有线程之前,一个进程在一个时刻只有一个执行点(即程序计数器 (PC) +寄存器保存的要执行指令的指针)。但线程的引入把进程内的这个单一执行点给扩展为多个执行点,即在进程中存在多个线程, +每个线程都有一个执行点。而且这些线程共享进程的地址空间,所以可以不必采用相对比较复杂的 IPC 机制(一般需要内核的介入), +而可以很方便地直接访问进程内的数据。 + +在线程的具体运行过程中,需要有程序计数器寄存器来记录当前的执行位置,需要有一组通用寄存器记录当前的指令的操作数据, +需要有一个栈来保存线程执行过程的函数调用栈和局部变量等,这就形成了线程上下文的主体部分。 +这样如果两个线程运行在一个处理器上,就需要采用类似两个进程运行在一个处理器上的调度/切换管理机制, +即需要在一定时刻进行线程切换,并进行线程上下文的保存与恢复。这样在一个进程中的多线程可以独立运行, +取代了进程,成为操作系统调度的基本单位。 + +由于把进程的结构进行了细化,通过线程来表示对处理器的虚拟化,使得进程成为了管理线程的容器。 +在进程中的线程没有父子关系,大家都是兄弟,但还是有个老大。这个代表老大的线程其实就是创建进程(比如通过 +``fork`` 系统调用创建进程)时,建立的第一个线程,它的线程标识符(TID)为 ``0`` 。 + + +线程模型与重要系统调用 +---------------------------------------------- + +目前,我们只介绍本章实现的内核中采用的一种非常简单的线程模型。这个线程模型有三个运行状态: +就绪态、运行态和等待态;共享所属进程的地址空间和其他共享资源(如文件等);可被操作系统调度来分时占用CPU执行; +可以动态创建和退出;可通过系统调用获得操作系统的服务。我们实现的线程模型建立在进程的地址空间抽象之上: +每个线程都共享进程的代码段和和可共享的地址空间(如全局数据段、堆等),但有自己的独占的栈。 +线程模型需要操作系统支持一些重要的系统调用:创建线程、等待子线程结束等,来支持灵活的多线程应用。 +接下来会介绍这些系统调用的基本功能和设计思路。 + + +线程创建系统调用 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +在一个进程的运行过程中,进程可以创建多个属于这个进程的线程,每个线程有自己的线程标识符(TID,Thread Identifier)。 +系统调用 ``thread_create`` 的原型如下: + +.. code-block:: rust + :linenos: + + /// 功能:当前进程创建一个新的线程 + /// 参数:entry 表示线程的入口函数地址 + /// 参数:arg:表示线程的一个参数 + pub fn sys_thread_create(entry: usize, arg: usize) -> isize + +当进程调用 ``thread_create`` 系统调用后,内核会在这个进程内部创建一个新的线程,这个线程能够访问到进程所拥有的代码段, +堆和其他数据段。但内核会给这个新线程分配一个它专有的用户态栈,这样每个线程才能相对独立地被调度和执行。 +另外,由于用户态进程与内核之间有各自独立的页表,所以二者需要有一个跳板页 ``TRAMPOLINE`` +来处理用户态切换到内核态的地址空间平滑转换的事务。所以当出现线程后,在进程中的每个线程也需要有一个独立的跳板页 +``TRAMPOLINE`` 来完成同样的事务。 + +相比于创建进程的 ``fork`` 系统调用,创建线程不需要要建立新的地址空间,这是二者之间最大的不同。 +另外属于同一进程中的线程之间没有父子关系,这一点也与进程不一样。 + +等待子线程系统调用 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +当一个线程执行完代表它的功能后,会通过 ``exit`` 系统调用退出。内核在收到线程发出的 ``exit`` 系统调用后, +会回收线程占用的部分资源,即用户态用到的资源,比如用户态的栈,用于系统调用和异常处理的跳板页等。 +而该线程的内核态用到的资源,比如内核栈等,需要通过进程/主线程调用 ``waittid`` 来回收了, +这样整个线程才能被彻底销毁。系统调用 ``waittid`` 的原型如下: + +.. code-block:: rust + :linenos: + + /// 参数:tid表示线程id + /// 返回值:如果线程不存在,返回-1;如果线程还没退出,返回-2;其他情况下,返回结束线程的退出码 + pub fn sys_waittid(tid: usize) -> i32 + + +一般情况下进程/主线程要负责通过 ``waittid`` 来等待它创建出来的线程(不是主线程)结束并回收它们在内核中的资源 +(如线程的内核栈、线程控制块等)。如果进程/主线程先调用了 ``exit`` 系统调用来退出,那么整个进程 +(包括所属的所有线程)都会退出,而对应父进程会通过 ``waitpid`` 回收子进程剩余还没被回收的资源。 + + +进程相关的系统调用 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +在引入了线程机制后,进程相关的重要系统调用: ``fork`` 、 ``exec`` 、 ``waitpid`` 虽然在接口上没有变化, +但在它要完成的功能上需要有一定的扩展。首先,需要注意到把以前进程中与处理器执行相关的部分拆分到线程中。这样,在通过 +``fork`` 创建进程其实也意味着要单独建立一个主线程来使用处理器,并为以后创建新的线程建立相应的线程控制块向量。 +相对而言, ``exec`` 和 ``waitpid`` 这两个系统调用要做的改动比较小,还是按照与之前进程的处理方式来进行。总体上看, +进程相关的这三个系统调用还是保持了已有的进程操作的语义,并没有由于引入了线程,而带来大的变化。 + + +应用程序示例 +---------------------------------------------- + +我们刚刚介绍了 thread_create/waittid 两个重要系统调用,我们可以借助它们和之前实现的系统调用, +开发出功能更为灵活的应用程序。下面我们通过描述一个多线程应用程序 ``threads`` 的开发过程来展示这些系统调用的使用方法。 + + +系统调用封装 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +同学可以在 user/src/syscall.rs 中看到以 sys_* 开头的系统调用的函数原型,它们后续还会在 user/src/lib.rs 中被封装成方便应用程序使用的形式。如 ``sys_thread_create`` 被封装成 ``thread_create`` ,而 ``sys_waittid`` 被封装成 ``waittid`` : + +.. code-block:: rust + :linenos: + + pub fn thread_create(entry: usize, arg: usize) -> isize { sys_thread_create(entry, arg) } + + pub fn waittid(tid: usize) -> isize { + loop { + match sys_waittid(tid) { + -2 => { yield_(); } + exit_code => return exit_code, + } + } + } + +waittid 等待一个线程标识符的值为tid 的线程结束。在具体实现方面,我们看到当 sys_waittid 返回值为 -2 ,即要等待的线程存在但它却尚未退出的时候,主线程调用 yield_ 主动交出 CPU 使用权,待下次 CPU 使用权被内核交还给它的时候再次调用 sys_waittid 查看要等待的线程是否退出。这样做是为了减小 CPU 资源的浪费。这种方法是为了尽可能简化内核的实现。 + + +多线程应用程序 -- threads +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +多线程应用程序 -- threads 开始执行后,先调用 ``thread_create`` 创建了三个线程,加上进程自带的主线程,其实一共有四个线程。每个线程在打印了1000个字符后,会执行 ``exit`` 退出。进程通过 ``waittid`` 等待这三个线程结束后,最终结束进程的执行。下面是多线程应用程序 -- threads 的源代码: + +.. code-block:: rust + :linenos: + + //usr/src/bin/ch8b_threads.rs + + #![no_std] + #![no_main] + + #[macro_use] + extern crate user_lib; + extern crate alloc; + + use user_lib::{thread_create, waittid, exit}; + use alloc::vec::Vec; + + pub fn thread_a() -> ! { + for _ in 0..1000 { print!("a"); } + exit(1) + } + + pub fn thread_b() -> ! { + for _ in 0..1000 { print!("b"); } + exit(2) + } + + pub fn thread_c() -> ! { + for _ in 0..1000 { print!("c"); } + exit(3) + } + + #[no_mangle] + pub fn main() -> i32 { + let mut v = Vec::new(); + v.push(thread_create(thread_a as usize, 0)); + v.push(thread_create(thread_b as usize, 0)); + v.push(thread_create(thread_c as usize, 0)); + for tid in v.iter() { + let exit_code = waittid(*tid as usize); + println!("thread#{} exited with code {}", tid, exit_code); + } + println!("main thread exited."); + 0 + } + +线程管理的核心数据结构 +----------------------------------------------- + +为了在现有进程管理的基础上实现线程管理,我们需要改进一些数据结构包含的内容及接口。 +基本思路就是把进程中与处理器相关的部分分拆出来,形成线程相关的部分。 + +本节将按照如下顺序来进行介绍: + +- 任务控制块 TaskControlBlock :表示线程的核心数据结构。 +- 任务管理器 TaskManager :管理线程集合的核心数据结构。 +- 处理器管理结构 Processor :用于线程调度,维护线程的处理器状态。 + +线程控制块 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +在内核中,每个线程的执行状态和线程上下文等均保存在一个被称为线程控制块 (TCB, Task Control Block) +的结构中,它是内核对线程进行管理的核心数据结构。在内核看来,它就等价于一个线程。 + +.. code-block:: rust + :linenos: + + pub struct TaskControlBlock { + // immutable + pub process: Weak, + pub kernel_stack: KernelStack, + // mutable + inner: UPSafeCell, + } + + pub struct TaskControlBlockInner { + pub trap_cx_ppn: PhysPageNum, + pub task_cx: TaskContext, + pub task_status: TaskStatus, + pub exit_code: Option, + pub res: Option, + } + +线程控制块就是任务控制块(TaskControlBlock),主要包括在线程初始化之后就不再变化的元数据: +线程所属的进程和线程的内核栈,以及在运行过程中可能发生变化的元数据: UPSafeCell 。 +大部分的细节放在 ``TaskControlBlockInner`` 中: + +之前进程中的定义不存在的: + +- ``res: Option`` 指出了用户态的线程代码执行需要的信息,这些在线程初始化之后就不再变化: + +.. code-block:: rust + :linenos: + + pub struct TaskUserRes { + pub tid: usize, + pub ustack_base: usize, + pub process: Weak, + } + +- tid:线程标识符 +- ustack_base:线程的栈顶地址 +- process:线程所属的进程 + +与之前进程中的定义相同/类似的部分: + +- ``trap_cx_ppn`` 指出了应用地址空间中线程的 Trap 上下文被放在的物理页帧的物理页号。 +- ``task_cx`` 保存暂停线程的线程上下文,用于线程切换。 +- ``task_status`` 维护当前线程的执行状态。 +- ``exit_code`` 线程退出码。 + + +包含线程的进程控制块 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +把线程相关数据单独组织成数据结构后,进程的结构也需要进行一定的调整: + +.. code-block:: rust + :linenos: + + pub struct ProcessControlBlock { + // immutable + pub pid: PidHandle, + // mutable + inner: UPSafeCell, + } + + pub struct ProcessControlBlockInner { + ... + pub tasks: Vec>>, + pub task_res_allocator: RecycleAllocator, + } + +从中可以看出,进程把与处理器执行相关的部分都移到了 ``TaskControlBlock`` 中,并组织为一个线程控制块向量中, +这就自然对应到多个线程的管理上了。而 ``RecycleAllocator`` 是对之前的 ``PidAllocator`` 的一个升级版, +即一个相对通用的资源分配器,可用于分配进程标识符(PID)和线程的内核栈(KernelStack)。 + +.. chyyuu 加一个PidAllocator的链接??? + +线程与处理器管理结构 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +线程管理的结构是线程管理器,即任务管理器,位于 ``os/src/task/manager.rs`` 中, +其数据结构和方法与之前章节中进程的任务管理器完全一样,仅负责管理所有线程。而处理器管理结构 ``Processor`` +负责维护 CPU 状态、调度和特权级切换等事务。其数据结构与之前章节中进程的处理器管理结构完全一样。 +但在相关方法上面,由于多个线程有各自的用户栈和跳板页,所以有些不同,下面会进一步分析。 + +.. chyyuu 加一个taskmanager,processor的链接??? + +线程管理机制的设计与实现 +----------------------------------------------- + +在上述线程模型和内核数据结构的基础上,我们还需完成线程管理的基本实现,从而构造出一个完整的“达科塔盗龙”操作系统。 +本节将分析如何实现线程管理: + +- 线程创建、线程退出与等待线程结束 +- 线程执行中的特权级切换 +.. - 进程管理中与线程相关的处理 + + +线程创建、线程退出与等待线程结束 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +线程创建 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +当一个进程执行中发出了创建线程的系统调用 ``sys_thread_create`` 后,操作系统就需要在当前进程的基础上创建一个线程了, +这里重点是需要了解创建线程控制块,在线程控制块中初始化各个成员变量,建立好进程和线程的关系等。 +只有建立好这些成员变量,才能给线程建立一个灵活方便的执行环境。这里列出支持线程正确运行所需的重要的执行环境要素: + +- 线程的用户态栈:确保在用户态的线程能正常执行函数调用; +- 线程的内核态栈:确保线程陷入内核后能正常执行函数调用; +- 线程的跳板页:确保线程能正确的进行用户态<-->内核态切换; +- 线程上下文:即线程用到的寄存器信息,用于线程切换。 + +线程创建的具体实现如下: + +.. code-block:: rust + :linenos: + + // os/src/syscall/thread.rs + + pub fn sys_thread_create(entry: usize, arg: usize) -> isize { + let task = current_task().unwrap(); + let process = task.process.upgrade().unwrap(); + // create a new thread + let new_task = Arc::new(TaskControlBlock::new( + Arc::clone(&process), + task.inner_exclusive_access().res.as_ref().unwrap().ustack_base, + true, + )); + // add new task to scheduler + add_task(Arc::clone(&new_task)); + let new_task_inner = new_task.inner_exclusive_access(); + let new_task_res = new_task_inner.res.as_ref().unwrap(); + let new_task_tid = new_task_res.tid; + let mut process_inner = process.inner_exclusive_access(); + // add new thread to current process + let tasks = &mut process_inner.tasks; + while tasks.len() < new_task_tid + 1 { + tasks.push(None); + } + tasks[new_task_tid] = Some(Arc::clone(&new_task)); + let new_task_trap_cx = new_task_inner.get_trap_cx(); + *new_task_trap_cx = TrapContext::app_init_context( + entry, + new_task_res.ustack_top(), + kernel_token(), + new_task.kernel_stack.get_top(), + trap_handler as usize, + ); + (*new_task_trap_cx).x[10] = arg; + new_task_tid as isize + } + +上述代码主要完成了如下事务: + +- 第4-5行,找到当前正在执行的线程 ``task`` 和此线程所属的进程 ``process`` 。 +- 第7-11行,调用 ``TaskControlBlock::new`` 方法,创建一个新的线程 ``new_task`` ,在创建过程中,建立与进程 + ``process`` 的所属关系,分配了线程用户态栈、内核态栈、用于异常/中断的跳板页。 +- 第13行,把线程挂到调度队列中。 +- 第19-22行,把线程接入到所需进程的线程列表 ``tasks`` 中。 +- 第25~32行,初始化位于该线程在用户态地址空间中的 Trap 上下文:设置线程的函数入口点和用户栈, + 使得第一次进入用户态时能从线程起始位置开始正确执行;设置好内核栈和陷入函数指针 ``trap_handler`` , + 保证在 Trap 的时候用户态的线程能正确进入内核态。 + +线程退出 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +当一个非主线程的其他线程发出 ``sys_exit`` 系统调用时,内核会调用 ``exit_current_and_run_next`` +函数退出当前线程并切换到下一个线程,但不会导致其所属进程的退出。当 **主线程** 即进程发出这个系统调用, +内核会回收整个进程(这包括了其管理的所有线程)资源,并退出。具体实现如下: + +.. code-block:: rust + :linenos: + + // 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/task/mod.rs + + pub fn exit_current_and_run_next(exit_code: i32) { + let task = take_current_task().unwrap(); + let mut task_inner = task.inner_exclusive_access(); + let process = task.process.upgrade().unwrap(); + let tid = task_inner.res.as_ref().unwrap().tid; + // record exit code + task_inner.exit_code = Some(exit_code); + task_inner.res = None; + // here we do not remove the thread since we are still using the kstack + // it will be deallocated when sys_waittid is called + drop(task_inner); + drop(task); + // however, if this is the main thread of current process + // the process should terminate at once + if tid == 0 { + let mut process_inner = process.inner_exclusive_access(); + // mark this process as a zombie process + process_inner.is_zombie = true; + // record exit code of main process + process_inner.exit_code = exit_code; + { + // move all child processes under init process + let mut initproc_inner = INITPROC.inner_exclusive_access(); + for child in process_inner.children.iter() { + child.inner_exclusive_access().parent = Some(Arc::downgrade(&INITPROC)); + initproc_inner.children.push(child.clone()); + } + } + let mut recycle_res = Vec::::new(); + // deallocate user res (including tid/trap_cx/ustack) of all threads + // it has to be done before we dealloc the whole memory_set + // otherwise they will be deallocated twice + for task in process_inner.tasks.iter().filter(|t| t.is_some()) { + let task = task.as_ref().unwrap(); + let mut task_inner = task.inner_exclusive_access(); + if let Some(res) = task_inner.res.take() { + recycle_res.push(res); + } + } + drop(process_inner); + recycle_res.clear(); + let mut process_inner = process.inner_exclusive_access(); + process_inner.children.clear(); + // deallocate other data in user space i.e. program code/data section + process_inner.memory_set.recycle_data_pages(); + } + drop(process); + // we do not have to save task context + let mut _unused = TaskContext::zero_init(); + schedule(&mut _unused as *mut _); + } + +上述代码主要完成了如下事务: + +- 第11-21行,回收线程的各种资源。 +- 第24-56行,如果是主线程发出的退去请求,则回收整个进程的部分资源,并退出进程。第 33~37 + 行所做的事情是将当前进程的所有子进程挂在初始进程 INITPROC 下面,其做法是遍历每个子进程, + 修改其父进程为初始进程,并加入初始进程的孩子向量中。第 49 行将当前进程的孩子向量清空。 +- 第58-59行,进行线程调度切换。 + +上述实现中很大一部分与第五章讲解的 进程的退出 的功能实现大致相同。 + +.. chyyuu 加上链接??? + +等待线程结束 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +主线程通过系统调用 ``sys_waittid`` 来等待其他线程的结束。具体实现如下: + +.. code-block:: rust + :linenos: + + // os/src/syscall/ch8b_thread.rs + + pub fn sys_waittid(tid: usize) -> i32 { + let task = current_task().unwrap(); + let process = task.process.upgrade().unwrap(); + let task_inner = task.inner_exclusive_access(); + let mut process_inner = process.inner_exclusive_access(); + // a thread cannot wait for itself + if task_inner.res.as_ref().unwrap().tid == tid { + return -1; + } + let mut exit_code: Option = None; + let waited_task = process_inner.tasks[tid].as_ref(); + if let Some(waited_task) = waited_task { + if let Some(waited_exit_code) = waited_task.inner_exclusive_access().exit_code { + exit_code = Some(waited_exit_code); + } + } else { + // waited thread does not exist + return -1; + } + if let Some(exit_code) = exit_code { + // dealloc the exited thread + process_inner.tasks[tid] = None; + exit_code + } else { + // waited thread has not exited + -2 + } + } + +上述代码主要完成了如下事务: + +- 第9-10行,如果是线程等自己,返回错误. +- 第12-21行,如果找到 ``tid`` 对应的退出线程,则收集该退出线程的退出码 ``exit_tid`` ,否则返回错误(退出线程不存在)。 +- 第22-29行,如果退出码存在,则清空进程中对应此退出线程的线程控制块(至此,线程所占资源算是全部清空了),否则返回错误(线程还没退出)。 + + +线程执行中的特权级切换和调度切换 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +线程执行中的特权级切换与第三章中 **任务切换的设计与实现** 小节中讲解的过程是一致的。而线程执行中的调度切换过程与第五章的 **进程调度机制** 小节中讲解的过程是一致的。 +这里就不用再赘述一遍了。 + + +.. [#dak] 达科塔盗龙是一种生存于距今6700万-6500万年前白垩纪晚期的兽脚类驰龙科恐龙,它主打的并不是霸王龙的力量路线,而是利用自己修长的后肢来提高敏捷度和奔跑速度。它全身几乎都长满了羽毛,可能会滑翔或者其他接近飞行行为的行动模式。 \ No newline at end of file diff --git a/_sources/chapter8/2lock.rst.txt b/_sources/chapter8/2lock.rst.txt new file mode 100644 index 0000000..f67f0ee --- /dev/null +++ b/_sources/chapter8/2lock.rst.txt @@ -0,0 +1,379 @@ +锁机制 +========================================= + +本节导读 +----------------------------------------- + +.. chyyuu https://en.wikipedia.org/wiki/Lock_(computer_science) + +到目前为止,我们已经实现了进程和线程,也能够理解在一个时间段内,会有多个线程在执行,这就是并发。 +而且,由于线程的引入,多个线程可以共享进程中的全局数据。如果多个线程都想读和更新全局数据, +那么谁先更新取决于操作系统内核的抢占式调度和分派策略。在一般情况下,每个线程都有可能先执行, +且可能由于中断等因素,随时被操作系统打断其执行,而切换到另外一个线程运行, +形成在一段时间内,多个线程交替执行的现象。如果没有一些保障机制(比如互斥、同步等), +那么这些对共享数据进行读写的交替执行的线程,其期望的共享数据的正确结果可能无法达到。 + +所以,我们需要研究一种保障机制 --- 锁 ,确保无论操作系统如何抢占线程,调度和切换线程的执行, +都可以保证对拥有锁的线程,可以独占地对共享数据进行读写,从而能够得到正确的共享数据结果。 +这种机制的能力来自于处理器的指令、操作系统系统调用的基本支持,从而能够保证线程间互斥地读写共享数据。 +下面各个小节将从为什么需要锁、锁的基本思路、锁的不同实现方式等逐步展开讲解。 + +为什么需要锁 +----------------------------------------- + +上一小节已经提到,没有保障机制的多个线程,在对共享数据进行读写的过程中,可能得不到预期的结果。 +我们来看看这个简单的例子: + +.. code-block:: c + :linenos: + :emphasize-lines: 4 + + // 线程的入口函数 + int a=0; + void f() { + a = a + 1; + } + +对于上述函数中的第 4 行代码,一般人理解处理器会一次就执行完这条简单的语句,但实际情况并不是这样。 +我们可以用 GCC 编译出上述函数的汇编码: + +.. code-block:: shell + :linenos: + + $ riscv64-unknown-elf-gcc -o f.s -S f.c + + +可以看到生成的汇编代码如下: + +.. code-block:: asm + :linenos: + :emphasize-lines: 18-23 + + //f.s + .text + .globl a + .section .sbss,"aw",@nobits + .align 2 + .type a, @object + .size a, 4 + a: + .zero 4 + .text + .align 1 + .globl f + .type f, @function + f: + addi sp,sp,-16 + sd s0,8(sp) + addi s0,sp,16 + lui a5,%hi(a) + lw a5,%lo(a)(a5) + addiw a5,a5,1 + sext.w a4,a5 + lui a5,%hi(a) + sw a4,%lo(a)(a5) + nop + ld s0,8(sp) + addi sp,sp,16 + jr ra + + +.. chyyuu 可以给上面的汇编码添加注释??? + +从中可以看出,对于高级语言的一条简单语句(C 代码的第 4 行,对全局变量进行读写),很可能是由多条汇编代码 +(汇编代码的第 18~23 行)组成。如果这个函数是多个线程要执行的函数,那么在上述汇编代码第 +18 行到第 23 行中的各行之间,可能会发生中断,从而导致操作系统执行抢占式的线程调度和切换, +就会得到不一样的结果。由于执行这段汇编代码(第 18~23 行))的多个线程在访问全局变量过程中可能导致竞争状态, +因此我们将此段代码称为临界区(critical section)。临界区是访问共享变量(或共享资源)的代码片段, +不能由多个线程同时执行,即需要保证互斥。 + +下面是有两个线程T0、T1在一个时间段内的一种可能的执行情况: + +===== ===== ======= ======= =========== ========= +时间 T0 T1 OS 共享变量a 寄存器a5 +===== ===== ======= ======= =========== ========= +1 L18 -- -- 0 a的高位地址 +2 -- -- 切换 0 0 +3 -- L18 -- 0 a的高位地址 +4 L20 -- -- 0 1 +5 -- -- 切换 0 a的高位地址 +6 -- L20 -- 0 1 +7 -- -- 切换 0 1 +8 L23 -- -- 1 1 +9 -- -- 切换 1 1 +10 -- L23 -- 1 1 +===== ===== ======= ======= =========== ========= + +一般情况下,线程 T0 执行完毕后,再执行线程 T1,那么共享全局变量 ``a`` 的值为 2 。但在上面的执行过程中, +可以看到在线程执行指令的过程中会发生线程切换,这样在时刻 10 的时候,共享全局变量 ``a`` 的值为 1 , +这不是我们预期的结果。出现这种情况的原因是两个线程在操作系统的调度下(在哪个时刻调度具有不确定性), +交错执行 ``a = a + 1`` 的不同汇编指令序列,导致虽然增加全局变量 ``a`` 的代码被执行了两次, +但结果还是只增加了 1 。这种多线程的最终执行结果不确定(indeterminate),取决于由于调度导致的、 +不确定指令执行序列的情况就是竞态条件(race condition)。 + +如果每个线程在执行 ``a = a + 1`` 这个 C 语句所对应多条汇编语句过程中,不会被操作系统切换, +那么就不会出现多个线程交叉读写全局变量的情况,也就不会出现结果不确定的问题了。 + +所以,访问(特指写操作)共享变量代码片段,不能由多个线程同时执行(即并行)或者在一个时间段内都去执行 +(即并发)。要做到这一点,需要互斥机制的保障。从某种角度上看,这种互斥性也是一种原子性, +即线程在临界区的执行过程中,不会出现只执行了一部分,就被打断并切换到其他线程执行的情况。即, +要么线程执行的这一系列操作/指令都完成,要么这一系列操作/指令都不做,不会出现指令序列执行中被打断的情况。 + + +锁的基本思路 +----------------------------------------- + +要保证多线程并发执行中的临界区的代码具有互斥性或原子性,我们可以建立一种锁, +只有拿到锁的线程才能在临界区中执行。这里的锁与现实生活中的锁的含义很类似。比如,我们可以写出如下的伪代码: + +.. code-block:: Rust + :linenos: + + lock(mutex); // 尝试取锁 + a = a + 1; // 临界区,访问临界资源 a + unlock(mutex); // 是否锁 + ... // 剩余区 + +对于一个应用程序而言,它的执行是受到其执行环境的管理和限制的,而执行环境的主要组成就是用户态的系统库、 +操作系统和更底层的处理器,这说明我们需要有硬件和操作系统来对互斥进行支持。一个自然的想法是,这个 +``lock/unlock`` 互斥操作就是CPU提供的机器指令,那上面这一段程序就很容易在计算机上执行了。 +但需要注意,这里互斥的对象是线程的临界区代码,而临界区代码可以访问各种共享变量(简称临界资源)。 +只靠两条机器指令,难以识别各种共享变量,不太可能约束可能在临界区的各种指令执行共享变量操作的互斥性。 +所以,我们还是需要有一些相对更灵活和复杂一点的方法,能够设置一种所有线程能看到的标记, +在一个能进入临界区的线程设置好这个标记后,其他线程都不能再进入临界区了。总体上看, +对临界区的访问过程分为四个部分: + +1. 尝试取锁: 查看锁是否可用,即临界区是否可访问(看占用临界区标志是否被设置),如果可以访问, + 则设置占用临界区标志(锁不可用)并转到步骤 2 ,否则线程忙等或被阻塞; +2. 临界区: 访问临界资源的系列操作 +3. 释放锁: 清除占用临界区标志(锁可用),如果有线程被阻塞,会唤醒阻塞线程; +4. 剩余区: 与临界区不相关部分的代码 + +根据上面的步骤,可以看到锁机制有两种:让线程忙等的忙等锁(spin lock),以及让线程阻塞的睡眠锁 +(sleep lock)。锁的实现大体上基于三类机制:用户态软件、机器指令硬件、内核态操作系统。 +下面我们介绍来 rCore 中基于内核态操作系统级方法实现的支持互斥的锁。 + +我们还需要知道如何评价各种锁实现的效果。一般我们需要关注锁的三种属性: + +1. 互斥性(mutual exclusion),即锁是否能够有效阻止多个线程进入临界区,这是最基本的属性。 +2. 公平性(fairness),当锁可用时,每个竞争线程是否有公平的机会抢到锁。 +3. 性能(performance),即使用锁的时间开销。 + + +内核态操作系统级方法实现锁 --- mutex 系统调用 +----------------------------------------- + + +使用 mutex 系统调用 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +如何能够实现轻量的可睡眠锁?一个自然的想法就是,让等待锁的线程睡眠,让释放锁的线程显式地唤醒等待锁的线程。 +如果有多个等待锁的线程,可以全部释放,让大家再次竞争锁;也可以只释放最早等待的那个线程。 +这就需要更多的操作系统支持,特别是需要一个等待队列来保存等待锁的线程。 + +我们先看看多线程应用程序如何使用mutex系统调用的: + + +.. code-block:: Rust + :linenos: + :emphasize-lines: 8,13,21,32,35,38 + + // user/src/bin/race_adder_mutex_blocking.rs + + static mut A: usize = 0; + ... + unsafe fn f() -> ! { + let mut t = 2usize; + for _ in 0..PER_THREAD { + mutex_lock(0); + let a = &mut A as *mut usize; + let cur = a.read_volatile(); + for _ in 0..500 { t = t * t % 10007; } + a.write_volatile(cur + 1); + mutex_unlock(0); + } + exit(t as i32) + } + + #[no_mangle] + pub fn main() -> i32 { + let start = get_time(); + assert_eq!(mutex_blocking_create(), 0); + let mut v = Vec::new(); + for _ in 0..THREAD_COUNT { + v.push(thread_create(f as usize, 0) as usize); + } + ... + } + + // usr/src/syscall.rs + + pub fn sys_mutex_create(blocking: bool) -> isize { + syscall(SYSCALL_MUTEX_CREATE, [blocking as usize, 0, 0]) + } + pub fn sys_mutex_lock(id: usize) -> isize { + syscall(SYSCALL_MUTEX_LOCK, [id, 0, 0]) + } + pub fn sys_mutex_unlock(id: usize) -> isize { + syscall(SYSCALL_MUTEX_UNLOCK, [id, 0, 0]) + } + + +- 第21行,创建了一个ID为 ``0`` 的互斥锁,对应的是第32行 ``SYSCALL_MUTEX_CREATE`` 系统调用; +- 第8行,尝试获取锁(对应的是第35行 ``SYSCALL_MUTEX_LOCK`` 系统调用),如果取得锁, + 将继续向下执行临界区代码;如果没有取得锁,将阻塞; +- 第13行,释放锁(对应的是第38行 ``SYSCALL_MUTEX_UNLOCK`` 系统调用),如果有等待在该锁上的线程, + 则唤醒这些等待线程。 + +mutex 系统调用的实现 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +操作系统如何实现这些系统调用呢?首先考虑一下与此相关的核心数据结构, +然后考虑与数据结构相关的相关函数/方法的实现。 + +在线程的眼里, **互斥** 是一种每个线程能看到的资源,且在一个进程中,可以存在多个不同互斥资源, +所以我们可以把所有的互斥资源放在一起让进程来管理,如下面代码第 9 行所示。这里需要注意的是: +``mutex_list: Vec>>`` 表示的是实现了 ``Mutex`` trait 的一个“互斥资源”的向量。而 +``MutexBlocking`` 是会实现 ``Mutex`` trait 的内核数据结构,它就是我们提到的 **互斥资源** 即 +**互斥锁** 。操作系统需要显式地施加某种控制,来确定当一个线程释放锁时,等待的线程谁将能抢到锁。 +为了做到这一点,操作系统需要有一个等待队列来保存等待锁的线程,如下面代码的第 20 行所示。 + +.. code-block:: Rust + :linenos: + :emphasize-lines: 9,20 + + pub struct ProcessControlBlock { + // immutable + pub pid: PidHandle, + // mutable + inner: UPSafeCell, + } + pub struct ProcessControlBlockInner { + ... + pub mutex_list: Vec>>, + } + pub trait Mutex: Sync + Send { + fn lock(&self); + fn unlock(&self); + } + pub struct MutexBlocking { + inner: UPSafeCell, + } + pub struct MutexBlockingInner { + locked: bool, + wait_queue: VecDeque>, + } + + +这样,在操作系统中,需要设计实现三个核心成员变量。互斥锁的成员变量有两个:表示是否锁上的 ``locked`` +和管理等待线程的等待队列 ``wait_queue``;进程的成员变量:锁向量 ``mutex_list`` 。 + +首先需要创建一个互斥锁,下面是应对 ``SYSCALL_MUTEX_CREATE`` 系统调用的创建互斥锁的函数: + +.. code-block:: Rust + :linenos: + :emphasize-lines: 14,18 + + // os/src/syscall/sync.rs + pub fn sys_mutex_create(blocking: bool) -> isize { + let process = current_process(); + let mut process_inner = process.inner_exclusive_access(); + if let Some(id) = process_inner + .mutex_list + .iter() + .enumerate() + .find(|(_, item)| item.is_none()) + .map(|(id, _)| id) { + process_inner.mutex_list[id] = if !blocking { + Some(Arc::new(MutexSpin::new())) + } else { + Some(Arc::new(MutexBlocking::new())) + }; + id as isize + } else { + process_inner.mutex_list.push(Some(Arc::new(MutexSpin::new()))); + process_inner.mutex_list.len() as isize - 1 + } + } + +- 第 14 行,如果向量中有空的元素,就在这个空元素的位置创建一个可睡眠的互斥锁; +- 第 18 行,如果向量满了,就在向量中添加新的可睡眠的互斥锁; + + +有了互斥锁,接下来就是实现 ``Mutex`` trait的内核函数:对应 ``SYSCALL_MUTEX_LOCK`` 系统调用的 +``sys_mutex_lock`` 。操作系统主要工作是,在锁已被其他线程获取的情况下,把当前线程放到等待队列中, +并调度一个新线程执行。主要代码如下: + +.. code-block:: Rust + :linenos: + :emphasize-lines: 8,16,17,19,21 + + // os/src/syscall/sync.rs + pub fn sys_mutex_lock(mutex_id: usize) -> isize { + let process = current_process(); + let process_inner = process.inner_exclusive_access(); + let mutex = Arc::clone(process_inner.mutex_list[mutex_id].as_ref().unwrap()); + drop(process_inner); + drop(process); + mutex.lock(); + 0 + } + + // os/src/sync/mutex.rs + impl Mutex for MutexBlocking { + fn lock(&self) { + let mut mutex_inner = self.inner.exclusive_access(); + if mutex_inner.locked { + mutex_inner.wait_queue.push_back(current_task().unwrap()); + drop(mutex_inner); + block_current_and_run_next(); + } else { + mutex_inner.locked = true; + } + } + } + +.. chyyuu drop的作用??? + +- 第 8 行,调用 ID 为 ``mutex_id`` 的互斥锁 ``mutex`` 的 ``lock`` 方法,具体工作由该方法来完成。 +- 第 16 行,如果互斥锁 ``mutex`` 已经被其他线程获取了,那么在第 17 行,将把当前线程放入等待队列中; + 在第 19 行,让当前线程处于等待状态,并调度其他线程执行。 +- 第 21 行,如果互斥锁 ``mutex`` 还没被获取,那么当前线程会获取给互斥锁,并返回系统调用。 + + +最后是实现 ``Mutex`` trait 的内核函数:对应 ``SYSCALL_MUTEX_UNLOCK`` 系统调用的 ``sys_mutex_unlock`` 。 +操作系统的主要工作是,如果有等待在这个互斥锁上的线程,需要唤醒最早等待的线程。主要代码如下: + +.. code-block:: Rust + :linenos: + :emphasize-lines: 8,17-18,20 + + // os/src/syscall/sync.rs + pub fn sys_mutex_unlock(mutex_id: usize) -> isize { + let process = current_process(); + let process_inner = process.inner_exclusive_access(); + let mutex = Arc::clone(process_inner.mutex_list[mutex_id].as_ref().unwrap()); + drop(process_inner); + drop(process); + mutex.unlock(); + 0 + } + + // os/src/sync/mutex.rs + impl Mutex for MutexBlocking { + fn unlock(&self) { + let mut mutex_inner = self.inner.exclusive_access(); + assert!(mutex_inner.locked); + if let Some(waking_task) = mutex_inner.wait_queue.pop_front() { + add_task(waking_task); + } else { + mutex_inner.locked = false; + } + } + } + +- 第 8 行,调用 ID 为 ``mutex_id`` 的互斥锁 ``mutex`` 的 ``unlock`` 方法,具体工作由该方法来完成的。 +- 第 17-18 行,如果有等待的线程,唤醒等待最久的那个线程,相当于将锁的所有权移交给该线程。 +- 第 20 行,若没有线程等待,则释放锁。 + + diff --git a/_sources/chapter8/3semaphore.rst.txt b/_sources/chapter8/3semaphore.rst.txt new file mode 100644 index 0000000..3a5f529 --- /dev/null +++ b/_sources/chapter8/3semaphore.rst.txt @@ -0,0 +1,275 @@ +信号量机制 +========================================= + +本节导读 +----------------------------------------- + +.. chyyuu https://en.wikipedia.org/wiki/Semaphore_(programming) + +在上一节中,我们介绍了互斥锁(mutex 或 lock)的起因、使用和实现过程。通过互斥锁, +可以让线程在临界区执行时,独占临界资源。当我们需要更灵活的互斥访问或同步操作方式,如提供了最多只允许 +N 个线程访问临界资源的情况,让某个线程等待另外一个线程执行完毕后再继续执行的同步过程等, +互斥锁这种方式就有点力不从心了。 + +在本节中,将介绍功能更加强大和灵活的同步互斥机制 -- 信号量(Semaphore),它的设计思路、 +使用和在操作系统中的具体实现。可以看到,信号量的实现需要互斥锁和处理器原子指令的支持, +它是一种更高级的同步互斥机制。 + + +信号量的起源和基本思路 +----------------------------------------- + +1963 年前后,当时的数学家(其实是计算机科学家)Edsger Dijkstra 和他的团队在为 Electrologica X8 +计算机开发一个操作系统(称为 THE multiprogramming system,THE 多道程序系统)的过程中,提出了信号量 +(Semphore)是一种变量或抽象数据类型,用于控制多个线程对共同资源的访问。 + +信号量是对互斥锁的一种巧妙的扩展。上一节中的互斥锁的初始值一般设置为 1 的整型变量, +表示临界区还没有被某个线程占用。互斥锁用 0 表示临界区已经被占用了,用 1 表示临界区为空,再通过 +``lock/unlock`` 操作来协调多个线程轮流独占临界区执行。而信号量的初始值可设置为 N 的整数变量, 如果 N +大于 0, 表示最多可以有 N 个线程进入临界区执行,如果 N 小于等于 0 ,表示不能有线程进入临界区了, +必须在后续操作中让信号量的值加 1 ,才能唤醒某个等待的线程。 + +Dijkstra 对信号量设计了两种操作:P(Proberen(荷兰语),尝试)操作和 V(Verhogen(荷兰语),增加)操作。 +P 操作是检查信号量的值是否大于 0,若该值大于 0,则将其值减 1 并继续(表示可以进入临界区了);若该值为 +0,则线程将睡眠。注意,此时 P 操作还未结束。而且由于信号量本身是一种临界资源(可回想一下上一节的锁, +其实也是一种临界资源),所以在 P 操作中,检查/修改信号量值以及可能发生的睡眠这一系列操作, +是一个不可分割的原子操作过程。通过原子操作才能保证,一旦 P 操作开始,则在该操作完成或阻塞睡眠之前, +其他线程均不允许访问该信号量。 + +V 操作会对信号量的值加 1 ,然后检查是否有一个或多个线程在该信号量上睡眠等待。如有, +则选择其中的一个线程唤醒并允许该线程继续完成它的 P 操作;如没有,则直接返回。注意,信号量的值加 1, +并可能唤醒一个线程的一系列操作同样也是不可分割的原子操作过程。不会有某个进程因执行 V 操作而阻塞。 + +如果信号量是一个任意的整数,通常被称为计数信号量(Counting Semaphore),或一般信号量(General +Semaphore);如果信号量只有0或1的取值,则称为二值信号量(Binary Semaphore)。可以看出, +互斥锁是信号量的一种特例 --- 二值信号量,信号量很好地解决了最多允许 N 个线程访问临界资源的情况。 + +信号量的一种实现伪代码如下所示: + +.. code-block:: rust + :linenos: + + fn P(S) { + if S >= 1 + S = S - 1; + else + ; + } + fn V(S) { + if + ; + else + S = S + 1; + } + +在上述实现中,S 的取值范围为大于等于 0 的整数。S 的初值一般设置为一个大于 0 的正整数, +表示可以进入临界区的线程数。当 S 取值为 1,表示是二值信号量,也就是互斥锁了。 +使用信号量实现线程互斥访问临界区的伪代码如下: + +.. code-block:: rust + :linenos: + + let static mut S: semaphore = 1; + + // Thread i + fn foo() { + ... + P(S); + execute Cricital Section; + V(S); + ... + } + +下面是另外一种信号量实现的伪代码: + +.. code-block:: rust + :linenos: + + fn P(S) { + S = S - 1; + if S < 0 then + ; + } + + fn V(S) { + S = S + 1; + if + ; + } + +在这种实现中,S 的初值一般设置为一个大于 0 的正整数,表示可以进入临界区的线程数。但 S +的取值范围可以是小于 0 的整数,表示等待进入临界区的睡眠线程数。 + +信号量的另一种用途是用于实现同步(synchronization)。比如,把信号量的初始值设置为 0 , +当一个线程 A 对此信号量执行一个 P 操作,那么该线程立即会被阻塞睡眠。之后有另外一个线程 B +对此信号量执行一个 V 操作,就会将线程 A 唤醒。这样线程 B 中执行 V 操作之前的代码序列 B-stmts +和线程 A 中执行 P 操作之后的代码 A-stmts 序列之间就形成了一种确定的同步执行关系,即线程 B 的 +B-stmts 会先执行,然后才是线程 A 的 A-stmts 开始执行。相关伪代码如下所示: + +.. code-block:: rust + :linenos: + + let static mut S: semaphore = 0; + + //Thread A + ... + P(S); + Label_2: + A-stmts after Thread B::Label_1; + ... + + //Thread B + ... + B-stmts before Thread A::Label_2; + Label_1: + V(S); + ... + + +实现信号量 +------------------------------------------ + +使用 semaphore 系统调用 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +我们通过例子来看看如何实际使用信号量。下面是面向应用程序对信号量系统调用的简单使用, +可以看到对它的使用与上一节介绍的互斥锁系统调用类似。 + +在这个例子中,主线程先创建了信号量初值为 0 的信号量 ``SEM_SYNC`` ,然后再创建两个线程 First +和 Second 。线程 First 会先睡眠 10ms,而当线程 Second 执行时,会由于执行信号量的 P +操作而等待睡眠;当线程 First 醒来后,会执行 V 操作,从而能够唤醒线程 Second。这样线程 First +和线程 Second 就形成了一种稳定的同步关系。 + +.. code-block:: rust + :linenos: + :emphasize-lines: 5,10,16,22,25,28 + + const SEM_SYNC: usize = 0; //信号量ID + unsafe fn first() -> ! { + sleep(10); + println!("First work and wakeup Second"); + semaphore_up(SEM_SYNC); //信号量V操作 + exit(0) + } + unsafe fn second() -> ! { + println!("Second want to continue,but need to wait first"); + semaphore_down(SEM_SYNC); //信号量P操作 + println!("Second can work now"); + exit(0) + } + pub fn main() -> i32 { + // create semaphores + assert_eq!(semaphore_create(0) as usize, SEM_SYNC); // 信号量初值为0 + // create first, second threads + ... + } + + pub fn sys_semaphore_create(res_count: usize) -> isize { + syscall(SYSCALL_SEMAPHORE_CREATE, [res_count, 0, 0]) + } + pub fn sys_semaphore_up(sem_id: usize) -> isize { + syscall(SYSCALL_SEMAPHORE_UP, [sem_id, 0, 0]) + } + pub fn sys_semaphore_down(sem_id: usize) -> isize { + syscall(SYSCALL_SEMAPHORE_DOWN, [sem_id, 0, 0]) + } + + +- 第 16 行,创建了一个初值为 0 ,ID 为 ``SEM_SYNC`` 的信号量,对应的是第 22 行 + ``SYSCALL_SEMAPHORE_CREATE`` 系统调用; +- 第 10 行,线程 Second 执行信号量 P 操作(对应第 28行 ``SYSCALL_SEMAPHORE_DOWN`` + 系统调用),由于信号量初值为 0 ,该线程将阻塞; +- 第 5 行,线程 First 执行信号量 V 操作(对应第 25 行 ``SYSCALL_SEMAPHORE_UP`` 系统调用), + 会唤醒等待该信号量的线程 Second。 + +实现 semaphore 系统调用 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +操作系统如何实现信号量系统调用呢?我们还是采用通常的分析做法:数据结构+方法, +即首先考虑一下与此相关的核心数据结构,然后考虑与数据结构相关的相关函数/方法的实现。 + +在线程的眼里,信号量是一种每个线程能看到的共享资源,且在一个进程中,可以存在多个不同信号量资源, +所以我们可以把所有的信号量资源放在一起让进程来管理,如下面代码第 9 行所示。这里需要注意的是: +``semaphore_list: Vec>>`` 表示的是信号量资源的列表。而 ``Semaphore`` +是信号量的内核数据结构,由信号量值和等待队列组成。操作系统需要显式地施加某种控制,来确定当一个线程执行 +P 操作和 V 操作时,如何让线程睡眠或唤醒线程。在这里,P 操作是由 ``Semaphore`` 的 ``down`` +方法实现,而 V 操作是由 ``Semaphore`` 的 ``up`` 方法实现。 + +.. code-block:: rust + :linenos: + :emphasize-lines: 9,16,17,34-36,44-47 + + pub struct ProcessControlBlock { + // immutable + pub pid: PidHandle, + // mutable + inner: UPSafeCell, + } + pub struct ProcessControlBlockInner { + ... + pub semaphore_list: Vec>>, + } + + pub struct Semaphore { + pub inner: UPSafeCell, + } + pub struct SemaphoreInner { + pub count: isize, + pub wait_queue: VecDeque>, + } + impl Semaphore { + pub fn new(res_count: usize) -> Self { + Self { + inner: unsafe { UPSafeCell::new( + SemaphoreInner { + count: res_count as isize, + wait_queue: VecDeque::new(), + } + )}, + } + } + + pub fn up(&self) { + let mut inner = self.inner.exclusive_access(); + inner.count += 1; + if inner.count <= 0 { + if let Some(task) = inner.wait_queue.pop_front() { + add_task(task); + } + } + } + + pub fn down(&self) { + let mut inner = self.inner.exclusive_access(); + inner.count -= 1; + if inner.count < 0 { + inner.wait_queue.push_back(current_task().unwrap()); + drop(inner); + block_current_and_run_next(); + } + } + } + + +首先是核心数据结构: + +- 第 9 行,进程控制块中管理的信号量列表。 +- 第 16-17 行,信号量的核心数据成员:信号量值和等待队列。 + +然后是重要的三个成员函数: + +- 第 20 行,创建信号量,信号量初值为参数 ``res_count`` 。 +- 第 31 行,实现 V 操作的 ``up`` 函数,第 34 行,当信号量值小于等于 0 时, + 将从信号量的等待队列中弹出一个线程放入线程就绪队列。 +- 第 41 行,实现 P 操作的 ``down`` 函数,第 44 行,当信号量值小于 0 时, + 将把当前线程放入信号量的等待队列,设置当前线程为挂起状态并选择新线程执行。 + + +Dijkstra, Edsger W. Cooperating sequential processes (EWD-123) (PDF). E.W. Dijkstra Archive. +Center for American History, University of Texas at Austin. (transcription) (September 1965) +https://www.cs.utexas.edu/users/EWD/transcriptions/EWD01xx/EWD123.html + +Downey, Allen B. (2016) [2005]. "The Little Book of Semaphores" (2nd ed.). Green Tea Press. + +Leppäjärvi, Jouni (May 11, 2008). "A pragmatic, historically oriented survey on the universality +of synchronization primitives" (pdf). University of Oulu, Finland. \ No newline at end of file diff --git a/_sources/chapter8/4condition-variable.rst.txt b/_sources/chapter8/4condition-variable.rst.txt new file mode 100644 index 0000000..699fa1a --- /dev/null +++ b/_sources/chapter8/4condition-variable.rst.txt @@ -0,0 +1,300 @@ +条件变量机制 +========================================= + +本节导读 +----------------------------------------- + +到目前为止,我们已经了解了操作系统提供的互斥锁和信号量。但应用程序在使用这两者时需要非常小心, +如果使用不当,就会产生效率低下、竞态条件、死锁或者其他一些不可预测的情况。为了简化编程、避免错误, +计算机科学家针对某些情况设计了一种更高层的同步互斥原语。具体而言,在有些情况下, +线程需要检查某一条件(condition)满足之后,才会继续执行。 + +我们来看一个例子,有两个线程 first 和 second 在运行,线程 first 会把全局变量 A 设置为 +1,而线程 second 在 ``A != 0`` 的条件满足后,才能继续执行,如下面的伪代码所示: + +.. code-block:: rust + :linenos: + + static mut A: usize = 0; + unsafe fn first() -> ! { + A=1; + ... + } + + unsafe fn second() -> ! { + while A==0 { + // 忙等或睡眠等待 A==1 + }; + //继续执行相关事务 + } + +在上面的例子中,如果线程 second 先执行,会忙等在 while 循环中,在操作系统的调度下,线程 +first 会执行并把 A 赋值为 1 后,然后线程 second 再次执行时,就会跳出 while 循环,进行接下来的工作。 +配合互斥锁,可以正确完成上述带条件的同步流程,如下面的伪代码所示: + +.. code-block:: rust + :linenos: + + static mut A: usize = 0; + unsafe fn first() -> ! { + mutex.lock(); + A=1; + mutex.unlock(); + ... + } + + unsafe fn second() -> ! { + mutex.lock(); + while A==0 { + mutex.unlock(); + // give other thread a chance to lock + mutex.lock(); + }; + mutex.unlock(); + //继续执行相关事务 + } + +这种实现能执行,但效率低下,因为线程 second 会忙等检查,浪费处理器时间。我们希望有某种方式让线程 +second 休眠,直到等待的条件满足,再继续执行。于是,我们可以写出如下的代码: + +.. code-block:: rust + :linenos: + + static mut A: usize = 0; + unsafe fn first() -> ! { + mutex.lock(); + A=1; + wakup(second); + mutex.unlock(); + ... + } + + unsafe fn second() -> ! { + mutex.lock(); + while A==0 { + wait(); + }; + mutex.unlock(); + //继续执行相关事务 + } + +粗略地看,这样就可以实现睡眠等待了。但请同学仔细想想,当线程 second 在睡眠的时候, ``mutex`` +是否已经上锁了? 确实,线程 second 是带着上锁的 ``mutex`` 进入等待睡眠状态的。 +如果这两个线程的调度顺序是先执行线程 second,再执行线程first,那么线程 second 会先睡眠且拥有 +``mutex`` 的锁;当线程 first 执行时,会由于没有 ``mutex`` 的锁而进入等待锁的睡眠状态。 +结果就是两个线程都睡了,都执行不下去,这就出现了 **死锁** 。 + +这里需要解决的两个关键问题: **如何等待一个条件?** 和 **在条件为真时如何向等待线程发出信号** 。 +我们的计算机科学家给出了 **管程(Monitor)** 和 **条件变量(Condition Variables)** +这种巧妙的方法。接下来,我们就会深入讲解条件变量的设计与实现。 + +条件变量的基本思路 +------------------------------------------- + +管程有一个很重要的特性,即任一时刻只能有一个活跃线程调用管程中的过程, +这一特性使线程在调用执行管程中过程时能保证互斥,这样线程就可以放心地访问共享变量。 +管程是编程语言的组成部分,编译器知道其特殊性,因此可以采用与其他过程调用不同的方法来处理对管程的调用. +因为是由编译器而非程序员来生成互斥相关的代码,所以出错的可能性要小。 + +管程虽然借助编译器提供了一种实现互斥的简便途径,但这还不够,还需要一种线程间的沟通机制。 +首先是等待机制:由于线程在调用管程中某个过程时,发现某个条件不满足,那就在无法继续运行而被阻塞。 +其次是唤醒机制:另外一个线程可以在调用管程的过程中,把某个条件设置为真,并且还需要有一种机制, +及时唤醒等待条件为真的阻塞线程。为了避免管程中同时有两个活跃线程, +我们需要一定的规则来约定线程发出唤醒操作的行为。目前有三种典型的规则方案: + +- Hoare 语义:线程发出唤醒操作后,马上阻塞自己,让新被唤醒的线程运行。注:此时唤醒线程的执行位置还在管程中。 +- Hansen 语义:是执行唤醒操作的线程必须立即退出管程,即唤醒操作只可能作为一个管程过程的最后一条语句。 + 注:此时唤醒线程的执行位置离开了管程。 +- Mesa 语义:唤醒线程在发出行唤醒操作后继续运行,并且只有它退出管程之后,才允许等待的线程开始运行。 + 注:此时唤醒线程的执行位置还在管程中。 + +一般开发者会采纳 Brinch Hansen 的建议,因为它在概念上更简单,并且更容易实现。这种沟通机制的具体实现就是 +**条件变量** 和对应的操作:wait 和 signal。线程使用条件变量来等待一个条件变成真。 +条件变量其实是一个线程等待队列,当条件不满足时,线程通过执行条件变量的 wait +操作就可以把自己加入到等待队列中,睡眠等待(waiting)该条件。另外某个线程,当它改变条件为真后, +就可以通过条件变量的 signal 操作来唤醒一个或者多个等待的线程(通过在该条件上发信号),让它们继续执行。 + +早期提出的管程是基于 Concurrent Pascal 来设计的,其他语言如 C 和 Rust 等,并没有在语言上支持这种机制。 +我们还是可以用手动加入互斥锁的方式来代替编译器,就可以在 C 和 Rust 的基础上实现原始的管程机制了。 +在目前的 C 语言应用开发中,实际上也是这么做的。这样,我们就可以用互斥锁和条件变量, +来重现上述的同步互斥例子: + +.. code-block:: rust + :linenos: + + static mut A: usize = 0; + unsafe fn first() -> ! { + mutex.lock(); + A=1; + condvar.wakup(); + mutex.unlock(); + ... + } + + unsafe fn second() -> ! { + mutex.lock(); + while A==0 { + condvar.wait(mutex); //在睡眠等待之前,需要释放mutex + }; + mutex.unlock(); + //继续执行相关事务 + } + +有了上面的介绍,我们就可以实现条件变量的基本逻辑了。下面是条件变量的 wait 和 signal 操作的伪代码: + +.. code-block:: rust + :linenos: + + fn wait(mutex) { + mutex.unlock(); + ; + mutex.lock(); + } + + fn signal() { + ; + } + +条件变量的wait操作包含三步,1. 释放锁;2. 把自己挂起;3. 被唤醒后,再获取锁。条件变量的 signal +操作只包含一步:找到挂在条件变量上睡眠的线程,把它唤醒。 + +注意,条件变量不像信号量那样有一个整型计数值的成员变量,所以条件变量也不能像信号量那样有读写计数值的能力。 +如果一个线程向一个条件变量发送唤醒操作,但是在该条件变量上并没有等待的线程,则唤醒操作实际上什么也没做。 + +实现条件变量 +------------------------------------------- + +使用 condvar 系统调用 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +我们通过例子来看看如何实际使用条件变量。下面是面向应用程序对条件变量系统调用的简单使用, +可以看到对它的使用与上一节介绍的信号量系统调用类似。 在这个例子中,主线程先创建了初值为 1 +的互斥锁和一个条件变量,然后再创建两个线程 First 和 Second。线程 First 会先睡眠 10ms,而当线程 +Second 执行时,会由于条件不满足执行条件变量的 wait 操作而等待睡眠;当线程 First 醒来后,通过设置 +A 为 1,让线程 second 等待的条件满足,然后会执行条件变量的 signal 操作,从而能够唤醒线程 Second。 +这样线程 First 和线程 Second 就形成了一种稳定的同步与互斥关系。 + +.. code-block:: rust + :linenos: + :emphasize-lines: 11,19,26,33,36,39 + + static mut A: usize = 0; //全局变量 + + const CONDVAR_ID: usize = 0; + const MUTEX_ID: usize = 0; + + unsafe fn first() -> ! { + sleep(10); + println!("First work, Change A --> 1 and wakeup Second"); + mutex_lock(MUTEX_ID); + A=1; + condvar_signal(CONDVAR_ID); + mutex_unlock(MUTEX_ID); + ... + } + unsafe fn second() -> ! { + println!("Second want to continue,but need to wait A=1"); + mutex_lock(MUTEX_ID); + while A==0 { + condvar_wait(CONDVAR_ID, MUTEX_ID); + } + mutex_unlock(MUTEX_ID); + ... + } + pub fn main() -> i32 { + // create condvar & mutex + assert_eq!(condvar_create() as usize, CONDVAR_ID); + assert_eq!(mutex_blocking_create() as usize, MUTEX_ID); + // create first, second threads + ... + } + + pub fn condvar_create() -> isize { + sys_condvar_create(0) + } + pub fn condvar_signal(condvar_id: usize) { + sys_condvar_signal(condvar_id); + } + pub fn condvar_wait(condvar_id: usize, mutex_id: usize) { + sys_condvar_wait(condvar_id, mutex_id); + } + +- 第 26 行,创建了一个 ID 为 ``CONDVAR_ID`` 的条件量,对应第 33 行 ``SYSCALL_CONDVAR_CREATE`` 系统调用; +- 第 19 行,线程 Second 执行条件变量 ``wait`` 操作(对应第 39 行 ``SYSCALL_CONDVAR_WAIT`` 系统调用), + 该线程将释放 ``mutex`` 锁并阻塞; +- 第 5 行,线程 First 执行条件变量 ``signal`` 操作(对应第 36 行 ``SYSCALL_CONDVAR_SIGNAL`` 系统调用), + 会唤醒等待该条件变量的线程 Second。 + + +实现 condvar 系统调用 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +操作系统如何实现条件变量系统调用呢?在线程的眼里,条件变量是一种每个线程能看到的共享资源, +且在一个进程中,可以存在多个不同条件变量资源,所以我们可以把所有的条件变量资源放在一起让进程来管理, +如下面代码第9行所示。这里需要注意的是: ``condvar_list: Vec>>`` +表示的是条件变量资源的列表。而 ``Condvar`` 是条件变量的内核数据结构,由等待队列组成。 +操作系统需要显式地施加某种控制,来确定当一个线程执行 ``wait`` 操作和 ``signal`` 操作时, +如何让线程睡眠或唤醒线程。在这里, ``wait`` 操作是由 ``Condvar`` 的 ``wait`` 方法实现,而 ``signal`` +操作是由 ``Condvar`` 的 ``signal`` 方法实现。 + +.. code-block:: rust + :linenos: + :emphasize-lines: 9,15,18,27,33 + + pub struct ProcessControlBlock { + // immutable + pub pid: PidHandle, + // mutable + inner: UPSafeCell, + } + pub struct ProcessControlBlockInner { + ... + pub condvar_list: Vec>>, + } + pub struct Condvar { + pub inner: UPSafeCell, + } + pub struct CondvarInner { + pub wait_queue: VecDeque>, + } + impl Condvar { + pub fn new() -> Self { + Self { + inner: unsafe { UPSafeCell::new( + CondvarInner { + wait_queue: VecDeque::new(), + } + )}, + } + } + pub fn signal(&self) { + let mut inner = self.inner.exclusive_access(); + if let Some(task) = inner.wait_queue.pop_front() { + add_task(task); + } + } + pub fn wait(&self, mutex:Arc) { + mutex.unlock(); + let mut inner = self.inner.exclusive_access(); + inner.wait_queue.push_back(current_task().unwrap()); + drop(inner); + block_current_and_run_next(); + mutex.lock(); + } + } + +首先是核心数据结构: + +- 第 9 行,进程控制块中管理的条件变量列表。 +- 第 15 行,条件变量的核心数据成员:等待队列。 + +然后是重要的三个成员函数: + +- 第 18 行,创建条件变量,即创建了一个空的等待队列。 +- 第 27 行,实现 ``signal`` 操作,将从条件变量的等待队列中弹出一个线程放入线程就绪队列。 +- 第 33 行,实现 ``wait`` 操作,释放 ``mutex`` 互斥锁,将把当前线程放入条件变量的等待队列, + 设置当前线程为挂起状态并选择新线程执行。在恢复执行后,再加上 ``mutex`` 互斥锁。 + +Hansen, Per Brinch (1993). "Monitors and concurrent Pascal: a personal history". HOPL-II: +The second ACM SIGPLAN conference on History of programming languages. History of Programming +Languages. New York, NY, USA: ACM. pp. 1–35. doi:10.1145/155360.155361. ISBN 0-89791-570-4. \ No newline at end of file diff --git a/_sources/chapter8/5exercise.rst.txt b/_sources/chapter8/5exercise.rst.txt new file mode 100644 index 0000000..c37fde6 --- /dev/null +++ b/_sources/chapter8/5exercise.rst.txt @@ -0,0 +1,132 @@ +chapter8 练习 +======================================= + +Lab5 编程作业 +-------------------------------------- + +.. warning:: + + 本次实验框架变动较大,且改动较为复杂,为降低同学们的工作量,本次实验不要求合并之前的实验内容, + 只需通过 ch8 的全部测例和其他章节的基础测例即可。你可以参考 `lab5(os8)参考框架: `_ 上完成以下作业。 + +.. note:: + + 本次实验的工作量约为 100 行代码。 + + +死锁检测 ++++++++++++++++++++++++++++++++ + +目前的 mutex 和 semaphore 相关的系统调用不会分析资源的依赖情况,用户程序可能出现死锁。 +我们希望在系统中加入死锁检测机制,当发现可能发生死锁时拒绝对应的资源获取请求。 +一种检测死锁的算法如下: + +定义如下三个数据结构: + +- 可利用资源向量 Available :含有 m 个元素的一维数组,每个元素代表可利用的某一类资源的数目, + 其初值是该类资源的全部可用数目,其值随该类资源的分配和回收而动态地改变。 + Available[j] = k,表示第 j 类资源的可用数量为 k。 +- 分配矩阵 Allocation:n * m 矩阵,表示每类资源已分配给每个线程的资源数。 + Allocation[i,j] = g,则表示线程 i 当前己分得第 j 类资源的数量为 g。 +- 需求矩阵 Need:n * m 的矩阵,表示每个线程还需要的各类资源数量。 + Need[i,j] = d,则表示线程 i 还需要第 j 类资源的数量为 d 。 + +算法运行过程如下: + +1. 设置两个向量: 工作向量 Work,表示操作系统可提供给线程继续运行所需的各类资源数目,它含有 + m 个元素。初始时,Work = Available ;结束向量 Finish,表示系统是否有足够的资源分配给线程, + 使之运行完成。初始时 Finish[0..n-1] = false,表示所有线程都没结束;当有足够资源分配给线程时, + 设置 Finish[i] = true。 +2. 从线程集合中找到一个能满足下述条件的线程 + +.. code-block:: Rust + :linenos: + + Finish[i] == false; + Need[i,j] ≤ Work[j]; + +若找到,执行步骤 3,否则执行步骤 4。 + +3. 当线程 thr[i] 获得资源后,可顺利执行,直至完成,并释放出分配给它的资源,故应执行: + +.. code-block:: Rust + :linenos: + + Work[j] = Work[j] + Allocation[i, j]; + Finish[i] = true; + +跳转回步骤2 + +4. 如果 Finish[0..n-1] 都为 true,则表示系统处于安全状态;否则表示系统处于不安全状态,即出现死锁。 + +出于兼容性和灵活性考虑,我们允许进程按需开启或关闭死锁检测功能。为此我们将实现一个新的系统调用: +``sys_enable_deadlock_detect`` 。 + +**enable_deadlock_detect**: + +* syscall ID: 469 +* 功能:为当前进程启用或禁用死锁检测功能。 +* C 接口: ``int enable_deadlock_detect(int is_enable)`` +* Rust 接口: ``fn enable_deadlock_detect(is_enable: i32) -> i32`` +* 参数: + * is_enable: 为 1 表示启用死锁检测, 0 表示禁用死锁检测。 +* 说明: + * 开启死锁检测功能后, ``mutex_lock`` 和 ``semaphore_down`` 如果检测到死锁, + 应拒绝相应操作并返回 -0xDEAD (十六进制值)。 + * 简便起见可对 mutex 和 semaphore 分别进行检测,无需考虑二者 (以及 ``waittid`` 等) + 混合使用导致的死锁。 +* 返回值:如果出现了错误则返回 -1,否则返回 0。 +* 可能的错误 + * 参数不合法 + * 死锁检测开启失败 + + +实验要求 ++++++++++++++++++++++++++++++++++++++++++ + +- `lab5(os8)参考框架: `_ +- 实验目录在 ``os8`` 。 +- 通过所有测例。 + +问答作业 +-------------------------------------------- + +1. 在我们的多线程实现中,当主线程 (即 0 号线程) 退出时,视为整个进程退出, + 此时需要结束该进程管理的所有线程并回收其资源。 + - 需要回收的资源有哪些? + - 其他线程的 TaskControlBlock 可能在哪些位置被引用,分别是否需要回收,为什么? +2. 对比以下两种 ``Mutex.unlock`` 的实现,二者有什么区别?这些区别可能会导致什么问题? + +.. code-block:: Rust + :linenos: + + impl Mutex for Mutex1 { + fn unlock(&self) { + let mut mutex_inner = self.inner.exclusive_access(); + assert!(mutex_inner.locked); + mutex_inner.locked = false; + if let Some(waking_task) = mutex_inner.wait_queue.pop_front() { + add_task(waking_task); + } + } + } + + impl Mutex for Mutex2 { + fn unlock(&self) { + let mut mutex_inner = self.inner.exclusive_access(); + assert!(mutex_inner.locked); + if let Some(waking_task) = mutex_inner.wait_queue.pop_front() { + add_task(waking_task); + } else { + mutex_inner.locked = false; + } + } + } + + +报告要求 +------------------------------- + +- 简单总结你实现的功能(200字以内,不要贴代码)及你完成本次实验所用的时间。 +- 完成问答题。 +- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 diff --git a/_sources/chapter8/index.rst.txt b/_sources/chapter8/index.rst.txt new file mode 100644 index 0000000..a89f945 --- /dev/null +++ b/_sources/chapter8/index.rst.txt @@ -0,0 +1,15 @@ +第八章:并发 +============================================== + +.. toctree:: + :maxdepth: 4 + + 0intro + 1thread-kernel + 2lock + 3semaphore + 4condition-variable + 5exercise + +.. chyyuu + 扩展章节,添加其他类型同步互斥的介绍 \ No newline at end of file diff --git a/_sources/index.rst.txt b/_sources/index.rst.txt new file mode 100644 index 0000000..ec870db --- /dev/null +++ b/_sources/index.rst.txt @@ -0,0 +1,85 @@ +.. rCore-Tutorial-Guide-2022S 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. + +2022年开源操作系统训练营 +================================================== + +.. toctree:: + :maxdepth: 2 + :caption: 正文 + :hidden: + + 0setup-devel-env + chapter1/index + chapter2/index + chapter3/index + chapter4/index + chapter5/index + chapter6/index + chapter7/index + chapter8/index + +.. toctree:: + :maxdepth: 2 + :caption: 附录 + :hidden: + + appendix-a/index + appendix-b/index + appendix-c/index + appendix-d/index + +.. toctree:: + :maxdepth: 2 + :caption: 开发注记 + :hidden: + + setup-sphinx + rest-example + + +项目简介 +--------------------- + +本教程展示了如何 **从零开始** 用 **Rust** 语言写一个基于 **RISC-V** 架构的 **类 Unix 内核** 。 + +用于 2022年开源操作系统训练营。 + +导读 +--------------------- + +请先阅读 :doc:`0setup-devel-env` 完成环境配置。 + +以下是读者为了完成实验需掌握的技术,你可以在实操中熟悉它们。 + +- 阅读简单的 Makefile 文件; +- 阅读简单的 RISC-V 汇编代码; +- git 的基本功能,解决 git merge 冲突的办法; +- Rust 基本语法和一些进阶语法,包括 **Cargo 项目结构、Trait、函数式编程、Unsafe Rust、错误处理等** 。 + +鸣谢 +---------------------- +本项目基于 `2022 年春季学期操作系统实验指导书 `_ ,重构的目标是在保留结构的基础上屏蔽不必要的细节,缩短篇幅,优化语言,降低阅读成本。 + +如果你觉得本教程某些章节不够细致或不够连贯,可以参考 `2022春季OS课程讲义 rCore Tutorial v3 Guide `_ 。 + +.. note:: + + 这是一个注解,以这种方式出现的卡片提供了非必要的背景知识,你可以选择忽略。 + + +.. attention:: + + 虽然实验本身在总评中占比有限,但根据往届经验,考试中可能大量出现与编程作业、思考题、代码实现思路直接相关的题目。 + + +项目协作 +---------------------- + +- :doc:`/setup-sphinx` 介绍了如何基于 Sphinx 框架配置文档开发环境,之后可以本地构建并渲染 html 或其他格式的文档; +- :doc:`/rest-example` 给出了目前编写文档才用的 ReStructuredText 标记语言的一些基础语法及用例; +- 时间仓促,本项目还有很多不完善之处,欢迎大家积极在每一个章节的评论区留言,或者提交 Issues 或 Pull Requests,让我们 + 一起努力让这本书变得更好! + diff --git a/_sources/rest-example.rst.txt b/_sources/rest-example.rst.txt new file mode 100644 index 0000000..dbaa0dc --- /dev/null +++ b/_sources/rest-example.rst.txt @@ -0,0 +1,76 @@ +reStructuredText 基本语法 +===================================================== + +.. toctree:: + :hidden: + :maxdepth: 4 + +.. note:: + 下面是一个注记。 + + `这里 `_ 给出了在 Sphinx 中 + 外部链接的引入方法。注意,链接的名字和用一对尖括号包裹起来的链接地址之间必须有一个空格。链接最后的下划线和片段的后续内容之间也需要 + 有一个空格。 + + 接下来是一个文档内部引用的例子。比如,戳 :doc:`chapter0/5setup-devel-env` 可以进入快速上手环节。 + +.. warning:: + + 下面是一个警告。 + + .. code-block:: rust + :linenos: + :caption: 一段示例 Rust 代码 + + // 我们甚至可以插入一段 Rust 代码! + fn add(a: i32, b: i32) -> i32 { a + b } + + 下面继续我们的警告。 + +.. attention:: Here is an attention. + +.. caution:: please be cautious! + +.. error:: + + 下面是一个错误。 + +.. danger:: it is dangerous! + + +.. tip:: here is a tip + +.. important:: this is important! + +.. hint:: this is a hint. + + + +这里是一行数学公式 :math:`\sin(\alpha+\beta)=\sin\alpha\cos\beta+\cos\alpha\sin\beta`。 + +基本的文本样式:这是 *斜体* ,这是 **加粗** ,接下来的则是行间公式 ``a0`` 。它们的前后都需要有一个空格隔开其他内容,这个让人挺不爽的... + +`这是 `_ 一个全面展示 +章节分布的例子,来自于 ReadTheDocs 的官方文档。事实上,现在我们也采用 ReadTheDocs 主题了,它非常美观大方。 + +下面是一个测试 gif。 + +.. image:: resources/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/_sources/setup-sphinx.rst.txt b/_sources/setup-sphinx.rst.txt new file mode 100644 index 0000000..850fcce --- /dev/null +++ b/_sources/setup-sphinx.rst.txt @@ -0,0 +1,16 @@ +修改和构建本项目 +==================================== + +.. toctree:: + :hidden: + :maxdepth: 4 + +TL;DR: ``python -m venv .venv`` 创建一个虚拟环境(你也可以使用 conda 等工具),activate 后 ``pip install -r requirements.txt``。 + +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. 确认修改无误之后,将更改提交到自己的仓库,然后向项目仓库提交 Pull Request。如有问题,可直接提交 Issue 或课程微信群内联系助教。 diff --git a/_static/basic.css b/_static/basic.css new file mode 100644 index 0000000..912859b --- /dev/null +++ b/_static/basic.css @@ -0,0 +1,904 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2021 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 p.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, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.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, +aside.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, +aside.sidebar > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.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, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption 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; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- 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; +} + +.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.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +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.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/_static/doctools.js b/_static/doctools.js new file mode 100644 index 0000000..8cbf1b1 --- /dev/null +++ b/_static/doctools.js @@ -0,0 +1,323 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Sphinx JavaScript utilities for all documentation. + * + * :copyright: Copyright 2007-2021 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 + * + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL + */ +jQuery.urldecode = function(x) { + if (!x) { + return 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; + } + break; + case 39: // right + var nextHref = $('link[rel="next"]').prop('href'); + if (nextHref) { + window.location.href = nextHref; + return false; + } + break; + } + } + }); + } +}; + +// quick alias for translations +_ = Documentation.gettext; + +$(document).ready(function() { + Documentation.init(); +}); diff --git a/_static/documentation_options.js b/_static/documentation_options.js new file mode 100644 index 0000000..3f308f5 --- /dev/null +++ b/_static/documentation_options.js @@ -0,0 +1,12 @@ +var DOCUMENTATION_OPTIONS = { + URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), + VERSION: '', + 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/_static/dracula.css b/_static/dracula.css new file mode 100644 index 0000000..30def14 --- /dev/null +++ b/_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/_static/file.png b/_static/file.png new file mode 100644 index 0000000000000000000000000000000000000000..a858a410e4faa62ce324d814e4b816fff83a6fb3 GIT binary patch literal 286 zcmV+(0pb3MP)s`hMrGg#P~ix$^RISR_I47Y|r1 z_CyJOe}D1){SET-^Amu_i71Lt6eYfZjRyw@I6OQAIXXHDfiX^GbOlHe=Ae4>0m)d(f|Me07*qoM6N<$f}vM^LjV8( literal 0 HcmV?d00001 diff --git a/_static/jquery-3.5.1.js b/_static/jquery-3.5.1.js new file mode 100644 index 0000000..5093733 --- /dev/null +++ b/_static/jquery-3.5.1.js @@ -0,0 +1,10872 @@ +/*! + * 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( " + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+ + +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/appendix-b/index.html b/appendix-b/index.html new file mode 100644 index 0000000..3c0e232 --- /dev/null +++ b/appendix-b/index.html @@ -0,0 +1,674 @@ + + + + + + + + 附录 B:常见工具的使用方法 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

附录 B:常见工具的使用方法

+
+
+
+

分析可执行文件

+

对于Rust编译器生成的执行程序,可通过各种有效工具进行分析。如果掌握了对这些工具的使用,那么在后续的开发工作中,对碰到的各种奇怪问题就进行灵活处理和解决了。 +我们以Rust编译生成的一个简单的“Hello, world”应用执行程序为分析对象,看看如何进行分析。

+

让我们先来通过 file 工具看看最终生成的可执行文件的格式:

+
$ 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, ......
+
+$
+
+
+

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

+
+

rust-readobj

+

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

+
$ rust-readobj -all target/debug/os
+
+
+

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

+
 1File: target/debug/os
+ 2Format: elf64-x86-64
+ 3Arch: x86_64
+ 4AddressSize: 64bit
+ 5LoadName:
+ 6ElfHeader {
+ 7Ident {
+ 8   Magic: (7F 45 4C 46)
+ 9   Class: 64-bit (0x2)
+10   DataEncoding: LittleEndian (0x1)
+11   FileVersion: 1
+12   OS/ABI: SystemV (0x0)
+13   ABIVersion: 0
+14   Unused: (00 00 00 00 00 00 00)
+15}
+16Type: SharedObject (0x3)
+17Machine: EM_X86_64 (0x3E)
+18Version: 1
+19Entry: 0x5070
+20ProgramHeaderOffset: 0x40
+21SectionHeaderOffset: 0x32D8D0
+22Flags [ (0x0)
+23]
+24HeaderSize: 64
+25ProgramHeaderEntrySize: 56
+26ProgramHeaderCount: 12
+27SectionHeaderEntrySize: 64
+28SectionHeaderCount: 42
+29StringTableSectionIndex: 41
+30}
+31......
+
+
+
    +
  • 第 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,下面是个具体的例子:

+
......
+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 给出。。

+

我们还能够看到程序中的符号表:

+
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文件得到:

+
$ rust-objdump -all target/debug/os
+
+
+

具体结果如下:

+
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 <main>
+   5098: ff 15 d2 3b 04 00             callq   *277458(%rip)  # 48c70 <_GLOBAL_OFFSET_TABLE_+0x2d8>
+......
+00000000000051a0 <main>:
+   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 工具来清除。

+
$ 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 只保留各个段的实际数据得到一个没有任何符号的纯二进制镜像文件:

+
$ rust-objcopy --strip-all target/debug/os -O binary target/debug/os.bin
+
+
+

这样就生成了一个没有任何符号的纯二进制镜像文件。由于缺少了必要的元数据,我们的 file 工具也没有办法 +对它完成解析了。而后,我们可直接将这个二进制镜像文件手动载入到内存中合适位置即可。

+
+
+
+

qemu 平台上可执行文件和二进制镜像的生成流程

+
+

make & 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 的具体命令:

+
 1KERNEL_ENTRY_PA := 0x80020000
+ 2
+ 3BOARD                ?= qemu
+ 4SBI                  ?= rustsbi
+ 5BOOTLOADER   := ../bootloader/$(SBI)-$(BOARD).bin
+ 6
+ 7run: run-inner
+ 8
+ 9run-inner: build
+10   @qemu-system-riscv64 \
+11      -machine virt \
+12      -nographic \
+13      -bios $(BOOTLOADER) \
+14      -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)
+
+
+
+
+

qemu

+

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

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

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

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

  • +
+

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

+
+

警告

+

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

+
+
+
+ +
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/appendix-c/index.html b/appendix-c/index.html new file mode 100644 index 0000000..5c04e9c --- /dev/null +++ b/appendix-c/index.html @@ -0,0 +1,392 @@ + + + + + + + + 附录 C:深入机器模式:RustSBI - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

附录 C:深入机器模式:RustSBI

+
+
+

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/appendix-d/index.html b/appendix-d/index.html new file mode 100644 index 0000000..d85681d --- /dev/null +++ b/appendix-d/index.html @@ -0,0 +1,435 @@ + + + + + + + + 附录 D:RISC-V相关信息 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ + +
+ + + + + + + + \ No newline at end of file diff --git a/chapter1/0intro.html b/chapter1/0intro.html new file mode 100644 index 0000000..dc6508d --- /dev/null +++ b/chapter1/0intro.html @@ -0,0 +1,497 @@ + + + + + + + + 引言 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

引言

+
+

本章导读

+

大多数程序员的职业生涯都从 Hello, world! 开始。

+
printf("Hello world!\n");
+cout << "Hello world!\n";
+print("Hello world!")
+System.out.println("Hello world!");
+echo "Hello world!"
+println!("Hello world!");
+
+
+

然而,要用几行代码向世界问好,并不像表面上那么简单。 +Hello, world! 程序能够编译运行,靠的是以 编译器 为主的开发环境和以 操作系统 为主的执行环境。

+

在本章中,我们将抽丝剥茧,一步步让 Hello, world! 程序脱离其依赖的执行环境, +编写一个能打印 Hello, world! 的 OS。这趟旅途将让我们对应用程序及其执行环境有更深入的理解。

+
+

注意

+

实验指导书存在的目的是帮助读者理解框架代码。

+

为便于测试,完成编程实验时,请以框架代码为基础,不必跟着文档从零开始编写内核。

+
+

为了做到这一步,首先需要让程序不依赖于标准库, +并通过编译。

+

接下来要让脱离了标准库的程序能输出(即支持 println!),这对程序的开发和调试至关重要。 +我们先在用户态下实现该功能,在 此处 获取相关代码。

+

最后把程序移植到内核态,构建在裸机上支持输出的最小运行时环境。

+
+
+

实践体验

+
+

注解

+

基于github classroom的开发方式

+

基于github classroom,可方便建立开发用的git repository,并可基于github的 codespace (在线版ubuntu +vscode)在线开发使用。整个开发环境仅仅需要一个网络浏览器。

+
    +
  1. 在网络浏览器中用自己的 github id 登录 github.com

  2. +
  3. 接收 第一个实验练习 setup-env-run-os1 的github classroom在线邀请 ,根据提示一路选择OK即可。

  4. +
  5. 完成第二步后,你的第一个实验练习 setup-env-run-os1 的 github repository 会被自动建立好,点击此github repository的链接,就可看到你要完成的第一个实验了。

  6. +
  7. 在你的第一个实验练习的网页的中上部可以看到一个醒目的 code 绿色按钮,点击后,可以进一步看到 codespace 标签和醒目的 create codesapce on main 绿色按钮。请点击这个绿色按钮,就可以进入到在线的ubuntu +vscode环境中

  8. +
  9. 再按照下面的环境安装提示在vscode的 console 中安装配置开发环境:rustc,qemu等工具。

  10. +
  11. 在vscode的 console 中执行 make setupclassroom_test1 (该命令仅执行一次)配置githubclassroom 自动评分功能。

  12. +
  13. 然后就可以基于在线vscode进行开发、运行、提交等完整的实验过程了。

  14. +
+

上述的3,4,5步不是必须的,你也可以线下本地开发。

+
+

本章一步步实现了支持打印字符串的简单操作系统。

+

获取本章代码:

+
$ git clone https://github.com/LearningOS/rust-based-os-comp2022.git
+$ cd rust-based-os-comp2022/
+$ make setupclassroom_test1  //注意:这一步很重要,是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次,以后一般就不用执行了,除非 .github/workflows/classroom.yml发生了变化。
+
+
+

运行本章代码,并设置日志级别为 TRACE

+
$ cd os1
+$ make run LOG=TRACE
+
+
+

预期输出:

+
+../_images/color-demo.png +
+

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

+
+
+

本章代码树

+
├── bootloader (内核依赖的运行在 M 特权级的 SBI 实现,本项目中我们使用 RustSBI)
+│   └── rustsbi-qemu.bin
+├── os
+│   ├── Cargo.toml (cargo 项目配置文件)
+│   ├── Makefile
+│   └── src
+│       ├── console.rs (将打印字符的 SBI 接口进一步封装实现更加强大的格式化输出)
+│       ├── entry.asm (设置内核执行环境的的一段汇编代码)
+│       ├── lang_items.rs (需要我们提供给 Rust 编译器的一些语义项,目前包含内核 panic 时的处理逻辑)
+│       ├── linker.ld (控制内核内存布局的链接脚本以使内核运行在 qemu 虚拟机上)
+│       ├── logging.rs (为本项目实现了日志功能)
+│       ├── main.rs (内核主函数)
+│       └── sbi.rs (封装底层 SBI 实现提供的 SBI 接口)
+└── rust-toolchain (整个项目的工具链版本)
+
+cloc os
+-------------------------------------------------------------------------------
+Language                     files          blank        comment           code
+-------------------------------------------------------------------------------
+Rust                             5             25              6            155
+make                             1             11              4             34
+Assembly                         1              1              0             11
+TOML                             1              2              1              7
+-------------------------------------------------------------------------------
+SUM:                             8             39             11            207
+-------------------------------------------------------------------------------
+
+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter1/1app-ee-platform.html b/chapter1/1app-ee-platform.html new file mode 100644 index 0000000..c16d2ac --- /dev/null +++ b/chapter1/1app-ee-platform.html @@ -0,0 +1,497 @@ + + + + + + + + 应用程序执行环境与平台支持 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

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

+
+
+
+

执行应用程序

+

我们先从最简单的 Rust Hello, world 程序开始,用 Cargo 工具创建 Rust 项目。

+
$ cargo new os
+
+
+

此时,项目的文件结构如下:

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

其中 Cargo.toml 中保存了项目的库依赖、作者信息等。

+

cargo 为我们准备好了 Hello world! 源代码:

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

输入 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! ,但为了打印出 Hello, world!,我们需要的不止几行源代码。

+
+
+

理解应用程序执行环境

+

在现代通用操作系统(如 Linux)上运行应用程序,需要多层次的执行环境栈支持:

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

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

+
+

我们的应用程序通过调用标准库或第三方库提供的接口,仅需少量源代码就能完成复杂的功能; +Hello, world! 程序调用的 println! 宏就是由 Rust 标准库 std 和 GNU Libc 等提供的。 +这些库属于应用程序的 执行环境 (Execution Environment),而它们的实现又依赖于操作系统提供的系统调用。

+
+
+

平台与目标三元组

+

编译器在编译、链接得到可执行文件时需要知道,程序要在哪个 平台 (Platform) 上运行, +目标三元组 (Target Triplet) 描述了目标平台的 CPU 指令集、操作系统类型和标准运行时库。

+

我们研究一下现在 Hello, world! 程序的目标三元组是什么:

+
$ rustc --version --verbose
+   rustc 1.61.0-nightly (68369a041 2022-02-22)
+   binary: rustc
+   commit-hash: 68369a041cea809a87e5bd80701da90e0e0a4799
+   commit-date: 2022-02-22
+   host: x86_64-unknown-linux-gnu
+   release: 1.61.0-nightly
+   LLVM version: 14.0.0
+
+
+

其中 host 一项表明默认目标平台是 x86_64-unknown-linux-gnu, +CPU 架构是 x86_64,CPU 厂商是 unknown,操作系统是 linux,运行时库是 gnu libc。

+

接下来,我们希望把 Hello, world! 移植到 RICV 目标平台 riscv64gc-unknown-none-elf 上运行。

+
+

注解

+

riscv64gc-unknown-none-elf 的 CPU 架构是 riscv64gc,厂商是 unknown,操作系统是 none, +elf 表示没有标准的运行时库。没有任何系统调用的封装支持,但可以生成 ELF 格式的执行程序。 +我们不选择有 linux-gnu 支持的 riscv64gc-unknown-linux-gnu,是因为我们的目标是开发操作系统内核,而非在 linux 系统上运行的应用程序。

+
+
+
+

修改目标平台

+

将程序的目标平台换成 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
+
+
+

报错的原因是目标平台上确实没有 Rust 标准库 std,也不存在任何受 OS 支持的系统调用。 +这样的平台被我们称为 裸机平台 (bare-metal)。

+

幸运的是,除了 std 之外,Rust 还有一个不需要任何操作系统支持的核心库 core, +它包含了 Rust 语言相当一部分核心机制,可以满足本门课程的需求。 +有很多第三方库也不依赖标准库 std,而仅仅依赖核心库 core。

+

为了以裸机平台为目标编译程序,我们要将对标准库 std 的引用换成核心库 core。

+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter1/2remove-std.html b/chapter1/2remove-std.html new file mode 100644 index 0000000..3cb12f1 --- /dev/null +++ b/chapter1/2remove-std.html @@ -0,0 +1,532 @@ + + + + + + + + 移除标准库依赖 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

移除标准库依赖

+
+
+

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

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

这将使 cargo 工具在 os 目录下默认会使用 riscv64gc-unknown-none-elf 作为目标平台。 +这种编译器运行的平台(x86_64)与可执行文件运行的目标平台不同的情况,称为 交叉编译 (Cross Compile)。

+
+

移除 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 的系统调用。 +无论如何,我们先将这行代码注释掉。

+
+
+

提供语义项 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
+
+
+
+

标准库 std 提供了 Rust 错误处理函数 #[panic_handler],其大致功能是打印出错位置和原因并杀死当前应用。 +但核心库 core 并没有提供这项功能,得靠我们自己实现。

+

新建一个子模块 lang_items.rs,在里面编写 panic 处理函数,通过标记 #[panic_handler] 告知编译器采用我们的实现:

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

目前我们遇到错误什么都不做,只在原地 loop

+
+
+

移除 main 函数

+

重新编译,又有了新错误:

+
+

错误

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

编译器提醒我们缺少一个名为 start 的语义项。 +start 语义项代表了标准库 std 在执行应用程序之前需要进行的一些初始化工作。由于我们禁用了标准库,编译器也就找不到这项功能的实现了。

+

main.rs 的开头加入设置 #![no_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 {}
+}
+
+
+
+
+

分析被移除标准库的程序

+

我们可以通过一些工具来分析目前的程序:

+
[文件格式]
+$ 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。 +再通过 rust-objdump 工具把它反汇编,没有生成任何汇编代码。 +可见,这个二进制程序虽然合法,但它是一个空程序,原因是缺少了编译器规定的入口函数 _start

+

从下一节开始,我们将着手实现本节移除的、由用户态执行环境提供的功能。

+
+

注解

+

本节内容部分参考自 BlogOS 的相关章节

+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter1/3mini-rt-usrland.html b/chapter1/3mini-rt-usrland.html new file mode 100644 index 0000000..c0efe02 --- /dev/null +++ b/chapter1/3mini-rt-usrland.html @@ -0,0 +1,596 @@ + + + + + + + + 构建用户态执行环境 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

构建用户态执行环境

+
+
+
+

注解

+

前三小节的用户态程序案例代码在 此处 获取。

+
+
+

用户态最小化执行环境

+
+

执行环境初始化

+

首先我们要给 Rust 编译器编译器提供入口函数 _start() , +在 main.rs 中添加如下内容:

+
// os/src/main.rs
+#[no_mangle]
+extern "C" fn _start() {
+    loop{};
+}
+
+
+

对上述代码重新编译,再用分析工具分析:

+
$ cargo build
+   Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os)
+    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
+
+[反汇编导出汇编程序]
+$ 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>
+
+
+

反汇编出的两条指令就是一个死循环, +这说明编译器生成的已经是一个合理的程序了。 +用 qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os 命令可以执行这个程序。

+
+
+

程序正常退出

+

我们把 _start() 函数中的循环语句注释掉,重新编译并分析,看到其汇编代码是:

+
$ 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
+
+
+

看起来是合法的执行程序。但如果我们执行它,会引发问题:

+
$ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os
+  段错误 (核心已转储)
+
+
+

这个简单的程序导致 qemu-riscv64 崩溃了!为什么会这样?

+
+

注解

+

QEMU有两种运行模式:

+

User mode 模式,即用户态模拟,如 qemu-riscv64 程序, +能够模拟不同处理器的用户态指令的执行,并可以直接解析ELF可执行文件, +加载运行那些为不同处理器编译的用户级Linux应用程序。

+

System mode 模式,即系统态模式,如 qemu-system-riscv64 程序, +能够模拟一个完整的基于不同CPU的硬件系统,包括处理器、内存及其他外部设备,支持运行完整的操作系统。

+
+

目前的执行环境还缺了一个退出机制,我们需要操作系统提供的 exit 系统调用来退出程序。这里先给出代码:

+
// os/src/main.rs
+
+const SYSCALL_EXIT: usize = 93;
+
+fn syscall(id: usize, args: [usize; 3]) -> isize {
+    let mut ret;
+    unsafe {
+        core::arch::asm!(
+            "ecall",
+            inlateout("x10") args[0] => ret,
+            in("x11") args[1],
+            in("x12") args[2],
+            in("x17") id,
+        );
+    }
+    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 增加的内容不多,但还是有点与一般的应用程序有所不同,因为它引入了汇编和系统调用。 +第二章的第二节 实现应用程序 会详细介绍上述代码的含义。 +这里读者只需要知道 _start 函数调用了一个 sys_exit 函数, +向操作系统发出了退出的系统调用请求,退出码为 9

+

我们编译执行以下修改后的程序:

+
$ 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
+
+[打印程序的返回值]
+$ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os; echo $?
+9
+
+
+

可以看到,返回的结果确实是 9 。这样,我们勉强完成了一个简陋的用户态最小化执行环境。

+
+
+
+

有显示支持的用户态执行环境

+

没有 println 输出信息,终究觉得缺了点啥。

+

Rust 的 core 库内建了以一系列帮助实现显示字符的基本 Trait 和数据结构,函数等,我们可以对其中的关键部分进行扩展,就可以实现定制的 println! 功能。

+
+

实现输出字符串的相关函数

+
+

注意

+

如果你觉得理解 Rust 宏有困难,把它当成黑盒就好!

+
+

首先封装一下对 SYSCALL_WRITE 系统调用。

+
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 函数进行包装。

+
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 )。

+
#[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)+)?));
+    }
+}
+
+
+

接下来,我们调整一下应用程序,让它发出显示字符串和退出的请求:

+
#[no_mangle]
+extern "C" fn _start() {
+    println!("Hello, world!");
+    sys_exit(9);
+}
+
+
+

现在,我们编译并执行一下,可以看到正确的字符串输出,且程序也能正确退出!

+
$ 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
+
+
+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter1/4mini-rt-baremetal.html b/chapter1/4mini-rt-baremetal.html new file mode 100644 index 0000000..f976604 --- /dev/null +++ b/chapter1/4mini-rt-baremetal.html @@ -0,0 +1,658 @@ + + + + + + + + 构建裸机执行环境 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

构建裸机执行环境

+
+
+

有了上一节实现的用户态的最小执行环境,稍加改造,就可以完成裸机上的最小执行环境了。 +本节中,我们将把 Hello world! 应用程序从用户态搬到内核态。

+
+

裸机启动过程

+

用 QEMU 软件 qemu-system-riscv64 来模拟 RISC-V 64 计算机。加载内核程序的命令如下:

+
qemu-system-riscv64 \
+            -machine virt \
+            -nographic \
+            -bios $(BOOTLOADER) \
+            -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)
+
+
+
    +
  • -bios $(BOOTLOADER) 意味着硬件加载了一个 BootLoader 程序,即 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 的位置,这里有固化在硬件中的一小段引导代码, +它会很快跳转到 0x80000000 的 RustSBI 处。 +RustSBI完成硬件初始化后,会跳转到 $(KERNEL_BIN) 所在内存位置 0x80200000 处, +执行操作系统的第一条指令。

+
+../_images/chap1-intro.png +
+
+

注解

+

RustSBI 是什么?

+

SBI 是 RISC-V 的一种底层规范,RustSBI 是它的一种实现。 +操作系统内核与 RustSBI 的关系有点像应用与操作系统内核的关系,后者向前者提供一定的服务。只是SBI提供的服务很少, +比如关机,显示字符串等。

+
+
+
+

实现关机功能

+

对上一节实现的代码稍作调整,通过 ecall 调用 RustSBI 实现关机功能:

+
// 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 {
+      core::arch::asm!(
+          "ecall",
+...
+
+const SBI_SHUTDOWN: usize = 8;
+
+pub fn shutdown() -> ! {
+    sbi_call(SBI_SHUTDOWN, 0, 0, 0);
+    panic!("It should shutdown!");
+}
+
+// os/src/main.rs
+#[no_mangle]
+extern "C" fn _start() {
+    shutdown();
+}
+
+
+

应用程序访问操作系统提供的系统调用的指令是 ecall ,操作系统访问 +RustSBI提供的SBI调用的指令也是 ecall , +虽然指令一样,但它们所在的特权级是不一样的。 +简单地说,应用程序位于最弱的用户特权级(User Mode), +操作系统位于内核特权级(Supervisor Mode), +RustSBI位于机器特权级(Machine Mode)。 +下一章会进一步阐释具体细节。

+

编译执行,结果如下:

+
# 编译生成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
+# 无法退出,风扇狂转,感觉碰到死循环
+
+
+

问题在哪?通过 rust-readobj 分析 os 可执行程序,发现其入口地址不是 +RustSBI 约定的 0x80200000 。我们需要修改程序的内存布局并设置好栈空间。

+
+
+

设置正确的程序内存布局

+

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

+

修改 Cargo 的配置文件来使用我们自己的链接脚本 os/src/linker.ld

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

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

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

第 1 行我们设置了目标平台为 riscv ;第 2 行我们设置了整个程序的入口点为之前定义的全局符号 _start; +第 3 行定义了一个常量 BASE_ADDRESS0x80200000 ,RustSBI 期望的 OS 起始地址;

+
+

注意

+

linker 脚本的语法不做要求,感兴趣的同学可以自行查阅相关资料。

+
+

BASE_ADDRESS 开始,代码段 .text, 只读数据段 .rodata,数据段 .data, bss 段 .bss 由低到高依次放置, +且每个段都有两个全局变量给出其起始和结束地址(比如 .text 段的开始和结束地址分别是 stextetext )。

+
+
+

正确配置栈空间布局

+

用另一段汇编代码初始化栈空间:

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

在第 8 行,我们预留了一块大小为 4096 * 16 字节,也就是 \(64\text{KiB}\) 的空间, +用作操作系统的栈空间。 +栈顶地址被全局符号 boot_stack_top 标识,栈底则被全局符号 boot_stack 标识。 +同时,这块栈空间被命名为 +.bss.stack ,链接脚本里有它的位置。

+

_start 作为操作系统的入口地址,将依据链接脚本被放在 BASE_ADDRESS 处。 +la sp, boot_stack_top 作为 OS 的第一条指令, +将 sp 设置为栈空间的栈顶。 +简单起见,我们目前不考虑 sp 越过栈底 boot_stack ,也就是栈溢出的情形。 +第二条指令则是函数调用 rust_main ,这里的 rust_main 是我们稍后自己编写的应用入口。

+

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

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

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

+

第 7 行,我们使用 global_asm 宏,将同目录下的汇编文件 entry.asm 嵌入到代码中。

+

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

+

再次使用上节中的编译,生成和运行操作,我们看到QEMU模拟的RISC-V 64计算机 优雅 地退出了!

+
+
+

清空 .bss 段

+

等一等,与内存相关的部分太容易出错了, 清零 .bss 段 的工作我们还没有完成。

+
 1// os/src/main.rs
+ 2fn clear_bss() {
+ 3    extern "C" {
+ 4        fn sbss();
+ 5        fn ebss();
+ 6    }
+ 7    (sbss as usize..ebss as usize).for_each(|a| {
+ 8        unsafe { (a as *mut u8).write_volatile(0) }
+ 9    });
+10}
+11
+12pub fn rust_main() -> ! {
+13    clear_bss();
+14    shutdown();
+15}
+
+
+

链接脚本 linker.ld 中给出的全局符号 sbssebss 让我们能轻松确定 .bss 段的位置。

+
+
+

添加裸机打印相关函数

+

在上一节中我们为用户态程序实现的 println 宏,略作修改即可用于本节的内核态操作系统。 +详见 os/src/console.rs

+

利用 println 宏,我们重写异常处理函数 panic,使其在 panic 时能打印错误发生的位置。 +相关代码位于 os/src/lang_items.rs 中。

+

我们还使用第三方库 log 为你实现了日志模块,相关代码位于 os/src/logging.rs 中。

+
+

注解

+

在 cargo 项目中引入外部库 log,需要修改 Cargo.toml 加入相应的依赖信息。

+
+

现在,让我们重复一遍本章开头的试验,make run LOG=TRACE

+
+../_images/color-demo.png +
+

产生 panic 的地点与源码中的实际位置一致!至此,我们完成了第一章的实验内容,

+
+

注解

+

背景知识:理解应用程序和执行环境

+
+
+
+ +
+ +
+ +
+
+ + + + + + + + + \ No newline at end of file diff --git a/chapter1/5exercise.html b/chapter1/5exercise.html new file mode 100644 index 0000000..9b0344f --- /dev/null +++ b/chapter1/5exercise.html @@ -0,0 +1,420 @@ + + + + + + + + chapter1练习(已经废弃,没删是怕以后有用) - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

chapter1练习(已经废弃,没删是怕以后有用)

+
+
+
    +
  • 本节难度:

  • +
+
+

编程作业

+
+

彩色化 LOG

+
+
+

实验要求

+
+
+

实验检查

+
+
+
+

问答作业

+
+
+

报告要求

+
    +
  • 简单总结你实现的功能(200字以内,不要贴代码)。

  • +
  • 完成问答题。

  • +
  • (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。

  • +
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter1/index.html b/chapter1/index.html new file mode 100644 index 0000000..bda5fdf --- /dev/null +++ b/chapter1/index.html @@ -0,0 +1,429 @@ + + + + + + + + 第一章:应用程序与基本执行环境 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ + +
+ + + + + + + + \ No newline at end of file diff --git a/chapter2/0intro.html b/chapter2/0intro.html new file mode 100644 index 0000000..fe3ac38 --- /dev/null +++ b/chapter2/0intro.html @@ -0,0 +1,552 @@ + + + + + + + + 引言 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

引言

+
+

本章导读

+

批处理系统 (Batch System) 出现于计算资源匮乏的年代,其核心思想是: +将多个程序打包到一起输入计算机;当一个程序运行结束后,计算机会 自动 执行下一个程序。

+

应用程序难免会出错,如果一个程序的错误导致整个操作系统都无法运行,那就太糟糕了。 +保护 操作系统不受出错程序破坏的机制被称为 特权级 (Privilege) 机制, +它实现了用户态和内核态的隔离。

+

本章在上一章的基础上,让我们的 OS 内核能以批处理的形式一次运行多个应用程序,同时利用特权级机制, +令 OS 不因出错的用户态程序而崩溃。

+

本章首先为批处理操作系统设计用户程序,再阐述如何将这些应用程序链接到内核中,最后介绍如何利用特权级机制处理 Trap.

+
+
+

实践体验

+
+

注解

+

基于github classroom的开发方式

+

基于github classroom,可方便建立开发用的git repository,并可基于github的 codespace (在线版ubuntu +vscode)在线开发使用。整个开发环境仅仅需要一个网络浏览器。

+
    +
  1. 在网络浏览器中用自己的 github id 登录 github.com

  2. +
  3. 接收 第二个实验练习的github classroom在线邀请 ,根据提示一路选择OK即可。

  4. +
  5. 完成第二步后,你的第二个实验练习的 github repository 会被自动建立好,点击此github repository的链接,就可看到你要完成的第一个实验了。

  6. +
  7. 在你的第二个实验练习的网页的中上部可以看到一个醒目的 code 绿色按钮,点击后,可以进一步看到 codespace 标签和醒目的 create codesapce on main 绿色按钮。请点击这个绿色按钮,就可以进入到在线的ubuntu +vscode环境中

  8. +
  9. 再按照下面的环境安装提示在vscode的 console 中安装配置开发环境:rustc,qemu等工具。

  10. +
  11. 在vscode的 console 中执行 make setupclassroom_test2 (该命令仅执行一次)配置githubclassroom 自动评分功能。

  12. +
  13. 然后就可以基于在线vscode进行开发、运行、提交等完整的实验过程了。

  14. +
+

上述的3,4,5步不是必须的,你也可以线下本地开发。

+
+

本章我们引入了用户程序。

+
$ git clone https://github.com/LearningOS/rust-based-os-comp2022.git
+$ cd rust-based-os-comp2022
+$ make setupclassroom_test2  //注意:这一步很重要,是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次,以后一般就不用执行了,除非 .github/workflows/classroom.yml发生了变化。
+
+
+

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

+
$ cd os2
+$ make run LOG=INFO
+
+
+

批处理系统自动加载并运行了所有的用户程序,尽管某些程序出错了:

+
 [rustsbi] RustSBI version 0.2.0-alpha.4
+.______       __    __      _______.___________.  _______..______   __
+|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
+|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
+|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
+|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
+| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|
+
+[rustsbi] Implementation: RustSBI-QEMU Version 0.0.1
+[rustsbi-dtb] Hart count: cluster0 with 1 cores
+[rustsbi] misa: RV64ACDFIMSU
+[rustsbi] mideleg: ssoft, stimer, sext (0x222)
+[rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab)
+[rustsbi] pmp0: 0x80000000 ..= 0x800fffff (rwx)
+[rustsbi] pmp1: 0x80000000 ..= 0x807fffff (rwx)
+[rustsbi] pmp2: 0x0 ..= 0xffffffffffffff (---)
+[rustsbi] enter supervisor 0x80200000
+[kernel] Hello, world!
+[ INFO] [kernel] num_app = 6
+[ INFO] [kernel] app_0 [0x8020b040, 0x8020f868)
+[ INFO] [kernel] app_1 [0x8020f868, 0x80214090)
+[ INFO] [kernel] app_2 [0x80214090, 0x80218988)
+[ INFO] [kernel] app_3 [0x80218988, 0x8021d160)
+[ INFO] [kernel] app_4 [0x8021d160, 0x80221a68)
+[ INFO] [kernel] app_5 [0x80221a68, 0x80226538)
+[ INFO] [kernel] Loading app_0
+[ERROR] [kernel] PageFault in application, core dumped.
+[ INFO] [kernel] Loading app_1
+[ERROR] [kernel] IllegalInstruction in application, core dumped.
+[ INFO] [kernel] Loading app_2
+[ERROR] [kernel] IllegalInstruction in application, core dumped.
+[ INFO] [kernel] Loading app_3
+[ INFO] [kernel] Application exited with code 1234
+[ INFO] [kernel] Loading app_4
+Hello, world from user mode program!
+[ INFO] [kernel] Application exited with code 0
+[ INFO] [kernel] Loading app_5
+3^10000=5079(MOD 10007)
+3^20000=8202(MOD 10007)
+3^30000=8824(MOD 10007)
+3^40000=5750(MOD 10007)
+3^50000=3824(MOD 10007)
+3^60000=8516(MOD 10007)
+3^70000=2510(MOD 10007)
+3^80000=9379(MOD 10007)
+3^90000=2621(MOD 10007)
+3^100000=2749(MOD 10007)
+Test power OK!
+[ INFO] [kernel] Application exited with code 0
+Panicked at src/batch.rs:68 All applications completed!
+
+
+
+
+

本章代码树

+
── os2
+│   ├── Cargo.toml
+│   ├── Makefile (修改:构建内核之前先构建应用)
+│   ├── build.rs (新增:生成 link_app.S 将应用作为一个数据段链接到内核)
+│   └── src
+│       ├── batch.rs(新增:实现了一个简单的批处理系统)
+│       ├── console.rs
+│       ├── entry.asm
+│       ├── lang_items.rs
+│       ├── link_app.S(构建产物,由 os/build.rs 输出)
+│       ├── linker.ld
+│       ├── logging.rs
+│       ├── main.rs(修改:主函数中需要初始化 Trap 处理并加载和执行应用)
+│       ├── sbi.rs
+│       ├── sync(新增:包装了RefCell,暂时不用关心)
+│       │   ├── mod.rs
+│       │   └── up.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 上下文保存与恢复的汇编代码)
+└── user(新增:应用测例保存在 user 目录下)
+   ├── Cargo.toml
+   ├── Makefile
+   └── src
+      ├── bin(基于用户库 user_lib 开发的应用,每个应用放在一个源文件中)
+      │   ├── ...
+      ├── console.rs
+      ├── lang_items.rs
+      ├── lib.rs(用户库 user_lib)
+      ├── linker.ld(应用的链接脚本)
+      └── syscall.rs(包含 syscall 方法生成实际用于系统调用的汇编指令,
+                     各个具体的 syscall 都是通过 syscall 来实现的)
+
+cloc os
+-------------------------------------------------------------------------------
+Language                     files          blank        comment           code
+-------------------------------------------------------------------------------
+Rust                            14             62             21            435
+Assembly                         3              9             16            106
+make                             1             12              4             36
+TOML                             1              2              1              9
+-------------------------------------------------------------------------------
+SUM:                            19             85             42            586
+-------------------------------------------------------------------------------
+
+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter2/2application.html b/chapter2/2application.html new file mode 100644 index 0000000..ba4cf8d --- /dev/null +++ b/chapter2/2application.html @@ -0,0 +1,586 @@ + + + + + + + + 实现应用程序 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

实现应用程序

+
+
+
+

注解

+

拓展阅读:RISC-V 特权级机制

+
+
+

应用程序设计

+
+

注意

+

用户库看起来很复杂,它预留了直到 ch7 内核才能实现的系统调用接口,console 模块还实现了输出缓存区。它们不是为本章准备的,你只需关注本节提到的部分即可。

+
+

应用程序、用户库(包括入口函数、初始化函数、I/O函数和系统调用接口等多个rs文件组成)放在项目根目录的 user 目录下:

+
    +
  • user/src/bin/*.rs:各个应用程序

  • +
  • user/src/*.rs:用户库(包括入口函数、初始化函数、I/O函数和系统调用接口等)

  • +
  • user/src/linker.ld:应用程序的内存布局说明

  • +
+
+

项目结构

+

user/src/bin 里面有多个文件,其中三个是:

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

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

  • +
  • power:不断在计算操作和打印字符串操作之间切换

  • +
+

批处理系统会按照文件名顺序加载并运行它们。

+

每个应用程序的实现都在对应的单个文件中。打开 hello_world.rs,能看到一个 main 函数,还有外部库引用:

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

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

+

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

+
1#[no_mangle]
+2#[link_section = ".text.entry"]
+3pub extern "C" fn _start() -> ! {
+4    clear_bss();
+5    exit(main());
+6}
+
+
+

第 2 行使用 link_section 宏将 _start 函数编译后的汇编代码放在名为 .text.entry 的代码段中, +方便用户库链接脚本将它作为用户程序的入口。

+

而从第 4 行开始,我们手动清零 .bss 段,然后调用 main 函数得到一个类型为 i32 的返回值, +最后,调用用户库提供的 exit 接口退出,并将返回值告知批处理系统。

+

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

+
1#![feature(linkage)]    // 启用弱链接特性
+2
+3#[linkage = "weak"]
+4#[no_mangle]
+5fn main() -> i32 {
+6    panic!("Cannot find main!");
+7}
+
+
+

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

+
+
+

内存布局

+

我们使用链接脚本 user/src/linker.ld 规定用户程序的内存布局:

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

  • +
  • _start 所在的 .text.entry 放在整个程序的开头 0x80400000; +批处理系统在加载应用后,跳转到 0x80400000,就进入了用户库的 _start 函数;

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

  • +
+

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

+
+
+

系统调用

+

在子模块 syscall 中我们来通过 ecall 调用批处理系统提供的接口, +由于应用程序运行在用户态(即 U 模式), ecall 指令会触发名为 Environment call from U-mode 的异常, +并 Trap 进入 S 模式执行批处理系统针对这个异常特别提供的服务程序。 +这个接口被称为 ABI 或者系统调用。 +现在我们不关心 S 态的批处理系统如何提供应用程序所需的功能,只考虑如何使用它。

+

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

+
+
第二章新增系统调用
+
/// 功能:将内存中缓冲区中的数据写入文件。
+/// 参数:`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) -> !;
+
+
+
+

实际调用时,我们要按照 RISC-V 调用规范,在合适的寄存器中放置参数, +然后执行 ecall 指令触发 Trap。当 Trap 结束,回到 U 模式后, +用户程序会从 ecall 的下一条指令继续执行,同时在合适的寄存器中读取返回值。

+
+

注解

+

RISC-V 寄存器编号从 0~31 ,表示为 x0~x31 。 其中: +- x10~x17 : 对应 a0~a7 +- x1 :对应 ra

+
+

约定寄存器 a0~a6 保存系统调用的参数, a0 保存系统调用的返回值, +寄存器 a7 用来传递 syscall ID。 +这超出了 Rust 语言的表达能力,我们需要内嵌汇编来完成参数/返回值绑定和 ecall 指令的插入:

+
 1// user/src/syscall.rs
+ 2
+ 3fn syscall(id: usize, args: [usize; 3]) -> isize {
+ 4   let mut ret: isize;
+ 5   unsafe {
+ 6       core::arch::asm!(
+ 7           "ecall",
+ 8           inlateout("x10") args[0] => ret,
+ 9           in("x11") args[1],
+10           in("x12") args[2],
+11           in("x17") id
+12       );
+13   }
+14   ret
+15}
+
+
+

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

+

第 6 行开始,我们使用 Rust 提供的 asm! 宏在代码中内嵌汇编。 +Rust 编译器无法判定汇编代码的安全性,所以我们需要将其包裹在 unsafe 块中。

+

简而言之,这条汇编代码的执行结果是以寄存器 a0~a2 来保存系统调用的参数,以及寄存器 a7 保存 syscall ID, +返回值通过寄存器 a0 传递给局部变量 ret

+

这段汇编代码与第一章中出现过的内嵌汇编很像,读者可以查看 os/src/sbi.rs

+
+

注解

+

可以查看 Inline assembly 了解 asm 宏。

+
+

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

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

我们将上述两个系统调用在用户库 user_lib 中进一步封装,像标准库一样:

+
1// user/src/lib.rs
+2use syscall::*;
+3
+4pub fn write(fd: usize, buf: &[u8]) -> isize { sys_write(fd, buf) }
+5pub fn exit(exit_code: i32) -> isize { sys_exit(exit_code) }
+
+
+

console 子模块中,借助 write,我们为应用程序实现了 println! 宏。 +传入到 writefd 参数设置为 1,代表标准输出 STDOUT,暂时不用考虑其他的 fd 选取情况。

+
+
+
+

编译生成应用程序二进制码

+

简要介绍一下应用程序的构建,在 user 目录下 make build

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

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

  4. +
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter2/3batch-system.html b/chapter2/3batch-system.html new file mode 100644 index 0000000..1c8154d --- /dev/null +++ b/chapter2/3batch-system.html @@ -0,0 +1,543 @@ + + + + + + + + 实现批处理操作系统 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

实现批处理操作系统

+
+
+
+

将应用程序链接到内核

+

在本章中,我们要把应用程序的二进制镜像文件作为数据段链接到内核里, +内核需要知道应用程序的数量和它们的位置。

+

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

+
core::arch::global_asm!(include_str!("link_app.S"));
+
+
+

这里我们引入了一段汇编代码 link_app.S ,它是在 make run 构建操作系统时自动生成的,里面的内容大致如下:

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

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

+

这个文件是在 cargo build 时,由脚本 os/build.rs 控制生成的。

+
+
+

找到并加载应用程序二进制码

+

我们在 osbatch 子模块中实现一个应用管理器 AppManager ,结构体定义如下:

+
struct AppManager {
+    num_app: usize,
+    current_app: usize,
+    app_start: [usize; MAX_APP_NUM + 1],
+}
+
+
+

初始化 AppManager 的全局实例:

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

初始化的逻辑很简单,就是找到 link_app.S 中提供的符号 _num_app ,并从这里开始解析出应用数量以及各个应用的开头地址。 +用容器 UPSafeCell 包裹 AppManager 是为了防止全局对象 APP_MANAGER 被重复获取。

+
+

注解

+

UPSafeCell 实现在 sync 模块中,调用 exclusive_access 方法能获取其内部对象的可变引用, +如果程序运行中同时存在多个这样的引用,会触发 already borrowed: BorrowMutError

+

UPSafeCell 既提供了内部可变性,又在单核情境下防止了内部对象被重复借用,我们将在后文中多次见到它。

+
+

这里使用了外部库 lazy_static 提供的 lazy_static! 宏。

+

lazy_static! 宏提供了全局变量的运行时初始化功能。一般情况下,全局变量必须在编译期设置初始值, +但是有些全局变量的初始化依赖于运行期间才能得到的数据。 +如这里我们借助 lazy_static! 声明了一个 AppManager 结构的名为 APP_MANAGER 的全局实例, +只有在它第一次被使用到的时候才会进行实际的初始化工作。

+

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

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

这个方法负责将参数 app_id 对应的应用程序的二进制镜像加载到物理内存以 0x80400000 起始的位置, +这个位置是批处理操作系统和应用程序之间约定的常数地址。 +我们将从这里开始的一块内存清空,然后找到待加载应用二进制镜像的位置,并将它复制到正确的位置。

+

清空内存前,我们插入了一条奇怪的汇编指令 fence.i ,它是用来清理 i-cache 的。 +我们知道, 缓存又分成 数据缓存 (d-cache) 和 指令缓存 (i-cache) 两部分,分别在 CPU 访存和取指的时候使用。 +通常情况下, CPU 会认为程序的代码段不会发生变化,因此 i-cache 是一种只读缓存。 +但在这里,我们会修改会被 CPU 取指的内存区域,使得 i-cache 中含有与内存不一致的内容, +必须使用 fence.i 指令手动清空 i-cache ,让里面所有的内容全部失效, +才能够保证程序执行正确性。

+
+

警告

+

模拟器与真机的不同之处

+

在 Qemu 模拟器上,即使不加刷新 i-cache 的指令,大概率也能正常运行,但在物理计算机上不是这样。

+
+

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

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

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

  • +
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter2/4trap-handling.html b/chapter2/4trap-handling.html new file mode 100644 index 0000000..ad9faa2 --- /dev/null +++ b/chapter2/4trap-handling.html @@ -0,0 +1,829 @@ + + + + + + + + 实现特权级的切换 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

实现特权级的切换

+
+
+
+

RISC-V特权级切换

+
+

特权级切换的起因

+

批处理操作系统为了建立好应用程序的执行环境,需要在执行应用程序前进行一些初始化工作, +并监控应用程序的执行,具体体现在:

+
    +
  • 启动应用程序时,需要初始化应用程序的用户态上下文,并能切换到用户态执行应用程序;

  • +
  • 应用程序发起系统调用后,需要切换到批处理操作系统中进行处理;

  • +
  • 应用程序执行出错时,批处理操作系统要杀死该应用并加载运行下一个应用;

  • +
  • 应用程序执行结束时,批处理操作系统要加载运行下一个应用。

  • +
+

这些处理都涉及到特权级切换,因此都需要硬件和操作系统协同提供的特权级切换机制。

+
+
+

特权级切换相关的控制状态寄存器

+

本章中我们仅考虑当 CPU 在 U 特权级运行用户程序的时候触发 Trap, +并切换到 S 特权级的批处理操作系统进行处理。

+
+ ++++ + + + + + + + + + + + + + + + + + + + + + + +
进入 S 特权级 Trap 的相关 CSR

CSR 名

该 CSR 与 Trap 相关的功能

sstatus

SPP 等字段给出 Trap 发生之前 CPU 处在哪个特权级(S/U)等信息

sepc

当 Trap 是一个异常的时候,记录 Trap 发生之前执行的最后一条指令的地址

scause

描述 Trap 的原因

stval

给出 Trap 附加信息

stvec

控制 Trap 处理代码的入口地址

+

特权级切换的具体过程一部分由硬件直接完成,另一部分则需要由操作系统来实现。

+
+
+
+

特权级切换的硬件控制机制

+

当 CPU 执行完一条指令并准备从用户特权级 陷入( Trap )到 S 特权级的时候,硬件会自动完成如下这些事情:

+
    +
  • sstatusSPP 字段会被修改为 CPU 当前的特权级(U/S)。

  • +
  • sepc 会被修改为 Trap 处理完成后默认会执行的下一条指令的地址。

  • +
  • scause/stval 分别会被修改成这次 Trap 的原因以及相关的附加信息。

  • +
  • CPU 会跳转到 stvec 所设置的 Trap 处理入口地址,并将当前特权级设置为 S ,然后从Trap 处理入口地址处开始执行。

  • +
+
+

注解

+

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 模式,

+
+

而当 CPU 完成 Trap 处理准备返回的时候,需要通过一条 S 特权级的特权指令 sret 来完成,这一条指令具体完成以下功能:

+
    +
  • CPU 会将当前的特权级按照 sstatusSPP 字段设置为 U 或者 S ;

  • +
  • CPU 会跳转到 sepc 寄存器指向的那条指令,然后继续执行。

  • +
+
+
+

用户栈与内核栈

+

在 Trap 触发的一瞬间, CPU 会切换到 S 特权级并跳转到 stvec 所指示的位置。 +但是在正式进入 S 特权级的 Trap 处理之前,我们必须保存原控制流的寄存器状态,这一般通过栈来完成。 +但我们需要用专门为操作系统准备的内核栈,而不是应用程序运行时用到的用户栈。

+

我们声明两个类型 KernelStackUserStack 分别表示用户栈和内核栈,它们都只是字节数组的简单包装:

+

两个栈以全局变量的形式实例化在批处理操作系统的 .bss 段中。

+

我们为两个类型实现了 get_sp 方法来获取栈顶地址。由于在 RISC-V 中栈是向下增长的, +我们只需返回包裹的数组的结尾地址,以用户栈类型 UserStack 为例:

+
1impl UserStack {
+2    fn get_sp(&self) -> usize {
+3        self.data.as_ptr() as usize + USER_STACK_SIZE
+4    }
+5}
+
+
+

换栈是非常简单的,只需将 sp 寄存器的值修改为 get_sp 的返回值即可。

+

接下来是 Trap 上下文,即在 Trap 发生时需要保存的物理资源内容,定义如下:

+
1// os/src/trap/context.rs
+2
+3#[repr(C)]
+4pub struct TrapContext {
+5    pub x: [usize; 32],
+6    pub sstatus: Sstatus,
+7    pub sepc: usize,
+8}
+
+
+

可以看到里面包含所有的通用寄存器 x0~x31 ,还有 sstatussepc

+
    +
  • 对于通用寄存器而言,两条控制流(应用程序控制流和内核控制流)运行在不同的特权级,所属的软件也可能由不同的编程语言编写,虽然在 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 上下文的保存与恢复

+

首先是具体实现 Trap 上下文保存和恢复的汇编代码。

+

在批处理操作系统初始化时,我们需要修改 stvec 寄存器来指向正确的 Trap 处理入口点。

+
 1// os/src/trap/mod.rs
+ 2
+ 3core::arch::global_asm!(include_str!("trap.S"));
+ 4
+ 5pub fn init() {
+ 6    extern "C" { fn __alltraps(); }
+ 7    unsafe {
+ 8        stvec::write(__alltraps as usize, TrapMode::Direct);
+ 9    }
+10}
+
+
+

这里我们引入了一个外部符号 __alltraps ,并将 stvec 设置为 Direct 模式指向它的地址。我们在 os/src/trap/trap.S +中实现 Trap 上下文保存/恢复的汇编代码,分别用外部符号 __alltraps__restore 标记为函数,并通过 global_asm! 宏将 trap.S 这段汇编代码插入进来。

+

Trap 处理的总体流程如下:首先通过 __alltraps 将 Trap 上下文保存在内核栈上,然后跳转到使用 Rust 编写的 trap_handler 函数 +完成 Trap 分发及处理。当 trap_handler 返回之后,使用 __restore 从保存在内核栈上的 Trap 上下文恢复寄存器。最后通过一条 +sret 指令回到应用程序执行。

+

首先是保存 Trap 上下文的 __alltraps 的实现:

+
 1# os/src/trap/trap.S
+ 2
+ 3.macro SAVE_GP n
+ 4    sd x\n, \n*8(sp)
+ 5.endm
+ 6
+ 7.align 2
+ 8__alltraps:
+ 9    csrrw sp, sscratch, sp
+10    # now sp->kernel stack, sscratch->user stack
+11    # allocate a TrapContext on kernel stack
+12    addi sp, sp, -34*8
+13    # save general-purpose registers
+14    sd x1, 1*8(sp)
+15    # skip sp(x2), we will save it later
+16    sd x3, 3*8(sp)
+17    # skip tp(x4), application does not use it
+18    # save x5~x31
+19    .set n, 5
+20    .rept 27
+21        SAVE_GP %n
+22        .set n, n+1
+23    .endr
+24    # we can use t0/t1/t2 freely, because they were saved on kernel stack
+25    csrr t0, sstatus
+26    csrr t1, sepc
+27    sd t0, 32*8(sp)
+28    sd t1, 33*8(sp)
+29    # read user stack from sscratch and save it on the kernel stack
+30    csrr t2, sscratch
+31    sd t2, 2*8(sp)
+32    # set input argument of trap_handler(cx: &mut TrapContext)
+33    mv a0, sp
+34    call trap_handler
+
+
+
    +
  • 第 7 行我们使用 .align__alltraps 的地址 4 字节对齐,这是 RISC-V 特权级规范的要求;

  • +
  • 第 9 行的 csrrw 原型是 \(\text{csrrw rd, csr, rs}\) 可以将 CSR 当前的值读到通用寄存器 \(\text{rd}\) 中,然后将 +通用寄存器 \(\text{rs}\) 的值写入该 CSR 。因此这里起到的是交换 sscratch 和 sp 的效果。在这一行之前 sp 指向用户栈, sscratch +指向内核栈(原因稍后说明),现在 sp 指向内核栈, sscratch 指向用户栈。

  • +
  • 第 12 行,我们准备在内核栈上保存 Trap 上下文,于是预先分配 \(34\times 8\) 字节的栈帧,这里改动的是 sp ,说明确实是在内核栈上。

  • +
  • 第 13~24 行,保存 Trap 上下文的通用寄存器 x0~x31,跳过 x0 和 tp(x4),原因之前已经说明。我们在这里也不保存 sp(x2),因为它在第 9 行 +后指向的是内核栈。用户栈的栈指针保存在 sscratch 中,必须通过 csrr 指令读到通用寄存器中后才能使用,因此我们先考虑保存其它通用寄存器,腾出空间。

    +

    我们要基于 sp 来找到每个寄存器应该被保存到的正确的位置。实际上,在栈帧分配之后,我们可用于保存 Trap 上下文的地址区间为 \([\text{sp},\text{sp}+8\times34)\) , +按照 TrapContext 结构体的内存布局,基于内核栈的位置(sp所指地址)来从低地址到高地址分别按顺序放置 x0~x31这些通用寄存器,最后是 sstatus 和 sepc 。因此通用寄存器 xn +应该被保存在地址区间 \([\text{sp}+8n,\text{sp}+8(n+1))\)

    +

    为了简化代码,x5~x31 这 27 个通用寄存器我们通过类似循环的 .rept 每次使用 SAVE_GP 宏来保存,其实质是相同的。注意我们需要在 +trap.S 开头加上 .altmacro 才能正常使用 .rept 命令。

    +
  • +
  • 第 25~28 行,我们将 CSR sstatus 和 sepc 的值分别读到寄存器 t0 和 t1 中然后保存到内核栈对应的位置上。指令 +\(\text{csrr rd, csr}\) 的功能就是将 CSR 的值读到寄存器 \(\text{rd}\) 中。这里我们不用担心 t0 和 t1 被覆盖, +因为它们刚刚已经被保存了。

  • +
  • 第 30~31 行专门处理 sp 的问题。首先将 sscratch 的值读到寄存器 t2 并保存到内核栈上,注意: sscratch 的值是进入 Trap 之前的 sp 的值,指向 +用户栈。而现在的 sp 则指向内核栈。

  • +
  • 第 33 行令 \(\text{a}_0\leftarrow\text{sp}\),让寄存器 a0 指向内核栈的栈指针也就是我们刚刚保存的 Trap 上下文的地址, +这是由于我们接下来要调用 trap_handler 进行 Trap 处理,它的第一个参数 cx 由调用规范要从 a0 中获取。而 Trap 处理函数 +trap_handler 需要 Trap 上下文的原因在于:它需要知道其中某些寄存器的值,比如在系统调用的时候应用程序传过来的 syscall ID 和 +对应参数。我们不能直接使用这些寄存器现在的值,因为它们可能已经被修改了,因此要去内核栈上找已经被保存下来的值。

  • +
+
+

注解

+

CSR 相关原子指令

+

RISC-V 中读写 CSR 的指令是一类能不会被打断地完成多个读写操作的指令。这种不会被打断地完成多个操作的指令被称为 原子指令 (Atomic Instruction)。这里的 原子 的含义是“不可分割的最小个体”,也就是说指令的多个操作要么都不完成,要么全部完成,而不会处于某种中间状态。

+
+

trap_handler 返回之后会从调用 trap_handler 的下一条指令开始执行,也就是从栈上的 Trap 上下文恢复的 __restore

+
 1.macro LOAD_GP n
+ 2    ld x\n, \n*8(sp)
+ 3.endm
+ 4
+ 5__restore:
+ 6    # case1: start running app by __restore
+ 7    # case2: back to U after handling trap
+ 8    mv sp, a0
+ 9    # now sp->kernel stack(after allocated), sscratch->user stack
+10    # restore sstatus/sepc
+11    ld t0, 32*8(sp)
+12    ld t1, 33*8(sp)
+13    ld t2, 2*8(sp)
+14    csrw sstatus, t0
+15    csrw sepc, t1
+16    csrw sscratch, t2
+17    # restore general-purpuse registers except sp/tp
+18    ld x1, 1*8(sp)
+19    ld x3, 3*8(sp)
+20    .set n, 5
+21    .rept 27
+22        LOAD_GP %n
+23        .set n, n+1
+24    .endr
+25    # release TrapContext on kernel stack
+26    addi sp, sp, 34*8
+27    # now sp->kernel stack, sscratch->user stack
+28    csrrw sp, sscratch, sp
+29    sret
+
+
+
    +
  • 第 8 行比较奇怪,我们暂且不管,假设它从未发生,那么 sp 仍然指向内核栈的栈顶。

  • +
  • 第 11~24 行负责从内核栈顶的 Trap 上下文恢复通用寄存器和 CSR 。注意我们要先恢复 CSR 再恢复通用寄存器,这样我们使用的三个临时寄存器 +才能被正确恢复。

  • +
  • 在第 26 行之前,sp 指向保存了 Trap 上下文之后的内核栈栈顶, sscratch 指向用户栈栈顶。我们在第 26 行在内核栈上回收 Trap 上下文所 +占用的内存,回归进入 Trap 之前的内核栈栈顶。第 27 行,再次交换 sscratch 和 sp,现在 sp 重新指向用户栈栈顶,sscratch 也依然保存 +进入 Trap 之前的状态并指向内核栈栈顶。

  • +
  • 在应用程序控制流状态被还原之后,第 28 行我们使用 sret 指令回到 U 特权级继续运行应用程序控制流。

  • +
+
+
+

Trap 分发与处理

+

Trap 在使用 Rust 实现的 trap_handler 函数中完成分发和处理:

+
 1// os/src/trap/mod.rs
+ 2
+ 3#[no_mangle]
+ 4pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
+ 5    let scause = scause::read();
+ 6    let stval = stval::read();
+ 7    match scause.cause() {
+ 8        Trap::Exception(Exception::UserEnvCall) => {
+ 9            cx.sepc += 4;
+10            cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize;
+11        }
+12        Trap::Exception(Exception::StoreFault) |
+13        Trap::Exception(Exception::StorePageFault) => {
+14            println!("[kernel] PageFault in application, core dumped.");
+15            run_next_app();
+16        }
+17        Trap::Exception(Exception::IllegalInstruction) => {
+18            println!("[kernel] IllegalInstruction in application, core dumped.");
+19            run_next_app();
+20        }
+21        _ => {
+22            panic!("Unsupported trap {:?}, stval = {:#x}!", scause.cause(), stval);
+23        }
+24    }
+25    cx
+26}
+
+
+
    +
  • 第 4 行声明返回值为 &mut TrapContext 并在第 25 行实际将传入的 cx 原样返回,因此在 __restore 的时候 a0 寄存器在调用 +trap_handler 前后并没有发生变化,仍然指向分配 Trap 上下文之后的内核栈栈顶,和此时 sp 的值相同,我们 \(\text{sp}\leftarrow\text{a}_0\) +并不会有问题;

  • +
  • 第 7 行根据 scause 寄存器所保存的 Trap 的原因进行分发处理。这里我们无需手动操作这些 CSR ,而是使用 Rust 第三方库 riscv 。

  • +
  • 第 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 分发到具体的处理函数:

+
1// os/src/syscall/mod.rs
+2
+3pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
+4    match syscall_id {
+5        SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]),
+6        SYSCALL_EXIT => sys_exit(args[0] as i32),
+7        _ => panic!("Unsupported syscall_id: {}", syscall_id),
+8    }
+9}
+
+
+

这里我们会将传进来的参数 args 转化成能够被具体的系统调用处理函数接受的类型。它们的实现都非常简单:

+
 1// os/src/syscall/fs.rs
+ 2
+ 3const FD_STDOUT: usize = 1;
+ 4
+ 5pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize {
+ 6    match fd {
+ 7        FD_STDOUT => {
+ 8            let slice = unsafe { core::slice::from_raw_parts(buf, len) };
+ 9            let str = core::str::from_utf8(slice).unwrap();
+10            print!("{}", str);
+11            len as isize
+12        },
+13        _ => {
+14            panic!("Unsupported fd in sys_write!");
+15        }
+16    }
+17}
+18
+19// os/src/syscall/process.rs
+20
+21pub fn sys_exit(xstate: i32) -> ! {
+22    println!("[kernel] Application exited with code {}", xstate);
+23    run_next_app()
+24}
+
+
+
    +
  • sys_write 我们将传入的位于应用程序内的缓冲区的开始地址和长度转化为一个字符串 &str ,然后使用批处理操作系统已经实现的 print! +宏打印出来。这里我们并没有检查传入参数的安全性,存在安全隐患。

  • +
  • sys_exit 打印退出的应用程序的返回值并同样调用 run_next_app 切换到下一个应用程序。

  • +
+
+
+
+

执行应用程序

+

当批处理操作系统初始化完成,或者是某个应用程序运行结束或出错的时候,我们要调用 run_next_app 函数切换到下一个应用程序。此时 CPU 运行在 +S 特权级,而它希望能够切换到 U 特权级。在 RISC-V 架构中,唯一一种能够使得 CPU 特权级下降的方法就是通过 Trap 返回系列指令,比如 +sret 。事实上,在运行应用程序之前要完成如下这些工作:

+
    +
  • 跳转到应用程序入口点 0x80400000

  • +
  • 将使用的栈切换到用户栈;

  • +
  • __alltraps 时我们要求 sscratch 指向内核栈,这个也需要在此时完成;

  • +
  • 从 S 特权级切换到 U 特权级。

  • +
+

它们可以通过复用 __restore 的代码来更容易的实现上述工作。我们只需要在内核栈上压入一个为启动应用程序而特殊构造的 Trap 上下文,再通过 __restore 函数,就能 +让这些寄存器到达启动应用程序所需要的上下文状态。

+
 1// os/src/trap/context.rs
+ 2
+ 3impl TrapContext {
+ 4    pub fn set_sp(&mut self, sp: usize) { self.x[2] = sp; }
+ 5    pub fn app_init_context(entry: usize, sp: usize) -> Self {
+ 6        let mut sstatus = sstatus::read();
+ 7        sstatus.set_spp(SPP::User);
+ 8        let mut cx = Self {
+ 9            x: [0; 32],
+10            sstatus,
+11            sepc: entry,
+12        };
+13        cx.set_sp(sp);
+14        cx
+15    }
+16}
+
+
+

TrapContext 实现 app_init_context 方法,修改其中的 sepc 寄存器为应用程序入口点 entry, sp 寄存器为我们设定的 +一个栈指针,并将 sstatus 寄存器的 SPP 字段设置为 User 。

+

run_next_app 函数中我们能够看到:

+
 1// os/src/batch.rs
+ 2
+ 3pub fn run_next_app() -> ! {
+ 4    let mut app_manager = APP_MANAGER.exclusive_access();
+ 5    let current_app = app_manager.get_current_app();
+ 6    unsafe {
+ 7        app_manager.load_app(current_app);
+ 8    }
+ 9    app_manager.move_to_next_app();
+10    drop(app_manager);
+11    // before this we have to drop local variables related to resources manually
+12    // and release the resources
+13    extern "C" {
+14        fn __restore(cx_addr: usize);
+15    }
+16    unsafe {
+17        __restore(KERNEL_STACK.push_context(TrapContext::app_init_context(
+18            APP_BASE_ADDRESS,
+19            USER_STACK.get_sp(),
+20        )) as *const _ as usize);
+21    }
+22    panic!("Unreachable in batch::run_current_app!");
+23}
+
+
+

__restore 所做的事情是在内核栈上压入一个 Trap 上下文,其 sepc 是应用程序入口地址 0x80400000 ,其 sp 寄存器指向用户栈,其 sstatus +的 SPP 字段被设置为 User 。 +push_context 的返回值是内核栈压入 Trap 上下文之后的栈顶,它会被作为 __restore 的参数( +回看 __restore 代码 ,这时我们可以理解为何 __restore 函数的起始部分会完成 +\(\text{sp}\leftarrow\text{a}_0\) ),这使得在 __restore 函数中 sp 仍然可以指向内核栈的栈顶。这之后,就和执行一次普通的 +__restore 函数调用一样了。

+
+

注解

+

有兴趣的读者可以思考: sscratch 是何时被设置为内核栈顶的?

+
+
+
+ +
+ +
+ +
+
+ + + + + + + + + \ No newline at end of file diff --git a/chapter2/5exercise.html b/chapter2/5exercise.html new file mode 100644 index 0000000..15d5854 --- /dev/null +++ b/chapter2/5exercise.html @@ -0,0 +1,421 @@ + + + + + + + + chapter2练习(已废弃) - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

chapter2练习(已废弃)

+
+
+
+

编程练习

+
+

简单安全检查

+
+
+

实验要求

+
+
+

实验约定

+
+
+

实验检查

+
+
+
+

简答题

+
+
+

报告要求

+
    +
  • 简单总结你实现的功能(200字以内,不要贴代码)。

  • +
  • 完成问答题。

  • +
  • (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。

  • +
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter2/index.html b/chapter2/index.html new file mode 100644 index 0000000..b13a949 --- /dev/null +++ b/chapter2/index.html @@ -0,0 +1,425 @@ + + + + + + + + 第二章:批处理系统 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ + +
+ + + + + + + + \ No newline at end of file diff --git a/chapter3/0intro.html b/chapter3/0intro.html new file mode 100644 index 0000000..bb6f404 --- /dev/null +++ b/chapter3/0intro.html @@ -0,0 +1,593 @@ + + + + + + + + 引言 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

引言

+
+

本章导读

+

本章的目标是实现分时多任务系统,它能并发地执行多个用户程序,并调度这些程序。为此需要实现

+
    +
  • 一次性加载所有用户程序,减少任务切换开销;

  • +
  • 支持任务切换机制,保存切换前后程序上下文;

  • +
  • 支持程序主动放弃处理器,实现 yield 系统调用;

  • +
  • 以时间片轮转算法调度用户程序,实现资源的时分复用。

  • +
+
+
+

实践体验

+
+

注解

+

基于github classroom的开发方式

+

基于github classroom,可方便建立开发用的git repository,并可基于github的 codespace (在线版ubuntu +vscode)在线开发使用。整个开发环境仅仅需要一个网络浏览器。

+
    +
  1. 在网络浏览器中用自己的 github id 登录 github.com

  2. +
  3. 接收 第一个实验(os3)的github classroom在线邀请 ,根据提示一路选择OK即可。

  4. +
  5. 完成第二步后,你的第一个实验的 github repository 会被自动建立好,点击此github repository的链接,就可看到你要完成的第一个实验了。

  6. +
  7. 在你的第一个实验的网页的中上部可以看到一个醒目的 code 绿色按钮,点击后,可以进一步看到 codespace 标签和醒目的 create codesapce on main 绿色按钮。请点击这个绿色按钮,就可以进入到在线的ubuntu +vscode环境中

  8. +
  9. 再按照下面的环境安装提示在vscode的 console 中安装配置开发环境:rustc,qemu等工具。

  10. +
  11. 在vscode的 console 中执行 make setupclassroom_test3 (该命令仅执行一次)配置githubclassroom 自动评分功能。

  12. +
  13. 然后就可以基于在线vscode进行开发、运行、提交等完整的实验过程了。

  14. +
+

上述的3,4,5步不是必须的,你也可以线下本地开发。

+
+
$ git clone https://github.com/LearningOS/rust-based-os-comp2022.git
+$ cd rust-based-os-comp2022/
+$ make setupclassroom_test3  //注意:这一步很重要,是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次,以后一般就不用执行了,除非 .github/workflows/classroom.yml发生了变化。
+
+
+

在 qemu 模拟器上运行 lab1(os3)参考框架:

+
$ cd os3-ref
+$ make run
+
+
+

运行代码,看到用户程序交替输出信息:

+
[rustsbi] RustSBI version 0.2.0-alpha.4
+.______       __    __      _______.___________.  _______..______   __
+|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
+|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
+|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
+|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
+| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|
+
+[rustsbi] Implementation: RustSBI-QEMU Version 0.0.1
+[rustsbi-dtb] Hart count: cluster0 with 1 cores
+[rustsbi] misa: RV64ACDFIMSU
+[rustsbi] mideleg: ssoft, stimer, sext (0x222)
+[rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab)
+[rustsbi] pmp0: 0x80000000 ..= 0x800fffff (rwx)
+[rustsbi] pmp1: 0x80000000 ..= 0x807fffff (rwx)
+[rustsbi] pmp2: 0x0 ..= 0xffffffffffffff (---)
+[rustsbi] enter supervisor 0x80200000
+[kernel] Hello, world!
+power_3 [10000/200000]
+power_3 [20000/200000]
+power_3 [30000/200000]
+power_3 [40000/200000]
+power_3 [50000/200000]
+power_3 [60000/200000]
+power_3 [70000/200000]
+power_3 [80000/200000]
+power_3 [90000/200000]
+power_3 [100000/200000]
+power_3 [110000/200000]
+power_3 [120000/200000]
+power_3 [130000/200000]
+power_3 [140000/200000]
+power_3 [150000/200000]
+power_3 [160000/200000]
+power_3 [170000/200000]
+power_3 [180000/200000]
+power_3 [190000/200000]
+power_3 [200000/200000]
+3^200000 = 871008973(MOD 998244353)
+Test power_3 OK!
+power_5 [10000/140000]
+power_5 [20000/140000]
+power_5 [30000/140000]
+power_5 [40000/140000]
+power_5 [50000/140000]
+power_5 [60000/140000]
+power_7 [10000/160000]
+power_7 [20000/160000]
+power_7 [30000/160000]
+power_7 [40000/160000]
+power_7 [50000/160000]
+power_7 [60000/160000]
+power_7 [70000/160000]
+power_7 [80000/160000]
+power_7 [90000/160000]
+power_7 [100000/160000]
+power_7 [110000/160000]
+power_7 [120000/160000]
+power_7 [130000/160000]
+power_7 [140000/160000]
+power_7 [150000/160000]
+power_7 [160000/160000]
+7^160000 = 667897727(MOD 998244353)
+Test power_7 OK!
+get_time OK! 42
+current time_msec = 42
+AAAAAAAAAA [1/5]
+BBBBBBBBBB [1/5]
+CCCCCCCCCC [1/5]
+power_5 [70000/140000]
+AAAAAAAAAA [2/5]
+BBBBBBBBBB [2/5]
+CCCCCCCCCC [2/5]
+power_5 [80000/140000]
+power_5 [90000/140000]
+power_5 [100000/140000]
+power_5 [110000/140000]
+power_5 [120000/140000]
+power_5 [130000/140000]
+power_5 [140000/140000]
+5^140000 = 386471875(MOD 998244353)
+Test power_5 OK!
+AAAAAAAAAA [3/5]
+BBBBBBBBBB [3/5]
+CCCCCCCCCC [3/5]
+AAAAAAAAAA [4/5]
+BBBBBBBBBB [4/5]
+CCCCCCCCCC [4/5]
+AAAAAAAAAA [5/5]
+BBBBBBBBBB [5/5]
+CCCCCCCCCC [5/5]
+Test write A OK!
+Test write B OK!
+Test write C OK!
+time_msec = 143 after sleeping 100 ticks, delta = 101ms!
+Test sleep1 passed!
+Test sleep OK!
+Panicked at src/task/mod.rs:98 All applications completed!
+
+
+
+
+

lab1(os3)参考框架:

+
── os3-ref
+   ├── build.rs
+   ├── Cargo.toml
+   ├── Makefile
+   └── src
+       ├── batch.rs(移除:功能分别拆分到 loader 和 task 两个子模块)
+       ├── config.rs(新增:保存内核的一些配置)
+       ├── console.rs
+       ├── logging.rs
+       ├── sync
+       ├── entry.asm
+       ├── lang_items.rs
+       ├── link_app.S
+       ├── linker.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
+
+cloc os
+-------------------------------------------------------------------------------
+Language                     files          blank        comment           code
+-------------------------------------------------------------------------------
+Rust                            21             87             20            627
+Assembly                         4             12             22            144
+make                             1             11              4             36
+TOML                             1              2              1             10
+-------------------------------------------------------------------------------
+SUM:                            27            112             47            817
+-------------------------------------------------------------------------------
+
+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter3/1multi-loader.html b/chapter3/1multi-loader.html new file mode 100644 index 0000000..4a0b87d --- /dev/null +++ b/chapter3/1multi-loader.html @@ -0,0 +1,463 @@ + + + + + + + + 多道程序放置与加载 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

多道程序放置与加载

+
+

多道程序放置

+

在第二章中,内核让所有应用都共享同一个固定的起始地址。 +正因如此,内存中同时最多只能驻留一个应用,

+

要一次加载运行多个程序,就要求每个用户程序被内核加载到内存中的起始地址都不同。 +为此,我们编写脚本 user/build.py 为每个应用定制各自的起始地址。 +它的思路很简单,对于每一个应用程序,使用 cargo rustc 单独编译, +用 -Clink-args=-Ttext=xxxx 选项指定链接时 .text 段的地址为 0x80400000 + app_id * 0x20000

+
+

注解

+

qemu 预留的内存空间是有限的,如果加载的程序过多,程序地址超出内存空间,可能出现 core dumped.

+
+
+
+

多道程序加载

+

在第二章中负责应用加载和执行的子模块 batch 被拆分为 loadertask , +前者负责启动时加载应用程序,后者负责切换和调度。

+

其中, loader 模块的 load_apps 函数负责将所有用户程序在内核初始化的时一并加载进内存。

+
 1 // os/src/loader.rs
+ 2
+ 3 pub fn load_apps() {
+ 4     extern "C" {
+ 5         fn _num_app();
+ 6     }
+ 7     let num_app_ptr = _num_app as usize as *const usize;
+ 8     let num_app = get_num_app();
+ 9     let app_start = unsafe { core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1) };
+10     // clear i-cache first
+11     unsafe {
+12         core::arch::asm!("fence.i");
+13     }
+14     // load apps
+15     for i in 0..num_app {
+16         let base_i = get_base_i(i);
+17         // clear region
+18         (base_i..base_i + APP_SIZE_LIMIT)
+19             .for_each(|addr| unsafe { (addr as *mut u8).write_volatile(0) });
+20         // load app from data section to memory
+21         let src = unsafe {
+22             core::slice::from_raw_parts(app_start[i] as *const u8, app_start[i + 1] - app_start[i])
+23         };
+24         let dst = unsafe { core::slice::from_raw_parts_mut(base_i as *mut u8, src.len()) };
+25         dst.copy_from_slice(src);
+26     }
+27 }
+
+
+

\(i\) 个应用被加载到以物理地址 base_i 开头的一段物理内存上,而 base_i 的计算方式如下:

+
1 // os/src/loader.rs
+2
+3 fn get_base_i(app_id: usize) -> usize {
+4     APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT
+5 }
+
+
+

我们可以在 config 子模块中找到这两个常数, APP_BASE_ADDRESS 被设置为 0x80400000 , +而 APP_SIZE_LIMIT 和上一章一样被设置为 0x20000 。这种放置方式与 user/build.py 的实现一致。

+
+
+ +
+ +
+ +
+
+ + + + + + + + + \ No newline at end of file diff --git a/chapter3/2task-switching.html b/chapter3/2task-switching.html new file mode 100644 index 0000000..e2e92ed --- /dev/null +++ b/chapter3/2task-switching.html @@ -0,0 +1,488 @@ + + + + + + + + 任务切换 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

任务切换

+

本节我们将见识操作系统的核心机制—— 任务切换 , +即应用在运行中主动或被动地交出 CPU 的使用权,内核可以选择另一个程序继续执行。 +内核需要保证用户程序两次运行期间,任务上下文(如寄存器、栈等)保持一致。

+
+

任务切换的设计与实现

+

任务切换与上一章提及的 Trap 控制流切换相比,有如下异同:

+
    +
  • 与 Trap 切换不同,它不涉及特权级切换,部分由编译器完成;

  • +
  • 与 Trap 切换相同,它对应用是透明的。

  • +
+

事实上,任务切换是来自两个不同应用在内核中的 Trap 控制流之间的切换。 +当一个应用 Trap 到 S 态 OS 内核中进行进一步处理时, +其 Trap 控制流可以调用一个特殊的 __switch 函数。 +在 __switch 返回之后,Trap 控制流将继续从调用该函数的位置继续向下执行。 +而在调用 __switch 之后到返回前的这段时间里, +原 Trap 控制流 A 会先被暂停并被切换出去, CPU 转而运行另一个应用的 Trap 控制流 B 。 +__switch 返回之后,原 Trap 控制流 A 才会从某一条 Trap 控制流 C 切换回来继续执行。

+

我们需要在 __switch 中保存 CPU 的某些寄存器,它们就是 任务上下文 (Task Context)。

+

下面我们给出 __switch 的实现:

+
 1# os/src/task/switch.S
+ 2
+ 3.altmacro
+ 4.macro SAVE_SN n
+ 5    sd s\n, (\n+2)*8(a0)
+ 6.endm
+ 7.macro LOAD_SN n
+ 8    ld s\n, (\n+2)*8(a1)
+ 9.endm
+10    .section .text
+11    .globl __switch
+12__switch:
+13    # __switch(
+14    #     current_task_cx_ptr: *mut TaskContext,
+15    #     next_task_cx_ptr: *const TaskContext
+16    # )
+17    # save kernel stack of current task
+18    sd sp, 8(a0)
+19    # save ra & s0~s11 of current execution
+20    sd ra, 0(a0)
+21    .set n, 0
+22    .rept 12
+23        SAVE_SN %n
+24        .set n, n + 1
+25    .endr
+26    # restore ra & s0~s11 of next execution
+27    ld ra, 0(a1)
+28    .set n, 0
+29    .rept 12
+30        LOAD_SN %n
+31        .set n, n + 1
+32    .endr
+33    # restore kernel stack of next task
+34    ld sp, 8(a1)
+35    ret
+
+
+

它的两个参数分别是当前和即将被切换到的 Trap 控制流的 task_cx_ptr ,从 RISC-V 调用规范可知,它们分别通过寄存器 a0/a1 传入。

+

内核先把 current_task_cx_ptr 中包含的寄存器值逐个保存,再把 next_task_cx_ptr 中包含的寄存器值逐个恢复。

+

TaskContext 里包含的寄存器有:

+
1// os/src/task/context.rs
+2#[repr(C)]
+3pub struct TaskContext {
+4    ra: usize,
+5    sp: usize,
+6    s: [usize; 12],
+7}
+
+
+

s0~s11 是被调用者保存寄存器, __switch 是用汇编编写的,编译器不会帮我们处理这些寄存器。 +保存 ra 很重要,它记录了 __switch 函数返回之后应该跳转到哪里继续执行。

+

我们将这段汇编代码 __switch 解释为一个 Rust 函数:

+
1// os/src/task/switch.rs
+2
+3core::arch::global_asm!(include_str!("switch.S"));
+4
+5extern "C" {
+6    pub fn __switch(
+7        current_task_cx_ptr: *mut TaskContext,
+8        next_task_cx_ptr: *const TaskContext);
+9}
+
+
+

我们会调用该函数来完成切换功能,而不是直接跳转到符号 __switch 的地址。 +因此在调用前后,编译器会帮我们保存和恢复调用者保存寄存器。

+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter3/3multiprogramming.html b/chapter3/3multiprogramming.html new file mode 100644 index 0000000..85fac28 --- /dev/null +++ b/chapter3/3multiprogramming.html @@ -0,0 +1,673 @@ + + + + + + + + 管理多道程序 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

管理多道程序

+

而内核为了管理任务,需要维护任务信息,相关内容包括:

+
    +
  • 任务运行状态:未初始化、准备执行、正在执行、已退出

  • +
  • 任务控制块:维护任务状态和任务上下文

  • +
  • 任务相关系统调用:程序主动暂停 sys_yield 和主动退出 sys_exit

  • +
+
+

yield 系统调用

+../_images/multiprogramming.png +

上图描述了一种多道程序执行的典型情况。其中横轴为时间线,纵轴为正在执行的实体。 +开始时,蓝色应用向外设提交了一个请求,外设随即开始工作, +但是它要一段时间后才能返回结果。蓝色应用于是调用 sys_yield 交出 CPU 使用权, +内核让绿色应用继续执行。一段时间后 CPU 切换回蓝色应用,发现外设仍未返回结果, +于是再次 sys_yield 。直到第二次切换回蓝色应用,外设才处理完请求,于是蓝色应用终于可以向下执行了。

+

我们还会遇到很多其他需要等待其完成才能继续向下执行的事件,调用 sys_yield 可以避免等待过程造成的资源浪费。

+
+
第三章新增系统调用(一)
+
/// 功能:应用主动交出 CPU 所有权并切换到其他应用。
+/// 返回值:总是返回 0。
+/// syscall ID:124
+fn sys_yield() -> isize;
+
+
+
+

用户库对应的实现和封装:

+
// user/src/syscall.rs
+
+pub fn sys_yield() -> isize {
+    syscall(SYSCALL_YIELD, [0, 0, 0])
+}
+
+// user/src/lib.rs
+// yield 是 Rust 的关键字
+pub fn yield_() -> isize { sys_yield() }
+
+
+

下文介绍内核应如何实现该系统调用。

+
+
+

任务控制块与任务运行状态

+

任务运行状态暂包括如下几种:

+
1// os/src/task/task.rs
+2
+3#[derive(Copy, Clone, PartialEq)]
+4pub enum TaskStatus {
+5    UnInit, // 未初始化
+6    Ready, // 准备运行
+7    Running, // 正在运行
+8    Exited, // 已退出
+9}
+
+
+

任务状态外和任务上下文一并保存在名为 任务控制块 (Task Control Block) 的数据结构中:

+
1// os/src/task/task.rs
+2
+3#[derive(Copy, Clone)]
+4pub struct TaskControlBlock {
+5    pub task_status: TaskStatus,
+6    pub task_cx: TaskContext,
+7}
+
+
+

任务控制块非常重要。在内核中,它就是应用的管理单位。后面的章节我们还会不断向里面添加更多内容。

+
+
+

任务管理器

+

内核需要一个全局的任务管理器来管理这些任务控制块:

+
// os/src/task/mod.rs
+
+pub struct TaskManager {
+    num_app: usize,
+    inner: UPSafeCell<TaskManagerInner>,
+}
+
+struct TaskManagerInner {
+    tasks: [TaskControlBlock; MAX_APP_NUM],
+    current_task: usize,
+}
+
+
+

这里用到了变量与常量分离的编程风格:字段 num_app 表示应用数目,它在 TaskManager 初始化后将保持不变; +而包裹在 TaskManagerInner 内的任务控制块数组 tasks,以及正在执行的应用编号 current_task 会在执行过程中变化。

+

初始化 TaskManager 的全局实例 TASK_MANAGER

+
 1// os/src/task/mod.rs
+ 2
+ 3lazy_static! {
+ 4    pub static ref TASK_MANAGER: TaskManager = {
+ 5        let num_app = get_num_app();
+ 6        let mut tasks = [TaskControlBlock {
+ 7            task_cx: TaskContext::zero_init(),
+ 8            task_status: TaskStatus::UnInit,
+ 9        }; MAX_APP_NUM];
+10        for (i, t) in tasks.iter_mut().enumerate().take(num_app) {
+11            t.task_cx = TaskContext::goto_restore(init_app_cx(i));
+12            t.task_status = TaskStatus::Ready;
+13        }
+14        TaskManager {
+15            num_app,
+16            inner: unsafe {
+17                UPSafeCell::new(TaskManagerInner {
+18                    tasks,
+19                    current_task: 0,
+20                })
+21            },
+22        }
+23    };
+24}
+
+
+
    +
  • 第 5 行:调用 loader 子模块提供的 get_num_app 接口获取链接到内核的应用总数;

  • +
  • 第 10~12 行:依次对每个任务控制块进行初始化,将其运行状态设置为 Ready ,并在它的内核栈栈顶压入一些初始化 +上下文,然后更新它的 task_cx 。一些细节我们会稍后介绍。

  • +
  • 从第 14 行开始:创建 TaskManager 实例并返回。

  • +
+
+

注解

+

关于 Rust 迭代器语法如 iter_mut/(a..b) ,及其方法如 enumerate/map/find/take,请参考 Rust 官方文档。

+
+
+
+

实现 sys_yield 和 sys_exit

+

sys_yield 的实现用到了 task 子模块提供的 suspend_current_and_run_next 接口,这个接口如字面含义,就是暂停当前的应用并切换到下个应用。

+
// 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 接口,它的含义是退出当前的应用并切换到下个应用:

+
// 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_nextexit_current_and_run_next 各是如何实现的呢?

+
// os/src/task/mod.rs
+
+pub fn suspend_current_and_run_next() {
+    TASK_MANAGER.mark_current_suspended();
+    TASK_MANAGER.run_next_task();
+}
+
+pub fn exit_current_and_run_next() {
+    TASK_MANAGER.mark_current_exited();
+    TASK_MANAGER.run_next_task();
+}
+
+
+

它们都是先修改当前应用的运行状态,然后尝试切换到下一个应用。修改运行状态比较简单,实现如下:

+
1// os/src/task/mod.rs
+2
+3impl TaskManager {
+4    fn mark_current_suspended(&self) {
+5        let mut inner = self.inner.exclusive_access();
+6        let current = inner.current_task;
+7        inner.tasks[current].task_status = TaskStatus::Ready;
+8    }
+9}
+
+
+

mark_current_suspended 为例。首先获得里层 TaskManagerInner 的可变引用,然后修改任务控制块数组 tasks 中当前任务的状态。

+

再看 run_next_task 的实现:

+
 1// os/src/task/mod.rs
+ 2
+ 3impl TaskManager {
+ 4    fn run_next_task(&self) {
+ 5        if let Some(next) = self.find_next_task() {
+ 6            let mut inner = self.inner.exclusive_access();
+ 7            let current = inner.current_task;
+ 8            inner.tasks[next].task_status = TaskStatus::Running;
+ 9            inner.current_task = next;
+10            let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext;
+11            let next_task_cx_ptr = &inner.tasks[next].task_cx as *const TaskContext;
+12            drop(inner);
+13            // before this, we should drop local variables that must be dropped manually
+14            unsafe {
+15                __switch(current_task_cx_ptr, next_task_cx_ptr);
+16            }
+17            // go back to user mode
+18        } else {
+19            panic!("All applications completed!");
+20        }
+21    }
+22
+23    fn find_next_task(&self) -> Option<usize> {
+24        let inner = self.inner.exclusive_access();
+25        let current = inner.current_task;
+26        (current + 1..current + self.num_app + 1)
+27            .map(|id| id % self.num_app)
+28            .find(|id| inner.tasks[*id].task_status == TaskStatus::Ready)
+29    }
+30}
+
+
+

run_next_task 会调用 find_next_task 方法尝试寻找一个运行状态为 Ready 的应用并获得其 ID 。 +如果找不到, 说明所有应用都执行完了, find_next_task 将返回 None ,内核 panic 退出。 +如果能够找到下一个可运行应用,我们就调用 __switch 切换任务。

+

切换任务之前,我们要手动 drop 掉我们获取到的 TaskManagerInner 可变引用。 +因为函数还没有返回, inner 不会自动销毁。我们只有令 TASK_MANAGERinner 字段回到未被借用的状态,下次任务切换时才能再借用。

+

我们可以总结一下应用的运行状态变化图:

+../_images/fsm-coop.png +
+
+

第一次进入用户态

+

我们在第二章中介绍过 CPU 第一次从内核态进入用户态的方法,只需在内核栈上压入构造好的 Trap 上下文, +然后 __restore 即可。本章要在此基础上做一些扩展。

+

在初始化任务控制块时,我们是这样做的:

+
// os/src/task/mod.rs
+
+for (i, t) in tasks.iter_mut().enumerate().take(num_app) {
+    t.task_cx = TaskContext::goto_restore(init_app_cx(i));
+    t.task_status = TaskStatus::Ready;
+}
+
+
+

init_app_cxloader 子模块中定义,它向内核栈压入了一个 Trap 上下文,并返回压入 Trap 上下文后 sp 的值。 +这个 Trap 上下文的构造方式与第二章相同。

+

goto_restore 保存传入的 sp,并将 ra 设置为 __restore 的入口地址,构造任务上下文后返回。这样,任务管理器中各个应用的任务上下文就得到了初始化。

+
// os/src/task/context.rs
+
+impl TaskContext {
+    pub fn goto_restore(kstack_ptr: usize) -> Self {
+        extern "C" { fn __restore(); }
+        Self {
+            ra: __restore as usize,
+            sp: kstack_ptr,
+            s: [0; 12],
+        }
+    }
+}
+
+
+

rust_main 中我们调用 task::run_first_task 来执行第一个应用:

+
 1// os/src/task/mod.rs
+ 2
+ 3fn run_first_task(&self) -> ! {
+ 4    let mut inner = self.inner.exclusive_access();
+ 5    let task0 = &mut inner.tasks[0];
+ 6    task0.task_status = TaskStatus::Running;
+ 7    let next_task_cx_ptr = &task0.task_cx as *const TaskContext;
+ 8    drop(inner);
+ 9    let mut _unused = TaskContext::zero_init();
+10    // before this, we should drop local variables that must be dropped manually
+11    unsafe {
+12        __switch(&mut _unused as *mut TaskContext, next_task_cx_ptr);
+13    }
+14    panic!("unreachable in run_first_task!");
+15}
+
+
+

我们显式声明了一个 _unused 变量,并将它的地址作为第一个参数传给 __switch , +声明此变量的意义仅仅是为了避免其他数据被覆盖。

+

__switch 中恢复 sp 后, sp 将指向 init_app_cx 构造的 Trap 上下文,后面就回到第二章的情况了。 +此外, __restore 的实现需要做出变化:它 不再需要 在开头 mv sp, a0 了。因为在 __switch 之后,sp 就已经正确指向了我们需要的 Trap 上下文地址。

+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter3/4time-sharing-system.html b/chapter3/4time-sharing-system.html new file mode 100644 index 0000000..efcb24e --- /dev/null +++ b/chapter3/4time-sharing-system.html @@ -0,0 +1,538 @@ + + + + + + + + 分时多任务系统 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

分时多任务系统

+

现代的任务调度算法基本都是抢占式的,它要求每个应用只能连续执行一段时间,然后内核就会将它强制性切换出去。 +一般将 时间片 (Time Slice) 作为应用连续执行时长的度量单位,每个时间片可能在毫秒量级。 +简单起见,我们使用 时间片轮转算法 (RR, Round-Robin) 来对应用进行调度。

+
+

时钟中断与计时器

+

实现调度算法需要计时。RISC-V 要求处理器维护时钟计数器 mtime,还有另外一个 CSR mtimecmp 。 +一旦计数器 mtime 的值超过了 mtimecmp,就会触发一次时钟中断。

+

运行在 M 特权级的 SEE 已经预留了相应的接口,基于此编写的 get_time 函数可以取得当前 mtime 计数器的值;

+
// os/src/timer.rs
+
+use riscv::register::time;
+
+pub fn get_time() -> usize {
+    time::read()
+}
+
+
+

在 10 ms 后设置时钟中断的代码如下:

+
 1// os/src/sbi.rs
+ 2
+ 3const SBI_SET_TIMER: usize = 0;
+ 4
+ 5pub fn set_timer(timer: usize) {
+ 6    sbi_call(SBI_SET_TIMER, timer, 0, 0);
+ 7}
+ 8
+ 9// os/src/timer.rs
+10
+11use crate::config::CLOCK_FREQ;
+12const TICKS_PER_SEC: usize = 100;
+13
+14pub fn set_next_trigger() {
+15    set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC);
+16}
+
+
+
    +
  • 第 5 行, sbi 子模块有一个 set_timer 调用,用来设置 mtimecmp 的值。

  • +
  • 第 14 行, timer 子模块的 set_next_trigger 函数对 set_timer 进行了封装, +它首先读取当前 mtime 的值,然后计算出 10ms 之内计数器的增量,再将 mtimecmp 设置为二者的和。 +这样,10ms 之后一个 S 特权级时钟中断就会被触发。

    +

    至于增量的计算方式, CLOCK_FREQ 是一个预先获取到的各平台不同的时钟频率,单位为赫兹,也就是一秒钟之内计数器的增量。 +它可以在 config 子模块中找到。10ms 的话只需除以常数 TICKS_PER_SEC 也就是 100 即可。

    +
  • +
+

后面可能还有一些计时的需求,我们再设计一个函数:

+
// os/src/timer.rs
+
+const MICRO_PER_SEC: usize = 1_000_000;
+
+pub fn get_time_us() -> usize {
+    time::read() / (CLOCK_FREQ / MICRO_PER_SEC)
+}
+
+
+

timer 子模块的 get_time_us 可以以微秒为单位返回当前计数器的值。

+

新增一个系统调用,使应用能获取当前的时间:

+
+
第三章新增系统调用(二)
+
/// 功能:获取当前的时间,保存在 TimeVal 结构体 ts 中,_tz 在我们的实现中忽略
+/// 返回值:返回是否执行成功,成功则返回 0
+/// syscall ID:169
+fn sys_get_time(ts: *mut TimeVal, _tz: usize) -> isize;
+
+
+
+

结构体 TimeVal 的定义如下,内核只需调用 get_time_us 即可实现该系统调用。

+
// os/src/syscall/process.rs
+
+#[repr(C)]
+pub struct TimeVal {
+    pub sec: usize,
+    pub usec: usize,
+}
+
+
+
+
+

RISC-V 架构中的嵌套中断问题

+

默认情况下,当 Trap 进入某个特权级之后,在 Trap 处理的过程中同特权级的中断都会被屏蔽。

+
    +
  • 当 Trap 发生时,sstatus.sie 会被保存在 sstatus.spie 字段中,同时 sstatus.sie 置零, +这也就在 Trap 处理的过程中屏蔽了所有 S 特权级的中断;

  • +
  • 当 Trap 处理完毕 sret 的时候, sstatus.sie 会恢复到 sstatus.spie 内的值。

  • +
+

也就是说,如果不去手动设置 sstatus CSR ,在只考虑 S 特权级中断的情况下,是不会出现 嵌套中断 (Nested Interrupt) 的。

+
+

注解

+

嵌套中断与嵌套 Trap

+

嵌套中断可以分为两部分:在处理一个中断的过程中又被同特权级/高特权级中断所打断。默认情况下硬件会避免前一部分, +也可以通过手动设置来允许前一部分的存在;而从上面介绍的规则可以知道,后一部分则是无论如何设置都不可避免的。

+

嵌套 Trap 则是指处理一个 Trap 过程中又再次发生 Trap ,嵌套中断算是嵌套 Trap 的一种。

+
+
+
+

抢占式调度

+

有了时钟中断和计时器,抢占式调度就很容易实现了:

+
// os/src/trap/mod.rs
+
+match scause.cause() {
+    Trap::Interrupt(Interrupt::SupervisorTimer) => {
+        set_next_trigger();
+        suspend_current_and_run_next();
+    }
+}
+
+
+

我们只需在 trap_handler 函数下新增一个分支,触发了 S 特权级时钟中断时,重新设置计时器, +调用 suspend_current_and_run_next 函数暂停当前应用并切换到下一个。

+

为了避免 S 特权级时钟中断被屏蔽,我们需要在执行第一个应用前调用 enable_timer_interrupt() 设置 sie.stie, +使得 S 特权级时钟中断不会被屏蔽;再设置第一个 10ms 的计时器。

+
 1// os/src/main.rs
+ 2
+ 3#[no_mangle]
+ 4pub fn rust_main() -> ! {
+ 5    // ...
+ 6    trap::enable_timer_interrupt();
+ 7    timer::set_next_trigger();
+ 8    // ...
+ 9}
+10
+11// os/src/trap/mod.rs
+12
+13use riscv::register::sie;
+14
+15pub fn enable_timer_interrupt() {
+16    unsafe { sie::set_stimer(); }
+17}
+
+
+

就这样,我们实现了时间片轮转任务调度算法。 power 系列用户程序可以验证我们取得的成果:这些应用并没有主动 yield, +内核仍能公平地把时间片分配给它们。

+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter3/5exercise.html b/chapter3/5exercise.html new file mode 100644 index 0000000..8ce8388 --- /dev/null +++ b/chapter3/5exercise.html @@ -0,0 +1,549 @@ + + + + + + + + chapter3练习 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

chapter3练习

+
+

Lab1 编程作业

+
+

获取任务信息

+

ch3 中,我们的系统已经能够支持多个任务分时轮流运行,我们希望引入一个新的系统调用 sys_task_info 以获取当前任务的信息,定义如下:

+
fn sys_task_info(ti: *mut TaskInfo) -> isize
+
+
+
    +
  • syscall ID: 410

  • +
  • 查询当前正在执行的任务信息,任务信息包括任务控制块相关信息(任务状态)、任务使用的系统调用及调用次数、任务总运行时长(单位ms)。

  • +
+
struct TaskInfo {
+    status: TaskStatus,
+    syscall_times: [u32; MAX_SYSCALL_NUM],
+    time: usize
+}
+
+
+
    +
  • +
    参数:
      +
    • ti: 待查询任务信息

    • +
    +
    +
    +
  • +
  • 返回值:执行成功返回0,错误返回-1

  • +
  • +
    说明:
      +
    • 相关结构已在框架中给出,只需添加逻辑实现功能需求即可。

    • +
    • 在我们的实验中,系统调用号一定小于 500,所以直接使用一个长为 MAX_SYSCALL_NUM=500 的数组做桶计数。

    • +
    • 运行时间 time 返回系统调用时刻距离任务第一次被调度时刻的时长,也就是说这个时长可能包含该任务被其他任务抢占后的等待重新调度的时间。

    • +
    • 由于查询的是当前任务的状态,因此 TaskStatus 一定是 Running。(助教起初想设计根据任务 id 查询,但是既不好定义任务 id 也不好写测例,遂放弃 QAQ)

    • +
    • 调用 sys_task_info 也会对本次调用计数。

    • +
    +
    +
    +
  • +
  • +
    提示:
      +
    • 大胆修改已有框架!除了配置文件,你几乎可以随意修改已有框架的内容。

    • +
    • 程序运行时间可以通过调用 get_time() 获取,注意任务运行总时长的单位是 ms。

    • +
    • 系统调用次数可以考虑在进入内核态系统调用异常处理函数之后,进入具体系统调用函数之前维护。

    • +
    • 阅读 TaskManager 的实现,思考如何维护内核控制块信息(可以在控制块可变部分加入需要的信息)。

    • +
    +
    +
    +
  • +
+
+
+

实验要求

+ +
├── os3(内核实现)
+│   ├── Cargo.toml(配置文件)
+│   └── src(所有内核的源代码放在 os/src 目录下)
+│       ├── main.rs(内核主函数)
+│       └── ...
+├── reports (不是 report)
+│   ├── lab1.md/pdf
+│   └── ...
+├── ...
+
+
+
    +
  • 通过所有测例:

    +
    +

    CI 使用的测例与本地相同,测试中,user 文件夹及其它与构建相关的文件将被替换,请不要试图依靠硬编码通过测试。

    +

    os3 目录下,默认情况下,makefile 仅编译基础测例 (BASE=1),即无需修改框架即可正常运行的测例。 +你需要在编译时指定 BASE=0 控制框架仅编译实验测例(在 os 目录执行 make run BASE=0), +或指定 BASE=2 控制框架同时编译基础测例和实验测例。

    +
    +
  • +
+
+

注解

+

你的实现只需且必须通过测例,建议读者感到困惑时先检查测例。

+
+
+
+
+

简答作业

+
    +
  1. 正确进入 U 态后,程序的特征还应有:使用 S 态特权指令,访问 S 态寄存器后会报错。 +请同学们可以自行测试这些内容 (运行 Rust 三个 bad 测例 (ch2b_bad_*.rs) , +注意在编译时至少需要指定 LOG=ERROR 才能观察到内核的报错信息) , +描述程序出错行为,同时注意注明你使用的 sbi 及其版本。

  2. +
  3. 深入理解 trap.S +中两个函数 __alltraps__restore 的作用,并回答如下问题:

    +
      +
    1. L40:刚进入 __restore 时,a0 代表了什么值。请指出 __restore 的两种使用情景。

    2. +
    3. L46-L51:这几行汇编代码特殊处理了哪些寄存器?这些寄存器的的值对于进入用户态有何意义?请分别解释。

      +
      ld t0, 32*8(sp)
      +ld t1, 33*8(sp)
      +ld t2, 2*8(sp)
      +csrw sstatus, t0
      +csrw sepc, t1
      +csrw sscratch, t2
      +
      +
      +
    4. +
    5. L53-L59:为何跳过了 x2x4

      +
      ld x1, 1*8(sp)
      +ld x3, 3*8(sp)
      +.set n, 5
      +.rept 27
      +   LOAD_GP %n
      +   .set n, n+1
      +.endr
      +
      +
      +
    6. +
    7. L63:该指令之后,spsscratch 中的值分别有什么意义?

      +
      csrrw sp, sscratch, sp
      +
      +
      +
    8. +
    9. __restore:中发生状态切换在哪一条指令?为何该指令执行之后会进入用户态?

    10. +
    11. L13:该指令之后,spsscratch 中的值分别有什么意义?

      +
      csrrw sp, sscratch, sp
      +
      +
      +
    12. +
    13. 从 U 态进入 S 态是哪一条指令发生的?

    14. +
    +
  4. +
+
+
+

报告要求

+
    +
  • 简单总结你实现的功能(200字以内,不要贴代码)。

  • +
  • 完成问答题。

  • +
  • (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。

  • +
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter3/index.html b/chapter3/index.html new file mode 100644 index 0000000..f341ab0 --- /dev/null +++ b/chapter3/index.html @@ -0,0 +1,427 @@ + + + + + + + + 第三章:多道程序与分时多任务 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ + +
+ + + + + + + + \ No newline at end of file diff --git a/chapter4/0intro.html b/chapter4/0intro.html new file mode 100644 index 0000000..ca4978b --- /dev/null +++ b/chapter4/0intro.html @@ -0,0 +1,491 @@ + + + + + + + + 引言 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

引言

+
+

本章导读

+

本章中内核将实现虚拟内存机制,这注定是一趟艰难的旅程。

+
+
+

实践体验

+
+

注解

+

基于github classroom的开发方式

+

基于github classroom,可方便建立开发用的git repository,并可基于github的 codespace (在线版ubuntu +vscode)在线开发使用。整个开发环境仅仅需要一个网络浏览器。

+
    +
  1. 在网络浏览器中用自己的 github id 登录 github.com

  2. +
  3. 接收 第二个实验(os4)的github classroom在线邀请 ,根据提示一路选择OK即可。

  4. +
  5. 完成第二步后,你的第二个实验的 github repository 会被自动建立好,点击此github repository的链接,就可看到你要完成的第一个实验了。

  6. +
  7. 在你的第二个实验的网页的中上部可以看到一个醒目的 code 绿色按钮,点击后,可以进一步看到 codespace 标签和醒目的 create codesapce on main 绿色按钮。请点击这个绿色按钮,就可以进入到在线的ubuntu +vscode环境中

  8. +
  9. 再按照下面的环境安装提示在vscode的 console 中安装配置开发环境:rustc,qemu等工具。

  10. +
  11. 在vscode的 console 中执行 make setupclassroom_test4 (该命令仅执行一次)配置githubclassroom 自动评分功能。

  12. +
  13. 然后就可以基于在线vscode进行开发、运行、提交等完整的实验过程了。

  14. +
+

上述的3,4,5步不是必须的,你也可以线下本地开发。

+
+

本章应用运行起来效果与上一章基本一致。

+

获取本章代码:

+
$ git clone https://github.com/LearningOS/rust-based-os-comp2022.git
+$ cd rust-based-os-comp2022/
+$ make setupclassroom_test4  //注意:这一步很重要,是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次,以后一般就不用执行了,除非 .github/workflows/classroom.yml发生了变化。
+
+
+

在 qemu 模拟器上运行 lab2(os4)参考框架:

+
$ cd os4-ref
+$ make run
+
+
+
+
+

lab2(os4)参考框架:

+
 1├── os4-ref
+ 2│   ├── ...
+ 3│   └── src
+ 4│       ├── ...
+ 5│       ├── config.rs(修改:新增一些内存管理的相关配置)
+ 6│       ├── linker.ld(修改:将跳板页引入内存布局)
+ 7│       ├── loader.rs(修改:仅保留获取应用数量和数据的功能)
+ 8│       ├── main.rs(修改)
+ 9│       ├── mm(新增:内存管理的 mm 子模块)
+10│       │   ├── address.rs(物理/虚拟 地址/页号的 Rust 抽象)
+11│       │   ├── frame_allocator.rs(物理页帧分配器)
+12│       │   ├── heap_allocator.rs(内核动态内存分配器)
+13│       │   ├── memory_set.rs(引入地址空间 MemorySet 及逻辑段 MemoryArea 等)
+14│       │   ├── mod.rs(定义了 mm 模块初始化方法 init)
+15│       │   └── page_table.rs(多级页表抽象 PageTable 以及其他内容)
+16│       ├── syscall
+17│       │   ├── fs.rs(修改:基于地址空间的 sys_write 实现)
+18│       │   ├── mod.rs
+19│       │   └── process.rs
+20│       ├── task
+21│       │   ├── context.rs(修改:构造一个跳转到不同位置的初始任务上下文)
+22│       │   ├── mod.rs(修改,详见文档)
+23│       │   ├── switch.rs
+24│       │   ├── switch.S
+25│       │   └── task.rs(修改,详见文档)
+26│       └── trap
+27│           ├── context.rs(修改:在 Trap 上下文中加入了更多内容)
+28│           ├── mod.rs(修改:基于地址空间修改了 Trap 机制,详见文档)
+29│           └── trap.S(修改:基于地址空间修改了 Trap 上下文保存与恢复汇编代码)
+30└── user
+31    ├── build.py(编译时不再使用)
+32    ├── ...
+33    └── src
+34        ├── linker.ld(修改:将所有应用放在各自地址空间中固定的位置)
+35        └── ...
+36
+37 cloc os4-ref
+38 -------------------------------------------------------------------------------
+39 Language                     files          blank        comment           code
+40 -------------------------------------------------------------------------------
+41 Rust                            26            138             56           1526
+42 Assembly                         3              3             26             86
+43 make                             1             11              4             36
+44 TOML                             1              2              1             13
+45 -------------------------------------------------------------------------------
+46 SUM:                            31            154             87           1661
+47 -------------------------------------------------------------------------------
+
+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter4/3sv39-implementation-1.html b/chapter4/3sv39-implementation-1.html new file mode 100644 index 0000000..9facd9b --- /dev/null +++ b/chapter4/3sv39-implementation-1.html @@ -0,0 +1,590 @@ + + + + + + + + 实现 SV39 多级页表机制(上) - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

实现 SV39 多级页表机制(上)

+
+

注解

+

背景知识: 地址空间

+

背景知识: SV39 多级页表原理

+
+

我们将在内核实现 RV64 架构 SV39 分页机制。由于内容过多,分成两个小节。

+
+

虚拟地址和物理地址

+
+

内存控制相关的CSR寄存器

+

默认情况下 MMU 未被使能,此时无论 CPU 处于哪个特权级,访存的地址都将直接被视作物理地址。 +可以通过修改 S 特权级的 satp CSR 来启用分页模式,此后 S 和 U 特权级的访存地址会被视为虚拟地址,经过 MMU 的地址转换获得对应物理地址,再通过它来访问物理内存。

+../_images/satp.png +

上图是 RV64 架构下 satp 的字段分布。当 MODE 设置为 0 的时候,所有访存都被视为物理地址;而设置为 8 +时,SV39 分页机制被启用,所有 S/U 特权级的访存被视为一个 39 位的虚拟地址,MMU 会将其转换成 56 位的物理地址;如果转换失败,则会触发异常。

+
+
+

地址格式与组成

+../_images/sv39-va-pa.png +

我们采用分页管理,单个页面的大小设置为 \(4\text{KiB}\) ,每个虚拟页面和物理页帧都按 4 KB 对齐。 +\(4\text{KiB}\) 需要用 12 位字节地址来表示,因此虚拟地址和物理地址都被分成两部分: +它们的低 12 位被称为 页内偏移 (Page Offset) 。虚拟地址的高 27 位,即 \([38:12]\) 为它的虚拟页号 VPN; +物理地址的高 44 位,即 \([55:12]\) 为它的物理页号 PPN。页号可以用来定位一个虚拟/物理地址属于哪一个虚拟页面/物理页帧。

+

地址转换是以页为单位进行的,转换前后地址页内偏移部分不变。MMU 只是从虚拟地址中取出 27 位虚拟页号, +在页表中查到其对应的物理页号,如果找到,就将得到的 44 位的物理页号与 12 位页内偏移拼接到一起,形成 56 位物理地址。

+
+

注解

+

RV64 架构中虚拟地址为何只有 39 位?

+

虚拟地址长度确实应该和位宽一致为 64 位,但是在启用 SV39 分页模式下,只有低 39 位是真正有意义的。 +SV39 分页模式规定 64 位虚拟地址的 \([63:39]\) 这 25 位必须和第 38 位相同,否则 MMU 会直接认定它是一个 +不合法的虚拟地址。。

+

也就是说,所有 \(2^{64}\) 个虚拟地址中,只有最低的 \(256\text{GiB}\) (当第 38 位为 0 时) +以及最高的 \(256\text{GiB}\) (当第 38 位为 1 时)是可能通过 MMU 检查的。

+
+
+
+

地址相关的数据结构抽象与类型定义

+

实现页表之前,先将地址和页号的概念抽象为 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);
+
+
+

上面分别给出了物理地址、虚拟地址、物理页号、虚拟页号的 Rust 类型声明,它们都是 usize 的一种简单包装。 +将它们各自抽象出来而不是直接使用 usize,是为了在 Rust 编译器的帮助下进行多种方便且安全的 类型转换 (Type Convertion) 。

+

这些类型本身可以和 usize 之间互相转换,地址和页号之间也可以相互转换。以物理地址和物理页号之间的转换为例:

+
 1// os/src/mm/address.rs
+ 2
+ 3impl PhysAddr {
+ 4    pub fn page_offset(&self) -> usize { self.0 & (PAGE_SIZE - 1) }
+ 5}
+ 6
+ 7impl From<PhysAddr> for PhysPageNum {
+ 8    fn from(v: PhysAddr) -> Self {
+ 9        assert_eq!(v.page_offset(), 0);
+10        v.floor()
+11    }
+12}
+13
+14impl From<PhysPageNum> for PhysAddr {
+15    fn from(v: PhysPageNum) -> Self { Self(v.0 << PAGE_SIZE_BITS) }
+16}
+
+
+

其中 PAGE_SIZE\(4096\)PAGE_SIZE_BITS\(12\) ,它们均定义在 config 子模块 +中,分别表示每个页面的大小和页内偏移的位宽。从物理页号到物理地址的转换只需左移 \(12\) 位即可,但是物理地址需要 +保证它与页面大小对齐才能通过右移转换为物理页号。

+

对于不对齐的情况,物理地址不能通过 From/Into 转换为物理页号,而是需要通过它自己的 floorceil 方法来 +进行下取整或上取整的转换。

+
// 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) }
+}
+
+
+
+
+
+

页表项的数据结构抽象与类型定义

+../_images/sv39-pte.png +

上图为 SV39 分页模式下的页表项,其中 \([53:10]\)\(44\) 位是物理页号,最低的 \(8\) 位 +\([7:0]\) 则是标志位,它们的含义如下:

+
    +
  • 仅当 V(Valid) 位为 1 时,页表项才是合法的;

  • +
  • R/W/X 分别控制索引到这个页表项的对应虚拟页面是否允许读/写/取指;

  • +
  • U 控制索引到这个页表项的对应虚拟页面是否在 CPU 处于 U 特权级的情况下是否被允许访问;

  • +
  • G 我们不理会;

  • +
  • A(Accessed) 记录自从页表项上的这一位被清零之后,页表项的对应虚拟页面是否被访问过;

  • +
  • D(Dirty) 则记录自从页表项上的这一位被清零之后,页表项的对应虚拟页表是否被修改过。

  • +
+

先来实现页表项中的标志位 PTEFlags

+
// 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 封装成一个标志位的集合类型,支持一些常见的集合 +运算。

+

接下来我们实现页表项 PageTableEntry

+
 1// os/src/mm/page_table.rs
+ 2
+ 3#[derive(Copy, Clone)]
+ 4#[repr(C)]
+ 5pub struct PageTableEntry {
+ 6    pub bits: usize,
+ 7}
+ 8
+ 9impl PageTableEntry {
+10    pub fn new(ppn: PhysPageNum, flags: PTEFlags) -> Self {
+11        PageTableEntry {
+12            bits: ppn.0 << 10 | flags.bits as usize,
+13        }
+14    }
+15    pub fn empty() -> Self {
+16        PageTableEntry {
+17            bits: 0,
+18        }
+19    }
+20    pub fn ppn(&self) -> PhysPageNum {
+21        (self.bits >> 10 & ((1usize << 44) - 1)).into()
+22    }
+23    pub fn flags(&self) -> PTEFlags {
+24        PTEFlags::from_bits(self.bits as u8).unwrap()
+25    }
+26}
+
+
+
    +
  • 第 3 行我们让编译器自动为 PageTableEntry 实现 Copy/Clone Trait,来让这个类型以值语义赋值/传参的时候 +不会发生所有权转移,而是拷贝一份新的副本。

  • +
  • 第 10 行使得我们可以从一个物理页号 PhysPageNum 和一个页表项标志位 PTEFlags 生成一个页表项 +PageTableEntry 实例;而第 20 行和第 23 行则分别可以从一个页表项将它们两个取出。

  • +
  • 第 15 行中,我们也可以通过 empty 方法生成一个全零的页表项,注意这隐含着该页表项的 V 标志位为 0 , +因此它是不合法的。

  • +
+

后面我们还为 PageTableEntry 实现了一些辅助函数(Helper Function),可以快速判断一个页表项的 V/R/W/X 标志位是否为 1,以 V +标志位的判断为例:

+
// os/src/mm/page_table.rs
+
+impl PageTableEntry {
+    pub fn is_valid(&self) -> bool {
+        (self.flags() & PTEFlags::V) != PTEFlags::empty()
+    }
+}
+
+
+

这里相当于判断两个集合的交集是否为空。

+
+
+ +
+ +
+ +
+
+ + + + + + + + + \ No newline at end of file diff --git a/chapter4/4sv39-implementation-2.html b/chapter4/4sv39-implementation-2.html new file mode 100644 index 0000000..98a3ac8 --- /dev/null +++ b/chapter4/4sv39-implementation-2.html @@ -0,0 +1,805 @@ + + + + + + + + 实现 SV39 多级页表机制(下) - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

实现 SV39 多级页表机制(下)

+
+

物理页帧管理

+
+

可用物理页的分配与回收

+

首先,我们需要知道物理内存的哪一部分是可用的。在 os/src/linker.ld 中,我们用符号 ekernel 指明了 +内核数据的终止物理地址,在它之后的物理内存都是可用的。而在 config 子模块中:

+
// os/src/config.rs
+
+pub const MEMORY_END: usize = 0x80800000;
+
+
+

我们硬编码整块物理内存的终止物理地址为 0x80800000 。 而物理内存的起始物理地址为 0x80000000 , +意味着我们将可用内存大小设置为 \(8\text{MiB}\) ,当然也可以设置的更大一点。

+

用一个左闭右开的物理页号区间来表示可用的物理内存,则:

+
    +
  • 区间的左端点应该是 ekernel 的物理地址以上取整方式转化成的物理页号;

  • +
  • 区间的右端点应该是 MEMORY_END 以下取整方式转化成的物理页号。

  • +
+

这个区间将被传给我们后面实现的物理页帧管理器用于初始化。

+

我们声明一个 FrameAllocator Trait 来描述一个物理页帧管理器需要提供哪些功能:

+
// os/src/mm/frame_allocator.rs
+
+trait FrameAllocator {
+    fn new() -> Self;
+    fn alloc(&mut self) -> Option<PhysPageNum>;
+    fn dealloc(&mut self, ppn: PhysPageNum);
+}
+
+
+

我们实现一种最简单的栈式物理页帧管理策略 StackFrameAllocator

+
// os/src/mm/frame_allocator.rs
+
+pub struct StackFrameAllocator {
+    current: usize,
+    end: usize,
+    recycled: Vec<usize>,
+}
+
+
+

其中各字段的含义是:物理页号区间 \([\text{current},\text{end})\) 此前均 从未 被分配出去过,而向量 +recycled 以后入先出的方式保存了被回收的物理页号(我们已经实现了堆分配器,参见第三章实验)。

+

初始化非常简单。在通过 FrameAllocatornew 方法创建实例的时候,只需将区间两端均设为 \(0\) , +然后创建一个新的向量;而在它真正被使用起来之前,需要调用 init 方法将自身的 \([\text{current},\text{end})\) +初始化为可用物理页号区间:

+
// 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;
+    }
+}
+
+
+

接下来我们来看核心的物理页帧分配和回收如何实现:

+
// os/src/mm/frame_allocator.rs
+
+impl FrameAllocator for StackFrameAllocator {
+    fn alloc(&mut self) -> Option<PhysPageNum> {
+        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 内有没有之前回收的物理页号,如果有的话直接弹出栈顶并返回; +否则的话我们只能从之前从未分配过的物理页号区间 \([\text{current},\text{end})\) 上进行分配,我们分配它的 +左端点 current ,同时将管理器内部维护的 current 加一代表 current 此前已经被分配过了。在即将返回 +的时候,我们使用 into 方法将 usize 转换成了物理页号 PhysPageNum

    +

    注意极端情况下可能出现内存耗尽分配失败的情况:即 recycled 为空且 \(\text{current}==\text{end}\) 。 +为了涵盖这种情况, alloc 的返回值被 Option 包裹,我们返回 None 即可。

    +
  • +
  • 在回收 dealloc 的时候,我们需要检查回收页面的合法性,然后将其压入 recycled 栈中。回收页面合法有两个 +条件:

    +
      +
    • 该页面之前一定被分配出去过,因此它的物理页号一定 \(<\text{current}\)

    • +
    • 该页面没有正处在回收状态,即它的物理页号不能在栈 recycled 中找到。

    • +
    +

    我们通过 recycled.iter() 获取栈上内容的迭代器,然后通过迭代器的 find 方法试图 +寻找一个与输入物理页号相同的元素。其返回值是一个 Option ,如果找到了就会是一个 Option::Some , +这种情况说明我们内核其他部分实现有误,直接报错退出。

    +
  • +
+

之后创建 StackFrameAllocator 的全局实例 FRAME_ALLOCATOR,并在正式分配物理页帧之前将 FRAME_ALLOCATOR 初始化,见 os/src/mm/frame_allocator.rs

+
+
+

分配/回收物理页帧的接口

+

公开给其他子模块调用的分配/回收物理页帧的接口:

+
// os/src/mm/frame_allocator.rs
+
+pub fn frame_alloc() -> Option<FrameTracker> {
+    FRAME_ALLOCATOR
+        .exclusive_access()
+        .alloc()
+        .map(FrameTracker::new)
+}
+
+fn frame_dealloc(ppn: PhysPageNum) {
+    FRAME_ALLOCATOR.exclusive_access().dealloc(ppn);
+}
+
+
+

可以发现, frame_alloc 的返回值类型并不是 FrameAllocator 要求的物理页号 PhysPageNum ,而是将其 +进一步包装为一个 FrameTracker ,其定义如下。 FrameTracker 被创建时,需要从 FRAME_ALLOCATOR 中分配一个物理页帧:

+
// 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 }
+    }
+}
+
+
+

我们将分配来的物理页帧的物理页号作为参数传给 FrameTrackernew 方法来创建一个 FrameTracker +实例。由于这个物理页帧之前可能被分配过并用做其他用途,我们在这里直接将这个物理页帧上的所有字节清零。这一过程并不 +那么显然,我们后面再详细介绍。

+

当一个 FrameTracker 生命周期结束被编译器回收的时候,我们需要将它控制的物理页帧回收掉 FRAME_ALLOCATOR 中:

+
// 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 我们就将它控制的物理页帧回收以供后续使用了。

+

最后做一个小结:从其他模块的视角看来,物理页帧分配的接口是调用 frame_alloc 函数得到一个 FrameTracker +(如果物理内存还有剩余),它就代表了一个物理页帧,当它的生命周期结束之后它所控制的物理页帧将被自动回收。

+
+
+
+

多级页表实现

+
+

页表基本数据结构与访问接口

+

我们知道,SV39 多级页表是以节点为单位进行管理的。每个节点恰好存储在一个物理页帧中,它的位置可以用一个物理页号来表示。

+
 1// os/src/mm/page_table.rs
+ 2
+ 3pub struct PageTable {
+ 4    root_ppn: PhysPageNum,
+ 5    frames: Vec<FrameTracker>,
+ 6}
+ 7
+ 8impl PageTable {
+ 9    pub fn new() -> Self {
+10        let frame = frame_alloc().unwrap();
+11        PageTable {
+12            root_ppn: frame.ppn,
+13            frames: vec![frame],
+14        }
+15    }
+16}
+
+
+

每个应用的地址空间都对应一个不同的多级页表,这也就意味这不同页表的起始地址(即页表根节点的地址)是不一样的。 +因此 PageTable 要保存它根节点的物理页号 root_ppn 作为页表唯一的区分标志。此外, +向量 framesFrameTracker 的形式保存了页表所有的节点(包括根节点)所在的物理页帧。这与物理页帧管理模块 +的测试程序是一个思路,即将这些 FrameTracker 的生命周期进一步绑定到 PageTable 下面。当 PageTable +生命周期结束后,向量 frames 里面的那些 FrameTracker 也会被回收,也就意味着存放多级页表节点的那些物理页帧 +被回收了。

+

当我们通过 new 方法新建一个 PageTable 的时候,它只需有一个根节点。为此我们需要分配一个物理页帧 +FrameTracker 并挂在向量 frames 下,然后更新根节点的物理页号 root_ppn

+

多级页表并不是被创建出来之后就不再变化的,为了 MMU 能够通过地址转换正确找到应用地址空间中的数据实际被内核放在内存中 +位置,操作系统需要动态维护一个虚拟页号到页表项的映射,支持插入/删除键值对,其方法签名如下:

+
// 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 方法来删除一个键值对,在调用时仅需给出作为索引的虚拟页号即可。

  • +
+

在这些操作的过程中,我们自然需要访问或修改多级页表节点的内容。每个节点都被保存在一个物理页帧中,在多级页表的架构中,我们以 +一个节点被存放在的物理页帧的物理页号作为指针指向该节点,这意味着,对于每个节点来说,一旦我们知道了指向它的物理页号,我们 +就能够修改这个节点的内容。

+

这就需要我们提前扩充多级页表维护的映射,使得对于每一个对应于某一特定物理页帧的物理页号 ppn ,均存在一个虚拟页号 +vpn 能够映射到它,而且要能够较为简单的针对一个 ppn 找到某一个能映射到它的 vpn 。这里我们采用一种最 +简单的 恒等映射 (Identical Mapping) ,也就是说对于物理内存上的每个物理页帧,我们都在多级页表中用一个与其 +物理页号相等的虚拟页号映射到它。当我们想针对物理页号构造一个能映射到它的虚拟页号的时候,也只需使用一个和该物理页号 +相等的虚拟页号即可。

+
+
+

内核中访问物理页帧的方法

+

于是,我们来看看在内核中应如何访问一个特定的物理页帧:

+
// 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<T>(&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 的解引用访问它指向的数据,而是可以像一个 +正常的可变引用一样直接访问。

+
+
+

建立和拆除虚实地址映射关系

+

接下来介绍建立和拆除虚实地址映射关系的 mapunmap 方法是如何实现的。它们都依赖于一个很重要的过程, +也即在多级页表中找到一个虚拟地址对应的页表项。找到之后,只要修改页表项的内容即可完成键值对的插入和删除。 +在寻找页表项的时候,可能出现页表的中间级节点还未被创建的情况,这个时候我们需要手动分配一个物理页帧来存放这个节点, +并将这个节点接入到当前的多级页表的某级中。

+
 1// os/src/mm/address.rs
+ 2
+ 3impl VirtPageNum {
+ 4    pub fn indexes(&self) -> [usize; 3] {
+ 5        let mut vpn = self.0;
+ 6        let mut idx = [0usize; 3];
+ 7        for i in (0..3).rev() {
+ 8            idx[i] = vpn & 511;
+ 9            vpn >>= 9;
+10        }
+11        idx
+12    }
+13}
+14
+15// os/src/mm/page_table.rs
+16
+17impl PageTable {
+18    fn find_pte_create(&mut self, vpn: VirtPageNum) -> Option<&mut PageTableEntry> {
+19        let idxs = vpn.indexes();
+20        let mut ppn = self.root_ppn;
+21        let mut result: Option<&mut PageTableEntry> = None;
+22        for i in 0..3 {
+23            let pte = &mut ppn.get_pte_array()[idxs[i]];
+24            if i == 2 {
+25                result = Some(pte);
+26                break;
+27            }
+28            if !pte.is_valid() {
+29                let frame = frame_alloc().unwrap();
+30                *pte = PageTableEntry::new(frame.ppn, PTEFlags::V);
+31                self.frames.push(frame);
+32            }
+33            ppn = pte.ppn();
+34        }
+35        result
+36    }
+37}
+
+
+
    +
  • VirtPageNumindexes 可以取出虚拟页号的三级页索引,并按照从高到低的顺序返回。注意它里面包裹的 +usize 可能有 \(27\) 位,也有可能有 \(64-12=52\) 位,但这里我们是用来在多级页表上进行遍历,因此 +只取出低 \(27\) 位。

  • +
  • PageTable::find_pte_create 在多级页表找到一个虚拟页号对应的页表项的可变引用方便后续的读写。如果在 +遍历的过程中发现有节点尚未创建则会新建一个节点。

    +

    变量 ppn 表示当前节点的物理页号,最开始指向多级页表的根节点。随后每次循环通过 get_pte_array 将 +取出当前节点的页表项数组,并根据当前级页索引找到对应的页表项。如果当前节点是一个叶节点,那么直接返回这个页表项 +的可变引用;否则尝试向下走。走不下去的话就新建一个节点,更新作为下级节点指针的页表项,并将新分配的物理页帧移动到 +向量 frames 中方便后续的自动回收。注意在更新页表项的时候,不仅要更新物理页号,还要将标志位 V 置 1, +不然硬件在查多级页表的时候,会认为这个页表项不合法,从而触发 Page Fault 而不能向下走。

    +
  • +
+

于是, map/unmap 就非常容易实现了:

+
// 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();
+    }
+}
+
+
+

只需根据虚拟页号找到页表项,然后修改或者直接清空其内容即可。

+
+

警告

+

目前的实现方式并不打算对物理页帧耗尽的情形做任何处理而是直接 panic 退出。因此在前面的代码中能够看到 +很多 unwrap ,这种使用方式并不为 Rust 所推荐,只是由于简单起见暂且这样做。

+
+

为了方便后面的实现,我们还需要 PageTable 提供一种不经过 MMU 而是手动查页表的方法:

+
 1// os/src/mm/page_table.rs
+ 2
+ 3impl PageTable {
+ 4    /// Temporarily used to get arguments from user space.
+ 5    pub fn from_token(satp: usize) -> Self {
+ 6        Self {
+ 7            root_ppn: PhysPageNum::from(satp & ((1usize << 44) - 1)),
+ 8            frames: Vec::new(),
+ 9        }
+10    }
+11    fn find_pte(&self, vpn: VirtPageNum) -> Option<&PageTableEntry> {
+12        let idxs = vpn.indexes();
+13        let mut ppn = self.root_ppn;
+14        let mut result: Option<&PageTableEntry> = None;
+15        for i in 0..3 {
+16            let pte = &ppn.get_pte_array()[idxs[i]];
+17            if i == 2 {
+18                result = Some(pte);
+19                break;
+20            }
+21            if !pte.is_valid() {
+22                return None;
+23            }
+24            ppn = pte.ppn();
+25        }
+26        result
+27    }
+28    pub fn translate(&self, vpn: VirtPageNum) -> Option<PageTableEntry> {
+29        self.find_pte(vpn)
+30            .map(|pte| {pte.clone()})
+31    }
+32}
+
+
+
    +
  • 第 5 行的 from_token 可以临时创建一个专用来手动查页表的 PageTable ,它仅有一个从传入的 satp token +中得到的多级页表根节点的物理页号,它的 frames 字段为空,也即不实际控制任何资源;

  • +
  • 第 11 行的 find_pte 和之前的 find_pte_create 不同之处在于它不会试图分配物理页帧。一旦在多级页表上遍历 +遇到空指针它就会直接返回 None 表示无法正确找到传入的虚拟页号对应的页表项;

  • +
  • 第 28 行的 translate 调用 find_pte 来实现,如果能够找到页表项,那么它会将页表项拷贝一份并返回,否则就 +返回一个 None

  • +
+
+
+
+ +
+ +
+ +
+
+ + + + + + + + + \ No newline at end of file diff --git a/chapter4/5kernel-app-spaces.html b/chapter4/5kernel-app-spaces.html new file mode 100644 index 0000000..e8754f5 --- /dev/null +++ b/chapter4/5kernel-app-spaces.html @@ -0,0 +1,931 @@ + + + + + + + + 内核与应用的地址空间 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

内核与应用的地址空间

+

本节我们就在内核中通过基于页表的各种数据结构实现地址空间的抽象。

+
+

实现地址空间抽象

+
+

逻辑段:一段连续地址的虚拟内存

+

我们以逻辑段 MapArea 为单位描述一段连续地址的虚拟内存。所谓逻辑段,就是指地址区间中的一段实际可用(即 MMU 通过查多级页表 +可以正确完成地址转换)的地址连续的虚拟地址区间,该区间内包含的所有虚拟页面都以一种相同的方式映射到物理页帧,具有可读/可写/可执行等属性。

+
// os/src/mm/memory_set.rs
+
+pub struct MapArea {
+    vpn_range: VPNRange,
+    data_frames: BTreeMap<VirtPageNum, FrameTracker>,
+    map_type: MapType,
+    map_perm: MapPermission,
+}
+
+
+

其中 VPNRange 描述一段虚拟页号的连续区间,表示该逻辑段在地址区间中的位置和长度。它是一个迭代器,可以使用 Rust +的语法糖 for-loop 进行迭代。有兴趣的读者可以参考 os/src/mm/address.rs 中它的实现。

+
+

注解

+

Rust 语法卡片:迭代器 Iterator

+

Rust编程的迭代器模式允许你对一个序列的项进行某些处理。迭代器(iterator)是负责遍历序列中的每一项和决定序列何时结束的控制逻辑。 +对于如何使用迭代器处理元素序列和如何实现 Iterator trait 来创建自定义迭代器的内容, +可以参考 Rust 程序设计语言-中文版第十三章第二节

+
+

MapType 描述该逻辑段内的所有虚拟页面映射到物理页帧的同一种方式,它是一个枚举类型,在内核当前的实现中支持两种方式:

+
// 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 +四个标志位,因为其他的标志位仅与硬件的地址转换机制细节相关,这样的设计能避免引入错误的标志位。

+
// 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 类型来表示:

+
// os/src/mm/memory_set.rs
+
+pub struct MemorySet {
+    page_table: PageTable,
+    areas: Vec<MapArea>,
+}
+
+
+

它包含了该地址空间的多级页表 page_table 和一个逻辑段 MapArea 的向量 areas 。注意 PageTable 下 +挂着所有多级页表的节点所在的物理页帧,而每个 MapArea 下则挂着对应逻辑段中的数据所在的物理页帧,这两部分 +合在一起构成了一个地址空间所需的所有物理页帧。这同样是一种 RAII 风格,当一个地址空间 MemorySet 生命周期结束后, +这些物理页帧都会被回收。

+

地址空间 MemorySet 的方法如下:

+
 1// os/src/mm/memory_set.rs
+ 2
+ 3impl MemorySet {
+ 4    pub fn new_bare() -> Self {
+ 5        Self {
+ 6            page_table: PageTable::new(),
+ 7            areas: Vec::new(),
+ 8        }
+ 9    }
+10    fn push(&mut self, mut map_area: MapArea, data: Option<&[u8]>) {
+11        map_area.map(&mut self.page_table);
+12        if let Some(data) = data {
+13            map_area.copy_data(&mut self.page_table, data);
+14        }
+15        self.areas.push(map_area);
+16    }
+17    /// Assume that no conflicts.
+18    pub fn insert_framed_area(
+19        &mut self,
+20        start_va: VirtAddr, end_va: VirtAddr, permission: MapPermission
+21    ) {
+22        self.push(MapArea::new(
+23            start_va,
+24            end_va,
+25            MapType::Framed,
+26            permission,
+27        ), None);
+28    }
+29    pub fn new_kernel() -> Self;
+30    /// Include sections in elf and trampoline and TrapContext and user stack,
+31    /// also returns user_sp and entry point.
+32    pub fn from_elf(elf_data: &[u8]) -> (Self, usize, usize);
+33}
+
+
+
    +
  • 第 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 +提供的另外几个方法:

+
 1// os/src/mm/memory_set.rs
+ 2
+ 3impl MapArea {
+ 4    pub fn new(
+ 5        start_va: VirtAddr,
+ 6        end_va: VirtAddr,
+ 7        map_type: MapType,
+ 8        map_perm: MapPermission
+ 9    ) -> Self {
+10        let start_vpn: VirtPageNum = start_va.floor();
+11        let end_vpn: VirtPageNum = end_va.ceil();
+12        Self {
+13            vpn_range: VPNRange::new(start_vpn, end_vpn),
+14            data_frames: BTreeMap::new(),
+15            map_type,
+16            map_perm,
+17        }
+18    }
+19    pub fn map(&mut self, page_table: &mut PageTable) {
+20        for vpn in self.vpn_range {
+21            self.map_one(page_table, vpn);
+22        }
+23    }
+24    pub fn unmap(&mut self, page_table: &mut PageTable) {
+25        for vpn in self.vpn_range {
+26            self.unmap_one(page_table, vpn);
+27        }
+28    }
+29    /// data: start-aligned but maybe with shorter length
+30    /// assume that all frames were cleared before
+31    pub fn copy_data(&mut self, page_table: &mut PageTable, data: &[u8]) {
+32        assert_eq!(self.map_type, MapType::Framed);
+33        let mut start: usize = 0;
+34        let mut current_vpn = self.vpn_range.get_start();
+35        let len = data.len();
+36        loop {
+37            let src = &data[start..len.min(start + PAGE_SIZE)];
+38            let dst = &mut page_table
+39                .translate(current_vpn)
+40                .unwrap()
+41                .ppn()
+42                .get_bytes_array()[..src.len()];
+43            dst.copy_from_slice(src);
+44            start += PAGE_SIZE;
+45            if start >= len {
+46                break;
+47            }
+48            current_vpn.step();
+49        }
+50    }
+51}
+
+
+
    +
  • 第 4 行的 new 方法可以新建一个逻辑段结构体,注意传入的起始/终止虚拟地址会分别被下取整/上取整为虚拟页号并传入 +迭代器 vpn_range 中;

  • +
  • 第 19 行的 map 和第 24 行的 unmap 可以将当前逻辑段到物理内存的映射从传入的该逻辑段所属的地址空间的 +多级页表中加入或删除。可以看到它们的实现是遍历逻辑段中的所有虚拟页面,并以每个虚拟页面为单位依次在多级页表中进行 +键值对的插入或删除,分别对应 MapAreamap_oneunmap_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_oneunmap_one 。显然它们的实现取决于当前 +逻辑段被映射到物理内存的方式:

+
 1// os/src/mm/memory_set.rs
+ 2
+ 3impl MemoryArea {
+ 4    pub fn map_one(&mut self, page_table: &mut PageTable, vpn: VirtPageNum) {
+ 5        let ppn: PhysPageNum;
+ 6        match self.map_type {
+ 7            MapType::Identical => {
+ 8                ppn = PhysPageNum(vpn.0);
+ 9            }
+10            MapType::Framed => {
+11                let frame = frame_alloc().unwrap();
+12                ppn = frame.ppn;
+13                self.data_frames.insert(vpn, frame);
+14            }
+15        }
+16        let pte_flags = PTEFlags::from_bits(self.map_perm.bits).unwrap();
+17        page_table.map(vpn, ppn, pte_flags);
+18    }
+19    pub fn unmap_one(&mut self, page_table: &mut PageTable, vpn: VirtPageNum) {
+20        match self.map_type {
+21            MapType::Framed => {
+22                self.data_frames.remove(&vpn);
+23            }
+24            _ => {}
+25        }
+26        page_table.unmap(vpn);
+27    }
+28}
+
+
+
    +
  • 对于第 4 行的 map_one 来说,在虚拟页号 vpn 已经确定的情况下,它需要知道要将一个怎么样的页表项插入多级页表。 +页表项的标志位来源于当前逻辑段的类型为 MapPermission 的统一配置,只需将其转换为 PTEFlags ;而页表项的 +物理页号则取决于当前逻辑段映射到物理内存的方式:

    +
      +
    • 当以恒等映射 Identical 方式映射的时候,物理页号就等于虚拟页号;

    • +
    • 当以 Framed 方式映射的时候,需要分配一个物理页帧让当前的虚拟页面可以映射过去,此时页表项中的物理页号自然就是 +这个被分配的物理页帧的物理页号。此时还需要将这个物理页帧挂在逻辑段的 data_frames 字段下。

    • +
    +

    当确定了页表项的标志位和物理页号之后,即可调用多级页表 PageTablemap 接口来插入键值对。

    +
  • +
  • 对于第 19 行的 unmap_one 来说,基本上就是调用 PageTableunmap 接口删除以传入的虚拟页号为键的 +键值对即可。然而,当以 Framed 映射的时候,不要忘记同时将虚拟页面被映射到的物理页帧 FrameTracker 从 +data_frames 中移除,这样这个物理页帧才能立即被回收以备后续分配。

  • +
+
+
+
+

内核地址空间

+

在本章之前,内核和应用代码的访存地址都被视为一个物理地址直接访问物理内存,而在分页模式开启之后,它们都需要通过 MMU 的 +地址转换变成物理地址再交给 CPU 的访存单元去访问物理内存。地址空间抽象的重要意义在于 隔离 (Isolation) ,当我们 +在执行每个应用的代码的时候,内核需要控制 MMU 使用这个应用地址空间的多级页表进行地址转换。由于每个应用地址空间在创建 +的时候也顺带设置好了多级页表使得只有那些存放了它的数据的物理页帧能够通过该多级页表被映射到,这样它就只能访问自己的数据 +而无法触及其他应用或是内核的数据。

+

启用分页模式下,内核代码的访存地址也会被视为一个虚拟地址并需要经过 MMU 的地址转换,因此我们也需要为内核对应构造一个 +地址空间,它除了仍然需要允许内核的各数据段能够被正常访问之后,还需要包含所有应用的内核栈以及一个 +跳板 (Trampoline) 。我们会在本章的最后一节再深入介绍跳板的机制。

+

下图是软件看到的 64 位地址空间在 SV39 分页模式下实际可能通过 MMU 检查的最高 \(256\text{GiB}\) (之前在 +这里 中解释过最高和最低 \(256\text{GiB}\) 的问题):

+../_images/kernel-as-high.png +

可以看到,跳板放在最高的一个虚拟页面中。接下来则是从高到低放置每个应用的内核栈,内核栈的大小由 config 子模块的 +KERNEL_STACK_SIZE 给出。它们的映射方式为 MapPermission 中的 rw 两个标志位,意味着这个逻辑段仅允许 +CPU 处于内核态访问,且只能读或写。

+

注意相邻两个内核栈之间会预留一个 保护页面 (Guard Page) ,它是内核地址空间中的空洞,多级页表中并不存在与它相关的映射。 +它的意义在于当内核栈空间不足(如调用层数过多或死递归)的时候,代码会尝试访问 +空洞区域内的虚拟地址,然而它无法在多级页表中找到映射,便会触发异常,此时控制权会交给 trap handler 对这种情况进行 +处理。由于编译器会对访存顺序和局部变量在栈帧中的位置进行优化,我们难以确定一个已经溢出的栈帧中的哪些位置会先被访问, +但总的来说,空洞区域被设置的越大,我们就能越早捕获到这一错误并避免它覆盖其他重要数据。由于我们的内核非常简单且内核栈 +的大小设置比较宽裕,在当前的设计中我们仅将空洞区域的大小设置为单个页面。

+

下面则给出了内核地址空间的低 \(256\text{GiB}\) 的布局:

+../_images/kernel-as-low.png +

四个逻辑段 .text/.rodata/.data/.bss 被恒等映射到物理内存,这使得我们在无需调整内核内存布局 os/src/linker.ld +的情况下就仍能和启用页表机制之前那样访问内核的各数据段。注意我们借用页表机制对这些逻辑段的访问方式做出了限制,这都是为了 +在硬件的帮助下能够尽可能发现内核中的 bug ,在这里:

+
    +
  • 四个逻辑段的 U 标志位均未被设置,使得 CPU 只能在处于 S 特权级(或以上)时访问它们;

  • +
  • 代码段 .text 不允许被修改;

  • +
  • 只读数据段 .rodata 不允许被修改,也不允许从它上面取指;

  • +
  • .data/.bss 均允许被读写,但是不允许从它上面取指。

  • +
+

此外, 之前 提到过内核地址空间中需要存在一个恒等映射到内核数据段之外的可用物理 +页帧的逻辑段,这样才能在启用页表机制之后,内核仍能以纯软件的方式读写这些物理页帧。它们的标志位仅包含 rw ,意味着该 +逻辑段只能在 S 特权级以上访问,并且只能读写。

+

下面我们给出创建内核地址空间的方法 new_kernel

+
 1// os/src/mm/memory_set.rs
+ 2
+ 3extern "C" {
+ 4    fn stext();
+ 5    fn etext();
+ 6    fn srodata();
+ 7    fn erodata();
+ 8    fn sdata();
+ 9    fn edata();
+10    fn sbss_with_stack();
+11    fn ebss();
+12    fn ekernel();
+13    fn strampoline();
+14}
+15
+16impl MemorySet {
+17    /// Without kernel stacks.
+18    pub fn new_kernel() -> Self {
+19        let mut memory_set = Self::new_bare();
+20        // map trampoline
+21        memory_set.map_trampoline();
+22        // map kernel sections
+23        println!(".text [{:#x}, {:#x})", stext as usize, etext as usize);
+24        println!(".rodata [{:#x}, {:#x})", srodata as usize, erodata as usize);
+25        println!(".data [{:#x}, {:#x})", sdata as usize, edata as usize);
+26        println!(".bss [{:#x}, {:#x})", sbss_with_stack as usize, ebss as usize);
+27        println!("mapping .text section");
+28        memory_set.push(MapArea::new(
+29            (stext as usize).into(),
+30            (etext as usize).into(),
+31            MapType::Identical,
+32            MapPermission::R | MapPermission::X,
+33        ), None);
+34        println!("mapping .rodata section");
+35        memory_set.push(MapArea::new(
+36            (srodata as usize).into(),
+37            (erodata as usize).into(),
+38            MapType::Identical,
+39            MapPermission::R,
+40        ), None);
+41        println!("mapping .data section");
+42        memory_set.push(MapArea::new(
+43            (sdata as usize).into(),
+44            (edata as usize).into(),
+45            MapType::Identical,
+46            MapPermission::R | MapPermission::W,
+47        ), None);
+48        println!("mapping .bss section");
+49        memory_set.push(MapArea::new(
+50            (sbss_with_stack as usize).into(),
+51            (ebss as usize).into(),
+52            MapType::Identical,
+53            MapPermission::R | MapPermission::W,
+54        ), None);
+55        println!("mapping physical memory");
+56        memory_set.push(MapArea::new(
+57            (ekernel as usize).into(),
+58            MEMORY_END.into(),
+59            MapType::Identical,
+60            MapPermission::R | MapPermission::W,
+61        ), None);
+62        memory_set
+63    }
+64}
+
+
+

new_kernel 将映射跳板和地址空间中最低 \(256\text{GiB}\) 中的所有的逻辑段。第 3 行开始,我们从 +os/src/linker.ld 中引用了很多表示了各个段位置的符号,而后在 new_kernel 中,我们从低地址到高地址 +依次创建 5 个逻辑段并通过 push 方法将它们插入到内核地址空间中,上面我们已经详细介绍过这 5 个逻辑段。跳板 +是通过 map_trampoline 方法来映射的,我们也将在本章最后一节进行讲解。

+
+
+

应用地址空间

+

现在我们来介绍如何创建应用的地址空间。在前面的章节中,我们直接将丢弃所有符号的应用二进制镜像链接到内核,在初始化的时候 +内核仅需将他们加载到正确的初始物理地址就能使它们正确执行。但本章中,我们希望效仿内核地址空间的设计,同样借助页表机制 +使得应用地址空间的各个逻辑段也可以有不同的访问方式限制,这样可以提早检测出应用的错误并及时将其终止以最小化它对系统带来的 +恶劣影响。

+

在第三章中,每个应用链接脚本中的起始地址被要求是不同的,这样它们的代码和数据存放的位置才不会产生冲突。但是这是一种对于应用开发者 +极其不友好的设计。现在,借助地址空间的抽象,我们终于可以让所有应用程序都使用同样的起始地址,这也意味着所有应用可以使用同一个链接脚本了:

+
 1/* user/src/linker.ld */
+ 2
+ 3OUTPUT_ARCH(riscv)
+ 4ENTRY(_start)
+ 5
+ 6BASE_ADDRESS = 0x0;
+ 7
+ 8SECTIONS
+ 9{
+10    . = BASE_ADDRESS;
+11    .text : {
+12        *(.text.entry)
+13        *(.text .text.*)
+14    }
+15    . = ALIGN(4K);
+16    .rodata : {
+17        *(.rodata .rodata.*)
+18    }
+19    . = ALIGN(4K);
+20    .data : {
+21        *(.data .data.*)
+22    }
+23    .bss : {
+24        *(.bss .bss.*)
+25    }
+26    /DISCARD/ : {
+27        *(.eh_frame)
+28        *(.debug*)
+29    }
+30}
+
+
+

我们将起始地址 BASE_ADDRESS 设置为 \(\text{0x0}\) ,显然它只能是一个地址空间中的虚拟地址而非物理地址。 +事实上由于我们将入口汇编代码段放在最低的地方,这也是整个应用的入口点。 +我们只需清楚这一事实即可,而无需像之前一样将其硬编码到代码中。此外,在 .text.rodata 中间以及 .rodata 和 +.data 中间我们进行了页面对齐,因为前后两个逻辑段的访问方式限制是不同的,由于我们只能以页为单位对这个限制进行设置, +因此就只能将下一个逻辑段对齐到下一个页面开始放置。相对的, .data.bss 两个逻辑段由于限制相同,它们中间 +则无需进行页面对齐。

+

下图展示了应用地址空间的布局:

+../_images/app-as-full.png +

左侧给出了应用地址空间最低 \(256\text{GiB}\) 的布局:从 \(\text{0x0}\) 开始向高地址放置应用内存布局中的 +各个逻辑段,最后放置带有一个保护页面的用户栈。这些逻辑段都是以 Framed 方式映射到物理内存的,从访问方式上来说都加上 +了 U 标志位代表 CPU 可以在 U 特权级也就是执行应用代码的时候访问它们。右侧则给出了最高的 \(256\text{GiB}\) , +可以看出它只是和内核地址空间一样将跳板放置在最高页,还将 Trap 上下文放置在次高页中。这两个虚拟页面虽然位于应用地址空间, +但是它们并不包含 U 标志位,事实上它们在地址空间切换的时候才会发挥作用,请同样参考本章的最后一节。

+

os/src/build.rs 中,我们不再将丢弃了所有符号的应用二进制镜像链接进内核,而是直接使用 ELF 格式的可执行文件, +因为在前者中内存布局中各个逻辑段的位置和访问限制等信息都被裁剪掉了。而 loader 子模块也变得极其精简:

+
// 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 格式数据进行解析,找到各个逻辑段所在位置和访问 +限制并插入进来,最终得到一个完整的应用地址空间:

+
 1// os/src/mm/memory_set.rs
+ 2
+ 3impl MemorySet {
+ 4    /// Include sections in elf and trampoline and TrapContext and user stack,
+ 5    /// also returns user_sp and entry point.
+ 6    pub fn from_elf(elf_data: &[u8]) -> (Self, usize, usize) {
+ 7        let mut memory_set = Self::new_bare();
+ 8        // map trampoline
+ 9        memory_set.map_trampoline();
+10        // map program headers of elf, with U flag
+11        let elf = xmas_elf::ElfFile::new(elf_data).unwrap();
+12        let elf_header = elf.header;
+13        let magic = elf_header.pt1.magic;
+14        assert_eq!(magic, [0x7f, 0x45, 0x4c, 0x46], "invalid elf!");
+15        let ph_count = elf_header.pt2.ph_count();
+16        let mut max_end_vpn = VirtPageNum(0);
+17        for i in 0..ph_count {
+18            let ph = elf.program_header(i).unwrap();
+19            if ph.get_type().unwrap() == xmas_elf::program::Type::Load {
+20                let start_va: VirtAddr = (ph.virtual_addr() as usize).into();
+21                let end_va: VirtAddr = ((ph.virtual_addr() + ph.mem_size()) as usize).into();
+22                let mut map_perm = MapPermission::U;
+23                let ph_flags = ph.flags();
+24                if ph_flags.is_read() { map_perm |= MapPermission::R; }
+25                if ph_flags.is_write() { map_perm |= MapPermission::W; }
+26                if ph_flags.is_execute() { map_perm |= MapPermission::X; }
+27                let map_area = MapArea::new(
+28                    start_va,
+29                    end_va,
+30                    MapType::Framed,
+31                    map_perm,
+32                );
+33                max_end_vpn = map_area.vpn_range.get_end();
+34                memory_set.push(
+35                    map_area,
+36                    Some(&elf.input[ph.offset() as usize..(ph.offset() + ph.file_size()) as usize])
+37                );
+38            }
+39        }
+40        // map user stack with U flags
+41        let max_end_va: VirtAddr = max_end_vpn.into();
+42        let mut user_stack_bottom: usize = max_end_va.into();
+43        // guard page
+44        user_stack_bottom += PAGE_SIZE;
+45        let user_stack_top = user_stack_bottom + USER_STACK_SIZE;
+46        memory_set.push(MapArea::new(
+47            user_stack_bottom.into(),
+48            user_stack_top.into(),
+49            MapType::Framed,
+50            MapPermission::R | MapPermission::W | MapPermission::U,
+51        ), None);
+52        // map TrapContext
+53        memory_set.push(MapArea::new(
+54            TRAP_CONTEXT.into(),
+55            TRAMPOLINE.into(),
+56            MapType::Framed,
+57            MapPermission::R | MapPermission::W,
+58        ), None);
+59        (memory_set, user_stack_top, elf.header.pt2.entry_point() as usize)
+60    }
+61}
+
+
+
    +
  • 第 9 行,我们将跳板插入到应用地址空间;

  • +
  • 第 11 行,我们使用外部 crate xmas_elf 来解析传入的应用 ELF 数据并可以轻松取出各个部分。 +此前 我们简要介绍过 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/chapter4/6multitasking-based-on-as.html b/chapter4/6multitasking-based-on-as.html new file mode 100644 index 0000000..2639c02 --- /dev/null +++ b/chapter4/6multitasking-based-on-as.html @@ -0,0 +1,1015 @@ + + + + + + + + 基于地址空间的分时多任务 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

基于地址空间的分时多任务

+

本节我们介绍如何基于地址空间抽象来实现第三章的分时多任务系统。

+
+

建立并开启基于分页模式的虚拟地址空间

+

当 SBI 实现(本项目中基于 RustSBI)初始化完成后, CPU 将跳转到内核入口点并在 S 特权级上执行,此时还并没有开启分页模式 +,内核的每一次访存仍被视为一个物理地址直接访问物理内存。而在开启分页模式之后,内核的代码在访存的时候只能看到内核地址空间, +此时每次访存将被视为一个虚拟地址且需要通过 MMU 基于内核地址空间的多级页表的地址转换。这两种模式之间的过渡在内核初始化期间 +完成。

+
+

创建内核地址空间

+

我们创建内核地址空间的全局实例:

+
// os/src/mm/memory_set.rs
+
+lazy_static! {
+    pub static ref KERNEL_SPACE: Arc<UPSafeCell<MemorySet>> = Arc::new(unsafe {
+        UPSafeCell::new(MemorySet::new_kernel()
+    )});
+}
+
+
+

从之前对于 lazy_static! 宏的介绍可知, KERNEL_SPACE 在运行期间它第一次被用到时才会实际进行初始化,而它所 +占据的空间则是编译期被放在全局数据段中。 Arc<UPSafeCell<_>> 同时带来 Arc<T> 提供的共享 +引用,和 UPSafeCell<T> 提供的互斥访问。

+

rust_main 函数中,我们首先调用 mm::init 进行内存管理子系统的初始化:

+
// 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.exclusive_access().activate();
+}
+
+
+

可以看到,我们最先进行了全局动态内存分配器的初始化,因为接下来马上就要用到 Rust 的堆数据结构。接下来我们初始化物理页帧 +管理器(内含堆数据结构 Vec<T> )使能可用物理页帧的分配和回收能力。最后我们创建内核地址空间并让 CPU 开启分页模式, +MMU 在地址转换的时候使用内核的多级页表,这一切均在一行之内做到:

+
    +
  • 首先,我们引用 KERNEL_SPACE ,这是它第一次被使用,就在此时它会被初始化,调用 MemorySet::new_kernel +创建一个内核地址空间并使用 Arc<UPSafeCell<T>> 包裹起来;

  • +
  • 最然后,我们调用 MemorySet::activate

    +
    +
     1// os/src/mm/page_table.rs
    + 2
    + 3pub fn token(&self) -> usize {
    + 4    8usize << 60 | self.root_ppn.0
    + 5}
    + 6
    + 7// os/src/mm/memory_set.rs
    + 8
    + 9impl MemorySet {
    +10    pub fn activate(&self) {
    +11        let satp = self.page_table.token();
    +12        unsafe {
    +13            satp::write(satp);
    +14            core::arch::asm!("sfence.vma");
    +15        }
    +16    }
    +17}
    +
    +
    +
    +

    PageTable::token 会按照 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 为例,不考虑大页)最顺利的情况下也需要三次访存。这些 +额外的访存和真正访问数据的那些访存在空间上并不相邻,加大了多级缓存的压力,一旦缓存缺失将带来巨大的性能惩罚。如果采用 +多级页表实现,这个问题会变得更为严重,使得地址空间抽象的性能开销过大。

+

为了解决性能问题,一种常见的做法是在 CPU 中利用部分硬件资源额外加入一个 快表 +(TLB, Translation Lookaside Buffer) , 它维护了部分虚拟页号到页表项的键值对。当 MMU 进行地址转换的时候,首先 +会到快表中看看是否匹配,如果匹配的话直接取出页表项完成地址转换而无需访存;否则再去查页表并将键值对保存在快表中。一旦 +我们修改了 satp 切换了地址空间,快表中的键值对就会失效,因为它还表示着上个地址空间的映射关系。为了 MMU 的地址转换 +能够及时与 satp 的修改同步,我们可以选择立即使用 sfence.vma 指令将快表清空,这样 MMU 就不会看到快表中已经 +过期的键值对了。

+
+
+
+

跳板的实现

+

上一小节我们看到无论是内核还是应用的地址空间,最高的虚拟页面都是一个跳板。同时应用地址空间的次高虚拟页面还被设置为用来 +存放应用的 Trap 上下文。那么跳板究竟起什么作用呢?为何不直接把 Trap 上下文仍放到应用的内核栈中呢?

+

回忆曾在第二章介绍过的,当一个应用 Trap 到内核的时候, +sscratch 已经指出了该应用内核栈的栈顶,我们用一条指令即可从用户栈切换到内核栈,然后直接将 Trap 上下文压入内核栈 +栈顶。当 Trap 处理完毕返回用户态的时候,将 Trap 上下文中的内容恢复到寄存器上,最后将保存着应用用户栈顶的 sscratch +与 sp 进行交换,也就从内核栈切换回了用户栈。在这个过程中, sscratch 起到了非常关键的作用,它使得我们可以在不破坏 +任何通用寄存器的情况下完成用户栈和内核栈顶的 Trap 上下文这两个工作区域之间的切换。

+

然而,一旦使能了分页机制,一切就并没有这么简单了,我们必须在这个过程中同时完成地址空间的切换。 +具体来说,当 __alltraps 保存 Trap 上下文的时候,我们必须通过修改 satp 从应用地址空间切换到内核地址空间, +因为 trap handler 只有在内核地址空间中才能访问; +同理,在 __restore 恢复 Trap 上下文的时候,我们也必须从内核地址空间切换回应用地址空间,因为应用的代码和 +数据只能在它自己的地址空间中才能访问,内核地址空间是看不到的。 +进而,地址空间的切换不能影响指令的连续执行,这就要求应用和内核地址空间在切换地址空间指令附近是平滑的。

+
+

注解

+

内核与应用地址空间的隔离

+

目前我们的设计是有一个唯一的内核地址空间存放内核的代码、数据,同时对于每个应用维护一个它们自己的地址空间,因此在 +Trap 的时候就需要进行地址空间切换,而在任务切换的时候无需进行(因为这个过程全程在内核内完成)。而教程前两版以及 +\(\mu\) core 中的设计是每个应用都有一个地址空间,可以将其中的逻辑段分为内核和用户两部分,分别映射到内核和 +用户的数据和代码,且分别在 CPU 处于 S/U 特权级时访问。此设计中并不存在一个单独的内核地址空间。

+

之前设计方式的优点在于: Trap 的时候无需切换地址空间,而在任务切换的时候才需要切换地址空间。由于后者比前者更容易 +实现,这降低了实现的复杂度。而且在应用高频进行系统调用的时候能够避免地址空间切换的开销,这通常源于快表或 cache +的失效问题。但是这种设计方式也有缺点:即内核的逻辑段需要在每个应用的地址空间内都映射一次,这会带来一些无法忽略的 +内存占用开销,并显著限制了嵌入式平台的任务并发数。此外,这种做法无法应对处理器的 熔断 +(Meltdown) 漏洞 , +使得恶意应用能够以某种方式看到它本来无权访问的地址空间中内核部分的数据。将内核与地址空间隔离便是修复此漏洞的一种方法。

+

经过权衡,在本教程中我们参考 MIT 的教学 OS xv6 , +采用内核和应用地址空间隔离的设计。

+
+

我们为何将应用的 Trap 上下文放到应用地址空间的次高页面而不是内核地址空间中的内核栈中呢?原因在于,假如我们将其放在内核栈 +中,在保存 Trap 上下文之前我们必须先切换到内核地址空间,这就需要我们将内核地址空间的 token 写入 satp 寄存器,之后我们 +还需要有一个通用寄存器保存内核栈栈顶的位置,这样才能以它为基址保存 Trap 上下文。在保存 Trap 上下文之前我们必须完成这 +两项工作。然而,我们无法在不破坏任何一个通用寄存器的情况下做到这一点。因为事实上我们需要用到内核的两条信息:内核地址空间 +的 token 还有应用内核栈顶的位置,硬件却只提供一个 sscratch 可以用来进行周转。所以,我们不得不将 Trap 上下文保存在 +应用地址空间的一个虚拟页面中以避免切换到内核地址空间才能保存。

+

为了方便实现,我们在 Trap 上下文中包含更多内容(和我们关于上下文的定义有些不同,它们在初始化之后便只会被读取而不会被写入 +,并不是每次都需要保存/恢复):

+
 1// os/src/trap/context.rs
+ 2
+ 3#[repr(C)]
+ 4pub struct TrapContext {
+ 5    pub x: [usize; 32],
+ 6    pub sstatus: Sstatus,
+ 7    pub sepc: usize,
+ 8    pub kernel_satp: usize,
+ 9    pub kernel_sp: usize,
+10    pub trap_handler: usize,
+11}
+
+
+

在多出的三个字段中:

+
    +
  • kernel_satp 表示内核地址空间的 token ;

  • +
  • kernel_sp 表示当前应用在内核地址空间中的内核栈栈顶的虚拟地址;

  • +
  • trap_handler 表示内核中 trap handler 入口点的虚拟地址。

  • +
+

它们在应用初始化的时候由内核写入应用地址空间中的 TrapContext 的相应位置,此后就不再被修改。

+

让我们来看一下现在的 __alltraps__restore 各是如何在保存和恢复 Trap 上下文的同时也切换地址空间的:

+
 1# os/src/trap/trap.S
+ 2
+ 3    .section .text.trampoline
+ 4    .globl __alltraps
+ 5    .globl __restore
+ 6    .align 2
+ 7__alltraps:
+ 8    csrrw sp, sscratch, sp
+ 9    # now sp->*TrapContext in user space, sscratch->user stack
+10    # save other general purpose registers
+11    sd x1, 1*8(sp)
+12    # skip sp(x2), we will save it later
+13    sd x3, 3*8(sp)
+14    # skip tp(x4), application does not use it
+15    # save x5~x31
+16    .set n, 5
+17    .rept 27
+18        SAVE_GP %n
+19        .set n, n+1
+20    .endr
+21    # we can use t0/t1/t2 freely, because they have been saved in TrapContext
+22    csrr t0, sstatus
+23    csrr t1, sepc
+24    sd t0, 32*8(sp)
+25    sd t1, 33*8(sp)
+26    # read user stack from sscratch and save it in TrapContext
+27    csrr t2, sscratch
+28    sd t2, 2*8(sp)
+29    # load kernel_satp into t0
+30    ld t0, 34*8(sp)
+31    # load trap_handler into t1
+32    ld t1, 36*8(sp)
+33    # move to kernel_sp
+34    ld sp, 35*8(sp)
+35    # switch to kernel space
+36    csrw satp, t0
+37    sfence.vma
+38    # jump to trap_handler
+39    jr t1
+40
+41__restore:
+42    # a0: *TrapContext in user space(Constant); a1: user space token
+43    # switch to user space
+44    csrw satp, a1
+45    sfence.vma
+46    csrw sscratch, a0
+47    mv sp, a0
+48    # now sp points to TrapContext in user space, start restoring based on it
+49    # restore sstatus/sepc
+50    ld t0, 32*8(sp)
+51    ld t1, 33*8(sp)
+52    csrw sstatus, t0
+53    csrw sepc, t1
+54    # restore general purpose registers except x0/sp/tp
+55    ld x1, 1*8(sp)
+56    ld x3, 3*8(sp)
+57    .set n, 5
+58    .rept 27
+59        LOAD_GP %n
+60        .set n, n+1
+61    .endr
+62    # back to user stack
+63    ld sp, 2*8(sp)
+64    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 段,并在调整内存布局的时候将它对齐到代码段的一个页面中:

+
 1# os/src/linker.ld
+ 2
+ 3    stext = .;
+ 4    .text : {
+ 5        *(.text.entry)
+ 6+        . = ALIGN(4K);
+ 7+        strampoline = .;
+ 8+        *(.text.trampoline);
+ 9+        . = ALIGN(4K);
+10        *(.text .text.*)
+11    }
+
+
+

这样,这段汇编代码放在一个物理页帧中,且 __alltraps 恰好位于这个物理页帧的开头,其物理地址被外部符号 +strampoline 标记。在开启分页模式之后,内核和应用代码都只能看到各自的虚拟地址空间,而在它们的视角中,这段汇编代码 +被放在它们地址空间的最高虚拟页面上,由于这段汇编代码在执行的时候涉及到地址空间切换,故而被称为跳板页面。

+

那么在产生trap前后的一小段时间内会有一个比较 极端 的情况,即刚产生trap时,CPU已经进入了内核态(即Supervisor Mode), +但此时执行代码和访问数据还是在应用程序所处的用户态虚拟地址空间中,而不是我们通常理解的内核虚拟地址空间。在这段特殊的时间内,CPU指令 +为什么能够被连续执行呢?这里需要注意:无论是内核还是应用的地址空间,跳板的虚拟页均位于同样位置,且它们也将会映射到同一个实际存放这段 +汇编代码的物理页帧。也就是说,在执行 __alltraps__restore 函数进行地址空间切换的时候, +应用的用户态虚拟地址空间和操作系统内核的内核态虚拟地址空间对切换地址空间的指令所在页的映射方式均是相同的, +这就说明了这段切换地址空间的指令控制流仍是可以连续执行的。

+

现在可以说明我们在创建用户/内核地址空间中用到的 map_trampoline 是如何实现的了:

+
 1// os/src/config.rs
+ 2
+ 3pub const TRAMPOLINE: usize = usize::MAX - PAGE_SIZE + 1;
+ 4
+ 5// os/src/mm/memory_set.rs
+ 6
+ 7impl MemorySet {
+ 8    /// Mention that trampoline is not collected by areas.
+ 9    fn map_trampoline(&mut self) {
+10        self.page_table.map(
+11            VirtAddr::from(TRAMPOLINE).into(),
+12            PhysAddr::from(strampoline as usize).into(),
+13            PTEFlags::R | PTEFlags::X,
+14        );
+15    }
+16}
+
+
+

这里我们为了实现方便并没有新增逻辑段 MemoryArea 而是直接在多级页表中插入一个从地址空间的最高虚拟页面映射到 +跳板汇编代码所在的物理页帧的键值对,访问方式限制与代码段相同,即 RX 。

+

最后可以解释为何我们在 __alltraps 中需要借助寄存器 jr 而不能直接 call trap_handler 了。因为在 +内存布局中,这条 .text.trampoline 段中的跳转指令和 trap_handler 都在代码段之内,汇编器(Assembler) +和链接器(Linker)会根据 linker.ld 的地址布局描述,设定电子指令的地址,并计算二者地址偏移量 +并让跳转指令的实际效果为当前 pc 自增这个偏移量。但实际上我们知道由于我们设计的缘故,这条跳转指令在被执行的时候, +它的虚拟地址被操作系统内核设置在地址空间中的最高页面之内,加上这个偏移量并不能正确的得到 trap_handler 的入口地址。

+

问题的本质可以概括为:跳转指令实际被执行时的虚拟地址和在编译器/汇编器/链接器进行后端代码生成和链接形成最终机器码时设置此指令的地址是不同的。

+
+
+

加载和执行应用程序

+
+

扩展任务控制块

+

为了让应用在运行时有一个安全隔离且符合编译器给应用设定的地址空间布局的虚拟地址空间,操作系统需要对任务进行更多的管理,所以任务控制块相比第三章也包含了更多内容:

+
1// os/src/task/task.rs
+2
+3pub struct TaskControlBlock {
+4    pub task_status: TaskStatus,
+5    pub task_cx: TaskContext,
+6    pub memory_set: MemorySet,
+7    pub trap_cx_ppn: PhysPageNum,
+8    pub base_size: usize,
+9}
+
+
+

除了应用的地址空间 memory_set 之外,还有位于应用地址空间次高页的 Trap 上下文被实际存放在物理页帧的物理页号 +trap_cx_ppn ,它能够方便我们对于 Trap 上下文进行访问。此外, base_size 统计了应用数据的大小,也就是 +在应用地址空间中从 \(\text{0x0}\) 开始到用户栈结束一共包含多少字节。它后续还应该包含用于应用动态内存分配的 +堆空间的大小,但我们暂不支持。

+
+
+

更新对任务控制块的管理

+

下面是任务控制块的创建:

+
 1// os/src/config.rs
+ 2
+ 3/// Return (bottom, top) of a kernel stack in kernel space.
+ 4pub fn kernel_stack_position(app_id: usize) -> (usize, usize) {
+ 5    let top = TRAMPOLINE - app_id * (KERNEL_STACK_SIZE + PAGE_SIZE);
+ 6    let bottom = top - KERNEL_STACK_SIZE;
+ 7    (bottom, top)
+ 8}
+ 9
+10// os/src/task/task.rs
+11
+12impl TaskControlBlock {
+13    pub fn new(elf_data: &[u8], app_id: usize) -> Self {
+14        // memory_set with elf program headers/trampoline/trap context/user stack
+15        let (memory_set, user_sp, entry_point) = MemorySet::from_elf(elf_data);
+16        let trap_cx_ppn = memory_set
+17            .translate(VirtAddr::from(TRAP_CONTEXT).into())
+18            .unwrap()
+19            .ppn();
+20        let task_status = TaskStatus::Ready;
+21        // map a kernel-stack in kernel space
+22        let (kernel_stack_bottom, kernel_stack_top) = kernel_stack_position(app_id);
+23        KERNEL_SPACE
+24            .exclusive_access()
+25            .insert_framed_area(
+26                kernel_stack_bottom.into(),
+27                kernel_stack_top.into(),
+28                MapPermission::R | MapPermission::W,
+29        );
+30        let task_control_block = Self {
+31            task_status,
+32            task_cx: TaskContext::goto_trap_return(kernel_stack_top),
+33            memory_set,
+34            trap_cx_ppn,
+35            base_size: user_sp,
+36        };
+37        // prepare TrapContext in user space
+38        let trap_cx = task_control_block.get_trap_cx();
+39        *trap_cx = TrapContext::app_init_context(
+40            entry_point,
+41            user_sp,
+42            KERNEL_SPACE.exclusive_access().token(),
+43            kernel_stack_top,
+44            trap_handler as usize,
+45        );
+46        task_control_block
+47    }
+48}
+
+
+
    +
  • 第 15 行,我们解析传入的 ELF 格式数据构造应用的地址空间 memory_set 并获得其他信息;

  • +
  • 第 16 行,我们从地址空间 memory_set 中查多级页表找到应用地址空间中的 Trap 上下文实际被放在哪个物理页帧;

  • +
  • 第 22 行,我们根据传入的应用 ID app_id 调用在 config 子模块中定义的 kernel_stack_position 找到 +应用的内核栈预计放在内核地址空间 KERNEL_SPACE 中的哪个位置,并通过 insert_framed_area 实际将这个逻辑段 +加入到内核地址空间中;

  • +
+
    +
  • 我们在应用的内核栈顶压入一个跳转到 trap_return 而不是 __restore 的任务上下文, +这主要是为了能够支持对该应用的启动并顺利切换到用户地址空间执行。在构造方式上,只是将 ra 寄存器的值设置为 +trap_return 的地址。 trap_return 是我们后面要介绍的新版的 Trap 处理的一部分。

  • +
  • 初始化该应用的 Trap 上下文,由于它是在应用地址空间而不是在内核地址空间中,我们只能手动查页表找到 +Trap 上下文实际被放在的物理页帧,再获得在用户空间的 Trap 上下文的可变引用用于初始化:

    +
    // 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`` 泛型函数的特定版本实现。

    +
     1// os/src/trap/context.rs
    + 2
    + 3impl TrapContext {
    + 4    pub fn set_sp(&mut self, sp: usize) { self.x[2] = sp; }
    + 5    pub fn app_init_context(
    + 6        entry: usize,
    + 7        sp: usize,
    + 8        kernel_satp: usize,
    + 9        kernel_sp: usize,
    +10        trap_handler: usize,
    +11    ) -> Self {
    +12        let mut sstatus = sstatus::read();
    +13        sstatus.set_spp(SPP::User);
    +14        let mut cx = Self {
    +15            x: [0; 32],
    +16            sstatus,
    +17            sepc: entry,
    +18            kernel_satp,
    +19            kernel_sp,
    +20            trap_handler,
    +21        };
    +22        cx.set_sp(sp);
    +23        cx
    +24    }
    +25}
    +
    +
    +

    和之前相比 TrapContext::app_init_context 需要补充上让应用在 __alltraps 能够顺利进入到内核地址空间 +并跳转到 trap handler 入口点的相关信息。

    +
  • +
+

在内核初始化的时候,需要将所有的应用加载到全局应用管理器中:

+
 1// os/src/task/mod.rs
+ 2
+ 3struct TaskManagerInner {
+ 4    tasks: Vec<TaskControlBlock>,
+ 5    current_task: usize,
+ 6}
+ 7
+ 8lazy_static! {
+ 9    pub static ref TASK_MANAGER: TaskManager = {
+10        info!("init TASK_MANAGER");
+11        let num_app = get_num_app();
+12        info!("num_app = {}", num_app);
+13        let mut tasks: Vec<TaskControlBlock> = Vec::new();
+14        for i in 0..num_app {
+15            tasks.push(TaskControlBlock::new(get_app_data(i), i));
+16        }
+17        TaskManager {
+18            num_app,
+19            inner: unsafe {
+20                UPSafeCell::new(TaskManagerInner {
+21                    tasks,
+22                    current_task: 0,
+23                })
+24            },
+25        }
+26    };
+27}
+
+
+

可以看到,在 TaskManagerInner 中我们使用向量 Vec 来保存任务控制块。在全局任务管理器 TASK_MANAGER +初始化的时候,只需使用 loader 子模块提供的 get_num_appget_app_data 分别获取链接到内核的应用 +数量和每个应用的 ELF 文件格式的数据,然后依次给每个应用创建任务控制块并加入到向量中即可。我们还将 current_task 设置 +为 0 ,于是将从第 0 个应用开始执行。

+

回过头来介绍一下应用构建器 os/build.rs 的改动:

+
    +
  • 首先,我们在 .incbin 中不再插入清除全部符号的应用二进制镜像 *.bin ,而是将构建得到的 ELF 格式文件直接链接进来;

  • +
  • 其次,在链接每个 ELF 格式文件之前我们都加入一行 .align 3 来确保它们对齐到 8 字节,这是由于如果不这样做, +xmas-elf crate 可能会在解析 ELF 的时候进行不对齐的内存读写,例如使用 ld 指令从内存的一个没有对齐到 8 字节的地址加载一个 64 位的值到一个通用寄存器。

  • +
+

为了方便后续的实现,全局任务管理器还需要提供关于当前应用与地址空间有关的一些信息。通过 current_user_token 和 +current_trap_cx 分别可以获得当前正在执行的应用的地址空间的 token 和可以在 +内核地址空间中修改位于该应用地址空间中的 Trap 上下文的可变引用。

+
+
+
+

改进 Trap 处理的实现

+

为了能够支持地址空间,让我们来看现在 trap_handler 的改进实现:

+
 1// os/src/trap/mod.rs
+ 2
+ 3fn set_kernel_trap_entry() {
+ 4    unsafe {
+ 5        stvec::write(trap_from_kernel as usize, TrapMode::Direct);
+ 6    }
+ 7}
+ 8
+ 9#[no_mangle]
+10pub fn trap_from_kernel() -> ! {
+11    panic!("a trap from kernel!");
+12}
+13
+14#[no_mangle]
+15pub fn trap_handler() -> ! {
+16    set_kernel_trap_entry();
+17    let cx = current_trap_cx();
+18    let scause = scause::read();
+19    let stval = stval::read();
+20    match scause.cause() {
+21        ...
+22    }
+23    trap_return();
+24}
+
+
+

由于应用的 Trap 上下文不在内核地址空间,因此我们调用 current_trap_cx 来获取当前应用的 Trap 上下文的可变引用 +而不是像之前那样作为参数传入 trap_handler 。至于 Trap 处理的过程则没有发生什么变化。

+

注意到,在 trap_handler 的开头还调用 set_kernel_trap_entrystvec 修改为同模块下另一个函数 +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 返回用户态:

+
 1// os/src/trap/mod.rs
+ 2
+ 3fn set_user_trap_entry() {
+ 4    unsafe {
+ 5        stvec::write(TRAMPOLINE as usize, TrapMode::Direct);
+ 6    }
+ 7}
+ 8
+ 9#[no_mangle]
+10pub fn trap_return() -> ! {
+11    set_user_trap_entry();
+12    let trap_cx_ptr = TRAP_CONTEXT;
+13    let user_satp = current_user_token();
+14    extern "C" {
+15        fn __alltraps();
+16        fn __restore();
+17    }
+18    let restore_va = __restore as usize - __alltraps as usize + TRAMPOLINE;
+19    unsafe {
+20            core::arch::asm!(
+21            "fence.i",
+22            "jr {restore_va}",
+23            restore_va = in(reg) restore_va,
+24            in("a0") trap_cx_ptr,
+25            in("a1") user_satp,
+26            options(noreturn)
+27        );
+28    }
+29    panic!("Unreachable in back_to_user!");
+30}
+
+
+
    +
  • 第 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 都是指编译器在链接时看到的内核内存布局中的地址。我们使用 jr 指令完成了跳转的任务。

  • +
  • 在开始执行应用之前,我们需要使用 fence.i 指令清空指令缓存 i-cache 。这是因为,在内核中进行的一些操作 +可能导致一些原先存放某个应用代码的物理页帧如今用来存放数据或者是其他应用的代码,i-cache 中可能还保存着该物理页帧的 +错误快照。因此我们直接将整个 i-cache 清空避免错误。

  • +
+
+
+

改进 sys_write 的实现

+

同样由于内核和应用地址空间的隔离, sys_write 不再能够直接访问位于应用空间中的数据,而需要手动查页表才能知道那些 +数据被放置在哪些物理页帧上并进行访问。

+

为此,页表模块 page_table 提供了将应用地址空间中一个缓冲区转化为在内核空间中能够直接访问的形式的辅助函数:

+
 1// os/src/mm/page_table.rs
+ 2
+ 3pub fn translated_byte_buffer(
+ 4    token: usize,
+ 5    ptr: *const u8,
+ 6    len: usize
+ 7) -> Vec<&'static [u8]> {
+ 8    let page_table = PageTable::from_token(token);
+ 9    let mut start = ptr as usize;
+10    let end = start + len;
+11    let mut v = Vec::new();
+12    while start < end {
+13        let start_va = VirtAddr::from(start);
+14        let mut vpn = start_va.floor();
+15        let ppn = page_table
+16            .translate(vpn)
+17            .unwrap()
+18            .ppn();
+19        vpn.step();
+20        let mut end_va: VirtAddr = vpn.into();
+21        end_va = end_va.min(VirtAddr::from(end));
+22        v.push(&ppn.get_bytes_array()[start_va.page_offset()..end_va.page_offset()]);
+23        start = end_va.into();
+24    }
+25    v
+26}
+
+
+

参数中的 token 是某个应用地址空间的 token , ptrlen 则分别表示该地址空间中的一段缓冲区的起始地址 +和长度。 translated_byte_buffer 会以向量的形式返回一组可以在内核空间中直接访问的字节数组切片,具体实现在这里 +不再赘述。

+

进而,我们完成对 sys_write 系统调用的改造:

+
// 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 然后输出即可。

+
+
+ +
+ +
+ +
+
+ + + + + + + + + \ No newline at end of file diff --git a/chapter4/7exercise.html b/chapter4/7exercise.html new file mode 100644 index 0000000..82c46ba --- /dev/null +++ b/chapter4/7exercise.html @@ -0,0 +1,546 @@ + + + + + + + + chapter4练习 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

chapter4练习

+
+

Lab2 编程作业

+
+

重写 sys_get_time 和 sys_task_info

+

引入虚存机制后,原来内核的 sys_get_time 和 sys_task_info 函数实现就无效了。请你重写这个函数,恢复其正常功能。

+
+
+

mmap 和 munmap 匿名映射

+

mmap 在 Linux 中主要用于在内存中映射文件, +本次实验简化它的功能,仅用于申请内存。

+

请实现 mmap 和 munmap 系统调用,mmap 定义如下:

+
fn sys_mmap(start: usize, len: usize, port: usize) -> isize
+
+
+
    +
  • syscall ID:222

  • +
  • 申请长度为 len 字节的物理内存(不要求实际物理内存位置,可以随便找一块),将其映射到 start 开始的虚存,内存页属性为 port

  • +
  • +
    参数:
      +
    • start 需要映射的虚存起始地址,要求按页对齐

    • +
    • len 映射字节长度,可以为 0

    • +
    • port:第 0 位表示是否可读,第 1 位表示是否可写,第 2 位表示是否可执行。其他位无效且必须为 0

    • +
    +
    +
    +
  • +
  • 返回值:执行成功则返回 0,错误返回 -1

  • +
  • +
    说明:
      +
    • 为了简单,目标虚存区间要求按页对齐,len 可直接按页向上取整,不考虑分配失败时的页回收。

    • +
    +
    +
    +
  • +
  • +
    可能的错误:
      +
    • start 没有按页大小对齐

    • +
    • port & !0x7 != 0 (port 其余位必须为0)

    • +
    • port & 0x7 = 0 (这样的内存无意义)

    • +
    • [start, start + len) 中存在已经被映射的页

    • +
    • 物理内存不足

    • +
    +
    +
    +
  • +
+

munmap 定义如下:

+
fn sys_munmap(start: usize, len: usize) -> isize
+
+
+
    +
  • syscall ID:215

  • +
  • 取消到 [start, start + len) 虚存的映射

  • +
  • 参数和返回值请参考 mmap

  • +
  • +
    说明:
      +
    • 为了简单,参数错误时不考虑内存的恢复和回收。

    • +
    +
    +
    +
  • +
  • +
    可能的错误:
      +
    • [start, start + len) 中存在未被映射的虚存。

    • +
    +
    +
    +
  • +
+

tips:

+
    +
  • 一定要注意 mmap 是的页表项,注意 riscv 页表项的格式与 port 的区别。

  • +
  • 你增加 PTE_U 了吗?

  • +
+
+
+

实验要求

+
    +
  • lab2(os4)参考框架:

  • +
  • os4 目录下,实现 mmap 和 munmap 两个系统调用,通过所有测例。

  • +
  • 报告命名 lab2.md,位于 reports 目录下

  • +
+

TIPS:注意 port 参数的语义,它与内核定义的 MapPermission 有明显不同!

+
+
+
+

问答作业

+
    +
  1. 请列举 SV39 页表页表项的组成,描述其中的标志位有何作用?

  2. +
  3. +
    缺页

    缺页指的是进程访问页面时页面不在页表中或在页表中无效的现象,此时 MMU 将会返回一个中断, +告知 os 进程内存访问出了问题。os 选择填补页表并重新执行异常指令或者杀死进程。

    +
      +
    • 请问哪些异常可能是缺页导致的?

    • +
    • 发生缺页时,描述相关重要寄存器的值,上次实验描述过的可以简略。

    • +
    +

    缺页有两个常见的原因,其一是 Lazy 策略,也就是直到内存页面被访问才实际进行页表操作。 +比如,一个程序被执行时,进程的代码段理论上需要从磁盘加载到内存。但是 os 并不会马上这样做, +而是会保存 .text 段在磁盘的位置信息,在这些代码第一次被执行时才完成从磁盘的加载操作。

    +
      +
    • 这样做有哪些好处?

    • +
    +

    其实,我们的 mmap 也可以采取 Lazy 策略,比如:一个用户进程先后申请了 10G 的内存空间, +然后用了其中 1M 就直接退出了。按照现在的做法,我们显然亏大了,进行了很多没有意义的页表操作。

    +
      +
    • 处理 10G 连续的内存页面,对应的 SV39 页表大致占用多少内存 (估算数量级即可)?

    • +
    • 请简单思考如何才能实现 Lazy 策略,缺页时又如何处理?描述合理即可,不需要考虑实现。

    • +
    +

    缺页的另一个常见原因是 swap 策略,也就是内存页面可能被换到磁盘上了,导致对应页面失效。

    +
      +
    • 此时页面失效如何表现在页表项(PTE)上?

    • +
    +
    +
    +
  4. +
  5. 双页表与单页表

    +

    为了防范侧信道攻击,我们的 os 使用了双页表。但是传统的设计一直是单页表的,也就是说, +用户线程和对应的内核线程共用同一张页表,只不过内核对应的地址只允许在内核态访问。 +(备注:这里的单/双的说法仅为自创的通俗说法,并无这个名词概念,详情见 KPTI )

    +
      +
    • 在单页表情况下,如何更换页表?

    • +
    • 单页表情况下,如何控制用户态无法访问内核页面?(tips:看看上一题最后一问)

    • +
    • 单页表有何优势?(回答合理即可)

    • +
    • 双页表实现下,何时需要更换页表?假设你写一个单页表操作系统,你会选择何时更换页表(回答合理即可)?

    • +
    +
  6. +
+
+
+

报告要求

+
    +
  • 简单总结你实现的功能(200字以内,不要贴代码)。

  • +
  • 完成问答题。

  • +
  • (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。

  • +
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter4/index.html b/chapter4/index.html new file mode 100644 index 0000000..8ab6503 --- /dev/null +++ b/chapter4/index.html @@ -0,0 +1,454 @@ + + + + + + + + 第四章:地址空间 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+ + +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter5/0intro.html b/chapter5/0intro.html new file mode 100644 index 0000000..ed496df --- /dev/null +++ b/chapter5/0intro.html @@ -0,0 +1,549 @@ + + + + + + + + 引言 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

引言

+
+

本章导读

+
+

注解

+

基于github classroom的开发方式

+

基于github classroom,可方便建立开发用的git repository,并可基于github的 codespace (在线版ubuntu +vscode)在线开发使用。整个开发环境仅仅需要一个网络浏览器。

+
    +
  1. 在网络浏览器中用自己的 github id 登录 github.com

  2. +
  3. 接收 第三个实验(os5)的github classroom在线邀请 ,根据提示一路选择OK即可。

  4. +
  5. 完成第二步后,你的第三个实验的 github repository 会被自动建立好,点击此github repository的链接,就可看到你要完成的第一个实验了。

  6. +
  7. 在你的第三个实验的网页的中上部可以看到一个醒目的 code 绿色按钮,点击后,可以进一步看到 codespace 标签和醒目的 create codesapce on main 绿色按钮。请点击这个绿色按钮,就可以进入到在线的ubuntu +vscode环境中

  8. +
  9. 再按照下面的环境安装提示在vscode的 console 中安装配置开发环境:rustc,qemu等工具。

  10. +
  11. 在vscode的 console 中执行 make setupclassroom_test5 (该命令仅执行一次)配置githubclassroom 自动评分功能。

  12. +
  13. 然后就可以基于在线vscode进行开发、运行、提交等完整的实验过程了。

  14. +
+

上述的3,4,5步不是必须的,你也可以线下本地开发。

+
+

我们将开发一个用户 终端 (Terminal) 或 命令行 (Command Line Application, 俗称 Shell ) , +形成用户与操作系统进行交互的命令行界面 (Command Line Interface)。

+

为此,我们要对任务建立新的抽象: 进程 ,并实现若干基于 进程 的强大系统调用。

+
+

注解

+

任务和进程的关系与区别

+

第三章提到的 任务 是这里提到的 进程 的初级阶段,与任务相比,进程能在运行中创建 子进程 、 +用新的 程序 内容覆盖已有的 程序 内容、可管理更多物理或虚拟 资源

+
+
+
+

实践体验

+

获取本章代码:

+
$ git clone https://github.com/LearningOS/rust-based-os-comp2022.git
+$ cd rust-based-os-comp2022/
+$ make setupclassroom_test5  //注意:这一步很重要,是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次,以后一般就不用执行了,除非 .github/workflows/classroom.yml发生了变化。
+
+
+

在 qemu 模拟器上运行`lab3(os5)参考框架: <https://github.com/LearningOS/rust-based-os-comp2022/tree/main/os5-ref>`_ :

+
$ cd os5-ref
+$ make run
+
+
+

待内核初始化完毕之后,将在屏幕上打印可用的应用列表并进入shell程序:

+
[rustsbi] RustSBI version 0.2.0-alpha.4
+.______       __    __      _______.___________.  _______..______   __
+|   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
+|  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
+|      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
+|  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
+| _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|
+
+[rustsbi] Implementation: RustSBI-QEMU Version 0.0.1
+[rustsbi-dtb] Hart count: cluster0 with 1 cores
+[rustsbi] misa: RV64ACDFIMSU
+[rustsbi] mideleg: ssoft, stimer, sext (0x222)
+[rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab)
+[rustsbi] pmp0: 0x80000000 ..= 0x800fffff (rwx)
+[rustsbi] pmp1: 0x80000000 ..= 0x807fffff (rwx)
+[rustsbi] pmp2: 0x0 ..= 0xffffffffffffff (---)
+[rustsbi] enter supervisor 0x80200000
+[kernel] Hello, world!
+/**** APPS ****
+ch2b_bad_address
+ch2b_bad_instructions
+ch2b_bad_register
+ch2b_hello_world
+ch2b_power_3
+ch2b_power_5
+ch2b_power_7
+ch3b_sleep
+ch3b_sleep1
+ch3b_yield0
+ch3b_yield1
+ch3b_yield2
+ch5b_exit
+ch5b_forktest
+ch5b_forktest2
+ch5b_forktest_simple
+ch5b_forktree
+ch5b_initproc
+ch5b_user_shell
+**************/
+Rust user shell
+>>
+
+
+

可以通过输入ch5b开头的应用来测试ch5实现的fork等功能:

+
>> ch5b_forktest_simple
+
+sys_wait without child process test passed!
+parent start, pid = 2!
+ready waiting on parent process!
+hello child process!
+child process pid = 3, exit code = 100
+Shell: Process 2 exited with code 0
+
+
+
+
+

lab3(os5)参考框架:

+
 1├── os5-ref
+ 2   ├── build.rs(修改:基于应用名的应用构建器)
+ 3   ├── ...
+ 4   └── src
+ 5       ├── ...
+ 6       ├── loader.rs(修改:基于应用名的应用加载器)
+ 7       ├── main.rs(修改)
+ 8       ├── mm(修改:为了支持本章的系统调用对此模块做若干增强)
+ 9       │   ├── address.rs
+10       │   ├── frame_allocator.rs
+11       │   ├── heap_allocator.rs
+12       │   ├── memory_set.rs
+13       │   ├── mod.rs
+14       │   └── page_table.rs
+15       ├── syscall
+16       │   ├── fs.rs(修改:新增 sys_read)
+17       │   ├── mod.rs(修改:新的系统调用的分发处理)
+18       │   └── process.rs(修改:新增 sys_getpid/fork/exec/waitpid)
+19       ├── task
+20       │   ├── context.rs
+21       │   ├── manager.rs(新增:任务管理器,为上一章任务管理器功能的一部分)
+22       │   ├── mod.rs(修改:调整原来的接口实现以支持进程)
+23       │   ├── pid.rs(新增:进程标识符和内核栈的 Rust 抽象)
+24       │   ├── processor.rs(新增:处理器管理结构 ``Processor`` ,为上一章任务管理器功能的一部分)
+25       │   ├── switch.rs
+26       │   ├── switch.S
+27       │   └── task.rs(修改:支持进程机制的任务控制块)
+28       └── trap
+29           ├── context.rs
+30           ├── mod.rs(修改:对于系统调用的实现进行修改以支持进程系统调用)
+31           └── trap.S
+32
+33cloc os
+34-------------------------------------------------------------------------------
+35Language                     files          blank        comment           code
+36-------------------------------------------------------------------------------
+37Rust                            29            180            138           2049
+38Assembly                         4             20             26            229
+39make                             1             11              4             36
+40TOML                             1              2              1             13
+41-------------------------------------------------------------------------------
+42SUM:                            35            213            169           2327
+43-------------------------------------------------------------------------------
+
+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter5/1process.html b/chapter5/1process.html new file mode 100644 index 0000000..427db0d --- /dev/null +++ b/chapter5/1process.html @@ -0,0 +1,619 @@ + + + + + + + + 与进程有关的重要系统调用 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

与进程有关的重要系统调用

+
+

重要系统调用

+
+

fork 系统调用

+
/// 功能:由当前进程 fork 出一个子进程。
+/// 返回值:对于子进程返回 0,对于当前进程则返回子进程的 PID 。
+/// syscall ID:220
+pub fn sys_fork() -> isize;
+
+
+
+
+

exec 系统调用

+
/// 功能:将当前进程的地址空间清空并加载一个特定的可执行文件,返回用户态后开始它的执行。
+/// 参数:字符串 path 给出了要加载的可执行文件的名字;
+/// 返回值:如果出错的话(如找不到名字相符的可执行文件)则返回 -1,否则不应该返回。
+/// 注意:path 必须以 "\0" 结尾,否则内核将无法确定其长度
+/// syscall ID:221
+pub fn sys_exec(path: &str) -> isize;
+
+
+

利用 forkexec 的组合,我们能让创建一个子进程,并令其执行特定的可执行文件。

+
+
+

waitpid 系统调用

+
/// 功能:当前进程等待一个子进程变为僵尸进程,回收其全部资源并收集其返回值。
+/// 参数:pid 表示要等待的子进程的进程 ID,如果为 -1 的话表示等待任意一个子进程;
+/// exit_code 表示保存子进程返回值的地址,如果这个地址为 0 的话表示不必保存。
+/// 返回值:如果要等待的子进程不存在则返回 -1;否则如果要等待的子进程均未结束则返回 -2;
+/// 否则返回结束的子进程的进程 ID。
+/// syscall ID:260
+pub fn sys_waitpid(pid: isize, exit_code: *mut i32) -> isize;
+
+
+

sys_waitpid 在用户库中被封装成两个不同的 API, wait(exit_code: &mut i32)waitpid(pid: usize, exit_code: &mut i32), +前者用于等待任意一个子进程,后者用于等待特定子进程。它们实现的策略是如果子进程还未结束,就以 yield 让出时间片:

+
 1// user/src/lib.rs
+ 2
+ 3pub fn wait(exit_code: &mut i32) -> isize {
+ 4    loop {
+ 5        match sys_waitpid(-1, exit_code as *mut _) {
+ 6            -2 => { sys_yield(); }
+ 7            n => { return n; }
+ 8        }
+ 9    }
+10}
+
+
+
+
+
+

应用程序示例

+

借助这三个重要系统调用,我们可以开发功能更强大的应用。下面是两个案例: 用户初始程序-initshell程序-user_shell

+
+

用户初始程序-initproc

+

在内核初始化完毕后创建的第一个进程,是 用户初始进程 (Initial Process) ,它将通过 +fork+exec 创建 user_shell 子进程,并将被用于回收僵尸进程。

+
 1// user/src/bin/ch5b_initproc.rs
+ 2
+ 3#![no_std]
+ 4#![no_main]
+ 5
+ 6#[macro_use]
+ 7extern crate user_lib;
+ 8
+ 9use user_lib::{
+10    fork,
+11    wait,
+12    exec,
+13    yield_,
+14};
+15
+16#[no_mangle]
+17fn main() -> i32 {
+18    if fork() == 0 {
+19        exec("ch5b_user_shell\0");
+20    } else {
+21        loop {
+22            let mut exit_code: i32 = 0;
+23            let pid = wait(&mut exit_code);
+24            if pid == -1 {
+25                yield_();
+26                continue;
+27            }
+28            println!(
+29                "[initproc] Released a zombie process, pid={}, exit_code={}",
+30                pid,
+31                exit_code,
+32            );
+33        }
+34    }
+35    0
+36}
+
+
+
    +
  • 第 19 行为 fork 出的子进程分支,通过 exec 启动shell程序 user_shell , +注意我们需要在字符串末尾手动加入 \0

  • +
  • 第 21 行开始则为父进程分支,表示用户初始程序-initproc自身。它不断循环调用 wait 来等待并回收系统中的僵尸进程占据的资源。 +如果回收成功的话则会打印一条报告信息给出被回收子进程的 PID 和返回值;否则就 yield_ 交出 CPU 资源并在下次轮到它执行的时候再回收看看。

  • +
+
+
+

shell程序-user_shell

+

user_shell 需要捕获用户输入并进行解析处理,为此添加一个能获取用户输入的系统调用:

+
/// 功能:从文件中读取一段内容到缓冲区。
+/// 参数:fd 是待读取文件的文件描述符,切片 buffer 则给出缓冲区。
+/// 返回值:如果出现了错误则返回 -1,否则返回实际读到的字节数。
+/// syscall ID:63
+pub fn sys_read(fd: usize, buffer: &mut [u8]) -> isize;
+
+
+

实际调用时,我们必须要同时向内核提供缓冲区的起始地址及长度:

+
// 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 函数。

+

shell程序 user_shell 实现如下:

+
 1// user/src/bin/ch5b_user_shell.rs
+ 2
+ 3#![no_std]
+ 4#![no_main]
+ 5
+ 6extern crate alloc;
+ 7
+ 8#[macro_use]
+ 9extern crate user_lib;
+10
+11const LF: u8 = 0x0au8;
+12const CR: u8 = 0x0du8;
+13const DL: u8 = 0x7fu8;
+14const BS: u8 = 0x08u8;
+15
+16use alloc::string::String;
+17use user_lib::{fork, exec, waitpid, yield_};
+18use user_lib::console::getchar;
+19
+20#[no_mangle]
+21pub fn main() -> i32 {
+22    println!("Rust user shell");
+23    let mut line: String = String::new();
+24    print!(">> ");
+25    loop {
+26        let c = getchar();
+27        match c {
+28            LF | CR => {
+29                println!("");
+30                if !line.is_empty() {
+31                    line.push('\0');
+32                    let pid = fork();
+33                    if pid == 0 {
+34                        // child process
+35                        if exec(line.as_str()) == -1 {
+36                            println!("Error when executing!");
+37                            return -4;
+38                        }
+39                        unreachable!();
+40                    } else {
+41                        let mut exit_code: i32 = 0;
+42                        let exit_pid = waitpid(pid as usize, &mut exit_code);
+43                        assert_eq!(pid, exit_pid);
+44                        println!(
+45                            "Shell: Process {} exited with code {}",
+46                            pid, exit_code
+47                        );
+48                    }
+49                    line.clear();
+50                }
+51                print!(">> ");
+52            }
+53            BS | DL => {
+54                if !line.is_empty() {
+55                    print!("{}", BS as char);
+56                    print!(" ");
+57                    print!("{}", BS as char);
+58                    line.pop();
+59                }
+60            }
+61            _ => {
+62                print!("{}", c as char);
+63                line.push(c as char);
+64            }
+65        }
+66    }
+67}
+
+
+

可以看到,在以第 25 行开头的主循环中,每次都是调用 getchar 获取一个用户输入的字符, +并根据它相应进行一些动作。第 23 行声明的字符串 line 则维护着用户当前输入的命令内容,它也在不断发生变化。

+
    +
  • 如果用户输入回车键(第 28 行),那么user_shell 会 fork 出一个子进程(第 34 行开始)并试图通过 +exec 系统调用执行一个应用,应用的名字在字符串 line 中给出。如果 exec 的返回值为 -1 , +说明在应用管理器中找不到对应名字的应用,此时子进程就直接打印错误信息并退出;否则子进程将开始执行目标应用。

    +

    fork 之后的 user_shell 进程自己的逻辑可以在第 41 行找到。它在等待 fork 出来的子进程结束并回收掉它的资源,还会顺带收集子进程的退出状态并打印出来。

    +
  • +
  • 如果用户输入退格键(第 53 行),首先我们需要将屏幕上当前行的最后一个字符用空格替换掉, +这可以通过输入一个特殊的退格字节 BS 来实现。其次,user_shell 进程内维护的 line 也需要弹出最后一个字符。

  • +
  • 如果用户输入了一个其他字符(第 61 行),就接将它打印在屏幕上,并加入到 line 中。

  • +
  • 按键 Ctrl+A 再输入 X 来退出qemu模拟器。

  • +
+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter5/2core-data-structures.html b/chapter5/2core-data-structures.html new file mode 100644 index 0000000..7b6a006 --- /dev/null +++ b/chapter5/2core-data-structures.html @@ -0,0 +1,899 @@ + + + + + + + + 进程管理的核心数据结构 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

进程管理的核心数据结构

+
+

本节导读

+

为了更好实现进程管理,我们需要设计和调整内核中的一些数据结构,包括:

+
    +
  • 基于应用名的应用链接/加载器

  • +
  • 进程标识符 PidHandle 以及内核栈 KernelStack

  • +
  • 任务控制块 TaskControlBlock

  • +
  • 任务管理器 TaskManager

  • +
  • 处理器管理结构 Processor

  • +
+
+
+

基于应用名的应用链接/加载器

+

在实现 exec 系统调用的时候,我们需要根据应用的名字而不仅仅是一个编号来获取应用的 ELF 格式数据。 +因此,在链接器 os/build.rs 中,我们按顺序保存链接进来的每个应用的名字:

+
 1    // os/build.rs
+ 2
+ 3    for i in 0..apps.len() {
+ 4        writeln!(f, r#"    .quad app_{}_start"#, i)?;
+ 5    }
+ 6    writeln!(f, r#"    .quad app_{}_end"#, apps.len() - 1)?;
+ 7
+ 8    writeln!(f, r#"
+ 9    .global _app_names
+10_app_names:"#)?;
+11    for app in apps.iter() {
+12        writeln!(f, r#"    .string "{}""#, app)?;
+13    }
+14
+15    for (idx, app) in apps.iter().enumerate() {
+16        ...
+17    }
+
+
+

第 8~13 行,各个应用的名字通过 .string 伪指令放到数据段中,注意链接器会自动在每个字符串的结尾加入分隔符 +\0 ,它们的位置由全局符号 _app_names 指出。

+

而在加载器 loader.rs 中,我们用一个全局可见的 只读 向量 APP_NAMES 来按照顺序将所有应用的名字保存在内存中:

+
// 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 +在内核初始化时被调用,它可以打印出所有可用应用的名字。

+
// 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!("**************/")
+}
+
+
+
+
+

进程标识符和内核栈

+
+

进程标识符

+

同一时间存在的所有进程都有一个自己的进程标识符,它们是互不相同的整数。这里将其抽象为一个 PidHandle +类型,当它的生命周期结束后,对应的整数会被编译器自动回收:

+
// os/src/task/pid.rs
+
+pub struct PidHandle(pub usize);
+
+
+

类似之前的物理页帧分配器 FrameAllocator ,我们实现一个同样使用简单栈式分配策略的进程标识符分配器 +PidAllocator ,并将其全局实例化为 PID_ALLOCATOR

+
// os/src/task/pid.rs
+
+struct PidAllocator {
+    current: usize,
+    recycled: Vec<usize>,
+}
+
+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: UPSafeCell<PidAllocator> =
+        unsafe { UPSafeCell::new(PidAllocator::new()) };
+}
+
+
+

PidAllocator::alloc 将会分配出去一个将 usize 包装之后的 PidHandle 。 +我们将其包装为一个全局分配进程标识符的接口 pid_alloc

+
// os/src/task/pid.rs
+
+pub fn pid_alloc() -> PidHandle {
+    PID_ALLOCATOR.exclusive_access().alloc()
+}
+
+
+

同时我们也需要为 PidHandle 实现 Drop Trait 来允许编译器进行自动的资源回收:

+
// os/src/task/pid.rs
+
+impl Drop for PidHandle {
+    fn drop(&mut self) {
+        //println!("drop pid {}", self.0);
+        PID_ALLOCATOR.exclusive_access().dealloc(self.0);
+    }
+}
+
+
+
+
+

内核栈

+

从本章开始,我们将应用编号替换为进程标识符来决定每个进程内核栈在地址空间中的位置。

+

在内核栈 KernelStack 中保存着它所属进程的 PID :

+
// os/src/task/pid.rs
+
+pub struct KernelStack {
+    pid: usize,
+}
+
+
+

它提供以下方法:

+
 1// os/src/task/pid.rs
+ 2
+ 3/// Return (bottom, top) of a kernel stack in kernel space.
+ 4pub fn kernel_stack_position(app_id: usize) -> (usize, usize) {
+ 5    let top = TRAMPOLINE - app_id * (KERNEL_STACK_SIZE + PAGE_SIZE);
+ 6    let bottom = top - KERNEL_STACK_SIZE;
+ 7    (bottom, top)
+ 8}
+ 9
+10impl KernelStack {
+11    pub fn new(pid_handle: &PidHandle) -> Self {
+12        let pid = pid_handle.0;
+13        let (kernel_stack_bottom, kernel_stack_top) = kernel_stack_position(pid);
+14        KERNEL_SPACE.exclusive_access().insert_framed_area(
+15            kernel_stack_bottom.into(),
+16            kernel_stack_top.into(),
+17            MapPermission::R | MapPermission::W,
+18        );
+19        KernelStack {
+20            pid: pid_handle.0,
+21        }
+22    }
+23    pub fn push_on_top<T>(&self, value: T) -> *mut T where
+24        T: Sized, {
+25        let kernel_stack_top = self.get_top();
+26        let ptr_mut = (kernel_stack_top - core::mem::size_of::<T>()) as *mut T;
+27        unsafe { *ptr_mut = value; }
+28        ptr_mut
+29    }
+30    pub fn get_top(&self) -> usize {
+31        let (_, kernel_stack_top) = kernel_stack_position(self.pid);
+32        kernel_stack_top
+33    }
+34}
+
+
+
    +
  • 第 11 行, new 方法可以从一个 PidHandle ,也就是一个已分配的进程标识符中对应生成一个内核栈 KernelStack 。 +它调用了第 4 行声明的 kernel_stack_position 函数来根据进程标识符计算内核栈在内核地址空间中的位置, +随即在第 14 行将一个逻辑段插入内核地址空间 KERNEL_SPACE 中。

  • +
  • 第 25 行的 push_on_top 方法可以将一个类型为 T 的变量压入内核栈顶并返回其裸指针, +这也是一个泛型函数。它在实现的时候用到了第 32 行的 get_top 方法来获取当前内核栈顶在内核地址空间中的地址。

  • +
+

内核栈 KernelStack 用到了 RAII 的思想,具体来说,实际保存它的物理页帧的生命周期被绑定到它下面,当 +KernelStack 生命周期结束后,这些物理页帧也将会被编译器自动回收:

+
// 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
+            .exclusive_access()
+            .remove_area_with_start_vpn(kernel_stack_bottom_va.into());
+    }
+}
+
+
+

KernelStack 实现 Drop Trait,一旦它的生命周期结束,就将内核地址空间中对应的逻辑段删除,为此在 MemorySet +中新增了一个名为 remove_area_with_start_vpn 的方法,感兴趣的读者可以查阅。

+
+
+
+

进程控制块

+

在内核中,每个进程的执行状态、资源控制等元数据均保存在一个被称为 进程控制块 (PCB, Process Control Block) +的结构中,它是内核对进程进行管理的单位。在内核看来,它就等价于一个进程。

+

承接前面的章节,我们仅需对任务控制块 TaskControlBlock 进行若干改动,让它直接承担进程控制块的功能:

+
 1// os/src/task/task.rs
+ 2
+ 3pub struct TaskControlBlock {
+ 4    // immutable
+ 5    pub pid: PidHandle,
+ 6    pub kernel_stack: KernelStack,
+ 7    // mutable
+ 8    inner: UPSafeCell<TaskControlBlockInner>,
+ 9}
+10
+11pub struct TaskControlBlockInner {
+12    pub trap_cx_ppn: PhysPageNum,
+13    pub base_size: usize,
+14    pub task_cx: TaskContext,
+15    pub task_status: TaskStatus,
+16    pub memory_set: MemorySet,
+17    pub parent: Option<Weak<TaskControlBlock>>,
+18    pub children: Vec<Arc<TaskControlBlock>>,
+19    pub exit_code: i32,
+20}
+
+
+

任务控制块中包含两部分:

+
    +
  • 在初始化之后就不再变化的作为一个字段直接放在任务控制块中。这里将进程标识符 PidHandle 和内核栈 KernelStack 放在其中;

  • +
  • 在运行过程中可能发生变化的则放在 TaskControlBlockInner 中,将它再包裹上一层 UPSafeCell<T> 放在任务控制块中。 +在此使用 UPSafeCell<T> 可以提供互斥从而避免数据竞争。

  • +
+

TaskControlBlockInner 中包含下面这些内容:

+
    +
  • trap_cx_ppn 指出了应用地址空间中的 Trap 上下文被放在的物理页帧的物理页号。

  • +
  • base_size 的含义是:应用数据仅有可能出现在应用地址空间低于 base_size 字节的区域中。借助它我们可以清楚的知道应用有多少数据驻留在内存中。

  • +
  • task_cx 保存任务上下文,用于任务切换。

  • +
  • task_status 维护当前进程的执行状态。

  • +
  • memory_set 表示应用地址空间。

  • +
  • parent 指向当前进程的父进程(如果存在的话)。注意我们使用 Weak 而非 Arc +来包裹另一个任务控制块,因此这个智能指针将不会影响父进程的引用计数。

  • +
  • children 则将当前进程的所有子进程的任务控制块以 Arc 智能指针的形式保存在一个向量中,这样才能够更方便的找到它们。

  • +
  • 当进程调用 exit 系统调用主动退出或者执行出错由内核终止的时候,它的退出码 exit_code +会被内核保存在它的任务控制块中,并等待它的父进程通过 waitpid 回收它的资源的同时也收集它的 PID 以及退出码。

  • +
+

注意我们在维护父子进程关系的时候大量用到了智能指针 Arc/Weak ,当且仅当它的引用计数变为 0 的时候,进程控制块以及被绑定到它上面的各类资源才会被回收。

+

TaskControlBlockInner 提供的方法主要是对于它内部字段的快捷访问:

+
// os/src/task/task.rs
+
+impl TaskControlBlockInner {
+    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 目前提供以下方法:

+
// os/src/task/task.rs
+
+impl TaskControlBlock {
+    pub fn inner_exclusive_access(&self) -> RefMut<'_, TaskControlBlockInner> {
+        self.inner.exclusive_access()
+    }
+    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<TaskControlBlock>) -> Arc<TaskControlBlock> {...}
+}
+
+
+
    +
  • inner_exclusive_access 尝试获取互斥锁来得到 TaskControlBlockInner 的可变引用。

  • +
  • getpidusize 的形式返回当前进程的进程标识符。

  • +
  • new 用来创建一个新的进程,目前仅用于内核中手动创建唯一一个初始进程 initproc

  • +
  • exec 用来实现 exec 系统调用,即当前进程加载并执行另一个 ELF 格式可执行文件。

  • +
  • fork 用来实现 fork 系统调用,即当前进程 fork 出来一个与之几乎相同的子进程。

  • +
+

new/exec/fork 的实现我们将在下一小节再介绍。

+
+
+

任务管理器

+

在前面的章节中,任务管理器 TaskManager 不仅负责管理所有的任务,还维护着 CPU 当前在执行哪个任务。 +由于这种设计不够灵活,我们需要将任务管理器对于 CPU 的监控职能拆分到处理器管理结构 Processor 中去, +任务管理器自身仅负责管理所有任务。在这里,任务指的就是进程。

+
 1// os/src/task/manager.rs
+ 2
+ 3pub struct TaskManager {
+ 4    ready_queue: VecDeque<Arc<TaskControlBlock>>,
+ 5}
+ 6
+ 7/// A simple FIFO scheduler.
+ 8impl TaskManager {
+ 9    pub fn new() -> Self {
+10        Self {
+11            ready_queue: VecDeque::new(),
+12        }
+13    }
+14    pub fn add(&mut self, task: Arc<TaskControlBlock>) {
+15        self.ready_queue.push_back(task);
+16    }
+17    pub fn fetch(&mut self) -> Option<Arc<TaskControlBlock>> {
+18        self.ready_queue.pop_front()
+19    }
+20}
+21
+22lazy_static! {
+23    pub static ref TASK_MANAGER: UPSafeCell<TaskManager> =
+24        unsafe { UPSafeCell::new(TaskManager::new()) };
+25}
+26
+27pub fn add_task(task: Arc<TaskControlBlock>) {
+28    TASK_MANAGER.exclusive_access().add(task);
+29}
+30
+31pub fn fetch_task() -> Option<Arc<TaskControlBlock>> {
+32    TASK_MANAGER.exclusive_access().fetch()
+33}
+
+
+

TaskManager 将所有的任务控制块用引用计数 Arc 智能指针包裹后放在一个双端队列 VecDeque 中。 +使用智能指针的原因在于,任务控制块经常需要被放入/取出,如果直接移动任务控制块自身将会带来大量的数据拷贝开销, +而对于智能指针进行移动则没有多少开销。其次,允许任务控制块的共享引用在某些情况下能够让我们的实现更加方便。

+

TaskManager 提供 add/fetch 两个操作,前者表示将一个任务加入队尾,后者则表示从队头中取出一个任务来执行。 +从调度算法来看,这里用到的就是最简单的 RR 算法。全局实例 TASK_MANAGER 则提供给内核的其他子模块 add_task/fetch_task 两个函数。

+
+
+

处理器管理结构

+

处理器管理结构 Processor 负责维护从任务管理器 TaskManager 分离出去的那部分 CPU 状态:

+
// os/src/task/processor.rs
+
+pub struct Processor {
+    current: Option<Arc<TaskControlBlock>>,
+    idle_task_cx: TaskContext,
+}
+
+
+

包括:

+
    +
  • current 表示在当前处理器上正在执行的任务;

  • +
  • idle_task_cx_ptr 表示当前处理器上的 idle 控制流的任务上下文的地址。

  • +
+

在单核环境下,我们仅创建单个 Processor 的全局实例 PROCESSOR

+
// os/src/task/processor.rs
+
+lazy_static! {
+    pub static ref PROCESSOR: UPSafeCell<Processor> = unsafe { UPSafeCell::new(Processor::new()) };
+}
+
+
+
+

正在执行的任务

+
 1// os/src/task/processor.rs
+ 2
+ 3impl Processor {
+ 4    pub fn take_current(&mut self) -> Option<Arc<TaskControlBlock>> {
+ 5        self.current.take()
+ 6    }
+ 7    pub fn current(&self) -> Option<Arc<TaskControlBlock>> {
+ 8        self.current.as_ref().map(|task| Arc::clone(task))
+ 9    }
+10}
+11
+12pub fn take_current_task() -> Option<Arc<TaskControlBlock>> {
+13    PROCESSOR.take_current()
+14}
+15
+16pub fn current_task() -> Option<Arc<TaskControlBlock>> {
+17    PROCESSOR.current()
+18}
+19
+20pub fn current_user_token() -> usize {
+21    let task = current_task().unwrap();
+22    let token = task.inner_exclusive_access().get_user_token();
+23    token
+24}
+25
+26pub fn current_trap_cx() -> &'static mut TrapContext {
+27    current_task()
+28        .unwrap()
+29        .inner_exclusive_access()
+30        .get_trap_cx()
+31}
+
+
+
    +
  • 第 4 行的 Processor::take_current 可以取出当前正在执行的任务。 Option::take 意味着 current 字段也变为 None

  • +
  • 第 7 行的 Processor::current 返回当前执行的任务的一份拷贝。。

  • +
  • current_user_tokencurrent_trap_cx 基于 current_task 实现,提供当前正在执行的任务的更多信息。

  • +
+
+
+

任务调度的 idle 控制流

+

每个 Processor 都有一个 idle 控制流,它们运行在每个核各自的启动栈上,功能是尝试从任务管理器中选出一个任务来在当前核上执行。 +在内核初始化完毕之后,核通过调用 run_tasks 函数来进入 idle 控制流:

+
 1// os/src/task/processor.rs
+ 2
+ 3impl Processor {
+ 4    fn get_idle_task_cx_ptr(&mut self) -> *mut TaskContext {
+ 5        &mut self.idle_task_cx as *mut _
+ 6    }
+ 7}
+ 8
+ 9pub fn run_tasks() {
+10    loop {
+11        let mut processor = PROCESSOR.exclusive_access();
+12        if let Some(task) = fetch_task() {
+13            let idle_task_cx_ptr = processor.get_idle_task_cx_ptr();
+14            // access coming task TCB exclusively
+15            let mut task_inner = task.inner_exclusive_access();
+16            let next_task_cx_ptr = &task_inner.task_cx as *const TaskContext;
+17            task_inner.task_status = TaskStatus::Running;
+18            drop(task_inner);
+19            // release coming task TCB manually
+20            processor.current = Some(task);
+21            // release processor manually
+22            drop(processor);
+23            unsafe {
+24                __switch(idle_task_cx_ptr, next_task_cx_ptr);
+25            }
+26        }
+27    }
+28}
+
+
+

调度功能的主体在 run_tasks 中实现。它循环调用 fetch_task 直到顺利从任务管理器中取出一个任务,然后获得 +__switch 两个参数进行任务切换。注意在整个过程中要严格控制临界区。

+

当一个应用交出 CPU 使用权时,进入内核后它会调用 schedule 函数来切换到 idle 控制流并开启新一轮的任务调度。

+
// os/src/task/processor.rs
+
+pub fn schedule(switched_task_cx_ptr: *mut TaskContext) {
+    let mut processor = PROCESSOR.exclusive_access();
+    let idle_task_cx_ptr = processor.get_idle_task_cx_ptr();
+    drop(processor);
+    unsafe {
+        __switch(switched_task_cx_ptr, idle_task_cx_ptr);
+    }
+}
+
+
+

切换回去之后,我们将跳转到 Processor::run__switch 返回之后的位置,也即开启了下一轮循环。

+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter5/3implement-process-mechanism.html b/chapter5/3implement-process-mechanism.html new file mode 100644 index 0000000..079f83b --- /dev/null +++ b/chapter5/3implement-process-mechanism.html @@ -0,0 +1,1025 @@ + + + + + + + + 进程管理机制的设计实现 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

进程管理机制的设计实现

+
+

本节导读

+

本节将介绍如何基于上一节设计的内核数据结构来实现进程管理:

+
    +
  • 初始进程 initproc 的创建;

  • +
  • 进程调度机制:当进程主动调用 sys_yield 交出 CPU 使用权,或者内核本轮分配的时间片用尽之后如何切换到下一个进程;

  • +
  • 进程生成机制:介绍进程相关的两个重要系统调用 sys_fork/sys_exec 的实现;

  • +
  • 字符输入机制:介绍 sys_read 系统调用的实现;

  • +
  • 进程资源回收机制:当进程调用 sys_exit 正常退出或者出错被内核终止后,如何保存其退出码,其父进程又是如何通过 +sys_waitpid 收集该进程的信息并回收其资源。

  • +
+
+
+

初始进程的创建

+

内核初始化完毕之后,即会调用 task 子模块提供的 add_initproc 函数来将初始进程 initproc +加入任务管理器,但在这之前,我们需要初始进程的进程控制块 INITPROC ,这基于 lazy_static 在运行时完成。

+
// os/src/task/mod.rs
+
+lazy_static! {
+    pub static ref INITPROC: Arc<TaskControlBlock> = 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 是如何实现的:

+
 1// os/src/task/task.rs
+ 2
+ 3use super::{PidHandle, pid_alloc, KernelStack};
+ 4use super::TaskContext;
+ 5use crate::config::TRAP_CONTEXT;
+ 6use crate::trap::TrapContext;
+ 7
+ 8// impl TaskControlBlock
+ 9pub fn new(elf_data: &[u8]) -> Self {
+10    // memory_set with elf program headers/trampoline/trap context/user stack
+11    let (memory_set, user_sp, entry_point) = MemorySet::from_elf(elf_data);
+12    let trap_cx_ppn = memory_set
+13        .translate(VirtAddr::from(TRAP_CONTEXT).into())
+14        .unwrap()
+15        .ppn();
+16    // alloc a pid and a kernel stack in kernel space
+17    let pid_handle = pid_alloc();
+18    let kernel_stack = KernelStack::new(&pid_handle);
+19    let kernel_stack_top = kernel_stack.get_top();
+20    // push a task context which goes to trap_return to the top of kernel stack
+21    let task_cx_ptr = kernel_stack.push_on_top(TaskContext::goto_trap_return());
+22    let task_control_block = Self {
+23        pid: pid_handle,
+24        kernel_stack,
+25        inner: unsafe { UPSafeCell::new(TaskControlBlockInner {
+26                trap_cx_ppn,
+27                base_size: user_sp,
+28                task_cx: TaskContext::goto_trap_return(kernel_stack_top),
+29                task_status: TaskStatus::Ready,
+30                memory_set,
+31                parent: None,
+32                children: Vec::new(),
+33                exit_code: 0,
+34            })
+35        },
+36    };
+37    // prepare TrapContext in user space
+38    let trap_cx = task_control_block.inner_exclusive_access().get_trap_cx();
+39    *trap_cx = TrapContext::app_init_context(
+40        entry_point,
+41        user_sp,
+42        KERNEL_SPACE.exclusive_access().token(),
+43        kernel_stack_top,
+44        trap_handler as usize,
+45    );
+46    task_control_block
+47}
+
+
+
    +
  • 第 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 的时候用户态能正确进入内核态。

  • +
+
+
+

进程调度机制

+

调用 task 子模块提供的 suspend_current_and_run_next 函数可以暂停当前任务,并切换到下一个任务,下面给出了两种典型的使用场景:

+
// 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 的实现也需要发生变化:

+
 1// os/src/task/mod.rs
+ 2
+ 3use processor::{task_current_task, schedule};
+ 4use manager::add_task;
+ 5
+ 6pub fn suspend_current_and_run_next() {
+ 7    // There must be an application running.
+ 8    let task = take_current_task().unwrap();
+ 9
+10    // ---- access current TCB exclusively
+11    let mut task_inner = task.inner_exclusive_access();
+12    let task_cx_ptr = &mut task_inner.task_cx as *mut TaskContext;
+13    // Change status to Ready
+14    task_inner.task_status = TaskStatus::Ready;
+15    drop(task_inner);
+16    // ---- release current PCB
+17
+18    // push back to ready queue.
+19    add_task(task);
+20    // jump to scheduling cycle
+21    schedule(task_cx_ptr);
+22}
+
+
+

首先通过 take_current_task 来取出当前正在执行的任务,修改其进程控制块内的状态,随后将这个任务放入任务管理器的队尾。接着调用 +schedule 函数来触发调度并切换任务。当仅有一个任务的时候, suspend_current_and_run_next 的效果是会继续执行这个任务。

+
+
+

进程的生成机制

+
+

fork 系统调用的实现

+

实现 fork 时,最为关键且困难一点的是为子进程创建一个和父进程几乎完全相同的地址空间。我们的实现如下:

+
 1// os/src/mm/memory_set.rs
+ 2
+ 3impl MapArea {
+ 4    pub fn from_another(another: &MapArea) -> Self {
+ 5        Self {
+ 6            vpn_range: VPNRange::new(
+ 7                another.vpn_range.get_start(),
+ 8                another.vpn_range.get_end()
+ 9            ),
+10            data_frames: BTreeMap::new(),
+11            map_type: another.map_type,
+12            map_perm: another.map_perm,
+13        }
+14    }
+15}
+16
+17impl MemorySet {
+18    pub fn from_existed_user(user_space: &MemorySet) -> MemorySet {
+19        let mut memory_set = Self::new_bare();
+20        // map trampoline
+21        memory_set.map_trampoline();
+22        // copy data sections/trap_context/user_stack
+23        for area in user_space.areas.iter() {
+24            let new_area = MapArea::from_another(area);
+25            memory_set.push(new_area, None);
+26            // copy data from another space
+27            for vpn in area.vpn_range {
+28                let src_ppn = user_space.translate(vpn).unwrap().ppn();
+29                let dst_ppn = memory_set.translate(vpn).unwrap().ppn();
+30                dst_ppn.get_bytes_array().copy_from_slice(src_ppn.get_bytes_array());
+31            }
+32        }
+33        memory_set
+34    }
+35}
+
+
+

这需要对内存管理子模块 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 来从父进程的进程控制块创建一份子进程的控制块:

+
 1// os/src/task/task.rs
+ 2
+ 3impl TaskControlBlock {
+ 4    pub fn fork(self: &Arc<TaskControlBlock>) -> Arc<TaskControlBlock> {
+ 5        // ---- access parent PCB exclusively
+ 6        let mut parent_inner = self.inner_exclusive_access();
+ 7        // copy user space(include trap context)
+ 8        let memory_set = MemorySet::from_existed_user(&parent_inner.memory_set);
+ 9        let trap_cx_ppn = memory_set
+10            .translate(VirtAddr::from(TRAP_CONTEXT).into())
+11            .unwrap()
+12            .ppn();
+13        // alloc a pid and a kernel stack in kernel space
+14        let pid_handle = pid_alloc();
+15        let kernel_stack = KernelStack::new(&pid_handle);
+16        let kernel_stack_top = kernel_stack.get_top();
+17        let task_control_block = Arc::new(TaskControlBlock {
+18            pid: pid_handle,
+19            kernel_stack,
+20            inner: unsafe {
+21                UPSafeCell::new(TaskControlBlockInner {
+22                    trap_cx_ppn,
+23                    base_size: parent_inner.base_size,
+24                    task_cx: TaskContext::goto_trap_return(kernel_stack_top),
+25                    task_status: TaskStatus::Ready,
+26                    memory_set,
+27                    parent: Some(Arc::downgrade(self)),
+28                    children: Vec::new(),
+29                    exit_code: 0,
+30                })
+31            },
+32        });
+33        // add child
+34        parent_inner.children.push(task_control_block.clone());
+35        // modify kernel_sp in trap_cx
+36        // **** access children PCB exclusively
+37        let trap_cx = task_control_block.inner_exclusive_access().get_trap_cx();
+38        trap_cx.kernel_sp = kernel_stack_top;
+39        // return
+40        task_control_block
+41        // ---- release parent PCB automatically
+42        // **** release children PCB automatically
+43    }
+44}
+
+
+

它基本上和新建进程控制块的 TaskControlBlock::new 是相同的,但要注意以下几点:

+
    +
  • 子进程的地址空间不是通过解析 ELF,而是通过在第 8 行调用 MemorySet::from_existed_user 复制父进程地址空间得到的;

  • +
  • 在 fork 的时候需要注意父子进程关系的维护。既要将父进程的弱引用计数放到子进程的进程控制块中,又要将子进程插入到父进程的孩子向量 children 中。

  • +
+

实现 sys_fork 时,我们需要特别注意如何体现父子进程的差异:

+
 1// os/src/syscall/process.rs
+ 2
+ 3pub fn sys_fork() -> isize {
+ 4    let current_task = current_task().unwrap();
+ 5    let new_task = current_task.fork();
+ 6    let new_pid = new_task.pid.0;
+ 7    // modify trap context of new_task, because it returns immediately after switching
+ 8    let trap_cx = new_task.inner_exclusive_access().get_trap_cx();
+ 9    // we do not have to move to next instruction since we have done it before
+10    // for child process, fork returns 0
+11    trap_cx.x[10] = 0;
+12    // add new task to scheduler
+13    add_task(new_task);
+14    new_pid as isize
+15}
+
+
+

在调用 sys_fork 之前,我们已经将当前进程 Trap 上下文中的 sepc 向后移动了 4 字节,使得它回到用户态之后会从 ecall +的下一条指令开始执行。之后,当我们复制地址空间时,子进程地址空间 Trap 上下文的 sepc 也是移动之后的值,我们无需再进行修改。

+

父子进程回到用户态的瞬间都处于刚刚从一次系统调用返回的状态,但二者返回值不同。第 8~11 行我们将子进程的 Trap +上下文中用来存放系统调用返回值的 a0 寄存器修改为 0 ,而父进程系统调用的返回值会在 syscall 返回之后再设置为 sys_fork +的返回值。这就做到了父进程 fork 的返回值为子进程的 PID ,而子进程的返回值为 0。

+
+
+

exec 系统调用的实现

+

exec 系统调用使得一个进程能够加载一个新的 ELF 可执行文件替换原有的应用地址空间并开始执行。我们先从进程控制块的层面进行修改:

+
 1// os/src/task/task.rs
+ 2
+ 3impl TaskControlBlock {
+ 4    pub fn exec(&self, elf_data: &[u8]) {
+ 5        // memory_set with elf program headers/trampoline/trap context/user stack
+ 6        let (memory_set, user_sp, entry_point) = MemorySet::from_elf(elf_data);
+ 7        let trap_cx_ppn = memory_set
+ 8            .translate(VirtAddr::from(TRAP_CONTEXT).into())
+ 9            .unwrap()
+10            .ppn();
+11
+12        // **** access inner exclusively
+13        let mut inner = self.inner_exclusive_access();
+14        // substitute memory_set
+15        inner.memory_set = memory_set;
+16        // update trap_cx ppn
+17        inner.trap_cx_ppn = trap_cx_ppn;
+18        // initialize trap_cx
+19        let trap_cx = inner.get_trap_cx();
+20        *trap_cx = TrapContext::app_init_context(
+21            entry_point,
+22            user_sp,
+23            KERNEL_SPACE.exclusive_access().token(),
+24            self.kernel_stack.get_top(),
+25            trap_handler as usize,
+26        );
+27        // **** release inner automatically
+28    }
+29}
+
+
+

它在解析传入的 ELF 格式数据之后只做了两件事情:

+
    +
  • 首先从 ELF 生成一个全新的地址空间并直接替换进来(第 15 行),这将导致原有地址空间生命周期结束,里面包含的全部物理页帧都会被回收;

  • +
  • 然后修改新的地址空间中的 Trap 上下文,将解析得到的应用入口点、用户栈位置以及一些内核的信息进行初始化,这样才能正常实现 Trap 机制。

  • +
+

sys_exec 的实现如下,它调用 translated_str 找到要执行的应用名,并试图从应用加载器提供的 get_app_data_by_name +接口中获取对应的 ELF 数据,如果找到的话就调用 TaskControlBlock::exec 替换地址空间。

+
// 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 系统调用中传递给内核的只有一个应用名字符串在用户地址空间中的首地址,内核必限手动查页表来获得字符串的值。

+

translated_str 用来从用户地址空间中查找字符串,其原理就是逐字节查页表直到发现一个 \0 为止。为什么要逐字节查页表? +因为内核不知道字符串的长度,且字符串可能是跨物理页的。

+
// 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
+}
+
+
+
+
+

系统调用后重新获取 Trap 上下文

+

原来在 trap_handler 中我们是这样处理系统调用的:

+
// 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 失效了,因为它是就原来的地址空间而言的。为了能够处理类似的这种情况,我们在 syscall +返回之后需要重新获取 cx ,目前的实现如下:

+
// 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();
+}
+
+
+
+
+
+

sys_read 获取输入

+

我们需要实现 sys_read 系统调用,使应用能够取得用户的键盘输入。

+
// 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 读入,且每次只能读入一个字符,这是利用 sbi +提供的接口 console_getchar 实现的。如果还没有输入,我们就切换到其他进程,等下次切换回来时再看看是否有输入了。 +获取到输入后就退出循环,并手动查页表将输入字符正确写入到应用地址空间。

+
+
+

进程资源回收机制

+
+

进程的退出

+

当应用调用 sys_exit 系统调用主动退出,或者出错由内核终止之后,会在内核中调用 exit_current_and_run_next 函数:

+
 1// os/src/syscall/process.rs
+ 2
+ 3pub fn sys_exit(exit_code: i32) -> ! {
+ 4    exit_current_and_run_next(exit_code);
+ 5    panic!("Unreachable in sys_exit!");
+ 6}
+ 7
+ 8// os/src/trap/mod.rs
+ 9
+10#[no_mangle]
+11pub fn trap_handler() -> ! {
+12    set_kernel_trap_entry();
+13    let scause = scause::read();
+14    let stval = stval::read();
+15    match scause.cause() {
+16        Trap::Exception(Exception::StoreFault) |
+17        Trap::Exception(Exception::StorePageFault) |
+18        Trap::Exception(Exception::InstructionFault) |
+19        Trap::Exception(Exception::InstructionPageFault) |
+20        Trap::Exception(Exception::LoadFault) |
+21        Trap::Exception(Exception::LoadPageFault) => {
+22            println!(
+23                "[kernel] {:?} in application, bad addr = {:#x}, bad instruction = {:#x}, core dumped.",
+24                scause.cause(),
+25                stval,
+26                current_trap_cx().sepc,
+27            );
+28            // page fault exit code
+29            exit_current_and_run_next(-2);
+30        }
+31        Trap::Exception(Exception::IllegalInstruction) => {
+32            println!("[kernel] IllegalInstruction in application, core dumped.");
+33            // illegal instruction exit code
+34            exit_current_and_run_next(-3);
+35        }
+36        ...
+37    }
+38    trap_return();
+39}
+
+
+

相比前面的章节, exit_current_and_run_next 带有一个退出码作为参数,这个退出码会在 +exit_current_and_run_next 写入当前进程的进程控制块:

+
 1// os/src/mm/memory_set.rs
+ 2
+ 3impl MemorySet {
+ 4    pub fn recycle_data_pages(&mut self) {
+ 5        self.areas.clear();
+ 6    }
+ 7}
+ 8
+ 9// os/src/task/mod.rs
+10
+11pub fn exit_current_and_run_next(exit_code: i32) {
+12    // take from Processor
+13    let task = take_current_task().unwrap();
+14    // **** access current TCB exclusively
+15    let mut inner = task.inner_exclusive_access();
+16    // Change status to Zombie
+17    inner.task_status = TaskStatus::Zombie;
+18    // Record exit code
+19    inner.exit_code = exit_code;
+20    // do not move to its parent but under initproc
+21
+22    // ++++++ access initproc TCB exclusively
+23    {
+24        let mut initproc_inner = INITPROC.inner_exclusive_access();
+25        for child in inner.children.iter() {
+26            child.inner_exclusive_access().parent = Some(Arc::downgrade(&INITPROC));
+27            initproc_inner.children.push(child.clone());
+28        }
+29    }
+30    // ++++++ release parent PCB
+31
+32    inner.children.clear();
+33    // deallocate user space
+34    inner.memory_set.recycle_data_pages();
+35    drop(inner);
+36    // **** release current PCB
+37    // drop task manually to maintain rc correctly
+38    drop(task);
+39    // we do not have to save task context
+40    let mut _unused = TaskContext::zero_init();
+41    schedule(&mut _unused as *mut _);
+42}
+
+
+
    +
  • 第 13 行,调用 take_current_task 来将当前进程控制块从处理器监控 PROCESSOR +中取出,而不只是得到一份拷贝,这是为了正确维护进程控制块的引用计数;

  • +
  • 第 17 行将进程控制块中的状态修改为 TaskStatus::Zombie 即僵尸进程;

  • +
  • 第 19 行将传入的退出码 exit_code 写入进程控制块中,后续父进程在 waitpid 的时候可以收集;

  • +
  • 第 24~26 行所做的事情是,将当前进程的所有子进程挂在初始进程 initproc 下面。第 32 行将当前进程的孩子向量清空。

  • +
  • 第 34 行,对于当前进程占用的资源进行早期回收。 MemorySet::recycle_data_pages 只是将地址空间中的逻辑段列表 +areas 清空,这将导致应用地址空间的所有数据被存放在的物理页帧被回收,而用来存放页表的那些物理页帧此时则不会被回收。

  • +
  • 最后在第 41 行我们调用 schedule 触发调度及任务切换,我们再也不会回到该进程的执行过程,因此无需关心任务上下文的保存。

  • +
+
+
+

父进程回收子进程资源

+
 1// os/src/syscall/process.rs
+ 2
+ 3/// If there is not a child process whose pid is same as given, return -1.
+ 4/// Else if there is a child process but it is still running, return -2.
+ 5pub fn sys_waitpid(pid: isize, exit_code_ptr: *mut i32) -> isize {
+ 6    let task = current_task().unwrap();
+ 7    // find a child process
+ 8
+ 9    // ---- access current TCB exclusively
+10    let mut inner = task.inner_exclusive_access();
+11    if !inner
+12        .children
+13        .iter()
+14        .any(|p| pid == -1 || pid as usize == p.getpid())
+15    {
+16        return -1;
+17        // ---- release current PCB
+18    }
+19    let pair = inner.children.iter().enumerate().find(|(_, p)| {
+20        // ++++ temporarily access child PCB lock exclusively
+21        p.inner_exclusive_access().is_zombie() && (pid == -1 || pid as usize == p.getpid())
+22        // ++++ release child PCB
+23    });
+24    if let Some((idx, _)) = pair {
+25        let child = inner.children.remove(idx);
+26        // confirm that child will be deallocated after removing from children list
+27        assert_eq!(Arc::strong_count(&child), 1);
+28        let found_pid = child.getpid();
+29        // ++++ temporarily access child TCB exclusively
+30        let exit_code = child.inner_exclusive_access().exit_code;
+31        // ++++ release child PCB
+32        *translated_refmut(inner.memory_set.token(), exit_code_ptr) = exit_code;
+33        found_pid as isize
+34    } else {
+35        -2
+36    }
+37    // ---- release current PCB lock automatically
+38}
+
+
+

sys_waitpid 是一个立即返回的系统调用,它的返回值语义是:如果当前的进程不存在一个符合要求的子进程,则返回 +-1;如果至少存在一个,但是其中没有僵尸进程(也即仍未退出)则返回 -2;如果都不是的话则可以正常回收并返回回收子进程的 +pid 。但在编写应用的开发者看来, wait/waitpid 两个辅助函数都必定能够返回一个有意义的结果,要么是 -1,要么是一个正数 +PID ,是不存在 -2 这种通过等待即可消除的中间结果的。等待的过程由用户库 user_lib 完成。

+

首先判断 sys_waitpid 是否会返回 -1 ,这取决于当前进程是否有一个符合要求的子进程。当传入的 pid 为 -1 +的时候,任何一个子进程都算是符合要求;但 pid 不为 -1 的时候,则只有 PID 恰好与 pid +相同的子进程才算符合条件。我们简单通过迭代器即可完成判断。

+

再判断符合要求的子进程中是否有僵尸进程。如果找不到的话直接返回 -2 ,否则进行下一步处理:

+

我们将子进程从向量中移除并置于当前上下文中,当它所在的代码块结束,这次引用变量的生命周期结束,子进程进程控制块的引用计数将变为 +0 ,内核将彻底回收掉它占用的所有资源,包括内核栈、它的 PID 、存放页表的那些物理页帧等等。

+

获得子进程退出码后,考虑到应用传入的指针指向应用地址空间,我们还需要手动查页表找到对应物理内存中的位置。 +translated_refmut 的实现可以在 os/src/mm/page_table.rs 中找到。

+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter5/4exercise.html b/chapter5/4exercise.html new file mode 100644 index 0000000..c885bfe --- /dev/null +++ b/chapter5/4exercise.html @@ -0,0 +1,540 @@ + + + + + + + + chapter5练习 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

chapter5练习

+
+

Lab3 编程作业

+
+

进程创建

+

大家一定好奇过为啥进程创建要用 fork + exec 这么一个奇怪的系统调用,就不能直接搞一个新进程吗? +思而不学则殆,我们就来试一试!这章的编程练习请大家实现一个完全 DIY 的系统调用 spawn,用以创建一个新进程。

+

spawn 系统调用定义( 标准spawn看这里 ):

+
fn sys_spawn(path: *const u8) -> isize
+
+
+
    +
  • syscall ID: 400

  • +
  • 功能:新建子进程,使其执行目标程序。

  • +
  • 说明:成功返回子进程id,否则返回 -1。

  • +
  • +
    可能的错误:
      +
    • 无效的文件名。

    • +
    • 进程池满/内存不足等资源错误。

    • +
    +
    +
    +
  • +
+

TIPS:虽然测例很简单,但提醒读者 spawn 不必 像 fork 一样复制父进程的地址空间。

+
+
+

stride 调度算法

+

ch3 中我们实现的调度算法十分简单。现在我们要为我们的 os 实现一种带优先级的调度算法:stride 调度算法。

+

算法描述如下:

+

(1) 为每个进程设置一个当前 stride,表示该进程当前已经运行的“长度”。另外设置其对应的 pass +值(只与进程的优先权有关系),表示对应进程在调度后,stride 需要进行的累加值。

+
    +
  1. 每次需要调度时,从当前 runnable 态的进程中选择 stride 最小的进程调度。对于获得调度的进程 P,将对应的 stride 加上其对应的步长 pass。

  2. +
  3. 一个时间片后,回到上一步骤,重新调度当前 stride 最小的进程。

  4. +
+

可以证明,如果令 P.pass = BigStride / P.priority 其中 P.priority 表示进程的优先权(大于 1),而 +BigStride 表示一个预先定义的大常数,则该调度方案为每个进程分配的时间将与其优先级成正比。证明过程我们在这里略去,有兴趣的同学可以在网上查找相关资料。

+

其他实验细节:

+
    +
  • stride 调度要求进程优先级 \(\geq 2\),所以设定进程优先级 \(\leq 1\) 会导致错误。

  • +
  • 进程初始 stride 设置为 0 即可。

  • +
  • 进程初始优先级设置为 16。

  • +
+

为了实现该调度算法,内核还要增加 set_prio 系统调用

+
// syscall ID:140
+// 设置当前进程优先级为 prio
+// 参数:prio 进程优先级,要求 prio >= 2
+// 返回值:如果输入合法则返回 prio,否则返回 -1
+fn sys_set_priority(prio: isize) -> isize;
+
+
+

实现 tips:

+
    +
  • 你可以在TCB加入新的字段来支持优先级等。

  • +
  • 为了减少整数除的误差,BIG_STRIDE 一般需要很大,但为了不至于发生反转现象(详见问答作业),或许选择一个适中的数即可,当然能进行溢出处理就更好了。

  • +
  • stride 算法要找到 stride 最小的进程,使用优先级队列是效率不错的办法,但是我们的实验测例很简单,所以效率完全不是问题。事实上,很推荐使用暴力扫一遍的办法找最小值。

  • +
  • 注意设置进程的初始优先级。

  • +
+
+

注意

+

为了让大家能在本编程作业中使用 Vec 等数据结构,我们利用第三方库 buddy_system_allocator +为大家实现了堆内存分配器,相关代码位于 mm/heap_allocator 模块。

+

背景知识: Rust 中的动态内存分配

+
+
+
+

实验要求

+
    +
  • lab3(os5)参考框架:

  • +
  • 实验目录在 os5 。注意在reports中放入lab1-3的所有报告。

  • +
  • 通过所有测例。

    +

    在 os5 目录下 make run BASE=2 加载所有测例, ch5_usertest 打包了所有你需要通过的测例, +你也可以通过修改这个文件调整本地测试的内容, 或者单独运行某测例来纠正特定的错误。 ch5_stride +检查 stride 调度算法是否满足公平性要求,六个子程序运行的次数应该大致与其优先级呈正比,测试通过标准是 +\(\max{\frac{runtimes}{prio}}/ \min{\frac{runtimes}{prio}} < 1.5\).

    +

    CI 的原理是用 ch5_usertest 替代 ch5b_initproc ,使内核在所有测例执行完后直接退出。

    +

    从本章开始,你的内核必须前向兼容,能通过前一章的所有测例。

    +
  • +
+
+

注解

+

利用 git cherry-pick 系列指令,能方便地将前一章分支 commit 移植到本章分支。

+
+
+
+
+

问答作业

+

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<Stride> 的 pop +方法能返回真正最小的 Stride。补全下列代码中的 partial_cmp 函数,假设两个 Stride 永远不会相等。

  • +
+
use core::cmp::Ordering;
+
+struct Stride(u64);
+
+impl PartialOrd for Stride {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        // ...
+    }
+}
+
+impl PartialEq for Stride {
+    fn eq(&self, other: &Self) -> bool {
+        false
+    }
+}
+
+
+

TIPS: 使用 8 bits 存储 stride, BigStride = 255, 则: (125 < 255) == false, (129 < 255) == true.

+
+
+
+

报告要求

+
    +
  • 简单总结你实现的功能(200字以内,不要贴代码)。

  • +
  • 完成问答题。

  • +
  • (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。

  • +
+
+
+ +
+ +
+ +
+
+ + + + + + + + + \ No newline at end of file diff --git a/chapter5/index.html b/chapter5/index.html new file mode 100644 index 0000000..ec588cd --- /dev/null +++ b/chapter5/index.html @@ -0,0 +1,454 @@ + + + + + + + + 第五章:进程及进程管理 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ + +
+ + + + + + + + \ No newline at end of file diff --git a/chapter6/0intro.html b/chapter6/0intro.html new file mode 100644 index 0000000..bc4231f --- /dev/null +++ b/chapter6/0intro.html @@ -0,0 +1,521 @@ + + + + + + + + 引言 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

引言

+
+

本章导读

+

本章我们将实现一个简单的文件系统 – easyfs,能够对 持久存储设备 (Persistent Storage) I/O 资源进行管理;将设计两种文件:常规文件和目录文件,它们均以文件系统所维护的 磁盘文件 形式被组织并保存在持久存储设备上。

+
+
+

实践体验

+
+

注解

+

基于github classroom的开发方式

+

基于github classroom,可方便建立开发用的git repository,并可基于github的 codespace (在线版ubuntu +vscode)在线开发使用。整个开发环境仅仅需要一个网络浏览器。

+
    +
  1. 在网络浏览器中用自己的 github id 登录 github.com

  2. +
  3. 接收 第四个实验(os6)的github classroom在线邀请 ,根据提示一路选择OK即可。

  4. +
  5. 完成第二步后,你的第四个实验的 github repository 会被自动建立好,点击此github repository的链接,就可看到你要完成的第一个实验了。

  6. +
  7. 在你的第四个实验的网页的中上部可以看到一个醒目的 code 绿色按钮,点击后,可以进一步看到 codespace 标签和醒目的 create codesapce on main 绿色按钮。请点击这个绿色按钮,就可以进入到在线的ubuntu +vscode环境中

  8. +
  9. 再按照下面的环境安装提示在vscode的 console 中安装配置开发环境:rustc,qemu等工具。

  10. +
  11. 在vscode的 console 中执行 make setupclassroom_test6 (该命令仅执行一次)配置githubclassroom 自动评分功能。

  12. +
  13. 然后就可以基于在线vscode进行开发、运行、提交等完整的实验过程了。

  14. +
+

上述的3,4,5步不是必须的,你也可以线下本地开发。

+
+

获取本章代码:

+
$ git clone https://github.com/LearningOS/rust-based-os-comp2022.git
+$ cd rust-based-os-comp2022/
+$ make setupclassroom_test6  //注意:这一步很重要,是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次,以后一般就不用执行了,除非 .github/workflows/classroom.yml发生了变化。
+
+
+

在 qemu 模拟器上运行本章代码参考框架:

+
$ cd os6-ref
+$ make run
+
+
+

内核初始化完成之后就会进入shell程序,在这里我们运行一下本章的测例 ch6b_filetest_simple

+
>> ch6b_filetest_simple
+file_test passed!
+Shell: Process 2 exited with code 0
+>>
+
+
+

它会将 Hello, world! 输出到另一个文件 filea ,并读取里面的内容确认输出正确。我们也可以通过命令行工具 ch6b_cat 来查看 filea 中的内容:

+
>> ch6b_cat
+Hello, world!
+Shell: Process 2 exited with code 0
+>>
+
+
+
+
+

easy-fs和 lab4(os6)参考框架:

+
 1├── easy-fs(新增:从内核中独立出来的一个简单的文件系统 EasyFileSystem 的实现)
+ 2│   ├── Cargo.toml
+ 3│   └── src
+ 4│       ├── bitmap.rs(位图抽象)
+ 5│       ├── block_cache.rs(块缓存层,将块设备中的部分块缓存在内存中)
+ 6│       ├── block_dev.rs(声明块设备抽象接口 BlockDevice,需要库的使用者提供其实现)
+ 7│       ├── efs.rs(实现整个 EasyFileSystem 的磁盘布局)
+ 8│       ├── layout.rs(一些保存在磁盘上的数据结构的内存布局)
+ 9│       ├── lib.rs
+10│       └── vfs.rs(提供虚拟文件系统的核心抽象,即索引节点 Inode)
+11├── easy-fs-fuse(新增:将当前 OS 上的应用可执行文件按照 easy-fs 的格式进行打包)
+12│   ├── Cargo.toml
+13│   └── src
+14│       └── main.rs
+15├── os
+16    ├── build.rs(修改:不再需要将用户态程序链接到内核中)
+17    ├── Cargo.toml(修改:新增 Qemu 的块设备驱动依赖 crate)
+18    ├── Makefile(修改:新增文件系统的构建流程)
+19    └── src
+20        ├── config.rs(修改:新增访问块设备所需的一些 MMIO 配置)
+21        ├── ...
+22        ├── drivers(新增:Qemu 平台的块设备驱动)
+23        │   ├── block
+24        │   │   ├── mod.rs(将不同平台上的块设备全局实例化为 BLOCK_DEVICE 提供给其他模块使用)
+25        │   │   └── virtio_blk.rs(Qemu 平台的 virtio-blk 块设备)
+26        │   └── mod.rs
+27        ├── fs(新增:对文件系统及文件抽象)
+28        │   ├── inode.rs(新增:将 easy-fs 提供的 Inode 抽象封装为内核看到的 OSInode
+29        │   │            并实现 fs 子模块的 File Trait)
+30        │   ├── mod.rs
+31        │   └── stdio.rs(新增:将标准输入输出也抽象为文件)
+32        ├── loader.rs(移除:应用加载器 loader 子模块,本章开始从文件系统中加载应用)
+33        ├── mm
+34        │   ├── address.rs
+35        │   ├── frame_allocator.rs
+36        │   ├── heap_allocator.rs
+37        │   ├── memory_set.rs(修改:在创建地址空间的时候插入 MMIO 虚拟页面)
+38        │   ├── mod.rs
+39        │   └── page_table.rs(新增:应用地址空间的缓冲区抽象 UserBuffer 及其迭代器实现)
+40        ├── syscall
+41        │   ├── fs.rs(修改:新增 sys_open,修改sys_read、sys_write)
+42        │   ├── mod.rs
+43        │   └── process.rs(修改:sys_exec 改为从文件系统中加载 ELF)
+44        ├── task
+45            ├── context.rs
+46            ├── manager.rs
+47            ├── mod.rs(修改:初始进程 INITPROC 的初始化)
+48            ├── pid.rs
+49            ├── processor.rs
+50            ├── switch.rs
+51            ├── switch.S
+52            └── task.rs(修改:在任务控制块中加入文件描述符表的相关机制)
+53
+54cloc easy-fs os
+55-------------------------------------------------------------------------------
+56Language                     files          blank        comment           code
+57-------------------------------------------------------------------------------
+58Rust                            41            306            418           3349
+59Assembly                         4             53             26            526
+60make                             1             13              4             48
+61TOML                             2              4              2             23
+62-------------------------------------------------------------------------------
+63SUM:                            48            376            450           3946
+64-------------------------------------------------------------------------------
+
+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter6/1file-descriptor.html b/chapter6/1file-descriptor.html new file mode 100644 index 0000000..dd67f2c --- /dev/null +++ b/chapter6/1file-descriptor.html @@ -0,0 +1,620 @@ + + + + + + + + 文件与文件描述符 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

文件与文件描述符

+
+

文件简介

+

文件可代表很多种不同类型的I/O 资源,但是在进程看来,所有文件的访问都可以通过一个简洁统一的抽象接口 File 进行:

+
// os/src/fs/mod.rs
+
+pub trait File : Send + Sync {
+    fn readable(&self) -> bool;
+    fn writable(&self) -> bool;
+    fn read(&self, buf: UserBuffer) -> usize;
+    fn write(&self, buf: UserBuffer) -> usize;
+}
+
+
+

这个接口在内存和I/O资源之间建立了数据交换的通道。其中 UserBuffer 是我们在 mm 子模块中定义的应用地址空间中的一段缓冲区,我们可以将它看成一个 &[u8] 切片。

+

read 指的是从文件(即I/O资源)中读取数据放到缓冲区中,最多将缓冲区填满(即读取缓冲区的长度那么多字节),并返回实际读取的字节数;而 write 指的是将缓冲区中的数据写入文件,最多将缓冲区中的数据全部写入,并返回直接写入的字节数。

+

回过头来再看一下用户缓冲区的抽象 UserBuffer ,它的声明如下:

+
// 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 还有 IntoIteratorIterator 两个 Trait 的使用方法。

+
+
+

标准输入和标准输出

+

其实我们在第二章就对应用程序引入了基于 文件 的标准输出接口 sys_write ,在第五章引入标准输入接口 sys_read 。我们提前把标准输出设备在文件描述符表中的文件描述符的值规定为 1 ,用 Stdout 表示;把标准输入设备文件描述符规定为 0,用 Stdin 表示 。现在,我们重写这些系统调用,先为标准输入和标准输出实现 File Trait:

+
 1// os/src/fs/stdio.rs
+ 2
+ 3pub struct Stdin;
+ 4
+ 5pub struct Stdout;
+ 6
+ 7impl File for Stdin {
+ 8    fn readable(&self) -> bool { true }
+ 9    fn writable(&self) -> bool { false }
+10    fn read(&self, mut user_buf: UserBuffer) -> usize {
+11        assert_eq!(user_buf.len(), 1);
+12        // busy loop
+13        let mut c: usize;
+14        loop {
+15            c = console_getchar();
+16            if c == 0 {
+17                suspend_current_and_run_next();
+18                continue;
+19            } else {
+20                break;
+21            }
+22        }
+23        let ch = c as u8;
+24        unsafe { user_buf.buffers[0].as_mut_ptr().write_volatile(ch); }
+25        1
+26    }
+27    fn write(&self, _user_buf: UserBuffer) -> usize {
+28        panic!("Cannot write to stdin!");
+29    }
+30}
+31
+32impl File for Stdout {
+33    fn readable(&self) -> bool { false }
+34    fn writable(&self) -> bool { true }
+35    fn read(&self, _user_buf: UserBuffer) -> usize{
+36        panic!("Cannot read from stdout!");
+37    }
+38    fn write(&self, user_buf: UserBuffer) -> usize {
+39        for buffer in user_buf.buffers.iter() {
+40            print!("{}", core::str::from_utf8(*buffer).unwrap());
+41        }
+42        user_buf.len()
+43    }
+44}
+
+
+

可以看到,标准输入文件 Stdin 是只读文件,只允许进程通过 read 从里面读入,目前每次仅支持读入一个字符,其实现与之前的 sys_read 基本相同,只是需要通过 UserBuffer 来获取具体将字节写入的位置。相反,标准输出文件 Stdout 是只写文件,只允许进程通过 write 写入到里面,实现方法是遍历每个切片,将其转化为字符串通过 print! 宏来输出。

+
+
+

文件描述符与文件描述符表

+

为简化操作系统设计实现,可以让每个进程都带有一个线性的 文件描述符表 ,记录所有它请求内核打开并可以读写的那些文件集合。而 文件描述符 (File Descriptor) 则是一个非负整数,表示文件描述符表中一个打开的 文件描述符 所处的位置(可理解为数组下标)。进程通过文件描述符,可以在自身的文件描述符表中找到对应的文件记录信息,从而也就找到了对应的文件,并对文件进行读写。当打开( open )或创建( create ) 一个文件的时候,如果顺利,内核会返回给应用刚刚打开或创建的文件对应的文件描述符;而当应用想关闭( close )一个文件的时候,也需要向内核提供对应的文件描述符。

+
+
+

文件I/O操作

+

在进程控制块中加入文件描述符表的相应字段:

+
 1// os/src/task/task.rs
+ 2
+ 3pub struct TaskControlBlockInner {
+ 4    pub trap_cx_ppn: PhysPageNum,
+ 5    pub base_size: usize,
+ 6    pub task_cx: TaskContext,
+ 7    pub task_status: TaskStatus,
+ 8    pub memory_set: MemorySet,
+ 9    pub parent: Option<Weak<TaskControlBlock>>,
+10    pub children: Vec<Arc<TaskControlBlock>>,
+11    pub exit_code: i32,
+12    pub fd_table: Vec<Option<Arc<dyn File + Send + Sync>>>,
+13}
+
+
+

可以看到 fd_table 的类型包含多层嵌套,我们从外到里分别说明:

+
    +
  • Vec 的动态长度特性使得我们无需设置一个固定的文件描述符数量上限;

  • +
  • Option 使得我们可以区分一个文件描述符当前是否空闲,当它是 None 的时候是空闲的,而 Some 则代表它已被占用;

  • +
  • Arc 首先提供了共享引用能力。后面我们会提到,可能会有多个进程共享同一个文件对它进行读写。此外被它包裹的内容会被放到内核堆而不是栈上,于是它便不需要在编译期有着确定的大小;

  • +
  • dyn 关键字表明 Arc 里面的类型实现了 File/Send/Sync 三个 Trait ,但是编译期无法知道它具体是哪个类型(可能是任何实现了 File Trait 的类型如 Stdin/Stdout ,故而它所占的空间大小自然也无法确定),需要等到运行时才能知道它的具体类型。

  • +
+
+

注解

+

Rust 语法卡片:Rust 中的多态

+

在编程语言中, 多态 (Polymorphism) 指的是在同一段代码中可以隐含多种不同类型的特征。在 Rust 中主要通过泛型和 Trait 来实现多态。

+

泛型是一种 编译期多态 (Static Polymorphism),在编译一个泛型函数的时候,编译器会对于所有可能用到的类型进行实例化并对应生成一个版本的汇编代码,在编译期就能知道选取哪个版本并确定函数地址,这可能会导致生成的二进制文件体积较大;而 Trait 对象(也即上面提到的 dyn 语法)是一种 运行时多态 (Dynamic Polymorphism),需要在运行时查一种类似于 C++ 中的 虚表 (Virtual Table) 才能找到实际类型对于抽象接口实现的函数地址并进行调用,这样会带来一定的运行时开销,但是更为灵活。

+
+

当新建一个进程的时候,我们需要按照先前的说明为进程打开标准输入文件和标准输出文件:

+
 1// os/src/task/task.rs
+ 2
+ 3impl TaskControlBlock {
+ 4    pub fn new(elf_data: &[u8]) -> Self {
+ 5        ...
+ 6        let task_control_block = Self {
+ 7            pid: pid_handle,
+ 8            kernel_stack,
+ 9            inner: unsafe {
+10                UPSafeCell::new(TaskControlBlockInner {
+11                    trap_cx_ppn,
+12                    base_size: user_sp,
+13                    task_cx: TaskContext::goto_trap_return(kernel_stack_top),
+14                    task_status: TaskStatus::Ready,
+15                    memory_set,
+16                    parent: None,
+17                    children: Vec::new(),
+18                    exit_code: 0,
+19                    fd_table: vec![
+20                        // 0 -> stdin
+21                        Some(Arc::new(Stdin)),
+22                        // 1 -> stdout
+23                        Some(Arc::new(Stdout)),
+24                        // 2 -> stderr
+25                        Some(Arc::new(Stdout)),
+26                    ],
+27                })
+28            },
+29        };
+30        ...
+31    }
+32}
+
+
+

此外,在 fork 时,子进程需要完全继承父进程的文件描述符表来和父进程共享所有文件。这样,即使我们仅手动为初始进程 initproc 打开了标准输入输出,所有进程也都可以访问它们。

+
+
+

文件读写系统调用

+

基于文件抽象接口和文件描述符表,我们终于可以让文件读写系统调用 sys_read/write 变得更加具有普适性,不仅仅局限于之前特定的标准输入输出:

+
// 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/chapter6/1fs-interface.html b/chapter6/1fs-interface.html new file mode 100644 index 0000000..5d8144c --- /dev/null +++ b/chapter6/1fs-interface.html @@ -0,0 +1,510 @@ + + + + + + + + 文件系统接口 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

文件系统接口

+
+

简易文件与目录抽象

+

与课堂所学相比,我们实现的文件系统进行了很大的简化:

+
    +
  • 扁平化:仅存在根目录 / 一个目录,所有的文件都放在根目录内。直接以文件名索引文件。

  • +
  • 不设置用户和用户组概念,不记录文件访问/修改的任何时间戳,不支持软硬链接。

  • +
  • 只实现了最基本的文件系统相关系统调用。

  • +
+
+
+

打开与读写文件的系统调用

+
+

打开文件

+
/// 功能:打开一个常规文件,并返回可以访问它的文件描述符。
+/// 参数:path 描述要打开的文件的文件名(简单起见,文件系统不需要支持目录,所有的文件都放在根目录 / 下),
+/// flags 描述打开文件的标志,具体含义下面给出。
+/// dirfd 和 mode 仅用于保证兼容性,忽略
+/// 返回值:如果出现了错误则返回 -1,否则返回打开常规文件的文件描述符。可能的错误原因是:文件不存在。
+/// syscall ID:56
+fn sys_openat(dirfd: usize, path: &str, flags: u32, mode: u32) -> isize
+
+
+

目前我们的内核支持以下几种标志(多种不同标志可能共存):

+
    +
  • 如果 flags 为 0,则表示以只读模式 RDONLY 打开;

  • +
  • 如果 flags 第 0 位被设置(0x001),表示以只写模式 WRONLY 打开;

  • +
  • 如果 flags 第 1 位被设置(0x002),表示既可读又可写 RDWR

  • +
  • 如果 flags 第 9 位被设置(0x200),表示允许创建文件 CREATE ,在找不到该文件的时候应创建文件;如果该文件已经存在则应该将该文件的大小归零;

  • +
  • 如果 flags 第 10 位被设置(0x400),则在打开文件的时候应该清空文件的内容并将该文件的大小归零,也即 TRUNC

  • +
+

在用户库 user_lib 中,我们将该系统调用封装为 open 接口:

+
// 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_openat(AT_FDCWD as usize, path, flags.bits, OpenFlags::RDWR.bits)
+}
+
+
+

借助 bitflags! 宏我们将一个 u32 的 flags 包装为一个 OpenFlags 结构体,可以从它的 bits 字段获得 u32 表示。

+
+
+

顺序读写文件

+

在打开一个文件之后,我们就可以用之前的 sys_read/sys_write 两个系统调用来对它进行读写了。本教程只实现文件的顺序读写,而不考虑随机读写。

+

以本章的测试用例 ch6b_filetest_simple 来介绍文件系统接口的使用方法:

+
 1// user/src/bin/ch6b_filetest_simple.rs
+ 2
+ 3#![no_std]
+ 4#![no_main]
+ 5
+ 6#[macro_use]
+ 7extern crate user_lib;
+ 8
+ 9use user_lib::{
+10    open,
+11    close,
+12    read,
+13    write,
+14    OpenFlags,
+15};
+16
+17#[no_mangle]
+18pub fn main() -> i32 {
+19    let test_str = "Hello, world!";
+20    let filea = "filea\0";
+21    let fd = open(filea, OpenFlags::CREATE | OpenFlags::WRONLY);
+22    assert!(fd > 0);
+23    let fd = fd as usize;
+24    write(fd, test_str.as_bytes());
+25    close(fd);
+26
+27    let fd = open(filea, OpenFlags::RDONLY);
+28    assert!(fd > 0);
+29    let fd = fd as usize;
+30    let mut buffer = [0u8; 100];
+31    let read_len = read(fd, &mut buffer) as usize;
+32    close(fd);
+33
+34    assert_eq!(
+35        test_str,
+36        core::str::from_utf8(&buffer[..read_len]).unwrap(),
+37    );
+38    println!("file_test passed!");
+39    0
+40}
+
+
+
    +
  • 第 20~25 行,我们以 只写 + 创建 的模式打开文件 filea ,向其中写入字符串 Hello, world! 而后关闭文件。

  • +
  • 第 27~32 行,我们以只读 的方式将文件 filea 的内容读取到缓冲区 buffer 中。 filea 的总大小不超过缓冲区的大小,因此通过单次 read 即可将内容全部读出来而更常见的情况是需要进行多次 read ,直到返回值为 0 才能确认文件已被读取完毕。

  • +
+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter6/2fs-implementation-1.html b/chapter6/2fs-implementation-1.html new file mode 100644 index 0000000..f7ab2d0 --- /dev/null +++ b/chapter6/2fs-implementation-1.html @@ -0,0 +1,1014 @@ + + + + + + + + 简易文件系统 easy-fs (上) - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

简易文件系统 easy-fs (上)

+
+

松耦合模块化设计思路

+

内核的功能越来越多,代码量也越来越大。出于解耦合考虑,文件系统 easy-fs 被从内核中分离出来,分成两个不同的 crate :

+
    +
  • easy-fs 是简易文件系统的本体;

  • +
  • easy-fs-fuse 是能在开发环境(如 Ubuntu)中运行的应用程序,用于将应用打包为 easy-fs 格式的文件系统镜像,也可以用来对 easy-fs 进行测试。

  • +
+

easy-fs与底层设备驱动之间通过抽象接口 BlockDevice 来连接,采用轮询方式访问 virtio_blk 虚拟磁盘设备,避免调用外设中断的相关内核函数。easy-fs 避免了直接访问进程相关的数据和函数,从而能独立于内核开发。

+

easy-fs crate 以层次化思路设计,自下而上可以分成五个层次:

+
    +
  1. 磁盘块设备接口层:以块为单位对磁盘块设备进行读写的 trait 接口

  2. +
  3. 块缓存层:在内存中缓存磁盘块的数据,避免频繁读写磁盘

  4. +
  5. 磁盘数据结构层:磁盘上的超级块、位图、索引节点、数据块、目录项等核心数据结构和相关处理

  6. +
  7. 磁盘块管理器层:合并了上述核心数据结构和磁盘布局所形成的磁盘文件系统数据结构

  8. +
  9. 索引节点层:管理索引节点,实现了文件创建/文件打开/文件读写等成员函数

  10. +
+

本节将介绍前三层,下一节将介绍后两层。

+../_images/easy-fs-demo.png +
+
+

块设备接口层

+

easy-fs 库的最底层声明了块设备的抽象接口 BlockDevice

+
// 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 的使用者将负责提供抽象方法的实现。

+
+
+

块缓存层

+

为了加速 IO,内存可以作为磁盘的缓存。实现磁盘块缓存功能的代码在 block_cache.rs

+
+

块缓存

+

块缓存 BlockCache 的声明如下:

+
// 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<dyn BlockDevice>,
+    modified: bool,
+}
+
+
+

其中:

+
    +
  • cache 是一个 512 字节的数组,表示位于内存中的缓冲区;

  • +
  • block_id 记录了这个块的编号;

  • +
  • block_device 记录块所属的底层设备;

  • +
  • modified 记录自从这个块缓存从磁盘载入内存之后,它有没有被修改过。

  • +
+

创建 BlockCache 时,将一个块从磁盘读到缓冲区 cache

+
// easy-fs/src/block_cache.rs
+
+impl BlockCache {
+    /// Load a new BlockCache from disk.
+    pub fn new(
+        block_id: usize,
+        block_device: Arc<dyn BlockDevice>
+    ) -> Self {
+        let mut cache = [0u8; BLOCK_SZ];
+        block_device.read_block(block_id, &mut cache);
+        Self {
+            cache,
+            block_id,
+            block_device,
+            modified: false,
+        }
+    }
+}
+
+
+

BlockCache 向上提供以下方法:

+
 1// easy-fs/src/block_cache.rs
+ 2
+ 3impl BlockCache {
+ 4    fn addr_of_offset(&self, offset: usize) -> usize {
+ 5        &self.cache[offset] as *const _ as usize
+ 6    }
+ 7
+ 8    pub fn get_ref<T>(&self, offset: usize) -> &T where T: Sized {
+ 9        let type_size = core::mem::size_of::<T>();
+10        assert!(offset + type_size <= BLOCK_SZ);
+11        let addr = self.addr_of_offset(offset);
+12        unsafe { &*(addr as *const T) }
+13    }
+14
+15    pub fn get_mut<T>(&mut self, offset: usize) -> &mut T where T: Sized {
+16        let type_size = core::mem::size_of::<T>();
+17        assert!(offset + type_size <= BLOCK_SZ);
+18        self.modified = true;
+19        let addr = self.addr_of_offset(offset);
+20        unsafe { &mut *(addr as *mut T) }
+21    }
+22}
+
+
+
    +
  • addr_of_offset 可以得到一个 BlockCache 内部的缓冲区中指定偏移量 offset 的字节地址;

  • +
  • get_ref 是一个泛型方法,它可以获取缓冲区中的位于偏移量 offset 的一个类型为 T 的磁盘上数据结构的不可变引用。该泛型方法的 Trait Bound 限制类型 T 必须是一个编译时已知大小的类型,我们通过 core::mem::size_of::<T>() 在编译时获取类型 T 的大小并确认该数据结构被整个包含在磁盘块及其缓冲区之内。这里编译器会自动进行生命周期标注,约束返回的引用的生命周期不超过 BlockCache 自身,在使用的时候我们会保证这一点。

  • +
  • get_mutget_ref 的不同之处在于它会获取磁盘上数据结构的可变引用,由此可以对数据结构进行修改。由于这些数据结构目前位于内存中的缓冲区中,我们需要将 BlockCachemodified 标记为 true 表示该缓冲区已经被修改,之后需要将数据写回磁盘块才能真正将修改同步到磁盘。

  • +
+

我们可以将 get_ref/get_mut 进一步封装为更为易用的形式:

+
// easy-fs/src/block_cache.rs
+
+impl BlockCache {
+    pub fn read<T, V>(&self, offset: usize, f: impl FnOnce(&T) -> V) -> V {
+        f(self.get_ref(offset))
+    }
+
+    pub fn modify<T, V>(&mut self, offset:usize, f: impl FnOnce(&mut T) -> V) -> V {
+        f(self.get_mut(offset))
+    }
+}
+
+
+

它们的含义是:在 BlockCache 缓冲区偏移量为 offset 的位置,获取一个类型为 T 不可变/可变引用,将闭包 f 作用于这个引用,返回 f 的返回值。 中所定义的操作。

+

这里我们传入闭包的类型为 FnOnce ,这是因为闭包里面的变量被捕获的方式涵盖了不可变引用/可变引用/和 move 三种可能性,故而我们需要选取范围最广的 FnOnce 。参数中的 impl 关键字体现了一种类似泛型的静态分发功能。

+
+

警告

+

Rust 语法卡片:闭包

+

闭包是持有外部环境变量的函数。所谓外部环境, 就是指创建闭包时所在的词法作用域。Rust中定义的闭包,按照对外部环境变量的使用方式(借用、复制、转移所有权),分为三个类型: Fn、FnMut、FnOnce。Fn类型的闭包会在闭包内部以共享借用的方式使用环境变量;FnMut类型的闭包会在闭包内部以独占借用的方式使用环境变量;而FnOnce类型的闭包会在闭包内部以所有者的身份使用环境变量。由此可见,根据闭包内使用环境变量的方式,即可判断创建出来的闭包的类型。

+
+

BlockCache 的生命周期结束后,缓冲区也会被回收, modified 标记将会决定数据是否需要写回磁盘:

+
// easy-fs/src/block_cache.rs
+
+impl Drop for BlockCache {
+    fn drop(&mut self) {
+        if self.modified {
+            self.modified = false;
+            self.block_device.write_block(self.block_id, &self.cache);
+        }
+    }
+}
+
+
+
+
+

块缓存全局管理器

+

内存只能同时缓存有限个磁盘块。当我们要对一个磁盘块进行读写时,块缓存全局管理器检查它是否已经被载入内存中,如果是则直接返回,否则就读取磁盘块到内存。如果内存中驻留的磁盘块缓冲区的数量已满,则需要进行缓存替换。这里使用一种类 FIFO 的缓存替换算法,在管理器中只需维护一个队列:

+
// easy-fs/src/block_cache.rs
+
+use alloc::collections::VecDeque;
+
+pub struct BlockCacheManager {
+    queue: VecDeque<(usize, Arc<Mutex<BlockCache>>)>,
+}
+
+
+

队列 queue 维护块编号和块缓存的二元组。块缓存的类型是一个 Arc<Mutex<BlockCache>> ,这是 Rust 中的经典组合,它可以同时提供共享引用和互斥访问。这里的共享引用意义在于块缓存既需要在管理器 BlockCacheManager 保留一个引用,还需要将引用返回给块缓存的请求者。而互斥访问在单核上的意义在于提供内部可变性通过编译,在多核环境下则可以帮助我们避免可能的并发冲突。

+

get_block_cache 方法尝试从块缓存管理器中获取一个编号为 block_id 的块缓存,如果找不到的话会读取磁盘,还有可能会发生缓存替换:

+
 1// easy-fs/src/block_cache.rs
+ 2
+ 3impl BlockCacheManager {
+ 4    pub fn get_block_cache(
+ 5        &mut self,
+ 6        block_id: usize,
+ 7        block_device: Arc<dyn BlockDevice>,
+ 8    ) -> Arc<Mutex<BlockCache>> {
+ 9        if let Some(pair) = self.queue
+10            .iter()
+11            .find(|pair| pair.0 == block_id) {
+12                Arc::clone(&pair.1)
+13        } else {
+14            // substitute
+15            if self.queue.len() == BLOCK_CACHE_SIZE {
+16                // from front to tail
+17                if let Some((idx, _)) = self.queue
+18                    .iter()
+19                    .enumerate()
+20                    .find(|(_, pair)| Arc::strong_count(&pair.1) == 1) {
+21                    self.queue.drain(idx..=idx);
+22                } else {
+23                    panic!("Run out of BlockCache!");
+24                }
+25            }
+26            // load block into mem and push back
+27            let block_cache = Arc::new(Mutex::new(
+28                BlockCache::new(block_id, Arc::clone(&block_device))
+29            ));
+30            self.queue.push_back((block_id, Arc::clone(&block_cache)));
+31            block_cache
+32        }
+33    }
+34}
+
+
+
    +
  • 第 9 行,遍历整个队列试图找到一个编号相同的块缓存,如果找到,将块缓存管理器中保存的块缓存的引用复制一份并返回;

  • +
  • 第 13 行对应找不到的情况,此时必须将块从磁盘读入内存中的缓冲区。读取前需要判断已保存的块数量是否达到了上限。是,则执行缓存替换算法,替换的标准是其强引用计数 \(\eq 1\) ,即除了块缓存管理器保留的一份副本之外,在外面没有副本正在使用。

  • +
  • 第 27 行开始,创建一个新的块缓存(会触发 read_block 进行块读取)并加入到队尾,最后返回给请求者。

  • +
+
+
+
+

磁盘布局及磁盘上数据结构

+

磁盘数据结构层的代码在 layout.rsbitmap.rs 中。

+
+

easy-fs 磁盘布局概述

+

easy-fs 磁盘按照块编号从小到大顺序分成 5 个连续区域:

+
    +
  • 第一个区域只包括一个块,它是 超级块 (Super Block),用于定位其他连续区域的位置,检查文件系统合法性。

  • +
  • 第二个区域是一个索引节点位图,长度为若干个块。它记录了索引节点区域中有哪些索引节点已经被分配出去使用了。

  • +
  • 第三个区域是索引节点区域,长度为若干个块。其中的每个块都存储了若干个索引节点。

  • +
  • 第四个区域是一个数据块位图,长度为若干个块。它记录了后面的数据块区域中有哪些已经被分配出去使用了。

  • +
  • 最后的区域则是数据块区域,其中的每个被分配出去的块保存了文件或目录的具体内容。

  • +
+
+
+

easy-fs 超级块

+

超级块 SuperBlock 的内容如下:

+
// 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 布局中后四个连续区域的长度各为多少个块。

+

下面是它实现的方法:

+
// 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,
+    );
+
+    pub fn is_valid(&self) -> bool {
+        self.magic == EFS_MAGIC
+    }
+}
+
+
+
    +
  • initialize 用于在创建一个 easy-fs 的时候初始化超级块,各个区域的块数由更上层的磁盘块管理器传入。

  • +
  • is_valid 则可以通过魔数判断超级块所在的文件系统是否合法。

  • +
+
+
+

位图

+

在 easy-fs 布局中存在两类不同的位图,分别对索引节点和数据块进行管理。每个位图都由若干个块组成,每个块大小 4096 bits。每个 bit 都代表一个索引节点/数据块的分配状态。

+
// easy-fs/src/bitmap.rs
+
+pub struct Bitmap {
+    start_block_id: usize,
+    blocks: usize,
+}
+
+type BitmapBlock = [u64; 64];
+
+
+

Bitmap 是位图区域的管理器,它保存了位图区域的起始块编号和块数。而 BitmapBlock 将位图区域中的一个磁盘块解释为长度为 64 的一个 u64 数组。

+

首先来看 Bitmap 如何分配一个bit:

+
 1// easy-fs/src/bitmap.rs
+ 2
+ 3const BLOCK_BITS: usize = BLOCK_SZ * 8;
+ 4
+ 5impl Bitmap {
+ 6    pub fn alloc(&self, block_device: &Arc<dyn BlockDevice>) -> Option<usize> {
+ 7        for block_id in 0..self.blocks {
+ 8            let pos = get_block_cache(
+ 9                block_id + self.start_block_id as usize,
+10                Arc::clone(block_device),
+11            )
+12            .lock()
+13            .modify(0, |bitmap_block: &mut BitmapBlock| {
+14                if let Some((bits64_pos, inner_pos)) = bitmap_block
+15                    .iter()
+16                    .enumerate()
+17                    .find(|(_, bits64)| **bits64 != u64::MAX)
+18                    .map(|(bits64_pos, bits64)| {
+19                        (bits64_pos, bits64.trailing_ones() as usize)
+20                    }) {
+21                    // modify cache
+22                    bitmap_block[bits64_pos] |= 1u64 << inner_pos;
+23                    Some(block_id * BLOCK_BITS + bits64_pos * 64 + inner_pos as usize)
+24                } else {
+25                    None
+26                }
+27            });
+28            if pos.is_some() {
+29                return pos;
+30            }
+31        }
+32        None
+33    }
+34}
+
+
+

其主要思路是遍历区域中的每个块,再在每个块中以bit组(每组 64 bits)为单位进行遍历,找到一个尚未被全部分配出去的组,最后在里面分配一个bit。它将会返回分配的bit所在的位置,等同于索引节点/数据块的编号。如果所有bit均已经被分配出去了,则返回 None

+

第 7 行枚举区域中的每个块(编号为 block_id ),在循环内部我们需要读写这个块,在块内尝试找到一个空闲的bit并置 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 中找到一个空闲的bit并返回其位置,如果不存在的话则返回 None 。它的思路是,遍历每 64 bits构成的组(一个 u64 ),如果它并没有达到 u64::MAX (即 \(2^{64}-1\) ),则通过 u64::trailing_ones 找到最低的一个 0 并置为 1 。如果能够找到的话,bit组的编号将保存在变量 bits64_pos 中,而分配的bit在组内的位置将保存在变量 inner_pos 中。在返回分配的bit编号的时候,它的计算方式是 block_id*BLOCK_BITS+bits64_pos*64+inner_pos 。注意闭包中的 block_id 并不在闭包的参数列表中,因此它是从外部环境(即自增 block_id 的循环)中捕获到的。

  • +
+

我们一旦在某个块中找到一个空闲的bit并成功分配,就不再考虑后续的块。第 28 行体现了提前返回的思路。

+

回收 bit 的方法类似,感兴趣的读者可自行阅读源代码。

+
+
+

磁盘上索引节点

+

在磁盘上的索引节点区域,每个块上都保存着若干个索引节点 DiskInode

+
// 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 都用来指向数据块区域中一个保存该文件内容的数据块,因此,最多能够索引 \(\frac{512}{4}=128\) 个数据块,对应 64KiB 的内容。

  • +
  • 当文件大小超过直接索引和一级索引支持的容量上限 78KiB 的时候,就需要用到二级间接索引 indirect2 。它指向一个位于数据块区域中的二级索引块。二级索引块中的每个 u32 指向一个不同的一级索引块,这些一级索引块也位于数据块区域中。因此,通过二级间接索引最多能够索引 \(128\times 64\text{KiB}=8\text{MiB}\) 的内容。

  • +
+

为了充分利用空间,我们将 DiskInode 的大小设置为 128 字节,每个块正好能够容纳 4 个 DiskInode 。在后续需要支持更多类型的元数据的时候,可以适当缩减直接索引 direct 的块数,并将节约出来的空间用来存放其他元数据,仍可保证 DiskInode 的总大小为 128 字节。

+

通过 initialize 方法可以初始化一个 DiskInode 为一个文件或目录:

+
// 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_fileis_dir 两个方法可以用来确认 DiskInode 的类型为文件还是目录:

+
// 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 个数据块的块编号,这样后续才能对这个数据块进行访问:

+
 1// easy-fs/src/layout.rs
+ 2
+ 3const INODE_INDIRECT1_COUNT: usize = BLOCK_SZ / 4;
+ 4const INDIRECT1_BOUND: usize = DIRECT_BOUND + INODE_INDIRECT1_COUNT;
+ 5type IndirectBlock = [u32; BLOCK_SZ / 4];
+ 6
+ 7impl DiskInode {
+ 8    pub fn get_block_id(&self, inner_id: u32, block_device: &Arc<dyn BlockDevice>) -> u32 {
+ 9        let inner_id = inner_id as usize;
+10        if inner_id < INODE_DIRECT_COUNT {
+11            self.direct[inner_id]
+12        } else if inner_id < INDIRECT1_BOUND {
+13            get_block_cache(self.indirect1 as usize, Arc::clone(block_device))
+14                .lock()
+15                .read(0, |indirect_block: &IndirectBlock| {
+16                    indirect_block[inner_id - INODE_DIRECT_COUNT]
+17                })
+18        } else {
+19            let last = inner_id - INDIRECT1_BOUND;
+20            let indirect1 = get_block_cache(
+21                self.indirect2 as usize,
+22                Arc::clone(block_device)
+23            )
+24            .lock()
+25            .read(0, |indirect2: &IndirectBlock| {
+26                indirect2[last / INODE_INDIRECT1_COUNT]
+27            });
+28            get_block_cache(
+29                indirect1 as usize,
+30                Arc::clone(block_device)
+31            )
+32            .lock()
+33            .read(0, |indirect1: &IndirectBlock| {
+34                indirect1[last % INODE_INDIRECT1_COUNT]
+35            })
+36        }
+37    }
+38}
+
+
+

这里需要说明的是:

+
    +
  • 第 10/12/18 行分别利用直接索引/一级索引和二级索引,具体选用哪种索引方式取决于 block_id 所在的区间。

  • +
  • 在对一个索引块进行操作的时候,我们将其解析为磁盘数据结构 IndirectBlock ,实质上就是一个 u32 数组,每个都指向一个下一级索引块或者数据块。

  • +
  • 对于二级索引的情况,需要先查二级索引块找到挂在它下面的一级索引块,再通过一级索引块找到数据块。

  • +
+

在初始化之后文件/目录的 size 均为 0 ,此时并不会索引到任何数据块。它需要通过 increase_size 方法逐步扩充容量。在扩充的时候,自然需要一些新的数据块来作为索引块或是保存内容的数据块。我们需要先编写一些辅助方法来确定在容量扩充的时候额外需要多少块:

+
// 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 可以计算将一个 DiskInodesize 扩容到 new_size 需要额外多少个数据和索引块。这只需要调用两次 total_blocks 作差即可。

+

下面给出 increase_size 方法的接口:

+
// easy-fs/src/layout.rs
+
+impl DiskInode {
+    pub fn increase_size(
+        &mut self,
+        new_size: u32,
+        new_blocks: Vec<u32>,
+        block_device: &Arc<dyn BlockDevice>,
+    );
+}
+
+
+

其中 new_size 表示容量扩充之后的文件大小; new_blocks 是一个保存了本次容量扩充所需块编号的向量,这些块都是由上层的磁盘块管理器负责分配的。 increase_size 的实现有些复杂,在这里不详细介绍。大致的思路是按照直接索引、一级索引再到二级索引的顺序进行扩充。

+

有些时候我们还需要清空文件的内容并回收所有数据和索引块。这是通过 clear_size 方法来实现的:

+
// 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<dyn BlockDevice>) -> Vec<u32>;
+}
+
+
+

它会将回收的所有块的编号保存在一个向量中返回给磁盘块管理器。它的实现原理和 increase_size 一样也分为多个阶段,在这里不展开。

+

接下来需要考虑通过 DiskInode 来读写它索引的那些数据块中的数据。这些数据可以被视为一个字节序列,而每次我们都是选取其中的一段连续区间进行操作,以 read_at 为例:

+
 1// easy-fs/src/layout.rs
+ 2
+ 3type DataBlock = [u8; BLOCK_SZ];
+ 4
+ 5impl DiskInode {
+ 6    pub fn read_at(
+ 7        &self,
+ 8        offset: usize,
+ 9        buf: &mut [u8],
+10        block_device: &Arc<dyn BlockDevice>,
+11    ) -> usize {
+12        let mut start = offset;
+13        let end = (offset + buf.len()).min(self.size as usize);
+14        if start >= end {
+15            return 0;
+16        }
+17        let mut start_block = start / BLOCK_SZ;
+18        let mut read_size = 0usize;
+19        loop {
+20            // calculate end of current block
+21            let mut end_current_block = (start / BLOCK_SZ + 1) * BLOCK_SZ;
+22            end_current_block = end_current_block.min(end);
+23            // read and update read size
+24            let block_read_size = end_current_block - start;
+25            let dst = &mut buf[read_size..read_size + block_read_size];
+26            get_block_cache(
+27                self.get_block_id(start_block as u32, block_device) as usize,
+28                Arc::clone(block_device),
+29            )
+30            .lock()
+31            .read(0, |data_block: &DataBlock| {
+32                let src = &data_block[start % BLOCK_SZ..start % BLOCK_SZ + block_read_size];
+33                dst.copy_from_slice(src);
+34            });
+35            read_size += block_read_size;
+36            // move to next block
+37            if end_current_block == end { break; }
+38            start_block += 1;
+39            start = end_current_block;
+40        }
+41        read_size
+42    }
+43}
+
+
+

它的含义是:将文件内容从 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 将文件大小扩充到区间的右端保证写入的完整性。

+
+
+

目录项

+

对于文件而言,它的内容在文件系统或内核看来没有任何既定的格式,只是一个字节序列。目录的内容却需要遵从一种特殊的格式,它可以看成一个目录项的序列,每个目录项都是一个二元组,包括目录下文件的文件名和索引节点编号。目录项 DirEntry 的定义如下:

+
// 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。目录项自身长 32 字节,每个数据块可以存储 16 个目录项。可以通过 emptynew 方法生成目录项,通过 nameinode_number 方法取出目录项中的内容:

+
// easy-fs/src/layout.rs
+
+impl DirEntry {
+    pub fn empty() -> Self;
+    pub fn new(name: &str, inode_number: u32) -> Self;
+    pub fn name(&self) -> &str;
+    pub fn inode_number(&self) -> u32
+}
+
+
+

在从目录中读取目录项,或将目录项写入目录时,需要将目录项转化为缓冲区(即字节切片)的形式来符合 read_at OR write_at 接口的要求:

+
// 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,
+            )
+        }
+    }
+}
+
+
+
+
+
+ +
+ +
+ +
+
+ + + + + + + + + \ No newline at end of file diff --git a/chapter6/2fs-implementation-2.html b/chapter6/2fs-implementation-2.html new file mode 100644 index 0000000..d2ffd3b --- /dev/null +++ b/chapter6/2fs-implementation-2.html @@ -0,0 +1,959 @@ + + + + + + + + 简易文件系统 easy-fs (下) - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

简易文件系统 easy-fs (下)

+
+

磁盘块管理器

+

本层的代码在 efs.rs 中。

+
// easy-fs/src/efs.rs
+
+pub struct EasyFileSystem {
+    pub block_device: Arc<dyn BlockDevice>,
+    pub inode_bitmap: Bitmap,
+    pub data_bitmap: Bitmap,
+    inode_area_start_block: u32,
+    data_area_start_block: u32,
+}
+
+
+

EasyFileSystem 包含索引节点和数据块的两个位图 inode_bitmapdata_bitmap ,还记录下索引节点区域和数据块区域起始块编号方便确定每个索引节点和数据块在磁盘上的具体位置。我们还要在其中保留块设备的一个指针 block_device ,在进行后续操作的时候,该指针会被拷贝并传递给下层的数据结构,让它们也能够直接访问块设备。

+

通过 create 方法可以在块设备上创建并初始化一个 easy-fs 文件系统:

+
 1// easy-fs/src/efs.rs
+ 2
+ 3impl EasyFileSystem {
+ 4    pub fn create(
+ 5        block_device: Arc<dyn BlockDevice>,
+ 6        total_blocks: u32,
+ 7        inode_bitmap_blocks: u32,
+ 8    ) -> Arc<Mutex<Self>> {
+ 9        // calculate block size of areas & create bitmaps
+10        let inode_bitmap = Bitmap::new(1, inode_bitmap_blocks as usize);
+11        let inode_num = inode_bitmap.maximum();
+12        let inode_area_blocks =
+13            ((inode_num * core::mem::size_of::<DiskInode>() + BLOCK_SZ - 1) / BLOCK_SZ) as u32;
+14        let inode_total_blocks = inode_bitmap_blocks + inode_area_blocks;
+15        let data_total_blocks = total_blocks - 1 - inode_total_blocks;
+16        let data_bitmap_blocks = (data_total_blocks + 4096) / 4097;
+17        let data_area_blocks = data_total_blocks - data_bitmap_blocks;
+18        let data_bitmap = Bitmap::new(
+19            (1 + inode_bitmap_blocks + inode_area_blocks) as usize,
+20            data_bitmap_blocks as usize,
+21        );
+22        let mut efs = Self {
+23            block_device: Arc::clone(&block_device),
+24            inode_bitmap,
+25            data_bitmap,
+26            inode_area_start_block: 1 + inode_bitmap_blocks,
+27            data_area_start_block: 1 + inode_total_blocks + data_bitmap_blocks,
+28        };
+29        // clear all blocks
+30        for i in 0..total_blocks {
+31            get_block_cache(
+32                i as usize,
+33                Arc::clone(&block_device)
+34            )
+35            .lock()
+36            .modify(0, |data_block: &mut DataBlock| {
+37                for byte in data_block.iter_mut() { *byte = 0; }
+38            });
+39        }
+40        // initialize SuperBlock
+41        get_block_cache(0, Arc::clone(&block_device))
+42        .lock()
+43        .modify(0, |super_block: &mut SuperBlock| {
+44            super_block.initialize(
+45                total_blocks,
+46                inode_bitmap_blocks,
+47                inode_area_blocks,
+48                data_bitmap_blocks,
+49                data_area_blocks,
+50            );
+51        });
+52        // write back immediately
+53        // create a inode for root node "/"
+54        assert_eq!(efs.alloc_inode(), 0);
+55        let (root_inode_block_id, root_inode_offset) = efs.get_disk_inode_pos(0);
+56        get_block_cache(
+57            root_inode_block_id as usize,
+58            Arc::clone(&block_device)
+59        )
+60        .lock()
+61        .modify(root_inode_offset, |disk_inode: &mut DiskInode| {
+62            disk_inode.initialize(DiskInodeType::Directory);
+63        });
+64        Arc::new(Mutex::new(efs))
+65    }
+66}
+
+
+
    +
  • 第 10~21 行根据传入的参数计算每个区域各应该包含多少块。根据 inode 位图的大小计算 inode 区域至少需要多少个块才能够使得 inode 位图中的每个bit都能够有一个实际的 inode 可以对应,这样就确定了 inode 位图区域和 inode 区域的大小。剩下的块都分配给数据块位图区域和数据块区域。我们希望数据块位图中的每个bit仍然能够对应到一个数据块,但是数据块位图又不能过小,不然会造成某些数据块永远不会被使用。因此数据块位图区域最合理的大小是剩余的块数除以 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_cachemodify 了。

  • +
+

通过 open 方法可以从一个已写入了 easy-fs 镜像的块设备上打开我们的 easy-fs :

+
// easy-fs/src/efs.rs
+
+impl EasyFileSystem {
+    pub fn open(block_device: Arc<dyn BlockDevice>) -> Arc<Mutex<Self>> {
+        // 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位图 或数据块位图上分配的 bit 编号,来算出各个存储inode和数据块的磁盘块在磁盘上的实际位置。

+
// 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::<DiskInode>();
+        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 和数据块的分配/回收也由它负责:

+
// 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_datadealloc_data 分配/回收数据块传入/返回的参数都表示数据块在块设备上的编号,而不是在数据块位图中分配的bit编号;

  • +
  • dealloc_inode 未实现,不支持文件删除。

  • +
+
+
+

索引节点

+

服务于文件相关系统调用的索引节点层的代码在 vfs.rs 中。

+

EasyFileSystem 实现了我们设计的磁盘布局并能够将所有块有效的管理起来。但是对于文件系统的使用者而言,他们往往不关心磁盘布局是如何实现的,而是更希望能够直接看到目录树结构中逻辑上的文件和目录。为此我们设计索引节点 Inode 暴露给文件系统的使用者,让他们能够直接对文件和目录进行操作。 InodeDiskInode 的区别从它们的名字中就可以看出: DiskInode 放在磁盘块中比较固定的位置,而 Inode 是放在内存中的记录文件索引节点信息的数据结构。

+
// easy-fs/src/vfs.rs
+
+pub struct Inode {
+    block_id: usize,
+    block_offset: usize,
+    fs: Arc<Mutex<EasyFileSystem>>,
+    block_device: Arc<dyn BlockDevice>,
+}
+
+
+

block_idblock_offset 记录该 Inode 对应的 DiskInode 保存在磁盘上的具体位置方便我们后续对它进行访问。 fs 是指向 EasyFileSystem 的一个指针,因为对 Inode 的种种操作实际上都是要通过底层的文件系统来完成。

+

仿照 BlockCache::read/modify ,我们可以设计两个方法来简化对于 Inode 对应的磁盘上的 DiskInode 的访问流程,而不是每次都需要 get_block_cache.lock.read/modify

+
// easy-fs/src/vfs.rs
+
+impl Inode {
+    fn read_disk_inode<V>(&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<V>(&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 :

+
// easy-fs/src/efs.rs
+
+impl EasyFileSystem {
+    pub fn root_inode(efs: &Arc<Mutex<Self>>) -> Inode {
+        let block_device = Arc::clone(&efs.lock().block_device);
+        // acquire efs lock temporarily
+        let (block_id, block_offset) = efs.lock().get_disk_inode_pos(0);
+        // release efs lock
+        Inode::new(
+            block_id,
+            block_offset,
+            Arc::clone(efs),
+            block_device,
+        )
+    }
+}
+
+// easy-fs/src/vfs.rs
+
+impl Inode {
+    /// We should not acquire efs lock here.
+    pub fn new(
+        block_id: u32,
+        block_offset: usize,
+        fs: Arc<Mutex<EasyFileSystem>>,
+        block_device: Arc<dyn BlockDevice>,
+    ) -> Self {
+        Self {
+            block_id: block_id as usize,
+            block_offset,
+            fs,
+            block_device,
+        }
+    }
+}
+
+
+

root_inode 中,主要是在 Inode::new 的时候将传入的 inode_id 设置为 0 ,因为根目录对应于文件系统中第一个分配的 inode ,因此它的 inode_id 总会是 0 。同时在设计上,我们不会在 Inode::new 中尝试获取整个 EasyFileSystem 的锁来查询 inode 在块设备中的位置,而是在调用它之前预先查询并作为参数传过去。

+
+
+

文件索引

+

为了尽可能简化我们的实现,所有的文件都在根目录下面。于是,我们不必实现目录索引。文件索引的查找比较简单,仅需在根目录的目录项中根据文件名找到文件的 inode 编号即可。由于没有子目录的存在,这个过程只会进行一次。

+
// easy-fs/src/vfs.rs
+
+impl Inode {
+    pub fn find(&self, name: &str) -> Option<Arc<Inode>> {
+        let fs = self.fs.lock();
+        self.read_disk_inode(|disk_inode| {
+            self.find_inode_id(name, disk_inode)
+            .map(|inode_id| {
+                let (block_id, block_offset) = fs.get_disk_inode_pos(inode_id);
+                Arc::new(Self::new(
+                    block_id,
+                    block_offset,
+                    self.fs.clone(),
+                    self.block_device.clone(),
+                ))
+            })
+        })
+    }
+
+    fn find_inode_id(
+        &self,
+        name: &str,
+        disk_inode: &DiskInode,
+    ) -> Option<u32> {
+        // 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 的互斥锁(相对的,文件系统内部的操作如之前的 Inode::new 或是上面的 find_inode_id 都是假定在已持有 efs 锁的情况下才被调用的,因此它们不应尝试获取锁)。这能够保证在多核情况下,同时最多只能有一个核在进行文件系统相关操作。这样也许会带来一些不必要的性能损失,但我们目前暂时先这样做。如果我们在这里加锁的话,其实就能够保证块缓存的互斥访问了。

+
+
+

文件列举

+

ls 方法可以收集根目录下的所有文件的文件名并以向量的形式返回,这个方法只有根目录的 Inode 才会调用:

+
// easy-fs/src/vfs.rs
+
+impl Inode {
+    pub fn ls(&self) -> Vec<String> {
+        let _fs = self.fs.lock();
+        self.read_disk_inode(|disk_inode| {
+            let file_count = (disk_inode.size as usize) / DIRENT_SZ;
+            let mut v: Vec<String> = 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 会调用:

+
 1// easy-fs/src/vfs.rs
+ 2
+ 3impl Inode {
+ 4    pub fn create(&self, name: &str) -> Option<Arc<Inode>> {
+ 5        let mut fs = self.fs.lock();
+ 6        if self.modify_disk_inode(|root_inode| {
+ 7            // assert it is a directory
+ 8            assert!(root_inode.is_dir());
+ 9            // has the file been created?
+10            self.find_inode_id(name, root_inode)
+11        }).is_some() {
+12            return None;
+13        }
+14        // create a new file
+15        // alloc a inode with an indirect block
+16        let new_inode_id = fs.alloc_inode();
+17        // initialize inode
+18        let (new_inode_block_id, new_inode_block_offset)
+19            = fs.get_disk_inode_pos(new_inode_id);
+20        get_block_cache(
+21            new_inode_block_id as usize,
+22            Arc::clone(&self.block_device)
+23        ).lock().modify(new_inode_block_offset, |new_inode: &mut DiskInode| {
+24            new_inode.initialize(DiskInodeType::File);
+25        });
+26        self.modify_disk_inode(|root_inode| {
+27            // append file in the dirent
+28            let file_count = (root_inode.size as usize) / DIRENT_SZ;
+29            let new_size = (file_count + 1) * DIRENT_SZ;
+30            // increase size
+31            self.increase_size(new_size as u32, root_inode, &mut fs);
+32            // write dirent
+33            let dirent = DirEntry::new(name, new_inode_id);
+34            root_inode.write_at(
+35                file_count * DIRENT_SZ,
+36                dirent.as_bytes(),
+37                &self.block_device,
+38            );
+39        });
+40
+41        let (block_id, block_offset) = fs.get_disk_inode_pos(new_inode_id);
+42        // return inode
+43        Some(Arc::new(Self::new(
+44            block_id,
+45            block_offset,
+46            self.fs.clone(),
+47            self.block_device.clone(),
+48        )))
+49        // release efs lock automatically by compiler
+50    }
+51}
+
+
+
    +
  • 第 6~13 行,检查文件是否已经在根目录下,如果找到的话返回 None

  • +
  • 第 14~25 行,为待创建文件分配一个新的 inode 并进行初始化;

  • +
  • 第 26~39 行,将待创建文件的目录项插入到根目录的内容中使得之后可以索引过来。

  • +
+
+
+

文件清空

+

在以某些标志位打开文件(例如带有 CREATE 标志打开一个已经存在的文件)的时候,需要首先将文件清空。在索引到文件的 Inode 之后可以调用 clear 方法:

+
// 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 一样,这里的读写作用在字节序列的一段区间上:

+
// easy-fs/src/vfs.rs
+
+impl Inode {
+    pub fn read_at(&self, offset: usize, buf: &mut [u8]) -> usize {
+        let _fs = 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 对自身进行扩容:

+
// easy-fs/src/vfs.rs
+
+impl Inode {
+    fn increase_size(
+        &self,
+        new_size: u32,
+        disk_inode: &mut DiskInode,
+        fs: &mut MutexGuard<EasyFileSystem>,
+    ) {
+        if new_size < disk_inode.size {
+            return;
+        }
+        let blocks_needed = disk_inode.blocks_num_needed(new_size);
+        let mut v: Vec<u32> = 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 镜像

+

在第六章中我们需要将所有的应用都链接到内核中,随后在应用管理器中通过应用名进行索引来找到应用的 ELF 数据。这样做有一个缺点,就是会造成内核体积过度膨胀。同时这也会浪费内存资源,因为未被执行的应用也占据了内存空间。在实现了我们自己的文件系统之后,终于可以将这些应用打包到 easy-fs 镜像中放到磁盘中,当我们要执行应用的时候只需从文件系统中取出ELF 执行文件格式的应用 并加载到内存中执行即可,这样就避免了上面的那些问题。

+

easy-fs-fuse 的主体 easy-fs-pack 函数就实现了这个功能:

+
 1// easy-fs-fuse/src/main.rs
+ 2
+ 3use clap::{Arg, App};
+ 4
+ 5fn easy_fs_pack() -> std::io::Result<()> {
+ 6    let matches = App::new("EasyFileSystem packer")
+ 7        .arg(Arg::with_name("source")
+ 8            .short("s")
+ 9            .long("source")
+10            .takes_value(true)
+11            .help("Executable source dir(with backslash)")
+12        )
+13        .arg(Arg::with_name("target")
+14            .short("t")
+15            .long("target")
+16            .takes_value(true)
+17            .help("Executable target dir(with backslash)")
+18        )
+19        .get_matches();
+20    let src_path = matches.value_of("source").unwrap();
+21    let target_path = matches.value_of("target").unwrap();
+22    println!("src_path = {}\ntarget_path = {}", src_path, target_path);
+23    let block_file = Arc::new(BlockFile(Mutex::new({
+24        let f = OpenOptions::new()
+25            .read(true)
+26            .write(true)
+27            .create(true)
+28            .open(format!("{}{}", target_path, "fs.img"))?;
+29        f.set_len(8192 * 512).unwrap();
+30        f
+31    })));
+32    // 4MiB, at most 4095 files
+33    let efs = EasyFileSystem::create(
+34        block_file.clone(),
+35        8192,
+36        1,
+37    );
+38    let root_inode = Arc::new(EasyFileSystem::root_inode(&efs));
+39    let apps: Vec<_> = read_dir(src_path)
+40        .unwrap()
+41        .into_iter()
+42        .map(|dir_entry| {
+43            let mut name_with_ext = dir_entry.unwrap().file_name().into_string().unwrap();
+44            name_with_ext.drain(name_with_ext.find('.').unwrap()..name_with_ext.len());
+45            name_with_ext
+46        })
+47        .collect();
+48    for app in apps {
+49        // load app data from host file system
+50        let mut host_file = File::open(format!("{}{}", target_path, app)).unwrap();
+51        let mut all_data: Vec<u8> = Vec::new();
+52        host_file.read_to_end(&mut all_data).unwrap();
+53        // create a file in easy-fs
+54        let inode = root_inode.create(app.as_str()).unwrap();
+55        // write data to easy-fs
+56        inode.write_at(0, all_data.as_slice());
+57    }
+58    // list apps
+59    for app in root_inode.ls() {
+60        println!("{}", app);
+61    }
+62    Ok(())
+63}
+
+
+
    +
  • 为了实现 easy-fs-fuseos/user 的解耦,第 6~21 行使用 clap crate 进行命令行参数解析,需要通过 -s-t 分别指定应用的源代码目录和保存应用 ELF 的目录而不是在 easy-fs-fuse 中硬编码。如果解析成功的话它们会分别被保存在变量 src_pathtarget_path 中。

  • +
  • 第 23~38 行依次完成:创建 4MiB 的 easy-fs 镜像文件、进行 easy-fs 初始化、获取根目录 inode 。

  • +
  • 第 39 行获取源码目录中的每个应用的源代码文件并去掉后缀名,收集到向量 apps 中。

  • +
  • 第 48 行开始,枚举 apps 中的每个应用,从放置应用执行程序的目录中找到对应应用的 ELF 文件(这是一个 HostOS 上的文件)并将数据读入内存。接着需要在我们的 easy-fs 中创建一个同名文件并将 ELF 数据写入到这个文件中。这个过程相当于将 HostOS 上的文件系统中的一个文件复制到我们的 easy-fs 中。

  • +
+

尽管没有进行任何同步写回磁盘的操作,我们也不用担心块缓存中的修改没有写回磁盘。因为在 easy-fs-fuse 这个应用正常退出的过程中,块缓存因生命周期结束会被回收,届时如果 modified 标志为 true 就会将修改写回磁盘。

+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter6/3using-easy-fs-in-kernel.html b/chapter6/3using-easy-fs-in-kernel.html new file mode 100644 index 0000000..781ebcf --- /dev/null +++ b/chapter6/3using-easy-fs-in-kernel.html @@ -0,0 +1,682 @@ + + + + + + + + 在内核中使用 easy-fs - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

在内核中使用 easy-fs

+
+

块设备驱动层

+

drivers 子模块中的 block/mod.rs 中,我们可以找到内核访问的块设备实例 BLOCK_DEVICE

+
// os/src/drivers/block/mod.rs
+
+type BlockDeviceImpl = virtio_blk::VirtIOBlock;
+
+lazy_static! {
+    pub static ref BLOCK_DEVICE: Arc<dyn BlockDevice> = Arc::new(BlockDeviceImpl::new());
+}
+
+
+

在 qemu 上,我们使用 VirtIOBlock 访问 VirtIO 块设备,并将它全局实例化为 BLOCK_DEVICE ,使内核的其他模块可以访问。

+

在启动 Qemu 模拟器的时候,我们可以配置参数来添加一块 VirtIO 块设备:

+
 1# os/Makefile
+ 2
+ 3FS_IMG := ../user/target/$(TARGET)/$(MODE)/fs.img
+ 4
+ 5run: build
+ 6    @qemu-system-riscv64 \
+ 7        -machine virt \
+ 8        -nographic \
+ 9        -bios $(BOOTLOADER) \
+10        -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) \
+11        -drive file=$(FS_IMG),if=none,format=raw,id=x0 \
+12        -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0
+
+
+
    +
  • 第 11 行,我们为虚拟机添加一块虚拟硬盘,内容为我们之前通过 easy-fs-fuse 工具打包的包含应用 ELF 的 easy-fs 镜像,并命名为 x0

  • +
  • 第 12 行,我们将硬盘 x0 作为一个 VirtIO 总线中的一个块设备接入到虚拟机系统中。 virtio-mmio-bus.0 表示 VirtIO 总线通过 MMIO 进行控制,且该块设备在总线中的编号为 0 。

  • +
+

内存映射 I/O (MMIO, Memory-Mapped I/O) 指通过特定的物理内存地址来访问外设的设备寄存器。查阅资料,可知 VirtIO 总线的 MMIO 物理地址区间为从 0x10001000 开头的 4KiB 。

+

config 子模块中我们硬编码 Qemu 上的 VirtIO 总线的 MMIO 地址区间(起始地址,长度)。在创建内核地址空间的时候需要建立页表映射:

+
// os/src/config.rs
+
+pub const MMIO: &[(usize, usize)] = &[
+    (0x10001000, 0x1000),
+];
+
+// 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
+    }
+}
+
+
+

这里我们进行的是透明的恒等映射,让内核可以兼容于直接访问物理地址的设备驱动库。

+

由于设备驱动的开发过程比较琐碎,我们这里直接使用已有的 virtio-drivers crate,感兴趣的同学可以自行了解。

+
+
+

内核索引节点层

+

内核将 easy-fs 提供的 Inode 进一步封装为 OS 中的索引节点 OSInode

+
// os/src/fs/inode.rs
+
+pub struct OSInode {
+    readable: bool,
+    writable: bool,
+    inner: UPSafeCell<OSInodeInner>,
+}
+
+pub struct OSInodeInner {
+    offset: usize,
+    inode: Arc<Inode>,
+}
+
+
+

OSInode 就表示进程中一个被打开的常规文件或目录。 readable/writable 分别表明该文件是否允许通过 sys_read/write 进行读写,读写过程中的偏移量 offsetInode 则加上互斥锁丢到 OSInodeInner 中。

+
+
+

文件描述符层

+

OSInode 也是要一种要放到进程文件描述符表中,通过 sys_read/write 进行读写的文件,我们需要为它实现 File Trait :

+
// 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
+    }
+}
+
+
+

read/write 的实现也比较简单,只需遍历 UserBuffer 中的每个缓冲区片段,调用 Inode 写好的 read/write_at 接口就好了。注意 read/write_at 的起始位置是在 OSInode 中维护的 offset ,这个 offset 也随着遍历的进行被持续更新。在 read/write 的全程需要获取 OSInode 的互斥锁,保证两个进程无法同时访问同个文件。

+

本章我们为 File Trait 新增了 readable/writable 两个抽象接口,从而在 sys_read/sys_write 的时候进行简单的访问权限检查。

+
+
+

文件系统相关内核机制实现

+
+

文件系统初始化

+

为了使用 easy-fs 提供的抽象,内核需要进行一些初始化操作。我们需要从块设备 BLOCK_DEVICE 上打开文件系统,并从文件系统中获取根目录的 inode 。

+
// os/src/fs/inode.rs
+
+lazy_static! {
+    pub static ref ROOT_INODE: Arc<Inode> = {
+        let efs = EasyFileSystem::open(BLOCK_DEVICE.clone());
+        Arc::new(EasyFileSystem::root_inode(&efs))
+    };
+}
+
+
+

这之后就可以使用根目录的 inode ROOT_INODE ,在内核中调用 easy-fs 的相关接口了。例如,在文件系统初始化完毕之后,调用 list_apps 函数来打印所有可用应用的文件名:

+
// os/src/fs/inode.rs
+
+pub fn list_apps() {
+    println!("/**** APPS ****");
+    for app in ROOT_INODE.ls() {
+        println!("{}", app);
+    }
+    println!("**************/")
+}
+
+
+
+
+

通过 sys_open 打开文件

+

在内核中也定义一份打开文件的标志 OpenFlags

+
// 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 内核函数,可根据文件名打开一个根目录下的文件:

+
// os/src/fs/inode.rs
+
+pub fn open_file(name: &str, flags: OpenFlags) -> Option<Arc<OSInode>> {
+    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 各标志位的语义。例如只有 flags 参数包含 CREATE 标志位才允许创建文件;而如果文件已经存在,则清空文件的内容。

+

在其基础上, sys_open 也就很容易实现了。

+
+
+

通过 sys_exec 加载并执行应用

+

有了文件系统支持后, sys_exec 所需的表示应用 ELF 格式数据改为从文件系统中获取:

+
 1// os/src/syscall/process.rs
+ 2
+ 3pub fn sys_exec(path: *const u8, mut args: *const usize) -> isize {
+ 4let token = current_user_token();
+ 5let path = translated_str(token, path);
+ 6let mut args_vec: Vec<String> = Vec::new();
+ 7loop {
+ 8    let arg_str_ptr = *translated_ref(token, args);
+ 9    if arg_str_ptr == 0 {
+10        break;
+11    }
+12    args_vec.push(translated_str(token, arg_str_ptr as *const u8));
+13    unsafe {
+14        args = args.add(1);
+15    }
+16}
+17if let Some(app_inode) = open_file(path.as_str(), OpenFlags::RDONLY) {
+18    let all_data = app_inode.read_all();
+19    let task = current_task().unwrap();
+20    let argc = args_vec.len();
+21    task.exec(all_data.as_slice(), args_vec);
+22    argc as isize
+23} else {
+24    -1
+25}
+
+
+

注意上面代码片段中的高亮部分。当执行获取应用的 ELF 数据的操作时,首先调用 open_file 函数,以只读的方式在内核中打开应用文件并获取它对应的 OSInode 。接下来可以通过 OSInode::read_all 将该文件的数据全部读到一个向量 all_data 中:

+

之后,就可以从向量 all_data 中拿到应用中的 ELF 数据,当解析完毕并创建完应用地址空间后该向量将会被回收。

+

同样的,我们在内核中创建初始进程 initproc 也需要替换为基于文件系统的实现:

+
// os/src/task/mod.rs
+
+lazy_static! {
+    pub static ref INITPROC: Arc<TaskControlBlock> = Arc::new({
+        let inode = open_file("ch6b_initproc", OpenFlags::RDONLY).unwrap();
+        let v = inode.read_all();
+        TaskControlBlock::new(v.as_slice())
+    });
+}
+
+
+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter6/4exercise.html b/chapter6/4exercise.html new file mode 100644 index 0000000..114dcb0 --- /dev/null +++ b/chapter6/4exercise.html @@ -0,0 +1,559 @@ + + + + + + + + chapter6练习 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

chapter6练习

+
+

Lab4 编程作业

+
+

硬链接

+

硬链接要求两个不同的目录项指向同一个文件,在我们的文件系统中也就是两个不同名称目录项指向同一个磁盘块。

+

本节要求实现三个系统调用 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 彻底删除文件的情况,此时需要回收inode以及它对应的数据块。

    • +
    +
    +
    +
  • +
  • 返回值:如果出现了错误则返回 -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: 文件状态结构体

    • +
    +
    #[repr(C)]
    +#[derive(Debug)]
    +pub struct Stat {
    +    /// 文件所在磁盘驱动器号,该实验中写死为 0 即可
    +    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;
    +    }
    +}
    +
    +
    +
    +
    +
  • +
+
+
+
+

实验要求

+
    +
  • lab4(os6)参考框架:

  • +
  • 实验目录要求不变。

  • +
  • 通过所有测例。

    +

    os6 目录下 make run BASE=2 加载所有测例, ch6_usertest 打包了所有你需要通过的测例,你也可以通过修改这个文件调整本地测试的内容。

    +

    你的内核必须前向兼容,能通过前一章的所有测例。

    +
  • +
+
+

注解

+

如何调试 easy-fs

+

如果你在第一章练习题中已经借助 log crate 实现了日志功能,那么你可以直接在 easy-fs 中引入 log crate,通过 log::info!/debug! 等宏即可进行调试并在内核中看到日志输出。具体来说,在 easy-fs 中的修改是:在 easy-fs/Cargo.toml 的依赖中加入一行 log = "0.4.0",然后在 easy-fs/src/lib.rs 中加入一行 extern crate log

+

你也可以完全在用户态进行调试。仿照 easy-fs-fuse 建立一个在当前操作系统中运行的应用程序,将测试逻辑写在 main 函数中。这个时候就可以将它引用的 easy-fsno_std 去掉并使用 println! 进行调试。

+
+
+
+
+

问答作业

+
    +
  1. 在我们的easy-fs中,root inode起着什么作用?如果root inode中的内容损坏了,会发生什么?

  2. +
+
+
+

报告要求

+
    +
  • 简单总结你实现的功能(200字以内,不要贴代码)。

  • +
  • 完成问答题。

  • +
  • (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。

  • +
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter6/index.html b/chapter6/index.html new file mode 100644 index 0000000..75bf234 --- /dev/null +++ b/chapter6/index.html @@ -0,0 +1,465 @@ + + + + + + + + 第六章:文件系统与I/O重定向 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+ + +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter7/0intro.html b/chapter7/0intro.html new file mode 100644 index 0000000..c188b11 --- /dev/null +++ b/chapter7/0intro.html @@ -0,0 +1,495 @@ + + + + + + + + 引言 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

引言

+
+

本章导读

+

本章将基于文件描述符实现父子进程之间的通信机制——管道。 +我们还将扩展 exec 系统调用,使之能传递运行参数,并进一步改进 shell 程序,使其支持重定向符号 ><

+
+
+

实践体验

+

获取本章代码:

+
$ git clone https://github.com/LearningOS/rust-based-os-comp2022.git
+$ cd rust-based-os-comp2022/
+//$ make setupclassroom  //注意:在本章不需要做这一步,因为这不是一个作业。(这一步很重要,是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次,以后一般就不用执行了,除非 .github/workflows/classroom.yml发生了变化。)
+
+
+

在 qemu 模拟器上运行 os7参考框架:

+
$ cd os7-ref
+$ make run
+
+
+

进入shell程序后,可以运行管道机制的简单测例 ch7b_pipetestch7b_pipetest 需要保证父进程通过管道传输给子进程的字符串不会发生变化。

+

测例输出大致如下:

+
>> ch7b_pipetest
+Read OK, child process exited!
+pipetest passed!
+Shell: Process 2 exited with code 0
+>>
+
+
+

同样的,也可以运行较为复杂的测例 ch7b_pipe_large_test,体验通过两个管道实现双向通信。

+

此外,在本章我们为shell程序支持了输入/输出重定向功能,可以将一个应用的输出保存到一个指定的文件。例如,下面的命令可以将 ch7b_yield 应用的输出保存在文件 fileb 当中,并在应用执行完毕之后确认它的输出:

+
>> ch7b_yield > fileb
+Shell: Process 2 exited with code 0
+>> ch7b_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
+>>
+
+
+
+
+

os7参考框架:

+
 ── os7-ref
+    └── src
+        ├── ...
+        ├── fs
+        │   ├── inode.rs
+        │   ├── mod.rs
+        │   ├── pipe.rs(新增:实现了 File Trait 的第三个实现——可用来进程间通信的管道)
+        │   └── stdio.rs
+        ├── mm
+        │   ├── address.rs
+        │   ├── frame_allocator.rs
+        │   ├── heap_allocator.rs
+        │   ├── memory_set.rs
+        │   ├── mod.rs
+        │   └── page_table.rs
+        ├── syscall
+        │   ├── fs.rs(修改:添加了sys_pipe和sys_dup)
+        │   ├── mod.rs
+        │   └── process.rs(修改:sys_exec添加了对参数的支持)
+        ├── task
+            ├── context.rs
+            ├── manager.rs
+            ├── mod.rs
+            ├── pid.rs
+            ├── processor.rs
+            ├── switch.rs
+            ├── switch.S
+            └── task.rs(修改:在exec中将参数压入用户栈中)
+
+cloc easy-fs os
+-------------------------------------------------------------------------------
+Language                     files          blank        comment           code
+-------------------------------------------------------------------------------
+Rust                            42            317            434           3574
+Assembly                         4             53             26            526
+make                             1             13              4             48
+TOML                             2              4              2             23
+-------------------------------------------------------------------------------
+SUM:                            49            387            466           4171
+-------------------------------------------------------------------------------
+
+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter7/1pipe.html b/chapter7/1pipe.html new file mode 100644 index 0000000..483003e --- /dev/null +++ b/chapter7/1pipe.html @@ -0,0 +1,726 @@ + + + + + + + + 管道 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

管道

+
+

管道的系统调用原型及使用方法

+

新增为当前进程打开一个管道(包含一个只读文件,一个只写文件)的系统调用:

+
/// 功能:为当前进程打开一个管道。
+/// 参数:pipe 表示应用地址空间中的一个长度为 2 的 usize 数组的起始地址,内核需要按顺序将管道读端
+/// 和写端的文件描述符写入到数组中。
+/// 返回值:如果出现了错误则返回 -1,否则返回 0 。可能的错误原因是:传入的地址不合法。
+/// syscall ID:59
+pub fn sys_pipe(pipe: *mut usize) -> isize;
+
+
+

用户库会将其包装为 pipe 函数:

+
// user/src/lib.rs
+
+pub fn pipe(pipe_fd: &mut [usize]) -> isize { sys_pipe(pipe_fd) }
+
+
+

只有当一个管道的所有读端文件/写端文件都被关闭之后,管道占用的资源才会被回收。

+
/// 功能:当前进程关闭一个文件。
+/// 参数:fd 表示要关闭的文件的文件描述符。
+/// 返回值:如果成功关闭则返回 0 ,否则返回 -1 。可能的出错原因:传入的文件描述符并不对应一个打开的文件。
+/// syscall ID:57
+pub fn sys_close(fd: usize) -> isize;
+
+
+

它会在用户库中被包装为 close 函数。

+

我们从测例 ch7b_pipetest 中理解管道的使用方法:

+
 1// user/src/bin/ch7b_pipetest.rs
+ 2
+ 3#![no_std]
+ 4#![no_main]
+ 5
+ 6#[macro_use]
+ 7extern crate user_lib;
+ 8
+ 9use user_lib::{fork, close, pipe, read, write, wait};
+10
+11static STR: &str = "Hello, world!";
+12
+13#[no_mangle]
+14pub fn main() -> i32 {
+15    // create pipe
+16    let mut pipe_fd = [0usize; 2];
+17    pipe(&mut pipe_fd);
+18    // read end
+19    assert_eq!(pipe_fd[0], 3);
+20    // write end
+21    assert_eq!(pipe_fd[1], 4);
+22    if fork() == 0 {
+23        // child process, read from parent
+24        // close write_end
+25        close(pipe_fd[1]);
+26        let mut buffer = [0u8; 32];
+27        let len_read = read(pipe_fd[0], &mut buffer) as usize;
+28        // close read_end
+29        close(pipe_fd[0]);
+30        assert_eq!(core::str::from_utf8(&buffer[..len_read]).unwrap(), STR);
+31        println!("Read OK, child process exited!");
+32        0
+33    } else {
+34        // parent process, write to child
+35        // close read end
+36        close(pipe_fd[0]);
+37        assert_eq!(write(pipe_fd[1], STR.as_bytes()), STR.len() as isize);
+38        // close write end
+39        close(pipe_fd[1]);
+40        let mut child_exit_code: i32 = 0;
+41        wait(&mut child_exit_code);
+42        assert_eq!(child_exit_code, 0);
+43        println!("pipetest passed!");
+44        0
+45    }
+46}
+
+
+

在父进程中,我们通过 pipe 打开一个管道文件数组,其中 pipe_fd[0] 保存了管道读端的文件描述符,而 pipe_fd[1] 保存了管道写端的文件描述符。在 fork 之后,子进程会完全继承父进程的文件描述符表,于是子进程也可以通过同样的文件描述符来访问同一个管道的读端和写端。之前提到过管道是单向的,在这个测例中我们希望管道中的数据从父进程流向子进程,也即父进程仅通过管道的写端写入数据,而子进程仅通过管道的读端读取数据。

+

因此,在第 25 和第 34 行,分别第一时间在子进程中关闭管道的写端和在父进程中关闭管道的读端。父进程在第 35 行将字符串 STR 写入管道的写端,随后在第 37 行关闭管道的写端;子进程在第 27 行从管道的读端读取字符串,并在第 29 行关闭。

+

如果想在父子进程之间实现双向通信,我们就必须创建两个管道。有兴趣的读者可以参考测例 ch7b_pipe_large_test

+
+
+

通过 sys_close 关闭文件

+

关闭文件的系统调用 sys_close 实现非常简单,我们只需将进程控制块中的文件描述符表对应的一项改为 None 代表它已经空闲即可,同时这也会导致内层的引用计数类型 Arc 被销毁,会减少一个文件的引用计数,当引用计数减少到 0 之后,文件所占用的资源就会被自动回收。

+
// 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 类型:

+
// os/src/fs/pipe.rs
+
+pub struct Pipe {
+    readable: bool,
+    writable: bool,
+    buffer: Arc<Mutex<PipeRingBuffer>>,
+}
+
+
+

readablewritable 分别指出该管道端可否支持读取/写入,通过 buffer 字段还可以找到该管道端所在的管道自身。后续我们将为它实现 File Trait ,之后它便可以通过文件描述符来访问。

+

而管道自身,也就是那个带有一定大小缓冲区的字节队列,我们抽象为 PipeRingBuffer 类型:

+
// 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<Weak<Pipe>>,
+}
+
+
+
    +
  • RingBufferStatus 记录了缓冲区目前的状态:FULL 表示缓冲区已满不能再继续写入; EMPTY 表示缓冲区为空无法从里面读取;而 NORMAL 则表示除了 FULLEMPTY 之外的其他状态。

  • +
  • PipeRingBufferarr/head/tail 三个字段用来维护一个循环队列,其中 arr 为存放数据的数组, head 为循环队列队头的下标, tail 为循环队列队尾的下标。

  • +
  • PipeRingBufferwrite_end 字段还保存了它的写端的一个弱引用计数,这是由于在某些情况下需要确认该管道所有的写端是否都已经被关闭了,通过这个字段很容易确认这一点。

  • +
+

从内存管理的角度,每个读端或写端中都保存着所属管道自身的强引用计数,且我们确保这些引用计数只会出现在管道端口 Pipe 结构体中。于是,一旦一个管道所有的读端和写端均被关闭,便会导致它们所属管道的引用计数变为 0 ,循环队列缓冲区所占用的资源被自动回收。虽然 PipeRingBuffer 中保存了一个指向写端的引用计数,但是它是一个弱引用,也就不会出现循环引用的情况导致内存泄露。

+
+

管道创建

+

通过 PipeRingBuffer::new 可以创建一个新的管道:

+
// 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,
+        }
+    }
+}
+
+
+

Piperead/write_end_with_buffer 方法可以分别从一个已有的管道创建它的读端和写端:

+
// os/src/fs/pipe.rs
+
+impl Pipe {
+    pub fn read_end_with_buffer(buffer: Arc<Mutex<PipeRingBuffer>>) -> Self {
+        Self {
+            readable: true,
+            writable: false,
+            buffer,
+        }
+    }
+    pub fn write_end_with_buffer(buffer: Arc<Mutex<PipeRingBuffer>>) -> Self {
+        Self {
+            readable: false,
+            writable: true,
+            buffer,
+        }
+    }
+}
+
+
+

可以看到,读端和写端的访问权限进行了相应设置:不允许向读端写入,也不允许从写端读取。

+

通过 make_pipe 方法可以创建一个管道并返回它的读端和写端:

+
// os/src/fs/pipe.rs
+
+impl PipeRingBuffer {
+    pub fn set_write_end(&mut self, write_end: &Arc<Pipe>) {
+        self.write_end = Some(Arc::downgrade(write_end));
+    }
+}
+
+/// Return (read_end, write_end)
+pub fn make_pipe() -> (Arc<Pipe>, Arc<Pipe>) {
+    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

+
 1// os/src/task/task.rs
+ 2
+ 3impl TaskControlBlockInner {
+ 4    pub fn alloc_fd(&mut self) -> usize {
+ 5        if let Some(fd) = (0..self.fd_table.len())
+ 6            .find(|fd| self.fd_table[*fd].is_none()) {
+ 7            fd
+ 8        } else {
+ 9            self.fd_table.push(None);
+10            self.fd_table.len() - 1
+11        }
+12    }
+13}
+14
+15// os/src/syscall/fs.rs
+16
+17pub fn sys_pipe(pipe: *mut usize) -> isize {
+18    let task = current_task().unwrap();
+19    let token = current_user_token();
+20    let mut inner = task.acquire_inner_lock();
+21    let (pipe_read, pipe_write) = make_pipe();
+22    let read_fd = inner.alloc_fd();
+23    inner.fd_table[read_fd] = Some(pipe_read);
+24    let write_fd = inner.alloc_fd();
+25    inner.fd_table[write_fd] = Some(pipe_write);
+26    *translated_refmut(token, pipe) = read_fd;
+27    *translated_refmut(token, unsafe { pipe.add(1) }) = write_fd;
+28    0
+29}
+
+
+

TaskControlBlockInner::alloc_fd 可以在进程控制块中分配一个最小的空闲文件描述符来访问一个新打开的文件。它先从小到大遍历所有曾经被分配过的文件描述符尝试找到一个空闲的,如果没有的话就需要拓展文件描述符表的长度并新分配一个。

+

sys_pipe 中,第 21 行我们调用 make_pipe 创建一个管道并获取其读端和写端,第 22~25 行我们分别为读端和写端分配文件描述符并将它们放置在文件描述符表中的相应位置中。第 26~27 行我们则是将读端和写端的文件描述符写回到应用地址空间。

+
+
+

管道读写

+

首先来看如何为 Pipe 实现 File Trait 的 read 方法,即从管道的读端读取数据。在此之前,我们需要对于管道循环队列进行封装来让它更易于使用:

+
 1// os/src/fs/pipe.rs
+ 2
+ 3impl PipeRingBuffer {
+ 4    pub fn read_byte(&mut self) -> u8 {
+ 5        self.status = RingBufferStatus::NORMAL;
+ 6        let c = self.arr[self.head];
+ 7        self.head = (self.head + 1) % RING_BUFFER_SIZE;
+ 8        if self.head == self.tail {
+ 9            self.status = RingBufferStatus::EMPTY;
+10        }
+11        c
+12    }
+13    pub fn available_read(&self) -> usize {
+14        if self.status == RingBufferStatus::EMPTY {
+15            0
+16        } else {
+17            if self.tail > self.head {
+18                self.tail - self.head
+19            } else {
+20                self.tail + RING_BUFFER_SIZE - self.head
+21            }
+22        }
+23    }
+24    pub fn all_write_ends_closed(&self) -> bool {
+25        self.write_end.as_ref().unwrap().upgrade().is_none()
+26    }
+27}
+
+
+

PipeRingBuffer::read_byte 方法可以从管道中读取一个字节,注意在调用它之前需要确保管道缓冲区中不是空的。它会更新循环队列队头的位置,并比较队头和队尾是否相同,如果相同的话则说明管道的状态变为空 EMPTY 。仅仅通过比较队头和队尾是否相同不能确定循环队列是否为空,因为它既有可能表示队列为空,也有可能表示队列已满。因此我们需要在 read_byte 的同时进行状态更新。

+

PipeRingBuffer::available_read 可以计算管道中还有多少个字符可以读取。我们首先需要需要判断队列是否为空,因为队头和队尾相等可能表示队列为空或队列已满,两种情况 available_read 的返回值截然不同。如果队列为空的话直接返回 0,否则根据队头和队尾的相对位置进行计算。

+

PipeRingBuffer::all_write_ends_closed 可以判断管道的所有写端是否都被关闭了,这是通过尝试将管道中保存的写端的弱引用计数升级为强引用计数来实现的。如果升级失败的话,说明管道写端的强引用计数为 0 ,也就意味着管道所有写端都被关闭了,从而管道中的数据不会再得到补充,待管道中仅剩的数据被读取完毕之后,管道就可以被销毁了。

+

下面是 Piperead 方法的实现:

+
 1// os/src/fs/pipe.rs
+ 2
+ 3impl File for Pipe {
+ 4    fn read(&self, buf: UserBuffer) -> usize {
+ 5        assert_eq!(self.readable, true);
+ 6        let mut buf_iter = buf.into_iter();
+ 7        let mut read_size = 0usize;
+ 8        loop {
+ 9            let mut ring_buffer = self.buffer.lock();
+10            let loop_read = ring_buffer.available_read();
+11            if loop_read == 0 {
+12                if ring_buffer.all_write_ends_closed() {
+13                    return read_size;
+14                }
+15                drop(ring_buffer);
+16                suspend_current_and_run_next();
+17                continue;
+18            }
+19            // read at most loop_read bytes
+20            for _ in 0..loop_read {
+21                if let Some(byte_ref) = buf_iter.next() {
+22                    unsafe { *byte_ref = ring_buffer.read_byte(); }
+23                    read_size += 1;
+24                } else {
+25                    return read_size;
+26                }
+27            }
+28        }
+29    }
+30}
+
+
+
    +
  • 第 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 个字节均被读取之后还没有填满应用缓冲区就需要进入循环的下一个轮次,否则就可以直接返回了。

    +
  • +
+

Pipewrite 方法——即通过管道的写端向管道中写入数据的实现和 read 的原理类似,篇幅所限在这里不再赘述,感兴趣的读者可自行查阅。

+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter7/2cmdargs-and-redirection.html b/chapter7/2cmdargs-and-redirection.html new file mode 100644 index 0000000..d1111fe --- /dev/null +++ b/chapter7/2cmdargs-and-redirection.html @@ -0,0 +1,704 @@ + + + + + + + + 命令行参数与标准 I/O 重定向 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

命令行参数与标准 I/O 重定向

+
+

命令行参数

+

使用 C 语言开发 Linux 应用时,可以使用标准库提供的 argc/argv 来获取命令行参数,我们希望在我们自己的内核和shell程序上支持这个功能。为了支持命令行参数, sys_exec 的系统调用接口需要发生变化:

+
// user/src/syscall.rs
+
+pub fn sys_exec(path: &str, args: &[*const u8]) -> isize;
+
+
+

可以看到,它的参数多出了一个 args 数组,数组中的每个元素都是命令行参数字符串的起始地址。实际传递给内核的实际上是这个数组的起始地址:

+
// 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) }
+
+
+
+

shell程序的命令行参数分割

+

回忆一下,在shell程序 user_shell 中,一旦接收到一个回车,我们就会将当前行的内容 line 作为一个名字并试图去执行同名的应用。但是现在 line 还可能包含一些命令行参数,只有最开头的一个才是要执行的应用名。因此我们要做的第一件事情就是将 line 用空格分割:

+
// user/src/bin/ch6b_user_shell.rs
+
+let args: Vec<_> = line.as_str().split(' ').collect();
+let mut args_copy: Vec<String> = 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_copyargs 中的字符串拷贝一份到堆上并在末尾手动加入 \0 。这样就可以安心的将 args_copy 中的字符串传入内核了。我们用 args_addr 来收集这些字符串的起始地址:

+
// user/src/bin/ch6b_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 出来的子进程中,我们调用 exec 传入命令行参数。

+
+
+

sys_exec 将命令行参数压入用户栈

+

sys_exec 中,首先需要将应用传进来的命令行参数取出来:

+
 1// os/src/syscall/process.rs
+ 2
+ 3pub fn sys_exec(path: *const u8, mut args: *const usize) -> isize {
+ 4    let token = current_user_token();
+ 5    let path = translated_str(token, path);
+ 6    let mut args_vec: Vec<String> = Vec::new();
+ 7    loop {
+ 8        let arg_str_ptr = *translated_ref(token, args);
+ 9        if arg_str_ptr == 0 {
+10            break;
+11        }
+12        args_vec.push(translated_str(token, arg_str_ptr as *const u8));
+13        unsafe { args = args.add(1); }
+14    }
+15    if let Some(app_inode) = open_file(path.as_str(), OpenFlags::RDONLY) {
+16        let all_data = app_inode.read_all();
+17        let task = current_task().unwrap();
+18        let argc = args_vec.len();
+19        task.exec(all_data.as_slice(), args_vec);
+20        // return argc because cx.x[10] will be covered with it later
+21        argc as isize
+22    } else {
+23        -1
+24    }
+25}
+
+
+

每次我们都可以从一个起始地址通过 translated_str 拿到一个字符串,直到 args 为 0 就说明没有更多命令行参数了。在第 19 行调用 TaskControlBlock::exec 的时候,我们需要将获取到的 args_vec 传入进去并将里面的字符串压入到用户栈上。

+
 1// os/src/task/task.rs
+ 2
+ 3impl TaskControlBlock {
+ 4    pub fn exec(&self, elf_data: &[u8], args: Vec<String>) {
+ 5        // memory_set with elf program headers/trampoline/trap context/user stack
+ 6        let (memory_set, mut user_sp, entry_point) = MemorySet::from_elf(elf_data);
+ 7        let trap_cx_ppn = memory_set
+ 8            .translate(VirtAddr::from(TRAP_CONTEXT).into())
+ 9            .unwrap()
+10            .ppn();
+11        // push arguments on user stack
+12        user_sp -= (args.len() + 1) * core::mem::size_of::<usize>();
+13        let argv_base = user_sp;
+14        let mut argv: Vec<_> = (0..=args.len())
+15            .map(|arg| {
+16                translated_refmut(
+17                    memory_set.token(),
+18                    (argv_base + arg * core::mem::size_of::<usize>()) as *mut usize
+19                )
+20            })
+21            .collect();
+22        *argv[args.len()] = 0;
+23        for i in 0..args.len() {
+24            user_sp -= args[i].len() + 1;
+25            *argv[i] = user_sp;
+26            let mut p = user_sp;
+27            for c in args[i].as_bytes() {
+28                *translated_refmut(memory_set.token(), p as *mut u8) = *c;
+29                p += 1;
+30            }
+31            *translated_refmut(memory_set.token(), p as *mut u8) = 0;
+32        }
+33        // make the user_sp aligned to 8B
+34        user_sp -= user_sp % core::mem::size_of::<usize>();
+35
+36        // **** access current TCB exclusively
+37        let mut inner = self.inner_exclusive_access();
+38        // substitute memory_set
+39        inner.memory_set = memory_set;
+40        // update trap_cx ppn
+41        inner.trap_cx_ppn = trap_cx_ppn;
+42        // initialize trap_cx
+43        let mut trap_cx = TrapContext::app_init_context(
+44            entry_point,
+45            user_sp,
+46            KERNEL_SPACE.exclusive_access().token(),
+47            self.kernel_stack.get_top(),
+48            trap_handler as usize,
+49        );
+50        trap_cx.x[10] = args.len();
+51        trap_cx.x[11] = argv_base;
+52        *inner.get_trap_cx() = trap_cx;
+53        // **** release current PCB
+54    }
+55}
+
+
+

第 11-34 行所做的主要工作是将命令行参数以某种格式压入用户栈。具体的格式可以参考下图(比如应用传入了两个命令行参数 aabb ):

+../_images/user-stack-cmdargs.png +
    +
  • 首先需要在用户栈上分配一个字符串指针数组,也就是蓝色区域。数组中的每个元素都指向一个用户栈更低处的命令行参数字符串的起始地址。在第 12~24 行可以看到,最开始我们只是分配空间,具体的值要等到字符串被放到用户栈上之后才能确定更新。

  • +
  • 第 23~32 行,我们逐个将传入的 args 中的字符串压入到用户栈中,对应于图中的橙色区域。为了实现方便,我们在用户栈上预留空间之后逐字节进行复制。注意 args 中的字符串是通过 translated_str 从应用地址空间取出的,它的末尾不包含 \0 。为了应用能知道每个字符串的长度,我们需要手动在末尾加入 \0

  • +
  • 第 34 行将 user_sp 以 8 字节对齐,在 Qemu 平台上其实可以忽略这一步。

  • +
+

我们还需要对应修改 Trap 上下文。首先是第 45 行,我们的 user_sp 相比之前已经发生了变化,它上面已经压入了命令行参数。同时,我们还需要修改 Trap 上下文中的 a0/a1 寄存器,让 a0 表示命令行参数的个数,而 a1 则表示图中 argv_base 即蓝色区域的起始地址。这两个参数在第一次进入对应应用的用户态的时候会被接收并用于还原命令行参数。

+
+
+

用户库从用户栈上还原命令行参数

+

在应用第一次进入用户态的时候,我们放在 Trap 上下文 a0/a1 两个寄存器中的内容可以被用户库中的入口函数以参数的形式接收:

+
 1// user/src/lib.rs
+ 2
+ 3#[no_mangle]
+ 4#[link_section = ".text.entry"]
+ 5pub extern "C" fn _start(argc: usize, argv: usize) -> ! {
+ 6    unsafe {    // 初始化堆分配器
+ 7        HEAP.lock()
+ 8            .init(HEAP_SPACE.as_ptr() as usize, USER_HEAP_SIZE);
+ 9    }
+10    let mut v: Vec<&'static str> = Vec::new();
+11    for i in 0..argc {
+12        let str_start = unsafe {
+13            ((argv + i * core::mem::size_of::<usize>()) as *const usize).read_volatile()
+14        };
+15        let len = (0usize..).find(|i| unsafe {
+16            ((str_start + *i) as *const u8).read_volatile() == 0
+17        }).unwrap();
+18        v.push(
+19            core::str::from_utf8(unsafe {
+20                core::slice::from_raw_parts(str_start as *const u8, len)
+21            }).unwrap()
+22        );
+23    }
+24    exit(main(argc, v.as_slice()));
+25}
+
+
+

可以看到,在入口 _start 中我们就接收到了命令行参数个数 argc 和字符串数组的起始地址 argv 。但是这个起始地址不太好用,我们希望能够将其转化为编写应用的时候看到的 &[&str] 的形式。转化的主体在第 10~23 行,就是分别取出 argc 个字符串的起始地址(基于字符串数组的 base 地址 argv ),从它向后找到第一个 \0 就可以得到一个完整的 &str 格式的命令行参数字符串并加入到向量 v 中。最后通过 v.as_slice 就得到了我们在 main 主函数中看到的 &[&str]

+

有了命令行参数支持,我们就可以编写命令行工具 ch6b_cat 来输出指定文件的内容了。读者可以自行参阅其实现。

+
+
+
+

标准输入输出重定向

+

为了增强 shell 程序使用文件系统时的灵活性,我们需要新增标准输入输出重定向功能。

+

重定向功能对于应用来说是透明的。在应用中除非明确指出了数据要从指定的文件输入或者输出到指定的文件,否则数据默认都是输入自进程文件描述表位置 0 处的标准输入,并输出到进程文件描述符表位置 1 处的标准输出。

+

为了对应用进程的文件描述符表进行某种替换,引入一个新的系统调用 sys_dup

+
// user/src/syscall.rs
+
+/// 功能:将进程中一个已经打开的文件复制一份并分配到一个新的文件描述符中。
+/// 参数:fd 表示进程中一个已经打开的文件的文件描述符。
+/// 返回值:如果出现了错误则返回 -1,否则能够访问已打开文件的新文件描述符。
+/// 可能的错误原因是:传入的 fd 并不对应一个合法的已打开文件。
+/// syscall ID:24
+pub fn sys_dup(fd: usize) -> isize;
+
+
+

这个系统调用的实现非常简单:

+
// 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
+}
+
+
+

sys_dup 函数中,首先检查传入 fd 的合法性。然后在文件描述符表中分配一个新的文件描述符,并保存 fd 指向的已打开文件的一份拷贝即可。

+

在shell程序 user_shell 分割命令行参数的时候,我们要检查是否存在通过 <> 进行输入输出重定向的情况,如果存在的话则需要将它们从命令行参数中移除,并记录匹配到的输入文件名或输出文件名到字符串 inputoutput 中。注意,为了实现方便,我们这里假设输入shell程序的命令一定合法:即 <> 最多只会出现一次,且后面总是会有一个参数作为重定向到的文件。

+
// user/src/bin/ch6b_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 之后的子进程分支中:

+
 1// user/src/bin/user_shell.rs
+ 2
+ 3let pid = fork();
+ 4if pid == 0 {
+ 5    // input redirection
+ 6    if !input.is_empty() {
+ 7        let input_fd = open(input.as_str(), OpenFlags::RDONLY);
+ 8        if input_fd == -1 {
+ 9            println!("Error when opening file {}", input);
+10            return -4;
+11        }
+12        let input_fd = input_fd as usize;
+13        close(0);
+14        assert_eq!(dup(input_fd), 0);
+15        close(input_fd);
+16    }
+17    // output redirection
+18    if !output.is_empty() {
+19        let output_fd = open(
+20            output.as_str(),
+21            OpenFlags::CREATE | OpenFlags::WRONLY
+22        );
+23        if output_fd == -1 {
+24            println!("Error when opening file {}", output);
+25            return -4;
+26        }
+27        let output_fd = output_fd as usize;
+28        close(1);
+29        assert_eq!(dup(output_fd), 1);
+30        close(output_fd);
+31    }
+32    // child process
+33    if exec(args_copy[0].as_str(), args_addr.as_slice()) == -1 {
+34        println!("Error when executing!");
+35        return -4;
+36    }
+37    unreachable!();
+38} else {
+39    let mut exit_code: i32 = 0;
+40    let exit_pid = waitpid(pid as usize, &mut exit_code);
+41    assert_eq!(pid, exit_pid);
+42    println!("Shell: Process {} exited with code {}", pid, exit_code);
+43}
+
+
+
    +
  • 输入重定向发生在第 6~16 行。我们尝试打开输入文件 inputinput_fd 中。之后,首先通过 close 关闭标准输入所在的文件描述符 0 。之后通过 dup 来分配一个新的文件描述符来访问 input_fd 对应的输入文件。这里用到了文件描述符分配的重要性质:即必定分配可用描述符中编号最小的一个。由于我们刚刚关闭了描述符 0 ,那么在 dup 的时候一定会将它分配出去,于是现在应用进程的文件描述符 0 就对应到输入文件了。最后,因为应用进程的后续执行不会用到输入文件原来的描述符 input_fd ,所以就将其关掉。

  • +
  • 输出重定向则发生在 18~31 行。它的原理和输入重定向几乎完全一致,只是通过 open 打开文件的标志不太相同

  • +
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter7/3exercise.html b/chapter7/3exercise.html new file mode 100644 index 0000000..4c09f81 --- /dev/null +++ b/chapter7/3exercise.html @@ -0,0 +1,423 @@ + + + + + + + + chapter7练习 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

chapter7练习

+
+

编程作业

+

本章无编程作业

+
+
+

问答作业

+
    +
  1. 举出使用 pipe 的一个实际应用的例子。

  2. +
  3. 如果需要在多个进程间互相通信,则需要为每一对进程建立一个管道,非常繁琐,请设计一个更易用的多进程通信机制。

  4. +
+
+
+

报告要求

+
    +
  • 完成问答题。

  • +
  • (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。

  • +
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter7/index.html b/chapter7/index.html new file mode 100644 index 0000000..39478bc --- /dev/null +++ b/chapter7/index.html @@ -0,0 +1,420 @@ + + + + + + + + 第七章:进程间通信 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ + +
+ + + + + + + + \ No newline at end of file diff --git a/chapter8/0intro.html b/chapter8/0intro.html new file mode 100644 index 0000000..036a4a1 --- /dev/null +++ b/chapter8/0intro.html @@ -0,0 +1,614 @@ + + + + + + + + 引言 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

引言

+
+

本章导读

+

到本章开始之前,我们好像已经完成了组成应用程序执行环境的操作系统的三个重要抽象:进程、地址空间和文件, +让应用程序开发、运行和存储数据越来越方便和灵活。有了进程以后,可以让操作系统从宏观层面实现多个应用的并发执行, +而并发是通过操作系统基于处理器的时间片不断地切换进程来达到的。到目前为止的并发,仅仅是进程间的并发, +对于一个进程内部还没有并发性的体现。而这就是线程(Thread)出现的起因:提高一个进程内的并发性。

+

有了进程以后,为什么还会出现线程呢?考虑如下情况,对于很多应用(以单一进程的形式运行)而言, +逻辑上存在多个可并行执行的任务,如果其中一个任务被阻塞,将会引起不依赖该任务的其他任务也被阻塞。 +举个具体的例子,我们平常用编辑器来编辑文本内容的时候,都会有一个定时自动保存的功能, +这个功能的作用是在系统或应用本身出现故障的情况前,已有的文档内容会被提前保存。 +假设编辑器自动保存时由于磁盘性能导致写入较慢,导致整个进程被操作系统挂起,这就会影响到用户编辑文档的人机交互体验: +即软件的及时响应能力不足,用户只有等到磁盘写入完成后,操作系统重新调度该进程运行后,用户才可编辑。 +如果我们把一个进程内的多个可并行执行任务通过一种更细粒度的方式让操作系统进行调度, +那么就可以通过处理器时间片切换实现这种细粒度的并发执行。这种细粒度的调度对象就是线程。

+
+

线程定义

+

简单地说,线程是进程的组成部分,进程可包含1 – n个线程,属于同一个进程的线程共享进程的资源, +比如地址空间、打开的文件等。基本的线程由线程ID、执行状态、当前指令指针 (PC)、寄存器集合和栈组成。 +线程是可以被操作系统或用户态调度器独立调度(Scheduling)和分派(Dispatch)的基本单位。

+

在本章之前,进程是程序的基本执行实体,是程序关于某数据集合上的一次运行活动,是系统进行资源(处理器、 +地址空间和文件等)分配和调度的基本单位。在有了线程后,对进程的定义也要调整了,进程是线程的资源容器, +线程成为了程序的基本执行实体。

+
+
+

同步互斥

+

在上面提到了同步互斥和数据一致性,它们的含义是什么呢?当多个线程共享同一进程的地址空间时, +每个线程都可以访问属于这个进程的数据(全局变量)。如果每个线程使用到的变量都是其他线程不会读取或者修改的话, +那么就不存在一致性问题。如果变量是只读的,多个线程读取该变量也不会有一致性问题。但是,当一个线程修改变量时, +其他线程在读取这个变量时,可能会看到一个不一致的值,这就是数据不一致性的问题。

+
+

注解

+

并发相关术语

+
    +
  • 共享资源(shared resource):不同的线程/进程都能访问的变量或数据结构。

  • +
  • 临界区(critical section):访问共享资源的一段代码。

  • +
  • 竞态条件(race condition):多个线程/进程都进入临界区时,都试图更新共享的数据结构,导致产生了不期望的结果。

  • +
  • 不确定性(indeterminate): 多个线程/进程在执行过程中出现了竞态条件,导致执行结果取决于哪些线程在何时运行, +即执行结果不确定,而开发者期望得到的是确定的结果。

  • +
  • 互斥(mutual exclusion):一种操作原语,能保证只有一个线程进入临界区,从而避免出现竞态,并产生确定的执行结果。

  • +
  • 原子性(atomic):一系列操作要么全部完成,要么一个都没执行,不会看到中间状态。在数据库领域, +具有原子性的一系列操作称为事务(transaction)。

  • +
  • 同步(synchronization):多个并发执行的进程/线程在一些关键点上需要互相等待,这种相互制约的等待称为进程/线程同步。

  • +
  • 死锁(dead lock):一个线程/进程集合里面的每个线程/进程都在等待只能由这个集合中的其他一个线程/进程 +(包括他自身)才能引发的事件,这种情况就是死锁。

  • +
  • 饥饿(hungry):指一个可运行的线程/进程尽管能继续执行,但由于操作系统的调度而被无限期地忽视,导致不能执行的情况。

  • +
+
+

在后续的章节中,会大量使用上述术语,如果现在还不够理解,没关系,随着后续的一步一步的分析和实验, +相信大家能够掌握上述术语的实际含义。

+
+
+
+

实践体验

+
+

注解

+

基于github classroom的开发方式

+

基于github classroom,可方便建立开发用的git repository,并可基于github的 codespace (在线版ubuntu +vscode)在线开发使用。整个开发环境仅仅需要一个网络浏览器。

+
    +
  1. 在网络浏览器中用自己的 github id 登录 github.com

  2. +
  3. 接收 第五个实验(os8)的github classroom在线邀请 ,根据提示一路选择OK即可。

  4. +
  5. 完成第二步后,你的第五个实验的 github repository 会被自动建立好,点击此github repository的链接,就可看到你要完成的第一个实验了。

  6. +
  7. 在你的第五个实验的网页的中上部可以看到一个醒目的 code 绿色按钮,点击后,可以进一步看到 codespace 标签和醒目的 create codesapce on main 绿色按钮。请点击这个绿色按钮,就可以进入到在线的ubuntu +vscode环境中

  8. +
  9. 再按照下面的环境安装提示在vscode的 console 中安装配置开发环境:rustc,qemu等工具。

  10. +
  11. 在vscode的 console 中执行 make setupclassroom_test8 (该命令仅执行一次)配置githubclassroom 自动评分功能。

  12. +
  13. 然后就可以基于在线vscode进行开发、运行、提交等完整的实验过程了。

  14. +
+

上述的3,4,5步不是必须的,你也可以线下本地开发。

+
+

获取本章代码:

+
$ git clone https://github.com/LearningOS/rust-based-os-comp2022.git
+$ cd rust-based-os-comp2022/
+$ make setupclassroom_test8  //注意:这一步很重要,是用于github classroom自动评测你的工作。这一步只需在首次克隆项目仓库时执行一次,以后一般就不用执行了,除非 .github/workflows/classroom.yml发生了变化。
+
+
+

在 qemu 模拟器上运行本章代码 lab5(os8)参考框架:

+
$ cd os8-ref
+$ make run
+
+
+

内核初始化完成之后就会进入 shell 程序,我们可以体会一下线程的创建和执行过程。在这里我们运行一下本章的测例 ch8b_threads

+
>> ch8b_threads
+aaa....bbb...ccc...
+thread#1 exited with code 1
+thread#2 exited with code 2
+thread#3 exited with code 3
+main thread exited.
+Shell: Process 2 exited with code 0
+>>
+
+
+

它会有4个线程在执行,等前3个线程执行完毕后,主线程退出,导致整个进程退出。

+

此外,在本章的操作系统支持通过互斥来执行“哲学家就餐问题”这个应用程序:

+
>> ch8b_phil_din_mutex
+Here comes 5 philosophers!
+time cost = 720
+'-' -> THINKING; 'x' -> EATING; ' ' -> WAITING
+#0: -------                 xxxxxxxx----------       xxxx-----  xxxxxx--xxx
+#1: ---xxxxxx--      xxxxxxx----------    x---xxxxxx
+#2: -----          xx---------xx----xxxxxx------------        xxxx
+#3: -----xxxxxxxxxx------xxxxx--------    xxxxxx--   xxxxxxxxx
+#4: ------         x------          xxxxxx--    xxxxx------   xx
+#0: -------                 xxxxxxxx----------       xxxx-----  xxxxxx--xxx
+Shell: Process 2 exited with code 0
+>>
+
+
+

我们可以看到5个代表“哲学家”的线程通过操作系统的 信号量 互斥机制在进行 “THINKING”、“EATING”、“WAITING” 的日常生活。 +没有哲学家由于拿不到筷子而饥饿,也没有两个哲学家同时拿到一个筷子。

+
+

注解

+

哲学家就餐问题

+

计算机科学家 Dijkstra 提出并解决的哲学家就餐问题是经典的进程同步互斥问题。哲学家就餐问题描述如下:

+

有5个哲学家共用一张圆桌,分别坐在周围的5张椅子上,在圆桌上有5个碗和5只筷子,他们的生活方式是交替地进行思考和进餐。 +平时,每个哲学家进行思考,饥饿时便试图拿起其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐完毕,放下筷子继续思考。

+
+
+
+

本章的 lab5(os8)参考框架: 代码树

+
 1 .
+ 2 ├── bootloader
+ 3 │   └── rustsbi-qemu.bin
+ 4 ├── Dockerfile
+ 5 ├── easy-fs
+ 6 │   ├── Cargo.lock
+ 7 │   ├── Cargo.toml
+ 8 │   └── src
+ 9 │       ├── bitmap.rs
+10 │       ├── block_cache.rs
+11 │       ├── block_dev.rs
+12 │       ├── efs.rs
+13 │       ├── layout.rs
+14 │       ├── lib.rs
+15 │       └── vfs.rs
+16 ├── easy-fs-fuse
+17 │   ├── Cargo.lock
+18 │   ├── Cargo.toml
+19 │   └── src
+20 │       └── main.rs
+21 ├── LICENSE
+22 ├── Makefile
+23 ├── os
+24 │   ├── build.rs
+25 │   ├── Cargo.lock
+26 │   ├── Cargo.toml
+27 │   ├── Makefile
+28 │   └── src
+29 │       ├── config.rs (修改:扩大了内核堆空间)
+30 │       ├── console.rs
+31 │       ├── drivers
+32 │       │   ├── block
+33 │       │   │   ├── mod.rs
+34 │       │   │   └── virtio_blk.rs
+35 │       │   └── mod.rs
+36 │       ├── entry.asm
+37 │       ├── fs
+38 │       │   ├── inode.rs
+39 │       │   ├── mod.rs
+40 │       │   ├── pipe.rs
+41 │       │   └── stdio.rs
+42 │       ├── lang_items.rs
+43 │       ├── linker.ld
+44 │       ├── logging.rs
+45 │       ├── main.rs
+46 │       ├── mm
+47 │       │   ├── address.rs
+48 │       │   ├── frame_allocator.rs
+49 │       │   ├── heap_allocator.rs
+50 │       │   ├── memory_set.rs (修改:去除了构建进程地址空间时分配用户栈和映射陷入上下文的逻辑)
+51 │       │   ├── mod.rs
+52 │       │   └── page_table.rs
+53 │       ├── sbi.rs
+54 │       ├── sync (新增:互斥锁、信号量和条件变量三种同步互斥机制的实现)
+55 │       │   ├── condvar.rs
+56 │       │   ├── mod.rs
+57 │       │   ├── mutex.rs
+58 │       │   ├── semaphore.rs
+59 │       │   └── up.rs
+60 │       ├── syscall
+61 │       │   ├── fs.rs (修改:将原先对 task 的调用改为对 process 的调用)
+62 │       │   ├── mod.rs
+63 │       │   ├── process.rs (修改:将原先对 task 的调用改为对 process 的调用)
+64 │       │   ├── sync.rs (新增:三种同步互斥机制相关的系统调用,以及基于定时器条件变量的 sleep 调用)
+65 │       │   └── thread.rs (新增:线程相关系统调用)
+66 │       ├── task
+67 │       │   ├── context.rs (修改:将任务上下文的成员变量改为 pub 类型)
+68 │       │   ├── id.rs (新增:由 pid.rs 修改而来,提供 pid/tid 、 kstack/ustack 的分配和回收机制)
+69 │       │   ├── kthread.rs (新增:完全在内核态运行的线程,仅供参考,在实验中未使用)
+70 │       │   ├── manager.rs
+71 │       │   ├── mod.rs (修改:增加阻塞线程的功能,将 exit 扩展到多线程,并在主线程退出时一并退出进程)
+72 │       │   ├── processor.rs (修改:增加获取当前线程的中断上下文虚拟地址及获取当前进程的功能)
+73 │       │   ├── process.rs (新增:将原先 Task 中的地址空间、文件等机制拆分为进程)
+74 │       │   ├── stackless_coroutine.rs (新增:完全在内核态运行的无栈协程,仅供参考,在实验中未使用)
+75 │       │   ├── switch.rs
+76 │       │   ├── switch.S
+77 │       │   └── task.rs (修改:将进程相关的功能移至 process.rs 中)
+78 │       ├── timer.rs (修改:增加定时器条件变量的实现)
+79 │       └── trap
+80 │           ├── context.rs
+81 │           ├── mod.rs (修改:使用线程对应的中断上下文地址而非固定的 TRAP_CONTEXT)
+82 │           └── trap.S
+83 ├── README.md
+84 └── rust-toolchain
+
+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter8/1thread-kernel.html b/chapter8/1thread-kernel.html new file mode 100644 index 0000000..a8fd5cd --- /dev/null +++ b/chapter8/1thread-kernel.html @@ -0,0 +1,853 @@ + + + + + + + + 内核态的线程管理 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

内核态的线程管理

+
+

线程概念

+

这里会结合与进程的比较来说明线程的概念。到本章之前,我们看到了进程这一抽象,操作系统让进程拥有相互隔离的虚拟的地址空间, +让进程感到在独占一个虚拟的处理器。其实这只是操作系统通过时分复用和空分复用技术来让每个进程复用有限的物理内存和物理CPU。 +而线程是在进程内中的一个新的抽象。在没有线程之前,一个进程在一个时刻只有一个执行点(即程序计数器 (PC) +寄存器保存的要执行指令的指针)。但线程的引入把进程内的这个单一执行点给扩展为多个执行点,即在进程中存在多个线程, +每个线程都有一个执行点。而且这些线程共享进程的地址空间,所以可以不必采用相对比较复杂的 IPC 机制(一般需要内核的介入), +而可以很方便地直接访问进程内的数据。

+

在线程的具体运行过程中,需要有程序计数器寄存器来记录当前的执行位置,需要有一组通用寄存器记录当前的指令的操作数据, +需要有一个栈来保存线程执行过程的函数调用栈和局部变量等,这就形成了线程上下文的主体部分。 +这样如果两个线程运行在一个处理器上,就需要采用类似两个进程运行在一个处理器上的调度/切换管理机制, +即需要在一定时刻进行线程切换,并进行线程上下文的保存与恢复。这样在一个进程中的多线程可以独立运行, +取代了进程,成为操作系统调度的基本单位。

+

由于把进程的结构进行了细化,通过线程来表示对处理器的虚拟化,使得进程成为了管理线程的容器。 +在进程中的线程没有父子关系,大家都是兄弟,但还是有个老大。这个代表老大的线程其实就是创建进程(比如通过 +fork 系统调用创建进程)时,建立的第一个线程,它的线程标识符(TID)为 0

+
+
+

线程模型与重要系统调用

+

目前,我们只介绍本章实现的内核中采用的一种非常简单的线程模型。这个线程模型有三个运行状态: +就绪态、运行态和等待态;共享所属进程的地址空间和其他共享资源(如文件等);可被操作系统调度来分时占用CPU执行; +可以动态创建和退出;可通过系统调用获得操作系统的服务。我们实现的线程模型建立在进程的地址空间抽象之上: +每个线程都共享进程的代码段和和可共享的地址空间(如全局数据段、堆等),但有自己的独占的栈。 +线程模型需要操作系统支持一些重要的系统调用:创建线程、等待子线程结束等,来支持灵活的多线程应用。 +接下来会介绍这些系统调用的基本功能和设计思路。

+
+

线程创建系统调用

+

在一个进程的运行过程中,进程可以创建多个属于这个进程的线程,每个线程有自己的线程标识符(TID,Thread Identifier)。 +系统调用 thread_create 的原型如下:

+
1/// 功能:当前进程创建一个新的线程
+2/// 参数:entry 表示线程的入口函数地址
+3/// 参数:arg:表示线程的一个参数
+4pub fn sys_thread_create(entry: usize, arg: usize) -> isize
+
+
+

当进程调用 thread_create 系统调用后,内核会在这个进程内部创建一个新的线程,这个线程能够访问到进程所拥有的代码段, +堆和其他数据段。但内核会给这个新线程分配一个它专有的用户态栈,这样每个线程才能相对独立地被调度和执行。 +另外,由于用户态进程与内核之间有各自独立的页表,所以二者需要有一个跳板页 TRAMPOLINE +来处理用户态切换到内核态的地址空间平滑转换的事务。所以当出现线程后,在进程中的每个线程也需要有一个独立的跳板页 +TRAMPOLINE 来完成同样的事务。

+

相比于创建进程的 fork 系统调用,创建线程不需要要建立新的地址空间,这是二者之间最大的不同。 +另外属于同一进程中的线程之间没有父子关系,这一点也与进程不一样。

+
+
+

等待子线程系统调用

+

当一个线程执行完代表它的功能后,会通过 exit 系统调用退出。内核在收到线程发出的 exit 系统调用后, +会回收线程占用的部分资源,即用户态用到的资源,比如用户态的栈,用于系统调用和异常处理的跳板页等。 +而该线程的内核态用到的资源,比如内核栈等,需要通过进程/主线程调用 waittid 来回收了, +这样整个线程才能被彻底销毁。系统调用 waittid 的原型如下:

+
1/// 参数:tid表示线程id
+2/// 返回值:如果线程不存在,返回-1;如果线程还没退出,返回-2;其他情况下,返回结束线程的退出码
+3pub fn sys_waittid(tid: usize) -> i32
+
+
+

一般情况下进程/主线程要负责通过 waittid 来等待它创建出来的线程(不是主线程)结束并回收它们在内核中的资源 +(如线程的内核栈、线程控制块等)。如果进程/主线程先调用了 exit 系统调用来退出,那么整个进程 +(包括所属的所有线程)都会退出,而对应父进程会通过 waitpid 回收子进程剩余还没被回收的资源。

+
+
+

进程相关的系统调用

+

在引入了线程机制后,进程相关的重要系统调用: forkexecwaitpid 虽然在接口上没有变化, +但在它要完成的功能上需要有一定的扩展。首先,需要注意到把以前进程中与处理器执行相关的部分拆分到线程中。这样,在通过 +fork 创建进程其实也意味着要单独建立一个主线程来使用处理器,并为以后创建新的线程建立相应的线程控制块向量。 +相对而言, execwaitpid 这两个系统调用要做的改动比较小,还是按照与之前进程的处理方式来进行。总体上看, +进程相关的这三个系统调用还是保持了已有的进程操作的语义,并没有由于引入了线程,而带来大的变化。

+
+
+
+

应用程序示例

+

我们刚刚介绍了 thread_create/waittid 两个重要系统调用,我们可以借助它们和之前实现的系统调用, +开发出功能更为灵活的应用程序。下面我们通过描述一个多线程应用程序 threads 的开发过程来展示这些系统调用的使用方法。

+
+

系统调用封装

+

同学可以在 user/src/syscall.rs 中看到以 sys_* 开头的系统调用的函数原型,它们后续还会在 user/src/lib.rs 中被封装成方便应用程序使用的形式。如 sys_thread_create 被封装成 thread_create ,而 sys_waittid 被封装成 waittid

+
 1pub fn thread_create(entry: usize, arg: usize) -> isize { sys_thread_create(entry, arg) }
+ 2
+ 3pub fn waittid(tid: usize) -> isize {
+ 4    loop {
+ 5        match sys_waittid(tid) {
+ 6            -2 => { yield_(); }
+ 7            exit_code => return exit_code,
+ 8        }
+ 9    }
+10}
+
+
+

waittid 等待一个线程标识符的值为tid 的线程结束。在具体实现方面,我们看到当 sys_waittid 返回值为 -2 ,即要等待的线程存在但它却尚未退出的时候,主线程调用 yield_ 主动交出 CPU 使用权,待下次 CPU 使用权被内核交还给它的时候再次调用 sys_waittid 查看要等待的线程是否退出。这样做是为了减小 CPU 资源的浪费。这种方法是为了尽可能简化内核的实现。

+
+
+

多线程应用程序 – threads

+

多线程应用程序 – threads 开始执行后,先调用 thread_create 创建了三个线程,加上进程自带的主线程,其实一共有四个线程。每个线程在打印了1000个字符后,会执行 exit 退出。进程通过 waittid 等待这三个线程结束后,最终结束进程的执行。下面是多线程应用程序 – threads 的源代码:

+
 1//usr/src/bin/ch8b_threads.rs
+ 2
+ 3#![no_std]
+ 4#![no_main]
+ 5
+ 6#[macro_use]
+ 7extern crate user_lib;
+ 8extern crate alloc;
+ 9
+10use user_lib::{thread_create, waittid, exit};
+11use alloc::vec::Vec;
+12
+13pub fn thread_a() -> ! {
+14    for _ in 0..1000 { print!("a"); }
+15    exit(1)
+16}
+17
+18pub fn thread_b() -> ! {
+19    for _ in 0..1000 { print!("b"); }
+20    exit(2)
+21}
+22
+23pub fn thread_c() -> ! {
+24    for _ in 0..1000 { print!("c"); }
+25    exit(3)
+26}
+27
+28#[no_mangle]
+29pub fn main() -> i32 {
+30    let mut v = Vec::new();
+31    v.push(thread_create(thread_a as usize, 0));
+32    v.push(thread_create(thread_b as usize, 0));
+33    v.push(thread_create(thread_c as usize, 0));
+34    for tid in v.iter() {
+35        let exit_code = waittid(*tid as usize);
+36        println!("thread#{} exited with code {}", tid, exit_code);
+37    }
+38    println!("main thread exited.");
+39    0
+40}
+
+
+
+
+
+

线程管理的核心数据结构

+

为了在现有进程管理的基础上实现线程管理,我们需要改进一些数据结构包含的内容及接口。 +基本思路就是把进程中与处理器相关的部分分拆出来,形成线程相关的部分。

+

本节将按照如下顺序来进行介绍:

+
    +
  • 任务控制块 TaskControlBlock :表示线程的核心数据结构。

  • +
  • 任务管理器 TaskManager :管理线程集合的核心数据结构。

  • +
  • 处理器管理结构 Processor :用于线程调度,维护线程的处理器状态。

  • +
+
+

线程控制块

+

在内核中,每个线程的执行状态和线程上下文等均保存在一个被称为线程控制块 (TCB, Task Control Block) +的结构中,它是内核对线程进行管理的核心数据结构。在内核看来,它就等价于一个线程。

+
 1pub struct TaskControlBlock {
+ 2    // immutable
+ 3    pub process: Weak<ProcessControlBlock>,
+ 4    pub kernel_stack: KernelStack,
+ 5    // mutable
+ 6    inner: UPSafeCell<TaskControlBlockInner>,
+ 7}
+ 8
+ 9pub struct TaskControlBlockInner {
+10    pub trap_cx_ppn: PhysPageNum,
+11    pub task_cx: TaskContext,
+12    pub task_status: TaskStatus,
+13    pub exit_code: Option<i32>,
+14    pub res: Option<TaskUserRes>,
+15}
+
+
+

线程控制块就是任务控制块(TaskControlBlock),主要包括在线程初始化之后就不再变化的元数据: +线程所属的进程和线程的内核栈,以及在运行过程中可能发生变化的元数据: UPSafeCell<TaskControlBlockInner> 。 +大部分的细节放在 TaskControlBlockInner 中:

+

之前进程中的定义不存在的:

+
    +
  • res: Option<TaskUserRes> 指出了用户态的线程代码执行需要的信息,这些在线程初始化之后就不再变化:

  • +
+
1pub struct TaskUserRes {
+2    pub tid: usize,
+3    pub ustack_base: usize,
+4    pub process: Weak<ProcessControlBlock>,
+5}
+
+
+
    +
  • tid:线程标识符

  • +
  • ustack_base:线程的栈顶地址

  • +
  • process:线程所属的进程

  • +
+

与之前进程中的定义相同/类似的部分:

+
    +
  • trap_cx_ppn 指出了应用地址空间中线程的 Trap 上下文被放在的物理页帧的物理页号。

  • +
  • task_cx 保存暂停线程的线程上下文,用于线程切换。

  • +
  • task_status 维护当前线程的执行状态。

  • +
  • exit_code 线程退出码。

  • +
+
+
+

包含线程的进程控制块

+

把线程相关数据单独组织成数据结构后,进程的结构也需要进行一定的调整:

+
 1pub struct ProcessControlBlock {
+ 2    // immutable
+ 3    pub pid: PidHandle,
+ 4    // mutable
+ 5    inner: UPSafeCell<ProcessControlBlockInner>,
+ 6}
+ 7
+ 8pub struct ProcessControlBlockInner {
+ 9    ...
+10    pub tasks: Vec<Option<Arc<TaskControlBlock>>>,
+11    pub task_res_allocator: RecycleAllocator,
+12}
+
+
+

从中可以看出,进程把与处理器执行相关的部分都移到了 TaskControlBlock 中,并组织为一个线程控制块向量中, +这就自然对应到多个线程的管理上了。而 RecycleAllocator 是对之前的 PidAllocator 的一个升级版, +即一个相对通用的资源分配器,可用于分配进程标识符(PID)和线程的内核栈(KernelStack)。

+
+
+

线程与处理器管理结构

+

线程管理的结构是线程管理器,即任务管理器,位于 os/src/task/manager.rs 中, +其数据结构和方法与之前章节中进程的任务管理器完全一样,仅负责管理所有线程。而处理器管理结构 Processor +负责维护 CPU 状态、调度和特权级切换等事务。其数据结构与之前章节中进程的处理器管理结构完全一样。 +但在相关方法上面,由于多个线程有各自的用户栈和跳板页,所以有些不同,下面会进一步分析。

+
+
+
+

线程管理机制的设计与实现

+

在上述线程模型和内核数据结构的基础上,我们还需完成线程管理的基本实现,从而构造出一个完整的“达科塔盗龙”操作系统。 +本节将分析如何实现线程管理:

+
    +
  • 线程创建、线程退出与等待线程结束

  • +
  • 线程执行中的特权级切换

  • +
+
+

线程创建、线程退出与等待线程结束

+
+

线程创建

+

当一个进程执行中发出了创建线程的系统调用 sys_thread_create 后,操作系统就需要在当前进程的基础上创建一个线程了, +这里重点是需要了解创建线程控制块,在线程控制块中初始化各个成员变量,建立好进程和线程的关系等。 +只有建立好这些成员变量,才能给线程建立一个灵活方便的执行环境。这里列出支持线程正确运行所需的重要的执行环境要素:

+
    +
  • 线程的用户态栈:确保在用户态的线程能正常执行函数调用;

  • +
  • 线程的内核态栈:确保线程陷入内核后能正常执行函数调用;

  • +
  • 线程的跳板页:确保线程能正确的进行用户态<–>内核态切换;

  • +
  • 线程上下文:即线程用到的寄存器信息,用于线程切换。

  • +
+

线程创建的具体实现如下:

+
 1// os/src/syscall/thread.rs
+ 2
+ 3pub fn sys_thread_create(entry: usize, arg: usize) -> isize {
+ 4    let task = current_task().unwrap();
+ 5    let process = task.process.upgrade().unwrap();
+ 6    // create a new thread
+ 7    let new_task = Arc::new(TaskControlBlock::new(
+ 8        Arc::clone(&process),
+ 9        task.inner_exclusive_access().res.as_ref().unwrap().ustack_base,
+10        true,
+11    ));
+12    // add new task to scheduler
+13    add_task(Arc::clone(&new_task));
+14    let new_task_inner = new_task.inner_exclusive_access();
+15    let new_task_res = new_task_inner.res.as_ref().unwrap();
+16    let new_task_tid = new_task_res.tid;
+17    let mut process_inner = process.inner_exclusive_access();
+18    // add new thread to current process
+19    let tasks = &mut process_inner.tasks;
+20    while tasks.len() < new_task_tid + 1 {
+21        tasks.push(None);
+22    }
+23    tasks[new_task_tid] = Some(Arc::clone(&new_task));
+24    let new_task_trap_cx = new_task_inner.get_trap_cx();
+25    *new_task_trap_cx = TrapContext::app_init_context(
+26        entry,
+27        new_task_res.ustack_top(),
+28        kernel_token(),
+29        new_task.kernel_stack.get_top(),
+30        trap_handler as usize,
+31    );
+32    (*new_task_trap_cx).x[10] = arg;
+33    new_task_tid as isize
+34}
+
+
+

上述代码主要完成了如下事务:

+
    +
  • 第4-5行,找到当前正在执行的线程 task 和此线程所属的进程 process

  • +
  • 第7-11行,调用 TaskControlBlock::new 方法,创建一个新的线程 new_task ,在创建过程中,建立与进程 +process 的所属关系,分配了线程用户态栈、内核态栈、用于异常/中断的跳板页。

  • +
  • 第13行,把线程挂到调度队列中。

  • +
  • 第19-22行,把线程接入到所需进程的线程列表 tasks 中。

  • +
  • 第25~32行,初始化位于该线程在用户态地址空间中的 Trap 上下文:设置线程的函数入口点和用户栈, +使得第一次进入用户态时能从线程起始位置开始正确执行;设置好内核栈和陷入函数指针 trap_handler , +保证在 Trap 的时候用户态的线程能正确进入内核态。

  • +
+
+
+

线程退出

+

当一个非主线程的其他线程发出 sys_exit 系统调用时,内核会调用 exit_current_and_run_next +函数退出当前线程并切换到下一个线程,但不会导致其所属进程的退出。当 主线程 即进程发出这个系统调用, +内核会回收整个进程(这包括了其管理的所有线程)资源,并退出。具体实现如下:

+
 1// os/src/syscall/process.rs
+ 2
+ 3pub fn sys_exit(exit_code: i32) -> ! {
+ 4    exit_current_and_run_next(exit_code);
+ 5    panic!("Unreachable in sys_exit!");
+ 6}
+ 7
+ 8// os/src/task/mod.rs
+ 9
+10pub fn exit_current_and_run_next(exit_code: i32) {
+11    let task = take_current_task().unwrap();
+12    let mut task_inner = task.inner_exclusive_access();
+13    let process = task.process.upgrade().unwrap();
+14    let tid = task_inner.res.as_ref().unwrap().tid;
+15    // record exit code
+16    task_inner.exit_code = Some(exit_code);
+17    task_inner.res = None;
+18    // here we do not remove the thread since we are still using the kstack
+19    // it will be deallocated when sys_waittid is called
+20    drop(task_inner);
+21    drop(task);
+22    // however, if this is the main thread of current process
+23    // the process should terminate at once
+24    if tid == 0 {
+25        let mut process_inner = process.inner_exclusive_access();
+26        // mark this process as a zombie process
+27        process_inner.is_zombie = true;
+28        // record exit code of main process
+29        process_inner.exit_code = exit_code;
+30        {
+31            // move all child processes under init process
+32            let mut initproc_inner = INITPROC.inner_exclusive_access();
+33            for child in process_inner.children.iter() {
+34                child.inner_exclusive_access().parent = Some(Arc::downgrade(&INITPROC));
+35                initproc_inner.children.push(child.clone());
+36            }
+37        }
+38        let mut recycle_res = Vec::<TaskUserRes>::new();
+39        // deallocate user res (including tid/trap_cx/ustack) of all threads
+40        // it has to be done before we dealloc the whole memory_set
+41        // otherwise they will be deallocated twice
+42        for task in process_inner.tasks.iter().filter(|t| t.is_some()) {
+43            let task = task.as_ref().unwrap();
+44            let mut task_inner = task.inner_exclusive_access();
+45            if let Some(res) = task_inner.res.take() {
+46                recycle_res.push(res);
+47            }
+48        }
+49        drop(process_inner);
+50        recycle_res.clear();
+51        let mut process_inner = process.inner_exclusive_access();
+52        process_inner.children.clear();
+53        // deallocate other data in user space i.e. program code/data section
+54        process_inner.memory_set.recycle_data_pages();
+55    }
+56    drop(process);
+57    // we do not have to save task context
+58    let mut _unused = TaskContext::zero_init();
+59    schedule(&mut _unused as *mut _);
+60}
+
+
+

上述代码主要完成了如下事务:

+
    +
  • 第11-21行,回收线程的各种资源。

  • +
  • 第24-56行,如果是主线程发出的退去请求,则回收整个进程的部分资源,并退出进程。第 33~37 +行所做的事情是将当前进程的所有子进程挂在初始进程 INITPROC 下面,其做法是遍历每个子进程, +修改其父进程为初始进程,并加入初始进程的孩子向量中。第 49 行将当前进程的孩子向量清空。

  • +
  • 第58-59行,进行线程调度切换。

  • +
+

上述实现中很大一部分与第五章讲解的 进程的退出 的功能实现大致相同。

+
+
+

等待线程结束

+

主线程通过系统调用 sys_waittid 来等待其他线程的结束。具体实现如下:

+
 1// os/src/syscall/ch8b_thread.rs
+ 2
+ 3pub fn sys_waittid(tid: usize) -> i32 {
+ 4    let task = current_task().unwrap();
+ 5    let process = task.process.upgrade().unwrap();
+ 6    let task_inner = task.inner_exclusive_access();
+ 7    let mut process_inner = process.inner_exclusive_access();
+ 8    // a thread cannot wait for itself
+ 9    if task_inner.res.as_ref().unwrap().tid == tid {
+10        return -1;
+11    }
+12    let mut exit_code: Option<i32> = None;
+13    let waited_task = process_inner.tasks[tid].as_ref();
+14    if let Some(waited_task) = waited_task {
+15        if let Some(waited_exit_code) = waited_task.inner_exclusive_access().exit_code {
+16            exit_code = Some(waited_exit_code);
+17        }
+18    } else {
+19        // waited thread does not exist
+20        return -1;
+21    }
+22    if let Some(exit_code) = exit_code {
+23        // dealloc the exited thread
+24        process_inner.tasks[tid] = None;
+25        exit_code
+26    } else {
+27        // waited thread has not exited
+28        -2
+29    }
+30}
+
+
+

上述代码主要完成了如下事务:

+
    +
  • 第9-10行,如果是线程等自己,返回错误.

  • +
  • 第12-21行,如果找到 tid 对应的退出线程,则收集该退出线程的退出码 exit_tid ,否则返回错误(退出线程不存在)。

  • +
  • 第22-29行,如果退出码存在,则清空进程中对应此退出线程的线程控制块(至此,线程所占资源算是全部清空了),否则返回错误(线程还没退出)。

  • +
+
+
+
+

线程执行中的特权级切换和调度切换

+

线程执行中的特权级切换与第三章中 任务切换的设计与实现 小节中讲解的过程是一致的。而线程执行中的调度切换过程与第五章的 进程调度机制 小节中讲解的过程是一致的。 +这里就不用再赘述一遍了。

+
+
1
+

达科塔盗龙是一种生存于距今6700万-6500万年前白垩纪晚期的兽脚类驰龙科恐龙,它主打的并不是霸王龙的力量路线,而是利用自己修长的后肢来提高敏捷度和奔跑速度。它全身几乎都长满了羽毛,可能会滑翔或者其他接近飞行行为的行动模式。

+
+
+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter8/2lock.html b/chapter8/2lock.html new file mode 100644 index 0000000..5c4be22 --- /dev/null +++ b/chapter8/2lock.html @@ -0,0 +1,804 @@ + + + + + + + + 锁机制 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

锁机制

+
+

本节导读

+

到目前为止,我们已经实现了进程和线程,也能够理解在一个时间段内,会有多个线程在执行,这就是并发。 +而且,由于线程的引入,多个线程可以共享进程中的全局数据。如果多个线程都想读和更新全局数据, +那么谁先更新取决于操作系统内核的抢占式调度和分派策略。在一般情况下,每个线程都有可能先执行, +且可能由于中断等因素,随时被操作系统打断其执行,而切换到另外一个线程运行, +形成在一段时间内,多个线程交替执行的现象。如果没有一些保障机制(比如互斥、同步等), +那么这些对共享数据进行读写的交替执行的线程,其期望的共享数据的正确结果可能无法达到。

+

所以,我们需要研究一种保障机制 — 锁 ,确保无论操作系统如何抢占线程,调度和切换线程的执行, +都可以保证对拥有锁的线程,可以独占地对共享数据进行读写,从而能够得到正确的共享数据结果。 +这种机制的能力来自于处理器的指令、操作系统系统调用的基本支持,从而能够保证线程间互斥地读写共享数据。 +下面各个小节将从为什么需要锁、锁的基本思路、锁的不同实现方式等逐步展开讲解。

+
+
+

为什么需要锁

+

上一小节已经提到,没有保障机制的多个线程,在对共享数据进行读写的过程中,可能得不到预期的结果。 +我们来看看这个简单的例子:

+
1// 线程的入口函数
+2int a=0;
+3void f() {
+4  a = a + 1;
+5}
+
+
+

对于上述函数中的第 4 行代码,一般人理解处理器会一次就执行完这条简单的语句,但实际情况并不是这样。 +我们可以用 GCC 编译出上述函数的汇编码:

+
1$ riscv64-unknown-elf-gcc -o f.s -S f.c
+
+
+

可以看到生成的汇编代码如下:

+
 1//f.s
+ 2  .text
+ 3  .globl    a
+ 4  .section  .sbss,"aw",@nobits
+ 5  .align    2
+ 6  .type     a, @object
+ 7  .size     a, 4
+ 8a:
+ 9  .zero     4
+10  .text
+11  .align    1
+12  .globl    f
+13  .type     f, @function
+14f:
+15  addi      sp,sp,-16
+16  sd        s0,8(sp)
+17  addi      s0,sp,16
+18  lui       a5,%hi(a)
+19  lw        a5,%lo(a)(a5)
+20  addiw     a5,a5,1
+21  sext.w    a4,a5
+22  lui       a5,%hi(a)
+23  sw        a4,%lo(a)(a5)
+24  nop
+25  ld        s0,8(sp)
+26  addi      sp,sp,16
+27  jr        ra
+
+
+

从中可以看出,对于高级语言的一条简单语句(C 代码的第 4 行,对全局变量进行读写),很可能是由多条汇编代码 +(汇编代码的第 18~23 行)组成。如果这个函数是多个线程要执行的函数,那么在上述汇编代码第 +18 行到第 23 行中的各行之间,可能会发生中断,从而导致操作系统执行抢占式的线程调度和切换, +就会得到不一样的结果。由于执行这段汇编代码(第 18~23 行))的多个线程在访问全局变量过程中可能导致竞争状态, +因此我们将此段代码称为临界区(critical section)。临界区是访问共享变量(或共享资源)的代码片段, +不能由多个线程同时执行,即需要保证互斥。

+

下面是有两个线程T0、T1在一个时间段内的一种可能的执行情况:

+
++++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

时间

T0

T1

OS

共享变量a

寄存器a5

1

L18

0

a的高位地址

2

切换

0

0

3

L18

0

a的高位地址

4

L20

0

1

5

切换

0

a的高位地址

6

L20

0

1

7

切换

0

1

8

L23

1

1

9

切换

1

1

10

L23

1

1

+

一般情况下,线程 T0 执行完毕后,再执行线程 T1,那么共享全局变量 a 的值为 2 。但在上面的执行过程中, +可以看到在线程执行指令的过程中会发生线程切换,这样在时刻 10 的时候,共享全局变量 a 的值为 1 , +这不是我们预期的结果。出现这种情况的原因是两个线程在操作系统的调度下(在哪个时刻调度具有不确定性), +交错执行 a = a + 1 的不同汇编指令序列,导致虽然增加全局变量 a 的代码被执行了两次, +但结果还是只增加了 1 。这种多线程的最终执行结果不确定(indeterminate),取决于由于调度导致的、 +不确定指令执行序列的情况就是竞态条件(race condition)。

+

如果每个线程在执行 a = a + 1 这个 C 语句所对应多条汇编语句过程中,不会被操作系统切换, +那么就不会出现多个线程交叉读写全局变量的情况,也就不会出现结果不确定的问题了。

+

所以,访问(特指写操作)共享变量代码片段,不能由多个线程同时执行(即并行)或者在一个时间段内都去执行 +(即并发)。要做到这一点,需要互斥机制的保障。从某种角度上看,这种互斥性也是一种原子性, +即线程在临界区的执行过程中,不会出现只执行了一部分,就被打断并切换到其他线程执行的情况。即, +要么线程执行的这一系列操作/指令都完成,要么这一系列操作/指令都不做,不会出现指令序列执行中被打断的情况。

+
+
+

锁的基本思路

+

要保证多线程并发执行中的临界区的代码具有互斥性或原子性,我们可以建立一种锁, +只有拿到锁的线程才能在临界区中执行。这里的锁与现实生活中的锁的含义很类似。比如,我们可以写出如下的伪代码:

+
1lock(mutex);    // 尝试取锁
+2a = a + 1;      // 临界区,访问临界资源 a
+3unlock(mutex);  // 是否锁
+4...             // 剩余区
+
+
+

对于一个应用程序而言,它的执行是受到其执行环境的管理和限制的,而执行环境的主要组成就是用户态的系统库、 +操作系统和更底层的处理器,这说明我们需要有硬件和操作系统来对互斥进行支持。一个自然的想法是,这个 +lock/unlock 互斥操作就是CPU提供的机器指令,那上面这一段程序就很容易在计算机上执行了。 +但需要注意,这里互斥的对象是线程的临界区代码,而临界区代码可以访问各种共享变量(简称临界资源)。 +只靠两条机器指令,难以识别各种共享变量,不太可能约束可能在临界区的各种指令执行共享变量操作的互斥性。 +所以,我们还是需要有一些相对更灵活和复杂一点的方法,能够设置一种所有线程能看到的标记, +在一个能进入临界区的线程设置好这个标记后,其他线程都不能再进入临界区了。总体上看, +对临界区的访问过程分为四个部分:

+
    +
  1. 尝试取锁: 查看锁是否可用,即临界区是否可访问(看占用临界区标志是否被设置),如果可以访问, +则设置占用临界区标志(锁不可用)并转到步骤 2 ,否则线程忙等或被阻塞;

  2. +
  3. 临界区: 访问临界资源的系列操作

  4. +
  5. 释放锁: 清除占用临界区标志(锁可用),如果有线程被阻塞,会唤醒阻塞线程;

  6. +
  7. 剩余区: 与临界区不相关部分的代码

  8. +
+

根据上面的步骤,可以看到锁机制有两种:让线程忙等的忙等锁(spin lock),以及让线程阻塞的睡眠锁 +(sleep lock)。锁的实现大体上基于三类机制:用户态软件、机器指令硬件、内核态操作系统。 +下面我们介绍来 rCore 中基于内核态操作系统级方法实现的支持互斥的锁。

+

我们还需要知道如何评价各种锁实现的效果。一般我们需要关注锁的三种属性:

+
    +
  1. 互斥性(mutual exclusion),即锁是否能够有效阻止多个线程进入临界区,这是最基本的属性。

  2. +
  3. 公平性(fairness),当锁可用时,每个竞争线程是否有公平的机会抢到锁。

  4. +
  5. 性能(performance),即使用锁的时间开销。

  6. +
+
+
+

内核态操作系统级方法实现锁 — mutex 系统调用

+
+

使用 mutex 系统调用

+

如何能够实现轻量的可睡眠锁?一个自然的想法就是,让等待锁的线程睡眠,让释放锁的线程显式地唤醒等待锁的线程。 +如果有多个等待锁的线程,可以全部释放,让大家再次竞争锁;也可以只释放最早等待的那个线程。 +这就需要更多的操作系统支持,特别是需要一个等待队列来保存等待锁的线程。

+

我们先看看多线程应用程序如何使用mutex系统调用的:

+
 1// user/src/bin/race_adder_mutex_blocking.rs
+ 2
+ 3static mut A: usize = 0;
+ 4...
+ 5unsafe fn f() -> ! {
+ 6    let mut t = 2usize;
+ 7    for _ in 0..PER_THREAD {
+ 8        mutex_lock(0);
+ 9        let a = &mut A as *mut usize;
+10        let cur = a.read_volatile();
+11        for _ in 0..500 { t = t * t % 10007; }
+12        a.write_volatile(cur + 1);
+13        mutex_unlock(0);
+14    }
+15    exit(t as i32)
+16}
+17
+18#[no_mangle]
+19pub fn main() -> i32 {
+20    let start = get_time();
+21    assert_eq!(mutex_blocking_create(), 0);
+22    let mut v = Vec::new();
+23    for _ in 0..THREAD_COUNT {
+24        v.push(thread_create(f as usize, 0) as usize);
+25    }
+26    ...
+27}
+28
+29// usr/src/syscall.rs
+30
+31pub fn sys_mutex_create(blocking: bool) -> isize {
+32    syscall(SYSCALL_MUTEX_CREATE, [blocking as usize, 0, 0])
+33}
+34pub fn sys_mutex_lock(id: usize) -> isize {
+35    syscall(SYSCALL_MUTEX_LOCK, [id, 0, 0])
+36}
+37pub fn sys_mutex_unlock(id: usize) -> isize {
+38    syscall(SYSCALL_MUTEX_UNLOCK, [id, 0, 0])
+39}
+
+
+
    +
  • 第21行,创建了一个ID为 0 的互斥锁,对应的是第32行 SYSCALL_MUTEX_CREATE 系统调用;

  • +
  • 第8行,尝试获取锁(对应的是第35行 SYSCALL_MUTEX_LOCK 系统调用),如果取得锁, +将继续向下执行临界区代码;如果没有取得锁,将阻塞;

  • +
  • 第13行,释放锁(对应的是第38行 SYSCALL_MUTEX_UNLOCK 系统调用),如果有等待在该锁上的线程, +则唤醒这些等待线程。

  • +
+
+
+

mutex 系统调用的实现

+

操作系统如何实现这些系统调用呢?首先考虑一下与此相关的核心数据结构, +然后考虑与数据结构相关的相关函数/方法的实现。

+

在线程的眼里, 互斥 是一种每个线程能看到的资源,且在一个进程中,可以存在多个不同互斥资源, +所以我们可以把所有的互斥资源放在一起让进程来管理,如下面代码第 9 行所示。这里需要注意的是: +mutex_list: Vec<Option<Arc<dyn Mutex>>> 表示的是实现了 Mutex trait 的一个“互斥资源”的向量。而 +MutexBlocking 是会实现 Mutex trait 的内核数据结构,它就是我们提到的 互斥资源 即 +互斥锁 。操作系统需要显式地施加某种控制,来确定当一个线程释放锁时,等待的线程谁将能抢到锁。 +为了做到这一点,操作系统需要有一个等待队列来保存等待锁的线程,如下面代码的第 20 行所示。

+
 1pub struct ProcessControlBlock {
+ 2    // immutable
+ 3    pub pid: PidHandle,
+ 4    // mutable
+ 5    inner: UPSafeCell<ProcessControlBlockInner>,
+ 6}
+ 7pub struct ProcessControlBlockInner {
+ 8    ...
+ 9    pub mutex_list: Vec<Option<Arc<dyn Mutex>>>,
+10}
+11pub trait Mutex: Sync + Send {
+12    fn lock(&self);
+13    fn unlock(&self);
+14}
+15pub struct MutexBlocking {
+16    inner: UPSafeCell<MutexBlockingInner>,
+17}
+18pub struct MutexBlockingInner {
+19    locked: bool,
+20    wait_queue: VecDeque<Arc<TaskControlBlock>>,
+21}
+
+
+

这样,在操作系统中,需要设计实现三个核心成员变量。互斥锁的成员变量有两个:表示是否锁上的 locked +和管理等待线程的等待队列 wait_queue;进程的成员变量:锁向量 mutex_list

+

首先需要创建一个互斥锁,下面是应对 SYSCALL_MUTEX_CREATE 系统调用的创建互斥锁的函数:

+
 1// os/src/syscall/sync.rs
+ 2pub fn sys_mutex_create(blocking: bool) -> isize {
+ 3    let process = current_process();
+ 4    let mut process_inner = process.inner_exclusive_access();
+ 5    if let Some(id) = process_inner
+ 6        .mutex_list
+ 7        .iter()
+ 8        .enumerate()
+ 9        .find(|(_, item)| item.is_none())
+10        .map(|(id, _)| id) {
+11        process_inner.mutex_list[id] = if !blocking {
+12            Some(Arc::new(MutexSpin::new()))
+13        } else {
+14            Some(Arc::new(MutexBlocking::new()))
+15        };
+16        id as isize
+17    } else {
+18        process_inner.mutex_list.push(Some(Arc::new(MutexSpin::new())));
+19        process_inner.mutex_list.len() as isize - 1
+20    }
+21}
+
+
+
    +
  • 第 14 行,如果向量中有空的元素,就在这个空元素的位置创建一个可睡眠的互斥锁;

  • +
  • 第 18 行,如果向量满了,就在向量中添加新的可睡眠的互斥锁;

  • +
+

有了互斥锁,接下来就是实现 Mutex trait的内核函数:对应 SYSCALL_MUTEX_LOCK 系统调用的 +sys_mutex_lock 。操作系统主要工作是,在锁已被其他线程获取的情况下,把当前线程放到等待队列中, +并调度一个新线程执行。主要代码如下:

+
 1// os/src/syscall/sync.rs
+ 2pub fn sys_mutex_lock(mutex_id: usize) -> isize {
+ 3    let process = current_process();
+ 4    let process_inner = process.inner_exclusive_access();
+ 5    let mutex = Arc::clone(process_inner.mutex_list[mutex_id].as_ref().unwrap());
+ 6    drop(process_inner);
+ 7    drop(process);
+ 8    mutex.lock();
+ 9    0
+10}
+11
+12// os/src/sync/mutex.rs
+13impl Mutex for MutexBlocking {
+14    fn lock(&self) {
+15        let mut mutex_inner = self.inner.exclusive_access();
+16        if mutex_inner.locked {
+17            mutex_inner.wait_queue.push_back(current_task().unwrap());
+18            drop(mutex_inner);
+19            block_current_and_run_next();
+20        } else {
+21            mutex_inner.locked = true;
+22        }
+23    }
+24}
+
+
+
    +
  • 第 8 行,调用 ID 为 mutex_id 的互斥锁 mutexlock 方法,具体工作由该方法来完成。

  • +
  • 第 16 行,如果互斥锁 mutex 已经被其他线程获取了,那么在第 17 行,将把当前线程放入等待队列中; +在第 19 行,让当前线程处于等待状态,并调度其他线程执行。

  • +
  • 第 21 行,如果互斥锁 mutex 还没被获取,那么当前线程会获取给互斥锁,并返回系统调用。

  • +
+

最后是实现 Mutex trait 的内核函数:对应 SYSCALL_MUTEX_UNLOCK 系统调用的 sys_mutex_unlock 。 +操作系统的主要工作是,如果有等待在这个互斥锁上的线程,需要唤醒最早等待的线程。主要代码如下:

+
 1// os/src/syscall/sync.rs
+ 2pub fn sys_mutex_unlock(mutex_id: usize) -> isize {
+ 3    let process = current_process();
+ 4    let process_inner = process.inner_exclusive_access();
+ 5    let mutex = Arc::clone(process_inner.mutex_list[mutex_id].as_ref().unwrap());
+ 6    drop(process_inner);
+ 7    drop(process);
+ 8    mutex.unlock();
+ 9    0
+10}
+11
+12// os/src/sync/mutex.rs
+13impl Mutex for MutexBlocking {
+14    fn unlock(&self) {
+15        let mut mutex_inner = self.inner.exclusive_access();
+16        assert!(mutex_inner.locked);
+17        if let Some(waking_task) = mutex_inner.wait_queue.pop_front() {
+18            add_task(waking_task);
+19        } else {
+20            mutex_inner.locked = false;
+21        }
+22    }
+23}
+
+
+
    +
  • 第 8 行,调用 ID 为 mutex_id 的互斥锁 mutexunlock 方法,具体工作由该方法来完成的。

  • +
  • 第 17-18 行,如果有等待的线程,唤醒等待最久的那个线程,相当于将锁的所有权移交给该线程。

  • +
  • 第 20 行,若没有线程等待,则释放锁。

  • +
+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter8/3semaphore.html b/chapter8/3semaphore.html new file mode 100644 index 0000000..7b7ac07 --- /dev/null +++ b/chapter8/3semaphore.html @@ -0,0 +1,643 @@ + + + + + + + + 信号量机制 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

信号量机制

+
+

本节导读

+

在上一节中,我们介绍了互斥锁(mutex 或 lock)的起因、使用和实现过程。通过互斥锁, +可以让线程在临界区执行时,独占临界资源。当我们需要更灵活的互斥访问或同步操作方式,如提供了最多只允许 +N 个线程访问临界资源的情况,让某个线程等待另外一个线程执行完毕后再继续执行的同步过程等, +互斥锁这种方式就有点力不从心了。

+

在本节中,将介绍功能更加强大和灵活的同步互斥机制 – 信号量(Semaphore),它的设计思路、 +使用和在操作系统中的具体实现。可以看到,信号量的实现需要互斥锁和处理器原子指令的支持, +它是一种更高级的同步互斥机制。

+
+
+

信号量的起源和基本思路

+

1963 年前后,当时的数学家(其实是计算机科学家)Edsger Dijkstra 和他的团队在为 Electrologica X8 +计算机开发一个操作系统(称为 THE multiprogramming system,THE 多道程序系统)的过程中,提出了信号量 +(Semphore)是一种变量或抽象数据类型,用于控制多个线程对共同资源的访问。

+

信号量是对互斥锁的一种巧妙的扩展。上一节中的互斥锁的初始值一般设置为 1 的整型变量, +表示临界区还没有被某个线程占用。互斥锁用 0 表示临界区已经被占用了,用 1 表示临界区为空,再通过 +lock/unlock 操作来协调多个线程轮流独占临界区执行。而信号量的初始值可设置为 N 的整数变量, 如果 N +大于 0, 表示最多可以有 N 个线程进入临界区执行,如果 N 小于等于 0 ,表示不能有线程进入临界区了, +必须在后续操作中让信号量的值加 1 ,才能唤醒某个等待的线程。

+

Dijkstra 对信号量设计了两种操作:P(Proberen(荷兰语),尝试)操作和 V(Verhogen(荷兰语),增加)操作。 +P 操作是检查信号量的值是否大于 0,若该值大于 0,则将其值减 1 并继续(表示可以进入临界区了);若该值为 +0,则线程将睡眠。注意,此时 P 操作还未结束。而且由于信号量本身是一种临界资源(可回想一下上一节的锁, +其实也是一种临界资源),所以在 P 操作中,检查/修改信号量值以及可能发生的睡眠这一系列操作, +是一个不可分割的原子操作过程。通过原子操作才能保证,一旦 P 操作开始,则在该操作完成或阻塞睡眠之前, +其他线程均不允许访问该信号量。

+

V 操作会对信号量的值加 1 ,然后检查是否有一个或多个线程在该信号量上睡眠等待。如有, +则选择其中的一个线程唤醒并允许该线程继续完成它的 P 操作;如没有,则直接返回。注意,信号量的值加 1, +并可能唤醒一个线程的一系列操作同样也是不可分割的原子操作过程。不会有某个进程因执行 V 操作而阻塞。

+

如果信号量是一个任意的整数,通常被称为计数信号量(Counting Semaphore),或一般信号量(General +Semaphore);如果信号量只有0或1的取值,则称为二值信号量(Binary Semaphore)。可以看出, +互斥锁是信号量的一种特例 — 二值信号量,信号量很好地解决了最多允许 N 个线程访问临界资源的情况。

+

信号量的一种实现伪代码如下所示:

+
 1fn P(S) {
+ 2    if S >= 1
+ 3        S = S - 1;
+ 4    else
+ 5        <block and enqueue the thread>;
+ 6}
+ 7fn V(S) {
+ 8    if <some threads are blocked on the queue>
+ 9        <unblock a thread>;
+10    else
+11        S = S + 1;
+12}
+
+
+

在上述实现中,S 的取值范围为大于等于 0 的整数。S 的初值一般设置为一个大于 0 的正整数, +表示可以进入临界区的线程数。当 S 取值为 1,表示是二值信号量,也就是互斥锁了。 +使用信号量实现线程互斥访问临界区的伪代码如下:

+
 1let static mut S: semaphore = 1;
+ 2
+ 3// Thread i
+ 4fn  foo() {
+ 5    ...
+ 6    P(S);
+ 7    execute Cricital Section;
+ 8    V(S);
+ 9    ...
+10}
+
+
+

下面是另外一种信号量实现的伪代码:

+
 1fn P(S) {
+ 2    S = S - 1;
+ 3    if S < 0 then
+ 4        <block and enqueue the thread>;
+ 5}
+ 6
+ 7fn V(S) {
+ 8    S = S + 1;
+ 9    if <some threads are blocked on the queue>
+10        <unblock a thread>;
+11}
+
+
+

在这种实现中,S 的初值一般设置为一个大于 0 的正整数,表示可以进入临界区的线程数。但 S +的取值范围可以是小于 0 的整数,表示等待进入临界区的睡眠线程数。

+

信号量的另一种用途是用于实现同步(synchronization)。比如,把信号量的初始值设置为 0 , +当一个线程 A 对此信号量执行一个 P 操作,那么该线程立即会被阻塞睡眠。之后有另外一个线程 B +对此信号量执行一个 V 操作,就会将线程 A 唤醒。这样线程 B 中执行 V 操作之前的代码序列 B-stmts +和线程 A 中执行 P 操作之后的代码 A-stmts 序列之间就形成了一种确定的同步执行关系,即线程 B 的 +B-stmts 会先执行,然后才是线程 A 的 A-stmts 开始执行。相关伪代码如下所示:

+
 1let static mut S: semaphore = 0;
+ 2
+ 3//Thread A
+ 4...
+ 5P(S);
+ 6Label_2:
+ 7A-stmts after Thread B::Label_1;
+ 8...
+ 9
+10//Thread B
+11...
+12B-stmts before Thread A::Label_2;
+13Label_1:
+14V(S);
+15...
+
+
+
+
+

实现信号量

+
+

使用 semaphore 系统调用

+

我们通过例子来看看如何实际使用信号量。下面是面向应用程序对信号量系统调用的简单使用, +可以看到对它的使用与上一节介绍的互斥锁系统调用类似。

+

在这个例子中,主线程先创建了信号量初值为 0 的信号量 SEM_SYNC ,然后再创建两个线程 First +和 Second 。线程 First 会先睡眠 10ms,而当线程 Second 执行时,会由于执行信号量的 P +操作而等待睡眠;当线程 First 醒来后,会执行 V 操作,从而能够唤醒线程 Second。这样线程 First +和线程 Second 就形成了一种稳定的同步关系。

+
 1const SEM_SYNC: usize = 0; //信号量ID
+ 2unsafe fn first() -> ! {
+ 3    sleep(10);
+ 4    println!("First work and wakeup Second");
+ 5    semaphore_up(SEM_SYNC); //信号量V操作
+ 6    exit(0)
+ 7}
+ 8unsafe fn second() -> ! {
+ 9    println!("Second want to continue,but need to wait first");
+10    semaphore_down(SEM_SYNC); //信号量P操作
+11    println!("Second can work now");
+12    exit(0)
+13}
+14pub fn main() -> i32 {
+15    // create semaphores
+16    assert_eq!(semaphore_create(0) as usize, SEM_SYNC); // 信号量初值为0
+17    // create first, second threads
+18    ...
+19}
+20
+21pub fn sys_semaphore_create(res_count: usize) -> isize {
+22    syscall(SYSCALL_SEMAPHORE_CREATE, [res_count, 0, 0])
+23}
+24pub fn sys_semaphore_up(sem_id: usize) -> isize {
+25    syscall(SYSCALL_SEMAPHORE_UP, [sem_id, 0, 0])
+26}
+27pub fn sys_semaphore_down(sem_id: usize) -> isize {
+28    syscall(SYSCALL_SEMAPHORE_DOWN, [sem_id, 0, 0])
+29}
+
+
+
    +
  • 第 16 行,创建了一个初值为 0 ,ID 为 SEM_SYNC 的信号量,对应的是第 22 行 +SYSCALL_SEMAPHORE_CREATE 系统调用;

  • +
  • 第 10 行,线程 Second 执行信号量 P 操作(对应第 28行 SYSCALL_SEMAPHORE_DOWN +系统调用),由于信号量初值为 0 ,该线程将阻塞;

  • +
  • 第 5 行,线程 First 执行信号量 V 操作(对应第 25 行 SYSCALL_SEMAPHORE_UP 系统调用), +会唤醒等待该信号量的线程 Second。

  • +
+
+
+

实现 semaphore 系统调用

+

操作系统如何实现信号量系统调用呢?我们还是采用通常的分析做法:数据结构+方法, +即首先考虑一下与此相关的核心数据结构,然后考虑与数据结构相关的相关函数/方法的实现。

+

在线程的眼里,信号量是一种每个线程能看到的共享资源,且在一个进程中,可以存在多个不同信号量资源, +所以我们可以把所有的信号量资源放在一起让进程来管理,如下面代码第 9 行所示。这里需要注意的是: +semaphore_list: Vec<Option<Arc<Semaphore>>> 表示的是信号量资源的列表。而 Semaphore +是信号量的内核数据结构,由信号量值和等待队列组成。操作系统需要显式地施加某种控制,来确定当一个线程执行 +P 操作和 V 操作时,如何让线程睡眠或唤醒线程。在这里,P 操作是由 Semaphoredown +方法实现,而 V 操作是由 Semaphoreup 方法实现。

+
 1pub struct ProcessControlBlock {
+ 2    // immutable
+ 3    pub pid: PidHandle,
+ 4    // mutable
+ 5    inner: UPSafeCell<ProcessControlBlockInner>,
+ 6}
+ 7pub struct ProcessControlBlockInner {
+ 8    ...
+ 9    pub semaphore_list: Vec<Option<Arc<Semaphore>>>,
+10}
+11
+12pub struct Semaphore {
+13    pub inner: UPSafeCell<SemaphoreInner>,
+14}
+15pub struct SemaphoreInner {
+16    pub count: isize,
+17    pub wait_queue: VecDeque<Arc<TaskControlBlock>>,
+18}
+19impl Semaphore {
+20    pub fn new(res_count: usize) -> Self {
+21        Self {
+22            inner: unsafe { UPSafeCell::new(
+23                SemaphoreInner {
+24                    count: res_count as isize,
+25                    wait_queue: VecDeque::new(),
+26                }
+27            )},
+28        }
+29    }
+30
+31    pub fn up(&self) {
+32        let mut inner = self.inner.exclusive_access();
+33        inner.count += 1;
+34        if inner.count <= 0 {
+35            if let Some(task) = inner.wait_queue.pop_front() {
+36                add_task(task);
+37            }
+38        }
+39    }
+40
+41    pub fn down(&self) {
+42        let mut inner = self.inner.exclusive_access();
+43        inner.count -= 1;
+44        if inner.count < 0 {
+45            inner.wait_queue.push_back(current_task().unwrap());
+46            drop(inner);
+47            block_current_and_run_next();
+48        }
+49    }
+50}
+
+
+

首先是核心数据结构:

+
    +
  • 第 9 行,进程控制块中管理的信号量列表。

  • +
  • 第 16-17 行,信号量的核心数据成员:信号量值和等待队列。

  • +
+

然后是重要的三个成员函数:

+
    +
  • 第 20 行,创建信号量,信号量初值为参数 res_count

  • +
  • 第 31 行,实现 V 操作的 up 函数,第 34 行,当信号量值小于等于 0 时, +将从信号量的等待队列中弹出一个线程放入线程就绪队列。

  • +
  • 第 41 行,实现 P 操作的 down 函数,第 44 行,当信号量值小于 0 时, +将把当前线程放入信号量的等待队列,设置当前线程为挂起状态并选择新线程执行。

  • +
+

Dijkstra, Edsger W. Cooperating sequential processes (EWD-123) (PDF). E.W. Dijkstra Archive. +Center for American History, University of Texas at Austin. (transcription) (September 1965) +https://www.cs.utexas.edu/users/EWD/transcriptions/EWD01xx/EWD123.html

+

Downey, Allen B. (2016) [2005]. “The Little Book of Semaphores” (2nd ed.). Green Tea Press.

+

Leppäjärvi, Jouni (May 11, 2008). “A pragmatic, historically oriented survey on the universality +of synchronization primitives” (pdf). University of Oulu, Finland.

+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter8/4condition-variable.html b/chapter8/4condition-variable.html new file mode 100644 index 0000000..41edcd3 --- /dev/null +++ b/chapter8/4condition-variable.html @@ -0,0 +1,676 @@ + + + + + + + + 条件变量机制 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

条件变量机制

+
+

本节导读

+

到目前为止,我们已经了解了操作系统提供的互斥锁和信号量。但应用程序在使用这两者时需要非常小心, +如果使用不当,就会产生效率低下、竞态条件、死锁或者其他一些不可预测的情况。为了简化编程、避免错误, +计算机科学家针对某些情况设计了一种更高层的同步互斥原语。具体而言,在有些情况下, +线程需要检查某一条件(condition)满足之后,才会继续执行。

+

我们来看一个例子,有两个线程 first 和 second 在运行,线程 first 会把全局变量 A 设置为 +1,而线程 second 在 A != 0 的条件满足后,才能继续执行,如下面的伪代码所示:

+
 1static mut A: usize = 0;
+ 2unsafe fn first() -> ! {
+ 3    A=1;
+ 4    ...
+ 5}
+ 6
+ 7unsafe fn second() -> ! {
+ 8    while A==0 {
+ 9      // 忙等或睡眠等待 A==1
+10    };
+11    //继续执行相关事务
+12}
+
+
+

在上面的例子中,如果线程 second 先执行,会忙等在 while 循环中,在操作系统的调度下,线程 +first 会执行并把 A 赋值为 1 后,然后线程 second 再次执行时,就会跳出 while 循环,进行接下来的工作。 +配合互斥锁,可以正确完成上述带条件的同步流程,如下面的伪代码所示:

+
 1static mut A: usize = 0;
+ 2unsafe fn first() -> ! {
+ 3    mutex.lock();
+ 4    A=1;
+ 5    mutex.unlock();
+ 6    ...
+ 7}
+ 8
+ 9unsafe fn second() -> ! {
+10    mutex.lock();
+11    while A==0 {
+12        mutex.unlock();
+13        // give other thread a chance to lock
+14        mutex.lock();
+15    };
+16    mutex.unlock();
+17    //继续执行相关事务
+18}
+
+
+

这种实现能执行,但效率低下,因为线程 second 会忙等检查,浪费处理器时间。我们希望有某种方式让线程 +second 休眠,直到等待的条件满足,再继续执行。于是,我们可以写出如下的代码:

+
 1static mut A: usize = 0;
+ 2unsafe fn first() -> ! {
+ 3    mutex.lock();
+ 4    A=1;
+ 5    wakup(second);
+ 6    mutex.unlock();
+ 7    ...
+ 8}
+ 9
+10unsafe fn second() -> ! {
+11    mutex.lock();
+12    while A==0 {
+13       wait();
+14    };
+15    mutex.unlock();
+16    //继续执行相关事务
+17}
+
+
+

粗略地看,这样就可以实现睡眠等待了。但请同学仔细想想,当线程 second 在睡眠的时候, mutex +是否已经上锁了? 确实,线程 second 是带着上锁的 mutex 进入等待睡眠状态的。 +如果这两个线程的调度顺序是先执行线程 second,再执行线程first,那么线程 second 会先睡眠且拥有 +mutex 的锁;当线程 first 执行时,会由于没有 mutex 的锁而进入等待锁的睡眠状态。 +结果就是两个线程都睡了,都执行不下去,这就出现了 死锁

+

这里需要解决的两个关键问题: 如何等待一个条件?在条件为真时如何向等待线程发出信号 。 +我们的计算机科学家给出了 管程(Monitor)条件变量(Condition Variables) +这种巧妙的方法。接下来,我们就会深入讲解条件变量的设计与实现。

+
+
+

条件变量的基本思路

+

管程有一个很重要的特性,即任一时刻只能有一个活跃线程调用管程中的过程, +这一特性使线程在调用执行管程中过程时能保证互斥,这样线程就可以放心地访问共享变量。 +管程是编程语言的组成部分,编译器知道其特殊性,因此可以采用与其他过程调用不同的方法来处理对管程的调用. +因为是由编译器而非程序员来生成互斥相关的代码,所以出错的可能性要小。

+

管程虽然借助编译器提供了一种实现互斥的简便途径,但这还不够,还需要一种线程间的沟通机制。 +首先是等待机制:由于线程在调用管程中某个过程时,发现某个条件不满足,那就在无法继续运行而被阻塞。 +其次是唤醒机制:另外一个线程可以在调用管程的过程中,把某个条件设置为真,并且还需要有一种机制, +及时唤醒等待条件为真的阻塞线程。为了避免管程中同时有两个活跃线程, +我们需要一定的规则来约定线程发出唤醒操作的行为。目前有三种典型的规则方案:

+
    +
  • Hoare 语义:线程发出唤醒操作后,马上阻塞自己,让新被唤醒的线程运行。注:此时唤醒线程的执行位置还在管程中。

  • +
  • Hansen 语义:是执行唤醒操作的线程必须立即退出管程,即唤醒操作只可能作为一个管程过程的最后一条语句。 +注:此时唤醒线程的执行位置离开了管程。

  • +
  • Mesa 语义:唤醒线程在发出行唤醒操作后继续运行,并且只有它退出管程之后,才允许等待的线程开始运行。 +注:此时唤醒线程的执行位置还在管程中。

  • +
+

一般开发者会采纳 Brinch Hansen 的建议,因为它在概念上更简单,并且更容易实现。这种沟通机制的具体实现就是 +条件变量 和对应的操作:wait 和 signal。线程使用条件变量来等待一个条件变成真。 +条件变量其实是一个线程等待队列,当条件不满足时,线程通过执行条件变量的 wait +操作就可以把自己加入到等待队列中,睡眠等待(waiting)该条件。另外某个线程,当它改变条件为真后, +就可以通过条件变量的 signal 操作来唤醒一个或者多个等待的线程(通过在该条件上发信号),让它们继续执行。

+

早期提出的管程是基于 Concurrent Pascal 来设计的,其他语言如 C 和 Rust 等,并没有在语言上支持这种机制。 +我们还是可以用手动加入互斥锁的方式来代替编译器,就可以在 C 和 Rust 的基础上实现原始的管程机制了。 +在目前的 C 语言应用开发中,实际上也是这么做的。这样,我们就可以用互斥锁和条件变量, +来重现上述的同步互斥例子:

+
 1static mut A: usize = 0;
+ 2unsafe fn first() -> ! {
+ 3    mutex.lock();
+ 4    A=1;
+ 5    condvar.wakup();
+ 6    mutex.unlock();
+ 7    ...
+ 8}
+ 9
+10unsafe fn second() -> ! {
+11    mutex.lock();
+12    while A==0 {
+13       condvar.wait(mutex); //在睡眠等待之前,需要释放mutex
+14    };
+15    mutex.unlock();
+16    //继续执行相关事务
+17}
+
+
+

有了上面的介绍,我们就可以实现条件变量的基本逻辑了。下面是条件变量的 wait 和 signal 操作的伪代码:

+
1fn wait(mutex) {
+2    mutex.unlock();
+3    <block and enqueue the thread>;
+4    mutex.lock();
+5}
+6
+7fn signal() {
+8    <unblock a thread>;
+9}
+
+
+

条件变量的wait操作包含三步,1. 释放锁;2. 把自己挂起;3. 被唤醒后,再获取锁。条件变量的 signal +操作只包含一步:找到挂在条件变量上睡眠的线程,把它唤醒。

+

注意,条件变量不像信号量那样有一个整型计数值的成员变量,所以条件变量也不能像信号量那样有读写计数值的能力。 +如果一个线程向一个条件变量发送唤醒操作,但是在该条件变量上并没有等待的线程,则唤醒操作实际上什么也没做。

+
+
+

实现条件变量

+
+

使用 condvar 系统调用

+

我们通过例子来看看如何实际使用条件变量。下面是面向应用程序对条件变量系统调用的简单使用, +可以看到对它的使用与上一节介绍的信号量系统调用类似。 在这个例子中,主线程先创建了初值为 1 +的互斥锁和一个条件变量,然后再创建两个线程 First 和 Second。线程 First 会先睡眠 10ms,而当线程 +Second 执行时,会由于条件不满足执行条件变量的 wait 操作而等待睡眠;当线程 First 醒来后,通过设置 +A 为 1,让线程 second 等待的条件满足,然后会执行条件变量的 signal 操作,从而能够唤醒线程 Second。 +这样线程 First 和线程 Second 就形成了一种稳定的同步与互斥关系。

+
 1static mut A: usize = 0;   //全局变量
+ 2
+ 3const CONDVAR_ID: usize = 0;
+ 4const MUTEX_ID: usize = 0;
+ 5
+ 6unsafe fn first() -> ! {
+ 7    sleep(10);
+ 8    println!("First work, Change A --> 1 and wakeup Second");
+ 9    mutex_lock(MUTEX_ID);
+10    A=1;
+11    condvar_signal(CONDVAR_ID);
+12    mutex_unlock(MUTEX_ID);
+13    ...
+14}
+15unsafe fn second() -> ! {
+16    println!("Second want to continue,but need to wait A=1");
+17    mutex_lock(MUTEX_ID);
+18    while A==0 {
+19        condvar_wait(CONDVAR_ID, MUTEX_ID);
+20    }
+21    mutex_unlock(MUTEX_ID);
+22    ...
+23}
+24pub fn main() -> i32 {
+25    // create condvar & mutex
+26    assert_eq!(condvar_create() as usize, CONDVAR_ID);
+27    assert_eq!(mutex_blocking_create() as usize, MUTEX_ID);
+28    // create first, second threads
+29    ...
+30}
+31
+32pub fn condvar_create() -> isize {
+33    sys_condvar_create(0)
+34}
+35pub fn condvar_signal(condvar_id: usize) {
+36    sys_condvar_signal(condvar_id);
+37}
+38pub fn condvar_wait(condvar_id: usize, mutex_id: usize) {
+39    sys_condvar_wait(condvar_id, mutex_id);
+40}
+
+
+
    +
  • 第 26 行,创建了一个 ID 为 CONDVAR_ID 的条件量,对应第 33 行 SYSCALL_CONDVAR_CREATE 系统调用;

  • +
  • 第 19 行,线程 Second 执行条件变量 wait 操作(对应第 39 行 SYSCALL_CONDVAR_WAIT 系统调用), +该线程将释放 mutex 锁并阻塞;

  • +
  • 第 5 行,线程 First 执行条件变量 signal 操作(对应第 36 行 SYSCALL_CONDVAR_SIGNAL 系统调用), +会唤醒等待该条件变量的线程 Second。

  • +
+
+
+

实现 condvar 系统调用

+

操作系统如何实现条件变量系统调用呢?在线程的眼里,条件变量是一种每个线程能看到的共享资源, +且在一个进程中,可以存在多个不同条件变量资源,所以我们可以把所有的条件变量资源放在一起让进程来管理, +如下面代码第9行所示。这里需要注意的是: condvar_list: Vec<Option<Arc<Condvar>>> +表示的是条件变量资源的列表。而 Condvar 是条件变量的内核数据结构,由等待队列组成。 +操作系统需要显式地施加某种控制,来确定当一个线程执行 wait 操作和 signal 操作时, +如何让线程睡眠或唤醒线程。在这里, wait 操作是由 Condvarwait 方法实现,而 signal +操作是由 Condvarsignal 方法实现。

+
 1pub struct ProcessControlBlock {
+ 2    // immutable
+ 3    pub pid: PidHandle,
+ 4    // mutable
+ 5    inner: UPSafeCell<ProcessControlBlockInner>,
+ 6}
+ 7pub struct ProcessControlBlockInner {
+ 8    ...
+ 9    pub condvar_list: Vec<Option<Arc<Condvar>>>,
+10}
+11pub struct Condvar {
+12    pub inner: UPSafeCell<CondvarInner>,
+13}
+14pub struct CondvarInner {
+15    pub wait_queue: VecDeque<Arc<TaskControlBlock>>,
+16}
+17impl Condvar {
+18    pub fn new() -> Self {
+19        Self {
+20            inner: unsafe { UPSafeCell::new(
+21                CondvarInner {
+22                    wait_queue: VecDeque::new(),
+23                }
+24            )},
+25        }
+26    }
+27    pub fn signal(&self) {
+28        let mut inner = self.inner.exclusive_access();
+29        if let Some(task) = inner.wait_queue.pop_front() {
+30            add_task(task);
+31        }
+32    }
+33    pub fn wait(&self, mutex:Arc<dyn Mutex>) {
+34        mutex.unlock();
+35        let mut inner = self.inner.exclusive_access();
+36        inner.wait_queue.push_back(current_task().unwrap());
+37        drop(inner);
+38        block_current_and_run_next();
+39        mutex.lock();
+40    }
+41}
+
+
+

首先是核心数据结构:

+
    +
  • 第 9 行,进程控制块中管理的条件变量列表。

  • +
  • 第 15 行,条件变量的核心数据成员:等待队列。

  • +
+

然后是重要的三个成员函数:

+
    +
  • 第 18 行,创建条件变量,即创建了一个空的等待队列。

  • +
  • 第 27 行,实现 signal 操作,将从条件变量的等待队列中弹出一个线程放入线程就绪队列。

  • +
  • 第 33 行,实现 wait 操作,释放 mutex 互斥锁,将把当前线程放入条件变量的等待队列, +设置当前线程为挂起状态并选择新线程执行。在恢复执行后,再加上 mutex 互斥锁。

  • +
+

Hansen, Per Brinch (1993). “Monitors and concurrent Pascal: a personal history”. HOPL-II: +The second ACM SIGPLAN conference on History of programming languages. History of Programming +Languages. New York, NY, USA: ACM. pp. 1–35. doi:10.1145/155360.155361. ISBN 0-89791-570-4.

+
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter8/5exercise.html b/chapter8/5exercise.html new file mode 100644 index 0000000..575f8a5 --- /dev/null +++ b/chapter8/5exercise.html @@ -0,0 +1,546 @@ + + + + + + + + chapter8 练习 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

chapter8 练习

+
+

Lab5 编程作业

+
+

警告

+

本次实验框架变动较大,且改动较为复杂,为降低同学们的工作量,本次实验不要求合并之前的实验内容, +只需通过 ch8 的全部测例和其他章节的基础测例即可。你可以参考 lab5(os8)参考框架: 上完成以下作业。

+
+
+

注解

+

本次实验的工作量约为 100 行代码。

+
+
+

死锁检测

+

目前的 mutex 和 semaphore 相关的系统调用不会分析资源的依赖情况,用户程序可能出现死锁。 +我们希望在系统中加入死锁检测机制,当发现可能发生死锁时拒绝对应的资源获取请求。 +一种检测死锁的算法如下:

+

定义如下三个数据结构:

+
    +
  • 可利用资源向量 Available :含有 m 个元素的一维数组,每个元素代表可利用的某一类资源的数目, +其初值是该类资源的全部可用数目,其值随该类资源的分配和回收而动态地改变。 +Available[j] = k,表示第 j 类资源的可用数量为 k。

  • +
  • 分配矩阵 Allocation:n * m 矩阵,表示每类资源已分配给每个线程的资源数。 +Allocation[i,j] = g,则表示线程 i 当前己分得第 j 类资源的数量为 g。

  • +
  • 需求矩阵 Need:n * m 的矩阵,表示每个线程还需要的各类资源数量。 +Need[i,j] = d,则表示线程 i 还需要第 j 类资源的数量为 d 。

  • +
+

算法运行过程如下:

+
    +
  1. 设置两个向量: 工作向量 Work,表示操作系统可提供给线程继续运行所需的各类资源数目,它含有 +m 个元素。初始时,Work = Available ;结束向量 Finish,表示系统是否有足够的资源分配给线程, +使之运行完成。初始时 Finish[0..n-1] = false,表示所有线程都没结束;当有足够资源分配给线程时, +设置 Finish[i] = true。

  2. +
  3. 从线程集合中找到一个能满足下述条件的线程

  4. +
+
1Finish[i] == false;
+2Need[i,j] ≤ Work[j];
+
+
+

若找到,执行步骤 3,否则执行步骤 4。

+
    +
  1. 当线程 thr[i] 获得资源后,可顺利执行,直至完成,并释放出分配给它的资源,故应执行:

  2. +
+
1Work[j] = Work[j] + Allocation[i, j];
+2Finish[i] = true;
+
+
+

跳转回步骤2

+
    +
  1. 如果 Finish[0..n-1] 都为 true,则表示系统处于安全状态;否则表示系统处于不安全状态,即出现死锁。

  2. +
+

出于兼容性和灵活性考虑,我们允许进程按需开启或关闭死锁检测功能。为此我们将实现一个新的系统调用: +sys_enable_deadlock_detect

+

enable_deadlock_detect

+
    +
  • syscall ID: 469

  • +
  • 功能:为当前进程启用或禁用死锁检测功能。

  • +
  • C 接口: int enable_deadlock_detect(int is_enable)

  • +
  • Rust 接口: fn enable_deadlock_detect(is_enable: i32) -> i32

  • +
  • +
    参数:
      +
    • is_enable: 为 1 表示启用死锁检测, 0 表示禁用死锁检测。

    • +
    +
    +
    +
  • +
  • +
    说明:
      +
    • 开启死锁检测功能后, mutex_locksemaphore_down 如果检测到死锁, +应拒绝相应操作并返回 -0xDEAD (十六进制值)。

    • +
    • 简便起见可对 mutex 和 semaphore 分别进行检测,无需考虑二者 (以及 waittid 等) +混合使用导致的死锁。

    • +
    +
    +
    +
  • +
  • 返回值:如果出现了错误则返回 -1,否则返回 0。

  • +
  • +
    可能的错误
      +
    • 参数不合法

    • +
    • 死锁检测开启失败

    • +
    +
    +
    +
  • +
+
+
+

实验要求

+ +
+
+
+

问答作业

+
    +
  1. 在我们的多线程实现中,当主线程 (即 0 号线程) 退出时,视为整个进程退出, +此时需要结束该进程管理的所有线程并回收其资源。 +- 需要回收的资源有哪些? +- 其他线程的 TaskControlBlock 可能在哪些位置被引用,分别是否需要回收,为什么?

  2. +
  3. 对比以下两种 Mutex.unlock 的实现,二者有什么区别?这些区别可能会导致什么问题?

  4. +
+
 1impl Mutex for Mutex1 {
+ 2    fn unlock(&self) {
+ 3        let mut mutex_inner = self.inner.exclusive_access();
+ 4        assert!(mutex_inner.locked);
+ 5        mutex_inner.locked = false;
+ 6        if let Some(waking_task) = mutex_inner.wait_queue.pop_front() {
+ 7            add_task(waking_task);
+ 8        }
+ 9    }
+10}
+11
+12impl Mutex for Mutex2 {
+13    fn unlock(&self) {
+14        let mut mutex_inner = self.inner.exclusive_access();
+15        assert!(mutex_inner.locked);
+16        if let Some(waking_task) = mutex_inner.wait_queue.pop_front() {
+17            add_task(waking_task);
+18        } else {
+19            mutex_inner.locked = false;
+20        }
+21    }
+22}
+
+
+
+
+

报告要求

+
    +
  • 简单总结你实现的功能(200字以内,不要贴代码)及你完成本次实验所用的时间。

  • +
  • 完成问答题。

  • +
  • (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。

  • +
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/chapter8/index.html b/chapter8/index.html new file mode 100644 index 0000000..809c7bd --- /dev/null +++ b/chapter8/index.html @@ -0,0 +1,470 @@ + + + + + + + + 第八章:并发 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+ + +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/genindex.html b/genindex.html new file mode 100644 index 0000000..0b9f9d2 --- /dev/null +++ b/genindex.html @@ -0,0 +1,363 @@ + + + + + + + 索引 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+ +
+

索引

+
+
+ +
+ +
+ +
+
+ + + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..25693c7 --- /dev/null +++ b/index.html @@ -0,0 +1,440 @@ + + + + + + + + Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

2022年开源操作系统训练营

+
+
+
+
+
+
+
+

项目简介

+

本教程展示了如何 从零开始Rust 语言写一个基于 RISC-V 架构的 类 Unix 内核

+

用于 2022年开源操作系统训练营。

+
+
+

导读

+

请先阅读 第零章:实验环境配置 完成环境配置。

+

以下是读者为了完成实验需掌握的技术,你可以在实操中熟悉它们。

+
    +
  • 阅读简单的 Makefile 文件;

  • +
  • 阅读简单的 RISC-V 汇编代码;

  • +
  • git 的基本功能,解决 git merge 冲突的办法;

  • +
  • Rust 基本语法和一些进阶语法,包括 Cargo 项目结构、Trait、函数式编程、Unsafe Rust、错误处理等

  • +
+
+
+

鸣谢

+

本项目基于 2022 年春季学期操作系统实验指导书 ,重构的目标是在保留结构的基础上屏蔽不必要的细节,缩短篇幅,优化语言,降低阅读成本。

+

如果你觉得本教程某些章节不够细致或不够连贯,可以参考 2022春季OS课程讲义 rCore Tutorial v3 Guide

+
+

注解

+

这是一个注解,以这种方式出现的卡片提供了非必要的背景知识,你可以选择忽略。

+
+
+

注意

+

虽然实验本身在总评中占比有限,但根据往届经验,考试中可能大量出现与编程作业、思考题、代码实现思路直接相关的题目。

+
+
+
+

项目协作

+
    +
  • 修改和构建本项目 介绍了如何基于 Sphinx 框架配置文档开发环境,之后可以本地构建并渲染 html 或其他格式的文档;

  • +
  • reStructuredText 基本语法 给出了目前编写文档才用的 ReStructuredText 标记语言的一些基础语法及用例;

  • +
  • 时间仓促,本项目还有很多不完善之处,欢迎大家积极在每一个章节的评论区留言,或者提交 Issues 或 Pull Requests,让我们 +一起努力让这本书变得更好!

  • +
+
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/objects.inv b/objects.inv new file mode 100644 index 0000000000000000000000000000000000000000..fa62fac197d569106461a2a1b31d5957d6b6fd5d GIT binary patch literal 2295 zcmVNERX>N99Zgg*Qc_4OWa&u{KZXhxWBOp+6Z)#;@bUGkUaAj^S zQ*U*0V`VK*Q!P|=bZ>HLVQei^b!}~BaxHIWEkkc)Wi2u=GBOGyAXa5^b7^mGIv@%o zAXI2&AaZ4GVQFq;WpW^IW*~HEX>%ZEX>4U6X>%ZBZ*6dLWpi_7WFU2OX>MmAdTeQ8 zE(&`>*hOyN8)WkArMJW^X&Qmz;LrX5ZW!slj@YEImmkaDycTNB}d- zM-n~~oCyZ)H=cm1lmx_r+jPT52XnIak@ zrKzm#D1TB6Ig5s}_xjepD>}NC^^T7_yXW3-2cNHYXzf+!KP%qa34Jr)Sy=V1YZ)xd z#J16dg>{0|5_(9i5~yQ4K-Ta$(R^&Vh)suBg<``1DTJ1-y8>#gjc!N0$kZ71uUHNEC*?`qpSde;8<*xi1a zfR3ktj&neN@Hc&V*?u=iFAnL#g}3v#b$#P)9=Y3f_iQTxmQ#Rb4)Biv>|XqrE*!dh z4f^uPJ*v{HO0-B*ukv*9yn>B60z zW8F{i3$f}r2G59tHLit0G1f-=mrLLl-@q`?z>-y)aB;hxGD zJLc^~60=WAVV{(ffLNL;<8CJOE3*O-MD3NgcFM~EDVHC|j$B0cjO`Gei`OH;0=mm} z`u4H6{>0r_ZC&q$)hi>&M=mrcWg~2Fx$f@IdyRKcx9HNMyZR;pPNRS%pnw!o;PcfI zkRWEja)K!ewA3Hy#!49I?&f>9ofn5SpAy!5Zb&a{j*2t`E5zbz_eew_Z+AL#8&Nsg z`~NB$+hbPjpb zx_ruHjHn&r+D+-4Ze#aZkKN(8PlNl_HZk;YT$_Vpk-aAi?!l!Whp-$%dD%Oq`?KD^ z8=dDL5;G$Kf&H}bNj8pcq9E`Z^Q{S`Bcrbyt;QN_C@5<1tCof zeKe)JyAe$}$1p4=CeRJv^|H^{f z@Pvb(6OicwWJZF84SD9S9n#7#383VQf4Qde1>?JJ)_t+T8p#rD&!7oA`wbM+Z&Qs) zw>2i+#@I8)SiWrgH7Z(79==?IBH<{+dq;6OeIGt9W21?=GzyOhC_K_j;SnK)NBWD| z!uzN@V-5m%b3gZ2C@QqJL)U+fQAfo@9!(+gD3mYNBwVdvrQ7JhrJpECvaVoj(niqh zB8w=hMPxw-FBXpc`T4rleCt1IEB}O|<)=mP+RMT&vz%?_QzF2u_C#Q`Hv*%=2#gMN zJ@af4fxDzLy8{pT7->v=>SHOVK9(DDiWb3&n5_LG>~#;%Rj9FE<7S|ojpZcGkRNjK zf|Y1Ou{jr(>O?PZ6CImHxMaA>$#Qhf-BfAiqO(}($0^80Nl}Abo`Q}Rk86BDj$Bm6 z_N%~kT6w_%PwRNc1Rd{Kwm0}=!r+e$q~C8*FHr}7fP#iXfWNjAAJT1yHYhVmN{Q93 zTKqM#_~8w(Bs1V0BO=|CBwGJKuV&rGI{Qp**WSY}+v|s!?${3%)ybk@_9y5j16RTE zhbW#4aP`?~0jS(RuT7rjzhusjzm&++wk4;kH1)QK5n;z@#>E{*R#NA+fq=JG}DGp0VdHVdhxh}^2CS9GPaa}w_lO{0fMSPFjX0A2x`6=&qVG{{SeGHG%P&N zTX>^sQ#!K8eSb7TYz6bz1VhJ@s(hzY)QRRO#v_r}=`Um!#=-uh?M}8$3p$7B-uHJF z|2=e@tprv9tg@;}n!)szfwcG~~| literal 0 HcmV?d00001 diff --git a/rest-example.html b/rest-example.html new file mode 100644 index 0000000..5bbe710 --- /dev/null +++ b/rest-example.html @@ -0,0 +1,458 @@ + + + + + + + + reStructuredText 基本语法 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

reStructuredText 基本语法

+
+
+
+

注解

+

下面是一个注记。

+

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

+

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

+
+
+

警告

+

下面是一个警告。

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

下面继续我们的警告。

+
+
+

注意

+

Here is an attention.

+
+
+

小心

+

please be cautious!

+
+
+

错误

+

下面是一个错误。

+
+
+

危险

+

it is dangerous!

+
+
+

小技巧

+

here is a tip

+
+
+

重要

+

this is important!

+
+
+

提示

+

this is a hint.

+
+

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

+

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

+

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

+

下面是一个测试 gif。

+resources/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/search.html b/search.html new file mode 100644 index 0000000..b4eace7 --- /dev/null +++ b/search.html @@ -0,0 +1,370 @@ + + + + + + + 搜索 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+ +
+

错误

+

+ Please activate JavaScript to enable the search functionality. +

+
+ + +
+ +
+ +
+ +
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/searchindex.js b/searchindex.js new file mode 100644 index 0000000..84af77a --- /dev/null +++ b/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({docnames:["0setup-devel-env","appendix-a/index","appendix-b/index","appendix-c/index","appendix-d/index","chapter1/0intro","chapter1/1app-ee-platform","chapter1/2remove-std","chapter1/3mini-rt-usrland","chapter1/4mini-rt-baremetal","chapter1/5exercise","chapter1/index","chapter2/0intro","chapter2/2application","chapter2/3batch-system","chapter2/4trap-handling","chapter2/5exercise","chapter2/index","chapter3/0intro","chapter3/1multi-loader","chapter3/2task-switching","chapter3/3multiprogramming","chapter3/4time-sharing-system","chapter3/5exercise","chapter3/index","chapter4/0intro","chapter4/3sv39-implementation-1","chapter4/4sv39-implementation-2","chapter4/5kernel-app-spaces","chapter4/6multitasking-based-on-as","chapter4/7exercise","chapter4/index","chapter5/0intro","chapter5/1process","chapter5/2core-data-structures","chapter5/3implement-process-mechanism","chapter5/4exercise","chapter5/index","chapter6/0intro","chapter6/1file-descriptor","chapter6/1fs-interface","chapter6/2fs-implementation-1","chapter6/2fs-implementation-2","chapter6/3using-easy-fs-in-kernel","chapter6/4exercise","chapter6/index","chapter7/0intro","chapter7/1pipe","chapter7/2cmdargs-and-redirection","chapter7/3exercise","chapter7/index","chapter8/0intro","chapter8/1thread-kernel","chapter8/2lock","chapter8/3semaphore","chapter8/4condition-variable","chapter8/5exercise","chapter8/index","index","rest-example","setup-sphinx"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":4,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":3,"sphinx.domains.rst":2,"sphinx.domains.std":2,sphinx:56},filenames:["0setup-devel-env.rst","appendix-a/index.rst","appendix-b/index.rst","appendix-c/index.rst","appendix-d/index.rst","chapter1/0intro.rst","chapter1/1app-ee-platform.rst","chapter1/2remove-std.rst","chapter1/3mini-rt-usrland.rst","chapter1/4mini-rt-baremetal.rst","chapter1/5exercise.rst","chapter1/index.rst","chapter2/0intro.rst","chapter2/2application.rst","chapter2/3batch-system.rst","chapter2/4trap-handling.rst","chapter2/5exercise.rst","chapter2/index.rst","chapter3/0intro.rst","chapter3/1multi-loader.rst","chapter3/2task-switching.rst","chapter3/3multiprogramming.rst","chapter3/4time-sharing-system.rst","chapter3/5exercise.rst","chapter3/index.rst","chapter4/0intro.rst","chapter4/3sv39-implementation-1.rst","chapter4/4sv39-implementation-2.rst","chapter4/5kernel-app-spaces.rst","chapter4/6multitasking-based-on-as.rst","chapter4/7exercise.rst","chapter4/index.rst","chapter5/0intro.rst","chapter5/1process.rst","chapter5/2core-data-structures.rst","chapter5/3implement-process-mechanism.rst","chapter5/4exercise.rst","chapter5/index.rst","chapter6/0intro.rst","chapter6/1file-descriptor.rst","chapter6/1fs-interface.rst","chapter6/2fs-implementation-1.rst","chapter6/2fs-implementation-2.rst","chapter6/3using-easy-fs-in-kernel.rst","chapter6/4exercise.rst","chapter6/index.rst","chapter7/0intro.rst","chapter7/1pipe.rst","chapter7/2cmdargs-and-redirection.rst","chapter7/3exercise.rst","chapter7/index.rst","chapter8/0intro.rst","chapter8/1thread-kernel.rst","chapter8/2lock.rst","chapter8/3semaphore.rst","chapter8/4condition-variable.rst","chapter8/5exercise.rst","chapter8/index.rst","index.rst","rest-example.rst","setup-sphinx.rst"],objects:{},objnames:{},objtypes:{},terms:{"&&":[0,35,60],"++++":35,"++++++":35,"--":[0,2,6,7,8,9,12,18,32,51,55],"---":[0,12,18,32,51],"----":[0,12,18,32,35,51],"-----":51,"------":51,"-------":51,"--------":51,"---------":51,"----------":51,"------------":51,"-------------------------------------------------------------------------------":[5,12,18,25,32,38,46],"----.":[0,12,18,32],".----":[0,12,18,32],"..":[0,2,9,12,14,18,19,21,27,28,29,32,34,40,41,42,43,47,48,52,53,56],"...":[9,12,22,23,25,29,32,34,35,36,38,39,43,46,51,52,53,54,55],"....":51,"......":[2,7],"._____":[0,12,18,32],".______":[0,12,18,32],"0.0":[12,18,32],"0.06":[7,8],"0.1":0,"0.15":9,"0.2":[0,12,18,32],"0.21":0,"0.26":[2,8],"0.4":44,"0.61":8,"00":2,"000":22,"0000000000005060":2,"0000000000005070":2,"00000000000051a0":2,"0000000000011120":8,"01":[2,8],"02":6,"03":2,"04":[0,2],"05":2,"06s":[7,8],"08":2,"09":8,"0d":2,"0f":2,"0o040000":44,"0o100000":44,"0u8":[40,41,47],"0usiz":[27,41,43,47,48],"0x0":[2,7,12,18,28,29,32],"0x00000000":0,"0x001":40,"0x002":40,"0x08u8":33,"0x0au8":33,"0x0du8":33,"0x1":2,"0x1000":[9,43],"0x10001000":43,"0x2":[2,7,8],"0x200":40,"0x20000":19,"0x222":[12,18,32],"0x2d8":2,"0x2d8d0":2,"0x3":2,"0x32d8d0":2,"0x3e":2,"0x4":2,"0x40":2,"0x400":40,"0x45":28,"0x46":28,"0x4c":28,"0x5020":2,"0x5070":2,"0x51a0":2,"0x52c0":2,"0x6":2,"0x628":2,"0x7":30,"0x7f":28,"0x7fu8":33,"0x80000000":[0,9,12,18,27,32],"0x80020000":2,"0x800fffff":[12,18,32],"0x80200000":[0,9,12,18,32],"0x80203000":0,"0x80205000":0,"0x80206000":0,"0x8020b040":12,"0x8020f868":12,"0x80214090":12,"0x80216000":0,"0x80217000":0,"0x80218988":12,"0x8021d160":12,"0x80221a68":12,"0x80226538":12,"0x80400000":[13,14,15,19],"0x807fffff":[12,18,32],"0x80800000":27,"0x87000000":0,"0x87000ef2":0,"0x88000000":0,"0xb1ab":[12,18,32],"0xdead":56,"0xe":2,"0xf3":7,"0xffffffffffffff":[12,18,32],"1.12":4,"1.15":6,"1.5":36,"1.61":6,"10":[0,2,15,18,21,22,26,28,35,36,40,41,42,43,47,48,52,53,54,55],"10.1145":55,"100":[18,22,32,40,44,56],"1000":52,"10000":[12,18],"100000":[12,18],"10007":[12,53],"101m":18,"106":12,"1080":0,"10g":30,"10m":[22,54,55],"11":[5,15,18,25,27,28,29,32,34,35,43,48,52,54,59],"110000":18,"11120":8,"11122":8,"112":18,"1145":55,"12":[2,4,12,15,18,20,21,26,27,29,35,41,43,48,52],"120000":18,"123":54,"1234":12,"124":21,"125":36,"127":0,"127.0":0,"128":41,"129":36,"13":[14,15,25,29,32,34,35,38,41,42,46,52,53],"130000":18,"138":[25,32],"14":[2,6,12,21,22,28,34,41,42,53],"14.0":6,"140":36,"140000":18,"143":18,"144":18,"14kib":41,"15":[2,26,28,29,35,55],"150000":18,"1526":25,"154":25,"155":5,"155360":55,"155360.155361":55,"155361":55,"157":2,"15s":[6,9],"16":[2,9,12,29,35,36,41,48,53,54],"160000":18,"1661":25,"169":[22,32],"17":[15,28,35,53,54],"170000":18,"18":[0,2,28,29,35,41,48,53,55],"18.04":0,"180":32,"180000":18,"19":[2,12,28,33,35,48,52,53,55],"190000":18,"1963":54,"1965":54,"1993":55,"1_000_000":22,"1e":2,"1m":30,"1u64":41,"1usiz":[26,27],"1z":0,"2.0":0,"2.48":0,"20":[0,2,15,18,26,32,35,40,53,54,59],"20.04":0,"200":[10,16,23,30,36,44,56],"20000":[12,18],"200000":18,"2005":54,"2008":54,"2016":54,"2020":[1,3],"2022":6,"2049":32,"207":5,"207887":2,"208006":2,"208067":2,"21":[0,2,12,15,18,33,35,42,47,52,53],"213":32,"215":30,"22":[2,6,18,29,42,47,52,54],"220":33,"221":33,"222":30,"228059":2,"229":32,"23":[26,33,38,42,46,48,53],"2327":32,"24":[2,15,28,35,48,52],"25":[2,5,15,26,33,34,40,42,47,52,54],"250":36,"2510":12,"255":36,"256":[26,28],"26":[2,15,25,32,35,38,41,42,46,47,55],"260":33,"2621":12,"264":2,"26s":[2,8],"27":[2,15,18,23,26,27,28,29,40,41,47,55],"2749":12,"277458":2,"278362":2,"28":[15,27,29,33,41,54],"29":[28,32,47,52],"297200":2,"2c":2,"2nd":54,"2usiz":53,"30":[15,29,42],"30000":[12,18],"306":38,"31":[2,13,15,25,28,48,54],"317":46,"32":[15,23,28,29,34,35,40,41,47,48,52,53],"33":[15,23,29,52,55],"3334992":2,"3349":38,"34":[5,15,28,29,33,35,47,48,54],"35":[29,32,44,47,53,55],"3574":46,"36":[12,18,25,28,29,32,55],"37":[29,44,47,52],"376":38,"37994":2,"37ca0":2,"37d10":2,"38":[26,42,53],"38021":2,"3824":12,"386471875":18,"387":46,"39":[5,26,28,29,35,42,55],"3946":38,"3b":2,"3cc85":2,"3d":2,"3f":2,"40":28,"400":36,"40000":[12,18],"4095":42,"4096":[9,26,27,41,42],"4097":42,"41":[2,33,35,38,42,54],"410":23,"4171":46,"418":38,"41fc":[8,9],"42":[2,12,18,46],"434":46,"435":12,"44":[2,26,27,29,54],"45":[2,29,48],"450":38,"46":[2,29],"466":46,"469":56,"47":[2,18,29],"48":[0,2,28,38,42,46],"48c70":2,"48fc0":2,"49":[2,46,52],"4c":2,"4k":[9,28,29],"4kib":43,"4mib":42,"50":2,"500":[23,53],"50000":[12,18],"505b":2,"5060":2,"5066":2,"5070":2,"5074":2,"5076":2,"5079":[2,12],"507a":2,"507d":2,"5081":2,"5082":2,"5083":2,"508a":2,"5091":2,"5098":2,"511":27,"512":[27,41,42],"5160":2,"51a0":2,"51a4":2,"51aa":2,"51ad":2,"51b4":2,"51b7":[8,9],"51b9":2,"51bc":2,"51c1":2,"51c5":2,"51ca":2,"51ce":2,"51cf":2,"52":27,"526":[38,46],"53":[26,28,33,38,46],"54":[2,42],"55":26,"56":[2,25,26,40,52],"57":47,"570":55,"5750":12,"58":52,"586":12,"59":[2,28,47,52],"5a":2,"5e":2,"5setup":59,"60":29,"60000":[12,18],"61":[6,33],"61s":8,"62":[12,15],"627":18,"63":[2,15,26,33,42],"64":[2,6,7,8,9,13,14,15,26,27,28,29,41],"64bit":[2,7],"64kib":41,"6500":52,"66":2,"667897727":18,"6700":52,"68":12,"68369a041":6,"68369a041cea809a87e5bd80701da90e0e0a4799":6,"7.0":0,"70000":[12,18],"720":51,"74":2,"78kib":41,"7a":2,"7f":2,"80":[8,44],"80000":[12,18],"817":18,"8192":42,"82":8,"8202":12,"83":2,"84":2,"8430":[8,9],"85":12,"8516":12,"86":[2,25],"87":[18,25],"871008973":18,"88":2,"8824":12,"89":2,"89791":55,"8a":2,"8b":[2,48],"8bit":36,"8d":2,"8n":15,"8usiz":29,"8woe":0,"90":2,"90000":[12,18],"93":[8,13],"9379":12,"98":18,"998244353":18,"\u4e00\u4e0b":[0,2,6,8,13,21,29,38,39,41,47,48,51,53,54,60],"\u4e00\u4e2a":[0,2,3,5,6,7,8,9,12,13,14,15,18,19,20,21,22,23,25,26,27,28,29,30,32,33,34,35,36,38,39,40,41,42,43,44,46,47,48,49,51,52,53,54,55,56,58,59,60],"\u4e00\u4e2a\u4e8c\u5143":41,"\u4e00\u4e9b":[0,2,5,7,15,18,21,22,25,26,28,29,33,34,35,38,41,42,43,48,51,52,53,55,58,60],"\u4e00\u4ef6":[42,48],"\u4e00\u4efd":[1,26,27,34,35,41,43,48],"\u4e00\u4f4d":26,"\u4e00\u5171":[29,52],"\u4e00\u5207":29,"\u4e00\u523b":29,"\u4e00\u53f0":[0,2],"\u4e00\u5757":[9,14,30,43],"\u4e00\u5b9a":[0,9,23,27,28,30,36,39,43,47,48,52,55],"\u4e00\u5bf9":[14,49,59],"\u4e00\u5c0f":[9,29],"\u4e00\u5c42":34,"\u4e00\u5e76":[19,21,51],"\u4e00\u5f20":[30,51],"\u4e00\u6574":2,"\u4e00\u6574\u5957":2,"\u4e00\u65e6":[22,27,29,34,41,47,48,54],"\u4e00\u65f6":34,"\u4e00\u65f6\u95f4":34,"\u4e00\u6761":[9,13,14,15,20,23,29,33,35,53,55],"\u4e00\u6837":[2,9,13,15,19,27,28,29,36,41,42,52,53],"\u4e00\u6b21":[0,5,12,14,15,18,19,22,23,24,25,29,30,32,35,36,38,42,46,48,51,52,53],"\u4e00\u6b21\u6027":18,"\u4e00\u6b65":[0,5,9,12,13,18,20,25,27,32,33,35,38,39,41,43,46,48,51,52,55],"\u4e00\u6b65\u6b65":5,"\u4e00\u6bb5":[5,9,14,19,21,22,29,31,33,39,41,42,48,51,53,59],"\u4e00\u6bb5\u65f6\u95f4":[21,22,53],"\u4e00\u70b9":[27,29,35,41,47,52,53],"\u4e00\u76f4":[2,30],"\u4e00\u77ac":15,"\u4e00\u77ac\u95f4":15,"\u4e00\u79cd":[3,9,14,15,21,22,26,27,28,29,36,39,41,43,51,52,53,54,55,56],"\u4e00\u79d2":22,"\u4e00\u79d2\u949f":22,"\u4e00\u7ae0":[9,12,13,19,20,25,32,36,44],"\u4e00\u7aef":47,"\u4e00\u7b49":9,"\u4e00\u7c7b":[15,56],"\u4e00\u7cfb":[3,8,31,51,53,54],"\u4e00\u7cfb\u5217":[3,8,31,51,53,54],"\u4e00\u7ea7":41,"\u4e00\u7ec4":[29,52],"\u4e00\u7ef4":56,"\u4e00\u81f4":[9,14,19,20,25,26,48,51,52],"\u4e00\u81f4\u6027":51,"\u4e00\u822c":[0,5,7,8,12,14,15,18,22,25,28,29,32,36,38,46,51,52,53,54,55],"\u4e00\u8282":[7,9,28,35,41,54,55],"\u4e00\u884c":[6,7,13,14,15,29,44,59],"\u4e00\u8d77":[12,15,26,28,53,54,55,58],"\u4e00\u8d9f":25,"\u4e00\u8def":[0,5,12,18,25,32,38,51],"\u4e00\u8f6e":[34,47],"\u4e00\u904d":[9,36,52],"\u4e00\u90e8":[6,15,22,27,28,29,32,52,53],"\u4e00\u90e8\u5206":[6,15,22,27,28,29,32,52,53],"\u4e00\u95e8":1,"\u4e00\u95ee":30,"\u4e00\u9879":[6,28,47],"\u4e00\u9898":30,"\u4e07\u5e74":52,"\u4e07\u5e74\u524d":52,"\u4e09\u4e2a":[13,14,15,23,29,32,33,39,41,44,46,47,51,52,53,54,55,56],"\u4e09\u5143":11,"\u4e09\u5143\u7ec4":11,"\u4e09\u5c42":41,"\u4e09\u65b9":[0,6,9,15,36],"\u4e09\u6761":29,"\u4e09\u6b21":29,"\u4e09\u6b65":55,"\u4e09\u79cd":[41,51,53,55],"\u4e09\u7ae0":[21,22,27,28,29,32,52],"\u4e09\u7c7b":53,"\u4e09\u7ea7":27,"\u4e0a\u4e0b":[12,17,18,20,21,25,28,29,34,36,37,48,51,52],"\u4e0a\u4e0b\u6587":[12,17,18,20,21,25,28,29,34,37,48,51,52],"\u4e0a\u53d6":28,"\u4e0a\u56fe":26,"\u4e0a\u5c42":[6,41],"\u4e0a\u6587":0,"\u4e0a\u6b21":30,"\u4e0a\u7f51":0,"\u4e0a\u8282":9,"\u4e0a\u8ff0":[0,5,8,9,12,13,15,18,25,32,38,41,51,52,53,54,55],"\u4e0a\u90e8":[0,5,12,18,25,32,38,51],"\u4e0a\u9501":55,"\u4e0a\u9650":[39,41],"\u4e0a\u9762":[0,2,22,26,27,28,34,39,42,43,48,51,52,53,55],"\u4e0b\u4e2a":21,"\u4e0b\u5212":59,"\u4e0b\u5212\u7ebf":59,"\u4e0b\u5217":36,"\u4e0b\u5219":[28,41],"\u4e0b\u53bb":[27,55],"\u4e0b\u56fe":[28,48],"\u4e0b\u5c42":[6,42],"\u4e0b\u6587":[12,17,18,20,21,25,28,29,34,37,48,51,52],"\u4e0b\u6765":[0,5,6,8,15,26,27,28,29,35,41,42,43,52,53,55,59],"\u4e0b\u6807":[39,47],"\u4e0b\u6b21":[21,33,35,52],"\u4e0b\u7ea7":27,"\u4e0b\u8282":14,"\u4e0b\u8f7d":0,"\u4e0b\u8f7d\u901f\u5ea6":0,"\u4e0b\u8ff0":56,"\u4e0b\u964d":15,"\u4e0b\u9762":[0,2,5,12,18,20,25,27,28,29,32,33,34,35,38,40,41,42,46,47,51,52,53,54,55,59],"\u4e0d\u4e00":51,"\u4e0d\u4e00\u81f4\u6027":51,"\u4e0d\u4ec5":[3,27,28,34,39,41],"\u4e0d\u4ec5\u4ec5":[34,39],"\u4e0d\u4f1a":[14,15,20,21,22,26,27,28,29,30,34,35,36,41,42,46,47,48,51,52,53,54,56],"\u4e0d\u50cf":55,"\u4e0d\u518d":[21,25,27,28,29,34,38,41,47,52],"\u4e0d\u5230":[7,9,13,21,29,33,35,40,41,51,53],"\u4e0d\u53bb":22,"\u4e0d\u53d8":[21,26,44],"\u4e0d\u53ef":[15,22,41,53,54,55],"\u4e0d\u53ef\u5206\u5272":[15,54],"\u4e0d\u53ef\u907f\u514d":22,"\u4e0d\u540c":[2,7,8,14,15,19,20,22,25,27,28,29,30,33,35,38,39,40,41,44,47,51,52,53,54,55],"\u4e0d\u540c\u4e4b\u5904":[14,27,41],"\u4e0d\u591f":[34,51,55,58],"\u4e0d\u592a":48,"\u4e0d\u592a\u53ef\u80fd":[29,53],"\u4e0d\u592a\u597d":48,"\u4e0d\u597d":23,"\u4e0d\u5b66":36,"\u4e0d\u5b66\u5219":36,"\u4e0d\u5f53":55,"\u4e0d\u5f97":29,"\u4e0d\u5f97\u4e0d":29,"\u4e0d\u5fc5":[5,28,33,36,42,52,58],"\u4e0d\u5fc5\u8981":[42,58],"\u4e0d\u65ad":[13,21,33,51],"\u4e0d\u662f":[0,2,5,9,12,13,14,15,18,20,23,25,26,27,28,29,32,35,36,38,39,42,46,47,51,52,53],"\u4e0d\u6b62":6,"\u4e0d\u7136":[9,27,41,42,60],"\u4e0d\u7136\u7684\u8bdd":41,"\u4e0d\u723d":59,"\u4e0d\u7528":[0,5,12,13,15,18,25,32,38,42,46,51,52],"\u4e0d\u786e\u5b9a\u6027":[51,53],"\u4e0d\u7ba1":15,"\u4e0d\u80fd":[15,26,27,28,29,36,41,42,47,51,53,54,55],"\u4e0d\u81f3\u4e8e":36,"\u4e0d\u826f":15,"\u4e0d\u826f\u5f71\u54cd":15,"\u4e0d\u8981":[0,10,16,23,28,30,36,44,56],"\u4e0d\u8db3":[28,30,36,51],"\u4e0d\u8fc7":30,"\u4e0d\u9519":36,"\u4e0e\u5176":[27,36],"\u4e0e\u6b64":[53,54],"\u4e0e\u6b64\u76f8\u5173":[53,54],"\u4e13\u6709":52,"\u4e13\u7528":27,"\u4e13\u95e8":[15,28],"\u4e14\u4f1a":7,"\u4e16\u754c":5,"\u4e1a\u751f":5,"\u4e22\u5f03":28,"\u4e24\u4e2a":[9,13,14,15,18,19,20,23,26,27,28,29,30,33,34,35,36,39,40,41,42,43,44,46,47,48,51,52,53,54,55,56],"\u4e24\u4ef6":35,"\u4e24\u53ea":51,"\u4e24\u5c42":[6,41],"\u4e24\u6761":[8,15,29,53],"\u4e24\u6b21":[20,41,53],"\u4e24\u7248":29,"\u4e24\u79cd":[2,8,23,28,29,35,38,41,47,53,54,56],"\u4e24\u7aef":27,"\u4e24\u7c7b":41,"\u4e24\u8005":55,"\u4e24\u904d":29,"\u4e24\u9879":29,"\u4e25\u683c":[34,36],"\u4e25\u683c\u63a7\u5236":34,"\u4e25\u91cd":29,"\u4e2a\u4eba":0,"\u4e2a\u4f53":15,"\u4e2a\u5757":[41,42],"\u4e2a\u5b57\u7b26":[47,52],"\u4e2a\u6570":48,"\u4e2a\u6cdb":27,"\u4e2d\u4e3a":[15,28],"\u4e2d\u4ee5":[29,41],"\u4e2d\u5199":44,"\u4e2d\u5219":29,"\u4e2d\u539f":28,"\u4e2d\u540c":22,"\u4e2d\u540e":[15,41],"\u4e2d\u56fd":0,"\u4e2d\u5c06":[33,41,46],"\u4e2d\u5e94":27,"\u4e2d\u5f39":[54,55],"\u4e2d\u6587":[28,60],"\u4e2d\u6587\u7248":28,"\u4e2d\u65ad":[15,18,24,30,41,51,52,53],"\u4e2d\u6709":41,"\u4e2d\u672a":51,"\u4e2d\u67e5":[26,29,41],"\u4e2d\u6808":15,"\u4e2d\u786c":42,"\u4e2d\u79d1":0,"\u4e2d\u79d1\u5927":0,"\u4e2d\u7ebf":52,"\u4e2d\u8981":34,"\u4e2d\u95f4":[15,27,28,35,41,48,51],"\u4e2d\u95f4\u72b6\u6001":[15,51],"\u4e2d\u95f4\u7ea7":27,"\u4e34\u65f6":[15,27],"\u4e34\u754c":[34,51,53,54],"\u4e3a\u4e3b":5,"\u4e3a\u4e86":[5,6,14,15,21,22,26,27,28,29,30,32,34,35,36,41,42,43,44,48,52,53,55,58],"\u4e3a\u4ec0\u4e48":[8,29,35,36,51,56,57],"\u4e3a\u4f55":[15,23,26,29],"\u4e3a\u4f8b":[0,15,21,26,29,41],"\u4e3a\u540c":29,"\u4e3a\u5565":36,"\u4e3a\u5b50":35,"\u4e3a\u5f31":13,"\u4e3a\u5f3a":47,"\u4e3a\u672c":5,"\u4e3a\u6808":9,"\u4e3a\u6b62":[35,51,53,55],"\u4e3a\u6b64":[0,18,19,27,29,32,33,34,42,56],"\u4e3a\u7a7a":[26,47],"\u4e3a\u952e":28,"\u4e3b\u4f53":[28,34,41,42,48,52],"\u4e3b\u52a8":[18,20,21,22,34,35,52],"\u4e3b\u6253":52,"\u4e3b\u7ebf":[51,52,54,55,56],"\u4e3b\u8981":[0,15,18,29,30,34,39,41,42,43,48,52,53],"\u4e3b\u9875":60,"\u4e3b\u9898":[59,60],"\u4e3e\u4e2a":51,"\u4e3e\u51fa":49,"\u4e4b\u4e0a":52,"\u4e4b\u4e0b":3,"\u4e4b\u4e3a":2,"\u4e4b\u5185":[22,29,41],"\u4e4b\u524d":[2,7,9,12,15,21,23,26,27,28,29,34,35,36,39,40,41,42,43,47,48,51,52,54,55,56],"\u4e4b\u540e":[0,15,20,21,22,23,26,27,28,29,32,33,34,35,38,40,41,42,43,46,47,48,51,52,54,55,58,60],"\u4e4b\u5904":[14,27,41],"\u4e4b\u590f":3,"\u4e4b\u5916":[2,5,6,28,29,41,47],"\u4e4b\u95f4":[6,13,14,20,26,28,29,39,41,46,47,52,53,54,59],"\u4e5f\u5c31\u662f\u8bf4":[15,22,23,26,27,29,30],"\u4e5f\u8bb8":42,"\u4e60\u60ef":0,"\u4e60\u9898":44,"\u4e86\u89e3":[2,13,43,52,55],"\u4e8b\u4ef6":[21,51],"\u4e8b\u52a1":[51,52,55],"\u4e8b\u5b9e":[15,20,28,29,36,42,59],"\u4e8b\u5b9e\u4e0a":[15,20,28,29,36,42,59],"\u4e8b\u60c5":[15,35,42,48,52],"\u4e8c\u4e2a":[12,25,29,41],"\u4e8c\u503c":54,"\u4e8c\u5143":41,"\u4e8c\u6761":9,"\u4e8c\u6b21":21,"\u4e8c\u6b65":[0,5,12,18,25,32,38,51],"\u4e8c\u7ae0":[8,13,19,21,29,39],"\u4e8c\u7ea7":41,"\u4e8c\u8005":[22,29,35,52,56],"\u4e8c\u8005\u4e4b\u95f4":52,"\u4e8c\u8282":[8,28],"\u4e8c\u8fdb\u5236":[3,7,9,17,28,29,39],"\u4e8c\u8fdb\u5236\u7801":17,"\u4e8e\u662f":[13,15,21,27,29,39,42,47,48,55],"\u4e8f\u5927":30,"\u4e92\u4e0d":34,"\u4e92\u65a5":[29,34,41,42,43,53,54,55,57],"\u4e92\u65a5\u6027":53,"\u4e92\u76f8":[26,49,51],"\u4e92\u8054":0,"\u4e92\u8054\u7f51":0,"\u4e94\u4e2a":[41,51],"\u4e94\u7ae0":[39,52],"\u4ea4\u4e92":[32,51],"\u4ea4\u51fa":[20,21,33,34,35,52],"\u4ea4\u53c9":[7,53],"\u4ea4\u6362":[15,29,39],"\u4ea4\u66ff":[18,51,53],"\u4ea4\u7ed9":28,"\u4ea4\u8fd8":52,"\u4ea4\u8fd8\u7ed9":52,"\u4ea4\u9519":53,"\u4ea4\u96c6":[26,28],"\u4ea7\u7269":12,"\u4ea7\u751f":[9,28,29,51,55],"\u4eab\u53d7":0,"\u4eba\u673a":51,"\u4eba\u673a\u4ea4\u4e92":51,"\u4ec0\u4e48":[2,6,7,8,9,23,29,35,36,44,51,55,56,57],"\u4ec5\u4e3a":30,"\u4ec5\u4ec5":[0,5,6,12,18,21,25,32,34,38,39,47,51],"\u4ec5\u4f9b":51,"\u4ec5\u4f9b\u53c2\u8003":51,"\u4ec5\u5269":47,"\u4ec5\u5f53":26,"\u4ecb\u5165":52,"\u4ecb\u7ecd":[0,1,8,12,13,14,21,22,27,28,29,34,35,40,41,42,52,53,54,55,58],"\u4ecd\u7136":[15,28,42],"\u4ecd\u80fd":[22,28,29],"\u4ece\u4e0b":7,"\u4ece\u4e2d":[2,42,52,53],"\u4ece\u5757":[41,43],"\u4ece\u5916\u5230\u91cc":39,"\u4ece\u5c0f":[41,47],"\u4ece\u5c0f\u5230\u5927":[41,47],"\u4ece\u6587\u4ef6":[2,33,47],"\u4ece\u672a":[15,27],"\u4ece\u6839":42,"\u4ece\u7236":[35,47],"\u4ece\u800c":[27,28,34,39,41,43,47,48,51,52,53,54,55],"\u4ece\u961f":34,"\u4ece\u96f6\u5f00\u59cb":[5,58],"\u4ed3\u4fc3":58,"\u4ed3\u5e93":[0,5,12,18,25,32,38,46,51,60],"\u4ed4\u7ec6":55,"\u4ed6\u4eec":[28,42,51],"\u4ed8\u8d39":0,"\u4ee3\u66ff":55,"\u4ee3\u7406":0,"\u4ee3\u7801":[0,2,3,6,7,8,9,10,11,13,14,15,16,17,18,20,22,23,25,26,27,28,29,30,32,35,36,38,39,41,42,43,44,46,52,53,54,55,56,57,58,59],"\u4ee3\u7801\u6267\u884c":52,"\u4ee3\u7801\u6bb5":[2,9,13,14,26,28,29,30,52],"\u4ee3\u7801\u751f\u6210":29,"\u4ee3\u7801\u8fd0\u884c":6,"\u4ee3\u8868":[7,13,23,27,28,39,41,47,48,51,52,56],"\u4ee5\u4e0a":[0,27,28,36],"\u4ee5\u4e0b":[0,8,15,27,34,35,40,41,56,58],"\u4ee5\u4e0b\u51e0\u70b9":35,"\u4ee5\u4f9b":27,"\u4ee5\u4fbf":3,"\u4ee5\u503c":26,"\u4ee5\u5185":[10,16,23,30,36,44,56],"\u4ee5\u524d":52,"\u4ee5\u53ca":[0,2,10,13,14,15,16,21,23,25,26,27,28,29,30,34,35,36,42,44,49,51,52,53,54,56],"\u4ee5\u540e":[0,5,12,18,25,27,32,38,46,51,52],"\u4ee5\u5757":41,"\u4ee5\u5907":28,"\u4ee5\u7eaf":28,"\u4ee5\u9875":[26,28],"\u4ef6\u5939":23,"\u4efb\u4e00":55,"\u4efb\u4f55":[0,2,6,7,13,27,29,35,39,40,41,42,47],"\u4efb\u52a1":[2,12,18,25,28,31,32,35,37,38,47,51,52],"\u4efb\u52a1\u8c03\u5ea6":[22,37],"\u4efb\u610f":[28,33,54],"\u4eff\u7167":[42,44],"\u4f11\u7720":55,"\u4f18\u5148":36,"\u4f18\u5148\u6743":36,"\u4f18\u5148\u7ea7":36,"\u4f18\u52bf":30,"\u4f18\u5316":[28,58],"\u4f18\u70b9":29,"\u4f18\u96c5":9,"\u4f1a\u4ee5":29,"\u4f1a\u5148":[20,28,54,55],"\u4f1a\u5230":29,"\u4f1a\u5e2e":20,"\u4f1a\u5fd9":55,"\u4f20\u5165":[13,15,20,21,27,28,29,35,41,42,47,48],"\u4f20\u53c2":26,"\u4f20\u7ed9":[2,15,21,27,41,42],"\u4f20\u7edf":[0,30],"\u4f20\u8f93":46,"\u4f20\u8fc7":15,"\u4f20\u8fc7\u6765":15,"\u4f20\u9012":[13,29,35,42,46,48],"\u4f2a\u6307\u4ee4":34,"\u4f30\u7b97":30,"\u4f46\u4f1a":13,"\u4f46\u662f":[14,15,21,23,26,27,28,29,30,35,36,39,42,47,48,51,55],"\u4f46\u672c":0,"\u4f4d\u4e3a":26,"\u4f4d\u4e8e":[2,9,15,28,29,30,35,36,41,42,52],"\u4f4d\u4ec5":28,"\u4f4d\u56fe":[41,42],"\u4f4d\u5747":28,"\u4f4d\u624d":43,"\u4f4d\u662f":26,"\u4f4d\u6709\u4f55":30,"\u4f4d\u7f6e":[0,2,7,9,14,15,20,25,27,28,29,30,34,35,39,41,42,43,47,48,52,53,55,56],"\u4f4d\u9875":26,"\u4f4e\u4e0b":55,"\u4f4e\u4e8e":34,"\u4f4e\u5904":48,"\u4f53\u4e2d":47,"\u4f53\u4f1a":51,"\u4f53\u4f4d":42,"\u4f53\u5185":41,"\u4f53\u73b0":[15,35,41,51],"\u4f53\u79ef":[39,42],"\u4f53\u9a8c":[11,17,24,31,37,45,50,57],"\u4f55\u610f":23,"\u4f55\u65f6":[15,28,30,51],"\u4f5c\u4e1a":[24,31,37,45,46,50,57,58],"\u4f5c\u4e3a":[2,3,6,7,9,12,13,14,15,21,22,27,28,29,34,35,39,41,42,43,48,55],"\u4f5c\u5dee":41,"\u4f5c\u7528":[23,28,29,30,41,42,44,51],"\u4f5c\u7528\u57df":41,"\u4f5c\u8005":6,"\u4f5c\u8fc7":54,"\u4f7f\u4e4b\u80fd":46,"\u4f7f\u5f97":[9,14,15,22,27,28,29,35,39,42,52],"\u4f7f\u7528":[0,3,4,5,7,9,12,13,14,15,18,19,20,21,22,23,25,26,27,28,29,30,32,34,35,36,38,39,40,41,42,44,45,48,49,50,51,52,56,57,60],"\u4f7f\u7528\u4e0d\u5f53":55,"\u4f7f\u7528\u66b4\u529b":36,"\u4f7f\u7528\u6743":[20,21,34,35,52],"\u4f7f\u7528\u8005":[38,41,42],"\u4f7f\u80fd":[15,29],"\u4f8b\u5916":15,"\u4f8b\u5982":[0,29,36,42,43,46],"\u4f8b\u5b50":[2,49,51,53,54,55,59],"\u4f8b\u6765":36,"\u4f9b\u53c2\u8003":51,"\u4f9d\u636e":9,"\u4f9d\u6b21":[9,21,28,29,42],"\u4f9d\u7136":15,"\u4f9d\u8d56":[0,2,5,6,9,11,13,14,27,38,44,51,56],"\u4f9d\u8d56\u4e8e":[2,5,6,14,27],"\u4f9d\u9760":23,"\u4fbf\u4e8e":5,"\u4fbf\u4f1a":[28,47],"\u4fbf\u662f":29,"\u4fd7\u79f0":32,"\u4fdd\u5b58":[2,6,12,13,17,18,20,21,22,25,27,28,29,30,33,34,35,38,41,42,46,47,48,51,52,53],"\u4fdd\u62a4":[12,28],"\u4fdd\u6301":[20,21,52],"\u4fdd\u6301\u4e00\u81f4":20,"\u4fdd\u7559":[2,25,28,41,42,47,58],"\u4fdd\u8bc1":[14,20,26,28,35,40,41,42,43,46,48,51,52,53,54,55],"\u4fdd\u969c":53,"\u4fdd\u969c\u673a\u5236":53,"\u4fe1\u53f7":[51,55,57],"\u4fe1\u53f7\u91cf":[51,55,57],"\u4fe1\u606f":[2,3,5,6,7,8,9,15,18,21,24,28,29,30,33,34,35,39,42,52],"\u4fe1\u9053":30,"\u4fee\u590d":29,"\u4fee\u6539":[0,8,9,11,12,14,15,18,21,23,25,26,27,28,29,32,35,36,38,40,41,42,44,46,48,51,52,54,58],"\u4fee\u957f":52,"\u501f\u52a9":[13,14,28,29,33,34,40,44,52,55],"\u501f\u7528":[14,21,27,28,41],"\u503c\u52a0":54,"\u503c\u5f97":1,"\u503c\u8bfb":15,"\u5047\u5982":29,"\u5047\u5b9a":42,"\u5047\u8bbe":[15,30,36,43,48,51],"\u504f\u79fb":[2,26,29,41,42,43],"\u504f\u79fb\u91cf":[29,41,43],"\u505a\u51fa":[21,28],"\u505a\u5230":[5,29,35,53],"\u505a\u6876":23,"\u505a\u6cd5":[0,2,29,30,52,54],"\u50a8\u5b58":36,"\u50cf\u662f":2,"\u50f5\u5c38":[33,35],"\u5141\u8bb8":[22,26,28,30,34,39,40,43,47,54,55,56],"\u5143\u7d20":[14,27,28,48,53,56],"\u5143\u7ec4":11,"\u5144\u5f1f":52,"\u5145\u5206":41,"\u5145\u5206\u5229\u7528":41,"\u5148\u524d":39,"\u5148\u540e":30,"\u5148\u67e5":41,"\u514b\u9686":[0,5,12,18,25,32,38,46,51],"\u514d\u8d39":0,"\u5165\u5148\u51fa":27,"\u5165\u53e3":[2,7,8,9,12,13,15,21,28,29,35,48,52,53],"\u5165\u95e8":1,"\u5168\u5c40":[9,14,15,18,21,27,29,34,38,43,45,51,52,53,55],"\u5168\u5c40\u53d8\u91cf":[9,14,15,51,53,55],"\u5168\u65b0":35,"\u5168\u7a0b":[0,15,29,42,43],"\u5168\u8eab":52,"\u5168\u90e8":[14,15,29,33,35,36,39,40,41,43,51,52,53,56],"\u5168\u90e8\u5185\u5bb9":41,"\u5168\u96f6":26,"\u5168\u9762":59,"\u516c\u5e73":[22,36,53],"\u516c\u5e73\u6027":[36,53],"\u516c\u5f00":27,"\u516c\u5f0f":59,"\u516d\u4e2a":36,"\u516d\u7ae0":42,"\u5171\u4eab":[19,29,34,39,41,51,52,53,54,55],"\u5171\u4eab\u8d44\u6e90":[51,52,53,54,55],"\u5171\u540c":[29,54],"\u5171\u5b58":40,"\u5171\u7528":[30,51],"\u5173\u4e8e":[21,29,51],"\u5173\u5fc3":[12,13,35,39,42],"\u5173\u6389":48,"\u5173\u673a":[5,11],"\u5173\u6ce8":[13,53],"\u5173\u7cfb":[2,9,28,29,31,32,34,35,36,51,52,54,55],"\u5173\u8054":31,"\u5173\u952e":[8,21,29,35,39,41,51,55],"\u5173\u952e\u5728\u4e8e":29,"\u5173\u952e\u5b57":[21,39,41],"\u5173\u952e\u70b9":51,"\u5173\u952e\u95ee\u9898":55,"\u5173\u95ed":[0,39,40,48,50,56],"\u5174\u8da3":[9,15,28,34,36,39,41,43,47],"\u5176\u4e00":30,"\u5176\u4e2d":[2,6,8,13,15,19,21,26,27,28,29,30,34,35,36,39,40,41,42,47,51,54],"\u5176\u4ed6":[0,13,15,18,21,23,25,27,28,29,30,33,34,35,36,38,41,42,43,47,51,52,53,54,55,56,58,59],"\u5176\u4f59":[13,30,41,42],"\u5176\u503c":[54,56],"\u5176\u5b83":[3,9,15],"\u5176\u5b9e":[13,15,30,36,39,42,48,52,54,55],"\u5176\u5b9e\u8d28":15,"\u5176\u5f3a":41,"\u5176\u6b21":[29,33,34,55],"\u5176\u7236":[35,52],"\u5176\u88f8":34,"\u5176\u8bfb":47,"\u5177\u4f53":[2,9,12,14,15,23,29,34,35,39,40,41,42,44,48,51,52,53,54,55],"\u5177\u4f53\u4f4d\u7f6e":42,"\u5177\u4f53\u5185\u5bb9":41,"\u5177\u4f53\u6765\u8bf4":[29,34,44],"\u5177\u6709":[28,39,51,53],"\u5178\u578b":[21,35,55],"\u517c\u5bb9":[36,40,43,44,56],"\u517c\u5bb9\u6027":[40,44,56],"\u5185\u4e2d":52,"\u5185\u4f1a":29,"\u5185\u542b":29,"\u5185\u5b58":[2,5,8,11,14,15,17,18,19,25,27,29,30,31,34,35,36,38,39,41,42,43,47,52],"\u5185\u5b58\u4e0d\u8db3":[30,36],"\u5185\u5b58\u5730\u5740":43,"\u5185\u5b58\u7a7a\u95f4":[19,28,30,42],"\u5185\u5bb9":[0,2,7,8,9,14,15,21,23,25,26,27,28,29,32,33,34,36,38,39,40,41,42,43,44,48,51,52,56,59],"\u5185\u5c42":47,"\u5185\u5d4c":13,"\u5185\u6838":[0,3,5,6,9,12,13,17,18,19,20,21,22,23,25,26,30,31,32,33,35,36,37,38,39,40,41,42,44,45,47,48,51,54,55,57,58],"\u5185\u7f6e":0,"\u5185\u90e8":[14,27,34,41,42,51,52,59],"\u518d\u4e5f":35,"\u518d\u4e5f\u4e0d\u4f1a":35,"\u518d\u6b21":[9,15,21,22,29,52,53,55],"\u518d\u6b21\u53d1\u751f":22,"\u518d\u7528":8,"\u5199\u5165":[13,15,28,29,35,39,40,41,42,47,51],"\u5199\u51fa":[53,55],"\u5199\u56de":[41,42],"\u5199\u7aef":47,"\u51b2\u7a81":[0,28,41,58],"\u51b3\u5b9a":[28,34,41],"\u51c6\u5907":[6,13,15,21,29,42],"\u51cf\u5c0f":52,"\u51cf\u5c11":[2,18,36,47],"\u51e0\u4e2a":[0,28],"\u51e0\u4e4e":[23,34,35,48,52],"\u51e0\u70b9":35,"\u51e0\u79cd":[21,40,42],"\u51e0\u884c":[0,5,6,23],"\u51fa\u4e8e":[15,28,41,56],"\u51fa\u53bb":[20,22,27,34,41,48],"\u51fa\u6765":[15,26,27,33,34,38,40,41,48,52],"\u51fa\u73b0":[0,12,13,15,19,22,27,28,33,34,40,41,44,47,48,51,52,53,55,56,58],"\u51fa\u9519":[7,9,12,14,15,23,33,34,35,47,55],"\u51fd\u6570":[2,5,11,12,13,14,15,18,19,20,21,22,23,26,27,28,29,30,33,34,35,36,39,41,42,43,44,47,48,52,53,54,55,58],"\u51fd\u6570\u6307\u9488":52,"\u51fd\u6570\u8c03\u7528":[8,9,15,47,52],"\u5206\u4e3a":[0,19,22,29,41,51,53],"\u5206\u5229":41,"\u5206\u522b":[2,9,14,15,18,20,23,26,28,29,39,41,42,43,47,48,51,56],"\u5206\u5272":[15,50,54],"\u5206\u53d1":[12,17,32,41],"\u5206\u5e03":[26,59],"\u5206\u6210":[14,26,41],"\u5206\u62c6":52,"\u5206\u652f":[22,33,36,48],"\u5206\u65f6":[31,52],"\u5206\u6790":[8,9,11,51,52,54,56],"\u5206\u6d3e":[51,53],"\u5206\u79bb":[21,29,34,41],"\u5206\u79bb\u51fa\u6765":41,"\u5206\u8bcd":60,"\u5206\u914d":[15,22,25,28,29,30,31,34,35,36,41,42,47,48,51,52,56],"\u5206\u914d\u5668":[25,27,29,34,36,48],"\u5206\u9694":34,"\u5206\u9694\u7b26":34,"\u5206\u9875":[26,27,29],"\u5207\u6362":[0,13,17,18,19,21,22,23,24,28,29,34,35,47,51,53,57],"\u5207\u7247":[28,29,33,35,39,41],"\u5212\u7ebf":59,"\u5217\u4e3a":47,"\u5217\u4e3e":[30,45],"\u5217\u51fa":52,"\u5217\u8868":[32,35,41,52,54,55],"\u521a\u521a":[15,35,39,48,52],"\u521b\u5efa":[6,7,21,27,28,31,32,33,34,37,38,39,40,41,43,44,45,50,51,53,54,55,57,60],"\u521d\u503c":[54,55,56],"\u521d\u59cb":[7,9,11,12,13,14,15,19,21,25,27,28,29,32,34,36,37,38,39,41,42,44,45,48,51,52,54,56],"\u521d\u59cb\u503c":[14,54],"\u521d\u59cb\u5316":[7,9,11,12,13,14,15,19,21,25,27,28,29,32,33,34,35,38,41,42,45,48,51,52],"\u521d\u6b65":0,"\u521d\u7ea7":32,"\u521d\u7ea7\u9636\u6bb5":32,"\u5220\u9664":[2,7,13,27,28,34,42,44],"\u5224\u5b9a":13,"\u5224\u65ad":[26,28,35,41,47],"\u5229\u7528":[2,9,12,29,33,35,36,41,52,56],"\u5230\u7236":35,"\u5230\u8fbe":15,"\u5230\u961f":41,"\u5236\u4f5c":3,"\u5236\u7ea6":51,"\u5237\u65b0":[14,29],"\u524d\u4e3a":[51,53,55],"\u524d\u4efb":[21,23,35],"\u524d\u540e":[15,18,20,26,28,29,54,59],"\u524d\u5411":[36,44],"\u524d\u8005":[9,19,28,29,33,34],"\u524d\u9762":[27,28,34,35],"\u5265\u8327":5,"\u5269\u4e0b":[35,41,42],"\u5269\u4f59":[27,42,52,53],"\u526f\u672c":[26,41],"\u529b\u4e0d\u4ece\u5fc3":54,"\u529b\u91cf":52,"\u529e\u6cd5":[2,36,58],"\u529f\u80fd":[0,5,6,7,8,10,11,12,13,14,15,16,18,20,21,22,23,25,27,30,32,33,34,36,38,40,41,42,44,46,47,48,51,52,54,56,58,59],"\u52a0\u4e00":27,"\u52a0\u4e0a":[2,7,15,28,29,36,41,43,52,55],"\u52a0\u5165":[0,7,9,23,25,28,29,33,34,35,36,38,39,41,44,48,52,55,56],"\u52a0\u5927":29,"\u52a0\u7535":9,"\u52a0\u7c97":59,"\u52a0\u8f7d":[2,8,9,12,13,15,17,18,24,28,30,31,32,33,35,36,37,38,42,44,45],"\u52a0\u901f":[0,41],"\u52a0\u9501":42,"\u52a1\u5668":0,"\u52a1\u5fc5":0,"\u52a8\u4f5c":33,"\u52a8\u6001":[25,27,29,36,39,52,56],"\u52a8\u6001\u5185\u5b58":[25,29,36],"\u52a8\u6001\u521b\u5efa":52,"\u52a9\u6559":[23,60],"\u52aa\u529b":58,"\u52c9\u5f3a":8,"\u5305\u4f1a":41,"\u5305\u542b":[0,2,5,6,9,12,15,20,23,28,29,34,35,39,41,42,43,47,48,51,55,57],"\u5305\u62ec":[2,8,13,21,23,27,34,35,41,42,51,52,58],"\u5305\u88c5":[8,12,13,15,26,27,34,39,40,47],"\u5305\u88f9":[13,14,15,21,27,29,34,39,59],"\u5316\u4e3a":[34,38,41,43],"\u5316\u5728":15,"\u5316\u6210":[15,27],"\u532e\u4e4f":12,"\u5339\u914d":[29,48],"\u533a\u4e2d":53,"\u533a\u4e3a":54,"\u533a\u5206":[27,39],"\u533a\u522b":[30,32,42,56],"\u533a\u57df":[2,14,28,29,34,41,42,48],"\u533a\u65f6":51,"\u533a\u95f4":[15,27,28,30,35,41,42,43,48],"\u533f\u540d":31,"\u5341\u4e09":28,"\u5341\u516d":56,"\u5341\u516d\u8fdb\u5236":56,"\u5341\u5206":36,"\u5347\u7ea7":[47,52],"\u5347\u7ea7\u7248":52,"\u534a\u5e74":0,"\u534f\u540c":15,"\u534f\u7a0b":51,"\u534f\u8c03":54,"\u5355\u4e00":[51,52],"\u5355\u4e2a":[13,26,28,34],"\u5355\u4f4d":[21,22,23,26,27,28,34,41,51,52],"\u5355\u5143":28,"\u5355\u5411":47,"\u5355\u6838":[14,34,41],"\u5355\u6b21":[29,40],"\u5355\u72ec":[19,29,35,36,52],"\u5360\u636e":[29,33,42],"\u5360\u7528":[15,29,30,35,39,47,52,53,54],"\u5361\u7247":[28,39,41,58],"\u5373\u4f1a":35,"\u5373\u4f7f":[14,29,39],"\u5373\u521a":29,"\u5373\u53ef":[0,2,5,9,12,13,15,18,21,22,23,25,26,27,28,29,30,32,35,36,38,39,40,41,42,44,47,48,51,56,60],"\u5373\u5c06":[2,20,27,28,29],"\u5373\u7236":47,"\u5373\u9501":53,"\u5374\u662f":29,"\u5382\u5546":6,"\u538b\u5165":[15,21,27,29,34,35,46,50],"\u538b\u529b":29,"\u539f\u5148":[29,51],"\u539f\u56e0":[6,7,15,28,29,30,34,40,47,48,53],"\u539f\u5730":7,"\u539f\u578b":[15,50,52],"\u539f\u59cb":55,"\u539f\u5b50":[15,51,53,54],"\u539f\u6709":[35,44],"\u539f\u6765":[7,30,32,35,48],"\u539f\u6837":15,"\u539f\u7406":[26,35,36,41,47,48],"\u539f\u8bed":[51,55],"\u53bb\u6389":[42,44],"\u53c2\u6570":[2,9,13,14,15,20,21,23,27,29,30,33,34,35,36,40,41,42,43,44,46,47,50,52,54,56],"\u53c2\u6570\u8bbe\u7f6e":13,"\u53c2\u8003":[3,7,21,23,24,28,29,30,31,36,37,39,44,45,47,48,50,56,57,58,60],"\u53c2\u89c1":[0,27],"\u53c2\u9605":48,"\u53ca\u5176":[5,8,21,23,29,38,41],"\u53ca\u65f6":[28,29,51,55],"\u53cb\u597d":28,"\u53cc\u5411":[46,47],"\u53cc\u5411\u901a\u4fe1":[46,47],"\u53cc\u7aef":34,"\u53cd\u6c47\u7f16":[2,7,8],"\u53cd\u8f6c":36,"\u53d1\u4fe1":55,"\u53d1\u4fe1\u53f7":55,"\u53d1\u51fa":[8,52,55],"\u53d1\u51fa\u4fe1\u53f7":55,"\u53d1\u6325":28,"\u53d1\u6325\u4f5c\u7528":28,"\u53d1\u73b0":[0,9,15,21,27,28,35,55,56],"\u53d1\u751f":[0,5,6,9,12,14,15,18,22,23,25,26,29,30,32,33,34,35,36,38,41,44,46,48,51,52,53,54,56],"\u53d1\u751f\u53d8\u5316":[14,15,33,34,35,46,48,52],"\u53d1\u884c":0,"\u53d1\u884c\u7248":0,"\u53d1\u8d77":[3,15],"\u53d1\u9001":55,"\u53d6\u4ee3":52,"\u53d6\u503c":[41,54],"\u53d6\u51b3":[28,35,41,51,53],"\u53d6\u51b3\u4e8e":[28,35,41,51,53],"\u53d6\u51fa":[15,26,27,28,29,34,35,41,42,47,48],"\u53d6\u5f97":[22,29,35,53],"\u53d6\u6307":[14,26,28,29],"\u53d6\u6574":[26,27,28,30,41,42],"\u53d6\u6d88":[30,44],"\u53d6\u9501":53,"\u53d7\u5230":53,"\u53d8\u4e3a":[33,34,35,47],"\u53d8\u52a8":56,"\u53d8\u5316":[0,5,12,14,15,18,21,25,27,29,32,33,34,35,38,46,48,51,52],"\u53d8\u5f97":[28,29,39,58],"\u53d8\u6027":[14,41],"\u53d8\u6210":[28,55],"\u53d8\u91cf":[0,9,13,14,15,21,27,28,34,35,41,42,51,52,53,54,57],"\u53e6\u5916":[0,2,22,28,36,52,53,54,55],"\u53ea\u4e0d\u8fc7":30,"\u53ea\u4f1a":[15,29,42,47],"\u53ea\u662f":[2,9,15,26,27,28,29,35,39,41,48,52],"\u53ea\u6709":[14,21,26,28,29,35,41,42,43,47,48,51,52,53,54,55],"\u53ea\u80fd":[2,15,19,22,27,28,29,35,41,42,48,51,55],"\u53ea\u8981":[0,27,39],"\u53ea\u8bfb":[9,14,28,34,39,40,43,47,51],"\u53ea\u9760":53,"\u53ef\u4ee5":[0,2,3,5,6,7,8,9,12,13,15,18,19,20,21,22,23,25,26,27,28,29,30,32,33,34,35,36,38,39,40,41,42,43,44,46,47,48,51,52,53,54,55,56,58,59,60],"\u53ef\u5206":[15,54],"\u53ef\u53d8":[14,21,23,27,28,29,34,35,41],"\u53ef\u53d8\u6027":[14,41],"\u53ef\u5426":47,"\u53ef\u6267\u884c\u6587\u4ef6":[6,7,8,9,13,28,33,34,35,38],"\u53ef\u6267\u884c\u7a0b\u5e8f":9,"\u53ef\u7528":[28,29,31,32,34,43,46,48,53,56],"\u53ef\u7528\u5185\u5b58":27,"\u53ef\u77e5":[20,29,43],"\u53ef\u80fd":[0,3,15,19,22,23,26,27,28,29,30,34,35,36,39,40,41,42,44,47,48,51,52,53,54,55,56,58,60],"\u53ef\u80fd\u6027":[41,55],"\u53ef\u884c":0,"\u53ef\u89c1":[7,34,41],"\u53ef\u8bfb":40,"\u53ef\u9009":28,"\u53ef\u9009\u5730":28,"\u53f3\u4fa7":28,"\u53f3\u5f00":27,"\u53f3\u79fb":26,"\u53f3\u7aef":41,"\u5404\u4e2a":[2,12,13,14,21,28,34,41,42,52,53],"\u5404\u4e3a":41,"\u5404\u5b57":27,"\u5404\u662f":[21,29],"\u5404\u79cd":[2,28,52,53],"\u5404\u7c7b":[34,56],"\u5404\u7ea7":6,"\u5404\u81ea":[14,19,25,26,29,34,52],"\u5404\u884c":53,"\u5408\u5728":28,"\u5408\u5728\u4e00\u8d77":28,"\u5408\u5e76":[41,56],"\u5408\u6cd5":[7,8,26,27,28,36,41,43,47,48,56],"\u5408\u6cd5\u6027":[27,41,48],"\u5408\u7406":[8,30,42],"\u5408\u9002":[0,2,13,28],"\u540c\u4e00":[19,28,29,34,39,44,47,51,52],"\u540c\u4e00\u4e2a":[19,28,29,39,44,47,51],"\u540c\u4e00\u65f6":34,"\u540c\u4e00\u65f6\u95f4":34,"\u540c\u4e2a":43,"\u540c\u4e8e":41,"\u540c\u540d":[13,42,44,48],"\u540c\u5b66":[0,9,23,36,43,52,55,56],"\u540c\u65f6":[9,12,13,14,19,22,23,27,28,29,33,34,35,41,42,43,47,48,51,53,55],"\u540c\u6837":[0,15,28,29,34,43,46,47,52,54],"\u540c\u6b65":[29,41,42,53,54,55,57],"\u540c\u6b65\u64cd\u4f5c":54,"\u540c\u7406":29,"\u540d\u4e3a":[7,13,14,21,34,42],"\u540d\u5b57":[9,13,33,34,41,42,48,59],"\u540d\u79f0":44,"\u540d\u8bcd":30,"\u540e\u4f1a":28,"\u540e\u53f0":3,"\u540e\u7eed":[2,15,27,28,29,35,41,42,47,48,51,52,54,59],"\u540e\u7f00":[13,42],"\u540e\u7f00\u540d":42,"\u540e\u8005":[9,19,29,33,34],"\u540e\u80a2":52,"\u540e\u80fd":52,"\u540e\u8be5":43,"\u540e\u9762":[14,21,22,26,27,28,29,39,41,48],"\u5411\u4e0a":[30,41],"\u5411\u4e0b":[15,20,21,27,42,53],"\u5411\u91cf":[27,28,29,34,35,41,42,43,48,52,53,56],"\u5426\u5219":[15,26,27,29,33,35,36,40,41,44,47,48,52,53,56],"\u542b\u4e49":[8,15,21,26,27,29,34,40,41,51,53],"\u542b\u6709":[14,56],"\u542f\u52a8":[3,11,15,19,29,33,34,43],"\u542f\u7528":[13,26,28,29,56],"\u544a\u77e5":[7,13,30],"\u544a\u8bc9":[2,7],"\u5468\u56f4":51,"\u5468\u671f":[27,28,34,35,41,42],"\u5468\u8f6c":29,"\u547d\u4ee4":[0,2,5,8,9,12,15,18,25,32,33,38,42,46,50,51],"\u547d\u4ee4\u884c":[0,32,38,42,50],"\u547d\u540d":[9,30,43],"\u54cd\u5e94":51,"\u54ea\u4e00\u6761":23,"\u54ea\u4e2a":[6,15,26,29,34,35,39,41,53],"\u54ea\u4e9b":[10,15,16,23,27,28,29,30,36,41,44,49,51,56],"\u54ea\u91cc":20,"\u54f2\u5b66":51,"\u54f2\u5b66\u5bb6":51,"\u5524\u9192":[53,54,55],"\u552f\u4e00":[15,27,29,34,42],"\u5546\u4e1a":0,"\u5546\u4e1a\u8f6f\u4ef6":0,"\u5668\u4f1a":[13,34],"\u56db\u4e2a":[28,38,41,52,53],"\u56de\u5230":[13,15,21,29,35,36,47],"\u56de\u53bb":34,"\u56de\u5f52":15,"\u56de\u5fc6":[29,48],"\u56de\u60f3":54,"\u56de\u6536":[15,28,29,30,31,33,34,37,41,42,43,44,47,51,52,56],"\u56de\u6765":[20,35,47],"\u56de\u770b":15,"\u56de\u7b54":[23,30],"\u56de\u8f66":[33,48],"\u56de\u8f66\u952e":33,"\u56de\u8fc7":[29,39],"\u56de\u8fc7\u5934\u6765":[29,39],"\u56de\u987e":29,"\u56e0\u4e3a":[6,8,15,21,27,28,29,35,41,42,46,47,48,55],"\u56e0\u6b64":[2,14,15,20,23,26,27,28,29,34,35,40,41,42,47,48,53,55],"\u56e0\u7d20":53,"\u56e2\u961f":54,"\u56f0\u60d1":23,"\u56f0\u96be":[8,35],"\u56fa\u4ef6":3,"\u56fa\u5316":9,"\u56fa\u5b9a":[2,19,25,39,42,51],"\u56fe\u4e2d":[6,48],"\u5706\u684c":51,"\u5728\u4e8e":[15,27,28,29,34,41],"\u5728\u4f4d":28,"\u5728\u5185":42,"\u5728\u5b50":[13,47],"\u5728\u6b64\u4e4b\u524d":47,"\u5728\u7ebf":[0,5,12,18,25,32,38,51,52,53,54,55],"\u5730\u5740":[0,2,7,9,13,14,15,19,20,21,25,30,33,34,35,36,38,39,41,43,47,48,51,52,53,59],"\u5730\u5740\u6620\u5c04":31,"\u5730\u5740\u680f":0,"\u5730\u65b9":[10,15,16,23,28,30,36,44,49,56],"\u5730\u70b9":9,"\u5730\u88ab":52,"\u5730\u8bf4":[9,51],"\u573a\u666f":35,"\u5750\u5728":51,"\u578b\u51fd\u6570":[27,29,34,39],"\u57fa\u4e8e":[0,5,8,12,15,18,21,22,25,28,31,32,35,37,38,39,43,46,48,50,51,53,55,58],"\u57fa\u5740":29,"\u57fa\u672c":[8,9,13,22,25,28,31,35,39,40,41,51,52,57,58,60],"\u57fa\u672c\u4e00\u81f4":25,"\u57fa\u672c\u4e0a":[28,35,41],"\u57fa\u672c\u529f":[52,58],"\u57fa\u672c\u529f\u80fd":[52,58],"\u57fa\u672c\u601d\u8def":[52,57],"\u57fa\u672c\u76f8\u540c":[13,39],"\u57fa\u7840":[5,12,21,23,29,43,52,55,56,58],"\u586b\u5145":[29,47],"\u586b\u6ee1":[39,41,47],"\u586b\u8865":30,"\u589e\u52a0":[8,15,30,36,51,53,54],"\u589e\u5f3a":[32,48],"\u589e\u91cf":22,"\u589e\u957f":15,"\u58f0\u660e":[9,14,15,21,26,27,29,33,34,38,39,41],"\u5904\u4e8e":[0,15,26,28,29,35,53,56],"\u5904\u5728":[15,27],"\u5904\u7406":[0,2,3,5,7,8,9,12,13,18,20,21,22,23,27,28,30,31,32,33,35,36,37,41,51,53,54,55,57,58],"\u5904\u7406\u51fd\u6570":[7,9,15,23],"\u5904\u7406\u5668":[3,8,18,22,29,32,35,37,51,53,54,55,57],"\u5904\u7406\u5b8c\u6bd5":[22,29],"\u5907\u6ce8":30,"\u590d\u5236":[2,14,28,35,36,41,42,48],"\u590d\u5236\u5230":[2,14,35,41,42],"\u590d\u6742":[3,6,13,29,41,46,52,53,56],"\u590d\u6742\u5ea6":29,"\u590d\u7528":[0,15,18,52],"\u590d\u7528\u6280\u672f":52,"\u5916\u8bbe":[21,41,43],"\u5916\u90e8":[8,9,13,14,15,28,29,41,59],"\u5916\u90e8\u73af\u5883":41,"\u5916\u90e8\u8bbe\u5907":8,"\u5916\u9762":[2,41],"\u591a\u4e2a":[2,12,13,14,15,18,19,23,39,41,49,51,52,53,54,55],"\u591a\u4efb\u52a1":[18,31],"\u591a\u51fa":[29,48],"\u591a\u53ea":[48,54],"\u591a\u5904":3,"\u591a\u5904\u7406\u5668":3,"\u591a\u5c11":[29,30,34,41,42,47],"\u591a\u5c42":[6,39],"\u591a\u5c42\u6b21":6,"\u591a\u6001":39,"\u591a\u6570":5,"\u591a\u6761":53,"\u591a\u6b21":[14,40],"\u591a\u79cd":[3,26,39,40],"\u591a\u79cd\u4e0d\u540c":[39,40],"\u591a\u7ea7":[25,28,29,31],"\u591a\u7ebf":[51,53,56,57],"\u591a\u7ebf\u7a0b":[51,53,56,57],"\u591a\u9053\u7a0b\u5e8f":54,"\u5927\u4e8e":[36,54],"\u5927\u4f53":53,"\u5927\u4f53\u4e0a":53,"\u5927\u591a":5,"\u5927\u591a\u6570":5,"\u5927\u5927":2,"\u5927\u5927\u51cf\u5c11":2,"\u5927\u5b66":0,"\u5927\u5bb6":[36,51,52,53,58],"\u5927\u5c0f":[2,9,26,27,28,29,30,39,40,41,42,47],"\u5927\u65b9":59,"\u5927\u80c6":23,"\u5927\u81f4":[7,14,30,36,41,46,52],"\u5927\u81f4\u76f8\u540c":52,"\u5927\u90e8":52,"\u5927\u90e8\u5206":52,"\u5927\u91cf":[29,34,51,58],"\u5927\u9875":29,"\u5931\u6548":[14,29,30,35],"\u5931\u8d25":[0,9,26,27,30,41,47,56],"\u5947\u602a":[2,14,15,36],"\u5954\u8dd1":52,"\u597d\u50cf":[7,51],"\u597d\u5730\u89e3\u51b3":54,"\u597d\u5904":30,"\u597d\u5947":36,"\u597d\u6808":9,"\u5982\u4e0b":[0,2,6,7,8,9,13,14,15,19,20,21,22,23,26,27,28,30,33,35,36,39,41,46,51,52,53,54,55,56],"\u5982\u4eca":29,"\u5982\u4f55":[2,7,12,13,15,21,22,23,27,28,29,30,35,41,42,44,47,52,53,54,55,58],"\u5982\u627e":33,"\u5982\u679c":[0,2,8,12,13,14,19,21,22,26,27,28,29,33,34,35,36,39,40,41,42,43,44,47,48,49,51,52,53,54,55,56,58],"\u5982\u6b64":[15,19],"\u59cb\u672b":14,"\u59cb\u7ec8":44,"\u5b50\u5173":52,"\u5b50\u76ee":42,"\u5b50\u76ee\u5f55":42,"\u5b50\u7cfb\u7edf":[0,29],"\u5b50\u96c6":28,"\u5b57\u6bb5":[15,22,27,28,29,34,35,41,47],"\u5b57\u7b26":[5,9,11,13,15,29,33,34,35,39,40,46,47,48,52],"\u5b57\u7b26\u4e32":[5,9,11,13,15,29,33,34,35,39,40,46,47,48],"\u5b57\u8282":[0,2,9,15,26,27,28,29,30,33,34,35,39,41,42,47,48],"\u5b57\u8282\u6570":[33,39,41],"\u5b57\u957f":29,"\u5b57\u9762":21,"\u5b58\u50a8":[27,36,38,41,42,51],"\u5b58\u50a8\u8bbe\u5907":38,"\u5b58\u5728":[3,5,6,14,15,22,27,28,29,30,33,34,35,40,41,42,43,44,47,48,51,52,53,54,55],"\u5b58\u653e":[2,27,28,29,35,41,47],"\u5b58\u653e\u6570\u636e":[29,47],"\u5b66\u4e60":1,"\u5b66\u5bb6":[51,54,55],"\u5b66\u671f":58,"\u5b66\u751f":0,"\u5b69\u5b50":[35,52],"\u5b83\u4eec":[2,6,9,13,14,15,20,21,22,26,27,28,29,33,34,38,39,41,42,47,48,51,52,55,58,59],"\u5b83\u4f1a":[9,15,27,29,38,41,47,51],"\u5b83\u6839":27,"\u5b89\u5168":[13,15,26,29,56],"\u5b89\u5168\u6027":[13,15],"\u5b89\u5168\u9690\u60a3":15,"\u5b89\u5fc3":48,"\u5b89\u88c5":[5,12,18,25,32,38,51,60],"\u5b8c\u5168":[0,3,29,35,36,39,41,44,47,48,51,52],"\u5b8c\u5168\u4e00\u81f4":48,"\u5b8c\u5168\u76f8\u540c":[0,35,41],"\u5b8c\u5168\u7b26\u5408":29,"\u5b8c\u540e":36,"\u5b8c\u5584":58,"\u5b8c\u6210":[0,2,5,6,8,9,10,12,13,14,15,16,18,20,21,23,25,27,28,29,30,32,35,36,38,42,44,49,51,52,53,54,55,56,58,60],"\u5b8c\u6574":[0,2,5,8,12,18,25,28,32,38,41,48,51,52],"\u5b8c\u6574\u6027":41,"\u5b8c\u6bd5":[22,29,32,33,34,35,40,43,46,47,48,51,53,54],"\u5b8f\u5728":13,"\u5b8f\u5c06":[9,13,15],"\u5b8f\u662f":7,"\u5b8f\u6709":8,"\u5b8f\u6765":[15,39],"\u5b8f\u89c2":51,"\u5b98\u65b9":[0,2,21,59],"\u5b9a\u4e49":[3,9,13,14,15,18,21,22,23,25,27,28,29,30,31,36,39,41,43,44,52,56,57],"\u5b9a\u4f1a":41,"\u5b9a\u4f4d":[26,39,41],"\u5b9a\u5236":[8,19],"\u5b9a\u5411":[46,50],"\u5b9a\u6027":[51,53],"\u5b9a\u65f6":51,"\u5b9a\u65f6\u5668":51,"\u5b9e\u4f53":[21,51],"\u5b9e\u4f8b":[14,15,21,26,27,29,34,38,39,41,42,43],"\u5b9e\u64cd":58,"\u5b9e\u73b0":[3,5,6,7,10,11,12,16,17,18,19,22,23,24,25,30,31,32,33,34,36,37,38,39,40,41,42,44,45,46,47,48,51,56,57,58],"\u5b9e\u8d28":[15,27,41],"\u5b9e\u8df5":[11,17,24,31,37,45,50,57],"\u5b9e\u9645":[0,2,9,12,13,14,15,27,28,29,30,33,34,35,36,39,41,42,47,48,49,51,53,54,55],"\u5b9e\u9645\u4e0a":[15,29,42,48,55],"\u5b9e\u9645\u6548\u679c":29,"\u5b9e\u9a8c":[3,5,9,12,18,24,25,27,31,32,37,38,45,49,51,57,58],"\u5b9e\u9a8c\u5ba4":3,"\u5b9e\u9a8c\u8bbe\u8ba1":[10,16,23,30,36,44,49,56],"\u5bb9\u5668":[14,28,51,52],"\u5bb9\u6027":[40,44,56],"\u5bb9\u6613":[9,15,22,27,29,43,47,53,55],"\u5bb9\u7eb3":41,"\u5bb9\u91cf":41,"\u5bbd\u88d5":28,"\u5bc4\u5b58":[9,13,17,20,23,29,30,31,35,43,48,51,52,53],"\u5bc4\u5b58\u5668":[9,13,17,20,23,29,30,31,35,43,48,51,52,53],"\u5bf9\u4e8e":[0,2,13,15,19,23,26,27,28,29,32,33,34,35,36,39,41,42,47,48,51,53],"\u5bf9\u5757":41,"\u5bf9\u5916":[14,41],"\u5bf9\u5916\u90e8":41,"\u5bf9\u5e94":[2,13,14,15,21,26,27,28,30,33,34,35,36,39,41,42,43,44,47,48,51,52,53,54,55,56],"\u5bf9\u6b64":[32,54],"\u5bf9\u6bd4":56,"\u5bf9\u7cfb\u7edf":28,"\u5bf9\u8c61":[2,14,39,51,53],"\u5bf9\u9f50":[15,26,28,29,30,48],"\u5bfb\u627e":[21,27],"\u5bfc\u51fa":[7,8],"\u5bfc\u81f4":[8,9,12,29,30,35,36,39,47,51,52,53,56],"\u5bfc\u8bfb":[11,17,24,31,37,45,50,57],"\u5c01\u88c5":[5,6,8,13,21,22,26,33,38,40,41,43,47,57],"\u5c06\u4f1a":[28,30,34,41,43,51],"\u5c06\u4f4d":41,"\u5c06\u5757":[38,41,42],"\u5c06\u5b50":35,"\u5c06\u8981":42,"\u5c0f\u4e8e":[23,28,54],"\u5c0f\u5fc3":55,"\u5c0f\u7ed3":27,"\u5c0f\u8282":[0,8,26,29,34,52,53],"\u5c11\u91cf":6,"\u5c16\u62ec\u53f7":59,"\u5c1a\u672a":[27,41,47,52],"\u5c1d\u8bd5":[21,27,28,29,34,36,41,42,47,48,53,54],"\u5c31\u662f":[6,8,9,13,14,15,20,21,22,23,26,27,28,29,30,34,35,36,41,42,44,47,48,51,52,53,54,55],"\u5c31\u662f\u6307":[28,41],"\u5c31\u662f\u8bf4":[15,22,23,26,27,29,30],"\u5c31\u7528\u5230":27,"\u5c31\u7eea":[52,54,55],"\u5c31\u884c\u4e86":42,"\u5c31\u8981":29,"\u5c31\u9910":51,"\u5c3d\u529b":0,"\u5c3d\u53ef":[28,41,42,52],"\u5c3d\u53ef\u80fd":[28,41,42,52],"\u5c3d\u7ba1":[12,42,51],"\u5c40\u90e8":[13,28,52],"\u5c40\u90e8\u53d8\u91cf":[13,28,52],"\u5c40\u9650":39,"\u5c40\u9650\u4e8e":39,"\u5c42\u6570":28,"\u5c42\u6b21":[6,41],"\u5c42\u6b21\u5316":41,"\u5c42\u9762":[35,51],"\u5c4a\u65f6":42,"\u5c4f\u5e55":[6,13,32,33],"\u5c4f\u853d":[22,58],"\u5c55\u5f00":[41,53],"\u5c55\u793a":[26,28,29,52,58,59],"\u5c5e\u4e8e":[6,26,28,44,51,52],"\u5c5e\u6027":[28,30,53],"\u5d29\u6e83":[8,12],"\u5d4c\u5165":[9,29],"\u5d4c\u5165\u5f0f":29,"\u5d4c\u5957":[15,24,39],"\u5de5\u4f5c":[0,2,5,7,9,10,12,14,15,16,18,21,23,25,29,30,32,36,38,44,46,48,49,51,53,55,56],"\u5de5\u4f5c\u91cf":[10,16,23,30,36,44,49,56],"\u5de5\u5177":[0,5,6,7,8,12,13,18,25,32,38,43,48,51,60],"\u5de6\u4fa7":28,"\u5de6\u53f3":[0,51],"\u5de6\u79fb":26,"\u5de6\u95ed":27,"\u5de7\u5999":[54,55],"\u5de8\u5927":29,"\u5dee\u522b":29,"\u5dee\u5f02":35,"\u5df1\u5206":56,"\u5df2\u6709":[23,32,43,47,51,52],"\u5df2\u6ee1":[41,47],"\u5df2\u77e5":[36,41],"\u5df2\u7ecf":[8,15,21,22,23,27,28,29,30,35,36,40,41,42,43,44,47,48,51,53,54,55],"\u5e03\u5c40":[2,5,11,15,17,25,27,28,29,38,42,45],"\u5e0c\u671b":[6,15,23,28,42,47,48,55,56],"\u5e26\u6709":[28,35,39,42,47],"\u5e26\u6765":[28,29,34,39,42,52],"\u5e2e\u52a9":[0,3,5,8,26,28,39,41],"\u5e2e\u5fd9":0,"\u5e38\u5bb9":27,"\u5e38\u6570":[2,14,19,22,36],"\u5e38\u751f":51,"\u5e38\u7528":[0,26,42],"\u5e38\u7528\u5de5\u5177":0,"\u5e38\u89c1":[26,29,30,40],"\u5e38\u89c4":[38,40,43],"\u5e38\u91cf":[9,21],"\u5e38\u9a7b":3,"\u5e73\u5316":40,"\u5e73\u53f0":[0,3,7,9,11,22,29,38,48],"\u5e73\u5e38":51,"\u5e73\u65f6":51,"\u5e73\u6ed1":[29,52],"\u5e74\u4ee3":12,"\u5e74\u524d":52,"\u5e76\u4e14":[14,28,55],"\u5e76\u4ee4":33,"\u5e76\u53d1":[18,29,41,51,53],"\u5e76\u6253\u5370":33,"\u5e76\u7528":[8,27],"\u5e76\u7f6e":41,"\u5e76\u884c":[51,53],"\u5e76\u884c\u6267\u884c":51,"\u5e78\u8fd0":[6,29],"\u5e8f\u5217":[28,41,42,53,54],"\u5e93\u5185":8,"\u5e94\u5bf9":[29,53],"\u5e94\u5f53":0,"\u5e94\u6709":23,"\u5e94\u7528":[0,2,5,7,8,9,12,17,18,19,20,21,22,25,27,31,32,35,37,38,39,41,44,45,46,47,48,49,51,53,54,55,57],"\u5e94\u7528\u7a0b\u5e8f":[2,5,7,8,9,12,17,19,28,31,37,39,41,44,51,53,54,55,57],"\u5e94\u8be5":[0,2,13,15,20,26,27,28,29,33,36,40,41,42],"\u5e95\u5c42":[5,9,41,42,53],"\u5ea6\u91cf":22,"\u5efa\u4e86":8,"\u5efa\u7acb":[0,5,12,15,18,25,31,32,38,39,43,44,49,51,52,53],"\u5efa\u8bae":[0,23,55],"\u5f00\u53d1":[2,3,5,6,12,18,25,28,32,33,35,38,41,43,48,51,52,54,55,58],"\u5f00\u53d1\u65b9\u5f0f":[0,5,12,18,25,32,38,51],"\u5f00\u53d1\u8005":[3,28,35,51,55],"\u5f00\u542f":[27,28,31,34,56],"\u5f00\u5934":[2,7,9,13,14,15,19,21,27,28,29,32,33,41,43,47,48,52],"\u5f00\u59cb":[0,2,5,6,7,9,13,14,15,21,27,28,29,30,33,34,35,36,38,41,42,47,48,51,52,54,55,58],"\u5f00\u59cb\u8fd0\u884c":55,"\u5f00\u6e90":0,"\u5f00\u9500":[18,29,34,39,53],"\u5f02\u540c":20,"\u5f02\u5e38":[9,13,15,23,26,28,30,52],"\u5f0f\u8c03\u5ea6":[24,53],"\u5f15\u5165":[8,9,12,14,15,18,23,25,28,30,35,39,44,48,52,53,59],"\u5f15\u53d1":[8,51],"\u5f15\u5bfc":[3,9],"\u5f15\u7528":[6,13,14,21,27,28,29,34,35,39,41,44,47,56,59],"\u5f15\u8a00":[11,17,24,31,37,45,50,57],"\u5f15\u8d77":[0,51],"\u5f20\u6c49\u4e1c":0,"\u5f31\u5316":29,"\u5f39\u51fa":27,"\u5f3a\u5236":[0,22],"\u5f3a\u5236\u6027":22,"\u5f3a\u5927":[5,32,33,54],"\u5f52\u96f6":40,"\u5f53\u4e14":34,"\u5f53\u4e2d":46,"\u5f53\u4e8e":[14,26,42,53],"\u5f53\u4ec5":35,"\u5f53\u4ee5":28,"\u5f53\u524d":[0,2,3,7,15,20,21,22,23,27,28,29,33,34,35,36,38,39,44,47,48,51,52,53,54,55,56],"\u5f53\u524d\u4efb\u52a1":[21,23,35],"\u5f53\u524d\u5de5\u4f5c":0,"\u5f53\u6210":8,"\u5f53\u65f6":54,"\u5f53\u6709":56,"\u5f53\u7136":[0,27,36],"\u5f53\u9501":53,"\u5f62\u5f0f":[12,15,27,29,34,38,41,42,48,51,52],"\u5f62\u6210":[26,29,32,41,52,53,54,55],"\u5f71\u54cd":[13,15,28,29,34,51],"\u5f7b\u5e95":[35,44,52],"\u5f80\u5c4a":58,"\u5f80\u5f80":42,"\u5f88\u591a":[6,15,21,27,28,30,41,51,58],"\u5f88\u5927":[29,36,40,52],"\u5f88\u5c0f":41,"\u5f88\u5c11":9,"\u5f88\u5feb":9,"\u5f88\u96be":15,"\u5f97\u4e0d\u5230":53,"\u5f97\u5230":[2,6,13,14,21,26,27,28,29,34,35,39,41,42,47,48,51,53],"\u5f97\u77e5":41,"\u5faa\u73af":[8,9,15,27,28,33,34,35,41,47,55],"\u5fae\u4fe1":60,"\u5fae\u79d2":22,"\u5fc3\u601d":12,"\u5fc5\u5b9a":[35,41,48],"\u5fc5\u5b9a\u4f1a":41,"\u5fc5\u8981":[2,28,42,58],"\u5fc5\u9650":35,"\u5fc5\u987b":[0,5,12,14,15,18,23,25,26,29,30,32,33,36,38,41,42,44,47,48,51,54,55,59],"\u5fd8\u8bb0":28,"\u5feb\u6377":34,"\u5feb\u7167":29,"\u5feb\u8868":29,"\u5feb\u8868\u4e2d":29,"\u5feb\u901f":[2,26,59],"\u5ffd\u7565":[22,29,40,44,48,58],"\u5ffd\u89c6":51,"\u6001\u4e0b":5,"\u6001\u540e":[23,33],"\u6001\u65f6":[35,52],"\u6001\u662f":23,"\u6001\u6808":52,"\u6001\u80fd":35,"\u600e\u4e48":28,"\u600e\u4e48\u6837":28,"\u601d\u60f3":[12,28,34],"\u601d\u8003":[15,23,30,51,58],"\u601d\u8003\u9898":58,"\u601d\u800c":36,"\u601d\u8def":[19,27,45,52,57,58],"\u6027\u80fd":[29,42,51,53],"\u6027\u8d28":48,"\u603b\u4f1a":42,"\u603b\u4f53":[15,29,52,53],"\u603b\u5757\u6570":41,"\u603b\u6570":21,"\u603b\u65f6\u957f":23,"\u603b\u662f":[15,21,48],"\u603b\u7684\u6765\u8bf4":28,"\u603b\u7ebf":43,"\u603b\u7ed3":[10,16,21,23,30,36,41,44,56],"\u603b\u8bc4":58,"\u6050\u9f99":52,"\u6052\u7b49":[27,28,29,43],"\u6062\u590d":[12,17,20,21,22,25,29,30,52,55],"\u606d\u559c":0,"\u6070\u597d":[27,29,35,41],"\u6076\u52a3":28,"\u6076\u52a3\u5f71\u54cd":28,"\u6076\u610f":29,"\u60c5\u51b5":[0,7,13,14,15,21,22,23,26,27,28,29,30,34,35,36,40,41,42,43,44,47,48,51,52,53,54,55,56],"\u60c5\u5883":14,"\u60c5\u5f62":[9,15,27],"\u60c5\u666f":23,"\u60e9\u7f5a":29,"\u60f3\u60f3":55,"\u60f3\u6cd5":53,"\u610f\u4e49":[7,15,21,23,26,28,29,30,35,41],"\u610f\u5473":[9,27,28,34,47,52],"\u610f\u5473\u7740":[9,27,28,34,47,52],"\u611f\u5174":[9,28,34,41,43,47],"\u611f\u5174\u8da3":[9,28,34,41,43,47],"\u611f\u5230":[23,52],"\u611f\u89c9":9,"\u611f\u8c22":0,"\u6210\u4e3a":[51,52],"\u6210\u529f":[0,13,22,23,30,33,36,41,42,47],"\u6210\u5458":[41,51,52,53,54,55],"\u6210\u672c":58,"\u6210\u679c":22,"\u6210\u6b63\u6bd4":36,"\u6210\u7acb":27,"\u6211\u4eec":[0,2,5,6,7,8,9,12,13,14,15,19,20,21,22,23,26,27,28,29,30,32,33,34,35,36,38,39,40,41,42,43,44,46,47,48,51,52,53,54,55,56,58,59],"\u6216\u662f":[15,28,41,42],"\u6216\u8005":[0,13,14,15,27,29,30,34,35,36,41,47,48,51,52,53,55,58],"\u6216\u8bb8":36,"\u622a\u7136":47,"\u622a\u7136\u4e0d\u540c":47,"\u6240\u4ee5":[13,15,23,29,35,36,48,52,53,54,55],"\u6240\u5728":[9,13,14,15,27,28,29,35,41,42,44,47,48],"\u6240\u5728\u4f4d\u7f6e":28,"\u6240\u5904":[29,39,41],"\u6240\u5b66":40,"\u6240\u5c5e":[15,28,34,41,47,52],"\u6240\u6307":15,"\u6240\u6709":[2,7,12,13,14,15,18,19,21,22,23,25,26,27,28,29,30,34,35,36,39,40,41,42,43,44,47,52,53,54,55,56],"\u6240\u6709\u6743":[21,26,41,53],"\u6240\u6709\u8005":41,"\u6240\u6b32":[10,16,23,30,36,44,49,56],"\u6240\u7528":56,"\u6240\u793a":[53,54,55],"\u6240\u8c13":[28,41],"\u6240\u9650":47,"\u6241\u5e73":40,"\u6241\u5e73\u5316":40,"\u624b\u518c":2,"\u624b\u52a8":[0,2,13,14,15,21,22,27,28,29,33,34,35,39,47,48,55],"\u624d\u80fd":[2,13,14,15,21,23,26,28,29,30,35,39,40,41,42,48,51,52,53,54,55],"\u6253\u5305":[12,36,38,41,43,44,45],"\u6253\u5370":[5,6,7,8,11,13,15,32,33,34,43,52],"\u6253\u5f00":[0,13,39,41,42,45,47,48,51],"\u6253\u65ad":[15,22,53],"\u6253\u7b97":27,"\u6267\u884c":[0,5,7,12,13,14,17,18,19,20,21,22,23,25,28,30,31,32,33,35,36,37,38,41,42,45,46,48,51,53,54,55,56,57],"\u6267\u884c\u7a0b\u5e8f":[2,6,7,8,42],"\u6269\u5145":[27,41],"\u6269\u5927":51,"\u6269\u5bb9":[41,42],"\u6269\u5c55":[8,21,31,46,51,52,54],"\u6279\u5904\u7406":[12,13,15],"\u627e\u51fa":[15,35],"\u627e\u5230":[0,15,17,19,21,22,26,27,28,29,33,34,35,36,39,41,42,43,47,48,52,55,56],"\u627f\u62c5":34,"\u627f\u63a5":34,"\u6280\u672f":[0,52,58],"\u62a2\u5230":53,"\u62a2\u5360":[23,24,53],"\u62a5\u544a":[24,31,33,37,45,50,57],"\u62a5\u9519":[6,7,15,23,27],"\u62bd\u4e1d":5,"\u62bd\u4e1d\u5265\u8327":5,"\u62bd\u8c61":[25,29,31,32,34,38,39,41,43,45,47,51,52,54],"\u62bd\u8c61\u6570\u636e\u7c7b\u578b":54,"\u62c5\u5fc3":[15,42],"\u62c6\u5206":[18,34,52],"\u62c6\u9664":31,"\u62d2\u7edd":56,"\u62d3\u5c55":[13,35,47],"\u62e5\u6709":[3,52,53,55],"\u62ec\u53f7":59,"\u62f7\u8d1d":[26,27,28,34,35,42,48],"\u62f7\u8d1d\u5230":28,"\u62fc\u63a5":26,"\u62ff\u5230":[43,48,51,53],"\u6301\u4e45":38,"\u6301\u6709":[41,42],"\u6301\u7eed":43,"\u6302\u5230":52,"\u6307\u4ee4":[2,3,6,8,9,12,13,14,15,23,29,30,34,35,36,51,52,53,54],"\u6307\u4ee4\u96c6":[3,6],"\u6307\u51fa":[9,23,29,34,47,48,52],"\u6307\u5357":[0,1],"\u6307\u5411":[2,9,15,21,27,28,29,34,35,41,42,44,47,48],"\u6307\u5b9a":[2,19,23,41,42,46,48],"\u6307\u5bfc":[1,5,58],"\u6307\u5bfc\u4e66":[5,58],"\u6307\u660e":27,"\u6307\u793a":[14,15],"\u6307\u9488":[15,27,29,34,35,42,47,48,51,52],"\u6309\u7167":[0,2,5,12,13,14,15,18,25,27,29,30,32,34,36,38,39,41,51,52],"\u6309\u94ae":[0,5,12,18,25,32,38,51],"\u6309\u952e":33,"\u6309\u9700\u5206\u914d":41,"\u6309\u9875":30,"\u6355\u83b7":[28,33,41],"\u635f\u574f":44,"\u635f\u5931":42,"\u6362\u6210":[0,6,26,27],"\u6362\u673a":28,"\u6362\u6808":15,"\u6362\u7b97":41,"\u636e\u5e93":51,"\u638c\u63e1":[2,51,58],"\u63a5\u4e0b":[0,5,6,8,15,26,27,28,29,35,41,42,43,52,53,55,59],"\u63a5\u4e0b\u6765":[0,5,6,8,15,26,27,28,29,35,41,42,43,52,53,55,59],"\u63a5\u5165":[27,43,52],"\u63a5\u53d7":15,"\u63a5\u53e3":[3,5,6,13,14,18,21,22,28,31,32,34,35,38,39,43,44,45,48,52,56],"\u63a5\u6536":[0,5,12,18,25,32,38,48,51],"\u63a5\u7740":[9,27,28,35,42,43],"\u63a5\u8fd1":52,"\u63a7\u5236":[2,5,14,17,18,20,23,24,27,28,30,31,32,35,37,38,39,43,47,53,54,55,57],"\u63a7\u5236\u6743":28,"\u63a7\u5236\u6d41":[15,20,29,37],"\u63a8\u8350":[0,27,36],"\u63cf\u8ff0":[2,6,13,15,21,23,27,28,29,30,33,35,36,38,40,44,45,46,47,48,51,52],"\u63cf\u8ff0\u7b26":[13,33,35,38,40,44,45,46,47,48],"\u63d0\u4ea4":[0,5,12,18,21,25,32,38,51,58,60],"\u63d0\u4f9b":[0,3,5,6,8,9,11,12,13,14,15,18,21,26,27,28,29,33,34,35,38,39,41,42,43,48,51,53,54,55,56,58],"\u63d0\u51fa":[51,54,55],"\u63d0\u5230":[13,27,28,29,32,39,47,51,53],"\u63d0\u524d":[27,39,41,51],"\u63d0\u53ca":[0,20],"\u63d0\u53d6":0,"\u63d0\u65e9":28,"\u63d0\u793a":[0,5,12,18,23,25,32,38,51],"\u63d0\u9192":[7,36],"\u63d0\u9ad8":[51,52],"\u63d2\u4ef6":[0,60],"\u63d2\u5165":[13,14,15,27,28,29,34,35,38,42,59],"\u641c\u7d22":0,"\u642d\u914d":0,"\u64cd\u4f5c":[0,3,5,6,8,9,12,13,15,17,20,27,29,30,32,34,41,42,43,44,45,51,52,54,55,56,57],"\u64cd\u4f5c\u7cfb\u7edf":[0,3,5,6,8,9,12,15,17,20,27,29,30,32,39,44,51,52,54,55,56,57],"\u64cd\u4f5c\u8fc7\u7a0b":54,"\u652f\u6301":[3,5,9,11,13,15,18,23,26,27,28,29,32,35,36,39,40,41,42,43,46,47,48,51,52,53,54,55],"\u6536\u5230":52,"\u6536\u96c6":[33,34,35,42,48,52],"\u6539\u4e3a":[38,43,47,51],"\u6539\u5199":28,"\u6539\u52a8":[15,29,34,52,56],"\u6539\u53d8":[55,56],"\u6539\u8fdb":[10,16,23,30,31,36,44,46,49,52,56],"\u6539\u9020":[9,29],"\u653b\u51fb":30,"\u653e\u4e0b":51,"\u653e\u5165":[34,35,36,48,53,54,55],"\u653e\u5230":[29,34,35,39,42,43,48,53],"\u653e\u5728":[2,9,12,13,23,25,27,28,29,34,35,40,42,47,48,52,53,54,55],"\u653e\u5f03":[18,23],"\u653e\u5fc3":55,"\u653e\u7f6e":[9,13,14,15,24,28,29,42,47],"\u6545\u5e94":56,"\u6545\u800c":[29,39,41],"\u6545\u969c":51,"\u6548\u4eff":28,"\u6548\u679c":[15,25,29,35,53],"\u6548\u7387":[36,55],"\u654f\u6377":52,"\u6559\u5b66":29,"\u6559\u5e08":0,"\u6559\u7a0b":[0,2,3,29,40,58],"\u6570\u503c":55,"\u6570\u5b66":[54,59],"\u6570\u5b66\u516c\u5f0f":59,"\u6570\u5b66\u5bb6":54,"\u6570\u636e":[2,8,9,12,13,14,21,25,28,29,31,35,36,37,38,39,42,43,44,45,47,48,51,53,54,55,56,57],"\u6570\u636e\u4ea4\u6362":39,"\u6570\u636e\u5e93":51,"\u6570\u636e\u7ed3\u6784":[8,21,28,29,31,35,36,37,38,42,45,51,53,54,55,56,57],"\u6570\u6765":28,"\u6570\u76ee":[2,21,28,41,56],"\u6570\u7ec4":[14,15,21,23,27,28,29,39,41,47,48,56],"\u6570\u91cf":[14,25,29,30,39,41,44,56],"\u6570\u91cf\u7ea7":30,"\u6574\u4e2a":[0,2,5,9,12,13,15,18,25,28,29,32,34,38,41,42,47,51,52,56],"\u6574\u4e3a":28,"\u6574\u5408":[27,29,35],"\u6574\u5757":27,"\u6574\u578b":[54,55],"\u6574\u5957":2,"\u6574\u5f62":36,"\u6574\u6570":[14,29,34,36,39,54],"\u6574\u6bb5":29,"\u6587\u4e2d":14,"\u6587\u4ef6":[0,5,6,7,8,9,12,13,14,23,28,29,30,33,34,35,36,38,44,46,48,50,51,52,58],"\u6587\u4ef6\u521b\u5efa":[41,45],"\u6587\u4ef6\u540d":[13,36,40,41,42,43,48],"\u6587\u4ef6\u5927\u5c0f":[28,41],"\u6587\u4ef6\u5939":23,"\u6587\u4ef6\u683c\u5f0f":[7,29,42],"\u6587\u4ef6\u7c7b\u578b":44,"\u6587\u4ef6\u7cfb\u7edf":[38,44,48],"\u6587\u672c":[51,59],"\u6587\u6863":[5,21,25,51,58,59,60],"\u6587\u7248":28,"\u659c\u4f53":59,"\u65b0\u4e00\u8f6e":34,"\u65b0\u521b":35,"\u65b0\u521b\u5efa":35,"\u65b0\u589e":[9,12,13,18,21,22,25,29,32,34,38,43,46,47,48,51],"\u65b0\u5efa":[0,7,27,28,35,36,39],"\u65b0\u5f00":1,"\u65b0\u624b":1,"\u65b0\u7248":29,"\u65b9\u4fbf":[0,5,12,13,15,18,25,26,27,29,32,34,36,38,42,44,48,51,52],"\u65b9\u5f0f":[0,5,12,18,19,21,22,25,27,28,29,32,35,38,40,41,43,51,52,53,54,55,58],"\u65b9\u6848":[0,36,55],"\u65b9\u6cd5":[0,12,14,15,21,25,26,28,29,31,34,36,39,40,41,42,43,50,52,54,55,57,59],"\u65b9\u9762":[27,52],"\u65bd\u52a0":[53,54,55],"\u65c5\u7a0b":25,"\u65c5\u9014":5,"\u65e0\u6548":[30,36],"\u65e0\u6743":29,"\u65e0\u6808":51,"\u65e0\u6cd5":[0,9,12,13,27,28,29,30,33,39,41,43,47,53,55,60],"\u65e0\u6cd5\u8bbf\u95ee":30,"\u65e0\u8bba":[7,15,22,26,29,53],"\u65e0\u8bba\u5982\u4f55":[7,22],"\u65e0\u8bba\u662f":29,"\u65e0\u8bef":60,"\u65e0\u9650":51,"\u65e0\u9650\u671f":51,"\u65e0\u9700":[15,23,27,28,29,35,39,44,56],"\u65e2\u5b9a":41,"\u65e2\u6709":47,"\u65e2\u7136":15,"\u65e2\u7136\u5982\u6b64":15,"\u65e2\u8981":35,"\u65e5\u5e38":51,"\u65e5\u5e38\u751f\u6d3b":51,"\u65e5\u5fd7":[5,9,44],"\u65e9\u671f":[35,55],"\u65f6\u4ec5":27,"\u65f6\u5019":[2,13,14,15,22,26,27,28,29,33,34,35,38,39,40,41,42,43,44,47,48,51,52,53,55],"\u65f6\u5148":23,"\u65f6\u5206":[18,52],"\u65f6\u5206\u590d\u7528":[18,52],"\u65f6\u523b":[23,52,53,55],"\u65f6\u5e93":6,"\u65f6\u624d":[29,30],"\u65f6\u62a5":13,"\u65f6\u6709":29,"\u65f6\u673a":13,"\u65f6\u67e5":39,"\u65f6\u80fd":[9,55],"\u65f6\u949f":[18,24],"\u65f6\u957f":[22,23],"\u65f6\u95f4":[15,18,20,21,22,23,29,33,34,35,36,40,47,51,53,55,56,58],"\u65f6\u95f4\u6bb5":53,"\u660e\u663e":30,"\u660e\u786e":48,"\u660e\u786e\u6307\u51fa":48,"\u6613\u4e8e":47,"\u6613\u7528":[41,49],"\u6620\u5c04":[28,29,31,35,43,51],"\u6625\u5b63":58,"\u662f\u4e0d\u662f":[2,28],"\u662f\u4ece":[15,28,39,41],"\u662f\u5426":[2,13,22,26,29,30,35,36,39,41,42,43,47,48,52,53,54,55,56],"\u662f\u5426\u662f":29,"\u662f\u56e0\u4e3a":[6,15,29,35,41],"\u663e\u5f0f":[21,41,53,54,55],"\u663e\u7136":[27,28,30],"\u663e\u793a":[9,11],"\u663e\u793a\u5b57\u7b26":8,"\u663e\u8457":29,"\u665a\u671f":52,"\u666e\u9002\u6027":39,"\u666e\u901a":15,"\u667a\u80fd":34,"\u6682\u4e14":[15,27],"\u6682\u505c":[20,21,22,35,52],"\u6682\u65f6":[0,12,13,42,47],"\u66b4\u529b":36,"\u66b4\u9732":[14,42],"\u66b4\u9732\u51fa":14,"\u66f4\u4e3a":[29,39,41,52],"\u66f4\u4e3a\u4e25\u91cd":29,"\u66f4\u52a0":[5,34,39,54],"\u66f4\u597d":[34,36,58],"\u66f4\u6362":[0,30],"\u66f4\u6539":60,"\u66f4\u65b0":[0,21,27,31,43,47,48,51,53,60],"\u66fe\u7ecf":47,"\u66ff\u4ee3":36,"\u66ff\u6362":[0,23,33,34,35,41,43,48],"\u66ff\u6362\u6210":0,"\u66ff\u6362\u7b97\u6cd5":41,"\u6700\u4e3a":35,"\u6700\u4e45":53,"\u6700\u4f4e":[26,28,41],"\u6700\u5148":29,"\u6700\u540e":[5,8,12,13,14,15,27,28,29,30,33,35,41,48,53,55,59],"\u6700\u591a":39,"\u6700\u5927":[28,52],"\u6700\u597d":0,"\u6700\u5c0f":[3,5,9,11,15,28,36,47,48],"\u6700\u5c0f\u503c":36,"\u6700\u5c0f\u5316":[11,28],"\u6700\u5e7f":41,"\u6700\u5e95\u5c42":41,"\u6700\u5f31":9,"\u6700\u65e9":53,"\u6700\u7ec8":[2,9,13,28,29,52,53],"\u6700\u9760\u8fd1":51,"\u6700\u9ad8":[26,28,29],"\u6709\u4e2a":52,"\u6709\u4e9b":[14,29,41,52,55],"\u6709\u4f55":30,"\u6709\u4f55\u610f\u4e49":23,"\u6709\u5173":[29,37],"\u6709\u6240":8,"\u6709\u6240\u4e0d\u540c":8,"\u6709\u6548":[2,42,53],"\u6709\u6743":[21,26,41,53],"\u6709\u6ca1\u6709":[27,41],"\u6709\u70b9":[8,9,54],"\u6709\u70b9\u50cf":9,"\u6709\u7740":39,"\u6709\u7a7a":53,"\u6709\u8bef":27,"\u6709\u9650":[19,41,52,58],"\u670d\u52a1":[0,9,13,42,52],"\u670d\u52a1\u5668":0,"\u670d\u52a1\u7a0b\u5e8f":13,"\u671f\u5f85":29,"\u671f\u671b":[9,51,53],"\u671f\u95f4":[14,20,29],"\u671f\u9650":0,"\u672a\u5b9a":44,"\u672a\u5b9a\u4e49":44,"\u672a\u77e5":0,"\u672b\u5c3e":[0,33,48],"\u672c\u4e66":[15,58],"\u672c\u4f53":41,"\u672c\u5730":[0,5,12,18,23,25,32,36,38,44,51,58,60],"\u672c\u5c42":42,"\u672c\u6765":[29,48],"\u672c\u6b21":[10,16,23,30,36,41,44,49,56],"\u672c\u76f8":[13,39],"\u672c\u7ae0":[9,11,13,14,15,17,21,24,28,31,34,36,37,40,43,45,49,50,52,57],"\u672c\u8282":[0,9,35,41,52,54],"\u672c\u8d28":29,"\u672c\u8eab":[2,26,51,54,58],"\u672c\u8f6e":35,"\u672c\u95e8":6,"\u672f\u8bed":51,"\u673a\u4f1a":53,"\u673a\u5236":[6,8,12,13,17,18,20,25,28,29,30,31,32,37,38,45,46,49,51,56,57],"\u673a\u5668":[9,29,53],"\u673a\u5668\u6307\u4ee4":53,"\u673a\u5668\u7801":29,"\u6740\u6b7b":[7,15,30],"\u6743\u8861":29,"\u6743\u9650":[2,35,43,47],"\u6761\u4ef6":[27,35,41,51,53,56,57],"\u6765\u6e90":28,"\u6765\u6e90\u4e8e":28,"\u6765\u770b":[2,27,29,34,41,47,55],"\u6765\u7b97\u51fa":42,"\u6765\u81ea":[15,20,28,53,59],"\u6765\u8bf4":[0,27,28,29,34,35,44,48],"\u6781\u5176":28,"\u6781\u7aef":[27,29],"\u6784\u5efa":[5,6,11,12,13,14,23,29,32,38,51,58],"\u6784\u6210":[28,41],"\u6784\u9020":[15,21,25,27,28,29,35,42,52],"\u679a\u4e3e":[28,41,42],"\u67b6\u6784":[3,6,15,24,26,27,58],"\u67d0\u4e00":55,"\u67d0\u4e2a":[13,15,22,29,39,41,54,55],"\u67d0\u4e9b":[0,12,15,20,28,34,42,47,55,58],"\u67d0\u6d4b":36,"\u67d0\u79cd":[15,29,48,53,54,55],"\u67d0\u79cd\u610f\u4e49":29,"\u67d0\u7ea7":27,"\u67e5\u627e":[28,34,35,36,42],"\u67e5\u770b":[2,13,38,52,53,60],"\u67e5\u8be2":[23,42],"\u67e5\u9605":[9,34,43,47],"\u6807\u51c6":[3,5,6,11,13,33,35,36,38,41,44,45,50],"\u6807\u51c6\u63a5\u53e3":44,"\u6807\u51c6\u89c4":3,"\u6807\u51c6\u89c4\u5b9a":3,"\u6807\u5fd7":[13,26,27,28,30,40,42,43,48,53],"\u6807\u6ce8":41,"\u6807\u7b7e":[0,5,12,18,25,32,38,51],"\u6807\u8bb0":[7,9,15,29,41,53,58],"\u6807\u8bc6":[9,27,32,37,52],"\u6807\u8bc6\u7b26":[32,37,52],"\u6808\u4e0a":[15,21,27,34,35,39,50],"\u6808\u4e2d":[27,29,46,48],"\u6808\u4ee5":15,"\u6808\u5219":28,"\u6808\u538b\u5165":[15,21],"\u6808\u5e95":9,"\u6808\u5f0f":[27,34],"\u6808\u6765":[15,52],"\u6808\u6808":[15,21,29],"\u6808\u9876":[9,15,27,29,34,52],"\u6811\u7ed3\u6784":42,"\u6821\u56ed":0,"\u6821\u56ed\u7f51":0,"\u6837\u5f0f":59,"\u6838\u4e0a":34,"\u6838\u548c\u7247":3,"\u6838\u5728":42,"\u6838\u5fc3":[6,7,8,12,14,20,27,37,38,41,53,54,55,57],"\u6838\u5fc3\u601d\u60f3":12,"\u6838\u5fc3\u6210\u5458":53,"\u6839\u636e":[0,5,12,15,18,23,25,27,28,29,32,33,34,38,41,42,43,47,51,53,58],"\u6839\u76ee\u5f55":[0,13,40,43,45,60],"\u683c\u5f0f":[5,6,7,8,9,28,29,30,31,34,35,38,41,42,43,48,58],"\u683c\u5f0f\u5316":[5,8],"\u683c\u5f0f\u6587\u4ef6":29,"\u6846\u67b6":[0,5,23,24,30,31,36,37,44,45,50,56,57,58],"\u6848\u4f8b":[8,33],"\u68c0\u67e5":[15,23,26,27,28,36,41,42,43,47,48,54,55],"\u68c0\u6d4b":[28,57],"\u6905\u5b50":51,"\u6982\u5ff5":[26,30,35,40,55,57],"\u6982\u62ec":29,"\u6982\u7387":14,"\u6982\u8ff0":45,"\u6a21\u5757":[7,9,12,13,14,15,18,19,21,22,25,26,27,28,29,32,34,35,36,38,39,43,45],"\u6a21\u5757\u5316":45,"\u6a21\u578b":57,"\u6a21\u5f0f":[2,8,13,15,26,28,31,40,52],"\u6a21\u62df":[2,8,9,12,14,18,25,32,33,38,43,46,51],"\u6a21\u62df\u5668":[12,14,18,25,32,33,38,43,46,51],"\u6a2a\u8f74":21,"\u6a59\u8272":48,"\u6b21\u6570":[23,36],"\u6b21\u9ad8\u9875":29,"\u6b22\u8fce":[10,16,23,30,36,44,49,56,58],"\u6b63\u56e0\u5982\u6b64":19,"\u6b63\u5728":[21,23,28,29,35,37,41,52],"\u6b63\u597d":41,"\u6b63\u5e38":[0,2,11,14,15,23,27,28,30,35,42,47,52,60],"\u6b63\u5f0f":[15,27],"\u6b63\u6570":35,"\u6b63\u6574\u6570":54,"\u6b63\u6587":0,"\u6b63\u6bd4":36,"\u6b63\u786e":[0,2,8,11,14,15,21,23,27,28,29,35,38,41,52,53,55],"\u6b63\u786e\u6027":14,"\u6b64\u524d":[27,28],"\u6b64\u540e":[26,29],"\u6b64\u57fa\u7840":21,"\u6b64\u5904":[5,8,29],"\u6b64\u5916":[21,27,28,29,39,41,46,51],"\u6b64\u65f6":[6,9,15,26,28,29,30,33,35,41,44,54,55,56],"\u6b64\u6bb5":53,"\u6b64\u7c7b":3,"\u6b65\u6b65":5,"\u6b65\u957f":36,"\u6b65\u9aa4":[36,53,56],"\u6b7b\u5faa\u73af":[8,9],"\u6b7b\u9501":[51,55,57],"\u6bb5\u4ec5":28,"\u6bb5\u65f6\u95f4":[20,21,22,29,53],"\u6bcf\u4e00\u9879":28,"\u6bcf\u4e2a":[2,9,12,13,14,15,19,21,22,26,27,28,29,34,35,36,39,41,42,43,47,48,51,52,53,54,55,56],"\u6bcf\u6b21":[15,27,29,33,35,36,39,41,42,47,48],"\u6bcf\u7ec4":41,"\u6bd4\u5982":[0,9,15,30,48,51,52,53,54,59],"\u6bd4\u7279":26,"\u6bd4\u8f83":[15,21,28,29,36,41,42,43,47,52],"\u6bd4\u8f83\u590d\u6742":52,"\u6bd4\u8f83\u7b80\u5355":[21,42,43],"\u6beb\u79d2":22,"\u6c38\u8fdc":[36,42],"\u6c47\u7f16":[2,5,7,8,9,12,13,14,15,18,20,23,25,28,29,39,53,58],"\u6c47\u7f16\u5668":29,"\u6c47\u7f16\u7a0b\u5e8f":[7,8],"\u6c60\u6ee1":36,"\u6c9f\u901a":55,"\u6ca1\u5173":51,"\u6ca1\u5173\u7cfb":51,"\u6ca1\u6709":[0,2,6,7,8,9,15,21,22,27,29,30,34,35,41,42,47,48,51,52,53,54,55],"\u6cbf\u7528":2,"\u6cc4\u9732":47,"\u6cdb\u578b":[27,39,41],"\u6ce8\u518c":0,"\u6ce8\u5b9a":25,"\u6ce8\u610f":[0,2,5,9,12,15,18,23,25,26,27,28,29,30,32,33,34,35,36,38,41,42,43,44,46,47,48,51,52,53,54,55,59,60],"\u6ce8\u660e":23,"\u6ce8\u89e3":58,"\u6ce8\u8bb0":59,"\u6ce8\u91ca":[7,8],"\u6d3b\u52a8":[3,51],"\u6d3b\u6027":[48,56],"\u6d3b\u8dc3":55,"\u6d41\u4ece":15,"\u6d41\u5411":47,"\u6d41\u7a0b":[0,15,29,38,42,55],"\u6d4b\u4f8b":[12,23,30,36,38,44,46,47,51,56],"\u6d4b\u8bd5":[0,5,13,23,27,32,36,40,41,44,59],"\u6d4b\u8bd5\u7528\u4f8b":40,"\u6d4b\u8bd5\u7a0b\u5e8f":27,"\u6d4b\u8bd5\u901a\u8fc7":36,"\u6d4f\u89c8":[0,5,12,18,25,32,38,51],"\u6d4f\u89c8\u5668":[0,5,12,18,25,32,38,51],"\u6d6a\u8d39":[21,42,52,55],"\u6d88\u9664":35,"\u6d89\u53ca":[15,20,28,29,41],"\u6db5\u76d6":[27,41],"\u6df1\u5165":[5,23,28,36,55],"\u6df7\u5408":56,"\u6df7\u6dc6":9,"\u6dfb\u52a0":[8,11,21,23,33,43,46,53],"\u6e05\u534e":0,"\u6e05\u695a":[28,34],"\u6e05\u7406":14,"\u6e05\u7a7a":[11,14,27,29,33,35,40,41,43,45,52],"\u6e05\u9664":[2,29,53],"\u6e05\u96f6":[9,13,26,27,41,42],"\u6e32\u67d3":58,"\u6e90\u4e8e":[28,29],"\u6e90\u4ee3\u7801":[6,23,41,42,52],"\u6e90\u6587\u4ef6":12,"\u6e90\u6765":0,"\u6e90\u7801":[0,3,9,42],"\u6e90\u7a0b\u5e8f":13,"\u6ea2\u51fa":[9,28,36],"\u6ed1\u7fd4":52,"\u6ee1\u8db3":[6,28,29,36,55,56],"\u6f0f\u6d1e":29,"\u7075\u6d3b":[0,2,34,39,48,51,52,53,54,56],"\u7075\u6d3b\u5904\u7406":2,"\u7075\u6d3b\u6027":[48,56],"\u70b9\u4e3a":[2,9],"\u70b9\u51fb":[0,5,12,18,25,32,38,51],"\u7136\u540e":[0,5,8,12,13,14,15,18,21,22,25,27,28,29,30,32,34,35,38,44,48,51,53,54,55,60],"\u7136\u800c":[5,28,29],"\u7194\u65ad":29,"\u719f\u6089":58,"\u7236\u5b50":[34,35,46,47,52],"\u7236\u5b50\u5173\u7cfb":52,"\u7247\u540e":36,"\u7247\u6bb5":[43,53,59],"\u7248\u672c":[0,2,5,23,29,39],"\u7269\u7406":[13,14,15,19,25,28,29,30,31,32,34,35,43,52],"\u7269\u7406\u5730\u5740":[13,19,27,28,29,31,43],"\u7279\u4f8b":54,"\u7279\u522b":[13,35,36,53],"\u7279\u5b9a":[3,9,27,28,29,33,36,39,43],"\u7279\u5f81":[23,39],"\u7279\u6027":[13,39,55],"\u7279\u6307":53,"\u7279\u6743":[3,5,9,12,13,17,20,22,23,26,28,29,57],"\u7279\u6b8a":[15,20,23,29,33,41,55],"\u7279\u6b8a\u6027":55,"\u72b6\u6001":[2,17,18,23,24,27,33,34,35,41,44,47,51,52,53,54,55,56],"\u72c2\u8f6c":9,"\u72ec\u5360":[41,52,53,54],"\u72ec\u7279":2,"\u72ec\u7acb":[38,41,51,52],"\u73af\u5883":[2,3,5,7,12,15,18,25,28,32,34,38,41,51,52,53,58,60],"\u73af\u5883\u53d8":[0,41],"\u73af\u5883\u53d8\u91cf":[0,41],"\u73af\u5883\u8981\u7d20":52,"\u73af\u8282":59,"\u73b0\u4ee3":[6,22],"\u73b0\u5728":[3,6,8,9,13,15,28,29,30,36,39,47,48,51,59],"\u73b0\u5b9e":53,"\u73b0\u5b9e\u751f\u6d3b":53,"\u73b0\u6709":52,"\u73b0\u8c61":[30,36,53],"\u7406\u4f1a":[26,28],"\u7406\u5668":[0,3,8,14,18,22,24,27,29,32,33,35,37,45,51,53,54,55,57],"\u7406\u5b8c":[22,29],"\u7406\u89e3":[5,8,9,11,15,23,29,39,47,51,53],"\u7406\u8bba":[0,30,36],"\u7410\u788e":43,"\u751a\u81f3":[15,59],"\u751f\u53d8":[14,15,33,34,35,46,48,52],"\u751f\u547d":[27,28,34,35,41,42],"\u751f\u547d\u5468\u671f":[27,28,34,35,41,42],"\u751f\u5b58":52,"\u751f\u6210":[6,7,8,9,12,14,17,26,28,29,34,37,39,41,42,53,55],"\u751f\u6548":0,"\u751f\u6d3b":[51,53],"\u751f\u6daf":5,"\u7528\u4e8e":[0,5,9,12,15,18,25,27,28,29,30,32,33,34,38,40,41,42,46,47,48,51,52,54,58],"\u7528\u4ee5":36,"\u7528\u4f5c":9,"\u7528\u4f8b":58,"\u7528\u5230":[14,15,21,27,28,29,34,39,41,42,48,52],"\u7528\u5c3d":35,"\u7528\u5de5":0,"\u7528\u6237":[0,2,5,7,9,11,12,13,17,18,19,20,22,23,24,28,29,30,32,35,37,38,39,40,44,46,47,50,51,52,53,56],"\u7528\u6237\u5e93":[12,13,21,33,35,40,47,50],"\u7528\u6237\u7a0b\u5e8f":[12,13,15,18,19,20,22,56],"\u7528\u6237\u7ec4":40,"\u7528\u6743":[20,21,34,35,52],"\u7528\u6765":[13,14,15,22,26,27,28,29,34,35,41,47],"\u7528\u6cd5":41,"\u7528\u8005":[20,28,38,41,42],"\u7528\u9014":[15,27,54],"\u7531\u4e8e":[2,7,13,15,23,26,27,28,29,34,35,41,42,43,47,48,51,52,53,54,55],"\u7531\u5143":2,"\u7531\u6b64":[41,42],"\u7531\u6b64\u53ef\u89c1":41,"\u7531\u8be5":53,"\u7533\u8bf7":30,"\u7535\u5b50":29,"\u7545\u6240\u6b32\u8a00":[10,16,23,30,36,44,49,56],"\u754c\u9762":32,"\u7559\u8a00":[0,58],"\u7565\u4f5c":9,"\u7565\u53bb":36,"\u767b\u5f55":[0,5,12,18,25,32,38,51],"\u767d\u57a9":52,"\u767d\u57a9\u7eaa":52,"\u767d\u8272":6,"\u767e\u5ea6":0,"\u767e\u5ea6\u7f51":0,"\u7684\u8bdd":[22,27,29,33,34,35,41,42,47,48,51],"\u76d1\u63a7":[15,34,35],"\u76d7\u9f99":52,"\u76ee\u524d":[0,5,7,8,9,15,27,28,29,34,35,39,40,41,42,47,51,52,53,55,56,58],"\u76ee\u524d\u4e3a\u6b62":[51,53,55],"\u76ee\u5f55":[0,2,7,9,12,13,23,30,36,38,43,44,45,56,60],"\u76ee\u5f55\u7d22\u5f15":42,"\u76ee\u6807":[2,3,7,9,11,18,28,30,33,36,58],"\u76ee\u6807\u7a0b\u5e8f":36,"\u76ee\u7684":5,"\u76f4\u5230":[13,21,30,34,35,40,48,55],"\u76f4\u63a5":[0,2,8,9,14,15,20,23,26,27,28,29,30,33,34,35,36,39,40,41,42,43,44,47,52,54,58,60],"\u76f4\u81f3":56,"\u76f8\u4e92":[26,51,52],"\u76f8\u4fe1":51,"\u76f8\u5173":[0,5,7,11,12,17,18,21,23,25,28,29,30,31,35,36,38,40,41,42,45,51,53,54,55,56,57,58],"\u76f8\u53cd":39,"\u76f8\u540c":[0,13,15,20,21,23,26,27,28,29,34,35,39,41,47,48,52],"\u76f8\u5bf9":[27,28,29,42,47,52,53],"\u76f8\u5bf9\u800c\u8a00":52,"\u76f8\u5e94":[9,18,22,29,33,39,47,52,56],"\u76f8\u5f53":[6,14,26,42,53],"\u76f8\u5f53\u4e8e":[14,26,42,53],"\u76f8\u6bd4":[20,29,32,35,40,48,52],"\u76f8\u7b26":33,"\u76f8\u7b49":[27,36,47],"\u76f8\u90bb":[6,14,28,29],"\u7701\u7565":29,"\u770b\u4e0d\u5230":29,"\u770b\u51fa":[2,28,42,52,53,54],"\u770b\u5230":[0,2,5,6,7,8,9,12,13,15,18,25,27,28,29,32,33,38,39,42,44,47,48,51,52,53,54,55],"\u770b\u6210":[27,29,39,41],"\u770b\u6765":[27,34,35,39,41,52],"\u770b\u6cd5":[10,16,23,30,36,44,49,56],"\u770b\u770b":[2,27,29,30,33,35,53,54,55],"\u770b\u8d77":[8,13],"\u770b\u8d77\u6765":[8,13],"\u771f\u540e":55,"\u771f\u65f6":55,"\u771f\u673a":14,"\u771f\u6b63":[2,26,27,28,29,35,36,41],"\u771f\u7684":55,"\u773c\u91cc":[53,54,55],"\u7740\u624b":7,"\u7761\u7720":[53,54,55],"\u77ac\u95f4":[15,35],"\u77e5\u8bc6":[9,26,36,58],"\u77e5\u9053":[2,6,8,14,15,22,27,28,29,34,35,39,42,48,53,55],"\u77e9\u9635":56,"\u7801\u4e3a":8,"\u7801\u4f1a":35,"\u7801\u540e":35,"\u7801\u957f":15,"\u7814\u7a76":[6,53],"\u7834\u574f":[12,29],"\u786c\u4ef6":[2,3,8,9,17,22,27,28,29,53],"\u786c\u4ef6\u5e73\u53f0":2,"\u786c\u4ef6\u8d44\u6e90":29,"\u786c\u76d8":43,"\u786e\u4fdd":[29,47,52,53],"\u786e\u5b9a":[9,28,33,39,41,42,47,48,51,53,54,55],"\u786e\u5b9a\u6027":[51,53],"\u786e\u5b9e":[6,8,15,26,55],"\u786e\u8ba4":[0,2,28,38,40,41,46,47,60],"\u78b0\u5230":[2,9],"\u78c1\u76d8":[30,38,44,45,51],"\u78c1\u76d8\u9a71\u52a8\u5668":44,"\u793a\u4f8b":[37,57,59],"\u7981\u7528":[7,56],"\u798f\u5229":0,"\u79bb\u5f00":55,"\u79cd\u79cd":42,"\u79cd\u7c7b":41,"\u79d1\u5854":52,"\u79d1\u5927":0,"\u79d1\u5b66":[0,51,54,55],"\u79d1\u5b66\u5bb6":[51,54,55],"\u79d1\u5b66\u6280\u672f":0,"\u79d2\u949f":22,"\u79ef\u6781":58,"\u79f0\u4e3a":[2,3,6,7,12,13,15,26,28,29,34,51,52,53,54],"\u79f0\u4e4b\u4e3a":2,"\u79fb\u4ea4":53,"\u79fb\u5230":52,"\u79fb\u52a8":[27,34,35],"\u79fb\u690d":[5,6,36],"\u79fb\u81f3":51,"\u79fb\u9664":[2,11,18,28,35,38,48],"\u7a0b\u5148":[52,54,55],"\u7a0b\u5e8f":[2,3,5,12,17,18,20,22,23,27,28,30,31,32,36,37,38,39,41,42,44,46,50,51,53,54,55,56,57],"\u7a0b\u5e8f\u5458":[5,55],"\u7a0b\u5e8f\u5b9e\u73b0":9,"\u7a0b\u5e8f\u5f00\u53d1":51,"\u7a0b\u5e8f\u6267\u884c":14,"\u7a0b\u5e8f\u63a7\u5236":15,"\u7a0b\u5e8f\u8bbe\u8ba1":[17,28],"\u7a0b\u5e8f\u8fd0\u884c":[12,13,14,15,23,36],"\u7a0b\u6765":52,"\u7a0b\u8981":52,"\u7a0d\u4f5c":9,"\u7a0d\u52a0":9,"\u7a0d\u540e":[9,15,21,29],"\u7a33\u5b9a":[54,55],"\u7a76\u7adf":[2,29],"\u7a7a\u4e14":27,"\u7a7a\u5206":52,"\u7a7a\u5219":47,"\u7a7a\u683c":[33,48,59],"\u7a7a\u6d1e":28,"\u7a7a\u95f2":[39,41,47],"\u7a7a\u95f4":[2,11,15,19,25,26,27,30,33,34,35,36,38,39,41,42,43,47,48,51,52],"\u7a7a\u95f4\u5e03\u5c40":[11,28,29],"\u7a7a\u95f4\u9694\u79bb":29,"\u7acb\u5373":[15,28,29,35,54,55],"\u7ade\u4e89":[34,53],"\u7ade\u6001":[51,53,55],"\u7ae0\u8282":[0,7,21,28,29,34,35,51,52,56,58,59,60],"\u7aef\u53e3":47,"\u7aef\u70b9":27,"\u7aef\u7684":47,"\u7b26\u53f7":[2,9,13,14,15,20,27,28,29,34,36,46],"\u7b26\u53f7\u8868":2,"\u7b26\u5408":[9,29,35,39,41],"\u7b26\u5408\u5b9e\u9645":39,"\u7b26\u5408\u6761\u4ef6":35,"\u7b26\u5408\u8981\u6c42":35,"\u7b2c\u4e00":[0,5,9,12,13,14,15,18,22,23,24,25,29,30,32,33,35,38,41,42,44,47,48,51,52],"\u7b2c\u4e00\u4e2a":[0,5,12,14,15,18,21,22,25,29,32,33,38,41,42,48,51,52],"\u7b2c\u4e00\u4ef6":[42,48],"\u7b2c\u4e00\u6761":9,"\u7b2c\u4e00\u6b21":[14,23,24,29,30,35,42,48,52],"\u7b2c\u4e00\u7ae0":[9,13,44],"\u7b2c\u4e09":[6,9,15,21,22,27,28,29,32,36,41,46,52],"\u7b2c\u4e09\u4e2a":[32,41,46],"\u7b2c\u4e09\u65b9":[6,9,15,36],"\u7b2c\u4e09\u7ae0":[21,22,27,28,29,32,52],"\u7b2c\u4e8c":[0,5,8,9,12,13,18,19,21,25,28,29,32,38,39,41,51],"\u7b2c\u4e8c\u4e2a":[12,25,29,41],"\u7b2c\u4e8c\u6761":9,"\u7b2c\u4e8c\u6b21":21,"\u7b2c\u4e8c\u6b65":[0,5,12,18,25,32,38,51],"\u7b2c\u4e8c\u7ae0":[8,13,19,21,29,39],"\u7b2c\u4e8c\u8282":[8,28],"\u7b2c\u4e94":[39,51,52],"\u7b2c\u4e94\u4e2a":51,"\u7b2c\u4e94\u7ae0":[39,52],"\u7b2c\u516d":42,"\u7b2c\u516d\u7ae0":42,"\u7b2c\u5341":28,"\u7b2c\u5341\u4e09":28,"\u7b2c\u5341\u4e09\u7ae0":28,"\u7b2c\u56db":[38,41],"\u7b2c\u56db\u4e2a":[38,41],"\u7b2c\u96f6":58,"\u7b49\u4e00\u7b49":9,"\u7b49\u4e8e":[28,54],"\u7b49\u4ef7":[13,34,52],"\u7b49\u5230":[39,42,47,48,51],"\u7b49\u540c":41,"\u7b49\u540c\u4e8e":41,"\u7b49\u5b8f":44,"\u7b49\u5f85":[21,23,33,34,35,47,51,53,54,55,57],"\u7b49\u7b49":35,"\u7b54\u9898":[10,23,30,36,44,49,56],"\u7b56\u7565":[27,30,33,34,53],"\u7b77\u5b50":51,"\u7b7e\u540d":27,"\u7b80\u4ecb":45,"\u7b80\u4fbf":[55,56],"\u7b80\u5316":[15,30,39,40,42,52,55],"\u7b80\u5355":[1,2,5,6,8,9,10,12,14,15,19,21,22,23,26,27,28,29,30,34,35,36,38,40,41,42,43,44,46,47,48,51,52,53,54,55,56,58],"\u7b80\u6613":45,"\u7b80\u6d01":39,"\u7b80\u7565":30,"\u7b80\u79f0":53,"\u7b80\u7b54":24,"\u7b80\u800c\u8a00\u4e4b":13,"\u7b80\u8981":[13,28],"\u7b80\u964b":8,"\u7b97\u51fa":42,"\u7b97\u662f":[22,35,52],"\u7b97\u673a":[2,9,12,14,51,53,54,55],"\u7b97\u6cd5":[18,22,34,37,41,56],"\u7ba1\u7406":[0,3,14,17,18,24,25,26,31,32,33,38,45,47,53,54,55,56,57],"\u7ba1\u7406\u5458":0,"\u7ba1\u7406\u5668":[0,14,18,24,27,29,32,33,35,37,45,52],"\u7ba1\u7406\u5b50\u7cfb\u7edf":29,"\u7ba1\u7406\u673a\u5236":[37,57],"\u7ba1\u7406\u7b56\u7565":27,"\u7ba1\u7a0b":55,"\u7ba1\u9053":[46,49,50],"\u7bc7\u5e45":[47,58],"\u7c7b\u4f3c":[15,34,35,39,41,47,52,53,54,55],"\u7c7b\u578b":[6,13,15,27,28,29,31,34,39,41,44,47,51,54],"\u7c7b\u578b\u5b9a\u4e49":31,"\u7c7b\u578b\u8f6c\u6362":26,"\u7c92\u5ea6":[27,51],"\u7c97\u7565":55,"\u7c97\u7565\u5730":55,"\u7cbe\u7b80":28,"\u7cdf\u7cd5":12,"\u7cfb\u5217":[3,8,15,22,31,36,51,53,54],"\u7cfb\u7edf":[0,3,5,6,7,8,9,12,15,18,20,23,24,27,28,29,30,32,34,36,37,38,44,46,48,50,51,56,57],"\u7d22\u5f15":[26,27,38,40,45],"\u7d27\u63a5":28,"\u7d27\u63a5\u7740":28,"\u7d2f\u52a0":36,"\u7e41\u7410":49,"\u7ea0\u6b63":36,"\u7ea6\u5b9a":[9,13,14,55],"\u7ea6\u675f":[41,53],"\u7ea7\u522b":5,"\u7ea7\u9875":27,"\u7eb5\u8f74":21,"\u7ebf\u4e0b":[0,5,12,18,25,32,38,51],"\u7ebf\u6027":[29,39],"\u7ebf\u6027\u8868":29,"\u7ebf\u7a0b":[30,53,54,55,56,57],"\u7ec3\u4e60":[0,5,12,24,31,37,45,50,57],"\u7ec3\u4e60\u9898":44,"\u7ec4\u5185":41,"\u7ec4\u5408":[33,41],"\u7ec4\u6210":[13,30,31,41,51,53,54,55],"\u7ec4\u6210\u90e8\u5206":[51,55],"\u7ec4\u7ec7":[38,52],"\u7ec6\u5316":52,"\u7ec6\u7c92":51,"\u7ec6\u7c92\u5ea6":51,"\u7ec6\u81f4":58,"\u7ec6\u8282":[9,15,21,28,36,41,52,58],"\u7ec8\u4e8e":[7,21,28,39,42],"\u7ec8\u6b62":[13,27,28,34,35],"\u7ec8\u7a76":8,"\u7ec8\u7aef":[0,2,32],"\u7ecf\u5178":[41,51],"\u7ecf\u5e38":[0,34],"\u7ecf\u8fc7":[2,26,27,28,29,48],"\u7ecf\u9a8c":58,"\u7ed1\u5b9a":[13,27,28,34],"\u7ed3\u5408":52,"\u7ed3\u5c3e":[15,33,34,48],"\u7ed3\u675f":[0,9,12,13,14,15,27,28,29,33,34,35,41,42,54,56,57],"\u7ed3\u6784":[6,8,14,15,17,21,22,23,28,29,31,32,35,36,37,38,40,42,44,45,47,51,53,54,55,56,57,58,60],"\u7ed3\u679c":[2,8,9,13,21,35,51,53,55],"\u7ed3\u8bba":36,"\u7ed5\u8fc7":[27,29],"\u7ed9\u51fa":[2,3,8,9,15,20,23,26,27,28,33,35,40,41,55,58,59],"\u7ed9\u5b50":46,"\u7edd\u5bf9":42,"\u7edd\u5bf9\u8def\u5f84":42,"\u7edf\u4e00":[28,39],"\u7edf\u8ba1":[29,41],"\u7ee7\u627f":[39,47],"\u7ee7\u7eed":[13,15,20,21,29,35,47,51,53,54,55,56,59],"\u7ee7\u7eed\u6267\u884c":[13,15,20,21,29,35,51,54,55],"\u7ef4\u62a4":[21,22,23,27,28,29,33,34,35,38,41,43,47,52],"\u7eff\u8272":[0,5,12,18,21,25,32,38,51],"\u7f13\u51b2":[13,15,29,33,38,39,40,41,43,47],"\u7f13\u51b2\u533a":[13,15,29,33,38,39,40,41,43,47],"\u7f13\u5b58":[13,14,29,38,42,45],"\u7f16\u5199":[5,7,9,15,19,20,22,35,41,48,58],"\u7f16\u53f7":[13,21,28,34,41,42,43,44,48],"\u7f16\u7801":[15,23,27,28,42,43],"\u7f16\u7a0b":[5,7,8,13,15,21,24,28,31,37,39,45,50,55,57,58],"\u7f16\u7a0b\u5e8f":[7,8],"\u7f16\u7a0b\u8bed\u8a00":[13,15,39,55],"\u7f16\u8bd1":[0,2,5,6,7,8,9,14,17,19,20,23,25,26,27,28,29,34,39,41,53,55],"\u7f16\u8bd1\u5668":[0,2,5,6,7,8,9,13,20,26,27,28,29,34,39,41,55],"\u7f16\u8bd1\u7a0b\u5e8f":6,"\u7f16\u8f91":[0,51],"\u7f16\u8f91\u5668":[0,51],"\u7f16\u8f91\u6587\u6863":51,"\u7f18\u6545":29,"\u7f29\u51cf":[28,41],"\u7f29\u77ed":58,"\u7f3a\u5931":29,"\u7f3a\u5c11":[2,7],"\u7f3a\u70b9":[29,42],"\u7f3a\u9875":30,"\u7f51\u4e0a":36,"\u7f51\u7ad9":0,"\u7f51\u7edc":[0,5,12,18,25,32,38,51],"\u7f51\u9875":[0,5,12,18,25,32,38,51],"\u7f6e\u4e8e":35,"\u7f8e\u89c2":59,"\u7f8e\u89c2\u5927\u65b9":59,"\u7fa4\u5185":60,"\u7fbd\u6bdb":52,"\u7ffb\u8bd1":2,"\u8001\u5927":52,"\u8001\u5e08":0,"\u8003\u8651":[7,9,13,15,22,23,29,30,35,36,40,41,44,51,53,54,56],"\u8003\u8651\u4e00\u4e0b":[53,54],"\u8003\u8bd5":58,"\u8003\u9898":58,"\u800c\u4e14":[15,27,29,52,53,54],"\u800c\u540e":[2,28,40],"\u800c\u5b50":[35,47],"\u800c\u5b9a":41,"\u800c\u662f":[15,26,27,28,29,30,35,42,52],"\u800c\u7236":35,"\u800c\u8a00":[13,15,35,41,42,51,52,53,55],"\u8017\u5c3d":27,"\u8026\u5408":45,"\u804c\u4e1a":5,"\u804c\u4e1a\u751f\u6daf":5,"\u804c\u80fd":34,"\u8054\u7cfb":60,"\u8054\u7f51":0,"\u80cc\u666f":[9,26,36,58],"\u80fd\u4ee5":12,"\u80fd\u529b":[13,29,39,51,53,55],"\u80fd\u5426":29,"\u80fd\u591f":[2,3,5,8,14,15,21,23,27,28,29,33,34,35,38,41,42,47,48,51,52,53,54,55],"\u811a\u672c":[0,2,5,9,12,13,14,19,28],"\u811a\u7c7b":52,"\u8131\u79bb":5,"\u817e\u51fa":15,"\u81a8\u80c0":42,"\u81ea\u4e0a\u800c\u4e0b":6,"\u81ea\u4e0b\u800c\u4e0a":41,"\u81ea\u4ece":[26,41],"\u81ea\u521b":30,"\u81ea\u52a8":[0,5,12,14,15,18,21,25,26,27,28,32,34,38,41,46,47,51],"\u81ea\u589e":[29,41],"\u81ea\u5b9a":28,"\u81ea\u5b9a\u4e49":28,"\u81ea\u5df1":[0,2,3,5,7,9,12,18,25,26,28,29,32,33,34,38,42,48,51,52,55,60],"\u81ea\u5e26":52,"\u81ea\u7136":[15,27,28,39,41,52,53],"\u81ea\u884c":[0,9,23,41,43,47,48],"\u81ea\u8eab":[0,27,33,34,39,41,42,43,47,51],"\u81f3\u4e8e":[22,29,36],"\u81f3\u5173":5,"\u81f3\u5173\u91cd\u8981":5,"\u81f3\u5c11":[23,35,42],"\u81f3\u6b64":[7,9,52],"\u8270\u96be":25,"\u8282\u6570":[33,39,41],"\u8282\u67e5":35,"\u8282\u70b9":[27,28,29,38,45],"\u8282\u7ea6":[29,41],"\u82af\u7247":0,"\u82e5\u5e72":[2,13,18,32,34,41],"\u82e5\u5e72\u4e2a":[2,41],"\u8303\u56f4":[0,41,54],"\u8377\u5170":54,"\u8377\u5170\u8bed":54,"\u83b7\u53d6":[3,5,8,14,15,21,22,24,25,27,28,29,32,33,34,37,38,39,41,43,44,45,46,47,48,51,53,55,56],"\u83b7\u5f97":[21,26,29,34,35,36,39,40,52,56],"\u84dd\u8272":[21,48],"\u865a\u5b58":30,"\u865a\u5b9e":31,"\u865a\u62df":[0,2,5,9,25,27,31,32,35,38,41,43,51,52,60],"\u865a\u62df\u5185\u5b58":[25,31],"\u865a\u62df\u5316":52,"\u865a\u62df\u5730\u5740":[27,28,31,35,51],"\u865a\u62df\u673a":[0,2,5,43],"\u865a\u62df\u73af\u5883":60,"\u865a\u8868":39,"\u867d\u7136":[7,9,13,15,27,28,29,36,47,52,53,55,58],"\u884c\u4e2d":[2,26,53],"\u884c\u4e3a":[9,23,33,44,52,55],"\u884c\u4ece":[28,47],"\u884c\u4f7f":26,"\u884c\u5219":[26,28,29],"\u884c\u5230":53,"\u884c\u52a8":52,"\u884c\u5728":15,"\u884c\u5c06":[34,35,42,47,48,52],"\u884c\u6240":[35,48,52],"\u884c\u6587":[6,7,8,9,13,28,33,34,35,38],"\u884c\u662f":2,"\u884c\u7a0b":[2,6,7,8,9,42],"\u884c\u95f4":59,"\u8865\u5145":[29,47],"\u8865\u5168":36,"\u8868\u4e2d":[39,43,47,48],"\u8868\u660e":[6,28,39,43],"\u8868\u6765":39,"\u8868\u683c":59,"\u8868\u73b0":30,"\u8868\u793a":[6,9,13,14,15,21,26,27,28,29,30,33,34,36,39,40,41,42,43,47,48,52,53,54,55,56],"\u8868\u793a\u547d\u4ee4":48,"\u8868\u8fbe":13,"\u8868\u8fbe\u80fd\u529b":13,"\u8868\u9762":5,"\u88ab\u52a8":20,"\u88c1\u526a":28,"\u88c5\u6ee1":41,"\u88c5\u8f7d":42,"\u88f8\u673a":[0,5,6,11],"\u8981\u4e48":[15,35,51,53],"\u8981\u6c42":[0,9,15,19,22,24,27,28,29,31,35,37,41,45,50,57],"\u8981\u7d20":52,"\u8986\u76d6":[15,21,28,32],"\u89c1\u5230":14,"\u89c1\u8bc6":20,"\u89c2\u5bdf":23,"\u89c4\u5219":[2,22,55],"\u89c4\u5b9a":[3,7,13,26,39],"\u89c4\u8303":[3,9,13,15,20,29],"\u89c6\u4e3a":[26,27,28,29,41,56],"\u89c6\u4f5c":26,"\u89c6\u89d2":[27,29],"\u89c9\u5f97":[8,58],"\u89d2\u5ea6":[47,53],"\u89e3\u51b3":[0,2,29,36,51,54,55,58],"\u89e3\u538b":0,"\u89e3\u6790":[2,8,14,28,29,33,35,41,42,43],"\u89e3\u91ca":[18,20,23,28,29,41],"\u89e6\u53ca":28,"\u89e6\u53d1":[13,14,15,22,26,27,28,29,35,41],"\u8b66\u544a":59,"\u8ba1\u6570":[22,23,34,35,41,47,52,54],"\u8ba1\u6570\u5668":[22,52],"\u8ba1\u65f6":[18,24],"\u8ba1\u65f6\u5668":[18,24],"\u8ba1\u7b97":[2,9,12,13,14,19,22,28,29,34,41,42,47,51,53,54,55],"\u8ba1\u7b97\u673a":[2,9,12,14,51,53,54,55],"\u8ba1\u7b97\u8d44\u6e90":12,"\u8ba4\u4e3a":[14,27],"\u8ba4\u5b9a":26,"\u8ba8\u8bba":[0,28,60],"\u8ba8\u8bba\u533a":[0,60],"\u8ba9\u51fa":33,"\u8bb0\u5f55":[14,15,20,26,28,35,39,40,41,42,47,48,52],"\u8bb2\u4e49":58,"\u8bb2\u89e3":[28,52,53,55],"\u8bb8\u591a":2,"\u8bbe\u4e3a":27,"\u8bbe\u5907":[3,8,38,39,42,45],"\u8bbe\u5b9a":[15,29,36],"\u8bbe\u7f6e":[0,5,7,11,13,14,15,19,21,22,26,27,28,29,35,36,39,40,41,42,47,52,53,54,55,56],"\u8bbe\u8ba1":[10,12,16,17,22,23,24,28,29,30,34,36,37,38,39,42,44,45,49,53,54,55,56,57],"\u8bbf\u5b58":[14,15,26,28,29],"\u8bbf\u95ee":[9,13,23,26,28,29,30,31,34,35,38,39,40,41,42,43,47,48,51,52,53,54,55],"\u8bbf\u95ee\u5171\u4eab":[51,53,55],"\u8bc1\u660e":36,"\u8bc4\u4ef7":53,"\u8bc4\u5206":[0,5,12,18,25,32,38,51],"\u8bc4\u6d4b":[0,5,12,18,25,32,38,46,51],"\u8bc4\u8bba":58,"\u8bc6\u522b":53,"\u8bcd\u6cd5":41,"\u8bd5\u4e00\u8bd5":36,"\u8bd5\u56fe":[23,27,33,35,41,48,51],"\u8bd5\u7528":40,"\u8bd5\u770b":6,"\u8bd5\u8bd5":6,"\u8bd5\u8bd5\u770b":6,"\u8bd5\u9a8c":9,"\u8be5\u503c":54,"\u8be5\u6cdb":41,"\u8be5\u7c7b":56,"\u8be6\u60c5":30,"\u8be6\u7ec6":[8,27,28,41],"\u8be6\u89c1":[9,25,36],"\u8bed\u4e49":[5,11,26,30,35,43,47,52,55],"\u8bed\u53e5":[8,53,55],"\u8bed\u6cd5":[2,9,21,28,39,41,58,60],"\u8bed\u8a00":[3,6,8,13,15,28,39,48,53,55,58],"\u8bef\u4fe1":[15,33],"\u8bef\u5dee":36,"\u8bf4\u660e":[8,13,14,15,21,23,27,29,30,33,36,39,41,44,47,48,52,53,56],"\u8bf4\u6cd5":[28,30],"\u8bf7\u4ee5":5,"\u8bf7\u6c42":[8,21,39,41,52,56],"\u8bf7\u6c42\u8005":41,"\u8bf7\u95ee":30,"\u8bfb\u5165":[35,39,41,42,47],"\u8bfb\u5199":[15,27,28,29,41,43,45,50,53,55],"\u8bfb\u5199\u64cd\u4f5c":15,"\u8bfb\u51fa":40,"\u8bfb\u51fa\u6765":40,"\u8bfb\u5230":[33,41],"\u8bfb\u53d6":[13,22,29,33,38,39,40,41,42,47,51],"\u8bfb\u53d6\u6570\u636e":[39,47],"\u8bfb\u7aef":47,"\u8bfb\u8005":[0,5,8,13,15,23,28,34,36,39,41,47,48,58],"\u8bfe\u5802":40,"\u8bfe\u7a0b":[1,6,58,60],"\u8c03\u5ea6":[18,19,23,24,37,51,53,55,57],"\u8c03\u6574":[0,8,9,13,28,29,32,34,36,44,51,52],"\u8c03\u7528":[6,7,8,9,12,14,15,17,18,20,22,23,24,27,28,29,30,32,34,36,37,41,42,43,44,45,46,48,50,51,56,57],"\u8c03\u7528\u51fd\u6570":23,"\u8c03\u7528\u8005":[20,28,41],"\u8c03\u8bd5":[2,5,44],"\u8c03\u8bd5\u4fe1\u606f":2,"\u8c03\u8bd5\u5668":0,"\u8d1f\u8d23":[14,15,18,19,28,34,41,42,52],"\u8d1f\u8d23\u7ba1\u7406":[34,52],"\u8d26\u53f7":0,"\u8d26\u6237":0,"\u8d44\u6599":[9,36,43],"\u8d44\u6e90":[12,15,18,21,27,29,32,33,34,36,37,38,39,42,47,51,52,53,54,55,56],"\u8d44\u6e90\u5206\u914d":[52,56],"\u8d4b\u503c":[26,55],"\u8d56\u4e8e":[2,5,6,14,27],"\u8d58\u8ff0":[29,41,47,52],"\u8d6b\u5179":22,"\u8d77\u521d":23,"\u8d77\u5230":[15,29],"\u8d77\u56e0":[17,51,54],"\u8d77\u59cb":[2,9,13,14,15,19,27,28,29,30,33,41,42,43,47,48,52],"\u8d77\u6765":[0,8,13,25,27,29,39,42,59],"\u8d77\u6e90":57,"\u8d77\u7740":44,"\u8d77\u89c1":56,"\u8d85\u51fa":[13,19,41,47],"\u8d85\u7ea7":[42,45],"\u8d85\u8fc7":[22,28,40,41],"\u8d8a\u6765":[41,51],"\u8d8a\u6765\u8d8a":[41,51],"\u8d8a\u8fc7":9,"\u8db3\u591f":[41,47,56],"\u8ddd\u4eca":52,"\u8ddd\u79bb":23,"\u8ddf\u7740":5,"\u8ddf\u8e2a":2,"\u8def\u5f84":[0,2,42,44],"\u8def\u7ebf":52,"\u8df3\u51fa":55,"\u8df3\u52a8":0,"\u8df3\u677f":[25,28,31,35,52],"\u8df3\u8f6c":[9,13,15,20,25,29,34,35],"\u8df3\u8fc7":[0,15,23,29],"\u8eab\u4efd":41,"\u8f6c\u4e3a":27,"\u8f6c\u50a8":8,"\u8f6c\u5230":53,"\u8f6c\u5316":[15,27,29,35,39,41,47,48],"\u8f6c\u5316\u6210":[15,27],"\u8f6c\u56de":56,"\u8f6c\u6210":[9,27],"\u8f6c\u6362":[26,27,28,29,52],"\u8f6c\u6362\u6210":[26,27],"\u8f6c\u6362\u673a\u5236":28,"\u8f6c\u79fb":[26,41],"\u8f6c\u800c":[7,20],"\u8f6e\u5230":33,"\u8f6e\u6b21":47,"\u8f6e\u6d41":[23,54],"\u8f6e\u8be2":41,"\u8f6e\u8f6c":[18,22],"\u8f6f\u4ef6":[0,9,15,28,51,53],"\u8f6f\u4ef6\u5de5\u5177":0,"\u8f6f\u786c":40,"\u8f7b\u677e":[9,28,35],"\u8f7b\u91cf":53,"\u8f7d\u5165":[2,29,41],"\u8f83\u4e3a":[27,46,56],"\u8f83\u4e3a\u7b80\u5355":27,"\u8f83\u5927":[39,56],"\u8f83\u6162":51,"\u8f85\u52a9":[26,29,35,41],"\u8f93\u5165":[0,2,6,7,12,27,32,33,36,37,38,45,46,50],"\u8f93\u5165\u8f93\u51fa":[38,39,50],"\u8f93\u51fa":[0,5,11,12,13,18,29,38,44,45,46,50],"\u8f93\u51fa\u8bbe\u5907":39,"\u8fb9\u754c":41,"\u8fb9\u754c\u6761\u4ef6":41,"\u8fbe\u5230":[41,51,53],"\u8fbe\u79d1":52,"\u8fbe\u79d1\u5854":52,"\u8fc7\u53bb":[28,42],"\u8fc7\u591a":[19,26,28],"\u8fc7\u5927":29,"\u8fc7\u5934":[29,39],"\u8fc7\u5ea6":42,"\u8fc7\u671f":29,"\u8fc7\u6765":[15,42],"\u8fc7\u6e21":29,"\u8fc7\u7a0b":[0,5,11,12,15,18,21,22,25,27,28,29,32,34,35,36,38,41,42,43,47,48,51,52,53,54,55,56],"\u8fd0\u7b97":26,"\u8fd0\u884c":[2,3,5,6,7,8,9,12,13,14,15,18,19,20,22,23,24,25,28,29,32,34,35,36,38,39,41,44,46,51,52,53,55,56],"\u8fd4\u56de":[8,13,15,20,21,22,23,27,28,29,30,33,34,35,36,39,40,41,42,43,44,47,48,52,53,54,56],"\u8fd4\u56de\u503c":[8,13,15,21,22,23,27,30,33,35,36,40,41,44,47,48,52,56],"\u8fd8\u4f1a":[21,33,51,52],"\u8fd8\u539f":[2,15,50],"\u8fd8\u662f":[8,29,41,52,53,54,55],"\u8fd8\u6709":[2,5,6,13,15,22,27,29,39,41,47,58],"\u8fd8\u7ed9":52,"\u8fd8\u8981":[27,36,42],"\u8fd9\u4e00":[27,28,52,55],"\u8fd9\u4e09\u6761":29,"\u8fd9\u4e2a":[0,2,3,5,7,8,12,13,14,15,18,21,23,25,26,27,28,29,30,32,33,34,35,36,38,39,41,42,43,44,47,48,51,52,53,54,55,59],"\u8fd9\u4e48":[29,36,55],"\u8fd9\u4e9b":[2,6,9,12,15,18,20,21,22,23,26,27,28,29,30,34,39,41,42,47,48,52,53,56],"\u8fd9\u4f1a":[29,42],"\u8fd9\u53f0":9,"\u8fd9\u5757":9,"\u8fd9\u5c31\u662f\u8bf4":29,"\u8fd9\u65f6":[14,15,47],"\u8fd9\u662f":[15,27,28,29,35,41,42,47,52,58,59],"\u8fd9\u6761":[15,53],"\u8fd9\u6837":[0,2,6,7,8,13,14,15,21,22,27,28,29,30,34,35,39,41,42,48,52,53,54,55],"\u8fd9\u6b21":[15,35],"\u8fd9\u6bb5":[20,29],"\u8fd9\u79cd":[7,15,19,27,28,29,34,35,51,52,53,54,55,58],"\u8fd9\u7ae0":36,"\u8fd9\u884c":7,"\u8fd9\u8d9f":5,"\u8fd9\u91cc":[0,2,8,9,14,15,21,26,27,28,29,30,32,34,35,36,38,41,42,43,47,48,51,52,53,54,55,59,60],"\u8fd9\u9879":7,"\u8fdb\u4e00\u6b65":[0,5,9,12,13,18,20,25,27,32,33,38,39,41,43,46,51,52],"\u8fdb\u5165":[0,5,12,13,18,22,23,24,25,29,32,34,35,38,46,47,48,51,52,53,54,55,59],"\u8fdb\u5236":[3,7,9,17,28,29,39,56],"\u8fdb\u53bb":[29,48],"\u8fdb\u6765":[15,28,29,34,35,42,48],"\u8fdb\u7a0b":[28,30,32,38,39,41,43,46,47,48,49,51,53,54,55,56,57],"\u8fdb\u7a0b\u540c\u6b65":51,"\u8fdb\u800c":29,"\u8fdb\u884c":[0,2,5,7,8,9,12,13,14,15,18,20,21,22,25,26,27,28,29,30,32,33,34,35,36,38,39,40,41,42,43,44,47,48,51,52,53,55,56],"\u8fdb\u9636":58,"\u8fdb\u9910":51,"\u8fde\u63a5":41,"\u8fde\u7eed":[22,29,30,31,41],"\u8fde\u7eed\u6027":29,"\u8fde\u8d2f":58,"\u8fed\u4ee3":[21,27,28,35,38,39,47],"\u9000\u51fa":[0,2,9,11,13,15,21,27,29,30,33,34,36,37,42,51,55,56,57],"\u9000\u53bb":52,"\u9000\u683c":33,"\u9002\u4e2d":36,"\u9002\u5f53":[2,41],"\u9002\u7528":0,"\u9002\u914d":3,"\u9009\u51fa":34,"\u9009\u53d6":[13,39,41],"\u9009\u62e9":[0,5,6,12,18,20,25,29,30,32,36,38,51,54,55,58],"\u9009\u7528":41,"\u9009\u9879":[0,19],"\u900f\u660e":[20,43,48],"\u9010\u4e2a":[20,42,48],"\u9010\u5b57":[35,39,47,48],"\u9010\u6b65":[41,53],"\u9010\u7ea7":42,"\u9010\u9875":28,"\u9012\u5f52":28,"\u9014\u5f84":55,"\u901a\u4fd7":30,"\u901a\u4fe1":[46,47,49],"\u901a\u5e38":[0,2,14,29,54],"\u901a\u7528":[3,6,9,15,29,52],"\u901a\u7528\u5bc4\u5b58\u5668":[9,15,29,52],"\u901a\u8fc7":[0,2,5,6,7,9,12,13,15,20,22,23,26,27,28,29,30,32,33,34,35,36,38,39,40,41,42,44,45,46,48,50,51,52,54,55,56],"\u901a\u9053":39,"\u901f\u5ea6":[0,52],"\u9020\u6210":[15,21,42],"\u903b\u8f91":[5,14,15,23,25,29,31,33,34,35,42,44,51,55],"\u9047\u5230":[0,7,15,21,27],"\u904d\u5386":[27,28,35,39,41,43,47,52],"\u9075\u4ece":41,"\u907f\u514d":[0,9,21,22,28,29,34,41,42,51,55],"\u907f\u514d\u51fa\u73b0":51,"\u9080\u8bf7":[0,5,12,18,25,32,38,51],"\u90a3\u4e2a":[47,53],"\u90a3\u4e48":[2,5,13,15,21,27,29,33,36,39,41,44,47,48,51,52,53,54,55],"\u90a3\u4e9b":[8,27,28,29,35,39,41,42],"\u90a3\u6761":15,"\u90a3\u6837":[26,28,29,55],"\u90a3\u91cc":15,"\u90e8\u5206":[0,1,2,6,7,8,9,13,14,15,20,22,23,26,27,28,29,32,34,35,38,41,43,51,52,53,55],"\u914d\u5408":55,"\u914d\u5668":[25,27,29,34,36,48],"\u914d\u7f6e":[2,5,11,12,18,23,25,28,32,38,43,51,58],"\u914d\u7f6e\u6587\u4ef6":[5,9,23],"\u9192\u6765":[54,55],"\u9192\u76ee":[0,5,12,18,25,32,38,51],"\u91c7\u53d6":30,"\u91c7\u7528":[0,7,26,27,28,29,41,52,54,55,59],"\u91c7\u7eb3":55,"\u91ca\u653e":[47,53,55,56],"\u91cc\u9762":[2,7,13,14,15,21,27,35,38,39,41,47,48,51],"\u91cd\u5199":[9,31,39],"\u91cd\u542f":0,"\u91cd\u590d":[9,14],"\u91cd\u5b9a\u5411":[46,50],"\u91cd\u65b0":[0,7,8,15,22,23,30,36,37,51],"\u91cd\u6784":58,"\u91cd\u70b9":52,"\u91cd\u73b0":55,"\u91cd\u8981":[0,5,12,18,20,21,25,27,28,30,32,35,37,38,41,46,48,51,54,55,57],"\u91cf\u503c":54,"\u91cf\u7ea7":[22,30],"\u9488\u5bf9":[13,27,29,55],"\u94fe\u4e2d":0,"\u94fe\u63a5":[0,2,5,6,9,12,13,17,18,19,21,25,28,29,32,37,38,40,42,45,51,59],"\u9500\u6bc1":[21,47,52],"\u9501\u4e0a":53,"\u9501\u65f6":53,"\u9501\u6765":34,"\u9501\u7528":54,"\u9519\u8bef":[7,8,9,12,13,15,23,28,29,30,33,36,40,44,47,48,52,55,56,58,59],"\u9519\u8bef\u4fe1\u606f":[15,33],"\u9519\u8bef\u5904\u7406":58,"\u952e\u503c":[27,28,29],"\u952e\u76d8":35,"\u952e\u76d8\u8f93\u5165":35,"\u955c\u50cf":[0,13,14,28,29,41,43,45],"\u955c\u50cf\u6587\u4ef6":[2,13,14,42],"\u957f\u4e3a":23,"\u957f\u5ea6":[2,13,15,26,28,29,30,33,35,36,39,41,43,47,48],"\u957f\u6ee1":52,"\u95ed\u5305":41,"\u95ed\u5305\u65f6":41,"\u95ee\u597d":5,"\u95ee\u7b54":[16,23,31,37,45,50,57],"\u95ee\u7b54\u9898":[10,16,23,30,36,44,49,56],"\u95ee\u9898":[0,2,8,9,15,23,24,28,29,30,36,42,51,53,55,56,60],"\u95f4\u63a5":[15,41],"\u95f4\u901a\u4fe1":46,"\u95f4\u9694":29,"\u9605\u8bfb":[0,13,23,28,41,58],"\u9610\u8ff0":12,"\u9610\u91ca":9,"\u961f\u5217":[34,36,41,47,52,53,54,55],"\u961f\u5934":47,"\u961f\u5c3e":[34,35,47],"\u9632\u6b62":14,"\u9632\u8303":30,"\u9636\u6bb5":[32,41],"\u963b\u585e":[51,53,54,55],"\u963b\u6b62":53,"\u9644\u52a0":[15,27],"\u9644\u8fd1":29,"\u964d\u4f4e":[29,56,58],"\u9650\u4e8e":39,"\u9650\u5236":[28,29,41,53],"\u9650\u671f":51,"\u9664\u4e86":[2,5,6,7,23,28,29,41,47,51],"\u9664\u4ee5":[22,41,42],"\u9664\u975e":[0,5,12,15,18,25,32,38,44,46,48,51],"\u9677\u5165":[15,51,52],"\u968f\u4fbf":30,"\u968f\u5373":[21,34],"\u968f\u540e":[0,27,29,35,41,42,47],"\u968f\u610f":23,"\u968f\u65f6":53,"\u968f\u673a":40,"\u968f\u7740":[0,35,43,51],"\u9690\u542b":[26,39],"\u9690\u5f0f":29,"\u9690\u60a3":15,"\u9694\u5f00":59,"\u9694\u79bb":[12,28,29,52],"\u96be\u4ee5":[28,53],"\u96be\u4ee5\u786e\u5b9a":28,"\u96be\u514d":12,"\u96be\u514d\u4f1a":12,"\u96be\u5ea6":[10,16,23,30,36,44,49,56],"\u96c6\u5408":[26,39,51,52,56],"\u9700\u4e14":23,"\u9700\u4e3a":27,"\u9700\u5757":41,"\u9700\u6709":27,"\u9700\u6c42":[2,6,22,23,56],"\u9700\u7528":41,"\u9700\u8981":[0,2,5,6,7,8,9,10,12,13,14,15,16,18,20,21,22,23,25,26,27,28,29,30,32,33,34,35,36,38,39,40,41,42,43,44,46,47,48,49,51,52,54,55,56,57,59,60],"\u9732\u51fa":14,"\u9738\u738b":52,"\u9738\u738b\u9f99":52,"\u9759\u6001":[27,29,41],"\u975e\u5e38":[15,21,27,28,29,36,47,48,49,52,55,59],"\u975e\u5e38\u5bb9\u6613":27,"\u975e\u5e38\u7b80\u5355":[15,27,28,36,47,48,52],"\u975e\u5fc5\u8981":58,"\u975e\u6cd5":[13,15],"\u975e\u8d1f":39,"\u9760\u8fd1":51,"\u9762\u5411":[54,55],"\u9762\u5bf9":28,"\u9875\u6a21\u5f0f":[26,28,31],"\u9875\u8868":[25,28,29,30,31,35,43,52],"\u9875\u9762":[26,27,28,29,30,35,38],"\u9875\u9762\u5931\u6548":30,"\u9879\u4e0a":26,"\u9879\u4e2d":[26,28,41,42],"\u9879\u5b9a":27,"\u9879\u624d":26,"\u9879\u76ee":[0,2,3,5,6,9,12,17,18,25,29,32,38,46,51],"\u987a\u5229":[29,34,39,56],"\u987a\u5e26":[28,33],"\u987a\u5e8f":[2,13,14,15,27,28,34,41,45,47,52,55],"\u9884\u5148":[15,22,36,42],"\u9884\u671f":[5,9,53],"\u9884\u6d4b":55,"\u9884\u7559":[9,13,15,19,22,28,48],"\u9884\u8ba1":29,"\u9884\u8bbe":2,"\u9886\u57df":51,"\u9891\u7387":22,"\u9891\u7e41":41,"\u9898\u76ee":58,"\u989d\u5916":[5,29,41],"\u98ce\u6247":9,"\u98ce\u683c":[21,28],"\u98ce\u9669":15,"\u98de\u884c":52,"\u9965\u997f":51,"\u9996\u5148":[0,2,5,7,8,12,15,21,22,26,27,29,33,35,39,41,42,43,47,48,52,53,54,55],"\u9996\u6b21":[0,5,12,18,25,32,38,46,51],"\u9a6c\u4e0a":[29,30,55],"\u9a71\u52a8":[38,41,44,45],"\u9a71\u52a8\u5668":44,"\u9a7b\u7559":[19,34,41],"\u9a8c\u8bc1":[22,41],"\u9ad8\u4eae":[2,9,43],"\u9ad8\u4f4d":53,"\u9ad8\u5230":[27,28],"\u9ad8\u5c42":55,"\u9ad8\u7ea7":[53,54],"\u9ad8\u9875":28,"\u9ad8\u9891":29,"\u9b54\u6570":[2,41],"\u9e4f\u57ce":3,"\u9ed1\u76d2":8,"\u9ed1\u8272":6,"\u9ed8\u8ba4":[0,6,7,15,22,23,26,28,48],"\u9f99\u79d1":52,"_+":2,"_______..______":[0,12,18,32],"_______.___________.":[0,12,18,32],"alpha.4":[12,18,32],"break":[27,28,35,39,41,43,48],"bus.0":43,"byte":[8,27,28,29,35,39,40,41,42,47,48],"c++":39,"char":[33,35,44],"class":2,"const":[8,9,13,14,15,19,20,21,22,26,27,28,29,33,34,35,36,39,40,41,43,44,47,48,54,55],"do":[35,43,52],"enum":[21,28,41,47],"export":[0,8],"for":[6,8,9,19,21,26,27,28,29,34,35,36,39,41,42,43,47,48,52,53,54,56],"function":[2,7,26,53],"goto":[21,29,35,39],"handle.0":34,"if":[14,21,27,28,33,34,35,39,41,42,43,47,48,52,53,54,55,56],"import":59,"in":[1,2,6,7,8,9,12,13,15,19,21,27,28,29,34,35,39,41,42,43,46,47,48,52,53],"int":[44,53,56],"l.0":27,"libglib2.0":0,"long":42,"new":[2,6,14,21,26,27,28,29,33,34,35,39,41,42,43,47,48,52,53,54,55],"null":44,"pa.0":27,"pair.0":41,"pair.1":41,"pid.0":[34,35],"ppn.0":[26,27,29],"r.0":27,"return":[27,28,29,33,34,35,39,41,42,43,47,48,52],"self.0":[26,27,34],"short":42,"so.2":2,"static":[0,14,21,27,28,29,34,35,39,43,47,48,53,54,55],"super":[35,41,42],"switch":[18,20,21,25,29,32,34,35,38,46,47,51],"true":[36,39,41,42,43,47,52,53,56],"ubuntu18.04":0,"v.0":26,"v0.1":[2,6,7,8,9],"v1.0":0,"void":53,"vpn.0":28,"while":[29,34,52,55],"with":[0,12,15,18,21,28,29,32,33,34,35,38,42,46,47,48,51,52],_0:15,__:[0,2,12,15,18,20,21,23,29,32,34,47],_____:[0,12,18,32],______:[0,12,18,32],_______:[0,12,18,32],___________:[0,12,18,32],__alltrap:[15,23,29],__libc_csu_fini:2,__libc_csu_init:2,__restor:[15,21,23,29],__rustc_debug_gdb_scripts_section__:2,__switch:[18,20,21,34,47],_app_nam:34,_data_block:41,_end:[14,34],_fs:42,_global_offset_table_:2,_info:7,_num_app:[14,19,28],_start:[2,7,8,9,13,14,28,34,48],_tz:22,_unus:[21,35,52],_user_buf:39,_zn2os4main17h717a6a6e05a70248:2,_zn3std2rt10lang_start17hc258028f546a93a1:2,a0:[8,13,15,20,21,23,29,35,48,59],a1:[20,29,48],a2:[13,15],a4:53,a5:53,a6:13,a7:[13,15],aa:48,aaa:51,aaaaaaaaaa:18,abi:[2,4,13],abivers:2,ac:2,access:[4,14,15,21,26,27,29,34,35,48,52,53,54,55,56],acm:55,acquir:[39,42,47,48],acquire_inner_lock:[39,47,48],activ:[29,60],adapt:0,add:[14,19,28,34,35,43,47,48,52,53,54,55,56,59],add_initproc:35,add_task:[34,35,52,53,54,55,56],adder:53,addi:[15,53],addiw:53,addq:2,addr:[2,9,15,19,28,35,41,43,48],addr_of_offset:41,address:[0,2,9,13,14,15,19,25,26,27,28,32,38,46,51],addressalign:2,addresss:[2,7],advanc:4,after:[15,18,35,54],again:35,al:2,align:[9,14,15,28,29,48,53],all:[2,9,12,14,18,21,28,42,43,47,48,52],all_data:[42,43,48],all_write_ends_clos:47,allen:54,alloc:[2,15,25,27,28,29,32,33,34,35,36,38,41,42,46,47,48,51,52,56],alloc_data:42,alloc_fd:[47,48],alloc_inod:42,alltrap:[15,23,29],alpha:[12,18,32,59],alreadi:14,also:28,altmacro:[15,20],am:46,american:54,an:[35,42,59],analyz:0,and:[2,4,15,21,22,28,29,35,39,41,47,52,53,54,55],andq:2,ani:[35,41],anoth:35,anywai:35,api:33,app:[12,14,15,18,19,21,28,29,32,34,35,42,43,48,52],app_0:12,app_0_end:14,app_0_start:14,app_1:12,app_1_end:14,app_1_start:14,app_2:12,app_2_end:14,app_2_start:14,app_3:12,app_4:12,app_5:12,app_:[14,34],app_base_address:[14,15,19],app_dst:14,app_id:[14,19,28,29,34],app_init_context:[15,29,35,48,52],app_inod:[43,48],app_manag:[14,15],app_nam:34,app_size_limit:[14,19],app_src:14,app_start:[14,19,28],app_start_raw:14,append:42,applic:[12,14,15,18,21,29,32,35],appmanag:14,apt:0,arc:[1,29,34,35,39,41,42,43,47,48,52,53,54,55],arch:[2,7,8,9,13,14,15,19,20,28,29],architectur:9,archiv:54,are:[41,52,54],area:[14,28,29,34,35,41,42],arg0:9,arg1:9,arg2:9,arg:[8,9,13,15,19,42,43,48,52],arg_str_ptr:[43,48],argc:[43,48],args_addr:48,args_copi:48,args_vec:[43,48],argument:[8,15,27,48],argv:48,argv_bas:48,arr:47,arrai:[27,28,29,35],as:[8,9,13,14,15,19,21,26,27,28,29,33,34,35,39,40,41,42,43,47,48,52,53,54,55],as_byt:[8,40,41,42,47,48],as_bytes_mut:[41,42],as_mut:27,as_mut_ptr:[33,35,39],as_ptr:[8,13,15,48],as_ref:[34,47,48,52,53],as_slic:[42,43,48],as_str:[33,35,42,43,48],asm:[0,5,8,9,12,13,14,15,18,19,20,29,51],assembl:[4,5,12,13,18,25,29,32,38,46],assert:[26,27,28,33,34,35,39,40,41,42,43,47,48,53,54,55,56],assert_eq:[26,28,33,35,39,40,42,43,47,48,53,54,55],assum:28,at:[0,12,18,40,41,42,43,44,47,52,54],at_fdcwd:[40,44],atom:[15,51],attent:59,austin:54,autoconf:0,automak:0,automat:[35,42],autotool:0,avail:[47,56],available_read:47,avoid:39,aw:53,back:[15,21,29,34,35,41,42,46,53,54,55],back_to_us:29,backslash:42,bad:[13,14,23,32,35],bad_address:[13,14],baidu:0,bare:[6,28,35],base:[0,5,9,12,14,15,18,19,23,25,28,29,32,34,35,36,38,39,44,46,48,51,52],base_address:[9,28],base_i:19,base_s:[29,34,35,39],bash:0,bashrc:0,batch:[12,14,15,18,19],bb:48,bbb:51,bbbbbbbbbb:18,bc:0,be:[6,21,35,41,48,52,59],becaus:[15,29,35,48],been:[27,29,34,42],befor:[15,21,27,28,35,52,54],benchmark:4,beta:59,bianari:9,big:36,big_strid:36,bigstrid:36,bin:[0,2,5,9,12,13,14,29,33,40,43,47,48,51,52,53],binari:[0,2,6,9,54],binaryheap:36,bind:2,bio:[2,9,43],bison:0,bit:[2,7,15,26,28,36,40,41,42],bitflag:[26,28,40,43,44],bitmanip:4,bitmap:[38,41,42,51],bitmap_block:41,bitmapblock:41,bits64:41,bits64_po:41,bkpt:[12,18,32],blank:[5,12,18,25,32,38,46],blk:[38,41,43,51],block:[21,29,34,35,38,39,41,42,43,51,52,53,54,55],block_bit:41,block_cach:[38,41,51],block_cache_s:41,block_current_and_run_next:[53,54,55],block_dev:[38,41,51],block_devic:[38,41,42,43],block_fil:42,block_id:[41,42],block_offset:42,block_read_s:41,block_sz:[41,42],blockcach:[41,42],blockcachemanag:41,blockdevic:[38,41,42,43],blockdeviceimpl:43,blockfil:42,blocks_need:42,blocks_num_need:[41,42],blogo:7,board:2,book:54,bool:[26,34,36,39,41,43,47,53],boot:[0,9],boot_stack:[0,9],boot_stack_top:9,bootload:[2,5,9,43,51],borrow:14,borrowmuterror:14,bottom:[28,29,34],bound:41,brinch:55,bs:33,bss:[0,11,13,15,28],btreemap:[28,35],buddi:36,buddy_system_alloc:36,buf:[13,15,29,35,39,41,42,43,47],buf_it:47,buffer:[8,13,29,33,35,39,40,43,47],bug:28,build:[0,2,7,8,9,12,13,14,18,19,25,28,29,32,34,38,43,51,60],built:0,bus:43,busi:39,but:[7,28,35,54,55],by:[15,29,34,35,42],byte_ref:47,bytes_arrai:27,c0:2,c3:2,c4:2,ca8c7ba6:[8,9],cach:[14,19,29,38,41,42,51],calcul:[41,42],call:[9,13,15,18,22,29,35,52],callq:2,can:[6,15,29,54],cannot:[7,13,39,52],cargo:[0,2,5,6,7,8,9,12,13,14,18,19,23,38,44,51,58],case1:15,case2:15,cat:[38,46,48],caus:[15,22,29,35],cautiou:59,ccc:51,cccccccccc:18,cd:[0,2,5,12,18,25,32,38,46,51],ce:2,ceil:[26,28],center:54,cento:0,cf:2,cforc:9,ch2b:[23,32],ch2b_bad_:23,ch2b_bad_address:32,ch2b_bad_instruct:32,ch2b_bad_regist:32,ch2b_hello_world:32,ch2b_power_3:32,ch2b_power_5:32,ch2b_power_7:32,ch3:[23,36],ch3b:32,ch3b_sleep1:32,ch3b_sleep:32,ch3b_yield0:32,ch3b_yield1:32,ch3b_yield2:32,ch5:[32,36],ch5_stride:36,ch5_usertest:36,ch5b:[32,33,36],ch5b_exit:32,ch5b_forktest2:32,ch5b_forktest:32,ch5b_forktest_simpl:32,ch5b_forktre:32,ch5b_initproc:[32,33,36],ch5b_user_shel:[32,33],ch6:44,ch6_usertest:44,ch6b:[38,40,43,48],ch6b_cat:[38,48],ch6b_filetest_simpl:[38,40],ch6b_initproc:43,ch6b_user_shel:48,ch7:13,ch7b:[46,47],ch7b_cat:46,ch7b_pipe_large_test:[46,47],ch7b_pipetest:[46,47],ch7b_yield:46,ch8:56,ch8b:[51,52],ch8b_phil_din_mutex:51,ch8b_thread:[51,52],ch:[35,39],chanc:55,chang:[35,55],chapter0:59,chapter3:24,chapter4:31,chapter5:37,chapter6:45,chapter7:50,chapter8:57,check:[27,43],cherri:36,child:[32,33,35,46,47,48,52],child_exit_cod:47,children:[34,35,39,52],chyyuu:[2,8,9],ci:[23,36],clap:42,classroom:[0,5,12,18,25,32,38,46,51],clean:[27,60],clear:[9,13,14,19,28,33,35,41,42,43,52],clear_bss:[9,13],clear_siz:[41,42],clink:[9,19],clion:0,cloc:[5,12,18,25,32,38,46],clock:22,clock_freq:22,clone:[0,5,12,18,21,25,26,27,28,32,34,35,38,39,41,42,43,46,47,48,51,52,53],close:[39,40,48,50],closur:1,cluster0:[12,18,32],cmp:36,cn:0,code:[0,1,5,12,13,15,18,21,25,32,33,34,35,38,39,46,47,48,51,52],codesapc:[0,5,12,18,25,32,38,51],codespac:[0,5,12,18,25,32,38,51],collect:[29,41,42,48],com:[0,3,5,12,18,25,32,38,46,51],come:[34,51],command:32,comment:[5,12,18,25,32,38,46,60],commit:[6,36],comp2022:[0,5,12,18,25,32,38,46,51],compil:[0,2,6,7,8,9,42],complet:[12,14,18,21],comprehens:4,concat:8,concurr:55,conda:60,condit:[51,53,55],condvar:[51,57],condvar_cr:55,condvar_id:55,condvar_list:55,condvar_sign:55,condvar_wait:55,condvarinn:55,confer:55,config:[0,7,9,18,19,22,25,26,27,28,29,35,38,43,51],configur:0,confirm:35,conflict:28,consol:[0,5,8,9,12,13,18,25,32,33,35,38,39,51],console_getchar:[35,39],constant:29,contain:43,content:41,context:[12,15,18,20,21,25,28,29,32,35,38,46,48,51,52],continu:[33,35,39,47,54,55],control:[4,21,29,34,35,39,52],convert:26,cooper:54,copi:[14,19,21,26,28,35,41,47,48],copy_data:28,copy_from_slic:[14,19,28,35,41],core:[6,7,8,9,12,13,14,15,18,19,20,27,28,29,32,34,35,36,39,40,41,42,47,48],coremark:4,coroutin:51,correctli:35,correspond:41,cos:59,cost:51,count:[12,18,28,32,35,41,42,53,54],cout:5,cover:48,cpu:[6,8,9,14,15,20,21,26,28,29,33,34,35,52,53],cr:33,crate:[0,6,8,13,21,22,26,28,29,33,35,38,40,41,42,43,44,47,52],creat:[0,5,12,18,25,27,32,38,39,40,42,43,47,48,51,52,53,54,55],cricit:54,critic:[51,53],cross:7,cs:54,csr:[4,22,29,31],csrr:[15,29],csrrw:[15,23,29],csrw:[15,23,29],csu:2,ctrl:[0,2,33],cur:53,curl:0,current:[14,15,18,20,21,22,27,28,29,34,35,39,41,43,47,48,52,53,54,55],current_app:[14,15],current_process:53,current_task:[21,29,34,35,39,43,47,48,52,53,54,55],current_task_cx_ptr:[20,21],current_trap_cx:[29,34,35],current_user_token:[29,34,35,39,43,47,48],current_vpn:28,cx:[15,20,21,29,34,35,39,48,52],cx_addr:15,cycl:35,d1:2,d2:2,danger:59,data:[0,9,14,15,19,28,29,34,35,39,41,42,43,48,52],data_area_block:[41,42],data_area_start_block:42,data_bitmap:42,data_bitmap_block:[41,42],data_block:[41,42],data_block_id:42,data_blocks_dealloc:42,data_fram:[28,35],data_total_block:42,datablock:[41,42],dataencod:2,date:6,db:2,dead:51,deadlock:[39,56],dealloc:[27,34,35,41,42,52],dealloc_data:42,dealloc_inod:42,debug:[0,2,4,6,7,8,28,44],debuginfo:[2,6,7,8],delta:18,depend:0,deriv:[21,26,28,41,44,47],descriptor:39,detect:56,dev:[0,2,6,7,8,38,41,44,51],devel:59,devic:[0,2,9,38,41,42,43],dii:36,dijkstra:[51,54],din:51,dinghao188:0,dir:[41,42,44],dir_entri:42,direct:[15,29,41],direct_bound:41,directori:[6,41,42,44],dirent:[41,42],dirent_sz:[41,42],direntri:[41,42],dirfd:[40,44],dirti:26,disassembl:[2,8],discard:[9,28],disk:[41,42],disk_inod:42,diskinod:[41,42],diskinodetyp:[41,42],dispatch:51,dist:0,dl:33,doc:60,docker:0,dockerfil:51,doe:[15,29,52],doi:55,done:[35,52],down:[54,56],downei:54,downgrad:[35,47,52],download:0,dr:60,drain:[41,42,48],drive:43,driver:[38,43,51],drop:[15,21,27,34,35,39,41,47,52,53,54,55],dst:[14,19,28,35,41],dst_ppn:35,dtb:[12,18,32],dump:[12,15,19,35],dup:[46,48],dure:35,dyn:[39,41,42,43,53,55],dynam:[2,39],e0463:6,e29e31e5328f:[8,9],e2:2,e4:2,e8:2,e9:2,each:[9,19,41,42,48],easi:[44,45,46,51],easy_fs_pack:42,easyf:38,easyfilesystem:[38,42,43],eat:51,ebp:2,ebss:[9,28],ec:2,ecal:[8,9,13,15,35],echo:[5,8],ed:[2,54],edata:[9,28],edi:2,edsger:54,edu:[0,54],ef:[41,42],efs:[38,42,43,51],efs_mag:41,eh:[9,28],eh_fram:[9,28],ekernel:[9,27,28],electrologica:54,elf64:[2,7,8],elf:[0,2,6,7,8,9,13,14,28,29,34,35,38,39,42,43,48,53],elf_data:[28,29,34,35,39,48],elf_head:28,elffil:28,elfhead:2,els:[21,27,33,34,35,39,41,43,47,48,52,53,54,56],em:[2,7],em_riscv:7,em_x86_64:2,emac:0,empti:[26,27,33,41,42,43,47,48],enabl:[22,56],enable_deadlock_detect:56,enable_timer_interrupt:22,end:[14,27,28,29,34,35,41,42,47],end_current_block:41,end_va:[28,29],end_vpn:28,endbr64:2,endm:[15,20],endr:[15,20,23,29],enqueu:[54,55],enter:[12,18,32],entri:[2,5,7,9,12,13,15,18,28,29,35,42,43,48,51,52],entry_point:[28,29,35,48],entrys:2,enumer:[21,34,35,41,48,53],env:[0,5,59],environ:[6,13,15],eq:[26,28,33,35,36,39,40,41,42,43,47,48,53,54,55],erodata:[9,28],error:[0,6,7,12,23,33,42,48],essenti:0,etext:[9,28],ewd01xx:54,ewd123:54,ewd:54,except:[15,29,35],exclus:[14,15,21,27,29,34,35,48,51,52,53,54,55,56],exclusive_access:[14,15,21,27,29,34,35,48,53,54,55,56],exec:[32,34,36,37,38,45,46,50,52],execinstr:2,execut:[2,6,7,20,28,33,42,48,54],exist:[35,52],exit:[8,12,13,15,24,32,33,34,35,38,39,46,47,48,51,52,53,54],exit_cod:[13,21,33,34,35,39,48,52],exit_code_ptr:35,exit_current_and_run_next:[21,35,52],exit_pid:[33,48],exit_tid:52,ext:42,extens:4,extern:[4,8,9,13,14,15,19,20,21,26,28,29,33,34,40,44,47,48,52],f0:2,f3:2,f6:2,fa:2,fair:53,fals:[36,39,41,43,47,53,56],fault:[27,35],fd:[8,13,15,29,33,35,39,40,44,47,48],fd_stdin:35,fd_stdout:[15,29],fd_tabl:[39,47,48],fdcwd:[40,44],featur:[0,13],fenc:[14,19,29],fetch:34,fetch_task:34,ff:2,fifo:[34,41],file:[2,5,6,7,8,9,12,18,25,28,32,38,39,40,41,42,43,44,46,47,48],file_count:42,file_nam:42,file_s:28,file_test:[38,40],filea:[38,40],fileb:46,filetest:[38,40],filevers:2,fill:14,filter:52,find:[6,7,13,21,27,34,35,41,42,43,47,48,53],find_inode_id:42,find_next_task:21,find_pt:27,find_pte_cr:27,fini:2,finish:[2,6,7,8,9,56],finland:54,firmwar:0,first:[19,21,54,55],fixm:2,flag:[2,26,27,28,40,43,44],flex:0,floor:[26,28,29],fmt:8,fn:[6,7,8,9,13,14,15,19,20,21,22,23,26,27,28,29,30,33,34,35,36,39,40,41,42,43,44,47,48,52,53,54,55,56,59],fnmut:41,fnonc:[41,42],foo:54,for_each:[9,19,41,42,48],fork:[32,34,36,37,39,47,48,52],forktest2:32,forktest:32,forktre:32,format:[2,7,8,42,43],format_arg:8,found:[0,7,35],found_pid:35,frac:[36,41],frame:[9,25,27,28,29,32,34,35,38,46,51],frame_alloc:[25,27,28,29,32,38,46,51],frame_dealloc:27,framealloc:[27,34],frametrack:[27,28],freeli:[15,29],freq:22,from:[12,13,14,15,19,26,27,28,29,34,35,39,40,41,42,47,48],from_anoth:35,from_bit:[26,28],from_elf:[28,29,35,48],from_existed_us:35,from_raw_part:[14,15,19,28,34,41,48],from_raw_parts_mut:[14,19,27,41],from_token:[27,29,35],from_utf8:[15,29,34,39,40,47,48],front:[34,41,53,54,55,56],fs:[12,15,18,25,29,32,35,39,44,45,46,47,48,51],fs_img:43,fstat:44,ftp:0,ftp_proxi:0,full:47,fuse:[38,41,42,43,44,51],gawk:0,gcc:[0,53],gdb:2,gener:[15,29,54],geq:36,get:[14,15,18,19,21,22,23,27,28,29,31,34,35,41,42,48,52,53],get_app_data:[28,29,34],get_app_data_by_nam:[34,35],get_base_i:19,get_block_cach:[41,42],get_block_id:41,get_bytes_arrai:[27,28,29,35],get_current_app:[14,15],get_data_block_id:42,get_disk_inode_po:42,get_end:[28,35],get_idle_task_cx_ptr:34,get_match:42,get_mut:[27,29,34,35,41],get_num_app:[19,21,28,29,34],get_pte_arrai:27,get_ref:41,get_sp:15,get_start:[28,35],get_statu:34,get_tim:[18,22,23,53],get_time_u:22,get_top:[34,35,48,52],get_trap_cx:[29,34,35,48,52],get_typ:28,get_user_token:34,getchar:[33,35,39],getpid:[32,34,35],gib:[26,28],gif:59,git:[0,5,12,18,25,32,36,38,46,51,58],github:[0,3,5,12,18,25,32,38,46,51],githubclassroom:[0,5,12,18,25,32,38,51],give:55,given:35,glib:0,global:[2,9,14,15,20,34],global_asm:[9,14,15,20],globl:[9,20,29,53],gnu:6,go:21,goe:35,got:2,goto_restor:21,goto_trap_return:[29,35,39],gp:[15,23,29],gperf:0,green:54,gthread:0,guard:28,guid:[1,58],handl:[15,34,35,39],handler:[11,12,15,22,28,29,35,48,52],hansen:55,hart:[0,12,18,32],has:[27,34,42,52],hash:6,have:[15,29,35,52],head:47,header:[2,13,28,29,35,48],headers:2,heap:[25,29,32,36,38,46,48,51],heap_alloc:[25,29,32,36,38,46,51],heap_spac:48,hello:[0,2,5,6,7,8,9,12,13,14,18,32,38,40,46,47],hello_world:[13,14],help:42,helper:26,here:[42,51,52,59],hi:53,hint:59,histor:54,histori:[54,55],hoar:55,home:[0,6,7,8],hopl:55,host:[0,6,42],host_fil:42,hosto:42,howev:52,html:[54,58,60],http:[0,3,5,12,18,25,32,38,46,51,54],http_proxi:0,https_proxi:0,hungri:51,i32:[8,13,15,21,33,34,35,39,40,44,47,48,52,53,54,55,56,59],ia:[12,18,32],icach:14,id:[0,5,8,12,13,14,15,18,19,21,22,23,25,28,29,30,32,33,34,36,38,40,41,42,43,44,47,48,51,52,53,54,55,56],ident:[2,27,28,43],identifi:52,idl:37,idle_task_cx:34,idle_task_cx_ptr:34,idx:[27,34,35,41,48],ii:55,illeg:35,illegalinstruct:[12,15,35],ima:[12,18,32],img:[42,43],imm:59,immedi:[35,42],immut:[34,52,53,54,55],impl:[8,15,21,26,27,28,29,34,35,36,39,41,42,43,47,48,53,54,55,56],implement:[0,12,18,32],incbin:[14,29],includ:[9,14,15,20,28,35,41,52],include_str:[9,14,15,20],increas:[41,42],increase_s:[41,42],indetermin:[51,53],index:[0,2,27,60],indirect1:41,indirect1_bound:41,indirect2:41,indirect:[41,42],indirect_block:41,indirectblock:41,info:[0,2,7,12,14,23,29,31,44],init:[2,14,15,21,25,27,29,33,35,48,52],init_app_cx:21,init_frame_alloc:29,init_heap:29,initi:[33,35,41,42,48],initproc:[32,34,35,36,37,38,39,43,52],initproc_inn:[35,52],inlateout:[8,13],inlin:[0,13],inner:[2,21,29,34,35,39,41,43,47,48,52,53,54,55,56],inner_exclusive_access:[34,35,48,52,53],inner_id:41,inner_po:41,ino:44,inod:[38,41,43,44,45,46,48,51],inode_area_block:[41,42],inode_area_start_block:42,inode_bitmap:42,inode_bitmap_block:[41,42],inode_direct_count:41,inode_id:42,inode_indirect1_count:41,inode_num:42,inode_numb:[41,42],inode_s:42,inode_total_block:42,inodes_per_block:42,input:[15,28,48],input_fd:48,insert:[28,29,34],insert_framed_area:[28,29,34],instal:[0,6,60],instruct:[4,15,32,35],instructionfault:35,instructionpagefault:35,interfac:32,interpret:2,interrupt:[4,22,35],into:[26,27,28,29,34,35,41,42,43,47,48],into_it:[42,47],into_str:42,intoiter:39,invalid:[27,28],io:[0,41,42],ipag:[12,18,32],ipc:52,is:[0,26,27,28,29,33,34,35,41,42,43,47,48,52,53,56,59],is_dir:[41,42],is_empti:[33,43,48],is_en:56,is_execut:28,is_fil:41,is_non:[34,47,48,53],is_read:28,is_som:[27,41,42,52],is_valid:[26,27,41,42],is_writ:28,is_zombi:[34,35,52],isa:4,isbn:55,isiz:[8,13,15,21,22,23,29,30,33,35,36,39,40,43,47,48,52,53,54,55],isol:28,issu:[58,60],it:[9,15,29,35,42,48,52,59],item:[5,7,9,12,18,51,53],iter:[21,27,28,34,35,39,41,42,43,46,47,48,52,53],iter_mut:[21,41,42,43,48],its:35,itself:52,iwipjxjxbdfs2qf:0,jal:59,jalr:59,jetbrain:0,jieba:60,jmp:2,jmpq:2,jouni:54,jr:[29,53],jump:[29,35],kb:26,kernel:[2,8,9,12,14,15,18,20,21,28,29,32,34,35,39,43,48,52],kernel_bin:[2,9,43],kernel_elf:2,kernel_entry_pa:[2,9,43],kernel_satp:29,kernel_sp:[29,35],kernel_spac:[29,34,35,48],kernel_stack:[15,34,35,39,48,52],kernel_stack_bottom:[29,34],kernel_stack_bottom_va:34,kernel_stack_posit:[29,34],kernel_stack_s:[28,29,34],kernel_stack_top:[29,34,35,39],kernel_token:52,kernelstack:[15,34,35,52],kib:[9,26,41],kpti:30,kstack:[21,51,52],kstack_ptr:21,kthread:51,l13:23,l18:53,l20:53,l23:53,l40:23,l46:23,l51:23,l53:23,l59:23,l63:23,la:[9,12,18,32],lab1:[24,36],lab2:31,lab3:37,lab4:45,lab5:57,lab:[8,9],label:54,label_1:54,label_2:54,lang:[0,5,7,9,12,18,51],lang_item:[5,7,9,12,18,51],languag:[5,12,18,25,32,38,46,55],larg:[46,47],last:41,later:[15,29,41,48],layout:[38,41,51],lazi:[14,21,29,30,34,35,43],lazy_stat:[14,21,29,34,35,43],ld:[2,5,9,12,13,15,18,20,23,25,27,28,29,51,53],leaq:2,learningo:[0,5,12,18,25,32,38,46,51],leftarrow:[15,59],len:[8,13,14,15,19,28,29,30,33,34,35,39,40,41,42,43,47,48,52,53],len_read:47,length:[28,41],lepp:54,leq:36,let:[8,9,13,14,15,19,21,27,28,29,33,34,35,39,40,41,42,43,47,48,52,53,54,55,56],level:4,lf:33,lib64:2,lib:[12,13,21,33,35,38,40,41,44,47,48,51,52],libc:[2,6],libexpat:0,libglib2:0,libgmp:0,libmpc:0,libmpfr:0,libpixman:0,libtool:0,licens:51,limit:[14,19,41],line:[32,33,48],link:[2,12,13,14,18,28,48],link_app:[12,14,18,28],link_sect:[13,48],linkabl:2,linkag:13,linkat:44,linker:[2,5,9,12,13,18,25,27,28,29,51],linux:[0,2,6,8,30,48],list:[0,34,35,42,43,53,54,55],list_app:[34,43],liter:8,littl:54,littleendian:2,littleriscv:[7,8],llvm:6,lo:53,load:[12,14,15,19,20,23,28,29,41,42],load_app:[14,15,19],load_gp:[15,23,29],load_sn:20,loader:[2,9,18,19,21,25,28,29,32,34,35,38,43],loadfault:35,loadnam:2,loadpagefault:35,local:[0,15,21],lock:[35,39,41,42,43,47,48,51,53,54,55,56],log:[0,5,9,12,18,23,44,51],lookasid:29,loop:[7,8,28,33,34,35,39,41,43,47,48,52],loop_read:47,low:4,lpage:[12,18,32],ls:[0,2,42,43],lsb:[2,7],lui:53,luojia65:3,lw:53,m1:0,machin:[0,2,7,9,43],maco:0,macro:[7,8,13,15,20,26,33,40,47,52],macro_export:8,macro_rul:8,macro_us:[13,26,33,40,47,52],magic:[2,28,41],mai:[6,54],main:[0,2,5,6,8,9,11,12,13,14,18,21,22,23,25,26,29,32,33,38,40,42,44,47,48,51,52,53,54,55],maintain:35,make:[0,5,9,12,13,14,18,23,25,32,36,38,44,46,47,48,51,60],make_pip:47,makefil:[5,12,18,23,38,43,51,58],manag:[14,15,21,29,32,34,35,38,46,51,52],mangl:[8,9,13,15,22,29,33,35,40,47,48,52,53],manual:[4,15,21,34,35,39],map:[21,27,28,29,34,35,41,42,43,48,53],map_area:28,map_on:28,map_perm:[28,35],map_trampolin:[28,29,35],map_typ:[28,35],maparea:[28,35,43],mappermiss:[28,29,30,34,43],maptyp:[28,43],mark:[21,52],mark_current_exit:21,mark_current_suspend:21,match:[15,22,28,29,33,35,42,52],max:[14,21,23,28,29,36,41],max_app_num:[14,21],max_end_va:28,max_end_vpn:28,max_syscall_num:23,maximum:42,mayb:28,md:[23,30,51],medeleg:[12,18,32],media:[8,9],meltdown:29,mem:[28,34,41,42,48],mem_siz:28,memori:[0,19,25,27,28,29,32,34,35,38,39,43,46,48,51,52],memory_end:[27,28],memory_set:[25,28,29,32,34,35,38,39,43,46,48,51,52],memoryarea:[25,28,29],memoryset:[25,28,29,34,35,39,43,48],mention:29,merg:58,mesa:55,metadata:2,metal:6,mib:[27,41],micro:22,micro_per_sec:22,mideleg:[12,18,32],min:[28,29,36,41],mirror:0,misa:[12,18,32],mit:29,mm:[25,26,27,28,29,32,35,36,38,39,43,46,51],mmap:31,mmio:[38,43],mmu:[26,27,28,29,30],mnt:0,mod:[7,9,12,15,18,21,22,25,29,32,35,38,39,43,46,51,52],mode:[2,8,9,12,13,15,21,26,29,40,43,44],modifi:[35,41,42],modify_disk_inod:42,monitor:55,most:[42,47],movb:2,move:[14,15,29,35,41,52],move_to_next_app:[14,15],movq:2,movslq:2,ms:[22,23],msec:18,mtime:22,mtimecmp:22,mu:29,multiprogram:54,munmap:31,must:[21,35],mut:[8,9,13,14,15,19,20,21,22,23,27,28,29,33,34,35,39,40,41,42,43,44,47,48,52,53,54,55,56],mutabl:[34,52,53,54,55],mutex1:56,mutex2:56,mutex:[1,41,42,47,51,54,55,56,57],mutex_blocking_cr:[53,55],mutex_id:[53,55],mutex_inn:[53,56],mutex_list:53,mutex_lock:[53,55,56],mutex_unlock:[53,55],mutexblock:53,mutexblockinginn:53,mutexguard:42,mutexspin:53,mutual:[51,53],mv:[15,21,29],name:[0,2,13,34,35,41,42,43],name_length_limit:41,name_with_ext:42,need:[41,42,54,55,56],nest:22,new_area:35,new_bar:[28,35],new_block:41,new_fd:48,new_inod:42,new_inode_block_id:42,new_inode_block_offset:42,new_inode_id:42,new_kernel:[28,29,43],new_pid:35,new_siz:[41,42],new_task:[35,52],new_task_inn:52,new_task_r:52,new_task_tid:52,new_task_trap_cx:52,newdirfd:44,newpath:44,next:[14,15,20,21,22,34,35,39,41,47,52,53,54,55],next_task_cx_ptr:[20,21,34],nightli:6,ninja:0,nkxq:0,nlink:44,no:[7,8,9,13,15,22,28,29,33,35,40,44,47,48,52,53],no_main:[7,9,33,40,47,52],no_mangl:[8,9,13,15,22,29,33,35,40,47,48,52,53],no_std:[7,9,33,40,44,47,52],nobit:53,node:42,nograph:[2,9,43],none:[2,6,7,8,9,13,14,21,27,28,34,35,39,41,42,43,47,48,52,53],nop:[2,53],noreturn:29,normal:47,not:[0,6,7,15,27,29,35,42,43,52],note:6,now:[15,29,54],nproc:0,ntarget:42,ntarget_path:42,num:[12,14,19,21,23,28,29,34,41,42],num_app:[12,14,19,21,28,29,34],num_app_ptr:[14,19,28],number:[41,42],ny:55,objcopi:[0,9,13],objdump:[0,7,8],object:[2,53],of:[1,2,8,15,20,28,29,34,35,41,42,48,52,54,55],offset:[2,26,28,29,41,42,43],ok:[0,5,8,12,18,25,32,38,42,46,47,51],olddirfd:44,oldpath:44,on:[0,5,12,15,18,25,28,29,32,34,35,38,41,48,51,54,55],onc:52,onli:[35,41],open:[38,39,40,42,45,48],open_fil:[43,48],openat:40,openflag:[40,43,48],openopt:42,optim:9,option:[10,16,21,23,27,28,29,30,34,36,39,41,42,43,44,47,49,52,53,54,55,56],or:41,ord:26,order:36,ordinari:44,org:0,orient:54,os1:[0,5],os2:12,os3:[23,24],os4:[30,31],os5:[36,37],os6:[44,45],os7:50,os8:[56,57],os:[1,2,5,6,7,8,9,12,13,14,15,18,19,20,21,22,23,25,26,27,28,29,30,32,34,35,36,38,39,42,43,46,47,48,51,52,53,58],os_kernel_lab:[8,9],osinod:[38,43],osinodeinn:43,other:[2,29,36,52,55],otherwis:52,oulu:54,out:[5,41],output:[9,28,48],output_arch:[9,28],output_fd:48,p1:36,p2:36,pa:[2,9,27,43],pack:42,packer:42,pad:44,page:[25,26,27,28,29,32,34,35,38,39,46,51,52],page_offset:[26,29],page_s:[26,28,29,34],page_size_bit:26,page_t:[25,26,27,28,29,32,35,38,39,46,51],pagefault:[12,15],paget:[25,27,28,29,35],pagetableentri:[26,27],pair:[35,41,43],pan:0,panic:[5,9,11,13,14,15,21,27,29,35,39,41,52],panic_handl:11,panicinfo:7,panick:[0,12,18],parent:[32,34,35,39,47,52],parent_inn:35,part:[14,15,19,27,28,34,41,48],partial:36,partial_cmp:36,partialeq:[21,26,28,36,41,47],partialord:[26,36],pascal:55,pass:[18,32,36,38,40,46,47],patchutil:0,path:[0,33,35,36,40,42,43,44,48],pc:[9,29,51,52,59],pcb:[34,35,48],pdf:[23,54],per:[22,42,53,55],per_thread:53,perform:53,perm:[28,35],permiss:28,persist:38,person:55,ph:28,ph_count:28,ph_flag:28,phil:51,philosoph:51,physaddr:[26,27,29],physic:28,physpagenum:[26,27,28,29,34,39,52],pick:36,pid:[32,33,34,35,38,39,46,48,51,52,53,54,55],pid_alloc:[34,35],pid_handl:[34,35,39],pidalloc:[34,52],pidhandl:[34,35,52,53,54,55],pip:60,pipe:[46,47,49,51],pipe_fd:47,pipe_read:47,pipe_writ:47,piperingbuff:47,pipetest:[46,47],pixman:0,pkg:0,platform:[0,6],pleas:59,plt:2,pmp01:0,pmp02:0,pmp03:0,pmp0:[12,18,32],pmp1:[12,18,32],pmp2:[12,18,32],point:[28,29,35,48],pointer:9,polymorph:39,pop:[27,33,34,36,53,54,55,56],pop_front:[34,53,54,55,56],popq:2,port:30,pos:[41,42],posit:[29,34],power:[12,13,14,18,22,32],power_3:18,power_5:18,power_7:18,pp:55,ppid:34,ppn:[26,27,28,29,34,35,39,48,52],pragmat:54,prepar:[29,35],present:0,press:54,primit:54,print:[5,8,14,15,29,33,39,52],print_app_info:14,printf:5,println:[5,6,8,9,11,13,15,21,28,33,34,35,40,42,43,44,47,48,52,54,55],prio:36,prioriti:36,privileg:12,pro:4,proberen:54,process:[12,15,18,21,22,25,32,33,34,35,38,43,46,47,48,51,52,53,54],process_inn:[52,53],processcontrolblock:[52,53,54,55],processcontrolblockinn:[52,53,54,55],processor:[4,32,34,35,38,46,51,52],progbit:2,program:[2,12,28,29,35,48,52,55],program_head:28,programheadercount:2,programheaderentrys:2,programheaderoffset:2,programm:4,proxi:0,pt1:28,pt2:28,pte:[27,28,30],pte_flag:28,pte_u:30,pteflag:[26,27,28,29],ptr:[8,13,14,15,19,20,21,28,29,33,34,35,39,43,48],ptr_mut:34,pub:[8,9,13,15,19,20,21,22,26,27,28,29,33,34,35,39,40,41,42,43,44,47,48,51,52,53,54,55],pull:[58,60],purpos:[15,29],purpus:15,push:[15,27,28,29,33,34,35,41,42,43,47,48,52,53,54,55],push_back:[34,41,53,54,55],push_context:15,push_on_top:[34,35],push_str:48,pushq:2,py:[19,25],python3:0,python:60,qaq:23,qemu:[5,8,9,12,14,18,19,25,32,33,38,43,46,48,51],quad:[14,34],queue:[34,35,41,53,54,55,56],quick:4,r8:2,r9:2,ra:[13,20,21,29,53],race:[51,53],race_adder_mutex_block:53,raii:[28,34],rang:[28,35],raw:[14,15,19,27,28,34,41,43,48],rax:2,rc:35,rcore:[3,6,7,8,53,58],rcx:2,rd:[15,59],rdi:2,rdonli:[40,43,48],rdwr:[40,43],rdx:2,read:[14,15,22,28,29,32,33,34,37,38,39,40,41,42,43,46,47,48,53,60],read_al:[43,48],read_at:[41,42,43],read_block:41,read_byt:47,read_dir:42,read_disk_inod:42,read_end:47,read_end_with_buff:47,read_fd:47,read_len:40,read_siz:[41,43,47],read_to_end:42,read_volatil:[14,28,34,48,53],read_writ:43,readabl:[39,43,47],readelf:0,readi:[21,29,32,34,35,39],readm:51,readobj:[7,9],readthedoc:59,ready_queu:34,record:[35,52],recycl:[27,34,35,52],recycle_data_pag:[35,52],recycle_r:52,recyclealloc:52,redirect:48,ref:[14,18,21,25,29,32,34,35,38,41,43,46,47,48,51,52,53],refcel:12,refer:4,refmut:[34,35,47,48],reg:29,region:[0,19],regist:[4,15,22,29,32,43],registri:0,regular:44,relat:15,releas:[2,6,9,13,14,15,33,34,35,39,42,48],remov:[28,34,35,52],remove_area_with_start_vpn:34,replac:0,report:[23,30,36],repositori:[0,5,12,18,25,32,38,51],repr:[15,20,22,26,29,41,44],rept:[15,20,23,29],request:[58,60],requir:[0,7,60],res:[52,54],res_count:54,resourc:[4,15,51],rest:60,restor:[15,20,21,23,29],restore_va:29,restructuredtext:[58,60],result:[8,27,35,42],ret:[8,9,13,20],retq:2,rev:27,ricv:6,ring:47,ring_buff:47,ring_buffer_s:47,ringbufferstatu:47,rip:2,risc:[0,3,7,9,13,17,20,24,58],riscv64:[0,2,7,8,9,43,53],riscv64gc:[2,6,7,8,9,13,14],riscv:[0,7,9,15,22,28,30],robin:22,rodata:[0,9,28],root:[0,27,29,42,43,44],root_inod:[42,43],root_inode_block_id:42,root_inode_offset:42,root_ppn:[27,29],round:22,rr:[22,34],rs:[0,5,6,7,8,9,12,13,14,15,18,19,20,21,22,23,25,26,27,28,29,32,33,34,35,38,39,40,41,42,43,44,46,47,48,51,52,53,59],rsi:2,rsp:2,rtd:60,rule:8,run:[0,2,5,6,9,12,14,15,18,21,22,23,25,32,34,35,36,38,39,41,43,44,46,47,51,52,53,54,55],run_current_app:15,run_first_task:21,run_next_app:[14,15],run_next_task:21,run_task:34,runnabl:36,runtim:36,rust:[3,5,6,7,8,9,12,13,15,18,20,21,22,23,25,26,27,28,29,32,33,36,38,39,41,44,46,51,55,56,58,59],rust_main:[9,21,22,29],rustc:[0,2,5,6,12,18,19,25,32,38,51],rustflag:9,rusto:1,rustsbi:[0,2,5,9,12,18,29,32,51],rustup:0,rustup_dist_serv:0,rustup_update_root:0,rv64:[7,15,26],rv64acdfimsu:[12,18,32],rvi:54,rw:28,rwx:[12,18,32],rwxrwxr:2,rx:29,s0:[20,53],s11:20,sa:[12,18,32],same:35,satp:[26,27,29],save:[15,20,29,35,52],save_gp:[15,29],save_sn:20,sbi:[0,2,3,5,9,12,13,18,22,23,29,35,51],sbi_cal:[9,22],sbi_set_tim:22,sbi_shutdown:9,sbss:[9,28,53],sbss_with_stack:28,scaus:[15,22,29,35],schedul:[34,35,51,52],scope:7,script:[2,9],sd:[15,20,29,53],sdata:[9,28],sec:22,second:[54,55],section:[2,8,9,13,14,19,20,28,29,35,48,51,52,53,54],sectionheadercount:2,sectionheaderentrys:2,sectionheaderoffset:2,see:22,self:[8,14,15,21,26,27,28,29,34,35,36,39,41,42,43,47,48,53,54,55,56],sem:54,sem_id:54,sem_sync:54,semaphor:[51,56,57],semaphore_cr:54,semaphore_down:[54,56],semaphore_list:54,semaphore_up:54,semaphoreinn:54,semphor:54,send:[39,41,53],sepc:[15,23,29,35],septemb:54,sequenti:54,server:0,set:[15,18,20,22,23,25,28,29,32,34,35,36,38,39,42,43,46,47,48,51,52],set_kernel_trap_entri:[29,35],set_len:42,set_next_trigg:[22,35],set_prio:36,set_sp:[15,29],set_spp:[15,29],set_stim:22,set_tim:[18,22],set_user_trap_entri:29,set_write_end:47,setup:[0,5],setupclassroom:[0,5,12,18,25,32,38,46,51],setupclassroom_test1:5,setupclassroom_test2:12,setupclassroom_test3:18,setupclassroom_test4:25,setupclassroom_test5:32,setupclassroom_test6:38,setupclassroom_test8:51,setupclassroom_testx:0,sext:[12,18,32,53],sfenc:29,sh:0,shadowsock:0,share:[2,51],sharedobject:2,shell:[32,37,38,46,50,51],shf:2,shf_alloc:2,shf_execinstr:2,shinbokuow:[0,6,7,8],shorter:28,should:[9,21,41,42,52],sht:2,sht_progbit:2,shutdown:[0,9],sie:22,signal:55,sigplan:55,simpl:[32,34,38,40],simplic:43,sin:59,sinc:[35,52],size:[2,14,15,19,26,28,29,34,35,39,41,42,43,47,48,53],size_of:[34,41,42,48],skernel:9,skip:[15,29],sleep1:[18,32],sleep:[18,32,51,53,54,55],slice:[14,15,19,22,27,28,34,35,41,42,43,48],smp:0,sn:20,so:[2,35],softmmu:0,some:[21,27,28,34,35,39,41,42,43,47,48,52,53,54,55,56],sourc:[0,42],sp:[9,15,20,21,23,28,29,35,39,48,53],space:[9,27,29,34,35,48,52],spage:[12,18,32],spawn:36,sphinx:[58,59,60],sphinx_rtd_them:60,spie:22,spin:53,split:48,spp:[15,29],src:[0,5,6,7,8,9,12,13,14,15,18,19,20,21,22,23,25,26,27,28,29,32,33,34,35,38,39,40,41,42,43,44,46,47,48,51,52,53],src_path:42,src_ppn:35,sret:[15,22,29],srodata:[9,28],sscratch:[15,23,29],ssf:0,ssoft:[12,18,32],sstatu:[15,22,23,29],st:44,stack:[0,9,15,20,28,29,34,35,39,43,48,52],stackframealloc:27,stackless:51,stackless_coroutin:51,stanford:1,start17hc258028f546a93a1:2,start:[2,7,8,9,13,14,15,19,28,29,30,32,34,35,41,42,48,53],start_block:41,start_block_id:41,start_va:[28,29],start_vpn:28,stat:44,statmod:44,statu:[4,21,23,29,34,35,39,47,52],std:[6,7,9,33,40,42,44,47,52],stderr:39,stdin:[35,39],stdio:[38,39,46,51],stdout:[8,13,15,29,39],step:[28,29],stepon:28,stext:[9,28,29],stie:22,still:[35,52],stimer:[12,18,22,32],stmt:54,storag:38,storefault:[15,35],storepagefault:[15,35],str:[8,9,14,15,20,29,33,34,35,39,40,41,42,43,47,48],str_start:48,strampolin:[28,29],stride:37,stride_max:36,stride_min:36,string:[33,34,35,42,43,48],stringtablesectionindex:2,strip:[2,9],strong:[35,41],strong_count:[35,41],struct:[8,14,15,20,21,22,23,26,27,28,29,34,36,39,40,41,42,43,44,47,52,53,54,55],studio:0,stval:[15,29,35],stvec:[15,29],su:0,sub:41,subq:2,substitut:[35,41,48],sudo:0,suit:4,sum:[5,12,18,25,32,38,46],summer:1,super_block:42,superblock:[41,42],supervisor:[0,4,9,12,18,29,32],supervisortim:[22,35],support:[0,35],survei:54,suspend:[21,22,35,39,47],suspend_current_and_run_next:[21,22,35,39,47],sv39:[28,29,30,31],sw:53,swap:30,switched_task_cx_ptr:34,sy:[8,13,15,22,23,24,25,31,32,33,36,37,38,39,40,44,45,46,50,52,53,54,55,56],symbol:2,sync:[12,14,18,39,41,51,53,54],synchron:[51,54],sys_:52,sys_clos:50,sys_condvar_cr:55,sys_condvar_sign:55,sys_condvar_wait:55,sys_dup:[46,48],sys_enable_deadlock_detect:56,sys_exec:[33,35,38,45,46,50],sys_exit:[8,13,15,24,35,52],sys_fork:[33,35],sys_get_tim:[22,31],sys_getpid:32,sys_linkat:44,sys_mmap:30,sys_munmap:30,sys_mutex_cr:53,sys_mutex_lock:53,sys_mutex_unlock:53,sys_open:[38,45],sys_openat:40,sys_pip:[46,47],sys_read:[32,33,37,38,39,40,43],sys_semaphore_cr:54,sys_semaphore_down:54,sys_semaphore_up:54,sys_set_prior:36,sys_spawn:36,sys_stat:44,sys_task_info:[23,31],sys_thread_cr:52,sys_unlinkat:44,sys_wait:32,sys_waitpid:[33,35],sys_waittid:52,sys_writ:[8,13,15,25,31,38,39,40,43],sys_yield:[24,33,35],syscal:[8,12,13,15,18,21,22,23,25,29,30,32,33,35,36,38,39,40,43,44,46,47,48,51,52,53,54,55,56],syscall_condvar_cr:55,syscall_condvar_sign:55,syscall_condvar_wait:55,syscall_exec:48,syscall_exit:[8,13,15],syscall_id:15,syscall_mutex_cr:53,syscall_mutex_lock:53,syscall_mutex_unlock:53,syscall_read:33,syscall_semaphore_cr:54,syscall_semaphore_down:54,syscall_semaphore_up:54,syscall_tim:23,syscall_writ:[8,13,15],syscall_yield:21,system:[0,2,5,8,9,12,35,36,42,43,54],systemv:2,sysv:2,sz:[41,42],t0:[15,23,29,53],t1:[15,23,29,53],t2:[15,23,29],tabl:[2,25,26,27,28,29,32,35,38,39,46,47,48,51],tail:[41,47],take:[21,34,35,42,47,52],take_curr:34,take_current_task:[34,35,52],takes_valu:42,tar:0,target:[0,2,6,7,8,9,13,14,42,43],target_path:42,task0:21,task:[18,19,20,21,23,25,29,31,32,34,35,38,39,43,46,47,48,51,52,53,54,55,56],task_control_block:[29,35,39],task_current_task:35,task_cx:[21,29,34,35,39,52],task_cx_ptr:[20,35],task_inn:[34,35,52],task_manag:[21,29,34],task_res_alloc:52,task_statu:[21,29,34,35,39,52],taskcontext:[18,20,21,29,34,35,39,52],taskcontrolblock:[18,21,29,34,35,39,43,48,52,53,54,55,56],taskcontrolblockinn:[34,35,39,47,52],taskinfo:23,taskmanag:[21,23,29,34,52],taskmanagerinn:[21,29],taskstatu:[18,21,23,29,34,35,39,52],taskuserr:52,tcb:[34,35,36,48,52],tea:54,temporarili:[27,35,42],termin:[32,52],test1:5,test2:12,test3:18,test4:25,test5:32,test6:38,test8:51,test:[4,12,18,32,38,40,46,47],test_str:40,testx:0,texa:54,texinfo:0,text:[2,8,9,13,15,19,20,26,27,28,29,30,41,48,53,59],that:[21,28,29,35,41],the:[6,15,35,41,42,48,52,54,55,60],thecod:[8,9],thei:[15,29,41,52],theme:60,then:54,there:35,thi:[7,15,21,52,59],think:51,thr:56,thread:[51,53,54,55,57],thread_a:52,thread_b:52,thread_c:52,thread_count:53,thread_creat:[52,53],ti:23,tick:[18,22],ticks_per_sec:22,tid:[51,52],time:[15,18,22,23,31,41,51,53],time_msec:18,timer:[18,22,51],times34:15,timev:22,tip:[30,36,59],tl:60,tlb:29,tmp:2,tmux:0,to:[0,14,15,19,21,27,29,35,39,41,42,47,48,52,54,55],token:[27,29,34,35,39,43,47,48,52],toml:[0,5,6,9,12,13,18,23,25,32,38,44,46,51],toolchain:[4,5,51],top:[9,28,29,34,35,39,48,52],total:[39,41,42,43],total_block:[41,42],total_read_s:43,total_write_s:43,tp:[15,29],trace:[5,9],trail:41,trailing_on:41,trait:[8,26,27,28,34,38,39,41,43,46,47,53,58],trampolin:[28,29,34,35,48,52],transact:51,transcript:54,translat:[27,28,29,35,39,43,47,48],translate_va:35,translated_byte_buff:[29,35,39],translated_ref:[43,48],translated_refmut:[35,47,48],translated_str:[35,43,48],trap:[12,13,17,18,20,21,22,23,25,28,31,32,34,37,39,48,51,52],trap_context:[28,29,35,48,51],trap_cx:[29,35,48,52],trap_cx_ppn:[29,34,35,39,48,52],trap_cx_ptr:29,trap_from_kernel:29,trap_handl:[12,15,22,29,35,48,52],trap_return:[29,35],trapcontext:[12,15,28,29,34,35,48,52],trapmod:[15,29],tree:[0,6,32],trigger:[22,35],triplet:6,trunc:[40,43],ts:22,tsinghua:0,tsrc:9,tt:8,ttext:19,tuna:0,tutori:[1,6,7,8,58],twice:52,txt:60,type:[2,7,26,28,35,41,43,53],type_:41,type_s:41,tz:22,u32:[23,40,41,42,43,44],u64:[36,41,44],u8:[8,9,13,14,15,19,26,27,28,29,33,34,35,36,39,41,42,43,44,47,48],ubuntu18:0,ubuntu:[0,5,12,18,25,32,38,41,51],ucb:7,uecal:[12,18,32],unblock:[54,55],under:[35,52],understand:1,uninit:21,univers:54,unix:[3,58],unknown:[0,2,6,7,8,9,13,14,53],unlink:44,unlinkat:44,unlock:[53,54,55,56],unmap:[27,28],unmap_on:28,unoptim:[2,6,7,8],unreach:[15,21,29,33,35,48,52],unsaf:[8,9,13,14,15,19,21,22,27,28,29,34,35,39,41,43,47,48,53,54,55,58],unsign:44,unsupport:[15,29,35],unus:[2,21,35,52],unwrap:[8,15,26,27,28,29,34,35,39,40,42,43,47,48,52,53,54,55],up:[12,51,54],updat:[0,35,41,48],upgrad:[47,52],upsafecel:[14,21,29,34,35,39,43,52,53,54,55],us:22,usa:55,use:[7,13,15,21,22,26,29,33,35,36,40,41,42,43,47,52],usec:22,used:27,user:[0,4,8,9,12,13,14,15,19,21,23,25,27,28,29,32,34,35,37,39,40,42,43,47,48,52,53,54],user_buf:39,user_heap_s:48,user_lib:[12,13,33,35,40,47,52],user_satp:29,user_shel:[37,48],user_sp:[28,29,35,39,48],user_spac:35,user_stack:[15,35],user_stack_bottom:28,user_stack_s:[15,28],user_stack_top:28,userbuff:[38,39,43,47],userbufferiter:39,userenvcal:[15,35],userstack:15,usertest:[36,44],using:52,usiz:[8,9,13,14,15,19,20,21,22,23,26,27,28,29,30,33,34,35,39,40,41,42,43,47,48,52,53,54,55],usr:[0,52,53],ustack:[51,52],ustack_bas:52,ustack_top:52,ustc:0,utexa:54,utf8:[15,29,34,39,40,47,48],v0:[2,6,7,8,9],v1:0,v3:[0,6,7,8,58],v64:9,va:[28,29,34,35],valid:[26,27,41,42,43],valu:[2,34,35,42],value_of:42,variabl:[15,21,55],vec:[27,28,29,34,35,36,39,41,42,43,48,52,53,54,55],vecdequ:[34,41,53,54,55],vector:[4,15],venv:60,verbos:6,verhogen:54,version:[0,2,4,6,7,12,18,32],vfs:[38,42,51],vim:0,virt:[2,9,43],virtaddr:[26,28,29,34,35,48],virtio:[0,38,41,43,51],virtio_blk:[38,41,43,51],virtioblock:43,virtpagenum:[26,27,28],virtual:[28,39],virtual_addr:28,visual:0,vma:29,volatil:[9,14,19,28,34,35,39,48,53],vpn:[26,27,28,29,34,35],vpn_rang:[28,35],vpnrang:[28,35],vscode:[0,5,12,18,25,32,38,51],wait:[32,33,35,47,51,52,53,54,55,56],wait_queu:[53,54,55,56],waited_exit_cod:52,waited_task:52,waitpid:[32,34,35,37,48,52],waittid:[52,56],wake:[53,56],wakeup:[54,55],waking_task:[53,56],wakup:55,want:[54,55],warn:0,we:[15,21,29,35,41,42,52],weak:[13,34,39,47,52],were:[15,28],wget:0,when:[33,41,48,52],where:[34,41],which:[9,35],whole:52,whose:35,will:[15,29,35,41,48,52],window:0,windows10:0,with_nam:42,without:[28,32,43],work:[54,55,56],workflow:[0,5,12,18,25,32,38,46,51],workspac:[6,7,8],world:[0,2,5,6,7,8,9,12,13,14,18,32,38,40,47],wr:0,writabl:[39,43,47],write:[7,8,9,13,15,18,19,25,28,31,35,38,39,40,41,42,43,47,53],write_at:[41,42,43],write_block:41,write_end:47,write_end_with_buff:47,write_fd:47,write_fmt:8,write_s:43,write_str:8,write_volatil:[9,19,35,39,53],writeln:34,wronli:[40,43,48],wsl1:0,wsl2:0,www:54,x0:[13,15,29,43],x10:[8,13],x11:[8,13],x12:[8,13],x17:[8,13],x1:[13,15,23,29],x2:[15,23,29],x31:[13,15,29],x3:[15,23,29],x4:[15,23,29],x5:[15,29],x86:[2,6,7],x86_64:[2,6,7],x8:54,xma:[28,29],xmas_elf:28,xn:15,xorl:2,xstate:[8,13,15],xv6:29,xvjf:0,xwr:0,xx:51,xxx:51,xxxx:[19,51],xxxxx:51,xxxxxx:51,xxxxxxx:51,xxxxxxxx:51,xxxxxxxxx:51,xxxxxxxxxx:51,xz:0,yes:9,yield0:32,yield1:32,yield2:32,yield:[18,22,24,33,35,46,52],yield_:[21,33,52],yml:[0,5,12,18,25,32,38,46,51],york:55,your:0,zero:[21,35,41,52,53],zero_init:[21,35,52],zlib1g:0,zn2os4main17h717a6a6e05a70248:2,zn3std2rt10lang:2,zombi:[33,34,35,52]},titles:["\u7b2c\u96f6\u7ae0\uff1a\u5b9e\u9a8c\u73af\u5883\u914d\u7f6e","\u9644\u5f55 A\uff1aRust \u7cfb\u7edf\u7f16\u7a0b\u8d44\u6599","\u9644\u5f55 B\uff1a\u5e38\u89c1\u5de5\u5177\u7684\u4f7f\u7528\u65b9\u6cd5","\u9644\u5f55 C\uff1a\u6df1\u5165\u673a\u5668\u6a21\u5f0f\uff1aRustSBI","\u9644\u5f55 D\uff1aRISC-V\u76f8\u5173\u4fe1\u606f","\u5f15\u8a00","\u5e94\u7528\u7a0b\u5e8f\u6267\u884c\u73af\u5883\u4e0e\u5e73\u53f0\u652f\u6301","\u79fb\u9664\u6807\u51c6\u5e93\u4f9d\u8d56","\u6784\u5efa\u7528\u6237\u6001\u6267\u884c\u73af\u5883","\u6784\u5efa\u88f8\u673a\u6267\u884c\u73af\u5883","chapter1\u7ec3\u4e60(\u5df2\u7ecf\u5e9f\u5f03\uff0c\u6ca1\u5220\u662f\u6015\u4ee5\u540e\u6709\u7528)","\u7b2c\u4e00\u7ae0\uff1a\u5e94\u7528\u7a0b\u5e8f\u4e0e\u57fa\u672c\u6267\u884c\u73af\u5883","\u5f15\u8a00","\u5b9e\u73b0\u5e94\u7528\u7a0b\u5e8f","\u5b9e\u73b0\u6279\u5904\u7406\u64cd\u4f5c\u7cfb\u7edf","\u5b9e\u73b0\u7279\u6743\u7ea7\u7684\u5207\u6362","chapter2\u7ec3\u4e60\uff08\u5df2\u5e9f\u5f03\uff09","\u7b2c\u4e8c\u7ae0\uff1a\u6279\u5904\u7406\u7cfb\u7edf","\u5f15\u8a00","\u591a\u9053\u7a0b\u5e8f\u653e\u7f6e\u4e0e\u52a0\u8f7d","\u4efb\u52a1\u5207\u6362","\u7ba1\u7406\u591a\u9053\u7a0b\u5e8f","\u5206\u65f6\u591a\u4efb\u52a1\u7cfb\u7edf","chapter3\u7ec3\u4e60","\u7b2c\u4e09\u7ae0\uff1a\u591a\u9053\u7a0b\u5e8f\u4e0e\u5206\u65f6\u591a\u4efb\u52a1","\u5f15\u8a00","\u5b9e\u73b0 SV39 \u591a\u7ea7\u9875\u8868\u673a\u5236\uff08\u4e0a\uff09","\u5b9e\u73b0 SV39 \u591a\u7ea7\u9875\u8868\u673a\u5236\uff08\u4e0b\uff09","\u5185\u6838\u4e0e\u5e94\u7528\u7684\u5730\u5740\u7a7a\u95f4","\u57fa\u4e8e\u5730\u5740\u7a7a\u95f4\u7684\u5206\u65f6\u591a\u4efb\u52a1","chapter4\u7ec3\u4e60","\u7b2c\u56db\u7ae0\uff1a\u5730\u5740\u7a7a\u95f4","\u5f15\u8a00","\u4e0e\u8fdb\u7a0b\u6709\u5173\u7684\u91cd\u8981\u7cfb\u7edf\u8c03\u7528","\u8fdb\u7a0b\u7ba1\u7406\u7684\u6838\u5fc3\u6570\u636e\u7ed3\u6784","\u8fdb\u7a0b\u7ba1\u7406\u673a\u5236\u7684\u8bbe\u8ba1\u5b9e\u73b0","chapter5\u7ec3\u4e60","\u7b2c\u4e94\u7ae0\uff1a\u8fdb\u7a0b\u53ca\u8fdb\u7a0b\u7ba1\u7406","\u5f15\u8a00","\u6587\u4ef6\u4e0e\u6587\u4ef6\u63cf\u8ff0\u7b26","\u6587\u4ef6\u7cfb\u7edf\u63a5\u53e3","\u7b80\u6613\u6587\u4ef6\u7cfb\u7edf easy-fs (\u4e0a)","\u7b80\u6613\u6587\u4ef6\u7cfb\u7edf easy-fs (\u4e0b)","\u5728\u5185\u6838\u4e2d\u4f7f\u7528 easy-fs","chapter6\u7ec3\u4e60","\u7b2c\u516d\u7ae0\uff1a\u6587\u4ef6\u7cfb\u7edf\u4e0eI/O\u91cd\u5b9a\u5411","\u5f15\u8a00","\u7ba1\u9053","\u547d\u4ee4\u884c\u53c2\u6570\u4e0e\u6807\u51c6 I/O \u91cd\u5b9a\u5411","chapter7\u7ec3\u4e60","\u7b2c\u4e03\u7ae0\uff1a\u8fdb\u7a0b\u95f4\u901a\u4fe1","\u5f15\u8a00","\u5185\u6838\u6001\u7684\u7ebf\u7a0b\u7ba1\u7406","\u9501\u673a\u5236","\u4fe1\u53f7\u91cf\u673a\u5236","\u6761\u4ef6\u53d8\u91cf\u673a\u5236","chapter8 \u7ec3\u4e60","\u7b2c\u516b\u7ae0\uff1a\u5e76\u53d1","2022\u5e74\u5f00\u6e90\u64cd\u4f5c\u7cfb\u7edf\u8bad\u7ec3\u8425","reStructuredText \u57fa\u672c\u8bed\u6cd5","\u4fee\u6539\u548c\u6784\u5efa\u672c\u9879\u76ee"],titleterms:{"2022":58,"\u4e00\u6b21":21,"\u4e00\u6bb5":28,"\u4e00\u7ae0":11,"\u4e00\u7cfb":28,"\u4e00\u7cfb\u5217":28,"\u4e03\u7ae0":50,"\u4e09\u5143":6,"\u4e09\u5143\u7ec4":6,"\u4e09\u7ae0":24,"\u4e0a\u4e0b":[15,35],"\u4e0a\u4e0b\u6587":[15,35],"\u4e0b\u6587":[15,35],"\u4e2d\u65ad":22,"\u4e3a\u4ec0\u4e48":53,"\u4e8c\u7ae0":17,"\u4e8c\u8fdb\u5236":[2,13,14],"\u4e8c\u8fdb\u5236\u7801":[13,14],"\u4e92\u65a5":51,"\u4e94\u7ae0":37,"\u4ec0\u4e48":53,"\u4ee3\u7801":[5,12,51],"\u4ee5\u540e":10,"\u4efb\u52a1":[20,21,22,23,24,29,34],"\u4efb\u52a1\u8c03\u5ea6":34,"\u4f53\u9a8c":[5,12,18,25,32,38,46,51],"\u4f5c\u4e1a":[10,23,30,36,44,49,56],"\u4f7f\u7528":[2,43,47,53,54,55],"\u4f9d\u8d56":7,"\u4fdd\u5b58":15,"\u4fe1\u53f7":54,"\u4fe1\u53f7\u91cf":54,"\u4fe1\u606f":[4,23],"\u4fee\u6539":[6,60],"\u5143\u7ec4":6,"\u5168\u5c40":41,"\u516b\u7ae0":57,"\u516d\u7ae0":45,"\u5173\u673a":9,"\u5173\u7cfb":27,"\u5173\u8054":28,"\u5173\u95ed":47,"\u5176\u4ed6":2,"\u5185\u5b58":[9,13,26,28],"\u5185\u6838":[14,15,27,28,29,34,43,52,53],"\u51fd\u6570":[7,8,9,59],"\u51fd\u6570\u8c03\u7528":59,"\u5206\u5272":48,"\u5206\u53d1":15,"\u5206\u65f6":[24,29],"\u5206\u6790":[2,7],"\u5206\u914d":27,"\u5207\u6362":[15,20,52],"\u5217\u4e3e":42,"\u521b\u5efa":[29,35,36,42,47,52],"\u521d\u59cb":[8,33,35,43],"\u521d\u59cb\u5316":[8,43],"\u529f\u80fd":9,"\u52a0\u8f7d":[14,19,29,34,43],"\u5305\u542b":52,"\u533f\u540d":30,"\u534f\u4f5c":58,"\u538b\u5165":48,"\u539f\u578b":47,"\u53c2\u6570":48,"\u53c2\u8003":[2,18,25,32,38,46,51],"\u53d8\u91cf":55,"\u53ef\u6267\u884c\u6587\u4ef6":2,"\u53ef\u7528":27,"\u540c\u6b65":51,"\u542f\u52a8":9,"\u547d\u4ee4":48,"\u547d\u4ee4\u884c":48,"\u56db\u7ae0":31,"\u56de\u6536":[27,35],"\u5730\u5740":[26,27,28,29,31],"\u5730\u5740\u6620\u5c04":27,"\u57fa\u4e8e":[29,34,47],"\u57fa\u672c":[11,27,53,54,55,59],"\u57fa\u672c\u601d\u8def":[53,54,55],"\u5904\u7406":[14,15,17,29,34,52],"\u5904\u7406\u5668":[34,52],"\u591a\u4efb\u52a1":[22,24,29],"\u591a\u7ea7":[26,27],"\u591a\u7ebf":52,"\u591a\u7ebf\u7a0b":52,"\u591a\u9053\u7a0b\u5e8f":[19,21,24],"\u5b57\u7b26":8,"\u5b57\u7b26\u4e32":8,"\u5b89\u5168":16,"\u5b89\u5168\u68c0\u67e5":16,"\u5b89\u88c5":0,"\u5b9a\u4e49":[26,51],"\u5b9a\u5411":[45,48],"\u5b9e\u73b0":[8,9,13,14,15,20,21,26,27,28,29,35,43,52,53,54,55],"\u5b9e\u8df5":[5,12,18,25,32,38,46,51],"\u5b9e\u9a8c":[0,10,16,23,30,36,44,56],"\u5bc4\u5b58":[15,26],"\u5bc4\u5b58\u5668":[15,26],"\u5bfc\u8bfb":[5,12,18,25,32,34,35,38,46,51,53,54,55,58],"\u5c01\u88c5":52,"\u5d4c\u5957":22,"\u5de5\u5177":2,"\u5df2\u7ecf":10,"\u5e03\u5c40":[9,13,41],"\u5e38\u89c1":2,"\u5e73\u53f0":[2,6],"\u5e76\u53d1":57,"\u5e94\u7528":[6,11,13,14,15,28,29,33,34,42,43,52],"\u5e94\u7528\u7a0b\u5e8f":[6,11,13,14,15,29,33,52],"\u5e9f\u5f03":[10,16],"\u5efa\u7acb":[27,29],"\u5f00\u53d1":0,"\u5f00\u542f":29,"\u5f00\u6e90":58,"\u5f0f\u8c03\u5ea6":22,"\u5f15\u8a00":[5,12,18,25,32,38,46,51],"\u5f69\u8272":10,"\u601d\u8def":[41,53,54,55],"\u6062\u590d":15,"\u6253\u5305":42,"\u6253\u5370":9,"\u6253\u5f00":[40,43],"\u6267\u884c":[2,6,8,9,11,15,29,34,43,52],"\u6269\u5c55":29,"\u6279\u5904\u7406":[14,17],"\u627e\u5230":14,"\u62a2\u5360":22,"\u62a5\u544a":[10,16,23,30,36,44,49,56],"\u62bd\u8c61":[26,28,40],"\u62c6\u9664":27,"\u6307\u4ee4":59,"\u63a5\u53e3":[27,40,41],"\u63a7\u5236":[15,21,26,29,34,52],"\u63a7\u5236\u6d41":34,"\u63cf\u8ff0":[39,43],"\u63cf\u8ff0\u7b26":[39,43],"\u63d0\u4f9b":7,"\u64cd\u4f5c":[14,39,53,58],"\u64cd\u4f5c\u7cfb\u7edf":[14,53,58],"\u652f\u6301":[0,6,8],"\u6539\u8fdb":29,"\u653e\u7f6e":19,"\u6570\u636e":[26,27,34,41,52],"\u6570\u636e\u7ed3\u6784":[26,27,34,41,52],"\u6587\u4ef6":[2,39,40,41,42,43,45,47],"\u6587\u4ef6\u521b\u5efa":42,"\u6587\u4ef6\u683c\u5f0f":2,"\u6587\u4ef6\u7cfb\u7edf":[40,41,42,43,45],"\u65b9\u6cd5":[2,27,47,53],"\u65f6\u949f":22,"\u6620\u5c04":[27,30],"\u663e\u793a":8,"\u66f4\u65b0":29,"\u6700\u5c0f":8,"\u6700\u5c0f\u5316":8,"\u6709\u5173":33,"\u6709\u7528":10,"\u672c\u7ae0":[5,12,18,25,32,38,46,51],"\u673a\u5236":[15,26,27,35,43,52,53,54,55],"\u673a\u5668":3,"\u6761\u4ef6":55,"\u6784\u5efa":[8,9,60],"\u67b6\u6784":22,"\u6807\u51c6":[7,39,48],"\u6807\u8bc6":34,"\u6807\u8bc6\u7b26":34,"\u6808\u4e0a":48,"\u6838\u5fc3":[34,52],"\u6839\u76ee\u5f55":42,"\u683c\u5f0f":[2,26],"\u6846\u67b6":[18,25,32,38,46,51],"\u68c0\u67e5":[10,16],"\u68c0\u6d4b":56,"\u6982\u5ff5":52,"\u6982\u8ff0":41,"\u6a21\u5757":41,"\u6a21\u5757\u5316":41,"\u6a21\u578b":52,"\u6a21\u5f0f":[3,29],"\u6a21\u62df":0,"\u6a21\u62df\u5668":0,"\u6b63\u5728":34,"\u6b63\u5e38":8,"\u6b63\u786e":9,"\u6b7b\u9501":56,"\u6c47\u7f16":4,"\u6d41\u7a0b":2,"\u6df1\u5165":3,"\u6dfb\u52a0":9,"\u6e05\u7a7a":[9,42],"\u7269\u7406":[26,27],"\u7269\u7406\u5730\u5740":26,"\u7279\u6743":[15,52],"\u72b6\u6001":[15,21],"\u73af\u5883":[0,6,8,9,11],"\u7406\u5668":[21,34,41,42,52],"\u7406\u89e3":6,"\u751f\u6210":[2,13,35],"\u7528\u6237":[8,15,21,33,48],"\u7528\u6237\u5e93":48,"\u76ee\u5f55":[40,41,42],"\u76ee\u6807":6,"\u76f8\u5173":[1,4,8,9,15,26,43,52],"\u786c\u4ef6":[4,15],"\u78c1\u76d8":[41,42],"\u793a\u4f8b":[33,52],"\u79fb\u9664":7,"\u7a0b\u5e8f":[6,7,8,9,11,13,14,15,19,21,24,29,33,48,52],"\u7a0b\u5e8f\u8bbe\u8ba1":13,"\u7a7a\u95f4":[9,28,29,31],"\u7a7a\u95f4\u5e03\u5c40":9,"\u7b2c\u4e00":[11,21],"\u7b2c\u4e00\u6b21":21,"\u7b2c\u4e00\u7ae0":11,"\u7b2c\u4e03":50,"\u7b2c\u4e03\u7ae0":50,"\u7b2c\u4e09":24,"\u7b2c\u4e09\u7ae0":24,"\u7b2c\u4e8c":17,"\u7b2c\u4e8c\u7ae0":17,"\u7b2c\u4e94":37,"\u7b2c\u4e94\u7ae0":37,"\u7b2c\u516b":57,"\u7b2c\u516b\u7ae0":57,"\u7b2c\u516d":45,"\u7b2c\u516d\u7ae0":45,"\u7b2c\u56db":31,"\u7b2c\u56db\u7ae0":31,"\u7b2c\u96f6":0,"\u7b49\u5f85":52,"\u7b54\u9898":16,"\u7b80\u4ecb":[39,58],"\u7b80\u5355":16,"\u7b80\u6613":[40,41,42],"\u7b80\u7b54":[16,23],"\u7b80\u7b54\u9898":16,"\u7b97\u6cd5":36,"\u7ba1\u7406":[15,21,27,29,34,35,37,41,42,52],"\u7ba1\u7406\u5668":[21,34,41,42],"\u7ba1\u7406\u673a\u5236":[35,52],"\u7ba1\u9053":47,"\u7c7b\u578b":26,"\u7c7b\u578b\u5b9a\u4e49":26,"\u7cfb\u5217":28,"\u7cfb\u7edf":[1,13,14,17,21,22,33,35,39,40,41,42,43,45,47,52,53,54,55,58],"\u7d22\u5f15":[41,42,43],"\u7ea6\u5b9a":16,"\u7ebf\u7a0b":[51,52],"\u7ec3\u4e60":[10,16,23,30,36,44,49,56],"\u7ec4\u6210":26,"\u7ed3\u675f":52,"\u7ed3\u6784":[13,26,27,34,41,52],"\u7f13\u5b58":41,"\u7f16\u7a0b":[1,10,16,23,30,36,44,49,56],"\u7f16\u8bd1":13,"\u8026\u5408":41,"\u8282\u70b9":[41,42,43],"\u83b7\u53d6":[23,35,42],"\u865a\u5b9e":27,"\u865a\u62df":[26,28,29],"\u865a\u62df\u5185\u5b58":28,"\u865a\u62df\u5730\u5740":[26,29],"\u884c\u6587":2,"\u88f8\u673a":9,"\u8981\u6c42":[10,16,23,30,36,44,49,56],"\u8ba1\u65f6":22,"\u8ba1\u65f6\u5668":22,"\u8bad\u7ec3":58,"\u8bad\u7ec3\u8425":58,"\u8bbe\u5907":[41,43],"\u8bbe\u7f6e":9,"\u8bbe\u8ba1":[13,20,35,41,52],"\u8bbf\u95ee":27,"\u8bd5\u8fd0":0,"\u8bd5\u8fd0\u884c":0,"\u8bed\u4e49":7,"\u8bed\u6cd5":59,"\u8bf4\u660e":2,"\u8bfb\u5199":[39,40,42,47],"\u8c03\u5ea6":[22,34,35,36,52],"\u8c03\u7528":[13,21,33,35,39,40,47,52,53,54,55,59],"\u8c03\u8bd5":0,"\u8d44\u6599":1,"\u8d44\u6e90":35,"\u8d77\u56e0":15,"\u8d77\u6e90":54,"\u8d85\u7ea7":41,"\u8df3\u677f":29,"\u8df3\u8f6c":59,"\u8f93\u5165":[35,39,48],"\u8f93\u5165\u8f93\u51fa":48,"\u8f93\u51fa":[8,39,48],"\u8fc7\u7a0b":9,"\u8fd0\u884c":[0,21],"\u8fd8\u539f":48,"\u8fdb\u5165":[15,21],"\u8fdb\u5236":[2,13,14],"\u8fdb\u7a0b":[33,34,35,36,37,50,52],"\u8fde\u7eed":28,"\u9000\u51fa":[8,35,52],"\u901a\u4fe1":50,"\u901a\u8fc7":[43,47],"\u903b\u8f91":28,"\u914d\u7f6e":[0,9],"\u91cd\u5199":30,"\u91cd\u5b9a\u5411":[45,48],"\u91cd\u65b0":35,"\u91cd\u8981":[33,52],"\u94fe\u63a5":[14,34,44],"\u955c\u50cf":[2,42],"\u95ee\u7b54":[10,30,36,44,49,56],"\u95ee\u9898":22,"\u95f4\u901a\u4fe1":50,"\u9644\u5f55":[1,2,3,4],"\u9700\u8981":53,"\u9875\u6a21\u5f0f":29,"\u9875\u8868":[26,27],"\u9879\u76ee":[13,58,60],"\u987a\u5e8f":40,"\u9a71\u52a8":43,"\u9e23\u8c22":58,bss:9,chapter1:10,chapter2:16,chapter3:23,chapter4:30,chapter5:36,chapter6:44,chapter7:49,chapter8:56,close:47,condvar:55,csr:[15,26],easi:[38,41,42,43],exec:[33,35,43,48],exit:21,fork:[33,35],fs:[38,41,42,43],gdb:0,get:30,handler:7,idl:34,info:30,initproc:33,inod:42,lab1:[18,23],lab2:[25,30],lab3:[32,36],lab4:[38,44],lab5:[51,56],log:10,main:7,make:2,makefil:2,mmap:30,munmap:30,mutex:53,objcopi:2,objdump:2,open:43,os3:18,os4:25,os5:32,os6:38,os7:46,os8:51,os:0,panic:7,panic_handl:7,pattern:1,println:7,qemu:[0,2],rcore:0,read:35,readobj:2,restructuredtext:59,risc:[4,15,22,59],riscv:4,rust:[0,1,2],rustsbi:3,semaphor:54,shell:[33,48],stride:36,sv39:[26,27],sy:[21,29,30,35,43,47,48],sys_clos:47,sys_exec:[43,48],sys_exit:21,sys_get_tim:30,sys_open:43,sys_read:35,sys_task_info:30,sys_writ:29,sys_yield:21,task:30,thread:52,time:30,trap:[15,29,35],tutori:0,user:33,user_shel:33,waitpid:33,write:29,yield:21}}) \ No newline at end of file diff --git a/setup-sphinx.html b/setup-sphinx.html new file mode 100644 index 0000000..08eead7 --- /dev/null +++ b/setup-sphinx.html @@ -0,0 +1,396 @@ + + + + + + + + 修改和构建本项目 - Open-Source-OS-Tutorial-Summer-of-Code-2022 文档 + + + + + + + + + + + + + + + + Contents + + + + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark mode + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
+

修改和构建本项目

+
+
+

TL;DR: python -m venv .venv 创建一个虚拟环境(你也可以使用 conda 等工具),activate 后 pip install -r requirements.txt

+
    +
  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. 确认修改无误之后,将更改提交到自己的仓库,然后向项目仓库提交 Pull Request。如有问题,可直接提交 Issue 或课程微信群内联系助教。

  14. +
+
+ +
+ +
+ +
+
+ + + + + + + + \ No newline at end of file