进程可以通过增加堆的大小来分配内存,堆就是一段长度可变的连续的虚拟内存,开始于未初始化数据段末尾,随着内存的分配和释放增减。通常堆的当前内存边界称为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内存分配以及相关的做了简单的介绍,如果有疑问可以给我留言。

 

Logo

更多推荐