malloc函數的底層實現你是否清楚
說起malloc函數,每個人都能說出它的功能,而且我們經常會用到,那么今天我要說的是關於malloc函數在編譯器的底層實現,如果你對它的實現已經很清楚了,那么你可以不往下看了,因為這篇博客只是就它的一些簡單原理進行了整理,你可以等我的下一篇博客,對它的深層的一些函數在進行的一些講述。
這篇博客對於深層的函數實現並沒有解釋,只是讓我們明白了windows系統中的一些分配算法的原理。請讀者多多指正,因為在空閑鏈表分配算法上我看到過不同的說法。
關於VirtualAlloc函數:
首先我們來看一下VirtualAlloc函數,它是WINDOWS系統提供給我們的一個API,它的功能就是向操作系統給我們批發內存。為什么叫批發呢,因為在操作系統中並不是我們所編寫的應用程序直接向系統申請內存,在VirtualAlloc函數向內存申請內存的時候是申請內存的大小必須是4096字節的整數倍,這就相當於是一個水果販子去大批發市場進貨,人家批發市場是有起發的要求的,不是說你去買上幾斤都行,所以我們都是從水果販子的手上再去買水果。這樣是有道理的,因為你一個程序假如只需要申請幾字節的內存,那么系統給我們4096字節的大小是不是很浪費。
那么用VirtualAlloc申請的內存又是怎么拿給我們的程序來用的呢,在這里就牽扯到了分配算法。其實在windows中在對管理器上提供了幾個函數用來創建、分配、釋放和銷毀堆空間(我們申請的空間是在堆上的),從而實現了分配算法。
HeapCreat:創建一個堆
HeapAlloc:在堆里分配空間
HeapFree:釋放分配的內存
HeapDestroy:銷毀一個堆
這里的HeapCreat便是創建一個堆空間它申請內存空間便是通過VirtualAlloc函數來實現的。HeapAlloc函數是在堆空間里面給用戶分配小的內存空間,如果內存不足它也能通過VirtualAlloc向系統申請更多的內存。
Malloc函數:
說了前面那么多,終於說道我們的重點了,malloc函數其實是上面幾個Heap操作函數的包裝。
堆空間是程序申請出來的一大塊空間,當我們用malloc函數申請空間的時候大小是不確定的,有可能是很小的一塊空間也有可能是很大的一塊空間,所以對堆空間也是需要一定的方法進行管理的,當你需要申請空間的時候按照你申請的大小以一定方法分配給你。
這就是分配算法,分配算法有很多種,對於不同的場合是有不同的分配算法的。在這里只簡單的描述一下幾種簡單的算法。
1. 空閑鏈表
我們知道在一塊堆空間中,能用的空間到時候是一塊一塊的分布着,空閑鏈表的方法就是把這些空閑的塊用鏈表的方式進行連接,如果用戶申請一塊空間,就可以遍歷這個空閑鏈表找到能夠容納你申請的大小的一個空閑塊,然后拆分,把合適大小的一塊返回給你;當用戶釋放一塊空間時,在把這塊空間鏈接在空閑鏈表上。下面來看一下它的結構:
這里畫的是其中的一塊,在這塊空間的前面存放了兩個指針,N代表next指針用來指向它的下一塊空間,如果下一塊為空,則指針指向NULL,P代表Prev指針,用來指向它的前一塊空間,如果為NULL代表他就是空閑鏈表的第一塊空間。
這里給出了一個只有三個塊的空閑鏈表,它們的指針指向就是如圖所指那樣,此時我的塊都是空閑的。假如我現在要申請一塊空間,它是怎么實現的呢?
假設我要申請的空間大小剛好是第二塊空間的大小,那么會把第二塊空間的地址返回給我們,然后把這塊空間從原來的空閑鏈表上刪掉。此時鏈表如下圖所示:
其實一般找到的空間都是比我們所申請的空間大的,然后把這塊空間進行了拆分,一部分就是就是我們所申請的空間,另一部分為剩下的空間,它還是對應在原來的空心鏈表上。
這樣就實現了一種簡單的分配算法,其實在釋放這塊空間的時候,雖然知道指向這塊空間的指針,但是堆並不知道這塊空間的大小,那么它就不知道該釋放多大的一塊空間。所以其實在我們剛才說的分配算法里,在給用戶分配空間的時候會多分配4個字節,作用就是用來存放這塊空間的大小,這樣的話在釋放的時候找到這一塊空間,就知道這塊空間到底有多大,然后進行釋放。
但是,我們知道有內存越界這種情況,就是在我們用內存的時候不小心用了它后面不屬於它的空間,那么像空閑鏈表這種結構,可能就會把那塊地方存放的兩個指針進行了修改,這樣不就破壞了我們的鏈表,然后整個空間就不能再使用了,這就是這種結構的缺點。
2. 位圖
還有一種分配方式是位圖,它是把我們的一塊堆空間進行划分,划分成大小形同的一些塊,當用戶申請空間時,它會給我們分配整數個塊以至於能存放你所申請的大小,第一個塊是頭,接下來的塊都是用戶申請的空間的主體,這樣的話在位圖中,我們的每一個快都有可能且只有可能有三種情況,就是頭(Head)/主體(Body)/空閑(Free)。
下圖就是一個例子,在下圖中分配了兩片內存,第一片占用了四個塊,第二片占用的五個塊。
這樣做的話到底有什么好處呢,首先這樣分配的話,對於這個堆的存儲信息,它是記錄在一個數組里。因為每一個小塊是有三種可能狀態,那么用二進制的兩個位就能夠表示了,假如設為00位空閑,10位主體,11為頭。這樣整個位圖在一個數組中就能表示了。
這樣每一個堆空間都可以用N個字節來表示,比如堆的大小為1Mb,分成1M/128=8000個塊(每個塊設為128字節),我們知道一個int是4個字節32位,那么能用8000/(32/2)=512個int來存儲,所以一個大小為512int的數組就是一個完整的位圖。
位圖有它的優點:
比空閑鏈表更加穩定,因為可以對數組進行備份,而且就算某個塊損壞,也不會影響整個位圖其他的塊空間。
速度比較快還容易管理。
同時也有它的缺點:
也容易造成塊的浪費,因為畢竟它是整數倍的分配。
當堆比較大的時候,可能這個位圖會很大,數組很龐大,可能效率也並不像想象那么快。