1. 程式人生 > >malloc和free實現的原理

malloc和free實現的原理

還是要認真看深入理解計算機系統

記憶體分配是按照堆塊實現的,一個堆塊是由頭部和有效載荷量組成,其中的有效載荷量就是我們申請的堆的大小。

頭部塊包括 塊大小和是否可用 這兩個部分組成。

在記憶體中這些堆塊以連結串列形勢組成

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;
}

如您所見,在這個分配程式中,記憶體的釋放使用了一個非常簡單的機制,在固定時間內完成記憶體釋放。

(總結就一句話,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
}

這個例子的執行環境是這樣的:

[[email protected] ~]$ 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()
STL崇尚拷貝,你往容器裡放東西或者從容器裡取東西,都是要呼叫拷貝建構函式的。比如,你有一個物件a要插入到map裡,過程是這樣的:
  • 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.
上面的8個allocator的實現中,bitmap_allocator、pool_allocator和__mt_alloc是基於cache的,其它的不基於cache
* 那麼?如何指定使用一個特殊的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的allocator
map<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。
為了測試方便,我在執行程式前,設定了程序的最大記憶體為200MB,使用的命令如下:

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"
6.2 申請記憶體的方式共有多少種?
  • 參考文獻:glibc manual中的第3章(見http://www.gnu.org/software/libc/manual/)
  • exec
  • fork
  • 程序內:
  • global var or static var
  • local var
  • malloc()
  • memory map file


相關推薦

mallocfree實現原理

還是要認真看深入理解計算機系統 記憶體分配是按照堆塊實現的,一個堆塊是由頭部和有效載荷量組成,其中的有效載荷量就是我們申請的堆的大小。 頭部塊包括 塊大小和是否可用 這兩個部分組成。 在記憶體中這些堆塊以連結串列形勢組成 malloc函式的實質體現在,它有一個將可用的

linux下malloc()free()的原理實現

在學習C語言的時候知道了動態記憶體分配的概念,也知道了malloc()的使用方式,但是一直沒有去了解或者認真學習malloc()的實現原理。今天看到關於動態記憶體分配方面的資料,就整理總結下。 在C語言中只能通過malloc()和其派生的函式進行動態的申請記憶

呼叫malloc()函式之後,核心發生了什麼?附malloc()free()實現的原始碼

         特此宣告:本文參照了另外一篇文章和一個帖子,再結合自己的理解總結了malloc()函式的實現機制。     我們經常會在C程式中呼叫malloc()函式動態分配一塊連續的記憶體空間並使用它們。那麼,這些使用者空間發生的事會引發核心空間什麼樣的反應呢? ma

Unix系統編程()mallocfree實現

原因 編程錯誤 alloc 系統編程 OS 內存分配 continued 我們 如何 盡管malloc和free所提供的內存分配接口比之brk和sbrk要容易許多,但在使用時仍然容易犯下各種編程錯誤。 理解malloc和free的實現,將使我們洞悉產生這些錯誤的原因

【Linux】mallocfree底層的簡單實現!!!

從作業系統角度來看,程序分配記憶體有兩種方式,分別由兩個系統呼叫完成:brk和mmap(當然在這裡是不考慮共享記憶體) brk是將資料段(.data)的最高地址指標_edata往高地址推; mmap是在程序的虛擬地址空間中(堆和棧中間,稱為檔案對映區域的地方

malloc free例程

就會 ret sca stdlib.h int 註意 申請 printf malloc #include <stdio.h>#include <stdlib.h>int main(){int a;scanf("%d",&a);int *p=(

分配內存malloc()free()

c1、首先回顧一下內存分配的有關事實。所有的程序都必須留出足夠內存來存儲他們使用的數據。一些內存分配是自動完成的。如:float x;char place[]="dancing oxen creek".於是系統將留出存儲float或者字符串足夠的內存空間,也可明確要求確切的內存,int a[100];這一聲明

Java並發機制底層實現原理

差距 32處理器 們的 trac 結點 exce jdk cep 定性   Java代碼在編譯後會變成Java字節碼,字節碼被類加載器加載到JVM裏,JVM執行字節碼轉化為匯編指令在CPU上執行。Java中的並發機制依賴於JVM的實現和CPU的指令。      Java語言

JMM底層實現原理

現代計算機物理上的記憶體模型 物理機遇到的併發問題與虛擬機器中的情況有不少相似之處,物理機對併發的處理方案對於虛擬機器的實現也有相當大的參考意義。 其中一個重要的複雜性來源是絕大多數的運算任務都不可能只靠處理器“計算”就能完成,處理器至少要與記憶體互動,如讀取運算資料、儲存運算結果

mallocfree函式詳解(轉載只是為了查閱方便,若侵權立刪)

malloc和free函式詳解   本文介紹malloc和free函式的內容。   在C中,對記憶體的管理是相當重要。下面開始介紹這兩個函式:     一、malloc()和free()的基本概念以及基本用法: 1、函式原型及說明: void *malloc(lon

基於接口回調詳解JUC中CallableFutureTask實現原理

cnblogs blog 異步編程 但是 迷糊 對象 extend href 增加 Callable接口和FutureTask實現類,是JUC(Java Util Concurrent)包中很重要的兩個技術實現,它們使獲取多線程運行結果成為可能。它們底層的實現,就是基於接口

mallocfree使用要小心

先說一下用法:        char *stemp = (char*)malloc(256 * sizeof(char));         if(stemp == NULL) return

C語言中 malloc free

from:http://blog.sina.com.cn/s/blog_af1a77fa0102xceb.html 一、malloc()和free()的基本概念以及基本用法: 1、函式原型及說明: void *malloc(long NumBytes):該函式分配了NumBytes個位元

記憶體管理(malloc free 用法)

一、malloc() 和 free() 的基本概念和基本用法 1. 函式原型及說明 void *malloc( long NumBytes) 該函式分配了NumBytes個位元組,並返回了指向這塊記憶體的指標。如果分配失敗,則返回一個空指標NULL。失敗的原因有很多

淺談C中的mallocfree

一、malloc()和free()的基本概念以及基本用法: 1、函式原型及說明: void *malloc(long NumBytes):該函式在堆上分配了NumBytes個位元組的空間,並返回了指向這塊記憶體的指標。如果分配失敗,則返回一個空指標(NULL)。 關於分

基於介面回撥詳解JUC中CallableFutureTask實現原理

Callable介面和FutureTask實現類,是JUC(Java Util Concurrent)包中很重要的兩個技術實現,它們使獲取多執行緒執行結果成為可能。它們底層的實現,就是基於介面回撥技術。介面回撥,許多程式設計師都耳熟能詳,這種技術被廣泛應用於非同步模組的開發中。它的實現原理並不複雜,但是對初學

Java併發程式設計的藝術——volatilesynchronized實現原理

volatile volatile變數修飾的共享變數進行寫操作時候,會多出lock字首指令。 lock字首指令在多核處理器下會引發一下兩件事情: 將當前處理器快取行的資料寫回到系統記憶體。 這個寫回記

redis主從複製叢集實現原理

redis主從複製 redis主從配置比較簡單,基本就是在從節點配置檔案加上:slaveof 192.168.33.130 6379 主要是通過master server持久化的rdb檔案實現的。master server 先dump出記憶體快照檔案,然後將rdb檔案傳給

事件觸發機制:Poll,SelectEpoll實現原理分析

Poll和Select和Epoll都是事件觸發機制,當等待的事件發生就觸發進行處理,多用於linux實現的伺服器對客戶端連線的處理。 Poll和Select都是這樣的機制:可以阻塞地同時探測一組支援非阻塞的IO裝置,是否有事件發生(如可讀,可寫,有高優先順序的錯誤輸出,出現

記憶體分配(malloc()free())

C語言的一個特性是接近底層,對於硬體的控制能力比其他高階動態語言要強。同時,C語言賦予程式設計師更大的自由度,更信任程式設計師。在記憶體的分配與釋放上,我們知道非靜態變數(塊作用域,無連結,自動生存期)在程式進入到變數定義所在的地方(塊或函式內)時分配記憶體,在離開塊作用域時釋放。對於靜態變數,在程式載入到記