MIT 6.828 操作系统课程系列0 基础和环境#
https://pdos.csail.mit.edu/6.828/2022/schedule.html
用尽全力强烈建议先完成CS143编译原理课程。因为会比较熟悉汇编和程序的底层整体架构。再来看os就是顺水推舟。
我是以前看过os这个课程,做了一半lab,但很多细节其实一知半解。
后来整完CS143。再回过来补全os这个课,认知就完全不一样了。
必须搞懂较为底层的细节才有能力自己做一个os。
课程目的#
理解操作系统的设计和实现
学习risc-v架构
能动手实现系统应用
能动手扩展os
能实现一个小型os
背景#
RISC(Reduced Instruction Set Computer)精简指令集计算机。
CISC(Complex Instruction Set Computer)复杂指令集计算机。
先有复杂指令集。越来越庞大、暴露各种缺点,就有了精简指令集。 risc不断进化,到了第五代,就是RISC-V。 RISC-V是开源的。人们可以在它基础上设计开发自己的cpu。
https://riscv.org/
riscv/riscv-isa-manual
riscv/riscv-isa-manualxv6是mit开发的一个教学级别的操作系统,受Unix V6影响。作者都是业界大佬级别,质量有保证。
xv6运行在多核64位riscv cpu
本课程就用这个操作系统,会深入其代码。一共就五十几个代码文件。
因为linux目前代码量巨大,很多层面上无从下手,那么从一个小而精的系统入手也是个很好的选择。 完成后希望能掌握一个os核心的框架。
xv6手册 https://pdos.csail.mit.edu/6.828/2022/xv6/book-riscv-rev3.pdf
主要按lab顺序来看代码,如果碰到相关性强的内容会提前看。
lab0尽量研究系统的基本启动相关流程。代码编译->制作fs->启动机器->运行kernel->用户的第一个程序。
其他代码在后续lab里研究。
每个lab都完成所有任务,得到满分。
环境#
环境安装非常简单
实验代码
git clone git://g.csail.mit.edu/xv6-labs-2022
实验代码是放在不同的分支里。默认branch为第一个lab。
也可以直接看正式的xv6代码mit-pdos/xv6-riscv
其make流程和各实验会有少许不同,总体一样的。
实验工具
sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu
直接make qemu
编完会自动进入模拟器运行kernel并显示命令行,就和一般linux系统一样。
默认的kernel功能简陋,只有一些最基本的命令。
qemu是一个开源的模拟器,主要用于各种系统的模拟。
比如模拟riscv架构,你编一个riscv的操作系统,可以在不同架构的windows桌面或者linux命令行之类环境下启动qemu来加载这个操作系统并运行。
https://www.qemu.org/docs/master/system/target-riscv.html#risc-v-system-emulator
virtio
虚拟化接口的一套标准/解决方案/框架。庞大的课题。
https://wiki.osdev.org/Virtio
https://docs.oasis-open.org/virtio/virtio/v1.1/virtio-v1.1.html
https://ozlabs.org/~rusty/virtio-spec/virtio-paper.pdf
qemu跑xv6代码也是用的virtio machine。
makefile的qemu部分
qemu: $K/kernel fs.img
$(QEMU) $(QEMUOPTS)
QEMU = qemu-system-riscv64 # qemu启动64位riscv系统
QEMUOPTS = -machine virt # machine类型
-bios none # 指定bios
-kernel $K/kernel # kernel的bzImage
-m 128M # ram
-smp $(CPUS) # SMP system cpu数
-nographic # 无图形化
QEMUOPTS += -global virtio-mmio.force-legacy=false
QEMUOPTS += -drive file=fs.img,if=none,format=raw,id=x0 # disk image
QEMUOPTS += -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0
qemu参数
https://www.qemu.org/docs/master/system/invocation.html
列出machine类型
qemu-system-riscv64 -machine help
Supported machines are:
none empty machine
sifive_e RISC-V Board compatible with SiFive E SDK
sifive_u RISC-V Board compatible with SiFive U SDK
spike RISC-V Spike Board (default)
spike_v1.10 RISC-V Spike Board (Privileged ISA v1.10)
spike_v1.9.1 RISC-V Spike Board (Privileged ISA v1.9.1)
virt RISC-V VirtIO board
qemu-system-riscv64 -device help
列出茫茫多设备。
启动qemu模拟器依赖于$K/kernel
和fs.img
。即操作系统程序和文件系统。
xv6的mkfs流程#
这个其实较为独立,大概从lab5才开始用到。可以暂时跳过。可以看个大概。
先看下手册第8章了解文件系统。
fs.img: mkfs/mkfs README $(UPROGS)
mkfs/mkfs fs.img README $(UPROGS)
# fs.img靠mkfs生成,传入UPROGS,一系列user程序。
UPROGS=\
$U/_cat\
$U/_echo\
$U/_forktest\
$U/_grep\
$U/_init\
$U/_kill\
$U/_ln\
$U/_ls\
$U/_mkdir\
$U/_rm\
$U/_sh\
$U/_stressfs\
$U/_usertests\
$U/_grind\
$U/_wc\
$U/_zombie\
ULIB = $U/ulib.o $U/usys.o $U/printf.o $U/umalloc.o
_%: %.o $(ULIB)
$(LD) $(LDFLAGS) -T $U/user.ld -o $@ $^
$(OBJDUMP) -S $@ > $*.asm
$(OBJDUMP) -t $@ | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $*.sym
# UPROGS中的_%程序依赖于%.o。
# 根据gcc的隐式规则,会找对应的.c来编。
#
# linker参数见https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_node/ld_3.html
# -T参数指定user.ld
#
# $@ $^见https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html#Automatic-Variables
# $@即为目标,最前边那个。$^为后面所有的依赖。
#
# linker为riscv64-linux-gnu-ld
# https://manpages.debian.org/testing/binutils-riscv64-linux-gnu/riscv64-linux-gnu-ld.1.en.html
#
# LDFLAGS中max-page-size设置最大页大小
# https://manpages.debian.org/testing/binutils-riscv64-linux-gnu/riscv64-linux-gnu-objdump.1.en.html
# riscv64-linux-gnu-objdump -S
# dump每个user程序到各自的.asm
# riscv64-linux-gnu-objdump -t
# 输出symbol table
# https://manpages.debian.org/testing/binutils-riscv64-linux-gnu/riscv64-linux-gnu-objcopy.1.en.html
mkfs/mkfs: mkfs/mkfs.c $K/fs.h $K/param.h
gcc -Werror -Wall -I. -o mkfs/mkfs mkfs/mkfs.c
linker script资料
https://home.cs.colorado.edu/~main/cs1300/doc/gnu/ld_toc.html
https://www.eecs.umich.edu/courses/eecs373/readings/Linker.pdf
https://wiki.osdev.org/Linker_Scripts
linker此时可忽略。后续会看。
需要编出所有UPROGS即用户程序,也就是一些常用命令比如cat/ls/rm。
用mkfs.c编出mkfs。
再mkfs/mkfs fs.img README $(UPROGS)
生成文件系统fs.img。并把常用命令放到根目录。
先看手册第8章文件系统的block和inode相关内容和代码。
//文件系统基本信息
#define BSIZE 1024 // 文件系统把存储资源看作一系列block。每个block为1k字节。
#define FSSIZE 2000 // 文件系统总block数2000。大约2m。
// xv6文件系统布局
// | boot | superblock | log | inodes | bitmap | data |
// block编号 0 1 2 ... ... ... ... FSSIZE-1
// 数量 1 1 nlog ninodeblocks nbitmap nblocks
#define MAXOPBLOCKS 10 // 一个fs操作进行写动作的最多block数
#define LOGSIZE (MAXOPBLOCKS*3) // log的block数
int nbitmap = FSSIZE/(BSIZE*8) + 1; // 每一位表示一个block的占用状态。2000个block算下来只用1个block就够了。1个block有8kbit。FSSIZE只有2000。
int nlog = LOGSIZE; // log的block数
// incode包含一个文件的信息
#define NDIRECT 12
struct dinode {
short type; // File type
short major; // Major device number (T_DEVICE only)
short minor; // Minor device number (T_DEVICE only)
short nlink; // Number of links to inode in file system
uint size; // Size of file (bytes)
uint addrs[NDIRECT+1]; // 见xv6手册8.10
};
#define IPB (BSIZE / sizeof(struct dinode)) // 一个block可以放几个dinode结构体
#define NINODES 200 // 最多200个inode/文件
int ninodeblocks = NINODES / IPB + 1; // 所有inode所需的block数
nmeta = 2 + nlog + ninodeblocks + nbitmap; // 除data外的block总数。系统自用数据。
nblocks = FSSIZE - nmeta; // 可用的block总数
// superblock填入文件系统总信息
struct superblock sb;
sb.magic = FSMAGIC; // # 0x10203040
sb.size = xint(FSSIZE);
sb.nblocks = xint(nblocks); // 可用的block总数。
sb.ninodes = xint(NINODES); // inode总数/文件总数=200
sb.nlog = xint(nlog); // log信息的block数
sb.logstart = xint(2); // log从block2开始
sb.inodestart = xint(2+nlog); // inode开始位置
sb.bmapstart = xint(2+nlog+ninodeblocks); // bitmap开始位置
// 生成文件系统fs.img流程
char zeroes[BSIZE];
char buf[BSIZE];
main
fsfd = open(argv[1], O_RDWR|O_CREAT|O_TRUNC, 0666); //打开/创建fs.img
// superblock填入基本信息
// 往文件里写入FSSIZE个block,都为0。
for(i = 0; i < FSSIZE; i++)
wsect(i, zeroes);
lseek(fsfd, sec * BSIZE, 0) // seek到指定的block
write(fsfd, buf, BSIZE) // 写block到文件。linux的write
// superblock写到第一个block。
// buf清零。sb挪到buf。buf写入文件。
memset(buf, 0, sizeof(buf));
memmove(buf, &sb, sizeof(sb));
wsect(1, buf);
// inode类型
#define T_DIR 1 // Directory
#define T_FILE 2 // File
#define T_DEVICE 3 // Device
#define IBLOCK(i, sb) ((i) / IPB + sb.inodestart) // 计算编号为i的inode在第几个block
// 分配一个新的inode。类型为文件夹。
// 根目录的inode号特地做成1
uint freeinode = 1;
rootino = ialloc(T_DIR);
uint inum = freeinode++; // freeinode不断增长
struct dinode din; // 起一个din
bzero(&din, sizeof(din)); // 清零。即memset
din.type = xshort(type); // 需转成riscv字节顺序
din.nlink = xshort(1);
din.size = xint(0);
winode(inum, &din); // 写文件。把din写到第inum个inode位置
char buf[BSIZE];
uint bn;
struct dinode *dip;
bn = IBLOCK(inum, sb); // 算出inode所在的block
rsect(bn, buf); // 读block到buf
dip = ((struct dinode*)buf) + (inum % IPB); // dip指向对应的incode数据
*dip = *ip; // 直接把新起的din复制到buf对应的inode位置。
wsect(bn, buf); // 最后把buf写回文件
return inum;
/*
inode具体数据布局
#define NDIRECT 12 // 12个直接block
#define NINDIRECT (BSIZE / sizeof(uint)) // INDIRECT数量。用一个block存地址,每个地址用uint存,一共能存几个地址。1024/4=256。
#define MAXFILE (NDIRECT + NINDIRECT) // 一个文件的最大block数。12+256=268
struct dinode {
short type; // File type
short major; // Major device number (T_DEVICE only)
short minor; // Minor device number (T_DEVICE only)
short nlink; // Number of links to inode in file system
uint size; // Size of file (bytes)
uint addrs[NDIRECT+1]; // 见xv6手册8.10
};
12个direct block的地址直接放在addrs里。addrs最后存一个block地址,包含sizeof(uint)个indirect block。
这样一个文件的前12k数据可以直接从inode数据中拿到。后续的数据得从那个indirect block中找。
*/
// dirent是目录中的一个个实体。可能是文件,可能是目录。
// 一个目录类型的inode里,实际数据就是一串dirent。
#define DIRSIZ 14 // dir名字最大长度
struct dirent {
ushort inum; // inode编号
char name[DIRSIZ]; // 名字
};
struct dirent de;
//当前目录
de.inum = xshort(rootino); // 文件夹的inum设为新生成的rootino=1
strcpy(de.name, "."); // 名字设为.
freeblock = nmeta; // 可用的block编号从nmeta开始
// ialloc生成新的inode。
// iappend把任意数据append到一个inode。可以多次append。
// 多次的效果是多个dirent顺序排在data block和inode数据里。
// 它这里先把"."和".."两个路径append到rootino。
// 复制de的数据到rootino
iappend(rootino, &de, sizeof(de)); // iappend(uint inum, void *xp, int n)
char *p = (char*)xp; // p指向dirent
uint fbn, off, n1;
struct dinode din;
char buf[BSIZE];
uint indirect[NINDIRECT]; // indirect block的地址。所有地址占一个block。实际是存block的id(见freeblock),并不是一般意义的地址。
uint x;
rinode(inum, &din); // 从文件读出inum编号的inode到din
off = xint(din.size); // 初始为0。如果已经append过,则为append的总数居。
while(n > 0){ // n = sizeof(de) 为剩余要复制的数据大小
fbn = off / BSIZE; // off从0开始不断增加。每次计算当前off落在哪个block。
assert(fbn < MAXFILE);
if(fbn < NDIRECT){
// 如果落在direct
if(xint(din.addrs[fbn]) == 0){
din.addrs[fbn] = xint(freeblock++); // 如果此block编号为0。做新编号。
}
x = xint(din.addrs[fbn]); // 记录block编号
} else {
// 如果落在indirect。已经处理完direct部分。
if(xint(din.addrs[NDIRECT]) == 0){ // addrs的最后一项为indirect block的编号
din.addrs[NDIRECT] = xint(freeblock++); // 为其分配block编号
}
rsect(xint(din.addrs[NDIRECT]), (char*)indirect); // 读出此block
if(indirect[fbn - NDIRECT] == 0){ // 减去NDIRECT,即从0开始。
indirect[fbn - NDIRECT] = xint(freeblock++); // 新做block编号
wsect(xint(din.addrs[NDIRECT]), (char*)indirect); // 把新的indirect数据存进去。
}
x = xint(indirect[fbn-NDIRECT]); // 记录block编号
}
// 这个代码挺精妙的。它可实现多次append。
// 如果当前offset不是1k对齐,就尝试先补齐。
// 比如offset为345字节,就先复制679字节(如果剩余大于679字节)。
// 然后就是每次复制1k,直到最后一次复制<=1k的数据。
n1 = min(n, (fbn + 1) * BSIZE - off);
rsect(x, buf);
bcopy(p, buf + off - (fbn * BSIZE), n1);
wsect(x, buf);
n -= n1;
off += n1;
p += n1;
}
din.size = xint(off); // 设置为最新的size
winode(inum, &din); // 更新inode到文件
// append ..路径
de.inum = xshort(rootino);
strcpy(de.name, "..");
iappend(rootino, &de, sizeof(de));
// 运行mkfs的参数为mkfs/mkfs fs.img README $(UPROGS)
// 对其余每个文件都起新的inode,并append到rootino。
// 对于T_DIR类型的inode,append路径。
// 对T_FILE类型的inode,直接append文件的原始数据。
for(i = 2; i < argc; i++)
fd = open(argv[i], 0)
inum = ialloc(T_FILE); // 起inode。类型为file。
// 新文件的inode append到rootino
de.inum = xshort(inum);
strncpy(de.name, shortname, DIRSIZ);
iappend(rootino, &de, sizeof(de));
// 直接append文件的原始数据
while((cc = read(fd, buf, sizeof(buf))) > 0)
iappend(inum, buf, cc);
balloc(int used) // balloc(freeblock);
uchar buf[BSIZE];
int i;
printf("balloc: first %d blocks have been allocated\n", used);
assert(used < BSIZE*8); // 一个block有BSIZE*8bit。只要小于就能用1block装下。
bzero(buf, BSIZE);
for(i = 0; i < used; i++){
buf[i/8] = buf[i/8] | (0x1 << (i%8)); // 把所有已使用的block的对应bit置1
}
printf("balloc: write bitmap block at sector %d\n", sb.bmapstart);
wsect(sb.bmapstart, buf); // 更新文件系统的bitmap数据
看完这个代码就清楚了fs.img到底是个啥。
本质上就是先制定了一个文件系统的格式协议。包括各种尺寸/顺序/结构。
那么不管在任何机器上都能通用了。
然后在我们host上按这个格式做一个文件fs.img,用这个文件模拟整个目标机器的文件系统。
后续会把fs.img作为文件系统加载到模拟器。
xv6的启动流程#
$K/kernel: $(OBJS) $K/kernel.ld $U/initcode
$(LD) $(LDFLAGS) -T $K/kernel.ld -o $K/kernel $(OBJS)
$(OBJDUMP) -S $K/kernel > $K/kernel.asm
$(OBJDUMP) -t $K/kernel | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $K/kernel.sym
$U/initcode: $U/initcode.S
$(CC) $(CFLAGS) -march=rv64g -nostdinc -I. -Ikernel -c $U/initcode.S -o $U/initcode.o
$(LD) $(LDFLAGS) -N -e start -Ttext 0 -o $U/initcode.out $U/initcode.o
$(OBJCOPY) -S -O binary $U/initcode.out $U/initcode
$(OBJDUMP) -S $U/initcode.o > $U/initcode.asm
OBJS = 各种.o
CFLAGS += -MD # MD选项生成.d文件。包含所有.o的依赖
# 它这个makefile应该是走了隐藏规则,自动找了.c文件去编译。
# https://www.gnu.org/software/make/manual/html_node/Using-Implicit.html
# 实验的makefile还和xv6-riscv本身有一定区别
看手册第2章了解系统整体信息。
总体启动流程
上电。bootloader加载kernel进内存。地址为0x80000000(kernel.ld中配置)。因为0x0到0x80000000包含io设备,所以放到0x80000000。
machine模式。cpu执行entry.S里的_entry(kernel.ld中配置ENTRY)。此时硬件分页关闭,虚拟地址直接映射到物理地址。
_entry
起一个stack0让xv6能够跑c代码。sp指针指向stack0+4096。开始跑start.c里的start()函数。start()做一些machine模式特许的操作。
切换到supervisor模式。写main函数(kernel/main.c)的地址到mepc,直接返回到main函数。
main()初始化各种设备和子系统。调用userinit()创建第一个进程。执行user/initcode.S。
initcode.S执行exec(system call),启动init程序(user/init.c)。进入shell等待用户输入。
# 系统程序入口。每个cpu都会执行。
# entry.S
# qemu -kernel loads the kernel at 0x80000000
# and causes each hart (i.e. CPU) to jump there.
# kernel.ld causes the following code to
# be placed at 0x80000000.
.section .text
.global _entry
_entry:
# set up a stack for C.
# stack0 is declared in start.c,
# with a 4096-byte stack per CPU.
# sp = stack0 + (hartid * 4096)
la sp, stack0 # 找到stack0的地址。让cpu用这个地址做栈。
li a0, 1024*4 # 做一个4096
csrr a1, mhartid # 获取cpu线程id
addi a1, a1, 1 # +1
mul a0, a0, a1 # *=4096
add sp, sp, a0 # sp挪到对应cpu位置
# jump to start() in start.c
call start # 走start.c的start()
spin:
j spin
讲了不少指令集/汇编相关基本概念。非常好。
https://riscv-programming.org/book/riscv-book.html
可结合生成的kernel.asm看流程。
先看看手册第4章熟悉一下trap/system call和riscv寄存器相关
Traps and system calls#
有三种事件会让cpu放下当前的指令,转到处理这些事件的特殊代码。
system call
user代码调用ecall指令请求kernel做某件事情。exception
user或kernel干了某件非法的事情比如除0。interrupt
硬件发出某种信号。比如硬盘完成了一次读/写请求。
这里用trap通指这些情况。一般来说trap完成后需要继续之前的流程,仿佛什么都没发生。
所谓transparent
,做了事,但不会被察觉到。
一般流程是,trap强制进kernel模式,kernel保存各种寄存器等以便回到之前的状态。
kernel执行handler代码。然后恢复到trap之前的状态。
xv6的trap处理分阶段
riscv cpu做硬件动作
一些汇编指令准备运行kernel的c代码
c代码决定如何处理trap
实际处理trap的system call或者设备驱动函数
根据三种trap的共性,kernel可以用同样的代码路径去处理。但是分三种情况处理更方便。
来自user空间的trap
来自kernel空间的trap
timer interrupts
处理kernel trap的代码称为handler。
RISC-V trap machinery#
这里需参考riscv文档
riscv cpu有一系列寄存器,kernel通过其配置trap的处理方式。也可以获取当前trap的信息。
riscv.h
里定义了各种xv6所需的riscv相关操作。
stvec(Supervisor Trap Vector Base Address Register)
kernel设置trap handler的地址,cpu跳转到此地址执行handler。sepc(Supervisor Exception Program Counter)
trap发生时cpu把当前的pc存这儿。pc设为stvec。sret是从trap返回的指令,当trap完成后,sret会把pc置为sepc,然后跳回初始状态。scause(Supervisor Cause Register)
cpu把trap原因存这儿sscratch(Supervisor Scratch Register)
用来保存某些context。具体不清楚。sstatus(Supervisor Status Register)
其中的SIE位表示硬件中断使能。
SPP设置模式。0-user。1-supervisor。
上述寄存器只能在supervisor模式操作。同样有一套machine模式的寄存器,只用在一些特殊情况。
多核架构的每个cpu都有一套相同的寄存器。同一个时刻可能有多个cpu在处理trap。
发生trap时cpu的动作(除了timer中断)
如果trap是硬件中断,且SIE为0。啥都不干
清SIE关中断
pc值保存到sepc
保存SPP
设置scause
转到supervisor模式
pc值设为stvec
执行pc
注意cpu并不转到kernel页表和kernel栈。除了pc也不存其他寄存器。
这些工作由kernel来做,并不是cpu来做。
让cpu只做最少的工作,是为了灵活性,可对kernel逻辑进行修改,而不是把逻辑写死在cpu。
start.c的start进行靠近cpu层面的各种配置。最后走main.c的main。
// start.c
// erntry.S里call start到这。每个cpu都会执行。
// 此时所有cpu为默认的machine模式
start()
// set M Previous Privilege mode to Supervisor, for mret.
unsigned long x = r_mstatus(); // 获取mstatus寄存器值
x &= ~MSTATUS_MPP_MASK; // SPP的两位写0,其他都为1。
x |= MSTATUS_MPP_S; // SPP置为01也就是supervisor模式。
w_mstatus(x); // 写寄存器。在mret时,会根据这个寄存器改变模式。
// set M Exception Program Counter to main, for mret.
// requires gcc -mcmodel=medany
w_mepc((uint64)main); // 设置pc寄存器。mret时会从pc开始执行指令。也就是mret时调用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);
// riscv-privileged文档3.1.8
// 默认所有trap都在machine模式下处理。可以设置代理让S模式处理。
// 这样设置后,trap会先走默认的mtvec,再执行stvec。实际是个转发的效果。
// configure Physical Memory Protection to give supervisor mode
// access to all of physical memory.
w_pmpaddr0(0x3fffffffffffffull);
w_pmpcfg0(0xf);
//
// ask for clock interrupts.
timerinit(); // 时钟相关。见lab4/lab7。
// keep each CPU's hartid in its tp register, for cpuid().
int id = r_mhartid(); // 获取hartid
w_tp(id); // 写到thread pointer寄存器
// switch to supervisor mode and jump to main().
asm volatile("mret"); // mepc设置为main。走main函数。同时模式改为之前设置的S。
最后走main.c的main。
// 此时cpu在S模式
main()
if(cpuid() == 0){
// 只有cpu0运行一次特殊的初始化流程
consoleinit(); // 初始化uart。可暂忽略
printfinit(); // 可暂忽略
printf("\n");
printf("xv6 kernel is booting\n");
printf("\n");
kinit(); // physical page allocator
// 物理内存页初始化。见lab2和3
kvminit(); // create kernel page table
// 创建kernel页表。kernel页表全系统唯一。见lab3
// 对各种kernel数据做映射
kvminithart(); // turn on paging
// 把kernel页表喂给cpu
procinit(); // process table
// 初始化进程array,都置为UNUSED。
// 每个进程有独立的kstack,1k大小,排在内存空间的末尾。见手册图3.3
trapinit(); // 忽略
trapinithart(); // install kernel trap vector
w_stvec((uint64)kernelvec);
// 配置stvec为kernelvec,碰到trap会走kernelvec。
plicinit(); // set up interrupt controller
plicinithart(); // ask PLIC for device interrupts
// 设备和中断相关。见lab7。可暂忽略
binit(); // buffer cache
iinit(); // inode table
fileinit(); // file table
virtio_disk_init(); // emulated hard disk
// 文件系统相关。见lab9。可暂时忽略
userinit(); // first user process
struct proc *p;
p = allocproc();
// 找一个可用的进程资源。状态置为USED。
// 给p->trapframe分配一个页
// proc_pagetable创建一个user页表
// 设置context信息。后续运行进程时用到
// 最初的ra设为forkret。那么第一次切换到该进程会先走一次forkret,回到user空间再跑epc。
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
initproc = p;
// allocate one user page and copy initcode's instructions
// and data into it.
// initcode是写死的initcode.S编译生成的数据
// 内容是发起syscall。exec调用init。
/* initcode.S
# exec(init, argv)
.globl start
start:
la a0, init # SYS_exec第一个参数为程序名。init
la a1, argv # SYS_exec参数列表。就一个程序名,没有多余的参数。
li a7, SYS_exec
ecall # 走system call流程和exec流程。运行用户程序init。
# for(;;) exit();
exit:
li a7, SYS_exit
ecall
jal exit
# char init[] = "/init\0";
init:
.string "/init\0"
# char *argv[] = { init, 0 };
.p2align 2
argv:
.long init
.long 0
*/
// uvmfirst从user页表分配一页,从虚拟地址0开始。把initcode数据复制进去,后续可直接运行。
uvmfirst(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
// user pc寄存器设为0。后续会根据这个值,运行上面0地址的指令。即运行initcode.S。
// 为什么是0。
// 其他的user程序是在user.ld指定ENTRY( _main )。然后exec时读elf里的这个entry给到epc。
// 但是initcode.S没走user.ld。
// 这里直接写0,我估计是因为initcode.S里把start写在最前面强制作为0,不十分确定。
// 如果写在最前面无法强制作为0,那么就是编出initcode的elf之后读到里面的entry值为0,然后这里epc写为0。
// 总之是人工进行了这里的0和initcode.S中start的对应。
p->trapframe->sp = PGSIZE; // user stack pointer
// sp从第2页开始
safestrcpy(p->name, "initcode", sizeof(p->name));
p->cwd = namei("/");
p->state = RUNNABLE;
// 状态设为RUNNABLE。后续进程调度时可开始执行。
release(&p->lock);
__sync_synchronize();
// memory barrier
// https://gcc.gnu.org/onlinedocs/gcc-4.6.2/gcc/Atomic-Builtins.html
// https://en.wikipedia.org/wiki/Memory_barrier
started = 1;
} else {
while(started == 0)
;
__sync_synchronize();
printf("hart %d starting\n", cpuid());
// 其他cpu开始自己的初始化。可忽略
kvminithart(); // turn on paging
trapinithart(); // install kernel trap vector
plicinithart(); // ask PLIC for device interrupts
}
// 开始进程调度。每个cpu会跑一个scheduler,死循环不断寻找需要运行的进程并切换运行。
// 此时只有之前userinit中创建的唯一一个进程。
scheduler();
struct proc *p;
struct cpu *c = mycpu();
c->proc = 0;
for(;;)
for(p = proc; p < &proc[NPROC]; p++)
acquire(&p->lock);
if(p->state == RUNNABLE) {
p->state = RUNNING;
c->proc = p;
swtch(&c->context, &p->context);
// 切换到userinit创建的进程
// swtch.S
// context包含ra/sp/s0-s11一共14个寄存器
// sd ra, 0(a0)
// sd sp, 8(a0)
// ...
// ld ra, 0(a1)
// ld sp, 8(a1)
// ...
// ret
// 根据riscv的calling convention。函数调用的前两个参数会存在a0和a1。
// sd把寄存器值存到内存地址
// ld把内存地址上的值存到寄存器
// 最后的效果就是把当前的context寄存器保存到c->context,
// 把p->context的寄存器值加载到cpu当前寄存器。
// 在swtch运行过程中把ra写为p->context中的ra。
// 那么swtch结束时会返回到之前设置的forkret。
forkret
usertrapret
// 此时cpu仍在S模式
// 开始为回到user模式做准备
struct proc *p = myproc();
// we're about to switch the destination of traps from
// kerneltrap() to usertrap(), so turn off interrupts until
// we're back in user space, where usertrap() is correct.
intr_off(); // 关中断
// send syscalls, interrupts, and exceptions to uservec in trampoline.S
uint64 trampoline_uservec = TRAMPOLINE + (uservec - trampoline);
w_stvec(trampoline_uservec);
// TRAMPOLINE = MAXVA - PGSIZE指向虚拟地址最后一页。已经在kvminit中映射到了trampoline.S中的trampoline。
// TRAMPOLINE是虚拟地址。uservec - trampoline是一个offset。
// trampoline_uservec得到uservec函数的地址
// 设置为stvec。后续user的trap会走uservec。
// 之前已经给cpu设置了kernel页表,所以这里都是传入虚拟地址。
// user空间的system call走ecall触发trap后就走uservec。然后走usertrap,调用syscall()。
// 完成后又走到这里,回到user模式,完成整个system call。
// 详见lab2
// set up trapframe values that uservec will need when
// the process next traps into the kernel.
p->trapframe->kernel_satp = r_satp(); // kernel page table
p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
p->trapframe->kernel_trap = (uint64)usertrap;
p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()
// 保存kernel页表。atp
// 保存kernel的sp
// 设置处理user trap的函数
// 保存hartid
// set up the registers that trampoline.S's sret will use
// to get to user space.
// set S Previous Privilege mode to User.
unsigned long x = r_sstatus();
x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode // 配置为user模式
x |= SSTATUS_SPIE; // enable interrupts in user mode // 打开中断
w_sstatus(x);
// set S Exception Program Counter to the saved user pc.
w_sepc(p->trapframe->epc);
// userinit中设置了0。这里取出来写到pc。
// tell trampoline.S the user page table to switch to.
uint64 satp = MAKE_SATP(p->pagetable);
// 页表配置为进程的user页表
// jump to userret in trampoline.S at the top of memory, which
// switches to the user page table, restores user registers,
// and switches to user mode with sret.
uint64 trampoline_userret = TRAMPOLINE + (userret - trampoline);
((void (*)(uint64))trampoline_userret)(satp);
// 走userret函数。sret后进user模式。
// pc也设置到了0,走之前设置好的initcode。即运行exec init。
// 开始运行init.c的main。起sh命令行。
// 到此系统就跑起来了。sh等待用户输命令。
c->proc = 0;
}
release(&p->lock);
到此可以说大致了解了一个简易riscv操作系统的运行模式。
本质上无非是让cpu运行指令。在这之上进行抽象,做出各自独立的进程/程序。
每个进程绑定各自的内存等资源。多进程排队抢cpu来运行自己的指令。
操作cpu寄存器实现进程的切换/模式的切换。
配合cpu的各种模式/功能实现更高级的功能。