0%

DMTCP 之内存管理 (jalib)

源码

https://github.com/dmtcp/dmtcp/tree/main/jalib

malloc 的特性和局限

malloc/free 是操作系统(或 C 库)提供的通用堆分配器。

  • 它通常会采用 “内存池 + 分块 + 空闲链表” 等技术,但它为了通用性和线程安全,设计得很复杂,开销较大。
  • 在高并发 / 频繁小块分配释放的场景下,malloc 的性能和碎片控制未必理想。

malloc 很难让你:

  • 控制分配内存的位置(如 DMTCP 需要特殊内存区域)
  • 统计 / 追踪所有分配的内存块
  • 实现定制的分配策略(如无锁、分层小块池、预扩展等)
  • 轻松调试和隔离内存问题

内存碎片

malloc 的确在其实现内部也维护着自己的 “内存池”,并且会对小块内存(small bins/tcache/fast bins 等)做优化和分组管理。比如在 glibc 的 malloc(ptmalloc)中,就有针对小块内存的快速分配机制。

  1. malloc 是 “通用分配器”
    malloc 需要支持所有应用场景,包括大 / 小 / 奇异尺寸的分配、跨多线程、兼容各种系统调用和 ABI。
    为了兼容性和健壮性,malloc 实现复杂,包含很多额外的元数据和检查,导致分配 / 释放开销更大。

  2. malloc 的小块管理是 “全局的”
    malloc 管理的小块是全进程共享的,所有线程 / 模块都会竞争同一套管理结构(如 fastbin、tcache、small bin)。
    在高并发、频繁小块分配 / 释放的场景下,锁竞争和同步成本变高,可能成为性能瓶颈。

  3. 自定义分配器(如 jalib)“更窄、更专用”
    jalib 只服务 DMTCP 内部的特殊内存分配需求,只关注固定几种典型的小块尺寸(如 64/256/1024…)。
    可以用更简单、更高效的 “无锁链表 + 内存对齐块” 来管理池,分配和释放几乎都是 O(1)的原子操作。
    不需要兼容所有 malloc 的场景(如 realloc、跨模块释放等),所以能极致优化。

  4. 控制权和可观测性
    jalib 可以完全掌控池的生命周期、分配区域、分配策略(如预扩展、定制回收),还可以追踪统计、调试。
    malloc 的内部状态你无法直接控制或感知,也无法方便地和 DMTCP 的 checkpoint、回滚等功能集成。

  5. 内存碎片和确定性
    专用分配器能保证分配块 “定长、对齐”,几乎无碎片,分配和回收都是确定性的。
    malloc 需要兼容各种尺寸,碎片和内存抖动不可避免。

jalib(自定义分配器)的设计动机

性能优化

  • DMTCP 频繁地分配和释放小块内存(如元数据、临时缓存等),如果每次都用 malloc,性能损耗大。
  • jalib 采用分级固定块池,每次分配 / 释放只需操作链表和原子变量,比 malloc 更快、更少碎片。

线程安全的高效实现

  • jalib 用无锁(128 位 CAS)或轻量级互斥方案,适合高并发分配 / 释放。
  • malloc 虽然线程安全,但实现方式更重,适用范围更广,未必最优。

可控性和可追踪性

  • jalib 可以统计分配次数、追踪所有内存池区,方便调试、分析和 checkpoint 恢复。
  • 可根据实际需求预分配或批量扩展,避免运行时大规模内存抖动。

适应 DMTCP 的特殊需求

  • DMTCP 需要在 checkpoint/restore 时管理所有内存区域,jalib 可以定制分配区域、分配方式,malloc 无法满足。
  • 可实现特定平台的优化,如 mmap 固定地址分配等。

故障隔离和调试

  • jalib 可以在有 bug 或内存泄漏时,帮助定位具体的分配 / 释放流程。
  • 可以方便地记录所有 arena 信息,甚至实现特殊的调试模式。

总结

虽然 malloc 也是内存池管理,但它是为通用用途设计的,不能满足 DMTCP 这类高性能、高可控性、特殊内存管理需求场景。自定义 jalib 分配器可以更高效地管理小块内存,优化多线程性能,便于调试和适配特定需求。

可以归纳为三点:

  • 性能更高,碎片更少
  • 更好地适应 DMTCP 的需求
  • 更易于调试和控制

jalloc 设计思路

多层级固定大小块分配(层级分配器)

  • 设计了 5 个分配层级(lvl1~lvl5),每层负责不同大小的定长内存块(如 64、256、1024、4096、16384 字节)。
  • 小于等于这 5 个等级的分配请求,会被分配到各自的层级。
  • 超过最大层级的请求,则直接调用 _alloc_raw(通常是 mmap)。

优点:

  • 小块内存可以复用,减少系统调用和碎片。
  • 大块内存仍可直接用系统接口,兼顾通用性。

固定块分配器 JFixedAllocStack

每个层级对应一个 JFixedAllocStack<N>,其核心是无锁栈式管理:

  • 内部维护一个空闲块栈(LIFO 链表)。
  • allocate 时从栈取出一个空闲块,栈空时调用 expand 申请一批新块。
  • deallocate 时将块归还到栈顶。

核心技术点

  • 原子双字比较交换(128 位 CAS)

    为了线程安全,栈顶指针 _top 需要原子更新。这里用到了 128 位 CAS(Compare-And-Swap),保证 node 指针和计数器同时更新,避免 ABA 问题。

  • CAS 不可用时的降级方案

    对于不支持 128 位原子操作的平台,采用 futex+memcpy 的方式手动实现互斥和原子性。

线程安全设计

分配和释放都用原子操作保护,无需锁,性能高。
多线程环境下不会出现竞争条件或内存破坏。

Arena 记录和调试

分配的内存区域(arena)可以记录到全局数组中,方便调试和统计。
通过 JAlloc::getAllocArenas() 可获得分配区域列表。

全局 new/delete 重载(可选)

如果定义了 OVERRIDE_GLOBAL_ALLOCATOR,会重载 operator newoperator delete,让全局 new/delete 也用这个分配器。

灵活切换

可以通过宏 JALIB_ALLOCATOR 切换:

  • 启用时用自定义分配器
  • 否则回退为标准 malloc/free

总结

本内存分配器的设计核心在于:

  • 采用多级固定块内存池 + 无锁算法,高效服务于小块高频分配 / 释放;
  • 通过 128 位原子操作或 futex 确保并发安全,适用多平台;
  • 提供 arena 管理和统计,方便调试与维护;
  • 兼容传统分配方式,易于集成和切换。

这种设计非常适合像 DMTCP 这样对性能和内存管理有特殊要求的系统级软件。