堆和栈详解
                            
                            
                            ? 堆(heap)和棧(stack)是非常重要的概念,當我們進行程序開發(fā)時理解它們非常重要,尤其是對于嵌入式系統(tǒng)開發(fā)。比如在嵌入式系統(tǒng)中,任務(wù)的棧通常都很小,可能也就幾K字節(jié)。在這種情況下,我們就應(yīng)當盡可能不要將占用內(nèi)存大的變量分配在棧上,而是應(yīng)當分配在堆上;此外,也盡量不要采用遞歸的方式來設(shè)計程序,否則很容易造成棧溢出。
?? ?
??? 從本質(zhì)上說,堆和棧都是內(nèi)存,那么我們只能從概念上對其進行區(qū)分了。為了方便說明,現(xiàn)在假設(shè)嵌入式軟件是一個單體程序(這一術(shù)語并不是嵌入式系統(tǒng)開發(fā)中的專用術(shù)語,是我為了方便說明而使用的),也就是操作系統(tǒng)和我們的應(yīng)用程序是被編譯在同一個可執(zhí)行程序當中的,比如,來自WindRiver的VxWorks就是采用這種方式的。我們知道一個可執(zhí)行程序存在最為重要的三個段。.text段用于存放程序的代碼,即放的是處理器的運行指令。.data用于存放初始化好的數(shù)據(jù),當boot loader(請參見《什么是boot loader》)加載程序文件時,會將程序文件中的.data段拷貝到內(nèi)存的VMA(Virtual Memory Address,在《熟悉binutils工具集》中有所提及,在嵌入式系統(tǒng)中絕大部分不用虛擬內(nèi)存,因此,VMA就是實地址)處,從而完成變量的初始化操作。雖然,我們在C/C++程序中是對全局變量一個一個初始化的,但實際上boot loader是對所有的全局變量通過將程序文件中的.data段拷貝到內(nèi)存中一次性的完成初始化的。.bss段用于存放沒有初始化好的變量,程序文件中并不存放.bss段的具體內(nèi)容,只是存有.bss段的起始地址和大小,當boot loader加載我們的嵌入式程序文件時,只是根據(jù)程序文件中的.bss信息對內(nèi)存中的.bss塊進行清零操作。
????
? ? boot loader與我們的單體程序共存的一個內(nèi)存和FLASH映射快照。其中我們假設(shè)內(nèi)存的大小是8M字節(jié)。可以看出在FLASH上即存放了boot loader程序,又存放了我們的單體程序,至于FLASH上是否有文件系統(tǒng)我們在此并不用關(guān)心。在內(nèi)存中你可以看出也存在一塊boot loader區(qū),這一塊區(qū)是由FLASH中的boot loader將自己加載到內(nèi)存中的,以便加快運行速度,可以想像這塊內(nèi)存區(qū)是最早被拷貝到內(nèi)存中的。當內(nèi)存中的boot loader運行時,其會讀取FLASH中的單體程序,這一單體程序通常是ELF格式的。其中的ELF頭指示了各段的VMA地址和大小以及各段內(nèi)容在單體程序中的偏移地址。boot loader通過ELF頭信息,將.texe段和.data段從FLASH拷貝到內(nèi)存中。顯然,boot loader和單體程序在內(nèi)存中所占用的地址空間是不能重疊的,否則當boot loader將單體程序從FLASH拷貝到內(nèi)存時,會將其自身的內(nèi)容給覆蓋掉,從而造成自己無法正常運行。地址的規(guī)劃是我們設(shè)計boot loader時需要考慮到的。圖中我們只示例了一個中斷向量表,其實,很有可能boot loader內(nèi)存區(qū)也有一個中斷向量表,是供boot loader運行時用的。還有就是有一塊臨時的棧空間,這一空間可以是boot loader和單體程序共同使用的,對于共同使用,需要強調(diào)的是boot loader與單體程序并不會同時運行,當boot loader運行完了以后,會調(diào)轉(zhuǎn)到單體程序的入口處開始運行,入口地址顯然應(yīng)當位于內(nèi)存的.text段。而單體程序在一開始運行時(此時操作系統(tǒng)還沒有起作用),仍是需要一塊小的內(nèi)存作為棧來使用,以便能進行函數(shù)調(diào)用。根據(jù)不同的設(shè)計,我們可以在單體程序運行的初始階段使用與boot loader相同的棧,當然也可以使用不同的棧。這里我們假設(shè)使用相同的棧。從圖1中,我們可以看出,內(nèi)存中還存在很大的一塊閑置區(qū),這一塊區(qū)暫時還沒有使用用途。
?
??? 一旦boot loader運行了我們的單體程序,我們說boot loader就不存在了,那此時boot loader所占用的內(nèi)存空間也就釋放出來了,如圖 2所示。從圖中可以看出內(nèi)存中的閑置空間加大了。那閑置空間被我們的單體程序用來做什么呢?做堆!在單體程序中的操作系統(tǒng)部分,會提供一定的管理模塊來管理這塊堆,并提供API(Application Programming Interface,應(yīng)用程序編程接口)讓我們調(diào)用,從而實現(xiàn)從堆中分配或是釋放內(nèi)存,這些API類似于C語言中的malloc ()/free ()。堆在管理上有一個特點,從堆中分配出來的內(nèi)存應(yīng)當是以某一大小字節(jié)為邊界的。比如,如果CPU中的double類型是占用內(nèi)存最多的數(shù)據(jù)類型且是8字節(jié),那么堆分配出來的內(nèi)存就必須保證是以8字節(jié)為邊界的。這一點請讀者想一想為什么?除了采用動態(tài)的內(nèi)存分配,在嵌入式系統(tǒng)中通常還會采用固定大小內(nèi)存塊的分配方法,這種分配方法的好處是非常的快,而且這種內(nèi)存在使用的過程中不會產(chǎn)生內(nèi)存碎片。
  
??? 堆我們說過了,那接下來我們看一看如果我們的單體程序繼續(xù)運行,會出現(xiàn)什么樣的內(nèi)存布局。我們知道,通常我們的單體程序在初始化時往往需要創(chuàng)建多個任務(wù)來實現(xiàn)其應(yīng)用功能。對于每一個任務(wù),它一塊內(nèi)存是私有的,那就是棧!當任務(wù)運行時,其需要用棧來做為函數(shù)調(diào)用時的參數(shù)傳遞空間,以及用棧來存儲函數(shù)內(nèi)的局部變量。假設(shè)我們的單體程序需要創(chuàng)建兩個任務(wù)A和B,這需要通過調(diào)用操作系統(tǒng)中的任務(wù)創(chuàng)建函數(shù)來達到這一目的。操作系統(tǒng)所提供的任務(wù)創(chuàng)建API往往需要我們指定任務(wù)棧的大小,有的甚至可以指定棧內(nèi)存空間。一旦任務(wù)創(chuàng)建的API被調(diào)用,那么操作系統(tǒng)會調(diào)用堆分配API為任務(wù)分配棧,此時的內(nèi)存布局如圖 3所示。任務(wù)創(chuàng)建完了以后,各任務(wù)就可以根據(jù)應(yīng)用程序邏輯的需要審請堆空間以實現(xiàn)其業(yè)務(wù)邏輯。
 
??? 對于堆我們已經(jīng)知道了必須調(diào)用相應(yīng)的API來分配內(nèi)存,那從棧空間分配內(nèi)存也需要調(diào)用API嗎?答案是通常不需要,為什么是通常?因為,在有的平臺上(Linux上就是)提供棧空間的分配API,即這種API被調(diào)用時,是從調(diào)用任務(wù)的棧空間中分配內(nèi)存的。對于這一功能,在嵌入式系統(tǒng)中使用得非常的少,我也不建議大家使用。對于下面的代碼,mem_main、mem_foo和mem_bar的大小是4K字節(jié)(假設(shè)int類型的大小是4字節(jié)),這些內(nèi)存就是自動(注意是自動)分配在運行任務(wù)的棧上的。我們假設(shè)某個任務(wù)當前所使用的棧是零字節(jié),當這一任務(wù)運行到main中且沒有進入foo ()時,其所占用的空間大小是大約4K字節(jié),之所以用大約這個詞,是因為函數(shù)的調(diào)用還有其它的棧開銷。一旦任務(wù)運行進入foo ()函數(shù)但沒有進入bar ()函數(shù),那么所占用的棧的大小就變?yōu)榇蠹s8K字節(jié)。同樣的,如果程序運行進入bar ()函數(shù),那么所占用的棧空間大約就是12K字節(jié)了。
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:?}
??? 如果程序繼續(xù)運行,從bar ()函數(shù)返回到foo ()函數(shù)中,那么其所占用的棧空間就從大約12K字節(jié)變成了大約8K字節(jié)了。相類似的是,如果程序從foo ()函數(shù)中返回到main ()函數(shù),那么所占用的棧空間又變?yōu)榇蠹s4K字節(jié)了。對于嵌入式系統(tǒng)開發(fā),由于任務(wù)棧通常都比較的小,那這告訴我們什么呢?我想有以下幾點需要注意。
??? 1)函數(shù)的調(diào)用深度越是深,由于每一級的函數(shù)通常都會有局部變量,那么所使用的棧空間也會累積得越大。
??? 2)遞歸調(diào)用需要的棧空間會相對的大(視具體的情況),在嵌入式系統(tǒng)中也建議少用。
??? 3)我們應(yīng)當盡可能的不要在函數(shù)中定義占用內(nèi)存空間較大的局部變量。
??? 下面,我們總結(jié)一下堆與棧的區(qū)別,它們是:
??? 1)堆是大家共享的。任務(wù)可以通過調(diào)用API來從堆中分配內(nèi)存空間。
??? 2)棧是任務(wù)所獨有的。在嵌入式系統(tǒng)中,當一個任務(wù)創(chuàng)建起來后其棧空間的大小往往是定了的。函數(shù)中的局部變量是由編程語言自動從棧上分配的,我們不需要調(diào)用API進行空間分配。
????
??? 最后我有一個問題留給讀者您,這個問題是:
??? 前面的講解中,我們說任務(wù)的棧是由操作系統(tǒng)的任務(wù)創(chuàng)建API從堆中分配出來的,那棧是否也可以位于.data段或是.bss段中呢?為什么?
答案
??? 由于堆從本質(zhì)上說來就是一塊內(nèi)存,由于在C語言中一塊內(nèi)存可以從堆中分配,也可以從.data段或是.bss段中分配。因此,任務(wù)的棧也是可以從這三塊內(nèi)存中分配獲得,也就是說最終的答案是:可以。
????
   
                            
                        
                        
                        ?? ?
??? 從本質(zhì)上說,堆和棧都是內(nèi)存,那么我們只能從概念上對其進行區(qū)分了。為了方便說明,現(xiàn)在假設(shè)嵌入式軟件是一個單體程序(這一術(shù)語并不是嵌入式系統(tǒng)開發(fā)中的專用術(shù)語,是我為了方便說明而使用的),也就是操作系統(tǒng)和我們的應(yīng)用程序是被編譯在同一個可執(zhí)行程序當中的,比如,來自WindRiver的VxWorks就是采用這種方式的。我們知道一個可執(zhí)行程序存在最為重要的三個段。.text段用于存放程序的代碼,即放的是處理器的運行指令。.data用于存放初始化好的數(shù)據(jù),當boot loader(請參見《什么是boot loader》)加載程序文件時,會將程序文件中的.data段拷貝到內(nèi)存的VMA(Virtual Memory Address,在《熟悉binutils工具集》中有所提及,在嵌入式系統(tǒng)中絕大部分不用虛擬內(nèi)存,因此,VMA就是實地址)處,從而完成變量的初始化操作。雖然,我們在C/C++程序中是對全局變量一個一個初始化的,但實際上boot loader是對所有的全局變量通過將程序文件中的.data段拷貝到內(nèi)存中一次性的完成初始化的。.bss段用于存放沒有初始化好的變量,程序文件中并不存放.bss段的具體內(nèi)容,只是存有.bss段的起始地址和大小,當boot loader加載我們的嵌入式程序文件時,只是根據(jù)程序文件中的.bss信息對內(nèi)存中的.bss塊進行清零操作。
????
? ? boot loader與我們的單體程序共存的一個內(nèi)存和FLASH映射快照。其中我們假設(shè)內(nèi)存的大小是8M字節(jié)。可以看出在FLASH上即存放了boot loader程序,又存放了我們的單體程序,至于FLASH上是否有文件系統(tǒng)我們在此并不用關(guān)心。在內(nèi)存中你可以看出也存在一塊boot loader區(qū),這一塊區(qū)是由FLASH中的boot loader將自己加載到內(nèi)存中的,以便加快運行速度,可以想像這塊內(nèi)存區(qū)是最早被拷貝到內(nèi)存中的。當內(nèi)存中的boot loader運行時,其會讀取FLASH中的單體程序,這一單體程序通常是ELF格式的。其中的ELF頭指示了各段的VMA地址和大小以及各段內(nèi)容在單體程序中的偏移地址。boot loader通過ELF頭信息,將.texe段和.data段從FLASH拷貝到內(nèi)存中。顯然,boot loader和單體程序在內(nèi)存中所占用的地址空間是不能重疊的,否則當boot loader將單體程序從FLASH拷貝到內(nèi)存時,會將其自身的內(nèi)容給覆蓋掉,從而造成自己無法正常運行。地址的規(guī)劃是我們設(shè)計boot loader時需要考慮到的。圖中我們只示例了一個中斷向量表,其實,很有可能boot loader內(nèi)存區(qū)也有一個中斷向量表,是供boot loader運行時用的。還有就是有一塊臨時的棧空間,這一空間可以是boot loader和單體程序共同使用的,對于共同使用,需要強調(diào)的是boot loader與單體程序并不會同時運行,當boot loader運行完了以后,會調(diào)轉(zhuǎn)到單體程序的入口處開始運行,入口地址顯然應(yīng)當位于內(nèi)存的.text段。而單體程序在一開始運行時(此時操作系統(tǒng)還沒有起作用),仍是需要一塊小的內(nèi)存作為棧來使用,以便能進行函數(shù)調(diào)用。根據(jù)不同的設(shè)計,我們可以在單體程序運行的初始階段使用與boot loader相同的棧,當然也可以使用不同的棧。這里我們假設(shè)使用相同的棧。從圖1中,我們可以看出,內(nèi)存中還存在很大的一塊閑置區(qū),這一塊區(qū)暫時還沒有使用用途。
?
??? 一旦boot loader運行了我們的單體程序,我們說boot loader就不存在了,那此時boot loader所占用的內(nèi)存空間也就釋放出來了,如圖 2所示。從圖中可以看出內(nèi)存中的閑置空間加大了。那閑置空間被我們的單體程序用來做什么呢?做堆!在單體程序中的操作系統(tǒng)部分,會提供一定的管理模塊來管理這塊堆,并提供API(Application Programming Interface,應(yīng)用程序編程接口)讓我們調(diào)用,從而實現(xiàn)從堆中分配或是釋放內(nèi)存,這些API類似于C語言中的malloc ()/free ()。堆在管理上有一個特點,從堆中分配出來的內(nèi)存應(yīng)當是以某一大小字節(jié)為邊界的。比如,如果CPU中的double類型是占用內(nèi)存最多的數(shù)據(jù)類型且是8字節(jié),那么堆分配出來的內(nèi)存就必須保證是以8字節(jié)為邊界的。這一點請讀者想一想為什么?除了采用動態(tài)的內(nèi)存分配,在嵌入式系統(tǒng)中通常還會采用固定大小內(nèi)存塊的分配方法,這種分配方法的好處是非常的快,而且這種內(nèi)存在使用的過程中不會產(chǎn)生內(nèi)存碎片。
??? 堆我們說過了,那接下來我們看一看如果我們的單體程序繼續(xù)運行,會出現(xiàn)什么樣的內(nèi)存布局。我們知道,通常我們的單體程序在初始化時往往需要創(chuàng)建多個任務(wù)來實現(xiàn)其應(yīng)用功能。對于每一個任務(wù),它一塊內(nèi)存是私有的,那就是棧!當任務(wù)運行時,其需要用棧來做為函數(shù)調(diào)用時的參數(shù)傳遞空間,以及用棧來存儲函數(shù)內(nèi)的局部變量。假設(shè)我們的單體程序需要創(chuàng)建兩個任務(wù)A和B,這需要通過調(diào)用操作系統(tǒng)中的任務(wù)創(chuàng)建函數(shù)來達到這一目的。操作系統(tǒng)所提供的任務(wù)創(chuàng)建API往往需要我們指定任務(wù)棧的大小,有的甚至可以指定棧內(nèi)存空間。一旦任務(wù)創(chuàng)建的API被調(diào)用,那么操作系統(tǒng)會調(diào)用堆分配API為任務(wù)分配棧,此時的內(nèi)存布局如圖 3所示。任務(wù)創(chuàng)建完了以后,各任務(wù)就可以根據(jù)應(yīng)用程序邏輯的需要審請堆空間以實現(xiàn)其業(yè)務(wù)邏輯。
??? 對于堆我們已經(jīng)知道了必須調(diào)用相應(yīng)的API來分配內(nèi)存,那從棧空間分配內(nèi)存也需要調(diào)用API嗎?答案是通常不需要,為什么是通常?因為,在有的平臺上(Linux上就是)提供棧空間的分配API,即這種API被調(diào)用時,是從調(diào)用任務(wù)的棧空間中分配內(nèi)存的。對于這一功能,在嵌入式系統(tǒng)中使用得非常的少,我也不建議大家使用。對于下面的代碼,mem_main、mem_foo和mem_bar的大小是4K字節(jié)(假設(shè)int類型的大小是4字節(jié)),這些內(nèi)存就是自動(注意是自動)分配在運行任務(wù)的棧上的。我們假設(shè)某個任務(wù)當前所使用的棧是零字節(jié),當這一任務(wù)運行到main中且沒有進入foo ()時,其所占用的空間大小是大約4K字節(jié),之所以用大約這個詞,是因為函數(shù)的調(diào)用還有其它的棧開銷。一旦任務(wù)運行進入foo ()函數(shù)但沒有進入bar ()函數(shù),那么所占用的棧的大小就變?yōu)榇蠹s8K字節(jié)。同樣的,如果程序運行進入bar ()函數(shù),那么所占用的棧空間大約就是12K字節(jié)了。
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:?}
??? 如果程序繼續(xù)運行,從bar ()函數(shù)返回到foo ()函數(shù)中,那么其所占用的棧空間就從大約12K字節(jié)變成了大約8K字節(jié)了。相類似的是,如果程序從foo ()函數(shù)中返回到main ()函數(shù),那么所占用的棧空間又變?yōu)榇蠹s4K字節(jié)了。對于嵌入式系統(tǒng)開發(fā),由于任務(wù)棧通常都比較的小,那這告訴我們什么呢?我想有以下幾點需要注意。
??? 1)函數(shù)的調(diào)用深度越是深,由于每一級的函數(shù)通常都會有局部變量,那么所使用的棧空間也會累積得越大。
??? 2)遞歸調(diào)用需要的棧空間會相對的大(視具體的情況),在嵌入式系統(tǒng)中也建議少用。
??? 3)我們應(yīng)當盡可能的不要在函數(shù)中定義占用內(nèi)存空間較大的局部變量。
??? 下面,我們總結(jié)一下堆與棧的區(qū)別,它們是:
??? 1)堆是大家共享的。任務(wù)可以通過調(diào)用API來從堆中分配內(nèi)存空間。
??? 2)棧是任務(wù)所獨有的。在嵌入式系統(tǒng)中,當一個任務(wù)創(chuàng)建起來后其棧空間的大小往往是定了的。函數(shù)中的局部變量是由編程語言自動從棧上分配的,我們不需要調(diào)用API進行空間分配。
????
??? 最后我有一個問題留給讀者您,這個問題是:
??? 前面的講解中,我們說任務(wù)的棧是由操作系統(tǒng)的任務(wù)創(chuàng)建API從堆中分配出來的,那棧是否也可以位于.data段或是.bss段中呢?為什么?
答案
??? 由于堆從本質(zhì)上說來就是一塊內(nèi)存,由于在C語言中一塊內(nèi)存可以從堆中分配,也可以從.data段或是.bss段中分配。因此,任務(wù)的棧也是可以從這三塊內(nèi)存中分配獲得,也就是說最終的答案是:可以。
????
總結(jié)
                            
                        - 上一篇: #undef及其用法
 - 下一篇: Java Web 前端高性能优化(一)