如何进行内存分配很大程度上取决于操作系统。您需要了解共享库在这些操作系统中的工作原理、C 语言与这些操作系统的关系以及共享库的概念。
C、C++ 和模块化编程
首先,我想提一下 C 语言 不是模块化语言 例如。它不支持模块或模块化编程。对于像 C 和 C++ 这样的语言,模块化编程的实现留给底层操作系统。共享库是用于使用 C 和 C++ 实现模块化编程的机制示例,因此我将它们称为 模块。
Module = 共享库和可执行文件
Linux 和类 Unix 系统
最初,Unix 系统上的所有内容都是静态链接的。共享库后来出现。由于 Unix 是 C 语言的起点,因此这些系统试图提供与 C 语言编程感觉接近的共享库编程接口。
这个想法是,最初编写的 C 代码应该在没有共享库的情况下进行构建,并且应该在不更改源代码的情况下工作。结果,提供的环境通常具有由所有加载的模块共享的单个进程范围的符号命名空间,例如整个过程中只能有一个名称为foo 的函数,除了static 函数(以及一些在使用操作系统特定机制的模块中为hidden 的函数)。基本上它与不允许重复符号的静态链接相同。
这对您的情况意味着在整个过程中始终使用一个名为 malloc 的函数,并且每个模块都在使用它,例如所有模块共享同一个内存分配器。
现在,如果进程碰巧有多个malloc 函数,则只会选择一个并将被所有模块使用。这里的机制非常简单——因为共享库不知道每个引用函数的位置,它们通常会通过某个表(GOT、PLT)调用它们,这些表将在第一次调用或加载时懒惰地填充所需的地址时间。相同的规则适用于提供原始函数的模块 - 即使在内部,该函数也将通过同一个表调用,即使在提供它的原始模块中也可以覆盖该函数(这是许多与在 Linux 上使用共享库,搜索 -fno-semantic-interposition, -fno-plt 来解决这个问题。
这里的一般规则是第一个引入符号的模块将是提供它的模块。因此,原始进程可执行文件在这里具有最高优先级,如果它定义了malloc 函数,那么malloc 函数将在进程中的任何地方使用。这同样适用于函数calloc、realloc、free 和其他函数。使用这个技巧和像LD_PRELOAD 这样的技巧可以让您覆盖应用程序的“默认内存分配器”。由于存在一些极端情况,因此不能保证有效。在执行此操作之前,您应该查阅您的库的文档。
我想特别指出,这意味着所有模块共享的进程中有一个堆,这是有充分理由的。类 Unix 系统通常提供两种在进程中分配内存的方式:
-
brk, sbrk 系统调用
-
mmap系统调用
第一个为您提供对通常直接在可执行映像之后分配的单个进程内存区域的访问。由于只有一个这样的区域,这种内存分配方式只能由进程中的单个分配器使用(并且通常已经被您的 C 库使用)。
在将任何自定义内存分配器放入进程之前了解这一点很重要 - 它不应该使用 brk、sbrk,或者应该覆盖 C 库的现有分配器。
第二个可用于直接从底层内核请求内存块。由于内核知道您的进程虚拟内存的结构,它能够在不干扰任何用户空间分配器的情况下分配内存页面。这也是在进程中拥有多个完全独立的内存分配器(堆)的唯一方法。
窗口
Windows 不像类 Unix 系统那样依赖 C 运行时。相反,它提供了自己的运行时 - Windows API。
有两种使用 Windows API 分配内存的方法:
- 使用
VirtualAlloc、MapViewOfFile等函数。
- 和堆分配函数 -
HeapCreate、HeapAlloc。
第一个等效于mmap,而第二个是malloc 的更高级版本,它在内部(我相信)基于VirtualAlloc。
现在因为 Windows 与 C 语言的关系不像 Unix-likes 那样,所以它不提供malloc 和free 函数。相反,这些是由在 Windows API 之上实现的 C 运行时库提供的。
关于 Windows 的另一件事 - 它没有单个进程符号命名空间的概念,例如你不能像在类 Unix 系统上那样覆盖函数。这允许您在同一个进程中同时存在多个 C 运行时,并且每个运行时都可以提供其独立的 malloc、free 等实现,每个运行在单独的堆上。
因此,在 Windows 上,所有库将共享单个进程 Windows API 特定的堆(可通过GetProcessHeap 获得),同时它们将共享进程中某个 C 运行时的堆。
那么如何将内存分配器集成到程序中呢?
这取决于。您需要了解您想要实现的目标。
您是否需要替换进程中每个人都使用的内存分配器,例如默认分配器?这只能在类 Unix 系统上实现。
这里唯一可移植的解决方案是显式使用您的特定分配器接口。您如何执行此操作并不重要,您只需要确保 Windows 上的所有库共享同一个堆。
这里的一般规则是,要么一切都应该静态链接,要么一切都应该动态链接。在两者之间进行某种混合可能真的很复杂,并且需要您将整个架构牢记在心,以避免在程序中混合堆或其他数据结构(如果您没有很多模块,这不是一个大问题) .如果您需要混合使用静态和动态链接,则应将分配器库构建为共享库,以便更轻松地在进程中实现单一实现。
类 Unix 和 Windows 之间的另一个区别是 Windows 没有“静态链接可执行文件”的概念。在 Windows 上,每个可执行文件都依赖于特定于 Windows 的动态库,例如 ntdll.dll。而 ELF 可执行文件具有“静态链接”和“动态链接”可执行文件的不同类型。
这主要是由于单个进程符号命名空间,这使得在 Unix 上混合共享和静态链接很危险,但允许 Windows 很好地混合静态和动态链接(几乎,不是真的)。
如果您使用其中一个库,则应确保将其与动态链接的可执行文件动态链接。想象一下,如果您将分配器静态链接到您的共享库,但您的进程中的另一个库也使用同一个库 - 您可能无意中使用了另一个分配器,而不是您所期望的。