系统调用syscall(ecall)
1.用户态的准备工作
cpu核心在用户线程(进程)上工作, pc(程序计数器寄存器)指向下一个执行的用户态指令(.text), sp(栈指针寄存器)指向当前用户态线程的栈顶,用于分割栈帧和栈帧中变量的定位.
当pc指向的下一条指令是ecall并开始执行时
-
权限提升:由 User Mode (U-Mode) 进入 Supervisor Mode (S-Mode)。
状态备份:
- 将
ecall指令本身的地址存入sepc(Supervisor Exception Program Counter)。 - 将
sstatus的 SPP (Supervisor Previous Privilege) 位设置为 0(表示来自 U-Mode)。 - 将
sstatus的 SPIE 位存入当前中断使能位,并随后关闭中断 (SIE = 0)。
异常原因:将
scause寄存器设置为 8 (Environment call from U-mode)。 - 将
-
控制权转移:
-
将pc修改成寄存器
stvec(trap vec)指向的地址, 这个地址指向trampoline页的uservec入口 trampoline是跨页执行的桥梁(用户页表和内核页表)
-
2. 为进入内核做准备
cpu核心开始执行pc指向的指令(uservec)
- **原子交换: **将寄存器
a0和寄存器sscratch交换值
sscratch预先存入了该进程的trapframe的虚拟地址, trapframe结构体是用来保存从用户态进入内核态前的寄存器状态和进入内核时所需要的页表,中断入口,内核入口等的地址,位于trampline的底下一页.所以 trapframe = 用户上下文 + 内核入口信息
- 保存寄存器: 将以
a0(已经指向了trapframe)为基址,将所有的通用寄存器(包括和sscratch交换的a0)保存进trapfram结构体中
当所有通用寄存器保存完毕后, 开始加载内核态状态信息
-
加载内核信息:从
trapframe中读取预存的kernel_satp(内核页表基址)、kernel_sp(内核栈指针)和kernel_trap(即usertrap函数的地址)到指定的寄存器上。 -
切换页表:通过
csrw satp, t1切换页表,t1中是刚才读入的kernel_satp。此时,MMU 开始使用内核页表,由于trampoline是等值映射,流水线可以继续执行。 trampline和trapframe在用户态和内核态中的虚拟地址都是一样的,由于内核页表通常不包含用户态的数据映射,Trampoline 页必须在用户页表和内核页表中进行等值映射 (Identity Mapping),以确保切换
satp寄存器时指令流水线不会崩溃。 -
刷新 TLB:执行
sfence.vma确保后续地址转换使用新的页表。 -
跳转:将
pc改为kernel_trap的值,正式进入内核态的 C 函数usertrap。
3.进入内核态
进入内核态的第一个入口函数usertrap, 会检查一些必要条件,然后决定执行是执行函数
-
权限检查: 检查是否是从user mode进入
-
中断类型检查:验证
scause是系统调用, 异常中断,软件中断, 如果是系统调用则进入syscall -
p->trapframe->epc += 4,使返回后的 PC 指向下一条用户指令。 -
开启中断
-
syscall函数:
- 从
trapframe->a7读取系统调用号。 - 检查索引是否合法,然后通过**系统调用派发表(syscalls array)**调用对应的实现函数。
- 参数读取:对应的内核处理函数(如
sys_write)会调用argint、argaddr等辅助函数。这些函数实质上是直接去读取保存在trapframe中的a0到a5寄存器的值。 - 返回值处理:系统调用函数的返回值被写入
p->trapframe->a0。 - 调度检查: 当处理函数返回后,会检查是否有定时器中断, 如果有就进入进程调度处理中
- 从
4. 离开内核态 (usertrapret & userret)
准备返回工作
-
关闭中断
-
存入内核信息: 将
kernel_satp(内核页表基址)、kernel_sp(内核栈指针)和kernel_trap(即usertrap函数的地址),kernel_hartid, 全部存入trapframe中, 为了下次进入内核读取 -
设置csr寄存器:
- 将
sstatus的SPP位清零并开启中断在用户态中 - 将
sepc恢复成之前保持的用户态的pc
- 将
-
转跳到
trampline.S的userret汇编函数 -
userret (汇编):
- 切换回用户页表。
- 恢复所有用户通用寄存器。
- 将
trapframe地址再次存入sscratch。 - 执行
sret:CPU 回到 User Mode,将pc设置为sepc的值,并打开中断(如果sstatus如此配置)。
thinking
我把用户态和内核态理解为两个不同的“虚拟世界”,页表就像一副 VR 眼镜,决定了 CPU 如何解释虚拟地址。
在 trap 发生时,CPU 虽然切换到了更高权限,但仍然使用原来的用户页表,因此无法直接访问内核代码。
为了解决这个问题,系统会构造一段特殊的内存区域(trampoline),它在用户页表和内核页表中具有相同的虚拟地址,并映射到同一物理地址。注意trapframe是不同的虚拟地址
这样,在切换页表的过程中,当前正在执行的代码不会消失,以为pc映射的物理地址依旧能够找到, 从而保证指令流的连续性,使系统能够安全地从用户态过渡到内核态。
因此,trampoline 的本质不是“入口”,而是一个保证页表切换过程中执行连续性的桥梁。
|---------------|---|===================|
| user | | 内核地址 |
|_______________|___|===================|
地址空间的转换
在计算机体系结构与操作系统设计中,**地址空间映射(Address Space Mapping)**是实现进程隔离、内存保护及硬件资源抽象的核心机制。
1. 映射的本质:解构与重组(The Mechanism)
地址映射并非简单的“数字替换”,而是一套基于**分级索引(Multi-level Indexing)**的逻辑转换。
- 空间分割: 虚拟地址(VA)被划分为 VPN(虚拟页号) 和 Offset(页内偏移)。在 RISC-V Sv39 架构中,VPN 进一步拆分为三级索引(9-9-9),这种设计确保了页表本身可以离散地存储在物理内存中,避免了为巨额虚拟空间预留连续物理内存的开销。
- 属性解耦: 每一个页表项(PTE)不仅记录了物理页号(PPN),还承载了元数据控制位(
R/W/X/U/A/D)。这使得同一块物理内存在不同的映射关系下,可以展现出完全不同的访问权限。
2. 映射的策略:直接映射与离散映射(Mapping Strategies)
- 直接映射(Direct Mapping / Identity Mapping):
- 定义: 虚拟地址 VA 线性对应物理地址 PA(通常满足 VA = PA 或 VA = PA + Const)。
- 专业价值: 主要用于内核启动初始化与高效率内存管理。它允许内核在不经过复杂计算的情况下直接操作物理设备和页表项。
- 离散映射(Fully Associative Mapping):
- 定义: 虚拟地址与物理地址之间不存在代数关系,映射关系完全随机。
- 专业价值: 这是**用户空间(Userspace)**的基础。它消除了物理内存碎片的影响,通过“虚连实散”的方式,为每个进程提供了一个连续、私有且从零开始的地址空间幻觉。
3. 映射的关键同步:页表切换与原子性(The Context Switch)
地址空间的映射关系是动态切换的,这引入了计算机系统中最精密的同步问题:
- 状态连续性(Continuity): 在切换
satp(根页表寄存器)的瞬间,必须存在一个恒等映射区(Trampoline)。该区域在旧映射与新映射下保持 VA => PA 的绝对一致,以确保指令流水线中的 PC 指针不会因地址翻译规则的突变而指向非法区域。 - TLB 刷新(TLB Flushing): 由于硬件会缓存映射结果(TLB),在更改映射规则或切换进程后,必须执行
sfence.vma指令。这在专业上称为地址空间的屏障(Barrier),防止旧映射残留导致的安全漏洞或逻辑错误。
4. 映射的演进:延迟绑定与按需映射(Advanced Semantics)
现代操作系统利用映射机制实现了很多高性能特性:
- 写时复制(Copy-on-Write, COW): 通过将映射标记为只读,捕获硬件异常(Page Fault),实现物理内存延迟分配。
- 延迟分配(Lazy Allocation):
walk(..., 0)与walk(..., 1)的选择体现了**按需分页(Demand Paging)**的思想。只有当程序真正触碰到地址时,映射关系才会从逻辑定义转变为物理存在。 - 地址空间布局随机化(ASLR): 故意破坏映射的规律性,增加攻击者预测内存布局的难度,这是映射机制在安全领域的高级应用。
总结
地址空间映射不仅仅是 VA 到 PA 的转换,它是操作系统对硬件资源的数字化定义。
专业定义: 地址映射是一种通过硬件(MMU)加速的、由软件(Kernel)定义的空间编址协议。它将碎片化的、有限的物理资源,转化为统一的、受保护的、近乎无限的逻辑资源。
启动
加载内核代码进入内存
bootloader按照链接脚本把内核代码加载到物理内存的固定位置(0x800000000), 并将pc指针指向entry.S的_entry函数地址
初始化硬件, 在进入内核前
为每个cpu核心初始化栈
在_entry中,每个核心都会执行一遍,通过每个核心的hartid,计算自己的栈的sp地址.然后跳转到c语言代码strat()中
最初的c语言
进入C语言, 第一步就是设置mstatus寄存器,用来记录之前的特权等级,把他设置成s mode, 当然这里是骗人的, 这是为了在mret时能够返回到s mode故意设置的,有种编造自己的来历的感觉
然后再设置寄存器mepc, 把他设置成mret后要跳到哪里去, 这里是跳到main.c的main()
并且将satp寄存器设置为0,关闭页表的使用,直接使用物理地址,以防万一
然后将异常和中断都代理给s mode, 并打开s mode下的中断源使能位, 因为中断异常默认走m mode, 这样设置,可以直接让内核处理大部分的中断异常
在然后给s mode授权物理地址的访问权限,m mode下给了s mode几乎全部的物理内存和RWX权限
初始化定时器中断 ??
将每个cpu核心的tp寄存器中写入hartid
然后mret
从开机到目前为止,所有核心都是并行的执行代码,初始化自己的寄存器和权限设置的
m mode也太权威了, 原来内核态的权利都是他给的, 就连访问内存的权限都是他给的
进入内核代码
现在所有的cpu核心都是以s mode进入内核态,开始执行内核初始化函数,在执行main()函数时,只由一个核心执行,避免出现多核竞争导致内核初始化失败.