上一篇我们聊了线程的基本概念说实话那篇写得有点水。这一篇我们换个硬核的从物理内存碎片这个老问题讲起看看分页式存储布局是怎么被逼出来的Linux内核又是怎么管理物理页的然后一路往下把页表、页目录、多级页表还有MMU的地址转换流程统统拆开揉碎。好我们开始。目录一、为什么需要分页从物理内存碎片说起1.1 虚拟地址空间与页表为什么会出现1.1.1 物理内存为什么会产生碎片1.1.2 如何实现“虚拟连续、物理离散”1.1.3 分页式存储的核心结论二、物理内存是如何被Linux管理的2.1 从源码认识struct page2.2 struct page中的几个关键成员2.2.1 flags记录物理页的状态2.2.2 _mapcount一个物理页被映射了多少次2.2.3 virtual记录页对应的虚拟地址2.3 一个物理页到底占用多少内存2.4 struct page如何关联到物理地址2.5 struct page还有哪些维护方式三、页表——虚拟地址如何找到物理地址3.1 页表中到底有多少个页表项3.2 虚拟地址与物理地址如何建立映射3.3 页表本身为什么也要占用内存3.4 页表太大怎么办四、多级页表与页目录结构4.1 什么是页目录结构4.2 两级页表如何完成地址转换4.2.1 两级页表的地址转换原理4.2.2 MMU如何完成地址转换4.2.3 快表TLB多级页表如何兼顾效率4.3 再深入理解页目录结构4.3.1 聚集效应与页表访问4.3.2 页目录结构中的其他细节4.3.3 能否从物理地址反推出虚拟地址五、缺页异常访问一个页面时会发生什么六、内存申请相关的几个问题6.1 mmap如何申请虚拟内存6.2 new、malloc与brk之间是什么关系6.3 内存越界了就一定会触发错误吗一、为什么需要分页从物理内存碎片说起1.1 虚拟地址空间与页表为什么会出现1.1.1 物理内存为什么会产生碎片先想一个场景假如没有虚拟内存也没有分页机制那么每一个用户程序在物理内存里占据的空间都必须是连续的一整块就像下面这张图。可问题是每个程序的代码和数据长度都不一样。照这种映射方式走下去物理内存很快就会被切得七零八落变成一堆大小不一的离散块。再跑上一段时间有些程序退出了它们占的内存被回收留下的就是一个个空洞物理内存就这么碎了一地。1.1.2 如何实现“虚拟连续、物理离散”那怎么办呢思路其实很直接操作系统提供给用户的空间必须是连续的但物理内存那边最好别要求连续。于是虚拟内存和分页机制应运而生。做法是把物理内存按固定长度切成一个个“页框”有时也叫物理页。每个页框装一个物理页页的大小等于页框的大小。以前我们讲的哪些问题其实都牵扯到这套机制子进程写时拷贝一次申请4KB共享内存一次申请4KB的整数倍可执行文件的section要合并成一个个4KB的segment。大多数32位体系结构支持4KB的页64位体系结构一般会支持8KB的页。这里有个区分必须说清楚页框和页不是一回事。页框是一个存储区域实实在在的一块物理地盘。页是一个数据块可以放在任何页框里也可以放在磁盘上。权限管理以页框为单位写时拷贝也是。有了这套机制CPU就不再直接访问物理内存地址了而是通过虚拟地址空间间接地去访问物理内存。所谓虚拟地址空间就是操作系统给每个正在执行的进程分配的一块逻辑地址在32位机上范围从0到4GB-1。操作系统在虚拟地址空间和物理内存地址之间建立起一套映射关系这就是页表。页表上记录了每一对“页”和“页框”的对应关系有了它CPU就能间接地访问到真正的物理内存。1.1.3 分页式存储的核心结论一句话就是把虚拟内存那边的逻辑地址空间切成若干页再把物理内存这边切成若干页框中间靠页表牵线搭桥。于是连续的虚拟内存就能映射到一堆不连续的物理页上。物理内存碎片这个老毛病也就这么被治住了。二、物理内存是如何被Linux管理的假设手里有一块4GB的可用物理内存。按每个页框4KB来切4GB ÷ 4KB算下来就是1048576 个页框一百多万个物理页。这么多页操作系统总不能撒手不管。哪些页正被占用哪些页还空着哪些页刚被回收哪些页已经在换出路上……它心里都得有数。2.1 从源码认识struct page内核里每一个物理页都由一个struct page结构体来代表。但物理页的数量动辄上百万要是每个struct page都大手大脚地占内存光是管理结构本身就能把内存吃垮。所以内核在这里用了大量的联合体union让不同的使用场景共用同一块存储空间。省就硬省。/* include/linux/mm_types.h */ struct page { /* 原子标志有些情况下会异步更新 */ unsigned long flags; union { struct { /* 换出页列表例如由 zone-lru_lock 保护的 active_list */ struct list_head lru; /* 如果最低位为 0则指向 inode address_space或为 NULL * 如果页映射为匿名内存最低位置位 * 而且该指针指向 anon_vma 对象 */ struct address_space* mapping; /* 在映射内的偏移量 */ pgoff_t index; /* * 由映射私有不透明数据 * 如果设置了 PagePrivate通常用于 buffer_heads * 如果设置了 PageSwapCache则用于 swp_entry_t * 如果设置了 PG_buddy则用于表示伙伴系统中的阶 */ unsigned long private; }; struct { /* slab, slob and slub */ union { struct list_head slab_list; /* uses lru */ struct { /* Partial pages */ struct page* next; #ifdef CONFIG_64BIT int pages; /* Nr of pages left */ int pobjects; /* Approximate count */ #else short int pages; short int pobjects; #endif }; }; struct kmem_cache* slab_cache; /* not slob */ /* Double-word boundary */ void* freelist; /* first free object */ union { void* s_mem; /* slab: first object */ unsigned long counters; /* SLUB */ struct { /* SLUB */ unsigned inuse : 16; /* 用于 SLUB 分配器对象的数目 */ unsigned objects : 15; unsigned frozen : 1; }; }; }; ... }; union { /* 内存管理子系统中映射的页表项计数用于表示页是否已经映射 * 还用于限制逆向映射搜索 */ atomic_t _mapcount; unsigned int page_type; unsigned int active; /* SLAB */ int units; /* SLOB */ }; ... #if defined(WANT_PAGE_VIRTUAL) /* 内核虚拟地址如果没有映射则为 NULL即高端内存 */ void* virtual; #endif /* WANT_PAGE_VIRTUAL */ ... }看这个结构体头一个感觉就是怎么这么多union原因其实很简单。一个物理页在不同场合下扮演的角色完全不同它可能是一个匿名页属于某个进程可能是一个文件页挂在某个inode的address_space上可能被slab分配器拿去当缓存也可能只是静静地躺在伙伴系统里等着被分配。这些角色同一时刻只会有一个。既然如此何必给每个角色都单独留一块内存用union把它们叠在一起同一块空间谁用谁说话。省下来的就是实打实的内存。所以struct page是一个高度压缩的结构体。它的设计哲学就是在有限的存储空间里塞下尽可能多的管理信息。毕竟要管一百多万个页每一个字节都得省着花。2.2 struct page中的几个关键成员2.2.1 flags记录物理页的状态flags是页的状态记录本。页脏不脏、有没有被锁在内存里、是不是正在写回磁盘全都归它管。它每一位单独表示一种状态一个32位的flags至少能同时表达32种不同的状态。这些标志都定义在linux/page-flags.h里。其中有些比特位分量极重比如PG_locked用来标记页是否被锁定PG_uptodate表示页的数据已经从块设备读取而且没出岔子。内核里这些标志长这样#define PG_locked 0 /* Page is locked. Dont touch. */ #define PG_error 1 #define PG_referenced 2 #define PG_uptodate 3 #define PG_dirty 4 #define PG_lru 5 #define PG_active 6 #define PG_slab 7 /* slab debug (Suparna wants this) */ #define PG_checked 8 /* kill me in 2.5.early. */ #define PG_arch_1 9 #define PG_reserved 10 #define PG_private 11 /* Has something at -private */ #define PG_writeback 12 /* Page is under writeback */ #define PG_nosave 13 /* Used for system suspend/resume */ #define PG_compound 14 /* Part of a compound page */ #define PG_swapcache 15 /* Swap page: swp_entry_t in private */ #define PG_mappedtodisk 16 /* Has blocks allocated on-disk */ #define PG_reclaim 17 /* To be reclaimed asap */ #define PG_nosave_free 18 /* Free, should not be written */ #define PG_buddy 19 /* Page is free, on buddy lists */挑几个最要紧的说PG_locked页被锁了别乱碰。通常是在做磁盘I/O的时候。PG_active/PG_referenced内存回收的LRU算法在用表示这个页活不活跃、最近有没有被访问过。PG_dirty脏页标记说明页的内容已经被改过等着写回磁盘。PG_lru页是不是挂在内存回收的LRU链表上。正好对应struct page里那个lru结构。2.2.2 _mapcount一个物理页被映射了多少次_mapcount这个字段记的是页表中有多少项正指向这个物理页说白了就是这一页被引用了多少次。当它的值变成-1就说明内核眼下没有任何引用指向它。既然没人用那在新的内存分配里它就可以被拿去重新使用了。2.2.3 virtual记录页对应的虚拟地址virtual这个字段记录的是页的虚拟地址。通常来说它就是这一页在虚拟内存里对应的地址。但有一类内存也就是所谓的高端内存并不会永久映射到内核地址空间上。碰到这种情况virtual的值就是NULL。等真正需要的时候再临时把这一页动态映射进来。总结一下我们申请物理内存到底在干什么说白了就三件事查数组、改page、把内核数据结构的对应关系建立起来。2.3 一个物理页到底占用多少内存注意struct page是跟物理页绑定的跟虚拟页没关系。而且系统里每一个物理页都得配这么一个结构体。那这么多结构体堆在一起到底要吃掉多少内存我们来算笔账。假设一个struct page占40个字节一般就在32到40字节之间晃悠。再假设物理页大小是4KB系统有4GB物理内存。那么总页数就是1048576个也就是一百多万个页。一百多万个struct page每个40字节加起来也就40MB左右。拿4GB总内存一比这点开销连个零头都算不上。所以说管理这么多物理页代价其实并不大。但这里有个关键角色页的大小。它直接决定了内存利用率和系统开销。页太大页内就会剩下大片用不上的空间这就是页内碎片白白浪费。页太小呢页内碎片是小了可页的数量嗖嗖往上涨页表跟着变长占的内存反而更多而且系统还得频繁做地址转换开销一层层叠上去也不划算。所以页的大小得取个中间值。通常落在512B到8KB之间而Windows和Linux的页框都定在4KB。这个数不是拍脑袋来的是权衡之后的结果。2.4 struct page如何关联到物理地址这里有个问题值得琢磨既然struct page代表一个物理页为什么它内部压根不存物理地址答案藏在数组的下标里。内核把所有物理页的管理统一转化成了对一个 struct page mem[...] 数组的操作。每一个struct page在这个数组里都有一个固定的下标。这个下标不是随便编的它和物理页的排列顺序一一对应。于是只要拿到下标用下标×4KB就能直接算出这个物理页的起始物理地址。再叠加上页内偏移具体的物理地址就到手了。既然物理地址能靠下标和固定页大小“天然算出来”那还费劲在结构体里再存一份干嘛纯属多此一举。省下这个字段结构体更瘦内存更省。一个数组下标顶一个物理地址字段用这笔买卖划算。2.5 struct page还有哪些维护方式前面说的全局page数组只是最基础的一种存法但不是唯一。这些struct page还能被塞进哈希表挂到radix tree上或者串进LRU链表里。老登啊radix tree是个什么玩意儿它的中文名叫“基数树”是一种树形数据结构。每个文件在内核里都对应一个address_space结构体里面挂着一棵基数树struct radix_tree_root page_tree专门用来管这个文件映射到内存里的所有物理页也就是Page Cache。基数树的节点radix_tree_node里有一个槽位数组void *slots[...]里面的指针直接指向内存页的描述符struct page。基数树是一种基于偏移量文件内的逻辑页索引比如 pgoff_t构建的多叉树。内核拿着文件的页偏移量按二进制位一层层往下摸最后在叶子节点的slots里找到对应的struct page。三、页表——虚拟地址如何找到物理地址3.1 页表中到底有多少个页表项页表里的每一个表项都指向一个物理页的起始地址。32位系统里虚拟内存的最大空间是4GB。这是每一个用户程序都拥有的虚拟内存空间。既然要让这4GB虚拟内存全部可用页表就得能把这4GB空间完整地表示出来。那得多少个表项才够4GB ÷ 4KB算下来就是1048576个表项。如下图所示3.2 虚拟地址与物理地址如何建立映射虚拟内存看着像是被虚线切成了一个个小单元但那只是个视觉上的示意不是真的把它切开了。虚拟内存本身依然是连续的一整条。那些虚线仅仅是在标记它与页表中每一个表项的对应关系以及最终会落到哪个同样大小的物理页上。页表里的物理地址跟物理内存之间是随机映射的哪里有空就指向哪里。所以最终用到的物理内存是离散的但和虚拟内存对应的线性地址始终是连续的。处理器在取数据、取指令的时候用的都是线性地址。只要它是连续的就没问题。至于背后到底落在哪几个物理页上页表会替它找到答案。3.3 页表本身为什么也要占用内存假设在32位系统里地址长度是4个字节那页表里每一个表项就占4个字节。整个页表算下来1048576 × 4正好4MB。4MB是什么概念换算成物理页就是1024个页框。也就是说光是这张映射表自己就得占掉1024个物理页。问题来了。我们当初为什么用页表不就是为了让进程不必连续地存放在物理内存里可以东一页、西一页地散着放。结果呢页表自己反倒要求1024个连续的页框。绕了一圈好像又回到了原点跟最初的目标有点背道而驰。再往深想一层。根据局部性原理进程在一段时间里通常只需要访问那么几个页就能正常跑下去。既然常用的就那么几页又何必让所有的物理页都常驻内存全塞进去既浪费也没必要。3.4 页表太大怎么办对付页表太占地方这件事最漂亮的招数就是把页表本身再分一次页。多级页表的思想就这么冒出来了。思路不复杂把原来那张单一的大页表拆成 1024 张小映射表。每张表里还是1024个表项1024 × 1024照样能把4GB的物理内存空间罩住。这里每一张小表才是真正意义上的页表。所以一共1024张页表。一张页表自身占4KB1024张加起来还是4MB。有人可能要撇嘴了这跟之前有啥区别总量不是一样吗别急账不能这么算。一个应用程序压根不可能把4GB空间全用满。大多数时候几十张页表就够打发了。举个例子一个用户程序的代码段、数据段、栈段加起来也就10MB左右那用3张页表就绰绰有余了。算一下这笔账每个页表项指向一个4KB的物理页一张页表里1024个页表项能覆盖4MB物理内存10MB的程序向上对齐到4MB的倍数就是12MB三张页表正好装下。四、多级页表与页目录结构4.1 什么是页目录结构到这一步1024个页表每个页表里的表项都指向一个页框。可问题又来了这1024个页表谁来管答案是再找一张表来管它们。这张管理页表的表就叫页目录表。于是二级页表的格局就此形成。具体怎么套所有页表的物理地址由页目录表项来指向。而页目录自己的物理地址则由CR3寄存器来指向。教材里的说法更严谨一点叫“指向当前硬件上下文”。为什么这么说因为页目录是跟着进程走的。进程一换CR3指向的页目录也跟着换。CR3里存的永远是当前正在执行的那个任务的页目录地址。所以操作系统加载用户程序的时候要分配的东西不止一样程序内容本身要占物理内存用来保存程序的页目录和页表同样也得占物理内存。一层套一层账得算清楚。4.2 两级页表如何完成地址转换4.2.1 两级页表的地址转换原理拿一个逻辑地址来走一遍。假设这个地址是0000000000 0000000001 111111111111在32位处理器里页大小是4KB。这意味着虚拟地址的低12位天然就是页内偏移剩下的高20位交给页表再一分为二每级各占10个 bit10 10。转换过程分四步CR3寄存器读出页目录的起始地址再根据一级页号去查页目录表找到下一级页表在物理内存里存放的位置。根据二级页号查表找到最终想要访问的那个内存块号。把页内偏移量拼上去物理地址就出来了。这里有个细节值得留意一个物理页的地址一定是4KB对齐的也就是说它的最后12位全是0。既然低12位注定是零那页表项里就只用记录物理页地址的高20位就够了。省下来的位还能拿去做别的标记。每一比特都物尽其用。4.2.2 MMU如何完成地址转换上面这套流程其实就是MMU在幕后干的活。MMU直接集成在CPU里是一块硬件电路速度极快。它接过虚拟地址再拿出 CR3 里保存的页目录地址照着刚才那套规则一路推算最终把物理地址交出来。不过地址转换只是MMU承接的业务之一。它的主场是内存管理换算地址不过是其中一项日常工作。这里还有一个绕不开的问题MMU得先查两次页表把物理地址确定下来等权限之类的检查也过了它才把这个物理地址送上总线。内存收到地址后才开始读取对应数据并返回。也就是说一次完整访问背后至少藏着两次页表检索再加一次真正的内存读写。如果页表从两级变成N级呢那就成了N次检索外加1次读写。页表层级越多查询步骤就越长CPU要等的时间也越久效率自然往下掉。多级页表省了空间却也拿时间做了交换这笔账操作系统算得比谁都清楚。4.2.3 快表TLB多级页表如何兼顾效率先回头总结一下单级页表对连续内存的要求太高于是我们引入了多级页表。可多级页表是把双刃剑它降低了对连续存储的要求也省下了存储空间但代价是查询效率被拉低了。那有没有两全其美的办法计算机科学里有一句老话所有问题都可以通过加一个中间层来解决。于是MMU掏出了一件新武器江湖人称“快表”的TLBTranslation Lookaside Buffer学名“转译后备缓冲区”。说白了它就是一块缓存。当CPU把一个新虚拟地址递给MMU时MMU先扭头问TLB“这条映射你那儿有吗”如果有直接拿物理地址发上总线交给内存齐活。可TLB容量有限难免有Cache Miss的时候。这时候MMU还有保底的老伙计页表。在页表里翻到之后MMU一边把地址送上总线读写内存一边顺手把这条映射关系塞给TLB让它记下来刷新缓存。下次再来同一个地址就不用费劲查页表了。TLB 里到底存了什么TLB本质上就是装在MMU内部的一块超高速缓存用的是SRAM速度快到能跟CPU并肩作战。它存的东西非常纯粹一张高频使用的“虚拟地址 → 物理地址”映射对照表本质上就是页表项的副本。里面每条记录基本就两样东西Key键虚拟页号VPNValue值对应的物理页框号PFN再加上几个控制位能不能读写、在不在内存里等等。一个键一个值外加几个标记位。麻雀虽小五脏俱全。每次地址转换MMU先拿VPN去TLB里对一眼命中就直达未命中再去页表里翻。一快一慢两条路效率就是这么抠出来的。4.3 再深入理解页目录结构4.3.1 聚集效应与页表访问虚拟地址的低12位负责区分页内偏移高20位则负责区分这是哪一页。高20位相同的地址注定落在同一个页里然后再靠低12位取出具体偏移。这就带来一个很有意思的现象聚集效应。你访问了某个地址接下来很可能还会访问它附近的地址因为它们大概率还在同一页里。于是每申请一个页就相当于一次性把周边的一小片空间都准备好了。效率就是这么被顺手优化出来的。4.3.2 页目录结构中的其他细节细节 1物理内存分配的流程申请内存 → 查找数组 → 找到没被使用的page → 拿到page的下标 → 换算成物理页框地址。一条链走下来内存就到手了。细节 2页表重建的场景写时拷贝、缺页中断、内存申请这些操作背后往往都需要重新建立页表和映射关系。页表不是一劳永逸的它会随着进程的运行不断地被修补和更新。细节 3进程的映射体系一个进程靠一张页目录加上n张页表撑起整套映射体系。虚拟地址是索引物理页框是目标。物理地址怎么算虚拟地址的低12位 页框地址 物理地址。简单直接一步到位。细节 4为什么偏偏是低12位因为页框大小是4KB页内字节偏移的范围就是[0, 4095]换算成二进制正好是2的12次方。所以低12位天然就该拿来做页内偏移量。ELF文件、代码段在内存里都落在同一个4KB页框里而且是有序排列的。说到底虚拟地址里总有一位要充当页表的下标而页表里存的内容正是页框的物理地址。4.3.3 能否从物理地址反推出虚拟地址假设我们手里攥着一个32位的物理地址00000000000000011111111111110000把它按前20位、后12位一刀切开物理页框号00000000000000011111页内偏移111111110000翻译过来这个物理地址落在第31号物理页框里页内偏移是第4080个字节。现在我们要反着推回虚拟地址。操作系统得这么干第一步找二级页号。看看这个物理页框号落在哪张页表里又是那张表的第几项第几行。把这个行号转成10位二进制它就是虚拟地址的中间段二级页号。对应图里它在页表中排第1022行转成二进制就是1111111110。第二步找一级页号。再看这张页表的基地址在左边的页目录表里排第几项。同样行号转成10位二进制这就是虚拟地址的最高段一级页号。对应图里该页表地址在页目录表第0行转成二进制就是0000000000。最后一步拼装。把一级页号10位 二级页号10位 页内偏移12位顺着顺序排成一排你要的虚拟地址就出来了。五、缺页异常访问一个页面时会发生什么设想一下CPU把一个虚拟地址递给MMUMMU先翻TLB没有再查页表还是没有对应的物理页。怎么办这就是缺页异常Page Fault。它本质上是一个由硬件中断触发、但可以靠软件逻辑纠正的错误。具体来说如果目标内存页在物理内存里压根没有对应的物理页或者有但权限不够CPU 就拿不到数据。拿不到数据CPU只能报告一个缺页错误。CPU没有数据就没法计算这一“罢工”用户进程就跟着遭殃进程从用户态切到内核态把烂摊子交给内核的Page Fault Handler处理。Page Fault Handler接手后会根据缺页的类型分门别类地处理Hard Page Fault硬缺页/主要缺页错误物理内存里没有对应的物理页CPU必须打开磁盘设备把数据读进物理内存再让MMU建立虚拟地址和物理地址的映射。动态库的加载走的就是这条路。Soft Page Fault软缺页/次要缺页错误物理内存里其实有对应的物理页只不过可能是其他进程调进来的当前进程不知道而已。这时候MMU只需要把映射建好就行不用去磁盘折腾。多进程共享内存区域时这种缺页很常见。Invalid Page Fault无效缺页错误进程访问的内存地址越界了或者对空指针解引用。内核一查直接报segmentation fault进程当场挂掉。最后来一次大总结执行流看到的资源本质是什么在合法的情况下你拥有多少虚拟地址虚拟地址就是资源的代表。虚拟地址空间里的 mm_struct 加 vm_area_struct本质是什么是资源的统计数据和整体数据。页表是什么是一张虚拟到物理的地址地图。资源划分本质就是地址空间的划分。资源共享本质就是虚拟地址的共享。从物理内存碎片到分页到页表、页目录、多级页表、MMU、TLB再到缺页异常——这一整条链路走下来你会发现操作系统对内存的管理说到底就是一场围绕“虚拟地址”展开的资源调度术。虚拟地址就是资源的化身。六、内存申请相关的几个问题6.1 mmap如何申请虚拟内存mmap走的是另一条路它是基于文件的空间申请方案把文件内容直接映射进虚拟地址空间。而new和malloc底层用到的brk其实干的事很朴素修改mm_struct里标识堆顶位置的指针值把堆空间撑大或缩小。它只动虚拟地址的边界并不急着去申请物理内存。真正把物理页交出来的是你第一次访问那块虚拟地址的时候缺页中断一触发内核才不慌不忙地补上一页。这种“延迟申请”的策略好处很实在原本划给你的内存在你真正用之前还可以先借给别人用。内存的利用率就这么被变相拉高了。6.2 new、malloc与brk之间是什么关系这三者经常被放在一起问但它们根本不在同一个层次上。从上到下是一条清晰的调用链newC 运算符 ↓ operator newC 运行时函数 ↓ mallocC 标准库分配器 ↓ brk / mmap系统调用 ↓ 内核真正分配虚拟地址物理页延迟到缺页中断再给new是C的运算符。它做两件事先调用operator new分配原始内存再在这块内存上调用构造函数。分配内存这一步它自己不干活甩给下层。operator new是C运行时提供的函数。它的默认实现通常就是直接调malloc。你也可以重载它换一套分配策略。malloc是C标准库的分配器。它不直接向内核伸手要内存而是先在自己维护的堆空闲链表、内存池里找找看。有小块空闲的直接给你没有合适的才通过brk或mmap向内核批发一大块虚拟地址然后切成小块慢慢发。brk是系统调用。它负责调整堆顶指针把堆空间撑大或缩小。malloc 只有在现有堆空间不够用时才会调 brk 去要更多虚拟地址。mmap也是系统调用。当malloc申请的内存足够大一般超过某个阈值比如128KB它就不走brk了改用mmap单独映射一块内存用完直接munmap还回去避免堆里留下难以回收的大空洞。所以三者的关系一句话就能串起来new管对象malloc管内存块brk/mmap 管虚拟地址的批发内核管物理页的最终兑现。一层套一层各司其职。你写的new不过是这条链条最顶端的那个按钮罢了。6.3 内存越界了就一定会触发错误吗先说结论不一定。有些越界操作系统压根就不知道。操作系统在处理中断或异常时会做两道检查页号合法性检查看看触发事件的虚拟地址页号合不合法。页号合法但页面不在内存里那是缺页中断。页号本身就非法那才叫越界访问。内存映射检查再看看这个虚拟地址是不是在当前进程的内存映射范围内。在范围内但页面不在内存里还是缺页中断。压根不在映射范围内那才是越界访问。所以结论很清晰越界了但越界后那片内存依然合法就不会崩越界后那片内存不合法才会崩。来看一个越界却不崩溃的经典例子死循环。int i; int array[10];因为int i声明在int array[10]前面i会被放在比数组更高地址的栈空间里而且紧紧挨着数组的末尾array[9]。数组访问array[i]本质上就是拿基地址加偏移量算出来的。当i涨到10的时候这个地址恰好就指到了变量i自己占的那4个字节上。于是执行array[10] 0的时候CPU把0写进了这个算出来的地址把变量i原来的值10强行改写成了0。而循环条件又是靠i来控制的i一归零循环条件重新成立于是死循环诞生了。越界了吗越了。崩溃了吗没有。因为那个越界后的地址依然落在进程合法的栈空间里。操作系统一看页号合法映射也在合法访问放行。如果这个系列对你有帮助别忘了点个赞、点个收藏、点个关注。你的每一次反馈都是我继续硬核输出的最大动力。我们下篇见。
企业数字化 ERP 产品动态
相关推荐
从零设计AI加速器:矩阵乘法原理与Verilog实现 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 13:08:17
Rust+Tauri轻量数据库工具DBX:80+数据库统一协议支持 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 13:08:17
YOLOv11跨模态融合:红外与可见光双传感器目标追踪 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 13:08:17
深入解析 lann/builder:用 Go 编写不可变、可复用的流式 Builder DSL 人工智能AI AgentAgent 沙箱云原生容器运行时零信任 【免费下载链接】substrate Agent Substrate: the core system 项目地址: https://gitcode.com/GitHub_Trending/substrate7/substrate 点击查看 免费下载 Builder 是 Go 语言中一套面向“流式(fluen… · 2026/9/24 13:36:08
烘焙后城市场景满是黑斑?用6步检查 Lightmap UV 与光照接缝 城市场景完成光照烘焙后,如果出现整面发黑、局部脏斑、模块接缝发亮,先不要急着提高灯光强度。更常见的原因是 Lightmap UV 重叠、UV 岛间距不足、光照贴图分辨率与对象尺寸不匹配,以及薄面、法线或模块边界存在问题。
本文用一个最小场景演… · 2026/9/24 13:36:08
openFrameworks 粒子系统实战:particlesExample 四种交互模式与源码级解析 图形学音视频 【免费下载链接】openFrameworks openFrameworks is a community-developed cross platform toolkit for creative coding in C. 项目地址: https://gitcode.com/gh_mirrors/op/openFrameworks 点击查看 免费下载 本文以 openFrameworks 官方示例 exa… · 2026/9/24 13:36:01
models 仓库 AlexNet ONNX 模型全解析:从模型清单、预处理到 int8 量化实战 人工智能大模型计算机视觉NLP模型评测 【免费下载链接】models A collection of pre-trained, state-of-the-art models in the ONNX format 项目地址: https://gitcode.com/gh_mirrors/model/models 点击查看 免费下载 AlexNet 是 2012 年 ImageNet 大规模视觉识… · 2026/9/24 13:36:01
shadcn-vue 在 Vite 项目中的安装与配置指南(Tailwind CSS v4 版) shadcn-vue 在 Vite 项目中的安装与配置指南(Tailwind CSS v4 版) 【免费下载链接】shadcn-vue Vue port of shadcn-ui 项目地址: https://gitcode.com/gh_mirrors/sh/shadcn-vue
本篇指南以 shadcn-vue 官方 Vite 安装文档(deprecate… · 2026/9/24 13:36:01
Ceph 三大存储接口之 RGW 对象存储深度梳理 Ceph 对象存储基于 Ceph RADOS Gateway(RGW),Ceph 实现兼容 S3、Swift 协议的对象存储,无需修改底层 RADOS 集群,对外提供 HTTP/HTTPS 对象访问能力。一、什么是 Ceph 对象网关 RGW
RGW(RADOS Gateway&… · 2026/9/24 13:35:55
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程 简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13
1D-CNN时间序列建模实战:从Conv1d原理到工业落地 简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26
柔软的L:汉语语流中被忽视的舌肌张力控制 1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44