rcore lab5&6 记录

新增了一个 shell。在此之前,程序时作为 “task” 存在的,而从这章开始,引入了进程的概念,不必按一定顺序依次执行所有程序了。

在之前的结构体的基础上,进行了一些变化:

结构体/模块名 变化/新增内容说明 作用
PidHandle 新增,用于唯一标识每个进程,并通过 PidAllocator 管理 提供进程的唯一标识和资源管理
PidAllocator 新增,负责分配和回收 PID 这是一个分配器,类似之前的 FrameAllocator
KernelStack 新增 每个进程有独立的内核栈以隔离执行上下文。RAII 自动回收物理页面。
TaskControlBlock 扩展,添加进程相关信息,如地址空间、文件表等 描述完整进程信息(不再只是一个“任务”)
TaskManager 拓展功能,从原本的简单任务队列演变为支持多进程调度、切换 全局调度器,支持并发进程管理
Processor 拓展,记录当前正在运行的进程(而非仅任务) 支持多核处理器上的多进程调度
由于本章需要进行进程之间的切换与调度,所以设计了 pid 机制来标识不同的进程,这些不同的进程需要不同的内核栈才能保证在上下文切换的时候不混乱(下文解释了)。为了保存进程信息,去修改了之前的 Task 管理器,并且拆分了进程调度的部分为一个新的结构体 Processor
内核栈用来做什么

本章新增里一个结构体是内核栈,在此之前,是没有给不同进程单独分配“内核栈”的做法的,因为之前的进程按照顺序执行,而从这章开始,涉及不同进程的调度和切换,如果两个进程都需要陷入内核的话,只有一个内核栈就会产生冲突了。

rust

pub struct TaskControlBlock {
    pub pid: PidHandle,
    pub kernel_stack: KernelStack,
    inner: UPSafeCell<TaskControlBlockInner>,
}

pub struct TaskControlBlockInner {
    pub trap_cx_ppn: PhysPageNum,
    pub base_size: usize,
    pub task_cx: TaskContext,
    pub task_status: TaskStatus,
    pub memory_set: MemorySet,
    pub parent: Option<Weak<TaskControlBlock>>,
    pub children: Vec<Arc<TaskControlBlock>>,
    pub exit_code: i32,
    pub heap_bottom: usize,
    pub program_brk: usize,
}

里面用到了几个指针,Weak Arc UPSafeCell。

这个结构的设计在 实现批处理操作系统 文档 中有详细介绍。UPSafeCellRefCell 的封装,既实现了内部可变性,又可以保证它包裹的数据的独占访问权。

Rust 有两种方式做到可变性

  • 继承可变性:比如一个 struct 声明时指定 let mut, 那么后续可以修改这个结构体的任一字段
  • 内部可变性:使用 Cell RefCell 包装变量或字段,这样即使外部的变量是只读的,也可以修改

看似继承可变性就够了,那么为什么还需要所谓的 interior mutability 内部可变性呢?因为 &mut 要求独占性,而有些场景需要共享访问。所以当一个结构需要共享访问,其成员又是 mutable 的时候,可以使用 RefCell 包装。

因为这样的包装就约定了,调用 exclusive_access 才可以得到它包裹的数据的独占访问权,获得一个可变的标记,销毁当前的标记才能进行下一次访问。

因为任务控制块经常需要被放入/取出,如果直接移动任务控制块自身将会带来大量的数据拷贝开销, 而对于 Arc 智能指针进行移动则没有多少开销。

cheats.rs 这张图片很形象

![[Pasted image 20250407210358.png]]

Arc<T> 的核心是一个指向堆分配结构的指针,另外还有强的计数器和弱的计数器,每次引用的时候,弱计数器增加:

rust

struct ArcInner<T> {
    strong: AtomicUsize,   // 强引用计数(原子操作)
    weak: AtomicUsize,     // 弱引用计数(原子操作 + 1,用于 Weak<T>)
    data: T,               // 实际存储的数据
}

pub struct Arc<T> {
    ptr: NonNull<ArcInner<T>>, // 指向 ArcInner 的指针
    phantom: PhantomData<ArcInner<T>>,
}

由于裸指针 (*mutNonNull) 不携带所有权和生命周期信息PhantomData 用于向编译器表明:

  • 所有权关系:Arc<T> 逻辑上拥有 ArcInner<T> 的所有权(即使通过指针间接持有)。
  • 生命周期关联:Arc<T> 的生命周期与 ArcInner<T> 中的 T 绑定,确保 T 的内存安全。

Weak 智能指针只是引用,不增加计数器,在这里也就是父进程死了,子进程的父进程 TaskControlBlock 的引用,也会死掉,不会留着。

文件相关的结构和 linux 比较类似的,stdin stdout 也是 FILE 结构体,每个进程有自己的 fd_table。这里用了多态(和 linux 类似),统一管理任何实现了 File Trait 的类型如 Stdin/Stdout

rust

// os/src/task/task.rs

pub struct TaskControlBlockInner {
	pub trap_cx_ppn: PhysPageNum,
	pub base_size: usize,
	pub task_cx: TaskContext,
	pub task_status: TaskStatus,
	pub parent: Option<Weak<TaskControlBlock>>,
	pub children: Vec<Arc<TaskControlBlock>>,
	pub exit_code: i32,
	pub fd_table: Vec<Option<Arc<dyn File + Send + Sync>>>,

image-20250411181846753

虽然层次非常复杂,但是编程的部分其实只涉及到了部分结构体,比如 disk_inodeinodeOSInode

disk_inode 记录的是实际上存放在磁盘里面的文件的 metadata,而 inode 是面向用户操作的,比如说 open 就会返回一个 inode。OSInode 包装了 Inode,也实现了 File Trait。

值得关注的是打包的方式。

打包是怎么做的?

在 easy_fs_pack 中,创建一个文件,在这个文件的基础上创建 BlockFile,把 app 的数据写道 BlockFile 里面(也就是写到这个新创建的文件里面)

easy-fs 与底层设备驱动之间通过抽象接口 BlockDevice 来连接,采用轮询方式访问 virtio_blk 虚拟磁盘设备,避免调用外设中断的相关内核函数。easy-fs 避免了直接访问进程相关的数据和函数,从而能独立于内核开发。

解耦合之后,内核怎么通过驱动和 fs 沟通呢?Virtio 设备的特点有:

  • 通过直接内存访问(DMA)实现高效数据传输。
  • 使用 VirtQueue 环形队列,通过这个结构来通知读写消息。

Hardware Abstraction Layer 的缩写。Virtio 设备(如块设备、网卡)需要通过 DMA(直接内存访问) 直接读写物理内存。因此 virtio_drivers 库抽象出 Hal trait,要求开发者根据具体操作系统实现:

  • 物理内存分配:为 DMA 分配连续的物理页帧。
  • 物理地址转换:将虚拟地址转换为物理地址(或直接返回物理地址)。
  • 物理/虚拟地址映射:处理地址空间的差异(如 MMU 是否启用)。

为了完成内存分配涉及到的函数有:

方法 功能
dma_alloc(pages) 分配 pages 个连续的物理页帧,返回起始物理地址(用于 DMA 缓冲区)。
dma_dealloc(pa, pages) 释放从物理地址 pa 开始的 pages 个连续页帧。
phys_to_virt(addr) 将物理地址转换为虚拟地址(若物理地址直接映射到虚拟地址,可直接返回 addr)。
virt_to_phys(vaddr) 将虚拟地址转换为物理地址(通过查询页表)。

有一个介绍的文章来分析 virtqueue 的作用 Virtqueues and virtio ring: How the data travels

virtqueue 包括三个结构:

  • Descriptor Table
    • 描述内存缓冲区的位置、长度和读写方向。
    • 每个 descriptor 结构体包括地址、长度、flags 和下一个索引。
  • Available Ring
    • Guest 填好 descriptor 后,把 descriptor 索引写入这个 ring。
    • 表示“这些请求我准备好了,可以处理”。
  • Used Ring
    • Host 处理完请求后,把结果的 descriptor 索引写入这里。
    • Guest 轮询或中断通知方式查看哪些请求处理完成。

一看就懂的图:

这一章节可以 git cherry-pick ch5 了,conflict 少了一点。

从 fd 为了向下转换,使用 downcast_ref 配合 Any trait。

这样不行:

rust

    if let Some(inode) = &inner.fd_table[fd] {
	    let inode_any: &dyn Any = inode;
        // check inode type is OSInode
        if let Some(os_inode) = inode_any.downcast_ref::<OSInode>() {

因为 fd_table 数组中存放的是 Arc<dyn File + ...>,需修改 File trait 使其支持类型向下转换。

rust

pub trait File: Any + Send + Sync {

    /// convert to a trait object
    fn as_any(&self) -> &dyn Any;
}

dyn Any:表示一个动态分发的 trait 对象,指向某个实现了 Any trait 的具体类型。

这么写不行

rust

let inner = current_task().unwrap().inner_exclusive_access();

得这么写

rust

    let task = current_task().unwrap();
    let inner = task.inner_exclusive_access();

因为获取一个临时值的 inner 也是临时的。

text

error[E0716]: temporary value dropped while borrowed
  --> src/syscall/fs.rs:84:17
   |
84 |     let inner = current_task().unwrap().inner_exclusive_access();
   |                 ^^^^^^^^^^^^^^^^^^^^^^^                         - temporary value is freed at the end of this statement
   |                 |
   |                 creates a temporary value which is freed while still in use
85 |     if let Some(inode) = &inner.fd_table[fd] {
   |                           ----- borrow later used here
   |
help: consider using a `let` binding to create a longer lived value
   |
84 ~     let binding = current_task().unwrap();
85 ~     let inner = binding.inner_exclusive_access();