在共享/静态库中集成C ++自定义内存分配器

时间:2017-11-18 23:30:34

标签: c++ memory memory-management

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

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

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

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

1 个答案:

答案 0 :(得分:2)

如何大量完成内存分配取决于操作系统。您需要了解共享库如何在这些操作系统中工作,C语言与这些操作系统的关系以及共享库的概念。

C,C ++和模块化编程

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

模块=共享库可执行文件

Linux和类Unix系统

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

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

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

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

这里的一般规则是引入符号的第一个模块将是提供符号的模块。因此,原始进程可执行文件在此处具有最高优先级,如果它定义了malloc函数,则该malloc函数将在该进程的任何位置使用。这同样适用于函数callocreallocfree等。使用像LD_PRELOAD这样的技巧和技巧允许你覆盖"默认的内存分配器"你的申请。由于存在一些极端情况,因此无法保证这一点。在执行此操作之前,您应该查阅库的文档。

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

  1. brksbrk系统调用
  2. mmap系统调用
  3. 第一个允许您访问通常在可执行映像之后直接分配的单个每进程内存区域。由于只有一个这样的区域,这种内存分配方式只能由进程中的单个分配器使用(并且它通常已被C库使用)。

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

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

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

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

    1. 使用VirtualAllocMapViewOfFile等函数。
    2. 堆分配函数 - HeapCreateHeapAlloc
    3. 第一个相当于mmap,而第二个是malloc的更高级版本,它基于VirtualAlloc内部(我相信)。

      现在因为Windows与C语言的关系不像Unix那样,它没有为您提供mallocfree功能。相反,它们是由C运行时库提供的,它是在Windows API之上实现的。

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

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

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

      这取决于。你需要了解你想要达到的目标。

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

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

      这里的一般规则是,要么所有内容都应该静态链接,要么所有内容都应该动态链接。在两者之间进行某种混合可能非常复杂,并且需要您保持整个架构在您的头脑中以避免在程序中混合堆或其他数据结构(如果您没有很多,这不是一个大问题模块)。如果需要混合使用静态和动态链接,则应将分配器库构建为共享库,以便在进程中单独实现它时更容易。

      Unix-alikes和Windows之间的另一个区别是Windows没有静态链接的可执行文件"的概念。在Windows上,每个可执行文件都依赖于特定于Windows的动态库,如ntdll.dll。虽然ELF可执行文件具有单独的类型,用于"静态链接"和"动态链接"可执行文件。

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

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