好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 brk
和 sbrk
函数,它们是用于直接控制进程堆 (heap) 大小的底层系统调用。虽然在现代编程中很少直接使用它们(通常由 malloc
/free
等库函数管理),但理解它们有助于深入理解进程内存布局和动态内存分配的原理。
1. 函数介绍 链接到标题
brk
和 sbrk
是 Linux 系统调用,它们直接操作进程的程序中断点 (program break),从而改变进程堆 (heap) 的大小。
- 程序中断点 (program break): 这是进程数据段(包括初始化数据
.data
、未初始化数据.bss
)的末尾,也是堆的起始边界。堆是从这个点开始向上(向更高内存地址)增长的。 - 堆 (heap): 这是进程内存空间中用于动态内存分配(如
malloc
,calloc
)的区域。通过移动程序中断点,可以增加或减少堆的可用空间。
简单来说,brk
和 sbrk
是调整“堆顶”的工具。
brk
: 直接将程序中断点设置到指定的地址。sbrk
: 将程序中断点增加(或减少)指定的字节数,并返回之前的程序中断点地址。
2. 函数原型 链接到标题
#include <unistd.h> // 必需
// 设置程序中断点到指定地址
int brk(void *addr);
// 将程序中断点增加 increment 字节
void *sbrk(intptr_t increment);
3. 功能 链接到标题
brk(void *addr)
:- 尝试将程序中断点设置为
addr
。 - 如果
addr
大于当前中断点,堆会增长。 - 如果
addr
小于当前中断点,堆会收缩。 - 内核会相应地分配或释放物理内存页。
- 尝试将程序中断点设置为
sbrk(intptr_t increment)
:- 将程序中断点增加
increment
字节。 - 如果
increment
为正,堆增长。 - 如果
increment
为负,堆收缩。 - 返回调整前的程序中断点地址(即旧的堆顶)。
- 如果
increment
为 0,则不改变堆大小,但仍返回当前的程序中断点地址。
- 将程序中断点增加
4. 参数 链接到标题
brk
:void *addr
: 期望设置的新的程序中断点的绝对地址。
sbrk
:intptr_t increment
: 相对于当前程序中断点的偏移量(以字节为单位)。可以是正数(增长)、负数(收缩)或零(查询)。
5. 返回值 链接到标题
brk
:- 成功时: 返回 0。
- 失败时: 返回 -1,并设置
errno
(例如ENOMEM
内存不足)。
sbrk
:- 成功时: 返回调整前的程序中断点地址。
- 失败时: 返回
(void *) -1
(即MAP_FAILED
常量,但这与 mmap 无关),并设置errno
。
6. 相似函数,或关联函数 链接到标题
malloc
,free
,calloc
,realloc
: 这些标准 C 库函数是更高级、更安全、更便携的动态内存分配接口。它们通常在底层使用brk
/sbrk
(对于小内存块)和mmap
(对于大内存块或避免堆碎片)来管理内存。mmap
,munmap
: 另一种动态内存分配方式,特别是对于大块内存或需要特殊权限(如可执行)的内存。mmap
分配的内存通常不在由brk
管理的堆区域。getrlimit
,setrlimit
(RLIMIT_DATA): 可以用来查询和设置进程数据段(包括堆)的最大大小限制。
7. 示例代码 链接到标题
示例 1:使用 sbrk
进行简单的内存分配和释放
链接到标题
这个例子演示了如何直接使用 sbrk
来分配和释放内存块。
#include <unistd.h> // sbrk, brk
#include <stdio.h> // perror, printf
#include <stdlib.h> // exit
// 简单的内存分配函数,使用 sbrk
void* my_malloc(size_t size) {
void *previous_brk;
// sbrk(0) 返回当前的程序中断点
previous_brk = sbrk(0);
printf("Current brk before allocation: %p\n", previous_brk);
// 请求增加 size 字节的内存
if (sbrk(size) == (void *) -1) {
perror("sbrk failed in my_malloc");
return NULL; // Allocation failed
}
printf("Allocated %zu bytes. New brk: %p\n", size, sbrk(0));
// 返回旧的中断点,这就是我们分配的内存块的起始地址
return previous_brk;
}
// 简单的内存释放函数,使用 brk
int my_free(void *ptr, size_t size) {
void *current_brk = sbrk(0);
void *expected_brk = (char *)ptr + size; // 计算释放后应有的中断点
printf("Attempting to free memory at %p (size %zu)\n", ptr, size);
printf("Current brk: %p, Expected brk after free: %p\n", current_brk, expected_brk);
// 为了简化,只允许释放最近一次分配的内存块
// (即堆顶的内存块)
if (ptr == NULL) {
printf("Cannot free NULL pointer.\n");
return -1;
}
if ((char*)ptr + size != current_brk) {
fprintf(stderr, "Error: Can only free the most recently allocated block (stack-like behavior).\n");
fprintf(stderr, "This simple 'my_free' only supports freeing the top of the heap.\n");
return -1; // Free failed
}
// 将程序中断点设置回分配前的位置
if (brk(ptr) == -1) {
perror("brk failed in my_free");
return -1; // Free failed
}
printf("Freed %zu bytes. New brk: %p\n", size, sbrk(0));
return 0; // Success
}
int main() {
char *block1, *block2;
size_t size1 = 1024;
size_t size2 = 512;
printf("--- Allocating block 1 ---\n");
block1 = (char *)my_malloc(size1);
if (block1 == NULL) {
exit(EXIT_FAILURE);
}
// 使用分配的内存
for (size_t i = 0; i < size1 && i < 20; ++i) {
block1[i] = 'A' + (i % 26);
}
printf("Written data to block1 (first 20 chars): ");
for(int i = 0; i < 20; ++i) printf("%c", block1[i]);
printf("\n");
printf("\n--- Allocating block 2 ---\n");
block2 = (char *)my_malloc(size2);
if (block2 == NULL) {
// 尝试释放 block1
my_free(block1, size1);
exit(EXIT_FAILURE);
}
// 使用分配的内存
for (size_t i = 0; i < size2 && i < 20; ++i) {
block2[i] = 'z' - (i % 26);
}
printf("Written data to block2 (first 20 chars): ");
for(int i = 0; i < 20; ++i) printf("%c", block2[i]);
printf("\n");
// --- 释放内存 ---
// 注意:由于 my_free 的简单实现,必须按相反顺序释放
// 先释放 block2
printf("\n--- Freeing block 2 ---\n");
if (my_free(block2, size2) != 0) {
fprintf(stderr, "Failed to free block2\n");
// 尝试释放 block1 anyway
my_free(block1, size1);
exit(EXIT_FAILURE);
}
// 再释放 block1
printf("\n--- Freeing block 1 ---\n");
if (my_free(block1, size1) != 0) {
fprintf(stderr, "Failed to free block1\n");
exit(EXIT_FAILURE);
}
printf("\nAll memory allocated and freed successfully.\n");
return 0;
}
代码解释:
my_malloc(size_t size)
:- 调用
sbrk(0)
获取当前的程序中断点地址。 - 调用
sbrk(size)
请求增加size
字节的内存。 - 检查
sbrk
是否失败。 - 返回之前的中断点地址,这就是新分配内存块的起始地址。
- 调用
my_free(void *ptr, size_t size)
:- 为了简化,这个实现只允许释放最近一次分配的内存块(模拟栈式分配)。
- 它检查要释放的指针
ptr
加上大小size
是否等于当前的程序中断点。 - 如果是,则调用
brk(ptr)
将程序中断点设置回ptr
,从而释放这块内存。 - 检查
brk
是否失败。
main
函数:- 演示了分配两个内存块,并按正确顺序(后进先出)释放它们。
- 如果释放顺序错误(例如先释放
block1
再释放block2
),my_free
会报错。
示例 2:使用 sbrk(0)
获取当前堆顶
链接到标题
这个例子展示了如何使用 sbrk(0)
来查询当前的程序中断点,即堆顶的位置。
#include <unistd.h> // sbrk
#include <stdio.h> // printf
#include <stdlib.h> // malloc, free
void print_brk(const char *msg) {
void *current_brk = sbrk(0);
printf("%s: Current program break (heap top) is at %p\n", msg, current_brk);
}
int main() {
void *initial_brk, *after_malloc_brk, *after_free_brk;
print_brk("At program start");
initial_brk = sbrk(0);
// 分配一些内存 (使用标准 malloc)
// malloc 可能使用 brk/sbrk 或 mmap,取决于实现和块大小
char *ptr = malloc(1024 * 1024); // 分配 1MB
if (ptr == NULL) {
perror("malloc");
exit(EXIT_FAILURE);
}
print_brk("After malloc(1MB)");
after_malloc_brk = sbrk(0);
// 使用分配的内存
ptr[0] = 'X';
ptr[1024 * 1024 - 1] = 'Y';
// 释放内存
free(ptr);
print_brk("After free(1MB)");
after_free_brk = sbrk(0);
printf("\n--- Summary ---\n");
printf("Initial brk: %p\n", initial_brk);
printf("Brk after malloc: %p (diff: %ld bytes)\n",
after_malloc_brk, (char*)after_malloc_brk - (char*)initial_brk);
printf("Brk after free: %p (diff: %ld bytes from malloc brk)\n",
after_free_brk, (char*)after_free_brk - (char*)after_malloc_brk);
// 注意:free() 不一定会立即将内存还给系统(降低 brk),
// 特别是对小块内存或为了性能考虑。这里分配的是 1MB,有可能观察到变化。
// 但不能保证一定会变化。
return 0;
}
代码解释:
- 定义了一个
print_brk
函数,它调用sbrk(0)
并打印返回的地址。 - 在程序开始、
malloc
之后、free
之后分别调用print_brk
。 - 通过比较这些地址,可以观察
malloc
和free
对程序中断点(堆顶)的影响。 - 重要提示:
malloc
和free
的具体实现很复杂。malloc
可能使用brk
/sbrk
(对于较小或连续的请求)或mmap
(对于较大请求或避免堆碎片)。free
也不会立即将内存还给操作系统,而是可能保留在内部池中供后续malloc
使用。因此,这个例子主要是演示如何查询堆顶,而不一定能观察到free
后堆顶的显著下降。
示例 3:直接使用 brk
设置堆大小
链接到标题
这个例子展示了如何使用 brk
直接设置程序中断点到一个绝对地址。
#include <unistd.h> // sbrk, brk
#include <stdio.h> // perror, printf
#include <stdlib.h> // exit
int main() {
void *initial_brk, *target_brk, *final_brk;
initial_brk = sbrk(0);
printf("Initial program break: %p\n", initial_brk);
// 计算一个目标地址(比如,在当前堆顶之上增加 2MB)
target_brk = (char *)initial_brk + 2 * 1024 * 1024;
printf("Target program break: %p (2MB increase)\n", target_brk);
// 使用 brk 直接设置到目标地址
if (brk(target_brk) == -1) {
perror("brk failed to increase heap");
exit(EXIT_FAILURE);
}
printf("Heap successfully increased using brk.\n");
final_brk = sbrk(0);
printf("Final program break: %p\n", final_brk);
if (final_brk == target_brk) {
printf("Confirmed: brk successfully set the break to the target address.\n");
} else {
printf("Warning: brk did not set the break exactly to the target address.\n");
}
// --- 现在可以使用 initial_brk 到 target_brk 之间的内存 ---
// 注意:这部分内存已经被内核分配,但内容是未定义的。
// 使用前最好初始化。
char *usable_memory = (char *)initial_brk;
size_t usable_size = (char *)target_brk - (char *)initial_brk;
printf("\n--- Using the newly allocated memory ---\n");
printf("Usable memory address range: %p to %p\n", (void*)usable_memory, (void*)((char*)usable_memory + usable_size - 1));
printf("Usable memory size: %zu bytes\n", usable_size);
// 初始化并使用一部分内存
for (size_t i = 0; i < 100 && i < usable_size; ++i) {
usable_memory[i] = 'M';
}
printf("Initialized first 100 bytes to 'M'.\n");
printf("Sample: [%c][%c][%c]...[%c]\n",
usable_memory[0], usable_memory[1], usable_memory[2],
usable_memory[99]);
// --- 缩小堆 ---
printf("\n--- Shrinking heap back to initial size ---\n");
if (brk(initial_brk) == -1) {
perror("brk failed to shrink heap");
// 即使失败,也继续执行,因为程序即将结束
} else {
printf("Heap successfully shrunk using brk.\n");
final_brk = sbrk(0);
printf("Final program break after shrinking: %p\n", final_brk);
if (final_brk == initial_brk) {
printf("Confirmed: brk successfully restored the break to initial address.\n");
}
}
printf("\nProgram finished.\n");
return 0;
}
代码解释:
- 获取初始的程序中断点
initial_brk
。 - 计算一个目标地址
target_brk
,即在当前堆顶之上增加 2MB。 - 调用
brk(target_brk)
尝试将程序中断点直接设置到这个目标地址。 - 检查调用是否成功,并通过再次调用
sbrk(0)
验证。 - 展示如何使用新分配的内存区域(从
initial_brk
到target_brk
)。 - 最后,调用
brk(initial_brk)
将堆大小恢复到初始状态。
总结:
brk
和 sbrk
提供了对进程堆的底层控制。虽然直接使用它们比较麻烦且容易出错(需要自己管理内存块、对齐、释放顺序等),但它们是理解操作系统内存管理以及标准库函数(如 malloc
)工作原理的基础。在实际应用开发中,强烈建议使用 malloc
/free
等标准库函数。