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“返回”到管理模式。
- 函数start执行一些仅在机器模式下允许的配置,然后切换到管理模式。
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
寄存器ra
、sp
(进程切换)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源码