Linux下的内存管理:01.从 malloc 到缺页中断——一次完整的内存分配剖析

前言

在之前的文章中,我们讲述了,从源码到进程中的过程,了解了源码是怎么变成进程,linux系统是怎么使用静态库与动态库的。

在这个小节我们就开始一个新的篇章,准备对linux的内存管理下的相关内容进行分析,了解Linux下的内存管理机制。

本篇内容就从最基础的malloc开始,讲述malloc背后的故事。

malloc为什么会慢?

如果 malloc 内部就像我们实现一个环形缓冲区时那么简单,只是移动指针,将需要的内存从一大块内存中取出,那么实际的内存分配不应该是任何性能瓶颈,但是在开发中,我们可能会遇见各种内存分配引发的性能问题:

  • 多线程锁竞争(Contention): 当给程序增加线程的数量时,发现没有性能,反而下降了? 因为可能多个线程在 malloc内部竞争同一把锁。
  • 隐藏的页面错误(Page Faults)): 为什么malloc申请到内存的时候,延迟极低,但是当第一次写入内存的时候,延迟一下子提高了?因为当时申请内存的时候只是一个 "标记",使用的时候才去真实请求。
  • 系统调用开销(Syscall Overhead):为什么有的 malloc 快如闪电,有的却延迟极高? 因为可能触发了昂贵的系统调用。
  • 内存碎片(Fragmentation): 为什么系统中显示我程序的内存占用总是比我自己统计的要高?因为glibc可能没有将你的内存归还给操作系统。
  • 缓存与TLB失效(Cache & TLB Misses): 为什么我的算法在逻辑上应该很快,但是数据跑起来很慢?因为你频繁触发了页面中段,CPU的缓存与TLB完全失效了。

正是内存可能会导致程序出现问题,我们才要去学习和了解malloc内部实现的机制,方便之后在遇到问题的时候,有思路查找和解决问题。

malloc的基本原理

首先需要强调的,也是最重要的一个内容: malloc并不是一个系统调用 (Sysyem Call),他是一个c标准库中提供的 用户态 函数

我们在linux上使用的 malloc通常是glibc提供的,也是工作在用户空间空间管理内存的,就像大家知道的用户态与内核态切换会带来较大的性能损耗, malloc也会尽可能避免和内核打交道

malloc自身的内存管理

glibc实现了一个名叫ptmalloc2的非常精巧的内存管理器,当你调用malloc分配,使用free释放内存的时候,实际上就是是向这个家伙去要和归还内存。

ptmalloc2自己会管理操作系统申请内存和归还的过程,为了实现尽量实现 少和内核打交道的目标,ptmalloc2内部有复杂的机制:

  • Arena: 为了解决多线程锁竞争,ptmalloc2会为不同的线程(核心)创建不同的“Arena”。每个Arena都是一个独立的内存池。线程分配时,会先尝试在自己的Arena中获取内存,这样就不用和其它线程抢锁了。这就是为什么jemalloctcmalloc(Google的)这类现代分配器在高并发下性能更好的原因,它们在“每线程缓存”上做得更彻底。
  • Bins: 在Arena内部,被free掉的内存块不会马上还给操作系统。它们会被根据大小,放进不同的“箱子”(空闲链表,free lists)里。
    • Fast Bins: 用于存放非常小的、刚被释放的内存块。下次你申请同样大小的内存时,它可以“闪电般”地从这里取出,连锁都不用加(使用原子操作)。
    • Small Bins / Large Bins: 存放其它大小的空闲块。

malloc(100) 的典型流程:

  1. 获取当前线程的Arena。
  2. (尝试)锁定这个Arena(如果是多线程)。
  3. 根据100字节的大小,计算出实际需要的块大小(需要加上头部管理信息)。
  4. 快速路径: 尝试从Fast Bins或Small Bins中寻找一个大小合适的空闲块。
  5. 如果找到: 完美!ptmalloc2将这个块从链表上摘下,标记为“已使用”,解锁,然后返回指针给你。这个过程极快,完全在用户态完成,没有内核介入。
  6. 如果没找到: 事情开始变得有趣了。这个时候就必须和内核交流了。

向内核请求内存

ptmalloc2发现自己的Bins里没有合适的内存块时,它必须向内核申请。它有两个主要的工具:brkmmap

  1. brk (sbrk):

    • 这是“传统”的方式。 每一个进程都有一个“堆”(Heap)内存区域。brk系统调用的作用就是移动“堆”的顶部指针(program break)。
    • void* sbrk(intptr_t increment);
    • ptmalloc2会通过brk(SIZE)告诉内核:“请把我的堆内存扩大SIZE个字节”。
    • 优点: 连续的内存,管理简单。
    • 缺点: 极其不灵活。 如果你free了堆中间的一块内存,brk无法“收缩”这部分,除非你free的是最顶部的内存。这会导致严重的内存碎片,内存“只增不减”。
  2. mmap (Memory Map):

    • 这是“现代”的方式。 mmap是一个功能强大无比的系统调用,它可以让内核在进程的 虚拟地址空间 中“凭空”创建一块新的、独立的内存区域。
    • void* mmap(NULL, SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    • 这个调用告诉内核:“请给我一块SIZE大小的、可读可写、私有且匿名的(不关联任何文件)内存”。
    • 优点: 极其灵活。当C++的delete调用free,并且ptmalloc2发现这个mmap出来的区域整个被释放了,它会使用munmap系统调用,将这块内存 完整地归还给操作系统
    • 缺点: 开销比brk略大,且容易产生大量离散的内存区域(VMA)。

ptmalloc2的策略是:

  • 小内存(默认 < 128KB): 优先使用brk来扩展主Arena的Heap。
  • 大内存(默认 >= 128KB): 直接使用 mmap。这就是为什么你malloc一个超大数组时,程序占用的“虚拟内存”会暴增。
  • 非主线程的Arena: 也会使用mmap来创建“线程堆”。

内核开始去规划分配内存

现在我们进入了内核态。无论你调用brk还是mmap,内核 都不会立即分配物理内存(RAM)

这可能是最重要的一个概念:Linux内核是“懒惰”的(Lazy Allocation)

当你调用mmap申请1GB内存时,内核只是在你的进程的“内存地图”(mm_struct)里创建了一个条目,这个条目叫 VMA (Virtual Memory Area)

VMA只是一张契约,它记录着:“这个进程 有权 访问从虚拟地址0xABCD00000xABCDFFFF的内存,权限是‘可读可写’”。

此时,物理RAM一字节都没有被占用。mmap系统调用(或brk)在微秒级就返回了。

页面中断,真正开始分配物理内存

malloc成功返回了一个指针ptr。你的C++代码开始 使用 这块内存:

char* ptr = (char*)malloc(1024 * 1024); // 申请 1MB
*ptr = 'H'; // 重点在这里!第一次写入

当CPU执行*ptr = 'H'时,奇妙的事情发生了:

  1. CPU的 MMU(内存管理单元) 尝试将这个虚拟地址ptr翻译成物理地址。
  2. MMU会去查询 页表(Page Table)
  3. MMU发现,这个虚拟地址对应的页表项(PTE)是空的,或者标记为“不存在”(Present=0)。
  4. 硬件中断! MMU无法完成翻译,它会立刻暂停你的程序,触发一个 #PF 异常,即“页面错误”(Page Fault)
  5. 控制权立刻从“用户态”切换到“内核态”,交给内核的Page Fault处理器(do_page_fault)。

内核的Page Fault处理器开始工作:

  1. 检查“契约”: 内核查看进程的VMA列表。“这个地址ptr是不是合法的?哦,是的,VMA表里记录了它有权访问这里。”(如果没记录,就是非法的,内核会杀死进程,这就是 Segment Fault)。
  2. 分配物理内存: 这是一个合法的“缺页”,只是因为内核偷懒还没分配。内核会立即调用它的“页分配器”(Buddy System,伙伴算法),从空闲的物理RAM中找出一个物理页框(Page Frame)(通常是4KB)。
  3. 清零(可选): 为安全起见(防止泄露其它进程的数据),内核会先把这4KB物理内存清零。
  4. 建立映射: 内核会更新页表,将你访问的 虚拟地址 ptr 所在的页,映射到刚刚分配的 物理页框
  5. 返回用户态: 内核处理完毕,返回用户态。
  6. CPU重新执行失败的指令:*ptr = 'H'
  7. 这一次,MMU通过页表,成功将虚拟地址翻译成物理地址,数据被写入RAM。

这就是为什么你malloc很快,但第一次memset很慢的原因。 你在memset时,在为这块内存的 每一个 4KB页面(Page)支付“Page Fault”的开销。

实验验证

我们在这个小节通过最简单的程序来观察,内存分配和缺页中断的过程

准备源码和可执行程序

准备一个简单的C程序 (test_mem.c):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main() 
{
    printf("My PID is: %d\n", getpid());

    // 1. 一个小的分配
    printf("Allocating small block (100 bytes)...\n");
    void* small_ptr = malloc(100);
    memset(small_ptr, 1, 100);
    printf("Small block allocated at: %p\n", small_ptr);

    // 2. 一个大的分配
    printf("Allocating large block (100 MB)...\n");
    void* large_ptr = malloc(100 * 1024 * 1024);
    printf("Large block allocated (virtually) at: %p\n", large_ptr);

    // 在这里暂停,让我们有机会观察
    printf("Press ENTER to write to large block (trigger Page Faults)...\n");
    getchar();
    
    // 3. 触发 Page Fault
    memset(large_ptr, 2, 100 * 1024 * 1024);
    printf("Large block written.n");
    
    printf("Press ENTER to exit...n");
    getchar();

    free(small_ptr);
    free(large_ptr);
    return 0;
}
# 编译并运行
gcc test_mem.c -o test_mem

strace 追踪系统调用

strace可以捕获程序发起的所有系统调用。

# -f 跟踪子进程, -e trace=memory 限制只看内存相关的系统调用
strace -f -e trace=memory ./test_mem

你会看到(简化后的)输出:

My PID is: 41302
Allocating small block (100 bytes)...
Small block allocated at: 0x5e99d577e6b0
Allocating large block (100 MB)...
mmap(NULL, 104861696, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7efaff1ff000
Large block allocated (virtually) at: 0x7efaff1ff010
Press ENTER to write to large block (trigger Page Faults)...

Large block written.
Press ENTER to exit..

munmap(0x7efaff1ff000, 104861696)       = 0  <-- free大内存时,munmap被调用
+++ exited with 0 +++

结论:

  1. 小的malloc(100)直接获取到了,没有由向内核重新申请。
  2. 大的malloc(100MB)mmap分配。
  3. memset(写入操作)没有触发新的系统调用,但它触发了内核态的 Page Fault

实验二:pmap 查看内存地图

打开另一个终端,在程序等待你按回车时,查看它的内存地图。

# 替换41302为你的PID
pmap -x 41302

按回车写入前,你会看到:

Address           Kbytes     RSS    Dirty Mode  Mapping
......
00005e99d577e000     132       4       4 rw---   [ anon ] <-- 最初malloc100使用的
00007efaff1ff000  102404       4       4 rw---   [ anon ] <-- mmap 的 100MB 区域
...

查看0x5e99d577e6b0和0x7efaff1ff000所在区域的相关内容,注意看 RSS(Resident Set Size),这是 真正 占用的物理内存。
[anon](mmap区域)的RSS是 4K!我们有132k的 虚拟 内存,占用了4k,因为就使用100字节的内存。
[anon](mmap区域)的RSS也是 4K!我们有100MB的 虚拟 内存,但没有占用同等大小的 物理 内存。

按下回车,让memset执行完后,再次运行pmap -x 41302

Address           Kbytes     RSS    Dirty Mode  Mapping
...
00005e99d577e000 132 4 4 rw--- [ anon ] 
00007efaff1ff000 102404 102404 102404 rw--- [ anon ]
...

RSS现在变成了 102404K! 所有的物理内存都在memset时,通过Page Fault被分配了。

从glibc到Linux内核的源码阅读

下面的代码对glibc和内核中的源码进行了高度简化和伪代码化的,但它们精确地反映了核心逻辑和函数调用链

1. 用户态:glibcptmalloc2 (malloc/malloc.c)

malloc(100)开始。它会调用_int_malloc,这是ptmalloc2的心脏。

_int_malloc:在 Bins 中寻找

假设我们调用malloc(100)ptmalloc2会先将其对齐并加上头部信息,比如需要nb = 112字节。

// (在 glibc/malloc/malloc.c 中的 _int_malloc)

static void*
_int_malloc(mstate av, size_t bytes)
{
    size_t nb = request2size(bytes); // nb = 112
    mchunkptr victim; // 这是我们要找的空闲块

    // 1. 尝试 "Fast Bins" (针对极小且刚释放的块)
    if (nb <= get_max_fast()) {
        int idx = fastbin_index(nb); // 计算它属于哪个 fastbin 
        mfastbinptr *fb = &&fastbin (av, idx);
        
        victim = *fb; // 拿到链表头
        if (victim != 0) {
            // 找到了!
            *fb = victim->fd; // 从链表中移除 (fd = forward pointer)
            // ... (一些检查) ...
            void *p = chunk2mem (victim);
	        alloc_perturb (p, bytes);
	        return p;; // 闪电般返回
        }
    }

    // 2. Fast Bins 中没有,尝试 "Small Bins"
    if (in_smallbin_range(nb)) {
        idx = smallbin_index (nb);
        bin = bin_at (av, idx);
        
        victim = *b; // 拿到链表头
        if ((victim = last (bin)) != bin) {
            // 找到了!
            void *p = chunk2mem (victim);
            alloc_perturb (p, bytes);
            return p;
        }
    }

    // 3. Small Bins 也没有,事情开始变慢...
    // 它会尝试合并 "Fast Bins" (malloc_consolidate),
    // 然后去 "Large Bins" 找,
    // 再去 "Unsorted Bin" (一个“垃圾回收”箱) 找...

    // 4. 【关键】所有 Bins 都找遍了,还是没有!
    // 我们必须向操作系统 "进货" 了。
    
    // (在 _int_malloc 的非常后面)
    victim = sysmalloc(nb, av); // <--- 在这里调用 sysmalloc!
    
    if (victim != 0) {
        // sysmalloc 成功从 OS 拿到了内存
        // ... (可能需要从这块新内存中切一小块出来) ...
        void *p = sysmalloc (nb, av);
        if (p != NULL)
            alloc_perturb (p, bytes);
        return p;
    }
}
  • _int_malloc的全部工作就是避免调用 sysmalloc
  • 它优先检查fastbinssmallbinsunsorted_binlargebins
  • 只有当所有“库存”都用完时,它才会调用sysmalloc去“进货”。

sysmalloc是用户态和内核态的“边界”。它决定是扩展堆(brk)还是创建一个新的匿名映射(mmap)。

sbrk(bytes)mmap(...)都是C库对系统调用的封装。现在,我们切换到内核态

2. 内核态:brk 系统调用 (mm/mmap.c)

glibc调用sbrk(SIZE)时,它最终会触发brk系统调用。

// (在 mm/mmap.c)

// SYSCALL_DEFINE1 是一个宏,它定义了一个系统调用
// "brk" 是系统调用名, "unsigned long, brk" 是参数
SYSCALL_DEFINE1(brk, unsigned long, brk_addr)
{
    struct mm_struct *mm = current->mm; // 获取当前进程的内存描述符
    unsigned long newbrk, oldbrk;

    // ... (加锁:mmap_write_lock(mm)) ...

    // 'mm->start_brk' 是堆的起始地址 (程序加载时固定)
    // 'mm->brk' 是堆的 *当前* 结尾 (program break)
    oldbrk = mm->brk; 
    min_brk = mm->start_brk;
    if (brk_addr < min_brk)
        goto out; // 错误:不能把堆缩到起始点之前

    // 检查是否超过了资源限制 (RLIMIT_DATA)
    if (check_data_rlimit(rlimit(RLIMIT_DATA), brk, mm->start_brk,
			      mm->end_data, mm->start_data))
		goto out;

    // 【核心】do_brk_flags 是真正干活的
    // 它只负责处理 VMA (地契),不分配物理内存
    if (do_brk_flags(&vmi, brkvma, oldbrk, newbrk - oldbrk, 0) < 0)
		goto out;

    if (newbrk == oldbrk)
        goto out; // 地址没变,什么也不做

    mm->brk = newbrk; // 更新进程的 "program break" 记录

out:
    // ... (解锁) ...
    return origbrk; // 返回新的 'program break' 地址
}

// (也在 mm/mmap.c)
static int do_brk_flags(struct vma_iterator *vmi, struct vm_area_struct *vma,
		unsigned long addr, unsigned long len, unsigned long flags)
{
    struct mm_struct *mm = current->mm;

    if (vma) {
        // 我们已经有了一个 [heap] VMA,现在只是要扩展它
        
        // ... (检查新旧 VMA 是否可以合并) ...
        
        // 【最关键的一行】
        // 内核只是简单地更新了 VMA 的结束边界!
        // 没有一行业代码是去分配物理内存(RAM)的!
        vma->vm_end = addr + len;
        
    } 
     
     // 这是第一次调用 brk,我们需要创建 [heap] VMA
     vma = vm_area_alloc(mm);
     // ... (设置 vma->vm_start, vma->vm_end, vma->vm_flags) ...
      vm_flags_init(vma, flags);
	  vma->vm_page_prot = vm_get_page_prot(flags);
	  vma_start_write(vma);
	  if (vma_iter_store_gfp(vmi, vma, GFP_KERNEL))
		  goto mas_store_fail;
      .....
}

brk系统调用极快。它所做的只是在内核中修改一个vm_area_struct(地契)的vm_end值。它承诺了这块虚拟地址是合法的,但没有分配一字节的物理RAM

3. 内核态:mmap 系统调用 (mm/mmap.c)

glibc调用mmap(...)时,它触发mmap系统调用(现代内核通常是mmap_pgoff)。

// (在 mm/mmap.c)
// (SYSCALL_DEFINE6 定义了6个参数的系统调用)

SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,
		unsigned long, prot, unsigned long, flags,
		unsigned long, fd, unsigned long, pgoff)
{
	return ksys_mmap_pgoff(addr, len, prot, flags, fd, pgoff);
}

中间经过 ksys_mmap_pgoff->vm_mmap_pgoff->do_mmap
// (也在 mm/mmap.c)
unsigned long do_mmap(struct file *file, unsigned long addr,
                      unsigned long len, unsigned long prot,
                      unsigned long flags, ...)
{
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma;

    // 1. 找到一个空闲的虚拟地址范围
    //    (如果 addr = 0,内核会帮你找)
    addr = get_unmapped_area(file, addr, len, pgoff, flags);
    if (IS_ERR_VALUE(addr))
        return addr; // -ENOMEM 或其他错误

    // 延迟实际物理页分配到 page fault
    addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);

    //.如果用户指定了 MAP_POPULATE,预分配
   if (!IS_ERR_VALUE(addr) &&
    ((vm_flags & VM_LOCKED) ||
     (flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))
    *populate = len;


    // 返回虚拟地址
    return addr;
}

brk一样,mmap也只是在“画饼”。它创建了一个vm_area_struct(地契),将其插入到进程的地址空间,然后就返回了。

4. 内核态:Page Fault(缺页中断) (arch/x86/mm/fault.c)

这是魔法发生的地方。当C++代码执行*ptr = 'H'时,CPU硬件触发了一个#PF中断。

// (在 arch/x86/mm/fault.c)

// 'do_page_fault' 是内核的中断处理函数
// 'regs' 是CPU寄存器
void do_user_addr_fault(struct pt_regs *regs,
			unsigned long error_code,
			unsigned long address)
{
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma;

    //【检查地契】在进程的 VMA 列表 (红黑树) 中查找
    //    这个 'address' 是否属于一个合法的 VMA?
    vma = find_vma(mm, address);

    if (unlikely(!vma)) {
		bad_area(regs, error_code, address);
		return;
	}
	if (likely(vma->vm_start <= address))
		goto good_area;
	if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
		bad_area(regs, error_code, address);
		return;
	}
	if (unlikely(expand_stack(vma, address))) {
		bad_area(regs, error_code, address);

    // 
    // 恭喜! 这不是一次 "非法" 访问,
    // 只是因为内核“偷懒”还没分配物理页。
    // 我们称之为 "Demand Paging" (按需调页)
    
    // 'handle_mm_fault' 是通用的、与架构无关的处理函数
    fault = handle_mm_fault(vma, address, flags, regs);

    if (fault & VM_FAULT_COMPLETED)
		return;
    
    // ... (处理成功,返回用户态,重新执行 'mov' 指令) ...
    return;

bad_area:
    // 这里就是 SEGFAULT (SIGSEGV) 的诞生地
    // ... (force_sig_info(SIGSEGV, ...)) ...
}


// (在 kernel/mm/memory.c)
// mm/memory.c
vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
                           unsigned int flags, struct pt_regs *regs)
{
    struct vm_fault vmf = {
        .vma = vma,
        .address = address,
        .flags = flags,
    };
    .....
    return handle_pte_fault(&vmf);
}

static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
	pte_t entry;

	if (unlikely(pmd_none(*vmf->pmd))) {
	.....
	if (!vmf->pte) {
		if (vma_is_anonymous(vmf->vma))
			return do_anonymous_page(vmf);
		else
			return do_fault(vmf);
	}

	....
}
// (在 kernel/mm/memory.c)
static int do_anonymous_page(struct vm_area_struct *vma, ...)
{
    // 1. 【物理分配】
    //    这是我们第一次看到物理内存分配!
    //    'alloc_page_vma' 会调用 "伙伴算法" (Buddy Allocator)
    //    __GFP_ZERO 标志告诉它 "请给我一个已经清零的物理页"
    struct page *page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
    if (!page)
        return oom; // 内核 OOM!

    // 2. 【建立映射】
    //    创建一个新的 Page Table Entry (PTE)
    //    这个 PTE 将 虚拟地址 链接到 物理页 (page)
    __SetPageUptodate(page);

	entry = mk_pte(page, vma->vm_page_prot);
	entry = pte_sw_mkyoung(entry);

    // 3. 【写入页表】
    //    将这个 PTE 原子地写入到进程的页表中
    set_pte_at(vma->vm_mm, address, ptep, entry);

    // 4. 更新 RSS 计数器 (你 Pmap 看到的)
    // ...
    // 成功!
}
  1. *ptr = 'H' 触发 do_page_fault
  2. 内核通过 find_vma 检查“地契”,确认访问合法。
  3. handle_mm_fault 发现页表项(PTE)是空的。
  4. do_anonymous_page 被调用,它执行两个关键操作:
    • alloc_page_vma (伙伴算法) 分配一个 4KB 物理页框
    • set_pte_at 将这个物理页框的地址写入页表
  5. 中断处理函数返回,CPU重新执行 *ptr = 'H'
  6. 这一次,MMU查找页表,找到了有效的PTE,成功将虚拟地址翻译为物理地址,写入完成。

这就是从malloc(100)*ptr = 'H',再到RAM中一个特定晶体管状态被改变的完整旅程。

mem_all

小结

现在,当你再遇到C++内存分配的性能问题时,你的思路就清晰了:

  • 问题:多线程卡顿(锁竞争)。

    • 原因: ptmalloc2的Arena锁在高度并发下表现不佳。
    • 解决: 换一个分配器。通过LD_PRELOAD环境变量,将你的程序链接到libjemalloc.solibtcmalloc.so。这通常能带来立竿见影的性能提升,因为它们有更激进的“每线程缓存”。
  • 问题:服务启动时,或访问大数据时P99延迟高(Page Faults)。

    • 原因: malloc后第一次访问触发了大量的“缺页中断”。
    • 解决:
      1. 预热(Warm-up):malloc后,立刻用memset(或在单独的初始化线程中)把内存“摸”一遍,提前支付Page Fault的开销。
      2. madvise: 使用madvise(ptr, size, MADV_WILLNEED)来“建议”内核预先把页面换入。
      3. MAP_POPULATE: 如果你是直接用mmap,可以加MAP_POPULATE标志,让内核在mmap返回前就分配好物理内存(不再懒惰)。
  • 问题:程序内存只增不减(碎片)。

    • 原因:
      1. 小对象分配导致brk管理的堆无法收缩。
      2. ptmalloc2为了性能,会“囤积”free的内存,不还给OS。
    • 解决:
      1. 对象池(Object Pools): C++开发者的利器。对于频繁创建和销毁的 同尺寸 对象(如BulletPacket),自己实现一个对象池。一次malloc一大块,然后用链表管理空闲对象。这完全避开了malloc的内部锁和复杂逻辑。
      2. mmap: 对那些“大且短命”的分配(如临时缓冲区),考虑绕过malloc,直接使用mmapmunmap
  • 问题:算法慢(TLB/Cache失效)。

    • 原因: malloc返回的内存页在物理上可能是零散的。当你的数据结构(如链表、树)在内存中“跳跃”访问时,会不断导致TLB Miss(地址翻译缓存失效),CPU需要花费几百个周期去“遍历页表”。
    • 解决:
      1. 数据导向设计(DOD): 尽量使用连续内存,比如用std::vector代替std::list
      2. 自定义分配器(C++ Allocator): 为你的std::mapstd::list提供一个自定义分配器,它从一个连续的内存池(Slab)中分配节点,保证高度的“内存局部性”。
      3. 巨页(Huge Pages): 对于GB级的超大内存(如数据库缓存),使用2MB或1GB的“巨页”(Huge Pages)来代替4KB的页。这能极大减少TLB条目,大幅提升性能。
posted @ 2025-11-01 14:49  ToBrightmoon  阅读(52)  评论(0)    收藏  举报

© ToBrightmoon. All Rights Reserved.

Powered by Cnblogs & Designed with ❤️ by Gemini.

湘ICP备XXXXXXXX号-X