xv6
xv6启动第一个进程
entry.S
- 从
_entry
开始运行xv6,此时虚拟地址直接映射到物理地址 kernel.ld
将 xv6内核加载到0x80000000
,0x0~0x80000000
包含IO设备- 设置栈(用于运行c代码),stack0在
start.c
文件中声明 - 执行 start.c 中的 start()
- 从
start.c
- 修改运行模式为 Machine 模式
- 设置main函数的地址
- 禁止虚拟地址转换
- 将所有的中断和异常委托给管理模式
- 内存保护
- 产生计时器中断
- 通过
mret
回到 Supervisor 模式
main.c
- 初始化设备、子系统
userinit
执行initcode.S
- scheduler() 会调度到第一次个proc,即init
init.c
- 创建 console
- 打开文件描述符
- 启动shell
页表
术语
- PTE: Page Table Entry
- PPN: Physical Page Nuber
- TLB: Translation Look-aside Buffer
页表用途
- 映射相同的内存到不同的地址空间
- 用未映射的页面保护内核和用户栈
分页
内核将所有物理内存映射到其页表,因此可以直接对物理内存操作
虚拟地址转化步骤
offset
12位:一个页表是4096字节,可以用12位二进制数字表示index
27位:每个PTE是8字节,一个页表可以储存512个PTE(9位),index是三级页表(L1、L2、L3各9位)PPN
44位:Sv39 RISC-V 仅用到44位物理页码
内核使用
satp
寄存器: 存放根页表页在内存中的地址- 每个 CPU 有独立的
satp
寄存器,因此不同CPU的虚拟地址空间不同,但是使用同一个内核地址空间 - 切换进程就需要修改
satp
寄存器 ?
常见标记位
PTE_V
: PTE是否存在,0表示不合法PTE_R
: 是否可读PTE_W
: 是否可写PTE_X
: 是否可执行PTE_U
: 用户能否访问,0表示仅内核可访问
内核地址空间
- 物理地址
0x8000000
以下: 保留了设备接口,作为内存映射控制寄存器暴露给软件,通过这些特殊的物理地址与设备交互 - 内核的虚拟地址和物理地址一致,可以通过虚拟地址直接操作物理内存
内核虚拟地址中非直接映射部分:
trampoline page
: 虚拟地址最后一页guard page
: 进程的内核栈会有 guard page,防止栈溢出。不会映射到物理地址空间,PTE_V 不设置。 - 内核会给每个进程分配一个内核栈
内核栈是在系统启动的时候就创建好的吗?对应的内核一直没使用?
进程地址空间
- 不同进程的页表将用户地址转换为物理内存的不同页面,因此进程的内存互相隔离
- 进程的虚拟内存空间是连续的,对应的物理内存可以是非连续的
- 将虚拟地址的最后一页映射到trampoline page,所有地址空间都有一个单独的物理内存页
- 用户栈前一页是 guard page,防止栈溢出
trap 和 系统调用
cpu转交控制权: 系统调用、异常、设备中断,将其都称之为trap
trap处理:CPU的硬件操作、汇编指令集、trap处理程序、设备驱动服务例程
三种trap来源:用户空间trap、内核空间trap、定时器中断
- trap触发,转交控制权到内核
- 内核保存寄存器和其他状态
- 执行handler代码
- 内核恢复寄存器和状态,返回到原来的代码
trap机制
寄存器
仅内核态可以读取,每个CPU都有独立的寄存器
stvec
: trap handler的地址sepc
: 保存程序计数器pc
,当trap恢复的时候,sret
会将sepc
赋值给pc
scause
: trap来源的标志位sscratch
: trap handler使用sstatus
SIE
: 判断中断是否启用,通过清除SIE
来禁用中断SPP
: 判断trap的来源,用户态 或 内核态
trap硬件操作
- 如果 trap 是设备中断,且
SIE
被清空,不执行任何操作 - 清除
SIE
禁用中断 - 将
pc
复制到sepc
- 将当前模式 (用户态、内核态) 保存在
sstatus
的SPP
中 - 设置
scause
标志位 - 转换为内核模式
- 把
stvec
复制到pc
- 跳转到
pc
指令,开始执行
用户态trap
- uservec: 保存用户寄存器(进程TRAPFRAME)
- usertrap: 确定trap的来源分发到不同的handler,
- usertrapret: 保留
stvec
,用于下一次trap,设置寄存器 - userret: 切换页表至原来的页表
内核态trap
- kernelvec: 保存所有寄存器(内核栈)、kerneltrap、恢复寄存器
- kerneltrap: 分发任务、恢复
sepc
和sstatus
- 异常:panic
- 计时器中断:yield
页面错误异常
用户态异常,内核终止故障进程;内核态异常,panic
页面错误
scause
: 页面错误类型stval
:无法翻译的地址
- 加载页面错误
- 存储页面错误
- 指令页面错误
COW fork
- 父子进程共享物理页面,但只读。
- 进程存储页面异常:内核复制错误地址的页面,生成可读写副本。此时返回故障指令,就会使用副本的pte
lazy allocation
- 用户态 sbrk 申请内存,此时申请的页表标记为无效。
- 出现页面错误后,将页表映射到物理内存
磁盘分页
- 内存换出,保存到磁盘中。
- 页面故障,将磁盘中存储的页面,读取到内存中。
- 内存页满之后,需要换出内存页表
中断和设备驱动
控制台输入
UART
- 控制台驱动通过 UART串口,接收键入字符。QEMU模拟了UART硬件,连接到键盘和显示器
- UART对于软件,可以看做内存映射的控制寄存器
consoleinit
: 初始化UART硬件,对字节输入生成接收中断,对字节输出生成发送中断
定时器中断
- 定时器中断来自每个CPU的时钟硬件
timerinit
: 在一定延迟后生成中断、保存寄存器、使能定时器中断timervec
: 触发一次软件中断
xv6
https://gjorn4389.github.io/2023/12/23/xv6/