├── show.sh ├── all.sh ├── source ├── _static │ ├── my_style.css │ └── dracula.css ├── chapter0 │ ├── EE.png │ ├── run-app.png │ ├── syscall.png │ ├── basic-EE.png │ ├── exception.png │ ├── file-disk.png │ ├── interrupt.png │ ├── complex-EE.png │ ├── k210-final.gif │ ├── k210-final.png │ ├── prepare-sd.gif │ ├── prepare-sd.png │ ├── qemu-final.gif │ ├── qemu-final.png │ ├── address-space.png │ ├── computer-hw-sw.png │ ├── context-switch.png │ ├── exception-old.png │ ├── prog-illusion.png │ ├── context-of-process.png │ ├── index.rst │ ├── 7exercise.rst │ ├── 6hardware.rst.org │ ├── 4os-features.rst │ ├── 8answer.rst │ └── 2os-interface.rst ├── chapter3 │ ├── ch3.pptx │ ├── switch.png │ ├── fsm-coop.png │ ├── switch-1.png │ ├── switch-2.png │ ├── task-context.png │ ├── task_context.png │ ├── multiprogramming.png │ ├── index.rst │ ├── 6answer.rst │ ├── 1multi-loader.rst │ ├── 5exercise.rst │ └── 2task-switching.rst ├── chapter4 │ ├── satp.png │ ├── trie.png │ ├── pte-rwx.png │ ├── trie-1.png │ ├── sv39-full.png │ ├── sv39-pte.png │ ├── app-as-full.png │ ├── linear-table.png │ ├── mem-levels.png │ ├── page-table.png │ ├── segmentation.png │ ├── sv39-va-pa.png │ ├── kernel-as-high.png │ ├── kernel-as-low.png │ ├── rust-containers.png │ ├── simple-base-bound.png │ ├── address-translation.png │ ├── index.rst │ ├── 8exercise.rst │ ├── 9answer.rst │ └── 2address-space.rst ├── chapter6 │ ├── ch6-fs.pptx │ ├── 文件系统布局.png │ ├── index.rst │ ├── 5answer.rst │ └── 4exercise.rst ├── chapter7 │ ├── signal.png │ ├── user-stack-cmdargs.png │ ├── index.rst │ └── 5exercise.rst ├── chapter9 │ ├── stream.png │ ├── vring.png │ ├── device-tree.png │ ├── stream-queue.png │ ├── virtio-arch.png │ ├── virtio-blk.png │ ├── virtio-simple-arch.png │ ├── virtio-test-example.png │ ├── virtio-cpu-device-io.png │ ├── virtio-cpu-device-io2.png │ ├── index.rst │ ├── 5answer.rst │ ├── 4exercise.rst │ └── 0intro.rst ├── resources │ ├── test.gif │ └── test.png ├── chapter1 │ ├── CallStack.png │ ├── MemoryLayout.png │ ├── StackFrame.png │ ├── boot_stack.png │ ├── color-demo.png │ ├── function-call.png │ ├── link-sections.png │ ├── load-into-qemu.png │ ├── app-software-stack.png │ ├── index.rst │ ├── 8answer.rst │ └── 7exercise.rst ├── chapter2 │ ├── deng-fish.png │ ├── PrivilegeStack.png │ ├── EnvironmentCallFlow.png │ ├── index.rst │ ├── 5exercise.rst │ ├── 6answer.rst │ └── ch2.py ├── chapter8 │ ├── app-as-full-with-threads.png │ ├── 7answer.rst │ ├── index.rst │ ├── 6exercise.rst │ ├── 5concurrency-problem.rst │ └── 3semaphore.rst ├── appendix-d │ ├── index.rst │ ├── 1asm.rst │ └── 2rv.rst ├── chapter5 │ ├── index.rst │ └── 6answer.rst ├── appendix-c │ └── index.rst ├── pygments-coloring.txt ├── setup-sphinx.rst ├── rest-example.rst ├── appendix-e │ └── index.rst ├── appendix-a │ └── index.rst ├── index.rst ├── log.rst ├── conf.py └── final-lab.rst ├── .gitignore ├── .gitmodules ├── requirements.txt ├── .github └── workflows │ └── deploy.yml ├── README.md ├── todo.md ├── make.bat ├── scripts └── fix-comments.py ├── Makefile └── outline.md /show.sh: -------------------------------------------------------------------------------- 1 | make html && google-chrome build/html/index.html 2 | 3 | -------------------------------------------------------------------------------- /all.sh: -------------------------------------------------------------------------------- 1 | make clean && make html && google-chrome build/html/index.html 2 | 3 | -------------------------------------------------------------------------------- /source/_static/my_style.css: -------------------------------------------------------------------------------- 1 | .wy-nav-content { 2 | max-width: 1200px !important; 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .vscode/ 3 | .idea 4 | source/_build/ 5 | pushall.sh 6 | os-lectures 7 | -------------------------------------------------------------------------------- /source/chapter0/EE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/EE.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "os-lectures"] 2 | path = os-lectures 3 | url = git@github.com:LearningOS/os-lectures.git 4 | -------------------------------------------------------------------------------- /source/chapter3/ch3.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter3/ch3.pptx -------------------------------------------------------------------------------- /source/chapter4/satp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter4/satp.png -------------------------------------------------------------------------------- /source/chapter4/trie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter4/trie.png -------------------------------------------------------------------------------- /source/chapter0/run-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/run-app.png -------------------------------------------------------------------------------- /source/chapter0/syscall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/syscall.png -------------------------------------------------------------------------------- /source/chapter3/switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter3/switch.png -------------------------------------------------------------------------------- /source/chapter4/pte-rwx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter4/pte-rwx.png -------------------------------------------------------------------------------- /source/chapter4/trie-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter4/trie-1.png -------------------------------------------------------------------------------- /source/chapter6/ch6-fs.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter6/ch6-fs.pptx -------------------------------------------------------------------------------- /source/chapter6/文件系统布局.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter6/文件系统布局.png -------------------------------------------------------------------------------- /source/chapter7/signal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter7/signal.png -------------------------------------------------------------------------------- /source/chapter9/stream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter9/stream.png -------------------------------------------------------------------------------- /source/chapter9/vring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter9/vring.png -------------------------------------------------------------------------------- /source/resources/test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/resources/test.gif -------------------------------------------------------------------------------- /source/resources/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/resources/test.png -------------------------------------------------------------------------------- /source/chapter0/basic-EE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/basic-EE.png -------------------------------------------------------------------------------- /source/chapter0/exception.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/exception.png -------------------------------------------------------------------------------- /source/chapter0/file-disk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/file-disk.png -------------------------------------------------------------------------------- /source/chapter0/interrupt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/interrupt.png -------------------------------------------------------------------------------- /source/chapter1/CallStack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter1/CallStack.png -------------------------------------------------------------------------------- /source/chapter2/deng-fish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter2/deng-fish.png -------------------------------------------------------------------------------- /source/chapter3/fsm-coop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter3/fsm-coop.png -------------------------------------------------------------------------------- /source/chapter3/switch-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter3/switch-1.png -------------------------------------------------------------------------------- /source/chapter3/switch-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter3/switch-2.png -------------------------------------------------------------------------------- /source/chapter4/sv39-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter4/sv39-full.png -------------------------------------------------------------------------------- /source/chapter4/sv39-pte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter4/sv39-pte.png -------------------------------------------------------------------------------- /source/chapter0/complex-EE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/complex-EE.png -------------------------------------------------------------------------------- /source/chapter0/k210-final.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/k210-final.gif -------------------------------------------------------------------------------- /source/chapter0/k210-final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/k210-final.png -------------------------------------------------------------------------------- /source/chapter0/prepare-sd.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/prepare-sd.gif -------------------------------------------------------------------------------- /source/chapter0/prepare-sd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/prepare-sd.png -------------------------------------------------------------------------------- /source/chapter0/qemu-final.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/qemu-final.gif -------------------------------------------------------------------------------- /source/chapter0/qemu-final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/qemu-final.png -------------------------------------------------------------------------------- /source/chapter1/MemoryLayout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter1/MemoryLayout.png -------------------------------------------------------------------------------- /source/chapter1/StackFrame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter1/StackFrame.png -------------------------------------------------------------------------------- /source/chapter1/boot_stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter1/boot_stack.png -------------------------------------------------------------------------------- /source/chapter1/color-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter1/color-demo.png -------------------------------------------------------------------------------- /source/chapter3/task-context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter3/task-context.png -------------------------------------------------------------------------------- /source/chapter3/task_context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter3/task_context.png -------------------------------------------------------------------------------- /source/chapter4/app-as-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter4/app-as-full.png -------------------------------------------------------------------------------- /source/chapter4/linear-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter4/linear-table.png -------------------------------------------------------------------------------- /source/chapter4/mem-levels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter4/mem-levels.png -------------------------------------------------------------------------------- /source/chapter4/page-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter4/page-table.png -------------------------------------------------------------------------------- /source/chapter4/segmentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter4/segmentation.png -------------------------------------------------------------------------------- /source/chapter4/sv39-va-pa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter4/sv39-va-pa.png -------------------------------------------------------------------------------- /source/chapter9/device-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter9/device-tree.png -------------------------------------------------------------------------------- /source/chapter9/stream-queue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter9/stream-queue.png -------------------------------------------------------------------------------- /source/chapter9/virtio-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter9/virtio-arch.png -------------------------------------------------------------------------------- /source/chapter9/virtio-blk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter9/virtio-blk.png -------------------------------------------------------------------------------- /source/chapter0/address-space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/address-space.png -------------------------------------------------------------------------------- /source/chapter0/computer-hw-sw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/computer-hw-sw.png -------------------------------------------------------------------------------- /source/chapter0/context-switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/context-switch.png -------------------------------------------------------------------------------- /source/chapter0/exception-old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/exception-old.png -------------------------------------------------------------------------------- /source/chapter0/prog-illusion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/prog-illusion.png -------------------------------------------------------------------------------- /source/chapter1/function-call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter1/function-call.png -------------------------------------------------------------------------------- /source/chapter1/link-sections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter1/link-sections.png -------------------------------------------------------------------------------- /source/chapter1/load-into-qemu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter1/load-into-qemu.png -------------------------------------------------------------------------------- /source/chapter2/PrivilegeStack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter2/PrivilegeStack.png -------------------------------------------------------------------------------- /source/chapter4/kernel-as-high.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter4/kernel-as-high.png -------------------------------------------------------------------------------- /source/chapter4/kernel-as-low.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter4/kernel-as-low.png -------------------------------------------------------------------------------- /source/chapter3/multiprogramming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter3/multiprogramming.png -------------------------------------------------------------------------------- /source/chapter4/rust-containers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter4/rust-containers.png -------------------------------------------------------------------------------- /source/chapter4/simple-base-bound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter4/simple-base-bound.png -------------------------------------------------------------------------------- /source/chapter0/context-of-process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter0/context-of-process.png -------------------------------------------------------------------------------- /source/chapter1/app-software-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter1/app-software-stack.png -------------------------------------------------------------------------------- /source/chapter2/EnvironmentCallFlow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter2/EnvironmentCallFlow.png -------------------------------------------------------------------------------- /source/chapter4/address-translation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter4/address-translation.png -------------------------------------------------------------------------------- /source/chapter7/user-stack-cmdargs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter7/user-stack-cmdargs.png -------------------------------------------------------------------------------- /source/chapter9/virtio-simple-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter9/virtio-simple-arch.png -------------------------------------------------------------------------------- /source/chapter9/virtio-test-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter9/virtio-test-example.png -------------------------------------------------------------------------------- /source/chapter9/virtio-cpu-device-io.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter9/virtio-cpu-device-io.png -------------------------------------------------------------------------------- /source/chapter9/virtio-cpu-device-io2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter9/virtio-cpu-device-io2.png -------------------------------------------------------------------------------- /source/chapter8/app-as-full-with-threads.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/rCore-Tutorial-Book-v3/HEAD/source/chapter8/app-as-full-with-threads.png -------------------------------------------------------------------------------- /source/appendix-d/index.rst: -------------------------------------------------------------------------------- 1 | 附录 D:RISC-V相关信息 2 | ================================================= 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | 1asm 9 | 2rv -------------------------------------------------------------------------------- /source/chapter9/index.rst: -------------------------------------------------------------------------------- 1 | .. _link-chapter9: 2 | 3 | 第九章:I/O设备管理 4 | ============================================== 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | 9 | 0intro 10 | 1io-interface 11 | 2device-driver-1 12 | 2device-driver-2 13 | 4exercise 14 | 5answer 15 | -------------------------------------------------------------------------------- /source/chapter2/index.rst: -------------------------------------------------------------------------------- 1 | .. _link-chapter2: 2 | 3 | 第二章:批处理系统 4 | ============================================== 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | 9 | 0intro 10 | 1rv-privilege 11 | 2application 12 | 3batch-system 13 | 4trap-handling 14 | 5exercise 15 | 6answer 16 | 17 | -------------------------------------------------------------------------------- /source/chapter8/7answer.rst: -------------------------------------------------------------------------------- 1 | 练习参考答案 2 | ===================================================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | 课后练习 9 | ------------------------------- 10 | 11 | 编程题 12 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 13 | 14 | 15 | 16 | 问答题 17 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 | -------------------------------------------------------------------------------- /source/chapter9/5answer.rst: -------------------------------------------------------------------------------- 1 | 练习参考答案 2 | ===================================================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | 课后练习 9 | ------------------------------- 10 | 11 | 编程题 12 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 13 | 14 | 15 | 16 | 问答题 17 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 | -------------------------------------------------------------------------------- /source/chapter5/index.rst: -------------------------------------------------------------------------------- 1 | .. _link-chapter5: 2 | 3 | 第五章:进程 4 | ============================================== 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | 9 | 0intro 10 | 1process 11 | 2core-data-structures 12 | 3implement-process-mechanism 13 | 4scheduling 14 | 5exercise 15 | 6answer 16 | 17 | .. chyyuu 有一节来 回顾,归纳总结进程 抽象/虚拟??? -------------------------------------------------------------------------------- /source/chapter3/index.rst: -------------------------------------------------------------------------------- 1 | .. _link-chapter3: 2 | 3 | 第三章:多道程序与分时多任务 4 | ============================================== 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | 9 | 0intro 10 | 1multi-loader 11 | 2task-switching 12 | 3multiprogramming 13 | 4time-sharing-system 14 | 5exercise 15 | 6answer 16 | 17 | .. chyyuu 可以在的最后一节这添加有关调度的介绍??? 18 | -------------------------------------------------------------------------------- /source/chapter6/index.rst: -------------------------------------------------------------------------------- 1 | .. _link-chapter6: 2 | 3 | 第六章:文件系统 4 | ============================================== 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | 9 | 0intro 10 | 1fs-interface 11 | 2fs-implementation 12 | 3using-easy-fs-in-kernel 13 | 4exercise 14 | 5answer 15 | 16 | .. chyyuu 17 | 最晚灭绝的“霸王龙”操作系统 18 | 在扩展章节,添加其他文件系统的介绍 19 | -------------------------------------------------------------------------------- /source/chapter0/index.rst: -------------------------------------------------------------------------------- 1 | .. _link-chapter0: 2 | 3 | 第零章:操作系统概述 4 | ============================================== 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | 9 | 0intro 10 | 1what-is-os 11 | 2os-interface 12 | 3os-hw-abstract 13 | 4os-features 14 | 5setup-devel-env 15 | 7exercise 16 | 8answer 17 | 18 | .. chyyuu 注:可以把“6hardware”k210相关的内容放到某个附录中 ??? -------------------------------------------------------------------------------- /source/chapter8/index.rst: -------------------------------------------------------------------------------- 1 | .. _link-chapter8: 2 | 3 | 第八章:并发 4 | ============================================== 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | 9 | 0intro 10 | 1thread 11 | 1thread-kernel 12 | 2lock 13 | 3semaphore 14 | 4condition-variable 15 | 5concurrency-problem 16 | 6exercise 17 | 7answer 18 | .. chyyuu 19 | 扩展章节,添加其他类型同步互斥的介绍 -------------------------------------------------------------------------------- /source/chapter7/index.rst: -------------------------------------------------------------------------------- 1 | .. _link-chapter7: 2 | 3 | 第七章:进程间通信与 I/O 重定向 4 | ============================================== 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | 9 | 0intro 10 | 1file-descriptor 11 | 2pipe 12 | 3cmdargs-and-redirection 13 | 4signal 14 | 5exercise 15 | 6answer 16 | 17 | .. chyyuu 18 | 有团队协作能力的“迅猛龙”操作系统。 19 | 在扩展章节,添加其他类型的IPC介绍 -------------------------------------------------------------------------------- /source/chapter4/index.rst: -------------------------------------------------------------------------------- 1 | .. _link-chapter4: 2 | 3 | 第四章:地址空间 4 | ============================================== 5 | 6 | .. toctree:: 7 | :maxdepth: 5 8 | 9 | 0intro 10 | 1rust-dynamic-allocation 11 | 2address-space 12 | 3sv39-implementation-1 13 | 4sv39-implementation-2 14 | 5kernel-app-spaces 15 | 6multitasking-based-on-as 16 | 7more-as 17 | 8exercise 18 | 9answer 19 | 20 | .. chyyuu 添加扩展阅读 虚拟内存超越物理内存,通过换页机制??? -------------------------------------------------------------------------------- /source/chapter1/index.rst: -------------------------------------------------------------------------------- 1 | .. _link-chapter1: 2 | 3 | 第一章:应用程序与基本执行环境 4 | ============================================== 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | 9 | 0intro 10 | 1app-ee-platform 11 | 2remove-std 12 | 3first-instruction-in-kernel1 13 | 4first-instruction-in-kernel2 14 | 5support-func-call 15 | 6print-and-shutdown-based-on-sbi 16 | 7exercise 17 | 8answer 18 | 19 | 20 | .. chyyuu 在后面介绍一些操作系统的大致架构,作为理论知识的扩展??? 21 | -------------------------------------------------------------------------------- /source/appendix-d/1asm.rst: -------------------------------------------------------------------------------- 1 | RISCV 汇编相关 2 | ========================= 3 | 4 | - `RISC-V Assembly Programmer's Manual `_ 5 | - `RISC-V Low-level Test Suits `_ 6 | - `CoreMark®-PRO comprehensive, advanced processor benchmark `_ 7 | - `riscv-tests的使用 `_ -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.12 2 | Babel==2.9.1 3 | certifi==2021.5.30 4 | charset-normalizer==2.0.4 5 | docutils==0.16 6 | idna==3.2 7 | imagesize==1.2.0 8 | jieba==0.42.1 9 | Jinja2==3.0.1 10 | MarkupSafe==2.0.1 11 | packaging==21.0 12 | Pygments==2.10.0 13 | pyparsing==2.4.7 14 | pytz==2021.1 15 | requests==2.26.0 16 | snowballstemmer==2.1.0 17 | Sphinx==4.3.2 18 | sphinx-comments==0.0.3 19 | sphinx-rtd-theme==1.0.0 20 | sphinx-tabs==3.2.0 21 | sphinxcontrib-applehelp==1.0.2 22 | sphinxcontrib-devhelp==1.0.2 23 | sphinxcontrib-htmlhelp==2.0.0 24 | sphinxcontrib-jsmath==1.0.1 25 | sphinxcontrib-qthelp==1.0.3 26 | sphinxcontrib-serializinghtml==1.1.5 27 | urllib3==1.26.6 28 | furo==2022.6.21 -------------------------------------------------------------------------------- /source/appendix-c/index.rst: -------------------------------------------------------------------------------- 1 | 附录 C:深入机器模式:RustSBI 2 | ================================================= 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | RISC-V指令集的SBI标准规定了类Unix操作系统之下的运行环境规范。这个规范拥有多种实现,RustSBI是它的一种实现。 9 | 10 | RISC-V架构中,存在着定义于操作系统之下的运行环境。这个运行环境不仅将引导启动RISC-V下的操作系统, 还将常驻后台,为操作系统提供一系列二进制接口,以便其获取和操作硬件信息。 RISC-V给出了此类环境和二进制接口的规范,称为“操作系统二进制接口”,即“SBI”。 11 | 12 | SBI的实现是在M模式下运行的特定于平台的固件,它将管理S、U等特权上的程序或通用的操作系统。 13 | 14 | RustSBI项目发起于鹏城实验室的“rCore代码之夏-2020”活动,它是完全由Rust语言开发的SBI实现。 现在它能够在支持的RISC-V设备上运行rCore教程和其它操作系统内核。 15 | 16 | RustSBI项目的目标是,制作一个从固件启动的最小Rust语言SBI实现,为可能的复杂实现提供参考和支持。 RustSBI也可以作为一个库使用,帮助更多的SBI开发者适配自己的平台,以支持更多处理器核和片上系统。 17 | 18 | 当前项目实现源码:https://github.com/rustsbi/rustsbi 19 | -------------------------------------------------------------------------------- /source/pygments-coloring.txt: -------------------------------------------------------------------------------- 1 | Pygments 默认配色: 2 | Keyword.Constant 深绿加粗 3 | Keyword.Declaration 深绿加粗 4 | Keyword.Namespace 深绿加粗 5 | Keyword.Pseudo 浅绿 6 | Keyword.Reserved 深绿加粗 7 | Keyword.Type 樱桃红 8 | Name.Attribute 棕黄 9 | Name.Builtin 浅绿 10 | Name.Builtin.Pseudo 浅绿 11 | Name.Class 深蓝加粗 12 | Name.Constant 棕红 13 | Name.Decorator 浅紫 14 | Name.Entity 灰色 15 | Name.Exception 深红 16 | Name.Function 深蓝 17 | Name.Function.Magic 深蓝 18 | Name.Label 棕黄 19 | Name.Namespace 深蓝加粗 20 | Name.Other 默认黑色 21 | Name.Tag 深绿加粗 22 | Name.Variable 蓝黑 23 | 24 | 25 | 通用寄存器 -> 棕黄 Name.Attribute 26 | CSR -> 棕红 Name.Constant 27 | 指令 -> 浅紫 Name.Decorator 28 | 伪指令 -> 樱桃红 Keyword.Type 29 | Directives -> 深蓝 Name.Function 30 | 标签/剩余字面量 -> 浅绿 Name.Builtin 31 | 数字 -> Number 32 | 33 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: [main, dev] 6 | 7 | jobs: 8 | deploy-doc: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | submodules: "true" 15 | - uses: actions/setup-python@v2 16 | with: 17 | python-version: "3.10" 18 | - name: Install dependencies 19 | run: pip install -r requirements.txt 20 | 21 | - name: build doc 22 | run: make html 23 | 24 | - name: create .nojekyll 25 | run: touch build/html/.nojekyll 26 | 27 | - name: Push to gh-pages 28 | uses: peaceiris/actions-gh-pages@v3 29 | with: 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | publish_dir: ./build/html 32 | -------------------------------------------------------------------------------- /source/setup-sphinx.rst: -------------------------------------------------------------------------------- 1 | 修改和构建本项目 2 | ==================================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | 1. 参考 `这里 `_ 安装 Sphinx。 9 | 2. ``pip install sphinx_rtd_theme`` 安装 Read The Docs 主题。 10 | 3. ``pip install jieba`` 安装中文分词。 11 | 4. ``pip install sphinx-comments`` 安装 Sphinx 讨论区插件。 12 | 5. :doc:`/rest-example` 是 ReST 的一些基本语法,也可以参考已完成的文档。 13 | 6. 修改之后,在项目根目录下 ``make clean && make html`` 即可在 ``build/html/index.html`` 查看本地构建的主页。请注意在修改章节目录结构之后需要 ``make clean`` 一下,不然可能无法正常更新。 14 | 7. 如想对项目做贡献的话,直接提交 pull request 即可。 15 | 16 | 补充: 17 | 18 | 支持实时显示修改rst文件后的html文档的方法: 19 | 20 | 1. ``pip install autoload`` 安装 Sphinx 自动加载插件。 21 | 2. 在项目根目录下 ``sphinx-autobuild source build/html`` 即可在浏览器中访问 `http://127.0.0.1:8000/` 查看本地构建的主页。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rCore-Tutorial-Book-v3 2 | Documentation of rCore-Tutorial version 3 in Chinese. 3 | 4 | ## news 5 | 6 | - 2022.07.01 Welcome to JOIN [**Open-Source-OS-Training-Camp-2022 !**](https://learningos.github.io/rust-based-os-comp2022/) 7 | 8 | ## [Deployed Page](https://rcore-os.github.io/rCore-Tutorial-Book-v3/). 9 | 10 | If you cannot access `github.io` normally due to network problems, please visit the [synchronized version](http://wyfcyx.gitee.io/rcore-tutorial-book-v3) hosted on gitee. 11 | 12 | ## Deploy your own docs 13 | 14 | ```sh 15 | $ FORK https://github.com/rcore-os/rCore-Tutorial-Book-v3.git to YOUR REPO 16 | $ git clone YOUR REPO(e.g. https://github.com/YOUR/rCore-Tutorial-Book-v3.git) 17 | $ cd rCore-Tutorial-Book-v3 18 | $ make html # After that, the generated doc can be found in rCore-Tutorial-Book-v3/build/html 19 | $ # modify the doc 20 | $ git push # or pull request 21 | ``` 22 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | # 对于一些概念等,要有一些比较具体的对应 2 | # 对于如何一步一步实现,需要有阶段性的实验结果 3 | 4 | ## ch0 5 | ### 2os-interface 6 | - 缺少对明确rcore-tutorial 系统调用的概述 7 | 8 | ### 3os-hw-abstract.html 9 | - 各种抽象能否有些具体的代码对应? 10 | 11 | ### 4os-features 12 | - 特征能用代码运行来展现吗? 13 | 14 | ### 5setup-devel-env 15 | - 想把k210相关的挪到附录中 16 | 17 | 最后有个Q&A比较好 18 | 19 | 20 | ## ch1 21 | 22 | 三叶虫需要的海洋(硬件)和食物(rustsbi),通过请求sbi call 获得输出能力 23 | 24 | - 介绍应用,以及围绕应用的环境 25 | 26 | - 解释 sbi 新的参数约定 27 | 28 | ## ch2 29 | - 引言 对应用程序的进一步讲解 30 | - 特权级在这一章不是必须的 31 | 32 | ## ch3-ch9 33 | - 引言 对应用程序的进一步讲解 34 | 35 | ## ch4 36 | - 页面置换算法的实践体现 37 | 38 | ## ch5 39 | - 调度算法的实践体现 40 | 41 | ## ch6 42 | - 从应用角度出发,基于ram来讲解,并逐步扩展,比较方便 43 | 44 | ## ch7 45 | - 需要循序渐进 46 | 47 | ## ch8 48 | - 银行家算法的实现 49 | - 死锁检测算法的实现 50 | 51 | ## ch9 52 | - 内核允许中断 53 | - 轮询,中断,DMA方式的实际展示 54 | - 各种驱动的比较详细的分析 55 | 56 | ## convert 57 | make epub //build epub book 58 | calibre // convert epub to docx 59 | -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /scripts/fix-comments.py: -------------------------------------------------------------------------------- 1 | # See #91. 2 | # Solution: if selector div.section cannot be found in a page, changing the pattern from div:section to 3 | # div > section can be a reasonable idea after observation. 4 | import os 5 | html_list = [] 6 | def collect_html(path): 7 | for item in os.listdir(path): 8 | new_path = path + '/' + item 9 | if os.path.isdir(new_path): 10 | collect_html(new_path) 11 | else: 12 | _, ext = os.path.splitext(new_path) 13 | if ext == '.html': 14 | html_list.append(new_path) 15 | 16 | collect_html('build/html') 17 | for html_file in html_list: 18 | html_content = "" 19 | with open(html_file, 'r') as f: 20 | html_content_lines = f.readlines() 21 | for line in html_content_lines: 22 | html_content += line 23 | if html_content.find('
`_ 7 | - `Interrupt `_ 8 | - `ISA & Extensions `_ 9 | - `Toolchain `_ 10 | - `Control and Status Registers (CSRs) `_ 11 | - `Accessing CSRs `_ 12 | - `Assembler & Instructions `_ 13 | 14 | ISA 15 | ------------------------ 16 | 17 | - `User-Level ISA, Version 1.12 `_ 18 | - `4 Supervisor-Level ISA, Version 1.12 `_ 19 | - `Vector Extension `_ 20 | - `RISC-V Bitmanip Extension `_ 21 | - `External Debug `_ 22 | - `ISA Resources `_ -------------------------------------------------------------------------------- /source/chapter0/7exercise.rst: -------------------------------------------------------------------------------- 1 | 练习 2 | ===================================================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | 9 | 课后练习 10 | ------------------------------- 11 | 12 | 课后练习和实验练习中的题目难度表示: 13 | 14 | - `*` 容易 15 | - `**` 有一定工作量 16 | - `***` 有难度 17 | 18 | 编程题 19 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 20 | 21 | 1. `*` 在你日常使用的操作系统环境中安装并配置好实验环境。简要说明你碰到的问题/困难和解决方法。 22 | 2. `*` 在Linux环境下编写一个会产生异常的应用程序,并简要解释操作系统的处理结果。 23 | 3. `**` 在Linux环境下编写一个可以睡眠5秒后打印出一个字符串,并把字符串内容存入一个文件中的应用程序A。(基于C或Rust语言) 24 | 4. `***` 在Linux环境下编写一个应用程序B,简要说明此程序能够体现操作系统的并发性、异步性、共享性和持久性。(基于C或Rust语言) 25 | 26 | 注: 在类Linux环境下编写尝试用GDB等调试工具调试应用程序A,能够设置断点,单步执行,显示变量信息。 27 | 28 | 问答题 29 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 30 | 31 | 1. `*` 什么是操作系统?操作系统的主要目标是什么? 32 | 2. `*` 面向服务器的操作系统与面向手机的操作系统在功能上有何异同? 33 | 3. `*` 对于目前的手机或桌面操作系统而言,操作系统是否应该包括网络浏览器?请说明理由。 34 | 4. `*` 操作系统的核心抽象有哪些?它们应对的对象是啥? 35 | 5. `*` 操作系统与应用程序之间通过什么来进行互操作和数据交换? 36 | 6. `*` 操作系统的特征是什么?请结合你日常使用的操作系统的具体运行情况来进一步说明操作系统的特征。 37 | 7. `*` 请说明基于C语言应用的执行环境与基于Java语言应用的执行环境的异同。 38 | 8. `**` 请简要列举操作系统的系统调用的作用,以及简要说明与程序执行、内存分配、文件读写相关的Linux系统调用的大致接口和含义。 39 | 9. `**` 以你编写的可以睡眠5秒后打印出一个字符串的应用程序A为例,说明什么是控制流?什么是异常控制流?什么是进程、地址空间和文件?并简要描述操作系统是如何支持这个应用程序完成其工作并结束的。 40 | 10. `*` 请简要描述支持单个应用的OS、批处理OS、多道程序OS、分时共享OS的特点。 -------------------------------------------------------------------------------- /source/chapter9/4exercise.rst: -------------------------------------------------------------------------------- 1 | 练习 2 | ================================================ 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | 9 | 课后练习 10 | ------------------------------- 11 | 12 | 编程题 13 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 14 | 15 | 1. `***` 在Linux的字符(命令行)模式下,编写贪吃蛇小游戏应用程序。 16 | 17 | 问答题 18 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 19 | 20 | 1. `*` 字符设备的特点是什么? 21 | 2. `*` 块设备的特点是什么? 22 | 3. `*` 网络设备的特点是什么? 23 | 4. `*` 阻塞I/O、非阻塞I/O、多路复用 I/O、信号驱动 I/O、异步I/O这几种I/O方式的特点和区别是? 24 | 5. `*` IO数据传输有哪几种?各自的特征是什么? 25 | 6. `*` 描述磁盘I/O操作时间组成。其中的瓶颈是哪部分? 26 | 7. `**` RISC-V中的异常,中断的区别是啥?有几类中断?每类中断有哪些具体的常见中断实例?PLIC/CLINT的具体功能是啥?中断可否从M态响应委托给S态响应?S态响应可否委托给U态响应?与中断相关的M态/S态寄存器有哪些,这些寄存器的功能是啥?外设产生一个中断后,PLIC/CPU/OS如何协同进行响应处理的? 27 | 8. `**` 是否可以把设备抽象为文件?如果可以,那用户进程对设备发出IO控制命令,如何通过系统调用实现? 28 | 9. `**` GPU是外设吗?GPU与CPU交互和数据传输的方式是什么?(需要查看一下相关GPU工作过程的信息) 29 | 30 | 31 | 实验练习 32 | ------------------------------- 33 | 34 | 实验练习包括实践作业和问答作业两部分。本次难度: **中** 35 | 36 | 实践作业 37 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 38 | 39 | 支持图形显示的应用 40 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 41 | 42 | 本章虽然讲述了virtio-gpu设备驱动,且可以直接进行图形显示,但这个设备驱动并没有加入到操作系统中,使得应用程序无法进行图形显示。lab8 的练习要求操作系统支持有彩色图形显示的应用,使得我们可以从单调的字符交互界跳入到多彩的图形界面中。 43 | 44 | 45 | 实验要求 46 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 47 | - 实现分支:ch9-lab 48 | - 实验目录要求不变 49 | - 在裸机上让操作系统支持“贪吃蛇”游戏应用 50 | 51 | 需要在操作系统中加入virtio-gpu设备驱动程序;需要实现设备文件 ``/dev/fb0`` 和相关操作,用于应用访问显存。 52 | 53 | 可以正确执行“贪吃蛇”游戏应用。 54 | 55 | 56 | 问答作业 57 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 58 | 59 | 60 | 1. 通过阅读和运行试验等分析,你认为在目前的操作系统中,如果运行在用户态,可以响应哪些中断?如果运行在内核态,可以响应哪些中断?请简要描述分析经过。 61 | 62 | 2. 对于串口驱动程序,在RustSBI中有具体的实现,请问它与本章讲的串口驱动有何异同之处? 63 | 64 | 3. 对于目前操作系统中的 ``virtio-blk`` 设备驱动程序,存在哪些可以改进的地方来提升性能? 65 | 66 | 实验练习的提交报告要求 67 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 68 | 69 | 70 | - 简单总结本次实验你编程的内容。(控制在5行以内,不要贴代码) 71 | - 完成问答问题。 72 | - (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 73 | -------------------------------------------------------------------------------- /source/chapter6/5answer.rst: -------------------------------------------------------------------------------- 1 | 练习参考答案 2 | ===================================================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | 课后练习 9 | ------------------------------- 10 | 11 | 问答题 12 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 13 | 14 | 1. `*` 文件系统的功能是什么? 15 | 16 | 将数据以文件的形式持久化保存在存储设备上。 17 | 18 | 2. `**` 目前的文件系统只有单级目录,假设想要支持多级文件目录,请描述你设想的实现方式,描述合理即可。 19 | 20 | 允许在目录项中存在目录(原本只能存在普通文件)即可。 21 | 22 | 3. `**` 软链接和硬链接是干什么的?有什么区别?当删除一个软链接或硬链接时分别会发生什么? 23 | 24 | 软硬链接的作用都是给一个文件以"别名",使得不同的多个路径可以指向同一个文件。当删除软链接时候,对文件没有任何影响,当删除硬链接时,文件的引用计数会被减一,若引用计数为0,则该文件所占据的磁盘空间将会被回收。 25 | 26 | 4. `***` 在有了多级目录之后,我们就也可以为一个目录增加硬链接了。在这种情况下,文件树中是否可能出现环路(软硬链接都可以,鼓励多尝试)?你认为应该如何解决?请在你喜欢的系统上实现一个环路,描述你的实现方式以及系统提示、实际测试结果。 27 | 28 | 是可以出现环路的,一种可能的解决方式是在访问文件的时候检查自己遍历的路径中是否有重复的inode,并在发现环路时返回错误。 29 | 30 | 5. `*` 目录是一类特殊的文件,存放的是什么内容?用户可以自己修改目录内容吗? 31 | 32 | 存放的是目录中的文件列表以及他们对应的inode,通常而言用户不能自己修改目录的内容,但是可以通过操作目录(如mv里面的文件)的方式间接修改。 33 | 34 | 6. `**` 在实际操作系统中,如Linux,为什么会存在大量的文件系统类型? 35 | 36 | 因为不同的文件系统有着不同的特性,比如对于特定种类的存储设备的优化,或是快照和多设备管理等高级特性,适用于不同的使用场景。 37 | 38 | 7. `**` 可以把文件控制块放到目录项中吗?这样做有什么优缺点? 39 | 40 | 可以,是对于小目录可以减少一次磁盘访问,提升性能,但是对大目录而言会使得在目录中查找文件的性能降低。 41 | 42 | 8. `**` 为什么要同时维护进程的打开文件表和操作系统的打开文件表?这两个打开文件表有什么区别和联系? 43 | 44 | 多个进程可能会同时打开同一个文件,操作系统级的打开文件表可以加快后续的打开操作,但同时由于每个进程打开文件时使用的访问模式或是偏移量不同,所以还需要进程的打开文件表另外记录。 45 | 46 | 9. `**` 文件分配的三种方式是如何组织文件数据块的?各有什么特征(存储、文件读写、可靠性)? 47 | 48 | 连续分配:实现简单、存取速度快,但是难以动态增加文件大小,长期使用后会产生大量无法使用(过小而无法放入大文件)碎片空间。 49 | 50 | 链接分配:可以处理文件大小的动态增长,也不会出现碎片,但是只能按顺序访问文件中的块,同时一旦有一个块损坏,后面的其他块也无法读取,可靠性差。 51 | 52 | 索引分配:可以随机访问文件中的偏移量,但是对于大文件需要实现多级索引,实现较为复杂。 53 | 54 | 10. `**` 如果一个程序打开了一个文件,写入了一些数据,但是没有及时关闭,可能会有什么后果?如果打开文件后,又进一步发出了读文件的系统调用,操作系统中各个组件是如何相互协作完成整个读文件的系统调用的? 55 | 56 | (若也没有flush的话)假如此时操作系统崩溃,尚处于内存缓冲区中未写入磁盘的数据将会丢失,同时也会占用文件描述符,造成资源的浪费。首先是系统调用处理的部分,将这一请求转发给文件系统子系统,文件系统子系统再将其转发给块设备子系统,最后再由块设备子系统转发给实际的磁盘驱动程序读取数据,最终返回给程序。 57 | 58 | 11. `***` 文件系统是一个操作系统必要的组件吗?是否可以将文件系统放到用户态?这样做有什么好处?操作系统需要提供哪些基本支持? 59 | 60 | 不是,如在本章之前的rCore就没有文件系统。可以,如在Linux下就有FUSE这样的框架可以实现这一点。这样可以使得文件系统的实现更为灵活,开发与调试更为简便。操作系统需要提供一个注册用户态文件系统实现的机制,以及将收到的文件系统相关系统调用转发给注册的用户态进程的支持。 61 | 62 | -------------------------------------------------------------------------------- /source/rest-example.rst: -------------------------------------------------------------------------------- 1 | reStructuredText 基本语法 2 | ===================================================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | .. note:: 9 | 下面是一个注记。 10 | 11 | `这里 `_ 给出了在 Sphinx 中外部链接的引入方法。注意,链接的名字和用一对尖括号包裹起来的链接地址之间必须有一个空格。链接最后的下划线和片段的后续内容之间也需要有一个空格。 12 | 13 | 接下来是一个文档内部引用的例子。比如,戳 :doc:`chapter0/5setup-devel-env` 可以进入快速上手环节。 14 | 15 | 文档间互引用:比如在关于进程的退出部分: 16 | 17 | .. code-block:: 18 | 19 | 可以使用 .. _process-exit: 记录文档的一个位置。 20 | 然后在文档中使用 :ref:`链接名 ` 创建指向上述位置的一个链接。 21 | 22 | .. warning:: 23 | 24 | 下面是一个警告。 25 | 26 | .. code-block:: rust 27 | :linenos: 28 | :caption: 一段示例 Rust 代码 29 | 30 | // 我们甚至可以插入一段 Rust 代码! 31 | fn add(a: i32, b: i32) -> i32 { a + b } 32 | 33 | 下面继续我们的警告。 34 | 35 | .. attention:: Here is an attention. 36 | 37 | .. caution:: please be cautious! 38 | 39 | .. error:: 40 | 41 | 下面是一个错误。 42 | 43 | .. danger:: it is dangerous! 44 | 45 | 46 | .. tip:: here is a tip 47 | 48 | .. important:: this is important! 49 | 50 | .. hint:: this is a hint. 51 | 52 | 53 | 54 | 这里是一行数学公式 :math:`\sin(\alpha+\beta)=\sin\alpha\cos\beta+\cos\alpha\sin\beta`。 55 | 56 | 基本的文本样式:这是 *斜体* ,这是 **加粗** ,接下来的则是行间公式 ``a0`` 。它们的前后都需要有一个空格隔开其他内容,这个让人挺不爽的... 57 | 58 | `这是 `_ 一个全面展示章节分布的例子,来自于 ReadTheDocs 的官方文档。事实上,现在我们也采用 ReadTheDocs 主题了,它非常美观大方。 59 | 60 | 下面是一个测试的截图。 61 | 62 | .. image:: resources/test.png 63 | 64 | 接下来是一个表格的例子。 65 | 66 | .. list-table:: RISC-V 函数调用跳转指令 67 | :widths: 20 30 68 | :header-rows: 1 69 | :align: center 70 | 71 | * - 指令 72 | - 指令功能 73 | * - :math:`\text{jal}\ \text{rd},\ \text{imm}[20:1]` 74 | - :math:`\text{rd}\leftarrow\text{pc}+4` 75 | 76 | :math:`\text{pc}\leftarrow\text{pc}+\text{imm}` 77 | * - :math:`\text{jalr}\ \text{rd},\ (\text{imm}[11:0])\text{rs}` 78 | - :math:`\text{rd}\leftarrow\text{pc}+4` 79 | 80 | :math:`\text{pc}\leftarrow\text{rs}+\text{imm}` -------------------------------------------------------------------------------- /source/appendix-e/index.rst: -------------------------------------------------------------------------------- 1 | 附录 E:操作系统进一步介绍 2 | ===================================================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | 9 | openEuler操作系统 10 | ------------------------------------ 11 | 12 | 国产操作系统始于九十年代,大多数基于Fedora/CentOS/Debian/Ubuntu进行二次开发,直到2019年,华为公司开源了openEuler操作系统, openEuler是自主演进的根操作系统,不基于其他任何OS二次开发,这是与其他国产OS的主要差异。 13 | 14 | openEuler是一个开源、免费的Linux发行版平台,其致力于打造中国原生开源、可自主演进操作系统根社区。openEuler前身是华为公司发展近10年的服务器操作系统EulerOS,2019年开源,更名为openEuler,当前有多个国产OSV厂商基于openEuler发布商用版本。openEuler最新定位是面向数字基础设施的开源操作系统,支持多样性计算,支持服务器、云计算、边缘计算、嵌入式等应用场景,支持OT(Operational Technology)领域应用及OT与ICT的融合。 15 | 16 | openEuler在具有通用的Linux系统架构,包括内存管理子系统、进程管理子系统、进程调度子系统、进程间通讯(IPC)、文件系统、网络子系统、设备管理子系统和虚拟化与容器子系统等。同时,openEuler又不同于其他通用操作系统,openEuler从OS内核、可靠性、安全性和生态使能等方面做了特性增强。openEuler的关键特性如下表所示: 17 | 18 | ========== ================================================== ================================================================================= 19 | 20 | **名称** **特性说明** **网页链接** 21 | StratoVirt 轻量级虚拟机引擎 https://docs.openeuler.org/zh/docs/21.03/docs/StratoVirt/StratoVirtGuide.html 22 | iSula 轻量级容器引擎 https://docs.openeuler.org/zh/docs/21.03/docs/Container/iSula容器引擎.html 23 | A-Tune AI智能调优引擎 https://docs.openeuler.org/zh/docs/21.03/docs/A-Tune/A-Tune.html 24 | secGear 跨平台机密计算框架使 https://docs.openeuler.org/zh/docs/21.03/docs/secGear/secGear.html 25 | 可信计算 安全可信计算 https://docs.openeuler.org/zh/docs/21.03/docs/Administration/可信计算.html 26 | KAE 鲲鹏加速引擎(Kunpeng Accelerator Engine) https://docs.openeuler.org/zh/docs/21.03/docs/Administration/使用KAE加速引擎.html 27 | MPAM Memory System Resource Partitioning and Monitoring https://mp.weixin.qq.com/s/0TgrFjFtobmk-h1HwJskqg 28 | 毕昇 JDK Huawei开源JDK https://gitee.com/openeuler/bishengjdk-8/wikis/Home 29 | ========== ================================================== ================================================================================= -------------------------------------------------------------------------------- /source/appendix-a/index.rst: -------------------------------------------------------------------------------- 1 | 附录 A:Rust 系统编程入门 2 | ============================= 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | 9 | .. .. note:: 10 | 11 | .. **Rust 语法卡片:外部符号引用** 12 | 13 | .. extern "C" 可以引用一个外部的 C 函数接口(这意味着调用它的时候要遵从目标平台的 C 语言调用规范)。但我们这里只是引用位置标志 14 | .. 并将其转成 usize 获取它的地址。由此可以知道 ``.bss`` 段两端的地址。 15 | 16 | .. **Rust 语法卡片:迭代器与闭包** 17 | 18 | .. 代码第 7 行用到了 Rust 的迭代器与闭包的语法,它们在很多情况下能够提高开发效率。如同学感兴趣的话也可以将其改写为等价的 for 19 | .. 循环实现。 20 | 21 | .. .. _term-raw-pointer: 22 | .. .. _term-dereference: 23 | .. .. warning:: 24 | 25 | .. **Rust 语法卡片:Unsafe** 26 | 27 | .. 代码第 8 行,我们将 ``.bss`` 段内的一个地址转化为一个 **裸指针** (Raw Pointer),并将它指向的值修改为 0。这在 C 语言中是 28 | .. 一种司空见惯的操作,但在 Rust 中我们需要将他包裹在 unsafe 块中。这是因为,Rust 认为对于裸指针的 **解引用** (Dereference) 29 | .. 是一种 unsafe 行为。 30 | 31 | .. 相比 C 语言,Rust 进行了更多的语义约束来保证安全性(内存安全/类型安全/并发安全),这在编译期和运行期都有所体现。但在某些时候, 32 | .. 尤其是与底层硬件打交道的时候,在 Rust 的语义约束之内没法满足我们的需求,这个时候我们就需要将超出了 Rust 语义约束的行为包裹 33 | .. 在 unsafe 块中,告知编译器不需要对它进行完整的约束检查,而是由程序员自己负责保证它的安全性。当代码不能正常运行的时候,我们往往也是 34 | .. 最先去检查 unsafe 块中的代码,因为它没有受到编译器的保护,出错的概率更大。 35 | 36 | .. C 语言中的指针相当于 Rust 中的裸指针,它无所不能但又太过于灵活,程序员对其不谨慎的使用常常会引起很多内存不安全问题,最常见的如 37 | .. 悬垂指针和多次回收的问题,Rust 编译器没法确认程序员对它的使用是否安全,因此将其划到 unsafe Rust 的领域。在 safe Rust 中,我们 38 | .. 有引用 ``&/&mut`` 以及各种功能各异的智能指针 ``Box/RefCell/Rc`` 可以使用,只要按照 Rust 的规则来使用它们便可借助 39 | .. 编译器在编译期就解决很多潜在的内存不安全问题。 40 | 41 | Rust编程相关 42 | -------------------------------- 43 | 44 | - `OS Tutorial Summer of Code 2020:Rust系统编程入门指导 `_ 45 | - `Stanford 新开的一门很值得学习的 Rust 入门课程 `_ 46 | - `一份简单的 Rust 入门介绍 `_ 47 | - `《RustOS Guide》中的 Rust 介绍部分 `_ 48 | - `一份简单的Rust宏编程新手指南 `_ 49 | 50 | 51 | Rust系统编程pattern 52 | --------------------------------- 53 | 54 | - `Arc> in Rust `_ 55 | - `Understanding Closures in Rust `_ 56 | - `Closures in Rust `_ 57 | -------------------------------------------------------------------------------- /source/chapter3/6answer.rst: -------------------------------------------------------------------------------- 1 | 练习参考答案 2 | ======================================= 3 | 4 | 5 | 课后练习 6 | ------------------------------- 7 | 8 | 编程题 9 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 10 | 11 | 12 | 1. `*` 扩展内核,能够显示操作系统切换任务的过程。 13 | 2. `**` 扩展内核,能够统计每个应用执行后的完成时间:用户态完成时间和内核态完成时间。 14 | 3. `**` 编写浮点应用程序A,并扩展内核,支持面向浮点应用的正常切换与抢占。 15 | 4. `**` 编写应用程序或扩展内核,能够统计任务切换的大致开销。 16 | 5. `***` 扩展内核,支持在内核态响应中断。 17 | 6. `***` 扩展内核,支持在内核运行的任务(简称内核任务),并支持内核任务的抢占式切换。 18 | 19 | 注:上述扩展内核的编程基于 rcore/ucore tutorial v3: Branch ch3 20 | 21 | 问答题 22 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 23 | 24 | 1. `*` 协作式调度与抢占式调度的区别是什么? 25 | 26 | 协作式调度中,进程主动放弃 (yield) 执行资源,暂停运行,将占用的资源让给其它进程;抢占式调度中,进程会被强制打断暂停,释放资源让给别的进程。 27 | 28 | 2. `*` 中断、异常和系统调用有何异同之处? 29 | 30 | * 相同点 31 | 32 | * 都会从通常的控制流中跳出,进入 trap handler 进行处理。 33 | 34 | * 不同点 35 | 36 | * 中断的来源是异步的外部事件,由外设、时钟、别的 hart 等外部来源,与 CPU 正在做什么没关系。 37 | * 异常是 CPU 正在执行的指令遇到问题无法正常进行而产生的。 38 | * 系统调用是程序有意想让操作系统帮忙执行一些操作,用专门的指令(如 ``ecall`` )触发的。 39 | 40 | 3. `*` RISC-V支持哪些中断/异常? 41 | 4. `*` 如何判断进入操作系统内核的起因是由于中断还是异常? 42 | 5. `**` 在 RISC-V 中断机制中,PLIC 和 CLINT 各起到了什么作用? 43 | 44 | CLINT 处理时钟中断 (``MTI``) 和核间的软件中断 (``MSI``);PLIC 处理外部来源的中断 (``MEI``)。 45 | 46 | PLIC 的规范文档: https://github.com/riscv/riscv-plic-spec 47 | 48 | .. TODO SiFive CLINT 的文档在哪儿? 49 | 50 | 6. `**` 基于RISC-V 的操作系统支持中断嵌套?请给出进一步的解释说明。 51 | 7. `**` 本章提出的任务的概念与前面提到的进程的概念之间有何区别与联系? 52 | 8. `*` 简单描述一下任务的地址空间中有哪些类型的数据和代码。 53 | 9. `*` 任务控制块保存哪些内容? 54 | 10. `*` 任务上下文切换需要保存与恢复哪些内容? 55 | 56 | 需要保存通用寄存器的值,PC;恢复的时候除了保存的内容以外还要恢复特权级到用户态。 57 | 58 | 11. `*` 特权级上下文和任务上下文有何异同? 59 | 12. `*` 上下文切换为什么需要用汇编语言实现? 60 | 61 | 上下文切换过程中,需要我们直接控制所有的寄存器。C 和 Rust 编译器在编译代码的时候都会“自作主张”使用通用寄存器,以及我们不知道的情况下访问栈,这是我们需要避免的。 62 | 63 | 切换到内核的时候,保存好用户态状态之后,我们将栈指针指向内核栈,相当于构建好一个高级语言可以正常运行的环境,这时候就可以由高级语言接管了。 64 | 65 | 13. `*` 有哪些可能的时机导致任务切换? 66 | 67 | 系统调用(包括进程结束执行)、时钟中断。 68 | 69 | 14. `**` 在设计任务控制块时,为何采用分离的内核栈和用户栈,而不用一个栈? 70 | 71 | 用户程序可以任意修改栈指针,将其指向任意位置,而内核在运行的时候总希望在某一个合法的栈上,所以需要用分开的两个栈。 72 | 73 | 此外,利用后面的章节的知识可以保护内核和用户栈,让用户无法读写内核栈上的内容,保证安全。 74 | 75 | 15. `***` (以下答案以 Linux 5.17 为准) 76 | 77 | 1. ``arch/riscv/kernel/entry.S`` 里的 ``handle_exception`` ; ``arch/riscv/kernel/head.S`` 里的 ``setup_trap_vector`` 78 | 2. ``arch/riscv/kernel/entry.S`` 里的 ``__switch_to`` 79 | 3. ``TrapContext`` 对应 ``pt_regs`` ; ``TaskContext`` 对应 ``task_struct`` (在 ``task_struct`` 中也包含一些其它的和调度相关的信息) 80 | 4. ``tp`` 指向当前被打断的任务的 ``task_struct`` (参见 ``arch/riscv/include/asm/current.h`` 里的宏 ``current`` ); ``sscratch`` 是 ``0`` 81 | 5. ``sscratch`` 指向当前正在运行的任务的 ``task_struct`` ,这样设计可以用来区分异常来自用户态还是内核态。 82 | 6. 所有通用寄存器, ``sstatus``, ``sepc``, ``scause`` 83 | 7. 内核栈底; ``arch/riscv/include/asm/processor.h`` 里的 ``task_pt_regs`` 宏 84 | 8. ``arch/riscv/kernel/syscall_table.c`` 里的 ``sys_call_table`` 作为跳转表,根据系统调用编号调用。 85 | 9. 从保存的 ``pt_regs`` 中读保存的 ``a0`` 到 ``a7`` 到机器寄存器里,这样系统调用实现的 C 函数就会作为参数接收到这些值,返回值是将返回的 ``a0`` 写入保存的 ``pt_regs`` ,然后切换回用户态的代码负责将其“恢复”到 ``a0`` 86 | -------------------------------------------------------------------------------- /source/index.rst: -------------------------------------------------------------------------------- 1 | .. rCore-Tutorial-Book-v3 documentation master file, created by 2 | sphinx-quickstart on Thu Oct 29 22:25:54 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | rCore-Tutorial-Book 第三版 7 | ================================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Part1 - Just do it! 12 | :hidden: 13 | 14 | chapter0/index 15 | chapter1/index 16 | chapter2/index 17 | chapter3/index 18 | chapter4/index 19 | chapter5/index 20 | chapter6/index 21 | chapter7/index 22 | chapter8/index 23 | chapter9/index 24 | 25 | .. toctree:: 26 | :maxdepth: 2 27 | :caption: Part2 - Do it better! 28 | :hidden: 29 | 30 | .. toctree:: 31 | :maxdepth: 2 32 | :caption: 附录 33 | :hidden: 34 | 35 | final-lab 36 | appendix-a/index 37 | appendix-b/index 38 | appendix-c/index 39 | appendix-d/index 40 | appendix-e/index 41 | terminology 42 | 43 | .. toctree:: 44 | :maxdepth: 2 45 | :caption: 开发注记 46 | :hidden: 47 | 48 | setup-sphinx 49 | rest-example 50 | log 51 | 52 | 欢迎来到 rCore-Tutorial-Book 第三版! 53 | 54 | 欢迎参加 `2022年开源操作系统训练营! `_ 55 | 56 | .. note:: 57 | 58 | :doc:`/log` 59 | 60 | 61 | 62 | 项目简介 63 | --------------------- 64 | 65 | 这本教程旨在一步一步展示如何 **从零开始** 用 **Rust** 语言写一个基于 **RISC-V** 架构的 **类 Unix 内核** 。值得注意的是,本项目不仅支持模拟器环境(如 Qemu/terminus 等),还支持在真实硬件平台 Kendryte K210 上运行。 66 | 67 | 68 | 导读 69 | --------------------- 70 | 71 | 请大家先阅读 :ref:`第零章 ` ,对于项目的开发背景和操作系统的概念有一个整体把控。 72 | 73 | 在正式进行实验之前,请先按照第零章章末的 :doc:`/chapter0/5setup-devel-env` 中的说明完成环境配置,再从第一章开始阅读正文。 74 | 75 | .. chyyuu 如果已经对 RISC-V 架构、Rust 语言和内核的知识有较多了解,第零章章末的 :doc:`/chapter0/6hardware` 提供了我们采用的真实硬件平台 Kendryte K210 的一些信息。 76 | 77 | 项目协作 78 | ---------------------- 79 | 80 | - :doc:`/setup-sphinx` 介绍了如何基于 Sphinx 框架配置文档开发环境,之后可以本地构建并渲染 html 或其他格式的文档; 81 | - :doc:`/rest-example` 给出了目前编写文档才用的 ReStructuredText 标记语言的一些基础语法及用例; 82 | - `项目的源代码仓库 `_ && `文档仓库 `_ 83 | - 时间仓促,本项目还有很多不完善之处,欢迎大家积极在每一个章节的评论区留言,或者提交 Issues 或 Pull Requests,让我们一起努力让这本书变得更好! 84 | - 欢迎大家加入项目交流 QQ 群,群号:735045051 85 | 86 | 项目进度 87 | ----------------------- 88 | 89 | - 2020-11-03:环境搭建完成,开始着手编写文档。 90 | - 2020-11-13:第一章完成。 91 | - 2020-11-27:第二章完成。 92 | - 2020-12-20:前七章代码完成。 93 | - 2021-01-10:第三章完成。 94 | - 2021-01-18:加入第零章。 95 | - 2021-01-30:第四章完成。 96 | - 2021-02-16:第五章完成。 97 | - 2021-02-20:第六章完成。 98 | - 2021-03-06:第七章完成。到这里为止第一版初稿就已经完成了。 99 | - 2021-10-20:第八章代码于前段时间完成。开始更新前面章节文档及完成第八章文档。 100 | - 2021-11-20:更新1~9章,添加第八章(同步互斥),原第八章(外设)改为第九章。 101 | - 2022-01-02:第一章文档更新完成。 102 | - 2022-01-05:第二章文档更新完成。 103 | - 2022-01-06:第三章文档更新完成。 104 | - 2022-01-07:第四章文档更新完成。 105 | - 2022-01-09:第五章文档更新完成。 -------------------------------------------------------------------------------- /source/chapter0/6hardware.rst.org: -------------------------------------------------------------------------------- 1 | .. chyyuu 可以把k210相关的内容放到某个附录中 2 | 3 | K210 开发板相关问题 4 | ===================================================== 5 | 6 | rCore Tutorial v3 是基于 RISC-V 特权级架构 1.10 版本来开发的。我们采用的真实硬件平台 Kendryte K210 基于 RISC-V 特权级架构 1.9.1 版本(2016 年),目前已经不被当前主流编译工具链所支持了。麻烦的是,RISC-V 特权级架构 1.9.1 版本和1.10版本确实有很多不同。为此,RustSBI 做了很多兼容性工作,使得基于RISC-V 特权级架构 1.10的系统软件几乎可以被不加修改的运行在 Kendryte K210 上。在这里我们先简单介绍一些开发板相关的问题。 7 | 8 | K210 相关 Demo 和文档 9 | -------------------------------------------- 10 | 11 | - `K210 datasheet `_ 12 | - `K210 官方 SDK `_ 13 | - `K210 官方 SDK 文档 `_ 14 | - `K210 官方 SDK Demo `_ 15 | - `K210 Demo in Rust `_ 16 | 17 | K210 相关工具 18 | -------------------------------------------- 19 | 20 | JTAG 调试 21 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 22 | 23 | - `一篇 Blog `_ 24 | - `Sipeed 工程师提供的详细配置文档 `_ 25 | - `MaixDock OpenOCD 调试配置 `_ 26 | 27 | 烧写 28 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 29 | 30 | - `kflash.py `_ 31 | - `kflash_gui `_ 32 | 33 | 34 | K210 可用内存大小 35 | -------------------------------------------- 36 | 37 | K210 的内存是由 CPU 和 KPU 共享使用的,如果想要 CPU 能够使用全部的 :math:`8\text{MiB}` 需要满足三个条件: 38 | 39 | - KPU 不能处于工作状态; 40 | - PLL1 必须被使能; 41 | - PLL1 的 clock gate 必须处于打开状态。 42 | 43 | 否则, CPU 仅能够使用 :math:`6\text{MiB}` 内存。 44 | 45 | 我们进行如下操作即可让 CPU 使用全部 :math:`8\text{MiB}` 内存(基于官方 SDK): 46 | 47 | .. code-block:: c 48 | 49 | sysctl_pll_enable(SYSCTL_PLL1); 50 | sysctl_clock_enable(SYSCTL_CLOCK_PLL1); 51 | 52 | K210 的频率 53 | -------------------------------------------- 54 | 默认情况下,K210 的 CPU 频率为 403000000 ,约 :math:`400\text{MHz}` 。而计数器 ``mtime`` CSR 增长的频率为 CPU 频率的 1/62 ,约 :math:`6.5\text{MHz}` 。 55 | 56 | 57 | K210 的 MMU 支持 58 | -------------------------------------------- 59 | 60 | K210 有完善的 SV39 多级页表机制,然而它是基于 1.9.1 版本特权级架构的,和我们目前使用的有一些不同。不过在 RustSBI 的帮助下,本项目中完全看不出 Qemu-5.0.0(特权级架构 1.10)和 K210( 特权级架构 1.9.1) 两个平台在这方面的区别。详情请参考 `RustSBI 的设计与实现 `_ 的 P11 页的内容。 61 | 62 | K210 的外部中断支持 63 | -------------------------------------------- 64 | 65 | K210 的 S 特权级外部中断不存在(被硬件置为零),因此任何软件/硬件代理均无法工作。为此,RustSBI 专门提供了一个新的 SBI call ,让 S 模式软件可以编写 S 特权级外部中断的 handler 并注册到 RustSBI 中,在中断触发的时候由 RustSBI 调用该 handler 处理中断。详情请参考 `RustSBI 的设计与实现 `_ 的 P12 页的内容。 -------------------------------------------------------------------------------- /source/chapter0/4os-features.rst: -------------------------------------------------------------------------------- 1 | 操作系统的特征 2 | ================================================ 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 5 7 | 8 | 9 | 基于操作系统的四个抽象,我们可以看出,从总体上看,操作系统具有五个方面的特征:虚拟化 (Virtualization)、并发性 (Concurrency)、异步性、共享性和持久性 (Persistency)。操作系统的虚拟化可以理解为它对内存、CPU 的抽象和处理;并发性和共享性可以理解为操作系统支持多个应用程序“同时”运行;异步性可以从操作系统调度、中断处理对应用程序执行造成的影响等几个方面来理解;持久性则可以从操作系统中的文件系统支持把数据方便地从磁盘等存储介质上存入和取出来理解。 10 | 11 | 虚拟性 12 | ---------------------------------- 13 | 14 | 15 | 内存虚拟化 16 | ~~~~~~~~~~~~~~ 17 | 18 | 首先来看看内存虚拟化。程序员在写应用程序的时候,不用考虑其程序的起始内存地址要放到计算机内存的具体某个位置,而是用字符串符号定义了各种变量和函数,直接在代码中便捷地使用这些符号就行了。这是由于操作系统建立了一个 *地址固定* , *空间巨大* 的虚拟内存给应用程序来运行,这是 **内存虚拟化** 。内存虚拟化的核心问题是:采用什么样的方式让虚拟地址和物理地址对应起来,也就是如何将虚拟地址“翻译”成物理地址。 19 | 20 | **内存虚拟化** 其实是一种 **“空间虚拟化”** , 可进一步细分为 **内存地址虚拟化** 和 **内存大小虚拟化** 。这里的每个符号在运行时是要对应到具体的内存地址的。这些内存地址的具体数值是什么?程序员不用关心。为什么?因为编译器会自动帮我们把这些符号翻译成地址,形成可执行程序。程序使用的内存是否占得太大了?在一般情况下,程序员也不用关心。 21 | 22 | .. note:: 23 | 24 | 还记得虚拟地址(逻辑地址)的描述吗? 25 | 26 | 实际上,编译器 (Compiler,比如 gcc) 和链接器 (linker,比如 ld) 也不知道程序每个符号对应的地址应该放在未来程序运行时的哪个物理内存地址中。所以,编译器的一个简单处理办法就是,设定一个固定地址(比如 0x10000)作为起始地址开始存放代码,代码之后是数据,所有变量和函数的符号都在这个起始地址之后的某个固定偏移位置。假定程序每次运行都是位于一个不会变化的起始地址。这里的变量指的是全局变量,其地址在编译链接后会确定不变。但局部变量是放在堆栈中的,会随着堆栈大小的动态变化而变化。这里编译器产生的地址就是虚拟地址。 27 | 28 | 这里,编译器和链接器图省事,找了一个适合它们的解决办法。当程序要运行的时候,这个符号所对应的虚拟内存地址到计算机的物理内存地址的映射必须要解决了,这自然就推到了操作系统身上。操作系统会把编译器和链接器生成的执行代码和数据放到空闲的物理内存中,并建立虚拟地址到物理地址的映射关系。由于物理内存中的空闲区域是动态变化的,这导致虚拟地址到物理地址的映射关系也是动态变化的,需要操作系统来维护好可变的映射关系,确保编译器“固定起始地址”的假设成立。只有操作系统维护好了这个映射关系,才能让程序员只需写一些易于人理解的字符串符号来代表一个内存空间地址。这样,编译器只需确定一个固定地址作为程序的起始地址,就可以不用考虑将来这个程序要在哪个物理地址空间运行的问题,从而实现了 **内存地址虚拟化** 。 29 | 30 | 应用程序在运行时不用考虑当前物理内存是否够用。如果应用程序需要一定空间的内存,但由于在某些情况下,物理内存的空闲空间可能不多了,这时操作系统通过把物理内存中最近没使用的空间(不是空闲的,只是最近用得少)换出(就是“挪地”)到硬盘上暂时缓存起来,这样空闲空间就大了,就可以满足应用程序的运行时内存需求了,从而实现了 **内存大小虚拟化** 。 31 | 32 | CPU 虚拟化 33 | ~~~~~~~~~~~~~~ 34 | 35 | 再来看 CPU 虚拟化。不同的应用程序可以在内存中并发运行,相同的应用程序也可有多个拷贝在内存中并发运行。而每个程序都“认为”自己完全独占了 CPU 在运行,这是 **“CPU虚拟化”**,也是一种 **“时间虚拟化”** 。操作系统给了运行的应用程序一个幻象,即操作系统把时间分成小段,每个应用程序占用其中一小段时间片运行,用完这一时间片后,操作系统会切换到另外一个应用程序,让它运行。由于时间片很短,操作系统的切换开销也很小,应用程序或使用应用程序的用户基本上是看不出的,反而感觉到多个程序各自在独立“并行”执行,从而实现了 **CPU虚拟化** 。 36 | 37 | 38 | 并发性 39 | ---------------------------------- 40 | 41 | 操作系统为了能够让 CPU 充分地忙起来,并充分利用各种资源,就需要有多种不同的应用程序在执行。这些应用程序是分时执行的,并由操作系统来完成各个应用在运行时的任务切换。并发性虽然能有效改善系统资源的利用率,但也带来了对共享资源的争夺问题,即同步互斥问题。还会带来执行时间的不确定性问题,即并发程序在执行中是走走停停,断续推进的,使得应用程序的完成时间是不确定的。并发性对操作系统的设计也带来了很多挑战,一不小心就会出现程序执行结果不确定,程序死锁等很难调试和重现的问题。 42 | 43 | .. _term-parallel-concurrency: 44 | 45 | .. note:: 46 | * 并行 (Parallel) 是指两个或者多个事件在同一时刻发生; 47 | * 并发 (Concurrent) 是指两个或多个事件在同一时间间隔内发生。 48 | 49 | 对于基于单 CPU 的计算机而言,各个“同时”运行的程序其实是串行分时复用一个 CPU ,任一个时刻点上只有一个程序在 CPU 上运行。 50 | 这些虚拟性的特征给应用程序的开发和执行提供了非常方便的执行环境,但也给操作系统的设计与实现提出了很多挑战。 51 | 52 | 异步性 53 | ---------------------------------- 54 | 55 | 在这里,异步是指由于操作系统的调度和中断等,会不时地暂停或打断当前正在运行的程序,使得程序的整个运行过程走走停停。在应用程序运行的表现上,特别体现在它的执行完成时间是不可预测的。但需要注意,只要应用程序的输入是一致的,那么它的输出结果应该是符合预期的。 56 | 57 | 共享性 58 | ---------------------------------- 59 | 60 | 共享是指多个应用并发运行时,宏观上体现出它们可同时访问同一个资源,即这个资源可被共享。但其实在微观上,操作系统在硬件等的支持下要确保应用程序互斥访问这个共享的资源。比如,在单核处理器下,对于两个应用同时访问同一个内存单元的情况,从宏观的应用层面上看,二者都能正确地读出同一个内存单元的内容;而在微观上,操作系统会调度应用程序的先后执行顺序,确保在任何一个时刻,只有一个应用去访问存储单元。在多核处理器下,多个 CPU 核可能同时访问同一内存单元,在这种多核场景下的共享性不仅仅由 OS 来保证,还需硬件级的 Cache 一致性保证。 61 | 62 | 持久性 63 | ---------------------------------- 64 | 65 | 操作系统提供了文件系统来从可持久保存的存储介质(磁盘, SSD 等,以后以硬盘来代表)中取数据和代码到内存中,并可以把内存中的数据写回到硬盘上。硬盘在这里是外设,具有持久性,以文件系统的形式呈现给应用程序。 66 | 67 | .. note:: 68 | 69 | 文件系统也可看成是操作系统对存储外设(如硬盘、SSD 等)的虚拟化。 70 | 这种持久性的特征进一步带来了共享属性,即在文件系统中的文件可以被多个运行的程序所访问,从而给应用程序之间实现数据共享提供了方便。即使掉电,存储外设上的数据还不会丢失,可以在下一次机器加电后提供给运行的程序使用。持久性对操作系统的执行效率提出了挑战,如何让数据在高速的内存和慢速的硬盘间高效流动是需要操作系统考虑的问题。 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /source/log.rst: -------------------------------------------------------------------------------- 1 | 更新日志 2 | =============================== 3 | 4 | 2022-12-14 5 | ------------------------------- 6 | 7 | - 移除各分支上的 K210 开发板支持,仅保留 ``k210`` 分支作为原先 ``ch8`` 分支的镜像支持 K210 。 8 | 9 | 2022-10-08 10 | ------------------------------- 11 | 12 | - 将各分支的 rustsbi-qemu 的版本更新到 701e891,支持 QEMU 7.1.0 版本。 13 | 14 | 2022-10-02 15 | ------------------------------- 16 | 17 | - 更新 VMware虚拟机镜像至 Ubuntu22.04 ,内置 QEMU7.0.0 版本。同时,还更新了各章节的 Docker 相关文件。 18 | 19 | 2022-01-02 20 | ------------------------------- 21 | 22 | - 第一章更新完成,Rust 版本升级至 ``nightly-2022-01-01`` , ``asm`` 和 ``global_asm`` 特性已稳定,相关的宏可在 ``core::arch`` 中找到。更新了作者和版权信息,版本暂定 ``3.6.0-alpha.1`` 。 23 | 24 | 2021-11-20 25 | ------------------------------- 26 | 27 | - 更新1~9章,添加第八章(同步互斥),原第八章(外设)改为第九章。 28 | 29 | 2021-10-20 30 | ------------------------------- 31 | 32 | - 旧版的 3.5.0 文档及代码(全七章)已经发布在 `这里 `_ 。目前开始在主分支上更新新版的文档和代码。 33 | 34 | 2021-03-15 35 | ------------------------------- 36 | 37 | - 增加了在做实验的时候打补丁继承上一章节修改的 :ref:`教程 ` 。 38 | 39 | 2021-03-09 40 | ------------------------------- 41 | 42 | - 将所有分支的 RustSBI 版本更新为 [81d53d8] 的 0.2.0-alpha.1 ,主要是在 Qemu 平台上支持非法指令的转发,目前可以正确处理带有非法指令的应用程序了。参考 ch2 分支上的测例 ``00hello_world.rs`` 。 43 | 44 | 45 | 2021-03-07 46 | ------------------------------- 47 | 48 | - 在各章分支的链接脚本中加入了 ``.srodata/.sbss/.sdata`` 。 49 | 50 | 2021-03-06 51 | ------------------------------- 52 | 53 | - 文档第一版初稿(全七章)完成! 54 | - 修复了框架中基于 Qemu 平台运行却仍需要下载 kflash.py 工具的问题。 55 | 56 | 2021-03-05 57 | ------------------------------- 58 | 59 | - 第三章练习中增加了对于 ``sys_gettime`` 语义在教程和测例中差异的相关说明。 60 | - 修正了第四章练习中 mmap 系统调用语义中的一处错误。 61 | 62 | 63 | 2021-03-03 64 | ------------------------------- 65 | 66 | - 更新了第四章练习题。 67 | - 为方便调试,提供了 riscv64 gcc 工具链的下载链接。 68 | - 将文档渲染改为宽屏模式。 69 | 70 | 2021-02-28 71 | ------------------------------- 72 | 73 | 修复了 ch3-coop 分支在 Rust 版本更新后无法成功运行的问题。 74 | 75 | 2021-02-27 76 | ------------------------------- 77 | 78 | 完善了 ``easy-fs`` : 79 | 80 | - 订正了 ``easy-fs`` 块缓存层的实现,移除了 ``dirty`` 子模块。 81 | - 支持二级间接块索引,使得支持的单个文件最大容量从 :math:`94\text{KiB}` 变为超过 :math:`8\text{MiB}` 。调整了单个 ``DiskInode`` 大小为 128 字节。 82 | - 在新建一个索引节点的时候不再直接分配一二级间接索引块,而是完全按需分配。 83 | - 将 ``easy-fs`` 的测试和应用程序打包的函数分离到另一个名为 ``easy-fs-fuse`` 的 crate 中。 84 | 85 | 从 ch7 开始: 86 | 87 | - 出于后续的一些需求, ``sys_exec`` 需要支持命令行参数,为此shell程序 ``user_shell`` 中需要相应增加一些解析功能,内核中 ``sys_exec`` 的实现也需要进行修改。新增了应用 ``cmdline_args`` 来打印传入的命令行参数。 88 | - 新增了应用 cat 工具可以读取一个文件的全部内容。 89 | - 在shell程序中支持通过 ``<`` 和 ``>`` 进行简单的输入/输出重定向,为此在内核中新增了一个 ``sys_dup`` 系统调用。 90 | 91 | 另外,在所有章节分支新增了 docker 支持来尽可能降低环境配置的时间成本,详见 :ref:`使用 Docker 环境 ` 。 92 | 93 | 2021-02-20 94 | ------------------------------- 95 | 96 | 第六章文档完成。 97 | 98 | 2021-02-16 99 | ------------------------------- 100 | 101 | 第五章文档完成。 102 | 103 | 2021-02-13 104 | ------------------------------- 105 | 106 | 将 ch2-ch6 的 build.rs 中的对齐需求修改为刚好合适。 107 | 108 | 2021-02-09 109 | ------------------------------- 110 | 111 | 在每一章的引言处加入了本章的代码树改动概况。 112 | 113 | 2021-02-08 114 | ------------------------------- 115 | 116 | 将 K210 开发板的烧写工具 ``kflash.py`` 从项目中移除。 117 | 118 | 2021-02-07 119 | ------------------------------- 120 | 121 | 将所有分支的 RustSBI 更新为最新的 0.1.1 版本[3257d899], **不加任何改动** 直接放在项目中。这导致 qemu 和 k210 两个平台的内核入口点变得不同,目前根据 RustSBI 的默认配置,qemu 平台上的内核入口点为 ``0x80200000`` ,而 k210 平台上为了提高烧写速度则为 ``0x80020000`` 。 122 | 123 | 前几个章节应用放置在内存中的位置也需要对应进行修改: 124 | 125 | - 第二章应用的起始地址变为 ``0x80400000`` ; 126 | - 第三章应用的起始地址变为 ``0x80400000`` 。 127 | 128 | 文档稍后更新。 129 | -------------------------------------------------------------------------------- /source/chapter7/5exercise.rst: -------------------------------------------------------------------------------- 1 | 练习 2 | =========================================== 3 | 4 | 课后练习 5 | ------------------------------- 6 | 7 | 编程题 8 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 9 | 10 | 1. `*` 分别编写基于UNIX System V IPC的管道、共享内存、信号量和消息队列的Linux应用程序,实现进程间的数据交换。 11 | 2. `**` 分别编写基于UNIX的signal机制的Linux应用程序,实现进程间异步通知。 12 | 3. `**` 参考rCore Tutorial 中的shell应用程序,在Linux环境下,编写一个简单的shell应用程序,通过管道相关的系统调用,能够支持管道功能。 13 | 4. `**` 扩展内核,实现共享内存机制。 14 | 5. `***` 扩展内核,实现signal机制。 15 | 16 | 问答题 17 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 | 19 | 1. `*` 直接通信和间接通信的本质区别是什么?分别举一个例子。 20 | 2. `**` 试说明基于UNIX的signal机制,如果在本章内核中实现,请描述其大致设计思路和运行过程。 21 | 3. `**` 比较在Linux中的无名管道(普通管道)与有名管道(FIFO)的异同。 22 | 4. `**` 请描述Linux中的无名管道机制的特征和适用场景。 23 | 5. `**` 请描述Linux中的消息队列机制的特征和适用场景。 24 | 6. `**` 请描述Linux中的共享内存机制的特征和适用场景。 25 | 7. `**` 请描述Linux的bash shell中执行与一个程序时,用户敲击 `Ctrl+C` 后,会产生什么信号(signal),导致什么情况出现。 26 | 8. `**` 请描述Linux的bash shell中执行与一个程序时,用户敲击 `Ctrl+Zombie` 后,会产生什么信号(signal),导致什么情况出现。 27 | 9. `**` 请描述Linux的bash shell中执行 `kill -9 2022` 这个命令的含义是什么?导致什么情况出现。 28 | 10. `**` 请指出一种跨计算机的主机间的进程间通信机制。 29 | 30 | 实验练习 31 | ------------------------------- 32 | 33 | 实验练习包括实践作业和问答作业两部分。 34 | 35 | **本次难度也就和lab3一样吧** 36 | 37 | 编程作业 38 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 39 | 40 | 进程通信:邮箱 41 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 42 | 43 | 这一章我们实现了基于 pipe 的进程间通信,但是看测例就知道了,管道不太自由,我们来实现一套乍一看更靠谱的通信 syscall吧!本节要求实现邮箱机制,以及对应的 syscall。 44 | 45 | - 邮箱说明:每个进程拥有唯一一个邮箱,基于“数据报”收发字节信息,利用环形buffer存储,读写顺序为 FIFO,不记录来源进程。每次读写单位必须为一个报文,如果用于接收的缓冲区长度不够,舍弃超出的部分(截断报文)。为了简单,邮箱中最多拥有16条报文,每条报文最大长度256字节。当邮箱满时,发送邮件(也就是写邮箱)会失败。不考虑读写邮箱的权限,也就是所有进程都能够随意给其他进程的邮箱发报。 46 | 47 | **mailread**: 48 | 49 | * syscall ID:401 50 | * Rust接口: ``fn mailread(buf: *mut u8, len: usize)`` 51 | * 功能:读取一个报文,如果成功返回报文长度. 52 | * 参数: 53 | * buf: 缓冲区头。 54 | * len:缓冲区长度。 55 | * 说明: 56 | * len > 256 按 256 处理,len < 队首报文长度且不为0,则截断报文。 57 | * len = 0,则不进行读取,如果没有报文读取,返回-1,否则返回0,这是用来测试是否有报文可读。 58 | * 可能的错误: 59 | * 邮箱空。 60 | * buf 无效。 61 | 62 | **mailwrite**: 63 | 64 | * syscall ID:402 65 | * Rust接口: ``fn mailwrite(pid: usize, buf: *mut u8, len: usize)`` 66 | * 功能:向对应进程邮箱插入一条报文. 67 | * 参数: 68 | * pid: 目标进程id。 69 | * buf: 缓冲区头。 70 | * len:缓冲区长度。 71 | * 说明: 72 | * len > 256 按 256 处理, 73 | * len = 0,则不进行写入,如果邮箱满,返回-1,否则返回0,这是用来测试是否可以发报。 74 | * 可以向自己的邮箱写入报文。 75 | * 可能的错误: 76 | * 邮箱满。 77 | * buf 无效。 78 | 79 | 实验要求 80 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 81 | - 实现分支:ch6-lab 82 | - 实验目录要求不变 83 | - 通过所有测例 84 | 85 | 在 os 目录下 ``make run TEST=1`` 加载所有测例, ``test_usertest`` 打包了所有你需要通过的测例,你也可以通过修改这个文件调整本地测试的内容。 86 | 87 | 你的内核必须前向兼容,能通过前一章的所有测例。 88 | 89 | challenge: 支持多核。 90 | 91 | 问答作业 92 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 93 | 94 | (1) 举出使用 pipe 的一个实际应用的例子。 95 | 96 | (2) 共享内存的测例中有如下片段(伪代码): 97 | 98 | .. code-block:: c 99 | 100 | int main() 101 | { 102 | uint64 *A = (void *)0x10000000; 103 | uint64 *B = (void *)(0x10000000 + 0x1000); 104 | uint64 len = 0x1000; 105 | make_shmem(A, B, len); // 将 [A, A + len) [B, B + len) 这两段虚存映射到同一段物理内存 106 | *A = 0xabab; 107 | __sync_synchronize(); // 这是什么? 108 | if(*B != 0xabab) { 109 | return ERROR; 110 | } 111 | printf("OK!"); 112 | return 0; 113 | } 114 | 115 | 请查阅相关资料,回答 ``__sync_synchronize`` 这行代码的作用,如果去掉它可能会导致什么错误?为什么? 116 | 117 | 118 | 实验练习的提交报告要求 119 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 120 | 121 | * 简单总结本次实验与上个实验相比你增加的东西。(控制在5行以内,不要贴代码) 122 | * 完成问答问题 123 | * (optional) 你对本次实验设计及难度的看法。 -------------------------------------------------------------------------------- /source/chapter4/8exercise.rst: -------------------------------------------------------------------------------- 1 | 练习 2 | ============================================ 3 | 4 | 课后练习 5 | ------------------------------- 6 | 7 | 编程题 8 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 9 | 10 | 1. `**` 使用sbrk,mmap,munmap,mprotect内存相关系统调用的linux应用程序。 11 | 2. `***` 修改本章操作系统内核,实现任务和操作系统内核共用同一张页表的单页表机制。 12 | 3. `***` 扩展内核,支持基于缺页异常机制,具有Lazy 策略的按需分页机制。 13 | 4. `***` 扩展内核,支持基于缺页异常的COW机制。(初始时,两个任务共享一个只读物理页。当一个任务执行写操作后,两个任务拥有各自的可写物理页) 14 | 5. `***` 扩展内核,实现swap in/out机制,并实现Clock置换算法或二次机会置换算法。 15 | 6. `***` 扩展内核,实现自映射机制。 16 | 17 | 问答题 18 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 19 | 20 | .. chyyuu 这次的实验没有涉及到缺页有点遗憾,主要是缺页难以测试,而且更多的是一种优化,不符合这次实验的核心理念,所以这里补两道小题。 21 | 22 | 1. `*` 在使用高级语言编写用户程序的时候,手动用嵌入汇编的方法随机访问一个不在当前程序逻辑地址范围内的地址,比如向该地址读/写数据。该用户程序执行的时候可能会生什么? 23 | 2. `*` 用户程序在运行的过程中,看到的地址是逻辑地址还是物理地址?从用户程序访问某一个地址,到实际内存中的对应单元被读/写,会经过什么样的过程,这个过程中操作系统有什么作用?(站在学过计算机组成原理的角度) 24 | 3. `*` 覆盖、交换和虚拟存储有何异同,虚拟存储的优势和挑战体现在什么地方? 25 | 4. `*` 什么是局部性原理?为何很多程序具有局部性?局部性原理总是正确的吗?为何局部性原理为虚拟存储提供了性能的理论保证? 26 | 5. `**` 一条load指令,最多导致多少次页访问异常?尝试考虑较多情况。 27 | 6. `**` 如果在页访问异常中断服务例程执行时,再次出现页访问异常,这时计算机系统(软件或硬件)会如何处理?这种情况可能出现吗? 28 | 7. `*` 全局和局部置换算法有何不同?分别有哪些算法? 29 | 8. `*` 简单描述OPT、FIFO、LRU、Clock、LFU的工作过程和特点 (不用写太多字,简明扼要即可) 30 | 9. `**` 综合考虑置换算法的收益和开销,综合评判在哪种程序执行环境下使用何种算法比较合适? 31 | 10. `**` Clock算法仅仅能够记录近期是否访问过这一信息,对于访问的频度几乎没有记录,如何改进这一点? 32 | 11. `***` 哪些算法有belady现象?思考belady现象的成因,尝试给出说明OPT和LRU等为何没有belady现象。 33 | 12. `*` 什么是工作集?什么是常驻集?简单描述工作集算法的工作过程。 34 | 13. `*` 请列举 SV39 页`*` 页表项的组成,结合课堂内容,描述其中的标志位有何作用/潜在作用? 35 | 14. `**` 请问一个任务处理 10G 连续的内存页面,需要操作的页表实际大致占用多少内存(给出数量级即可)? 36 | 15. `**` 缺页指的是进程访问页面时页面不在页表中或在页表中无效的现象,此时 MMU 将会返回一个中断,告知操作系统:该进程内存访问出了问题。然后操作系统可选择填补页表并重新执行异常指令或者杀死进程。操作系统基于缺页异常进行优化的两个常见策略中,其一是 Lazy 策略,也就是直到内存页面被访问才实际进行页表操作。比如,一个程序被执行时,进程的代码段理论上需要从磁盘加载到内存。但是 操作系统并不会马上这样做,而是会保存 .text 段在磁盘的位置信息,在这些代码第一次被执行时才完成从磁盘的加载操作。 另一个常见策略是 swap 页置换策略,也就是内存页面可能被换到磁盘上了,导致对应页面失效,操作系统在任务访问到该页产生异常时,再把数据从磁盘加载到内存。 37 | 38 | - 哪些异常可能是缺页导致的?发生缺页时,描述与缺页相关的CSR寄存器的值及其含义。 39 | - Lazy 策略有哪些好处?请描述大致如何实现Lazy策略? 40 | - swap 页置换策略有哪些好处?此时页面失效如何表现在页表项(PTE)上?请描述大致如何实现swap策略? 41 | 42 | 16. `**` 为了防范侧信道攻击,本章的操作系统使用了双页表。但是传统的操作系统设计一般采用单页表,也就是说,任务和操作系统内核共用同一张页表,只不过内核对应的地址只允许在内核态访问。(备注:这里的单/双的说法仅为自创的通俗说法,并无这个名词概念,详情见 `KPTI `_ ) 43 | 44 | - 单页表情况下,如何控制用户态无法访问内核页面? 45 | - 相对于双页表,单页表有何优势? 46 | - 请描述:在单页表和双页表模式下,分别在哪个时机,如何切换页表? 47 | 48 | 49 | 实验练习 50 | ------------------------------- 51 | 52 | 实验练习包括实践作业和问答作业两部分。 53 | 54 | 实践作业 55 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 56 | 57 | 重写 sys_get_time 58 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 59 | 60 | 引入虚存机制后,原来内核的 sys_get_time 函数实现就无效了。请你重写这个函数,恢复其正常功能。 61 | 62 | mmap 和 munmap 匿名映射 63 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 64 | 65 | `mmap `_ 在 Linux 中主要用于在内存中映射文件,本次实验简化它的功能,仅用于申请内存。 66 | 67 | 请实现 mmap 和 munmap 系统调用,mmap 定义如下: 68 | 69 | 70 | .. code-block:: rust 71 | 72 | fn sys_mmap(start: usize, len: usize, prot: usize) -> isize 73 | 74 | - syscall ID:222 75 | - 申请长度为 len 字节的物理内存(不要求实际物理内存位置,可以随便找一块),将其映射到 start 开始的虚存,内存页属性为 prot 76 | - 参数: 77 | - start 需要映射的虚存起始地址,要求按页对齐 78 | - len 映射字节长度,可以为 0 79 | - prot:第 0 位表示是否可读,第 1 位表示是否可写,第 2 位表示是否可执行。其他位无效且必须为 0 80 | - 返回值:执行成功则返回 0,错误返回 -1 81 | - 说明: 82 | - 为了简单,目标虚存区间要求按页对齐,len 可直接按页向上取整,不考虑分配失败时的页回收。 83 | - 可能的错误: 84 | - start 没有按页大小对齐 85 | - prot & !0x7 != 0 (prot 其余位必须为0) 86 | - prot & 0x7 = 0 (这样的内存无意义) 87 | - [start, start + len) 中存在已经被映射的页 88 | - 物理内存不足 89 | 90 | munmap 定义如下: 91 | 92 | .. code-block:: rust 93 | 94 | fn sys_munmap(start: usize, len: usize) -> isize 95 | 96 | - syscall ID:215 97 | - 取消到 [start, start + len) 虚存的映射 98 | - 参数和返回值请参考 mmap 99 | - 说明: 100 | - 为了简单,参数错误时不考虑内存的恢复和回收。 101 | - 可能的错误: 102 | - [start, start + len) 中存在未被映射的虚存。 103 | 104 | 105 | TIPS:注意 prot 参数的语义,它与内核定义的 MapPermission 有明显不同! 106 | 107 | 实验要求 108 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 109 | 110 | - 实现分支:ch4-lab 111 | - 实验目录要求不变 112 | - 通过所有测例 113 | 114 | 在 os 目录下 ``make run TEST=1`` 测试 sys_get_time, ``make run TEST=2`` 测试 map 和 unmap。 115 | 116 | challenge: 支持多核。 117 | 118 | 问答作业 119 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 120 | 121 | 无 122 | 123 | 实验练习的提交报告要求 124 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 125 | 126 | * 简单总结本次实验与上个实验相比你增加的东西。(控制在5行以内,不要贴代码) 127 | * 完成问答问题。 128 | * (optional) 你对本次实验设计及难度的看法。 129 | -------------------------------------------------------------------------------- /source/chapter2/5exercise.rst: -------------------------------------------------------------------------------- 1 | 练习 2 | ===================================================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | 9 | 课后练习 10 | ------------------------------- 11 | 12 | 编程题 13 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 14 | 1. `***` 实现一个裸机应用程序A,能打印调用栈。 15 | 2. `**` 扩展内核,实现新系统调用get_taskinfo,能显示当前task的id和task name;实现一个裸机应用程序B,能访问get_taskinfo系统调用。 16 | 3. `**` 扩展内核,能够统计多个应用的执行过程中系统调用编号和访问此系统调用的次数。 17 | 4. `**` 扩展内核,能够统计每个应用执行后的完成时间。 18 | 5. `***` 扩展内核,统计执行异常的程序的异常情况(主要是各种特权级涉及的异常),能够打印异常程序的出错的地址和指令等信息。 19 | 20 | 21 | 注:上述编程基于 rcore/ucore tutorial v3: Branch ch2 22 | 23 | 问答题 24 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 25 | 26 | 1. `*` 函数调用与系统调用有何区别? 27 | 2. `**` 为了方便操作系统处理,M态软件会将 S 态异常/中断委托给 S 态软件,请指出有哪些寄存器记录了委托信息,rustsbi 委托了哪些异常/中断?(也可以直接给出寄存器的值) 28 | 3. `**` 如果操作系统以应用程序库的形式存在,应用程序可以通过哪些方式破坏操作系统? 29 | 4. `**` 编译器/操作系统/处理器如何合作,可采用哪些方法来保护操作系统不受应用程序的破坏? 30 | 5. `**` RISC-V处理器的S态特权指令有哪些,其大致含义是什么,有啥作用? 31 | 6. `**` RISC-V处理器在用户态执行特权指令后的硬件层面的处理过程是什么? 32 | 7. `**` 操作系统在完成用户态<-->内核态双向切换中的一般处理过程是什么? 33 | 8. `**` 程序陷入内核的原因有中断、异常和陷入(系统调用),请问 riscv64 支持哪些中断 / 异常?如何判断进入内核是由于中断还是异常?描述陷入内核时的几个重要寄存器及其值。 34 | 9. `*` 在哪些情况下会出现特权级切换:用户态-->内核态,以及内核态-->用户态? 35 | 10. `**` Trap上下文的含义是啥?在本章的操作系统中,Trap上下文的具体内容是啥?如果不进行Trap上下文的保存于恢复,会出现什么情况? 36 | 37 | 实验练习 38 | ------------------------------- 39 | 40 | 实验练习包括实践作业和问答作业两部分。 41 | 42 | 实践作业 43 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 44 | 45 | sys_write 安全检查 46 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 47 | 48 | ch2 中,我们实现了第一个系统调用 ``sys_write``,这使得我们可以在用户态输出信息。但是 os 在提供服务的同时,还有保护 os 本身以及其他用户程序不受错误或者恶意程序破坏的功能。 49 | 50 | 由于还没有实现虚拟内存,我们可以在用户程序中指定一个属于其他程序字符串,并将它输出,这显然是不合理的,因此我们要对 sys_write 做检查: 51 | 52 | - sys_write 仅能输出位于程序本身内存空间内的数据,否则报错。 53 | 54 | 实验要求 55 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 56 | - 实现分支: ch2-lab 57 | - 目录要求不变 58 | - 为 sys_write 增加安全检查 59 | 60 | 在 os 目录下执行 ``make run TEST=1`` 测试 ``sys_write`` 安全检查的实现,正确执行目标用户测例,并得到预期输出(详见测例注释)。 61 | 62 | 注意:如果设置默认 log 等级,从 lab2 开始关闭所有 log 输出。 63 | 64 | challenge: 支持多核,实现多个核运行用户程序。 65 | 66 | 实验约定 67 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 68 | 69 | 在第二章的测试中,我们对于内核有如下仅仅为了测试方便的要求,请调整你的内核代码来符合这些要求。 70 | 71 | - 用户栈大小必须为 4096,且按照 4096 字节对齐。这一规定可以在实验4开始删除,仅仅为通过 lab2/3 测例设置。 72 | 73 | .. _inherit-last-ch-changes: 74 | 75 | .. note:: 76 | 77 | **如何快速继承上一章练习题的修改** 78 | 79 | 从这一章开始,在完成本章习题之前,首先要做的就是将上一章框架的修改继承到本章的框架代码。出于各种原因,实际上通过 ``git merge`` 并不是很方便,这里给出一种打 patch 的方法,希望能够有所帮助。 80 | 81 | 1. 切换到上一章的分支,通过 ``git log`` 找到你在此分支上的第一次 commit 的前一个 commit 的 ID ,复制其前 8 位,记作 ``base-commit`` 。假设分支上最新的一次 commit ID 是 ``last-commit`` 。 82 | 2. 确保你位于项目根目录 ``rCore-Tutorial-v3`` 下。通过 ``git diff > `` 即可在 ``patch-path`` 路径位置(比如 ``~/Desktop/chx.patch`` )生成一个描述你对于上一章分支进行的全部修改的一个补丁文件。打开看一下,它给出了每个被修改的文件中涉及了哪些块的修改,还附加了块前后的若干行代码。如果想更加灵活进行合并的话,可以通过 ``git format-patch `` 命令在当前目录下生成一组补丁,它会对于 ``base-commit`` 后面的每一次 commit 均按照顺序生成一个补丁。 83 | 3. 切换到本章分支,通过 ``git apply --reject `` 来将一个补丁打到当前章节上。它的大概原理是对于补丁中的每个被修改文件中的每个修改块,尝试通过块的前后若干行代码来定位它在当前分支上的位置并进行替换。有一些块可能无法匹配,此时会生成与这些块所在的文件同名的 ``*.rej`` 文件,描述了哪些块替换失败了。在项目根目录 ``rCore-Tutorial-v3`` 下,可以通过 ``find . -name *.rej`` 来找到所有相关的 ``*.rej`` 文件并手动完成替换。 84 | 4. 在处理完所有 ``*.rej`` 之后,将它们删除并 commit 一下。现在就可以开始本章的实验了。 85 | 86 | 问答作业 87 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 88 | 89 | 1. 正确进入 U 态后,程序的特征还应有:使用 S 态特权指令,访问 S 态寄存器后会报错。请自行测试这些内容 (运行 Rust 三个 bad 测例 ) ,描述程序出错行为,注明你使用的 sbi 及其版本。 90 | 91 | 2. 请结合用例理解 `trap.S `_ 中两个函数 ``__alltraps`` 和 ``__restore`` 的作用,并回答如下几个问题: 92 | 93 | 1. L40:刚进入 ``__restore`` 时,``a0`` 代表了什么值。请指出 ``__restore`` 的两种使用情景。 94 | 95 | 2. L46-L51:这几行汇编代码特殊处理了哪些寄存器?这些寄存器的的值对于进入用户态有何意义?请分别解释。 96 | 97 | .. code-block:: riscv 98 | 99 | ld t0, 32*8(sp) 100 | ld t1, 33*8(sp) 101 | ld t2, 2*8(sp) 102 | csrw sstatus, t0 103 | csrw sepc, t1 104 | csrw sscratch, t2 105 | 106 | 3. L53-L59:为何跳过了 ``x2`` 和 ``x4``? 107 | 108 | .. code-block:: riscv 109 | 110 | ld x1, 1*8(sp) 111 | ld x3, 3*8(sp) 112 | .set n, 5 113 | .rept 27 114 | LOAD_GP %n 115 | .set n, n+1 116 | .endr 117 | 118 | 4. L63:该指令之后,``sp`` 和 ``sscratch`` 中的值分别有什么意义? 119 | 120 | .. code-block:: riscv 121 | 122 | csrrw sp, sscratch, sp 123 | 124 | 5. ``__restore``:中发生状态切换在哪一条指令?为何该指令执行之后会进入用户态? 125 | 126 | 6. L13:该指令之后,``sp`` 和 ``sscratch`` 中的值分别有什么意义? 127 | 128 | .. code-block:: riscv 129 | 130 | csrrw sp, sscratch, sp 131 | 132 | 7. 从 U 态进入 S 态是哪一条指令发生的? 133 | 134 | 135 | 136 | 3. 对于任何中断,``__alltraps`` 中都需要保存所有寄存器吗?你有没有想到一些加速 ``__alltraps`` 的方法?简单描述你的想法。 137 | 138 | 实验练习的提交报告要求 139 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 140 | 141 | - 简单总结与上次实验相比本次实验你增加的东西(控制在5行以内,不要贴代码)。 142 | - 完成问答问题。 143 | - (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 144 | 145 | 146 | -------------------------------------------------------------------------------- /source/chapter6/4exercise.rst: -------------------------------------------------------------------------------- 1 | 练习 2 | ================================================ 3 | 4 | 课后练习 5 | ------------------------------- 6 | 7 | 编程题 8 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 9 | 10 | 1. `*` 扩展easy-fs文件系统功能,扩大单个文件的大小,支持三重间接inode。 11 | 2. `*` 扩展内核功能,支持stat系统调用,能显示文件的inode元数据信息。 12 | 3. `**` 扩展内核功能,支持mmap系统调用,支持对文件的映射,实现基于内存读写方式的文件读写功能。 13 | 4. `**` 扩展easy-fs文件系统功能,支持二级目录结构。可扩展:支持N级目录结构。 14 | 5. `***` 扩展easy-fs文件系统功能,通过日志机制支持crash一致性。 15 | 16 | 问答题 17 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 | 19 | 1. `*` 文件系统的功能是什么? 20 | 2. `**` 目前的文件系统只有单级目录,假设想要支持多级文件目录,请描述你设想的实现方式,描述合理即可。 21 | 3. `**` 软链接和硬链接是干什么的?有什么区别?当删除一个软链接或硬链接时分别会发生什么? 22 | 4. `***` 在有了多级目录之后,我们就也可以为一个目录增加硬链接了。在这种情况下,文件树中是否可能出现环路(软硬链接都可以,鼓励多尝试)?你认为应该如何解决?请在你喜欢的系统上实现一个环路,描述你的实现方式以及系统提示、实际测试结果。 23 | 5. `*` 目录是一类特殊的文件,存放的是什么内容?用户可以自己修改目录内容吗? 24 | 6. `**` 在实际操作系统中,如Linux,为什么会存在大量的文件系统类型? 25 | 7. `**` 可以把文件控制块放到目录项中吗?这样做有什么优缺点? 26 | 8. `**` 为什么要同时维护进程的打开文件表和操作系统的打开文件表?这两个打开文件表有什么区别和联系? 27 | 9. `**` 文件分配的三种方式是如何组织文件数据块的?各有什么特征(存储、文件读写、可靠性)? 28 | 10. `**` 如果一个程序打开了一个文件,写入了一些数据,但是没有及时关闭,可能会有什么后果?如果打开文件后,又进一步发出了读文件的系统调用,操作系统中各个组件是如何相互协作完成整个读文件的系统调用的? 29 | 11. `***` 文件系统是一个操作系统必要的组件吗?是否可以将文件系统放到用户态?这样做有什么好处?操作系统需要提供哪些基本支持? 30 | 31 | 实验练习 32 | ------------------------------- 33 | 34 | 实验练习包括实践作业和问答作业两部分。 35 | 36 | **理解文件系统比较费事,编程难度适中** 37 | 38 | 实践作业 39 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 40 | 41 | 硬链接 42 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 43 | 44 | 硬链接要求两个不同的目录项指向同一个文件,在我们的文件系统中也就是两个不同名称目录项指向同一个磁盘块。 45 | 46 | 本节要求实现三个系统调用 ``sys_linkat、sys_unlinkat、sys_stat`` 。 47 | 48 | **linkat**: 49 | 50 | * syscall ID: 37 51 | * 功能:创建一个文件的一个硬链接, `linkat标准接口 `_ 。 52 | * C接口: ``int linkat(int olddirfd, char* oldpath, int newdirfd, char* newpath, unsigned int flags)`` 53 | * Rust 接口: ``fn linkat(olddirfd: i32, oldpath: *const u8, newdirfd: i32, newpath: *const u8, flags: u32) -> i32`` 54 | * 参数: 55 | * olddirfd,newdirfd: 仅为了兼容性考虑,本次实验中始终为 AT_FDCWD (-100),可以忽略。 56 | * flags: 仅为了兼容性考虑,本次实验中始终为 0,可以忽略。 57 | * oldpath:原有文件路径 58 | * newpath: 新的链接文件路径。 59 | * 说明: 60 | * 为了方便,不考虑新文件路径已经存在的情况(属于未定义行为),除非链接同名文件。 61 | * 返回值:如果出现了错误则返回 -1,否则返回 0。 62 | * 可能的错误 63 | * 链接同名文件。 64 | 65 | **unlinkat**: 66 | 67 | * syscall ID: 35 68 | * 功能:取消一个文件路径到文件的链接, `unlinkat标准接口 `_ 。 69 | * C接口: ``int unlinkat(int dirfd, char* path, unsigned int flags)`` 70 | * Rust 接口: ``fn unlinkat(dirfd: i32, path: *const u8, flags: u32) -> i32`` 71 | * 参数: 72 | * dirfd: 仅为了兼容性考虑,本次实验中始终为 AT_FDCWD (-100),可以忽略。 73 | * flags: 仅为了兼容性考虑,本次实验中始终为 0,可以忽略。 74 | * path:文件路径。 75 | * 说明: 76 | * 为了方便,不考虑使用 unlink 彻底删除文件的情况。 77 | * 返回值:如果出现了错误则返回 -1,否则返回 0。 78 | * 可能的错误 79 | * 文件不存在。 80 | 81 | **fstat**: 82 | 83 | * syscall ID: 80 84 | * 功能:获取文件状态。 85 | * C接口: ``int fstat(int fd, struct Stat* st)`` 86 | * Rust 接口: ``fn fstat(fd: i32, st: *mut Stat) -> i32`` 87 | * 参数: 88 | * fd: 文件描述符 89 | * st: 文件状态结构体 90 | 91 | .. code-block:: rust 92 | 93 | #[repr(C)] 94 | #[derive(Debug)] 95 | pub struct Stat { 96 | /// 文件所在磁盘驱动器号,该实验中写死为 0 即可 97 | pub dev: u64, 98 | /// inode 文件所在 inode 编号 99 | pub ino: u64, 100 | /// 文件类型 101 | pub mode: StatMode, 102 | /// 硬链接数量,初始为1 103 | pub nlink: u32, 104 | /// 无需考虑,为了兼容性设计 105 | pad: [u64; 7], 106 | } 107 | 108 | /// StatMode 定义: 109 | bitflags! { 110 | pub struct StatMode: u32 { 111 | const NULL = 0; 112 | /// directory 113 | const DIR = 0o040000; 114 | /// ordinary regular file 115 | const FILE = 0o100000; 116 | } 117 | } 118 | 119 | 120 | 实验要求 121 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 122 | - 实现分支:ch7-lab 123 | - 实验目录要求不变 124 | - 通过所有测例 125 | 126 | 在 os 目录下 ``make run TEST=1`` 加载所有测例, ``test_usertest`` 打包了所有你需要通过的测例,你也可以通过修改这个文件调整本地测试的内容。 127 | 128 | 你的内核必须前向兼容,能通过前一章的所有测例。 129 | 130 | .. note:: 131 | 132 | **如何调试 easy-fs** 133 | 134 | 如果你在第一章练习题中已经借助 ``log`` crate 实现了日志功能,那么你可以直接在 ``easy-fs`` 中引入 ``log`` crate,通过 ``log::info!/debug!`` 等宏即可进行调试并在内核中看到日志输出。具体来说,在 ``easy-fs`` 中的修改是:在 ``easy-fs/Cargo.toml`` 的依赖中加入一行 ``log = "0.4.0"``,然后在 ``easy-fs/src/lib.rs`` 中加入一行 ``extern crate log`` 。 135 | 136 | 你也可以完全在用户态进行调试。仿照 ``easy-fs-fuse`` 建立一个在当前操作系统中运行的应用程序,将测试逻辑写在 ``main`` 函数中。这个时候就可以将它引用的 ``easy-fs`` 的 ``no_std`` 去掉并使用 ``println!`` 进行调试。 137 | 138 | 139 | 问答作业 140 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 141 | 142 | 无 143 | 144 | 实验练习的提交报告要求 145 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 146 | * 简单总结本次实验与上个实验相比你增加的东西。(控制在5行以内,不要贴代码) 147 | * 完成问答问题 148 | * (optional) 你对本次实验设计及难度的看法。 -------------------------------------------------------------------------------- /source/_static/dracula.css: -------------------------------------------------------------------------------- 1 | /* Dracula Theme v1.2.5 2 | * 3 | * https://github.com/zenorocha/dracula-theme 4 | * 5 | * Copyright 2016, All rights reserved 6 | * 7 | * Code licensed under the MIT license 8 | * http://zenorocha.mit-license.org 9 | * 10 | * @author Rob G 11 | * @author Chris Bracco 12 | * @author Zeno Rocha 13 | */ 14 | 15 | .highlight .hll { background-color: #111110 } 16 | .highlight { background: #282a36; color: #f8f8f2 } 17 | .highlight .c { color: #6272a4 } /* Comment */ 18 | .highlight .err { color: #f8f8f2 } /* Error */ 19 | .highlight .g { color: #f8f8f2 } /* Generic */ 20 | .highlight .k { color: #ff79c6 } /* Keyword */ 21 | .highlight .l { color: #f8f8f2 } /* Literal */ 22 | .highlight .n { color: #f8f8f2 } /* Name */ 23 | .highlight .o { color: #ff79c6 } /* Operator */ 24 | .highlight .x { color: #f8f8f2 } /* Other */ 25 | .highlight .p { color: #f8f8f2 } /* Punctuation */ 26 | .highlight .ch { color: #6272a4 } /* Comment.Hashbang */ 27 | .highlight .cm { color: #6272a4 } /* Comment.Multiline */ 28 | .highlight .cp { color: #ff79c6 } /* Comment.Preproc */ 29 | .highlight .cpf { color: #6272a4 } /* Comment.PreprocFile */ 30 | .highlight .c1 { color: #6272a4 } /* Comment.Single */ 31 | .highlight .cs { color: #6272a4 } /* Comment.Special */ 32 | .highlight .gd { color: #962e2f } /* Generic.Deleted */ 33 | .highlight .ge { color: #f8f8f2; text-decoration: underline } /* Generic.Emph */ 34 | .highlight .gr { color: #f8f8f2 } /* Generic.Error */ 35 | .highlight .gh { color: #f8f8f2; font-weight: bold } /* Generic.Heading */ 36 | .highlight .gi { color: #f8f8f2; font-weight: bold } /* Generic.Inserted */ 37 | .highlight .go { color: #44475a } /* Generic.Output */ 38 | .highlight .gp { color: #f8f8f2 } /* Generic.Prompt */ 39 | .highlight .gs { color: #f8f8f2 } /* Generic.Strong */ 40 | .highlight .gu { color: #f8f8f2; font-weight: bold } /* Generic.Subheading */ 41 | .highlight .gt { color: #f8f8f2 } /* Generic.Traceback */ 42 | .highlight .kc { color: #ff79c6 } /* Keyword.Constant */ 43 | .highlight .kd { color: #8be9fd; font-style: italic } /* Keyword.Declaration */ 44 | .highlight .kn { color: #ff79c6 } /* Keyword.Namespace */ 45 | .highlight .kp { color: #ff79c6 } /* Keyword.Pseudo */ 46 | .highlight .kr { color: #ff79c6 } /* Keyword.Reserved */ 47 | .highlight .kt { color: #8be9fd } /* Keyword.Type */ 48 | .highlight .ld { color: #f8f8f2 } /* Literal.Date */ 49 | .highlight .m { color: #bd93f9 } /* Literal.Number */ 50 | .highlight .s { color: #f1fa8c } /* Literal.String */ 51 | .highlight .na { color: #50fa7b } /* Name.Attribute */ 52 | .highlight .nb { color: #8be9fd; font-style: italic } /* Name.Builtin */ 53 | .highlight .nc { color: #50fa7b } /* Name.Class */ 54 | .highlight .no { color: #f8f8f2 } /* Name.Constant */ 55 | .highlight .nd { color: #f8f8f2 } /* Name.Decorator */ 56 | .highlight .ni { color: #f8f8f2 } /* Name.Entity */ 57 | .highlight .ne { color: #f8f8f2 } /* Name.Exception */ 58 | .highlight .nf { color: #50fa7b } /* Name.Function */ 59 | .highlight .nl { color: #8be9fd; font-style: italic } /* Name.Label */ 60 | .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ 61 | .highlight .nx { color: #f8f8f2 } /* Name.Other */ 62 | .highlight .py { color: #f8f8f2 } /* Name.Property */ 63 | .highlight .nt { color: #ff79c6 } /* Name.Tag */ 64 | .highlight .nv { color: #8be9fd; font-style: italic } /* Name.Variable */ 65 | .highlight .ow { color: #ff79c6 } /* Operator.Word */ 66 | .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ 67 | .highlight .mb { color: #bd93f9 } /* Literal.Number.Bin */ 68 | .highlight .mf { color: #bd93f9 } /* Literal.Number.Float */ 69 | .highlight .mh { color: #bd93f9 } /* Literal.Number.Hex */ 70 | .highlight .mi { color: #bd93f9 } /* Literal.Number.Integer */ 71 | .highlight .mo { color: #bd93f9 } /* Literal.Number.Oct */ 72 | .highlight .sa { color: #f1fa8c } /* Literal.String.Affix */ 73 | .highlight .sb { color: #f1fa8c } /* Literal.String.Backtick */ 74 | .highlight .sc { color: #f1fa8c } /* Literal.String.Char */ 75 | .highlight .dl { color: #f1fa8c } /* Literal.String.Delimiter */ 76 | .highlight .sd { color: #f1fa8c } /* Literal.String.Doc */ 77 | .highlight .s2 { color: #f1fa8c } /* Literal.String.Double */ 78 | .highlight .se { color: #f1fa8c } /* Literal.String.Escape */ 79 | .highlight .sh { color: #f1fa8c } /* Literal.String.Heredoc */ 80 | .highlight .si { color: #f1fa8c } /* Literal.String.Interpol */ 81 | .highlight .sx { color: #f1fa8c } /* Literal.String.Other */ 82 | .highlight .sr { color: #f1fa8c } /* Literal.String.Regex */ 83 | .highlight .s1 { color: #f1fa8c } /* Literal.String.Single */ 84 | .highlight .ss { color: #f1fa8c } /* Literal.String.Symbol */ 85 | .highlight .bp { color: #f8f8f2; font-style: italic } /* Name.Builtin.Pseudo */ 86 | .highlight .fm { color: #50fa7b } /* Name.Function.Magic */ 87 | .highlight .vc { color: #8be9fd; font-style: italic } /* Name.Variable.Class */ 88 | .highlight .vg { color: #8be9fd; font-style: italic } /* Name.Variable.Global */ 89 | .highlight .vi { color: #8be9fd; font-style: italic } /* Name.Variable.Instance */ 90 | .highlight .vm { color: #8be9fd; font-style: italic } /* Name.Variable.Magic */ 91 | .highlight .il { color: #bd93f9 } /* Literal.Number.Integer.Long */ 92 | -------------------------------------------------------------------------------- /source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'rCore-Tutorial-Book-v3' 21 | copyright = '2020-2022, Yu Chen, Yifan Wu' 22 | author = 'Yu Chen, Yifan Wu' 23 | language = 'zh_CN' 24 | html_search_language = 'zh' 25 | 26 | # The full version, including alpha/beta/rc tags 27 | release = '3.6.0-alpha.1' 28 | 29 | 30 | # -- General configuration --------------------------------------------------- 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | "sphinx_comments" 37 | ] 38 | 39 | comments_config = { 40 | "utterances": { 41 | "repo": "rcore-os/rCore-Tutorial-Book-v3", 42 | "issue-term": "pathname", 43 | "label": "comments", 44 | "theme": "github-light", 45 | "crossorigin": "anonymous", 46 | } 47 | } 48 | 49 | # Add any paths that contain templates here, relative to this directory. 50 | templates_path = ['_templates'] 51 | 52 | # List of patterns, relative to source directory, that match files and 53 | # directories to ignore when looking for source files. 54 | # This pattern also affects html_static_path and html_extra_path. 55 | exclude_patterns = [] 56 | 57 | 58 | # -- Options for HTML output ------------------------------------------------- 59 | 60 | # The theme to use for HTML and HTML Help pages. See the documentation for 61 | # a list of builtin themes. 62 | # 63 | #html_theme = 'sphinx_rtd_theme' 64 | html_theme = 'furo' 65 | # Add any paths that contain custom static files (such as style sheets) here, 66 | # relative to this directory. They are copied after the builtin static files, 67 | # so a file named "default.css" will overwrite the builtin "default.css". 68 | html_static_path = ['_static'] 69 | 70 | html_css_files = [ 71 | 'my_style.css', 72 | #'dracula.css', 73 | ] 74 | 75 | from pygments.lexer import RegexLexer 76 | from pygments import token 77 | from sphinx.highlighting import lexers 78 | 79 | class RVLexer(RegexLexer): 80 | name = 'riscv' 81 | tokens = { 82 | 'root': [ 83 | # Comment 84 | (r'#.*\n', token.Comment), 85 | # General Registers 86 | (r'\b(?:x[1-2]?[0-9]|x30|x31|zero|ra|sp|gp|tp|fp|t[0-6]|s[0-9]|s1[0-1]|a[0-7]|pc)\b', token.Name.Attribute), 87 | # CSRs 88 | (r'\bs(?:status|tvec|ip|ie|counteren|scratch|epc|cause|tval|atp|)\b', token.Name.Constant), 89 | (r'\bm(?:isa|vendorid|archid|hardid|status|tvec|ideleg|ip|ie|counteren|scratch|epc|cause|tval)\b', token.Name.Constant), 90 | # Instructions 91 | (r'\b(?:(addi?w?)|(slti?u?)|(?:and|or|xor)i?|(?:sll|srl|sra)i?w?|lui|auipc|subw?|jal|jalr|beq|bne|bltu?|bgeu?|s[bhwd]|(l[bhw]u?)|ld)\b', token.Name.Decorator), 92 | (r'\b(?:csrr?[rws]i?)\b', token.Name.Decorator), 93 | (r'\b(?:ecall|ebreak|[msu]ret|wfi|sfence.vma)\b', token.Name.Decorator), 94 | (r'\b(?:nop|li|la|mv|not|neg|negw|sext.w|seqz|snez|sltz|sgtz|f(?:mv|abs|neg).(?:s|d)|b(?:eq|ne|le|ge|lt)z|bgt|ble|bgtu|bleu|j|jr|ret|call)\b', token.Name.Decorator), 95 | (r'(?:%hi|%lo|%pcrel_hi|%pcrel_lo|%tprel_(?:hi|lo|add))', token.Name.Decorator), 96 | # Directives 97 | (r'(?:.2byte|.4byte|.8byte|.quad|.half|.word|.dword|.byte|.dtpreldword|.dtprelword|.sleb128|.uleb128|.asciz|.string|.incbin|.zero)', token.Name.Function), 98 | (r'(?:.align|.balign|.p2align)', token.Name.Function), 99 | (r'(?:.globl|.local|.equ)', token.Name.Function), 100 | (r'(?:.text|.data|.rodata|.bss|.comm|.common|.section)', token.Name.Function), 101 | (r'(?:.option|.macro|.endm|.file|.ident|.size|.type)', token.Name.Function), 102 | (r'(?:.set|.rept|.endr|.macro|.endm|.altmacro)', token.Name.Function), 103 | # Number 104 | (r'\b(?:(?:0x|)[\da-f]+|(?:0o|)[0-7]+|\d+)\b', token.Number), 105 | # Labels 106 | (r'\S+:', token.Name.Builtin), 107 | # Whitespace 108 | (r'\s', token.Whitespace), 109 | # Other operators 110 | (r'[,\+\*\-\(\)\\%]', token.Text), 111 | # Hacks 112 | (r'(?:SAVE_GP|trap_handler|__switch|LOAD_GP|SAVE_SN|LOAD_SN|__alltraps|__restore)', token.Name.Builtin), 113 | (r'(?:.trampoline)', token.Name.Function), 114 | (r'(?:n)', token.Name.Entity), 115 | (r'(?:x)', token.Text), 116 | ], 117 | } 118 | 119 | lexers['riscv'] = RVLexer() 120 | -------------------------------------------------------------------------------- /source/chapter3/1multi-loader.rst: -------------------------------------------------------------------------------- 1 | 多道程序放置与加载 2 | ===================================== 3 | 4 | 本节导读 5 | -------------------------- 6 | 7 | 本节我们将实现可以把多个应用放置到内存中的二叠纪“锯齿螈” [#prionosuchus]_ 操作系统,“锯齿螈”能够上陆了!能实现二叠纪“锯齿螈”操作系统的一个重要前提是计算机中物理内存容量增加了,足以容纳多个应用程序的内容。在计算机的发展史上,我们也确实看到,随着集成电路的快速发展,计算机的内存容量也越来越大了。 8 | 9 | 在本章的引言中我们提到每个应用都需要按照它的编号被分别放置并加载到内存中不同的位置。本节我们就来介绍多应用的内存放置是如何实现的。通过具体实现,可以看到多个应用程序被一次性地加载到内存中,这样在切换到另外一个应用程序执行会很快,不像前一章介绍的操作系统,还要有清空前一个应用,然后加载当前应用的过程开销。 10 | 11 | 但我们也会了解到,每个应用程序需要知道自己运行时在内存中的不同位置,这对应用程序的编写带来了一定的麻烦。而且操作系统也要知道每个应用程序运行时的位置,不能任意移动应用程序所在的内存空间,即不能在运行时根据内存空间的动态空闲情况,把应用程序调整到合适的空闲空间中。这是“锯齿螈” [#prionosuchus]_ 操作系统在动态内存管理上的不足之处。 12 | 13 | .. 14 | chyyuu:有一个ascii图,画出我们做的OS在本节的部分。 15 | 16 | 多道程序放置 17 | ---------------------------- 18 | 19 | 与第二章相同,所有应用的 ELF 格式执行文件都经过 ``objcopy`` 工具丢掉所有 ELF header 和符号变为二进制镜像文件,随后以同样的格式通过在操作系统内核中嵌入 ``link_user.S`` 文件,在编译时直接把应用链接到内核的数据段中。不同的是,我们对相关模块进行了调整:在第二章中应用的加载和执行进度控制都交给 ``batch`` 子模块,而在第三章中我们将应用的加载这部分功能分离出来在 ``loader`` 子模块中实现,应用的执行和切换功能则交给 ``task`` 子模块。 20 | 21 | 注意,我们需要调整每个应用被构建时使用的链接脚本 ``linker.ld`` 中的起始地址 ``BASE_ADDRESS`` ,这个地址是应用被内核加载到内存中的起始地址。也就是要做到:应用知道自己会被加载到某个地址运行,而内核也确实能做到将应用加载到它指定的那个地址。这算是应用和内核在某种意义上达成的一种协议。之所以要有这么苛刻的条件,是因为目前的操作系统内核的能力还是比较弱的,对应用程序通用性的支持也不够(比如不支持加载应用到内存中的任意地址运行),这也进一步导致了应用程序编程上不够方便和通用(应用需要指定自己运行的内存地址)。事实上,目前应用程序的编址方式是基于绝对位置的,并没做到与位置无关,内核也没有提供相应的地址重定位机制。 22 | 23 | .. note:: 24 | 25 | 对于编址方式,需要再回顾一下编译原理课讲解的后端代码生成技术,以及计算机组成原理课的指令寻址方式的内容。可以在 `这里 `_ 找到更多有关 26 | 位置无关和重定位的说明。 27 | 28 | 由于每个应用被加载到的位置都不同,也就导致它们的链接脚本 ``linker.ld`` 中的 ``BASE_ADDRESS`` 都是不同的。实际上,我们不是直接用 ``cargo build`` 构建应用的链接脚本,而是写了一个脚本定制工具 ``build.py`` ,为每个应用定制了各自的链接脚本: 29 | 30 | .. code-block:: python 31 | :linenos: 32 | 33 | # user/build.py 34 | 35 | import os 36 | 37 | base_address = 0x80400000 38 | step = 0x20000 39 | linker = 'src/linker.ld' 40 | 41 | app_id = 0 42 | apps = os.listdir('src/bin') 43 | apps.sort() 44 | for app in apps: 45 | app = app[:app.find('.')] 46 | lines = [] 47 | lines_before = [] 48 | with open(linker, 'r') as f: 49 | for line in f.readlines(): 50 | lines_before.append(line) 51 | line = line.replace(hex(base_address), hex(base_address+step*app_id)) 52 | lines.append(line) 53 | with open(linker, 'w+') as f: 54 | f.writelines(lines) 55 | os.system('cargo build --bin %s --release' % app) 56 | print('[build.py] application %s start with address %s' %(app, hex(base_address+step*app_id))) 57 | with open(linker, 'w+') as f: 58 | f.writelines(lines_before) 59 | app_id = app_id + 1 60 | 61 | 它的思路很简单,在遍历 ``app`` 的大循环里面只做了这样几件事情: 62 | 63 | - 第 16~22 行,找到 ``src/linker.ld`` 中的 ``BASE_ADDRESS = 0x80400000;`` 这一行,并将后面的地址替换为和当前应用对应的一个地址; 64 | - 第 23 行,使用 ``cargo build`` 构建当前的应用,注意我们可以使用 ``--bin`` 参数来只构建某一个应用; 65 | - 第 25~26 行,将 ``src/linker.ld`` 还原。 66 | 67 | 68 | 多道程序加载 69 | ---------------------------- 70 | 71 | 应用的加载方式也和上一章的有所不同。上一章中讲解的加载方法是让所有应用都共享同一个固定的加载物理地址。也是因为这个原因,内存中同时最多只能驻留一个应用,当它运行完毕或者出错退出的时候由操作系统的 ``batch`` 子模块加载一个新的应用来替换掉它。本章中,所有的应用在内核初始化的时候就一并被加载到内存中。为了避免覆盖,它们自然需要被加载到不同的物理地址。这是通过调用 ``loader`` 子模块的 ``load_apps`` 函数实现的: 72 | 73 | .. code-block:: rust 74 | :linenos: 75 | 76 | // os/src/loader.rs 77 | 78 | pub fn load_apps() { 79 | extern "C" { fn _num_app(); } 80 | let num_app_ptr = _num_app as usize as *const usize; 81 | let num_app = get_num_app(); 82 | let app_start = unsafe { 83 | core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1) 84 | }; 85 | // clear i-cache first 86 | unsafe { asm!("fence.i" :::: "volatile"); } 87 | // load apps 88 | for i in 0..num_app { 89 | let base_i = get_base_i(i); 90 | // clear region 91 | (base_i..base_i + APP_SIZE_LIMIT).for_each(|addr| unsafe { 92 | (addr as *mut u8).write_volatile(0) 93 | }); 94 | // load app from data section to memory 95 | let src = unsafe { 96 | core::slice::from_raw_parts( 97 | app_start[i] as *const u8, 98 | app_start[i + 1] - app_start[i] 99 | ) 100 | }; 101 | let dst = unsafe { 102 | core::slice::from_raw_parts_mut(base_i as *mut u8, src.len()) 103 | }; 104 | dst.copy_from_slice(src); 105 | } 106 | } 107 | 108 | 可以看出,第 :math:`i` 个应用被加载到以物理地址 ``base_i`` 开头的一段物理内存上,而 ``base_i`` 的计算方式如下: 109 | 110 | .. code-block:: rust 111 | :linenos: 112 | 113 | // os/src/loader.rs 114 | 115 | fn get_base_i(app_id: usize) -> usize { 116 | APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT 117 | } 118 | 119 | 我们可以在 ``config`` 子模块中找到这两个常数。从这一章开始, ``config`` 子模块用来存放内核中所有的常数。看到 ``APP_BASE_ADDRESS`` 被设置为 ``0x80400000`` ,而 ``APP_SIZE_LIMIT`` 和上一章一样被设置为 ``0x20000`` ,也就是每个应用二进制镜像的大小限制。因此,应用的内存布局就很明朗了——就是从 ``APP_BASE_ADDRESS`` 开始依次为每个应用预留一段空间。这样,我们就说清楚了多个应用是如何被构建和加载的。 120 | 121 | 122 | 执行应用程序 123 | ---------------------------- 124 | 125 | 当多道程序的初始化放置工作完成,或者是某个应用程序运行结束或出错的时候,我们要调用 run_next_app 函数切换到下一个应用程序。此时 CPU 运行在 S 特权级的操作系统中,而操作系统希望能够切换到 U 特权级去运行应用程序。这一过程与上章的 :ref:`执行应用程序 ` 一节的描述类似。相对不同的是,操作系统知道每个应用程序预先加载在内存中的位置,这就需要设置应用程序返回的不同 Trap 上下文(Trap 上下文中保存了 放置程序起始地址的 ``epc`` 寄存器内容): 126 | 127 | - 跳转到应用程序(编号 :math:`i` )的入口点 :math:`\text{entry}_i` 128 | - 将使用的栈切换到用户栈 :math:`\text{stack}_i` 129 | 130 | 我们的“锯齿螈”初级多道程序操作系统就算是实现完毕了。它支持把多个应用的代码和数据放置到内存中,并能够依次执行每个应用,提高了应用切换的效率,这就达到了本章对操作系统的初级需求。但“锯齿螈”操作系统在任务调度的灵活性上还有很大的改进空间,下一节我们将开始改进这方面的问题。 131 | 132 | .. 133 | chyyuu:有一个ascii图,画出我们做的OS。 134 | 135 | 136 | .. [#prionosuchus] 锯齿螈身长可达9米,是迄今出现过的最大的两栖动物,是二叠纪时期江河湖泊和沼泽中的顶级掠食者。 -------------------------------------------------------------------------------- /source/chapter8/6exercise.rst: -------------------------------------------------------------------------------- 1 | 练习 2 | =========================================== 3 | 4 | 课后练习 5 | ------------------------------- 6 | 7 | 编程题 8 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 9 | 10 | 在Linux环境中,基于pthread线程,有一系列的系统调用实现对应用程序的线程间同步互斥的支持。 11 | 12 | 信号量是一种特殊的变量,可用于线程同步。它只取自然数值,并且只支持两种操作:P(SV): 如果信号量SV大于0,将它减一;如果SV值为0,则挂起该线程。V(SV): 如果有其他进程因为等待SV而挂起,则唤醒,然后将SV+1;否则直接将SV+1。其系统调用为: 13 | 14 | - `sem_wait(sem_t *sem)`:以原子操作的方式将信号量减1,如果信号量值为0,则sem_wait将被阻塞,直到这个信号量具有非0值。 15 | - `sem_post(sem_t *sem)`:以原子操作将信号量值+1。当信号量大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒。 16 | 17 | 互斥量:互斥量又称互斥锁,主要用于线程互斥,不能保证按序访问,可以和条件锁一起实现同步。当进入临界区 时,需要获得互斥锁并且加锁;当离开临界区时,需要对互斥锁解锁,以唤醒其他等待该互斥锁的线程。其主要的系统调用如下: 18 | 19 | - `pthread_mutex_init`: 初始化互斥锁 20 | - `pthread_mutex_destroy`: 销毁互斥锁 21 | - pthread_mutex_lock: 以原子操作的方式给一个互斥锁加锁,如果目标互斥锁已经被上锁,pthread_mutex_lock调用将阻塞,直到该互斥锁的占有者将其解锁。 22 | - `pthread_mutex_unlock`: 以一个原子操作的方式给一个互斥锁解锁。 23 | 24 | 25 | 条件变量:条件变量,又称条件锁,用于在线程之间同步共享数据的值。条件变量提供一种线程间通信机制:当某个共享数据达到某个值时,唤醒等待这个共享数据的一个/多个线程。即,当某个共享变量等于某个值时,调用 signal/broadcast。此时操作共享变量时需要加锁。其主要的系统调用如下: 26 | 27 | - `pthread_cond_init`: 初始化条件变量 28 | - `pthread_cond_destroy`: 销毁条件变量 29 | - `pthread_cond_signal`: 唤醒一个等待目标条件变量的线程。哪个线程被唤醒取决于调度策略和优先级。 30 | - `pthread_cond_wait`: 等待目标条件变量。需要一个加锁的互斥锁确保操作的原子性。该函数中在进入wait状态前首先进行解锁,然后接收到信号后会再加锁,保证该线程对共享资源正确访问。 31 | 32 | 1. `**` 在Linux环境下,请用信号量实现哲学家就餐的多线程应用程序。 33 | 2. `**` 在Linux环境下,请用互斥锁和条件变量实现哲学家就餐的多线程应用程序。 34 | 3. `**` 在Linux环境下,请建立一个多线程的模拟资源分配管理库,可通过银行家算法来避免死锁。 35 | 4. `**` 扩展内核功能,实现读者优先的读写信号量。 36 | 5. `**` 扩展内核功能,实现写者优先的读写信号量。 37 | 6. `***` 扩展内核功能,在内核中支持内核线程。 38 | 7. `***` 进一步扩展内核功能,在内核线程中支持同步互斥机制,实现内核线程用的mutex, semaphore, cond-var。 39 | 8. `***` 扩展内核功能,实现多核支持下的同步互斥机制。 40 | 9. `***` 解决优先级反转问题:实现RM实时调度算法,设计优先级反转的实例,实现优先级天花板和优先级继承方法。 41 | 42 | 问答题 43 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 44 | 45 | 1. `*` 什么是并行?什么是并发? 46 | 2. `*` 为了创造临界区,单核处理器上可以【关中断】,多核处理器上需要使用【自旋锁】。请回答下列问题: 47 | 48 | - 多核上可不可以只用【关中断】? 49 | - 单核上可不可以只用【自旋锁】? 50 | - 多核上的【自旋锁】是否需要同时【关中断】? 51 | - [进阶] 假如某个锁不会在中断处理函数中被访问,是否还需要【关中断】? 52 | 53 | 3. `**` Linux的多线程应用程序使用的锁(例如 pthread_mutex_t)不是自旋锁,当上锁失败时会切换到其它进程执行。分析它和自旋锁的优劣,并说明为什么它不用自旋锁? 54 | 4. `***` 程序在运行时具有两种性质:safety: something bad will never happen;liveness: something good will eventually occur. 分析并证明 Peterson 算法的 safety 和 liveness 性质。 55 | 5. `*` 信号量结构中的整数分别为+n、0、-n 的时候,各自代表什么状态或含义? 56 | 6. `**` 考虑如下信号量实现代码: 57 | 58 | .. code-block:: rust 59 | 60 | class Semaphore { 61 | int sem; 62 | WaitQueue q; 63 | } 64 | Semaphore::P() { 65 | sem --; 66 | if(sem < 0) { 67 | Add this thread to q. 68 | block. 69 | } 70 | } 71 | Semaphore::V() { 72 | sem ++; 73 | if(sem <= 0) { 74 | t = Remove a thread from q; 75 | wakeup(t); 76 | } 77 | } 78 | 79 | 假如 P操作或V操作不是原子操作,会出现什么问题?举一个例子说明。上述代码能否运行在用户态?上面代码的原子性是如何保证的? 80 | 81 | 7. `**` 条件变量的 Wait 操作为什么必须关联一个锁? 82 | 83 | 8. `**` 下面是条件变量的wait操作实现伪代码: 84 | 85 | .. code-block:: rust 86 | 87 | Condvar::wait(lock) { 88 | Add this thread to q. 89 | lock.unlock(); 90 | schedule(); 91 | lock.lock(); 92 | } 93 | 94 | 如果改成下面这样: 95 | 96 | .. code-block:: rust 97 | 98 | Condvar::wait() { 99 | Add this thread to q. 100 | schedule(); 101 | } 102 | lock.unlock(); 103 | condvar.wait(); 104 | lock.lock(); 105 | 106 | 会出现什么问题?举一个例子说明。 107 | 108 | 9. `*` 死锁的必要条件是什么? 109 | 10. `*` 什么是死锁预防,举例并分析。 110 | 11. `**` 描述银行家算法如何判断安全性。 111 | 112 | 实验练习 113 | ------------------------------- 114 | 115 | 实验练习包括实践作业和问答作业两部分。 116 | 117 | 118 | 编程作业 119 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 120 | 121 | 银行家算法——分数更新 122 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 123 | 124 | .. note:: 125 | 126 | 本实验为用户态实验,请在 Linux 环境下完成。 127 | 128 | 背景:在智能体大赛平台 `Saiblo `_ 网站上每打完一场双人天梯比赛后需要用 ELO 算法更新双方比分。由于 Saiblo 的评测机并发性很高,且 ELO 算法中的分值变动与双方变动前的分数有关,因此更新比分前时必须先为两位选手加锁。 129 | 130 | 作业:请模拟一下上述分数更新过程,简便起见我们简化为有 p 位选手参赛(编号 [0, p) 或 [1, p] ),初始分值为 1000 分,有 m 个评测机线程(生产者)给出随机的评测结果(两位不同选手的编号以及胜负结果,结果可能为平局),有 n 个 worker 线程(消费者)获取结果队列并更新数据库(全局变量等共享数据)记录的分数。m 个评测机各自模拟 k 场对局结果后结束线程,全部对局比分更新完成后主线程打印每位选手最终成绩以及所有选手分数之和。 131 | 132 | 上述参数 p、m、n、k 均为可配置参数(命令行传参或程序启动时从stdin输入)。 133 | 134 | 简便起见不使用 ELO 算法,简化更新规则为:若不为平局,当 胜者分数 >= 败者分数 时胜者 +20,败者 -20,否则胜者 +30,败者 -30;若为平局,分高者 -10,分低者+10(若本就同分保持则不变)。 135 | 136 | 消费者核心部分可参考如下伪码: 137 | 获取选手A的锁 138 | 获取选手B的锁 139 | 更新A、B分数 140 | 睡眠 1ms(模拟数据库更新延时) 141 | 释放选手B的锁 142 | 释放选手A的锁 143 | 144 | tips: 145 | - 由于 ELO 以及本题中给出的简化更新算法均为零和算法,因此出现冲突后可以从所有选手分数之和明显看出来,正确处理时它应该永远为 1000p 146 | - 将一个 worker 线程看作哲学家,将 worker 正在处理的一场对局的两位选手看作两根筷子,则得到了经典的哲学家就餐问题 147 | 148 | 实现 eventfd 149 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 150 | 151 | 在 Linux 中有一种用于事件通知的文件描述符,称为 eventfd 。其核心是一个 64 位无符号整数的计数器,在非信号量模式下,若计数器值不为零,则 `read` 函数会从中读出计数值并将其清零,否则读取失败; `write` 函数将缓冲区中的数值加入到计数器中。在信号量模式下,若计数器值非零,则 `read` 操作将计数值减一,并返回 1 ; `write` 将计数值加一。我们将实现一个新的系统调用: `sys_eventfd2` 。 152 | 153 | **eventfd**: 154 | 155 | * syscall ID: 290 156 | * 功能:创建一个 eventfd, `eventfd 标准接口 `_ 。 157 | * C 接口: ``int eventfd(unsigned int initval, int flags)`` 158 | * Rust 接口: ``fn eventfd(initval: u32, flags: i32) -> i32`` 159 | * 参数: 160 | * initval: 计数器的初值。 161 | * flags: 可以设置为 0 或以下两个 flag 的任意组合(按位或): 162 | * EFD_SEMAPHORE (1) :设置该 flag 时,将以信号量模式创建 eventfd 。 163 | * EFD_NONBLOCK (2048) :若设置该 flag ,对 eventfd 读写失败时会返回 -2 ,否则将阻塞等待直至读或写操作可执行为止。 164 | * 说明: 165 | * 通过 `write` 写入 eventfd 时,缓冲区大小必须为 8 字节。 166 | * 进程 `fork` 时,子进程会继承父进程创建的 eventfd ,且指向同一个计数器。 167 | * 返回值:如果出现了错误则返回 -1,否则返回创建成功的 eventfd 编号。 168 | * 可能的错误 169 | * flag 不合法。 170 | * 创建的文件描述符数量超过进程限制 171 | 172 | .. note:: 173 | 还有一个 `sys_eventfd` 系统调用(调用号 284),与 `sys_eventfd2` 的区别在于前者不支持传入 flags 。 174 | 175 | Linux 中的原生异步 IO 接口 libaio 就使用了 eventfd 作为内核完成 IO 操作之后通知应用程序的机制。 176 | 177 | 178 | 179 | 实验要求 180 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 181 | 182 | - 完成分支: ch8-lab 183 | - 实验目录要求不变。 184 | - 通过所有测例。 185 | 186 | 问答作业 187 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 188 | 189 | 无 190 | 191 | 实验练习的提交报告要求 192 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 193 | 194 | * 简单总结本次实验与上个实验相比你增加的东西。(控制在5行以内,不要贴代码) 195 | * 完成问答问题 196 | * (optional) 你对本次实验设计及难度的看法。 -------------------------------------------------------------------------------- /source/chapter5/6answer.rst: -------------------------------------------------------------------------------- 1 | 练习参考答案 2 | ============================================== 3 | 4 | 课后练习 5 | ------------------------------- 6 | 7 | 编程题 8 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 9 | 10 | 1. `*` 实现一个使用nice,fork,exec,spawn等与进程管理相关的系统调用的linux应用程序。 11 | 12 | 参考实现: 13 | 14 | .. code-block:: c 15 | 16 | #include 17 | #include 18 | #include 19 | int main(void) 20 | { 21 | int childpid; 22 | int i; 23 | 24 | if (fork() == 0){ 25 | //child process 26 | char * execv_str[] = {"echo", "child process, executed by execv",NULL}; 27 | if (execv("/usr/bin/echo",execv_str) <0 ){ 28 | perror("error on exec\n"); 29 | exit(0); 30 | } 31 | }else{ 32 | //parent process 33 | wait(&childpid); 34 | printf("parent process, execv done\n"); 35 | } 36 | return 0; 37 | } 38 | 39 | 2. `*` 扩展操作系统内核,能够显示操作系统切换进程的过程。 40 | 41 | 体现调度的过程十分简单,只需要在调度器部分,在寻找或运行下一任务的函数中加入一些输出调试信息就可以看到效果了,但切换可能会比较频繁,因此输出会很多。 42 | 43 | 3. `*` 请阅读下列代码,分析程序的输出 ``A`` 的数量:( 已知 ``&&`` 的优先级比 ``||`` 高) 44 | 45 | .. code-block:: c 46 | 47 | int main() { 48 | fork() && fork() && fork() || fork() && fork() || fork() && fork(); 49 | printf("A"); 50 | return 0; 51 | } 52 | 53 | 如果给出一个 ``&&`` ``||`` 的序列,如何设计一个程序来得到答案? 54 | 55 | 22个。&&优先级高于||,根据fork子进程返回值为0父进程返回pid和逻辑运算符的短路现象(&&左边为F即短路,||左边为T即短路), 56 | 可以按||分割来进行判断,共1+1*3+3*2+3*2*2=22. 57 | 58 | .. code-block:: python 59 | 60 | def count_fork(seq): 61 | counts = [1] + [i.count("&&") + 1 for i in seq.split("||")] 62 | total = sum([np.prod(counts[:i + 1]) for i in range(len(counts))]) 63 | return total 64 | 65 | 4. `**` 在本章操作系统中实现本章提出的某一种调度算法(RR调度除外)。 66 | 67 | 先来先服务调度算法FCFS:它与 RR 调度的区别在于没有时钟中断导致的任务切换,其他细节上相似。 68 | 因此基于已有的 RR 调度,删除对 Timer 中断的相关处理即可得到一个 FCFS 调度。 69 | 70 | 以 ucore 本章节代码为例,一种处理方式是将 `trap.c` 的 `usertrap` 函数中 `case SupervisorTimer` 部分的 `yield();` 一句删除 71 | 即可去掉 RR 特性,得到一个 FCFS 调度。 72 | 73 | 代码略。 74 | 75 | 5. `***` 扩展操作系统内核,支持多核处理器。 76 | 77 | 题目编程内容过于复杂,不建议作为练习题。 78 | 6. `***` 扩展操作系统内核,支持在内核态响应并处理中断。 79 | 80 | 题目编程内容过于复杂,不建议作为练习题。 81 | 82 | 83 | 问答题 84 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 85 | 86 | 1. `*` 如何查看Linux操作系统中的进程? 87 | 88 | 使用ps命令,常用方法: 89 | 90 | .. code-block:: bash 91 | 92 | $ ps aux 93 | 94 | 2. `*` 简单描述一下进程的地址空间中有哪些数据和代码。 95 | 96 | 代码(text)段,数据(data)段: 已初始化的全局变量的内存映射,bss段:未初始化或默认初始化为0的全局变量,堆(heap),用户栈(stack),共享内存段 97 | 98 | 3. `*` 进程控制块保存哪些内容? 99 | 100 | 进程标识符、进程调度信息(进程状态,进程的优先级,进程调度所需的其它信息)、进程间通信信息、内存管理信息(基地址、页表或段表等存储空间结构)、进程所用资源( I/O 设备列表、打开文件列表等)、处理机信息(通用寄存器、指令计数器、用户的栈指针) 101 | 102 | 4. `*` 进程上下文切换需要保存哪些内容? 103 | 104 | 页全局目录、部分寄存器、内核栈、当前运行位置 105 | 106 | 5. `**` fork 为什么需要在父进程和子进程提供不同的返回值? 107 | 108 | 可以根据返回值区分父子进程,明确进程之间的关系,方便用户为不同进程执行不同的操作。 109 | 110 | 6. `**` fork + exec 的一个比较大的问题是 fork 之后的内存页/文件等资源完全没有使用就废弃了,针对这一点,有什么改进策略? 111 | 112 | 采用COW(copy on write),或使用使⽤vfork等。 113 | 114 | 7. `**` 其实使用了6的策略之后,fork + exec 所带来的无效资源的问题已经基本被解决了,但是近年来fork 还是在被不断的批判,那么到底是什么正在"杀死"fork?可以参考 `论文 `_ 。 115 | 116 | fork 和其他的操作不正交,也就是 os 每增加一个功能,都要改 fork, 这导致新功能开发困难,设计受限.有些和硬件相关的甚至根本无法支持 fork. 117 | 118 | fork 得到的父子进程可能产生共享资源的冲突; 119 | 120 | 子进程继承父进程,如果父进程处理不当,子进程可以找到父进程的安全漏洞进而威胁父进程; 121 | 122 | 还有比如 fork 必须要虚存, SAS 无法支持等等. 123 | 124 | 8. `**` 请阅读下列代码,并分析程序的输出,假定不发生运行错误,不考虑行缓冲,不考虑中断: 125 | 126 | .. code-block:: c 127 | 128 | int main(){ 129 | int val = 2; 130 | 131 | printf("%d", 0); 132 | int pid = fork(); 133 | if (pid == 0) { 134 | val++; 135 | printf("%d", val); 136 | } else { 137 | val--; 138 | printf("%d", val); 139 | wait(NULL); 140 | } 141 | val++; 142 | printf("%d", val); 143 | return 0; 144 | } 145 | 146 | 147 | 如果 fork() 之后主程序先运行,则结果如何?如果 fork() 之后 child 先运行,则结果如何? 148 | 149 | 01342 03412 150 | 151 | 9. `**` 为什么子进程退出后需要父进程对它进行 wait,它才能被完全回收? 152 | 153 | 当一个进程通过exit系统调用退出之后,它所占用的资源并不能够立即全部回收,需要由该进程的父进程通过wait收集该进程的返回状态并回收掉它所占据的全部资源,防止子进程变为僵尸进程造成内存泄漏。同时父进程通过wait可以获取子进程执行结果,判断运行是否达到预期,进行管理。 154 | 155 | 10. `**` 有哪些可能的时机导致进程切换? 156 | 157 | 进程主动放弃cpu:运行结束、调用yield/sleep等、运行发生异常中断 158 | 159 | 进程被动失去cpu:时间片用完、新进程到达、发生I/O中断等 160 | 161 | 11. `**` 请描述在本章操作系统中实现本章提出的某一种调度算法(RR调度除外)的简要实现步骤。 162 | 163 | 可降低优先级的MLFQ:将manager的进程就绪队列变为数个,初始进程进入第一队列,调度器每次选择第一队列的队首进程执行,当一个进程用完时间片而未执行完,就在将它重新添加至就绪队列时添加到下一队列,直到进程位于底部队列。 164 | 165 | 12. `*` 非抢占式的调度算法,以及抢占式的调度算法,他们的优点各是什么? 166 | 167 | 非抢占式:中断响应性能好、进程执行连续,便于分析管理 168 | 169 | 抢占式:任务级响应时间最优,更能满足紧迫作业要求 170 | 171 | 13. `**` 假设我们简单的将进程分为两种:前台交互(要求短时延)、后台计算(计算量大)。下列进程/或进程组分别是前台还是后台?a) make 编译 linux; b) vim 光标移动; c) firefox 下载影片; d) 某游戏处理玩家点击鼠标开枪; e) 播放交响乐歌曲; f) 转码一个电影视频。除此以外,想想你日常应用程序的运行,它们哪些是前台,哪些是后台的? 172 | 173 | 前台:b,d,e 174 | 175 | 后台:a,c,f 176 | 177 | 14. `**` RR 算法的时间片长短对系统性能指标有什么影响? 178 | 179 | 时间片太大,可以让每个任务都在时间片内完成,但进程平均周转时间会比较长,极限情况下甚至退化为FCFS; 180 | 181 | 时间片过小,反应迅速,响应时间会比较短,可以提高批量短任务的完成速度。但产生大量上下文切换开销,使进程的实际执行时间受到挤占。 182 | 183 | 因此需要在响应时间和进程切换开销之间进行权衡,合理设定时间片大小。 184 | 185 | 15. `**` MLFQ 算法并不公平,恶意的用户程序可以愚弄 MLFQ 算法,大幅挤占其他进程的时间。(MLFQ 的规则:“如果一个进程,时间片用完了它还在执行用户计算,那么 MLFQ 下调它的优先级”)你能举出一个例子,使得你的用户程序能够挤占其他进程的时间吗? 186 | 187 | 每次连续执行只进行大半个时间片长度即通过执行一个IO操作等让出cpu,这样优先级不会下降,仍能很快得到下一次调度。 188 | 189 | 16. `***` 多核执行和调度引入了哪些新的问题和挑战? 190 | 191 | 多处理机之间的负载不均问题:在调度时,如何保证每一个处理机的就绪队列保证优先级、性能指标的同时负载均衡 192 | 193 | 数据在不同处理机之间的共享与同步问题:除了Cache一致性的问题,在不同处理机上同时运行的进程可能对共享的数据区域产生相同的数据要求,这时就需要避免数据冲突,采用同步互斥机制处理资源竞争; 194 | 195 | 线程化问题:如何将单个进程分为多线程放在多个处理机上 196 | 197 | Cache一致性问题:由于各个处理机有自己的私有Cache,需要保证不同处理机下的Cache之中的数据一致性 198 | 199 | 处理器亲和性问题:在单一处理机上运行的进程可以利用Cache实现内存访问的优化与加速,这就需要我们规划调度策略,尽量使一个进程在它前一次运行过的同一个CPU上运行,也即满足处理器亲和性。 200 | 201 | 通信问题:类似同步问题,如何降低核间的通信代价 202 | 203 | 204 | -------------------------------------------------------------------------------- /source/chapter2/6answer.rst: -------------------------------------------------------------------------------- 1 | 练习参考答案 2 | ===================================================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | 9 | 课后练习 10 | ------------------------------- 11 | 12 | 编程题 13 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 14 | 1. `***` 实现一个裸机应用程序A,能打印调用栈。 15 | 16 | 以 rCore tutorial ch2 代码为例,在编译选项中我们已经让编译器对所有函数调用都保存栈指针(参考 ``os/.cargo/config`` ),因此我们可以直接从 `fp` 寄存器追溯调用栈: 17 | 18 | .. code-block:: rust 19 | :caption: ``os/src/stack_trace.rs`` 20 | 21 | use core::{arch::asm, ptr}; 22 | 23 | pub unsafe fn print_stack_trace() -> () { 24 | let mut fp: *const usize; 25 | asm!("mv {}, fp", out(reg) fp); 26 | 27 | println!("== Begin stack trace =="); 28 | while fp != ptr::null() { 29 | let saved_ra = *fp.sub(1); 30 | let saved_fp = *fp.sub(2); 31 | 32 | println!("0x{:016x}, fp = 0x{:016x}", saved_ra, saved_fp); 33 | 34 | fp = saved_fp as *const usize; 35 | } 36 | println!("== End stack trace =="); 37 | } 38 | 39 | 之后我们将其加入 ``main.rs`` 作为一个子模块: 40 | 41 | .. code-block:: rust 42 | :caption: 加入 ``os/src/main.rs`` 43 | :emphasize-lines: 4 44 | 45 | // ... 46 | mod syscall; 47 | mod trap; 48 | mod stack_trace; 49 | // ... 50 | 51 | 作为一个示例,我们可以将打印调用栈的代码加入 panic handler 中,在每次 panic 的时候打印调用栈: 52 | 53 | .. code-block:: rust 54 | :caption: ``os/lang_items.rs`` 55 | :emphasize-lines: 3,9 56 | 57 | use crate::sbi::shutdown; 58 | use core::panic::PanicInfo; 59 | use crate::stack_trace::print_stack_trace; 60 | 61 | #[panic_handler] 62 | fn panic(info: &PanicInfo) -> ! { 63 | // ... 64 | 65 | unsafe { print_stack_trace(); } 66 | 67 | shutdown() 68 | } 69 | 70 | 现在,panic 的时候输入的信息变成了这样: 71 | 72 | .. code-block:: 73 | 74 | Panicked at src/batch.rs:68 All applications completed! 75 | == Begin stack trace == 76 | 0x0000000080200e12, fp = 0x0000000080205cf0 77 | 0x0000000080201bfa, fp = 0x0000000080205dd0 78 | 0x0000000080200308, fp = 0x0000000080205e00 79 | 0x0000000080201228, fp = 0x0000000080205e60 80 | 0x00000000802005b4, fp = 0x0000000080205ef0 81 | 0x0000000080200424, fp = 0x0000000000000000 82 | == End stack trace == 83 | 84 | 这里打印的两个数字,第一个是栈帧上保存的返回地址,第二个是保存的上一个 frame pointer。 85 | 86 | 87 | 2. `**` 扩展内核,实现新系统调用get_taskinfo,能显示当前task的id和task name;实现一个裸机应用程序B,能访问get_taskinfo系统调用。 88 | 3. `**` 扩展内核,能够统计多个应用的执行过程中系统调用编号和访问此系统调用的次数。 89 | 4. `**` 扩展内核,能够统计每个应用执行后的完成时间。 90 | 5. `***` 扩展内核,统计执行异常的程序的异常情况(主要是各种特权级涉及的异常),能够打印异常程序的出错的地址和指令等信息。 91 | 92 | 93 | 注:上述编程基于 rcore/ucore tutorial v3: Branch ch2 94 | 95 | 问答题 96 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 97 | 98 | 1. `*` 函数调用与系统调用有何区别? 99 | 100 | * 函数调用用普通的控制流指令,不涉及特权级的切换;系统调用使用专门的指令(如 RISC-V 上的 `ecall`),会切换到内核特权级。 101 | * 函数调用可以随意指定调用目标;系统调用只能将控制流切换给调用操作系统内核给定的目标。 102 | 103 | 2. `**` 为了方便操作系统处理,M态软件会将 S 态异常/中断委托给 S 态软件,请指出有哪些寄存器记录了委托信息,rustsbi 委托了哪些异常/中断?(也可以直接给出寄存器的值) 104 | 105 | * 两个寄存器记录了委托信息: ``mideleg`` (中断委托)和 ``medeleg`` (异常委托) 106 | 107 | * 参考 RustSBI 输出 108 | 109 | .. code-block:: 110 | 111 | [rustsbi] mideleg: ssoft, stimer, sext (0x222) 112 | [rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab) 113 | 114 | 可知委托了中断: 115 | 116 | * ``ssoft`` : S-mode 软件中断 117 | * ``stimer`` : S-mode 时钟中断 118 | * ``sext`` : S-mode 外部中断 119 | 120 | 委托了异常: 121 | 122 | * ``ima`` : 指令未对齐 123 | * ``ia`` : 取指访问异常 124 | * ``bkpt`` : 断点 125 | * ``la`` : 读异常 126 | * ``sa`` : 写异常 127 | * ``uecall`` : U-mode 系统调用 128 | * ``ipage`` : 取指 page fault 129 | * ``lpage`` : 读 page fault 130 | * ``spage`` : 写 page fault 131 | 132 | 3. `**` 如果操作系统以应用程序库的形式存在,应用程序可以通过哪些方式破坏操作系统? 133 | 4. `**` 编译器/操作系统/处理器如何合作,可采用哪些方法来保护操作系统不受应用程序的破坏? 134 | 5. `**` RISC-V处理器的S态特权指令有哪些,其大致含义是什么,有啥作用? 135 | 6. `**` RISC-V处理器在用户态执行特权指令后的硬件层面的处理过程是什么? 136 | 7. `**` 操作系统在完成用户态<-->内核态双向切换中的一般处理过程是什么? 137 | 8. `**` 程序陷入内核的原因有中断、异常和陷入(系统调用),请问 riscv64 支持哪些中断 / 异常?如何判断进入内核是由于中断还是异常?描述陷入内核时的几个重要寄存器及其值。 138 | 139 | * 具体支持的异常和中断,参见 RISC-V 特权集规范 *The RISC-V Instruction Set Manual Volume II: Privileged Architecture* 。其它很多问题在这里也有答案。 140 | * `scause` 的最高位,为 1 表示中断,为 0 表示异常 141 | * 重要的寄存器: 142 | 143 | * `scause` :发生了具体哪个异常或中断 144 | * `sstatus` :其中的一些控制为标志发生异常时的处理器状态,如 `sstatus.SPP` 表示发生异常时处理器在哪个特权级。 145 | * `sepc` :发生异常或中断的时候,将要执行但未成功执行的指令地址 146 | * `stval` :值与具体异常相关,可能是发生异常的地址,指令等 147 | 148 | 9. `*` 在哪些情况下会出现特权级切换:用户态-->内核态,以及内核态-->用户态? 149 | 10. `**` Trap上下文的含义是啥?在本章的操作系统中,Trap上下文的具体内容是啥?如果不进行Trap上下文的保存于恢复,会出现什么情况? 150 | 151 | 实验练习 152 | ------------------------------- 153 | 154 | 问答作业 155 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 156 | 157 | 1. 正确进入 U 态后,程序的特征还应有:使用 S 态特权指令,访问 S 态寄存器后会报错。请自行测试这些内容 (运行 Rust 三个 bad 测例 ) ,描述程序出错行为,注明你使用的 sbi 及其版本。 158 | 159 | 2. 请结合用例理解 `trap.S `_ 中两个函数 ``__alltraps`` 和 ``__restore`` 的作用,并回答如下几个问题: 160 | 161 | 1. L40:刚进入 ``__restore`` 时,``a0`` 代表了什么值。请指出 ``__restore`` 的两种使用情景。 162 | 163 | 2. L46-L51:这几行汇编代码特殊处理了哪些寄存器?这些寄存器的的值对于进入用户态有何意义?请分别解释。 164 | 165 | .. code-block:: riscv 166 | 167 | ld t0, 32*8(sp) 168 | ld t1, 33*8(sp) 169 | ld t2, 2*8(sp) 170 | csrw sstatus, t0 171 | csrw sepc, t1 172 | csrw sscratch, t2 173 | 174 | 3. L53-L59:为何跳过了 ``x2`` 和 ``x4``? 175 | 176 | .. code-block:: riscv 177 | 178 | ld x1, 1*8(sp) 179 | ld x3, 3*8(sp) 180 | .set n, 5 181 | .rept 27 182 | LOAD_GP %n 183 | .set n, n+1 184 | .endr 185 | 186 | 4. L63:该指令之后,``sp`` 和 ``sscratch`` 中的值分别有什么意义? 187 | 188 | .. code-block:: riscv 189 | 190 | csrrw sp, sscratch, sp 191 | 192 | 5. ``__restore``:中发生状态切换在哪一条指令?为何该指令执行之后会进入用户态? 193 | 194 | 6. L13:该指令之后,``sp`` 和 ``sscratch`` 中的值分别有什么意义? 195 | 196 | .. code-block:: riscv 197 | 198 | csrrw sp, sscratch, sp 199 | 200 | 7. 从 U 态进入 S 态是哪一条指令发生的? 201 | 202 | 203 | 204 | 3. 对于任何中断,``__alltraps`` 中都需要保存所有寄存器吗?你有没有想到一些加速 ``__alltraps`` 的方法?简单描述你的想法。 205 | -------------------------------------------------------------------------------- /source/chapter1/8answer.rst: -------------------------------------------------------------------------------- 1 | 练习参考答案 2 | ===================================================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | 9 | 课后练习 10 | ------------------------------- 11 | 12 | 编程题 13 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 14 | 1. `*` 实现一个linux应用程序A,显示当前目录下的文件名。(用C或Rust编程) 15 | 16 | 参考实现: 17 | 18 | .. code-block:: c 19 | 20 | #include 21 | #include 22 | 23 | int main() { 24 | DIR *dir = opendir("."); 25 | 26 | struct dirent *entry; 27 | 28 | while ((entry = readdir(dir))) { 29 | printf("%s\n", entry->d_name); 30 | } 31 | 32 | return 0; 33 | } 34 | 35 | 可能的输出: 36 | 37 | .. code-block:: console 38 | 39 | $ ./ls 40 | . 41 | .. 42 | .git 43 | .dockerignore 44 | Dockerfile 45 | LICENSE 46 | Makefile 47 | [...] 48 | 49 | 50 | 2. `***` 实现一个linux应用程序B,能打印出调用栈链信息。(用C或Rust编程) 51 | 52 | 以使用 GCC 编译的 C 语言程序为例,使用编译参数 ``-fno-omit-frame-pointer`` 的情况下,会保存栈帧指针 ``fp`` 。 53 | 54 | ``fp`` 指向的栈位置的负偏移量处保存了两个值: 55 | 56 | * ``-8(fp)`` 是保存的 ``ra`` 57 | * ``-16(fp)`` 是保存的上一个 ``fp`` 58 | 59 | .. TODO:这个规范在哪里? 60 | 61 | 因此我们可以像链表一样,从当前的 ``fp`` 寄存器的值开始,每次找到上一个 ``fp`` ,逐帧恢复我们的调用栈: 62 | 63 | .. code-block:: c 64 | 65 | #include 66 | #include 67 | #include 68 | 69 | // Compile with -fno-omit-frame-pointer 70 | void print_stack_trace_fp_chain() { 71 | printf("=== Stack trace from fp chain ===\n"); 72 | 73 | uintptr_t *fp; 74 | asm("mv %0, fp" : "=r"(fp) : : ); 75 | 76 | // When should this stop? 77 | while (fp) { 78 | printf("Return address: 0x%016" PRIxPTR "\n", fp[-1]); 79 | printf("Old stack pointer: 0x%016" PRIxPTR "\n", fp[-2]); 80 | printf("\n"); 81 | 82 | fp = (uintptr_t *) fp[-2]; 83 | } 84 | printf("=== End ===\n\n"); 85 | } 86 | 87 | 但是这里会遇到一个问题,因为我们的标准库并没有保存栈帧指针,所以找到调用栈到标准的库时候会打破我们对栈帧格式的假设,出现异常。 88 | 89 | 我们也可以不做关于栈帧保存方式的假设,而是明确让编译器告诉我们每个指令处的调用栈如何恢复。在编译的时候加入 ``-funwind-tables`` 会开启这个功能,将调用栈恢复的信息存入可执行文件中。 90 | 91 | 有一个叫做 `libunwind `_ 的库可以帮我们读取这些信息生成调用栈信息,而且它可以正确发现某些栈帧不知道怎么恢复,避免异常退出。 92 | 93 | 正确安装 libunwind 之后,我们也可以用这样的方式生成调用栈信息: 94 | 95 | .. code-block:: c 96 | 97 | #include 98 | #include 99 | #include 100 | 101 | #define UNW_LOCAL_ONLY 102 | #include 103 | 104 | // Compile with -funwind-tables -lunwind 105 | void print_stack_trace_libunwind() { 106 | printf("=== Stack trace from libunwind ===\n"); 107 | 108 | unw_cursor_t cursor; unw_context_t uc; 109 | unw_word_t pc, sp; 110 | 111 | unw_getcontext(&uc); 112 | unw_init_local(&cursor, &uc); 113 | 114 | while (unw_step(&cursor) > 0) { 115 | unw_get_reg(&cursor, UNW_REG_IP, &pc); 116 | unw_get_reg(&cursor, UNW_REG_SP, &sp); 117 | 118 | printf("Program counter: 0x%016" PRIxPTR "\n", (uintptr_t) pc); 119 | printf("Stack pointer: 0x%016" PRIxPTR "\n", (uintptr_t) sp); 120 | printf("\n"); 121 | } 122 | printf("=== End ===\n\n"); 123 | } 124 | 125 | 126 | 3. `**` 实现一个基于rcore/ucore tutorial的应用程序C,用sleep系统调用睡眠5秒(in rcore/ucore tutorial v3: Branch ch1) 127 | 128 | 注: 尝试用GDB等调试工具和输出字符串的等方式来调试上述程序,能设置断点,单步执行和显示变量,理解汇编代码和源程序之间的对应关系。 129 | 130 | 131 | 问答题 132 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 133 | 134 | 1. `*` 应用程序在执行过程中,会占用哪些计算机资源? 135 | 2. `*` 请用相关工具软件分析并给出应用程序A的代码段/数据段/堆/栈的地址空间范围。 136 | 3. `*` 请用分析并给出应用程序C的代码段/数据段/堆/栈的地址空间范围。 137 | 4. `*` 请结合编译器的知识和编写的应用程序B,说明应用程序B是如何建立调用栈链信息的。 138 | 5. `*` 请简要说明应用程序与操作系统的异同之处。 139 | 6. `**` 请基于QEMU模拟RISC—V的执行过程和QEMU源代码,说明RISC-V硬件加电后的几条指令在哪里?完成了哪些功能? 140 | 141 | 在 QEMU 源码 [#qemu_bootrom]_ 中可以找到“上电”的时候刚执行的几条指令,如下: 142 | 143 | .. code-block:: c 144 | 145 | uint32_t reset_vec[10] = { 146 | 0x00000297, /* 1: auipc t0, %pcrel_hi(fw_dyn) */ 147 | 0x02828613, /* addi a2, t0, %pcrel_lo(1b) */ 148 | 0xf1402573, /* csrr a0, mhartid */ 149 | #if defined(TARGET_RISCV32) 150 | 0x0202a583, /* lw a1, 32(t0) */ 151 | 0x0182a283, /* lw t0, 24(t0) */ 152 | #elif defined(TARGET_RISCV64) 153 | 0x0202b583, /* ld a1, 32(t0) */ 154 | 0x0182b283, /* ld t0, 24(t0) */ 155 | #endif 156 | 0x00028067, /* jr t0 */ 157 | start_addr, /* start: .dword */ 158 | start_addr_hi32, 159 | fdt_load_addr, /* fdt_laddr: .dword */ 160 | 0x00000000, 161 | /* fw_dyn: */ 162 | }; 163 | 164 | 完成的工作是: 165 | 166 | - 读取当前的 Hart ID CSR ``mhartid`` 写入寄存器 ``a0`` 167 | - (我们还没有用到:将 FDT (Flatten device tree) 在物理内存中的地址写入 ``a1``) 168 | - 跳转到 ``start_addr`` ,在我们实验中是 RustSBI 的地址 169 | 170 | 7. `*` RISC-V中的SBI的含义和功能是啥? 171 | 8. `**` 为了让应用程序能在计算机上执行,操作系统与编译器之间需要达成哪些协议? 172 | 9. `**` 请简要说明从QEMU模拟的RISC-V计算机加电开始运行到执行应用程序的第一条指令这个阶段的执行过程。 173 | 10. `**` 为何应用程序员编写应用时不需要建立栈空间和指定地址空间? 174 | 11. `***` 现代的很多编译器生成的代码,默认情况下不再严格保存/恢复栈帧指针。在这个情况下,我们只要编译器提供足够的信息,也可以完成对调用栈的恢复。(题目剩余部分省略) 175 | 176 | * 首先,我们当前的 ``pc`` 在 ``flip`` 函数的开头,这是我们正在运行的函数。返回给调用者处的地址在 ``ra`` 寄存器里,是 ``0x10742`` 。因为我们还没有开始操作栈指针,所以调用处的 ``sp`` 与我们相同,都是 ``0x40007f1310`` 。 177 | * ``0x10742`` 在 ``flap`` 函数内。根据 ``flap`` 函数的开头可知,这个函数的栈帧大小是 16 个字节,所以调用者处的栈指针应该是 ``sp + 16 = 0x40007f1320``。调用 ``flap`` 的调用者返回地址保存在栈上 ``8(sp)`` ,可以读出来是 ``0x10750`` ,还在 ``flap`` 函数内。 178 | * 依次类推,只要能理解已知地址对应的函数代码,就可以完成恢复操作。 179 | 180 | 实验练习 181 | ------------------------------- 182 | 183 | 问答作业 184 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 185 | 186 | 1. 请学习 gdb 调试工具的使用(这对后续调试很重要),并通过 gdb 简单跟踪从机器加电到跳转到 0x80200000 的简单过程。只需要描述重要的跳转即可,只需要描述在 qemu 上的情况。 187 | 188 | 189 | .. [#qemu_bootrom] https://github.com/qemu/qemu/blob/0ebf76aae58324b8f7bf6af798696687f5f4c2a9/hw/riscv/boot.c#L300 -------------------------------------------------------------------------------- /source/chapter9/0intro.rst: -------------------------------------------------------------------------------- 1 | 引言 2 | ========================================= 3 | 4 | 本章导读 5 | ----------------------------------------- 6 | 7 | 进化的 “达科塔盗龙” 操作系统已经具备了传统操作系统中的内在重要因素,如进程、文件、地址空间、进程间通信、线程并发执行等,应用程序也能通过操作系统输入输出字符,读写在磁盘上的数据。不过与我们常见的操作系统(如Linux,Windows等)比起来,好像感知与交互的I/O能力还比较弱。 8 | 9 | 终于到了I/O设备管理这一章了。人靠衣裳马靠鞍,如果操作系统不能把计算机的外设功能给发挥出来,那应用程序感知外在环境的能力和展示内在计算的能力都会大打折扣。比如基于中断机制的高效I/O处理,图形化的显示,这些操作系统新技能将在本章展现出来。所以本章要完成的操作系统的核心目标是: **让应用能便捷地访问外设** 。 10 | 11 | 12 | 其实在第一章就非常简单介绍了QEMU模拟的RISC-V 64计算机中存在的外设:UART、时钟、virtio-net/block/console/gpu等。并且LibOS模式的操作系统就已通过RustSBI间接地接触过串口设备了,即通过RustSBI提供的一个SBI调用 ``SBI_CONSOLE_PUTCHAR`` 来完成字符输出功能的。 13 | 14 | 在第三章,为了能实现抢占式调度,引入了时钟这个外设,结合硬件中断机制,并通过SBI调用 ``SBI_SET_TIMER`` 来帮助操作系统在固定时间间隔内获得控制权。而到了第五章,我们通过另外一个SBI调用 ``SBI_CONSOLE_GETCHAR`` 来获得输入的字符能力。这时的操作系统就拥有了与使用者进行简单字符交互的能力了。 15 | 16 | 后来在第六章又引入了另外一个外设virtio-block设备,即一个虚拟的磁盘设备。还通过这个存储设备完成了对数据的持久存储,并在其上实现了管理存储设备上持久性数据的文件系统。对virtio-block设备的I/O访问没有通过RustSBI来完成,而是直接调用了 ``virtio_drivers`` crate中的 ``virtio-blk`` 设备驱动程序来实现。但我们并没有深入分析这个设备驱动程序的具体实现。 17 | 18 | 可以说在操作系统中,I/O设备管理无处不在,且与I/O设备相关的操作系统代码--设备驱动程序在整个操作系统中的代码量比例是最高的(Linux/Windows等都达到了75%以上),也是出错概率最大的地方。虽然前面章节的操作系统已经涉及了很多I/O设备访问的相关处理,但我们并没有对I/O设备进行比较全面的分析和讲解。这主要是由于各种I/O设备差异性比较大,操作系统很难像进程/地址空间/文件那样,对各种I/O设备建立一个一致通用的抽象和对应的解决方案。 19 | 20 | 但I/O设备非常重要,由于各种I/O(输入输出)设备的存在才使得计算机的强大功能得以展现在大众面前,事实上对于各种I/O设备的高效管理是计算机系统操作系统能够在大众中普及的重要因素。比如对于手机而言,大众关注的不是CPU有多快,内存有多大,而是关注显示是否流畅,触摸是否敏捷这些外设带来的人机交互体验。而这些体验在很大程度上取决于操作系统对外设的管理与访问效率。 21 | 22 | 另外,对I/O设备的管理体现了操作系统最底层的设计机制,如中断,并发,异步,缓冲,同步互斥等。这对上层的进程,地址空间,文件等有着深刻的影响。所以在设计和实现了进程,地址空间,文件这些经典的操作系统抽象概念后,我们需要再重新思考一下,具备I/O设备管理能力的操作系统应该如何设计,特别是是否能给I/O设备也建立一个操作系统抽象。如果同学带着这些问题来思考和实践,将会对操作系统有更全面的体会。 23 | 24 | .. note:: 25 | 26 | **UNIX诞生是从磁盘驱动程序开始的** 27 | 28 | 回顾UNIX诞生的历史,你会发现一个有趣的故事:贝尔实验室的Ken Tompson在退出Mulitics操作系统开发后,还是想做继续操作系统方面的探索。他先是给一台闲置的PDP-7计算机的磁盘驱动器写了一个包含磁盘调度算法的磁盘驱动程序,希望提高磁盘I/O读写速度。为了测试磁盘访问性能,Ken Tompson花了三周时间写了一个操作系统,这就是Unix的诞生。这说明是磁盘驱动程序促使了UNIX的诞生。 29 | 30 | 31 | .. chyyuu 可以介绍包括各种外设的 PC OS??? 32 | https://blog.ysndr.de/posts/essays/2021-12-12-rust-for-iot/ 33 | https://english.stackexchange.com/questions/56183/origin-of-the-term-driver-in-computer-science 34 | https://en.wikipedia.org/wiki/MS-DOS 35 | https://en.wikipedia.org/wiki/Microsoft_Windows 36 | https://en.wikipedia.org/wiki/MacOS 37 | https://en.wikipedia.org/wiki/IOS_version_history 38 | https://en.wikipedia.org/wiki/Android_(operating_system) 39 | https://en.wikipedia.org/wiki/History_of_the_graphical_user_interface 40 | 41 | .. note:: 42 | 43 | 设备驱动程序是操作系统的一部分? 44 | 45 | 我们都知道计算机是由CPU、内存和I/O设备组成的。即使是图灵创造的图灵机这一理论模型,也有其必须存在的I/O设备:笔和纸。1946年出现的远古计算机ENIAC,都具有读卡器和打卡器来读入和输出穿孔卡片中的数据。当然,这些外设不需要额外编写软件,直接通过硬件电路就可以完成I/O操作了。但后续磁带和磁盘等外设的出现,使得需要通过软件来管理越来越复杂的外设功能了,这样设备驱动程序(Device Driver)就出现了,它甚至出现在操作系统之前,以子程序库的形式存在,以便于应用程序来访问硬件。 46 | 47 | 随着计算机外部设备越来越多,越来越复杂,设备驱动程序在操作系统中的代码比重也越来越大。甚至某些操作系统的名称直接加入了外设名,如微软在 1981 年至 1995 年间主导了个人计算机市场的DOS操作系统的全称是“Disk Operating System”。1973 年,施乐 PARC 开发了Alto个人电脑,它是第一台具有图形用户界面(GUI) 的计算机,直接影响了苹果公司和微软公司设计的带图形界面的操作系统。微软后续开发的操作系统名称“Windows”也直接体现了图形显示设备(显卡)能够展示的抽象概念,显卡驱动和基于显卡驱动的图形界面子系统在Windows操作系统中始终处于非常重要的位置。 48 | 49 | 目前评价操作系统被产业界接受的程度有一个公认的量化指标,该操作系统的设备驱动程序支持的外设种类和数量。量越大说明它在市场上的接受度就越高。正是由于操作系统能够访问和管理各种外设,才给了应用程序丰富多彩的功能。 50 | 51 | 52 | 53 | 本章的目标是深入理解I/O设备管理,并将站在I/O设备管理的角度来分析I/O设备的特征,操作系统与I/O设备的交互方式。接着会进一步通过串口,磁盘,图形显示等各种外设的具体实现来展现操作系统是如何管理I/O设备的,并展现设备驱动与操作系统内核其它重要部分的交互, 通过扩展操作系统的I/O能力,形成具有灵活感知和捕猎能力的侏罗猎龙 [#juravenator]_ 操作系统。 54 | 55 | 56 | 实践体验 57 | ----------------------------------------- 58 | 59 | 获取本章代码: 60 | 61 | .. code-block:: console 62 | 63 | $ git clone https://github.com/rcore-os/virtio-drivers.git 64 | $ cd virtio-drivers 65 | $ cd examples/riscv 66 | 67 | 在 qemu 模拟器上运行本章代码: 68 | 69 | .. code-block:: console 70 | 71 | $ make run 72 | 73 | .. image:: virtio-test-example.png 74 | :align: center 75 | :name: virtio-test-example 76 | 77 | 本章代码树 78 | ----------------------------------------- 79 | 80 | .. code-block:: 81 | :linenos: 82 | 83 | virtio-drivers crate 84 | ######################## 85 | ./os/src 86 | Rust 8 Files 1150 Lines 87 | ./examples/riscv/src 88 | Rust 2 Files 138 Lines 89 | 90 | . 91 | ├── Cargo.lock 92 | ├── Cargo.toml 93 | ├── examples 94 | │   └── riscv 95 | │   ├── Cargo.toml 96 | │   ├── linker32.ld 97 | │   ├── linker64.ld 98 | │   ├── Makefile 99 | │   ├── rust-toolchain 100 | │   └── src 101 | │   ├── main.rs (各种virtio设备的测试用例) 102 | │   └── virtio_impl.rs (用于I/O数据的物理内存空间管理的简单实现) 103 | ├── LICENSE 104 | ├── README.md 105 | └── src 106 | ├── blk.rs (virtio-blk 驱动) 107 | ├── gpu.rs (virtio-gpu 驱动) 108 | ├── hal.rs (用于I/O数据的物理内存空间管理接口) 109 | ├── header.rs (VirtIOHeader: MMIO Device Register Interface) 110 | ├── input.rs (virtio-input 驱动) 111 | ├── lib.rs 112 | ├── net.rs (virtio-net 驱动) 113 | └── queue.rs (virtqueues: 批量I/O数据传输的机制) 114 | 115 | 4 directories, 20 files 116 | 117 | 118 | 本章代码导读 119 | ----------------------------------------------------- 120 | 121 | 本章涉及的代码主要与设备驱动相关,需要了解硬件,需要阅读和运行测试相关代码。这里简要介绍一下在内核中添加设备驱动的大致开发过程。对于设计实现设备驱动,首先需要大致了解对应设备的硬件规范。在本章中,主要有两类设备,一类是实际的物理设备 -- UART(QEMU模拟了这种NS16550A UART芯片规范);另外一类是虚拟设备(如各种Virtio设备)。 122 | 123 | 然后需要了解外设是如何与CPU连接的。首先是CPU访问外设的方式,在RISC-V环境中,把外设相关的控制寄存器映射为某特定的内存区域(即MMIO映射方式),然后CPU通过读写这些特殊区域来访问外设(即PIO访问方式)。外设可以通过DMA来读写主机内存中的数据,并可通过中断来通知CPU。外设并不直接连接CPU,这就需要了解RISC-V中的平台级中断控制器(Platform-Level Interrupt Controller,PLIC),它管理并收集各种外设中断信息,并传递给CPU。 124 | 125 | 对于设备驱动程序对外设的具体管理过程,大致会有初始化外设和I/O读写与控制操作。理解这些操作和对应的关键数据结构,就大致理解外设驱动要完成的功能包含哪些内容。每个设备驱动的关键数据结构和处理过程有共性部分和特定的部分。建议从 ``virtio-drivers`` crate 中的 ``examples/riscv/src/main.rs`` 这个virtio设备的功能测试例子入手来分析。 126 | 127 | 以 ``virtio-blk`` 存储设备为例,可以看到,首先是访问 ``OpenSBI`` (这里没有用RustSBI,用的是QEMU内置的SBI实现)提供的设备树信息,了解QEMU硬件中存在的各种外设,根据外设ID来找到 ``virtio-blk`` 存储设备;找到后,就进行外设的初始化,如果学习了 virtio规范(需要关注的是 virtqueue、virtio-mmio device, virtio-blk device的描述内容),那就可以看出代码实现的初始化过程和virtio规范中的virtio设备初始化步骤基本上是一致的,但也有与具体设备相关的特定初始化内容,比如分配 I/O buffer等。初始化完毕后,设备驱动在收到上层内核发出的读写扇区/磁盘块的请求后,就能通过 ``virtqueue`` 传输通道发出 ``virtio-blk`` 设备能接收的I/O命令和I/O buffer的区域信息; ``virtio-blk`` 设备收到信息后,会通过DMA操作完成磁盘数据的读写,然后通过中断或其他方式让设备驱动知道命令完成或命令执行失败。而 ``virtio-gpu`` 设备驱动程序的设计实现与 ``virtio-blk`` 设备驱动程序类似。 128 | 129 | 注:目前还没有提供相关的系统调用来方便应用程序访问virtio-gpu外设。 130 | 131 | 132 | 133 | .. [#juravenator] 侏罗猎龙是一种小型恐龙,生活在1亿5千万年前的侏罗纪,它有独特的鳞片状的皮肤感觉器官,具有类似鳄鱼的触觉、冷热以及pH等综合感知能力,可能对狩猎有很大帮助。 -------------------------------------------------------------------------------- /source/chapter3/5exercise.rst: -------------------------------------------------------------------------------- 1 | 练习 2 | ======================================= 3 | 4 | 5 | 课后练习 6 | ------------------------------- 7 | 8 | 编程题 9 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 10 | 11 | 12 | 1. `*` 扩展内核,能够显示操作系统切换任务的过程。 13 | 2. `**` 扩展内核,能够统计每个应用执行后的完成时间:用户态完成时间和内核态完成时间。 14 | 3. `**` 编写浮点应用程序A,并扩展内核,支持面向浮点应用的正常切换与抢占。 15 | 4. `**` 编写应用程序或扩展内核,能够统计任务切换的大致开销。 16 | 5. `***` 扩展内核,支持在内核态响应中断。 17 | 6. `***` 扩展内核,支持在内核运行的任务(简称内核任务),并支持内核任务的抢占式切换。 18 | 19 | 注:上述扩展内核的编程基于 rcore/ucore tutorial v3: Branch ch3 20 | 21 | 问答题 22 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 23 | 24 | 1. `*` 协作式调度与抢占式调度的区别是什么? 25 | 2. `*` 中断、异常和系统调用有何异同之处? 26 | 3. `*` RISC-V支持哪些中断/异常? 27 | 4. `*` 如何判断进入操作系统内核的起因是由于中断还是异常? 28 | 5. `**` 在 RISC-V 中断机制中,PLIC 和 CLINT 各起到了什么作用? 29 | 6. `**` 基于RISC-V 的操作系统支持中断嵌套?请给出进一步的解释说明。 30 | 7. `**` 本章提出的任务的概念与前面提到的进程的概念之间有何区别与联系? 31 | 8. `*` 简单描述一下任务的地址空间中有哪些类型的数据和代码。 32 | 9. `*` 任务控制块保存哪些内容? 33 | 10. `*` 任务上下文切换需要保存与恢复哪些内容? 34 | 11. `*` 特权级上下文和任务上下文有何异同? 35 | 12. `*` 上下文切换为什么需要用汇编语言实现? 36 | 13. `*` 有哪些可能的时机导致任务切换? 37 | 14. `**` 在设计任务控制块时,为何采用分离的内核栈和用户栈,而不用一个栈? 38 | 15. `***` 我们已经在 rCore 里实现了不少操作系统的基本功能:特权级、上下文切换、系统调用……为了让大家对相关代码更熟悉,我们来以另一个操作系统为例,比较一下功能的实现。看看换一段代码,你还认不认识操作系统。 39 | 40 | 阅读 Linux 源代码,特别是 ``riscv`` 架构相关的代码,回答以下问题: 41 | 42 | 1. Linux 正常运行的时候, ``stvec`` 指向哪个函数?是哪段代码设置的 ``stvec`` 的值? 43 | 2. Linux 里进行上下文切换的函数叫什么?(对应 rCore 的 ``__switch`` ) 44 | 3. Linux 里,和 rCore 中的 ``TrapContext`` 和 ``TaskContext`` 这两个类型大致对应的结构体叫什么? 45 | 4. Linux 在内核态运行的时候, ``tp`` 寄存器的值有什么含义? ``sscratch`` 的值是什么? 46 | 5. Linux 在用户态运行的时候, ``sscratch`` 的值有什么含义? 47 | 6. Linux 在切换到内核态的时候,保存了和用户态程序相关的什么状态? 48 | 7. Linux 在内核态的时候,被打断的用户态程序的寄存器值存在哪里?在 C 代码里如何访问? 49 | 8. Linux 是如何根据系统调用编号找到对应的函数的?(对应 rCore 的 ``syscall::syscall()`` 函数的功能) 50 | 9. Linux 用户程序调用 ``ecall`` 的参数是怎么传给系统调用的实现的?系统调用的返回值是怎样返回给用户态的? 51 | 52 | 阅读代码的时候,可以重点关注一下如下几个文件,尤其是第一个 ``entry.S`` ,当然也可能会需要读到其它代码: 53 | 54 | * ``arch/riscv/kernel/entry.S`` (与 rCore 的 ``switch.S`` 对比) 55 | * ``arch/riscv/include/asm/current.h`` 56 | * ``arch/riscv/include/asm/processor.h`` 57 | * ``arch/riscv/include/asm/switch_to.h`` 58 | * ``arch/riscv/kernel/process.c`` 59 | * ``arch/riscv/kernel/syscall_table.c`` 60 | * ``arch/riscv/kernel/traps.c`` 61 | * ``include/linux/sched.h`` 62 | 63 | 此外,推荐使用 https://elixir.bootlin.com 阅读 Linux 源码,方便查找各个函数、类型、变量的定义及引用情况。 64 | 65 | 一些提示: 66 | 67 | * Linux 支持各种架构,查找架构相关的代码的时候,请认准文件名中的 ``arch/riscv`` 。 68 | * 为了同时兼容 RV32 和 RV64,Linux 在汇编代码中用了几个宏定义。例如, ``REG_L`` 在 RV32 上是 ``lw`` ,而在 RV64 上是 ``ld`` 。同理, ``REG_S`` 在 RV32 上是 ``sw`` ,而在 RV64 上是 ``sd`` 。 69 | * 如果看到 ``#ifdef CONFIG_`` 相关的预处理指令,是 Linux 根据编译时的配置启用不同的代码。一般阅读代码时,要么比较容易判断出这些宏有没有被定义,要么其实无关紧要。比如,Linux 内核确实应该和 rCore 一样,是在 S-mode 运行的,所以 ``CONFIG_RISCV_M_MODE`` 应该是没有启用的。 70 | * 汇编代码中可能会看到有些 ``TASK_`` 和 `PT_` 开头的常量,找不到定义。这些常量并没有直接写在源码里,而是自动生成的。 71 | 72 | 在汇编语言中需要用到的很多 ``struct`` 里偏移量的常量定义可以在 ``arch/riscv/kernel/asm-offsets.c`` 文件里找到。其中, ``OFFSET(NAME, struct_name, field)`` 指的是 ``NAME`` 的值定义为 ``field`` 这一项在 ``struct_name`` 结构体里,距离结构体开头的偏移量。最终这些代码会生成 ``asm/asm-offsets.h`` 供汇编代码使用。 73 | * ``#include `` 在 ``arch/riscv/include/uapi/asm/unistd.h`` , ``#include `` 在 ``include/uapi/asm-generic/unistd.h`` 。 74 | 75 | .. chyyuu:任务与进程,类似青蛙生长过程中的蝌蚪与青蛙的区别与联系。 76 | 77 | 78 | 79 | 实验练习 80 | ------------------------------- 81 | 82 | 实验练习包括实践作业和问答作业两部分。 83 | 84 | 实践作业 85 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 86 | 87 | 获取任务信息 88 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 89 | 90 | ch3 中,我们的系统已经能够支持多个任务分时轮流运行,我们希望引入一个新的系统调用 ``sys_task_info`` 以获取任务的信息,定义如下: 91 | 92 | .. code-block:: rust 93 | 94 | fn sys_task_info(id: usize, ts: *mut TaskInfo) -> isize 95 | 96 | - syscall ID: 410 97 | - 根据任务 ID 查询任务信息,任务信息包括任务 ID、任务控制块相关信息(任务状态)、任务使用的系统调用及调用次数、任务总运行时长。 98 | 99 | .. code-block:: rust 100 | 101 | struct TaskInfo { 102 | id: usize, 103 | status: TaskStatus, 104 | call: [SyscallInfo; MAX_SYSCALL_NUM], 105 | time: usize 106 | } 107 | 108 | - 系统调用信息采用数组形式对每个系统调用的次数进行统计,相关结构定义如下: 109 | 110 | .. code-block:: rust 111 | 112 | struct SyscallInfo { 113 | id: usize, 114 | times: usize 115 | } 116 | 117 | - 参数: 118 | - id: 待查询任务id 119 | - ts: 待查询任务信息 120 | - 返回值:执行成功返回0,错误返回-1 121 | - 说明: 122 | - 相关结构已在框架中给出,只需添加逻辑实现功能需求即可。 123 | - 提示: 124 | - 大胆修改已有框架!除了配置文件,你几乎可以随意修改已有框架的内容。 125 | - 程序运行时间可以通过调用 ``get_time()`` 获取。 126 | - 系统调用次数可以考虑在进入内核态系统调用异常处理函数之后,进入具体系统调用函数之前维护。 127 | - 阅读 TaskManager 的实现,思考如何维护内核控制块信息(可以在控制块可变部分加入其他需要的信息) 128 | 129 | 打印调用堆栈(选做) 130 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 131 | 132 | 我们在调试程序时,除了正在执行的函数外,往往还需要知道当前的调用堆栈。这样的功能通常由调试器、运行环境、 IDE 或操作系统等提供,但现在我们只能靠自己了。最基本的实现只需打印出调用链上的函数地址,更丰富的功能包括打印出函数名、函数定义、传递的参数等等。 133 | 134 | 本实验我们不提供新的测例,仅提供参考实现,各位同学可以通过对照 GDB 、参考实现或自行构造调用链等方式检验自己的实现是否正确。 135 | 136 | .. hint:: 可以参考《编译原理》课程中关于函数调用栈帧的内容。 137 | 138 | 实验要求 139 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 140 | 141 | - 完成分支: ch3-lab 142 | 143 | - 实验目录要求 144 | 145 | .. code-block:: 146 | 147 | ├── os(内核实现) 148 | │   ├── Cargo.toml(配置文件) 149 | │   └── src(所有内核的源代码放在 os/src 目录下) 150 | │   ├── main.rs(内核主函数) 151 | │   └── ... 152 | ├── reports (不是 report) 153 | │   ├── lab3.md/pdf 154 | │   └── ... 155 | ├── ... 156 | 157 | 158 | - 通过所有已有的测例: 159 | 160 | CI 使用的测例与本地相同,测试中,user 文件夹及其它与构建相关的文件将被替换,请不要试图依靠硬编码通过测试。 161 | 162 | .. note:: 163 | 164 | 你的实现只需且必须通过测例,建议读者感到困惑时先检查测例。 165 | 166 | 实验约定 167 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 168 | 169 | 问答作业 170 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 171 | 172 | 1. 正确进入 U 态后,程序的特征还应有:使用 S 态特权指令,访问 S 态寄存器后会报错。 173 | 请同学们可以自行测试这些内容 (运行 `Rust 两个 bad 测例 (ch2b_bad_*.rs) `_ ) , 174 | 描述程序出错行为,同时注意注明你使用的 sbi 及其版本。 175 | 2. 请通过 gdb 跟踪或阅读源代码了解机器从加电到跳转到 0x80200000 的过程,并描述重要的跳转。回答内核是如何进入 S 态的? 176 | 177 | - 事实上进入 rustsbi (0x80000000) 之后就不需要使用 gdb 调试了。可以直接阅读 `代码 `_ 。 178 | - 可以使用 Makefile 中的 ``make debug`` 指令。 179 | - 一些可能用到的 gdb 指令: 180 | - ``x/10i 0x80000000`` : 显示 0x80000000 处的10条汇编指令。 181 | - ``x/10i $pc`` : 显示即将执行的10条汇编指令。 182 | - ``x/10xw 0x80000000`` : 显示 0x80000000 处的10条数据,格式为16进制32bit。 183 | - ``info register``: 显示当前所有寄存器信息。 184 | - ``info r t0``: 显示 t0 寄存器的值。 185 | - ``break funcname``: 在目标函数第一条指令处设置断点。 186 | - ``break *0x80200000``: 在 0x80200000 出设置断点。 187 | - ``continue``: 执行直到碰到断点。 188 | - ``si``: 单步执行一条汇编指令。 189 | 190 | 191 | 192 | 实验练习的提交报告要求 193 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 194 | 195 | - 简单总结与上次实验相比本次实验你增加的东西(控制在5行以内,不要贴代码)。 196 | - 完成问答问题。 197 | - (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 198 | -------------------------------------------------------------------------------- /source/chapter0/8answer.rst: -------------------------------------------------------------------------------- 1 | 练习参考答案 2 | ===================================================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | 9 | 编程题 10 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 11 | 12 | 1. `*` 在你日常使用的操作系统环境中安装并配置好实验环境。简要说明你碰到的问题/困难和解决方法。 13 | 14 | 2. `*` 在Linux环境下编写一个会产生异常的应用程序,并简要解释操作系统的处理结果。 15 | 16 | 例如,对于这样一段 C 程序,其中包含一个除以零的操作: 17 | 18 | .. code-block:: c 19 | :linenos: 20 | 21 | #include 22 | 23 | int main() { 24 | printf("1 / 0 = %d", 1 / 0); 25 | return 0; 26 | } 27 | 28 | 在基于 x86-64 Linux 的环境下编译运行结果如下: 29 | 30 | .. code-block:: console 31 | 32 | $ gcc divzero 33 | $ ./divzero 34 | Floating point exception (core dumped) 35 | 36 | 程序接收到了一个“浮点数异常”而异常终止。使用 ``strace`` 可以看到更详细的信号信息: 37 | 38 | .. code-block:: console 39 | 40 | $ strace ./divzero 41 | [... 此处省略系统调用跟踪输出] 42 | --- SIGFPE {si_signo=SIGFPE, si_code=FPE_INTDIV, si_addr=0x401131} --- 43 | +++ killed by SIGFPE (core dumped) +++ 44 | 45 | 用 gdb 的 ``disassemble`` 命令可以看到发生异常的指令 46 | 47 | .. code-block:: console 48 | 49 | $ gdb divzero 50 | Reading symbols from divzero... 51 | (gdb) r 52 | Starting program: [...]/divzero 53 | 54 | Program received signal SIGFPE, Arithmetic exception. 55 | 0x0000000000401131 in main () 56 | (gdb) disassemble 57 | Dump of assembler code for function main: 58 | 0x0000000000401122 <+0>: push %rbp 59 | 0x0000000000401123 <+1>: mov %rsp,%rbp 60 | 0x0000000000401126 <+4>: mov $0x1,%eax 61 | 0x000000000040112b <+9>: mov $0x0,%ecx 62 | 0x0000000000401130 <+14>: cltd 63 | => 0x0000000000401131 <+15>: idiv %ecx 64 | 0x0000000000401133 <+17>: mov %eax,%esi 65 | 0x0000000000401135 <+19>: mov $0x402004,%edi 66 | 0x000000000040113a <+24>: mov $0x0,%eax 67 | 0x000000000040113f <+29>: call 0x401030 68 | 0x0000000000401144 <+34>: mov $0x0,%eax 69 | 0x0000000000401149 <+39>: pop %rbp 70 | 0x000000000040114a <+40>: ret 71 | 72 | 可以看出,应用程序在执行 `idiv` 指令(有符号除法指令)时发生了除以零异常,跳转至操作系统处理。操作系统把它转换为一个信号 `SIGFPE`,使用信号处理机制处理这个异常。该程序收到 `SIGFPE` 时应发生的行为是异常终止,于是操作系统将其终止,并将异常退出的信息报告给 shell 进程。 73 | 74 | 需要注意的是,异常的处理和与信号的对应是与架构相关的。例如,RISC-V 架构下除以零不是异常,而是有个确定的结果。此外,不同架构下具体异常和信号的对应关系也是不同的,甚至有些混乱(例如,这里明明是整数除以零错误,却报告了“浮点数”异常)。 75 | 76 | 3. `**` 在Linux环境下编写一个可以睡眠5秒后打印出一个字符串,并把字符串内容存入一个文件中的应用程序A。(基于C或Rust语言) 77 | 78 | 样例实现如下(未包含错误处理) 79 | 80 | .. code-block:: c 81 | 82 | #include 83 | #include 84 | 85 | int main() { 86 | sleep(5); 87 | 88 | const char* hello_string = "Hello Linux!\n"; 89 | 90 | printf(hello_string); 91 | 92 | FILE *output_file = fopen("output.txt", "w"); 93 | fputs(hello_string, output_file); 94 | fclose(output_file); 95 | 96 | return 0; 97 | } 98 | 99 | 100 | 编译运行,查看程序的输出和文件,结果如下: 101 | 102 | .. code-block:: console 103 | 104 | $ gcc -o program-a program-a.c 105 | $ ./program-a 106 | Hello Linux! [这一行等待 5s 后才输出] 107 | $ cat output.txt 108 | Hello Linux! 109 | 110 | 4. `***` 在Linux环境下编写一个应用程序B,简要说明此程序能够体现操作系统的并发性、异步性、共享性和持久性。(基于C或Rust语言) 111 | 112 | 注: 在类Linux环境下编写尝试用GDB等调试工具调试应用程序A,能够设置断点,单步执行,显示变量信息。 113 | 114 | 问答题 115 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 116 | 117 | 1. `*` 什么是操作系统?操作系统的主要目标是什么? 118 | 119 | 2. `*` 面向服务器的操作系统与面向手机的操作系统在功能上有何异同? 120 | 121 | * 相同部分举例: 122 | 123 | * 都需要进行进程的管理 124 | * 都需要管理 CPU,内存这些基本资源 125 | * 都需要隔离保护等安全机制 126 | * (服务器和手机的操作系统共通之处很多,甚至可以共用最基础的部分,比如都用 Linux 内核) 127 | 128 | * 不同部分举例: 129 | 130 | * 支持的外设不同: 131 | 132 | * 服务器:高性能网卡(如光纤)、存储设备(如 HBA)等 133 | * 手机:屏幕,音响和话筒,基带(移动网络)、WiFi 等 134 | 135 | * 服务器和手机都有的资源,管理起来也有区别: 136 | 137 | * 服务器:内存很多,CPU 核数很多,可能需要支持更复杂的硬件拓扑,如 NUMA 架构 138 | * 手机:对低功耗的要求,需要操作系统在调度时考虑到电源管理,平衡省电和性能 139 | 140 | * 应用不同,导致对安全性要求不同: 141 | 142 | * 服务器:上千用户,少数几个应用,需要各种阻止用户利用漏洞操控服务器上的程序的功能 143 | * 手机:一个或少数几个用户,几十个应用,需要隐私保护功能 144 | 145 | 3. `*` 对于目前的手机或桌面操作系统而言,操作系统是否应该包括网络浏览器?请说明理由。 146 | 147 | 4. `*` 操作系统的核心抽象有哪些?它们应对的对象是啥? 148 | 149 | * 进程/线程 -- CPU 时间 150 | * 地址空间 -- 内存 151 | * 执行环境 -- CPU 上复杂的环境(有中断异常等),和操作系统提供的功能(如系统调用) 152 | * 文件和文件描述符 -- 存储和输入输出设备 153 | 154 | 5. `*` 操作系统与应用程序之间通过什么来进行互操作和数据交换? 155 | 156 | * 互操作的方式: 157 | 158 | * 应用程序调用系统调用主动让操作系统进行操作 159 | * 操作系统在中断异常发生时强制暂停应用程序进行相关操作 160 | 161 | * 数据交换的方式: 162 | 163 | * 系统调用时根据 ABI 规定在(比如)寄存器中传递参数 164 | * 复制数据:在内核占用的空间和用户占用的空间之间互相复制数据,如读写文件的时候从应用程序给出的缓冲区复制写的数据,或者复制读的数据到缓冲区 165 | * (共享内存空间:如 `io_uring`) 166 | 167 | 6. `*` 操作系统的特征是什么?请结合你日常使用的操作系统的具体运行情况来进一步说明操作系统的特征。 168 | 169 | 以在普通的桌面机上运行基于 Linux 的平台上做操作系统实验时可能发生的事情为例: 170 | 171 | * 虚拟性:如果物理内存相对不足,Linux 的 swap 机制会将不常用的内存内容转存到硬盘上,优先保证活跃的进程可以高速访问物理内存。 172 | * 并发性:正在运行的程序数量(包括并发运行的 `gcc` 或 `rustc`,和其它程序)可以超过 CPU 核心数,由操作系统来调度分配使用 CPU 时间。 173 | * 异步性:并行运行的编译器太多,可能会影响写代码用的文本编辑器性能,因为操作系统安排了更多时间运行编译器而不是文本编辑器进程。 174 | * 共享性:写代码和编译可以共享同一个文件系统,在同一块硬盘上读写,操作系统的文件系统相关模块(VFS)会调用文件系统实现和硬盘驱动安排这些读写操作。 175 | * 持久性:操作系统实验用的文件保存在文件系统中,晚上宿舍断电关机,明天早上起来可以从昨天保存的文件的状态继续工作。 176 | 177 | 7. `*` 请说明基于C语言应用的执行环境与基于Java语言应用的执行环境的异同。 178 | 179 | 8. `**` 请简要列举操作系统的系统调用的作用,以及简要说明与程序执行、内存分配、文件读写相关的Linux系统调用的大致接口和含义。 180 | 181 | * 系统调用的作用: 182 | 183 | * 将操作系统实现和用户程序调用方式分开,作为一个抽象层方便开发和使用 184 | * 在让用户程序没法直接访问别的用户程序的地址空间和操作系统地址空间的情况下,作为一个统一的应用程序请求操作系统服务的入口,方便安全检查和审计。 185 | * 让用户程序能有限访问其不能直接访问的计算机资源和操作系统对象 186 | 187 | * 在 Linux 中: 188 | 189 | * `clone`(曾用的有 `fork`)创建进程;`execve` 传入文件路径、命令行参数、环境变量,加载新的程序替换当前进程。 190 | * `brk` 修改或获取当前的堆顶的位置,增加堆顶地址分配空间,减小堆底地址释放空间;`mmap` 用于映射地址空间,也可以分配物理内存(`MAP_ANONYMOUS`),`munmap` 释放对应的地址空间和对应的内存。 191 | * 文件读写相关有 `openat`(曾用的有 `open`)打开文件,`read` 读文件,`write` 写文件等。 192 | 193 | 9. `**` 以你编写的可以睡眠5秒后打印出一个字符串的应用程序A为例,说明什么是控制流?什么是异常控制流?什么是进程、地址空间和文件?并简要描述操作系统是如何支持这个应用程序完成其工作并结束的。 194 | 195 | 应用程序A中体现的操作系统相关概念: 196 | 197 | * 控制流: ``main`` 函数内一行一行往下执行,是一个顺序控制流;其中调用的标准库函数,是函数调用的控制流。 198 | * 异常控制流: 在 ``sleep`` 的时候,执行系统调用 `clock_nanosleep`,此时控制流跳出了该程序,进入了操作系统的系统调用实现代码,之后操作系统将应用程序A进入睡眠状态,转而运行别的进程,这里体现了异常控制流。在执行其它系统调用的时候也会有类似的情况。 199 | * 进程:整个应用程序A是在一个新的进程中运行的,与启动它的 shell。 200 | * 地址空间:字符串 ``hello_string`` 所在的地址,是在这个进程自己的地址空间内有效的,和别的进程无关。 201 | * 文件:应用程序A打开文件名为 ``output.txt`` 的文件,向其中写入了一个字符串。此外, ``printf`` 是向标准输出写入,标准输出在此时是一个对应当前终端的文件。 202 | 203 | 操作系统支持应用程序A运行的流程: 204 | 205 | * 操作系统加载程序和 C 语言标准库到内存中 206 | * 操作系统设置一些初始的虚拟内存配置和一些数据,然后切换到用户态跳转到程序的入口点开始运行。 207 | * 应用程序调用 C 语言标准库,库再进行系统调用,进入内核,内核处理相关的系统调用然后恢复应用程序运行,具体来说: 208 | 209 | * 对于 ``sleep`` 来说,操作系统在 5 秒时间内切换到别的任务,不运行应用程序A,5 秒过后再继续运行 210 | * 对于文件来说,操作系统在文件系统内找到对应的文件写入,或者找到对应的终端将写入的内容发送过去 211 | 212 | * 最后 C 语言标准库会调用 `exit_group` 系统调用退出进程。操作系统接收这个系统调用后,不再回到应用程序,释放该进程相关资源。 213 | 214 | 10. `*` 请简要描述支持单个应用的OS、批处理OS、多道程序OS、分时共享OS的特点。 215 | -------------------------------------------------------------------------------- /source/chapter4/9answer.rst: -------------------------------------------------------------------------------- 1 | 练习参考答案 2 | ============================================ 3 | 4 | 课后练习 5 | ------------------------------- 6 | 7 | 编程题 8 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 9 | 10 | 1. `**` 使用sbrk,mmap,munmap,mprotect内存相关系统调用的linux应用程序。 11 | 2. `***` 修改本章操作系统内核,实现任务和操作系统内核共用同一张页表的单页表机制。 12 | 3. `***` 扩展内核,支持基于缺页异常机制,具有Lazy 策略的按需分页机制。 13 | 4. `***` 扩展内核,支持基于缺页异常的COW机制。(初始时,两个任务共享一个只读物理页。当一个任务执行写操作后,两个任务拥有各自的可写物理页) 14 | 5. `***` 扩展内核,实现swap in/out机制,并实现Clock置换算法或二次机会置换算法。 15 | 6. `***` 扩展内核,实现自映射机制。 16 | 17 | 问答题 18 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 19 | 20 | .. chyyuu 这次的实验没有涉及到缺页有点遗憾,主要是缺页难以测试,而且更多的是一种优化,不符合这次实验的核心理念,所以这里补两道小题。 21 | 22 | 1. `*` 在使用高级语言编写用户程序的时候,手动用嵌入汇编的方法随机访问一个不在当前程序逻辑地址范围内的地址,比如向该地址读/写数据。该用户程序执行的时候可能会生什么? 23 | 24 | 可能会报出缺页异常. 25 | 26 | 2. `*` 用户程序在运行的过程中,看到的地址是逻辑地址还是物理地址?从用户程序访问某一个地址,到实际内存中的对应单元被读/写,会经过什么样的过程,这个过程中操作系统有什么作用?(站在学过计算机组成原理的角度) 27 | 28 | 逻辑地址。这个过程需要经过页表的转换,操作系统会负责建立页表映射。实际程序执行时的具体VA到PA的转换是在CPU的MMU之中进行的。 29 | 30 | 3. `*` 覆盖、交换和虚拟存储有何异同,虚拟存储的优势和挑战体现在什么地方? 31 | 32 | 它们都是采取层次存储的思路,将暂时不用的内存放到外存中去,以此来缓解内存不足的问题。 33 | 34 | 不同之处:覆盖是程序级的,需要程序员自行处理。交换则不同,由OS控制交换程序段。虚拟内存也由OS和CPU来负责处理,可以实现内存交换到外存的过程。 35 | 36 | 虚拟存储的优势:1.与段/页式存储完美契合,方便非连续内存分配。2.粒度合适,比较灵活。兼顾了覆盖和交换的好处:可以在较小粒度上置换;自动化程度高,编程简单,受程序本身影响很小。(覆盖的粒度受限于程序模块的大小,对编程技巧要求很高。交换粒度较大,受限于程序所需内存。尤其页式虚拟存储,几乎不受程序影响,一般情况下,只要置换算法合适,表现稳定、高效)3.页式虚拟存储还可以同时解决内存外碎片。提高空间利用率。 37 | 38 | 虚拟存储的挑战: 1.依赖于置换算法的性能。2.相比于覆盖和交换,需要比较高的硬件支持。3.较小的粒度在面临大规模的置换时会发生多次较小规模置换,降低效率。典型情况是程序第一次执行时的大量page fault,可配合预取技术缓解这一问题。 39 | 40 | 4. `*` 什么是局部性原理?为何很多程序具有局部性?局部性原理总是正确的吗?为何局部性原理为虚拟存储提供了性能的理论保证? 41 | 42 | 局部性分时间局部性和空间局部性(以及分支局部性)。局部性的原理是程序经常对一块相近的地址进行访问或者是对一个范围内的指令进行操作。局部性原理不一定是一直正确的。虚拟存储以页为单位,局部性使得数据和指令的访存局限在几页之中,可以避免页的频繁换入换出的开销,同时也符合TLB和cache的工作机制。 43 | 44 | 5. `**` 一条load指令,最多导致多少次页访问异常?尝试考虑较多情况。 45 | 46 | 考虑多级页表的情况。首先指令和数据读取都可能缺页。因此指令会有3次访存,之后的数据读取除了页表页缺失的3次访存外,最后一次还可以出现地址不对齐的异常,因此可以有7次异常。若考更加极端的情况,也就是页表的每一级都是不对齐的地址并且处在两页的交界处(straddle),此时一次访存会触发2次读取页面,如果这两页都缺页的话,会有更多的异常次数。 47 | 48 | 6. `**` 如果在页访问异常中断服务例程执行时,再次出现页访问异常,这时计算机系统(软件或硬件)会如何处理?这种情况可能出现吗? 49 | 50 | 我们实验的os在此时不支持内核的异常中断,因此此时会直接panic掉,并且这种情况在我们的os中这种情况不可能出现。像linux系统,也不会出现嵌套的page fault。 51 | 52 | 7. `*` 全局和局部置换算法有何不同?分别有哪些算法? 53 | 54 | 8. `*` 简单描述OPT、FIFO、LRU、Clock、LFU的工作过程和特点 (不用写太多字,简明扼要即可) 55 | 56 | 9. `**` 综合考虑置换算法的收益和开销,综合评判在哪种程序执行环境下使用何种算法比较合适? 57 | 58 | 10. `**` Clock算法仅仅能够记录近期是否访问过这一信息,对于访问的频度几乎没有记录,如何改进这一点? 59 | 60 | 11. `***` 哪些算法有belady现象?思考belady现象的成因,尝试给出说明OPT和LRU等为何没有belady现象。 61 | 62 | FIFO算法、Clock算法。 63 | 64 | 页面调度算法可分为堆栈式和非堆栈式,LRU、LFU、OPT均为堆栈类算法,FIFO、Clock为非堆栈类算法,只有非堆栈类才会出现Belady现象。 65 | 66 | 12. `*` 什么是工作集?什么是常驻集?简单描述工作集算法的工作过程。 67 | 68 | 工作集为一个进程当前正在使用的逻辑页面集合,可表示为二元函数$W(t, \Delta)$,t 为执行时刻,$\Delta$ 称为工作集窗口,即一个定长的页面访问时间窗口,$W(t, \Delta)$是指在当前时刻 t 前的 $\Delta$ 时间窗口中的所有访问页面所组成的集合,$|W(t, \Delta)|$为工作集的大小,即页面数目。 69 | 70 | 13. `*` 请列举 SV39 页`*` 页表项的组成,结合课堂内容,描述其中的标志位有何作用/潜在作用? 71 | 72 | [63:54]为保留项,[53:10]为44位物理页号,最低的8位[7:0]为标志位。 73 | 74 | - V(Valid):仅当位 V 为 1 时,页表项才是合法的; 75 | - R(Read)/W(Write)/X(eXecute):分别控制索引到这个页表项的对应虚拟页面是否允许读/写/执行; 76 | - U(User):控制索引到这个页表项的对应虚拟页面是否在 CPU 处于 U 特权级的情况下是否被允许访问; 77 | - A(Accessed):处理器记录自从页表项上的这一位被清零之后,页表项的对应虚拟页面是否被访问过; 78 | - D(Dirty):处理器记录自从页表项上的这一位被清零之后,页表项的对应虚拟页面是否被修改过。 79 | 80 | 14. `**` 请问一个任务处理 10G 连续的内存页面,需要操作的页表实际大致占用多少内存(给出数量级即可)? 81 | 82 | 大致占用`10G/512=20M`内存。 83 | 84 | 15. `**` 缺页指的是进程访问页面时页面不在页表中或在页表中无效的现象,此时 MMU 将会返回一个中断,告知操作系统:该进程内存访问出了问题。然后操作系统可选择填补页表并重新执行异常指令或者杀死进程。操作系统基于缺页异常进行优化的两个常见策略中,其一是 Lazy 策略,也就是直到内存页面被访问才实际进行页表操作。比如,一个程序被执行时,进程的代码段理论上需要从磁盘加载到内存。但是 操作系统并不会马上这样做,而是会保存 .text 段在磁盘的位置信息,在这些代码第一次被执行时才完成从磁盘的加载操作。 另一个常见策略是 swap 页置换策略,也就是内存页面可能被换到磁盘上了,导致对应页面失效,操作系统在任务访问到该页产生异常时,再把数据从磁盘加载到内存。 85 | 86 | 1. 哪些异常可能是缺页导致的?发生缺页时,描述与缺页相关的CSR寄存器的值及其含义。 87 | 88 | - 答案: `mcause` 寄存器中会保存发生中断异常的原因,其中 `Exception Code` 为 `12` 时发生指令缺页异常,为 `15` 时发生 `store/AMO` 缺页异常,为 `13` 时发生 `load` 缺页异常。 89 | 90 | CSR寄存器: 91 | 92 | - `scause`: 中断/异常发生时, `CSR` 寄存器 `scause` 中会记录其信息, `Interrupt` 位记录是中断还是异常, `Exception Code` 记录中断/异常的种类。 93 | - `sstatus`: 记录处理器当前状态,其中 `SPP` 段记录当前特权等级。 94 | - `stvec`: 记录处理 `trap` 的入口地址,现有两种模式 `Direct` 和 `Vectored` 。 95 | - `sscratch`: 其中的值是指向hart相关的S态上下文的指针,比如内核栈的指针。 96 | - `sepc`: `trap` 发生时会将当前指令的下一条指令地址写入其中,用于 `trap` 处理完成后返回。 97 | - `stval`: `trap` 发生进入S态时会将异常信息写入,用于帮助处理 `trap` ,其中会保存导致缺页异常的虚拟地址。 98 | 99 | 2. Lazy 策略有哪些好处?请描述大致如何实现Lazy策略? 100 | 101 | - 答案:Lazy策略一定不会比直接加载策略慢,并且可能会提升性能,因为可能会有些页面被加载后并没有进行访问就被释放或替代了,这样可以避免很多无用的加载。分配内存时暂时不进行分配,只是将记录下来,访问缺页时会触发缺页异常,在`trap handler`中处理相应的异常,在此时将内存加载或分配即可。 102 | 103 | 3. swap 页置换策略有哪些好处?此时页面失效如何表现在页表项(PTE)上?请描述大致如何实现swap策略? 104 | 105 | - 答案:可以为用户程序提供比实际物理内存更大的内存空间。页面失效会将标志位`V`置为`0`。将置换出的物理页面保存在磁盘中,在之后访问再次触发缺页异常时将该页面写入内存。 106 | 107 | 16. `**` 为了防范侧信道攻击,本章的操作系统使用了双页表。但是传统的操作系统设计一般采用单页表,也就是说,任务和操作系统内核共用同一张页表,只不过内核对应的地址只允许在内核态访问。(备注:这里的单/双的说法仅为自创的通俗说法,并无这个名词概念,详情见 `KPTI `_ ) 108 | 109 | 1. 单页表情况下,如何控制用户态无法访问内核页面? 110 | 111 | - 答案:将内核页面的 pte 的`U`标志位设置为0。 112 | 113 | 2. 相对于双页表,单页表有何优势? 114 | 115 | - 答案:在内核和用户态之间转换时不需要更换页表,也就不需要跳板,可以像之前一样直接切换上下文。 116 | 117 | 3. 请描述:在单页表和双页表模式下,分别在哪个时机,如何切换页表? 118 | 119 | - 答案:双页表实现下用户程序和内核转换时、用户程序转换时都需要更换页表,而对于单页表操作系统,不同用户线程切换时需要更换页表。 120 | 121 | 实验练习 122 | ------------------------------- 123 | 124 | 实验练习包括实践作业和问答作业两部分。 125 | 126 | 实践作业 127 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 128 | 129 | 重写 sys_get_time 130 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 131 | 132 | 引入虚存机制后,原来内核的 sys_get_time 函数实现就无效了。请你重写这个函数,恢复其正常功能。 133 | 134 | mmap 和 munmap 匿名映射 135 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 136 | 137 | `mmap `_ 在 Linux 中主要用于在内存中映射文件,本次实验简化它的功能,仅用于申请内存。 138 | 139 | 请实现 mmap 和 munmap 系统调用,mmap 定义如下: 140 | 141 | 142 | .. code-block:: rust 143 | 144 | fn sys_mmap(start: usize, len: usize, prot: usize) -> isize 145 | 146 | - syscall ID:222 147 | - 申请长度为 len 字节的物理内存(不要求实际物理内存位置,可以随便找一块),将其映射到 start 开始的虚存,内存页属性为 prot 148 | - 参数: 149 | - start 需要映射的虚存起始地址,要求按页对齐 150 | - len 映射字节长度,可以为 0 151 | - prot:第 0 位表示是否可读,第 1 位表示是否可写,第 2 位表示是否可执行。其他位无效且必须为 0 152 | - 返回值:执行成功则返回 0,错误返回 -1 153 | - 说明: 154 | - 为了简单,目标虚存区间要求按页对齐,len 可直接按页向上取整,不考虑分配失败时的页回收。 155 | - 可能的错误: 156 | - start 没有按页大小对齐 157 | - prot & !0x7 != 0 (prot 其余位必须为0) 158 | - prot & 0x7 = 0 (这样的内存无意义) 159 | - [start, start + len) 中存在已经被映射的页 160 | - 物理内存不足 161 | 162 | munmap 定义如下: 163 | 164 | .. code-block:: rust 165 | 166 | fn sys_munmap(start: usize, len: usize) -> isize 167 | 168 | - syscall ID:215 169 | - 取消到 [start, start + len) 虚存的映射 170 | - 参数和返回值请参考 mmap 171 | - 说明: 172 | - 为了简单,参数错误时不考虑内存的恢复和回收。 173 | - 可能的错误: 174 | - [start, start + len) 中存在未被映射的虚存。 175 | 176 | 177 | TIPS:注意 prot 参数的语义,它与内核定义的 MapPermission 有明显不同! 178 | 179 | 实验要求 180 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 181 | 182 | - 实现分支:ch4-lab 183 | - 实验目录要求不变 184 | - 通过所有测例 185 | 186 | 在 os 目录下 ``make run TEST=1`` 测试 sys_get_time, ``make run TEST=2`` 测试 map 和 unmap。 187 | 188 | challenge: 支持多核。 189 | 190 | 问答作业 191 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 192 | 193 | 无 194 | 195 | 实验练习的提交报告要求 196 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 197 | 198 | * 简单总结本次实验与上个实验相比你增加的东西。(控制在5行以内,不要贴代码) 199 | * 完成问答问题。 200 | * (optional) 你对本次实验设计及难度的看法。 201 | -------------------------------------------------------------------------------- /source/chapter0/2os-interface.rst: -------------------------------------------------------------------------------- 1 | 操作系统的系统调用接口 2 | ================================================ 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 5 7 | 8 | API与ABI 9 | -------------------- 10 | 11 | 站在使用操作系统的角度会比较容易对操作系统内核的功能产生初步的认识。操作系统内核是一个提供各种服务的软件,其服务对象是应用程序,而用户(这里可以理解为一般使用计算机的人)是通过应用程序的服务间接获得操作系统的服务的,因此操作系统内核藏在一般用户看不到的地方。但应用程序需要访问操作系统获得操作系统的服务,这就需要通过操作系统的接口才能完成。操作系统与运行在用户态软件之间的接口形式就是上一节提到的应用程序二进制接口 (ABI, Application Binary Interface)。 12 | 13 | 操作系统不能只提供面向单一编程语言的函数库的编程接口 (API, Application Programming Interface) ,它的接口需要考虑对基于各种编程语言的应用支持,以及访问安全等因素,使得应用软件不能像访问函数库一样的直接访问操作系统内部函数,更不能直接读写操作系统内部的地址空间。为此,操作系统设计了一套安全可靠的二进制接口,我们称为系统调用接口 (System Call Interface)。系统调用接口通常面向应用程序提供了 API 的描述,但在具体实现上,还需要提供 ABI 的接口描述规范。 14 | 15 | 在现代处理器的安全支持(特权级隔离,内存空间隔离等)下,应用程序就不能直接以函数调用的方式访问操作系统的函数,以及直接读写操作系统的数据变量。不同类型的应用程序可以通过符合操作系统规定的系统调用接口,发出系统调用请求,来获得操作系统的服务。操作系统提供完服务后,返回应用程序继续执行。 16 | 17 | 18 | .. note:: 19 | 20 | **API 与 ABI 的区别** 21 | 22 | 应用程序二进制接口 ABI 是不同二进制代码片段的连接纽带。ABI 定义了二进制机器代码级别的规则,主要包括基本数据类型、通用寄存器的使用、参数的传递规则、以及堆栈的使用等等。ABI 与处理器和内存地址等硬件架构相关,是用来约束链接器 (Linker) 和汇编器 (Assembler) 的。在同一处理器下,基于不同高级语言编写的应用程序、库和操作系统,如果遵循同样的 ABI 定义,那么它们就能正确链接和执行。 23 | 24 | 应用程序编程接口 API 是不同源代码片段的连接纽带。API 定义了一个源码级(如 C 语言)函数的参数,参数的类型,函数的返回值等。因此 API 是用来约束编译器 (Compiler) 的:一个 API 是给编译器的一些指令,它规定了源代码可以做以及不可以做哪些事。API 与编程语言相关,如 libc 是基于 C 语言编写的标准库,那么基于 C 的应用程序就可以通过编译器建立与 libc 的联系,并能在运行中正确访问 libc 中的函数。 25 | 26 | .. chyyuu 应该给具体的例子,说明 API, ABI的区别。学生提问"一直想不出来ABI是怎么被用户空间程序调用的" 27 | 28 | 系统调用接口与功能 29 | ------------------------------ 30 | 31 | 对于通用的应用程序,一般需要关注如下问题,并希望得到操作系统的支持: 32 | 33 | - 一个运行的程序如何能输出字符信息?如何能获得输入字符信息? 34 | - 一个运行的程序可以要求更多(或更少)的内存空间吗? 35 | - 一个运行的程序如何持久地存储用户数据? 36 | - 一个运行的程序如何与连接到计算机的设备通信并通过它们与物理世界通信? 37 | - 多个运行的程序如何同步互斥地对共享资源进行访问? 38 | - 一个运行的程序可以创建另一个程序的实例吗?需要等待另外一个程序执行完成吗?一个运行的程序能暂停或恢复另一个正在运行的程序吗? 39 | 40 | 操作系统主要通过基于 ABI 的系统调用接口来给应用程序提供上述服务,以支持应用程序的各种需求。对于实际操作系统而言,有多少操作系统,就有多少种不同类型的系统调用接口。通用操作系统为支持各种应用的服务需求,需要有相对多的系统调用服务接口,比如目前 Linux 有超过三百个的系统调用接口。下面列出了一些相对比较重要的操作系统接口或抽象,以及它们的大致功能: 41 | 42 | * 进程(即程序运行过程)管理:复制创建进程 fork 、退出进程 exit 、执行进程 exec 等。 43 | * 线程管理:线程(即程序的一个执行流)的创建、执行、调度切换等。 44 | * 线程同步互斥的并发控制:互斥锁 mutex 、信号量 semaphore 、管程 monitor 、条件变量 condition variable 等。 45 | * 进程间通信:管道 pipe 、信号 signal 、事件 event 等。 46 | * 虚存管理:内存空间映射 mmap 、改变数据段地址空间大小 sbrk 、共享内存 shm 等。 47 | * 文件 I/O 操作:对存储设备中的文件进行读 read 、写 write 、打开 open 、关闭 close 等操作。 48 | * 外设 I/O 操作:外设包括键盘、显示器、串口、磁盘、时钟 ... ,主要采用文件 I/O 操作接口。 49 | 50 | .. note:: 51 | 52 | 上述表述在某种程度上说明了操作系统对计算机硬件重要组成的抽象和虚拟化,这样会有助于应用程序开发。应用程序员只需访问统一的抽象概念(如文件、进程等),就可以使用各种复杂的计算机物理资源(处理器、内存、外设等): 53 | 54 | * 文件 (File) 是外设的一种抽象和虚拟化。特别对于存储外设而言,文件是持久存储的抽象。 55 | * 地址空间 (Address Space) 是对内存的抽象和虚拟化。 56 | * 进程 (Process) 是对计算机资源的抽象和虚拟化。而其中最核心的部分是对 CPU 的抽象与虚拟化。 57 | 58 | 59 | 60 | .. image:: run-app.png 61 | :align: center 62 | :name: run-app 63 | 64 | 有了这些系统调用接口,简单的应用程序就不用考虑底层硬件细节,可以在操作系统的服务支持和管理下简洁地完成其应用功能了。在现阶段,也许大家对进程、文件、地址空间等抽象概念还不了解,在接下来的章节会对这些概念有进一步的介绍。值得注意的是,我们设计的各种操作系统总共只用到三十个左右系统调用功能接口(如下表所示),就可以支持应用需要的上述功能。而且这些调用与最初的 UNIX 的系统调用接口类似,几乎没有变化。尽管UNIX 的系统调用最早是在 1970 年左右设计和实现的,但这些调用中的大多数仍然在今天的系统中广泛使用。 65 | 66 | .. chyyuu 在线组织表格 https://tableconvert.com/restructuredtext-generator 再用 format current (ctrl-alt-T C)格式化 67 | 68 | 69 | 70 | ==== ==================== ============= =============================== 71 | 编号 系统调用 所在章节 功能描述 72 | ==== ==================== ============= =============================== 73 | 1 sys_exit 2 结束执行 74 | 2 sys_write 2/6 (2)输出字符串/(6)写文件 75 | 3 sys_yield 3 暂时放弃执行 76 | 4 sys_get_time 3 获取当前时间 77 | 5 sys_getpid 5 获取进程id 78 | 6 sys_fork 5 创建子进程 79 | 7 sys_exec 5 执行新程序 80 | 8 sys_waitpid 5 等待子进程结束 81 | 9 sys_read 5/6 (5)读取字符串/(6)读文件 82 | 10 sys_open 6 打开/创建文件 83 | 11 sys_close 6 关闭文件 84 | 12 sys_dup 7 复制文件描述符 85 | 13 sys_pipe 7 创建管道 86 | 14 sys_kill 7 发送信号给某进程 87 | 15 sys_sigaction 7 设立信号处理例程 88 | 16 sys_sigprocmask 7 设置要阻止的信号 89 | 17 sys_sigreturn 7 从信号处理例程返回 90 | 18 sys_sleep 8 进程休眠一段时间 91 | 19 sys_thread_create 8 创建线程 92 | 20 sys_gettid 8 获取线程id 93 | 21 sys_waittid 8 等待线程结束 94 | 22 sys_mutex_create 8 创建锁 95 | 23 sys_mutex_lock 8 获取锁 96 | 24 sys_mutex_unlock 8 释放锁 97 | 25 sys_semaphore_create 8 创建信号量 98 | 26 sys_semaphore_up 8 减少信号量的计数 99 | 27 sys_semaphore_down 8 增加信号量的计数 100 | 28 sys_condvar_create 8 创建条件变量 101 | 29 sys_condvar_signal 8 唤醒阻塞在条件变量上的线程 102 | 30 sys_condvar_wait 8 阻塞与此条件变量关联的当前线程 103 | ==== ==================== ============= =============================== 104 | 105 | 106 | 系统调用接口举例 107 | --------------------------------------------------- 108 | 109 | .. chyyuu 可以有两个例子,体现API和ABI 110 | #![feature(asm)] 111 | let cmd = 0xd1; 112 | unsafe { 113 | asm!("out 0x64, eax", in("eax") cmd); 114 | } 115 | 116 | use std::io::{self, Write}; 117 | 118 | fn main() -> io::Result<()> { 119 | io::stdout().write_all(b"hello world")?; 120 | 121 | Ok(()) 122 | } 123 | 124 | 125 | 我们以rCore-Tutorial中的例子,一个应用程序显示一个字符串,来看看系统调用的具体内容。应用程序的代码如下: 126 | 127 | .. code-block:: rust 128 | :linenos: 129 | 130 | // user/src/bin/hello_world.rs 131 | ... 132 | pub fn main() -> i32 { 133 | println!("Hello world from user mode program!"); 134 | 0 135 | } 136 | 137 | 这个程序的功能就是显示一行字符串(重点看第8行的代码)。注意,这里的 `println!` 一个宏。而进一步跟踪源代码 (位于 `user/src/console.rs` ),可以看到 `println!` 会进一步展开为 `write` 函数: 138 | 139 | .. code-block:: rust 140 | :linenos: 141 | 142 | // user/src/console.rs 143 | ... 144 | impl Write for Stdout { 145 | fn write_str(&mut self, s: &str) -> fmt::Result { 146 | write(STDOUT, s.as_bytes()); 147 | Ok(()) 148 | } 149 | } 150 | 151 | 这个write函数就是对系统调用 `sys_write` 的封装: 152 | 153 | 154 | .. code-block:: rust 155 | :linenos: 156 | 157 | // user/src/lib.rs 158 | ... 159 | pub fn write(fd: usize, buf: &[u8]) -> isize { 160 | sys_write(fd, buf) 161 | } 162 | 163 | // user/src/syscall.rs 164 | ... 165 | pub fn sys_write(fd: usize, buffer: &[u8]) -> isize { 166 | syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()]) 167 | } 168 | 169 | 170 | `sys_write` 用户库函数封装了 `sys_write` 系统调用的API接口,这个系统调用API的参数和返回值的含义如下: 171 | 172 | - `SYSCALL_WRITE` 表示 `sys_write` 的系统调用号 173 | - `fd` 表示待写入文件的文件描述符; 174 | - `buf` 表示内存中缓冲区的起始地址; 175 | - `len` 表示内存中缓冲区的长度; 176 | - 返回值:返回成功写入的长度或错误值 177 | 178 | 而 `sys_write` 系统调用的ABI接口描述了具体用哪些寄存器来保存参数和返回值: 179 | 180 | .. code-block:: rust 181 | :linenos: 182 | 183 | // user/src/syscall.rs 184 | ... 185 | fn syscall(id: usize, args: [usize; 3]) -> isize { 186 | let mut ret: isize; 187 | unsafe { 188 | asm!( 189 | "ecall", 190 | inlateout("x10") args[0] => ret, 191 | in("x11") args[1], 192 | in("x12") args[2], 193 | in("x17") id 194 | ); 195 | } 196 | ret 197 | } 198 | 199 | 这里我们看到,API中的各个参数和返回值分别被RISC-V通用寄存器 `x10` (即存放系统调用号,也保存返回值)、 `x11`(存放 `fd` ) 、 `x12` (存放 `buf` )和 `x17` (存放 `len` )保存。 200 | -------------------------------------------------------------------------------- /source/chapter2/ch2.py: -------------------------------------------------------------------------------- 1 | from manimlib.imports import * 2 | 3 | class EnvironmentCallFlow(Scene): 4 | CONFIG = { 5 | "camera_config": { 6 | "background_color": WHITE, 7 | }, 8 | } 9 | def construct(self): 10 | os = Rectangle(height=FRAME_HEIGHT*.8, width=2.0, color=BLACK, fill_color=WHITE, fill_opacity=1.0) 11 | app = Rectangle(height=FRAME_HEIGHT*.8, width=2.0, color=BLACK, fill_color=WHITE, fill_opacity=1.0) 12 | app.shift(np.array([-4, 0, 0])) 13 | see = Rectangle(height=FRAME_HEIGHT*.8, width=2.0, color=BLACK, fill_color=WHITE, fill_opacity=1.0) 14 | see.shift(np.array([4, 0, 0])) 15 | self.add(os, app, see) 16 | os_text = TextMobject("OS", color=BLACK).next_to(os, UP, buff=0.2) 17 | app_text = TextMobject("Application", color=BLACK).next_to(app, UP, buff=0.2) 18 | see_text = TextMobject("SEE", color=BLACK).next_to(see, UP, buff=.2) 19 | self.add(os_text, app_text, see_text) 20 | app_ecall = Rectangle(height=0.5, width=2.0, color=BLACK, fill_color=BLUE, fill_opacity=1.0) 21 | app_ecall.move_to(app) 22 | app_ecall_text = TextMobject("ecall", color=BLACK).move_to(app_ecall) 23 | self.add(app_ecall, app_ecall_text) 24 | app_code1 = Rectangle(width=2.0, height=2.0, color=BLACK, fill_color=GREEN, fill_opacity=1.0) 25 | app_code1.next_to(app_ecall, UP, buff=0) 26 | self.add(app_code1) 27 | app_code2 = Rectangle(width=2.0, height=2.5, color=BLACK, fill_color=GREEN, fill_opacity=1.0) 28 | app_code2.next_to(app_ecall, DOWN, buff=0) 29 | self.add(app_code2) 30 | app_code1_text = TextMobject("U Code", color=BLACK).move_to(app_code1).shift(np.array([-.15, 0, 0])) 31 | app_code2_text = TextMobject("U Code", color=BLACK).move_to(app_code2).shift(np.array([-.15, 0, 0])) 32 | self.add(app_code1_text, app_code2_text) 33 | os_ecall = Rectangle(height=.5, width=2.0, color=BLACK, fill_color=BLUE, fill_opacity=1.0) 34 | os_ecall.move_to(os) 35 | os_ecall_text = TextMobject("ecall", color=BLACK).move_to(os_ecall) 36 | self.add(os_ecall, os_ecall_text) 37 | os_code1 = Rectangle(width=2.0, height=2.0, color=BLACK, fill_color=PURPLE, fill_opacity=1.0).next_to(os_ecall, UP, buff=0) 38 | os_code1_text = TextMobject("S Code", color=BLACK).move_to(os_code1).shift(np.array([-.15, 0, 0])) 39 | os_code2 = Rectangle(width=2.0, height=2.5, color=BLACK, fill_color=PURPLE, fill_opacity=1.0).next_to(os_ecall, DOWN, buff=0) 40 | os_code2_text = TextMobject("S Code", color=BLACK).move_to(os_code2).shift(np.array([-.15, 0, 0])) 41 | self.add(os_code1, os_code2, os_code1_text, os_code2_text) 42 | app_ecall_anchor = app_ecall.get_center() + np.array([0.8, 0, 0]) 43 | app_front = Line(start=app_ecall_anchor+np.array([0, 2, 0]), end=app_ecall_anchor, color=RED) 44 | app_front.add_tip(tip_length=0.2) 45 | self.add(app_front) 46 | os_ecall_anchor = os_ecall.get_center() + np.array([0.8, 0, 0]) 47 | os_front = Line(start=os_ecall_anchor+np.array([0, 2, 0]), end=os_ecall_anchor, color=RED) 48 | os_front.add_tip(tip_length=.2) 49 | self.add(os_front) 50 | trap_to_os = DashedLine(start=app_ecall_anchor, end=os_ecall_anchor+np.array([0, 2, 0]), color=RED) 51 | trap_to_os.add_tip(tip_length=.2) 52 | self.add(trap_to_os) 53 | see_entry = see.get_center()+np.array([0.8, 2, 0]) 54 | see_exit = see_entry+np.array([0, -4, 0]) 55 | see_code = Rectangle(width=2.0, height=see_entry[1]-see_exit[1], color=BLACK, fill_color=GRAY, fill_opacity=1.0).move_to(see) 56 | self.add(see_code) 57 | see_text = TextMobject("M Code", color=BLACK).move_to(see_code).shift(np.array([-.15, 0, 0])) 58 | self.add(see_text) 59 | see_front = Line(start=see_entry, end=see_exit, color=RED).add_tip(tip_length=.2) 60 | self.add(see_front) 61 | trap_to_see = DashedLine(start=os_ecall_anchor, end=see_entry, color=RED).add_tip(tip_length=.2) 62 | self.add(trap_to_see) 63 | os_back_anchor = os_ecall_anchor+np.array([0, -.5, 0]) 64 | trap_back_to_os = DashedLine(start=see_exit, end=os_back_anchor, color=RED).add_tip(tip_length=.2) 65 | self.add(trap_back_to_os) 66 | os_exit = os_back_anchor+np.array([0, -2, 0]) 67 | os_front2 = Line(start=trap_back_to_os, end=os_exit, color=RED).add_tip(tip_length=.2) 68 | self.add(os_front2) 69 | app_back_anchor = app_ecall_anchor+np.array([0, -.5, 0]) 70 | trap_back_to_app = DashedLine(start=os_exit, end=app_back_anchor, color=RED).add_tip(tip_length=.2) 71 | self.add(trap_back_to_app) 72 | app_front2 = Line(start=app_back_anchor, end=app_back_anchor+np.array([0, -2, 0]), color=RED) 73 | app_front2.add_tip(tip_length=.2) 74 | self.add(app_front2) 75 | u_into_s = TextMobject("U into S", color=BLACK).next_to(app_ecall, RIGHT, buff=0).shift(np.array([0, .5, 0])).scale(0.5) 76 | s_back_u = TextMobject("S back to U", color=BLACK).next_to(app_ecall, RIGHT, buff=0).shift(np.array([-.3, -1, 0])).scale(0.5) 77 | s_into_m = TextMobject("S into M", color=BLACK).next_to(os_ecall, RIGHT, buff=0).shift(np.array([0, .5, 0])).scale(.5) 78 | m_back_s = TextMobject("M back to S", color=BLACK).next_to(os_ecall, RIGHT, buff=0).shift(np.array([-.3, -1, 0])).scale(.5) 79 | self.add(u_into_s, s_back_u, s_into_m, m_back_s) 80 | 81 | 82 | 83 | class PrivilegeStack(Scene): 84 | CONFIG = { 85 | "camera_config": { 86 | "background_color": WHITE, 87 | }, 88 | } 89 | def construct(self): 90 | os = Rectangle(width=4.0, height=1.0, color=BLACK, fill_color=WHITE, fill_opacity=1.0) 91 | os_text = TextMobject("OS", color=BLACK).move_to(os) 92 | self.add(os, os_text) 93 | 94 | sbi = Rectangle(width=4.0, height=1.0, color=BLACK, fill_color=BLACK, fill_opacity=1.0) 95 | sbi.next_to(os, DOWN, buff=0) 96 | sbi_text = TextMobject("SBI", color=WHITE).move_to(sbi) 97 | self.add(sbi, sbi_text) 98 | 99 | see = Rectangle(width=4.0, height=1.0, color=BLACK, fill_color=WHITE, fill_opacity=1.0) 100 | see.next_to(sbi, DOWN, buff=0) 101 | see_text = TextMobject("SEE", color=BLACK).move_to(see) 102 | self.add(see, see_text) 103 | 104 | abi0 = Rectangle(height=1.0, width=1.8, color=BLACK, fill_color=BLACK, fill_opacity=1.0) 105 | abi0.next_to(os, UP, buff=0).align_to(os, LEFT) 106 | abi0_text = TextMobject("ABI", color=WHITE).move_to(abi0) 107 | self.add(abi0, abi0_text) 108 | 109 | abi1 = Rectangle(height=1.0, width=1.8, color=BLACK, fill_color=BLACK, fill_opacity=1.0) 110 | abi1.next_to(os, UP, buff=0).align_to(os, RIGHT) 111 | abi1_text = TextMobject("ABI", color=WHITE).move_to(abi1) 112 | self.add(abi1, abi1_text) 113 | 114 | app0 = Rectangle(height=1.0, width=1.8, color=BLACK, fill_color=WHITE, fill_opacity=1.0) 115 | app0.next_to(abi0, UP, buff=0) 116 | app0_text = TextMobject("App", color=BLACK).move_to(app0) 117 | self.add(app0, app0_text) 118 | 119 | app1 = Rectangle(height=1.0, width=1.8, color=BLACK, fill_color=WHITE, fill_opacity=1.0) 120 | app1.next_to(abi1, UP, buff=0) 121 | app1_text = TextMobject("App", color=BLACK).move_to(app1) 122 | self.add(app1, app1_text) 123 | 124 | line0 = DashedLine(sbi.get_right(), sbi.get_right() + np.array([3, 0, 0]), color=BLACK) 125 | self.add(line0) 126 | line1 = DashedLine(abi1.get_right(), abi1.get_right() + np.array([3, 0, 0]), color=BLACK) 127 | self.add(line1) 128 | 129 | machine = TextMobject("Machine", color=BLACK).next_to(see, RIGHT, buff=.8) 130 | supervisor = TextMobject("Supervisor", color=BLACK).next_to(os, RIGHT, buff=.8) 131 | user = TextMobject("User", color=BLACK).next_to(app1, RIGHT, buff=.8) 132 | self.add(machine, supervisor, user) 133 | 134 | -------------------------------------------------------------------------------- /source/chapter8/5concurrency-problem.rst: -------------------------------------------------------------------------------- 1 | 并发中的问题 2 | ========================================= 3 | 4 | 本节导读 5 | ----------------------------------------- 6 | 7 | 应用程序员在开发并发应用的过程中,经常由于各种 `不小心` 编写出各种并发缺陷。并发缺陷有很多种,典型的主要有三类:互斥缺陷、同步缺陷和死锁缺陷。了解这些缺陷的模式是写出健壮、正确并发程序的关键。 8 | 9 | 10 | 11 | 互斥缺陷 12 | ----------------------------------------- 13 | 14 | 互斥缺陷也称为违反原子性缺陷,在并发应用程序中对共享变量没进行合理的保护是导致出现这类缺陷的一个重要原因。下面是一个简单的例子: 15 | 16 | .. code-block:: Rust 17 | :linenos: 18 | :emphasize-lines: 4,10 19 | 20 | static mut A: usize = 0; 21 | //... other code 22 | unsafe fn thr1() -> ! { 23 | if (A == 0) { 24 | println!("thr1: A is Zero --> {}", A); 25 | } 26 | //... other code 27 | } 28 | unsafe fn thr2() -> ! { 29 | A = A+1; 30 | println!("thr2: A is One --> {}", A); 31 | } 32 | 33 | A是共享变量。粗略地看,可以估计执行流程为:第一个线程thr1检查A的值,如果为0,则显示“"thr1: A is Zero --> 0”;第二个线程thr2设置A的值为2,并显示"thr2: A is One --> 1”。但如果线程thr1执行完第4行代码,准备执行第5行代码前发生了线程切换,开始执行线程th2;当线程thr2完成第10行后,操作系统有切换回线程thr1继续执行,那么线程thr1就会输出“thr1: A is Zero --> 1” 这样的奇怪结果。 34 | 35 | 这里出现问题的根源是线程在对共享变量进行访问时,违反了临界区的互斥性(原子性)原则。解决这样的问题需要给共享变量的访问加锁,确保每个线程访问共享变量时,都持有锁,修改后的代码如下: 36 | 37 | .. code-block:: Rust 38 | :linenos: 39 | 40 | static mut A: usize = 0; 41 | //... other code 42 | unsafe fn thr1() -> ! { 43 | mutex.lock(); 44 | if (A == 0) { 45 | println!("thr1: A is Zero --> {}", A); 46 | } 47 | mutex.unlock(); 48 | //... other code 49 | } 50 | unsafe fn thr2() -> ! { 51 | mutex.lock(); 52 | A = A+1; 53 | println!("thr2: A is One --> {}", A); 54 | mutex.unlock(); 55 | } 56 | 57 | 这种问题如果能发现,那么修复相对比较简单,即在对共享变量进行访问的代码区域前后加上请求锁和释放锁的操作。但主要的问题是发现缺陷比较难,特别是代码量比较大,代码的控制逻辑比较复杂的情况。 58 | 59 | 同步缺陷 60 | ----------------------------------------- 61 | 62 | 同步缺陷也称为违反顺序缺陷,在并发应用程序中对共享变量访问的先后顺序的可能性没有充分分析是导致出现这类缺陷的一个重要原因。下面是一个简单的例子: 63 | 64 | .. code-block:: Rust 65 | :linenos: 66 | :emphasize-lines: 5,9 67 | 68 | static mut A: usize = 0; 69 | ... 70 | unsafe fn thr1() -> ! { 71 | ... //在某种情况下会休眠 72 | A = 1; 73 | ... 74 | } 75 | unsafe fn thr2() -> ! { 76 | if A==1 { 77 | println!("Correct"); 78 | }else{ 79 | println!("Panic"); 80 | } 81 | } 82 | pub fn main() -> i32 { 83 | let mut v = Vec::new(); 84 | v.push(thread_create(thr1 as usize, 0)); 85 | sleep(10); 86 | ... 87 | v.push(thread_create(thr2 as usize, 0)); 88 | ... 89 | } 90 | 91 | A是共享变量。粗略地看,可以估计执行流程为:线程thr1先被创建,等了10ms后,线程thr2再被创建。一般情况下,这就导致了thr1先于thr2执行,即第5行会先于第10行执行,得到预期的结果。但可能出现一种执行情况:线程thr1在执行第5句前,由于某种原因进入了休眠,导致线程thr2执行第10行在前,线程th1执行第5行在后,导致获得非预期的错误结果。 92 | 93 | 这里出现问题的根源是线程在对共享变量进行访问时,违反了临界区的预期顺序原则。解决这样的问题需要给线程的相关代码位置加上同步操作(如通过信号量或条件变量等),确保线程间的执行顺序符合预期,修改后的代码如下: 94 | 95 | .. code-block:: Rust 96 | :linenos: 97 | :emphasize-lines: 5,9 98 | 99 | static mut A: usize = 0; 100 | semaphore.value = 0; //信号量初值为0 101 | unsafe fn thr1() -> ! { 102 | ... //在某种情况下会休眠 103 | A = 1; 104 | semaphore.up(); 105 | ... 106 | } 107 | unsafe fn thr2() -> ! { 108 | semaphore.down(); // 需要等待 semaphore.up()的唤醒 109 | if A==1 { 110 | println!("Correct"); 111 | }else{ 112 | println!("Panic"); 113 | } 114 | } 115 | pub fn main() -> i32 { 116 | let mut v = Vec::new(); 117 | v.push(thread_create(thr1 as usize, 0)); 118 | sleep(10); 119 | ... 120 | v.push(thread_create(thr2 as usize, 0)); 121 | ... 122 | } 123 | 124 | 125 | 这种问题如果能发现,那么修复相对也比较简单,即在线程的代码区域设置合理的同步操作,让线程间的执行顺序符合预期。但主要的问题还是发现缺陷比较难,特别是代码量比较大,代码的控制逻辑比较复杂的情况。 126 | 127 | 也许有同学说,这样的错误缺陷很容易发现呀,只要开发者在编写时注意一下,就可以了。但其实不尽然,因为我们这里给出的是一个刻意简化的例子,在实际的并发应用程序中,由于代码量远大于这个例子,控制逻辑会有循环、跳转、函数调用等,涉及到的共享变量的数量、访问操作,以及与互斥/同步操作的关系等会错综复杂,难以一下子就能一目了然地分析清楚,导致很容易出现互斥和同步缺陷。 128 | 129 | 130 | 131 | 死锁缺陷 132 | ----------------------------------------- 133 | 134 | 除了上面的两类并发缺陷,还有一类导致程序无法正常执行的并发缺陷 -- 死锁(Dead lock)。在并发应用中,经常需要线程排他性地访问若干种资源。大部分死锁都和不可抢占的资源相关,这里把线程需要申请获取、排他性使用和释放的对象称为资源(resource)。需要互斥访问的共享变量就是一种资源。操作系统通过互斥锁、信号量或条件变量等同步互斥机制,能授权一个线程(临时)具有排他地访问某一种资源的能力。下面是一个死锁的例子: 135 | 136 | 137 | .. code-block:: Rust 138 | :linenos: 139 | 140 | unsafe fn thr1() -> ! { 141 | mutex1.lock(); 142 | mutex2.lock(); 143 | ... 144 | } 145 | unsafe fn thr2() -> ! { 146 | mutex2.lock(); 147 | mutex1.lock(); 148 | ... 149 | } 150 | 151 | 当线程thr1持有锁mutex1,正在等待另外一个锁mutex2,而线程thr2持有锁mutex2,正在等待另外一个锁mutex1时,死锁就产生了。对于这个代码,可以很容易避免死锁: 152 | 153 | .. code-block:: Rust 154 | :linenos: 155 | 156 | unsafe fn thr1() -> ! { 157 | mutex1.lock(); 158 | mutex2.lock(); 159 | ... 160 | } 161 | unsafe fn thr2() -> ! { 162 | mutex1.lock(); 163 | mutex2.lock(); 164 | ... 165 | } 166 | 167 | 168 | 只要线程thr1和线程thr2都用相同的请求锁顺序,就不会发生死锁了。但这与上面的分析一样,对于实际的复杂程序,发现死锁就是一个很费劲的事情。目前计算机科学家对死锁的研究比较深入,指出了死锁产生的四个必要条件: 169 | 170 | 171 | - 互斥:线程互斥地访问资源。 172 | - 持有并等待:线程已持有了部分资源,同时又在等待其他资源。 173 | - 非抢占:线程已持有的资源不能被抢占。 174 | - 循环等待:线程之间存在一个资源持有/等待的环,环上每个线程都持有部分资源,而这部分资源又是下一个线程在等待申请的资源。 175 | 176 | 177 | 178 | 死锁预防 179 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 180 | 181 | 如果线程间产生了死锁,那么上面四个条件一定会发生。换个角度来看,如果这四个条件中的任意一个没有满足,死锁就不会产生。 182 | 183 | 一个比较实用的预防死锁的方法是打破循环等待,具体做法就是给锁/访问的资源进行排序,要求每个线程都按照排好的顺序依次申请锁和访问资源。这种顺序性避免了循环等待,也就不会产生死锁。 184 | 185 | 186 | 187 | 死锁避免 188 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 189 | 190 | 计算机科学家Dijkstra在1965年为THE操作系统设计提出的一种死锁避免(avoidance)的调度算法,称为银行家算法(banker's algorithm)算法的核心是判断满足线程的资源请求是否会导致整个系统进入不安全状态。如果是,就拒绝线程的资源请求;如果满足请求后系统状态仍然是安全的,就分配资源给线程。 191 | 192 | 193 | 194 | 状态是安全的,是指存在一个资源分配/线程执行序列使得所有的线程都能获取其所需资源并完成线程的工作。如果找不到这样的资源分配/线程执行序列,那么状态是不安全的。这里把线程的执行过程简化为:申请资源、释放资源的一系列资源操作。这意味这线程执行完毕后,会释放其占用的所有资源。 195 | 196 | 我们需要知道,不安全状态并不等于死锁,而是指有死锁的可能性。安全全状态和不安全状态的区别是:从安全状态出发,操作系统通过调度线程执行序列,能够保证所有线程都能完成,一定不会出现死锁;而从不安全状态出发,就没有这样的保证,可能出现死锁。 197 | 198 | .. chyyuu 有一个安全,不安全,死锁的图??? 199 | 200 | 银行家算法的数据结构 201 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 202 | 203 | 为了描述操作系统中可利用的资源、所有线程对资源的最大需求、系统中的资源分配,以及所有线程还需要多少资源的情况,需要定义对应的四个数据结构: 204 | 205 | - 可利用资源向量 Available:含有 m 个元素的一维数组,每个元素代表可利用的某一类资源的数目,其初值是该类资源的全部可用数目,其值随该类资源的分配和回收而动态地改变。Available[j] = k,表示第j类资源的可用数量为k。 206 | - 最大需求矩阵Max:n * m矩阵,表示n个线程中,每个线程对m类资源的最大需求量。Max[i,j] = h,表示线程i需要第j类资源的最大数量为h。 207 | - 分配矩阵 Allocation:n * m矩阵,表示每类资源已分配给每个线程的资源数。Allocation[i,j] = g,则表示线程i当前己分得第j类资源的数量为g。 208 | - 需求矩阵Need:n * m的矩阵,表示每个线程还需要的各类资源数量。Need[i,j] = d,则表示线程i还需要第j类资源的数量为d。 209 | 210 | 上述三个矩阵间存在如下关系:  211 | 212 | Need[i,j] = Max[i,j] - allocation[i, j] 213 | 214 | 215 | 银行家算法的步骤 216 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 217 | 218 | 设 Request是线程的请求资源矩阵,如果 Requesti[i,j] = t,表示线程thr[i]需要t个第j类型的资源。当线程thr[i]发出资源请求后,操作系统的银行家算法按下述步骤执行: 219 | 220 | 1. 如果 Request[i,j] ≤ Need[i,j],则转步骤2;否则出错,因为线程所需的资源数已超过它所宣布的最大值。 221 | 2. 如果 Request[i,j] ≤ Available[j],则转步骤3;否则,表示尚无足够资源,线程thr[i]进入等待状态。 222 | 3. 操作系统试着把资源分配给线程thr[i],并修改下面数据结构中的值: 223 | 224 | .. code-block:: Rust 225 | :linenos: 226 | 227 | Available[j] = Available[j] - Request[i,j]; 228 | Allocation[i,j] = Allocation[i,j] + Request[i,j]; 229 | Need[i,j] = Need[i,j] - Request[i,j]; 230 | 231 | 4. 操作系统执行安全性检查算法,检查此次资源分配后系统是否处于安全状态。若安全,则实际将资源分配给线程thr[i];否则不进行资源分配,让线程thr[i]等待。 232 |    233 | 安全性检查算法 234 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 235 | 236 | 安全性检查算法如下: 237 | 238 | 1. 设置两个向量:工作向量Work,表示操作系统可提供给线程继续运行所需的各类资源数目,它含有m个元素,初始时,Work = Available;结束向量Finish,表示系统是否有足够的资源分配给线程,使之运行完成。初始时 Finish[0..n-1] = false,表示所有线程都没结束;当有足够资源分配给线程时,设置Finish[i] = true。 239 | 2. 从线程集合中找到一个能满足下述条件的线程 240 | 241 | .. code-block:: Rust 242 | :linenos: 243 | 244 | Finish[i] == false; 245 | Need[i,j] <= Work[j]; 246 | 247 | 若找到,执行步骤3,否则,执行步骤4。 248 | 249 | 3. 当线程thr[i]获得资源后,可顺利执行,直至完成,并释放出分配给它的资源,故应执行: 250 | 251 | .. code-block:: Rust 252 | :linenos: 253 | 254 | Work[j] = Work[j] + Allocation[i,j]; 255 | Finish[i] = true; 256 | 257 | 跳转回步骤2 258 | 259 | 4. 如果Finish[0..n-1] 都为true,则表示系统处于安全状态;否则表示系统处于不安全状态。 260 | 261 | 262 | 通过操作系统调度,如银行家算法来避免死锁不是广泛使用的通用方案。因为从线程执行的一般情况上看,银行家算法需要提前获知线程总的资源申请量,以及未来的每一次请求,而这些请求对于一般线程而言在运行前是不可知或随机的。另外,即使在某些特殊情况下,可以提前知道线程的资源申请量等信息,多重循环的银行家算法开销也是很大的,不适合于对性能要求很高的操作系统中。 -------------------------------------------------------------------------------- /source/chapter1/7exercise.rst: -------------------------------------------------------------------------------- 1 | 练习 2 | ===================================================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :maxdepth: 4 7 | 8 | 9 | 课后练习 10 | ------------------------------- 11 | 12 | 编程题 13 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 14 | 1. `*` 实现一个linux应用程序A,显示当前目录下的文件名。(用C或Rust编程) 15 | 2. `***` 实现一个linux应用程序B,能打印出调用栈链信息。(用C或Rust编程) 16 | 3. `**` 实现一个基于rcore/ucore tutorial的应用程序C,用sleep系统调用睡眠5秒(in rcore/ucore tutorial v3: Branch ch1) 17 | 18 | 注: 尝试用GDB等调试工具和输出字符串的等方式来调试上述程序,能设置断点,单步执行和显示变量,理解汇编代码和源程序之间的对应关系。 19 | 20 | 21 | 问答题 22 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 23 | 24 | 1. `*` 应用程序在执行过程中,会占用哪些计算机资源? 25 | 2. `*` 请用相关工具软件分析并给出应用程序A的代码段/数据段/堆/栈的地址空间范围。 26 | 3. `*` 请用分析并给出应用程序C的代码段/数据段/堆/栈的地址空间范围。 27 | 4. `*` 请结合编译器的知识和编写的应用程序B,说明应用程序B是如何建立调用栈链信息的。 28 | 5. `*` 请简要说明应用程序与操作系统的异同之处。 29 | 6. `**` 请基于QEMU模拟RISC—V的执行过程和QEMU源代码,说明RISC-V硬件加电后的几条指令在哪里?完成了哪些功能? 30 | 7. `*` RISC-V中的SBI的含义和功能是啥? 31 | 8. `**` 为了让应用程序能在计算机上执行,操作系统与编译器之间需要达成哪些协议? 32 | 9. `**` 请简要说明从QEMU模拟的RISC-V计算机加电开始运行到执行应用程序的第一条指令这个阶段的执行过程。 33 | 10. `**` 为何应用程序员编写应用时不需要建立栈空间和指定地址空间? 34 | 11. `***` 现代的很多编译器生成的代码,默认情况下不再严格保存/恢复栈帧指针。在这个情况下,我们只要编译器提供足够的信息,也可以完成对调用栈的恢复。 35 | 36 | 我们可以手动阅读汇编代码和栈上的数据,体验一下这个过程。例如,对如下两个互相递归调用的函数: 37 | 38 | .. code-block:: 39 | 40 | void flip(unsigned n) { 41 | if ((n & 1) == 0) { 42 | flip(n >> 1); 43 | } else if ((n & 1) == 1) { 44 | flap(n >> 1); 45 | } 46 | } 47 | 48 | void flap(unsigned n) { 49 | if ((n & 1) == 0) { 50 | flip(n >> 1); 51 | } else if ((n & 1) == 1) { 52 | flap(n >> 1); 53 | } 54 | } 55 | 56 | 在某种编译环境下,编译器产生的代码不包括保存和恢复栈帧指针 ``fp`` 的代码。以下是 GDB 输出的本次运行的时候,这两个函数所在的地址和对应地址指令的反汇编,为了方便阅读节选了重要的控制流和栈操作(省略部分不含栈操作): 57 | 58 | .. code-block:: 59 | 60 | (gdb) disassemble flap 61 | Dump of assembler code for function flap: 62 | 0x0000000000010730 <+0>: addi sp,sp,-16 // 唯一入口 63 | 0x0000000000010732 <+2>: sd ra,8(sp) 64 | ... 65 | 0x0000000000010742 <+18>: ld ra,8(sp) 66 | 0x0000000000010744 <+20>: addi sp,sp,16 67 | 0x0000000000010746 <+22>: ret // 唯一出口 68 | ... 69 | 0x0000000000010750 <+32>: j 0x10742 70 | 71 | (gdb) disassemble flip 72 | Dump of assembler code for function flip: 73 | 0x0000000000010752 <+0>: addi sp,sp,-16 // 唯一入口 74 | 0x0000000000010754 <+2>: sd ra,8(sp) 75 | ... 76 | 0x0000000000010764 <+18>: ld ra,8(sp) 77 | 0x0000000000010766 <+20>: addi sp,sp,16 78 | 0x0000000000010768 <+22>: ret // 唯一出口 79 | ... 80 | 0x0000000000010772 <+32>: j 0x10764 81 | End of assembler dump. 82 | 83 | 启动这个程序,在运行的时候的某个状态将其打断。此时的 ``pc``, ``sp``, ``ra`` 寄存器的值如下所示。此外,下面还给出了栈顶的部分内容。(为阅读方便,栈上的一些未初始化的垃圾数据用 ``???`` 代替。) 84 | 85 | .. code-block:: 86 | 87 | (gdb) p $pc 88 | $1 = (void (*)()) 0x10752 89 | 90 | (gdb) p $sp 91 | $2 = (void *) 0x40007f1310 92 | 93 | (gdb) p $ra 94 | $3 = (void (*)()) 0x10742 95 | 96 | (gdb) x/6a $sp 97 | 0x40007f1310: ??? 0x10750 98 | 0x40007f1320: ??? 0x10772 99 | 0x40007f1330: ??? 0x10764 100 | 101 | 根据给出这些信息,调试器可以如何复原出最顶层的几个调用栈信息?假设调试器可以理解编译器生成的汇编代码 [#dwarf]_ 。 102 | 103 | 104 | 105 | 实验练习 106 | ------------------------------- 107 | 108 | 实验练习包括实践作业和问答作业两部分。 109 | 110 | 实践作业 111 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 112 | 113 | 彩色化 LOG 114 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 115 | 116 | lab1 的工作使得我们从硬件世界跳入了软件世界,当看到自己的小 os 可以在裸机硬件上输出 ``hello world`` 是不是很高兴呢?但是为了后续的一步开发,更好的调试环境也是必不可少的,第一章的练习要求大家实现更加炫酷的彩色log。 117 | 118 | 详细的原理不多说,感兴趣的同学可以参考 `ANSI转义序列 `_ ,现在执行如下这条命令试试 119 | 120 | .. code-block:: console 121 | 122 | $ echo -e "\x1b[31mhello world\x1b[0m" 123 | 124 | 如果你明白了我们是如何利用串口实现输出,那么要实现彩色输出就十分容易了,只需要用需要输出的字符串替换上一条命令中的 ``hello world``,用期望颜色替换 ``31(代表红色)`` 即可。 125 | 126 | .. warning:: 127 | 128 | 以下内容仅为推荐实现,不是练习要求,有时间和兴趣的同学可以尝试。 129 | 130 | 我们推荐实现如下几个等级的输出,输出优先级依次降低: 131 | 132 | .. list-table:: log 等级推荐 133 | :header-rows: 1 134 | :align: center 135 | 136 | * - 名称 137 | - 颜色 138 | - 用途 139 | * - ERROR 140 | - 红色(31) 141 | - 表示发生严重错误,很可能或者已经导致程序崩溃 142 | * - WARN 143 | - 黄色(93) 144 | - 表示发生不常见情况,但是并不一定导致系统错误 145 | * - INFO 146 | - 蓝色(34) 147 | - 比较中庸的选项,输出比较重要的信息,比较常用 148 | * - DEBUG 149 | - 绿色(32) 150 | - 输出信息较多,在 debug 时使用 151 | * - TRACE 152 | - 灰色(90) 153 | - 最详细的输出,跟踪了每一步关键路径的执行 154 | 155 | 我们可以输出比设定输出等级以及更高输出等级的信息,如设置 ``LOG = INFO``,则输出 ``ERROR``、``WARN``、``INFO`` 等级的信息。简单 demo 如下,输出等级为 INFO: 156 | 157 | .. image:: color-demo.png 158 | 159 | 为了方便使用彩色输出,我们要求同学们实现彩色输出的宏或者函数,用以代替 print 完成输出内核信息的功能,它们有着和 prinf 十分相似的使用格式,要求支持可变参数解析,形如: 160 | 161 | .. code-block:: rust 162 | 163 | // 这段代码输出了 os 内存空间布局,这到这些信息对于编写 os 十分重要 164 | 165 | info!(".text [{:#x}, {:#x})", s_text as usize, e_text as usize); 166 | debug!(".rodata [{:#x}, {:#x})", s_rodata as usize, e_rodata as usize); 167 | error!(".data [{:#x}, {:#x})", s_data as usize, e_data as usize); 168 | 169 | .. code-block:: c 170 | 171 | info("load range : [%d, %d] start = %d\n", s, e, start); 172 | 173 | 在以后,我们还可以在 log 信息中增加线程、CPU等信息(只是一个推荐,不做要求),这些信息将极大的方便你的代码调试。 174 | 175 | 176 | 实验要求 177 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 178 | 179 | - 实现分支:ch1 180 | - 完成实验指导书中的内容并在裸机上实现 ``hello world`` 输出。 181 | - 实现彩色输出宏(只要求可以彩色输出,不要求 log 等级控制,不要求多种颜色) 182 | - 隐形要求 183 | 184 | 可以关闭内核所有输出。从 lab2 开始要求关闭内核所有输出(如果实现了 log 等级控制,那么这一点自然就实现了)。 185 | 186 | - 利用彩色输出宏输出 os 内存空间布局 187 | 188 | 输出 ``.text``、``.data``、``.rodata``、``.bss`` 各段位置,输出等级为 ``INFO``。 189 | 190 | challenge: 支持多核,实现多个核的 boot。 191 | 192 | 实验检查 193 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 194 | 195 | - 实验目录要求(Rust) 196 | 197 | .. code-block:: 198 | 199 | ├── os(内核实现) 200 | │   ├── Cargo.toml(配置文件) 201 | │   ├── Makefile (要求 make run LOG=xxx 可以正确执行,可以不实现对 LOG 这一属性的支持,设置默认输出等级为 INFO) 202 | │   └── src(所有内核的源代码放在 os/src 目录下) 203 | │   ├── main.rs(内核主函数) 204 | │   └── ... 205 | ├── reports 206 | │   ├── lab1.md/pdf 207 | │   └── ... 208 | ├── README.md(其他必要的说明) 209 | ├── ... 210 | 211 | 报告命名 labx.md/pdf,统一放在 reports 目录下。每个实验新增一个报告,为了方便修改,检查报告是以最新分支的所有报告为准。 212 | 213 | - 检查 214 | 215 | .. code-block:: console 216 | 217 | $ cd os 218 | $ git checkout ch1 219 | $ make run LOG=INFO 220 | 221 | 可以正确执行(可以不支持LOG参数,只有要彩色输出就好),可以看到正确的内存布局输出,根据实现不同数值可能有差异,但应该位于 ``linker.ld`` 中指示 ``BASE_ADDRESS`` 后一段内存,输出之后关机。 222 | 223 | tips 224 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 225 | 226 | - 对于 Rust, 可以使用 crate `log `_ ,推荐参考 `rCore `_ 227 | - 对于 C,可以实现不同的函数(注意不推荐多层可变参数解析,有时会出现不稳定情况),也可以参考 `linux printk `_ 使用宏实现代码重用。 228 | - 两种语言都可以使用 ``extern`` 关键字获得在其他文件中定义的符号。 229 | 230 | 问答作业 231 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 232 | 233 | 1. 请学习 gdb 调试工具的使用(这对后续调试很重要),并通过 gdb 简单跟踪从机器加电到跳转到 0x80200000 的简单过程。只需要描述重要的跳转即可,只需要描述在 qemu 上的情况。 234 | 235 | 2. tips: 236 | 237 | - 事实上进入 rustsbi 之后就不需要使用 gdb 调试了。可以直接阅读代码。`rustsbi起始代码 `_ 。 238 | - 可以使用示例代码 Makefile 中的 ``make debug`` 指令。 239 | - 一些可能用到的 gdb 指令: 240 | - ``x/10i 0x80000000`` : 显示 0x80000000 处的10条汇编指令。 241 | - ``x/10i $pc`` : 显示即将执行的10条汇编指令。 242 | - ``x/10xw 0x80000000`` : 显示 0x80000000 处的10条数据,格式为16进制32bit。 243 | - ``info register``: 显示当前所有寄存器信息。 244 | - ``info r t0``: 显示 t0 寄存器的值。 245 | - ``break funcname``: 在目标函数第一条指令处设置断点。 246 | - ``break *0x80200000``: 在 0x80200000 处设置断点。 247 | - ``continue``: 执行直到碰到断点。 248 | - ``si``: 单步执行一条汇编指令。 249 | 250 | 实验练习的提交报告要求 251 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 252 | 253 | - 简单总结本次实验你编程的内容。(控制在5行以内,不要贴代码) 254 | - 由于彩色输出不好自动测试,请附正确运行后的截图。 255 | - 完成问答问题。 256 | - (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。 257 | 258 | .. [#dwarf] 对编译器如何向调试器提供生成的代码的信息,有兴趣可以参阅 `DWARF 规范 `_ 259 | -------------------------------------------------------------------------------- /source/final-lab.rst: -------------------------------------------------------------------------------- 1 | 综合练习 2 | ================================================ 3 | 4 | .. _term-final-lab: 5 | 6 | - 本节难度:**对OS的全局理解要求较高**。 7 | - 实验分为基础作业实验和扩展作业实验(二选一)。 8 | 9 | 基础作业 10 | ------------------------------------------------- 11 | 12 | **在保持 syscall 数量和基本含义不变的情况下,通过对 OS 内部的改进,提升 OS 的质量**。 13 | 14 | 同学们通过独立完成前面的实验后,应该对于操作系统的核心关键机制有了较好的了解,并知道如何形成一个有进程 / 地址空间 / 文件核心概念的基本功能 OS。但同学自制的 OS 可能还需进一步完善,才能在功能 / 性能 / 可靠性上进一步进化,以使得测试用例的正常运行。 15 | 16 | 综合实验的目的是希望同学们能够在完成前面实验的基础上,站在全局视角,分析之前的测试用例(没增加新的 syscall 访问,只是更加全面和深入地测试操作系统的质量和能力)的运行情况,分析和理解自己写的 OS 是否能比较好地满足应用需求?如果不满足应用需求,或者应用导致系统缓慢甚至崩溃,那原因出在哪里?应该如何修改?修改后的 OS 是否更加完善还是缺陷更多? 17 | 18 | 实验要求 19 | +++++++++++++++++++++++++++++++++++++++++++++++++++++ 20 | 21 | - 实现分支:final。 22 | - 运行 `final测例 `_ ,观察并分析部分测试用例对 OS 造成的不良影响。 23 | - 结合你学到的操作系统课程知识和你的操作系统具体实践情况,分析你写的 OS 对 测试用例中 的 app 支持不好的原因,比如:为何没运行通过,为何死在某处了,为何系统崩溃,为何系统非常缓慢。分析可能的解决方法。(2~4 个,4 个合理的分析就可得到满分,超过 4 个不额外得分)。 24 | - 更进一步完成编程实现,使其可以通过一些原本 fail 的测例。(1~2 个,超过 2 个不额外得分)。 25 | 26 | 报告要求 27 | +++++++++++++++++++++++++++++++++++++++++++++++++++++ 28 | 29 | - 对于失败测例的现象观察,原因分析,并提出可能的解决思路(2~4个)。 30 | - 编程实现的具体内容,不需要贴出全部代码,重要的是描述清楚自己的思路和做法(1~2个)。 31 | - (optional)你对本次实验的其他看法。 32 | 33 | 其他说明 34 | +++++++++++++++++++++++++++++++++++++++++++++++++++++ 35 | 36 | - 注意:编程实现部分的底线是 OS 不崩溃,如果你解决不了问题,就解决出问题的进程。可以通过简单杀死进程方式保证OS不会死掉。比如不支持某种 corner case,就把触发该 case 的进程杀掉,如果是这样,至少完成两个。会根据报告综合给分。 37 | - 有些测例属于非法程序,比如申请过量内存,对于这些程序,杀死进程其实就是正确的做法。参考: `OOM killer `_ 。 38 | - 不一定所有的测例都会导致自己实现的 OS 崩溃,与语言和实现都有关系,选择出问题的测例分析即可。对于没有出错的测例,可以选择性分析自己的 OS 是如何预防这些"刁钻"测例的。对于测例没有测到的,也可以分析自己觉得安全 / 高效的实现,只要分析合理及给分。 39 | - 鼓励针对有趣的测例进行分析!开放思考! 40 | 41 | .. note:: 42 | 43 | 1. **本次实验的分值与之前 lab 相同,截至是时间为 15 周周末,基础实验属于必做实验(除非你选择做扩展作业来代替基础作业)**。 44 | 45 | 2. 在测例中有简明描述:想测试OS哪方面的质量。同学请量力而行,推荐不要超过上述上限。咱们不要卷。 46 | 47 | 3. 对于有特殊要求的同学(比如你觉得上面的实验太难),可单独找助教或老师说出你感兴趣或力所能及的实验内容,得到老师和助教同意后,做你提出的实验。 48 | 49 | 4. **欢迎同学们贡献新测例,有意义测例经过助教检查可以写进报告充当工作量,欢迎打掉框架代码OS,也欢迎打掉其他同学的OS**。 50 | 51 | 实验检查 52 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 53 | 54 | - 实验目录要求 55 | 56 | 目录要求不变(参考 lab1 目录或者示例代码目录结构)。同样在 os 目录下 `make run` 之后可以正确加载用户程序并执行。 57 | 58 | 加载的用户测例位置: `../user/build/elf`。 59 | 60 | - 检查 61 | 62 | 可以正确 `make run` 执行,可以正确执行目标用户测例,并得到预期输出(详见测例注释)。 63 | 64 | 65 | 问答作业 66 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 67 | 68 | 无 69 | 70 | .. _term-chapter8-extended-exercise: 71 | 72 | 拓展作业(可选) 73 | ------------------------------------------------- 74 | 75 | 给部分同学不同的OS设计与实现的实验选择。扩展作业选项(1-14)基于 之前的OS来实现,扩展作业选项(15)是发现目标内核(ucore / rcore os)漏洞。可选内容(有大致难度估计)如下: 76 | 77 | 1. 实现多核支持,设计多核相关测试用例,并通过已有和新的测试用例(难度:8) 78 | 79 | * 某学长的有bug的rcore tutorial参考实现 `https://github.com/xy-plus/rCore-Tutorial-v3/tree/ch7 `_ 80 | 81 | 2. 实现slab内存分配算法,通过相关测试用例(难度:7) 82 | 83 | * `https://github.com/tokio-rs/slab `_ 84 | 85 | 3. 实现新的调度算法,如 CFS、BFS 等,通过相关测试用例(难度:7) 86 | 87 | * `https://en.wikipedia.org/wiki/Completely_Fair_Scheduler `_ 88 | * `https://www.kernel.org/doc/html/latest/scheduler/sched-design-CFS.html `_ 89 | 90 | 4. 实现某种 IO buffer 缓存替换算法,如2Q, LRU-K,LIRS等,通过相关测试用例(难度:6) 91 | 92 | * `LIRS: http://web.cse.ohio-state.edu/~zhang.574/lirs-sigmetrics-02.html `_ 93 | * `2Q: https://nyuscholars.nyu.edu/en/publications/2q-a-low-overhead-high-performance-buffer-replacement-algorithm `_ 94 | * `LRU-K: https://dl.acm.org/doi/10.1145/170036.170081 `_ 95 | 96 | 5. 实现某种页替换算法,如Clock, 二次机会算法等,通过相关测试用例(难度:6) 97 | 98 | 6. 实现支持日志机制的可靠文件系统,可参考OSTEP教材中对日志文件系统的描述(难度:7) 99 | 100 | 7. 支持virtio disk的中断机制,提高IO性能(难度:4) 101 | 102 | * `chapter8 https://github.com/rcore-os/rCore-Tutorial-Book-v3/tree/chy `_ 103 | * `https://github.com/rcore-os/virtio-drivers `_ 104 | * `https://github.com/belowthetree/TisuOS `_ 105 | 106 | 8. 支持 virtio framebuffer /键盘/鼠标处理,给出demo(推荐类似 pong 的 graphic game)的测试用例(难度:6) 107 | 108 | * code: `https://github.com/sgmarz/osblog/tree/pong `_ 109 | * code: `https://github.com/belowthetree/TisuOS `_ 110 | * `tutorial doc: Talking with our new Operating System by Handling Input Events and Devices `_ 111 | * `tutorial doc: Getting Graphical Output from our Custom RISC-V Operating System in Rust `_ 112 | * `tutorial doc: Writing Pong Game in Rust for my OS Written in Rust `_ 113 | 114 | 9. 支持virtio NIC,给出测试用例(难度:7) 115 | 116 | * `https://github.com/rcore-os/virtio-drivers `_ 117 | 118 | 10. 支持 virtio fs or其他virtio虚拟外设,通过测试用例(难度:5) 119 | 120 | * `https://docs.oasis-open.org/virtio/virtio/v1.1/csprd01/virtio-v1.1-csprd01.html `_ 121 | 122 | 11. 支持 `testsuits for kernel `_ 中15个以上的syscall,通过相关测试用例(难度:6) 123 | 124 | * 大部分与我们实验涉及的 syscall 类似 125 | * `https://gitee.com/oscomp/testsuits-for-oskernel#testsuits-for-os-kernel `_ 126 | 127 | 12. 支持新文件系统,比如 fat32 或 ext2 等,通过相关测试用例(难度:7) 128 | 129 | * `https://github.com/rafalh/rust-fatfs `_ 130 | * `https://github.com/pi-pi3/ext2-rs `_ 131 | 132 | 13. 支持物理硬件(如全志哪吒开发板,K210开发板等)。(难度:7) 133 | 134 | * 可找老师要物理硬件开发板和相关开发资料 135 | 136 | 14. 支持其他处理器(如鲲鹏 ARM64、x64 架构等)。(难度:7) 137 | 138 | * 可基于 QEMU 来开发 139 | * 可找老师要基于其他处理器的物理硬件开发板(如树莓派等)和相关开发资料 140 | 141 | 142 | 15. 对fork/exec/spawn等进行扩展,并改进shell程序,实现“|”这种经典的管道机制。(难度:4) 143 | 144 | * 参考 rcore tutorial 文档中 chapter7 中内容 145 | 146 | 16. 向实验用操作系统发起 fuzzing 攻击(难度:6) 147 | 148 | * 其实助教或老师写出的OS kernel也是漏洞百出,不堪一击。我们缺少的仅仅是一个可以方便发现bug的工具。也许同学们能写出或改造出一个os kernel fuzzing工具来发现并crash它/它们。下面的仅仅是参考,应该还不能直接用,也许能给你一些启发。 149 | * `gustave fuzzer for os kernel tutorial `_ 150 | * `gustave fuzzer project `_ 151 | * `paper: GUSTAVE: Fuzzing OS kernels like simple applications `_ 152 | 153 | 17. **学生自己的想法,但需要告知老师或助教,并得到同意。** 154 | 155 | .. note:: 156 | 157 | 1. 支持 1~3 人组队,如果确定并组队完成,请在截止期前通过电子邮件告知助教。成员的具体得分可能会通过与老师和助教的当面交流综合判断给出。尽量减少划水与抱大腿。 158 | 159 | 2. 根据老师和助教的评价,可获得额外得分,但不会超过实验 的满分(30分)。也就是如果前面实验有失分,可以通过一个简单扩展把这部分分数拿回来。 160 | 161 | 其他说明 162 | +++++++++++++++++++++++++++++++++++++++++++++++++++++ 163 | 164 | - 不能抄袭其他上课同学的作业,查出后,**所有实验成绩清零**。 165 | - final 扩展作业可代替 final 基础作业。拓展实验给分要求会远低于大实验,简单的拓展也可以的得到较高的评价。在完成代码的同时,也要求写出有关设计思路,问题及解决方法,实验分析等内容的实验报告。 166 | - 完成之前的编程作业也可得满分。这个扩展作业不是必须要做的,是给有兴趣但不想选择大实验的同学一个选择。 167 | 168 | 实验检查 169 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 170 | 171 | 完成后当面交流。 172 | 173 | 问答作业 174 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 175 | 176 | 无 -------------------------------------------------------------------------------- /source/chapter4/2address-space.rst: -------------------------------------------------------------------------------- 1 | 地址空间 2 | ===================================== 3 | 4 | 5 | 本节导读 6 | -------------------------- 7 | 8 | 9 | 直到现在,我们的操作系统给应用看到的是一个非常原始的物理内存空间,可以简单地理解为一个可以随便访问的大数组。为了限制应用访问内存空间的范围并给操作系统提供内存管理的灵活性,计算机硬件引入了各种内存保护/映射/地址转换硬件机制,如 RISC-V 的基址-边界翻译和保护机制、x86 的分段机制、RISC-V/x86/ARM 都有的分页机制。如果在地址转换过程中,无法找到物理地址或访问权限有误,则处理器产生非法访问内存的异常错误。 10 | 11 | 为了发挥上述硬件机制的能力,操作系统也需要升级自己的能力,更好地管理物理内存和虚拟内存,并给应用程序提供统一的虚拟内存访问接口。计算机科学家观察到这些不同硬件中的共同之处,即 CPU 访问数据和指令的内存地址是虚地址,通过硬件机制(比如 MMU +页表查询)进行地址转换,找到对应的物理地址。为此,计算机科学家提出了 **地址空间(Address Space)** 抽象,并在内核中建立虚实地址空间的映射机制,给应用程序提供一个基于地址空间的安全虚拟内存环境,让应用程序简单灵活地使用内存。 12 | 13 | 本节将结合操作系统的发展历程回顾来介绍地址空间抽象的实现策略是如何变化的。 14 | 15 | 虚拟地址与地址空间 16 | ------------------------------- 17 | 18 | 地址虚拟化出现之前 19 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 20 | 21 | 我们之前介绍过,在远古计算机时代,整硬件资源只用来执行单个裸机应用的时候,并不存在真正意义上的操作系统,而只能算是一种应用函数库。那个时候,物理内存的一部分用来保存函数库的代码和数据,余下的部分都交给应用来使用。从功能上可以将应用占据的内存分成几个段:代码段、全局数据段、堆和栈等。当然,由于就只有这一个应用,它想如何调整布局都是它自己的事情。从内存使用的角度来看,批处理系统和裸机应用很相似:批处理系统的每个应用也都是独占内核之外的全部内存空间,只不过当一个应用出错或退出之后,它所占据的内存区域会被清空,而应用序列中的下一个应用将自己的代码和数据放置进来。这个时期,内核提供给应用的访存视角是一致的,因为它们确实会在运行过程中始终独占一块固定的内存区域,每个应用开发者都基于这一认知来规划程序的内存布局。 22 | 23 | 后来,为了降低等待 I/O 带来的无意义的 CPU 资源损耗,多道程序出现了。而为了提升用户的交互式体验,提高生产力,分时多任务系统诞生了。它们的特点在于:应用开始多出了一种“暂停”状态,这可能来源于它主动 yield 交出 CPU 资源,或是在执行了足够长时间之后被内核强制性放弃处理器。当应用处于暂停状态的时候,它驻留在内存中的代码、数据该何去何从呢?曾经有一种省内存的做法是每个应用仍然和在批处理系统中一样独占内核之外的整块内存,当暂停的时候,内核负责将它的代码、数据保存在外存(如硬盘)中,然后把即将换入的应用在外存上的代码、数据恢复到内存,这些都做完之后才能开始执行新的应用。 24 | 25 | 不过,由于这种做法需要大量读写外部存储设备,而它们的速度都比 CPU 慢上几个数量级,这导致任务切换的开销过大,甚至完全不能接受。既然如此,就只能像我们在第三章中的做法一样,限制每个应用的最大可用内存空间小于物理内存的容量,这样就可以同时把多个应用的数据驻留在内存中。在任务切换的时候只需完成任务上下文保存与恢复即可,这只是在内存的帮助下保存、恢复少量通用寄存器,甚至无需访问外存,这从很大程度上降低了任务切换的开销。 26 | 27 | 在本章的引言中介绍过第三章中操作系统的做法对应用程序开发带了一定的困难。从应用开发的角度看,需要应用程序决定自己会被加载到哪个物理地址运行,需要直接访问真实的物理内存。这就要求应用开发者对于硬件的特性和使用方法有更多了解,产生额外的学习成本,也会为应用的开发和调试带来不便。从内核的角度来看,将直接访问物理内存的权力下放到应用会使得它难以对应用程序的访存行为进行有效管理,已有的特权级机制亦无法阻止很多来自应用程序的恶意行为。 28 | 29 | 加一层抽象加强内存管理 30 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 31 | 32 | 为了解决这种困境,抽象仍然是最重要的指导思想。在这里,抽象意味着内核要负责将物理内存管理起来,并为上面的应用提供一层抽象接口,从之前的失败经验学习,这层抽象需要达成下面的设计目标: 33 | 34 | - *透明* :应用开发者可以不必了解底层真实物理内存的硬件细节,且在非必要时也不必关心内核的实现策略, 35 | 最小化他们的心智负担; 36 | - *高效* :这层抽象至少在大多数情况下不应带来过大的额外开销; 37 | - *安全* :这层抽象应该有效检测并阻止应用读写其他应用或内核的代码、数据等一系列恶意行为。 38 | 39 | .. _term-address-space: 40 | .. _term-virtual-address: 41 | 42 | 最终,到目前为止仍被操作系统内核广泛使用的抽象被称为 **地址空间** (Address Space) 。某种程度上讲,可以将它看成一块巨大但并不一定真实存在的内存。在每个应用程序的视角里,操作系统分配给应用程序一个地址范围受限(容量很大),独占的连续地址空间(其中有些地方被操作系统限制不能访问,如内核本身占用的虚地址空间等),因此应用程序可以在划分给它的地址空间中随意规划内存布局,它的各个段也就可以分别放置在地址空间中它希望的位置(当然是操作系统允许应用访问的地址)。应用同样可以使用一个地址作为索引来读写自己地址空间的数据,就像用物理地址作为索引来读写物理内存上的数据一样。这种地址被称为 **虚拟地址** (Virtual Address) 。当然,操作系统要达到地址空间抽象的设计目标,需要有计算机硬件的支持,这就是计算机组成原理课上讲到的 ``MMU`` 和 ``TLB`` 等硬件机制。 43 | 44 | 从此,应用能够直接看到并访问的内存就只有操作系统提供的地址空间,且它的任何一次访存使用的地址都是虚拟地址,无论取指令来执行还是读写栈、堆或是全局数据段都是如此。事实上,特权级机制被拓展,使得应用不再具有直接访问物理内存的能力。应用所处的执行环境在安全方面被进一步强化,形成了用户态特权级和地址空间的二维安全措施。 45 | 46 | 由于每个应用独占一个地址空间,里面只含有自己的各个段,于是它可以随意规划属于它自己的各个段的分布而无需考虑和其他应用冲突;同时鉴于应用只能通过虚拟地址读写它自己的地址空间,它完全无法窃取或者破坏其他应用的数据,毕竟那些段在其他应用的地址空间内,这是它没有能力去访问的。这是地址空间抽象和具体硬件机制对应用程序执行的安全性和稳定性的一种保障。 47 | 48 | .. image:: address-translation.png 49 | 50 | .. _term-mmu: 51 | .. _term-address-translation: 52 | 53 | 54 | 我们知道应用的数据终归还是存在物理内存中的,那么虚拟地址如何形成地址空间,虚拟地址空间如何转换为物理内存呢?操作系统可以设计巧妙的数据结构来表示地址空间。但如果完全由操作系统来完成转换每次处理器地址访问所需的虚实地址转换,那开销就太大了。这就需要扩展硬件功能来加速地址转换过程(回忆 *计算机组成原理* 课上讲的 ``MMU`` 和 ``TLB`` )。 55 | 56 | 57 | 增加硬件加速虚实地址转换 58 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 59 | 60 | 我们回顾一下 **计算机组成原理** 课,如上图所示,当 CPU 取指令或者执行一条访存指令的时候,它都是基于虚拟地址访问属于当前正在运行的应用的地址空间。此时,CPU 中的 **内存管理单元** (MMU, Memory Management Unit) 自动将这个虚拟地址进行 **地址转换** (Address Translation) 变为一个物理地址,即这个应用的数据/指令的物理内存位置。也就是说,在 MMU 的帮助下,应用对自己虚拟地址空间的读写才能被实际转化为对于物理内存的访问。 61 | 62 | 事实上,每个应用的地址空间都存在一个从虚拟地址到物理地址的映射关系。可以想象对于不同的应用来说,该映射可能是不同的,即 MMU 可能会将来自不同两个应用地址空间的相同虚拟地址转换成不同的物理地址。要做到这一点,就需要硬件提供一些寄存器,软件可以对它进行设置来控制 MMU 按照哪个应用的地址映射关系进行地址转换。于是,将应用的代码/数据放到物理内存并进行管理,建立好应用的地址映射关系,在任务切换时控制 MMU 选用应用的地址映射关系,则是作为软件部分的内核需要完成的重要工作。 63 | 64 | 回过头来,在介绍内核对于 CPU 资源的抽象——时分复用的时候,我们曾经提到它为应用制造了一种每个应用独占整个 CPU 的幻象,而隐藏了多个应用分时共享 CPU 的实质。而地址空间也是如此,应用只需、也只能看到它独占整个地址空间的幻象,而藏在背后的实质仍然是多个应用共享物理内存,它们的数据分别存放在内存的不同位置。 65 | 66 | 地址空间只是一层抽象接口,它有很多种具体的实现策略。对于不同的实现策略来说,操作系统内核如何规划应用数据放在物理内存的位置,而 MMU 又如何进行地址转换也都是不同的。下面我们简要介绍几种曾经被使用的策略,并探讨它们的优劣。 67 | 68 | 分段内存管理 69 | ------------------------------------- 70 | 71 | .. image:: simple-base-bound.png 72 | 73 | .. _term-slot: 74 | 75 | 曾经的一种做法如上图所示:每个应用的地址空间大小限制为一个固定的常数 ``bound`` ,也即每个应用的可用虚拟地址区间均为 :math:`[0,\text{bound})` 。随后,就可以以这个大小为单位,将物理内存除了内核预留空间之外的部分划分为若干个大小相同的 **插槽** (Slot) ,每个应用的所有数据都被内核放置在其中一个插槽中,对应于物理内存上的一段连续物理地址区间,假设其起始物理地址为 :math:`\text{base}` ,则由于二者大小相同,这个区间实际为 :math:`[\text{base},\text{base}+\text{bound})` 。因此地址转换很容易完成,只需检查一下虚拟地址不超过地址空间的大小限制(此时需要借助特权级机制通过异常来进行处理),然后做一个线性映射,将虚拟地址加上 :math:`\text{base}` 就得到了数据实际所在的物理地址。 76 | 77 | .. _term-bitmap: 78 | 79 | 可以看出,这种实现极其简单:MMU 只需要 :math:`\text{base,bound}` 两个寄存器,在地址转换进行比较或加法运算即可;而内核只需要在任务切换时完成切换 :math:`\text{base}` 寄存器。在对一个应用的内存管理方面,只需考虑一组插槽的占用状态,可以用一个 **位图** (Bitmap) 来表示,随着应用的新增和退出对应置位或清空。 80 | 81 | .. _term-internal-fragment: 82 | 83 | 然而,它的问题在于:可能浪费的内存资源过多。注意到应用地址空间预留了一部分,它是用来让栈得以向低地址增长,同时允许堆往高地址增长(支持应用运行时进行动态内存分配)。每个应用的情况都不同,内核只能按照在它能力范围之内的消耗内存最多的应用的情况来统一指定地址空间的大小,而其他内存需求较低的应用根本无法充分利用内核给他们分配的这部分空间。但这部分空间又是一个完整的插槽的一部分,也不能再交给其他应用使用。这种在已分配/使用的地址空间内部无法被充分利用的空间就是 **内碎片** (Internal Fragment) ,它限制了系统同时共存的应用数目。如果应用的需求足够多样化,那么内核无论如何设置应用地址空间的大小限制也不能得到满意的结果。这就是固定参数的弊端:虽然实现简单,但不够灵活。 84 | 85 | 为了解决这个问题,一种分段管理的策略开始被使用,如下图所示: 86 | 87 | .. image:: segmentation.png 88 | 89 | 注意到内核开始以更细的粒度,也就是应用地址空间中的一个逻辑段作为单位来安排应用的数据在物理内存中的布局。对于每个段来说,从它在某个应用地址空间中的虚拟地址到它被实际存放在内存中的物理地址中间都要经过一个不同的线性映射,于是 MMU 需要用一对不同的 :math:`\text{base/bound}` 进行区分。这里由于每个段的大小都是不同的,我们也不再能仅仅使用一个 :math:`\text{bound}` 进行简化。当任务切换的时候,这些对寄存器也需要被切换。 90 | 91 | 简单起见,我们这里忽略一些不必要的细节。比如应用在以虚拟地址为索引访问地址空间的时候,它如何知道该地址属于哪个段,从而硬件可以使用正确的一对 :math:`\text{base/bound}` 寄存器进行合法性检查和完成实际的地址转换。这里只关注分段管理是否解决了内碎片带来的内存浪费问题。注意到每个段都只会在内存中占据一块与它实际所用到的大小相等的空间。堆的情况可能比较特殊,它的大小可能会在运行时增长,但是那需要应用通过系统调用向内核请求。也就是说这是一种按需分配,而不再是内核在开始时就给每个应用分配一大块很可能用不完的内存。由此,不再有内碎片了。 92 | 93 | .. _term-external-fragment: 94 | 95 | 尽管内碎片被消除了,但内存浪费问题并没有完全解决。这是因为每个段的大小都是不同的(它们可能来自不同的应用,功能也不同),内核就需要使用更加通用、也更加复杂的连续内存分配算法来进行内存管理,而不能像之前的插槽那样以一个比特为单位。顾名思义,连续内存分配算法就是每次需要分配一块连续内存来存放一个段的数据。随着一段时间的分配和回收,物理内存还剩下一些相互不连续的较小的可用连续块,其中有一些只是两个已分配内存块之间的很小的间隙,它们自己可能由于空间较小,已经无法被用于分配,这就是 **外碎片** (External Fragment) 。 96 | 97 | 如果这时再想分配一个比较大的块,就需要将这些不连续的外碎片“拼起来”,形成一个大的连续块。然而这是一件开销很大的事情,涉及到极大的内存读写开销。具体而言,这需要移动和调整一些已分配内存块在物理内存上的位置,才能让那些小的外碎片能够合在一起,形成一个大的空闲块。如果连续内存分配算法选取得当,可以尽可能减少这种操作。操作系统课上所讲到的那些算法,包括 first-fit/worst-fit/best-fit 或是 buddy system,其具体表现取决于实际的应用需求,各有优劣。 98 | 99 | 那么,分段内存管理带来的外碎片和连续内存分配算法比较复杂的问题可否被解决呢? 100 | 101 | 分页内存管理 102 | -------------------------------------- 103 | 104 | 仔细分析一下可以发现,段的大小不一是外碎片产生的根本原因。之前我们把应用的整个地址空间连续放置在物理内存中,在每个应用的地址空间大小均相同的情况下,只需利用类似位图的数据结构维护一组插槽的占用状态,从逻辑上分配和回收都是以一个固定的比特为单位,自然也就不会存在外碎片了。但是这样粒度过大,不够灵活,又在地址空间内部产生了内碎片。 105 | 106 | 若要结合二者的优点的话,就需要内核始终以一个同样大小的单位来在物理内存上放置应用地址空间中的数据,这样内核就可以使用简单的插槽式内存管理,使得内存分配算法比较简单且不会产生外碎片;同时,这个单位的大小要足够小,从而其内部没有被用到的内碎片的大小也足够小,尽可能提高内存利用率。这便是我们将要介绍的分页内存管理。 107 | 108 | .. image:: page-table.png 109 | 110 | .. _term-page: 111 | .. _term-frame: 112 | 113 | 如上图所示,内核以页为单位进行物理内存管理。每个应用的地址空间可以被分成若干个(虚拟) **页面** (Page) ,而可用的物理内存也同样可以被分成若干个(物理) **页帧** (Frame) ,虚拟页面和物理页帧的大小相同。每个虚拟页面中的数据实际上都存储在某个物理页帧上。相比分段内存管理,分页内存管理的粒度更小且大小固定,应用地址空间中的每个逻辑段都由多个虚拟页面组成。而且每个虚拟页面在地址转换的过程中都使用与运行的应用绑定的不同的线性映射,而不像分段内存管理那样每个逻辑段都使用一个相同的线性映射。 114 | 115 | .. _term-virtual-page-number: 116 | .. _term-physical-page-number: 117 | .. _term-page-table: 118 | 119 | 为了方便实现虚拟页面到物理页帧的地址转换,我们给每个虚拟页面和物理页帧一个编号,分别称为 **虚拟页号** (VPN, Virtual Page Number) 和 **物理页号** (PPN, Physical Page Number) 。每个应用都有一个表示地址映射关系的 **页表** (Page Table) ,里面记录了该应用地址空间中的每个虚拟页面映射到物理内存中的哪个物理页帧,即数据实际被内核放在哪里。我们可以用页号来代表二者,因此如果将页表看成一个键值对,其键的类型为虚拟页号,值的类型则为物理页号。当 MMU 进行地址转换的时候,虚拟地址会分为两部分(虚拟页号,页内偏移),MMU首先找到虚拟地址所在虚拟页面的页号,然后查当前应用的页表,根据虚拟页号找到物理页号;最后按照虚拟地址的页内偏移,给物理页号对应的物理页帧的起始地址加上一个偏移量,这就得到了实际访问的物理地址。 120 | 121 | 在页表中,还针对虚拟页号设置了一组保护位,它限制了应用对转换得到的物理地址对应的内存的使用方式。最典型的如 ``rwx`` , ``r`` 表示当前应用可以读该内存; ``w`` 表示当前应用可以写该内存; ``x`` 则表示当前应用可以从该内存取指令用来执行。一旦违反了这种限制则会触发异常,并被内核捕获到。通过适当的设置,可以检查一些应用在运行时的明显错误:比如应用修改只读的代码段,或者从数据段取指令来执行。 122 | 123 | 当一个应用的地址空间比较大的时候,页表中的项数会很多(事实上每个虚拟页面都应该对应页表中的一项,上图中我们已经省略掉了那些未被使用的虚拟页面),导致它的容量极速膨胀,已经不再是像之前那样数个寄存器便可存下来的了,CPU 内也没有足够的硬件资源能够将它存下来。因此它只能作为一种被内核管理的数据结构放在内存中,但是 CPU 也会直接访问它来查页表,这也就需要内核和硬件之间关于页表的内存布局达成一致。 124 | 125 | 由于分页内存管理既简单又灵活,它逐渐成为了主流的内存管理机制,RISC-V 架构也使用了这种机制。后面我们会基于这种机制,自己动手从物理内存抽象出应用的地址空间来。 126 | 127 | .. note:: 128 | 129 | 本节部分内容参考自 `Operating Systems: Three Easy Pieces `_ 130 | 教材的 13~16 小节。 131 | 132 | -------------------------------------------------------------------------------- /source/chapter3/2task-switching.rst: -------------------------------------------------------------------------------- 1 | 任务切换 2 | ================================ 3 | 4 | 本节导读 5 | -------------------------- 6 | 7 | 在上一节实现的二叠纪“锯齿螈”操作系统还是比较原始,一个应用会独占 CPU 直到它出错或主动退出。操作系统还是以程序的一次执行过程(从开始到结束)作为处理器切换程序的时间段。为了提高效率,我们需要引入新的操作系统概念 **任务** 、 **任务切换** 、**任务上下文** 。为此,我们需要实现从“螈”到“恐龙”的进化,实现“始初龙”操作系统。 8 | 9 | 如果把应用程序执行的整个过程进行进一步分析,可以看到,当程序访问 I/O 外设或睡眠时,其实是不需要占用处理器的,于是我们可以把应用程序在不同时间段的执行过程分为两类,占用处理器执行有效任务的计算阶段和不必占用处理器的等待阶段。这些阶段就形成了一个我们熟悉的“暂停-继续...”组合的控制流或执行历史。从应用程序开始执行到结束的整个控制流就是应用程序的整个执行过程。 10 | 11 | 本节的重点是操作系统的核心机制—— **任务切换** 。 任务切换支持的场景是:一个应用在运行途中便会主动或被动交出 CPU 的使用权,此时它只能暂停执行,等到内核重新给它分配处理器资源之后才能恢复并继续执行。有了任务切换的能力,“螈”级的操作系统才能跳出水坑,进入陆地,才有能力进化到“恐龙”级的操作系统。 12 | 13 | 任务的概念形成 14 | --------------------------------- 15 | 16 | .. 17 | chyyuu:程序执行过程的图示。 18 | 19 | 如果操作系统能够在某个应用程序处于等待阶段的时候,把处理器转给另外一个处于计算阶段的应用程序,那么只要转换的开销不大,那么处理器的执行效率就会大大提高。当然,这需要应用程序在运行途中能主动交出 CPU 的使用权,此时它处于等待阶段,等到操作系统让它再次执行后,那它就可以继续执行了。 20 | 21 | .. _term-task: 22 | .. _term-task-switch: 23 | 24 | 到这里,我们就把应用程序的一次执行过程(也是一段控制流)称为一个 **任务** ,把应用执行过程中的一个时间片段上的执行片段或空闲片段称为 “ **计算任务片** ” 或“ **空闲任务片** ” 。当应用程序的所有任务片都完成后,应用程序的一次任务也就完成了。从一个程序的任务切换到另外一个程序的任务称为 **任务切换** 。为了确保切换后的任务能够正确继续执行,操作系统需要支持让任务的执行“暂停”和“继续”。 25 | 26 | .. _term-task-context: 27 | 28 | 我们又看到了熟悉的“暂停-继续”组合。一旦一条控制流需要支持“暂停-继续”,就需要提供一种控制流切换的机制,而且需要保证程序执行的控制流被切换出去之前和切换回来之后,能够继续正确执行。这需要让程序执行的状态(也称上下文),即在执行过程中同步变化的资源(如寄存器、栈等)保持不变,或者变化在它的预期之内。不是所有的资源都需要被保存,事实上只有那些对于程序接下来的正确执行仍然有用,且在它被切换出去的时候有被覆盖风险的那些资源才有被保存的价值。这些需要保存与恢复的资源被称为 **任务上下文 (Task Context)** 。 29 | 30 | 31 | .. hint:: 32 | 33 | **抽象与具体** 34 | 35 | 注意:同学会在具体的操作系统设计实现过程中接触到一些抽象的概念,其实这些概念都是具体代码的结构和代码动态执行过程的文字表述而已。 36 | 37 | 38 | 不同类型的上下文与切换 39 | --------------------------------- 40 | 41 | 在控制流切换过程中,我们需要结合硬件机制和软件实现来保存和恢复任务上下文。任务的一次切换涉及到被换出和即将被换入的两条控制流(分属两个应用的不同任务),通常它们都需要共同遵循某些约定来合作完成这一过程。在前两章,我们已经看到了两种上下文保存/恢复的实例。让我们再来回顾一下它们: 42 | 43 | - 第一章“应用程序与基本执行环境”中,我们介绍了 :ref:`函数调用与栈 ` 。当时提到过,为了支持嵌套函数调用,不仅需要硬件平台提供特殊的跳转指令,还需要保存和恢复 :ref:`函数调用上下文 ` 。注意在上述定义中,函数调用包含在普通控制流(与异常控制流相对)之内,且始终用一个固定的栈来保存执行的历史记录,因此函数调用并不涉及控制流的特权级切换。但是我们依然可以将其看成调用者和被调用者两个执行过程的“切换”,二者的协作体现在它们都遵循调用规范,分别保存一部分通用寄存器,这样的好处是编译器能够有足够的信息来尽可能减少需要保存的寄存器的数目。虽然当时用了很大的篇幅来说明,但其实整个过程都是编译器负责完成的,我们只需设置好栈就行了。 44 | - 第二章“批处理系统”中第一次涉及到了某种异常(Trap)控制流,即两条控制流的特权级切换,需要保存和恢复 :ref:`系统调用(Trap)上下文 ` 。当时,为了让内核能够 *完全掌控* 应用的执行,且不会被应用破坏整个系统,我们必须利用硬件提供的特权级机制,让应用和内核运行在不同的特权级。应用运行在 U 特权级,它所被允许的操作进一步受限,处处被内核监督管理;而内核运行在 S 特权级,有能力处理应用执行过程中提出的请求或遇到的状况。 45 | 46 | 应用程序与操作系统打交道的核心在于硬件提供的 Trap 机制,也就是在 U 特权级运行的应用控制流和在 S 特权级运行的 Trap 控制流(操作系统的陷入处理部分)之间的切换。Trap 控制流是在 Trap 触发的一瞬间生成的,它和原应用控制流有着很密切的联系,因为它几乎唯一的目标就是处理 Trap 并恢复到原应用控制流。而且,由于 Trap 机制对于应用来说几乎是透明的,所以基本上都是 Trap 控制流在“负重前行”。Trap 控制流需要把 Trap 上下文(即几乎所有的通用寄存器)保存在自己的内核栈上,因为在 Trap 处理过程中所有的通用寄存器都可能被用到。可以回看 :ref:`Trap 上下文保存与恢复 ` 小节。 47 | 48 | 49 | .. _term-task-switch-impl: 50 | 51 | 任务切换的设计与实现 52 | --------------------------------- 53 | 54 | 本节所讲的任务切换是第二章提及的 Trap 控制流切换之外的另一种异常控制流,都是描述两条控制流之间的切换,如果将它和 Trap 切换进行比较,会有如下异同: 55 | 56 | - 与 Trap 切换不同,它不涉及特权级切换; 57 | - 与 Trap 切换不同,它的一部分是由编译器帮忙完成的; 58 | - 与 Trap 切换相同,它对应用是透明的。 59 | 60 | 事实上,任务切换是来自两个不同应用在内核中的 Trap 控制流之间的切换。当一个应用 Trap 到 S 模式的操作系统内核中进行进一步处理(即进入了操作系统的 Trap 控制流)的时候,其 Trap 控制流可以调用一个特殊的 ``__switch`` 函数。这个函数表面上就是一个普通的函数调用:在 ``__switch`` 返回之后,将继续从调用该函数的位置继续向下执行。但是其间却隐藏着复杂的控制流切换过程。具体来说,调用 ``__switch`` 之后直到它返回前的这段时间,原 Trap 控制流 *A* 会先被暂停并被切换出去, CPU 转而运行另一个应用在内核中的 Trap 控制流 *B* 。然后在某个合适的时机,原 Trap 控制流 *A* 才会从某一条 Trap 控制流 *C* (很有可能不是它之前切换到的 *B* )切换回来继续执行并最终返回。不过,从实现的角度讲, ``__switch`` 函数和一个普通的函数之间的核心差别仅仅是它会 **换栈** 。 61 | 62 | .. image:: task-context.png 63 | 64 | 当 Trap 控制流准备调用 ``__switch`` 函数使任务从运行状态进入暂停状态的时候,让我们考察一下它内核栈上的情况。如上图左侧所示,在准备调用 ``__switch`` 函数之前,内核栈上从栈底到栈顶分别是保存了应用执行状态的 Trap 上下文以及内核在对 Trap 处理的过程中留下的调用栈信息。由于之后还要恢复回来执行,我们必须保存 CPU 当前的某些寄存器,我们称它们为 **任务上下文** (Task Context)。我们会在稍后介绍里面需要包含哪些寄存器。至于上下文保存的位置,下一节在我们会介绍任务管理器 ``TaskManager`` ,在里面能找到一个数组 ``tasks`` ,其中的每一项都是一个任务控制块即 ``TaskControlBlock`` ,它负责保存一个任务的状态,而任务上下文 ``TaskContext`` 被保存在任务控制块中。在内核运行时我们会初始化 ``TaskManager`` 的全局实例 ``TASK_MANAGER`` ,因此所有任务上下文实际保存在在 ``TASK_MANAGER`` 中,从内存布局来看则是放在内核的全局数据 ``.data`` 段中。当我们将任务上下文保存完毕之后则转化为下图右侧的状态。当要从其他任务切换回来继续执行这个任务的时候,CPU 会读取同样的位置并从中恢复任务上下文。 65 | 66 | .. 至于保存的位置,我们将任务 ``i`` 的任务上下文直接放在 ``TaskManager`` --> ``TaskManagerInner`` --> ``tasks[i]`` --> ``task_cx`` 中 ,从这一点上来说它和函数调用不同,它并没有放到栈中。注:这只是放置任务上下文的一种实现方式,我们也可以采用把任务上下文放到内核栈中的另一种实现方式。 67 | 68 | 对于当前正在执行的任务的 Trap 控制流,我们用一个名为 ``current_task_cx_ptr`` 的变量来保存放置当前任务上下文的地址;而用 ``next_task_cx_ptr`` 的变量来保存放置下一个要执行任务的上下文的地址。利用 C 语言的引用来描述的话就是: 69 | 70 | .. code-block:: c 71 | 72 | TaskContext *current_task_cx_ptr = &tasks[current].task_cx; 73 | TaskContext *next_task_cx_ptr = &tasks[next].task_cx; 74 | 75 | .. 76 | 由于我们要用 ``task_cx_ptr`` 这个变量来进行保存任务上下文的地址,自然也要对任务上下文的地址进行读写操作。于是我们还需要指向 ``task_cx_ptr`` 这个变量的指针 ``task_cx_ptr2`` : 77 | 78 | .. code-block:: C 79 | 80 | TaskContext **task_cx_ptr2 = &task_cx_ptr; 81 | 82 | 接下来我们同样从栈上内容的角度来看 ``__switch`` 的整体流程: 83 | 84 | .. image:: switch.png 85 | 86 | Trap 控制流在调用 ``__switch`` 之前就需要明确知道即将切换到哪一条目前正处于暂停状态的 Trap 控制流,因此 ``__switch`` 有两个参数,第一个参数代表它自己,第二个参数则代表即将切换到的那条 Trap 控制流。这里我们用上面提到过的 ``current_task_cx_ptr`` 和 ``next_task_cx_ptr`` 作为代表。在上图中我们假设某次 ``__switch`` 调用要从 Trap 控制流 A 切换到 B,一共可以分为四个阶段,在每个阶段中我们都给出了 A 和 B 内核栈上的内容。 87 | 88 | - 阶段 [1]:在 Trap 控制流 A 调用 ``__switch`` 之前,A 的内核栈上只有 Trap 上下文和 Trap 处理函数的调用栈信息,而 B 是之前被切换出去的; 89 | - 阶段 [2]:A 在 A 任务上下文空间在里面保存 CPU 当前的寄存器快照; 90 | - 阶段 [3]:这一步极为关键,读取 ``next_task_cx_ptr`` 指向的 B 任务上下文,根据 B 任务上下文保存的内容来恢复 ``ra`` 寄存器、``s0~s11`` 寄存器以及 ``sp`` 寄存器。只有这一步做完后, ``__switch`` 才能做到一个函数跨两条控制流执行,即 *通过换栈也就实现了控制流的切换* 。 91 | - 阶段 [4]:上一步寄存器恢复完成后,可以看到通过恢复 ``sp`` 寄存器换到了任务 B 的内核栈上,进而实现了控制流的切换。这就是为什么 ``__switch`` 能做到一个函数跨两条控制流执行。此后,当 CPU 执行 ``ret`` 汇编伪指令完成 ``__switch`` 函数返回后,任务 B 可以从调用 ``__switch`` 的位置继续向下执行。 92 | 93 | 从结果来看,我们看到 A 控制流 和 B 控制流的状态发生了互换, A 在保存任务上下文之后进入暂停状态,而 B 则恢复了上下文并在 CPU 上继续执行。 94 | 95 | 下面我们给出 ``__switch`` 的实现: 96 | 97 | .. code-block:: riscv 98 | :linenos: 99 | 100 | # os/src/task/switch.S 101 | 102 | .altmacro 103 | .macro SAVE_SN n 104 | sd s\n, (\n+2)*8(a0) 105 | .endm 106 | .macro LOAD_SN n 107 | ld s\n, (\n+2)*8(a1) 108 | .endm 109 | .section .text 110 | .globl __switch 111 | __switch: 112 | # 阶段 [1] 113 | # __switch( 114 | # current_task_cx_ptr: *mut TaskContext, 115 | # next_task_cx_ptr: *const TaskContext 116 | # ) 117 | # 阶段 [2] 118 | # save kernel stack of current task 119 | sd sp, 8(a0) 120 | # save ra & s0~s11 of current execution 121 | sd ra, 0(a0) 122 | .set n, 0 123 | .rept 12 124 | SAVE_SN %n 125 | .set n, n + 1 126 | .endr 127 | # 阶段 [3] 128 | # restore ra & s0~s11 of next execution 129 | ld ra, 0(a1) 130 | .set n, 0 131 | .rept 12 132 | LOAD_SN %n 133 | .set n, n + 1 134 | .endr 135 | # restore kernel stack of next task 136 | ld sp, 8(a1) 137 | # 阶段 [4] 138 | ret 139 | 140 | 我们手写汇编代码来实现 ``__switch`` 。在阶段 [1] 可以看到它的函数原型中的两个参数分别是当前 A 任务上下文指针 ``current_task_cx_ptr`` 和即将被切换到的 B 任务上下文指针 ``next_task_cx_ptr`` ,从 :ref:`RISC-V 调用规范 ` 可以知道它们分别通过寄存器 ``a0/a1`` 传入。阶段 [2] 体现在第 19~27 行,即根据 B 任务上下文保存的内容来恢复 ``ra`` 寄存器、``s0~s11`` 寄存器以及 ``sp`` 寄存器。从中我们也能够看出 ``TaskContext`` 里面究竟包含哪些寄存器: 141 | 142 | .. code-block:: rust 143 | :linenos: 144 | 145 | // os/src/task/context.rs 146 | 147 | pub struct TaskContext { 148 | ra: usize, 149 | sp: usize, 150 | s: [usize; 12], 151 | } 152 | 153 | 保存 ``ra`` 很重要,它记录了 ``__switch`` 函数返回之后应该跳转到哪里继续执行,从而在任务切换完成并 ``ret`` 之后能到正确的位置。对于一般的函数而言,Rust/C 编译器会在函数的起始位置自动生成代码来保存 ``s0~s11`` 这些被调用者保存的寄存器。但 ``__switch`` 是一个用汇编代码写的特殊函数,它不会被 Rust/C 编译器处理,所以我们需要在 ``__switch`` 中手动编写保存 ``s0~s11`` 的汇编代码。 不用保存其它寄存器是因为:其它寄存器中,属于调用者保存的寄存器是由编译器在高级语言编写的调用函数中自动生成的代码来完成保存的;还有一些寄存器属于临时寄存器,不需要保存和恢复。 154 | 155 | 我们会将这段汇编代码中的全局符号 ``__switch`` 解释为一个 Rust 函数: 156 | 157 | .. code-block:: rust 158 | :linenos: 159 | 160 | // os/src/task/switch.rs 161 | 162 | global_asm!(include_str!("switch.S")); 163 | 164 | use super::TaskContext; 165 | 166 | extern "C" { 167 | pub fn __switch( 168 | current_task_cx_ptr: *mut TaskContext, 169 | next_task_cx_ptr: *const TaskContext 170 | ); 171 | } 172 | 173 | 我们会调用该函数来完成切换功能而不是直接跳转到符号 ``__switch`` 的地址。因此在调用前后 Rust 编译器会自动帮助我们插入保存/恢复调用者保存寄存器的汇编代码。 174 | 175 | 仔细观察的话可以发现 ``TaskContext`` 很像一个普通函数栈帧中的内容。正如之前所说, ``__switch`` 的实现除了换栈之外几乎就是一个普通函数,也能在这里得到体现。尽管如此,二者的内涵却有着很大的不同。 176 | 177 | 同学可以自行对照注释看看图示中的后面几个阶段各是如何实现的。另外,当内核仅运行单个应用的时候,无论该任务主动/被动交出 CPU 资源最终都会交还给自己,这将导致传给 ``__switch`` 的两个参数相同,也就是某个 Trap 控制流自己切换到自己的情形,请同学对照图示思考目前的实现能否对它进行正确处理。 178 | -------------------------------------------------------------------------------- /source/chapter8/3semaphore.rst: -------------------------------------------------------------------------------- 1 | 信号量机制 2 | ========================================= 3 | 4 | 本节导读 5 | ----------------------------------------- 6 | 7 | .. chyyuu https://en.wikipedia.org/wiki/Semaphore_(programming) 8 | 9 | 在上一节中,我们介绍了互斥锁(mutex 或 lock)的起因、使用和实现过程。通过互斥锁,可以让线程在临界区执行时,独占临界资源。当我们需要更灵活的互斥访问或同步操作方式,如提供了最多只允许N个线程访问临界资源的情况,让某个线程等待另外一个线程执行完毕后再继续执行的同步过程等,互斥锁这种方式就有点力不从心了。 10 | 11 | 在本节中,将介绍功能更加强大和灵活的同步互斥机制 -- Semaphore(信号量),设计它的设计思路,使用和在操作系统中的具体实现。可以看到,信号量的实现需要互斥锁和处理器原子指令的支持,它是一种更高级的同步互斥机制。 12 | 13 | 14 | 信号量的起源和基本思路 15 | ----------------------------------------- 16 | 17 | 18 | 1963年前后,当时的数学家(其实是计算机科学家)Edsger Dijkstra和他的团队正在为Electrologica X8计算机开发一个操作系统(称为 THE multiprogramming system,THE多道程序系统)的过程中,提出了信号(Semphore)是一种变量或抽象数据类型,用于控制多个线程对共同资源的访问。在1965年,Edsger Dijkstra发表了论文手稿 "Cooperating sequential processes" ,详细论述了在多个顺序代码执行流(论文用语:sequential processes)的并发执行过程中,如果没有约束机制,会有不确定的执行结果,为此他提出了信号量的设计思路,能够让这些松耦合的顺序代码执行流能进行同步操作并能对共享资源进行互斥访问。对于由于不当互斥同步操作引入的死锁(论文用语:Deadly Embrace),可通过其设计的银行家算法(The Banker's Algorithm)来解决。 19 | 20 | 注:银行家算法将在下一节讲解。 21 | 22 | Edsger Dijkstra和他的团队提出的信号量是对互斥锁的一种巧妙的扩展。上一节中的互斥锁的初始值一般设置为 1 的整型变量, 表示临界区还没有被某个线程占用。互斥锁用 0 表示临界区已经被占用了,用 1 表示临界区为空。再通过 lock/unlock 操作来协调多个线程轮流独占临界区执行。而信号量的初始值可设置为 N 的整数变量, 如果 N 大于 0, 表示最多可以有N个线程进入临界区执行,如果 N 小于等于 0 , 表示不能有线程进入临界区了,必须在后续操作中让信号量的值加 1 ,才能唤醒某个等待的线程。 23 | 24 | 25 | Dijkstra对信号量设立两种操作:P(Proberen(荷兰语),尝试)操作和V(Verhogen(荷兰语),增加)操作。P操作是检查信号量的值是否大于0,若该值大于0,则将其值减1并继续(表示可以进入临界区了);若该值为0,则线程将睡眠。注意,此时P操作还未结束。而且由于信号量本身是一种临界资源(可回想一下上一节的锁,其实也是一种临界资源),所以在P操作中,检查/修改信号量值以及可能发生的睡眠这一系列操作是一个不可分割的原子操作过程。通过原子操作才能保证一旦P操作开始,则在该操作完成或阻塞睡眠之前,其他线程均不允许访问该信号量。 26 | 27 | V操作会对信号量的值加1,然后检查是否有一个或多个线程在该信号量上睡眠等待。如有,则选择其中的一个线程唤醒并允许该线程继续完成它的P操作;如没有,则直接返回。注意,信号量的值加1,并可能唤醒一个线程的一系列操作同样也是不可分割的原子操作过程。不会有某个进程因执行v操作而阻塞。 28 | 29 | 30 | 如果信号量是一个任意的整数,通常被称为计数信号量(Counting Semaphore),或一般信号量(General Semaphore);如果信号量只有0或1的取值,则称为二值信号量(Binary Semaphore)。可以看出,互斥锁只是信号量的一种特例 -- 二值信号量,信号量很好地解决了最多只允许N个线程访问临界资源的情况。 31 | 32 | 33 | 34 | 关于一种信号量实现的伪代码如下所示: 35 | 36 | 37 | .. code-block:: rust 38 | :linenos: 39 | 40 | fn P(S) { 41 | if S >= 1 42 | S = S - 1; 43 | else 44 | ; 45 | } 46 | fn V(S) { 47 | if 48 | ; 49 | else 50 | S = S + 1; 51 | } 52 | 53 | 54 | 在上述实现中,S的取值范围为大于等于0 的整数。 S的初值一般设置为一个大于0的正整数,表示可以进入临界区的线程数。当S取值为1,表示是二值信号量,也就是互斥锁了。使用信号量实现线程互斥访问临界区的伪代码如下: 55 | 56 | 57 | .. code-block:: rust 58 | :linenos: 59 | 60 | let static mut S: semaphore = 1; 61 | 62 | // Thread i 63 | fn foo() { 64 | ... 65 | P(S); 66 | execute Cricital Section; 67 | V(S); 68 | ... 69 | } 70 | 71 | 72 | 73 | 74 | 下面是另外一种信号量实现的伪代码: 75 | 76 | .. code-block:: rust 77 | :linenos: 78 | 79 | fn P(S) { 80 | S = S - 1; 81 | if 0 > S then 82 | ; 83 | } 84 | fn V(S) { 85 | S = S + 1; 86 | if 87 | ; 88 | } 89 | 90 | 91 | 在这种实现中,S的初值一般设置为一个大于0的正整数,表示可以进入临界区的线程数。但S的取值范围可以是小于 0 的整数,表示等待进入临界区的睡眠线程数。 92 | 93 | 信号量的另一种用途是用于实现同步(synchronization)。比如,把信号量的初始值设置为 0 ,当一个线程A对此信号量执行一个P操作,那么该线程立即会被阻塞睡眠。之后有另外一个线程B对此信号量执行一个V操作,就会将线程A唤醒。这样线程B中执行V操作之前的代码序列B-stmts和线程A中执行P操作之后的代码A-stmts序列之间就形成了一种确定的同步执行关系,即线程B的B-stmts会先执行,然后才是线程A的A-stmts开始执行。相关伪代码如下所示: 94 | 95 | 96 | 97 | .. code-block:: rust 98 | :linenos: 99 | 100 | let static mut S: semaphore = 0; 101 | 102 | //Thread A 103 | ... 104 | P(S); 105 | Label_2: 106 | A-stmts after Thread B::Label_1; 107 | ... 108 | 109 | //Thread B 110 | ... 111 | B-stmts before Thread A::Label_2; 112 | Label_1: 113 | V(S); 114 | 115 | ... 116 | 117 | 118 | 实现信号量 119 | ------------------------------------------ 120 | 121 | 122 | 使用semaphore系统调用 123 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 124 | 125 | 我们通过例子来看看如何实际使用信号量。下面是面向应用程序对信号量系统调用的简单使用,可以看到对它的使用与上一节介绍的互斥锁系统调用类似。 126 | 在这个例子中,主线程先创建了初值为0的信号量SEM_SYNC,然后再创建两个线程 First和Second。线程First会先睡眠10ms,而当线程Second执行时,会由于执行信号量的P操作而等待睡眠;当线程First醒来后,会执行V操作, 127 | 从而能够唤醒线程Second。这样线程First和线程Second就形成了一种稳定的同步关系。 128 | 129 | .. code-block:: rust 130 | :linenos: 131 | :emphasize-lines: 5,10,16,22,25,28 132 | 133 | const SEM_SYNC: usize = 0; //信号量ID 134 | unsafe fn first() -> ! { 135 | sleep(10); 136 | println!("First work and wakeup Second"); 137 | semaphore_up(SEM_SYNC); //信号量V操作 138 | exit(0) 139 | } 140 | unsafe fn second() -> ! { 141 | println!("Second want to continue,but need to wait first"); 142 | semaphore_down(SEM_SYNC); //信号量P操作 143 | println!("Second can work now"); 144 | exit(0) 145 | } 146 | pub fn main() -> i32 { 147 | // create semaphores 148 | assert_eq!(semaphore_create(0) as usize, SEM_SYNC); // 信号量初值为0 149 | // create first, second threads 150 | ... 151 | } 152 | 153 | pub fn sys_semaphore_create(res_count: usize) -> isize { 154 | syscall(SYSCALL_SEMAPHORE_CREATE, [res_count, 0, 0]) 155 | } 156 | pub fn sys_semaphore_up(sem_id: usize) -> isize { 157 | syscall(SYSCALL_SEMAPHORE_UP, [sem_id, 0, 0]) 158 | } 159 | pub fn sys_semaphore_down(sem_id: usize) -> isize { 160 | syscall(SYSCALL_SEMAPHORE_DOWN, [sem_id, 0, 0]) 161 | } 162 | 163 | 164 | - 第16行,创建了一个初值为0,ID为 SEM_SYNC 的信号量,对应的是第22行 SYSCALL_SEMAPHORE_CREATE 系统调用; 165 | - 第10行,线程Second执行信号量P操作(对应的是第28行 SYSCALL_SEMAPHORE_DOWN 系统调用),由于信号量初值为0,该线程将阻塞; 166 | - 第5行,线程First执行信号量V操作(对应的是第25行 SYSCALL_SEMAPHORE_UP 系统调用),会唤醒等待该信号量的线程Second。 167 | 168 | 实现semaphore系统调用 169 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 170 | 171 | 172 | 操作系统如何实现信号量系统调用呢?我们首先考虑一下与此相关的核心数据结构,然后考虑与数据结构相关的相关函数/方法的实现。 173 | 174 | 在线程的眼里,信号量是一种每个线程能看到的共享资源,且可以存在多个不同信号量来合理使用不同的资源。所以我们可以把信号量也看成四一种资源,可放在一起让进程来管理,如下面代码第9行所示。这里需要注意的是: ``semaphore_list: Vec>>`` 表示的是信号量资源的列表。而 ``Semaphore`` 是信号量的内核数据结构,由信号量值和等待队列组成。操作系统需要显式地施加某种控制,来确定当一个线程执行P操作和V操作时,如何让线程睡眠或唤醒线程。在这里,P操作是由 ``Semaphore`` 的 ``down`` 方法实现,而V操作是由 ``Semaphore`` 的 ``up`` 方法实现。 175 | 176 | 177 | .. code-block:: rust 178 | :linenos: 179 | :emphasize-lines: 9,16,17 180 | 181 | pub struct ProcessControlBlock { 182 | // immutable 183 | pub pid: PidHandle, 184 | // mutable 185 | inner: UPSafeCell, 186 | } 187 | pub struct ProcessControlBlockInner { 188 | ... 189 | pub semaphore_list: Vec>>, 190 | } 191 | 192 | pub struct Semaphore { 193 | pub inner: UPSafeCell, 194 | } 195 | pub struct SemaphoreInner { 196 | pub count: isize, 197 | pub wait_queue: VecDeque>, 198 | } 199 | impl Semaphore { 200 | pub fn new(res_count: usize) -> Self { 201 | Self { 202 | inner: unsafe { UPSafeCell::new( 203 | SemaphoreInner { 204 | count: res_count as isize, 205 | wait_queue: VecDeque::new(), 206 | } 207 | )}, 208 | } 209 | } 210 | 211 | pub fn up(&self) { 212 | let mut inner = self.inner.exclusive_access(); 213 | inner.count += 1; 214 | if inner.count <= 0 { 215 | if let Some(task) = inner.wait_queue.pop_front() { 216 | add_task(task); 217 | } 218 | } 219 | } 220 | 221 | pub fn down(&self) { 222 | let mut inner = self.inner.exclusive_access(); 223 | inner.count -= 1; 224 | if inner.count < 0 { 225 | inner.wait_queue.push_back(current_task().unwrap()); 226 | drop(inner); 227 | block_current_and_run_next(); 228 | } 229 | } 230 | } 231 | 232 | 233 | 首先是核心数据结构: 234 | 235 | - 第9行,进程控制块中管理的信号量列表。 236 | - 第16-17行,信号量的核心数据成员:信号量值和等待队列。 237 | 238 | 然后是重要的三个成员函数: 239 | 240 | - 第20行,创建信号量,信号量初值为参数 res_count。 241 | - 第31行,实现V操作的up函数,第34行,当信号量值小于等于0时,将从信号量的等待队列中弹出一个线程放入线程就绪队列。 242 | - 第41行,实现P操作的down函数,第44行,当信号量值小于0时,将把当前线程放入信号量的等待队列,设置当前线程为挂起状态并选择新线程执行。 243 | 244 | 245 | Dijkstra, Edsger W. Cooperating sequential processes (EWD-123) (PDF). E.W. Dijkstra Archive. Center for American History, University of Texas at Austin. (transcription) (September 1965) https://www.cs.utexas.edu/users/EWD/transcriptions/EWD01xx/EWD123.html 246 | 247 | Downey, Allen B. (2016) [2005]. "The Little Book of Semaphores" (2nd ed.). Green Tea Press. 248 | 249 | Leppäjärvi, Jouni (May 11, 2008). "A pragmatic, historically oriented survey on the universality of synchronization primitives" (pdf). University of Oulu, Finland. -------------------------------------------------------------------------------- /outline.md: -------------------------------------------------------------------------------- 1 | # 更新记录 2 | ### 2021-01-04更新: 3 | 4 | Chaptter4 添加实现的过程描述:改进内存隔离的好处; 5 | 6 | ### 2020-12-20更新: 7 | 8 | 将文件描述符从 Chapter7 移动到 Chapter6。 9 | 10 | ### 2020-12-02更新: 11 | 12 | 根据讨论更新了 Chapter1-Chapter7 到分割线之前的内容作为 Tutorial 的第一部分,即让系统能够将所有的资源都利用起来。第二部分则讨论如何做的更好。在 12 月 26 日之前尽可能按照大纲完成多个不同版本的 demo。 13 | 14 | [https://shimo.im/sheets/wV3VVxl04EieK3y1/MODOC](https://shimo.im/sheets/wV3VVxl04EieK3y1/MODOC)是目前的系统调用一览表,预计只需要实现 14 个系统调用就能初步满足要求。 15 | 16 | ### 2020-11-30更新: 17 | 18 | 更新了Chapter2。 19 | 20 | 合并了Chapter3/Chapter4为Chapter3,目前覆盖范围为Chapter1-Chapter5。 21 | 22 | ### lab 设计:2020-11-01 23 | 24 | #### 可能的章节与代码风格 25 | 26 | * 新 OS 实验的目的是:“**强化学生对 OS 的整体观念**”。OS的目的是满足应用需求,为此需要一定的硬件支持和自身逐步增强的功能。鼓励学生自己从头写(有参考实现)、强化整体观、step-by-step。 27 | * **整个文档的风格是应用**导向的,每个 step 的任务一定不是凭空而来、而是**应用**的需求。每一章都是为了解决一个应用具体需求而要求OS要完成的功能,这个功能需要一定的硬件支持。 28 | * 每个章节给出完整可运行且带有完整注释(可以通过 rustdoc 工具生成 html 版)的代码。 29 | * 文档中给出重要的代码片段(照顾到纸质版的读者,事实上在网页版给出代码的链接即可)而并不需要完整的代码,但是需要有完整的执行流程叙述,对于边界条件有足够的讨论。在文档中插入的代码不带有注释,而是将解释放到文档的文字部分。 30 | * 类似xv6,每一章的小节描述一项小功能是如何实现的,不同小节之间可能有一定的先后关系,也有可能是并列的。 31 | * 尽可能讲清楚设计背后的思想与优缺点。 32 | * 在讲解OS设计方面,尽量做到与语言无关。在讲解例子的时候,应该有对应的C和rust版本。 33 | * 在某些具体例子中,最好能体现rust比c强 34 | * 2020-10-28:前几章 Chapter1-4 需要等具体实现出来之后再规划章节。 35 | 36 | # 章节大纲 37 | ## Chapter0 Hello world! 之旅(偏概述) 38 | 39 | ### 主要动机: 40 | 41 | 参考 csapp 第一章,站在一个相对宏观的视角解释一个非常简单的 hello world! 程序是在哪些硬件/软件的支持下得以编译/运行起来的。 42 | 43 | helloworld.c 如何被编译器编译成执行程序,且如何被操作系统执行的。 44 | 45 | gcc 46 | 47 | strace 48 | 49 | ## Chapter1 裸机应用(优先级1) 50 | 51 | ### 主要动机 52 | 53 | 支持应用进行计算与结果输出。 54 | 55 | 在裸机上输出 Hello world,就像在其他 OS 上一样。 56 | 57 | app列表: 58 | 59 | * hello_world:输出字符串 60 | * count_sum:累加一维数组的和,并输出结果 61 | 62 | 备注:不需要输入功能 63 | 64 | ### 内核应完成功能 65 | 66 | 内存地址空间: 67 | 68 | 知道自己在内存的哪个位置。理解编译器生成的代码。 69 | 70 | init:基本初始化 71 | 72 | 主要是硬件加电后的硬件初始化,以前是OS做,后面给BIOS, bootloader等完成初步初始化。OS需要知道内存大小,IO分布。 73 | 74 | write函数:输出字符串 75 | 76 | 驱动串口的初始化,能够通过串口输出。 77 | 78 | exit函数:表明程序结束 79 | 80 | 其它(不是主要的): 81 | 82 | 在 qemu/k210 平台上基于 RustSBI 跳转到内核,打印调试信息,支持内核堆内存分配。 83 | 84 | ### 章节分布 85 | 86 | 基本上和第二版/第三版一致。注意需要考虑上面的应用和功能。 87 | 88 | ## Chapter2批处理系统(优先级1) 89 | 90 | ### 主要动机 91 | 92 | 内核不会被应用程序破坏 93 | 94 | ### 用户程序 95 | 96 | 支持应用进行计算与结果输出。在裸机上输出 Hello world,就像在其他 OS 上一样。但应用程序无法破坏内核,但能得到内核的服务。 97 | 98 | app列表: 99 | 100 | * hello_world:输出字符串。 101 | * count_sum:累加一维数组的和,并输出结果。 102 | ### 内核应完成功能 103 | 104 | 设置好内核和用户运行的栈,内核初始化完成后通过 sret 跳转到用户程序进行执行,然后在用户程序系统调用的时候完成特权级切换、上下文保存/恢复及栈的切换 105 | 106 | 按顺序加载运行多个应用程序。当应用程序出错(非法指令基于 RustSBI 不容易完成,比如访问非法的物理地址)之后直接杀死应用程序并切换到下一个。 107 | 108 | ### 新增系统调用 109 | 110 | * sys_write:向串口写 111 | * sys_exit: 表明任务结束。 112 | ### 实现备注 113 | 114 | 将编译之后的用户镜像和内核打包到一起放到内存上 115 | 116 | 分离用户和内核特权级,保护OS,用户需要请求内核提供的服务 117 | 118 | ## 119 | ## Chapter3 分时多任务系统之一非抢占式调度(优先级1) 120 | 121 | ### 主要动机 122 | 123 | 提高整个应用的CPU利用率 124 | 125 | 多任务,因此需要实现任务切换,可采用如下方法: 126 | 127 | * 批处理:在内存中放多个程序,执行完一个再执行下一个。当执行IO操作时,采用的是忙等的方式,效率差。 128 | * 非抢占切换:CPU和I/O设备之间速度不匹配矛盾,程序之间的公平性。当一个程序主动要求暂停或退出时,换另外一个程序执行CPU计算。 129 | 130 | *>> 这时,可能需要引入中断(但中断不是本章主要的内容,如果不引入更好)。* 131 | 132 | ### 用户程序 133 | 134 | 两个程序放置在一个不同的固定的物理地址上(这样不需要页表机制等虚存能力),完成的功能为:一个程序完成一些计算&输出,主动暂停,OS切换到另外一个程序执行,交替运行。 135 | 136 | * count_multiplication:一维数组的乘法,并输出结果 137 | * count_sum:累加一维数组的和,并输出结果 138 | * [wyf 的具体实现]三个输出小程序,详见[here](https://github.com/rcore-os/rCore-Tutorial-v3/tree/ch3-coop/user/src/bin) 139 | ### 内核应完成功能 140 | 141 | 实现通过 sys_yield 交出当前任务的 CPU 所有权,通过 sys_exit 表明任务结束。需要为每个任务分配一个用户栈和内核栈,且需要实现类似 switch 用来任务切换的函数。 142 | 143 | * sys_yield:让出CPU 144 | * sys_exit:退出当前任务并让出 CPU 145 | ### 实现备注 146 | 147 | 重点是实现switch 148 | 149 | 当所有任务运行结束后退出内核 150 | 151 | ## Chapter3 分时多任务系统之二 抢占式调度(优先级1) 152 | 153 | ### 主要动机 154 | 155 | 进一步提高整个应用的CPU利用率/交互性与任务之间的公平性 156 | 157 | 因此需要实现强制任务切换,并引入中断,可采用如下方法: 158 | 159 | * 时钟中断:基于时间片进行调度 160 | * (不在这里引入)串口中断:在发出输出请求后,不是轮询忙等,而是中断方式响应 161 | ### 用户程序 162 | 163 | * [wyf 的具体实现]三个计算质数幂次的小程序,外加一个 sleep 的程序。[here](https://github.com/rcore-os/rCore-Tutorial-v3/tree/ch3/user/src/bin) 164 | ### 内核应完成功能 165 | 166 | 实现时钟/串口中断处理,以及基于中断的基本时间片轮转调度 167 | 168 | ### 新增系统调用 169 | 170 | * sys_get_time:返回当前的 CPU 时钟周期数 171 | ## Chapter4 内存隔离安全性:地址空间(优先级1) 172 | 173 | ### 主要动机 174 | 175 | * 更好地支持应用(包括内核)的动态内存需求。首先:在内核态实现动态内存分配(这是物理内存),这样引入了堆的概念 176 | * 更好地支持在内核中对非法地址的访问的检查。在内核态实现页表机制,这样内核访问异常地址也能及时报警。 177 | * 提高应用间的安全性(通过页机制实现隔离) 178 | * 附带好处:应用程序地址空间可以相同,便于应用程序的开发 179 | ### 用户程序 180 | 181 | 应用程序与上一章基本相同,只不过应用程序的地址空间起始位置应该相同。而且这一章需要将 ELF 链接进内核而不是二进制镜像。 182 | 183 | 特别的,可以设置访问其他应用程序地址空间或是访问内核地址空间的应用程序,内核会将其杀死。 184 | 185 | 在用户库使用 sbrk 申请动态分配空间而不是放在数据段中。 186 | 187 | ### 内核应完成功能 188 | 189 | * 内核动态内存分配器(对于 Rust 而言,对于 C 仍可以考虑静态分配) 190 | * 物理页帧分配器 191 | * 页表机制,特别是用户和内核地址空间的隔离(参考 xv6) 192 | * ELF 解析和加载(在内核初始化的时候完成全部的地址空间创建和加载即可) 193 | ### 新增系统调用 194 | 195 | * sys_sbrk:拓展或缩减当前应用程序的堆空间大小 196 | ### 建议实现过程: 197 | 198 | 1. 在Chapter1的基础上实现基本的物理内存管理机制,即连续内存的动态分配。 199 | 2. 在Chapter1的基础上实现基本的页表机制。 200 | 3. 然后再合并到Chapter3上。 201 | ## Chapter5 进程及重要系统调用(优先级1) 202 | 203 | ### 主要动机 204 | 205 | 应用以进程的方式进行运行,简化了应用开发的负担,OS也更好管理 206 | 207 | 引入重要的进程概念,整合Chapt1~4的内容抽象出进程,实现一系列相关机制及 syscall 208 | 209 | ### 用户程序 210 | 211 | shell程序 user_shell以及一些相应的测试 212 | 213 | ### 内核应完成功能 214 | 215 | 实现完整的子进程机制,初始化第一个用户进程 initproc。 216 | 217 | ### 新增系统调用 218 | 219 | * sys_fork 220 | * sys_wait(轮询版) 221 | * sys_exec 222 | * sys_getpid 223 | * sys_yield更新 224 | * sys_exit 更新 225 | * sys_read:终端需要从串口读取命令 226 | ## Chapter6 文件系统与进程间通信(优先级1) 227 | 228 | ### 主要动机 229 | 230 | 进程之间需要进行一些协作。本章主要是通过管道进行通信。 231 | 232 | 同时,需要引入文件系统,并通过文件描述符来访问对应类型的 Unix 资源。 233 | 234 | ### 用户程序 235 | 236 | 简单的通过 fork 和子进程共享管道的测试; 237 | 238 | 【可选】强化shell程序的功能,支持使用 | 进行管道连接。 239 | 240 | ### 内核应完成功能 241 | 242 | 实现管道。 243 | 244 | 将字符设备(标准输入/输出)和管道封装为通过文件描述符访问的文件。 245 | 246 | ### 新增系统调用 247 | 248 | * sys_pipe:目前对于管道的 read/write 只需实现轮询版本。 249 | * sys_close:作用是关闭管道 250 | ## Chapter7 数据持久化(优先级1) 251 | 252 | ### 主要动机 253 | 254 | 实现数据持久化存储。 255 | 256 | ### 用户程序 257 | 258 | 多种不同大小的文件读写。 259 | 260 | ### 内核应完成功能 261 | 262 | 实现另一种在块设备上持久化存储的文件。 263 | 264 | 文件系统不需要实现目录。 265 | 266 | ### 新增系统调用 267 | 268 | * sys_open:创建或打开一个文件 269 | # ----------------------------分割线------------------------------------------------- 270 | 271 | ## Chapter6 单核同步互斥(优先级1,需要划分为单核/多核两部分) 272 | 273 | ### 主要动机: 274 | 275 | 应用之间需要在操作系统的帮助下有序共享资源(如串口,内存等)。 276 | 277 | 解释内核中已有的同步互斥问题,并实现阻塞机制。 278 | 279 | ### 内核应完成功能: 280 | 281 | 实现死锁检测机制,并基于阻塞机制实现 sys_sleep 和 sys_wait 以及 sys_kill 282 | 283 | ### 新增系统调用: 284 | 285 | sys_sleep 以及 sys_wait/sys_kill 的更新 286 | 287 | ### 章节分布: 288 | 289 | #### 基于原子指令实现自旋锁 290 | 291 | * 讨论并发冲突的来源(单核/多核) 292 | * 关中断/自旋/自旋关中断锁各自什么情况下能起作用,在课上还讲到一种获取锁失败直接 yield 的锁 293 | * 原子指令与内存一致性模型简介 294 | * 具体实现 295 | * 需要说明的是,课上的锁是针对于同一时刻只能有一个进程处于临界区之内。但是 Rust 风格的锁,也就是 Mutex 更加类似于一个管程(尽管 Rust 语言并没有这个概念),它用来保护一个数据结构,保证同一时间只有一个进程对于这个数据结构进行操作,自然保证了一致性。而 xv6 里面的锁只能保护临界区,相对而言对于数据结构一致性的保护就需要更加复杂的讨论。 296 | #### 死锁检测 297 | 298 | #### 阻塞的同步原语:条件变量 299 | 300 | 简单讨论一下其他的同步原语。 301 | 302 | * 课上提到的信号量和互斥量(后者是前者的特例)保护的都是某一个临界区 303 | #### 基于条件变量实现 sys_sleep 304 | 305 | #### 基于条件变量重新实现 sys_wait 306 | 307 | #### 更新 sys_kill 使得支持 kill 掉正在阻塞的进程 308 | 309 | ## ChapterX IPC(优先级1) 310 | 311 | ### 主要动机: 312 | 313 | 应用之间需要交换信息 314 | 315 | ### 内核应完成功能: 316 | 317 | * pipe 318 | * shared mem 319 | ### 新增系统调用: 320 | 321 | ## Chapter8 设备驱动(优先级2) 322 | 323 | ### 主要动机: 324 | 325 | 应用可以把I/O 设备用起来。 326 | 327 | ### 内核应完成功能: 328 | 329 | 实现块设备驱动和串口驱动,理解同步/异步两种驱动实现方式 330 | 331 | #### 背景知识:设备驱动、设备寄存器、轮询、中断 332 | 333 | #### 设备树(可选) 334 | 335 | #### 实现 virtio_disk 块设备的块读写(同步+轮询风格) 336 | 337 | #### 实现 virtio_disk 块设备的块读写(异步+中断风格) 338 | 339 | #### 实现串口设备的异步输入和同步输出 340 | 341 | * 参考 xv6,可以在内核里面维护一个 FIFO,这样即使串口本身没有 FIFO 也可以 342 | ## Chapter9 Unix 资源:文件(优先级1) 343 | 344 | ### 主要动机: 345 | 346 | 应用可以通过单一接口(文件)访问磁盘来保存信息和访问其他外设 347 | 348 | Unix 万物皆文件,将文件作为进程可以访问的内核资源单位 349 | 350 | ### 内核应完成功能: 351 | 352 | 支持三种不同的 Unix 资源:字符设备(串口)、块设备(文件系统)、管道 353 | 354 | ### 新增系统调用: 355 | 356 | sys_open/sys_close 357 | 358 | ### 背景知识:Unix 万物皆文件/进程对于文件的访问方式 359 | 360 | #### file 抽象接口 361 | 362 | * 支持 read/write 两种操作,表示 file 到地址空间中一块缓冲区的读写操作 363 | #### 字符设备路线 364 | 365 | * 直接将串口设备驱动封装一下即可。 366 | #### 文件系统路线 367 | 368 | * 分成多个子章节,等实现出来之后才知道怎么写 369 | #### 管道路线 370 | 371 | * 一个非常经典的读者/写者问题。 372 | ### ChapterX 虚存管理(优先级2) 373 | 374 | ### 主要动机: 375 | 376 | 提高应用执行的效率(侧重内存) 377 | 378 | - 支持物理内存不够的情况 379 | 380 | - copy on write 381 | 382 | ### 内核应完成功能: 383 | 384 | ### 新增系统调用: 385 | 386 | 387 | 388 | ### Chapter10 多核(可选) 389 | 390 | ### 主要动机: 391 | 392 | 提高应用执行的并行执行效率(侧重多处理器) 393 | 394 | ### 内核应完成功能: 395 | 396 | ### 新增系统调用: 397 | 398 | #### 多核启动与 IPI 399 | 400 | #### 多核调度 401 | 402 | ### Chapter11多核下的同步互斥(可选) 403 | 404 | ### 主要动机: 405 | 406 | 提高应用并行执行下的正确性(侧重多处理器) 407 | 408 | ### 内核应完成功能: 409 | 410 | ### 新增系统调用: 411 | 412 | #### 多核启动与 IPI 413 | 414 | #### 多核调度 415 | 416 | ## Appendix A Rust 语言快速入门与练习题 417 | 418 | ## Appendix B 常见构建工具的使用方法 419 | 420 | 比如 Makefile\ld 等。 421 | 422 | ## Appendix C RustSBI 与 Kendryte K210 兼容性设计 423 | 424 | ## 其他附录… 425 | 426 | --------------------------------------------------------------------------------