Linux 堆内存管理:brk 系统调用与 sbrk 库函数详解

一、背景知识

1.1 什么是 Program Break

在 Linux 进程的用户空间内存布局中,堆(heap)位于 BSS 段之上、栈之下。Program break(简称 break)是堆的末尾地址,也就是堆顶指针。堆从低地址向高地址增长,每次增长都是通过调整这个 break 指针来实现的。

高地址  ┌──────────────┐
       │     栈       │  ← 向低地址增长
       │      ↓       │
       ├──────────────┤
       │      ↑       │
       │     堆       │  ← 向高地址增长
       ├──────────────┤
       │    BSS 段    │  (未初始化全局变量)
       ├──────────────┤
       │   数据段      │  (已初始化全局变量)
       ├──────────────┤
       │   文本段      │  (代码)
低地址  └──────────────┘

注意:上述是经典内存布局示意图。在现代 Linux 系统中,由于 ASLR(地址空间布局随机化)的存在,各段的实际位置会被随机化,且堆和栈之间还存在 mmap 映射区域等,布局远比此图复杂。

1.2 brk 与 sbrk

Linux 提供了 brk 系统调用来操作 program break,而 C 标准库(如 glibc)在此基础上封装了 sbrk 函数:

函数原型性质作用返回值
brk()int brk(void *addr)系统调用绝对定位:直接将堆顶设置到 addr成功返回 0,失败返回 -1 并设置 errno
sbrk()void *sbrk(intptr_t increment)C库函数(glibc封装)相对增量:在当前堆顶基础上增加 increment 字节增加的旧堆顶地址;失败返回 (void*)-1 并设置 errno

sbrk(0) 是一个特殊用法:增量为 0,不改变堆大小,仅返回当前堆顶地址,常用于查询 program break。

重要提醒sbrk 并非系统调用,而是 glibc 等 C 库在 brk 系统调用之上封装的便捷函数。它通过先调用 sbrk(0) 获取当前地址,再调用 brk() 设置新地址来实现相对增量。

1.3 ASLR(地址空间布局随机化)

ASLR 是一种安全机制,通过在每次程序启动时随机化内存区域(栈、堆、mmap 区域、库加载基址等)的起始地址,使攻击者无法依赖固定的内存地址来构造缓冲区溢出攻击。

通过以下文件控制:

# 查看当前状态
cat /proc/sys/kernel/randomize_va_space

# 关闭 ASLR
echo 0 > /proc/sys/kernel/randomize_va_space

# 开启 ASLR(默认值 2,表示完全随机化)
echo 2 > /proc/sys/kernel/randomize_va_space

randomize_va_space 的取值含义:

  • 0:关闭 ASLR
  • 1:随机化栈、共享库映射区、mmap 区域
  • 2:在 1 的基础上,增加堆的随机化(即 brk 基址随机化)

二、实验代码

#include <stdio.h>      // 标准输入输出库,提供 printf()、getchar() 等函数
#include <unistd.h>     // POSIX 标准库,提供 brk()、sbrk()、getpid() 等声明
#include <stdint.h>     // 提供 intptr_t 等整数类型定义

/**
 * @brief 主函数:演示 brk 和 sbrk 对堆内存的操作
 * @return int 程序退出状态码,0 表示成功
 */
int main(void) {
    // 定义三个 void* 指针,用于存储堆的结束地址(program break)
    void *curr_brk, *tmp_brk, *pre_brk;

    // getpid() 获取当前进程 PID,方便后续查看 /proc/PID/maps
    printf("当前进程 PID:%d\n", getpid());

    // ==================== 第一步:初始化 ====================
    // sbrk(0) 不改变堆大小,只返回当前 program break
    // tmp_brk 保存初始地址,用于最后恢复
    tmp_brk = curr_brk = sbrk(0);
    printf("初始化后的结束地址:%p\n", curr_brk);
    getchar();  // 暂停,等待在另一个终端查看内存映射

    // ==================== 第二步:brk() 增加堆空间 ====================
    // brk() 是绝对定位:直接将堆顶设置到 curr_brk + 4096
    // 注意:将 void* 转换为 char* 后再做指针运算,符合 C 标准
    brk((char *)curr_brk + 4096);
    curr_brk = sbrk(0);
    printf("brk 之后的结束地址:%p\n", curr_brk);
    getchar();

    // ==================== 第三步:sbrk() 增加堆空间 ====================
    // sbrk(4096) 是相对增量:在当前堆顶基础上增加 4096 字节
    // 返回值是增加前的旧堆顶地址
    pre_brk = sbrk(4096);
    curr_brk = sbrk(0);
    printf("sbrk 返回值(即之前的结束地址):%p\n", pre_brk);
    printf("sbrk 之后的结束地址:%p\n", curr_brk);
    getchar();

    // ==================== 第四步:恢复堆空间 ====================
    // brk(tmp_brk) 将堆顶收缩回初始地址,释放之前分配的空间
    brk(tmp_brk);
    curr_brk = sbrk(0);
    printf("恢复到初始化时的结束地址:%p\n", curr_brk);
    getchar();

    return 0;  // 主函数正常返回
}

三、实验操作

3.1 两个终端配合

本实验需要开启两个终端:

终端用途
终端 1运行 ./a.out,程序会在每个 getchar() 处暂停
终端 2运行 `cat /proc//maps

3.2 操作流程

终端 1                                终端 2
─────────────────────────────         ────────────────────────────────
$ ./a.out                             $ cat /proc/27759/maps | grep heap
当前进程 PID:27759                    56558000-56579000 rw-p ... [heap]
初始化后的结束地址:0x56579000
(程序暂停,等待输入...)       →      (观察堆地址)
(按回车继续)
brk 之后的结束地址:0x5657a000
(程序暂停...)                →       $ cat /proc/27759/maps | grep heap
                                       56558000-5657a000 rw-p ... [heap]
                                       (堆结束地址增加了 4KB)
(按回车继续)
sbrk 返回值:0x5657a000
sbrk 之后的结束地址:0x5657b000
(程序暂停...)                →       $ cat /proc/27759/maps | grep heap
                                       56558000-5657b000 rw-p ... [heap]
                                       (堆结束地址又增加了 4KB)
...

注意:PID 每次运行可能不同,终端 2 要使用终端 1 输出的实际 PID。


四、实验结果分析

4.1 关闭 ASLR(randomize_va_space = 0

第一步:初始化
当前进程 PID:27759
初始化后的结束地址:0x56579000
# cat /proc/27759/maps
...
56557000-56558000 rw-p 00001000 08:01 28587506  /home/a.out   ← 数据段
56558000-56579000 rw-p 00000000 00:00 0         [heap]        ← 堆
...
  • 数据段范围:0x56557000 - 0x56558000,大小 = 0x1000 = 4KB
  • 堆范围:0x56558000 - 0x56579000,大小 = 0x21000 = 132KB
  • 数据段结束地址 0x56558000 = 堆开始地址 0x56558000在本实验环境中两者紧密相连
  • sbrk(0) 返回 0x56579000 → 这就是当前的 program break

补充说明:为何堆初始大小已有 132KB?这是因为程序中调用了 printf() 等标准库函数,这些函数内部会调用 malloc() 进行内存分配,从而触发 glibc 的内存分配器预先扩展了堆空间。如果程序完全不调用任何会触发 malloc 的函数,初始堆大小会更小(通常为 0 或仅 1 页)。

第二步:brk(curr_brk + 4096)
brk 之后的结束地址:0x5657a000
56558000-5657a000 rw-p 00000000 00:00 0  [heap]
  • 堆结束地址:0x565790000x5657a000
  • 差值 = 0x5657a000 - 0x56579000 = 0x1000 = 4096 字节 = 1 页
  • 堆开始地址 0x56558000 不变
  • brk() 是绝对定位:直接告诉内核"把堆顶设到这个地址"
第三步:sbrk(4096)
sbrk 返回值(即之前的结束地址):0x5657a000
sbrk 之后的结束地址:0x5657b000
56558000-5657b000 rw-p 00000000 00:00 0  [heap]
  • sbrk(4096) 返回 0x5657a000 → 这是增加的旧堆顶
  • 堆结束地址:0x5657a0000x5657b000
  • 差值 = 0x5657b000 - 0x5657a000 = 0x1000 = 4096 字节 = 1 页
  • sbrk() 是相对增量:在当前堆顶基础上增加指定字节数
第四步:brk(tmp_brk) 恢复
恢复到初始化时的结束地址:0x56579000
56558000-56579000 rw-p 00000000 00:00 0  [heap]
  • tmp_brk 保存的是第一步的初始值 0x56579000
  • brk(0x56579000) 将堆顶收缩回初始位置
  • 堆大小回到 0x21000 = 132KB
  • 证明 brk() 既可以扩大也可以缩小
地址变化汇总
步骤操作堆开始堆结束堆大小变化量
初始化sbrk(0)0x565580000x56579000132KB
brkbrk(curr+4096)0x565580000x5657a000136KB+4KB
sbrksbrk(4096)0x565580000x5657b000140KB+4KB
恢复brk(tmp_brk)0x565580000x56579000132KB-8KB

4.2 开启 ASLR(randomize_va_space = 2

当前进程 PID:28025
初始化后的结束地址:0x578ad000
# cat /proc/28025/maps
...
5663f000-56640000 rw-p 00001000 08:01 28587506  /home/a.out   ← 数据段
5788c000-578ad000 rw-p 00000000 00:00 0         [heap]        ← 堆
...
关键发现
项目关闭 ASLR开启 ASLR
数据段结束0x565580000x56640000
堆开始0x565580000x5788c000
堆结束0x565790000x578ad000
数据段与堆的间隙0(本例中紧密相连)0x5788c000 - 0x56640000 = 0x124C00019.2MB
堆大小132KB132KB(相同)

分析:

  1. 数据段结束地址 ≠ 堆开始地址(开启 ASLR 时)

    • 关闭 ASLR:0x56558000 == 0x56558000 → 本例中相等,紧密相连
    • 开启 ASLR:0x56640000 ≠ 0x5788c000 → 相差约 19.2MB

    注意:即使关闭 ASLR,堆的起始地址也不一定精确等于数据段结束地址。内核会将 start_brk 对齐到页边界,如果数据段结束地址本身不是页对齐的,两者之间会存在一个小于 1 页的间隙。

  2. ASLR 在数据段和堆之间插入了随机间隙

    • 这个随机间隙是安全机制的一部分
    • 使得攻击者无法通过已知的数据段地址推算出堆地址
    • 每次运行程序,这个间隙大小都会不同
  3. 堆的相对行为不变

    • 无论 ASLR 是否开启,brk()sbrk() 的增长行为完全一致
    • 堆大小仍然是 0x21000 = 132KB(0x578ad000 - 0x5788c000 = 0x21000
    • ASLR 只影响起始位置的随机化,不影响堆的管理逻辑

五、brk 与 sbrk 对比总结

对比项brk(addr)sbrk(increment)
性质系统调用C 库函数(glibc 封装 brk)
参数含义新的堆顶绝对地址堆顶的增量(可正可负)
返回值成功返回 0,失败返回 -1返回旧的堆顶地址;失败返回 (void*)-1
典型用法直接设置堆顶到指定位置分配内存,返回值可直接作为指针
缩小堆brk(较小的地址)sbrk(-N)
查询堆顶不支持,需配合 sbrk(0)sbrk(0)

sbrk 返回值的设计意义

sbrk() 返回旧地址而非新地址,这个设计非常巧妙——返回值正好是新分配内存区域的起始指针:

// 分配 4096 字节,ptr 直接指向新分配区域的起始位置
void *ptr = sbrk(4096);
if (ptr != (void *)-1) {
    // ptr 可以安全使用,范围是 [ptr, ptr+4096)
    memset(ptr, 0, 4096);
}

注意:在多线程环境下,brk/sbrk 不是线程安全的,因为它们操作的是全局的堆指针。实际应用中应使用 malloc 等线程安全的内存分配函数。


六、延伸:与 malloc 的关系

6.1 为什么 malloc 不直接调用 brk/sbrk

我们在前面实验中看到,brk()/sbrk() 是非常底层的原语——它们只能整体移动堆顶,没有"分配指定大小块"的能力,更没有"释放某一块"的能力。如果每次分配都直接调 sbrk(size),会导致:

  • 无法精确释放sbrk(-N) 只能从堆顶收缩,中间释放的碎片无法归还内核
  • 性能极差:每次分配/释放都要陷入内核态(系统调用开销)
  • 无内存复用:释放的内存无法被后续分配重用

因此 glibc 的 ptmalloc2 分配器在 brk()/sbrk()mmap() 之上构建了一整套内存管理层,核心思想是:向内核批量申请大块内存,再在用户态切分为小块分配

6.2 malloc 的两种底层内存来源

┌─────────────────────────────────────────────────────┐
│                   用户程序                           │
│              malloc() / free()                       │
├─────────────────────────────────────────────────────┤
│              ptmalloc2 分配器                        │
│    ┌──────────────┐      ┌───────────────────┐      │
│    │   Arena      │      │   mmap 区域        │      │
│    │  (brk 堆)    │      │  (大块匿名映射)     │      │
│    └──────┬───────┘      └─────────┬─────────┘      │
├───────────┼────────────────────────┼────────────────┤
│           ▼                        ▼                │
│      brk() 系统调用          mmap() 系统调用         │
├─────────────────────────────────────────────────────┤
│                   Linux 内核                         │
└─────────────────────────────────────────────────────┘
分配方式底层系统调用适用场景特点
Arena(brk 堆)brk()小块内存(默认 < 128KB)内存连续、可扩展收缩、通过 top chunk 管理
mmap 匿名映射mmap()大块内存(默认 >= 128KB)独立映射、free 时直接 munmap 归还内核

阈值说明:glibc 默认的 M_MMAP_THRESHOLD 初始值为 128KB(DEFAULT_MMAP_THRESHOLD)。但从 glibc 2.18 开始引入了动态 mmap 阈值调整机制:当释放一个大于当前阈值的 mmap 块时,分配器会自动将阈值提升到该块的大小(上限为 DEFAULT_MMAP_THRESHOLD_MAX,32 位系统 512KB,64 位系统 32MB)。因此"小于 128KB 用 brk,大于等于 128KB 用 mmap"只是初始行为,运行中阈值会动态变化。

6.3 ptmalloc2 的核心数据结构

6.3.1 内存块(chunk)结构

ptmalloc2 将堆内存划分为连续的"块"(chunk),每个 chunk 的结构如下:

┌────────────────────────────────────────────────────┐
│  malloc_chunk 结构                                   │
├──────────┬──────────┬───────────┬──────────────────┤
│  prev_size │   size   │   fd/bk   │    用户数据区      │
│ (8 字节)  │ (8 字节)  │ (8 字节)  │  (实际分配大小)     │
└──────────┴──────────┴───────────┴──────────────────┘
  ← 元数据(header,16 字节)→    ← malloc 返回的指针 →

各字段含义(以 64 位系统为例):

字段大小作用
prev_size8 字节前一个 chunk 的大小。仅当前一个 chunk 空闲时有意义(用于合并相邻空闲块)
size8 字节当前 chunk 的大小(包含 header)。低 3 位为标志位:bit0 = PREV_INUSE,bit1 = IS_MMAPPED,bit2 = NON_MAIN_ARENA
fd8 字节Forward pointer:指向 bins 双向链表中的下一个空闲 chunk(仅空闲时有效)
bk8 字节Backward pointer:指向 bins 双向链表中的上一个空闲 chunk(仅空闲时有效)

关键设计:当一个 chunk 被分配时,fdbk 字段的位置就是用户数据区的起始位置,所以 malloc 返回的指针紧挨在 size 字段之后。这意味着用户数据区的起始地址 = chunk 起始地址 + 16 字节。这也是为什么每个 malloc 分配都有至少 16 字节的内部开销。

6.3.2 size 字段的三个标志位

size 字段的低 3 位被复用为标志位(因为所有 chunk 都按 16 字节对齐,低 3 位恒为 0):

标志位含义
PREV_INUSE(bit0)前一个 chunk 是否正在使用。为 1 表示前一个 chunk 是已分配的
IS_MMAPPED(bit1)该 chunk 是否通过 mmap() 分配
NON_MAIN_ARENA(bit2)该 chunk 是否属于非主 arena(线程 arena)

实际大小 = size & ~0x7(清除低 3 位标志后得到真实大小)。

6.4 Bins:空闲块的管理

ptmalloc2 使用 bins(箱)来管理不同大小的空闲 chunk,是 malloc 高效分配的核心。

6.4.1 Bins 分类
Bins
├── Unsorted Bin(1 个)
│   └── 最近释放的 chunk 暂存区,释放时先放入这里
│
├── Small Bins(62 个)
│   └── 索引 2~63,每个 bin 管理固定大小的 chunk
│       大小范围:16 字节 ~ 504 字节(64 位系统),步长 16 字节
│
└── Large Bins(63 个)
    └── 索引 64~126,每个 bin 管理一个大小区间的 chunk
        大小范围:512 字节以上,按区间递增

工作流程

  1. 分配时:先在对应大小的 bin 中查找,找到则从链表摘除并返回
  2. 释放时:并非立即归还内核,而是将 chunk 放入 Unsorted Bin 暂存
  3. 后续分配扫描 Unsorted Bin 时,会将 chunk 分类移动到对应的 Small/Large Bin 中

这种"延迟分类"的设计减少了释放时的开销。

6.4.2 Fast Bins:小内存快速通道

对于非常小的分配(默认 <= 128 字节,共 10 个 fast bin),ptmalloc2 使用 Fast Bins 做进一步加速:

  • 单链表(非双向链表),操作更快(LIFO 栈式)
  • 不合并相邻空闲块,避免碎片合并的开销
  • 释放的 chunk 直接放入 fast bin,分配时优先从 fast bin 取

这就是为什么频繁分配/释放小对象时 malloc 性能很好的原因——绝大多数操作都在用户态的 fast bin 单链表上完成,不涉及系统调用。

6.5 Top Chunk 与堆扩展

每个 Arena(brk 堆)的顶部有一个特殊的 Top Chunk,它是堆中最高地址的空闲 chunk,充当"预留空间":

┌──────────┐
│ chunk 1  │ ← 已分配
├──────────┤
│ chunk 2  │ ← 空闲(在某个 bin 中)
├──────────┤
│ chunk 3  │ ← 已分配
├──────────┤
│Top Chunk │ ← 空闲,但不属于任何 bin
│          │    新分配从这里切割
├──────────┤  ← program break(brk 堆顶)
│  (未映射) │
└──────────┘

分配流程

  1. 在 bins 中查找合适大小的空闲 chunk
  2. 若 bins 中没有,尝试从 Top Chunk 切割
  3. 若 Top Chunk 不够大,调用 brk() / sbrk() 扩展堆,增大 Top Chunk
  4. 若通过 brk 扩展失败(堆已达上限或地址空间不足),尝试用 mmap() 分配

收缩流程

  1. free() 释放的内存通常只放回 bins,不归还内核
  2. 当 Top Chunk 超过一定阈值(trim_threshold,默认 128KB)时,ptmalloc2 会调用 brk() 收缩堆顶,将多余页面归还内核
  3. mmap 分配的大块内存在 free 时会直接 munmap() 归还内核

6.6 多线程支持:Arena 机制

ptmalloc2 通过 Arena 机制支持多线程并发分配,避免所有线程竞争同一个堆:

┌─────────────────────────────────────────────┐
│  主线程                                      │
│  ┌─────────────────────────────────┐        │
│  │  Main Arena(使用 brk 堆)        │        │
│  │  - 所有线程共享                   │        │
│  │  - 通过 mutex 保护               │        │
│  └─────────────────────────────────┘        │
├─────────────────────────────────────────────┤
│  线程 1                                      │
│  ┌─────────────────────────────────┐        │
│  │  Thread Arena 1(使用 mmap 堆)    │        │
│  └─────────────────────────────────┘        │
├─────────────────────────────────────────────┤
│  线程 2                                      │
│  ┌─────────────────────────────────┐        │
│  │  Thread Arena 2(使用 mmap 堆)    │        │
│  └─────────────────────────────────┘        │
└─────────────────────────────────────────────┘
  • Main Arena:主线程使用,对应进程的 brk 堆
  • Thread Arena:其他线程各自拥有(或共享),使用 mmap() 创建独立堆
  • Arena 数量有上限(默认 32 位系统 2 × CPU核心数,64 位系统 8 × CPU核心数),超出时线程需要排队等待已有的 Arena

对比 brk/sbrk:这就是为什么说 brk/sbrk 不是线程安全的——它们操作的是全局唯一的 program break,而 malloc 通过 Arena 机制将不同线程的堆隔离,实现了线程安全(每个 Arena 有自己的锁)。

6.7 malloc 与 brk/sbrk 的交互实验

下面通过一段代码直观观察 malloc 是如何间接使用 brk 的:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
    void *brk_before, *brk_after;

    printf("PID: %d\n", getpid());

    // ====== 阶段 1:首次 malloc,观察堆是否扩展 ======
    brk_before = sbrk(0);
    printf("[阶段1] malloc 前,program break: %p\n", brk_before);

    // 分配一块小内存(256 字节)
    void *p1 = malloc(256);
    memset(p1, 'A', 256);

    brk_after = sbrk(0);
    printf("[阶段1] malloc 256B 后,program break: %p\n", brk_after);
    printf("[阶段1] 堆增长量: %ld 字节\n\n",
           (char *)brk_after - (char *)brk_before);

    // ====== 阶段 2:多次 malloc 小块,观察是否需要再次 brk ======
    brk_before = sbrk(0);
    void *p2 = malloc(64);   // 64 字节
    void *p3 = malloc(128);  // 128 字节
    void *p4 = malloc(32);   // 32 字节

    brk_after = sbrk(0);
    printf("[阶段2] 再 malloc 64+128+32B 后,program break: %p\n", brk_after);
    printf("[阶段2] 堆增长量: %ld 字节", (char *)brk_after - (char *)brk_before);
    printf("(若为 0,说明复用了阶段 1 预分配的 Top Chunk)\n\n");

    // ====== 阶段 3:free 后 malloc 同样大小,观察是否复用 ======
    free(p1);
    free(p2);
    free(p3);

    // p1 释放后放入 unsorted bin / small bin
    // 再次分配 256 字节,应从 bin 中复用,不需要 brk
    brk_before = sbrk(0);
    void *p5 = malloc(256);
    brk_after = sbrk(0);
    printf("[阶段3] free 后再 malloc 256B,program break: %p\n", brk_after);
    printf("[阶段3] 堆增长量: %ld 字节", (char *)brk_after - (char *)brk_before);
    printf("(若为 0,说明从 bin 中复用了释放的 chunk)\n\n");

    // ====== 阶段 4:大块 malloc,观察是否使用 mmap ======
    printf("[阶段4] 尝试分配 1MB 大内存...\n");
    void *big = malloc(1024 * 1024);  // 1MB,远超 mmap 阈值
    if (big) {
        printf("[阶段4] 分配成功,指针: %p\n", big);
        printf("[阶段4] 1MB 大块通常由 mmap 分配,不在 brk 堆中\n");
        printf("[阶段4] 此时 program break: %p(应与之前相同)\n\n", sbrk(0));
    }

    free(big);
    free(p4);
    free(p5);

    printf("按回车退出...\n");
    getchar();
    return 0;
}

预期输出分析

阶段预期行为原因
阶段 1不增长(增量为 0)glibc 在程序启动时(main() 之前)已预分配堆空间,malloc(256) 直接从现有 Top Chunk 切割
阶段 2不增长(增量为 0)64+128+32 字节远小于 Top Chunk,直接从 Top Chunk 切割
阶段 3不增长(增量为 0)释放的 256B chunk 被放入 bin,再次 malloc 256B 直接从 bin 取出复用
阶段 4program break 不变1MB 超过 mmap 阈值,通过 mmap() 分配独立映射区域,不走 brk 堆

注意:文章第 4.1 节中提到的"初始堆大小 132KB"是在 printf() 等库函数调用后观察到的。现代 glibc 在程序启动代码(__libc_start_main)中就会初始化堆,因此当 main() 开始执行时,堆已经存在。如果要观察堆从 0 开始增长的过程,需要使用更底层的程序(不链接标准库或使用静态链接并避免任何库调用)。

6.8 brk/sbrk 与 malloc/free 的对比总结

对比项brk()/sbrk()malloc()/free()
抽象层次系统调用 / 库函数(极底层)C 标准库函数(高层封装)
分配粒度只能移动堆顶,无分块能力任意大小,精确分配
释放能力只能从顶部收缩可释放任意位置的块
碎片处理bins 管理、合并相邻空闲块
性能每次调用都陷入内核态大部分操作在用户态完成(fast bin)
线程安全不安全安全(Arena + 锁)
大块内存不支持自动切换到 mmap
适用场景实现内存分配器、教学实验日常开发

核心结论brk/sbrk 是构建内存分配器的"原材料",而 malloc/free 是在这之上建造的"成品"。除非你在写一个自定义的内存分配器,否则永远应该使用 malloc/free


七、结论

  1. brk() 是系统调用,支持绝对定位,直接设置 program break 到指定地址
  2. sbrk() 是 C 库函数,在 brk 基础上封装了相对增量操作,返回旧堆顶,适合作为 malloc 的底层实现
  3. 堆向高地址增长,内核以页为单位管理,因此堆大小的实际调整通常以页(4096 字节)为最小单位
  4. 关闭 ASLR 时,数据段和堆通常紧邻(具体取决于数据段结束地址的页对齐情况),地址可预测
  5. 开启 ASLR 时,数据段和堆之间被插入随机间隙(本实验中约 19.2MB),地址不可预测,但堆的相对增长行为不变
  6. brk() 既可以扩大也可以缩小堆,将参数设为较小的地址即可释放堆空间
  7. 实际开发中应使用 malloc/free,而非直接调用 brk/sbrk,因为 malloc 提供了更完善的内存管理和线程安全保障
  8. ptmalloc2 通过 chunk + bins + arena 三层架构实现高效分配:chunk 是基本单位,bins 管理空闲块,arena 实现线程隔离
  9. malloc 有两种底层内存来源:小块走 brk 堆(通过 Top Chunk 扩展),大块走 mmap 匿名映射,阈值会动态调整
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lcreek

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值