当前位置: 首页 > news >正文

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

目录

  • 说明
  • 1.xv6的启动
    • 1.1.kernel/entry.S
    • 1.2.kernel/start.c
    • 1.3.kernel/main.c
    • 1.4.kernel/proc.c
  • 2.进程
    • 2.1.进程管理
    • 2.2 进程状态
  • 参考资料

说明

  • 阅读的代码是 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源码

相关文章:

  • 金仓数据库KingbaseES客户端应用参考手册--13. sys_isready
  • 前端工程师面试题总结
  • 从“1L 小钢炮”到 “PC界变形金刚”——Tiny助力企业数智转型的十年进化之路
  • 【数据结构:1.绪论】
  • 计算机组成原理第二章----数据信息的表示 详解版
  • 网络安全-防火墙安全加固
  • 中秋节祝福程序源代码分享:土地分类数据阈值筛选和重投影分类
  • Java新手小白入门篇 API - 多线程
  • Deep Reinforcement Learning with Double Q-learning(double DQN)
  • 【博客472】k8s中如何使用shared memory
  • SpringBoot2.6.8 多环境配置
  • 安利网课查题接口系统
  • spring-security-oauth2之WebSecurityConfigurerAdapter浅析
  • Windows与网络基础-14-NTFS权限规则
  • 【Python】列表生成式应用的八重境界
  • [case10]使用RSQL实现端到端的动态查询
  • Android Volley源码解析
  • C++11: atomic 头文件
  • flask接收请求并推入栈
  • JavaScript新鲜事·第5期
  • Java多态
  • Leetcode 27 Remove Element
  • python学习笔记 - ThreadLocal
  • React的组件模式
  • Spark RDD学习: aggregate函数
  • Tornado学习笔记(1)
  • vue的全局变量和全局拦截请求器
  • 成为一名优秀的Developer的书单
  • 给Prometheus造假数据的方法
  • 如何在GitHub上创建个人博客
  • 我建了一个叫Hello World的项目
  • LIGO、Virgo第三轮探测告捷,同时探测到一对黑洞合并产生的引力波事件 ...
  • #NOIP 2014# day.1 T3 飞扬的小鸟 bird
  • #预处理和函数的对比以及条件编译
  • (floyd+补集) poj 3275
  • (初研) Sentence-embedding fine-tune notebook
  • (免费领源码)Java#ssm#MySQL 创意商城03663-计算机毕业设计项目选题推荐
  • (三)Pytorch快速搭建卷积神经网络模型实现手写数字识别(代码+详细注解)
  • (原創) 如何解决make kernel时『clock skew detected』的warning? (OS) (Linux)
  • ./include/caffe/util/cudnn.hpp: In function ‘const char* cudnnGetErrorString(cudnnStatus_t)’: ./incl
  • .NET Core 中的路径问题
  • .NET Framework 和 .NET Core 在默认情况下垃圾回收(GC)机制的不同(局部变量部分)
  • .NET 设计一套高性能的弱事件机制
  • .NET6 命令行启动及发布单个Exe文件
  • .NetCore项目nginx发布
  • .NET设计模式(11):组合模式(Composite Pattern)
  • .net通用权限框架B/S (三)--MODEL层(2)
  • .pyc文件是什么?
  • .skip() 和 .only() 的使用
  • @Transactional 竟也能解决分布式事务?
  • [ CTF ] WriteUp- 2022年第三届“网鼎杯”网络安全大赛(朱雀组)
  • [20161214]如何确定dbid.txt
  • [acwing周赛复盘] 第 94 场周赛20230311
  • [C#]OpenCvSharp结合yolov8-face实现L2CS-Net眼睛注视方向估计或者人脸朝向估计
  • [C++]Leetcode17电话号码的字母组合