Linux内存分配原理
- 虚拟内存分区
- Linux内存分配时的maps文件
- brk()与sbrk()
- mmap()与munmap()
- mmap()
- munmap()
- 内存分配过程
更多资讯、知识,微信公众号搜索:“上官宏竹”。
虚拟内存分区
虚拟内存由于用途不同,分类也不尽相同,一般我们对于内存的分类也就这几种:栈区(stack area)、堆区(heap area)、全局区(静态区)(存放全局变量与静态变量static)、BSS段(存放未初始化的全局变量,未初始化的全局变量默认值为0)、文字常量区、数据区(data area)、代码区(code area)等。
栈区
栈区的内存空间由系统管理 --> 即方法调用开始时开辟空间,方法调用结束时回收空间。
栈区是从高地址向低地址扩展,是一块连续的内存区域,遵循先进后出,后进先出(FILO)原则,使用效率高。
栈区的内存空间是在运行时由系统进行分配。
哪些变量是栈区的?例如方法的入参,内部定义的局部变量等,都存放在栈区。
堆区
最大的特点 --> 空间大,需程序员自己手动管理。(当然,在ARC时代也是系统自动管理的)
堆区是从低地址向高地址扩展,与栈区相反,遵循先进先出(FIFO)的原则。
堆区由系统api开辟空间(c/c++ --> malloc、calloc、realloc, oc --> alloc new),这个空间可以是不连续的,以链表结构存在,
开辟出的空间的首地址是在栈区,例如LGPerson *person = [[LGPerson alloc] init];这个person指向所指向的地址是在栈区。
内存回收 --> free回收,做了两件事,一是释放堆区的内存,二是将栈区的指针置为nil。
全局静态区
存放全局变量 和 静态变量。
内存空间也是由系统管理 -->程序启动时开辟,程序结束时回收,程序执行期间一直存在。
static修饰的变量仅执行一次,生命周期为整个程序运行期。
常量区
存放常量(整型、字符型,浮点型,字符串等),整个程序运行期不能被改变。
已初始化的全局变量
已初始化的静态变量
空间由系统管理,生命周期为整个程序运行期。
代码区
存放程序执行的CPU指令,一种二进制文件。(编译期将代码转换为CPU指令)
Linux内存分配时的maps文件
# include<stdio.h>
# include<stdlib.h>
int num1;/*BSS段*/
int num2 = 2;/*全局区*/
char * str1 = "str1";/*文字常量区*/
int main(void){
printf("%d\n",getpid());/*获取当前进程id号*/
int num3 = 3;/*栈区*/
static int num4 = 4;/*全局区*/
const int num5 = 5;/*栈区*/
char * str2 = "str2";/*文字常量区*/
char str3[] = "str3";/*栈区*/
int * p = malloc(sizeof(0));/*&p在栈区,p在堆区*/
printf("num1:%p\nnum2:%p\nnum3:%p\nnum4:%p\nnum5:%p\n",&num1,&num2,&num3,&num4,&num5);
printf("str1:%p\nstr2:%p\nstr3:%p\n",str1,str2,str3);
printf("&p:%p\np:%p\n",&p,p);
while(1){}/*死循环以保证进程不会结束,方便查看/proc/pid/maps文件*/
free(p);
return 0;
}
参考:内存管理与分页机制
brk()与sbrk()
参考:系统调用与内存管理(sbrk、brk、mmap、munmap)
mmap()与munmap()
mmap()
//函数原型:
#incldue<sys/mman.h>
void * mmap(void * addr, size_t length,int prot,int flags,int fd,off_t offset);
mmap函数(地址映射):mmap将一个文件或者其它对象映射进内存。
文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零(Linux堆空间未使用内存均清零)。这里我们只研究mmap的内存映射,而暂时不讨论文件方面的问题。
mmap内存地址映射区域是在堆、栈之间的区域,如下图所示的文件映射部分。
- 参数
(1)、addr:
起始地址,置零让系统自行选择并返回即可。
(2)、length:
长度,不够一页会自动凑够一页的整数倍,我们可以宏定义#define MIN_LENGTH_MMAP 4096为一页大小。
(3)、prot:
读写操作权限,PROT_READ可读、PROT_WRITE可写、PROT_EXEC可执行、PROT_NONE映射区域不能读取。(注意PROT_XXXXX与文件本身的权限不冲突,如果在程序中不设定任何权限,即使本身存在读写权限,该进程也不能对其操作)。
(4)、flags常用标志:
① MAP_SHARED【share this mapping】、MAP_PRIVATE【Create a private copy-on-write mapping】
MAP_SHARED只能设置文件共享,不能地址共享,即使设置了共享,对于两个进程来说,也不会生效。而MAP_PRIVATE则对于文件与内存都可以设置为私有。
② MAP_ANON【Deprecated】、MAP_ANONYMOUS:匿名映射,如果映射地址需要加该参数,如果不加默认映射文件。MAP_ANON已经过时,只需使用MAP_ANONYMOUS即可。
(5)、fd:文件描述符。
(6)、offset:文件描述符偏移量
(fd和offset对于一般性内存分配来说设置为0即可) - 返回值
失败返回MAP_FAILED,即(void * (-1))并设置errno全局变量。
成功返回指向mmap area的指针pointer。 - 常见errno错误
①ENOMEM:内存不足;
②EAGAIN:文件被锁住或有太多内存被锁住;
③EBADF:参数fd不是有效的文件描述符;
④EACCES:存在权限错误,。如果是MAP_PRIVATE情况下文件必须可读;使用MAP_SHARED则文件必须能写入,且设置prot权限必须为PROT_WRITE。
⑤EINVAL:参数addr、length或者offset中有不合法参数存在。
munmap()
munmap函数:解除映射关系
// addr为mmap函数返回接收的地址,length为请求分配的长度。
int munmap(void * addr, size_t length);
内存分配过程
从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。brk是将数据段(.data)的最高地址指针_edata往高地址推,mmap是在进程的虚拟地址空间中(一般是堆和栈中间)找一块空闲的。
这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。
在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。
下面以一个例子来说明内存分配的原理:
1 进程启动的时候,其虚拟内存空间的初始布局如图1所示。其中,mmap内存映射文件是在堆和栈的中间(例如libc-2.2.93.so,其它数据文件等),为了简单起见,省略了内存映射文件。_edata指针(glibc里面定义)指向数据段的最高地址。
2 进程调用A=malloc(30K)以后,内存空间如图2:malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配。你可能会问:只要把_edata+30K就完成内存分配了?事实是这样的,_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的。
3进程调用B=malloc(40K)以后,内存空间如图3.
4 进程调用C=malloc(200K)以后,内存空间如图4:默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。这样子做主要是因为brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的),而mmap分配的内存可以单独释放。当然,还有其它的好处,也有坏处,再具体下去,有兴趣的同学可以去看glibc里面malloc的代码了。
5 进程调用D=malloc(100K)以后,内存空间如图5.
6 进程调用free©以后,C对应的虚拟内存和物理内存一起释放
7 进程调用free(B)以后,如图7所示。B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,如果往回推,那么D这块内存怎么办呢?当然,B这块内存,是可以重用的,如果这个时候再来一个40K的请求,那么malloc很可能就把B这块内存返回回去了。
8 进程调用free(D)以后,如图8所示。B和D连接起来,变成一块140K的空闲内存。
9 默认情况下:当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩,变成图9所示。
更多资讯、知识,微信公众号搜索:“上官宏竹”。