内存分配方式主要取决于操作系统。你需要了解在这些操作系统中共享库的工作方式,以及C语言与这些操作系统和共享库概念的关系。
首先,我想提到的是C语言不是一种模块化语言,例如它不支持模块或模块化编程。对于像C和C++这样的语言,模块化编程的实现留给底层操作系统。共享库是实现C和C++模块化编程的机制之一,因此我将把它们称为“模块”。
模块=共享库和可执行文件
最初,Unix系统上的所有内容都是静态链接的。共享库稍后才出现。由于Unix是C语言的起点,这些系统尝试提供接近于C语言编程的共享库编程接口。
其想法是,最初没有考虑到共享库的C代码应该构建并且应该在不更改源代码的情况下工作。结果,提供的环境通常具有单个进程范围的符号命名空间,该命名空间由加载的所有模块共享,例如整个进程中只能有一个名为“foo”的函数,除了静态函数(以及使用特定于操作系统的机制在模块中“隐藏”的某些函数)。基本上,这与静态链接相同,您不允许有重复的符号。
对于您的情况来说,这意味着在整个进程中始终存在一个名为"malloc"的函数,并且每个模块都在使用它。所有模块共享相同的内存分配器。
如果进程恰好具有多个malloc函数,则只会选择一个函数,并将由所有模块使用。此处的机制非常简单:因为共享库不知道每个被引用函数的位置,它们通常通过某个表(GOT、PLT)调用这些函数,该表在第一次调用或加载时进行填充所需地址。同样的规则适用于提供原始函数的模块——即使在原始模块中也会通过同一张表来调用该函数,从而使得在提供它的原始模块甚至可以覆盖该函数(这是与在Linux上使用共享库相关的许多效率问题的源头,搜索-fno-semantic-interposition、-fno-plt来克服这些问题)。
一般规则是,首个引入符号的模块将提供该符号。因此,原始进程可执行文件在这里具有最高优先级,如果它定义了
malloc
函数,则该函数将在整个进程中使用。对于函数
calloc
、
realloc
、
free
和其他函数同样适用。通过使用这种技巧以及像
LD_PRELOAD
这样的技巧,您可以覆盖应用程序的“默认内存分配器”。但这不能保证总是奏效,因为还存在一些特殊情况。在进行此操作之前,请查阅您库的文档。
我想特别指出,这意味着在进程中有一个共享的堆,并且有一个很好的理由。类Unix系统通常提供两种在进程中分配内存的方式:
1.
brk
、
sbrk
系统调用
2.
mmap
系统调用
第一种提供了一种访问单个进程内存区域的方法,通常在可执行映像后直接分配。因为只有一个这样的区域,所以在进程中仅可由单个分配器使用此内存分配方式(通常已由您的C库使用)。
在将自定义内存分配器添加到进程之前,必须理解这一点-它不应使用
brk
、
sbrk
,或者应该覆盖您的C库的现有分配器。
第二种可以直接从底层内核请求内存块。由于内核知道进程虚拟内存的结构,因此它能够分配内存页而不干扰任何用户空间分配器。这也是在进程中具有多个完全独立的内存分配器(堆)的唯一方法。
Windows
Windows没有像类Unix系统那样依赖于C运行时。相反,它提供了自己的运行时-Windows API。
使用Windows API分配内存通常有两种方式:
1. 使用像
VirtualAlloc
、
MapViewOfFile
这样的函数。
2. 堆分配函数 -
HeapCreate
、
HeapAlloc
。
第一种等同于
mmap
,而第二种则是更高级版本的
malloc
,它在内部基于
VirtualAlloc
(据我所知)。
现在,因为Windows与C语言的关系与类Unix系统不同,它不会为您提供
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很好地混合静态和动态链接(几乎可以,但并非完全如此)。
如果您使用自己的库,则应确保在动态链接的可执行文件中动态链接它。想象一下,如果您将分配器静态链接到共享库中,但进程中的另一个库也使用了相同的库-您可能会意外使用另一个分配器,而不是您期望的那个。