进程可以通过增加堆的大小来分配内存,堆就是一段长度可变的连续的虚拟内存,开始于未初始化数据段末尾,随着内存的分配和释放增减。通常堆的当前内存边界称为program break。最初,program break正好位于未出华数据段末尾之后(&end位置)。
当program break位置上升后,程序可以访问新分配的任何内存地址,而此时物理内存尚未分配,内核会在进程试图首次访问这些虚拟内存地址时才分配物理内存页。
下面来讲讲2个操作program break的系统调用:brk()和sbrk(),虽然在代码中我们很少用到,但是了解下有助于我们弄清内存分配的工作过程。
int brk(void *end_data_segment);
void *sbrk(intptr_t increment);
系统调用brk()会将program break设置为参数end_data_segment所指定的位置,由于虚拟内存是以页为单位分配的,end_data_segment实际会四舍五入到下一个内存页的边界处。
sbrk()将program break在原来的地址上增加increment大小,并返回前一个program break的位置。所以sbrk(0)则返回当前program break位置。
获取内存页大小
//pagesize=4096
printf("pagesize=%d\n", getpagesize());
malloc()和free()
相较于brk()和sbrk(),他们更简单,允许分配小块内存,允许随意释放内存。malloc分配的内存会自动对齐。比如我的64位linux会以16字节对齐。
free()用来释放所指的内存块,通常情况下,free()并不降低program break的位置,而只是将这块内存添加到空闲内存列表,供后续的malloc()函数循环使用。这样做的好处是
1.被释放的内存块通常位于堆的中间,而非堆的顶部,因而降低program break是不可能的。
2.这样可以最大幅度的减少sbrk的调用次数,因为系统调用更耗资源。
malloc()和free()内部实现
malloc()实现很简单,它首先会扫描之前由free()所释放的空闲内存块,以一定策略(first-fit或best-fit)找到大于或等于要求的一块空闲内存,如果有直接返回,如果是大块内存,则对其进行分割返回,其他的继续保留在空闲内存列表。malloc分配内存时会额外分配几个字节用来存储内存大小如图,而实际返回给用户的是这个长度记录字节之后。
如果找不到,则会调用sbrk()以分配更多的内存。为了减少sbrk()的调用次数,sbrk()会申请更多的内存。
内存操作应该遵循以下几个规则:
1.分配一块内存后应该谨慎使用,避免操作该内存以外的字节。
2.不允许释放一个内存超过1次。
3.如果不是由malloc函数包分配的内存,决不能用free()函数释放。
分配对齐的内存memalign()和posix_memalign()
我们知道malloc已经帮我们实现了内存对齐,比如我的64位linux为16字节对齐,所以大多数情况下都不需要下面2个函数来手动指定对齐参数。只有在一些特殊场合malloc满足不了的时候才会用下面2个函数。
void *memalign(size_t boundary, size_t size);
分配size个字节内存,内存的起始地址时参数boundary的整数倍,而boundary必须是2的整数次幂。返回分配内存的地址。
int posix_memalign(void **memptr, size_t alignment, size_t size);
已分配的内存地址通过memptr返回,内存的起始地址必须是alignment的整数倍,alignment必须是sizeof(void*)与2的整数次幂两者的乘积。
在栈上分配内存:alloca()
void *alloca(size_t size);
在栈上分配的内存不能手动释放,栈帧的移除(函数返回)的时候自动释放,速度比在堆上分配更快。另一个优点是在信号处理程序中调用longjmp()或siglongjmp以执行非局部跳转时,起跳函数和落地函数之间的函数中如果使用了malloc()来分配内存,想要避免内存泄漏及其困难,甚至不可能。但是alloca完全可以避免这个问题,因为堆栈是由这些调用展开的,当堆栈重置,栈帧被移除时,内存会随栈帧一起被移除。
总结
本文对linux内存分配以及相关的做了简单的介绍,如果有疑问可以给我留言。