红联Linux门户
Linux帮助

浅析Linux下的堆内存管理

发布时间:2016-07-22 09:51:30来源:linux网站作者:caoyan_12727
最近在看一本叫《程序员的自我修养-链接.装载域库》(俞甲子,石凡,潘爱民著)这本书,不得不像大家安利这本书,从最基本的EFL文件到链接,再到装载,虽然现在还没看完,但是对程序的可执行文件和虚拟内存的布局有了进一步的了解,不得不说是一本好书。言归正传,本文讨论的话题是堆内存的管理,首先让我们来了解一下进程的虚拟地址空间的组成部分(如下图):
浅析Linux下的堆内存管理
首先来讨论一下这个program break,手册上说brk和sbrk会改变program break的位置,program break被定义为程序data segment的结束位置。感觉program break被定义成程序data segment的结束位置理解有点模糊,下面我们就用最基本的程序来验证。
 
1、程序代码如下:
#include <stdio.h> 
#include <unistd.h>  
#include <apue.h> 
int main(){
void* p = sbrk(0);  //获取当前program break的当前位置(此时是heap的起始地址)  
int* p1 = (int *)p; // 将当前空指针转化成int型指针  
brk(p1+4);   //分配了16个字节的空间  
p1[0] = 10;
p1[1] = 20;
p1[2] = 30;
p1[3] = 40;
p1[4] = 50;   
printf("%p\n",p1);  
printf("%p\n",p1+1);  
printf("%p\n",p1+2);  
printf("%p\n",p1+3);   
printf("%p\n",sbrk(0));//这个时候program break的值比较大,因为一次性分配了较大的空间,而不必每次都调用sbrk()来分配堆空间  
while(1){  
sleep(1); 
}  
}
首先我们将程序在后台运行(./a.out &),得到的结果是:
浅析Linux下的堆内存管理
然后,执行命令cat  /proc/18092/maps就可以得到进程堆的地址范围,如下图:
浅析Linux下的堆内存管理
可以看到我们进程堆的范围是09c2b000-09c4d000 rw-p 00000000 00:00 0  [heap],堆的起始地址和p1的地址一样,说明程序brk()在堆上申请空间,也就是说刚开始的时候program break和堆的起始地址是一样的。
 
2、我们对上面的程序进行修改,在这个基础上继续分配空间,如下:
#include <stdio.h> 
#include <unistd.h>  
#include <apue.h> 
int main(){
void* p = sbrk(0);  //获取当前program break的当前位置(此时是heap的起始地址)  
int* p1 = (int *)p; // 将当前空指针转化成int型指针  
brk(p1+4);
printf("program break is :%p\n",sbrk(0));//这个时候program break的值比较大,因为一次性分配了较大的空间,而不必每次都调用sbrk()来分配堆空间   
p1[0] = 10;  
printf("program break is %p\n",sbrk(0));
p1[1] = 20;
p1[2] = 30;
printf("program break is %p\n",sbrk(0));  
p1[3] = 40;
p1[4] = 50;   
printf("%d,%p\n",*p1,p1);  
printf("%d,%p\n",*(p1+1),p1+1);  
printf("%d,%p\n",*(p1+2),p1+2);  
printf("%d,%p\n",*(p1+3),p1+3);   
printf("program break is %p\n",sbrk(0));//这个时候program break的值比较大,因为一次性分配了较大的空间,而不必每次都调用sbrk()来分配堆空间   
}
运行结果为:
浅析Linux下的堆内存管理
那么问题来了,我们在调用brk()之后,program break的值为0x95f6010,刚好是从0x95f6000地址开始的16个字节,但当我们对空间进行赋值之后,program break的值就变成0x9618000,一下子就变大了很多,这是疑问?
 
3、我们继续修改程序:
#include <stdio.h> 
#include <unistd.h>  
#include <apue.h> 
int main(){
void* p = sbrk(0);  //获取当前program break的当前位置(此时是heap的起始地址)  
int* p1 = (int *)p; // 将当前空指针转化成int型指针  
int *p2,*temp;  
brk(p1+4);
printf("program break is :%p\n",sbrk(0));   
p1[0] = 10;
printf("program break is :%p\n",sbrk(0));   
p1[1] = 20;
p1[2] = 30;
p1[3] = 40;
p1[4] = 50;   
printf("%p\n",&p1[0]);  
printf("%p\n",&p1[1]);  
printf("%p\n",&p1[2]);  
printf("%p\n",&p1[3]);   
temp=p1+4;  
brk(temp+2);  
temp[0]=60;  
temp[1]=70;  
printf("%p\n",&temp[0]);  
printf("%p\n",&temp[1]);  
printf("program break is :%p\n",sbrk(0));   
}
输出结果为:
浅析Linux下的堆内存管理
这个时候我们看到program break的位置本来已经在一个足够大的位置上(0x962f000),但是在追加申请空间之后又变回到0x960d018(这个偏移量刚好为24个字节,是6个int型变量的大小),为什么program break从0x962f000回退到了0x960d018,这又是个疑问?
 
4、下面来看看第二个程序:
#include <stdio.h>  
#include <stdlib.h>  
#include<unistd.h>  
int main(){  
void* ptr, *ptr1,*ptr2;  
ptr = sbrk(0);  
printf("sbrk:%p\n", ptr);  
ptr1 = malloc(100);  
ptr = sbrk(0);  
printf("sbrk:%p, ptr1:%p\n", ptr, ptr1);  
free(ptr1);  
ptr2=malloc(50);  
ptr = sbrk(0);  
printf("sbrk:%p,ptr2:%p\n",ptr,ptr2);  
}  
输出结果为:
浅析Linux下的堆内存管理
我们可以看到ptr的当前地址为0x991f000,这个地址就是堆空间的起始地址,也是program break的位置,后来我们调用malloc()在堆上分配100字节的空间,这100字节空间起始地址为0x991f410(至于为什么和0x991f000相差这么多还没搞明白),如果是正真分配100字节,那么program break的位置应该是(0x991f410+100字节=0x991f474),但是我们显示program break当前位置(0x9940000)的时候却比0x991f474大很多,说明系统一次性就分配了比100字节大很多的堆空间,至于为什么?我想如果每次分配几字节或几十字节的空间都调用sbrk()函数的话,系统开销太大,所以索性一次分配足够大的空间。当进程需要另外申请空间时,系统可能的工作就是在这个一次性分配足够大的剩余空间里再分配。至于为什么不从0x991f000直接开始分配,初步设想是0x991f000-0x991f410这段堆空间肯能用作记录目前堆的分配情况。
 
本文永久更新地址:http://www.linuxdiyf.com/linux/22595.html