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.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.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.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);
}

调用逻辑

  • 调用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];               // 进程名称
};
  • 用于管理进程的变量和函数
// 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){}

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
};

参考资料

  • http://xv6.dgs.zone/tranlate_books/book-riscv-rev1/c1/s0.html
  • xv6-riscv源码
  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

binary~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值