├── .github ├── ISSUE_TEMPLATE │ ├── 0-----.md │ ├── 1-bug---.md │ └── 2-----.md └── workflows │ ├── deploy.yml │ └── main.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── SUMMARY.md ├── book.json ├── deploy.sh ├── docs ├── format │ ├── code.md │ ├── doc.md │ └── partition.md ├── lab-0 │ ├── guide │ │ ├── intro.md │ │ ├── part-1.md │ │ ├── part-2.md │ │ ├── part-3.md │ │ ├── part-4.md │ │ ├── part-5.md │ │ ├── part-6.md │ │ ├── part-7.md │ │ ├── part-8.md │ │ ├── part-9.md │ │ └── summary.md │ └── pics │ │ ├── painter.key │ │ └── typical-layout.png ├── lab-1 │ ├── guide │ │ ├── intro.md │ │ ├── part-1.md │ │ ├── part-2.md │ │ ├── part-3.md │ │ ├── part-4.md │ │ ├── part-5.md │ │ ├── part-6.md │ │ └── summary.md │ └── practice.md ├── lab-2 │ ├── guide │ │ ├── intro.md │ │ ├── part-1.md │ │ ├── part-2.md │ │ ├── part-3.md │ │ └── summary.md │ └── practice.md ├── lab-3 │ ├── guide │ │ ├── intro.md │ │ ├── part-1.md │ │ ├── part-2.md │ │ ├── part-3.md │ │ ├── part-4.md │ │ ├── part-5.md │ │ └── summary.md │ ├── pics │ │ ├── rcore_memory_layout.png │ │ ├── sv39_address.png │ │ ├── sv39_pagetable.jpg │ │ ├── sv39_pte.jpg │ │ ├── sv39_rwx.jpg │ │ └── sv39_satp.jpg │ └── practice.md ├── lab-4 │ ├── guide │ │ ├── intro.md │ │ ├── part-1.md │ │ ├── part-2.md │ │ ├── part-3.md │ │ ├── part-4.md │ │ ├── part-5.md │ │ ├── part-6.md │ │ └── summary.md │ ├── practice-1.md │ └── practice-2.md ├── lab-5 │ ├── files │ │ └── rcore-fs-analysis.pdf │ ├── guide │ │ ├── intro.md │ │ ├── part-1.md │ │ ├── part-2.md │ │ ├── part-3.md │ │ ├── part-4.md │ │ └── summary.md │ └── pics │ │ ├── design.key │ │ ├── design.png │ │ ├── device-tree.png │ │ └── virtio.gif ├── lab-6 │ ├── guide │ │ ├── intro.md │ │ ├── part-1.md │ │ ├── part-2.md │ │ ├── part-3.md │ │ ├── part-4.md │ │ ├── part-5.md │ │ ├── part-6.md │ │ └── summary.md │ └── practice.md └── pre-lab │ ├── env.md │ ├── gdb.md │ ├── os.md │ └── rust.md ├── gitalk.html ├── notes ├── log.md ├── 关于课程设计.md ├── 方案设计文档.md ├── 结题答辩(13周).key ├── 结题答辩(13周).pdf ├── 设计预期和目标.md └── 课程设计方案幻灯片.pdf ├── os ├── .cargo │ └── config ├── Cargo.toml ├── Makefile └── src │ ├── algorithm │ ├── Cargo.toml │ └── src │ │ ├── allocator │ │ ├── mod.rs │ │ ├── segment_tree_allocator.rs │ │ └── stacked_allocator.rs │ │ ├── lib.rs │ │ ├── scheduler │ │ ├── fifo_scheduler.rs │ │ ├── hrrn_scheduler.rs │ │ └── mod.rs │ │ └── unsafe_wrapper.rs │ ├── console.rs │ ├── drivers │ ├── block │ │ ├── mod.rs │ │ └── virtio_blk.rs │ ├── bus │ │ ├── mod.rs │ │ └── virtio_mmio.rs │ ├── device_tree.rs │ ├── driver.rs │ └── mod.rs │ ├── entry.asm │ ├── fs │ ├── config.rs │ ├── inode_ext.rs │ ├── mod.rs │ ├── stdin.rs │ └── stdout.rs │ ├── interrupt │ ├── context.rs │ ├── handler.rs │ ├── interrupt.asm │ ├── mod.rs │ └── timer.rs │ ├── kernel │ ├── condvar.rs │ ├── fs.rs │ ├── mod.rs │ ├── process.rs │ └── syscall.rs │ ├── linker.ld │ ├── main.rs │ ├── memory │ ├── address.rs │ ├── config.rs │ ├── frame │ │ ├── allocator.rs │ │ ├── frame_tracker.rs │ │ └── mod.rs │ ├── heap.rs │ ├── mapping │ │ ├── mapping.rs │ │ ├── memory_set.rs │ │ ├── mod.rs │ │ ├── page_table.rs │ │ ├── page_table_entry.rs │ │ └── segment.rs │ ├── mod.rs │ └── range.rs │ ├── panic.rs │ ├── process │ ├── config.rs │ ├── kernel_stack.rs │ ├── lock.rs │ ├── mod.rs │ ├── process.rs │ ├── processor.rs │ └── thread.rs │ └── sbi.rs ├── rust-toolchain └── user ├── .cargo └── config ├── Cargo.toml ├── Makefile └── src ├── bin ├── hello_world.rs └── notebook.rs ├── config.rs ├── console.rs ├── lib.rs └── syscall.rs /.github/ISSUE_TEMPLATE/0-----.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 0 学习探讨 3 | about: 讨论 Rust 或操作系统相关的问题 4 | title: '' 5 | labels: learning 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### 标题 11 | 大致描述问题 12 | 13 | > “生命周期的默认推断问题” 要好于 “Rust 语法为什么这么难” 14 | 15 | ### 格式 16 | 使用 Markdown 来格式化问题,可以切换到 Preview 预览格式化呈现效果 17 | 18 | 应使用 `` ` `` 和 `` ``` `` 符号来标记代码,例如: 19 | 20 | `fn foo()` 21 | ```rust 22 | fn foo() 23 | ``` 24 | 25 | ### Issue 的管理 26 | 所有学习讨论问题会使用 **learning** 标签,可以在 Issues 页面将 Filter 设置为 `label:learning` 来查看所有讨论 27 | 28 | 为了便于浏览,已解决的问题会被关闭。建议提问者在问题解决后进行回复并 @ 仓库维护人员 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug---.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 1 Bug 汇报 3 | about: 汇报 rCore-Tutorial 中遇到的问题 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 欢迎提出任何问题 11 | 12 | ### 涉及文件 13 | 例如 `os/Makefile` 14 | 15 | ### 相关段落 16 | 最好包括文档 / 代码片段的引用,以便于我们定位问题 17 | 18 | ### 遇到问题 19 | 代码无法工作?文档与实际不符?如果能够进一步分析问题的原因则是更加欢迎的 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-----.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 2 教程建议 3 | about: 给 rCore-Tutorial 提建议 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 文档讲得不清楚?代码设计可以更好?欢迎向教程提出建议! 11 | 12 | 如果你想为 rCore-Tutorial 做出贡献,可以先通过 Issue 来探讨改进方案,然后在 fork 的仓库中自己试着实现,并且发出 Pull Request 13 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-18.04 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Install Node.js 14 | uses: actions/setup-node@v2.1.0 15 | with: 16 | node-version: '10.21.0' 17 | - name: Build GitBook 18 | run: | 19 | npm install gitbook-cli -g 20 | gitbook install 21 | gitbook build 22 | - name: Push to remote repo 23 | uses: peaceiris/actions-gh-pages@v3 24 | with: 25 | deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 26 | external_repository: rcore-os/rCore-Tutorial-deploy 27 | publish_branch: master 28 | publish_dir: ./_book 29 | commit_message: "[Auto-Deploy] " -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | check: 7 | runs-on: ubuntu-20.04 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions-rs/toolchain@v1 11 | with: 12 | target: riscv64imac-unknown-none-elf 13 | components: rustfmt, clippy 14 | - name: Check user 15 | run: | 16 | cd user 17 | cargo fmt -- --check 18 | cargo clippy -- -D warnings 19 | - name: Check os 20 | run: | 21 | export USER_IMG="../usr/build/riscv64.img" 22 | cd os 23 | cargo fmt -- --check 24 | cargo clippy -- -D warnings 25 | 26 | build: 27 | runs-on: ${{ matrix.os }} 28 | strategy: 29 | matrix: 30 | os: [ubuntu-20.04, macos-latest] 31 | steps: 32 | - uses: actions/checkout@v2 33 | - uses: actions-rs/toolchain@v1 34 | with: 35 | target: riscv64imac-unknown-none-elf 36 | components: llvm-tools-preview 37 | - name: Install cargo-binutils 38 | run: cargo install cargo-binutils 39 | - name: Install QEMU 40 | if: ${{ matrix.os == 'ubuntu-20.04' }} 41 | run: | 42 | sudo apt update 43 | sudo apt install qemu-utils 44 | - name: Install QEMU 45 | if: ${{ matrix.os == 'macos-latest' }} 46 | run: brew install qemu 47 | - name: Build user 48 | run: cd user && make build 49 | - name: Build os 50 | run: cd os && make build 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### 环境文件 2 | # macOS 3 | .DS_Store 4 | # JetBrains 5 | **/.idea 6 | # VS Code 7 | **/.vscode 8 | 9 | ### GitBook 10 | # GitBook 部署文件 11 | _book 12 | public 13 | # npm 依赖文件 14 | node_modules 15 | package-lock.json 16 | 17 | ### Cargo 18 | # Cargo 包设置(自动生成) 19 | Cargo.lock 20 | 21 | ### rCore 22 | # Rust 编译文件 23 | os/target 24 | user/target 25 | user/build 26 | # GDB 临时文件 27 | .gdb_history 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | @make -C user build 3 | @make -C os run 4 | 5 | clean: 6 | @make -C user clean 7 | @make -C os clean 8 | 9 | fmt: 10 | @cd os && cargo fmt 11 | @cd os/src/algorithm && cargo fmt 12 | @cd user && cargo fmt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rCore-Tutorial V3 2 | 3 | **注意:本项目已经不再维护,有兴趣的同学请看[rCore-Tutorial-v3](https://github.com/rcore-os/rCore-Tutorial-v3)。** 4 | 5 | [本教学仓库](https://github.com/rcore-os/rCore-Tutorial)是继 [rCore_tutorial V2](https://rcore-os.github.io/rCore_tutorial_doc/) 后重构的 V3 版本。 6 | 7 | 本文档的目标主要针对「做实验的同学」,我们会对每章结束后提供完成的代码,你的练习题只需要基于我们给出的版本上增量实现即可,不需要重新按照教程写一遍。 8 | 9 | 而对想完整实现一个 rCore 的同学来说,我们的文档可能不太友好。因为在编写教程过程中,我们需要对清晰和全面做很多的权衡和考虑、需要省略掉大量 Rust 语法层面和 OS 无关的代码以带来更好的可读性和精简性,所以想参考本文档并完整实现的同学可能不会有从头复制到尾的流畅(这样的做法也不是学习的初衷),可能需要自己有一些完整的认识和思考。 10 | 11 | 另外,如果你觉得字体大小和样式不舒服,可以通过 GitBook 上方的按钮调节。 12 | 13 | ## 仓库目录 14 | 15 | - `docs/`:教学实验指导分实验内容和开发规范 16 | - `notes/`:开题报告和若干讨论 17 | - `os/`:操作系统代码 18 | - `user/`:用户态代码 19 | - `SUMMARY.md`:GitBook 目录页 20 | - `book.json`:GitBook 配置文件 21 | - `rust-toolchain`:限定 Rust 工具链版本 22 | - `deploy.sh`:自动部署脚本 23 | 24 | 25 | ## 实验指导 26 | 27 | 基于 GitBook,目前已经部署到了 [GitHub Pages](https://rcore-os.github.io/rCore-Tutorial-deploy/) 上面。 28 | 29 | ### 文档本地使用方法 30 | 31 | 32 | ```bash 33 | npm install -g gitbook-cli 34 | gitbook install 35 | gitbook serve 36 | ``` 37 | 38 | ## 代码 39 | 40 | ### 操作系统代码 41 | 本项目基于 cargo 和 make 等工具,在根目录通过 `make run` 命令即可运行代码,更具体的细节请参见 `Makefile`、`os/Makefile` 以及 `user/Makefile`。 42 | 43 | ### 参考和感谢 44 | 45 | 本文档和代码部分参考了: 46 | - [rCore](https://github.com/rcore-os/rCore) 47 | - [zCore](https://github.com/rcore-os/zCore) 48 | - [rCore_tutorial V2](https://rcore-os.github.io/rCore_tutorial_doc/) 49 | - [使用Rust编写操作系统](https://github.com/rustcc/writing-an-os-in-rust) 50 | 51 | 在此对仓库的开发和维护者表示感谢,同时也感谢很多在本项目开发中一起讨论和勘误的老师和同学们。 52 | 53 | 54 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # rCore 教学实验文档 2 | 3 | * [实验简介](README.md) 4 | * [更新日志](notes/log.md) 5 | 6 | ## 实验之前 7 | 8 | 9 | * [环境部署](docs/pre-lab/env.md) 10 | * [GDB 调试方法*](docs/pre-lab/gdb.md) 11 | 12 | ## 实验指导 13 | * 实验指导零 14 | * [摘要](docs/lab-0/guide/intro.md) 15 | * [创建项目](docs/lab-0/guide/part-1.md) 16 | * [移除标准库依赖](docs/lab-0/guide/part-2.md) 17 | * [移除运行时环境依赖](docs/lab-0/guide/part-3.md) 18 | * [编译为裸机目标](docs/lab-0/guide/part-4.md) 19 | * [生成内核镜像](docs/lab-0/guide/part-5.md) 20 | * [调整内存布局](docs/lab-0/guide/part-6.md) 21 | * [重写程序入口点](docs/lab-0/guide/part-7.md) 22 | * [使用 QEMU 运行](docs/lab-0/guide/part-8.md) 23 | * [接口封装和代码整理](docs/lab-0/guide/part-9.md) 24 | * [小结](docs/lab-0/guide/summary.md) 25 | * 实验指导一 26 | * [摘要](docs/lab-1/guide/intro.md) 27 | * [什么是中断](docs/lab-1/guide/part-1.md) 28 | * [RISC-V 中的中断](docs/lab-1/guide/part-2.md) 29 | * [程序运行状态](docs/lab-1/guide/part-3.md) 30 | * [状态的保存与恢复](docs/lab-1/guide/part-4.md) 31 | * [进入中断处理流程](docs/lab-1/guide/part-5.md) 32 | * [时钟中断](docs/lab-1/guide/part-6.md) 33 | * [小结](docs/lab-1/guide/summary.md) 34 | * 实验指导二 35 | * [摘要](docs/lab-2/guide/intro.md) 36 | * [动态内存分配](docs/lab-2/guide/part-1.md) 37 | * [物理内存探测](docs/lab-2/guide/part-2.md) 38 | * [物理内存管理](docs/lab-2/guide/part-3.md) 39 | * [小结](docs/lab-2/guide/summary.md) 40 | * 实验指导三 41 | * [摘要](docs/lab-3/guide/intro.md) 42 | * [从虚拟地址到物理地址](docs/lab-3/guide/part-1.md) 43 | * [修改内核](docs/lab-3/guide/part-2.md) 44 | * [实现页表](docs/lab-3/guide/part-3.md) 45 | * [实现内核重映射](docs/lab-3/guide/part-4.md) 46 | * [页面置换*](docs/lab-3/guide/part-5.md) 47 | * [小结](docs/lab-3/guide/summary.md) 48 | * 实验指导四 49 | * [摘要](docs/lab-4/guide/intro.md) 50 | * [线程和进程](docs/lab-4/guide/part-1.md) 51 | * [线程的创建](docs/lab-4/guide/part-2.md) 52 | * [线程的切换](docs/lab-4/guide/part-3.md) 53 | * [线程的结束](docs/lab-4/guide/part-4.md) 54 | * [内核栈](docs/lab-4/guide/part-5.md) 55 | * [线程调度](docs/lab-4/guide/part-6.md) 56 | * [小结](docs/lab-4/guide/summary.md) 57 | * 实验指导五 58 | * [摘要](docs/lab-5/guide/intro.md) 59 | * [设备树](docs/lab-5/guide/part-1.md) 60 | * [virtio](docs/lab-5/guide/part-2.md) 61 | * [驱动和块设备驱动](docs/lab-5/guide/part-3.md) 62 | * [文件系统](docs/lab-5/guide/part-4.md) 63 | * [小结](docs/lab-5/guide/summary.md) 64 | * 实验指导六 65 | * [摘要](docs/lab-6/guide/intro.md) 66 | * [构建用户程序框架](docs/lab-6/guide/part-1.md) 67 | * [打包为磁盘镜像](docs/lab-6/guide/part-2.md) 68 | * [解析 ELF 文件并创建线程](docs/lab-6/guide/part-3.md) 69 | * [实现系统调用](docs/lab-6/guide/part-4.md) 70 | * [处理文件描述符](docs/lab-6/guide/part-5.md) 71 | * [条件变量](docs/lab-6/guide/part-6.md) 72 | * [小结](docs/lab-6/guide/summary.md) 73 | 74 | ## 实验题 75 | * [实验一:中断](docs/lab-1/practice.md) 76 | * [实验二:内存分配](docs/lab-2/practice.md) 77 | * [实验三:虚实地址转换](docs/lab-3/practice.md) 78 | * [实验四(上):线程](docs/lab-4/practice-1.md) 79 | * [实验四(下):线程调度](docs/lab-4/practice-2.md) 80 | * [实验六:系统调用](docs/lab-6/practice.md) 81 | 82 | ## 开发笔记 83 | * [文档代码划分](docs/format/partition.md) 84 | * [文档格式规范](docs/format/doc.md) 85 | * [代码格式规范](docs/format/code.md) -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "hide-element", 4 | "chapter-fold", 5 | "katex", 6 | "alerts", 7 | "emphasize", 8 | "mermaid-gb3", 9 | "codeblock-label", 10 | "code", 11 | "search-pro", 12 | "click-reveal", 13 | "expandable-chapters-interactive", 14 | "localized-footer" 15 | ], 16 | "pluginsConfig": { 17 | "fontsettings": { 18 | "theme": "white", 19 | "family": "sans", 20 | "size": 1 21 | }, 22 | "localized-footer": { 23 | "filename": "gitalk.html" 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 下面的 DEPLOY_DIR 目录将关联到 https://github.com/rcore-os/rCore-Tutorial-deploy 远程仓库 3 | # 部署后可以通过 https://rcore-os.github.io/rCore-Tutorial-deploy 来访问 4 | DEPLOY_DIR=../rCore-Tutorial-deploy/ 5 | CURRENT_DIR=$(pwd) 6 | DEPLOY_DIR=$CURRENT_DIR/$DEPLOY_DIR 7 | 8 | # 判断是否需要重新 clone 仓库 9 | if [ -d "$DEPLOY_DIR" ]; then 10 | echo "$DEPLOY_DIR exists, resetting to remote master ..." 11 | cd $DEPLOY_DIR 12 | else 13 | echo "$DEPLOY_DIR doesn't exist, cloning from remote ..." 14 | mkdir -p $DEPLOY_DIR 15 | cd $DEPLOY_DIR 16 | git init 17 | # 该脚本将强制通过 SSH 协议和 GitHub 通信 18 | git remote add origin git@github.com:rcore-os/rCore-Tutorial-deploy.git 19 | fi 20 | 21 | # 重置到 remote master 22 | git fetch origin 23 | git reset --hard origin/master 24 | 25 | # 构建 GitBook 并复制 26 | cd $CURRENT_DIR 27 | gitbook build 28 | cp -r _book/* $DEPLOY_DIR 29 | cd $DEPLOY_DIR 30 | 31 | # Commit 并 push 32 | CURRENT_TIME=$(date +"%Y-%m-%d %H:%m:%S") 33 | git add * 34 | git commit -m "[Auto-deploy] Build $CURRENT_TIME" 35 | git push origin master 36 | 37 | # 返回当前目录 38 | cd $CURRENT_DIR -------------------------------------------------------------------------------- /docs/format/code.md: -------------------------------------------------------------------------------- 1 | ## 代码规范 2 | 3 | ### 代码风格 4 | - 以 cargo 输出没有 warning 为准 5 | - 可以通过根目录的 `make fmt` 来自动调用 `cargo fmt` 规范全部的代码 6 | 7 | ### 注释规范 8 | - 用 `//!` 注释外层内容,例如在文件开始注释整个模块 9 | - 用 `///` 为函数添加 doc 注释,其内部使用 Markdown 语法 10 | - 可以使用 markdown 格式的链接,链接内容使用 Rust 可以直接链上,例如 11 | ```rust 12 | /// 样例注释 13 | /// [`link`]: crate::xxx::xxx 14 | fn some_func() {} 15 | ``` 16 | 17 | - 对于地址 literal,使用小写,使用 `_` 每隔四位进行标记,例如 `0x8000_0000` `0xffff_ffff_c000_0000` 18 | 19 | - 实现一个 trait 时,doc 是可选的,而如果有,应当写在 impl 上面,而不是具体的方法上面 20 | ```rust 21 | /// doc here (optional) 22 | impl Default for Type { 23 | /// not here 24 | fn default() -> Self { ... } 25 | } 26 | ``` 27 | 28 | ### 参考 29 | - https://doc.rust-lang.org/1.26.2/book/first-edition/comments.html 30 | - https://doc.rust-lang.org/1.26.2/book/second-edition/ch14-02-publishing-to-crates-io.html -------------------------------------------------------------------------------- /docs/format/doc.md: -------------------------------------------------------------------------------- 1 | ## 文档规范 2 | 3 | ### 一些标准的架构、软件名词写法 4 | - 语言相关 5 | - Rust 6 | - C 7 | - C++ 8 | - Markdown 9 | - 教程 10 | - rCore-Tutorial 11 | - 操作系统相关 12 | - uCore 13 | - rCore 14 | - Linux 15 | - macOS 16 | - Windows 17 | - Ubuntu 18 | - 操作系统技术相关 19 | - 物理页(而不是物理页帧) 20 | - 虚拟页(而不是物理页帧) 21 | - 架构相关 22 | - x86_64 23 | - RISC-V 64 24 | - 其他一些名词 25 | - ABI 26 | - GitHub 27 | - virtio 28 | - Rust 相关 29 | - rustup 30 | - cargo 31 | - rustc 32 | - 其他软件 33 | - QEMU 34 | - Homebrew 35 | 36 | ### 内容控制 37 | - 我们的目标针对于「做实验的同学」,对想完整实现一个 rCore 的同学来说可能不太友好; 38 | - 但是我们也相信,这部分想完整实现的同学也不会因为我们在文档中少些了一部分非常细节的诸如模块调用的内容就放弃,而且从头复制到尾也不是一个好的做法,这不会让你对操作系统有更深刻的理解; 39 | - 所以,在文档开发过程中,我们需要对清晰和全面做很多的权衡和考虑,需要省略掉大量语法层面而 OS 无关的代码来带来更多的可读性和精简性; 40 | - 所以,在文档中引用的代码,只需要写主体的函数,不需要把一系列调用、头部注释全部加入进去。在最后,可能会再利用代码折叠的方式对这个问题进一步权衡。 41 | 42 | ### 书写格式 43 | - 在数字、英文、独立的标点或记号两侧的中文之间要加空格,如: 44 | - 安装 QEMU 45 | - 分为 2 个部分 46 | - 命令、宏名、寄存器、类型名、函数名、变量名、行间输出、编译选项、路径和文件名需要使用 \`\` 记号 47 | - 寄存器 `a0` 这样的也需要使用 \`\` 记号,而不是 $$a_0$$,这是为了和 `sepc` 统一 48 | - 行内命令或运行输出引用使用 \`\` 记号,并在两侧加入空格,如: 49 | - `cargo run` 50 | - 出现 `ERROR: pkg-config binary 'pkg-config' not found` 时 51 | - 行间命令使用 \`\`\` 记号并加入语言记号: 52 | - 命令使用 bash 记号 53 | - Rust 语言使用 rust 记号 54 | - cargo 的配置使用 toml 记号 55 | - 如何命令只是命令,则不需要 $ 记号(方便同学复制),如: 56 | ```bash 57 | echo "Hello, world." 58 | ``` 59 | - 如果在展示一个命令带来的输出效果,需要加入 $ 记号表示一个命令的开始,如: 60 | ```bash 61 | $ echo "Hello, world." 62 | Hello, world. 63 | ``` 64 | - 粗体使用 \*\* **粗体** \*\* 记号 65 | - 一些重要的概念最好进行加粗 66 | - 斜体使用 \* *斜体* \* 记号,而不要混合使用 \_ _Italic_ \_ 记号 67 | - 在正式的段落中要加入标点符号,在 - 记号开始的列表中的单独名词表项不加入标点符号(但是如果是段落需要加),如: 68 | - 操作系统有(名词罗列): 69 | - macOS 70 | - Windows 71 | - 我们需要(连贯段落): 72 | - 先打开 QEMU; 73 | - 再关闭 QEMU。 74 | - 在 / 记号两侧添加空格,如: 75 | - Linux / Windows WSL 76 | - 中文名词的英文解释多用大写,如: 77 | - 裸机(Bare Metal) 78 | - `sepc` (Supervisor Exception Program Counter) 79 | - 只要是主体是中文的段落,括号统一使用中文括号(),如果主体是英文则使用英文括号 () 80 | - 值得注意的是中文括号两侧本来就会又留白,这里不会在括号两侧加入空格 81 | - 英文空格两侧最好加上空格 82 | - 在文档中引用成段的代码时,需要填写上文件的路径,如: 83 | 84 | {% label %}os/src/sbi.rs{% endlabel %} 85 | ```rust 86 | /// 向控制台输出一个字符 87 | /// 88 | /// 需要注意我们不能直接使用 Rust 中的 char 类型 89 | pub fn console_putchar(c: usize) { 90 | sbi_call(SBI_CONSOLE_PUTCHAR, c, 0, 0); 91 | } 92 | ``` 93 | - 在使用伪代码时,不使用 `$` `%` 等符号描述寄存器,使用 `:=` 表示赋值,例如 `pc := sepc` 94 | - 代码过长或会让文档显得很长时,需要进行折叠 95 | - 强调请使用「强调」,而不是『强调』 96 | 97 | ### 小节格式 98 | 99 | - 章节的标题为使用 `#` 一级标题,后面的子标题依次加级别 100 | - 小节的标题统一使用 `##` 二级标题,后面的子标题依次加级别 -------------------------------------------------------------------------------- /docs/format/partition.md: -------------------------------------------------------------------------------- 1 | ## 文档代码划分 2 | 3 | ### 文档和代码功能划分和注意事项 4 | 5 | - 文档专注于 OS 的原理和同时涉及 OS 和 Rust 特性的 Rust 语言特性,不会花篇幅来讲解 Rust 语言本身; 6 | - 文档中引用的代码和代码目录中代码保持一致; 7 | - 代码中书写的注释专注于功能性,不会有流程性和原理性的介绍; 8 | - 文档应有全面的包括流程性、原理性和功能性的介绍。 -------------------------------------------------------------------------------- /docs/lab-0/guide/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导零 2 | 3 | ## 实验概要 4 | 5 | 这一章的实验指导中,你将会学到: 6 | 7 | - 使用 Rust 包管理器 cargo 创建一个 Rust 项目 8 | - 移除 Rust 程序对操作系统的依赖,构建一个独立化可执行的程序 9 | - 我们将程序的目标平台设置为 RISC-V,这样我们的代码将可以在 RISC-V 指令集的裸机(Bare Metal)上执行 Rust 代码 10 | - 生成内核镜像、调整代码的内存布局并在 QEMU 模拟器中启动 11 | - 封装如输出、关机等一些 SBI 的接口,方便后续开发 -------------------------------------------------------------------------------- /docs/lab-0/guide/part-1.md: -------------------------------------------------------------------------------- 1 | ## 创建 Rust 项目 2 | 3 | ### 创建项目 4 | 我们首先创建一个整个项目的目录,并在工作目录中首先创建一个名为 `rust-toolchain` 的文件,并在其中写入所需要的工具链版本: 5 | 6 | {% label %}rust-toolchain{% endlabel %} 7 | ``` 8 | nightly-2020-06-27 9 | ``` 10 | 11 | 之后在目录内部使用 `cargo new` 命令在我们的项目目录内创建一个新的 Rust 项目 os,命令如下: 12 | 13 | {% label %}运行命令{% endlabel %} 14 | ```bash 15 | cargo new os 16 | ``` 17 | 18 | 这里我们把项目命名为 os。同时,cargo 默认为我们添加了 `--bin` 选项,说明我们将要创建一个可执行文件而非一个库。 19 | 20 | ### 目录结构 21 | 22 | 创建完成后,整个项目的目录结构如下: 23 | 24 | {% label %}目录结构{% endlabel %} 25 | ```bash 26 | Project 项目目录 27 | ├── rust-toolchain Rust 工具链版本 28 | └── os 29 | ├── Cargo.toml 项目配置文件 30 | └── src 源代码路径 31 | └── main.rs 源程序 32 | ``` 33 | 34 | ### 构建和运行 35 | 36 | 接下来我们进入 os 文件夹,并尝试构建、运行项目: 37 | 38 | {% label %}运行输出{% endlabel %} 39 | ```bash 40 | $ cargo run 41 | ... 42 | Hello, world! 43 | ``` 44 | 45 | 打开 `os/src/main.rs` 发现里面确实只是输出了一行 `Hello, world!`。这个应用可以正常运行,但是即使只是这么一个简单的功能,也离不开所在操作系统的帮助。我们既然要写一个新的操作系统,就不能依赖于任何已有操作系统,接下来我们尝试移除该项目对于操作系统的依赖。 -------------------------------------------------------------------------------- /docs/lab-0/guide/part-2.md: -------------------------------------------------------------------------------- 1 | ## 移除标准库依赖 2 | 3 | ### 禁用标准库 4 | 项目默认是链接 Rust 标准库 std 的,它依赖于操作系统,因此我们需要显式通过 `#![no_std]` 将其禁用: 5 | 6 | {% label %}os/src/main.rs{% endlabel %} 7 | ```rust 8 | //! # 全局属性 9 | //! - `#![no_std]` 10 | //! 禁用标准库 11 | #![no_std] 12 | 13 | fn main() { 14 | println!("Hello, rCore-Tutorial!"); 15 | } 16 | ``` 17 | 18 | 我们使用 `cargo build` 构建项目,会出现下面的错误: 19 | 20 | {% label %}运行输出{% endlabel %} 21 | ```rust 22 | error: cannot find macro `println` in this scope 23 | --> src/main.rs:3:5 24 | | 25 | 7 | println!("Hello, rCore-Tutorial!"); 26 | | ^^^^^^^ 27 | error: `#[panic_handler]` function required, but not found 28 | error: language item required, but not found: `eh_personality` 29 | ``` 30 | 31 | 接下来,我们依次解决这些问题。 32 | 33 | ### 宏 println! 34 | 35 | 第一个错误是说 `println!` 宏未找到,实际上这个宏属于 Rust 标准库 std,它会依赖操作系统标准输出等一系列功能。由于它被我们禁用了当然就找不到了。我们暂时将该输出语句删除,之后给出不依赖操作系统的实现。 36 | 37 | ### panic 处理函数 38 | 39 | 第二个错误是说需要一个函数作为 `panic_handler` ,这个函数负责在程序发生 panic 时调用。它默认使用标准库 std 中实现的函数并依赖于操作系统特殊的文件描述符,由于我们禁用了标准库,因此只能自己实现它: 40 | 41 | {% label %}os/src/main.rs{% endlabel %} 42 | ```rust 43 | use core::panic::PanicInfo; 44 | 45 | /// 当 panic 发生时会调用该函数 46 | /// 我们暂时将它的实现为一个死循环 47 | #[panic_handler] 48 | fn panic(_info: &PanicInfo) -> ! { 49 | loop {} 50 | } 51 | ``` 52 | 53 | > **[info] Rust Panic** 54 | > 55 | > Panic 在 Rust 中表明程序遇到了错误,需要被迫停止运行或者通过捕获的机制来处理。 56 | 57 | 类型为 `PanicInfo` 的参数包含了 panic 发生的文件名、代码行数和可选的错误信息。这个函数从不返回,所以他被标记为发散函数(Diverging Function)。发散函数的返回类型称作 Never 类型("never" type),记为 `!`。对这个函数,我们目前能做的很少,所以我们只需编写一个死循环 `loop {}`。 58 | 59 | 这里我们用到了核心库 core,与标准库 std 不同,这个库不需要操作系统的支持,下面我们还会与它打交道。 60 | 61 | ### eh_personality 语义项 62 | 63 | 第三个错误提到了语义项(Language Item) ,它是编译器内部所需的特殊函数或类型。刚才的 `panic_handler` 也是一个语义项,我们要用它告诉编译器当程序发生 panic 之后如何处理。 64 | 65 | 而这个错误相关语义项 `eh_personality` ,其中 eh 是 Exception Handling 的缩写,它是一个标记某函数用来实现**堆栈展开**处理功能的语义项。这个语义项也与 panic 有关。 66 | 67 | > **[info] 堆栈展开 (Stack Unwinding) ** 68 | > 69 | > 通常当程序出现了异常时,从异常点开始会沿着 caller 调用栈一层一层回溯,直到找到某个函数能够捕获这个异常或终止程序。这个过程称为堆栈展开。 70 | > 71 | > 当程序出现异常时,我们需要沿着调用栈一层层回溯上去回收每个 caller 中定义的局部变量(这里的回收包括 C++ 的 RAII 的析构以及 Rust 的 drop 等)避免造成捕获异常并恢复后的内存溢出。 72 | > 73 | > 而在 Rust 中,panic 证明程序出现了错误,我们则会对于每个 caller 函数调用依次这个被标记为堆栈展开处理函数的函数进行清理。 74 | > 75 | > 这个处理函数是一个依赖于操作系统的复杂过程,在标准库中实现。但是我们禁用了标准库使得编译器找不到该过程的实现函数了。 76 | 77 | 简单起见,我们这里不会进一步捕获异常也不需要清理现场,我们设置为直接退出程序即可。这样堆栈展开处理函数不会被调用,编译器也就不会去寻找它的实现了。 78 | 79 | 因此,我们在项目配置文件中直接将 dev 配置和 release 配置的 panic 的处理策略设为直接终止,也就是直接调用我们的 `panic_handler` 而不是先进行堆栈展开等处理再调用。 80 | 81 | {% label %}os/Cargo.toml{% endlabel %} 82 | ```toml 83 | ... 84 | 85 | # panic 时直接终止,因为我们没有实现堆栈展开的功能 86 | [profile.dev] 87 | panic = "abort" 88 | 89 | [profile.release] 90 | panic = "abort" 91 | ``` 92 | 93 | 此时,我们 `cargo build` ,但是又出现了新的错误,我们将在后面的部分解决: 94 | 95 | {% label %}运行输出{% endlabel %} 96 | ```bash 97 | error: requires `start` lang_item 98 | ``` -------------------------------------------------------------------------------- /docs/lab-0/guide/part-3.md: -------------------------------------------------------------------------------- 1 | ## 移除运行时环境依赖 2 | 3 | ### 运行时系统 4 | 对于大多数语言,他们都使用了**运行时系统**(Runtime System),这可能导致 `main` 函数并不是实际执行的第一个函数。 5 | 6 | 以 Rust 语言为例,一个典型的链接了标准库的 Rust 程序会首先跳转到 C 语言运行时环境中的 `crt0`(C Runtime Zero)进入 C 语言运行时环境设置 C 程序运行所需要的环境(如创建堆栈或设置寄存器参数等)。 7 | 8 | 然后 C 语言运行时环境会跳转到 Rust 运行时环境的入口点(Entry Point)进入 Rust 运行时入口函数继续设置 Rust 运行环境,而这个 Rust 的运行时入口点就是被 `start` 语义项标记的。Rust 运行时环境的入口点结束之后才会调用 `main` 函数进入主程序。 9 | 10 | C 语言运行时环境和 Rust 运行时环境都需要标准库支持,我们的程序无法访问。如果覆盖了 `start` 语义项,仍然需要 `crt0`,并不能解决问题。所以需要重写覆盖整个 `crt0` 入口点: 11 | 12 | {% label %}os/src/main.rs{% endlabel %} 13 | ```rust 14 | //! # 全局属性 15 | //! - `#![no_std]` 16 | //! 禁用标准库 17 | #![no_std] 18 | //! 19 | //! - `#![no_main]` 20 | //! 不使用 `main` 函数等全部 Rust-level 入口点来作为程序入口 21 | #![no_main] 22 | 23 | use core::panic::PanicInfo; 24 | 25 | /// 当 panic 发生时会调用该函数 26 | /// 我们暂时将它的实现为一个死循环 27 | #[panic_handler] 28 | fn panic(_info: &PanicInfo) -> ! { 29 | loop {} 30 | } 31 | 32 | /// 覆盖 crt0 中的 _start 函数 33 | /// 我们暂时将它的实现为一个死循环 34 | #[no_mangle] 35 | pub extern "C" fn _start() -> ! { 36 | loop {} 37 | } 38 | ``` 39 | 40 | 我们加上 `#![no_main]` 告诉编译器我们不用常规的入口点。 41 | 42 | 同时我们实现一个 `_start` 函数来代替 `crt0`,并加上 `#[no_mangle]` 告诉编译器对于此函数禁用编译期间的名称重整(Name Mangling),即确保编译器生成一个名为 `_start` 的函数,而非为了实现函数重载等而生成的形如 `_ZN3blog_os4_start7hb173fedf945531caE` 散列化后的函数名。由于 `_start` 是大多数系统的默认入口点名字,所以我们要确保它不会发生变化。 43 | 44 | 接着,我们使用 `extern "C"` 描述 `_start` 函数,这是 Rust 中的 FFI (Foreign Function Interface, 语言交互接口)语法,表示此函数是一个 C 函数而非 Rust 函数。由于 `_start` 是作为 C 语言运行时的入口点,看起来合情合理。 45 | 46 | 由于程序会一直停在 `crt0` 的入口点,我们可以移除没用的 `main` 函数。 47 | 48 | ### 链接错误 49 | 50 | 再次 `cargo build` ,我们会看到一大段链接错误。 51 | 52 | 链接器(Linker)是一个程序,它将生成的目标文件组合为一个可执行文件。不同的操作系统如 Windows、macOS 或 Linux,规定了不同的可执行文件格式,因此也各有自己的链接器,抛出不同的错误;但这些错误的根本原因还是相同的:链接器的默认配置假定程序依赖于 C 语言的运行时环境,但我们的程序并不依赖于它。 53 | 54 | 为了解决这个错误,我们需要告诉链接器,它不应该包含 C 语言运行时环境。我们可以选择提供特定的链接器参数(Linker Argument),也可以选择编译为裸机目标(Bare Metal Target),我们将沿着后者的思路在后面解决这个问题,即直接编译为裸机目标不链接任何运行时环境。 -------------------------------------------------------------------------------- /docs/lab-0/guide/part-4.md: -------------------------------------------------------------------------------- 1 | ## 编译为裸机目标 2 | 3 | 在默认情况下,Rust 尝试适配当前的系统环境,编译可执行程序。举个例子,如果你使用 x86_64 平台的 Windows 系统,Rust 将尝试编译一个扩展名为 `.exe` 的 Windows 可执行程序,并使用 `x86_64` 指令集。这个环境又被称作为你的宿主系统(Host System)。 4 | 5 | 为了描述不同的环境,Rust 使用一个称为目标三元组(Target Triple)的字符串 `---`。要查看当前系统的目标三元组,我们可以运行 `rustc --version --verbose`: 6 | 7 | {% label %}运行输出{% endlabel %} 8 | ```bash 9 | rustc 1.46.0-nightly (7750c3d46 2020-06-26) 10 | binary: rustc 11 | commit-hash: 7750c3d46bc19784adb1ee6e37a5ec7e4cd7e772 12 | commit-date: 2020-06-26 13 | host: x86_64-unknown-linux-gnu 14 | release: 1.46.0-nightly 15 | LLVM version: 10.0 16 | ``` 17 | 18 | 上面这段输出来自一个 x86_64 平台下的 Linux 系统。我们能看到,host 字段的值为三元组 x86_64-unknown-linux-gnu,它包含了 CPU 架构 x86_64、供应商 unknown、操作系统 linux 和二进制接口 gnu。 19 | 20 | Rust 编译器尝试为当前系统的三元组编译,并假定底层有一个类似于 Windows 或 Linux 的操作系统提供 C 语言运行环境,然而这将导致链接器错误。所以,为了避免这个错误,我们可以另选一个底层没有操作系统的运行环境。 21 | 22 | 这样的运行环境被称作裸机环境,例如目标三元组 riscv64imac-unknown-none-elf 描述了一个 RISC-V 64 位指令集的系统。我们暂时不需要了解它的细节,只需要知道这个环境底层没有操作系统,这是由三元组中的 none 描述的。要为这个目标编译,我们需要使用 rustup 添加它: 23 | 24 | {% label %}运行命令{% endlabel %} 25 | ```bash 26 | rustup target add riscv64imac-unknown-none-elf 27 | ``` 28 | 29 | 这行命令将为目标下载一个标准库和 core 库。这之后,我们就能为这个目标成功构建独立式可执行程序了: 30 | 31 | {% label %}运行命令{% endlabel %} 32 | ```bash 33 | cargo build --target riscv64imac-unknown-none-elf 34 | ``` 35 | 36 | 编译出的结果被放在了 `os/target/riscv64imac-unknown-none-elf/debug` 文件夹中。可以看到其中有一个名为 `os` 的可执行文件。不过由于它的目标平台是 RISC-V 64,我们暂时还不能通过我们的开发环境执行它。 37 | 38 | 由于我们之后都会使用 RISC-V 作为编译目标,为了避免每次都要加 `--target` 参数,我们可以使用 [cargo 配置文件](https://doc.rust-lang.org/cargo/reference/config.html)为项目配置默认的编译选项。 39 | 40 | 在 `os` 文件夹中创建一个 `.cargo` 文件夹,并在其中创建一个名为 `config` 的文件,在其中填入以下内容: 41 | 42 | {% label %}os/.cargo/config{% endlabel %} 43 | ```toml 44 | # 编译的目标平台 45 | [build] 46 | target = "riscv64imac-unknown-none-elf" 47 | ``` 48 | 49 | 这指定了此项目编译时默认的目标。以后我们就可以直接使用 `cargo build` 来编译了。 50 | 51 | 至此,我们完成了在 RISC-V 64 位平台的二进制程序编译,后面我们将通过布局和代码的简单调整实现一个最简单的内核。 52 | -------------------------------------------------------------------------------- /docs/lab-0/guide/part-7.md: -------------------------------------------------------------------------------- 1 | ## 重写程序入口点 `_start` 2 | 3 | 我们在第一章中,曾自己重写了一个入口点 `_start`,在那里我们仅仅只是让它死循环。但是现在,类似 C 语言运行时环境,我们希望这个函数可以为我们设置内核的运行环境。随后,我们才真正开始执行内核的代码。 4 | 5 | 但是具体而言我们需要设置怎样的运行环境呢? 6 | 7 | > **[info] 第一条指令** 8 | > 9 | > 在 CPU 加电或 Reset 后,它首先会进行自检(POST, Power-On Self-Test),通过自检后会跳转到**启动代码(Bootloader)**的入口。在 bootloader 中,我们进行外设探测,并对内核的运行环境进行初步设置。随后,bootloader 会将内核代码从硬盘加载到内存中,并跳转到内核入口,正式进入内核。也就是说,CPU 所执行的第一条指令其实是指 bootloader 的第一条指令。 10 | 11 | 幸运的是, 我们已经有现成的 bootloader 实现 [OpenSBI](https://github.com/riscv/opensbi) 固件(Firmware)。 12 | 13 | > **[info] Firmware 固件** 14 | > 15 | > 在计算中,固件是一种特定的计算机软件,它为设备的特定硬件提供低级控制进一步加载其他软件的功能。固件可以为设备更复杂的软件(如操作系统)提供标准化的操作环境,或者,对于不太复杂的设备,充当设备的完整操作系统,执行所有控制、监视和数据操作功能。在基于 x86 的计算机系统中, BIOS 或 UEFI 是一种固件;在基于 RISC-V 的计算机系统中,OpenSBI 是一种固件。 16 | 17 | OpenSBI 固件运行在特权级别很高的计算机硬件环境中,即 RISC-V 64 的 M Mode(CPU 加电后也就运行在 M Mode),我们将要实现的 OS 内核运行在 S Mode,而我们要支持的用户程序运行在 U Mode。在开发过程中我们重点关注 S Mode。 18 | 19 | > **[info] RISC-V 64 的特权级** 20 | > 21 | > RISC-V 共有 3 种特权级,分别是 U Mode(User / Application 模式)、S Mode(Supervisor 模式)和 M Mode(Machine 模式)。 22 | > 23 | > 从 U 到 S 再到 M,权限不断提高,这意味着你可以使用更多的特权指令,访需求权限更高的寄存器等等。我们可以使用一些指令来修改 CPU 的**当前特权级**。而当当前特权级不足以执行特权指令或访问一些寄存器时,CPU 会通过某种方式告诉我们。 24 | 25 | OpenSBI 所做的一件事情就是把 CPU 从 M Mode 切换到 S Mode,接着跳转到一个固定地址 0x80200000,开始执行内核代码。 26 | 27 | > **[info] RISC-V 的 M Mode** 28 | > 29 | > Machine 模式是 RISC-V 中可以执行的最高权限模式。在机器态下运行的代码对内存、I/O 和一些对于启动和配置系统来说必要的底层功能有着完全的使用权。 30 | > 31 | > **RISC-V 的 S Mode** 32 | > 33 | > Supervisor 模式是支持现代类 Unix 操作系统的权限模式,支持现代类 Unix 操作系统所需要的基于页面的虚拟内存机制是其核心。 34 | > 35 | 36 | 接着我们要在 `_start` 中设置内核的运行环境了,我们直接来看代码: 37 | 38 | {% label %}os/src/entry.asm{% endlabel %} 39 | ```assembly 40 | # 操作系统启动时所需的指令以及字段 41 | # 42 | # 我们在 linker.ld 中将程序入口设置为了 _start,因此在这里我们将填充这个标签 43 | # 它将会执行一些必要操作,然后跳转至我们用 rust 编写的入口函数 44 | # 45 | # 关于 RISC-V 下的汇编语言,可以参考 https://github.com/riscv/riscv-asm-manual/blob/master/riscv-asm.md 46 | 47 | .section .text.entry 48 | .globl _start 49 | # 目前 _start 的功能:将预留的栈空间写入 $sp,然后跳转至 rust_main 50 | _start: 51 | la sp, boot_stack_top 52 | call rust_main 53 | 54 | # 回忆:bss 段是 ELF 文件中只记录长度,而全部初始化为 0 的一段内存空间 55 | # 这里声明字段 .bss.stack 作为操作系统启动时的栈 56 | .section .bss.stack 57 | .global boot_stack 58 | boot_stack: 59 | # 16K 启动栈大小 60 | .space 4096 * 16 61 | .global boot_stack_top 62 | boot_stack_top: 63 | # 栈结尾 64 | ``` 65 | 66 | 可以看到我们在 .bss 中加入了 .stack 段,并在这里分配了一块 $$4096\times{4}\text{\ Bytes}=16 \text{\ KBytes}$$ 的内存作为启动时内核的栈。之前的 .text.entry 也出现了,也就是我们将 `_start` 函数放在了 .text 段的开头。 67 | 68 | 我们看看 `_start` 里面做了什么: 69 | 70 | 1. 修改栈指针寄存器 `sp` 为 .bss.stack 段的结束地址,由于栈是从高地址往低地址增长,所以高地址是初始的栈顶; 71 | 2. 使用 `call` 指令跳转到 `rust_main` 。这意味着我们的内核运行环境设置完成了,正式进入内核。 72 | 73 | 我们将 `os/src/main.rs` 里面的 `_start` 函数删除,并换成 `rust_main` : 74 | 75 | {% label %}os/src/main.rs{% endlabel %} 76 | ```rust 77 | //! # 全局属性 78 | //! - `#![no_std]` 79 | //! 禁用标准库 80 | #![no_std] 81 | //! 82 | //! - `#![no_main]` 83 | //! 不使用 `main` 函数等全部 Rust-level 入口点来作为程序入口 84 | #![no_main] 85 | //! 86 | //! - `#![feature(global_asm)]` 87 | //! 内嵌整个汇编文件 88 | #![feature(global_asm)] 89 | 90 | // 汇编编写的程序入口,具体见该文件 91 | global_asm!(include_str!("entry.asm")); 92 | 93 | use core::panic::PanicInfo; 94 | 95 | /// 当 panic 发生时会调用该函数 96 | /// 我们暂时将它的实现为一个死循环 97 | #[panic_handler] 98 | fn panic(_info: &PanicInfo) -> ! { 99 | loop {} 100 | } 101 | 102 | /// Rust 的入口函数 103 | /// 104 | /// 在 `_start` 为我们进行了一系列准备之后,这是第一个被调用的 Rust 函数 105 | #[no_mangle] 106 | pub extern "C" fn rust_main() -> ! { 107 | loop {} 108 | } 109 | ``` 110 | 111 | 到现在为止我们终于将一切都准备好了,接下来就要配合 OpenSBI 运行我们的内核! -------------------------------------------------------------------------------- /docs/lab-0/guide/part-8.md: -------------------------------------------------------------------------------- 1 | ## 使用 QEMU 运行内核 2 | 3 | ### 使用 OpenSBI 4 | 5 | 新版 QEMU 中内置了 OpenSBI 固件,它主要负责在操作系统运行前的硬件初始化和加载操作系统的功能。我们使用以下命令尝试运行一下: 6 | 7 | {% label %}运行输出{% endlabel %} 8 | ```bash 9 | $ qemu-system-riscv64 \ 10 | --machine virt \ 11 | --nographic \ 12 | --bios default 13 | 14 | OpenSBI v0.6 15 | ____ _____ ____ _____ 16 | / __ \ / ____| _ \_ _| 17 | | | | |_ __ ___ _ __ | (___ | |_) || | 18 | | | | | '_ \ / _ \ '_ \ \___ \| _ < | | 19 | | |__| | |_) | __/ | | |____) | |_) || |_ 20 | \____/| .__/ \___|_| |_|_____/|____/_____| 21 | | | 22 | |_| 23 | 24 | Platform Name : QEMU Virt Machine 25 | Platform HART Features : RV64ACDFIMSU 26 | Platform Max HARTs : 8 27 | Current Hart : 0 28 | Firmware Base : 0x80000000 29 | Firmware Size : 120 KB 30 | Runtime SBI Version : 0.2 31 | 32 | MIDELEG : 0x0000000000000222 33 | MEDELEG : 0x000000000000b109 34 | PMP0 : 0x0000000080000000-0x000000008001ffff (A) 35 | PMP1 : 0x0000000000000000-0xffffffffffffffff (A,R,W,X) 36 | ``` 37 | 38 | 可以看到我们已经在 qemu-system-riscv64 模拟的 QEMU Virt Machine 硬件上将 OpenSBI 这个固件跑起来了。QEMU 可以使用 `ctrl+a` (macOS 为 `control+a`) 再按下 `x` 键退出。 39 | 40 | ### 加载内核镜像 41 | 42 | 为了确信我们已经跑起来了内核里面的代码,我们最好在 `rust_main` 里面加上简单的输出: 43 | 44 | {% label %}os/src/main.rs{% endlabel %} 45 | ```rust 46 | //! # 全局属性 47 | //! - `#![no_std]` 48 | //! 禁用标准库 49 | #![no_std] 50 | //! 51 | //! - `#![no_main]` 52 | //! 不使用 `main` 函数等全部 Rust-level 入口点来作为程序入口 53 | #![no_main] 54 | //! # 一些 unstable 的功能需要在 crate 层级声明后才可以使用 55 | //! - `#![feature(llvm_asm)]` 56 | //! 内嵌汇编 57 | #![feature(llvm_asm)] 58 | //! 59 | //! - `#![feature(global_asm)]` 60 | //! 内嵌整个汇编文件 61 | #![feature(global_asm)] 62 | 63 | // 汇编编写的程序入口,具体见该文件 64 | global_asm!(include_str!("entry.asm")); 65 | 66 | use core::panic::PanicInfo; 67 | 68 | /// 当 panic 发生时会调用该函数 69 | /// 我们暂时将它的实现为一个死循环 70 | #[panic_handler] 71 | fn panic(_info: &PanicInfo) -> ! { 72 | loop {} 73 | } 74 | 75 | /// 在屏幕上输出一个字符,目前我们先不用了解其实现原理 76 | pub fn console_putchar(ch: u8) { 77 | let _ret: usize; 78 | let arg0: usize = ch as usize; 79 | let arg1: usize = 0; 80 | let arg2: usize = 0; 81 | let which: usize = 1; 82 | unsafe { 83 | llvm_asm!("ecall" 84 | : "={x10}" (_ret) 85 | : "{x10}" (arg0), "{x11}" (arg1), "{x12}" (arg2), "{x17}" (which) 86 | : "memory" 87 | : "volatile" 88 | ); 89 | } 90 | } 91 | 92 | /// Rust 的入口函数 93 | /// 94 | /// 在 `_start` 为我们进行了一系列准备之后,这是第一个被调用的 Rust 函数 95 | #[no_mangle] 96 | pub extern "C" fn rust_main() -> ! { 97 | // 在屏幕上输出 "OK\n" ,随后进入死循环 98 | console_putchar(b'O'); 99 | console_putchar(b'K'); 100 | console_putchar(b'\n'); 101 | 102 | loop {} 103 | } 104 | ``` 105 | 106 | 这样,如果我们将内核镜像加载完成后,屏幕上出现了 OK ,就说明我们之前做的事情没有问题。 107 | 108 | 109 | 110 | 现在我们生成内核镜像要通过多条命令来完成,我们可以通过在 os 目录下建立一个 Makefile 来简化这一过程: 111 | 112 | {% label %}os/Makefile{% endlabel %} 113 | ```makefile 114 | TARGET := riscv64imac-unknown-none-elf 115 | MODE := debug 116 | KERNEL_FILE := target/$(TARGET)/$(MODE)/os 117 | BIN_FILE := target/$(TARGET)/$(MODE)/kernel.bin 118 | 119 | OBJDUMP := rust-objdump --arch-name=riscv64 120 | OBJCOPY := rust-objcopy --binary-architecture=riscv64 121 | 122 | .PHONY: doc kernel build clean qemu run 123 | 124 | # 默认 build 为输出二进制文件 125 | build: $(BIN_FILE) 126 | 127 | # 通过 Rust 文件中的注释生成 os 的文档 128 | doc: 129 | @cargo doc --document-private-items 130 | 131 | # 编译 kernel 132 | kernel: 133 | @cargo build 134 | 135 | # 生成 kernel 的二进制文件 136 | $(BIN_FILE): kernel 137 | @$(OBJCOPY) $(KERNEL_FILE) --strip-all -O binary $@ 138 | 139 | # 查看反汇编结果 140 | asm: 141 | @$(OBJDUMP) -d $(KERNEL_FILE) | less 142 | 143 | # 清理编译出的文件 144 | clean: 145 | @cargo clean 146 | 147 | # 运行 QEMU 148 | qemu: build 149 | @qemu-system-riscv64 \ 150 | -machine virt \ 151 | -nographic \ 152 | -bios default \ 153 | -device loader,file=$(BIN_FILE),addr=0x80200000 154 | 155 | # 一键运行 156 | run: build qemu 157 | 158 | ``` 159 | 160 | 这里我们通过参数 `-device` 来将内核镜像加载到 QEMU 中,我们指定了内核镜像文件,并告诉 OpenSBI 最后跳转到 0x80200000 这个入口地址。 161 | 162 | 最后,我们可以使用 `make run` 来用 Qemu 加载内核镜像并运行。匆匆翻过一串长长的 OpenSBI 输出,我们看到了 OK!于是历经了千辛万苦我们终于将我们的内核跑起来了! 163 | 164 | 下一节我们实现格式化输出来使得我们后续能够更加方便的通过输出来进行内核调试。 -------------------------------------------------------------------------------- /docs/lab-0/guide/summary.md: -------------------------------------------------------------------------------- 1 | ## 小结 2 | 3 | 本章作为一个预备实验,用 Rust 实现了一个最小化的内核,并成功通过 QEMU 中的 OpenSBI 启动了我们的内核。在下一章中,我们会和硬件进一步打交道,实现中断机制。 -------------------------------------------------------------------------------- /docs/lab-0/pics/painter.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcore-os/rCore-Tutorial/ceb9688a54a937b4838c5b761874192092d3b361/docs/lab-0/pics/painter.key -------------------------------------------------------------------------------- /docs/lab-0/pics/typical-layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcore-os/rCore-Tutorial/ceb9688a54a937b4838c5b761874192092d3b361/docs/lab-0/pics/typical-layout.png -------------------------------------------------------------------------------- /docs/lab-1/guide/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导一 2 | 3 | ## 实验概要 4 | 5 | 这一章的实验指导中,我们将会学习 6 | 7 | - RISC-V 中有关中断处理的寄存器和相关流程 8 | - 如何保存上下文,使得中断处理流程前后,原本正在执行的程序感知不到发生了中断 9 | - 处理最简单的断点中断和时钟中断 -------------------------------------------------------------------------------- /docs/lab-1/guide/part-1.md: -------------------------------------------------------------------------------- 1 | ## 什么是中断 2 | 3 | 中断是我们在操作系统上首先实现的功能,因为它是操作系统所有功能的基础。假如没有中断,操作系统在唤起一个用户程序之后,就只能等到用户程序执行完成之后才能继续执行,那操作系统完全无法进行资源调度。 4 | 5 | ### 一个比喻 6 | 7 | 操作系统就像家长,他将孩子(用户程序)送到学校(开始运行)之后便不再管。 8 | - 但是如果孩子闯了祸,老师(硬件)就找到家长,这便是**异常** 9 | - 孩子的作业可能需要家长来签字(系统调用),他就会主动找家长,这便是**陷阱** 10 | - 放学时间(时钟中断)到,那么不管孩子想不想回家,家长都会把他接走,这便是**硬件中断** 11 | 12 | ### 中断的种类 13 | 14 | #### 异常(Exception) 15 | 执行指令时产生的,通常无法预料的错误。例如:访问无效内存地址、执行非法指令(除以零)等。 16 | 17 | 有的异常可以恢复,例如缺页异常;有的异常会导致用户程序被终止,例如非法访问。 18 | 19 | #### 陷阱(Trap) 20 | 陷阱是一系列强行导致中断的指令,例如:**系统调用(Syscall)**等。 21 | 22 | #### 硬件中断(Hardware Interrupt) 23 | 前两种都是指令导致的异常,而硬件中断是由 CPU 之外的硬件产生的异步中断,例如:时钟中断、外设发来数据等。 -------------------------------------------------------------------------------- /docs/lab-1/guide/part-2.md: -------------------------------------------------------------------------------- 1 | ## RISC-V 与中断相关的寄存器和指令 2 | 3 | > **[info] 回顾:RISC-V 中的机器态(Machine Mode,机器模式,M 模式)** 4 | > - 是 RISC-V 中的最高权限模式,一些底层操作的指令只能由机器态进行使用。 5 | > - 是所有标准 RISC-V 处理器都必须实现的模式。 6 | > - 默认所有中断实际上是交给机器态处理的,但是为了实现更多功能,机器态会将某些中断交由内核态处理。这些异常也正是我们编写操作系统所需要实现的。 7 | > 8 | > **回顾:RISC-V 中的内核态(Supervisor Mode,内核模式,S 模式)** 9 | > - 通常为操作系统使用,可以访问一些 supervisor 级别的寄存器,通过这些寄存器对中断和虚拟内存映射进行管理。 10 | > - Unix 系统中,大部分的中断都是内核态的系统调用。机器态可以通过异常委托机制(Machine Interrupt Delegation)将一部分中断设置为不经过机器态,直接由内核态处理 11 | 12 | 在实验中,我们主要关心的就是内核态可以使用的一些特权指令和寄存器。其中关于中断的会在本章用到,而关于内存映射的部分将会在第三部分用到。 13 | 14 | ### 与中断相关的寄存器 15 | 16 | 在内核态和机器态中,RISC-V 设计了一些 CSR(Control and Status Registers)寄存器用来保存控制信息。目前我们关心的是其中涉及到控制中断的寄存器。 17 | 18 | #### 发生中断时,硬件自动填写的寄存器 19 | 20 | - `sepc` 21 | 即 Exception Program Counter,用来记录触发中断的指令的地址。 22 | 23 | > 和我们之前学的 MIPS 32 系统不同,RISC-V 中不需要考虑延迟槽的问题。但是 RISC-V 中的指令不定长,如果中断处理需要恢复到异常指令后一条指令进行执行,就需要正确判断将 `pc` 寄存器加上多少字节。 24 | 25 | - `scause` 26 | 记录中断是否是硬件中断,以及具体的中断原因。 27 | 28 | - `stval` 29 | `scause` 不足以存下中断所有的必须信息。例如缺页异常,就会将 `stval` 设置成需要访问但是不在内存中的地址,以便于操作系统将这个地址所在的页面加载进来。 30 | 31 | #### 指导硬件处理中断的寄存器 32 | 33 | - `stvec` 34 | 设置内核态中断处理流程的入口地址。存储了一个基址 BASE 和模式 MODE: 35 | 36 | - MODE 为 0 表示 Direct 模式,即遇到中断便跳转至 BASE 进行执行。 37 | 38 | - MODE 为 1 表示 Vectored 模式,此时 BASE 应当指向一个向量,存有不同处理流程的地址,遇到中断会跳转至 `BASE + 4 * cause` 进行处理流程。 39 | 40 | - `sstatus` 41 | 具有许多状态位,控制全局中断使能等。 42 | 43 | - `sie` 44 | 即 Supervisor Interrupt Enable,用来控制具体类型中断的使能,例如其中的 STIE 控制时钟中断使能。 45 | 46 | - `sip` 47 | 即 Supervisor Interrupt Pending,和 `sie` 相对应,记录每种中断是否被触发。仅当 `sie` 和 `sip` 的对应位都为 1 时,意味着开中断且已发生中断,这时中断最终触发。 48 | 49 | #### `sscratch` 50 | 51 | (这个寄存器的用处会在实现线程时起到作用,目前仅了解即可) 52 | 53 | 在用户态,`sscratch` 保存内核栈的地址;在内核态,`sscratch` 的值为 0。 54 | 55 | 为了能够执行内核态的中断处理流程,仅有一个入口地址是不够的。中断处理流程很可能需要使用栈,而程序当前的用户栈是不安全的。因此,我们还需要一个预设的安全的栈空间,存放在这里。 56 | 57 | 在内核态中,`sp` 可以认为是一个安全的栈空间,`sscratch` 便不需要保存任何值。此时将其设为 0,可以在遇到中断时通过 `sscratch` 中的值判断中断前程序是否处于内核态。 58 | 59 | ### 与中断相关的指令 60 | 61 | #### 进入和退出中断 62 | 63 | - `ecall` 64 | 触发中断,进入更高一层的中断处理流程之中。用户态进行系统调用进入内核态中断处理流程,内核态进行 SBI 调用进入机器态中断处理流程,使用的都是这条指令。 65 | 66 | - `sret` 67 | 从内核态返回用户态,同时将 `pc` 的值设置为 `sepc`。(如果需要返回到 `sepc` 后一条指令,就需要在 `sret` 之前修改 `sepc` 的值) 68 | 69 | - `ebreak` 70 | 触发一个断点。 71 | 72 | - `mret` 73 | 从机器态返回内核态,同时将 `pc` 的值设置为 `mepc`。 74 | 75 | #### 操作 CSR 76 | 77 | 只有一系列特殊的指令(CSR Instruction)可以读写 CSR。尽管所有模式都可以使用这些指令,用户态只能只读的访问某几个寄存器。 78 | 79 | 为了让操作 CSR 的指令不被干扰,许多 CSR 指令都是结合了读写的原子操作。不过在实验中,我们只用到几个简单的指令。 80 | 81 | - `csrrw dst, csr, src`(CSR Read Write) 82 | 同时读写的原子操作,将指定 CSR 的值写入 `dst`,同时将 `src` 的值写入 CSR。 83 | 84 | - `csrr dst, csr`(CSR Read) 85 | 仅读取一个 CSR 寄存器。 86 | 87 | - `csrw csr, src`(CSR Write) 88 | 仅写入一个 CSR 寄存器。 89 | 90 | - `csrc(i) csr, rs1`(CSR Clear) 91 | 将 CSR 寄存器中指定的位清零,`csrc` 使用通用寄存器作为 mask,`csrci` 则使用立即数。 92 | 93 | - `csrs(i) csr, rs1`(CSR Set) 94 | 将 CSR 寄存器中指定的位置 1,`csrc` 使用通用寄存器作为 mask,`csrci` 则使用立即数。 95 | 96 | ### 了解更多 97 | 98 | RISC-V 官方文档: 99 | 100 | - CSR 寄存器(Chapter 4,p59) 101 | https://content.riscv.org/wp-content/uploads/2017/05/riscv-privileged-v1.10.pdf 102 | 103 | - CSR 指令(Section 2.8,p33) 104 | https://content.riscv.org/wp-content/uploads/2017/05/riscv-spec-v2.2.pdf 105 | -------------------------------------------------------------------------------- /docs/lab-1/guide/part-3.md: -------------------------------------------------------------------------------- 1 | ## 程序运行状态 2 | 3 | 对于用户程序而言,中断的处理应当是不留任何痕迹的:只要中断处理改动了一个寄存器,都可能导致原本正在运行的线程出现错误。因此,在处理中断之前,必须要保存所有可能被修改的寄存器,并且在处理完成后恢复。因此,我们需要保存所有通用寄存器,`sepc`、`scause` 和 `stval` 这三个会被硬件自动写入的 CSR 寄存器,以及 `sstatus`。因为中断可能会涉及到权限的切换,以及中断的开关,这些都会修改 `sstatus`。 4 | 5 | ### Context 6 | 7 | 我们把在中断时保存了各种寄存器的结构体叫做 `Context`,他表示原来程序正在执行所在的上下文(这个概念在后面线程的部分还会用到),这里我们和 `scause` 以及 `stval` 作为一个区分,后两者将不会放在 `Context` 而仅仅被看做一个临时的变量(在后面会被用到),`Context` 的定义如下: 8 | 9 | {% label %}os/src/interrupt/context.rs{% endlabel %} 10 | ```rust 11 | use riscv::register::sstatus::Sstatus; 12 | 13 | #[repr(C)] 14 | #[derive(Debug)] 15 | pub struct Context { 16 | pub x: [usize; 32], // 32 个通用寄存器 17 | pub sstatus: Sstatus, 18 | pub sepc: usize 19 | } 20 | ``` 21 | 22 | 这里我们使用了 rCore 中的库 riscv 封装的一些寄存器操作,需要在 `os/Cargo.toml` 中添加依赖。 23 | 24 | {% label %}os/Cargo.toml{% endlabel %} 25 | ```toml 26 | [dependencies] 27 | riscv = { git = "https://github.com/rcore-os/riscv", features = ["inline-asm"] } 28 | ``` -------------------------------------------------------------------------------- /docs/lab-1/guide/part-4.md: -------------------------------------------------------------------------------- 1 | ## 状态的保存与恢复 2 | 3 | ### 操作流程 4 | 5 | 为了状态的保存与恢复,我们可以先用栈上的一小段空间来把需要保存的全部通用寄存器和 CSR 寄存器保存在栈上,保存完之后在跳转到 Rust 编写的中断处理函数;而对于恢复,则直接把备份在栈上的内容写回寄存器。由于涉及到了寄存器级别的操作,我们需要用汇编来实现。 6 | 7 | 而对于如何保存在栈上,我们可以直接令 `sp` 栈寄存器直接减去相应需要开辟的大小,然后依次放在栈上。需要注意的是,`sp` 寄存器又名 `x2`,我们需要不断用到这个寄存器告诉 CPU 其他寄存器放在哪个地址,所以处理这个 `sp` 寄存器本身的保存时也需要格外小心。 8 | 9 | ### 编写汇编 10 | 11 | 因为汇编代码较长,这里我们新建一个 `os/src/interrupt/interrupt.asm` 文件来编写这段操作: 12 | 13 | {% label %}os/src/interrupt/interrupt.asm{% endlabel %} 14 | ```asm 15 | # 我们将会用一个宏来用循环保存寄存器。这是必要的设置 16 | .altmacro 17 | # 寄存器宽度对应的字节数 18 | .set REG_SIZE, 8 19 | # Context 的大小 20 | .set CONTEXT_SIZE, 34 21 | 22 | # 宏:将寄存器存到栈上 23 | .macro SAVE reg, offset 24 | sd \reg, \offset*8(sp) 25 | .endm 26 | 27 | .macro SAVE_N n 28 | SAVE x\n, \n 29 | .endm 30 | 31 | 32 | # 宏:将寄存器从栈中取出 33 | .macro LOAD reg, offset 34 | ld \reg, \offset*8(sp) 35 | .endm 36 | 37 | .macro LOAD_N n 38 | LOAD x\n, \n 39 | .endm 40 | 41 | .section .text 42 | .globl __interrupt 43 | # 进入中断 44 | # 保存 Context 并且进入 Rust 中的中断处理函数 interrupt::handler::handle_interrupt() 45 | __interrupt: 46 | # 在栈上开辟 Context 所需的空间 47 | addi sp, sp, -34*8 48 | 49 | # 保存通用寄存器,除了 x0(固定为 0) 50 | SAVE x1, 1 51 | # 将原来的 sp(sp 又名 x2)写入 2 位置 52 | addi x1, sp, 34*8 53 | SAVE x1, 2 54 | # 保存 x3 至 x31 55 | .set n, 3 56 | .rept 29 57 | SAVE_N %n 58 | .set n, n + 1 59 | .endr 60 | 61 | # 取出 CSR 并保存 62 | csrr t0, sstatus 63 | csrr t1, sepc 64 | SAVE t0, 32 65 | SAVE t1, 33 66 | 67 | # 调用 handle_interrupt,传入参数 68 | # context: &mut Context 69 | mv a0, sp 70 | # scause: Scause 71 | csrr a1, scause 72 | # stval: usize 73 | csrr a2, stval 74 | jal handle_interrupt 75 | 76 | .globl __restore 77 | # 离开中断 78 | # 从 Context 中恢复所有寄存器,并跳转至 Context 中 sepc 的位置 79 | __restore: 80 | # 恢复 CSR 81 | LOAD t0, 32 82 | LOAD t1, 33 83 | csrw sstatus, t0 84 | csrw sepc, t1 85 | 86 | # 恢复通用寄存器 87 | LOAD x1, 1 88 | # 恢复 x3 至 x31 89 | .set n, 3 90 | .rept 29 91 | LOAD_N %n 92 | .set n, n + 1 93 | .endr 94 | 95 | # 恢复 sp(又名 x2)这里最后恢复是为了上面可以正常使用 LOAD 宏 96 | LOAD x2, 2 97 | sret 98 | ``` 99 | 100 | 这样的话我们就完成了对当前执行现场保存,我们把 `Context` 以及 `scause` 和 `stval` 作为参数传入了 `handle_interrupt` 函数中,这是一个 Rust 编写的函数,后面我们将会实现它。 101 | -------------------------------------------------------------------------------- /docs/lab-1/guide/part-5.md: -------------------------------------------------------------------------------- 1 | ## 进入中断处理流程 2 | 3 | 接下来,我们将要手动触发一个 Trap(`ebreak`),并且进入中断处理流程。 4 | 5 | ### 开启中断 6 | 7 | 为了让硬件能够找到我们编写的 `__interrupt` 入口,在操作系统初始化时,需要将其写入 `stvec` 寄存器中: 8 | 9 | {% label %}os/src/interrupt/handler.rs{% endlabel %} 10 | ```rust 11 | use super::context::Context; 12 | use riscv::register::stvec; 13 | 14 | global_asm!(include_str!("./interrupt.asm")); 15 | 16 | /// 初始化中断处理 17 | /// 18 | /// 把中断入口 `__interrupt` 写入 `stvec` 中,并且开启中断使能 19 | pub fn init() { 20 | unsafe { 21 | extern "C" { 22 | /// `interrupt.asm` 中的中断入口 23 | fn __interrupt(); 24 | } 25 | // 使用 Direct 模式,将中断入口设置为 `__interrupt` 26 | stvec::write(__interrupt as usize, stvec::TrapMode::Direct); 27 | } 28 | } 29 | ``` 30 | 31 | ### 处理中断 32 | 33 | 然后,我们再补上 `__interrupt` 后跳转的中断处理流程 `handle_interrupt()`: 34 | 35 | {% label %}os/src/interrupt/handler.rs{% endlabel %} 36 | ```rust 37 | /// 中断的处理入口 38 | /// 39 | /// `interrupt.asm` 首先保存寄存器至 Context,其作为参数和 scause 以及 stval 一并传入此函数 40 | /// 具体的中断类型需要根据 scause 来推断,然后分别处理 41 | #[no_mangle] 42 | pub fn handle_interrupt(context: &mut Context, scause: Scause, stval: usize) { 43 | panic!("Interrupted: {:?}", scause.cause()); 44 | } 45 | ``` 46 | 47 | ### 触发中断 48 | 49 | 最后,我们把刚刚写的函数封装一下: 50 | 51 | {% label %}os/src/interrupt/mod.rs{% endlabel %} 52 | ```rust 53 | //! 中断模块 54 | //! 55 | //! 56 | 57 | mod handler; 58 | mod context; 59 | 60 | /// 初始化中断相关的子模块 61 | /// 62 | /// - [`handler::init`] 63 | /// - [`timer::init`] 64 | pub fn init() { 65 | handler::init(); 66 | println!("mod interrupt initialized"); 67 | } 68 | ``` 69 | 70 | 同时,我们在 main 函数中主动使用 `ebreak` 来触发一个中断。 71 | 72 | {% label %}os/src/main.rs{% endlabel %} 73 | ```rust 74 | ... 75 | mod interrupt; 76 | ... 77 | 78 | /// Rust 的入口函数 79 | /// 80 | /// 在 `_start` 为我们进行了一系列准备之后,这是第一个被调用的 Rust 函数 81 | #[no_mangle] 82 | pub extern "C" fn rust_main() -> ! { 83 | // 初始化各种模块 84 | interrupt::init(); 85 | 86 | unsafe { 87 | llvm_asm!("ebreak"::::"volatile"); 88 | }; 89 | 90 | unreachable!(); 91 | } 92 | ``` 93 | 94 | 运行一下,可以看到 `ebreak` 导致程序进入了中断处理并退出,而没有执行到后面的 `unreachable!()`: 95 | 96 | {% label %}运行输出{% endlabel %} 97 | ``` 98 | Hello rCore-Tutorial! 99 | mod interrupt initialized 100 | panic: 'Interrupted: Exception(Breakpoint)' 101 | ``` -------------------------------------------------------------------------------- /docs/lab-1/guide/part-6.md: -------------------------------------------------------------------------------- 1 | ## 时钟中断 2 | 3 | 本章的最后,我们来实现操作系统中极其重要的时钟中断。时钟中断是操作系统能够进行线程调度的基础,操作系统会在每次时钟中断时被唤醒,暂停正在执行的线程,并根据调度算法选择下一个应当运行的线程。 4 | 5 | > **[info] RISC-V 中断寄存器的细分** 6 | > 7 | > 在[前面](part-2.md#指导硬件处理中断的寄存器)提到,`sie` 和 `sip` 寄存器分别保存不同中断种类的使能和触发记录。例如,软件中断的使能是 `sie` 中的 SSIE 位,触发记录是 `sip` 中的 SSIP 位。 8 | > 9 | > RISC-V 中将中断分为三种: 10 | > - 软件中断(Software Interrupt),对应 SSIE 和 SSIP 11 | > - 时钟中断(Timer Interrupt),对应 STIE 和 STIP 12 | > - 外部中断(External Interrupt),对应 SEIE 和 SEIP 13 | 14 | ### 开启时钟中断 15 | 16 | 时钟中断也需要我们在初始化操作系统时开启,我们同样只需使用 riscv 库中提供的接口即可。 17 | 18 | {% label %}os/src/interrupt/timer.rs{% endlabel %} 19 | ```rust 20 | //! 预约和处理时钟中断 21 | 22 | use crate::sbi::set_timer; 23 | use riscv::register::{time, sie, sstatus}; 24 | 25 | /// 初始化时钟中断 26 | /// 27 | /// 开启时钟中断使能,并且预约第一次时钟中断 28 | pub fn init() { 29 | unsafe { 30 | // 开启 STIE,允许时钟中断 31 | sie::set_stimer(); 32 | // 开启 SIE(不是 sie 寄存器),允许内核态被中断打断 33 | sstatus::set_sie(); 34 | } 35 | // 设置下一次时钟中断 36 | set_next_timeout(); 37 | } 38 | ``` 39 | 40 | 这里可能引起误解的是 `sstatus::set_sie()`,它的作用是开启 `sstatus` 寄存器中的 SIE 位,与 `sie` 寄存器无关。SIE 位决定中断是否能够打断 supervisor 线程。在这里我们需要允许时钟中断打断 内核态线程,因此置 SIE 位为 1。 41 | 另外,无论 SIE 位为什么值,中断都可以打断用户态的线程。 42 | 43 | ### 设置时钟中断 44 | 45 | 每一次的时钟中断都需要操作系统设置一个下一次中断的时间,这样硬件会在指定的时间发出时钟中断。为简化操作系统实现,操作系统可请求(`sbi_call` 调用 `ecall` 指令)SBI 服务来完成时钟中断的设置。OpenSBI 固件在接到 SBI 服务请求后,会帮助 OS 设置下一次要触发时钟中断的时间,CPU 在执行过程中会检查当前的时间间隔是否已经超过设置的时钟中断时间间隔,如果超时则会触发时钟中断。 46 | {% label %}os/src/sbi.rs{% endlabel %} 47 | ```rust 48 | /// 设置下一次时钟中断的时间 49 | pub fn set_timer(time: usize) { 50 | sbi_call(SBI_SET_TIMER, time, 0, 0); 51 | } 52 | ``` 53 | 54 | 为了便于后续处理,我们设置时钟间隔为 100000 个 CPU 周期。越短的间隔可以让 CPU 调度资源更加细致,但同时也会导致更多资源浪费在操作系统上。 55 | 56 | {% label %}os/src/interrupt/timer.rs{% endlabel %} 57 | ```rust 58 | /// 时钟中断的间隔,单位是 CPU 指令 59 | static INTERVAL: usize = 100000; 60 | 61 | /// 设置下一次时钟中断 62 | /// 63 | /// 获取当前时间,加上中断间隔,通过 SBI 调用预约下一次中断 64 | fn set_next_timeout() { 65 | set_timer(time::read() + INTERVAL); 66 | } 67 | ``` 68 | 69 | 由于没有一个接口来设置固定重复的时间中断间隔,因此我们需要在每一次时钟中断时,设置再下一次的时钟中断。 70 | 71 | {% label %}os/src/interrupt/timer.rs{% endlabel %} 72 | ```rust 73 | /// 触发时钟中断计数 74 | pub static mut TICKS: usize = 0; 75 | 76 | /// 每一次时钟中断时调用 77 | /// 78 | /// 设置下一次时钟中断,同时计数 +1 79 | pub fn tick() { 80 | set_next_timeout(); 81 | unsafe { 82 | TICKS += 1; 83 | if TICKS % 100 == 0 { 84 | println!("{} tick", TICKS); 85 | } 86 | } 87 | } 88 | ``` 89 | 90 | ### 实现时钟中断的处理流程 91 | 92 | 接下来,我们在 `handle_interrupt()` 根据不同中断种类进行不同的处理流程。 93 | 94 | {% label %}os/src/interrupt/handler.rs{% endlabel %} 95 | ```rust 96 | use riscv::register::scause::{Exception, Interrupt, Scause, Trap}; 97 | 98 | /// 中断的处理入口 99 | /// 100 | /// `interrupt.asm` 首先保存寄存器至 Context,其作为参数和 scause 以及 stval 一并传入此函数 101 | /// 具体的中断类型需要根据 scause 来推断,然后分别处理 102 | #[no_mangle] 103 | pub fn handle_interrupt(context: &mut Context, scause: Scause, stval: usize) { 104 | // 可以通过 Debug 来查看发生了什么中断 105 | // println!("{:x?}", scause.cause()); 106 | match scause.cause() { 107 | // 断点中断(ebreak) 108 | Trap::Exception(Exception::Breakpoint) => breakpoint(context), 109 | // 时钟中断 110 | Trap::Interrupt(Interrupt::SupervisorTimer) => supervisor_timer(context), 111 | // 其他情况,终止当前线程 112 | _ => fault(context, scause, stval), 113 | } 114 | } 115 | 116 | /// 处理 ebreak 断点 117 | /// 118 | /// 继续执行,其中 `sepc` 增加 2 字节,以跳过当前这条 `ebreak` 指令 119 | fn breakpoint(context: &mut Context) { 120 | println!("Breakpoint at 0x{:x}", context.sepc); 121 | context.sepc += 2; 122 | } 123 | 124 | /// 处理时钟中断 125 | /// 126 | /// 目前只会在 [`timer`] 模块中进行计数 127 | fn supervisor_timer(_: &Context) { 128 | timer::tick(); 129 | } 130 | 131 | /// 出现未能解决的异常 132 | fn fault(context: &mut Context, scause: Scause, stval: usize) { 133 | panic!( 134 | "Unresolved interrupt: {:?}\n{:x?}\nstval: {:x}", 135 | scause.cause(), 136 | context, 137 | stval 138 | ); 139 | } 140 | ``` 141 | 142 | 至此,时钟中断就可以正常工作了。我们在 `os/interrupt/mod.rs` 中引入 `mod timer` 并在 初始化 `handler::init()` 语句的后面加入 `timer::init()` 就成功加载了模块。 143 | 144 | 最后我们在 main 函数中去掉 `unreachable!()`,然后观察时钟中断。应当可以看到程序每隔一秒左右进行一次输出 `100 ticks` `200 ticks`…… 145 | -------------------------------------------------------------------------------- /docs/lab-1/guide/summary.md: -------------------------------------------------------------------------------- 1 | ## 小结 2 | 3 | 本章完成了 RISC-V 中有关中断处理的部分,我们实现了中断相关的上下文保存和切换,使得原来正在的运行的程序不需要做任何处理就可以让操作系统处理好中断或异常。我们进一步完成了简单的断点中断和时钟中断,展示了中断处理的执行过程,为后面的章节(包括系统调用的处理)打下了一定的基础。 4 | 5 | 在下一章节中,我们将从物理内存的管理出发,让操作系统真正可以去管理我们的可以使用的内存。 6 | 7 | 截至目前的所有代码在 `lab-1` 分支中。 -------------------------------------------------------------------------------- /docs/lab-1/practice.md: -------------------------------------------------------------------------------- 1 | ## 实验一:中断 2 | 3 | ### 实验之前 4 | 5 | - 阅读实验指导零和一,最好一步步跟着实现一遍。 6 | - checkout 到仓库中的 `lab-1` 分支,实验题将以此展开。 7 | 8 | > 我们的实验题会提供一个基础的代码框架,以便于进行实验。如果你选择参考教程,自己编写操作系统,这个代码框架也可以用来进行对照。 9 | 10 | ### 实验题 11 | 12 | 16 | 17 | 1. 原理:在 `rust_main` 函数中,执行 `ebreak` 命令后至函数结束前,`sp` 寄存器的值是怎样变化的? 18 | 19 | {% reveal %} 20 | > - `sp` 首先减去一个 `Context` 的大小(入栈),然后原 `sp` 的值被保存到这个入栈的 `Context` 中。 21 | > 22 | > - 执行 `handle_interrupt` 的过程中,随着局部变量的使用,编译器可能会自动加入一些出入栈操作。但无论如何,`handle_interrupt` 前后 `sp` 的值是一样的。 23 | > 24 | > - 从 `handle_interrupt` 返回后,执行 `__restore`,在最后将保存的原 `sp` 值恢复。 25 | {% endreveal %} 26 | 27 |
28 | 2. 分析:如果去掉 `rust_main` 后的 `panic` 会发生什么,为什么? 29 | 30 | {% reveal %} 31 | > `rust_main` 返回后,程序并没有停止。`rust_main` 是在 `entry.asm` 中通过 `jal` 指令调用的,因此其执行完后会回到 `entry.asm` 中。但是,`entry.asm` 并没有在后面写任何指令,这意味着程序将接着向后执行内存中的任何指令。 32 | > 33 | > 我们可以通过 `rust-objdump -d -S os/target/riscv64imac-unknown-none-elf/debug/os | less` 来查看汇编代码,其中就能看到:`_start` 只有短短三条指令,而后面则放着许多 Rust 库中的函数。这些指令可能导致程序进入循环,或崩溃退出。 34 | {% endreveal %} 35 | 36 |
37 | 3. 实验 38 | 1. 如果程序访问不存在的地址,会得到 `Exception::LoadFault`。模仿捕获 `ebreak` 和时钟中断的方法,捕获 `LoadFault`(之后 `panic` 即可)。 39 | 40 | {% reveal %} 41 | > 直接在 `match` 中添加一个 arm 即可。例如 `Trap::Exception(Exception::LoadFault) => panic!()` 42 | {% endreveal %} 43 | 44 |
45 | 2. 在处理异常的过程中,如果程序想要非法访问的地址是 `0x0`,则打印 `SUCCESS!`。 46 | 47 | {% reveal %} 48 | > 如果程序因无效访问内存造成异常,这个访问的地址会被存放在 `stval` 中,而它已经被我们作为参数传入 `handle_interrupt` 了,因此直接判断即可 49 | {% endreveal %} 50 | 51 |
52 | 3. 添加或修改少量代码,使得运行时触发这个异常,并且打印出 `SUCCESS!`。 53 | - 要求:不允许添加或修改任何 unsafe 代码 54 | 55 |
56 | 57 | {% reveal %} 58 | > - 解法 1:在 `interrupt/handler.rs` 的 `breakpoint` 函数中,将 `context.sepc += 2` 修改为 `context.sepc = 0`(则 `sret` 时程序会跳转到 `0x0`) 59 | > - 解法 2:去除 `rust_main` 中的 `panic` 语句,并在 `entry.asm` 的 `jal rust_main` 之后,添加一行读取 `0x0` 地址的指令(例如 `jr x0` 或 `ld x1, (x0)`) 60 | 61 | {% endreveal %} 62 | -------------------------------------------------------------------------------- /docs/lab-2/guide/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导二 2 | 3 | ## 实验概要 4 | 5 | 这一章的实验指导中,你将会学到: 6 | 7 | - 实现动态内存的分配 8 | - 了解 QEMU 模拟的 RISC-V Virt 计算机的物理内存 9 | - 通过页的方式对物理内存进行管理 -------------------------------------------------------------------------------- /docs/lab-2/guide/part-1.md: -------------------------------------------------------------------------------- 1 | ## 动态内存分配 2 | 3 | 我们之前在 C/C++ 语言等中使用过 `malloc/free` 等动态内存分配方法,与在编译期就已完成的静态内存分配相比,动态内存分配可以根据程序运行时状态修改内存申请的时机及大小,显得更为灵活,但是这是需要操作系统的支持的,同时也会带来一些开销。 4 | 5 | 我们的内核中也需要动态内存分配。典型的应用场景有: 6 | 7 | - `Box` ,你可以理解为它和 `malloc` 有着相同的功能; 8 | - 引用计数 `Rc`,原子引用计数 `Arc`,主要用于在引用计数清零,即某对象不再被引用时,对该对象进行自动回收; 9 | - 一些 Rust std 标准库中的数据结构,如 `Vec` 和 `HashMap` 等。 10 | 11 | 我们编写的操作系统不能直接使用 Rust std 标准库提供的动态内存分配功能,因为这些功能需要底层操作系统的支持,这就形成了循环依赖的矛盾了。为了在我们的内核中支持动态内存分配,在 Rust 语言中,我们需要实现 `Trait GlobalAlloc`,将这个类实例化,并使用语义项 `#[global_allocator]` 进行标记。这样的话,编译器就会知道如何使用我们提供的内存分配函数进行动态内存分配。 12 | 13 | 为了实现 `Trait GlobalAlloc`,我们需要支持这么两个函数: 14 | 15 | ```rust 16 | unsafe fn alloc(&self, layout: Layout) -> *mut u8; 17 | unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout); 18 | ``` 19 | 20 | 可见我们要分配/回收一块虚拟内存。 21 | 22 | 那么这里面的 `Layout` 又是什么呢?从文档中可以找到,它有两个字段:`size` 表示要分配的字节数,`align` 则表示分配的虚拟地址的最小对齐要求,即分配的地址要求是 `align` 的倍数。这里的 `align` 必须是 2 的幂次。 23 | 24 | 也就表示,我们的需求是分配一块连续的、大小至少为 `size` 字节的虚拟内存,且对齐要求为 `align` 。 25 | 26 | ### 连续内存分配算法 27 | 28 | 假设我们已经有一整块虚拟内存用来分配,那么如何进行分配呢? 29 | 30 | 我们可能会想到一些简单粗暴的方法,比如对于一个分配任务,贪心地将其分配到可行的最小地址去。这样一直分配下去的话,我们分配出去的内存都是连续的,看上去很合理的利用了内存。 31 | 32 | 但是一旦涉及到回收的话,设想我们在连续分配出去的很多块内存中间突然回收掉一块,它虽然是可用的,但是由于上下两边都已经被分配出去,它就只有这么大而不能再被拓展了,这种可用的内存我们称之为**外碎片**。 33 | 34 | 随着不断回收会产生越来越多的碎片,某个时刻我们可能会发现,需要分配一块较大的内存,几个碎片加起来大小是足够的,但是单个碎片是不够的。我们会想到通过**碎片整理**将几个碎片合并起来。但是这个过程的开销极大。 35 | 36 | 在操作系统课程上,我们了解到还有若干更有效的内存分配算法,包括伙伴系统(Buddy System)和 SLAB 分配器等算法,我们在这里使用 Buddy System 来实现这件事情。 37 | 38 | ### 支持动态内存分配 39 | 40 | 为了避免重复造轮子,我们可以直接开一个静态的 8M 数组作为堆的空间,然后调用 [@jiege](https://github.com/jiegec/) 开发的 [Buddy System Allocator](https://github.com/rcore-os/buddy_system_allocator)。 41 | 42 | {% label %}os/src/memory/config.rs{% endlabel %} 43 | ```rust 44 | /// 操作系统动态分配内存所用的堆大小(8M) 45 | pub const KERNEL_HEAP_SIZE: usize = 0x80_0000; 46 | ``` 47 | 48 | {% label %}os/src/memory/heap.rs{% endlabel %} 49 | ```rust 50 | /// 进行动态内存分配所用的堆空间 51 | /// 52 | /// 大小为 [`KERNEL_HEAP_SIZE`] 53 | /// 这段空间编译后会被放在操作系统执行程序的 bss 段 54 | static mut HEAP_SPACE: [u8; KERNEL_HEAP_SIZE] = [0; KERNEL_HEAP_SIZE]; 55 | 56 | /// 堆,动态内存分配器 57 | /// 58 | /// ### `#[global_allocator]` 59 | /// [`LockedHeap`] 实现了 [`alloc::alloc::GlobalAlloc`] trait, 60 | /// 可以为全局需要用到堆的地方分配空间。例如 `Box` `Arc` 等 61 | #[global_allocator] 62 | static HEAP: LockedHeap = LockedHeap::empty(); 63 | 64 | /// 初始化操作系统运行时堆空间 65 | pub fn init() { 66 | // 告诉分配器使用这一段预留的空间作为堆 67 | unsafe { 68 | HEAP.lock().init( 69 | HEAP_SPACE.as_ptr() as usize, KERNEL_HEAP_SIZE 70 | ) 71 | } 72 | } 73 | 74 | /// 空间分配错误的回调,直接 panic 退出 75 | #[alloc_error_handler] 76 | fn alloc_error_handler(_: alloc::alloc::Layout) -> ! { 77 | panic!("alloc error") 78 | } 79 | ``` 80 | 81 | 同时还有一些模块调用等细节代码,这里不再贴出,请参考完成本章后的仓库中的代码。 82 | 83 | 84 | {% reveal %} 85 | > 提示: 86 | > 87 | > 1. 在 `os/Cargo.toml` 中添加相关的依赖; 88 | > 2. 在 `os/main.rs` 中添加对 Rust 新特性 `alloc_error_handler` 的引用。 89 | {% endreveal %} 90 | 91 | 92 | ### 动态内存分配测试 93 | 94 | 现在我们来测试一下动态内存分配是否有效,分配一个动态数组: 95 | 96 | {% label %}os/src/main.rs{% endlabel %} 97 | ```rust 98 | /// Rust 的入口函数 99 | /// 100 | /// 在 `_start` 为我们进行了一系列准备之后,这是第一个被调用的 Rust 函数 101 | #[no_mangle] 102 | pub extern "C" fn rust_main() -> ! { 103 | // 初始化各种模块 104 | interrupt::init(); 105 | memory::init(); 106 | 107 | // 动态内存分配测试 108 | use alloc::boxed::Box; 109 | use alloc::vec::Vec; 110 | let v = Box::new(5); 111 | assert_eq!(*v, 5); 112 | core::mem::drop(v); 113 | 114 | let mut vec = Vec::new(); 115 | for i in 0..10000 { 116 | vec.push(i); 117 | } 118 | assert_eq!(vec.len(), 10000); 119 | for (i, value) in vec.into_iter().enumerate() { 120 | assert_eq!(value, i); 121 | } 122 | println!("heap test passed"); 123 | 124 | panic!() 125 | } 126 | ``` 127 | 128 | 最后,运行一下会看到 `heap test passed` 类似的输出。有了这个工具之后,后面我们就可以使用一系列诸如 `Vec` 等基于动态分配实现的库中的结构了。 129 | 130 | ### 思考 131 | 132 | 动态分配的内存地址在哪个范围里? 133 | 134 | {% reveal %} 135 | > 在 .bss 段中,因为我们用来存放动态分配的这段是一个静态的没有初始化的数组,算是内核代码的一部分。 136 | {% endreveal %} 137 | 138 | -------------------------------------------------------------------------------- /docs/lab-2/guide/part-2.md: -------------------------------------------------------------------------------- 1 | ## 物理内存探测 2 | 3 | ### 物理内存的相关概念 4 | 5 | 我们知道,物理地址访问的通常是一片 DRAM,我们可以把它看成一个以字节为单位的大数组,通过物理地址找到对应的位置进行读写。但是,物理地址并不仅仅只能访问 DRAM,也可以用来访问其他的外设,因此你也可以认为 DRAM 也算是一种外设,物理地址则是一个对可以存储的介质的一种抽象。 6 | 7 | 而如果访问其他外设要使用不同的指令(如 x86 单独提供了 `in` 和 `out` 等指令来访问不同于内存的 IO 地址空间),会比较麻烦;于是,很多指令集架构(如 RISC-V、ARM 和 MIPS 等)通过 MMIO(Memory Mapped I/O)技术将外设映射到一段物理地址,这样我们访问其他外设就和访问物理内存一样了。 8 | 9 | 我们先不管那些外设,来看物理内存。 10 | 11 | ### 物理内存探测 12 | 13 | 操作系统怎样知道物理内存所在的那段物理地址呢?在 RISC-V 中,这个一般是由 bootloader,即 OpenSBI 固件来完成的。它来完成对于包括物理内存在内的各外设的扫描,将扫描结果以 DTB(Device Tree Blob)的格式保存在物理内存中的某个地方。随后 OpenSBI 固件会将其地址保存在 `a1` 寄存器中,给我们使用。 14 | 15 | 这个扫描结果描述了所有外设的信息,当中也包括 QEMU 模拟的 RISC-V Virt 计算机中的物理内存。 16 | 17 | > **[info] QEMU 模拟的 RISC-V Virt 计算机中的物理内存** 18 | > 19 | > 通过查看 QEMU 代码中 [`hw/riscv/virt.c`](https://github.com/qemu/qemu/blob/master/hw/riscv/virt.c) 的 `virt_memmap[]` 的定义,可以了解到 QEMU 模拟的 RISC-V Virt 计算机的详细物理内存布局。可以看到,整个物理内存中有不少内存空洞(即含义为 unmapped 的地址空间),也有很多外设特定的地址空间,现在我们看不懂没有关系,后面会慢慢涉及到。目前只需关心最后一块含义为 DRAM 的地址空间,这就是 OS 将要管理的 128 MB 的内存空间。 20 | > 21 | > | 起始地址 | 终止地址 | 含义 | 22 | > | :--------: | :--------: | :---------------------------------------------------- | 23 | > | 0x0 | 0x100 | QEMU VIRT_DEBUG | 24 | > | 0x100 | 0x1000 | unmapped | 25 | > | 0x1000 | 0x12000 | QEMU MROM | 26 | > | 0x12000 | 0x100000 | unmapped | 27 | > | 0x100000 | 0x101000 | QEMU VIRT_TEST | 28 | > | 0x101000 | 0x2000000 | unmapped | 29 | > | 0x2000000 | 0x2010000 | QEMU VIRT_CLINT | 30 | > | 0x2010000 | 0x3000000 | unmapped | 31 | > | 0x3000000 | 0x3010000 | QEMU VIRT_PCIE_PIO | 32 | > | 0x3010000 | 0xc000000 | unmapped | 33 | > | 0xc000000 | 0x10000000 | QEMU VIRT_PLIC | 34 | > | 0x10000000 | 0x10000100 | QEMU VIRT_UART0 | 35 | > | 0x10000100 | 0x10001000 | unmapped | 36 | > | 0x10001000 | 0x10002000 | QEMU VIRT_VIRTIO | 37 | > | 0x10002000 | 0x20000000 | unmapped | 38 | > | 0x20000000 | 0x24000000 | QEMU VIRT_FLASH | 39 | > | 0x24000000 | 0x30000000 | unmapped | 40 | > | 0x30000000 | 0x40000000 | QEMU VIRT_PCIE_ECAM | 41 | > | 0x40000000 | 0x80000000 | QEMU VIRT_PCIE_MMIO | 42 | > | 0x80000000 | 0x88000000 | DRAM 缺省 128MB,大小可配置 | 43 | 44 | 不过为了简单起见,我们并不打算自己去解析这个结果。因为我们知道,QEMU 规定的 DRAM 物理内存的起始物理地址为 0x80000000 。而在 QEMU 中,可以使用 `-m` 指定 RAM 的大小,默认是 128 MB 。因此,默认的 DRAM 物理内存地址范围就是 [0x80000000, 0x88000000)。 45 | 46 | 因为后面还会涉及到虚拟地址、物理页和虚拟页面的概念,为了进一步区分而不是简单的只是使用 `usize` 类型来存储,我们首先建立一个 `PhysicalAddress` 的类,然后对其实现一系列的 `usize` 的加、减和输出等等操作,由于这部分实现偏向于 Rust 语法而非 OS,这里不贴出代码,请参考 `os/src/memory/address.rs` 文件。 47 | 48 | 然后,我们直接将 DRAM 物理内存结束地址硬编码到内核中,同时因为我们操作系统本身也用了一部分空间,我们也记录下操作系统用到的地址结尾(即 linker script 中的 `kernel_end`)。 49 | 50 | {% label %}os/src/memory/config.rs{% endlabel %} 51 | ```rust 52 | lazy_static! { 53 | /// 内核代码结束的地址,即可以用来分配的内存起始地址 54 | /// 55 | /// 因为 Rust 语言限制,我们只能将其作为一个运行时求值的 static 变量,而不能作为 const 56 | pub static ref KERNEL_END_ADDRESS: PhysicalAddress = PhysicalAddress(kernel_end as usize); 57 | } 58 | 59 | extern "C" { 60 | /// 由 `linker.ld` 指定的内核代码结束位置 61 | /// 62 | /// 作为变量存在 [`KERNEL_END_ADDRESS`] 63 | fn kernel_end(); 64 | } 65 | ``` 66 | 67 | 这里使用了 `lazy_static` 库,由于 Rust 语言的限制,我们能对编译时 `kernel_end` 做一个求值然后赋值到 `KERNEL_END_ADDRESS` 中;所以,`lazy_static!` 宏帮助我们在第一次使用 `lazy_static!` 宏包裹的变量时自动完成这些求值工作。 68 | 69 | 最后,我们在各级文件中加入模块调用,并在 `os/src/main.rs` 尝试输出。 70 | 71 | {% label %}os/src/main.rs{% endlabel %} 72 | ```rust 73 | /// Rust 的入口函数 74 | /// 75 | /// 在 `_start` 为我们进行了一系列准备之后,这是第一个被调用的 Rust 函数 76 | #[no_mangle] 77 | pub extern "C" fn rust_main() -> ! { 78 | // 初始化各种模块 79 | interrupt::init(); 80 | memory::init(); 81 | 82 | // 注意这里的 KERNEL_END_ADDRESS 为 ref 类型,需要加 * 83 | println!("{}", *memory::config::KERNEL_END_ADDRESS); 84 | 85 | panic!() 86 | } 87 | ``` 88 | 89 | 最后运行,可以看到成功显示了我们内核使用的结尾地址 `PhysicalAddress(0x8020b220)`;注意到这里,你的输出可能因为实现上的细节并不完全一样。 -------------------------------------------------------------------------------- /docs/lab-2/guide/summary.md: -------------------------------------------------------------------------------- 1 | ## 小结 2 | 3 | 本章完成了动态分配内存的管理和物理内存的管理,我们通过划分出一段静态内存为操作系统实现了动态内存的分配;通过页的管理模式,实现了物理页的分配器。 4 | 5 | 本章还只是物理内存的管理,后面为了进一步支持多线程的内存管理,我们将在下一章实现内存的虚拟化。 6 | 7 | 截至目前的所有代码在 `lab-2` 分支中 -------------------------------------------------------------------------------- /docs/lab-2/practice.md: -------------------------------------------------------------------------------- 1 | ## 实验二:内存分配 2 | 3 | ### 实验之前 4 | 5 | - 阅读实验指导二。 6 | - checkout 到仓库中的 `lab-2` 分支,实验题将以此展开。 7 | 8 | ### 实验题 9 | 10 | 1. 原理:.bss 字段是什么含义?为什么我们要将动态分配的内存(堆)空间放在 .bss 字段? 11 | 12 | {% reveal %} 13 | > 对于一个 ELF 程序文件而言,.bss 字段一般包含全局变量的名称和长度,在执行时由操作系统分配空间并初始化为零。 14 | > 15 | > *不过,在我们执行 `rust-objcopy` 时,不同的字段会相应地被处理而形成一段连续的二进制数据,这段二进制数据会直接写入到 QEMU 所模拟的机器的 `0x80200000` 位置。这是因为我们写的操作系统是直接运行在机器上的,而不是一个被操作系统加载的程序。* 16 | > 17 | > 我们一般遇到应用程序的动态内存分配(堆)是由其操作系统提供的。例如在 C 语言中的 `malloc()`,glibc 运行库会维护一个堆空间,而这个空间是通过 `brk()` 等系统调用向内核索要的。由于我们编写操作系统,自然就无法像这样获取空间。但是此时我们具有随意使用内存空间的权力,因此我们可以在内存中随意划一段空间,然后用相应的算法来实现一个堆。 18 | > 19 | > 至于为何堆在 .bss 字段,实际上这也不是必须的——我们完全可以随意指定一段可以访问的内存空间。不过,在代码中用全局变量来表示堆并将其放在 .bss 字段,是一个很简单的实现:这样堆空间就包含在内核的二进制数据之中了,而自 `KERNEL_END_ADDRESS` 以后的空间就都可以给进程使用。 20 | {% endreveal %} 21 | 22 |
23 | 2. 分析:我们在动态内存分配中实现了一个堆,它允许我们在内核代码中使用动态分配的内存,例如 `Vec` `Box` 等。那么,如果我们在实现这个堆的过程中使用 `Vec` 而不是 `[u8]`,会出现什么结果? 24 | 25 | - 无法编译? 26 | 27 | - 运行时错误? 28 | 29 | - 正常运行? 30 |
31 | 32 | {% reveal %} 33 | > **都不会!**程序会陷入一个循环:它需要在堆上分配空间,但是分配器又需要在堆上分配空间…… 34 | {% endreveal %} 35 | 36 |
37 | 3. 实验 38 | 1. 回答:`algorithm/src/allocator` 下有一个 `Allocator` trait,我们之前用它实现了物理页面分配。这个算法的时间和空间复杂度是什么? 39 | 40 | {% reveal %} 41 | > 时间复杂度是 O(1),空间复杂度是 O(n) 42 | {% endreveal %} 43 | 44 |
45 | 2. 二选一:实现基于线段树的物理页面分配算法(不需要考虑合并分配);或尝试修改 `FrameAllocator`,令其使用未被分配的页面空间(而不是全局变量)来存放页面使用状态。 46 | 47 |
48 | 4. 挑战实验(选做) 49 | 1. 在 `memory/heap2.rs` 中,提供了一个手动实现堆的方法。它使用 `algorithm::VectorAllocator` 作为其根本分配算法,而我们目前提供了一个非常简单的 bitmap 算法(而且只开了很小的空间)。请在 `algorithm` crate 中利用伙伴算法实现 `VectorAllocator` trait。 50 | 51 |
52 | 2. 前面说到,堆的实现本身不能完全使用动态内存分配。但有没有可能让堆能够利用动态分配的空间,这样做会带来什么好处? 53 | 54 | {% reveal %} 55 | > 我们以一个朴素的分配器算法为例:将每一次内存分配记录用链表存起来。 56 | > 57 | > 分配器最初必须具有一个节点的静态空间。而每当它仅剩一个节点空间时,都可以用它来为自己分配一块更大的空间。如此,就实现了分配器动态分配自己。 58 | > 59 | > 再考虑到,每次分配 1KB 或 1MB 都需要额外保存一份元信息。如果只用静态分配,就必须按最坏情况(每次都只分配最小单元)来预先留好空间。使用动态分配就可以减少空间浪费。 60 | {% endreveal %} 61 | -------------------------------------------------------------------------------- /docs/lab-3/guide/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导三 2 | 3 | ## 实验概要 4 | 5 | 这一章的实验指导中,你将会学到: 6 | 7 | - 虚拟地址和物理地址的概念和关系 8 | - 利用页表完成虚拟地址到物理地址的映射 9 | - 实现内核的重映射 -------------------------------------------------------------------------------- /docs/lab-3/guide/part-1.md: -------------------------------------------------------------------------------- 1 | ## 从虚拟内存到物理内存 2 | 3 | ### 虚拟地址和物理地址 4 | 5 | 到目前为止,我们简易的操作系统还只是一个内核在执行,还没有多任务的概念。在现代的操作系统中,为了让其他的程序能方便的运行在操作系统上,需要完成的一个很重要的抽象是「每个程序有自己的地址空间,且地址空间范围是一样的」,这将会减少了上层程序的大量麻烦,否则程序本身要维护自己需要的物理内存,这也会导致极大程度的不安全。 6 | 7 | 这个执行上看到的地址空间,就是虚拟内存。而访问虚拟内存的地址就是**虚拟地址(Virtual Address)**,与之对应的是**物理地址(Physical Address)**。这样的设计会导致上层的应用程序可能会访问同一个值相等的虚拟地址,所以操作系统需要做的就是替这些程序维护这个虚拟地址到物理地址的映射。甚者,为了统一和连贯,内核自己本身访问内存也将会通过虚拟地址。 8 | 9 | 我们可以说这个映射的**维护**是操作系统在做,但是大量频繁的访存不可能全部通过调用操作系统的接口来获取真实的物理地址。所以,这里硬件也会参与,帮我们快速**查询**操作系统维护的映射,而这个机制就是页表。 10 | 11 | ![](../pics/rcore_memory_layout.png) 12 | 13 | 如上图所示,这里的图表示了非教学版 rCore 的虚拟地址和物理地址的映射关系。可以看到内核的数据放在了一段高虚拟地址空间,然后会映射到 0x80200000 开始的一段低物理地址空间;而所有的用户程序,将通过操作系统维护的页表映射到不同的物理空间。当然,这只是非教学版 rCore 的设计,在本教程中 kernel layout 和 user layout 会和图有些出入,具体细节可以翻看 linker script。 14 | 15 | ### Sv39 16 | 17 | ![](../pics/sv39_address.png) 18 | 19 | 页表的设计和接口会有很多种,这里我们选择 RISC-V 本身硬件支持的 Sv39 模式作为页表的实现。 20 | 21 | 在 Sv39 模式中,定义物理地址有 56 位,而虚拟地址有 64 位。虽然虚拟地址有 64 位,只有低 39 位有效。不过这不是说高 25 位可以随意取值,规定 63-39 位的值必须等于第 38 位的值,否则会认为该虚拟地址不合法,在访问时会产生异常。 22 | 23 | Sv39 模式同样是基于页的,在物理内存那一节曾经提到**物理页(Frame)**与**物理页号(PPN,Physical Page Number)**。在这里物理页号为 44 位,每个物理页大小为 4KB。同理,我们对于虚拟内存定义**虚拟页(Page)**以及**虚拟页号(VPN, Virtual Page Number)** 。在这里虚拟页号为 27 位,每个虚拟页大小也为 4KB。物理地址和虚拟地址的最后 12 位都表示页内偏移,即表示该地址在所在物理页(虚拟页)上的什么位置。 24 | 25 | 虚拟地址到物理地址的映射以页为单位,也就是说把虚拟地址所在的虚拟页映射到一个物理页,然后再在这个物理页上根据页内偏移找到物理地址,从而完成映射。我们要实现虚拟页到物理页的映射,由于虚拟页与虚拟页号一一对应,物理页帧与物理页号一一对应,本质上我们要实现虚拟页号到物理页号的映射,而这就是页表所做的事情。 26 | 27 | ### 页表项 28 | 29 | ![](../pics/sv39_pte.jpg) 30 | 31 | 一个**页表项(PTE,Page Table Entry)**是用来描述一个虚拟页号如何映射到物理页号的。如果一个虚拟页号通过某种手段找到了一个页表项,并通过读取上面的物理页号完成映射,我们称这个虚拟页号通过该页表项完成映射的。 32 | 33 | 我们可以看到 Sv39 模式里面的一个页表项大小为 64 位(即 8 字节)。其中第 53-10 共 44 位为一个物理页号,表示这个虚拟页号映射到的物理页号。后面的第 9-0 位则描述页的相关状态信息。 34 | 35 | - `V` 表示这个页表项是否合法。如果为 0 表示不合法,此时页表项其他位的值都会被忽略。 36 | 37 | - `R,W,X` 分别表示是否可读(Readable)、可写(Writable)和可执行(Executable)。 38 | 39 | - 以 `W` 这一位为例,如果为零表示不可写,那么如果一条 `store` 的指令,它通过这个页表项完成了虚拟页号到物理页号的映射,找到了物理地址。但是仍然会报出异常,是因为这个页表项规定如果物理地址是通过它映射得到的,执行的行为和页表描述的状态并不一致。 40 | 41 | - 同时,根据 `R,W,X` 取值的不同,我们还有一些特别表示和约定: 42 | 43 | ![](../pics/sv39_rwx.jpg) 44 | 45 | - 也就是说,如果 `R,W,X` 均为 0,文档上说这表示这个页表项指向下一级页表,我们先暂时记住就好。 46 | 47 | - `U` 为 1 表示用户态运行的程序可以通过该页表项完成地址映射。事实上用户态运行的程序也只能够通过 `U` 为 1 的页表项进行虚实地址映射。 48 | 49 | - 然而,我们所处在的 S 态也并不是理所当然的可以访问通过这些 `U` 为 1 的页表项进行映射的用户态内存空间。我们需要将 S 态的状态寄存器 `sstatus` 上的 `SUM (permit Supervisor User Memory access)` 位手动设置为 1 才可以做到这一点。否则 S 态通过的 `load/store` 等指令在访问`U` 为 1 的页表项映射的用合同内存空间时,CPU 会报出异常。 50 | 51 | - `A` 表示 Accessed,如果为 1 则表示自从上次 `A` 被清零后,有虚拟地址通过这个页表项进行读写。 52 | 53 | - `D` 表示 Dirty,如果为 1 表示自从上次 `D` 被清零后,有虚拟地址通过这个页表项进行写入。 54 | 55 | - `RSW` 两位留给 S 态的程序来进行拓展功能实现。 56 | 57 | ### 多级页表 58 | 59 | 一个虚拟页号要通过某种手段找到页表项,那么要怎么才能找到呢? 60 | 61 | 想一种最为简单粗暴的方法,在物理内存中开一个大数组作为页表,把所有虚拟页号对应的页表项都存下来。在找的时候根据虚拟页号来索引页表项。即,假设大数组开头的物理地址为 a,虚拟页号为 $$\text{VPN}$$,则该虚拟页号对应的页表项的物理地址为 $$a+\text{VPN}\times8$$(每个页表项 8 字节)。 62 | 63 | 但是这样会花掉我们大量的内存空间。我们目前只有可怜的 128MB 内存,即使我们有足够的内存也不应该这样去浪费。这是由于有很多虚拟地址我们根本没有用到,因此他们对应的虚拟页号不需要映射,我们浪费了很多无用的内存。 64 | 65 | 事实上,在 Sv39 模式中我们采用三级页表,即将 27 位的虚拟页号分为三个等长的部分,第 26-18 位为三级索引 $$\text{VPN}_2$$,第 17-9 位为二级索引 $$\text{VPN}_1$$,第 8-0 位为一级索引 $$\text{VPN}_0$$。 66 | 67 | 我们也将页表分为三级页表,二级页表,一级页表。每个页表都用 9 位索引的,因此有 $$2^{9}=512$$ 个页表项,而每个页表项都是 8 字节,因此每个页表大小都为 $$512\times 8=4\text{KB}$$。正好是一个物理页的大小。我们可以把一个页表放到一个物理页中,并用一个物理页号来描述它。事实上,三级页表的每个页表项中的物理页号可描述一个二级页表;二级页表的每个页表项中的物理页号可描述一个一级页表;一级页表中的页表项内容则和我们刚才提到的页表项一样,其内容包含物理页号,即描述一个要映射到的物理页。 68 | 69 | 具体来说,假设我们有虚拟地址 $$(\text{VPN}_2, \text{VPN}_1, \text{VPN}_0, \text{offset})$$: 70 | 71 | - 我们首先会记录装载「当前所用的三级页表的物理页」的页号到 `satp` 寄存器中; 72 | - 把 $$\text{VPN}_2$$ 作为偏移在三级页表的物理页中找到第二级页表的物理页号; 73 | - 把 $$\text{VPN}_1$$ 作为偏移在二级页表的物理页中找到第一级页表的物理页号; 74 | - 把 $$\text{VPN}_0$$ 作为偏移在一级页表的物理页中找到要访问位置的物理页号; 75 | - 物理页号对应的物理页基址加上 $$\text{offset}$$ 就是虚拟地址对应的物理地址。 76 | 77 | 上述流程也可以用下图表(来源于 MIT 6.828 课程)示: 78 | ![](../pics/sv39_pagetable.jpg) 79 | 80 | 我们通过这种复杂的手段,终于从虚拟页号找到了一级页表项,从而得出了物理页号。刚才我们提到若页表项满足 `R,W,X` 都为 0,表明这个页表项指向下一级页表。在这里三级和二级页表项的 `R,W,X` 为 0 应该成立,因为它们指向了下一级页表。 81 | 82 | 然而三级和二级页表项不一定要指向下一级页表。我们知道每个一级页表项控制一个虚拟页号,即控制 4KB 虚拟内存;每个二级页表项则控制 9 位虚拟页号,总计控制 $$4\text{KB}\times 2^9=2\text{MB}$$ 虚拟内存;每个三级页表项控制 18 位虚拟页号,总计控制 $$2\text{MB}\times 2^9=1\text{GB}$$ 虚拟内存。我们可以将二级页表项的 `R,W,X` 设置为不是全 0 的,那么它将与一级页表项类似,只不过可以映射一个 2MB 的**大页(Huge Page)**。同理,也可以将三级页表项看作一个叶子,来映射一个 1GB 的大页。这样在 RISC-V 中,可以很方便地建立起大页机制。 83 | 84 | ### 页表基址 85 | 86 | 页表的基址(起始地址)一般会保存在一个特殊的寄存器中。在 RISC-V 中,这个特殊的寄存器就是页表寄存器 satp。 87 | 88 | ![](../pics/sv39_satp.jpg) 89 | 90 | 我们使用寄存器 `satp` 来控制 CPU 进行页表映射。 91 | 92 | - `MODE` 控制 CPU 使用哪种页表实现,我们只需将 `MODE` 设置为 8 即表示 CPU 使用 Sv39 。 93 | - `ASID` 表示地址空间标识符,这里还没有涉及到进程的概念,我们不需要管这个地方。 94 | - `PPN` 存的是三级页表所在的物理页号。这样,给定一个虚拟页号,CPU 就可以从三级页表开始一步步的将其映射到一个物理页号。 95 | 96 | 于是,OS 可以在内存中为不同的应用分别建立不同虚实映射的页表,并通过修改寄存器 `satp` 的值指向不同的页表,从而可以修改 CPU 虚实地址映射关系及内存保护的行为。 97 | 98 | ### 快表(TLB) 99 | 100 | 我们知道,物理内存的访问速度要比 CPU 的运行速度慢很多。如果我们按照页表机制循规蹈矩的一步步走,将一个虚拟地址转化为物理地址需要访问 3 次物理内存,得到物理地址后还需要再访问一次物理内存,才能完成访存。这无疑很大程度上降低了效率。 101 | 102 | 事实上,实践表明虚拟地址的访问具有时间局部性和空间局部性。因此,在 CPU 内部,我们使用**快表(TLB, Translation Lookaside Buffer)**来作为虚拟页号到物理页号的映射的缓存。这部分知识在计算机组成原理课程中有所体现,当我们要做一个映射时,会有很大可能这个映射在近期被完成过,所以我们可以先到 TLB 里面去查一下,如果有的话我们就可以直接完成映射,而不用访问那么多次内存了。 103 | 104 | 但如果修改了 `satp` 寄存器,说明 OS 切换到了一个与先前映射方式完全不同的页表。此时快表里面存储的映射已经失效了,这种情况下 OS 要在修改 `satp` 的指令后面马上使用 `sfence.vma` 指令刷新整个 TLB。 105 | 106 | 同样,我们手动修改一个页表项之后,也修改了映射,但 TLB 并不会自动刷新,我们也需要使用 `sfence.vma` 指令刷新 TLB。如果不加参数的,`sfence.vma` 会刷新整个 TLB。你可以在后面加上一个虚拟地址,这样 `sfence.vma` 只会刷新这个虚拟地址的映射。 107 | -------------------------------------------------------------------------------- /docs/lab-3/guide/summary.md: -------------------------------------------------------------------------------- 1 | ## 小结 2 | 3 | 回顾本章,我们理清了虚拟地址和物理地址的概念和关系;并利用页表完成虚拟地址到物理地址的映射;最后实现了内核空间段的重映射。 4 | 5 | 如果说本章和前一个章节是对空间的划分和管理,那么在下一个小节中,我们将实现对时间的划分和管理,也就是线程。 -------------------------------------------------------------------------------- /docs/lab-3/pics/rcore_memory_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcore-os/rCore-Tutorial/ceb9688a54a937b4838c5b761874192092d3b361/docs/lab-3/pics/rcore_memory_layout.png -------------------------------------------------------------------------------- /docs/lab-3/pics/sv39_address.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcore-os/rCore-Tutorial/ceb9688a54a937b4838c5b761874192092d3b361/docs/lab-3/pics/sv39_address.png -------------------------------------------------------------------------------- /docs/lab-3/pics/sv39_pagetable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcore-os/rCore-Tutorial/ceb9688a54a937b4838c5b761874192092d3b361/docs/lab-3/pics/sv39_pagetable.jpg -------------------------------------------------------------------------------- /docs/lab-3/pics/sv39_pte.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcore-os/rCore-Tutorial/ceb9688a54a937b4838c5b761874192092d3b361/docs/lab-3/pics/sv39_pte.jpg -------------------------------------------------------------------------------- /docs/lab-3/pics/sv39_rwx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcore-os/rCore-Tutorial/ceb9688a54a937b4838c5b761874192092d3b361/docs/lab-3/pics/sv39_rwx.jpg -------------------------------------------------------------------------------- /docs/lab-3/pics/sv39_satp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcore-os/rCore-Tutorial/ceb9688a54a937b4838c5b761874192092d3b361/docs/lab-3/pics/sv39_satp.jpg -------------------------------------------------------------------------------- /docs/lab-3/practice.md: -------------------------------------------------------------------------------- 1 | ## 实验三:虚实地址转换 2 | 3 | ### 实验之前 4 | 5 | - 阅读实验指导三,可以结合 `lab-3` 分支的代码来理解。 6 | - 本次的实验题将使用 `lab-3+` 分支,它包含了后面章节的内容,而我们不需要管那些部分。 7 | 8 | ### 实验题 9 | 10 | 1. 原理:在 `os/src/entry.asm` 中,`boot_page_table` 的意义是什么?当跳转执行 `rust_main` 时,不考虑缓存,硬件通过哪些地址找到了 `rust_main` 的第一条指令? 11 | 12 | {% reveal %} 13 | > 1. `boot_page_table` 是一个用二进制表示的根页表,其中包含两个 1GB 大页,分别是将虚拟地址 `0x8000_0000` 至 `0xc000_0000` 映射到物理地址 `0x8000_0000` 至 `0xc000_0000`,以及将虚拟地址 `0xffff_ffff_8000_0000` 至 `0xffff_ffff_c000_0000` 映射到物理地址 `0x8000_0000` 至 `0xc000_0000`。 14 | > 15 | > 由于我们在 `linker.ld` 中指定了起始地址为 `0xffff_ffff_8020_0000`,操作系统执行文件会认为所有的符号都是在这个高地址上的。但是我们在硬件上只能将内核加载到 `0x8020_0000` 开始的内存空间上,此时的 `pc` 也会调转到这里。 16 | > 17 | > 为了让程序能够正确跳转至高地址的 `rust_main`,我们需要在 `entry.asm` 中先应用内核重映射,即将高地址映射到低地址。但我们不可能在替换页表的同时修改 `pc`,此时 `pc` 仍然处于低地址。所以,页表中的另一项(低地址的恒等映射)则保证程序替换页表后的短暂时间内,`pc` 仍然可以顺着低地址去执行内存中的指令。 18 | > 19 | > *注:如果 `boot_page_table` 中不包含低地址恒等映射,程序可能仍然可以正常运行。这可能和硬件的缓存设计有关。但保险起见,应当保留这两个映射。* 20 | > 21 | > 2. 执行 `jal rust_main` 时,硬件需要加载 `rust_main` 对应的地址,大概是 `0xffff_ffff_802x_xxxx`。 22 | > - 页表已经启用,硬件先从 `satp` 高位置读取内存映射模式,再从 `satp` 低位置读取根页表页号,即 `boot_page_table` 的物理页号 23 | > - 对于 Sv39 模式,页号有三级共 27 位。对于 `rust_main` 而言,一级页号是其 [30:38] 位,即 510。硬件此时定位到根页表的第 510 项 24 | > - 这一项的标志为 XWR,说明它指向一个大页而不是指向下一级页表;目标的页号为 `0x8_0000`,即物理地址 `0x8000_0000` 开始的区间;这一项的 V 位为 1,说明目标在内存中。因此,硬件寻址到页基址 + 页内偏移,即 `0x8000_0000 + 0x2x_xxxx`,找到 `rust_main` 25 | {% endreveal %} 26 | 27 |
28 | 2. 分析:为什么 `Mapping` 中的 `page_tables` 和 `mapped_pairs` 都保存了一些 `FrameTracker`?二者有何不同? 29 | 30 | {% reveal %} 31 | > 页表也是需要我们去分配页面来存储的。因此,`page_tables` 存放了所有页表所用到的页面,而 `mapped_pairs` 则存放了进程所用到的页面。 32 | {% endreveal %} 33 | 34 |
35 | 3. 分析:假设某进程需要虚拟地址 A 到物理地址 B 的映射,这需要操作系统来完成。那么操作系统在建立映射时有没有访问 B?如果有,它是怎么在还没有映射的情况下访问 B 的呢? 36 | 37 | {% reveal %} 38 | > 建立映射不需要访问 B,而只需要操作页表即可。不过,通常程序都会需要操作系统建立映射的同时向页面中加载一些数据。此时,尽管 A→B 的映射尚不存在,因为我们将整个可用物理内存都建立了内核映射,所以操作系统仍然可以通过线性偏移量来访问到 B。 39 | {% endreveal %} 40 | 41 |
42 | 4. 实验:了解并实现时钟页面置换算法(或任何你感兴趣的算法),可以自行设计样例来比较性能 43 | 44 | - 置换算法只需要修改 `os/src/memory/mapping/swapper.rs` 45 | 46 | - 在 `main.rs` 中调用 `create_kernel_thread` 来创建线程,你可以任意修改其中运行的函数,以达到测试效果 47 | -------------------------------------------------------------------------------- /docs/lab-4/guide/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导四 2 | 3 | ## 实验概要 4 | 5 | 这一章的实验指导中,你将会学到: 6 | 7 | - 线程和进程的概念以及运行状态的表示 8 | - 线程的切换 9 | - 对 CPU 进行抽象在上面完成对线程的调度 -------------------------------------------------------------------------------- /docs/lab-4/guide/part-1.md: -------------------------------------------------------------------------------- 1 | ## 线程和进程 2 | 3 | ### 基本概念 4 | 5 | 从**源代码**经过编译器一系列处理(编译、链接、优化等)得到的可执行文件,我们称为**程序(Program)**。而通俗地说,**进程(Process)**就是**正在运行**并**使用计算机资源**的程序,与放在磁盘中一动不动的程序不同:首先,进程得到了操作系统提供的**资源**:程序的代码、数据段被加载到**内存**中,程序所需的虚拟内存空间被真正构建出来。同时操作系统还给进程分配了程序所要求的各种**其他资源**,如我们上面几个章节中提到过的页表、文件的资源。 6 | 7 | 然而如果仅此而已,进程还尚未体现出其“**正在运行**”的动态特性。而正在运行意味着 **CPU** 要去执行程序代码段中的代码,为了能够进行函数调用,我们还需要**运行栈(Stack)**。 8 | 9 | 出于OS对计算机系统精细管理的目的,我们通常将“正在运行”的动态特性从进程中剥离出来,这样的一个借助 CPU 和栈的执行流,我们称之为**线程 (Thread)** 。一个进程可以有多个线程,也可以如传统进程一样只有一个线程。 10 | 11 | 这样,进程虽然仍是代表一个正在运行的程序,但是其主要功能是作为**资源的分配单位**,管理页表、文件、网络等资源。而一个进程的多个线程则共享这些资源,专注于执行,从而作为**执行的调度单位**。举一个例子,为了分配给进程一段内存,我们把一整个页表交给进程,而出于某些目的(比如为了加速需要两个线程放在两个 CPU 的核上),我们需要线程的概念来进一步细化执行的方式,这时进程内部的全部这些线程看到的就是同样的页表,看到的也是相同的地址。但是需要注意的是,这些线程为了可以独立运行,有自己的栈(会放在相同地址空间的不同位置),CPU 也会以它们这些线程为一个基本调度单位。 12 | 13 | ### 线程的表示 14 | 15 | 在不同操作系统中,为每个线程所保存的信息都不同。在这里,我们提供一种基础的实现,每个线程会包括: 16 | 17 | - **线程 ID**:用于唯一确认一个线程,它会在系统调用等时刻用到。 18 | - **运行栈**:每个线程都必须有一个独立的运行栈,保存运行时数据。 19 | - **线程执行上下文**:当线程不在执行时,我们需要保存其上下文(其实就是一堆**寄存器**的值),这样之后才能够将其恢复,继续运行。和之前实现的中断一样,上下文由 `Context` 类型保存。(注:这里的**线程执行上下文**与前面提到的**中断上下文**是不同的概念) 20 | - **所属进程的记号**:同一个进程中的多个线程,会共享页表、打开文件等信息。因此,我们将它们提取出来放到线程中。 21 | - ***内核栈***:除了线程运行必须有的运行栈,中断处理也必须有一个单独的栈。之前,我们的中断处理是直接在原来的栈上进行(我们直接将 `Context` 压入栈)。但是在后面我们会引入用户线程,这时就只有上帝才知道发生了什么——栈指针、程序指针都可能在跨国(**国 == 特权态**)旅游。为了确保中断处理能够进行(让操作系统能够接管这样的线程),中断处理必须运行在一个准备好的、安全的栈上。这就是内核栈。不过,内核栈并没有存储在线程信息中。(注:**它的使用方法会有些复杂,我们会在后面讲解**。) 22 | 23 | {% label %}os/src/process/thread.rs{% endlabel %} 24 | ```rust 25 | /// 线程的信息 26 | pub struct Thread { 27 | /// 线程 ID 28 | pub id: ThreadID, 29 | /// 线程的栈 30 | pub stack: Range, 31 | /// 所属的进程 32 | pub process: Arc, 33 | /// 用 `Mutex` 包装一些可变的变量 34 | pub inner: Mutex, 35 | } 36 | 37 | /// 线程中需要可变的部分 38 | pub struct ThreadInner { 39 | /// 线程执行上下文 40 | /// 41 | /// 当且仅当线程被暂停执行时,`context` 为 `Some` 42 | pub context: Option, 43 | /// 是否进入休眠 44 | pub sleeping: bool, 45 | /// 是否已经结束 46 | pub dead: bool, 47 | } 48 | ``` 49 | 50 | 注意到,因为线程一般使用 `Arc` 来保存,它是不可变的,所以其中再用 `Mutex` 来包装一部分,让这部分可以修改。 51 | 52 | ### 进程的表示 53 | 54 | 在我们实现的简单操作系统中,进程只需要维护页面映射,并且存储一点额外信息: 55 | 56 | - **用户态标识**:我们会在后面进行区分内核态线程和用户态线程。 57 | - **访存空间 `MemorySet`**:进程中的线程会共享同一个页表,即可以访问的虚拟内存空间(简称:访存空间)。 58 | 59 | {% label %}os/src/process/process.rs{% endlabel %} 60 | ```rust 61 | /// 进程的信息 62 | pub struct Process { 63 | /// 是否属于用户态 64 | pub is_user: bool, 65 | /// 用 `Mutex` 包装一些可变的变量 66 | pub inner: Mutex, 67 | } 68 | 69 | pub struct ProcessInner { 70 | /// 进程中的线程公用页表 / 内存映射 71 | pub memory_set: MemorySet, 72 | // /// 打开的文件描述符(实验五) 73 | // pub descriptors: Vec>, 74 | } 75 | ``` 76 | 77 | 同样地,线程也需要一部分是可变的。 78 | 79 | ### 处理器 80 | 81 | 有了线程和进程,现在,我们再抽象出「处理器」来存放和管理线程池。同时,也需要存放和管理目前正在执行的线程(即中断前执行的线程,因为操作系统在工作时是处于中断、异常或系统调用服务之中)。 82 | 83 | {% label %}os/src/process/processor.rs{% endlabel %} 84 | ```rust 85 | /// 线程调度和管理 86 | /// 87 | /// 休眠线程会从调度器中移除,单独保存。在它们被唤醒之前,不会被调度器安排。 88 | pub struct Processor { 89 | /// 当前正在执行的线程 90 | current_thread: Option>, 91 | /// 线程调度器,记录活跃线程 92 | scheduler: SchedulerImpl>, 93 | /// 保存休眠线程 94 | sleeping_threads: HashSet>, 95 | } 96 | ``` 97 | 98 | - `current_thread` 需要保存当前正在运行的线程,这样当出现系统调用的时候,操作系统便可以方便地知道是哪个线程在举手。 99 | - `scheduler` 会负责调度线程,其接口就是简单的“添加”“移除”“获取下一个”,我们会在[后面](part-6.md)详细讲到。 100 | - 休眠线程是指等待一些外部资源(例如硬盘读取、外设读取等)的线程,这时 CPU 如果给其时间片运行是没有意义的,因此它们也就需要移出调度器而单独保存。 101 | 102 | {% label %}os/src/process/processor.rs{% endlabel %} 103 | ```rust 104 | lazy_static! { 105 | /// 全局的 [`Processor`] 106 | pub static ref PROCESSOR: Lock = Lock::new(Processor::default()); 107 | } 108 | ``` 109 | 110 | 注意到这里我们用了一个 `Lock`(`os/process/lock.rs`),它封装了 `spin::Mutex`,而在其基础上进一步关闭了中断。这是因为我们(以后)在内核线程中也有可能访问 `PROCESSOR`,但是此时我们不希望它被时钟打断,这样在中断处理中就无法访问 `PROCESSOR` 了,因为它已经被锁住。 111 | -------------------------------------------------------------------------------- /docs/lab-4/guide/part-2.md: -------------------------------------------------------------------------------- 1 | ## 线程的创建 2 | 3 | 接下来,我们的第一个目标就是创建一个线程并且让他运行起来。一个线程要开始运行,需要这些准备工作: 4 | 5 | - 建立页表映射,需要包括以下映射空间: 6 | - 线程所执行的一段指令 7 | - 线程执行栈 8 | - *操作系统的部分内存空间* 9 | - 设置起始执行的地址 10 | - 初始化各种寄存器,比如 `sp` 11 | - 可选:设置一些执行参数(例如 `argc` 和 `argv`等 ) 12 | 13 | 思考:为什么线程即便与操作系统无关,也需要在内存中映射操作系统的内存空间呢? 14 | 15 | {% reveal %} 16 | > 当发生中断时,需要跳转到 `stvec` 所指向的中断处理过程。如果操作系统的内存不在页表之中,将无法处理中断。 17 | > 18 | > 当然,也不是所有操作系统的代码都需要被映射,但是为了实现简便,我们会为每个进程的页表映射全部操作系统的内存。而由于这些页表都标记为**内核权限**(即 `U` 位为 0),也不必担心用户线程可以随意访问。 19 | {% endreveal %} 20 | 21 | ### 执行第一个线程 22 | 23 | 因为启动线程需要修改各种寄存器的值,所以我们又要使用汇编了。不过,这一次我们只需要对 `interrupt.asm` 稍作修改就可以了。 24 | 25 | 在 `interrupt.asm` 中的 `__restore` 标签现在就能派上用途了。原本这段汇编代码的作用是将之前所保存的 `Context` 恢复到寄存器中,而现在我们让它使用一个精心设计的 `Context`,就可以让程序在恢复后直接进入我们的新线程。 26 | 27 | 首先我们稍作修改,添加一行 `mv sp, a0`。原本这里是读取之前存好的 `Context`,现在我们让其从 `a0` 中读取我们设计好的 `Context`。这样,我们可以直接在 Rust 代码中调用 `__restore(context)`。 28 | 29 | {% label %}os/src/interrupt/interrupt.asm{% endlabel %} 30 | ```asm 31 | __restore: 32 | mv sp, a0 # 加入这一行 33 | # ... 34 | ``` 35 | 36 | #### 那么我们需要如何设计 `Context` 呢? 37 | 38 | - 通用寄存器 39 | - `sp`:应当指向该线程的栈顶 40 | - `a0`-`a7`:按照函数调用规则,用来传递参数 41 | - `ra`:线程执行完应该跳转到哪里呢?在后续**系统调用**章节我们会介绍正确的处理方式。现在,我们先将其设为一个不可执行的地址,这样线程一结束就会触发页面异常 42 | - `sepc` 43 | - 执行 `sret` 指令后会跳转到这里,所以 `sepc` 应当存储线程的入口地址(执行的函数地址) 44 | - `sstatus` 45 | - `spp` 位按照用户态或内核态有所不同 46 | - `spie` 位为 1 47 | 48 | > **[info] `sstatus` 标志位的具体意义** 49 | > 50 | > - `spp`:中断前系统处于内核态(1)还是用户态(0) 51 | > - `sie`:内核态是否允许中断。对用户态而言,无论 `sie` 取何值都开启中断 52 | > - `spie`:中断前是否开中断(用户态中断时可能 `sie` 为 0) 53 | > 54 | > **硬件处理流程** 55 | > 56 | > - 在中断发生时,系统要切换到内核态。此时,**切换前的状态**会被保存在 **`spp`** 位中(1 表示切换前处于内核态)。同时,**切换前是否开中断**会被保存在 **`spie`** 位中,而 `sie` 位会被置 0,表示关闭中断。 57 | > - 在中断结束,执行 `sret` 指令时,会根据 `spp` 位的值决定 `sret` 执行后是处于内核态还是用户态。与此同时,`spie` 位的值会被写入 `sie` 位,而 `spie` 位置 1。这样,特权状态和中断状态就全部恢复了。 58 | > 59 | > **为何如此繁琐?** 60 | > 61 | > - 特权状态: 62 | > 中断处理流程必须切换到内核态,所以中断时需要用 `spp` 来保存之前的状态。 63 | > 回忆计算机组成原理的知识,`sret` 指令必须同时完成跳转并切换状态的工作。 64 | > - 中断状态: 65 | > 中断刚发生时,必须关闭中断,以保证现场保存的过程不会被干扰。同理,现场恢复的过程也必须关中断。因此,需要有以上两个硬件自动执行的操作。 66 | > 由于中断可能嵌套,在保存现场后,根据中断的种类,可能会再开启部分中断的使能。 67 | 68 | 设计好 `Context` 之后,我们只需要将它应用到所有的寄存器上(即执行 `__restore`),就可以切换到第一个线程了。 69 | 70 | {% label %}os/src/main.rs: rust_main(){% endlabel %} 71 | ```rust 72 | extern "C" { 73 | fn __restore(context: usize); 74 | } 75 | // 获取第一个线程的 Context,具体原理后面讲解 76 | let context = PROCESSOR.lock().prepare_next_thread(); 77 | // 启动第一个线程 78 | unsafe { __restore(context as usize) }; 79 | unreachable!() 80 | ``` 81 | 82 | #### 为什么 `unreachable` 83 | 84 | 我们直接调用的 `__restore` 并没有 `ret` 指令,甚至 `ra` 都会被 `Context` 中的数值直接覆盖。这意味着,一旦我们执行了 `__restore(context)`,程序就无法返回到调用它的位置了。**注:直接 jump 是一个非常危险的操作**。 85 | 86 | 但是没有关系,我们也不需要这个函数返回。因为开始执行第一个线程,意味着操作系统的初始化已经完成,再回到 `rust_main()` 也没有意义了。甚至原本我们使用的栈 `bootstack`,也可以被回收(不过我们现在就丢掉不管吧)。 87 | 88 | #### 在启动时不打开中断 89 | 90 | 现在,我们会在线程开始运行时开启中断,而在操作系统初始化的过程中是不应该有中断的。所以,我们删去之前设置「开启中断」的代码。 91 | 92 | {% label %}os/interrupt/timer.rs{% endlabel %} 93 | ```rust 94 | /// 初始化时钟中断 95 | /// 96 | /// 开启时钟中断使能,并且预约第一次时钟中断 97 | pub fn init() { 98 | unsafe { 99 | // 开启 STIE,允许时钟中断 100 | sie::set_stimer(); 101 | // (删除)开启 SIE(不是 sie 寄存器),允许内核态被中断打断 102 | // sstatus::set_sie(); 103 | } 104 | // 设置下一次时钟中断 105 | set_next_timeout(); 106 | } 107 | ``` 108 | 109 | ### 小结 110 | 111 | 为了执行一个线程,我们需要初始化所有寄存器的值。为此,我们选择构建一个 `Context` 然后跳转至 `interrupt.asm` 中的 `__restore` 来执行,用这个 `Context` 来写入所有寄存器。 112 | 113 | #### 思考 114 | 115 | `__restore` 现在会将 `a0` 寄存器视为一个 `*mut Context` 来读取,因此我们在执行第一个线程时只需调用 `__restore(context)`。 116 | 117 | 那么,如果是程序发生了中断,执行到 `__restore` 的时候,`a0` 的值又是谁赋予的呢? 118 | -------------------------------------------------------------------------------- /docs/lab-4/guide/part-3.md: -------------------------------------------------------------------------------- 1 | ## 线程的切换 2 | 3 | 回答一下前一节的思考题:当发生中断时,在 `__restore` 时,`a0` 寄存器的值是 `handle_interrupt` 函数的返回值。也就是说,如果我们令 `handle_interrupt` 函数返回另一个线程的 `*mut Context`,就可以在时钟中断后跳转到这个线程来执行。 4 | 5 | ### 修改中断处理 6 | 7 | 在线程切换时(即时钟中断时),`handle_interrupt` 函数需要将上一个线程的 `Context` 保存起来,然后将下一个线程的 `Context` 恢复并返回。 8 | 9 | > 注 1:为什么不直接 in-place 修改 `Context` 呢?这是因为 `handle_interrupt` 函数返回的 `Context` 指针除了存储上下文以外,还提供了内核栈的地址。这个会在后面详细阐述。 10 | > 11 | > 注 2:在 Rust 中,引用 `&mut` 和指针 `*mut` 只是编译器的理解不同,其本质都是一个存储对象地址的寄存器。这里返回值使用指针而不是引用,是因为其指向的位置十分特殊,其生命周期在这里没有意义。 12 | 13 | {% label %}os/src/interrupt/handler.rs{% endlabel %} 14 | ```rust 15 | /// 中断的处理入口 16 | #[no_mangle] 17 | pub fn handle_interrupt(context: &mut Context, scause: Scause, stval: usize) -> *mut Context { 18 | /* ... */ 19 | } 20 | 21 | /// 处理 ebreak 断点 22 | fn breakpoint(context: &mut Context) -> *mut Context { 23 | println!("Breakpoint at 0x{:x}", context.sepc); 24 | context.sepc += 2; 25 | context 26 | } 27 | 28 | /// 处理时钟中断 29 | fn supervisor_timer(context: &mut Context) -> *mut Context { 30 | timer::tick(); 31 | PROCESSOR.lock().park_current_thread(context); 32 | PROCESSOR.lock().prepare_next_thread() 33 | } 34 | ``` 35 | 36 | 可以看到,当发生断点中断时,直接返回原来的上下文(修改一下 `sepc`);而如果是时钟中断的时候,我们执行了两个函数得到的返回值作为上下文,那它又是怎么工作的呢? 37 | 38 | ### 线程切换 39 | 40 | 让我们看一下 `Processor` 中的这两个方法是如何实现的。 41 | 42 | (调度器 `scheduler` 会在后面的小节中讲解,我们只需要知道它能够返回下一个等待执行的线程。) 43 | 44 | {% label %}os/src/process/processor.rs: impl Processor{% endlabel %} 45 | ```rust 46 | /// 保存当前线程的 `Context` 47 | pub fn park_current_thread(&mut self, context: &Context) { 48 | self.current_thread().park(*context); 49 | } 50 | 51 | /// 在一个时钟中断时,替换掉 context 52 | pub fn prepare_next_thread(&mut self) -> *mut Context { 53 | // 向调度器询问下一个线程 54 | if let Some(next_thread) = self.scheduler.get_next() { 55 | // 准备下一个线程 56 | let context = next_thread.prepare(); 57 | self.current_thread = Some(next_thread); 58 | context 59 | } else { 60 | // 没有活跃线程 61 | if self.sleeping_threads.is_empty() { 62 | // 也没有休眠线程,则退出 63 | panic!("all threads terminated, shutting down"); 64 | } else { 65 | // 有休眠线程,则等待中断 66 | /* ... */ 67 | } 68 | } 69 | } 70 | ``` 71 | 72 | #### 上下文 `Context` 的保存和取出 73 | 74 | 在线程切换时,我们需要保存前一个线程的 `Context`,为此我们实现 `Thread::park` 函数。 75 | 76 | {% label %}os/src/process/thread.rs: impl Thread{% endlabel %} 77 | ```rust 78 | /// 发生时钟中断后暂停线程,保存状态 79 | pub fn park(&self, context: Context) { 80 | // 检查目前线程内的 context 应当为 None 81 | assert!(self.inner().context.is_none()); 82 | // 将 Context 保存到线程中 83 | self.inner().context.replace(context); 84 | } 85 | ``` 86 | 87 | 然后,我们需要取出下一个线程的 `Context`,为此我们实现 `Thread::prepare`。不过这次需要注意的是,启动一个线程除了需要 `Context`,还需要切换页表。这个操作我们也在这个方法中完成。 88 | 89 | {% label %}os/src/process/thread.rs: impl Thread{% endlabel %} 90 | ```rust 91 | /// 准备执行一个线程 92 | /// 93 | /// 激活对应进程的页表,并返回其 Context 94 | pub fn prepare(&self) -> *mut Context { 95 | // 激活页表 96 | self.process.inner().memory_set.activate(); 97 | // 取出 Context 98 | let parked_frame = self.inner().context.take().unwrap(); 99 | // 将 Context 放至内核栈顶 100 | unsafe { KERNEL_STACK.push_context(parked_frame) } 101 | } 102 | ``` 103 | 104 | 思考:在 `run` 函数中,我们在一开始就激活了页表,会不会导致后续流程无法正常执行? 105 | 106 | #### 内核栈? 107 | 108 | 现在,线程保存 `Context` 都是根据 `sp` 指针,在栈上压入一个 `Context` 来存储。但是,对于一个用户线程而言,它在用户态运行时用的是位于用户空间的用户栈。而它在用户态运行中如果触发中断,`sp` 指针指向的是用户空间的某地址,但此时 RISC-V CPU 会切换到内核态继续执行,就不能再用这个 `sp` 指针指向的用户空间地址了。这样,我们需要为 sp 指针准备好一个专门用于在内核态执行函数的内核栈。所以,为了不让一个线程的崩溃导致操作系统的崩溃,我们需要提前准备好内核栈,当线程发生中断时可用来存储线程的 `Context`。在下一节我们将具体讲解该如何做。 109 | 110 | ### 小结 111 | 112 | 为了实现线程的切换,我们让 `handle_interrupt` 返回一个 `*mut Context`。如果需要切换线程,就将前一个线程的 `Context` 保存起来换上新的线程的 `Context`。而如果不需要切换,那么直接返回原本的 `Context` 即可。 113 | -------------------------------------------------------------------------------- /docs/lab-4/guide/part-4.md: -------------------------------------------------------------------------------- 1 | ## 线程的结束 2 | 3 | ### 现有问题 4 | 5 | 当内核线程终止时,会发生什么?如果就按目前的实现,我们会发现线程所执行的函数末尾会触发 `Exception::InstructionPageFault` 而终止,其中访问的的地址 `stval = 0`。 6 | 7 | 这是因为内核线程在执行完 `entry_point` 所指向的函数后会返回到 `ra` 指向的地址,而我们没有为其赋初值(初值为 0)。此时,程序就会尝试跳转到 `0x0` 地址,而显然它是不存在的。 8 | 9 | ### 解决办法 10 | 11 | 很自然的,我们希望能够让内核线程在结束时触发一个友善的中断(而不是一个看上去像是错误的缺页异常),然后被操作系统释放。我们可能会想到系统调用,但很可惜我们无法使用它,因为系统调用的本质是一个环境调用 `ecall`,而在内核线程(内核态)中进行的环境调用是用来与 M 态通信的。我们之前实现的 SBI 调用就是使用的 S 态 `ecall`。 12 | 13 | 因此,我们设计一个折衷的解决办法:内核线程将自己标记为“已结束”,同时触发一个普通的异常 `ebreak`。此时操作系统观察到线程的标记,便将其终止。 14 | 15 | {% label %}os/src/main.rs{% endlabel %} 16 | ```rust 17 | /// 内核线程需要调用这个函数来退出 18 | fn kernel_thread_exit() { 19 | // 当前线程标记为结束 20 | PROCESSOR.lock().current_thread().as_ref().inner().dead = true; 21 | // 制造一个中断来交给操作系统处理 22 | unsafe { llvm_asm!("ebreak" :::: "volatile") }; 23 | } 24 | ``` 25 | 26 | 然后,我们将这个函数作为内核线程的 `ra`,使得它执行的函数完成后便执行 `kernel_thread_exit()` 27 | 28 | {% label %}os/src/main.rs{% endlabel %} 29 | ```rust 30 | /// 创建一个内核进程 31 | pub fn create_kernel_thread( 32 | process: Arc, 33 | entry_point: usize, 34 | arguments: Option<&[usize]>, 35 | ) -> Arc { 36 | // 创建线程 37 | let thread = Thread::new(process, entry_point, arguments).unwrap(); 38 | // 设置线程的返回地址为 kernel_thread_exit 39 | thread.as_ref().inner().context.as_mut().unwrap() 40 | .set_ra(kernel_thread_exit as usize); 41 | thread 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/lab-4/guide/part-5.md: -------------------------------------------------------------------------------- 1 | ## 内核栈 2 | 3 | ### 为什么 / 怎么做 4 | 5 | 在实现内核栈之前,让我们先检查一下需求和我们的解决办法。 6 | 7 | - **不是每个线程都需要一个独立的内核栈**,因为内核栈只会在中断时使用,而中断结束后就不再使用。在只有一个 CPU 的情况下,不会有两个线程同时出现中断,**所以我们只需要实现一个共用的内核栈就可以了**。 8 | - **每个线程都需要能够在中断时第一时间找到内核栈的地址**。这时,所有通用寄存器的值都无法预知,也无法从某个变量来加载地址。为此,**我们将内核栈的地址存放到内核态使用的特权寄存器 `sscratch` 中**。这个寄存器只能在内核态访问,这样在中断发生时,就可以安全地找到内核栈了。 9 | 10 | 因此,我们的做法就是: 11 | 12 | - 预留一段空间作为内核栈 13 | - 运行线程时,在 `sscratch` 寄存器中保存内核栈指针 14 | - 如果线程遇到中断,则从将 `Context` 压入 `sscratch` 指向的栈中(`Context` 的地址为 `sscratch - size_of::()`),同时用新的栈地址来替换 `sp`(此时 `sp` 也会被复制到 `a0` 作为 `handle_interrupt` 的参数) 15 | - 从中断中返回时(`__restore` 时),`a0` 应指向**被压在内核栈中的 `Context`**。此时出栈 `Context` 并且将栈顶保存到 `sscratch` 中 16 | 17 | ### 实现 18 | 19 | #### 为内核栈预留空间 20 | 21 | 我们直接使用一个 `static mut` 来指定一段空间作为栈。 22 | 23 | {% label %}os/src/process/kernel_stack.rs{% endlabel %} 24 | ```rust 25 | /// 内核栈 26 | #[repr(align(16))] 27 | #[repr(C)] 28 | pub struct KernelStack([u8; KERNEL_STACK_SIZE]); 29 | 30 | /// 公用的内核栈 31 | pub static mut KERNEL_STACK: KernelStack = KernelStack([0; STACK_SIZE]); 32 | ``` 33 | 34 | 在我们创建线程时,需要使用的操作就是在内核栈顶压入一个初始状态 `Context`: 35 | 36 | {% label %}os/src/process/kernel_stack.rs{% endlabel %} 37 | ```rust 38 | impl KernelStack { 39 | /// 在栈顶加入 Context 并且返回新的栈顶指针 40 | pub fn push_context(&mut self, context: Context) -> *mut Context { 41 | // 栈顶 42 | let stack_top = &self.0 as *const _ as usize + size_of::(); 43 | // Context 的位置 44 | let push_address = (stack_top - size_of::()) as *mut Context; 45 | unsafe { 46 | *push_address = context; 47 | } 48 | push_address 49 | } 50 | } 51 | ``` 52 | 53 | #### 修改 `interrupt.asm` 54 | 55 | 在这个汇编代码中,我们需要加入对 `sscratch` 的判断和使用。 56 | 57 | {% label %}os/src/interrput/interrupt.asm{% endlabel %} 58 | ```asm 59 | __interrupt: 60 | # 因为线程当前的栈不一定可用,必须切换到内核栈来保存 Context 并进行中断流程 61 | # 因此,我们使用 sscratch 寄存器保存内核栈地址 62 | # 思考:sscratch 的值最初是在什么地方写入的? 63 | 64 | # 交换 sp 和 sscratch(切换到内核栈) 65 | csrrw sp, sscratch, sp 66 | # 在内核栈开辟 Context 的空间 67 | addi sp, sp, -36*8 68 | 69 | # 保存通用寄存器,除了 x0(固定为 0) 70 | SAVE x1, 1 71 | # 将本来的栈地址 sp(即 x2)保存 72 | csrr x1, sscratch 73 | SAVE x1, 2 74 | 75 | # ... 76 | ``` 77 | 78 | 以及事后的恢复: 79 | 80 | {% label %}os/src/interrupt/interrupt.asm{% endlabel %} 81 | ```asm 82 | # 离开中断 83 | # 此时内核栈顶被推入了一个 Context,而 a0 指向它 84 | # 接下来从 Context 中恢复所有寄存器,并将 Context 出栈(用 sscratch 记录内核栈地址) 85 | # 最后跳转至恢复的 sepc 的位置 86 | __restore: 87 | # 从 a0 中读取 sp 88 | # 思考:a0 是在哪里被赋值的?(有两种情况) 89 | mv sp, a0 90 | # 恢复 CSR 91 | LOAD t0, 32 92 | LOAD t1, 33 93 | csrw sstatus, t0 94 | csrw sepc, t1 95 | # 将内核栈地址写入 sscratch 96 | addi t0, sp, 36*8 97 | csrw sscratch, t0 98 | 99 | # 恢复通用寄存器 100 | # ... 101 | ``` 102 | 103 | ### 小结 104 | 105 | 为了能够鲁棒地处理用户线程产生的异常,我们为线程准备好一个内核栈,发生中断时会切换到这里继续处理。 106 | 107 | #### 思考 108 | 109 | 在栈的切换过程中,会不会导致一些栈空间没有被释放,或者被错误释放的情况? 110 | -------------------------------------------------------------------------------- /docs/lab-4/guide/part-6.md: -------------------------------------------------------------------------------- 1 | ## 调度器 2 | 3 | 调度器的算法有许多种,我们将它提取出一个 trait 作为接口 4 | 5 | {% label %}os/src/algorithm/src/scheduler/mod.rs{% endlabel %} 6 | ```rust 7 | /// 线程调度器 8 | /// 9 | /// 这里 `ThreadType` 就是 `Arc` 10 | pub trait Scheduler: Default { 11 | /// 优先级的类型 12 | type Priority; 13 | /// 向线程池中添加一个线程 14 | fn add_thread(&mut self, thread: ThreadType); 15 | /// 获取下一个时间段应当执行的线程 16 | fn get_next(&mut self) -> Option; 17 | /// 移除一个线程 18 | fn remove_thread(&mut self, thread: &ThreadType); 19 | /// 设置线程的优先级 20 | fn set_priority(&mut self, thread: ThreadType, priority: Self::Priority); 21 | } 22 | ``` 23 | 24 | 具体的算法就不在此展开了,我们可以参照目录 `os/src/algorithm/src/scheduler` 下的一些样例。 25 | 26 | ### 运行! 27 | 28 | 修改 `main.rs`,我们就可以跑起来多线程了。 29 | 30 | {% label %}os/src/main.rs{% endlabel %} 31 | ```rust 32 | #[no_mangle] 33 | pub extern "C" fn rust_main() -> ! { 34 | memory::init(); 35 | interrupt::init(); 36 | 37 | { 38 | let mut processor = PROCESSOR.lock(); 39 | // 创建一个内核进程 40 | let kernel_process = Process::new_kernel().unwrap(); 41 | // 为这个进程创建多个线程,并设置入口均为 sample_process,而参数不同 42 | for i in 1..9usize { 43 | processor.add_thread(create_kernel_thread( 44 | kernel_process.clone(), 45 | sample_process as usize, 46 | Some(&[i]), 47 | )); 48 | } 49 | } 50 | 51 | extern "C" { 52 | fn __restore(context: usize); 53 | } 54 | // 获取第一个线程的 Context 55 | let context = PROCESSOR.lock().prepare_next_thread(); 56 | // 启动第一个线程 57 | unsafe { __restore(context as usize) }; 58 | unreachable!() 59 | } 60 | 61 | fn sample_process(id: usize) { 62 | println!("hello from kernel thread {}", id); 63 | } 64 | ``` 65 | 66 | 运行一下,我们会得到类似的输出: 67 | 68 | {% label %}运行输出{% endlabel %} 69 | ``` 70 | hello from kernel thread 7 71 | thread 7 exit 72 | hello from kernel thread 6 73 | thread 6 exit 74 | hello from kernel thread 5 75 | thread 5 exit 76 | hello from kernel thread 4 77 | thread 4 exit 78 | hello from kernel thread 3 79 | thread 3 exit 80 | hello from kernel thread 2 81 | thread 2 exit 82 | hello from kernel thread 1 83 | thread 1 exit 84 | hello from kernel thread 8 85 | thread 8 exit 86 | src/process/processor.rs:87: 'all threads terminated, shutting down' 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/lab-4/guide/summary.md: -------------------------------------------------------------------------------- 1 | ## 小结 2 | 3 | 本章我们的工作有: 4 | 5 | - 理清线程和进程的概念 6 | - 通过设置 `Context`,可以构造一个线程的初始状态 7 | - 通过 `__restore` 标签,直接进入第一个线程之中 8 | - 用 `Context` 来保存进程的状态,从而实现在时钟中断时切换线程 9 | - 实现内核栈,提供安全的中断处理空间 10 | - 实现调度器,完成线程的调度 11 | 12 | 同时,可以发现我们这一章的内容集中在内核线程上面,对用户进程还没有过多的提及。而为了让用户进程可以在我们的系统上运行起来,一个优美的做法将会是隔开用户程序和内核。需要注意到现在的内核还直接放在了内存上,在下一个章节,我们暂时跳过用户进程,实现可以放置用户数据的文件系统。 13 | 14 | ### 思考 15 | 16 | 可以看到我们的设计中用了大量的锁结构,很多都是为了让 Rust 知道我们是安全的,而且大部分情况下我们**仅仅**会在中断发生的时候来使用这些逻辑,这意味着,只要内核线程里面不用,就不会发生死锁,但是真的是这样吗?即使我们不在内核中使用各种 `Processor` 和 `Thread` 等等的逻辑,仅仅完成一些简单的运算,真的没有死锁吗? 17 | 18 | {% reveal %} 19 | > 会有死锁,比如我们在内核线程中构造一个 `Vec`,然后在里面 push 几个元素,这个时候就可能产生死锁。 20 | > 21 | > 需要注意到,我们的动态分配器是一个 `LockedHeap`,是外面加了锁的一个分配器,如果在线程里面 push 的过程中需要动态分配,然后正好在上完锁而且没有释放锁的时候产生了中断,而中断中我们的 `Scheduler` 也用到了 `Vec`,这个时候会再去锁住,但是又拿不到,同时需要注意的是在处理中断本身时,我们的时钟中断是关掉的,这意味着我们的锁会一直去申请,就形成了类似死锁的死循环。 22 | > 23 | > 解决这个问题需要把申请到锁之后加上关闭中断,通过这种抢占式的方法彻底执行完分配逻辑之后再关闭锁同时打开中断。这个问题是一个设计上的取舍,如果我们不支持内核抢占,就需要很多精妙的设计来绕开这个问题。在这里,我们先不会理会这个问题。 24 | > 25 | {% endreveal %} -------------------------------------------------------------------------------- /docs/lab-4/practice-1.md: -------------------------------------------------------------------------------- 1 | ## 实验四(上):线程 2 | 3 | ### 实验之前 4 | 5 | - 阅读实验指导四。 6 | - 从本次实验起,我们将不再提供“截至当前章节的代码框架”。你可以直接在 `master` 分支上查看代码,因为后面章节基本只会添加代码而鲜有修改。 7 | - 实验用到的代码在 `lab-4` 分支上,与 `master` 稍有修改。 8 | 9 | ### 实验题目 10 | 11 | 1. 原理:线程切换之中,页表是何时切换的?页表的切换会不会影响程序 / 操作系统的运行?为什么? 12 | 13 | {% reveal %} 14 | > 页表是在 `Process::prepare_next_thread()` 中调用 `Thread::prepare()`,其中换入了新线程的页表。 15 | > 16 | > 它不会影响执行,因为在中断期间是操作系统正在执行,而操作系统所用到的内核线性映射是存在于每个页表中的。 17 | {% endreveal %} 18 | 19 |
20 | 2. 设计:如果不使用 `sscratch` 提供内核栈,而是像原来一样,遇到中断就直接将上下文压栈,请举出(思路即可,无需代码): 21 | - 一种情况不会出现问题 22 | - 一种情况导致异常无法处理(指无法进入 `handle_interrupt`) 23 | - 一种情况导致产生嵌套异常(指第二个异常能够进行到调用 `handle_interrupt`,不考虑后续执行情况) 24 | - 一种情况导致一个用户进程(先不考虑是怎么来的)可以将自己变为内核进程,或以内核态执行自己的代码 25 | 26 | {% reveal %} 27 | > - 只运行一个非常善意的线程,比如 `loop {}` 28 | > - 线程把自己的 `sp` 搞丢了,比如 `mv sp, x0`。此时无法保存寄存器,也没有能够支持操作系统正常运行的栈 29 | > - 运行两个线程。在两个线程切换的时候,会需要切换页表。但是此时操作系统运行在前一个线程的栈上,一旦切换,再访问栈就会导致缺页,因为每个线程的栈只在自己的页表中 30 | > - 用户进程巧妙地设计 `sp`,使得它恰好落在内核的某些变量附近,于是在保存寄存器时就修改了变量的值。这相当于任意修改操作系统的控制信息 31 | {% endreveal %} 32 | 33 |
34 | 3. 实验:当键盘按下 Ctrl + C 时,操作系统应该能够捕捉到中断。实现操作系统捕获该信号并结束当前运行的线程(你可能需要阅读一点在实验指导中没有提到的代码) 35 | 36 |
37 | 4. 实验:实现进程的 `fork()`。目前的内核线程不能进行系统调用,所以我们先简化地实现为“按 F 进行 fork”。fork 后应当为目前的进程复制一份几乎一样的拷贝。 38 | 39 | > 旧题目:这个题目有一些问题,会导致线程中对栈上的指针失效。如果已经完成了 `clone()` 实验,推荐但不必须重新做 `fork()`。 40 | > 41 | > 实现线程的 `clone()`。目前的内核线程不能进行系统调用,所以我们先简化地实现为“按 C 进行 clone”。clone 后应当为目前的线程复制一份几乎一样的拷贝,新线程与旧线程同属一个进程,公用页表和大部分内存空间,而新线程的栈是一份拷贝。 42 | -------------------------------------------------------------------------------- /docs/lab-4/practice-2.md: -------------------------------------------------------------------------------- 1 | ## 实验四(下):线程调度 2 | 3 | ### 实验题目 4 | 5 | 1. 实验:了解并实现 Stride Scheduling 调度算法,为不同线程设置不同优先级,使得其获得与优先级成正比的运行时间。 6 | 7 | 2. 分析: 8 | - 在 Stride Scheduling 算法下,如果一个线程进入了一段时间的等待(例如等待输入,此时它不会被运行),会发生什么? 9 | - 对于两个优先级分别为 9 和 1 的线程,连续 10 个时间片中,前者的运行次数一定更多吗? 10 | - 你认为 Stride Scheduling 算法有什么不合理之处?可以怎样改进? 11 | -------------------------------------------------------------------------------- /docs/lab-5/files/rcore-fs-analysis.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcore-os/rCore-Tutorial/ceb9688a54a937b4838c5b761874192092d3b361/docs/lab-5/files/rcore-fs-analysis.pdf -------------------------------------------------------------------------------- /docs/lab-5/guide/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导五 2 | 3 | ## 实验概要 4 | 5 | 这一章的实验指导中,你将会学到: 6 | 7 | - 设备树的概念和读取 8 | - virtio 总线协议 9 | - 块设备驱动的实现 10 | - 将块设备托管给文件系统 -------------------------------------------------------------------------------- /docs/lab-5/guide/part-1.md: -------------------------------------------------------------------------------- 1 | ## 设备树 2 | 3 | ### 从哪里读取设备信息 4 | 5 | 既然我们要实现把数据放在某个存储设备上并让操作系统来读取,首先操作系统就要有一个读取全部已接入设备信息的能力,而设备信息放在哪里又是谁帮我们来做的呢?这个问题其实在[物理内存探测](../../lab-2/guide/part-2.md)中就提到过,在 RISC-V 中,这个一般是由 bootloader,即 OpenSBI 固件完成的。它来完成对于包括物理内存在内的各外设的扫描,将扫描结果以**设备树二进制对象(DTB,Device Tree Blob)**的格式保存在物理内存中的某个地方。而这个放置的物理地址将放在 `a1` 寄存器中,而将会把 HART ID (**HART,Hardware Thread,硬件线程,可以理解为执行的 CPU 核**)放在 `a0` 寄存器上。 6 | 7 | 在我们之前的函数中并没有使用过这两个参数,如果要使用,我们不需要修改任何入口汇编的代码,只需要给 `rust_main` 函数增加两个参数即可: 8 | 9 | {% label %}os/src/main.rs{% endlabel %} 10 | ```rust 11 | /// Rust 的入口函数 12 | /// 13 | /// 在 `_start` 为我们进行了一系列准备之后,这是第一个被调用的 Rust 函数 14 | #[no_mangle] 15 | pub extern "C" fn rust_main(_hart_id: usize, dtb_pa: PhysicalAddress) -> ! { 16 | memory::init(); 17 | interrupt::init(); 18 | drivers::init(dtb_pa); 19 | ... 20 | } 21 | ``` 22 | 23 | 打印输出一下,`dtb_pa` 变量约在 0x82200000 附近,而内核结束的地址约为 0x80b17000,也就是在我们内核的后面放着,这意味着当我们内核代码超过 32MB 的时候就会出现问题,在更好的实现中,其实 OpenSBI 固件启动的应该是第二级小巧的 Bootloader,而我们现在全部内核内容都在内存中且也没 32MB 那么大,我们暂时不理会这个问题。 24 | 25 | ### 设备树 26 | 27 | 上面提到 OpenSBI 固件会把设备信息以设备树的格式放在某个地址上,哪设备树格式究竟是怎样的呢?在各种操作系统中,我们打开设备管理器(Windows)和系统报告(macOS)等内置的系统软件就可以看到我们使用的电脑的设备树,一个典型的设备树如下图所示: 28 | 29 |
30 | 31 | 每个设备在物理上连接到了父设备上最后再通过总线等连接起来构成一整个设备树,在每个节点上都描述了对应设备的信息,如支持的协议是什么类型等等。而操作系统就是通过这些节点上的信息来实现对设备的识别的。 32 | 33 | > **[info] 设备节点属性** 34 | > 35 | > 具体而言,一个设备节点上会有几个标准属性,这里简要介绍我们需要用到的几个: 36 | > 37 | > - compatible:该属性指的是该设备的编程模型,一般格式为 "manufacturer,model",分别指一个出厂标签和具体模型。如 "virtio,mmio" 指的是这个设备通过 virtio 协议、MMIO(内存映射 I/O)方式来驱动 38 | > - model:指的是设备生产商给设备的型号 39 | > - reg:当一些很长的信息或者数据无法用其他标准属性来定义时,可以用 reg 段来自定义存储一些信息 40 | > 41 | > 设备树是一个比较复杂的标准,更多细节可以参考 [Device Tree Reference](https://elinux.org/Device_Tree_Reference)。 42 | 43 | ### 解析设备树 44 | 45 | 对于上面的属性,我们不需要自己来实现这件事情,可以直接调用 rCore 中 device_tree 库,然后遍历树上节点即可: 46 | 47 | {% label %}os/src/drivers/device_tree.rs{% endlabel %} 48 | ```rust 49 | /// 递归遍历设备树 50 | fn walk(node: &Node) { 51 | // 检查设备的协议支持并初始化 52 | if let Ok(compatible) = node.prop_str("compatible") { 53 | if compatible == "virtio,mmio" { 54 | virtio_probe(node); 55 | } 56 | } 57 | // 遍历子树 58 | for child in node.children.iter() { 59 | walk(child); 60 | } 61 | } 62 | 63 | /// 整个设备树的 Headers(用于验证和读取) 64 | struct DtbHeader { 65 | magic: u32, 66 | size: u32, 67 | } 68 | 69 | /// 遍历设备树并初始化设备 70 | pub fn init(dtb_va: VirtualAddress) { 71 | let header = unsafe { &*(dtb_va.0 as *const DtbHeader) }; 72 | // from_be 是大小端序的转换(from big endian) 73 | let magic = u32::from_be(header.magic); 74 | if magic == DEVICE_TREE_MAGIC { 75 | let size = u32::from_be(header.size); 76 | // 拷贝数据,加载并遍历 77 | let data = unsafe { slice::from_raw_parts(dtb_va.0 as *const u8, size as usize) }; 78 | if let Ok(dt) = DeviceTree::load(data) { 79 | walk(&dt.root); 80 | } 81 | } 82 | } 83 | ``` 84 | 85 | 注:在开始的时候,有一步来验证 Magic Number,这一步是一个保证系统可靠性的要求,是为了验证这段内存到底是不是设备树。在遍历过程中,一旦发现了一个支持 "virtio,mmio" 的设备(其实就是 QEMU 模拟的存储设备),就进入下一步加载驱动的逻辑。 86 | -------------------------------------------------------------------------------- /docs/lab-5/guide/part-3.md: -------------------------------------------------------------------------------- 1 | ## 驱动和块设备驱动 2 | 3 | ### 什么是块设备 4 | 5 | 注意到我们在介绍 virtio 时提到了 virtio-blk 设备,这种设备提供了以整块为粒度的读和写操作,一般对应到真实的物理设备是那种硬盘。而之所以是以块为单位是为了加快读写的速度,毕竟硬盘等设备还需要寻道等等操作,一次性读取很大的一块将会节约很多时间。 6 | 7 | ### 抽象驱动 8 | 9 | 在写块设备驱动之前,我们先抽象驱动的概念,也方便后面网络设备等的介入。 10 | 11 | {% label %}os/src/drivers/driver.rs{% endlabel %} 12 | ```rust 13 | /// 驱动类型 14 | /// 15 | /// 目前只有块设备,可能还有网络、GPU 设备等 16 | #[derive(Debug, Eq, PartialEq)] 17 | pub enum DeviceType { 18 | Block, 19 | } 20 | 21 | /// 驱动的接口 22 | pub trait Driver: Send + Sync { 23 | /// 设备类型 24 | fn device_type(&self) -> DeviceType; 25 | 26 | /// 读取某个块到 buf 中(块设备接口) 27 | fn read_block(&self, _block_id: usize, _buf: &mut [u8]) -> bool { 28 | unimplemented!("not a block driver") 29 | } 30 | 31 | /// 将 buf 中的数据写入块中(块设备接口) 32 | fn write_block(&self, _block_id: usize, _buf: &[u8]) -> bool { 33 | unimplemented!("not a block driver") 34 | } 35 | } 36 | 37 | lazy_static! { 38 | /// 所有驱动 39 | pub static ref DRIVERS: RwLock>> = RwLock::new(Vec::new()); 40 | } 41 | ``` 42 | 43 | 这里暂时只有块设备这个种类,不过这样写还是为了方便未来的扩展。 44 | 45 | ### 抽象块设备 46 | 47 | 有了驱动的概念,我们进一步抽象块设备: 48 | 49 | {% label %}os/src/drivers/block/mod.rs{% endlabel %} 50 | ```rust 51 | /// 块设备抽象(驱动的引用) 52 | pub struct BlockDevice(pub Arc); 53 | 54 | /// 为 [`BlockDevice`] 实现 [`rcore-fs`] 中 [`BlockDevice`] trait 55 | /// 56 | /// 使得文件系统可以通过调用块设备的该接口来读写 57 | impl dev::BlockDevice for BlockDevice { 58 | /// 每个块的大小(取 2 的对数) 59 | /// 60 | /// 这里取 512B 是因为 virtio 驱动对设备的操作粒度为 512B 61 | const BLOCK_SIZE_LOG2: u8 = 9; 62 | 63 | /// 读取某个块到 buf 中 64 | fn read_at(&self, block_id: usize, buf: &mut [u8]) -> dev::Result<()> { 65 | match self.0.read_block(block_id, buf) { 66 | true => Ok(()), 67 | false => Err(dev::DevError), 68 | } 69 | } 70 | 71 | /// 将 buf 中的数据写入块中 72 | fn write_at(&self, block_id: usize, buf: &[u8]) -> dev::Result<()> { 73 | match self.0.write_block(block_id, buf) { 74 | true => Ok(()), 75 | false => Err(dev::DevError), 76 | } 77 | } 78 | 79 | /// 执行和设备的同步 80 | /// 81 | /// 因为我们这里全部为阻塞 I/O 所以不存在同步的问题 82 | fn sync(&self) -> dev::Result<()> { 83 | Ok(()) 84 | } 85 | } 86 | ``` 87 | 88 | 这里所谓的 `BlockDevice` 其实就是一个 `Driver` 的引用。而且利用 rcore-fs 中提供的 `BlockDevice` trait 实现了为文件系统的接口,实际上是对上传文件系统的连接。 89 | 90 | ### virtio-blk 块设备驱动 91 | 92 | 最后,我们来实现 virtio-blk 的驱动(主要通过调用现成的库完成): 93 | 94 | {% label %}os/src/drivers/block/virtio_blk.rs{% endlabel %} 95 | ```rust 96 | /// virtio 协议的块设备驱动 97 | struct VirtIOBlkDriver(Mutex>); 98 | 99 | /// 为 [`VirtIOBlkDriver`] 实现 [`Driver`] trait 100 | /// 101 | /// 调用了 [`virtio_drivers`] 库,其中规定的块大小为 512B 102 | impl Driver for VirtIOBlkDriver { 103 | /// 设备类型 104 | fn device_type(&self) -> DeviceType { 105 | DeviceType::Block 106 | } 107 | 108 | /// 读取某个块到 buf 中 109 | fn read_block(&self, block_id: usize, buf: &mut [u8]) -> bool { 110 | self.0.lock().read_block(block_id, buf).is_ok() 111 | } 112 | 113 | /// 将 buf 中的数据写入块中 114 | fn write_block(&self, block_id: usize, buf: &[u8]) -> bool { 115 | self.0.lock().write_block(block_id, buf).is_ok() 116 | } 117 | } 118 | 119 | /// 将从设备树中读取出的设备信息放到 [`static@DRIVERS`] 中 120 | pub fn add_driver(header: &'static mut VirtIOHeader) { 121 | let virtio_blk = VirtIOBlk::new(header).expect("failed to init blk driver"); 122 | let driver = Arc::new(VirtIOBlkDriver(Mutex::new(virtio_blk))); 123 | DRIVERS.write().push(driver.clone()); 124 | } 125 | ``` 126 | 127 | 需要注意的是,现在的逻辑怎么看都不像是之前提到的**异步 DMA + IRQ 中断**的高级 I/O 操作技术,而更像是阻塞的读取。实际上的确是阻塞的读取,目前 virtio-drivers 库中的代码虽然调用了 DMA,但是返回时还是阻塞的逻辑,我们这里为了简化也没有设计 IRQ 的响应机制。 128 | 129 | 130 | 131 | ### 小结 132 | 133 | 至此,我们完成了全部的驱动逻辑,我们总结一下目前的设计模式如下所示: 134 | 135 |
136 | 137 | 其中 `Driver` 作为一个核心 trait 为上提供实现,上层也就是 `Driver` 的使用侧(设备的抽象),而下层则是 `Driver` 的实现侧(设备的实现)。而下一个小节,我们将利用这些驱动来实现文件系统。 138 | -------------------------------------------------------------------------------- /docs/lab-5/guide/part-4.md: -------------------------------------------------------------------------------- 1 | ## 文件系统 2 | 3 | 之前我们在加载 QEMU 的时候引入了一个磁盘镜像文件,这个文件的打包是由 [rcore-fs-fuse 工具](https://github.com/rcore-os/rcore-fs/tree/master/rcore-fs-fuse) 来完成的,它会根据不同的格式把目录的文件封装成到一个文件系统中,并把文件系统封装为一个磁盘镜像文件。然后我们把这个镜像文件像设备一样挂载在 QEMU 上,QEMU 就把它模拟为一个块设备了。接下来我们需要让操作系统理解块设备里面的文件系统。 4 | 5 | ### Simple File System 6 | 7 | 因为文件系统本身比较庞大,我们这里还是用了 rCore 中的文件系统模块 [rcore-fs](https://github.com/rcore-os/rcore-fs),其中实现了很多格式的文件系统,我们这里选择最简单的 Simple File System(这也是为什么 QEMU 中的设备 id 为 `sfs`),关于文件系统的细节,这里将不展开描述,可以参考[前人的分析](../files/rcore-fs-analysis.pdf)。 8 | 9 | 不过,为了使用这个模块,一个自然的想法是存取根目录的 `INode`(一个 `INode` 是对一个文件的位置抽象,目录也是文件的一种),后面对于文件系统的操作都可以通过根目录来实现。 10 | 11 | ### 实现 12 | 13 | 这里我们用到了我们的老朋友 `lazy_static` 宏,将会在我们第一次使用 `ROOT_INODE` 时进行初始化,而初始化的方式是找到全部设备驱动中的第一个存储设备作为根目录。 14 | 15 | {% label %}os/src/fs/mod.rs{% endlabel %} 16 | ```rust 17 | lazy_static! { 18 | /// 根文件系统的根目录的 INode 19 | pub static ref ROOT_INODE: Arc = { 20 | // 选择第一个块设备 21 | for driver in DRIVERS.read().iter() { 22 | if driver.device_type() == DeviceType::Block { 23 | let device = BlockDevice(driver.clone()); 24 | // 动态分配一段内存空间作为设备 Cache 25 | let device_with_cache = Arc::new(BlockCache::new(device, BLOCK_CACHE_CAPACITY)); 26 | return SimpleFileSystem::open(device_with_cache) 27 | .expect("failed to open SFS") 28 | .root_inode(); 29 | } 30 | } 31 | panic!("failed to load fs") 32 | }; 33 | } 34 | ``` 35 | 36 | 同时,还可以注意到我们也加入了一个 `BlockCache`,该模块也是 rcore-fs 提供的,提供了一个存储设备在内存 Cache 的抽象,通过调用 `BlockCache::new(device, BLOCK_CACHE_CAPACITY)` 就可以把 `device` 自动变为一个有 Cache 的设备。最后我们用 `SimpleFileSystem::open` 打开并返回根节点即可。 37 | 38 | ### 测试 39 | 40 | 终于到了激动人心的测试环节了!我们首先在触发一下 `ROOT_INODE` 的初始化,然后尝试输出一下根目录的内容: 41 | 42 | {% label %}os/src/fs/mod.rs{% endlabel %} 43 | ```rust 44 | /// 打印某个目录的全部文件 45 | pub fn ls(path: &str) { 46 | let mut id = 0; 47 | let dir = ROOT_INODE.lookup(path).unwrap(); 48 | print!("files in {}: \n ", path); 49 | while let Ok(name) = dir.get_entry(id) { 50 | id += 1; 51 | print!("{} ", name); 52 | } 53 | print!("\n"); 54 | } 55 | 56 | /// 触发 [`static@ROOT_INODE`] 的初始化并打印根目录内容 57 | pub fn init() { 58 | ls("/"); 59 | println!("mod fs initialized"); 60 | } 61 | ``` 62 | 63 | 最后在主函数中测试初始化,然后测试在另一个内核线程中创建个文件夹,而之所以在另一个线程中做是为了验证我们之前写驱动涉及到的页表的那些操作: 64 | 65 | {% label %}os/src/fs/mod.rs{% endlabel %} 66 | ```rust 67 | /// Rust 的入口函数 68 | /// 69 | /// 在 `_start` 为我们进行了一系列准备之后,这是第一个被调用的 Rust 函数 70 | #[no_mangle] 71 | pub extern "C" fn rust_main(_hart_id: usize, dtb_pa: PhysicalAddress) -> ! { 72 | memory::init(); 73 | interrupt::init(); 74 | drivers::init(dtb_pa); 75 | fs::init(); 76 | 77 | let process = Process::new_kernel().unwrap(); 78 | 79 | PROCESSOR 80 | .lock() 81 | .add_thread(Thread::new(process.clone(), simple as usize, Some(&[0])).unwrap()); 82 | 83 | // 把多余的 process 引用丢弃掉 84 | drop(process); 85 | 86 | PROCESSOR.lock().run() 87 | } 88 | 89 | /// 测试任何内核线程都可以操作文件系统和驱动 90 | fn simple(id: usize) { 91 | println!("hello from thread id {}", id); 92 | // 新建一个目录 93 | fs::ROOT_INODE 94 | .create("tmp", rcore_fs::vfs::FileType::Dir, 0o666) 95 | .expect("failed to mkdir /tmp"); 96 | // 输出根文件目录内容 97 | fs::ls("/"); 98 | 99 | loop {} 100 | } 101 | ``` 102 | 103 | `make run` 一下,你会得到类似的输出: 104 | 105 | {% label %}运行输出{% endlabel %} 106 | ``` 107 | mod memory initialized 108 | mod interrupt initialized 109 | mod driver initialized 110 | files in /: 111 | . .. temp rust 112 | mod fs initialized 113 | hello from thread id 0 114 | files in /: 115 | . .. temp rust tmp 116 | 100 tick 117 | 200 tick 118 | ... 119 | ``` 120 | 121 | 成功了!我们可以看到系统正确的读出了文件,而且也正确地创建了文件,这为后面用户进程数据的放置提供了很好的保障。 122 | -------------------------------------------------------------------------------- /docs/lab-5/guide/summary.md: -------------------------------------------------------------------------------- 1 | ## 小结 2 | 3 | 本章我们的工作有: 4 | 5 | - 在 QEMU 上挂载了存储设备 6 | - 通过读取设备树找到了挂载的设备 7 | - 实现了 virtio 驱动,把物理设备抽象为了驱动 8 | - 进一步把驱动抽象给上层文件系统使用 9 | - 调用 rcore-fs 的文件系统实现对文件的管理 10 | 11 | 现在,我们再也不会担心用户数据没有地方放置了,在下一个章节中,我们将实现用户进程,并让内核把用户进程加载和运行,实现和用户进程的交互。 12 | -------------------------------------------------------------------------------- /docs/lab-5/pics/design.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcore-os/rCore-Tutorial/ceb9688a54a937b4838c5b761874192092d3b361/docs/lab-5/pics/design.key -------------------------------------------------------------------------------- /docs/lab-5/pics/design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcore-os/rCore-Tutorial/ceb9688a54a937b4838c5b761874192092d3b361/docs/lab-5/pics/design.png -------------------------------------------------------------------------------- /docs/lab-5/pics/device-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcore-os/rCore-Tutorial/ceb9688a54a937b4838c5b761874192092d3b361/docs/lab-5/pics/device-tree.png -------------------------------------------------------------------------------- /docs/lab-5/pics/virtio.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcore-os/rCore-Tutorial/ceb9688a54a937b4838c5b761874192092d3b361/docs/lab-5/pics/virtio.gif -------------------------------------------------------------------------------- /docs/lab-6/guide/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导六 2 | 3 | ## 实验概要 4 | 5 | 这一章的实验指导中,你将会学到: 6 | 7 | - 单独生成 ELF 格式的用户程序,并打包进文件系统中 8 | - 创建并运行用户进程 9 | - 使用系统调用为用户程序提供服务 10 | -------------------------------------------------------------------------------- /docs/lab-6/guide/part-1.md: -------------------------------------------------------------------------------- 1 | ## 构建用户程序框架 2 | 3 | 接下来我们要做的工作,和实验准备中为操作系统「去除依赖」的工作十分类似:我们需要为用户程序提供一个类似的没有Rust std标准运行时依赖的极简运行时环境。这里我们会快速梳理一遍我们为用户程序进行的流程。 4 | 5 | ### 建立 crate 6 | 7 | 我们在 `os` 的旁边建立一个 `user` crate。此时,我们移除默认的 `main.rs`,而是在 `src` 目录下建立 `lib` 和 `bin` 子目录, 在 `lib` 中存放的是极简运行时环境,在 `bin` 中存放的源文件会被编译成多个单独的执行文件。 8 | 9 | {% label %}运行命令{% endlabel %} 10 | ```bash 11 | cargo new --bin user 12 | ``` 13 | 14 | {% label %}目录结构{% endlabel %} 15 | ``` 16 | rCore-Tutorial 17 | - os 18 | - user 19 | - src 20 | - bin 21 | - hello_world.rs 22 | - lib.rs 23 | - Cargo.toml 24 | ``` 25 | 26 | ### 基础框架搭建 27 | 28 | 和操作系统一样,我们需要为用户程序移除 std 依赖,并且补充一些必要的功能。 29 | 30 | #### `lib.rs` 31 | 32 | - `#![no_std]` 移除标准库 33 | - `#![feature(...)]` 开启一些不稳定的功能 34 | - `#[global_allocator]` 使用库来实现动态内存分配 35 | - `#[panic_handler]` panic 时终止 36 | 37 | #### 其他文件 38 | 39 | - `.cargo/config` 设置编译目标为 RISC-V 64 40 | - `console.rs` 实现 `print!` `println!` 宏 41 | -------------------------------------------------------------------------------- /docs/lab-6/guide/part-2.md: -------------------------------------------------------------------------------- 1 | ## 打包为磁盘镜像 2 | 3 | 在上一章我们已经实现了文件系统,并且可以让操作系统加载磁盘镜像。现在,我们只需要利用工具将编译后的用户程序打包为镜像,就可以使用了。 4 | 5 | ### 工具安装 6 | 7 | 通过 cargo 来安装 `rcore-fs-fuse` 工具: 8 | 9 | {% label %}运行命令{% endlabel %} 10 | ```bash 11 | cargo install rcore-fs-fuse --git https://github.com/rcore-os/rcore-fs 12 | ``` 13 | 14 | ### 打包 15 | 16 | 这个工具可以将一个目录打包成 SimpleFileSystem 格式的磁盘镜像。为此,我们需要将编译得到的 ELF 文件单独放在一个导出目录中,即 `user/build/disk`。 17 | 18 | {% label %}user/Makefile{% endlabel %} 19 | ```makefile 20 | build: dependency 21 | # 编译 22 | @cargo build 23 | @echo Targets: $(patsubst $(SRC_DIR)/%.rs, %, $(SRC_FILES)) 24 | # 移除原有的所有文件 25 | @rm -rf $(OUT_DIR) 26 | @mkdir -p $(OUT_DIR) 27 | # 复制编译生成的 ELF 至目标目录 28 | @cp $(BIN_FILES) $(OUT_DIR) 29 | # 使用 rcore-fs-fuse 工具进行打包 30 | @rcore-fs-fuse --fs sfs $(IMG_FILE) $(OUT_DIR) zip 31 | # 将镜像文件的格式转换为 QEMU 使用的高级格式 32 | @qemu-img convert -f raw $(IMG_FILE) -O qcow2 $(QCOW_FILE) 33 | # 提升镜像文件的容量(并非实际大小),来允许更多数据写入 34 | @qemu-img resize $(QCOW_FILE) +1G 35 | ``` 36 | 37 | 在 `os/Makefile` 中指定我们新生成的 `QCOW_FILE` 为加载镜像,就可以在操作系统中看到打包好的目录了。 38 | -------------------------------------------------------------------------------- /docs/lab-6/guide/part-3.md: -------------------------------------------------------------------------------- 1 | ## 解析 ELF 文件并创建线程 2 | 3 | 在之前实现内核线程时,我们只需要为线程指定一个起始位置就够了,因为所有的代码都在操作系统之中。但是现在,我们需要从 ELF 文件中加载用户程序的代码和数据信息,并且映射到内存中。 4 | 5 | 当然,我们不需要自己实现 ELF 文件解析器,因为有 `xmas-elf` 这个 crate 替我们实现了 ELF 的解析。 6 | 7 | ### `xmas-elf` 解析器 8 | 9 | tips:如果 IDE 无法对其中的类型进行推断,可以在 rustdoc 中找到该 crate 进行查阅。 10 | 11 | #### 读取文件内容 12 | 13 | `xmas-elf` 需要将 ELF 文件首先读取到内存中。在上一章文件系统的基础上,我们很容易为 `INode` 添加一个将整个文件作为 `[u8]` 读取出来的方法: 14 | 15 | {% label %}os/src/fs/inode_ext.rs{% endlabel %} 16 | ```rust 17 | fn readall(&self) -> Result> { 18 | // 从文件头读取长度 19 | let size = self.metadata()?.size; 20 | // 构建 Vec 并读取 21 | let mut buffer = Vec::with_capacity(size); 22 | unsafe { buffer.set_len(size) }; 23 | self.read_at(0, buffer.as_mut_slice())?; 24 | Ok(buffer) 25 | } 26 | ``` 27 | 28 | ### 解析各个字段 29 | 30 | 对于 ELF 中的不同字段,其存放的地址通常是不连续的,同时其权限也会有所不同。我们利用 `xmas-elf` 库中的接口,便可以从读出的 ELF 文件中对应建立 `MemorySet`。 31 | 32 | 注意到,用户程序也会首先映射所有内核态的空间,否则将无法进行中断处理。 33 | 34 | {% label %}os/src/memory/mapping/memory_set.rs{% endlabel %} 35 | ```rust 36 | /// 通过 elf 文件创建内存映射(不包括栈) 37 | pub fn from_elf(file: &ElfFile, is_user: bool) -> MemoryResult { 38 | // 建立带有内核映射的 MemorySet 39 | let mut memory_set = MemorySet::new_kernel()?; 40 | 41 | // 遍历 elf 文件的所有部分 42 | for program_header in file.program_iter() { 43 | if program_header.get_type() != Ok(Type::Load) { 44 | continue; 45 | } 46 | // 从每个字段读取「起始地址」「大小」和「数据」 47 | let start = VirtualAddress(program_header.virtual_addr() as usize); 48 | let size = program_header.mem_size() as usize; 49 | let data: &[u8] = 50 | if let SegmentData::Undefined(data) = program_header.get_data(file).unwrap() { 51 | data 52 | } else { 53 | return Err("unsupported elf format"); 54 | }; 55 | 56 | // 将每一部分作为 Segment 进行映射 57 | let segment = Segment { 58 | map_type: MapType::Framed, 59 | range: Range::from(start..(start + size)), 60 | flags: Flags::user(is_user) 61 | | Flags::readable(program_header.flags().is_read()) 62 | | Flags::writable(program_header.flags().is_write()) 63 | | Flags::executable(program_header.flags().is_execute()), 64 | }; 65 | 66 | // 建立映射并复制数据 67 | memory_set.add_segment(segment, Some(data))?; 68 | } 69 | 70 | Ok(memory_set) 71 | } 72 | ``` 73 | 74 | ### 加载数据到内存中 75 | 76 | 思考:我们在为用户程序建立映射时,虚拟地址是 ELF 文件中写明的,那物理地址是程序在磁盘中存储的地址吗?这样做有什么问题吗? 77 | 78 | {% reveal %} 79 | > 我们在模拟器上运行可能不觉得,但是如果直接映射磁盘空间,使用时会带来巨大的延迟,所以需要在程序准备运行时,将其磁盘中的数据复制到内存中。如果程序较大,操作系统可能只会复制少量数据,而更多的则在需要时再加载。当然,我们实现的简单操作系统就一次性全都加载到内存中了。 80 | > 81 | > 而且,就算是想要直接映射磁盘空间,也不一定可行。这是因为虚实地址转换时,页内偏移是不变的。这是就无法保证在 ELF 中指定的地址和其在磁盘中的地址满足这样的关系。 82 | {% endreveal %} 83 | 84 |
85 | 86 | 我们将修改 `Mapping::map` 函数,为其增加一个参数表示用于初始化的数据。在实现时,有一些重要的细节需要考虑。 87 | 88 | - 因为用户程序的内存分配是动态的,其分配到的物理页面不一定连续,所以必须单独考虑每一个页面 89 | - 每一个字段的长度不一定是页大小的倍数,所以需要考虑不足一个页时的复制情况 90 | - 程序有一个 bss 段,它在 ELF 中不保存数据,而其在加载到内存是需要零初始化 91 | - 对于一个页面,有其**物理地址**、**虚拟地址**和**待加载数据的地址**。此时,是不是直接从**待加载数据的地址**拷贝到页面的**虚拟地址**,如同 `memcpy` 一样就可以呢? 92 | 93 | {% reveal %} 94 | > 在目前的框架中,只有当线程将要运行时,才会加载其页表。因此,除非我们额外的在每映射一个页面之后,就更新一次页表并且刷新 TLB,否则此时的**虚拟地址**是无法访问的。 95 | > 96 | > 但是,我们通过分配器得到了页面的**物理地址**,而这个物理地址实际上已经在内核的线性映射当中了。所以,这里实际上用的是**物理地址**来写入数据。 97 | {% endreveal %} 98 | 99 |
100 | 101 | 具体的实现,可以查看 `os/src/memory/mapping/mapping.rs` 中的 `Mapping::map` 函数。 102 | 103 | ### 运行 Hello World? 104 | 105 | 现在,我们就可以在操作系统中运行磁盘镜像中的用户程序了,代码示例如下: 106 | 107 | {% label %}os/src/main.rs{% endlabel %} 108 | ```rust 109 | // 从文件系统中找到程序 110 | let app = fs::ROOT_INODE.find("hello_world").unwrap(); 111 | // 读取数据 112 | let data = app.readall().unwrap(); 113 | // 解析 ELF 文件 114 | let elf = ElfFile::new(data.as_slice()).unwrap(); 115 | // 利用 ELF 文件创建线程,映射空间并加载数据 116 | let process = Process::from_elf(&elf, true).unwrap(); 117 | // 再从 ELF 中读出程序入口地址 118 | let thread = Thread::new(process, elf.header.pt2.entry_point() as usize, None).unwrap(); 119 | // 添加线程 120 | PROCESSOR.lock().add_thread(thread); 121 | ``` 122 | 123 | 可惜的是,我们不能像内核线程一样在用户程序中直接使用 `print`。前者是基于 OpenSBI 的机器态 SBI 调用,而为了让用户程序能够打印字符,我们还需要在操作系统中实现系统调用来给用户进程提供服务。 124 | -------------------------------------------------------------------------------- /docs/lab-6/guide/part-4.md: -------------------------------------------------------------------------------- 1 | ## 实现系统调用 2 | 3 | 目前,我们实现 `sys_read` `sys_write` 和 `sys_exit` 三个简单的系统调用。通过学习它们的实现,更多的系统调用也并没有多难。 4 | 5 | ### 用户程序中调用系统调用 6 | 7 | 在用户程序中实现系统调用比较容易,就像我们之前在操作系统中使用 `sbi_call` 一样,只需要符合规则传递参数即可。而且这一次我们甚至不需要参考任何标准,每个人都可以为自己的操作系统实现自己的标准。 8 | 9 | 例如,在实验指导中,系统调用的编号使用了 musl 中的编码和参数格式。但实际上,在实现操作系统的时候,编码和参数格式都可以随意调整,只要在用户程序中的调用和操作系统中的解释相符即可。 10 | 11 | {% label %}代码示例{% endlabel %} 12 | ```rust 13 | // musl 中的 sys_read 调用格式 14 | llvm_asm!("ecall" : 15 | "={x10}" (/* 返回读取长度 */) : 16 | "{x10}" (/* 文件描述符 */), 17 | "{x11}" (/* 读取缓冲区 */), 18 | "{x12}" (/* 缓冲区长度 */), 19 | "{x17}" (/* sys_read 编号 63 */) :: 20 | ); 21 | // 一种可能的 sys_read 调用格式 22 | llvm_asm!("ecall" : 23 | "={x10}" (/* 现在的时间 */), 24 | "={x11}" (/* 今天的天气 */), 25 | "={x12}" (/* 读取一个字符 */) : 26 | "{x20}" (/* sys_read 编号 0x595_7ead */) :: 27 | ); 28 | ``` 29 | 30 | 实验指导提供了第一种无趣的系统调用格式。 31 | 32 | ### 避免忙等待 33 | 34 | 在常见操作系统中,一些延迟非常大的操作,例如文件读写、网络通讯,都可以使用异步接口来进行。但是为了实现更加简便,我们的读写系统调用都是阻塞的。在 `sys_read` 中,使用了 `loop` 来保证仅当成功读取字符时才返回。 35 | 36 | 此时,如果用户程序需要获取从控制台输入的字符,但是此时并没有任何字符到来。那么,程序将被阻塞,而操作系统的职责就是尽量减少线程执行无用阻塞占用 CPU 的时间,而是将这段时间分配给其他可以执行的线程。具体的做法,将会在后面**条件变量**的章节讲述。 37 | 38 | ### 操作系统中实现系统调用 39 | 40 | 在操作系统中,系统调用的实现和中断处理一样,有同样的入口,而针对不同的参数设置不同的处理流程。为了简化流程,我们不妨把系统调用的处理结果分为三类: 41 | 42 | - 返回一个数值,程序继续执行 43 | - 程序进入等待 44 | - 程序将被终止 45 | 46 | #### 系统调用的处理流程 47 | 48 | - 首先,从相应的寄存器中取出调用代号和参数 49 | - 根据调用代号,进入不同的处理流程,得到处理结果 50 | - 返回数值并继续执行: 51 | - 返回值存放在 `x10` 寄存器,`sepc += 4`,继续此 `context` 的执行 52 | - 程序进入等待 53 | - 同样需要更新 `x10` 和 `sepc`,但是需要将当前线程标记为等待,切换其他线程来执行 54 | - 程序终止 55 | - 不需要考虑系统调用的返回,直接删除线程 56 | 57 | #### 具体的调用实现 58 | 59 | 那么具体该如何实现读 / 写系统调用呢?这里我们会利用文件的统一接口 `INode`,使用其中的 `read_at()` 和 `write_at()` 接口即可。下一节就将讲解如何处理文件描述符。 60 | -------------------------------------------------------------------------------- /docs/lab-6/guide/part-5.md: -------------------------------------------------------------------------------- 1 | ## 处理文件描述符 2 | 3 | 尽管很不像,但是在大多操作系统中,标准输入输出流 `stdin` 和 `stdout` 虽然叫做「流」,但它们都有文件的接口。我们同样也会将它们实现成为文件。 4 | 5 | 但是不用担心,作为文件的许多功能,`stdin` 和 `stdout` 都不会支持。我们只需要为其实现最简单的读写接口。 6 | 7 | ### 进程打开的文件 8 | 9 | 操作系统需要为进程维护一个进程打开的文件清单。其中,一定存在的是 `stdin` `stdout` 和 `stderr`。为了简便,我们只实现 `stdin` 和 `stdout`,它们的文件描述符数值分别为 0 和 1。 10 | 11 | ### `stdout` 12 | 13 | 输出流最为简单:每当遇到系统调用时,直接将缓冲区中的字符通过 SBI 调用打印出去。 14 | 15 | ### `stdin` 16 | 17 | 输入流较为复杂:每当遇到系统调用时,通过中断或轮询方式获取字符:如果有,就进一步获取;如果没有就等待。直到收到约定长度的字符串才返回。 18 | 19 | #### 外部中断 20 | 21 | 对于用户程序而言,外部输入是随时主动读取的数据。但是事实上外部输入通常时间短暂且不会等待,需要操作系统立即处理并缓冲下来,再等待程序进行读取。所以,每一个键盘按键对于操作系统而言都是一次短暂的中断。 22 | 23 | 而在之前的实验中操作系统不会因为一个按键就崩溃,是因为 OpenSBI 默认会关闭各种外部中断。但是现在我们需要将其打开,来接受按键信息。 24 | 25 | {% label %}os/src/interrupt/handler.rs{% endlabel %} 26 | ```rust 27 | /// 初始化中断处理 28 | /// 29 | /// 把中断入口 `__interrupt` 写入 `stvec` 中,并且开启中断使能 30 | pub fn init() { 31 | unsafe { 32 | extern "C" { 33 | /// `interrupt.asm` 中的中断入口 34 | fn __interrupt(); 35 | } 36 | // 使用 Direct 模式,将中断入口设置为 `__interrupt` 37 | stvec::write(__interrupt as usize, stvec::TrapMode::Direct); 38 | 39 | // 开启外部中断使能 40 | sie::set_sext(); 41 | 42 | // 在 OpenSBI 中开启外部中断 43 | *PhysicalAddress(0x0c00_2080).deref_kernel() = 1 << 10; 44 | // 在 OpenSBI 中开启串口 45 | *PhysicalAddress(0x1000_0004).deref_kernel() = 0x0bu8; 46 | *PhysicalAddress(0x1000_0001).deref_kernel() = 0x01u8; 47 | } 48 | } 49 | ``` 50 | 51 | 这里,我们需要按照 OpenSBI 的接口在指定的地址进行配置。好在这些地址都在文件系统映射的空间内,就不需要再为其单独建立内存映射了。开启中断使能后,任何一个按键都会导致程序进入 `unimplemented!` 的区域。 52 | 53 | #### 实现输入流 54 | 55 | 输入流则需要配有一个缓冲区,我们可以用 `alloc::collections::VecDeque` 来实现。在遇到键盘中断时,调用 `sbi_call` 来获取字符并加入到缓冲区中。当遇到系统调用 `sys_read` 时,再相应从缓冲区中取出一定数量的字符。 56 | 57 | 那么,如果遇到了 `sys_read` 系统调用,而缓冲区并没有数据可以读取,应该如何让线程进行等待,而又不浪费 CPU 资源呢? 58 | -------------------------------------------------------------------------------- /docs/lab-6/guide/part-6.md: -------------------------------------------------------------------------------- 1 | ## 条件变量 2 | 3 | 条件变量(conditional variable)的常见接口是这样的: 4 | 5 | - wait:当前线程开始等待这个条件变量 6 | - notify_one:让某一个等待此条件变量的线程继续运行 7 | - notify_all:让所有等待此变量的线程继续运行 8 | 9 | 条件变量和互斥锁的区别在于,互斥锁解铃还须系铃人,但条件变量可以由任何来源发出 notify 信号。同时,互斥锁的一次 lock 一定对应一次 unlock,但条件变量多次 notify 只能保证 wait 的线程执行次数不超过 notify 次数。 10 | 11 | 为输入流加入条件变量后,就可以使得调用 `sys_read` 的线程在等待期间保持休眠,不被调度器选中,消耗 CPU 资源。 12 | 13 | ### 调整调度器 14 | 15 | 为了继续沿用调度算法,不带来太多修改,我们为线程池单独设立一个「休眠区」,其中保存的线程与调度器互斥。当线程进入等待,就将它从调度器中取出,避免之后再被无用唤起。 16 | 17 | {% label %}os/src/process/processor.rs{% endlabel %} 18 | ```rust 19 | pub struct Processor { 20 | /// 当前正在执行的线程 21 | current_thread: Option>, 22 | /// 线程调度器,记录活跃线程 23 | scheduler: SchedulerImpl>, 24 | /// 保存休眠线程 25 | sleeping_threads: HashSet>, 26 | } 27 | ``` 28 | 29 | ### 实现条件变量 30 | 31 | 条件变量会被包含在输入流等涉及等待和唤起的结构中,而一个条件变量保存的就是所有等待它的线程。 32 | 33 | {% label %}os/src/kernel/condvar.rs{% endlabel %} 34 | ```rust 35 | #[derive(Default)] 36 | pub struct Condvar { 37 | /// 所有等待此条件变量的线程 38 | watchers: Mutex>>, 39 | } 40 | ``` 41 | 42 | 当一个线程调用 `sys_read` 而缓冲区为空时,就会将其加入条件变量的 `watcher` 中,同时在 `Processor` 中移出活跃线程。而当键盘中断到来,读取到字符时,就会将线程重新放回调度器中,准备下一次调用。 43 | 44 | 开放思考:如果多个线程同时等待输入流会怎么样?有什么解决方案吗? 45 | -------------------------------------------------------------------------------- /docs/lab-6/guide/summary.md: -------------------------------------------------------------------------------- 1 | ## 总结 2 | 3 | 这一章的实验指导中,我们成功单独生成 ELF 格式的用户程序,并打包进文件系统中;同时,从中读取,创建并运行用户进程;而为了可以让用户程序享受到操作系统的功能,我们使用系统调用为用户程序提供服务。 -------------------------------------------------------------------------------- /docs/lab-6/practice.md: -------------------------------------------------------------------------------- 1 | ## 实验六:系统调用 2 | 3 | ### 实验之前 4 | 5 | - 阅读实验指导五、六。 6 | 7 | ### 实验题目 8 | 9 | 1. 原理:使用条件变量之后,分别从线程和操作系统的角度而言读取字符的系统调用是阻塞的还是非阻塞的? 10 | 11 | {% reveal %} 12 | > 对于线程而言,是阻塞的,因为在等待有效输入之前线程都会暂停。但对于操作系统而言,等待输入的时间完全分配给了其他线程,所以对于操作系统来说是非阻塞的。 13 | {% endreveal %} 14 | 15 |
16 | 2. 设计:如果要让用户线程能够使用 `Vec` 等,需要做哪些工作?如果要让用户线程能够使用大于其栈大小的动态分配空间,需要做哪些工作? 17 | 18 | 3. 实验:实现 `get_tid` 系统调用,使得用户线程可以获取自身的线程 ID。 19 | 20 | 4. 实验:基于你在实验四(上)的实践,实现 `sys_fork` 系统调用。该系统调用复制一个进程,并为父进程返回 1(目前没有引入进程 ID,也可以自行补充为进程 ID),而为子进程返回 0。 21 | 22 | 相比于实验四,你可能需要额外注意文件描述符的复制。 23 | 24 | 5. 实验:将一个文件打包进用户镜像,并让一个用户进程读取它并打印其内容。需要实现 `sys_open`,将文件描述符加入进程的 `descriptors` 中并返回,然后通过 `sys_read` 来读取。 25 | 26 | 6. 挑战实验:实现 `sys_pipe`,为进程添加并返回两个文件描述符,分别为一个管道的读和写端。用户进程调用完 `sys_pipe` 后调用 `sys_fork`,父进程写入管道,子进程可以读取。读取时尽量避免忙等待。 27 | 28 | ```rust 29 | /// 用户进程样例 30 | pub fn main() -> usize { 31 | let (mut write_fd, mut read_fd) = sys_pipe(); 32 | if sys_fork() { 33 | // 父进程 34 | sys_close(read_fd); // 不一定需要实现 35 | sys_write(write_fd, "hello_world".as_bytes()); 36 | } else { 37 | // 子进程 38 | sys_close(write_fd); // 不一定需要实现 39 | let mut buffer = [0u8; 64]; 40 | let len = sys_read(read_fd, &mut buffer); 41 | println!("{}", core::str::from_utf8(&buffer)); 42 | } 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/pre-lab/gdb.md: -------------------------------------------------------------------------------- 1 | # 使用 GDB 对 rCore 进行 debug* 2 | 3 | > *:使用 GDB 调试可以方便观察 rCore 运行过程,但不属于教程要求 4 | 5 | GDB 需要支持 riscv64 架构才能够对 rCore 进行 debug。 6 | 7 | - 运行 `gdb --configuration` 来查看本地的 GDB 支持什么架构,其中 `--target` 参数指定了它可以 debug 的架构 8 | - 如果 `gdb` 不支持,可以按照下面的步骤来安装 `riscv64-unknown-elf-gdb` 9 | 10 | ## 安装 `riscv64-unknown-elf-gdb` 11 | 12 | 0. 安装依赖(针对 Linux,macOS 可以遇到错误再去搜索) 13 | - python 并非必须 14 | - 在 `Ubuntu 20.04` 等系统中,`python` 和 `python-dev` 需要替换成 `python2` 和 `python2-dev` 15 | ```bash 16 | sudo apt-get install libncurses5-dev python python-dev texinfo libreadline-dev 17 | ``` 18 | 19 | 1. 前往[清华镜像](https://mirrors.tuna.tsinghua.edu.cn/gnu/gdb/?C=M&O=D)下载最新的 GDB 源代码 20 | 21 | 2. 解压源代码,并定位到目录 22 | 23 | 3. 执行以下命令 24 | - `--prefix` 是安装的路径,按照以上指令会安装到 `/usr/local/bin/` 下 25 | - `--with-python` 是 `python2` 的地址,它和 `--enable-tui` 都是为了支持后续安装一个可视化插件,并非必须 26 | ```bash 27 | mkdir build 28 | cd build 29 | ../configure --prefix=/usr/local --with-python=/usr/bin/python --target=riscv64-unknown-elf --enable-tui=yes 30 | ``` 31 | 32 | 4. 编译安装 33 | 34 | ```bash 35 | # Linux 36 | make -j$(nproc) 37 | # macOS 38 | make -j$(sysctl -n hw.ncpu) 39 | 40 | sudo make install 41 | ``` 42 | 43 | 5. (可选)安装 [`gdb-dashboard`](https://github.com/cyrus-and/gdb-dashboard/) 插件,优化 debug 体验 44 | ```bash 45 | wget -P ~ https://git.io/.gdbinit 46 | ``` 47 | 48 | ## 使用 GDB 对 rCore 进行 debug 49 | 50 | 在 `os/Makefile` 中,包含了 `debug` 方法,可以执行 `make debug` 来在 `tmux` 中开启调试。 51 | 52 | 手动: 53 | 54 | - 将 QEMU 的运行参数加上 `-s -S`,它将在 1234 端口等待调试器接入 55 | - 运行 `riscv64-unknown-elf-gdb` 56 | - 在 GDB 中执行 `file target/riscv64imac-unknown-none-elf/debug/os` 来加载未被 `strip` 过的内核文件中的各种符号 57 | - 在 GDB 中执行 `target remote localhost:1234` 来连接 QEMU,开始调试 58 | 59 | ## GDB 简单使用方法 60 | 61 | ### 控制流 62 | 63 | - `b <函数名>` 在函数进入时设置断点,例如 `b rust_main` 或 `b os::memory::heap::init` 64 | - `cont` 继续执行 65 | - `n` 执行下一行代码,不进入函数 66 | - `ni` 执行下一条指令(跳转指令则执行至返回) 67 | - `s` 执行下一行代码,进入函数 68 | - `si` 执行下一条指令,包括跳转指令 69 | 70 | ### 查看状态 71 | 72 | - 如果没有安装 `gdb-dashboard`,可以通过 `layout` 指令来呈现寄存器等信息,具体查看 `help layout` 73 | - 使用 `x/<格式> <地址>` 来查看内存,例如 `x/8i 0x80200000` 表示查看 `0x80200000` 起始的 8 条指令。具体格式查看 `help x` 74 | 75 | ## 注意 76 | 77 | 调试虚实地址转换时,GDB 完全通过读取文件来判断函数地址,因此可能会遇到一些问题,需要手动设置地址来调试 78 | -------------------------------------------------------------------------------- /docs/pre-lab/os.md: -------------------------------------------------------------------------------- 1 | # 操作系统背景知识 2 | 3 | -------------------------------------------------------------------------------- /docs/pre-lab/rust.md: -------------------------------------------------------------------------------- 1 | # Rust 基础介绍 2 | 3 | -------------------------------------------------------------------------------- /gitalk.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | -------------------------------------------------------------------------------- /notes/log.md: -------------------------------------------------------------------------------- 1 | # 实验框架重要更新记录 2 | 3 | ## 2020/07/25 4 | 5 | - 实验三:**内存置换**实验分支 `lab-3` 和 `lab-3+` 已经更新。 6 | - 实验四:内核栈重命名为**中断栈**(尚未合并到 master 分支)。 7 | - 实验四、六:`clone` 实验改为 `fork`(复制进程)。 8 | 9 | ## 2020/07/22 10 | 11 | - 实验一:`interrupt.asm` 中的寄存器保存和读取,使用循环来代替大段相似代码。 12 | - 实验四:`Process` 不再使用 `RwLock` 来包装,而使用 `Mutex` 来存放其中的可变部分。 13 | - 实验五:文件描述符从线程移动到进程中。 14 | -------------------------------------------------------------------------------- /notes/关于课程设计.md: -------------------------------------------------------------------------------- 1 | - 目前的 rCore tutorial 存在一些问题: 2 | - 代码 3 | - 完全没有体现出 Rust 的优点,几乎所有需要做 borrow check 之类的地方都用 unsafe 糊过去了 4 | - 设计模式极为混乱,基本不考虑封装、模块化,互相有关的代码也零散在不同文件、不同类型中 5 | - 有些地方可读性不强,而且注释非常少,很多风格也不统一,也有一些 hardcoded 的部分 6 | - 文档部分,在最后几个章节没有前几个章节更加细致,很多行的代码只用了一句话来带过 7 | - 实验设计 8 | - 比较简单?而且这还是 140 分。在有同学帮助的情况下三天做完没什么问题 9 | - 涵盖的知识点比较少,我不看课件,文档也不完全看一遍,也能直接做题(不过因为有考试这方面倒是不太重要) 10 | - 缺少自动化的测试 11 | - 做 tutorial 的理由(为什么做一个 tutorial 就有大实验的分量): 12 | - 希望能够帮到学弟学妹,目标是把整个 OS 课程内容融合到里面去,增加同学的参与程度 13 | - 做一套 tutorial 涉及重构整个 rCore,不会像现有 tutorial 一样参照原版,因此可以说非常全面地掌握了 OS 课程所有内容 14 | - 如果和之前一样,是做一个 tutorial 附带好多别的东西,会导致 tutorial 赶工质量粗糙(感觉好对不起小班助教) 15 | - 我们的计划: 16 | - 重新写一个简化版操作系统 + 教程 17 | - 实验内容修改 18 | - 这个看老师意见,我们可以把实验设计得更多更难,也可以不增加代码量而涵盖更多知识点 19 | - 要带大家用 qemu + gdb 从汇编层面理解代码,理解操作系统是怎么 work 的 20 | - 我写实验作业的时候就跑过,配置起来很容易 21 | -------------------------------------------------------------------------------- /notes/方案设计文档.md: -------------------------------------------------------------------------------- 1 | # 大实验方案设计文档:rCore Tutorial 重构 2 | 3 | 涂轶翔 2017011422 4 | 赵成钢 2017011362 5 | 6 | 7 | 8 | ### 相关工作参考 9 | 10 | - [**rCore_tutorial**](https://github.com/rcore-os/rCore_tutorial) 和 [**rCore**](https://github.com/rcore-os/rCore) 11 | - 代码的主要参考,会在其基础上重构代码结构,而保留大部分的具体实现 12 | - [**uCore 实验指导书**](https://learningos.github.io/ucore_os_webdocs/) 13 | - 实验设计主要参照,视进度尝试移植更多挑战实验 14 | - [Writing an OS in Rust](https://os.phil-opp.com/) 15 | 16 | 17 | 18 | ### 设计方案 19 | 20 | #### 1. 在多个方面改善 rCore 教学代码、文档和实验内容 21 | 22 | ##### 实验设计 23 | 24 | - 改善自动化测试 25 | *目前部分测试需要手动终止运行并人工观察输出* 26 | 27 | - 测试指令直接返回成绩,节省助教检查的时间 28 | - 使用更强(例如随机化)的测例 29 | 30 | - 探索更丰富的实验方法,不增加工作量而更考察知识 31 | 32 | - 例如,要求学生设计函数,使得某种调度算法的效率提升 / 降低 33 | - 例如,要求学生修改一些汇编,使得用户程序越界访问 34 | 35 | - 尝试结合随堂作业、涵盖更多知识点 36 | 37 | - - 部分作业比较适合作为额外挑战放在实验中 38 | 39 | ##### 代码 40 | 41 | - 优化设计模式 42 | *目前代码中存在许多软件工程角度的缺陷* 43 | - 贯彻封装思想,将各个模块之间最大化地解耦合 44 | - 更多利用 rust 的特有功能,例如 Clone Drop 等 trait 45 | - 提升代码可读性 46 | *目前代码注释较少且不够起到解释作用* 47 | - 添加规范格式的注释 48 | - 使用更加清晰的变量命名 49 | - 通过提取函数等形式让程序工作流程更加易读 50 | - 规范代码 51 | - 让代码格式更加符合 rust 的 best practice,消除 Warning 52 | - 完善对 Windows / macOS / Linux 的环境支持 53 | 54 | ##### 文档 55 | 56 | - 细化对代码的讲解 57 | - 为函数添加规范的功能注释,可以自动生成 doc 58 | - 重要的代码添加注释 59 | - 文档中深入阐释涉及的原理 60 | *这部分现有文档仍有不足* 61 | 62 | ##### 功能 63 | 64 | - 实现一些目前框架缺少的功能 65 | - 资源的释放 66 | - 进程 vs 线程等 67 | 68 | #### 2. 与 uCore 小组进行协作 69 | 70 | - 尽可能在实验内容上达成统一 71 | - 文档在代码讲解上差异会很大,而原理讲解上尽量做到一致 72 | 73 | #### 3. 可能的额外项目 74 | 75 | - 移植 uCore 中的扩展练习 和/或 将随堂作业相关内容作为挑战作业 76 | - 物理硬件上运行 及 多核支持 77 | 78 | 79 | 80 | ### 实验计划 81 | 82 | #### 分工 83 | 84 | - 对于每个实验,一人负责代码与测试,另一人负责注释与文档 85 | - 所有实验开发过程中会有轮换 86 | 87 | #### 时间计划 88 | 89 | - 7 ~ 10 周完成 8 个基础 lab(每周 2 个) 90 | - 最晚 11 周时完成度达到足以供教学使用的进度 91 | - 后续开发更多 lab 内容 和/或 拓展框架 -------------------------------------------------------------------------------- /notes/结题答辩(13周).key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcore-os/rCore-Tutorial/ceb9688a54a937b4838c5b761874192092d3b361/notes/结题答辩(13周).key -------------------------------------------------------------------------------- /notes/结题答辩(13周).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcore-os/rCore-Tutorial/ceb9688a54a937b4838c5b761874192092d3b361/notes/结题答辩(13周).pdf -------------------------------------------------------------------------------- /notes/设计预期和目标.md: -------------------------------------------------------------------------------- 1 | - 代码层面 2 | - 进一步体现 Rust 的优点,更接近真实开发社区的规范 3 | - 重新设计结构,完善设计模式 4 | - 增强可读性,添加可以完善了解系统代码的注释 5 | - 完善代码规范和风格,更贴近现代软件工程的准则 6 | - 完善 Windows / macOS / Linux 的环境支持和配置 7 | - 功能层面 8 | - 进一步完善教学版 rCore 的功能(如进程、线程进一步区分),使得教学版有更具完整更加贴近课程 9 | - 文档层面 10 | - 完善文档到非常细节的程度 11 | - 添加一些扩展信息 12 | - 进一步贴合课程所学 13 | - 微调文档的结构 14 | - 添加一些比如 qemu + gdb 从汇编层面的实践认知上的理解,添加例子进一步让同学理解操作系统如何工作 15 | - 实验设计层面 16 | - 目标是让同学有对操作系统从前到后的完整理解,少一些类似数据结构的题目 17 | - 让工作量多一些系统层面原理的认识,减少一部分简单算法的 dirty work 18 | - 测试的方法会完善,强度会进一步加大,尽可能实现全自动化测试而且避免一些在代码中 cheet 的可能性 19 | - 设计上尽量可以检验同学们阅读代码的成果,可以设计些贯穿流程的问题或者代码填空,而不是基于可以运行的系统上来增量开发 20 | - 更加具体的内容和难度上需要参考老师的建议,可以把实验设计得更多更难,也可以不增加代码量而涵盖更多知识点 21 | - 测评尽量从每个章节的版本出发,而不是基于一个大的完整版 22 | - 其他 23 | - 和做 uCore 的 RISC-V 支持的同学沟通并统一意见 24 | -------------------------------------------------------------------------------- /notes/课程设计方案幻灯片.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcore-os/rCore-Tutorial/ceb9688a54a937b4838c5b761874192092d3b361/notes/课程设计方案幻灯片.pdf -------------------------------------------------------------------------------- /os/.cargo/config: -------------------------------------------------------------------------------- 1 | # 编译的目标平台 2 | [build] 3 | target = "riscv64imac-unknown-none-elf" 4 | 5 | # 使用我们的 linker script 来进行链接 6 | [target.riscv64imac-unknown-none-elf] 7 | rustflags = [ 8 | "-C", "link-arg=-Tsrc/linker.ld", 9 | ] 10 | -------------------------------------------------------------------------------- /os/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "os" 3 | version = "0.1.0" 4 | authors = ["涂轶翔 ", "赵成钢 "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | algorithm = { path = 'src/algorithm' } 11 | bit_field = "0.10.0" 12 | bitflags = "1.2.1" 13 | buddy_system_allocator = "0.4.0" 14 | hashbrown = "0.8.1" 15 | lazy_static = { version = "1.4.0", features = ["spin_no_std"] } 16 | riscv = { git = "https://github.com/rcore-os/riscv", features = ["inline-asm"] } 17 | spin = "0.5.2" 18 | device_tree = { git = "https://github.com/rcore-os/device_tree-rs" } 19 | virtio-drivers = { git = "https://github.com/rcore-os/virtio-drivers" } 20 | rcore-fs = { git = "https://github.com/rcore-os/rcore-fs"} 21 | rcore-fs-sfs = { git = "https://github.com/rcore-os/rcore-fs"} 22 | xmas-elf = "0.7.0" 23 | 24 | # panic 时直接终止,因为我们没有实现堆栈展开的功能 25 | [profile.dev] 26 | panic = "abort" 27 | [profile.release] 28 | panic = "abort" 29 | -------------------------------------------------------------------------------- /os/Makefile: -------------------------------------------------------------------------------- 1 | TARGET := riscv64imac-unknown-none-elf 2 | MODE := debug 3 | KERNEL_FILE := target/$(TARGET)/$(MODE)/os 4 | BIN_FILE := target/$(TARGET)/$(MODE)/kernel.bin 5 | 6 | USER_DIR := ../user 7 | USER_BUILD := $(USER_DIR)/build 8 | IMG_FILE := $(USER_BUILD)/disk.img 9 | 10 | OBJDUMP := rust-objdump --arch-name=riscv64 11 | OBJCOPY := rust-objcopy --binary-architecture=riscv64 12 | 13 | .PHONY: doc kernel build clean qemu run env 14 | 15 | # 默认 build 为输出二进制文件 16 | build: $(BIN_FILE) 17 | 18 | # 通过 Rust 文件中的注释生成文档 19 | doc: 20 | @cargo doc --document-private-items 21 | 22 | # 编译 kernel 23 | kernel: 24 | @cargo build 25 | 26 | # 生成 kernel 的二进制文件 27 | $(BIN_FILE): kernel 28 | @$(OBJCOPY) $(KERNEL_FILE) --strip-all -O binary $@ 29 | 30 | # 查看反汇编结果 31 | asm: 32 | @$(OBJDUMP) -d $(KERNEL_FILE) | less 33 | 34 | # 清理编译出的文件 35 | clean: 36 | @cargo clean 37 | 38 | # 运行 QEMU 39 | qemu: build 40 | @qemu-system-riscv64 \ 41 | -machine virt \ 42 | -nographic \ 43 | -bios default \ 44 | -device loader,file=$(BIN_FILE),addr=0x80200000 \ 45 | -drive file=$(IMG_FILE),format=qcow2,id=sfs \ 46 | -device virtio-blk-device,drive=sfs 47 | 48 | # 一键运行 49 | run: build qemu 50 | 51 | # 一键 gdb 52 | debug: build 53 | @tmux new-session -d \ 54 | "qemu-system-riscv64 -machine virt -nographic -bios default -device loader,file=$(BIN_FILE),addr=0x80200000 \ 55 | -drive file=$(IMG_FILE),format=qcow2,id=sfs -device virtio-blk-device,drive=sfs -s -S" && \ 56 | tmux split-window -h "riscv64-unknown-elf-gdb -ex 'file $(KERNEL_FILE)' -ex 'set arch riscv:rv64' -ex 'target remote localhost:1234'" && \ 57 | tmux -2 attach-session -d -------------------------------------------------------------------------------- /os/src/algorithm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "algorithm" 3 | version = "0.1.0" 4 | authors = ["涂轶翔 ", "赵成钢 "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | bit_field = "0.10.0" 11 | -------------------------------------------------------------------------------- /os/src/algorithm/src/allocator/mod.rs: -------------------------------------------------------------------------------- 1 | //! 负责分配 / 回收的数据结构 2 | 3 | mod segment_tree_allocator; 4 | mod stacked_allocator; 5 | 6 | /// 分配器:固定容量,每次分配 / 回收一个元素 7 | pub trait Allocator { 8 | /// 给定容量,创建分配器 9 | fn new(capacity: usize) -> Self; 10 | /// 分配一个元素,无法分配则返回 `None` 11 | fn alloc(&mut self) -> Option; 12 | /// 回收一个元素 13 | fn dealloc(&mut self, index: usize); 14 | } 15 | 16 | pub use segment_tree_allocator::SegmentTreeAllocator; 17 | pub use stacked_allocator::StackedAllocator; 18 | 19 | /// 默认使用的分配器 20 | pub type AllocatorImpl = StackedAllocator; 21 | -------------------------------------------------------------------------------- /os/src/algorithm/src/allocator/segment_tree_allocator.rs: -------------------------------------------------------------------------------- 1 | //! 提供线段树实现的分配器 [`SegmentTreeAllocator`] 2 | 3 | use super::Allocator; 4 | use alloc::{vec, vec::Vec}; 5 | use bit_field::BitArray; 6 | 7 | /// 使用线段树实现分配器 8 | pub struct SegmentTreeAllocator { 9 | /// 树本身 10 | tree: Vec, 11 | } 12 | 13 | impl Allocator for SegmentTreeAllocator { 14 | fn new(capacity: usize) -> Self { 15 | assert!(capacity >= 8); 16 | // 完全二叉树的树叶数量 17 | let leaf_count = capacity.next_power_of_two(); 18 | let mut tree = vec![0u8; 2 * leaf_count]; 19 | // 去除尾部超出范围的空间 20 | for i in ((capacity + 7) / 8)..(leaf_count / 8) { 21 | tree[leaf_count / 8 + i] = 255u8; 22 | } 23 | for i in capacity..(capacity + 8) { 24 | tree.set_bit(leaf_count + i, true); 25 | } 26 | // 沿树枝向上计算 27 | for i in (1..leaf_count).rev() { 28 | let v = tree.get_bit(i * 2) && tree.get_bit(i * 2 + 1); 29 | tree.set_bit(i, v); 30 | } 31 | Self { tree } 32 | } 33 | 34 | fn alloc(&mut self) -> Option { 35 | if self.tree.get_bit(1) { 36 | None 37 | } else { 38 | let mut node = 1; 39 | // 递归查找直到找到一个值为 0 的树叶 40 | while node < self.tree.len() / 2 { 41 | if !self.tree.get_bit(node * 2) { 42 | node *= 2; 43 | } else if !self.tree.get_bit(node * 2 + 1) { 44 | node = node * 2 + 1; 45 | } else { 46 | panic!("tree is full or damaged"); 47 | } 48 | } 49 | // 检验 50 | assert!(!self.tree.get_bit(node), "tree is damaged"); 51 | // 修改树 52 | self.update_node(node, true); 53 | Some(node - self.tree.len() / 2) 54 | } 55 | } 56 | 57 | fn dealloc(&mut self, index: usize) { 58 | let node = index + self.tree.len() / 2; 59 | assert!(self.tree.get_bit(node)); 60 | self.update_node(node, false); 61 | } 62 | } 63 | 64 | impl SegmentTreeAllocator { 65 | /// 更新线段树中一个树叶,然后递归更新其祖先 66 | fn update_node(&mut self, mut index: usize, value: bool) { 67 | self.tree.set_bit(index, value); 68 | while index > 1 { 69 | index /= 2; 70 | let v = self.tree.get_bit(index * 2) && self.tree.get_bit(index * 2 + 1); 71 | self.tree.set_bit(index, v); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /os/src/algorithm/src/allocator/stacked_allocator.rs: -------------------------------------------------------------------------------- 1 | //! 提供栈结构实现的分配器 [`StackedAllocator`] 2 | 3 | use super::Allocator; 4 | use alloc::{vec, vec::Vec}; 5 | 6 | /// 使用栈结构实现分配器 7 | /// 8 | /// 在 `Vec` 末尾进行加入 / 删除。 9 | /// 每个元素 tuple `(start, end)` 表示 [start, end) 区间为可用。 10 | pub struct StackedAllocator { 11 | list: Vec<(usize, usize)>, 12 | } 13 | 14 | impl Allocator for StackedAllocator { 15 | fn new(capacity: usize) -> Self { 16 | Self { 17 | list: vec![(0, capacity)], 18 | } 19 | } 20 | 21 | fn alloc(&mut self) -> Option { 22 | if let Some((start, end)) = self.list.pop() { 23 | if end - start > 1 { 24 | self.list.push((start + 1, end)); 25 | } 26 | Some(start) 27 | } else { 28 | None 29 | } 30 | } 31 | 32 | fn dealloc(&mut self, index: usize) { 33 | self.list.push((index, index + 1)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /os/src/algorithm/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 一些可能用到,而又不好找库的数据结构 2 | //! 3 | //! 以及有多种实现,会留作业的数据结构 4 | #![no_std] 5 | #![feature(drain_filter)] 6 | 7 | extern crate alloc; 8 | 9 | mod allocator; 10 | mod scheduler; 11 | 12 | pub use allocator::*; 13 | pub use scheduler::*; 14 | -------------------------------------------------------------------------------- /os/src/algorithm/src/scheduler/fifo_scheduler.rs: -------------------------------------------------------------------------------- 1 | //! 先入先出队列的调度器 [`FifoScheduler`] 2 | 3 | use super::Scheduler; 4 | use alloc::collections::LinkedList; 5 | 6 | /// 采用 FIFO 算法的线程调度器 7 | pub struct FifoScheduler { 8 | pool: LinkedList, 9 | } 10 | 11 | /// `Default` 创建一个空的调度器 12 | impl Default for FifoScheduler { 13 | fn default() -> Self { 14 | Self { 15 | pool: LinkedList::new(), 16 | } 17 | } 18 | } 19 | 20 | impl Scheduler for FifoScheduler { 21 | type Priority = (); 22 | fn add_thread(&mut self, thread: ThreadType) { 23 | // 加入链表尾部 24 | self.pool.push_back(thread); 25 | } 26 | fn get_next(&mut self) -> Option { 27 | // 从头部取出放回尾部,同时将其返回 28 | if let Some(thread) = self.pool.pop_front() { 29 | self.pool.push_back(thread.clone()); 30 | Some(thread) 31 | } else { 32 | None 33 | } 34 | } 35 | fn remove_thread(&mut self, thread: &ThreadType) { 36 | // 移除相应的线程并且确认恰移除一个线程 37 | let mut removed = self.pool.drain_filter(|t| t == thread); 38 | assert!(removed.next().is_some() && removed.next().is_none()); 39 | } 40 | fn set_priority(&mut self, _thread: ThreadType, _priority: ()) {} 41 | } 42 | -------------------------------------------------------------------------------- /os/src/algorithm/src/scheduler/hrrn_scheduler.rs: -------------------------------------------------------------------------------- 1 | //! 最高响应比优先算法的调度器 [`HrrnScheduler`] 2 | 3 | use super::Scheduler; 4 | use alloc::collections::LinkedList; 5 | 6 | /// 将线程和调度信息打包 7 | struct HrrnThread { 8 | /// 进入线程池时,[`current_time`] 中的时间 9 | birth_time: usize, 10 | /// 被分配时间片的次数 11 | service_count: usize, 12 | /// 线程数据 13 | pub thread: ThreadType, 14 | } 15 | 16 | /// 采用 HRRN(最高响应比优先算法)的调度器 17 | pub struct HrrnScheduler { 18 | /// 当前时间,单位为 `get_next()` 调用次数 19 | current_time: usize, 20 | /// 带有调度信息的线程池 21 | pool: LinkedList>, 22 | } 23 | 24 | /// `Default` 创建一个空的调度器 25 | impl Default for HrrnScheduler { 26 | fn default() -> Self { 27 | Self { 28 | current_time: 0, 29 | pool: LinkedList::new(), 30 | } 31 | } 32 | } 33 | 34 | impl Scheduler for HrrnScheduler { 35 | type Priority = (); 36 | 37 | fn add_thread(&mut self, thread: ThreadType) { 38 | self.pool.push_back(HrrnThread { 39 | birth_time: self.current_time, 40 | service_count: 0, 41 | thread, 42 | }) 43 | } 44 | fn get_next(&mut self) -> Option { 45 | // 计时 46 | self.current_time += 1; 47 | 48 | // 遍历线程池,返回响应比最高者 49 | let current_time = self.current_time; // borrow-check 50 | if let Some(best) = self.pool.iter_mut().max_by(|x, y| { 51 | ((current_time - x.birth_time) * y.service_count) 52 | .cmp(&((current_time - y.birth_time) * x.service_count)) 53 | }) { 54 | best.service_count += 1; 55 | Some(best.thread.clone()) 56 | } else { 57 | None 58 | } 59 | } 60 | fn remove_thread(&mut self, thread: &ThreadType) { 61 | // 移除相应的线程并且确认恰移除一个线程 62 | let mut removed = self.pool.drain_filter(|t| t.thread == *thread); 63 | assert!(removed.next().is_some() && removed.next().is_none()); 64 | } 65 | fn set_priority(&mut self, _thread: ThreadType, _priority: ()) {} 66 | } 67 | -------------------------------------------------------------------------------- /os/src/algorithm/src/scheduler/mod.rs: -------------------------------------------------------------------------------- 1 | //! 线程调度算法 2 | 3 | mod fifo_scheduler; 4 | mod hrrn_scheduler; 5 | 6 | /// 线程调度器 7 | /// 8 | /// `ThreadType` 应为 `Arc` 9 | /// 10 | /// ### 使用方法 11 | /// - 在每一个时间片结束后,调用 [`Scheduler::get_next()`] 来获取下一个时间片应当执行的线程。 12 | /// 这个线程可能是上一个时间片所执行的线程。 13 | /// - 当一个线程结束时,需要调用 [`Scheduler::remove_thread()`] 来将其移除。这个方法必须在 14 | /// [`Scheduler::get_next()`] 之前调用。 15 | pub trait Scheduler: Default { 16 | /// 优先级的类型 17 | type Priority; 18 | /// 向线程池中添加一个线程 19 | fn add_thread(&mut self, thread: ThreadType); 20 | /// 获取下一个时间段应当执行的线程 21 | fn get_next(&mut self) -> Option; 22 | /// 移除一个线程 23 | fn remove_thread(&mut self, thread: &ThreadType); 24 | /// 设置线程的优先级 25 | fn set_priority(&mut self, thread: ThreadType, priority: Self::Priority); 26 | } 27 | 28 | pub use fifo_scheduler::FifoScheduler; 29 | pub use hrrn_scheduler::HrrnScheduler; 30 | 31 | pub type SchedulerImpl = HrrnScheduler; 32 | -------------------------------------------------------------------------------- /os/src/algorithm/src/unsafe_wrapper.rs: -------------------------------------------------------------------------------- 1 | //! 允许像 C 指针一样随意使用的 [`UnsafeWrapper`] 2 | 3 | // 所以在模块范围内不提示「未使用的函数」等警告 4 | #![allow(dead_code)] 5 | 6 | use alloc::boxed::Box; 7 | use core::cell::UnsafeCell; 8 | 9 | /// 允许从 &self 获取 &mut 内部变量 10 | pub struct UnsafeWrapper { 11 | object: UnsafeCell, 12 | } 13 | 14 | impl UnsafeWrapper { 15 | pub fn new(object: T) -> Self { 16 | Self { 17 | object: UnsafeCell::new(object), 18 | } 19 | } 20 | 21 | #[allow(clippy::mut_from_ref)] 22 | pub fn get(&self) -> &mut T { 23 | unsafe { &mut *self.object.get() } 24 | } 25 | } 26 | 27 | impl Default for UnsafeWrapper { 28 | fn default() -> Self { 29 | Self { 30 | object: UnsafeCell::new(T::default()), 31 | } 32 | } 33 | } 34 | 35 | unsafe impl Sync for UnsafeWrapper {} 36 | 37 | pub trait StaticUnsafeInit { 38 | fn static_unsafe_init() -> Self; 39 | } 40 | 41 | pub struct StaticUnsafeWrapper { 42 | pointer: UnsafeCell<*const UnsafeCell>, 43 | _phantom: core::marker::PhantomData, 44 | } 45 | 46 | impl StaticUnsafeWrapper { 47 | pub const fn new() -> Self { 48 | Self { 49 | pointer: UnsafeCell::new(0 as *const _), 50 | _phantom: core::marker::PhantomData, 51 | } 52 | } 53 | } 54 | 55 | impl StaticUnsafeWrapper { 56 | #[allow(clippy::mut_from_ref)] 57 | pub fn get(&self) -> &mut T { 58 | unsafe { 59 | if *self.pointer.get() as usize == 0 { 60 | let boxed = Box::new(UnsafeCell::new(T::default())); 61 | *self.pointer.get() = Box::into_raw(boxed); 62 | } 63 | &mut *(**self.pointer.get()).get() 64 | } 65 | } 66 | } 67 | 68 | impl core::ops::Deref for StaticUnsafeWrapper { 69 | type Target = T; 70 | fn deref(&self) -> &Self::Target { 71 | self.get() 72 | } 73 | } 74 | 75 | unsafe impl Sync for StaticUnsafeWrapper {} 76 | -------------------------------------------------------------------------------- /os/src/console.rs: -------------------------------------------------------------------------------- 1 | //! 实现控制台的字符输入和输出 2 | //! 3 | //! # 格式化输出 4 | //! 5 | //! [`core::fmt::Write`] trait 包含 6 | //! - 需要实现的 [`write_str`] 方法 7 | //! - 自带实现,但依赖于 [`write_str`] 的 [`write_fmt`] 方法 8 | //! 9 | //! 我们声明一个类型,为其实现 [`write_str`] 方法后,就可以使用 [`write_fmt`] 来进行格式化输出 10 | //! 11 | //! [`write_str`]: core::fmt::Write::write_str 12 | //! [`write_fmt`]: core::fmt::Write::write_fmt 13 | 14 | use crate::sbi::*; 15 | use core::fmt::{self, Write}; 16 | 17 | /// 一个 [Zero-Sized Type],实现 [`core::fmt::Write`] trait 来进行格式化输出 18 | /// 19 | /// ZST 只可能有一个值(即为空),因此它本身就是一个单件 20 | /// 21 | /// [Zero-Sized Type]: https://doc.rust-lang.org/nomicon/exotic-sizes.html#zero-sized-types-zsts 22 | struct Stdout; 23 | 24 | impl Write for Stdout { 25 | /// 打印一个字符串 26 | /// 27 | /// [`console_putchar`] sbi 调用每次接受一个 `usize`,但实际上会把它作为 `u8` 来打印字符。 28 | /// 因此,如果字符串中存在非 ASCII 字符,需要在 utf-8 编码下,对于每一个 `u8` 调用一次 [`console_putchar`] 29 | fn write_str(&mut self, s: &str) -> fmt::Result { 30 | let mut buffer = [0u8; 4]; 31 | for c in s.chars() { 32 | for code_point in c.encode_utf8(&mut buffer).as_bytes().iter() { 33 | console_putchar(*code_point as usize); 34 | } 35 | } 36 | Ok(()) 37 | } 38 | } 39 | 40 | /// 打印由 [`core::format_args!`] 格式化后的数据 41 | /// 42 | /// [`print!`] 和 [`println!`] 宏都将展开成此函数 43 | /// 44 | /// [`core::format_args!`]: https://doc.rust-lang.org/nightly/core/macro.format_args.html 45 | pub fn print(args: fmt::Arguments) { 46 | Stdout.write_fmt(args).unwrap(); 47 | } 48 | 49 | /// 实现类似于标准库中的 `print!` 宏 50 | /// 51 | /// 使用实现了 [`core::fmt::Write`] trait 的 [`console::Stdout`] 52 | #[macro_export] 53 | macro_rules! print { 54 | ($fmt: literal $(, $($arg: tt)+)?) => { 55 | $crate::console::print(format_args!($fmt $(, $($arg)+)?)); 56 | } 57 | } 58 | 59 | /// 实现类似于标准库中的 `println!` 宏 60 | /// 61 | /// 使用实现了 [`core::fmt::Write`] trait 的 [`console::Stdout`] 62 | #[macro_export] 63 | macro_rules! println { 64 | ($fmt: literal $(, $($arg: tt)+)?) => { 65 | $crate::console::print(format_args!(concat!($fmt, "\n") $(, $($arg)+)?)); 66 | } 67 | } 68 | 69 | /// 类似 `std::dbg` 宏 70 | /// 71 | /// 可以实现方便的对变量输出的效果 72 | #[macro_export] 73 | #[allow(unused_macros)] 74 | macro_rules! dbg { 75 | () => { 76 | println!("[{}:{}]", file!(), line!()); 77 | }; 78 | ($val:expr) => { 79 | match $val { 80 | tmp => { 81 | println!("[{}:{}] {} = {:#?}", 82 | file!(), line!(), stringify!($val), &tmp); 83 | tmp 84 | } 85 | } 86 | }; 87 | ($val:expr,) => { $crate::dbg!($val) }; 88 | ($($val:expr),+ $(,)?) => { 89 | ($($crate::dbg!($val)),+,) 90 | }; 91 | } 92 | 93 | /// 类似 `std::dbg` 宏(16 进制输出) 94 | /// 95 | /// 可以实现方便的对变量输出的效果(16 进制输出) 96 | #[macro_export] 97 | #[allow(unused_macros)] 98 | macro_rules! dbgx { 99 | () => { 100 | println!("[{}:{}]", file!(), line!()); 101 | }; 102 | ($val:expr) => { 103 | match $val { 104 | tmp => { 105 | println!("[{}:{}] {} = {:#x?}", 106 | file!(), line!(), stringify!($val), &tmp); 107 | tmp 108 | } 109 | } 110 | }; 111 | ($val:expr,) => { dbgx!($val) }; 112 | ($($val:expr),+ $(,)?) => { 113 | ($(dbgx!($val)),+,) 114 | }; 115 | } 116 | -------------------------------------------------------------------------------- /os/src/drivers/block/mod.rs: -------------------------------------------------------------------------------- 1 | //! 块设备抽象 2 | //! 3 | //! 目前仅仅实现了 virtio 协议的块设备,另外还有类似 AHCI 等协议 4 | 5 | use super::driver::Driver; 6 | use alloc::sync::Arc; 7 | use rcore_fs::dev; 8 | 9 | pub mod virtio_blk; 10 | 11 | /// 块设备抽象(驱动的引用) 12 | pub struct BlockDevice(pub Arc); 13 | 14 | /// 为 [`BlockDevice`] 实现 [`rcore-fs`] 中 [`BlockDevice`] trait 15 | /// 16 | /// 使得文件系统可以通过调用块设备的该接口来读写 17 | impl dev::BlockDevice for BlockDevice { 18 | /// 每个块的大小(取 2 的对数) 19 | /// 20 | /// 这里取 512B 是因为 virtio 驱动对设备的操作粒度为 512B 21 | const BLOCK_SIZE_LOG2: u8 = 9; 22 | 23 | /// 读取某个块到 buf 中 24 | fn read_at(&self, block_id: usize, buf: &mut [u8]) -> dev::Result<()> { 25 | match self.0.read_block(block_id, buf) { 26 | true => Ok(()), 27 | false => Err(dev::DevError), 28 | } 29 | } 30 | 31 | /// 将 buf 中的数据写入块中 32 | fn write_at(&self, block_id: usize, buf: &[u8]) -> dev::Result<()> { 33 | match self.0.write_block(block_id, buf) { 34 | true => Ok(()), 35 | false => Err(dev::DevError), 36 | } 37 | } 38 | 39 | /// 执行和设备的同步 40 | /// 41 | /// 因为我们这里全部为阻塞 I/O 所以不存在同步的问题 42 | fn sync(&self) -> dev::Result<()> { 43 | Ok(()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /os/src/drivers/block/virtio_blk.rs: -------------------------------------------------------------------------------- 1 | use super::super::driver::{DeviceType, Driver, DRIVERS}; 2 | use alloc::sync::Arc; 3 | use spin::Mutex; 4 | use virtio_drivers::{VirtIOBlk, VirtIOHeader}; 5 | 6 | /// virtio 协议的块设备驱动 7 | struct VirtIOBlkDriver(Mutex>); 8 | 9 | /// 为 [`VirtIOBlkDriver`] 实现 [`Driver`] trait 10 | /// 11 | /// 调用了 [`virtio_drivers`] 库,其中规定的块大小为 512B 12 | impl Driver for VirtIOBlkDriver { 13 | /// 设备类型 14 | fn device_type(&self) -> DeviceType { 15 | DeviceType::Block 16 | } 17 | 18 | /// 读取某个块到 buf 中 19 | fn read_block(&self, block_id: usize, buf: &mut [u8]) -> bool { 20 | self.0.lock().read_block(block_id, buf).is_ok() 21 | } 22 | 23 | /// 将 buf 中的数据写入块中 24 | fn write_block(&self, block_id: usize, buf: &[u8]) -> bool { 25 | self.0.lock().write_block(block_id, buf).is_ok() 26 | } 27 | } 28 | 29 | /// 将从设备树中读取出的设备信息放到 [`static@DRIVERS`] 中 30 | pub fn add_driver(header: &'static mut VirtIOHeader) { 31 | let virtio_blk = VirtIOBlk::new(header).expect("failed to init blk driver"); 32 | let driver = Arc::new(VirtIOBlkDriver(Mutex::new(virtio_blk))); 33 | DRIVERS.write().push(driver); 34 | } 35 | -------------------------------------------------------------------------------- /os/src/drivers/bus/mod.rs: -------------------------------------------------------------------------------- 1 | //! 总线协议驱动 2 | //! 3 | //! 目前仅仅实现了 virtio MMIO 协议,另外还有类似 PCI 等协议 4 | //! MMIO 指通过读写特定内存段来实现设备交互 5 | 6 | pub mod virtio_mmio; 7 | -------------------------------------------------------------------------------- /os/src/drivers/bus/virtio_mmio.rs: -------------------------------------------------------------------------------- 1 | //! virtio MMIO 总线协议驱动 2 | //! 3 | //! 目前仅仅实现了 virtio Block Device 协议,另外还有类似 virtio Network 等协议 4 | 5 | use super::super::block::virtio_blk; 6 | use crate::memory::{ 7 | frame::{FrameTracker, FRAME_ALLOCATOR}, 8 | mapping::Mapping, 9 | PhysicalAddress, VirtualAddress, PAGE_SIZE, 10 | }; 11 | use alloc::collections::btree_map::BTreeMap; 12 | use device_tree::{util::SliceRead, Node}; 13 | use lazy_static::lazy_static; 14 | use spin::RwLock; 15 | use virtio_drivers::{DeviceType, VirtIOHeader}; 16 | 17 | /// 从设备树的某个节点探测 virtio 协议具体类型 18 | pub fn virtio_probe(node: &Node) { 19 | // reg 属性中包含了描述设备的 Header 的位置 20 | let reg = match node.prop_raw("reg") { 21 | Some(reg) => reg, 22 | _ => return, 23 | }; 24 | let pa = PhysicalAddress(reg.as_slice().read_be_u64(0).unwrap() as usize); 25 | let va = VirtualAddress::from(pa); 26 | let header = unsafe { &mut *(va.0 as *mut VirtIOHeader) }; 27 | // 目前只支持某个特定版本的 virtio 协议 28 | if !header.verify() { 29 | return; 30 | } 31 | // 判断设备类型 32 | match header.device_type() { 33 | DeviceType::Block => virtio_blk::add_driver(header), 34 | device => println!("unrecognized virtio device: {:?}", device), 35 | } 36 | } 37 | 38 | lazy_static! { 39 | /// 用于放置给设备 DMA 所用的物理页([`FrameTracker`]) 40 | pub static ref TRACKERS: RwLock> = 41 | RwLock::new(BTreeMap::new()); 42 | } 43 | 44 | /// 为 DMA 操作申请连续 pages 个物理页(为 [`virtio_drivers`] 库提供) 45 | /// 46 | /// 为什么要求连续的物理内存?设备的 DMA 操作只涉及到内存和对应设备 47 | /// 这个过程不会涉及到 CPU 的 MMU 机制,我们只能给设备传递物理地址 48 | /// 而陷于我们之前每次只能分配一个物理页的设计,这里我们假设我们连续分配的地址是连续的 49 | #[no_mangle] 50 | extern "C" fn virtio_dma_alloc(pages: usize) -> PhysicalAddress { 51 | let mut pa: PhysicalAddress = Default::default(); 52 | let mut last: PhysicalAddress = Default::default(); 53 | for i in 0..pages { 54 | let tracker: FrameTracker = FRAME_ALLOCATOR.lock().alloc().unwrap(); 55 | if i == 0 { 56 | pa = tracker.address(); 57 | } else { 58 | assert_eq!(last + PAGE_SIZE, tracker.address()); 59 | } 60 | last = tracker.address(); 61 | TRACKERS.write().insert(last, tracker); 62 | } 63 | pa 64 | } 65 | 66 | /// 为 DMA 操作释放对应的之前申请的连续的物理页(为 [`virtio_drivers`] 库提供) 67 | #[no_mangle] 68 | extern "C" fn virtio_dma_dealloc(pa: PhysicalAddress, pages: usize) -> i32 { 69 | for i in 0..pages { 70 | TRACKERS.write().remove(&(pa + i * PAGE_SIZE)); 71 | } 72 | 0 73 | } 74 | 75 | /// 将物理地址转为虚拟地址(为 [`virtio_drivers`] 库提供) 76 | /// 77 | /// 需要注意,我们在 0xffffffff80200000 到 0xffffffff88000000 是都有对应的物理地址映射的 78 | /// 因为在内核重映射的时候,我们已经把全部的段放进去了 79 | /// 所以物理地址直接加上 Offset 得到的虚拟地址是可以通过任何内核进程的页表来访问的 80 | #[no_mangle] 81 | extern "C" fn virtio_phys_to_virt(pa: PhysicalAddress) -> VirtualAddress { 82 | VirtualAddress::from(pa) 83 | } 84 | 85 | /// 将虚拟地址转为物理地址(为 [`virtio_drivers`] 库提供) 86 | /// 87 | /// 需要注意,实现这个函数的目的是告诉 DMA 具体的请求,请求在实现中会放在栈上面 88 | /// 而在我们的实现中,栈是以 Framed 的形式分配的,并不是高地址的线性映射 Linear 89 | /// 为了得到正确的物理地址并告诉 DMA 设备,我们只能查页表 90 | #[no_mangle] 91 | extern "C" fn virtio_virt_to_phys(va: VirtualAddress) -> PhysicalAddress { 92 | Mapping::lookup(va).unwrap() 93 | } 94 | -------------------------------------------------------------------------------- /os/src/drivers/device_tree.rs: -------------------------------------------------------------------------------- 1 | //! 设备树读取 2 | //! 3 | //! 递归遍历设备树并初始化 4 | 5 | use super::bus::virtio_mmio::virtio_probe; 6 | use crate::memory::VirtualAddress; 7 | use core::slice; 8 | use device_tree::{DeviceTree, Node}; 9 | 10 | /// 验证某内存段为设备树格式的 Magic Number(固定) 11 | const DEVICE_TREE_MAGIC: u32 = 0xd00d_feed; 12 | 13 | /// 递归遍历设备树 14 | fn walk(node: &Node) { 15 | // 检查设备的协议支持并初始化 16 | if let Ok(compatible) = node.prop_str("compatible") { 17 | if compatible == "virtio,mmio" { 18 | virtio_probe(node); 19 | } 20 | } 21 | // 遍历子树 22 | for child in node.children.iter() { 23 | walk(child); 24 | } 25 | } 26 | 27 | /// 整个设备树的 Headers(用于验证和读取) 28 | struct DtbHeader { 29 | magic: u32, 30 | size: u32, 31 | } 32 | 33 | /// 遍历设备树并初始化设备 34 | pub fn init(dtb_va: VirtualAddress) { 35 | let header = unsafe { &*(dtb_va.0 as *const DtbHeader) }; 36 | // from_be 是大小端序的转换(from big endian) 37 | let magic = u32::from_be(header.magic); 38 | if magic == DEVICE_TREE_MAGIC { 39 | let size = u32::from_be(header.size); 40 | // 拷贝数据,加载并遍历 41 | let data = unsafe { slice::from_raw_parts(dtb_va.0 as *const u8, size as usize) }; 42 | if let Ok(dt) = DeviceTree::load(data) { 43 | walk(&dt.root); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /os/src/drivers/driver.rs: -------------------------------------------------------------------------------- 1 | //! 驱动接口的定义 2 | //! 3 | //! 目前接口中只支持块设备类型 4 | 5 | use alloc::{sync::Arc, vec::Vec}; 6 | use lazy_static::lazy_static; 7 | use spin::RwLock; 8 | 9 | /// 驱动类型 10 | /// 11 | /// 目前只有块设备,可能还有网络、GPU 设备等 12 | #[derive(Debug, Eq, PartialEq)] 13 | pub enum DeviceType { 14 | Block, 15 | } 16 | 17 | /// 驱动的接口 18 | pub trait Driver: Send + Sync { 19 | /// 设备类型 20 | fn device_type(&self) -> DeviceType; 21 | 22 | /// 读取某个块到 buf 中(块设备接口) 23 | fn read_block(&self, _block_id: usize, _buf: &mut [u8]) -> bool { 24 | unimplemented!("not a block driver") 25 | } 26 | 27 | /// 将 buf 中的数据写入块中(块设备接口) 28 | fn write_block(&self, _block_id: usize, _buf: &[u8]) -> bool { 29 | unimplemented!("not a block driver") 30 | } 31 | } 32 | 33 | lazy_static! { 34 | /// 所有驱动 35 | pub static ref DRIVERS: RwLock>> = RwLock::new(Vec::new()); 36 | } 37 | -------------------------------------------------------------------------------- /os/src/drivers/mod.rs: -------------------------------------------------------------------------------- 1 | //! 驱动模块 2 | //! 3 | //! 负责驱动管理 4 | 5 | use crate::memory::{PhysicalAddress, VirtualAddress}; 6 | 7 | pub mod block; 8 | pub mod bus; 9 | pub mod device_tree; 10 | pub mod driver; 11 | 12 | /// 从设备树的物理地址来获取全部设备信息并初始化 13 | pub fn init(dtb_pa: PhysicalAddress) { 14 | let dtb_va = VirtualAddress::from(dtb_pa); 15 | device_tree::init(dtb_va); 16 | println!("mod driver initialized") 17 | } 18 | -------------------------------------------------------------------------------- /os/src/entry.asm: -------------------------------------------------------------------------------- 1 | # 操作系统启动时所需的指令以及字段 2 | # 3 | # 我们在 linker.ld 中将程序入口设置为了 _start,因此在这里我们将填充这个标签 4 | # 它将会执行一些必要操作,然后跳转至我们用 rust 编写的入口函数 5 | # 6 | # 关于 RISC-V 下的汇编语言,可以参考 https://github.com/riscv/riscv-asm-manual/blob/master/riscv-asm.md 7 | # %hi 表示取 [12,32) 位,%lo 表示取 [0,12) 位 8 | 9 | .section .text.entry 10 | .globl _start 11 | # 目前 _start 的功能:将预留的栈空间写入 $sp,然后跳转至 rust_main 12 | _start: 13 | # 通过线性映射关系计算 boot_page_table 的物理页号 14 | lui t0, %hi(boot_page_table) 15 | li t1, 0xffffffff00000000 16 | sub t0, t0, t1 17 | srli t0, t0, 12 18 | # 8 << 60 是 satp 中使用 Sv39 模式的记号 19 | li t1, (8 << 60) 20 | or t0, t0, t1 21 | # 写入 satp 并更新 TLB 22 | csrw satp, t0 23 | sfence.vma 24 | 25 | # 加载栈的虚拟地址 26 | lui sp, %hi(boot_stack_top) 27 | addi sp, sp, %lo(boot_stack_top) 28 | # 跳转至 rust_main 29 | # 这里同时伴随 hart 和 dtb_pa 两个指针的传入(是 OpenSBI 帮我们完成的) 30 | lui t0, %hi(rust_main) 31 | addi t0, t0, %lo(rust_main) 32 | jr t0 33 | 34 | # 回忆:bss 段是 ELF 文件中只记录长度,而全部初始化为 0 的一段内存空间 35 | # 这里声明字段 .bss.stack 作为操作系统启动时的栈 36 | .section .bss.stack 37 | .global boot_stack 38 | boot_stack: 39 | # 16K 启动栈大小 40 | .space 4096 * 16 41 | .global boot_stack_top 42 | boot_stack_top: 43 | # 栈结尾 44 | 45 | # 初始内核映射所用的页表 46 | .section .data 47 | .align 12 48 | .global boot_page_table 49 | boot_page_table: 50 | # .8byte表示长度为8个字节的整数 51 | .8byte 0 52 | .8byte 0 53 | # 第 2 项:0x8000_0000 -> 0x8000_0000,0xcf 表示 VRWXAD 均为 1 54 | .8byte (0x80000 << 10) | 0xcf 55 | .zero 505 * 8 56 | # 第 508 项(外设用):0xffff_ffff_0000_0000 -> 0x0000_0000,0xcf 表示 VRWXAD 均为 1 57 | .8byte (0x00000 << 10) | 0xcf 58 | .8byte 0 59 | # 第 510 项:0xffff_ffff_8000_0000 -> 0x8000_0000,0xcf 表示 VRWXAD 均为 1 60 | .8byte (0x80000 << 10) | 0xcf 61 | .8byte 0 62 | -------------------------------------------------------------------------------- /os/src/fs/config.rs: -------------------------------------------------------------------------------- 1 | //! 文件系统的配置信息 2 | 3 | /// 块设备的 Cache 块个数 4 | pub const BLOCK_CACHE_CAPACITY: usize = 0x10; 5 | -------------------------------------------------------------------------------- /os/src/fs/inode_ext.rs: -------------------------------------------------------------------------------- 1 | //! 为 [`INode`] 实现 trait [`INodeExt`] 以扩展功能 2 | 3 | use super::*; 4 | 5 | /// 为 [`INode`] 类型添加的扩展功能 6 | pub trait INodeExt { 7 | /// 打印当前目录的文件 8 | fn ls(&self); 9 | 10 | /// 读取文件内容 11 | fn readall(&self) -> Result>; 12 | } 13 | 14 | impl INodeExt for dyn INode { 15 | fn ls(&self) { 16 | let mut id = 0; 17 | while let Ok(name) = self.get_entry(id) { 18 | println!("{}", name); 19 | id += 1; 20 | } 21 | } 22 | 23 | fn readall(&self) -> Result> { 24 | // 从文件头读取长度 25 | let size = self.metadata()?.size; 26 | // 构建 Vec 并读取 27 | let mut buffer = Vec::with_capacity(size); 28 | unsafe { buffer.set_len(size) }; 29 | self.read_at(0, buffer.as_mut_slice())?; 30 | Ok(buffer) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /os/src/fs/mod.rs: -------------------------------------------------------------------------------- 1 | //! 文件系统 2 | //! 3 | //! 将读取第一个块设备作为根文件系统 4 | 5 | use crate::drivers::{ 6 | block::BlockDevice, 7 | driver::{DeviceType, DRIVERS}, 8 | }; 9 | use crate::kernel::Condvar; 10 | use alloc::{sync::Arc, vec::Vec}; 11 | use core::any::Any; 12 | use lazy_static::lazy_static; 13 | use rcore_fs_sfs::SimpleFileSystem; 14 | use spin::Mutex; 15 | 16 | mod config; 17 | mod inode_ext; 18 | mod stdin; 19 | mod stdout; 20 | 21 | pub use config::*; 22 | pub use inode_ext::INodeExt; 23 | pub use rcore_fs::{dev::block_cache::BlockCache, vfs::*}; 24 | pub use stdin::STDIN; 25 | pub use stdout::STDOUT; 26 | 27 | lazy_static! { 28 | /// 根文件系统的根目录的 INode 29 | pub static ref ROOT_INODE: Arc = { 30 | // 选择第一个块设备 31 | for driver in DRIVERS.read().iter() { 32 | if driver.device_type() == DeviceType::Block { 33 | let device = BlockDevice(driver.clone()); 34 | // 动态分配一段内存空间作为设备 Cache 35 | let device_with_cache = Arc::new(BlockCache::new(device, BLOCK_CACHE_CAPACITY)); 36 | return SimpleFileSystem::open(device_with_cache) 37 | .expect("failed to open SFS") 38 | .root_inode(); 39 | } 40 | } 41 | panic!("failed to load fs") 42 | }; 43 | } 44 | 45 | /// 触发 [`static@ROOT_INODE`] 的初始化并打印根目录内容 46 | pub fn init() { 47 | ROOT_INODE.ls(); 48 | println!("mod fs initialized"); 49 | } 50 | -------------------------------------------------------------------------------- /os/src/fs/stdin.rs: -------------------------------------------------------------------------------- 1 | //! 键盘输入 [`Stdin`] 2 | 3 | use super::*; 4 | use alloc::collections::VecDeque; 5 | 6 | lazy_static! { 7 | pub static ref STDIN: Arc = Default::default(); 8 | } 9 | 10 | /// 控制台键盘输入,实现 [`INode`] 接口 11 | #[derive(Default)] 12 | pub struct Stdin { 13 | /// 从后插入,前段弹出 14 | buffer: Mutex>, 15 | /// 条件变量用于使等待输入的线程休眠 16 | condvar: Condvar, 17 | } 18 | 19 | impl INode for Stdin { 20 | /// Read bytes at `offset` into `buf`, return the number of bytes read. 21 | fn read_at(&self, offset: usize, buf: &mut [u8]) -> Result { 22 | if offset != 0 { 23 | // 不支持 offset 24 | Err(FsError::NotSupported) 25 | } else if self.buffer.lock().len() == 0 { 26 | // 缓冲区没有数据,将当前线程休眠 27 | self.condvar.wait(); 28 | Ok(0) 29 | } else { 30 | let mut stdin_buffer = self.buffer.lock(); 31 | for (i, byte) in buf.iter_mut().enumerate() { 32 | if let Some(b) = stdin_buffer.pop_front() { 33 | *byte = b; 34 | } else { 35 | return Ok(i); 36 | } 37 | } 38 | Ok(buf.len()) 39 | } 40 | } 41 | 42 | /// Write bytes at `offset` from `buf`, return the number of bytes written. 43 | fn write_at(&self, _offset: usize, _buf: &[u8]) -> Result { 44 | Err(FsError::NotSupported) 45 | } 46 | 47 | fn poll(&self) -> Result { 48 | Err(FsError::NotSupported) 49 | } 50 | 51 | /// This is used to implement dynamics cast. 52 | /// Simply return self in the implement of the function. 53 | fn as_any_ref(&self) -> &dyn Any { 54 | self 55 | } 56 | } 57 | 58 | impl Stdin { 59 | /// 向缓冲区插入一个字符,然后唤起一个线程 60 | pub fn push(&self, c: u8) { 61 | self.buffer.lock().push_back(c); 62 | self.condvar.notify_one(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /os/src/fs/stdout.rs: -------------------------------------------------------------------------------- 1 | //! 控制台输出 [`Stdout`] 2 | 3 | use super::*; 4 | 5 | lazy_static! { 6 | pub static ref STDOUT: Arc = Default::default(); 7 | } 8 | 9 | /// 控制台输出 10 | #[derive(Default)] 11 | pub struct Stdout; 12 | 13 | impl INode for Stdout { 14 | fn write_at(&self, offset: usize, buf: &[u8]) -> Result { 15 | if offset != 0 { 16 | Err(FsError::NotSupported) 17 | } else if let Ok(string) = core::str::from_utf8(buf) { 18 | print!("{}", string); 19 | Ok(buf.len()) 20 | } else { 21 | Err(FsError::InvalidParam) 22 | } 23 | } 24 | 25 | /// Read bytes at `offset` into `buf`, return the number of bytes read. 26 | fn read_at(&self, _offset: usize, _buf: &mut [u8]) -> Result { 27 | Err(FsError::NotSupported) 28 | } 29 | 30 | fn poll(&self) -> Result { 31 | Err(FsError::NotSupported) 32 | } 33 | 34 | /// This is used to implement dynamics cast. 35 | /// Simply return self in the implement of the function. 36 | fn as_any_ref(&self) -> &dyn Any { 37 | self 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /os/src/interrupt/context.rs: -------------------------------------------------------------------------------- 1 | //! 保存现场所用的 struct [`Context`] 2 | 3 | use core::mem::zeroed; 4 | use riscv::register::sstatus::{self, Sstatus, SPP::*}; 5 | 6 | /// 发生中断时,保存的寄存器 7 | /// 8 | /// 包括所有通用寄存器,以及: 9 | /// - `sstatus`:各种状态位 10 | /// - `sepc`:产生中断的地址 11 | /// 12 | /// ### `#[repr(C)]` 属性 13 | /// 要求 struct 按照 C 语言的规则进行内存分布,否则 Rust 可能按照其他规则进行内存排布 14 | #[repr(C)] 15 | #[derive(Clone, Copy, Debug)] 16 | pub struct Context { 17 | /// 通用寄存器 18 | pub x: [usize; 32], 19 | /// 保存诸多状态位的特权态寄存器 20 | pub sstatus: Sstatus, 21 | /// 保存中断地址的特权态寄存器 22 | pub sepc: usize, 23 | } 24 | 25 | /// 创建一个用 0 初始化的 Context 26 | /// 27 | /// 这里使用 [`core::mem::zeroed()`] 来强行用全 0 初始化。 28 | /// 因为在一些类型中,0 数值可能不合法(例如引用),所以 [`zeroed()`] 是 unsafe 的 29 | impl Default for Context { 30 | fn default() -> Self { 31 | unsafe { zeroed() } 32 | } 33 | } 34 | 35 | #[allow(unused)] 36 | impl Context { 37 | /// 获取栈指针 38 | pub fn sp(&self) -> usize { 39 | self.x[2] 40 | } 41 | 42 | /// 设置栈指针 43 | pub fn set_sp(&mut self, value: usize) -> &mut Self { 44 | self.x[2] = value; 45 | self 46 | } 47 | 48 | /// 获取返回地址 49 | pub fn ra(&self) -> usize { 50 | self.x[1] 51 | } 52 | 53 | /// 设置返回地址 54 | pub fn set_ra(&mut self, value: usize) -> &mut Self { 55 | self.x[1] = value; 56 | self 57 | } 58 | 59 | /// 按照函数调用规则写入参数 60 | /// 61 | /// 没有考虑一些特殊情况,例如超过 8 个参数,或 struct 空间展开 62 | pub fn set_arguments(&mut self, arguments: &[usize]) -> &mut Self { 63 | assert!(arguments.len() <= 8); 64 | self.x[10..(10 + arguments.len())].copy_from_slice(arguments); 65 | self 66 | } 67 | 68 | /// 为线程构建初始 `Context` 69 | pub fn new( 70 | stack_top: usize, 71 | entry_point: usize, 72 | arguments: Option<&[usize]>, 73 | is_user: bool, 74 | ) -> Self { 75 | let mut context = Self::default(); 76 | 77 | // 设置栈顶指针 78 | context.set_sp(stack_top); 79 | // 设置初始参数 80 | if let Some(args) = arguments { 81 | context.set_arguments(args); 82 | } 83 | // 设置入口地址 84 | context.sepc = entry_point; 85 | 86 | // 设置 sstatus 87 | context.sstatus = sstatus::read(); 88 | if is_user { 89 | context.sstatus.set_spp(User); 90 | } else { 91 | context.sstatus.set_spp(Supervisor); 92 | } 93 | // 这样设置 SPIE 位,使得替换 sstatus 后关闭中断, 94 | // 而在 sret 到用户线程时开启中断。详见 SPIE 和 SIE 的定义 95 | context.sstatus.set_spie(true); 96 | 97 | context 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /os/src/interrupt/handler.rs: -------------------------------------------------------------------------------- 1 | use super::context::Context; 2 | use super::timer; 3 | use crate::fs::STDIN; 4 | use crate::kernel::syscall_handler; 5 | use crate::memory::*; 6 | use crate::process::PROCESSOR; 7 | use crate::sbi::console_getchar; 8 | use riscv::register::{ 9 | scause::{Exception, Interrupt, Scause, Trap}, 10 | sie, stvec, 11 | }; 12 | 13 | global_asm!(include_str!("./interrupt.asm")); 14 | 15 | /// 初始化中断处理 16 | /// 17 | /// 把中断入口 `__interrupt` 写入 `stvec` 中,并且开启中断使能 18 | pub fn init() { 19 | unsafe { 20 | extern "C" { 21 | /// `interrupt.asm` 中的中断入口 22 | fn __interrupt(); 23 | } 24 | // 使用 Direct 模式,将中断入口设置为 `__interrupt` 25 | stvec::write(__interrupt as usize, stvec::TrapMode::Direct); 26 | 27 | // 开启外部中断使能 28 | sie::set_sext(); 29 | 30 | // 在 OpenSBI 中开启外部中断 31 | *PhysicalAddress(0x0c00_2080).deref_kernel() = 1u32 << 10; 32 | // 在 OpenSBI 中开启串口 33 | *PhysicalAddress(0x1000_0004).deref_kernel() = 0x0bu8; 34 | *PhysicalAddress(0x1000_0001).deref_kernel() = 0x01u8; 35 | // 其他一些外部中断相关魔数 36 | *PhysicalAddress(0x0C00_0028).deref_kernel() = 0x07u32; 37 | *PhysicalAddress(0x0C20_1000).deref_kernel() = 0u32; 38 | } 39 | } 40 | 41 | /// 中断的处理入口 42 | /// 43 | /// `interrupt.asm` 首先保存寄存器至 Context,其作为参数和 scause 以及 stval 一并传入此函数 44 | /// 具体的中断类型需要根据 scause 来推断,然后分别处理 45 | #[no_mangle] 46 | pub fn handle_interrupt(context: &mut Context, scause: Scause, stval: usize) -> *mut Context { 47 | // 首先检查线程是否已经结束(内核线程会自己设置标记来结束自己) 48 | { 49 | let mut processor = PROCESSOR.lock(); 50 | let current_thread = processor.current_thread(); 51 | if current_thread.as_ref().inner().dead { 52 | println!("thread {} exit", current_thread.id); 53 | processor.kill_current_thread(); 54 | return processor.prepare_next_thread(); 55 | } 56 | } 57 | // 根据中断类型来处理,返回的 Context 必须位于放在内核栈顶 58 | match scause.cause() { 59 | // 断点中断(ebreak) 60 | Trap::Exception(Exception::Breakpoint) => breakpoint(context), 61 | // 系统调用 62 | Trap::Exception(Exception::UserEnvCall) => syscall_handler(context), 63 | // 时钟中断 64 | Trap::Interrupt(Interrupt::SupervisorTimer) => supervisor_timer(context), 65 | // 外部中断(键盘输入) 66 | Trap::Interrupt(Interrupt::SupervisorExternal) => supervisor_external(context), 67 | // 其他情况,无法处理 68 | _ => fault("unimplemented interrupt type", scause, stval), 69 | } 70 | } 71 | 72 | /// 处理 ebreak 断点 73 | /// 74 | /// 继续执行,其中 `sepc` 增加 2 字节,以跳过当前这条 `ebreak` 指令 75 | fn breakpoint(context: &mut Context) -> *mut Context { 76 | println!("Breakpoint at 0x{:x}", context.sepc); 77 | context.sepc += 2; 78 | context 79 | } 80 | 81 | /// 处理时钟中断 82 | fn supervisor_timer(context: &mut Context) -> *mut Context { 83 | timer::tick(); 84 | PROCESSOR.lock().park_current_thread(context); 85 | PROCESSOR.lock().prepare_next_thread() 86 | } 87 | 88 | /// 处理外部中断,只实现了键盘输入 89 | fn supervisor_external(context: &mut Context) -> *mut Context { 90 | let mut c = console_getchar(); 91 | if c <= 255 { 92 | if c == '\r' as usize { 93 | c = '\n' as usize; 94 | } 95 | STDIN.push(c as u8); 96 | } 97 | context 98 | } 99 | 100 | /// 出现未能解决的异常,终止当前线程 101 | fn fault(msg: &str, scause: Scause, stval: usize) -> *mut Context { 102 | println!( 103 | "{:#x?} terminated: {}", 104 | PROCESSOR.lock().current_thread(), 105 | msg 106 | ); 107 | println!("cause: {:?}, stval: {:x}", scause.cause(), stval); 108 | 109 | PROCESSOR.lock().kill_current_thread(); 110 | // 跳转到 PROCESSOR 调度的下一个线程 111 | PROCESSOR.lock().prepare_next_thread() 112 | } 113 | -------------------------------------------------------------------------------- /os/src/interrupt/interrupt.asm: -------------------------------------------------------------------------------- 1 | # 我们将会用一个宏来用循环保存寄存器。这是必要的设置 2 | .altmacro 3 | # 寄存器宽度对应的字节数 4 | .set REG_SIZE, 8 5 | # Context 的大小 6 | .set CONTEXT_SIZE, 34 7 | 8 | # 宏:将寄存器存到栈上 9 | .macro SAVE reg, offset 10 | sd \reg, \offset * REG_SIZE(sp) 11 | .endm 12 | 13 | # 宏:将寄存器从栈中取出 14 | .macro LOAD reg, offset 15 | ld \reg, \offset * REG_SIZE(sp) 16 | .endm 17 | 18 | # 宏:将 n 号寄存器保存在第 n 个位置 19 | .macro SAVE_N n 20 | SAVE x\n, n 21 | .endm 22 | 23 | # 宏:将 n 号寄存器从第 n 个位置取出 24 | .macro LOAD_N n 25 | LOAD x\n, n 26 | .endm 27 | 28 | .section .text 29 | .globl __interrupt 30 | # 进入中断 31 | # 保存 Context 并且进入 Rust 中的中断处理函数 interrupt::handler::handle_interrupt() 32 | __interrupt: 33 | # 因为线程当前的栈不一定可用,必须切换到内核栈来保存 Context 并进行中断流程 34 | # 因此,我们使用 sscratch 寄存器保存内核栈地址 35 | # 思考:sscratch 的值最初是在什么地方写入的? 36 | 37 | # 交换 sp 和 sscratch(切换到内核栈) 38 | csrrw sp, sscratch, sp 39 | # 在内核栈开辟 Context 的空间 40 | addi sp, sp, -CONTEXT_SIZE * REG_SIZE 41 | 42 | # 保存通用寄存器,除了 x0(固定为 0) 43 | SAVE x1, 1 44 | # 将本来的栈地址 sp(即 x2)保存 45 | csrr x1, sscratch 46 | SAVE x1, 2 47 | # 保存 x3 至 x31 48 | .set n, 3 49 | .rept 29 50 | SAVE_N %n 51 | .set n, n + 1 52 | .endr 53 | 54 | # 取出 CSR 并保存 55 | csrr t0, sstatus 56 | csrr t1, sepc 57 | SAVE t0, 32 58 | SAVE t1, 33 59 | # 调用 handle_interrupt,传入参数 60 | # context: &mut Context 61 | mv a0, sp 62 | # scause: Scause 63 | csrr a1, scause 64 | # stval: usize 65 | csrr a2, stval 66 | jal handle_interrupt 67 | 68 | .globl __restore 69 | # 离开中断 70 | # 此时内核栈顶被推入了一个 Context,而 a0 指向它 71 | # 接下来从 Context 中恢复所有寄存器,并将 Context 出栈(用 sscratch 记录内核栈地址) 72 | # 最后跳转至恢复的 sepc 的位置 73 | __restore: 74 | # 从 a0 中读取 sp 75 | # 思考:a0 是在哪里被赋值的?(有两种情况) 76 | mv sp, a0 77 | # 恢复 CSR 78 | LOAD t0, 32 79 | LOAD t1, 33 80 | csrw sstatus, t0 81 | csrw sepc, t1 82 | # 将内核栈地址写入 sscratch 83 | addi t0, sp, CONTEXT_SIZE * REG_SIZE 84 | csrw sscratch, t0 85 | 86 | # 恢复通用寄存器 87 | LOAD x1, 1 88 | # 恢复 x3 至 x31 89 | .set n, 3 90 | .rept 29 91 | LOAD_N %n 92 | .set n, n + 1 93 | .endr 94 | 95 | # 恢复 sp(又名 x2)这里最后恢复是为了上面可以正常使用 LOAD 宏 96 | LOAD x2, 2 97 | sret -------------------------------------------------------------------------------- /os/src/interrupt/mod.rs: -------------------------------------------------------------------------------- 1 | //! 中断模块 2 | //! 3 | //! 4 | 5 | mod context; 6 | mod handler; 7 | mod timer; 8 | 9 | pub use context::Context; 10 | 11 | /// 初始化中断相关的子模块 12 | /// 13 | /// - [`handler::init`] 14 | /// - [`timer::init`] 15 | pub fn init() { 16 | handler::init(); 17 | timer::init(); 18 | println!("mod interrupt initialized"); 19 | } 20 | -------------------------------------------------------------------------------- /os/src/interrupt/timer.rs: -------------------------------------------------------------------------------- 1 | //! 预约和处理时钟中断 2 | 3 | use crate::sbi::set_timer; 4 | use riscv::register::{sie, time}; 5 | 6 | /// 触发时钟中断计数 7 | pub static mut TICKS: usize = 0; 8 | 9 | /// 时钟中断的间隔,单位是 CPU 指令 10 | static INTERVAL: usize = 100000; 11 | 12 | /// 初始化时钟中断 13 | /// 14 | /// 开启时钟中断使能,并且预约第一次时钟中断 15 | pub fn init() { 16 | unsafe { 17 | // 开启 STIE,允许时钟中断 18 | sie::set_stimer(); 19 | } 20 | // 设置下一次时钟中断 21 | set_next_timeout(); 22 | } 23 | 24 | /// 设置下一次时钟中断 25 | /// 26 | /// 获取当前时间,加上中断间隔,通过 SBI 调用预约下一次中断 27 | fn set_next_timeout() { 28 | set_timer(time::read() + INTERVAL); 29 | } 30 | 31 | /// 每一次时钟中断时调用 32 | /// 33 | /// 设置下一次时钟中断,同时计数 +1 34 | pub fn tick() { 35 | set_next_timeout(); 36 | unsafe { 37 | TICKS += 1; 38 | // if TICKS % 100 == 0 { 39 | // println!("{} tick", TICKS); 40 | // } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /os/src/kernel/condvar.rs: -------------------------------------------------------------------------------- 1 | //! 条件变量 2 | 3 | use super::*; 4 | use alloc::collections::VecDeque; 5 | 6 | #[derive(Default)] 7 | pub struct Condvar { 8 | /// 所有等待此条件变量的线程 9 | watchers: Mutex>>, 10 | } 11 | 12 | impl Condvar { 13 | /// 令当前线程休眠,等待此条件变量 14 | pub fn wait(&self) { 15 | self.watchers 16 | .lock() 17 | .push_back(PROCESSOR.lock().current_thread()); 18 | PROCESSOR.lock().sleep_current_thread(); 19 | } 20 | 21 | /// 唤起一个等待此条件变量的线程 22 | pub fn notify_one(&self) { 23 | if let Some(thread) = self.watchers.lock().pop_front() { 24 | PROCESSOR.lock().wake_thread(thread); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /os/src/kernel/fs.rs: -------------------------------------------------------------------------------- 1 | //! 文件相关的内核功能 2 | 3 | use super::*; 4 | use core::slice::from_raw_parts_mut; 5 | 6 | /// 从指定的文件中读取字符 7 | /// 8 | /// 如果缓冲区暂无数据,返回 0;出现错误返回 -1 9 | pub(super) fn sys_read(fd: usize, buffer: *mut u8, size: usize) -> SyscallResult { 10 | // 从进程中获取 inode 11 | let process = PROCESSOR.lock().current_thread().process.clone(); 12 | if let Some(inode) = process.inner().descriptors.get(fd) { 13 | // 从系统调用传入的参数生成缓冲区 14 | let buffer = unsafe { from_raw_parts_mut(buffer, size) }; 15 | // 尝试读取 16 | if let Ok(ret) = inode.read_at(0, buffer) { 17 | let ret = ret as isize; 18 | if ret > 0 { 19 | return SyscallResult::Proceed(ret); 20 | } 21 | if ret == 0 { 22 | return SyscallResult::Park(ret); 23 | } 24 | } 25 | } 26 | SyscallResult::Proceed(-1) 27 | } 28 | 29 | /// 将字符写入指定的文件 30 | pub(super) fn sys_write(fd: usize, buffer: *mut u8, size: usize) -> SyscallResult { 31 | // 从进程中获取 inode 32 | let process = PROCESSOR.lock().current_thread().process.clone(); 33 | if let Some(inode) = process.inner().descriptors.get(fd) { 34 | // 从系统调用传入的参数生成缓冲区 35 | let buffer = unsafe { from_raw_parts_mut(buffer, size) }; 36 | // 尝试写入 37 | if let Ok(ret) = inode.write_at(0, buffer) { 38 | let ret = ret as isize; 39 | if ret >= 0 { 40 | return SyscallResult::Proceed(ret); 41 | } 42 | } 43 | } 44 | SyscallResult::Proceed(-1) 45 | } 46 | -------------------------------------------------------------------------------- /os/src/kernel/mod.rs: -------------------------------------------------------------------------------- 1 | //! 为进程提供系统调用等内核功能 2 | 3 | mod condvar; 4 | mod fs; 5 | mod process; 6 | mod syscall; 7 | 8 | use crate::interrupt::*; 9 | use crate::process::*; 10 | use alloc::sync::Arc; 11 | pub(self) use fs::*; 12 | pub(self) use process::*; 13 | use spin::Mutex; 14 | pub(self) use syscall::*; 15 | 16 | pub use condvar::Condvar; 17 | pub use syscall::syscall_handler; 18 | -------------------------------------------------------------------------------- /os/src/kernel/process.rs: -------------------------------------------------------------------------------- 1 | //! 进程相关的内核功能 2 | 3 | use super::*; 4 | 5 | pub(super) fn sys_exit(code: usize) -> SyscallResult { 6 | println!( 7 | "thread {} exit with code {}", 8 | PROCESSOR.lock().current_thread().id, 9 | code 10 | ); 11 | SyscallResult::Kill 12 | } 13 | -------------------------------------------------------------------------------- /os/src/kernel/syscall.rs: -------------------------------------------------------------------------------- 1 | //! 实现各种系统调用 2 | 3 | use super::*; 4 | 5 | pub const SYS_READ: usize = 63; 6 | pub const SYS_WRITE: usize = 64; 7 | pub const SYS_EXIT: usize = 93; 8 | 9 | /// 系统调用在内核之内的返回值 10 | pub(super) enum SyscallResult { 11 | /// 继续执行,带返回值 12 | Proceed(isize), 13 | /// 记录返回值,但暂存当前线程 14 | Park(isize), 15 | /// 丢弃当前 context,调度下一个线程继续执行 16 | Kill, 17 | } 18 | 19 | /// 系统调用的总入口 20 | pub fn syscall_handler(context: &mut Context) -> *mut Context { 21 | // 无论如何处理,一定会跳过当前的 ecall 指令 22 | context.sepc += 4; 23 | 24 | let syscall_id = context.x[17]; 25 | let args = [context.x[10], context.x[11], context.x[12]]; 26 | 27 | let result = match syscall_id { 28 | SYS_READ => sys_read(args[0], args[1] as *mut u8, args[2]), 29 | SYS_WRITE => sys_write(args[0], args[1] as *mut u8, args[2]), 30 | SYS_EXIT => sys_exit(args[0]), 31 | _ => { 32 | println!("unimplemented syscall: {}", syscall_id); 33 | SyscallResult::Kill 34 | } 35 | }; 36 | 37 | match result { 38 | SyscallResult::Proceed(ret) => { 39 | // 将返回值放入 context 中 40 | context.x[10] = ret as usize; 41 | context 42 | } 43 | SyscallResult::Park(ret) => { 44 | // 将返回值放入 context 中 45 | context.x[10] = ret as usize; 46 | // 保存 context,准备下一个线程 47 | PROCESSOR.lock().park_current_thread(context); 48 | PROCESSOR.lock().prepare_next_thread() 49 | } 50 | SyscallResult::Kill => { 51 | // 终止,跳转到 PROCESSOR 调度的下一个线程 52 | PROCESSOR.lock().kill_current_thread(); 53 | PROCESSOR.lock().prepare_next_thread() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /os/src/linker.ld: -------------------------------------------------------------------------------- 1 | /* Linker Script 语法可以参见:http://www.scoberlin.de/content/media/http/informatik/gcc_docs/ld_3.html */ 2 | 3 | /* 目标架构 */ 4 | OUTPUT_ARCH(riscv) 5 | 6 | /* 执行入口 */ 7 | ENTRY(_start) 8 | 9 | /* 数据存放起始地址 */ 10 | BASE_ADDRESS = 0xffffffff80200000; 11 | 12 | SECTIONS 13 | { 14 | /* . 表示当前地址(location counter) */ 15 | . = BASE_ADDRESS; 16 | 17 | /* start 符号表示全部的开始位置 */ 18 | kernel_start = .; 19 | 20 | . = ALIGN(4K); 21 | text_start = .; 22 | 23 | /* .text 字段 */ 24 | .text : { 25 | /* 把 entry 函数放在最前面 */ 26 | *(.text.entry) 27 | /* 要链接的文件的 .text 字段集中放在这里 */ 28 | *(.text .text.*) 29 | } 30 | 31 | . = ALIGN(4K); 32 | rodata_start = .; 33 | 34 | /* .rodata 字段 */ 35 | .rodata : { 36 | /* 要链接的文件的 .rodata 字段集中放在这里 */ 37 | *(.rodata .rodata.*) 38 | } 39 | 40 | . = ALIGN(4K); 41 | data_start = .; 42 | 43 | /* .data 字段 */ 44 | .data : { 45 | /* 要链接的文件的 .data 字段集中放在这里 */ 46 | *(.data .data.*) 47 | } 48 | 49 | . = ALIGN(4K); 50 | bss_start = .; 51 | 52 | /* .bss 字段 */ 53 | .bss : { 54 | /* 要链接的文件的 .bss 字段集中放在这里 */ 55 | *(.sbss .bss .bss.*) 56 | } 57 | 58 | /* 结束地址 */ 59 | . = ALIGN(4K); 60 | kernel_end = .; 61 | } -------------------------------------------------------------------------------- /os/src/main.rs: -------------------------------------------------------------------------------- 1 | //! # 全局属性 2 | //! 3 | //! - `#![no_std]` 4 | //! 禁用标准库 5 | #![no_std] 6 | //! 7 | //! - `#![no_main]` 8 | //! 不使用 `main` 函数等全部 Rust-level 入口点来作为程序入口 9 | #![no_main] 10 | //! 11 | //! - `#![deny(missing_docs)]` 12 | //! 任何没有注释的地方都会产生警告:这个属性用来压榨写实验指导的学长,同学可以删掉了 13 | #![warn(missing_docs)] 14 | //! # 一些 unstable 的功能需要在 crate 层级声明后才可以使用 15 | //! 16 | //! - `#![feature(alloc_error_handler)]` 17 | //! 我们使用了一个全局动态内存分配器,以实现原本标准库中的堆内存分配。 18 | //! 而语言要求我们同时实现一个错误回调,这里我们直接 panic 19 | #![feature(alloc_error_handler)] 20 | //! 21 | //! - `#![feature(llvm_asm)]` 22 | //! 内嵌汇编 23 | #![feature(llvm_asm)] 24 | //! 25 | //! - `#![feature(global_asm)]` 26 | //! 内嵌整个汇编文件 27 | #![feature(global_asm)] 28 | //! 29 | //! - `#![feature(panic_info_message)]` 30 | //! panic! 时,获取其中的信息并打印 31 | #![feature(panic_info_message)] 32 | //! 33 | //! - `#![feature(naked_functions)]` 34 | //! 允许使用 naked 函数,即编译器不在函数前后添加出入栈操作。 35 | //! 这允许我们在函数中间内联汇编使用 `ret` 提前结束,而不会导致栈出现异常 36 | #![feature(naked_functions)] 37 | //! 38 | //! - `#![feature(slice_fill)]` 39 | //! 允许将 slice 填充值 40 | #![feature(slice_fill)] 41 | 42 | #[macro_use] 43 | mod console; 44 | mod drivers; 45 | mod fs; 46 | mod interrupt; 47 | mod kernel; 48 | mod memory; 49 | mod panic; 50 | mod process; 51 | mod sbi; 52 | extern crate alloc; 53 | 54 | use alloc::sync::Arc; 55 | use fs::{INodeExt, ROOT_INODE}; 56 | use memory::PhysicalAddress; 57 | use process::*; 58 | use xmas_elf::ElfFile; 59 | 60 | // 汇编编写的程序入口,具体见该文件 61 | global_asm!(include_str!("entry.asm")); 62 | 63 | /// Rust 的入口函数 64 | /// 65 | /// 在 `_start` 为我们进行了一系列准备之后,这是第一个被调用的 Rust 函数 66 | #[no_mangle] 67 | pub extern "C" fn rust_main(_hart_id: usize, dtb_pa: PhysicalAddress) -> ! { 68 | memory::init(); 69 | interrupt::init(); 70 | drivers::init(dtb_pa); 71 | fs::init(); 72 | 73 | { 74 | let mut processor = PROCESSOR.lock(); 75 | // 创建一个内核进程 76 | let kernel_process = Process::new_kernel().unwrap(); 77 | // 为这个进程创建多个线程,并设置入口均为 sample_process,而参数不同 78 | for i in 1..9usize { 79 | processor.add_thread(create_kernel_thread( 80 | kernel_process.clone(), 81 | sample_process as usize, 82 | Some(&[i]), 83 | )); 84 | } 85 | } 86 | 87 | extern "C" { 88 | fn __restore(context: usize); 89 | } 90 | // 获取第一个线程的 Context 91 | let context = PROCESSOR.lock().prepare_next_thread(); 92 | // 启动第一个线程 93 | unsafe { __restore(context as usize) }; 94 | unreachable!() 95 | } 96 | 97 | fn sample_process(id: usize) { 98 | println!("hello from kernel thread {}", id); 99 | } 100 | 101 | /// 创建一个内核进程 102 | pub fn create_kernel_thread( 103 | process: Arc, 104 | entry_point: usize, 105 | arguments: Option<&[usize]>, 106 | ) -> Arc { 107 | // 创建线程 108 | let thread = Thread::new(process, entry_point, arguments).unwrap(); 109 | // 设置线程的返回地址为 kernel_thread_exit 110 | thread 111 | .as_ref() 112 | .inner() 113 | .context 114 | .as_mut() 115 | .unwrap() 116 | .set_ra(kernel_thread_exit as usize); 117 | 118 | thread 119 | } 120 | 121 | /// 创建一个用户进程,从指定的文件名读取 ELF 122 | pub fn create_user_process(name: &str) -> Arc { 123 | // 从文件系统中找到程序 124 | let app = ROOT_INODE.find(name).unwrap(); 125 | // 读取数据 126 | let data = app.readall().unwrap(); 127 | // 解析 ELF 文件 128 | let elf = ElfFile::new(data.as_slice()).unwrap(); 129 | // 利用 ELF 文件创建线程,映射空间并加载数据 130 | let process = Process::from_elf(&elf, true).unwrap(); 131 | // 再从 ELF 中读出程序入口地址 132 | Thread::new(process, elf.header.pt2.entry_point() as usize, None).unwrap() 133 | } 134 | 135 | /// 内核线程需要调用这个函数来退出 136 | fn kernel_thread_exit() { 137 | // 当前线程标记为结束 138 | PROCESSOR.lock().current_thread().as_ref().inner().dead = true; 139 | // 制造一个中断来交给操作系统处理 140 | unsafe { llvm_asm!("ebreak" :::: "volatile") }; 141 | } 142 | -------------------------------------------------------------------------------- /os/src/memory/config.rs: -------------------------------------------------------------------------------- 1 | //! 定义一些内存相关的常量 2 | 3 | use super::address::*; 4 | use lazy_static::*; 5 | 6 | /// 页 / 帧大小,必须是 2^n 7 | pub const PAGE_SIZE: usize = 4096; 8 | 9 | /// MMIO 设备段内存区域起始地址 10 | pub const DEVICE_START_ADDRESS: PhysicalAddress = PhysicalAddress(0x1000_0000); 11 | /// MMIO 设备段内存区域结束地址 12 | pub const DEVICE_END_ADDRESS: PhysicalAddress = PhysicalAddress(0x1001_0000); 13 | 14 | /// 可以访问的内存区域起始地址 15 | pub const MEMORY_START_ADDRESS: PhysicalAddress = PhysicalAddress(0x8000_0000); 16 | /// 可以访问的内存区域结束地址 17 | pub const MEMORY_END_ADDRESS: PhysicalAddress = PhysicalAddress(0x8800_0000); 18 | 19 | lazy_static! { 20 | /// 内核代码结束的地址,即可以用来分配的内存起始地址 21 | /// 22 | /// 因为 Rust 语言限制,我们只能将其作为一个运行时求值的 static 变量,而不能作为 const 23 | pub static ref KERNEL_END_ADDRESS: VirtualAddress = VirtualAddress(kernel_end as usize); 24 | } 25 | /// 操作系统动态分配内存所用的堆大小(8M) 26 | pub const KERNEL_HEAP_SIZE: usize = 0x80_0000; 27 | 28 | /// 内核使用线性映射的偏移量 29 | pub const KERNEL_MAP_OFFSET: usize = 0xffff_ffff_0000_0000; 30 | 31 | extern "C" { 32 | /// 由 `linker.ld` 指定的内核代码结束位置 33 | /// 34 | /// 作为变量存在 [`static@KERNEL_END_ADDRESS`] 35 | fn kernel_end(); 36 | } 37 | -------------------------------------------------------------------------------- /os/src/memory/frame/allocator.rs: -------------------------------------------------------------------------------- 1 | //! 提供帧分配器 [`FRAME_ALLOCATOR`](FrameAllocator) 2 | //! 3 | //! 返回的 [`FrameTracker`] 类型代表一个帧,它在被 drop 时会自动将空间补回分配器中。 4 | 5 | use super::*; 6 | use crate::memory::*; 7 | use algorithm::*; 8 | use lazy_static::*; 9 | use spin::Mutex; 10 | 11 | lazy_static! { 12 | /// 帧分配器 13 | pub static ref FRAME_ALLOCATOR: Mutex> = Mutex::new(FrameAllocator::new(Range::from( 14 | PhysicalPageNumber::ceil(PhysicalAddress::from(*KERNEL_END_ADDRESS))..PhysicalPageNumber::floor(MEMORY_END_ADDRESS), 15 | ) 16 | )); 17 | } 18 | 19 | /// 基于线段树的帧分配 / 回收 20 | pub struct FrameAllocator { 21 | /// 可用区间的起始 22 | start_ppn: PhysicalPageNumber, 23 | /// 分配器 24 | allocator: T, 25 | } 26 | 27 | impl FrameAllocator { 28 | /// 创建对象 29 | pub fn new(range: impl Into> + Copy) -> Self { 30 | FrameAllocator { 31 | start_ppn: range.into().start, 32 | allocator: T::new(range.into().len()), 33 | } 34 | } 35 | 36 | /// 分配帧,如果没有剩余则返回 `Err` 37 | pub fn alloc(&mut self) -> MemoryResult { 38 | self.allocator 39 | .alloc() 40 | .ok_or("no available frame to allocate") 41 | .map(|offset| FrameTracker(self.start_ppn + offset)) 42 | } 43 | 44 | /// 将被释放的帧添加到空闲列表的尾部 45 | /// 46 | /// 这个函数会在 [`FrameTracker`] 被 drop 时自动调用,不应在其他地方调用 47 | pub(super) fn dealloc(&mut self, frame: &FrameTracker) { 48 | self.allocator.dealloc(frame.page_number() - self.start_ppn); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /os/src/memory/frame/frame_tracker.rs: -------------------------------------------------------------------------------- 1 | //! 提供物理页的「`Box`」 [`FrameTracker`] 2 | 3 | use crate::memory::{address::*, FRAME_ALLOCATOR, PAGE_SIZE}; 4 | 5 | /// 分配出的物理页 6 | /// 7 | /// # `Tracker` 是什么? 8 | /// 太长不看 9 | /// > 可以理解为 [`Box`](alloc::boxed::Box),而区别在于,其空间不是分配在堆上, 10 | /// > 而是直接在内存中划一片(一个物理页)。 11 | /// 12 | /// 在我们实现操作系统的过程中,会经常遇到「指定一块内存区域作为某种用处」的情况。 13 | /// 此时,我们说这块内存可以用,但是因为它不在堆栈上,Rust 编译器并不知道它是什么,所以 14 | /// 我们需要 unsafe 地将其转换为 `&'static mut T` 的形式(`'static` 一般可以省略)。 15 | /// 16 | /// 但是,比如我们用一块内存来作为页表,而当这个页表我们不再需要的时候,就应当释放空间。 17 | /// 我们其实更需要一个像「创建一个有生命期的对象」一样的模式来使用这块内存。因此, 18 | /// 我们不妨用 `Tracker` 类型来封装这样一个 `&'static mut` 引用。 19 | /// 20 | /// 使用 `Tracker` 其实就很像使用一个 smart pointer。如果需要引用计数, 21 | /// 就在外面再套一层 [`Arc`](alloc::sync::Arc) 就好 22 | pub struct FrameTracker(pub(super) PhysicalPageNumber); 23 | 24 | impl FrameTracker { 25 | /// 帧的物理地址 26 | pub fn address(&self) -> PhysicalAddress { 27 | self.0.into() 28 | } 29 | /// 帧的物理页号 30 | pub fn page_number(&self) -> PhysicalPageNumber { 31 | self.0 32 | } 33 | } 34 | 35 | /// `FrameTracker` 可以 deref 得到对应的 `[u8; PAGE_SIZE]` 36 | impl core::ops::Deref for FrameTracker { 37 | type Target = [u8; PAGE_SIZE]; 38 | fn deref(&self) -> &Self::Target { 39 | self.page_number().deref_kernel() 40 | } 41 | } 42 | 43 | /// `FrameTracker` 可以 deref 得到对应的 `[u8; PAGE_SIZE]` 44 | impl core::ops::DerefMut for FrameTracker { 45 | fn deref_mut(&mut self) -> &mut Self::Target { 46 | self.page_number().deref_kernel() 47 | } 48 | } 49 | 50 | /// 帧在释放时会放回 [`static@FRAME_ALLOCATOR`] 的空闲链表中 51 | impl Drop for FrameTracker { 52 | fn drop(&mut self) { 53 | FRAME_ALLOCATOR.lock().dealloc(self); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /os/src/memory/frame/mod.rs: -------------------------------------------------------------------------------- 1 | //! 物理页的分配与回收 2 | 3 | mod allocator; 4 | mod frame_tracker; 5 | 6 | pub use allocator::FRAME_ALLOCATOR; 7 | pub use frame_tracker::FrameTracker; 8 | -------------------------------------------------------------------------------- /os/src/memory/heap.rs: -------------------------------------------------------------------------------- 1 | //! 实现操作系统动态内存分配所用的堆 2 | //! 3 | //! 基于 `buddy_system_allocator` crate,致敬杰哥。 4 | 5 | use super::config::KERNEL_HEAP_SIZE; 6 | use buddy_system_allocator::LockedHeap; 7 | 8 | /// 进行动态内存分配所用的堆空间 9 | /// 10 | /// 大小为 [`KERNEL_HEAP_SIZE`] 11 | /// 这段空间编译后会被放在操作系统执行程序的 bss 段 12 | static mut HEAP_SPACE: [u8; KERNEL_HEAP_SIZE] = [0; KERNEL_HEAP_SIZE]; 13 | 14 | /// 堆,动态内存分配器 15 | /// 16 | /// ### `#[global_allocator]` 17 | /// [`LockedHeap`] 实现了 [`alloc::alloc::GlobalAlloc`] trait, 18 | /// 可以为全局需要用到堆的地方分配空间。例如 `Box` `Arc` 等 19 | #[global_allocator] 20 | static HEAP: LockedHeap = LockedHeap::empty(); 21 | 22 | /// 初始化操作系统运行时堆空间 23 | pub fn init() { 24 | // 告诉分配器使用这一段预留的空间作为堆 25 | unsafe { 26 | HEAP.lock() 27 | .init(HEAP_SPACE.as_ptr() as usize, KERNEL_HEAP_SIZE) 28 | } 29 | } 30 | 31 | /// 空间分配错误的回调,直接 panic 退出 32 | #[alloc_error_handler] 33 | fn alloc_error_handler(_: alloc::alloc::Layout) -> ! { 34 | panic!("alloc error") 35 | } 36 | -------------------------------------------------------------------------------- /os/src/memory/mapping/mod.rs: -------------------------------------------------------------------------------- 1 | //! 内存映射 2 | //! 3 | //! 每个线程保存一个 [`Mapping`],其中记录了所有的字段 [`Segment`]。 4 | //! 同时,也要追踪为页表或字段分配的所有物理页,目的是 drop 掉之后可以安全释放所有资源。 5 | 6 | #[allow(clippy::module_inception)] 7 | mod mapping; 8 | mod memory_set; 9 | mod page_table; 10 | mod page_table_entry; 11 | mod segment; 12 | 13 | pub use mapping::Mapping; 14 | pub use memory_set::MemorySet; 15 | pub use page_table::{PageTable, PageTableTracker}; 16 | pub use page_table_entry::{Flags, PageTableEntry}; 17 | pub use segment::{MapType, Segment}; 18 | -------------------------------------------------------------------------------- /os/src/memory/mapping/page_table.rs: -------------------------------------------------------------------------------- 1 | //! 单一页表页面(4K) [`PageTable`],以及相应封装 [`FrameTracker`] 的 [`PageTableTracker`] 2 | //! 3 | //! 每个页表中包含 512 条页表项 4 | //! 5 | //! # 页表工作方式 6 | //! 1. 首先从 `satp` 中获取页表根节点的页号,找到根页表 7 | //! 2. 对于虚拟地址中每一级 VPN(9 位),在对应的页表中找到对应的页表项 8 | //! 3. 如果对应项 Valid 位为 0,则发生 Page Fault 9 | //! 4. 如果对应项 Readable / Writable 位为 1,则表示这是一个叶子节点。 10 | //! 页表项中的值便是虚拟地址对应的物理页号 11 | //! 如果此时还没有达到最低级的页表,说明这是一个大页 12 | //! 5. 将页表项中的页号作为下一级查询目标,查询直到达到最低级的页表,最终得到页号 13 | 14 | use super::page_table_entry::PageTableEntry; 15 | use crate::memory::{address::*, config::PAGE_SIZE, frame::FrameTracker}; 16 | /// 存有 512 个页表项的页表 17 | /// 18 | /// 注意我们不会使用常规的 Rust 语法来创建 `PageTable`。相反,我们会分配一个物理页, 19 | /// 其对应了一段物理内存,然后直接把其当做页表进行读写。我们会在操作系统中用一个「指针」 20 | /// [`PageTableTracker`] 来记录这个页表。 21 | #[repr(C)] 22 | pub struct PageTable { 23 | pub entries: [PageTableEntry; PAGE_SIZE / 8], 24 | } 25 | 26 | impl PageTable { 27 | /// 将页表清零 28 | pub fn zero_init(&mut self) { 29 | self.entries = [Default::default(); PAGE_SIZE / 8]; 30 | } 31 | } 32 | 33 | /// 类似于 [`FrameTracker`],用于记录某一个内存中页表 34 | /// 35 | /// 注意到,「真正的页表」会放在我们分配出来的物理页当中,而不应放在操作系统的运行栈或堆中。 36 | /// 而 `PageTableTracker` 会保存在某个线程的元数据中(也就是在操作系统的堆上),指向其真正的页表。 37 | /// 38 | /// 当 `PageTableTracker` 被 drop 时,会自动 drop `FrameTracker`,进而释放帧。 39 | pub struct PageTableTracker(pub FrameTracker); 40 | 41 | impl PageTableTracker { 42 | /// 将一个分配的帧清零,形成空的页表 43 | pub fn new(frame: FrameTracker) -> Self { 44 | let mut page_table = Self(frame); 45 | page_table.zero_init(); 46 | page_table 47 | } 48 | /// 获取物理页号 49 | pub fn page_number(&self) -> PhysicalPageNumber { 50 | self.0.page_number() 51 | } 52 | } 53 | 54 | // PageTableEntry 和 PageTableTracker 都可以 deref 到对应的 PageTable 55 | // (使用线性映射来访问相应的物理地址) 56 | 57 | impl core::ops::Deref for PageTableTracker { 58 | type Target = PageTable; 59 | fn deref(&self) -> &Self::Target { 60 | self.0.address().deref_kernel() 61 | } 62 | } 63 | 64 | impl core::ops::DerefMut for PageTableTracker { 65 | fn deref_mut(&mut self) -> &mut Self::Target { 66 | self.0.address().deref_kernel() 67 | } 68 | } 69 | 70 | // 因为 PageTableEntry 和具体的 PageTable 之间没有生命周期关联,所以返回 'static 引用方便写代码 71 | impl PageTableEntry { 72 | pub fn get_next_table(&self) -> &'static mut PageTable { 73 | self.address().deref_kernel() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /os/src/memory/mapping/page_table_entry.rs: -------------------------------------------------------------------------------- 1 | //! 页表项 [`PageTableEntry`] 2 | //! 3 | //! # RISC-V 64 中的页表项结构 4 | //! 每个页表项长度为 64 位,每个页面大小是 4KB,即每个页面能存下 2^9=512 个页表项。 5 | //! 每一个页表存放 512 个页表项,说明每一级页表使用 9 位来标记 VPN。 6 | //! 7 | //! # RISC-V 64 两种页表组织方式:Sv39 和 Sv48 8 | //! 64 位能够表示的空间大小太大了,因此现有的 64 位硬件实际上都不会支持 64 位的地址空间。 9 | //! 10 | //! RISC-V 64 现有两种地址长度:39 位和 48 位,其中 Sv39 的虚拟地址就包括三级页表和页内偏移。 11 | //! `3 * 9 + 12 = 39` 12 | //! 13 | //! 我们使用 Sv39,Sv48 同理,只是它具有四级页表。 14 | 15 | use crate::memory::address::*; 16 | use bit_field::BitField; 17 | use bitflags::*; 18 | 19 | /// Sv39 结构的页表项 20 | #[derive(Copy, Clone, Default)] 21 | pub struct PageTableEntry(usize); 22 | 23 | /// Sv39 页表项中标志位的位置 24 | const FLAG_RANGE: core::ops::Range = 0..8; 25 | /// Sv39 页表项中物理页号的位置 26 | const PAGE_NUMBER_RANGE: core::ops::Range = 10..54; 27 | 28 | impl PageTableEntry { 29 | /// 将相应页号和标志写入一个页表项 30 | pub fn new(page_number: Option, mut flags: Flags) -> Self { 31 | // 标志位中是否包含 Valid 取决于 page_number 是否为 Some 32 | flags.set(Flags::VALID, page_number.is_some()); 33 | Self( 34 | *0usize 35 | .set_bits(FLAG_RANGE, flags.bits() as usize) 36 | .set_bits(PAGE_NUMBER_RANGE, page_number.unwrap_or_default().into()), 37 | ) 38 | } 39 | /// 设置物理页号,同时根据 ppn 是否为 Some 来设置 Valid 位 40 | pub fn update_page_number(&mut self, ppn: Option) { 41 | if let Some(ppn) = ppn { 42 | self.0 43 | .set_bits(FLAG_RANGE, (self.flags() | Flags::VALID).bits() as usize) 44 | .set_bits(PAGE_NUMBER_RANGE, ppn.into()); 45 | } else { 46 | self.0 47 | .set_bits(FLAG_RANGE, (self.flags() - Flags::VALID).bits() as usize) 48 | .set_bits(PAGE_NUMBER_RANGE, 0); 49 | } 50 | } 51 | /// 清除 52 | pub fn clear(&mut self) { 53 | self.0 = 0; 54 | } 55 | /// 获取页号 56 | pub fn page_number(&self) -> PhysicalPageNumber { 57 | PhysicalPageNumber::from(self.0.get_bits(10..54)) 58 | } 59 | /// 获取地址 60 | pub fn address(&self) -> PhysicalAddress { 61 | PhysicalAddress::from(self.page_number()) 62 | } 63 | /// 获取标志位 64 | pub fn flags(&self) -> Flags { 65 | unsafe { Flags::from_bits_unchecked(self.0.get_bits(..8) as u8) } 66 | } 67 | /// 是否为空(可能非空也非 Valid) 68 | pub fn is_empty(&self) -> bool { 69 | self.0 == 0 70 | } 71 | /// 是否指向下一级(RWX 全为0) 72 | pub fn has_next_level(&self) -> bool { 73 | let flags = self.flags(); 74 | !(flags.contains(Flags::READABLE) 75 | || flags.contains(Flags::WRITABLE) 76 | || flags.contains(Flags::EXECUTABLE)) 77 | } 78 | } 79 | 80 | impl core::fmt::Debug for PageTableEntry { 81 | fn fmt(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { 82 | formatter 83 | .debug_struct("PageTableEntry") 84 | .field("value", &self.0) 85 | .field("page_number", &self.page_number()) 86 | .field("flags", &self.flags()) 87 | .finish() 88 | } 89 | } 90 | 91 | bitflags! { 92 | /// 页表项中的 8 个标志位 93 | #[derive(Default)] 94 | pub struct Flags: u8 { 95 | /// 有效位 96 | const VALID = 1 << 0; 97 | /// 可读位 98 | const READABLE = 1 << 1; 99 | /// 可写位 100 | const WRITABLE = 1 << 2; 101 | /// 可执行位 102 | const EXECUTABLE = 1 << 3; 103 | /// 用户位 104 | const USER = 1 << 4; 105 | /// 全局位,我们不会使用 106 | const GLOBAL = 1 << 5; 107 | /// 已使用位,用于替换算法 108 | const ACCESSED = 1 << 6; 109 | /// 已修改位,用于替换算法 110 | const DIRTY = 1 << 7; 111 | } 112 | } 113 | 114 | macro_rules! implement_flags { 115 | ($field: ident, $name: ident, $quote: literal) => { 116 | impl Flags { 117 | #[doc = "返回 `Flags::"] 118 | #[doc = $quote] 119 | #[doc = "` 或 `Flags::empty()`"] 120 | pub fn $name(value: bool) -> Flags { 121 | if value { 122 | Flags::$field 123 | } else { 124 | Flags::empty() 125 | } 126 | } 127 | } 128 | }; 129 | } 130 | 131 | implement_flags! {USER, user, "USER"} 132 | implement_flags! {READABLE, readable, "READABLE"} 133 | implement_flags! {WRITABLE, writable, "WRITABLE"} 134 | implement_flags! {EXECUTABLE, executable, "EXECUTABLE"} 135 | -------------------------------------------------------------------------------- /os/src/memory/mapping/segment.rs: -------------------------------------------------------------------------------- 1 | //! 映射类型 [`MapType`] 和映射片段 [`Segment`] 2 | 3 | use crate::memory::{address::*, mapping::Flags, range::Range}; 4 | 5 | /// 映射的类型 6 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 7 | pub enum MapType { 8 | /// 线性映射,操作系统使用 9 | Linear, 10 | /// 按帧分配映射 11 | Framed, 12 | } 13 | 14 | /// 一个映射片段(对应旧 tutorial 的 `MemoryArea`) 15 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 16 | pub struct Segment { 17 | /// 映射类型 18 | pub map_type: MapType, 19 | /// 所映射的虚拟地址 20 | pub range: Range, 21 | /// 权限标志 22 | pub flags: Flags, 23 | } 24 | 25 | impl Segment { 26 | /// 遍历对应的物理地址(如果可能) 27 | pub fn iter_mapped(&self) -> Option> { 28 | match self.map_type { 29 | // 线性映射可以直接将虚拟地址转换 30 | MapType::Linear => Some(self.page_range().into().iter()), 31 | // 按帧映射无法直接获得物理地址,需要分配 32 | MapType::Framed => None, 33 | } 34 | } 35 | 36 | /// 将地址相应地上下取整,获得虚拟页号区间 37 | pub fn page_range(&self) -> Range { 38 | Range::from( 39 | VirtualPageNumber::floor(self.range.start)..VirtualPageNumber::ceil(self.range.end), 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /os/src/memory/mod.rs: -------------------------------------------------------------------------------- 1 | //! 内存管理模块 2 | //! 3 | //! 负责空间分配和虚拟地址映射 4 | 5 | // 因为模块内包含许多基础设施类别,实现了许多以后可能会用到的函数, 6 | // 所以在模块范围内不提示「未使用的函数」等警告 7 | #![allow(dead_code)] 8 | 9 | pub mod address; 10 | pub mod config; 11 | pub mod frame; 12 | pub mod heap; 13 | pub mod mapping; 14 | pub mod range; 15 | 16 | /// 一个缩写,模块中一些函数会使用 17 | pub type MemoryResult = Result; 18 | 19 | pub use { 20 | address::*, 21 | config::*, 22 | frame::FRAME_ALLOCATOR, 23 | mapping::{Flags, MapType, MemorySet, Segment}, 24 | range::Range, 25 | }; 26 | 27 | /// 初始化内存相关的子模块 28 | /// 29 | /// - [`heap::init`] 30 | pub fn init() { 31 | heap::init(); 32 | // 允许内核读写用户态内存 33 | unsafe { riscv::register::sstatus::set_sum() }; 34 | 35 | println!("mod memory initialized"); 36 | } 37 | -------------------------------------------------------------------------------- /os/src/memory/range.rs: -------------------------------------------------------------------------------- 1 | //! 表示一个页面区间 [`Range`],提供迭代器功能 2 | 3 | /// 表示一段连续的页面 4 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 5 | pub struct Range + Into + Copy> { 6 | pub start: T, 7 | pub end: T, 8 | } 9 | 10 | /// 创建一个区间 11 | impl + Into + Copy, U: Into> From> for Range { 12 | fn from(range: core::ops::Range) -> Self { 13 | Self { 14 | start: range.start.into(), 15 | end: range.end.into(), 16 | } 17 | } 18 | } 19 | 20 | impl + Into + Copy> Range { 21 | /// 检测两个 [`Range`] 是否存在重合的区间 22 | pub fn overlap_with(&self, other: &Range) -> bool { 23 | self.start.into() < other.end.into() && self.end.into() > other.start.into() 24 | } 25 | 26 | /// 迭代区间中的所有页 27 | pub fn iter(&self) -> impl Iterator { 28 | (self.start.into()..self.end.into()).map(T::from) 29 | } 30 | 31 | /// 区间大小 32 | pub fn len(&self) -> usize { 33 | self.end.into() - self.start.into() 34 | } 35 | 36 | /// 支持物理 / 虚拟页面区间互相转换 37 | pub fn into + Into + Copy + From>(self) -> Range { 38 | Range:: { 39 | start: U::from(self.start), 40 | end: U::from(self.end), 41 | } 42 | } 43 | 44 | /// 从区间中用下标取元素 45 | pub fn get(&self, index: usize) -> T { 46 | assert!(index < self.len()); 47 | T::from(self.start.into() + index) 48 | } 49 | 50 | /// 区间是否包含指定的值 51 | pub fn contains(&self, value: T) -> bool { 52 | self.start.into() <= value.into() && value.into() < self.end.into() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /os/src/panic.rs: -------------------------------------------------------------------------------- 1 | //! 代替 std 库,实现 panic 和 abort 的功能 2 | 3 | use crate::sbi::shutdown; 4 | use core::panic::PanicInfo; 5 | 6 | /// 打印 panic 的信息并 [`shutdown`] 7 | /// 8 | /// ### `#[panic_handler]` 属性 9 | /// 声明此函数是 panic 的回调 10 | #[panic_handler] 11 | fn panic_handler(info: &PanicInfo) -> ! { 12 | // `\x1b[??m` 是控制终端字符输出格式的指令,在支持的平台上可以改变文字颜色等等 13 | // 参考:https://misc.flogisoft.com/bash/tip_colors_and_formatting 14 | // 这里使用错误红 15 | // 需要全局开启 feature(panic_info_message) 才可以调用 .message() 函数 16 | if let Some(location) = info.location() { 17 | println!( 18 | "\x1b[1;31m{}:{}: '{}'\x1b[0m", 19 | location.file(), 20 | location.line(), 21 | info.message().unwrap() 22 | ); 23 | } else { 24 | println!("\x1b[1;31mpanic: '{}'\x1b[0m", info.message().unwrap()); 25 | } 26 | shutdown() 27 | } 28 | 29 | /// 终止程序 30 | /// 31 | /// 调用 [`panic_handler`] 32 | #[no_mangle] 33 | extern "C" fn abort() -> ! { 34 | panic!("abort()") 35 | } 36 | -------------------------------------------------------------------------------- /os/src/process/config.rs: -------------------------------------------------------------------------------- 1 | //! 定义一些进程相关的常量 2 | 3 | /// 每个线程的运行栈大小 512 KB 4 | pub const STACK_SIZE: usize = 0x8_0000; 5 | 6 | /// 共用的内核栈大小 512 KB 7 | pub const KERNEL_STACK_SIZE: usize = 0x8_0000; 8 | -------------------------------------------------------------------------------- /os/src/process/kernel_stack.rs: -------------------------------------------------------------------------------- 1 | //! 内核栈 [`KernelStack`] 2 | //! 3 | //! 用户态的线程出现中断时,因为用户栈无法保证可用性,中断处理流程必须在内核栈上进行。 4 | //! 所以我们创建一个公用的内核栈,即当发生中断时,会将 Context 写到内核栈顶。 5 | //! 6 | //! ### 线程 [`Context`] 的存放 7 | //! > 1. 线程初始化时,一个 `Context` 放置在内核栈顶,`sp` 指向 `Context` 的位置 8 | //! > (即栈顶 - `size_of::()`) 9 | //! > 2. 切换到线程,执行 `__restore` 时,将 `Context` 的数据恢复到寄存器中后, 10 | //! > 会将 `Context` 出栈(即 `sp += size_of::()`), 11 | //! > 然后保存 `sp` 至 `sscratch`(此时 `sscratch` 即为内核栈顶) 12 | //! > 3. 发生中断时,将 `sscratch` 和 `sp` 互换,入栈一个 `Context` 并保存数据 13 | //! 14 | //! 容易发现,线程的 `Context` 一定保存在内核栈顶。因此,当线程需要运行时, 15 | //! 从 [`Thread`] 中取出 `Context` 然后置于内核栈顶即可 16 | 17 | use super::*; 18 | use core::mem::size_of; 19 | 20 | /// 内核栈 21 | #[repr(align(16))] 22 | #[repr(C)] 23 | pub struct KernelStack([u8; KERNEL_STACK_SIZE]); 24 | 25 | /// 公用的内核栈 26 | pub static mut KERNEL_STACK: KernelStack = KernelStack([0; KERNEL_STACK_SIZE]); 27 | 28 | impl KernelStack { 29 | /// 在栈顶加入 Context 并且返回新的栈顶指针 30 | pub fn push_context(&mut self, context: Context) -> *mut Context { 31 | // 栈顶 32 | let stack_top = &self.0 as *const _ as usize + size_of::(); 33 | // Context 的位置 34 | let push_address = (stack_top - size_of::()) as *mut Context; 35 | unsafe { 36 | *push_address = context; 37 | } 38 | push_address 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /os/src/process/lock.rs: -------------------------------------------------------------------------------- 1 | //! 一个关闭中断的互斥锁 [`Lock`] 2 | 3 | use spin::{Mutex, MutexGuard}; 4 | 5 | /// 关闭中断的互斥锁 6 | #[derive(Default)] 7 | pub struct Lock(pub(self) Mutex); 8 | 9 | /// 封装 [`MutexGuard`] 来实现 drop 时恢复 sstatus 10 | pub struct LockGuard<'a, T> { 11 | /// 在 drop 时需要先 drop 掉 [`MutexGuard`] 再恢复 sstatus 12 | guard: Option>, 13 | /// 保存的关中断前 sstatus 14 | sstatus: usize, 15 | } 16 | 17 | impl Lock { 18 | /// 创建一个新对象 19 | pub fn new(obj: T) -> Self { 20 | Self(Mutex::new(obj)) 21 | } 22 | 23 | /// 获得上锁的对象 24 | pub fn lock(&self) -> LockGuard<'_, T> { 25 | let sstatus: usize; 26 | unsafe { 27 | llvm_asm!("csrrci $0, sstatus, 1 << 1" : "=r"(sstatus) ::: "volatile"); 28 | } 29 | LockGuard { 30 | guard: Some(self.0.lock()), 31 | sstatus, 32 | } 33 | } 34 | } 35 | 36 | /// 释放时,先释放内部的 MutexGuard,再恢复 sstatus 寄存器 37 | impl<'a, T> Drop for LockGuard<'a, T> { 38 | fn drop(&mut self) { 39 | self.guard.take(); 40 | unsafe { llvm_asm!("csrs sstatus, $0" :: "r"(self.sstatus & 2) :: "volatile") }; 41 | } 42 | } 43 | 44 | impl<'a, T> core::ops::Deref for LockGuard<'a, T> { 45 | type Target = T; 46 | fn deref(&self) -> &Self::Target { 47 | self.guard.as_ref().unwrap().deref() 48 | } 49 | } 50 | 51 | impl<'a, T> core::ops::DerefMut for LockGuard<'a, T> { 52 | fn deref_mut(&mut self) -> &mut Self::Target { 53 | self.guard.as_mut().unwrap().deref_mut() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /os/src/process/mod.rs: -------------------------------------------------------------------------------- 1 | //! 管理进程 / 线程 2 | 3 | mod config; 4 | mod kernel_stack; 5 | mod lock; 6 | #[allow(clippy::module_inception)] 7 | mod process; 8 | mod processor; 9 | mod thread; 10 | 11 | use crate::interrupt::*; 12 | use crate::memory::*; 13 | use alloc::{sync::Arc, vec, vec::Vec}; 14 | use spin::Mutex; 15 | 16 | pub use config::*; 17 | pub use kernel_stack::KERNEL_STACK; 18 | pub use lock::Lock; 19 | pub use process::Process; 20 | pub use processor::PROCESSOR; 21 | pub use thread::Thread; 22 | -------------------------------------------------------------------------------- /os/src/process/process.rs: -------------------------------------------------------------------------------- 1 | //! 进程 [`Process`] 2 | 3 | use super::*; 4 | use crate::fs::*; 5 | use xmas_elf::ElfFile; 6 | 7 | /// 进程的信息 8 | pub struct Process { 9 | /// 是否属于用户态 10 | pub is_user: bool, 11 | /// 用 `Mutex` 包装一些可变的变量 12 | pub inner: Mutex, 13 | } 14 | 15 | pub struct ProcessInner { 16 | /// 进程中的线程公用页表 / 内存映射 17 | pub memory_set: MemorySet, 18 | /// 打开的文件描述符 19 | pub descriptors: Vec>, 20 | } 21 | 22 | #[allow(unused)] 23 | impl Process { 24 | /// 创建一个内核进程 25 | pub fn new_kernel() -> MemoryResult> { 26 | Ok(Arc::new(Self { 27 | is_user: false, 28 | inner: Mutex::new(ProcessInner { 29 | memory_set: MemorySet::new_kernel()?, 30 | descriptors: vec![STDIN.clone(), STDOUT.clone()], 31 | }), 32 | })) 33 | } 34 | 35 | /// 创建进程,从文件中读取代码 36 | pub fn from_elf(file: &ElfFile, is_user: bool) -> MemoryResult> { 37 | Ok(Arc::new(Self { 38 | is_user, 39 | inner: Mutex::new(ProcessInner { 40 | memory_set: MemorySet::from_elf(file, is_user)?, 41 | descriptors: vec![STDIN.clone(), STDOUT.clone()], 42 | }), 43 | })) 44 | } 45 | 46 | /// 上锁并获得可变部分的引用 47 | pub fn inner(&self) -> spin::MutexGuard { 48 | self.inner.lock() 49 | } 50 | 51 | /// 分配一定数量的连续虚拟空间 52 | /// 53 | /// 从 `memory_set` 中找到一段给定长度的未占用虚拟地址空间,分配物理页面并建立映射。返回对应的页面区间。 54 | /// 55 | /// `flags` 只需包括 rwx 权限,user 位会根据进程而定。 56 | pub fn alloc_page_range( 57 | &self, 58 | size: usize, 59 | flags: Flags, 60 | ) -> MemoryResult> { 61 | let memory_set = &mut self.inner().memory_set; 62 | 63 | // memory_set 只能按页分配,所以让 size 向上取整页 64 | let alloc_size = (size + PAGE_SIZE - 1) & !(PAGE_SIZE - 1); 65 | // 从 memory_set 中找一段不会发生重叠的空间 66 | let mut range = Range::::from(0x1000000..0x1000000 + alloc_size); 67 | while memory_set.overlap_with(range.into()) { 68 | range.start += alloc_size; 69 | range.end += alloc_size; 70 | } 71 | // 分配物理页面,建立映射 72 | memory_set.add_segment( 73 | Segment { 74 | map_type: MapType::Framed, 75 | range, 76 | flags: flags | Flags::user(self.is_user), 77 | }, 78 | None, 79 | )?; 80 | // 返回地址区间(使用参数 size,而非向上取整的 alloc_size) 81 | Ok(Range::from(range.start..(range.start + size))) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /os/src/process/processor.rs: -------------------------------------------------------------------------------- 1 | //! 实现线程的调度和管理 [`Processor`] 2 | 3 | use super::*; 4 | use algorithm::*; 5 | use hashbrown::HashSet; 6 | use lazy_static::*; 7 | 8 | lazy_static! { 9 | /// 全局的 [`Processor`] 10 | pub static ref PROCESSOR: Lock = Lock::new(Processor::default()); 11 | } 12 | 13 | lazy_static! { 14 | /// 空闲线程:当所有线程进入休眠时,切换到这个线程——它什么都不做,只会等待下一次中断 15 | static ref IDLE_THREAD: Arc = Thread::new( 16 | Process::new_kernel().unwrap(), 17 | wait_for_interrupt as usize, 18 | None, 19 | ).unwrap(); 20 | } 21 | 22 | /// 不断让 CPU 进入休眠等待下一次中断 23 | unsafe fn wait_for_interrupt() { 24 | loop { 25 | llvm_asm!("wfi" :::: "volatile"); 26 | } 27 | } 28 | 29 | /// 线程调度和管理 30 | /// 31 | /// 休眠线程会从调度器中移除,单独保存。在它们被唤醒之前,不会被调度器安排。 32 | /// 33 | /// # 用例 34 | /// 35 | /// ### 切换线程(在中断中) 36 | /// ```rust 37 | /// processor.park_current_thread(context); 38 | /// processor.prepare_next_thread() 39 | /// ``` 40 | /// 41 | /// ### 结束线程(在中断中) 42 | /// ```rust 43 | /// processor.kill_current_thread(); 44 | /// processor.prepare_next_thread() 45 | /// ``` 46 | /// 47 | /// ### 休眠线程(在中断中) 48 | /// ```rust 49 | /// processor.park_current_thread(context); 50 | /// processor.sleep_current_thread(); 51 | /// processor.prepare_next_thread() 52 | /// ``` 53 | /// 54 | /// ### 唤醒线程 55 | /// 线程会根据调度器分配执行,不一定会立即执行。 56 | /// ```rust 57 | /// processor.wake_thread(thread); 58 | /// ``` 59 | #[derive(Default)] 60 | pub struct Processor { 61 | /// 当前正在执行的线程 62 | current_thread: Option>, 63 | /// 线程调度器,记录活跃线程 64 | scheduler: SchedulerImpl>, 65 | /// 保存休眠线程 66 | sleeping_threads: HashSet>, 67 | } 68 | 69 | impl Processor { 70 | /// 获取一个当前线程的 `Arc` 引用 71 | pub fn current_thread(&self) -> Arc { 72 | self.current_thread.as_ref().unwrap().clone() 73 | } 74 | 75 | /// 激活下一个线程的 `Context` 76 | pub fn prepare_next_thread(&mut self) -> *mut Context { 77 | // 向调度器询问下一个线程 78 | if let Some(next_thread) = self.scheduler.get_next() { 79 | // 准备下一个线程 80 | let context = next_thread.prepare(); 81 | self.current_thread = Some(next_thread); 82 | context 83 | } else { 84 | // 没有活跃线程 85 | if self.sleeping_threads.is_empty() { 86 | // 也没有休眠线程,则退出 87 | panic!("all threads terminated, shutting down"); 88 | } else { 89 | // 有休眠线程,则等待中断 90 | self.current_thread = Some(IDLE_THREAD.clone()); 91 | IDLE_THREAD.prepare() 92 | } 93 | } 94 | } 95 | 96 | /// 添加一个待执行的线程 97 | pub fn add_thread(&mut self, thread: Arc) { 98 | self.scheduler.add_thread(thread); 99 | } 100 | 101 | /// 唤醒一个休眠线程 102 | pub fn wake_thread(&mut self, thread: Arc) { 103 | thread.inner().sleeping = false; 104 | self.sleeping_threads.remove(&thread); 105 | self.scheduler.add_thread(thread); 106 | } 107 | 108 | /// 保存当前线程的 `Context` 109 | pub fn park_current_thread(&mut self, context: &Context) { 110 | self.current_thread().park(*context); 111 | } 112 | 113 | /// 令当前线程进入休眠 114 | pub fn sleep_current_thread(&mut self) { 115 | // 从 current_thread 中取出 116 | let current_thread = self.current_thread(); 117 | // 记为 sleeping 118 | current_thread.inner().sleeping = true; 119 | // 从 scheduler 移出到 sleeping_threads 中 120 | self.scheduler.remove_thread(¤t_thread); 121 | self.sleeping_threads.insert(current_thread); 122 | } 123 | 124 | /// 终止当前的线程 125 | pub fn kill_current_thread(&mut self) { 126 | // 从调度器中移除 127 | let thread = self.current_thread.take().unwrap(); 128 | self.scheduler.remove_thread(&thread); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /os/src/process/thread.rs: -------------------------------------------------------------------------------- 1 | //! 线程 [`Thread`] 2 | 3 | use super::*; 4 | use core::hash::{Hash, Hasher}; 5 | 6 | /// 线程 ID 使用 `isize`,可以用负数表示错误 7 | pub type ThreadID = isize; 8 | 9 | /// 线程计数,用于设置线程 ID 10 | static mut THREAD_COUNTER: ThreadID = 0; 11 | 12 | /// 线程的信息 13 | pub struct Thread { 14 | /// 线程 ID 15 | pub id: ThreadID, 16 | /// 线程的栈 17 | pub stack: Range, 18 | /// 所属的进程 19 | pub process: Arc, 20 | /// 用 `Mutex` 包装一些可变的变量 21 | pub inner: Mutex, 22 | } 23 | 24 | /// 线程中需要可变的部分 25 | pub struct ThreadInner { 26 | /// 线程执行上下文 27 | /// 28 | /// 当且仅当线程被暂停执行时,`context` 为 `Some` 29 | pub context: Option, 30 | /// 是否进入休眠 31 | pub sleeping: bool, 32 | /// 是否已经结束 33 | pub dead: bool, 34 | } 35 | 36 | impl Thread { 37 | /// 准备执行一个线程 38 | /// 39 | /// 激活对应进程的页表,并返回其 Context 40 | pub fn prepare(&self) -> *mut Context { 41 | // 激活页表 42 | self.process.inner().memory_set.activate(); 43 | // 取出 Context 44 | let parked_frame = self.inner().context.take().unwrap(); 45 | // 将 Context 放至内核栈顶 46 | unsafe { KERNEL_STACK.push_context(parked_frame) } 47 | } 48 | 49 | /// 发生时钟中断后暂停线程,保存状态 50 | pub fn park(&self, context: Context) { 51 | // 检查目前线程内的 context 应当为 None 52 | assert!(self.inner().context.is_none()); 53 | // 将 Context 保存到线程中 54 | self.inner().context.replace(context); 55 | } 56 | 57 | /// 创建一个线程 58 | pub fn new( 59 | process: Arc, 60 | entry_point: usize, 61 | arguments: Option<&[usize]>, 62 | ) -> MemoryResult> { 63 | // 让所属进程分配并映射一段空间,作为线程的栈 64 | let stack = process.alloc_page_range(STACK_SIZE, Flags::READABLE | Flags::WRITABLE)?; 65 | 66 | // 构建线程的 Context 67 | let context = Context::new(stack.end.into(), entry_point, arguments, process.is_user); 68 | 69 | // 打包成线程 70 | let thread = Arc::new(Thread { 71 | id: unsafe { 72 | THREAD_COUNTER += 1; 73 | THREAD_COUNTER 74 | }, 75 | stack, 76 | process, 77 | inner: Mutex::new(ThreadInner { 78 | context: Some(context), 79 | sleeping: false, 80 | dead: false, 81 | }), 82 | }); 83 | 84 | Ok(thread) 85 | } 86 | 87 | /// 上锁并获得可变部分的引用 88 | pub fn inner(&self) -> spin::MutexGuard { 89 | self.inner.lock() 90 | } 91 | } 92 | 93 | /// 通过线程 ID 来判等 94 | impl PartialEq for Thread { 95 | fn eq(&self, other: &Self) -> bool { 96 | self.id == other.id 97 | } 98 | } 99 | 100 | /// 通过线程 ID 来判等 101 | /// 102 | /// 在 Rust 中,[`PartialEq`] trait 不要求任意对象 `a` 满足 `a == a`。 103 | /// 将类型标注为 [`Eq`],会沿用 `PartialEq` 中定义的 `eq()` 方法, 104 | /// 同时声明对于任意对象 `a` 满足 `a == a`。 105 | impl Eq for Thread {} 106 | 107 | /// 通过线程 ID 来哈希 108 | impl Hash for Thread { 109 | fn hash(&self, state: &mut H) { 110 | state.write_isize(self.id); 111 | } 112 | } 113 | 114 | /// 打印线程除了父进程以外的信息 115 | impl core::fmt::Debug for Thread { 116 | fn fmt(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { 117 | formatter 118 | .debug_struct("Thread") 119 | .field("thread_id", &self.id) 120 | .field("stack", &self.stack) 121 | .field("context", &self.inner().context) 122 | .finish() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /os/src/sbi.rs: -------------------------------------------------------------------------------- 1 | //! 调用 Machine 层的操作 2 | // 目前还不会用到全部的 SBI 调用,暂时允许未使用的变量或函数 3 | #![allow(unused)] 4 | 5 | /// SBI 调用 6 | #[inline(always)] 7 | fn sbi_call(which: usize, arg0: usize, arg1: usize, arg2: usize) -> usize { 8 | let mut ret; 9 | unsafe { 10 | llvm_asm!("ecall" 11 | : "={x10}" (ret) 12 | : "{x10}" (arg0), "{x11}" (arg1), "{x12}" (arg2), "{x17}" (which) 13 | : "memory" // 如果汇编可能改变内存,则需要加入 memory 选项 14 | : "volatile"); // 防止编译器做激进的优化(如调换指令顺序等破坏 SBI 调用行为的优化) 15 | } 16 | ret 17 | } 18 | 19 | const SBI_SET_TIMER: usize = 0; 20 | const SBI_CONSOLE_PUTCHAR: usize = 1; 21 | const SBI_CONSOLE_GETCHAR: usize = 2; 22 | const SBI_CLEAR_IPI: usize = 3; 23 | const SBI_SEND_IPI: usize = 4; 24 | const SBI_REMOTE_FENCE_I: usize = 5; 25 | const SBI_REMOTE_SFENCE_VMA: usize = 6; 26 | const SBI_REMOTE_SFENCE_VMA_ASID: usize = 7; 27 | const SBI_SHUTDOWN: usize = 8; 28 | 29 | /// 向控制台输出一个字符 30 | /// 31 | /// 需要注意我们不能直接使用 Rust 中的 char 类型 32 | pub fn console_putchar(c: usize) { 33 | sbi_call(SBI_CONSOLE_PUTCHAR, c, 0, 0); 34 | } 35 | 36 | /// 从控制台中读取一个字符 37 | /// 38 | /// 没有读取到字符则返回 -1 39 | pub fn console_getchar() -> usize { 40 | sbi_call(SBI_CONSOLE_GETCHAR, 0, 0, 0) 41 | } 42 | 43 | /// 调用 SBI_SHUTDOWN 来关闭操作系统(直接退出 QEMU) 44 | pub fn shutdown() -> ! { 45 | sbi_call(SBI_SHUTDOWN, 0, 0, 0); 46 | unreachable!() 47 | } 48 | 49 | /// 设置下一次时钟中断的时间 50 | pub fn set_timer(time: usize) { 51 | sbi_call(SBI_SET_TIMER, time, 0, 0); 52 | } 53 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | nightly-2020-06-27 -------------------------------------------------------------------------------- /user/.cargo/config: -------------------------------------------------------------------------------- 1 | # 编译的目标平台 2 | [build] 3 | target = "riscv64imac-unknown-none-elf" 4 | -------------------------------------------------------------------------------- /user/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "user_lib" 3 | version = "0.1.0" 4 | authors = ["涂轶翔 ", "赵成钢 "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | buddy_system_allocator = "0.4.0" -------------------------------------------------------------------------------- /user/Makefile: -------------------------------------------------------------------------------- 1 | TARGET := riscv64imac-unknown-none-elf 2 | MODE := debug 3 | 4 | # 用户程序目录 5 | SRC_DIR := src/bin 6 | # 编译后执行文件目录 7 | TARGET_DIR := target/$(TARGET)/$(MODE) 8 | # 用户程序源文件 9 | SRC_FILES := $(wildcard $(SRC_DIR)/*.rs) 10 | # 根据源文件取得编译后的执行文件 11 | BIN_FILES := $(patsubst $(SRC_DIR)/%.rs, $(TARGET_DIR)/%, $(SRC_FILES)) 12 | 13 | OUT_DIR := build/disk 14 | IMG_FILE := build/raw.img 15 | QCOW_FILE := build/disk.img 16 | 17 | .PHONY: dependency build clean 18 | 19 | # 安装 rcore-fs-fuse 工具 20 | dependency: 21 | ifeq ($(shell which rcore-fs-fuse),) 22 | @echo Installing rcore-fs-fuse 23 | @cargo install rcore-fs-fuse --git https://github.com/rcore-os/rcore-fs 24 | endif 25 | 26 | # 编译、打包、格式转换、预留空间 27 | build: dependency 28 | @cargo build 29 | @echo Targets: $(patsubst $(SRC_DIR)/%.rs, %, $(SRC_FILES)) 30 | @rm -rf $(OUT_DIR) 31 | @mkdir -p $(OUT_DIR) 32 | @cp $(BIN_FILES) $(OUT_DIR) 33 | @rcore-fs-fuse --fs sfs $(IMG_FILE) $(OUT_DIR) zip 34 | @qemu-img convert -f raw $(IMG_FILE) -O qcow2 $(QCOW_FILE) 35 | @qemu-img resize $(QCOW_FILE) +1G 36 | 37 | clean: 38 | @cargo clean 39 | @rm -rf $(OUT_DIR) $(IMG_FILE) $(QCOW_FILE) -------------------------------------------------------------------------------- /user/src/bin/hello_world.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | #[macro_use] 5 | extern crate user_lib; 6 | 7 | #[no_mangle] 8 | pub fn main() -> usize { 9 | println!("Hello world from user mode program!"); 10 | 0 11 | } 12 | -------------------------------------------------------------------------------- /user/src/bin/notebook.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | #[macro_use] 5 | extern crate user_lib; 6 | 7 | use user_lib::console::*; 8 | 9 | #[no_mangle] 10 | pub fn main() -> ! { 11 | println!("\x1b[2J"); 12 | loop { 13 | let string = getchars(); 14 | print!("{}", string); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /user/src/config.rs: -------------------------------------------------------------------------------- 1 | /// 每个用户进程所用的堆大小(1M) 2 | pub const USER_HEAP_SIZE: usize = 0x10_0000; 3 | -------------------------------------------------------------------------------- /user/src/console.rs: -------------------------------------------------------------------------------- 1 | //! 在系统调用基础上实现 `print!` `println!` 2 | //! 3 | //! 代码与 `os` crate 中的 `console.rs` 基本相同 4 | 5 | use crate::syscall::*; 6 | use alloc::string::String; 7 | use core::fmt::{self, Write}; 8 | 9 | /// 实现 [`core::fmt::Write`] trait 来进行格式化输出 10 | struct Stdout; 11 | 12 | impl Write for Stdout { 13 | /// 打印一个字符串 14 | fn write_str(&mut self, s: &str) -> fmt::Result { 15 | sys_write(STDOUT, s.as_bytes()); 16 | Ok(()) 17 | } 18 | } 19 | 20 | /// 打印由 [`core::format_args!`] 格式化后的数据 21 | pub fn print(args: fmt::Arguments) { 22 | Stdout.write_fmt(args).unwrap(); 23 | } 24 | 25 | /// 实现类似于标准库中的 `print!` 宏 26 | #[macro_export] 27 | macro_rules! print { 28 | ($fmt: literal $(, $($arg: tt)+)?) => { 29 | $crate::console::print(format_args!($fmt $(, $($arg)+)?)); 30 | } 31 | } 32 | 33 | /// 实现类似于标准库中的 `println!` 宏 34 | #[macro_export] 35 | macro_rules! println { 36 | ($fmt: literal $(, $($arg: tt)+)?) => { 37 | $crate::console::print(format_args!(concat!($fmt, "\n") $(, $($arg)+)?)); 38 | } 39 | } 40 | 41 | /// 从控制台读取一个字符(阻塞) 42 | pub fn getchar() -> u8 { 43 | let mut c = [0u8; 1]; 44 | sys_read(STDIN, &mut c); 45 | c[0] 46 | } 47 | 48 | /// 从控制台读取一个或多个字符(阻塞) 49 | pub fn getchars() -> String { 50 | let mut buffer = [0u8; 64]; 51 | loop { 52 | let size = sys_read(STDIN, &mut buffer); 53 | if let Ok(string) = String::from_utf8(buffer.iter().copied().take(size as usize).collect()) 54 | { 55 | return string; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /user/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 为各种用户程序提供依赖 2 | //! 3 | //! - 动态内存分配(允许使用 alloc,但总大小固定) 4 | //! - 错误处理(打印信息并退出程序) 5 | 6 | #![no_std] 7 | #![feature(llvm_asm)] 8 | #![feature(lang_items)] 9 | #![feature(panic_info_message)] 10 | #![feature(linkage)] 11 | 12 | pub mod config; 13 | pub mod syscall; 14 | 15 | #[macro_use] 16 | pub mod console; 17 | 18 | extern crate alloc; 19 | 20 | pub use crate::syscall::*; 21 | use buddy_system_allocator::LockedHeap; 22 | use config::USER_HEAP_SIZE; 23 | use core::alloc::Layout; 24 | use core::panic::PanicInfo; 25 | 26 | /// 大小为 [`USER_HEAP_SIZE`] 的堆空间 27 | static mut HEAP_SPACE: [u8; USER_HEAP_SIZE] = [0; USER_HEAP_SIZE]; 28 | 29 | /// 使用 `buddy_system_allocator` 中的堆 30 | #[global_allocator] 31 | static HEAP: LockedHeap = LockedHeap::empty(); 32 | 33 | /// 打印 panic 信息并退出用户程序 34 | #[panic_handler] 35 | fn panic(info: &PanicInfo) -> ! { 36 | if let Some(location) = info.location() { 37 | println!( 38 | "\x1b[1;31m{}:{}: '{}'\x1b[0m", 39 | location.file(), 40 | location.line(), 41 | info.message().unwrap() 42 | ); 43 | } else { 44 | println!("\x1b[1;31mpanic: '{}'\x1b[0m", info.message().unwrap()); 45 | } 46 | sys_exit(-1); 47 | } 48 | 49 | /// 程序入口 50 | #[no_mangle] 51 | pub extern "C" fn _start(_args: isize, _argv: *const u8) -> ! { 52 | unsafe { 53 | HEAP.lock() 54 | .init(HEAP_SPACE.as_ptr() as usize, USER_HEAP_SIZE); 55 | } 56 | sys_exit(main()) 57 | } 58 | 59 | /// 默认的 main 函数 60 | /// 61 | /// 设置了弱的 linkage,会被 `bin` 中文件的 `main` 函数取代 62 | #[linkage = "weak"] 63 | #[no_mangle] 64 | fn main() -> isize { 65 | panic!("no main() linked"); 66 | } 67 | 68 | /// 终止程序 69 | #[no_mangle] 70 | pub extern "C" fn abort() { 71 | panic!("abort"); 72 | } 73 | 74 | /// 内存不足时终止程序 75 | #[lang = "oom"] 76 | fn oom(_: Layout) -> ! { 77 | panic!("out of memory"); 78 | } 79 | -------------------------------------------------------------------------------- /user/src/syscall.rs: -------------------------------------------------------------------------------- 1 | //! 系统调用 2 | 3 | pub const STDIN: usize = 0; 4 | pub const STDOUT: usize = 1; 5 | 6 | const SYSCALL_READ: usize = 63; 7 | const SYSCALL_WRITE: usize = 64; 8 | const SYSCALL_EXIT: usize = 93; 9 | 10 | /// 将参数放在对应寄存器中,并执行 `ecall` 11 | fn syscall(id: usize, arg0: usize, arg1: usize, arg2: usize) -> isize { 12 | // 返回值 13 | let mut ret; 14 | unsafe { 15 | llvm_asm!("ecall" 16 | : "={x10}" (ret) 17 | : "{x10}" (arg0), "{x11}" (arg1), "{x12}" (arg2), "{x17}" (id) 18 | : "memory" // 如果汇编可能改变内存,则需要加入 memory 选项 19 | : "volatile"); // 防止编译器做激进的优化(如调换指令顺序等破坏 SBI 调用行为的优化) 20 | } 21 | ret 22 | } 23 | 24 | /// 读取字符 25 | pub fn sys_read(fd: usize, buffer: &mut [u8]) -> isize { 26 | loop { 27 | let ret = syscall( 28 | SYSCALL_READ, 29 | fd, 30 | buffer as *const [u8] as *const u8 as usize, 31 | buffer.len(), 32 | ); 33 | if ret > 0 { 34 | return ret; 35 | } 36 | } 37 | } 38 | 39 | /// 打印字符串 40 | pub fn sys_write(fd: usize, buffer: &[u8]) -> isize { 41 | syscall( 42 | SYSCALL_WRITE, 43 | fd, 44 | buffer as *const [u8] as *const u8 as usize, 45 | buffer.len(), 46 | ) 47 | } 48 | 49 | /// 退出并返回数值 50 | pub fn sys_exit(code: isize) -> ! { 51 | syscall(SYSCALL_EXIT, code as usize, 0, 0); 52 | unreachable!() 53 | } 54 | --------------------------------------------------------------------------------