malloc和free实现的原理
還是要認真看深入理解計算機系統
http://blog.csdn.net/llhyy17/article/details/5375298
內存分配是按照堆塊實現的,一個堆塊是由頭部和有效載荷量組成,其中的有效載荷量就是我們申請的堆的大小。
頭部塊包括 塊大小和是否可用 這兩個部分組成。
在內存中這些堆塊以鏈表形勢組成
malloc函數的實質體現在,它有一個將可用的內存塊連接為一個長長的列表的所謂空閑鏈表。調用malloc函數時,它沿連接表尋找一個大到足以滿足用戶請求所需要的內存塊。然后,將該內存塊一分為二(一塊的大小與用戶請求的大小相等,另一塊的大小就是剩下的字節)。接下來,將分配給用戶的那塊內存傳給用戶,并將剩下的那塊(如果有的話)返回到連接表上。調用free函數時,它將用戶釋放的內存塊連接到空閑鏈上。到最后,空閑鏈會被切成很多的小內存片段,如果這時用戶申請一個大的內存片段,那么空閑鏈上可能沒有可以滿足用戶要求的片段了。于是,malloc函數請求延時,并開始在空閑鏈上翻箱倒柜地檢查各內存片段,對它們進行整理,將相鄰的小空閑塊合并成較大的內存塊。?
glibc維護了不止一個不定長的內存塊鏈表,而是好幾個,每一個這種鏈表負責一個大小范圍,這種做法有效減少了分配大內存時的遍歷開銷,類似于哈希的方式,將很大的范圍的數據散列到有限的幾個小的范圍內而不是所有數據都放在一起,雖然最終還是要在小的范圍內查找,但是最起碼省去了很多的開銷,如果只有一個不定長鏈表那么就要全部遍歷,如果分成3個,就省去了2/3的開銷,總之這個策略十分類似于散列。glibc另外的策略就是不止維護一類空閑鏈表,而是另外再維護一個緩沖鏈表和一個高速緩沖鏈表,在分配的時候首先在高速緩存中查找,失敗之后再在空閑鏈表查找,如果找到的內存塊比較大,那么將切割之后的剩余內存塊插入到緩存鏈表,如果空閑鏈表查找失敗那么就往緩存鏈表中查找. 如果還是沒有合適的空閑塊,就向內存申請比請求數更大的內存塊,然后把剩下的內存放入鏈表中。
在對內存塊進行了?free?調用之后,我們需要做的是諸如將它們標記為未被使用的等事情,并且,在調用?malloc?時,我們要能夠定位未被使用的內存塊。因此,?malloc返回的每塊內存的起始處首先要有這個結構:
這就解釋了,為什么在程序中free之后,但是堆的內存還是沒有釋放。
//清單?3.?內存控制塊結構定義
struct?mem_control_block?{
????int?is_available;
????int?size;
};
現在,您可能會認為當程序調用?malloc?時這會引發問題?——?它們如何知道這個結構?答案是它們不必知道;在返回指針之前,我們會將其移動到這個結構之后,把它隱藏起來。這使得返回的指針指向沒有用于任何其他用途的內存。那樣,從調用程序的角度來看,它們所得到的全部是空閑的、開放的內存。然后,當通過?free()?將該指針傳遞回來時,我們只需要倒退幾個內存字節就可以再次找到這個結構。
在討論分配內存之前,我們將先討論釋放,因為它更簡單。為了釋放內存,我們必須要做的惟一一件事情就是,獲得我們給出的指針,回退?sizeof(struct mem_control_block)?個字節,并將其標記為可用的。這里是對應的代碼:
清單?4.?解除分配函數
void?free(void?*firstbyte)?{
????struct?mem_control_block?*mcb;
/*?Backup?from?the?given?pointer?to?find?the
?*?mem_control_block
?*/
???mcb?=?firstbyte?-?sizeof(struct?mem_control_block);
/*?Mark?the?block?as?being?available?*/
??mcb->is_available?=?1;
/*?That''s?It!??We''re?done.?*/
??return;
}
如您所見,在這個分配程序中,內存的釋放使用了一個非常簡單的機制,在固定時間內完成內存釋放。
http://blog.163.com/dengminwen@126/blog/static/870226720097189486788/
(總結就一句話,stl沒有侯捷書上寫的有一個鏈表管理內存塊,而是簡單的調用malloc和free,我想這是因為malloc內部已經實現了內存池)
1. 背景
前些天在一個技術分享會上,某大牛說,STL使用了內存池,釋放內存的時候,并不釋放給OS,而是自己由留著用。
聽到這些觀點后,我就有些著急了,因為我以前一直是直接使用STL的一些工具類的,比如std::string、std::map、std::vector、std::list等等,從來都沒有關注過內存的問題。
帶著內存的問題,我花了兩三天的時間去閱讀STL的代碼,并且寫一些簡單的程序進行測試;下面列舉一些心得體會,但是卻沒有什么大的結論 -.-?
2. 容易誤解的簡單例子
我們以STL中的map為例,下面有一個使用map的簡單例子,大部分人可以在30秒內寫好。
void testmap()
{
? map<int, float> testmap;
? for (int i = 0; i < 1000000; i++) {
??? testmap[i] = (float)i;
? }
? testmap.clear();
}
為了在調用map::clear()之后查看進程的內存使用量,我們可以加幾行代碼讓程序暫停一下。
void testmap()
{
? map<int, float> testmap;
? for (int i = 0; i < 1000000; i++) {
??? testmap[i] = (float)i;
? }
? testmap.clear();
? // 觀察點
? int tmp; cout << "use ps to see my momory now, and enter int to continue:"; cin >> tmp;
}
編譯運行上面的程序,你會看見這樣的情況:ps顯示進程的內存使用量為40MB多。這時,你會毫不猶豫地說,STL的map使用了內存池(memory pool)。
然后,我就跑去閱讀libstdc++的STL的源代碼,STL提供了很多種Allocator的實現,有基于內存池的,但是默認的std::allocator的實現是new_allocator,這個實現只是簡單的對new和delete進行了簡單的封裝,并沒有使用內存池。這樣,懷疑的對象就轉移到glibc的malloc函數了。malloc提供的兩個函數來查看當前申請的內存的狀態,分別是malloc_stats()和mallinfo(),它們都定義在<malloc.h>里。
為了弄清楚這個問題,我們對上面的例子進行如下的改造:
#include <malloc.h>
void testmap()
{
? malloc_stats();??? ??? // <======== 觀察點1
? map<int, float> testmap;
? for (int i = 0; i < 1000000; i++) {
??? testmap[i] = (float)i;
? }
? malloc_stats();??? ??? // <======== 觀察點2
? testmap.clear();
? malloc_stats();??? ??? // <======== 觀察點3
}
這個例子的運行環境是這樣的:
[dengmw@my ~]$ g++ -v
Reading specs from /usr/lib/gcc/x86_64-redhat-linux/3.4.6/specs
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --enable-shared --enable-threads=posix --disable-checking --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-java-awt=gtk --host=x86_64-redhat-linux
Thread model: posix
gcc version 3.4.6 20060404 (Red Hat 3.4.6-9)
程序的運行結果是這樣的:
在觀察點1:
*?????? system bytes???? =????????? 0
*?????? in use bytes???? =????????? 0
在觀察點2:
*?????? system bytes???? =????????? 48144384
*?????? in use bytes???? =????????? 48005120
在觀察點3:
*?????? system bytes???? =????????? 48140288??? <==== malloc cache the memory here
*?????? in use bytes???? =????????? 5120
很明顯,盡管程序員顯式地調用了map::clear(),但是malloc并沒有把這些內存歸還給OS,而是緩存起來了。所以說,這個例子的罪魁禍首并不是libstdc++的的STL,而是glibc的malloc。
3. 侯捷的《STL源碼剖析》有點過時了
- 在調試上面的例子的時候,我在看了不少的書籍和網上的文章,其中就包括了侯捷的《STL源碼剖析》,但是這本書已經過時了,因為他寫這本書的時候,g++的版本才2.9。我把g++的各個版本的源代碼都下載下來了,并且進行了比較,總結如下:
- 侯捷的《STL源碼剖析》只對于gcc-3.3.*及以前的版本是對的;對于gcc-3.4.*以后的版本,STL中關于內存的代碼變了
- 當前,大家使用的gcc大都是3.4.6版本或者更加新的版本
- gcc-3.3分支從2003-05-13發布第1版,到2005-05-03發布3.3.6
- gcc-3.3的默認的Allocator,定義在"include/bits/stl_alloc.h"里,確實是帶有cache的 (即常說的memory pool)
- gcc-3.4的默認的Allocator,定義在"include/bits/allocator.h"里,它的真實的實現是"include/ext/new_allocator.h",這個實現不帶cache,只是new和delete的簡單封裝
4. STL內存管理的基礎知識(gcc-3.4.*及以后的)?
通過這次對STL的研究,我學到不不少新的知識。可能這些內容你都已經會了,-.-,我比較弱,下面的內容我是第一次知道的:
STL有很多種allocator,默認采用的是std::allocator,我們沿著這樣的頭文件路線,可以找到它的最終實現:
- -> "include/bits/allocator.h"
- -> "include/i386-redhat-linux/bits/c++allocator.h"
- -> "include/ext/new_allocator.h"(即是說,std::allocator == __gnu_cxx::new_allocator)
根據C++的標準,STL的allocator,把對象的申請和釋放分成了4步:
- 第1步:申請內存空間,對應函數是allocator::allocate()
- 第2步:執行構造函數,對應函數是allocator::construct()
- 第3步:執行析構函數,對應函數是allocator::destroy()
- 第4步:釋放內存空間,對應函數是allocator::deallocate()
- map先申請一個結點的空間
- 調用拷貝構造函數初始化該結點
- 把新結點插入到map的紅黑樹中
STL中實現了好多種不同的更為具體的allocator,如下( GNU GCC關于Memory的官方文檔 ):
- __gnu_cxx::new_allocator: 簡單地封裝了new和delete操作符,通常就是std::allocator
- __gnu_cxx::malloc_allocator: 簡單地封裝了malloc和free函數
- __gnu_cxx::array_allocator: 申請一堆內存
- __gnu_cxx::debug_allocator: 用于debug
- __gnu_cxx::throw_allocator: 用于異常
- __gnu_cxx::__pool_alloc: 基于內存池
- __gnu_cxx::__mt_alloc: 對多線程環境進行了優化
- __gnu_cxx::bitmap_allocator: keep track of the used and unused memory locations.
* 那么?如何指定使用一個特殊的allocator呢?示例如下:
map<int, int> a1;??? ??? ??? ??? ??? ??? ??? ??? ??? // 方法1 map<int, int, less<int>, std::allocator<pair<int, int> > > a3;??? ? // 方法2 // 方法3,方法1、方法2、方法3都是等價的map<int, int, less<int>, __gnu_cxx::new_allocator<pair<int, int> > > a2;?
// 方法4,使用了基于cache的allocatormap<int, int, less<int>, __gnu_cxx::__pool_alloc<pair<int, int> > >? a4;?
5. 內存碎片是容易被忽視的導致OutOfMemory的原因
這個觀點有點類似于磁盤碎片,也可以稱為內存碎片吧,當內存碎片過多的時候,極容易出現OutOfMemory錯誤;
使用STL的map特別容易出現這種情況,往map里插入了海量的小對象,然后釋放了一些,然后再想申請內存時,就出現OutOfMemory錯誤了;
這種現象不只是在使用STL的情況會發現,下面舉一個例子來說明內存碎片的問題,盡管這個例子沒有使用STL。
舉例之前,先說明一下這個例子中使用的兩個查看當前進程的內存統計量的2個函數:
- int get_max_malloc_length_inMB() : 得到當前可以申請的最長的內存長度(MB);這個函數不停地調用p=malloc(length*1024*1024);如果成功,則length++,并且free(p);如果失敗,返回(length-1)。
- int get_free_mem_inKB() : 得到當前可以申請的內存總量(KB);這個函數不停地調用malloc(1024)來申請1KB的內存;如果成功,把這1KB的內存存起來,并且count++;如果失敗,則把所有的1KB內存釋放,再返回count。
ulimit -m 204800;
ulimit -v 204800;
這個例子把申請到的內存以矩陣的形式存儲起來,先按列優先把指針存起來,再按行優先進行free,這樣會造成大量的內存碎片;例子的偽代碼如下:
typedef char* PtrType; PtrType ** Ptrs = (PtrType**) malloc( ROW * sizeof(PtrType*) ); ...// 第1步: 占領所有的內存,按列優先進行申請 for(j=0; j<COL; ++j) {for(i=0; i<ROW; ++i) {Ptrs[j][i] = malloc(1024);} }// 第2步:按行優先釋放所有的內存,在中間多次調用get_max_malloc_length_inMB和get_free_mem_inKB來查看內存使用情況 for (i=0; i<ROW; ++i) {for (j=0; j<COL; ++j) {free( Ptrs[i][j] );}free(Ptrs[i]);// 得到兩個關于內存的統計量get_max_malloc_length_inMB();get_free_mem_inKB(); }// 第3步:釋放Ptrs,再獲取一次內存的統計量 free(Ptrs); get_max_malloc_length_inMB(); get_free_mem_inKB();
需要關注的是,內存的申請的順序是按列優先的,而釋放的順序是按行優先的,這種做法就是模擬內存的碎片。<BR>
運行上面的程序后,得到的結果是:在釋放內存的過程中,max_malloc_length_inMB長期保持在0 MB,當全部釋放完后,max_malloc_length_inMB變成了 193 MB<BR>
max_malloc_length_inMB: 196 MB -> 0 MB -> 0 MB -> ... -> 0 MB -> 0 MB -> ... -> 0 MB -> 0 MB -> 195 MB free_mem_inKB: 199374 KB -> 528 KB -> 826 KB -> ... -> 96037 KB -> 96424 KB -> ... -> 197828 KB -> 198215 KB -> 198730 KB
上面的結果引申出這樣的結論:
- OutOfMemory錯誤,并不一定是內存使用得太多;
- 當一個程序申請了大量的小內存塊 (比如往std::map中插入海量的小對象),導致內存碎片過多的話,一樣有可能出現OutOfMemory錯誤
6. 一些別的收獲
6.1 libc.so.6和glibc-2.9有什么不同?
- 參考文獻:http://en.wikipedia.org/wiki/GNU_C_Library
- 在80年代,FSF寫了glibc;
- 后來,linux kernel的人照著glibc,寫了"Linux libc",一直從libc.so.2到libc.so.5
- 到1997年,FSF發布了glibc-2.0,這個版本有很多優點,比如支持有更多的標準,更可移植;linux kernel的人就把"Linux libc"的項目砍掉了,重新使用glibc-2.0,然后就命名為libc.so.6
- 如果你運行一下這個命令"ls -lh /lib/libc.so.6",你會發現它其實是一個符號鏈接,在我的電腦上,它指向了"/lib/libc-2.9.so"
- 參考文獻:glibc manual中的第3章(見http://www.gnu.org/software/libc/manual/)
- exec
- fork
- 進程內:
- global var or static var
- local var
- malloc()
- memory map file
與50位技術專家面對面20年技術見證,附贈技術全景圖
總結
以上是生活随笔為你收集整理的malloc和free实现的原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux进程间通信分类 以及 pipe
- 下一篇: GraphChi