理解了堆、虚拟地址空间和缺页之后,一个很自然的问题就是:
程序里写的
malloc,到底是怎么和操作系统内存管理接上的?
这背后最常见的一组概念就是 malloc、brk 和 mmap。
一、先说结论
最重要的一句话是:
malloc是用户态库函数;brk和mmap是向操作系统申请虚拟地址空间的底层机制。
理解了堆、虚拟地址空间和缺页之后,一个很自然的问题就是:
程序里写的
malloc,到底是怎么和操作系统内存管理接上的?
这背后最常见的一组概念就是 malloc、brk 和 mmap。
最重要的一句话是:
malloc是用户态库函数;brk和mmap是向操作系统申请虚拟地址空间的底层机制。
内存管理不只是“怎么分配页、怎么换页”,还包括一个更关键的问题:
为什么一个进程不能随便访问另一个进程的内存? 既然进程隔离这么强,为什么多个进程又能共享同一块内存?
这背后依赖的就是内存保护与共享机制。
内存保护可以简单理解成:
操作系统要保证一个进程只能按规定方式访问属于自己的那部分内存。
它要防止这些情况:
内存泄漏和内存溢出经常一起被问,但它们不是一回事。
最核心的一句话是:
内存泄漏是“该回收的没回收”,内存溢出是“想申请的申请不到”。
内存泄漏是指:
程序已经不再需要某块内存了,但这块内存没有被正确释放或回收,导致它一直被占用。
它的特点通常是:
这篇不讲新的知识点,主要整理内存管理里最容易混淆的边界问题。
很多时候不是完全不会,而是:
所以这篇的重点不是“扩知识”,而是“压边界”。
很多人会把它们直接画等号,觉得:
TLB miss = 缺页
这是不对的。
堆和栈是面试里非常高频的一题。虽然很多时候会放在语言题里问,但它本质上和操作系统内存管理密切相关。
真正需要抓住的,不是“栈快堆慢”这句结论,而是:
栈和堆分别服务什么场景,它们为什么会采用完全不同的管理方式。
栈主要服务函数调用过程。
函数调用时,系统通常会在栈上创建一个新的栈帧(stack frame),里面保存:
工作集和抖动,是把缺页中断、页面置换和 swap 真正落到系统性能上的关键概念。
这部分主要回答两个问题:
为什么程序不是随机访问全部页,而往往只集中访问一小部分? 为什么内存不够时,系统不是稍微慢一点,而是会突然变得非常卡?
工作集可以理解成:
一个进程在某一段时间内,频繁访问、真正活跃使用的那部分页面集合。
虽然一个进程理论上可能拥有很多页,但在某个短时间窗口里,程序通常只会反复访问其中一小部分。这一小部分页,就是它当前的工作集。
内存管理最基础的一层,就是分配和回收。
程序运行离不开内存,进程、线程、函数调用、对象创建都要占用内存;程序结束或者某块空间不再使用后,又要把这部分空间收回来重新利用。所以操作系统必须解决两个问题:怎么分,怎么收。
内存是有限资源,但系统里会同时运行很多进程。每个进程都要用内存来存放代码、数据、栈、堆以及各种运行时状态。
因此操作系统需要处理:
分页、分段、段页式是操作系统组织和管理内存的三种经典思路。
它们的核心差别,不只在实现形式,更在出发点:分页更偏向系统管理效率,分段更偏向程序逻辑结构,段页式则试图兼顾两者。
分页的核心思想是:
把虚拟地址空间和物理内存都划分成固定大小的块。
其中:
每一页大小固定,比如常见的 4KB。分页后,一个进程的多个页可以离散存放在不同物理页框中。
理解虚拟内存之后,下一步就要回答一个更具体的问题:
程序拿到一个虚拟地址之后,到底是怎么一步步变成物理地址的?
这背后主要依赖分页、页表、MMU 和 TLB 这些机制配合完成。
如果要求一个进程始终使用连续物理内存,会带来外部碎片严重、分配不灵活等问题。
所以现代操作系统通常采用分页机制:
这样,一个进程的内容可以分散放在多个物理页框中,而不要求整块连续物理空间。
理解页表之后,一个很自然的问题就是:
页表本身会不会太大?
这正是多级页表要解决的问题。
页表的作用是记录虚拟页到物理页框的映射关系。理论上,虚拟地址空间里的每个虚拟页都需要对应一个页表项。
所以虚拟页越多,页表就越大。
以 32 位系统为例,如果:
那么虚拟页数就是:
4GB / 4KB = 2^20