把整段对话中涉及的概念、问答、纠偏、例子和最终实践项目,整理成一份可离线阅读的单文件说明书。
DeleteObject 只负责销毁某些 GDI 对象本体(例如 bitmap、brush、font、pen),不负责你把它选进哪个 DC、不负责 DC 本身,也不负责你保存的旧句柄或外部缓冲区。正确理解是:它释放的是“对象本体生命周期”,不是“所有引用关系和外部绑定关系”。CreateCompatibleDC / CreateDC 产生的 DC 一般用 DeleteDC 释放;GetDC 拿到的 DC 用 ReleaseDC 归还;BeginPaint / EndPaint 里的 DC 由系统管理,不要 DeleteDC。核心原则是:谁创建,谁释放;系统借给你的,用系统 API 归还。DeleteObject、DeleteDC、ReleaseDC 对应不同层级,不能互相替代。
malloc/new 更底层,工作在页级别,适合大块、连续、可控的内存管理。HeapAlloc 是“堆管理器分配”,通常用于小块内存,内部会做切分、复用和碎片管理;VirtualAlloc 是直接向 OS 要页,粒度更大,更适合大块、连续、可分区管理的内存。可以把 HeapAlloc 理解为“切蛋糕”,VirtualAlloc 理解为“拿地”。MEM_RESERVE 是在进程虚拟地址空间里“占坑”,只占地址,不一定占物理页;MEM_COMMIT 是让该虚拟页具备可访问资格,系统承诺未来访问时会有页可用。它们是不同层:reserve 解决地址布局,commit 解决页面可访问。VirtualFree(ptr, 0, MEM_RELEASE)。如果只是取消 commit 但保留占位,可以用 MEM_DECOMMIT。MEM_RESERVE 负责“地址空间布局”,MEM_COMMIT 负责“页面可访问”,而物理页是否立刻出现,往往由第一次访问时的 page fault 决定。
malloc ≈ HeapAlloc 这一层的用户态分配器;mmap ≈ VirtualAlloc 这一层的 OS 虚拟内存映射接口。malloc 常常底层会再用 brk/sbrk 或 mmap。malloc 是 C 标准库/运行时(如 glibc、musl、jemalloc)提供的用户态分配器,不是内核 API。底层小对象常从连续 heap(brk/sbrk 扩展)切分,大对象常直接用 mmap 创建独立 mapping。真正差别不是“有没有碎片”,而是“映射/回收的粒度与耦合方式”。sbrk(0) 读出当前 break,再一次性扩一大段,然后自己用 bump pointer 在线性区里切分。这非常像 arena / stack-like allocator:大块拿到后,内部只前进指针,不做复杂 free。适合生命周期整体一致、批量释放的场景。brk(addr) 是把 program break 直接设到绝对地址;sbrk(delta) 是相对移动。brk(0) 不是惯用写法,通常失败,因为它试图把 heap 末尾设为 NULL;brk(1) 也几乎一定失败;brk(start) 如果把 break 缩回去,会使新 break 之后的内存失效,之前分配出去但位于该边界后的对象就不再属于 heap 了。brk 是绝对设置;sbrk 是相对移动 program break。它本来就是对 brk 的包装。brk/sbrk 操作的是“共享的进程主 heap 边界”,因此和 malloc 强耦合;mmap 是独立 mapping,所以用户自己用 mmap 通常不会干扰 malloc 的主 heap 状态。
mmap() 会在进程地址空间中建立一段虚拟地址映射,把这段地址与一个 memory object(由文件描述符代表的对象)关联起来。返回的地址 pa 是一个实现定义的地址;映射的虚拟地址范围和文件偏移范围都必须在“可能的合法范围”之内。文档强调的重点是:mmap 建的是“合法的虚拟映射”,不等于立刻把整段都变成已驻留物理内存。void *mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t off); 里的参数分别干什么?addr 是你希望映射出现的位置,通常传 NULL 让 OS 自选;len 是长度;prot 是权限,如读写执行;flags 决定映射行为,如 MAP_PRIVATE、MAP_SHARED、MAP_ANONYMOUS、MAP_FIXED;fildes 是文件描述符;off 是文件偏移,从文件的哪个位置开始映射。user code
↓ syscall
kernel
↓ check page cache
↓ (miss) submit block IO
storage controller + DMA
↓
RAM (page cache)
↓ memcpy
user buffer
这里真正额外的 CPU 工作是 memcpy。磁盘页到 RAM 的搬运通常由 DMA 完成。
user code loads [p + i]
↓ MMU lookup
page not present?
↓ page fault
kernel loads file page into RAM
↓ update page table
retry load
↓
CPU reads RAM directly
mmap 避免的是 page cache → user buffer 这一步显式复制,而不是让磁盘“跳过 RAM”。
unrar 后,用 unrar x file.rar 解压。也可以安装 7zip,用 7z x file.rar。unrar 对 RAR 支持更完整,7z 更通用。只要允许任意大小、任意顺序 free、长短生命周期混合,就必然会出现空洞。外部碎片是“总空闲不少,但连续空间不够”;内部碎片是“按页对齐后多出来的浪费”。OS、allocator、brk、mmap、VirtualAlloc 都无法彻底消灭碎片,只能管理和缓解它。
brk/sbrk 维护的是一个连续、只会从末尾增长或缩小的 heap 边界,因此具有 stack-like 的线性特征:轻松处理末尾、难以处理中间。malloc/free 之所以复杂,是因为它在这块连续区域里自己维护空闲链表和块元数据,试图把线性区域变成“任意大小、任意顺序释放”的通用分配器。
mmap 的优势不是没有碎片,而是可以把不同生命周期的大块区域独立成不同 mapping,整体释放更容易。若你自己再在一个大 mmap 区域里切小块,它同样会碎片化。真正决定碎片程度的是“生命周期是否统一、释放策略是否可整体回收”。
一个页可能已经存在、已经映射、甚至已经有物理 RAM,但这不等于它对你的程序“合法”。相反,某些逻辑上“已经申请”的区域也可能尚未真正分配物理页。理解虚拟内存,必须同时分清:物理页、虚拟页、allocator 边界、语言对象有效范围。
viewer image.bmp。它不仅是练习,而是能完整串起“文件 → 虚拟内存 → allocator → framebuffer → 窗口显示”的工具。
你将做一个只用 CPU 软件渲染的图片查看器,支持 BMP 文件加载、mmap 读取、窗口显示、缩放、移动和基础性能分析。整个过程从文件 IO、虚拟内存、内存布局、像素写入、到窗口呈现,全部亲手实现。
| 层级 | 技术 | 作用 | 为什么选它 |
|---|---|---|---|
| 语言 | Rust stable | 写底层、内存安全、跨平台 | 既能接近系统,又比 C 更稳 |
| 窗口 | winit | 创建窗口、输入事件、主循环 | 平台层薄,便于理解 |
| 显示 | softbuffer | 把 CPU framebuffer 提交到窗口 | 保留软件渲染风格 |
| 文件映射 | libc::mmap / munmap | 把 BMP 文件映射到虚拟内存 | 理解 page fault / page cache |
| 内存分配 | 自写 arena allocator | 临时对象、图像元数据、渲染缓冲 | 理解 brk/sbrk、碎片、线性分配 |
| 图像格式 | 24-bit BMP | 最简单的裸像素图片格式 | 便于手写解析 |
| 性能分析 | std::time / perf | 测帧时间、观察 page fault、cache miss | 让理解可验证 |
src/
main.rs // 程序入口:事件循环、命令行参数、整体编排
platform.rs // 窗口、输入、softbuffer显示
renderer.rs // CPU绘制:清屏、矩形、缩放、采样
bitmap.rs // BMP解析、像素访问、文件头结构
memory.rs // arena allocator、mmap封装、临时内存
目标:创建窗口,在窗口中显示纯色或渐变的 CPU 像素缓冲区。
输入:无,或先用代码生成的颜色值。
输出:一个固定尺寸窗口(例如 480×270),内容为纯蓝、渐变或棋盘格。
技术:winit 创建窗口,softbuffer 获取 buffer,直接写 buffer[y * width + x]。
关键实现思路:先把窗口和像素内存跑通,不碰图片、不碰文件、不碰复杂状态。你要先证明“我能写像素并显示”。
需要观察的点:宽高、stride、像素格式(如 0xAARRGGBB)、buffer 提交和窗口刷新之间的关系。
目标:实现最小图元绘制:清屏、矩形、线段、简单填充。
输入:函数调用,如 draw_rect(x, y, w, h, color)。
输出:窗口中出现矩形、线段和渐变。
技术:纯 CPU 写 buffer,不使用 GPU 图形 API。
关键实现思路:把图形最终都还原成像素循环,明白 clipping、rasterization、行优先访问的 cache locality。
建议实验:随机矩形压力测试、鼠标拖拽画线、不同扫描顺序的性能比较。
目标:手写 24-bit BMP 文件解析器,先用普通文件读取实现,再扩展到 mmap 版本。
输入:一个 BMP 文件(建议从最简单的未压缩 24-bit BMP 开始)。
输出:得到宽、高、像素数组,正确显示 BMP 图像。
技术:二进制头结构、字节对齐、little endian、scanline padding、bottom-up 存储。
关键实现思路:先只支持最简单的 BMP 子集,明确文件头和像素区的内存布局,再逐步扩展。
建议实验:翻转上下、做灰度化、验证每行 4 字节对齐的 padding。
目标:把 BMP 的加载从 read() 切换为 mmap(),直接从映射内存解析。
输入:同一个 BMP 文件。
输出:功能保持一致,但文件内容来自映射而不是拷贝出来的 Vec。
技术:libc::mmap、munmap、页对齐、offset、page fault、page cache。
关键实现思路:映射后不要先复制整文件,直接把头结构和像素区“当作内存”去读。这样你会非常直观地感受到“文件变成地址空间的一部分”。
建议实验:大文件 mmap 观察“映射快、首次访问慢”;随机访问与顺序扫描的差别。
目标:实现一个 bump / arena 分配器,用于临时对象、图像处理临时缓冲、字符串拼接等。
输入:arena_alloc(size)、arena_reset()。
输出:一块连续、可快速分配和整体重置的内存区域。
技术:底层可由 mmap(MAP_ANONYMOUS) 提供大块连续内存,再在其上 bump pointer 分配。
关键实现思路:把生命周期一致的临时对象放进 arena;帧结束时 reset,避免细粒度 free。
建议实验:做一个每帧重置的 frame arena,用于临时字符串、临时图像转换数据。
目标:让查看器能够移动、缩放、重采样。
输入:键盘 WASD、鼠标滚轮、拖拽。
输出:图片可自由平移缩放。
技术:坐标变换、nearest-neighbor sampling、可选 bilinear sampling。
关键实现思路:把显示坐标映射回图像坐标,再采样。这样你会真正理解 texture sampling 和 raster space。
建议实验:16 倍像素放大、双线性过滤、观察锯齿和插值差异。
目标:理解软件渲染性能如何受缓存、分页和访问模式影响。
输入:不同尺寸图片、不同缩放比例、不同绘制顺序。
输出:帧时间统计、page fault 观察、cache locality 对比。
技术:std::time、Linux perf、可选统计 page fault。
关键实现思路:比较 row-major / column-major 访问,观察扫图时的连续访问优势;比较 mmap 大文件顺序访问和随机访问的差异。
目标:把图片查看器扩展成一个真正的小型 2D 软件渲染器。
功能:sprite、tilemap、alpha blending、简单动画、UI 叠层。
技术:依旧是 CPU 写像素;增加混合、遮罩、层级顺序、时间步进。
关键实现思路:从“看图”扩展到“画图”,把查看器变成一个更完整的 2D 软件渲染实验台。
因为它会把下面这些概念串成一条链:
| 术语 | 一行解释 |
|---|---|
| DeleteObject | 释放某些 GDI 对象本体,不负责 DC、外部 buffer 或所有引用。 |
| DeleteDC | 释放 CreateDC / CreateCompatibleDC 得到的 DC。 |
| ReleaseDC | 归还从 GetDC 得到的 DC。 |
| VirtualAlloc | Windows 页级虚拟内存 API,可 reserve / commit。 |
| HeapAlloc | Windows 堆分配接口,面向小对象和复用。 |
| MEM_RESERVE | 预留虚拟地址空间,占坑不一定占物理页。 |
| MEM_COMMIT | 让页面具备可访问资格,物理页可按需出现。 |
| brk / sbrk | Linux 传统 heap 边界控制,线性增长/缩小。 |
| mmap | 创建虚拟内存映射,文件映射或匿名内存都可。 |
| page fault | 访问未映射或未驻留页面时由 CPU 触发的异常。 |
| page cache | 内核中的文件页缓存,磁盘内容通常先进入这里。 |
| DMA | 设备控制器直接搬运数据到 RAM,CPU 不逐字节拷贝。 |
| zero-copy | 通常指减少额外 buffer copy,不代表没有任何物理搬运。 |
| softbuffer | 把 CPU framebuffer 提交到窗口系统的 Rust 库。 |
| arena allocator | 线性分配器,适合统一生命周期对象批量管理。 |
| Handmade Hero | 强调自己控制平台层、内存和软件渲染的学习路线。 |