├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── all.sh ├── make.bat ├── requirements.txt ├── show.sh └── source ├── 0setup-devel-env.rst ├── _static ├── dracula.css └── my_style.css ├── appendix-a └── index.rst ├── appendix-b └── index.rst ├── appendix-c └── index.rst ├── appendix-d └── index.rst ├── chapter1 ├── 0intro.rst ├── 1app-ee-platform.rst ├── 2remove-std.rst ├── 3mini-rt-usrland.rst ├── 4mini-rt-baremetal.rst ├── 5exercise.rst ├── app-software-stack.png ├── chap1-intro.png ├── color-demo.png └── index.rst ├── chapter2 ├── 0intro.rst ├── 2application.rst ├── 3batch-system.rst ├── 4trap-handling.rst ├── 5exercise.rst └── index.rst ├── chapter3 ├── 0intro.rst ├── 1multi-loader.rst ├── 2task-switching.rst ├── 3multiprogramming.rst ├── 4time-sharing-system.rst ├── 5exercise.rst ├── fsm-coop.png ├── index.rst └── multiprogramming.png ├── chapter4 ├── 0intro.rst ├── 3sv39-implementation-1.rst ├── 4sv39-implementation-2.rst ├── 5kernel-app-spaces.rst ├── 6multitasking-based-on-as.rst ├── 7exercise.rst ├── address-translation.png ├── app-as-full.png ├── index.rst ├── kernel-as-high.png ├── kernel-as-low.png ├── linear-table.png ├── page-table.png ├── pte-rwx.png ├── rust-containers.png ├── satp.png ├── segmentation.png ├── simple-base-bound.png ├── sv39-full.png ├── sv39-pte.png ├── sv39-va-pa.png ├── trie-1.png └── trie.png ├── chapter5 ├── 0intro.rst ├── 1process.rst ├── 2core-data-structures.rst ├── 3implement-process-mechanism.rst ├── 4exercise.rst └── index.rst ├── chapter6 ├── 0intro.rst ├── 1file-descriptor.rst ├── 1fs-interface.rst ├── 2fs-implementation-1.rst ├── 2fs-implementation-2.rst ├── 3using-easy-fs-in-kernel.rst ├── 4exercise.rst ├── easy-fs-demo.png └── index.rst ├── chapter7 ├── 0intro.rst ├── 1pipe.rst ├── 2cmdargs-and-redirection.rst ├── 3exercise.rst ├── index.rst ├── user-stack-cmdargs.png └── user-stack-cmdargs.pptx ├── chapter8 ├── 0intro.rst ├── 1thread-kernel.rst ├── 2lock.rst ├── 3semaphore.rst ├── 4condition-variable.rst ├── 5exercise.rst └── index.rst ├── conf.py ├── honorcode.rst ├── index.rst ├── pygments-coloring.txt ├── rest-example.rst └── setup-sphinx.rst /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: [main, dev] 6 | 7 | jobs: 8 | deploy-doc: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-python@v2 14 | with: 15 | python-version: "3.8.16" 16 | - name: Install dependencies 17 | run: pip install -r requirements.txt 18 | 19 | - name: build doc 20 | run: make html 21 | 22 | - name: create .nojekyll 23 | run: touch build/html/.nojekyll 24 | 25 | - name: Push to gh-pages 26 | uses: peaceiris/actions-gh-pages@v3 27 | with: 28 | github_token: ${{ secrets.GITHUB_TOKEN }} 29 | publish_dir: ./build/html 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .vscode/ 3 | .idea 4 | source/_build/ 5 | .venv/ 6 | *.bak 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile deploy 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | 22 | view: 23 | make html && firefox build/html/index.html 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rCore-Tutorial-Guide-2023S 2 | 3 | ### Code 4 | - [Soure Code of labs for 2023S](https://github.com/LearningOS/rCore-Tutorial-Code-2023S) 5 | ### Documents 6 | 7 | - Concise Manual: [rCore-Tutorial-Guide-2023S](https://LearningOS.github.io/rCore-Tutorial-Guide-2023S/) 8 | 9 | - Detail Book [rCore-Tutorial-Book-v3](https://rcore-os.github.io/rCore-Tutorial-Book-v3/) 10 | 11 | 12 | ### OS API docs of rCore Tutorial Code 2023S 13 | - [OS API docs of ch1](https://learningos.github.io/rCore-Tutorial-Code-2023S/ch1/os/index.html) 14 | AND [OS API docs of ch2](https://learningos.github.io/rCore-Tutorial-Code-2023S/ch2/os/index.html) 15 | - [OS API docs of ch3](https://learningos.github.io/rCore-Tutorial-Code-2023S/ch3/os/index.html) 16 | AND [OS API docs of ch4](https://learningos.github.io/rCore-Tutorial-Code-2023S/ch4/os/index.html) 17 | - [OS API docs of ch5](https://learningos.github.io/rCore-Tutorial-Code-2023S/ch5/os/index.html) 18 | AND [OS API docs of ch6](https://learningos.github.io/rCore-Tutorial-Code-2023S/ch6/os/index.html) 19 | - [OS API docs of ch7](https://learningos.github.io/rCore-Tutorial-Code-2023S/ch7/os/index.html) 20 | AND [OS API docs of ch8](https://learningos.github.io/rCore-Tutorial-Code-2023S/ch8/os/index.html) 21 | - [OS API docs of ch9](https://learningos.github.io/rCore-Tutorial-Code-2023S/ch9/os/index.html) 22 | 23 | ### Related Resources 24 | - [Learning Resource](https://github.com/LearningOS/rust-based-os-comp2022/blob/main/relatedinfo.md) 25 | 26 | 27 | ### Build & Run 28 | 29 | ```bash 30 | # setup build&run environment first 31 | $ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2023S.git 32 | $ cd rCore-Tutorial-Code-2023S 33 | $ git clone https://github.com/LearningOS/rCore-Tutorial-Test-2023S.git user 34 | $ cd os 35 | $ git checkout ch$ID 36 | $ make run 37 | ``` 38 | Notice: $ID is from [1-9] 39 | 40 | ### Grading 41 | 42 | ```bash 43 | # setup build&run environment first 44 | $ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2023S.git 45 | $ cd rCore-Tutorial-Code-2023S 46 | $ git clone https://github.com/LearningOS/rCore-Tutorial-Checker-2023S.git ci-user 47 | $ git clone https://github.com/LearningOS/rCore-Tutorial-Test-2023S.git ci-user/user 48 | $ cd ci-user && make test CHAPTER=$ID 49 | ``` 50 | Notice: $ID is from [3,4,5,6,8] -------------------------------------------------------------------------------- /all.sh: -------------------------------------------------------------------------------- 1 | make clean && make html && google-chrome build/html/index.html 2 | 3 | -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.12 2 | Babel==2.9.1 3 | certifi==2021.5.30 4 | charset-normalizer==2.0.4 5 | docutils==0.16 6 | idna==3.2 7 | imagesize==1.2.0 8 | jieba==0.42.1 9 | Jinja2==3.0.1 10 | MarkupSafe==2.0.1 11 | packaging==21.0 12 | Pygments==2.10.0 13 | pyparsing==2.4.7 14 | pytz==2021.1 15 | requests==2.26.0 16 | snowballstemmer==2.1.0 17 | Sphinx==4.1.2 18 | sphinx-comments==0.0.3 19 | sphinx-rtd-theme==0.5.2 20 | sphinx-tabs==3.2.0 21 | sphinxcontrib-applehelp==1.0.2 22 | sphinxcontrib-devhelp==1.0.2 23 | sphinxcontrib-htmlhelp==2.0.0 24 | sphinxcontrib-jsmath==1.0.1 25 | sphinxcontrib-qthelp==1.0.3 26 | sphinxcontrib-serializinghtml==1.1.5 27 | urllib3==1.26.6 28 | furo==2021.8.31 29 | -------------------------------------------------------------------------------- /show.sh: -------------------------------------------------------------------------------- 1 | make html && google-chrome build/html/index.html 2 | -------------------------------------------------------------------------------- /source/0setup-devel-env.rst: -------------------------------------------------------------------------------- 1 | 第零章:实验环境配置 2 | ================================ 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | 本节我们将完成环境配置并成功运行 rCore-Tutorial 。整个流程分为下面几个部分: 9 | 10 | - OS 环境配置 11 | - Rust 开发环境配置 12 | - Qemu 模拟器安装 13 | - 其他工具安装 14 | - 试运行 rCore-Tutorial 15 | 16 | 如果你在环境配置中遇到了无法解决的问题,请在本节讨论区留言,我们会尽力提供帮助。 17 | 18 | OS 环境配置 19 | ------------------------------- 20 | 21 | 目前,实验主要支持 Ubuntu18.04/20.04 操作系统。使用 Windows10 和 macOS 的读者,可以安装一台 Ubuntu18.04 虚拟机或 Docker 22 | 进行实验。 23 | 24 | Windows10 用户可以通过系统内置的 **WSL2** 虚拟机(请不要使用 WSL1)来安装 Ubuntu 18.04 / 20.04 。读者请自行在互联网上搜索相关安装教程,或 `适用于 Linux 的 Windows 子系统安装指南 (Windows 10) `_ 。 25 | 26 | .. note:: 27 | 28 | **Docker 开发环境** 29 | 30 | 感谢 dinghao188 和张汉东老师帮忙配置好的 Docker 开发环境,进入 Docker 开发环境之后不需要任何软件工具链的安装和配置,可以直接将 tutorial 运行起来,目前应该仅支持将 tutorial 运行在 Qemu 模拟器上。 31 | 32 | 使用方法如下(以 Ubuntu18.04 为例): 33 | 34 | 1. 通过 ``su`` 切换到管理员账户 ``root`` ; 35 | 2. 在 ``rCore-Tutorial`` 根目录下 ``make docker`` 进入到 Docker 环境; 36 | 3. 进入 Docker 之后,会发现当前处于根目录 ``/`` ,我们通过 ``cd mnt`` 将当前工作路径切换到 ``/mnt`` 目录; 37 | 4. 通过 ``ls`` 可以发现 ``/mnt`` 目录下的内容和 ``rCore-Tutorial-v3`` 目录下的内容完全相同,接下来就可以在这个环境下运行 tutorial 了。例如 ``cd os && make run`` 。 38 | 39 | 使用 macOS 进行实验理论上也是可行的,但本章节仅介绍 Ubuntu 下的环境配置方案。 40 | 41 | .. note:: 42 | 43 | 经初步测试,使用 M1 芯片的 macOS 也可以运行本实验的框架,即我们的实验对平台的要求不是很高。但我们仍建议同学配置 Ubuntu 环境,以避免未知的环境问题。 44 | 45 | Rust 开发环境配置 46 | ------------------------------------------- 47 | 48 | 首先安装 Rust 版本管理器 rustup 和 Rust 包管理器 cargo,可以使用官方安装脚本: 49 | 50 | .. code-block:: bash 51 | 52 | curl https://sh.rustup.rs -sSf | sh 53 | 54 | 如果因网络问题通过命令行下载脚本失败了,可以在浏览器地址栏中输入 ``_ 将脚本下载到本地运行。或者使用字节跳动提供的镜像源。 55 | 56 | 建议将 rustup 的镜像地址修改为中科大的镜像服务器,以加速安装: 57 | 58 | .. code-block:: bash 59 | 60 | export RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static 61 | export RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup 62 | curl https://sh.rustup.rs -sSf | sh 63 | 64 | 或者使用 tuna 源来加速(建议清华同学在校园网中使用) `参见 rustup 帮助 `_: 65 | 66 | .. code-block:: bash 67 | 68 | export RUSTUP_DIST_SERVER=https://mirrors.tuna.edu.cn/rustup 69 | export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.edu.cn/rustup/rustup 70 | curl https://sh.rustup.rs -sSf | sh 71 | 72 | 也可以设置科学上网代理: 73 | 74 | .. code-block:: bash 75 | 76 | # e.g. Shadowsocks 代理,请根据自身配置灵活调整下面的链接 77 | export https_proxy=http://127.0.0.1:1080 78 | export http_proxy=http://127.0.0.1:1080 79 | export ftp_proxy=http://127.0.0.1:1080 80 | 81 | 安装中全程选择默认选项即可。 82 | 83 | 安装完成后,我们可以重新打开一个终端来让新设置的环境变量生效,也可以手动将环境变量设置应用到当前终端, 84 | 只需输入以下命令: 85 | 86 | .. code-block:: bash 87 | 88 | source $HOME/.cargo/env 89 | 90 | 确认一下我们正确安装了 Rust 工具链: 91 | 92 | .. code-block:: bash 93 | 94 | rustc --version 95 | 96 | 最好把 Rust 包管理器 cargo 镜像地址 crates.io 也替换成中国科学技术大学的镜像服务器,来加速三方库的下载。 97 | 打开或新建 ``~/.cargo/config`` 文件,并把内容修改为: 98 | 99 | .. code-block:: toml 100 | 101 | [source.crates-io] 102 | registry = "https://github.com/rust-lang/crates.io-index" 103 | replace-with = 'ustc' 104 | [source.ustc] 105 | registry = "git://mirrors.ustc.edu.cn/crates.io-index" 106 | 107 | 同样,也可以使用tuna源 `参见 crates.io 帮助 `_: 108 | 109 | .. code-block:: toml 110 | 111 | [source.crates-io] 112 | replace-with = 'tuna' 113 | 114 | [source.tuna] 115 | registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git" 116 | 117 | 118 | 推荐 JetBrains Clion + Rust插件 或者 Visual Studio Code 搭配 rust-analyzer 和 RISC-V Support 插件 进行代码阅读和开发。 119 | 120 | .. note:: 121 | 122 | * JetBrains Clion是付费商业软件,但对于学生和教师,只要在 JetBrains 网站注册账号,可以享受一定期限(半年左右)的免费使用的福利。 123 | * Visual Studio Code 是开源软件。 124 | * 当然,采用 VIM,Emacs 等传统的编辑器也是没有问题的。 125 | 126 | Qemu 模拟器安装 127 | ---------------------------------------- 128 | 129 | 我们需要使用 Qemu 7.0.0 版本进行实验,为此,从源码手动编译安装 Qemu 模拟器: 130 | 131 | .. attention:: 132 | 133 | 也可以使用 Qemu6,但要小心潜在的不兼容问题! 134 | 135 | .. code-block:: bash 136 | 137 | # 安装编译所需的依赖包 138 | sudo apt install autoconf automake autotools-dev curl libmpc-dev libmpfr-dev libgmp-dev \ 139 | gawk build-essential bison flex texinfo gperf libtool patchutils bc \ 140 | zlib1g-dev libexpat-dev pkg-config libglib2.0-dev libpixman-1-dev git tmux python3 141 | # 下载源码包 142 | # 如果下载速度过慢可以使用我们提供的百度网盘链接:https://pan.baidu.com/s/1z-iWIPjxjxbdFS2Qf-NKxQ 143 | # 提取码 8woe 144 | wget https://download.qemu.org/qemu-7.0.0.tar.xz 145 | # 解压 146 | tar xvJf qemu-7.0.0.tar.xz 147 | # 编译安装并配置 RISC-V 支持 148 | cd qemu-7.0.0 149 | ./configure --target-list=riscv64-softmmu,riscv64-linux-user 150 | make -j$(nproc) 151 | 152 | .. note:: 153 | 154 | 注意,上面的依赖包可能并不完全,比如在 Ubuntu 18.04 上: 155 | 156 | - 出现 ``ERROR: pkg-config binary 'pkg-config' not found`` 时,可以安装 ``pkg-config`` 包; 157 | - 出现 ``ERROR: glib-2.48 gthread-2.0 is required to compile QEMU`` 时,可以安装 158 | ``libglib2.0-dev`` 包; 159 | - 出现 ``ERROR: pixman >= 0.21.8 not present`` 时,可以安装 ``libpixman-1-dev`` 包。 160 | 161 | 另外一些 Linux 发行版编译 Qemu 的依赖包可以从 `这里 `_ 162 | 找到。 163 | 164 | 请自行选择合适的编译器版本编译Qemu。 165 | 166 | 之后我们可以在同目录下 ``sudo make install`` 将 Qemu 安装到 ``/usr/local/bin`` 目录下,但这样经常会引起 167 | 冲突。个人来说更习惯的做法是,编辑 ``~/.bashrc`` 文件(如果使用的是默认的 ``bash`` 终端),在文件的末尾加入 168 | 几行: 169 | 170 | .. code-block:: bash 171 | 172 | # 注意 $HOME 是 Linux 自动设置的表示你家目录的环境变量,你也可以根据实际位置灵活调整 173 | export PATH="$HOME/os-env/qemu-7.0.0/build/:$PATH" 174 | export PATH="$HOME/os-env/qemu-7.0.0/build/riscv64-softmmu:$PATH" 175 | export PATH="$HOME/os-env/qemu-7.0.0/build/riscv64-linux-user:$PATH" 176 | 177 | 随后即可在当前终端 ``source ~/.bashrc`` 更新系统路径,或者直接重启一个新的终端。 178 | 179 | 确认 Qemu 的版本: 180 | 181 | .. code-block:: bash 182 | 183 | qemu-system-riscv64 --version 184 | qemu-riscv64 --version 185 | 186 | 试运行 rCore-Tutorial 187 | ------------------------------------------------------------ 188 | 189 | .. code-block:: bash 190 | 191 | git clone https://github.com/LearningOS/rCore-Tutorial-Code-2023S 192 | cd rCore-Tutorial-Code-2023S 193 | 194 | 我们先运行不需要处理用户代码的 ch1 分支: 195 | 196 | .. code-block:: bash 197 | 198 | git checkout ch1 199 | cd os 200 | LOG=DEBUG make run 201 | 202 | 如果你的环境配置正确,你应当会看到如下输出: 203 | 204 | .. code-block:: bash 205 | 206 | [rustsbi] RustSBI version 0.3.0-alpha.4, adapting to RISC-V SBI v1.0.0 207 | .______ __ __ _______.___________. _______..______ __ 208 | | _ \ | | | | / | | / || _ \ | | 209 | | |_) | | | | | | (----`---| |----`| (----`| |_) || | 210 | | / | | | | \ \ | | \ \ | _ < | | 211 | | |\ \----.| `--' |.----) | | | .----) | | |_) || | 212 | | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| 213 | [rustsbi] Implementation : RustSBI-QEMU Version 0.2.0-alpha.2 214 | [rustsbi] Platform Name : riscv-virtio,qemu 215 | [rustsbi] Platform SMP : 1 216 | [rustsbi] Platform Memory : 0x80000000..0x88000000 217 | [rustsbi] Boot HART : 0 218 | [rustsbi] Device Tree Region : 0x87e00000..0x87e00f85 219 | [rustsbi] Firmware Address : 0x80000000 220 | [rustsbi] Supervisor Address : 0x80200000 221 | [rustsbi] pmp01: 0x00000000..0x80000000 (-wr) 222 | [rustsbi] pmp02: 0x80000000..0x80200000 (---) 223 | [rustsbi] pmp03: 0x80200000..0x88000000 (xwr) 224 | [rustsbi] pmp04: 0x88000000..0x00000000 (-wr) 225 | [kernel] Hello, world! 226 | [DEBUG] [kernel] .rodata [0x80203000, 0x80205000) 227 | [ INFO] [kernel] .data [0x80205000, 0x80206000) 228 | [ WARN] [kernel] boot_stack top=bottom=0x80216000, lower_bound=0x80206000 229 | [ERROR] [kernel] .bss [0x80216000, 0x80217000) 230 | 231 | 通常 rCore 会自动关闭 Qemu 。如果在某些情况下需要强制结束,可以先按下 ``Ctrl+A`` ,再按下 ``X`` 来退出 Qemu。 232 | 233 | .. attention:: 234 | 235 | 请务必执行 ``make run``,这将为你安装一些上文没有提及的 Rust 包依赖。 236 | 237 | 如果卡在了 238 | 239 | .. code-block:: 240 | 241 | Updating git repository `https://github.com/rcore-os/riscv` 242 | 243 | 请通过更换 hosts 等方式解决科学上网问题,或者将 riscv 项目下载到本地,并修改 os/Cargo.toml 中的 riscv 包依赖路径 244 | 245 | .. code-block:: 246 | 247 | [dependencies] 248 | riscv = { path = "YOUR riscv PATH", features = ["inline-asm"] } 249 | 250 | 恭喜你完成了实验环境的配置,可以开始阅读教程的正文部分了! 251 | 252 | GDB 调试支持* 253 | ------------------------------ 254 | 255 | .. attention:: 256 | 257 | 使用 GDB debug 并不是必须的,你可以暂时跳过本小节。 258 | 259 | 260 | 261 | 在 ``os`` 目录下 ``make debug`` 可以调试我们的内核,这需要安装终端复用工具 ``tmux`` ,还需要基于 riscv64 平台的 gdb 调试器 ``riscv64-unknown-elf-gdb`` 。该调试器包含在 riscv64 gcc 工具链中,工具链的预编译版本可以在如下链接处下载: 262 | 263 | - `Ubuntu 平台 `_ 264 | - `macOS 平台 `_ 265 | - `Windows 平台 `_ 266 | - `CentOS 平台 `_ 267 | 268 | 解压后在 ``bin`` 目录下即可找到 ``riscv64-unknown-elf-gdb`` 以及另外一些常用工具 ``objcopy/objdump/readelf`` 等。 269 | -------------------------------------------------------------------------------- /source/_static/dracula.css: -------------------------------------------------------------------------------- 1 | /* Dracula Theme v1.2.5 2 | * 3 | * https://github.com/zenorocha/dracula-theme 4 | * 5 | * Copyright 2016, All rights reserved 6 | * 7 | * Code licensed under the MIT license 8 | * http://zenorocha.mit-license.org 9 | * 10 | * @author Rob G 11 | * @author Chris Bracco 12 | * @author Zeno Rocha 13 | */ 14 | 15 | .highlight .hll { background-color: #111110 } 16 | .highlight { background: #282a36; color: #f8f8f2 } 17 | .highlight .c { color: #6272a4 } /* Comment */ 18 | .highlight .err { color: #f8f8f2 } /* Error */ 19 | .highlight .g { color: #f8f8f2 } /* Generic */ 20 | .highlight .k { color: #ff79c6 } /* Keyword */ 21 | .highlight .l { color: #f8f8f2 } /* Literal */ 22 | .highlight .n { color: #f8f8f2 } /* Name */ 23 | .highlight .o { color: #ff79c6 } /* Operator */ 24 | .highlight .x { color: #f8f8f2 } /* Other */ 25 | .highlight .p { color: #f8f8f2 } /* Punctuation */ 26 | .highlight .ch { color: #6272a4 } /* Comment.Hashbang */ 27 | .highlight .cm { color: #6272a4 } /* Comment.Multiline */ 28 | .highlight .cp { color: #ff79c6 } /* Comment.Preproc */ 29 | .highlight .cpf { color: #6272a4 } /* Comment.PreprocFile */ 30 | .highlight .c1 { color: #6272a4 } /* Comment.Single */ 31 | .highlight .cs { color: #6272a4 } /* Comment.Special */ 32 | .highlight .gd { color: #962e2f } /* Generic.Deleted */ 33 | .highlight .ge { color: #f8f8f2; text-decoration: underline } /* Generic.Emph */ 34 | .highlight .gr { color: #f8f8f2 } /* Generic.Error */ 35 | .highlight .gh { color: #f8f8f2; font-weight: bold } /* Generic.Heading */ 36 | .highlight .gi { color: #f8f8f2; font-weight: bold } /* Generic.Inserted */ 37 | .highlight .go { color: #44475a } /* Generic.Output */ 38 | .highlight .gp { color: #f8f8f2 } /* Generic.Prompt */ 39 | .highlight .gs { color: #f8f8f2 } /* Generic.Strong */ 40 | .highlight .gu { color: #f8f8f2; font-weight: bold } /* Generic.Subheading */ 41 | .highlight .gt { color: #f8f8f2 } /* Generic.Traceback */ 42 | .highlight .kc { color: #ff79c6 } /* Keyword.Constant */ 43 | .highlight .kd { color: #8be9fd; font-style: italic } /* Keyword.Declaration */ 44 | .highlight .kn { color: #ff79c6 } /* Keyword.Namespace */ 45 | .highlight .kp { color: #ff79c6 } /* Keyword.Pseudo */ 46 | .highlight .kr { color: #ff79c6 } /* Keyword.Reserved */ 47 | .highlight .kt { color: #8be9fd } /* Keyword.Type */ 48 | .highlight .ld { color: #f8f8f2 } /* Literal.Date */ 49 | .highlight .m { color: #bd93f9 } /* Literal.Number */ 50 | .highlight .s { color: #f1fa8c } /* Literal.String */ 51 | .highlight .na { color: #50fa7b } /* Name.Attribute */ 52 | .highlight .nb { color: #8be9fd; font-style: italic } /* Name.Builtin */ 53 | .highlight .nc { color: #50fa7b } /* Name.Class */ 54 | .highlight .no { color: #f8f8f2 } /* Name.Constant */ 55 | .highlight .nd { color: #f8f8f2 } /* Name.Decorator */ 56 | .highlight .ni { color: #f8f8f2 } /* Name.Entity */ 57 | .highlight .ne { color: #f8f8f2 } /* Name.Exception */ 58 | .highlight .nf { color: #50fa7b } /* Name.Function */ 59 | .highlight .nl { color: #8be9fd; font-style: italic } /* Name.Label */ 60 | .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ 61 | .highlight .nx { color: #f8f8f2 } /* Name.Other */ 62 | .highlight .py { color: #f8f8f2 } /* Name.Property */ 63 | .highlight .nt { color: #ff79c6 } /* Name.Tag */ 64 | .highlight .nv { color: #8be9fd; font-style: italic } /* Name.Variable */ 65 | .highlight .ow { color: #ff79c6 } /* Operator.Word */ 66 | .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ 67 | .highlight .mb { color: #bd93f9 } /* Literal.Number.Bin */ 68 | .highlight .mf { color: #bd93f9 } /* Literal.Number.Float */ 69 | .highlight .mh { color: #bd93f9 } /* Literal.Number.Hex */ 70 | .highlight .mi { color: #bd93f9 } /* Literal.Number.Integer */ 71 | .highlight .mo { color: #bd93f9 } /* Literal.Number.Oct */ 72 | .highlight .sa { color: #f1fa8c } /* Literal.String.Affix */ 73 | .highlight .sb { color: #f1fa8c } /* Literal.String.Backtick */ 74 | .highlight .sc { color: #f1fa8c } /* Literal.String.Char */ 75 | .highlight .dl { color: #f1fa8c } /* Literal.String.Delimiter */ 76 | .highlight .sd { color: #f1fa8c } /* Literal.String.Doc */ 77 | .highlight .s2 { color: #f1fa8c } /* Literal.String.Double */ 78 | .highlight .se { color: #f1fa8c } /* Literal.String.Escape */ 79 | .highlight .sh { color: #f1fa8c } /* Literal.String.Heredoc */ 80 | .highlight .si { color: #f1fa8c } /* Literal.String.Interpol */ 81 | .highlight .sx { color: #f1fa8c } /* Literal.String.Other */ 82 | .highlight .sr { color: #f1fa8c } /* Literal.String.Regex */ 83 | .highlight .s1 { color: #f1fa8c } /* Literal.String.Single */ 84 | .highlight .ss { color: #f1fa8c } /* Literal.String.Symbol */ 85 | .highlight .bp { color: #f8f8f2; font-style: italic } /* Name.Builtin.Pseudo */ 86 | .highlight .fm { color: #50fa7b } /* Name.Function.Magic */ 87 | .highlight .vc { color: #8be9fd; font-style: italic } /* Name.Variable.Class */ 88 | .highlight .vg { color: #8be9fd; font-style: italic } /* Name.Variable.Global */ 89 | .highlight .vi { color: #8be9fd; font-style: italic } /* Name.Variable.Instance */ 90 | .highlight .vm { color: #8be9fd; font-style: italic } /* Name.Variable.Magic */ 91 | .highlight .il { color: #bd93f9 } /* Literal.Number.Integer.Long */ 92 | -------------------------------------------------------------------------------- /source/_static/my_style.css: -------------------------------------------------------------------------------- 1 | .wy-nav-content { 2 | max-width: 1200px !important; 3 | } 4 | -------------------------------------------------------------------------------- /source/appendix-a/index.rst: -------------------------------------------------------------------------------- 1 | 附录 A:Rust 系统编程资料 2 | ============================= 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | 9 | .. .. note:: 10 | 11 | .. **Rust 语法卡片:外部符号引用** 12 | 13 | .. extern "C" 可以引用一个外部的 C 函数接口(这意味着调用它的时候要遵从目标平台的 C 语言调用规范)。但我们这里只是引用位置标志 14 | .. 并将其转成 usize 获取它的地址。由此可以知道 ``.bss`` 段两端的地址。 15 | 16 | .. **Rust 语法卡片:迭代器与闭包** 17 | 18 | .. 代码第 7 行用到了 Rust 的迭代器与闭包的语法,它们在很多情况下能够提高开发效率。如读者感兴趣的话也可以将其改写为等价的 for 19 | .. 循环实现。 20 | 21 | .. .. _term-raw-pointer: 22 | .. .. _term-dereference: 23 | .. .. warning:: 24 | 25 | .. **Rust 语法卡片:Unsafe** 26 | 27 | .. 代码第 8 行,我们将 ``.bss`` 段内的一个地址转化为一个 **裸指针** (Raw Pointer),并将它指向的值修改为 0。这在 C 语言中是 28 | .. 一种司空见惯的操作,但在 Rust 中我们需要将他包裹在 unsafe 块中。这是因为,Rust 认为对于裸指针的 **解引用** (Dereference) 29 | .. 是一种 unsafe 行为。 30 | 31 | .. 相比 C 语言,Rust 进行了更多的语义约束来保证安全性(内存安全/类型安全/并发安全),这在编译期和运行期都有所体现。但在某些时候, 32 | .. 尤其是与底层硬件打交道的时候,在 Rust 的语义约束之内没法满足我们的需求,这个时候我们就需要将超出了 Rust 语义约束的行为包裹 33 | .. 在 unsafe 块中,告知编译器不需要对它进行完整的约束检查,而是由程序员自己负责保证它的安全性。当代码不能正常运行的时候,我们往往也是 34 | .. 最先去检查 unsafe 块中的代码,因为它没有受到编译器的保护,出错的概率更大。 35 | 36 | .. C 语言中的指针相当于 Rust 中的裸指针,它无所不能但又太过于灵活,程序员对其不谨慎的使用常常会引起很多内存不安全问题,最常见的如 37 | .. 悬垂指针和多次回收的问题,Rust 编译器没法确认程序员对它的使用是否安全,因此将其划到 unsafe Rust 的领域。在 safe Rust 中,我们 38 | .. 有引用 ``&/&mut`` 以及各种功能各异的智能指针 ``Box/RefCell/Rc`` 可以使用,只要按照 Rust 的规则来使用它们便可借助 39 | .. 编译器在编译期就解决很多潜在的内存不安全问题。 40 | 41 | Rust编程相关 42 | -------------------------------- 43 | 44 | - `OS Tutorial Summer of Code 2020:Rust系统编程入门指导 `_ 45 | - `Stanford 新开的一门很值得学习的 Rust 入门课程 `_ 46 | - `一份简单的 Rust 入门介绍 `_ 47 | - `《RustOS Guide》中的 Rust 介绍部分 `_ 48 | - `一份简单的Rust宏编程新手指南 `_ 49 | 50 | 51 | Rust系统编程pattern 52 | --------------------------------- 53 | 54 | - `Arc> in Rust `_ 55 | - `Understanding Closures in Rust `_ 56 | - `Closures in Rust `_ -------------------------------------------------------------------------------- /source/appendix-c/index.rst: -------------------------------------------------------------------------------- 1 | 附录 C:深入机器模式:RustSBI 2 | ================================================= 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | RISC-V指令集的SBI标准规定了类Unix操作系统之下的运行环境规范。这个规范拥有多种实现,RustSBI是它的一种实现。 9 | 10 | RISC-V架构中,存在着定义于操作系统之下的运行环境。这个运行环境不仅将引导启动RISC-V下的操作系统, 还将常驻后台,为操作系统提供一系列二进制接口,以便其获取和操作硬件信息。 RISC-V给出了此类环境和二进制接口的规范,称为“操作系统二进制接口”,即“SBI”。 11 | 12 | SBI的实现是在M模式下运行的特定于平台的固件,它将管理S、U等特权上的程序或通用的操作系统。 13 | 14 | RustSBI项目发起于鹏城实验室的“rCore代码之夏-2020”活动,它是完全由Rust语言开发的SBI实现。 现在它能够在支持的RISC-V设备上运行rCore教程和其它操作系统内核。 15 | 16 | RustSBI项目的目标是,制作一个从固件启动的最小Rust语言SBI实现,为可能的复杂实现提供参考和支持。 RustSBI也可以作为一个库使用,帮助更多的SBI开发者适配自己的平台,以支持更多处理器核和片上系统。 17 | 18 | 当前项目实现源码:https://github.com/luojia65/rustsbi -------------------------------------------------------------------------------- /source/appendix-d/index.rst: -------------------------------------------------------------------------------- 1 | 附录 D:RISC-V相关信息 2 | ================================================= 3 | 4 | RISCV汇编相关 5 | ----------------------------------------------- 6 | 7 | - `RISC-V Assembly Programmer's Manual `_ 8 | - `RISC-V Low-level Test Suits `_ 9 | - `CoreMark®-PRO comprehensive, advanced processor benchmark `_ 10 | - `riscv-tests的使用 `_ 11 | 12 | RISCV硬件相关 13 | ----------------------------------------------- 14 | 15 | Quick Reference 16 | 17 | - `Registers & ABI `_ 18 | - `Interrupt `_ 19 | - `ISA & Extensions `_ 20 | - `Toolchain `_ 21 | - `Control and Status Registers (CSRs) `_ 22 | - `Accessing CSRs `_ 23 | - `Assembler & Instructions `_ 24 | 25 | ISA 26 | 27 | - `User-Level ISA, Version 1.12 `_ 28 | - `4 Supervisor-Level ISA, Version 1.12 `_ 29 | - `Vector Extension `_ 30 | - `RISC-V Bitmanip Extension `_ 31 | - `External Debug `_ 32 | - `ISA Resources `_ -------------------------------------------------------------------------------- /source/chapter1/0intro.rst: -------------------------------------------------------------------------------- 1 | 引言 2 | ===================== 3 | 4 | 本章导读 5 | -------------------------- 6 | 7 | 大多数程序员的职业生涯都从 ``Hello, world!`` 开始。 8 | 9 | .. code-block:: 10 | 11 | printf("Hello world!\n"); 12 | cout << "Hello world!\n"; 13 | print("Hello world!") 14 | System.out.println("Hello world!"); 15 | echo "Hello world!" 16 | println!("Hello world!"); 17 | 18 | 然而,要用几行代码向世界问好,并不像表面上那么简单。 19 | ``Hello, world!`` 程序能够编译运行,靠的是以 **编译器** 为主的开发环境和以 **操作系统** 为主的执行环境。 20 | 21 | 在本章中,我们将抽丝剥茧,一步步让 ``Hello, world!`` 程序脱离其依赖的执行环境, 22 | 编写一个能打印 ``Hello, world!`` 的 OS。这趟旅途将让我们对应用程序及其执行环境有更深入的理解。 23 | 24 | .. attention:: 25 | 实验指导书存在的目的是帮助读者理解框架代码。 26 | 27 | 为便于测试,完成编程实验时,请以框架代码为基础,不必跟着文档从零开始编写内核。 28 | 29 | 为了做到这一步,首先需要让程序不依赖于标准库, 30 | 并通过编译。 31 | 32 | 接下来要让脱离了标准库的程序能输出(即支持 ``println!``),这对程序的开发和调试至关重要。 33 | 我们先在用户态下实现该功能,在 `此处 `_ 获取相关代码。 34 | 35 | 最后把程序移植到内核态,构建在裸机上支持输出的最小运行时环境。 36 | 37 | 实践体验 38 | --------------------------- 39 | 40 | 本章一步步实现了支持打印字符串的简单操作系统。 41 | 42 | 获取本章代码: 43 | 44 | .. code-block:: console 45 | 46 | $ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2023S 47 | $ cd rCore-Tutorial-Code-2023S 48 | $ git checkout ch1 49 | 50 | 运行本章代码,并设置日志级别为 ``TRACE``: 51 | 52 | .. code-block:: console 53 | 54 | $ cd os 55 | $ make run LOG=TRACE 56 | 57 | 58 | 预期输出: 59 | 60 | .. figure:: color-demo.png 61 | :align: center 62 | 63 | 除了 ``Hello, world!`` 之外还有一些额外的信息,最后关机。 64 | 65 | 本章代码树 66 | ------------------------------------------------ 67 | 68 | 69 | .. code-block:: 70 | 71 | ├── bootloader (内核依赖的运行在 M 特权级的 SBI 实现,本项目中我们使用 RustSBI) 72 | │   └── rustsbi-qemu.bin 73 | ├── os 74 | │   ├── Cargo.toml (cargo 项目配置文件) 75 | │   ├── Makefile 76 | │   └── src 77 | │   ├── console.rs (将打印字符的 SBI 接口进一步封装实现更加强大的格式化输出) 78 | │   ├── entry.asm (设置内核执行环境的的一段汇编代码) 79 | │   ├── lang_items.rs (需要我们提供给 Rust 编译器的一些语义项,目前包含内核 panic 时的处理逻辑) 80 | │   ├── linker.ld (控制内核内存布局的链接脚本以使内核运行在 qemu 虚拟机上) 81 | │   ├── logging.rs (为本项目实现了日志功能) 82 | │   ├── main.rs (内核主函数) 83 | │   └── sbi.rs (封装底层 SBI 实现提供的 SBI 接口) 84 | └── rust-toolchain (整个项目的工具链版本) 85 | 86 | cloc os 87 | ------------------------------------------------------------------------------- 88 | Language files blank comment code 89 | ------------------------------------------------------------------------------- 90 | Rust 5 25 6 155 91 | make 1 11 4 34 92 | Assembly 1 1 0 11 93 | TOML 1 2 1 7 94 | ------------------------------------------------------------------------------- 95 | SUM: 8 39 11 207 96 | ------------------------------------------------------------------------------- -------------------------------------------------------------------------------- /source/chapter1/1app-ee-platform.rst: -------------------------------------------------------------------------------- 1 | 应用程序执行环境与平台支持 2 | ================================================ 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 5 7 | 8 | 9 | 执行应用程序 10 | ------------------------------- 11 | 12 | 我们先从最简单的 Rust ``Hello, world`` 程序开始,用 Cargo 工具创建 Rust 项目。 13 | 14 | .. code-block:: console 15 | 16 | $ cargo new os 17 | 18 | 此时,项目的文件结构如下: 19 | 20 | .. code-block:: console 21 | 22 | $ tree os 23 | os 24 | ├── Cargo.toml 25 | └── src 26 | └── main.rs 27 | 28 | 1 directory, 2 files 29 | 30 | 其中 ``Cargo.toml`` 中保存了项目的库依赖、作者信息等。 31 | 32 | cargo 为我们准备好了 ``Hello world!`` 源代码: 33 | 34 | .. code-block:: rust 35 | :linenos: 36 | :caption: 最简单的 Rust 应用 37 | 38 | fn main() { 39 | println!("Hello, world!"); 40 | } 41 | 42 | 输入 ``cargo run`` 构建并运行项目: 43 | 44 | .. code-block:: console 45 | 46 | Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os) 47 | Finished dev [unoptimized + debuginfo] target(s) in 1.15s 48 | Running `target/debug/os` 49 | Hello, world! 50 | 51 | 我们在屏幕上看到了一行 ``Hello, world!`` ,但为了打印出 ``Hello, world!``,我们需要的不止几行源代码。 52 | 53 | 理解应用程序执行环境 54 | ------------------------------- 55 | 56 | 在现代通用操作系统(如 Linux)上运行应用程序,需要多层次的执行环境栈支持: 57 | 58 | 59 | .. figure:: app-software-stack.png 60 | :align: center 61 | 62 | 应用程序执行环境栈:图中的白色块自上而下表示各级执行环境,黑色块则表示相邻两层执行环境之间的接口。 63 | 下层作为上层的执行环境,支持上层代码运行。 64 | 65 | 我们的应用程序通过调用标准库或第三方库提供的接口,仅需少量源代码就能完成复杂的功能; 66 | ``Hello, world!`` 程序调用的 ``println!`` 宏就是由 Rust 标准库 std 和 GNU Libc 等提供的。 67 | 这些库属于应用程序的 **执行环境** (Execution Environment),而它们的实现又依赖于操作系统提供的系统调用。 68 | 69 | 平台与目标三元组 70 | --------------------------------------- 71 | 72 | 编译器在编译、链接得到可执行文件时需要知道,程序要在哪个 **平台** (Platform) 上运行, 73 | **目标三元组** (Target Triplet) 描述了目标平台的 CPU 指令集、操作系统类型和标准运行时库。 74 | 75 | 我们研究一下现在 ``Hello, world!`` 程序的目标三元组是什么: 76 | 77 | .. code-block:: console 78 | 79 | $ rustc --version --verbose 80 | rustc 1.61.0-nightly (68369a041 2022-02-22) 81 | binary: rustc 82 | commit-hash: 68369a041cea809a87e5bd80701da90e0e0a4799 83 | commit-date: 2022-02-22 84 | host: x86_64-unknown-linux-gnu 85 | release: 1.61.0-nightly 86 | LLVM version: 14.0.0 87 | 88 | 其中 host 一项表明默认目标平台是 ``x86_64-unknown-linux-gnu``, 89 | CPU 架构是 x86_64,CPU 厂商是 unknown,操作系统是 linux,运行时库是 gnu libc。 90 | 91 | 接下来,我们希望把 ``Hello, world!`` 移植到 RICV 目标平台 ``riscv64gc-unknown-none-elf`` 上运行。 92 | 93 | .. note:: 94 | 95 | ``riscv64gc-unknown-none-elf`` 的 CPU 架构是 riscv64gc,厂商是 unknown,操作系统是 none, 96 | elf 表示没有标准的运行时库。没有任何系统调用的封装支持,但可以生成 ELF 格式的执行程序。 97 | 我们不选择有 linux-gnu 支持的 ``riscv64gc-unknown-linux-gnu``,是因为我们的目标是开发操作系统内核,而非在 linux 系统上运行的应用程序。 98 | 99 | 修改目标平台 100 | ---------------------------------- 101 | 102 | 将程序的目标平台换成 ``riscv64gc-unknown-none-elf``,试试看会发生什么: 103 | 104 | .. code-block:: console 105 | 106 | $ cargo run --target riscv64gc-unknown-none-elf 107 | Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os) 108 | error[E0463]: can't find crate for `std` 109 | | 110 | = note: the `riscv64gc-unknown-none-elf` target may not be installed 111 | 112 | 113 | 报错的原因是目标平台上确实没有 Rust 标准库 std,也不存在任何受 OS 支持的系统调用。 114 | 这样的平台被我们称为 **裸机平台** (bare-metal)。 115 | 116 | 幸运的是,除了 std 之外,Rust 还有一个不需要任何操作系统支持的核心库 core, 117 | 它包含了 Rust 语言相当一部分核心机制,可以满足本门课程的需求。 118 | 有很多第三方库也不依赖标准库 std,而仅仅依赖核心库 core。 119 | 120 | 为了以裸机平台为目标编译程序,我们要将对标准库 std 的引用换成核心库 core。 -------------------------------------------------------------------------------- /source/chapter1/2remove-std.rst: -------------------------------------------------------------------------------- 1 | .. _term-remove-std: 2 | 3 | 移除标准库依赖 4 | ========================== 5 | 6 | .. toctree:: 7 | :hidden: 8 | :maxdepth: 5 9 | 10 | 11 | 首先在 ``os`` 目录下新建 ``.cargo`` 目录,并在这个目录下创建 ``config`` 文件,输入如下内容: 12 | 13 | .. code-block:: toml 14 | 15 | # os/.cargo/config 16 | [build] 17 | target = "riscv64gc-unknown-none-elf" 18 | 19 | 20 | 这将使 cargo 工具在 os 目录下默认会使用 riscv64gc-unknown-none-elf 作为目标平台。 21 | 这种编译器运行的平台(x86_64)与可执行文件运行的目标平台不同的情况,称为 **交叉编译** (Cross Compile)。 22 | 23 | 移除 println! 宏 24 | ---------------------------------- 25 | 26 | 27 | 我们在 ``main.rs`` 的开头加上一行 ``#![no_std]``, 28 | 告诉 Rust 编译器不使用 Rust 标准库 std 转而使用核心库 core。重新编译,报错如下: 29 | 30 | .. error:: 31 | 32 | .. code-block:: console 33 | 34 | $ cargo build 35 | Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os) 36 | error: cannot find macro `println` in this scope 37 | --> src/main.rs:4:5 38 | | 39 | 4 | println!("Hello, world!"); 40 | | ^^^^^^^ 41 | 42 | println! 宏是由标准库 std 提供的,且会使用到一个名为 write 的系统调用。 43 | 无论如何,我们先将这行代码注释掉。 44 | 45 | 46 | 提供语义项 panic_handler 47 | ---------------------------------------------------- 48 | 49 | .. error:: 50 | 51 | .. code-block:: console 52 | 53 | $ cargo build 54 | Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os) 55 | error: `#[panic_handler]` function required, but not found 56 | 57 | 标准库 std 提供了 Rust 错误处理函数 ``#[panic_handler]``,其大致功能是打印出错位置和原因并杀死当前应用。 58 | 但核心库 core 并没有提供这项功能,得靠我们自己实现。 59 | 60 | 新建一个子模块 ``lang_items.rs``,在里面编写 panic 处理函数,通过标记 ``#[panic_handler]`` 告知编译器采用我们的实现: 61 | 62 | .. code-block:: rust 63 | 64 | // os/src/lang_items.rs 65 | use core::panic::PanicInfo; 66 | 67 | #[panic_handler] 68 | fn panic(_info: &PanicInfo) -> ! { 69 | loop {} 70 | } 71 | 72 | 目前我们遇到错误什么都不做,只在原地 ``loop`` 。 73 | 74 | 移除 main 函数 75 | ----------------------------- 76 | 77 | 重新编译,又有了新错误: 78 | 79 | .. error:: 80 | 81 | .. code-block:: 82 | 83 | $ cargo build 84 | Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os) 85 | error: requires `start` lang_item 86 | 87 | 编译器提醒我们缺少一个名为 ``start`` 的语义项。 88 | ``start`` 语义项代表了标准库 std 在执行应用程序之前需要进行的一些初始化工作。由于我们禁用了标准库,编译器也就找不到这项功能的实现了。 89 | 90 | 在 ``main.rs`` 的开头加入设置 ``#![no_main]`` 告诉编译器我们没有一般意义上的 ``main`` 函数, 91 | 并将原来的 ``main`` 函数删除。这样编译器也就不需要考虑初始化工作了。 92 | 93 | .. code-block:: console 94 | 95 | $ cargo build 96 | Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os) 97 | Finished dev [unoptimized + debuginfo] target(s) in 0.06s 98 | 99 | 至此,我们终于移除了所有标准库依赖,目前的代码如下: 100 | 101 | .. code-block:: rust 102 | 103 | // os/src/main.rs 104 | #![no_std] 105 | #![no_main] 106 | 107 | mod lang_items; 108 | 109 | // os/src/lang_items.rs 110 | use core::panic::PanicInfo; 111 | 112 | #[panic_handler] 113 | fn panic(_info: &PanicInfo) -> ! { 114 | loop {} 115 | } 116 | 117 | 118 | 分析被移除标准库的程序 119 | ----------------------------- 120 | 121 | 我们可以通过一些工具来分析目前的程序: 122 | 123 | .. code-block:: console 124 | 125 | [文件格式] 126 | $ file target/riscv64gc-unknown-none-elf/debug/os 127 | target/riscv64gc-unknown-none-elf/debug/os: ELF 64-bit LSB executable, UCB RISC-V, ...... 128 | 129 | [文件头信息] 130 | $ rust-readobj -h target/riscv64gc-unknown-none-elf/debug/os 131 | File: target/riscv64gc-unknown-none-elf/debug/os 132 | Format: elf64-littleriscv 133 | Arch: riscv64 134 | AddressSize: 64bit 135 | ...... 136 | Type: Executable (0x2) 137 | Machine: EM_RISCV (0xF3) 138 | Version: 1 139 | Entry: 0x0 140 | ...... 141 | } 142 | 143 | [反汇编导出汇编程序] 144 | $ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os 145 | target/riscv64gc-unknown-none-elf/debug/os: file format elf64-littleriscv 146 | 147 | 148 | 通过 ``file`` 工具对二进制程序 ``os`` 的分析可以看到,它好像是一个合法的 RV64 执行程序, 149 | 但 ``rust-readobj`` 工具告诉我们它的入口地址 Entry 是 ``0``。 150 | 再通过 ``rust-objdump`` 工具把它反汇编,没有生成任何汇编代码。 151 | 可见,这个二进制程序虽然合法,但它是一个空程序,原因是缺少了编译器规定的入口函数 ``_start`` 。 152 | 153 | 从下一节开始,我们将着手实现本节移除的、由用户态执行环境提供的功能。 154 | 155 | .. note:: 156 | 157 | 本节内容部分参考自 `BlogOS 的相关章节 `_ 。 158 | 159 | -------------------------------------------------------------------------------- /source/chapter1/3mini-rt-usrland.rst: -------------------------------------------------------------------------------- 1 | .. _term-print-userminienv: 2 | 3 | 构建用户态执行环境 4 | ================================= 5 | 6 | .. toctree:: 7 | :hidden: 8 | :maxdepth: 5 9 | 10 | .. note:: 11 | 12 | 前三小节的用户态程序案例代码在 `此处 `_ 获取。 13 | 14 | 15 | 用户态最小化执行环境 16 | ---------------------------- 17 | 18 | 执行环境初始化 19 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 20 | 21 | 首先我们要给 Rust 编译器编译器提供入口函数 ``_start()`` , 22 | 在 ``main.rs`` 中添加如下内容: 23 | 24 | 25 | .. code-block:: rust 26 | 27 | // os/src/main.rs 28 | #[no_mangle] 29 | extern "C" fn _start() { 30 | loop{}; 31 | } 32 | 33 | 34 | 对上述代码重新编译,再用分析工具分析: 35 | 36 | 37 | .. code-block:: console 38 | 39 | $ cargo build 40 | Compiling os v0.1.0 (/home/shinbokuow/workspace/v3/rCore-Tutorial-v3/os) 41 | Finished dev [unoptimized + debuginfo] target(s) in 0.06s 42 | 43 | [反汇编导出汇编程序] 44 | $ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os 45 | target/riscv64gc-unknown-none-elf/debug/os: file format elf64-littleriscv 46 | 47 | Disassembly of section .text: 48 | 49 | 0000000000011120 <_start>: 50 | ; loop {} 51 | 11120: 09 a0 j 2 <_start+0x2> 52 | 11122: 01 a0 j 0 <_start+0x2> 53 | 54 | 55 | 反汇编出的两条指令就是一个死循环, 56 | 这说明编译器生成的已经是一个合理的程序了。 57 | 用 ``qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os`` 命令可以执行这个程序。 58 | 59 | 60 | 程序正常退出 61 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 62 | 63 | 我们把 ``_start()`` 函数中的循环语句注释掉,重新编译并分析,看到其汇编代码是: 64 | 65 | 66 | .. code-block:: console 67 | 68 | $ rust-objdump -S target/riscv64gc-unknown-none-elf/debug/os 69 | 70 | target/riscv64gc-unknown-none-elf/debug/os: file format elf64-littleriscv 71 | 72 | 73 | Disassembly of section .text: 74 | 75 | 0000000000011120 <_start>: 76 | ; } 77 | 11120: 82 80 ret 78 | 79 | 看起来是合法的执行程序。但如果我们执行它,会引发问题: 80 | 81 | .. code-block:: console 82 | 83 | $ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os 84 | 段错误 (核心已转储) 85 | 86 | 这个简单的程序导致 ``qemu-riscv64`` 崩溃了!为什么会这样? 87 | 88 | .. note:: 89 | 90 | QEMU有两种运行模式: 91 | 92 | ``User mode`` 模式,即用户态模拟,如 ``qemu-riscv64`` 程序, 93 | 能够模拟不同处理器的用户态指令的执行,并可以直接解析ELF可执行文件, 94 | 加载运行那些为不同处理器编译的用户级Linux应用程序。 95 | 96 | ``System mode`` 模式,即系统态模式,如 ``qemu-system-riscv64`` 程序, 97 | 能够模拟一个完整的基于不同CPU的硬件系统,包括处理器、内存及其他外部设备,支持运行完整的操作系统。 98 | 99 | 100 | 目前的执行环境还缺了一个退出机制,我们需要操作系统提供的 ``exit`` 系统调用来退出程序。这里先给出代码: 101 | 102 | .. code-block:: rust 103 | 104 | // os/src/main.rs 105 | 106 | const SYSCALL_EXIT: usize = 93; 107 | 108 | fn syscall(id: usize, args: [usize; 3]) -> isize { 109 | let mut ret; 110 | unsafe { 111 | core::arch::asm!( 112 | "ecall", 113 | inlateout("x10") args[0] => ret, 114 | in("x11") args[1], 115 | in("x12") args[2], 116 | in("x17") id, 117 | ); 118 | } 119 | ret 120 | } 121 | 122 | pub fn sys_exit(xstate: i32) -> isize { 123 | syscall(SYSCALL_EXIT, [xstate as usize, 0, 0]) 124 | } 125 | 126 | #[no_mangle] 127 | extern "C" fn _start() { 128 | sys_exit(9); 129 | } 130 | 131 | ``main.rs`` 增加的内容不多,但还是有点与一般的应用程序有所不同,因为它引入了汇编和系统调用。 132 | 第二章的第二节 :doc:`/chapter2/2application` 会详细介绍上述代码的含义。 133 | 这里读者只需要知道 ``_start`` 函数调用了一个 ``sys_exit`` 函数, 134 | 向操作系统发出了退出的系统调用请求,退出码为 ``9`` 。 135 | 136 | 我们编译执行以下修改后的程序: 137 | 138 | .. code-block:: console 139 | 140 | $ cargo build --target riscv64gc-unknown-none-elf 141 | Compiling os v0.1.0 (/media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/os_kernel_lab/os) 142 | Finished dev [unoptimized + debuginfo] target(s) in 0.26s 143 | 144 | [打印程序的返回值] 145 | $ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os; echo $? 146 | 9 147 | 148 | 可以看到,返回的结果确实是 ``9`` 。这样,我们勉强完成了一个简陋的用户态最小化执行环境。 149 | 150 | 151 | 有显示支持的用户态执行环境 152 | ---------------------------- 153 | 154 | 没有 ``println`` 输出信息,终究觉得缺了点啥。 155 | 156 | Rust 的 core 库内建了以一系列帮助实现显示字符的基本 Trait 和数据结构,函数等,我们可以对其中的关键部分进行扩展,就可以实现定制的 ``println!`` 功能。 157 | 158 | 159 | 实现输出字符串的相关函数 160 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 161 | .. attention:: 162 | 163 | 如果你觉得理解 Rust 宏有困难,把它当成黑盒就好! 164 | 165 | 166 | 首先封装一下对 ``SYSCALL_WRITE`` 系统调用。 167 | 168 | .. code-block:: rust 169 | 170 | const SYSCALL_WRITE: usize = 64; 171 | 172 | pub fn sys_write(fd: usize, buffer: &[u8]) -> isize { 173 | syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()]) 174 | } 175 | 176 | 然后实现基于 ``Write`` Trait 的数据结构,并完成 ``Write`` Trait 所需要的 ``write_str`` 函数,并用 ``print`` 函数进行包装。 177 | 178 | 179 | .. code-block:: rust 180 | 181 | struct Stdout; 182 | 183 | impl Write for Stdout { 184 | fn write_str(&mut self, s: &str) -> fmt::Result { 185 | sys_write(1, s.as_bytes()); 186 | Ok(()) 187 | } 188 | } 189 | 190 | pub fn print(args: fmt::Arguments) { 191 | Stdout.write_fmt(args).unwrap(); 192 | } 193 | 194 | 最后,实现基于 ``print`` 函数,实现Rust语言 **格式化宏** ( `formatting macros `_ )。 195 | 196 | 197 | .. code-block:: rust 198 | 199 | #[macro_export] 200 | macro_rules! print { 201 | ($fmt: literal $(, $($arg: tt)+)?) => { 202 | $crate::console::print(format_args!($fmt $(, $($arg)+)?)); 203 | } 204 | } 205 | 206 | #[macro_export] 207 | macro_rules! println { 208 | ($fmt: literal $(, $($arg: tt)+)?) => { 209 | print(format_args!(concat!($fmt, "\n") $(, $($arg)+)?)); 210 | } 211 | } 212 | 213 | 接下来,我们调整一下应用程序,让它发出显示字符串和退出的请求: 214 | 215 | .. code-block:: rust 216 | 217 | #[no_mangle] 218 | extern "C" fn _start() { 219 | println!("Hello, world!"); 220 | sys_exit(9); 221 | } 222 | 223 | 224 | 现在,我们编译并执行一下,可以看到正确的字符串输出,且程序也能正确退出! 225 | 226 | 227 | .. code-block:: console 228 | 229 | $ cargo build --target riscv64gc-unknown-none-elf 230 | Compiling os v0.1.0 (/media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/os_kernel_lab/os) 231 | Finished dev [unoptimized + debuginfo] target(s) in 0.61s 232 | 233 | $ qemu-riscv64 target/riscv64gc-unknown-none-elf/debug/os; echo $? 234 | Hello, world! 235 | 9 236 | 237 | 238 | .. 下面出错的情况是会在采用 linker.ld,加入了 .cargo/config 239 | .. 的内容后会出错: 240 | .. .. [build] 241 | .. .. target = "riscv64gc-unknown-none-elf" 242 | .. .. [target.riscv64gc-unknown-none-elf] 243 | .. .. rustflags = [ 244 | .. .. "-Clink-arg=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes" 245 | .. .. ] 246 | 247 | .. 重新定义了栈和地址空间布局后才会出错 248 | 249 | .. 段错误 (核心已转储) 250 | 251 | .. 系统崩溃了!借助以往的操作系统内核编程经验和与下一节调试kernel的成果经验,我们直接定位为是 **栈** (Stack) 没有设置的问题。我们需要添加建立栈的代码逻辑。 252 | 253 | .. .. code-block:: asm 254 | 255 | .. # entry.asm 256 | 257 | .. .section .text.entry 258 | .. .globl _start 259 | .. _start: 260 | .. la sp, boot_stack_top 261 | .. call rust_main 262 | 263 | .. .section .bss.stack 264 | .. .globl boot_stack 265 | .. boot_stack: 266 | .. .space 4096 * 16 267 | .. .globl boot_stack_top 268 | .. boot_stack_top: 269 | 270 | .. 然后把汇编代码嵌入到 ``main.rs`` 中,并进行微调。 271 | 272 | .. .. code-block:: rust 273 | 274 | .. #![feature(global_asm)] 275 | 276 | .. global_asm!(include_str!("entry.asm")); 277 | 278 | .. #[no_mangle] 279 | .. #[link_section=".text.entry"] 280 | .. extern "C" fn rust_main() { 281 | 282 | .. 再次编译执行,可以看到正确的字符串输出,且程序也能正确结束! 283 | -------------------------------------------------------------------------------- /source/chapter1/4mini-rt-baremetal.rst: -------------------------------------------------------------------------------- 1 | .. _term-print-kernelminienv: 2 | 3 | 构建裸机执行环境 4 | ================================= 5 | 6 | .. toctree:: 7 | :hidden: 8 | :maxdepth: 5 9 | 10 | 有了上一节实现的用户态的最小执行环境,稍加改造,就可以完成裸机上的最小执行环境了。 11 | 本节中,我们将把 ``Hello world!`` 应用程序从用户态搬到内核态。 12 | 13 | 14 | 裸机启动过程 15 | ---------------------------- 16 | 17 | 用 QEMU 软件 ``qemu-system-riscv64`` 来模拟 RISC-V 64 计算机。加载内核程序的命令如下: 18 | 19 | .. code-block:: bash 20 | 21 | qemu-system-riscv64 \ 22 | -machine virt \ 23 | -nographic \ 24 | -bios $(BOOTLOADER) \ 25 | -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) 26 | 27 | 28 | - ``-bios $(BOOTLOADER)`` 意味着硬件加载了一个 BootLoader 程序,即 RustSBI 29 | - ``-device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)`` 表示硬件内存中的特定位置 ``$(KERNEL_ENTRY_PA)`` 放置了操作系统的二进制代码 ``$(KERNEL_BIN)`` 。 ``$(KERNEL_ENTRY_PA)`` 的值是 ``0x80200000`` 。 30 | 31 | 当我们执行包含上述启动参数的 qemu-system-riscv64 软件,就意味给这台虚拟的 RISC-V64 计算机加电了。 32 | 此时,CPU 的其它通用寄存器清零,而 PC 会指向 ``0x1000`` 的位置,这里有固化在硬件中的一小段引导代码, 33 | 它会很快跳转到 ``0x80000000`` 的 RustSBI 处。 34 | RustSBI完成硬件初始化后,会跳转到 ``$(KERNEL_BIN)`` 所在内存位置 ``0x80200000`` 处, 35 | 执行操作系统的第一条指令。 36 | 37 | .. figure:: chap1-intro.png 38 | :align: center 39 | 40 | .. note:: 41 | 42 | **RustSBI 是什么?** 43 | 44 | SBI 是 RISC-V 的一种底层规范,RustSBI 是它的一种实现。 45 | 操作系统内核与 RustSBI 的关系有点像应用与操作系统内核的关系,后者向前者提供一定的服务。只是SBI提供的服务很少, 46 | 比如关机,显示字符串等。 47 | 48 | 实现关机功能 49 | ---------------------------- 50 | 51 | 对上一节实现的代码稍作调整,通过 ``ecall`` 调用 RustSBI 实现关机功能: 52 | 53 | .. _term-llvm-sbicall: 54 | 55 | .. code-block:: rust 56 | 57 | // bootloader/rustsbi-qemu.bin 直接添加的SBI规范实现的二进制代码,给操作系统提供基本支持服务 58 | 59 | // os/src/sbi.rs 60 | fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize { 61 | let mut ret; 62 | unsafe { 63 | core::arch::asm!( 64 | "ecall", 65 | ... 66 | 67 | const SBI_SHUTDOWN: usize = 8; 68 | 69 | pub fn shutdown() -> ! { 70 | sbi_call(SBI_SHUTDOWN, 0, 0, 0); 71 | panic!("It should shutdown!"); 72 | } 73 | 74 | // os/src/main.rs 75 | #[no_mangle] 76 | extern "C" fn _start() { 77 | shutdown(); 78 | } 79 | 80 | 81 | 应用程序访问操作系统提供的系统调用的指令是 ``ecall`` ,操作系统访问 82 | RustSBI提供的SBI调用的指令也是 ``ecall`` , 83 | 虽然指令一样,但它们所在的特权级是不一样的。 84 | 简单地说,应用程序位于最弱的用户特权级(User Mode), 85 | 操作系统位于内核特权级(Supervisor Mode), 86 | RustSBI位于机器特权级(Machine Mode)。 87 | 下一章会进一步阐释具体细节。 88 | 89 | 编译执行,结果如下: 90 | 91 | .. code-block:: bash 92 | 93 | # 编译生成ELF格式的执行文件 94 | $ cargo build --release 95 | Compiling os v0.1.0 (/media/chyyuu/ca8c7ba6-51b7-41fc-8430-e29e31e5328f/thecode/rust/os_kernel_lab/os) 96 | Finished release [optimized] target(s) in 0.15s 97 | # 把ELF执行文件转成bianary文件 98 | $ rust-objcopy --binary-architecture=riscv64 target/riscv64gc-unknown-none-elf/release/os --strip-all -O binary target/riscv64gc-unknown-none-elf/release/os.bin 99 | 100 | # 加载运行 101 | $ qemu-system-riscv64 -machine virt -nographic -bios ../bootloader/rustsbi-qemu.bin -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000 102 | # 无法退出,风扇狂转,感觉碰到死循环 103 | 104 | 问题在哪?通过 rust-readobj 分析 ``os`` 可执行程序,发现其入口地址不是 105 | RustSBI 约定的 ``0x80200000`` 。我们需要修改程序的内存布局并设置好栈空间。 106 | 107 | 108 | 设置正确的程序内存布局 109 | ---------------------------- 110 | 111 | 可以通过 **链接脚本** (Linker Script) 调整链接器的行为,使得最终生成的可执行文件的内存布局符合我们的预期。 112 | 113 | 修改 Cargo 的配置文件来使用我们自己的链接脚本 ``os/src/linker.ld``: 114 | 115 | .. code-block:: 116 | :linenos: 117 | :emphasize-lines: 5,6,7,8 118 | 119 | // os/.cargo/config 120 | [build] 121 | target = "riscv64gc-unknown-none-elf" 122 | 123 | [target.riscv64gc-unknown-none-elf] 124 | rustflags = [ 125 | "-Clink-arg=-Tsrc/linker.ld", "-Cforce-frame-pointers=yes" 126 | ] 127 | 128 | 具体的链接脚本 ``os/src/linker.ld`` 如下: 129 | 130 | .. code-block:: 131 | :linenos: 132 | 133 | OUTPUT_ARCH(riscv) 134 | ENTRY(_start) 135 | BASE_ADDRESS = 0x80200000; 136 | 137 | SECTIONS 138 | { 139 | . = BASE_ADDRESS; 140 | skernel = .; 141 | 142 | stext = .; 143 | .text : { 144 | *(.text.entry) 145 | *(.text .text.*) 146 | } 147 | 148 | . = ALIGN(4K); 149 | etext = .; 150 | srodata = .; 151 | .rodata : { 152 | *(.rodata .rodata.*) 153 | } 154 | 155 | . = ALIGN(4K); 156 | erodata = .; 157 | sdata = .; 158 | .data : { 159 | *(.data .data.*) 160 | } 161 | 162 | . = ALIGN(4K); 163 | edata = .; 164 | .bss : { 165 | *(.bss.stack) 166 | sbss = .; 167 | *(.bss .bss.*) 168 | } 169 | 170 | . = ALIGN(4K); 171 | ebss = .; 172 | ekernel = .; 173 | 174 | /DISCARD/ : { 175 | *(.eh_frame) 176 | } 177 | } 178 | 179 | 第 1 行我们设置了目标平台为 riscv ;第 2 行我们设置了整个程序的入口点为之前定义的全局符号 ``_start``; 180 | 第 3 行定义了一个常量 ``BASE_ADDRESS`` 为 ``0x80200000`` ,RustSBI 期望的 OS 起始地址; 181 | 182 | .. attention:: 183 | 184 | linker 脚本的语法不做要求,感兴趣的同学可以自行查阅相关资料。 185 | 186 | 从 ``BASE_ADDRESS`` 开始,代码段 ``.text``, 只读数据段 ``.rodata``,数据段 ``.data``, bss 段 ``.bss`` 由低到高依次放置, 187 | 且每个段都有两个全局变量给出其起始和结束地址(比如 ``.text`` 段的开始和结束地址分别是 ``stext`` 和 ``etext`` )。 188 | 189 | 190 | 正确配置栈空间布局 191 | ---------------------------- 192 | 193 | 用另一段汇编代码初始化栈空间: 194 | 195 | .. code-block:: asm 196 | :linenos: 197 | 198 | # os/src/entry.asm 199 | .section .text.entry 200 | .globl _start 201 | _start: 202 | la sp, boot_stack_top 203 | call rust_main 204 | 205 | .section .bss.stack 206 | .globl boot_stack 207 | boot_stack: 208 | .space 4096 * 16 209 | .globl boot_stack_top 210 | boot_stack_top: 211 | 212 | 在第 8 行,我们预留了一块大小为 4096 * 16 字节,也就是 :math:`64\text{KiB}` 的空间, 213 | 用作操作系统的栈空间。 214 | 栈顶地址被全局符号 ``boot_stack_top`` 标识,栈底则被全局符号 ``boot_stack`` 标识。 215 | 同时,这块栈空间被命名为 216 | ``.bss.stack`` ,链接脚本里有它的位置。 217 | 218 | ``_start`` 作为操作系统的入口地址,将依据链接脚本被放在 ``BASE_ADDRESS`` 处。 219 | ``la sp, boot_stack_top`` 作为 OS 的第一条指令, 220 | 将 sp 设置为栈空间的栈顶。 221 | 简单起见,我们目前不考虑 sp 越过栈底 ``boot_stack`` ,也就是栈溢出的情形。 222 | 第二条指令则是函数调用 ``rust_main`` ,这里的 ``rust_main`` 是我们稍后自己编写的应用入口。 223 | 224 | 接着,我们在 ``main.rs`` 中嵌入这些汇编代码并声明应用入口 ``rust_main`` : 225 | 226 | .. code-block:: rust 227 | :linenos: 228 | :emphasize-lines: 7,9,10,11,12 229 | 230 | // os/src/main.rs 231 | #![no_std] 232 | #![no_main] 233 | 234 | mod lang_items; 235 | 236 | core::arch::global_asm!(include_str!("entry.asm")); 237 | 238 | #[no_mangle] 239 | pub fn rust_main() -> ! { 240 | shutdown(); 241 | } 242 | 243 | 背景高亮指出了 ``main.rs`` 中新增的代码。 244 | 245 | 第 7 行,我们使用 ``global_asm`` 宏,将同目录下的汇编文件 ``entry.asm`` 嵌入到代码中。 246 | 247 | 从第 9 行开始, 248 | 我们声明了应用的入口点 ``rust_main`` ,需要注意的是,这里通过宏将 ``rust_main`` 249 | 标记为 ``#[no_mangle]`` 以避免编译器对它的名字进行混淆,不然在链接时, 250 | ``entry.asm`` 将找不到 ``main.rs`` 提供的外部符号 ``rust_main``,导致链接失败。 251 | 252 | 再次使用上节中的编译,生成和运行操作,我们看到QEMU模拟的RISC-V 64计算机 **优雅** 地退出了! 253 | 254 | .. code-block:: console 255 | # 教程使用的 RustSBI 版本比代码框架稍旧,输出有所不同 256 | $ qemu-system-riscv64 \ 257 | > -machine virt \ 258 | > -nographic \ 259 | > -bios ../bootloader/rustsbi-qemu.bin \ 260 | > -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000 261 | [rustsbi] Version 0.1.0 262 | .______ __ __ _______.___________. _______..______ __ 263 | | _ \ | | | | / | | / || _ \ | | 264 | | |_) | | | | | | (----`---| |----`| (----`| |_) || | 265 | | / | | | | \ \ | | \ \ | _ < | | 266 | | |\ \----.| `--' |.----) | | | .----) | | |_) || | 267 | | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| 268 | 269 | [rustsbi] Platform: QEMU 270 | [rustsbi] misa: RV64ACDFIMSU 271 | [rustsbi] mideleg: 0x222 272 | [rustsbi] medeleg: 0xb1ab 273 | [rustsbi] Kernel entry: 0x80200000 274 | 275 | 276 | 清空 .bss 段 277 | ---------------------------------- 278 | 279 | 等一等,与内存相关的部分太容易出错了, **清零 .bss 段** 的工作我们还没有完成。 280 | 281 | .. code-block:: rust 282 | :linenos: 283 | 284 | // os/src/main.rs 285 | fn clear_bss() { 286 | extern "C" { 287 | fn sbss(); 288 | fn ebss(); 289 | } 290 | (sbss as usize..ebss as usize).for_each(|a| { 291 | unsafe { (a as *mut u8).write_volatile(0) } 292 | }); 293 | } 294 | 295 | pub fn rust_main() -> ! { 296 | clear_bss(); 297 | shutdown(); 298 | } 299 | 300 | 链接脚本 ``linker.ld`` 中给出的全局符号 ``sbss`` 和 ``ebss`` 让我们能轻松确定 ``.bss`` 段的位置。 301 | 302 | 303 | 添加裸机打印相关函数 304 | ---------------------------------- 305 | 306 | 在上一节中我们为用户态程序实现的 ``println`` 宏,略作修改即可用于本节的内核态操作系统。 307 | 详见 ``os/src/console.rs``。 308 | 309 | 利用 ``println`` 宏,我们重写异常处理函数 ``panic``,使其在 panic 时能打印错误发生的位置。 310 | 相关代码位于 ``os/src/lang_items.rs`` 中。 311 | 312 | 我们还使用第三方库 ``log`` 为你实现了日志模块,相关代码位于 ``os/src/logging.rs`` 中。 313 | 314 | .. note:: 315 | 316 | 在 cargo 项目中引入外部库 log,需要修改 ``Cargo.toml`` 加入相应的依赖信息。 317 | 318 | 现在,让我们重复一遍本章开头的试验,``make run LOG=TRACE``! 319 | 320 | .. figure:: color-demo.png 321 | :align: center 322 | 323 | 至此,我们完成了第一章的实验内容, 324 | 325 | 326 | .. note:: 327 | 328 | 背景知识:`理解应用程序和执行环境 `_ -------------------------------------------------------------------------------- /source/chapter1/5exercise.rst: -------------------------------------------------------------------------------- 1 | chapter1练习(已经废弃,没删是怕以后有用) 2 | ===================================================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | - 本节难度: **低** 9 | 10 | 编程作业 11 | ------------------------------- 12 | 13 | 彩色化 LOG 14 | +++++++++++++++++++++++++++++++ 15 | 16 | .. lab1 的工作使得我们从硬件世界跳入了软件世界,当看到自己的小 os 可以在裸机硬件上输出 ``hello world`` 是不是很高兴呢?但是为了后续的一步开发,更好的调试环境也是必不可少的,第一章的练习要求大家实现更加炫酷的彩色log。 17 | 18 | .. 详细的原理不多说,感兴趣的同学可以参考 `ANSI转义序列 `_ ,现在执行如下这条命令试试 19 | 20 | .. .. code-block:: console 21 | 22 | .. $ echo -e "\x1b[31mhello world\x1b[0m" 23 | 24 | .. 如果你明白了我们是如何利用串口实现输出,那么要实现彩色输出就十分容易了,只需要用需要输出的字符串替换上一条命令中的 ``hello world``,用期望颜色替换 ``31(代表红色)`` 即可。 25 | 26 | .. .. warning:: 27 | 28 | .. 以下内容仅为推荐实现,不是练习要求,有时间和兴趣的同学可以尝试。 29 | 30 | .. 我们推荐实现如下几个等级的输出,输出优先级依次降低: 31 | 32 | .. .. list-table:: log 等级推荐 33 | .. :header-rows: 1 34 | .. :align: center 35 | 36 | .. * - 名称 37 | .. - 颜色 38 | .. - 用途 39 | .. * - ERROR 40 | .. - 红色(31) 41 | .. - 表示发生严重错误,很可能或者已经导致程序崩溃 42 | .. * - WARN 43 | .. - 黄色(93) 44 | .. - 表示发生不常见情况,但是并不一定导致系统错误 45 | .. * - INFO 46 | .. - 蓝色(34) 47 | .. - 比较中庸的选项,输出比较重要的信息,比较常用 48 | .. * - DEBUG 49 | .. - 绿色(32) 50 | .. - 输出信息较多,在 debug 时使用 51 | .. * - TRACE 52 | .. - 灰色(90) 53 | .. - 最详细的输出,跟踪了每一步关键路径的执行 54 | 55 | .. 我们可以输出比设定输出等级以及更高输出等级的信息,如设置 ``LOG = INFO``,则输出 ``ERROR``、``WARN``、``INFO`` 等级的信息。简单 demo 如下,输出等级为 INFO: 56 | 57 | .. .. image:: color-demo.png 58 | 59 | .. 为了方便使用彩色输出,我们要求同学们实现彩色输出的宏或者函数,用以代替 print 完成输出内核信息的功能,它们有着和 prinf 十分相似的使用格式,要求支持可变参数解析,形如: 60 | 61 | .. .. code-block:: rust 62 | 63 | .. // 这段代码输出了 os 内存空间布局,这到这些信息对于编写 os 十分重要 64 | 65 | .. info!(".text [{:#x}, {:#x})", s_text as usize, e_text as usize); 66 | .. debug!(".rodata [{:#x}, {:#x})", s_rodata as usize, e_rodata as usize); 67 | .. error!(".data [{:#x}, {:#x})", s_data as usize, e_data as usize); 68 | 69 | .. .. code-block:: c 70 | 71 | .. info("load range : [%d, %d] start = %d\n", s, e, start); 72 | 73 | .. 在以后,我们还可以在 log 信息中增加线程、CPU等信息(只是一个推荐,不做要求),这些信息将极大的方便你的代码调试。 74 | 75 | 76 | 实验要求 77 | +++++++++++++++++++++++++++++++ 78 | 79 | .. - 实现分支:ch1。 80 | .. - 完成实验指导书中的内容,在裸机上实现 ``hello world`` 输出。 81 | .. - 实现彩色输出宏(只要求可以彩色输出,不要求 log 等级控制,不要求多种颜色)。 82 | .. - 隐形要求:可以关闭内核所有输出。从 lab2 开始要求关闭内核所有输出(如果实现了 log 等级控制,那么这一点自然就实现了)。 83 | .. - 利用彩色输出宏输出 os 内存空间布局,即:输出 ``.text``、``.data``、``.rodata``、``.bss`` 各段位置,输出等级为 ``INFO``。 84 | 85 | 实验检查 86 | +++++++++++++++++++++++++++++++ 87 | 88 | .. - 实验目录要求(Rust) 89 | 90 | .. .. code-block:: 91 | 92 | .. ├── os(内核实现) 93 | .. │   ├── Cargo.toml(配置文件) 94 | .. │   ├── Makefile (要求 make run LOG=xxx 可以正确执行,可以不实现对 LOG 这一属性的支持,设置默认输出等级为 INFO) 95 | .. │   └── src(所有内核的源代码放在 os/src 目录下) 96 | .. │   ├── main.rs(内核主函数) 97 | .. │   └── ... 98 | .. ├── reports 99 | .. │   ├── lab1.md/pdf 100 | .. │   └── ... 101 | .. ├── README.md(其他必要的说明) 102 | .. ├── ... 103 | 104 | .. 报告命名 labx.md/pdf,统一放在 reports 目录下。每个实验新增一个报告,为了方便修改,检查报告是以最新分支的所有报告为准。 105 | 106 | .. - 检查 107 | 108 | .. .. code-block:: console 109 | 110 | .. $ cd os 111 | .. $ git checkout ch1 112 | .. $ make run LOG=INFO 113 | 114 | .. 可以正确执行(可以不支持LOG参数,只有要彩色输出就好),可以看到正确的内存布局输出,根据实现不同数值可能有差异,但应该位于 ``linker.ld`` 中指示 ``BASE_ADDRESS`` 后一段内存,输出之后关机。 115 | 116 | 问答作业 117 | ------------------------------- 118 | 119 | .. 1. 为了方便 os 处理,M态软件会将 S 态异常/中断委托给 S 态软件,请指出有哪些寄存器记录了委托信息,rustsbi 委托了哪些异常/中断?(也可以直接给出寄存器的值) 120 | 121 | .. 2. 请学习 gdb 调试工具的使用(这对后续调试很重要),并通过 gdb 简单跟踪从机器加电到跳转到 0x80200000 的简单过程。只需要描述重要的跳转即可,只需要描述在 qemu 上的情况。 122 | 123 | .. 3. tips: 124 | 125 | .. - 事实上进入 rustsbi 之后就不需要使用 gdb 调试了。可以直接阅读代码。`rustsbi起始代码 `_ 。 126 | .. - 可以使用示例代码 Makefile 中的 ``make debug`` 指令。 127 | .. - 一些可能用到的 gdb 指令: 128 | .. - ``x/10i 0x80000000`` : 显示 0x80000000 处的10条汇编指令。 129 | .. - ``x/10i $pc`` : 显示即将执行的10条汇编指令。 130 | .. - ``x/10xw 0x80000000`` : 显示 0x80000000 处的10条数据,格式为16进制32bit。 131 | .. - ``info register``: 显示当前所有寄存器信息。 132 | .. - ``info r t0``: 显示 t0 寄存器的值。 133 | .. - ``break funcname``: 在目标函数第一条指令处设置断点。 134 | .. - ``break *0x80200000``: 在 0x80200000 出设置断点。 135 | .. - ``continue``: 执行直到碰到断点。 136 | .. - ``si``: 单步执行一条汇编指令。 137 | 138 | 报告要求 139 | ------------------------------- 140 | 141 | - 简单总结你实现的功能(200字以内,不要贴代码)。 142 | - 完成问答题。 143 | - (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 -------------------------------------------------------------------------------- /source/chapter1/app-software-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter1/app-software-stack.png -------------------------------------------------------------------------------- /source/chapter1/chap1-intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter1/chap1-intro.png -------------------------------------------------------------------------------- /source/chapter1/color-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter1/color-demo.png -------------------------------------------------------------------------------- /source/chapter1/index.rst: -------------------------------------------------------------------------------- 1 | .. _link-chapter1: 2 | 3 | 第一章:应用程序与基本执行环境 4 | ============================================== 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | 9 | 0intro 10 | 1app-ee-platform 11 | 2remove-std 12 | 3mini-rt-usrland 13 | 4mini-rt-baremetal 14 | -------------------------------------------------------------------------------- /source/chapter2/0intro.rst: -------------------------------------------------------------------------------- 1 | 引言 2 | ================================ 3 | 4 | 本章导读 5 | --------------------------------- 6 | 7 | 8 | **批处理系统** (Batch System) 出现于计算资源匮乏的年代,其核心思想是: 9 | 将多个程序打包到一起输入计算机;当一个程序运行结束后,计算机会 *自动* 执行下一个程序。 10 | 11 | 应用程序难免会出错,如果一个程序的错误导致整个操作系统都无法运行,那就太糟糕了。 12 | *保护* 操作系统不受出错程序破坏的机制被称为 **特权级** (Privilege) 机制, 13 | 它实现了用户态和内核态的隔离。 14 | 15 | 本章在上一章的基础上,让我们的 OS 内核能以批处理的形式一次运行多个应用程序,同时利用特权级机制, 16 | 令 OS 不因出错的用户态程序而崩溃。 17 | 18 | 本章首先为批处理操作系统设计用户程序,再阐述如何将这些应用程序链接到内核中,最后介绍如何利用特权级机制处理 Trap. 19 | 20 | 实践体验 21 | --------------------------- 22 | 23 | 本章我们引入了用户程序。为了将内核与应用解耦,我们将二者分成了两个仓库,分别是存放内核程序的 ``rCore-Tutorial-Code-20xxx`` (下称代码仓库,最后几位 x 表示学期)与存放用户程序的 ``rCore-Tutorial-Test-20xxx`` (下称测例仓库)。 你首先需要进入代码仓库文件夹并 clone 用户程序仓库(如果已经执行过该步骤则不需要再重复执行): 24 | 25 | .. code-block:: console 26 | 27 | $ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2023S.git 28 | $ cd rCore-Tutorial-Code-2023S 29 | $ git checkout ch2 30 | $ git clone https://github.com/LearningOS/rCore-Tutorial-Test-2023S.git user 31 | 32 | 上面的指令会将测例仓库克隆到代码仓库下并命名为 ``user`` ,注意 ``/user`` 在代码仓库的 ``.gitignore`` 文件中,因此不会出现 ``.git`` 文件夹嵌套的问题,并且你在代码仓库进行 checkout 操作时也不会影响测例仓库的内容。 33 | 34 | 在 qemu 模拟器上运行本章代码: 35 | 36 | .. code-block:: console 37 | 38 | $ cd os 39 | $ make run LOG=INFO 40 | 41 | 批处理系统自动加载并运行了所有的用户程序,尽管某些程序出错了: 42 | 43 | .. code-block:: 44 | 45 | [rustsbi] RustSBI version 0.2.0-alpha.4 46 | .______ __ __ _______.___________. _______..______ __ 47 | | _ \ | | | | / | | / || _ \ | | 48 | | |_) | | | | | | (----`---| |----`| (----`| |_) || | 49 | | / | | | | \ \ | | \ \ | _ < | | 50 | | |\ \----.| `--' |.----) | | | .----) | | |_) || | 51 | | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| 52 | 53 | [rustsbi] Implementation: RustSBI-QEMU Version 0.0.1 54 | [rustsbi-dtb] Hart count: cluster0 with 1 cores 55 | [rustsbi] misa: RV64ACDFIMSU 56 | [rustsbi] mideleg: ssoft, stimer, sext (0x222) 57 | [rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab) 58 | [rustsbi] pmp0: 0x80000000 ..= 0x800fffff (rwx) 59 | [rustsbi] pmp1: 0x80000000 ..= 0x807fffff (rwx) 60 | [rustsbi] pmp2: 0x0 ..= 0xffffffffffffff (---) 61 | [rustsbi] enter supervisor 0x80200000 62 | [kernel] Hello, world! 63 | [ INFO] [kernel] num_app = 6 64 | [ INFO] [kernel] app_0 [0x8020b040, 0x8020f868) 65 | [ INFO] [kernel] app_1 [0x8020f868, 0x80214090) 66 | [ INFO] [kernel] app_2 [0x80214090, 0x80218988) 67 | [ INFO] [kernel] app_3 [0x80218988, 0x8021d160) 68 | [ INFO] [kernel] app_4 [0x8021d160, 0x80221a68) 69 | [ INFO] [kernel] app_5 [0x80221a68, 0x80226538) 70 | [ INFO] [kernel] Loading app_0 71 | [ERROR] [kernel] PageFault in application, core dumped. 72 | [ INFO] [kernel] Loading app_1 73 | [ERROR] [kernel] IllegalInstruction in application, core dumped. 74 | [ INFO] [kernel] Loading app_2 75 | [ERROR] [kernel] IllegalInstruction in application, core dumped. 76 | [ INFO] [kernel] Loading app_3 77 | [ INFO] [kernel] Application exited with code 1234 78 | [ INFO] [kernel] Loading app_4 79 | Hello, world from user mode program! 80 | [ INFO] [kernel] Application exited with code 0 81 | [ INFO] [kernel] Loading app_5 82 | 3^10000=5079(MOD 10007) 83 | 3^20000=8202(MOD 10007) 84 | 3^30000=8824(MOD 10007) 85 | 3^40000=5750(MOD 10007) 86 | 3^50000=3824(MOD 10007) 87 | 3^60000=8516(MOD 10007) 88 | 3^70000=2510(MOD 10007) 89 | 3^80000=9379(MOD 10007) 90 | 3^90000=2621(MOD 10007) 91 | 3^100000=2749(MOD 10007) 92 | Test power OK! 93 | [ INFO] [kernel] Application exited with code 0 94 | Panicked at src/batch.rs:68 All applications completed! 95 | 96 | 本章代码树 97 | ------------------------------------------------- 98 | 99 | .. code-block:: 100 | 101 | ── os 102 | │   ├── Cargo.toml 103 | │   ├── Makefile (修改:构建内核之前先构建应用) 104 | │   ├── build.rs (新增:生成 link_app.S 将应用作为一个数据段链接到内核) 105 | │   └── src 106 | │   ├── batch.rs(新增:实现了一个简单的批处理系统) 107 | │   ├── console.rs 108 | │   ├── entry.asm 109 | │   ├── lang_items.rs 110 | │   ├── link_app.S(构建产物,由 os/build.rs 输出) 111 | │   ├── linker.ld 112 | │   ├── logging.rs 113 | │   ├── main.rs(修改:主函数中需要初始化 Trap 处理并加载和执行应用) 114 | │   ├── sbi.rs 115 | │   ├── sync(新增:包装了RefCell,暂时不用关心) 116 | │   │   ├── mod.rs 117 | │   │   └── up.rs 118 | │   ├── syscall(新增:系统调用子模块 syscall) 119 | │   │   ├── fs.rs(包含文件 I/O 相关的 syscall) 120 | │   │   ├── mod.rs(提供 syscall 方法根据 syscall ID 进行分发处理) 121 | │   │   └── process.rs(包含任务处理相关的 syscall) 122 | │   └── trap(新增:Trap 相关子模块 trap) 123 | │   ├── context.rs(包含 Trap 上下文 TrapContext) 124 | │   ├── mod.rs(包含 Trap 处理入口 trap_handler) 125 | │   └── trap.S(包含 Trap 上下文保存与恢复的汇编代码) 126 | └── user(新增:应用测例保存在 user 目录下) 127 | ├── Cargo.toml 128 | ├── Makefile 129 | └── src 130 | ├── bin(基于用户库 user_lib 开发的应用,每个应用放在一个源文件中) 131 | │   ├── ... 132 | ├── console.rs 133 | ├── lang_items.rs 134 | ├── lib.rs(用户库 user_lib) 135 | ├── linker.ld(应用的链接脚本) 136 | └── syscall.rs(包含 syscall 方法生成实际用于系统调用的汇编指令, 137 | 各个具体的 syscall 都是通过 syscall 来实现的) 138 | 139 | cloc os 140 | ------------------------------------------------------------------------------- 141 | Language files blank comment code 142 | ------------------------------------------------------------------------------- 143 | Rust 14 62 21 435 144 | Assembly 3 9 16 106 145 | make 1 12 4 36 146 | TOML 1 2 1 9 147 | ------------------------------------------------------------------------------- 148 | SUM: 19 85 42 586 149 | ------------------------------------------------------------------------------- 150 | -------------------------------------------------------------------------------- /source/chapter2/2application.rst: -------------------------------------------------------------------------------- 1 | 实现应用程序 2 | =========================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 5 7 | 8 | .. note:: 9 | 10 | 拓展阅读:`RISC-V 特权级机制 `_ 11 | 12 | 13 | 应用程序设计 14 | ----------------------------- 15 | 16 | .. attention:: 17 | 18 | 用户库看起来很复杂,它预留了直到 ch7 内核才能实现的系统调用接口,console 模块还实现了输出缓存区。它们不是为本章准备的,你只需关注本节提到的部分即可。 19 | 20 | 21 | 应用程序、用户库(包括入口函数、初始化函数、I/O函数和系统调用接口等多个rs文件组成)放在项目根目录的 ``user`` 目录下: 22 | 23 | - user/src/bin/*.rs:各个应用程序 24 | - user/src/*.rs:用户库(包括入口函数、初始化函数、I/O函数和系统调用接口等) 25 | - user/src/linker.ld:应用程序的内存布局说明 26 | 27 | 项目结构 28 | ^^^^^^^^^^^^^^^^^^^^^^ 29 | 30 | ``user/src/bin`` 里面有多个文件,其中三个是: 31 | 32 | - ``hello_world``:在屏幕上打印一行 ``Hello, world!`` 33 | - ``bad_address``:访问一个非法的物理地址,测试批处理系统是否会被该错误影响 34 | - ``power``:不断在计算操作和打印字符串操作之间切换 35 | 36 | 批处理系统会按照文件名顺序加载并运行它们。 37 | 38 | 每个应用程序的实现都在对应的单个文件中。打开 ``hello_world.rs``,能看到一个 ``main`` 函数,还有外部库引用: 39 | 40 | .. code-block:: rust 41 | 42 | #[macro_use] 43 | extern crate user_lib; 44 | 45 | 这个外部库其实就是 ``user`` 目录下的 ``lib.rs`` 以及它引用的若干子模块。 46 | 在 ``user/Cargo.toml`` 中我们对于库的名字进行了设置: ``name = "user_lib"`` 。 47 | 它作为 ``bin`` 目录下的源程序所依赖的用户库,等价于其他编程语言提供的标准库。 48 | 49 | 在 ``lib.rs`` 中我们定义了用户库的入口点 ``_start`` : 50 | 51 | .. code-block:: rust 52 | :linenos: 53 | 54 | #[no_mangle] 55 | #[link_section = ".text.entry"] 56 | pub extern "C" fn _start() -> ! { 57 | clear_bss(); 58 | exit(main()); 59 | } 60 | 61 | 第 2 行使用 ``link_section`` 宏将 ``_start`` 函数编译后的汇编代码放在名为 ``.text.entry`` 的代码段中, 62 | 方便用户库链接脚本将它作为用户程序的入口。 63 | 64 | 而从第 4 行开始,我们手动清零 ``.bss`` 段,然后调用 ``main`` 函数得到一个类型为 ``i32`` 的返回值, 65 | 最后,调用用户库提供的 ``exit`` 接口退出,并将返回值告知批处理系统。 66 | 67 | 我们在 ``lib.rs`` 中看到了另一个 ``main`` : 68 | 69 | .. code-block:: rust 70 | :linenos: 71 | 72 | #![feature(linkage)] // 启用弱链接特性 73 | 74 | #[linkage = "weak"] 75 | #[no_mangle] 76 | fn main() -> i32 { 77 | panic!("Cannot find main!"); 78 | } 79 | 80 | 我们使用 Rust 宏将其标志为弱链接。这样在最后链接的时候, 81 | 虽然 ``lib.rs`` 和 ``bin`` 目录下的某个应用程序中都有 ``main`` 符号, 82 | 但由于 ``lib.rs`` 中的 ``main`` 符号是弱链接, 83 | 链接器会使用 ``bin`` 目录下的函数作为 ``main`` 。 84 | 如果在 ``bin`` 目录下找不到任何 ``main`` ,那么编译也能通过,但会在运行时报错。 85 | 86 | 内存布局 87 | ^^^^^^^^^^^^^^^^^^^^^^ 88 | 89 | 我们使用链接脚本 ``user/src/linker.ld`` 规定用户程序的内存布局: 90 | 91 | - 将程序的起始物理地址调整为 ``0x80400000`` ,三个应用程序都会被加载到这个物理地址上运行; 92 | - 将 ``_start`` 所在的 ``.text.entry`` 放在整个程序的开头 ``0x80400000``; 93 | 批处理系统在加载应用后,跳转到 ``0x80400000``,就进入了用户库的 ``_start`` 函数; 94 | - 提供了最终生成可执行文件的 ``.bss`` 段的起始和终止地址,方便 ``clear_bss`` 函数使用。 95 | 96 | 其余的部分和第一章基本相同。 97 | 98 | 系统调用 99 | ^^^^^^^^^^^^^^^^^^^^^^ 100 | 101 | 在子模块 ``syscall`` 中我们来通过 ``ecall`` 调用批处理系统提供的接口, 102 | 由于应用程序运行在用户态(即 U 模式), ``ecall`` 指令会触发名为 ``Environment call from U-mode`` 的异常, 103 | 并 Trap 进入 S 模式执行批处理系统针对这个异常特别提供的服务程序。 104 | 这个接口被称为 ABI 或者系统调用。 105 | 现在我们不关心 S 态的批处理系统如何提供应用程序所需的功能,只考虑如何使用它。 106 | 107 | 在本章中,应用程序和批处理系统约定如下两个系统调用: 108 | 109 | .. code-block:: rust 110 | :caption: 第二章新增系统调用 111 | 112 | /// 功能:将内存中缓冲区中的数据写入文件。 113 | /// 参数:`fd` 表示待写入文件的文件描述符; 114 | /// `buf` 表示内存中缓冲区的起始地址; 115 | /// `len` 表示内存中缓冲区的长度。 116 | /// 返回值:返回成功写入的长度。 117 | /// syscall ID:64 118 | fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize; 119 | 120 | /// 功能:退出应用程序并将返回值告知批处理系统。 121 | /// 参数:`xstate` 表示应用程序的返回值。 122 | /// 返回值:该系统调用不应该返回。 123 | /// syscall ID:93 124 | fn sys_exit(xstate: usize) -> !; 125 | 126 | 实际调用时,我们要按照 RISC-V 调用规范,在合适的寄存器中放置参数, 127 | 然后执行 ``ecall`` 指令触发 Trap。当 Trap 结束,回到 U 模式后, 128 | 用户程序会从 ``ecall`` 的下一条指令继续执行,同时在合适的寄存器中读取返回值。 129 | 130 | .. note:: 131 | 132 | RISC-V 寄存器编号从 ``0~31`` ,表示为 ``x0~x31`` 。 其中: 133 | - ``x10~x17`` : 对应 ``a0~a7`` 134 | - ``x1`` :对应 ``ra`` 135 | 136 | 约定寄存器 ``a0~a6`` 保存系统调用的参数, ``a0`` 保存系统调用的返回值, 137 | 寄存器 ``a7`` 用来传递 syscall ID。 138 | 这超出了 Rust 语言的表达能力,我们需要内嵌汇编来完成参数/返回值绑定和 ``ecall`` 指令的插入: 139 | 140 | .. code-block:: rust 141 | :linenos: 142 | 143 | // user/src/syscall.rs 144 | 145 | fn syscall(id: usize, args: [usize; 3]) -> isize { 146 | let mut ret: isize; 147 | unsafe { 148 | core::arch::asm!( 149 | "ecall", 150 | inlateout("x10") args[0] => ret, 151 | in("x11") args[1], 152 | in("x12") args[2], 153 | in("x17") id 154 | ); 155 | } 156 | ret 157 | } 158 | 159 | 第 3 行,我们将所有的系统调用都封装成 ``syscall`` 函数,可以看到它支持传入 syscall ID 和 3 个参数。 160 | 161 | 第 6 行开始,我们使用 Rust 提供的 ``asm!`` 宏在代码中内嵌汇编。 162 | Rust 编译器无法判定汇编代码的安全性,所以我们需要将其包裹在 unsafe 块中。 163 | 164 | 简而言之,这条汇编代码的执行结果是以寄存器 ``a0~a2`` 来保存系统调用的参数,以及寄存器 ``a7`` 保存 syscall ID, 165 | 返回值通过寄存器 ``a0`` 传递给局部变量 ``ret``。 166 | 167 | 这段汇编代码与第一章中出现过的内嵌汇编很像,读者可以查看 ``os/src/sbi.rs`` 。 168 | 169 | .. note:: 170 | 171 | 可以查看 `Inline assembly `_ 了解 ``asm`` 宏。 172 | 173 | 于是 ``sys_write`` 和 ``sys_exit`` 只需将 ``syscall`` 进行包装: 174 | 175 | .. code-block:: rust 176 | :linenos: 177 | 178 | // user/src/syscall.rs 179 | 180 | const SYSCALL_WRITE: usize = 64; 181 | const SYSCALL_EXIT: usize = 93; 182 | 183 | pub fn sys_write(fd: usize, buffer: &[u8]) -> isize { 184 | syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()]) 185 | } 186 | 187 | pub fn sys_exit(xstate: i32) -> isize { 188 | syscall(SYSCALL_EXIT, [xstate as usize, 0, 0]) 189 | } 190 | 191 | 我们将上述两个系统调用在用户库 ``user_lib`` 中进一步封装,像标准库一样: 192 | 193 | .. code-block:: rust 194 | :linenos: 195 | 196 | // user/src/lib.rs 197 | use syscall::*; 198 | 199 | pub fn write(fd: usize, buf: &[u8]) -> isize { sys_write(fd, buf) } 200 | pub fn exit(exit_code: i32) -> isize { sys_exit(exit_code) } 201 | 202 | 在 ``console`` 子模块中,借助 ``write``,我们为应用程序实现了 ``println!`` 宏。 203 | 传入到 ``write`` 的 ``fd`` 参数设置为 1,代表标准输出 STDOUT,暂时不用考虑其他的 ``fd`` 选取情况。 204 | 205 | 206 | 编译生成应用程序二进制码 207 | ------------------------------- 208 | 209 | 简要介绍一下应用程序的构建,在 ``user`` 目录下 ``make build``: 210 | 211 | 1. 对于 ``src/bin`` 下的每个应用程序, 212 | 在 ``target/riscv64gc-unknown-none-elf/release`` 目录下生成一个同名的 ELF 可执行文件; 213 | 2. 使用 objcopy 二进制工具删除所有 ELF header 和符号,得到 ``.bin`` 后缀的纯二进制镜像文件。 214 | 它们将被链接进内核,并由内核在合适的时机加载到内存。 215 | -------------------------------------------------------------------------------- /source/chapter2/3batch-system.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _term-batchos: 3 | 4 | 实现批处理操作系统 5 | ============================== 6 | 7 | .. toctree:: 8 | :hidden: 9 | :maxdepth: 5 10 | 11 | 将应用程序链接到内核 12 | -------------------------------------------- 13 | 14 | 在本章中,我们要把应用程序的二进制镜像文件作为数据段链接到内核里, 15 | 内核需要知道应用程序的数量和它们的位置。 16 | 17 | 在 ``os/src/main.rs`` 中能够找到这样一行: 18 | 19 | .. code-block:: rust 20 | 21 | core::arch::global_asm!(include_str!("link_app.S")); 22 | 23 | 这里我们引入了一段汇编代码 ``link_app.S`` ,它是在 ``make run`` 构建操作系统时自动生成的,里面的内容大致如下: 24 | 25 | .. code-block:: asm 26 | :linenos: 27 | 28 | # os/src/link_app.S 29 | 30 | .align 3 31 | .section .data 32 | .global _num_app 33 | _num_app: 34 | .quad 3 35 | .quad app_0_start 36 | .quad app_1_start 37 | .quad app_2_start 38 | .quad app_2_end 39 | 40 | .section .data 41 | .global app_0_start 42 | .global app_0_end 43 | app_0_start: 44 | .incbin "../user/target/riscv64gc-unknown-none-elf/release/hello_world.bin" 45 | app_0_end: 46 | 47 | .section .data 48 | .global app_1_start 49 | .global app_1_end 50 | app_1_start: 51 | .incbin "../user/target/riscv64gc-unknown-none-elf/release/bad_address.bin" 52 | app_1_end: 53 | 54 | .section .data 55 | .global app_2_start 56 | .global app_2_end 57 | app_2_start: 58 | .incbin "../user/target/riscv64gc-unknown-none-elf/release/power.bin" 59 | app_2_end: 60 | 61 | 第 13 行开始的三个数据段分别插入了三个应用程序的二进制镜像, 62 | 并且各自有一对全局符号 ``app_*_start, app_*_end`` 指示它们的开始和结束位置。 63 | 而第 3 行开始的另一个数据段相当于一个 64 位整数数组。 64 | 数组中的第一个元素表示应用程序的数量,后面则按照顺序放置每个应用程序的起始地址, 65 | 最后一个元素放置最后一个应用程序的结束位置。这样数组中相邻两个元素记录了每个应用程序的始末位置, 66 | 这个数组所在的位置由全局符号 ``_num_app`` 所指示。 67 | 68 | 这个文件是在 ``cargo build`` 时,由脚本 ``os/build.rs`` 控制生成的。 69 | 70 | 找到并加载应用程序二进制码 71 | ----------------------------------------------- 72 | 73 | 我们在 ``os`` 的 ``batch`` 子模块中实现一个应用管理器 ``AppManager`` ,结构体定义如下: 74 | 75 | .. code-block:: rust 76 | 77 | struct AppManager { 78 | num_app: usize, 79 | current_app: usize, 80 | app_start: [usize; MAX_APP_NUM + 1], 81 | } 82 | 83 | 初始化 ``AppManager`` 的全局实例: 84 | 85 | .. code-block:: rust 86 | 87 | lazy_static! { 88 | static ref APP_MANAGER: UPSafeCell = unsafe { 89 | UPSafeCell::new({ 90 | extern "C" { 91 | fn _num_app(); 92 | } 93 | let num_app_ptr = _num_app as usize as *const usize; 94 | let num_app = num_app_ptr.read_volatile(); 95 | let mut app_start: [usize; MAX_APP_NUM + 1] = [0; MAX_APP_NUM + 1]; 96 | let app_start_raw: &[usize] = 97 | core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1); 98 | app_start[..=num_app].copy_from_slice(app_start_raw); 99 | AppManager { 100 | num_app, 101 | current_app: 0, 102 | app_start, 103 | } 104 | }) 105 | }; 106 | } 107 | 108 | 初始化的逻辑很简单,就是找到 ``link_app.S`` 中提供的符号 ``_num_app`` ,并从这里开始解析出应用数量以及各个应用的开头地址。 109 | 用容器 ``UPSafeCell`` 包裹 ``AppManager`` 是为了防止全局对象 ``APP_MANAGER`` 被重复获取。 110 | 111 | .. note:: 112 | 113 | ``UPSafeCell`` 实现在 ``sync`` 模块中,调用 ``exclusive_access`` 方法能获取其内部对象的可变引用, 114 | 如果程序运行中同时存在多个这样的引用,会触发 ``already borrowed: BorrowMutError``。 115 | 116 | ``UPSafeCell`` 既提供了内部可变性,又在单核情境下防止了内部对象被重复借用,我们将在后文中多次见到它。 117 | 118 | 这里使用了外部库 ``lazy_static`` 提供的 ``lazy_static!`` 宏。 119 | 120 | ``lazy_static!`` 宏提供了全局变量的运行时初始化功能。一般情况下,全局变量必须在编译期设置初始值, 121 | 但是有些全局变量的初始化依赖于运行期间才能得到的数据。 122 | 如这里我们借助 ``lazy_static!`` 声明了一个 ``AppManager`` 结构的名为 ``APP_MANAGER`` 的全局实例, 123 | 只有在它第一次被使用到的时候才会进行实际的初始化工作。 124 | 125 | ``AppManager`` 的方法中, ``print_app_info/get_current_app/move_to_next_app`` 都相当简单直接,需要说明的是 ``load_app``: 126 | 127 | .. code-block:: rust 128 | :linenos: 129 | 130 | unsafe fn load_app(&self, app_id: usize) { 131 | if app_id >= self.num_app { 132 | panic!("All applications completed!"); 133 | } 134 | info!("[kernel] Loading app_{}", app_id); 135 | // clear icache 136 | core::arch::asm!("fence.i"); 137 | // clear app area 138 | core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, APP_SIZE_LIMIT).fill(0); 139 | let app_src = core::slice::from_raw_parts( 140 | self.app_start[app_id] as *const u8, 141 | self.app_start[app_id + 1] - self.app_start[app_id], 142 | ); 143 | let app_dst = core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, app_src.len()); 144 | app_dst.copy_from_slice(app_src); 145 | } 146 | 147 | 这个方法负责将参数 ``app_id`` 对应的应用程序的二进制镜像加载到物理内存以 ``0x80400000`` 起始的位置, 148 | 这个位置是批处理操作系统和应用程序之间约定的常数地址。 149 | 我们将从这里开始的一块内存清空,然后找到待加载应用二进制镜像的位置,并将它复制到正确的位置。 150 | 151 | 清空内存前,我们插入了一条奇怪的汇编指令 ``fence.i`` ,它是用来清理 i-cache 的。 152 | 我们知道, 缓存又分成 **数据缓存** (d-cache) 和 **指令缓存** (i-cache) 两部分,分别在 CPU 访存和取指的时候使用。 153 | 通常情况下, CPU 会认为程序的代码段不会发生变化,因此 i-cache 是一种只读缓存。 154 | 但在这里,我们会修改会被 CPU 取指的内存区域,使得 i-cache 中含有与内存不一致的内容, 155 | 必须使用 ``fence.i`` 指令手动清空 i-cache ,让里面所有的内容全部失效, 156 | 才能够保证程序执行正确性。 157 | 158 | .. warning:: 159 | 160 | **模拟器与真机的不同之处** 161 | 162 | 在 Qemu 模拟器上,即使不加刷新 i-cache 的指令,大概率也能正常运行,但在物理计算机上不是这样。 163 | 164 | ``batch`` 子模块对外暴露出如下接口: 165 | 166 | - ``init`` :调用 ``print_app_info`` 的时第一次用到了全局变量 ``APP_MANAGER`` ,它在这时完成初始化; 167 | - ``run_next_app`` :批处理操作系统的核心操作,即加载并运行下一个应用程序。 168 | 批处理操作系统完成初始化,或者应用程序运行结束/出错后会调用该函数。下节再介绍其具体实现。 -------------------------------------------------------------------------------- /source/chapter2/5exercise.rst: -------------------------------------------------------------------------------- 1 | chapter2练习(已废弃) 2 | ===================================================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | 编程练习 9 | ------------------------------- 10 | 11 | 简单安全检查 12 | +++++++++++++++++++++++++++++++ 13 | 14 | .. lab2 中,我们实现了第一个系统调用 ``sys_write``,这使得我们可以在用户态输出信息。但是 os 在提供服务的同时,还有保护 os 本身以及其他用户程序不受错误或者恶意程序破坏的功能。 15 | 16 | .. 由于还没有实现虚拟内存,我们可以在用户程序中指定一个属于其他程序字符串,并将它输出,这显然是不合理的,因此我们要对 sys_write 做检查: 17 | 18 | .. - sys_write 仅能输出位于程序本身内存空间内的数据,否则报错。 19 | 20 | 实验要求 21 | +++++++++++++++++++++++++++++++ 22 | .. - 实现分支: ch2。 23 | .. - 完成实验指导书中的内容,能运行用户态程序并执行 sys_write,sys_exit 系统调用。 24 | .. - 为 sys_write 增加安全性检查,并通过 `Rust测例 `_ 中 chapter2 对应的所有测例,测例详情见对应仓库。 25 | 26 | .. challenge: 支持多核,实现多个核运行用户程序。 27 | 28 | 实验约定 29 | ++++++++++++++++++++++++++++++ 30 | 31 | .. 在第二章的测试中,我们对于内核有如下仅仅为了测试方便的要求,请调整你的内核代码来符合这些要求。 32 | 33 | .. - 用户栈大小必须为 4096,且按照 4096 字节对齐。这一规定可以在实验4开始删除,仅仅为通过 lab2/3 测例设置。 34 | 35 | .. .. _inherit-last-ch-changes: 36 | 37 | .. .. note:: 38 | 39 | .. **如何快速继承上一章练习题的修改** 40 | 41 | .. 从这一章开始,在完成本章习题之前,首先要做的就是将上一章框架的修改继承到本章的框架代码。出于各种原因,实际上通过 ``git merge`` 并不是很方便,这里给出一种打 patch 的方法,希望能够有所帮助。 42 | 43 | .. 1. 切换到上一章的分支,通过 ``git log`` 找到你在此分支上的第一次 commit 的前一个 commit 的 ID ,复制其前 8 位,记作 ``base-commit`` 。假设分支上最新的一次 commit ID 是 ``last-commit`` 。 44 | .. 2. 确保你位于项目根目录 ``rCore-Tutorial-v3`` 下。通过 ``git diff > `` 即可在 ``patch-path`` 路径位置(比如 ``~/Desktop/chx.patch`` )生成一个描述你对于上一章分支进行的全部修改的一个补丁文件。打开看一下,它给出了每个被修改的文件中涉及了哪些块的修改,还附加了块前后的若干行代码。如果想更加灵活进行合并的话,可以通过 ``git format-patch `` 命令在当前目录下生成一组补丁,它会对于 ``base-commit`` 后面的每一次 commit 均按照顺序生成一个补丁。 45 | .. 3. 切换到本章分支,通过 ``git apply --reject `` 来将一个补丁打到当前章节上。它的大概原理是对于补丁中的每个被修改文件中的每个修改块,尝试通过块的前后若干行代码来定位它在当前分支上的位置并进行替换。有一些块可能无法匹配,此时会生成与这些块所在的文件同名的 ``*.rej`` 文件,描述了哪些块替换失败了。在项目根目录 ``rCore-Tutorial-v3`` 下,可以通过 ``find . -name *.rej`` 来找到所有相关的 ``*.rej`` 文件并手动完成替换。 46 | .. 4. 在处理完所有 ``*.rej`` 之后,将它们删除并 commit 一下。现在就可以开始本章的实验了。 47 | 48 | 49 | 实验检查 50 | ++++++++++++++++++++++++++++++ 51 | 52 | .. - 实验目录要求(Rust) 53 | 54 | .. .. code-block:: 55 | 56 | .. ├── os(内核实现) 57 | .. │   ├── build.rs (在这里实现用户程序的打包) 58 | .. │   ├── Cargo.toml(配置文件) 59 | .. │   ├── Makefile (要求 make run 可以正确执行,尽量不输出调试信息) 60 | .. │   ├── src(所有内核的源代码放在 os/src 目录下) 61 | .. │   ├── main.rs(内核主函数) 62 | .. │   ├── ... 63 | .. ├── reports 64 | .. │   ├── lab2.md/pdf 65 | .. │   └── ... 66 | .. ├── README.md(其他必要的说明) 67 | .. ├── ... 68 | 69 | .. 参考示例目录结构。目标用户目录 ``../user/build/bin``。 70 | 71 | .. - 检查 72 | 73 | .. .. code-block:: console 74 | 75 | .. $ git checkout ch2 76 | .. $ cd os 77 | .. $ make run 78 | 79 | .. 可以正确执行正确执行目标用户测例,并得到预期输出(详见测例注释)。 80 | 81 | 82 | 简答题 83 | ------------------------------- 84 | 85 | .. 1. 正确进入 U 态后,程序的特征还应有:使用 S 态特权指令,访问 S 态寄存器后会报错。目前由于一些其他原因,这些问题不太好测试,请同学们可以自行测试这些内容(参考 `前三个测例 `_ ),描述程序出错行为,同时注意注明你使用的 sbi 及其版本。 86 | 87 | .. 2. 请结合用例理解 `trap.S `_ 中两个函数 ``__alltraps`` 和 ``__restore`` 的作用,并回答如下几个问题: 88 | 89 | .. 1. L40:刚进入 ``__restore`` 时,``a0`` 代表了什么值。请指出 ``__restore`` 的两种使用情景。 90 | 91 | .. 2. L46-L51:这几行汇编代码特殊处理了哪些寄存器?这些寄存器的的值对于进入用户态有何意义?请分别解释。 92 | 93 | .. .. code-block:: riscv 94 | 95 | .. ld t0, 32*8(sp) 96 | .. ld t1, 33*8(sp) 97 | .. ld t2, 2*8(sp) 98 | .. csrw sstatus, t0 99 | .. csrw sepc, t1 100 | .. csrw sscratch, t2 101 | 102 | .. 3. L53-L59:为何跳过了 ``x2`` 和 ``x4``? 103 | 104 | .. .. code-block:: riscv 105 | 106 | .. ld x1, 1*8(sp) 107 | .. ld x3, 3*8(sp) 108 | .. .set n, 5 109 | .. .rept 27 110 | .. LOAD_GP %n 111 | .. .set n, n+1 112 | .. .endr 113 | 114 | .. 4. L63:该指令之后,``sp`` 和 ``sscratch`` 中的值分别有什么意义? 115 | 116 | .. .. code-block:: riscv 117 | 118 | .. csrrw sp, sscratch, sp 119 | 120 | .. 5. ``__restore``:中发生状态切换在哪一条指令?为何该指令执行之后会进入用户态? 121 | 122 | .. 6. L13:该指令之后,``sp`` 和 ``sscratch`` 中的值分别有什么意义? 123 | 124 | .. .. code-block:: riscv 125 | 126 | .. csrrw sp, sscratch, sp 127 | 128 | .. 7. 从 U 态进入 S 态是哪一条指令发生的? 129 | 130 | .. 3. 程序陷入内核的原因有中断和异常(系统调用),请问 riscv64 支持哪些中断 / 异常?如何判断进入内核是由于中断还是异常?描述陷入内核时的几个重要寄存器及其值。 131 | 132 | .. 4. 对于任何中断,``__alltraps`` 中都需要保存所有寄存器吗?你有没有想到一些加速 ``__alltraps`` 的方法?简单描述你的想法。 133 | 134 | 报告要求 135 | ------------------------------- 136 | 137 | - 简单总结你实现的功能(200字以内,不要贴代码)。 138 | - 完成问答题。 139 | - 推荐markdown文档格式。 140 | - (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 141 | -------------------------------------------------------------------------------- /source/chapter2/index.rst: -------------------------------------------------------------------------------- 1 | .. _link-chapter2: 2 | 3 | 第二章:批处理系统 4 | ============================================== 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | 9 | 0intro 10 | 2application 11 | 3batch-system 12 | 4trap-handling 13 | 14 | -------------------------------------------------------------------------------- /source/chapter3/0intro.rst: -------------------------------------------------------------------------------- 1 | 引言 2 | ======================================== 3 | 4 | 本章导读 5 | -------------------------- 6 | 7 | 8 | 本章的目标是实现分时多任务系统,它能并发地执行多个用户程序,并调度这些程序。为此需要实现 9 | 10 | - 一次性加载所有用户程序,减少任务切换开销; 11 | - 支持任务切换机制,保存切换前后程序上下文; 12 | - 支持程序主动放弃处理器,实现 yield 系统调用; 13 | - 以时间片轮转算法调度用户程序,实现资源的时分复用。 14 | 15 | 16 | 实践体验 17 | ------------------------------------- 18 | 19 | .. code-block:: console 20 | 21 | $ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2023S.git 22 | $ cd rCore-Tutorial-Code-2023S 23 | $ git checkout ch3 24 | $ git clone https://github.com/LearningOS/rCore-Tutorial-Test-2023S.git user 25 | 26 | 在 qemu 模拟器上运行本章代码: 27 | 28 | .. code-block:: console 29 | 30 | $ cd os 31 | $ make run 32 | 33 | 运行代码,看到用户程序交替输出信息: 34 | 35 | .. code-block:: 36 | 37 | [rustsbi] RustSBI version 0.2.0-alpha.4 38 | .______ __ __ _______.___________. _______..______ __ 39 | | _ \ | | | | / | | / || _ \ | | 40 | | |_) | | | | | | (----`---| |----`| (----`| |_) || | 41 | | / | | | | \ \ | | \ \ | _ < | | 42 | | |\ \----.| `--' |.----) | | | .----) | | |_) || | 43 | | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| 44 | 45 | [rustsbi] Implementation: RustSBI-QEMU Version 0.0.1 46 | [rustsbi-dtb] Hart count: cluster0 with 1 cores 47 | [rustsbi] misa: RV64ACDFIMSU 48 | [rustsbi] mideleg: ssoft, stimer, sext (0x222) 49 | [rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab) 50 | [rustsbi] pmp0: 0x80000000 ..= 0x800fffff (rwx) 51 | [rustsbi] pmp1: 0x80000000 ..= 0x807fffff (rwx) 52 | [rustsbi] pmp2: 0x0 ..= 0xffffffffffffff (---) 53 | [rustsbi] enter supervisor 0x80200000 54 | [kernel] Hello, world! 55 | power_3 [10000/200000] 56 | power_3 [20000/200000] 57 | power_3 [30000/200000] 58 | power_3 [40000/200000] 59 | power_3 [50000/200000] 60 | power_3 [60000/200000] 61 | power_3 [70000/200000] 62 | power_3 [80000/200000] 63 | power_3 [90000/200000] 64 | power_3 [100000/200000] 65 | power_3 [110000/200000] 66 | power_3 [120000/200000] 67 | power_3 [130000/200000] 68 | power_3 [140000/200000] 69 | power_3 [150000/200000] 70 | power_3 [160000/200000] 71 | power_3 [170000/200000] 72 | power_3 [180000/200000] 73 | power_3 [190000/200000] 74 | power_3 [200000/200000] 75 | 3^200000 = 871008973(MOD 998244353) 76 | Test power_3 OK! 77 | power_5 [10000/140000] 78 | power_5 [20000/140000] 79 | power_5 [30000/140000] 80 | power_5 [40000/140000] 81 | power_5 [50000/140000] 82 | power_5 [60000/140000] 83 | power_7 [10000/160000] 84 | power_7 [20000/160000] 85 | power_7 [30000/160000] 86 | power_7 [40000/160000] 87 | power_7 [50000/160000] 88 | power_7 [60000/160000] 89 | power_7 [70000/160000] 90 | power_7 [80000/160000] 91 | power_7 [90000/160000] 92 | power_7 [100000/160000] 93 | power_7 [110000/160000] 94 | power_7 [120000/160000] 95 | power_7 [130000/160000] 96 | power_7 [140000/160000] 97 | power_7 [150000/160000] 98 | power_7 [160000/160000] 99 | 7^160000 = 667897727(MOD 998244353) 100 | Test power_7 OK! 101 | get_time OK! 42 102 | current time_msec = 42 103 | AAAAAAAAAA [1/5] 104 | BBBBBBBBBB [1/5] 105 | CCCCCCCCCC [1/5] 106 | power_5 [70000/140000] 107 | AAAAAAAAAA [2/5] 108 | BBBBBBBBBB [2/5] 109 | CCCCCCCCCC [2/5] 110 | power_5 [80000/140000] 111 | power_5 [90000/140000] 112 | power_5 [100000/140000] 113 | power_5 [110000/140000] 114 | power_5 [120000/140000] 115 | power_5 [130000/140000] 116 | power_5 [140000/140000] 117 | 5^140000 = 386471875(MOD 998244353) 118 | Test power_5 OK! 119 | AAAAAAAAAA [3/5] 120 | BBBBBBBBBB [3/5] 121 | CCCCCCCCCC [3/5] 122 | AAAAAAAAAA [4/5] 123 | BBBBBBBBBB [4/5] 124 | CCCCCCCCCC [4/5] 125 | AAAAAAAAAA [5/5] 126 | BBBBBBBBBB [5/5] 127 | CCCCCCCCCC [5/5] 128 | Test write A OK! 129 | Test write B OK! 130 | Test write C OK! 131 | time_msec = 143 after sleeping 100 ticks, delta = 101ms! 132 | Test sleep1 passed! 133 | Test sleep OK! 134 | Panicked at src/task/mod.rs:98 All applications completed! 135 | 136 | 137 | 本章代码树 138 | --------------------------------------------- 139 | 140 | .. code-block:: 141 | 142 | ── os 143 |    ├── build.rs 144 |    ├── Cargo.toml 145 |    ├── Makefile 146 |    └── src 147 |    ├── batch.rs(移除:功能分别拆分到 loader 和 task 两个子模块) 148 |   ├── config.rs(新增:保存内核的一些配置) 149 |    ├── console.rs 150 | ├── logging.rs 151 | ├── sync 152 |    ├── entry.asm 153 |    ├── lang_items.rs 154 |    ├── link_app.S 155 |    ├── linker.ld 156 |    ├── loader.rs(新增:将应用加载到内存并进行管理) 157 |    ├── main.rs(修改:主函数进行了修改) 158 |    ├── sbi.rs(修改:引入新的 sbi call set_timer) 159 |    ├── syscall(修改:新增若干 syscall) 160 |    │   ├── fs.rs 161 |    │   ├── mod.rs 162 |    │   └── process.rs 163 |    ├── task(新增:task 子模块,主要负责任务管理) 164 |    │   ├── context.rs(引入 Task 上下文 TaskContext) 165 |    │   ├── mod.rs(全局任务管理器和提供给其他模块的接口) 166 |    │   ├── switch.rs(将任务切换的汇编代码解释为 Rust 接口 __switch) 167 |    │   ├── switch.S(任务切换的汇编代码) 168 |    │   └── task.rs(任务控制块 TaskControlBlock 和任务状态 TaskStatus 的定义) 169 |    ├── timer.rs(新增:计时器相关) 170 |    └── trap 171 |    ├── context.rs 172 |    ├── mod.rs(修改:时钟中断相应处理) 173 |    └── trap.S 174 | 175 | cloc os 176 | ------------------------------------------------------------------------------- 177 | Language files blank comment code 178 | ------------------------------------------------------------------------------- 179 | Rust 21 87 20 627 180 | Assembly 4 12 22 144 181 | make 1 11 4 36 182 | TOML 1 2 1 10 183 | ------------------------------------------------------------------------------- 184 | SUM: 27 112 47 817 185 | ------------------------------------------------------------------------------- 186 | 187 | 188 | .. 本章代码导读 189 | .. ----------------------------------------------------- 190 | 191 | .. 本章的重点是实现对应用之间的协作式和抢占式任务切换的操作系统支持。与上一章的操作系统实现相比,有如下一些不同的情况导致实现上也有差异: 192 | 193 | .. - 多个应用同时放在内存中,所以他们的起始地址是不同的,且地址范围不能重叠 194 | .. - 应用在整个执行过程中会暂停或被抢占,即会有主动或被动的任务切换 195 | 196 | .. 这些实现上差异主要集中在对应用程序执行过程的管理、支持应用程序暂停的系统调用和主动切换应用程序所需的时钟中断机制的管理。 197 | 198 | .. 对于第一个不同情况,需要对应用程序的地址空间布局进行调整,每个应用的地址空间都不相同,且不能重叠。这并不要修改应用程序本身,而是通过一个脚本 ``build.py`` 来针对每个应用程序修改链接脚本 ``linker.ld`` 中的 ``BASE_ADDRESS`` ,让编译器在编译不同应用时用到的 ``BASE_ADDRESS`` 都不同,且有足够大的地址间隔。这样就可以让每个应用所在的内存空间是不同的。 199 | 200 | .. 对于第二个不同情况,需要实现任务切换,这就需要在上一章的 ``trap`` 上下文切换的基础上,再加上一个 ``task`` 上下文切换,才能完成完整的任务切换。这里面的关键数据结构是表示应用执行上下文的 ``TaskContext`` 数据结构和具体完成上下文切换的汇编语言编写的 ``__switch`` 函数。一个应用的执行需要被操作系统管理起来,这是通过 ``TaskControlBlock`` 数据结构来表示应用执行上下文的动态过程和动态状态(运行态、就绪态等)。而为了做好应用程序第一次执行的前期初始化准备, ``TaskManager`` 数据结构的全局变量实例 ``TASK_MANAGER`` 描述了应用程序初始化所需的数据, 而 ``TASK_MANAGER`` 的初始化赋值过程是实现这个准备的关键步骤。 201 | 202 | .. 应用程序可以在用户态执行后,还需要有新的系统调用 ``sys_yield`` 的实现来支持应用自己的主动暂停;还要添加对时钟中断的处理,来支持抢占应用执行的抢占式切换。有了时钟中断,就可以在一定时间内打断应用的执行,并主动切换到另外一个应用,这部分主要是通过对 ``trap_handler`` 函数中进行扩展,来完成在时钟中断产生时可能进行的任务切换。 ``TaskManager`` 数据结构的成员函数 ``run_next_task`` 来实现基于任务控制块的切换,并会具体调用 ``__switch`` 函数完成硬件相关部分的任务上下文切换。 203 | 204 | .. 如果理解了上面的数据结构和相关函数的关系和相互调用的情况,那么就比较容易理解本章改进后的操作系统了。 205 | 206 | 207 | .. .. [#prionosuchus] 锯齿螈身长可达9米,是迄今出现过的最大的两栖动物,是二叠纪时期江河湖泊和沼泽中的顶级掠食者。 208 | .. .. [#eoraptor] 始初龙(也称始盗龙)是后三叠纪时期的两足食肉动物,也是目前所知最早的恐龙,它们只有一米长,却代表着恐龙的黎明。 209 | .. .. [#coelophysis] 腔骨龙(也称虚形龙)最早出现于三叠纪晚期,它体形纤细,善于奔跑,以小型动物为食。 210 | -------------------------------------------------------------------------------- /source/chapter3/1multi-loader.rst: -------------------------------------------------------------------------------- 1 | 多道程序放置与加载 2 | ===================================== 3 | 4 | 多道程序放置 5 | ---------------------------- 6 | 7 | 8 | 在第二章中,内核让所有应用都共享同一个固定的起始地址。 9 | 正因如此,内存中同时最多只能驻留一个应用, 10 | 11 | 要一次加载运行多个程序,就要求每个用户程序被内核加载到内存中的起始地址都不同。 12 | 为此,我们编写脚本 ``user/build.py`` 为每个应用定制各自的起始地址。 13 | 它的思路很简单,对于每一个应用程序,使用 ``cargo rustc`` 单独编译, 14 | 用 ``-Clink-args=-Ttext=xxxx`` 选项指定链接时 .text 段的地址为 ``0x80400000 + app_id * 0x20000`` 。 15 | 16 | .. note:: 17 | 18 | qemu 预留的内存空间是有限的,如果加载的程序过多,程序地址超出内存空间,可能出现 ``core dumped``. 19 | 20 | 多道程序加载 21 | ---------------------------- 22 | 23 | 在第二章中负责应用加载和执行的子模块 ``batch`` 被拆分为 ``loader`` 和 ``task`` , 24 | 前者负责启动时加载应用程序,后者负责切换和调度。 25 | 26 | 其中, ``loader`` 模块的 ``load_apps`` 函数负责将所有用户程序在内核初始化的时一并加载进内存。 27 | 28 | .. code-block:: rust 29 | :linenos: 30 | 31 | // os/src/loader.rs 32 | 33 | pub fn load_apps() { 34 | extern "C" { 35 | fn _num_app(); 36 | } 37 | let num_app_ptr = _num_app as usize as *const usize; 38 | let num_app = get_num_app(); 39 | let app_start = unsafe { core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1) }; 40 | // clear i-cache first 41 | unsafe { 42 | core::arch::asm!("fence.i"); 43 | } 44 | // load apps 45 | for i in 0..num_app { 46 | let base_i = get_base_i(i); 47 | // clear region 48 | (base_i..base_i + APP_SIZE_LIMIT) 49 | .for_each(|addr| unsafe { (addr as *mut u8).write_volatile(0) }); 50 | // load app from data section to memory 51 | let src = unsafe { 52 | core::slice::from_raw_parts(app_start[i] as *const u8, app_start[i + 1] - app_start[i]) 53 | }; 54 | let dst = unsafe { core::slice::from_raw_parts_mut(base_i as *mut u8, src.len()) }; 55 | dst.copy_from_slice(src); 56 | } 57 | } 58 | 59 | 第 :math:`i` 个应用被加载到以物理地址 ``base_i`` 开头的一段物理内存上,而 ``base_i`` 的计算方式如下: 60 | 61 | .. code-block:: rust 62 | :linenos: 63 | 64 | // os/src/loader.rs 65 | 66 | fn get_base_i(app_id: usize) -> usize { 67 | APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT 68 | } 69 | 70 | 我们可以在 ``config`` 子模块中找到这两个常数, ``APP_BASE_ADDRESS`` 被设置为 ``0x80400000`` , 71 | 而 ``APP_SIZE_LIMIT`` 和上一章一样被设置为 ``0x20000`` 。这种放置方式与 ``user/build.py`` 的实现一致。 72 | -------------------------------------------------------------------------------- /source/chapter3/2task-switching.rst: -------------------------------------------------------------------------------- 1 | 任务切换 2 | ================================ 3 | 4 | 5 | 本节我们将见识操作系统的核心机制—— **任务切换** , 6 | 即应用在运行中主动或被动地交出 CPU 的使用权,内核可以选择另一个程序继续执行。 7 | 内核需要保证用户程序两次运行期间,任务上下文(如寄存器、栈等)保持一致。 8 | 9 | 任务切换的设计与实现 10 | --------------------------------- 11 | 12 | 任务切换与上一章提及的 Trap 控制流切换相比,有如下异同: 13 | 14 | - 与 Trap 切换不同,它不涉及特权级切换,部分由编译器完成; 15 | - 与 Trap 切换相同,它对应用是透明的。 16 | 17 | 事实上,任务切换是来自两个不同应用在内核中的 Trap 控制流之间的切换。 18 | 当一个应用 Trap 到 S 态 OS 内核中进行进一步处理时, 19 | 其 Trap 控制流可以调用一个特殊的 ``__switch`` 函数。 20 | 在 ``__switch`` 返回之后,Trap 控制流将继续从调用该函数的位置继续向下执行。 21 | 而在调用 ``__switch`` 之后到返回前的这段时间里, 22 | 原 Trap 控制流 ``A`` 会先被暂停并被切换出去, CPU 转而运行另一个应用的 Trap 控制流 ``B`` 。 23 | ``__switch`` 返回之后,原 Trap 控制流 ``A`` 才会从某一条 Trap 控制流 ``C`` 切换回来继续执行。 24 | 25 | 我们需要在 ``__switch`` 中保存 CPU 的某些寄存器,它们就是 **任务上下文** (Task Context)。 26 | 27 | 下面我们给出 ``__switch`` 的实现: 28 | 29 | .. code-block:: riscv 30 | :linenos: 31 | 32 | # os/src/task/switch.S 33 | 34 | .altmacro 35 | .macro SAVE_SN n 36 | sd s\n, (\n+2)*8(a0) 37 | .endm 38 | .macro LOAD_SN n 39 | ld s\n, (\n+2)*8(a1) 40 | .endm 41 | .section .text 42 | .globl __switch 43 | __switch: 44 | # __switch( 45 | # current_task_cx_ptr: *mut TaskContext, 46 | # next_task_cx_ptr: *const TaskContext 47 | # ) 48 | # save kernel stack of current task 49 | sd sp, 8(a0) 50 | # save ra & s0~s11 of current execution 51 | sd ra, 0(a0) 52 | .set n, 0 53 | .rept 12 54 | SAVE_SN %n 55 | .set n, n + 1 56 | .endr 57 | # restore ra & s0~s11 of next execution 58 | ld ra, 0(a1) 59 | .set n, 0 60 | .rept 12 61 | LOAD_SN %n 62 | .set n, n + 1 63 | .endr 64 | # restore kernel stack of next task 65 | ld sp, 8(a1) 66 | ret 67 | 68 | 它的两个参数分别是当前和即将被切换到的 Trap 控制流的 ``task_cx_ptr`` ,从 RISC-V 调用规范可知,它们分别通过寄存器 ``a0/a1`` 传入。 69 | 70 | 内核先把 ``current_task_cx_ptr`` 中包含的寄存器值逐个保存,再把 ``next_task_cx_ptr`` 中包含的寄存器值逐个恢复。 71 | 72 | ``TaskContext`` 里包含的寄存器有: 73 | 74 | .. code-block:: rust 75 | :linenos: 76 | 77 | // os/src/task/context.rs 78 | #[repr(C)] 79 | pub struct TaskContext { 80 | ra: usize, 81 | sp: usize, 82 | s: [usize; 12], 83 | } 84 | 85 | ``s0~s11`` 是被调用者保存寄存器, ``__switch`` 是用汇编编写的,编译器不会帮我们处理这些寄存器。 86 | 保存 ``ra`` 很重要,它记录了 ``__switch`` 函数返回之后应该跳转到哪里继续执行。 87 | 88 | 我们将这段汇编代码 ``__switch`` 解释为一个 Rust 函数: 89 | 90 | .. code-block:: rust 91 | :linenos: 92 | 93 | // os/src/task/switch.rs 94 | 95 | core::arch::global_asm!(include_str!("switch.S")); 96 | 97 | extern "C" { 98 | pub fn __switch( 99 | current_task_cx_ptr: *mut TaskContext, 100 | next_task_cx_ptr: *const TaskContext); 101 | } 102 | 103 | 我们会调用该函数来完成切换功能,而不是直接跳转到符号 ``__switch`` 的地址。 104 | 因此在调用前后,编译器会帮我们保存和恢复调用者保存寄存器。 105 | -------------------------------------------------------------------------------- /source/chapter3/4time-sharing-system.rst: -------------------------------------------------------------------------------- 1 | 分时多任务系统 2 | =========================================================== 3 | 4 | 5 | 现代的任务调度算法基本都是抢占式的,它要求每个应用只能连续执行一段时间,然后内核就会将它强制性切换出去。 6 | 一般将 **时间片** (Time Slice) 作为应用连续执行时长的度量单位,每个时间片可能在毫秒量级。 7 | 简单起见,我们使用 **时间片轮转算法** (RR, Round-Robin) 来对应用进行调度。 8 | 9 | 10 | 时钟中断与计时器 11 | ------------------------------------------------------------------ 12 | 13 | 实现调度算法需要计时。RISC-V 要求处理器维护时钟计数器 ``mtime``,还有另外一个 CSR ``mtimecmp`` 。 14 | 一旦计数器 ``mtime`` 的值超过了 ``mtimecmp``,就会触发一次时钟中断。 15 | 16 | 运行在 M 特权级的 SEE 已经预留了相应的接口,基于此编写的 ``get_time`` 函数可以取得当前 ``mtime`` 计数器的值; 17 | 18 | .. code-block:: rust 19 | 20 | // os/src/timer.rs 21 | 22 | use riscv::register::time; 23 | 24 | pub fn get_time() -> usize { 25 | time::read() 26 | } 27 | 28 | 在 10 ms 后设置时钟中断的代码如下: 29 | 30 | .. code-block:: rust 31 | :linenos: 32 | 33 | // os/src/sbi.rs 34 | 35 | const SBI_SET_TIMER: usize = 0; 36 | 37 | pub fn set_timer(timer: usize) { 38 | sbi_call(SBI_SET_TIMER, timer, 0, 0); 39 | } 40 | 41 | // os/src/timer.rs 42 | 43 | use crate::config::CLOCK_FREQ; 44 | const TICKS_PER_SEC: usize = 100; 45 | 46 | pub fn set_next_trigger() { 47 | set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC); 48 | } 49 | 50 | - 第 5 行, ``sbi`` 子模块有一个 ``set_timer`` 调用,用来设置 ``mtimecmp`` 的值。 51 | - 第 14 行, ``timer`` 子模块的 ``set_next_trigger`` 函数对 ``set_timer`` 进行了封装, 52 | 它首先读取当前 ``mtime`` 的值,然后计算出 10ms 之内计数器的增量,再将 ``mtimecmp`` 设置为二者的和。 53 | 这样,10ms 之后一个 S 特权级时钟中断就会被触发。 54 | 55 | 至于增量的计算方式, ``CLOCK_FREQ`` 是一个预先获取到的各平台不同的时钟频率,单位为赫兹,也就是一秒钟之内计数器的增量。 56 | 它可以在 ``config`` 子模块中找到。10ms 的话只需除以常数 ``TICKS_PER_SEC`` 也就是 100 即可。 57 | 58 | 后面可能还有一些计时的需求,我们再设计一个函数: 59 | 60 | .. code-block:: rust 61 | 62 | // os/src/timer.rs 63 | 64 | const MICRO_PER_SEC: usize = 1_000_000; 65 | 66 | pub fn get_time_us() -> usize { 67 | time::read() / (CLOCK_FREQ / MICRO_PER_SEC) 68 | } 69 | 70 | 71 | ``timer`` 子模块的 ``get_time_us`` 可以以微秒为单位返回当前计数器的值。 72 | 73 | 新增一个系统调用,使应用能获取当前的时间: 74 | 75 | .. code-block:: rust 76 | :caption: 第三章新增系统调用(二) 77 | 78 | /// 功能:获取当前的时间,保存在 TimeVal 结构体 ts 中,_tz 在我们的实现中忽略 79 | /// 返回值:返回是否执行成功,成功则返回 0 80 | /// syscall ID:169 81 | fn sys_get_time(ts: *mut TimeVal, _tz: usize) -> isize; 82 | 83 | 结构体 ``TimeVal`` 的定义如下,内核只需调用 ``get_time_us`` 即可实现该系统调用。 84 | 85 | .. code-block:: rust 86 | 87 | // os/src/syscall/process.rs 88 | 89 | #[repr(C)] 90 | pub struct TimeVal { 91 | pub sec: usize, 92 | pub usec: usize, 93 | } 94 | 95 | RISC-V 架构中的嵌套中断问题 96 | ----------------------------------- 97 | 98 | 默认情况下,当 Trap 进入某个特权级之后,在 Trap 处理的过程中同特权级的中断都会被屏蔽。 99 | 100 | - 当 Trap 发生时,``sstatus.sie`` 会被保存在 ``sstatus.spie`` 字段中,同时 ``sstatus.sie`` 置零, 101 | 这也就在 Trap 处理的过程中屏蔽了所有 S 特权级的中断; 102 | - 当 Trap 处理完毕 ``sret`` 的时候, ``sstatus.sie`` 会恢复到 ``sstatus.spie`` 内的值。 103 | 104 | 也就是说,如果不去手动设置 ``sstatus`` CSR ,在只考虑 S 特权级中断的情况下,是不会出现 **嵌套中断** (Nested Interrupt) 的。 105 | 106 | .. note:: 107 | 108 | **嵌套中断与嵌套 Trap** 109 | 110 | 嵌套中断可以分为两部分:在处理一个中断的过程中又被同特权级/高特权级中断所打断。默认情况下硬件会避免前一部分, 111 | 也可以通过手动设置来允许前一部分的存在;而从上面介绍的规则可以知道,后一部分则是无论如何设置都不可避免的。 112 | 113 | 嵌套 Trap 则是指处理一个 Trap 过程中又再次发生 Trap ,嵌套中断算是嵌套 Trap 的一种。 114 | 115 | 116 | 抢占式调度 117 | ----------------------------------- 118 | 119 | 有了时钟中断和计时器,抢占式调度就很容易实现了: 120 | 121 | .. code-block:: rust 122 | 123 | // os/src/trap/mod.rs 124 | 125 | match scause.cause() { 126 | Trap::Interrupt(Interrupt::SupervisorTimer) => { 127 | set_next_trigger(); 128 | suspend_current_and_run_next(); 129 | } 130 | } 131 | 132 | 我们只需在 ``trap_handler`` 函数下新增一个分支,触发了 S 特权级时钟中断时,重新设置计时器, 133 | 调用 ``suspend_current_and_run_next`` 函数暂停当前应用并切换到下一个。 134 | 135 | 为了避免 S 特权级时钟中断被屏蔽,我们需要在执行第一个应用前调用 ``enable_timer_interrupt()`` 设置 ``sie.stie``, 136 | 使得 S 特权级时钟中断不会被屏蔽;再设置第一个 10ms 的计时器。 137 | 138 | .. code-block:: rust 139 | :linenos: 140 | 141 | // os/src/main.rs 142 | 143 | #[no_mangle] 144 | pub fn rust_main() -> ! { 145 | // ... 146 | trap::enable_timer_interrupt(); 147 | timer::set_next_trigger(); 148 | // ... 149 | } 150 | 151 | // os/src/trap/mod.rs 152 | 153 | use riscv::register::sie; 154 | 155 | pub fn enable_timer_interrupt() { 156 | unsafe { sie::set_stimer(); } 157 | } 158 | 159 | 就这样,我们实现了时间片轮转任务调度算法。 ``power`` 系列用户程序可以验证我们取得的成果:这些应用并没有主动 yield, 160 | 内核仍能公平地把时间片分配给它们。 161 | 162 | -------------------------------------------------------------------------------- /source/chapter3/5exercise.rst: -------------------------------------------------------------------------------- 1 | chapter3练习 2 | ======================================= 3 | 4 | 编程作业 5 | -------------------------------------- 6 | 7 | 获取任务信息 8 | ++++++++++++++++++++++++++ 9 | 10 | ch3 中,我们的系统已经能够支持多个任务分时轮流运行,我们希望引入一个新的系统调用 ``sys_task_info`` 以获取当前任务的信息,定义如下: 11 | 12 | .. code-block:: rust 13 | 14 | fn sys_task_info(ti: *mut TaskInfo) -> isize 15 | 16 | - syscall ID: 410 17 | - 查询当前正在执行的任务信息,任务信息包括任务控制块相关信息(任务状态)、任务使用的系统调用及调用次数、任务总运行时长(单位ms)。 18 | 19 | .. code-block:: rust 20 | 21 | struct TaskInfo { 22 | status: TaskStatus, 23 | syscall_times: [u32; MAX_SYSCALL_NUM], 24 | time: usize 25 | } 26 | 27 | - 参数: 28 | - ti: 待查询任务信息 29 | - 返回值:执行成功返回0,错误返回-1 30 | - 说明: 31 | - 相关结构已在框架中给出,只需添加逻辑实现功能需求即可。 32 | - 在我们的实验中,系统调用号一定小于 500,所以直接使用一个长为 ``MAX_SYSCALL_NUM=500`` 的数组做桶计数。 33 | - 运行时间 time 返回系统调用时刻距离任务第一次被调度时刻的时长,也就是说这个时长可能包含该任务被其他任务抢占后的等待重新调度的时间。 34 | - 由于查询的是当前任务的状态,因此 TaskStatus 一定是 Running。(助教起初想设计根据任务 id 查询,但是既不好定义任务 id 也不好写测例,遂放弃 QAQ) 35 | - 调用 ``sys_task_info`` 也会对本次调用计数。 36 | - 提示: 37 | - 大胆修改已有框架!除了配置文件,你几乎可以随意修改已有框架的内容。 38 | - 程序运行时间可以通过调用 ``get_time()`` 获取,注意任务运行总时长的单位是 ms。 39 | - 系统调用次数可以考虑在进入内核态系统调用异常处理函数之后,进入具体系统调用函数之前维护。 40 | - 阅读 TaskManager 的实现,思考如何维护内核控制块信息(可以在控制块可变部分加入需要的信息)。 41 | - 虽然系统调用接口采用桶计数,但是内核采用相同的方法进行维护会遇到什么问题?是不是可以用其他结构计数? 42 | 43 | 44 | 实验要求 45 | +++++++++++++++++++++++++++++++++++++++++ 46 | 47 | - 完成分支: ch3。 48 | 49 | - 实验目录要求 50 | 51 | .. code-block:: 52 | 53 | ├── os(内核实现) 54 | │   ├── Cargo.toml(配置文件) 55 | │   └── src(所有内核的源代码放在 os/src 目录下) 56 | │   ├── main.rs(内核主函数) 57 | │   └── ... 58 | ├── reports (不是 report) 59 | │   ├── lab1.md/pdf 60 | │   └── ... 61 | ├── ... 62 | 63 | 64 | - 通过所有测例: 65 | 66 | CI 使用的测例与本地相同,测试中,user 文件夹及其它与构建相关的文件将被替换,请不要试图依靠硬编码通过测试。 67 | 68 | 默认情况下,makefile 仅编译基础测例 (``BASE=1``),即无需修改框架即可正常运行的测例。 69 | 你需要在编译时指定 ``BASE=0`` 控制框架仅编译实验测例(在 os 目录执行 ``make run BASE=0``), 70 | 或指定 ``BASE=2`` 控制框架同时编译基础测例和实验测例。 71 | 72 | .. note:: 73 | 74 | 你的实现只需且必须通过测例,建议读者感到困惑时先检查测例。 75 | 76 | 77 | 简答作业 78 | -------------------------------------------- 79 | 80 | 1. 正确进入 U 态后,程序的特征还应有:使用 S 态特权指令,访问 S 态寄存器后会报错。 81 | 请同学们可以自行测试这些内容 (运行 `Rust 三个 bad 测例 (ch2b_bad_*.rs) `_ , 82 | 注意在编译时至少需要指定 ``LOG=ERROR`` 才能观察到内核的报错信息) , 83 | 描述程序出错行为,同时注意注明你使用的 sbi 及其版本。 84 | 85 | 2. 深入理解 `trap.S `_ 86 | 中两个函数 ``__alltraps`` 和 ``__restore`` 的作用,并回答如下问题: 87 | 88 | 1. L40:刚进入 ``__restore`` 时,``a0`` 代表了什么值。请指出 ``__restore`` 的两种使用情景。 89 | 90 | 2. L43-L48:这几行汇编代码特殊处理了哪些寄存器?这些寄存器的的值对于进入用户态有何意义?请分别解释。 91 | 92 | .. code-block:: riscv 93 | 94 | ld t0, 32*8(sp) 95 | ld t1, 33*8(sp) 96 | ld t2, 2*8(sp) 97 | csrw sstatus, t0 98 | csrw sepc, t1 99 | csrw sscratch, t2 100 | 101 | 3. L50-L56:为何跳过了 ``x2`` 和 ``x4``? 102 | 103 | .. code-block:: riscv 104 | 105 | ld x1, 1*8(sp) 106 | ld x3, 3*8(sp) 107 | .set n, 5 108 | .rept 27 109 | LOAD_GP %n 110 | .set n, n+1 111 | .endr 112 | 113 | 4. L60:该指令之后,``sp`` 和 ``sscratch`` 中的值分别有什么意义? 114 | 115 | .. code-block:: riscv 116 | 117 | csrrw sp, sscratch, sp 118 | 119 | 5. ``__restore``:中发生状态切换在哪一条指令?为何该指令执行之后会进入用户态? 120 | 121 | 6. L13:该指令之后,``sp`` 和 ``sscratch`` 中的值分别有什么意义? 122 | 123 | .. code-block:: riscv 124 | 125 | csrrw sp, sscratch, sp 126 | 127 | 7. 从 U 态进入 S 态是哪一条指令发生的? 128 | 129 | 报告要求 130 | ------------------------------- 131 | 132 | - 简单总结你实现的功能(200字以内,不要贴代码)。 133 | - 完成问答题。 134 | - 加入 :doc:`/honorcode` 的内容。否则,你的提交将视作无效,本次实验的成绩将按“0”分计。 135 | - 推荐markdown文档格式。 136 | - (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 137 | -------------------------------------------------------------------------------- /source/chapter3/fsm-coop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter3/fsm-coop.png -------------------------------------------------------------------------------- /source/chapter3/index.rst: -------------------------------------------------------------------------------- 1 | .. _link-chapter3: 2 | 3 | 第三章:多道程序与分时多任务 4 | ============================================== 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | 9 | 0intro 10 | 1multi-loader 11 | 2task-switching 12 | 3multiprogramming 13 | 4time-sharing-system 14 | 5exercise 15 | -------------------------------------------------------------------------------- /source/chapter3/multiprogramming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter3/multiprogramming.png -------------------------------------------------------------------------------- /source/chapter4/0intro.rst: -------------------------------------------------------------------------------- 1 | 引言 2 | ============================== 3 | 4 | 本章导读 5 | ------------------------------- 6 | 7 | 本章中内核将实现虚拟内存机制,这注定是一趟艰难的旅程。 8 | 9 | 10 | 实践体验 11 | ----------------------- 12 | 13 | 本章应用运行起来效果与上一章基本一致。 14 | 15 | 获取本章代码: 16 | 17 | .. code-block:: console 18 | 19 | $ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2023S.git 20 | $ cd rCore-Tutorial-Code-2023S 21 | $ git checkout ch4 22 | $ git clone https://github.com/LearningOS/rCore-Tutorial-Test-2023S.git user 23 | 24 | 或许你之前已经克隆过了仓库,只希望从远程仓库更新,而非再克隆一次: 25 | 26 | .. code-block:: console 27 | 28 | $ cd rCore-Tutorial-Code-2023S 29 | # 你可以将 upstream 改为你喜欢的名字 30 | $ git remote add upstream https://github.com/LearningOS/rCore-Tutorial-Code-2023S.git 31 | # 更新仓库信息 32 | $ git fetch upstream 33 | # 查看已添加的远程仓库;应该能看到已有一个 origin 和新添加的 upstream 仓库 34 | $ git remote -v 35 | # 根据需求选择以下一种操作即可 36 | # 在本地新建一个与远程仓库对应的分支: 37 | $ git checkout -b ch4 upstream/ch4 38 | # 本地已有分支,从远程仓库更新: 39 | $ git checkout ch4 40 | $ git merge upstream/ch4 41 | # 将更新推送到自己的远程仓库 42 | $ git push origin ch4 43 | 44 | 在 qemu 模拟器上运行本章代码: 45 | 46 | .. code-block:: console 47 | 48 | $ cd os 49 | $ make run 50 | 51 | 52 | 本章代码树 53 | ----------------------------------------------------- 54 | 55 | .. code-block:: 56 | :linenos: 57 | 58 | ├── os 59 | │   ├── ... 60 | │   └── src 61 | │   ├── ... 62 | │   ├── config.rs(修改:新增一些内存管理的相关配置) 63 | │   ├── linker.ld(修改:将跳板页引入内存布局) 64 | │   ├── loader.rs(修改:仅保留获取应用数量和数据的功能) 65 | │   ├── main.rs(修改) 66 | │   ├── mm(新增:内存管理的 mm 子模块) 67 | │   │   ├── address.rs(物理/虚拟 地址/页号的 Rust 抽象) 68 | │   │   ├── frame_allocator.rs(物理页帧分配器) 69 | │   │   ├── heap_allocator.rs(内核动态内存分配器) 70 | │   │   ├── memory_set.rs(引入地址空间 MemorySet 及逻辑段 MemoryArea 等) 71 | │   │   ├── mod.rs(定义了 mm 模块初始化方法 init) 72 | │   │   └── page_table.rs(多级页表抽象 PageTable 以及其他内容) 73 | │   ├── syscall 74 | │   │   ├── fs.rs(修改:基于地址空间的 sys_write 实现) 75 | │   │   ├── mod.rs 76 | │   │   └── process.rs 77 | │   ├── task 78 | │   │   ├── context.rs(修改:构造一个跳转到不同位置的初始任务上下文) 79 | │   │   ├── mod.rs(修改,详见文档) 80 | │   │   ├── switch.rs 81 | │   │   ├── switch.S 82 | │   │   └── task.rs(修改,详见文档) 83 | │   └── trap 84 | │   ├── context.rs(修改:在 Trap 上下文中加入了更多内容) 85 | │   ├── mod.rs(修改:基于地址空间修改了 Trap 机制,详见文档) 86 | │   └── trap.S(修改:基于地址空间修改了 Trap 上下文保存与恢复汇编代码) 87 | └── user 88 | ├── build.py(编译时不再使用) 89 | ├── ... 90 | └── src 91 | ├── linker.ld(修改:将所有应用放在各自地址空间中固定的位置) 92 | └── ... 93 | 94 | cloc os 95 | ------------------------------------------------------------------------------- 96 | Language files blank comment code 97 | ------------------------------------------------------------------------------- 98 | Rust 26 138 56 1526 99 | Assembly 3 3 26 86 100 | make 1 11 4 36 101 | TOML 1 2 1 13 102 | ------------------------------------------------------------------------------- 103 | SUM: 31 154 87 1661 104 | ------------------------------------------------------------------------------- 105 | 106 | 107 | .. 本章代码导读 108 | .. ----------------------------------------------------- 109 | 110 | .. 本章涉及的代码量相对多了起来,也许同学们不知如何从哪里看起或从哪里开始尝试实验。这里简要介绍一下“头甲龙”操作系统的大致开发过程。 111 | 112 | .. 我们先从简单的地方入手,那当然就是先改进应用程序了。具体而言,主要就是把 ``linker.ld`` 中应用程序的起始地址都改为 ``0x0`` ,这是假定我们操作系统能够通过分页机制把不同应用的相同虚地址映射到不同的物理地址中。这样我们写应用就不用考虑物理地址布局的问题,能够以一种更加统一的方式编写应用程序,可以忽略掉一些不必要的细节。 113 | 114 | .. 为了能够在内核中动态分配内存,我们的第二步需要在内核增加连续内存分配的功能,具体实现主要集中在 ``os/src/mm/heap_allocator.rs`` 中。完成这一步后,我们就可以在内核中用到Rust的堆数据结构了,如 ``Vec`` 、 ``Box`` 等,这样内核编程就更加灵活了。 115 | 116 | .. 操作系统如果要建立页表,首先要能管理整个系统的物理内存,这就需要知道物理内存哪些区域放置内核的代码、数据,哪些区域则是空闲的等信息。所以需要了解整个系统的物理内存空间的范围,并以物理页帧为单位分配和回收物理内存,具体实现主要集中在 ``os/src/mm/frame_allocator.rs`` 中。 117 | 118 | .. 页表中的页表项的索引其实是虚拟地址中的虚拟页号,页表项的重要内容是物理地址的物理页帧号。为了能够灵活地在虚拟地址、物理地址、虚拟页号、物理页号之间进行各种转换,在 ``os/src/mm/address.rs`` 中实现了各种转换函数。 119 | 120 | .. 完成上述工作后,基本上就做好了建立页表的前期准备。我们就可以开始建立页表,这主要涉及到页表项的数据结构表示,以及多级页表的起始物理页帧位置和整个所占用的物理页帧的记录。具体实现主要集中在 ``os/src/mm/page_table.rs`` 中。 121 | 122 | .. 一旦使能分页机制,那么内核中也将基于虚地址进行虚存访问,所以在给应用添加虚拟地址空间前,内核自己也会建立一个页表,把整个物理地址空间通过简单的恒等映射对应到一个虚拟地址空间中。后续的应用在执行前,也需要建立一个虚拟地址空间,这意味着第三章的 ``task`` 将进化到第五章的拥有独立页表的进程 。虚拟地址空间需要有一个数据结构管理起来,这就是 ``MemorySet`` ,即地址空间这个抽象概念所对应的具象体现。在一个虚拟地址空间中,有代码段,数据段等不同属性且不一定连续的子空间,它们通过一个重要的数据结构 ``MapArea`` 来表示和管理。围绕 ``MemorySet`` 等一系列的数据结构和相关操作的实现,主要集中在 ``os/src/mm/memory_set.rs`` 中。比如内核的页表和虚拟空间的建立在如下代码中: 123 | 124 | .. .. code-block:: rust 125 | .. :linenos: 126 | 127 | .. // os/src/mm/memory_set.rs 128 | 129 | .. lazy_static! { 130 | .. pub static ref KERNEL_SPACE: Arc> = Arc::new(Mutex::new( 131 | .. MemorySet::new_kernel() 132 | .. )); 133 | .. } 134 | 135 | .. 完成到这里,我们就可以使能分页机制了。且我们应该有更加方便的机制来给支持应用运行。在本章之前,都是把应用程序的所有元数据丢弃从而转换成二进制格式来执行,这其实把编译器生成的 ELF 执行文件中大量有用的信息给去掉了,比如代码段、数据段的各种属性,程序的入口地址等。既然有了给应用运行提供虚拟地址空间的能力,我们就可以利用 ELF 执行文件中的各种信息来灵活构建应用运行所需要的虚拟地址空间。在 ``os/src/loader.rs`` 中可以看到如何获取一个应用的 ELF 执行文件数据,而在 ``os/src/mm/memory_set`` 中的 ``MemorySet::from_elf`` 可以看到如何通过解析 ELF 来创建一个应用地址空间。 136 | 137 | .. 对于有了虚拟地址空间的 *任务* ,我们可以把它叫做 *进程* 了。操作系统为此需要扩展任务控制块 ``TaskControlBlock`` 的管理范围,使得操作系统能管理拥有独立页表和虚拟地址空间的应用程序的运行。相关主要的改动集中在 ``os/src/task/task.rs`` 中。 138 | 139 | .. 由于代表应用程序运行的进程和管理应用的操作系统各自有独立的页表和虚拟地址空间,所以这就出现了两个比较挑战的事情。一个是由于系统调用、中断或异常导致的应用程序和操作系统之间的 Trap 上下文切换不像以前那么简单了,因为需要切换页表,这需要看看 ``os/src/trap/trap.S`` ;还有就是需要对来自用户态和内核态的 Trap 分别进行处理,这需要看看 ``os/src/trap/mod.rs`` 和 :ref:`跳板的实现 ` 中的讲解。 140 | 141 | .. 另外一个挑战是,在内核地址空间中执行的内核代码常常需要读写应用地址空间的数据,这无法简单的通过一次访存交给 MMU 来解决,而是需要手动查应用地址空间的页表。在访问应用地址空间中的一块跨多个页数据的时候还需要注意处理边界条件。可以参考 ``os/src/syscall/fs.rs``、 ``os/src/mm/page_table.rs`` 中的 ``translated_byte_buffer`` 函数的实现。 142 | 143 | .. 实现到这,应该就可以给应用程序运行提供一个方便且安全的虚拟地址空间了。 -------------------------------------------------------------------------------- /source/chapter4/3sv39-implementation-1.rst: -------------------------------------------------------------------------------- 1 | 实现 SV39 多级页表机制(上) 2 | ======================================================== 3 | 4 | .. note:: 5 | 6 | 背景知识: `地址空间 `_ 7 | 8 | 背景知识: `SV39 多级页表原理 `_ 9 | 10 | 11 | 我们将在内核实现 RV64 架构 SV39 分页机制。由于内容过多,分成两个小节。 12 | 13 | 虚拟地址和物理地址 14 | ------------------------------------------------------ 15 | 16 | 内存控制相关的CSR寄存器 17 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 18 | 19 | 默认情况下 MMU 未被使能,此时无论 CPU 处于哪个特权级,访存的地址都将直接被视作物理地址。 20 | 可以通过修改 S 特权级的 ``satp`` CSR 来启用分页模式,此后 S 和 U 特权级的访存地址会被视为虚拟地址,经过 MMU 的地址转换获得对应物理地址,再通过它来访问物理内存。 21 | 22 | .. image:: satp.png 23 | :name: satp-layout 24 | 25 | 上图是 RV64 架构下 ``satp`` 的字段分布。当 ``MODE`` 设置为 0 的时候,所有访存都被视为物理地址;而设置为 8 26 | 时,SV39 分页机制被启用,所有 S/U 特权级的访存被视为一个 39 位的虚拟地址,MMU 会将其转换成 56 位的物理地址;如果转换失败,则会触发异常。 27 | 28 | 29 | 地址格式与组成 30 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 31 | 32 | .. image:: sv39-va-pa.png 33 | 34 | 我们采用分页管理,单个页面的大小设置为 :math:`4\text{KiB}` ,每个虚拟页面和物理页帧都按 4 KB 对齐。 35 | :math:`4\text{KiB}` 需要用 12 位字节地址来表示,因此虚拟地址和物理地址都被分成两部分: 36 | 它们的低 12 位被称为 **页内偏移** (Page Offset) 。虚拟地址的高 27 位,即 :math:`[38:12]` 为它的虚拟页号 VPN; 37 | 物理地址的高 44 位,即 :math:`[55:12]` 为它的物理页号 PPN。页号可以用来定位一个虚拟/物理地址属于哪一个虚拟页面/物理页帧。 38 | 39 | 地址转换是以页为单位进行的,转换前后地址页内偏移部分不变。MMU 只是从虚拟地址中取出 27 位虚拟页号, 40 | 在页表中查到其对应的物理页号,如果找到,就将得到的 44 位的物理页号与 12 位页内偏移拼接到一起,形成 56 位物理地址。 41 | 42 | .. note:: 43 | 44 | **RV64 架构中虚拟地址为何只有 39 位?** 45 | 46 | 虚拟地址长度确实应该和位宽一致为 64 位,但是在启用 SV39 分页模式下,只有低 39 位是真正有意义的。 47 | SV39 分页模式规定 64 位虚拟地址的 :math:`[63:39]` 这 25 位必须和第 38 位相同,否则 MMU 会直接认定它是一个 48 | 不合法的虚拟地址。。 49 | 50 | 也就是说,所有 :math:`2^{64}` 个虚拟地址中,只有最低的 :math:`256\text{GiB}` (当第 38 位为 0 时) 51 | 以及最高的 :math:`256\text{GiB}` (当第 38 位为 1 时)是可能通过 MMU 检查的。 52 | 53 | 地址相关的数据结构抽象与类型定义 54 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 55 | 56 | 实现页表之前,先将地址和页号的概念抽象为 Rust 中的类型。 57 | 58 | 首先是这些类型的定义: 59 | 60 | .. code-block:: rust 61 | 62 | // os/src/mm/address.rs 63 | 64 | #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] 65 | pub struct PhysAddr(pub usize); 66 | 67 | #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] 68 | pub struct VirtAddr(pub usize); 69 | 70 | #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] 71 | pub struct PhysPageNum(pub usize); 72 | 73 | #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] 74 | pub struct VirtPageNum(pub usize); 75 | 76 | .. _term-type-convertion: 77 | 78 | 上面分别给出了物理地址、虚拟地址、物理页号、虚拟页号的 Rust 类型声明,它们都是 usize 的一种简单包装。 79 | 将它们各自抽象出来而不是直接使用 usize,是为了在 Rust 编译器的帮助下进行多种方便且安全的 **类型转换** (Type Convertion) 。 80 | 81 | 这些类型本身可以和 usize 之间互相转换,地址和页号之间也可以相互转换。以物理地址和物理页号之间的转换为例: 82 | 83 | .. code-block:: rust 84 | :linenos: 85 | 86 | // os/src/mm/address.rs 87 | 88 | impl PhysAddr { 89 | pub fn page_offset(&self) -> usize { self.0 & (PAGE_SIZE - 1) } 90 | } 91 | 92 | impl From for PhysPageNum { 93 | fn from(v: PhysAddr) -> Self { 94 | assert_eq!(v.page_offset(), 0); 95 | v.floor() 96 | } 97 | } 98 | 99 | impl From for PhysAddr { 100 | fn from(v: PhysPageNum) -> Self { Self(v.0 << PAGE_SIZE_BITS) } 101 | } 102 | 103 | 其中 ``PAGE_SIZE`` 为 :math:`4096` , ``PAGE_SIZE_BITS`` 为 :math:`12` ,它们均定义在 ``config`` 子模块 104 | 中,分别表示每个页面的大小和页内偏移的位宽。从物理页号到物理地址的转换只需左移 :math:`12` 位即可,但是物理地址需要 105 | 保证它与页面大小对齐才能通过右移转换为物理页号。 106 | 107 | 对于不对齐的情况,物理地址不能通过 ``From/Into`` 转换为物理页号,而是需要通过它自己的 ``floor`` 或 ``ceil`` 方法来 108 | 进行下取整或上取整的转换。 109 | 110 | .. code-block:: rust 111 | 112 | // os/src/mm/address.rs 113 | 114 | impl PhysAddr { 115 | pub fn floor(&self) -> PhysPageNum { PhysPageNum(self.0 / PAGE_SIZE) } 116 | pub fn ceil(&self) -> PhysPageNum { PhysPageNum((self.0 + PAGE_SIZE - 1) / PAGE_SIZE) } 117 | } 118 | 119 | 页表项的数据结构抽象与类型定义 120 | ----------------------------------------- 121 | 122 | .. image:: sv39-pte.png 123 | 124 | 上图为 SV39 分页模式下的页表项,其中 :math:`[53:10]` 这 :math:`44` 位是物理页号,最低的 :math:`8` 位 125 | :math:`[7:0]` 则是标志位,它们的含义如下: 126 | 127 | - 仅当 V(Valid) 位为 1 时,页表项才是合法的; 128 | - R/W/X 分别控制索引到这个页表项的对应虚拟页面是否允许读/写/取指; 129 | - U 控制索引到这个页表项的对应虚拟页面是否在 CPU 处于 U 特权级的情况下是否被允许访问; 130 | - G 我们不理会; 131 | - A(Accessed) 记录自从页表项上的这一位被清零之后,页表项的对应虚拟页面是否被访问过; 132 | - D(Dirty) 则记录自从页表项上的这一位被清零之后,页表项的对应虚拟页表是否被修改过。 133 | 134 | 先来实现页表项中的标志位 ``PTEFlags`` : 135 | 136 | .. code-block:: rust 137 | 138 | // os/src/main.rs 139 | 140 | #[macro_use] 141 | extern crate bitflags; 142 | 143 | // os/src/mm/page_table.rs 144 | 145 | use bitflags::*; 146 | 147 | bitflags! { 148 | pub struct PTEFlags: u8 { 149 | const V = 1 << 0; 150 | const R = 1 << 1; 151 | const W = 1 << 2; 152 | const X = 1 << 3; 153 | const U = 1 << 4; 154 | const G = 1 << 5; 155 | const A = 1 << 6; 156 | const D = 1 << 7; 157 | } 158 | } 159 | 160 | `bitflags `_ 是一个 Rust 中常用来比特标志位的 crate 。它提供了 161 | 一个 ``bitflags!`` 宏,如上面的代码段所展示的那样,可以将一个 ``u8`` 封装成一个标志位的集合类型,支持一些常见的集合 162 | 运算。 163 | 164 | 接下来我们实现页表项 ``PageTableEntry`` : 165 | 166 | .. code-block:: rust 167 | :linenos: 168 | 169 | // os/src/mm/page_table.rs 170 | 171 | #[derive(Copy, Clone)] 172 | #[repr(C)] 173 | pub struct PageTableEntry { 174 | pub bits: usize, 175 | } 176 | 177 | impl PageTableEntry { 178 | pub fn new(ppn: PhysPageNum, flags: PTEFlags) -> Self { 179 | PageTableEntry { 180 | bits: ppn.0 << 10 | flags.bits as usize, 181 | } 182 | } 183 | pub fn empty() -> Self { 184 | PageTableEntry { 185 | bits: 0, 186 | } 187 | } 188 | pub fn ppn(&self) -> PhysPageNum { 189 | (self.bits >> 10 & ((1usize << 44) - 1)).into() 190 | } 191 | pub fn flags(&self) -> PTEFlags { 192 | PTEFlags::from_bits(self.bits as u8).unwrap() 193 | } 194 | } 195 | 196 | - 第 3 行我们让编译器自动为 ``PageTableEntry`` 实现 ``Copy/Clone`` Trait,来让这个类型以值语义赋值/传参的时候 197 | 不会发生所有权转移,而是拷贝一份新的副本。 198 | - 第 10 行使得我们可以从一个物理页号 ``PhysPageNum`` 和一个页表项标志位 ``PTEFlags`` 生成一个页表项 199 | ``PageTableEntry`` 实例;而第 20 行和第 23 行则分别可以从一个页表项将它们两个取出。 200 | - 第 15 行中,我们也可以通过 ``empty`` 方法生成一个全零的页表项,注意这隐含着该页表项的 V 标志位为 0 , 201 | 因此它是不合法的。 202 | 203 | 后面我们还为 ``PageTableEntry`` 实现了一些辅助函数(Helper Function),可以快速判断一个页表项的 V/R/W/X 标志位是否为 1,以 V 204 | 标志位的判断为例: 205 | 206 | .. code-block:: rust 207 | 208 | // os/src/mm/page_table.rs 209 | 210 | impl PageTableEntry { 211 | pub fn is_valid(&self) -> bool { 212 | (self.flags() & PTEFlags::V) != PTEFlags::empty() 213 | } 214 | } 215 | 216 | 这里相当于判断两个集合的交集是否为空。 217 | -------------------------------------------------------------------------------- /source/chapter4/7exercise.rst: -------------------------------------------------------------------------------- 1 | chapter4练习 2 | ============================================ 3 | 4 | 编程作业 5 | --------------------------------------------- 6 | 7 | 重写 sys_get_time 和 sys_task_info 8 | ++++++++++++++++++++++++++++++++++++++++++++ 9 | 10 | 引入虚存机制后,原来内核的 sys_get_time 和 sys_task_info 函数实现就无效了。请你重写这个函数,恢复其正常功能。 11 | 12 | mmap 和 munmap 匿名映射 13 | ++++++++++++++++++++++++++++++++++++++++++++ 14 | 15 | `mmap `_ 在 Linux 中主要用于在内存中映射文件, 16 | 本次实验简化它的功能,仅用于申请内存。 17 | 18 | 请实现 mmap 和 munmap 系统调用,mmap 定义如下: 19 | 20 | 21 | .. code-block:: rust 22 | 23 | fn sys_mmap(start: usize, len: usize, port: usize) -> isize 24 | 25 | - syscall ID:222 26 | - 申请长度为 len 字节的物理内存(不要求实际物理内存位置,可以随便找一块),将其映射到 start 开始的虚存,内存页属性为 port 27 | - 参数: 28 | - start 需要映射的虚存起始地址,要求按页对齐 29 | - len 映射字节长度,可以为 0 30 | - port:第 0 位表示是否可读,第 1 位表示是否可写,第 2 位表示是否可执行。其他位无效且必须为 0 31 | - 返回值:执行成功则返回 0,错误返回 -1 32 | - 说明: 33 | - 为了简单,目标虚存区间要求按页对齐,len 可直接按页向上取整,不考虑分配失败时的页回收。 34 | - 可能的错误: 35 | - start 没有按页大小对齐 36 | - port & !0x7 != 0 (port 其余位必须为0) 37 | - port & 0x7 = 0 (这样的内存无意义) 38 | - [start, start + len) 中存在已经被映射的页 39 | - 物理内存不足 40 | 41 | munmap 定义如下: 42 | 43 | .. code-block:: rust 44 | 45 | fn sys_munmap(start: usize, len: usize) -> isize 46 | 47 | - syscall ID:215 48 | - 取消到 [start, start + len) 虚存的映射 49 | - 参数和返回值请参考 mmap 50 | - 说明: 51 | - 为了简单,参数错误时不考虑内存的恢复和回收。 52 | - 可能的错误: 53 | - [start, start + len) 中存在未被映射的虚存。 54 | 55 | tips: 56 | 57 | - 一定要注意 mmap 是的页表项,注意 riscv 页表项的格式与 port 的区别。 58 | - 你增加 PTE_U 了吗? 59 | 60 | 实验要求 61 | ++++++++++++++++++++++++++++++++++++++++++ 62 | 63 | - 实现分支:ch4。 64 | - 实现 mmap 和 munmap 两个系统调用,通过所有测例。 65 | - 实验目录请参考 ch3,报告命名 lab2.md/pdf 66 | 67 | TIPS:注意 port 参数的语义,它与内核定义的 MapPermission 有明显不同! 68 | 69 | 问答作业 70 | ------------------------------------------------- 71 | 72 | 1. 请列举 SV39 页表页表项的组成,描述其中的标志位有何作用? 73 | 74 | 2. 缺页 75 | 缺页指的是进程访问页面时页面不在页表中或在页表中无效的现象,此时 MMU 将会返回一个中断, 76 | 告知 os 进程内存访问出了问题。os 选择填补页表并重新执行异常指令或者杀死进程。 77 | 78 | - 请问哪些异常可能是缺页导致的? 79 | - 发生缺页时,描述相关重要寄存器的值,上次实验描述过的可以简略。 80 | 81 | 缺页有两个常见的原因,其一是 Lazy 策略,也就是直到内存页面被访问才实际进行页表操作。 82 | 比如,一个程序被执行时,进程的代码段理论上需要从磁盘加载到内存。但是 os 并不会马上这样做, 83 | 而是会保存 .text 段在磁盘的位置信息,在这些代码第一次被执行时才完成从磁盘的加载操作。 84 | 85 | - 这样做有哪些好处? 86 | 87 | 其实,我们的 mmap 也可以采取 Lazy 策略,比如:一个用户进程先后申请了 10G 的内存空间, 88 | 然后用了其中 1M 就直接退出了。按照现在的做法,我们显然亏大了,进行了很多没有意义的页表操作。 89 | 90 | - 处理 10G 连续的内存页面,对应的 SV39 页表大致占用多少内存 (估算数量级即可)? 91 | - 请简单思考如何才能实现 Lazy 策略,缺页时又如何处理?描述合理即可,不需要考虑实现。 92 | 93 | 缺页的另一个常见原因是 swap 策略,也就是内存页面可能被换到磁盘上了,导致对应页面失效。 94 | 95 | - 此时页面失效如何表现在页表项(PTE)上? 96 | 97 | 3. 双页表与单页表 98 | 99 | 为了防范侧信道攻击,我们的 os 使用了双页表。但是传统的设计一直是单页表的,也就是说, 100 | 用户线程和对应的内核线程共用同一张页表,只不过内核对应的地址只允许在内核态访问。 101 | (备注:这里的单/双的说法仅为自创的通俗说法,并无这个名词概念,详情见 `KPTI `_ ) 102 | 103 | - 在单页表情况下,如何更换页表? 104 | - 单页表情况下,如何控制用户态无法访问内核页面?(tips:看看上一题最后一问) 105 | - 单页表有何优势?(回答合理即可) 106 | - 双页表实现下,何时需要更换页表?假设你写一个单页表操作系统,你会选择何时更换页表(回答合理即可)? 107 | 108 | 报告要求 109 | -------------------------------------------------------- 110 | 111 | - 简单总结你实现的功能(200字以内,不要贴代码)。 112 | - 完成问答题。 113 | - 加入 :doc:`/honorcode` 的内容。否则,你的提交将视作无效,本次实验的成绩将按“0”分计。 114 | - 推荐markdown文档格式。 115 | - (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 116 | 117 | 选做题目 118 | -------------------------------------------------------- 119 | 120 | 选作题目列表 121 | 122 | - (4分)惰性页面分配(Lazy page allocation) 123 | - (4分)局部页面置换算法:改进的Clock页面置换算法 124 | - (5分)全局页面置换算法:工作集置换策略 125 | - (5分)全局页面置换算法:缺页率置换策略 126 | 127 | 提交要求 128 | 129 | - (占分比:40%)实现代码(包括基本的注释) 130 | - (占分比:50%)设计与功能/性能测试分析文档,测试用例。 131 | - (占分比:10%)鼓励形成可脱离OS独立存在的库,可以裸机测试或在用户态测试(比如easyfs那样) -------------------------------------------------------------------------------- /source/chapter4/address-translation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter4/address-translation.png -------------------------------------------------------------------------------- /source/chapter4/app-as-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter4/app-as-full.png -------------------------------------------------------------------------------- /source/chapter4/index.rst: -------------------------------------------------------------------------------- 1 | 第四章:地址空间 2 | ============================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | 0intro 8 | 3sv39-implementation-1 9 | 4sv39-implementation-2 10 | 5kernel-app-spaces 11 | 6multitasking-based-on-as 12 | 7exercise 13 | -------------------------------------------------------------------------------- /source/chapter4/kernel-as-high.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter4/kernel-as-high.png -------------------------------------------------------------------------------- /source/chapter4/kernel-as-low.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter4/kernel-as-low.png -------------------------------------------------------------------------------- /source/chapter4/linear-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter4/linear-table.png -------------------------------------------------------------------------------- /source/chapter4/page-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter4/page-table.png -------------------------------------------------------------------------------- /source/chapter4/pte-rwx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter4/pte-rwx.png -------------------------------------------------------------------------------- /source/chapter4/rust-containers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter4/rust-containers.png -------------------------------------------------------------------------------- /source/chapter4/satp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter4/satp.png -------------------------------------------------------------------------------- /source/chapter4/segmentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter4/segmentation.png -------------------------------------------------------------------------------- /source/chapter4/simple-base-bound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter4/simple-base-bound.png -------------------------------------------------------------------------------- /source/chapter4/sv39-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter4/sv39-full.png -------------------------------------------------------------------------------- /source/chapter4/sv39-pte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter4/sv39-pte.png -------------------------------------------------------------------------------- /source/chapter4/sv39-va-pa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter4/sv39-va-pa.png -------------------------------------------------------------------------------- /source/chapter4/trie-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter4/trie-1.png -------------------------------------------------------------------------------- /source/chapter4/trie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter4/trie.png -------------------------------------------------------------------------------- /source/chapter5/0intro.rst: -------------------------------------------------------------------------------- 1 | 引言 2 | =========================================== 3 | 4 | 本章导读 5 | ------------------------------------------- 6 | 7 | 我们将开发一个用户 **终端** (Terminal) 或 **命令行** (Command Line Application, 俗称 **Shell** ) , 8 | 形成用户与操作系统进行交互的命令行界面 (Command Line Interface)。 9 | 10 | 为此,我们要对任务建立新的抽象: **进程** ,并实现若干基于 **进程** 的强大系统调用。 11 | 12 | .. note:: 13 | 14 | **任务和进程的关系与区别** 15 | 16 | 第三章提到的 **任务** 是这里提到的 **进程** 的初级阶段,与任务相比,进程能在运行中创建 **子进程** 、 17 | 用新的 **程序** 内容覆盖已有的 **程序** 内容、可管理更多物理或虚拟 **资源** 。 18 | 19 | 实践体验 20 | ------------------------------------------- 21 | 22 | 获取本章代码: 23 | 24 | .. code-block:: console 25 | 26 | $ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2023S.git 27 | $ cd rCore-Tutorial-Code-2023S 28 | $ git checkout ch5 29 | $ git clone https://github.com/LearningOS/rCore-Tutorial-Test-2023S.git user 30 | 31 | 请仿照ch4的做法将代码在本地更新并push到自己的实验仓库中。 32 | 33 | 注意:user仓库有对ch5的测例更新,请重新clone或者使用git pull等获取。 34 | 35 | 在 qemu 模拟器上运行本章代码: 36 | 37 | .. code-block:: console 38 | 39 | $ cd os 40 | $ make run 41 | 42 | 待内核初始化完毕之后,将在屏幕上打印可用的应用列表并进入shell程序: 43 | 44 | .. code-block:: 45 | 46 | [rustsbi] RustSBI version 0.2.0-alpha.4 47 | .______ __ __ _______.___________. _______..______ __ 48 | | _ \ | | | | / | | / || _ \ | | 49 | | |_) | | | | | | (----`---| |----`| (----`| |_) || | 50 | | / | | | | \ \ | | \ \ | _ < | | 51 | | |\ \----.| `--' |.----) | | | .----) | | |_) || | 52 | | _| `._____| \______/ |_______/ |__| |_______/ |______/ |__| 53 | 54 | [rustsbi] Implementation: RustSBI-QEMU Version 0.0.1 55 | [rustsbi-dtb] Hart count: cluster0 with 1 cores 56 | [rustsbi] misa: RV64ACDFIMSU 57 | [rustsbi] mideleg: ssoft, stimer, sext (0x222) 58 | [rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab) 59 | [rustsbi] pmp0: 0x80000000 ..= 0x800fffff (rwx) 60 | [rustsbi] pmp1: 0x80000000 ..= 0x807fffff (rwx) 61 | [rustsbi] pmp2: 0x0 ..= 0xffffffffffffff (---) 62 | [rustsbi] enter supervisor 0x80200000 63 | [kernel] Hello, world! 64 | /**** APPS **** 65 | ch2b_bad_address 66 | ch2b_bad_instructions 67 | ch2b_bad_register 68 | ch2b_hello_world 69 | ch2b_power_3 70 | ch2b_power_5 71 | ch2b_power_7 72 | ch3b_sleep 73 | ch3b_sleep1 74 | ch3b_yield0 75 | ch3b_yield1 76 | ch3b_yield2 77 | ch5b_exit 78 | ch5b_forktest 79 | ch5b_forktest2 80 | ch5b_forktest_simple 81 | ch5b_forktree 82 | ch5b_initproc 83 | ch5b_user_shell 84 | **************/ 85 | Rust user shell 86 | >> 87 | 88 | 可以通过输入ch5b开头的应用来测试ch5实现的fork等功能: 89 | 90 | .. code-block:: 91 | 92 | >> ch5b_forktest_simple 93 | 94 | sys_wait without child process test passed! 95 | parent start, pid = 2! 96 | ready waiting on parent process! 97 | hello child process! 98 | child process pid = 3, exit code = 100 99 | Shell: Process 2 exited with code 0 100 | 101 | 本章代码树 102 | -------------------------------------- 103 | 104 | .. code-block:: 105 | :linenos: 106 | 107 | ├── os 108 |    ├── build.rs(修改:基于应用名的应用构建器) 109 |    ├── ... 110 |    └── src 111 |    ├── ... 112 |    ├── loader.rs(修改:基于应用名的应用加载器) 113 |    ├── main.rs(修改) 114 |    ├── mm(修改:为了支持本章的系统调用对此模块做若干增强) 115 |    │   ├── address.rs 116 |    │   ├── frame_allocator.rs 117 |    │   ├── heap_allocator.rs 118 |    │   ├── memory_set.rs 119 |    │   ├── mod.rs 120 |    │   └── page_table.rs 121 |    ├── syscall 122 |    │   ├── fs.rs(修改:新增 sys_read) 123 |    │   ├── mod.rs(修改:新的系统调用的分发处理) 124 |    │   └── process.rs(修改:新增 sys_getpid/fork/exec/waitpid) 125 |    ├── task 126 |    │   ├── context.rs 127 |    │   ├── manager.rs(新增:任务管理器,为上一章任务管理器功能的一部分) 128 |    │   ├── mod.rs(修改:调整原来的接口实现以支持进程) 129 |    │   ├── pid.rs(新增:进程标识符和内核栈的 Rust 抽象) 130 |    │   ├── processor.rs(新增:处理器管理结构 ``Processor`` ,为上一章任务管理器功能的一部分) 131 |    │   ├── switch.rs 132 |    │   ├── switch.S 133 |    │   └── task.rs(修改:支持进程机制的任务控制块) 134 |    └── trap 135 |    ├── context.rs 136 |    ├── mod.rs(修改:对于系统调用的实现进行修改以支持进程系统调用) 137 |    └── trap.S 138 | 139 | cloc os 140 | ------------------------------------------------------------------------------- 141 | Language files blank comment code 142 | ------------------------------------------------------------------------------- 143 | Rust 29 180 138 2049 144 | Assembly 4 20 26 229 145 | make 1 11 4 36 146 | TOML 1 2 1 13 147 | ------------------------------------------------------------------------------- 148 | SUM: 35 213 169 2327 149 | ------------------------------------------------------------------------------- 150 | 151 | 152 | .. 本章代码导读 153 | .. ----------------------------------------------------- 154 | 155 | .. 本章的第一小节 :doc:`/chapter5/1process` 介绍了操作系统中经典的进程概念,并描述我们将要实现的参考自 Unix 系内核并经过简化的精简版进程模型。在该模型下,若想对进程进行管理,实现创建、退出等操作,核心就在于 ``fork/exec/waitpid`` 三个系统调用。 156 | 157 | .. 首先我们修改运行在应用态的应用软件,它们均放置在 ``user`` 目录下。在新增系统调用的时候,需要在 ``user/src/lib.rs`` 中新增一个 ``sys_*`` 的函数,它的作用是将对应的系统调用按照与内核约定的 ABI 在 ``syscall`` 中转化为一条用于触发系统调用的 ``ecall`` 的指令;还需要在用户库 ``user_lib`` 将 ``sys_*`` 进一步封装成一个应用可以直接调用的与系统调用同名的函数。通过这种方式我们新增三个进程模型中核心的系统调用 ``fork/exec/waitpid`` ,一个查看进程 PID 的系统调用 ``getpid`` ,还有一个允许应用程序获取用户键盘输入的 ``read`` 系统调用。 158 | 159 | .. 基于进程模型,我们在 ``user/src/bin`` 目录下重新实现了一组应用程序。其中有两个特殊的应用程序:用户初始程序 ``initproc.rs`` 和 shell 程序 ``user_shell.rs`` ,可以认为它们位于内核和其他应用程序之间的中间层提供一些基础功能,但是它们仍处于应用层。前者会被内核唯一自动加载、也是最早加载并执行,后者则负责从键盘接收用户输入的应用名并执行对应的应用。剩下的应用从不同层面测试了我们内核实现的正确性,读者可以自行参考。值得一提的是, ``usertests`` 可以按照顺序执行绝大部分应用,会在测试的时候为我们提供很多方便。 160 | 161 | .. 接下来就需要在内核中实现简化版的进程机制并支持新增的系统调用。在本章第二小节 :doc:`/chapter5/2core-data-structures` 中我们对一些进程机制相关的数据结构进行了重构或者修改: 162 | 163 | .. - 为了支持基于应用名而不是应用 ID 来查找应用 ELF 可执行文件,从而实现灵活的应用加载,在 ``os/build.rs`` 以及 ``os/src/loader.rs`` 中更新了 ``link_app.S`` 的格式使得它包含每个应用的名字,另外提供 ``get_app_data_by_name`` 接口获取应用的 ELF 数据。 164 | .. - 在本章之前,任务管理器 ``TaskManager`` 不仅负责管理所有的任务状态,还维护着我们的 CPU 当前正在执行哪个任务。这种设计耦合度较高,我们将后一个功能分离到 ``os/src/task/processor.rs`` 中的处理器管理结构 ``Processor`` 中,它负责管理 CPU 上执行的任务和一些其他信息;而 ``os/src/task/manager.rs`` 中的任务管理器 ``TaskManager`` 仅负责管理所有任务。 165 | .. - 针对新的进程模型,我们复用前面章节的任务控制块 ``TaskControlBlock`` 作为进程控制块来保存进程的一些信息,相比前面章节还要新增 PID、内核栈、应用数据大小、父子进程、退出码等信息。它声明在 ``os/src/task/task.rs`` 中。 166 | .. - 从本章开始,内核栈在内核地址空间中的位置由所在进程的 PID 决定,我们需要在二者之间建立联系并提供一些相应的资源自动回收机制。可以参考 ``os/src/task/pid.rs`` 。 167 | 168 | .. 有了这些数据结构的支撑,我们在本章第三小节 :doc:`/chapter5/3implement-process-mechanism` 实现进程机制。它可以分成如下几个方面: 169 | 170 | .. - 初始进程的自动创建。在内核初始化的时候需要调用 ``os/src/task/mod.rs`` 中的 ``add_initproc`` 函数,它会调用 ``TaskControlBlock::new`` 读取并解析初始应用 ``initproc`` 的 ELF 文件数据并创建初始进程 ``INITPROC`` ,随后会将它加入到全局任务管理器 ``TASK_MANAGER`` 中参与调度。 171 | .. - 进程切换机制。当一个进程退出或者是主动/被动交出 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`` 方法),然后在里面选取要切换到的进程并切换过去。 172 | .. - 进程调度机制。在进程切换的时候我们需要选取一个进程切换过去。选取进程逻辑可以参考 ``os/src/task/manager.rs`` 中的 ``TaskManager::fetch_task`` 方法。 173 | .. - 进程生成机制。这主要是指 ``fork/exec`` 两个系统调用。它们的实现分别可以在 ``os/src/syscall/process.rs`` 中找到,分别基于 ``os/src/process/task.rs`` 中的 ``TaskControlBlock::fork/exec`` 。 174 | .. - 进程资源回收机制。当一个进程主动退出或出错退出的时候,在 ``exit_current_and_run_next`` 中会立即回收一部分资源并在进程控制块中保存退出码;而需要等到它的父进程通过 ``waitpid`` 系统调用(与 ``fork/exec`` 两个系统调用放在相同位置)捕获到它的退出码之后,它的进程控制块才会被回收,从而所有资源都被回收。 175 | .. - 为了支持用户终端 ``user_shell`` 读取用户键盘输入的功能,还需要实现 ``read`` 系统调用,它可以在 ``os/src/syscall/fs.rs`` 中找到。 -------------------------------------------------------------------------------- /source/chapter5/1process.rst: -------------------------------------------------------------------------------- 1 | 与进程有关的重要系统调用 2 | ================================================ 3 | 4 | 重要系统调用 5 | ------------------------------------------------------------ 6 | 7 | fork 系统调用 8 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 9 | 10 | .. code-block:: rust 11 | 12 | /// 功能:由当前进程 fork 出一个子进程。 13 | /// 返回值:对于子进程返回 0,对于当前进程则返回子进程的 PID 。 14 | /// syscall ID:220 15 | pub fn sys_fork() -> isize; 16 | 17 | exec 系统调用 18 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 19 | 20 | .. code-block:: rust 21 | 22 | /// 功能:将当前进程的地址空间清空并加载一个特定的可执行文件,返回用户态后开始它的执行。 23 | /// 参数:字符串 path 给出了要加载的可执行文件的名字; 24 | /// 返回值:如果出错的话(如找不到名字相符的可执行文件)则返回 -1,否则不应该返回。 25 | /// 注意:path 必须以 "\0" 结尾,否则内核将无法确定其长度 26 | /// syscall ID:221 27 | pub fn sys_exec(path: &str) -> isize; 28 | 29 | 利用 ``fork`` 和 ``exec`` 的组合,我们能让创建一个子进程,并令其执行特定的可执行文件。 30 | 31 | waitpid 系统调用 32 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 33 | 34 | .. code-block:: rust 35 | 36 | /// 功能:当前进程等待一个子进程变为僵尸进程,回收其全部资源并收集其返回值。 37 | /// 参数:pid 表示要等待的子进程的进程 ID,如果为 -1 的话表示等待任意一个子进程; 38 | /// exit_code 表示保存子进程返回值的地址,如果这个地址为 0 的话表示不必保存。 39 | /// 返回值:如果要等待的子进程不存在则返回 -1;否则如果要等待的子进程均未结束则返回 -2; 40 | /// 否则返回结束的子进程的进程 ID。 41 | /// syscall ID:260 42 | pub fn sys_waitpid(pid: isize, exit_code: *mut i32) -> isize; 43 | 44 | 45 | ``sys_waitpid`` 在用户库中被封装成两个不同的 API, ``wait(exit_code: &mut i32)`` 和 ``waitpid(pid: usize, exit_code: &mut i32)``, 46 | 前者用于等待任意一个子进程,后者用于等待特定子进程。它们实现的策略是如果子进程还未结束,就以 yield 让出时间片: 47 | 48 | .. code-block:: rust 49 | :linenos: 50 | 51 | // user/src/lib.rs 52 | 53 | pub fn wait(exit_code: &mut i32) -> isize { 54 | loop { 55 | match sys_waitpid(-1, exit_code as *mut _) { 56 | -2 => { sys_yield(); } 57 | n => { return n; } 58 | } 59 | } 60 | } 61 | 62 | 63 | 应用程序示例 64 | ----------------------------------------------- 65 | 66 | 借助这三个重要系统调用,我们可以开发功能更强大的应用。下面是两个案例: **用户初始程序-init** 和 **shell程序-user_shell** 。 67 | 68 | 用户初始程序-initproc 69 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 70 | 71 | 在内核初始化完毕后创建的第一个进程,是 **用户初始进程** (Initial Process) ,它将通过 72 | ``fork+exec`` 创建 ``user_shell`` 子进程,并将被用于回收僵尸进程。 73 | 74 | .. code-block:: rust 75 | :linenos: 76 | 77 | // user/src/bin/ch5b_initproc.rs 78 | 79 | #![no_std] 80 | #![no_main] 81 | 82 | #[macro_use] 83 | extern crate user_lib; 84 | 85 | use user_lib::{ 86 | fork, 87 | wait, 88 | exec, 89 | yield_, 90 | }; 91 | 92 | #[no_mangle] 93 | fn main() -> i32 { 94 | if fork() == 0 { 95 | exec("ch5b_user_shell\0"); 96 | } else { 97 | loop { 98 | let mut exit_code: i32 = 0; 99 | let pid = wait(&mut exit_code); 100 | if pid == -1 { 101 | yield_(); 102 | continue; 103 | } 104 | println!( 105 | "[initproc] Released a zombie process, pid={}, exit_code={}", 106 | pid, 107 | exit_code, 108 | ); 109 | } 110 | } 111 | 0 112 | } 113 | 114 | - 第 19 行为 ``fork`` 出的子进程分支,通过 ``exec`` 启动shell程序 ``user_shell`` , 115 | 注意我们需要在字符串末尾手动加入 ``\0`` 。 116 | - 第 21 行开始则为父进程分支,表示用户初始程序-initproc自身。它不断循环调用 ``wait`` 来等待并回收系统中的僵尸进程占据的资源。 117 | 如果回收成功的话则会打印一条报告信息给出被回收子进程的 PID 和返回值;否则就 ``yield_`` 交出 CPU 资源并在下次轮到它执行的时候再回收看看。 118 | 119 | 120 | shell程序-user_shell 121 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 122 | 123 | user_shell 需要捕获用户输入并进行解析处理,为此添加一个能获取用户输入的系统调用: 124 | 125 | .. code-block:: rust 126 | 127 | /// 功能:从文件中读取一段内容到缓冲区。 128 | /// 参数:fd 是待读取文件的文件描述符,切片 buffer 则给出缓冲区。 129 | /// 返回值:如果出现了错误则返回 -1,否则返回实际读到的字节数。 130 | /// syscall ID:63 131 | pub fn sys_read(fd: usize, buffer: &mut [u8]) -> isize; 132 | 133 | 实际调用时,我们必须要同时向内核提供缓冲区的起始地址及长度: 134 | 135 | .. code-block:: rust 136 | 137 | // user/src/syscall.rs 138 | 139 | pub fn sys_read(fd: usize, buffer: &mut [u8]) -> isize { 140 | syscall(SYSCALL_READ, [fd, buffer.as_mut_ptr() as usize, buffer.len()]) 141 | } 142 | 143 | 我们在用户库中将其进一步封装成每次能够从 **标准输入** 中获取一个字符的 ``getchar`` 函数。 144 | 145 | shell程序 ``user_shell`` 实现如下: 146 | 147 | .. code-block:: rust 148 | :linenos: 149 | :emphasize-lines: 28,53,61 150 | 151 | // user/src/bin/ch5b_user_shell.rs 152 | 153 | #![no_std] 154 | #![no_main] 155 | 156 | extern crate alloc; 157 | 158 | #[macro_use] 159 | extern crate user_lib; 160 | 161 | const LF: u8 = 0x0au8; 162 | const CR: u8 = 0x0du8; 163 | const DL: u8 = 0x7fu8; 164 | const BS: u8 = 0x08u8; 165 | 166 | use alloc::string::String; 167 | use user_lib::{fork, exec, waitpid, yield_}; 168 | use user_lib::console::getchar; 169 | 170 | #[no_mangle] 171 | pub fn main() -> i32 { 172 | println!("Rust user shell"); 173 | let mut line: String = String::new(); 174 | print!(">> "); 175 | loop { 176 | let c = getchar(); 177 | match c { 178 | LF | CR => { 179 | println!(""); 180 | if !line.is_empty() { 181 | line.push('\0'); 182 | let pid = fork(); 183 | if pid == 0 { 184 | // child process 185 | if exec(line.as_str()) == -1 { 186 | println!("Error when executing!"); 187 | return -4; 188 | } 189 | unreachable!(); 190 | } else { 191 | let mut exit_code: i32 = 0; 192 | let exit_pid = waitpid(pid as usize, &mut exit_code); 193 | assert_eq!(pid, exit_pid); 194 | println!( 195 | "Shell: Process {} exited with code {}", 196 | pid, exit_code 197 | ); 198 | } 199 | line.clear(); 200 | } 201 | print!(">> "); 202 | } 203 | BS | DL => { 204 | if !line.is_empty() { 205 | print!("{}", BS as char); 206 | print!(" "); 207 | print!("{}", BS as char); 208 | line.pop(); 209 | } 210 | } 211 | _ => { 212 | print!("{}", c as char); 213 | line.push(c as char); 214 | } 215 | } 216 | } 217 | } 218 | 219 | 可以看到,在以第 25 行开头的主循环中,每次都是调用 ``getchar`` 获取一个用户输入的字符, 220 | 并根据它相应进行一些动作。第 23 行声明的字符串 ``line`` 则维护着用户当前输入的命令内容,它也在不断发生变化。 221 | 222 | - 如果用户输入回车键(第 28 行),那么user_shell 会 fork 出一个子进程(第 34 行开始)并试图通过 223 | ``exec`` 系统调用执行一个应用,应用的名字在字符串 ``line`` 中给出。如果 exec 的返回值为 -1 , 224 | 说明在应用管理器中找不到对应名字的应用,此时子进程就直接打印错误信息并退出;否则子进程将开始执行目标应用。 225 | 226 | fork 之后的 user_shell 进程自己的逻辑可以在第 41 行找到。它在等待 fork 出来的子进程结束并回收掉它的资源,还会顺带收集子进程的退出状态并打印出来。 227 | - 如果用户输入退格键(第 53 行),首先我们需要将屏幕上当前行的最后一个字符用空格替换掉, 228 | 这可以通过输入一个特殊的退格字节 ``BS`` 来实现。其次,user_shell 进程内维护的 ``line`` 也需要弹出最后一个字符。 229 | - 如果用户输入了一个其他字符(第 61 行),就接将它打印在屏幕上,并加入到 ``line`` 中。 230 | - 按键 ``Ctrl+A`` 再输入 ``X`` 来退出qemu模拟器。 -------------------------------------------------------------------------------- /source/chapter5/4exercise.rst: -------------------------------------------------------------------------------- 1 | chapter5练习 2 | ============================================== 3 | 4 | 编程作业 5 | --------------------------------------------- 6 | 7 | 进程创建 8 | +++++++++++++++++++++++++++++++++++++++++++++ 9 | 10 | 大家一定好奇过为啥进程创建要用 fork + exec 这么一个奇怪的系统调用,就不能直接搞一个新进程吗? 11 | 思而不学则殆,我们就来试一试!这章的编程练习请大家实现一个完全 DIY 的系统调用 spawn,用以创建一个新进程。 12 | 13 | spawn 系统调用定义( `标准spawn看这里 `_ ): 14 | 15 | .. code-block:: rust 16 | 17 | fn sys_spawn(path: *const u8) -> isize 18 | 19 | - syscall ID: 400 20 | - 功能:新建子进程,使其执行目标程序。 21 | - 说明:成功返回子进程id,否则返回 -1。 22 | - 可能的错误: 23 | - 无效的文件名。 24 | - 进程池满/内存不足等资源错误。 25 | 26 | TIPS:虽然测例很简单,但提醒读者 spawn **不必** 像 fork 一样复制父进程的地址空间。 27 | 28 | stride 调度算法 29 | +++++++++++++++++++++++++++++++++++++++++ 30 | 31 | ch3 中我们实现的调度算法十分简单。现在我们要为我们的 os 实现一种带优先级的调度算法:stride 调度算法。 32 | 33 | 算法描述如下: 34 | 35 | (1) 为每个进程设置一个当前 stride,表示该进程当前已经运行的“长度”。另外设置其对应的 pass 36 | 值(只与进程的优先权有关系),表示对应进程在调度后,stride 需要进行的累加值。 37 | 38 | (2) 每次需要调度时,从当前 runnable 态的进程中选择 stride 最小的进程调度。对于获得调度的进程 P,将对应的 stride 加上其对应的步长 pass。 39 | 40 | (3) 一个时间片后,回到上一步骤,重新调度当前 stride 最小的进程。 41 | 42 | 可以证明,如果令 P.pass = BigStride / P.priority 其中 P.priority 表示进程的优先权(大于 1),而 43 | BigStride 表示一个预先定义的大常数,则该调度方案为每个进程分配的时间将与其优先级成正比。证明过程我们在这里略去,有兴趣的同学可以在网上查找相关资料。 44 | 45 | 其他实验细节: 46 | 47 | - stride 调度要求进程优先级 :math:`\geq 2`,所以设定进程优先级 :math:`\leq 1` 会导致错误。 48 | - 进程初始 stride 设置为 0 即可。 49 | - 进程初始优先级设置为 16。 50 | 51 | 为了实现该调度算法,内核还要增加 set_prio 系统调用 52 | 53 | .. code-block:: rust 54 | 55 | // syscall ID:140 56 | // 设置当前进程优先级为 prio 57 | // 参数:prio 进程优先级,要求 prio >= 2 58 | // 返回值:如果输入合法则返回 prio,否则返回 -1 59 | fn sys_set_priority(prio: isize) -> isize; 60 | 61 | 实现 tips: 62 | 63 | - 你可以在TCB加入新的字段来支持优先级等。 64 | - 为了减少整数除的误差,BIG_STRIDE 一般需要很大,但为了不至于发生反转现象(详见问答作业),或许选择一个适中的数即可,当然能进行溢出处理就更好了。 65 | - stride 算法要找到 stride 最小的进程,使用优先级队列是效率不错的办法,但是我们的实验测例很简单,所以效率完全不是问题。事实上,很推荐使用暴力扫一遍的办法找最小值。 66 | - 注意设置进程的初始优先级。 67 | 68 | .. attention:: 69 | 70 | 为了让大家能在本编程作业中使用 ``Vec`` 等数据结构,我们利用第三方库 ``buddy_system_allocator`` 71 | 为大家实现了堆内存分配器,相关代码位于 ``mm/heap_allocator`` 模块。 72 | 73 | 背景知识: `Rust 中的动态内存分配 `_ 74 | 75 | 实验要求 76 | +++++++++++++++++++++++++++++++++++++++++++++ 77 | - 实现分支:ch5。 78 | - 实验目录请参考 ch3。注意在reports中放入lab1-3的所有报告。 79 | - 通过所有测例。 80 | 81 | 在 os 目录下 ``make run BASE=2`` 加载所有测例, ``ch5_usertest`` 打包了所有你需要通过的测例, 82 | 你也可以通过修改这个文件调整本地测试的内容, 或者单独运行某测例来纠正特定的错误。 ``ch5_stride`` 83 | 检查 stride 调度算法是否满足公平性要求,六个子程序运行的次数应该大致与其优先级呈正比,测试通过标准是 84 | :math:`\max{\frac{runtimes}{prio}}/ \min{\frac{runtimes}{prio}} < 1.5`. 85 | 86 | CI 的原理是用 ``ch5_usertest`` 替代 ``ch5b_initproc`` ,使内核在所有测例执行完后直接退出。 87 | 88 | 从本章开始,你的内核必须前向兼容,能通过前一章的所有测例。 89 | 90 | .. note:: 91 | 92 | 利用 ``git cherry-pick`` 系列指令,能方便地将前一章分支 commit 移植到本章分支。 93 | 94 | 问答作业 95 | -------------------------------------------- 96 | 97 | stride 算法深入 98 | 99 | stride 算法原理非常简单,但是有一个比较大的问题。例如两个 pass = 10 的进程,使用 8bit 无符号整形储存 100 | stride, p1.stride = 255, p2.stride = 250,在 p2 执行一个时间片后,理论上下一次应该 p1 执行。 101 | 102 | - 实际情况是轮到 p1 执行吗?为什么? 103 | 104 | 我们之前要求进程优先级 >= 2 其实就是为了解决这个问题。可以证明, **在不考虑溢出的情况下** , 在进程优先级全部 >= 2 105 | 的情况下,如果严格按照算法执行,那么 STRIDE_MAX – STRIDE_MIN <= BigStride / 2。 106 | 107 | - 为什么?尝试简单说明(不要求严格证明)。 108 | 109 | - 已知以上结论,**考虑溢出的情况下**,可以为 Stride 设计特别的比较器,让 BinaryHeap 的 pop 110 | 方法能返回真正最小的 Stride。补全下列代码中的 ``partial_cmp`` 函数,假设两个 Stride 永远不会相等。 111 | 112 | .. code-block:: rust 113 | 114 | use core::cmp::Ordering; 115 | 116 | struct Stride(u64); 117 | 118 | impl PartialOrd for Stride { 119 | fn partial_cmp(&self, other: &Self) -> Option { 120 | // ... 121 | } 122 | } 123 | 124 | impl PartialEq for Stride { 125 | fn eq(&self, other: &Self) -> bool { 126 | false 127 | } 128 | } 129 | 130 | TIPS: 使用 8 bits 存储 stride, BigStride = 255, 则: ``(125 < 255) == false``, ``(129 < 255) == true``. 131 | 132 | 报告要求 133 | ------------------------------------------------------------ 134 | 135 | - 简单总结你实现的功能(200字以内,不要贴代码)。 136 | - 完成问答题。 137 | - 加入 :doc:`/honorcode` 的内容。否则,你的提交将视作无效,本次实验的成绩将按“0”分计。 138 | - 推荐markdown文档格式。 139 | - (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 140 | 141 | 142 | 选做题目 143 | -------------------------------------------------------- 144 | 145 | 选作题目列表 146 | 147 | - (6分)相同页面共享(Same page sharing)fork时的Copy on Write,要求持续支持到ch8的实验内容。 148 | - (4分)实现多种(>3种)调度算法:可动态提升/降低优先级的多级反馈队列、实时调度等 149 | - (7分)多核支持与多核调度(支持进程迁移和多核模式执行应用程序,但在内核中没有抢占和多核支持),要求持续支持到ch8的实验内容。 150 | 151 | 提交要求 152 | 153 | - (占分比:40%)实现代码(包括基本的注释) 154 | - (占分比:50%)设计与功能/性能测试分析文档,测试用例。 155 | - (占分比:10%)鼓励形成可脱离OS独立存在的库,可以裸机测试或在用户态测试(比如easyfs那样) -------------------------------------------------------------------------------- /source/chapter5/index.rst: -------------------------------------------------------------------------------- 1 | 第五章:进程及进程管理 2 | ============================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | 0intro 8 | 1process 9 | 2core-data-structures 10 | 3implement-process-mechanism 11 | 4exercise 12 | -------------------------------------------------------------------------------- /source/chapter6/0intro.rst: -------------------------------------------------------------------------------- 1 | 引言 2 | ========================================= 3 | 4 | 本章导读 5 | ----------------------------------------- 6 | 7 | 本章我们将实现一个简单的文件系统 -- easyfs,能够对 **持久存储设备** (Persistent Storage) I/O 资源进行管理;将设计两种文件:常规文件和目录文件,它们均以文件系统所维护的 **磁盘文件** 形式被组织并保存在持久存储设备上。 8 | 9 | 实践体验 10 | ----------------------------------------- 11 | 12 | 获取本章代码: 13 | 14 | .. code-block:: console 15 | 16 | $ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2023S.git 17 | $ cd rCore-Tutorial-Code-2023S 18 | $ git checkout ch6 19 | 20 | 在 qemu 模拟器上运行本章代码: 21 | 22 | .. code-block:: console 23 | 24 | $ cd os 25 | $ make run 26 | 27 | 内核初始化完成之后就会进入shell程序,在这里我们运行一下本章的测例 ``ch6b_filetest_simple`` : 28 | 29 | .. code-block:: 30 | 31 | >> ch6b_filetest_simple 32 | file_test passed! 33 | Shell: Process 2 exited with code 0 34 | >> 35 | 36 | 它会将 ``Hello, world!`` 输出到另一个文件 ``filea`` ,并读取里面的内容确认输出正确。我们也可以通过命令行工具 ``ch6b_cat`` 来查看 ``filea`` 中的内容: 37 | 38 | .. code-block:: 39 | 40 | >> ch6b_cat 41 | Hello, world! 42 | Shell: Process 2 exited with code 0 43 | >> 44 | 45 | 本章代码树 46 | ----------------------------------------- 47 | 48 | .. code-block:: 49 | :linenos: 50 | 51 | ├── easy-fs(新增:从内核中独立出来的一个简单的文件系统 EasyFileSystem 的实现) 52 | │   ├── Cargo.toml 53 | │   └── src 54 | │   ├── bitmap.rs(位图抽象) 55 | │   ├── block_cache.rs(块缓存层,将块设备中的部分块缓存在内存中) 56 | │   ├── block_dev.rs(声明块设备抽象接口 BlockDevice,需要库的使用者提供其实现) 57 | │   ├── efs.rs(实现整个 EasyFileSystem 的磁盘布局) 58 | │   ├── layout.rs(一些保存在磁盘上的数据结构的内存布局) 59 | │   ├── lib.rs 60 | │   └── vfs.rs(提供虚拟文件系统的核心抽象,即索引节点 Inode) 61 | ├── easy-fs-fuse(新增:将当前 OS 上的应用可执行文件按照 easy-fs 的格式进行打包) 62 | │   ├── Cargo.toml 63 | │   └── src 64 | │   └── main.rs 65 | ├── os 66 |    ├── build.rs(修改:不再需要将用户态程序链接到内核中) 67 |    ├── Cargo.toml(修改:新增 Qemu 的块设备驱动依赖 crate) 68 |    ├── Makefile(修改:新增文件系统的构建流程) 69 |    └── src 70 |    ├── config.rs(修改:新增访问块设备所需的一些 MMIO 配置) 71 |    ├── ... 72 |    ├── drivers(新增:Qemu 平台的块设备驱动) 73 |    │   ├── block 74 |    │   │   ├── mod.rs(将不同平台上的块设备全局实例化为 BLOCK_DEVICE 提供给其他模块使用) 75 |    │   │   └── virtio_blk.rs(Qemu 平台的 virtio-blk 块设备) 76 |    │   └── mod.rs 77 |    ├── fs(新增:对文件系统及文件抽象) 78 |    │   ├── inode.rs(新增:将 easy-fs 提供的 Inode 抽象封装为内核看到的 OSInode 79 |    │   │ 并实现 fs 子模块的 File Trait) 80 |    │   ├── mod.rs 81 |    │   └── stdio.rs(新增:将标准输入输出也抽象为文件) 82 |    ├── loader.rs(移除:应用加载器 loader 子模块,本章开始从文件系统中加载应用) 83 |    ├── mm 84 |    │   ├── address.rs 85 |    │   ├── frame_allocator.rs 86 |    │   ├── heap_allocator.rs 87 |    │   ├── memory_set.rs(修改:在创建地址空间的时候插入 MMIO 虚拟页面) 88 |    │   ├── mod.rs 89 |    │   └── page_table.rs(新增:应用地址空间的缓冲区抽象 UserBuffer 及其迭代器实现) 90 |    ├── syscall 91 |    │   ├── fs.rs(修改:新增 sys_open,修改sys_read、sys_write) 92 |    │   ├── mod.rs 93 |    │   └── process.rs(修改:sys_exec 改为从文件系统中加载 ELF) 94 |    ├── task 95 |       ├── context.rs 96 |       ├── manager.rs 97 |       ├── mod.rs(修改:初始进程 INITPROC 的初始化) 98 |       ├── pid.rs 99 |       ├── processor.rs 100 |       ├── switch.rs 101 |       ├── switch.S 102 |       └── task.rs(修改:在任务控制块中加入文件描述符表的相关机制) 103 | 104 | cloc easy-fs os 105 | ------------------------------------------------------------------------------- 106 | Language files blank comment code 107 | ------------------------------------------------------------------------------- 108 | Rust 41 306 418 3349 109 | Assembly 4 53 26 526 110 | make 1 13 4 48 111 | TOML 2 4 2 23 112 | ------------------------------------------------------------------------------- 113 | SUM: 48 376 450 3946 114 | ------------------------------------------------------------------------------- 115 | 116 | .. 本章代码导读 117 | .. ----------------------------------------------------- 118 | 119 | .. 本章涉及的代码量相对较多,且与进程执行相关的管理还有直接的关系。其实我们是参考经典的UNIX基于索引的文件系统,设计了一个简化的有一级目录并支持创建/打开/读写/关闭文件一系列操作的文件系统。这里简要介绍一下在内核中添加文件系统的大致开发过程。 120 | 121 | .. 第一步是能够写出与文件访问相关的应用。这里是参考了Linux的创建/打开/读写/关闭文件的系统调用接口,力图实现一个 :ref:`简化版的文件系统模型 ` 。在用户态我们只需要遵从相关系统调用的接口约定,在用户库里完成对应的封装即可。这一过程我们在前面的章节中已经重复过多次,读者应当对其比较熟悉。其中最为关键的是系统调用可以参考 :ref:`sys_open 语义介绍 ` ,此外我们还给出了 :ref:`测例代码解读 ` 。 122 | 123 | .. 第二步就是要实现 easyfs 文件系统了。由于 Rust 语言的特点,我们可以在用户态实现 easyfs 文件系统,并在用户态完成文件系统功能的基本测试并基本验证其实现正确性之后,就可以放心的将该模块嵌入到操作系统内核中。当然,有了文件系统的具体实现,还需要对上一章的操作系统内核进行扩展,实现与 easyfs 文件系统对接的接口,这样才可以让操作系统拥有一个简单可用的文件系统。从而,内核可以支持允许文件读写功能的更复杂的应用,在命令行参数机制的加持下,可以进一步提升整个系统的灵活性,让应用的开发和调试变得更为轻松。 124 | 125 | .. easyfs 文件系统的整体架构自下而上可分为五层。它的最底层就是对块设备的访问操作接口。在 ``easy-fs/src/block_dev.rs`` 中,可以看到 ``BlockDevice`` trait 代表了一个抽象块设备,该 trait 仅需求两个函数 ``read_block`` 和 ``write_block`` ,分别代表将数据从块设备读到内存中的缓冲区中,或者将数据从内存中的缓冲区写回到块设备中,数据需要以块为单位进行读写。easy-fs 库的使用者需要负责为它们看到的实际的块设备具体实现 ``BlockDevice`` trait 并提供给 easy-fs 库的上层,这样的话 easy-fs 库的最底层就与一个具体的执行环境对接起来了。至于为什么块设备层位于 easy-fs 的最底层,是因为文件系统仅仅是在块设备上存储的结构稍微复杂一点的数据,但无论它的操作变换如何复杂,从块设备的角度终究可以被分解成若干次块读写。 126 | 127 | .. 尽管在最底层我们就已经有了块读写的能力,但从编程方便性和性能的角度,仅有块读写这么基础的底层接口是不足以实现如此复杂的文件系统的,虽然它已经被我们大幅简化过了。比如,将一个块的内容读到内存的缓冲区,对缓冲区进行修改,并尚未写回的时候,如果由于编程上的不小心再次将该块的内容读到另一个缓冲区,而不是使用已有的缓冲区,这将会造成不一致问题。此外还有可能增加很多不必要的块读写次数,大幅降低文件系统的性能。因此,通过程序自动而非程序员手动对块的缓冲区进行统一管理也就势在必行了,该机制被我们抽象为 easy-fs 自底向上的第二层,即块缓存层。在 ``easy-fs/src/block_cache.rs`` 中, ``BlockCache`` 代表一个被我们管理起来的块的缓冲区,它带有缓冲区本体以及块的编号等信息。当它被创建的时候,将触发一次 ``read_block`` 将数据从块设备读到它的缓冲区中。接下来只要它驻留在内存中,便可保证对于同一个块的所有操作都会直接在它的缓冲区中进行而无需额外的 ``read_block`` 。块缓存管理器 ``BlockManager`` 在内存中管理有限个 ``BlockCache`` 并实现了类似 FIFO 的缓存替换算法,当一个块缓存被换出的时候视情况可能调用 ``write_block`` 将缓冲区数据写回块设备。总之,块缓存层对上提供 ``get_block_cache`` 接口来屏蔽掉相关细节,从而可以透明的读写一个块。 128 | 129 | .. 有了块缓存,我们就可以在内存中方便地处理easyfs文件系统在磁盘上的各种数据了,这就是第三层文件系统的磁盘数据结构。easyfs文件系统中的所有需要持久保存的数据都会放到磁盘上,这包括了管理这个文件系统的 **超级块 (Super Block)**,管理空闲磁盘块的 **索引节点位图区** 和 **数据块位图区** ,以及管理文件的 **索引节点区** 和 放置文件数据的 **数据块区** 组成。 130 | 131 | .. easyfs文件系统中管理这些磁盘数据的控制逻辑主要集中在 **磁盘块管理器** 中,这是文件系统的第四层。对于文件系统管理而言,其核心是 ``EasyFileSystem`` 数据结构及其关键成员函数: 132 | 133 | .. - EasyFileSystem.create:创建文件系统 134 | .. - EasyFileSystem.open:打开文件系统 135 | .. - EasyFileSystem.alloc_inode:分配inode (dealloc_inode未实现,所以还不能删除文件) 136 | .. - EasyFileSystem.alloc_data:分配数据块 137 | .. - EasyFileSystem.dealloc_data:回收数据块 138 | 139 | .. 对于单个文件的管理和读写的控制逻辑主要是 **索引节点** 来完成,这是文件系统的第五层,其核心是 ``Inode`` 数据结构及其关键成员函数: 140 | 141 | .. - Inode.new:在磁盘上的文件系统中创建一个inode 142 | .. - Inode.find:根据文件名查找对应的磁盘上的inode 143 | .. - Inode.create:在根目录下创建一个文件 144 | .. - Inode.read_at:根据inode找到文件数据所在的磁盘数据块,并读到内存中 145 | .. - Inode.write_at:根据inode找到文件数据所在的磁盘数据块,把内存中数据写入到磁盘数据块中 146 | 147 | .. 上述五层就构成了easyfs文件系统的整个内容。我们可以把easyfs文件系统看成是一个库,被应用程序调用。而 ``easy-fs-fuse`` 这个应用就通过调用easyfs文件系统库中各种函数,并用Linux上的文件模拟了一个块设备,就可以在这个模拟的块设备上创建了一个easyfs文件系统。 148 | 149 | .. 第三步,我们需要把easyfs文件系统加入到我们的操作系统内核中。这还需要做两件事情,第一件是在Qemu模拟的 ``virtio`` 块设备上实现块设备驱动程序 ``os/src/drivers/block/virtio_blk.rs`` 。由于我们可以直接使用 ``virtio-drivers`` crate中的块设备驱动,所以只要提供这个块设备驱动所需要的内存申请与释放以及虚实地址转换的4个函数就可以了。而我们之前操作系统中的虚存管理实现中,以及有这些函数,导致块设备驱动程序很简单,具体实现细节都被 ``virtio-drivers`` crate封装好了。 150 | 151 | .. 第二件事情是把文件访问相关的系统调用与easyfs文件系统连接起来。在easfs文件系统中是没有进程的概念的。而进程是程序运行过程中访问资源的管理实体,这就要对 ``easy-fs`` crate 提供的 ``Inode`` 结构进一步封装,形成 ``OSInode`` 结构,以表示进程中一个打开的常规文件。对于应用程序而言,它理解的磁盘数据是常规的文件和目录,不是 ``OSInode`` 这样相对复杂的结构。其实常规文件对应的 OSInode 是文件在操作系统内核中的内部表示,因此需要为它实现 File Trait 从而能够可以将它放入到进程文件描述符表中,并通过 sys_read/write 系统调用进行读写。这样就建立了文件与 ``OSInode`` 的对应关系,并通过上面描述的三个步骤完成了包含文件系统的操作系统内核,并能给应用提供基于文件的系统调用服务。 152 | 153 | .. 完成包含文件系统的操作系统内核后,我们在shell程序和内核中支持命令行参数的解析和传递,这样可以让应用根据灵活地通过命令行参数来动态地表示要操作的文件。这需要扩展对应的系统调用 ``sys_exec`` ,主要的改动就是在创建新进程时,把命令行参数压入用户栈中,这样应用程序在执行时就可以从用户栈中获取到命令行的参数值了。 154 | 155 | .. 在上一章,我们提到了把标准输出设备在文件描述符表中的文件描述符的值规定为 1 ,用 Stdin 表示;把标准输入设备在文件描述符表中的文件描述符的值规定为 0,用 stdout 表示 。另外,还有一条文件描述符相关的重要规则:即进程打开一个文件的时候,内核总是会将文件分配到该进程文件描述符表中编号 最小的 空闲位置。利用这些约定,只实现新的系统调用 ``sys_dup`` 完成对文件描述符的复制,就可以巧妙地实现标准 I/O 重定向功能了。 156 | 157 | .. 具体思路是,在某应用进程执行之前,父进程(比如 user_shell进程)要对子应用进程的文件描述符表进行某种替换。以输出为例,父进程在创建子进程前,提前打开一个常规文件 A,然后 ``fork`` 子进程,在子进程的最初执行中,通过 ``sys_close`` 关闭 Stdout 文件描述符,用 ``sys_dup`` 复制常规文件 A 的文件描述符,这样 Stdout 文件描述符实际上指向的就是常规文件A了,这时再通过 ``sys_close`` 关闭常规文件 A 的文件描述符。至此,常规文件 A 替换掉了应用文件描述符表位置 1 处的标准输出文件,这就完成了所谓的 **重定向** ,即完成了执行新应用前的准备工作。 158 | 159 | .. 接下来是子进程调用 ``sys_exec`` 系统调用,创建并开始执行新子应用进程。在重定向之后,新的子应用进程认为自己输出到 fd=1 的标准输出文件,但实际上是输出到父进程(比如 user_shell进程)指定的文件A中。文件这一抽象概念透明化了文件、I/O设备之间的差异,因为在进程看来无论是标准输出还是常规文件都是一种文件,可以通过同样的接口来读写。这就是文件的强大之处。 160 | -------------------------------------------------------------------------------- /source/chapter6/1file-descriptor.rst: -------------------------------------------------------------------------------- 1 | 文件与文件描述符 2 | =========================================== 3 | 4 | 文件简介 5 | ------------------------------------------- 6 | 7 | 文件可代表很多种不同类型的I/O 资源,但是在进程看来,所有文件的访问都可以通过一个简洁统一的抽象接口 ``File`` 进行: 8 | 9 | .. code-block:: rust 10 | 11 | // os/src/fs/mod.rs 12 | 13 | pub trait File : Send + Sync { 14 | fn readable(&self) -> bool; 15 | fn writable(&self) -> bool; 16 | fn read(&self, buf: UserBuffer) -> usize; 17 | fn write(&self, buf: UserBuffer) -> usize; 18 | } 19 | 20 | 21 | 这个接口在内存和I/O资源之间建立了数据交换的通道。其中 ``UserBuffer`` 是我们在 ``mm`` 子模块中定义的应用地址空间中的一段缓冲区,我们可以将它看成一个 ``&[u8]`` 切片。 22 | 23 | ``read`` 指的是从文件(即I/O资源)中读取数据放到缓冲区中,最多将缓冲区填满(即读取缓冲区的长度那么多字节),并返回实际读取的字节数;而 ``write`` 指的是将缓冲区中的数据写入文件,最多将缓冲区中的数据全部写入,并返回直接写入的字节数。 24 | 25 | 回过头来再看一下用户缓冲区的抽象 ``UserBuffer`` ,它的声明如下: 26 | 27 | .. code-block:: rust 28 | 29 | // os/src/mm/page_table.rs 30 | 31 | pub fn translated_byte_buffer( 32 | token: usize, 33 | ptr: *const u8, 34 | len: usize 35 | ) -> Vec<&'static mut [u8]>; 36 | 37 | pub struct UserBuffer { 38 | pub buffers: Vec<&'static mut [u8]>, 39 | } 40 | 41 | impl UserBuffer { 42 | pub fn new(buffers: Vec<&'static mut [u8]>) -> Self { 43 | Self { buffers } 44 | } 45 | pub fn len(&self) -> usize { 46 | let mut total: usize = 0; 47 | for b in self.buffers.iter() { 48 | total += b.len(); 49 | } 50 | total 51 | } 52 | } 53 | 54 | 它只是将我们调用 ``translated_byte_buffer`` 获得的包含多个切片的 ``Vec`` 进一步包装起来,通过 ``len`` 方法可以得到缓冲区的长度。此外,我们还让它作为一个迭代器可以逐字节进行读写。有兴趣的读者可以参考类型 ``UserBufferIterator`` 还有 ``IntoIterator`` 和 ``Iterator`` 两个 Trait 的使用方法。 55 | 56 | 标准输入和标准输出 57 | -------------------------------------------- 58 | 59 | 其实我们在第二章就对应用程序引入了基于 **文件** 的标准输出接口 ``sys_write`` ,在第五章引入标准输入接口 ``sys_read`` 。我们提前把标准输出设备在文件描述符表中的文件描述符的值规定为 ``1`` ,用 ``Stdout`` 表示;把标准输入设备文件描述符规定为 ``0``,用 ``Stdin`` 表示 。现在,我们重写这些系统调用,先为标准输入和标准输出实现 ``File`` Trait: 60 | 61 | .. code-block:: rust 62 | :linenos: 63 | 64 | // os/src/fs/stdio.rs 65 | 66 | pub struct Stdin; 67 | 68 | pub struct Stdout; 69 | 70 | impl File for Stdin { 71 | fn readable(&self) -> bool { true } 72 | fn writable(&self) -> bool { false } 73 | fn read(&self, mut user_buf: UserBuffer) -> usize { 74 | assert_eq!(user_buf.len(), 1); 75 | // busy loop 76 | let mut c: usize; 77 | loop { 78 | c = console_getchar(); 79 | if c == 0 { 80 | suspend_current_and_run_next(); 81 | continue; 82 | } else { 83 | break; 84 | } 85 | } 86 | let ch = c as u8; 87 | unsafe { user_buf.buffers[0].as_mut_ptr().write_volatile(ch); } 88 | 1 89 | } 90 | fn write(&self, _user_buf: UserBuffer) -> usize { 91 | panic!("Cannot write to stdin!"); 92 | } 93 | } 94 | 95 | impl File for Stdout { 96 | fn readable(&self) -> bool { false } 97 | fn writable(&self) -> bool { true } 98 | fn read(&self, _user_buf: UserBuffer) -> usize{ 99 | panic!("Cannot read from stdout!"); 100 | } 101 | fn write(&self, user_buf: UserBuffer) -> usize { 102 | for buffer in user_buf.buffers.iter() { 103 | print!("{}", core::str::from_utf8(*buffer).unwrap()); 104 | } 105 | user_buf.len() 106 | } 107 | } 108 | 109 | 可以看到,标准输入文件 ``Stdin`` 是只读文件,只允许进程通过 ``read`` 从里面读入,目前每次仅支持读入一个字符,其实现与之前的 ``sys_read`` 基本相同,只是需要通过 ``UserBuffer`` 来获取具体将字节写入的位置。相反,标准输出文件 ``Stdout`` 是只写文件,只允许进程通过 ``write`` 写入到里面,实现方法是遍历每个切片,将其转化为字符串通过 ``print!`` 宏来输出。 110 | 111 | 文件描述符与文件描述符表 112 | -------------------------------------------- 113 | 114 | 为简化操作系统设计实现,可以让每个进程都带有一个线性的 **文件描述符表** ,记录所有它请求内核打开并可以读写的那些文件集合。而 **文件描述符** (File Descriptor) 则是一个非负整数,表示文件描述符表中一个打开的 **文件描述符** 所处的位置(可理解为数组下标)。进程通过文件描述符,可以在自身的文件描述符表中找到对应的文件记录信息,从而也就找到了对应的文件,并对文件进行读写。当打开( ``open`` )或创建( ``create`` ) 一个文件的时候,如果顺利,内核会返回给应用刚刚打开或创建的文件对应的文件描述符;而当应用想关闭( ``close`` )一个文件的时候,也需要向内核提供对应的文件描述符。 115 | 116 | 117 | 文件I/O操作 118 | ------------------------------------------- 119 | 120 | 在进程控制块中加入文件描述符表的相应字段: 121 | 122 | .. code-block:: rust 123 | :linenos: 124 | :emphasize-lines: 12 125 | 126 | // os/src/task/task.rs 127 | 128 | pub struct TaskControlBlockInner { 129 | pub trap_cx_ppn: PhysPageNum, 130 | pub base_size: usize, 131 | pub task_cx: TaskContext, 132 | pub task_status: TaskStatus, 133 | pub memory_set: MemorySet, 134 | pub parent: Option>, 135 | pub children: Vec>, 136 | pub exit_code: i32, 137 | pub fd_table: Vec>>, 138 | } 139 | 140 | 可以看到 ``fd_table`` 的类型包含多层嵌套,我们从外到里分别说明: 141 | 142 | - ``Vec`` 的动态长度特性使得我们无需设置一个固定的文件描述符数量上限; 143 | - ``Option`` 使得我们可以区分一个文件描述符当前是否空闲,当它是 ``None`` 的时候是空闲的,而 ``Some`` 则代表它已被占用; 144 | - ``Arc`` 首先提供了共享引用能力。后面我们会提到,可能会有多个进程共享同一个文件对它进行读写。此外被它包裹的内容会被放到内核堆而不是栈上,于是它便不需要在编译期有着确定的大小; 145 | - ``dyn`` 关键字表明 ``Arc`` 里面的类型实现了 ``File/Send/Sync`` 三个 Trait ,但是编译期无法知道它具体是哪个类型(可能是任何实现了 ``File`` Trait 的类型如 ``Stdin/Stdout`` ,故而它所占的空间大小自然也无法确定),需要等到运行时才能知道它的具体类型。 146 | 147 | .. note:: 148 | 149 | **Rust 语法卡片:Rust 中的多态** 150 | 151 | 在编程语言中, **多态** (Polymorphism) 指的是在同一段代码中可以隐含多种不同类型的特征。在 Rust 中主要通过泛型和 Trait 来实现多态。 152 | 153 | 泛型是一种 **编译期多态** (Static Polymorphism),在编译一个泛型函数的时候,编译器会对于所有可能用到的类型进行实例化并对应生成一个版本的汇编代码,在编译期就能知道选取哪个版本并确定函数地址,这可能会导致生成的二进制文件体积较大;而 Trait 对象(也即上面提到的 ``dyn`` 语法)是一种 **运行时多态** (Dynamic Polymorphism),需要在运行时查一种类似于 C++ 中的 **虚表** (Virtual Table) 才能找到实际类型对于抽象接口实现的函数地址并进行调用,这样会带来一定的运行时开销,但是更为灵活。 154 | 155 | 当新建一个进程的时候,我们需要按照先前的说明为进程打开标准输入文件和标准输出文件: 156 | 157 | .. code-block:: rust 158 | :linenos: 159 | :emphasize-lines: 19-26 160 | 161 | // os/src/task/task.rs 162 | 163 | impl TaskControlBlock { 164 | pub fn new(elf_data: &[u8]) -> Self { 165 | ... 166 | let task_control_block = Self { 167 | pid: pid_handle, 168 | kernel_stack, 169 | inner: unsafe { 170 | UPSafeCell::new(TaskControlBlockInner { 171 | trap_cx_ppn, 172 | base_size: user_sp, 173 | task_cx: TaskContext::goto_trap_return(kernel_stack_top), 174 | task_status: TaskStatus::Ready, 175 | memory_set, 176 | parent: None, 177 | children: Vec::new(), 178 | exit_code: 0, 179 | fd_table: vec![ 180 | // 0 -> stdin 181 | Some(Arc::new(Stdin)), 182 | // 1 -> stdout 183 | Some(Arc::new(Stdout)), 184 | // 2 -> stderr 185 | Some(Arc::new(Stdout)), 186 | ], 187 | }) 188 | }, 189 | }; 190 | ... 191 | } 192 | } 193 | 194 | 此外,在 fork 时,子进程需要完全继承父进程的文件描述符表来和父进程共享所有文件。这样,即使我们仅手动为初始进程 ``initproc`` 打开了标准输入输出,所有进程也都可以访问它们。 195 | 196 | 文件读写系统调用 197 | --------------------------------------------------- 198 | 199 | 基于文件抽象接口和文件描述符表,我们终于可以让文件读写系统调用 ``sys_read/write`` 变得更加具有普适性,不仅仅局限于之前特定的标准输入输出: 200 | 201 | .. code-block:: rust 202 | 203 | // os/src/syscall/fs.rs 204 | 205 | pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize { 206 | let token = current_user_token(); 207 | let task = current_task().unwrap(); 208 | let inner = task.acquire_inner_lock(); 209 | if fd >= inner.fd_table.len() { 210 | return -1; 211 | } 212 | if let Some(file) = &inner.fd_table[fd] { 213 | let file = file.clone(); 214 | // release Task lock manually to avoid deadlock 215 | drop(inner); 216 | file.write( 217 | UserBuffer::new(translated_byte_buffer(token, buf, len)) 218 | ) as isize 219 | } else { 220 | -1 221 | } 222 | } 223 | 224 | pub fn sys_read(fd: usize, buf: *const u8, len: usize) -> isize { 225 | let token = current_user_token(); 226 | let task = current_task().unwrap(); 227 | let inner = task.acquire_inner_lock(); 228 | if fd >= inner.fd_table.len() { 229 | return -1; 230 | } 231 | if let Some(file) = &inner.fd_table[fd] { 232 | let file = file.clone(); 233 | // release Task lock manually to avoid deadlock 234 | drop(inner); 235 | file.read( 236 | UserBuffer::new(translated_byte_buffer(token, buf, len)) 237 | ) as isize 238 | } else { 239 | -1 240 | } 241 | } 242 | 243 | 我们都是在当前进程的文件描述符表中通过文件描述符找到某个文件,无需关心文件具体的类型,只要知道它一定实现了 ``File`` Trait 的 ``read/write`` 方法即可。Trait 对象提供的运行时多态能力会在运行的时候帮助我们定位到 ``read/write`` 的符合实际类型的实现。 244 | -------------------------------------------------------------------------------- /source/chapter6/1fs-interface.rst: -------------------------------------------------------------------------------- 1 | 文件系统接口 2 | ================================================= 3 | 4 | 简易文件与目录抽象 5 | ------------------------------------------------- 6 | 7 | 与课堂所学相比,我们实现的文件系统进行了很大的简化: 8 | 9 | - 扁平化:仅存在根目录 ``/`` 一个目录,所有的文件都放在根目录内。直接以文件名索引文件。 10 | - 不设置用户和用户组概念,不记录文件访问/修改的任何时间戳,不支持软硬链接。 11 | - 只实现了最基本的文件系统相关系统调用。 12 | 13 | 打开与读写文件的系统调用 14 | -------------------------------------------------- 15 | 16 | 打开文件 17 | ++++++++++++++++++++++++++++++++++++++++++++++++++ 18 | 19 | .. code-block:: rust 20 | 21 | /// 功能:打开一个常规文件,并返回可以访问它的文件描述符。 22 | /// 参数:path 描述要打开的文件的文件名(简单起见,文件系统不需要支持目录,所有的文件都放在根目录 / 下), 23 | /// flags 描述打开文件的标志,具体含义下面给出。 24 | /// dirfd 和 mode 仅用于保证兼容性,忽略 25 | /// 返回值:如果出现了错误则返回 -1,否则返回打开常规文件的文件描述符。可能的错误原因是:文件不存在。 26 | /// syscall ID:56 27 | fn sys_openat(dirfd: usize, path: &str, flags: u32, mode: u32) -> isize 28 | 29 | 目前我们的内核支持以下几种标志(多种不同标志可能共存): 30 | 31 | - 如果 ``flags`` 为 0,则表示以只读模式 *RDONLY* 打开; 32 | - 如果 ``flags`` 第 0 位被设置(0x001),表示以只写模式 *WRONLY* 打开; 33 | - 如果 ``flags`` 第 1 位被设置(0x002),表示既可读又可写 *RDWR* ; 34 | - 如果 ``flags`` 第 9 位被设置(0x200),表示允许创建文件 *CREATE* ,在找不到该文件的时候应创建文件;如果该文件已经存在则应该将该文件的大小归零; 35 | - 如果 ``flags`` 第 10 位被设置(0x400),则在打开文件的时候应该清空文件的内容并将该文件的大小归零,也即 *TRUNC* 。 36 | 37 | 在用户库 ``user_lib`` 中,我们将该系统调用封装为 ``open`` 接口: 38 | 39 | .. code-block:: rust 40 | 41 | // user/src/lib.rs 42 | 43 | bitflags! { 44 | pub struct OpenFlags: u32 { 45 | const RDONLY = 0; 46 | const WRONLY = 1 << 0; 47 | const RDWR = 1 << 1; 48 | const CREATE = 1 << 9; 49 | const TRUNC = 1 << 10; 50 | } 51 | } 52 | 53 | pub fn open(path: &str, flags: OpenFlags) -> isize { 54 | sys_openat(AT_FDCWD as usize, path, flags.bits, OpenFlags::RDWR.bits) 55 | } 56 | 57 | 借助 ``bitflags!`` 宏我们将一个 ``u32`` 的 flags 包装为一个 ``OpenFlags`` 结构体,可以从它的 ``bits`` 字段获得 ``u32`` 表示。 58 | 59 | 60 | 顺序读写文件 61 | ++++++++++++++++++++++++++++++++++++++++++++++++++ 62 | 63 | 在打开一个文件之后,我们就可以用之前的 ``sys_read/sys_write`` 两个系统调用来对它进行读写了。本教程只实现文件的顺序读写,而不考虑随机读写。 64 | 65 | 以本章的测试用例 ``ch6b_filetest_simple`` 来介绍文件系统接口的使用方法: 66 | 67 | .. code-block:: rust 68 | :linenos: 69 | 70 | // user/src/bin/ch6b_filetest_simple.rs 71 | 72 | #![no_std] 73 | #![no_main] 74 | 75 | #[macro_use] 76 | extern crate user_lib; 77 | 78 | use user_lib::{ 79 | open, 80 | close, 81 | read, 82 | write, 83 | OpenFlags, 84 | }; 85 | 86 | #[no_mangle] 87 | pub fn main() -> i32 { 88 | let test_str = "Hello, world!"; 89 | let filea = "filea\0"; 90 | let fd = open(filea, OpenFlags::CREATE | OpenFlags::WRONLY); 91 | assert!(fd > 0); 92 | let fd = fd as usize; 93 | write(fd, test_str.as_bytes()); 94 | close(fd); 95 | 96 | let fd = open(filea, OpenFlags::RDONLY); 97 | assert!(fd > 0); 98 | let fd = fd as usize; 99 | let mut buffer = [0u8; 100]; 100 | let read_len = read(fd, &mut buffer) as usize; 101 | close(fd); 102 | 103 | assert_eq!( 104 | test_str, 105 | core::str::from_utf8(&buffer[..read_len]).unwrap(), 106 | ); 107 | println!("file_test passed!"); 108 | 0 109 | } 110 | 111 | - 第 20~25 行,我们以 *只写 + 创建* 的模式打开文件 ``filea`` ,向其中写入字符串 ``Hello, world!`` 而后关闭文件。 112 | - 第 27~32 行,我们以只读 的方式将文件 ``filea`` 的内容读取到缓冲区 ``buffer`` 中。 ``filea`` 的总大小不超过缓冲区的大小,因此通过单次 ``read`` 即可将内容全部读出来而更常见的情况是需要进行多次 ``read`` ,直到返回值为 0 才能确认文件已被读取完毕。 113 | -------------------------------------------------------------------------------- /source/chapter6/4exercise.rst: -------------------------------------------------------------------------------- 1 | chapter6练习 2 | ================================================ 3 | 4 | 编程作业 5 | ------------------------------------------------- 6 | 7 | 硬链接 8 | ++++++++++++++++++++++++++++++++++++++++++++++++++ 9 | 10 | 硬链接要求两个不同的目录项指向同一个文件,在我们的文件系统中也就是两个不同名称目录项指向同一个磁盘块。 11 | 12 | 本节要求实现三个系统调用 ``sys_linkat、sys_unlinkat、sys_stat`` 。 13 | 14 | **linkat**: 15 | 16 | * syscall ID: 37 17 | * 功能:创建一个文件的一个硬链接, `linkat标准接口 `_ 。 18 | * C接口: ``int linkat(int olddirfd, char* oldpath, int newdirfd, char* newpath, unsigned int flags)`` 19 | * Rust 接口: ``fn linkat(olddirfd: i32, oldpath: *const u8, newdirfd: i32, newpath: *const u8, flags: u32) -> i32`` 20 | * 参数: 21 | * olddirfd,newdirfd: 仅为了兼容性考虑,本次实验中始终为 AT_FDCWD (-100),可以忽略。 22 | * flags: 仅为了兼容性考虑,本次实验中始终为 0,可以忽略。 23 | * oldpath:原有文件路径 24 | * newpath: 新的链接文件路径。 25 | * 说明: 26 | * 为了方便,不考虑新文件路径已经存在的情况(属于未定义行为),除非链接同名文件。 27 | * 返回值:如果出现了错误则返回 -1,否则返回 0。 28 | * 可能的错误 29 | * 链接同名文件。 30 | 31 | **unlinkat**: 32 | 33 | * syscall ID: 35 34 | * 功能:取消一个文件路径到文件的链接, `unlinkat标准接口 `_ 。 35 | * C接口: ``int unlinkat(int dirfd, char* path, unsigned int flags)`` 36 | * Rust 接口: ``fn unlinkat(dirfd: i32, path: *const u8, flags: u32) -> i32`` 37 | * 参数: 38 | * dirfd: 仅为了兼容性考虑,本次实验中始终为 AT_FDCWD (-100),可以忽略。 39 | * flags: 仅为了兼容性考虑,本次实验中始终为 0,可以忽略。 40 | * path:文件路径。 41 | * 说明: 42 | * 注意考虑使用 unlink 彻底删除文件的情况,此时需要回收inode以及它对应的数据块。 43 | * 返回值:如果出现了错误则返回 -1,否则返回 0。 44 | * 可能的错误 45 | * 文件不存在。 46 | 47 | **fstat**: 48 | 49 | * syscall ID: 80 50 | * 功能:获取文件状态。 51 | * C接口: ``int fstat(int fd, struct Stat* st)`` 52 | * Rust 接口: ``fn fstat(fd: i32, st: *mut Stat) -> i32`` 53 | * 参数: 54 | * fd: 文件描述符 55 | * st: 文件状态结构体 56 | 57 | .. code-block:: rust 58 | 59 | #[repr(C)] 60 | #[derive(Debug)] 61 | pub struct Stat { 62 | /// 文件所在磁盘驱动器号,该实验中写死为 0 即可 63 | pub dev: u64, 64 | /// inode 文件所在 inode 编号 65 | pub ino: u64, 66 | /// 文件类型 67 | pub mode: StatMode, 68 | /// 硬链接数量,初始为1 69 | pub nlink: u32, 70 | /// 无需考虑,为了兼容性设计 71 | pad: [u64; 7], 72 | } 73 | 74 | /// StatMode 定义: 75 | bitflags! { 76 | pub struct StatMode: u32 { 77 | const NULL = 0; 78 | /// directory 79 | const DIR = 0o040000; 80 | /// ordinary regular file 81 | const FILE = 0o100000; 82 | } 83 | } 84 | 85 | 86 | 实验要求 87 | +++++++++++++++++++++++++++++++++++++++++++++ 88 | - 实现分支:ch6。 89 | - 实验目录要求不变。 90 | - 通过所有测例。 91 | 92 | 在 os 目录下 ``make run BASE=2`` 加载所有测例, ``ch6_usertest`` 打包了所有你需要通过的测例,你也可以通过修改这个文件调整本地测试的内容。 93 | 94 | 你的内核必须前向兼容,能通过前一章的所有测例。 95 | 96 | .. note:: 97 | 98 | **如何调试 easy-fs** 99 | 100 | 如果你在第一章练习题中已经借助 ``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`` 。 101 | 102 | 你也可以完全在用户态进行调试。仿照 ``easy-fs-fuse`` 建立一个在当前操作系统中运行的应用程序,将测试逻辑写在 ``main`` 函数中。这个时候就可以将它引用的 ``easy-fs`` 的 ``no_std`` 去掉并使用 ``println!`` 进行调试。 103 | 104 | 105 | 问答作业 106 | ---------------------------------------------------------- 107 | 108 | 1. 在我们的easy-fs中,root inode起着什么作用?如果root inode中的内容损坏了,会发生什么? 109 | 110 | 报告要求 111 | ----------------------------------------------------------- 112 | - 简单总结你实现的功能(200字以内,不要贴代码)。 113 | - 完成问答题。 114 | - 加入 :doc:`/honorcode` 的内容。否则,你的提交将视作无效,本次实验的成绩将按“0”分计。 115 | - 推荐markdown文档格式。 116 | - (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 117 | 118 | 119 | 选做题目 120 | -------------------------------------------------------- 121 | 122 | 选作题目列表 123 | 124 | - (6分)按需加载执行文件(Demanding Paging),要求持续支持到ch8的实验内容。 125 | - (7分)log-easyfs:实现基于日志的可靠文件系统(可参考xv6-fs),要求持续支持到ch8的实验内容。 126 | 127 | 提交要求 128 | 129 | - (占分比:40%)实现代码(包括基本的注释) 130 | - (占分比:50%)设计与功能/性能测试分析文档,测试用例。 131 | - (占分比:10%)鼓励形成可脱离OS独立存在的库,可以裸机测试或在用户态测试(比如easyfs那样) 132 | -------------------------------------------------------------------------------- /source/chapter6/easy-fs-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter6/easy-fs-demo.png -------------------------------------------------------------------------------- /source/chapter6/index.rst: -------------------------------------------------------------------------------- 1 | 第六章:文件系统与I/O重定向 2 | ============================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | 0intro 8 | 1file-descriptor.rst 9 | 1fs-interface 10 | 2fs-implementation-1 11 | 2fs-implementation-2 12 | 3using-easy-fs-in-kernel 13 | 4exercise 14 | -------------------------------------------------------------------------------- /source/chapter7/0intro.rst: -------------------------------------------------------------------------------- 1 | 引言 2 | ========================================= 3 | 4 | 本章导读 5 | ----------------------------------------- 6 | 7 | 本章将基于文件描述符实现父子进程之间的通信机制——管道。 8 | 我们还将扩展 ``exec`` 系统调用,使之能传递运行参数,并进一步改进 shell 程序,使其支持重定向符号 ``>`` 和 ``<`` 。 9 | 10 | 实践体验 11 | ----------------------------------------- 12 | 13 | 获取本章代码: 14 | 15 | .. code-block:: console 16 | 17 | $ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2023S.git 18 | $ cd rCore-Tutorial-Code-2023S 19 | $ git checkout ch7 20 | 21 | 在 qemu 模拟器上运行本章代码: 22 | 23 | .. code-block:: console 24 | 25 | $ cd os 26 | $ make run 27 | 28 | 进入shell程序后,可以运行管道机制的简单测例 ``ch7b_pipetest``, ``ch7b_pipetest`` 需要保证父进程通过管道传输给子进程的字符串不会发生变化。 29 | 30 | 测例输出大致如下: 31 | 32 | .. code-block:: 33 | 34 | >> ch7b_pipetest 35 | Read OK, child process exited! 36 | pipetest passed! 37 | Shell: Process 2 exited with code 0 38 | >> 39 | 40 | 同样的,也可以运行较为复杂的测例 ``ch7b_pipe_large_test``,体验通过两个管道实现双向通信。 41 | 42 | 此外,在本章我们为shell程序支持了输入/输出重定向功能,可以将一个应用的输出保存到一个指定的文件。例如,下面的命令可以将 ``ch7b_yield`` 应用的输出保存在文件 ``fileb`` 当中,并在应用执行完毕之后确认它的输出: 43 | 44 | .. code-block:: 45 | 46 | >> ch7b_yield > fileb 47 | Shell: Process 2 exited with code 0 48 | >> ch7b_cat fileb 49 | Hello, I am process 2. 50 | Back in process 2, iteration 0. 51 | Back in process 2, iteration 1. 52 | Back in process 2, iteration 2. 53 | Back in process 2, iteration 3. 54 | Back in process 2, iteration 4. 55 | yield pass. 56 | 57 | Shell: Process 2 exited with code 0 58 | >> 59 | 60 | 本章代码树 61 | ----------------------------------------- 62 | 63 | .. code-block:: 64 | 65 | ── os 66 |    └── src 67 |    ├── ... 68 |    ├── fs 69 |    │   ├── inode.rs 70 |    │   ├── mod.rs 71 |    │   ├── pipe.rs(新增:实现了 File Trait 的第三个实现——可用来进程间通信的管道) 72 |    │   └── stdio.rs 73 |    ├── mm 74 |    │   ├── address.rs 75 |    │   ├── frame_allocator.rs 76 |    │   ├── heap_allocator.rs 77 |    │   ├── memory_set.rs 78 |    │   ├── mod.rs 79 |    │   └── page_table.rs 80 |    ├── syscall 81 |    │   ├── fs.rs(修改:添加了sys_pipe和sys_dup) 82 |    │   ├── mod.rs 83 |    │   └── process.rs(修改:sys_exec添加了对参数的支持) 84 |    ├── task 85 |       ├── context.rs 86 |       ├── manager.rs 87 |       ├── mod.rs 88 |       ├── pid.rs 89 |       ├── processor.rs 90 |       ├── switch.rs 91 |       ├── switch.S 92 |       └── task.rs(修改:在exec中将参数压入用户栈中) 93 | 94 | cloc easy-fs os 95 | ------------------------------------------------------------------------------- 96 | Language files blank comment code 97 | ------------------------------------------------------------------------------- 98 | Rust 42 317 434 3574 99 | Assembly 4 53 26 526 100 | make 1 13 4 48 101 | TOML 2 4 2 23 102 | ------------------------------------------------------------------------------- 103 | SUM: 49 387 466 4171 104 | ------------------------------------------------------------------------------- 105 | 106 | 107 | .. 本章代码导读 108 | .. ----------------------------------------------------- 109 | 110 | .. 在本章第一节 :doc:`/chapter6/1file-descriptor` 中,我们引入了文件的概念,用它来代表进程可以读写的多种被内核管理的硬件/软件资源。进程必须通过系统调用打开一个文件,将文件加入到自身的文件描述符表中,才能通过文件描述符(也就是某个特定文件在自身文件描述符表中的下标)来读写该文件。 111 | 112 | .. 文件的抽象 Trait ``File`` 声明在 ``os/src/fs/mod.rs`` 中,它提供了 ``read/write`` 两个接口,可以将数据写入应用缓冲区抽象 ``UserBuffer`` ,或者从应用缓冲区读取数据。应用缓冲区抽象类型 ``UserBuffer`` 来自 ``os/src/mm/page_table.rs`` 中,它将 ``translated_byte_buffer`` 得到的 ``Vec<&'static mut [u8]>`` 进一步包装,不仅保留了原有的分段读写能力,还可以将其转化为一个迭代器逐字节进行读写,这在读写一些流式设备的时候特别有用。 113 | 114 | .. 在进程控制块 ``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`` 复制一份给子进程。 115 | 116 | .. 到本章为止我们支持两种文件:标准输入输出和管道。不同于前面章节,我们将标准输入输出分别抽象成 ``Stdin`` 和 ``Stdout`` 两个类型,并为他们实现 ``File`` Trait 。在 ``TaskControlBlock::new`` 创建初始进程的时候,就默认打开了标准输入输出,并分别绑定到文件描述符 0 和 1 上面。 117 | 118 | .. 管道 ``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`` 两个文件。 119 | -------------------------------------------------------------------------------- /source/chapter7/3exercise.rst: -------------------------------------------------------------------------------- 1 | chapter7练习 2 | =========================================== 3 | 4 | 编程作业 5 | ------------------------------------------- 6 | 7 | 本章无编程作业 8 | 9 | 问答作业 10 | ------------------------------------------- 11 | 12 | (1) 举出使用 pipe 的一个实际应用的例子。 13 | 14 | tips: 15 | 16 | - 想想你平时咋使用 linux terminal 的? 17 | - 如何使用 cat 和 wc 完成一个文件的行数统计? 18 | 19 | 20 | (2) 如果需要在多个进程间互相通信,则需要为每一对进程建立一个管道,非常繁琐,请设计一个更易用的多进程通信机制。 21 | 22 | 报告要求 23 | --------------------------------------- 24 | 25 | - 注意本节问答题请和 ch6 一并完成,并一同交到 ch6 分支的 lab4.md 或 lab4.pdf。 26 | - (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 27 | -------------------------------------------------------------------------------- /source/chapter7/index.rst: -------------------------------------------------------------------------------- 1 | 第七章:进程间通信 2 | ============================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | 0intro 8 | 1pipe 9 | 2cmdargs-and-redirection 10 | 3exercise 11 | -------------------------------------------------------------------------------- /source/chapter7/user-stack-cmdargs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter7/user-stack-cmdargs.png -------------------------------------------------------------------------------- /source/chapter7/user-stack-cmdargs.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Guide-2023S/d54ed0c058bdd6b75a6145949ae3a7da14f6a376/source/chapter7/user-stack-cmdargs.pptx -------------------------------------------------------------------------------- /source/chapter8/0intro.rst: -------------------------------------------------------------------------------- 1 | 引言 2 | ========================================= 3 | 4 | 本章导读 5 | ----------------------------------------- 6 | 7 | 到本章开始之前,我们好像已经完成了组成应用程序执行环境的操作系统的三个重要抽象:进程、地址空间和文件, 8 | 让应用程序开发、运行和存储数据越来越方便和灵活。有了进程以后,可以让操作系统从宏观层面实现多个应用的并发执行, 9 | 而并发是通过操作系统基于处理器的时间片不断地切换进程来达到的。到目前为止的并发,仅仅是进程间的并发, 10 | 对于一个进程内部还没有并发性的体现。而这就是线程(Thread)出现的起因:提高一个进程内的并发性。 11 | 12 | .. chyyuu 13 | https://en.wikipedia.org/wiki/Per_Brinch_Hansen 关于操作系统并发 Binch Hansen 和 Hoare ??? 14 | https://en.wikipedia.org/wiki/Thread_(computing) 关于线程 15 | http://www.serpentine.com/blog/threads-faq/the-history-of-threads/ The history of threads 16 | https://en.wikipedia.org/wiki/Core_War 我喜欢的一种早期游戏 17 | [Dijkstra, 65] Dijkstra, E. W., Cooperating sequential processes, in Programming Languages, Genuys, F. (ed.), Academic Press, 1965. 18 | [Saltzer, 66] Saltzer, J. H., Traffic control in a multiplexed computer system, MAC-TR-30 (Sc.D. Thesis), July, 1966. 19 | https://en.wikipedia.org/wiki/THE_multiprogramming_system 20 | http://www.cs.utexas.edu/users/EWD/ewd01xx/EWD196.PDF 21 | https://en.wikipedia.org/wiki/Edsger_W._Dijkstra 22 | https://en.wikipedia.org/wiki/Per_Brinch_Hansen 23 | https://en.wikipedia.org/wiki/Tony_Hoare 24 | https://en.wikipedia.org/wiki/Mutual_exclusion 25 | https://en.wikipedia.org/wiki/Semaphore_(programming) 26 | https://en.wikipedia.org/wiki/Monitor_(synchronization) 27 | 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) 28 | 29 | 30 | 有了进程以后,为什么还会出现线程呢?考虑如下情况,对于很多应用(以单一进程的形式运行)而言, 31 | 逻辑上存在多个可并行执行的任务,如果其中一个任务被阻塞,将会引起不依赖该任务的其他任务也被阻塞。 32 | 举个具体的例子,我们平常用编辑器来编辑文本内容的时候,都会有一个定时自动保存的功能, 33 | 这个功能的作用是在系统或应用本身出现故障的情况前,已有的文档内容会被提前保存。 34 | 假设编辑器自动保存时由于磁盘性能导致写入较慢,导致整个进程被操作系统挂起,这就会影响到用户编辑文档的人机交互体验: 35 | 即软件的及时响应能力不足,用户只有等到磁盘写入完成后,操作系统重新调度该进程运行后,用户才可编辑。 36 | 如果我们把一个进程内的多个可并行执行任务通过一种更细粒度的方式让操作系统进行调度, 37 | 那么就可以通过处理器时间片切换实现这种细粒度的并发执行。这种细粒度的调度对象就是线程。 38 | 39 | 40 | .. _term-thread-define: 41 | 42 | 线程定义 43 | ~~~~~~~~~~~~~~~~~~~~ 44 | 45 | 简单地说,线程是进程的组成部分,进程可包含1 -- n个线程,属于同一个进程的线程共享进程的资源, 46 | 比如地址空间、打开的文件等。基本的线程由线程ID、执行状态、当前指令指针 (PC)、寄存器集合和栈组成。 47 | 线程是可以被操作系统或用户态调度器独立调度(Scheduling)和分派(Dispatch)的基本单位。 48 | 49 | 在本章之前,进程是程序的基本执行实体,是程序关于某数据集合上的一次运行活动,是系统进行资源(处理器、 50 | 地址空间和文件等)分配和调度的基本单位。在有了线程后,对进程的定义也要调整了,进程是线程的资源容器, 51 | 线程成为了程序的基本执行实体。 52 | 53 | 54 | 同步互斥 55 | ~~~~~~~~~~~~~~~~~~~~~~ 56 | 57 | 在上面提到了同步互斥和数据一致性,它们的含义是什么呢?当多个线程共享同一进程的地址空间时, 58 | 每个线程都可以访问属于这个进程的数据(全局变量)。如果每个线程使用到的变量都是其他线程不会读取或者修改的话, 59 | 那么就不存在一致性问题。如果变量是只读的,多个线程读取该变量也不会有一致性问题。但是,当一个线程修改变量时, 60 | 其他线程在读取这个变量时,可能会看到一个不一致的值,这就是数据不一致性的问题。 61 | 62 | .. note:: 63 | 64 | **并发相关术语** 65 | 66 | - 共享资源(shared resource):不同的线程/进程都能访问的变量或数据结构。 67 | - 临界区(critical section):访问共享资源的一段代码。 68 | - 竞态条件(race condition):多个线程/进程都进入临界区时,都试图更新共享的数据结构,导致产生了不期望的结果。 69 | - 不确定性(indeterminate): 多个线程/进程在执行过程中出现了竞态条件,导致执行结果取决于哪些线程在何时运行, 70 | 即执行结果不确定,而开发者期望得到的是确定的结果。 71 | - 互斥(mutual exclusion):一种操作原语,能保证只有一个线程进入临界区,从而避免出现竞态,并产生确定的执行结果。 72 | - 原子性(atomic):一系列操作要么全部完成,要么一个都没执行,不会看到中间状态。在数据库领域, 73 | 具有原子性的一系列操作称为事务(transaction)。 74 | - 同步(synchronization):多个并发执行的进程/线程在一些关键点上需要互相等待,这种相互制约的等待称为进程/线程同步。 75 | - 死锁(dead lock):一个线程/进程集合里面的每个线程/进程都在等待只能由这个集合中的其他一个线程/进程 76 | (包括他自身)才能引发的事件,这种情况就是死锁。 77 | - 饥饿(hungry):指一个可运行的线程/进程尽管能继续执行,但由于操作系统的调度而被无限期地忽视,导致不能执行的情况。 78 | 79 | 在后续的章节中,会大量使用上述术语,如果现在还不够理解,没关系,随着后续的一步一步的分析和实验, 80 | 相信大家能够掌握上述术语的实际含义。 81 | 82 | 83 | 84 | 实践体验 85 | ----------------------------------------- 86 | 87 | 获取本章代码: 88 | 89 | .. code-block:: console 90 | 91 | $ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2023S.git 92 | $ cd rCore-Tutorial-Code-2023S 93 | $ git checkout ch8 94 | $ git clone https://github.com/LearningOS/rCore-Tutorial-Test-2023S.git user 95 | 96 | 记得更新测例仓库的代码。 97 | 98 | 在 qemu 模拟器上运行本章代码: 99 | 100 | .. code-block:: console 101 | 102 | $ cd os 103 | $ make run 104 | 105 | 内核初始化完成之后就会进入 shell 程序,我们可以体会一下线程的创建和执行过程。在这里我们运行一下本章的测例 ``ch8b_threads`` : 106 | 107 | .. code-block:: 108 | 109 | >> ch8b_threads 110 | aaa....bbb...ccc... 111 | thread#1 exited with code 1 112 | thread#2 exited with code 2 113 | thread#3 exited with code 3 114 | main thread exited. 115 | Shell: Process 2 exited with code 0 116 | >> 117 | 118 | 它会有4个线程在执行,等前3个线程执行完毕后,主线程退出,导致整个进程退出。 119 | 120 | 此外,在本章的操作系统支持通过互斥来执行“哲学家就餐问题”这个应用程序: 121 | 122 | .. code-block:: 123 | 124 | >> ch8b_phil_din_mutex 125 | Here comes 5 philosophers! 126 | time cost = 720 127 | '-' -> THINKING; 'x' -> EATING; ' ' -> WAITING 128 | #0: ------- xxxxxxxx---------- xxxx----- xxxxxx--xxx 129 | #1: ---xxxxxx-- xxxxxxx---------- x---xxxxxx 130 | #2: ----- xx---------xx----xxxxxx------------ xxxx 131 | #3: -----xxxxxxxxxx------xxxxx-------- xxxxxx-- xxxxxxxxx 132 | #4: ------ x------ xxxxxx-- xxxxx------ xx 133 | #0: ------- xxxxxxxx---------- xxxx----- xxxxxx--xxx 134 | Shell: Process 2 exited with code 0 135 | >> 136 | 137 | 我们可以看到5个代表“哲学家”的线程通过操作系统的 **信号量** 互斥机制在进行 “THINKING”、“EATING”、“WAITING” 的日常生活。 138 | 没有哲学家由于拿不到筷子而饥饿,也没有两个哲学家同时拿到一个筷子。 139 | 140 | .. note:: 141 | 142 | **哲学家就餐问题** 143 | 144 | 计算机科学家 Dijkstra 提出并解决的哲学家就餐问题是经典的进程同步互斥问题。哲学家就餐问题描述如下: 145 | 146 | 有5个哲学家共用一张圆桌,分别坐在周围的5张椅子上,在圆桌上有5个碗和5只筷子,他们的生活方式是交替地进行思考和进餐。 147 | 平时,每个哲学家进行思考,饥饿时便试图拿起其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐完毕,放下筷子继续思考。 148 | 149 | 150 | 本章代码树 151 | ----------------------------------------- 152 | 153 | .. code-block:: 154 | :linenos: 155 | 156 | . 157 | ├── bootloader 158 | │ └── rustsbi-qemu.bin 159 | ├── Dockerfile 160 | ├── easy-fs 161 | │ ├── Cargo.lock 162 | │ ├── Cargo.toml 163 | │ └── src 164 | │ ├── bitmap.rs 165 | │ ├── block_cache.rs 166 | │ ├── block_dev.rs 167 | │ ├── efs.rs 168 | │ ├── layout.rs 169 | │ ├── lib.rs 170 | │ └── vfs.rs 171 | ├── easy-fs-fuse 172 | │ ├── Cargo.lock 173 | │ ├── Cargo.toml 174 | │ └── src 175 | │ └── main.rs 176 | ├── LICENSE 177 | ├── Makefile 178 | ├── os 179 | │ ├── build.rs 180 | │ ├── Cargo.lock 181 | │ ├── Cargo.toml 182 | │ ├── Makefile 183 | │ └── src 184 | │ ├── config.rs (修改:扩大了内核堆空间) 185 | │ ├── console.rs 186 | │ ├── drivers 187 | │ │ ├── block 188 | │ │ │ ├── mod.rs 189 | │ │ │ └── virtio_blk.rs 190 | │ │ └── mod.rs 191 | │ ├── entry.asm 192 | │ ├── fs 193 | │ │ ├── inode.rs 194 | │ │ ├── mod.rs 195 | │ │ ├── pipe.rs 196 | │ │ └── stdio.rs 197 | │ ├── lang_items.rs 198 | │ ├── linker.ld 199 | │ ├── logging.rs 200 | │ ├── main.rs 201 | │ ├── mm 202 | │ │ ├── address.rs 203 | │ │ ├── frame_allocator.rs 204 | │ │ ├── heap_allocator.rs 205 | │ │ ├── memory_set.rs (修改:去除了构建进程地址空间时分配用户栈和映射陷入上下文的逻辑) 206 | │ │ ├── mod.rs 207 | │ │ └── page_table.rs 208 | │ ├── sbi.rs 209 | │ ├── sync (新增:互斥锁、信号量和条件变量三种同步互斥机制的实现) 210 | │ │ ├── condvar.rs 211 | │ │ ├── mod.rs 212 | │ │ ├── mutex.rs 213 | │ │ ├── semaphore.rs 214 | │ │ └── up.rs 215 | │ ├── syscall 216 | │ │ ├── fs.rs (修改:将原先对 task 的调用改为对 process 的调用) 217 | │ │ ├── mod.rs 218 | │ │ ├── process.rs (修改:将原先对 task 的调用改为对 process 的调用) 219 | │ │ ├── sync.rs (新增:三种同步互斥机制相关的系统调用,以及基于定时器条件变量的 sleep 调用) 220 | │ │ └── thread.rs (新增:线程相关系统调用) 221 | │ ├── task 222 | │ │ ├── context.rs (修改:将任务上下文的成员变量改为 pub 类型) 223 | │ │ ├── id.rs (新增:由 pid.rs 修改而来,提供 pid/tid 、 kstack/ustack 的分配和回收机制) 224 | │ │ ├── kthread.rs (新增:完全在内核态运行的线程,仅供参考,在实验中未使用) 225 | │ │ ├── manager.rs 226 | │ │ ├── mod.rs (修改:增加阻塞线程的功能,将 exit 扩展到多线程,并在主线程退出时一并退出进程) 227 | │ │ ├── processor.rs (修改:增加获取当前线程的中断上下文虚拟地址及获取当前进程的功能) 228 | │ │ ├── process.rs (新增:将原先 Task 中的地址空间、文件等机制拆分为进程) 229 | │ │ ├── stackless_coroutine.rs (新增:完全在内核态运行的无栈协程,仅供参考,在实验中未使用) 230 | │ │ ├── switch.rs 231 | │ │ ├── switch.S 232 | │ │ └── task.rs (修改:将进程相关的功能移至 process.rs 中) 233 | │ ├── timer.rs (修改:增加定时器条件变量的实现) 234 | │ └── trap 235 | │ ├── context.rs 236 | │ ├── mod.rs (修改:使用线程对应的中断上下文地址而非固定的 TRAP_CONTEXT) 237 | │ └── trap.S 238 | ├── README.md 239 | └── rust-toolchain 240 | -------------------------------------------------------------------------------- /source/chapter8/3semaphore.rst: -------------------------------------------------------------------------------- 1 | 信号量机制 2 | ========================================= 3 | 4 | 本节导读 5 | ----------------------------------------- 6 | 7 | .. chyyuu https://en.wikipedia.org/wiki/Semaphore_(programming) 8 | 9 | 在上一节中,我们介绍了互斥锁(mutex 或 lock)的起因、使用和实现过程。通过互斥锁, 10 | 可以让线程在临界区执行时,独占临界资源。当我们需要更灵活的互斥访问或同步操作方式,如提供了最多只允许 11 | N 个线程访问临界资源的情况,让某个线程等待另外一个线程执行完毕后再继续执行的同步过程等, 12 | 互斥锁这种方式就有点力不从心了。 13 | 14 | 在本节中,将介绍功能更加强大和灵活的同步互斥机制 -- 信号量(Semaphore),它的设计思路、 15 | 使用和在操作系统中的具体实现。可以看到,信号量的实现需要互斥锁和处理器原子指令的支持, 16 | 它是一种更高级的同步互斥机制。 17 | 18 | 19 | 信号量的起源和基本思路 20 | ----------------------------------------- 21 | 22 | 1963 年前后,当时的数学家(其实是计算机科学家)Edsger Dijkstra 和他的团队在为 Electrologica X8 23 | 计算机开发一个操作系统(称为 THE multiprogramming system,THE 多道程序系统)的过程中,提出了信号量 24 | (Semphore)是一种变量或抽象数据类型,用于控制多个线程对共同资源的访问。 25 | 26 | 信号量是对互斥锁的一种巧妙的扩展。上一节中的互斥锁的初始值一般设置为 1 的整型变量, 27 | 表示临界区还没有被某个线程占用。互斥锁用 0 表示临界区已经被占用了,用 1 表示临界区为空,再通过 28 | ``lock/unlock`` 操作来协调多个线程轮流独占临界区执行。而信号量的初始值可设置为 N 的整数变量, 如果 N 29 | 大于 0, 表示最多可以有 N 个线程进入临界区执行,如果 N 小于等于 0 ,表示不能有线程进入临界区了, 30 | 必须在后续操作中让信号量的值加 1 ,才能唤醒某个等待的线程。 31 | 32 | Dijkstra 对信号量设计了两种操作:P(Proberen(荷兰语),尝试)操作和 V(Verhogen(荷兰语),增加)操作。 33 | P 操作是检查信号量的值是否大于 0,若该值大于 0,则将其值减 1 并继续(表示可以进入临界区了);若该值为 34 | 0,则线程将睡眠。注意,此时 P 操作还未结束。而且由于信号量本身是一种临界资源(可回想一下上一节的锁, 35 | 其实也是一种临界资源),所以在 P 操作中,检查/修改信号量值以及可能发生的睡眠这一系列操作, 36 | 是一个不可分割的原子操作过程。通过原子操作才能保证,一旦 P 操作开始,则在该操作完成或阻塞睡眠之前, 37 | 其他线程均不允许访问该信号量。 38 | 39 | V 操作会对信号量的值加 1 ,然后检查是否有一个或多个线程在该信号量上睡眠等待。如有, 40 | 则选择其中的一个线程唤醒并允许该线程继续完成它的 P 操作;如没有,则直接返回。注意,信号量的值加 1, 41 | 并可能唤醒一个线程的一系列操作同样也是不可分割的原子操作过程。不会有某个进程因执行 V 操作而阻塞。 42 | 43 | 如果信号量是一个任意的整数,通常被称为计数信号量(Counting Semaphore),或一般信号量(General 44 | Semaphore);如果信号量只有0或1的取值,则称为二值信号量(Binary Semaphore)。可以看出, 45 | 互斥锁是信号量的一种特例 --- 二值信号量,信号量很好地解决了最多允许 N 个线程访问临界资源的情况。 46 | 47 | 信号量的一种实现伪代码如下所示: 48 | 49 | .. code-block:: rust 50 | :linenos: 51 | 52 | fn P(S) { 53 | if S >= 1 54 | S = S - 1; 55 | else 56 | ; 57 | } 58 | fn V(S) { 59 | if 60 | ; 61 | else 62 | S = S + 1; 63 | } 64 | 65 | 在上述实现中,S 的取值范围为大于等于 0 的整数。S 的初值一般设置为一个大于 0 的正整数, 66 | 表示可以进入临界区的线程数。当 S 取值为 1,表示是二值信号量,也就是互斥锁了。 67 | 使用信号量实现线程互斥访问临界区的伪代码如下: 68 | 69 | .. code-block:: rust 70 | :linenos: 71 | 72 | let static mut S: semaphore = 1; 73 | 74 | // Thread i 75 | fn foo() { 76 | ... 77 | P(S); 78 | execute Cricital Section; 79 | V(S); 80 | ... 81 | } 82 | 83 | 下面是另外一种信号量实现的伪代码: 84 | 85 | .. code-block:: rust 86 | :linenos: 87 | 88 | fn P(S) { 89 | S = S - 1; 90 | if S < 0 then 91 | ; 92 | } 93 | 94 | fn V(S) { 95 | S = S + 1; 96 | if 97 | ; 98 | } 99 | 100 | 在这种实现中,S 的初值一般设置为一个大于 0 的正整数,表示可以进入临界区的线程数。但 S 101 | 的取值范围可以是小于 0 的整数,表示等待进入临界区的睡眠线程数。 102 | 103 | 信号量的另一种用途是用于实现同步(synchronization)。比如,把信号量的初始值设置为 0 , 104 | 当一个线程 A 对此信号量执行一个 P 操作,那么该线程立即会被阻塞睡眠。之后有另外一个线程 B 105 | 对此信号量执行一个 V 操作,就会将线程 A 唤醒。这样线程 B 中执行 V 操作之前的代码序列 B-stmts 106 | 和线程 A 中执行 P 操作之后的代码 A-stmts 序列之间就形成了一种确定的同步执行关系,即线程 B 的 107 | B-stmts 会先执行,然后才是线程 A 的 A-stmts 开始执行。相关伪代码如下所示: 108 | 109 | .. code-block:: rust 110 | :linenos: 111 | 112 | let static mut S: semaphore = 0; 113 | 114 | //Thread A 115 | ... 116 | P(S); 117 | Label_2: 118 | A-stmts after Thread B::Label_1; 119 | ... 120 | 121 | //Thread B 122 | ... 123 | B-stmts before Thread A::Label_2; 124 | Label_1: 125 | V(S); 126 | ... 127 | 128 | 129 | 实现信号量 130 | ------------------------------------------ 131 | 132 | 使用 semaphore 系统调用 133 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 134 | 135 | 我们通过例子来看看如何实际使用信号量。下面是面向应用程序对信号量系统调用的简单使用, 136 | 可以看到对它的使用与上一节介绍的互斥锁系统调用类似。 137 | 138 | 在这个例子中,主线程先创建了信号量初值为 0 的信号量 ``SEM_SYNC`` ,然后再创建两个线程 First 139 | 和 Second 。线程 First 会先睡眠 10ms,而当线程 Second 执行时,会由于执行信号量的 P 140 | 操作而等待睡眠;当线程 First 醒来后,会执行 V 操作,从而能够唤醒线程 Second。这样线程 First 141 | 和线程 Second 就形成了一种稳定的同步关系。 142 | 143 | .. code-block:: rust 144 | :linenos: 145 | :emphasize-lines: 5,10,16,22,25,28 146 | 147 | const SEM_SYNC: usize = 0; //信号量ID 148 | unsafe fn first() -> ! { 149 | sleep(10); 150 | println!("First work and wakeup Second"); 151 | semaphore_up(SEM_SYNC); //信号量V操作 152 | exit(0) 153 | } 154 | unsafe fn second() -> ! { 155 | println!("Second want to continue,but need to wait first"); 156 | semaphore_down(SEM_SYNC); //信号量P操作 157 | println!("Second can work now"); 158 | exit(0) 159 | } 160 | pub fn main() -> i32 { 161 | // create semaphores 162 | assert_eq!(semaphore_create(0) as usize, SEM_SYNC); // 信号量初值为0 163 | // create first, second threads 164 | ... 165 | } 166 | 167 | pub fn sys_semaphore_create(res_count: usize) -> isize { 168 | syscall(SYSCALL_SEMAPHORE_CREATE, [res_count, 0, 0]) 169 | } 170 | pub fn sys_semaphore_up(sem_id: usize) -> isize { 171 | syscall(SYSCALL_SEMAPHORE_UP, [sem_id, 0, 0]) 172 | } 173 | pub fn sys_semaphore_down(sem_id: usize) -> isize { 174 | syscall(SYSCALL_SEMAPHORE_DOWN, [sem_id, 0, 0]) 175 | } 176 | 177 | 178 | - 第 16 行,创建了一个初值为 0 ,ID 为 ``SEM_SYNC`` 的信号量,对应的是第 22 行 179 | ``SYSCALL_SEMAPHORE_CREATE`` 系统调用; 180 | - 第 10 行,线程 Second 执行信号量 P 操作(对应第 28行 ``SYSCALL_SEMAPHORE_DOWN`` 181 | 系统调用),由于信号量初值为 0 ,该线程将阻塞; 182 | - 第 5 行,线程 First 执行信号量 V 操作(对应第 25 行 ``SYSCALL_SEMAPHORE_UP`` 系统调用), 183 | 会唤醒等待该信号量的线程 Second。 184 | 185 | 实现 semaphore 系统调用 186 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 187 | 188 | 操作系统如何实现信号量系统调用呢?我们还是采用通常的分析做法:数据结构+方法, 189 | 即首先考虑一下与此相关的核心数据结构,然后考虑与数据结构相关的相关函数/方法的实现。 190 | 191 | 在线程的眼里,信号量是一种每个线程能看到的共享资源,且在一个进程中,可以存在多个不同信号量资源, 192 | 所以我们可以把所有的信号量资源放在一起让进程来管理,如下面代码第 9 行所示。这里需要注意的是: 193 | ``semaphore_list: Vec>>`` 表示的是信号量资源的列表。而 ``Semaphore`` 194 | 是信号量的内核数据结构,由信号量值和等待队列组成。操作系统需要显式地施加某种控制,来确定当一个线程执行 195 | P 操作和 V 操作时,如何让线程睡眠或唤醒线程。在这里,P 操作是由 ``Semaphore`` 的 ``down`` 196 | 方法实现,而 V 操作是由 ``Semaphore`` 的 ``up`` 方法实现。 197 | 198 | .. code-block:: rust 199 | :linenos: 200 | :emphasize-lines: 9,16,17,34-36,44-47 201 | 202 | pub struct ProcessControlBlock { 203 | // immutable 204 | pub pid: PidHandle, 205 | // mutable 206 | inner: UPSafeCell, 207 | } 208 | pub struct ProcessControlBlockInner { 209 | ... 210 | pub semaphore_list: Vec>>, 211 | } 212 | 213 | pub struct Semaphore { 214 | pub inner: UPSafeCell, 215 | } 216 | pub struct SemaphoreInner { 217 | pub count: isize, 218 | pub wait_queue: VecDeque>, 219 | } 220 | impl Semaphore { 221 | pub fn new(res_count: usize) -> Self { 222 | Self { 223 | inner: unsafe { UPSafeCell::new( 224 | SemaphoreInner { 225 | count: res_count as isize, 226 | wait_queue: VecDeque::new(), 227 | } 228 | )}, 229 | } 230 | } 231 | 232 | pub fn up(&self) { 233 | let mut inner = self.inner.exclusive_access(); 234 | inner.count += 1; 235 | if inner.count <= 0 { 236 | if let Some(task) = inner.wait_queue.pop_front() { 237 | add_task(task); 238 | } 239 | } 240 | } 241 | 242 | pub fn down(&self) { 243 | let mut inner = self.inner.exclusive_access(); 244 | inner.count -= 1; 245 | if inner.count < 0 { 246 | inner.wait_queue.push_back(current_task().unwrap()); 247 | drop(inner); 248 | block_current_and_run_next(); 249 | } 250 | } 251 | } 252 | 253 | 254 | 首先是核心数据结构: 255 | 256 | - 第 9 行,进程控制块中管理的信号量列表。 257 | - 第 16-17 行,信号量的核心数据成员:信号量值和等待队列。 258 | 259 | 然后是重要的三个成员函数: 260 | 261 | - 第 20 行,创建信号量,信号量初值为参数 ``res_count`` 。 262 | - 第 31 行,实现 V 操作的 ``up`` 函数,第 34 行,当信号量值小于等于 0 时, 263 | 将从信号量的等待队列中弹出一个线程放入线程就绪队列。 264 | - 第 41 行,实现 P 操作的 ``down`` 函数,第 44 行,当信号量值小于 0 时, 265 | 将把当前线程放入信号量的等待队列,设置当前线程为挂起状态并选择新线程执行。 266 | 267 | 268 | Dijkstra, Edsger W. Cooperating sequential processes (EWD-123) (PDF). E.W. Dijkstra Archive. 269 | Center for American History, University of Texas at Austin. (transcription) (September 1965) 270 | https://www.cs.utexas.edu/users/EWD/transcriptions/EWD01xx/EWD123.html 271 | 272 | Downey, Allen B. (2016) [2005]. "The Little Book of Semaphores" (2nd ed.). Green Tea Press. 273 | 274 | Leppäjärvi, Jouni (May 11, 2008). "A pragmatic, historically oriented survey on the universality 275 | of synchronization primitives" (pdf). University of Oulu, Finland. -------------------------------------------------------------------------------- /source/chapter8/4condition-variable.rst: -------------------------------------------------------------------------------- 1 | 条件变量机制 2 | ========================================= 3 | 4 | 本节导读 5 | ----------------------------------------- 6 | 7 | 到目前为止,我们已经了解了操作系统提供的互斥锁和信号量。但应用程序在使用这两者时需要非常小心, 8 | 如果使用不当,就会产生效率低下、竞态条件、死锁或者其他一些不可预测的情况。为了简化编程、避免错误, 9 | 计算机科学家针对某些情况设计了一种更高层的同步互斥原语。具体而言,在有些情况下, 10 | 线程需要检查某一条件(condition)满足之后,才会继续执行。 11 | 12 | 我们来看一个例子,有两个线程 first 和 second 在运行,线程 first 会把全局变量 A 设置为 13 | 1,而线程 second 在 ``A != 0`` 的条件满足后,才能继续执行,如下面的伪代码所示: 14 | 15 | .. code-block:: rust 16 | :linenos: 17 | 18 | static mut A: usize = 0; 19 | unsafe fn first() -> ! { 20 | A=1; 21 | ... 22 | } 23 | 24 | unsafe fn second() -> ! { 25 | while A==0 { 26 | // 忙等或睡眠等待 A==1 27 | }; 28 | //继续执行相关事务 29 | } 30 | 31 | 在上面的例子中,如果线程 second 先执行,会忙等在 while 循环中,在操作系统的调度下,线程 32 | first 会执行并把 A 赋值为 1 后,然后线程 second 再次执行时,就会跳出 while 循环,进行接下来的工作。 33 | 配合互斥锁,可以正确完成上述带条件的同步流程,如下面的伪代码所示: 34 | 35 | .. code-block:: rust 36 | :linenos: 37 | 38 | static mut A: usize = 0; 39 | unsafe fn first() -> ! { 40 | mutex.lock(); 41 | A=1; 42 | mutex.unlock(); 43 | ... 44 | } 45 | 46 | unsafe fn second() -> ! { 47 | mutex.lock(); 48 | while A==0 { 49 | mutex.unlock(); 50 | // give other thread a chance to lock 51 | mutex.lock(); 52 | }; 53 | mutex.unlock(); 54 | //继续执行相关事务 55 | } 56 | 57 | 这种实现能执行,但效率低下,因为线程 second 会忙等检查,浪费处理器时间。我们希望有某种方式让线程 58 | second 休眠,直到等待的条件满足,再继续执行。于是,我们可以写出如下的代码: 59 | 60 | .. code-block:: rust 61 | :linenos: 62 | 63 | static mut A: usize = 0; 64 | unsafe fn first() -> ! { 65 | mutex.lock(); 66 | A=1; 67 | wakup(second); 68 | mutex.unlock(); 69 | ... 70 | } 71 | 72 | unsafe fn second() -> ! { 73 | mutex.lock(); 74 | while A==0 { 75 | wait(); 76 | }; 77 | mutex.unlock(); 78 | //继续执行相关事务 79 | } 80 | 81 | 粗略地看,这样就可以实现睡眠等待了。但请同学仔细想想,当线程 second 在睡眠的时候, ``mutex`` 82 | 是否已经上锁了? 确实,线程 second 是带着上锁的 ``mutex`` 进入等待睡眠状态的。 83 | 如果这两个线程的调度顺序是先执行线程 second,再执行线程first,那么线程 second 会先睡眠且拥有 84 | ``mutex`` 的锁;当线程 first 执行时,会由于没有 ``mutex`` 的锁而进入等待锁的睡眠状态。 85 | 结果就是两个线程都睡了,都执行不下去,这就出现了 **死锁** 。 86 | 87 | 这里需要解决的两个关键问题: **如何等待一个条件?** 和 **在条件为真时如何向等待线程发出信号** 。 88 | 我们的计算机科学家给出了 **管程(Monitor)** 和 **条件变量(Condition Variables)** 89 | 这种巧妙的方法。接下来,我们就会深入讲解条件变量的设计与实现。 90 | 91 | 条件变量的基本思路 92 | ------------------------------------------- 93 | 94 | 管程有一个很重要的特性,即任一时刻只能有一个活跃线程调用管程中的过程, 95 | 这一特性使线程在调用执行管程中过程时能保证互斥,这样线程就可以放心地访问共享变量。 96 | 管程是编程语言的组成部分,编译器知道其特殊性,因此可以采用与其他过程调用不同的方法来处理对管程的调用. 97 | 因为是由编译器而非程序员来生成互斥相关的代码,所以出错的可能性要小。 98 | 99 | 管程虽然借助编译器提供了一种实现互斥的简便途径,但这还不够,还需要一种线程间的沟通机制。 100 | 首先是等待机制:由于线程在调用管程中某个过程时,发现某个条件不满足,那就在无法继续运行而被阻塞。 101 | 其次是唤醒机制:另外一个线程可以在调用管程的过程中,把某个条件设置为真,并且还需要有一种机制, 102 | 及时唤醒等待条件为真的阻塞线程。为了避免管程中同时有两个活跃线程, 103 | 我们需要一定的规则来约定线程发出唤醒操作的行为。目前有三种典型的规则方案: 104 | 105 | - Hoare 语义:线程发出唤醒操作后,马上阻塞自己,让新被唤醒的线程运行。注:此时唤醒线程的执行位置还在管程中。 106 | - Hansen 语义:是执行唤醒操作的线程必须立即退出管程,即唤醒操作只可能作为一个管程过程的最后一条语句。 107 | 注:此时唤醒线程的执行位置离开了管程。 108 | - Mesa 语义:唤醒线程在发出行唤醒操作后继续运行,并且只有它退出管程之后,才允许等待的线程开始运行。 109 | 注:此时唤醒线程的执行位置还在管程中。 110 | 111 | 下面介绍一个基于 Mesa 语义的沟通机制。这种沟通机制的具体实现就是 112 | **条件变量** 和对应的操作:wait 和 signal。线程使用条件变量来等待一个条件变成真。 113 | 条件变量其实是一个线程等待队列,当条件不满足时,线程通过执行条件变量的 wait 114 | 操作就可以把自己加入到等待队列中,睡眠等待(waiting)该条件。另外某个线程,当它改变条件为真后, 115 | 就可以通过条件变量的 signal 操作来唤醒一个或者多个等待的线程(通过在该条件上发信号),让它们继续执行。 116 | 117 | 早期提出的管程是基于 Concurrent Pascal 来设计的,其他语言如 C 和 Rust 等,并没有在语言上支持这种机制。 118 | 我们还是可以用手动加入互斥锁的方式来代替编译器,就可以在 C 和 Rust 的基础上实现原始的管程机制了。 119 | 在目前的 C 语言应用开发中,实际上也是这么做的。这样,我们就可以用互斥锁和条件变量, 120 | 来重现上述的同步互斥例子: 121 | 122 | .. code-block:: rust 123 | :linenos: 124 | 125 | static mut A: usize = 0; 126 | unsafe fn first() -> ! { 127 | mutex.lock(); 128 | A=1; 129 | condvar.wakup(); 130 | mutex.unlock(); 131 | ... 132 | } 133 | 134 | unsafe fn second() -> ! { 135 | mutex.lock(); 136 | while A==0 { 137 | condvar.wait(mutex); //在睡眠等待之前,需要释放mutex 138 | }; 139 | mutex.unlock(); 140 | //继续执行相关事务 141 | } 142 | 143 | 有了上面的介绍,我们就可以实现条件变量的基本逻辑了。下面是条件变量的 wait 和 signal 操作的伪代码: 144 | 145 | .. code-block:: rust 146 | :linenos: 147 | 148 | fn wait(mutex) { 149 | mutex.unlock(); 150 | ; 151 | mutex.lock(); 152 | } 153 | 154 | fn signal() { 155 | ; 156 | } 157 | 158 | 条件变量的wait操作包含三步,1. 释放锁;2. 把自己挂起;3. 被唤醒后,再获取锁。条件变量的 signal 159 | 操作只包含一步:找到挂在条件变量上睡眠的线程,把它唤醒。 160 | 161 | 注意,条件变量不像信号量那样有一个整型计数值的成员变量,所以条件变量也不能像信号量那样有读写计数值的能力。 162 | 如果一个线程向一个条件变量发送唤醒操作,但是在该条件变量上并没有等待的线程,则唤醒操作实际上什么也没做。 163 | 164 | 实现条件变量 165 | ------------------------------------------- 166 | 167 | 使用 condvar 系统调用 168 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 169 | 170 | 我们通过例子来看看如何实际使用条件变量。下面是面向应用程序对条件变量系统调用的简单使用, 171 | 可以看到对它的使用与上一节介绍的信号量系统调用类似。 在这个例子中,主线程先创建了初值为 1 172 | 的互斥锁和一个条件变量,然后再创建两个线程 First 和 Second。线程 First 会先睡眠 10ms,而当线程 173 | Second 执行时,会由于条件不满足执行条件变量的 wait 操作而等待睡眠;当线程 First 醒来后,通过设置 174 | A 为 1,让线程 second 等待的条件满足,然后会执行条件变量的 signal 操作,从而能够唤醒线程 Second。 175 | 这样线程 First 和线程 Second 就形成了一种稳定的同步与互斥关系。 176 | 177 | .. code-block:: rust 178 | :linenos: 179 | :emphasize-lines: 11,19,26,33,36,39 180 | 181 | static mut A: usize = 0; //全局变量 182 | 183 | const CONDVAR_ID: usize = 0; 184 | const MUTEX_ID: usize = 0; 185 | 186 | unsafe fn first() -> ! { 187 | sleep(10); 188 | println!("First work, Change A --> 1 and wakeup Second"); 189 | mutex_lock(MUTEX_ID); 190 | A=1; 191 | condvar_signal(CONDVAR_ID); 192 | mutex_unlock(MUTEX_ID); 193 | ... 194 | } 195 | unsafe fn second() -> ! { 196 | println!("Second want to continue,but need to wait A=1"); 197 | mutex_lock(MUTEX_ID); 198 | while A==0 { 199 | condvar_wait(CONDVAR_ID, MUTEX_ID); 200 | } 201 | mutex_unlock(MUTEX_ID); 202 | ... 203 | } 204 | pub fn main() -> i32 { 205 | // create condvar & mutex 206 | assert_eq!(condvar_create() as usize, CONDVAR_ID); 207 | assert_eq!(mutex_blocking_create() as usize, MUTEX_ID); 208 | // create first, second threads 209 | ... 210 | } 211 | 212 | pub fn condvar_create() -> isize { 213 | sys_condvar_create(0) 214 | } 215 | pub fn condvar_signal(condvar_id: usize) { 216 | sys_condvar_signal(condvar_id); 217 | } 218 | pub fn condvar_wait(condvar_id: usize, mutex_id: usize) { 219 | sys_condvar_wait(condvar_id, mutex_id); 220 | } 221 | 222 | - 第 26 行,创建了一个 ID 为 ``CONDVAR_ID`` 的条件量,对应第 33 行 ``SYSCALL_CONDVAR_CREATE`` 系统调用; 223 | - 第 19 行,线程 Second 执行条件变量 ``wait`` 操作(对应第 39 行 ``SYSCALL_CONDVAR_WAIT`` 系统调用), 224 | 该线程将释放 ``mutex`` 锁并阻塞; 225 | - 第 5 行,线程 First 执行条件变量 ``signal`` 操作(对应第 36 行 ``SYSCALL_CONDVAR_SIGNAL`` 系统调用), 226 | 会唤醒等待该条件变量的线程 Second。 227 | 228 | 229 | 实现 condvar 系统调用 230 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 231 | 232 | 操作系统如何实现条件变量系统调用呢?在线程的眼里,条件变量是一种每个线程能看到的共享资源, 233 | 且在一个进程中,可以存在多个不同条件变量资源,所以我们可以把所有的条件变量资源放在一起让进程来管理, 234 | 如下面代码第9行所示。这里需要注意的是: ``condvar_list: Vec>>`` 235 | 表示的是条件变量资源的列表。而 ``Condvar`` 是条件变量的内核数据结构,由等待队列组成。 236 | 操作系统需要显式地施加某种控制,来确定当一个线程执行 ``wait`` 操作和 ``signal`` 操作时, 237 | 如何让线程睡眠或唤醒线程。在这里, ``wait`` 操作是由 ``Condvar`` 的 ``wait`` 方法实现,而 ``signal`` 238 | 操作是由 ``Condvar`` 的 ``signal`` 方法实现。 239 | 240 | .. code-block:: rust 241 | :linenos: 242 | :emphasize-lines: 9,15,18,27,33 243 | 244 | pub struct ProcessControlBlock { 245 | // immutable 246 | pub pid: PidHandle, 247 | // mutable 248 | inner: UPSafeCell, 249 | } 250 | pub struct ProcessControlBlockInner { 251 | ... 252 | pub condvar_list: Vec>>, 253 | } 254 | pub struct Condvar { 255 | pub inner: UPSafeCell, 256 | } 257 | pub struct CondvarInner { 258 | pub wait_queue: VecDeque>, 259 | } 260 | impl Condvar { 261 | pub fn new() -> Self { 262 | Self { 263 | inner: unsafe { UPSafeCell::new( 264 | CondvarInner { 265 | wait_queue: VecDeque::new(), 266 | } 267 | )}, 268 | } 269 | } 270 | pub fn signal(&self) { 271 | let mut inner = self.inner.exclusive_access(); 272 | if let Some(task) = inner.wait_queue.pop_front() { 273 | wakeup_task(task); 274 | } 275 | } 276 | pub fn wait(&self, mutex:Arc) { 277 | mutex.unlock(); 278 | let mut inner = self.inner.exclusive_access(); 279 | inner.wait_queue.push_back(current_task().unwrap()); 280 | drop(inner); 281 | block_current_and_run_next(); 282 | mutex.lock(); 283 | } 284 | } 285 | 286 | 首先是核心数据结构: 287 | 288 | - 第 9 行,进程控制块中管理的条件变量列表。 289 | - 第 15 行,条件变量的核心数据成员:等待队列。 290 | 291 | 然后是重要的三个成员函数: 292 | 293 | - 第 18 行,创建条件变量,即创建了一个空的等待队列。 294 | - 第 27 行,实现 ``signal`` 操作,将从条件变量的等待队列中弹出一个线程放入线程就绪队列。 295 | - 第 33 行,实现 ``wait`` 操作,释放 ``mutex`` 互斥锁,将把当前线程放入条件变量的等待队列, 296 | 设置当前线程为挂起状态并选择新线程执行。在恢复执行后,再加上 ``mutex`` 互斥锁。 297 | 298 | Hansen, Per Brinch (1993). "Monitors and concurrent Pascal: a personal history". HOPL-II: 299 | The second ACM SIGPLAN conference on History of programming languages. History of Programming 300 | Languages. New York, NY, USA: ACM. pp. 1–35. doi:10.1145/155360.155361. ISBN 0-89791-570-4. -------------------------------------------------------------------------------- /source/chapter8/5exercise.rst: -------------------------------------------------------------------------------- 1 | chapter8 练习 2 | ======================================= 3 | 4 | 编程作业 5 | -------------------------------------- 6 | 7 | .. warning:: 8 | 9 | 本次实验框架变动较大,且改动较为复杂,为降低同学们的工作量,本次实验不要求合并之前的实验内容, 10 | 只需通过 ch8 的全部测例和其他章节的基础测例即可。你可以直接在实验框架的 ch8 分支上完成以下作业。 11 | 12 | .. note:: 13 | 14 | 本次实验的工作量约为 100 行代码。 15 | 16 | 17 | 死锁检测 18 | +++++++++++++++++++++++++++++++ 19 | 20 | 目前的 mutex 和 semaphore 相关的系统调用不会分析资源的依赖情况,用户程序可能出现死锁。 21 | 我们希望在系统中加入死锁检测机制,当发现可能发生死锁时拒绝对应的资源获取请求。 22 | 一种检测死锁的算法如下: 23 | 24 | 定义如下三个数据结构: 25 | 26 | - 可利用资源向量 Available :含有 m 个元素的一维数组,每个元素代表可利用的某一类资源的数目, 27 | 其初值是该类资源的全部可用数目,其值随该类资源的分配和回收而动态地改变。 28 | Available[j] = k,表示第 j 类资源的可用数量为 k。 29 | - 分配矩阵 Allocation:n * m 矩阵,表示每类资源已分配给每个线程的资源数。 30 | Allocation[i,j] = g,则表示线程 i 当前己分得第 j 类资源的数量为 g。 31 | - 需求矩阵 Need:n * m 的矩阵,表示每个线程还需要的各类资源数量。 32 | Need[i,j] = d,则表示线程 i 还需要第 j 类资源的数量为 d 。 33 | 34 | 算法运行过程如下: 35 | 36 | 1. 设置两个向量: 工作向量 Work,表示操作系统可提供给线程继续运行所需的各类资源数目,它含有 37 | m 个元素。初始时,Work = Available ;结束向量 Finish,表示系统是否有足够的资源分配给线程, 38 | 使之运行完成。初始时 Finish[0..n-1] = false,表示所有线程都没结束;当有足够资源分配给线程时, 39 | 设置 Finish[i] = true。 40 | 2. 从线程集合中找到一个能满足下述条件的线程 41 | 42 | .. code-block:: Rust 43 | :linenos: 44 | 45 | Finish[i] == false; 46 | Need[i,j] ≤ Work[j]; 47 | 48 | 若找到,执行步骤 3,否则执行步骤 4。 49 | 50 | 3. 当线程 thr[i] 获得资源后,可顺利执行,直至完成,并释放出分配给它的资源,故应执行: 51 | 52 | .. code-block:: Rust 53 | :linenos: 54 | 55 | Work[j] = Work[j] + Allocation[i, j]; 56 | Finish[i] = true; 57 | 58 | 跳转回步骤2 59 | 60 | 4. 如果 Finish[0..n-1] 都为 true,则表示系统处于安全状态;否则表示系统处于不安全状态,即出现死锁。 61 | 62 | 出于兼容性和灵活性考虑,我们允许进程按需开启或关闭死锁检测功能。为此我们将实现一个新的系统调用: 63 | ``sys_enable_deadlock_detect`` 。 64 | 65 | **enable_deadlock_detect**: 66 | 67 | * syscall ID: 469 68 | * 功能:为当前进程启用或禁用死锁检测功能。 69 | * C 接口: ``int enable_deadlock_detect(int is_enable)`` 70 | * Rust 接口: ``fn enable_deadlock_detect(is_enable: i32) -> i32`` 71 | * 参数: 72 | * is_enable: 为 1 表示启用死锁检测, 0 表示禁用死锁检测。 73 | * 说明: 74 | * 开启死锁检测功能后, ``mutex_lock`` 和 ``semaphore_down`` 如果检测到死锁, 75 | 应拒绝相应操作并返回 -0xDEAD (十六进制值)。 76 | * 简便起见可对 mutex 和 semaphore 分别进行检测,无需考虑二者 (以及 ``waittid`` 等) 77 | 混合使用导致的死锁。 78 | * 返回值:如果出现了错误则返回 -1,否则返回 0。 79 | * 可能的错误 80 | * 参数不合法 81 | * 死锁检测开启失败 82 | 83 | 84 | 实验要求 85 | +++++++++++++++++++++++++++++++++++++++++ 86 | 87 | - 完成分支: ch8。 88 | - 实验目录要求不变。 89 | - 通过所有测例。 90 | 91 | 问答作业 92 | -------------------------------------------- 93 | 94 | 1. 在我们的多线程实现中,当主线程 (即 0 号线程) 退出时,视为整个进程退出, 95 | 此时需要结束该进程管理的所有线程并回收其资源。 96 | - 需要回收的资源有哪些? 97 | - 其他线程的 TaskControlBlock 可能在哪些位置被引用,分别是否需要回收,为什么? 98 | 2. 对比以下两种 ``Mutex.unlock`` 的实现,二者有什么区别?这些区别可能会导致什么问题? 99 | 100 | .. code-block:: Rust 101 | :linenos: 102 | 103 | impl Mutex for Mutex1 { 104 | fn unlock(&self) { 105 | let mut mutex_inner = self.inner.exclusive_access(); 106 | assert!(mutex_inner.locked); 107 | mutex_inner.locked = false; 108 | if let Some(waking_task) = mutex_inner.wait_queue.pop_front() { 109 | add_task(waking_task); 110 | } 111 | } 112 | } 113 | 114 | impl Mutex for Mutex2 { 115 | fn unlock(&self) { 116 | let mut mutex_inner = self.inner.exclusive_access(); 117 | assert!(mutex_inner.locked); 118 | if let Some(waking_task) = mutex_inner.wait_queue.pop_front() { 119 | add_task(waking_task); 120 | } else { 121 | mutex_inner.locked = false; 122 | } 123 | } 124 | } 125 | 126 | 127 | 报告要求 128 | ------------------------------- 129 | 130 | - 简单总结你实现的功能(200字以内,不要贴代码)及你完成本次实验所用的时间。 131 | - 完成问答题。 132 | - 加入 :doc:`/honorcode` 的内容。否则,你的提交将视作无效,本次实验的成绩将按“0”分计。 133 | - 推荐markdown文档格式。 134 | - (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 135 | 136 | 137 | 选作题目 138 | -------------------------------------------------------- 139 | 140 | 选做题目列表 141 | 142 | - (7分)基于多核的OS内核线程支持,内核支持抢占,支持多核方式下的同步互斥 143 | - (7分)提升多核的OS内核性能,实现内核中的并行性能优化(fs中的缓冲区管理并行化, 物理内存分配的并行化) 144 | - (7分)更通用的内核+应用的死锁检查(参考Linux的动态内核死锁检测方法) 145 | 146 | 提交要求 147 | 148 | - (占分比:40%)实现代码(包括基本的注释) 149 | - (占分比:50%)设计与功能/性能测试分析文档,测试用例。 150 | - (占分比:10%)鼓励形成可脱离OS独立存在的库,可以裸机测试或在用户态测试(比如easyfs那样) -------------------------------------------------------------------------------- /source/chapter8/index.rst: -------------------------------------------------------------------------------- 1 | 第八章:并发 2 | ============================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | 0intro 8 | 1thread-kernel 9 | 2lock 10 | 3semaphore 11 | 4condition-variable 12 | 5exercise 13 | 14 | .. chyyuu 15 | 扩展章节,添加其他类型同步互斥的介绍 -------------------------------------------------------------------------------- /source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'rCore-Tutorial-Guide-2023S' 21 | copyright = 'OS2023Spring' 22 | author = 'Yifan Wu' 23 | language = 'zh_CN' 24 | html_search_language = 'zh' 25 | 26 | # The full version, including alpha/beta/rc tags 27 | # release = '0.1' 28 | 29 | 30 | # -- General configuration --------------------------------------------------- 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | "sphinx_comments", 37 | # "sphinx_tabs.tabs" 38 | ] 39 | 40 | comments_config = { 41 | "utterances": { 42 | "repo": "LearningOS/rCore-Tutorial-Guide-2023S", 43 | "issue-term": "pathname", 44 | "label": "comments", 45 | "theme": "github-light", 46 | "crossorigin": "anonymous", 47 | } 48 | } 49 | 50 | # Add any paths that contain templates here, relative to this directory. 51 | templates_path = ['_templates'] 52 | 53 | # List of patterns, relative to source directory, that match files and 54 | # directories to ignore when looking for source files. 55 | # This pattern also affects html_static_path and html_extra_path. 56 | exclude_patterns = [] 57 | 58 | 59 | # -- Options for HTML output ------------------------------------------------- 60 | 61 | # The theme to use for HTML and HTML Help pages. See the documentation for 62 | # a list of builtin themes. 63 | # 64 | html_theme = 'furo' 65 | 66 | # Add any paths that contain custom static files (such as style sheets) here, 67 | # relative to this directory. They are copied after the builtin static files, 68 | # so a file named "default.css" will overwrite the builtin "default.css". 69 | html_static_path = ['_static'] 70 | 71 | html_css_files = [ 72 | 'my_style.css', 73 | #'dracula.css', 74 | ] 75 | 76 | from pygments.lexer import RegexLexer 77 | from pygments import token 78 | from sphinx.highlighting import lexers 79 | 80 | class RVLexer(RegexLexer): 81 | name = 'riscv' 82 | tokens = { 83 | 'root': [ 84 | # Comment 85 | (r'#.*\n', token.Comment), 86 | # General Registers 87 | (r'\b(?:x[1-2]?[0-9]|x30|x31|zero|ra|sp|gp|tp|fp|t[0-6]|s[0-9]|s1[0-1]|a[0-7]|pc)\b', token.Name.Attribute), 88 | # CSRs 89 | (r'\bs(?:status|tvec|ip|ie|counteren|scratch|epc|cause|tval|atp|)\b', token.Name.Constant), 90 | (r'\bm(?:isa|vendorid|archid|hardid|status|tvec|ideleg|ip|ie|counteren|scratch|epc|cause|tval)\b', token.Name.Constant), 91 | # Instructions 92 | (r'\b(?:(addi?w?)|(slti?u?)|(?:and|or|xor)i?|(?:sll|srl|sra)i?w?|lui|auipc|subw?|jal|jalr|beq|bne|bltu?|bgeu?|s[bhwd]|(l[bhw]u?)|ld)\b', token.Name.Decorator), 93 | (r'\b(?:csrr?[rws]i?)\b', token.Name.Decorator), 94 | (r'\b(?:ecall|ebreak|[msu]ret|wfi|sfence.vma)\b', token.Name.Decorator), 95 | (r'\b(?:nop|li|la|mv|not|neg|negw|sext.w|seqz|snez|sltz|sgtz|f(?:mv|abs|neg).(?:s|d)|b(?:eq|ne|le|ge|lt)z|bgt|ble|bgtu|bleu|j|jr|ret|call)\b', token.Name.Decorator), 96 | (r'(?:%hi|%lo|%pcrel_hi|%pcrel_lo|%tprel_(?:hi|lo|add))', token.Name.Decorator), 97 | # Directives 98 | (r'(?:.2byte|.4byte|.8byte|.quad|.half|.word|.dword|.byte|.dtpreldword|.dtprelword|.sleb128|.uleb128|.asciz|.string|.incbin|.zero)', token.Name.Function), 99 | (r'(?:.align|.balign|.p2align)', token.Name.Function), 100 | (r'(?:.globl|.local|.equ)', token.Name.Function), 101 | (r'(?:.text|.data|.rodata|.bss|.comm|.common|.section)', token.Name.Function), 102 | (r'(?:.option|.macro|.endm|.file|.ident|.size|.type)', token.Name.Function), 103 | (r'(?:.set|.rept|.endr|.macro|.endm|.altmacro)', token.Name.Function), 104 | # Number 105 | (r'\b(?:(?:0x|)[\da-f]+|(?:0o|)[0-7]+|\d+)\b', token.Number), 106 | # Labels 107 | (r'\S+:', token.Name.Builtin), 108 | # Whitespace 109 | (r'\s', token.Whitespace), 110 | # Other operators 111 | (r'[,\+\*\-\(\)\\%]', token.Text), 112 | # Hacks 113 | (r'(?:SAVE_GP|trap_handler|__switch|LOAD_GP|SAVE_SN|LOAD_SN|__alltraps|__restore)', token.Name.Builtin), 114 | (r'(?:.trampoline)', token.Name.Function), 115 | (r'(?:n)', token.Name.Entity), 116 | (r'(?:x)', token.Text), 117 | ], 118 | } 119 | 120 | lexers['riscv'] = RVLexer() 121 | -------------------------------------------------------------------------------- /source/honorcode.rst: -------------------------------------------------------------------------------- 1 | 2 | **荣誉准则** 3 | ---------------- 4 | .. warning:: 5 | 6 | 请把填写了《你的说明》的下述内容拷贝到的到实验报告中。 7 | 否则,你的提交将视作无效,本次实验的成绩将按“0”分计。 8 | 9 | 1. 在完成本次实验的过程(含此前学习的过程)中,我曾分别与 **以下各位** 就(与本次实验相关的)以下方面做过交流,还在代码中对应的位置以注释形式记录了具体的交流对象及内容: 10 | 11 | *《你交流的对象说明》* 12 | 13 | 2. 此外,我也参考了 **以下资料** ,还在代码中对应的位置以注释形式记录了具体的参考来源及内容: 14 | 15 | *《你参考的资料说明》* 16 | 17 | 3. 我独立完成了本次实验除以上方面之外的所有工作,包括代码与文档。 18 | 我清楚地知道,从以上方面获得的信息在一定程度上降低了实验难度,可能会影响起评分。 19 | 20 | 4. 我从未使用过他人的代码,不管是原封不动地复制,还是经过了某些等价转换。 21 | 我未曾也不会向他人(含此后各届同学)复制或公开我的实验代码,我有义务妥善保管好它们。 22 | 我提交至本实验的评测系统的代码,均无意于破坏或妨碍任何计算机系统的正常运转。 23 | 我清楚地知道,以上情况均为本课程纪律所禁止,若违反,对应的实验成绩将按“-100”分计。 -------------------------------------------------------------------------------- /source/index.rst: -------------------------------------------------------------------------------- 1 | .. rCore-Tutorial-Guide-2023S documentation master file, created by 2 | sphinx-quickstart on Thu Oct 29 22:25:54 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | rCore-Tutorial-Guide 2023 春季学期 7 | ================================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: 正文 12 | :hidden: 13 | 14 | 0setup-devel-env 15 | chapter1/index 16 | chapter2/index 17 | chapter3/index 18 | chapter4/index 19 | chapter5/index 20 | chapter6/index 21 | chapter7/index 22 | chapter8/index 23 | 24 | .. toctree:: 25 | :maxdepth: 2 26 | :caption: 附录 27 | :hidden: 28 | 29 | appendix-a/index 30 | appendix-b/index 31 | appendix-c/index 32 | appendix-d/index 33 | 34 | .. toctree:: 35 | :maxdepth: 2 36 | :caption: 开发注记 37 | :hidden: 38 | 39 | setup-sphinx 40 | rest-example 41 | 42 | 43 | 项目简介 44 | --------------------- 45 | 46 | 本教程展示了如何 **从零开始** 用 **Rust** 语言写一个基于 **RISC-V** 架构的 **类 Unix 内核** 。 47 | 48 | 用于 2023 年春季学期操作系统课堂教学。 49 | 50 | 导读 51 | --------------------- 52 | 53 | 请先阅读 :doc:`0setup-devel-env` 完成环境配置。 54 | 55 | 以下是读者为了完成实验需掌握的技术,你可以在实操中熟悉它们。 56 | 57 | - 阅读简单的 Makefile 文件; 58 | - 阅读简单的 RISC-V 汇编代码; 59 | - git 的基本功能,解决 git merge 冲突的办法; 60 | - Rust 基本语法和一些进阶语法,包括 **Cargo 项目结构、Trait、函数式编程、Unsafe Rust、错误处理等** 。 61 | 62 | 鸣谢 63 | ---------------------- 64 | 本项目基于2022 年春秋季学期操作系统实验指导书 ,重构的目标是在保留结构的基础上屏蔽不必要的细节,缩短篇幅,优化语言,降低阅读成本。 65 | 66 | 如果你觉得本教程某些章节不够细致或不够连贯,可以参考上学期实验指导书的对应章节。 67 | 68 | .. note:: 69 | 70 | 这是一个注解,以这种方式出现的卡片提供了非必要的背景知识,你可以选择忽略。 71 | 72 | 73 | .. attention:: 74 | 75 | 虽然实验本身在总评中占比有限,但根据往届经验,考试中可能大量出现与编程作业、思考题、代码实现思路直接相关的题目。 76 | 77 | 78 | 项目协作 79 | ---------------------- 80 | 81 | - :doc:`/setup-sphinx` 介绍了如何基于 Sphinx 框架配置文档开发环境,之后可以本地构建并渲染 html 或其他格式的文档; 82 | - :doc:`/rest-example` 给出了目前编写文档才用的 ReStructuredText 标记语言的一些基础语法及用例; 83 | - 时间仓促,本项目还有很多不完善之处,欢迎大家积极在每一个章节的评论区留言,或者提交 Issues 或 Pull Requests,让我们 84 | 一起努力让这本书变得更好! 85 | 86 | -------------------------------------------------------------------------------- /source/pygments-coloring.txt: -------------------------------------------------------------------------------- 1 | Pygments 默认配色: 2 | Keyword.Constant 深绿加粗 3 | Keyword.Declaration 深绿加粗 4 | Keyword.Namespace 深绿加粗 5 | Keyword.Pseudo 浅绿 6 | Keyword.Reserved 深绿加粗 7 | Keyword.Type 樱桃红 8 | Name.Attribute 棕黄 9 | Name.Builtin 浅绿 10 | Name.Builtin.Pseudo 浅绿 11 | Name.Class 深蓝加粗 12 | Name.Constant 棕红 13 | Name.Decorator 浅紫 14 | Name.Entity 灰色 15 | Name.Exception 深红 16 | Name.Function 深蓝 17 | Name.Function.Magic 深蓝 18 | Name.Label 棕黄 19 | Name.Namespace 深蓝加粗 20 | Name.Other 默认黑色 21 | Name.Tag 深绿加粗 22 | Name.Variable 蓝黑 23 | 24 | 25 | 通用寄存器 -> 棕黄 Name.Attribute 26 | CSR -> 棕红 Name.Constant 27 | 指令 -> 浅紫 Name.Decorator 28 | 伪指令 -> 樱桃红 Keyword.Type 29 | Directives -> 深蓝 Name.Function 30 | 标签/剩余字面量 -> 浅绿 Name.Builtin 31 | 数字 -> Number 32 | 33 | -------------------------------------------------------------------------------- /source/rest-example.rst: -------------------------------------------------------------------------------- 1 | reStructuredText 基本语法 2 | ===================================================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | .. note:: 9 | 下面是一个注记。 10 | 11 | `这里 `_ 给出了在 Sphinx 中 12 | 外部链接的引入方法。注意,链接的名字和用一对尖括号包裹起来的链接地址之间必须有一个空格。链接最后的下划线和片段的后续内容之间也需要 13 | 有一个空格。 14 | 15 | 接下来是一个文档内部引用的例子。比如,戳 :doc:`chapter0/5setup-devel-env` 可以进入快速上手环节。 16 | 17 | .. warning:: 18 | 19 | 下面是一个警告。 20 | 21 | .. code-block:: rust 22 | :linenos: 23 | :caption: 一段示例 Rust 代码 24 | 25 | // 我们甚至可以插入一段 Rust 代码! 26 | fn add(a: i32, b: i32) -> i32 { a + b } 27 | 28 | 下面继续我们的警告。 29 | 30 | .. attention:: Here is an attention. 31 | 32 | .. caution:: please be cautious! 33 | 34 | .. error:: 35 | 36 | 下面是一个错误。 37 | 38 | .. danger:: it is dangerous! 39 | 40 | 41 | .. tip:: here is a tip 42 | 43 | .. important:: this is important! 44 | 45 | .. hint:: this is a hint. 46 | 47 | 48 | 49 | 这里是一行数学公式 :math:`\sin(\alpha+\beta)=\sin\alpha\cos\beta+\cos\alpha\sin\beta`。 50 | 51 | 基本的文本样式:这是 *斜体* ,这是 **加粗** ,接下来的则是行间公式 ``a0`` 。它们的前后都需要有一个空格隔开其他内容,这个让人挺不爽的... 52 | 53 | `这是 `_ 一个全面展示 54 | 章节分布的例子,来自于 ReadTheDocs 的官方文档。事实上,现在我们也采用 ReadTheDocs 主题了,它非常美观大方。 55 | 56 | 下面是一个测试 gif。 57 | 58 | .. image:: resources/test.gif 59 | 60 | 接下来是一个表格的例子。 61 | 62 | .. list-table:: RISC-V 函数调用跳转指令 63 | :widths: 20 30 64 | :header-rows: 1 65 | :align: center 66 | 67 | * - 指令 68 | - 指令功能 69 | * - :math:`\text{jal}\ \text{rd},\ \text{imm}[20:1]` 70 | - :math:`\text{rd}\leftarrow\text{pc}+4` 71 | 72 | :math:`\text{pc}\leftarrow\text{pc}+\text{imm}` 73 | * - :math:`\text{jalr}\ \text{rd},\ (\text{imm}[11:0])\text{rs}` 74 | - :math:`\text{rd}\leftarrow\text{pc}+4` 75 | 76 | :math:`\text{pc}\leftarrow\text{rs}+\text{imm}` -------------------------------------------------------------------------------- /source/setup-sphinx.rst: -------------------------------------------------------------------------------- 1 | 修改和构建本项目 2 | ==================================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | TL;DR: ``python -m venv .venv`` 创建一个虚拟环境(你也可以使用 conda 等工具),activate 后 ``pip install -r requirements.txt``。 9 | 10 | 1. 参考 `这里 `_ 安装 Sphinx。 11 | 2. ``pip install sphinx_rtd_theme`` 安装 Read The Docs 主题。 12 | 3. ``pip install jieba`` 安装中文分词。 13 | 4. ``pip install sphinx-comments`` 安装 Sphinx 讨论区插件。 14 | 5. :doc:`/rest-example` 是 ReST 的一些基本语法,也可以参考已完成的文档。 15 | 6. 修改之后,在项目根目录下 ``make clean && make html`` 即可在 ``build/html/index.html`` 查看本地构建的主页。请注意在修改章节目录结构之后需要 ``make clean`` 一下,不然可能无法正常更新。 16 | 7. 确认修改无误之后,将更改提交到自己的仓库,然后向项目仓库提交 Pull Request。如有问题,可直接提交 Issue 或课程微信群内联系助教。 17 | --------------------------------------------------------------------------------