堆(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块进行清零操作。
图 1示例了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中,我们可以看出,内存中还存在很大的一块闲置区,这一块区暂时还没有使用用途。
![](https://i-blog.csdnimg.cn/blog_migrate/be74583e912be87f0979c4b13049d55a.png)
图 1
![](https://i-blog.csdnimg.cn/blog_migrate/05ccff73d1c8bb30099fac45c464ac44.png)
图 2
![](https://i-blog.csdnimg.cn/blog_migrate/cc29ddcc978d55c0c8be46e7390f625a.png)
图 3
对于堆我们已经知道了必须调用相应的API来分配内存,那从栈空间分配内存也需要调用API吗?答案是通常不需要,为什么是通常?因为,在有的平台上(Linux上就是)提供栈空间的分配API,即这种API被调用时,是从调用任务的栈空间中分配内存的。对于这一功能,在嵌入式系统中使用得非常的少,我也不建议大家使用。对于下面的代码,mem_main、mem_foo和mem_bar的大小是4K字节(假设int类型的大小是4字节),这些内存就是自动(注意是自动)分配在运行任务的栈上的。我们假设某个任务当前所使用的栈是零字节,当这一任务运行到main中且没有进入foo ()时,其所占用的空间大小是大约4K字节,之所以用大约这个词,是因为函数的调用还有其它的栈开销。一旦任务运行进入foo ()函数但没有进入bar ()函数,那么所占用的栈的大小就变为大约8K字节。同样的,如果程序运行进入bar ()函数,那么所占用的栈空间大约就是12K字节了。
- void bar ()
- {
- int mem_bar [1024];
- // application logic
- }
-
- void foo ()
- {
- int mem_foo [1024];
- bar ();
- }
-
- int main ()
- {
- int mem_main [1024];
- foo ();
- return 0;
- }
1)函数的调用深度越是深,由于每一级的函数通常都会有局部变量,那么所使用的栈空间也会累积得越大。
2)递归调用需要的栈空间会相对的大(视具体的情况),在嵌入式系统中也建议少用。
3)我们应当尽可能的不要在函数中定义占用内存空间较大的局部变量。
下面,我们总结一下堆与栈的区别,它们是:
1)堆是大家共享的。任务可以通过调用API来从堆中分配内存空间。
2)栈是任务所独有的。在嵌入式系统中,当一个任务创建起来后其栈空间的大小往往是定了的。函数中的局部变量是由编程语言自动从栈上分配的,我们不需要调用API进行空间分配。
最后我有一个问题留给读者您,这个问题是:
答案
由于堆从本质上说来就是一块内存,由于在C语言中一块内存可以从堆中分配,也可以从.data段或是.bss段中分配。因此,任务的栈也是可以从这三块内存中分配获得,也就是说最终的答案是:可以。