资讯动态

Linux内存管理三层次:从虚拟内存到物理页分配

发布时间:2026/9/13 3:54:05 来源:尧图企业网站定制
搞懂Linux内存管理最怕的就是把它当成一个平面去看。很多人学了free、top、ps这些命令看到内存占用率高了就紧张看到Swap用了就慌但内存管理系统本质上是三个层次叠在一起协同工作的用户空间的虚拟内存、内核地址空间、物理内存的组织与分配。把这三个层次分开看你会发现很多疑难杂症其实特别简单——比如为什么malloc之后内存没涨、为什么一个进程占了20G虚拟内存但物理内存才用了200M、为什么删了文件之后内存还是没释放。这篇文章把这三层从用户态讲到内核态再落到物理页结合我在实际项目里排查内存问题时的真实场景尽量说人话遇到关键机制会解释底层原理文末还有几个高频面试题的解法和内存排查经验希望对正在啃Linux的开发者、运维还有准备面试的朋友有点帮助。1. 第一层用户空间那块看似独立的虚拟内存1.1 每个进程都活在自己的平行世界里先抛一个很反直觉的结论在Linux里一个进程真正能直接操作的内存并不是物理内存条上的地址而是一个虚拟地址空间。每个进程都有一套完整的虚拟地址互不干扰。32位下这个空间是4GB64位下则是128TB用户空间部分这就是为什么你拿一个8GB内存的机器去跑几十个进程每个进程还能各自认为自己握有几TB的内存。很多初学者会问虚拟内存是不是假的不是假它是真实存在的抽象层。CPU发出的地址是虚拟地址由MMU内存管理单元通过页表翻译成物理地址翻译失败就抛缺页异常。这个机制带来的核心收益有三个进程隔离A进程不能直接访问B进程的地址空间安全性大幅提升。按需分配malloc申请1GB并不是立刻分配1GB物理页而是记账记了1GB虚拟地址真正写入的时候才分配物理页。共享能力动态库、内核代码段可以映射进多个进程的地址空间物理内存只保留一份。这个理解一旦到位后面你看到进程VSZ虚拟内存大小飙到几十G就不会焦虑了它不一定代表物理内存吃紧。1.2 进程地址空间户型图一个典型的用户空间布局x86_64从低地址到高地址大致是这样的区域起始方向说明代码段text低地址可执行指令只读数据段data/bss低地址已初始化/未初始化的全局变量堆heap向上增长brk分配的区域malloc小对象多落这里内存映射区mmap area向下增长动态库、匿名映射、malloc大对象落这里栈stack向下增长局部变量、函数调用帧一般8MB上限这里面堆和内存映射区是相向生长的中间留一大片空闲区系统通过mmap_min_addr之类的参数防止非法访问。每次分配内存的路径有两条brk把堆顶往上推适合分配小块、频繁申请释放的场景。mmap在映射区找一块空闲空间映射适合大块分配glibc默认超过128KB走这条路。实际分配的时候glibc的malloc会维护一堆缓存池小的对象走brk拿大的对象走mmap拿避免频繁系统调用。你写的C/C代码里new、delete、malloc、free底层都会汇聚到brk或mmap这两个系统调用上。顺带一提Qt的内存管理之所以和传统C体现不同是因为Qt用父子对象树来做生命周期管理但它在底层依然依赖malloc这一套只是包了一层策略。1.3 用/proc/pid/maps看虚拟内存排查问题时我经常先去看进程的真实内存映射cat /proc/12345/maps | head -20 cat /proc/12345/smaps | grep -A 10 ^[0-9a-f].*r-xp | head -30maps里每一行代表一段虚拟地址区间smaps里则细化到每段的RSS、PSS、Swap等指标。RSS是当前真正占了多少物理页PSS是按共享比例摊分后的物理内存。这里经常有一个误解你写代码malloc了500MB但只是初始化了前10MBRSS就只在10MB附近波动500MB会被算进VSZ不会算进实际物理占用。这也是为什么top里VSZ大不等于内存泄漏。2. 第二层内核空间那片受管控的高地址区域2.1 用户态到内核态的地界x86_64下地址空间的划分大概是这样0x0000000000000000到0x00007fffffffffff是用户空间从0xffff800000000000开始是内核空间。用户进程用到的每一条地址翻译不过去就触发缺页而内核可以访问用户空间的地址只是需要借助copy_from_user/copy_to_user这类接口原因包括安全校验、SMAP隔离等硬件保护。内核空间内部的布局也不是一锅粥它被划分为直接映射区线性映射区从PAGE_OFFSET开始把物理内存按顺序映射到这个区域virt_to_phys和phys_to_virt就是简单的加减偏移。这个区域的好处是CPU访问效率高坏处是必须要求物理连续。vmalloc区在这里可以分配虚拟地址连续但物理地址不连续的内存适合大块内核缓冲区。模块区与fixmap区加载内核模块、固定映射地址的特殊区段。CPU entry area每个CPU自己的一份入口栈、中断栈区域。2.2 kmalloc、vmalloc、kvmalloc怎么选驱动开发里有个很经典的困惑内核态分配内存到底该用kmalloc还是vmalloc函数物理连续性虚拟连续性性能适用场景kmalloc连续连续高小块、频繁分配分配后访问频繁vmalloc不连续连续低需要改页表大块且不常访问的缓冲区kvmalloc先试kmalloc失败退vmalloc视情况中不确定大小但能接受两种行为kmalloc底层走的是slab/slub分配器从预分配好的对象桶里取内存vmalloc则是一页一页地从物理内存拿然后在页表上把每页串起来。性能上kmalloc明显优于vmalloc因为vmalloc每次分配都要操作页表还可能导致TLB刷新。所以常规驱动里几百KB以下优先kmalloc超过一两个连续物理页实在申请不到才考虑vmalloc。这里有一个非常容易出现的内核内存问题kmalloc申请不到大块连续物理内存。物理内存有很多剩余但碎片化严重凑不出连续的128KBkmalloc就会失败。这种情况在长时间运行的嵌入式设备上尤其常见。解决办法是尽量在系统刚启动、内存还很整齐时做预留或者使用CMA连续内存分配器机制。2.3 slab/slub内核里的小对象自动贩卖机为什么要有slab因为内核到处都是相同大小的小对象——task_struct、inode、dentry每次创建销毁都从伙伴系统分配释放页开销高得吓人。slab/slub的做法是一次性从伙伴系统拿几页切成统一大小的小块维护空闲链表分配时取一块释放时还回去。默认的kmalloc-8、kmalloc-16、kmalloc-192、kmalloc-4k这一系列桶就是按对象大小划分的。排查内核对象异常增长时我会用slabtop和/proc/slabinfoslabtop -s c cat /proc/slabinfo | head -30比如之前遇到一个容器平台问题一个业务进程频繁创建socket连接又异常断开每次都会创建新的sock_inode_cache对象因为异常路径没有正常释放slab里的对象数持续上涨。看/proc/slabinfo里sock_inode_cache的active_objs一直在涨基本就能锁定是fd泄漏或者socket异常处理路径的引用计数问题比傻傻抓日志高效很多。3. 第三层物理内存的家族族谱node、zone与伙伴系统3.1 物理内存不是一整块而是分家族的现代服务器动辄几十个核、几十G内存物理内存也不是一块板子到底。Linux把物理内存划分成node、zone、page三个层级。node对应一个NUMA节点。每个CPU访问自己node的内存快访问远端node的内存慢。你可以用numactl --hardware看机器上有几个node。zone每个node内部继续画区x86_64上常见的有DMA区低16MB老设备DMA用、DMA32区低4GB、Normal区普通内存、Movable区专门给可迁移的页面。32位时代的HighMem在64位下基本退场但嵌入式32位内核依然会遇到。page物理内存的最小管理单位默认4KB。每个物理页对应一个struct page结构内核通过mem_map数组管理所有页。管理物理页的核心算法叫伙伴系统Buddy System。它的思想特别朴素把空闲页按2的幂次分成不同大小的链表order0是1页4KBorder1是2页8KB……一直到order104MB。分配时从小到大找够用的块没有就拆大块释放时看隔壁小伙伴是否空闲空闲则合并成更大块。举个例子驱动调kmalloc想分配64KB。64KB就是16页order4。分配器先去看order4链表中是否有空闲块如果没有去order5链表借一块32页的块劈成两半一半返回给你另一半挂回order4链表。释放时类似找不到buddy就一直挂着找到了就往上合并。这个机制保障了Linux分配连续物理内存的能力但也会因为长时间分配释放不均衡导致内存碎片。3.2 GFP标志告诉内核我可以等多久内核内存分配API的第二个参数——gfp_mask常常被新手忽略但它决定了分配器的行为和成败。几个最常用的标志含义典型场景GFP_KERNEL普通内核分配可能睡眠可能触发回收进程上下文GFP_ATOMIC原子分配绝不放睡内存不够就失败中断上下文、自旋锁内__GFP_DMA必须在DMA zone分配DMA描述符__GFP_HIGH允许使用紧急预留内存内存回收路径调用kmalloc时选错标志后果很典型在中断上下文用GFP_KERNEL会出现休眠导致的oops或者死锁。在普通进程上下文用GFP_ATOMIC虽然能跑但莫名地分配失败概率变高因为原子分配不能触发页回收内存不足时失败率陡增。我的习惯是能明确上下文就选匹配标志不确定要不要休眠就选GFP_ATOMIC并做好失败兜底。3.3 overcommit与OOM系统是怎么赖账的很多时候你看到top里内存已经满了但进程还能继续malloc成功这是因为Linux默认启用了启发式内存过度分配overcommit_memory0。简单说系统允许高估自己的内存能力malloc只做记账并没有做真实承诺。只有overcommit_memory设为2时系统才会严格拒绝超过(swap ram * ratio)的虚拟内存申请。过度承诺必然带来风险——当多进程一起真实写入内存时物理内存不够了内核就得启动OOM Killer挑进程杀掉。选谁看oom_score它综合了进程内存占用、CPU占用、存活时间、oom_score_adj调整值。这里有一个大坑ssh、数据库、监控agent这些你不想杀的进程如果内存吃得多也可能被选中。生产环境我会用systemd或echo命令给关键进程设置oom_score_adj-999尽量排除误杀。4. 三层次之间的传动轴缺页异常、写时复制与回收机制4.1 一次malloc一次读取背后发生什么把三个层次串起来可以讲一条完整的链路。假设一个进程执行char *p malloc(100 * 1024 * 1024); // 申请100MB p[0] a; // 第一次写第一句malloc执行完内核只是把进程的vm_area_structVMA列表里加了一段100MB的区间页表里根本没有对应条目。打开/proc/PID/smaps会看到这段RSS是0VSZ多了100MB。真正发生物理页分配的是第二句p[0]aCPU去翻译p[0]这个地址时页表查不到触发缺页异常。缺页异常根据情况分两种次缺页minor fault物理页已经在page cache里或者只需要从伙伴系统分配一个新页填上页表就能返回。主缺页major fault内容在磁盘上比如mmap读文件需要发起磁盘I/O开销很大。你可以用以下命令观察缺页情况cat /proc/PID/status | grep -E VmRSS|RssAnon|RssShmem|voluntaryvoluntary_ctxt_switches和nonvoluntary_ctxt_switches是任务切换计数缺页统计则要看性能工具perf。按需分配的核心思想是不访问不分配一访问立刻给。所以你的程序如果只malloc而不碰内存物理内存压力几乎没有。4.2 写时复制fork一个进程不等于复制整块内存fork()创建子进程时如果老老实实把父进程的全部物理页复制一份一个大进程fork一次就够系统喝一壶了。Linux的做法是父子进程先共享所有私有页但页表项标记为只读。任何一方尝试写入时触发缺页异常内核这才把物理页复制一份更新写方的页表指向新页恢复写权限。这就是COWCopy-On-Write。COW的代价也很明显fork之后父子进程都会去碰同一片内存时会触发大量缺页性能反而低下。这种情况下更好的做法是改用posix_spawn或者vfork或者确保fork前主动预热写入提前分裂页面。我在自研服务里做过一个优化fork前先用madvise(MADV_WIPEONFORK)标记不需要继承的缓存区减少子进程初始化时的COW压力。4.3 回收与swap知道谁先走、谁后走当系统发现内存紧张会触发回收机制。回收的优先级是page cache干净页直接丢弃脏页面写回磁盘后丢弃。这是为什么文件缓存占内存高时系统一般还能撑住因为可以随时回收。匿名页如果配置了swap会换出到交换分区/交换文件没有swap就只能靠OOM。内核slab/不可回收内存这里最难办一旦内核对象泄漏再怎么回收都收不回来只能重启或定位泄漏源。回收由kswapd内核线程在后台触发在分配路径上被迫直接回收的情况叫direct reclaim。当系统中出现大量direct reclaim计数增长说明kswapd已经来不及回收内存压力非常大。可以看cat /proc/vmstat | grep pgscan_directswap的调优也有讲究。默认的swappiness是60这个值容易让人误判系统明明还有内存却把不常用的匿名页换出到磁盘。服务器场景我一般建议调到10以下桌面端可以稍微高一点。这样能减少不必要的swap I/O同时保证内存压力真正大的时候依然可以换出。5. 用三层次思维排查几个典型内存问题5.1 buff/cache高到底是不是问题很多人一跑free -h看到buff/cache占了十几个G就开始紧张其实这大概率是好事。page cache本来就是Linux用空闲内存来缓存文件数据你需要时它会主动让出来。可以用available列判断total used free shared buff/cache available Mem: 15Gi 4.1Gi 8.2Gi 211Mi 3.2Gi 10Giavailable是估算的在不触发swap的情况下还能给新进程多少内存它已经把可回收的缓存算了进去。如果available还很充足buff/cache高一点完全不用管。不建议动不动手动执行echo 3 /proc/sys/vm/drop_caches你只是把缓存清了看起来内存free多了实际下次访问那些文件又要重新读盘性能更差。只有在你刚做完大文件复制、准备立刻跑一个内存密集型测试时才值得主动清理。5.2 明明还有内存进程却OOM了这类问题经常发生在开启了overcommit_memory2或者cgroup内存限制的容器里。宿主机free还有大量可用但容器cgroup的内存限制已经到了上限进程在容器里被OOM Kill。排查顺序是先看dmesg里有没有Out of memory: Kill process记录确认是哪个限制触发的。看/proc/PID/cgroup确认进程在哪个cgroup。用cat /sys/fs/cgroup/memory.max和memory.current确认cgroup限制和当前用量。容器场景里page cache也计入cgroup的memory.current。如果业务大量读写文件page cache会把memory.current推高即使进程本身不泄漏也会被误杀。解决方案是给容器设置memory.high上限或者在应用层控制好缓存行为。5.3 RSS不降是泄漏还是缓存程序运行一段时间RSS越涨越高不回落第一反应是内存泄漏。但有另一种可能glibc的malloc释放内存后并不会立刻把内存还给操作系统而是留在进程的缓存池里方便后面复用。这就是内存碎片释放不归还的假象。怎么判断真假泄漏看smaps里的Private_Dirty和Swap字段grep -E ^Pss|^Private_Dirty|^Swap /proc/PID/smaps | awk {print $2} | paste - - - | awk {sum $2} END {print sum}如果Private_Dirty持续上涨很可能真泄漏如果是Pss稳定而远端映射多则更可能是缓存行为。还有一种办法是连续跑两次内存快照间隔一段时间观察增长是否线性如果线性增长且没有对应业务增长再去valgrind或ASan查泄漏点。5.4 slab内存居高不下的定位free里buff/cache高一般没问题但如果slab一项占了很多就不是page cache回收能解决的了。用slabtop看是哪个对象占用最高常见有dentry/inode文件系统元数据缓存大量小文件操作后容易出现可以用drop_caches回收一部分。sock_inode_cache/kmalloc-1k大概率是socket或网络缓冲区泄漏。kernfs_node_cachesysfs/cgroupfs相关某种内核子系统反复创建删除节点。我的习惯是先记录/proc/slabinfo两次间隔半小时的差值看哪个对象持续增长再结合业务代码去猜是哪个子系统造成的。定位到对象类型后再使用perf、ftrace跟踪相关调用路径比盲目看代码快得多。5.5 关于WSL删除文件后空间不释放这个坑这个热词看起来和内存管理无关但它其实是一个典型的缓存未释放认知问题。WSL的虚拟磁盘vhdx不会因为你在Linux里删了文件就自动缩小而且文件系统的truncate操作并不会立刻把ext4的块释放到Windows层。这和内存回收在思路上非常相似系统都倾向于保持已分配的资源以备复用。想释放WSL磁盘空间需要执行sudo fstrim -av然后在Windows侧用diskpart或wsl --manage缩小虚拟磁盘。别把它当作内存问题但从资源回收视角看思维路径是一模一样的。6. 面试里最常被问的四个内存题我的答法这几个问题几乎每次面试都会碰到整理一下我的答法实际上就是把三层次思维落到具体问题上的过程。6.1 malloc返回NULL就是内存不足不一定。malloc失败通常是虚拟地址空间不足、overcommit限制或glibc堆管理异常并不代表物理内存耗尽。真正物理内存不足时系统更明显的表现是OOM Kill而不是malloc返回NULL。所以写的代码要同时考虑两种情况malloc失败要处理OOM被kill也要有兜底方案比如关键进程的oom_score_adj设置。6.2 free后内存为什么不降前面说的glibc内存池机制。小块内存释放后不会真正munmap归还系统而是留在进程的heap里。大块内存mmap分配的释放后会立即munmapRSS才会降。如果想让malloc尽快归还内存可以把环境变量MALLOC_TRIM_THRESHOLD_调小或者用malloc_trim(0)但不建议在生产环境频繁调用性能损耗明显。6.3 一个进程到底占多少内存这是个典型的看VSZ还是RSS还是PSS问题。单个进程请用PSS因为它能正确反映共享库的分摊容量规划和OOM分析请用RSS加上swap计数排查泄漏则重点看Private_Dirty和Swap的变化趋势。没有唯一正确的指标只有当前问题最合适的指标。6.4 为什么内存碎片化严重该怎么解决伙伴系统的拆分和合并机制决定了碎片化无法彻底避免。应对手段有尽量复用长期存活的大块内存避免反复申请释放。使用CMA预留一段连续的物理内存给需要大块连续内存的驱动。借助madvise(MADV_MERGEABLE)做内存合并或者把进程做长时间稳定运行测试观察碎片化趋势。7. 最后我把三层次落到排查路径上的习惯把三层彻底想清楚后我处理内存问题的顺序基本固定了先用free -h、top、vmstat 1判断当前是总量不足还是单进程异常。如果是单进程看/proc/pid/status、smaps区分RSS、VSZ、Swap的变化趋势。如果嫌疑在内核对象用slabtop和/proc/slabinfo看是哪个缓存类在涨。如果还定位不到用perf trace跟踪特定进程的page fault和mmap行为定位具体代码路径。这套流程不一定每一步都能直指根因但它能把大海捞针变成按图索骥。毕竟Linux内存管理不是一门靠背命令就能掌握的学科核心是理解那三层抽象是如何隔离、映射、分配和回收的。把这三层次刻在脑子里再去看那些内存监控数据和内核日志你会觉得一切都有章可循。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价