【问题标题】:Integrating C++ custom memory allocators across shared/static libraries跨共享/静态库集成 C++ 自定义内存分配器
【发布时间】:2017-11-18 23:30:33
【问题描述】:

我开始在我的项目中使用一些自定义内存分配器,例如 rpmallocltmalloc,但我对集成有些担心,我的项目有各种内部模块构建为共享库或静态库(取决于我如何配置它们在我的构建系统中)并且应该为 Windows/Linux/FreeBSD/Mac OS X 以及 x86 和 ARM 等架构构建/运行,我不知道我是否应该在内部调用我的内存分配器集成头文件或应保留在 cpp 文件中。

如果内存分配器调用保留在头文件中,每个模块都应该链接内存分配器的静态库,如果它保存在 .cpp 文件中,则调用包含在包含它们的库中,并且只有那个模块应该链接自定义内存分配器,但该模块应该包含一个接口,每个模块都可以分配它们(避免内存分配不一致)

我读过here 如果内存分配正常(就像 malloc/free/syscalls 一样)每个共享库都有自己的堆,但是如果使用 mmap 分配的内存不属于程序的堆。

我的问题是,如果将共享/静态库保存在一个库中(但每个其他库都应该链接它以访问它们的内存分配接口),它是否会给我的共享/静态库带来任何危险?还是应该在头文件中内联所有内容,并且每个库都应该链接内存分配器库?。

【问题讨论】:

  • 不要过度使用内联,我建议你在 Scott Meyer 的 Effective C++ 中进一步阅读
  • 您应该添加有关您的操作系统的更多信息,因为这对这个问题非常重要。

标签: c++ memory memory-management


【解决方案1】:

如何进行内存分配很大程度上取决于操作系统。您需要了解共享库在这些操作系统中的工作原理、C 语言与这些操作系统的关系以及共享库的概念。

C、C++ 和模块化编程

首先,我想提一下 C 语言 不是模块化语言 例如。它不支持模块模块化编程。对于像 C 和 C++ 这样的语言,模块化编程的实现留给底层操作系统。共享库是用于使用 C 和 C++ 实现模块化编程的机制示例,因此我将它们称为 模块

Module = 共享库可执行文件

Linux 和类 Unix 系统

最初,Unix 系统上的所有内容都是静态链接的。共享库后来出现。由于 Unix 是 C 语言的起点,因此这些系统试图提供与 C 语言编程感觉接近的共享库编程接口。

这个想法是,最初编写的 C 代码应该在没有共享库的情况下进行构建,并且应该在不更改源代码的情况下工作。结果,提供的环境通常具有由所有加载的模块共享的单个进程范围的符号命名空间,例如整个过程中只能有一个名称为foo 的函数,除了static 函数(以及一些在使用操作系统特定机制的模块中为hidden 的函数)。基本上它与不允许重复符号的静态链接相同。

这对您的情况意味着在整个过程中始终使用一个名为 malloc 的函数,并且每个模块都在使用它,例如所有模块共享同一个内存分配器

现在,如果进程碰巧有多个malloc 函数,则只会选择一个并将被所有模块使用。这里的机制非常简单——因为共享库不知道每个引用函数的位置,它们通常会通过某个表(GOTPLT)调用它们,这些表将在第一次调用或加载时懒惰地填充所需的地址时间。相同的规则适用于提供原始函数的模块 - 即使在内部,该函数也将通过同一个表调用,即使在提供它的原始模块中也可以覆盖该函数(这是许多与在 Linux 上使用共享库,搜索 -fno-semantic-interposition, -fno-plt 来解决这个问题。

这里的一般规则是第一个引入符号的模块将是提供它的模块。因此,原始进程可执行文件在这里具有最高优先级,如果它定义了malloc 函数,那么malloc 函数将在进程中的任何地方使用。这同样适用于函数callocreallocfree 和其他函数。使用这个技巧和像LD_PRELOAD 这样的技巧可以让您覆盖应用程序的“默认内存分配器”。由于存在一些极端情况,因此不能保证有效。在执行此操作之前,您应该查阅您的库的文档。

我想特别指出,这意味着所有模块共享的进程中有一个堆,这是有充分理由的。类 Unix 系统通常提供两种在进程中分配内存的方式:

  1. brk, sbrk 系统调用
  2. mmap系统调用

第一个为您提供对通常直接在可执行映像之后分配的单个进程内存区域的访问。由于只有一个这样的区域,这种内存分配方式只能由进程中的单个分配器使用(并且通常已经被您的 C 库使用)。

在将任何自定义内存分配器放入进程之前了解这一点很重要 - 它不应该使用 brksbrk,或者应该覆盖 C 库的现有分配器。

第二个可用于直接从底层内核请求内存块。由于内核知道您的进程虚拟内存的结构,它能够在不干扰任何用户空间分配器的情况下分配内存页面。这也是在进程中拥有多个完全独立的内存分配器(堆)的唯一方法。

窗口

Windows 不像类 Unix 系统那样依赖 C 运行时。相反,它提供了自己的运行时 - Windows API。

有两种使用 Windows API 分配内存的方法:

  1. 使用VirtualAllocMapViewOfFile等函数。
  2. 和堆分配函数 - HeapCreateHeapAlloc

第一个等效于mmap,而第二个是malloc 的更高级版本,它在内部(我相信)基于VirtualAlloc

现在因为 Windows 与 C 语言的关系不像 Unix-likes 那样,所以它不提供mallocfree 函数。相反,这些是由在 Windows API 之上实现的 C 运行时库提供的。

关于 Windows 的另一件事 - 它没有单个进程符号命名空间的概念,例如你不能像在类 Unix 系统上那样覆盖函数。这允许您在同一个进程中同时存在多个 C 运行时,并且每个运行时都可以提供其独立的 mallocfree 等实现,每个运行在单独的堆上。

因此,在 Windows 上,所有库将共享单个进程 Windows API 特定的堆(可通过GetProcessHeap 获得),同时它们将共享进程中某个 C 运行时的堆。

那么如何将内存分配器集成到程序中呢?

这取决于。您需要了解您想要实现的目标。

您是否需要替换进程中每个人都使用的内存分配器,例如默认分配器?这只能在类 Unix 系统上实现。

这里唯一可移植的解决方案是显式使用您的特定分配器接口。您如何执行此操作并不重要,您只需要确保 Windows 上的所有库共享同一个堆。

这里的一般规则是,要么一切都应该静态链接,要么一切都应该动态链接。在两者之间进行某种混合可能真的很复杂,并且需要您将整个架构牢记在心,以避免在程序中混合堆或其他数据结构(如果您没有很多模块,这不是一个大问题) .如果您需要混合使用静态和动态链接,则应将分配器库构建为共享库,以便更轻松地在进程中实现单一实现。


类 Unix 和 Windows 之间的另一个区别是 Windows 没有“静态链接可执行文件”的概念。在 Windows 上,每个可执行文件都依赖于特定于 Windows 的动态库,例如 ntdll.dll。而 ELF 可执行文件具有“静态链接”和“动态链接”可执行文件的不同类型。

这主要是由于单个进程符号命名空间,这使得在 Unix 上混合共享和静态链接很危险,但允许 Windows 很好地混合静态和动态链接(几乎,不是真的)。​​

如果您使用其中一个库,则应确保将其与动态链接的可执行文件动态链接。想象一下,如果您将分配器静态链接到您的共享库,但您的进程中的另一个库也使用同一个库 - 您可能无意中使用了另一个分配器,而不是您所期望的。

【讨论】:

  • > 这里的一般规则是,要么一切都应该静态链接,要么一切都应该动态链接。正如您所说,我的项目中的每个模块或库都是静态的或共享的,没有混合,其中一个库是所有其他库的“核心”,该“核心”库包含我的自定义内存分配器,其他所有库都应该使用他们的记忆功能。
  • 因此,您可以从“核心”模块中提供单独的内存分配接口,并在最重要的地方使用它。这是最简单、最便携的解决方案。只需确保您的分配器不依赖于 Unix-likes 上的 brksbrk。在整个过程中覆盖使用是不可移植的——在 Windows 上没有可靠的方法来做到这一点,即使在 Unix 上,这也可能是危险的。
  • 它不依赖这两个,仅在 Unix 中使用 mmap,在 Windows 中使用它们的等价物:VirtualAlloc
  • 因为它们似乎都使用全局状态并且至少在默认情况下不尝试替换标准malloc。如果您在一个过程中只保留一份副本,您应该是安全的。这不应该是全动态链接或全静态链接的问题(除非您的进程中的另一个模块静态链接您的一个分配器)。
  • 不,我对一个模块中的一个分配器有严格的规定,分配器模块统治着所有其余的模块。
猜你喜欢
  • 2017-11-11
  • 2013-10-12
  • 2016-10-21
  • 1970-01-01
  • 2015-11-15
  • 2015-07-23
  • 2011-01-27
  • 1970-01-01
相关资源
最近更新 更多