首页 时政热点 科技头条 智能AI 安全攻防 数码硬件 开发者生态 归档 标签 关于

malloc 到底在做什么?

摘要

malloc has a very nice and simple interface: you ask for a certain amount of memory, and you’re given a void pointer to your block of contiguous bytes that’s been magicked into existence. Once you’re ...

the and for program you void that with brk malloc
2026-11-09 1 阅读 约8分钟阅读 benmandrew
分享:
字号:
malloc 有一个非常漂亮且简单的接口:您请求一定量的内存,然后您会得到一个指向连续字节块的空指针,该块已被魔法化而存在。一旦你完成了它,你用同一个指针调用 free,内存就会消失在以太中。但这里到底发生了什么?显然,当我们作为用户空间程序运行时, malloc 必须通过 syscalls 向内核请求内存。然而,我们可能会分配许多小块,并且系统调用的成本很高(空块需要 575 ns,如下所示),因此一些簿记工作是在后台完成的。我们将使用下面的程序片段在 GNU libc ( glibc ) 上演示这一点,该片段调用 malloc(16) 一千次。静态无效*ptrs[1000]; int main ( void ) { for ( int i = 0 ; i < 1000 ; i ++ ) ptrs [ i ] = malloc ( 16 ) ;使用 strace ,我们可以看到整个循环只进行了两次 brk 系统调用:第一个询问程序中断在哪里(进程堆结束的地址),第二个将堆增长了 132 KiB。这些都是在第一个 malloc 调用中完成的 - 其他 999 个只是从已经分配的内存中获取。 $ strace -e trace = brk,mmap,munmap,write \ ./malloc1000 > /dev/null ... brk(NULL) = 0x59faca231000 brk(0x59faca252000) = 0x59faca252000 +++ exited with 0 +++ 你可能会在讨论中看到 brk 和 sbrk 。 brk 是一个系统调用,它将程序中断设置为它提供的地址,而 sbrk 是一个 libc 便利包装器,它采用 delta ,将程序中断增加该数量。地址空间 正在运行的进程有自己的地址空间,分为多个区域。其中包括可执行代码的文本、已初始化全局变量的数据以及未初始化全局变量的 bss 等。以下程序打印进程每个区域的一个地址。 #define SHOW ( label , p ) \ printf ( "%-22s %p\n" , label , ( void * ) ( p ) ) SHOW ( "main (text)" , main ) ; SHOW("初始化(数据)",&初始化); SHOW(“未初始化(bss)”,&未初始化); SHOW ( "malloc(16) (堆)" , malloc ( 16 ) ) ; SHOW(“程序中断”,sbrk(0));显示(“printf(libc)”,printf); SHOW("本地(堆栈)",&本地);地址从二进制代码一直到其堆栈。 $ ./layout main (文本) 0x59c0791d7149 已初始化 (数据) 0x59c0791da020 未初始化 (bss) 0x59c0791da028 malloc(16) (堆) 0x59c0903182b0 程序中断 0x59c090338000 printf (libc) 0x711dcf7c15b0 本地(堆栈) 0x7ffff86c45ec 其中一些区域可能会随着程序运行而改变。例如,malloc(16) 会增加堆并移动分解。请注意,在此运行中,libc 中的 break 和 printf 相距 23.4 TiB,大约是机器 15.6 GiB RAM 的 1,530 倍。几乎所有范围都未映射,因此不消耗物理内存。 low high at start 文本数据 BSS 间隙中断 23 TiB 未映射的 libc 堆栈 malloc(16) 文本数据 BSS 间隙堆中断 23 TiB 未映射的 libc 堆栈 带 sbrk 的玩具分配器 因为我们可以直接移动程序中断,所以我们可以编写自己的小分配器。这称为碰撞分配器,因为每次调用都会增加允许我们使用的内存量。 void * my_malloc(size_t size) { void * p = sbrk(align_up(size));返回 p == (void *) - 1 ?空:p; } void my_free ( void * ptr ) { ( void ) ptr ;这个分配器不太好!它会移动每个请求的中断,并且每个移动都是一个系统调用。我们可以对 malloc 进行系统调用,以了解为什么 malloc 从内核请求大块并自行切割它们。以下三个函数的调用次数均超过一百万次。静态无效 * 挥发性接收器;静态无效call_getppid(void){系统调用(SYS_getppid); } 静态无效call_sbrk(无效){接收器= sbrk(16); } 静态 void call_malloc ( void ) { 汇 = malloc ( 16 ) ; sbrk(16) 只比空系统调用多花费 35 ns,而 malloc(16) 的运行速度比 sbrk(16) 快 22.7 倍。 $ ./syscall-cost syscall(SYS_getppid) 575.2 ns 每次调用 sbrk(16) 610.1 ns 每次调用 malloc(16) 26.9 ns 每次调用 sbrk(16) / malloc(16) 22.7x 碰撞分配器也无法重用已释放的内存。每个请求都直接在前一个请求之后到达,因此在下面的示例中,即使 b 已被释放,d 也会在 c 之后到达。 $ ./my-bump-1 a = my_malloc(16) start+0 b = my_malloc(1) start+16 c = my_malloc(100) start+32 my_free(b) d = my_malloc(1) start+144 break start+160 帖子的其余部分一次改进分配器一个阶段。块和标头 malloc 实际上分配的内存比您请求的要多。这是因为每个分配或块在返回的指针之前都有一个大小标头;这就是 free 在不传递明确大小的情况下工作的方式。块大小始终是 16 的倍数,因此大小的低位始终为零,并且 glibc 在其中保留三个标志。 header 24 字节用于调用者 header p = malloc(24) 下一个 chunk chunk : 32 字节 size = 32 0 A 0 M 1 P 一个 8 字节字 A : 另一个 are
这篇文章对您有帮助吗?

订阅66必读

每日精选科技资讯,直达你的邮箱