新闻详情

新闻详情

首页 / 资讯中心 / 详情

Linux 0.11 内存空间深度解析:从分页机制到进程隔离

发布时间:2026/9/28 23:39:37来源:尧图网络
Linux 0.11 内存空间深度解析:从分页机制到进程隔离
聊Linux 0.11的内存空间可能是学习操作系统最划算的一笔投入。这个版本代码量不到两万行却把80386的分段、分页、进程隔离、物理页分配这些概念全部串在了一起。很多人一上来啃《深入理解Linux内核》这类大部头结果被各种抽象机制直接淹没我自己的经验反而是从0.11入手把内存空间这四个字拆开一条线走到黑才真正建立起对内存管理的直觉。这篇文章打算把Linux 0.11的内存空间完整梳理一遍从物理内存布局、线性地址分配、页目录页表组织到fork时的页表复制、写时保护触发一路讲到常见误区和调试手段。无论你是做嵌入式、搞驱动、准备Linux相关面试还是单纯想搞懂内核这条梳理路径都值得跟着走一遍。1. 为什么是Linux 0.11为什么要梳理内存空间1.1 0.11在Linux历史中的坐标Linux 0.11是1991年的版本距今已经三十多年。它比0.01成熟得多能跑多进程、有文件系统、支持终端还能自己编译内核但比后来的0.12、0.95又简单太多没有复杂的swap、没有完整的虚拟内存系统、没有细粒度的内存对象缓存。它正好卡在分段分页全都有、但还没被各种优化细节淹没的位置上。这个版本最难得的地方在于你可以在一个周末把关键代码从头到尾读完。现代内核像一座城市你只能看到局部0.11像一座小镇你站在制高点能看清每条街道怎么走。我读它的时候最大的感受是内存管理不是一堆抽象名词而是几段随时可以追踪到具体实现的代码。0.11就是这样一个天然的解剖样本。1.2 内存模型的基本盘分段与分页并存眼花但不乱0.11运行在80386保护模式下CPU的段机制和页机制是同时开启的。这让不少人头大因为平时习惯了虚拟地址→物理地址的线性思维突然告诉你中间还有一层线性地址确实容易懵。0.11好在把两种机制用得很直白段机制负责进程隔离。每个进程通过LDT局部描述符表里的代码段、数据段描述符把自己的线性地址空间定位到进程号乘以64MB的位置。页机制负责物理内存管理。线性地址经过页目录、页表两级转换映射到真正的物理页框。一个用户程序访问一个内存地址先做段转换得到线性地址再做页转换得到物理地址。这就是逻辑地址→线性地址→物理地址的完整链路。这条链路一旦走通后面看任何内存相关代码都是在这个框架里补细节。1.3 梳理内存空间这条路谁需要走一遍如果你在做嵌入式Linux或者驱动开发内存空间的划分、页表的组织、进程隔离的实现都是绕不开的基本功。如果你在准备面试0.11里的写时复制、缺页处理、进程地址空间切换正好是那些高频题目的原型。如果你只是好奇操作系统到底怎么管理内存0.11是最合适的起点。下面我会从物理布局讲到线性地址从数据结构讲到关键代码路径最后给出我在Bochs里调试的真实教训。这篇文章不会让你瞬间成为内核专家但能让你以后再看到内存踩踏0地址访问写时复制这些词时脑子里浮现出一个清晰的图景而不是一团模糊的概念。2. 从物理内存到虚拟内存的全局观2.1 物理内存布局1MB以下的禁区、1MB以上的地盘先看物理内存。0.11启动时setup.s通过BIOS中断拿到机器装配的内存大小传给main.c。main.c里对物理内存做分区划分逻辑大致如下地址范围用途0x000000 - 0x0FFFFF低端1MB实模式中断向量、BIOS数据、显存等内核不碰1MB - buffer_memory_end缓冲区给硬盘等块设备做cachebuffer_memory_end - memory_end主内存区交给mem_map管理进程要页从这里领megabytes这个buffer_memory_end不是写死的。main.c里的判断是内存大于12MB时缓冲区上限设为4MB大于6MB时设为2MB否则设为1MB。说白了就是在内存紧张的时候宁可少给缓冲区一点空间也要保证主内存区够进程用。这个取舍放在今天看依然合理甚至可以说是早期Linux里少有的不为设计而设计的实用主义。低端1MB在0.11里其实还有一个细节内核代码和核心数据结构页目录、GDT、IDT、每个进程的TSS/LDT描述符都挤在这1MB以内。所以内核开发时你顺手改一个数组定义可能就让整个内核镜像膨胀到超出了低端内存的容纳范围启动直接失败。这也是我最早踩过的坑之一。2.2 内核空间与用户空间的边界不是3G/1G模型现代Linux把4GB虚拟地址空间划成用户态3GB、内核态1GB所有进程共享内核部分的高地址映射。这个模型在0.11里完全不存在。0.11的线性地址空间里每个进程各占一块64MB的区域内核代码在物理内存低端通过内核段的base0来访问。换句话说0.11的用户空间不是一个固定的区间而是由进程的LDT段描述符动态指定的。进程0在0-64MB进程1在64-128MB进程2在128-192MB以此类推。这种隔离方式非常原始但非常直观每个进程的线性地址区间天然错开互不重叠。这个差异如果不搞清楚后面看任何源码都会迷惑。比如你打开0.11的memory.c看到一堆关于64MB、进程号的计算第一反应可能是这写的是什么但只要你脑子里有这个每进程一块线性区的模型一切都顺理成章。2.3 每进程64MB线性空间当时的进程隔离方案为什么偏偏是64MB因为在0.11的设计里进程号nr作为索引线性地址基址base nr * 0x40000000x4000000就是64MB。0x4000000这个数很讲究它对齐到了页目录项的边界上一个页目录项映射4MB64MB正好是16个页目录项。这样任意两个进程的线性地址区间都能整齐地按页目录项切分复制页表时非常方便。当时单进程的地址空间上限就是64MB在现代看来小得可怜但放到1991年完全够用——那时主流机器的内存普遍只有4MB、8MB、16MB。0.11的设计哲学就是看起来够用就行没有现代内核那么多防御性设计。理解这种时代局限性反而能帮你更好地理解内核演进的逻辑很多东西不是天生如此是慢慢被现实逼出来的。同时要注意0.11最多支持64个进程NR_TASKS64这个限制也和线性地址空间划分直接相关。GDT里为每个进程预留TSS和LDT两个槽位64个进程就需要128个槽加上系统占用的几个也就把GDT的256项空间用掉了一半以上。2.4 一次地址访问要闯过哪几道关举个具体的例子。假设进程5的代码里访问一个变量编译后产生一个逻辑地址0x1234。CPU执行访问指令时实际的流程是根据当前LDT进程5的LDT取数据段描述符得到段基址5 * 64MB 320MB。逻辑地址0x1234加上段基址得到线性地址0x140001234。线性地址按位拆分页目录索引为线性地址的高10位320MB / 4MB 第80项页表索引为中间10位页内偏移为低12位。查页目录第80项拿到页表地址再查页表对应项拿到物理页框号加上页内偏移最终得到物理地址。整个过程由CPU自动完成但每一道关卡都在访问内存管理数据结构。哪一环断了CPU就会触发缺页异常把控制权交给内核的缺页处理函数。这也是为什么page fault处理在整个内核中都是重头戏——它承担着补齐页表的职责。3. 内存管理的核心数据结构3.1 task_struct里的内存字段0.11的task_struct里没有独立的mm_struct而是把内存相关字段直接摊在进程控制块里。看include/linux/sched.h你会发现这样一段struct task_struct { long state; long counter; long priority; long signal; struct sigaction sigaction[32]; long blocked; int exit_code; unsigned long start_code, end_code, end_data, brk, start_stack; long pid, father, pgrp, session, leader; ... struct desc_struct ldt[2]; struct tss_struct tss; };start_code、end_code、end_data、brk、start_stack是exec后由do_execve填充的分别表示代码段起点、代码段终点、数据段终点、堆终点、栈起点。这四个字段决定了进程的代码、数据、堆、栈在逻辑地址空间里的边界。ldt[2]放的是进程自己的两个段描述符。tss里保存了esp0、cr3等寄存器现场。注意0.11的TSS是直接嵌在task_struct里的而不是现代内核那样单独分配。这种全部摊在一个结构体里的做法虽然不够优雅但胜在简单切换进程时整个进程控制块都是连续的物理内存访问起来毫无压力。3.2 页目录与页表的组织0.11的全局页目录是swapper_pg_dir定义在head.s里。关键点是所有进程复用同一个页目录不同进程只是在这个页目录的不同索引段上建立映射。每个进程占用16个页目录项64MB / 4MB这些目录项指向进程自己的页表。页表是运行时动态分配的物理页。fork时copy_mem会复制父进程的页表到子进程的线性地址区间缺页时do_no_page会按需建立新的页表项。每个页表正好占一页物理内存对应1024项映射4MB的线性地址空间。页表项的结构和现代x86基本一致几个关键位的含义要记牢位含义bit0存在位P为0表示页不在内存bit1读写位R/W为0表示只读bit2用户/管理位U/S为0表示仅内核态可访问bit3页级写穿PWTbit4页级缓存PCDbit5访问位Abit6脏位Dbit12-31物理页框地址0.11的写时复制正好利用了bit1fork后父子进程的页表项都被清掉R/W位一旦有人写就触发page fault进入do_wp_page复制页面后再把对应页表项置为可写。3.3 mem_map物理页面的记账本主内存区的物理页用mem_map数组管理。这个数组的内存管理方式原始到不能再原始没有buddy系统、没有slab缓存、没有LRU链表只有一个0/1标记数组。mem_map的下标通过MAP_NR(addr)计算对应物理地址从LOW_MEM0x640000即640KB开始的每一页。元素为0表示空闲为1表示已分配。get_free_page从数组末尾向前找第一个0把它置1算出物理地址清零页面后返回。这个设计也不记录页的所有者、不记录引用计数。一页内存要么没有主人要么只有一个主人。这带来一个明显隐患如果某页被错误释放两次第一次置0第二次又置0这页就可能被两个进程同时拿到造成内存数据重叠。0.11里没出大事故纯粹是因为运行场景小、并发低。到了0.12之后内核才逐渐引入更完善的页表项管理这就是后话了。3.4 GDT/IDT/LDT段机制的三件套0.11的GDT在head.s里定义sched_init中动态填充。布局大致如下GDT索引内容0空描述符1内核代码段base02内核数据段base03系统调用段base04任务0的TSS5任务0的LDT6任务1的TSS7任务1的LDT...后续任务的TSS/LDT每个进程在GDT里占两个槽一个TSS一个LDT。进程切换通过ljmp到目标进程的TSS选择子CPU自动加载新的TSS和LDT从而切换到目标进程的线性地址空间。选择子的规律也比较规整TSS选择子 0x20 nr16LDT选择子 0x28 nr16。LDT里放两个描述符代码段和数据段。它们的base都被设为nr*64MBlimit设为64MB。这意味着用户态代码编译出来的逻辑地址天然在0-64MB范围内经过段基址平移后落到自己的线性区间。这套段基址平移的方案在今天看来笨重但在没有硬件ASID地址空间标识的年代它是一种干净利落的进程隔离手段。4. 关键路径源码拆解4.1 copy_mem与fork时内存复制fork时最核心的内存操作在copy_mem。流程可以概括为四步根据子进程的进程号nr计算线性地址基址new_data_base nr * 0x4000000。记录父进程当前的基址old_data_base。设置子进程LDT的代码段、数据段描述符把段基址改为new_data_base。调用copy_page_tables把父进程线性地址区域的页目录项和页表项整体复制到new_data_base处。copy_page_tables内部会为每个被复制的页表重新分配一页物理内存把源页表内容整体拷贝过去确保子进程有自己的页表副本。这里有一个关键操作它会把源页表项和目的页表项的R/W位都清掉让所有映射变成只读。这就是写时复制COW的触发条件。复制结束后还有一段TLB刷新逻辑。0.11的做法比较粗暴重新加载CR3让整个TLB全部失效。这当然会带来一点性能开销但对当时的进程数量来说完全可接受。你需要记住的是修改页表后必须让CPU丢掉旧的缓存否则新映射可能不生效这个问题的排查往往非常隐蔽。4.2 get_free_page与物理页分配get_free_page是0.11物理内存分配的核心函数逻辑非常简单unsigned long get_free_page(void) { for (i mem_map PAGES - 1; i mem_map; i--) if (!*i) { *i 1; page LOW_MEM (i - mem_map) * PAGE_SIZE; clear_page(page); return page; } return 0; }注意它从高地址向低地址找空闲页。这个方向和很多现代内核相反但目的很明确让靠近缓冲区、低端内核区的页面尽量少被分配从而降低碎片化风险。clear_page(page)把物理页清零。这个清零不是可有可无的如果不做新进程很可能读到上一个使用者的残留数据造成严重的信息泄漏。0.11虽然古老但这一点做得很到位。分配失败返回0调用方需要自行处理错误。比如copy_page_tables在分配失败时会释放已建立的页表并返回负值导致fork失败。错误路径虽然简单但逻辑闭环是完整的。4.3 free_page与页释放free_page的对立面实现同样简单void free_page(unsigned long addr) { if (addr LOW_MEM) return; if (addr high_memory) return; mem_map[MAP_NR(addr)]--; }这个实现里有几个值得注意的点。第一它对低端内存和超出high_memory的地址做了保护避免误释放内核区或不存在的内存。第二它用mem_map--而不是直接置0。分配时明明只置1为什么释放要递减看起来像是为引用计数预留的口子但实际上0.11并没有完整的引用计数机制。如果不小心对同一个地址调用两次free_pagemem_map会变成负数但这个负数不会被检查页面随后还可能被再次分配出去。这个计数衰减设计埋了个雷。第三free_page不会清理对应的页表项。这意味着释放物理页后页表项仍然标记该页存在且可读写后续访问会读到别的进程的数据。0.11在exit和exec路径里专门提供了free_page_tables来拆除整个进程的页表那才是真正干净的释放操作。直接调free_page释放物理页再把进程转出去很容易产生内存错乱。4.4 缺页异常与写保护处理缺页入口在page.s里的汇编封装根据错误码跳到do_no_page页不存在或do_wp_page写保护页。do_wp_page处理的是写保护页。0.11里的处理逻辑简单直接只要写保护异常发生在用户空间就分配一个新页把旧页内容拷贝过来再把对应的页表项改为可写。它不区分本来就是只读的代码页和本来可写但因COW变成只读的数据页一律复制。代码段正常不会有人写所以影响不大但这确实属于吃性能的糙办法。do_no_page处理的是页不存在的情况。它先判断地址是否落在进程的文件映射区域内如果是就从文件读取对应内容填充到新分配的页面。0.11没有swap机制所以对于超出文件范围的地址直接分配一页清零返回。这就是懒加载的雏形exec之后进程的代码段和数据段并不是一次性全部读入内存而是等访问时缺页再从文件系统按需读取。这个思想一直延续到今天只是现代内核的缺页路径要复杂得多。4.5 缓冲区与外设IO的关系缓冲区也是0.11内存空间的重要组成。buffer_init在1MB到buffer_memory_end之间建立空闲链表每个buffer head对应一块数据区域。硬盘读写时块设备驱动通过缓冲区把数据搬进内存。0.11的缓冲区是静态划定的和现代内核里动态伸缩的page cache完全不同。好处是实现简单所有缓冲区操作都不涉及内存压力坏处是内存利用率低缓冲区空闲时不能被进程借用紧张时也不能自动缩水。理解这个背景后再看后来内核引入动态页缓存的设计思路就会明白这是被实际使用场景逼出来的优化。5. 常见问题、误区与调试方法5.1 fork后父子进程为何能隔离常见误区以为0.11的fork是直接复制页表所以父子进程共享物理页。实际上页表虽然复制了但物理页引用是共享的且被标记为只读。子进程或者父进程一旦写内存就触发do_wp_page复制物理页并把这个页对应的页表项改成可写。从这个意义上说0.11已经实现了写时复制只是它的COW不区分文件映射页和匿名页一律复制。这也带出一个经典面试题为什么0.11 fork之后父子进程的代码段可以共享答案就是代码段通常只读写保护异常几乎不会触发所以两进程实际共享同一份物理代码页。但一旦有人往代码段写页面数据就会被错误复制一份——虽然成本高但安全上反而没问题。5.2 内存踩踏的经典案例我在Bochs里试过一个场景写一个用户程序通过系统调用访问一个内核地址然后往里写数据。0.11没有现代内核那种强隔离但页表项的U/S位是有区分作用的用户态访问内核页会触发保护异常。0.11对这种异常的处理比较直接die。所以它也不是完全没有保护只是保护边界不如现代版本细腻。我实际遇到过的踩踏案例是缓冲区管理代码越界写了一个字节把主内存区某个mem_map标记写坏导致某页被分配了两次。表面现象是进程A的数据突然变成进程B的数据非常难查。最后是在get_free_page里临时加打印输出每次分配的物理页地址和调用栈才定位到是哪个模块越界。这种页面被重复分配的问题在0.11里排查起来尤其麻烦因为它没有引用计数来检测。5.3 用模拟器观察内存现场我强烈推荐Bochs它自带调试器可以单步执行、读写物理内存、设置内存断点。调试0.11时我的常用操作bochs -q # 在Bochs命令行 # info memory 查看内存布局 # x /64bx 0x100000 查看物理内存内容 # u /10 反汇编当前EIP处指令QEMU配合GDB也可以gdb里target remote :1234然后用monitor info mem看客户机的页表情况。但我个人的体感是QEMU对古老Linux镜像的兼容性不如Bochs稳定遇到一些奇怪的指令Bochs能跑而QEMU不动的时候以Bochs为准。另一个实用技巧是在page.s的入口处打印CR2寄存器的值。CR2保存了触发缺页的线性地址配合错误码能快速判断是页不存在还是写保护。我靠这一招省下的时间够我再完整读一遍0.11的内存管理代码。5.4 面试高频问题速查把0.11相关的常见问题整理成一张速查表方便复习问题0.11的答案进程地址空间如何隔离段基址隔离每个进程占64MB线性地址区页表何时建立fork时复制父进程页表缺页时动态建立写时复制如何触发fork后页表项清R/W位写访问触发do_wp_page物理页如何分配mem_map找空闲页从高地址向前遍历内核与用户空间边界段描述符区分用户走LDT内核走GDT缺页后如何懒加载do_no_page按文件偏移读取内容填页进程切换如何换地址空间ljmp加载TSS和LDT切换段基址这些问题放到今天依然是Linux内存面试题的核心骨架只是现代内核的答案要复杂得多0.11反而是最容易讲清楚原型的版本。6. 我的实操体会与后续扩展方向我第一次完整读完0.11的进程切换和内存管理代码是在一个周末。说实话读的时候总觉得内存还是一个模模糊糊的平面。后来我在Bochs里把fork前后的CR3、LDT、页表项全部打印出来对着物理地址一个个核对才真正把逻辑地址→线性地址→物理地址这三个概念焊死在一起。建议你也这样干一次不要停留在读代码想尽办法在模拟器里看到内存的实际变化。比如fork之后打开子进程的线性地址区间对比页表项和父进程的差异比如故意触发do_wp_page观察物理页什么时候被复制、引用地址什么时候变化。这种亲眼所见的经验比任何原理讲解都更深刻。再往后学的话可以顺着三个方向延伸一是从0.11跳到0.95之后的版本看3G/1G模型、完整的页缓存、swap机制是怎么演进出来的二是对照现代x86的PML4四级页表看64位下地址转换如何扩展三是把0.11的buffer机制和后来的radix tree、page cache对比理解内存管理从手写链表到通用框架的演化动机。对了最后分享一个小技巧如果你在调试时发现进程内存数据莫名其妙被改先别急着怀疑业务代码。在0.11里优先怀疑mem_map的越界写和缓冲区越界这两个地方一个会导致页面重复分配一个会导致内核和用户数据互相污染。把这两个点查完大部分诡异问题都会水落石出。这个排查思路到现在做嵌入式开发依然适用。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Linux进程控制核心机制:fork/wait/exit深度解析 2026/9/29 1:19:05

Linux进程控制核心机制:fork/wait/exit深度解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
LDN双模键盘深度解析:从芯片协议到故障根因 2026/9/29 1:19:05

LDN双模键盘深度解析:从芯片协议到故障根因

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
ComfyUI+AnimateDiff+ControlNet角色动画工作流实战 2026/9/29 1:19:05

ComfyUI+AnimateDiff+ControlNet角色动画工作流实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Jlink烧录仿真工具全解析:从SWD接线到常见报错排查 2026/9/29 1:19:05

Jlink烧录仿真工具全解析:从SWD接线到常见报错排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
CSS居中方案详解:水平、垂直与水平垂直居中 2026/9/29 1:19:05

CSS居中方案详解:水平、垂直与水平垂直居中

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
基于STM32单片机太阳能手机无线充电宝电压电流锂电池蓝牙/WiFi/视频监控/云平台无线APP-DIY设计S528 2026/9/29 1:18:58

基于STM32单片机太阳能手机无线充电宝电压电流锂电池蓝牙/WiFi/视频监控/云平台无线APP-DIY设计S528

S528-太阳能无线充电宝USB输出输出电压电流功率锂电池电压电量欠压OLED屏声光提醒按键蓝牙/WiFi/视频监控/云平台APP本系统由STM32F103C8T6单片机核心板、OLED屏、无线蓝牙/WIFI/视频监控/云平台模块-可选、太阳能接口、锂电池充电管理电路、锂电池升压电路、无线充电接口、USB…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉