好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 brk 和 sbrk 函数,它们是用于直接控制进程堆 (heap) 大小的底层系统调用。虽然在现代编程中很少直接使用它们(通常由 malloc/free 等库函数管理),但理解它们有助于深入理解进程内存布局和动态内存分配的原理。
1. 函数介绍 Link to heading
brk 和 sbrk 是 Linux 系统调用,它们直接操作进程的程序中断点 (program break),从而改变进程堆 (heap) 的大小。
- 程序中断点 (program break): 这是进程数据段(包括初始化数据 
.data、未初始化数据.bss)的末尾,也是堆的起始边界。堆是从这个点开始向上(向更高内存地址)增长的。 - 堆 (heap): 这是进程内存空间中用于动态内存分配(如 
malloc,calloc)的区域。通过移动程序中断点,可以增加或减少堆的可用空间。 
简单来说,brk 和 sbrk 是调整“堆顶”的工具。
brk: 直接将程序中断点设置到指定的地址。sbrk: 将程序中断点增加(或减少)指定的字节数,并返回之前的程序中断点地址。
2. 函数原型 Link to heading
#include <unistd.h> // 必需
// 设置程序中断点到指定地址
int brk(void *addr);
// 将程序中断点增加 increment 字节
void *sbrk(intptr_t increment);
3. 功能 Link to heading
brk(void *addr):- 尝试将程序中断点设置为 
addr。 - 如果 
addr大于当前中断点,堆会增长。 - 如果 
addr小于当前中断点,堆会收缩。 - 内核会相应地分配或释放物理内存页。
 
- 尝试将程序中断点设置为 
 sbrk(intptr_t increment):- 将程序中断点增加 
increment字节。 - 如果 
increment为正,堆增长。 - 如果 
increment为负,堆收缩。 - 返回调整前的程序中断点地址(即旧的堆顶)。
 - 如果 
increment为 0,则不改变堆大小,但仍返回当前的程序中断点地址。 
- 将程序中断点增加 
 
4. 参数 Link to heading
brk:void *addr: 期望设置的新的程序中断点的绝对地址。
sbrk:intptr_t increment: 相对于当前程序中断点的偏移量(以字节为单位)。可以是正数(增长)、负数(收缩)或零(查询)。
5. 返回值 Link to heading
brk:- 成功时: 返回 0。
 - 失败时: 返回 -1,并设置 
errno(例如ENOMEM内存不足)。 
sbrk:- 成功时: 返回调整前的程序中断点地址。
 - 失败时: 返回 
(void *) -1(即MAP_FAILED常量,但这与 mmap 无关),并设置errno。 
6. 相似函数,或关联函数 Link to heading
malloc,free,calloc,realloc: 这些标准 C 库函数是更高级、更安全、更便携的动态内存分配接口。它们通常在底层使用brk/sbrk(对于小内存块)和mmap(对于大内存块或避免堆碎片)来管理内存。mmap,munmap: 另一种动态内存分配方式,特别是对于大块内存或需要特殊权限(如可执行)的内存。mmap分配的内存通常不在由brk管理的堆区域。getrlimit,setrlimit(RLIMIT_DATA): 可以用来查询和设置进程数据段(包括堆)的最大大小限制。
7. 示例代码 Link to heading
  示例 1:使用 sbrk 进行简单的内存分配和释放
  
    
    Link to heading
  
这个例子演示了如何直接使用 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) 获取当前堆顶
  
    
    Link to heading
  
这个例子展示了如何使用 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 设置堆大小
  
    
    Link to heading
  
这个例子展示了如何使用 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 等标准库函数。