堆和栈详解

  堆(heap)和栈(stack)是非常重要的概念,当我们进行程序开发时理解它们非常重要,尤其是对于嵌入式系统开发。比如在嵌入式系统中,任务的栈通常都很小,可能也就几K字节。在这种情况下,我们就应当尽可能不要将占用内存大的变量分配在栈上,而是应当分配在堆上;此外,也尽量不要采用递归的方式来设计程序,否则很容易造成栈溢出。
    
    从本质上说,堆和栈都是内存,那么我们只能从概念上对其进行区分了。为了方便说明,现在假设嵌入式软件是一个单体程序(这一术语并不是嵌入式系统开发中的专用术语,是我为了方便说明而使用的),也就是操作系统和我们的应用程序是被编译在同一个可执行程序当中的,比如,来自WindRiver的VxWorks就是采用这种方式的。我们知道一个可执行程序存在最为重要的三个段。.text段用于存放程序的代码,即放的是处理器的运行指令。.data用于存放初始化好的数据,当boot
loader(请参见《什么是boot loader》)加载程序文件时,会将程序文件中的.data段拷贝到内存的VMA(Virtual Memory Address,在《熟悉binutils工具集》中有所提及,在嵌入式系统中绝大部分不用虚拟内存,因此,VMA就是实地址)处,从而完成变量的初始化操作。虽然,我们在C/C++程序中是对全局变量一个一个初始化的,但实际上boot
loader是对所有的全局变量通过将程序文件中的.data段拷贝到内存中一次性的完成初始化的。.bss段用于存放没有初始化好的变量,程序文件中并不存放.bss段的具体内容,只是存有.bss段的起始地址和大小,当boot loader加载我们的嵌入式程序文件时,只是根据程序文件中的.bss信息对内存中的.bss块进行清零操作。
    
    boot loader与我们的单体程序共存的一个内存和FLASH映射快照。其中我们假设内存的大小是8M字节。可以看出在FLASH上即存放了boot loader程序,又存放了我们的单体程序,至于FLASH上是否有文件系统我们在此并不用关心。在内存中你可以看出也存在一块boot loader区,这一块区是由FLASH中的boot loader将自己加载到内存中的,以便加快运行速度,可以想像这块内存区是最早被拷贝到内存中的。当内存中的boot
loader运行时,其会读取FLASH中的单体程序,这一单体程序通常是ELF格式的。其中的ELF头指示了各段的VMA地址和大小以及各段内容在单体程序中的偏移地址。boot loader通过ELF头信息,将.texe段和.data段从FLASH拷贝到内存中。显然,boot loader和单体程序在内存中所占用的地址空间是不能重叠的,否则当boot loader将单体程序从FLASH拷贝到内存时,会将其自身的内容给覆盖掉,从而造成自己无法正常运行。地址的规划是我们设计boot loader时需要考虑到的。图中我们只示例了一个中断向量表,其实,很有可能boot
loader内存区也有一个中断向量表,是供boot loader运行时用的。还有就是有一块临时的栈空间,这一空间可以是boot loader和单体程序共同使用的,对于共同使用,需要强调的是boot loader与单体程序并不会同时运行,当boot loader运行完了以后,会调转到单体程序的入口处开始运行,入口地址显然应当位于内存的.text段。而单体程序在一开始运行时(此时操作系统还没有起作用),仍是需要一块小的内存作为栈来使用,以便能进行函数调用。根据不同的设计,我们可以在单体程序运行的初始阶段使用与boot
loader相同的栈,当然也可以使用不同的栈。这里我们假设使用相同的栈。从图1中,我们可以看出,内存中还存在很大的一块闲置区,这一块区暂时还没有使用用途。

 

    一旦boot loader运行了我们的单体程序,我们说boot loader就不存在了,那此时boot loader所占用的内存空间也就释放出来了,如图 2所示。从图中可以看出内存中的闲置空间加大了。那闲置空间被我们的单体程序用来做什么呢?做堆!在单体程序中的操作系统部分,会提供一定的管理模块来管理这块堆,并提供API(Application Programming Interface,应用程序编程接口)让我们调用,从而实现从堆中分配或是释放内存,这些API类似于C语言中的malloc
()/free ()。堆在管理上有一个特点,从堆中分配出来的内存应当是以某一大小字节为边界的。比如,如果CPU中的double类型是占用内存最多的数据类型且是8字节,那么堆分配出来的内存就必须保证是以8字节为边界的。这一点请读者想一想为什么?除了采用动态的内存分配,在嵌入式系统中通常还会采用固定大小内存块的分配方法,这种分配方法的好处是非常的快,而且这种内存在使用的过程中不会产生内存碎片。

    堆我们说过了,那接下来我们看一看如果我们的单体程序继续运行,会出现什么样的内存布局。我们知道,通常我们的单体程序在初始化时往往需要创建多个任务来实现其应用功能。对于每一个任务,它一块内存是私有的,那就是栈!当任务运行时,其需要用栈来做为函数调用时的参数传递空间,以及用栈来存储函数内的局部变量。假设我们的单体程序需要创建两个任务A和B,这需要通过调用操作系统中的任务创建函数来达到这一目的。操作系统所提供的任务创建API往往需要我们指定任务栈的大小,有的甚至可以指定栈内存空间。一旦任务创建的API被调用,那么操作系统会调用堆分配API为任务分配栈,此时的内存布局如图
3所示。任务创建完了以后,各任务就可以根据应用程序逻辑的需要审请堆空间以实现其业务逻辑。

    对于堆我们已经知道了必须调用相应的API来分配内存,那从栈空间分配内存也需要调用API吗?答案是通常不需要,为什么是通常?因为,在有的平台上(Linux上就是)提供栈空间的分配API,即这种API被调用时,是从调用任务的栈空间中分配内存的。对于这一功能,在嵌入式系统中使用得非常的少,我也不建议大家使用。对于下面的代码,mem_main、mem_foo和mem_bar的大小是4K字节(假设int类型的大小是4字节),这些内存就是自动(注意是自动)分配在运行任务的栈上的。我们假设某个任务当前所使用的栈是零字节,当这一任务运行到main中且没有进入foo
()时,其所占用的空间大小是大约4K字节,之所以用大约这个词,是因为函数的调用还有其它的栈开销。一旦任务运行进入foo ()函数但没有进入bar ()函数,那么所占用的栈的大小就变为大约8K字节。同样的,如果程序运行进入bar ()函数,那么所占用的栈空间大约就是12K字节了。

00001: void bar ()

00002: {

00003:     int mem_bar [1024];

00004:     // application logic

00005: }

00006:

00007: void foo ()

00008: {

00009:     int mem_foo [1024];

00010:     bar ();

00011: }

00012:

00013: int main ()

00014: {

00015:     int mem_main [1024];

00016:     foo ();

00017:     return 0;

00018: }

    如果程序继续运行,从bar ()函数返回到foo ()函数中,那么其所占用的栈空间就从大约12K字节变成了大约8K字节了。相类似的是,如果程序从foo ()函数中返回到main ()函数,那么所占用的栈空间又变为大约4K字节了。对于嵌入式系统开发,由于任务栈通常都比较的小,那这告诉我们什么呢?我想有以下几点需要注意。
    1)函数的调用深度越是深,由于每一级的函数通常都会有局部变量,那么所使用的栈空间也会累积得越大。
    2)递归调用需要的栈空间会相对的大(视具体的情况),在嵌入式系统中也建议少用。
    3)我们应当尽可能的不要在函数中定义占用内存空间较大的局部变量。

    下面,我们总结一下堆与栈的区别,它们是:
    1)堆是大家共享的。任务可以通过调用API来从堆中分配内存空间。
    2)栈是任务所独有的。在嵌入式系统中,当一个任务创建起来后其栈空间的大小往往是定了的。函数中的局部变量是由编程语言自动从栈上分配的,我们不需要调用API进行空间分配。
    
    最后我有一个问题留给读者您,这个问题是:

    前面的讲解中,我们说任务的栈是由操作系统的任务创建API从堆中分配出来的,那栈是否也可以位于.data段或是.bss段中呢?为什么?

答案
    由于堆从本质上说来就是一块内存,由于在C语言中一块内存可以从堆中分配,也可以从.data段或是.bss段中分配。因此,任务的栈也是可以从这三块内存中分配获得,也就是说最终的答案是:可以。

    

时间: 2024-08-15 05:35:04

堆和栈详解的相关文章

JavaScript数据结构与算法之栈详解

 这篇文章主要介绍了JavaScript数据结构与算法之栈详解,本文讲解了对栈的操作.对栈的实现实例等内容,需要的朋友可以参考下     在上一篇博客介绍了下列表,列表是最简单的一种结构,但是如果要处理一些比较复杂的结构,列表显得太简陋了,所以我们需要某种和列表类似但是更复杂的数据结构---栈.栈是一种高效的数据结构,因为数据只能在栈顶添加或删除,所以这样操作很快,而且容易实现. 一:对栈的操作. 栈是一种特殊的列表,栈内的元素只能通过列表的一端访问,这一端陈为栈顶.比如餐馆里面洗盘子,只能先洗

c语言stack(栈)和heap(堆)的使用详解_C 语言

一.预备知识-程序的内存分配 一个由C/C++编译的程序占用的内存分为以下几个部分 1.栈区(stack)-由编译器自动分配释放,存放函数的参数值,局部变量的值等.其操作方式类似于数据结构中的栈.2.堆区(heap)-一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收.注意它与数据结构中的堆是两回事,分配方式倒是类似于链表.3.全局区(静态区)(static)-全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另

C++中静态存储区与栈以及堆的区别详解_C 语言

学习c++如果不了解内存分配是一件非常可悲的事情.而且,可以这样讲,一个C++程序员无法掌握内存.无法了解内存,是不能够成为一个合格的C++程序员的.一.内存基本构成可编程内存在基本上分为这样的几大部分:静态存储区.堆区和栈区.他们的功能不同,对他们使用方式也就不同.静态存储区:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.它主要存放静态数据.全局数据和常量.栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放.栈内存分配运算

JavaScript数据结构与算法之栈详解_javascript技巧

在上一篇博客介绍了下列表,列表是最简单的一种结构,但是如果要处理一些比较复杂的结构,列表显得太简陋了,所以我们需要某种和列表类似但是更复杂的数据结构---栈.栈是一种高效的数据结构,因为数据只能在栈顶添加或删除,所以这样操作很快,而且容易实现. 一:对栈的操作. 栈是一种特殊的列表,栈内的元素只能通过列表的一端访问,这一端陈为栈顶.比如餐馆里面洗盘子,只能先洗最上面的盘子,盘子洗完后,也只能螺到这一摞盘子的最上面.栈被称为 "后入先出"(LIFO)的数据结构. 由于栈具有后入先出的特点

Struts2数据输入验证教程详解_java

一.前言 1.1.什么是输入验证?为什么需要输入验证? 在上一篇文章中,我们学习了数据类型转换,我们提到了表示层数据处理的两个方法,也提到了用户输入数据需要进行类型转换才能得到我们想要的数据,那么,我们怎么确定类型转换后的数据,是我们想要的数据呢?这里有点绕.你可以这样想:一个成年男子年龄是18岁,你现在想要得到18这个数据,但是,用户输入32,经过类型转换也是对的,但是数据不是你想要的.这时候,我们要怎么办?所以输入验证在这里就有用处了. 类型转换和输入验证的关系是:类型转换是输入验证的前提,

详解Java的堆内存与栈内存的存储机制_java

堆与内存优化    今天测了一个项目的数据自动整理功能,对数据库中几万条记录及图片进行整理操作,运行接近到最后,爆出了java.lang.outOfMemoryError,java heap space方面的错误,以前写程序很少遇到这种内存上的错误,因为java有垃圾回收器机制,就一直没太关注.今天上网找了点资料,在此基础上做了个整理.  一.堆和栈     堆-用new建立,垃圾回收器负责回收          1.程序开始运行时,JVM从OS获取一些内存,部分是堆内存.堆内存通常在存储地址的

Java中堆内存和栈内存详解

Java把内存分成两种,一种叫做栈内存,一种叫做堆内存. 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配.当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用. 堆内存用于存放由new创建的对象和数组.在堆中分配的内存,由java虚拟机自动垃圾回收器来管理.在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存

Java中堆和栈的区别详解_java

当一个人开始学习Java或者其他编程语言的时候,会接触到堆和栈,由于一开始没有明确清晰的说明解释,很多人会产生很多疑问,什么是堆,什么是栈,堆和栈有什么区别?更糟糕的是,Java中存在栈这样一个后进先出(Last In First Out)的顺序的数据结构,这就是java.util.Stack.这种情况下,不免让很多人更加费解前面的问题.事实上,堆和栈都是内存中的一部分,有着不同的作用,而且一个程序需要在这片区域上分配内存.众所周知,所有的Java程序都运行在JVM虚拟机内部,我们这里介绍的自然

详解堆的javascript实现方法_javascript技巧

堆的定义 最大(最小)堆是一棵每一个节点的键值都不小于(大于)其孩子(如果存在)的键值的树.大顶堆是一棵完全二叉树,同时也是一棵最大树.小顶堆是一棵完全完全二叉树,同时也是一棵最小树. 另外,记住这两个概念,对写代码太重要了:       1.父节点和子节点的关系:看定义       2.完全二叉树:参考[2] 基本操作       1.Build(构建堆)       2.Insert(插入)       3.Delete(删除:最小或者最大的那个) 代码实现 首先,写代码前有两个非常重要的点