如何全面理解使用动态内存导致进程比使用堆栈内存慢?
自动变量使用的内存栈区分配的内存,而动态内存分配所用的内存是由堆区分配的内存
每一个进程都有自己的内存虚拟地址空间,内存最小的单位是页(page).
虚拟地址通过页表(Page Table)映射到物理内存,页表由操作系统维护并被CPU引用.
用户进程部分分段存储内容如下所示(从内存高地址到低地址):
栈(stack): 函数参数、返回地址、局部变量等
BSS段(bss): 未初始化或初值为0的全局变量和静态局部变量
数据段(data): 已初始化且初值非0的全局变量和静态局部变量
代码段(text): 可执行代码、字符串字面值、只读变量
内核总是驻留在内存中,是操作系统的一部分。内核空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数。
栈又称堆栈,由编译器自动分配释放,行为类似数据结构中的栈(先进后出)。
(1)为函数内部声明的非静态局部变量(C语言中称“自动变量”)提供存储空间。
它包括函数返回地址,不适合装入寄存器的函数参数及一些寄存器值的保存。
除递归调用外,堆栈并非必需。因为编译时可获知局部变量,参数和返回地址所需空间,并将其分配于BSS段。
(3)临时存储区,用于暂存长算术表达式部分计算结果或alloca()函数分配的栈内内存。
持续地重用栈空间有助于使活跃的栈内存保持在CPU缓存中,从而加速访问。
进程中的每个线程都有属于自己的栈。
向栈中不断压入数据时,若超出其容量就会耗尽栈对应的内存区域,从而触发一个页错误。
此时若栈的大小低于堆栈最大值RLIMIT_STACK(Linux通常是8M),则栈会动态增长,程序继续运行。
映射的栈区扩展到所需大小后,不再收缩。
注意,调高堆栈容量可能会增加内存开销和启动时间。
堆栈既可向下增长(向内存低地址)也可向上增长, 这依赖于具体的实现。本文所述堆栈向下增长。
堆栈的大小在运行时由内核动态调整。
此处,内核将硬盘文件的内容直接映射到内存,任何应用程序都可通过Linux的mmap()系统调用
内存映射是一种方便高效的文件I/O方式,因而被用于装载动态共享库。
用户也可创建匿名内存映射,该映射没有对应的文件,可用于存放程序数据。
在Linux中,若通过malloc()请求一大块内存,C运行库将创建一个匿名内存映射,而不使用堆内存。
该区域用于映射可执行文件用到的动态链接库。
在Linux 2.4内核中,若可执行文件依赖共享库,则系统会为这些动态库在从0x开始的地址分配相应空间,并在程序装载时将其载入到该空间。
在Linux 2.6内核中,共享库的起始地址被往上移动至更靠近栈区的位置。
从进程地址空间的布局可以看到,在有共享库的情况下,留给堆的可用空间还有两处:
一处是从BSS段到0x,约不到1GB的空间;另一处是从共享库到栈之间的空间,约不到2GB。
这两块空间大小取决于栈、共享库的大小和数量。这样来看,是否应用程序可申请的最大堆空间只有2GB?
事实上,这与Linux内核版本有关。
在上面给出的进程地址空间经典布局图中,共享库的装载地址为0x,这实际上是Linux kernel 2.6版本之前的情况了,
在2.6版本里,共享库的装载地址已经被挪到靠近栈的位置,即位于0xBFxxxxxx附近,
因此,此时的堆范围就不会被共享库分割成2个“碎片”,故kernel 2.6的32位Linux系统中,malloc申请的最大内存理论值在2.9GB左右。
堆用于存放进程运行时动态分配的内存段,可动态扩张或缩减。
堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问。
当进程调用malloc(C)/new(C++)等函数分配内存时,新分配的内存动态添加到堆上(扩张);
当调用free(C)/delete(C++)等函数释放内存时,被释放的内存从堆中剔除(缩减) 。
分配的堆内存是经过字节对齐的空间,以适合原子操作。
堆管理器通过链表管理每个申请的内存,由于堆申请和释放是无序的,最终会产生内存碎片。
堆内存一般由应用程序分配释放,回收的内存可供重新使用。
若程序员不释放,程序结束时操作系统可能会自动回收。
堆的末端由break指针标识,当堆管理器需要更多内存时,可通过系统调用brk()和sbrk()来移动break指针以扩张堆,一般由系统自动调用。
使用堆时经常出现两种问题:
(1) 释放或改写仍在使用的内存(“内存破坏”);
(2) 未释放不再使用的内存(“内存泄漏”)。当释放次数少于申请次数时,可能已造成内存泄漏。
泄漏的内存往往比忘记释放的数据结构更大,因为所分配的内存通常会圆整为下个大于申请数量的2的幂次(如申请212B,会圆整为256B)。
注意,堆不同于数据结构中的”堆”,其行为类似链表。
未初始化的全局变量和静态局部变量.
初始值为0的全局变量和静态局部变量(依赖于编译器实现).
未定义且初值不为0的符号(该初值即common block的大小).
C语言中,未显式初始化的静态分配变量被初始化为0(算术类型)或空指针(指针类型)。
由于程序加载时,BSS会被操作系统清零,所以未赋初值或初值为0的全局变量都在BSS中。
BSS段仅为未初始化的静态分配变量预留位置,在目标文件中并不占据空间,这样可减少目标文件体积。
但程序运行时需为变量分配内存空间,故目标文件必须记录所有未初始化的静态分配变量大小总和(通过start_bss和end_bss地址写入机器代码)。
当加载器(loader)加载程序时,将为BSS段分配的内存初始化为0。
在嵌入式软件中,进入main()函数之前BSS段被C运行时系统映射到初始化为全零的内存(效率较高)。
注意,尽管均放置于BSS段,但初值为0的全局变量是强符号,而未初始化的全局变量是弱符号。
若其他地方已定义同名的强符号(初值可能非0),则弱符号与之链接时不会引起重定义错误,但运行时的初值可能并非期望值(会被强符号覆盖)。
因此,定义全局变量时,若只有本文件使用,则尽量使用static关键字修饰;否则需要为全局变量定义赋初值(哪怕0值),
保证该变量为强符号,以便链接时发现变量名冲突,而不是被未知值覆盖。
某些编译器将未初始化的全局变量保存在common段,链接时再将其放入BSS段。
在编译阶段可通过-fno-common选项来禁止将未初始化的全局变量放入common段。
此外,由于目标文件不含BSS段,故程序烧入存储器(Flash)后BSS段地址空间内容未知。
搬迁(拷贝)到SDRAM空间后必须人为添加清零BSS段的代码,而不可依赖于Stage2代码中变量定义时赋0值。
BSS段不包含数据,仅维护开始和结束地址,以便内存能在运行时被有效地清零。
BSS所需的运行时空间由目标文件记录,但BSS并不占用目标文件内的实际空间,即BSS节段应用程序的二进制映象文件中并不存在。
数据段通常用于存放程序中已初始化且初值不为0的全局变量和静态局部变量。
数据段属于静态内存分配(静态存储区),可读可写。
数据段保存在目标文件中(在嵌入式系统里一般固化在镜像文件中),其内容由程序初始化。
例如,对于全局变量int gVar = 10,必须在目标文件数据段中保存10这个数据,然后在程序加载时复制到相应的内存。
数据段与BSS段的区别如下:
(1) BSS段不占用物理文件尺寸,但占用内存空间;数据段占用物理文件,也占用内存空间。
ar1放在BSS段,只记录共有10000*4个字节需要初始化为0,而不是像ar0那样记录每个数据1、2、3...,此时BSS为目标文件所节省的磁盘空间相当可观。
(2) 当程序读取数据段的数据时,系统会发出缺页故障,从而分配相应的物理内存;
当程序读取BSS段的数据时,内核会将其转到一个全零页面,不会发生缺页故障,也不会为其分配相应的物理内存。
运行时数据段和BSS段的整个区段通常称为数据区。某些资料中“数据段”指代数据段 + BSS段 + 堆。
代码段也称正文段或文本段,通常用于存放程序执行代码(即CPU执行的机器指令)。
一般C语言执行语句都编译成机器代码保存在代码段。
通常代码段是可共享的,因此频繁执行的程序只需要在内存中拥有一份拷贝即可。
代码段通常属于只读,以防止其他程序意外地修改其指令(对该段的写操作将导致段错误)。
某些架构也允许代码段为可写,即允许修改程序。
代码段指令根据程序设计流程依次执行,对于顺序指令,只会执行一次(每个进程);若有反复,则需使用跳转指令;若进行递归,则需要借助栈来实现。
代码段指令中包括操作码和操作对象(或对象地址引用)。若操作对象是立即数(具体数值),将直接包含在代码中;
若是局部数据,将在栈区分配空间,然后引用该数据地址;若位于BSS段和数据段,同样引用该数据地址。
代码段最容易受优化措施影响。
位于虚拟地址空间的最低部分,未赋予物理地址。任何对它的引用都是非法的,用于捕捉使用空指针和小整型值指针引用内存的异常情况。
它并不是一个单一的内存区域,而是对地址空间中受到操作系统保护而禁止用户进程访问的地址区域的总称。
大多数操作系统中,极小的地址通常都是不允许访问的,如NULL。
C语言将无效指针赋值为0也是出于这种考虑,因为0地址上正常情况下不会存放有效的可访问数据。
在32位x86架构的Linux系统中,用户进程可执行程序一般从虚拟地址空间0x开始加载。
该加载地址由ELF文件头决定,可通过自定义链接器脚本覆盖链接器默认配置,进而修改加载地址。
0x以下的地址空间通常由C动态链接库、动态加载器ld.so和内核VDSO(内核提供的虚拟共享库)等占用。
通过使用mmap系统调用,可访问0x以下的地址空间。
Linux虚拟地址空间布局
在栈上分配内存只需增加栈指针即可,函数返回后内存自然被释放。
而在堆上分配内存需要管理分配出的每一块内存块(一般是 glibc 管理的),分配时还需要寻找合适大小的内存块,开销自然大了不少。
建议把问题的表述修改一下好一点,所谓的“动态内存”应该改为在堆(heap)区分配的内存,所谓的“堆栈内存”直接改为栈(stack)区分配的内存。虽然一般说的堆栈段内存,其实就是指栈(stack)内存,把“堆”和“栈”分开,这样看起来才更有针对性。
有关“堆栈”的区别,以及相关内存分配的时期、结构的不同,可以参考:
重温c语言 一切都明白~
以上是 的全部内容, 来源链接: