知识全景图/ 软件工程与系统/ 如何设计系统/ 如何设计操作系统/ 虚拟内存与地址空间

如何设计虚拟内存与地址空间

让每个进程以为自己拥有一片连续内存,同时不能破坏内核和其他进程。

定位:这页不把虚拟内存讲成术语表。它只回答一个失败:用户程序写错地址时,为什么不能让它把整个系统写坏。
一、坏场景:用户程序乱写内存
失败:程序 A 写到错误地址,覆盖了程序 B 的数据;或者用户程序写入内核地址,把调度器、文件表、页表自己破坏掉。没有地址空间,程序就是带权限的炸弹。
为什么失败被迫设计目标
内存只是地址到字节,不知道归属地址空间每个进程有自己的地址视图
虚拟地址需要落到物理内存页表记录虚拟页到物理页的映射
有些地址不能读写执行页权限把读、写、执行、用户态访问分开
非法访问不能拖垮系统异常处理杀掉坏进程或返回错误,内核继续运行
二、最小地址空间长什么样
区域放什么权限为什么
text程序代码R / X代码可执行,但不应被普通写入修改
data全局变量R / W程序状态需要读写
heap动态分配内存R / Wmalloc / new 需要增长空间
stack调用栈R / W函数调用和局部变量需要栈
kernel mapping内核代码和数据仅内核态系统调用和异常处理需要进入内核,但用户不能访问
三、最小对象
对象字段负责什么
AddressSpacepage_table、regions一个进程看到的虚拟内存世界
PageTablevirtual_page、physical_page、flags地址翻译和权限检查
MemoryRegionstart、end、type、permissions记录 text / data / heap / stack 的边界
PageFaultaddress、reason、process、mode解释为什么访问失败
FrameAllocatorfree_pages、used_pages、reserved_pages分配和回收物理页
map_user_page(address_space, va, pa, flags): assert flags.User == true assert va not in kernel_range page_table[va.page] = { physical: pa.page, flags: flags } on_page_fault(fault): if fault.address in kernel_range and fault.mode == user: kill(fault.process) else if can_grow_stack(fault): allocate_and_map_page() else: kill(fault.process)
四、四个关键设计取舍
问题第一版选择代价
是否每个进程独立页表切换进程要切换页表,但隔离清楚
是否映射内核空间所有进程页表保留内核映射,但用户不可访问系统调用快,但权限必须严格
是否支持懒加载第二版再做第一版先完整加载程序,简单可靠
是否支持 swap不做先不把内存管理扩成性能工程
五、验收实验
实验通过标准证明了什么
互相隔离程序 A 不能读取程序 B 的地址进程地址空间独立
内核保护用户程序写内核地址触发异常,系统继续运行用户态 / 内核态边界成立
权限保护写 text 段失败,执行 data 段失败R / W / X 权限有效
栈增长合法栈增长可分配新页,越界增长被拒绝异常处理不是只有杀进程,也能按规则补页
内存耗尽物理页不够时返回错误,不破坏页表资源失败路径可控
六、回到操作系统主线
下一页:系统调用与内核边界
内存隔离之后,用户程序仍然要请求资源。继续看 如何设计系统调用与内核边界
上一步:进程与调度器
如果还没看“程序为什么会失控、内核怎样夺回 CPU”,先回到 如何设计进程与调度器
回总页
如果你想看完整 ToyOS 设计路线,回到 如何设计操作系统
一句话:虚拟内存不是为了“看起来内存更大”,第一版最重要的价值是隔离:让坏程序只能破坏自己,不能破坏内核和别人。