Page 508 - 《软件学报》2024年第4期
P. 508
2086 软件学报 2024 年第 35 卷第 4 期
心 ID 来提示内存分配器使用不同的管理方式.
榫卯框架通过为层级函数扩展出策略槽, 以提供更好的可定制性和代码复用能力. 策略槽的实现实际上是一
组遵循相同调用规则的接口, 利用 C 预处理的元编程特性可以将策略函数作为层级函数的输入参数填入. 榫卯框
架目前包含以下类型的策略函数.
(1) 分级管理策略. 在现代内存分配器中通常使用分级管理, 对不同大小范围的内存请求采用不同的内存分配
算法. 为了实现这种分级管理, 榫卯内存分配框架将其抽象为分级管理策略, 由 SIZE_HANDLE 宏定义其管理的
内存大小范围, 若超过范围则直接转交给下一层处理.
(2) 大小阶策略. 分离适配算法中通过大小阶区分不同大小的内存块请求, 大小阶的划分粒度通常会影响到内
存碎片率. 对于使用分离适配的基础层, 本框架将大小阶划分抽象为统一的接口, 然后使用 USE_SIZECLASS 宏为
每层定制大小阶划分策略. 每一个大小阶划分策略必须需要实现 size_to_szcls 和 szcls_to_size 接口. 榫卯框架提
供 tcmalloc, supermalloc 等分配器中的大小阶策略实现, 同时还提供配置间隔的自定义大小阶策略.
(3) 同步策略. 线程共享的层级存在临界访问的问题, 必须依赖线程间同步机制保证并发访问共享数据结构的
线程安全. 本文提出的框架将同步机制抽象为同步策略, 将临界区执行封装为同步过程调用 [14] , 使用统一 atomic_exec
接口执行. 开发者能够使用 USE_SYNC 宏为层级函数指定同步方式. 榫卯框架提供多种类型的阻塞式同步算法实
现, 如传统的竞争自旋锁 spinlock, 排队锁 mcslock, 基于合并同步的 ccsynch, 或是基于委任的 RCL, 这些同步算法
适用于不同的硬件和应用场景.
(4) 校验策略. 使用非类型安全的编程语言编写的用户应用程序可能存在非法使用内存的行为, 包括缓冲区溢
出, 释放后使用, 空指针, 重复释放等. 针对这些内存漏洞, 内存分配器可为用户内存块增加额外字段用于记录校验
和, 检验内存块是否被非法使用. 校验策略需要定义校验字 chksum_t 以及 chksum_calculate 接口, 该接口依据内存
指针以及大小生成校验字. 通常, 为了减少性能开销, 开发者可使用简单的魔数 (magic number) 校验或是异或校
验, 而在要求更高检出率的场景下, 开发者可以进一步扩展校验字大小, 使用更复杂的 CRC 冗余校验.
(5) 错误处理策略. 错误处理策略定义在检测出内存非法使用时, 如何进行相应的处理. 榫卯框架将错误处理
抽象为 raise_err 接口, 统一编码错误类型. 用户可以通过定制该接口实现更灵活的错误处理方式, 如进行容错处
理 (如丢弃被改写的内存块) 或是执行用户注册的错误处理函数.
2.3 榫卯框架小结
榫卯内存分配框架允许用户利用现有的层级函数和策略函数快速构建和定制动态内存分配器, 同时也支持用
户编写新的层级函数. 框架本身也包含大量基础数据结构与函数组件用于编写层级函数. 与现有的内存分配框架
HeapLayer 相比, 榫卯框架是以函数式编程的方式处理内存分配过程, 其定制性, 组合性和简洁性更佳. 一方面, 榫
卯框架提供了策略抽象以提供更好的可定制性, 如同步策略, 错误处理策略抽象等; 另一方面, 这些遵循同一调用
规则的可复合的层级函数允许更灵活的分配器组合, 例如条件绕过上层函数向指定层传递内存块, 以及对内存请
求上下文 mctx 的扩展. 这些都是 HeapLayer 框架无法做到的. 同时, 榫卯框架将复杂性转移到框架本身的实现上,
呈现给用户的接口也更为简洁. 例如榫卯框架复合层级函数使用的 LAYER_COMPOSE 传入的参数是从高层到低
层的层级列表, 避免了在 HeapLayer 中复合多个层级时容易陷入括号嵌套的情况, 因此代码更为直观, 可读性更
好. 基于函数式编程思想构建内存分配器也能够简化分配器模型的表示, 例如分配器状态全部封装在内存请求上
下文中, 层级函数的副作用也只会修改内存请求上下文. 这些特性都有助于内存分配器抽象模型与实现的形式化
验证.
在性能开销方面, HeapLayer 框架利用 C++ mix-in 元编程机制实现零开销的层级组合, 而榫卯框架基于 C 宏
元编程, 以传入上层函数回调, 生成中间函数的形式来实现层级函数的复合. 这种方式可能会造成不必要的函数调
用过程. 不过, 此性能开销可由现代编译器优化消除. 主流的 C 编译器会将 inline 函数的调用过程强制展开, 从而
避免额外的函数调用过程. 此外, 内存请求上下文的构造和在层级间的传递也存在一定的栈开销和重复的逻辑判
断. 不过由于该过程发生在该框架内部, 且框架全部以.h 头文件实现, 因而现代 C 编译器能够对框架内所有可见
代码进行自动推导, 利用常量折叠, 死代码消除等优化方式消除这部分开销.