背景 今天在学习Linux内存相关知识的时候,看到了虚拟地址相关的内容,所以记录下虚拟地址空间、堆和栈相关的知识。由于我看的文章中内核版本比较老,所以就记录一下,后续学习中会整理关于新版本内核的知识。Linux虚拟地址空间在IA-32下,虚拟地址空间通常是一个4GB的地址块,通常按3:1划分为用户空间和内核空间。3:1不是唯一的选择。由于边界定义在源码中定义为常量,所以选择其他划分方式基本没有工作量。在某些情况下,最好按对称性(1:1)进行划分。它可以由__page_offset定义。但这并不意味着内核只有那么多的物理内存可用,它只意味着他可以控制这部分地址空间,并根据需要将其映射到物理内存。虚拟内存段通过表将虚拟地址映射到物理内存,由操作系统维护。内核空间在页表中具有更高的特权级别。当用户态程序访问这些页面时,会引起页面错误(Pagefault)。内核空间持久存在,可以映射到所有进程中的同一物理内存。内核代码和数据总是可寻址的,相比之下,用户态地址空间映射随着进程切换而不断变化。用户进程段用户进程段命名如下:栈局部变量、函数参数、返回地址等堆动态分配内存[ brk() ]BSS段未初始化或初始值为0globalvariablesand静态局部变量数据段已经初始化,初始值不为0。全局变量和静态局部变量代码段可执行代码,字符串字面值,只读变量堆由程序员自己管理,显示应用并释放;bss段,数据段而代码段是可执行程序编译时的段堆和栈是程序运行时的段分段详细解释内核空间 内核一直驻留在内存中,是一部分操作系统的权限,不允许应用程序读取写入此区域,或直接调用内核代码定义的函数。栈(Stack) 栈,又称堆栈,由编译器自动分配和释放,其行为类似于数据结构中的栈(先进后出)。堆栈主要有三个用途:1. 为函数内部声明的非静态局部变量(C语言中称为“自动变量”)提供存储空间。2. 记录函数调用过程相关的维护信息,称为栈帧(StackFrame)或过程激活记录(ProcedureActivationRecord)。它包括函数返回地址、不适合寄存器的函数参数以及一些寄存器值的保存。除了递归调用,栈不是必需的。因为局部变量、参数和返回地址所需的空间在编译时就可以知道,并分配在BSS段中。3. 暂存区,用于暂存长算术表达式的部分计算结果或alloca()函数分配的栈内存。 持续重用堆栈空间有助于在CPU缓存中保持活跃的堆栈内存,从而加快访问速度。进程中的每个线程都有自己的栈。当连续向栈中压入数据时,如果超过其容量,栈对应的内存区域就会被耗尽,从而引发页面错误。这时,如果栈的大小低于栈的最大值RLIMIT_STACK(一般为8M),栈会动态增长,程序会继续运行。映射栈区扩大到需要的大小后,就不再缩小了。 Linux中的ulimit-s命令可以查看和设置栈的最大值。当程序使用的栈超过这个值时,就会发生栈溢出(StackOverflow),程序收到段错误(SegmentationFault)。请注意,增加堆栈大小可能会增加内存开销和启动时间。堆栈可以向下(朝向较低的内存位置)或向上增长,具体取决于实现。本文描述的栈是向下增长的。堆栈的大小由内核在运行时动态调整。内存映射段(mmap)内核直接将硬盘文件的内容映射到内存中,任何应用程序都可以通过Linux的mmap()系统调用或Windows的CreateFileMapping()/MapViewOfFile()来请求这种映射。内存映射是一种方便高效的文件I/O方式,因此被用来加载动态共享库。用户也可以创建匿名内存映射,它没有对应的文件,可以用来存储程序数据。在Linux中,如果通过malloc()请求大块内存,C运行时将创建一个匿名内存映射,而不是使用堆内存。“大块”是指大于阈值MMAP_THRESHOLD,默认为128KB,可以通过mallopt()调整。该区域用于映射可执行文件使用的动态链接库。在Linux2.4版本中,如果可执行文件依赖共享库,系统会在0x40000000开始的地址为这些动态库分配相应的空间,并在程序加载时加载到这个空间。在Linux2.6内核中,共享库的起始地址被上移到更靠近堆栈区的位置。从进程地址空间的布局可以看出,在共享库的情况下,堆的可用空间还有两个:一个是.bss段到0x40000000,不足1GB;另一个是共享库和栈之间的空间小于2GB。这两个空间的大小取决于堆栈和共享库的大小和数量。这样看来,一个应用程序最多可以申请的堆空间是不是只有2GB?其实跟Linux内核版本有关系。在上面给出的进程地址空间的经典布局图中,共享库的加载地址为0x40000000,这实际上是Linux内核2.6版本之前的情况。在2.6版本中,共享库的加载地址已经被移到了靠近栈的位置,因此此时的堆范围不会被共享库分成两个“碎片”。因此,在内核为2.6的32位Linux系统中,malloc申请的理论最大内存值约为2.9GB。堆(heap)堆用于存放进程运行时动态分配的内存段,可以动态扩容或缩容。堆中的内容是匿名的,不能通过名称直接访问,只能通过指针间接访问。当进程调用malloc(C)/new(C++)等函数分配内存时,将新分配的内存动态添加到堆中(扩容);当调用free(C)/delete(C++)等函数释放内存时,释放的内存将从堆中剔除(收缩)。分配的堆内存是适合原子操作的字节对齐空间。堆管理器通过链表管理每个应用程序的内存。由于堆的申请和释放是乱序的,最终会产生内存碎片。堆内存一般由应用程序分配和释放,回收的内存可以重复使用。如果程序员不释放,操作系统可能会在程序结束时自动回收。堆的末尾由中断指针标记。当堆管理器需要更多内存时,可以通过系统调用brk()和sbrk()移动中断指针来扩展堆,通常由系统自动调用。使用堆时经常会出现两类问题:1)释放或覆盖仍在使用的内存(“内存损坏”);2)不释放不再使用的内存(“内存泄漏”)。当释放次数小于请求次数时,可能已经造成内存泄漏。泄漏的内存往往比忘记释放的数据结构要大,因为分配的内存通常会四舍五入到大于请求量的2次方(比如请求的212B,会四舍五入到256B)。注意,堆不同于数据结构中的“堆”,它的行为类似于链表。堆和栈的区别①管理方式:栈由编译器自动管理;堆由程序员控制,使用方便,但容易出现内存泄漏。②增长方向:栈向低地址扩展(即“向下增长”),是一块连续的内存区域;堆向高地址扩展(即“向上增长”),这是一个不连续的内存区域。这是因为系统使用链表来存储空闲内存地址,自然是不连续的,链表是从低地址向高地址遍历的。③空间大小:栈顶地址和栈的最大容量由系统预先确定(通常默认为2M或10M);堆的大小受限于计算机系统中的有效虚拟内存,32位Linux系统中堆内存可达2.9G空间。④存储内容:栈在被函数调用时,先压入调用函数中下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数参数,再压入调用函数的局部变量称为函数。本次调用结束后,先出栈局部变量,再出栈参数,最后栈顶指针指向第一条存储指令的地址,程序继续运行下一条可执行语句从这点来说。堆在头部通常用一个字节来存储它的大小,堆用来存储生命周期与函数调用无关的数据,具体内容由程序员安排。⑤分配方式:栈可以静态分配,也可以动态分配。静态分配由编译器完成,就像局部变量的分配一样。动态分配使用alloca函数在栈上申请空间,用完自动释放。堆只能动态分配和手动释放。⑥分配效率:栈由计算机底层支持:分配一个专门的寄存器来存放栈地址,push和pop由专门的指令执行,效率高。堆由函数库提供,机制复杂,效率比栈低很多。在Windows系统中,VirtualAlloc可以直接在进程地址空间分配一块内存,快速灵活。⑦分配后系统响应:只要栈的剩余空间大于申请的空间,系统就会为程序提供内存,否则会报异常,提示栈溢出。操作系统为堆维护一个空闲内存地址的链表。当系统收到程序申请内存分配时,会遍历链表找到第一个空间大于申请空间的堆节点,然后从空闲节点链表中删除该节点,并分配空间要编程的节点。如果空间不够(可能是内存碎片太多),可以调用系统函数增加程序数据段的内存空间,这样才有机会分配足够的内存,然后返回,大部分系统都会在内存空间的首地址记录分配的内存大小,以便后续的释放函数(如free/delete)正确释放内存空间。另外,由于查找到的堆节点的大小不一定正好等于请求的大小,系统会自动将多余的部分放回空闲链表中。⑧碎片问题:栈不会有碎片问题,因为栈是一个先进后出的队列,在内存块出栈之前,在它上面前进的栈的内容有被弹出。但是,频繁的申请和释放操作会造成堆内存空间的不连续,从而产生大量的碎片,降低程序效率。可见堆容易造成内存碎片;由于没有专门的系统支持,效率很低;因为可能会造成用户态和内核态的切换,内存申请的代价比较大。因此,栈在程序中的使用最为广泛,函数调用也是利用栈完成的。调用过程中的参数、返回地址、栈基指针和局部变量都存放在栈中。所以建议尽量使用栈,只有在分配大或大内存空间时才使用堆。在使用栈和堆时,应避免越界,否则可能导致程序崩溃或破坏程序堆和栈结构,造成意想不到的后果。bss段BSS(BlockStartedbySymbol)段在程序中通常存放以下符号:未初始化的全局变量和初始值为0的静态局部变量全局变量和静态局部变量(取决于编译器实现)是未定义的,初始的value不为0的符号(初始值为公共块的大小)在C语言中,未显式初始化的静态分配变量被初始化为0(算术类型)或空指针(指针类型)。由于BSS会在程序加载时被操作系统清空,所以没有初值或初值为0的全局变量都在BSS中。BSS段只为未初始化的静态分配变量保留位置,不占用目标文件空间,可以减小目标文件的大小。但是程序运行时需要为变量分配内存空间,所以目标文件必须记录所有未初始化的静态分配变量的大小之和(通过start_bss和end_bss地址写入机器码)。当加载程序加载程序时,为BSS段分配的内存被初始化为0。在嵌入式软件中,在进入main()函数之前,BSS段被C运行时系统映射到初始化为全零的内存(效率更高)).注意,虽然都放在BSS段,但是初始值为0的全局变量是强符号,而未初始化的全局变量是弱符号。如果在别处已经定义了同名的强符号(初始值可能不是0),弱符号链接到它时不会导致重定义错误,但运行时的初始值可能不是预期值(它将被强符号覆盖)。因此,在定义全局变量时,如果只在本文件中使用,尽量使用static关键字修饰;否则,需要给全局变量定义赋初值(即使是0值),保证变量是强符号,这样在链接时才可以发现变量名冲突,而不是被未知值覆盖.有些编译器把未初始化的全局变量存放在common段,链接时放到BSS段。在编译阶段,可以使用-fno-common选项禁止未初始化的全局变量放在common段。另外,由于目标文件不包含BSS段,程序烧录到内存(Flash)后,BSS段地址空间的内容是未知的。在U-Boot启动过程中,将U-BootStage2代码(通常位于lib_xxxx/board.c文件)重定位(复制)到SDRAM空间后,必须手动添加清除BSS段的代码,并且你不能依赖Stage2代码中的变量定义时赋值0。数据段(Data)数据段通常用来存放程序中已经初始化过且初始值不为0的全局变量和静态局部变量。数据段属于静态内存分配(静态存储区),即可读可写。数据段保存在目标文件中(在嵌入式系统中一般固化在镜像文件中),其内容由程序初始化。例如,对于全局变量intgVar=10,10的数据必须保存在目标文件的数据段中,然后在程序加载时复制到相应的内存中。数据段和BSS段的区别如下:1)BSS段不占用物理文件大小,而是占用内存空间;数据段占用物理文件,也占用内存空间。对于intar0[10000]={1,2,3,...}和intar1[10000]这样的大数组,ar1放在BSS段,只有records总共需要10000*4字节要初始化为0而不是像ar0一样记录每个数据1,2,3...。此时BSS为目标文件节省的磁盘空间是相当可观的。2)当程序读取数据段中的数据时,系统会触发缺页,分配相应的物理内存;当程序读取BSS段中的数据时,内核会将其转移到一个全零的页面中,不会发生页面错误,也不会为其分配相应的物理内存。运行时数据段和BSS段的整段一般称为数据区。在一些资料中,“数据段”是指数据段+BSS段+堆。代码段(文本)代码段也称为文本域或文本段,通常用于存放程序执行代码(即CPU执行的机器指令)。C语言的执行语句一般被编译成机器码存放在代码段中。通常代码段是可共享的,所以频繁执行的程序只需要在内存中有一个副本。代码段通常被归类为只读,以防止其他程序意外修改其指令(写入该段会导致段错误)。一些体系结构还允许代码段是可写的,这允许修改程序。代码段指令按照程序设计流程依次执行。对于顺序指令,它们只会被执行一次(每个进程);如果有重复,需要使用跳转指令;如果做递归,就需要用到栈来实现。代码段指令包括操作码和操作数对象(或对象地址引用)。如果操作对象是立即数(具体值),则直接包含在代码中;如果是本地数据,会在栈区分配空间,然后引用数据地址;如果位于BSS段和数据段,数据地址也会被引用。代码段最容易进行优化。参考:https://www.cnblogs.com/clove...
