• xv6源码阅读——xv6的启动,进程初识


    说明

    • 阅读的代码是 xv6-riscv 版本的
      涉及到的文件如下
    • kernel
      entry.S、start.c、main.c、kalloc.c、vm.c、proc.c、swtch.S、proc.h、printf.c、trap.c
    • user
      initcode.S

    1.xv6的启动

    • 这一部分讲述xv6 在启动过程中的配置以及 xv6 中第一个 shell 进程的创建过程

    1.1.kernel/entry.S

    • 当 xv6 的系统启动的时候,首先会启动一个引导加载程序(存在 ROM 里面),之后装载内核程序进内存
      注意由于只有一个内核栈,内核栈部分的地址空间可以是固定,因此 xv6 启动的时候并没有开启硬件支持的 paging 策略,也就是说,对于内核栈而言,它的物理地址和虚拟地址是一样的

    • 在机器模式下,CPU是从_entry开始执行的

    # kernel/entry.S
    _entry:
        # 设置一个内核栈
        # stack0 在 start.c 中声明, 每个内核栈的大小为 4096 byte
        # 以下的代码表示将 sp 指向某个 CPU 对应的内核栈的起始地址
        # 也就是说, 进行如下设置: sp = stack0 + (hartid + 1) * 4096
        la sp, stack0           # sp = stack0
        li a0, 1024*4           # a0 = 4096
        csrr a1, mhartid        # 从寄存器 mhartid 中读取出当前对应的 CPU 号
                                # a1 = hartid
        addi a1, a1, 1          # 地址空间向下增长, 因此将起始地址设置为最大
        mul a0, a0, a1          # a0 = 4096 * (hartid + 1)
        add sp, sp, a0          # sp = stack0 + (hartid + 1) * 4096
    
        # 跳转到 kernel/start.c 执行内核代码
        call start
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    1.2.kernel/start.c

    • 函数start执行一些仅在机器模式下允许的配置,然后切换到管理模式。RISC-V提供指令mret以进入管理模式,该指令最常用于将管理模式切换到机器模式的调用中返回。而start并非从这样的调用返回,而是执行以下操作:它在寄存器mstatus中将先前的运行模式改为管理模式,它通过将main函数的地址写入寄存器mepc将返回地址设为main,它通过向页表寄存器satp写入0来在管理模式下禁用虚拟地址转换,并将所有的中断和异常委托给管理模式。
    • strart()函数的调用
      • 函数start执行一些仅在机器模式下允许的配置,然后切换到管理模式。
        • 它在寄存器mstatus中将先前的运行模式改为管理模式
        • 它通过将main函数的地址写入寄存器mepc将返回地址设为main
        • 它通过向页表寄存器satp写入0来在管理模式下禁用虚拟地址转换,并将所有的中断和异常委托给管理模式。
        • 对时钟芯片进行编程以产生计时器中断。
      • start通过调用mret“返回”到管理模式。
    void
    start()
    {
      // set M Previous Privilege mode to Supervisor, for mret.
      unsigned long x = r_mstatus();
      x &= ~MSTATUS_MPP_MASK;
      x |= MSTATUS_MPP_S;
      w_mstatus(x);
    
      // set M Exception Program Counter to main, for mret.
      // requires gcc -mcmodel=medany
      w_mepc((uint64)main);
    
      // disable paging for now.
      w_satp(0);
    
      // delegate all interrupts and exceptions to supervisor mode.
      w_medeleg(0xffff);
      w_mideleg(0xffff);
      w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);
    
      // ask for clock interrupts.
      timerinit();
    
      // keep each CPU's hartid in its tp register, for cpuid().
      int id = r_mhartid();
      w_tp(id);
    
      // switch to supervisor mode and jump to main().
      asm volatile("mret");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    1.3.kernel/main.c

    • 主要工作就是初始化一些配置
    void
    main()
    {
      if(cpuid() == 0){
        consoleinit();  // 配置控制台属性(锁, uart寄存器配置)
        printfinit();   // 配置 printf 属性(锁)
        printf("\n");
        printf("xv6 kernel is booting\n");
        printf("\n");
        kinit();         //物理页分配器
        kvminit();       // 创建内核页表
        kvminithart();   // 开启分页机制
        procinit();      // 初始化进程表(最多支持 64 个进程)
        trapinit();      // 初始化中断异常处理程序的一些配置(锁)
        trapinithart();  // 设置内核异常
        plicinit();      // 设置中断控制器
        plicinithart();  // 请求PLIC设备中断
        binit();         // 初始化高速缓冲存储器
        iinit();         // 初始化inode缓存
        fileinit();      // 初始化文件表
        virtio_disk_init(); // emulated hard disk
        userinit();      //创建第一个进程
        __sync_synchronize();
        started = 1;
      } else {
        while(started == 0)
          ;
        __sync_synchronize();
        printf("hart %d starting\n", cpuid());
        kvminithart();    // turn on paging
        trapinithart();   // install kernel trap vector
        plicinithart();   // ask PLIC for device interrupts
      }
    
      scheduler();        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    1.4.kernel/proc.c

    • 下面我们看一看userinit()函数具体干了些什么
    // Set up first user process.
    void userinit(void)
    {
      struct proc *p;
    
      p = allocproc();
      initproc = p;
    
      // allocate one user page and copy init's instructions
      // and data into it.
      uvminit(p->pagetable, initcode, sizeof(initcode));
      p->sz = PGSIZE;
    
      // prepare for the very first "return" from kernel to user.
      p->trapframe->epc = 0;     // user program counter
      p->trapframe->sp = PGSIZE; // user stack pointer
    
      safestrcpy(p->name, "initcode", sizeof(p->name));
      p->cwd = namei("/");
    
      p->state = RUNNABLE;
    
      release(&p->lock);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    调用逻辑

    • 调用allocproc()函数来获取一个空闲进程,及状态为 UNUSED 的进程
      • proc[NPROC]中寻找一个 状态为 UNUSED 的进程
        • 找不到返回0
        • 找到了对该进程进行一些初始化配置,然后返回一个struct proc
          • 计算 pid
          • 调用 kalloc() 分配一个 trapframe
            • 从空闲链表中分配一块空闲页
          • 分配失败则调用freeproc(p)释放
          • 调用函数proc_pagetable(p)为用户态分配一个页表
          • 设置 context 寄存器 rasp(进程切换)
            • ra:用户态执行的上下文
            • sp:栈指针
      • 把初始化代码(一段机器代码)放入进程的页表中(只是加载进去,并没有执行)
      • 准备从内核到用户的第一次“返回”。
      • epc = 0 用户程序计数器
      • sp = PGSIZE用户栈指针
      • 设置进程名称为 initcode,进程工作目录为 /
      • 设置进程状态为 RUNNABLE
    • 最后返回 kernel/main.c 中执行进程调度程序 scheduler(),然后经调度后才开始执行那一段机器代码。

    2.进程

    2.1.进程管理

    • proc结构体
    // kernel/proc.h
    struct proc {
       struct spinlock lock; // 当前进程的锁
    
       // 以下内容如果需要修改的话, 必须持有当前进程的锁 lock
       enum procstate state;        // 当前进程所处的状态
       void *chan;                  // 非 0 表示当前进程处于 sleep 状态(睡眠地址)
       int killed;                  // 非 0 则表示当前进程被 killed
       int xstate;                  // 退出状态, 可以被父进程的 wait() 检查
       int pid;                     // 进程 ID 号, pid
    
       // 如果需要修改父进程指针的话, 需要持有整个进程树的锁
       // kernel/proc.c: pid_lock
       struct proc *parent;         // 父进程指针
    
       // 这些变量对于一个进程来说是私有的, 修改的时候不需要加锁
       uint64 kstack;               // 内核栈的虚拟地址
       uint64 sz;                   // 进程所占的内存大小
       pagetable_t pagetable;       // 用户页表
       struct trapframe *trapframe; // 当进程在用户态和内核态之间切换时
                                    // 用于保存/恢复进程的状态
                                    // 用于保存寄存器
       struct context context;      // 切换进程所需要保存的进程状态
       struct file *ofile[NOFILE];  // 打开文件列表
       struct inode *cwd;           // 当前工作目录
       char name[16];               // 进程名称
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 用于管理进程的变量和函数
    // kernel/proc.c
    // 变量
    int nextpid = 1;            // 用于进程号的编码
    struct proc proc[NPROC];    // 最多支持 64 个进程
    struct spinlock pid_lock;   // 当修改一些整个进程树相关的内容的时候, 需要加的锁
                                // 例如新建一个进程的时候, 需要从 nextpid 中生成一个新的 pid
    struct spinlock wait_lock;  // 辅助于 wait() 使用
    
    // 函数
    // 创建一个新的进程并且初始化这个进程, 具体内容在上面已经提到过了
    void allocproc(void){}
    // 释放进程的内容空间
    static void freeproc(struct proc *p){}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2.2 进程状态

    在xv6中进程会有5中状态
    UNUSED
    SLEEPING
    RUNNABLE
    RUNNING
    ZOMBIE

    enum procstate {
        // 当前进程没有被使用, 属于空闲进程
        // (1) 系统启动的时候, 所有的进程的状态都被初始化 UNUSED
        //     当 shell 或者其他方式想要新建一个进程的时候, 会查询是否存在状态为 UNUSED 的进程
        // (2) 一个 ZOMBIE 进程被回收之后(wait()), 状态会被修改为 UNUSED
        UNUSED,
    
        // 处于睡眠状态
        // 调用 sleep() 的时候会从 RUNNING 状态进入 SLEEPING
        SLEEPING,
    
        // 表示当前继承处于可以被调度运行的状态
        // (1) wakeup() 可以将一个进程从 SLEEPING 转向 RUNNABLE
        // (2) kill() 会将 SLEEPING 进程状态修改为 RUNNABLE
        // (3) yield() 会让出当前进程的执行权, 让 CPU 重新调度
        //     状态: RUNNING -> RUNNABLE
        RUNNABLE,
    
        // (1) userinit() 会将 USED 状态修改为 RUNNING
        //     这个调用仅在初始化第一个进程的时候出现
        // (2) 在调用 fork() 的时候, 刚刚被 allocproc() 申请的进程在经过错误检查之后,
        //     USED 状态会被修改为 RUNNABLE
        // (3) scheduler() 调度程序可以把 RUNNABLE 状态的程序修改为 RUNNING
        RUNNING,
    
        // 处于进程退出但是还没有被回收的状态(资源已经被回收, 但是还没有被父进程发现)
        // (1) exit() 的调用会让进程 从高 RUNNING 转变为 ZOMBIE
        ZOMBIE
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    参考资料

    • http://xv6.dgs.zone/tranlate_books/book-riscv-rev1/c1/s0.html
    • xv6-riscv源码
  • 相关阅读:
    Netty——Path和Files类中方法说明与代码示例
    HTML5期末学生大作业:基于HTML+CSS+JavaScript书城小说书籍网站带首页psd(6页)
    Unicode码转UTF8
    js数组对象中根据某个相同的字段找到其他对象中有值的数据
    illustrator插件-什么是脚本-如何使用-什么是动作-AI插件
    4.凸优化问题
    计算机视觉论文精度大纲
    1. CSS 选择器及其优先级?
    OpenCV利用Camshift实现目标追踪
    LeetCode 42. 接雨水
  • 原文地址:https://blog.csdn.net/m0_61705102/article/details/126634565