第2章:Linux内存管理基础

做机顶盒开发这些年,我见过太多因为内存问题导致的诡异故障。有时候画面卡顿,有时候直接黑屏重启,查到最后,十有八九是内存管理出了问题。所以这一章,咱们得把Linux内存管理的基础打牢。

2.1 虚拟内存与物理内存

先说说虚拟内存和物理内存的关系。说白了,虚拟内存就是个「中间人」。

每个进程都以为自己独占4GB空间(32位系统),实际上物理内存可能只有512MB。这中间怎么玩的?靠的就是虚拟内存映射。

我举个例子。你在机顶盒上跑一个播放器,它申请了100MB内存。但这100MB不一定是连续的物理内存,甚至可能一部分还在硬盘的swap分区里。进程自己完全不知道,它只看到连续的虚拟地址。

为什么要这么设计?三个好处:

  • 隔离性:进程A崩了不会影响进程B
  • 安全性:进程不能直接访问物理内存
  • 灵活性:可以用比物理内存更大的地址空间

重点:虚拟内存是给进程看的,物理内存是给硬件用的。中间靠页表来翻译。

2.2 页表与MMU

页表是什么?就是一张映射表。虚拟地址到物理地址的翻译手册。

MMU(内存管理单元)是硬件,专门干翻译这活的。每次CPU访问内存,MMU都会查页表,把虚拟地址转成物理地址。

我记得有一次排查机顶盒花屏问题,发现是页表项被破坏了。MMU翻译出错,读到的数据全是乱的。那感觉就像你查字典,字典印错了,查出来的意思当然不对。

页表的结构,我简单说一下:

  • 每个进程有自己的页表
  • 页表项(PTE)包含物理页框号和权限位
  • 权限位控制读写、执行、用户/内核访问

小技巧:排查内存泄漏时,可以看/proc/pid/smaps里的页表信息。我经常用这个来确认某块内存到底有没有被释放。

2.3 malloc/free底层实现原理

很多工程师用malloc/free用得飞起,但底层怎么工作的?其实没那么神秘。

malloc底层有两种实现方式:

  • brk方式:适合小内存分配,通过移动堆顶指针
  • mmap方式:适合大内存分配,直接映射匿名页

glibc里有个阈值,默认128KB。小于这个值走brk,大于等于走mmap。

你想想看,为什么要有这个区分?

brk分配的内存释放后,不会立即还给系统。它留在进程的堆里,下次malloc可以直接复用。而mmap分配的内存,free后会通过munmap立即归还系统。

我遇到过一个问题:机顶盒跑几天后内存越来越小。查了半天,发现是某个模块频繁分配130KB左右的内存,每次都走mmap。free之后内存是释放了,但mmap/munmap的开销太大,导致性能下降。后来我把阈值调大,改成走brk,问题就解决了。

注意:brk方式有个坑——内存碎片。频繁分配释放小块内存,堆会变得支离破碎。我曾经在机顶盒上见过堆碎片导致malloc失败,明明还有几百KB空闲,就是分配不出连续的64KB。

2.4 brk与mmap系统调用

这两个系统调用,是内存分配的底层工具。

brk

  • 改变进程堆的结束地址
  • sbrk(0)可以获取当前堆顶位置
  • 分配和释放都在堆上操作

mmap

  • 在进程地址空间创建映射
  • MAP_ANONYMOUS用于匿名映射(不关联文件)
  • MAP_PRIVATE用于写时拷贝

看个简单的例子:

// brk方式分配内存
void *ptr = sbrk(4096);  // 扩展堆4KB

// mmap方式分配内存
void *ptr = mmap(NULL, 4096, 
                 PROT_READ | PROT_WRITE,
                 MAP_PRIVATE | MAP_ANONYMOUS,
                 -1, 0);

我个人习惯在排查内存泄漏时,用strace抓系统调用。看看进程到底调用了多少次brk和mmap,每次分配多大。这比看代码快多了。

有一次,我发现一个进程mmap调用了上万次,每次只分配4KB。这明显不合理。后来定位到是某个循环里反复调用malloc(1),而glibc内部为了对齐,每次实际分配了至少16字节。虽然单次泄漏不大,但循环一多,内存就爆了。

分配方式 适用场景 释放行为 碎片风险
brk 小内存(<128KB) 不立即归还系统
mmap 大内存(≥128KB) 立即归还系统

核心要点:理解brk和mmap的区别,是排查内存泄漏的基础。你看到内存只增不减,先判断是堆没释放,还是mmap映射没解除。方向对了,问题就解决了一半。

嗯,这一章的内容就这些。下一章咱们聊聊实际的内存泄漏排查工具和方法。到时候我会分享几个真实案例,都是我在机顶盒项目里踩过的坑。