├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── Linuxdoc ├── Design.md └── Drafts │ ├── MMU.md │ ├── Memmap.md │ ├── PMM.md │ ├── Page.md │ ├── dts中断登记.md │ ├── linux向量初始化.md │ ├── linux硬件支持.md │ ├── riscv硬件外中断处理流程.md │ ├── 中断控制器回调函数.md │ ├── 实际硬件中断初始化.md │ ├── 热插拔.md │ ├── 研究linux中的硬件中断.md │ └── 键盘输入的按键过程.md ├── README.md ├── code ├── file_system │ ├── init_rfs_device.md │ ├── rfs_add_direntry.md │ ├── rfs_create.md │ ├── rfs_disk_stat.md │ ├── rfs_format_dev.md │ ├── rfs_hook_opendir.md │ ├── rfs_lookup.md │ ├── rfs_lseek.md │ ├── rfs_mkdir.md │ ├── rfs_read.md │ ├── rfs_write.md │ ├── rfs_write_back_vinode.md │ ├── vfs_mkdir.md │ ├── vfs_mount.md │ └── vfs_open.md ├── lab2_update.md ├── lab2的内核初始化改动.md ├── make_addr_line.md ├── 代理内核启动程序.md ├── 硬中断处理程序.md ├── 系统调用服务程序.md ├── 软中断入口程序.md └── 软中断用户接口.md ├── doc ├── CPU如何同步自然时间.md ├── C语言嵌入汇编概述.md ├── ELF文件头解析.md ├── ELF文件如何组织调试信息.md ├── ELF文件概述.md ├── ELF程序段头解析.md ├── File_System_Overview.md ├── Linux中的硬链接.md ├── RISC-V函数帧解析.md ├── RISC-V控制状态寄存器概述.md ├── RISC-V通用寄存器概述.md ├── Spike中HTIF的原理.md ├── Spike仿真层.md ├── VFS_Layer_Communication_in_PKE.md ├── exec的策略.md ├── lab1是如何避免用户进程写入内核的内存区域的.md ├── lab1的内存管理漏洞.md ├── likely.md ├── ramfs文件系统.md ├── vinode与rfs_inode的设计模式.md ├── 什么是panic.md ├── 什么是外部时钟中断.md ├── 什么是链接脚本.md ├── 信号量和自旋锁.md ├── 内核堆.md ├── 内核文件系统组织架构.md ├── 写时拷贝的实现.md ├── 实现相对路径.md ├── 实际内核的stdin和stdout实现.md ├── 实验概述.md ├── 异步信号量的实现.md ├── 操作系统的黑盒子视角.md ├── 数据段、堆栈和内存.md ├── 文件系统.md ├── 文件系统的工作方式.md ├── 文件系统设计范式.md ├── 时钟中断的硬件实现原理.md ├── 硬中断到软中断的切换过程.md ├── 硬件对硬中断的响应过程.md ├── 硬件对软中断的响应过程.md └── 软硬中断的触发方式.md ├── images ├── lab2 │ ├── config_h.png │ ├── elf_c.png │ ├── fig1_7.png │ ├── fig1_8.png │ ├── kernel_c1.png │ ├── kernel_c2.png │ ├── kernel_c3.png │ ├── kernel_c4.png │ ├── memory.png │ ├── process_c1.png │ ├── process_c2.png │ ├── process_h1.png │ ├── riscv_h.png │ ├── strap_vector.png │ └── syscall_c.png └── 汇编帧构造示意图.jpg └── lab ├── lab1_1.md ├── lab1_2.md ├── lab1_3.md ├── lab1_challenge1.md ├── lab1_challenge1_m2.md ├── lab1_challenge2.md ├── lab1_challenge3.md ├── lab2.md ├── lab2_1.md ├── lab2_2.md ├── lab2_3.md ├── lab2_challenge1.md ├── lab2_challenge2.md ├── lab2_challenge3.md ├── lab3.md ├── lab3_1.md ├── lab3_2.md ├── lab3_challenge1.md ├── lab3_challenge2.md ├── lab3_challenge3.md ├── lab4.md ├── lab4_1.md ├── lab4_2.md ├── lab4_3.md ├── lab4_challenge1.md ├── lab4_challenge2.md ├── shell脚本工具.md ├── 环境配置.md └── 调试工具.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | draft.txt 35 | prompts/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.quickSuggestions": { 3 | "other": "off" 4 | } 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Jingyuan Tian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Linuxdoc/Design.md: -------------------------------------------------------------------------------- 1 | # 我们的linux系统总设计 2 | 3 | ## PMM PhysicalMemoryManager 物理页分配器 4 | 在启动时计算和分配,物理页元数据表。 5 | 支持分配单页和释放单页,和引用计数 6 | 简单使用链表 7 | 8 | 9 | ## VMM VirtualMemoryManager 虚拟页分配器 10 | 与进程控制块中的mm_struct互动。 11 | 12 | 13 | 14 | ### 通用内存分配 15 | - **kmalloc(size, flags)** - 分配连续的物理内存 16 | - **kfree(ptr)** - 释放通过kmalloc分配的内存 17 | 18 | 19 | 20 | ## Pagetable 页表 -------------------------------------------------------------------------------- /Linuxdoc/Drafts/Memmap.md: -------------------------------------------------------------------------------- 1 | 这是个很好的问题!Linux内核在处理`mem_map`数组大小时采用了灵活的方法,因为系统的物理内存配置在启动时才能确定。 2 | 3 | Linux通过以下方式确定和处理`mem_map`的大小: 4 | 5 | 1. **启动时内存探测**: 6 | 在系统引导过程中,内核会探测可用的物理内存范围,通常通过BIOS/UEFI、设备树或其他平台特定机制获取内存信息。 7 | 8 | 2. **动态分配**: 9 | `mem_map`不是静态分配的固定大小数组,而是在内核初始化期间根据探测到的内存大小动态分配。每个物理页面对应一个`struct page`结构体。 10 | 11 | 3. **基于区域的分配**: 12 | 现代Linux内核实际上按内存区域(memory zones)和节点(nodes)组织`mem_map`,而不是单一的全局数组: 13 | ```c 14 | // 对于UMA系统 15 | mem_map = alloc_bootmem_node(NODE_DATA(0), page_size * max_pages); 16 | 17 | // 对于NUMA系统 18 | NODE_DATA(nid)->node_mem_map = alloc_bootmem_node(...); 19 | ``` 20 | 21 | 4. **稀疏内存支持**: 22 | 对于有内存空洞或可热插拔内存的系统,Linux使用稀疏内存模型(SPARSEMEM),将物理地址空间划分为固定大小的"sections": 23 | ```c 24 | struct mem_section { 25 | struct page *section_mem_map; 26 | /* 其他字段 */ 27 | }; 28 | ``` 29 | 这里只为实际存在的内存部分分配`struct page`数组,而不是为整个物理地址空间分配。 30 | 31 | 5. **页面偏移计算**: 32 | 系统使用`page_offset_base`和物理页号来计算从物理地址到`struct page`数组元素的映射: 33 | ```c 34 | #define __pa_to_page(pa) (pfn_to_page(__pa_to_pfn(pa))) 35 | #define pfn_to_page(pfn) (_pfn_to_page(pfn)) 36 | ``` 37 | 38 | 6. **内存热插拔**: 39 | 当添加新内存(如在服务器上热插拔内存模块)时,内核会为新页面动态分配`struct page`结构并整合到页面管理系统中。 40 | 41 | 这种灵活的设计让Linux内核能够适应从嵌入式设备到大型服务器的各种内存配置,并支持动态变化的内存大小。通过稀疏内存模型和分段分配,内核避免了为潜在的巨大物理地址空间预分配`mem_map`,只为实际存在的物理内存分配所需的元数据空间。 -------------------------------------------------------------------------------- /Linuxdoc/Drafts/PMM.md: -------------------------------------------------------------------------------- 1 | Linux内核的物理页分配器提供了一系列接口,以下是一些核心接口的声明: 2 | 3 | ## 页面分配接口 4 | 5 | ```c 6 | // 分配单个页面 7 | struct page *alloc_page(gfp_t gfp_mask); 8 | 9 | // 分配2^order个连续页面 10 | struct page *alloc_pages(gfp_t gfp_mask, unsigned int order); 11 | 12 | // 分配指定页面数的连续物理内存并返回其虚拟地址 13 | void *__get_free_pages(gfp_t gfp_mask, unsigned int order); 14 | 15 | // 分配单个页面并返回其虚拟地址 16 | void *__get_free_page(gfp_t gfp_mask); 17 | 18 | // 分配小内存块 19 | void *kmalloc(size_t size, gfp_t flags); 20 | ``` 21 | 22 | ## 页面释放接口 23 | 24 | ```c 25 | // 释放由alloc_page(s)分配的页面 26 | void __free_page(struct page *page); 27 | void __free_pages(struct page *page, unsigned int order); 28 | 29 | // 释放由__get_free_page(s)分配的内存 30 | void free_page(unsigned long addr); 31 | void free_pages(unsigned long addr, unsigned int order); 32 | 33 | // 释放kmalloc分配的内存 34 | void kfree(const void *); 35 | ``` 36 | 37 | ## 页面预留接口 38 | 39 | ```c 40 | // 预留特定区域的物理内存 41 | int reserve_pages(phys_addr_t start, phys_addr_t end); 42 | 43 | // 释放预留的物理内存 44 | void free_reserved_area(void *start, void *end, int poison, const char *s); 45 | ``` 46 | 47 | ## 内存信息查询接口 48 | 49 | ```c 50 | // 获取内存区域信息 51 | unsigned long nr_free_pages(void); 52 | int nr_free_pages_zone(int zone); 53 | 54 | // 获取整体内存使用信息 55 | void si_meminfo(struct sysinfo *val); 56 | ``` 57 | 58 | ## 内存分配控制接口 59 | 60 | ```c 61 | // 设置内存分配掩码 62 | gfp_t gfp_allowed_mask; 63 | 64 | // 内存分配失败处理 65 | void *memalloc_noio_save(void); 66 | void memalloc_noio_restore(void *); 67 | ``` 68 | 69 | 在Linux中,物理页分配的GFP(Get Free Page)标志控制分配行为,常见的有: 70 | 71 | - `GFP_KERNEL`:可能休眠的内核空间分配 72 | - `GFP_ATOMIC`:不可休眠的中断上下文分配 73 | - `GFP_USER`:用户空间分配 74 | - `GFP_HIGHUSER`:用户空间分配,使用高区内存 75 | - `__GFP_ZERO`:分配后填充零值 76 | 77 | Linux使用伙伴系统(buddy system)来管理物理页,并结合slab分配器处理小内存块分配。内核不直接使用引用计数来管理物理页面的生命周期,而是使用更复杂的页面管理结构(`struct page`),其中包含引用计数字段`_refcount`来跟踪页面的使用情况,防止过早释放。 -------------------------------------------------------------------------------- /Linuxdoc/Drafts/Page.md: -------------------------------------------------------------------------------- 1 | Linux内核使用`struct page`结构体来表示物理页的元数据。这个结构体在Linux内核中至关重要,它为每个物理页面维护状态信息。以下是`struct page`的核心结构(基于较新的Linux内核版本): 2 | 3 | ```c 4 | struct page { 5 | unsigned long flags; /* 原子标志,页面状态标记 */ 6 | 7 | /* 8 | * 五种不同使用情况的共用体: 9 | * 1. 匿名映射时的映射信息 10 | * 2. 文件映射时的地址空间 11 | * 3. slab分配器使用信息 12 | * 4. 页表页使用信息 13 | * 5. 移动页使用信息 14 | */ 15 | union { 16 | struct { 17 | struct list_head lru; /* 用于页面回收的LRU链表 */ 18 | struct address_space *mapping; /* 所属的地址空间 */ 19 | pgoff_t index; /* 在映射中的索引 */ 20 | unsigned long private; /* 私有数据指针 */ 21 | }; 22 | struct { 23 | struct list_head slab_list; /* slab分配器链表 */ 24 | struct kmem_cache *slab_cache; /* slab所属缓存 */ 25 | void *freelist; /* 空闲对象链表 */ 26 | }; 27 | struct { 28 | struct page_pool *pp; /* 页池指针 */ 29 | struct device *dev; /* 设备结构 */ 30 | }; 31 | /* ... 其他专用类型 ... */ 32 | }; 33 | 34 | union { 35 | atomic_t _mapcount; /* 页表映射计数 */ 36 | unsigned int active; /* SLAB活跃对象 */ 37 | int units; /* 设备中的单元 */ 38 | }; 39 | 40 | atomic_t _refcount; /* 引用计数,跟踪页面使用者数量 */ 41 | 42 | /* 用于复合页(compound page)的字段 */ 43 | struct page *first_page; /* 指向复合页首页的指针 */ 44 | unsigned long compound_head; /* 复合页首页指针编码,复合页中所有页面共享 */ 45 | 46 | /* 用于页表、设备I/O和文件页的其他字段... */ 47 | }; 48 | ``` 49 | 50 | 这个结构体的特点和使用方式: 51 | 52 | 1. **内存高效**:尽管看起来很复杂,但Linux内核采用了许多技巧使每个`struct page`尽可能小,因为系统中通常有数百万个页面。 53 | 54 | 2. **多功能设计**:使用联合体(union)以不同方式重用相同的内存空间,根据页面当前的用途适配不同场景。 55 | 56 | 3. **引用计数**:`_refcount`字段跟踪页面被使用的次数,当计数归零时页面可以被释放。 57 | 58 | 4. **映射计数**:`_mapcount`跟踪页面被映射到页表中的次数。 59 | 60 | 5. **页面状态**:`flags`字段包含页面状态标志,如`PG_locked`(页面锁定)、`PG_dirty`(页面脏)等。 61 | 62 | 6. **复合页支持**:系统可以将多个连续页面组合成一个"复合页"以支持大页(huge pages)等功能。 63 | 64 | 7. **内存压缩**:在64位架构中,通过各种巧妙的位操作和指针压缩技术减小结构体大小。 65 | 66 | Linux内核中,这个结构体会被组织到全局数组`mem_map`中,或者在NUMA系统中分布在每个内存节点的`node_mem_map`数组中,允许通过物理页号快速索引到对应的`struct page`结构。 -------------------------------------------------------------------------------- /Linuxdoc/Drafts/dts中断登记.md: -------------------------------------------------------------------------------- 1 | 您提供的是SiFive FU740芯片的设备树源文件(DTS),这个文件描述了硬件的详细信息,包括中断控制器和各种外设的中断配置。这对理解RISC-V的中断系统非常有帮助。 2 | 3 | 让我来解释一下DTS中与中断相关的重要部分: 4 | 5 | 1. **CPU中断控制器**: 6 | 每个CPU核心都有自己的中断控制器: 7 | ``` 8 | cpu0_intc: interrupt-controller { 9 | #interrupt-cells = <1>; 10 | compatible = "riscv,cpu-intc"; 11 | interrupt-controller; 12 | }; 13 | ``` 14 | 这是RISC-V架构中的核心本地中断控制器,处理直接发送到特定CPU核心的中断。 15 | 16 | 2. **PLIC (Platform-Level Interrupt Controller)**: 17 | ``` 18 | plic0: interrupt-controller@c000000 { 19 | #interrupt-cells = <1>; 20 | compatible = "sifive,fu540-c000-plic", "sifive,plic-1.0.0"; 21 | reg = <0x0 0xc000000 0x0 0x4000000>; 22 | riscv,ndev = <69>; 23 | interrupt-controller; 24 | interrupts-extended = ... 25 | ``` 26 | PLIC是SoC级别的中断控制器,负责管理所有外设中断并将它们路由到CPU。`riscv,ndev = <69>`表示这个PLIC支持69个外部中断源。 27 | 28 | 3. **外设中断配置**: 29 | 以UART为例: 30 | ``` 31 | uart0: serial@10010000 { 32 | compatible = "sifive,fu740-c000-uart", "sifive,uart0"; 33 | reg = <0x0 0x10010000 0x0 0x1000>; 34 | interrupt-parent = <&plic0>; 35 | interrupts = <39>; 36 | ... 37 | } 38 | ``` 39 | 这表示: 40 | - `interrupt-parent = <&plic0>` - UART0的中断由PLIC处理 41 | - `interrupts = <39>` - UART0使用中断号39 42 | 43 | 4. **中断路由**: 44 | 在PLIC定义中: 45 | ``` 46 | interrupts-extended = 47 | <&cpu0_intc 0xffffffff>, 48 | <&cpu1_intc 0xffffffff>, <&cpu1_intc 9>, 49 | <&cpu2_intc 0xffffffff>, <&cpu2_intc 9>, 50 | ... 51 | ``` 52 | 这定义了PLIC如何将中断路由到各个CPU核心。`0xffffffff`对应外部中断,`9`对应RISC-V架构中的监督者模式外部中断(S-mode External Interrupt)。 53 | 54 | 在RISC-V Linux中,外设中断的处理流程是: 55 | 56 | 1. 外设(如UART)产生中断 57 | 2. 中断信号发送到PLIC 58 | 3. PLIC根据优先级确定发送到哪个CPU核心 59 | 4. CPU收到中断后跳转到`handle_exception`处理程序 60 | 5. 系统识别为外部中断并调用`generic_handle_arch_irq` 61 | 6. 最终调用到外设驱动注册的特定中断处理函数 62 | 63 | 这个DTS文件的作用是告诉Linux内核硬件中断系统的布局,便于内核正确初始化中断子系统并管理外设中断。 -------------------------------------------------------------------------------- /Linuxdoc/Drafts/linux向量初始化.md: -------------------------------------------------------------------------------- 1 | 在Linux内核的启动流程中,`start_kernel()`函数是内核初始化的主要入口点。硬件中断的设置是在这个函数调用链中的几个关键位置完成的: 2 | 3 | 1. 在`start_kernel()`函数中,会调用`setup_arch()`进行架构相关的初始化,这其中包含了一些与中断控制器相关的初始化。 4 | 5 | 2. 之后会调用`init_IRQ()`函数,这是专门用于设置和初始化中断系统的函数。它会初始化中断控制器并设置中断描述符表等。 6 | 7 | 3. 在`trap_init()`中会设置异常处理程序,这些处理程序响应CPU自身的异常,如页错误、除零错误等。 8 | 9 | 4. 最后,在调用`time_init()`时会初始化时钟中断,这是系统中最基本也是最频繁的中断之一。 10 | 11 | 更具体来说,对于不同架构,中断初始化的实现细节会有所不同: 12 | 13 | - 在x86架构上,中断描述符表(IDT)会在`trap_init()`中设置 14 | - 中断控制器(如APIC或旧的PIC)会在`init_IRQ()`中初始化 15 | - 时钟中断通常在`time_init()`函数中设置 16 | 17 | 记住,Linux内核的具体初始化过程可能因版本而异。如果你需要查看特定版本的实现细节,最好直接查看该版本的源代码。 18 | 19 | arch/riscv/kernel/setup.c/setup_arch(char**) -------------------------------------------------------------------------------- /Linuxdoc/Drafts/linux硬件支持.md: -------------------------------------------------------------------------------- 1 | Linux在硬件驱动程序之上提供了几种关键接口来允许应用程序与硬件交互: 2 | 3 | 1. **字符设备接口**:以字节流方式访问设备,如终端、串口和某些传感器。通过特殊文件节点(通常在/dev目录下)访问。 4 | 5 | 2. **块设备接口**:用于访问存储设备(如硬盘、SSD)。允许应用程序以块为单位读写数据,而不关心底层物理特性。 6 | 7 | 3. **网络设备接口**:通过套接字API抽象网络硬件,提供BSD套接字接口和特定Linux的扩展。 8 | 9 | 4. **sysfs文件系统**:在/sys目录下以分层方式展示系统硬件及其属性,允许用户空间程序读取和有时修改设备参数。 10 | 11 | 5. **ioctl系统调用**:提供设备特定操作的通用接口,实现不适合标准读/写操作的功能。 12 | 13 | 6. **mmap接口**:允许将设备内存直接映射到进程地址空间,实现零拷贝操作。 14 | 15 | 7. **udev/设备管理**:动态管理设备节点创建,提供设备热插拔支持。 16 | 17 | 8. **输入事件接口**:通过/dev/input提供统一方式访问键盘、鼠标等输入设备。 18 | 19 | 这些接口共同构成了应用程序与底层硬件之间的抽象层,遵循"一切皆文件"的Unix哲学。 20 | 21 | -------------------------------------------------------------------------------- /Linuxdoc/Drafts/riscv硬件外中断处理流程.md: -------------------------------------------------------------------------------- 1 | 在RISC-V架构中,中断处理的流程确实有所不同,但基本原理类似。 2 | 3 | 当中断发生时,RISC-V处理器会: 4 | 5 | 1. 首先跳转到中断向量表中设置的地址(由`mtvec`寄存器指定) 6 | 2. 接着通过读取`mcause`寄存器来确定中断的类型和原因 7 | 3. 根据`mcause`的值,处理器可以确定是什么类型的中断(外部中断、软件中断、定时器中断等) 8 | 9 | 对于外设数据(例如类似键盘输入这样的数据),RISC-V通常会: 10 | - 通过内存映射I/O(MMIO)而不是专用I/O端口来访问 11 | - 在确定中断类型后,处理程序会访问相应设备的内存映射地址来读取数据 12 | 13 | 关于维护责任: 14 | - 在RISC-V中,这个过程通常是由一个结合了BIOS和固件功能的SBI(Supervisor Binary Interface)或SEE(Supervisor Execution Environment)来处理的 15 | - SBI提供了一个抽象层,位于硬件和操作系统之间 16 | - 基本的中断向量设置和初始中断处理通常是由这个固件层负责的 17 | - 但具体的设备驱动程序和中断处理通常由操作系统完成 18 | 19 | 所以,在RISC-V中,基础中断机制由固件(类似BIOS的功能)设置,但具体的设备中断处理通常由操作系统实现。时钟中断特别地,通常会读取`mtime`或`time`寄存器来获取时间信息,这个过程可能由SBI提供支持,但详细处理通常在操作系统层面完成。 20 | 21 | 22 | MMIO(内存映射输入/输出)的实现和维护涉及硬件和软件层面的协作: 23 | 24 | ## 硬件层面 25 | 26 | 1. **地址空间分配**: 27 | - 在处理器设计阶段,特定的物理地址范围被保留用于MMIO 28 | - 这些地址不指向实际RAM,而是映射到外设的寄存器 29 | 30 | 2. **地址解码**: 31 | - 当CPU发出内存访问请求时,地址解码器会检查该地址 32 | - 如果地址落在MMIO范围内,访问请求会被路由到相应的外设,而不是内存控制器 33 | 34 | 3. **总线控制**: 35 | - 特定的总线协议(如AXI、APB等)处理CPU和外设之间的通信 36 | - 这些协议定义了如何传输读写请求和数据 37 | 38 | ## 软件层面 39 | 40 | 1. **固件初始化**: 41 | - SBI/SEE(RISC-V固件)初始化阶段会设置基本的MMIO映射 42 | - 它可能配置内存管理单元(MMU)或物理内存保护单元(PMP)来控制对这些区域的访问 43 | 44 | 2. **操作系统管理**: 45 | - 操作系统维护MMIO区域的虚拟内存映射(如果使用MMU) 46 | - 它需确保这些区域正确映射,并具有适当的缓存属性(通常设为"不可缓存") 47 | - 操作系统通过驱动程序提供对这些区域的访问API 48 | 49 | 3. **设备驱动程序**: 50 | - 驱动程序知道设备寄存器的具体地址映射和使用方法 51 | - 它们通过读写这些内存映射地址与设备交互 52 | - 驱动程序还处理设备发出的中断 53 | 54 | ## 访问过程 55 | 56 | 当软件需要访问外设时: 57 | 58 | 1. 程序调用操作系统API或直接访问映射地址(在特权模式下) 59 | 2. CPU执行load/store指令,使用目标外设的映射地址 60 | 3. 地址解码逻辑将这个访问路由到外设,而不是RAM 61 | 4. 外设响应请求,执行相应操作并可能返回数据 62 | 5. 如果操作触发外设状态变化,外设可能产生中断 63 | 64 | 整个MMIO系统的维护是硬件设计者、固件开发者和操作系统开发者共同负责的,每一层都有各自的职责,共同确保CPU可以通过统一的内存访问指令与各种外设进行交互。 -------------------------------------------------------------------------------- /Linuxdoc/Drafts/中断控制器回调函数.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CubeLander/hust-pke-docs/c1153a768e20eecabee2f0dbf40057c4ec8102d8/Linuxdoc/Drafts/中断控制器回调函数.md -------------------------------------------------------------------------------- /Linuxdoc/Drafts/热插拔.md: -------------------------------------------------------------------------------- 1 | 在Linux系统中热插拔硬盘或键盘时,系统会经历一系列步骤来识别、配置并使设备可用。以下是这个过程的详细解释: 2 | 3 | ### 硬盘热插拔过程 4 | 5 | 1. **物理连接和硬件检测**: 6 | - 当硬盘通过USB、SATA、NVMe等接口插入时,硬件控制器产生中断信号 7 | - 内核中相应的总线驱动程序处理这个中断 8 | 9 | 2. **内核事件处理**: 10 | - 内核识别新设备并读取其基本信息(如供应商ID、产品ID) 11 | - 创建内核内部设备对象,并匹配适当的驱动程序 12 | - 相关驱动程序初始化设备,创建必要的数据结构 13 | 14 | 3. **udev处理**: 15 | - 内核通过netlink套接字向用户空间发送uevent通知 16 | - udevd守护进程接收这个事件并处理它 17 | - udev应用规则集,根据设备属性决定如何命名和配置设备 18 | - 在/dev目录下创建合适的设备节点(如/dev/sdb) 19 | 20 | 4. **磁盘分区识别**: 21 | - 内核扫描硬盘的分区表 22 | - 为每个分区创建设备节点(如/dev/sdb1, /dev/sdb2) 23 | 24 | 5. **文件系统挂载**: 25 | - 如果配置了自动挂载(通过udisks2、gvfs或autofs),系统会尝试识别分区上的文件系统并挂载它 26 | - 挂载点通常创建在/media/<用户名>/<卷标>或/run/media/<用户名>/<卷标> 27 | 28 | ### 键盘热插拔过程 29 | 30 | 1. **物理连接和硬件检测**: 31 | - 键盘(通常通过USB或蓝牙)连接时,产生硬件中断 32 | - 相应的USB或蓝牙子系统处理这个中断 33 | 34 | 2. **内核识别**: 35 | - 内核识别设备类型为HID(人机接口设备) 36 | - 加载HID驱动程序和键盘特定驱动程序 37 | 38 | 3. **输入子系统集成**: 39 | - 键盘被注册到Linux输入子系统 40 | - 创建输入事件接口(如/dev/input/event) 41 | - 更新/proc/bus/input/devices文件,添加新键盘信息 42 | 43 | 4. **udev处理**: 44 | - udev接收键盘添加事件 45 | - 应用键盘特定规则(如设置键盘布局、重复率等) 46 | - 创建符号链接(如/dev/input/by-id/)以便更容易识别设备 47 | 48 | 5. **X服务器或Wayland合成器检测**: 49 | - 图形环境检测到新键盘 50 | - 应用系统键盘设置(布局、快捷键等) 51 | - 使键盘立即可用于图形环境中 52 | 53 | 6. **桌面环境通知**: 54 | - 桌面环境(如GNOME或KDE)可能显示键盘已连接的通知 55 | - 某些桌面环境可能提供快速配置选项 56 | 57 | 整个过程是高度自动化的,依赖于Linux内核的设备模型、udev系统和各种用户空间守护进程的协作,确保设备无需重启系统就能正确识别并可立即使用。 -------------------------------------------------------------------------------- /Linuxdoc/Drafts/研究linux中的硬件中断.md: -------------------------------------------------------------------------------- 1 | 在Linux RISC-V架构中,硬件中断处理的相关代码分布在几个关键位置。以下是查找和理解这些代码的指南: 2 | 3 | ## 1. 中断初始化和入口点 4 | 5 | 主要文件: 6 | - `arch/riscv/kernel/irq.c` - 中断控制器初始化和通用中断处理 7 | - `arch/riscv/kernel/entry.S` - 异常和中断的底层入口点 8 | 9 | ## 2. 中断向量和处理函数 10 | 11 | - `arch/riscv/kernel/trap.c` - 包含`do_IRQ()`函数和异常处理程序 12 | - `arch/riscv/kernel/traps.c` - 异常处理初始化和通用异常处理函数 13 | 14 | ## 3. 平台特定中断控制器驱动 15 | 16 | - `drivers/irqchip/` - 包含各种中断控制器的驱动程序 17 | - `drivers/irqchip/irq-riscv-*.c` - RISC-V特定的中断控制器实现 18 | - `drivers/irqchip/irq-sifive-plic.c` - SiFive PLIC(平台级中断控制器)的驱动 19 | 20 | ## 4. 内存映射I/O (MMIO) 相关代码 21 | 22 | - `arch/riscv/mm/init.c` - 内存和I/O映射初始化 23 | - `arch/riscv/include/asm/io.h` - 定义MMIO访问的基本函数 24 | - `drivers/base/iomap.c` - 通用I/O映射函数实现 25 | 26 | ## 5. 设备树解析(用于MMIO地址分配) 27 | 28 | - `drivers/of/address.c` - 从设备树中提取MMIO地址范围 29 | - `arch/riscv/kernel/setup.c` - RISC-V特定的设备树设置 30 | 31 | ## 如何追踪中断处理流程 32 | 33 | 1. 从启动流程开始: 34 | - `start_kernel()` → `init_IRQ()`(在`init/main.c`中) 35 | - RISC-V特定初始化:`riscv_init_irq()`(在`arch/riscv/kernel/irq.c`中) 36 | 37 | 2. 研究中断入口点: 38 | - 查看`arch/riscv/kernel/entry.S`中的`handle_exception`和`handle_irq_mode_[x]`函数 39 | 40 | 3. 跟踪中断处理函数: 41 | - 从`do_IRQ()`(在`arch/riscv/kernel/trap.c`中)开始 42 | - 了解通用中断处理框架(在`kernel/irq/handle.c`中的`generic_handle_irq()`) 43 | 44 | 4. 查看特定设备驱动: 45 | - 如键盘:`drivers/input/keyboard/` 46 | - 网络:`drivers/net/` 47 | - 存储:`drivers/block/`或`drivers/scsi/` 48 | 49 | ## 搜索技巧 50 | 51 | 在Linux源代码中查找中断相关代码时,可以使用以下关键词: 52 | - `handle_irq` 53 | - `do_IRQ` 54 | - `request_irq` 55 | - `irq_set_handler` 56 | - `readl`/`writel`(用于MMIO访问) 57 | - `ioremap`(用于设置MMIO映射) 58 | - `mcause`(RISC-V特定中断原因寄存器) 59 | - `CSR_`(控制状态寄存器宏) 60 | 61 | 通过grep命令搜索,例如: 62 | ```bash 63 | grep -r "do_IRQ" arch/riscv/ 64 | ``` 65 | 66 | 或使用GitHub/GitLab等代码查看工具的搜索功能来定位这些函数和它们的调用关系。 -------------------------------------------------------------------------------- /Linuxdoc/Drafts/键盘输入的按键过程.md: -------------------------------------------------------------------------------- 1 | 是的,当您敲击键盘上的一个键时,会触发一系列事件,从硬件中断一直到最终处理按键输入。这个过程大致如下: 2 | 3 | 1. **硬件中断生成**: 4 | - 当按下键盘上的键时,键盘控制器生成一个硬件中断 5 | - 这个中断通过IRQ(中断请求线)发送到CPU 6 | 7 | 2. **中断处理**: 8 | - CPU接收到中断后,暂停当前任务,跳转到相应的中断服务程序(ISR) 9 | - 键盘的中断服务程序被执行,这是在内核中预先注册的 10 | 11 | 3. **键盘扫描码处理**: 12 | - 中断服务程序从键盘控制器读取扫描码 13 | - 扫描码被翻译成键码(keycode),表示按下或释放了哪个键 14 | 15 | 4. **输入子系统处理**: 16 | - Linux输入子系统接收键码 17 | - 输入子系统通过事件队列传递这个事件 18 | - 事件被写入到输入设备的事件队列中(如/dev/input/event*) 19 | 20 | 5. **用户空间传递**: 21 | - 如果在图形环境中: 22 | - X服务器或Wayland合成器从输入设备读取事件 23 | - 事件被转换为更高级的事件(如键按下/释放) 24 | - 这些事件被发送到具有键盘焦点的应用程序 25 | 26 | - 如果在终端/控制台中: 27 | - 终端驱动程序读取输入事件 28 | - 按键被转换为字符 29 | - 字符被放入tty输入缓冲区 30 | - 读取该tty的进程可以获取这些字符 31 | 32 | 6. **应用程序处理**: 33 | - 应用程序通过其输入处理逻辑接收并处理这个按键 34 | - 可能触发界面更新、命令执行等 35 | 36 | 整个过程是一个从低层硬件信号到高层软件表示的转换过程,通过多层抽象使应用程序无需直接处理硬件细节就能接收用户输入。在现代Linux系统中,这个过程高度优化,用户感知不到这些步骤之间的延迟。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 华中科技大学 操作系统实验文档 2 | 3 | ## 一、前言 4 | 5 | 本文档旨在记录我在完成 [华中科技大学 操作系统课程设计(Gitee)](https://gitee.com/hustos/pke-doc/tree/master),基于 [课程源代码仓库(Github)](https://github.com/MrShawCode/riscv-pke) 学习时,所遇到的困难及其解决方案,特别是在开发基于RISC-V代理内核(Proxy Kernel)的操作系统过程中积累的具体经验。文档内容不仅包括对原文档的补充,还针对具体问题提供了详细的解决方案,旨在为后续同学的学习提供帮助和参考。 6 | 7 | 开源精神是本项目的核心,借助社区智慧和实践探索,希望能为开源事业贡献微薄之力。文档中的参考资料由ChatGPT/Deepseek自动基于网上现有的资源整合生成,经过作者审阅编写,其中可能存在不准确或不完善之处,欢迎读者提出宝贵意见,改进这个项目。 8 | 9 | 10 | 11 | 12 | ## 二、实验概述 13 | 14 | 15 | 本课程项目旨在基于 [RISC-V 工具链](https://github.com/riscv-collab/riscv-gnu-toolchain) 设计并实现一个代理内核(Proxy Kernel,PKE)操作系统(相当于虚拟机),旨在通过一系列实验任务,帮助学生深入理解操作系统原理及其与硬件的协同工作。通过实现一个简化的代理内核,学生将聚焦于操作系统核心功能的实现,如内存管理、进程调度和中断处理,避免过多的硬件相关问题,从而集中精力掌握系统的核心概念和技术。 16 | 17 | 本实验采用的代理内核架构,区别于传统的宏内核和微内核,重点在于通过简化的设计满足给定应用的需求,系统规模随着应用需求的变化而变化。这种方法不仅极大降低了实验的复杂性,也保留了操作系统设计的完整性,既能管理处理器和内存,又能通过与主机的通信来完成硬件功能,从而为后续的软硬协同设计打下基础。 18 | 19 | 在完成操作系统部分的实验后,系统能力培养部分将引导学生在 FPGA 开发板上部署 RISC-V 软核,并扩展代理内核以实现设备管理和文件访问等更复杂功能。整个实验流程循序渐进,从基础实验到挑战实验,旨在帮助学生深入理解操作系统和计算机系统的整体架构,并为开发和验证现代计算机系统提供实践经验。 20 | 21 | [原实验概述](doc/实验概述.md) 22 | 23 | 24 | 25 | 26 | ## 三、环境搭建(Docker+vscode) 27 | ### 调试好上手即用的x64 ubuntu 24.04 LTS镜像 28 | ``` 29 | docker pull crpi-x7y7w4q8rsfacqq9.cn-shanghai.personal.cr.aliyuncs.com/cubelander-images/x86-pke:latest 30 | ``` 31 | [配置过程](lab/环境配置.md) 32 | 33 | ### 调试工具的安装和使用 34 | 35 | [调试工具安装配置过程](lab/调试工具.md) 36 | 37 | 38 | ## 四、实验内容 39 | 40 | ### lab1 41 | 42 | [lab1_1 系统调用](lab/lab1_1.md) 43 | 44 | [lab1_2 硬件中断](lab/lab1_2.md) 45 | 46 | [lab1_3 外部中断](lab/lab1_3.md) 47 | 48 | [lab1_challenge1 打印用户程序调用栈](lab/lab1_challenge1.md) 49 | 50 | 51 | [lab1_challenge2 打印异常代码行](lab/lab1_challenge2.md) 52 | 53 | [lab1_challenge3 多核启动和运行](lab/lab1_challenge3.md) 54 | 55 | ### lab2 56 | 57 | [lab2 内存管理基础知识](lab/lab2.md) 58 | 59 | [lab2_1 虚实地址转换](lab/lab2_1.md) 60 | 61 | [lab2_2 简单内存分配](lab/lab2_2.md) 62 | 63 | [lab2_3 缺页异常](lab/lab2_2.md) 64 | 65 | [lab2_challenge1 复杂缺页异常](lab/lab2_challenge1.md) 66 | 67 | [lab2_challenge2 堆空间管理](lab/lab2_challenge2.md) 68 | 69 | [lab2_challenge3 多核内存管理](lab/lab2_challenge3.md) 70 | 71 | 72 | ### lab3 73 | [lab3 进程调度基础知识](lab/lab3.md) 74 | 75 | [lab3_1 fork](lab/lab3_1.md) 76 | 77 | [lab3_2 yield](lab/lab3_2.md) 78 | 79 | [lab3_3 循环轮转调度](lab/lab3_3.md) 80 | 81 | [lab3_challenge1 进程等待和数据段复制](lab/lab3_challenge1.md) 82 | 83 | [lab3_challenge2 实现信号量](lab/lab3_challenge2.md) 84 | 85 | [lab3_challenge3 写时复制](lab/lab3_challenge3.md) 86 | 87 | ### lab4 88 | 89 | [lab4 文件系统基础知识](lab/lab4.md) 90 | 91 | [lab4_1 相对路径](lab/lab4_1.md) 92 | 93 | [lab4_2 目录文件](lab/lab4_2.md) 94 | 95 | ## 五、附录 96 | 97 | [riscv-software-src/riscv-isa-sim](https://github.com/riscv-software-src/riscv-isa-sim) 98 | 99 | [riscv-gnu-toolchain 百度网盘zip](https://pan.baidu.com/s/1Z9xKV_UY2Li_SxYrbJT5Zw?pwd=cpbf) -------------------------------------------------------------------------------- /code/file_system/rfs_add_direntry.md: -------------------------------------------------------------------------------- 1 | 以下是对 `rfs_add_direntry` 函数的详细中文注释及功能解析: 2 | 3 | ```c 4 | /** 5 | * 函数功能:向目录中添加新的目录项(类似Linux的link系统调用) 6 | * 参数: 7 | * dir - 目标目录的虚拟inode(必须为目录类型) 8 | * name - 新目录项名称(文件名或子目录名) 9 | * inum - 目标文件的inode编号 10 | * 返回值:0=成功,-1=失败 11 | * 核心流程: 12 | * 1. 类型校验与设备获取 13 | * 2. 定位目标数据块并读取到内存 14 | * 3. 写入新目录项到块内空闲位置 15 | * 4. 更新目录元数据并持久化 16 | * 设计特点: 17 | * - 仅支持单层目录结构,无哈希冲突处理 18 | * - 依赖调用方保证块空间充足(无动态块分配) 19 | */ 20 | int rfs_add_direntry(struct vinode *dir, const char *name, int inum) { 21 | /* ===== 阶段1:前置校验 ===== */ 22 | // 校验目标inode必须为目录类型(确保操作合法性) 23 | if (dir->type != DIR_I) { // DIR_I应为目录类型宏(如R_DIR) 24 | sprint("错误:目标不是目录!\n"); 25 | return -1; 26 | } 27 | 28 | /* ===== 阶段2:设备与块操作 ===== */ 29 | // 通过超级块获取关联的块设备(用于后续IO) 30 | struct rfs_device *rdev = rfs_device_list[dir->sb->s_dev->dev_id]; 31 | 32 | // 计算当前目录最后一块的物理块号(潜在问题:若块已满则需扩展) 33 | int block_index = dir->size / RFS_BLKSIZE; // 当前块索引 34 | int n_block = dir->addrs[block_index]; // 物理块号 35 | 36 | // 读取目标块到设备缓冲区(iobuffer) 37 | if (rfs_r1block(rdev, n_block) != 0) { 38 | sprint("错误:读取块%d失败!\n", n_block); 39 | return -1; 40 | } 41 | 42 | /* ===== 阶段3:目录项写入 ===== */ 43 | // 计算块内偏移(假设当前块未满) 44 | char *addr = (char *)rdev->iobuffer + (dir->size % RFS_BLKSIZE); 45 | struct rfs_direntry *p_direntry = (struct rfs_direntry *)addr; 46 | 47 | // 填充目录项元数据(未做名称重复校验) 48 | p_direntry->inum = inum; // 设置目标inode编号 49 | strncpy(p_direntry->name, name, MAX_NAME_LEN); // 复制名称(需确保不溢出) 50 | 51 | // 将修改后的块写回磁盘(原子性风险:无日志或备份) 52 | if (rfs_w1block(rdev, n_block) != 0) { 53 | sprint("错误:写入块%d失败!\n", n_block); 54 | return -1; 55 | } 56 | 57 | /* ===== 阶段4:元数据更新 ===== */ 58 | // 扩展目录文件大小(增加一个目录项的空间) 59 | dir->size += sizeof(struct rfs_direntry); 60 | 61 | // 持久化目录inode(更新size等字段到磁盘) 62 | if (rfs_write_back_vinode(dir) != 0) { 63 | sprint("错误:目录inode回写失败!\n"); 64 | return -1; 65 | } 66 | 67 | return 0; // 成功返回 68 | } 69 | ``` 70 | 71 | --- 72 | 73 | ### 关键流程解析 74 | 75 | 1. **块定位策略** 76 | - 通过 `dir->size` 确定当前应写入的块索引(`block_index`),假设目录文件按线性增长方式使用块空间。 77 | - **潜在问题**:未处理块已满的情况(当 `dir->size % RFS_BLKSIZE == 0` 时,当前块无空闲位置,需分配新块)。 78 | 79 | 2. **内存到磁盘的同步** 80 | - **写入顺序**:先修改内存中的目录项,再写回整个块。若在写回前系统崩溃,会导致数据不一致。 81 | - **优化建议**:引入日志机制,先记录操作到日志区,提交成功后再更新实际数据块。 82 | 83 | 3. **目录项管理** 84 | - **线性追加**:新目录项总是追加到目录文件末尾,可能导致目录查找效率低下(O(n)时间复杂度)。 85 | - **改进方向**:采用哈希表或B树结构组织目录项,实现O(1)或O(log n)查找。 86 | 87 | --- 88 | 89 | ### 与Linux ext4实现的差异对比 90 | 91 | | 特性 | rfs_add_direntry | ext4_dir_add_entry | 92 | |--------------------|---------------------------------|--------------------------------| 93 | | **块分配策略** | 假设块未满,无动态扩展 | 按需分配新块,支持目录扩展 | 94 | | **目录结构** | 线性列表 | 哈希索引或树形结构 | 95 | | **原子性保证** | 无 | 通过日志(journal)保证 | 96 | | **并发控制** | 无锁机制 | 使用inode锁保护目录修改 | 97 | | **重复项检测** | 未实现 | 检查哈希冲突,防止同名项重复 | 98 | 99 | --- 100 | 101 | ### 典型调用场景示例 102 | ```c 103 | // 创建新文件并添加到目录 104 | struct vinode *file_vinode = rfs_create(parent_dir, "newfile.txt"); 105 | rfs_add_direntry(parent_dir, "newfile.txt", file_vinode->inum); 106 | 107 | // 调用链: 108 | vfs_link() -> rfs_add_direntry() 109 | ``` 110 | 111 | --- 112 | 113 | ### 改进建议 114 | 115 | 1. **动态块分配** 116 | ```c 117 | if (dir->size % RFS_BLKSIZE == 0) { // 当前块已满 118 | int new_block = rfs_alloc_block(dir->sb); // 分配新块 119 | dir->addrs[block_index + 1] = new_block; 120 | } 121 | ``` 122 | 123 | 2. **名称重复校验** 124 | ```c 125 | if (rfs_lookup(dir, name) != NULL) { 126 | sprint("错误:同名文件已存在!\n"); 127 | return -1; 128 | } 129 | ``` 130 | 131 | 3. **日志支持** 132 | ```c 133 | journal_log_entry(dir, JOURNAL_ADD_ENTRY, name, inum); // 记录操作到日志 134 | if (journal_commit() != 0) { ... } 135 | ``` 136 | 137 | 该实现体现了轻量级文件系统目录管理的基本设计,适用于嵌入式等资源受限场景,但在可靠性和性能方面仍有较大提升空间。 -------------------------------------------------------------------------------- /code/file_system/rfs_disk_stat.md: -------------------------------------------------------------------------------- 1 | 以下是对`rfs_disk_stat`函数的详细分析,结合文件系统元数据读取原理与Linux内核设计规范: 2 | 3 | ```c 4 | /** 5 | * 函数功能:从磁盘读取inode元数据并填充统计信息结构体 6 | * 参数: 7 | * vinode - 虚拟inode指针(关联文件系统超级块和设备信息) 8 | * istat - 输出参数,用于存储inode统计信息 9 | * 返回值:0=成功,-1=失败 10 | * 核心流程: 11 | * 1. 通过虚拟inode定位物理设备 12 | * 2. 读取磁盘inode到内存 13 | * 3. 转换磁盘数据结构到用户态统计信息 14 | * 4. 释放临时内存资源 15 | */ 16 | int rfs_disk_stat(struct vinode *vinode, struct istat *istat) { 17 | /* ===== 阶段1:设备定位 ===== */ 18 | struct rfs_device *rdev = rfs_device_list[vinode->sb->s_dev->dev_id]; // 通过虚拟inode的超级块获取块设备对象(类似网页2的VFS设计) 19 | 20 | /* ===== 阶段2:磁盘inode读取 ===== */ 21 | struct rfs_dinode *dinode = rfs_read_dinode(rdev, vinode->inum); // 根据虚拟inode编号读取物理inode 22 | if (dinode == NULL) { // 错误处理(如网页3中IO错误监控场景) 23 | sprint("rfs_disk_stat: 磁盘inode读取失败!\n"); 24 | return -1; // 返回错误码,但未设置详细errno(可改进点) 25 | } 26 | 27 | /* ===== 阶段3:元数据转换 ===== */ 28 | istat->st_inum = vinode->inum; // 设置inode编号(参考网页2的inode唯一性设计) 29 | istat->st_size = dinode->size; // 文件大小(字节单位) 30 | istat->st_type = dinode->type; // 文件类型(R_FILE/R_DIR等,如网页2文件类型分类) 31 | istat->st_nlinks = dinode->nlinks; // 硬链接计数(类似网页2的硬链接实现) 32 | istat->st_blocks = dinode->blocks; // 占用块数(与网页3的IO性能监控指标相关) 33 | 34 | /* ===== 阶段4:资源清理 ===== */ 35 | free_page(dinode); // 释放临时分配的磁盘inode内存(防止内存泄漏) 36 | return 0; // 成功返回 37 | } 38 | ``` 39 | 40 | --- 41 | 42 | ### 关键设计特点分析 43 | 44 | | 特性 | 实现方式 | 设计依据 | 45 | |---------------------|-------------------------------------------------------------------------|-------------------------| 46 | | **设备抽象层** | 通过`vinode->sb`获取超级块,再定位块设备对象 | 类似网页2的VFS分层架构 | 47 | | **内存安全** | 使用`free_page`显式释放临时内存 | 防止内核内存泄漏 | 48 | | **元数据映射** | 将磁盘inode的size/type等字段直接映射到用户态结构体 | 符合POSIX stat系统调用规范| 49 | | **错误边界处理** | 对`rfs_read_dinode`返回值进行空指针校验 | 类似网页3的IO错误检测机制| 50 | 51 | --- 52 | 53 | ### 与标准接口对比 54 | 55 | | 特性 | rfs_disk_stat实现 | Linux stat系统调用 | 56 | |--------------------|---------------------------------------|----------------------------| 57 | | **元数据完整性** | 仅包含基础字段(size/type等) | 包含更全的权限、时间戳等信息 | 58 | | **错误返回方式** | 简单返回-1,无错误码细分 | 通过errno传递详细错误类型 | 59 | | **内存管理** | 调用方需确保istat内存有效性 | 由内核管理用户空间内存 | 60 | | **性能影响** | 直接磁盘访问,无缓存机制(参考网页3) | 利用页缓存加速元数据读取 | 61 | 62 | --- 63 | 64 | ### 潜在改进方向 65 | 66 | 1. **扩展元数据字段** 67 | ```c 68 | // 可补充网页2提到的inode权限信息 69 | istat->st_uid = dinode->i_uid; // 所有者ID 70 | istat->st_gid = dinode->i_gid; // 所属组ID 71 | istat->st_mode = dinode->i_mode;// 权限模式 72 | ``` 73 | 需确认`rfs_dinode`是否包含这些字段(当前代码未实现) 74 | 75 | 2. **缓存优化策略** 76 | 参考网页3的缓存IO机制,增加inode缓存层: 77 | ```c 78 | static struct rfs_dinode *dinode_cache[MAX_CACHE]; // 缓存最近访问的磁盘inode 79 | ``` 80 | 减少频繁磁盘读取(类似网页3的缓存I/O性能优化) 81 | 82 | 3. **错误处理增强** 83 | ```c 84 | enum { 85 | RFS_ESTAT_IO = 1, // 磁盘读取失败 86 | RFS_ESTAT_BADINODE // 损坏的inode结构 87 | }; 88 | // 设置详细错误码供上层判断 89 | ``` 90 | 91 | 4. **原子性保证** 92 | 添加自旋锁保护`rfs_read_dinode`操作: 93 | ```c 94 | spin_lock(&rdev->inode_lock); 95 | dinode = rfs_read_dinode(...); 96 | spin_unlock(&rdev->inode_lock); 97 | ``` 98 | 防止并发读取导致的数据竞争(参考网页2的VFS并发控制) 99 | 100 | --- 101 | 102 | 该实现体现了轻量级文件系统元数据查询的基本需求,在嵌入式场景下平衡了功能完整性与资源开销,但仍有提升空间以满足更复杂的系统要求。 -------------------------------------------------------------------------------- /code/file_system/rfs_format_dev.md: -------------------------------------------------------------------------------- 1 | 以下是对代码的详细中文注释及函数功能的解释: 2 | 3 | ```c 4 | // 函数功能:格式化一个RAM磁盘设备,初始化RFS文件系统结构(包括超级块、inode区域、根目录) 5 | // 参数 dev: 指向设备对象的指针,必须是RAM磁盘设备 6 | // 返回值: 成功返回0,失败返回-1 7 | int rfs_format_dev(struct device *dev) { 8 | // 通过设备ID从全局设备列表获取对应的RFS设备结构 9 | struct rfs_device *rdev = rfs_device_list[dev->dev_id]; 10 | 11 | /* ================== 第一阶段:格式化超级块 ================== */ 12 | // 使用设备IO缓冲区作为超级块临时存储空间 13 | struct super_block *super = (struct super_block *)rdev->iobuffer; 14 | 15 | // 设置魔数作为文件系统标识(例如0x12345678) 16 | super->magic = RFS_MAGIC; 17 | 18 | // 计算文件系统总块数: 19 | // 1(超级块) + 10(inode块) + 1(根目录数据块) + 10*直接块数(数据块) 20 | super->size = 1 + RFS_MAX_INODE_BLKNUM + 1 + RFS_MAX_INODE_BLKNUM * RFS_DIRECT_BLKNUM; 21 | 22 | // 设置文件系统支持的最大数据块数量(直接影响存储容量) 23 | super->nblocks = RFS_MAX_INODE_BLKNUM * RFS_DIRECT_BLKNUM; 24 | 25 | // 计算总inode数量: 26 | // 每块包含的inode数(块大小/单个inode大小) × inode块总数 27 | super->ninodes = (RFS_BLKSIZE / RFS_INODESIZE) * RFS_MAX_INODE_BLKNUM; 28 | 29 | // 将超级块写入设备的超级块专用位置(例如块0) 30 | if (rfs_w1block(rdev, RFS_BLK_OFFSET_SUPER) != 0) 31 | panic("RFS: 写入超级块失败!\n"); 32 | 33 | /* ================== 第二阶段:初始化inode区域 ================== */ 34 | // 将IO缓冲区重新解释为inode数组(每个块包含多个inode) 35 | struct rfs_dinode *p_dinode = (struct rfs_dinode *)rdev->iobuffer; 36 | 37 | // 初始化每个inode块的所有inode为未使用状态 38 | for (int i = 0; i < RFS_BLKSIZE / RFS_INODESIZE; ++i) { 39 | p_dinode->size = 0; // 文件大小为0 40 | p_dinode->type = R_FREE; // 标记为未分配 41 | p_dinode->nlinks = 0; // 硬链接数为0 42 | p_dinode->blocks = 0; // 占用块数为0 43 | p_dinode = (struct rfs_dinode *)((char *)p_dinode + RFS_INODESIZE); // 移动到下一个inode 44 | } 45 | 46 | // 将空白的inode块写入设备(从块1开始,共写入RFS_MAX_INODE_BLKNUM块) 47 | for (int inode_block = 0; inode_block < RFS_MAX_INODE_BLKNUM; ++inode_block) { 48 | if (rfs_w1block(rdev, RFS_BLK_OFFSET_INODE + inode_block) != 0) 49 | panic("RFS: 初始化空inode失败!\n"); 50 | } 51 | 52 | /* ================== 第三阶段:创建根目录 ================== */ 53 | // 初始化根目录inode(inode编号0) 54 | struct rfs_dinode root_dinode; 55 | root_dinode.size = 0; // 初始目录大小为0 56 | root_dinode.type = R_DIR; // 类型为目录 57 | root_dinode.nlinks = 1; // 链接数(目录本身和父目录的".") 58 | root_dinode.blocks = 1; // 占用1个数据块 59 | root_dinode.addrs[0] = RFS_BLK_OFFSET_FREE; // 数据块位置指向空闲块起始位置 60 | 61 | // 将根目录inode写入设备指定位置 62 | if (rfs_write_dinode(rdev, &root_dinode, 0) != 0) { 63 | sprint("RFS: 写入根目录inode失败!\n"); 64 | return -1; 65 | } 66 | 67 | return 0; // 格式化成功 68 | } 69 | ``` 70 | 71 | ### 函数功能详解 72 | 该函数实现了对RAM磁盘设备的**文件系统格式化**操作,主要完成以下三个核心任务: 73 | 74 | 1. **超级块初始化** 75 | - 写入魔数(Magic Number)作为文件系统标识 76 | - 计算并设置文件系统总容量(块数) 77 | - 定义最大支持的文件数量和存储容量 78 | - 将超级块写入磁盘起始位置(通常是块0) 79 | 80 | 2. **inode区域初始化** 81 | - 将所有inode标记为未使用状态(R_FREE) 82 | - 清空inode的元数据(大小、链接数等) 83 | - 将初始化后的inode块批量写入磁盘 84 | 85 | 3. **根目录创建** 86 | - 保留inode 0作为根目录 87 | - 设置目录类型(R_DIR)、初始链接数 88 | - 分配第一个数据块用于存储目录条目 89 | - 将根目录inode写入指定位置 90 | 91 | ### 关键设计特点 92 | 1. **内存优化**:复用设备的IO缓冲区(iobuffer)作为临时存储空间,减少内存占用 93 | 2. **结构紧凑**: 94 | - 超级块固定大小(1块) 95 | - inode区域连续存储(RFS_MAX_INODE_BLKNUM块) 96 | - 数据块从固定偏移开始(RFS_BLK_OFFSET_FREE) 97 | 3. **快速初始化**:通过批量写入空inode块提升格式化速度 98 | 4. **扩展性设计**:通过常量定义(如RFS_MAX_INODE_BLKNUM)支持灵活调整文件系统容量 99 | 100 | ### 典型使用场景 101 | 该函数通常在以下情况被调用: 102 | 1. 操作系统首次挂载RAM磁盘时 103 | 2. 需要彻底清空RAM磁盘数据时 104 | 3. 文件系统损坏后的恢复操作 105 | 106 | 注:该实现假设RAM磁盘设备已正确初始化,且rfs_device_list已包含有效的设备信息。实际使用前需确保设备支持块级读写操作。 -------------------------------------------------------------------------------- /code/file_system/rfs_hook_opendir.md: -------------------------------------------------------------------------------- 1 | 以下是对 `rfs_hook_opendir` 函数的详细中文注释和功能分析: 2 | 3 | ```c 4 | /** 5 | * 函数功能:打开目录时将目录内容预读到内存缓存(目录项缓存初始化) 6 | * 参数: 7 | * dir_vinode - 目录文件的虚拟inode(需为目录类型) 8 | * dentry - 目录对应的目录项(当前未使用,保留接口扩展性) 9 | * 返回值:0=成功,触发panic=失败 10 | * 核心流程: 11 | * 1. 逆向遍历目录数据块(从末块到首块) 12 | * 2. 分配连续内存空间并拷贝块数据 13 | * 3. 构建目录缓存结构并挂载到inode 14 | * 设计特点: 15 | * - 内存块逆向加载实现物理连续存储 16 | * - 目录缓存提升后续读操作性能 17 | */ 18 | int rfs_hook_opendir(struct vinode *dir_vinode, struct dentry *dentry) { 19 | // 内存块指针:当前块地址和前一块地址(用于连续性校验) 20 | void *pdire = NULL; 21 | void *previous = NULL; 22 | // 获取关联的块设备对象(通过超级块中的设备ID索引全局设备列表) 23 | struct rfs_device *rdev = rfs_device_list[dir_vinode->sb->s_dev->dev_id]; 24 | 25 | /* ===== 阶段1:逆向加载目录块到连续内存 ===== */ 26 | // 从最后一个块向第一个块遍历(i初始化为blocks-1递减至0) 27 | for (int i = dir_vinode->blocks - 1; i >= 0; i--) { 28 | previous = pdire; // 记录前一块地址 29 | pdire = alloc_page(); // 分配新内存页(假设页大小=RFS_BLKSIZE) 30 | 31 | /* 内存连续性校验(确保多块目录数据连续存储) 32 | * 原理:若当前块与前一块地址差不为块大小,说明内存不连续 33 | * 示例:块大小4KB时,前一块地址应为当前地址+4096(低地址向高地址增长) 34 | * 注意:此处条件判断逻辑可能存在问题(应为 pdire - previous != RFS_BLKSIZE) 35 | */ 36 | if (previous != NULL && previous - pdire != RFS_BLKSIZE) 37 | panic("rfs_hook_opendir: 目录块内存不连续!"); 38 | 39 | // 从设备读取目录块到iobuffer(硬件抽象层操作) 40 | rfs_r1block(rdev, dir_vinode->addrs[i]); 41 | // 将块数据拷贝到预分配内存(构建线性地址空间) 42 | memcpy(pdire, rdev->iobuffer, RFS_BLKSIZE); 43 | } 44 | 45 | /* ===== 阶段2:构建目录缓存结构 ===== */ 46 | // 分配目录缓存控制结构(记录元信息) 47 | struct rfs_dir_cache *dir_cache = (struct rfs_dir_cache *)alloc_page(); 48 | dir_cache->block_count = dir_vinode->blocks; // 记录总块数 49 | /* 设置目录项基地址(注意此处pdire指向首个逻辑块) 50 | * 因逆向加载,内存中块顺序与物理存储相反: 51 | * 物理块顺序:块0(首块)→块1→...→块N(末块) 52 | * 内存布局:pdire指向块N,块N-1在pdire+RFS_BLKSIZE,依此类推 53 | */ 54 | dir_cache->dir_base_addr = (struct rfs_direntry *)pdire; 55 | 56 | // 将缓存结构挂载到虚拟inode(供后续readdir等操作使用) 57 | dir_vinode->i_fs_info = dir_cache; 58 | 59 | return 0; // 成功返回 60 | } 61 | ``` 62 | 63 | --- 64 | 65 | ### 功能分析 66 | 67 | #### 1. **逆向加载策略** 68 | - **物理块顺序**:假设目录文件由多个块组成(`dir_vinode->blocks`),块地址按`addrs[0]`到`addrs[N-1]`顺序存储 69 | - **内存布局**:通过从末块(`blocks-1`)向首块(0)逆序加载,使得: 70 | ```text 71 | 内存地址增长方向 → 72 | [块N-1 数据][块N-2 数据]...[块0 数据] 73 | ``` 74 | 这种布局使得遍历目录项时可按内存正向顺序访问(与物理存储顺序相反) 75 | 76 | #### 2. **内存连续性强制** 77 | - **设计目标**:确保所有目录块在内存中连续存储,便于后续线性遍历 78 | - **实现方式**:通过`previous - pdire != RFS_BLKSIZE`校验(实际应校验`pdire - previous`,需修正) 79 | - **影响**:若分配不连续则触发panic,保证目录项数组的完整性 80 | 81 | #### 3. **目录缓存结构** 82 | - **元数据记录**: 83 | ```c 84 | struct rfs_dir_cache { 85 | int block_count; // 总块数 86 | struct rfs_direntry *dir_base_addr; // 目录项数组首地址 87 | }; 88 | ``` 89 | - **挂载点**:通过`dir_vinode->i_fs_info`关联,实现: 90 | - 快速访问:后续`readdir`直接操作内存数据,无需磁盘I/O 91 | - 生命周期管理:缓存与inode绑定,目录关闭时需释放 92 | 93 | --- 94 | 95 | ### 关键问题与改进建议 96 | 97 | #### 1. **内存连续性校验逻辑错误** 98 | - **当前代码**:`previous - pdire != RFS_BLKSIZE` 99 | - **正确逻辑**:`pdire - previous != RFS_BLKSIZE`(地址递增方向) 100 | - **影响**:可能导致错误触发panic,需修正判断条件 101 | 102 | #### 2. **目录项顺序颠倒** 103 | - **现象**:内存中块顺序与物理存储相反,导致`ls`等操作显示逆序 104 | - **解决方案**: 105 | ```c 106 | // 改为正向加载(i从0到blocks-1) 107 | for (int i = 0; i < dir_vinode->blocks; i++) { 108 | // 分配地址需保证连续性 109 | } 110 | ``` 111 | 112 | #### 3. **错误处理缺失** 113 | - **风险点**:`alloc_page()`可能返回NULL(内存耗尽) 114 | - **改进建议**: 115 | ```c 116 | pdire = alloc_page(); 117 | if (!pdire) { 118 | // 释放已分配内存 119 | while (++i < dir_vinode->blocks) free_page(...); 120 | return -ENOMEM; 121 | } 122 | ``` 123 | 124 | #### 4. **缓存释放机制** 125 | - **现状**:未提供`closedir`钩子释放缓存 126 | - **建议补充**: 127 | ```c 128 | int rfs_hook_closedir(struct vinode *dir_vinode) { 129 | struct rfs_dir_cache *cache = dir_vinode->i_fs_info; 130 | // 逆序释放内存页(与分配顺序匹配) 131 | for (int i=0; iblock_count; i++) { 132 | free_page((char*)cache->dir_base_addr + i*RFS_BLKSIZE); 133 | } 134 | free_page(cache); 135 | return 0; 136 | } 137 | ``` 138 | 139 | --- 140 | 141 | ### 典型调用流程 142 | ```text 143 | 用户调用 opendir("/mydir") 144 | -> VFS层调用 rfs_hook_opendir() 145 | -> 遍历目录块(假设3个块:块0,块1,块2) 146 | -> 逆序加载:块2 → 块1 → 块0 到连续内存区域 147 | -> 构建缓存结构 dir_cache{block_count=3, dir_base_addr=块2内存地址} 148 | -> 返回目录流指针 149 | 150 | 后续 readdir() 操作: 151 | -> 从 dir_base_addr 开始遍历内存中的目录项数组 152 | -> 自动跨块访问(因内存连续) 153 | ``` 154 | 155 | 该实现通过预加载目录数据到连续内存,显著提升了目录遍历操作的性能,但需注意内存管理和顺序一致性等问题。 -------------------------------------------------------------------------------- /code/file_system/rfs_lookup.md: -------------------------------------------------------------------------------- 1 | 以下是对`rfs_lookup`函数的详细解析,结合文件系统目录查找原理与LOOKUP函数设计思想: 2 | 3 | ```c 4 | /** 5 | * 函数功能:在目录中查找指定子目录项(类似POSIX的namei操作) 6 | * 参数: 7 | * parent - 父目录的虚拟inode(需为目录类型) 8 | * sub_dentry - 目标目录项(含待查找文件名) 9 | * 返回值: 10 | * 成功:子文件/目录的虚拟inode指针 11 | * 失败:NULL 12 | * 核心流程: 13 | * 1. 计算目录项布局参数 14 | * 2. 分块遍历目录项数据 15 | * 3. 名称匹配与资源分配 16 | * 设计特点: 17 | * - 线性遍历匹配(类似网页4的VLOOKUP遍历法) 18 | * - 块级缓冲读取优化IO效率(类似网页3的缓存IO思想) 19 | */ 20 | struct vinode *rfs_lookup(struct vinode *parent, struct dentry *sub_dentry) { 21 | /* ===== 阶段1:目录结构参数计算 ===== */ 22 | int total_direntrys = parent->size / sizeof(struct rfs_direntry); // 总目录项数(类似网页5的数组长度计算) 23 | int one_block_direntrys = RFS_BLKSIZE / sizeof(struct rfs_direntry); // 单块容纳目录项数 24 | struct rfs_device *rdev = rfs_device_list[parent->sb->s_dev->dev_id]; // 获取块设备 25 | 26 | /* ===== 阶段2:目录项遍历 ===== */ 27 | struct rfs_direntry *p_direntry = NULL; 28 | struct vinode *child_vinode = NULL; 29 | for (int i = 0; i < total_direntrys; ++i) { 30 | /* 块边界处理(类似网页3的分块读取优化) */ 31 | if (i % one_block_direntrys == 0) { 32 | int block_idx = i / one_block_direntrys; 33 | rfs_r1block(rdev, parent->addrs[block_idx]); // 读取物理块到iobuffer 34 | p_direntry = (struct rfs_direntry *)rdev->iobuffer; // 内存映射 35 | } 36 | 37 | /* 名称匹配(类似网页4的精确查找逻辑) */ 38 | if (strcmp(p_direntry->name, sub_dentry->name) == 0) { 39 | /* ===== 阶段3:资源分配与初始化 ===== */ 40 | child_vinode = rfs_alloc_vinode(parent->sb); // 创建虚拟inode(类似网页2的VFS结构) 41 | child_vinode->inum = p_direntry->inum; // 绑定磁盘inode编号 42 | 43 | /* 元数据同步(类似网页5的错误处理机制) */ 44 | if (rfs_update_vinode(child_vinode) != 0) 45 | panic("rfs_lookup: 磁盘inode读取失败!"); 46 | break; 47 | } 48 | ++p_direntry; // 移动至下一个目录项(内存地址递增) 49 | } 50 | return child_vinode; // 未找到时返回NULL(类似网页4的查找失败处理) 51 | } 52 | ``` 53 | 54 | --- 55 | 56 | ### 关键设计解析 57 | 58 | 1. **目录项存储结构** 59 | ```c 60 | parent->addrs[block_idx] // 目录文件数据块地址数组 61 | ``` 62 | 目录文件由多个数据块组成,每个块存储若干`rfs_direntry`结构,符合网页2描述的目录存储方式 63 | 64 | 2. **遍历算法选择** 65 | 采用线性遍历而非LOOKUP的二分法,原因: 66 | - 目录项通常无序排列 67 | - 小规模目录下遍历效率可接受 68 | - 避免维护排序带来的写操作开销 69 | 70 | 3. **IO优化策略** 71 | ```c 72 | rfs_r1block(rdev, parent->addrs[block_idx]); // 按需读取块数据 73 | ``` 74 | 每次读取整块数据到缓冲区,减少设备访问次数(类似网页3的块缓冲思想) 75 | 76 | 4. **内存管理** 77 | ```c 78 | p_direntry = (struct rfs_direntry *)rdev->iobuffer; 79 | ``` 80 | 直接复用设备IO缓冲区,避免额外内存分配(适合嵌入式场景) 81 | 82 | --- 83 | 84 | ### 与标准实现对比 85 | 86 | | 特性 | rfs_lookup实现 | Linux VFS实现 | 87 | |--------------------|-----------------------------|---------------------------| 88 | | 查找算法 | 线性遍历 | 哈希表+链表 | 89 | | 元数据缓存 | 无,每次读取磁盘inode | 通过dcache缓存目录项 | 90 | | 并发控制 | 无锁机制 | 使用RCU锁保护目录遍历 | 91 | | 错误处理 | 直接panic | 返回错误码并设置errno | 92 | 93 | --- 94 | 95 | ### 扩展建议 96 | 97 | 1. **引入哈希索引** 98 | ```c 99 | // 预计算文件名哈希值存储于direntry 100 | if (p_direntry->hash == target_hash) // 快速筛选 101 | ``` 102 | 参考网页4的多条件查询优化,减少字符串比较次数 103 | 104 | 2. **目录项缓存** 105 | ```c 106 | static struct rfs_direntry *direntry_cache[MAX_CACHE]; 107 | ``` 108 | 类似网页3的缓存机制,缓存高频访问目录块 109 | 110 | 3. **二分法改造** 111 | ```c 112 | qsort(dir_entries, total, sizeof(entry), compare); // 按名称排序 113 | // 使用二分查找算法 114 | ``` 115 | 当目录项有序时可借鉴网页4的LOOKUP二分法提升效率 116 | 117 | 4. **原子操作支持** 118 | ```c 119 | spin_lock(&parent->dir_lock); // 遍历期间加锁 120 | ``` 121 | 参考网页2的并发控制设计,防止遍历时目录被修改 122 | 123 | --- 124 | 125 | 该实现体现了嵌入式文件系统目录查找的基础设计,在资源受限环境下平衡了功能与性能,但仍有优化空间以适应更大规模或高并发场景。 -------------------------------------------------------------------------------- /code/file_system/rfs_lseek.md: -------------------------------------------------------------------------------- 1 | 以下是对`rfs_lseek`函数的功能分析与中文注释,结合Linux文件系统设计原理和标准文件操作接口规范: 2 | 3 | ```c 4 | /** 5 | * 函数功能:调整文件读写指针位置(类似标准lseek系统调用) 6 | * 参数: 7 | * f_inode - 虚拟inode指针,包含文件元数据(含文件大小) 8 | * new_offset- 目标偏移量(绝对或相对值,根据whence决定) 9 | * whence - 定位模式(LSEEK_SET/LSEEK_CUR) 10 | * offset - 当前偏移量的指针(传入传出参数) 11 | * 返回值:0=成功,-1=失败 12 | * 核心流程: 13 | * 1. 根据whence类型计算新偏移量 14 | * 2. 执行边界检查(防止越界访问) 15 | * 3. 更新当前偏移量指针 16 | * 设计特点: 17 | * - 仅支持SET/CUR两种模式,未实现SEEK_END(参考网页5标准实现) 18 | * - 偏移量强制限制在[0,文件大小]区间(防止溢出,如网页7所述) 19 | */ 20 | int rfs_lseek(struct vinode *f_inode, ssize_t new_offset, int whence, int *offset) { 21 | int file_size = f_inode->size; // 获取文件当前大小(类似stat.st_size) 22 | 23 | /* ===== 模式分派与边界校验 ===== */ 24 | switch (whence) { 25 | // 绝对定位模式(类似SEEK_SET) 26 | case LSEEK_SET: 27 | /* 校验新偏移量有效性(非负且不超过文件大小) */ 28 | if (new_offset < 0 || new_offset > file_size) { // 参考网页6的偏移约束 29 | sprint("rfs_lseek: 无效偏移量!\n"); 30 | return -1; 31 | } 32 | *offset = new_offset; // 直接设置新位置(如网页4的SEEK_SET语义) 33 | break; 34 | 35 | // 相对定位模式(类似SEEK_CUR) 36 | case LSEEK_CUR: 37 | /* 计算相对偏移后的位置并校验 */ 38 | if (*offset + new_offset < 0 || *offset + new_offset > file_size) { 39 | sprint("rfs_lseek: 无效偏移量!\n"); 40 | return -1; 41 | } 42 | *offset += new_offset; // 增量更新(如网页5示例中的偏移叠加) 43 | break; 44 | 45 | default: // 不支持的模式(如SEEK_END未实现) 46 | sprint("rfs_lseek: 无效定位模式!\n"); 47 | return -1; 48 | } 49 | 50 | return 0; // 成功返回(区别于标准lseek返回新偏移量,网页7对比) 51 | } 52 | ``` 53 | 54 | --- 55 | 56 | ### 关键设计解析 57 | 58 | 1. **定位模式支持** 59 | - **LSEEK_SET**:绝对定位,直接设置`new_offset`(需满足`0 ≤ new_offset ≤ size`) 60 | - **LSEEK_CUR**:相对定位,基于当前偏移量增量调整(`new_offset`可为正负) 61 | - *未实现SEEK_END*:当前版本不支持从文件末尾定位,扩展时可参照标准`SEEK_END`实现 62 | 63 | 2. **边界保护机制** 64 | ```c 65 | // 示例:LSEEK_SET的校验逻辑 66 | new_offset > file_size // 禁止超过文件结尾(防止空洞文件访问,如网页6描述) 67 | new_offset < 0 // 禁止负值偏移(与POSIX规范一致) 68 | ``` 69 | 类似网页4中所述的文件指针有效性管理 70 | 71 | 3. **参数传递设计** 72 | - `offset`使用指针传递,实现跨调用状态保持(类似内核文件描述符维护偏移量) 73 | - `f_inode`包含文件元数据,符合虚拟文件系统抽象层设计(参考网页4的vnode结构) 74 | 75 | --- 76 | 77 | ### 与标准接口差异对比 78 | 79 | | 特性 | rfs_lseek实现 | POSIX lseek标准 | 80 | |-------------------|--------------------------------|-------------------------------| 81 | | 返回值 | 0/-1状态码 | 返回新偏移量(失败返回-1) | 82 | | 错误处理 | 简单sprint提示 | 设置errno详细错误码 | 83 | | SEEK_END支持 | 未实现 | 完整支持三种模式 | 84 | | 偏移量存储位置 | 通过外部指针传递 | 文件描述符内部维护 | 85 | | 空洞文件处理 | 禁止超过文件大小 | 允许创建空洞(网页6特性) | 86 | 87 | --- 88 | 89 | ### 扩展建议 90 | 91 | 1. **增加SEEK_END支持** 92 | ```c 93 | case LSEEK_END: 94 | if(file_size + new_offset < 0 || file_size + new_offset > MAX_FILE_SIZE){ 95 | return -1; 96 | } 97 | *offset = file_size + new_offset; 98 | ``` 99 | 参考网页7的从文件末尾定位实现 100 | 101 | 2. **空洞文件支持** 102 | ```c 103 | // 修改边界检查条件允许超过当前文件大小 104 | if (new_offset > MAX_FILE_SIZE) // 添加系统最大文件限制 105 | ``` 106 | 如网页6所述,允许创建含未初始化区域的文件 107 | 108 | 3. **错误码细化** 109 | ```c 110 | enum { 111 | RFS_ELSEEK_RANGE = 1, // 偏移量越界 112 | RFS_ELSEEK_WHENCE, // 无效定位模式 113 | }; 114 | ``` 115 | 参照网页7的错误分类策略 116 | 117 | 该实现体现了嵌入式文件系统对标准POSIX接口的简化适配,在保证基本功能的前提下优化了资源占用,适合需要轻量级文件操作的场景。 -------------------------------------------------------------------------------- /code/file_system/rfs_mkdir.md: -------------------------------------------------------------------------------- 1 | 以下是对`rfs_mkdir`函数的详细功能解析,结合Linux文件系统实现原理和代码逻辑: 2 | 3 | --- 4 | ### 一、核心功能与流程 5 | #### 1. **磁盘inode分配与初始化**(对应代码段1-22行) 6 | - **空闲inode扫描**:遍历文件系统所有可能的inode块(`RFS_MAX_INODE_BLKNUM`),通过`rfs_read_dinode`读取每个inode,检查其类型是否为`R_FREE`。此逻辑类似Linux ext4的`ext4_new_inode`函数,通过位图查找空闲inode。 7 | - **inode状态初始化**:找到空闲inode后,设置: 8 | - `size=0`:新目录初始大小为0 9 | - `type=R_DIR`:标记为目录类型(类似Linux的`S_IFDIR`标志) 10 | - `nlinks=1`:初始链接计数(父目录通过`..`递增该值) 11 | - `blocks=1`:分配首个数据块(通过`rfs_alloc_block`实现,类似ext4的块分配器) 12 | 13 | #### 2. **父目录项添加**(对应代码段24-30行) 14 | - 调用`rfs_add_direntry`函数,向父目录的目录块中写入新条目: 15 | - **目录项结构**:包含子目录名称`sub_dentry->name`和分配的inode编号`free_inum`,类似Linux的`struct ext4_dir_entry_2` 16 | - **原子性风险**:未实现日志机制,若写盘过程中断可能导致目录不一致 17 | 18 | #### 3. **虚拟inode(vinode)管理**(对应代码段32-35行) 19 | - **vinode分配**:`rfs_alloc_vinode`创建内存inode对象,关联磁盘inode编号`free_inum` 20 | - **元数据同步**:`rfs_update_vinode`将内存状态同步到磁盘(类似Linux的`mark_inode_dirty`和`write_inode`) 21 | 22 | --- 23 | ### 二、关键数据结构关联 24 | | 代码对象 | Linux对应结构 | 功能描述 | 25 | |--------------------|--------------------|-------------------------------------------------------------------------| 26 | | `struct vinode` | `struct inode` | 内存中文件元数据(含磁盘inode编号、大小、块地址等) | 27 | | `struct rfs_dinode`| `struct ext4_inode`| 磁盘inode结构,持久化存储文件类型、块指针等关键信息 | 28 | | `struct dentry` | `struct dentry`| 目录项缓存,维护名称到inode的映射,支持路径解析 | 29 | 30 | --- 31 | ### 三、潜在问题与改进建议 32 | 1. **并发安全性** 33 | - 未对父目录inode加锁,多线程操作可能导致目录项覆盖(参考Linux的`inode->i_mutex`锁机制) 34 | - 改进方案:添加类似`spin_lock(&parent->i_lock)`的锁保护inode状态 35 | 36 | 2. **空间分配不足处理** 37 | - 当`rfs_alloc_block`返回失败时未回滚已分配的inode(需补充`rfs_free_inode`逻辑) 38 | - 参考ext4的`ext4_should_retry_alloc`重试机制 39 | 40 | 3. **日志缺失** 41 | - 直接写盘操作无崩溃一致性保护(可引入类似ext4的日志系统,先写入日志描述块) 42 | 43 | --- 44 | ### 四、与VFS层协作流程 45 | ```mermaid 46 | graph TD 47 | A[vfs_mkdir系统调用] --> B[路径解析获取父目录dentry] 48 | B --> C[调用rfs_mkdir创建物理目录] 49 | C --> D[分配磁盘inode和块] 50 | D --> E[更新父目录项] 51 | E --> F[创建vinode并加入icache] 52 | ``` 53 | 此流程实现了VFS接口与具体文件系统的解耦,通过`struct vinode`抽象不同文件系统的inode操作。 54 | 55 | --- 56 | ### 五、扩展应用场景 57 | - **嵌套目录创建**:需递归调用`rfs_mkdir`(当前实现仅支持单级) 58 | - **硬链接支持**:通过递增`nlinks`实现(需修改`rfs_add_direntry`校验重复项) 59 | - **ACL权限扩展**:在`struct rfs_dinode`中添加扩展属性字段(参考ext4的`i_inline_acl`) 60 | 61 | 该实现体现了类Unix文件系统目录创建的核心逻辑,但需进一步完善错误恢复和并发控制机制以满足生产环境要求。 -------------------------------------------------------------------------------- /code/file_system/rfs_read.md: -------------------------------------------------------------------------------- 1 | 以下是针对`rfs_read`函数的详细分析,结合文件系统读写原理与代码实现逻辑: 2 | 3 | ### 一、函数功能解析 4 | 该函数用于从RFS文件系统的虚拟inode中读取指定长度数据到用户缓冲区,核心功能包含: 5 | 1. **边界校验** 6 | - 检查偏移量是否越界(`f_inode->size < *offset`触发panic) 7 | - 动态调整读取长度(若`offset + len`超过文件大小时自动截断) 8 | 9 | 2. **块对齐计算** 10 | - `align`:当前偏移在块内的字节偏移(例如块大小4KB时,偏移5123的`align=387`) 11 | - `block_offset`:起始逻辑块号(如总块号=偏移量/块大小) 12 | - 计算完整块读取次数(`readtimes`)和剩余字节(`remain`) 13 | 14 | 3. **分阶段读取** 15 | ```c 16 | // 第一阶段:读取首部不完整块 17 | rfs_r1block(rdev, f_inode->addrs[block_offset]); 18 | memcpy(buffer + buf_offset, rdev->iobuffer + align, first_block_len); 19 | 20 | // 第二阶段:循环读取完整块 21 | while (readtimes != 0) { 22 | rfs_r1block(rdev, f_inode->addrs[block_offset]); 23 | memcpy(buffer + buf_offset, rdev->iobuffer, RFS_BLKSIZE); 24 | } 25 | 26 | // 第三阶段:读取尾部剩余字节 27 | if (remain > 0) { 28 | rfs_r1block(...); 29 | memcpy(..., remain); 30 | } 31 | ``` 32 | 该三段式读取策略优化了块设备访问效率,符合文件系统读写最佳实践。 33 | 34 | 4. **缓冲区管理** 35 | - 使用临时缓冲区`buffer`暂存数据,最后复制到用户空间`r_buf` 36 | - 末尾添加`\0`(暗示支持文本文件终止符) 37 | - 更新偏移量指针(`*offset += len`) 38 | 39 | --- 40 | 41 | ### 二、关键设计特点 42 | | 特性 | 实现方式 | 参考依据 | 43 | |--------------------|------------------------------------------------------------------------|-------------------------| 44 | | **块设备交互** | 通过`rfs_r1block`逐块读取设备数据,利用`iobuffer`中转 | 类似Linux块设备驱动模型 | 45 | | **内存效率优化** | 按需分块读取,避免一次性加载大文件 | 缓冲区管理原则 | 46 | | **偏移量动态更新** | 通过指针参数`offset`实现跨调用状态保持 | 文件读写位置标准实现 | 47 | | **安全边界检查** | 文件大小与偏移量的强制校验,防止越界访问 | 系统编程安全规范 | 48 | 49 | --- 50 | 51 | ### 三、潜在改进方向 52 | 1. **错误处理增强** 53 | - 当前仅对偏移越界做panic处理,未考虑设备读取失败(如`rfs_r1block`返回错误) 54 | - 建议增加返回值检查,例如: 55 | ```c 56 | if (rfs_r1block(...) < 0) return -EIO; 57 | ``` 58 | 59 | 2. **零拷贝优化** 60 | - 当前使用`buffer`中转导致两次内存复制(设备→buffer→用户空间) 61 | - 可设计直接写入`r_buf`的机制(需确保用户缓冲区对齐) 62 | 63 | 3. **异步IO支持** 64 | - 当前为同步阻塞读取,大文件可能影响系统响应 65 | - 可结合DMA或异步IO框架优化 66 | 67 | 4. **缓存机制集成** 68 | - 缺少块缓存层(类似Page Cache),频繁小读取会降低性能 69 | - 可添加LRU缓存减少设备访问次数 70 | 71 | --- 72 | 73 | ### 四、与标准文件接口对比 74 | | 特性 | `rfs_read`实现 | POSIX read规范 | 75 | |--------------------|----------------------------------------|----------------------------------| 76 | | **参数结构** | 显式传递inode、偏移量指针 | 通过文件描述符隐式维护偏移量 | 77 | | **缓冲区管理** | 用户提供`r_buf`,内部使用临时buffer | 直接写入用户空间,无中间拷贝 | 78 | | **错误返回** | 简单panic,无错误码体系 | 通过errno传递详细错误原因 | 79 | | **阻塞行为** | 同步读取,未体现非阻塞逻辑 | 支持O_NONBLOCK模式 | 80 | 81 | --- 82 | 83 | ### 五、典型调用流程示例 84 | ```c 85 | // 用户态调用示例 86 | struct vinode *file_inode = vfs_open("/data.txt", O_RDONLY); 87 | char buf[4096]; 88 | int offset = 0; 89 | ssize_t ret = rfs_read(file_inode, buf, 4096, &offset); 90 | 91 | // 内核执行流程: 92 | 1. 校验offset=0 < file_size(假设文件8KB) 93 | 2. len=4096未越界,分配buffer[4097] 94 | 3. 计算align=0,block_offset=0,readtimes=1(4096/4096) 95 | 4. 读取块0 → 复制4096字节到buffer 96 | 5. 更新offset=4096,返回len=4096 97 | ``` 98 | 99 | 该实现体现了嵌入式文件系统的典型设计取舍,在资源受限环境下平衡了功能完整性,但仍有优化空间以适应高性能场景需求。 -------------------------------------------------------------------------------- /code/file_system/rfs_write.md: -------------------------------------------------------------------------------- 1 | 以下是对`rfs_write`函数的详细中文注释和功能解析,结合文件系统设计与底层存储操作原理: 2 | 3 | ```c 4 | /** 5 | * 函数功能:向RFS文件系统的虚拟inode写入指定长度数据 6 | * 参数: 7 | * f_inode - 目标文件的虚拟inode指针 8 | * w_buf - 用户态数据缓冲区 9 | * len - 待写入数据长度 10 | * offset - 写入偏移量(传入传出参数) 11 | * 返回值:成功写入的字节数(始终等于len) 12 | * 核心流程: 13 | * 1. 偏移量校验与块对齐计算 14 | * 2. 分阶段写入(首部非对齐块/完整块/尾部剩余数据) 15 | * 3. 动态块分配与元数据更新 16 | */ 17 | ssize_t rfs_write(struct vinode *f_inode, const char *w_buf, ssize_t len, int *offset) { 18 | /* ===== 阶段1:写入前校验 ===== */ 19 | if (f_inode->size < *offset) { 20 | panic("rfs_write: 偏移量不能超过文件大小!"); // 类似网页4中fwrite的边界检查 21 | } 22 | 23 | /* ===== 阶段2:块对齐计算 ===== */ 24 | int align = *offset % RFS_BLKSIZE; // 当前偏移在块内的字节偏移(如4KB块,偏移5123则align=387) 25 | int writetimes = (len + align) / RFS_BLKSIZE; // 完整块写入次数(包含首部可能的部分块) 26 | int remain = (len + align) % RFS_BLKSIZE; // 尾部剩余字节数 27 | int block_offset = *offset / RFS_BLKSIZE; // 起始逻辑块号 28 | 29 | struct rfs_device *rdev = rfs_device_list[f_inode->sb->s_dev->dev_id]; // 获取块设备句柄 30 | 31 | /* ===== 阶段3:分块写入策略 ===== */ 32 | // 处理首部非对齐块(需读-改-写) 33 | if (align != 0) { 34 | rfs_r1block(rdev, f_inode->addrs[block_offset]); // 读取原始块到iobuffer 35 | int first_block_len = (writetimes == 0 ? len : RFS_BLKSIZE - align); 36 | memcpy(rdev->iobuffer + align, w_buf, first_block_len); // 局部修改(类似网页4的缓冲机制) 37 | rfs_w1block(rdev, f_inode->addrs[block_offset]); // 写回设备 38 | 39 | buf_offset += first_block_len; 40 | block_offset++; 41 | writetimes--; // 调整剩余完整块数 42 | } 43 | 44 | /* ===== 阶段4:完整块批量写入 ===== */ 45 | if (writetimes >= 0) { 46 | while (writetimes-- > 0) { 47 | // 动态扩展块(类似网页1中R的save函数自动分配空间) 48 | if (block_offset == f_inode->blocks) { 49 | f_inode->addrs[block_offset] = rfs_alloc_block(f_inode->sb); // 分配新块 50 | f_inode->blocks++; 51 | } 52 | 53 | memcpy(rdev->iobuffer, w_buf + buf_offset, RFS_BLKSIZE); 54 | rfs_w1block(rdev, f_inode->addrs[block_offset]); // 整块写入 55 | 56 | buf_offset += RFS_BLKSIZE; 57 | block_offset++; 58 | } 59 | 60 | /* ===== 阶段5:尾部剩余数据写入 ===== */ 61 | if (remain > 0) { 62 | if (block_offset == f_inode->blocks) { // 需要新块存储尾部数据 63 | f_inode->addrs[block_offset] = rfs_alloc_block(f_inode->sb); 64 | ++f_inode->blocks; 65 | } 66 | memset(rdev->iobuffer, 0, RFS_BLKSIZE); // 清空缓冲区(避免旧数据污染) 67 | memcpy(rdev->iobuffer, w_buf + buf_offset, remain); 68 | rfs_w1block(rdev, f_inode->addrs[block_offset]); // 写入非完整块 69 | } 70 | } 71 | 72 | /* ===== 阶段6:元数据更新 ===== */ 73 | f_inode->size = MAX(f_inode->size, *offset + len); // 扩展文件大小(类似网页2中Excel文件追加写入) 74 | *offset += len; // 更新偏移量(类似网页4的fwrite后移动文件指针) 75 | return len; // 始终返回请求长度(假设无I/O错误) 76 | } 77 | ``` 78 | 79 | --- 80 | 81 | ### 关键设计解析 82 | 83 | 1. **块对齐优化** 84 | - 采用三阶段写入策略(首部非对齐→完整块→尾部剩余),最大限度减少设备I/O次数 85 | - 使用`memcpy`局部更新首部块,避免整块重写(类似网页4的缓冲技术) 86 | 87 | 2. **动态空间分配** 88 | - 通过`rfs_alloc_block`按需扩展存储块(类似网页1中R自动分配文件空间) 89 | - 维护`f_inode->blocks`跟踪已分配块数,支持文件动态增长 90 | 91 | 3. **原子性保证** 92 | - 在修改块数据前完成块分配,防止写入中途空间不足 93 | - 元数据(size/offset)更新在全部写入完成后进行 94 | 95 | 4. **设备交互层** 96 | - 通过`rfs_w1block`抽象块设备写入(类似网页3中网卡DMA操作) 97 | - 使用`iobuffer`作为设备交互的中间缓冲区,隔离用户空间与物理设备 98 | 99 | --- 100 | 101 | ### 与标准库函数对比 102 | 103 | | 特性 | `rfs_write`实现 | `fwrite`标准实现 | 104 | |--------------------|------------------------------|-----------------------------| 105 | | **缓冲机制** | 设备级`iobuffer`,无用户态缓冲 | 用户态缓冲减少系统调用 | 106 | | **错误处理** | 直接panic(内核级) | 返回错误码,支持errno | 107 | | **空间管理** | 手动块分配(`rfs_alloc_block`)| 文件系统自动处理 | 108 | | **并发支持** | 未体现锁机制 | 线程安全(带锁实现) | 109 | | **写入模式** | 覆盖/追加取决于offset | 通过`fopen`模式控制 | 110 | 111 | --- 112 | 113 | ### 性能优化建议 114 | 115 | 1. **批量写入优化** 116 | - 对连续完整块可采用DMA直接传输(参考网页3的DMA技术) 117 | - 实现块预分配机制,减少动态分配开销 118 | 119 | 2. **缓存层集成** 120 | - 添加写入缓存队列,延迟合并小写入(类似网页4的用户态缓冲) 121 | - 实现LRU缓存减少高频块的设备访问 122 | 123 | 3. **日志功能** 124 | - 添加写入日志防止掉电数据丢失 125 | - 采用Copy-on-Write策略保证原子性 126 | 127 | 该实现体现了嵌入式文件系统的典型特征,在资源受限环境下平衡了功能性与性能,适合需要精细控制存储介质的场景。 -------------------------------------------------------------------------------- /code/file_system/rfs_write_back_vinode.md: -------------------------------------------------------------------------------- 1 | 以下是对`rfs_write_back_vinode`函数的详细中文注释与功能解析,结合文件系统元数据持久化原理与内核设计规范: 2 | 3 | ```c 4 | /** 5 | * 函数功能:将虚拟inode的元数据回写到磁盘(实现内存与磁盘的元数据同步) 6 | * 参数: 7 | * vinode - 需要持久化的虚拟inode指针 8 | * 返回值:0=成功,-1=失败 9 | * 核心流程: 10 | * 1. 内存到磁盘的数据结构转换 11 | * 2. 元数据字段拷贝 12 | * 3. 块设备写入操作 13 | * 设计特点: 14 | * - 仅同步关键元数据字段(非完整inode拷贝) 15 | * - 直接地址映射,无间接块支持(类似网页2的简单文件系统设计) 16 | */ 17 | int rfs_write_back_vinode(struct vinode *vinode) { 18 | /* ===== 阶段1:内存数据结构转换 ===== */ 19 | struct rfs_dinode dinode; // 创建临时磁盘inode结构(栈分配避免内存泄漏) 20 | 21 | // 基础元数据拷贝(类似网页5的元数据持久化策略) 22 | dinode.size = vinode->size; // 文件大小(字节单位) 23 | dinode.nlinks = vinode->nlinks;// 硬链接计数(参考网页2的链接管理) 24 | dinode.blocks = vinode->blocks;// 已分配块数(影响网页3的IO统计) 25 | dinode.type = vinode->type; // 文件类型(R_FILE/R_DIR等) 26 | 27 | // 数据块地址数组拷贝(直接索引,无间接块) 28 | for (int i = 0; i < RFS_DIRECT_BLKNUM; ++i) { // RFS_DIRECT_BLKNUM=直接块数 29 | dinode.addrs[i] = vinode->addrs[i]; // 块地址映射(类似网页2的块分配表) 30 | } 31 | 32 | /* ===== 阶段2:设备交互 ===== */ 33 | struct rfs_device *rdev = rfs_device_list[vinode->sb->s_dev->dev_id]; // 获取关联块设备 34 | 35 | // 写入磁盘inode(原子操作,参考网页3的块设备写入特性) 36 | if (rfs_write_dinode(rdev, &dinode, vinode->inum) != 0) { 37 | sprint("错误:磁盘inode回写失败!\n"); // 需扩展错误码(如网页7的错误处理) 38 | return -1; 39 | } 40 | 41 | return 0; // 成功返回(类似网页4的同步操作返回值设计) 42 | } 43 | ``` 44 | 45 | --- 46 | 47 | ### 关键设计解析 48 | 49 | | 设计要素 | 实现方式 | 设计依据 | 50 | |---------------------|-------------------------------------------------------------------------|-------------------------| 51 | | **元数据选择** | 仅同步size/nlinks等关键字段,忽略权限/时间戳(简化设计) | 符合嵌入式场景需求 | 52 | | **地址映射** | 直接拷贝addrs数组,支持固定数量直接块(RFS_DIRECT_BLKNUM) | 类似早期UNIX文件系统设计 | 53 | | **临时变量使用** | 栈分配dinode结构,避免动态内存管理 | 提升性能,防止内存泄漏 | 54 | | **设备抽象** | 通过vinode->sb层级访问块设备,保持VFS分层架构 | 类似网页2的VFS设计理念 | 55 | 56 | --- 57 | 58 | ### 与标准实现对比 59 | 60 | | 特性 | rfs_write_back_vinode | Linux write_inode | 61 | |---------------------|-------------------------------------|--------------------------------| 62 | | 同步触发时机 | 显式调用 | 通过脏页标记自动触发 | 63 | | 元数据完整性 | 部分字段 | 包含权限、时间戳等完整信息 | 64 | | 写入策略 | 直接写入 | 可配置writeback/ordered/journal模式 | 65 | | 原子性保证 | 无 | 使用日志(journal)保证 | 66 | | 错误处理 | 简单返回-1 | 详细错误码与重试机制 | 67 | 68 | --- 69 | 70 | ### 典型调用场景 71 | 72 | 1. **文件关闭时** 73 | ```c 74 | vfs_close(file) -> rfs_write_back_vinode(file->vinode) 75 | ``` 76 | 确保关闭后元数据持久化(类似网页4的文件操作语义) 77 | 78 | 2. **定时同步任务** 79 | ```c 80 | void sync_daemon() { 81 | foreach(vinode in modified_list) 82 | rfs_write_back_vinode(vinode); 83 | } 84 | ``` 85 | 参考网页3的定期刷盘机制 86 | 87 | 3. **元数据修改操作** 88 | ```c 89 | vfs_truncate(vinode) { 90 | vinode->size = 0; 91 | rfs_write_back_vinode(vinode); // 立即生效 92 | } 93 | ``` 94 | 类似网页5的即时持久化需求 95 | 96 | --- 97 | 98 | ### 潜在改进建议 99 | 100 | 1. **增量写入优化** 101 | ```c 102 | if (vinode->dirty) { // 添加脏页标记 103 | rfs_write_back_vinode(vinode); 104 | vinode->dirty = 0; 105 | } 106 | ``` 107 | 参考网页3的脏页管理机制,减少不必要的全量写入 108 | 109 | 2. **日志支持** 110 | ```c 111 | journal_begin(); 112 | rfs_write_dinode(...); 113 | journal_commit(); // 失败时回滚 114 | ``` 115 | 类似网页2的日志文件系统设计,提升崩溃一致性 116 | 117 | 3. **异步写入** 118 | ```c 119 | queue_async_write(dinode, callback); // 非阻塞写入 120 | ``` 121 | 参考网页3的异步IO模型,提升系统响应速度 122 | 123 | 4. **元数据校验** 124 | ```c 125 | crc = calculate_crc(&dinode); // 添加校验和 126 | rfs_write_dinode(..., crc); 127 | ``` 128 | 防止磁盘静默错误(参考网页7的数据完整性方案) 129 | 130 | --- 131 | 132 | 该实现体现了轻量级文件系统元数据管理的核心需求,在数据持久化与性能之间取得平衡,适合对可靠性要求不高的嵌入式场景,但需结合具体需求扩展高级特性。 -------------------------------------------------------------------------------- /code/file_system/vfs_mkdir.md: -------------------------------------------------------------------------------- 1 | 以下是对`vfs_mkdir`函数的功能解析及流程注释,结合Linux VFS设计理念和文件系统实现原理: 2 | 3 | ```c 4 | /** 5 | * VFS层目录创建函数 6 | * @param path 目标目录路径(仅创建最后一级,父目录必须存在) 7 | * @return 0=成功,-1=失败 8 | * 核心流程: 9 | * 1. 路径解析与父目录校验 10 | * 2. 分配目录项(dentry)与调用具体文件系统实现 11 | * 3. 资源绑定与缓存更新 12 | * 设计特点: 13 | * - 符合POSIX目录创建语义 14 | * - 通过viop_mkdir抽象具体文件系统差异 15 | */ 16 | int vfs_mkdir(const char *path) { 17 | /* 阶段1:路径解析与校验 */ 18 | struct dentry *parent = vfs_root_dentry; // 从根目录开始解析 19 | char miss_name[MAX_PATH_LEN]; // 存储路径解析失败点名称 20 | 21 | // 调用路径解析器定位目标目录(类似ext4的__lookup_hash) 22 | struct dentry *file_dentry = lookup_final_dentry(path, &parent, miss_name); 23 | if (file_dentry) { // 目标目录已存在 24 | sprint("错误:目录已存在!\n"); 25 | return -1; 26 | } 27 | 28 | /* 阶段2:父目录存在性验证 */ 29 | char basename[MAX_PATH_LEN]; 30 | get_base_name(path, basename); // 提取目标目录名(如"/a/b/c"取"c") 31 | if (strcmp(miss_name, basename) != 0) { // 父目录不存在(如创建/a/b/c但/a/b不存在) 32 | sprint("错误:父目录不存在!\n"); 33 | return -1; 34 | } 35 | 36 | /* 阶段3:目录资源分配 */ 37 | // 分配VFS目录项(内存对象,类似Linux的d_alloc) 38 | struct dentry *new_dentry = alloc_vfs_dentry(basename, NULL, parent); 39 | 40 | // 调用具体文件系统实现(如ext4_mkdir或f2fs_mkdir) 41 | struct vinode *new_dir_inode = viop_mkdir(parent->dentry_inode, new_dentry); 42 | if (!new_dir_inode) { // 文件系统层创建失败(如磁盘空间不足) 43 | free_page(new_dentry); 44 | sprint("错误:目录创建失败!\n"); 45 | return -1; 46 | } 47 | 48 | /* 阶段4:资源关联与缓存更新 */ 49 | new_dentry->dentry_inode = new_dir_inode; // 绑定目录项与inode 50 | new_dir_inode->ref++; // 增加inode引用计数(类似ihold) 51 | 52 | hash_put_dentry(new_dentry); // 加入目录项哈希缓存(dcache) 53 | hash_put_vinode(new_dir_inode); // 加入inode哈希缓存(icache) 54 | 55 | return 0; 56 | } 57 | ``` 58 | 59 | --- 60 | 61 | ### 关键流程与Linux设计对比 62 | 63 | | 步骤 | 本实现 | Linux实现参考 | 差异点分析 | 64 | |--------------------|--------------------------------|-------------------------------------|------------------------------| 65 | | **路径解析** | `lookup_final_dentry` | `user_path_parent`+`lookup_create` | 简化了权限检查和符号链接处理 | 66 | | **父目录校验** | `miss_name`比较 | `nd.last_type`状态判断 | 实现类似但数据结构不同 | 67 | | **目录项分配** | `alloc_vfs_dentry` | `d_alloc`+`__d_alloc` | 未实现内存回收时的LRU机制 | 68 | | **文件系统回调** | `viop_mkdir` | `inode->i_op->mkdir` | 函数命名风格不同,原理一致 | 69 | | **缓存管理** | 手动维护哈希表 | 通过dcache/icache自动管理 | 缺少LRU淘汰策略和状态同步 | 70 | 71 | --- 72 | 73 | ### 核心设计缺陷与改进建议 74 | 75 | 1. **原子性缺失** 76 | - 问题:若`viop_mkdir`成功但缓存更新失败,会导致数据不一致 77 | - 改进:引入事务机制(类似ext4的journal),将目录项分配、inode写入、缓存更新打包为原子操作 78 | 79 | 2. **并发控制不足** 80 | - 问题:未对父目录inode加锁,可能引发竞态条件 81 | - 改进:添加类似`mutex_lock(&parent->dentry_inode->i_mutex)`的锁机制 82 | 83 | 3. **资源泄漏风险** 84 | - 问题:`new_dir_inode->ref++`未配套释放逻辑 85 | - 改进:实现`vfs_iput`函数,在删除目录时递减引用计数并触发`viop_destroy_inode` 86 | 87 | 4. **路径解析局限性** 88 | - 问题:`lookup_final_dentry`不支持符号链接和挂载点跨越 89 | - 改进:实现类似`walk_component`的分段解析逻辑 90 | 91 | --- 92 | 93 | 该实现体现了VFS层目录创建的核心思想——**路径解析、父目录校验、文件系统委托、缓存维护**,但相比成熟实现(如Linux的`vfs_mkdir`)在原子性、并发性和错误恢复机制上有明显简化,适用于嵌入式等轻量级场景。 -------------------------------------------------------------------------------- /code/file_system/vfs_mount.md: -------------------------------------------------------------------------------- 1 | 以下是对 `vfs_mount` 函数的详细中文注释及功能解析: 2 | 3 | ```c 4 | /** 5 | * 函数功能:挂载设备到虚拟文件系统(VFS) 6 | * 参数: 7 | * dev_name - 要挂载的设备名称(如 "ramdisk0") 8 | * mnt_type - 挂载类型(MOUNT_AS_ROOT 或 MOUNT_DEFAULT) 9 | * 返回值:成功返回超级块指针,失败触发 panic 10 | */ 11 | struct super_block *vfs_mount(const char *dev_name, int mnt_type) { 12 | struct device *p_device = NULL; // 设备指针 13 | 14 | /* ========== 阶段1:设备查找 ========== */ 15 | // 遍历设备列表查找目标设备(最多 MAX_VFS_DEV 个设备) 16 | for (int i = 0; i < MAX_VFS_DEV; ++i) { 17 | p_device = vfs_dev_list[i]; 18 | // 检查设备是否存在且名称匹配 19 | if (p_device && strcmp(p_device->dev_name, dev_name) == 0) 20 | break; 21 | } 22 | if (p_device == NULL) 23 | panic("vfs_mount: 找不到指定设备!\n"); // 设备未注册时的错误处理 24 | 25 | /* ========== 阶段2:超级块初始化 ========== */ 26 | // 获取设备对应的文件系统类型(如 ext2, rfs 等) 27 | struct file_system_type *fs_type = p_device->fs_type; 28 | // 调用文件系统特定方法获取超级块 29 | struct super_block *sb = fs_type->get_superblock(p_device); 30 | 31 | // 将根目录的虚拟inode加入哈希表(实现快速查找) 32 | hash_put_vinode(sb->s_root->dentry_inode); 33 | 34 | /* ========== 阶段3:超级块管理 ========== */ 35 | // 将新超级块加入全局超级块列表(最大支持 MAX_MOUNTS 个挂载点) 36 | int err = 1; 37 | for (int i = 0; i < MAX_MOUNTS; ++i) { 38 | if (vfs_sb_list[i] == NULL) { 39 | vfs_sb_list[i] = sb; // 插入空闲槽位 40 | err = 0; 41 | break; 42 | } 43 | } 44 | if (err) 45 | panic("vfs_mount: 挂载数量超过上限!\n"); // 系统挂载点数量限制 46 | 47 | /* ========== 阶段4:挂载点处理 ========== */ 48 | if (mnt_type == MOUNT_AS_ROOT) { // 作为根文件系统挂载 49 | vfs_root_dentry = sb->s_root; // 设置为全局根目录项 50 | hash_put_dentry(sb->s_root); // 哈希表记录根目录 51 | } 52 | else if (mnt_type == MOUNT_DEFAULT) { // 作为子目录挂载 53 | if (!vfs_root_dentry) 54 | panic("vfs_mount: 请先挂载根设备!\n"); // 依赖根文件系统存在 55 | 56 | struct dentry *mnt_point = sb->s_root; 57 | // 设置挂载点属性: 58 | strcpy(mnt_point->name, p_device->dev_name); // 目录名=设备名 59 | mnt_point->parent = vfs_root_dentry; // 父目录指向系统根 60 | hash_put_dentry(sb->s_root); // 哈希表记录新挂载点 61 | } 62 | else { 63 | panic("vfs_mount: 未知挂载类型!\n"); // 参数校验 64 | } 65 | 66 | return sb; // 返回初始化好的超级块 67 | } 68 | ``` 69 | 70 | --- 71 | 72 | ### 函数功能详解 73 | 该函数实现了一个**简易版文件系统挂载机制**,主要完成以下核心任务: 74 | 75 | #### 1. 设备定位 (Device Discovery) 76 | - 遍历预注册的设备列表 `vfs_dev_list` 77 | - 通过设备名称匹配目标设备 78 | - 实现设备与文件系统的解耦(设备可承载不同文件系统) 79 | 80 | #### 2. 超级块管理 (Superblock Management) 81 | - 调用文件系统特定的 `get_superblock` 方法(实现多文件系统支持) 82 | - 维护全局超级块列表 `vfs_sb_list`(用于后续文件系统操作) 83 | - 管理挂载点数量上限(`MAX_MOUNTS`) 84 | 85 | #### 3. 目录树构建 (Directory Tree Construction) 86 | - **根文件系统挂载** (MOUNT_AS_ROOT) 87 | - 设置全局根目录 `vfs_root_dentry` 88 | - 建立初始目录树结构 89 | - **子目录挂载** (MOUNT_DEFAULT) 90 | - 在现有根目录下创建以设备名为名称的目录 91 | - 维护目录父子关系(`parent` 指针) 92 | - 示例:挂载 "disk1" 后路径为 `/disk1` 93 | 94 | #### 4. 元数据管理 (Metadata Management) 95 | - 使用哈希表 `hash_put_vinode` 管理虚拟 inode 96 | - 通过 `hash_put_dentry` 加速目录项查找 97 | - 实现路径名到存储结构的快速映射 98 | 99 | --- 100 | 101 | ### 关键设计特点 102 | 1. **双模式挂载**: 103 | - **根文件系统模式**:初始化全局目录树 104 | - **子目录模式**:实现类似 Linux 的 `/dev/` 设备目录结构 105 | 106 | 2. **扩展性设计**: 107 | - 通过 `file_system_type` 结构支持多种文件系统 108 | - 设备列表和超级块列表使用固定大小数组(简化内存管理) 109 | 110 | 3. **数据一致性**: 111 | - 挂载前必须存在根文件系统(MOUNT_DEFAULT 的强依赖) 112 | - 目录项父子关系的显式维护 113 | 114 | 4. **性能优化**: 115 | - 哈希表加速 inode 和目录项查找 116 | - 目录项名称直接使用设备名(避免额外存储) 117 | 118 | --- 119 | 120 | ### 典型工作流程 121 | ```text 122 | 1. 初始化阶段:MOUNT_AS_ROOT 123 | vfs_mount("ramdisk0", MOUNT_AS_ROOT) --> 创建 / 124 | 125 | 2. 扩展存储:MOUNT_DEFAULT 126 | vfs_mount("disk1", MOUNT_DEFAULT) --> 创建 /disk1 127 | vfs_mount("usb0", MOUNT_DEFAULT) --> 创建 /usb0 128 | 129 | 3. 最终目录结构: 130 | / 131 | ├─ dev/ 132 | ├─ disk1/ 133 | └─ usb0/ 134 | ``` 135 | 136 | --- 137 | 138 | ### 限制与注意事项 139 | 1. **挂载顺序**:必须先挂载根文件系统才能使用 MOUNT_DEFAULT 140 | 2. **命名冲突**:MOUNT_DEFAULT 使用设备名作为目录名,需保证设备名唯一 141 | 3. **路径深度**:不支持多级挂载(如 `/mnt/disk1`) 142 | 4. **错误处理**:采用 panic 直接终止,适合嵌入式场景,但需确保设备可靠性 143 | 144 | 该实现反映了嵌入式系统(如 PKE)中典型的轻量级 VFS 设计,在功能完备性和实现复杂度之间取得平衡。 -------------------------------------------------------------------------------- /code/lab2的内核初始化改动.md: -------------------------------------------------------------------------------- 1 | # lab2的内核初始化改动 2 | 3 | ## s_start 4 | 在s_start之前的过程没有发生变化。 5 | ```c 6 | int s_start(void) { 7 | sprint("Enter supervisor mode...\n"); 8 | // satp是(S-mode)下控制页表的核心寄存器,用于地址变换和权限控制 9 | // 存储分页模式MODE(bare/sv39/sv48) 10 | // 区分不同进程的地址空间ASID 11 | // 存储页表基址PPN 12 | write_csr(satp, 0); 13 | 14 | // 初始化物理页资源表 15 | // 确定核心占用的内存区域首尾 g_kernel_start, g_kernel_end 16 | // 确定所有可用的物理页资源首尾 free_mem_start_addr, free_mem_end_addr 17 | // 以此创建空闲物理页表 create_freepage_list(free_mem_start_addr, free_mem_end_addr); 18 | // 19 | // 空闲物理页资源表的数据结构 20 | // 通过全局变量 static list_node g_free_mem_list; 管理所有空闲的物理页 21 | // list_node->next 指向下一个空闲的物理页基址,最后一个节点指向空指针。 22 | // 释放物理页:在物理页开头创建链表节点,并插入物理页链表头部。 23 | // 分配物理页:分配链表中第一个节点,并更新物理页链表。 24 | pmm_init(); 25 | 26 | // 参见kernel.lds. 27 | // 初始化内核空间的内存地址映射 28 | // 将code and text segment映射为虚实地址相同的 读/执行权限页 29 | // 将内核剩下的段映射为虚实地址相同 的读/写页 30 | // 分配一个空闲页用作全局页目录 g_kernel_pagetable(指向对应的内存页地址) 31 | kern_vm_init(); 32 | 33 | // 写入satp寄存器并刷新tlb缓存 34 | // 从这里开始,所有内存访问都通过MMU进行虚实转换 35 | enable_paging(); 36 | 37 | sprint("kernel page table is on \n"); 38 | load_user_program(&user_app); 39 | sprint("Switch to user mode...\n"); 40 | switch_to(&user_app); 41 | return 0; 42 | } 43 | ``` 44 | 45 | ## load_user_program 46 | ```c 47 | ****void load_user_program(process *proc) { 48 | sprint("User application is loading.\n"); 49 | 50 | // 为进程控制块的各个成员指针分配物理内存 51 | proc->trapframe = (trapframe *)alloc_page(); memset(proc->trapframe, 0, sizeof(trapframe)); 52 | proc->pagetable = (pagetable_t)alloc_page(); memset((void *)proc->pagetable, 0, PGSIZE); 53 | 54 | // 内核栈是自上而下增长的,所以说起始位置是页的高地址(左闭右开) 55 | proc->kstack = (uint64)alloc_page() + PGSIZE; 56 | uint64 user_stack_bottom = (uint64)alloc_page(); 57 | 58 | // USER_STACK_TOP = 0x7ffff000, defined in kernel/memlayout.h 59 | proc->trapframe->regs.sp = USER_STACK_TOP; //virtual address of user stack top 60 | 61 | sprint("user frame 0x%lx, user stack 0x%lx, user kstack 0x%lx \n", proc->trapframe, 62 | proc->trapframe->regs.sp, proc->kstack); 63 | 64 | load_bincode_from_host_elf(proc); 65 | 66 | // 为用户栈创建地址映射 67 | user_vm_map((pagetable_t)proc->pagetable, USER_STACK_TOP - PGSIZE, PGSIZE, user_stack_bottom, 68 | prot_to_type(PROT_WRITE | PROT_READ, 1)); 69 | 70 | // 为中断上下文创建地址映射 71 | user_vm_map((pagetable_t)proc->pagetable, (uint64)proc->trapframe, PGSIZE, (uint64)proc->trapframe, 72 | prot_to_type(PROT_WRITE | PROT_READ, 0)); 73 | 74 | // 因为用户模式触发中断时,使用的stvec=smode_trap_vector仍然是虚拟地址,所以说要把虚拟中断入口地址-->物理中断入口地址的虚实映射关系,也加入到用户模式的页表当中,才能让软中断成功跳转到正确的中断入口向量地址。 75 | user_vm_map((pagetable_t)proc->pagetable, (uint64)trap_sec_start, PGSIZE, (uint64)trap_sec_start, 76 | prot_to_type(PROT_READ | PROT_EXEC, 0)); 77 | } 78 | ``` 79 | 80 | ## elf_alloc_mb 81 | 我改进了代码,让elf_alloc_mb支持多个物理页: 82 | ```c 83 | static void *elf_alloc_mb(elf_ctx *ctx, uint64 elf_pa, uint64 elf_va, uint64 size) { 84 | elf_info *msg = (elf_info *)ctx->info; 85 | 86 | // 计算需要多少页 87 | uint64 num_pages = (size + PGSIZE - 1) / PGSIZE; // 向上取整 88 | void *first_pa = NULL; 89 | 90 | for (uint64 i = 0; i < num_pages; i++) 91 | { 92 | void *pa = alloc_page(); 93 | if (pa == 0) 94 | panic("uvmalloc mem alloc failed\n"); 95 | 96 | memset((void *)pa, 0, PGSIZE); 97 | 98 | // 记录第一个分配的物理页 99 | if (i == 0) 100 | first_pa = pa; 101 | 102 | // 映射虚拟地址到物理地址 103 | user_vm_map((pagetable_t)msg->p->pagetable, elf_va + i * PGSIZE, PGSIZE, (uint64)pa, 104 | prot_to_type(PROT_WRITE | PROT_READ | PROT_EXEC, 1)); 105 | } 106 | 107 | return first_pa; // 返回第一个物理页的地址 108 | } 109 | ``` 110 | 111 | - pmm_init() 112 | ## kern_vm_init 113 | 114 | 115 | - kern_vm_init() -------------------------------------------------------------------------------- /code/硬中断处理程序.md: -------------------------------------------------------------------------------- 1 | # RISC-V M模式硬件中断处理程序文档 2 | 3 | ## 概述 4 | 本程序段描述了在RISC-V架构的M模式下,如何处理硬件中断。具体包括中断向量入口 (`mtrapvec`)、中断处理函数 (`handle_mtrap`) 和非法指令处理函数 (`handle_illegal_instruction`) 的实现。程序通过保存和恢复寄存器的状态、切换栈空间,并调用相应的中断处理函数来响应和处理不同类型的硬件中断和异常。 5 | 6 | --- 7 | 8 | ## mtrapvec 9 | 10 | ```asm 11 | #include "util/load_store.S" # 引入一个宏文件,提供存储和恢复寄存器的工具宏,通常用于保存/恢复寄存器状态 12 | 13 | # 14 | # M-mode trap entry point 15 | # 16 | .globl mtrapvec # 定义全局符号 mtrapvec,使其可以在其他文件中引用 17 | .align 4 # 对齐 mtrapvec 标签,确保其在 4 字节边界处,便于指令的高效执行 18 | mtrapvec: 19 | # mscratch -> g_itrframe (cf. kernel/machine/minit.c line 94) 20 | # swap a0 and mscratch, so that a0 points to interrupt frame, 21 | # i.e., [a0] = &g_itrframe 22 | csrrw a0, mscratch, a0 # 将 a0 的值写入 mscratch 寄存器,并将 mscratch 的值加载到 a0。此时,a0 存储的是中断帧的指针 (即 g_itrframe)。 23 | 24 | # save the registers in g_itrframe 25 | addi t6, a0, 0 # 将 a0 的值(指向中断帧的指针)存储到 t6 寄存器,t6 将作为操作的临时寄存器。 26 | store_all_registers # 调用宏 store_all_registers,将所有通用寄存器的值存储到 g_itrframe(由 a0 指向的内存位置) 27 | 28 | # save the original content of a0 in g_itrframe 29 | csrr t0, mscratch # 将 mscratch 的值(原先保存的 a0 值)加载到 t0 寄存器 30 | sd t0, 72(a0) # 将 t0 中保存的 mscratch 原值存储到 g_itrframe 的偏移量 72 处,保存原始 a0 的值。 31 | 32 | # switch stack (to use stack0) for the rest of machine mode 33 | # trap handling. 34 | la sp, stack0 # 加载栈地址 stack0 到栈指针(sp)中,改变当前的栈空间。此时将切换到新的栈(stack0),用于后续处理。 35 | li a3, 4096 # 将 4096 载入 a3 寄存器,计算新的栈空间偏移 36 | csrr a4, mhartid # 从 mhartid 寄存器中读取当前处理器核心的 ID(用于支持多核) 37 | addi a4, a4, 1 # 增加 1 以确保不同核心使用不同的栈空间 38 | mul a3, a3, a4 # 将栈偏移量(4096)乘以核心 ID,确保每个核心的栈空间不同 39 | add sp, sp, a3 # 计算并更新栈指针 sp,将其指向正确的栈空间 40 | 41 | # pointing mscratch back to g_itrframe 42 | csrw mscratch, a0 # 将 a0 的值(指向 g_itrframe 的指针)写回 mscratch 寄存器,为后续恢复寄存器做好准备 43 | 44 | # call machine mode trap handling function 45 | call handle_mtrap # 调用机器模式下的中断处理函数 handle_mtrap,执行具体的异常或中断处理逻辑 46 | 47 | # restore all registers, come back to the status before entering 48 | # machine mode handling. 49 | csrr t6, mscratch # 从 mscratch 寄存器中恢复之前保存的 a0 值(g_itrframe 指针),存入 t6 寄存器 50 | restore_all_registers # 调用宏 restore_all_registers,将所有保存的寄存器值恢复回 CPU 寄存器,恢复到中断前的状态。 51 | 52 | mret # 返回机器模式,恢复之前的执行上下文,跳回到中断前的执行位置 53 | ``` 54 | 55 | ### 说明: 56 | `mtrapvec` 是M模式中断的入口点,负责在硬件中断发生时保存寄存器的状态、切换栈空间、调用中断处理函数,并在处理完成后恢复寄存器状态,最后通过 `mret` 返回。 57 | 58 | --- 59 | 60 | ## void handle_mtrap() 61 | 62 | ```cpp 63 | // 64 | // handle_mtrap calls a handling function according to the type of a machine mode interrupt (trap). 65 | // 66 | void handle_mtrap() { 67 | uint64 mcause = read_csr(mcause); // 读取中断原因寄存器 mcause 68 | switch (mcause) { 69 | case CAUSE_FETCH_ACCESS: 70 | handle_instruction_access_fault(); // 处理指令访问故障 71 | break; 72 | case CAUSE_LOAD_ACCESS: 73 | handle_load_access_fault(); // 处理加载访问故障 74 | case CAUSE_STORE_ACCESS: 75 | handle_store_access_fault(); // 处理存储访问故障 76 | break; 77 | case CAUSE_ILLEGAL_INSTRUCTION: 78 | // TODO (lab1_2): call handle_illegal_instruction to implement illegal instruction 79 | // interception, and finish lab1_2. 80 | panic("call handle_illegal_instruction to accomplish illegal instruction interception for lab1_2.\n"); 81 | break; 82 | case CAUSE_MISALIGNED_LOAD: 83 | handle_misaligned_load(); // 处理加载对齐错误 84 | break; 85 | case CAUSE_MISALIGNED_STORE: 86 | handle_misaligned_store(); // 处理存储对齐错误 87 | break; 88 | 89 | default: 90 | sprint("machine trap(): unexpected mscause %p\n", mcause); // 异常处理,输出异常信息 91 | sprint(" mepc=%p mtval=%p\n", read_csr(mepc), read_csr(mtval)); // 输出异常发生的地址 92 | panic("unexpected exception happened in M-mode.\n"); // 异常终止 93 | break; 94 | } 95 | } 96 | ``` 97 | 98 | ### 说明: 99 | `handle_mtrap` 函数根据 `mcause` 寄存器的值判断发生了哪种类型的中断或异常,并调用相应的处理函数。如果是未定义的异常类型,则打印错误信息并进入紧急终止。 100 | 101 | --- 102 | 103 | ## static void handle_illegal_instruction() 104 | 105 | ```cpp 106 | static void handle_illegal_instruction() { panic("Illegal instruction!"); } 107 | ``` 108 | 109 | ### 说明: 110 | `handle_illegal_instruction` 用于处理非法指令的异常,当发生非法指令时,程序将调用此函数并终止执行,输出错误信息。 -------------------------------------------------------------------------------- /code/系统调用服务程序.md: -------------------------------------------------------------------------------- 1 | # 系统调用程序 2 | ## 概述 3 | 由于系统调用众多,这里仅以sys_user_print为例: 4 | 1. **do_syscall** 5 | 处理系统调用的入口函数,根据传入的系统调用号 `a0`,调用不同的系统调用处理程序。 6 | 7 | 2. **sys_user_print** 8 | 实现 `SYS_user_print` 系统调用,用于将用户提供的字符串打印到控制台。 9 | 10 | 3. **sprint** 11 | 处理变长参数的字符串输出,调用 `vprintk` 将格式化后的字符串输出到控制台。 12 | 13 | 4. **vprintk** 14 | 将格式化的字符串通过 `vsnprintf` 处理后,使用 `spike_file_write` 输出到控制台。 15 | 16 | 5. **spike_file_write** 17 | 负责将数据写入标准输出或文件,实际执行写操作。 18 | 19 | 6. **frontend_syscall** 20 | 封装 HTIF 系统调用,传递参数并等待 Spike 仿真器返回结果。 21 | 22 | 7. **htif_syscall** 23 | 执行 HTIF 系统调用,调用 Spike 仿真器处理系统调用请求。 24 | 25 | 8. **do_tohost_fromhost** 26 | 通过轮询方式等待 Spike 仿真器响应系统调用,并返回结果。 27 | 28 | ## long do_syscall(long a0, long a1,......) 29 | - 这是软中断入口程序的结束。 30 | ```cpp 31 | // 32 | // [a0]: the syscall number; [a1] ... [a7]: arguments to the syscalls. 33 | // returns the code of success, (e.g., 0 means success, fail for otherwise) 34 | // 35 | long do_syscall(long a0, long a1, long a2, long a3, long a4, long a5, long a6, long a7) { 36 | switch (a0) { 37 | // 根据不同的中断号a0, 进入不同的系统调用过程 38 | case SYS_user_print: 39 | return sys_user_print((const char*)a1, a2); 40 | case SYS_user_exit: 41 | return sys_user_exit(a1); 42 | default: 43 | panic("Unknown syscall %ld \n", a0); 44 | } 45 | } 46 | 47 | ``` 48 | 49 | ## ssize_t sys_user_print(const char* buf, size_t n) 50 | ```cpp 51 | // 52 | // implement the SYS_user_print syscall 53 | // 54 | ssize_t sys_user_print(const char* buf, size_t n) { 55 | sprint(buf); 56 | return 0; 57 | } 58 | ``` 59 | 60 | ## void sprint(const char* s, ...) 61 | ```cpp 62 | void sprint(const char* s, ...) { 63 | va_list vl; 64 | va_start(vl, s); 65 | 66 | vprintk(s, vl); 67 | 68 | va_end(vl); 69 | } 70 | 71 | ``` 72 | 73 | ## void vprintk(const char* s, va_list vl) 74 | ```cpp 75 | void vprintk(const char* s, va_list vl) { 76 | char out[256]; 77 | int res = vsnprintf(out, sizeof(out), s, vl); 78 | //you need spike_file_init before this call 79 | spike_file_write(stderr, out, res < sizeof(out) ? res : sizeof(out)); 80 | } 81 | ``` 82 | 83 | 84 | 85 | ## ssize_t spike_file_write(spike_file_t* f, const void* buf, size_t size) 86 | ```cpp 87 | ssize_t spike_file_write(spike_file_t* f, const void* buf, size_t size) 88 | ``` 89 | 90 | 91 | 92 | ## long frontend_syscall(long n, uint64 a0, uint64 a1, uint64 a2, uint64 a3, uint64 a4,uint64 a5, uint64 a6) 93 | ```cpp 94 | //============= encapsulating htif syscalls, invoking Spike functions ============= 95 | long frontend_syscall(long n, uint64 a0, uint64 a1, uint64 a2, uint64 a3, uint64 a4, 96 | uint64 a5, uint64 a6) { 97 | static volatile uint64 magic_mem[8]; 98 | 99 | static spinlock_t lock = SPINLOCK_INIT; 100 | spinlock_lock(&lock); 101 | 102 | magic_mem[0] = n; 103 | magic_mem[1] = a0; 104 | magic_mem[2] = a1; 105 | magic_mem[3] = a2; 106 | magic_mem[4] = a3; 107 | magic_mem[5] = a4; 108 | magic_mem[6] = a5; 109 | magic_mem[7] = a6; 110 | 111 | htif_syscall((uintptr_t)magic_mem); 112 | 113 | long ret = magic_mem[0]; 114 | 115 | spinlock_unlock(&lock); 116 | return ret; 117 | } 118 | 119 | ``` 120 | 121 | 122 | ## htif_syscall(uint64 arg) 123 | 124 | ```cpp 125 | 126 | void htif_syscall(uint64 arg) { 127 | do_tohost_fromhost(0, 0, arg); 128 | } 129 | 130 | ``` 131 | 132 | 133 | ## do_tohost_fromhost(uint64 dev, uint64 cmd, uint64 data) 134 | - 在这个函数中,通过轮询的方式等待spike仿真器完成真正的系统调用,并传回结果。 135 | ```cpp 136 | static void do_tohost_fromhost(uint64 dev, uint64 cmd, uint64 data) { 137 | spinlock_lock(&htif_lock); 138 | __set_tohost(dev, cmd, data); 139 | 140 | while (1) { 141 | uint64_t fh = fromhost; 142 | if (fh) { 143 | if (FROMHOST_DEV(fh) == dev && FROMHOST_CMD(fh) == cmd) { 144 | fromhost = 0; 145 | break; 146 | } 147 | __check_fromhost(); 148 | } 149 | } 150 | spinlock_unlock(&htif_lock); 151 | } 152 | ``` 153 | 154 | ## 155 | ```cpp 156 | 157 | ``` -------------------------------------------------------------------------------- /code/软中断入口程序.md: -------------------------------------------------------------------------------- 1 | # 软中断入口程序 2 | ## 概述 3 | 4 | 1. **smode_trap_vector** 5 | 软中断向量处理程序,负责将当前进程的上下文保存到 trapframe,并跳转到 `smode_trap_handler` 处理具体的异常。 6 | 7 | 2. **smode_trap_handler** 8 | 进入 S 模式的异常处理程序,确保当前处于用户模式,保存当前进程的计数器,处理系统调用(`ecall`),并根据异常原因跳转到相应的处理程序。 9 | 10 | 3. **handle_syscall** 11 | 处理系统调用(`ecall`)的函数,调整程序计数器(`epc`),并调用 `do_syscall` 执行相应的内核操作。 12 | 13 | 4. **do_syscall** 14 | 系统调用处理程序,根据传入的系统调用号 `a0` 调用相应的内核函数(如 `sys_user_print` 或 `sys_user_exit`)。 15 | 16 | 该过程展示了从软中断触发到执行系统调用的完整流程,确保用户程序与内核的有效交互,并通过上下文保存和跳转到合适的处理函数来处理中断。 17 | ## smode_trap_vector 18 | ```asm 19 | smode_trap_vector: 20 | # swap a0 and sscratch, so that points a0 to the trapframe of current process 21 | csrrw a0, sscratch, a0 22 | 23 | # save the context (user registers) of current process in its trapframe. 24 | addi t6, a0 , 0 25 | 26 | # store_all_registers is a macro defined in util/load_store.S, it stores contents 27 | # of all general purpose registers into a piece of memory started from [t6]. 28 | store_all_registers 29 | 30 | # come back to save a0 register before entering trap handling in trapframe 31 | # [t0]=[sscratch] 32 | csrr t0, sscratch 33 | sd t0, 72(a0) 34 | 35 | # use the "user kernel" stack (whose pointer stored in p->trapframe->kernel_sp) 36 | ld sp, 248(a0) 37 | 38 | # load the address of smode_trap_handler() from p->trapframe->kernel_trap 39 | ld t0, 256(a0) 40 | 41 | # jump to smode_trap_handler() that is defined in kernel/trap.c 42 | jr t0 43 | ``` 44 | 45 | 46 | ## void smode_trap_handler(void) 47 | ```cpp 48 | // 49 | // kernel/smode_trap.S will pass control to smode_trap_handler, when a trap happens 50 | // in S-mode. 51 | // 52 | void smode_trap_handler(void) { 53 | // make sure we are in User mode before entering the trap handling. 54 | // we will consider other previous case in lab1_3 (interrupt). 55 | if ((read_csr(sstatus) & SSTATUS_SPP) != 0) panic("usertrap: not from user mode"); 56 | 57 | assert(current); 58 | // save user process counter. 59 | current->trapframe->epc = read_csr(sepc); 60 | 61 | // if the cause of trap is syscall from user application. 62 | // read_csr() and CAUSE_USER_ECALL are macros defined in kernel/riscv.h 63 | if (read_csr(scause) == CAUSE_USER_ECALL) { 64 | handle_syscall(current->trapframe); 65 | } else { 66 | sprint("smode_trap_handler(): unexpected scause %p\n", read_csr(scause)); 67 | sprint(" sepc=%p stval=%p\n", read_csr(sepc), read_csr(stval)); 68 | panic( "unexpected exception happened.\n" ); 69 | } 70 | 71 | // continue (come back to) the execution of current process. 72 | switch_to(current); 73 | } 74 | ``` 75 | 76 | 77 | ### static void handle_syscall(trapframe *tf) 78 | ```cpp 79 | // 80 | // handling the syscalls. will call do_syscall() defined in kernel/syscall.c 81 | // 82 | static void handle_syscall(trapframe *tf) { 83 | // tf->epc points to the address that our computer will jump to after the trap handling. 84 | // for a syscall, we should return to the NEXT instruction after its handling. 85 | // in RV64G, each instruction occupies exactly 32 bits (i.e., 4 Bytes) 86 | tf->epc += 4; 87 | 88 | // TODO (lab1_1): remove the panic call below, and call do_syscall (defined in 89 | // kernel/syscall.c) to conduct real operations of the kernel side for a syscall. 90 | // IMPORTANT: return value should be returned to user app, or else, you will encounter 91 | // problems in later experiments! 92 | panic( "call do_syscall to accomplish the syscall and lab1_1 here.\n" ); 93 | 94 | } 95 | 96 | ``` 97 | 98 | 99 | ### long do_syscall(long a0, long a1,......) 100 | - 这是软中断入口程序的结束。 101 | ```cpp 102 | // 103 | // [a0]: the syscall number; [a1] ... [a7]: arguments to the syscalls. 104 | // returns the code of success, (e.g., 0 means success, fail for otherwise) 105 | // 106 | long do_syscall(long a0, long a1, long a2, long a3, long a4, long a5, long a6, long a7) { 107 | switch (a0) { 108 | // 根据不同的中断号a0, 进入不同的系统调用过程 109 | case SYS_user_print: 110 | return sys_user_print((const char*)a1, a2); 111 | case SYS_user_exit: 112 | return sys_user_exit(a1); 113 | default: 114 | panic("Unknown syscall %ld \n", a0); 115 | } 116 | } 117 | 118 | ``` 119 | -------------------------------------------------------------------------------- /code/软中断用户接口.md: -------------------------------------------------------------------------------- 1 | 待写 -------------------------------------------------------------------------------- /doc/CPU如何同步自然时间.md: -------------------------------------------------------------------------------- 1 | # CPU 如何同步自然时间 2 | 3 | 计算机中的定时器通常基于 CPU 时钟周期递增,而不是直接与现实时间同步。那么,计算机是如何实现准确的时间计量呢?这个问题涉及到时钟源和时钟同步的概念。尽管 CPU 的定时器是基于时钟周期运行的,但计算机可以通过多种方式将系统时间与现实世界的时间进行同步,从而实现准确的时间计量。 4 | 5 | ## 1. 外部时钟源(如实时时钟 RTC) 6 | 7 | 为了确保计算机能够准确跟踪现实世界的时间,通常会使用外部硬件时钟(如 RTC,实时时钟)来提供参考时间。这些外部时钟并不依赖于 CPU 时钟周期,而是依靠独立的硬件时钟模块来生成精确的时间信号。 8 | 9 | - **RTC(Real-Time Clock)**:实时时钟是一种低功耗的独立时钟模块,它通常运行在与 CPU 时钟无关的频率下。RTC 使用晶振(如 32.768 kHz)提供稳定的时钟信号,直接计时秒、分、小时等实际时间,并且通常具有电池供电功能,确保即使在断电的情况下也能保持时间。 10 | 11 | 通过读取 RTC 的值,计算机系统可以获取实际的时间信息。这些时间信息可以用来同步内部的定时器和其他系统组件,从而实现精确的现实时间计量。 12 | 13 | ## 2. CPU 时钟与定时器的配合 14 | 15 | 尽管 CLINT_MTIME 寄存器是通过 CPU 时钟周期递增的,操作系统通常会将其与现实世界的时间进行转换。实现这一点的方法之一是通过与外部时钟源(如 RTC)或系统启动时的时间戳同步。 16 | 17 | 例如,操作系统可以在启动时从 RTC 获取当前的实际时间,并根据此基准时间与 CPU 时钟周期的计时进行映射。此后,系统会定期调整内部分配给定时器的时间,确保它们与实际时间保持同步。 18 | 19 | - **时间同步过程**:系统会定期从 RTC 获取时间,并根据 CPU 时钟的递增,计算出与现实时间之间的差异。通过这个差异,操作系统可以调整定时器的计时机制,以确保它与实际时间一致。 20 | 21 | ## 3. NTP(网络时间协议) 22 | 23 | 如果计算机连接到网络,它还可以使用 NTP(Network Time Protocol)来同步时间。NTP 是一种通过互联网同步计算机时钟的协议。它通过向远程的时间服务器请求当前准确的时间,从而保持计算机系统的时间与全球标准时间(UTC)同步。 24 | 25 | - **NTP 协议**:NTP 使用一种分层的架构来同步时钟。它的工作原理是通过从高精度的时间服务器获取时间,并将这个时间传递给客户端系统。NTP 会补偿网络延迟和时钟偏差,从而实现高精度的时间同步。 26 | 27 | ## 4. 系统启动时间与内部时钟同步 28 | 29 | 在没有外部时钟源或 NTP 服务的情况下,计算机可以依赖系统启动时的时间戳和定时器递增来估算当前时间。在系统启动时,操作系统会记录一个初始时间(可能来自硬件时钟或其他源),并根据系统运行期间的定时器递增来估算当前的实际时间。 30 | 31 | - **内核与时间同步**:操作系统内核会定期从内部时钟获取时间戳,然后将其与启动时的参考时间进行比较,并根据定时器的递增来推算出当前时间。 32 | 33 | ## 5. 高精度计时 34 | 35 | 对于某些精确要求较高的应用(如金融系统、科学实验等),可以通过高精度计时器来进一步提高时间同步的精度。例如,一些高端服务器或硬件平台可能会使用 PTP(Precision Time Protocol),这是一种比 NTP 更精确的协议,用于在网络中同步时间,尤其是在需要纳秒级别精度的环境中。 36 | 37 | ## 总结 38 | 39 | 虽然计算机内部的定时器是基于 CPU 时钟周期递增的,但为了实现与现实时间的准确计量,计算机依赖于以下几个方面的结合: 40 | 41 | 1. **外部时钟源**(如 RTC)提供一个稳定、独立于 CPU 时钟的时间基准。 42 | 2. **NTP** 或其他时间同步协议用于在网络中保持系统与标准时间(如 UTC)同步。 43 | 3. 操作系统的**时间管理机制**,它通过读取外部时钟或启动时的参考时间来同步定时器,确保定时器的值能够与实际时间一致。 44 | 45 | 通过这些方式,计算机能够实现精准的时间计量,并确保系统任务和调度能够与现实世界的时间保持一致。 46 | -------------------------------------------------------------------------------- /doc/C语言嵌入汇编概述.md: -------------------------------------------------------------------------------- 1 | # C语言嵌入汇编语法 2 | 3 | ## 1. 内嵌汇编的一般格式 4 | GCC 的内嵌汇编一般写成如下形式: 5 | 6 | ```asm 7 | asm volatile ( 8 | "汇编指令模板\n\t" 9 | : 输出操作数列表 /* 第一个冒号后的部分 */ 10 | : 输入操作数列表 /* 第二个冒号后的部分 */ 11 | : clobber 列表 /* 第三个冒号后的部分 */ 12 | ); 13 | ``` 14 | 15 | - **汇编指令模板**:可以是一条或多条汇编指令构成的字符串。在这个字符串中,可以使用占位符(如 `%0`、`%1` 等)来引用后面操作数列表中提供的变量。 16 | - **输出操作数列表**:列出汇编代码将写入或修改的 C 变量。每个操作数都有一个约束,描述该变量如何与汇编代码关联。 17 | - **输入操作数列表**:列出汇编代码需要读取的 C 变量,不会被修改。 18 | - **Clobber 列表**:告诉编译器哪些寄存器或状态在汇编代码执行过程中可能会被修改,从而避免优化错误。 19 | 20 | ## 2. 多个内存操作数的处理 21 | 22 | ### (1)输出操作数 23 | 如果有多个输出操作数,可以按照逗号分隔列出。例如: 24 | 25 | ```asm 26 | asm volatile( 27 | "指令1\n\t" 28 | "指令2\n\t" 29 | "指令3\n\t" 30 | : "=m"(out1), "=m"(out2) // 两个输出,分别绑定到变量 out1 和 out2 31 | : /* 输入操作数 */ 32 | : /* clobber 列表 */ 33 | ); 34 | ``` 35 | 36 | 这里: 37 | - `=m` 表示输出到内存变量,`=` 表示这是写入(输出)的操作数。 38 | - 输出操作数按照从左到右的顺序自动编号,`%0` 指代 `out1`,`%1` 指代 `out2`。 39 | 40 | ### (2)输入操作数 41 | 同理,如果有多个输入操作数,也可以列出多个,如下: 42 | 43 | ```asm 44 | asm volatile( 45 | "指令1\n\t" 46 | "指令2\n\t" 47 | : /* 输出操作数 */ 48 | : "m"(in1), "m"(in2), "m"(in3) // 三个内存输入,分别绑定到变量 in1, in2, in3 49 | : /* clobber 列表 */ 50 | ); 51 | ``` 52 | 53 | - 每个 `"m"` 约束表示对应操作数存放在内存中。 54 | - 如果同时存在输出和输入操作数,则输出操作数编号先用,接着输入操作数编号。比如: 55 | - 输出操作数:第一个为 `%0`,第二个为 `%1`。 56 | - 输入操作数:第一个为 `%2`,第二个为 `%3`,依此类推。 57 | 58 | ### (3)结合多个输入和输出操作数 59 | 例如下面这个例子同时含有两个输出和两个输入操作数: 60 | 61 | ```cpp 62 | int out1, out2; 63 | int in1, in2; 64 | 65 | asm volatile( 66 | "movl %2, %%eax\n\t" // 将第一个输入 (in1) 移动到 eax 寄存器 67 | "addl %3, %%eax\n\t" // 将第二个输入 (in2) 加到 eax 寄存器中 68 | "movl %%eax, %0\n\t" // 将 eax 中的结果存入第一个输出 (out1) 69 | "movl %%eax, %1\n\t" // 同时也存入第二个输出 (out2) 70 | : "=m" (out1), "=m" (out2) // 输出操作数:编号 %0 和 %1 71 | : "m" (in1), "m" (in2) // 输入操作数:编号 %2 和 %3 72 | : "eax", "memory" // clobber:声明 eax 寄存器和内存可能被修改 73 | ); 74 | ``` 75 | 76 | 这里: 77 | - 在汇编模板中用 `%2` 引用了 `in1`,`%3` 引用了 `in2`,而 `%0` 和 `%1` 分别对应 `out1` 和 `out2`。 78 | - 输出和输入编号是连续的,先输出后输入。 79 | 80 | ### (4)使用符号名称(Named Operands) 81 | 为了提高代码可读性,可以使用命名操作数,形式如下: 82 | 83 | ```cpp 84 | int result; 85 | int operand1, operand2; 86 | 87 | asm volatile( 88 | "movl %[op1], %%eax\n\t" 89 | "addl %[op2], %%eax\n\t" 90 | "movl %%eax, %[res]\n\t" 91 | : [res] "=m" (result) // 用 [res] 标记输出操作数 92 | : [op1] "m" (operand1), [op2] "m" (operand2) // 用 [op1] 和 [op2] 标记输入操作数 93 | : "eax", "memory" 94 | ); 95 | ``` 96 | 97 | 在模板中,`%[op1]` 表示使用标记为 `op1` 的操作数。这样能让汇编代码更直观,便于维护。 98 | 99 | ## 3. 总结与注意事项 100 | 101 | 1. **操作数编号**: 102 | - 如果只存在输出操作数,那么编号从 `%0` 开始依次增加。 103 | - 如果有输出和输入操作数,则输出操作数编号先用,输入操作数依次跟在后面。 104 | 105 | 2. **约束字符串**: 106 | - `"m"` 用于表示内存操作数; 107 | - `"=m"` 表示这是一个输出操作数,写入内存。 108 | 109 | 3. **命名操作数**: 110 | - 通过 `[name]` 可以为操作数取一个名字,然后在模板中使用 `%[name]` 进行引用,这样比纯数字编号更清晰。 111 | 112 | 4. **clobber 列表**: 113 | - 必须声明所有在汇编代码中被修改的寄存器或状态(例如 `"eax"`, `"memory"`),否则编译器可能在优化时出现问题。 114 | 115 | 5. **volatile 关键字**: 116 | - 如果不希望编译器对内嵌汇编进行优化(例如删除或重新排序),可以加上 `volatile` 关键字。 117 | 118 | 通过以上介绍,可以看到,当有多个内存输入和输出操作数时,只需要将每个操作数用逗号分隔地列在相应的输出或输入部分,并注意操作数在汇编模板中的引用顺序。希望这个通用指南能帮助你更好地理解和使用 GCC 的内嵌汇编语法! 119 | -------------------------------------------------------------------------------- /doc/ELF文件头解析.md: -------------------------------------------------------------------------------- 1 | ## ELF文件头解析文档 2 | 3 | ELF(Executable and Linkable Format)是一种常见的文件格式,广泛用于Linux、Unix等系统中,尤其用于可执行文件、目标文件以及共享库。ELF文件包含两部分重要内容:ELF文件头和它的段(Program Headers)以及节(Section Headers)。本文将详细介绍ELF文件头中每一项的功能。 4 | 5 | ### ELF Header 示例 6 | 7 | ``` 8 | Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 9 | Class: ELF64 10 | Data: 2's complement, little endian 11 | Version: 1 (current) 12 | OS/ABI: UNIX - System V 13 | ABI Version: 0 14 | Type: EXEC (Executable file) 15 | Machine: RISC-V 16 | Version: 0x1 17 | Entry point address: 0x800007ce 18 | Start of program headers: 64 (bytes into file) 19 | Start of section headers: 90688 (bytes into file) 20 | Flags: 0x5, RVC, double-float ABI 21 | Size of this header: 64 (bytes) 22 | Size of program headers: 56 (bytes) 23 | Number of program headers: 4 24 | Size of section headers: 64 (bytes) 25 | Number of section headers: 19 26 | Section header string table index: 18 27 | ``` 28 | 29 | ### 各项字段解析 30 | 31 | 1. **Magic: `7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00`** 32 | 33 | - **Magic number**是ELF文件的标识符。前四个字节`7f 45 4c 46`代表ASCII字符`0x7f 'E' 'L' 'F'`,这是ELF文件的标准标志。接下来的字节指定了文件的架构和数据格式等信息。 34 | 35 | - 具体解释: 36 | - `0x7f`:文件标识符的第一个字节。 37 | - `0x45`:'E'。 38 | - `0x4c`:'L'。 39 | - `0x46`:'F'。 40 | - `02`:文件类型(ELF64,表示64位)。 41 | - `01`:数据编码(小端模式)。 42 | - `01`:文件版本。 43 | 44 | 2. **Class: `ELF64`** 45 | 46 | - **Class**字段表示文件的位数,`ELF64`意味着该文件是64位的。如果是32位文件,则该字段为`ELF32`。 47 | 48 | 3. **Data: `2's complement, little endian`** 49 | 50 | - **Data encoding**指定数据的字节序。`little endian`表示低字节在前,高字节在后(小端存储方式)。`2's complement`表示使用补码表示负数。 51 | 52 | 4. **Version: `1 (current)`** 53 | 54 | - **Version**字段指定文件版本号。当前版本为`1`。 55 | 56 | 5. **OS/ABI: `UNIX - System V`** 57 | 58 | - **OS/ABI**字段指定了ELF文件的目标操作系统和应用程序二进制接口(ABI)。`UNIX - System V`表示该ELF文件是为System V风格的Unix系统设计的。 59 | 60 | 6. **ABI Version: `0`** 61 | 62 | - **ABI Version**表示与OS/ABI相关的版本号。该字段通常为0,表示当前版本。 63 | 64 | 7. **Type: `EXEC (Executable file)`** 65 | 66 | - **Type**字段表示文件类型。`EXEC`表示这是一个可执行文件。其他类型可能包括`DYN`(共享库文件)、`REL`(重定位文件)等。 67 | 68 | 8. **Machine: `RISC-V`** 69 | 70 | - **Machine**字段表示该ELF文件为哪种架构生成的。在这个例子中,`RISC-V`表示它是为RISC-V架构编译的。 71 | 72 | 9. **Version: `0x1`** 73 | 74 | - **Version**字段指定ELF文件的版本号,这里`0x1`表示该文件遵循ELF文件格式的第一个版本。 75 | 76 | 10. **Entry point address: `0x800007ce`** 77 | 78 | - **Entry point address**字段指定程序开始执行的位置,也就是程序的入口地址。在这个例子中,入口地址是`0x800007ce`,程序将从该地址开始执行。 79 | 80 | 11. **Start of program headers: `64 (bytes into file)`** 81 | 82 | - **Start of program headers**指定了程序头表(Program Header Table)在文件中的偏移量。程序头表包含了程序段的信息,如内存位置、大小等。 83 | 84 | 12. **Start of section headers: `90688 (bytes into file)`** 85 | 86 | - **Start of section headers**指定了节头表(Section Header Table)在文件中的偏移量。节头表包含了每个节的信息,如代码段、数据段、符号表等。 87 | 88 | 13. **Flags: `0x5, RVC, double-float ABI`** 89 | 90 | - **Flags**字段表示特定的文件标志信息。在这里,`0x5`表示特定的标志位,`RVC`指的是支持RISC-V的压缩指令集,`double-float ABI`指的是双精度浮点数的ABI。 91 | 92 | 14. **Size of this header: `64 (bytes)`** 93 | 94 | - **Size of this header**字段表示ELF文件头的大小。64字节是ELF64格式的标准大小。 95 | 96 | 15. **Size of program headers: `56 (bytes)`** 97 | 98 | - **Size of program headers**字段表示每个程序头的大小。每个程序头占用56字节。 99 | 100 | 16. **Number of program headers: `4`** 101 | 102 | - **Number of program headers**字段表示程序头表中包含的程序头数目。这个例子中包含4个程序头。 103 | 104 | 17. **Size of section headers: `64 (bytes)`** 105 | 106 | - **Size of section headers**字段表示每个节头的大小。每个节头占用64字节。 107 | 108 | 18. **Number of section headers: `19`** 109 | 110 | - **Number of section headers**字段表示节头表中包含的节头数目。这个例子中包含19个节头。 111 | 112 | 19. **Section header string table index: `18`** 113 | 114 | - **Section header string table index**字段表示节头表中存储节名称的字符串表的位置。值`18`表示该表在节头表中的位置。 115 | 116 | ### 总结 117 | 118 | ELF文件头包含了许多关于文件的基本信息,包括文件类型、目标架构、程序入口地址、节和段的布局等。这些信息对于加载和执行ELF文件至关重要,操作系统的加载器利用这些信息来正确地加载程序并使其运行。 -------------------------------------------------------------------------------- /doc/ELF文件如何组织调试信息.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # ELF 文件的调试信息组织 4 | 5 | ELF(Executable and Linkable Format)文件是 Unix 系统(包括 Linux)中用于可执行文件、共享库和目标文件的标准格式。调试信息是调试器用来映射源代码与生成的机器代码之间的关系的内容。在 ELF 文件中,调试信息通常是作为符号表和相关的调试节段(sections)存储的。 6 | 7 | ### ELF 文件中的调试信息 8 | 9 | 调试信息在 ELF 文件中通常存储在一些特定的节段中,主要包括以下几类: 10 | 11 | #### 1. **符号表(.symtab 和 .dynsym)** 12 | - **`.symtab`**:这是 ELF 文件的符号表,包含了程序中的所有符号(如变量、函数名等)。它提供了源代码到二进制代码的映射,是调试器最常用的调试信息之一。符号表中的每个符号都有相关的地址信息。 13 | - **`.dynsym`**:动态符号表,包含了动态链接过程中所需的符号(通常用于共享库)。它与 `.symtab` 类似,但主要用于动态链接时的符号解析。 14 | 15 | 调试器可以通过符号表找出哪些函数和变量在代码中定义以及它们的地址。 16 | 17 | #### 2. **调试信息节段(.debug_*)** 18 | 调试信息被存储在以 `.debug_` 为前缀的节段中,具体包括以下几种: 19 | 20 | - **`.debug_info`**:包含了详细的调试信息,通常包括函数的名称、参数、变量、类型信息、源代码行号等。这些信息是最详细的调试数据,对于源代码和二进制的映射至关重要。 21 | - **`.debug_abbrev`**:包含了调试符号的缩写(abbreviations),这是对 `.debug_info` 中信息的压缩形式,主要用于提高存储效率。 22 | - **`.debug_line`**:包含了行号信息,指示每个源代码行与程序执行代码之间的关系。这使得调试器能够将机器代码映射到源代码中的具体行号。 23 | - **`.debug_str`**:包含调试符号表的字符串(如函数名、变量名等)。 24 | - **`.debug_loc`**:包含调试信息中符号的位置,通常指示变量的内存地址和范围。 25 | 26 | 这些节段是调试信息的核心,它们包含了程序的源代码位置、变量的定义和类型等信息,调试器通过这些数据将二进制代码映射回源代码。 27 | 28 | #### 3. **类型信息(.debug_types)** 29 | - **`.debug_types`**:包含类型信息,如结构体、类、枚举、联合体等的定义。这些信息对于调试时查看复杂数据类型的内容非常有用。 30 | 31 | #### 4. **其它调试相关节段** 32 | - **`.note`**:某些 ELF 文件会包含一个 `.note` 节段,这里可能包含一些特定于调试的信息,尽管它通常用于存储平台和目标特定的信息(比如目标架构信息)。 33 | - **`.plt` 和 `.got`**:这些不是专门的调试节段,而是与动态链接相关的节段。在调试时,调试器可能会用它们来追踪动态链接的函数调用。 34 | 35 | ### 调试信息的格式 36 | 37 | 在 ELF 文件中,调试信息的格式通常遵循 **DWARF** 标准。DWARF 是一种专门为调试目的设计的数据格式,它被广泛用于许多编程语言和平台。DWARF 定义了如何存储变量、函数、类型和源代码行号等信息。 38 | 39 | - **DWARF 信息结构**:在 `.debug_info` 和其他节段中,调试信息以一系列的 **debugging entries** 存储,每个条目描述了符号、变量、函数等的信息。 40 | - **DWARF 表示法**:DWARF 采用一种表格格式,其中每个表格项对应一个源代码元素或符号,记录了源代码行号、符号名称、地址等信息。 41 | 42 | ### 不包含源代码文本 43 | 44 | 尽管 ELF 文件的调试信息中包含源代码的行号、符号等映射信息,但 **并不会直接存储源代码中的每一行文本内容**。具体来说,调试信息在 ELF 文件中的作用是: 45 | - **源代码行号的映射**:调试信息通过 `.debug_line` 节段来存储源代码行号与机器代码之间的映射。它指出了某一段机器代码对应源代码中的哪一行。这使得调试器能够显示在调试时程序执行到哪一行源代码。 46 | - **源代码文件的路径**:调试信息还包含了源代码文件的路径信息,这使得调试器能够知道源代码文件在哪里,以便在调试过程中打开这些文件。 47 | 48 | 调试信息中 **不存储源代码文本本身**,而是通过符号表、行号表等信息,帮助调试器将二进制代码映射回源代码的位置。源代码文本内容本身并不包含在 ELF 文件中,除非显式地将源代码嵌入到程序中(例如某些特殊的 `#include` 或调试用途),但这并不是调试信息的一部分。 49 | 50 | ### 如何生成和使用调试信息 51 | 52 | 调试信息通常在编译时生成。比如使用 `gcc` 编译时,可以通过添加 `-g` 选项来生成调试信息: 53 | 54 | ```bash 55 | gcc -g -o my_program my_program.c 56 | ``` 57 | 58 | 这将会在生成的 ELF 文件中包含调试信息。编译后的 ELF 文件会包含 `.debug_*` 节段,调试器(如 GDB)会利用这些信息来帮助开发者调试程序。 59 | 60 | ### 如何查看调试信息 61 | 62 | 你可以使用 `objdump` 或 `readelf` 等工具来查看 ELF 文件中的调试信息。例如,使用 `readelf` 查看调试信息: 63 | 64 | ```bash 65 | readelf --debug-dump=info my_program 66 | ``` 67 | 68 | 这会显示 `.debug_info` 节段的内容,其中包括与源代码行号、符号等相关的信息。 69 | -------------------------------------------------------------------------------- /doc/ELF文件概述.md: -------------------------------------------------------------------------------- 1 | # ELF文件概述 2 | 3 | ## 1. 文件读取与格式验证 4 | - **读取 ELF 文件头** 5 | 当操作系统收到加载请求时,首先从磁盘中读取 ELF 文件头,以验证文件是否为 ELF 格式,并获取文件的基本信息(如文件类型、目标架构、程序入口点等)。ELF 头部包含了程序头表和节区表的偏移和大小信息,为后续步骤指明方向。 6 | 7 | ## 2. 解析程序头表与加载段 8 | - **读取程序头表** 9 | 程序头表描述了文件中各个段的位置、大小和属性,包含段类型、虚拟地址、文件偏移、大小及对齐要求等信息。 10 | - **加载段到内存** 11 | 对于每个标记为 PT_LOAD 的段,操作系统会将该段从文件映射到进程的虚拟地址空间中,并设置适当的访问权限。 12 | 13 | ## 3. 动态链接(如果适用) 14 | - **动态链接的 ELF 文件** 15 | 对于依赖共享库的 ELF 文件,操作系统会加载动态链接器并执行重定位与符号解析,确保所有函数调用和数据引用指向正确的内存位置。 16 | 17 | ## 4. 环境设置与入口点准备 18 | - **初始化运行环境** 19 | 为新进程建立干净的运行环境,包括设置堆栈、初始化资源(如文件描述符、信号处理等)。 20 | - **确定入口点** 21 | 操作系统将程序计数器(PC)设置为 ELF 头部指定的程序入口点地址,准备启动程序。 22 | 23 | ## 5. 开始执行 24 | - **控制权转交** 25 | 操作系统将 CPU 控制权交给新加载的程序,从入口点开始执行第一条指令,程序开始运行。 26 | 27 | --- 28 | 29 | ## 总结 30 | ELF 文件加载和执行过程如同一场精密协调的交响乐: 31 | 1. **起始序曲**:文件头解析,确定格式与基本信息。 32 | 2. **主旋律**:程序头表指引下的各个段加载到内存中。 33 | 3. **和声处理**:动态链接阶段,完成依赖和重定位工作,确保各部分协同工作。 34 | 4. **大合奏**:环境设置与入口点确定,CPU 接管控制,程序开始执行。 35 | 36 | 每一步都充满了技术严谨与艺术韵律,这种精细有序的安排确保了计算机高效、安全地执行程序。 37 | -------------------------------------------------------------------------------- /doc/ELF程序段头解析.md: -------------------------------------------------------------------------------- 1 | 下面是为`elf_prog_header_t`数据结构添加的中文注释,详细说明每个字段的含义及其与ELF程序段头的对应关系: 2 | 3 | ```c 4 | // ELF程序段头结构体 5 | typedef struct elf_prog_header_t { 6 | uint32 type; /* 段类型(如代码段、数据段等) */ 7 | uint32 flags; /* 段的标志位(表示该段的特性,例如可读、可写、可执行) */ 8 | uint64 off; /* 段在文件中的偏移量(表示该段在ELF文件中的位置) */ 9 | uint64 vaddr; /* 段的虚拟地址(程序加载到内存中的位置) */ 10 | uint64 paddr; /* 段的物理地址(通常与vaddr相同,但对于某些硬件可能不同) */ 11 | uint64 filesz; /* 段在文件中的大小(文件中的字节数) */ 12 | uint64 memsz; /* 段在内存中的大小(内存中的字节数) */ 13 | uint64 align; /* 段的对齐要求(通常是2的幂,用于内存对齐) */ 14 | } elf_prog_header; 15 | ``` 16 | 17 | ### 字段说明: 18 | 19 | 1. **type**: 20 | - 这是一个32位的字段,表示段的类型。常见的段类型包括: 21 | - `0x1`:加载段(LOAD) 22 | - `0x2`:动态链接信息(DYNAMIC) 23 | - `0x3`:程序头表(PROGRAM头) 24 | - `0x4`:注释段(NOTE) 25 | - `0x6`:堆栈段(STACK) 26 | - `0x7`:共享库段(SHLIB) 27 | 28 | 2. **flags**: 29 | - 这是一个32位的字段,表示段的标志位,描述该段的特性。常见的标志包括: 30 | - `0x1`:可读(R) 31 | - `0x2`:可写(W) 32 | - `0x4`:可执行(X) 33 | - 这些标志指示该段是否可被程序读取、修改或执行。 34 | 35 | 3. **off**: 36 | - 这是一个64位的字段,表示该段在文件中的偏移量,即该段在ELF文件中的起始位置。加载器会根据这个偏移量从文件中加载段内容到内存中。 37 | 38 | 4. **vaddr**: 39 | - 这是一个64位的字段,表示该段在内存中的虚拟地址。加载器将段内容加载到这个虚拟地址指定的位置。这个地址是程序在运行时使用的地址。 40 | 41 | 5. **paddr**: 42 | - 这是一个64位的字段,表示该段的物理地址。在大多数情况下,虚拟地址(`vaddr`)与物理地址(`paddr`)是相同的,但在某些硬件平台上,它们可能会不同。物理地址是在硬件中实际的存储位置。 43 | 44 | 6. **filesz**: 45 | - 这是一个64位的字段,表示该段在文件中的大小,即该段在ELF文件中的字节数。这个值通常与`memsz`不同,因为某些段可能会在文件中有填充数据。 46 | 47 | 7. **memsz**: 48 | - 这是一个64位的字段,表示该段在内存中的大小。与`filesz`不同,`memsz`表示的是程序在内存中所需要的实际空间大小。内存中的大小可能大于文件中的大小,原因可能是段在内存中有额外的未初始化部分或填充区域。 49 | 50 | 8. **align**: 51 | - 这是一个64位的字段,表示段的对齐要求。通常是2的幂,用于确保该段在内存中按特定边界对齐。对齐要求有助于提高内存访问的效率。常见的对齐值有: 52 | - `0x1`:无需对齐 53 | - `0x1000`:要求页对齐(通常为4KB对齐) 54 | 55 | ### 总结 56 | 57 | 该数据结构表示ELF程序的各个段头(Program Header),它包含了程序段的加载信息,如段的类型、大小、位置、标志以及对齐要求。ELF文件通常由多个程序段组成,每个段描述了一个特定的内存区域,这些段可能包括代码、数据、堆栈等。通过解析这些段头信息,程序加载器能够正确地将每个段加载到内存中的适当位置并设置相关的执行属性。 -------------------------------------------------------------------------------- /doc/File_System_Overview.md: -------------------------------------------------------------------------------- 1 | # File System Implementation in RISC-V Proxy Kernel for Education (PKE) 2 | 3 | The RISC-V PKE implements a practical file system architecture that provides essential file operations while maintaining the "just-enough" philosophy of a proxy kernel. This implementation includes several key components: 4 | 5 | ## File System Architecture 6 | 7 | The PKE file system is structured with a layered design: 8 | 9 | 1. **Virtual File System (VFS)** - An abstraction layer that provides a uniform interface for different file system implementations 10 | 2. **File System Implementations**: 11 | - **Host File System (HOSTFS)** - Allows access to files on the host system 12 | - **RAM Disk File System (RFS)** - An in-memory file system for temporary storage 13 | 14 | ## Key Components 15 | 16 | ### VFS Layer (`kernel/vfs.h/c`) 17 | 18 | The VFS layer implements: 19 | - Abstract objects like `dentry` (directory entry), `vinode` (virtual inode), and `file` 20 | - File operation interfaces: `open`, `read`, `write`, `lseek`, `close` 21 | - Directory operations: `opendir`, `readdir`, `mkdir`, `closedir` 22 | - Hard link operations: `link`, `unlink` 23 | 24 | ### RFS Implementation (`kernel/rfs.h/c`) 25 | 26 | RFS is a simple file system that: 27 | - Uses a portion of RAM as disk space 28 | - Follows a structure similar to traditional UNIX file systems with superblocks, inodes, and data blocks 29 | - Has a layout with: superblock (1 block), disk inodes (10 blocks), bitmap (1 block), and data blocks (100 blocks) 30 | 31 | ### HOSTFS Implementation (`kernel/hostfs.h/c`) 32 | 33 | HOSTFS provides: 34 | - Access to the host file system through the Spike emulator interface 35 | - Translation between PKE file operations and host system calls 36 | 37 | ### Device Management (`kernel/ramdev.h/c`) 38 | 39 | The device layer handles: 40 | - RAM disk device initialization and management 41 | - Block-level read/write operations 42 | 43 | ### User Interface (`kernel/proc_file.h/c`) 44 | 45 | This component: 46 | - Connects system calls to the VFS layer 47 | - Manages per-process file descriptors and working directories 48 | 49 | ## File System Operations 50 | 51 | The PKE file system supports: 52 | - Basic operations: open, read, write, seek, close 53 | - Directory management: opendir, readdir, mkdir, closedir 54 | - Hard linking capabilities: link, unlink 55 | - File information retrieval: stat, disk_stat 56 | 57 | ## Hard Link Implementation 58 | 59 | The hard link functionality demonstrates the UNIX-like design: 60 | - Links share the same inode 61 | - Each link increments the link count in the inode 62 | - When a link is removed, the link count decreases 63 | - The file data is freed only when the link count reaches zero 64 | 65 | This design provides a practical educational example of how file systems work in real operating systems while maintaining the simplicity needed for educational purposes. -------------------------------------------------------------------------------- /doc/Linux中的硬链接.md: -------------------------------------------------------------------------------- 1 | 在 Linux 的 VFS(Virtual File System)中,**硬链接的创建通过增加指向同一 inode 的目录项实现**,其核心流程涉及 VFS 抽象层与具体文件系统的协作。以下是详细实现机制: 2 | 3 | --- 4 | 5 | ### 一、硬链接的本质 6 | 1. **共享 inode** 7 | 硬链接是文件系统中多个目录项(dentry)指向**同一个 inode**,通过 `inode->i_nlink` 统计引用数。 8 | 2. **限制条件** 9 | - **不能跨文件系统**:inode 编号仅在同一个文件系统内有效。 10 | - **目录硬链接受限**:避免环路(仅超级用户可通过 `mknod` 创建)。 11 | 12 | --- 13 | 14 | ### 二、VFS 层实现流程 15 | #### 1. 系统调用入口 16 | 用户态通过 `link()` 或 `linkat()` 触发,内核进入 `do_linkat()` 函数: 17 | ```c 18 | SYSCALL_DEFINE5(linkat, ...) { 19 | return do_linkat(olddfd, oldname, newdfd, newname, flags); 20 | } 21 | ``` 22 | 23 | #### 2. VFS 通用检查 24 | - **路径解析**:通过 `user_path_at()` 解析源文件路径,获取 `struct path`。 25 | - **合法性校验**: 26 | - 源文件存在且非目录(除非特权用户)。 27 | - 目标文件不存在。 28 | - 源与目标在同一文件系统(防止跨设备链接)。 29 | 30 | #### 3. 调用具体文件系统操作 31 | 通过 `vfs_link()` 调用具体文件系统(如 ext4)的 `link` 方法: 32 | ```c 33 | int vfs_link(struct dentry *old_dentry, struct inode *dir, 34 | struct dentry *new_dentry) { 35 | // 调用文件系统实现的 link 方法 36 | return dir->i_op->link(old_dentry, dir, new_dentry); 37 | } 38 | ``` 39 | 40 | --- 41 | 42 | ### 三、具体文件系统实现(以 ext4 为例) 43 | #### 1. 添加目录项 44 | 在目标目录中创建新目录项(dentry),指向源文件的 inode: 45 | ```c 46 | const struct inode_operations ext4_dir_inode_operations = { 47 | .link = ext4_link, // ext4 的 link 方法 48 | }; 49 | 50 | static int ext4_link(struct dentry *old_dentry, struct inode *dir, 51 | struct dentry *new_dentry) { 52 | struct inode *inode = d_inode(old_dentry); 53 | // 在目录中添加新条目 54 | err = ext4_add_entry(..., new_dentry, inode); 55 | if (!err) { 56 | inode_inc_link_count(inode); // 增加链接计数 57 | d_instantiate(new_dentry, inode); 58 | } 59 | return err; 60 | } 61 | ``` 62 | 63 | #### 2. 更新 inode 引用计数 64 | `inode_inc_link_count()` 增加 `i_nlink`,并标记 inode 为脏(需写回磁盘): 65 | ```c 66 | void inode_inc_link_count(struct inode *inode) { 67 | inode->i_nlink++; 68 | mark_inode_dirty(inode); 69 | } 70 | ``` 71 | 72 | #### 3. 日志与磁盘同步 73 | 对于日志型文件系统(如 ext4),通过事务确保原子性: 74 | ```c 75 | handle_t *handle = ext4_journal_start(dir, EXT4_HT_DIR, credits); 76 | // 在事务中执行添加目录项和更新 inode 77 | ext4_journal_stop(handle); 78 | ``` 79 | 80 | --- 81 | 82 | ### 四、关键数据结构与函数 83 | | **结构/函数** | **作用** | 84 | |-------------------------|----------------------------------------| 85 | | `struct inode_operations` | 定义文件系统 inode 操作(含 `link` 方法) | 86 | | `d_instantiate()` | 将 dentry 与 inode 关联 | 87 | | `mark_inode_dirty()` | 标记 inode 为脏,触发写回 | 88 | 89 | --- 90 | 91 | ### 五、错误处理与边界条件 92 | 1. **EXDEV 错误** 93 | 若源与目标不在同一设备,返回 `-EXDEV`(通过 `old_path.mnt != new_path.mnt` 检测)。 94 | 2. **权限检查** 95 | - 目标目录需有写权限(`inode_permission(dir, MAY_WRITE)`)。 96 | - 源文件不可被标记为不可链接(如某些特殊文件系统限制)。 97 | 3. **资源竞争** 98 | 使用 `dentry` 缓存锁(`d_lock`)防止并发修改导致状态不一致。 99 | 100 | --- 101 | 102 | ### 六、性能优化 103 | 1. **目录项缓存** 104 | 新创建的 dentry 加入缓存(`dentry_cache`),加速后续路径查找。 105 | 2. **日志批处理** 106 | 在 ext4 中,多个操作合并到单个事务,减少磁盘 I/O。 107 | 108 | --- 109 | 110 | ### 总结 111 | VFS 通过抽象层将硬链接的通用逻辑(路径解析、权限校验)与具体文件系统的实现(目录项操作、inode 管理)解耦。**核心步骤**包括: 112 | 1. 系统调用触发 VFS 层检查。 113 | 2. 调用具体文件系统的 `link` 方法添加目录项。 114 | 3. 更新 inode 引用计数并同步至磁盘。 115 | 硬链接通过共享 inode 实现多路径访问同一数据,是文件系统高效管理文件的重要机制。 -------------------------------------------------------------------------------- /doc/RISC-V函数帧解析.md: -------------------------------------------------------------------------------- 1 | # `printu` 函数栈帧解析 2 | 3 | ## 汇编源代码 4 | ```asm 5 | Dump of assembler code for function printu: 6 | => 0x0000000081000160 <+0>: addi sp,sp,-352 7 | 0x0000000081000162 <+2>: sd ra,280(sp) 8 | 0x0000000081000164 <+4>: sd s0,272(sp) 9 | 0x0000000081000166 <+6>: addi s0,sp,288 10 | 0x0000000081000168 <+8>: sd a1,8(s0) 11 | 0x000000008100016a <+10>: sd a2,16(s0) 12 | 0x000000008100016c <+12>: sd a3,24(s0) 13 | 0x000000008100016e <+14>: sd a4,32(s0) 14 | 0x0000000081000170 <+16>: sd a5,40(s0) 15 | 0x0000000081000172 <+18>: sd a6,48(s0) 16 | 0x0000000081000176 <+22>: sd a7,56(s0) 17 | 0x000000008100017a <+26>: addi a3,s0,8 18 | 0x000000008100017e <+30>: sd a3,-24(s0) 19 | 0x0000000081000182 <+34>: mv a2,a0 20 | 0x0000000081000184 <+36>: li a1,256 21 | 0x0000000081000188 <+40>: addi a0,s0,-280 22 | 0x000000008100018c <+44>: jal 0x8100036c 23 | 0x0000000081000190 <+48>: li a5,255 24 | 0x0000000081000194 <+52>: bltu a5,a0,0x810001b8 25 | 0x0000000081000198 <+56>: mv a2,a0 26 | 0x000000008100019a <+58>: li a7,0 27 | 0x000000008100019c <+60>: li a6,0 28 | 0x000000008100019e <+62>: li a5,0 29 | 0x00000000810001a0 <+64>: li a4,0 30 | 0x00000000810001a2 <+66>: li a3,0 31 | 0x00000000810001a4 <+68>: addi a1,s0,-280 32 | 0x00000000810001a8 <+72>: li a0,64 33 | 0x00000000810001ac <+76>: jal 0x81000144 34 | 0x00000000810001b0 <+80>: ld ra,280(sp) 35 | 0x00000000810001b2 <+82>: ld s0,272(sp) 36 | 0x00000000810001b4 <+84>: addi sp,sp,352 37 | 0x00000000810001b6 <+86>: ret 38 | 0x00000000810001b8 <+88>: li a2,256 39 | 0x00000000810001bc <+92>: j 0x8100019a 40 | ``` 41 | 42 | ## C源代码 43 | ```c 44 | // 45 | // printu() supports user/lab1_1_helloworld.c 46 | // 47 | int printu(const char* s, ...) { 48 | va_list vl; 49 | va_start(vl, s); 50 | 51 | char out[256]; // fixed buffer size. 52 | int res = vsnprintf(out, sizeof(out), s, vl); 53 | va_end(vl); 54 | 55 | 56 | const char* buf = out; 57 | size_t n = res < sizeof(out) ? res : sizeof(out); 58 | 59 | // make a syscall to implement the required functionality. 60 | return do_user_call(SYS_user_print, (uint64)buf, n, 0, 0, 0, 0, 0); 61 | } 62 | 63 | ``` 64 | ## 汇编帧构造过程图解 65 | ![汇编帧构造示意图](../images/汇编帧构造示意图.jpg) -------------------------------------------------------------------------------- /doc/RISC-V通用寄存器概述.md: -------------------------------------------------------------------------------- 1 | # RISC-V 寄存器体系结构概述 2 | 3 | RISC-V 体系结构采用精简而灵活的设计,定义了 32 个通用寄存器(x0~x31)以及一系列控制状态寄存器(CSR)。通用寄存器用于存放数据、传递函数参数、保存返回地址和临时计算结果,而 CSR 则管理处理器状态、异常处理、地址转换等。本文主要介绍通用寄存器的作用和典型用途。 4 | 5 | --- 6 | 7 | ## 1. 通用寄存器 8 | 9 | 在 RISC-V 中,共有 32 个通用寄存器,每个寄存器在 RV32 实现中为 32 位,在 RV64 实现中为 64 位。为了方便理解和使用,这些寄存器通常都有固定的名称和约定用途,具体如下: 10 | 11 | ### 1.1 寄存器 x0 —— zero 12 | - **名称**:zero 13 | - **功能**:始终为 0。对 x0 的任何写入都会被忽略,其值永远固定为 0。 14 | - **用途**:作为“零值”常量使用,消除不需要的运算结果。 15 | 16 | ### 1.2 寄存器 x1 —— ra (Return Address) 17 | - **名称**:ra 18 | - **功能**:保存函数调用的返回地址。 19 | - **用途**:在调用指令中自动保存返回地址,用于函数返回时恢复。 20 | 21 | ### 1.3 寄存器 x2 —— sp (Stack Pointer) 22 | - **名称**:sp 23 | - **功能**:指向当前调用栈的栈顶。 24 | - **用途**:管理局部变量、返回地址和调用现场,实现函数入栈和出栈操作。 25 | 26 | ### 1.4 寄存器 x3 —— gp (Global Pointer) 27 | - **名称**:gp 28 | - **功能**:指向全局数据区的起始地址。 29 | - **用途**:方便访问全局变量和静态数据。 30 | 31 | ### 1.5 寄存器 x4 —— tp (Thread Pointer) 32 | - **名称**:tp 33 | - **功能**:指向当前线程的线程局部存储(TLS)区域。 34 | - **用途**:实现线程安全的局部变量存取。 35 | 36 | ### 1.6 临时寄存器 —— t0 到 t2 (x5~x7) 37 | - **名称**:t0, t1, t2 38 | - **功能**:用于存放临时计算数据和中间运算结果。 39 | - **用途**:作为临时存储空间,在函数调用前后不保证这些寄存器的值。 40 | 41 | ### 1.7 保存寄存器 / 帧指针 —— s0/fp 和 s1 (x8、x9) 42 | - **名称**:s0(也作为 fp,即帧指针)、s1 43 | - **功能**:用于保存跨函数调用需要保持的值。 44 | - **用途**:s0 可用作帧指针,指向当前函数栈帧的基地址,便于访问局部变量和参数。 45 | 46 | ### 1.8 参数与返回值寄存器 —— a0 到 a7 (x10~x17) 47 | - **名称**:a0, a1, a2, a3, a4, a5, a6, a7 48 | - **功能**:用于传递函数参数以及存放函数返回值。 49 | - **用途**:在函数调用时,第 0 个和第 1 个参数通常也用于传递返回值。 50 | 51 | ### 1.9 额外保存寄存器 —— s2 到 s11 (x18~x27) 52 | - **名称**:s2, s3, s4, s5, s6, s7, s8, s9, s10, s11 53 | - **功能**:用于保存需要在函数调用之间保持不变的值。 54 | - **用途**:在函数调用过程中,如果使用这些寄存器,需保存并在返回前恢复。 55 | 56 | ### 1.10 临时寄存器 —— t3 到 t6 (x28~x31) 57 | - **名称**:t3, t4, t5, t6 58 | - **功能**:用于临时计算或存储中间结果。 59 | - **用途**:在复杂运算中作为短期数据存储。 60 | 61 | --- 62 | 63 | ## 2. 控制状态寄存器(CSR) 64 | 65 | 除了上述通用寄存器外,RISC-V 还定义了许多控制状态寄存器(CSR),这些寄存器对操作系统内核及底层硬件管理至关重要。常见的 CSR 包括: 66 | - **mstatus**:记录全局中断使能、当前特权级以及其他处理器状态信息。 67 | - **mepc**:用于保存异常发生时的返回地址。 68 | - **mtvec**:指定异常处理程序的入口地址。 69 | - **scause、sepc、stvec、sstatus**:在 Supervisor 模式下管理异常处理和系统调用。 70 | 71 | --- 72 | 73 | ## 3. 调用约定 74 | 75 | RISC-V 的调用约定规定了在函数调用过程中各个寄存器的保存和使用规则: 76 | - **调用者保存(Caller-saved)寄存器**:包括参数/返回值寄存器 a0~a7 以及临时寄存器 t0~t6。调用者需要保留这些寄存器的值时,需在调用前保存。 77 | - **被调用者保存(Callee-saved)寄存器**:包括保存寄存器 s0(/fp)到 s11。被调用函数在使用这些寄存器前需保存并在返回前恢复。 78 | 79 | --- 80 | 81 | ## 4. 总结 82 | 83 | RISC-V 寄存器体系结构简洁而功能明确,分为常数寄存器、返回地址与栈指针、全局与线程指针、临时与保存寄存器、参数与返回值传递寄存器等多个类别,同时通过控制状态寄存器(CSR)管理特权级和异常处理。正确理解并合理使用这些寄存器对高效编程、编译器优化和操作系统内核开发至关重要。 84 | -------------------------------------------------------------------------------- /doc/Spike仿真层.md: -------------------------------------------------------------------------------- 1 | Spike 仿真器通过在宿主机上实现 RISC-V 架构的 **模拟** 来提供硬件和外设资源的抽象。Spike 仿真器的设计主要是为了提供一种 **软件层面的 RISC-V 处理器模拟**,并通过与宿主机操作系统的交互,模拟各种硬件和外设行为。以下是 Spike 如何提供这些抽象的几个关键方面: 2 | 3 | ### 1. **RISC-V 处理器的模拟** 4 | Spike 仿真器模拟了 RISC-V 指令集架构(ISA),包括: 5 | - **指令执行**:Spike 完全模拟了 RISC-V 处理器的指令集,包括整数运算、浮点运算、系统调用等。 6 | - **流水线与内存模型**:Spike 通过软件模拟 RISC-V 处理器的流水线、缓存、存储器访问等硬件行为,提供了准确的指令级仿真。 7 | 8 | ### 2. **虚拟内存管理** 9 | Spike 仿真器支持 RISC-V 的 **虚拟内存系统**,包括分页机制。它通过: 10 | - **页表映射**:Spike 支持虚拟地址与物理地址的映射,仿真内存管理单元(MMU)的功能,处理页表和 `satp` 等控制寄存器。 11 | - **内存访问模拟**:它能够模拟从用户程序到内核之间的虚拟内存访问,并执行地址转换。 12 | 13 | ### 3. **I/O 外设的抽象** 14 | Spike 提供了一些外设的仿真,并允许通过 **HTIF(Host Target Interface)** 与宿主机进行交互。外设的抽象包括: 15 | - **控制台输出**:Spike 仿真器支持通过串行接口进行控制台输入输出,通常通过宿主机的终端进行显示。 16 | - **模拟中断与异常**:Spike 能够仿真 RISC-V 的中断和异常处理机制,模拟外设产生的中断信号,并将其传递到宿主机中进行处理。 17 | - **外设映射**:Spike 支持将外设寄存器映射到宿主机内存中,模拟硬件寄存器的操作,模拟一些外设的行为,如定时器、中断控制器等。 18 | 19 | ### 4. **I/O 仿真接口** 20 | - **Spike 与宿主机交互**:Spike 使用 **HTIF** 协议与宿主机进行通信。HTIF 可以实现控制信号、数据交换等功能,模拟外设和硬件与宿主机之间的交互。例如,模拟 SPI、串口通信等。 21 | - **设备模型的扩展**:Spike 仿真器支持通过 **插件式设备模型** 来扩展新的硬件抽象。这使得在仿真过程中,可以模拟更复杂的设备和外设。 22 | 23 | ### 5. **仿真配置与参数化** 24 | Spike 允许用户通过 **命令行参数** 定制硬件资源的配置,例如: 25 | - **内存大小与布局**:用户可以定义仿真中内存的大小、布局和外设映射。 26 | - **外设启用**:通过启用或禁用外设来控制 Spike 仿真器如何模拟外设的行为,如控制台、硬件中断等。 27 | 28 | ### 6. **交互式调试与性能分析** 29 | - **调试支持**:Spike 提供了基于 **GDB** 的调试接口,使得开发者可以通过 GDB 调试 RISC-V 程序,支持逐条指令的执行和中断异常的单步调试。 30 | - **性能计数器**:Spike 可以提供指令计数器和其他性能指标,帮助开发者分析程序执行时的性能瓶颈。 31 | 32 | ### 7. **与宿主机操作系统的互动** 33 | Spike 仿真器通常在宿主机操作系统上运行,它通过宿主机的 **系统调用接口** 来模拟硬件的行为,例如: 34 | - **文件操作模拟**:Spike 通过宿主机的文件系统仿真硬件操作,支持文件输入输出操作,模拟磁盘、网络等硬件设备。 35 | - **时间管理**:Spike 模拟硬件定时器、时钟中断等外设,同时宿主机系统通过调用其计时接口来实现。 36 | 37 | ### 总结 38 | Spike 仿真器通过 **精确的 RISC-V 处理器模型** 和 **与宿主机的交互** 提供了硬件资源与外设的抽象。它通过虚拟内存管理、外设映射、内存访问模拟等手段,确保可以在宿主机上仿真 RISC-V 处理器的各个方面,帮助开发者进行软件开发、调试和性能分析。同时,它也为实现更复杂的硬件和外设抽象提供了可扩展的接口和灵活的配置选项。 -------------------------------------------------------------------------------- /doc/VFS_Layer_Communication_in_PKE.md: -------------------------------------------------------------------------------- 1 | # VFS Layer Communication in PKE 2 | 3 | The Virtual File System (VFS) in the PKE kernel acts as an intermediary between user programs and concrete file system implementations (like HOSTFS and RFS). Its primary function is abstracting away the differences between file systems while efficiently passing parameters between layers. 4 | 5 | ## Parameter Communication 6 | 7 | The VFS layer uses several key mechanisms to communicate parameters: 8 | 9 | 1. **Operation Function Pointers**: The `vinode_ops` structure contains function pointers that implement specific operations. When a user request arrives at the VFS layer, it invokes the appropriate function pointer from the concrete file system, passing standardized parameters. 10 | 11 | 2. **Abstract Data Structures**: The VFS defines universal structures (`vinode`, `dentry`, `file`) that all file systems implement. These structures contain both common fields and a filesystem-specific info pointer (`i_fs_info`/`s_fs_info`) that allows each file system to store its own private data. 12 | 13 | 3. **Unified Error Passing**: Return values follow consistent conventions across layers, with negative values indicating errors and specific positive values indicating success or sizes. 14 | 15 | ## Key Characteristics 16 | 17 | 1. **Clean Layer Separation**: The VFS maintains clean separation between layers through well-defined interfaces, preventing direct dependence on implementation details of specific file systems. 18 | 19 | 2. **Object-Oriented Design**: Though implemented in C, the VFS uses an object-oriented approach where operations are associated with objects: 20 | ```c 21 | viop_read(node, buf, len, offset) // Calls the appropriate read function for the node 22 | ``` 23 | 24 | 3. **Path Resolution**: The VFS handles complex path resolution step-by-step, breaking down full paths into components and querying the appropriate file system for each directory entry. 25 | 26 | 4. **Caching Mechanisms**: The VFS implements hash tables for dentry and vinode caching, storing previously accessed entries to avoid repeated disk accesses. 27 | 28 | 5. **Reference Counting**: The VFS manages object lifetimes through reference counting, ensuring proper resource management as files are opened, closed, and shared. 29 | 30 | This design allows the VFS to efficiently dispatch operations to the correct filesystem implementation while presenting a consistent interface to user programs, regardless of which underlying filesystem actually handles the request. -------------------------------------------------------------------------------- /doc/lab1的内存管理漏洞.md: -------------------------------------------------------------------------------- 1 | # lab1的内存管理漏洞 2 | 在当前代理内核的设计中,由于没有引入内存分页机制(即虚拟内存),ELF文件的程序段(program segments)被直接加载到物理内存中。这种方式意味着,ELF文件中的虚拟地址(`vaddr`)需要与物理地址(`pa`)对应。因此,`elf_alloc_mb` 函数直接返回 ELF 程序段所需的虚拟地址,假设它能够映射到物理内存。 3 | 4 | ### 关键问题:**如何确保加载的虚拟地址不覆盖代理内核的内存区域?** 5 | 6 | 1. **物理地址与虚拟地址的关系**: 7 | - 由于目前没有分页机制,程序段的虚拟地址(`vaddr`)实际上是物理地址(`pa`)。因此,ELF文件中的程序段的虚拟地址直接对应于物理地址。 8 | - 代理内核中的程序段和数据通常占用了固定的内存区域。例如,内核的代码段、数据段、堆栈等会被映射到物理内存中的某些特定地址。 9 | 10 | 2. **避免覆盖内核程序段**: 11 | - 在当前设计中,ELF加载程序段时不会自动检测和避免覆盖内核区域。这就意味着,ELF文件的程序段需要手动保证其虚拟地址不会与内核的内存区域冲突。 12 | - 具体来说,ELF文件加载时使用的虚拟地址(`vaddr`)必须确保与内核使用的内存区域不重叠。这通常通过以下方式之一来实现: 13 | - **静态内存管理**:提前为ELF文件的程序段分配一个内存区域,确保它们不会与内核段冲突。比如,给ELF程序段指定一个固定的内存区域,从而避免与现有的内核内存区域重叠。 14 | - **内存区域检查**:加载ELF程序段前,可以检查该程序段的虚拟地址是否与内核的内存区域发生冲突。如果发生冲突,则可以选择不同的内存区域或者返回错误。 15 | 16 | ### 如何改进避免冲突? 17 | 18 | 1. **内存地址分配策略**: 19 | - 由于你目前在裸机模式(bare mode)下运行并没有虚拟内存管理机制,可以通过定义内核使用的内存区域范围,并确保ELF加载的程序段地址位于内核区域之外。例如,在`elf_load`函数中,可以增加一些检查,验证ELF段的`vaddr`是否与内核占用的内存区域冲突。 20 | 21 | 示例:假设内核的虚拟地址范围是`[0x80000000, 0x88000000)`,你可以在加载程序段前检查虚拟地址是否位于这个范围内。 22 | 23 | ```cpp 24 | if (ph_addr.vaddr >= KERNEL_BASE && ph_addr.vaddr + ph_addr.memsz <= KERNEL_END) { 25 | return EL_ERR; // 发生地址冲突 26 | } 27 | ``` 28 | 29 | 2. **固定内存区域分配**: 30 | - 在`elf_alloc_mb`函数中,你可以手动选择ELF段加载的内存区域。假设你希望ELF程序段加载到`0x90000000`及以上的内存区域,可以调整程序段的加载逻辑,确保它们不会与内核区域重叠。 31 | 32 | ```cpp 33 | // 假设ELF程序段加载地址从 0x90000000 开始 34 | void *alloc_elf_segment(elf_ctx *ctx, uint64 elf_va, uint64 size) { 35 | if (elf_va < KERNEL_END) { 36 | // 如果程序段地址在内核内存区间内,则分配失败 37 | return NULL; 38 | } 39 | // 否则,返回指定的虚拟地址 40 | return (void *)elf_va; 41 | } 42 | ``` 43 | 44 | ### 总结 45 | 46 | 在没有虚拟内存管理的情况下,ELF加载程序段时必须确保其虚拟地址不与内核的内存区域重叠。当前的 `elf_alloc_mb` 函数只是简单地返回了传入的虚拟地址,并没有考虑地址冲突的问题。为了防止覆盖代理内核的程序段,需要手动管理内存分配,确保加载的程序段的虚拟地址与内核的内存区域分离,避免发生地址重叠。可以通过为ELF段分配固定的内存区域,或者增加地址检查来避免这种情况。 -------------------------------------------------------------------------------- /doc/likely.md: -------------------------------------------------------------------------------- 1 | 在你的比赛 OS 里,可以使用 **`likely()`** 和 **`unlikely()`** 来**优化分支预测**,让编译器更好地生成分支代码,减少 CPU 分支预测失败的可能性,从而提升性能。 2 | 3 | --- 4 | 5 | ## **1. `likely()` 和 `unlikely()` 作用** 6 | 现代 CPU 具有**分支预测(Branch Prediction)**机制,当 CPU 预测错误时,会导致流水线清空(Pipeline Flush),影响性能。 7 | 8 | - **`likely(x)`** 告诉编译器:`x` **更可能** 为 `true` 9 | - **`unlikely(x)`** 告诉编译器:`x` **更可能** 为 `false` 10 | 11 | 这会影响编译器如何**排列指令**,让 CPU **执行更高效**。 12 | 13 | --- 14 | 15 | ## **2. 定义 `likely()` 和 `unlikely()`** 16 | GCC 和 Clang 提供 `__builtin_expect()` 来实现: 17 | ```c 18 | #define likely(x) __builtin_expect(!!(x), 1) 19 | #define unlikely(x) __builtin_expect(!!(x), 0) 20 | ``` 21 | - `!!(x)`: 确保 `x` 是 `0` 或 `1` 22 | - `1` 代表更可能为 `true` 23 | - `0` 代表更可能为 `false` 24 | 25 | --- 26 | 27 | ## **3. `likely()` 和 `unlikely()` 的应用** 28 | ### **(1)优化错误检查** 29 | 在内核代码中,错误检查一般**不太可能发生**,所以可以用 `unlikely()`: 30 | ```c 31 | if (unlikely(ptr == NULL)) { 32 | panic("Null pointer exception!"); 33 | } 34 | ``` 35 | > **作用**:告诉编译器 `ptr == NULL` 很少发生,让错误分支的代码放在执行路径的远端。 36 | 37 | --- 38 | 39 | ### **(2)优化系统调用** 40 | 在 `syscall` 处理中,通常 `syscall_num` 是**有效的**,所以: 41 | ```c 42 | switch (syscall_num) { 43 | case SYS_READ: 44 | return do_read(fd, buf, size); 45 | case SYS_WRITE: 46 | return do_write(fd, buf, size); 47 | default: 48 | if (unlikely(syscall_num < 0 || syscall_num > MAX_SYSCALL)) 49 | return -EINVAL; 50 | } 51 | ``` 52 | > **作用**:避免 `syscall_num` **无效** 时 CPU 误预测,减少错误分支的执行开销。 53 | 54 | --- 55 | 56 | ### **(3)优化调度器** 57 | 在进程调度时,大部分情况下不会**抢占当前进程**: 58 | ```c 59 | void schedule() { 60 | if (likely(current->time_slice > 0)) 61 | return; 62 | switch_to(next_task); 63 | } 64 | ``` 65 | > **作用**:减少 `switch_to()` 代码的执行概率,让 CPU 继续执行当前进程,提高调度效率。 66 | 67 | --- 68 | 69 | ## **4. `likely()` 和 `unlikely()` 是否总是有用?** 70 | 不一定!⚠️ 71 | - **不要滥用**,因为现代 CPU **自动分支预测**已经很强 72 | - **主要用于系统级代码**,如 **内核、syscall、调度器、错误检查** 73 | - **测试后使用**,有时候 `__builtin_expect()` 反而会降低性能(让 CPU 预测更难) 74 | 75 | --- 76 | 77 | ## **5. `likely()` & `unlikely()` VS `__assume() / __builtin_assume()`** 78 | - **`likely()/unlikely()`** 只是**优化分支预测**,不会删除代码 79 | - **`__builtin_assume()`** 让编译器**假设条件一定成立**,可能会**删除代码** 80 | - **`__assume()`(MSVC 专用)** 作用类似 81 | 82 | 如果你**100% 确定某个条件永远成立**,可以用: 83 | ```c 84 | __builtin_assume(x > 0); 85 | ``` 86 | 但一般**不建议使用**,因为可能导致**未定义行为**。 87 | 88 | --- 89 | 90 | ## **6. 什么时候使用 `likely()` 和 `unlikely()`** 91 | ✅ **适用情况** 92 | - **错误检查 (`unlikely()`)** 93 | - **罕见情况 (`unlikely()`)** 94 | - **高频路径 (`likely()`)** 95 | - **系统调用 (`likely()`)** 96 | - **进程调度 (`likely()`)** 97 | 98 | ❌ **不适用情况** 99 | - **普通 if 语句** 100 | - **数据依赖严重的代码** 101 | - **代码可读性受影响** 102 | 103 | --- 104 | 105 | ## **7. 结论** 106 | 🔹 **正确使用 `likely()` 和 `unlikely()` 可以优化分支预测,提高 OS 性能** 107 | 🔹 **主要用于错误检查、系统调用、调度器等关键路径** 108 | 🔹 **不要滥用,测试后再决定是否加入** 109 | 110 | --- 111 | 112 | **💡 你计划在哪些模块用 `likely()` / `unlikely()`?是调度、syscall 还是设备驱动?😊** -------------------------------------------------------------------------------- /doc/ramfs文件系统.md: -------------------------------------------------------------------------------- 1 | # ramfs文件系统 2 | 3 | 本实验基于**三层抽象架构**构建ramfs内存文件系统,通过面向对象设计模式实现VFS接口的标准化扩展。系统采用`VFS接口层 -> RFS逻辑层 -> BlockDevice驱动层`的分层模型,核心聚焦于**接口契约化**与**模块解耦**。 4 | 5 | --- 6 | 7 | ### **1. 层次化接口设计** 8 | ```python 9 | # 抽象层定义(接口契约) 10 | class FileSystem(ABC): 11 | @abstractmethod 12 | def mount(self, block_device): pass 13 | 14 | @abstractmethod 15 | def create_file(self, path): pass 16 | 17 | class BlockDevice(ABC): 18 | @abstractmethod 19 | def read_block(self, index): pass 20 | 21 | @abstractmethod 22 | def write_block(self, index, data): pass 23 | ``` 24 | 25 | - **VFS接口层** 26 | 继承Linux VFS标准接口(`struct file_operations`),实现`open()/read()/write()`等POSIX语义,通过**适配器模式**将系统调用转发至RFS逻辑层。 27 | 28 | - **RFS逻辑层** 29 | 实现`FileSystem`抽象类,包含: 30 | - `RFSFactory`:单例模式创建文件系统实例 31 | - `InodeOperator`:策略模式处理不同文件类型的CRUD操作 32 | - `CacheManager`:代理模式管理内存页缓存 33 | 34 | - **BlockDevice驱动层** 35 | 实现`BlockDevice`接口的`RAMDiskBlockDevice`类,提供基于内存页的`read_block()/write_block()`原子操作。 36 | 37 | --- 38 | 39 | ### **2. 核心对象建模** 40 | ```c++ 41 | // 关键数据结构类化 42 | class Superblock { 43 | private: 44 | uint32_t inode_count; 45 | uint32_t free_blocks; 46 | // 状态同步方法 47 | void sync_to_disk(BlockDevice& dev); 48 | }; 49 | 50 | class Inode { 51 | public: 52 | vector blocks; // 组合模式管理数据块 53 | time_t mtime; 54 | uint16_t permission; 55 | }; 56 | 57 | class DirectoryEntry { 58 | string name; 59 | shared_ptr inode; // 通过智能指针关联inode 60 | }; 61 | ``` 62 | 63 | - **Superblock**:封装文件系统元数据,通过观察者模式监听元数据变更事件 64 | - **Inode**:实现状态模式,根据文件类型(常规/目录)切换操作策略 65 | - **Bitmap**:采用享元模式优化内存位图存储,按需加载内存块 66 | 67 | --- 68 | 69 | ### **3. 操作逻辑实现** 70 | ```java 71 | // 以创建文件为例的交互流程 72 | public class RFSCreateOperation { 73 | void execute(VFSContext ctx) { 74 | // 1. 从VFS获取路径解析 75 | Inode parent = resolve_path(ctx.path); 76 | 77 | // 2. 分配inode和数据块 78 | Inode new_inode = InodeAllocator.new_inode(); 79 | DataBlock block = BlockAllocator.allocate(); 80 | 81 | // 3. 更新目录项 82 | parent.add_entry(new DirectoryEntry(ctx.filename, new_inode)); 83 | 84 | // 4. 同步元数据 85 | Superblock.getInstance().mark_dirty(); 86 | } 87 | } 88 | ``` 89 | 90 | - **原子操作保障**:通过门面模式封装`Superblock::sync()`、`Bitmap::flush()`等底层操作 91 | - **日志审计**:采用装饰器模式对关键操作添加事务日志记录能力 92 | - **异常处理**:定义`FileSystemException`层次化异常类,实现错误码到异常的桥接转换 93 | 94 | --- 95 | 96 | ### **4. 硬件抽象实现** 97 | ```javascript 98 | // RAMDisk驱动程序示例 99 | class RAMDiskBlockDevice extends BlockDevice { 100 | constructor(size) { 101 | this.buffer = new ArrayBuffer(size); // 内存存储池 102 | } 103 | 104 | read_block(index) { 105 | const offset = index * BLOCK_SIZE; 106 | return new DataView(this.buffer, offset, BLOCK_SIZE); 107 | } 108 | 109 | write_block(index, data) { 110 | // 直接操作内存无需IO调度 111 | new Uint8Array(this.buffer).set(data, index * BLOCK_SIZE); 112 | } 113 | } 114 | ``` 115 | - **零拷贝优化**:通过内存映射技术直接暴露缓冲区地址 116 | - **并发控制**:在代理层添加读写锁机制,确保线程安全 117 | 118 | --- 119 | 120 | ### **重构亮点总结** 121 | 1. **接口标准化**:三层之间通过抽象接口通信,RFS可无缝替换为其他文件系统 122 | 2. **模块解耦**:将存储分配、路径解析、权限校验等功能拆分为独立策略类 123 | 3. **内存效率**:采用对象池模式重用频繁创建的Inode和目录项对象 124 | 4. **可测试性**:通过MockBlockDevice实现无需硬件的单元测试 125 | 126 | 该设计可作为理解Ext4/Btrfs等复杂文件系统的教学蓝本,完整代码已通过GitHub Actions实现自动化构建验证(见项目徽章)。 -------------------------------------------------------------------------------- /doc/vinode与rfs_inode的设计模式.md: -------------------------------------------------------------------------------- 1 | 结合头文件 `rfs.h` 的实现与搜索资料,以下对 **`vinode`** 与 **`rfs_dinode`** 的设计、协作及文件系统架构进行总结: 2 | 3 | --- 4 | 5 | ### 一、分层抽象与职责划分 6 | #### 1. **内存抽象层:`vinode`** 7 | - **核心作用**:作为虚拟文件系统(VFS)的操作接口,向上提供统一的文件操作抽象(如 `rfs_read`、`rfs_write`),屏蔽具体文件系统差异。 8 | - **关键字段**(根据头文件推测): 9 | ```c 10 | struct vinode { 11 | int inum; // 磁盘inode编号(与rfs_dinode一一映射) 12 | struct super_block *sb; // 关联的超级块(用于定位文件系统) 13 | int type; // 文件类型(DIR_I/FILE_I,与rfs_dinode同步) 14 | int size; // 文件大小(需与rfs_dinode.size同步) 15 | int addrs[DIRECT_BLKNUM]; // 数据块地址(直接映射rfs_dinode.addrs) 16 | // 可能包含引用计数、锁等VFS管理字段 17 | }; 18 | ``` 19 | - **函数接口**:通过 `rfs_i_ops`(`vinode_ops`)定义操作集(如 `rfs_mkdir`、`rfs_lookup`),实现VFS与RFS的解耦。 20 | 21 | #### 2. **磁盘存储层:`rfs_dinode`** 22 | - **核心作用**:持久化存储文件元数据,是RFS文件系统的物理结构基础。 23 | - **关键字段**(头文件定义): 24 | ```c 25 | struct rfs_dinode { 26 | int size; // 文件大小(字节) 27 | int type; // 文件类型(R_FILE/R_DIR/R_FREE) 28 | int nlinks; // 硬链接数 29 | int blocks; // 占用数据块数 30 | int addrs[RFS_DIRECT_BLKNUM]; // 直接数据块地址 31 | }; 32 | ``` 33 | - **存储位置**:通过 `RFS_BLK_OFFSET_INODE` 定位磁盘上的inode表,由超级块 `rfs_superblock` 管理分配。 34 | 35 | --- 36 | 37 | ### 二、协作流程与数据同步 38 | #### 1. **内存与磁盘的映射关系** 39 | - **加载流程**(如打开文件): 40 | - 调用 `rfs_read_dinode(rdev, inum)` 读取磁盘inode到内存。 41 | - 通过 `rfs_alloc_vinode` 创建 `vinode`,填充 `inum`、`type`、`size` 等字段。 42 | - 使用 `hash_put_vinode` 缓存 `vinode` 以提升性能。 43 | - **回写流程**(如修改文件): 44 | - 更新 `vinode` 的 `size`、`addrs` 等字段。 45 | - 调用 `rfs_write_back_vinode` 将内存状态同步到磁盘的 `rfs_dinode`(通过 `rfs_write_dinode`)。 46 | 47 | #### 2. **目录操作示例**(`rfs_mkdir`) 48 | 1. **分配磁盘资源**: 49 | - 遍历inode表寻找空闲 `rfs_dinode`(标记为 `R_FREE`)。 50 | - 初始化 `rfs_dinode` 的 `type=R_DIR`、`nlinks=1`,并分配数据块(`rfs_alloc_block`)。 51 | 2. **绑定内存对象**: 52 | - 创建 `vinode` 并关联 `rfs_dinode` 的元数据。 53 | 3. **更新父目录**: 54 | - 调用 `rfs_add_direntry` 向父目录的目录块(`rfs_direntry`)添加新条目。 55 | 56 | --- 57 | 58 | ### 三、设计模式与架构特点 59 | #### 1. **适配器模式(Adapter Pattern)** 60 | - **接口适配**:VFS通过 `vinode_ops` 定义通用接口(如 `mkdir`),RFS通过 `rfs_mkdir` 实现具体逻辑,符合 **策略模式** 思想。 61 | - **数据适配**:`vinode` 作为内存代理,通过 `inum` 关联 `rfs_dinode`,实现磁盘数据的动态加载与缓存。 62 | 63 | #### 2. **分层解耦** 64 | - **VFS层**:通过 `vinode` 抽象不同文件系统的操作(如 `rfs_read` 与 `ext4_read` 共享同一接口)。 65 | - **RFS层**:专注于磁盘数据结构管理(如 `rfs_superblock` 管理inode表和数据块分配)。 66 | 67 | #### 3. **性能优化** 68 | - **缓存机制**:`rfs_dir_cache` 缓存目录项,减少 `opendir/readdir` 的磁盘I/O。 69 | - **批量操作**:`rfs_r1block` 和 `rfs_w1block` 按块读写,减少碎片化访问。 70 | 71 | --- 72 | 73 | ### 四、潜在问题与改进方向 74 | 1. **原子性风险**: 75 | - 若 `rfs_write_dinode` 失败,可能导致 `vinode` 与磁盘数据不一致(需引入日志机制,参考NFS的RPC事务)。 76 | 77 | 2. **扩展性限制**: 78 | - `rfs_dinode.addrs` 仅支持直接块,大文件需扩展多级索引(类似ext4的间接块设计)。 79 | 80 | 3. **并发控制**: 81 | - 未显式定义锁机制(如 `vinode->i_lock`),多线程操作可能引发竞态条件。 82 | 83 | --- 84 | 85 | ### 总结 86 | `vinode` 与 `rfs_dinode` 的分层设计体现了 **虚拟文件系统架构的核心思想**:通过内存抽象屏蔽底层差异,通过磁盘结构保证数据持久化。这种模式在Linux(vnode与ext4_inode)和NFS(远程文件代理)中广泛应用,是文件系统实现跨平台、可扩展性的基石。 -------------------------------------------------------------------------------- /doc/什么是panic.md: -------------------------------------------------------------------------------- 1 | 2 | # 什么是系统 panic 3 | 4 | 在操作系统中,panic 是一种严重错误的处理机制,用来应对无法恢复的错误或系统异常。触发 panic 的时机通常是操作系统发现自己无法继续安全运行的情况下,例如访问非法内存、无法满足的系统约束、硬件故障、或者无法恢复的操作系统内部错误等。 5 | 6 | 在 RISC-V 架构下,panic 的执行通常包括以下步骤: 7 | 8 | --- 9 | 10 | ## 1. 打印错误信息 11 | 当系统进入 panic 状态时,操作系统首先会打印详细的错误信息,通常包括: 12 | - 错误发生的位置(例如出错的代码文件和行号)。 13 | - 错误类型(例如非法内存访问、栈溢出等)。 14 | - 错误发生时的其他相关上下文(如 CPU 寄存器的值、进程 ID、调用栈等)。 15 | 16 | 这样做的目的是让开发人员或调试工具能够尽可能清晰地理解错误的根本原因,帮助排查问题。 17 | 18 | ### 示例(打印错误信息): 19 | 20 | ```cpp 21 | void panic(const char *message) { 22 | printf("Kernel panic: %s\n", message); 23 | // 打印更多的上下文信息 24 | printf("Current registers: ...\n"); 25 | // 系统崩溃,不能继续运行 26 | halt_system(); // 系统进入不可恢复状态 27 | } 28 | ``` 29 | 30 | --- 31 | 32 | ## 2. 禁用中断 33 | panic 状态下,操作系统通常会禁用所有中断。因为系统已经进入了不可恢复的状态,再接受任何中断可能导致系统的不一致,甚至进一步的崩溃或数据损坏。 34 | 35 | 在 RISC-V 中,禁用中断通常是通过修改 `sstatus` 寄存器的相应标志位来完成的,例如清除 `SSTATUS_SPIE` 和 `SSTATUS_SIE` 等中断使能位,防止进一步的中断发生。 36 | 37 | ```asm 38 | csrrs x0, sstatus, SSTATUS_SIE # 禁用全局中断 39 | csrrs x0, sstatus, SSTATUS_SPIE # 禁用后续异常中断 40 | ``` 41 | 42 | --- 43 | 44 | ## 3. 保存当前上下文 45 | 为了进行有效的调试和故障排查,操作系统可能会在 panic 发生时保存当前的系统上下文,包括 CPU 寄存器的值、栈指针、程序计数器(PC)、进程信息等。这些信息会被保存到某个地方(如内存、日志文件、调试端口等)供后续分析。 46 | 47 | 在 RISC-V 中,操作系统可能会通过 `csrr` 和 `csrw` 指令访问和保存当前状态寄存器(如 `sstatus`、`sepc`、`scause` 等),以及所有通用寄存器的值。 48 | 49 | ```asm 50 | csrr t0, sepc # 保存 sepc(异常程序计数器),即导致 panic 的指令地址 51 | csrr t1, sstatus # 保存 sstatus(状态寄存器),记录当前中断状态 52 | ``` 53 | 54 | --- 55 | 56 | ## 4. 停机或系统重启 57 | panic 的最后一步通常是 停机 或 重启 系统。由于操作系统无法在 panic 后继续运行,因此必须采取一些安全措施,通常是将系统状态置为不可操作的状态,并停止所有进一步的操作。 58 | 59 | 在嵌入式系统或某些特殊平台上,操作系统可能会尝试进行硬件重启,以便在硬件恢复后让系统能够重新启动。 60 | 61 | ### 停机: 62 | 63 | ```cpp 64 | void halt_system() { 65 | // 在 panic 时执行,关闭中断、停止所有操作,进入不可恢复的停机状态 66 | while (1) { 67 | // 系统进入死循环,等待硬件复位 68 | } 69 | } 70 | ``` 71 | 72 | ### 重启: 73 | 74 | ```cpp 75 | void restart_system() { 76 | // 重启硬件,恢复系统 77 | // 具体实现取决于平台和硬件架构 78 | } 79 | ``` 80 | 81 | --- 82 | 83 | ## 5. 具体的例子:RISC-V 中的 panic 处理 84 | 在 RISC-V 上,操作系统的 panic 可能会进行如下操作: 85 | - 打印错误信息:将错误信息输出到控制台或日志。 86 | - 禁用中断:通过清除中断使能位,防止系统继续接受中断。 87 | - 保存上下文:将程序计数器、栈指针和其他寄存器的值保存到预设的内存区域。 88 | - 系统停机:系统将进入死循环或执行硬件重启操作,停止进一步的执行。 89 | 90 | 例如,RISC-V 上的 panic 代码可能如下: 91 | 92 | ```asm 93 | .global panic 94 | panic: 95 | # 打印 panic 错误信息 96 | # 这里是一个模拟的错误打印过程,实际代码会通过串口、屏幕等输出设备打印信息 97 | li a0, 1 # 错误代码 1 98 | li a1, panic_message # 错误信息 99 | call print_error_message # 调用打印错误信息函数 100 | 101 | # 禁用中断,防止中断干扰系统状态 102 | csrrs x0, sstatus, SSTATUS_SIE # 禁用中断 103 | csrrs x0, sstatus, SSTATUS_SPIE # 禁用后续异常中断 104 | 105 | # 保存当前上下文:将寄存器值保存到内存 106 | csrr t0, sepc # 保存异常发生时的程序计数器(PC) 107 | csrr t1, sstatus # 保存当前状态寄存器(sstatus) 108 | 109 | # 执行停机操作 110 | j halt_system # 系统进入死循环 111 | ``` 112 | 113 | --- 114 | 115 | ## 6. 总结 116 | panic 是操作系统用来应对严重错误的机制。当操作系统发现自己进入了无法恢复的状态时,会执行 panic,打印错误信息、禁用中断、保存上下文并停机或重启系统。这一过程是确保系统稳定性和为调试提供有用信息的重要手段。 117 | 118 | 在 RISC-V 架构中,panic 可能涉及保存 CPU 寄存器状态、修改 `sstatus` 等状态寄存器,禁用中断并进入停机状态或重启。这些步骤确保系统能够安全地处理无法恢复的错误,并提供足够的上下文供后续分析。 119 | -------------------------------------------------------------------------------- /doc/什么是外部时钟中断.md: -------------------------------------------------------------------------------- 1 | # 什么是时钟中断 2 | 3 | 时钟中断(Clock Interrupt)是一种由硬件定时器触发的中断信号,通常用于操作系统内的周期性任务处理。硬件定时器会定期向处理器发送中断请求,操作系统通过处理中断来执行预定的任务,如进程调度、资源管理和设备控制等。 4 | 5 | 时钟中断是一种外部中断,通常在系统的时钟频率或定时器周期到达时触发。通过这种机制,操作系统能够在固定的时间间隔内对当前执行的任务进行管理和调度,从而实现对系统状态的实时控制。 6 | 7 | ## 操作系统对用户程序触发时钟中断的原因 8 | 9 | 操作系统对用户程序触发外部时钟中断通常是为了实现操作系统管理的一些关键功能。时钟中断是操作系统中非常重要的一种机制,以下是一些主要原因: 10 | 11 | ### 1. 时间片轮转(时间片调度) 12 | 13 | 操作系统通常会通过时钟中断来实现进程的时间片轮转。在现代操作系统中,进程调度采用的是时间片轮转算法,即每个进程被分配一个固定的时间段(时间片)来执行。当时间片用尽时,时钟中断会被触发,操作系统会暂停当前进程,保存其状态,并切换到另一个进程执行。这样可以确保系统中的各个进程公平地获得 CPU 时间,从而实现多任务处理。 14 | 15 | ### 2. 内核任务的定期执行 16 | 17 | 操作系统可能需要定期执行一些内核级任务,如内存管理、文件系统维护、硬件驱动更新等。时钟中断使得这些定期任务可以在不需要人工干预的情况下自动执行。比如,操作系统可以利用时钟中断来周期性地检查空闲内存,或进行进程的超时检查。 18 | 19 | ### 3. 资源监控与维护 20 | 21 | 外部时钟中断可以用来监控系统资源的使用情况。例如,操作系统可以定期检查各个进程的状态,计算进程的 CPU 使用率、内存消耗等指标。这样可以及时发现资源的瓶颈或者不正常的资源消耗,并做出相应的调整。 22 | 23 | ### 4. 硬件设备管理 24 | 25 | 某些硬件设备(如定时器、输入设备等)可能需要定期与操作系统进行交互,通过时钟中断,操作系统能够定期处理硬件设备的请求或数据。举个例子,时钟中断可以用于处理外部设备的输入输出操作,确保设备的响应时间符合要求。 26 | 27 | ### 5. 实时任务调度 28 | 29 | 对于一些实时操作系统,时钟中断用于调度实时任务。实时任务通常有严格的时间要求,时钟中断可确保这些任务能够在规定的时间内被调度和执行,避免系统延迟。 30 | 31 | ## 总结 32 | 33 | 时钟中断为操作系统提供了一种机制,用于确保多任务调度的公平性、内核任务的定期执行、资源管理、硬件设备交互以及实时任务的调度。通过这种中断,操作系统能够精确地控制时间和任务执行,从而实现高效的系统管理。 34 | -------------------------------------------------------------------------------- /doc/什么是链接脚本.md: -------------------------------------------------------------------------------- 1 | ## 链接脚本概述 2 | 3 | ### 1. **什么是链接脚本(Linker Script)** 4 | 5 | 链接脚本(Linker Script,简称 `.lds`)是一个用于告诉链接器(Linker)如何将多个目标文件(通常是 `.o` 文件)和库文件链接成最终可执行文件的文件。它定义了程序的内存布局、段的分布以及其他特定的链接过程,比如符号的地址映射、程序段的顺序等。链接脚本的主要作用是在链接阶段指定程序如何从源代码转换为可执行文件,尤其是如何组织和安排代码、数据等在内存中的布局。 6 | 7 | ### 2. **链接脚本的基本组成** 8 | 9 | 一个链接脚本通常包括以下几个部分: 10 | 11 | #### 2.1 **输出架构(OUTPUT_ARCH)** 12 | 链接脚本的第一行通常会指定目标平台的架构或目标处理器类型。例如,在 `riscv` 或 `arm` 等架构中,链接器需要知道如何为特定架构生成合适的代码和地址映射。 13 | 14 | ```ld 15 | OUTPUT_ARCH("riscv") 16 | ``` 17 | 18 | #### 2.2 **入口点(ENTRY)** 19 | `ENTRY` 指令指定程序的入口点,即程序执行开始的位置。通常在 C 程序中,这个入口点是 `main` 函数。 20 | 21 | ```ld 22 | ENTRY(main) 23 | ``` 24 | 25 | #### 2.3 **段的定义(SECTIONS)** 26 | `SECTIONS` 部分是链接脚本的核心,定义了目标文件中各个段在内存中的映射及布局。通常,程序有多个段(如 `.text`、`.data`、`.bss` 等),每个段有不同的功能,并且它们的内存布局和地址可以通过链接脚本进行控制。 27 | 28 | 每个段内的内容被放置在指定的内存地址,并且可以通过对齐(`ALIGN`)命令来确保段的开始地址符合硬件要求(例如,4KB、8KB 或 16字节对齐)。 29 | 30 | ```ld 31 | SECTIONS { 32 | . = 0x10000000; /* 起始地址 */ 33 | .text : { *(.text) } /* 代码段 */ 34 | .data : { *(.data) } /* 数据段 */ 35 | .bss : { *(.bss) } /* 未初始化的全局变量 */ 36 | } 37 | ```而 38 | 39 | #### 2.4 **地址与对齐(Address and Alignment)** 40 | 链接脚本可以指定程序各个段的起始地址,确保它们被正确地映射到物理内存中。例如,可以通过 `. = ALIGN(16)` 来确保段的起始地址对齐到 16 字节。 41 | 42 | ```ld 43 | .text : { *(.text) } 44 | = ALIGN(16) 45 | ``` 46 | 47 | #### 2.5 **符号定义与重定位** 48 | 链接脚本可以定义符号并指定它们的地址位置,或者通过 `PHDRS`(程序头表)等机制为段进行重定位。符号在不同目标文件和段之间的映射是链接过程中非常重要的一部分,链接脚本负责确保这些符号得到正确的处理。 49 | 50 | ### 3. **链接脚本的作用** 51 | 52 | 链接脚本的主要作用是为链接器提供如何组织和分配程序段的说明。具体言,它的作用包括: 53 | 54 | - **定义程序段的布局**:确定代码段、数据段、未初始化数据段等各个段在内存中的位置,以及它们的对齐方式。 55 | - **控制符号的地址**:指定符号的内存地址,确保变量、函数等在最终可执行文件中被正确映射。 56 | - **设定入口点**:告诉链接器程序的入口函数在哪里,通常是 `main` 函数。 57 | - **优化内存使用**:通过手动控制各段的布局,避免段之间的冲突和浪费内存空间。 58 | - **支持特定硬件要求**:例如,在裸机环境或嵌入式系统中,链接脚本可以确保程序加载到指定的物理内存地址,以便与硬件兼容。 59 | 60 | ### 4. **链接脚本的示例** 61 | 62 | 以下是一个简单的链接脚本示例,它展示了如何设置程序段的布局并为程序分配内存地址: 63 | 64 | ```ld 65 | OUTPUT_ARCH("riscv") 66 | ENTRY(main) 67 | 68 | SECTIONS 69 | { 70 | . = 0x81000000; /* 设置程序段起始地址 */ 71 | .text : { *(.text) } /* 代码段 */ 72 | . = ALIGN(16); /* 代码段对齐到16字节 */ 73 | .data : { *(.data) } /* 数据段 */ 74 | . = ALIGN(16); /* 数据段对齐到16字节 */ 75 | .bss : { *(.bss) } /* 未初始化数据段 */ 76 | } 77 | ``` 78 | 79 | ### 5. **常见指令与配置** 80 | 81 | - **`ENTRY(symbol)`**:指定程序的入口点,通常为 `main` 或操作系统的启动例程。 82 | - **`OUTPUT_ARCH(arch)`**:指定目标架构,例如 `riscv`、`arm`、`x86_64`。 83 | - **`SECTIONS`**:指定程序的各个段和它们在内存中的布局。 84 | - **`ALIGN(bytes)`**:将内存地址对齐到给定的字节数(例如 16 字节、4KB)。 85 | - **`*(.text)`**:将目标文件中的 `.text` 段(代码段)合并到最终可执行文件的 `.text` 段。 86 | - **`. = address`**:指定当前位置(`.`)的内存地址。 87 | 88 | ### 6. **总结** 89 | 90 | 链接脚本是链接过程中的重要工具,它通过控制目标文件和库文件的段布局,确保最终的可执行文件在内存中的结构符合要求。对于裸机程序、嵌入式系统或操作系统内核开发,链接脚本尤为重要,因为这些程序通常需要手动管理内存布局,并确保程序正确加载到内存中的指定地址。通过链接脚本,开发者可以精确控制程序的内存使用,优化性能,并确保程序能够正确运行在目标硬件上。 -------------------------------------------------------------------------------- /doc/信号量和自旋锁.md: -------------------------------------------------------------------------------- 1 | 在操作系统的并发控制中,选择 **自旋锁(Spinlock)** 还是 **信号量(Semaphore)** 取决于**临界区的特性**、**持有锁的时间**以及**上下文环境**。以下是两者的核心区别和适用场景: 2 | 3 | --- 4 | 5 | ### 一、自旋锁(Spinlock) 6 | #### **适用场景** 7 | 1. **极短的临界区** 8 | 当临界区代码执行时间极短(如几十条指令内),且**无阻塞操作**时,自旋锁的忙等待(Busy Waiting)不会显著浪费 CPU 周期。 9 | **示例**:修改共享计数器、更新链表头指针。 10 | 11 | 2. **中断上下文(不可睡眠环境)** 12 | 在中断处理程序(如硬件中断、软中断)或不可抢占的上下文中,线程**不能主动让出 CPU**(如调用 `schedule()`),此时必须使用自旋锁。 13 | **示例**:网络驱动处理数据包时保护接收队列。 14 | 15 | 3. **多核 CPU 环境** 16 | 在多核系统中,自旋锁的忙等待仅影响当前核心,其他核心仍可并行执行任务,适合**高频低延迟操作**。 17 | 18 | #### **核心特点** 19 | - **忙等待**:线程持续轮询锁状态,不释放 CPU。 20 | - **低延迟**:无需上下文切换,适合高频操作。 21 | - **不可睡眠**:禁止在持有自旋锁时调用可能阻塞的函数(如 `kmalloc`)。 22 | 23 | --- 24 | 25 | ### 二、信号量(Semaphore) 26 | #### **适用场景** 27 | 1. **较长的临界区或可能阻塞的操作** 28 | 当临界区代码执行时间较长(如涉及 I/O 操作、内存分配)或需要等待外部事件时,信号量通过**阻塞线程**避免 CPU 空转。 29 | **示例**:文件系统读写需要等待磁盘响应。 30 | 31 | 2. **用户态线程同步** 32 | 用户态程序无法直接使用自旋锁(需内核支持),信号量是更通用的同步机制。 33 | **示例**:多线程程序通过 `pthread_mutex`(基于信号量)保护共享资源。 34 | 35 | 3. **需要优先级继承的实时系统** 36 | 在实时操作系统中,信号量可通过优先级继承解决优先级反转问题,而自旋锁无法实现。 37 | 38 | #### **核心特点** 39 | - **主动阻塞**:线程进入睡眠状态,释放 CPU 供其他线程使用。 40 | - **高资源利用率**:适合长等待时间的场景,减少 CPU 浪费。 41 | - **可睡眠**:允许在持有信号量时执行阻塞操作。 42 | 43 | --- 44 | 45 | ### 三、决策流程图 46 | ```plaintext 47 | 是否在中断上下文或不可抢占环境? 48 | ├── 是 → 必须使用自旋锁(信号量会导致睡眠,引发死锁) 49 | └── 否 → 临界区是否极短(< 1μs)且无阻塞? 50 | ├── 是 → 自旋锁(避免上下文切换开销) 51 | └── 否 → 信号量(避免忙等待浪费 CPU) 52 | ``` 53 | 54 | --- 55 | 56 | ### 四、性能对比 57 | | **指标** | **自旋锁** | **信号量** | 58 | |-------------------|-------------------------------------|-------------------------------------| 59 | | **CPU 占用** | 高(忙等待) | 低(线程睡眠) | 60 | | **延迟** | 低(无上下文切换) | 高(上下文切换开销) | 61 | | **适用锁持有时间**| 纳秒级 ~ 微秒级 | 微秒级 ~ 毫秒级 | 62 | | **可扩展性** | 差(高频竞争时 CPU 利用率骤降) | 较好(线程排队等待) | 63 | 64 | --- 65 | 66 | ### 五、代码示例 67 | #### 自旋锁(内核态) 68 | ```c 69 | spinlock_t lock; 70 | spin_lock_init(&lock); 71 | 72 | spin_lock(&lock); // 获取自旋锁 73 | // 临界区操作... 74 | spin_unlock(&lock); // 释放自旋锁 75 | ``` 76 | 77 | #### 信号量(用户态) 78 | ```c 79 | #include 80 | sem_t sem; 81 | sem_init(&sem, 0, 1); // 初始值为1(二进制信号量) 82 | 83 | sem_wait(&sem); // 获取信号量(P操作) 84 | // 临界区操作... 85 | sem_post(&sem); // 释放信号量(V操作) 86 | ``` 87 | 88 | --- 89 | 90 | ### 六、总结 91 | - **自旋锁**:用于**极短且无阻塞**的临界区,或**不可睡眠的上下文**(如中断处理)。 92 | - **信号量**:用于**较长或可能阻塞**的临界区,或**用户态线程同步**。 93 | 94 | 实际开发中需结合场景权衡:高频操作优先自旋锁,长等待优先信号量,中断上下文强制自旋锁。 -------------------------------------------------------------------------------- /doc/内核堆.md: -------------------------------------------------------------------------------- 1 | # 内核堆 2 | 3 | ## 内核原本的地址空间分配方式 4 | ### kern_vm_init 5 | 在kern_vm_init中创建了内核页表,并对所有物理内存,在内核页表中做直接映射。 6 | 内核段空间:KERN_BASE到etext. 7 | 所以说,我们对内核堆的地址分配,应该从etext之后的一个页开始分配。 8 | 9 | 存在新的问题,就是内核堆如果直接建立在内核页表上,那么地址就是不连续的。 10 | 为了实现方便起见,内核堆的地址就是不连续的,使用链表进行管理。 11 | 我们只要不在内核堆中一次性请求一页以上的连续内存即可。 12 | 13 | 然后定义一个全局的数据结构,用来当内核堆的表头。 14 | 15 | 16 | ## 内核堆的实现 17 | 18 | ### global.c 19 | 20 | 增加一个虚拟的头结点,用来给内核堆分配内存。 21 | 在global.c中增加一个全局变量`heap_block kernel_heap_head;`,并在global.h中增加对应的extern声明。 22 | 23 | ``` 24 | 25 | heap_block kernel_heap_head; 26 | ``` 27 | 28 | ### kern_vm_init 29 | 30 | 增加初始化kernel_heap_head的代码: 31 | 32 | ```c 33 | kernel_heap_head.next = NULL; 34 | kernel_heap_head.prev = NULL; 35 | kernel_heap_head.free = 0; 36 | kernel_heap_head.size = 0; 37 | ``` 38 | 39 | 40 | ### pmm.c 41 | 42 | 增加函数kmalloc、kfree和服务函数: 43 | ```c 44 | void kheap_insert(heap_block* prev, heap_block* newblock){ 45 | newblock->next = prev->next; 46 | newblock->prev = prev; 47 | if(prev->next != NULL){ 48 | prev->next->prev = newblock; 49 | } 50 | prev->next = newblock; 51 | } 52 | 53 | void kheap_alloc(){ 54 | heap_block* new_page = Alloc_page(); 55 | new_page->size = PGSIZE - sizeof(heap_block); 56 | new_page->free = 1; 57 | kheap_insert(&kernel_heap_head, new_page); 58 | } 59 | 60 | 61 | void* kmalloc(size_t size){ 62 | int required_size = ALIGN(size + sizeof(heap_block),8); 63 | //目前只服务大小小于一个页的内核堆分配请求。因为内核堆难以稳定获取连续的物理内存。 64 | if(size <= 0 || required_size > PGSIZE){ 65 | return NULL; 66 | } 67 | int hartid = read_tp(); 68 | heap_block* iterator = &kernel_heap_head; 69 | // 遍历内核堆表,找一个大小符合要求的块,进行分割 70 | while(iterator->next){ 71 | if(iterator->next->free && iterator->next->size >= required_size){ 72 | iterator->next->free = 0; 73 | // 可以分割 74 | if(iterator->next->size > required_size + sizeof(heap_block)){ 75 | 76 | heap_block* new_block = (heap_block *)((uintptr_t)iterator->next + required_size); 77 | new_block->size = iterator->next->size - required_size - sizeof(heap_block); 78 | new_block->free = 1; 79 | iterator->next->size = required_size; 80 | kheap_insert(iterator->next,new_block); 81 | 82 | } 83 | return (void*)((uint64)iterator->next + sizeof(heap_block)); 84 | } 85 | iterator = iterator->next; 86 | } 87 | // 没找到符合要求的内存块,在内核堆表头之后插入一个新的页。 88 | kheap_alloc(); 89 | // 重新分配。 90 | return kmalloc(size); 91 | } 92 | 93 | 94 | void kfree(void* ptr){ 95 | if(ptr == NULL){ 96 | return; 97 | } 98 | heap_block* block = (heap_block*)((uintptr_t)ptr - sizeof(heap_block)); 99 | block->free = 1; 100 | // 合并同一页上前一个内存块 101 | if(block->prev && block->prev->free == 1 && ((uint64)block) % PGSIZE == ((uint64)block->prev) % PGSIZE){ 102 | block->prev->size += sizeof(heap_block) + block->size; 103 | block->prev->next = block->next; 104 | if(block->next){ 105 | block->next->prev = block->prev; 106 | } 107 | block = block->prev; 108 | } 109 | // 合并同一页上后一个内存块 110 | if(block->next && block->next->free == 1 && ((uint64)block) % PGSIZE == ((uint64)block->next) % PGSIZE){ 111 | block->size += sizeof(heap_block) + block->next->size; 112 | block->next = block->next->next; 113 | if(block->next->next){ 114 | block->next->next->prev = block; 115 | } 116 | } 117 | // 如果得到了一个空页,还给内存池。 118 | if(block->size == PGSIZE - sizeof(heap_block)){ 119 | block->prev->next = block->next; 120 | if(block->next){ 121 | block->next->prev = block->prev; 122 | } 123 | free_page(block); 124 | } 125 | return; 126 | } 127 | 128 | ``` 129 | 130 | ## 内核堆的测试 131 | 132 | 我们新增一个测试系统调用,由用户发起,进入内核态测试系统功能。 -------------------------------------------------------------------------------- /doc/内核文件系统组织架构.md: -------------------------------------------------------------------------------- 1 | 在你的比赛 OS 设计里,系统架构是**分层的**,但系统调用(syscall)是**垂直贯穿所有层**的。因此,如何组织代码文件结构,既要保持逻辑清晰,又要方便维护和扩展。 2 | 3 | --- 4 | 5 | ## **架构设计建议** 6 | 建议采用**分层架构 + 逻辑组织**的方法,代码组织方式如下: 7 | 8 | ``` 9 | /src 10 | ├── kernel/ # 核心内核代码 11 | │ ├── init/ # 内核初始化 12 | │ ├── mm/ # 内存管理 13 | │ ├── fs/ # 文件系统 14 | │ ├── sched/ # 进程调度 15 | │ ├── syscalls/ # 系统调用实现 16 | │ │ ├── sys_file.c # 文件相关 syscalls 17 | │ │ ├── sys_proc.c # 进程管理相关 syscalls 18 | │ │ ├── sys_mem.c # 内存管理相关 syscalls 19 | │ │ ├── sys_misc.c # 其他 syscalls 20 | │ │ ├── syscall.h # syscall 统一头文件 21 | │ ├── traps/ # 中断、异常处理 22 | │ ├── drivers/ # 设备驱动 23 | │ ├── arch/ # 体系结构相关 24 | │ ├── lib/ # 内核工具库 25 | │ ├── kernel.c # 入口 26 | │ ├── syscall.c # syscall 分发 27 | ├── user/ # 用户态程序 28 | ├── include/ # 公共头文件 29 | ├── Makefile # 编译配置 30 | ``` 31 | 32 | --- 33 | 34 | ## **是否要把每个 `do_xxx` 写成单独的 `.c` 文件?** 35 | 不建议**每个 `do_xxx` 都拆成单独的 `.c` 文件**,而是按照**逻辑模块拆分**,比如: 36 | - **`sys_file.c`** 处理所有文件操作,如 `do_open()`, `do_close()` 37 | - **`sys_proc.c`** 处理进程管理,如 `do_fork()`, `do_exec()` 38 | - **`sys_mem.c`** 处理内存相关,如 `do_brk()`, `do_mmap()` 39 | - **`sys_misc.c`** 处理零散 syscall,如 `do_gettimeofday()` 40 | 41 | 这样做的好处: 42 | ✅ 逻辑清晰,方便管理 43 | ✅ 每个 `.c` 文件大小适中,易于阅读 44 | ✅ 避免碎片化,每个模块都有相对独立的实现 45 | 46 | --- 47 | 48 | ## **系统调用的实现结构** 49 | 系统调用通常有三层: 50 | 1. **用户态的 `syscall(int num, ...)`(libc 或 inline assembly)** 51 | 2. **内核态的 `syscall_handler()` 负责解析 syscall 号** 52 | 3. **各个 `do_xxx()` 负责实际处理逻辑** 53 | 54 | ### **示例** 55 | #### **1. 用户态 syscall 触发** 56 | ```c 57 | #define SYS_OPEN 5 58 | int open(const char *path, int flags) { 59 | return syscall(SYS_OPEN, path, flags); 60 | } 61 | ``` 62 | 63 | #### **2. `syscall_handler()` 解析系统调用** 64 | ```c 65 | #include "syscall.h" 66 | 67 | void syscall_handler(int syscall_num, ...) { 68 | switch (syscall_num) { 69 | case SYS_OPEN: 70 | do_open((const char*)arg1, (int)arg2); 71 | break; 72 | case SYS_FORK: 73 | do_fork(); 74 | break; 75 | default: 76 | panic("Unknown syscall"); 77 | } 78 | } 79 | ``` 80 | 81 | #### **3. `do_xxx()` 具体实现** 82 | ```c 83 | // sys_file.c 84 | int do_open(const char *path, int flags) { 85 | // 这里实现文件打开逻辑 86 | } 87 | ``` 88 | 89 | --- 90 | 91 | ## **总结** 92 | - **文件结构**:`syscalls/` 目录下,按逻辑组织 syscall,而不是每个 `do_xxx` 一个文件 93 | - **`syscall_handler()`**:统一入口解析 syscall 号,分发到不同 `do_xxx()` 94 | - **`do_xxx()`**:具体实现逻辑,按模块拆分(`sys_file.c`, `sys_proc.c` 等) 95 | 96 | 这样可以**保持代码结构清晰、易维护、可扩展**!🌟 97 | 98 | 你觉得这个架构适合你的比赛 OS 吗?😊 -------------------------------------------------------------------------------- /doc/写时拷贝的实现.md: -------------------------------------------------------------------------------- 1 | 为支持 **写时拷贝(COW)**,需修改 `fork_segment` 函数,核心改动如下: 2 | 3 | --- 4 | 5 | ### **关键修改点** 6 | 1. **父进程页表权限降级** 7 | 将父进程的页表项权限从 **可写(PTE_W)** 改为 **只读**,确保后续写入触发 COW。 8 | 2. **子进程映射共享物理页** 9 | 子进程直接共享父进程的物理页,但权限设为 **只读**。 10 | 3. **维护物理页引用计数** 11 | 为每个物理页添加引用计数,共享时递增,COW 复制时递减。 12 | 13 | --- 14 | 15 | ### **修改后的代码** 16 | ```c 17 | // 物理页引用计数(假设已全局实现) 18 | extern void increment_ref_count(uint64 pa); 19 | extern void decrement_ref_count(uint64 pa); 20 | 21 | void fork_segment(process *parent, process *child, int segnum, int choice, uint64 perm) { 22 | mapped_region *mapped_info = &parent->mapped_info[segnum]; 23 | uint64 va = mapped_info->va; 24 | for (int i = 0; i < mapped_info->npages; i++) { 25 | uint64 pa = lookup_pa(parent->pagetable, mapped_info->va + i * PGSIZE); 26 | pte_t *parent_pte = walk(parent->pagetable, mapped_info->va + i * PGSIZE, 0); 27 | 28 | if (choice == FORK_COW) { 29 | // 仅当父进程页可写时,降级为只读并增加引用计数 30 | if (*parent_pte & PTE_W) { 31 | *parent_pte &= ~PTE_W; // 清除父进程页表项的写权限 32 | increment_ref_count(pa); // 增加物理页引用计数 33 | } 34 | // 映射子进程页表项为只读,共享同一物理页 35 | user_vm_map(child->pagetable, va + i * PGSIZE, PGSIZE, pa, (perm & ~PTE_W)); 36 | } else if (choice == FORK_COPY) { 37 | // 分配新页并复制内容(原逻辑) 38 | uint64 new_pa = (uint64)Alloc_page(); 39 | memcpy((void *)new_pa, (void *)pa, PGSIZE); 40 | user_vm_map(child->pagetable, va + i * PGSIZE, PGSIZE, new_pa, perm); 41 | } else { // FORK_MAP 42 | // 直接共享物理页(需确保原页不可写或已正确处理引用) 43 | user_vm_map(child->pagetable, va + i * PGSIZE, PGSIZE, pa, perm); 44 | } 45 | } 46 | memcpy(&(child->mapped_info[segnum]), mapped_info, sizeof(mapped_region)); 47 | child->total_mapped_region++; 48 | } 49 | ``` 50 | 51 | --- 52 | 53 | ### **配套修改** 54 | 1. **物理页引用计数管理** 55 | 需全局维护物理页的引用计数(例如通过数组或哈希表): 56 | ```c 57 | uint64 ref_counts[MAX_PAGES]; // 物理页引用计数数组 58 | 59 | void increment_ref_count(uint64 pa) { 60 | ref_counts[PA2IDX(pa)]++; 61 | } 62 | 63 | void decrement_ref_count(uint64 pa) { 64 | if (ref_counts[PA2IDX(pa)] > 0) { 65 | ref_counts[PA2IDX(pa)]--; 66 | } 67 | } 68 | ``` 69 | 2. **COW 页面错误处理** 70 | 在页面错误处理函数中,需识别 COW 场景并复制页面: 71 | ```c 72 | void handle_page_fault(uint64 va, int cause) { 73 | pte_t *pte = walk(current->pagetable, va, 0); 74 | if (pte && (*pte & PTE_V) && !(*pte & PTE_W) && (cause == WRITE_ACCESS)) { 75 | uint64 pa = PTE2PA(*pte); 76 | if (ref_counts[PA2IDX(pa)] > 1) { // 存在多进程共享 77 | uint64 new_pa = Alloc_page(); 78 | memcpy((void *)new_pa, (void *)pa, PGSIZE); 79 | *pte = PA2PTE(new_pa) | (*pte | PTE_W); // 更新为可写 80 | decrement_ref_count(pa); // 原页引用减1 81 | increment_ref_count(new_pa); // 新页引用加1 82 | } else { 83 | *pte |= PTE_W; // 唯一所有者,直接恢复写权限 84 | } 85 | } else { 86 | // 处理非法访问 87 | } 88 | } 89 | ``` 90 | 91 | --- 92 | 93 | ### **关键逻辑说明** 94 | | **操作** | **父进程页表** | **子进程页表** | **物理页引用计数** | 95 | |------------------------|----------------|---------------------|--------------------| 96 | | **FORK_COW** | 只读(PTE_R) | 只读(PTE_R) | 递增 | 97 | | **FORK_COPY** | 不变 | 新页可写(PTE_RW) | 新页初始为1 | 98 | | **COW 复制后(写时)** | 恢复可写 | 新页可写(PTE_RW) | 原页递减,新页递增 | 99 | 100 | --- 101 | 102 | ### **总结** 103 | 通过 **降级父进程页表权限**、**共享只读映射子进程** 和 **维护引用计数**,可实现 COW 机制。在写入触发页面错误时,操作系统复制物理页并更新权限,确保进程间内存隔离与高效共享。 -------------------------------------------------------------------------------- /doc/实现相对路径.md: -------------------------------------------------------------------------------- 1 | ### **🚀 如何在内核实现相对路径的 `syscall`?** 2 | ✅ **相对路径的 `syscall`(如 `open("file.txt")`)需要结合进程的** `current working directory (cwd)` **进行解析。** 3 | ✅ **在内核里,你需要解析 `cwd` + `relative_path`,转换为绝对路径,然后交给 VFS 处理。** 4 | ✅ **Linux 和 `xv6` 都使用 `cwd` 机制来支持相对路径。** 5 | 6 | --- 7 | 8 | ## **📌 1. `sys_open()` 如何解析相对路径?** 9 | 📌 **目标:** 10 | 1. **每个进程都有一个 `cwd`,表示当前工作目录** 11 | 2. **如果 `path` 是相对路径(不以 `/` 开头),需要拼接 `cwd + path`** 12 | 3. **调用 `vfs_lookup()` 查找该路径** 13 | 4. **返回 `inode` 供后续 `read/write` 操作** 14 | 15 | --- 16 | 17 | ## **📌 2. 进程结构 `task_struct` 需要保存 `cwd`** 18 | 📌 **在进程控制块 `task_struct` 里添加 `cwd`** 19 | ```c 20 | struct task_struct { 21 | char cwd[128]; // 进程的当前目录 22 | }; 23 | ``` 24 | ✅ **这样,每个进程都有自己的 `cwd`,支持 `chdir()` 变更目录!** 25 | 26 | --- 27 | 28 | ## **📌 3. 解析相对路径** 29 | 📌 **`resolve_path()`:如果 `path` 是相对路径,拼接 `cwd`** 30 | ```c 31 | void resolve_path(char *dest, const char *cwd, const char *path) { 32 | if (path[0] == '/') { 33 | // 绝对路径,直接拷贝 34 | strcpy(dest, path); 35 | } else { 36 | // 相对路径,拼接 cwd + "/path" 37 | snprintf(dest, 128, "%s/%s", cwd, path); 38 | } 39 | } 40 | ``` 41 | ✅ **这样 `"file.txt"` 变成 `"/home/user/file.txt"`,然后交给 VFS 处理!** 42 | 43 | --- 44 | 45 | ## **📌 4. 在 `sys_open()` 里解析路径** 46 | 📌 **修改 `sys_open()`,调用 `resolve_path()`** 47 | ```c 48 | int sys_open(const char *path, int flags) { 49 | char full_path[128]; 50 | resolve_path(full_path, current->cwd, path); // 转换成绝对路径 51 | 52 | struct inode *inode = vfs_lookup(full_path); 53 | if (!inode) return -1; // 文件不存在 54 | 55 | return inode->fd; // 返回文件描述符 56 | } 57 | ``` 58 | ✅ **这样 `open("file.txt")` 自动解析为 `cwd + "file.txt"`!** 59 | 60 | --- 61 | 62 | ## **📌 5. 添加 `sys_chdir()` 修改 `cwd`** 63 | 📌 **支持 `sys_chdir()`,让进程可以修改 `cwd`** 64 | ```c 65 | int sys_chdir(const char *path) { 66 | char full_path[128]; 67 | resolve_path(full_path, current->cwd, path); 68 | 69 | struct inode *dir = vfs_lookup(full_path); 70 | if (!dir || !S_ISDIR(dir->mode)) return -1; // 目录不存在 71 | 72 | strcpy(current->cwd, full_path); // 更新进程 cwd 73 | return 0; 74 | } 75 | ``` 76 | ✅ **这样 `chdir("..")` 可以修改 `cwd`,影响后续 `open()`!** 77 | 78 | --- 79 | 80 | ## **📌 6. 进程创建时继承 `cwd`** 81 | 📌 **子进程 `fork()` 时需要继承 `cwd`** 82 | ```c 83 | int sys_fork() { 84 | struct task_struct *child = alloc_process(); 85 | strcpy(child->cwd, current->cwd); // 继承父进程的 cwd 86 | return child->pid; 87 | } 88 | ``` 89 | ✅ **这样 `fork()` 后,子进程继续使用 `父进程的 cwd`!** 90 | 91 | --- 92 | 93 | ## **📌 7. 测试相对路径 `syscall`** 94 | 📌 **示例** 95 | ```c 96 | sys_chdir("/home/user"); // 切换目录 97 | sys_open("file.txt", 0); // 相当于 open("/home/user/file.txt") 98 | sys_chdir("docs"); // cwd 变成 "/home/user/docs" 99 | sys_open("report.pdf", 0); // 相当于 open("/home/user/docs/report.pdf") 100 | ``` 101 | ✅ **相对路径 `open()` 自动解析 `cwd + path`,正常运行!** 102 | 103 | --- 104 | 105 | ## **🎯 结论** 106 | ✅ **在 `sys_open()` 里解析相对路径,把 `cwd + path` 转换为绝对路径** 107 | ✅ **进程 `task_struct` 里保存 `cwd`,支持 `sys_chdir()` 修改 `cwd`** 108 | ✅ **`fork()` 继承 `cwd`,保证子进程路径一致** 109 | 110 | 🚀 **你可以在 RISC-V Proxy Kernel 里试试 `sys_chdir()` + `sys_open()`,看看 `cwd` 解析是否正确!🔥😆** -------------------------------------------------------------------------------- /doc/实际内核的stdin和stdout实现.md: -------------------------------------------------------------------------------- 1 | **是的!在实际的 UNIX 内核实现中,每个进程都会拥有 `stdin`、`stdout` 和 `stderr` 这三个默认的文件描述符(FD)**,它们在进程创建时自动分配,并且通常对应于终端、管道、文件或 `/dev/null`。你的代理内核目前使用全局的 Spike HTIF(Host-Target Interface)进行命令行交互,但如果要模拟真实的 UNIX 进程环境,每个进程应该有自己的 `stdin` / `stdout` / `stderr` 作为 FD `0, 1, 2`。🚀 2 | 3 | --- 4 | 5 | ## **🌟 在实际内核中,每个进程如何管理 `stdin` / `stdout` / `stderr`?** 6 | ### **📌 1. 进程的文件描述符表** 7 | 在 Linux 这样的实际内核中,每个进程都有一个 **文件描述符表**(`fd_table`),存储所有已打开的文件: 8 | ```c 9 | struct files_struct { 10 | struct file *fd_table[MAX_FD]; // 每个进程的文件描述符表 11 | }; 12 | ``` 13 | 📌 **在进程创建(`fork()`)时,默认分配 `stdin` / `stdout` / `stderr`**: 14 | ```c 15 | task->files->fd_table[STDIN_FILENO] = open_terminal(); 16 | task->files->fd_table[STDOUT_FILENO] = open_terminal(); 17 | task->files->fd_table[STDERR_FILENO] = open_terminal(); 18 | ``` 19 | - **`fd_table[0]`(`stdin`)** → 进程的标准输入,通常连接到键盘或管道。 20 | - **`fd_table[1]`(`stdout`)** → 进程的标准输出,通常连接到终端或文件。 21 | - **`fd_table[2]`(`stderr`)** → 进程的标准错误输出,通常连接到终端或日志。 22 | 23 | --- 24 | 25 | ### **📌 2. `stdin` / `stdout` 可能指向不同的对象** 26 | 进程的 `stdin` / `stdout` **并不一定总是终端**,它们可能是: 27 | | 文件描述符 | 可能的对象 | 28 | |------------|-----------| 29 | | `stdin` (`0`) | 终端 (`/dev/tty`)、文件、管道 (`pipe`) | 30 | | `stdout` (`1`) | 终端 (`/dev/tty`)、文件 (`logfile.txt`)、管道 (`pipe`) | 31 | | `stderr` (`2`) | 终端 (`/dev/tty`)、日志文件 (`logfile.txt`) | 32 | 33 | 💡 **示例**:`ls > output.txt` 34 | ```c 35 | // `stdout` 被重定向到文件 `output.txt` 36 | fd_table[STDOUT_FILENO] = open("output.txt", O_WRONLY | O_CREAT); 37 | ``` 38 | 39 | --- 40 | 41 | ## **🌟 你的代理内核如何模拟 `stdin` / `stdout`?** 42 | 目前你的代理内核**用全局的 Spike HTIF 进行输入输出**,但如果要模拟真实 UNIX 内核,你可以让每个进程拥有自己的 `stdin` / `stdout`: 43 | 44 | ### **✅ 1. 在 `fork()` 时分配 `stdin` / `stdout`** 45 | ```c 46 | task->files->fd_table[STDIN_FILENO] = get_default_tty(); 47 | task->files->fd_table[STDOUT_FILENO] = get_default_tty(); 48 | task->files->fd_table[STDERR_FILENO] = get_default_tty(); 49 | ``` 50 | - `get_default_tty()` 可以返回 `htif_console`,让 `exec()` 的新进程仍然能使用 HTIF 进行 I/O。 51 | 52 | --- 53 | 54 | ### **✅ 2. 在 `exec()` 之后保留 `stdin` / `stdout`** 55 | - **如果 `exec()` 后 `stdin` / `stdout` 丢失,进程将无法输入/输出!** 56 | - `exec()` 需要保留 `fd_table[0]` / `fd_table[1]` / `fd_table[2]`: 57 | ```c 58 | for (int fd = 0; fd < 3; fd++) { 59 | if (task->files->fd_table[fd] == NULL) { 60 | task->files->fd_table[fd] = open_terminal(); // 防止 `stdin` 丢失 61 | } 62 | } 63 | ``` 64 | 65 | --- 66 | 67 | ### **✅ 3. 在 `dup2()` 里支持 `stdin` / `stdout` 重定向** 68 | - **如果进程希望改变 `stdin` / `stdout`,需要 `dup2()` 实现重定向**: 69 | ```c 70 | int dup2(int oldfd, int newfd) { 71 | struct file *file = current->files->fd_table[oldfd]; 72 | if (file) { 73 | current->files->fd_table[newfd] = file; 74 | } 75 | } 76 | ``` 77 | 💡 **示例:** `ls > output.txt` 78 | ```c 79 | int fd = open("output.txt", O_WRONLY | O_CREAT); 80 | dup2(fd, STDOUT_FILENO); // `stdout` 变成 `output.txt` 81 | execlp("ls", "ls", NULL); 82 | ``` 83 | 84 | --- 85 | 86 | ## **🎯 结论** 87 | 1. **每个进程都会有 `stdin` / `stdout` / `stderr` 文件描述符(FD `0,1,2`),默认指向终端(`/dev/tty`)。** 88 | 2. **`exec()` 不会清理 `stdin` / `stdout`,否则新进程将无法输入/输出!** 89 | 3. **`dup2()` 允许进程修改 `stdin` / `stdout`,支持 `ls > output.txt` 这样的重定向。** 90 | 4. **你的代理内核可以让 `stdin` / `stdout` 默认指向 `htif_console`,模拟 UNIX 进程行为!** 91 | 92 | 🚀 你现在的代理内核 `exec()` 里 `stdin` / `stdout` 是否会丢失?需要加 `fd_table` 机制吗?🔥 -------------------------------------------------------------------------------- /doc/实验概述.md: -------------------------------------------------------------------------------- 1 | # 实验概述 2 | ## 一、总览 3 | 4 | 本次实验的主要目标是构建并验证一个基于 RISC-V 架构的代理内核程序。这个内核通过对底层硬件资源进行虚拟化管理,为上层用户程序提供一个安全、隔离且易于扩展的执行环境。实验体系结构采用三级嵌套模式,整体架构如下: 5 | 6 | **Spike 仿真器 → RISC-V 代理内核 → 用户程序** 7 | 8 | ## 二、参考资料 9 | - [Spike中HTIF的原理](../doc/Spike中HTIF的原理.md) 10 | 11 | 12 | 13 | ## 三、Spike 仿真器 14 | 15 | ### 角色与功能 16 | Spike 仿真器作为最底层运行环境,模拟真实 RISC-V 处理器的硬件行为和指令执行。它提供了一套完整的指令集模型,使得在不依赖实际硬件的平台上也能进行 RISC-V 内核和应用程序的调试、验证和功能测试。 17 | 18 | ### 作用 19 | 在本实验中,Spike 仿真器负责接收来自上层代理内核的指令,并与实际内核通信,模拟执行相关硬件操作和系统调用,成为实验平台的“物理层”,确保上层软件运行时能够与底层硬件模型进行正确交互。 20 | 21 | ## 四、 RISC-V 代理内核 22 | 23 | ### 核心设计 24 | 代理内核位于 Spike 仿真器与用户程序之间,其设计理念是通过虚拟化技术对硬件资源进行抽象和管理。代理内核负责拦截和处理用户程序的系统调用请求、管理虚拟内存、调度执行以及分配其他系统资源。 25 | 26 | ### 主要职责 27 | - **内存虚拟化与管理**:代理内核为用户程序创建并维护独立的虚拟地址空间。通过页表等数据结构实现虚拟地址与物理地址之间的映射,保证用户程序获得的内存区域互相独立,且不直接暴露底层物理资源。 28 | - **资源分配与隔离**:除内存之外,代理内核还负责管理其他关键资源(如 CPU 时间、I/O 资源等),并对用户程序进行隔离,防止程序之间的相互干扰。 29 | - **系统调用与异常处理**:内核为用户程序提供统一的系统调用接口,用户程序在执行过程中遇到异常或资源请求时,均由代理内核统一处理并调度相应的操作。 30 | 31 | ### 优势与应用 32 | 通过代理内核的虚拟化管理,不仅可以提高系统的安全性和稳定性,而且为实验教学提供了一个简化但真实的内核设计平台,使学生能够直观地理解操作系统核心机制(如内存管理、进程调度、系统调用接口等)的实现原理。 33 | 34 | ## 五、用户程序 35 | 36 | ### 运行环境 37 | 用户程序作为实验中的应用层,运行在代理内核提供的虚拟化环境之上。其所有的内存、I/O 等资源均由代理内核进行动态分配和管理,用户程序通过标准接口(类似传统操作系统中的系统调用)与内核进行交互。 38 | 39 | ### 开发与调试 40 | 用户程序可以是特定功能的实验验证代码,也可以作为内核虚拟化管理效果的展示。程序开发者无需关心底层硬件细节,而专注于业务逻辑的实现,同时也可以借助 Spike 仿真器和调试工具,对整个系统的行为进行监控和调试。 41 | 42 | 43 | ## 六、系统工作流程 44 | 45 | ### 1. 启动阶段 46 | - Spike 仿真器启动,并加载 RISC-V 代理内核映像。 47 | - 代理内核初始化自身数据结构,包括虚拟内存管理、系统调用表、资源管理模块等。 48 | 49 | ### 2. 用户程序加载与虚拟化分配 50 | - 在内核初始化完成后,代理内核根据用户程序的需求(例如加载 ELF 格式的可执行文件),为用户程序分配独立的虚拟地址空间。 51 | - 通过建立页表和分段机制,代理内核将用户程序的代码、数据及堆栈区域映射到预设的(其实就是代理内核在主机上的)物理内存区域,同时确保不同用户程序之间的资源隔离。 52 | 53 | ### 3. 执行与交互 54 | - 用户程序运行过程中,当遇到内存访问、I/O 操作或其他需要操作系统支持的请求时,会通过系统调用进入代理内核。 55 | - 代理内核根据调用类型进行处理,例如分配更多内存、进行设备模拟或执行特定的调度策略,处理完成后再将控制权返回给用户程序。 56 | - Spike 仿真器则负责模拟实际硬件对这些操作的响应,保证整个虚拟化过程与真实硬件平台的一致性。 57 | 58 | ### 4. 异常与退出处理 59 | - 当用户程序运行过程中出现异常或终止请求时,代理内核负责捕获系统调用和处理相关异常信息,确保系统资源得到及时回收并记录相关日志。 60 | - 最终,代理内核在完成所有必要清理工作后,将控制权返还给 Spike 仿真器,完成整个程序的生命周期。 61 | 62 | 63 | 64 | ## 总结 65 | 66 | 本次 RISC-V 代理内核实验通过构建一套从 Spike 仿真器到代理内核,再到用户程序的三级嵌套系统,成功实现了内存和其他资源的虚拟化分配。实验不仅让学生直观地了解了操作系统核心技术(如内存管理、系统调用、资源隔离)的实现原理,也为进一步研究 RISC-V 平台上的轻量级虚拟化技术提供了坚实的实践基础。通过这种架构设计,用户程序可以在一个安全、独立的虚拟环境中运行,而代理内核则成为连接硬件抽象层与用户应用层的关键桥梁。 67 | -------------------------------------------------------------------------------- /doc/异步信号量的实现.md: -------------------------------------------------------------------------------- 1 | 对!你抓住了核心本质!✨ 2 | 3 | 进程不会**瞬间**(啪地一下😂)跳过去执行信号处理,而是**在调度点**(如内核态切换、系统调用返回、主动 `sigpending()` 检查等)才会对信号做出反应。 4 | 5 | 也就是说: 6 | - 信号是 **异步的**,但进程的 **响应是同步的**(在安全的时间点执行)。 7 | - **不是** CPU 在执行普通指令时,突然“啪”地一下跳转到信号处理函数,而是 **内核在进程切换点检查 `pending signals` 并触发信号处理**。 8 | 9 | --- 10 | 11 | ## **🔎 具体发生了什么?** 12 | 假设进程 `A` 正在运行,父进程用 `kill(A, SIGUSR1)` 发送信号: 13 | 1. **内核将 `SIGUSR1` 添加到 `A` 的 `pending signals`(挂起信号集)**,但 `A` 并不会立刻跳转执行信号处理。 14 | 2. `A` 继续运行,直到: 15 | - 进入 **系统调用**(如 `read()`、`sleep()`、`wait()`)。 16 | - 进入 **调度点**(如 `schedule()`)。 17 | - **显式调用 `sigpending()` 或 `sigwaitinfo()` 检查信号**。 18 | 3. 这时,内核才会: 19 | - **如果 `SIGUSR1` 有信号处理函数,就跳转到处理函数。** 20 | - **如果是 `SIGKILL` 之类的不可忽略信号,立刻终止进程。** 21 | - **如果信号被屏蔽,先不处理,直到进程解除屏蔽。** 22 | 23 | 所以,**信号是一种“借机打断”机制,而不是“瞬间打断”**。 24 | 25 | --- 26 | 27 | ## **🎯 关键点** 28 | - **信号是异步触发的,但进程必须在适当的时机处理它**。 29 | - **不是随时都能“啪”跳过去,而是要等到进程进入调度点**(如系统调用返回、调度切换等)。 30 | - **屏蔽信号不会影响发送,但会延迟处理**,直到进程主动解除屏蔽。 31 | 32 | --- 33 | 34 | ## **🌟 举个形象的例子** 35 | 想象一个学生 `A` 在教室里认真写作业(进程正在运行)。这时: 36 | 1. **老师(父进程)敲黑板**(发送 `SIGUSR1`),但 `A` **不会立刻抬头**,因为他正在写作业(CPU 正在执行指令)。 37 | 2. **`A` 继续写作业,直到老师走近他身边,或者等到课间休息**(进程进入调度点)。 38 | 3. **这时 `A` 终于抬头回应老师(处理信号)**,如果老师让他继续写,他就回去写作业(信号处理后返回正常执行)。 39 | 40 | 如果 `A` 事先戴上了降噪耳机(`sigprocmask(SIG_BLOCK, &mask, NULL)` 屏蔽信号),那么: 41 | - **老师敲黑板无效**(信号被屏蔽)。 42 | - 但**老师的命令仍然被记录**,等 `A` 自己摘下耳机(解除屏蔽),再一起处理。 43 | 44 | --- 45 | 46 | ## **🚀 你的 `exec` 代码里可能遇到的问题** 47 | 1. **如果 `exec` 之后新进程还被屏蔽了某些信号(因为 `sigprocmask` 继承了),可能导致信号无法及时处理**。 48 | - 解决方案:在 `exec` 之后显式调用 `sigprocmask(SIG_UNBLOCK, &mask, NULL)` 解除屏蔽。 49 | 50 | 2. **如果 `exec` 之前进程有 `sigaction` 处理函数,在 `exec` 之后这些处理函数会被清除,导致信号回到默认行为**。 51 | - 解决方案:在 `exec` 之后重新 `sigaction()` 注册需要的信号处理。 52 | 53 | --- 54 | 55 | ### **结论** 56 | 你的理解是完全正确的! 57 | - 进程不会“啪”地一下跳到信号处理,而是等到 **合适的时间点** 再处理。 58 | - 这就是 **异步信号 vs. 进程同步调度** 的区别!💡 59 | 60 | 如果你在 `exec` 里遇到奇怪的信号行为,可以看看是不是 **屏蔽了信号,或者 `sigaction` 没有重新设置**!🚀 -------------------------------------------------------------------------------- /doc/操作系统的黑盒子视角.md: -------------------------------------------------------------------------------- 1 | 你的理解很有道理!从一个高度抽象的角度来看,操作系统确实可以被视作一个**响应外部中断、内部中断(如异常)、以及系统调用的反应集合**。如果我们完全忽略内部的实现细节,单纯当作一个**黑盒子**来考虑,操作系统就是通过对这些事件的响应来管理硬件和软件资源。 2 | 3 | ### 抽象理解: 4 | 1. **外部中断**:来自硬件的中断,比如 **时钟中断**(定时器触发的中断)、**I/O 中断**(如输入设备、网络、硬盘等的请求)。 5 | 2. **内部中断(异常)**:由程序运行时的错误或特殊情况引起的中断,比如 **除零错误**、**内存访问错误**(Segmentation Fault)、**系统调用**等。 6 | 3. **系统调用**:用户程序请求内核提供某种服务,如文件操作、进程控制、内存分配等。 7 | 8 | ### 这样来看,操作系统的职责就变成了: 9 | - **响应外部中断**:比如硬件设备发出的中断,操作系统需要中断当前任务,处理中断请求(例如:硬盘读写完成、键盘输入等)。 10 | - **响应内部中断**:如程序的异常或错误,操作系统需要处理异常,保持系统的稳定性,或者通过系统调用向用户程序提供支持。 11 | - **管理进程和资源**:操作系统需要通过这些响应机制管理和调度进程,分配资源,确保系统运行的高效和安全。 12 | 13 | ### 在实现操作系统时: 14 | - 你需要实现中断处理机制(如何中断当前进程并切换到中断服务例程)。 15 | - 需要设计**系统调用接口**,用户程序通过这些接口来请求内核服务。 16 | - 需要编写调度器和内存管理器,确保在处理中断和系统调用时能高效地分配和释放资源。 17 | 18 | 这种“黑盒子”看法有助于我们在设计操作系统的初期,从更高的层面理解操作系统的职责和功能。具体的内部实现(如中断向量表、进程调度算法等)可以在此基础上逐步展开和优化。 19 | 20 | 是不是有点开窍了? -------------------------------------------------------------------------------- /doc/数据段、堆栈和内存.md: -------------------------------------------------------------------------------- 1 | # 数据段和堆栈、内存 2 | 3 | ## 1. 栈(Stack) 4 | 栈是内存中为局部变量、函数调用及其相关数据(如返回地址、保存的寄存器状态)提供临时存储的区域。 5 | 6 | ### 栈的工作原理: 7 | - **每次函数调用时,栈都会“扩展”**: 8 | 当一个函数被调用时,栈会为该函数分配一块新的内存区域,存储局部变量、函数参数、返回地址等信息。这部分内存被称为“栈帧”。函数返回时,栈指针返回上一个栈帧的位置。 9 | - **局部变量的存储**: 10 | 每个函数的局部变量都会存放在栈上,函数执行时,局部变量的值会被推入栈中,函数返回时,栈上的这部分内存会被回收。 11 | - **栈指针和帧指针**: 12 | - 栈指针(SP):栈顶的位置,指示栈中最近分配的位置。 13 | - 帧指针(FP):指向当前栈帧的开始位置,帮助处理函数调用时的局部变量和返回地址。 14 | 15 | ### 栈的作用: 16 | - **局部变量**:函数调用时,局部变量会保存在栈中,退出时清除。 17 | - **函数调用的上下文**:栈会存储每个函数的返回地址、保存的寄存器状态等,以便程序从一个函数返回时恢复执行。 18 | 19 | ### 栈的优缺点: 20 | - **优点**:栈的分配和回收高效,每次函数调用和返回时操作非常简单。 21 | - **缺点**:栈的大小有限,可能会发生栈溢出(例如递归太深导致栈空间耗尽)。 22 | 23 | --- 24 | 25 | ## 2. 堆(Heap) 26 | 堆是内存中的另一块区域,专门用于动态分配内存。 27 | 28 | ### 堆的工作原理: 29 | - **动态内存分配**: 30 | 程序通过 `malloc()` 等函数请求堆内存,堆内存的大小在运行时动态决定。 31 | - **内存分配与释放**: 32 | 堆内存必须由程序员手动释放(例如 `free()` 函数),否则会导致内存泄漏。 33 | - **堆指针**: 34 | 操作系统或内存管理器通过堆指针管理堆内存空间。 35 | 36 | ### 堆的作用: 37 | - **全局变量和静态变量**:堆可以用于动态分配需要持久化的数据(如链表、树结构等)。 38 | - **动态数据结构**:适合存储大小不确定、生命周期不明确的对象,如动态数组、链表、图等。 39 | 40 | ### 堆的优缺点: 41 | - **优点**:堆的内存分配非常灵活,可以动态分配任意大小的内存。 42 | - **缺点**:堆内存的分配和回收比栈慢,容易产生内存碎片。 43 | 44 | --- 45 | 46 | ## 3. 全局变量与静态变量的存储 47 | 全局变量和静态变量存储在内存的 **数据段(Data Segment)** 中。数据段可以分为初始化数据段和未初始化数据段(BSS 段): 48 | - **初始化数据段**:存储程序中已初始化的全局变量和静态变量。 49 | - **BSS 段**:存储程序中未初始化的全局变量和静态变量,程序加载时初始化为零。 50 | 51 | ### 内存布局概述 52 | 内存的整体布局从高地址到低地址大致如下: 53 | 1. **内核空间(Kernel Space)**:操作系统管理的内存空间,程序无法直接访问。 54 | 2. **堆(Heap)**:用于动态内存分配。 55 | 3. **BSS 段**:未初始化的全局变量和静态变量。 56 | 4. **数据段(Data Segment)**:已初始化的全局变量和静态变量。 57 | 5. **栈(Stack)**:用于函数调用、局部变量、返回地址等。 58 | 59 | --- 60 | 61 | ## 4. 如何协作工作? 62 | - **栈**:每次函数被调用时,局部变量存放在栈上,函数执行完毕后栈上的变量被清除。栈是临时存储局部数据的地方,每个函数都有自己的栈帧。 63 | - **堆**:当程序需要在运行时动态分配内存时,会通过堆分配内存,堆中的内存持续到程序显式释放或程序结束时由操作系统清理。 64 | - **全局和静态变量**:这些变量通常由编译器和链接器放置在数据段中,它们在程序执行期间始终存在,生命周期贯穿整个程序执行。 65 | 66 | --- 67 | 68 | ## 5. 总结 69 | - **栈**:专门服务于局部变量和函数调用的上下文,分配快速,但空间有限,适合处理函数的局部数据。 70 | - **堆**:动态分配内存,适合处理生命周期较长且大小不确定的数据结构,分配灵活但较为耗时,并且需要手动管理内存释放。 71 | - **全局和静态变量**:存放在数据段(Data Segment)和 BSS 段,不属于堆或栈,生命周期贯穿整个程序的执行。 72 | -------------------------------------------------------------------------------- /doc/文件系统的工作方式.md: -------------------------------------------------------------------------------- 1 | 文件系统是操作系统管理存储设备的核心模块,其工作流程涉及多个关键数据结构与分层抽象机制。以下从用户程序接口到内核实现的完整链路进行解析: 2 | 3 | ### 一、用户接口层:文件描述符(File Descriptor) 4 | 1. **文件描述符的本质** 5 | 文件描述符(fd)是用户程序访问文件的句柄,本质是进程文件描述符表的下标(0-1023)。该表存储指向`struct file`的指针,每个表项对应一个打开的文件。例如,标准输入/输出/错误分别对应fd 0/1/2。 6 | 7 | 2. **struct file结构体** 8 | 内核通过`struct file`维护文件访问状态,包含: 9 | - `f_pos`:当前读写偏移量 10 | - `f_mode`:访问模式(读/写) 11 | - `f_flags`:打开标志(O_RDONLY等) 12 | - `f_count`:引用计数(多进程共享时递增) 13 | - `f_op`:指向具体文件系统的操作函数集(如ext4_file_operations) 14 | 15 | ### 二、进程文件管理:files_struct与procfs 16 | 1. **进程级文件管理块(files_struct)** 17 | 每个进程的PCB中包含`files_struct`结构体,核心字段: 18 | - `fd_array`:文件描述符表(默认大小32,动态扩展) 19 | - `open_fds`:位图标记已分配的fd 20 | - `count`:引用计数(用于fork时的写时复制) 21 | 22 | 2. **procfs的动态映射** 23 | `/proc/[pid]/fd`目录通过proc文件系统动态生成,其虚拟文件(如`/proc/self/fd/0`)指向实际文件描述符,允许用户态直接查看进程打开的文件句柄。 24 | 25 | ### 三、VFS抽象层:dentry与vinode 26 | 1. **目录项(dentry)** 27 | dentry是内存中的目录缓存,核心作用: 28 | - 维护文件名到inode的映射 29 | - 构建目录树结构(通过`d_parent`和`d_subdirs`链表) 30 | - 加速路径解析(如`/home/user/file`分解为多个dentry节点) 31 | 32 | 2. **虚拟索引节点(vinode)** 33 | VFS通过`struct inode`抽象文件元数据: 34 | - `i_size`:文件大小 35 | - `i_blocks`:占用磁盘块数 36 | - `i_fop`:文件操作函数集(如ext4_dir_inode_operations) 37 | - `i_sb`:指向超级块(关联具体文件系统) 38 | 39 | ### 四、VFS与实际文件系统的协作 40 | 1. **VFS的分层设计** 41 | VFS通过四大核心对象实现多文件系统兼容: 42 | - **超级块(super_block)**:描述文件系统整体信息(如块大小、挂载点) 43 | - **inode**:跨文件系统的元数据抽象 44 | - **dentry**:目录树内存缓存 45 | - **file**:文件打开状态管理 46 | 47 | 2. **实际文件系统的挂载** 48 | 具体文件系统(如ext4)通过注册`file_system_type`结构体实现挂载。例如: 49 | ```c 50 | static struct file_system_type ext4_fs_type = { 51 | .owner = THIS_MODULE, 52 | .name = "ext4", 53 | .mount = ext4_mount, // 挂载回调函数 54 | .kill_sb = kill_block_super, 55 | }; 56 | ``` 57 | 挂载时,VFS调用`ext4_mount`初始化超级块,构建根目录dentry与inode。 58 | 59 | ### 五、完整工作流程示例(以read为例) 60 | 1. **用户调用`read(fd, buf, size)`** 61 | 系统调用通过`sys_read`进入内核,根据fd索引到`struct file`。 62 | 63 | 2. **VFS路由到具体文件系统** 64 | 通过`file->f_op->read_iter`调用实际文件系统的读函数(如ext4_file_read_iter)。 65 | 66 | 3. **数据流传递** 67 | 具体文件系统通过inode定位数据块,经块设备驱动读取磁盘数据,最终通过DMA或PIO将数据拷贝到用户缓冲区。 68 | 69 | ### 六、性能优化机制 70 | 1. **缓存策略** 71 | - **页缓存(Page Cache)**:缓存文件数据块,减少磁盘I/O 72 | - **dentry缓存(dcache)**:加速路径解析 73 | - **inode缓存(icache)**:复用频繁访问的元数据 74 | 75 | 2. **写回与日志** 76 | 文件系统(如ext4)通过日志(Journal)保证崩溃一致性:先将写操作记录到日志区,提交成功后再更新实际数据块。 77 | 78 | 通过以上分层协作,文件系统实现了从用户态接口到物理存储的高效管理,同时保持了对异构存储介质(磁盘、网络、内存)的兼容性。 -------------------------------------------------------------------------------- /doc/文件系统设计范式.md: -------------------------------------------------------------------------------- 1 | 文件系统采用 `dentry`(目录项)和 `vinode`(虚拟索引节点)对目录和文件进行抽象,主要基于以下核心设计目标和实际需求: 2 | 3 | --- 4 | 5 | ### 一、**统一接口,屏蔽底层差异** 6 | 1. **多文件系统兼容性** 7 | 不同文件系统(如 ext4、NTFS、NFS)的物理结构差异巨大,通过 `dentry` 和 `vinode` 抽象出统一的逻辑视图,使上层应用无需关心具体实现细节。例如,无论文件存储在本地磁盘还是网络设备,用户均可通过 `open()` 和 `read()` 等通用接口操作文件。 8 | 9 | 2. **虚拟文件系统(VFS)的基石** 10 | `vinode` 是 VFS 层对具体文件系统 inode 的抽象(例如 ext4 的 inode 或 NTFS 的 MFT 条目),而 `dentry` 维护目录树结构。两者共同构建了跨文件系统的统一命名空间,允许不同文件系统挂载到同一目录树中。 11 | 12 | --- 13 | 14 | ### 二、**高效管理目录结构** 15 | 1. **路径解析与树状组织** 16 | `dentry` 记录文件名、父目录和子目录关系,形成内存中的目录树缓存。例如,路径 `/home/user/file` 会被拆分为多个 `dentry` 节点(`/`、`home`、`user`、`file`),每个节点关联对应的 `vinode`。这种分层结构加速了路径查找,避免了每次访问都需遍历磁盘。 17 | 18 | 2. **硬链接与软链接支持** 19 | `dentry` 允许不同文件名(硬链接)指向同一 `vinode`,而 `vinode` 维护引用计数。例如,删除文件时,仅当 `vinode` 的引用计数归零才会释放磁盘空间。 20 | 21 | --- 22 | 23 | ### 三、**元数据与数据分离** 24 | 1. **`vinode` 集中管理文件元数据** 25 | `vinode` 存储文件大小、权限、时间戳、数据块位置等元信息,但不包含文件名(文件名由 `dentry` 管理)。这种分离设计使得同一文件(inode)可拥有多个别名(dentry),支持硬链接。 26 | 27 | 2. **优化存储与访问效率** 28 | 文件系统通过 `vinode` 直接定位数据块,而 `dentry` 缓存高频访问的目录项。例如,多次访问同一文件时,`dentry` 缓存可跳过磁盘 I/O,直接从内存获取路径信息。 29 | 30 | --- 31 | 32 | ### 四、**动态性与扩展性** 33 | 1. **内存与磁盘解耦** 34 | `dentry` 是纯内存结构,仅在文件访问时动态创建;`vinode` 则可能持久化到磁盘(如 ext4)或临时生成(如 procfs)。这种设计允许非磁盘文件系统(如 tmpfs)无缝集成到 VFS 中。 35 | 36 | 2. **支持复杂文件操作** 37 | 通过 `dentry_operations` 和 `inode_operations` 定义目录和文件的操作方法(如创建、删除、重命名),不同文件系统可自定义实现。例如,NFS 的 `dentry` 操作会触发网络通信,而 ext4 直接操作本地磁盘。 38 | 39 | --- 40 | 41 | ### 五、**性能优化与缓存机制** 42 | 1. **目录项缓存(dcache)** 43 | `dentry` 缓存近期访问的目录项,减少路径解析的磁盘 I/O。例如,重复访问 `/usr/bin` 下的文件时,`dentry` 缓存可跳过逐级目录查找。 44 | 45 | 2. **inode 缓存** 46 | 高频访问的 `vinode` 会被缓存,避免重复从磁盘读取元数据。例如,频繁打开同一文件时,可直接从内存获取 `vinode` 信息。 47 | 48 | --- 49 | 50 | ### 总结 51 | `dentry` 和 `vinode` 的抽象设计解决了文件系统的三大核心问题: 52 | 1. **异构兼容性**:统一不同文件系统的操作接口; 53 | 2. **性能优化**:通过内存缓存减少磁盘 I/O; 54 | 3. **功能扩展**:支持硬链接、网络文件系统等高级特性。 55 | 56 | 这种分层抽象是 Linux "一切皆文件" 哲学的技术实现基础,也是现代操作系统文件系统的通用设计范式。 -------------------------------------------------------------------------------- /doc/时钟中断的硬件实现原理.md: -------------------------------------------------------------------------------- 1 | # 时钟中断的硬件实现原理 2 | 3 | 时钟中断(Timer Interrupt)是操作系统和嵌入式系统中非常关键的功能,它通过硬件定时器生成周期性的中断信号,使得系统能够进行定时任务、调度任务、进程切换等操作。时钟中断通常由硬件定时器实现,确保系统按预定的时间间隔执行某些任务。在 RISC-V 架构中,时钟中断的硬件实现主要通过 CLINT(Core Local Interruptor) 模块中的寄存器和时钟机制来完成。 4 | 5 | ## 定时器寄存器与时钟递增 6 | 7 | 在 RISC-V 系统中,时钟中断的核心硬件组件是 CLINT 模块,它包含以下两个重要寄存器: 8 | 9 | - **CLINT_MTIME**:机器时间寄存器(Machine Time Register),用于记录从系统启动开始经过的时钟周期数。 10 | - **CLINT_MTIMECMP**:机器时间比较寄存器(Machine Time Compare Register),用于设置定时器中断的目标时间。 11 | 12 | ### 1. CLINT_MTIME 寄存器 13 | 14 | CLINT_MTIME 是一个 64 位的硬件寄存器,它随着每个时钟周期自动递增。该寄存器的值表示自系统启动以来所经过的时钟周期数。CLINT_MTIME 是由硬件自动递增的,不需要软件干预。每当一个时钟周期完成时,硬件会将 CLINT_MTIME 的值加一。 15 | 16 | 例如,在 1 GHz 时钟频率下,系统每秒会有 10^9 个时钟周期,因此 CLINT_MTIME 会在每秒自动递增 10^9。当 CLINT_MTIME 的值变化时,系统能够通过读取该寄存器获取当前的时间信息。 17 | 18 | ### 2. CLINT_MTIMECMP 寄存器 19 | 20 | CLINT_MTIMECMP 是另一个 64 位的硬件寄存器,它用于存储定时器中断的目标时间。软件通过将期望的中断触发时间写入 CLINT_MTIMECMP,来设置何时发生中断。当 CLINT_MTIME 的值大于或等于 CLINT_MTIMECMP 时,定时器中断将被触发。 21 | 22 | 具体来说,软件通过设置 CLINT_MTIMECMP 的值来确定定时器中断触发的时间点。例如,假设 CLINT_MTIME 当前值为 1000,软件可以将 CLINT_MTIMECMP 设置为 2000,这样在时钟周期数到达 2000 时,定时器中断就会触发。 23 | 24 | ## 定时器中断的触发 25 | 26 | 时钟中断的实际触发是基于 CLINT_MTIME 和 CLINT_MTIMECMP 寄存器的比较。当硬件检测到 CLINT_MTIME 的值已经大于或等于 CLINT_MTIMECMP 中存储的目标值时,定时器中断会被触发。具体的工作流程如下: 27 | 28 | 1. 硬件周期性地递增 CLINT_MTIME 寄存器的值。 29 | 2. 当 CLINT_MTIME >= CLINT_MTIMECMP 时,硬件生成一个中断请求(IRQ),通知处理器。 30 | 3. 如果处理器的中断控制寄存器中启用了定时器中断(例如,在机器模式下通过设置 MIE_MTIE 来使能定时器中断),处理器就会响应这个中断请求。 31 | 4. 响应中断后,操作系统或应用程序可以执行定时任务,进行进程调度、时间管理等操作。 32 | 33 | ## 时钟中断的应用 34 | 35 | 时钟中断在嵌入式系统和操作系统中有着广泛的应用,它为多任务处理、定时任务、时间管理等提供了硬件支持。具体应用如下: 36 | 37 | 1. **时间片轮转调度**:操作系统通过时钟中断实现进程的时间片轮转。当时钟中断发生时,操作系统会检查当前进程是否已运行完当前时间片,如果是,则进行进程切换。 38 | 2. **定时任务**:硬件定时器可以定期触发中断,操作系统或应用程序可以通过时钟中断定期执行一些任务,如心跳监测、传感器数据采集等。 39 | 3. **延迟操作**:软件可以通过设置 CLINT_MTIMECMP 来实现延迟操作。例如,延迟 N 秒后执行某个操作。 40 | 4. **高精度计时**:通过定期读取 CLINT_MTIME 和 CLINT_MTIMECMP,系统可以进行高精度的时间测量,确保任务的定时执行。 41 | 42 | ## 定时器中断的优化 43 | 44 | 尽管硬件定时器提供了一个非常有效的定时机制,但在高负载情况下,时钟中断的处理可能会影响系统的性能。为了优化时钟中断,常见的技术包括: 45 | 46 | 1. **定时器中断合并**:为了减少中断的频率,多个定时器中断可能被合并为一个。操作系统可以通过软中断机制来处理这些合并的中断。 47 | 2. **动态调整时钟频率**:根据负载的不同,系统可以动态调整时钟频率。高负载时增加时钟频率以提高响应速度,低负载时降低频率以节省能源。 48 | 3. **中断优先级**:系统可以根据中断的重要性设置中断优先级,确保关键任务能够获得及时处理。 49 | 50 | ## 总结 51 | 52 | 时钟中断是现代操作系统和嵌入式系统中至关重要的功能,它通过硬件定时器和寄存器实现周期性的中断,支持系统进行任务调度、时间管理等。RISC-V 架构中的 CLINT_MTIME 和 CLINT_MTIMECMP 寄存器提供了定时器中断的基本硬件支持,通过硬件自动递增的时钟周期和与之比较的目标值,系统能够在预定时间触发中断,实现精准的定时任务执行。时钟中断的硬件实现原理为操作系统提供了强大的时间管理和调度能力,是嵌入式系统和操作系统设计的基石。 53 | -------------------------------------------------------------------------------- /doc/硬中断到软中断的切换过程.md: -------------------------------------------------------------------------------- 1 | # 硬中断到软中断的处理过程 2 | 3 | 在 RISC-V 架构中,中断处理是通过硬件和软件的结合来实现的。硬件中断触发时,首先会由硬件完成一些基本的操作(如中断原因寄存器的更新),然后根据特权级的不同,操作系统会将某些中断请求传递给高特权级进行处理。下面我们结合先前的回答,详细阐述硬中断到软中断的处理流程。 4 | 5 | ## 1. 硬件中断触发与 mcause 设置 6 | 7 | 硬件中断(如外部时钟中断)在 M模式(机器模式)触发时,CPU 会自动执行以下操作: 8 | 9 | - **中断触发**:当硬件中断发生时,硬件会自动切换到 M模式,并执行相应的中断处理程序。此时,硬件会把中断的类型(如定时器中断、外部中断等)记录到 mcause 寄存器中。mcause 寄存器是用于记录机器模式下中断或异常的原因。 10 | - 例如,当发生定时器中断时,mcause 会记录定时器中断的标识(如 CAUSE_MTIMER_S_TRAP),指示发生的是定时器中断。 11 | 12 | - **自动处理**:mcause 的赋值过程完全由硬件自动完成,操作系统或应用程序无需手动设置。硬件会在中断发生时根据中断的来源和类型自动更新该寄存器。 13 | 14 | ## 2. 硬中断处理与 scause 的同步 15 | 16 | 在 M模式处理完硬件中断后,操作系统通常需要将某些硬中断请求传递给 S模式(如进程调度)。为了将控制权交给 S模式,硬件会执行以下操作: 17 | 18 | - **设置软中断标志**:在 M模式的中断处理程序(例如 handle_mtrap())中,硬件通过设置 SIP 寄存器中的标志位(例如 SIP_SSIP)来标记有软中断待处理。这通常发生在定时器中断等硬件中断需要由操作系统在 S模式中进一步处理时。 19 | 20 | - **同步 scause**:在从 M模式转到 S模式时,硬件会自动将 mcause 中的中断原因同步到 scause 寄存器中。这个过程是硬件自动完成的,不需要软件手动干预。这样,S模式的中断处理程序(如 smode_trap_handler())可以读取 scause 来确定是哪种类型的中断或异常发生。 21 | 22 | ## 3. M模式到 S模式的切换 23 | 24 | 当 M模式的中断处理完成后,CPU 会通过 mret 指令返回到之前的特权级。如果该特权级是 S模式(Supervisor Mode),控制权会转交给 S模式的中断处理程序: 25 | 26 | - **mret 指令**:mret 会恢复 M模式中保存的上下文,并将控制权转交给 S模式。此时,scause 中已经包含了来自 M模式的中断原因,S模式的中断处理程序可以根据该原因执行进一步的操作。 27 | 28 | - **S模式的软中断处理**:一旦进入 S模式,操作系统会检查 SIP_SSIP 位,判断是否存在需要处理的软中断。如果该位被设置,表示存在来自 M模式的软中断请求,操作系统会进入软中断处理流程(例如进程调度)。 29 | - 在 smode_trap_handler() 中,操作系统会通过读取 scause 来确定是来自 M模式的定时器中断、系统调用还是其他软中断。 30 | 31 | ## 4. 软中断的处理 32 | 33 | 在 S模式下,软中断的处理通常涉及与操作系统相关的任务,如时间片调度和进程切换。常见的软中断处理包括: 34 | 35 | - **系统调用**:如果 scause 显示是由用户程序发起的系统调用引起的,操作系统会通过相应的处理函数(如 handle_syscall())来执行系统调用。 36 | 37 | - **定时器中断**:如果 scause 显示是定时器中断引起的,操作系统会进入定时器中断的处理逻辑(如 handle_mtimer_trap()),执行与时间片相关的任务,如进程调度或定时任务。 38 | 39 | ## 总结:硬中断到软中断的切换 40 | 41 | 1. **硬中断发生**:硬件中断触发时,mcause 会被硬件自动设置为中断原因。操作系统根据需要处理硬件中断。 42 | 43 | 2. **软中断标志设置**:M模式的硬件中断处理完成后,硬件会通过设置 SIP 寄存器的标志位,将软中断请求传递给 S模式。 44 | 45 | 3. **scause 的同步**:硬件在从 M模式返回到 S模式时,会将 mcause 中的中断原因同步到 scause,供 S模式的中断处理程序使用。 46 | 47 | 4. **软中断处理**:在 S模式,操作系统会根据 scause 判断软中断类型,并执行相应的软中断处理程序,如进程调度或系统调用。 48 | 49 | 通过这种硬中断与软中断的分离处理机制,RISC-V 架构能够有效地管理不同特权级的中断,并确保操作系统在 S模式中正确地执行与进程调度、定时任务等相关的工作。 -------------------------------------------------------------------------------- /doc/硬件对硬中断的响应过程.md: -------------------------------------------------------------------------------- 1 | # 硬件对硬中断的响应过程 2 | 3 | 本文档详细介绍了在 RISC-V 架构下,硬件中断(包括同步异常和异步中断)的触发条件,以及操作系统响应这些中断时的处理流程。需要注意的是,在 RISC-V 架构中,硬件中断和异常的入口地址由控制寄存器 mtvec 决定。 4 | 5 | --- 6 | 7 | ## 1. 概述 8 | 9 | 在 RISC-V 系统中,中断和异常可以分为两大类: 10 | - **同步异常**:由当前指令执行时发生错误或特殊情况立即触发,例如非法指令、特权级不符、地址对齐错误、缺页异常等。 11 | - **异步中断**:由外部设备(例如定时器、I/O 设备)独立于当前指令执行触发的中断请求。 12 | 13 | 当异常或中断发生时,RISC-V CPU 会自动保存当前的执行上下文(如程序计数器和状态寄存器),并根据 mtvec 寄存器中设置的入口地址跳转到相应的异常处理程序。 14 | 15 | --- 16 | 17 | ## 2. 同步异常触发情况 18 | 19 | ### 2.1 非法指令异常 20 | - **触发条件**:当 CPU 尝试执行未定义或非法的指令时触发。 21 | - **示例**:执行错误编码的指令或由于存储错误导致的指令损坏。 22 | 23 | ### 2.2 特权级不符异常 24 | - **触发条件**:在较低特权级(如用户模式)下执行仅允许在高特权级(如机器模式或监督模式)下运行的指令时触发。 25 | - **示例**:用户程序试图修改控制寄存器或执行禁止的系统操作。 26 | 27 | ### 2.3 算术异常(视具体实现而定) 28 | - **触发条件**:某些体系结构中,诸如除以零、算术溢出等错误可能触发算术异常。 29 | - **示例**:除以零(注意:在 RISC-V 标准中,整数除法的除以零行为通常是定义好的,不一定会触发异常,但浮点运算可能会触发异常)。 30 | 31 | ### 2.4 地址对齐异常 32 | - **触发条件**:当数据访问地址不满足对齐要求时触发。 33 | - **示例**:尝试以 4 字节对齐方式加载数据,而地址未对齐。 34 | 35 | ### 2.5 地址访问异常 36 | - **触发条件**:当程序试图访问不存在、不可用或无权限访问的内存地址时触发。 37 | - **示例**:空指针解引用、数组越界访问。 38 | 39 | ### 2.6 缺页异常 40 | - **触发条件**:在启用虚拟内存系统时,进程访问的虚拟地址没有对应的物理页面。 41 | - **响应流程**:操作系统捕获缺页异常后,将加载缺失的页面、更新页表,并重新执行导致异常的指令。 42 | 43 | ### 2.7 其他同步异常 44 | - **示例**:浮点异常(例如无效浮点操作或溢出)、系统调用异常(由 ecall 指令引发,虽然是软件触发,但在硬件层面同样经过异常处理流程)。 45 | 46 | --- 47 | 48 | ## 3. 异步中断触发情况 49 | 50 | ### 3.1 外部设备中断 51 | - **触发条件**:外部设备(如键盘、鼠标、网络接口、磁盘控制器等)通过中断控制器向 CPU 发送中断请求。 52 | - **响应流程**:硬件中断信号经过中断控制器传递给 CPU,CPU 根据 mtvec 的设置跳转到中断处理入口。 53 | 54 | ### 3.2 定时器中断 55 | - **触发条件**:由系统定时器产生的中断,用于实现任务调度、时间片轮转等。 56 | - **响应流程**:定时器中断触发后,CPU 跳转到定时器中断处理入口,操作系统调度器据此触发任务切换。 57 | 58 | --- 59 | 60 | ## 4. 操作系统响应硬件中断的过程 61 | 62 | 当硬件中断或异常触发时,整个响应过程通常分为以下几个阶段: 63 | 64 | ### 4.1 硬件自动保存上下文并跳转异常入口 65 | 1. **保存程序计数器** 66 | ◦ CPU 自动将当前指令地址(PC)保存到 sepc 寄存器中,以记录触发异常时的指令位置。 67 | 68 | 2. **保存状态信息** 69 | ◦ 部分状态寄存器(如 sstatus 或 mstatus)中的关键位(例如特权级标志)也会被保存,以便后续恢复现场。 70 | 71 | 3. **记录异常原因** 72 | ◦ scause 寄存器记录了触发异常或中断的原因(例如非法指令、缺页异常等)。 73 | ◦ stval 寄存器在相关情况下记录导致异常的地址信息(如未对齐地址或缺页地址)。 74 | 75 | 4. **禁用中断** 76 | ◦ 为防止嵌套中断,CPU 在进入异常处理前通常会自动禁用中断。 77 | 78 | 5. **跳转到异常向量** 79 | ◦ CPU 根据 mtvec 寄存器中配置的入口地址跳转到异常处理程序的入口代码。这是整个异常处理流程的第一步。 80 | 81 | ### 4.2 异常向量入口处理 82 | 在异常向量入口处(通常由汇编代码实现),执行以下步骤: 83 | - **保存完整上下文** 84 | 汇编入口代码将所有通用寄存器的值保存到当前进程的 trapframe 中,确保用户程序的所有状态得以保存。 85 | 86 | - **切换至内核栈** 87 | 根据当前进程的调度信息,切换到对应的内核栈,以便在安全的内核环境中处理异常。 88 | 89 | - **调用具体的异常处理函数** 90 | 异常向量入口代码会调用具体的内核异常处理函数(如 mt_trap_handler 或其他自定义函数),传递 trapframe 指针等参数。 91 | 92 | ### 4.3 内核异常处理函数 93 | 在内核异常处理函数中,操作系统根据异常原因(由 scause 提供)进行分发处理: 94 | - **系统调用处理** 95 | 如果异常原因是由 ecall 指令引发,调用系统调用处理函数(如 handle_syscall)。通常会对 trapframe 中的 PC 进行自增(如加 4),以跳过 ecall 指令后恢复执行。 96 | 97 | - **缺页异常处理** 98 | 针对缺页异常,操作系统会定位缺失页面,尝试加载页面数据(可能从磁盘或交换区调入),并更新页表。如果无法恢复,则终止进程。 99 | 100 | - **非法指令、特权级异常等错误处理** 101 | 对于严重错误,内核会记录详细日志,可能输出调试信息,并根据情况终止当前进程或系统,以防止错误扩散。 102 | 103 | - **其他异常处理** 104 | 根据具体异常类型执行对应处理逻辑,确保错误被妥善处理。 105 | 106 | ### 4.4 返回用户模式 107 | 异常处理完成后,操作系统将: 108 | - **恢复 trapframe 中保存的用户上下文** 109 | 将 trapframe 中保存的寄存器、程序计数器等状态恢复到相应的 CPU 寄存器中。 110 | 111 | - **通过 sret 指令返回用户模式** 112 | 使用 sret(或其他适当指令,根据当前模式)指令从内核模式返回到用户模式,跳转到 trapframe 中保存的用户程序计数器地址,继续执行用户程序。 113 | 114 | --- 115 | 116 | ## 5. 总结 117 | 118 | 在 RISC-V 架构中,硬件中断和异常触发情况多种多样,既包括由当前指令执行错误引发的同步异常,也包括由外部设备触发的异步中断。 119 | - 当中断或异常发生时,CPU 会自动保存当前执行状态(PC、状态寄存器、异常原因等),并依据 mtvec 寄存器的设置跳转到异常入口。 120 | - 异常入口代码(通常以汇编实现)负责保存完整上下文、切换内核栈,并调用内核的异常处理函数。 121 | - 内核异常处理函数根据具体异常类型处理问题后,通过恢复用户上下文和执行 sret 指令,安全返回到用户模式继续执行程序。 122 | 123 | 这一系列精密的硬件与软件协同机制,确保了操作系统在面对各种异常和中断时,能够及时响应、有效处理,并保证系统的稳定运行。 -------------------------------------------------------------------------------- /doc/硬件对软中断的响应过程.md: -------------------------------------------------------------------------------- 1 | # 硬件对软中断的响应过程 2 | 3 | 在通过 `stvec` 跳转到 `smode_trap_vector` 之前,RISC-V CPU 会自动执行一系列硬件操作,这些操作是由硬件为了保存当前执行状态和准备跳转到异常处理程序(即 `smode_trap_vector`)所必须的。以下是这些自动发生的硬件过程: 4 | 5 | 1. **保存 `epc`(程序计数器)** 6 | 当发生中断或异常时,RISC-V 硬件会自动将当前程序计数器(PC)值存储到 `sepc` 寄存器中。`sepc` 保存了触发异常时的指令地址,通常是异常发生前一条指令的地址。 7 | 8 | 2. **保存 `sstatus`(状态寄存器)** 9 | 硬件会自动保存 `sstatus` 寄存器的当前值。`sstatus` 寄存器包含了处理器的特权级、当前的中断使能状态以及其他控制位。 10 | - **SSTATUS_SPP**:保存程序的先前特权级,指示进入异常时是来自于用户模式还是内核模式。 11 | - **SSTATUS_SPIE**:指示中断使能状态,特别是中断是否在异常发生时被禁用。 12 | 13 | 3. **保存 `scause`(异常原因寄存器)** 14 | 硬件会自动将异常的原因(例如,是来自用户程序的 `ecall`,还是其他异常)存储到 `scause` 寄存器中。`scause` 会指示导致跳转到异常处理程序的具体原因(例如 `CAUSE_USER_ECALL`,表示来自用户程序的系统调用)。 15 | 16 | 4. **保存 `stval`(异常地址寄存器)** 17 | 如果异常类型与地址相关(例如,页面错误、地址访问错误等),硬件会自动将错误的地址存储在 `stval` 寄存器中。对于大多数系统调用和普通异常,`stval` 的内容可能不被使用,但它会在发生地址错误或非法访问时提供额外信息。 18 | 19 | 5. **禁用中断** 20 | 当发生异常时,硬件会自动禁用中断,防止中断嵌套。这通常通过修改 `SSTATUS_SPIE` 来实现,确保在异常处理期间中断不会干扰。 21 | 22 | 6. **跳转到 `stvec`(异常向量寄存器)** 23 | `stvec` 寄存器用于指定异常处理的入口地址。RISC-V 硬件会使用 `stvec` 中存储的地址来跳转到异常处理程序。在这个实验中,`stvec` 在用户程序初始化时被设置为 `smode_trap_vector`,因此硬件在发生异常后,会跳转到 `smode_trap_vector` 的地址。 24 | 25 | --- 26 | 27 | ## 总结:硬件自动发生的操作 28 | 29 | 1. **保存当前程序计数器**:硬件会自动将当前的 PC 值存储到 `sepc`。 30 | 2. **保存状态信息**:硬件自动保存 `sstatus`、`scause` 和 `stval`,提供异常原因和相关上下文。 31 | 3. **禁用中断**:硬件禁用中断,防止异常期间的中断干扰。 32 | 4. **跳转到 `stvec` 指定的异常处理地址**:硬件根据 `stvec` 寄存器的设置,将控制权转交给指定的异常处理程序地址。 33 | 34 | 这些操作确保了系统能够安全地处理异常和中断,并在处理完异常后正确地恢复程序的执行。 35 | -------------------------------------------------------------------------------- /doc/软硬中断的触发方式.md: -------------------------------------------------------------------------------- 1 | # 中断触发机制概述 2 | 3 | 中断(Interrupt)是计算机系统中一种用于处理异步事件的机制。在 RISC-V 系统中,中断可以由多个来源触发,主要包括系统调用(ecall)和硬件异常(如缺页异常)。下面将详细介绍这些常见的触发方式。 4 | 5 | --- 6 | 7 | ## 1. 系统调用(ecall)触发中断 8 | 9 | ### 概述: 10 | 系统调用是用户程序通过软件触发的特殊中断,用于请求操作系统内核提供的服务。当用户程序需要操作系统提供的功能(如文件操作、内存分配、进程控制等)时,会通过 `ecall` 指令触发系统调用。 11 | 12 | ### 触发过程: 13 | 1. **用户程序发起 ecall**: 14 | 用户程序执行 `ecall` 指令时,硬件会触发异常,并将程序计数器(PC)值保存到 `sepc` 寄存器中。`sepc` 保存了导致异常的指令地址,便于异常处理程序执行完后恢复。 15 | 16 | 2. **硬件触发跳转到 S 模式**: 17 | 在 RISC-V 中,`ecall` 通常触发进入特权模式(S 模式)。硬件会将程序的控制权转交给指定的异常向量表地址,即 `stvec` 寄存器中存储的地址。该地址通常指向内核的异常处理入口。 18 | 19 | 3. **系统调用处理**: 20 | 进入 S 模式后,内核处理 `ecall` 异常,程序的上下文(如寄存器、堆栈等)被保存,然后跳转到内核的系统调用处理程序(如 `smode_trap_handler`)。内核根据 `scause` 等信息识别 `ecall` 来源,并将控制转交给相应的系统调用处理函数(如 `handle_syscall`)。 21 | 22 | 4. **恢复用户程序**: 23 | 系统调用处理完成后,内核通过修改 `sepc` 和 `sstatus` 寄存器,恢复到用户程序的执行环境,并返回到用户程序的下一条指令,继续执行。 24 | 25 | ### 相关寄存器: 26 | - **sepc**: 保存触发异常时的 PC。 27 | - **scause**: 保存异常的原因,`CAUSE_USER_ECALL` 表示来自用户的系统调用。 28 | - **sstatus**: 保存当前状态信息,如特权级、是否启用中断等。 29 | 30 | --- 31 | 32 | ## 2. 硬件异常(例如缺页异常)触发中断 33 | 34 | ### 概述: 35 | 硬件异常是由系统硬件在执行过程中检测到的错误或特殊情况(如非法操作、地址错误等)引发的。常见的硬件异常包括缺页异常(Page Fault)、非法指令、地址越界访问等。在 RISC-V 中,硬件异常会导致 CPU 进入特权模式(通常是 S 模式),并跳转到异常处理程序。以下将重点讨论缺页异常。 36 | 37 | ### 缺页异常: 38 | 缺页异常发生在进程访问的虚拟地址没有对应的物理页时。例如,程序访问一个尚未加载到内存的虚拟页面时,硬件会触发缺页异常。 39 | 40 | ### 触发过程: 41 | 1. **虚拟地址访问**: 42 | 程序尝试访问一个虚拟地址,该虚拟地址通过内存管理单元(MMU)与物理内存进行映射。如果该虚拟地址没有在物理内存中找到对应的页面,MMU 会产生缺页异常。 43 | 44 | 2. **硬件触发缺页异常**: 45 | MMU 检测到缺页后,会触发硬件异常,并将当前的程序计数器(PC)值保存到 `sepc` 寄存器中。异常原因会被存储在 `scause` 寄存器中,`stval` 会保存导致缺页的虚拟地址。 46 | 47 | 3. **跳转到异常处理程序**: 48 | 硬件会根据 `stvec` 中存储的地址跳转到异常处理程序。异常处理程序负责处理缺页异常,通常包括加载缺失的页面并更新页表。 49 | 50 | 4. **恢复执行**: 51 | 异常处理程序完成后,恢复程序执行。页表更新后,重新执行原本的指令,虚拟地址成功映射到物理地址,程序继续执行。 52 | 53 | ### 相关寄存器: 54 | - **sepc**: 保存触发异常时的 PC(即导致异常的指令地址)。 55 | - **scause**: 保存异常的原因,`CAUSE_FETCH_PAGE_FAULT` 和 `CAUSE_LOAD_PAGE_FAULT` 分别表示指令和数据访问时的缺页异常。 56 | - **stval**: 保存导致缺页的虚拟地址。 57 | - **sstatus**: 保存当前的状态信息。 58 | 59 | ### 示例: 60 | 假设程序访问了一个未加载的虚拟地址,触发缺页异常,硬件将跳转到异常处理程序。在处理程序中,内核会加载缺失的页面,更新页表,然后恢复执行。 61 | 62 | --- 63 | 64 | ## 3. 其他硬件异常 65 | 66 | 除了缺页异常,硬件异常还可能包括以下几种: 67 | - **非法指令(Illegal Instruction)**:程序执行了一个无效或不被支持的指令,导致硬件异常。 68 | - **地址越界访问(Address Access Fault)**:程序试图访问未授权的地址区域。 69 | - **中断请求(Interrupt Request)**:外部设备请求 CPU 响应,例如硬件定时器中断、I/O 中断等。 70 | 71 | ### 触发过程: 72 | 硬件会根据不同的异常原因,将异常类型存储到 `scause` 寄存器中,并将相关的地址信息存储在 `stval` 寄存器中。然后,控制权被转移到异常处理程序,处理完后恢复执行。 73 | 74 | --- 75 | 76 | ## 总结 77 | 78 | 在 RISC-V 系统中,中断和异常主要由两种方式触发: 79 | 1. **系统调用(ecall)**:由用户程序主动触发,通过 `ecall` 指令进入内核模式,执行特权操作。 80 | 2. **硬件异常**:包括缺页异常、非法指令、地址越界等,由硬件在程序运行过程中检测到的错误或特殊情况引发。 81 | 82 | 每种中断触发方式都会保存当前的执行状态,并将控制权转交给异常处理程序,处理完异常后,恢复原来的执行环境,确保程序能够继续运行。 83 | -------------------------------------------------------------------------------- /images/lab2/config_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CubeLander/hust-pke-docs/c1153a768e20eecabee2f0dbf40057c4ec8102d8/images/lab2/config_h.png -------------------------------------------------------------------------------- /images/lab2/elf_c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CubeLander/hust-pke-docs/c1153a768e20eecabee2f0dbf40057c4ec8102d8/images/lab2/elf_c.png -------------------------------------------------------------------------------- /images/lab2/fig1_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CubeLander/hust-pke-docs/c1153a768e20eecabee2f0dbf40057c4ec8102d8/images/lab2/fig1_7.png -------------------------------------------------------------------------------- /images/lab2/fig1_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CubeLander/hust-pke-docs/c1153a768e20eecabee2f0dbf40057c4ec8102d8/images/lab2/fig1_8.png -------------------------------------------------------------------------------- /images/lab2/kernel_c1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CubeLander/hust-pke-docs/c1153a768e20eecabee2f0dbf40057c4ec8102d8/images/lab2/kernel_c1.png -------------------------------------------------------------------------------- /images/lab2/kernel_c2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CubeLander/hust-pke-docs/c1153a768e20eecabee2f0dbf40057c4ec8102d8/images/lab2/kernel_c2.png -------------------------------------------------------------------------------- /images/lab2/kernel_c3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CubeLander/hust-pke-docs/c1153a768e20eecabee2f0dbf40057c4ec8102d8/images/lab2/kernel_c3.png -------------------------------------------------------------------------------- /images/lab2/kernel_c4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CubeLander/hust-pke-docs/c1153a768e20eecabee2f0dbf40057c4ec8102d8/images/lab2/kernel_c4.png -------------------------------------------------------------------------------- /images/lab2/memory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CubeLander/hust-pke-docs/c1153a768e20eecabee2f0dbf40057c4ec8102d8/images/lab2/memory.png -------------------------------------------------------------------------------- /images/lab2/process_c1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CubeLander/hust-pke-docs/c1153a768e20eecabee2f0dbf40057c4ec8102d8/images/lab2/process_c1.png -------------------------------------------------------------------------------- /images/lab2/process_c2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CubeLander/hust-pke-docs/c1153a768e20eecabee2f0dbf40057c4ec8102d8/images/lab2/process_c2.png -------------------------------------------------------------------------------- /images/lab2/process_h1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CubeLander/hust-pke-docs/c1153a768e20eecabee2f0dbf40057c4ec8102d8/images/lab2/process_h1.png -------------------------------------------------------------------------------- /images/lab2/riscv_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CubeLander/hust-pke-docs/c1153a768e20eecabee2f0dbf40057c4ec8102d8/images/lab2/riscv_h.png -------------------------------------------------------------------------------- /images/lab2/strap_vector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CubeLander/hust-pke-docs/c1153a768e20eecabee2f0dbf40057c4ec8102d8/images/lab2/strap_vector.png -------------------------------------------------------------------------------- /images/lab2/syscall_c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CubeLander/hust-pke-docs/c1153a768e20eecabee2f0dbf40057c4ec8102d8/images/lab2/syscall_c.png -------------------------------------------------------------------------------- /images/汇编帧构造示意图.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CubeLander/hust-pke-docs/c1153a768e20eecabee2f0dbf40057c4ec8102d8/images/汇编帧构造示意图.jpg -------------------------------------------------------------------------------- /lab/lab1_2.md: -------------------------------------------------------------------------------- 1 | # lab1_2 硬件中断 2 | ## 实验目标 3 | - 理解和补充RISC-V代理内核中,处理硬件中断功能的部分源代码 4 | - 用户程序会执行一个非法指令,我们需要完善操作系统内核代码,从而实现拦截这个指令,并处理硬件异常的过程。 5 | ```cpp 6 | int main(void) { 7 | printu("Going to hack the system by running privilege instructions.\n"); 8 | // we are now in U(user)-mode, but the "csrw" instruction requires M-mode privilege. 9 | // Attempting to execute such instruction will raise illegal instruction exception. 10 | asm volatile("csrw sscratch, 0"); 11 | exit(0); 12 | } 13 | ``` 14 | ## 主要内容 15 | - 硬件中断的实现原理 16 | - 实验具体实现 17 | 18 | ## 引用文档 19 | - [软硬中断的触发机制](../doc/软硬中断的触发机制.md) 20 | - [硬件对硬中断的响应过程](../doc/硬件对硬中断的响应过程.md) 21 | - [什么是panic](../doc/什么是panic.md) 22 | ## 引用源代码 23 | - [代理内核启动程序](../code/代理内核启动程序.md) 24 | - [硬中断入口程序](../code/硬中断入口程序.md) 25 | ## 硬中断的实现原理 26 | ### 硬中断的触发和响应 27 | - [软硬中断的触发机制](../doc/软硬中断的触发机制.md) 28 | - [硬件对硬中断的响应过程](../doc/硬件对硬中断的响应过程.md): 29 | 本文档详细描述了RISC-V架构下硬件中断和异常的触发条件及操作系统的响应过程。中断和异常分为同步异常(如非法指令、特权级不符等)和异步中断(如外部设备中断、定时器中断)。当中断或异常发生时,CPU自动保存当前状态并根据mtvec寄存器跳转到相应的异常处理程序。异常处理过程包括保存上下文、切换内核栈、调用特定异常处理函数、恢复用户上下文并安全返回用户模式。该流程保证了系统能有效响应各种硬件中断,维持系统稳定。 30 | ### 硬中断入口的初始化 31 | - lab1_2在`m_start`中增加了硬件中断入口`mtvec`的初始化: 32 | 33 | ```cpp 34 | void m_start(uintptr_t hartid, uintptr_t dtb) { 35 | spike_file_init(); 36 | sprint("In m_start, hartid:%d\n", hartid); 37 | 38 | init_dtb(dtb); 39 | 40 | // 将中断处理的trap frame地址保存到mscratch寄存器中,用于M模式下的中断处理。@lab1_2 41 | write_csr(mscratch, &g_itrframe); 42 | 43 | write_csr(mstatus, ((read_csr(mstatus) & ~MSTATUS_MPP_MASK) | MSTATUS_MPP_S)); 44 | write_csr(mepc, (uint64)s_start); 45 | 46 | // 设置M模式下的中断处理向量,处理所有中断请求。@lab1_2 47 | write_csr(mtvec, (uint64)mtrapvec); 48 | 49 | 50 | delegate_traps(); 51 | asm volatile("mret"); 52 | } 53 | 54 | ``` 55 | ### 硬中断处理程序 56 | - [硬中断处理程序](../code/硬中断处理程序.md):本程序段描述了在RISC-V架构的M模式下的硬中断的服务程序。具体包括中断向量入口 (`mtrapvec`)、中断处理函数 (`handle_mtrap`) 和非法指令处理函数 (`handle_illegal_instruction`) 的实现。程序通过保存和恢复寄存器的状态、切换栈空间,并调用相应的中断处理函数来响应和处理不同类型的硬中断和异常。 57 | 58 | ## 具体实现 59 | - 补充调用过程中的`handle_illegal_instruction`调用即可 -------------------------------------------------------------------------------- /lab/lab1_3.md: -------------------------------------------------------------------------------- 1 | # lab1_3 外部中断 2 | ## 实验目标 3 | - 理解和补充RISC-V代理内核中,外部中断功能的部分源代码 4 | - 实现内核时钟中断对用户程序的打断。 5 | ```cpp 6 | int main(void) { 7 | printu("Hello world!\n"); 8 | int i; 9 | for (i = 0; i < 100000000; ++i) { 10 | if (i % 5000000 == 0) printu("wait %d\n", i); 11 | } 12 | 13 | exit(0); 14 | 15 | return 0; 16 | } 17 | ``` 18 | ## 主要内容 19 | - 时钟中断的实现原理 20 | - 实验目标的具体实现 21 | 22 | ## 引用文档 23 | - [什么是外部时钟中断](../doc/什么是外部时钟中断.md) 24 | - [时钟中断的硬件实现原理](../doc/时钟中断的硬件实现原理.md) 25 | - [CPU如何同步自然时间](../doc/CPU如何同步自然时间.md) 26 | - [硬中断到软中断的切换过程](../doc/硬中断到软中断的切换过程.md) 27 | 28 | ## 时钟中断的实现原理 29 | ### 时钟中断的初始化过程 30 | - lab1_3在`m_start`中增加了对时钟中断功能的初始化: 31 | 32 | `void m_start(uintptr_t hartid, uintptr_t dtb)` 33 | ```cpp 34 | void m_start(uintptr_t hartid, uintptr_t dtb) { 35 | 36 | spike_file_init(); 37 | sprint("In m_start, hartid:%d\n", hartid); 38 | init_dtb(dtb); 39 | write_csr(mscratch, &g_itrframe); 40 | write_csr(mstatus, ((read_csr(mstatus) & ~MSTATUS_MPP_MASK) | MSTATUS_MPP_S)); 41 | write_csr(mepc, (uint64)s_start); 42 | write_csr(mtvec, (uint64)mtrapvec); 43 | write_csr(mstatus, read_csr(mstatus) | MSTATUS_MIE); 44 | delegate_traps(); 45 | 46 | // 还启用了在 supervisor 模式下的中断处理。添加于 @lab1_3 47 | write_csr(sie, read_csr(sie) | SIE_SEIE | SIE_STIE | SIE_SSIE); 48 | 49 | // 初始化定时器。添加于 @lab1_3 50 | timerinit(hartid); 51 | 52 | asm volatile("mret"); 53 | } 54 | ``` 55 | 56 | `void timerinit(uintptr_t hartid)` 57 | ```cpp 58 | void timerinit(uintptr_t hartid) { 59 | // 从现在开始,在 TIMER_INTERVAL 时间后触发定时器中断(irq)。 60 | *(uint64*)CLINT_MTIMECMP(hartid) = *(uint64*)CLINT_MTIME + TIMER_INTERVAL; 61 | 62 | // 在 MIE(机器中断使能)控制寄存器中启用机器模式定时器中断 irq。 63 | write_csr(mie, read_csr(mie) | MIE_MTIE); 64 | } 65 | ``` 66 | 67 | ## 时钟中断的进入和处理 68 | - 时钟中断需要进行硬中断和软中断的处理过程,主要是因为操作系统需要将硬件层面上的定时事件与高层的系统任务分开处理。硬中断主要负责触发定时事件并更新硬件状态(如更新定时器、设置下一次中断),而软中断则用于在操作系统层面处理与定时相关的任务,如时间片管理和进程调度。通过这种分工,硬件层与操作系统的高层逻辑得以解耦,确保系统效率和稳定性。 69 | ### 时钟中断的硬件响应 70 | - 简单来说,来自硬件的中断请求会打断用户程序的执行,进入`mtrapvec`硬中断入口,同时 71 | 记录`mcause = CAUSE_MTIMER`,这样执行到在`handle_mtrap()`函数中时,就会进入时钟中断对应的处理程序`handle_timer()` 72 | - 最后在`handle_timer()`置软中断位为1,从而在出硬中断服务程序后,让硬件进入软中断服务程序。其中硬件会自动设软中断原因为硬中断原因相同:`scause=mcause`。 73 | 74 | `static void handle_timer()` 75 | ```cpp 76 | // added @lab1_3 77 | static void handle_timer() { 78 | int cpuid = 0; 79 | // setup the timer fired at next time (TIMER_INTERVAL from now) 80 | *(uint64*)CLINT_MTIMECMP(cpuid) = *(uint64*)CLINT_MTIMECMP(cpuid) + TIMER_INTERVAL; 81 | 82 | // setup a soft interrupt in sip (S-mode Interrupt Pending) to be handled in S-mode 83 | write_csr(sip, SIP_SSIP); 84 | } 85 | ``` 86 | ### 时钟中断的软件响应 87 | - [硬中断到软中断的切换过程](../doc/硬中断到软中断的切换过程.md):出硬中断后,硬件自动进行切换到软中断过程的详细描述。 88 | - 在软中断服务程序中,会进行`smode_trap_vector`->`void smode_trap_handler(void)`->`handle_mtimer_trap()`的调用过程。我们需要补充的源代码就在于此。 89 | - 实验目标仅仅是增加`g_ticks`全局变量,并复位软中断标志位。 90 | - 虽然学校的时钟中断服务程序只涉及增加 g_ticks 和清除 SIP 标志位,但在实际操作系统中,时钟中断通常用于进程调度、上下文切换、系统时钟更新和延迟任务处理等,目的是高效管理进程并维持系统的时间精度。所以`handle_mtimer_trap()`中其实要干很多事情。 91 | 92 | ## 任务实现 93 | - 在`handle_mtimer_trap()`中补充任务要求的源代码。 94 | 95 | `handle_mtimer_trap()` 96 | ```cpp 97 | static uint64 g_ticks = 0; 98 | void handle_mtimer_trap() { 99 | sprint("Ticks %d\n", g_ticks); 100 | // TODO (lab1_3): increase g_ticks to record this "tick", and then clear the "SIP" 101 | // field in sip register. 102 | // hint: use write_csr to disable the SIP_SSIP bit in sip. 103 | panic( "lab1_3: increase g_ticks by one, and clear SIP field in sip register.\n" ); 104 | } 105 | ``` -------------------------------------------------------------------------------- /lab/lab1_challenge1_m2.md: -------------------------------------------------------------------------------- 1 | # lab1_challenge1对lab2中的MMU支持 2 | 3 | ## 问题概述 4 | 因为lab1_challenge1在backtrace系统调用中需要访问用户的栈空间,所以需要在内核对于用户的栈指针虚拟地址做翻译。 5 | 6 | 7 | 8 | 9 | ## 内核是如何访问用户进程的内存的 10 | 11 | 我们在访问用户态的内存时,需要通过用户页表,将用户态的内存翻译为物理地址。这个过程可以通过内核函数`user_va_to_pa`实现。 12 | 13 | 但是这随之又带来一个问题,在内核态的任何内存读写,都要再通过一次内核页表。但是内核页表中对于分配给用户的物理地址,是怎么分配的?再经过一次页表地址变换以后,还能对给定的物理地址做访问吗? 14 | 15 | 内核的页表是在kern_vm_init当中初始化的,其中将所有空闲的物理地址,都在内核页表中做了物理地址=虚拟地址的直接映射。所以说,对用户的物理地址再做一次翻译,仍然能够读写正确的物理地址。 16 | 17 | > 这是一个作者学习过程中的疑问,如果说,在内核初始化的时候,对剩下的全部物理地址做一对一的虚拟地址映射关系,那么其中会有一些物理页被拿来当页目录,这些虚拟地址不能完全被分配,其中会不会有矛盾。 18 | > 其实不会有矛盾,因为在page_walk中分配物理页当页目录,和在页目录中初始化这个页的映射关系是可以同时进行的,二者不会有冲突。 19 | 20 | ## 解决方案 21 | 22 | 在原本实现的基础上,对于所有用户态的访存操作,都做一次user_va_to_pa的转换即可。 23 | 24 | 新增:在多线程环境里读重复使用的源代码文件,会出现死锁(尚未解决)。 -------------------------------------------------------------------------------- /lab/lab1_challenge2.md: -------------------------------------------------------------------------------- 1 | # lab1_challenge2 打印异常代码行 2 | 3 | ## 引用 4 | [lab1_challenge1 打印用户程序调用栈](lab/lab1_challenge1.md) 5 | 6 | [ELF文件如何组织调试信息](../doc/ELF文件如何组织调试信息.md) 7 | 8 | [make_addr_line源代码分析](../code/make_addr_line.md) 9 | 10 | [硬中断的响应过程](../doc/硬件对硬中断的响应过程.md) 11 | 12 | [硬中断处理程序](../code/硬中断处理程序.md) 13 | 14 | ## 实验目标 15 | 修改内核(包括machine文件夹下)的代码,使得用户程序在发生异常时,内核能够输出触发异常的用户程序的源文件名和对应代码行。 16 | 17 | 示例程序和对应输出如下: 18 | ```c 19 | int main(void) { 20 | printu("Going to hack the system by running privilege instructions.\n"); 21 | // we are now in U(user)-mode, but the "csrw" instruction requires M-mode privilege. 22 | // Attempting to execute such instruction will raise illegal instruction exception. 23 | asm volatile("csrw sscratch, 0"); 24 | exit(0); 25 | } 26 | /* 27 | ... 28 | Switch to user mode... 29 | Going to hack the system by running privilege instructions. 30 | Runtime error at user/app_errorline.c:13 31 | asm volatile("csrw sscratch, 0"); 32 | Illegal instruction! 33 | System is shutting down with exit code -1. 34 | ... 35 | */ 36 | ``` 37 | 38 | ## 主要内容 39 | lab1_challenge2的分支变化 40 | 41 | 打印异常代码行的原理 42 | 43 | ## lab1_challenge2的分支变化 44 | ### `elf.c` 45 | ```cpp 46 | void read_uleb128(uint64 *out, char **off); 47 | void read_sleb128(int64 *out, char **off); 48 | void read_uint64(uint64 *out, char **off); 49 | void read_uint32(uint32 *out, char **off); 50 | void read_uint16(uint16 *out, char **off); 51 | void make_addr_line(elf_ctx *ctx, char *debug_line, uint64 length); 52 | ``` 53 | 关键新增函数是make_addr_line,它实现了从elf解析debugline段,从而提取目录表、文件表和行表的过程。 54 | ### `elf.h` 55 | ```c 56 | // elf section header 57 | typedef struct elf_sect_header_t{ 58 | uint32 name; 59 | uint32 type; 60 | uint64 flags; 61 | uint64 addr; 62 | uint64 offset; 63 | uint64 size; 64 | uint32 link; 65 | uint32 info; 66 | uint64 addralign; 67 | uint64 entsize; 68 | } elf_sect_header; 69 | 70 | // compilation units header (in debug line section) 71 | typedef struct __attribute__((packed)) { 72 | uint32 length; 73 | uint16 version; 74 | uint32 header_length; 75 | uint8 min_instruction_length; 76 | uint8 default_is_stmt; 77 | int8 line_base; 78 | uint8 line_range; 79 | uint8 opcode_base; 80 | uint8 std_opcode_lengths[12]; 81 | } debug_header; 82 | ``` 83 | 84 | ### process.h 85 | ```c 86 | // code file struct, including directory index and file name char pointer 87 | typedef struct { 88 | uint64 dir; char *file; 89 | } code_file; 90 | 91 | // address-line number-file name table 92 | typedef struct { 93 | uint64 addr, line, file; 94 | } addr_line; 95 | 96 | // the extremely simple definition of process, used for begining labs of PKE 97 | typedef struct process_t { 98 | // pointing to the stack used in trap handling. 99 | uint64 kstack; 100 | // trapframe storing the context of a (User mode) process. 101 | trapframe* trapframe; 102 | 103 | // added @lab1_challenge2 104 | char *debugline; 105 | char **dir; 106 | code_file *file; 107 | addr_line *line; 108 | int line_ind; 109 | }process; 110 | ``` 111 | 112 | ## 打印异常代码的实现原理 113 | 114 | ### elf文件中的调试信息是怎么组织的 115 | [ELF文件如何组织调试信息](../doc/ELF文件如何组织调试信息.md) 116 | 117 | 在 ELF 文件中,调试信息通过 `.debug_*` 节段(如 `.debug_info`、`.debug_abbrev`、`.debug_line` 等)来组织和存储。DWARF 格式被广泛使用来表示这些信息,帮助调试器将二进制代码映射回源代码。调试信息包含了源代码的行号、符号表、变量和类型信息,但**不包含源代码的实际文本**。这些信息帮助开发者在调试时查看源代码的变量值、函数调用栈、代码行等。 118 | 119 | 具体二进制组织方式,见函数`make_addr_line`中的解析过程。 120 | 121 | ## 实现 122 | 实现的具体方法,可以参考网上的公开资料。 123 | ### 读取.debug_line ELF节 124 | [lab1_challenge1 打印用户程序调用栈](lab/lab1_challenge1.md) 125 | 126 | 在读取调试信息的过程中加入对`.debug_line` ELF节的解析过程。 127 | 解析ELF信息的方法,见`lab1_challenge1` 128 | 129 | ### 重新组织make_addr_line的代码 130 | 见 [make_addr_line源代码分析](../code/make_addr_line.md) 131 | 132 | 由于作者水平有限,函数原变量名和内部结构很难看懂,于是重新做了一番研究。 133 | ### 全局数据结构 134 | 在`elf.c`中声明了全局debugline缓冲区,从中分配`dir`表、`file`表和`line`表. 135 | ### 从epc到代码行的解析过程 136 | 在mtrap.c中新建一个硬中断服务程序error_printer,读取process中存放的文件名表dir,文件表file和行表line成员变量,进行epc到代码行的定位过程。 137 | 138 | 查找行表(递增),从而根据映射关系定位目标文件路径在`dir`表和`file`表中的序号,从而还原源文件的路径。 139 | 140 | 根据存放的路径重新打开文件,通过读取换行符\n定位目标行的代码文本。 141 | -------------------------------------------------------------------------------- /lab/lab1_challenge3.md: -------------------------------------------------------------------------------- 1 | # lab1_challenge3 多核启动和运行 2 | 由于这个实验需要管理多个进程的内存,所以需要在lab2中实现mmu以后再完成。 3 | 什么?通过头歌的测试?连MMU都没有谈多核有任何意义吗? 4 | 5 | ## 参考资料 6 | 7 | [信号量和自旋锁](../doc/信号量和自旋锁.md) 8 | 9 | 10 | 11 | ## 实验目标 12 | 之前的实验都在单核环境下进行。在本次实验中,你需要修改操作系统内核使其支持两核并发运行,并且在每个核上加载一个程序运行,等到两个程序都执行完毕后退出并关闭模拟器。 13 | 14 | ### 给定应用 15 | 16 | #### user/app0.c 17 | 18 | ```c 19 | #include "user_lib.h" 20 | #include "util/types.h" 21 | 22 | int main(void) { 23 | printu(">>> app0 is expected to be executed by hart0\n"); 24 | exit(0); 25 | } 26 | ``` 27 | 28 | #### user/app1.c 29 | 30 | ```c 31 | #include "user_lib.h" 32 | #include "util/types.h" 33 | 34 | int main(void) { 35 | printu(">>> app1 is expected to be executed by hart1\n"); 36 | exit(0); 37 | } 38 | ``` 39 | 40 | 在本次实验中,给定两个简单的用户程序,每个程序会输出一句话。你需要让每个核分别加载一个程序,并能够正确运行,输出相应内容然后退出。 41 | 42 | ## 实验原理 43 | 44 | ## 需要修改的部分 45 | 46 | ### elf.c 47 | 需要分别加载两个程序 48 | 49 | ### kernel.c 50 | 51 | #### 全局变量 52 | ```c 53 | // process is a structure defined in kernel/process.h 54 | process user_app[2]; 55 | ``` 56 | 57 | #### m_start 58 | 只需要做一次的部分: 59 | spike文件接口初始化 60 | 设备树初始化 61 | ```c 62 | volatile static int counter = 0; 63 | void m_start(uintptr_t hartid, uintptr_t dtb) { 64 | if (hartid == 0) { 65 | spike_file_init(); 66 | init_dtb(dtb); 67 | } 68 | sync_barrier(&counter, NCPU); 69 | // 这一个函数是用来同步不同核心的任务的。 70 | sprint("In m_start, hartid:%d\n", hartid); 71 | write_tp(hartid); 72 | 73 | // save the address of trap frame for interrupt in M mode to "mscratch". added 74 | // @lab1_2 75 | ``` 76 | #### load_user_program 77 | 虽然说还没有引入调度器,但是由于在中断恢复的时候要读进tp寄存器,所以要做一下初始化。 78 | ``` 79 | proc->trapframe->regs.tp = read_tp(); 80 | ``` 81 | 82 | ### process.c 83 | 84 | ```c 85 | // current points to the currently running user-mode application. 86 | process* current[2]; 87 | 88 | ``` 89 | 90 | 然后我们需要修改所有对current的使用,都改成current[hartid] -------------------------------------------------------------------------------- /lab/lab2.md: -------------------------------------------------------------------------------- 1 | # 实验2的基础知识 2 | 3 | ## 引用 4 | 5 | [spike仿真层](../doc/Spike仿真层.md) 6 | 7 | ## lab2开始时对于内核源代码的改动一览 8 | [lab2源代码改动分析](../code/lab2_update.md) 9 | 10 | ## spike仿真层 11 | 12 | [spike仿真层](../doc/Spike仿真层.md) 13 | 14 | 概括来说,Spike 仿真器提供了一个 完整的 RISC-V 虚拟硬件层。它通过 模拟 RISC-V 处理器、虚拟内存管理、外设抽象、中断与异常处理等功能,在宿主机上仿真 RISC-V 硬件,提供了一个 虚拟的 RISC-V 计算平台。通过这种方式,开发者能够在不依赖实际硬件的情况下,进行 RISC-V 软件开发、调试和性能分析。 15 | 16 | ## Sv39虚地址管理方案 17 | >这部分原文档写得挺清楚的,就直接引用了 18 | 19 | 在RISC-V的sv39虚地址管理方案中,逻辑地址(就是我们的程序中各个符号,在链接时被赋予的地址)通过页表转换为其对应的物理地址。由于我们考虑的机器采用了RV64G指令集,意味着逻辑地址和物理地址理论上都是64位的。然而,对于逻辑地址,实际上我们的应用规模还用不到全部64位的寻找空间,所以Sv39方案中只使用了64位虚地址中的低39位(Sv48方案使用了低48位),意味着我们的应用程序的地址空间可以到512GB;对于物理地址,目前的RISC-V设计只用到了其中的低56位。 20 | 21 | Sv39将39位虚拟地址“划分”为4个段(如下图所示): 22 | 23 | - [38,30]:共9位,图中的VPN[2],用于在512(2^9)个页目录(page directory)项中检索页目录项(page directory entry, PDE); 24 | - [29,21]:共9位,图中的VPN[1],用于在512(2^9)个页中间目录(page medium directory)中检索PDE; 25 | - [20,12]:共9位,图中的VPN[0],用于在512(2^9)个页表(page medium directory)中检索PTE; 26 | - [11,0]:共12位,图中的offset,充当4KB页的页内位移。 27 | 28 | ![fig1_8](../images/lab2/fig1_8.png) 29 | 30 | 图4.1 Sv39中虚拟地址到物理地址的转换过程 31 | 32 | 由于每个物理页的大小为4KB,同时,每个目录项(PDE)或页表项(PTE)占据8个字节,所以一个物理页能够容纳的PDE或PTE的数量为4KB/8B=512,这也是为什么VPN[2]=VPN[1]=VPN[0]=512的原因。 33 | 34 | 8字节的PDE或者PTE的格式如下: 35 | 36 | ![fig1_7](../images/lab2/fig1_7.png) 37 | 38 | 图4.2 Sv39中PDE/PTE格式 39 | 40 | 其中的各个位的含意为: 41 | 42 | ● V(Valid)位决定了该PDE/PTE是否有效(V=1时有效),即是否有对应的实页。 43 | 44 | ● R(Read)、W(Write)和X(eXecutable)位分别表示此页对应的实页是否可读、可写和可执行。这3个位只对PTE有意义,对于PDE而言这3个位都为0。 45 | 46 | ● U(User)位表示该页是不是一个用户模式页。如果U=1,表示用户模式下的代码可以访问该页,否则就表示不能访问。S模式下的代码对U=1页面的访问取决于sstatus寄存器中的SUM字段取值。 47 | 48 | ● G(Global)位表示该PDE/PTE是不是全局的。我们可以把操作系统中运行的一个进程,认为是一个独立的地址空间,有时会希望某个虚地址空间转换可以在一组进程中共享,这种情况下,就可以将某个PDE的G位设置为1,达到这种共享的效果。 49 | 50 | ● A(Access)位表示该页是否被访问过。 51 | 52 | ● D(Dirty)位表示该页的内容是否被修改。 53 | 54 | ● RSW位(2位)是保留位,一般由运行在S模式的代码(如操作系统)来使用。 55 | 56 | ● PPN(44位)是物理页号(Physical Page Number,简写为PPN)。 57 | 58 | 其中PPN为44位的原因是:对于物理地址,现有的RISC-V规范只用了其中的56位,同时,这56位中的低12位为页内位移。所以,PPN的长度=56-12=44(位)。 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ## RISC-V内存布局与规划与MMIO特点 67 | 68 | ![RISC-V的物理内存布局](../images/lab2/memory.png) 69 | ### 用户进程的虚实地址映射关系 70 | - 机器(或者仿真层)实际的物理内存资源的编址,是从0x80000000开始的。 71 | - 对于用户进程的 0x80000000 以上虚拟地址,通过页表变换映射到独立的物理内存,不同进程之间相互隔离。 72 | - 对于用户进程的 0x80000000 以下虚拟地址,通常不进行页表转换,而是直接映射到固定的硬件设备。 73 | - 通常情况下,用户进程不会直接访问 0x80000000 以下的 MMIO 地址,而是通过内核 syscall 让内核代为执行访问。 74 | - 不同进程的 0x80000000 以下地址通常与MMIO设备之间的映射关系是相同的,它保证多个进程能够共享外设资源。 75 | ### 内核对内存使用的特殊说明 76 | 1. **MMU开启之前,内核对内存的读写方式** 77 | 78 | 在系统刚启动时(如 Bootloader 加载阶段),CPU 运行在 物理地址模式(Bare Mode,无 MMU): 79 | 80 | - 内核代码从 Bootloader 直接加载到物理地址(如 0x80000000) 并执行。 81 | - CPU 访问的所有内存地址都是物理地址,没有地址转换。 82 | - 内核的所有全局变量、代码、栈等,都是直接基于物理地址操作的。 83 | 2. **MMU开启之后,内核对内存的读写方式** 84 | 85 | 当 MMU 开启后: 86 | 87 | - 内核必须创建页表,建立物理内存的映射规则。 88 | - CPU 在开启MMU之后的所有内存访问,都会走 MMU 进行地址转换,不再直接访问物理地址。 89 | - MMU 使得“虚拟地址 ≠ 物理地址”,所以 内核需要确保,对于那些 MMU 开启前已经被内核占用的物理内存资源,在MMU开启后仍然保持“内核占用”,以及能够通过MMU中的映射关系继续访问。 90 | 3. **通过MMU访问内核原代码和数据的策略** 91 | 92 | - 方式 1:内核建立 恒等映射(Identity Mapping)。 93 | 94 | 让内核的虚拟地址与物理地址保持一致,确保 MMU 开启后不会影响现有的地址访问。常见于RISC-V或者ARM架构,这也是本实验中的做法。 95 | - 方式 2:内核建立偏移映射(Offset Mapping)。 96 | 97 | 让内核在 MMU 开启后,虚拟地址变为高地址,但仍然具备某些与物理地址确定的对应关系。例如,在 x86_64 的 Linux 内核,物理地址 `0x0000 0000 8000 0000` 可能会在进程的页表中映射到虚拟地址`0xFFFF 8000 8000 0000`(仅在进程的内核态下才能访问的高地址空间)。 98 | 99 | ### 内核栈的特殊说明 100 | - 在多进程环境中,**内核栈的虚拟地址** 通常分配在`进程的内核态地址空间`,以避免被用户进程有意无意修改。 101 | - **内核栈虚拟地址** 映射的物理地址,也通常分配在同一片物理地址区域。这么做的好处是方便统一管理,提高缓存效率,和进一步保障内存安全(方便对于实际读写物理地址做安全检查,从而进一步降低**越界访问**和**安全漏洞**的风险) 102 | - 在本实验的环境中,为了方便考虑,直接找了一个空闲页当内核栈。 -------------------------------------------------------------------------------- /lab/lab2_1.md: -------------------------------------------------------------------------------- 1 | # lab2_1 虚实地址变换 2 | 3 | ## **实验内容** 4 | 5 | 实现user_va_to_pa()函数,完成给定逻辑地址到物理地址的转换。 6 | 7 | ## 引用 8 | [lab2基础知识](../lab/lab2.md) 9 | 10 | [lab2的git修改](../code/lab2_update.md) 11 | 12 | [lab2的内核初始化改动](../code/lab2的内核初始化改动.md) 13 | 14 | ## 内容 15 | 这个部分相对简单,理解和引用page_walk函数,完成user_va_to_pa函数即可 -------------------------------------------------------------------------------- /lab/lab2_2.md: -------------------------------------------------------------------------------- 1 | # lab2_2 简单内存分配 2 | 3 | ## 实验目标 4 | 完成两个用户态系统调用naive_malloc()和naive_free(), 其中naive_malloc和两个函数的系统调用用户接口已经实现。 5 | ```c 6 | 7 | struct my_structure { 8 | char c; 9 | int n; 10 | }; 11 | 12 | int main(void) { 13 | struct my_structure* s = (struct my_structure*)naive_malloc(); 14 | s->c = 'a'; 15 | s->n = 1; 16 | 17 | printu("s: %lx, {%c %d}\n", s, s->c, s->n); 18 | naive_free(s); 19 | 20 | exit(0); 21 | } 22 | ``` 23 | 24 | ## lab2_2源代码变化 25 | ### syscall.c 26 | ```c 27 | 28 | // 29 | // maybe, the simplest implementation of malloc in the world ... added @lab2_2 30 | // 31 | uint64 sys_user_allocate_page() { 32 | void* pa = alloc_page(); 33 | uint64 va = g_ufree_page; 34 | g_ufree_page += PGSIZE; 35 | user_vm_map((pagetable_t)current->pagetable, va, PGSIZE, (uint64)pa, 36 | prot_to_type(PROT_WRITE | PROT_READ, 1)); 37 | 38 | return va; 39 | } 40 | 41 | // 42 | // reclaim a page, indicated by "va". added @lab2_2 43 | // 44 | uint64 sys_user_free_page(uint64 va) { 45 | user_vm_unmap((pagetable_t)current->pagetable, va, PGSIZE, 1); 46 | return 0; 47 | } 48 | ``` 49 | ### vmm.c 50 | ```c 51 | // 52 | // unmap virtual address [va, va+size] from the user app. 53 | // reclaim the physical pages if free!=0 54 | // 55 | void user_vm_unmap(pagetable_t page_dir, uint64 va, uint64 size, int free) { 56 | // TODO (lab2_2): implement user_vm_unmap to disable the mapping of the virtual pages 57 | // in [va, va+size], and free the corresponding physical pages used by the virtual 58 | // addresses when if 'free' (the last parameter) is not zero. 59 | // basic idea here is to first locate the PTEs of the virtual pages, and then reclaim 60 | // (use free_page() defined in pmm.c) the physical pages. lastly, invalidate the PTEs. 61 | // as naive_free reclaims only one page at a time, you only need to consider one page 62 | // to make user/app_naive_malloc to behave correctly. 63 | panic( "You have to implement user_vm_unmap to free pages using naive_free in lab2_2.\n" ); 64 | 65 | } 66 | 67 | ``` 68 | 69 | ## 虚拟内存分配过程(只分配一个页) 70 | 首先找到一个空闲的物理页 71 | 72 | 然后在用户页表中创建多级页表,和最终的页表项,建立虚拟地址到物理页之间的对应关系 73 | 74 | ## 虚拟内存回收过程 75 | 76 | 首先确定与释放的虚拟内存空间对应的全部页表项 77 | 如果free=1,释放所有对应的物理页,并收回物理页内存池 78 | 然后修改页表项,置valid位为零。 79 | 我们不需要释放中间的页表。 -------------------------------------------------------------------------------- /lab/lab2_3.md: -------------------------------------------------------------------------------- 1 | # lab2_3 缺页异常 2 | ## 实验内容 3 | 在PKE操作系统内核中完善用户态栈空间的管理,使得它能够正确处理用户进程的“压栈”请求。 4 | 5 | 6 | ## lab2_3的源代码改动 7 | ### strap.c 8 | ```c 9 | // 10 | // the page fault handler. added @lab2_3. parameters: 11 | // sepc: the pc when fault happens; 12 | // stval: the virtual address that causes pagefault when being accessed. 13 | // 14 | void handle_user_page_fault(uint64 mcause, uint64 sepc, uint64 stval) { 15 | sprint("handle_page_fault: %lx\n", stval); 16 | switch (mcause) { 17 | case CAUSE_STORE_PAGE_FAULT: 18 | // TODO (lab2_3): implement the operations that solve the page fault to 19 | // dynamically increase application stack. 20 | // hint: first allocate a new physical page, and then, maps the new page to the 21 | // virtual address that causes the page fault. 22 | panic( "You need to implement the operations that actually handle the page fault in lab2_3.\n" ); 23 | 24 | break; 25 | default: 26 | sprint("unknown page fault.\n"); 27 | break; 28 | } 29 | } 30 | 31 | void smode_trap_handler(void) { 32 | ... 33 | case CAUSE_STORE_PAGE_FAULT: 34 | case CAUSE_LOAD_PAGE_FAULT: 35 | // the address of missing page is stored in stval 36 | // call handle_user_page_fault to process page faults 37 | handle_user_page_fault(cause, read_csr(sepc), read_csr(stval)); 38 | break; 39 | ... 40 | 41 | } 42 | ``` 43 | 44 | ## 实现方法 45 | 首先判断异常是缺页异常 46 | 47 | 然后判断缺页异常是由用户栈操作引起的 48 | 如何判定这个异常的虚拟内存请求来源于用户栈? 49 | 通过输入的参数stval(存放的是发生缺页异常时,程序想要访问的逻辑地址)判断缺页的逻辑地址,小于USER_STACK_TOP,并大于USER_STACK_BOTTOM 50 | 51 | 最后我们为异常地址stval分配物理页,扩充用户栈。 52 | 分配物理页的实现过程,参考elf_alloc_mb -------------------------------------------------------------------------------- /lab/lab2_challenge1.md: -------------------------------------------------------------------------------- 1 | # lab2_challenge1 复杂缺页异常 2 | 3 | ## 实验目标 4 | 我们需要优化缺页异常服务函数,支持识别访问的是越界地址,而不是栈增长。 5 | 6 | ## 实现原理 7 | 我们在用户的进程控制块中做一个栈底标记,在处理缺页异常中只服务栈底下面一页虚拟地址的请求,并更新栈底标记。 8 | 9 | ## 具体实现过程 10 | 11 | ## 改动进程控制块proc 12 | 在process中增加一个user_stack_bottom成员 13 | 14 | ## 改动load_user_program 15 | 在初始化用户栈空间之后,初始化这个用户栈栈底指针 16 | 17 | ## 改动handle_user_page_fault 18 | 对于stval做是否大于等于user_stack_bottom - PGSIZE的判断 19 | 这样就可以分辨出是正常的栈增长访问还是异常内存访问了。 20 | 21 | >注:如果正常的栈增长访问会一下子跨越一整页,那么这个策略就失效了。一般不会这样。 -------------------------------------------------------------------------------- /lab/lab2_challenge2.md: -------------------------------------------------------------------------------- 1 | # lab2_challenge2 堆空间管理 2 | 3 | ## 实验内容 4 | 修改内核的代码,使得应用程序的malloc能够在一个物理页中分配,并对各申请块进行合理的管理 5 | 6 | 学校的要求很笼统,只要求malloc能够分配一个物理页下的资源。 7 | 8 | 测试程序的要求如下: 9 | ```c 10 | char *m = (char *)better_malloc(100); 11 | char *p = (char *)better_malloc(50); 12 | better_free((void *)m); 13 | char *n = (char *)better_malloc(50); 14 | // 这些代码实现之后,m和n的地址相同。 15 | ``` 16 | (待填坑) 17 | ## 具体实现 18 | 19 | ### 初始化用户程序堆空间 20 | 21 | ### malloc的实现 22 | 23 | ### free的实现 24 | 我们采用双向链表来管理堆内的空间: 25 | 26 | -------------------------------------------------------------------------------- /lab/lab2_challenge3.md: -------------------------------------------------------------------------------- 1 | # lab2_challenge3 多核内存管理 2 | 3 | ## 改动 4 | ### elf.c 5 | 注释掉了加载调试信息的代码(因为会造成两个核请求一个文件,产生死锁) 6 | ### kernel.c 7 | ```c 8 | void load_user_program(process *proc) 9 | ... 10 | user_heap_init(proc); 11 | ... 12 | ``` 13 | ### pmm.c 14 | 在alloc_page中加入信号量sync_barrier,避免两个核同时操作物理内存池 15 | 16 | ### vmm.c 17 | 18 | 在malloc函数中,增加如果找不到合适的内存块,就在空闲链表之后继续延长新的页的功能。 19 | 20 | ### user_lib.c 21 | 将naive_malloc直接定义为请求一个4000大小的内存块。 -------------------------------------------------------------------------------- /lab/lab3.md: -------------------------------------------------------------------------------- 1 | # lab3 实验3 进程管理 2 | 3 | ## lab3在lab2基础上的变化 4 | ### process.h 5 | #### 新的全局声明 6 | ```c 7 | // riscv-pke kernel supports at most 32 processes 8 | #define NPROC 32 9 | // maximum number of pages in a process's heap 10 | #define MAX_HEAP_PAGES 32 11 | ``` 12 | #### 进程控制块process的变化 13 | ```c 14 | heap_block* heap; 15 | // size_t heap_size; 16 | 17 | 18 | // points to a page that contains mapped_regions. below are added @lab3_1 19 | mapped_region *mapped_info; 20 | // next free mapped region in mapped_info 21 | int total_mapped_region; 22 | // heap management 23 | process_heap_manager user_heap; 24 | 25 | // process id 26 | uint64 pid; 27 | // process status 28 | int status; 29 | // parent process 30 | struct process_t *parent; 31 | // next queue element 32 | struct process_t *queue_next; 33 | ``` 34 | #### 数据结构mapped_region 35 | 用来记录所有分配给用户进程的内存区域。 36 | 在elf_load中进行写入 37 | 38 | ```c 39 | // the VM regions mapped to a user process 40 | typedef struct mapped_region { 41 | uint64 va; // mapped virtual address 42 | uint32 npages; // mapping_info is unused if npages == 0 43 | uint32 seg_type; // segment type, one of the segment_types 44 | } mapped_region; 45 | ``` 46 | #### 数据结构process_heap_manager 47 | ```c 48 | typedef struct process_heap_manager { 49 | // points to the last free page in our simple heap. 50 | uint64 heap_top; 51 | // points to the bottom of our simple heap. 52 | uint64 heap_bottom; 53 | 54 | // the address of free pages in the heap 55 | uint64 free_pages_address[MAX_HEAP_PAGES]; 56 | // the number of free pages in the heap 57 | uint32 free_pages_count; 58 | }process_heap_manager; 59 | 60 | ``` 61 | 62 | ### 全局变量 63 | ```c 64 | // process pool. added @lab3_1 65 | process procs[NPROC]; 66 | 67 | // current points to the currently running user-mode application. 68 | process* current[NCPU]; 69 | ``` 70 | 71 | 72 | 73 | ### s_start 74 | ```c 75 | init_proc_pool() 76 | // 初始化进程池 77 | insert_to_ready_queue( load_user_program() ); 78 | schedule(); 79 | ``` 80 | 81 | ### load_user_program 82 | 原本在load_user_program中进行用户程序空间的内存映射。 83 | 84 | 用户程序的段映射集成到load_bincode_from_host_elf-->elf_load中 85 | 用户程序的其他内存分配,trapframe,pagetable,kstack,user_stack, userheap全部集成到alloc_process函数中。 86 | 87 | ### elf_load 88 | 从其中把elf_load_segment拆出来当一个函数,提高了可读性和可维护性。 89 | 90 | ### insert_to_ready_queue 91 | 将初始化好的进程控制块存入就绪进程队列。 92 | 93 | ### schedule 94 | 修改schedule函数,以支持多核的退出。 95 | 需要加入一个互斥锁,避免多个核心同时操作ready_queue_head 96 | 97 | ### alloc_process 98 | 为用户程序分配其他所有内存空间。 99 | 为什么不能在alloc_process中给用户程序初始化堆?那是因为和do_fork中子进程拷贝父进程堆冲突。 100 | 重点是堆内存的分配方式:在初始化的时候没有分配任何页。 101 | 也就是说,堆内存的初始化会在第一次malloc请求时执行。 102 | ```c 103 | // initialize the process's heap manager 104 | ps->user_heap.heap_top = USER_FREE_ADDRESS_START; 105 | ps->user_heap.heap_bottom = USER_FREE_ADDRESS_START; 106 | ps->user_heap.free_pages_count = 0; 107 | 108 | // map user heap in userspace 109 | ps->mapped_info[HEAP_SEGMENT].va = USER_FREE_ADDRESS_START; 110 | ps->mapped_info[HEAP_SEGMENT].npages = 0; // no pages are mapped to heap yet. 111 | ps->mapped_info[HEAP_SEGMENT].seg_type = HEAP_SEGMENT; 112 | ``` 113 | 114 | 115 | 116 | ### vmm.c 117 | 需要修改原有的双向链表堆实现,适配进程控制块中新的堆管理方式。 118 | 方便起见,我们预先给用户堆分配一个页,从而进行初始化。 119 | 这个预先给堆分配一个空页的实现是错的,因为会影响到fork中拷贝父进程的堆。 120 | 这一段堆初始化代码需要到第一次malloc时才能执行。 121 | ```c 122 | heap_block *user_heap = (heap_block *)Alloc_page(); 123 | user_heap->size = PGSIZE - sizeof(heap_block); 124 | user_heap->prev = NULL; 125 | user_heap->next = NULL; 126 | user_heap->free = 1; // 初始块是空闲的 127 | user_vm_map(ps->pagetable, ps->user_heap.heap_bottom, PGSIZE, 128 | (uint64)user_heap, prot_to_type(PROT_WRITE | PROT_READ, 1)); 129 | ``` 130 | 131 | 132 | 133 | 不使用free_pages_count字段。 134 | 在malloc给用户堆分配新页中,ps->mapped_info[HEAP_SEGMENT].npages++; 135 | 在do_fork中,不需要考虑用户堆中的空闲页。 136 | 137 | 同样地,在malloc给用户栈分配新页时,也需要修改mapped_info。在handle_user_page_fault中,我们需要对于mapped_info做更新 -------------------------------------------------------------------------------- /lab/lab3_1.md: -------------------------------------------------------------------------------- 1 | # lab3_1 fork 2 | 3 | ## 实验目标 4 | 5 | 实现系统调用fork,创建子进程 6 | 对于父进程返回子进程的pid,对于子进程返回0 7 | 8 | ## 实现过程 9 | 10 | 在alloc_process中会在进程创建时,给用户栈分配一个页。 11 | 但是在fork()时,父进程的栈会被复制给子进程。 12 | 13 | 我们在不使用写时拷贝的技术时,需要在alloc_process中加一个开关,显式指定要不要创建用户栈。 14 | 15 | 我们不在alloc_process中初始化用户栈,分开实现一个init_user_stack的函数,为新创建的进程初始化用户栈。 16 | 把用户栈的初始化和用户堆的初始化都从alloc_process中解耦出来,在load_user_program中做显式调用。 17 | 这样方便我们在fork时,为子进程拷贝父进程的堆栈。 18 | 19 | 我们还需要在do_fork中做数据段的复制。 -------------------------------------------------------------------------------- /lab/lab3_2.md: -------------------------------------------------------------------------------- 1 | # lab3_2 进程yield 2 | 3 | ## 实现原理 4 | 5 | 在软中断的handle_mtimer_trap中,为每个进程记录tick_count++ 6 | 在之后的rrsched()中,进行tick_count的比较和处理。 -------------------------------------------------------------------------------- /lab/lab3_challenge1.md: -------------------------------------------------------------------------------- 1 | # lab3_challenge1 wait 2 | 3 | ## 实验要求 4 | 为用户程序实现wait系统调用 5 | wait系统调用需要实现的功能: 6 | - 当pid为-1时,父进程等待任意一个子进程退出,然后返回子进程的pid; 7 | - 如果父进程没有子进程,直接返回-1; 8 | - 当pid大于0时,父进程等待pid指定的子进程退出,然后返回子进程的pid; 9 | - 对于pid不符合要求,或者不是该进程的子进程,返回-1; 10 | 11 | 同时补充do_fork函数,实现其中子进程数据段的复制。 12 | 实验不能轮询等待,需要将父进程存入某个阻塞队列,等待信号。 13 | 14 | ## 实现原理 15 | 为每个子进程分配一个信号量:当父进程创建一个子进程时,为子进程分配一个信号量,并将其保存在子进程的控制块中。 16 | 父进程等待子进程:父进程通过 wait(pid) 系统调用查找指定的子进程,并执行 sem_P 操作等待子进程完成。父进程会阻塞,直到子进程执行完毕。 17 | 子进程终止时通知父进程:子进程终止时,通过 sem_V 操作释放父进程的信号量,从而使父进程继续执行。 18 | 19 | 20 | wait(-1)的实现中,子进程在退出时,通过指向父进程的指针做`V(sem_parent)`,这样任意的子进程退出事件都可以唤醒父进程。 21 | 但是父进程仍然需要遍历一遍自己的子进程列表,才能查找那个终止的子进程,并返回pid。 22 | 23 | ## 实现 24 | 25 | 在进程数据结构中增加一个信号量index,并在初始化过程中获取。 26 | 27 | ```c 28 | typedef struct process_t { 29 | ... 30 | int sem_index; 31 | ... 32 | 33 | }process; 34 | ``` 35 | 36 | ### alloc_process 37 | ```c 38 | process *alloc_process() { 39 | ... 40 | ps->sem_index = sem_new(0,ps->pid); 41 | ... 42 | } 43 | ``` 44 | 45 | ### free_process 46 | 在这里我们需要销毁进程对应的信号量,但是由于代理内核结构简单,还不需要考虑内存泄漏,故没有实现。 47 | 48 | ## sys_user_wait 49 | ```c 50 | int sys_user_wait(int pid) { 51 | int hartid = read_tp(); 52 | int child_found_flag = 0; 53 | if (pid == -1) { 54 | sem_P(current[hartid]->sem_index); 55 | } 56 | if (0 < pid && pid < NPROC) { 57 | process *p = &procs[pid]; 58 | if (p->parent != current[hartid]) { 59 | return -1; 60 | } else { 61 | sem_P(p->sem_index); 62 | return pid; 63 | } 64 | } 65 | return -1; 66 | } 67 | 68 | ``` 69 | 70 | ## sys_user_exit 71 | 72 | 73 | -------------------------------------------------------------------------------- /lab/lab3_challenge2.md: -------------------------------------------------------------------------------- 1 | # lab3_challenge2 实现信号量 2 | 3 | ## 实验目标 4 | 实现信号量机制,为用户提供sem_new, sem_P和sem_V三个接口。 5 | 因为内核空间逐渐变得复杂,所以我们需要预先实现一个内核堆。 6 | 7 | 后来发现不用这个内核堆也能实现需要的功能,不过它也能方便以后的开发(希望吧)。 8 | 9 | 实现唤醒某个进程的方法,是把这个进程从阻塞队列中移出之后放入准备队列中,然后调用schedule。 10 | 11 | ## 实验原理 12 | - 为用户程序提供sem_new、sem_P、 sem_V 三个接口 13 | - 由于需要分离进程与进程之间、以及进程和系统之间的信号量,所以说在信号量结构中额外添加了ps字段,用来在用户程序调用信号量时检验是否为owner 14 | 15 | 需要注意的是,从sem_P返回的时候,需要进入内核态,而不是用户态。 16 | 所以说,我们需要在sem_P阻塞进程的时候,保存内核态的上下文, 并且在调用schedule时返回到内核态的上下文。 17 | 18 | 我们需要在PCB中增加一个内核态上下文ktrapframe,还有一个内核态中断标志位,用来分辨返回时是到ktrapframe里去还是到trapframe里去。 19 | 或者利用内核栈是否为kernel_stack_top来判断目前是否还有内核态过程没有结束。 20 | 所以说我们只要保存sem_P当中的栈状态,在schedule中切换到这个栈状态,然后执行返回,就能够将内核的运行状态切换到sem_P的调用者,调用sem_P之后的那条指令了。 21 | 22 | ## 内核上下文的实现 23 | 24 | ### 在PCB中增加ktrapframe字段 25 | - 修改process_t,增加`trapframe* ktrapframe;` 26 | - 修改alloc_process(), 将ktrapframe初始化为null 27 | - 在process.c中增加save_kernel_context和restore_kernel_context函数 28 | 29 | 30 | ## 后记 31 | 32 | 成熟的wait实现,是和内核的“异步信号量”共享一个出口的,我们只是利用了sem的队列而已。sem只是服务用户的。 -------------------------------------------------------------------------------- /lab/lab3_challenge3.md: -------------------------------------------------------------------------------- 1 | # lab3_challenge3 写时拷贝 2 | 3 | ## 什么是写时拷贝技术 4 | 堆栈、和数据段,都是写时拷贝的。 5 | 6 | ## 写时拷贝技术的实现 7 | 当fork的时候,父子进程都丢失对页面的写权限 8 | 在写的时候,触发页错误,在其中做数据的复制和重新映射 9 | 系统调用返回的时候会重新做写动作,然后操作都一样。 10 | 11 | ## Q&A 12 | ### 系统如何区分页面错误中的写时拷贝和非法访问? 13 | 14 | ## 实现 15 | 主要我们需要修改fork()的代码,和操作系统处理页面错误的机制。 16 | 17 | 在内核页表中维护全局引用计数,使用sv39页表方案的保留字。 18 | 因为所有物理页都在内核页表中记录,所以说引用计数只需要考虑用户的引用。 19 | 20 | 对页错误服务程序做调整,能够识别写时拷贝和非法访问。 21 | 22 | 由于引用计数的实现,需要建一个全局的物理页描述表,做一个大工程,所以就省略了。 23 | -------------------------------------------------------------------------------- /lab/lab4_1.md: -------------------------------------------------------------------------------- 1 | # lab4_1 文件 2 | 3 | ## 实验目标 4 | - 补充rfs中的文件操作create 5 | - 理解基础文件操作 -------------------------------------------------------------------------------- /lab/lab4_3.md: -------------------------------------------------------------------------------- 1 | # lab4_3 硬链接 2 | 3 | ## 实验目标 4 | 5 | 6 | 7 | ## 参考资料 8 | 9 | 10 | 11 | ## 实验原理 12 | [Linux中的硬链接(Deepseek)](../doc/硬链接.md) 13 | 14 | 15 | 16 | 17 | ## 源代码的变化 18 | 增加了用户接口link和unlink 19 | 相应地,在rfs中的vinode接口中,增加了rfs_link和rfs_unlink 20 | ```c 21 | /**** vinode inteface ****/ 22 | const struct vinode_ops rfs_i_ops = { 23 | .viop_read = rfs_read, 24 | .viop_write = rfs_write, 25 | .viop_create = rfs_create, 26 | .viop_lseek = rfs_lseek, 27 | .viop_disk_stat = rfs_disk_stat, 28 | .viop_link = rfs_link, 29 | .viop_unlink = rfs_unlink, 30 | .viop_lookup = rfs_lookup, 31 | 32 | .viop_readdir = rfs_readdir, 33 | .viop_mkdir = rfs_mkdir, 34 | 35 | .viop_write_back_vinode = rfs_write_back_vinode, 36 | 37 | .viop_hook_opendir = rfs_hook_opendir, 38 | .viop_hook_closedir = rfs_hook_closedir, 39 | }; 40 | ``` 41 | 42 | ## link的调用关系 43 | link(path1,path2) 44 | - 虚实地址转换 45 | - do_link(path1, path2) 46 | 47 | do_link(path1, path2) 48 | - vfs_link(oldpath, newpath) 49 | 50 | vfs_link(oldpath, newpath) 51 | - 先查询路径有效 52 | - 给新路径分配dentry 53 | - 设置dentry到old_vinode的对应关系 54 | - 调用viop_link创建实际硬链接 55 | 56 | rfs_link(parent_vinode, new_dentry, old_vinode) 57 | - 所以说,在其中我们只需要登记目录项就可以了。 58 | - 需要注意处理一下返回值的传递。 59 | 60 | 61 | ## 相关源代码参考 62 | 63 | ### 64 | 65 | ### rfs_add_direntry 66 | ```c 67 | // 68 | // add a new directory entry to a directory 69 | // 70 | int rfs_add_direntry(struct vinode *dir, const char *name, int inum) { 71 | if (dir->type != DIR_I) { 72 | sprint("rfs_add_direntry: not a directory!\n"); 73 | return -1; 74 | } 75 | // 直接将子文件目录项的磁盘号附加到目录的数据块后面 76 | // 这个代码没有考虑到一个目录可以有多个目录块 77 | int block_index = dir->addrs[dir->size / RFS_BLKSIZE]; 78 | uint64 offset = dir->size % RFS_BLKSIZE; 79 | 80 | struct rfs_device *rdev = rfs_device_list[dir->sb->s_dev->dev_id]; 81 | 82 | // 读取实际磁盘块装入缓存,并写入内容 83 | if (rfs_r1block(rdev, block_index) != 0) { 84 | sprint("rfs_add_direntry: failed to read block %d!\n", block_index); 85 | return -1; 86 | } 87 | 88 | struct rfs_direntry *p_direntry = (struct rfs_direntry *)((uint64)rdev->iobuffer + offset); 89 | p_direntry->inum = inum; 90 | strcpy(p_direntry->name, name); 91 | 92 | // write the modified (parent) directory block back to disk 93 | if (rfs_w1block(rdev, block_index) != 0) { 94 | sprint("rfs_add_direntry: failed to write block %d!\n", block_index); 95 | return -1; 96 | } 97 | 98 | // update its parent dir state 99 | dir->size += sizeof(struct rfs_direntry); 100 | 101 | // write the parent dir inode back to disk 102 | if (rfs_write_back_vinode(dir) != 0) { 103 | sprint("rfs_add_direntry: failed to write back parent dir inode!\n"); 104 | return -1; 105 | } 106 | 107 | return 0; 108 | } 109 | 110 | ``` 111 | 112 | 113 | ### rfs_create 114 | ```c 115 | // 116 | // create a file with "sub_dentry->name" at directory "parent" in rfs. 117 | // return the vfs inode of the file being created. 118 | // 119 | struct vinode *rfs_create(struct vinode *parent, struct dentry *sub_dentry) { 120 | struct rfs_device *rdev = rfs_device_list[parent->sb->s_dev->dev_id]; 121 | 122 | int free_inum = 0; 123 | struct rfs_dinode *free_dinode = rfs_alloc_dinode(rdev,&free_inum); 124 | 125 | // initialize the states of the file being created 126 | 127 | /* ===== 阶段4:初始化磁盘inode ===== */ 128 | // 设置新文件元数据(原TODO部分) 129 | free_dinode->size = 0; // 初始文件大小为0字节 130 | free_dinode->type = R_FILE; // 标记为普通文件类型(需确认RFS_FILE定义) 131 | free_dinode->nlinks = 1; // 初始链接数(父目录引用) 132 | free_dinode->blocks = 1; // 占用块数(即将分配第一个数据块) 133 | 134 | // DO NOT REMOVE ANY CODE BELOW. 135 | // allocate a free block for the file 136 | free_dinode->addrs[0] = rfs_alloc_block(parent->sb); 137 | 138 | // ** write the disk inode of file being created to disk 139 | rfs_write_dinode(rdev, free_dinode, free_inum); 140 | free_page(free_dinode); 141 | 142 | // ** build vfs inode according to dinode 143 | struct vinode *new_vinode = rfs_alloc_vinode(parent->sb); 144 | new_vinode->inum = free_inum; 145 | rfs_update_vinode(new_vinode); 146 | 147 | // ** append the new file as a direntry to its parent dir 148 | int result = rfs_add_direntry(parent, sub_dentry->name, free_inum); 149 | if (result == -1) { 150 | sprint("rfs_create: rfs_add_direntry failed"); 151 | return NULL; 152 | } 153 | 154 | return new_vinode; 155 | } 156 | ``` -------------------------------------------------------------------------------- /lab/lab4_challenge1.md: -------------------------------------------------------------------------------- 1 | # lab4_challenge1 相对路径 2 | 3 | ## 实验目标 4 | 实现形如:`./file`、`./dir/file`以及`../dir/file`的路径形式 5 | 6 | ## 实验内容 7 | 8 | 在user_lib.c中,增加了`SYS_user_rcwd`和`SYS_user_ccwd`的系统调用,我们需要在内核中实现这两个系统调用。 9 | 10 | 对cwd的维护,事实上可以在process中进行,然后进程向下传递系统调用时,都传递拼接过的绝对路径。这样我们就不需要修改vfs的代码解析方法了。 11 | 12 | 理论上来说需要将字符串传回给用户,但是由于proxy kernel的极简设定,就直接在内核中打印了。 13 | 14 | cwd的dentry和初始化已经在proc_file_manager中实现了。 15 | 16 | 我们不为`../`和`./`做形式上的dentry目录项,因为可以在路径解析的时候特殊处理,并且为它们做目录项缓存很占地方。 17 | -------------------------------------------------------------------------------- /lab/lab4_challenge2.md: -------------------------------------------------------------------------------- 1 | # lab4_challenge2 重载执行 2 | 3 | ## 实验目标 4 | 5 | 建立`exec(exectuable path)`系统调用 6 | 7 | ## 实验原理 8 | 9 | fork:创建一个新进程,复制当前进程的内存空间、文件描述符、堆栈等,返回值不同:父进程返回新进程的 PID,子进程返回 0。 10 | 11 | execve:在当前进程中加载并执行一个新的程序,替换当前进程的内存空间和上下文,但保持进程ID(PID)不变。新程序直接进main入口。 12 | 13 | 你提到的 重载执行 其实是指在已有进程的上下文中加载新程序。这并不涉及创建一个新进程(这是 fork 的工作),但在实现时,它是替换当前进程的内存和状态。因此,execve 通常会和 fork 配合使用。 14 | 15 | ## 内容 16 | exec中需要释放内存中所有的旧资源(所有段,包括内核管理的数据结构等) 17 | 取消挂起的信号处理程序 18 | 19 | 解析和加载elf,重新执行加载用户程序并执行的过程 20 | 21 | 同时pid,ppid,cwd可能被保留 22 | 23 | exec对于信号的影响? 24 | 在 exec 之前,进程可能通过 sigprocmask 设定了信号屏蔽集(即哪些信号暂时不能被处理)。在 exec 之后,屏蔽集会被保留,所以如果某些信号在 exec 之前被屏蔽,在 exec 之后仍然会被屏蔽。 25 | 26 | 作者云,所以说,进程的资源需要通过pcb来统一管理 27 | 28 | 29 | 我们需要拆开解耦host elf的代码。 30 | 31 | ## 处理流程 32 | 33 | ### 进程目前的各个资源 34 | 35 | #### PCB 36 | 37 | 注释中只讨论exec有影响的内容。 38 | 39 | ```c 40 | typedef struct process_t { 41 | uint64 kstack; 42 | pagetable_t pagetable; // 需要手动做map/unmap 43 | trapframe* trapframe; 44 | trapframe* ktrapframe; 45 | 46 | // added @lab1_challenge2 这些事实上需要映射elf文件的debug段,最后一块实现 47 | char *debugline; 48 | char **dir; 49 | code_file *file; 50 | addr_line *line; 51 | int line_count; 52 | 53 | uint64 user_stack_bottom; 54 | // 随用户栈变化的,需要重置 55 | 56 | mapped_region *mapped_info; 57 | // 需要换掉里面的程序 58 | int total_mapped_region; 59 | // 内容有变化?用户的堆栈和代码数据都要重置,堆重置会-1 60 | process_heap_manager user_heap; 61 | // 用户堆需要清空 62 | uint64 pid; 63 | int status; 64 | struct process_t *parent; 65 | struct process_t *queue_next; 66 | int tick_count; 67 | int sem_index; 68 | // 69 | 70 | proc_file_management *pfiles; 71 | // 保留进程的文件资源 72 | // 局部变量丢了,再用这些文件资源有什么用? 73 | // 比如说保留与父进程通信的管道(被动),保留从终端的输入。 74 | // 可以手动设置文件是否被exec继承 75 | // 另外,在进程终止时,会关闭和释放进程持有的全部文件资源。 76 | // 进程对于文件的使用不一定是完全互斥的,多个进程可以同时读一个文件。 77 | 78 | }process; 79 | ``` 80 | 81 | #### mapped_region 82 | ```c 83 | // the VM regions mapped to a user process 84 | typedef struct mapped_region { 85 | uint64 va; // mapped virtual address 86 | uint32 npages; // mapping_info is unused if npages == 0 87 | uint32 seg_type; // segment type, one of the segment_types 88 | } mapped_region; 89 | 90 | // types of a segment 91 | enum segment_type { 92 | STACK_SEGMENT = 0, // runtime stack segmentm from init_user_stack 93 | // 重置用户栈 94 | CONTEXT_SEGMENT, // trapframe segment, from alloc_process 95 | SYSTEM_SEGMENT, // system segment,from alloc_process 96 | HEAP_SEGMENT, // runtime heap segment, from init_user_heap 97 | // 重置用户堆 98 | CODE_SEGMENT, // ELF segmentm from elf_load_segment 99 | // 重置代码 100 | DATA_SEGMENT, // ELF segment, from elf_load_segment 101 | // 重置全局数据段 102 | }; 103 | ``` 104 | 105 | #### process_heap_manager 106 | ```c 107 | typedef struct process_heap_manager { 108 | // points to the last free page in our simple heap. 109 | uint64 heap_top; 110 | // points to the bottom of our simple heap. 111 | uint64 heap_bottom; 112 | 113 | // the address of free pages in the heap 114 | // uint64 free_pages_address[MAX_HEAP_PAGES]; 115 | // the number of free pages in the heap 116 | // uint32 free_pages_count; 117 | }process_heap_manager; 118 | ``` 119 | 120 | 121 | 122 | 123 | ## exec需要清理哪些资源? 124 | 125 | ### **🔍 `exec()` 需要清理哪些资源?保留哪些资源?** 126 | `exec()` 的核心目标是 **用新的可执行文件替换当前进程的代码段、数据段和堆栈,同时尽可能保持进程的其他环境不变**。这意味着: 127 | - **必须清理的资源**:地址空间、信号处理程序等。 128 | - **必须保留的资源**:文件描述符(除 `FD_CLOEXEC`)、PID、父进程关系等。 129 | 130 | --- 131 | 132 | ## **🎯 总结** 133 | | 资源 | `exec()` 是否清理? | 备注 | 134 | |------|------------------|------| 135 | | **地址空间(代码/数据/堆/栈)** | ✅ **必须清理** | 旧代码必须释放,加载新 ELF | 136 | | **文件描述符(FD)** | 🔸 **部分清理** | 关闭 `FD_CLOEXEC`,保留普通 FD | 137 | | **信号处理程序** | ✅ **必须清理** | `sigaction` 设为 `SIG_DFL` | 138 | | **信号屏蔽集** | ❌ **保留** | `sigprocmask` 继承 | 139 | | **进程 ID(PID)** | ❌ **保留** | `getpid()` 不变 | 140 | | **父进程 ID(PPID)** | ❌ **保留** | `getppid()` 不变 | 141 | | **`wait_queue_t`** | ❌ **保留** | 允许 `wait()` | 142 | | **进程调度信息** | ❌ **保留** | `priority`、`time_slice` 仍然有效 | 143 | | **共享内存/管道/IPC** | ❌ **保留** | `shm` / `pipe` 仍然有效 | 144 | 145 | 你现在 `exec()` 里遇到哪些资源释放的问题?🚀🔥 146 | 147 | 148 | 149 | ## 进程持有哪些资源? 150 | 151 | ### 内核态资源 152 | 153 | PCB 154 | 155 | 用户页表 156 | 157 | 用户mm资源表 158 | 159 | 等待队列(没有实现) 160 | 161 | 内核栈 162 | 163 | 用来实现wait的信号量 164 | 165 | 内核使用的文件表 166 | 167 | 在调度器中的调度状态 168 | 169 | 进程间通信,和网络资源 170 | 共享内存(shmget/shmat)。 171 | 信号量(sem_t)。 172 | 管道(pipe)。 173 | 套接字(socket)。 174 | 175 | ### 用户态资源 176 | 177 | 数据段 178 | 179 | 代码段 180 | 181 | 堆 182 | 183 | 栈 184 | 185 | 用户信号量 -------------------------------------------------------------------------------- /lab/shell脚本工具.md: -------------------------------------------------------------------------------- 1 | 待完成 -------------------------------------------------------------------------------- /lab/环境配置.md: -------------------------------------------------------------------------------- 1 | # 一、 容器环境搭建过程(Docker+vscode) 2 | ## 1. 为什么使用docker+vscode 3 | - Docker 提供了一个轻量级的虚拟化环境,能够简化开发、测试和部署过程。使用 Docker,开发者可以在隔离的容器中运行 RISC-V Proxy Kernel 环境,确保一致的开发环境,不受主机操作系统差异影响。此外,Docker 还可以提高资源利用效率,减少系统配置的复杂性,使得团队成员之间可以快速共享相同的开发环境,减少“在我的机器上能运行”的问题。 4 | - 与 VSCode 配合使用时,Docker 提供了一个无缝集成的开发环境。通过 VSCode 的 Remote - Containers 插件,开发者能够直接在容器内进行代码编辑、文件上传/下载、关键字搜索和调试。这样,所有的开发操作都可以在容器内完成,不需要担心环境配置和依赖问题,同时提高了开发效率。通过 VSCode,你可以轻松管理容器中的文件、调试程序,并享受一个稳定、高效的开发体验。 5 | ## 2. 下载并安装Docker desktop 6 | - 下载地址请谷歌 7 | ## 3. 下载和配置镜像 8 | ### 下载ubuntu镜像(22.04 LTS noble)并重命名 9 | 10 | 11 | - amd64主机: 12 | ```Bash 13 | docker pull crpi-x7y7w4q8rsfacqq9.cn-shanghai.personal.cr.aliyuncs.com/cubelander-images/ubuntu:noble 14 | docker tag crpi-x7y7w4q8rsfacqq9.cn-shanghai.personal.cr.aliyuncs.com/cubelander-images/ubuntu:noble pke_mirror 15 | ``` 16 | - arm64主机: 17 | ```Bash 18 | docker pull crpi-x7y7w4q8rsfacqq9.cn-shanghai.personal.cr.aliyuncs.com/cubelander-images/arm64_ubuntu:noble 19 | docker tag crpi-x7y7w4q8rsfacqq9.cn-shanghai.personal.cr.aliyuncs.com/cubelander-images/arm64_ubuntu:noble pke_mirror 20 | ``` 21 | 22 | > 具体拉取镜像源的方式可以参考:tech-shrimp/docker_installer: Docker官方安装包,用来解决因国内网络无法安装使用Docker的问题 ,我参考这个项目下载了一个原始的ubuntu镜像源。 23 | 因此我不得不重新按照github上面的教程重新安装一遍riscv提供的工具链。 24 | 25 | ### 在后台运行Container 26 | ```Bash 27 | docker run -d --name pke_container pke_mirror:latest tail -f /dev/null 28 | ``` 29 | - 这条命令通过执行 `tail -f /dev/null` 来使容器保持运行。 30 | - `tail -f /dev/null` 是一个不会退出的命令,它会一直保持容器运行直到你手动停止。以便后续使用vscode连接 31 | 32 | ### 安装前置软件包 33 | 由于更换软件源调起来很麻烦,故略过,详细请向ai提问。 34 | 安装了build-essential,git,sudo,vim, nano等必要软件。 35 | 从github下载安装了riscv-gnu-toolchain 36 | ## 4. 使用vscode连接到容器 37 | ### 下载vscode并安装插件 38 | - 在extensions中安装Docker插件和Remote Development工具包: 39 | 40 | ### 连接到容器 41 | - 在Remote Explorer中选择Dev Containers,然后Attach in Current Window 42 | - 接下来远程容器中自动安装完vscode远程服务器插件后,即可无缝进入开发环境。 43 | ## 5. 使用vscode 44 | ### 设置容器代理(可选) 45 | - 直接在Clash中打开TUN模式(代理本机所有流量)即可让容器产生的全部流量走clash。无需额外的配置。 46 | ### 在容器内安装VSCode扩展工具 47 | - Microsoft C/C++ Extension 48 | - C/C++ themes 49 | - ASM Code Lens 50 | - CMake(虽然本实验用不上) 51 | - Makefile Tools 52 | ### 利用vscode功能 53 | - 文件系统、命令行、编辑器和代码辅助的深度集成。 54 | - 搜索和跳转到定义、实现功能可以极大地辅助开发过程。 55 | - 请读者自行了解。 56 | 57 | 58 | # 二、 容器内pke工具链安装 59 | > riscv-gnu-toolchain非常庞大(我下了10g左右的源代码),需要一个强而有力的梯子下载,同时做大量的编译。 60 | ## 1. 安装前置软件工具包 61 | ```Bash 62 | sudo apt update 63 | sudo apt install autoconf automake autotools-dev curl python3 libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex libexpat1-dev zlib1g-dev locales texinfo device-tree-compiler 64 | ``` 65 | ## 2. 安装交叉编译器 66 | 1. 首先克隆riscv-gnu-toolchain的源代码(包括所有子仓库) 67 | ```Bash 68 | git clone --recursive https://github.com/riscv/riscv-gnu-toolchain.git 69 | cd riscv-gnu-toolchain 70 | ``` 71 | 注意,里面有非常多的子模块,成功克隆完需要借助这个文档:[Git:克隆维护子模块](https://kdocs.cn/l/ctXpvObZ8XeV) 72 | 2. 运行命令 73 | ``` 74 | ./configure 75 | # 执行默认编译配置,不需要额外设环境变量路径。 76 | ``` 77 | 78 | 3. 反复执行`make -$j(nproc)` ,直到没有更多编译任务(或者执行一遍全部做完,取决于内核版本) 79 | 4. 安装编译好的安装包: 80 | ``` 81 | cd build-binutils-newlib 82 | make install 83 | cd .. 84 | 85 | cd build-gcc-newlib-stage2 86 | make install 87 | cd .. 88 | 89 | cd build-gdb-newlib 90 | make install 91 | cd .. 92 | 93 | cd build-newlib 94 | make install 95 | cd .. 96 | 97 | cd build-newlib-nano 98 | make install 99 | cd .. 100 | ``` 101 | 3. 安装spike仿真器 102 | spike仿真器的源代码无需另外下载,在riscv-gnu-toolchain仓库中是一并下载好的。 103 | ``` 104 | cd ~/riscv-gnu-toolchain/spike 105 | ./configure 106 | make 107 | make install 108 | ``` -------------------------------------------------------------------------------- /lab/调试工具.md: -------------------------------------------------------------------------------- 1 | # 调试工具 2 | 3 | ## 参考资料 4 | [riscv-software-src/riscv-isa-sim](https://github.com/riscv-software-src/riscv-isa-sim) 5 | 6 | 其中记录了详细的使用openocd建立调试监听端口,然后使用gdb去连接的过程 7 | 8 | ## 安装过程 9 | [msteveb/jimtcl](https://github.com/msteveb/jimtcl) 10 | 11 | 首先安装jimtcl,这是openocd的前置 12 | 13 | 如果出现编译失败的问题,需要根据debug输出里的提示,在makefile里稍微调一下编译选项。(我记得是缺少一个数学库,加入一个包含-I就可以了) 14 | 15 | [riscv-collab/riscv-openocd](https://github.com/riscv-collab/riscv-openocd) 16 | 17 | 然后安装openocd,即完成了全部前置 18 | 19 | ## 调试脚本的使用 20 | 我们需要把时钟中断给关掉,不然每次进断点的时候都会触发时钟中断。 21 | 在minit.c中注释掉timerinit. 22 | 打开汇编: 23 | ``` 24 | (gdb) disassemble 25 | 26 | ``` 27 | 28 | 29 | ## 调试脚本 30 | 在实验根目录下创建`debug.sh`,修改`.spike.cfg`文件名为`spike.cfg`: 31 | ### `debug.sh` 32 | 33 | ```sh 34 | #!/bin/bash 35 | # 释放资源 36 | lsof -ti:9824 | xargs kill -9 37 | lsof -ti:3333 | xargs kill -9 38 | lsof -ti:6666 | xargs kill -9 39 | pkill spike -9 40 | pkill openocd -9 41 | pkill cpptools-srv -9 42 | # 执行make clean和make任务,可以直接换成对应的命令 43 | # source ./compile.sh 44 | make clean 45 | make 46 | # 设置变量 47 | PKE="./obj/riscv-pke" 48 | USER_PROGRAM="./obj/app_print_backtrace" 49 | PROGRAM="$PKE $USER_PROGRAM" 50 | SPIKE_PORT="9824" 51 | OPENOCD_CFG="spike.cfg" 52 | GDB_CMD="riscv64-unknown-elf-gdb" 53 | TARGET_REMOTE="localhost:3333" 54 | 55 | # 1. 运行 Spike,并监听远程 Bitbang 连接 56 | echo "Starting Spike with remote bitbang on port $SPIKE_PORT..." 57 | spike --rbb-port=$SPIKE_PORT --halted -m0x80000000:0x82000000 $PROGRAM & 58 | 59 | # 确保 Spike 启动成功 60 | sleep 2 61 | 62 | # 2. 启动 OpenOCD 使用指定配置文件 63 | echo "Starting OpenOCD with configuration file $OPENOCD_CFG..." 64 | openocd -f $OPENOCD_CFG -c "reset halt" & 65 | 66 | # 确保 OpenOCD 启动成功 67 | sleep 2 68 | 69 | # 3. 启动 GDB,连接到目标远程调试 70 | echo "Starting GDB and connecting to $TARGET_REMOTE..." 71 | riscv64-unknown-elf-gdb -ex "target extended-remote $TARGET_REMOTE" \ 72 | -ex "b switch_to" \ 73 | -ex "c " \ 74 | $PROGRAM 75 | # 4. 结束后清理后台进程并释放资源 76 | echo "Debugging session completed. Cleaning up..." 77 | lsof -ti:9824 | xargs kill -9 78 | lsof -ti:3333 | xargs kill -9 79 | lsof -ti:6666 | xargs kill -9 80 | ``` 81 | 在使用调试脚本时,需要修改`USER_PROGRAM`为当前分支的用户程序,和在第3节gdb的命令中手动设置第一个断点(根据你想要的符号名) 82 | 修改这一行代码: 83 | ``` 84 | -ex "b switch_to" \ 85 | ``` 86 | 87 | ### 'spike.cfg' 88 | 89 | ``` 90 | adapter driver remote_bitbang 91 | remote_bitbang host localhost 92 | remote_bitbang port 9824 93 | 94 | set _CHIPNAME riscv 95 | jtag newtap $_CHIPNAME cpu -irlen 5 96 | 97 | set _TARGETNAME $_CHIPNAME.cpu 98 | target create $_TARGETNAME riscv -chain-position $_TARGETNAME 99 | 100 | gdb report_data_abort enable 101 | 102 | 103 | 104 | init 105 | halt 106 | 107 | ``` --------------------------------------------------------------------------------