内存管理:

kinit(); // physical page allocator

void
kinit()
{
  initlock(&kmem.lock, "kmem");
  printf("kinit called\n");
  freerange(end, (void*)PHYSTOP);
}

为什么要管理?

**硬件: **内存的本质是 一块连续的物理空间

但是对于OS来说:

操作系统是资源的调度者,对内存来说,它必须解决两个问题:

  1. 哪些内存能用?

一些页存放内核代码、数据

一些页映射了设备

只有“空闲页”才能分配出去

所以你要记录:“哪些页是空的?”

  1. 怎么分配、怎么回收?

kalloc() 要找一个空闲页 怎么找?

kfree(p) 要回收到系统中 怎么定位回收?

所以你要一个“可操作的数据结构”来支撑分配和回收

同时需要锁的支持 避免同时修改内存中的链表结构!

虚拟内存管理:

当一个进程请求内存时(如sbrk()系统调用):

步骤1:扩展虚拟地址空间

// 假设进程请求扩大堆空间 char *new_addr = sbrk(4096); // 分配4096字节的虚拟地址 操作系统仅在进程的页表中标记一段新的虚拟地址范围(如0x1000~0x2000)可用

此时无物理内存分配,页表项被标记为"无效"(Present=0)。

malloc的内存分配机制

  1. 虚拟地址分配:

操作系统在进程的页表中标记一段虚拟地址范围可用(如0x1000~0x3000)。

此时不分配物理内存,页表项标记为"无效"(Present=0)。

  1. 首次访问触发缺页:

p[0] = 'A'; // 第一次写入 CPU发现虚拟地址0x1000无物理页 → 触发缺页异常。

  1. 内核处理异常:

调用kalloc()分配物理页。

更新页表,建立0x1000→物理页的映射。

  1. 恢复用户进程执行。

后续访问:

页表已有映射 → 直接访问物理内存。

#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);

密码/密钥泄漏:如用户进程的内存页曾存储密码,释放后未清空,被恶意进程分配到。

隐私数据暴露:如浏览器缓存的历史记录残留。

流程

  1. 初始化内存分页 并 添加锁
  2. 调用宏计算将地址对齐
  3. 寻找空闲物理页 并将其 添加到 freelist中!供后续分配使用

Why:

内存回收: 当内核或用户程序不再需要某块物理内存时,通过 kfree 将其归还给系统,避免内存泄漏。

支持动态分配: kfree 和 kalloc 配合实现物理内存的分配与释放(类似 malloc/free,但更底层)

寄存器

CPU中寄存器的原理

简单理解了一下寄存器: 通过电路的设计 使电路具有了记忆能够记住上一时刻的状态 也就保存了数据 !

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 (用户栈顶)

  1. 0x1010 处的指令是 ecall,也就是用户程序请求内核服务
  2. ecall 指令会从用户态触发陷入内核态(S-mode)。
  3. SATP = 用户页表基址
  4. stvec 是内核指定 "trap 入口地址" 的寄存器。发生 trap(如 ecall、中断、异常)时,CPU 就跳到 stvec 这个地址开始执行。

注意: 当发生ecall时 虽然会切换模式 为 内核态 但是 并不会自动切换页表,仍然使用的是 当前用户的页表,所有用户页表中都会映射这个 trampoline 页