每一个进程都会对应一个虚拟地址空间,32位操作系统会为每个进程分配4G(2的32次方)的虚拟地址空间,而MMU(Memory Management Unit,内存管理单元)负责把这4G虚拟内存映射到实际的物理内存中。这4G空间分为用户空间和内核空间两部分。Windows系统下,用户空间和内核空间以2:2比例划分,Linux系统下用户空间和内核空间以3:1划分。内核空间由所有进程共享,用户空间各自独立,程序是无法直接访问内核空间的。 虚拟地址空间分布如下图所示(以Linux系统为例):
1.保留区(受保护的地址)
保留区即为受保护的地址,大小为128M,位于虚拟地址空间的最低部分,未赋予物理地址。任何对它的引用都是非法的,用于捕捉使用空指针和小整型值指针引用内存的异常情况。
它并不是一个单一的内存区域,而是对地址空间中受到操作系统保护而禁止用户进程访问的地址区域的总称。大多数操作系统中,极小的地址通常都是不允许访问的,如NULL。C语言将无效指针赋值为0也是出于这种考虑,因为0地址上正常情况下不会存放有效的可访问数据。。
2.代码段
代码段也称正文段或文本段,通常用于存放程序执行代码(即CPU执行的机器指令)。一般C语言执行语句都编译成机器代码保存在代码段。通常代码段是可共享的,因此频繁执行的程序只需要在内存中拥有一份拷贝即可。代码段通常属于只读,以防止其他程序意外地修改其指令(对该段的写操作将导致段错误)。某些架构也允许代码段为可写,即允许修改程序。
3.数据段(.data段)
数据段通常用于存放程序中已初始化的全局变量和静态局部变量。数据段属于静态内存分配(静态存储区),可读可写。由于全局变量未初始化时,其默认值为0,因此值为0的全局变量位于.bss段(不位于数据段)。对于未初始化的局部变量,其值是不可预测的。注意:在代码段和数据段之间还包括其它段:只读数据段和符号段等。
4. .bss段
该段用于存放未初始化的全局变量和静态局部变量,包括值为0的全局变量。 数据段和.bss段又称为全局数据区,前者初始化,后者未初始化。
ELF段包括:代码段、其它段(在.data段和.text段之间,包括只读数据段和符号段等)、.data段(数据段)和.bss段,都属于可执行程序部分。
BSS是“Block Started bySymbol”的缩写,意为“以符号开始的块”。
BSS是Unix链接器产生的未初始化数据段。其他的段分别是包含程序代码的“text”段和包含已初始化数据的“data”段。BSS段的变量只有名称和大小却没有值。此名后来被许多文件格式使用,包括PE。“以符号开始的块”指的是编译器处理未初始化数据的地方。BSS节不包含任何数据,只是简单的维护开始和结束的地址,以便内存区能在运行时被有效地清零。BSS节在应用程序的二进制映象文件中并不存在。
在采用段式内存管理的架构中(比如intel的80x86系统),bss段(Block Started by Symbolsegment)通常是指用来存放程序中未初始化的全局变量的一块内存区域,一般在初始化时bss段部分将会清零。bss段属于静态内存分配,即程序一开始就将其清零了。
比如,在C语言之类的程序编译完成之后,已初始化的全局变量保存在.data段中,未初始化的全局变量保存在.bss 段中。
text和data段都在可执行文件中(在嵌入式系统里一般是固化在镜像文件中),由系统从可执行文件中加载;而bss段不在可执行文件中,由系统初始化。
5.堆空间
new( )和malloc( )函数分配的空间就属于堆空间。
分配的堆内存是经过字节对齐的空间,以适合原子操作。堆管理器通过链表管理每个申请的内存,由于堆申请和释放是无序的,最终会产生内存碎片。堆内存一般由应用程序分配释放,回收的内存可供重新使用。若程序员不释放,程序结束时操作系统可能会自动回收。
堆的末端由break指针标识,当堆管理器需要更多内存时,可通过系统调用brk()和sbrk()来移动break指针以扩张堆,一般由系统自动调用。
使用堆时经常出现两种问题:1) 释放或改写仍在使用的内存(“内存破坏”);2)未释放不再使用的内存(“内存泄漏”)。当释放次数少于申请次数时,可能已造成内存泄漏。泄漏的内存往往比忘记释放的数据结构更大,因为所分配的内存通常会圆整为下个大于申请数量的2的幂次(如申请212B,会圆整为256B)。
注意,堆不同于数据结构中的”堆”,其行为类似链表。
6.内存映射段(共享库)
内核将硬盘文件的内容直接映射到内存, 任何应用程序都可通过Linux的mmap()系统调用请求这种映射。内存映射是一种方便高效的文件I/O方式, 因而被用于装载动态共享库。如C标准库函数(fread、fwrite、fopen等)和Linux系统I/O函数,它们都是动态库函数,其中C标准库函数都被封装在了/lib/libc.so库文件中,都是二进制文件。这些动态库函数都是与位置无关的代码,即每次被加载进入内存映射区时的位置都是不一样的,因此使用的是其本身的逻辑地址,经过变换成线性地址(虚拟地址),然后再映射到内存。而静态库不一样,由于静态库被链接到可执行文件中,因此其位于代码段,每次在地址空间中的位置都是固定的。
7.栈空间
用于存放局部变量(非静态局部变量,C语言称为自动变量),分配存储空间时从上往下。
【扩展阅读】栈和堆的区别1.管理方式:栈由编译器自动管理;堆由程序员控制,使用方便,但易产生内存泄露。2.生长方向:栈向低地址扩展(即”向下生长”),是连续的内存区域;堆向高地址扩展(即”向上生长”),
是不连续的内存区域。这是由于系统用链表来存储空闲内存地址,自然不连续,而链表从低地址向高地址遍历。3.空间大小:栈顶地址和栈的最大容量由系统预先规定(通常默认2M或10M);
堆的大小则受限于计算机系统中有效的虚拟内存,32位Linux系统中堆内存可达2.9G空间。4.存储内容:栈在函数调用时,首先压入主调函数中下条指令(函数调用语句的下条可执行语句)的地址,
然后是函数实参,然后是被调函数的局部变量。本次调用结束后,局部变量先出栈,
然后是参数,最后栈顶指针指向最开始存的指令地址,程序由该点继续运行下条可执行语句。
堆通常在头部用一个字节存放其大小,堆用于存储生存期与函数调用无关的数据,具体内容由程序员安排。5.分配方式:栈可静态分配或动态分配。静态分配由编译器完成,如局部变量的分配。
动态分配由alloca函数在栈上申请空间,用完后自动释放。堆只能动态分配且手工释放。6.分配效率:栈由计算机底层提供支持:分配专门的寄存器存放栈地址,压栈出栈由专门的指令执行,
因此效率较高。堆由函数库提供,机制复杂,效率比栈低得多。
Windows系统中VirtualAlloc可直接在进程地址空间中分配一块内存,快速且灵活。7.分配后系统响应:只要栈剩余空间大于所申请空间,系统将为程序提供内存,否则报告异常提示栈溢出。操作系统为堆维护一个记录空闲内存地址的链表。当系统收到程序的内存分配申请时,会遍历该链表寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点空间分配给程序。若无足够大小的空间(可能由于内存碎片太多),有可能调用系统功能去增加程序数据段的内存空间,以便有机会分到足够大小的内存,然后进行返回。大多数系统会在该内存空间首地址处记录本次分配的内存大小,供后续的释放函数(如free/delete)正确释放本内存空间。此外,由于找到的堆结点大小不一定正好等于申请的大小,系统会自动将多余的部分重新放入空闲链表中。8.碎片问题:栈不会存在碎片问题,因为栈是先进后出的队列,内存块弹出栈之前,在其上面的后进的栈内容已弹出。
而频繁申请释放操作会造成堆内存空间的不连续,从而造成大量碎片,使程序效率降低。可见,堆容易造成内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和内核态切换,内存申请的代价更为昂贵。所以栈在程序中应用最广泛,函数调用也利用栈来完成,调用过程中的参数、返回地址、栈基指针和局部变量等都采用栈的方式存放。所以,建议尽量使用栈,仅在分配大量或大块内存空间时使用堆。使用栈和堆时应避免越界发生,否则可能程序崩溃或破坏程序堆、栈结构,产生意想不到的后果。
8.命令行参数
该段用于存放命令行参数的内容:argc和argv。
9.环境变量
用于存放当前的环境变量,在Linux中用env命令可以查看其值。
10. 内核空间
内核总是驻留在内存中,是操作系统的一部分。内核空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数。
扩展:一个由c/C++编译的程序占用的内存分为以下几个部分1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。
其操作方式类似于数据结构中的栈。2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。
注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,
初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。
- 程序结束后有系统释放4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放5、程序代码区—存放函数体的二进制代码。
以下两段示例代码:
int a = 0; 全局初始化区char *p1; 全局未初始化区main(){
int b; 栈char s[] = "abc"; 栈char *p2; 栈char *p3 = "123456"; 123456在常量区,p3在栈上。static int c =0; 全局(静态)初始化区p1 = (char *)malloc(10);p2 = (char *)malloc(20);分配得来得10和20字节的区域就在堆区。strcpy(p1, "123456"); 123456放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。}
#include<stdio.h>int a; //未初始化全局区 .bss
int b=1; //已初始化全局区 .data
static int c=2; //已初始化全局区 .data
const int d=3; //只读数据段,也叫文字常量区 ro.data, d的值不能被修改
int main(void)
{int e=4; //栈区static int f=5; //已初始化全局区const int g=6; //栈区,不能通过变量名修改其值,但可通过其地址修改其值int *p=malloc(sizeof(int)) //指针变量p在栈区,但其所指向的4字节空间在堆区char *str="abcd"; //字符串“abcd”存在文字常量区,指针变量str在栈区,存的是“abcd”的起始地址return 0;
}
参考:
https://www.cnblogs.com/icmzn/p/11824802.html
https://www.cnblogs.com/clover-toeic/p/3754433.html