0.内存空间架构
1.用户空间
在 Linux 系统中,应用程序通过 malloc() 申请内存,并通过 free() 释放内存时,底层的内存管理是由 glibc(GNU C Library)中的内存分配器实现的。glibc 的内存分配器负责与操作系统的内核交互,以高效地管理堆内存的分配和释放。
以下是 malloc() 和 free() 在 glibc 中的工作原理及其实现细节:
1.1 glibc 内存分配器概述
glibc 使用了一种高效的用户态内存管理机制,称为 ptmalloc(Pthreads malloc)。ptmalloc 是基于早期的 dlmalloc(Doug Lea's malloc)改进而来,主要目标是支持多线程环境下的高效内存分配。
(1) 主要特点
- 多线程支持: ptmalloc 为每个线程维护独立的堆(arena),减少多线程间的锁争用。
- 内存池化: 小块内存会被分配到内存池中,避免频繁调用系统调用。
- 内存复用: 释放的内存不会立即归还给操作系统,而是保留在内存池中供后续重用。
- 大块内存优化: 大块内存直接使用 mmap() 分配,避免对堆的影响。
其它内存分配器:还由谷歌公司的tcmalloc分配器与freeBSD 的jemalloc
1.2 malloc() 的工作原理
malloc() 是 glibc 提供的用户态函数,用于动态分配内存。其底层实现会根据请求的内存大小选择不同的策略。
(1) 小块内存分配(< 128KB 或其他阈值)
堆扩展:
- 如果堆中有足够的空闲内存,则直接从堆中分配。
- 如果堆中没有足够的空闲内存,则调用 brk() 系统调用来扩展堆。
内存池管理:
- glibc 使用一种称为 bins 的数据结构来管理小块内存。
- 不同大小的内存块会被存储在不同的 bins 中,便于快速查找和分配。
(2) 大块内存分配(>= 128KB 或其他阈值)
匿名映射:
对于大块内存,malloc() 会直接调用 mmap() 系统调用创建一个独立的匿名映射区域。
这样可以避免对堆的影响,并且释放时可以直接调用 munmap() 归还给操作系统。
(3) 底层实现
malloc() 的核心实现位于 glibc 源码中的 malloc.c 文件。
它依赖于内部的数据结构(如 malloc_chunk)来管理内存块。
1.3 free() 的工作原理
free() 是 glibc 提供的用户态函数,用于释放由 malloc() 分配的内存。它的实现会根据内存块的大小和分配方式采取不同的策略。
(1) 小块内存释放
内存回收:
- 释放的小块内存不会立即归还给操作系统,而是被放回到 glibc 的内存池中。
- glibc 会将释放的内存块插入到对应的 bin 中,以便后续重用。
合并空闲块:
- 如果相邻的内存块也是空闲的,glibc 会尝试将它们合并为更大的空闲块,以减少碎片化。
(2) 大块内存释放
解除映射:
- 对于通过 mmap() 分配的大块内存,free() 会直接调用 munmap() 系统调用将其归还给操作系统。
立即释放:
- 大块内存不会保留在内存池中,而是立即释放。
(3) 堆收缩
如果堆中有大量连续的空闲内存,glibc 可能会调用 brk() 系统调用来收缩堆,将未使用的内存归还给操作系统。
不过,这种行为通常受到一定的限制,因为频繁调整堆顶可能会影响性能。
1.4 glibc 内存分配器的核心数据结构
glibc 的内存分配器依赖于一些核心数据结构来管理内存块和空闲列表。
(1) malloc_chunk
- 每个分配的内存块都由一个 malloc_chunk 结构体描述。
- 它包含以下信息:
- 内存块的大小。
- 是否已使用。
- 指向前一个和后一个内存块的指针(用于链表管理)。
struct malloc_chunk {size_t prev_size; // 前一个块的大小(如果前一个块是空闲的)size_t size; // 当前块的大小(包括元数据)struct malloc_chunk *fd; // 指向下一个空闲块struct malloc_chunk *bk; // 指向前一个空闲块
};
(2) bins
glibc 使用多个 bins 来管理不同大小的空闲内存块。
Fast bins:
- 用于管理非常小的内存块(如 16 字节、32 字节等)。
- Fast bins 不会合并相邻的空闲块,以提高分配速度。
Unsorted bin:
- 用于暂存刚刚释放的内存块,稍后再分类到合适的 bin 中。
Small bins 和 Large bins:
- Small bins 用于管理固定大小的内存块。
- Large bins 用于管理较大范围的内存块。
以下是一个简单的示例,展示 malloc() 和 free() 的使用:
#include <stdio.h>
#include <stdlib.h>int main() {// 分配小块内存int *small_mem = (int *)malloc(1024); // 1 KBif (small_mem == NULL) {perror("malloc failed");return -1;}printf("Small memory allocated at: %p\n", small_mem);// 分配大块内存void *large_mem = malloc(1024 * 1024); // 1 MBif (large_mem == NULL) {perror("malloc failed");return -1;}printf("Large memory allocated at: %p\n", large_mem);// 释放内存free(small_mem);free(large_mem);return 0;
}
2.内核空间
2.1 内核空间的基本功能
虚拟内存管理
- 虚拟内存管理负责从进程的虚拟地址空间分配虚拟页。
- sys_brk 用来扩大或收缩堆。
- sys_mmap 用来在内存映射区域分配虚拟页。
- sys_munmap 用来释放虚拟页。
内核使用延迟分配物理内存的策略:
- 当进程请求分配内存时(如通过 brk() 或 mmap()),内核仅分配虚拟页,而不立即分配物理页。
- 只有当进程实际访问这些虚拟页时,才会触发缺页异常(Page Fault),内核才会分配物理页并更新页表。
- 进程第一次访问虚拟页的时候,触发页错误异常。
- 页错误异常处理程序从页分配器申请物理页,在进程的页表中把虚拟页映射到物理页。
页分配器
- 页分配器负责分配物理页,目前使用的页分配器是伙伴分配器。
- 内核空间提供把页划分成小内存块分配的块分配器,提供分配内存的接口 kmalloc() 和释放内存的函数 kfree()。
支持三种分配器:slab 分配器、slub 分配器和 slob 分配器。
在内核初始化的过程中,页分配器还没有准备好,需要使用临时的引导内存分配器分配内存。
2.2内核空间的扩展功能
不连续页分配器
- 提供分配内存的接口 vmalloc 和释放内存的接口 vfree。
- 在Linux内核中,vmalloc函数用于分配不连续的物理内存页面,并将它们映射到虚拟地址空间中的一个连续区域。这与直接使用kmalloc或__get_free_pages等接口不同,后者要求分配的物理页面必须是连续的。
- 在内存碎片化的时候,申请连续物理页的成功率很低。
- 可以申请不连续的物理页,映射到连续的虚拟页,即虚拟地址连续但物理地址不连续。
- 每个处理器内存分配器用来为每个处理器变量分配内存。
连续内存分配器
- 用来给驱动程序预留一段连续的内存。
- 当驱动程序不用的时候,可以给进程使用。
- 当驱动程序需要使用的时候,把进程占用的内存通过回收或迁移的方式让出来,给驱动程序使用。
内存控制组
- 用来控制进程占用的内存资源。
3.硬件层面
内存管理单元 (MMU, Memory Management Unit)
- 处理器包含一个称为内存管理单元 (MMU) 的部件。
- MMU 负责将虚拟地址转换成物理地址。这种转换对于操作系统来说非常重要,因为它允许每个进程拥有自己的独立地址空间,增强了系统的安全性和稳定性。
- 保护机制:通过地址转换过程,MMU 可以实现对内存访问的权限控制。例如,它能够阻止一个进程直接访问另一个进程的内存区域,或者限制对特定内存页的读写权限。
页表缓存 (TLB, Translation Lookaside Buffer)
- 内存管理单元包含一个页表缓存 (TLB) 部件。
- TLB 保存最近使用过的页表映射,避免每次把虚拟地址转换为物理地址时都需要查询内存中的页表。
- 加速地址转换:每次进行虚拟地址到物理地址的转换都需要查询内存中的页表,这会消耗一定的时间。为了提高效率,MMU 使用 TLB 来缓存最近使用过的地址映射。当需要再次访问相同的虚拟地址时,可以直接从 TLB 中获取对应的物理地址,而无需再访问主存中的页表。
- 性能提升:由于 TLB 的存在,大多数情况下地址转换可以非常快速地完成,从而显著提升了系统性能。然而,如果所需的地址映射不在 TLB 中(称为 TLB Miss),则仍然需要访问页表来完成地址转换,并且该映射会被添加到 TLB 中以便将来使用
缓存
- 为了解决处理器的执行速度和内存访问速度不匹配的问题,在处理器和内存之间增加缓存。
- 缓存通常分为一级缓存 (L1 Cache) 和二级缓存 (L2 Cache)。
- 为了支持并行地取指令和取数据,一级缓存分为数据缓存 (Data Cache) 和指令缓存 (Instruction Cache)。