- 本节总结了8.3~8.5的内容
数据段的管理
在老版本的Unix系统中,堆和栈还在同一个段中。
- 堆中动态存储器的分配由数据段的底部向上生长;栈从数据段的顶部向着堆往下生长。
- 堆和栈的分界线叫做中断(break)或中断点(break point)。
在现代系统中,数据段存在于它自己的内存映射中,仍用中断点来标记映射的结束地址。
提供以下函数:
#include
int brk(void *end);
void *sbrk(intptr_t increment);
- 调用brk()会设置中断点(数据段的末端)的地址为end。
- 调用sbrk()将数据段末端增加increment字节,increment可正可负。
匿名存储器映射
首先来看看伙伴内存分配算法:
glibc的内存分配使用了数据段和内存映射。
实现malloc( )最经典方法就是将数据段分为一系列的大小为2的幂的块,返回最小的符合要求的那个块来满足请求。 释放只是简单的将这块区域标记为未使用。
- 优点:高速、简单
- 缺点:产生两种类型的碎片:1.内部碎片:使用的内存块大于请求的大小;2.外部碎片:空闲存储器合计起来足够满足一个请求,但是没有一个单独的空闲块可以来处理这个请求。
- 另外:一个长期存在的内存分配可能把另外的空闲空间“栓”住。(内存中已经被分配块A和块B,块A处于中断点位置,而块B处于块A的下方,当块B被释放,而块A没有被释放时,glibc也不能相应的调整中断点。)
匿名内存映射(anonymous memory mapping)
因为伙伴内存分配算法存在可能被“栓”住的问题,glibc对于较大的分配,并不使用堆而是创建一个匿名内存映射来满足要求。
- 一个匿名内存映射只是一块已经用0初始化的大的内存块,以供用户使用。(可以把它想成为单独为某次分配而使用的堆)
- 因为这种映射的存储不是基于堆的,所以并不会在数据段内产生碎片。
- 综合一下两者的优缺点,glibc的malloc( )使用数据段来满足小的分配(小于128KB),而匿名内存映射则用来满足大的分配。(临界点一般是128KB,但其值是可调的)
创建匿名存储器映射
匿名存储器映射与内存映射很像:
/* for memory map */
#include
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *start, size_t length);
- 因为不需要打开和管理文件,创建匿名存储器映射要比创建基于文件的存储器映射更简单。两者最关键的差别在于是否有匿名标记。
一个匿名映射的例子:
void *p;
p = mmap(NULL, 512*1024, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (p == MAP_FAILED)
perror("mmap");
else
/* 'p' points at 512 KB of anonymous memory ... */
参数含义:
- 第一个参数是start,被设为NULL,意味着匿名映射可以让内核安排的子任意地址上。(non-NULL值也是可以的,只要它是页对齐的,但这样会限制可移植性)
- prot参数经常被设置为PROT_READ和PROT_WRITE位,使得映射是可读可写的。一块不能读写的空存储器映射是没有用的。
- flags参数设置MAP_ANONYMOUS位,来使得映射是匿名的,设置MAP_PRIVATE位,使得映射是私有的。
- 假如MAP_ANONYMOUS被设置了,fd和offset参数将被忽略。(一般将fd写为-1, 考虑到可移植性)
使用匿名映射进行分配的一个好处是所有的页都已经用0进行了初始化。
系统调用munmap( )释放一个匿名映射,归还已分配的内存给内核。
int ret;
ret = munmap(p, 512*1024);
if (ret)
perror("munmap");
映射到/dev/zero
其他Unix系统(例如BSD),并没有MAP_ANONYMOUS标记。它们使用特殊的设备文件/dev/zero来实现了一个类似的解决方案。这个设备文件提供了和匿名内存相同的语义。
void *p;
int fd;
/* open /dev/zero for reading and writing */
fd = open("/dev/zero", O_RDWR);
if (fd < 0) {
perror("open");
return -1;
}
/* map [0, page_size) of /dev/zero */
p = mmap(NULL, getpagesize( ), PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (p == MAP_FAILED) {
perror("mmap");
if (close(fd))
perror("close");
return -1;
}
/* close /dev/zero, no longer needed */
if (close(fd))
perror("close");
/* 'p' points at one page of memory, use it ... */
采用这种映射方式的存储器也是用munmap( )来取消映射的。
然而这种方法因为要打开和关闭设备文件,所以会有额外的开销。因此匿名内存映射是一种较块的方法。
高级存储器分配
许多存储分配操作都是为内核的参数所控制的(例如malloc( )时,是从数据段分配还是从匿名内存映射,这个临界值,是内核参数),但是程序员可以修改这些参数!
- 注:不应该将这个修改用于调试和教学以外的其它地方,它们是不可移植的,而且会将glibc内存分配系统的一些底层细节暴露给你的程序。
- mallopt( )函数:
调用mallopt( )会将由param确定的存储管理相关的参数设为value。
linux目前支持六种param值,被定义在中: - M_CHECK_ACTION
- M_MMAP_MAX
- M_MMAP_THRESHOLD(数据段和匿名内存映射的临界值)
- M_MXFAST
- M_TOP_PAD
程序必须在调用malloc( )或是其它内存分配函数前使用mallopt( ):
/* use mmap( ) for all allocations over 64KB */
ret = mallopt(M_MMAP_THRESHOLD, 64 * 1024);
if (!ret)
fprintf(stderr, "mallopt failed! \n");
- malloc_usable_size( ):
用于查询一块已分配内存中有多少可用内存:
#include
size_t malloc_usable_size(void *ptr);
- 因为glibc可能扩大动态内存来适应一个已存在的块或者匿名映射,动态存储器分配中的可使用空间可能会比请求的大。当然,永远不可能比请求的小。
size_t len = 21;
size_t size;
char *buf;
buf = malloc(len);
if (!buf) {
perror("malloc");
return -1;
}
size = malloc_usable_size(buf);
/* we can actually use 'size' bytes of 'buf' ... */
则我们可用过malloc_usable_size( )这个函数得到ptr指向的内存中实际可用大小。
- malloc_trim( ):
该函数允许程序强制glibc归还所有的可释放的动态内存给内核:
#include
int malloc_trim(size_t padding);
- 调用malloc_trim( )成功时,数据段会尽可能地收缩,但是填充字节数被保留下来。成功时返回1, 失败时返回0。
- 一般来说,每当空闲的内存到达M_TRIM_THRESHOLD字节时,glibc会自动做这种收缩。使用M_TOP_PAD来做填充。