操作系统学习16 物理内存探测与分配器(PMM)
2026/7/26 7:39:08 网站建设 项目流程

操作系统学习16 物理内存探测与分配器(PMM)

  • 一、本章说明
    • 1. 前置知识
    • 2. 本章目标
    • 3. 目录结构
    • 4. 与 lesson15 的差异
  • 二、为什么不能直接“使用”所有内存?
  • 三、BIOS INT 15h(E820)探测物理内存
    • 1. ARDS 是什么?
      • 如何判定「无效 / 空」条目(检测为 0)
    • 2. ARDS 结构(20 字节)
    • 3. 迭代查询流程
    • 4. 本课实现(`boot/loader.s`)
  • 四、物理页分配器(PMM)
    • 1. 位图约定
    • 2. 数据结构(`kernel/pmm.h`)
    • 3. 初始化四步(`pmm_init`)
    • 4. 分配与释放
    • 5. kernel_end
      • kernel_end.s 本身
      • 它靠链接顺序“钉”在末尾:
      • c 侧使用
      • 对齐说明
  • 五、内核演示(`kernel/kernel.c`)
  • 六、本章小结

一、本章说明

1. 前置知识

本章接续 lesson15(8253 PIT + 抢占式多任务)。前面已经能进保护模式、处理中断并做任务切换,但物理内存地址仍依赖“假定机器有多少 RAM”的硬编码——换一台虚拟机配置就可能踩到空洞或设备预留区。

2. 本章目标

完成本章后,我们将能够:

  • 实模式下用 BIOSINT 15h, AX=E820h探测真实物理内存布局(ARDS)
  • 把杂乱的内存段清洗为4KB 物理页帧(Page Frame)
  • 用**位图(Bitmap)**实现物理内存分配器(PMM):pmm_init/pmm_alloc_page/pmm_free_page
  • 在内核中打印 ARDS、总内存与分配/释放结果,在 Bochs 中验证

3. 目录结构

lesson16/ ├── Makefile ├── boot/ │ ├── loader.s # 读盘 + E820 探测 + 进保护模式 │ └── pm.inc # 段描述符宏 ├── kernel/ │ ├── main.c # 打印 ARDS / PMM 状态,演示 alloc/free │ ├── pmm.c / pmm.h # 位图物理页分配器 │ ├── kernel_end.s # 导出 kernel_end(位图紧随内核放置) │ ├── utilities.s # 显存输出等(沿用前几章) │ └── utilities.h └── README.md

4. 与 lesson15 的差异

项目lesson15lesson16
引导读盘后直接进 PM进 PM 前增加E820 探测
内存管理无(栈/任务栈静态数组)Bitmap PMM
多任务task.c/task.s+ IRQ0本章演示不再依赖调度
内核入口创建 A/B 任务并切换初始化 PMM 并打印分配结果

实模式读盘

INT 15h E820

计数@0x8000 / ARDS@0x8010

进入保护模式

pmm_init

bitmap 管理 4KB 页

alloc / free


二、为什么不能直接“使用”所有内存?

系统开机时,物理 RAM 并不是连续且完全可用的。部分区域被固件、显存或主板设备占用:

0x00000000 +-----------------------------------+ | IVT / BIOS Data Area | <- 需谨慎使用 0x000A0000 +-----------------------------------+ | Video RAM (VGA 640KB~1MB) | <- 不可作普通 RAM 0x00100000 +-----------------------------------+ | Extended Memory(内核通常在此) | <- 可用 Usable | ... | +-----------------------------------+ | ACPI / Reserved / Device MMIO | <- 硬件预留 +-----------------------------------+

盲目向0xA0000写数据会花屏;写入 Reserved 区可能直接崩溃。因此:先探测、后分配


三、BIOS INT 15h(E820)探测物理内存

1. ARDS 是什么?

ARDS全称Address Range Descriptor Structure(地址范围描述符结构)。

可以把它理解成 BIOS 给出的一张「内存地契」:一条 ARDS = 物理地址空间里的一段连续区域,说明这段从哪开始、有多长、能不能当普通 RAM 用。

E820 不会一次把整张内存地图塞给你,而是迭代返回多条 ARDS;把它们拼起来,就是整机的物理内存布局。例如典型机器可能返回:

条目大致范围Type含义
#00x00000000起一小段1可用低端 RAM
#10x000A0000附近2显存 / 设备预留,不可当堆用
#20x00100000起大段1可用扩展内存(内核常在此)
3/4 等ACPI 等特殊用途

内核只把Type = 1(Usable)的页交给分配器;其它类型一律视为不可分配。

如何判定「无效 / 空」条目(检测为 0)

BIOS 不会用「全 0 结构」作为官方结束符——正常结束看的是EBX == 0(或 CF=1)
但缓冲区里仍可能出现全 0 槽位(未写入的残留、被跳过的洞)。判定方式:

/* type==0 或 length==0 → 无效,直接跳过 */staticinlineintards_is_valid(constards_t*e){returne->type!=0&&e->length!=0;}
字段为 0 时含义
type == 0不是合法 E820 类型(合法值从 1 起),视为空槽
length == 0零长度区间,无意义,跳过
baselength皆 0典型的未初始化填充,与上面两条一并过滤

截图里[6][7]type=0就是这类脏槽。真正的条目数应以 loader 写入的ARDS count为准;若 count 被破坏,再按ards_is_valid过滤并钳制上限。

2. ARDS 结构(20 字节)

每条 ARDS 固定20 字节(本课按经典 E820 请求;部分 BIOS 还可返回 24 字节扩展属性,此处不展开):

偏移大小名称含义
0x008BaseAddr该段物理起始地址(64 位)
0x088Length该段长度(字节,64 位)
0x104Type1=可用 RAM;2=预留;3=ACPI 可回收;4=ACPI NVS;0=无效

本课约定内存地图布局(均在内核0xC200之前):

地址内容
0x8000uint16有效 ARDS 条数
0x8010ARDS 数组(每项 20 字节)

3. 迭代查询流程

每次调用返回一条 ARDS,并用EBX作为 continuation;EBX == 0表示结束。

CF=1 或 EAX != 'SMAP'

CF=0 且 EAX == 'SMAP'

设置 EAX=0xE820, ECX=20, EDX='SMAP', EBX=0

INT 15h

检查返回值

失败 / 结束

保存 ARDS,DI+=20,计数+1

EBX == 0 ?

结束

继续循环

4. 本课实现(boot/loader.s

在读盘成功、enter_pm之前调用detect_memory

ARDS_COUNT EQU 0x8000 ARDS_BUFFER EQU 0x8010 detect_memory: xor ebx, ebx xor ax, ax mov ds, ax mov es, ax mov di, ARDS_BUFFER mov word [ARDS_COUNT], 0 .e820_loop: mov eax, 0xE820 mov ecx, 20 mov edx, 0x534D4150 ; 'SMAP' int 0x15 jc .e820_done cmp eax, 0x534D4150 jne .e820_error ; type==0 或 length==0 → 跳过(不前进 DI,下次覆盖写) cmp dword [es:di + 16], 0 je .e820_skip cmp dword [es:di + 8], 0 jne .e820_accept cmp dword [es:di + 12], 0 je .e820_skip .e820_accept: add di, 20 inc word [ARDS_COUNT] .e820_skip: test ebx, ebx jnz .e820_loop .e820_done: ret

对应 C 侧判定(与 20 字节条目一一对应):

typedefstruct{uint64_tbase_addr;uint64_tlength;uint32_ttype;}__attribute__((packed))ards_t;/* 检测是否为 0 / 无效条目 */staticinlineintards_is_valid(constards_t*e){returne->type!=0&&e->length!=0;}

四、物理页分配器(PMM)

x86 常见以4KB为一页。若物理上界为 128MB:

总页数 = 128 × 1024 × 1024 4096 = 32768 \text{总页数} = \frac{128 \times 1024 \times 1024}{4096} = 32768总页数=4096128×1024×1024=32768

1. 位图约定

  • 1 bit ↔ 1 个 4KB 页
  • 0= 空闲,1= 已占用 / 不可用

8 bit 管理 (8 \times 4\text{KB} = 32\text{KB});管理 128MB 仅需约4KB位图。

2. 数据结构(kernel/pmm.h

typedefstruct{uint8_t*bitmap;uint32_tbitmap_size;uint32_ttotal_pages;uint32_tfree_pages;uint32_tmax_mem_size;}pmm_manager_t;

kernel_end由链接末尾的kernel_end.s导出,位图放在页对齐后的内核末尾,避免踩内核代码/数据。

3. 初始化四步(pmm_init

  1. 扫描type == 1的 ARDS,得到最大物理上界 →total_pages/bitmap_size
  2. 位图默认全部置0xFF(全部占用,安全第一)
  3. 再把可用段对应页清 0,并累计free_pages
  4. 低端内存 + 内核 + 位图自身重新置 1

半页处理:可用段起始页向上对齐、结束页向下对齐,避免把非完整页标成空闲。

4. 分配与释放

void*pmm_alloc_page(void);/* 找第一个空闲 bit,置 1,返回页物理地址 */voidpmm_free_page(void*p);/* 页对齐检查后清 0,free_pages++ */

分配是线性扫描位图(教学实现,清晰优先;后续可改为 buddy / 空闲链表等)。

5. kernel_end

kernel_end.s 本身

链接时把一个空标签放到内核映像最后,标签的地址就是“内核结束地址”。
对于kernel_end.s本身:

  • global kernel_end:把符号导出,给 C 用
  • kernel_end::只是一个标签,后面没有任何指令/数据
  • 所以它不占有效内容,只是一个地址标记

它靠链接顺序“钉”在末尾:

kernel.bin: main.o utilities.o pmm.o kernel_end.o$(LD)$(LDFLAGS)-Ttext0xC200...$(TARGET)main.o$(TARGET)utilities.o$(TARGET)pmm.o$(TARGET)kernel_end.o
链接器按命令行顺序排布:
[0xC200]main.o → utilities.o → pmm.o → kernel_end.o ↑ kernel_end 落在这里

-Ttext 0xC200 指定内核从 0xC200 开始;kernel_end.o 放在最后,所以 kernel_end 的值 ≈ 内核映像末尾地址。

c 侧使用

externuint8_tkernel_end[];/* 由 kernel_end.s 提供,链在内核映像末尾 */pmm_init(ards,ards_count,(uint32_t)kernel_end);
  • 声明成数组,取的是地址,不是内容
  • (uint32_t)kernel_end 就是内核结束地址
uint32_tbitmap_addr=align_up(kernel_end_addr,PAGE_SIZE);

把位图放在 kernel_end 向上对齐到页边界之后,这样位图紧挨内核,又不会踩到内核代码/数据。

对齐说明

向上对齐是为了在页边界上安全地放下位图,且不踩内核。

假设kernel_end = 0xC345,页大小0x1000

方向结果问题
向下对齐0xC000落进内核,会覆盖代码/数据
不齐0xC345位图和内核尾巴挤在同一页
向上对齐0xD000越过内核,从整页开始

PMM 按整页管理(分配、回收、预留都是PAGE_SIZE)。位图放在页对齐地址后:

  1. 内核最后那半页只属于内核,不会和位图混在一起
  2. 后面按页预留时更干净:
uint32_t reserved_end = align_up( (uint32_t)pmm.bitmap + pmm.bitmap_size, PAGE_SIZE); uint32_t reserved_pages = reserved_end / PAGE_SIZE;

一句话:向下会覆盖内核;不齐会让一页里既有内核又有位图;向上才是“紧随内核之后、又按页对齐”的安全位置。


五、内核演示(kernel/kernel.c

voidmain(void){ards_t*ards=(ards_t*)ARDS_BUFFER_ADDR;/* 0x8010 */uint16_tards_count=*(uint16_t*)ARDS_COUNT_ADDR;/* 0x8000 */clear_screen();/* 打印若干条 ARDS */pmm_init(ards,ards_count,(uint32_t)kernel_end);/* 打印 Total Memory / Total Pages / Free Pages */void*p1=pmm_alloc_page();void*p2=pmm_alloc_page();pmm_free_page(p1);/* ... */}

Bochs 中典型输出(内存大小随配置变化):

Lesson16: Physical Memory Manager (PMM) ======================================== ARDS count: ... [0] base=0x00000000 len=... type=1 ... === PMM Init === PMM state: mem=31MB pages=8176 used=113 free=8063_ bitmap@0x0000F000 size=1022_ regions: [USED] 0x00000000-0x00010000 [#] [FREE] 0x00010000-0x0009F000 [.] [USED] 0x0009F000-0x00100000 [#] [FREE] 0x00100000-0x01FF0000 [...................] Allocated Page 1: 0x00010000_ Allocated Page 2: 0x00011000_ Freed Page 1. Free Pages: 8062_

构建与运行:

makemakerun


六、本章小结

步骤实现内容关键点
1. 探测loader.s中 E820 迭代实模式、SMAP、ARDS @0x8000
2. 页框4KB 粒度上界 →total_pages
3. 位图先全占再按 ARDS 释放保护内核与位图自身
4. APIalloc/freebit 的 set / clear / test

至此内核掌握了物理页的分配权。以后可以建立页表,把“物理页”映射为进程可用的虚拟地址空间,从而实现隔离与更灵活的内存布局。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询