6.s081
内存管理:
kinit(); // physical page allocator
void
kinit()
{
initlock(&kmem.lock, "kmem");
printf("kinit called\n");
freerange(end, (void*)PHYSTOP);
}
为什么要管理?
**硬件: **内存的本质是 一块连续的物理空间
但是对于OS来说:
操作系统是资源的调度者,对内存来说,它必须解决两个问题:
- 哪些内存能用?
一些页存放内核代码、数据
一些页映射了设备
只有“空闲页”才能分配出去
所以你要记录:“哪些页是空的?”
- 怎么分配、怎么回收?
kalloc() 要找一个空闲页 怎么找?
kfree(p) 要回收到系统中 怎么定位回收?
所以你要一个“可操作的数据结构”来支撑分配和回收
同时需要锁的支持 避免同时修改内存中的链表结构!
虚拟内存管理:
当一个进程请求内存时(如sbrk()系统调用):
步骤1:扩展虚拟地址空间
// 假设进程请求扩大堆空间 char *new_addr = sbrk(4096); // 分配4096字节的虚拟地址 操作系统仅在进程的页表中标记一段新的虚拟地址范围(如0x1000~0x2000)可用。
此时无物理内存分配,页表项被标记为"无效"(Present=0)。
malloc的内存分配机制
- 虚拟地址分配:
操作系统在进程的页表中标记一段虚拟地址范围可用(如0x1000~0x3000)。
此时不分配物理内存,页表项标记为"无效"(Present=0)。
- 首次访问触发缺页:
p[0] = 'A'; // 第一次写入 CPU发现虚拟地址0x1000无物理页 → 触发缺页异常。
- 内核处理异常:
调用kalloc()分配物理页。
更新页表,建立0x1000→物理页的映射。
- 恢复用户进程执行。
后续访问:
页表已有映射 → 直接访问物理内存。
#define PGROUNDUP(sz) (((sz)+PGSIZE-1) & ~(PGSIZE-1))
#define PGROUNDDOWN(a) (((a)) & ~(PGSIZE-1))
大体的意思是 :
让我们以一个例子来说明,如果在一个 PGSIZE 为 1KB 的系统上调用 PGROUNDUP,地址为 620:
PGROUNDUP(620) ==> ((620 + (1024 -1)) & ~(1023)) ==> 1024
地址620被向上舍入到1024。
类似地,对于 PGROUNDDOWN 考虑:
PGROUNDDOWN(2400) ==> (2400 & ~(1023)) ==> 2048
地址2400被向下舍入到2048。
这也就是 页对齐
CPU 的 MMU(内存管理单元) 在管理虚拟内存时,要求 页表项(PTE)指向的物理地址必须是页对齐的(即低 12 位全 0,如果是 4KB 页)。
如果不对齐,CPU 可能无法正确映射内存,导致 缺页异常(Page Fault) 或 数据错误。
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);
密码/密钥泄漏:如用户进程的内存页曾存储密码,释放后未清空,被恶意进程分配到。
隐私数据暴露:如浏览器缓存的历史记录残留。
流程
- 初始化内存分页 并 添加锁
- 调用宏计算将地址对齐
- 寻找空闲物理页 并将其 添加到 freelist中!供后续分配使用
Why:
内存回收: 当内核或用户程序不再需要某块物理内存时,通过 kfree 将其归还给系统,避免内存泄漏。
支持动态分配: kfree 和 kalloc 配合实现物理内存的分配与释放(类似 malloc/free,但更底层)
寄存器
简单理解了一下寄存器: 通过电路的设计 使电路具有了记忆能够记住上一时刻的状态 也就保存了数据 !
RISC-V中通常是32个寄存器 一些主要的寄存器
SP
PC
MODE
SATP
STVEC
SEPC
SSRATCH
具体示例:
当执行系统调用 write(1, "hello", 5) 时
一个进程的地址空间:
高地址 ┌───────────────────────┐ │ 内核空间 │ ← 所有进程共享相同映射 │ - 内核代码和数据 │ │ - 设备内存映射 │ │ - 当前进程的内核栈 │ ├───────────────────────┤ ← 用户空间上限(如0x00007FFFFFFFFFFF) │ 栈空间(向下增长) │ ├───────────────────────┤ │ 共享库映射区 │ ├───────────────────────┤ │ 堆空间(向上增长) │ ├───────────────────────┤ │ 数据段(.data/.bss) │ ├───────────────────────┤ │ 代码段(.text) │ └───────────────────────┘ 低地址(0x0000000000400000)
关键: 所有进程共享相同映射 也就是内核部分的内存映射都是相同的
然后寄存器的状态开始发生改变:
PC = 0x1010 (指向ecall)
MODE = U (用户态)
SATP = 用户页表基址
STVEC = 0xC0000000 (内核trap入口)
SP = 0x7FFFFFFF (用户栈顶)
- 0x1010 处的指令是 ecall,也就是用户程序请求内核服务
- ecall 指令会从用户态触发陷入内核态(S-mode)。
- SATP = 用户页表基址
- stvec 是内核指定 "trap 入口地址" 的寄存器。发生 trap(如 ecall、中断、异常)时,CPU 就跳到 stvec 这个地址开始执行。
注意: 当发生ecall时 虽然会切换模式 为 内核态 但是 并不会自动切换页表,仍然使用的是 当前用户的页表,所有用户页表中都会映射这个 trampoline 页