好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 brksbrk 函数,它们是用于直接控制进程堆 (heap) 大小的底层系统调用。虽然在现代编程中很少直接使用它们(通常由 malloc/free 等库函数管理),但理解它们有助于深入理解进程内存布局和动态内存分配的原理。


1. 函数介绍 链接到标题

brksbrk 是 Linux 系统调用,它们直接操作进程的程序中断点 (program break),从而改变进程堆 (heap) 的大小。

  • 程序中断点 (program break): 这是进程数据段(包括初始化数据 .data、未初始化数据 .bss)的末尾,也是堆的起始边界。堆是从这个点开始向上(向更高内存地址)增长的。
  • 堆 (heap): 这是进程内存空间中用于动态内存分配(如 malloc, calloc)的区域。通过移动程序中断点,可以增加或减少堆的可用空间。

简单来说,brksbrk 是调整“堆顶”的工具。

  • 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;
}

代码解释:

  1. my_malloc(size_t size):
    • 调用 sbrk(0) 获取当前的程序中断点地址。
    • 调用 sbrk(size) 请求增加 size 字节的内存。
    • 检查 sbrk 是否失败。
    • 返回之前的中断点地址,这就是新分配内存块的起始地址。
  2. my_free(void *ptr, size_t size):
    • 为了简化,这个实现只允许释放最近一次分配的内存块(模拟栈式分配)。
    • 它检查要释放的指针 ptr 加上大小 size 是否等于当前的程序中断点。
    • 如果是,则调用 brk(ptr) 将程序中断点设置回 ptr,从而释放这块内存。
    • 检查 brk 是否失败。
  3. 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;
}

代码解释:

  1. 定义了一个 print_brk 函数,它调用 sbrk(0) 并打印返回的地址。
  2. 在程序开始、malloc 之后、free 之后分别调用 print_brk
  3. 通过比较这些地址,可以观察 mallocfree 对程序中断点(堆顶)的影响。
  4. 重要提示: mallocfree 的具体实现很复杂。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;
}

代码解释:

  1. 获取初始的程序中断点 initial_brk
  2. 计算一个目标地址 target_brk,即在当前堆顶之上增加 2MB。
  3. 调用 brk(target_brk) 尝试将程序中断点直接设置到这个目标地址。
  4. 检查调用是否成功,并通过再次调用 sbrk(0) 验证。
  5. 展示如何使用新分配的内存区域(从 initial_brktarget_brk)。
  6. 最后,调用 brk(initial_brk) 将堆大小恢复到初始状态。

总结:

brksbrk 提供了对进程堆的底层控制。虽然直接使用它们比较麻烦且容易出错(需要自己管理内存块、对齐、释放顺序等),但它们是理解操作系统内存管理以及标准库函数(如 malloc)工作原理的基础。在实际应用开发中,强烈建议使用 malloc/free 等标准库函数。