├── .gitignore ├── README.md ├── SUMMARY.md ├── book.json ├── comment.html ├── lab0.md ├── lab0 ├── lab0_1_goals.md ├── lab0_2_1_about_labs.md ├── lab0_2_2_1_lab_steps.md ├── lab0_2_2_2_vm_experiment.md ├── lab0_2_2_3_1_softwares.md ├── lab0_2_2_3_install.md ├── lab0_2_2_environment.md ├── lab0_2_3_1_1_compile_c_prog.md ├── lab0_2_3_1_2_att_asm.md ├── lab0_2_3_1_3_gcc_inline_asm.md ├── lab0_2_3_1_4_extend_gcc_asm.md ├── lab0_2_3_1_gcc_usage.md ├── lab0_2_3_2_make_makefile.md ├── lab0_2_3_3_gdb.md ├── lab0_2_3_4_further.md ├── lab0_2_3_tools.md ├── lab0_2_4_1_1_linux_runtime.md ├── lab0_2_4_1_2_1_patch_qemu.md ├── lab0_2_4_1_2_2_configure_make_install_qemu.md ├── lab0_2_4_1_2_linux_source_install.md ├── lab0_2_4_1_install_qemu.md ├── lab0_2_4_2_1_qemu_runtime_arguments.md ├── lab0_2_4_2_2_qemu_monitor_debug.md ├── lab0_2_4_2_qemu_usage.md ├── lab0_2_4_3_qemu_debug_ucore.md ├── lab0_2_4_4_1_make_obj.md ├── lab0_2_4_4_2_ucore_make.md ├── lab0_2_4_4_3_remote_debug.md ├── lab0_2_4_4_4_gdb_config_file.md ├── lab0_2_4_4_5_load_debug_target.md ├── lab0_2_4_4_6_set_debug_arch.md ├── lab0_2_4_4_gdb_qemu_debug_ucore.md ├── lab0_2_4_debug_with_emulator.md ├── lab0_2_5_1_intel_80386_modes.md ├── lab0_2_5_2_intel_80386_mem.md ├── lab0_2_5_3_intel_80386_registers.md ├── lab0_2_5_cpu_hardware.md ├── lab0_2_6_1_oop.md ├── lab0_2_6_2_1_linked_list.md ├── lab0_2_6_2_generic_data_structure.md ├── lab0_2_6_ucore_programming.md ├── lab0_2_prepare.md ├── lab0_ref_ucore-resource.md └── lab0_ref_ucore-tools.md ├── lab0_figs ├── image001.png ├── image002.png ├── image003.png ├── image004.png ├── image005.png ├── image006.png ├── image007.png └── image008.png ├── lab1.md ├── lab1 ├── lab1_1_goals.md ├── lab1_2_1_1_ex1.md ├── lab1_2_1_2_ex2.md ├── lab1_2_1_3_ex3.md ├── lab1_2_1_4_ex4.md ├── lab1_2_1_5_ex5.md ├── lab1_2_1_6_ex6.md ├── lab1_2_1_7_ex7.md ├── lab1_2_1_exercise.md ├── lab1_2_2_files.md ├── lab1_2_labs.md ├── lab1_3_1_bios_booting.md ├── lab1_3_2_1_protection_mode.md ├── lab1_3_2_2_address_space.md ├── lab1_3_2_3_dist_accessing.md ├── lab1_3_2_4_elf.md ├── lab1_3_2_bootloader.md ├── lab1_3_3_1_function_stack.md ├── lab1_3_3_2_interrupt_exception.md ├── lab1_3_3_3_lab1_interrupt.md ├── lab1_3_3_booting_os.md ├── lab1_3_booting.md ├── lab1_4_lab_requirement.md ├── lab1_5_appendix.md └── lab1_appendix_a20.md ├── lab1_figs ├── image001.png ├── image002.png ├── image003.png ├── image004.png ├── image005.png ├── image006.png ├── image007.png ├── image008.png ├── image009.png ├── image010.png ├── image011.png └── image012.png ├── lab2.md ├── lab2 ├── lab2_3_1_phymemlab_goal.md ├── lab2_3_2_1_phymemlab_exercise.md ├── lab2_3_2_2_phymemlab_files.md ├── lab2_3_2_phymemlab_contents.md ├── lab2_3_3_1_phymemlab_overview.md ├── lab2_3_3_2_search_phymem_layout.md ├── lab2_3_3_3_phymem_pagelevel.md ├── lab2_3_3_4_phymem_allocation.md ├── lab2_3_3_5_1_segment_and_paging.md ├── lab2_3_3_5_2_key_problems_in_seg_page.md ├── lab2_3_3_5_3_setup_paging_map.md ├── lab2_3_3_5_4_maping_relations.md ├── lab2_3_3_5_paging.md ├── lab2_3_3_6_self_mapping.md ├── lab2_3_3_phymem_manage.md ├── lab2_3_4_phymemlab_require.md ├── lab2_3_5_probe_phymem_methods.md ├── lab2_3_6_implement_probe_phymem.md ├── lab2_3_7_phymemlab_concepts.md └── rename.py ├── lab2_figs ├── image001.png ├── image002.png ├── image003.png ├── image004.png ├── image006.png ├── image007.png └── image008.png ├── lab3.md ├── lab3 ├── lab3_1_goals.md ├── lab3_2_1_exercises.md ├── lab3_2_2_files.md ├── lab3_2_lab2.md ├── lab3_3_1_vmm_principles.md ├── lab3_3_2_labs_steps.md ├── lab3_3_3_data_structures.md ├── lab3_3_vmm.md ├── lab3_4_page_fault_handler.md ├── lab3_5_1_page_swapping.md ├── lab3_5_2_page_swapping_principles.md ├── lab3_5_swapping.md └── lab3_6_labs_requirement.md ├── lab3_figs ├── image001.png └── image002.png ├── lab4.md ├── lab4 ├── lab4_1_goals.md ├── lab4_2_1_exercises.md ├── lab4_2_2_files.md ├── lab4_2_labs.md ├── lab4_3_1_lab_steps.md ├── lab4_3_2_pcb.md ├── lab4_3_3_1_create_kthread_idleproc.md ├── lab4_3_3_2_create_kthread_initproc.md ├── lab4_3_3_3_sched_run_kthread.md ├── lab4_3_3_create_exec_kernel_thread.md ├── lab4_3_kernel_thread_management.md ├── lab4_4_labs_requirement.md ├── lab4_5_appendix_a.md └── lab4_6_appendix_b.md ├── lab5.md ├── lab5 ├── lab5_1_goals.md ├── lab5_2_1_exercises.md ├── lab5_2_2_files.md ├── lab5_2_lab2.md ├── lab5_3_1_lab_steps.md ├── lab5_3_2_create_user_process.md ├── lab5_3_3_process_exit_wait.md ├── lab5_3_4_syscall.md ├── lab5_3_user_process.md ├── lab5_4_lab_requirement.md └── lab5_5_appendix.md ├── lab5_figs └── image001.png ├── lab6.md ├── lab6 ├── hehe ├── lab6_1_goals.md ├── lab6_2_1_exercises.md ├── lab6_2_2_files.md ├── lab6_2_labs.md ├── lab6_3_1_exercises.md ├── lab6_3_3_process_state.md ├── lab6_3_4_1_kernel_preempt_point.md ├── lab6_3_4_2_process_switch.md ├── lab6_3_4_process_implement.md ├── lab6_3_5_1_designed.md ├── lab6_3_5_2_data_structure.md ├── lab6_3_5_3_scheduler_point_functions.md ├── lab6_3_5_4_RR.md ├── lab6_3_5_scheduler_framework.md ├── lab6_3_6_1_basic_method.md ├── lab6_3_6_2_priority_queue.md ├── lab6_3_6_stride_scheduling.md ├── lab6_3_scheduler_design.md └── lab6_4_labs_requirement.md ├── lab6_figs ├── image001.png └── image002.png ├── lab7.md ├── lab7 ├── lab7_1_goals.md ├── lab7_2_1_exercises.md ├── lab7_2_2_files.md ├── lab7_2_labs.md ├── lab7_3_1_experiment.md ├── lab7_3_2_1_timer.md ├── lab7_3_2_2_interrupt.md ├── lab7_3_2_3_waitqueue.md ├── lab7_3_2_synchronization_basic_support.md ├── lab7_3_3_semaphore.md ├── lab7_3_4_monitors.md ├── lab7_3_synchronization_implement.md ├── lab7_4_lab_requirement.md └── lab7_5_appendix.md ├── lab7_figs └── image001.png ├── lab8.md ├── lab8 ├── lab8_1_goals.md ├── lab8_2_1_exercises.md ├── lab8_2_2_files.md ├── lab8_2_labs.md ├── lab8_3_1_ucore_fs_introduction.md ├── lab8_3_2_fs_interface.md ├── lab8_3_3_1_fs_layout.md ├── lab8_3_3_2_inode.md ├── lab8_3_3_sfs.md ├── lab8_3_4_1_file_dir_interface.md ├── lab8_3_4_2_inode_interface.md ├── lab8_3_4_fs_abstract.md ├── lab8_3_5_1_data_structure.md ├── lab8_3_5_2_stdout_dev_file.md ├── lab8_3_5_3_stdin_dev_file.md ├── lab8_3_5_dev_file_io_layer.md ├── lab8_3_6_labs_steps.md ├── lab8_3_7_1_file_open.md ├── lab8_3_7_2_file_read.md ├── lab8_3_7_file_op_implement.md ├── lab8_3_fs_design_implement.md └── lab8_4_lab_requirement.md ├── lab8_figs ├── image001.png ├── image002.png ├── image003.png └── image004.png └── update_book.sh /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | _book 3 | assets 4 | *.bak 5 | *.org 6 | *~ 7 | *.dot 8 | autocover 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uCore OS 实验指导书和源码网址 (2020) 2 | 3 | - [ucore 实验指导书](https://learningos.github.io/ucore_os_webdocs/) 4 | - [ucore labs 1-8 源码和参考答案 ](https://github.com/learningos/ucore_os_lab) 5 | - [os tutorial lab](https://github.com/chyyuu/os_tutorial_lab) 6 | 7 | # 实验总体流程 8 | 9 | 1. 在[学堂在线](https://www.xuetangx.com/courses/TsinghuaX/30240243X/2015_T1/about)查看 OS 相关原理和 labX 的视频; 10 | 2. 在[实验指导书 on github page](https://learningos.github.io/ucore_os_webdocs/)上阅读实验指导书,并参考其内容完成练习和实验报告; 11 | 3. 在实验环境中完成实验并提交实验到 git server(清华学生需要在学校内部的 git server 上,其他同学可提交在其他 git server 上); 12 | 4. 如没有解答,可在[piazza 在线 OS 课程问答和交流区](https://piazza.com/tsinghua.edu.cn/spring2015/30240243x/home)或微信群提问,(QQ 群 181873534 主要用于 OS 课程一般性交流); 13 | 14 | 15 | ## 四种学习目标和对应手段 16 | 17 | 1. 掌握 OS 基本概念:看在线课程,能理解 OS 原理与概念;看在线实验指导书并分析源码,能理解 labcodes_answer 的 labs 运行结果 18 | 2. 掌握 OS 设计实现:在 1 的基础上,能够通过编程完成 labcodes 的 8 个 lab 实验中的基本练习和实验报告 19 | 3. 掌握 OS 核心功能:在 2 的基础上,能够通过编程完成 labcodes 的 8 个 lab 实验中的 challenge 练习 20 | 4. 掌握 OS 科学研究:在 3 的基础上,能够通过阅读论文、设计、编程、实验评价等过程来完成课程设计(大实验) 21 | 22 | 【**注意**】 23 | 24 | - **筑基内功**--请提前学习计算机原理、C 语言、数据结构课程 25 | - **工欲善其事,必先利其器**--请掌握七种武器 [实验常用工具列表](https://github.com/chyyuu/ucore_os_docs/blob/master/lab0/lab0_ref_ucore-tools.md) 26 | - **学至于行之而止矣**--请在实验中体会操作系统的精髓 27 | - **打通任督二脉**--lab1 和 lab2 比较困难,有些同学由于畏难而止步与此,很可惜。通过 lab1 和 lab2 后,对计算机原理中的中断、段页表机制、特权级等的理解会更深入,等会有等同于打通了任督二脉,后面的实验将一片坦途。 28 | 29 | > [实验指导书 on github page](https://learningos.github.io/ucore_os_webdocs/)中会存在一些 bug,欢迎在在[piazza 在线 OS 课程问答和交流区](https://piazza.com/tsinghua.edu.cn/spring2015/30240243x/home)提出问题或修改意见。 30 | 31 | # 维护者 32 | 33 | - yuchen AT tsinghua.edu.cn 34 | - xyong AT tsinghua.edu.cn 35 | - liufengyuan 36 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "introduction": { 3 | "path": "README.md", 4 | "title": "介绍" 5 | }, 6 | "plugins": [ 7 | "-mathjax", 8 | "-lunr", 9 | "-search", 10 | "search-pro", 11 | "chapter-fold", 12 | "code", 13 | "github", 14 | "localized-footer", 15 | "ancre-navigation" 16 | ], 17 | 18 | "pluginsConfig": { 19 | "github": { 20 | "url": "https://github.com/LearningOS/ucore_os_docs" 21 | }, 22 | "localized-footer": { 23 | "filename": "comment.html" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /comment.html: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 19 | 36 | -------------------------------------------------------------------------------- /lab0.md: -------------------------------------------------------------------------------- 1 | # 实验零:操作系统实验准备 2 | -------------------------------------------------------------------------------- /lab0/lab0_1_goals.md: -------------------------------------------------------------------------------- 1 | ## 实验目的: 2 | 3 | - 了解操作系统开发实验环境 4 | - 熟悉命令行方式的编译、调试工程 5 | - 掌握基于硬件模拟器的调试技术 6 | - 熟悉 C 语言编程和指针的概念 7 | - 了解 X86 汇编语言 8 | -------------------------------------------------------------------------------- /lab0/lab0_2_1_about_labs.md: -------------------------------------------------------------------------------- 1 | ### 了解 OS 实验 2 | 3 | 写一个操作系统难吗?别被现在上百万行的 Linux 和 Windows 操作系统吓倒。当年 Thompson 乘他老婆带着小孩度假留他一人在家时,写了 UNIX;当年 Linus 还是一个 21 岁大学生时完成了 Linux 雏形。站在这些巨人的肩膀上,我们能否也尝试一下做“巨人”的滋味呢? 4 | 5 | MIT 的 Frans Kaashoek 等在 2006 年参考 PDP-11 上的 UNIX Version 6 写了一个可在 X86 上跑的操作系统 xv6(基于 MIT License),用于学生学习操作系统。我们可以站在他们的肩膀上,基于 xv6 的设计,尝试着一步一步完成一个从“空空如也”到“五脏俱全”的“麻雀”操作系统—ucore,此“麻雀”包含虚存管理、进程管理、处理器调度、同步互斥、进程间通信、文件系统等主要内核功能,总的内核代码量(C+asm)不会超过 5K 行。充分体现了“小而全”的指导思想。 6 | 7 | ucore 的运行环境可以是真实的 X86 计算机,不过考虑到调试和开发的方便,我们可采用 X86 硬件模拟器,比如 QEMU、BOCHS、VirtualBox、VMware Player 等。ucore 的开发环境主要是 GCC 中的 gcc、gas、ld 和 MAKE 等工具,也可采用集成了这些工具的 IDE 开发环境 Eclipse-CDT 等。在分析源代码上,可以采用 Scitools 提供的 understand 软件(跨平台),windows 环境上的 source insight 软件,或者基于 emacs+ctags,vim+ctags 等,都可以比较方便在在一堆文件中查找变量、函数定义、调用/访问关系等。软件开发的版本管理可以采用 GIT、SVN 等。比较文件和目录的不同可发现不同实验中的差异性和进行文件合并操作,可使用 meld、kdiff3、UltraCompare 等软件。调试(deubg)实验有助于发现设计中的错误,可采用 gdb(配合 qemu)等调试工具软件。并可整个实验的运行环境和开发环境既可以在 Linux 或 Windows 中使用。推荐使用 Linux 环境。 8 | 9 | 那我们准备如何一步一步来实现 ucore 呢?根据一个操作系统的设计实现过程,我们可以有如下的实验步骤: 10 | 11 | 1. 启动操作系统的 bootloader,用于了解操作系统启动前的状态和要做的准备工作,了解运行操作系统的硬件支持,操作系统如何加载到内存中,理解两类中断--“外设中断”,“陷阱中断”等; 12 | 2. 物理内存管理子系统,用于理解 x86 分段/分页模式,了解操作系统如何管理物理内存; 13 | 3. 虚拟内存管理子系统,通过页表机制和换入换出(swap)机制,以及中断-“故障中断”、缺页故障处理等,实现基于页的内存替换算法; 14 | 4. 内核线程子系统,用于了解如何创建相对与用户进程更加简单的内核态线程,如果对内核线程进行动态管理等; 15 | 5. 用户进程管理子系统,用于了解用户态进程创建、执行、切换和结束的动态管理过程,了解在用户态通过系统调用得到内核态的内核服务的过程; 16 | 6. 处理器调度子系统,用于理解操作系统的调度过程和调度算法; 17 | 7. 同步互斥与进程间通信子系统,了解进程间如何进行信息交换和共享,并了解同步互斥的具体实现以及对系统性能的影响,研究死锁产生的原因,以及如何避免死锁; 18 | 8. 文件系统,了解文件系统的具体实现,与进程管理等的关系,了解缓存对操作系统 IO 访问的性能改进,了解虚拟文件系统(VFS)、buffer cache 和 disk driver 之间的关系。 19 | 20 | 其中每个开发步骤都是建立在上一个步骤之上的,就像搭积木,从一个一个小木块,最终搭出来一个小房子。在搭房子的过程中,完成从理解操作系统原理到实践操作系统设计与实现的探索过程。这个房子最终的建筑架构和建设进度如下图所示: 21 | 22 | ![ucore系统结构图](../lab0_figs/image001.png "ucore系统结构图") 23 | 24 | 图 1 ucore 系统结构图 25 | 26 | 如果完成上诉实验后还想做更大的挑战,那么可以参加 ucore 的研发项目,我们可以完成 ucore 的网络协议栈,增加图形系统,在 ARM 嵌入式系统上运行,支持虚拟化功能等。这些项目已经有同学参与,欢迎有兴趣的同学加入! 27 | -------------------------------------------------------------------------------- /lab0/lab0_2_2_1_lab_steps.md: -------------------------------------------------------------------------------- 1 | #### 开发 OS lab 实验的简单步骤 2 | 3 | 在某 git server,比如 https://github.com/chyyuu/ucore_lab 可下载我们提供的 lab1~lab8 实验软件中,大致经过如下过程就可以完成使用。 4 | 5 | 1. 在学堂在线查看 OS 相关原理和 labX 的课程视频 6 | 1. 如果第一次做 lab,需要建立 lab 试验环境,可采用基于 virtualbox 虚拟机的最简单方式完成 7 | 1. 阅读本次 lab 的[实验指导书](http://objectkuan.gitbooks.io/ucore-docs/),了解本次 lab 的试验要求 8 | 1. 下载源码(可以直接在 github 下载,或通过 git pull 下载) 9 | 1. 进入各个 OS 实验工程目录 例如: cd labcodes/lab1 10 | 1. 根据实验要求阅读源码并修改代码(用各种代码分析工具和文本编辑器) 11 | 1. 并编译源码 例如执行:make 12 | 1. 如编译不过则返回步骤 3 13 | 1. 如编译通过则测试是否基本正确,例如执行:make grade 14 | 1. 如果实现基本正确(即看到步骤 6 的输出存在不是 OK 的情况)则返回步骤 3 15 | 1. 如果实现基本正确(即看到步骤 6 的输出都是 OK)则生成实验提交软件包,例如执行:make handin 16 | 1. 对于本校学生,把生成的使用提交软件包和实验报告上传到指定的 git server,便于助教和老师查看。 17 | 18 | > 另外,可以通过”make qemu”让 OS 实验工程在 qemu 上运行;可以通过”make debug”或“make debug-nox “命令实现通过 gdb 远程调试 OS 实验工程;通过"make grade"可以看自己完成的对错情况。 19 | -------------------------------------------------------------------------------- /lab0/lab0_2_2_2_vm_experiment.md: -------------------------------------------------------------------------------- 1 | #### 通过虚拟机使用 Linux 实验环境(推荐:最容易的实验环境安装方法) 2 | 3 | 这是最简单的一种通过虚拟机方式使用 Linux 并完成 OS 各个实验的方法,不需要安装 Linux 操作系统和各种实验所需开发软件。首先安装 VirtualBox 虚拟机软件(有 windows 版本和其他 OS 版本,可到 http://www.virtualbox.org/wiki/Downloads 下载),然后在[百度云盘上](http://pan.baidu.com/s/11zjRK)下载一个已经安装好各种所需编辑/开发/调试/运行软件的 Linux 实验环境的 VirtualBox 虚拟硬盘文件(mooc-os-2015.vdi.xz,包含一个虚拟磁盘镜像文件和两个配置描述文件,下载此文件的网址址见https://github.com/chyyuu/ucore_lab下的README中的描述)。用 2345 好压软件(有 windows 版本,可到http://www.haozip.com 下载。一般软件解压不了 xz 格式的压缩文件)先解压到 C 盘的 vms 目录下即: 4 | C:\vms\mooc-os-2015.vdi 5 | 6 | 解压后这个文件所占用的硬盘空间为 6GB 左右。在 VirtualBox 中创建新虚拟机(设置 64 位 Linux 系统,指定配置刚解压的这个虚拟硬盘 mooc-os-2015.vdi),就可以启动并运行已经配置好相关工具的 Linux 实验环境了。 7 | 8 | 如果提示用户“moocos”输入口令时,只需简单敲一个空格键和回车键即可。然后就进入到开发环境中了。实验内容位于 ucore_lab 目录下。可以通过如下命令获得整个实验的代码和文档: 9 | \$ git clone https://github.com/chyyuu/ucore_lab.git 10 | 11 | 并可通过如下命令获得以后更新后的代码和文档: 12 | \$ git pull 13 | 当然,你需要了解一下 git 的基本使用方法,这可以通过网络获得很多这方面的信息。 14 | -------------------------------------------------------------------------------- /lab0/lab0_2_2_3_1_softwares.md: -------------------------------------------------------------------------------- 1 | ##### 实验中可能使用的软件 2 | 3 | **_编辑器_** 4 | 5 | (1) Ubuntu 下自带的编辑器可以作为代码编辑的工具。例如 gedit 是 gnome 桌面环境下兼容 UTF-8 的文本编辑器。它十分的简单易用,有良好的语法高亮,对中文支持很好。通常可以通过双击或者命令行打开目标文件进行编辑。 6 | 7 | (2) Vim 编辑器:Vim 是一款极方便的文本编辑软件,是 UNIX 下的同类型软件 VI 的改进版本。Vim 经常被看作是“专门为程序员打造的文本编辑器”,功能强大且方便使用,便于进行程序开发。 8 | Ubuntu 下默认安装的 vi 版本较低,功能较弱,建议在系统内安装或者升级到最新版本的 Vim。 9 | 10 | [1]关于 Vim 的常用命令以及使用,可以通过网络进行查找。 11 | 12 | [2]配置文件:Vim 的使用需要配置文件进行设置,例如: 13 | 14 | set nocompatible 15 | set encoding=utf-8 16 | set fileencodings=utf-8,chinese 17 | set tabstop=4 18 | set cindent shiftwidth=4 19 | set backspace=indent,eol,start 20 | autocmd Filetype c set omnifunc=ccomplete#Complete 21 | autocmd Filetype cpp set omnifunc=cppcomplete#Complete 22 | set incsearch 23 | set number 24 | set display=lastline 25 | set ignorecase 26 | syntax on 27 | set nobackup 28 | set ruler 29 | set showcmd 30 | set smartindent 31 | set hlsearch 32 | set cmdheight=1 33 | set laststatus=2 34 | set shortmess=atI 35 | set formatoptions=tcrqn 36 | set autoindent 37 | 38 | 可以将上述配置文件保存到: 39 | 40 | ~/.vimrc 41 | 42 | 注意:.vimrc 默认情况下隐藏不可见,可以在命令行中通过 “ls -a” 命令进行查看。如果 '~' 目录下不存在该文件,可以手动创建。修改该文件以后,重启 Vim 可以使配置生效。 43 | 44 | **_exuberant-ctags_** 45 | exuberant-ctags 可以为程序语言对象生成索引,其结果能够被一个文本编辑器或者其他工具简捷迅速的定位。支持的编辑器有 Vim、Emacs 等。 46 | 实验中,可以使用命令: 47 | 48 | ctags -h=.h.c.S -R 49 | 50 | 默认的生成文件为 tags (可以通过 -f 来指定),在相同路径下使用 Vim 可以使用改索引文件,例如: 51 | 52 | 使用 “ctrl + ]” 可以跳转到相应的声明或者定义处,使用 “ctrl + t” 返回(查询堆栈)等。 53 | 54 | 提示:习惯 GUI 方式的同学,可采用图形界面的 understand、source insight 等软件。 55 | **_diff & patch_** 56 | 57 | diff 为 Linux 命令,用于比较文本或者文件夹差异,可以通过 man 来查询其功能以及参数的使用。使用 patch 命令可以对文件或者文件夹应用修改。 58 | 59 | 例如实验中可能会在 proj_b 中应用前一个实验 proj_a 中对文件进行的修改,可以使用如下命令: 60 | 61 | diff -r -u -P proj_a_original proj_a_mine > diff.patch 62 | cd proj_b 63 | patch -p1 -u < ../diff.patch 64 | 65 | 注意:proj_a_original 指 proj_a 的源文件,即未经修改的源码包,proj_a_mine 是修改后的代码包。第一条命令是递归的比较文件夹差异,并将结果重定向输出到 diff.patch 文件中;第三条命令是将 proj_a 的修改应用到 proj_b 文件夹中的代码中。 66 | 67 | 提示:习惯 GUI 方式的同学,可采用图形界面的 meld、kdiff3、UltraCompare 等软件。 68 | -------------------------------------------------------------------------------- /lab0/lab0_2_2_environment.md: -------------------------------------------------------------------------------- 1 | ### 设置实验环境 2 | 3 | 我们参考了 MIT 的 xv6、Harvard 的 OS161 和 Linux 等设计了 ucore OS 实验,所有 OS 实验需在 Linux 下运行。对于经验不足的同学,推荐参考“通过虚拟机使用 Linux 实验环境”一节用虚拟机方式进行试验。 4 | 5 | > 也有同学在 MAC 系统和 Windows 系统中搭建实验环境,不过过程相对比较复杂,这里就不展开介绍了。 6 | -------------------------------------------------------------------------------- /lab0/lab0_2_3_1_1_compile_c_prog.md: -------------------------------------------------------------------------------- 1 | ###### 编译简单的 C 程序 2 | 3 | C 语言经典的入门例子是 Hello World,下面是一示例代码: 4 | 5 | #include 6 | int 7 | main(void) 8 | { 9 | printf("Hello, world!\n"); 10 | return 0; 11 | } 12 | 13 | 我们假定该代码存为文件‘hello.c’。要用 gcc 编译该文件,使用下面的命令: 14 | 15 | $ gcc -Wall hello.c -o hello 16 | 17 | 该命令将文件‘hello.c’中的代码编译为机器码并存储在可执行文件 ‘hello’中。机器码的文件名是通过 -o 选项指定的。该选项通常作为命令行中的最后一个参数。如果被省略,输出文件默认为 ‘a.out’。 18 | 19 | 注意到如果当前目录中与可执行文件重名的文件已经存在,它将被复盖。 20 | 选项 -Wall 开启编译器几乎所有常用的警告 ──**强烈建议你始终使用该选项**。编译器有很多其他的警告选项,但 -Wall 是最常用的。默认情况下 GCC 不会产生任何警告信息。当编写 C 或 C++ 程序时编译器警告非常有助于检测程序存在的问题。 21 | 22 | 本例中,编译器使用了 -Wall 选项而没产生任何警告,因为示例程序是完全合法的。 23 | 24 | 要运行该程序,输入可执行文件的路径如下: 25 | 26 | $ ./hello 27 | Hello, world! 28 | 29 | 这将可执行文件载入内存,并使 CPU 开始执行其包含的指令。 路径 ./ 指代当前目录,因此 ./hello 载入并执行当前目录下的可执行文件 ‘hello’。 30 | -------------------------------------------------------------------------------- /lab0/lab0_2_3_1_2_att_asm.md: -------------------------------------------------------------------------------- 1 | ##### AT&T 汇编基本语法 2 | 3 | Ucore 中用到的是 AT&T 格式的汇编,与 Intel 格式的汇编有一些不同。二者语法上主要有以下几个不同: 4 | 5 | ``` 6 | * 寄存器命名原则 7 | AT&T: %eax Intel: eax 8 | * 源/目的操作数顺序 9 | AT&T: movl %eax, %ebx Intel: mov ebx, eax 10 | * 常数/立即数的格式  11 | AT&T: movl $_value, %ebx Intel: mov eax, _value 12 | 把value的地址放入eax寄存器 13 | AT&T: movl $0xd00d, %ebx Intel: mov ebx, 0xd00d 14 | * 操作数长度标识 15 | AT&T: movw %ax, %bx Intel: mov bx, ax 16 | * 寻址方式 17 | AT&T: immed32(basepointer, indexpointer, indexscale) 18 | Intel: [basepointer + indexpointer × indexscale + imm32) 19 | ``` 20 | 21 | 如果操作系统工作于保护模式下,用的是 32 位线性地址,所以在计算地址时不用考虑 segment:offset 的问题。上式中的地址应为: 22 | 23 | ``` 24 | imm32 + basepointer + indexpointer × indexscale 25 | ``` 26 | 27 | 下面是一些例子: 28 | 29 | ``` 30 | * 直接寻址 31 | AT&T: foo Intel: [foo] 32 | boo是一个全局变量。注意加上$是表示地址引用,不加是表示值引用。对于局部变量,可以通过堆栈指针引用。 33 | 34 | * 寄存器间接寻址 35 | AT&T: (%eax) Intel: [eax] 36 | 37 | * 变址寻址 38 | AT&T: _variable(%eax) Intel: [eax + _variable] 39 | AT&T: _array( ,%eax, 4) Intel: [eax × 4 + _array] 40 | AT&T: _array(%ebx, %eax,8) Intel: [ebx + eax × 8 + _array] 41 | ``` 42 | -------------------------------------------------------------------------------- /lab0/lab0_2_3_1_3_gcc_inline_asm.md: -------------------------------------------------------------------------------- 1 | ##### GCC 基本内联汇编 2 | 3 | GCC 提供了两内内联汇编语句(inline asm statements):基本内联汇编语句(basic inline asm statement)和扩展内联汇编语句(extended inline asm statement)。GCC 基本内联汇编很简单,一般是按照下面的格式: 4 | asm("statements"); 5 | 6 | 例如: 7 | 8 | asm("nop"); asm("cli"); 9 | 10 | "asm" 和 "\_\_asm\_\_" 的含义是完全一样的。如果有多行汇编,则每一行都要加上 "\n\t"。其中的 “\n” 是换行符,"\t” 是 tab 符,在每条命令的 结束加这两个符号,是为了让 gcc 把内联汇编代码翻译成一般的汇编代码时能够保证换行和留有一定的空格。对于基本 asm 语句,GCC 编译出来的汇编代码就是双引号里的内容。例如: 11 | 12 | ``` 13 | asm( "pushl %eax\n\t" 14 | "movl $0,%eax\n\t" 15 | "popl %eax" 16 | ); 17 | ``` 18 | 19 | 实际上 gcc 在处理汇编时,是要把 asm(...)的内容"打印"到汇编文件中,所以格式控制字符是必要的。再例如: 20 | 21 | ``` 22 | asm("movl %eax, %ebx"); 23 | asm("xorl %ebx, %edx"); 24 | asm("movl $0, _boo); 25 | ``` 26 | 27 | 在上面的例子中,由于我们在内联汇编中改变了 edx 和 ebx 的值,但是由于 gcc 的特殊的处理方法,即先形成汇编文件,再交给 GAS 去汇编,所以 GAS 并不知道我们已经改变了 edx 和 ebx 的值,如果程序的上下文需要 edx 或 ebx 作其他内存单元或变量的暂存,就会产生没有预料的多次赋值,引起严重的后果。对于变量 \_boo 也存在一样的问题。为了解决这个问题,就要用到扩展 GCC 内联汇编语法。 28 | 29 | 参考: 30 | 31 | - [GCC Manual, 版本为 5.0.0 pre-release,6.43 节(How to Use Inline Assembly Language in C Code)](https://gcc.gnu.org/onlinedocs/gcc.pdf) 32 | - [GCC-Inline-Assembly-HOWTO](http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html) 33 | -------------------------------------------------------------------------------- /lab0/lab0_2_3_1_gcc_usage.md: -------------------------------------------------------------------------------- 1 | #### gcc 的基本用法 2 | 3 | 如果你还没装 gcc 编译环境或自己不确定装没装,不妨先执行 : 4 | 5 | sudo apt-get install build-essential 6 | -------------------------------------------------------------------------------- /lab0/lab0_2_3_2_make_makefile.md: -------------------------------------------------------------------------------- 1 | #### make 和 Makefile 2 | 3 | GNU make(简称 make)是一种代码维护工具,在大中型项目中,它将根据程序各个模块的更新情况,自动的维护和生成目标代码。 4 | 5 | make 命令执行时,需要一个 makefile (或 Makefile)文件,以告诉 make 命令需要怎么样的去编译和链接程序。首先,我们用一个示例来说明 makefile 的书写规则。以便给大家一个感兴认识。这个示例来源于 gnu 的 make 使用手册,在这个示例中,我们的工程有 8 个 c 文件,和 3 个头文件,我们要写一个 makefile 来告诉 make 命令如何编译和链接这几个文件。我们的规则是: 6 | 7 | - 如果这个工程没有编译过,那么我们的所有 c 文件都要编译并被链接。 8 | - 如果这个工程的某几个 c 文件被修改,那么我们只编译被修改的 c 文件,并链接目标程序。 9 | - 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的 c 文件,并链接目标程序。 10 | 11 | 只要我们的 makefile 写得够好,所有的这一切,我们只用一个 make 命令就可以完成,make 命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译,从而自己编译所需要的文件和链接目标程序。 12 | 13 | ##### 2.3.2.1.1 makefile 的规则 14 | 15 | 在讲述这个 makefile 之前,还是让我们先来粗略地看一看 makefile 的规则。 16 | 17 | ``` 18 | target ... : prerequisites ... 19 | command 20 | ... 21 | ... 22 | ``` 23 | 24 | target 也就是一个目标文件,可以是 object file,也可以是执行文件。还可以是一个标签(label)。prerequisites 就是,要生成那个 target 所需要的文件或是目标。command 也就是 make 需要执行的命令(任意的 shell 命令)。 这是一个文件的依赖关系,也就是说,target 这一个或多个的目标文件依赖于 prerequisites 中的文件,其生成规则定义在 command 中。如果 prerequisites 中有一个以上的文件比 target 文件要新,那么 command 所定义的命令就会被执行。这就是 makefile 的规则。也就是 makefile 中最核心的内容。 25 | -------------------------------------------------------------------------------- /lab0/lab0_2_3_3_gdb.md: -------------------------------------------------------------------------------- 1 | #### gdb 使用 2 | 3 | gdb 是功能强大的调试程序,可完成如下的调试任务: 4 | 5 | - 设置断点 6 | - 监视程序变量的值 7 | - 程序的单步(step in/step over)执行 8 | - 显示/修改变量的值 9 | - 显示/修改寄存器 10 | - 查看程序的堆栈情况 11 | - 远程调试 12 | - 调试线程 13 | 14 | 在可以使用 gdb 调试程序之前,必须使用 -g 或 –ggdb 编译选项编译源文件。运行 gdb 调试程序时通常使用如下的命令: 15 | 16 | gdb progname 17 | 18 | 在 gdb 提示符处键入 help,将列出命令的分类,主要的分类有: 19 | 20 | - aliases:命令别名 21 | - breakpoints:断点定义; 22 | - data:数据查看; 23 | - files:指定并查看文件; 24 | - internals:维护命令; 25 | - running:程序执行; 26 | - stack:调用栈查看; 27 | - status:状态查看; 28 | - tracepoints:跟踪程序执行。 29 | 30 | 键入 help 后跟命令的分类名,可获得该类命令的详细清单。gdb 的常用命令如下表所示。 31 | 32 | 表 gdb 的常用命令 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
break FILENAME:NUM在特定源文件特定行上设置断点
clear FILENAME:NUM删除设置在特定源文件特定行上的断点
run运行调试程序
step单步执行调试程序,不会直接执行函数
next单步执行调试程序,会直接执行函数
backtrace显示所有的调用栈帧。该命令可用来显示函数的调用顺序
where continue继续执行正在调试的程序
display EXPR每次程序停止后显示表达式的值,表达式由程序定义的变量组成
file FILENAME装载指定的可执行文件进行调试
help CMDNAME显示指定调试命令的帮助信息
info break显示当前断点列表,包括到达断点处的次数等
info files显示被调试文件的详细信息
info func显示被调试程序的所有函数名称
info prog显示被调试程序的执行状态
info local显示被调试程序当前函数中的局部变量信息
info var显示被调试程序的所有全局和静态变量名称
kill终止正在被调试的程序
list显示被调试程序的源代码
quit退出 gdb
55 | 56 | #### gdb 调试实例 57 | 58 | 下面以一个有错误的例子程序来介绍 gdb 的使用: 59 | 60 | /*bugging.c*/ 61 | #include 62 | #include 63 | 64 | static char buff [256]; 65 | static char* string; 66 | int main () 67 | { 68 | printf ("Please input a string: "); 69 | gets (string); 70 | printf ("\nYour string is: %s\n", string); 71 | } 72 | 73 | 这个程序是接受用户的输入,然后将用户的输入打印出来。该程序使用了一个未经过初始化的字符串地址 string,因此,编译并运行之后,将出现 "Segment Fault"错误: 74 | 75 | $ gcc -o bugging -g bugging.c 76 | $ ./bugging 77 | Please input a string: asdf 78 | Segmentation fault (core dumped) 79 | 80 | 为了查找该程序中出现的问题,我们利用 gdb,并按如下的步骤进行: 81 | 82 | [1] 运行 “gdb bugging” ,加载 bugging 可执行文件; 83 | 84 | $gdb bugging 85 | 86 | [2] 执行装入的 bugging 命令; 87 | 88 | (gdb) run 89 | 90 | [3] 使用 where 命令查看程序出错的地方; 91 | 92 | (gdb) where 93 | 94 | [4] 利用 list 命令查看调用 gets 函数附近的代码; 95 | 96 | (gdb) list 97 | 98 | [5] 在 gdb 中,我们在第 11 行处设置断点,看看是否是在第 11 行出错; 99 | 100 | (gdb) break 11 101 | 102 | [6] 程序重新运行到第 11 行处停止,这时程序正常,然后执行单步命令 next; 103 | (gdb) next 104 | [7] 程序确实出错,能够导致 gets 函数出错的因素就是变量 string。重新执行测试程,用 print 命令查看 string 的值; 105 | 106 | (gdb) run 107 | (gdb) print string 108 | (gdb) $1=0x0 109 | 110 | [8] 问题在于 string 指向的是一个无效指针,修改程序,在 10 行和 11 行之间增加一条语句 “string=buff; ”,重新编译程序,然后继续运行,将看到正确的程序运行结果。 111 | 112 | 用 gdb 查看源代码可以用 list 命令,但是这个不够灵活。可以使用"layout src"命令,或者按 Ctrl-X 再按 A,就会出现一个窗口可以查看源代码。也可以用使用-tui 参数,这样进入 gdb 里面后就能直接打开代码查看窗口。其他代码窗口相关命令: 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 |
info win显示窗口的大小
layout next切换到下一个布局模式
layout prev切换到上一个布局模式
layout src只显示源代码
layout asm只显示汇编代码
layout split显示源代码和汇编代码
layout regs增加寄存器内容显示
focus cmd/src/asm/regs/next/prev切换当前窗口
refresh刷新所有窗口
tui reg next显示下一组寄存器
tui reg system显示系统寄存器
update更新源代码窗口和当前执行点
winheight name +/- line调整name窗口的高度
tabset nchar设置tab为nchar个字符
130 | -------------------------------------------------------------------------------- /lab0/lab0_2_3_4_further.md: -------------------------------------------------------------------------------- 1 | ##### 进一步的相关内容 2 | 3 | 请同学网上搜寻相关资料学习: 4 | 5 | gcc tools 相关文档 6 | 7 | 版本管理软件(CVS、SVN、GIT 等)的使用 8 | 9 | … 10 | -------------------------------------------------------------------------------- /lab0/lab0_2_3_tools.md: -------------------------------------------------------------------------------- 1 | ### 了解编程开发调试的基本工具 2 | 3 | 在 Ubuntu Linux 中的 C 语言编程主要基于 GNU C 的语法,通过 gcc 来编译并生成最终执行文件。GNU 汇编(assembler)采用的是 AT&T 汇编格式,Microsoft 汇编采用 Intel 格式。 4 | -------------------------------------------------------------------------------- /lab0/lab0_2_4_1_1_linux_runtime.md: -------------------------------------------------------------------------------- 1 | ##### Linux 运行环境 2 | 3 | QEMU 用于模拟一台 x86 计算机,让 ucore 能够运行在 QEMU 上。为了能够正确的编译和安装 qemu,尽量使用最新版本的[qemu](http://wiki.qemu.org/Download),或者 os ftp 服务器上提供的 qemu 源码:qemu-1.1.0.tar.gz)。目前 qemu 能够支持最新的 gcc-4.x 编译器。例如:在 Ubuntu 12.04 系统中,默认得版本是 gcc-4.6.x (可以通过 gcc -v 或者 gcc --version 进行查看)。 4 | 5 | 可直接使用 ubuntu 中提供的 qemu,只需执行如下命令即可。 6 | 7 | sudo apt-get install qemu-system 8 | 9 | 也可采用下面描述的方法对 qemu 进行源码级安装。 10 | -------------------------------------------------------------------------------- /lab0/lab0_2_4_1_2_1_patch_qemu.md: -------------------------------------------------------------------------------- 1 | ###### 获得并应用修改 2 | 3 | 编译 qemu 还会用到的库文件有 libsdl1.2-dev 等。安装命令如下: 4 | 5 | sudo apt-get install libsdl1.2-dev # 安装库文件 libsdl1.2-dev 6 | 7 | 获得 qemu 的安装包以后,对其进行解压缩(如果格式无法识别,请下载相应的解压缩软件)。 8 | 9 | 例如 qemu.tar.gz/qemu.tar.bz2 文件,在命令行中可以使用: 10 | 11 | tar zxvf qemu.tar.gz 12 | 13 | 或者 14 | 15 | tar jxvf qemu.tar.bz2 16 | 17 | 对 qemu 应用修改:如果实验中使用的 qemu 需要打 patch,应用过程如下所示: 18 | 19 | chy@chyhome-PC:~$ls 20 | qemu.patch qemu 21 | chy@chyhome-PC:~$cd qemu 22 | chy@chyhome-PC:~$patch -p1 -u < ../qemu.patch 23 | -------------------------------------------------------------------------------- /lab0/lab0_2_4_1_2_2_configure_make_install_qemu.md: -------------------------------------------------------------------------------- 1 | ###### 配置、编译和安装 2 | 3 | 编译以及安装 qemu 前需要使用 (表示 qemu 解压缩路径)下面的 configure 脚本生成相应的配置文件等。而 configure 脚本有较多的参数可供选择,可以通过如下命令进行查看: 4 | configure --help 5 | 6 | 实验中可能会用到的命令例如: 7 | 8 | configure --target-list="i386-softmmu" # 配置qemu,可模拟X86-32硬件环境 9 | make # 编译qemu 10 | sudo make install # 安装qemu 11 | 12 | qemu 执行程序将缺省安装到 /usr/local/bin 目录下。 13 | 14 | 如果使用的是默认的安装路径,那么在 “/usr/local/bin” 下面即可看到安装结果: 15 | 16 | qemu-system-i386 qemu-img qemu-nbd …… 17 | 18 | 建立符号链接文件 qemu 19 | 20 | sudo ln –s /usr/local/bin/qemu-system-i386 /usr/local/bin/qemu 21 | -------------------------------------------------------------------------------- /lab0/lab0_2_4_1_2_linux_source_install.md: -------------------------------------------------------------------------------- 1 | ##### Linux 环境下的源码级安装过程 2 | -------------------------------------------------------------------------------- /lab0/lab0_2_4_1_install_qemu.md: -------------------------------------------------------------------------------- 1 | #### 安装硬件模拟器 QEMU 2 | -------------------------------------------------------------------------------- /lab0/lab0_2_4_2_1_qemu_runtime_arguments.md: -------------------------------------------------------------------------------- 1 | ##### 运行参数 2 | 3 | 如果 qemu 使用的是默认 /usr/local/bin 安装路径,则在命令行中可以直接使用 qemu 命令运行程序。qemu 运行可以有多参数,格式如: 4 | 5 | qemu [options] [disk_image] 6 | 7 | 其中 disk_image 即硬盘镜像文件。 8 | 9 | 部分参数说明: 10 | 11 | `-hda file' `-hdb file' `-hdc file' `-hdd file' 12 | 使用 file 作为硬盘0、1、2、3镜像。 13 | `-fda file' `-fdb file' 14 | 使用 file 作为软盘镜像,可以使用 /dev/fd0 作为 file 来使用主机软盘。 15 | `-cdrom file' 16 | 使用 file 作为光盘镜像,可以使用 /dev/cdrom 作为 file 来使用主机 cd-rom。 17 | `-boot [a|c|d]' 18 | 从软盘(a)、光盘(c)、硬盘启动(d),默认硬盘启动。 19 | `-snapshot' 20 | 写入临时文件而不写回磁盘镜像,可以使用 C-a s 来强制写回。 21 | `-m megs' 22 | 设置虚拟内存为 msg M字节,默认为 128M 字节。 23 | `-smp n' 24 | 设置为有 n 个 CPU 的 SMP 系统。以 PC 为目标机,最多支持 255 个 CPU。 25 | `-nographic' 26 | 禁止使用图形输出。 27 | 其他: 28 | 可用的主机设备 dev 例如: 29 | vc 30 | 虚拟终端。 31 | null 32 | 空设备 33 | /dev/XXX 34 | 使用主机的 tty。 35 | file: filename 36 | 将输出写入到文件 filename 中。 37 | stdio 38 | 标准输入/输出。 39 | pipe:pipename 40 | 命令管道 pipename。 41 | 等。 42 | 使用 dev 设备的命令如: 43 | `-serial dev' 44 | 重定向虚拟串口到主机设备 dev 中。 45 | `-parallel dev' 46 | 重定向虚拟并口到主机设备 dev 中。 47 | `-monitor dev' 48 | 重定向 monitor 到主机设备 dev 中。 49 | 其他参数: 50 | `-s' 51 | 等待 gdb 连接到端口 1234。 52 | `-p port' 53 | 改变 gdb 连接端口到 port。 54 | `-S' 55 | 在启动时不启动 CPU, 需要在 monitor 中输入 'c',才能让qemu继续模拟工作。 56 | `-d' 57 | 输出日志到 qemu.log 文件。 58 | 59 | 其他参数说明可以参考:http://bellard.org/qemu/qemu-doc.html#SEC15 。其他 qemu 的安装和使用的说明可以参考http://bellard.org/qemu/user-doc.html。 60 | 61 | 或者在命令行收入 qemu (没有参数) 显示帮助。 62 | 63 | 在实验中,例如 lab1,可能用到的命令如: 64 | 65 | qemu -hda ucore.img -parallel stdio # 让ucore在qemu模拟的x86硬件环境中执行 66 | 67 | 或 68 | 69 | qemu -S -s -hda ucore.img -monitor stdio # 用于与gdb配合进行源码调试 70 | -------------------------------------------------------------------------------- /lab0/lab0_2_4_2_2_qemu_monitor_debug.md: -------------------------------------------------------------------------------- 1 | ##### 常用调试命令 2 | 3 | qemu 中 monitor 的常用命令: 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
help查看 qemu 帮助,显示所有支持的命令。
q|quit|exit退出 qemu。
stop停止 qemu。
c|cont|continue连续执行。
x /fmt addr
xp /fmt addr
显示内存内容,其中 'x' 为虚地址,'xp' 为实地址。
参数 /fmt i 表示反汇编,缺省参数为前一次参数。
p|print'计算表达式值并显示,例如 $reg 表示寄存器结果。
memsave addr size file
pmemsave addr size file
将内存保存到文件,memsave 为虚地址,pmemsave 为实地址。
breakpoint 相关:设置、查看以及删除 breakpoint,pc执行到 breakpoint,qemu 停止。(暂时没有此功能)
watchpoint 相关:设置、查看以及删除 watchpoint, 当 watchpoint 地址内容被修改,停止。(暂时没有此功能)
s|step单步一条指令,能够跳过断点执行。
r|registers显示全部寄存器内容。
info 相关操作查询 qemu 支持的关于系统状态信息的操作。
19 | 20 | 其他具体的命令格式以及说明,参见 qemu help 命令帮助。 21 | 22 | 注意:qemu 默认有 ‘singlestep arg’ 命令(arg 为 参数),该命令为设置单步标志命令。例如:'singlestep off' 运行结果为禁止单步,'singlestep on' 结果为允许单步。在允许单步条件下,使用 cont 命令进行单步操作。如: 23 | 24 | (qemu) xp /3i $pc 25 | 0xfffffff0: ljmp $0xf000, $0xe05b 26 | 0xfffffff5: xor %bh, (%bx, %si) 27 | 0xfffffff7: das 28 | (qemu) singlestep on 29 | (qemu) cont 30 | 0x000fe05b: xor %ax, %ax 31 | 32 | step 命令为单步命令,即 qemu 执行一步,能够跳过 breakpoint 断点执行。如果此时使用 cont 命令,则 qemu 运行改为连续执行。 33 | 34 | log 命令能够保存 qemu 模拟过程产生的信息(与 qemu 运行参数 `-d' 相同),具体参数可以参考命令帮助。产生的日志信息保存在 “/tmp/qemu.log” 中,例如使用 'log in_asm'命令以后,运行过程产生的的 qemu.log 文件为: 35 | 36 | 1 ---------------- 37 | 2 IN: 38 | 3 0xfffffff0: ljmp $0xf000,$0xe05b 39 | 4 40 | 5 ---------------- 41 | 6 IN: 42 | 7 0x000fe05b: xor %ax,%ax 43 | 8 0x000fe05d: out %al,$0xd 44 | 9 0x000fe05f: out %al,$0xda 45 | 10 0x000fe061: mov $0xc0,%al 46 | 11 0x000fe063: out %al,$0xd6 47 | 12 0x000fe065: mov $0x0,%al 48 | 13 0x000fe067: out %al,$0xd4 49 | -------------------------------------------------------------------------------- /lab0/lab0_2_4_2_qemu_usage.md: -------------------------------------------------------------------------------- 1 | #### 使用硬件模拟器 QEMU 2 | -------------------------------------------------------------------------------- /lab0/lab0_2_4_3_qemu_debug_ucore.md: -------------------------------------------------------------------------------- 1 | #### 基于 qemu 内建模式调试 ucore 2 | 3 | 调试举例:调试 lab1,跟踪 bootmain 函数: 4 | 5 | (1) 运行 qemu -S -hda ucore.img -monitor stdio 6 | 7 | (2) 查看 bootblock.asm 得到 bootmain 函数地址为 0x7d60,并插入断点。 8 | 9 | (3) 使用命令 c 连续执行到断点。 10 | 11 | (4) 使用 xp 命令进行反汇编。 12 | 13 | (5) 使用 s 命令进行单步执行。 14 | 运行结果如下: 15 | 16 | chy@laptop: ~/lab1$ qemu -S -hda ucore.img -monitor stdio 17 | (qemu) b 0x7d60 18 | insert breakpoint 0x7d60 success! 19 | (qemu) c 20 | working … 21 | (qemu) 22 | break: 23 | 0x00007d60: push %ebp 24 | (qemu) xp /10i $pc 25 | 0x00007d60: push %ebp 26 | 0x00007d61: mov %esp,%ebp 27 | 0x00007d63: push %esi 28 | 0x00007d64: push %ebx 29 | 0x00007d65: sub $0x4,%esp 30 | 0x00007d68: mov 0x7da8,%esi 31 | 0x00007d6e: mov $0x0,%ebx 32 | 0x00007d73: movsbl (%esi,%ebx,1),%eax 33 | 0x00007d77: mov %eax,(%esp,1) 34 | 0x00007d7a: call 0x7c6c 35 | (qemu) step 36 | 0x00007d61: mov %esp,%ebp 37 | (qemu) step 38 | 0x00007d63: push %esi 39 | -------------------------------------------------------------------------------- /lab0/lab0_2_4_4_1_make_obj.md: -------------------------------------------------------------------------------- 1 | ##### 编译可调试的目标文件 2 | 3 | 为了使得编译出来的代码是能够被 gdb 这样的调试器调试,我们需要在使用 gcc 编译源文件的时候添加参数:"-g"。这样编译出来的目标文件中才会包含可以用于调试器进行调试的相关符号信息。 4 | -------------------------------------------------------------------------------- /lab0/lab0_2_4_4_2_ucore_make.md: -------------------------------------------------------------------------------- 1 | ##### ucore 代码编译 2 | 3 | (1) 编译过程:在解压缩后的 ucore 源码包中使用 make 命令即可。例如 lab1 中: 4 | 5 | ``` 6 | chy@laptop: ~/lab1$ make 7 | ``` 8 | 9 | 在 lab1 目录下的 bin 目录中,生成一系列的目标文件: 10 | 11 | - ucore.img:被 qemu 访问的虚拟硬盘文件 12 | - kernel: ELF 格式的 toy ucore kernel 执行文,被嵌入到了 ucore.img 中 13 | - bootblock: 虚拟的硬盘主引导扇区(512 字节),包含了 bootloader 执行代码,被嵌入到了 ucore.img 中 14 | - sign:外部执行程序,用来生成虚拟的硬盘主引导扇区 15 | 16 | 还生成了其他很多文件,这里就不一一列举了。 17 | 18 | (2) 保存修改: 19 | 20 | 使用 diff 命令对修改后的 ucore 代码和 ucore 源码进行比较,比较之前建议使用 make clean 命令清除不必要文件。(如果有 ctags 文件,需要手工清除。) 21 | 22 | (3)应用修改:参见 patch 命令说明。 23 | -------------------------------------------------------------------------------- /lab0/lab0_2_4_4_3_remote_debug.md: -------------------------------------------------------------------------------- 1 | ##### 使用远程调试 2 | 3 | 为了与 qemu 配合进行源代码级别的调试,需要先让 qemu 进入等待 gdb 调试器的接入并且还不能让 qemu 中的 CPU 执行,因此启动 qemu 的时候,我们需要使用参数-S –s 这两个参数来做到这一点。在使用了前面提到的参数启动 qemu 之后,qemu 中的 CPU 并不会马上开始执行,这时我们启动 gdb,然后在 gdb 命令行界面下,使用下面的命令连接到 qemu: 4 | 5 | (gdb) target remote 127.0.0.1:1234 6 | 7 | 然后输入 c(也就是 continue)命令之后,qemu 会继续执行下去,但是 gdb 由于不知道任何符号信息,并且也没有下断点,是不能进行源码级的调试的。为了让 gdb 获知符号信息,需要指定调试目标文件,gdb 中使用 file 命令: 8 | 9 | (gdb) file ./bin/kernel 10 | 11 | 之后 gdb 就会载入这个文件中的符号信息了。 12 | 13 | 通过 gdb 可以对 ucore 代码进行调试,以 lab1 中调试 memset 函数为例: 14 | 15 | (1) 运行 `qemu -S -s -hda ./bin/ucore.img -monitor stdio` 16 | 17 | (2) 运行 gdb 并与 qemu 进行连接 18 | 19 | (3) 设置断点并执行 20 | 21 | (4) qemu 单步调试。 22 | 23 | 运行过程以及结果如下: 24 | 25 | 26 | 27 | 28 | 31 | 44 |
窗口一窗口二
29 | chy@laptop: ~/lab1$ qemu -S -s -hda ./bin/ucore.img 30 | 32 | chy@laptop: ~/lab1$ gdb ./bin/kernel
33 | (gdb) target remote:1234
34 | Remote debugging using :1234
35 | 0x0000fff0 in ?? ()
36 | (gdb) break memset
37 | Breakpoint 1, memset (s=0xc029b000, c=0x0, n=0x1000) at libs/string.c:271
38 | (gdb) continue
39 | Continuing.
40 | Breakpoint 1, memset (s=0xc029b000, c=0x0, n=0x1000) at libs/string.c:271
41 | 271 memset(void *s, char c, size_t n) {
42 | (gdb) 43 |
45 | -------------------------------------------------------------------------------- /lab0/lab0_2_4_4_4_gdb_config_file.md: -------------------------------------------------------------------------------- 1 | ##### 使用 gdb 配置文件 2 | 3 | 在上面可以看到,为了进行源码级调试,需要输入较多的东西,很麻烦。为了方便,可以将这些命令存在脚本中,并让 gdb 在启动的时候自动载入。 4 | 5 | 以 lab1 为例,在 lab1/tools 目录下,执行完`make`后,我们可以创建文件`gdbinit`,并输入下面的内容: 6 | 7 | ``` 8 | target remote 127.0.0.1:1234 9 | file bin/kernel 10 | ``` 11 | 12 | 为了让 gdb 在启动时执行这些命令,使用下面的命令启动 gdb: 13 | 14 | ``` 15 | $ gdb -x tools/gdbinit 16 | ``` 17 | 18 | 如果觉得这个命令太长,可以将这个命令存入一个文件中,当作脚本来执行。 19 | 20 | 另外,如果直接使用上面的命令,那么得到的界面是一个纯命令行的界面,不够直观,就像下图这样: 21 | 22 | ![纯命令行的界面](../lab0_figs/image002.png "纯命令行的界面") 23 | 24 | 如果想获得上面右图那样的效果,只需要再加上参数-tui 就行了,比如: 25 | 26 | ``` 27 | gdb -tui -x tools/gdbinit 28 | ``` 29 | -------------------------------------------------------------------------------- /lab0/lab0_2_4_4_5_load_debug_target.md: -------------------------------------------------------------------------------- 1 | ##### 加载调试目标 2 | 3 | 在上面小节,我们提到为了能够让 gdb 识别变量的符号,我们必须给 gdb 载入符号表等信息。在进行 gdb 本地应用程序调试的时候,因为在指定了执行文件时就已经加载了文件中包含的调试信息,因此不用再使用 gdb 命令专门加载了。但是在使用 qemu 进行远程调试的时候,我们必须手动加载符号表,也就是在 gdb 中用 file 命令。 4 | 5 | 这样加载调试信息都是按照 elf 文件中制定的虚拟地址进行加载的,这在静态连接的代码中没有任何问题。但是在调试含有动态链接库的代码时,动态链接库的 ELF 执行文件头中指定的加载虚拟地址都是 0,这个地址实际上是不正确的。从操作系统角度来看,用户态的动态链接库的加载地址都是由操作系统动态分配的,没有一个固定值。然后操作系统再把动态链接库加载到这个地址,并由用户态的库链接器(linker)把动态链接库中的地址信息重新设置,自此动态链接库才可正常运行。 6 | 7 | 由于分配地址的动态性,gdb 并不知道这个分配的地址是多少,因此当我们在对这样动态链接的代码进行调试的时候,需要手动要求 gdb 将调试信息加载到指定地址。 8 | 9 | 下面,我们要求 gdb 将 linker 加载到 0x6fee6180 这个地址上: 10 | 11 | (gdb) add-symbol-file android_test/system/bin/linker 0x6fee6180 12 | 13 | 这样的命令默认是将代码段(.data)段的调试信息加载到 0x6fee6180 上,当然,你也可以通过“-s”这个参数来指定,比如: 14 | (gdb) add-symbol-file android_test/system/bin/linker –s .text 0x6fee6180 15 | 16 | 这样,在执行到 linker 中代码时 gdb 就能够显示出正确的代码和调试信息出来。 17 | 18 | 这个方法在操作系统中调试动态链接器时特别有用。 19 | -------------------------------------------------------------------------------- /lab0/lab0_2_4_4_6_set_debug_arch.md: -------------------------------------------------------------------------------- 1 | ##### 设定调试目标架构 2 | 3 | 在调试的时候,我们也许需要调试不是 i386 保护模式的代码,比如 8086 实模式的代码,我们需要设定当前使用的架构: 4 | 5 | (gdb) set arch i8086 6 | 7 | 这个方法在调试不同架构或者说不同模式的代码时还是有点用处的。 8 | -------------------------------------------------------------------------------- /lab0/lab0_2_4_4_gdb_qemu_debug_ucore.md: -------------------------------------------------------------------------------- 1 | #### 结合 gdb 和 qemu 源码级调试 ucore 2 | -------------------------------------------------------------------------------- /lab0/lab0_2_4_debug_with_emulator.md: -------------------------------------------------------------------------------- 1 | ### 基于硬件模拟器实现源码级调试 2 | -------------------------------------------------------------------------------- /lab0/lab0_2_5_1_intel_80386_modes.md: -------------------------------------------------------------------------------- 1 | #### Intel 80386 运行模式 2 | 3 | 一般 CPU 只有一种运行模式,能够支持多个程序在各自独立的内存空间中并发执行,且有用户特权级和内核特权级的区分,让一般应用不能破坏操作系统内核和执行特权指令。80386 处理器有四种运行模式:实模式、保护模式、SMM 模式和虚拟 8086 模式。这里对涉及 ucore 的实模式、保护模式做一个简要介绍。 4 | 5 | 实模式:这是个人计算机早期的 8086 处理器采用的一种简单运行模式,当时微软的 MS-DOS 操作系统主要就是运行在 8086 的实模式下。80386 加电启动后处于实模式运行状态,在这种状态下软件可访问的物理内存空间不能超过 1MB,且无法发挥 Intel 80386 以上级别的 32 位 CPU 的 4GB 内存管理能力。实模式将整个物理内存看成分段的区域,程序代码和数据位于不同区域,操作系统和用户程序并没有区别对待,而且每一个指针都是指向实际的物理地址。这样用户程序的一个指针如果指向了操作系统区域或其他用户程序区域,并修改了内容,那么其后果就很可能是灾难性的。 6 | 7 | > 对于 ucore 其实没有必要涉及,这主要是 Intel x86 的向下兼容需求导致其一直存在。其他一些 CPU,比如 ARM、MIPS 等就没有实模式,而是只有类似保护模式这样的 CPU 模式。 8 | 9 | 保护模式:保护模式的一个主要目标是确保应用程序无法对操作系统进行破坏。实际上,80386 就是通过在实模式下初始化控制寄存器(如 GDTR,LDTR,IDTR 与 TR 等管理寄存器)以及页表,然后再通过设置 CR0 寄存器使其中的保护模式使能位置位,从而进入到 80386 的保护模式。当 80386 工作在保护模式下的时候,其所有的 32 根地址线都可供寻址,物理寻址空间高达 4GB。在保护模式下,支持内存分页机制,提供了对虚拟内存的良好支持。保护模式下 80386 支持多任务,还支持优先级机制,不同的程序可以运行在不同的特权级上。特权级一共分 0 ~ 3 四个级别,操作系统运行在最高的特权级 0 上,应用程序则运行在比较低的级别上;配合良好的检查机制后,既可以在任务间实现数据的安全共享也可以很好地隔离各个任务。 10 | 11 | > 这一段中很多术语没有解释,在后续的章节中会逐一展开阐述。 12 | -------------------------------------------------------------------------------- /lab0/lab0_2_5_2_intel_80386_mem.md: -------------------------------------------------------------------------------- 1 | #### Intel 80386 内存架构 2 | 3 | 地址是访问内存空间的索引。一般而言,内存地址有两个:一个是 CPU 通过总线访问物理内存用到的物理地址,一个是我们编写的应用程序所用到的逻辑地址(也有人称为虚拟地址)。比如如下 C 代码片段: 4 | 5 | ``` 6 | int boo=1; 7 | int *foo=&a; 8 | ``` 9 | 10 | 这里的 boo 是一个整型变量,foo 变量是一个指向 boo 地址的整型指针变量,foo 中储存的内容就是 boo 的逻辑地址。 11 | 12 | 80386 是 32 位的处理器,即可以寻址的物理内存地址空间为 2\^32=4G 字节。为更好理解面向 80386 处理器的 ucore 操作系统,需要用到三个地址空间的概念:物理地址、线性地址和逻辑地址。物理内存地址空间是处理器提交到总线上用于访问计算机系统中的内存和外设的最终地址。一个计算机系统中只有一个物理地址空间。线性地址空间是 80386 处理器通过段(Segment)机制控制下的形成的地址空间。在操作系统的管理下,每个运行的应用程序有相对独立的一个或多个内存空间段,每个段有各自的起始地址和长度属性,大小不固定,这样可让多个运行的应用程序之间相互隔离,实现对地址空间的保护。 13 | 14 | 在操作系统完成对 80386 处理器段机制的初始化和配置(主要是需要操作系统通过特定的指令和操作建立全局描述符表,完成虚拟地址与线性地址的映射关系)后,80386 处理器的段管理功能单元负责把虚拟地址转换成线性地址,在没有下面介绍的页机制启动的情况下,这个线性地址就是物理地址。 15 | 16 | 相对而言,段机制对大量应用程序分散地使用大内存的支持能力较弱。所以 Intel 公司又加入了页机制,每个页的大小是固定的(一般为 4KB),也可完成对内存单元的安全保护,隔离,且可有效支持大量应用程序分散地使用大内存的情况。 17 | 18 | 在操作系统完成对 80386 处理器页机制的初始化和配置(主要是需要操作系统通过特定的指令和操作建立页表,完成虚拟地址与线性地址的映射关系)后,应用程序看到的逻辑地址先被处理器中的段管理功能单元转换为线性地址,然后再通过 80386 处理器中的页管理功能单元把线性地址转换成物理地址。 19 | 20 | > 页机制和段机制有一定程度的功能重复,但 Intel 公司为了向下兼容等目标,使得这两者一直共存。 21 | 22 | 上述三种地址的关系如下: 23 | 24 | - 分段机制启动、分页机制未启动:逻辑地址---\>**_段机制处理_**---\>线性地址=物理地址 25 | 26 | - 分段机制和分页机制都启动:逻辑地址---\>**_段机制处理_**---\>线性地址---\>**_页机制处理_**---\>物理地址 27 | -------------------------------------------------------------------------------- /lab0/lab0_2_5_3_intel_80386_registers.md: -------------------------------------------------------------------------------- 1 | #### Intel 80386 寄存器 2 | 3 | 这里假定读者对 80386 CPU 有一定的了解,所以只作简单介绍。80386 的寄存器可以分为 8 组:通用寄存器,段寄存器,指令指针寄存器,标志寄存器,系统地址寄存器,控制寄存器,调试寄存器,测试寄存器,它们的宽度都是 32 位。一般程序员看到的寄存器包括通用寄存器,段寄存器,指令指针寄存器,标志寄存器。 4 | 5 | General Register(通用寄存器):EAX/EBX/ECX/EDX/ESI/EDI/ESP/EBP 这些寄存器的低 16 位就是 8086 的 AX/BX/CX/DX/SI/DI/SP/BP,对于 AX,BX,CX,DX 这四个寄存器来讲,可以单独存取它们的高 8 位和低 8 位 (AH,AL,BH,BL,CH,CL,DH,DL)。它们的含义如下: 6 | 7 | ``` 8 | EAX:累加器 9 | EBX:基址寄存器 10 | ECX:计数器 11 | EDX:数据寄存器 12 | ESI:源地址指针寄存器 13 | EDI:目的地址指针寄存器 14 | EBP:基址指针寄存器 15 | ESP:堆栈指针寄存器 16 | ``` 17 | 18 | ![通用寄存器](../lab0_figs/image003.png "通用寄存器") 19 | 20 | Segment Register(段寄存器,也称 Segment Selector,段选择符,段选择子):除了 8086 的 4 个段外(CS,DS,ES,SS),80386 还增加了两个段 FS,GS,这些段寄存器都是 16 位的,用于不同属性内存段的寻址,它们的含义如下: 21 | 22 | ``` 23 | CS:代码段(Code Segment) 24 | DS:数据段(Data Segment) 25 | ES:附加数据段(Extra Segment) 26 | SS:堆栈段(Stack Segment) 27 | FS:附加段 28 | GS 附加段 29 | ``` 30 | 31 | ![段寄存器](../lab0_figs/image004.png "段寄存器") 32 | 33 | Instruction Pointer(指令指针寄存器):EIP 的低 16 位就是 8086 的 IP,它存储的是下一条要执行指令的内存地址,在分段地址转换中,表示指令的段内偏移地址。 34 | 35 | ![状态和指令寄存器](../lab0_figs/image005.png "状态和指令寄存器") 36 | 37 | Flag Register(标志寄存器):EFLAGS,和 8086 的 16 位标志寄存器相比,增加了 4 个控制位,这 20 位控制/标志位的位置如下图所示: 38 | 39 | ![状态寄存器](../lab0_figs/image006.png "状态寄存器") 40 | 41 | 相关的控制/标志位含义是: 42 | 43 | ``` 44 | CF(Carry Flag):进位标志位; 45 | PF(Parity Flag):奇偶标志位; 46 | AF(Assistant Flag):辅助进位标志位; 47 | ZF(Zero Flag):零标志位; 48 | SF(Singal Flag):符号标志位; 49 | IF(Interrupt Flag):中断允许标志位,由CLI,STI两条指令来控制;设置IF位使CPU可识别外部(可屏蔽)中断请求,复位IF位则禁止中断,IF位对不可屏蔽外部中断和故障中断的识别没有任何作用; 50 | DF(Direction Flag):向量标志位,由CLD,STD两条指令来控制; 51 | OF(Overflow Flag):溢出标志位; 52 | IOPL(I/O Privilege Level):I/O特权级字段,它的宽度为2位,它指定了I/O指令的特权级。如果当前的特权级别在数值上小于或等于IOPL,那么I/O指令可执行。否则,将发生一个保护性故障中断; 53 | NT(Nested Task):控制中断返回指令IRET,它宽度为1位。若NT=0,则用堆栈中保存的值恢复EFLAGS,CS和EIP从而实现中断返回;若NT=1,则通过任务切换实现中断返回。在ucore中,设置NT为0。 54 | ``` 55 | 56 | 还有一些应用程序无法访问的控制寄存器,如 CR0,CR2,CR3...,将在后续章节逐一讲解。 57 | -------------------------------------------------------------------------------- /lab0/lab0_2_5_cpu_hardware.md: -------------------------------------------------------------------------------- 1 | ### 了解处理器硬件 2 | 3 | 要想深入理解 ucore,就需要了解支撑 ucore 运行的硬件环境,即了解处理器体系结构(了解硬件对 ucore 带来影响)和机器指令集(读懂 ucore 的汇编)。ucore 目前支持的硬件环境是基于 Intel 80386 以上的计算机系统。更多的硬件相关内容(比如保护模式等)将随着实现 ucore 的过程逐渐展开介绍。 4 | -------------------------------------------------------------------------------- /lab0/lab0_2_6_1_oop.md: -------------------------------------------------------------------------------- 1 | #### 面向对象编程方法 2 | 3 | uCore 设计中采用了一定的面向对象编程方法。虽然 C 语言对面向对象编程并没有原生支持,但没有原生支持并不等于我们不能用 C 语言写面向对象程序。需要注意,我们并不需要用 C 语言模拟出一个常见 C++ 编译器已经实现的对象模型。如果是这样,还不如直接采用 C++编程。 4 | 5 | uCore 的面向对象编程方法,目前主要是采用了类似 C++的接口(interface)概念,即是让实现细节不同的某类内核子系统(比如物理内存分配器、调度器,文件系统等)有共同的操作方式,这样虽然内存子系统的实现千差万别,但它的访问接口是不变的。这样不同的内核子系统之间就可以灵活组合在一起,实现风格各异,功能不同的操作系统。接口在 C 语言中,表现为一组函数指针的集合。放在 C++ 中,即为虚表。接口设计的难点是如果找出各种内核子系统的共性访问/操作模式,从而可以根据访问模式提取出函数指针列表。 6 | 7 | 比如对于 uCore 内核中的物理内存管理子系统,首先通过分析内核中其他子系统可能对物理内存管理子系统,明确物理内存管理子系统的访问/操作模式,然后我们定义了 pmm_manager 数据结构(位于 lab2/kern/mm/pmm.h)如下: 8 | 9 | // pmm_manager is a physical memory management class. A special pmm manager - XXX_pmm_manager 10 | // only needs to implement the methods in pmm_manager class, then XXX_pmm_manager can be used 11 | // by ucore to manage the total physical memory space. 12 | struct pmm_manager { 13 | // XXX_pmm_manager's name 14 | const char *name; 15 | // initialize internal description&management data structure 16 | // (free block list, number of free block) of XXX_pmm_manager 17 | void (*init)(void); 18 | // setup description&management data structcure according to 19 | // the initial free physical memory space 20 | void (*init_memmap)(struct Page *base, size_t n); 21 | // allocate >=n pages, depend on the allocation algorithm 22 | struct Page *(*alloc_pages)(size_t n); 23 | // free >=n pages with "base" addr of Page descriptor structures(memlayout.h) 24 | void (*free_pages)(struct Page *base, size_t n); 25 | // return the number of free pages 26 | size_t (*nr_free_pages)(void); 27 | // check the correctness of XXX_pmm_manager 28 | void (*check)(void); 29 | }; 30 | 31 | 这样基于此数据结构,我们可以实现不同连续内存分配算法的物理内存管理子系统,而这些物理内存管理子系统需要编写算法,把算法实现在此结构中定义的 init(初始化)、init_memmap(分析空闲物理内存并初始化管理)、alloc_pages(分配物理页)、free_pages(释放物理页)函数指针所对应的函数中。而其他内存子系统需要与物理内存管理子系统交互时,只需调用特定物理内存管理子系统所采用的 pmm_manager 数据结构变量中的函数指针即可 32 | -------------------------------------------------------------------------------- /lab0/lab0_2_6_2_generic_data_structure.md: -------------------------------------------------------------------------------- 1 | #### 通用数据结构 2 | -------------------------------------------------------------------------------- /lab0/lab0_2_6_ucore_programming.md: -------------------------------------------------------------------------------- 1 | ### 了解 ucore 编程方法和通用数据结构 2 | -------------------------------------------------------------------------------- /lab0/lab0_2_prepare.md: -------------------------------------------------------------------------------- 1 | ## 准备知识 2 | -------------------------------------------------------------------------------- /lab0/lab0_ref_ucore-tools.md: -------------------------------------------------------------------------------- 1 | # ucore 实验中的常用工具 2 | 3 | 在 ucore 实验中,一些基本的常用工具如下: 4 | 5 | - 命令行 shell: bash shell -- 有对文件和目录操作的各种命令,如 ls、cd、rm、pwd... 6 | - 系统维护工具:apt、git 7 | - apt:安装管理各种软件,主要在 debian, ubuntu linux 系统中 8 | - git:开发软件的版本维护工具 9 | - 源码阅读与编辑工具:eclipse-CDT、understand、gedit、vim 10 | - Eclipse-CDT:基于 Eclipse 的 C/C++集成开发环境、跨平台、丰富的分析理解代码的功能,可与 qemu 结合,联机源码级 Debug uCore OS。 11 | - Understand:商业软件、跨平台、丰富的分析理解代码的功能,Windows 上有类似的 sourceinsight 软件 12 | - gedit:Linux 中的常用文本编辑,Windows 上有类似的 notepad 13 | - vim: Linux/unix 中的传统编辑器,类似有 emacs 等,可通过 exuberant-ctags、cscope 等实现代码定位 14 | - 源码比较和打补丁工具:diff、meld,用于比较不同目录或不同文件的区别, patch 是打补丁工具 15 | - diff, patch 是命令行工具,使用简单 16 | - meld 是图形界面的工具,功能相对直观和方便,类似的工具还有 kdiff3、diffmerge、P4merge 17 | - 开发编译调试工具:gcc 、gdb 、make 18 | - gcc:C 语言编译器 19 | - gdb:执行程序调试器 20 | - ld:链接器 21 | - objdump:对 ELF 格式执行程序文件进行反编译、转换执行格式等操作的工具 22 | - nm:查看执行文件中的变量、函数的地址 23 | - readelf:分析 ELF 格式的执行程序文件 24 | - make:软件工程管理工具, make 命令执行时,需要一个 makefile 文件,以告诉 make 命令如何去编译和链接程序 25 | - dd:读写数据到文件和设备中的工具 26 | - 硬件模拟器:qemu -- qemu 可模拟多种 CPU 硬件环境,本实验中,用于模拟一台 intel x86-32 的计算机系统。类似的工具还有 BOCHS, SkyEye 等 27 | - markdown 文本格式的编写和阅读工具(比如阅读 ucore_docs) 28 | - 编写工具 haroopad 29 | - 阅读工具 gitbook 30 | 31 | # 上述工具的使用方法在线信息 32 | 33 | - apt-get 34 | - http://wiki.ubuntu.org.cn/Apt-get%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97 35 | - git github 36 | - http://www.cnblogs.com/cspku/articles/Git_cmds.html 37 | - http://www.worldhello.net/gotgithub/index.html 38 | - diff patch 39 | - http://www.ibm.com/developerworks/cn/linux/l-diffp/index.html 40 | - http://www.cnblogs.com/itech/archive/2009/08/19/1549729.html 41 | - gcc 42 | - http://wiki.ubuntu.org.cn/Gcchowto 43 | - http://wiki.ubuntu.org.cn/Compiling_Cpp 44 | - http://wiki.ubuntu.org.cn/C_Cpp_IDE 45 | - http://wiki.ubuntu.org.cn/C%E8%AF%AD%E8%A8%80%E7%AE%80%E8%A6%81%E8%AF%AD%E6%B3%95%E6%8C%87%E5%8D%97 46 | - gdb 47 | - http://wiki.ubuntu.org.cn/%E7%94%A8GDB%E8%B0%83%E8%AF%95%E7%A8%8B%E5%BA%8F 48 | - make & makefile 49 | - http://wiki.ubuntu.com.cn/index.php?title=%E8%B7%9F%E6%88%91%E4%B8%80%E8%B5%B7%E5%86%99Makefile&variant=zh-cn 50 | - http://blog.csdn.net/a_ran/article/details/43937041 51 | - shell 52 | - http://wiki.ubuntu.org.cn/Shell%E7%BC%96%E7%A8%8B%E5%9F%BA%E7%A1%80 53 | - http://wiki.ubuntu.org.cn/%E9%AB%98%E7%BA%A7Bash%E8%84%9A%E6%9C%AC%E7%BC%96%E7%A8%8B%E6%8C%87%E5%8D%97 54 | - understand 55 | - http://blog.csdn.net/qwang24/article/details/4064975 56 | - vim 57 | - http://www.httpy.com/html/wangluobiancheng/Perljiaocheng/2014/0613/93894.html 58 | - http://wenku.baidu.com/view/4b004dd5360cba1aa811da77.html 59 | - meld 60 | - https://linuxtoy.org/archives/meld-2.html 61 | - qemu 62 | - http://wenku.baidu.com/view/04c0116aa45177232f60a2eb.html 63 | - Eclipse-CDT 64 | - http://blog.csdn.net/anzhu_111/article/details/5946634 65 | - haroopad 66 | - http://pad.haroopress.com/ 67 | - gitbook 68 | - https://github.com/GitbookIO/gitbook https://www.gitbook.com/ 69 | -------------------------------------------------------------------------------- /lab0_figs/image001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab0_figs/image001.png -------------------------------------------------------------------------------- /lab0_figs/image002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab0_figs/image002.png -------------------------------------------------------------------------------- /lab0_figs/image003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab0_figs/image003.png -------------------------------------------------------------------------------- /lab0_figs/image004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab0_figs/image004.png -------------------------------------------------------------------------------- /lab0_figs/image005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab0_figs/image005.png -------------------------------------------------------------------------------- /lab0_figs/image006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab0_figs/image006.png -------------------------------------------------------------------------------- /lab0_figs/image007.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab0_figs/image007.png -------------------------------------------------------------------------------- /lab0_figs/image008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab0_figs/image008.png -------------------------------------------------------------------------------- /lab1.md: -------------------------------------------------------------------------------- 1 | # 实验一:系统软件启动过程 2 | -------------------------------------------------------------------------------- /lab1/lab1_1_goals.md: -------------------------------------------------------------------------------- 1 | ## 实验目的: 2 | 3 | 操作系统是一个软件,也需要通过某种机制加载并运行它。在这里我们将通过另外一个更加简单的软件-bootloader 来完成这些工作。为此,我们需要完成一个能够切换到 x86 的保护模式并显示字符的 bootloader,为启动操作系统 ucore 做准备。lab1 提供了一个非常小的 bootloader 和 ucore OS,整个 bootloader 执行代码小于 512 个字节,这样才能放到硬盘的主引导扇区中。通过分析和实现这个 bootloader 和 ucore OS,读者可以了解到: 4 | 5 | - 计算机原理 6 | 7 | - CPU 的编址与寻址: 基于分段机制的内存管理 8 | - CPU 的中断机制 9 | - 外设:串口/并口/CGA,时钟,硬盘 10 | 11 | - Bootloader 软件 12 | 13 | - 编译运行 bootloader 的过程 14 | - 调试 bootloader 的方法 15 | - PC 启动 bootloader 的过程 16 | - ELF 执行文件的格式和加载 17 | - 外设访问:读硬盘,在 CGA 上显示字符串 18 | 19 | - ucore OS 软件 20 | - 编译运行 ucore OS 的过程 21 | - ucore OS 的启动过程 22 | - 调试 ucore OS 的方法 23 | - 函数调用关系:在汇编级了解函数调用栈的结构和处理过程 24 | - 中断管理:与软件相关的中断处理 25 | - 外设管理:时钟 26 | -------------------------------------------------------------------------------- /lab1/lab1_2_1_1_ex1.md: -------------------------------------------------------------------------------- 1 | #### 练习 1:理解通过 make 生成执行文件的过程。(要求在报告中写出对下述问题的回答) 2 | 3 | 列出本实验各练习中对应的 OS 原理的知识点,并说明本实验中的实现部分如何对应和体现了原理中的基本概念和关键知识点。 4 | 5 | 在此练习中,大家需要通过静态分析代码来了解: 6 | 7 | 1. 操作系统镜像文件 ucore.img 是如何一步一步生成的?(需要比较详细地解释 Makefile 中每一条相关命令和命令参数的含义,以及说明命令导致的结果) 8 | 2. 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么? 9 | 10 | 补充材料: 11 | 12 | 如何调试 Makefile 13 | 14 | 当执行 make 时,一般只会显示输出,不会显示 make 到底执行了哪些命令。 15 | 16 | 如想了解 make 执行了哪些命令,可以执行: 17 | 18 | $ make "V=" 19 | 20 | 要获取更多有关 make 的信息,可上网查询,并请执行 21 | 22 | $ man make 23 | -------------------------------------------------------------------------------- /lab1/lab1_2_1_2_ex2.md: -------------------------------------------------------------------------------- 1 | #### 练习 2:使用 qemu 执行并调试 lab1 中的软件。(要求在报告中简要写出练习过程) 2 | 3 | 为了熟悉使用 qemu 和 gdb 进行的调试工作,我们进行如下的小练习: 4 | 5 | 1. 从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行。 6 | 2. 在初始化位置 0x7c00 设置实地址断点,测试断点正常。 7 | 3. 从 0x7c00 开始跟踪代码运行,将单步跟踪反汇编得到的代码与 bootasm.S 和 bootblock.asm 进行比较。 8 | 4. 自己找一个 bootloader 或内核中的代码位置,设置断点并进行测试。 9 | 10 | > 提示:参考附录“启动后第一条执行的指令”,可了解更详细的解释,以及如何单步调试和查看 BIOS 代码。 11 | 12 | > 提示:查看 labcodes_answer/lab1_result/tools/lab1init 文件,用如下命令试试如何调试 bootloader 第一条指令: 13 | 14 | ``` 15 | $ cd labcodes_answer/lab1_result/ 16 | $ make lab1-mon 17 | ``` 18 | 19 | 补充材料: 20 | 我们主要通过硬件模拟器 qemu 来进行各种实验。在实验的过程中我们可能会遇上各种各样的问题,调试是必要的。qemu 支持使用 gdb 进行的强大而方便的调试。所以用好 qemu 和 gdb 是完成各种实验的基本要素。 21 | 22 | 默认的 gdb 需要进行一些额外的配置才进行 qemu 的调试任务。qemu 和 gdb 之间使用网络端口 1234 进行通讯。在打开 qemu 进行模拟之后,执行 gdb 并输入 23 | 24 | target remote localhost:1234 25 | 26 | 即可连接 qemu,此时 qemu 会进入停止状态,听从 gdb 的命令。 27 | 28 | 另外,我们可能需要 qemu 在一开始便进入等待模式,则我们不再使用 make qemu 开始系统的运行,而使用 make debug 来完成这项工作。这样 qemu 便不会在 gdb 尚未连接的时候擅自运行了。 29 | 30 | **_gdb 的地址断点_** 31 | 32 | 在 gdb 命令行中,使用 b \*[地址]便可以在指定内存地址设置断点,当 qemu 中的 cpu 执行到指定地址时,便会将控制权交给 gdb。 33 | 34 | **_关于代码的反汇编_** 35 | 36 | 有可能 gdb 无法正确获取当前 qemu 执行的汇编指令,通过如下配置可以在每次 gdb 命令行前强制反汇编当前的指令,在 gdb 命令行或配置文件中添加: 37 | 38 | define hook-stop 39 | x/i $pc 40 | end 41 | 42 | 即可 43 | 44 | **_gdb 的单步命令_** 45 | 46 | 在 gdb 中,有 next, nexti, step, stepi 等指令来单步调试程序,他们功能各不相同,区别在于单步的“跨度”上。 47 | 48 | next 单步到程序源代码的下一行,不进入函数。 49 | nexti 单步一条机器指令,不进入函数。 50 | step 单步到下一个不同的源代码行(包括进入函数)。 51 | stepi 单步一条机器指令。 52 | -------------------------------------------------------------------------------- /lab1/lab1_2_1_3_ex3.md: -------------------------------------------------------------------------------- 1 | #### 练习 3:分析 bootloader 进入保护模式的过程。(要求在报告中写出分析) 2 | 3 | BIOS 将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行 bootloader。请分析 bootloader 是如何完成从实模式进入保护模式的。 4 | 5 | 提示:需要阅读**小节“保护模式和分段机制”**和 lab1/boot/bootasm.S 源码,了解如何从实模式切换到保护模式,需要了解: 6 | 7 | - 为何开启 A20,以及如何开启 A20 8 | - 如何初始化 GDT 表 9 | - 如何使能和进入保护模式 10 | -------------------------------------------------------------------------------- /lab1/lab1_2_1_4_ex4.md: -------------------------------------------------------------------------------- 1 | #### 练习 4:分析 bootloader 加载 ELF 格式的 OS 的过程。(要求在报告中写出分析) 2 | 3 | 通过阅读 bootmain.c,了解 bootloader 如何加载 ELF 文件。通过分析源代码和通过 qemu 来运行并调试 bootloader&OS, 4 | 5 | - bootloader 如何读取硬盘扇区的? 6 | - bootloader 是如何加载 ELF 格式的 OS? 7 | 8 | 提示:可阅读“硬盘访问概述”,“ELF 执行文件格式概述”这两小节。 9 | -------------------------------------------------------------------------------- /lab1/lab1_2_1_5_ex5.md: -------------------------------------------------------------------------------- 1 | #### 练习 5:实现函数调用堆栈跟踪函数 (需要编程) 2 | 3 | 我们需要在 lab1 中完成 kdebug.c 中函数 print_stackframe 的实现,可以通过函数 print_stackframe 来跟踪函数调用堆栈中记录的返回地址。在如果能够正确实现此函数,可在 lab1 中执行 “make qemu”后,在 qemu 模拟器中得到类似如下的输出: 4 | 5 | …… 6 | ebp:0x00007b28 eip:0x00100992 args:0x00010094 0x00010094 0x00007b58 0x00100096 7 | kern/debug/kdebug.c:305: print_stackframe+22 8 | ebp:0x00007b38 eip:0x00100c79 args:0x00000000 0x00000000 0x00000000 0x00007ba8 9 | kern/debug/kmonitor.c:125: mon_backtrace+10 10 | ebp:0x00007b58 eip:0x00100096 args:0x00000000 0x00007b80 0xffff0000 0x00007b84 11 | kern/init/init.c:48: grade_backtrace2+33 12 | ebp:0x00007b78 eip:0x001000bf args:0x00000000 0xffff0000 0x00007ba4 0x00000029 13 | kern/init/init.c:53: grade_backtrace1+38 14 | ebp:0x00007b98 eip:0x001000dd args:0x00000000 0x00100000 0xffff0000 0x0000001d 15 | kern/init/init.c:58: grade_backtrace0+23 16 | ebp:0x00007bb8 eip:0x00100102 args:0x0010353c 0x00103520 0x00001308 0x00000000 17 | kern/init/init.c:63: grade_backtrace+34 18 | ebp:0x00007be8 eip:0x00100059 args:0x00000000 0x00000000 0x00000000 0x00007c53 19 | kern/init/init.c:28: kern_init+88 20 | ebp:0x00007bf8 eip:0x00007d73 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8 21 | : -- 0x00007d72 – 22 | …… 23 | 24 | 请完成实验,看看输出是否与上述显示大致一致,并解释最后一行各个数值的含义。 25 | 26 | 提示:可阅读小节“函数堆栈”,了解编译器如何建立函数调用关系的。在完成 lab1 编译后,查看 lab1/obj/bootblock.asm,了解 bootloader 源码与机器码的语句和地址等的对应关系;查看 lab1/obj/kernel.asm,了解 27 | ucore OS 源码与机器码的语句和地址等的对应关系。 28 | 29 | 要求完成函数 kern/debug/kdebug.c::print_stackframe 的实现,提交改进后源代码包(可以编译执行),并在实验报告中简要说明实现过程,并写出对上述问题的回答。 30 | 31 | 补充材料: 32 | 33 | 由于显示完整的栈结构需要解析内核文件中的调试符号,较为复杂和繁琐。代码中有一些辅助函数可以使用。例如可以通过调用 print_debuginfo 函数完成查找对应函数名并打印至屏幕的功能。具体可以参见 kdebug.c 代码中的注释。 34 | -------------------------------------------------------------------------------- /lab1/lab1_2_1_6_ex6.md: -------------------------------------------------------------------------------- 1 | #### 练习 6:完善中断初始化和处理 (需要编程) 2 | 3 | 请完成编码工作和回答如下问题: 4 | 5 | 1. 中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口? 6 | 2. 请编程完善 kern/trap/trap.c 中对中断向量表进行初始化的函数 idt_init。在 idt_init 函数中,依次对所有中断入口进行初始化。使用 mmu.h 中的 SETGATE 宏,填充 idt 数组内容。每个中断的入口由 tools/vectors.c 生成,使用 trap.c 中声明的 vectors 数组即可。 7 | 3. 请编程完善 trap.c 中的中断处理函数 trap,在对时钟中断进行处理的部分填写 trap 函数中处理时钟中断的部分,使操作系统每遇到 100 次时钟中断后,调用 print_ticks 子程序,向屏幕上打印一行文字”100 ticks”。 8 | 9 | > 【注意】除了系统调用中断(T_SYSCALL)使用陷阱门描述符且权限为用户态权限以外,其它中断均使用特权级(DPL)为0的中断门描述符,权限为内核态权限;而 ucore 的应用程序处于特权级3,需要采用` int 0x80`指令操作(这种方式称为软中断,软件中断,Tra 中断,在 lab5 会碰到)来发出系统调用请求,并要能实现从特权级3到特权级0的转换,所以系统调用中断(T_SYSCALL)所对应的中断门描述符中的特权级(DPL)需要设置为3。 10 | 11 | 要求完成问题 2 和问题 3 提出的相关函数实现,提交改进后的源代码包(可以编译执行),并在实验报告中简要说明实现过程,并写出对问题 1 的回答。完成这问题 2 和 3 要求的部分代码后,运行整个系统,可以看到大约每 1 秒会输出一次”100 ticks”,而按下的键也会在屏幕上显示。 12 | 13 | 提示:可阅读小节“中断与异常”。 14 | -------------------------------------------------------------------------------- /lab1/lab1_2_1_7_ex7.md: -------------------------------------------------------------------------------- 1 | #### 扩展练习 Challenge 1(需要编程) 2 | 3 | 扩展 proj4,增加 syscall 功能,即增加一用户态函数(可执行一特定系统调用:获得时钟计数值),当内核初始完毕后,可从内核态返回到用户态的函数,而用户态的函数又通过系统调用得到内核态的服务(通过网络查询所需信息,可找老师咨询。如果完成,且有兴趣做代替考试的实验,可找老师商量)。需写出详细的设计和分析报告。 4 | 5 | 提示: 6 | 规范一下 challenge 的流程。 7 | 8 | kern_init 调用 switch_test,该函数如下: 9 | 10 | ``` 11 | static void 12 | switch_test(void) { 13 | print_cur_status(); // print 当前 cs/ss/ds 等寄存器状态 14 | cprintf("+++ switch to user mode +++\n"); 15 | switch_to_user(); // switch to user mode 16 | print_cur_status(); 17 | cprintf("+++ switch to kernel mode +++\n"); 18 | switch_to_kernel(); // switch to kernel mode 19 | print_cur_status(); 20 | } 21 | ``` 22 | 23 | switch*to*\* 函数建议通过 中断处理的方式实现。主要要完成的代码是在 trap 里面处理 T_SWITCH_TO\* 中断,并设置好返回的状态。 24 | 25 | 在 lab1 里面完成代码以后,执行 make grade 应该能够评测结果是否正确。 26 | 27 | #### 扩展练习 Challenge 2(需要编程) 28 | 29 | 用键盘实现用户模式内核模式切换。具体目标是:“键盘输入 3 时切换到用户模式,键盘输入 0 时切换到内核模式”。 30 | 基本思路是借鉴软中断(syscall 功能)的代码,并且把 trap.c 中软中断处理的设置语句拿过来。 31 | 32 | 注意: 33 | 34 | 1.关于调试工具,不建议用 lab1_print_cur_status()来显示,要注意到寄存器的值要在中断完成后 tranentry.S 里面 iret 结束的时候才写回,所以再 trap.c 里面不好观察,建议用 print_trapframe(tf) 35 | 36 | 2.关于内联汇编,最开始调试的时候,参数容易出现错误,可能的错误代码如下 37 | 38 | ``` 39 | asm volatile ( "sub $0x8, %%esp \n" 40 | "int %0 \n" 41 | "movl %%ebp, %%esp" 42 | : ) 43 | ``` 44 | 45 | 要去掉参数 int %0 \n 这一行 46 | 47 | 3.软中断是利用了临时栈来处理的,所以有压栈和出栈的汇编语句。硬件中断本身就在内核态了,直接处理就可以了。 48 | 49 | 4. 参考答案在 mooc_os_lab 中的 mooc_os_2014 branch 中的 labcodes_answer/lab1_result 目录下 50 | -------------------------------------------------------------------------------- /lab1/lab1_2_1_exercise.md: -------------------------------------------------------------------------------- 1 | ### 练习 2 | 3 | 为了实现 lab1 的目标,lab1 提供了 6 个基本练习和 1 个扩展练习,要求完成实验报告。 4 | 5 | 对实验报告的要求: 6 | 7 | - 基于 markdown 格式来完成,以文本方式为主。 8 | - 填写各个基本练习中要求完成的报告内容 9 | - 完成实验后,请分析 ucore_lab 中提供的参考答案,并请在实验报告中说明你的实现与参考答案的区别 10 | - 列出你认为本实验中重要的知识点,以及与对应的 OS 原理中的知识点,并简要说明你对二者的含义,关系,差异等方面的理解(也可能出现实验中的知识点没有对应的原理知识点) 11 | - 列出你认为 OS 原理中很重要,但在实验中没有对应上的知识点 12 | -------------------------------------------------------------------------------- /lab1/lab1_2_labs.md: -------------------------------------------------------------------------------- 1 | ## 实验内容: 2 | 3 | lab1 中包含一个 bootloader 和一个 OS。这个 bootloader 可以切换到 X86 保护模式,能够读磁盘并加载 ELF 执行文件格式,并显示字符。而这 lab1 中的 OS 只是一个可以处理时钟中断和显示字符的幼儿园级别 OS。 4 | -------------------------------------------------------------------------------- /lab1/lab1_3_1_bios_booting.md: -------------------------------------------------------------------------------- 1 | ### BIOS 启动过程 2 | 3 | 当计算机加电后,一般不直接执行操作系统,而是执行系统初始化软件完成基本 IO 初始化和引导加载功能。简单地说,系统初始化软件就是在操作系统内核运行之前运行的一段小软件。通过这段小软件,我们可以初始化硬件设备、建立系统的内存空间映射图,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核准备好正确的环境。最终引导加载程序把操作系统内核映像加载到 RAM 中,并将系统控制权传递给它。 4 | 5 | 对于绝大多数计算机系统而言,操作系统和应用软件是存放在磁盘(硬盘/软盘)、光盘、EPROM、ROM、Flash 等可在掉电后继续保存数据的存储介质上。计算机启动后,CPU 一开始会到一个特定的地址开始执行指令,这个特定的地址存放了系统初始化软件,负责完成计算机基本的 IO 初始化,这是系统加电后运行的第一段软件代码。对于 Intel 80386 的体系结构而言,PC 机中的系统初始化软件由 BIOS \(Basic Input Output System,即基本输入/输出系统,其本质是一个固化在主板 Flash/CMOS 上的软件\)和位于软盘/硬盘引导扇区中的 OS Boot Loader(在 ucore 中的 bootasm.S 和 bootmain.c)一起组成。BIOS 实际上是被固化在计算机 ROM(只读存储器)芯片上的一个特殊的软件,为上层软件提供最底层的、最直接的硬件控制与支持。更形象地说,BIOS 就是 PC 计算机硬件与上层软件程序之间的一个"桥梁",负责访问和控制硬件。 6 | 7 | 以 Intel 80386 为例,计算机加电后,CPU 从物理地址 0xFFFFFFF0(由初始化的 CS:EIP 确定,此时 CS 和 IP 的值分别是 0xF000 和 0xFFF0\))开始执行。在 0xFFFFFFF0 这里只是存放了一条跳转指令,通过跳转指令跳到 BIOS 例行程序起始点。BIOS 做完计算机硬件自检和初始化后,会选择一个启动设备(例如软盘、硬盘、光盘等),并且读取该设备的第一扇区\(即主引导扇区或启动扇区\)到内存一个特定的地址 0x7c00 处,然后 CPU 控制权会转移到那个地址继续执行。至此 BIOS 的初始化工作做完了,进一步的工作交给了 ucore 的 bootloader。 8 | 9 | ## 补充信息 10 | 11 | Intel 的 CPU 具有很好的向后兼容性。在 16 位的 8086 CPU 时代,内存限制在 1MB 范围内,且 BIOS 的代码固化在 EPROM 中。在基于 Intel 的 8086 CPU 的 PC 机中的 EPROM 被编址在 1 M B 内存地址空间的最高 64KB 中。PC 加电后,CS 寄存器初始化为 0xF000,IP 寄存器初始化为 0xFFF0,所以 CPU 要执行的第一条指令的地址为 CS:IP=0xF000:0XFFF0(**Segment:Offset 表示**)=0xFFFF0(**Linear 表示**)。这个地址位于被固化 EPROM 中,指令是一个长跳转指令`JMP F000:E05B`。这样就开启了 BIOS 的执行过程。 12 | 13 | 到了 32 位的 80386 CPU 时代,内存空间扩大到了 4G,多了段机制和页机制,但 Intel 依然很好地保证了 80386 向后兼容 8086。地址空间的变化导致无法直接采用 8086 的启动约定。如果把 BIOS 启动固件编址在 0xF000 起始的 64KB 内存地址空间内,就会把整个物理内存地址空间隔离成不连续的两段,一段是 0xF000 以前的地址,一段是 1MB 以后的地址,这很不协调。为此,intel 采用了一个折中的方案:默认将执行 BIOS ROM 编址在 32 位内存地址空间的最高端,即位于 4GB 地址的最后一个 64KB 内。在 PC 系统开机复位时,CPU 进入实模式,并将 CS 寄存器设置成 0xF000,将它的 shadow register 的 Base 值初始化设置为 0xFFFF0000,EIP 寄存器初始化设置为 0x0000FFF0。所以机器执行的第一条指令的物理地址是 0xFFFFFFF0。80386 的 BIOS 代码也要和以前 8086 的 BIOS 代码兼容,故地址 0xFFFFFFF0 处的指令还是一条长跳转指令\`jmp F000:E05B\`。注意,这个长跳转指令会触发更新 CS 寄存器和它的 shadow register,即执行\`jmp F000 : E05B\`后,CS 将被更新成 0xF000。表面上看 CS 其实没有变化,但 CS 的 shadow register 被更新为另外一个值了,它的 Base 域被更新成 0x000F0000,此时形成的物理地址为 Base+EIP=0x000FE05B,这就是 CPU 执行的第二条指令的地址。此时这条指令的地址已经是 1M 以内了,且此地址不再位于 BIOS ROM 中,而是位于 RAM 空间中。由于 Intel 设计了一种映射机制,将内存高端的 BIOS ROM 映射到 1MB 以内的 RAM 空间里,并且可以使这一段被映射的 RAM 空间具有与 ROM 类似的只读属性。所以 PC 机启动时将开启这种映射机制,让 4GB 地址空间的最高一个 64KB 的内容等同于 1MB 地址空间的最高一个 64K 的内容,从而使得执行了长跳转指令后,其实是回到了早期的 8086 CPU 初始化控制流,保证了向下兼容。 14 | -------------------------------------------------------------------------------- /lab1/lab1_3_2_2_address_space.md: -------------------------------------------------------------------------------- 1 | #### 地址空间 2 | 3 | 分段机制涉及 5 个关键内容:逻辑地址(Logical Address,应用程序员看到的地址,在操作系统原理上称为虚拟地址,以后提到虚拟地址就是指逻辑地址)、物理地址(Physical Address, 实际的物理内存地址)、段描述符表(包含多个段描述符的“数组”)、段描述符(描述段的属性,及段描述符表这个“数组”中的“数组元素”)、段选择子(即段寄存器中的值,用于定位段描述符表中段描述符表项的索引) 4 | 5 | (1) 逻辑地址空间 6 | 从应用程序的角度看,逻辑地址空间就是应用程序员编程所用到的地址空间,比如下面的程序片段: 7 | int val=100; 8 | int \* point=&val; 9 | 10 | 其中指针变量 point 中存储的即是一个逻辑地址。在基于 80386 的计算机系统中,逻辑地址有一个 16 位的段寄存器(也称段选择子,段选择子)和一个 32 位的偏移量构成。 11 | 12 | (2) 物理地址空间 13 | 从操作系统的角度看,CPU、内存硬件(通常说的“内存条”)和各种外设是它主要管理的硬件资源而内存硬件和外设分布在物理地址空间中。物理地址空间就是一个“大数组”,CPU 通过索引(物理地址)来访问这个“大数组”中的内容。物理地址是指 CPU 提交到内存总线上用于访问计算机内存和外设的最终地址。 14 | 15 | 物理地址空间的大小取决于 CPU 实现的物理地址位数,在基于 80386 的计算机系统中,CPU 的物理地址空间为 4GB,如果计算机系统实际上有 1GB 物理内存(即我们通常说的内存条),而其他硬件设备的 IO 寄存器映射到起始物理地址为 3GB 的 256MB 大小的地址空间,则该计算机系统的物理地址空间如下所示: 16 | 17 | +------------------+ <- 0xFFFFFFFF (4GB) 18 | | 无效空间 | 19 | | | 20 | +------------------+ <- addr:3G+256M 21 | | 256MB | 22 | | IO外设地址空间 | 23 | | | 24 | +------------------+ <- 0xC0000000(3GB) 25 | | | 26 | /\/\/\/\/\/\/\/\/\/\ 27 | 28 | /\/\/\/\/\/\/\/\/\/\ 29 | | 无效空间 | 30 | +------------------+ <- 0x40000000(1GB) 31 | | | 32 | | 实际有效内存 | 33 | | | 34 | +------------------+ <- 0x00100000 (1MB) 35 | | BIOS ROM | 36 | +------------------+ <- 0x000F0000 (960KB) 37 | | 16-bit devices, | 38 | | expansion ROMs | 39 | +------------------+ <- 0x000C0000 (768KB) 40 | | VGA Display | 41 | +------------------+ <- 0x000A0000 (640KB) 42 | | | 43 | | Low Memory | 44 | | | 45 | +------------------+ <- 0x00000000 46 | 47 | 图 6 X86 计算机系统的物理地址空间 48 | 49 | (3) 线性地址空间 50 | 51 | 一台计算机只有一个物理地址空间,但在操作系统的管理下,每个程序都认为自己独占整个计算机的物理地址空间。为了让多个程序能够有效地相互隔离和使用物理地址空间,引入线性地址空间(也称虚拟地址空间)的概念。线性地址空间的大小取决于 CPU 实现的线性地址位数,在基于 80386 的计算机系统中,CPU 的线性地址空间为 4GB。线性地址空间会被映射到某一部分或整个物理地址空间,并通过索引(线性地址)来访问其中的内容。线性地址又称虚拟地址,是进行逻辑地址转换后形成的地址索引,用于寻址线性地址空间。但 CPU 未启动分页机制时,线性地址等于物理地址;当 CPU 启动分页机制时,线性地址还需经过分页地址转换形成物理地址后,CPU 才能访问内存硬件和外设。三种地址的关系如下所示: 52 | 53 | - 启动分段机制,未启动分页机制:逻辑地址--> (分段地址转换) -->线性地址==物理地址 54 | - 启动分段和分页机制:逻辑地址--> (分段地址转换) -->线性地址-->分页地址转换) -->物理地址 55 | 56 | 在操作系统的管理下,采用灵活的内存管理机制,在只有一个物理地址空间的情况下,可以存在多个线性地址空间。一个典型的线性地址空间 57 | -------------------------------------------------------------------------------- /lab1/lab1_3_2_3_dist_accessing.md: -------------------------------------------------------------------------------- 1 | #### 硬盘访问概述 2 | 3 | bootloader 让 CPU 进入保护模式后,下一步的工作就是从硬盘上加载并运行 OS。考虑到实现的简单性,bootloader 的访问硬盘都是 LBA 模式的 PIO(Program IO)方式,即所有的 IO 操作是通过 CPU 访问硬盘的 IO 地址寄存器完成。 4 | 5 | 一般主板有 2 个 IDE 通道,每个通道可以接 2 个 IDE 硬盘。访问第一个硬盘的扇区可设置 IO 地址寄存器 0x1f0-0x1f7 实现的,具体参数见下表。一般第一个 IDE 通道通过访问 IO 地址 0x1f0-0x1f7 来实现,第二个 IDE 通道通过访问 0x170-0x17f 实现。每个通道的主从盘的选择通过第 6 个 IO 偏移地址寄存器来设置。 6 | 7 | 表一 磁盘 IO 地址和对应功能 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 第6位:为1=LBA模式;0 = CHS模式 第7位和第5位必须为1 18 | 19 |
IO地址功能
0x1f0读数据,当0x1f7不为忙状态时,可以读。
0x1f2要读写的扇区数,每次读写前,你需要表明你要读写几个扇区。最小是1个扇区
0x1f3如果是LBA模式,就是LBA参数的0-7位
0x1f4如果是LBA模式,就是LBA参数的8-15位
0x1f5如果是LBA模式,就是LBA参数的16-23位
0x1f6第0~3位:如果是LBA模式就是24-27位 第4位:为0主盘;为1从盘
0x1f7状态和命令寄存器。操作时先给命令,再读取,如果不是忙状态就从0x1f0端口读数据
20 | 21 | 当前 硬盘数据是储存到硬盘扇区中,一个扇区大小为 512 字节。读一个扇区的流程(可参看 boot/bootmain.c 中的 readsect 函数实现)大致如下: 22 | 23 | 1. 等待磁盘准备好 24 | 2. 发出读取扇区的命令 25 | 3. 等待磁盘准备好 26 | 4. 把磁盘扇区数据读到指定内存 27 | -------------------------------------------------------------------------------- /lab1/lab1_3_2_4_elf.md: -------------------------------------------------------------------------------- 1 | #### ELF 文件格式概述 2 | 3 | ELF(Executable and linking format)文件格式是 Linux 系统下的一种常用目标文件(object file)格式,有三种主要类型: 4 | 5 | - 用于执行的可执行文件(executable file),用于提供程序的进程映像,加载的内存执行。 这也是本实验的 OS 文件类型。 6 | - 用于连接的可重定位文件(relocatable file),可与其它目标文件一起创建可执行文件和共享目标文件。 7 | - 共享目标文件(shared object file),连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。 8 | 9 | 这里只分析与本实验相关的 ELF 可执行文件类型。ELF header 在文件开始处描述了整个文件的组织。ELF 的文件头包含整个执行文件的控制结构,其定义在 elf.h 中: 10 | 11 | ``` 12 | struct elfhdr { 13 | uint magic; // must equal ELF_MAGIC 14 | uchar elf[12]; 15 | ushort type; 16 | ushort machine; 17 | uint version; 18 | uint entry; // 程序入口的虚拟地址 19 | uint phoff; // program header 表的位置偏移 20 | uint shoff; 21 | uint flags; 22 | ushort ehsize; 23 | ushort phentsize; 24 | ushort phnum; //program header表中的入口数目 25 | ushort shentsize; 26 | ushort shnum; 27 | ushort shstrndx; 28 | }; 29 | ``` 30 | 31 | program header 描述与程序执行直接相关的目标文件结构信息,用来在文件中定位各个段的映像,同时包含其他一些用来为程序创建进程映像所必需的信息。可执行文件的程序头部是一个 program header 结构的数组, 每个结构描述了一个段或者系统准备程序执行所必需的其它信息。目标文件的 “段” 包含一个或者多个 “节区”(section) ,也就是“段内容(Segment Contents)” 。程序头部仅对于可执行文件和共享目标文件有意义。可执行目标文件在 ELF 头部的 e_phentsize 和 e_phnum 成员中给出其自身程序头部的大小。程序头部的数据结构如下表所示: 32 | 33 | ``` 34 | struct proghdr { 35 | uint type; // 段类型 36 | uint offset; // 段相对文件头的偏移值 37 | uint va; // 段的第一个字节将被放到内存中的虚拟地址 38 | uint pa; 39 | uint filesz; 40 | uint memsz; // 段在内存映像中占用的字节数 41 | uint flags; 42 | uint align; 43 | }; 44 | ``` 45 | 46 | 根据 elfhdr 和 proghdr 的结构描述,bootloader 就可以完成对 ELF 格式的 ucore 操作系统的加载过程(参见 boot/bootmain.c 中的 bootmain 函数)。 47 | 48 | **_[补充材料]_** 49 | 50 | Link addr& Load addr 51 | 52 | Link Address 是指编译器指定代码和数据所需要放置的内存地址,由链接器配置。Load Address 是指程序被实际加载到内存的位置(由程序加载器 ld 配置)。一般由可执行文件结构信息和加载器可保证这两个地址相同。Link Addr 和 LoadAddr 不同会导致: 53 | 54 | - 直接跳转位置错误 55 | - 直接内存访问(只读数据区或 bss 等直接地址访问)错误 56 | - 堆和栈等的使用不受影响,但是可能会覆盖程序、数据区域 57 | 注意:也存在 Link 地址和 Load 地址不一样的情况(例如:动态链接库)。 58 | -------------------------------------------------------------------------------- /lab1/lab1_3_2_bootloader.md: -------------------------------------------------------------------------------- 1 | ### bootloader 启动过程 2 | 3 | BIOS 将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行 bootloader。bootloader 完成的工作包括: 4 | 5 | - 切换到保护模式,启用分段机制 6 | - 读磁盘中 ELF 执行文件格式的 ucore 操作系统到内存 7 | - 显示字符串信息 8 | - 把控制权交给 ucore 操作系统 9 | 10 | 对应其工作的实现文件在 lab1 中的 boot 目录下的三个文件 asm.h、bootasm.S 和 bootmain.c。下面从原理上介绍完成上述工作的计算机系统硬件和软件背景知识。 11 | -------------------------------------------------------------------------------- /lab1/lab1_3_3_1_function_stack.md: -------------------------------------------------------------------------------- 1 | #### 函数堆栈 2 | 3 | 栈是一个很重要的编程概念(编译课和程序设计课都讲过相关内容),与编译器和编程语言有紧密的联系。理解调用栈最重要的两点是:栈的结构,EBP 寄存器的作用。一个函数调用动作可分解为:零到多个 PUSH 指令(用于参数入栈),一个 CALL 指令。CALL 指令内部其实还暗含了一个将返回地址(即 CALL 指令下一条指令的地址)压栈的动作(由硬件完成)。几乎所有本地编译器都会在每个函数体之前插入类似如下的汇编指令: 4 | 5 | ``` 6 | pushl %ebp 7 | movl %esp , %ebp 8 | ``` 9 | 10 | 这样在程序执行到一个函数的实际指令前,已经有以下数据顺序入栈:参数、返回地址、ebp 寄存器。由此得到类似如下的栈结构(参数入栈顺序跟调用方式有关,这里以 C 语言默认的 CDECL 为例): 11 | 12 | ``` 13 | +| 栈底方向 | 高位地址 14 | | ... | 15 | | ... | 16 | | 参数3 | 17 | | 参数2 | 18 | | 参数1 | 19 | | 返回地址 | 20 | | 上一层[ebp] | <-------- [ebp] 21 | | 局部变量 | 低位地址 22 | ``` 23 | 24 | 图 7 函数调用栈结构 25 | 26 | 这两条汇编指令的含义是:首先将 ebp 寄存器入栈,然后将栈顶指针 esp 赋值给 ebp。“mov ebp esp”这条指令表面上看是用 esp 覆盖 ebp 原来的值,其实不然。因为给 ebp 赋值之前,原 ebp 值已经被压栈(位于栈顶),而新的 ebp 又恰恰指向栈顶。此时 ebp 寄存器就已经处于一个非常重要的地位,该寄存器中存储着栈中的一个地址(原 ebp 入栈后的栈顶),从该地址为基准,向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值,而该地址处又存储着上一层函数调用时的 ebp 值。 27 | 28 | 一般而言,ss:[ebp+4]处为返回地址,ss:[ebp+8]处为第一个参数值(最后一个入栈的参数值,此处假设其占用 4 字节内存),ss:[ebp-4]处为第一个局部变量,ss:[ebp]处为上一层 ebp 值。由于 ebp 中的地址处总是“上一层函数调用时的 ebp 值”,而在每一层函数调用中,都能通过当时的 ebp 值“向上(栈底方向)”能获取返回地址、参数值,“向下(栈顶方向)”能获取函数局部变量值。如此形成递归,直至到达栈底。这就是函数调用栈。 29 | 30 | 提示:练习 5 的正确实现取决于对这一小节的正确理解和掌握。 31 | -------------------------------------------------------------------------------- /lab1/lab1_3_3_3_lab1_interrupt.md: -------------------------------------------------------------------------------- 1 | #### lab1 中对中断的处理实现 2 | 3 | (1) 外设基本初始化设置 4 | 5 | Lab1 实现了中断初始化和对键盘、串口、时钟外设进行中断处理。串口的初始化函数 serial_init(位于/kern/driver/console.c)中涉及中断初始化工作的很简单: 6 | 7 | ``` 8 | ...... 9 | // 使能串口1接收字符后产生中断 10 | outb(COM1 + COM_IER, COM_IER_RDI); 11 | ...... 12 | // 通过中断控制器使能串口1中断 13 | pic_enable(IRQ_COM1); 14 | ``` 15 | 16 | 键盘的初始化函数 kbd_init(位于 kern/driver/console.c 中)完成了对键盘的中断初始化工作,具体操作更加简单: 17 | 18 | ``` 19 | ...... 20 | // 通过中断控制器使能键盘输入中断 21 | pic_enable(IRQ_KBD); 22 | ``` 23 | 24 | 时钟是一种有着特殊作用的外设,其作用并不仅仅是计时。在后续章节中将讲到,正是由于有了规律的时钟中断,才使得无论当前 CPU 运行在哪里,操作系统都可以在预先确定的时间点上获得 CPU 控制权。这样当一个应用程序运行了一定时间后,操作系统会通过时钟中断获得 CPU 控制权,并可把 CPU 资源让给更需要 CPU 的其他应用程序。时钟的初始化函数 clock_init(位于 kern/driver/clock.c 中)完成了对时钟控制器 8253 的初始化: 25 | 26 | ``` 27 | ...... 28 | //设置时钟每秒中断100次 29 | outb(IO_TIMER1, TIMER_DIV(100) % 256); 30 | outb(IO_TIMER1, TIMER_DIV(100) / 256); 31 | // 通过中断控制器使能时钟中断 32 | pic_enable(IRQ_TIMER); 33 | ``` 34 | 35 | (2) 中断初始化设置 36 | 37 | 操作系统如果要正确处理各种不同的中断事件,就需要安排应该由哪个中断服务例程负责处理特定的中断事件。系统将所有的中断事件统一进行了编号(0 ~ 255),这个编号称为中断向量。以 ucore 为例,操作系统内核启动以后,会通过 idt_init 函数初始化 idt 表 (参见 trap.c),而其中 vectors 中存储了中断处理程序的入口地址。vectors 定义在 vector.S 文件中,通过一个工具程序 vector.c 生成。其中仅有 System call 中断的权限为用户权限 (DPL_USER),即仅能够使用 int 0x80 指令。此外还有对 tickslock 的初始化,该锁用于处理时钟中断。 38 | 39 | vector.S 文件通过 vectors.c 自动生成,其中定义了每个中断的入口程序和入口地址 (保存在 vectors 数组中)。其中,中断可以分成两类:一类是压入错误编码的 (error code),另一类不压入错误编码。对于第二类, vector.S 自动压入一个 0。此外,还会压入相应中断的中断号。在压入两个必要的参数之后,中断处理函数跳转到统一的入口 alltraps 处。 40 | 41 | (3) 中断的处理过程 42 | 43 | trap 函数(定义在 trap.c 中)是对中断进行处理的过程,所有的中断在经过中断入口函数\_\_alltraps 预处理后 (定义在 trapasm.S 中) ,都会跳转到这里。在处理过程中,根据不同的中断类型,进行相应的处理。在相应的处理过程结束以后,trap 将会返回,被中断的程序会继续运行。整个中断处理流程大致如下: 44 | 45 | 46 | 47 | 48 | 98 | 99 | 100 | 101 | 112 | 113 | 120 | 121 | 122 | 123 | 124 |
trapasm.Strap.c
49 | 1)产生中断后,CPU 跳转到相应的中断处理入口 (vectors),并在桟中压入相应的 error_code(是否存在与异常号相关) 以及 trap_no,然后跳转到 alltraps 函数入口: 50 |
51 | 注意:此处的跳转是 jmp 过程 52 | 53 | 54 | 55 | 56 | 57 | 58 |
(high)...
产生中断时的 eip →eip
error_code
esp →trap_no
(low)...
59 |
60 | 在栈中保存当前被打断程序的 trapframe 结构(参见过程trapasm.S)。设置 kernel (内核) 的数据段寄存器,最后压入 esp,作为 trap 函数参数(struct trapframe * tf) 并跳转到中断处理函数 trap 处: 61 | 62 | 63 | 80 | 90 |
64 | Struct trapframe
65 | {
66 | uint edi;
67 | uint esi;
68 | uint ebp;
69 | …
70 | ushort es;
71 | ushort padding1;
72 | ushort ds;
73 | ushort padding2;
74 | uint trapno;
75 | uint err;
76 | uint eip;
77 | ...
78 | } 79 |
81 | 观察 trapframe 结构与中断产生过程的压桟顺序。
82 | 需要明确 pushal 指令都保存了哪些寄存器,按照什么顺序?
83 |
84 |
85 |
86 | ← trap_no
87 | ← trap_error
88 | ← 产生中断处的 eip
89 |
91 |
92 | 注意:此时的跳转是 call 调用,会压入返回地址 eip,注意区分此处eip与trapframe中eip: 93 |
94 | trapframe的结构为: 95 |
96 | 进入 trap 函数,对中断进行相应的处理: 97 |
102 | 2)详细的中断分类以及处理流程如下: 103 |
104 | 根据中断号对不同的中断进行处理。其中,若中断号是IRQ_OFFSET + IRQ_TIMER 为时钟中断,则把ticks 将增加一。 105 |
106 | 若中断号是IRQ_OFFSET + IRQ_COM1 为串口中断,则显示收到的字符。 107 |
108 | 若中断号是IRQ_OFFSET + IRQ_KBD 为键盘中断,则显示收到的字符。 109 |
110 | 若为其他中断且产生在内核状态,则挂起系统; 111 |
114 | 3)结束 trap 函数的执行后,通过 ret 指令返回到 alltraps 执行过程。 115 |
116 | 从栈中恢复所有寄存器的值。 117 |
118 | 调整 esp 的值:跳过栈中的 trap_no 与 error_code,使esp指向中断返回 eip,通过 iret 调用恢复 cs、eflag以及 eip,继续执行。 119 |
125 | 126 | 图 13 ucore 中断处理流程 127 | 128 | 至此,对整个 lab1 中的主要部分的背景知识和实现进行了阐述。请大家能够根据前面的练习要求完成所有的练习。 129 | -------------------------------------------------------------------------------- /lab1/lab1_3_3_booting_os.md: -------------------------------------------------------------------------------- 1 | ### 操作系统启动过程 2 | 3 | 当 bootloader 通过读取硬盘扇区把 ucore 在系统加载到内存后,就转跳到 ucore 操作系统在内存中的入口位置(kern/init.c 中的 kern_init 函数的起始地址),这样 ucore 就接管了整个控制权。当前的 ucore 功能很简单,只完成基本的内存管理和外设中断管理。ucore 主要完成的工作包括: 4 | 5 | - 初始化终端; 6 | - 显示字符串; 7 | - 显示堆栈中的多层函数调用关系; 8 | - 切换到保护模式,启用分段机制; 9 | - 初始化中断控制器,设置中断描述符表,初始化时钟中断,使能整个系统的中断机制; 10 | - 执行 while(1)死循环。 11 | 12 | 以后的实验中会大量涉及各个函数直接的调用关系,以及由于中断处理导致的异步现象,可能对大家实现操作系统和改正其中的错误有很大影响。而理解好函数调用关系的建立机制和中断处理机制,对后续实验会有很大帮助。下面就练习 5 涉及的函数栈调用关系和练习 6 中的中断机制的建立进行阐述。 13 | -------------------------------------------------------------------------------- /lab1/lab1_3_booting.md: -------------------------------------------------------------------------------- 1 | ## 从机器启动到操作系统运行的过程 2 | -------------------------------------------------------------------------------- /lab1/lab1_4_lab_requirement.md: -------------------------------------------------------------------------------- 1 | ## 实验报告要求 2 | 3 | 从 git server 网站上取得 ucore_lab 后,进入目录 labcodes/lab1,完成实验要求的各个练习。在实验报告中回答所有练习中提出的问题。 4 | 在目录 labcodes/lab1 下存放实验报告,实验报告文档命名为 lab1.md,使用**markdown**格式。 5 | 对于 lab1 中编程任务,完成编写之后,再通过 git push 命令把代码同步回 git server 网站。最后请一定提前或按时提交到 git server 网站。 6 | 7 | 注意有“LAB1”的注释,代码中所有需要完成的地方(challenge 除外)都有“LAB1”和“YOUR CODE”的注释,请在提交时特别注意保持注释,并将“YOUR CODE”替换为自己的学号,并且将所有标有对应注释的部分填上正确的代码。 8 | -------------------------------------------------------------------------------- /lab1/lab1_5_appendix.md: -------------------------------------------------------------------------------- 1 | ## 附录“启动后第一条执行的指令” 2 | 3 | ### intel 资料的说明 4 | 5 | 【参考 IA-32 Intel Architecture Software Developer’s Manual Volume 3: System Programming Guide Section 9.1.4】 6 | 7 | 9.1.4 First Instruction Executed 8 | 9 | The first instruction that is fetched and executed following a hardware reset is located at physical address FFFFFFF0H. This address is 16 bytes below the processor’s uppermost physical address. The EPROM containing the softwareinitialization code must be located at this address. 10 | 11 | The address FFFFFFF0H is beyond the 1-MByte addressable range of the processor while in real-address mode. The processor is initialized to this starting address as follows. The CS register has two parts: the visible segment selector part and the hidden base address part. In real-address mode, the base address is normally formed by shifting the 16-bit segment selector value 4 bits to the left to produce a 20-bit base address. However, during a hardware reset, the segment selector in the CS register is loaded with F000H and the base address is loaded with FFFF0000H. The starting address is thus formed by adding the base address to the value in the EIP register (that is, FFFF0000 + FFF0H = FFFFFFF0H). 12 | 13 | The first time the CS register is loaded with a new value after a hardware reset, the processor will follow the normal rule for address translation in real-address mode (that is, [CS base address = CS segment selector * 16]). To insure that the base address in the CS register remains unchanged until the EPROM based softwareinitialization code is completed, the code must not contain a far jump or far call or allow an interrupt to occur (which would cause the CS selector value to be changed). 14 | 15 | ### 单步调试和查看 BIOS 代码 16 | 17 | 如果你是想看 BIOS 的汇编,可试试如下方法: 18 | 练习 2 可以单步跟踪,方法如下: 19 | 20 | 1 修改 lab1/tools/gdbinit, 21 | 22 | ``` 23 | set architecture i8086 24 | target remote :1234 25 | ``` 26 | 27 | 2 在 lab1 目录下,执行 28 | 29 | ``` 30 | make debug 31 | ``` 32 | 33 | 这时 gdb 停在 BIOS 的第一条指令处: 34 | 35 | ``` 36 | 0xffff0: ljmp $0xf000,$0xe05b 37 | ``` 38 | 39 | 3 在看到 gdb 的调试界面(gdb)后,执行如下命令,就可以看到 BIOS 在执行了 40 | 41 | ``` 42 | si 43 | si 44 | ... 45 | ``` 46 | 47 | 4 此时的`CS=0xf000, EIP=0xfff0`,如果想看 BIOS 的代码 48 | 49 | ``` 50 | x /2i 0xffff0 51 | ``` 52 | 53 | 应该可以看到 54 | 55 | ``` 56 | 0xffff0: ljmp $0xf000,$0xe05b 57 | 0xffff5: xor %dh,0x322f 58 | ``` 59 | 60 | 进一步可以执行 61 | 62 | ``` 63 | x /10i 0xfe05b 64 | ``` 65 | 66 | 可以看到后续的 BIOS 代码。 67 | -------------------------------------------------------------------------------- /lab1/lab1_appendix_a20.md: -------------------------------------------------------------------------------- 1 | ## 附录“关于 A20 Gate” 2 | 3 | 【参考“关于 A20 Gate” http://hengch.blog.163.com/blog/static/107800672009013104623747/ 】 4 | 5 | 【参考“百度文库 激活 A20 地址线详解” http://wenku.baidu.com/view/d6efe68fcc22bcd126ff0c00.html】 6 | 7 | Intel 早期的 8086 CPU 提供了 20 根地址线,可寻址空间范围即 0~2^20(00000H~FFFFFH)的 1MB 内存空间。但 8086 的数据处理位宽位 16 位,无法直接寻址 1MB 内存空间,所以 8086 提供了段地址加偏移地址的地址转换机制。PC 机的寻址结构是 segment:offset,segment 和 offset 都是 16 位的寄存器,最大值是 0ffffh,换算成物理地址的计算方法是把 segment 左移 4 位,再加上 offset,所以 segment:offset 所能表达的寻址空间最大应为 0ffff0h + 0ffffh = 10ffefh(前面的 0ffffh 是 segment=0ffffh 并向左移动 4 位的结果,后面的 0ffffh 是可能的最大 offset),这个计算出的 10ffefh 是多大呢?大约是 1088KB,就是说,segment:offset 的地址表示能力,超过了 20 位地址线的物理寻址能力。所以当寻址到超过 1MB 的内存时,会发生“回卷”(不会发生异常)。但下一代的基于 Intel 80286 CPU 的 PC AT 计算机系统提供了 24 根地址线,这样 CPU 的寻址范围变为 2^24=16M,同时也提供了保护模式,可以访问到 1MB 以上的内存了,此时如果遇到“寻址超过 1MB”的情况,系统不会再“回卷”了,这就造成了向下不兼容。为了保持完全的向下兼容性,IBM 决定在 PC AT 计算机系统上加个硬件逻辑,来模仿以上的回绕特征,于是出现了 A20 Gate。他们的方法就是把 A20 地址线控制和键盘控制器的一个输出进行 AND 操作,这样来控制 A20 地址线的打开(使能)和关闭(屏蔽\禁止)。一开始时 A20 地址线控制是被屏蔽的(总为 0),直到系统软件通过一定的 IO 操作去打开它(参看 bootasm.S)。很显然,在实模式下要访问高端内存区,这个开关必须打开,在保护模式下,由于使用 32 位地址线,如果 A20 恒等于 0,那么系统只能访问奇数兆的内存,即只能访问 0--1M、2-3M、4-5M......,这样无法有效访问所有可用内存。所以在保护模式下,这个开关也必须打开。 8 | 9 | 在保护模式下,为了使能所有地址位的寻址能力,需要打开 A20 地址线控制,即需要通过向键盘控制器 8042 发送一个命令来完成。键盘控制器 8042 将会将它的的某个输出引脚的输出置高电平,作为 A20 地址线控制的输入。一旦设置成功之后,内存将不会再被绕回(memory wrapping),这样我们就可以寻址整个 286 的 16M 内存,或者是寻址 80386 级别机器的所有 4G 内存了。 10 | 11 | 键盘控制器 8042 的逻辑结构图如下所示。从软件的角度来看,如何控制 8042 呢?早期的 PC 机,控制键盘有一个单独的单片机 8042,现如今这个芯片已经给集成到了其它大片子中,但其功能和使用方法还是一样,当 PC 机刚刚出现 A20 Gate 的时候,估计为节省硬件设计成本,工程师使用这个 8042 键盘控制器来控制 A20 Gate,但 A20 Gate 与键盘管理没有一点关系。下面先从软件的角度简单介绍一下 8042 这个芯片。 12 | 13 | ![键盘控制器8042的逻辑结构图](../lab1_figs/image012.png "键盘控制器8042的逻辑结构图") 14 | 图 13 键盘控制器 8042 的逻辑结构图 15 | 16 | 8042 键盘控制器的 IO 端口是 0x60 ~ 0x6f,实际上 IBM PC/AT 使用的只有 0x60 和 0x64 两个端口(0x61、0x62 和 0x63 用于与 XT 兼容目的)。8042 通过这些端口给键盘控制器或键盘发送命令或读取状态。输出端口 P2 用于特定目的。位 0(P20 引脚)用于实现 CPU 复位操作,位 1(P21 引脚)用户控制 A20 信号线的开启与否。系统向输入缓冲(端口 0x64)写入一个字节,即发送一个键盘控制器命令。可以带一个参数。参数是通过 0x60 端口发送的。 命令的返回值也从端口 0x60 去读。8042 有 4 个寄存器: 17 | 18 | - 1 个 8-bit 长的 Input buffer;Write-Only; 19 | - 1 个 8-bit 长的 Output buffer; Read-Only; 20 | - 1 个 8-bit 长的 Status Register;Read-Only; 21 | - 1 个 8-bit 长的 Control Register;Read/Write。 22 | 23 | 有两个端口地址:60h 和 64h,有关对它们的读写操作描述如下: 24 | 25 | - 读 60h 端口,读 output buffer 26 | - 写 60h 端口,写 input buffer 27 | - 读 64h 端口,读 Status Register 28 | - 操作 Control Register,首先要向 64h 端口写一个命令(20h 为读命令,60h 为写命令),然后根据命令从 60h 端口读出 Control Register 的数据或者向 60h 端口写入 Control Register 的数据(64h 端口还可以接受许多其它的命令)。 29 | 30 | Status Register 的定义(要用 bit 0 和 bit 1): 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
bitmeaning
0output register (60h) 中有数据
1input register (60h/64h) 有数据
2系统标志(上电复位后被置为0)
3data in input register is command (1) or data (0)
41=keyboard enabled, 0=keyboard disabled (via switch)
51=transmit timeout (data transmit not complete)
61=receive timeout (data transmit not complete)
71=even parity rec'd, 0=odd parity rec'd (should be odd)
43 | 44 | 除了这些资源外,8042 还有 3 个内部端口:Input Port、Outport Port 和 Test Port,这三个端口的操作都是通过向 64h 发送命令,然后在 60h 进行读写的方式完成,其中本文要操作的 A20 Gate 被定义在 Output Port 的 bit 1 上,所以有必要对 Outport Port 的操作及端口定义做一个说明。 45 | 46 | - 读 Output Port:向 64h 发送 0d0h 命令,然后从 60h 读取 Output Port 的内容 47 | - 写 Output Port:向 64h 发送 0d1h 命令,然后向 60h 写入 Output Port 的数据 48 | - 禁止键盘操作命令:向 64h 发送 0adh 49 | - 打开键盘操作命令:向 64h 发送 0aeh 50 | 51 | 有了这些命令和知识,就可以实现操作 A20 Gate 来从实模式切换到保护模式了。 52 | 理论上讲,我们只要操作 8042 芯片的输出端口(64h)的 bit 1,就可以控制 A20 Gate,但实际上,当你准备向 8042 的输入缓冲区里写数据时,可能里面还有其它数据没有处理,所以,我们要首先禁止键盘操作,同时等待数据缓冲区中没有数据以后,才能真正地去操作 8042 打开或者关闭 A20 Gate。打开 A20 Gate 的具体步骤大致如下(参考 bootasm.S): 53 | 54 | 1. 等待 8042 Input buffer 为空; 55 | 2. 发送 Write 8042 Output Port (P2)命令到 8042 Input buffer; 56 | 3. 等待 8042 Input buffer 为空; 57 | 4. 将 8042 Output Port(P2)得到字节的第 2 位置 1,然后写入 8042 Input buffer; 58 | -------------------------------------------------------------------------------- /lab1_figs/image001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab1_figs/image001.png -------------------------------------------------------------------------------- /lab1_figs/image002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab1_figs/image002.png -------------------------------------------------------------------------------- /lab1_figs/image003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab1_figs/image003.png -------------------------------------------------------------------------------- /lab1_figs/image004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab1_figs/image004.png -------------------------------------------------------------------------------- /lab1_figs/image005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab1_figs/image005.png -------------------------------------------------------------------------------- /lab1_figs/image006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab1_figs/image006.png -------------------------------------------------------------------------------- /lab1_figs/image007.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab1_figs/image007.png -------------------------------------------------------------------------------- /lab1_figs/image008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab1_figs/image008.png -------------------------------------------------------------------------------- /lab1_figs/image009.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab1_figs/image009.png -------------------------------------------------------------------------------- /lab1_figs/image010.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab1_figs/image010.png -------------------------------------------------------------------------------- /lab1_figs/image011.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab1_figs/image011.png -------------------------------------------------------------------------------- /lab1_figs/image012.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab1_figs/image012.png -------------------------------------------------------------------------------- /lab2.md: -------------------------------------------------------------------------------- 1 | # 实验二:物理内存管理 2 | 3 | 实验一过后大家做出来了一个可以启动的系统,实验二主要涉及操作系统的物理内存管理。操作系统为了使用内存,还需高效地管理内存资源。在实验二中大家会了解并且自己动手完成一个简单的物理内存管理系统。 4 | -------------------------------------------------------------------------------- /lab2/lab2_3_1_phymemlab_goal.md: -------------------------------------------------------------------------------- 1 | ## 实验目的 2 | 3 | 实验一过后大家做出来了一个可以启动的系统,实验二主要涉及操作系统的物理内存管理。操作系统为了使用内存,还需高效地管理内存资源。在实验二中大家会了解并且自己动手完成一个简单的物理内存管理系统。 4 | 5 | - 理解基于段页式内存地址的转换机制 6 | - 理解页表的建立和使用方法 7 | - 理解物理内存的管理方法 8 | -------------------------------------------------------------------------------- /lab2/lab2_3_2_1_phymemlab_exercise.md: -------------------------------------------------------------------------------- 1 | ### 练习 2 | 3 | 为了实现 lab2 的目标,lab2 提供了 3 个基本练习和 2 个扩展练习,要求完成实验报告。 4 | 5 | 对实验报告的要求: 6 | 7 | - 基于 markdown 格式来完成,以文本方式为主 8 | - 填写各个基本练习中要求完成的报告内容 9 | - 完成实验后,请分析 ucore_lab 中提供的参考答案,并请在实验报告中说明你的实现与参考答案的区别 10 | - 列出你认为本实验中重要的知识点,以及与对应的 OS 原理中的知识点,并简要说明你对二者的含义,关系,差异等方面的理解(也可能出现实验中的知识点没有对应的原理知识点) 11 | - 列出你认为 OS 原理中很重要,但在实验中没有对应上的知识点 12 | 13 | **练习 0:填写已有实验** 14 | 15 | 本实验依赖实验 1。请把你做的实验 1 的代码填入本实验中代码中有“LAB1”的注释相应部分。提示:可采用 diff 和 patch 工具进行半自动的合并(merge),也可用一些图形化的比较/merge 工具来手动合并,比如 meld,eclipse 中的 diff/merge 工具,understand 中的 diff/merge 工具等。 16 | 17 | **练习 1:实现 first-fit 连续物理内存分配算法(需要编程)** 18 | 19 | 在实现 first fit 20 | 内存分配算法的回收函数时,要考虑地址连续的空闲块之间的合并操作。提示:在建立空闲页块链表时,需要按照空闲页块起始地址来排序,形成一个有序的链表。可能会修改 default_pmm.c 中的 default_init,default_init_memmap,default_alloc_pages, 21 | default_free_pages 等相关函数。请仔细查看和理解 default_pmm.c 中的注释。 22 | 23 | 请在实验报告中简要说明你的设计实现过程。请回答如下问题: 24 | 25 | - 你的 first fit 算法是否有进一步的改进空间 26 | 27 | **练习 2:实现寻找虚拟地址对应的页表项(需要编程)** 28 | 29 | 通过设置页表和对应的页表项,可建立虚拟内存地址和物理内存地址的对应关系。其中的 get_pte 函数是设置页表项环节中的一个重要步骤。此函数找到一个虚地址对应的二级页表项的内核虚地址,如果此二级页表项不存在,则分配一个包含此项的二级页表。本练习需要补全 get_pte 函数 30 | in 31 | kern/mm/pmm.c,实现其功能。请仔细查看和理解 get_pte 函数中的注释。get_pte 函数的调用关系图如下所示: 32 | 33 | ![](../lab2_figs/image001.png) 34 | 图 1 get_pte 函数的调用关系图 35 | 36 | 请在实验报告中简要说明你的设计实现过程。请回答如下问题: 37 | 38 | - 请描述页目录项(Page Directory Entry)和页表项(Page Table Entry)中每个组成部分的含义以及对 ucore 而言的潜在用处。 39 | - 如果 ucore 执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情? 40 | 41 | **练习 3:释放某虚地址所在的页并取消对应二级页表项的映射(需要编程)** 42 | 43 | 当释放一个包含某虚地址的物理内存页时,需要让对应此物理内存页的管理数据结构 Page 做相关的清除处理,使得此物理内存页成为空闲;另外还需把表示虚地址与物理地址对应关系的二级页表项清除。请仔细查看和理解 page_remove_pte 函数中的注释。为此,需要补全在 44 | kern/mm/pmm.c 中的 page_remove_pte 函数。page_remove_pte 函数的调用关系图如下所示: 45 | 46 | ![](../lab2_figs/image002.png) 47 | 48 | 图 2 page_remove_pte 函数的调用关系图 49 | 50 | 请在实验报告中简要说明你的设计实现过程。请回答如下问题: 51 | 52 | - 数据结构 Page 的全局变量(其实是一个数组)的每一项与页表中的页目录项和页表项有无对应关系?如果有,其对应关系是啥? 53 | - 如果希望虚拟地址与物理地址相等,则需要如何修改 lab2,完成此事? **鼓励通过编程来具体完成这个问题** 54 | 55 | **扩展练习 Challenge:buddy system(伙伴系统)分配算法(需要编程)** 56 | 57 | Buddy System 算法把系统中的可用存储空间划分为存储块(Block)来进行管理, 每个存储块的大小必须是 2 的 n 次幂(Pow(2, n)), 即 1, 2, 4, 8, 16, 32, 64, 128... 58 | 59 | - 参考[伙伴分配器的一个极简实现](http://coolshell.cn/articles/10427.html), 在 ucore 中实现 buddy system 分配算法,要求有比较充分的测试用例说明实现的正确性,需要有设计文档。 60 | 61 | **扩展练习 Challenge:任意大小的内存单元 slub 分配算法(需要编程)** 62 | 63 | slub 算法,实现两层架构的高效内存单元分配,第一层是基于页大小的内存分配,第二层是在第一层基础上实现基于任意大小的内存分配。可简化实现,能够体现其主体思想即可。 64 | 65 | - 参考[linux 的 slub 分配算法/](http://www.ibm.com/developerworks/cn/linux/l-cn-slub/),在 ucore 中实现 slub 分配算法。要求有比较充分的测试用例说明实现的正确性,需要有设计文档。 66 | 67 | > Challenges 是选做,做一个就很好了。完成 Challenge 的同学可单独提交 Challenge。 68 | -------------------------------------------------------------------------------- /lab2/lab2_3_2_2_phymemlab_files.md: -------------------------------------------------------------------------------- 1 | ### 项目组成 2 | 3 | 表 1: 实验二文件列表 4 | 5 | ``` 6 | bash 7 | |-- boot 8 | | |-- asm.h 9 | | |-- bootasm.S 10 | | \`-- bootmain.c 11 | |-- kern 12 | | |-- init 13 | | | |-- entry.S 14 | | | \`-- init.c 15 | | |-- mm 16 | | | |-- default\_pmm.c 17 | | | |-- default\_pmm.h 18 | | | |-- memlayout.h 19 | | | |-- mmu.h 20 | | | |-- pmm.c 21 | | | \`-- pmm.h 22 | | |-- sync 23 | | | \`-- sync.h 24 | | \`-- trap 25 | | |-- trap.c 26 | | |-- trapentry.S 27 | | |-- trap.h 28 | | \`-- vectors.S 29 | |-- libs 30 | | |-- atomic.h 31 | | |-- list.h 32 | \`-- tools 33 | |-- kernel.ld 34 | ``` 35 | 36 | 相对与实验一,实验二主要增加和修改的文件如上表所示。主要改动如下: 37 | 38 | - boot/bootasm.S:增加了对计算机系统中物理内存布局的探测功能; 39 | - kern/init/entry.S:根据临时段表重新暂时建立好新的段空间,为进行分页做好准备。 40 | - kern/mm/default_pmm.[ch]:提供基本的基于链表方法的物理内存管理(分配单位为页,即 4096 字节); 41 | - kern/mm/pmm.[ch]:pmm.h 定义物理内存管理类框架 struct 42 | pmm_manager,基于此通用框架可以实现不同的物理内存管理策略和算法(default_pmm.[ch] 43 | 实现了一个基于此框架的简单物理内存管理策略); 44 | pmm.c 包含了对此物理内存管理类框架的访问,以及与建立、修改、访问页表相关的各种函数实现。 45 | - kern/sync/sync.h:为确保内存管理修改相关数据时不被中断打断,提供两个功能,一个是保存 eflag 寄存器中的中断屏蔽位信息并屏蔽中断的功能,另一个是根据保存的中断屏蔽位信息来使能中断的功能;(可不用细看) 46 | - libs/list.h:定义了通用双向链表结构以及相关的查找、插入等基本操作,这是建立基于链表方法的物理内存管理(以及其他内核功能)的基础。其他有类似双向链表需求的内核功能模块可直接使用 list.h 中定义的函数。 47 | - libs/atomic.h:定义了对一个变量进行读写的原子操作,确保相关操作不被中断打断。(可不用细看) 48 | - tools/kernel.ld:ld 形成执行文件的地址所用到的链接脚本。修改了 ucore 的起始入口和代码段的起始地址。相关细节可参看附录 C。 49 | 50 | **编译方法** 51 | 52 | 编译并运行代码的命令如下: 53 | 54 | ```bash 55 | make 56 | 57 | make qemu 58 | ``` 59 | 60 | 则可以得到如下显示界面(仅供参考) 61 | 62 | ```bash 63 | chenyu$ make qemu 64 | (THU.CST) os is loading ... 65 | 66 | Special kernel symbols: 67 | entry 0xc010002c (phys) 68 | etext 0xc010537f (phys) 69 | edata 0xc01169b8 (phys) 70 | end 0xc01178dc (phys) 71 | Kernel executable memory footprint: 95KB 72 | memory managment: default_pmm_manager 73 | e820map: 74 | memory: 0009f400, [00000000, 0009f3ff], type = 1. 75 | memory: 00000c00, [0009f400, 0009ffff], type = 2. 76 | memory: 00010000, [000f0000, 000fffff], type = 2. 77 | memory: 07efd000, [00100000, 07ffcfff], type = 1. 78 | memory: 00003000, [07ffd000, 07ffffff], type = 2. 79 | memory: 00040000, [fffc0000, ffffffff], type = 2. 80 | check_alloc_page() succeeded! 81 | check_pgdir() succeeded! 82 | check_boot_pgdir() succeeded! 83 | -------------------- BEGIN -------------------- 84 | PDE(0e0) c0000000-f8000000 38000000 urw 85 | |-- PTE(38000) c0000000-f8000000 38000000 -rw 86 | PDE(001) fac00000-fb000000 00400000 -rw 87 | |-- PTE(000e0) faf00000-fafe0000 000e0000 urw 88 | |-- PTE(00001) fafeb000-fafec000 00001000 -rw 89 | --------------------- END --------------------- 90 | ++ setup timer interrupts 91 | 100 ticks 92 | 100 ticks 93 | …… 94 | ``` 95 | 96 | 通过上图,我们可以看到 ucore 在显示其 entry(入口地址)、etext(代码段截止处地址)、edata(数据段截止处地址)、和 end(ucore 截止处地址)的值后,探测出计算机系统中的物理内存的布局(e820map 下的显示内容)。接下来 ucore 会以页为最小分配单位实现一个简单的内存分配管理,完成二级页表的建立,进入分页模式,执行各种我们设置的检查,最后显示 ucore 建立好的二级页表内容,并在分页模式下响应时钟中断。 97 | -------------------------------------------------------------------------------- /lab2/lab2_3_2_phymemlab_contents.md: -------------------------------------------------------------------------------- 1 | ## 实验内容 2 | 3 | 本次实验包含三个部分。首先了解如何发现系统中的物理内存;然后了解如何建立对物理内存的初步管理,即了解连续物理内存管理;最后了解页表相关的操作,即如何建立页表来实现虚拟内存到物理内存之间的映射,对段页式内存管理机制有一个比较全面的了解。本实验里面实现的内存管理还是非常基本的,并没有涉及到对实际机器的优化,比如针对 cache 的优化等。如果大家有余力,尝试完成扩展练习。 4 | -------------------------------------------------------------------------------- /lab2/lab2_3_3_1_phymemlab_overview.md: -------------------------------------------------------------------------------- 1 | ### 实验执行流程概述 2 | 3 | 本次实验主要完成 ucore 内核对物理内存的管理工作。参考 ucore 总控函数 kern_init 的代码,可以清楚地看到在调用完成物理内存初始化的 pmm_init 函数之前和之后,是已有 lab1 实验的工作,好像没啥修改。其实不然,ucore 有两个方面的扩展。首先,bootloader 的工作有增加,在 bootloader 中,完成了对物理内存资源的探测工作(可进一步参阅附录 A 和附录 B),让 ucore 4 | kernel 在后续执行中能够基于 bootloader 探测出的物理内存情况进行物理内存管理初始化工作。其次,bootloader 不像 lab1 那样,直接调用 kern_init 函数,而是先调用位于 lab2/kern/init/entry.S 中的 kern_entry 函数。kern_entry 函数的主要任务是为执行 kern_init 建立一个良好的 C 语言运行环境(设置堆栈),而且临时建立了一个段映射关系,为之后建立分页机制的过程做一个准备(细节在 3.5 小节有进一步阐述)。完成这些工作后,才调用 kern_init 函数。 5 | 6 | kern_init 函数在完成一些输出并对 lab1 实验结果的检查后,将进入物理内存管理初始化的工作,即调用 pmm_init 函数完成物理内存的管理,这也是我们 lab2 的内容。接着是执行中断和异常相关的初始化工作,即调用 pic_init 函数和 idt_init 函数等,这些工作与 lab1 的中断异常初始化工作的内容是相同的。 7 | 8 | 为了完成物理内存管理,这里首先需要探测可用的物理内存资源;了解到物理内存位于什么地方,有多大之后,就以固定页面大小来划分整个物理内存空间,并准备以此为最小内存分配单位来管理整个物理内存,管理在内核运行过程中每页内存,设定其可用状态(free 的,used 的,还是 reserved 的),这其实就对应了我们在课本上讲到的连续内存分配概念和原理的具体实现;接着 ucore 9 | kernel 就要建立页表, 10 | 启动分页机制,让 CPU 的 MMU 把预先建立好的页表中的页表项读入到 TLB 中,根据页表项描述的虚拟页(Page)与物理页帧(Page 11 | Frame)的对应关系完成 CPU 对内存的读、写和执行操作。这一部分其实就对应了我们在课本上讲到内存映射、页表、多级页表等概念和原理的具体实现。 12 | 13 | 在代码分析上,建议根据执行流程来直接看源代码,并可采用 GDB 源码调试的手段来动态地分析 ucore 的执行过程。内存管理相关的总体控制函数是 pmm_init 函数,它完成的主要工作包括: 14 | 15 | 1. 初始化物理内存页管理器框架 pmm_manager; 16 | 2. 建立空闲的 page 链表,这样就可以分配以页(4KB)为单位的空闲内存了; 17 | 3. 检查物理内存页分配算法; 18 | 4. 为确保切换到分页机制后,代码能够正常执行,先建立一个临时二级页表; 19 | 5. 建立一一映射关系的二级页表; 20 | 6. 使能分页机制; 21 | 7. 从新设置全局段描述符表; 22 | 8. 取消临时二级页表; 23 | 9. 检查页表建立是否正确; 24 | 10. 通过自映射机制完成页表的打印输出(这部分是扩展知识) 25 | 26 | 另外,主要注意的相关代码内容包括: 27 | 28 | - boot/bootasm.S 中探测内存部分(从 probe_memory 到 finish_probe 的代码); 29 | - 管理每个物理页的 Page 数据结构(在 mm/memlayout.h 中),这个数据结构也是实现连续物理内存分配算法的关键数据结构,可通过此数据结构来完成空闲块的链接和信息存储,而基于这个数据结构的管理物理页数组起始地址就是全局变量 pages,具体初始化此数组的函数位于 page_init 函数中; 30 | - 用于实现连续物理内存分配算法的物理内存页管理器框架 pmm_manager,这个数据结构定义了实现内存分配算法的关键函数指针,而同学需要完成这些函数的具体实现; 31 | - 设定二级页表和建立页表项以完成虚实地址映射关系,这与硬件相关,且用到不少内联函数,源代码相对难懂一些。具体完成页表和页表项建立的重要函数是 boot_map_segment 函数,而 get_pte 函数是完成虚实映射关键的关键。 32 | -------------------------------------------------------------------------------- /lab2/lab2_3_3_2_search_phymem_layout.md: -------------------------------------------------------------------------------- 1 | ### 探测系统物理内存布局 2 | 3 | 当 ucore 4 | 被启动之后,最重要的事情就是知道还有多少内存可用,一般来说,获取内存大小的方法由 5 | BIOS 中断调用和直接探测两种。但 BIOS 6 | 中断调用方法是一般只能在实模式下完成,而直接探测方法必须在保护模式下完成。通过 7 | BIOS 中断获取内存布局有三种方式,都是基于 INT 15h 中断,分别为 88h e801h 8 | e820h。但是 并非在所有情况下这三种方式都能工作。在 Linux kernel 9 | 里,采用的方法是依次尝试这三 10 | 种方法。而在本实验中,我们通过 e820h 中断获取内存信息。因为 e820h 中断必须在实模式下使用,所以我们在 11 | bootloader 进入保护模式之前调用这个 BIOS 中断,并且把 e820 映 12 | 射结构保存在物理地址 0x8000 处。具体实现详见 boot/bootasm.S。有关探测系统物理内存方法和具体实现的 13 | 信息参见 lab2 试验指导的附录 A“探测物理内存分布和大小的方法”和附录 B“实现物理内存探测”。 14 | -------------------------------------------------------------------------------- /lab2/lab2_3_3_4_phymem_allocation.md: -------------------------------------------------------------------------------- 1 | ### 物理内存页分配算法实现 2 | 3 | 如果要在 ucore 中实现连续物理内存分配算法,则需要考虑的事情比较多,相对课本上的物理内存分配算法描述要复杂不少。下面介绍一下如果要实现一个 FirstFit 内存分配算法的大致流程。 4 | 5 | lab2 的第一部分是完成 first_fit 的分配算法。原理 FirstFit 内存分配算法上很简单,但要在 ucore 中实现,需要充分了解和利用 ucore 已有的数据结构和相关操作、关键的一些全局变量等。 6 | 7 | **关键数据结构和变量** 8 | 9 | first_fit 分配算法需要维护一个查找有序(地址按从小到大排列)空闲块(以页为最小单位的连续地址空间)的数据结构,而双向链表是一个很好的选择。 10 | 11 | libs/list.h 定义了可挂接任意元素的通用双向链表结构和对应的操作,所以需要了解如何使用这个文件提供的各种函数,从而可以完成对双向链表的初始化/插入/删除等。 12 | 13 | kern/mm/memlayout.h 中定义了一个 free_area_t 数据结构,包含成员结构 14 | 15 | ```c 16 | list_entry_t free_list; // the list header 空闲块双向链表的头 17 | unsigned int nr_free; // # of free pages in this free list 空闲块的总数(以页为单位) 18 | ``` 19 | 20 | 显然,我们可以通过此数据结构来完成对空闲块的管理。而 default_pmm.c 中定义的 free_area 变量就是干这个事情的。 21 | 22 | kern/mm/pmm.h 中定义了一个通用的分配算法的函数列表,用 pmm_manager 23 | 表示。其中 init 函数就是用来初始化 free_area 变量的, 24 | first_fit 分配算法可直接重用 default_init 函数的实现。init_memmap 函数需要根据现有的内存情况构建空闲块列表的初始状态。何时应该执行这个函数呢? 25 | 26 | 通过分析代码,可以知道: 27 | 28 | ``` 29 | kern_init --> pmm_init-->page_init-->init_memmap--> pmm_manager->init_memmap 30 | ``` 31 | 32 | 所以,default_init_memmap 需要根据 page_init 函数中传递过来的参数(某个连续地址的空闲块的起始页,页个数)来建立一个连续内存空闲块的双向链表。这里有一个假定 page_init 函数是按地址从小到大的顺序传来的连续内存空闲块的。链表头是 free_area.free_list,链表项是 Page 数据结构的 base-\>page_link。这样我们就依靠 Page 数据结构中的成员变量 page_link 形成了连续内存空闲块列表。 33 | 34 | **设计实现** 35 | 36 | default_init_memmap 函数将根据每个物理页帧的情况来建立空闲页链表,且空闲页块应该是根据地址高低形成一个有序链表。根据上述变量的定义,default_init_memmap 可大致实现如下: 37 | 38 | ```c 39 | default_init_memmap(struct Page *base, size_t n) { 40 | struct Page *p = base; 41 | for (; p != base + n; p ++) { 42 | p->flags = p->property = 0; 43 | set_page_ref(p, 0); 44 | } 45 | base->property = n; 46 | SetPageProperty(base); 47 | nr_free += n; 48 | list_add(&free_list, &(base->page_link)); 49 | } 50 | ``` 51 | 52 | 如果要分配一个页,那要考虑哪些呢?这里就需要考虑实现 default_alloc_pages 函数,注意参数 n 表示要分配 n 个页。另外,需要注意实现时尽量多考虑一些边界情况,这样确保软件的鲁棒性。比如 53 | 54 | ```c 55 | if (n > nr_free) { 56 | return NULL; 57 | } 58 | ``` 59 | 60 | 这样可以确保分配不会超出范围。也可加一些 61 | assert 函数,在有错误出现时,能够迅速发现。比如 n 应该大于 0,我们就可以加上 62 | 63 | ```c 64 | assert(n \> 0); 65 | ``` 66 | 67 | 这样在 n<=0 的情况下,ucore 会迅速报错。firstfit 需要从空闲链表头开始查找最小的地址,通过 list_next 找到下一个空闲块元素,通过 le2page 宏可以由链表元素获得对应的 Page 指针 p。通过 p-\>property 可以了解此空闲块的大小。如果\>=n,这就找到了!如果 nr_free) { 74 | return NULL; 75 | } 76 | struct Page *page = NULL; 77 | list_entry_t *le = &free_list; 78 | while ((le = list_next(le)) != &free_list) { 79 | struct Page *p = le2page(le, page_link); 80 | if (p->property >= n) { 81 | page = p; 82 | break; 83 | } 84 | } 85 | if (page != NULL) { 86 | list_del(&(page->page_link)); 87 | if (page->property > n) { 88 | struct Page *p = page + n; 89 | p->property = page->property - n; 90 | list_add(&free_list, &(p->page_link)); 91 | } 92 | nr_free -= n; 93 | ClearPageProperty(page); 94 | } 95 | return page; 96 | } 97 | ``` 98 | 99 | default_free_pages 函数的实现其实是 default_alloc_pages 的逆过程,不过需要考虑空闲块的合并问题。这里就不再细讲了。注意,上诉代码只是参考设计,不是完整的正确设计。更详细的说明位于 lab2/kernel/mm/default_pmm.c 的注释中。希望同学能够顺利完成本实验的第一部分。 100 | -------------------------------------------------------------------------------- /lab2/lab2_3_3_5_1_segment_and_paging.md: -------------------------------------------------------------------------------- 1 | ### 段页式管理基本概念 2 | 3 | 如图 4 在保护模式中,x86 4 | 体系结构将内存地址分成三种:逻辑地址(也称虚地址)、线性地址和物理地址。逻辑地址即是程序指令中使用的地址,物理地址是实际访问内存的地址。逻 5 | 辑地址通过段式管理的地址映射可以得到线性地址,线性地址通过页式管理的地址映射得到物理地址。 6 | 7 | ![](../lab2_figs/image004.png) 8 | 9 | 图 4 段页式管理总体框架图 10 | 11 | 段式管理前一个实验已经讨论过。在 ucore 12 | 中段式管理只起到了一个过渡作用,它将逻辑地址不加转换直接映射成线性地址,所以我们在下面的讨论中可以对这两个地址不加区分(目前的 13 | OS 实现也是不加区分的)。对段式管理有兴趣的同学可以参照《Intel® 64 and 14 | IA-32Architectures Software Developer ’s Manual – Volume 3A》3.2 节。 15 | 16 | 如图 5 所示,页式管理将线性地址分成三部分(图中的 17 | Linear Address 的 Directory 部分、 Table 部分和 Offset 部分)。ucore 18 | 的页式管理通过一个二级的页表实现。一级页表的起始物理地址存放在 cr3 19 | 寄存器中,这个地址必须是一个页对齐的地址,也就是低 12 位必须为 20 | 0。目前,ucore 用 boot_cr3(mm/pmm.c)记录这个值。 21 | 22 | ![](../lab2_figs/image006.png) 23 | 24 | 图 5 分页机制管理 25 | -------------------------------------------------------------------------------- /lab2/lab2_3_3_5_2_key_problems_in_seg_page.md: -------------------------------------------------------------------------------- 1 | ### 建立段页式管理中需要考虑的关键问题 2 | 3 | 为了实现分页机制,需要建立好虚拟内存和物理内存的页映射关系,即正确建立二级页表。此过程涉及硬件细节,不同的地址映射关系组合,相对比较复杂。总体而言,我们需要思考如下问题: 4 | 5 | - 如何在建立页表的过程中维护全局段描述符表(GDT)和页表的关系,确保 ucore 能够在各个时间段上都能正常寻址? 6 | - 对于哪些物理内存空间需要建立页映射关系? 7 | - 具体的页映射关系是什么? 8 | - 页目录表的起始地址设置在哪里? 9 | - 页表的起始地址设置在哪里,需要多大空间? 10 | - 如何设置页目录表项的内容? 11 | - 如何设置页表项的内容? 12 | -------------------------------------------------------------------------------- /lab2/lab2_3_3_5_3_setup_paging_map.md: -------------------------------------------------------------------------------- 1 | ###建立虚拟页和物理页帧的地址映射关系 2 | 3 | **建立二级页表** 4 | 5 | Intel 80386 采用了二级页表来建立线性地址与物理地址之间的映射关系。由于我们已经具有了一个物理内存页管理器 default_pmm_manager,支持动态分配和释放内存页的功能,我们就可以用它来获得所需的空闲物理页。在二级页表结构中,页目录表占 4KB 空间,可通过 alloc_page 函数获得一个空闲物理页作为页目录表(Page Directory Table,PDT)。同理,ucore 也通过这种类似方式获得一个页表(Page Table,PT)所需的 4KB 空间。 6 | 7 | 整个页目录表和页表所占空间大小取决与二级页表要管理和映射的物理页数。假定当前物理内存 0~16MB,每物理页(也称 Page Frame)大小为 4KB,则有 4096 个物理页,也就意味这有 4 个页目录项和 4096 个页表项需要设置。一个页目录项(Page Directory Entry,PDE)和一个页表项(Page Table Entry,PTE)占 4B。即使是 4 个页目录项也需要一个完整的页目录表(占 4KB)。而 4096 个页表项需要 16KB(即 4096\*4B)的空间,也就是 4 个物理页,16KB 的空间。所以对 16MB 物理页建立一一映射的 16MB 虚拟页,需要 5 个物理页,即 20KB 的空间来形成二级页表。 8 | 9 | 完成前一节所述的前两个阶段的地址映射变化后,为把 0\~KERNSIZE(明确 ucore 设定实际物理内存不能超过 KERNSIZE 值,即 0x38000000 字节,896MB,3670016 个物理页)的物理地址一一映射到页目录项和页表项的内容,其大致流程如下: 10 | 11 | 1. 指向页目录表的指针已存储在 boot_pgdir 变量中。 12 | 2. 映射 0~4MB 的首个页表已经填充好。 13 | 3. 调用 boot_map_segment 函数进一步建立一一映射关系,具体处理过程以页为单位进行设置,即 14 | 15 | ``` 16 | linear addr = phy addr + 0xC0000000 17 | ``` 18 | 19 | 设一个 32bit 线性地址 la 有一个对应的 32bit 物理地址 pa,如果在以 la 的高 10 位为索引值的页目录项中的存在位(PTE_P)为 0,表示缺少对应的页表空间,则可通过 alloc_page 获得一个空闲物理页给页表,页表起始物理地址是按 4096 字节对齐的,这样填写页目录项的内容为 20 | 21 | ``` 22 | 页目录项内容 = (页表起始物理地址 & ~0x0FFF) | PTE_U | PTE_W | PTE_P 23 | ``` 24 | 25 | 进一步对于页表中以线性地址 la 的中 10 位为索引值对应页表项的内容为 26 | 27 | ``` 28 | 页表项内容 = (pa & ~0x0FFF) | PTE_P | PTE_W 29 | ``` 30 | 31 | 其中: 32 | 33 | - PTE_U:位 3,表示用户态的软件可以读取对应地址的物理内存页内容 34 | - PTE_W:位 2,表示物理内存页内容可写 35 | - PTE_P:位 1,表示物理内存页存在 36 | 37 | ucore 的内存管理经常需要查找页表:给定一个虚拟地址,找出这个虚拟地址在二级页表中对应的项。通过更改此项的值可以方便地将虚拟地址映射到另外的页上。可完成此功能的这个函数是 get_pte 函数。它的原型为 38 | 39 | ```c 40 | pte_t *get_pte(pde_t *pgdir, uintptr_t la, bool create) 41 | ``` 42 | 43 | 下面的调用关系图可以比较好地看出 get_pte 在实现上述流程中的位置: 44 | 45 | ![](../lab2_figs/image007.png) 46 | 47 | 图 6 get_pte 调用关系图 48 | 49 | 这里涉及到三个类型 pte_t、pde_t 和 uintptr_t。通过参见 mm/mmlayout.h 和 libs/types.h,可知它们其实都是 unsigned int 类型。在此做区分,是为了分清概念。 50 | 51 | pde_t 全称为 page directory entry,也就是一级页表的表项(注意:pgdir 实际不是表项,而是一级页表本身。实际上应该新定义一个类型 pgd_t 来表示一级页表本身)。pte_t 全称为 page table entry,表示二级页表的表项。uintptr_t 表示为线性地址,由于段式管理只做直接映射,所以它也是逻辑地址。 52 | 53 | pgdir 给出页表起始地址。通过查找这个页表,我们需要给出二级页表中对应项的地址。虽然目前我们只有 boot_pgdir 一个页表,但是引入进程的概念之后每个进程都会有自己的页表。 54 | 55 | 有可能根本就没有对应的二级页表的情况,所以二级页表不必要一开始就分配,而是等到需要的时候再添加对应的二级页表。如果在查找二级页表项时,发现对应的二级页表不存在,则需要根据 create 参数的值来处理是否创建新的二级页表。如果 create 参数为 0,则 get_pte 返回 NULL;如果 create 参数不为 0,则 get_pte 需要申请一个新的物理页(通过 alloc_page 来实现,可在 mm/pmm.h 中找到它的定义),再在一级页表中添加页目录项指向表示二级页表的新物理页。注意,新申请的页必须全部设定为零,因为这个页所代表的虚拟地址都没有被映射。 56 | 57 | 当建立从一级页表到二级页表的映射时,需要注意设置控制位。这里应该设置同时设置上 PTE_U、PTE_W 和 PTE_P(定义可在 mm/mmu.h)。如果原来就有二级页表,或者新建立了页表,则只需返回对应项的地址即可。 58 | 59 | 虚拟地址只有映射上了物理页才可以正常的读写。在完成映射物理页的过程中,除了要象上面那样在页表的对应表项上填上相应的物理地址外,还要设置正确的控制位。有关 x86 中页表控制位的详细信息,请参照《Intel® 64 and IA-32 Architectures Software Developer ’s Manual – Volume 3A》4.11 节。 60 | 61 | 只有当一级二级页表的项都设置了用户写权限后,用户才能对对应的物理地址进行读写。所以我们可以在一级页表先给用户写权限,再在二级页表上面根据需要限制用户的权限,对物理页进行保护。由于一个物理页可能被映射到不同的虚拟地址上去(譬如一块内存在不同进程间共享),当这个页需要在一个地址上解除映射时,操作系统不能直接把这个页回收,而是要先看看它还有没有映射到别的虚拟地址上。这是通过查找管理该物理页的 Page 数据结构的成员变量 ref(用来表示虚拟页到物理页的映射关系的个数)来实现的,如果 ref 为 0 了,表示没有虚拟页到物理页的映射关系了,就可以把这个物理页给回收了,从而这个物理页是 free 的了,可以再被分配。page_insert 函数将物理页映射在了页表上。可参看 page_insert 函数的实现来了解 ucore 内核是如何维护这个变量的。当不需要再访问这块虚拟地址时,可以把这块物理页回收并在将来用在其他地方。取消映射由 page_remove 来做,这其实是 page_insert 的逆操作。 62 | 63 | 建立好一一映射的二级页表结构后,由于分页机制在前一节所述的前两个阶段已经开启,分页机制到此初始化完毕。当执行完毕 gdt_init 函数后,新的段页式映射已经建立好了。 64 | 65 | 在 pmm_init 函数建立完实现物理内存一一映射和页目录表自映射的页目录表和页表后,ucore 看到的内核虚拟地址空间如下图所示: 66 | 67 | ![](../lab2_figs/image008.png) 68 | 69 | 图 7 最终虚拟地址空间图 70 | -------------------------------------------------------------------------------- /lab2/lab2_3_3_5_paging.md: -------------------------------------------------------------------------------- 1 | ## 实现分页机制 2 | 3 | 在本实验中,需要重点了解和实现基于页表的页机制和以页为单位的物理内存管理方法和分配算法等。由于 ucore OS 是基于 80386 CPU 实现的,所以 CPU 在进入保护模式后,就直接使能了段机制,并使得 ucore OS 需要在段机制的基础上建立页机制。下面比较详细地介绍了实现分页机制的过程。 4 | -------------------------------------------------------------------------------- /lab2/lab2_3_3_6_self_mapping.md: -------------------------------------------------------------------------------- 1 | ### 自映射机制 2 | 3 | 这是扩展知识。 4 | 上一小节讲述了通过 boot_map_segment 函数建立了基于一一映射关系的页目录表项和页表项,这里的映射关系为: 5 | 6 | virtual addr (KERNBASE\~KERNBASE+KMEMSIZE) = physical_addr 7 | (0\~KMEMSIZE) 8 | 9 | 这样只要给出一个虚地址和一个物理地址,就可以设置相应 PDE 和 PTE,就可完成正确的映射关系。 10 | 11 | 如果我们这时需要按虚拟地址的地址顺序显示整个页目录表和页表的内容,则要查找页目录表的页目录表项内容,根据页目录表项内容找到页表的物理地址,再转换成对应的虚地址,然后访问页表的虚地址,搜索整个页表的每个页目录项。这样过程比较繁琐。 12 | 13 | 我们需要有一个简洁的方法来实现这个查找。ucore 做了一个很巧妙的地址自映射设计,把页目录表和页表放在一个连续的 4MB 虚拟地址空间中,并设置页目录表自身的虚地址<--\>物理地址映射关系。这样在已知页目录表起始虚地址的情况下,通过连续扫描这特定的 4MB 虚拟地址空间,就很容易访问每个页目录表项和页表项内容。 14 | 15 | 具体而言,ucore 是这样设计的,首先设置了一个常量(memlayout.h): 16 | 17 | VPT=0xFAC00000, 这个地址的二进制表示为: 18 | 19 | 1111 1010 1100 0000 0000 0000 0000 0000 20 | 21 | 高 10 位为 1111 1010 22 | 11,即 10 进制的 1003,中间 10 位为 0,低 12 位也为 0。在 pmm.c 中有两个全局初始化变量 23 | 24 | pte_t \* const vpt = (pte_t \*)VPT; 25 | 26 | pde_t \* const vpd = (pde_t \*)PGADDR(PDX(VPT), PDX(VPT), 0); 27 | 28 | 并在 pmm_init 函数执行了如下语句: 29 | 30 | boot_pgdir[PDX(VPT)] = PADDR(boot_pgdir) | PTE_P | PTE_W; 31 | 32 | 这些变量和语句有何特殊含义呢?其实 vpd 变量的值就是页目录表的起始虚地址 0xFAFEB000,且它的高 10 位和中 10 位是相等的,都是 10 进制的 1003。当执行了上述语句,就确保了 vpd 变量的值就是页目录表的起始虚地址,且 vpt 是页目录表中第一个目录表项指向的页表的起始虚地址。此时描述内核虚拟空间的页目录表的虚地址为 0xFAFEB000,大小为 4KB。页表的理论连续虚拟地址空间 0xFAC00000\~0xFB000000,大小为 4MB。因为这个连续地址空间的大小为 4MB,可有 1M 个 PTE,即可映射 4GB 的地址空间。 33 | 34 | 但 ucore 实际上不会用完这么多项,在 memlayout.h 中定义了常量 35 | 36 | ```c 37 | #define KERNBASE 0xC0000000 38 | #define KMEMSIZE 0x38000000 // the maximum amount of physical memory 39 | #define KERNTOP (KERNBASE + KMEMSIZE) 40 | ``` 41 | 42 | 表示 ucore 只支持 896MB 的物理内存空间,这个 896MB 只是一个设定,可以根据情况改变。则最大的内核虚地址为常量 43 | 44 | ```c 45 | #define KERNTOP (KERNBASE + KMEMSIZE)=0xF8000000 46 | ``` 47 | 48 | 所以最大内核虚地址 KERNTOP 的页目录项虚地址为 49 | 50 | ``` 51 | vpd+0xF8000000/0x400000*4=0xFAFEB000+0x3E0*4=0xFAFEBF80 52 | ``` 53 | 54 | 最大内核虚地址 KERNTOP 的页表项虚地址为: 55 | 56 | ``` 57 | vpt+0xF8000000/0x1000*4=0xFAC00000+0xF8000*4=0xFAFE0000 58 | ``` 59 | 60 | > 需要注意,页目录项和页表项是 4 字节对齐的。从上面的设置可以看出 KERNTOP/4M 后的值是 4 字节对齐的,所以这样算出来的页目录项和页表项地址的最后两位一定是 0。 61 | 62 | 在 pmm.c 中的函数 print_pgdir 就是基于 ucore 的页表自映射方式完成了对整个页目录表和页表的内容扫描和打印。注意,这里不会出现某个页表的虚地址与页目录表虚地址相同的情况。 63 | 64 | print_pgdir 函数使得 ucore 具备和 qemu 的 info pg 相同的功能,即 print pgdir 能 65 | 够从内存中,将当前页表内有效数据(PTE_P)印出来。拷贝出的格式如下所示: 66 | 67 | ``` 68 | PDE(0e0) c0000000-f8000000 38000000 urw 69 | |-- PTE(38000) c0000000-f8000000 38000000 -rw 70 | PDE(001) fac00000-fb000000 00400000 -rw 71 | |-- PTE(000e0) faf00000-fafe0000 000e0000 urw 72 | |-- PTE(00001) fafeb000-fafec000 00001000 -rw 73 | ``` 74 | 75 | 上面中的数字包括括号里的,都是十六进制。 76 | 77 | 主要的功能是从页表中将具备相同权限的 PDE 和 PTE 78 | 项目组织起来。比如上表中: 79 | 80 | ``` 81 | PDE(0e0) c0000000-f8000000 38000000 urw 82 | ``` 83 | 84 | • PDE(0e0):0e0 表示 PDE 表中相邻的 224 项具有相同的权限; 85 | • c0000000-f8000000:表示 PDE 表中,这相邻的两项所映射的线性地址的范围; 86 | • 38000000:同样表示范围,即 f8000000 减去 c0000000 的结果; 87 | • urw:PDE 表中所给出的权限位,u 表示用户可读,即 PTE_U,r 表示 PTE_P,w 表示用 88 | 户可写,即 PTE_W。 89 | 90 | ``` 91 | PDE(001) fac00000-fb000000 00400000 -rw 92 | ``` 93 | 94 | 表示仅 1 条连续的 PDE 表项具备相同的属性。相应的,在这条表项中遍历找到 2 95 | 组 PTE 表项,输出如下: 96 | 97 | ``` 98 | |-- PTE(000e0) faf00000-fafe0000 000e0000 urw 99 | |-- PTE(00001) fafeb000-fafec000 00001000 -rw 100 | ``` 101 | 102 | 注意: 103 | 104 | 1. PTE 中输出的权限是 PTE 表中的数据给出的,并没有和 PDE 105 | 表中权限做与运算。 106 | 2. 107 | 108 | 整个 print_pgdir 函数强调两点:第一是相同权限,第二是连续。 3. 109 | print_pgdir 中用到了 vpt 和 vpd 两个变量。可以参 110 | 考 VPT 和 PGADDR 两个宏。 111 | 112 | 自映射机制还可方便用户态程序访问页表。因为页表是内核维护的,用户程序很难知道自己页表的映射结构。VPT 113 | 实际上在内核地址空间的,我们可以用同样的方式实现一个用户地址空间的映射(比如 114 | pgdir[UVPT] = PADDR(pgdir) | PTE_P | PTE_U,注意,这里不能给写权限,并且 115 | pgdir 是每个进程的 page table,不是 116 | boot_pgdir),这样,用户程序就可以用和内核一样的 print_pgdir 117 | 函数遍历自己的页表结构了。 118 | -------------------------------------------------------------------------------- /lab2/lab2_3_3_phymem_manage.md: -------------------------------------------------------------------------------- 1 | ## 物理内存管理 2 | 3 | 接下来将首先对实验的执行流程做个介绍,并进一步介绍如何探测物理内存的大小与布局,如何以页为单位来管理计算机系统中的物理内存,如何设计物理内存页的分配算法,最后比较详细地分析了在 80386 的段页式硬件机制下,ucore 操作系统把段式内存管理的功能弱化,并实现以分页为主的页式内存管理的过程。 4 | -------------------------------------------------------------------------------- /lab2/lab2_3_4_phymemlab_require.md: -------------------------------------------------------------------------------- 1 | ## 实验报告要求 2 | 3 | 从 git server 网站上取得 ucore_lab 后,进入目录 labcodes/lab2,完成实验要求的各个练习。在实验报告中回答所有练习中提出的问题。 4 | 在目录 labcodes/lab2 下存放实验报告,实验报告文档命名为 lab2.md,使用**markdown**格式。 5 | 对于 lab2 中编程任务,完成编写之后,再通过 git push 命令把代码同步回 git server 网站。最后请一定提前或按时提交到 git server 网站。 6 | 7 | 注意有**_“LAB2”_**的注释,代码中所有需要完成的地方(challenge 除外)都有**_“LAB2”_**和**_“YOUR CODE”_**的注释,请在提交时特别注意保持注释,并将**_“YOUR CODE”_**替换为自己的学号,并且将所有标有对应注释的部分填上正确的代码。 8 | -------------------------------------------------------------------------------- /lab2/lab2_3_5_probe_phymem_methods.md: -------------------------------------------------------------------------------- 1 | **探测物理内存分布和大小的方法** 2 | 3 | 操作系统需要知道了解整个计算机系统中的物理内存如何分布的,哪些可用,哪些不可用。其基本方法是通过 BIOS 中断调用来帮助完成的。其中 BIOS 中断调用必须在实模式下进行,所以在 bootloader 进入保护模式前完成这部分工作相对比较合适。这些部分由 boot/bootasm.S 中从 probe_memory 处到 finish_probe 处的代码部分完成。通过 BIOS 中断获取内存可调用参数为 e820h 的 INT 4 | 15h BIOS 中断。BIOS 通过系统内存映射地址描述符(Address Range 5 | Descriptor)格式来表示系统物理内存布局,其具体表示如下: 6 | 7 | ``` 8 | Offset Size Description 9 | 00h 8字节 base address #系统内存块基地址 10 | 08h 8字节 length in bytes #系统内存大小 11 | 10h 4字节 type of address range #内存类型 12 | ``` 13 | 14 | 看下面的(Values for System Memory Map address type) 15 | 16 | ``` 17 | Values for System Memory Map address type: 18 | 01h memory, available to OS 19 | 02h reserved, not available (e.g. system ROM, memory-mapped device) 20 | 03h ACPI Reclaim Memory (usable by OS after reading ACPI tables) 21 | 04h ACPI NVS Memory (OS is required to save this memory between NVS sessions) 22 | other not defined yet -- treat as Reserved 23 | ``` 24 | 25 | INT15h BIOS 中断的详细调用参数: 26 | 27 | ``` 28 | eax:e820h:INT 15的中断调用参数; 29 | edx:534D4150h (即4个ASCII字符“SMAP”) ,这只是一个签名而已; 30 | ebx:如果是第一次调用或内存区域扫描完毕,则为0。 如果不是,则存放上次调用之后的计数值; 31 | ecx:保存地址范围描述符的内存大小,应该大于等于20字节; 32 | es:di:指向保存地址范围描述符结构的缓冲区,BIOS把信息写入这个结构的起始地址。 33 | ``` 34 | 35 | 此中断的返回值为: 36 | 37 | ``` 38 | eflags的CF位:若INT 15中断执行成功,则不置位,否则置位; 39 | 40 | eax:534D4150h ('SMAP') ; 41 | 42 | es:di:指向保存地址范围描述符的缓冲区,此时缓冲区内的数据已由BIOS填写完毕 43 | 44 | ebx:下一个地址范围描述符的计数地址 45 | 46 | ecx :返回BIOS往ES:DI处写的地址范围描述符的字节大小 47 | 48 | ah:失败时保存出错代码 49 | ``` 50 | 51 | 这样,我们通过调用 INT 15h 52 | BIOS 中断,递增 di 的值(20 的倍数),让 BIOS 帮我们查找出一个一个的内存布局 entry,并放入到一个保存地址范围描述符结构的缓冲区中,供后续的 ucore 进一步进行物理内存管理。这个缓冲区结构定义在 memlayout.h 中: 53 | 54 | ```c 55 | struct e820map { 56 | int nr_map; 57 | struct { 58 | long long addr; 59 | long long size; 60 | long type; 61 | } map[E820MAX]; 62 | }; 63 | ``` 64 | 65 | --- 66 | -------------------------------------------------------------------------------- /lab2/lab2_3_6_implement_probe_phymem.md: -------------------------------------------------------------------------------- 1 | **实现物理内存探测** 2 | 3 | 物理内存探测是在 bootasm.S 中实现的,相关代码很短,如下所示: 4 | 5 | ```x86asm 6 | probe_memory: 7 | //对0x8000处的32位单元清零,即给位于0x8000处的 8 | //struct e820map的成员变量nr_map清零 9 | movl $0, 0x8000 10 | xorl %ebx, %ebx 11 | //表示设置调用INT 15h BIOS中断后,BIOS返回的映射地址描述符的起始地址 12 | movw $0x8004, %di 13 | start_probe: 14 | movl $0xE820, %eax // INT 15的中断调用参数 15 | //设置地址范围描述符的大小为20字节,其大小等于struct e820map的成员变量map的大小 16 | movl $20, %ecx 17 | //设置edx为534D4150h (即4个ASCII字符“SMAP”),这是一个约定 18 | movl $SMAP, %edx 19 | //调用int 0x15中断,要求BIOS返回一个用地址范围描述符表示的内存段信息 20 | int $0x15 21 | //如果eflags的CF位为0,则表示还有内存段需要探测 22 | jnc cont 23 | //探测有问题,结束探测 24 | movw $12345, 0x8000 25 | jmp finish_probe 26 | cont: 27 | //设置下一个BIOS返回的映射地址描述符的起始地址 28 | addw $20, %di 29 | //递增struct e820map的成员变量nr_map 30 | incl 0x8000 31 | //如果INT0x15返回的ebx为零,表示探测结束,否则继续探测 32 | cmpl $0, %ebx 33 | jnz start_probe 34 | finish_probe: 35 | ``` 36 | 37 | 上述代码正常执行完毕后,在 0x8000 地址处保存了从 BIOS 中获得的内存分布信息,此信息按照 struct 38 | e820map 的设置来进行填充。这部分信息将在 bootloader 启动 ucore 后,由 ucore 的 page_init 函数来根据 struct 39 | e820map 的 memmap(定义了起始地址为 0x8000)来完成对整个机器中的物理内存的总体管理。 40 | -------------------------------------------------------------------------------- /lab2/rename.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | 5 | lines = [line for line in open("hehe.txt")] 6 | 7 | for line in lines: 8 | i = 0 9 | for c in line: 10 | if (c != '_' and not (c >= '0' and c <= '9')): 11 | break 12 | i+=1 13 | 14 | cmd = "mv " + line[0:i].strip() + line[i+5:].strip() + " lab2_" + line[0:i].strip() + line[i+5:].strip() 15 | print cmd 16 | os.system(cmd) 17 | continue 18 | 19 | index = line.find("_lab2_") 20 | num = line[0 : index + 1] 21 | value = line[index + 6 : ] 22 | nn = "lab2_" + num + value 23 | cmd = "mv 3_" + line.strip() + " " + nn 24 | #print cmd 25 | os.system(cmd) 26 | -------------------------------------------------------------------------------- /lab2_figs/image001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab2_figs/image001.png -------------------------------------------------------------------------------- /lab2_figs/image002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab2_figs/image002.png -------------------------------------------------------------------------------- /lab2_figs/image003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab2_figs/image003.png -------------------------------------------------------------------------------- /lab2_figs/image004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab2_figs/image004.png -------------------------------------------------------------------------------- /lab2_figs/image006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab2_figs/image006.png -------------------------------------------------------------------------------- /lab2_figs/image007.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab2_figs/image007.png -------------------------------------------------------------------------------- /lab2_figs/image008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab2_figs/image008.png -------------------------------------------------------------------------------- /lab3.md: -------------------------------------------------------------------------------- 1 | # 实验三:虚拟内存管理 2 | 3 | 做完实验二后,大家可以了解并掌握物理内存管理中的连续空间分配算法的具体实现以及如何建立二级页表。本次实验是在实验二的基础上,借助于页表机制和实验一中涉及的中断异常处理机制,完成 Page 4 | Fault 异常处理和 FIFO 页替换算法的实现。实验原理最大的区别是在设计了如何在磁盘上缓存内存页,从而能够支持虚存管理,提供一个比实际物理内存空间“更大”的虚拟内存空间给系统使用。 5 | -------------------------------------------------------------------------------- /lab3/lab3_1_goals.md: -------------------------------------------------------------------------------- 1 | ## 实验目的 2 | 3 | - 了解虚拟内存的 Page Fault 异常处理实现 4 | - 了解页替换算法在操作系统中的实现 5 | -------------------------------------------------------------------------------- /lab3/lab3_2_1_exercises.md: -------------------------------------------------------------------------------- 1 | ### 练习 2 | 3 | 对实验报告的要求: 4 | 5 | - 基于 markdown 格式来完成,以文本方式为主 6 | - 填写各个基本练习中要求完成的报告内容 7 | - 完成实验后,请分析 ucore_lab 中提供的参考答案,并请在实验报告中说明你的实现与参考答案的区别 8 | - 列出你认为本实验中重要的知识点,以及与对应的 OS 原理中的知识点,并简要说明你对二者的含义,关系,差异等方面的理解(也可能出现实验中的知识点没有对应的原理知识点) 9 | - 列出你认为 OS 原理中很重要,但在实验中没有对应上的知识点 10 | 11 | #### 练习 0:填写已有实验 12 | 13 | 本实验依赖实验 1/2。请把你做的实验 1/2 的代码填入本实验中代码中有“LAB1”,“LAB2”的注释相应部分。 14 | 15 | #### 练习 1:给未被映射的地址映射上物理页(需要编程) 16 | 17 | 完成 do_pgfault(mm/vmm.c)函数,给未被映射的地址映射上物理页。设置访问权限 18 | 的时候需要参考页面所在 VMA 19 | 的权限,同时需要注意映射物理页时需要操作内存控制 20 | 结构所指定的页表,而不是内核的页表。注意:在 LAB3 EXERCISE 21 | 1 处填写代码。执行 22 | 23 | ``` 24 | make qemu 25 | ``` 26 | 27 | 后,如果通过 check_pgfault 函数的测试后,会有“check_pgfault() 28 | succeeded!”的输出,表示练习 1 基本正确。 29 | 30 | 请在实验报告中简要说明你的设计实现过程。请回答如下问题: 31 | 32 | - 请描述页目录项(Page Directory Entry)和页表项(Page Table Entry)中组成部分对 ucore 实现页替换算法的潜在用处。 33 | - 如果 ucore 的缺页服务例程在执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情? 34 | 35 | #### 练习 2:补充完成基于 FIFO 的页面替换算法(需要编程) 36 | 37 | 完成 vmm.c 中的 do_pgfault 函数,并且在实现 FIFO 算法的 swap_fifo.c 中完成 map_swappable 和 swap_out_victim 函数。通过对 swap 的测试。注意:在 LAB3 38 | EXERCISE 2 处填写代码。执行 39 | 40 | ``` 41 | make qemu 42 | ``` 43 | 44 | 后,如果通过 check_swap 函数的测试后,会有“check_swap() 45 | succeeded!”的输出,表示练习 2 基本正确。 46 | 47 | 请在实验报告中简要说明你的设计实现过程。 48 | 49 | 请在实验报告中回答如下问题: 50 | 51 | - 如果要在 ucore 上实现"extended clock 页替换算法"请给你的设计方案,现有的 swap_manager 框架是否足以支持在 ucore 中实现此算法?如果是,请给你的设计方案。如果不是,请给出你的新的扩展和基此扩展的设计方案。并需要回答如下问题 52 | - 需要被换出的页的特征是什么? 53 | - 在 ucore 中如何判断具有这样特征的页? 54 | - 何时进行换入和换出操作? 55 | 56 | #### 扩展练习 Challenge 1:实现识别 dirty bit 的 extended clock 页替换算法(需要编程) 57 | 58 | #### 扩展练习 Challenge 2:实现不考虑实现开销和效率的 LRU 页替换算法(需要编程) 59 | 60 | challenge 部分不是必做部分。需写出有详细的设计、分析和测试的实验报告。 61 | -------------------------------------------------------------------------------- /lab3/lab3_2_lab2.md: -------------------------------------------------------------------------------- 1 | ## 实验内容 2 | 3 | 本次实验是在实验二的基础上,借助于页表机制和实验一中涉及的中断异常处理机制,完成 Page 4 | Fault 异常处理和 FIFO 页替换算法的实现,结合磁盘提供的缓存空间,从而能够支持虚存管理,提供一个比实际物理内存空间“更大”的虚拟内存空间给系统使用。这个实验与实际操作系统中的实现比较起来要简单,不过需要了解实验一和实验二的具体实现。实际操作系统系统中的虚拟内存管理设计与实现是相当复杂的,涉及到与进程管理系统、文件系统等的交叉访问。如果大家有余力,可以尝试完成扩展练习,实现 extended   clock 页替换算法。 5 | -------------------------------------------------------------------------------- /lab3/lab3_3_1_vmm_principles.md: -------------------------------------------------------------------------------- 1 | ### 基本原理概述 2 | 3 | 什么是虚拟内存?简单地说是指程序员或 CPU“看到”的内存。但有几点需要注意: 4 | 5 | 1. 虚拟内存单元不一定有实际的物理内存单元对应,即实际的物理内存单元可能不存在; 6 | 2. 如果虚拟内存单元对应有实际的物理内存单元,那二者的地址一般是不相等的; 7 | 3. 通过操作系统实现的某种内存映射可建立虚拟内存与物理内存的对应关系,使得程序员或 CPU 访问的虚拟内存地址会自动转换为一个物理内存地址。 8 | 9 | 那么这个“虚拟”的作用或意义在哪里体现呢?在操作系统中,虚拟内存其实包含多个虚拟层次,在不同的层次体现了不同的作用。首先,在有了分页机制后,程序员或 CPU“看到”的地址已经不是实际的物理地址了,这已经有一层虚拟化,我们可简称为内存地址虚拟化。有了内存地址虚拟化,我们就可以通过设置页表项来限定软件运行时的访问空间,确保软件运行不越界,完成内存访问保护的功能。 10 | 11 | 通过内存地址虚拟化,可以使得软件在没有访问某虚拟内存地址时不分配具体的物理内存,而只有在实际访问某虚拟内存地址时,操作系统再动态地分配物理内存,建立虚拟内存到物理内存的页映射关系,这种技术称为按需分页(demand paging)。把不经常访问的数据所占的内存空间临时写到硬盘上,这样可以腾出更多的空闲内存空间给经常访问的数据;当 CPU 访问到不经常访问的数据时,再把这些数据从硬盘读入到内存中,这种技术称为页换入换出(page   swap in/out)。这种内存管理技术给了程序员更大的内存“空间”,从而可以让更多的程序在内存中并发运行。 12 | -------------------------------------------------------------------------------- /lab3/lab3_3_2_labs_steps.md: -------------------------------------------------------------------------------- 1 | ### 实验执行流程概述 2 | 3 | 本次实验主要完成 ucore 内核对虚拟内存的管理工作。其总体设计思路还是比较简单,即首先完成初始化虚拟内存管理机制,即需要设置好哪些页需要放在物理内存中,哪些页不需要放在物理内存中,而是可被换出到硬盘上,并涉及完善建立页表映射、页访问异常处理操作等函数实现。然后就执行一组访存测试,看看我们建立的页表项是否能够正确完成虚实地址映射,是否正确描述了虚拟内存页在物理内存中还是在硬盘上,是否能够正确把虚拟内存页在物理内存和硬盘之间进行传递,是否正确实现了页面替换算法等。lab3 的总体执行流程如下。 4 | 5 | 首先是初始化过程。参考 ucore 总控函数 init 的代码,可以看到在调用完成虚拟内存初始化的 vmm_init 函数之前,需要首先调用 pmm_init 函数完成物理内存的管理,这也是我们 lab2 已经完成的内容。接着是执行中断和异常相关的初始化工作,即调用 pic_init 函数和 idt_init 函数等,这些工作与 lab1 的中断异常初始化工作的内容是相同的。 6 | 7 | 在调用完 idt_init 函数之后,将进一步调用三个 lab3 中才有的新函数 vmm_init、ide_init 和 swap_init。这三个函数涉及了本次实验中的两个练习。第一个函数 vmm_init 是检查我们的练习 1 是否正确实现了。为了表述不在物理内存中的“合法”虚拟页,需要有数据结构来描述这样的页,为此 ucore 建立了 mm_struct 和 vma_struct 数据结构(接下来的小节中有进一步详细描述),假定我们已经描述好了这样的“合法”虚拟页,当 ucore 访问这些“合法”虚拟页时,会由于没有虚实地址映射而产生页访问异常。如果我们正确实现了练习 1,则 do_pgfault 函数会申请一个空闲物理页,并建立好虚实映射关系,从而使得这样的“合法”虚拟页有实际的物理页帧对应。这样练习 1 就算完成了。 8 | 9 | ide_init 和 swap_init 是为练习 2 准备的。由于页面置换算法的实现存在对硬盘数据块的读写,所以 ide_init 就是完成对用于页换入换出的硬盘(简称 swap 硬盘)的初始化工作。完成 ide_init 函数后,ucore 就可以对这个 swap 硬盘进行读写操作了。swap_init 函数首先建立 swap_manager,swap_manager 是完成页面替换过程的主要功能模块,其中包含了页面置换算法的实现(具体内容可参考 5 小节)。然后会进一步调用执行 check_swap 函数在内核中分配一些页,模拟对这些页的访问,这会产生页访问异常。如果我们正确实现了练习 2,就可通过 do_pgfault 来调用 swap_map_swappable 函数来查询这些页的访问情况并间接调用实现页面置换算法的相关函数,把“不常用”的页换出到磁盘上。 10 | 11 | ucore 在实现上述技术时,需要解决三个关键问题: 12 | 13 | 1. 当程序运行中访问内存产生 page 14 | fault 异常时,如何判定这个引起异常的虚拟地址内存访问是越界、写只读页的“非法地址”访问还是由于数据被临时换出到磁盘上或还没有分配内存的“合法地址”访问? 15 | 2. 何时进行请求调页/页换入换出处理? 16 | 3. 如何在现有 ucore 的基础上实现页替换算法? 17 | 18 | 接下来将进一步分析完成 lab3 主要注意的关键问题和涉及的关键数据结构。 19 | -------------------------------------------------------------------------------- /lab3/lab3_3_3_data_structures.md: -------------------------------------------------------------------------------- 1 | ### 关键数据结构和相关函数分析 2 | 3 | 对于第一个问题的出现,在于实验二中有关内存的数据结构和相关操作都是直接针对实际存在的资源--物理内存空间的管理,没有从一般应用程序对内存的“需求”考虑,即需要有相关的数据结构和操作来体现一般应用程序对虚拟内存的“需求”。一般应用程序的对虚拟内存的“需求”与物理内存空间的“供给”没有直接的对应关系,ucore 是通过 page 4 | fault 异常处理来间接完成这二者之间的衔接。 5 | 6 | page_fault 函数不知道哪些是“合法”的虚拟页,原因是 ucore 还缺少一定的数据结构来描述这种不在物理内存中的“合法”虚拟页。为此 ucore 通过建立 mm_struct 和 vma_struct 数据结构,描述了 ucore 模拟应用程序运行所需的合法内存空间。当访问内存产生 page 7 | fault 异常时,可获得访问的内存的方式(读或写)以及具体的虚拟内存地址,这样 ucore 就可以查询此地址,看是否属于 vma_struct 数据结构中描述的合法地址范围中,如果在,则可根据具体情况进行请求调页/页换入换出处理(这就是练习 2 涉及的部分);如果不在,则报错。mm_struct 和 vma_struct 数据结构结合页表表示虚拟地址空间和物理地址空间的示意图如下所示: 8 | 9 | 图 虚拟地址空间和物理地址空间的示意图 10 | 11 | ![image](../lab3_figs/image001.png) 12 | 13 | 在 ucore 中描述应用程序对虚拟内存“需求”的数据结构是 vma_struct(定义在 vmm.h 中),以及针对 vma_struct 的函数操作。这里把一个 vma_struct 结构的变量简称为 vma 变量。vma_struct 的定义如下: 14 | 15 | ```c 16 | struct vma_struct { 17 | // the set of vma using the same PDT 18 | struct mm_struct *vm_mm; 19 | uintptr_t vm_start; // start addr of vma 20 | uintptr_t vm_end; // end addr of vma 21 | uint32_t vm_flags; // flags of vma 22 | //linear list link which sorted by start addr of vma 23 | list_entry_t list_link; 24 | }; 25 | ``` 26 | 27 | vm_start 和 vm_end 描述了一个连续地址的虚拟内存空间的起始位置和结束位置,这两个值都应该是 PGSIZE 对齐的,而且描述的是一个合理的地址空间范围(即严格确保 vm_start < vm_end 的关系);list_link 是一个双向链表,按照从小到大的顺序把一系列用 vma_struct 表示的虚拟内存空间链接起来,并且还要求这些链起来的 vma_struct 应该是不相交的,即 vma 之间的地址空间无交集;vm_flags 表示了这个虚拟内存空间的属性,目前的属性包括: 28 | 29 | ```c 30 | #define VM_READ 0x00000001 //只读 31 | #define VM_WRITE 0x00000002 //可读写 32 | #define VM_EXEC 0x00000004 //可执行 33 | ``` 34 | 35 | vm_mm 是一个指针,指向一个比 vma_struct 更高的抽象层次的数据结构 mm_struct,这里把一个 mm_struct 结构的变量简称为 mm 变量。这个数据结构表示了包含所有虚拟内存空间的共同属性,具体定义如下 36 | 37 | ```c 38 | struct mm_struct { 39 | // linear list link which sorted by start addr of vma 40 | list_entry_t mmap_list; 41 | // current accessed vma, used for speed purpose 42 | struct vma_struct *mmap_cache; 43 | pde_t *pgdir; // the PDT of these vma 44 | int map_count; // the count of these vma 45 | void *sm_priv; // the private data for swap manager 46 | }; 47 | ``` 48 | 49 | mmap_list 是双向链表头,链接了所有属于同一页目录表的虚拟内存空间,mmap_cache 是指向当前正在使用的虚拟内存空间,由于操作系统执行的“局部性”原理,当前正在用到的虚拟内存空间在接下来的操作中可能还会用到,这时就不需要查链表,而是直接使用此指针就可找到下一次要用到的虚拟内存空间。由于 mmap_cache 的引入,可使得 mm_struct 数据结构的查询加速 30% 以上。pgdir 50 | 所指向的就是 mm_struct 数据结构所维护的页表。通过访问 pgdir 可以查找某虚拟地址对应的页表项是否存在以及页表项的属性等。map_count 记录 mmap_list 里面链接的 vma_struct 的个数。sm_priv 指向用来链接记录页访问情况的链表头,这建立了 mm_struct 和后续要讲到的 swap_manager 之间的联系。 51 | 52 | 涉及 vma_struct 的操作函数也比较简单,主要包括三个: 53 | 54 | - vma_create--创建 vma 55 | - insert_vma_struct--插入一个 vma 56 | - find_vma--查询 vma。 57 | 58 | vma_create 函数根据输入参数 vm_start、vm_end、vm_flags 来创建并初始化描述一个虚拟内存空间的 vma_struct 结构变量。insert_vma_struct 函数完成把一个 vma 变量按照其空间位置[vma-\>vm\_start,vma-\>vm\_end]从小到大的顺序插入到所属的 mm 变量中的 mmap_list 双向链表中。find_vma 根据输入参数 addr 和 mm 变量,查找在 mm 变量中的 mmap_list 双向链表中某个 vma 包含此 addr,即 vma-\>vm_start<=addr end。这三个函数与后续讲到的 page fault 异常处理有紧密联系。 59 | 60 | 涉及 mm_struct 的操作函数比较简单,只有 mm_create 和 mm_destroy 两个函数,从字面意思就可以看出是是完成 mm_struct 结构的变量创建和删除。在 mm_create 中用 kmalloc 分配了一块空间,所以在 mm_destroy 中也要对应进行释放。在 ucore 运行过程中,会产生描述虚拟内存空间的 vma_struct 结构,所以在 mm_destroy 中也要进对这些 mmap_list 中的 vma 进行释放。 61 | -------------------------------------------------------------------------------- /lab3/lab3_3_vmm.md: -------------------------------------------------------------------------------- 1 | ## 虚拟内存管理 2 | -------------------------------------------------------------------------------- /lab3/lab3_4_page_fault_handler.md: -------------------------------------------------------------------------------- 1 | ## Page Fault 异常处理 2 | 3 | 实现虚存管理的一个关键是 page fault 异常处理,其过程中主要涉及到函数 -- do_pgfault 的具体实现。比如,在程序的执行过程中由于某种原因(页框不存在/写只读页等)而使 CPU 无法最终访问到相应的物理内存单元,即无法完成从虚拟地址到物理地址映射时,CPU 会产生一次页访问异常,从而需要进行相应的页访问异常的中断服务例程。这个页访问异常处理的时机被操作系统充分利用来完成虚存管理,即实现“按需调页”/“页换入换出”处理的执行时机。当相关处理完成后,页访问异常服务例程会返回到产生异常的指令处重新执行,使得应用软件可以继续正常运行下去。 4 | 5 | 具体而言,当启动分页机制以后,如果一条指令或数据的虚拟地址所对应的物理页框不在内存中或者访问的类型有错误(比如写一个只读页或用户态程序访问内核态的数据等),就会发生页访问异常。产生页访问异常的原因主要有: 6 | 7 | - 目标页帧不存在(页表项全为 0,即该线性地址与物理地址尚未建立映射或者已经撤销); 8 | - 相应的物理页帧不在内存中(页表项非空,但 Present 标志位=0,比如在 swap 分区或磁盘文件上),这在本次实验中会出现,我们将在下面介绍换页机制实现时进一步讲解如何处理; 9 | - 不满足访问权限(此时页表项 P 标志=1,但低权限的程序试图访问高权限的地址空间,或者有程序试图写只读页面). 10 | 11 | 当出现上面情况之一,那么就会产生页面 page fault(\#PF)异常。CPU 会把产生异常的线性地址存储在 CR2 中,并且把表示页访问异常类型的值(简称页访问异常错误码,errorCode)保存在中断栈中。 12 | 13 | > [提示]页访问异常错误码有 32 位。位 0 为1表示对应物理页不存在;位1为1表示写异常(比如写了只读页;位2为1表示访问权限异常(比如用户态程序访问内核空间的数据) 14 | 15 | > [提示]  CR2 是页故障线性地址寄存器,保存最后一次出现页故障的全 32 位线性地址。CR2 用于发生页异常时报告出错信息。当发生页异常时,处理器把引起页异常的线性地址保存在 CR2 中。操作系统中对应的中断服务例程可以检查 CR2 的内容,从而查出线性地址空间中的哪个页引起本次异常。 16 | 17 | 产生页访问异常后,CPU 硬件和软件都会做一些事情来应对此事。首先页访问异常也是一种异常,所以针对一般异常的硬件处理操作是必须要做的,即 CPU 在当前内核栈保存当前被打断的程序现场,即依次压入当前被打断程序使用的 EFLAGS,CS,EIP,errorCode;由于页访问异常的中断号是 0xE,CPU 把异常中断号 0xE 对应的中断服务例程的地址(vectors.S 中的标号 vector14 处)加载到 CS 和 EIP 寄存器中,开始执行中断服务例程。这时 ucore 开始处理异常中断,首先需要保存硬件没有保存的寄存器。在 vectors.S 中的标号 vector14 处先把中断号压入内核栈,然后再在 trapentry.S 中的标号\_\_alltraps 处把 DS、ES 和其他通用寄存器都压栈。自此,被打断的程序执行现场(context)被保存在内核栈中。接下来,在 trap.c 的 trap 函数开始了中断服务例程的处理流程,大致调用关系为: 18 | 19 | > trap--\> trap_dispatch--\>pgfault_handler--\>do_pgfault 20 | 21 | 下面需要具体分析一下 do_pgfault 函数。do_pgfault 的调用关系如下图所示: 22 | 23 | 图 do_pgfault 的调用关系图 24 | 25 | ![image](../lab3_figs/image002.png) 26 | 27 | 产生页访问异常后,CPU 把引起页访问异常的线性地址装到寄存器 CR2 中,并给出了出错码 errorCode,说明了页访问异常的类型。ucore OS 会把这个值保存在 struct trapframe 中 tf_err 成员变量中。而中断服务例程会调用页访问异常处理函数 do_pgfault 进行具体处理。这里的页访问异常处理是实现按需分页、页换入换出机制的关键之处。 28 | 29 | ucore 中 do_pgfault 函数是完成页访问异常处理的主要函数,它根据从 CPU 的控制寄存器 CR2 中获取的页访问异常的物理地址以及根据 errorCode 的错误类型来查找此地址是否在某个 VMA 的地址范围内以及是否满足正确的读写权限,如果在此范围内并且权限也正确,这认为这是一次合法访问,但没有建立虚实对应关系。所以需要分配一个空闲的内存页,并修改页表完成虚地址到物理地址的映射,刷新 TLB,然后调用 iret 中断,返回到产生页访问异常的指令处重新执行此指令。如果该虚地址不在某 VMA 范围内,则认为是一次非法访问。 30 | -------------------------------------------------------------------------------- /lab3/lab3_5_1_page_swapping.md: -------------------------------------------------------------------------------- 1 | ### 页替换算法 2 | 3 | 操作系统为何要进行页面置换呢?这是由于操作系统给用户态的应用程序提供了一个虚拟的“大容量”内存空间,而实际的物理内存空间又没有那么大。所以操作系统就就“瞒着”应用程序,只把应用程序中“常用”的数据和代码放在物理内存中,而不常用的数据和代码放在了硬盘这样的存储介质上。如果应用程序访问的是“常用”的数据和代码,那么操作系统已经放置在内存中了,不会出现什么问题。但当应用程序访问它认为应该在内存中的的数据或代码时,如果这些数据或代码不在内存中,则根据上一小节的介绍,会产生页访问异常。这时,操作系统必须能够应对这种页访问异常,即尽快把应用程序当前需要的数据或代码放到内存中来,然后重新执行应用程序产生异常的访存指令。如果在把硬盘中对应的数据或代码调入内存前,操作系统发现物理内存已经没有空闲空间了,这时操作系统必须把它认为“不常用”的页换出到磁盘上去,以腾出内存空闲空间给应用程序所需的数据或代码。 4 | 5 | 操作系统迟早会碰到没有内存空闲空间而必须要置换出内存中某个“不常用”的页的情况。如何判断内存中哪些是“常用”的页,哪些是“不常用”的页,把“常用”的页保持在内存中,在物理内存空闲空间不够的情况下,把“不常用”的页置换到硬盘上就是页替换算法着重考虑的问题。容易理解,一个好的页替换算法会导致页访问异常次数少,也就意味着访问硬盘的次数也少,从而使得应用程序执行的效率就高。本次实验涉及的页替换算法(包括扩展练习): 6 | 7 | - 先进先出(First In First Out, FIFO)页替换算法:该算法总是淘汰最先进入内存的页,即选择在内存中驻留时间最久的页予以淘汰。只需把一个应用程序在执行过程中已调入内存的页按先后次序链接成一个队列,队列头指向内存中驻留时间最久的页,队列尾指向最近被调入内存的页。这样需要淘汰页时,从队列头很容易查找到需要淘汰的页。FIFO 算法只是在应用程序按线性顺序访问地址空间时效果才好,否则效率不高。因为那些常被访问的页,往往在内存中也停留得最久,结果它们因变“老”而不得不被置换出去。FIFO 算法的另一个缺点是,它有一种异常现象(Belady 现象),即在增加放置页的页帧的情况下,反而使页访问异常次数增多。 8 | 9 | - 时钟(Clock)页替换算法:是 LRU 算法的一种近似实现。时钟页替换算法把各个页面组织成环形链表的形式,类似于一个钟的表面。然后把一个指针(简称当前指针)指向最老的那个页面,即最先进来的那个页面。另外,时钟算法需要在页表项(PTE)中设置了一位访问位来表示此页表项对应的页当前是否被访问过。当该页被访问时,CPU 中的 MMU 硬件将把访问位置“1”。当操作系统需要淘汰页时,对当前指针指向的页所对应的页表项进行查询,如果访问位为“0”,则淘汰该页,如果该页被写过,则还要把它换出到硬盘上;如果访问位为“1”,则将该页表项的此位置“0”,继续访问下一个页。该算法近似地体现了 LRU 的思想,且易于实现,开销少,需要硬件支持来设置访问位。时钟页替换算法在本质上与 FIFO 算法是类似的,不同之处是在时钟页替换算法中跳过了访问位为 1 的页。 10 | 11 | - 改进的时钟(Enhanced Clock)页替换算法:在时钟置换算法中,淘汰一个页面时只考虑了页面是否被访问过,但在实际情况中,还应考虑被淘汰的页面是否被修改过。因为淘汰修改过的页面还需要写回硬盘,使得其置换代价大于未修改过的页面,所以优先淘汰没有修改的页,减少磁盘操作次数。改进的时钟置换算法除了考虑页面的访问情况,还需考虑页面的修改情况。即该算法不但希望淘汰的页面是最近未使用的页,而且还希望被淘汰的页是在主存驻留期间其页面内容未被修改过的。这需要为每一页的对应页表项内容中增加一位引用位和一位修改位。当该页被访问时,CPU 中的 MMU 硬件将把访问位置“1”。当该页被“写”时,CPU 中的 MMU 硬件将把修改位置“1”。这样这两位就存在四种可能的组合情况:(0,0)表示最近未被引用也未被修改,首先选择此页淘汰;(0,1)最近未被使用,但被修改,其次选择;(1,0)最近使用而未修改,再次选择;(1,1)最近使用且修改,最后选择。该算法与时钟算法相比,可进一步减少磁盘的 I/O 操作次数,但为了查找到一个尽可能适合淘汰的页面,可能需要经过多次扫描,增加了算法本身的执行开销。 12 | -------------------------------------------------------------------------------- /lab3/lab3_5_swapping.md: -------------------------------------------------------------------------------- 1 | ## 页面置换机制的实现 2 | -------------------------------------------------------------------------------- /lab3/lab3_6_labs_requirement.md: -------------------------------------------------------------------------------- 1 | ## 实验报告要求 2 | 3 | 从 git server 网站上取得 ucore_lab 后,进入目录 labcodes/lab3,完成实验要求的各个练习。在实验报告中回答所有练习中提出的问题。 4 | 在目录 labcodes/lab3 下存放实验报告,实验报告文档命名为 lab3.md,实验报告使用**markdown**格式。 5 | 对于 lab3 中编程任务,完成编写之后,再通过 git push 命令把代码同步回 git server 网站。最后请一定提前或按时提交到 git server 网站。 6 | 7 | 注意有“LAB3”的注释,代码中所有需要完成的地方(challenge 除外)都有“LAB3”和“YOUR CODE”的注释,请在提交时特别注意保持注释,并将“YOUR CODE”替换为自己的学号,并且将所有标有对应注释的部分填上正确的代码。 8 | 9 | 附录:正确输出的参考: 10 | 11 | ``` 12 | $ make qemu 13 | (THU.CST) os is loading ... 14 | Special kernel symbols: 15 | entry 0xc010002c (phys) 16 | etext 0xc010962b (phys) 17 | edata 0xc0122ac8 (phys) 18 | end 0xc0123c10 (phys) 19 | Kernel executable memory footprint: 143KB 20 | memory management: default_pmm_manager 21 | e820map: 22 | memory: 0009f400, [00000000, 0009f3ff], type = 1. 23 | memory: 00000c00, [0009f400, 0009ffff], type = 2. 24 | memory: 00010000, [000f0000, 000fffff], type = 2. 25 | memory: 07efd000, [00100000, 07ffcfff], type = 1. 26 | memory: 00003000, [07ffd000, 07ffffff], type = 2. 27 | memory: 00040000, [fffc0000, ffffffff], type = 2. 28 | check_alloc_page() succeeded! 29 | check_pgdir() succeeded! 30 | check_boot_pgdir() succeeded! 31 | -------------------- BEGIN -------------------- 32 | PDE(0e0) c0000000-f8000000 38000000 urw 33 | |-- PTE(38000) c0000000-f8000000 38000000 -rw 34 | PDE(001) fac00000-fb000000 00400000 -rw 35 | |-- PTE(000e0) faf00000-fafe0000 000e0000 urw 36 | |-- PTE(00001) fafeb000-fafec000 00001000 -rw 37 | --------------------- END --------------------- 38 | check_vma_struct() succeeded! 39 | page fault at 0x00000100: K/W [no page found]. 40 | check_pgfault() succeeded! 41 | check_vmm() succeeded. 42 | ide 0: 10000(sectors), 'QEMU HARDDISK'. 43 | ide 1: 262144(sectors), 'QEMU HARDDISK'. 44 | SWAP: manager = fifo swap manager 45 | BEGIN check_swap: count 1, total 31992 46 | mm->sm_priv c0123c04 in fifo_init_mm 47 | setup Page Table for vaddr 0X1000, so alloc a page 48 | setup Page Table vaddr 0~4MB OVER! 49 | set up init env for check_swap begin! 50 | page fault at 0x00001000: K/W [no page found]. 51 | page fault at 0x00002000: K/W [no page found]. 52 | page fault at 0x00003000: K/W [no page found]. 53 | page fault at 0x00004000: K/W [no page found]. 54 | set up init env for check_swap over! 55 | write Virt Page c in fifo_check_swap 56 | write Virt Page a in fifo_check_swap 57 | write Virt Page d in fifo_check_swap 58 | write Virt Page b in fifo_check_swap 59 | write Virt Page e in fifo_check_swap 60 | page fault at 0x00005000: K/W [no page found]. 61 | swap_out: i 0, store page in vaddr 0x1000 to disk swap entry 2 62 | write Virt Page b in fifo_check_swap 63 | write Virt Page a in fifo_check_swap 64 | page fault at 0x00001000: K/W [no page found]. 65 | swap_out: i 0, store page in vaddr 0x2000 to disk swap entry 3 66 | swap_in: load disk swap entry 2 with swap_page in vadr 0x1000 67 | write Virt Page b in fifo_check_swap 68 | page fault at 0x00002000: K/W [no page found]. 69 | swap_out: i 0, store page in vaddr 0x3000 to disk swap entry 4 70 | swap_in: load disk swap entry 3 with swap_page in vadr 0x2000 71 | write Virt Page c in fifo_check_swap 72 | page fault at 0x00003000: K/W [no page found]. 73 | swap_out: i 0, store page in vaddr 0x4000 to disk swap entry 5 74 | swap_in: load disk swap entry 4 with swap_page in vadr 0x3000 75 | write Virt Page d in fifo_check_swap 76 | page fault at 0x00004000: K/W [no page found]. 77 | swap_out: i 0, store page in vaddr 0x5000 to disk swap entry 6 78 | swap_in: load disk swap entry 5 with swap_page in vadr 0x4000 79 | check_swap() succeeded! 80 | ++ setup timer interrupts 81 | 100 ticks 82 | End of Test. 83 | kernel panic at kern/trap/trap.c:20: 84 | EOT: kernel seems ok. 85 | Welcome to the kernel debug monitor!! 86 | Type 'help' for a list of commands. 87 | ``` 88 | -------------------------------------------------------------------------------- /lab3_figs/image001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab3_figs/image001.png -------------------------------------------------------------------------------- /lab3_figs/image002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab3_figs/image002.png -------------------------------------------------------------------------------- /lab4.md: -------------------------------------------------------------------------------- 1 | # 实验四:内核线程管理 2 | -------------------------------------------------------------------------------- /lab4/lab4_1_goals.md: -------------------------------------------------------------------------------- 1 | ## 实验目的 2 | 3 | - 了解内核线程创建/执行的管理过程 4 | - 了解内核线程的切换和基本调度过程 5 | -------------------------------------------------------------------------------- /lab4/lab4_2_1_exercises.md: -------------------------------------------------------------------------------- 1 | ### 练习 2 | 3 | 对实验报告的要求: 4 | 5 | - 基于 markdown 格式来完成,以文本方式为主 6 | - 填写各个基本练习中要求完成的报告内容 7 | - 完成实验后,请分析 ucore_lab 中提供的参考答案,并请在实验报告中说明你的实现与参考答案的区别 8 | - 列出你认为本实验中重要的知识点,以及与对应的 OS 原理中的知识点,并简要说明你对二者的含义,关系,差异等方面的理解(也可能出现实验中的知识点没有对应的原理知识点) 9 | - 列出你认为 OS 原理中很重要,但在实验中没有对应上的知识点 10 | 11 | #### 练习 0:填写已有实验 12 | 13 | 本实验依赖实验 1/2/3。请把你做的实验 1/2/3 的代码填入本实验中代码中有“LAB1”,“LAB2”,“LAB3”的注释相应部分。 14 | 15 | #### 练习 1:分配并初始化一个进程控制块(需要编码) 16 | 17 | alloc_proc 函数(位于 kern/process/proc.c 中)负责分配并返回一个新的 struct proc_struct 结构,用于存储新建立的内核线程的管理信息。ucore 需要对这个结构进行最基本的初始化,你需要完成这个初始化过程。 18 | 19 | > 【提示】在 alloc_proc 函数的实现中,需要初始化的 proc_struct 结构中的成员变量至少包括:state/pid/runs/kstack/need_resched/parent/mm/context/tf/cr3/flags/name。 20 | 21 | 请在实验报告中简要说明你的设计实现过程。请回答如下问题: 22 | 23 | - 请说明 proc_struct 中`struct context context`和`struct trapframe *tf`成员变量含义和在本实验中的作用是啥?(提示通过看代码和编程调试可以判断出来) 24 | 25 | #### 练习 2:为新创建的内核线程分配资源(需要编码) 26 | 27 | 创建一个内核线程需要分配和设置好很多资源。kernel_thread 函数通过调用**do_fork**函数完成具体内核线程的创建工作。do_kernel 函数会调用 alloc_proc 函数来分配并初始化一个进程控制块,但 alloc_proc 只是找到了一小块内存用以记录进程的必要信息,并没有实际分配这些资源。ucore 一般通过 do_fork 实际创建新的内核线程。do_fork 的作用是,创建当前内核线程的一个副本,它们的执行上下文、代码、数据都一样,但是存储位置不同。在这个过程中,需要给新内核线程分配资源,并且复制原进程的状态。你需要完成在 kern/process/proc.c 中的 do_fork 函数中的处理过程。它的大致执行步骤包括: 28 | 29 | - 调用 alloc_proc,首先获得一块用户信息块。 30 | - 为进程分配一个内核栈。 31 | - 复制原进程的内存管理信息到新进程(但内核线程不必做此事) 32 | - 复制原进程上下文到新进程 33 | - 将新进程添加到进程列表 34 | - 唤醒新进程 35 | - 返回新进程号 36 | 37 | 请在实验报告中简要说明你的设计实现过程。请回答如下问题: 38 | 39 | - 请说明 ucore 是否做到给每个新 fork 的线程一个唯一的 id?请说明你的分析和理由。 40 | 41 | #### 练习 3:阅读代码,理解 proc_run 函数和它调用的函数如何完成进程切换的。(无编码工作) 42 | 43 | 请在实验报告中简要说明你对 proc_run 函数的分析。并回答如下问题: 44 | 45 | - 在本实验的执行过程中,创建且运行了几个内核线程? 46 | - 语句`local_intr_save(intr_flag);....local_intr_restore(intr_flag);`在这里有何作用?请说明理由 47 | 48 | 完成代码编写后,编译并运行代码:make qemu 49 | 50 | 如果可以得到如 附录 A 所示的显示内容(仅供参考,不是标准答案输出),则基本正确。 51 | 52 | #### 扩展练习 Challenge:实现支持任意大小的内存分配算法 53 | 54 | 这不是本实验的内容,其实是上一次实验内存的扩展,但考虑到现在的 slab 算法比较复杂,有必要实现一个比较简单的任意大小内存分配算法。可参考本实验中的 slab 如何调用基于页的内存分配算法(注意,不是要你关注 slab 的具体实现)来实现 first-fit/best-fit/worst-fit/buddy 等支持任意大小的内存分配算法。。 55 | 56 | 【注意】下面是相关的 Linux 实现文档,供参考 57 | 58 | SLOB 59 | 60 | [http://en.wikipedia.org/wiki/SLOB](http://en.wikipedia.org/wiki/SLOB) 61 | [http://lwn.net/Articles/157944/](http://lwn.net/Articles/157944/) 62 | 63 | SLAB 64 | 65 | [https://www.ibm.com/developerworks/cn/linux/l-linux-slab-allocator/](https://www.ibm.com/developerworks/cn/linux/l-linux-slab-allocator/) 66 | -------------------------------------------------------------------------------- /lab4/lab4_2_2_files.md: -------------------------------------------------------------------------------- 1 | ### 项目组成 2 | 3 | ``` 4 | ├── boot 5 | ├── kern 6 | │ ├── debug 7 | │ ├── driver 8 | │ ├── fs 9 | │ ├── init 10 | │ │ ├── init.c 11 | │ │ └── ... 12 | │ ├── libs 13 | │ │ ├── rb\_tree.c 14 | │ │ ├── rb\_tree.h 15 | │ │ └── ... 16 | │ ├── mm 17 | │ │ ├── kmalloc.c 18 | │ │ ├── kmalloc.h 19 | │ │ ├── memlayout.h 20 | │ │ ├── pmm.c 21 | │ │ ├── pmm.h 22 | │ │ ├── swap.c 23 | │ │ ├── vmm.c 24 | │ │ └── ... 25 | │ ├── process 26 | │ │ ├── entry.S 27 | │ │ ├── proc.c 28 | │ │ ├── proc.h 29 | │ │ └── switch.S 30 | │ ├── schedule 31 | │ │ ├── sched.c 32 | │ │ └── sched.h 33 | │ ├── sync 34 | │ │ └── sync.h 35 | │ └── trap 36 | │ ├── trapentry.S 37 | │ └── ... 38 | ├── libs 39 | │ ├── hash.c 40 | │ ├── stdlib.h 41 | │ ├── unistd.h 42 | │ └── ... 43 | ├── Makefile 44 | └── tools 45 | ``` 46 | 47 | 相对与实验三,实验四中主要改动如下: 48 | 49 | - kern/process/ (新增进程管理相关文件) 50 | - proc.[ch]:新增:实现进程、线程相关功能,包括:创建进程/线程,初始化进程/线程,处理进程/线程退出等功能 51 | - entry.S:新增:内核线程入口函数 kernel_thread_entry 的实现 52 | - switch.S:新增:上下文切换,利用堆栈保存、恢复进程上下文 53 | 54 | - kern/init/ 55 | - init.c:修改:完成进程系统初始化,并在内核初始化后切入 idle 进程 56 | 57 | - kern/mm/ (基本上与本次实验没有太直接的联系,了解 kmalloc 和 kfree 如何使用即可) 58 | - kmalloc.[ch]:新增:定义和实现了新的 kmalloc/kfree 函数。具体实现是基于 slab 分配的简化算法 (只要求会调用这两个函数即可) 59 | - memlayout.h:增加 slab 物理内存分配相关的定义与宏 (可不用理会)。 60 | - pmm.[ch]:修改:在 pmm.c 中添加了调用 kmalloc_init 函数,取消了老的 kmalloc/kfree 的实现;在 pmm.h 中取消了老的 kmalloc/kfree 的定义 61 | - swap.c:修改:取消了用于 check 的 Line 185 的执行 62 | - vmm.c:修改:调用新的 kmalloc/kfree 63 | 64 | - kern/trap/ 65 | - trapentry.S:增加了汇编写的函数 forkrets,用于 do_fork 调用的返回处理。 66 | 67 | - kern/schedule/ 68 | - sched.[ch]:新增:实现 FIFO 策略的进程调度 69 | 70 | - kern/libs 71 | - rb_tree.[ch]:新增:实现红黑树,被 slab 分配的简化算法使用(可不用理会) 72 | 73 | **编译执行** 74 | 75 | 编译并运行代码的命令如下: 76 | 77 | ``` 78 | make 79 | make qemu 80 | ``` 81 | 82 | 则可以得到如下的显示内容(仅供参考,不是标准答案输出) 83 | 84 | ``` 85 | (THU.CST) os is loading ... 86 | 87 | Special kernel symbols: 88 | entry 0xc010002a (phys) 89 | etext 0xc010a708 (phys) 90 | edata 0xc0127ae0 (phys) 91 | end 0xc012ad58 (phys) 92 | 93 | ... 94 | 95 | ++ setup timer interrupts 96 | this initproc, pid = 1, name = "init" 97 | To U: "Hello world!!". 98 | To U: "en.., Bye, Bye. :)" 99 | kernel panic at kern/process/proc.c:354: 100 | process exit!!. 101 | 102 | Welcome to the kernel debug monitor!! 103 | Type 'help' for a list of commands. 104 | K> qemu: terminating on signal 2 105 | ``` 106 | -------------------------------------------------------------------------------- /lab4/lab4_2_labs.md: -------------------------------------------------------------------------------- 1 | ## 实验内容 2 | 3 | 实验 2/3 完成了物理和虚拟内存管理,这给创建内核线程(内核线程是一种特殊的进程)打下了提供内存管理的基础。当一个程序加载到内存中运行时,首先通过 ucore OS 的内存管理子系统分配合适的空间,然后就需要考虑如何分时使用 CPU 来“并发”执行多个程序,让每个运行的程序(这里用线程或进程表示)“感到”它们各自拥有“自己”的 CPU。 4 | 5 | 本次实验将首先接触的是内核线程的管理。内核线程是一种特殊的进程,内核线程与用户进程的区别有两个: 6 | 7 | - 内核线程只运行在内核态 8 | - 用户进程会在在用户态和内核态交替运行 9 | - 所有内核线程共用 ucore 内核内存空间,不需为每个内核线程维护单独的内存空间 10 | - 而用户进程需要维护各自的用户内存空间 11 | 12 | 相关原理介绍可看附录 B:【原理】进程/线程的属性与特征解析。 13 | -------------------------------------------------------------------------------- /lab4/lab4_3_1_lab_steps.md: -------------------------------------------------------------------------------- 1 | ### 实验执行流程概述 2 | 3 | lab2 和 lab3 完成了对内存的虚拟化,但整个控制流还是一条线串行执行。lab4 将在此基础上进行 CPU 的虚拟化,即让 ucore 实现分时共享 CPU,实现多条控制流能够并发执行。从某种程度上,我们可以把控制流看作是一个内核线程。本次实验将首先接触的是内核线程的管理。内核线程是一种特殊的进程,内核线程与用户进程的区别有两个:内核线程只运行在内核态而用户进程会在在用户态和内核态交替运行;所有内核线程直接使用共同的 ucore 内核内存空间,不需为每个内核线程维护单独的内存空间而用户进程需要维护各自的用户内存空间。从内存空间占用情况这个角度上看,我们可以把线程看作是一种共享内存空间的轻量级进程。 4 | 5 | 为了实现内核线程,需要设计管理线程的数据结构,即进程控制块(在这里也可叫做线程控制块)。如果要让内核线程运行,我们首先要创建内核线程对应的进程控制块,还需把这些进程控制块通过链表连在一起,便于随时进行插入,删除和查找操作等进程管理事务。这个链表就是进程控制块链表。然后在通过调度器(scheduler)来让不同的内核线程在不同的时间段占用 CPU 执行,实现对 CPU 的分时共享。那 lab4 中是如何一步一步实现这个过程的呢? 6 | 7 | 我们还是从 lab4/kern/init/init.c 中的 kern_init 函数入手分析。在 kern_init 函数中,当完成虚拟内存的初始化工作后,就调用了 proc_init 函数,这个函数完成了 idleproc 内核线程和 initproc 内核线程的创建或复制工作,这也是本次实验要完成的练习。idleproc 内核线程的工作就是不停地查询,看是否有其他内核线程可以执行了,如果有,马上让调度器选择那个内核线程执行(请参考 cpu_idle 函数的实现)。所以 idleproc 内核线程是在 ucore 操作系统没有其他内核线程可执行的情况下才会被调用。接着就是调用 kernel_thread 函数来创建 initproc 内核线程。initproc 内核线程的工作就是显示“Hello World”,表明自己存在且能正常工作了。 8 | 9 | 调度器会在特定的调度点上执行调度,完成进程切换。在 lab4 中,这个调度点就一处,即在 cpu_idle 函数中,此函数如果发现当前进程(也就是 idleproc)的 need_resched 置为 1(在初始化 idleproc 的进程控制块时就置为 1 了),则调用 schedule 函数,完成进程调度和进程切换。进程调度的过程其实比较简单,就是在进程控制块链表中查找到一个“合适”的内核线程,所谓“合适”就是指内核线程处于“PROC_RUNNABLE”状态。在接下来的 switch_to 函数(在后续有详细分析,有一定难度,需深入了解一下)完成具体的进程切换过程。一旦切换成功,那么 initproc 内核线程就可以通过显示字符串来表明本次实验成功。 10 | 11 | 接下来将主要介绍了进程创建所需的重要数据结构--进程控制块 12 | proc_struct,以及 ucore 创建并执行内核线程 idleproc 和 initproc 的两种不同方式,特别是创建 initproc 的方式将被延续到实验五中,扩展为创建用户进程的主要方式。另外,还初步涉及了进程调度(实验六涉及并会扩展)和进程切换内容。 13 | -------------------------------------------------------------------------------- /lab4/lab4_3_2_pcb.md: -------------------------------------------------------------------------------- 1 | ### 设计关键数据结构 -- 进程控制块 2 | 3 | 在实验四中,进程管理信息用 struct 4 | proc_struct 表示,在*kern/process/proc.h*中定义如下: 5 | 6 | ``` 7 | struct proc_struct { 8 | enum proc_state state; // Process state 9 | int pid; // Process ID 10 | int runs; // the running times of Proces 11 | uintptr_t kstack; // Process kernel stack 12 | volatile bool need_resched; // need to be rescheduled to release CPU? 13 | struct proc_struct *parent; // the parent process 14 | struct mm_struct *mm; // Process's memory management field 15 | struct context context; // Switch here to run process 16 | struct trapframe *tf; // Trap frame for current interrupt 17 | uintptr_t cr3; // the base addr of Page Directroy Table(PDT) 18 | uint32_t flags; // Process flag 19 | char name[PROC_NAME_LEN + 1]; // Process name 20 | list_entry_t list_link; // Process link list 21 | list_entry_t hash_link; // Process hash list 22 | }; 23 | ``` 24 | 25 | 下面重点解释一下几个比较重要的成员变量: 26 | 27 | ● mm:内存管理的信息,包括内存映射列表、页表指针等。mm 成员变量在 lab3 中用于虚存管理。但在实际 OS 中,内核线程常驻内存,不需要考虑 swap page 问题,在 lab5 中涉及到了用户进程,才考虑进程用户内存空间的 swap page 问题,mm 才会发挥作用。所以在 lab4 中 mm 对于内核线程就没有用了,这样内核线程的 proc_struct 的成员变量\*mm=0 是合理的。mm 里有个很重要的项 pgdir,记录的是该进程使用的一级页表的物理地址。由于\*mm=NULL,所以在 proc_struct 数据结构中需要有一个代替 pgdir 项来记录页表起始地址,这就是 proc_struct 数据结构中的 cr3 成员变量。 28 | 29 | ● state:进程所处的状态。 30 | 31 | ● parent:用户进程的父进程(创建它的进程)。在所有进程中,只有一个进程没有父进程,就是内核创建的第一个内核线程 idleproc。内核根据这个父子关系建立一个树形结构,用于维护一些特殊的操作,例如确定某个进程是否可以对另外一个进程进行某种操作等等。 32 | 33 | ● context:进程的上下文,用于进程切换(参见 switch.S)。在 uCore 中,所有的进程在内核中也是相对独立的(例如独立的内核堆栈以及上下文等等)。使用 context 保存寄存器的目的就在于在内核态中能够进行上下文之间的切换。实际利用 context 进行上下文切换的函数是在*kern/process/switch.S*中定义 switch_to。 34 | 35 | ● tf:中断帧的指针,总是指向内核栈的某个位置:当进程从用户空间跳到内核空间时,中断帧记录了进程在被中断前的状态。当内核需要跳回用户空间时,需要调整中断帧以恢复让进程继续执行的各寄存器值。除此之外,uCore 内核允许嵌套中断。因此为了保证嵌套中断发生时 tf 总是能够指向当前的 trapframe,uCore 在内核栈上维护了 tf 的链,可以参考 trap.c::trap 函数做进一步的了解。 36 | 37 | ● cr3: cr3 保存页表的物理地址,目的就是进程切换的时候方便直接使用 lcr3 实现页表切换,避免每次都根据 mm 来计算 cr3。mm 数据结构是用来实现用户空间的虚存管理的,但是内核线程没有用户空间,它执行的只是内核中的一小段代码(通常是一小段函数),所以它没有 mm 结构,也就是 NULL。当某个进程是一个普通用户态进程的时候,PCB 中的 cr3 就是 mm 中页表(pgdir)的物理地址;而当它是内核线程的时候,cr3 等于 boot_cr3。而 boot_cr3 指向了 uCore 启动时建立好的饿内核虚拟空间的页目录表首地址。 38 | 39 | ● kstack: 每个线程都有一个内核栈,并且位于内核地址空间的不同位置。对于内核线程,该栈就是运行时的程序使用的栈;而对于普通进程,该栈是发生特权级改变的时候使保存被打断的硬件信息用的栈。uCore 在创建进程时分配了 2 个连续的物理页(参见 memlayout.h 中 KSTACKSIZE 的定义)作为内核栈的空间。这个栈很小,所以内核中的代码应该尽可能的紧凑,并且避免在栈上分配大的数据结构,以免栈溢出,导致系统崩溃。kstack 记录了分配给该进程/线程的内核栈的位置。主要作用有以下几点。首先,当内核准备从一个进程切换到另一个的时候,需要根据 kstack 的值正确的设置好 tss (可以回顾一下在实验一中讲述的 tss 在中断处理过程中的作用),以便在进程切换以后再发生中断时能够使用正确的栈。其次,内核栈位于内核地址空间,并且是不共享的(每个线程都拥有自己的内核栈),因此不受到 mm 的管理,当进程退出的时候,内核能够根据 kstack 的值快速定位栈的位置并进行回收。uCore 的这种内核栈的设计借鉴的是 linux 的方法(但由于内存管理实现的差异,它实现的远不如 linux 的灵活),它使得每个线程的内核栈在不同的位置,这样从某种程度上方便调试,但同时也使得内核对栈溢出变得十分不敏感,因为一旦发生溢出,它极可能污染内核中其它的数据使得内核崩溃。如果能够通过页表,将所有进程的内核栈映射到固定的地址上去,能够避免这种问题,但又会使得进程切换过程中对栈的修改变得相当繁琐。感兴趣的同学可以参考 linux kernel 的代码对此进行尝试。 40 | 41 | 为了管理系统中所有的进程控制块,uCore 维护了如下全局变量(位于*kern/process/proc.c*): 42 | 43 | ● static struct proc \*current:当前占用 CPU 且处于“运行”状态进程控制块指针。通常这个变量是只读的,只有在进程切换的时候才进行修改,并且整个切换和修改过程需要保证操作的原子性,目前至少需要屏蔽中断。可以参考 switch_to 的实现。 44 | 45 | ● static struct proc \*initproc:本实验中,指向一个内核线程。本实验以后,此指针将指向第一个用户态进程。 46 | 47 | ● static list_entry_t hash_list[HASH\_LIST\_SIZE]:所有进程控制块的哈希表,proc_struct 中的成员变量 hash_link 将基于 pid 链接入这个哈希表中。 48 | 49 | ● list_entry_t proc_list:所有进程控制块的双向线性列表,proc_struct 中的成员变量 list_link 将链接入这个链表中。 50 | -------------------------------------------------------------------------------- /lab4/lab4_3_3_1_create_kthread_idleproc.md: -------------------------------------------------------------------------------- 1 | #### 创建第 0 个内核线程 idleproc 2 | 3 | 在 init.c::kern_init 函数调用了 proc.c::proc_init 函数。proc_init 函数启动了创建内核线程的步骤。首先当前的执行上下文(从 kern_init 启动至今)就可以看成是 uCore 内核(也可看做是内核进程)中的一个内核线程的上下文。为此,uCore 通过给当前执行的上下文分配一个进程控制块以及对它进行相应初始化,将其打造成第 0 个内核线程 -- idleproc。具体步骤如下: 4 | 5 | 首先调用 alloc_proc 函数来通过 kmalloc 函数获得 proc_struct 结构的一块内存块-,作为第 0 个进程控制块。并把 proc 进行初步初始化(即把 proc_struct 中的各个成员变量清零)。但有些成员变量设置了特殊的值,比如: 6 | 7 | ``` 8 | proc->state = PROC_UNINIT; 设置进程为“初始”态 9 | proc->pid = -1; 设置进程pid的未初始化值 10 | proc->cr3 = boot_cr3; 使用内核页目录表的基址 11 | ... 12 | ``` 13 | 14 | 上述三条语句中,第一条设置了进程的状态为“初始”态,这表示进程已经 15 | “出生”了,正在获取资源茁壮成长中;第二条语句设置了进程的 pid 为-1,这表示进程的“身份证号”还没有办好;第三条语句表明由于该内核线程在内核中运行,故采用为 uCore 内核已经建立的页表,即设置为在 uCore 内核页表的起始地址 boot_cr3。后续实验中可进一步看出所有内核线程的内核虚地址空间(也包括物理地址空间)是相同的。既然内核线程共用一个映射内核空间的页表,这表示内核空间对所有内核线程都是“可见”的,所以更精确地说,这些内核线程都应该是从属于同一个唯一的“大内核进程”—uCore 内核。 16 | 17 | 接下来,proc_init 函数对 idleproc 内核线程进行进一步初始化: 18 | 19 | ``` 20 | idleproc->pid = 0; 21 | idleproc->state = PROC_RUNNABLE; 22 | idleproc->kstack = (uintptr_t)bootstack; 23 | idleproc->need_resched = 1; 24 | set_proc_name(idleproc, "idle"); 25 | ``` 26 | 27 | 需要注意前 4 条语句。第一条语句给了 idleproc 合法的身份证号--0,这名正言顺地表明了 idleproc 是第 0 个内核线程。通常可以通过 pid 的赋值来表示线程的创建和身份确定。“0”是第一个的表示方法是计算机领域所特有的,比如 C 语言定义的第一个数组元素的小标也是“0”。第二条语句改变了 idleproc 的状态,使得它从“出生”转到了“准备工作”,就差 uCore 调度它执行了。第三条语句设置了 idleproc 所使用的内核栈的起始地址。需要注意以后的其他线程的内核栈都需要通过分配获得,因为 uCore 启动时设置的内核栈直接分配给 idleproc 使用了。第四条很重要,因为 uCore 希望当前 CPU 应该做更有用的工作,而不是运行 idleproc 这个“无所事事”的内核线程,所以把 idleproc-\>need_resched 设置为“1”,结合 idleproc 的执行主体--cpu_idle 函数的实现,可以清楚看出如果当前 idleproc 在执行,则只要此标志为 1,马上就调用 schedule 函数要求调度器切换其他进程执行。 28 | -------------------------------------------------------------------------------- /lab4/lab4_3_3_2_create_kthread_initproc.md: -------------------------------------------------------------------------------- 1 | #### 创建第 1 个内核线程 initproc 2 | 3 | 第 0 个内核线程主要工作是完成内核中各个子系统的初始化,然后就通过执行 cpu_idle 函数开始过退休生活了。所以 uCore 接下来还需创建其他进程来完成各种工作,但 idleproc 内核子线程自己不想做,于是就通过调用 kernel_thread 函数创建了一个内核线程 init_main。在实验四中,这个子内核线程的工作就是输出一些字符串,然后就返回了(参看 init_main 函数)。但在后续的实验中,init_main 的工作就是创建特定的其他内核线程或用户进程(实验五涉及)。下面我们来分析一下创建内核线程的函数 kernel_thread: 4 | 5 | ``` 6 | kernel_thread(int (*fn)(void *), void *arg, uint32_t clone_flags) 7 | { 8 | struct trapframe tf; 9 | memset(&tf, 0, sizeof(struct trapframe)); 10 | tf.tf_cs = KERNEL_CS; 11 | tf.tf_ds = tf_struct.tf_es = tf_struct.tf_ss = KERNEL_DS; 12 | tf.tf_regs.reg_ebx = (uint32_t)fn; 13 | tf.tf_regs.reg_edx = (uint32_t)arg; 14 | tf.tf_eip = (uint32_t)kernel_thread_entry; 15 | return do_fork(clone_flags | CLONE_VM, 0, &tf); 16 | } 17 | ``` 18 | 19 | 注意,kernel_thread 函数采用了局部变量 tf 来放置保存内核线程的临时中断帧,并把中断帧的指针传递给 do_fork 函数,而 do_fork 函数会调用 copy_thread 函数来在新创建的进程内核栈上专门给进程的中断帧分配一块空间。 20 | 21 | 给中断帧分配完空间后,就需要构造新进程的中断帧,具体过程是:首先给 tf 进行清零初始化,并设置中断帧的代码段(tf.tf_cs)和数据段(tf.tf_ds/tf_es/tf_ss)为内核空间的段(KERNEL_CS/KERNEL_DS),这实际上也说明了 initproc 内核线程在内核空间中执行。而 initproc 内核线程从哪里开始执行呢?tf.tf_eip 的指出了是 kernel_thread_entry(位于 kern/process/entry.S 中),kernel_thread_entry 是 entry.S 中实现的汇编函数,它做的事情很简单: 22 | 23 | ``` 24 | kernel_thread_entry: # void kernel_thread(void) 25 | pushl %edx # push arg 26 | call *%ebx # call fn 27 | pushl %eax # save the return value of fn(arg) 28 | call do_exit # call do_exit to terminate current thread 29 | ``` 30 | 31 | 从上可以看出,kernel_thread_entry 函数主要为内核线程的主体 fn 函数做了一个准备开始和结束运行的“壳”,并把函数 fn 的参数 arg(保存在 edx 寄存器中)压栈,然后调用 fn 函数,把函数返回值 eax 寄存器内容压栈,调用 do_exit 函数退出线程执行。 32 | 33 | do_fork 是创建线程的主要函数。kernel_thread 函数通过调用 do_fork 函数最终完成了内核线程的创建工作。下面我们来分析一下 do_fork 函数的实现(练习 2)。do_fork 函数主要做了以下 6 件事情: 34 | 35 | 1. 分配并初始化进程控制块(alloc_proc 函数); 36 | 2. 分配并初始化内核栈(setup_stack 函数); 37 | 3. 根据 clone_flag 标志复制或共享进程内存管理结构(copy_mm 函数); 38 | 4. 设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文(copy_thread 函数); 39 | 5. 把设置好的进程控制块放入 hash_list 和 proc_list 两个全局进程链表中; 40 | 6. 自此,进程已经准备好执行了,把进程状态设置为“就绪”态; 41 | 7. 设置返回码为子进程的 id 号。 42 | 43 | 这里需要注意的是,如果上述前 3 步执行没有成功,则需要做对应的出错处理,把相关已经占有的内存释放掉。copy_mm 函数目前只是把 current-\>mm 设置为 NULL,这是由于目前在实验四中只能创建内核线程,proc-\>mm 描述的是进程用户态空间的情况,所以目前 mm 还用不上。copy_thread 函数做的事情比较多,代码如下: 44 | 45 | ``` 46 | static void 47 | copy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf) { 48 | //在内核堆栈的顶部设置中断帧大小的一块栈空间 49 | proc->tf = (struct trapframe *)(proc->kstack + KSTACKSIZE) - 1; 50 | *(proc->tf) = *tf; //拷贝在kernel_thread函数建立的临时中断帧的初始值 51 | proc->tf->tf_regs.reg_eax = 0; 52 | //设置子进程/线程执行完do_fork后的返回值 53 | proc->tf->tf_esp = esp; //设置中断帧中的栈指针esp 54 | proc->tf->tf_eflags |= FL_IF; //使能中断 55 | proc->context.eip = (uintptr_t)forkret; 56 | proc->context.esp = (uintptr_t)(proc->tf); 57 | } 58 | ``` 59 | 60 | 此函数首先在内核堆栈的顶部设置中断帧大小的一块栈空间,并在此空间中拷贝在 kernel_thread 函数建立的临时中断帧的初始值,并进一步设置中断帧中的栈指针 esp 和标志寄存器 eflags,特别是 eflags 设置了 FL_IF 标志,这表示此内核线程在执行过程中,能响应中断,打断当前的执行。执行到这步后,此进程的中断帧就建立好了,对于 initproc 而言,它的中断帧如下所示: 61 | 62 | ``` 63 | //所在地址位置 64 | initproc->tf= (proc->kstack+KSTACKSIZE) – sizeof (struct trapframe); 65 | //具体内容 66 | initproc->tf.tf_cs = KERNEL_CS; 67 | initproc->tf.tf_ds = initproc->tf.tf_es = initproc->tf.tf_ss = KERNEL_DS; 68 | initproc->tf.tf_regs.reg_ebx = (uint32_t)init_main; 69 | initproc->tf.tf_regs.reg_edx = (uint32_t) ADDRESS of "Helloworld!!"; 70 | initproc->tf.tf_eip = (uint32_t)kernel_thread_entry; 71 | initproc->tf.tf_regs.reg_eax = 0; 72 | initproc->tf.tf_esp = esp; 73 | initproc->tf.tf_eflags |= FL_IF; 74 | ``` 75 | 76 | 设置好中断帧后,最后就是设置 initproc 的进程上下文,(process context,也称执行现场)了。只有设置好执行现场后,一旦 uCore 调度器选择了 initproc 执行,就需要根据 initproc-\>context 中保存的执行现场来恢复 initproc 的执行。这里设置了 initproc 的执行现场中主要的两个信息:上次停止执行时的下一条指令地址 context.eip 和上次停止执行时的堆栈地址 context.esp。其实 initproc 还没有执行过,所以这其实就是 initproc 实际执行的第一条指令地址和堆栈指针。可以看出,由于 initproc 的中断帧占用了实际给 initproc 分配的栈空间的顶部,所以 initproc 就只能把栈顶指针 context.esp 设置在 initproc 的中断帧的起始位置。根据 context.eip 的赋值,可以知道 initproc 实际开始执行的地方在 forkret 函数(主要完成 do_fork 函数返回的处理工作)处。至此,initproc 内核线程已经做好准备执行了。 77 | -------------------------------------------------------------------------------- /lab4/lab4_3_3_create_exec_kernel_thread.md: -------------------------------------------------------------------------------- 1 | ### 创建并执行内核线程 2 | 3 | 建立进程控制块(proc.c 中的 alloc_proc 函数)后,现在就可以通过进程控制块来创建具体的进程/线程了。首先,考虑最简单的内核线程,它通常只是内核中的一小段代码或者函数,没有自己的“专属”空间。这是由于在 uCore OS 启动后,已经对整个内核内存空间进行了管理,通过设置页表建立了内核虚拟空间(即 boot_cr3 指向的二级页表描述的空间)。所以 uCore OS 内核中的所有线程都不需要再建立各自的页表,只需共享这个内核虚拟空间就可以访问整个物理内存了。从这个角度看,内核线程被 uCore OS 内核这个大“内核进程”所管理。 4 | -------------------------------------------------------------------------------- /lab4/lab4_3_kernel_thread_management.md: -------------------------------------------------------------------------------- 1 | ## 内核线程管理 2 | -------------------------------------------------------------------------------- /lab4/lab4_4_labs_requirement.md: -------------------------------------------------------------------------------- 1 | ## 实验报告要求 2 | 3 | 从 git server 网站上取得 ucore_lab 后,进入目录 labcodes/lab4,完成实验要求的各个练习。在实验报告中回答所有练习中提出的问题。 4 | 在目录 labcodes/lab4 下存放实验报告,实验报告文档命名为 lab4.md,使用**markdown**格式。 5 | 对于 lab4 中编程任务,完成编写之后,再通过 git push 命令把代码同步回 git server 网站。最后请一定提前或按时提交到 git server 网站。 6 | 7 | 注意有“LAB4”的注释,代码中所有需要完成的地方(challenge 除外)都有“LAB4”和“YOUR CODE”的注释,请在提交时特别注意保持注释,并将“YOUR CODE”替换为自己的学号,并且将所有标有对应注释的部分填上正确的代码。 8 | -------------------------------------------------------------------------------- /lab4/lab4_5_appendix_a.md: -------------------------------------------------------------------------------- 1 | ## 附录 A:实验四的参考输出如下: 2 | 3 | ``` 4 | make qemu 5 | (THU.CST) os is loading ... 6 | 7 | Special kernel symbols: 8 | entry 0xc010002c (phys) 9 | etext 0xc010d0f7 (phys) 10 | edata 0xc012dad0 (phys) 11 | end 0xc0130e78 (phys) 12 | Kernel executable memory footprint: 196KB 13 | memory management: default_pmm_manager 14 | e820map: 15 | memory: 0009f400, [00000000, 0009f3ff], type = 1. 16 | memory: 00000c00, [0009f400, 0009ffff], type = 2. 17 | memory: 00010000, [000f0000, 000fffff], type = 2. 18 | memory: 07efd000, [00100000, 07ffcfff], type = 1. 19 | memory: 00003000, [07ffd000, 07ffffff], type = 2. 20 | memory: 00040000, [fffc0000, ffffffff], type = 2. 21 | check_alloc_page() succeeded! 22 | check_pgdir() succeeded! 23 | check_boot_pgdir() succeeded! 24 | -------------------- BEGIN -------------------- 25 | PDE(0e0) c0000000-f8000000 38000000 urw 26 | |-- PTE(38000) c0000000-f8000000 38000000 -rw 27 | PDE(001) fac00000-fb000000 00400000 -rw 28 | |-- PTE(000e0) faf00000-fafe0000 000e0000 urw 29 | |-- PTE(00001) fafeb000-fafec000 00001000 -rw 30 | --------------------- END --------------------- 31 | check_slab() succeeded! 32 | kmalloc_init() succeeded! 33 | check_vma_struct() succeeded! 34 | page fault at 0x00000100: K/W [no page found]. 35 | check_pgfault() succeeded! 36 | check_vmm() succeeded. 37 | ide 0: 10000(sectors), 'QEMU HARDDISK'. 38 | ide 1: 262144(sectors), 'QEMU HARDDISK'. 39 | SWAP: manager = fifo swap manager 40 | BEGIN check_swap: count 1, total 31944 41 | mm->sm_priv c0130e64 in fifo_init_mm 42 | setup Page Table for vaddr 0X1000, so alloc a page 43 | setup Page Table vaddr 0~4MB OVER! 44 | set up init env for check_swap begin! 45 | page fault at 0x00001000: K/W [no page found]. 46 | page fault at 0x00002000: K/W [no page found]. 47 | page fault at 0x00003000: K/W [no page found]. 48 | page fault at 0x00004000: K/W [no page found]. 49 | set up init env for check_swap over! 50 | write Virt Page c in fifo_check_swap 51 | write Virt Page a in fifo_check_swap 52 | write Virt Page d in fifo_check_swap 53 | write Virt Page b in fifo_check_swap 54 | write Virt Page e in fifo_check_swap 55 | page fault at 0x00005000: K/W [no page found]. 56 | swap_out: i 0, store page in vaddr 0x1000 to disk swap entry 2 57 | write Virt Page b in fifo_check_swap 58 | write Virt Page a in fifo_check_swap 59 | page fault at 0x00001000: K/W [no page found]. 60 | swap_out: i 0, store page in vaddr 0x2000 to disk swap entry 3 61 | swap_in: load disk swap entry 2 with swap_page in vadr 0x1000 62 | write Virt Page b in fifo_check_swap 63 | page fault at 0x00002000: K/W [no page found]. 64 | swap_out: i 0, store page in vaddr 0x3000 to disk swap entry 4 65 | swap_in: load disk swap entry 3 with swap_page in vadr 0x2000 66 | write Virt Page c in fifo_check_swap 67 | page fault at 0x00003000: K/W [no page found]. 68 | swap_out: i 0, store page in vaddr 0x4000 to disk swap entry 5 69 | swap_in: load disk swap entry 4 with swap_page in vadr 0x3000 70 | write Virt Page d in fifo_check_swap 71 | page fault at 0x00004000: K/W [no page found]. 72 | swap_out: i 0, store page in vaddr 0x5000 to disk swap entry 6 73 | swap_in: load disk swap entry 5 with swap_page in vadr 0x4000 74 | check_swap() succeeded! 75 | ++ setup timer interrupts 76 | this initproc, pid = 1, name = "init" 77 | To U: "Hello world!!". 78 | To U: "en.., Bye, Bye. :)" 79 | kernel panic at kern/process/proc.c:316: 80 | process exit!!. 81 | 82 | Welcome to the kernel debug monitor!! 83 | Type 'help' for a list of commands. 84 | K> 85 | ``` 86 | -------------------------------------------------------------------------------- /lab4/lab4_6_appendix_b.md: -------------------------------------------------------------------------------- 1 | ## 附录 B:【原理】进程的属性与特征解析 2 | 3 | 操作系统负责进程管理,即从程序加载到运行结束的全过程,这个程序运行过程将经历从“出生”到“死亡”的完整“生命”历程。所谓“进程”就是指这个程序运行的整个执行过程。为了记录、描述和管理程序执行的动态变化过程,需要有一个数据结构,这就是进程控制块。进程与进程控制块是一一对应的。为此,ucore 需要建立合适的进程控制块数据结构,并基于进程控制块来完成对进程的管理。 4 | 5 | 为了让多个程序能够使用 CPU 执行任务,需要设计用于进程管理的内核数据结构“进程控制块”。但到底如何设计进程控制块,如何管理进程?如果对进程的属性和特征了解不够,则无法有效地设计进程控制块和实现进程管理。 6 | 7 | 再一次回到进程的定义:一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。这里有四个关键词:程序、数据集合、执行和动态执行过程。从 CPU 的角度来看,所谓程序就是一段特定的指令机器码序列而已。CPU 会一条一条地取出在内存中程序的指令并按照指令的含义执行各种功能;所谓数据集合就是使用的内存;所谓执行就是让 CPU 工作。这个数据集合和执行其实体现了进程对资源的占用。动态执行过程体现了程序执行的不同“生命”阶段:诞生、工作、休息/等待、死亡。如果这一段指令执行完毕,也就意味着进程结束了。从开始执行到执行结束是一个进程的全过程。那么操作系统需要管理进程的什么?如果计算机系统中只有一个进程,那操作系统的工作就简单了。进程管理就是管理进程执行的指令,进程占用的资源,进程执行的状态。这可归结为对一个进程内的管理工作。但实际上在计算机系统的内存中,可以放很多程序,这也就意味着操作系统需要管理多个进程,那么,为了协调各进程对系统资源的使用,进程管理还需要做一些与进程协调有关的其他管理工作,包括进程调度、进程间的数据共享、进程间执行的同步互斥关系(后续相关实验涉及)等。下面逐一进行解析。 8 | 9 | #### 1. 资源管理 10 | 11 | 在计算机系统中,进程会占用内存和 CPU,这都是有限的资源,如果不进行合理的管理,资源会耗尽或无法高效公平地使用,从而会导致计算机系统中的多个进程执行效率很低,甚至由于资源不够而无法正常执行。 12 | 13 | 对于用户进程而言,操作系统是它的“上帝”,操作系统给了用户进程可以运行所需的资源,最基本的资源就是内存和 CPU。在实验二/三中涉及的内存管理方法和机制可直接应用到进程的内存资源管理中来。在有多个进程存在的情况下,对于 CPU 这种资源,则需要通过进程调度来合理选择一个进程,并进一步通过进程分派和进程切换让不同的进程分时复用 CPU,执行各自的工作。对于无法剥夺的共享资源,如果资源管理不当,多个进程会出现死锁或饥饿现象。 14 | 15 | #### 2. 进程状态管理 16 | 17 | 用户进程有不同的状态(可理解为“生命”的不同阶段),当操作系统把程序的放到内存中后,这个进程就“诞生”了,不过还没有开始执行,但已经消耗了内存资源,处于“创建”状态;当进程准备好各种资源,就等能够使用 CPU 时,进程处于“就绪”状态;当进程终于占用 CPU,程序的指令被 CPU 一条一条执行的时候,这个进程就进入了“运行”状态,这时除了继续占用内存资源外,还占用了 CPU 资源;当进程由于等待某个资源而无法继续执行时,进程可放弃 CPU 使用,即释放 CPU 资源,进入“等待”状态;当程序指令执行完毕,由操作系统回收进程所占用的资源时,进程进入了“死亡”状态。 18 | 19 | 这些进程状态的转换时机需要操作系统管理起来,而且进程的创建和清除等服务必须由操作系统提供,而且在“运行”与“就绪”/“等待”状态之间的转换,涉及到保存和恢复进程的“执行现场”,也就是进程上下文,这是确保进程即使“断断续续”地执行,也能正确完成工作的必要保证。 20 | 21 | #### 3. 进程与线程 22 | 23 | 一个进程拥有一个存放程序和数据的的虚拟地址空间以及其他资源。一个进程基于程序的指令流执行,其执行过程可能与其它进程的执行过程交替进行。因此,一个具有执行状态(运行态、就绪态等)的进程是一个被操作系统分配资源(比如分配内存)并调度(比如分时使用 CPU)的单位。在大多数操作系统中,这两个特点是进程的主要本质特征。但这两个特征相对独立,操作系统可以把这两个特征分别进行管理。 24 | 25 | 这样可以把拥有资源所有权的单位通常仍称作进程,对资源的管理成为进程管理;把指令执行流的单位称为线程,对线程的管理就是线程调度和线程分派。对属于同一进程的所有线程而言,这些线程共享进程的虚拟地址空间和其他资源,但每个线程都有一个独立的栈,还有独立的线程运行上下文,用于包含表示线程执行现场的寄存器值等信息。 26 | 27 | 在多线程环境中,进程被定义成资源分配与保护的单位,与进程相关联的信息主要有存放进程映像的虚拟地址空间等。在一个进程中,可能有一个或多个线程,每个线程有线程执行状态(运行、就绪、等待等),保存上次运行时的线程上下文、线程的执行栈等。考虑到 CPU 有不同的特权模式,参照进程的分类,线程又可进一步细化为用户线程和内核线程。 28 | 29 | 到目前为止,我们就可以明确用户进程、内核进程(可把 ucore 看成一个内核进程)、用户线程、内核线程的区别了。从本质上看,线程就是一个特殊的不用拥有资源的轻量级进程,在 ucore 的调度和执行管理中,并没有区分线程和进程。且由于 ucore 内核中的所有内核线程共享一个内核地址空间和其他资源,所以这些内核线程从属于同一个唯一的内核进程,即 ucore 内核本身。理解了进程或线程的上述属性和特征,就可以进行进程/线程管理的设计与实现了。但是为了叙述上的简便,以下用户态的进程/线程统称为用户进程。 30 | -------------------------------------------------------------------------------- /lab5.md: -------------------------------------------------------------------------------- 1 | # 实验五:用户进程管理 2 | -------------------------------------------------------------------------------- /lab5/lab5_1_goals.md: -------------------------------------------------------------------------------- 1 | ## 实验目的 2 | 3 | - 了解第一个用户进程创建过程 4 | - 了解系统调用框架的实现机制 5 | - 了解 ucore 如何实现系统调用 sys_fork/sys_exec/sys_exit/sys_wait 来进行进程管理 6 | -------------------------------------------------------------------------------- /lab5/lab5_2_1_exercises.md: -------------------------------------------------------------------------------- 1 | ### 练习 2 | 3 | 对实验报告的要求: 4 | 5 | - 基于 markdown 格式来完成,以文本方式为主 6 | - 填写各个基本练习中要求完成的报告内容 7 | - 完成实验后,请分析 ucore_lab 中提供的参考答案,并请在实验报告中说明你的实现与参考答案的区别 8 | - 列出你认为本实验中重要的知识点,以及与对应的 OS 原理中的知识点,并简要说明你对二者的含义,关系,差异等方面的理解(也可能出现实验中的知识点没有对应的原理知识点) 9 | - 列出你认为 OS 原理中很重要,但在实验中没有对应上的知识点 10 | 11 | #### 练习 0:填写已有实验 12 | 13 | 本实验依赖实验 1/2/3/4。请把你做的实验 1/2/3/4 的代码填入本实验中代码中有“LAB1”/“LAB2”/“LAB3”/“LAB4”的注释相应部分。注意:为了能够正确执行 lab5 的测试应用程序,可能需对已完成的实验 1/2/3/4 的代码进行进一步改进。 14 | 15 | #### 练习 1: 加载应用程序并执行(需要编码) 16 | 17 | **do_execv**函数调用 load_icode(位于 kern/process/proc.c 中)来加载并解析一个处于内存中的 ELF 执行文件格式的应用程序,建立相应的用户内存空间来放置应用程序的代码段、数据段等,且要设置好 proc_struct 结构中的成员变量 trapframe 中的内容,确保在执行此进程后,能够从应用程序设定的起始执行地址开始执行。需设置正确的 trapframe 内容。 18 | 19 | 请在实验报告中简要说明你的设计实现过程。 20 | 21 | 请在实验报告中描述当创建一个用户态进程并加载了应用程序后,CPU 是如何让这个应用程序最终在用户态执行起来的。即这个用户态进程被 ucore 选择占用 CPU 执行(RUNNING 态)到具体执行应用程序第一条指令的整个经过。 22 | 23 | #### 练习 2: 父进程复制自己的内存空间给子进程(需要编码) 24 | 25 | 创建子进程的函数 do_fork 在执行中将拷贝当前进程(即父进程)的用户内存地址空间中的合法内容到新进程中(子进程),完成内存资源的复制。具体是通过 copy_range 函数(位于 kern/mm/pmm.c 中)实现的,请补充 copy_range 的实现,确保能够正确执行。 26 | 27 | 请在实验报告中简要说明如何设计实现”Copy on Write 机制“,给出概要设计,鼓励给出详细设计。 28 | 29 | > Copy-on-write(简称 COW)的基本概念是指如果有多个使用者对一个资源 A(比如内存块)进行读操作,则每个使用者只需获得一个指向同一个资源 A 的指针,就可以该资源了。若某使用者需要对这个资源 A 进行写操作,系统会对该资源进行拷贝操作,从而使得该“写操作”使用者获得一个该资源 A 的“私有”拷贝—资源 B,可对资源 B 进行写操作。该“写操作”使用者对资源 B 的改变对于其他的使用者而言是不可见的,因为其他使用者看到的还是资源 A。 30 | 31 | #### 练习 3: 阅读分析源代码,理解进程执行 fork/exec/wait/exit 的实现,以及系统调用的实现(不需要编码) 32 | 33 | 请在实验报告中简要说明你对 fork/exec/wait/exit 函数的分析。并回答如下问题: 34 | 35 | - 请分析 fork/exec/wait/exit 在实现中是如何影响进程的执行状态的? 36 | - 请给出 ucore 中一个用户态进程的执行状态生命周期图(包执行状态,执行状态之间的变换关系,以及产生变换的事件或函数调用)。(字符方式画即可) 37 | 38 | 执行:make grade。如果所显示的应用程序检测都输出 ok,则基本正确。(使用的是 qemu-1.0.1) 39 | 40 | #### 扩展练习 Challenge :实现 Copy on Write (COW)机制 41 | 42 | 给出实现源码,测试用例和设计报告(包括在 cow 情况下的各种状态转换(类似有限状态自动机)的说明)。 43 | 44 | 这个扩展练习涉及到本实验和上一个实验“虚拟内存管理”。在 ucore 操作系统中,当一个用户父进程创建自己的子进程时,父进程会把其申请的用户空间设置为只读,子进程可共享父进程占用的用户内存空间中的页面(这就是一个共享的资源)。当其中任何一个进程修改此用户内存空间中的某页面时,ucore 会通过 page fault 异常获知该操作,并完成拷贝内存页面,使得两个进程都有各自的内存页面。这样一个进程所做的修改不会被另外一个进程可见了。请在 ucore 中实现这样的 COW 机制。 45 | 46 | 由于 COW 实现比较复杂,容易引入 bug,请参考 https://dirtycow.ninja/   看看能否在 ucore 的 COW 实现中模拟这个错误和解决方案。需要有解释。 47 | 48 | 这是一个 big challenge. 49 | -------------------------------------------------------------------------------- /lab5/lab5_2_2_files.md: -------------------------------------------------------------------------------- 1 | ### 项目组成 2 | 3 | ``` 4 | ├── boot 5 | ├── kern 6 | │ ├── debug 7 | │ │ ├── kdebug.c 8 | │ │ └── …… 9 | │ ├── mm 10 | │ │ ├── memlayout.h 11 | │ │ ├── pmm.c 12 | │ │ ├── pmm.h 13 | │ │ ├── ...... 14 | │ │ ├── vmm.c 15 | │ │ └── vmm.h 16 | │ ├── process 17 | │ │ ├── proc.c 18 | │ │ ├── proc.h 19 | │ │ └── ...... 20 | │ ├── schedule 21 | │ │ ├── sched.c 22 | │ │ └── ...... 23 | │ ├── sync 24 | │ │ └── sync.h 25 | │ ├── syscall 26 | │ │ ├── syscall.c 27 | │ │ └── syscall.h 28 | │ └── trap 29 | │ ├── trap.c 30 | │ ├── trapentry.S 31 | │ ├── trap.h 32 | │ └── vectors.S 33 | ├── libs 34 | │ ├── elf.h 35 | │ ├── error.h 36 | │ ├── printfmt.c 37 | │ ├── unistd.h 38 | │ └── ...... 39 | ├── tools 40 | │ ├── user.ld 41 | │ └── ...... 42 | └── user 43 | ├── hello.c 44 | ├── libs 45 | │ ├── initcode.S 46 | │ ├── syscall.c 47 | │ ├── syscall.h 48 | │ └── ...... 49 | └── ...... 50 | ``` 51 | 52 | 相对与实验四,实验五主要增加的文件如上表红色部分所示,主要修改的文件如上表紫色部分所示。主要改动如下: 53 | 54 | ◆ kern/debug/ 55 | 56 | kdebug.c:修改:解析用户进程的符号信息表示(可不用理会) 57 | 58 | ◆ kern/mm/ (与本次实验有较大关系) 59 | 60 | memlayout.h:修改:增加了用户虚存地址空间的图形表示和宏定义 (需仔细理解)。 61 | 62 | pmm.[ch]:修改:添加了用于进程退出(do_exit)的内存资源回收的 page_remove_pte、unmap_range、exit_range 函数和用于创建子进程(do_fork)中拷贝父进程内存空间的 copy_range 函数,修改了 pgdir_alloc_page 函数 63 | 64 | vmm.[ch]:修改:扩展了 mm_struct 数据结构,增加了一系列函数 65 | 66 | - mm_map/dup_mmap/exit_mmap:设定/取消/复制/删除用户进程的合法内存空间 67 | 68 | - copy_from_user/copy_to_user:用户内存空间内容与内核内存空间内容的相互拷贝的实现 69 | 70 | - user_mem_check:搜索 vma 链表,检查是否是一个合法的用户空间范围 71 | 72 | ◆ kern/process/ (与本次实验有较大关系) 73 | 74 | proc.[ch]:修改:扩展了 proc_struct 数据结构。增加或修改了一系列函数 75 | 76 | - setup_pgdir/put_pgdir:创建并设置/释放页目录表 77 | 78 | - copy_mm:复制用户进程的内存空间和设置相关内存管理(如页表等)信息 79 | 80 | - do_exit:释放进程自身所占内存空间和相关内存管理(如页表等)信息所占空间,唤醒父进程,好让父进程收了自己,让调度器切换到其他进程 81 | 82 | - load_icode:被 do_execve 调用,完成加载放在内存中的执行程序到进程空间,这涉及到对页表等的修改,分配用户栈 83 | 84 | - do_execve:先回收自身所占用户空间,然后调用 load_icode,用新的程序覆盖内存空间,形成一个执行新程序的新进程 85 | 86 | - do_yield:让调度器执行一次选择新进程的过程 87 | 88 | - do_wait:父进程等待子进程,并在得到子进程的退出消息后,彻底回收子进程所占的资源(比如子进程的内核栈和进程控制块) 89 | 90 | - do_kill:给一个进程设置 PF_EXITING 标志(“kill”信息,即要它死掉),这样在 trap 函数中,将根据此标志,让进程退出 91 | 92 | - KERNEL_EXECVE/\_\_KERNEL_EXECVE/\_\_KERNEL_EXECVE2:被 user_main 调用,执行一用户进程 93 | 94 | ◆ kern/trap/ 95 | 96 | trap.c:修改:在 idt_init 函数中,对 IDT 初始化时,设置好了用于系统调用的中断门(idt[T\_SYSCALL])信息。这主要与 syscall 的实现相关 97 | 98 | ◆ user/\* 99 | 100 | 新增的用户程序和用户库 101 | -------------------------------------------------------------------------------- /lab5/lab5_2_lab2.md: -------------------------------------------------------------------------------- 1 | ## 实验内容 2 | 3 | 实验 4 完成了内核线程,但到目前为止,所有的运行都在内核态执行。实验 5 将创建用户进程,让用户进程在用户态执行,且在需要 ucore 支持时,可通过系统调用来让 ucore 提供服务。为此需要构造出第一个用户进程,并通过系统调用 sys_fork/sys_exec/sys_exit/sys_wait 来支持运行不同的应用程序,完成对用户进程的执行过程的基本管理。相关原理介绍可看附录 B。 4 | -------------------------------------------------------------------------------- /lab5/lab5_3_1_lab_steps.md: -------------------------------------------------------------------------------- 1 | ### 实验执行流程概述 2 | 3 | 到实验四为止,ucore 还一直在核心态“打转”,没有到用户态执行。提供各种操作系统功能的内核线程只能在 CPU 核心态运行是操作系统自身的要求,操作系统就要呆在核心态,才能管理整个计算机系统。但应用程序员也需要编写各种应用软件,且要在计算机系统上运行。如果把这些应用软件都作为内核线程来执行,那系统的安全性就无法得到保证了。所以,ucore 要提供用户态进程的创建和执行机制,给应用程序执行提供一个用户态运行环境。接下来我们就简要分析本实验的执行过程,以及分析用户进程的整个生命周期来阐述用户进程管理的设计与实现。 4 | 5 | 显然,由于进程的执行空间扩展到了用户态空间,且出现了创建子进程执行应用程序等与 lab4 有较大不同的地方,所以具体实现的不同主要集中在进程管理和内存管理部分。首先,我们从 ucore 的初始化部分来看,会发现初始化的总控函数 kern_init 没有任何变化。但这并不意味着 lab4 与 lab5 差别不大。其实 kern_init 调用的物理内存初始化,进程管理初始化等都有一定的变化。 6 | 7 | 在内存管理部分,与 lab4 最大的区别就是增加用户态虚拟内存的管理。为了管理用户态的虚拟内存,需要对页表的内容进行扩展,能够把部分物理内存映射为用户态虚拟内存。如果某进程执行过程中,CPU 在用户态下执行(在 CS 段寄存器最低两位包含有一个 2 位的优先级域,如果为 0,表示 CPU 运行在特权态;如果为 3,表示 CPU 运行在用户态。),则可以访问本进程页表描述的用户态虚拟内存,但由于权限不够,不能访问内核态虚拟内存。另一方面,不同的进程有各自的页表,所以即使不同进程的用户态虚拟地址相同,但由于页表把虚拟页映射到了不同的物理页帧,所以不同进程的虚拟内存空间是被隔离开的,相互之间无法直接访问。在用户态内存空间和内核态内核空间之间需要拷贝数据,让 CPU 处在内核态才能完成对用户空间的读或写,为此需要设计专门的拷贝函数(copy_from_user 和 copy_to_user)完成。但反之则会导致违反 CPU 的权限管理,导致内存访问异常。 8 | 9 | 在进程管理方面,主要涉及到的是进程控制块中与内存管理相关的部分,包括建立进程的页表和维护进程可访问空间(可能还没有建立虚实映射关系)的信息;加载一个 ELF 格式的程序到进程控制块管理的内存中的方法;在进程复制(fork)过程中,把父进程的内存空间拷贝到子进程内存空间的技术。另外一部分与用户态进程生命周期管理相关,包括让进程放弃 CPU 而睡眠等待某事件;让父进程等待子进程结束;一个进程杀死另一个进程;给进程发消息;建立进程的血缘关系链表。 10 | 11 | 当实现了上述内存管理和进程管理的需求后,接下来 ucore 的用户进程管理工作就比较简单了。首先,“硬”构造出第一个进程(lab4 中已有描述),它是后续所有进程的祖先;然后,在 proc_init 函数中,通过 alloc 把当前 ucore 的执行环境转变成 idle 内核线程的执行现场;然后调用 kernl_thread 来创建第二个内核线程 init_main,而 init_main 内核线程有创建了 user_main 内核线程.。到此,内核线程创建完毕,应该开始用户进程的创建过程,这第一步实际上是通过 user_main 函数调用 kernel_tread 创建子进程,通过 kernel_execve 调用来把某一具体程序的执行内容放入内存。具体的放置方式是根据 ld 在此文件上的地址分配为基本原则,把程序的不同部分放到某进程的用户空间中,从而通过此进程来完成程序描述的任务。一旦执行了这一程序对应的进程,就会从内核态切换到用户态继续执行。以此类推,CPU 在用户空间执行的用户进程,其地址空间不会被其他用户的进程影响,但由于系统调用(用户进程直接获得操作系统服务的唯一通道)、外设中断和异常中断的会随时产生,从而间接推动了用户进程实现用户态到到内核态的切换工作。ucore 对 CPU 内核态与用户态的切换过程需要比较仔细地分析(这其实是实验一的扩展练习)。当进程执行结束后,需回收进程占用和没消耗完毕的设备整个过程,且为新的创建进程请求提供服务。在本实验中,当系统中存在多个进程或内核线程时,ucore 采用了一种 FIFO 的很简单的调度方法来管理每个进程占用 CPU 的时间和频度等。在 ucore 运行过程中,由于调度、时间中断、系统调用等原因,使得进程会进行切换、创建、睡眠、等待、发消息等各种不同的操作,周而复始,生生不息。 12 | -------------------------------------------------------------------------------- /lab5/lab5_3_3_process_exit_wait.md: -------------------------------------------------------------------------------- 1 | ### 进程退出和等待进程 2 | 3 | 当进程执行完它的工作后,就需要执行退出操作,释放进程占用的资源。ucore 分了两步来完成这个工作,首先由进程本身完成大部分资源的占用内存回收工作,然后由此进程的父进程完成剩余资源占用内存的回收工作。为何不让进程本身完成所有的资源回收工作呢?这是因为进程要执行回收操作,就表明此进程还存在,还在执行指令,这就需要内核栈的空间不能释放,且表示进程存在的进程控制块不能释放。所以需要父进程来帮忙释放子进程无法完成的这两个资源回收工作。 4 | 5 | 为此在用户态的函数库中提供了 exit 函数,此函数最终访问 sys_exit 系统调用接口让操作系统来帮助当前进程执行退出过程中的部分资源回收。我们来看看 ucore 是如何做进程退出工作的。 6 | 7 | 首先,exit 函数会把一个退出码 error_code 传递给 ucore,ucore 通过执行内核函数 do_exit 来完成对当前进程的退出处理,主要工作简单地说就是回收当前进程所占的大部分内存资源,并通知父进程完成最后的回收工作,具体流程如下: 8 | 9 | **1.** 如果 current-\>mm != NULL,表示是用户进程,则开始回收此用户进程所占用的用户态虚拟内存空间; 10 | 11 | a) 12 | 首先执行“lcr3(boot_cr3)”,切换到内核态的页表上,这样当前用户进程目前只能在内核虚拟地址空间执行了,这是为了确保后续释放用户态内存和进程页表的工作能够正常执行; 13 | 14 | b) 15 | 如果当前进程控制块的成员变量 mm 的成员变量 mm_count 减 1 后为 0(表明这个 mm 没有再被其他进程共享,可以彻底释放进程所占的用户虚拟空间了。),则开始回收用户进程所占的内存资源: 16 | 17 | i. 18 | 调用 exit_mmap 函数释放 current-\>mm-\>vma 链表中每个 vma 描述的进程合法空间中实际分配的内存,然后把对应的页表项内容清空,最后还把页表所占用的空间释放并把对应的页目录表项清空; 19 | 20 | ii. 调用 put_pgdir 函数释放当前进程的页目录所占的内存; 21 | 22 | iii. 调用 mm_destroy 函数释放 mm 中的 vma 所占内存,最后释放 mm 所占内存; 23 | 24 | c) 25 | 此时设置 current-\>mm 为 NULL,表示与当前进程相关的用户虚拟内存空间和对应的内存管理成员变量所占的内核虚拟内存空间已经回收完毕; 26 | 27 | **2.** 28 | 这时,设置当前进程的执行状态 current-\>state=PROC_ZOMBIE,当前进程的退出码 current-\>exit_code=error_code。此时当前进程已经不能被调度了,需要此进程的父进程来做最后的回收工作(即回收描述此进程的内核栈和进程控制块); 29 | 30 | **3.** 如果当前进程的父进程 current-\>parent 处于等待子进程状态: 31 | 32 | current-\>parent-\>wait_state==WT_CHILD, 33 | 34 | 则唤醒父进程(即执行“wakup_proc(current-\>parent)”),让父进程帮助自己完成最后的资源回收; 35 | 36 | **4.** 37 | 如果当前进程还有子进程,则需要把这些子进程的父进程指针设置为内核线程 initproc,且各个子进程指针需要插入到 initproc 的子进程链表中。如果某个子进程的执行状态是 PROC_ZOMBIE,则需要唤醒 initproc 来完成对此子进程的最后回收工作。 38 | 39 | **5.** 执行 schedule()函数,选择新的进程执行。 40 | 41 | 那么父进程如何完成对子进程的最后回收工作呢?这要求父进程要执行 wait 用户函数或 wait_pid 用户函数,这两个函数的区别是,wait 函数等待任意子进程的结束通知,而 wait_pid 函数等待进程 id 号为 pid 的子进程结束通知。这两个函数最终访问 sys_wait 系统调用接口让 ucore 来完成对子进程的最后回收工作,即回收子进程的内核栈和进程控制块所占内存空间,具体流程如下: 42 | 43 | **1.** 44 | 如果 pid!=0,表示只找一个进程 id 号为 pid 的退出状态的子进程,否则找任意一个处于退出状态的子进程; 45 | 46 | **2.** 47 | 如果此子进程的执行状态不为 PROC_ZOMBIE,表明此子进程还没有退出,则当前进程只好设置自己的执行状态为 PROC_SLEEPING,睡眠原因为 WT_CHILD(即等待子进程退出),调用 schedule()函数选择新的进程执行,自己睡眠等待,如果被唤醒,则重复跳回步骤 1 处执行; 48 | 49 | **3.** 50 | 如果此子进程的执行状态为 PROC_ZOMBIE,表明此子进程处于退出状态,需要当前进程(即子进程的父进程)完成对子进程的最终回收工作,即首先把子进程控制块从两个进程队列 proc_list 和 hash_list 中删除,并释放子进程的内核堆栈和进程控制块。自此,子进程才彻底地结束了它的执行过程,消除了它所占用的所有资源。 51 | -------------------------------------------------------------------------------- /lab5/lab5_3_user_process.md: -------------------------------------------------------------------------------- 1 | ## 用户进程管理 2 | -------------------------------------------------------------------------------- /lab5/lab5_4_lab_requirement.md: -------------------------------------------------------------------------------- 1 | ## 实验报告要求 2 | 3 | 从 git server 网站上取得 ucore_lab 后,进入目录 labcodes/lab5,完成实验要求的各个练习。 4 | 在实验报告中回答所有练习中提出的问题。在目录 labcodes/lab5 下存放实验报告,实验报告文档命名为 lab5.md,使用**markdown**格式。 5 | 对于 lab5 中编程任务,完成编写之后,再通过 git push 命令把代码同步回 git server 网站。最后请一定提前或按时提交到 git server 网站。 6 | 7 | 注意有“LAB5”的注释,代码中所有需要完成的地方(challenge 除外)都有“LAB5”和“YOUR CODE”的注释,请在提交时特别注意保持注释,并将“YOUR CODE”替换为自己的学号,并且将所有标有对应注释的部分填上正确的代码。 8 | -------------------------------------------------------------------------------- /lab5_figs/image001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab5_figs/image001.png -------------------------------------------------------------------------------- /lab6.md: -------------------------------------------------------------------------------- 1 | # 实验六: 调度器 2 | -------------------------------------------------------------------------------- /lab6/hehe: -------------------------------------------------------------------------------- 1 | *[] (lab6/lab6_1_goals.md) 2 | *[] (lab6/lab6_2_1_exercises.md) 3 | *[] (lab6/lab6_2_2_files.md) 4 | *[] (lab6/lab6_2_labs.md) 5 | *[] (lab6/lab6_3_1_exercises.md) 6 | *[] (lab6/lab6_3_2_scheduler_implement.md) 7 | *[] (lab6/lab6_3_3_process_state.md) 8 | *[] (lab6/lab6_3_4_1_kernel_preempt_point.md) 9 | *[] (lab6/lab6_3_4_2_process_switch.md) 10 | *[] (lab6/lab6_3_4_process_implement.md) 11 | *[] (lab6/lab6_3_5_1_designed.md) 12 | *[] (lab6/lab6_3_5_2_data_structure.md) 13 | *[] (lab6/lab6_3_5_3_scheduler_point_functions.md) 14 | *[] (lab6/lab6_3_5_4_RR.md) 15 | *[] (lab6/lab6_3_5_scheduler_framework.md) 16 | *[] (lab6/lab6_3_6_1_basic_method.md) 17 | *[] (lab6/lab6_3_6_2_priority_queue.md) 18 | *[] (lab6/lab6_3_6_stride_scheduling.md) 19 | *[] (lab6/lab6_3_scheduler_design.md) 20 | *[] (lab6/lab6_4_labs_requirement.md) 21 | -------------------------------------------------------------------------------- /lab6/lab6_1_goals.md: -------------------------------------------------------------------------------- 1 | ## 实验目的 2 | 3 | - 理解操作系统的调度管理机制 4 | - 熟悉 ucore 的系统调度器框架,以及缺省的 Round-Robin 调度算法 5 | - 基于调度器框架实现一个(Stride Scheduling)调度算法来替换缺省的调度算法 6 | -------------------------------------------------------------------------------- /lab6/lab6_2_1_exercises.md: -------------------------------------------------------------------------------- 1 | ### 练习 2 | 3 | 对实验报告的要求: 4 | 5 | - 基于 markdown 格式来完成,以文本方式为主 6 | - 填写各个基本练习中要求完成的报告内容 7 | - 完成实验后,请分析 ucore_lab 中提供的参考答案,并请在实验报告中说明你的实现与参考答案的区别 8 | - 列出你认为本实验中重要的知识点,以及与对应的 OS 原理中的知识点,并简要说明你对二者的含义,关系,差异等方面的理解(也可能出现实验中的知识点没有对应的原理知识点) 9 | - 列出你认为 OS 原理中很重要,但在实验中没有对应上的知识点 10 | 11 | #### 练习 0:填写已有实验 12 | 13 | 本实验依赖实验 1/2/3/4/5。请把你做的实验 2/3/4/5 的代码填入本实验中代码中有“LAB1”/“LAB2”/“LAB3”/“LAB4”“LAB5”的注释相应部分。并确保编译通过。注意:为了能够正确执行 lab6 的测试应用程序,可能需对已完成的实验 1/2/3/4/5 的代码进行进一步改进。 14 | 15 | #### 练习 1: 使用 Round Robin 调度算法(不需要编码) 16 | 17 | 完成练习 0 后,建议大家比较一下(可用 kdiff3 等文件比较软件)个人完成的 lab5 和练习 0 完成后的刚修改的 lab6 之间的区别,分析了解 lab6 采用 RR 调度算法后的执行过程。执行 make grade,大部分测试用例应该通过。但执行 priority.c 应该过不去。 18 | 19 | 请在实验报告中完成: 20 | 21 | - 请理解并分析 sched_class 中各个函数指针的用法,并结合 Round Robin 调度算法描 ucore 的调度执行过程 22 | - 请在实验报告中简要说明如何设计实现”多级反馈队列调度算法“,给出概要设计,鼓励给出详细设计 23 | 24 | #### 练习 2: 实现 Stride Scheduling 调度算法(需要编码) 25 | 26 | 首先需要换掉 RR 调度器的实现,即用 default_sched_stride_c 覆盖 default_sched.c。然后根据此文件和后续文档对 Stride 度器的相关描述,完成 Stride 调度算法的实现。 27 | 28 | 后面的实验文档部分给出了 Stride 调度算法的大体描述。这里给出 Stride 调度算法的一些相关的资料(目前网上中文的资料比较欠缺)。 29 | 30 | - [strid-shed paper location1](http://wwwagss.informatik.uni-kl.de/Projekte/Squirrel/stride/node3.html) 31 | - [strid-shed paper location2](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.138.3502&rank=1) 32 | - 也可 GOOGLE “Stride Scheduling” 来查找相关资料 33 | 34 | 执行:make grade。如果所显示的应用程序检测都输出 ok,则基本正确。如果只是 priority.c 过不去,可执行 make run-priority 命令来单独调试它。大致执行结果可看附录。( 使用的是 qemu-1.0.1 )。 35 | 36 | 请在实验报告中简要说明你的设计实现过程。 37 | 38 | #### 扩展练习 Challenge 1 :实现 Linux 的 CFS 调度算法 39 | 40 | 在 ucore 的调度器框架下实现下 Linux 的 CFS 调度算法。可阅读相关 Linux 内核书籍或查询网上资料,可了解 CFS 的细节,然后大致实现在 ucore 中。 41 | 42 | #### 扩展练习 Challenge 2 :在 ucore 上实现尽可能多的各种基本调度算法(FIFO, SJF,...),并设计各种测试用例,能够定量地分析出各种调度算法在各种指标上的差异,说明调度算法的适用范围。 43 | -------------------------------------------------------------------------------- /lab6/lab6_2_2_files.md: -------------------------------------------------------------------------------- 1 | ### 项目组成 2 | 3 | ``` 4 | ├── boot 5 | ├── kern 6 | │ ├── debug 7 | │ ├── driver 8 | │ ├── fs 9 | │ ├── init 10 | │ ├── libs 11 | │ ├── mm 12 | │ ├── process 13 | │ │ ├── ..... 14 | │ │ ├── proc.c 15 | │ │ ├── proc.h 16 | │ │ └── switch.S 17 | │ ├── schedule 18 | │ │ ├── default\_sched.c 19 | │ │ ├── default\_sched.h 20 | │ │ ├── default\_sched\_stride\_c 21 | │ │ ├── sched.c 22 | │ │ └── sched.h 23 | │ ├── syscall 24 | │ │ ├── syscall.c 25 | │ │ └── syscall.h 26 | … 27 | ``` 28 | 29 | 相对与实验五,实验六主要增加的文件如上表红色部分所示,主要修改的文件如上表紫色部分所示。主要改动如下: 30 | 简单说明如下: 31 | 32 | - libs/skew_heap.h: 33 | 提供了基本的优先队列数据结构,为本次实验提供了抽象数据结构方面的支持。 34 | - kern/process/proc.[ch]:proc.h 中扩展了 proc_struct 的成员变量,用于 RR 和 stride 调度算法。proc.c 中实现了 lab6_set_priority,用于设置进程的优先级。 35 | - kern/schedule/{sched.h,sched.c}: 定义了 ucore 的调度器框架,其中包括相关的数据结构(包括调度器的接口和运行队列的结构),和具体的运行时机制。 36 | - kern/schedule/{default_sched.h,default_sched.c}: 具体的 round-robin 算法,在本次实验中你需要了解其实现。 37 | - kern/schedule/default_sched_stride_c: Stride Scheduling 调度器的基本框架,在此次实验中你需要填充其中的空白部分以实现一个完整的 Stride 调度器。 38 | - kern/syscall/syscall.[ch]: 增加了 sys_gettime 系统调用,便于用户进程获取当前时钟值;增加了 sys_lab6_set_priority 系统调用,便于用户进程设置进程优先级(给 priority.c 用) 39 | - user/{matrix.c,priority.c,. . . }: 相关的一些测试用户程序,测试调度算法的正确性,user 目录下包含但不限于这些程序。在完成实验过程中,建议阅读这些测试程序,以了解这些程序的行为,便于进行调试。 40 | -------------------------------------------------------------------------------- /lab6/lab6_2_labs.md: -------------------------------------------------------------------------------- 1 | ## 实验内容 2 | 3 | 实验五完成了用户进程的管理,可在用户态运行多个进程。但到目前为止,采用的调度策略是很简单的 FIFO 调度策略。本次实验,主要是熟悉 ucore 的系统调度器框架,以及基于此框架的 Round-Robin(RR) 调度算法。然后参考 RR 调度算法的实现,完成 Stride Scheduling 调度算法。 4 | -------------------------------------------------------------------------------- /lab6/lab6_3_1_exercises.md: -------------------------------------------------------------------------------- 1 | ### 实验执行流程概述 2 | 3 | 在实验五,创建了用户进程,并让它们正确运行。这中间也实现了 FIFO 调度策略。可通过阅读实验五下的 kern/schedule/sched.c 的 schedule 函数的实现来了解其 FIFO 调度策略。与实验五相比,实验六专门需要针对处理器调度框架和各种算法进行设计与实现,为此对 ucore 的调度部分进行了适当的修改,使得 kern/schedule/sched.c 只实现调度器框架,而不再涉及具体的调度算法实现。而调度算法在单独的文件(default_sched.[ch])中实现。 4 | 5 | 除此之外,实验中还涉及了 idle 进程的概念。当 cpu 没有进程可以执行的时候,系统应该如何工作?在实验五的 scheduler 实现中,ucore 内核不断的遍历进程池,直到找到第一个 runnable 状态的 process,调用并执行它。也就是说,当系统没有进程可以执行的时候,它会把所有 cpu 时间用在搜索进程池,以实现 idle 的目的。但是这样的设计不被大多数操作系统所采用,原因在于它将进程调度和 idle 进程两种不同的概念混在了一起,而且,当调度器比较复杂时,schedule 函数本身也会比较复杂,这样的设计结构很不清晰而且难免会出现错误。所以在此次实验中,ucore 建立了一个单独的进程(kern/process/proc.c 中的 idleproc)作为 cpu 空闲时的 idle 进程,这个程序是通常一个死循环。你需要了解这个程序的实现。 6 | 7 | 接下来可看看实验六的大致执行过程,在 init.c 中的 kern_init 函数增加了对 sched_init 函数的调用。sched_init 函数主要完成了对实现特定调度算法的调度类(sched_class)的绑定,使得 ucore 在后续的执行中,能够通过调度框架找到实现特定调度算法的调度类并完成进程调度相关工作。为了更好地理解实验六整个运行过程,这里需要关注的重点问题包括: 8 | 9 | 1. 何时或何事件发生后需要调度? 10 | 2. 何时或何事件发生后需要调整实现调度算法所涉及的参数? 11 | 3. 如果基于调度框架设计具体的调度算法? 12 | 4. 如果灵活应用链表等数据结构管理进程调度? 13 | 14 | 大家可带着这些问题进一步阅读后续的内容。 15 | -------------------------------------------------------------------------------- /lab6/lab6_3_3_process_state.md: -------------------------------------------------------------------------------- 1 | ### 进程状态 2 | 3 | 在此次实验中,进程的状态之间的转换需要有一个更为清晰的表述,在 ucore 中,runnable 的进程会被放在运行队列中。值得注意的是,在具体实现中,ucore 定义的进程控制块 struct proc_struct 包含了成员变量 state,用于描述进程的运行状态,而 running 和 runnable 共享同一个状态(state)值(PROC_RUNNABLE。不同之处在于处于 running 态的进程不会放在运行队列中。进程的正常生命周期如下: 4 | 5 | - 进程首先在 cpu 初始化或者 sys_fork 的时候被创建,当为该进程分配了一个进程控制块之后,该进程进入 uninit 态(在 proc.c 中 alloc_proc)。 6 | - 当进程完全完成初始化之后,该进程转为 runnable 态。 7 | - 当到达调度点时,由调度器 sched_class 根据运行队列 rq 的内容来判断一个进程是否应该被运行,即把处于 runnable 态的进程转换成 running 状态,从而占用 CPU 执行。 8 | - running 态的进程通过 wait 等系统调用被阻塞,进入 sleeping 态。 9 | - sleeping 态的进程被 wakeup 变成 runnable 态的进程。 10 | - running 态的进程主动 exit 变成 zombie 态,然后由其父进程完成对其资源的最后释放,子进程的进程控制块成为 unused。 11 | - 所有从 runnable 态变成其他状态的进程都要出运行队列,反之,被放入某个运行队列中。 12 | -------------------------------------------------------------------------------- /lab6/lab6_3_4_1_kernel_preempt_point.md: -------------------------------------------------------------------------------- 1 | #### 内核抢占点 2 | 3 | 调度本质上体现了对 CPU 资源的抢占。对于用户进程而言,由于有中断的产生,可以随时打断用户进程的执行,转到操作系统内部,从而给了操作系统以调度控制权,让操作系统可以根据具体情况(比如用户进程时间片已经用完了)选择其他用户进程执行。这体现了用户进程的可抢占性(preemptive)。但如果把 ucore 操作系统也看成是一个特殊的内核进程或多个内核线程的集合,那 ucore 是否也是可抢占的呢?其实 ucore 内核执行是不可抢占的(non-preemptive),即在执行“任意”内核代码时,CPU 控制权可被强制剥夺。这里需要注意,不是在所有情况下 ucore 内核执行都是不可抢占的,有以下几种“固定”情况是例外: 4 | 5 | 1. 进行同步互斥操作,比如争抢一个信号量、锁(lab7 中会详细分析); 6 | 2. 进行磁盘读写等耗时的异步操作,由于等待完成的耗时太长,ucore 会调用 shcedule 让其他就绪进程执行。 7 | 8 | 这几种情况其实都是由于当前进程所需的某个资源(也可称为事件)无法得到满足,无法继续执行下去,从而不得不主动放弃对 CPU 的控制权。如果参照用户进程任何位置都可被内核打断并放弃 CPU 控制权的情况,这些在内核中放弃 CPU 控制权的执行地点是“固定”而不是“任意”的,不能体现内核任意位置都可抢占性的特点。我们搜寻一下实验五的代码,可发现在如下几处地方调用了 shedule 函数: 9 | 10 | 表一:调用进程调度函数 schedule 的位置和原因 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 |
编号位置原因
1proc.c::do_exit用户线程执行结束,主动放弃CPU控制权。
2proc.c::do_wait用户线程等待子进程结束,主动放弃CPU控制权。
3proc.c::init_main1. initproc内核线程等待所有用户进程结束,如果没有结束,就主动放弃CPU控制权; 17 | 2. initproc内核线程在所有用户进程结束后,让kswapd内核线程执行10次,用于回收空闲内存资源
4proc.c::cpu_idleidleproc内核线程的工作就是等待有处于就绪态的进程或线程,如果有就调用schedule函数
5sync.h::lock在获取锁的过程中,如果无法得到锁,则主动放弃CPU控制权
6trap.c::trap如果在当前进程在用户态被打断去,且当前进程控制块的成员变量need_resched设置为1,则当前线程会放弃CPU控制权
22 | 23 | 仔细分析上述位置,第 1、2、5 处的执行位置体现了由于获取某种资源一时等不到满足、进程要退出、进程要睡眠等原因而不得不主动放弃 CPU。第 3、4 处的执行位置比较特殊,initproc 内核线程等待用户进程结束而执行 schedule 函数;idle 内核线程在没有进程处于就绪态时才执行,一旦有了就绪态的进程,它将执行 schedule 函数完成进程调度。这里只有第 6 处的位置比较特殊: 24 | 25 | ``` 26 | if (!in_kernel) { 27 | …… 28 | 29 | if (current->need_resched) { 30 | schedule(); 31 | } 32 | } 33 | ``` 34 | 35 | 这里表明了只有当进程在用户态执行到“任意”某处用户代码位置时发生了中断,且当前进程控制块成员变量 need_resched 为 1(表示需要调度了)时,才会执行 shedule 函数。这实际上体现了对用户进程的可抢占性。如果没有第一行的 if 语句,那么就可以体现对内核代码的可抢占性。但如果要把这一行 if 语句去掉,我们就不得不实现对 ucore 中的所有全局变量的互斥访问操作,以防止所谓的 racecondition 现象,这样 ucore 的实现复杂度会增加不少。 36 | -------------------------------------------------------------------------------- /lab6/lab6_3_4_2_process_switch.md: -------------------------------------------------------------------------------- 1 | #### 进程切换过程 2 | 3 | 进程调度函数 schedule 选择了下一个将占用 CPU 执行的进程后,将调用进程切换,从而让新的进程得以执行。通过实验四和实验五的理解,应该已经对进程调度和上下文切换有了初步的认识。在实验五中,结合调度器框架的设计,可对 ucore 中的进程切换以及堆栈的维护和使用等有更加深刻的认识。假定有两个用户进程,在二者进行进程切换的过程中,具体的步骤如下: 4 | 5 | 首先在执行某进程 A 的用户代码时,出现了一个 trap (例如是一个外设产生的中断),这个时候就会从进程 A 的用户态切换到内核态(过程(1)),并且保存好进程 A 的 trapframe;当内核态处理中断时发现需要进行进程切换时,ucore 要通过 schedule 函数选择下一个将占用 CPU 执行的进程(即进程 B),然后会调用 proc_run 函数,proc_run 函数进一步调用 switch_to 函数,切换到进程 B 的内核态(过程(2)),继续进程 B 上一次在内核态的操作,并通过 iret 指令,最终将执行权转交给进程 B 的用户空间(过程(3))。 6 | 7 | 当进程 B 由于某种原因发生中断之后(过程(4)),会从进程 B 的用户态切换到内核态,并且保存好进程 B 的 trapframe;当内核态处理中断时发现需要进行进程切换时,即需要切换到进程 A,ucore 再次切换到进程 A(过程(5)),会执行进程 A 上一次在内核调用 schedule (具体还要跟踪到 switch_to 函数)函数返回后的下一行代码,这行代码当然还是在进程 A 的上一次中断处理流程中。最后当进程 A 的中断处理完毕的时候,执行权又会反交给进程 A 的用户代码(过程(6))。这就是在只有两个进程的情况下,进程切换间的大体流程。 8 | 9 | 几点需要强调的是: 10 | 11 | **a)** 12 | 需要透彻理解在进程切换以后,程序是从哪里开始执行的?需要注意到虽然指令还是同一个 cpu 上执行,但是此时已经是另外一个进程在执行了,且使用的资源已经完全不同了。 13 | 14 | **b)** 15 | 内核在第一个程序运行的时候,需要进行哪些操作?有了实验四和实验五的经验,可以确定,内核启动第一个用户进程的过程,实际上是从进程启动时的内核状态切换到该用户进程的内核状态的过程,而且该用户进程在用户态的起始入口应该是 forkret。 16 | -------------------------------------------------------------------------------- /lab6/lab6_3_4_process_implement.md: -------------------------------------------------------------------------------- 1 | ### 进程调度实现 2 | -------------------------------------------------------------------------------- /lab6/lab6_3_5_1_designed.md: -------------------------------------------------------------------------------- 1 | #### 设计思路 2 | 3 | 实行一个进程调度策略,到底需要实现哪些基本功能对应的数据结构?首先考虑到一个无论哪种调度算法都需要选择一个就绪进程来占用 CPU 运行。为此我们可把就绪进程组织起来,可用队列(双向链表)、二叉树、红黑树、数组…等不同的组织方式。 4 | 5 | 在操作方面,如果需要选择一个就绪进程,就可以从基于某种组织方式的就绪进程集合中选择出一个进程执行。需要注意,这里“选择”和“出”是两个操作,选择是在集合中挑选一个“合适”的进程,“出”意味着离开就绪进程集合。另外考虑到一个处于运行态的进程还会由于某种原因(比如时间片用完了)回到就绪态而不能继续占用 CPU 执行,这就会重新进入到就绪进程集合中。这两种情况就形成了调度器相关的三个基本操作:在就绪进程集合中选择、进入就绪进程集合和离开就绪进程集合。这三个操作属于调度器的基本操作。 6 | 7 | 在进程的执行过程中,就绪进程的等待时间和执行进程的执行时间是影响调度选择的重要因素,这两个因素随着时间的流逝和各种事件的发生在不停地变化,比如处于就绪态的进程等待调度的时间在增长,处于运行态的进程所消耗的时间片在减少等。这些进程状态变化的情况需要及时让进程调度器知道,便于选择更合适的进程执行。所以这种进程变化的情况就形成了调度器相关的一个变化感知操作:timer 时间事件感知操作。这样在进程运行或等待的过程中,调度器可以调整进程控制块中与进程调度相关的属性值(比如消耗的时间片、进程优先级等),并可能导致对进程组织形式的调整(比如以时间片大小的顺序来重排双向链表等),并最终可能导致调选择新的进程占用 CPU 运行。这个操作属于调度器的进程调度属性调整操作。 8 | -------------------------------------------------------------------------------- /lab6/lab6_3_5_2_data_structure.md: -------------------------------------------------------------------------------- 1 | #### 数据结构 2 | 3 | 在理解框架之前,需要先了解一下调度器框架所需要的数据结构。 4 | 5 | - 通常的操作系统中,进程池是很大的(虽然在 ucore 中,MAX_PROCESS 很小)。在 ucore 中,调度器引入 run-queue(简称 rq,即运行队列)的概念,通过链表结构管理进程。 6 | - 由于目前 ucore 设计运行在单 CPU 上,其内部只有一个全局的运行队列,用来管理系统内全部的进程。 7 | - 运行队列通过链表的形式进行组织。链表的每一个节点是一个 list_entry_t,每个 list_entry_t 又对应到了 struct proc_struct \*,这其间的转换是通过宏 le2proc 来完成 的。具体来说,我们知道在 struct proc_struct 中有一个叫 run_link 的 list_entry_t,因此可以通过偏移量逆向找到对因某个 run_list 的 struct proc_struct。即进程结构指针 proc = le2proc(链表节点指针, run_link)。 8 | - 为了保证调度器接口的通用性,ucore 调度框架定义了如下接口,该接口中,几乎全部成员变量均为函数指针。具体的功能会在后面的框架说明中介绍。 9 | 10 | ``` 11 | 1 struct sched_class { 12 | 2 // 调度器的名字 13 | 3 const char *name; 14 | 4 // 初始化运行队列 15 | 5 void (*init) (struct run_queue *rq); 16 | 6 // 将进程 p 插入队列 rq 17 | 7 void (*enqueue) (struct run_queue *rq, struct proc_struct *p); 18 | 8 // 将进程 p 从队列 rq 中删除 19 | 9 void (*dequeue) (struct run_queue *rq, struct proc_struct *p); 20 | 10 // 返回 运行队列 中下一个可执行的进程 21 | 11 struct proc_struct* (*pick_next) (struct run_queue *rq); 22 | 12 // timetick 处理函数 23 | 13 void (*proc_tick)(struct run_queue* rq, struct proc_struct* p); 24 | 14 }; 25 | ``` 26 | 27 | - 此外,proc.h 中的 struct proc_struct 中也记录了一些调度相关的信息: 28 | 29 | ``` 30 | 1 struct proc_struct { 31 | 2 // . . . 32 | 3 // 该进程是否需要调度,只对当前进程有效 33 | 4 volatile bool need_resched; 34 | 5 // 该进程的调度链表结构,该结构内部的连接组成了 运行队列 列表 35 | 6 list_entry_t run_link; 36 | 7 // 该进程剩余的时间片,只对当前进程有效 37 | 8 int time_slice; 38 | 9 // round-robin 调度器并不会用到以下成员 39 | 10 // 该进程在优先队列中的节点,仅在 LAB6 使用 40 | 11 skew_heap_entry_t lab6_run_pool; 41 | 12 // 该进程的调度优先级,仅在 LAB6 使用 42 | 13 uint32_t lab6_priority; 43 | 14 // 该进程的调度步进值,仅在 LAB6 使用 44 | 15 uint32_t lab6_stride; 45 | 16 }; 46 | ``` 47 | 48 | 在此次实验中,你需要了解 default_sched.c 中的实现 RR 调度算法的函数。在该文件中,你可以看到 ucore 已经为 RR 调度算法创建好了一个名为 RR_sched_class 的调度策略类。 49 | 50 | 通过数据结构 struct run_queue 来描述完整的 run_queue(运行队列)。它的主要结构如下: 51 | 52 | ``` 53 | 1 struct run_queue { 54 | 2 //其运行队列的哨兵结构,可以看作是队列头和尾 55 | 3 list_entry_t run_list; 56 | 4 //优先队列形式的进程容器,只在 LAB6 中使用 57 | 5 skew_heap_entry_t *lab6_run_pool; 58 | 6 //表示其内部的进程总数 59 | 7 unsigned int proc_num; 60 | 8 //每个进程一轮占用的最多时间片 61 | 9 int max_time_slice; 62 | 10 }; 63 | ``` 64 | 65 | 在 ucore 框架中,运行队列存储的是当前可以调度的进程,所以,只有状态为 runnable 的进程才能够进入运行队列。当前正在运行的进程并不会在运行队列中,这一点需要注意。 66 | -------------------------------------------------------------------------------- /lab6/lab6_3_5_3_scheduler_point_functions.md: -------------------------------------------------------------------------------- 1 | #### 调度点的相关关键函数 2 | 3 | 虽然进程各种状态变化的原因和导致的调度处理各异,但其实仔细观察各个流程的共性部分,会发现其中只涉及了三个关键调度相关函数:wakup_proc、shedule、run_timer_list。如果我们能够让这三个调度相关函数的实现与具体调度算法无关,那么就可以认为 ucore 实现了一个与调度算法无关的调度框架。 4 | 5 | wakeup_proc 函数其实完成了把一个就绪进程放入到就绪进程队列中的工作,为此还调用了一个调度类接口函数 sched_class_enqueue,这使得 wakeup_proc 的实现与具体调度算法无关。schedule 函数完成了与调度框架和调度算法相关三件事情:把当前继续占用 CPU 执行的运行进程放放入到就绪进程队列中,从就绪进程队列中选择一个“合适”就绪进程,把这个“合适”的就绪进程从就绪进程队列中摘除。通过调用三个调度类接口函数 sched_class_enqueue、sched_class_pick_next、sched_class_enqueue 来使得完成这三件事情与具体的调度算法无关。run_timer_list 函数在每次 timer 中断处理过程中被调用,从而可用来调用调度算法所需的 timer 时间事件感知操作,调整相关进程的进程调度相关的属性值。通过调用调度类接口函数 sched_class_proc_tick 使得此操作与具体调度算法无关。 6 | 7 | 这里涉及了一系列调度类接口函数: 8 | 9 | - sched_class_enqueue 10 | - sched_class_dequeue 11 | - sched_class_pick_next 12 | - sched_class_proc_tick 13 | 14 | 这 4 个函数的实现其实就是调用某基于 sched_class 数据结构的特定调度算法实现的 4 个指针函数。采用这样的调度类框架后,如果我们需要实现一个新的调度算法,则我们需要定义一个针对此算法的调度类的实例,一个就绪进程队列的组织结构描述就行了,其他的事情都可交给调度类框架来完成。 15 | -------------------------------------------------------------------------------- /lab6/lab6_3_5_4_RR.md: -------------------------------------------------------------------------------- 1 | #### RR 调度算法实现 2 | 3 | RR 调度算法的调度思想 是让所有 runnable 态的进程分时轮流使用 CPU 时间。RR 调度器维护当前 runnable 进程的有序运行队列。当前进程的时间片用完之后,调度器将当前进程放置到运行队列的尾部,再从其头部取出进程进行调度。RR 调度算法的就绪队列在组织结构上也是一个双向链表,只是增加了一个成员变量,表明在此就绪进程队列中的最大执行时间片。而且在进程控制块 proc_struct 中增加了一个成员变量 time_slice,用来记录进程当前的可运行时间片段。这是由于 RR 调度算法需要考虑执行进程的运行时间不能太长。在每个 timer 到时的时候,操作系统会递减当前执行进程的 time_slice,当 time_slice 为 0 时,就意味着这个进程运行了一段时间(这个时间片段称为进程的时间片),需要把 CPU 让给其他进程执行,于是操作系统就需要让此进程重新回到 rq 的队列尾,且重置此进程的时间片为就绪队列的成员变量最大时间片 max_time_slice 值,然后再从 rq 的队列头取出一个新的进程执行。下面来分析一下其调度算法的实现。 4 | 5 | RR_enqueue 的函数实现如下表所示。即把某进程的进程控制块指针放入到 rq 队列末尾,且如果进程控制块的时间片为 0,则需要把它重置为 rq 成员变量 max_time_slice。这表示如果进程在当前的执行时间片已经用完,需要等到下一次有机会运行时,才能再执行一段时间。 6 | 7 | ``` 8 | static void 9 | RR_enqueue(struct run_queue *rq, struct proc_struct *proc) { 10 | assert(list_empty(&(proc->run_link))); 11 | list_add_before(&(rq->run_list), &(proc->run_link)); 12 | if (proc->time_slice == 0 || proc->time_slice > rq->max_time_slice) { 13 | proc->time_slice = rq->max_time_slice; 14 | } 15 | proc->rq = rq; 16 | rq->proc_num ++; 17 | } 18 | ``` 19 | 20 | RR_pick_next 的函数实现如下表所示。即选取就绪进程队列 rq 中的队头队列元素,并把队列元素转换成进程控制块指针。 21 | 22 | ``` 23 | static struct proc_struct * 24 | FCFS_pick_next(struct run_queue *rq) { 25 | list_entry_t *le = list_next(&(rq->run_list)); 26 | if (le != &(rq->run_list)) { 27 | return le2proc(le, run_link); 28 | } 29 | return NULL; 30 | } 31 | ``` 32 | 33 | RR_dequeue 的函数实现如下表所示。即把就绪进程队列 rq 的进程控制块指针的队列元素删除,并把表示就绪进程个数的 proc_num 减一。 34 | 35 | ``` 36 | static void 37 | FCFS_dequeue(struct run_queue *rq, struct proc_struct *proc) { 38 | assert(!list_empty(&(proc->run_link)) && proc->rq == rq); 39 | list_del_init(&(proc->run_link)); 40 | rq->proc_num --; 41 | } 42 | ``` 43 | 44 | RR_proc_tick 的函数实现如下表所示。即每次 timer 到时后,trap 函数将会间接调用此函数来把当前执行进程的时间片 time_slice 减一。如果 time_slice 降到零,则设置此进程成员变量 need_resched 标识为 1,这样在下一次中断来后执行 trap 函数时,会由于当前进程程成员变量 need_resched 标识为 1 而执行 schedule 函数,从而把当前执行进程放回就绪队列末尾,而从就绪队列头取出在就绪队列上等待时间最久的那个就绪进程执行。 45 | 46 | ``` 47 | static void 48 | RR_proc_tick(struct run_queue *rq, struct proc_struct *proc) { 49 | if (proc->time_slice > 0) { 50 | proc->time_slice --; 51 | } 52 | if (proc->time_slice == 0) { 53 | proc->need_resched = 1; 54 | } 55 | } 56 | ``` 57 | -------------------------------------------------------------------------------- /lab6/lab6_3_5_scheduler_framework.md: -------------------------------------------------------------------------------- 1 | ### 调度框架和调度算法 2 | -------------------------------------------------------------------------------- /lab6/lab6_3_6_1_basic_method.md: -------------------------------------------------------------------------------- 1 | #### 基本思路 2 | 3 | **【提示】请先看练习 2 中提到的论文, 理解后在看下面的内容。** 4 | 5 | 考察 round-robin 调度器,在假设所有进程都充分使用了其拥有的 CPU 时间资源的情况下,所有进程得到的 CPU 时间应该是相等的。但是有时候我们希望调度器能够更智能地为每个进程分配合理的 CPU 资源。假设我们为不同的进程分配不同的优先级,则我们有可能希望每个进程得到的时间资源与他们的优先级成正比关系。Stride 调度是基于这种想法的一个较为典型和简单的算法。除了简单易于实现以外,它还有如下的特点: 6 | 7 | - 可控性:如我们之前所希望的,可以证明 Stride Scheduling 对进程的调度次数正比于其优先级。 8 | - 确定性:在不考虑计时器事件的情况下,整个调度机制都是可预知和重现的。该算法的基本思想可以考虑如下: 9 | 10 | 1. 为每个 runnable 的进程设置一个当前状态 stride,表示该进程当前的调度权。另外定义其对应的 pass 值,表示对应进程在调度后,stride 需要进行的累加值。 11 | 2. 每次需要调度时,从当前 runnable 态的进程中选择 stride 最小的进程调度。 12 | 3. 对于获得调度的进程 P,将对应的 stride 加上其对应的步长 pass(只与进程的优先权有关系)。 13 | 4. 在一段固定的时间之后,回到 2.步骤,重新调度当前 stride 最小的进程。 14 | 可以证明,如果令 P.pass =BigStride / P.priority 其中 P.priority 表示进程的优先权(大于 1),而 BigStride 表示一个预先定义的大常数,则该调度方案为每个进程分配的时间将与其优先级成正比。证明过程我们在这里略去,有兴趣的同学可以在网上查找相关资料。将该调度器应用到 15 | ucore 的调度器框架中来,则需要将调度器接口实现如下: 16 | 17 | - init: 18 | – 初始化调度器类的信息(如果有的话)。 19 | – 初始化当前的运行队列为一个空的容器结构。(比如和 RR 调度算法一样,初始化为一个有序列表) 20 | 21 | - enqueue 22 | – 初始化刚进入运行队列的进程 proc 的 stride 属性。 23 | – 将 proc 插入放入运行队列中去(注意:这里并不要求放置在队列头部)。 24 | 25 | - dequeue 26 | – 从运行队列中删除相应的元素。 27 | 28 | - pick next 29 | – 扫描整个运行队列,返回其中 stride 值最小的对应进程。 30 | – 更新对应进程的 stride 值,即 pass = BIG_STRIDE / P-\>priority; P-\>stride += pass。 31 | 32 | - proc tick: 33 | – 检测当前进程是否已用完分配的时间片。如果时间片用完,应该正确设置进程结构的相关标记来引起进程切换。 34 | – 一个 process 最多可以连续运行 rq.max_time_slice 个时间片。 35 | 36 | 在具体实现时,有一个需要注意的地方:stride 属性的溢出问题,在之前的实现里面我们并没有考虑 stride 的数值范围,而这个值在理论上是不断增加的,在 37 | stride 溢出以后,基于 stride 的比较可能会出现错误。比如假设当前存在两个进程 A 和 B,stride 属性采用 16 位无符号整数进行存储。当前队列中元素如下(假设当前运行的进程已经被重新放置进运行队列中): 38 | 39 | ![image](../lab6_figs/image001.png) 40 | 41 | 此时应该选择 A 作为调度的进程,而在一轮调度后,队列将如下: 42 | 43 | ![image](../lab6_figs/image002.png) 44 | 45 | 可以看到由于溢出的出现,进程间 stride 的理论比较和实际比较结果出现了偏差。我们首先在理论上分析这个问题:令 PASS_MAX 为当前所有进程里最大的步进值。则我们可以证明如下结论:对每次 Stride 调度器的调度步骤中,有其最大的步进值 STRIDE_MAX 和最小的步进值 STRIDE_MIN 之差: 46 | 47 | STRIDE_MAX – STRIDE_MIN <= PASS_MAX 48 | 49 | 提问 1:如何证明该结论? 50 | 51 | 有了该结论,在加上之前对优先级有 Priority \> 1 限制,我们有 STRIDE_MAX – STRIDE_MIN <= BIG_STRIDE,于是我们只要将 BigStride 取在某个范围之内,即可保证对于任意两个 Stride 之差都会在机器整数表示的范围之内。而我们可以通过其与 0 的比较结构,来得到两个 Stride 的大小关系。在上例中,虽然在直接的数值表示上 98 < 65535,但是 98 - 65535 的结果用带符号的 16 位整数表示的结果为 99,与理论值之差相等。所以在这个意义下 98 \> 65535。基于这种特殊考虑的比较方法,即便 Stride 有可能溢出,我们仍能够得到理论上的当前最小 Stride,并做出正确的调度决定。 52 | 53 | 提问 2:在 ucore 中,目前 Stride 是采用无符号的 32 位整数表示。则 BigStride 应该取多少,才能保证比较的正确性? 54 | -------------------------------------------------------------------------------- /lab6/lab6_3_6_2_priority_queue.md: -------------------------------------------------------------------------------- 1 | #### 使用优先队列实现 Stride Scheduling 2 | 3 | 在上述的实现描述中,对于每一次 pick_next 函数,我们都需要完整地扫描来获得当前最小的 stride 及其进程。这在进程非常多的时候是非常耗时和低效的,有兴趣的同学可以在实现了基于列表扫描的 Stride 调度器之后比较一下 priority 程序在 Round-Robin 及 Stride 调度器下各自的运行时间。考虑到其调度选择于优先队列的抽象逻辑一致,我们考虑使用优化的优先队列数据结构实现该调度。 4 | 5 | 优先队列是这样一种数据结构:使用者可以快速的插入和删除队列中的元素,并且在预先指定的顺序下快速取得当前在队列中的最小(或者最大)值及其对应元素。可以看到,这样的数据结构非常符合 Stride 调度器的实现。 6 | 7 | 本次实验提供了 libs/skew_heap.h 8 | 作为优先队列的一个实现,该实现定义相关的结构和接口,其中主要包括: 9 | 10 | ``` 11 | 1 // 优先队列节点的结构 12 | 2 typedef struct skew_heap_entry skew_heap_entry_t; 13 | 3 // 初始化一个队列节点 14 | 4 void skew_heap_init(skew_heap_entry_t *a); 15 | 5 // 将节点 b 插入至以节点 a 为队列头的队列中去,返回插入后的队列 16 | 6 skew_heap_entry_t *skew_heap_insert(skew_heap_entry_t *a, 17 | 7 skew_heap_entry_t *b, 18 | 8 compare_f comp); 19 | 9 // 将节点 b 插入从以节点 a 为队列头的队列中去,返回删除后的队列 20 | 10 skew_heap_entry_t *skew_heap_remove(skew_heap_entry_t *a, 21 | 11 skew_heap_entry_t *b, 22 | 12 compare_f comp); 23 | ``` 24 | 25 | 其中优先队列的顺序是由比较函数 comp 决定的,sched_stride.c 中提供了 proc_stride_comp_f 比较器用来比较两个 stride 的大小,你可以直接使用它。当使用优先队列作为 Stride 调度器的实现方式之后,运行队列结构也需要作相关改变,其中包括: 26 | 27 | - struct 28 | run_queue 中的 lab6_run_pool 指针,在使用优先队列的实现中表示当前优先队列的头元素,如果优先队列为空,则其指向空指针(NULL)。 29 | 30 | - struct 31 | proc_struct 中的 lab6_run_pool 结构,表示当前进程对应的优先队列节点。本次实验已经修改了系统相关部分的代码,使得其能够很好地适应 LAB6 新加入的数据结构和接口。而在实验中我们需要做的是用优先队列实现一个正确和高效的 Stride 调度器,如果用较简略的伪代码描述,则有: 32 | 33 | - init(rq): 34 | – Initialize rq-\>run_list 35 | – Set rq-\>lab6_run_pool to NULL 36 | – Set rq-\>proc_num to 0 37 | 38 | - enqueue(rq, proc) 39 | – Initialize proc-\>time_slice 40 | – Insert proc-\>lab6_run_pool into rq-\>lab6_run_pool 41 | – rq-\>proc_num ++ 42 | 43 | - dequeue(rq, proc) 44 | – Remove proc-\>lab6_run_pool from rq-\>lab6_run_pool 45 | – rq-\>proc_num -- 46 | 47 | - pick_next(rq) 48 | – If rq-\>lab6_run_pool == NULL, return NULL 49 | – Find the proc corresponding to the pointer rq-\>lab6_run_pool 50 | – proc-\>lab6_stride += BIG_STRIDE / proc-\>lab6_priority 51 | – Return proc 52 | 53 | - proc_tick(rq, proc): 54 | – If proc-\>time_slice \> 0, proc-\>time_slice -- 55 | – If proc-\>time_slice == 0, set the flag proc-\>need_resched 56 | -------------------------------------------------------------------------------- /lab6/lab6_3_6_stride_scheduling.md: -------------------------------------------------------------------------------- 1 | ### Stride Scheduling 2 | -------------------------------------------------------------------------------- /lab6/lab6_3_scheduler_design.md: -------------------------------------------------------------------------------- 1 | ## 调度框架和调度算法设计与实现 2 | -------------------------------------------------------------------------------- /lab6/lab6_4_labs_requirement.md: -------------------------------------------------------------------------------- 1 | ## 实验报告要求 2 | 3 | 从 git server 网站上取得 ucore_lab 后,进入目录 labcodes/lab6,完成实验要求的各个练习。 4 | 在实验报告中回答所有练习中提出的问题。在目录 labcodes/lab6 下存放实验报告,实验报告文档命名为 lab6.md,使用**markdown**格式。 5 | 对于 lab6 中编程任务,完成编写之后,再通过 git push 命令把代码同步回 git server 网站。最后请一定提前或按时提交到 git server 网站。 6 | 7 | 注意有“LAB6”的注释,主要是修改 default_sched_swide_c 中的内容。代码中所有需要完成的地方(challenge 除外)都有“LAB6”和“YOUR CODE”的注释,请在提交时特别注意保持注释,并将“YOUR CODE”替换为自己的学号,并且将所有标有对应注释的部分填上正确的代码。 8 | 9 | ## 附录:执行 priority 大致的显示输出 10 | 11 | ``` 12 | $ make run-priority 13 | ...... 14 | check_swap() succeeded! 15 | ++ setup timer interrupts 16 | kernel_execve: pid = 2, name = "priority". 17 | main: fork ok,now need to wait pids. 18 | child pid 7, acc 2492000, time 2001 19 | child pid 6, acc 1944000, time 2001 20 | child pid 4, acc 960000, time 2002 21 | child pid 5, acc 1488000, time 2003 22 | child pid 3, acc 540000, time 2004 23 | main: pid 3, acc 540000, time 2004 24 | main: pid 4, acc 960000, time 2004 25 | main: pid 5, acc 1488000, time 2004 26 | main: pid 6, acc 1944000, time 2004 27 | main: pid 7, acc 2492000, time 2004 28 | main: wait pids over 29 | stride sched correct result: 1 2 3 4 5 30 | all user-mode processes have quit. 31 | init check memory pass. 32 | kernel panic at kern/process/proc.c:426: 33 | initproc exit. 34 | 35 | Welcome to the kernel debug monitor!! 36 | Type 'help' for a list of commands. 37 | K> 38 | ``` 39 | -------------------------------------------------------------------------------- /lab6_figs/image001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab6_figs/image001.png -------------------------------------------------------------------------------- /lab6_figs/image002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab6_figs/image002.png -------------------------------------------------------------------------------- /lab7.md: -------------------------------------------------------------------------------- 1 | # 实验七:同步互斥 2 | -------------------------------------------------------------------------------- /lab7/lab7_1_goals.md: -------------------------------------------------------------------------------- 1 | ## 实验目的 2 | 3 | - 理解操作系统的同步互斥的设计实现; 4 | - 理解底层支撑技术:禁用中断、定时器、等待队列; 5 | - 在 ucore 中理解信号量(semaphore)机制的具体实现; 6 | - 理解管程机制,在 ucore 内核中增加基于管程(monitor)的条件变量(condition 7 | variable)的支持; 8 | - 了解经典进程同步问题,并能使用同步机制解决进程同步问题。 9 | -------------------------------------------------------------------------------- /lab7/lab7_2_1_exercises.md: -------------------------------------------------------------------------------- 1 | ### 练习 2 | 3 | 对实验报告的要求: 4 | 5 | - 基于 markdown 格式来完成,以文本方式为主 6 | - 填写各个基本练习中要求完成的报告内容 7 | - 完成实验后,请分析 ucore_lab 中提供的参考答案,并请在实验报告中说明你的实现与参考答案的区别 8 | - 列出你认为本实验中重要的知识点,以及与对应的 OS 原理中的知识点,并简要说明你对二者的含义,关系,差异等方面的理解(也可能出现实验中的知识点没有对应的原理知识点) 9 | - 列出你认为 OS 原理中很重要,但在实验中没有对应上的知识点 10 | 11 | #### 练习 0:填写已有实验 12 | 13 | 本实验依赖实验 1/2/3/4/5/6。请把你做的实验 1/2/3/4/5/6 的代码填入本实验中代码中有“LAB1”/“LAB2”/“LAB3”/“LAB4”/“LAB5”/“LAB6”的注释相应部分。并确保编译通过。注意:为了能够正确执行 lab7 的测试应用程序,可能需对已完成的实验 1/2/3/4/5/6 的代码进行进一步改进。 14 | 15 | #### 练习 1: 理解内核级信号量的实现和基于内核级信号量的哲学家就餐问题(不需要编码) 16 | 17 | 完成练习 0 后,建议大家比较一下(可用 meld 等文件 diff 比较软件)个人完成的 lab6 和练习 0 完成后的刚修改的 lab7 之间的区别,分析了解 lab7 采用信号量的执行过程。执行`make grade`,大部分测试用例应该通过。 18 | 19 | 请在实验报告中给出内核级信号量的设计描述,并说明其大致执行流程。 20 | 21 | 请在实验报告中给出给用户态进程/线程提供信号量机制的设计方案,并比较说明给内核级提供信号量机制的异同。 22 | 23 | #### 练习 2: 完成内核级条件变量和基于内核级条件变量的哲学家就餐问题(需要编码) 24 | 25 | 首先掌握管程机制,然后基于信号量实现完成条件变量实现,然后用管程机制实现哲学家就餐问题的解决方案(基于条件变量)。 26 | 27 | 执行:`make grade` 28 | 。如果所显示的应用程序检测都输出 ok,则基本正确。如果只是某程序过不去,比如 matrix.c,则可执行 29 | 30 | ``` 31 | make run-matrix 32 | ``` 33 | 34 | 命令来单独调试它。大致执行结果可看附录。 35 | 36 | 请在实验报告中给出内核级条件变量的设计描述,并说明其大致执行流程。 37 | 38 | 请在实验报告中给出给用户态进程/线程提供条件变量机制的设计方案,并比较说明给内核级提供条件变量机制的异同。 39 | 40 | 请在实验报告中回答:能否不用基于信号量机制来完成条件变量?如果不能,请给出理由,如果能,请给出设计说明和具体实现。 41 | 42 | #### 扩展练习 Challenge : 在 ucore 中实现简化的死锁和重入探测机制 43 | 44 | 在 ucore 下实现一种探测机制,能够在多进程/线程运行同步互斥问题时,动态判断当前系统是否出现了死锁产生的必要条件,是否产生了多个进程进入临界区的情况。 45 | 如果发现,让系统进入 monitor 状态,打印出你的探测信息。 46 | 47 | #### 扩展练习 Challenge : 参考 Linux 的 RCU 机制,在 ucore 中实现简化的 RCU 机制 48 | 49 | 在 ucore 50 | 下实现下 Linux 的 RCU 同步互斥机制。可阅读相关 Linux 内核书籍或查询网上资料,可了解 RCU 的设计实现细节,然后简化实现在 ucore 中。 51 | 要求有实验报告说明你的设计思路,并提供测试用例。下面是一些参考资料: 52 | 53 | - [http://www.ibm.com/developerworks/cn/linux/l-rcu/](http://www.ibm.com/developerworks/cn/linux/l-rcu/) 54 | - [http://www.diybl.com/course/6_system/linux/Linuxjs/20081117/151814.html](http://www.diybl.com/course/6_system/linux/Linuxjs/20081117/151814.html) 55 | -------------------------------------------------------------------------------- /lab7/lab7_2_2_files.md: -------------------------------------------------------------------------------- 1 | ### 项目组成 2 | 3 | 此次实验中,主要有如下一些需要关注的文件: 4 | 5 | ``` 6 | . 7 | ├── boot 8 | ├── kern 9 | │ ├── driver 10 | │ ├── fs 11 | │ ├── init 12 | │ ├── libs 13 | │ ├── mm 14 | │ │ ├── ...... 15 | │ │ ├── vmm.c 16 | │ │ └── vmm.h 17 | │ ├── process 18 | │ │ ├── proc.c 19 | │ │ ├── proc.h 20 | │ │ └──...... 21 | │ ├── schedule 22 | │ ├── sync 23 | │ │ ├── check\_sync.c 24 | │ │ ├── monitor.c 25 | │ │ ├── monitor.h 26 | │ │ ├── sem.c 27 | │ │ ├── sem.h 28 | │ │ ├── sync.h 29 | │ │ ├── wait.c 30 | │ │ └── wait.h 31 | │ ├── syscall 32 | │ │ ├── syscall.c 33 | │ │ └──...... 34 | │ └── trap 35 | ├── libs 36 | └── user 37 | ├── forktree.c 38 | ├── libs 39 | │ ├── syscall.c 40 | │ ├── syscall.h 41 | │ ├── ulib.c 42 | │ ├── ulib.h 43 | │ └── ...... 44 | ├── priority.c 45 | ├── sleep.c 46 | ├── sleepkill.c 47 | ├── softint.c 48 | ├── spin.c 49 | └── ...... 50 | ``` 51 | 52 | 简单说明如下: 53 | 54 | - kern/schedule/{sched.h,sched.c}: 增加了定时器(timer)机制,用于进程/线程的 do_sleep 功能。 55 | - kern/sync/sync.h: 去除了 lock 实现(这对于不抢占内核没用)。 56 | - kern/sync/wait.[ch]: 57 | 定义了等待队列 wait_queue 结构和等待 entry 的 wait 结构以及在此之上的函数,这是 ucore 中的信号量 semophore 机制和条件变量机制的基础,在本次实验中你需要了解其实现。 58 | - kern/sync/sem.[ch]:定义并实现了 ucore 中内核级信号量相关的数据结构和函数,本次试验中你需要了解其中的实现,并基于此完成内核级条件变量的设计与实现。 59 | - user/ libs/ {syscall.[ch],ulib.[ch] 60 | }与 kern/sync/syscall.c:实现了进程 sleep 相关的系统调用的参数传递和调用关系。 61 | - user/{ sleep.c,sleepkill.c}: 进程睡眠相关的一些测试用户程序。 62 | - kern/sync/monitor.[ch]:基于管程的条件变量的实现程序,在本次实验中是练习的一部分,要求完成。 63 | - kern/sync/check_sync.c:实现了基于管程的哲学家就餐问题,在本次实验中是练习的一部分,要求完成基于管程的哲学家就餐问题。 64 | - kern/mm/vmm.[ch]:用信号量 mm_sem 取代 mm_struct 中原有的 mm_lock。(本次实验不用管) 65 | -------------------------------------------------------------------------------- /lab7/lab7_2_labs.md: -------------------------------------------------------------------------------- 1 | ## 实验内容 2 | 3 | 实验六完成了用户进程的调度框架和具体的调度算法,可调度运行多个进程。如果多个进程需要协同操作或访问共享资源,则存在如何同步和有序竞争的问题。本次实验,主要是熟悉 ucore 的进程同步机制—信号量(semaphore)机制,以及基于信号量的哲学家就餐问题解决方案。然后掌握管程的概念和原理,并参考信号量机制,实现基于管程的条件变量机制和基于条件变量来解决哲学家就餐问题。 4 | 5 | 在本次实验中,在 kern/sync/check_sync.c 中提供了一个基于信号量的哲学家就餐问题解法。同时还需完成练习,即实现基于管程(主要是灵活运用条件变量和互斥信号量)的哲学家就餐问题解法。哲学家就餐问题描述如下:有五个哲学家,他们的生活方式是交替地进行思考和进餐。哲学家们公用一张圆桌,周围放有五把椅子,每人坐一把。在圆桌上有五个碗和五根筷子,当一个哲学家思考时,他不与其他人交谈,饥饿时便试图取用其左、右最靠近他的筷子,但他可能一根都拿不到。只有在他拿到两根筷子时,方能进餐,进餐完后,放下筷子又继续思考。 6 | -------------------------------------------------------------------------------- /lab7/lab7_3_1_experiment.md: -------------------------------------------------------------------------------- 1 | ### 实验执行流程概述 2 | 3 | 互斥是指某一资源同时只允许一个进程对其进行访问,具有唯一性和排它性,但互斥不用限制进程对资源的访问顺序,即访问可以是无序的。同步是指在进程间的执行必须严格按照规定的某种先后次序来运行,即访问是有序的,这种先后次序取决于要系统完成的任务需求。在进程写资源情况下,进程间要求满足互斥条件。在进程读资源情况下,可允许多个进程同时访问资源。 4 | 5 | 实验七设计实现了多种同步互斥手段,包括时钟中断管理、等待队列、信号量、管程机制(包含条件变量设计)等,并基于信号量实现了哲学家问题的执行过程。而本次实验的练习是要求用管程机制实现哲学家问题的执行过程。在实现信号量机制和管程机制时,需要让无法进入临界区的进程睡眠,为此在 ucore 中设计了等待队列 wait_queue。当进程无法进入临界区(即无法获得信号量)时,可让进程进入等待队列,这时的进程处于等待状态(也可称为阻塞状态),从而会让实验六中的调度器选择一个处于就绪状态(即 RUNNABLE 6 | STATE)的进程,进行进程切换,让新进程有机会占用 CPU 执行,从而让整个系统的运行更加高效。 7 | 8 | 在实验七中的 ucore 初始化过程,开始的执行流程都与实验六相同,直到执行到创建第二个内核线程 init_main 时,修改了 init_main 的具体执行内容,即增加了 check_sync 函数的调用,而位于 lab7_figs/kern/sync/check_sync.c 中的 check_sync 函数可以理解为是实验七的起始执行点,是实验七的总控函数。进一步分析此函数,可以看到这个函数主要分为了两个部分,第一部分是实现基于信号量的哲学家问题,第二部分是实现基于管程的哲学家问题。 9 | 10 | 对于 check_sync 函数的第一部分,首先实现初始化了一个互斥信号量,然后创建了对应 5 个哲学家行为的 5 个信号量,并创建 5 个内核线程代表 5 个哲学家,每个内核线程完成了基于信号量的哲学家吃饭睡觉思考行为实现。这部分是给学生作为练习参考用的。学生可以看看信号量是如何实现的,以及如何利用信号量完成哲学家问题。 11 | 12 | 对于 check_sync 函数的第二部分,首先初始化了管程,然后又创建了 5 个内核线程代表 5 个哲学家,每个内核线程要完成基于管程的哲学家吃饭、睡觉、思考的行为实现。这部分需要学生来具体完成。学生需要掌握如何用信号量来实现条件变量,以及包含条件变量的管程如何能够确保哲学家能够正常思考和吃饭。 13 | -------------------------------------------------------------------------------- /lab7/lab7_3_2_1_timer.md: -------------------------------------------------------------------------------- 1 | ### 定时器 2 | 3 | 在传统的操作系统中,定时器是其中一个基础而重要的功能.它提供了基于时间事件的调度机制。在 ucore 中,时钟(timer)中断给操作系统提供了有一定间隔的时间事件,操作系统将其作为基本的调度和计时单位(我们记两次时间中断之间的时间间隔为一个时间片,timer splice)。 4 | 5 | 基于此时间单位,操作系统得以向上提供基于时间点的事件,并实现基于时间长度的睡眠等待和唤醒机制。在每个时钟中断发生时,操作系统产生对应的时间事件。应用程序或者操作系统的其他组件可以以此来构建更复杂和高级的进程管理和调度算法。 6 | 7 | - sched.h, sched.c 定义了有关 timer 的各种相关接口来使用 timer 服务,其中主要包括: 8 | - typedef struct {……} timer_t: 定义了 timer_t 的基本结构,其可以用 sched.h 中的 timer_init 函数对其进行初始化。 9 | - void timer_init(timer t \*timer, struct proc_struct \*proc, int expires): 对某定时器 进行初始化,让它在 expires 时间片之后唤醒 proc 10 | 进程。 11 | - void add_timer(timer t \*timer): 向系统添加某个初始化过的 timer_t,该定时器在 指定时间后被激活,并将对应的进程唤醒至 runnable(如果当前进程处在等待状态)。 12 | - void del_timer(timer_t \*time): 向系统删除(或者说取消)某一个定时器。该定时器在取消后不会被系统激活并唤醒进程。 13 | - void run_timer_list(void): 更新当前系统时间点,遍历当前所有处在系统管理内的定时器,找出所有应该激活的计数器,并激活它们。该过程在且只在每次定时器中断时被调用。在 ucore 中,其还会调用调度器事件处理程序。 14 | 15 | 一个 timer_t 在系统中的存活周期可以被描述如下: 16 | 17 | 1. timer_t 在某个位置被创建和初始化,并通过 18 | add_timer 加入系统管理列表中 19 | 2. 系统时间被不断累加,直到 run_timer_list 发现该 timer_t 到期。 20 | 3. run_timer_list 更改对应的进程状态,并从系统管理列表中移除该 timer_t。 21 | 22 | 尽管本次实验并不需要填充定时器相关的代码,但是作为系统重要的组件(同时定时器也是调度器的一个部分),你应该了解其相关机制和在 ucore 中的实现方法和使用方法。且在 trap_dispatch 函数中修改之前对时钟中断的处理,使得 ucore 能够利用定时器提供的功能完成调度和睡眠唤醒等操作。 23 | -------------------------------------------------------------------------------- /lab7/lab7_3_2_2_interrupt.md: -------------------------------------------------------------------------------- 1 | ### 屏蔽与使能中断 2 | 3 | 根据操作系统原理的知识,我们知道如果没有在硬件级保证读内存-修改值-写回内存的原子性,我们只能通过复杂的软件来实现同步互斥操作。但由于有开关中断和 test_and_set_bit 等原子操作机器指令的存在,使得我们在实现同步互斥原语上可以大大简化。 4 | 5 | 在 ucore 中提供的底层机制包括中断屏蔽/使能控制等。kern/sync.c 中实现的开关中断的控制函数 local_intr_save(x)和 local_intr_restore(x),它们是基于 kern/driver 文件下的 intr_enable()、intr_disable()函数实现的。具体调用关系为: 6 | 7 | ``` 8 | 关中断:local_intr_save --> __intr_save --> intr_disable --> cli 9 | 开中断:local_intr_restore--> __intr_restore --> intr_enable --> sti 10 | ``` 11 | 12 | 最终的 cli 和 sti 是 x86 的机器指令,最终实现了关(屏蔽)中断和开(使能)中断,即设置了 eflags 寄存器中与中断相关的位。通过关闭中断,可以防止对当前执行的控制流被其他中断事件处理所打断。既然不能中断,那也就意味着在内核运行的当前进程无法被打断或被重新调度,即实现了对临界区的互斥操作。所以在单处理器情况下,可以通过开关中断实现对临界区的互斥保护,需要互斥的临界区代码的一般写法为: 13 | 14 | ``` 15 | local_intr_save(intr_flag); 16 | { 17 | 临界区代码 18 | } 19 | local_intr_restore(intr_flag); 20 | …… 21 | ``` 22 | 23 | 由于目前 ucore 只实现了对单处理器的支持,所以通过这种方式,就可简单地支撑互斥操作了。在多处理器情况下,这种方法是无法实现互斥的,因为屏蔽了一个 CPU 的中断,只能阻止本地 CPU 上的进程不会被中断或调度,并不意味着其他 CPU 上执行的进程不能执行临界区的代码。所以,开关中断只对单处理器下的互斥操作起作用。在本实验中,开关中断机制是实现信号量等高层同步互斥原语的底层支撑基础之一。 24 | -------------------------------------------------------------------------------- /lab7/lab7_3_2_synchronization_basic_support.md: -------------------------------------------------------------------------------- 1 | ### 同步互斥的底层支撑 2 | 3 | 由于有处理器调度的存在,且进程在访问某类资源暂时无法满足的情况下,进程会进入等待状态。这导致了多进程执行时序的不确定性和潜在执行结果的不确定性。为了确保执行结果的正确性,本试验需要设计更加完善的进程等待和互斥的底层支撑机制,确保能正确提供基于信号量和条件变量的同步互斥机制。 4 | 5 | 根据操作系统原理的知识,我们知道如果没有在硬件级保证读内存-修改值-写回内存的原子性,我们只能通过复杂的软件来实现同步互斥操作。但由于有定时器、屏蔽/使能中断、等待队列 wait_queue 支持 test_and_set_bit 等原子操作机器指令(在本次实验中没有用到)的存在,使得我们在实现进程等待、同步互斥上得到了极大的简化。下面将对定时器、屏蔽/使能中断和等待队列进行进一步讲解。 6 | -------------------------------------------------------------------------------- /lab7/lab7_3_synchronization_implement.md: -------------------------------------------------------------------------------- 1 | ## 同步互斥的设计与实现 2 | -------------------------------------------------------------------------------- /lab7/lab7_4_lab_requirement.md: -------------------------------------------------------------------------------- 1 | ## 实验报告要求 2 | 3 | 从 git server 网站上取得 ucore_lab 后,进入目录 labcodes/lab7,完成实验要求的各个练习。在实验报告中回答所有练习中提出的问题。 4 | 在目录 labcodes/lab7 下存放实验报告,实验报告文档命名为 lab7.md,使用**markdown**格式。 5 | 对于 lab7 中编程任务,完成编写之后,再通过 git push 命令把代码同步回 git server 网站。最后请一定提前或按时提交到 git server 网站。。 6 | 7 | 注意有“LAB7”的注释,主要是修改 condvar.c 和 check_sync.c 中的内容。代码中所有需要完成的地方 challenge 除外)都有“LAB7”和“YOUR 8 | CODE”的注释,请在提交时特别注意保持注释,并将“YOUR 9 | CODE”替换为自己的学号,并且将所有标有对应注释的部分填上正确的代码。 10 | -------------------------------------------------------------------------------- /lab7/lab7_5_appendix.md: -------------------------------------------------------------------------------- 1 | ## 附录:执行 ”make run-matrix”的大致的显示输出 2 | 3 | ``` 4 | (THU.CST) os is loading ... 5 | …… 6 | check_alloc_page() succeeded! 7 | …… 8 | check_swap() succeeded! 9 | ++ setup timer interrupts 10 | I am No.4 philosopher_condvar 11 | Iter 1, No.4 philosopher_condvar is thinking 12 | I am No.3 philosopher_condvar 13 | …… 14 | I am No.1 philosopher_sema 15 | Iter 1, No.1 philosopher_sema is thinking 16 | I am No.0 philosopher_sema 17 | Iter 1, No.0 philosopher_sema is thinking 18 | kernel_execve: pid = 2, name = “matrix”. 19 | pid 14 is running (1000 times)!. 20 | pid 13 is running (1000 times)!. 21 | phi_test_condvar: state_condvar[4] will eating 22 | phi_test_condvar: signal self_cv[4] 23 | Iter 1, No.4 philosopher_condvar is eating 24 | phi_take_forks_condvar: 3 didn’t get fork and will wait 25 | phi_test_condvar: state_condvar[2] will eating 26 | phi_test_condvar: signal self_cv[2] 27 | Iter 1, No.2 philosopher_condvar is eating 28 | phi_take_forks_condvar: 1 didn’t get fork and will wait 29 | phi_take_forks_condvar: 0 didn’t get fork and will wait 30 | pid 14 done!. 31 | pid 13 done!. 32 | Iter 1, No.4 philosopher_sema is eating 33 | Iter 1, No.2 philosopher_sema is eating 34 | …… 35 | pid 18 done!. 36 | pid 23 done!. 37 | pid 22 done!. 38 | pid 33 done!. 39 | pid 27 done!. 40 | pid 25 done!. 41 | pid 32 done!. 42 | pid 29 done!. 43 | pid 20 done!. 44 | matrix pass. 45 | all user-mode processes have quit. 46 | init check memory pass. 47 | kernel panic at kern/process/proc.c:426: 48 | initproc exit. 49 | Welcome to the kernel debug monitor!! 50 | Type 'help' for a list of commands. 51 | K> qemu: terminating on signal 2 52 | ``` 53 | -------------------------------------------------------------------------------- /lab7_figs/image001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab7_figs/image001.png -------------------------------------------------------------------------------- /lab8.md: -------------------------------------------------------------------------------- 1 | # 实验八:文件系统 2 | -------------------------------------------------------------------------------- /lab8/lab8_1_goals.md: -------------------------------------------------------------------------------- 1 | ## 实验目的 2 | 3 | 通过完成本次实验,希望能达到以下目标 4 | 5 | - 了解基本的文件系统系统调用的实现方法; 6 | - 了解一个基于索引节点组织方式的 Simple FS 文件系统的设计与实现; 7 | - 了解文件系统抽象层-VFS 的设计与实现; 8 | -------------------------------------------------------------------------------- /lab8/lab8_2_1_exercises.md: -------------------------------------------------------------------------------- 1 | ### 练习 2 | 3 | 对实验报告的要求: 4 | 5 | - 基于 markdown 格式来完成,以文本方式为主 6 | - 填写各个基本练习中要求完成的报告内容 7 | - 完成实验后,请分析 ucore_lab 中提供的参考答案,并请在实验报告中说明你的实现与参考答案的区别 8 | - 列出你认为本实验中重要的知识点,以及与对应的 OS 原理中的知识点,并简要说明你对二者的含义,关系,差异等方面的理解(也可能出现实验中的知识点没有对应的原理知识点) 9 | - 列出你认为 OS 原理中很重要,但在实验中没有对应上的知识点 10 | 11 | #### 练习 0:填写已有实验 12 | 13 | 本实验依赖实验 1/2/3/4/5/6/7。请把你做的实验 1/2/3/4/5/6/7 的代码填入本实验中代码中有“LAB1”/“LAB2”/“LAB3”/“LAB4”/“LAB5”/“LAB6” 14 | /“LAB7”的注释相应部分。并确保编译通过。注意:为了能够正确执行 lab8 的测试应用程序,可能需对已完成的实验 1/2/3/4/5/6/7 的代码进行进一步改进。 15 | 16 | #### 练习 1: 完成读文件操作的实现(需要编码) 17 | 18 | 首先了解打开文件的处理流程,然后参考本实验后续的文件读写操作的过程分析,编写在 sfs_inode.c 中 sfs_io_nolock 读文件中数据的实现代码。 19 | 20 | 请在实验报告中给出设计实现”UNIX 的 PIPE 机制“的概要设方案,鼓励给出详细设计方案 21 | 22 | #### 练习 2: 完成基于文件系统的执行程序机制的实现(需要编码) 23 | 24 | 改写 proc.c 中的 load_icode 函数和其他相关函数,实现基于文件系统的执行程序机制。执行:make qemu。如果能看看到 sh 用户程序的执行界面,则基本成功了。如果在 sh 用户界面上可以执行”ls”,”hello”等其他放置在 sfs 文件系统中的其他执行程序,则可以认为本实验基本成功。 25 | 26 | 请在实验报告中给出设计实现基于”UNIX 的硬链接和软链接机制“的概要设方案,鼓励给出详细设计方案 27 | 28 | **祝贺你通过自己的努力,完成了 ucore OS lab1-lab8!** 29 | -------------------------------------------------------------------------------- /lab8/lab8_2_2_files.md: -------------------------------------------------------------------------------- 1 | ### 项目组成 2 | 3 | ``` 4 | . 5 | ├── boot 6 | ├── kern 7 | │ ├── debug 8 | │ ├── driver 9 | │ │ ├── clock.c 10 | │ │ ├── clock.h 11 | │ │ └── …… 12 | │ ├── fs 13 | │ │ ├── devs 14 | │ │ │ ├── dev.c 15 | │ │ │ ├── dev\_disk0.c 16 | │ │ │ ├── dev.h 17 | │ │ │ ├── dev\_stdin.c 18 | │ │ │ └── dev\_stdout.c 19 | │ │ ├── file.c 20 | │ │ ├── file.h 21 | │ │ ├── fs.c 22 | │ │ ├── fs.h 23 | │ │ ├── iobuf.c 24 | │ │ ├── iobuf.h 25 | │ │ ├── sfs 26 | │ │ │ ├── bitmap.c 27 | │ │ │ ├── bitmap.h 28 | │ │ │ ├── sfs.c 29 | │ │ │ ├── sfs\_fs.c 30 | │ │ │ ├── sfs.h 31 | │ │ │ ├── sfs\_inode.c 32 | │ │ │ ├── sfs\_io.c 33 | │ │ │ └── sfs\_lock.c 34 | │ │ ├── swap 35 | │ │ │ ├── swapfs.c 36 | │ │ │ └── swapfs.h 37 | │ │ ├── sysfile.c 38 | │ │ ├── sysfile.h 39 | │ │ └── vfs 40 | │ │ ├── inode.c 41 | │ │ ├── inode.h 42 | │ │ ├── vfs.c 43 | │ │ ├── vfsdev.c 44 | │ │ ├── vfsfile.c 45 | │ │ ├── vfs.h 46 | │ │ ├── vfslookup.c 47 | │ │ └── vfspath.c 48 | │ ├── init 49 | │ ├── libs 50 | │ │ ├── stdio.c 51 | │ │ ├── string.c 52 | │ │ └── …… 53 | │ ├── mm 54 | │ │ ├── vmm.c 55 | │ │ └── vmm.h 56 | │ ├── process 57 | │ │ ├── proc.c 58 | │ │ ├── proc.h 59 | │ │ └── …… 60 | │ ├── schedule 61 | │ ├── sync 62 | │ ├── syscall 63 | │ │ ├── syscall.c 64 | │ │ └── …… 65 | │ └── trap 66 | │ ├── trap.c 67 | │ └── …… 68 | ├── libs 69 | ├── tools 70 | │ ├── mksfs.c 71 | │ └── …… 72 | └── user 73 | ├── badarg.c 74 | ├── badsegment.c 75 | ├── divzero.c 76 | ├── exit.c 77 | ├── faultread.c 78 | ├── faultreadkernel.c 79 | ├── forktest.c 80 | ├── forktree.c 81 | ├── hello.c 82 | ├── libs 83 | │ ├── dir.c 84 | │ ├── dir.h 85 | │ ├── file.c 86 | │ ├── file.h 87 | │ ├── initcode.S 88 | │ ├── lock.h 89 | │ ├── stdio.c 90 | │ ├── syscall.c 91 | │ ├── syscall.h 92 | │ ├── ulib.c 93 | │ ├── ulib.h 94 | │ └── umain.c 95 | ├── ls.c 96 | ├── sh.c 97 | └── …… 98 | ``` 99 | 100 | 本次实验主要是理解 kern/fs 目录中的部分文件,并可用 user/\*.c 测试所实现的 Simple 101 | FS 文件系统是否能够正常工作。本次实验涉及到的代码包括: 102 | 103 | - 文件系统测试用例: user/\*.c:对文件系统的实现进行测试的测试用例; 104 | 105 | - 通用文件系统接口 106 | n user/libs/file.[ch]|dir.[ch]|syscall.c:与文件系统操作相关的用户库实行; 107 | n kern/syscall.[ch]:文件中包含文件系统相关的内核态系统调用接口 108 | n kern/fs/sysfile.[ch]|file.[ch]:通用文件系统接口和实行 109 | 110 | - 文件系统抽象层-VFS 111 | n kern/fs/vfs/\*.[ch]:虚拟文件系统接口与实现 112 | 113 | - Simple FS 文件系统 114 | n kern/fs/sfs/\*.[ch]:SimpleFS 文件系统实现 115 | 116 | - 文件系统的硬盘 IO 接口 117 | n kern/fs/devs/dev.[ch]|dev_disk0.c:disk0 硬盘设备提供给文件系统的 I/O 访问接口和实现 118 | 119 | - 辅助工具 120 | n tools/mksfs.c:创建一个 Simple FS 文件系统格式的硬盘镜像。(理解此文件的实现细节对理解 SFS 文件系统很有帮助) 121 | 122 | - 对内核其它模块的扩充 123 | n kern/process/proc.[ch]:增加成员变量 struct fs_struct \*fs_struct,用于支持进程对文件的访问;重写了 do_execve load_icode 等函数以支持执行文件系统中的文件。 124 | n kern/init/init.c:增加调用初始化文件系统的函数 fs_init。 125 | -------------------------------------------------------------------------------- /lab8/lab8_2_labs.md: -------------------------------------------------------------------------------- 1 | ## 实验内容 2 | 3 | 实验七完成了在内核中的同步互斥实验。本次实验涉及的是文件系统,通过分析了解 ucore 文件系统的总体架构设计,完善读写文件操作,从新实现基于文件系统的执行程序机制(即改写 do_execve),从而可以完成执行存储在磁盘上的文件和实现文件读写等功能。 4 | -------------------------------------------------------------------------------- /lab8/lab8_3_1_ucore_fs_introduction.md: -------------------------------------------------------------------------------- 1 | ### ucore 文件系统总体介绍 2 | 3 | 操作系统中负责管理和存储可长期保存数据的软件功能模块称为文件系统。在本次试验中,主要侧重文件系统的设计实现和对文件系统执行流程的分析与理解。 4 | 5 | ucore 的文件系统模型源于 Havard 的 OS161 的文件系统和 Linux 文件系统。但其实这二者都是源于传统的 UNIX 文件系统设计。UNIX 提出了四个文件系统抽象概念:文件(file)、目录项(dentry)、索引节点(inode)和安装点(mount point)。 6 | 7 | - 文件:UNIX 文件中的内容可理解为是一有序字节 buffer,文件都有一个方便应用程序识别的文件名称(也称文件路径名)。典型的文件操作有读、写、创建和删除等。 8 | - 目录项:目录项不是目录(又称文件路径),而是目录的组成部分。在 UNIX 中目录被看作一种特定的文件,而目录项是文件路径中的一部分。如一个文件路径名是“/test/testfile”,则包含的目录项为:根目录“/”,目录“test”和文件“testfile”,这三个都是目录项。一般而言,目录项包含目录项的名字(文件名或目录名)和目录项的索引节点(见下面的描述)位置。 9 | - 索引节点:UNIX 将文件的相关元数据信息(如访问控制权限、大小、拥有者、创建时间、数据内容等等信息)存储在一个单独的数据结构中,该结构被称为索引节点。 10 | - 安装点:在 UNIX 中,文件系统被安装在一个特定的文件路径位置,这个位置就是安装点。所有的已安装文件系统都作为根文件系统树中的叶子出现在系统中。 11 | 12 | 上述抽象概念形成了 UNIX 文件系统的逻辑数据结构,并需要通过一个具体文件系统的架构设计与实现把上述信息映射并储存到磁盘介质上,从而在具体文件系统的磁盘布局(即数据在磁盘上的物理组织)上具体体现出上述抽象概念。比如文件元数据信息存储在磁盘块中的索引节点上。当文件被载入内存时,内核需要使用磁盘块中的索引点来构造内存中的索引节点。 13 | 14 | ucore 模仿了 UNIX 的文件系统设计,ucore 的文件系统架构主要由四部分组成: 15 | 16 | - 通用文件系统访问接口层:该层提供了一个从用户空间到文件系统的标准访问接口。这一层访问接口让应用程序能够通过一个简单的接口获得 ucore 内核的文件系统服务。 17 | 18 | - 文件系统抽象层:向上提供一个一致的接口给内核其他部分(文件系统相关的系统调用实现模块和其他内核功能模块)访问。向下提供一个同样的抽象函数指针列表和数据结构屏蔽不同文件系统的实现细节。 19 | 20 | - Simple FS 文件系统层:一个基于索引方式的简单文件系统实例。向上通过各种具体函数实现以对应文件系统抽象层提出的抽象函数。向下访问外设接口 21 | 22 | - 外设接口层:向上提供 device 访问接口屏蔽不同硬件细节。向下实现访问各种具体设备驱动的接口,比如 disk 设备接口/串口设备接口/键盘设备接口等。 23 | 24 | 对照上面的层次我们再大致介绍一下文件系统的访问处理过程,加深对文件系统的总体理解。假如应用程序操作文件(打开/创建/删除/读写),首先需要通过文件系统的通用文件系统访问接口层给用户空间提供的访问接口进入文件系统内部,接着由文件系统抽象层把访问请求转发给某一具体文件系统(比如 SFS 文件系统),具体文件系统(Simple FS 文件系统层)把应用程序的访问请求转化为对磁盘上的 block 的处理请求,并通过外设接口层交给磁盘驱动例程来完成具体的磁盘操作。结合用户态写文件函数 write 的整个执行过程,我们可以比较清楚地看出 ucore 文件系统架构的层次和依赖关系。 25 | 26 | ![image](../lab8_figs/image001.png) 27 | 28 | **ucore 文件系统总体结构** 29 | 30 | 从 ucore 操作系统不同的角度来看,ucore 中的文件系统架构包含四类主要的数据结构, 31 | 它们分别是: 32 | 33 | - 超级块(SuperBlock),它主要从文件系统的全局角度描述特定文件系统的全局信息。它的作用范围是整个 OS 空间。 34 | - 索引节点(inode):它主要从文件系统的单个文件的角度它描述了文件的各种属性和数据所在位置。它的作用范围是整个 OS 空间。 35 | - 目录项(dentry):它主要从文件系统的文件路径的角度描述了文件路径中的一个特定的目录项(注:一系列目录项形成目录/文件路径)。它的作用范围是整个 OS 空间。对于 SFS 而言,inode(具体为 struct sfs_disk_inode)对应于物理磁盘上的具体对象,dentry(具体为 struct sfs_disk_entry)是一个内存实体,其中的 ino 成员指向对应的 inode number,另外一个成员是 file name(文件名). 36 | - 文件(file),它主要从进程的角度描述了一个进程在访问文件时需要了解的文件标识,文件读写的位置,文件引用情况等信息。它的作用范围是某一具体进程。 37 | 38 | 如果一个用户进程打开了一个文件,那么在 ucore 中涉及的相关数据结构(其中相关数据结构将在下面各个小节中展开叙述)和关系如下图所示: 39 | 40 | ![image](../lab8_figs/image002.png) 41 | 42 | **ucore 中文件相关关键数据结构及其关系** 43 | -------------------------------------------------------------------------------- /lab8/lab8_3_2_fs_interface.md: -------------------------------------------------------------------------------- 1 | ### 通用文件系统访问接口 2 | 3 | **文件和目录相关用户库函数** 4 | 5 | Lab8 中部分用户库函数与文件系统有关,我们先讨论对单个文件进行操作的系统调用,然后讨论对目录和文件系统进行操作的系统调用。 6 | 7 | 在文件操作方面,最基本的相关函数是 open、close、read、write。在读写一个文件之前,首先要用 open 系统调用将其打开。open 的第一个参数指定文件的路径名,可使用绝对路径名;第二个参数指定打开的方式,可设置为 O_RDONLY、O_WRONLY、O_RDWR,分别表示只读、只写、可读可写。在打开一个文件后,就可以使用它返回的文件描述符 fd 对文件进行相关操作。在使用完一个文件后,还要用 close 系统调用把它关闭,其参数就是文件描述符 fd。这样它的文件描述符就可以空出来,给别的文件使用。 8 | 9 | 读写文件内容的系统调用是 read 和 write。read 系统调用有三个参数:一个指定所操作的文件描述符,一个指定读取数据的存放地址,最后一个指定读多少个字节。在 C 程序中调用该系统调用的方法如下: 10 | 11 | ``` 12 | count = read(filehandle, buffer, nbytes); 13 | ``` 14 | 15 | 该系统调用会把实际读到的字节数返回给 count 变量。在正常情形下这个值与 nbytes 相等,但有时可能会小一些。例如,在读文件时碰上了文件结束符,从而提前结束此次读操作。 16 | 17 | 如果由于参数无效或磁盘访问错误等原因,使得此次系统调用无法完成,则 count 被置为-1。而 write 函数的参数与之完全相同。 18 | 19 | 对于目录而言,最常用的操作是跳转到某个目录,这里对应的用户库函数是 chdir。然后就需要读目录的内容了,即列出目录中的文件或目录名,这在处理上与读文件类似,即需要通过 opendir 函数打开目录,通过 readdir 来获取目录中的文件信息,读完后还需通过 closedir 函数来关闭目录。由于在 ucore 中把目录看成是一个特殊的文件,所以 opendir 和 closedir 实际上就是调用与文件相关的 open 和 close 函数。只有 readdir 需要调用获取目录内容的特殊系统调用 sys_getdirentry。而且这里没有写目录这一操作。在目录中增加内容其实就是在此目录中创建文件,需要用到创建文件的函数。 20 | 21 | **文件和目录访问相关系统调用** 22 | 23 | 与文件相关的 open、close、read、write 用户库函数对应的是 sys_open、sys_close、sys_read、sys_write 四个系统调用接口。与目录相关的 readdir 用户库函数对应的是 sys_getdirentry 系统调用。这些系统调用函数接口将通过 syscall 函数来获得 ucore 的内核服务。当到了 ucore 内核后,在调用文件系统抽象层的 file 接口和 dir 接口。 24 | -------------------------------------------------------------------------------- /lab8/lab8_3_3_1_fs_layout.md: -------------------------------------------------------------------------------- 1 | #### 文件系统的布局 2 | 3 | 文件系统通常保存在磁盘上。在本实验中,第三个磁盘(即 disk0,前两个磁盘分别是 4 | ucore.img 和 swap.img)用于存放一个 SFS 文件系统(Simple 5 | Filesystem)。通常文件系统中,磁盘的使用是以扇区(Sector)为单位的,但是为了实现简便,SFS 6 | 中以 block (4K,与内存 page 大小相等)为基本单位。 7 | 8 | SFS 文件系统的布局如下图所示。 9 | 10 | ![image](../lab8_figs/image003.png) 11 | 12 | 第 0 个块(4K)是超级块(superblock),它包含了关于文件系统的所有关键参数,当计算机被启动或文件系统被首次接触时,超级块的内容就会被装入内存。其定义如下: 13 | 14 | ``` 15 | struct sfs_super { 16 | uint32_t magic; /* magic number, should be SFS_MAGIC */ 17 | uint32_t blocks; /* # of blocks in fs */ 18 | uint32_t unused_blocks; /* # of unused blocks in fs */ 19 | char info[SFS_MAX_INFO_LEN + 1]; /* infomation for sfs */ 20 | }; 21 | ``` 22 | 23 | 可以看到,包含一个成员变量魔数 magic,其值为 0x2f8dbe2a,内核通过它来检查磁盘镜像是否是合法的 SFS img;成员变量 blocks 记录了 SFS 中所有 block 的数量,即 img 的大小;成员变量 unused_block 记录了 SFS 中还没有被使用的 block 的数量;成员变量 info 包含了字符串"simple file system"。 24 | 25 | 第 1 个块放了一个 root-dir 的 inode,用来记录根目录的相关信息。有关 inode 还将在后续部分介绍。这里只要理解 root-dir 是 SFS 文件系统的根结点,通过这个 root-dir 的 inode 信息就可以定位并查找到根目录下的所有文件信息。 26 | 27 | 从第 2 个块开始,根据 SFS 中所有块的数量,用 1 个 bit 来表示一个块的占用和未被占用的情况。这个区域称为 SFS 的 freemap 区域,这将占用若干个块空间。为了更好地记录和管理 freemap 区域,专门提供了两个文件 kern/fs/sfs/bitmap.[ch]来完成根据一个块号查找或设置对应的 bit 位的值。 28 | 29 | 最后在剩余的磁盘空间中,存放了所有其他目录和文件的 inode 信息和内容数据信息。需要注意的是虽然 inode 的大小小于一个块的大小(4096B),但为了实现简单,每个 inode 都占用一个完整的 block。 30 | 31 | 在 sfs_fs.c 文件中的 sfs_do_mount 函数中,完成了加载位于硬盘上的 SFS 文件系统的超级块 superblock 和 freemap 的工作。这样,在内存中就有了 SFS 文件系统的全局信息。 32 | -------------------------------------------------------------------------------- /lab8/lab8_3_3_sfs.md: -------------------------------------------------------------------------------- 1 | ### Simple FS 文件系统 2 | 3 | 这里我们没有按照从上到下先讲文件系统抽象层,再讲具体的文件系统。这是由于如果能够理解 Simple 4 | FS(简称 SFS)文件系统,就可更好地分析文件系统抽象层的设计。即从具体走向抽象。ucore 内核把所有文件都看作是字节流,任何内部逻辑结构都是专用的,由应用程序负责解释。但是 ucore 区分文件的物理结构。ucore 目前支持如下几种类型的文件: 5 | 6 | - 常规文件:文件中包括的内容信息是由应用程序输入。SFS 文件系统在普通文件上不强加任何内部结构,把其文件内容信息看作为字节。 7 | - 目录:包含一系列的 entry,每个 entry 包含文件名和指向与之相关联的索引节点(index node)的指针。目录是按层次结构组织的。 8 | - 链接文件:实际上一个链接文件是一个已经存在的文件的另一个可选择的文件名。 9 | - 设备文件:不包含数据,但是提供了一个映射物理设备(如串口、键盘等)到一个文件名的机制。可通过设备文件访问外围设备。 10 | - 管道:管道是进程间通讯的一个基础设施。管道缓存了其输入端所接受的数据,以便在管道输出端读的进程能一个先进先出的方式来接受数据。 11 | 12 | 在 lab8 中关注的主要是 SFS 支持的常规文件、目录和链接中的 hardlink 的设计实现。SFS 文件系统中目录和常规文件具有共同的属性,而这些属性保存在索引节点中。SFS 通过索引节点来管理目录和常规文件,索引节点包含操作系统所需要的关于某个文件的关键信息,比如文件的属性、访问许可权以及其它控制信息都保存在索引节点中。可以有多个文件名可指向一个索引节点。 13 | -------------------------------------------------------------------------------- /lab8/lab8_3_4_1_file_dir_interface.md: -------------------------------------------------------------------------------- 1 | #### file & dir 接口 2 | 3 | file&dir 接口层定义了进程在内核中直接访问的文件相关信息,这定义在 file 数据结构中,具体描述如下: 4 | 5 | ``` 6 | struct file { 7 | enum { 8 | FD_NONE, FD_INIT, FD_OPENED, FD_CLOSED, 9 | } status; //访问文件的执行状态 10 | bool readable; //文件是否可读 11 | bool writable; //文件是否可写 12 | int fd; //文件在filemap中的索引值 13 | off_t pos; //访问文件的当前位置 14 | struct inode *node; //该文件对应的内存inode指针 15 | int open_count; //打开此文件的次数 16 | }; 17 | ``` 18 | 19 | 而在 kern/process/proc.h 中的 proc_struct 结构中描述了进程访问文件的数据接口 files_struct,其数据结构定义如下: 20 | 21 | ``` 22 | struct files_struct { 23 | struct inode *pwd; //进程当前执行目录的内存inode指针 24 | struct file *fd_array; //进程打开文件的数组 25 | atomic_t files_count; //访问此文件的线程个数 26 | semaphore_t files_sem; //确保对进程控制块中fs_struct的互斥访问 27 | }; 28 | ``` 29 | 30 | 当创建一个进程后,该进程的 files_struct 将会被初始化或复制父进程的 files_struct。当用户进程打开一个文件时,将从 fd_array 数组中取得一个空闲 file 项,然后会把此 file 的成员变量 node 指针指向一个代表此文件的 inode 的起始地址。 31 | -------------------------------------------------------------------------------- /lab8/lab8_3_4_2_inode_interface.md: -------------------------------------------------------------------------------- 1 | #### inode 接口 2 | 3 | index 4 | node 是位于内存的索引节点,它是 VFS 结构中的重要数据结构,因为它实际负责把不同文件系统的特定索引节点信息(甚至不能算是一个索引节点)统一封装起来,避免了进程直接访问具体文件系统。其定义如下: 5 | 6 | ``` 7 | struct inode { 8 | union { //包含不同文件系统特定inode信息的union成员变量 9 | struct device __device_info; //设备文件系统内存inode信息 10 | struct sfs_inode __sfs_inode_info; //SFS文件系统内存inode信息 11 | } in_info; 12 | enum { 13 | inode_type_device_info = 0x1234, 14 | inode_type_sfs_inode_info, 15 | } in_type; //此inode所属文件系统类型 16 | atomic_t ref_count; //此inode的引用计数 17 | atomic_t open_count; //打开此inode对应文件的个数 18 | struct fs *in_fs; //抽象的文件系统,包含访问文件系统的函数指针 19 | const struct inode_ops *in_ops; //抽象的inode操作,包含访问inode的函数指针 20 | }; 21 | ``` 22 | 23 | 在 inode 中,有一成员变量为 in_ops,这是对此 inode 的操作函数指针列表,其数据结构定义如下: 24 | 25 | ``` 26 | struct inode_ops { 27 | unsigned long vop_magic; 28 | int (*vop_open)(struct inode *node, uint32_t open_flags); 29 | int (*vop_close)(struct inode *node); 30 | int (*vop_read)(struct inode *node, struct iobuf *iob); 31 | int (*vop_write)(struct inode *node, struct iobuf *iob); 32 | int (*vop_getdirentry)(struct inode *node, struct iobuf *iob); 33 | int (*vop_create)(struct inode *node, const char *name, bool excl, struct inode **node_store); 34 | int (*vop_lookup)(struct inode *node, char *path, struct inode **node_store); 35 | …… 36 | }; 37 | ``` 38 | 39 | 参照上面对 SFS 中的索引节点操作函数的说明,可以看出 inode_ops 是对常规文件、目录、设备文件所有操作的一个抽象函数表示。对于某一具体的文件系统中的文件或目录,只需实现相关的函数,就可以被用户进程访问具体的文件了,且用户进程无需了解具体文件系统的实现细节。 40 | -------------------------------------------------------------------------------- /lab8/lab8_3_4_fs_abstract.md: -------------------------------------------------------------------------------- 1 | ### 文件系统抽象层 - VFS 2 | 3 | 文件系统抽象层是把不同文件系统的对外共性接口提取出来,形成一个函数指针数组,这样,通用文件系统访问接口层只需访问文件系统抽象层,而不需关心具体文件系统的实现细节和接口。 4 | -------------------------------------------------------------------------------- /lab8/lab8_3_5_1_data_structure.md: -------------------------------------------------------------------------------- 1 | #### 关键数据结构 2 | 3 | 为了表示一个设备,需要有对应的数据结构,ucore 为此定义了 struct device,其描述如下: 4 | 5 | ``` 6 | struct device { 7 | size_t d_blocks; //设备占用的数据块个数 8 | size_t d_blocksize; //数据块的大小 9 | int (*d_open)(struct device *dev, uint32_t open_flags); //打开设备的函数指针 10 | int (*d_close)(struct device *dev); //关闭设备的函数指针 11 | int (*d_io)(struct device *dev, struct iobuf *iob, bool write); //读写设备的函数指针 12 | int (*d_ioctl)(struct device *dev, int op, void *data); //用ioctl方式控制设备的函数指针 13 | }; 14 | ``` 15 | 16 | 这个数据结构能够支持对块设备(比如磁盘)、字符设备(比如键盘、串口)的表示,完成对设备的基本操作。ucore 虚拟文件系统为了把这些设备链接在一起,还定义了一个设备链表,即双向链表 vdev_list,这样通过访问此链表,可以找到 ucore 能够访问的所有设备文件。 17 | 18 | 但这个设备描述没有与文件系统以及表示一个文件的 inode 数据结构建立关系,为此,还需要另外一个数据结构把 device 和 inode 联通起来,这就是 vfs_dev_t 数据结构: 19 | 20 | ``` 21 | // device info entry in vdev_list 22 | typedef struct { 23 | const char *devname; 24 | struct inode *devnode; 25 | struct fs *fs; 26 | bool mountable; 27 | list_entry_t vdev_link; 28 | } vfs_dev_t; 29 | ``` 30 | 31 | 利用 vfs_dev_t 数据结构,就可以让文件系统通过一个链接 vfs_dev_t 结构的双向链表找到 device 对应的 inode 数据结构,一个 inode 节点的成员变量 in_type 的值是 0x1234,则此 inode 的成员变量 in_info 将成为一个 device 结构。这样 inode 就和一个设备建立了联系,这个 inode 就是一个设备文件。 32 | -------------------------------------------------------------------------------- /lab8/lab8_3_5_2_stdout_dev_file.md: -------------------------------------------------------------------------------- 1 | #### stdout 设备文件 2 | 3 | **初始化** 4 | 5 | 既然 stdout 设备是设备文件系统的文件,自然有自己的 inode 结构。在系统初始化时,即只需如下处理过程 6 | 7 | ``` 8 | kern_init-->fs_init-->dev_init-->dev_init_stdout --> dev_create_inode 9 | --> stdout_device_init 10 | --> vfs_add_dev 11 | ``` 12 | 13 | 在 dev_init_stdout 中完成了对 stdout 设备文件的初始化。即首先创建了一个 inode,然后通过 stdout_device_init 完成对 inode 中的成员变量 inode-\>\_\_device_info 进行初始: 14 | 15 | 这里的 stdout 设备文件实际上就是指的 console 外设(它其实是串口、并口和 CGA 的组合型外设)。这个设备文件是一个只写设备,如果读这个设备,就会出错。接下来我们看看 stdout 设备的相关处理过程。 16 | 17 | **初始化** 18 | 19 | stdout 设备文件的初始化过程主要由 stdout_device_init 完成,其具体实现如下: 20 | 21 | ``` 22 | static void 23 | stdout_device_init(struct device *dev) { 24 | dev->d_blocks = 0; 25 | dev->d_blocksize = 1; 26 | dev->d_open = stdout_open; 27 | dev->d_close = stdout_close; 28 | dev->d_io = stdout_io; 29 | dev->d_ioctl = stdout_ioctl; 30 | } 31 | ``` 32 | 33 | 可以看到,stdout_open 函数完成设备文件打开工作,如果发现用户进程调用 open 函数的参数 flags 不是只写(O_WRONLY),则会报错。 34 | 35 | **访问操作实现** 36 | 37 | stdout_io 函数完成设备的写操作工作,具体实现如下: 38 | 39 | ``` 40 | static int 41 | stdout_io(struct device *dev, struct iobuf *iob, bool write) { 42 | if (write) { 43 | char *data = iob->io_base; 44 | for (; iob->io_resid != 0; iob->io_resid --) { 45 | cputchar(*data ++); 46 | } 47 | return 0; 48 | } 49 | return -E_INVAL; 50 | } 51 | ``` 52 | 53 | 可以看到,要写的数据放在 iob-\>io_base 所指的内存区域,一直写到 iob-\>io_resid 的值为 0 为止。每次写操作都是通过 cputchar 来完成的,此函数最终将通过 console 外设驱动来完成把数据输出到串口、并口和 CGA 显示器上过程。另外,也可以注意到,如果用户想执行读操作,则 stdout_io 函数直接返回错误值**-**E_INVAL。 54 | -------------------------------------------------------------------------------- /lab8/lab8_3_5_3_stdin_dev_file.md: -------------------------------------------------------------------------------- 1 | #### stdin 设备文件 2 | 3 | 这里的 stdin 设备文件实际上就是指的键盘。这个设备文件是一个只读设备,如果写这个设备,就会出错。接下来我们看看 stdin 设备的相关处理过程。 4 | 5 | **初始化** 6 | 7 | stdin 设备文件的初始化过程主要由 stdin_device_init 完成了主要的初始化工作,具体实现如下: 8 | 9 | ``` 10 | static void 11 | stdin_device_init(struct device *dev) { 12 | dev->d_blocks = 0; 13 | dev->d_blocksize = 1; 14 | dev->d_open = stdin_open; 15 | dev->d_close = stdin_close; 16 | dev->d_io = stdin_io; 17 | dev->d_ioctl = stdin_ioctl; 18 | 19 | p_rpos = p_wpos = 0; 20 | wait_queue_init(wait_queue); 21 | } 22 | ``` 23 | 24 | 相对于 stdout 的初始化过程,stdin 的初始化相对复杂一些,多了一个 stdin_buffer 缓冲区,描述缓冲区读写位置的变量 p_rpos、p_wpos 以及用于等待缓冲区的等待队列 wait_queue。在 stdin_device_init 函数的初始化中,也完成了对 p_rpos、p_wpos 和 wait_queue 的初始化。 25 | 26 | **访问操作实现** 27 | 28 | stdin_io 函数负责完成设备的读操作工作,具体实现如下: 29 | 30 | ``` 31 | static int 32 | stdin_io(struct device *dev, struct iobuf *iob, bool write) { 33 | if (!write) { 34 | int ret; 35 | if ((ret = dev_stdin_read(iob->io_base, iob->io_resid)) > 0) { 36 | iob->io_resid -= ret; 37 | } 38 | return ret; 39 | } 40 | return -E_INVAL; 41 | } 42 | ``` 43 | 44 | 可以看到,如果是写操作,则 stdin_io 函数直接报错返回。所以这也进一步说明了此设备文件是只读文件。如果此读操作,则此函数进一步调用 dev_stdin_read 函数完成对键盘设备的读入操作。dev_stdin_read 函数的实现相对复杂一些,主要的流程如下: 45 | 46 | ``` 47 | static int 48 | dev_stdin_read(char *buf, size_t len) { 49 | int ret = 0; 50 | bool intr_flag; 51 | local_intr_save(intr_flag); 52 | { 53 | for (; ret < len; ret ++, p_rpos ++) { 54 | try_again: 55 | if (p_rpos < p_wpos) { 56 | *buf ++ = stdin_buffer[p_rpos % stdin_BUFSIZE]; 57 | } 58 | else { 59 | wait_t __wait, *wait = &__wait; 60 | wait_current_set(wait_queue, wait, WT_KBD); 61 | local_intr_restore(intr_flag); 62 | 63 | schedule(); 64 | 65 | local_intr_save(intr_flag); 66 | wait_current_del(wait_queue, wait); 67 | if (wait->wakeup_flags == WT_KBD) { 68 | goto try_again; 69 | } 70 | break; 71 | } 72 | } 73 | } 74 | local_intr_restore(intr_flag); 75 | return ret; 76 | } 77 | ``` 78 | 79 | 在上述函数中可以看出,如果 p_rpos < p_wpos,则表示有键盘输入的新字符在 stdin_buffer 中,于是就从 stdin_buffer 中取出新字符放到 iobuf 指向的缓冲区中;如果 p_rpos \>=p_wpos,则表明没有新字符,这样调用 read 用户态库函数的用户进程就需要采用等待队列的睡眠操作进入睡眠状态,等待键盘输入字符的产生。 80 | 81 | 键盘输入字符后,如何唤醒等待键盘输入的用户进程呢?回顾 lab1 中的外设中断处理,可以了解到,当用户敲击键盘时,会产生键盘中断,在 trap_dispatch 函数中,当识别出中断是键盘中断(中断号为 IRQ_OFFSET + IRQ_KBD)时,会调用 dev_stdin_write 函数,来把字符写入到 stdin_buffer 中,且会通过等待队列的唤醒操作唤醒正在等待键盘输入的用户进程。 82 | -------------------------------------------------------------------------------- /lab8/lab8_3_5_dev_file_io_layer.md: -------------------------------------------------------------------------------- 1 | ### 设备层文件 IO 层 2 | 3 | 在本实验中,为了统一地访问设备,我们可以把一个设备看成一个文件,通过访问文件的接口来访问设备。目前实现了 stdin 设备文件文件、stdout 设备文件、disk0 设备。stdin 设备就是键盘,stdout 设备就是 CONSOLE(串口、并口和文本显示器),而 disk0 设备是承载 SFS 文件系统的磁盘设备。下面我们逐一分析 ucore 是如何让用户把设备看成文件来访问。 4 | -------------------------------------------------------------------------------- /lab8/lab8_3_6_labs_steps.md: -------------------------------------------------------------------------------- 1 | ### 实验执行流程概述 2 | 3 | 与实验七相比,实验八增加了文件系统,并因此实现了通过文件系统来加载可执行文件到内存中运行的功能,导致对进程管理相关的实现比较大的调整。我们来简单看看文件系统是如何初始化并能在 ucore 的管理下正常工作的。 4 | 5 | 首先看看 kern_init 函数,可以发现与 lab7 相比增加了对 fs_init 函数的调用。fs_init 函数就是文件系统初始化的总控函数,它进一步调用了虚拟文件系统初始化函数 vfs_init,与文件相关的设备初始化函数 dev_init 和 Simple FS 文件系统的初始化函数 sfs_init。这三个初始化函数联合在一起,协同完成了整个虚拟文件系统、SFS 文件系统和文件系统对应的设备(键盘、串口、磁盘)的初始化工作。其函数调用关系图如下所示: 6 | 7 | ![image](../lab8_figs/image004.png) 8 | 9 | 文件系统初始化调用关系图 10 | 11 | 参考上图,并结合源码分析,可大致了解到文件系统的整个初始化流程。vfs_init 主要建立了一个 device 12 | list 双向链表 vdev_list,为后续具体设备(键盘、串口、磁盘)以文件的形式呈现建立查找访问通道。dev_init 函数通过进一步调用 disk0/stdin/stdout_device_init 完成对具体设备的初始化,把它们抽象成一个设备文件,并建立对应的 inode 数据结构,最后把它们链入到 vdev_list 中。这样通过虚拟文件系统就可以方便地以文件的形式访问这些设备了。sfs_init 是完成对 Simple FS 的初始化工作,并把此实例文件系统挂在虚拟文件系统中,从而让 ucore 的其他部分能够通过访问虚拟文件系统的接口来进一步访问到 SFS 实例文件系统。 13 | -------------------------------------------------------------------------------- /lab8/lab8_3_7_1_file_open.md: -------------------------------------------------------------------------------- 1 | #### 打开文件 2 | 3 | 有了上述分析后,我们可以看看如果一个用户进程打开文件会做哪些事情?首先假定用户进程需要打开的文件已经存在在硬盘上。以 user/sfs_filetest1.c 为例,首先用户进程会调用在 main 函数中的如下语句: 4 | 5 | ``` 6 | int fd1 = safe_open("sfs\_filetest1", O_RDONLY); 7 | ``` 8 | 9 | 从字面上可以看出,如果 ucore 能够正常查找到这个文件,就会返回一个代表文件的文件描述符 fd1,这样在接下来的读写文件过程中,就直接用这样 fd1 来代表就可以了。那这个打开文件的过程是如何一步一步实现的呢? 10 | 11 | **通用文件访问接口层的处理流程** 12 | 13 | 首先进入通用文件访问接口层的处理流程,即进一步调用如下用户态函数: open-\>sys_open-\>syscall,从而引起系统调用进入到内核态。到了内核态后,通过中断处理例程,会调用到 sys_open 内核函数,并进一步调用 sysfile_open 内核函数。到了这里,需要把位于用户空间的字符串"sfs_filetest1"拷贝到内核空间中的字符串 path 中,并进入到文件系统抽象层的处理流程完成进一步的打开文件操作中。 14 | 15 | **文件系统抽象层的处理流程** 16 | 17 | 1. 分配一个空闲的 file 数据结构变量 file 在文件系统抽象层的处理中,首先调用的是 file_open 函数,它要给这个即将打开的文件分配一个 file 数据结构的变量,这个变量其实是当前进程的打开文件数组 current-\>fs_struct-\>filemap[]中的一个空闲元素(即还没用于一个打开的文件),而这个元素的索引值就是最终要返回到用户进程并赋值给变量 fd1。到了这一步还仅仅是给当前用户进程分配了一个 file 数据结构的变量,还没有找到对应的文件索引节点。 18 | 19 | 为此需要进一步调用 vfs_open 函数来找到 path 指出的文件所对应的基于 inode 数据结构的 VFS 索引节点 node。vfs_open 函数需要完成两件事情:通过 vfs_lookup 找到 path 对应文件的 inode;调用 vop_open 函数打开文件。 20 | 21 | 2. 找到文件设备的根目录“/”的索引节点需要注意,这里的 vfs_lookup 函数是一个针对目录的操作函数,它会调用 vop_lookup 函数来找到 SFS 文件系统中的“/”目录下的“sfs_filetest1”文件。为此,vfs_lookup 函数首先调用 get_device 函数,并进一步调用 vfs_get_bootfs 函数(其实调用了)来找到根目录“/”对应的 inode。这个 inode 就是位于 vfs.c 中的 inode 变量 bootfs_node。这个变量在 init_main 函数(位于 kern/process/proc.c)执行时获得了赋值。 22 | 23 | 3. 通过调用 vop_lookup 函数来查找到根目录“/”下对应文件 sfs_filetest1 的索引节点,,如果找到就返回此索引节点。 24 | 25 | 4. 把 file 和 node 建立联系。完成第 3 步后,将返回到 file_open 函数中,通过执行语句“file-\>node=node;”,就把当前进程的 current-\>fs_struct-\>filemap[fd](即 file 所指变量)的成员变量 node 指针指向了代表 sfs_filetest1 文件的索引节点 inode。这时返回 fd。经过重重回退,通过系统调用返回,用户态的 syscall-\>sys_open-\>open-\>safe_open 等用户函数的层层函数返回,最终把把 fd 赋值给 fd1。自此完成了打开文件操作。但这里我们还没有分析第 2 和第 3 步是如何进一步调用 SFS 文件系统提供的函数找位于 SFS 文件系统上的 sfs_filetest1 文件所对应的 sfs 磁盘 inode 的过程。下面需要进一步对此进行分析。 26 | 27 | **SFS 文件系统层的处理流程** 28 | 29 | 这里需要分析文件系统抽象层中没有彻底分析的 vop_lookup 函数到底做了啥。下面我们来看看。在 sfs_inode.c 中的 sfs_node_dirops 变量定义了“.vop_lookup = sfs_lookup”,所以我们重点分析 sfs_lookup 的实现。注意:在 lab8 中,为简化代码,sfs_lookup 函数中并没有实现能够对多级目录进行查找的控制逻辑(在 ucore_plus 中有实现)。 30 | 31 | sfs_lookup 有三个参数:node,path,node_store。其中 node 是根目录“/”所对应的 inode 节点;path 是文件 sfs_filetest1 的绝对路径/sfs_filetest1,而 node_store 是经过查找获得的 sfs_filetest1 所对应的 inode 节点。 32 | 33 | sfs_lookup 函数以“/”为分割符,从左至右逐一分解 path 获得各个子目录和最终文件对应的 inode 节点。在本例中是调用 sfs_lookup_once 查找以根目录下的文件 sfs_filetest1 所对应的 inode 节点。当无法分解 path 后,就意味着找到了 sfs_filetest1 对应的 inode 节点,就可顺利返回了。 34 | 35 | 当然这里讲得还比较简单,sfs_lookup_once 将调用 sfs_dirent_search_nolock 函数来查找与路径名匹配的目录项,如果找到目录项,则根据目录项中记录的 inode 所处的数据块索引值找到路径名对应的 SFS 磁盘 inode,并读入 SFS 磁盘 inode 对的内容,创建 SFS 内存 inode。 36 | -------------------------------------------------------------------------------- /lab8/lab8_3_7_2_file_read.md: -------------------------------------------------------------------------------- 1 | #### 读文件 2 | 3 | 读文件其实就是读出目录中的目录项,首先假定文件在磁盘上且已经打开。用户进程有如下语句: 4 | 5 | ``` 6 | read(fd, data, len); 7 | ``` 8 | 9 | 即读取 fd 对应文件,读取长度为 len,存入 data 中。下面来分析一下读文件的实现。 10 | 11 | **通用文件访问接口层的处理流程** 12 | 13 | 先进入通用文件访问接口层的处理流程,即进一步调用如下用户态函数:read-\>sys_read-\>syscall,从而引起系统调用进入到内核态。到了内核态以后,通过中断处理例程,会调用到 sys_read 内核函数,并进一步调用 sysfile_read 内核函数,进入到文件系统抽象层处理流程完成进一步读文件的操作。 14 | 15 | **文件系统抽象层的处理流程** 16 | 17 | 1. 检查错误,即检查读取长度是否为 0 和文件是否可读。 18 | 19 | 2. 分配 buffer 空间,即调用 kmalloc 函数分配 4096 字节的 buffer 空间。 20 | 21 | 3. 读文件过程 22 | 23 | [1] 实际读文件 24 | 25 | 循环读取文件,每次读取 buffer 大小。每次循环中,先检查剩余部分大小,若其小于 4096 字节,则只读取剩余部分的大小。然后调用 file_read 函数(详细分析见后)将文件内容读取到 buffer 中,alen 为实际大小。调用 copy_to_user 函数将读到的内容拷贝到用户的内存空间中,调整各变量以进行下一次循环读取,直至指定长度读取完成。最后函数调用层层返回至用户程序,用户程序收到了读到的文件内容。 26 | 27 | [2] file_read 函数 28 | 29 | 这个函数是读文件的核心函数。函数有 4 个参数,fd 是文件描述符,base 是缓存的基地址,len 是要读取的长度,copied_store 存放实际读取的长度。函数首先调用 fd2file 函数找到对应的 file 结构,并检查是否可读。调用 filemap_acquire 函数使打开这个文件的计数加 1。调用 vop_read 函数将文件内容读到 iob 中(详细分析见后)。调整文件指针偏移量 pos 的值,使其向后移动实际读到的字节数 iobuf_used(iob)。最后调用 filemap_release 函数使打开这个文件的计数减 1,若打开计数为 0,则释放 file。 30 | 31 | **SFS 文件系统层的处理流程** 32 | 33 | vop_read 函数实际上是对 sfs_read 的包装。在 sfs_inode.c 中 sfs_node_fileops 变量定义了.vop_read = sfs_read,所以下面来分析 sfs_read 函数的实现。 34 | 35 | sfs_read 函数调用 sfs_io 函数。它有三个参数,node 是对应文件的 inode,iob 是缓存,write 表示是读还是写的布尔值(0 表示读,1 表示写),这里是 0。函数先找到 inode 对应 sfs 和 sin,然后调用 sfs_io_nolock 函数进行读取文件操作,最后调用 iobuf_skip 函数调整 iobuf 的指针。 36 | 37 | 在 sfs_io_nolock 函数中,先计算一些辅助变量,并处理一些特殊情况(比如越界),然后有 sfs_buf_op = sfs_rbuf,sfs_block_op = sfs_rblock,设置读取的函数操作。接着进行实际操作,先处理起始的没有对齐到块的部分,再以块为单位循环处理中间的部分,最后处理末尾剩余的部分。每部分中都调用 sfs_bmap_load_nolock 函数得到 blkno 对应的 inode 编号,并调用 sfs_rbuf 或 sfs_rblock 函数读取数据(中间部分调用 sfs_rblock,起始和末尾部分调用 sfs_rbuf),调整相关变量。完成后如果 offset + alen \> din-\>fileinfo.size(写文件时会出现这种情况,读文件时不会出现这种情况,alen 为实际读写的长度),则调整文件大小为 offset + alen 并设置 dirty 变量。 38 | 39 | sfs_bmap_load_nolock 函数将对应 sfs_inode 的第 index 个索引指向的 block 的索引值取出存到相应的指针指向的单元(ino_store)。它调用 sfs_bmap_get_nolock 来完成相应的操作。sfs_rbuf 和 sfs_rblock 函数最终都调用 sfs_rwblock_nolock 函数完成操作,而 sfs_rwblock_nolock 函数调用 dop_io-\>disk0_io-\>disk0_read_blks_nolock-\>ide_read_secs 完成对磁盘的操作。 40 | -------------------------------------------------------------------------------- /lab8/lab8_3_7_file_op_implement.md: -------------------------------------------------------------------------------- 1 | ### 文件操作实现 2 | -------------------------------------------------------------------------------- /lab8/lab8_3_fs_design_implement.md: -------------------------------------------------------------------------------- 1 | ## 文件系统设计与实现 2 | -------------------------------------------------------------------------------- /lab8/lab8_4_lab_requirement.md: -------------------------------------------------------------------------------- 1 | ## 实验报告要求 2 | 3 | 从 git server 网站上取得 ucore_lab 后,进入目录 labcodes/lab8,完成实验要求的各个练习。在实验报告中回答所有练习中提出的问题。 4 | 在目录 labcodes/lab8 下存放实验报告,实验报告文档命名为 lab8.md,使用**markdown**格式。 5 | 对于 lab8 中编程任务,完成编写之后,再通过 git push 命令把代码同步回 git server 网站。最后请一定提前或按时提交到 git server 网站。 6 | 7 | 注意有“LAB8”的注释,这是需要主要修改的内容。代码中所有需要完成的地方 challenge 除外)都有“LAB8”和“YOUR CODE”的注释,请在提交时特别注意保持注释,并将“YOUR CODE”替换为自己的学号,并且将所有标有对应注释的部分填上正确的代码。 8 | -------------------------------------------------------------------------------- /lab8_figs/image001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab8_figs/image001.png -------------------------------------------------------------------------------- /lab8_figs/image002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab8_figs/image002.png -------------------------------------------------------------------------------- /lab8_figs/image003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab8_figs/image003.png -------------------------------------------------------------------------------- /lab8_figs/image004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearningOS/ucore_os_docs/864e21f36b670099c6ffd20f3c8ce4d0b4ada89a/lab8_figs/image004.png -------------------------------------------------------------------------------- /update_book.sh: -------------------------------------------------------------------------------- 1 | echo "building, it will take about 2 min" 2 | # if you firt run this script, you should run 'gitbook install' 3 | gitbook build 4 | cd _book 5 | rm .gitignore 6 | rm update_book.sh 7 | git init 8 | git remote add origin https://github.com/LearningOS/ucore_os_webdocs.git 9 | git add . 10 | git commit -m "update" 11 | git push origin master -f 12 | cd .. 13 | --------------------------------------------------------------------------------