Linux 基础七 内存
在操作系统中,进程的内存布局通常分为几个区域,包括代码段、已初始化的数据段、未初始化的数据段(BSS段)、堆和栈。其中,堆用于动态内存分配,其大小可以在运行时根据需要增长或收缩。
文章目录
- 7.1 在堆上分配内存
- 7.1.1 调整 program break:brk() 和 sbrk()
- 内存分配的工作过程
- 限制和注意事项
- 标准化状态
- 7.1.2 在堆上分配内存:`malloc()` 和 `free()`
- `malloc()` 函数
- `free()` 函数
- `malloc()` 和 `free()` 的优点
- 示例程序:`free()` 对 `program break` 的影响
- 运行结果分析
- 总结
- 7.1.3 `malloc()` 和 `free()` 的实现
- 1. `malloc()` 的实现
- 2. `free()` 的实现
- 3. 内存管理和常见错误
- 4. 总结
- 4. `malloc` 调试工具和库
- 1. glibc 提供的 `malloc` 调试工具
- 1.1 `mtrace()` 和 `muntrace()`
- 1.2 `mcheck()` 和 `mprobe()`
- 1.3 `MALLOC_CHECK_` 环境变量
- 2. 第三方 `malloc` 调试库
- 2.1 Electric Fence
- 2.2 dmalloc
- 2.3 Valgrind
- 2.4 Insure++
- 3. 控制和监测 `malloc` 函数包
- 3.1 `mallopt()`
- 3.2 `mallinfo()`
- 4. 总结
- 5. 在堆上分配内存的其他方法
- 1. `calloc()`
- 2. `realloc()`
- 3. `memalign()` 和 `posix_memalign()`
- 3.1 `memalign()`
- 3.2 `posix_memalign()`
- 4. 总结
- 7.2 在堆栈上分配内存:`alloca()`
- 1. `alloca()` 的基本用法
- 2. `alloca()` 的优势
- 2.1 自动释放内存
- 2.2 适用于信号处理程序
- 2.3 快速分配
- 3. `alloca()` 的局限性
- 3.1 堆栈溢出风险
- 3.2 不能在函数参数列表中调用 `alloca()`
- 4. `alloca()` 的可移植性
- 5. 总结
7.1 在堆上分配内存
当程序需要更多的内存时,它可以通过增加堆的大小来实现。堆是一段长度可变的连续虚拟内存,位于进程的未初始化数据段末尾之后。堆的增长是通过移动所谓的“program break”来实现的,这是堆的当前边界。
7.1.1 调整 program break:brk() 和 sbrk()
brk()
和 sbrk()
是两个系统调用,它们允许程序直接调整 program break 的位置,从而改变堆的大小。虽然现代的 C 语言程序更倾向于使用 malloc()
等高级内存分配函数,但了解 brk()
和 sbrk()
可以帮助我们理解底层的内存管理机制。
-
brk(void *addr)
- 作用:将 program break 设置为参数
addr
所指定的位置。 - 返回值:成功时返回 0,失败时返回 -1。
- 注意事项:由于虚拟内存是以页为单位分配的,
addr
实际会被四舍五入到下一个内存页的边界。如果尝试将 program break 设置为低于初始值的位置,可能会导致未定义行为,例如分段错误(SIGSEGV)。
- 作用:将 program break 设置为参数
-
sbrk(intptr_t increment)
- 作用:将 program break 在原有地址上增加
increment
字节。如果increment
为正数,则堆会增长;如果为负数,则堆会收缩。 - 返回值:成功时返回调整前的 program break 地址,失败时返回
(void *)-1
。 - 特殊情况:如果
increment
为 0,sbrk(0)
会返回当前的 program break 地址,而不做任何改变。这在调试或监控内存分配时非常有用。
- 作用:将 program break 在原有地址上增加
内存分配的工作过程
当程序调用 sbrk()
或 brk()
来增加 program break 时,内核并不会立即为这些新增的虚拟地址分配物理内存页。相反,内核会在进程首次访问这些地址时,通过页面错误(page fault)机制自动分配物理内存页。这种方式称为按需分页(demand paging),它可以提高内存的使用效率,因为只有当程序真正需要这些内存时,才会实际分配物理页。
限制和注意事项
-
资源限制:program break 的最大值受到进程资源限制的影响,特别是
RLIMIT_DATA
,它限制了数据段的最大大小。此外,内存映射区域、共享内存段和共享库的位置也会影响 program break 的上限。 -
不可逆性:虽然可以使用
sbrk()
减少 program break,但在某些系统上,减少后的内存可能不会立即返回给操作系统,而是保留在进程中,供后续的内存分配使用。
标准化状态
在 POSIX 标准中,brk()
和 sbrk()
被标记为 Legacy(传统),意味着它们在较新的标准中已经被废弃。尽管如此,它们仍然在许多 Unix-like 系统(如 Linux)中可用,并且在一些低级别的内存管理场景中仍然有应用。
7.1.2 在堆上分配内存:malloc()
和 free()
在 C 语言中,malloc()
和 free()
是用于动态内存分配和释放的函数。它们比底层的 brk()
和 sbrk()
更加高级、易用,并且更适合现代编程的需求。以下是关于这两个函数的详细介绍:
malloc()
函数
malloc()
函数用于在堆上分配指定大小的内存块,并返回指向这块内存的指针。
- 原型:
#include <stdlib.h> void *malloc(size_t size);
- 作用:在堆上分配
size
字节的内存,并返回指向这块内存起始位置的指针。 - 返回值:
- 成功时返回一个指向已分配内存的指针,类型为
void *
,可以赋值给任意类型的指针。 - 如果无法分配内存(例如因为内存不足),则返回
NULL
,并且设置errno
以指示错误。
- 成功时返回一个指向已分配内存的指针,类型为
- 对齐方式:
malloc()
返回的内存块总是按照适当的边界对齐,以便高效访问任何 C 语言数据结构。通常,这意味着内存块会按照 8 字节或 16 字节的边界对齐。 malloc(0)
的行为:根据 POSIX 标准(SUSv3),malloc(0)
可以返回NULL
或者返回一个小的、可以被free()
释放的内存块。在 Linux 中,malloc(0)
通常返回一个非空指针,这个指针可以安全地传递给free()
。
free()
函数
free()
函数用于释放之前由 malloc()
或其他堆分配函数(如 calloc()
、realloc()
)分配的内存。
- 原型:
#include <stdlib.h> void free(void *ptr);
- 作用:释放
ptr
指向的内存块。ptr
必须是之前由malloc()
、calloc()
或realloc()
分配的内存块的地址。 - 行为:
- 如果
ptr
是NULL
,free()
不做任何操作,这是合法的调用。 - 释放内存后,
ptr
指向的内存不再有效,继续使用它会导致未定义行为(例如段错误)。 free()
通常不会立即减少程序的program break
,而是将这块内存添加到空闲内存列表中,供后续的malloc()
调用重用。
- 如果
- 为什么
free()
不降低program break
:- 内存位置:被释放的内存块通常位于堆的中间,而不是堆的顶部。因此,直接降低
program break
是不可能的。 - 性能优化:频繁调用
sbrk()
系统调用来调整program break
会带来较大的开销,因此free()
通常会尽量避免这样做。 - 内存碎片:如果频繁释放和重新分配小块内存,可能会导致内存碎片化。通过将释放的内存块保留在空闲列表中,
malloc()
可以更好地管理这些碎片,提高内存利用率。
- 内存位置:被释放的内存块通常位于堆的中间,而不是堆的顶部。因此,直接降低
malloc()
和 free()
的优点
相比 brk()
和 sbrk()
,malloc()
和 free()
具有以下优点:
- 标准库支持:
malloc()
和free()
是 C 语言标准库的一部分,具有广泛的支持和兼容性。 - 多线程友好:它们可以在多线程环境中安全使用,而
brk()
和sbrk()
可能会导致竞态条件。 - 灵活的内存管理:
malloc()
可以分配任意大小的内存块,而brk()
和sbrk()
只能调整整个堆的大小。 - 自动内存回收:
free()
会将释放的内存块添加到空闲列表中,供后续的malloc()
调用重用,从而减少系统调用的频率和内存碎片。
示例程序:free()
对 program break
的影响
下面是一个示例程序,展示了 free()
如何影响 program break
。该程序分配了多个内存块,然后根据命令行参数释放部分或全部内存,并观察 program break
的变化。
#include "tlpi_hdr.h" // 假设这是一个包含常用头文件和宏的自定义头文件
#define MAX_ALLOCS 1000000
int main(int argc, char *argv[]) {
char *ptr[MAX_ALLOCS];
int freeStep, freeMin, freeMax, blockSize, numAllocs, j;
// 打印初始的 program break
printf("Initial program break: %10p\n", sbrk(0));
// 解析命令行参数
if (argc < 3 || strcmp(argv[1], "--help") == 0) {
usageErr("%s num-allocs block-size [step [min [max]]]\n", argv[0]);
}
numAllocs = getInt(argv[1], GN_GT_0, "num-allocs");
if (numAllocs > MAX_ALLOCS) {
cmdLineErr("num-allocs > %d\n", MAX_ALLOCS);
}
blockSize = getInt(argv[2], GN_GT_0 | GN_ANY_BASE, "block-size");
freeStep = (argc > 3) ? getInt(argv[3], GN_GT_0, "step") : 1;
freeMin = (argc > 4) ? getInt(argv[4], GN_GT_0, "min") : 1;
freeMax = (argc > 5) ? getInt(argv[5], GN_GT_0, "max") : numAllocs;
if (freeMax > numAllocs) {
cmdLineErr("free-max > num-allocs\n");
}
// 分配内存
printf("Allocating %d * %d bytes\n", numAllocs, blockSize);
for (j = 0; j < numAllocs; j++) {
ptr[j] = malloc(blockSize);
if (ptr[j] == NULL) {
errExit("malloc");
}
printf("Program break is now: %10p\n", sbrk(0));
}
// 释放内存
printf("Freeing blocks from %d to %d in steps of %d\n", freeMin, freeMax, freeStep);
for (j = freeMin - 1; j < freeMax; j += freeStep) {
free(ptr[j]);
}
// 打印释放后的 program break
printf("After free(), program break is: %10p\n", sbrk(0));
exit(EXIT_SUCCESS);
}
运行结果分析
-
释放所有内存块:
- 当程序释放所有内存块后,
program break
的位置仍然保持在分配后的高位。这是因为free()
并没有立即调用sbrk()
来减少program break
,而是将这些内存块添加到空闲列表中,供后续的malloc()
调用重用。
$ ./free_and_sbrk 1000 10240 2 Initial program break: 0x804a6bc Allocating 1000 * 10240 bytes Program break is now: 0x8a13000 Freeing blocks from 1 to 1000 in steps of 2 After free(), program break is: 0x8a13000
- 当程序释放所有内存块后,
-
释放除最后一块外的所有内存块:
- 即使释放了大部分内存块,
program break
仍然保持在高位。这是因为最后一块内存仍然被占用,free()
无法将program break
降低到这块内存之前。
$ ./free_and_sbrk 1000 10240 1 1 999 Initial program break: 0x804a6bc Allocating 1000 * 10240 bytes Program break is now: 0x8a13000 Freeing blocks from 1 to 999 in steps of 1 After free(), program break is: 0x8a13000
- 即使释放了大部分内存块,
-
释放堆顶部的连续内存块:
- 当释放的是堆顶部的连续内存块时,
free()
会检测到这一情况,并调用sbrk()
来降低program break
。这是因为这些内存块已经不再被使用,且它们位于堆的顶部,可以直接缩小堆的大小。
$ ./free_and_sbrk 1000 10240 1 500 1000 Initial program break: 0x804a6bc Allocating 1000 * 10240 bytes Program break is now: 0x8a13000 Freeing blocks from 500 to 1000 in steps of 1 After free(), program break is: 0x852b000
- 当释放的是堆顶部的连续内存块时,
总结
malloc()
和free()
是 C 语言中常用的动态内存分配和释放函数,提供了比brk()
和sbrk()
更高级、更灵活的接口。malloc()
分配的内存块会被对齐,以便高效访问各种数据结构。free()
通常不会立即减少program break
,而是将释放的内存块添加到空闲列表中,供后续的malloc()
调用重用。只有当释放的内存块位于堆的顶部且足够大时,free()
才会调用sbrk()
来降低program break
。- 通过合理的内存管理和释放策略,
malloc()
和free()
可以有效地减少内存碎片,提高内存利用率。
7.1.3 malloc()
和 free()
的实现
malloc()
和 free()
是 C 语言中用于动态内存管理的核心函数。它们的实现涉及复杂的内存分配和回收机制,理解这些实现细节有助于避免常见的编程错误,并优化内存使用。以下是关于 malloc()
和 free()
实现的详细说明。
1. malloc()
的实现
malloc()
的主要任务是在堆上分配指定大小的内存块。它的实现通常包括以下几个步骤:
-
扫描空闲内存列表:
malloc()
首先会检查之前由free()
释放的空闲内存块列表(也称为“空闲链表”),寻找一个足够大的空闲内存块。- 扫描策略:不同的实现可能会采用不同的扫描策略来选择合适的内存块。常见的策略包括:
- First-fit:从头开始扫描空闲链表,找到第一个足够大的空闲块。
- Best-fit:遍历整个空闲链表,选择最接近所需大小的空闲块。
- Worst-fit:选择最大的空闲块,虽然这种策略较少使用。
- Next-fit:从上次分配的位置继续扫描,直到找到合适的空间。
-
分割大块内存:
- 如果找到的空闲块比所需的内存稍大,
malloc()
会将这块内存分割成两部分:一部分返回给调用者,另一部分保留在空闲链表中,供后续的malloc()
调用使用。 - 最小块大小:为了减少内存碎片,
malloc()
通常不会分配非常小的内存块。它会确保每个分配的内存块至少有一个最小的固定大小(例如 16 字节或 32 字节)。
- 如果找到的空闲块比所需的内存稍大,
-
扩展堆:
- 如果在空闲链表中找不到足够大的空闲块,
malloc()
会调用sbrk()
系统调用来扩展堆,增加更多的可用内存。 - 批量分配:为了减少对
sbrk()
的频繁调用,malloc()
通常会一次性申请比实际需要更多的内存(通常是虚拟内存页大小的倍数)。多余的内存会被放入空闲链表中,供后续的malloc()
调用使用。
- 如果在空闲链表中找不到足够大的空闲块,
-
记录内存块大小:
malloc()
在分配内存时,会在实际返回给用户的内存块之前额外分配几个字节,用于存储该内存块的大小信息。这个大小信息通常位于内存块的起始位置,而用户实际获得的指针则指向这个大小信息之后的位置。- 这种设计使得
free()
可以在释放内存时知道这块内存的实际大小,从而正确地将其放回空闲链表中。
2. free()
的实现
free()
的主要任务是将已分配的内存块归还给系统或放入空闲链表中,以便后续的 malloc()
调用可以重用这些内存。它的实现包括以下几个步骤:
-
获取内存块大小:
- 当
free()
接收到一个指针时,它会通过指针减去一个小的偏移量(通常是 8 字节或 16 字节),找到内存块的起始位置,并从中读取之前由malloc()
存储的内存块大小信息。 - 这样,
free()
就能准确地知道这块内存的大小,并将其正确地放回空闲链表中。
- 当
-
合并相邻的空闲块:
free()
会检查新释放的内存块是否与空闲链表中的其他空闲块相邻。如果是相邻的,free()
会将这些相邻的空闲块合并成一个更大的空闲块,以减少内存碎片。- 双向链表:空闲链表通常是一个双向链表,每个空闲块都包含指向前一个和后一个空闲块的指针。这样可以方便地进行合并操作。
-
调整
program break
:- 如果释放的内存块位于堆的顶部,并且空闲链表中有足够大的连续空闲块,
free()
可能会调用sbrk()
来减少program break
,从而释放不再使用的内存回到操作系统。 - 阈值:
free()
并不会每次释放内存时都调用sbrk()
,而是只有当空闲的内存块足够大(通常是 128KB 或更大)时才会这样做。这减少了对sbrk()
的频繁调用,提高了性能。
- 如果释放的内存块位于堆的顶部,并且空闲链表中有足够大的连续空闲块,
3. 内存管理和常见错误
尽管 malloc()
和 free()
提供了相对简单的接口,但在使用时仍然容易犯下各种编程错误。理解它们的实现可以帮助我们避免这些错误。以下是一些常见的错误及其原因:
-
越界访问:
- 问题:程序可能会错误地访问分配的内存块之外的区域,导致覆盖内存块的大小信息或其他关键数据结构。
- 原因:错误的指针运算、循环更新内存内容时的“off-by-one”错误等。
- 解决方法:始终确保只在分配的内存范围内进行操作,避免越界访问。
-
重复释放:
- 问题:程序可能会多次释放同一块内存,导致不可预知的行为,甚至引发段错误(SIGSEGV)。
- 原因:程序员可能不小心多次调用
free()
,或者在多线程环境中多个线程同时释放同一块内存。 - 解决方法:确保每块内存只被释放一次。可以在释放后将指针设置为
NULL
,以防止意外的重复释放。
-
释放未分配的内存:
- 问题:程序可能会尝试释放从未分配过的内存,或者使用非
malloc()
系列函数分配的内存。 - 原因:程序员可能误用了
free()
,传递了无效的指针。 - 解决方法:确保只释放由
malloc()
、calloc()
或realloc()
分配的内存块。
- 问题:程序可能会尝试释放从未分配过的内存,或者使用非
-
内存泄漏:
- 问题:程序可能会分配内存但忘记释放,导致堆不断增长,最终耗尽可用的虚拟内存。
- 原因:程序员没有正确管理内存生命周期,特别是在长时间运行的程序(如 shell 或网络守护进程)中。
- 解决方法:确保在不再需要内存时及时调用
free()
,并定期检查是否存在内存泄漏。
-
悬空指针:
- 问题:程序可能会在释放内存后继续使用已经无效的指针,导致不可预知的行为。
- 原因:程序员在释放内存后没有将指针设置为
NULL
,或者在多线程环境中共享指针。 - 解决方法:在释放内存后立即将指针设置为
NULL
,并在使用指针前进行检查。
4. 总结
malloc()
和free()
的实现涉及到复杂的内存管理和优化策略,旨在提高内存分配的效率并减少碎片。malloc()
通过扫描空闲链表、分割大块内存和扩展堆来满足内存分配请求,而free()
则通过合并相邻的空闲块和调整program break
来回收内存。- 为了避免常见的编程错误,开发者应遵守以下规则:
- 不要越界访问分配的内存。
- 不要重复释放同一块内存。
- 不要释放未分配的内存。
- 及时释放不再需要的内存,避免内存泄漏。
- 在释放内存后将指针设置为
NULL
,防止悬空指针问题。
通过理解 malloc()
和 free()
的内部工作原理,开发者可以编写更高效、更可靠的代码,并避免潜在的内存管理问题。
4. malloc
调试工具和库
在开发过程中,内存管理错误(如越界访问、重复释放、内存泄漏等)可能会导致难以调试的问题。为了帮助开发者发现和修复这些问题,许多工具和库提供了对 malloc
和 free
的调试功能。以下是 glibc 提供的调试工具以及一些常见的第三方调试库。
1. glibc 提供的 malloc
调试工具
glibc 提供了多种内置的调试工具,可以帮助开发者检测和诊断内存管理问题。这些工具包括:
1.1 mtrace()
和 muntrace()
-
功能:
mtrace()
和muntrace()
函数用于跟踪程序中的内存分配调用。它们与环境变量MALLOC_TRACE
配合使用,后者定义了一个文件名,用于记录所有对malloc
函数包中函数的调用。 -
使用方法:
- 在程序中调用
mtrace()
来启用跟踪,调用muntrace()
来关闭跟踪。 - 设置环境变量
MALLOC_TRACE
,指定跟踪信息的输出文件。 - 使用
mtrace
脚本来分析生成的跟踪文件,生成易于理解的报告。
export MALLOC_TRACE=/path/to/tracefile ./your_program mtrace ./your_program /path/to/tracefile
- 在程序中调用
-
注意事项:
mtrace()
会在程序启动时检查MALLOC_TRACE
环境变量,并尝试打开指定的文件进行写入。- 设置用户 ID 和设置组 ID 的程序会忽略对
mtrace()
的调用,出于安全原因。
1.2 mcheck()
和 mprobe()
-
功能:
mcheck()
和mprobe()
函数允许程序对已分配的内存块进行一致性检查。它们可以在程序运行时捕获内存管理错误,例如越界写操作。 -
使用方法:
- 在程序启动时调用
mcheck()
来启用一致性检查。 - 使用
mprobe()
检查特定的内存块是否有效。
#include <mcheck.h> int main() { mcheck(NULL); // 启用一致性检查 void *ptr = malloc(100); mprobe(ptr); // 检查 ptr 是否有效 free(ptr); return 0; }
- 在程序启动时调用
-
链接选项:使用
mcheck
库时,必须在编译时链接-lmcheck
选项。
1.3 MALLOC_CHECK_
环境变量
-
功能:
MALLOC_CHECK_
环境变量提供了类似于mcheck()
和mprobe()
的功能,但无需修改或重新编译程序。通过设置不同的值,可以控制程序对内存分配错误的响应方式。 -
设置选项:
0
:忽略错误。1
:在标准错误输出(stderr
)中打印诊断信息。2
:调用abort()
终止程序。
export MALLOC_CHECK_=2 ./your_program
-
优点:快速、易用,且不需要修改代码。
-
局限性:并非所有的内存分配和释放错误都能被
MALLOC_CHECK_
捕获,它主要检测常见错误。
2. 第三方 malloc
调试库
除了 glibc 提供的工具外,还有一些第三方库提供了更强大的内存调试功能。这些库通常会替代标准的 malloc
实现,并在运行时捕获各种内存管理错误。以下是一些常用的第三方调试库:
2.1 Electric Fence
-
功能:Electric Fence 是一个简单的内存调试库,它通过将每个分配的内存块放在独立的页面边界上,来捕获越界访问错误。当程序试图访问未分配的内存时,Electric Fence 会触发段错误(SIGSEGV),从而帮助定位问题。
-
使用方法:
- 编译时链接
libefence.so
。 - 运行程序时设置环境变量
EFENCE
来控制其行为。
gcc -g -o your_program your_program.c -lefence export EFENCE=1 ./your_program
- 编译时链接
2.2 dmalloc
-
功能:dmalloc 是一个功能丰富的内存调试库,提供了详细的内存分配和释放跟踪功能。它可以检测多种内存管理错误,包括越界访问、重复释放、内存泄漏等。
-
使用方法:
- 编译时链接
libdmalloc.so
。 - 运行程序时设置环境变量
DMALLOC_OPTIONS
来控制其行为。
gcc -g -o your_program your_program.c -ldmalloc export DMALLOC_OPTIONS=debug=1,log=stdout ./your_program
- 编译时链接
2.3 Valgrind
-
功能:Valgrind 是一个功能强大的内存调试和性能分析工具。它不仅可以检测内存管理错误,还可以发现其他类型的错误,如未初始化的内存访问、条件竞争等。Valgrind 通过模拟 CPU 执行程序,因此可以提供非常详细的错误报告。
-
使用方法:
- 无需修改代码,直接运行程序。
valgrind --leak-check=full ./your_program
-
优点:能够发现多种类型的错误,不仅限于
malloc
和free
。 -
缺点:运行速度较慢,适合开发和调试阶段,不适合生产环境。
2.4 Insure++
-
功能:Insure++ 是一个商业化的内存调试工具,提供了类似 Valgrind 的功能,但具有更好的性能和更多的特性。它可以检测内存泄漏、越界访问、未初始化的内存访问等问题。
-
使用方法:
- 无需修改代码,直接运行程序。
insure ./your_program
-
优点:性能优于 Valgrind,适合大型项目。
-
缺点:需要购买许可证。
3. 控制和监测 malloc
函数包
glibc 还提供了一些非标准的函数,用于控制和监测 malloc
函数包的行为。这些函数虽然不具有良好的可移植性,但在某些情况下仍然非常有用。
3.1 mallopt()
-
功能:
mallopt()
函数用于修改malloc
的内部参数,以控制其行为。例如,可以通过mallopt()
设置在调用sbrk()
收缩堆之前,空闲列表中必须保留的最小可释放内存空间。 -
使用方法:
mallopt(int param, int value)
,其中param
是要修改的参数,value
是新的值。
常见的参数包括:
M_TRIM_THRESHOLD
:设置在调用sbrk()
收缩堆之前,空闲列表中必须保留的最小可释放内存空间。M_TOP_PAD
:设置从堆中分配的内存块大小的上限,超出上限的内存块将使用mmap()
系统调用分配。
3.2 mallinfo()
-
功能:
mallinfo()
函数返回一个结构体,其中包含malloc
分配内存的各种统计数据。这些数据可以帮助开发者了解程序的内存使用情况。 -
使用方法:
#include <malloc.h> struct mallinfo mi = mallinfo(); printf("Total non-mmapped bytes (arena): %d\n", mi.arena); printf("Number of free chunks (ordblks): %d\n", mi.ordblks);
-
注意事项:
mallinfo()
的接口和返回值可能因不同的 glibc 版本而有所不同,因此在使用时需要注意版本兼容性。
4. 总结
- glibc 内置工具:
mtrace()
、mcheck()
、MALLOC_CHECK_
等工具可以帮助开发者快速发现常见的内存管理错误,适合简单的调试场景。 - 第三方调试库:如 Electric Fence、dmalloc、Valgrind 和 Insure++ 提供了更强大的功能,能够发现更多类型的错误,适合复杂的调试需求。
- 控制和监测函数:
mallopt()
和mallinfo()
可以帮助开发者控制malloc
的行为并监控内存使用情况,但需要注意其可移植性问题。
通过使用这些工具和库,开发者可以更有效地发现和修复内存管理问题,确保程序的稳定性和可靠性。
5. 在堆上分配内存的其他方法
除了 malloc()
,C 标准库还提供了其他几个用于在堆上分配内存的函数,每个函数都有其特定的用途和行为。以下是这些函数的详细介绍:
1. calloc()
calloc()
函数用于为一组相同类型的对象分配内存,并将分配的内存初始化为零。
-
原型:
#include <stdlib.h> void *calloc(size_t num, size_t size);
-
作用:为
num
个大小为size
的对象分配内存,并将分配的内存初始化为零。 -
返回值:
- 成功时返回指向已分配内存块的指针。
- 如果无法分配内存,则返回
NULL
。
-
特点:
calloc()
与malloc()
类似,但会自动将分配的内存初始化为零。- 适用于需要初始化为零的数组或结构体。
-
示例:
int *arr = (int *)calloc(10, sizeof(int)); if (arr == NULL) { // 处理内存分配失败的情况 } // arr 现在是一个包含 10 个整数的数组,所有元素都初始化为 0 free(arr);
2. realloc()
realloc()
函数用于调整已分配内存块的大小。它可以增加或减少内存块的大小,甚至移动内存块的位置。
-
原型:
#include <stdlib.h> void *realloc(void *ptr, size_t size);
-
作用:调整由
ptr
指向的内存块的大小为size
字节。ptr
必须是之前由malloc()
、calloc()
或realloc()
分配的内存块的指针。 -
返回值:
- 成功时返回指向调整后内存块的指针。如果内存块被移动,返回的指针可能与原来的
ptr
不同。 - 如果无法调整内存大小,则返回
NULL
,而原始的ptr
指针仍然有效。
- 成功时返回指向调整后内存块的指针。如果内存块被移动,返回的指针可能与原来的
-
特点:
- 如果
ptr
为NULL
,realloc(NULL, size)
等效于malloc(size)
。 - 如果
size
为 0,realloc(ptr, 0)
等效于free(ptr)
后调用malloc(0)
。 realloc()
可能会移动内存块,因此必须使用返回的指针来访问调整后的内存。- 如果
realloc()
增加了内存块的大小,新增的部分不会被初始化,内容是未定义的。
- 如果
-
示例:
int *arr = (int *)malloc(5 * sizeof(int)); if (arr == NULL) { // 处理内存分配失败的情况 } // 尝试将数组大小增加到 10 个元素 int *new_arr = realloc(arr, 10 * sizeof(int)); if (new_arr == NULL) { // 处理内存调整失败的情况 free(arr); } else { arr = new_arr; // arr 现在是一个包含 10 个整数的数组 } free(arr);
-
注意事项:
realloc()
可能会移动内存块,因此任何指向原内存块内部的指针在调用realloc()
后都可能失效。- 应尽量避免频繁调用
realloc()
,因为它可能会导致性能问题,尤其是在内存块位于堆中部且需要复制数据的情况下。
3. memalign()
和 posix_memalign()
memalign()
和 posix_memalign()
用于分配对齐的内存,即内存块的起始地址是对齐到指定边界的整数倍。这对某些应用程序(如多线程编程、硬件加速等)非常有用。
3.1 memalign()
-
原型:
#include <malloc.h> void *memalign(size_t boundary, size_t size);
-
作用:分配
size
字节的内存,起始地址是对boundary
的整数倍对齐。boundary
必须是 2 的幂次方。 -
返回值:
- 成功时返回指向已分配内存块的指针。
- 如果无法分配内存,则返回
NULL
。
-
特点:
memalign()
并非在所有 UNIX 实现中都存在,某些系统可能需要包含<malloc.h>
而不是<stdlib.h>
。memalign()
返回的内存块应该使用free()
来释放。
3.2 posix_memalign()
-
原型:
#include <stdlib.h> int posix_memalign(void **memptr, size_t alignment, size_t size);
-
作用:分配
size
字节的内存,起始地址是对alignment
的整数倍对齐。alignment
必须是sizeof(void*)
的倍数,并且是 2 的幂次方。 -
返回值:
- 成功时返回 0,并通过
memptr
参数返回指向已分配内存块的指针。 - 如果无法分配内存或参数无效,则返回一个错误码(如
EINVAL
或ENOMEM
)。
- 成功时返回 0,并通过
-
特点:
posix_memalign()
是 POSIX 标准的一部分,具有更好的可移植性。posix_memalign()
返回的内存块也应该使用free()
来释放。
-
示例:
void *ptr; int ret = posix_memalign(&ptr, 4096, 65536); // 分配 65536 字节的内存,并与 4096 字节边界对齐 if (ret != 0) { // 处理内存分配失败的情况 } // 使用 ptr free(ptr);
-
注意事项:
posix_memalign()
的返回值是错误码,而不是指针,因此需要通过memptr
参数获取分配的内存地址。alignment
必须是sizeof(void*)
的倍数,并且是 2 的幂次方。在大多数硬件架构上,sizeof(void*)
为 4 或 8 字节。
4. 总结
calloc()
:用于为一组相同类型的对象分配内存,并将内存初始化为零。适用于需要初始化为零的数组或结构体。realloc()
:用于调整已分配内存块的大小。可以增加或减少内存块的大小,甚至移动内存块的位置。需要注意的是,realloc()
可能会移动内存块,因此必须使用返回的指针来访问调整后的内存。memalign()
和posix_memalign()
:用于分配对齐的内存,适用于需要特定对齐要求的应用程序。posix_memalign()
是 POSIX 标准的一部分,具有更好的可移植性,推荐优先使用。
通过合理使用这些函数,开发者可以根据具体需求选择合适的内存分配方式,确保程序的高效性和稳定性。
7.2 在堆栈上分配内存:alloca()
alloca()
是一个用于在堆栈上动态分配内存的函数,与 malloc()
不同的是,它不是从堆中分配内存,而是通过扩展当前函数的栈帧来分配内存。由于栈帧位于堆栈的顶部,因此可以通过调整堆栈指针来实现内存分配。alloca()
分配的内存具有自动释放的特性,当调用它的函数返回时,分配的内存会随着栈帧的移除而自动释放。
1. alloca()
的基本用法
-
原型:
#include <alloca.h> void *alloca(size_t size);
-
作用:在堆栈上分配
size
字节的内存,并返回指向已分配内存块的指针。 -
特点:
- 自动释放:由
alloca()
分配的内存会在调用它的函数返回时自动释放,无需手动调用free()
。 - 不可调整大小:不能使用
realloc()
来调整由alloca()
分配的内存大小。 - 快速分配:
alloca()
的实现通常被编译器优化为内联代码,直接通过调整堆栈指针来分配内存,因此速度比malloc()
更快。 - 不需要维护空闲列表:
alloca()
不需要像malloc()
那样维护空闲内存块列表,减少了管理开销。
- 自动释放:由
-
示例:
#include <stdio.h> #include <alloca.h> void example_function(int n) { // 使用 alloca() 分配 n 个整数的内存 int *arr = (int *)alloca(n * sizeof(int)); for (int i = 0; i < n; i++) { arr[i] = i; } // 打印数组内容 for (int i = 0; i < n; i++) { printf("%d ", arr[i]); } printf("\n"); } int main() { example_function(5); // 调用 example_function,分配的内存会在函数返回时自动释放 return 0; }
2. alloca()
的优势
2.1 自动释放内存
alloca()
分配的内存会在调用它的函数返回时自动释放,这使得编写代码更加简单,尤其是在函数有多个返回路径的情况下。开发者不需要担心在每个返回路径中都调用 free()
来释放内存,从而减少了内存泄漏的风险。
2.2 适用于信号处理程序
在信号处理程序中调用 longjmp()
或 siglongjmp()
以执行非局部跳转时,alloca()
的自动释放特性非常有用。如果使用 malloc()
分配内存,可能会导致内存泄漏,因为 longjmp()
会跳过正常的函数返回路径,导致 free()
没有机会被调用。而 alloca()
分配的内存会随着栈帧的移除而自动释放,避免了这一问题。
2.3 快速分配
alloca()
的实现通常被编译器优化为内联代码,直接通过调整堆栈指针来分配内存,因此速度比 malloc()
更快。对于频繁分配和释放小块内存的情况,alloca()
可能是一个更好的选择。
3. alloca()
的局限性
3.1 堆栈溢出风险
alloca()
分配的内存来自堆栈,而不是堆。堆栈的大小通常是有限的,因此如果分配的内存过大,可能会导致堆栈溢出。堆栈溢出会导致程序行为不可预知,甚至可能触发段错误(SIGSEGV)。因此,使用 alloca()
时应谨慎,确保分配的内存大小适中。
- 示例:
void dangerous_function() { // 分配过大的内存可能导致堆栈溢出 char *large_buffer = (char *)alloca(1024 * 1024 * 1024); // 1GB // 程序可能会崩溃或行为异常 }
3.2 不能在函数参数列表中调用 alloca()
alloca()
不能在函数的参数列表中调用,因为这样会导致 alloca()
分配的堆栈空间出现在当前函数参数的空间内,而函数参数是位于栈帧内的固定位置。相反,应该将 alloca()
的调用放在函数体内部。
-
错误示例:
void bad_function(int n, void *ptr = alloca(n)) { // 错误:不能在函数参数列表中调用 alloca() }
-
正确示例:
void good_function(int n) { void *ptr = alloca(n); // 正确:在函数体内部调用 alloca() // 使用 ptr }
4. alloca()
的可移植性
虽然 alloca()
不是 POSIX 标准(SUSv3)的一部分,但大多数 UNIX 实现都提供了该函数,因此它在实际应用中具有较好的可移植性。不过,不同系统对 alloca()
的声明头文件可能有所不同:
-
glibc 和其他一些 UNIX 实现:需要包含
<stdlib.h>
或<alloca.h>
。 -
BSD 衍生版本:通常需要包含
<alloca.h>
。 -
示例:
#include <stdlib.h> // 或者 #include <alloca.h> void *ptr = alloca(100); // 分配 100 字节的内存
5. 总结
alloca()
:用于在堆栈上动态分配内存,分配的内存会在调用它的函数返回时自动释放。- 优点:
- 自动释放内存,简化代码编写。
- 适用于信号处理程序中的非局部跳转,避免内存泄漏。
- 分配速度快,适合频繁分配和释放小块内存。
- 局限性:
- 存在堆栈溢出的风险,应谨慎使用。
- 不能在函数参数列表中调用
alloca()
。 - 不是 POSIX 标准的一部分,但在大多数 UNIX 实现中可用。
通过合理使用 alloca()
,开发者可以在某些场景下获得更高效的内存分配和更简洁的代码结构。然而,由于其潜在的堆栈溢出风险,建议在使用时保持谨慎,确保分配的内存大小适中。