Skip to content
Zhiyuan Zeng edited this page Dec 19, 2022 · 21 revisions

uCore-Tutorial-Code-2022A说明

项目文档

地址:https://jude-lu.github.io/uCore-Tutorial-Code-2022A/

如何部署

添加注释

更多规范参见:https://doxygen.nl/manual/docblocks.html

  • 文件注释
/**
 * @file bio.c
 * @brief Buffer cache.
 * @details
 * The buffer cache is a linked list of buf structures holding \n
 * cached copies of disk block contents.  Caching disk blocks \n
 * in memory reduces the number of disk reads and also provides \n
 * a synchronization point for disk blocks used by multiple processes.
 */
  • 变量注释
struct superblock {
    uint magic; ///< Must be FSMAGIC
    uint size; ///< Size of file system image (blocks)
    uint nblocks; ///< Number of data blocks
    uint ninodes; ///< Number of inodes.
    uint inodestart; ///< Block number of first inode block
    uint bmapstart; ///< Block number of first free map block
};
  • 函数注释
/**
 * @brief Free user memory pages, then free page-table pages.
 * @param max_page The max vaddr of user-space.
 */
  • 单行注释
/// 初始化sys_context
  • 多行注释
/**
 * Used in fork. \n
 * Copy the pagetable page and all the user pages. \n
 * Return 0 on success, -1 on error.
 */

使用说明

总体概述

约定

每个模块中都必须包含4个文件:defs.hlog.hdependency.hmodule.h

  1. defs.h规定了uCore项目的所有语义规范,如需开发其它模块,也要遵守该约定,包含类型定义、宏定义和riscv汇编函数
  2. log.h规定了uCore项目的打印输出规范,如需在其它模块中输出内核调试信息,也要引用该文件,包含了不同级别的调试输出函数
  3. dependency.h列出了该模块需要内核提供的所有函数,如需使用该模块,必须要在内核中有相应的实现,否则无法运行,没有列出的函数默认模块自己可以提供支持
  4. module.h列出了该模块可以向内核提供的全部功能,内核如需使用该模块,只需引用该头文件即可,模块中所有对外暴露的接口函数和数据结构都应在该头文件中被包含

理念

总体设计思路是:使OS的各个功能模块完全解耦,互相之间没有依赖。有些模块的函数可能确实需要另一个模块的函数接口,但是我们在设计时不直接调用其接口,而是需要使用者提供一个提供该功能的函数地址,传入对应的结构体中。

由于目前uCore的所有模块均为同一团队设计,所以这里无需对齐,可以直接将另一个模块的函数地址直接传入。但如果之后某一模块的函数接口发生了修改,则与其关联的模块均无需修改,而由该模块的使用者来完成二者的对齐工作,将新函数的地址传入。

有两处例外:一是utils模块,该模块不属于功能模块,而是工具类,故其它模块可以直接调用该模块的函数接口;二是console模块,该模块属于功能模块,但是每个模块都需要其提供的打印功能来debug(当然也可以都删除,就实现了完全解决依赖,但这样显然不好),全部按照一般的设计显得很啰嗦,故采用了extern的方法绕开这个问题,向使用者示意,这里需要一个统一的打印输出函数,对于uCore而言,就是console模块。

结构

为了说明模块之间确实已经完全解耦,我们将整个项目完全包含在一个没有分支的目录下,一共有三类文件夹:一是模块的文件夹,二是每章OS的文件夹,三是辅助的文件(如bootloader、nfs、asm、script等)。每个模块的文件夹下都有单独的Makefile,可以编译成静态库文件。每个OS文件夹都可以编译成内核kernel,如需使用任何模块直接链接对应的静态库即可。理论上,可以实现模块与OS在不同的项目中,但是这样需要将使用到的模块的所有头文件都拷贝一份到OS文件夹中,不是很有必要,所以暂且放在同一根目录下,可以直接通过modules.h进行引用,具体设计可参见代码。

模块划分

console

  • 描述:包含3个源文件console.cprintf.csbi.c,主要负责最底层输入输出等操作。

  • 函数:

    void consputc(int); // 控制台输出字符
    int consgetc(); // 控制台输入字符
    void console_init(); // 控制台初始化
    
    void printf(char*, ...); // 最基本的printf函数
    
    void console_putchar(int); // 调用ecall实现putchar
    int console_getchar(); // 调用ecall实现getchar
    void shutdown(); // 调用ecall实现关机
    void set_timer(uint64); // 调用ecall设置下一时钟中断
  • 评价:该模块中的函数实现均为直接调用汇编ecall完成底层任务,其它模块基本都需要其提供的printf和shutdown函数,采用全局定义,其实现即在该模块中。(与其它模块的设计不太一样)

  • dependency:

    extern int procid();
    extern int threadid();
  • module:

    #include "console.h"
    #include "printf.h"
    #include "sbi.h"

disk

  • 描述:该模块实现对磁盘块的读写,原则上应当搭配磁盘缓存机制。

  • 结构体:

    struct buf {
        int valid; ///< has data been read from disk?
        int disk; ///< does disk "own" buf?
        uint dev;
        uint blockno;
        uint refcnt;
        struct buf *prev; ///< LRU cache list
        struct buf *next;
        uchar data[BSIZE];
    }; // 实现了磁盘缓存的数据结构buf,一个buf对应了一个磁盘的block
    
    struct virtio_context
    {
        void (*yield)(); // 对于操作外设的模块,可能需要交出当前进程的 CPU 控制权 
    };
  • 函数:

    void set_virtio(struct virtio_context *virtio_context); // 设置virtio_disk_context
    
    void binit(); // 初始化一个buf
    void brelse(void*); // 释放一个buffer
    
    void* bread(uint dev, uint blockno); // 读磁盘上block到缓存buf的数据,返回buf的指针。dev是设备编号,blockno是磁盘block的编号
    void bwrite(void*); // 把buf里面的数据写回磁盘上block,指针指向该buf
    
    // 增加和减少一个buf的引用计数
    void bpin(void*);
    void bunpin(void*);
    
    uchar* buf_data(void*); // 返回buf存放实际数据位置的地址
  • 评价:disk模块是一个与磁盘这样一种外设交互的模块,其需要提供一些对磁盘block操作的接口。一般来说,因为读写磁盘非常非常慢,所以该模块的实现应当基于磁盘缓存机制;具体来说,对磁盘的读写都会被转化为对 buf 的读写;当 buf 有效时,读写 buf;buf 无效时(类似页表缺页或者 TLB 缺失),就实际读写磁盘、将 buf 变得有效,然后继续读写 buf。所以,接口实现实际是在对buf进行操作,并且上层使用者不用关心这一缓存机制的具体实现,可以认为对buf读写就是在对磁盘直接读写,这也可以理解为做了一层抽象。

  • dependency:

    #include "string.h"
    
    extern int PID;
    
    extern void printf(char*, ...);
    extern int procid();
    extern int threadid();
    extern void dummy(int, ...);
    extern void shutdown();
  • module:

    #include "bio.h"
    #include "virtio.h"

easy-fs

  • 描述:uCore的文件系统。

  • 结构体:

    struct inode {
        uint dev; ///< Device number
        uint inum; ///< Inode number
        int ref; ///< Reference count
        int valid; ///< inode has been read from disk?
        short type; ///< copy of disk inode
        uint size;
        uint addrs[NDIRECT + 1];
    }; // inode 指的是Index Node(索引节点),是文件系统中的一种重要数据结构。逻辑目录树结构中的每个文件和目录都对应一个 inode。在 inode 中不仅包含了文件/目录的元数据(大小/访问权限/类型等信息),还包含实际保存对应文件/目录数据的数据块(位于最后的数据块区域中)的索引信息,从而能够找到文件/目录的数据被保存在磁盘的哪些块中。
    
    struct file {
        enum { FD_NONE = 0, FD_PIPE, FD_INODE, FD_STDIO } type;
        int ref; ///< reference count
        char readable;
        char writable;
        void *pipe; ///< FD_PIPE
        struct inode *ip; ///< FD_INODE
        uint off; // 读写文件的偏移量;可以使用lseek函数来改变这个偏移的位置以支持随机读写
    }; // 在进程中,我们使用file结构体来标识一个被进程使用的文件
    
    struct FSManager
    {
        int (*fdalloc)(struct file *f); // 文件对于进程而言也是其需要记录的一种资源,每个打开的文件会有一个非负整数 fd 指代它,这是一种已经被打开文件的索引。该接口用于让当前进程分配一个新的fd。
        struct file* (*filealloc)(); // 同上,分配进程中实际存储 struct file 指针的资源
    
        // 以下三个接口用于和当前进程的虚存做交互
        pagetable_t (*get_curr_pagetable)();
        int (*either_copyout)(pagetable_t, int, uint64, char*, uint64);
        int (*either_copyin)(pagetable_t, int, uint64, char*, uint64);
    
        // 对于用户来说,pipe被抽象为了文件,也就是说文件系统要提供操作pipe的接口。实际的实现文件系统不负责,需要内核核心提供的创建和关闭管道的接口;内核核心可以直接使用pipe模块提供的接口
        void* (*pipeopen)();
        void (*pipeclose)(void *_pi, int writable);
    
        // 以下为与磁盘交互的接口。内核核心可以把disk模块对外暴露的接口提供给easy-fs模块使用
        void* (*bread)(uint, uint);
        void (*brelse)(void*);
        void (*bwrite)(void*);
        void (*bpin)(void*);
        void (*bunpin)(void*);
        uchar* (*buf_data)(void*);
    };
  • 函数:

    void set_file(struct FSManager *FSManager); // 设置fs_manager
    
    int fileopen(char *path, uint64 omode); // 打开路径为path的文件;如果omode为O_CREATE需要新建一个文件,否则打开一个已有的文件
    void fileclose(struct file *f); // 关闭文件f
    
    int pipealloc(struct file *f1, struct file *f2); // 新建一个pipe,读端和写端对用户暴露接口的文件分别为f1和f2
    
    struct file *stdio_init(int fd); // 初始化一个file类型的结构体
    
    uint64 inodewrite(struct file *f, uint64 va, uint64 len); // 向文件写数据,写入数据的起始虚拟地址是va、数据的字节长度为len,返回成功写入的数据长度。这里取名为inode 是因为,easy-fs实际上是对文件被绑定的inode写数据的;考虑同时打开了多次同一个文件(假设不关闭)的情形,这时候进程里面会有多个fd(以及对应的 file 结构体,它们会有不同的off表示读写的偏移量)对应同一个inode
    uint64 inoderead(struct file *f, uint64 va, uint64 len); // 从文件读数据
    
    struct inode *namei(char *path); // 返回path路径对应文件的inode
  • 评价:完整的文件系统需要实现很多系统调用,我们这里的easy-fs模块只提供了很少一部分的接口,所以能实现的系统调用比较有限。

  • dependency:

    #include "string.h"
    
    extern void printf(char*, ...);
    extern int procid();
    extern int threadid();
    extern void dummy(int, ...);
    extern void shutdown();
  • module:

    #include "fs.h"
    #include "file.h"

kernel-vm

  • 描述:包含三个源文件kalloc.cmap.cvm.c,负责全部内存部分的管理。

  • 函数:

    void* kalloc(); // 分配一块内存
    void kfree(void*); // 释放一块内存
    void kinit(); // 内存分配初始化
    
    void kvmmap(pagetable_t, uint64, uint64, uint64, int); // 为内核分配虚拟地址空间
    int uvmmap(pagetable_t, uint64, uint64, int); // 建立虚拟地址到物理地址映射
    void uvmunmap(pagetable_t, uint64, uint64, int); // 释放虚拟地址到物理地址映射
    pagetable_t uvmcreate(); // 创建空用户页表
    void uvmfree(pagetable_t, uint64); // 释放用户地址空间
    int uvmcopy(pagetable_t, pagetable_t, uint64); // 拷贝用户地址空间
    
    pte_t* walk(pagetable_t, uint64, int); // 两种功能:地址转换;建立映射
    uint64 useraddr(pagetable_t, uint64); // 虚拟地址转物理地址函数
    void freewalk(pagetable_t); // 释放地址空间函数
    // 以下为U态和S态拷贝函数
    int copyout(pagetable_t, uint64, char*, uint64);
    int copyin(pagetable_t, char*, uint64, uint64);
    int copyinstr(pagetable_t, char*, uint64, uint64);
    int either_copyout(pagetable_t, int, uint64, char*, uint64);
    int either_copyin(pagetable_t, int, uint64, char*, uint64);
  • 评价:kalloc.c主要负责物理内存的管理,vm.c主要负责虚拟内存的管理,map.c主要负责页表的管理。有一点说明:三个文件中的函数或许不够凝练(如copy函数等),不能完全把kernel-vm模块最有共性的内容抽取出来,是因为ucore项目的历史原因,如果过于追求common,则每章的os都要实现完全相同的很多函数,使代码的冗余度增加。后期可以考虑更细粒度的模块划分,将非最核心的函数拿出到二级模块中,作为ucore项目的特殊适配模块。

  • dependency:

    #include "string.h"
    
    extern char ekernel[];
    extern char trampoline[];
    
    extern void printf(char*, ...);
    extern int procid();
    extern int threadid();
    extern void dummy(int, ...);
    extern void shutdown();
  • module:

    #include "kalloc.h"
    #include "vm.h"
    #include "map.h"

pipe

  • 描述:管道的内部实现。

  • 结构体:

    struct pipe; // 实现管道的数据结构
    
    struct pipe_context
    {
        pagetable_t (*get_curr_pagetable)(); // 得到当前进程的页表
        void (*yield)(); // 写一个满的管道或者读一个空的管道时,需要等待其它进程的操作,所以要交出 CPU 控制权
    
        // 管道的数据是存放在物理内存里面的ring buffer,因此需要分配和释放物理内存。这两个接口可以直接让模块分配和释放物理页
        void* (*kalloc)();
        void (*kfree)(void* pa);
    
        // 管道需要和进程虚拟内存里面的数据进行交互,比如把进程虚存里面的东西写入管道里面,因此需要copyin和copyout接口
    	int (*copyin)(pagetable_t pagetable, char* dst, uint64 srcva, uint64 len);
        int (*copyout)(pagetable_t pagetable, uint64 dstva, char* src, uint64 len);
    };
  • 函数:

    void set_pipe(struct pipe_context *pipe_context); // 初始化pipe_os_context
    
    void* pipeopen(); // 创建一个管道
    void pipeclose(void *_pi, int writable); // 关闭一个管道的读/写端。如果writeable为1表示关闭写端,否则表示关闭读端
    int pipewrite(void *_pi, uint64 addr, int n); // 往一个管道里面写数据。数据的虚存起始地址为addr,有n个字节
    int piperead(void *_pi, uint64 addr, int n); // 从一个管道里面读数据
  • 评价:这个模块完成了pipe的内部实现。需要注意的是,对于应用程序而言pipe被抽象成了文件,这样应用程序是通过操作文件的接口来操作pipe的。在目前的设计里面,我们把这部分接口的实现放在了文件系统里面,而内核核心可以通过向文件系统提供上面的接口,使得文件系统可以完成接口的实现。

  • dependency:

    extern void printf(char*, ...);
    extern int procid();
    extern int threadid();
    extern void dummy(int, ...);
    extern void shutdown();
  • module:

    #include "pipe.h"

signal

  • 描述:信号模块,实现了相关的数据结构及维护操作,具体的响应操作(包括需要内核处理的信号,和调用用户注册的handler)由内核核心完成。

  • 结构体:

    // 对于某种具体的信号,会有一个sigaction标明其相关信息
    struct sigaction {
        void (*handler)(int); ///< 该信号(由用户注册的)signal handler
        uint64   sa_mask; // 在处理该信号的时候,需要屏蔽的信号
    };
    
    // 进程数据结构里面负责信号的数据结构
    struct signal_block {
        uint32 signals; // 要响应的信号
        uint32 signal_mask; // 要屏蔽的信号
        uint32 handling_sig; // 正在处理的信号。如果没有正在处理任何信号,则该变量为0
        int killed, frozen; // 当前进程是否已经被杀死/是否被冻住
        struct sigaction sig_actions[32]; // 每个种类的信号对应的sigaction数据结构
    };
    
    struct signal_context {
        pagetable_t (*get_curr_pagetable)();  ///< 得到当前进程的页表
    
        struct signal_block* (*get_curr_sig_block)(); ///< 得到当前进程的signal_block
        struct signal_block* (*pid2sig_block)(int pid); ///< 返回pid进程对应的signal_block数据结构的指针。向该进程发送信号的时候需要对该数据结构进行操作
    
        void (*customized_sigreturn)(); ///< 恢复到执行signal handler之前的状态
    
        /// sigaction需要和进程虚拟内存里面的数据进行交互
        int (*copyin)(pagetable_t pagetable, char* dst, uint64 srcva, uint64 len);
        int (*copyout)(pagetable_t pagetable, uint64 dstva, char* src, uint64 len);
    };
  • 函数

    void set_signal(struct signal_context*); // 初始化signal_context
    
    int sigaction(uint32 signum, uint64 va_act, uint64 va_oldact); // 对当前进程信号signum的sigaction数据结构进行操作,把它原本的信息复制到va_oldact开头的虚拟地址里面,把va_act开头的虚拟地址的数据复制过来
    int sigprocmask(uint32 mask); // 修改当前进程的signal_mask为mask
    int sigreturn(); // 对于用户注册的signal handeler,需要在最后调用该syscall。signal模块会对应恢复进程的signal_block数据结构,同时会调用内核核心提供的customized_sigreturn恢复signal模块不维护的信息
    int sigkill(int pid, uint32 signum); // 给进程pid发送信号signum
  • 内核核心需要实现的部分:

    在进行任务调度的过程中,不能调度被冻住的任务

    // proc.c
    void scheduler()
    {
        struct thread *t;
        for (;;) {
    	    t = fetch_task();
    	    // ...
    	    if (t->process->sig_block.frozen) // 被杀死的任务不算被冻住、仍然可以被调度,因此吧这个if对他们不会满足,在其准备结束trap handler的时候将其杀死。
    	    {
    		    add_task(t);
    		    continue;
    	    }
            // ...
        }
    }

    在结束trap handler的时候,trap模块会调用customized_usertrap,内核核心可以在这里响应信号。

    void os9_customized_usertrap(int cause)
    {
        struct signal_block *sig_block = &curr_proc()->sig_block;
        if (sig_block->killed) // 在这里exit这个进程,彻底杀死
        {
    	    exit_proc(137);
    	    __builtin_unreachable();
        }
        if (sig_block->frozen) // 如果被冻住了直接yield
    	    yield();
        if (!sig_block->handling_sig) // 如果当前没有在处理任何信号,尝试找一个需要响应的信号;如果当前正在处理某个信号(该进程的某个线程正在用户注册的signal handler里面),那么直接返回用户态即可(无论是正在signal handler里面的线程,还是其它线程)
    	    for (uint32 signum = 1U;signum <= 31U;++signum)
    		    if (sig_block->signals & (1U << signum))
    		    {
    			    struct trapframe* trapframe = ((struct thread*)curr_task())->trapframe;
    			    curr_proc()->sig_trapframe = *trapframe;
                    // 上面两个代码是在把原本的trapframe给保存下来。在当前的线程结束了signal handler之后,还需要按照正常的异常处理流程恢复到发生trap的地方
    
    			    sig_block->handling_sig = signum;
    			    trapframe->epc = (uint64)sig_block->sig_actions[signum].handler; // 这里只修改了epc为signal handler的地址,因此之后usertrapret()会进入到signal handler。如果还想给signal handler传参(这个应当由OS和用户约定好,现在的设计里面要求signal handler参数列表为空),可以在这里修改trapframe里面保存的寄存器
    			    break;
    		    }
        usertrapret();
    }

    在signal模块里面sigreturn的最后,会调用内核核心提供的customized_sigreturn

    void customized_sigreturn() // 目前的设计下需要恢复trapframe
    {
        struct trapframe* trapframe = ((struct thread*)curr_task())->trapframe;
        *trapframe = curr_proc()->sig_trapframe;
    }
  • 评价:目前实现的signal模块里面有如下几点值得关注。

    • 目前的信号处理机制十分基本和简单。在实际的操作系统中这个机制十分复杂。比如,有的进程有权限杀死其它进程、有的进程没有。有兴趣的同学可以查找实际操作系统(比如Linux等)在信号处理上的具体实现
    • 目前信号处理机制的相当一部分实现放在了内核核心里面,signal模块主要完成的是对信号相关数据结构的维护,因此这部分的接口划分还有待商榷。这里主要是因为,内核核心负责了trapframe的恢复和保存,以及确实要通过修改epc来完成进入用户注册的signal handler的过程;除此之外,killed和frozen状态的实现也需要内核核心的密切配合,尤其是调度的逻辑
  • dependency

    extern void printf(char*, ...);
    extern int procid();
    extern int threadid();
    extern void dummy(int, ...);
    extern void shutdown();
    extern void set_timer(uint64);
  • module

    #include "signal.h"

sync

  • 描述:同步互斥模块,实现了mutex(锁)、semaphore(信号量)、condvar(条件变量)三种机制。

  • 结构体:

    void set_sync(struct synchronization_context *synchronization_context); // 初始化sync_context
    
    // mutex(锁)、semaphore(信号量)、condvar(条件变量)的数据结构。
    struct mutex;
    struct semaphore;
    struct condvar;
    
    struct synchronization_context
    {
        // mutex、semaphore、condvar是进程掌握的资源,因此需要让内核核心实现分配的功能
        struct mutex* (*alloc_mutex)();
        struct semaphore* (*alloc_semaphore)();
        struct condvar* (*alloc_codvar)();
    
        int (*curr_task_id)(); // 获取当前正在运行线程的id。内核核心需要给每个线程提供互不相同的int类型id,以供同步互斥模块识别
    
        void (*yield)(); // 对于spin mutex(自旋锁)的机制,线程抢不到锁需要等待,这时候需要主动放弃 CPU 主动权
    
        void (*sleeping)(); // 让当前抢不到资源的线程睡眠,等到后面资源空闲时被唤醒
        void (*running)(int id); // 唤醒编号为id的线程
    };
  • 函数:

    struct mutex *mutex_create(int blocking); // 创建一个mutex,其中blocking表示mutex的类型,为0是自旋锁、否则是阻塞锁
    void mutex_lock(struct mutex *); // 加锁
    void mutex_unlock(struct mutex *); // 解锁
    
    struct semaphore *semaphore_create(int count); // 创建一个初值为count的semaphore
    void semaphore_up(struct semaphore *); // 将semaphore的值加1
    void semaphore_down(struct semaphore *); // 将semaphore的值减1
    
    struct condvar *condvar_create(); // 创建一个condvar
    void cond_signal(struct condvar *); // 在该condvar上发信号,唤醒正在等待的线程
    void cond_wait(struct condvar *, struct mutex *); // 线程使用该接口来等到一个condvar为真(即有其它线程发信号)
  • 评价:该模块实现了同步互斥的内部机制。内核核心需要正确支持yield()sleepingrunning(id),以遵循该模块的调度。这一模块相对比较独立,可以单独拿出来测试,原则上不需要依赖内核核心。

  • dependency:

    #include "queue.h"
    
    extern void printf(char*, ...);
    extern int procid();
    extern int threadid();
    extern void dummy(int, ...);
    extern void shutdown();
  • module:

    #include "sync.h"

syscall

  • 描述:只有一个源文件syscall.c,主要负责系统调用的处理。

  • 结构体:

    struct syscall_context{
        // ch2 syscall
        uint64 (*sys_write)(int fd, uint64 va, uint64 len);
        void (*sys_exit)(int code);
        // 省略其它章节的系统调用函数指针定义
        // ……
    };
  • 函数

    int syscall(uint64 a0, uint64 a1, uint64 a2, uint64 a3, uint64 a4, uint64 a5, uint64 a6, uint64 a7); // 分别调用对应的syscall处理函数
    void set_syscall(struct syscall_context *sys_context); // 初始化sys_context
  • 评价:syscall模块只需要完成一个功能,根据传入的syscall id调用对应的syscall函数,起到一个分发的作用。至于具体的syscall实现虽然每章有共同之处,但放在每章分别实现,只需在初始化时传入函数指针即可。

  • dependency:

    extern void printf(char*, ...);
    extern int procid();
    extern int threadid();
    extern void dummy(int, ...);
    extern void shutdown();
  • module:

    #include "syscall_ids.h"
    #include "syscall.h"

task-manage

  • 描述:只有一个源文件processor.c,主要负责任务的调度。

  • 结构体:

    struct manager {
        void* (*create)(); // 创建任务
        void (*remove)(void* p); // 删除任务
        void* (*get_task_by_id)(int id); // 根据id获取任务
        int (*get_id_by_task)(void* p); // 根据任务获取id
        void (*add)(void* p); // 将任务加入调度队列
        void* (*fetch)(); // 从调度队列中取出相应任务
    };
  • 函数

    void set_manager(struct manager*); // manager初始化
    void* curr_task(); // 获得当前任务
    void set_curr(void*); // 设置当前任务
    void* get_task(int); // 获得指定id的任务
    int get_id(void*); // 获得指定任务的id
    void* alloc_task(); // 创建一个新任务
    void free_task(void*); // 结束一个任务
    void add_task(void*); // 阻塞一个任务
    void* fetch_task(); // 获得将调度的任务
  • 评价:任务调度模块实际上只定义了接口,但没有做任何的实现,manager结构体的所有函数实现放在每章分别实现:ch3-ch4采用循环遍历,ch5-ch8采用队列先进先出。当需要该模块提供函数功能时,manager中的函数不对外暴露,而使用头文件中定义的函数。对于ch3-ch7,”任务“即指的是进程,但对于ch8,这里采用了一种取巧的设计:创建与结束的单位是进程,而调度和阻塞的单位是线程,具体原因可以阅读代码理解,之后或许可以采用更优的方法改进这里的语义约定。

  • dependency:

    extern void printf(char*, ...);
    extern int procid();
    extern int threadid();
    extern void dummy(int, ...);
    extern void shutdown();
  • module:

    #include "processor.h"

trap

  • 描述:负责发生异常时操作系统的处理。

  • 结构体:

    struct trapframe; // 处理trap时用于保存上下文和其它必要信息的数据结构。
    
    struct trap_handler_context
    {
        void (*yield)(); // 如果是在U态发生的时钟中断,需要当前任务交出 CPU 使用权
    
        int (*cpuid)(); // 当发生外部中断时,需要知道当前的cpu id是什么
    
        // set_usertrap和set_kerneltrap分别用于设置在U态和S态下发生异常时的异常处理代码入口,这样硬件可以在对应情况下触发异常控制流
        // 操作系统可以选择把这些代码映射到每个进程的虚拟内存上的一个固定位置
        void (*set_usertrap)();
        void (*set_kerneltrap)();
    
        // 本模块需要下面的信息以在切换控制流时正确设置页表、恢复上下文等。
        struct trapframe* (*get_trapframe)(); // 用于得到当前的trapframe。内核可以直接使用该物理地址
        uint64 (*get_trapframe_va)();  // 用于得到当前trapframe在当前进程下的虚拟地址。如果当前使用的页表是某个进程的用户页表,需要使用该地址访问trapframe
        pagetable_t (*get_satp)(); // 用于得到页表的物理地址
        uint64 (*get_kernel_sp)(); // 用于得到内核栈的栈顶指针
    
        uint64 (*get_userret)(); // 得到userret的虚拟地址。操作系统可以选择把它的代码映射到每个进程的虚拟内存上的一个固定位置
        void (*customized_usertrap)(int cause); // cause是异常编号。该函数在usertrap(即通用的异常处理流程)执行完成后被执行,完成不同操作系统可能的个性需求
        void (*error_in_trap)(int status); // 用于处理当trap处理发生错误时要做的事
    
        int (*syscall)(uint64 a0, uint64 a1, uint64 a2, uint64 a3, uint64 a4, uint64 a5, uint64 a6, uint64 a7); // 用于处理系统调用导致的trap
    
        void (*super_external_handler)(int cpuid); // 用于处理非时间中断的一切外部中断,会在文件系统里面用到。需要每个章节的操作系统自行实现
    };
  • 函数

    void set_trap(struct trap_handler_context *trap_handler_context); //初始化trap_context
    void usertrapret(); // 不同操作系统完成异常处理的个性需求(即customized_usertrap(cause))之后,调用该接口从S态回到U态;该接口会完成页表切换、上下文切换等流程
  • 评价:该模块作为异常的处理模块,应该总地来说应该完成三个事情:(1)硬件使得异常控制流刚开始时,完成一些准备工作,包括切换到内核页表、保存用户进程上下文等,这一部分在trampoline.Suservec里面完成;(2)根据不同的异常类型,做类似syscall模块里面的分发,交换给内核核心的实现处理;(3)实现usertrapret(),当异常处理完成后回到U态,包括切换回用户页表、恢复用户进程上下文等。由于该模块目前仅为uCore-Tutorial设计,所以(2)不够完整,比如缺页异常应当进行页置换的处理、但在这里没有实现;原则上应当对于每种异常都在结构体里面留出一个处理的接口、交给内核核心去实现和填充,同时在void usertrap()里面正确分发。

  • dependency:

    extern void printf(char*, ...);
    extern int procid();
    extern int threadid();
    extern void dummy(int, ...);
    extern void shutdown();
    extern void set_timer(uint64);
  • module:

    #include "trap.h"
    #include "plic.h"
    #include "timer.h"

utils

  • 描述:完成了两个工具类string.cqueue.c的简单实现。

  • 结构体:

    struct queue {
        int *data; // 起始地址
        int size; // 队列大小
        int front; // 队首id
        int tail; // 队尾id
        int empty; // 空标志位
    };
  • 函数

    void init_queue(struct queue *, int, int *); // 队列初始化
    void push_queue(struct queue *, int); // 进队
    int pop_queue(struct queue *); // 出队
    int is_empty(struct queue *); // 队列是否为空
    
    int memcmp(const void *, const void *, uint); // 比较内存数据
    void *memmove(void *, const void *, uint); // 拷贝内存数据
    void *memset(void *, int, uint); // 设置内存数据
    char *safestrcpy(char *, const char *, int); // 拷贝字符串
    int strlen(const char *); // 字符串长度
    int strncmp(const char *, const char *, uint); // 字符串比较
    char *strncpy(char *, const char *, int); // 字符串拷贝
  • 评价:因为C语言此处没有像Rust一样的基本工具类,沿用原来uCore的设计手动实现了两个比较重要的工具类,在其它模块中调用,只需引入相应的头文件,链接相应的静态库即可。严格地说不属于uCore必需的模块,设计者可以给出自己的实现。

  • dependency:

    extern void printf(char*, ...);
    extern int procid();
    extern int threadid();
    extern void dummy(int, ...);
    extern void shutdown();
  • module:

    #include "string.h"
    #include "queue.h"

模块关系

如何运行