1. 程式人生 > >malloc函式實現原理!

malloc函式實現原理!

任何一個用過或學過C的人對malloc都不會陌生。大家都知道malloc可以分配一段連續的記憶體空間,並且在不再使用時可以通過free釋放掉。但是,許多程式設計師對malloc背後的事情並不熟悉,許多人甚至把malloc當做作業系統所提供的系統呼叫或C的關鍵字。實際上,malloc只是C的標準庫中提供的一個普通函式,而且實現malloc的基本思想並不複雜,任何一個對C和作業系統有些許瞭解的程式設計師都可以很容易理解。

這篇文章通過實現一個簡單的malloc來描述malloc背後的機制。當然與現有C的標準庫實現(例如glibc)相比,我們實現的malloc並不是特別高效,但是這個實現比目前真實的malloc實現要簡單很多,因此易於理解。重要的是,這個實現和真實實現在基本原理上是一致的。

這篇文章將首先介紹一些所需的基本知識,如作業系統對程序的記憶體管理以及相關的系統呼叫,然後逐步實現一個簡單的malloc。為了簡單起見,這篇文章將只考慮x86_64體系結構,作業系統為Linux。

1 什麼是malloc

在實現malloc之前,先要相對正式地對malloc做一個定義。

根據標準C庫函式的定義,malloc具有如下原型:

C
1 void*malloc(size_tsize);

這個函式要實現的功能是在系統中分配一段連續的可用的記憶體,具體有如下要求:

  • malloc分配的記憶體大小至少為size引數所指定的位元組數
  • malloc的返回值是一個指標,指向一段可用記憶體的起始地址
  • 多次呼叫malloc所分配的地址不能有重疊部分,除非某次malloc所分配的地址被釋放掉
  • malloc應該儘快完成記憶體分配並返回(不能使用NP-hard的記憶體分配演算法)
  • 實現malloc時應同時實現記憶體大小調整和記憶體釋放函式(即realloc和free)

對於malloc更多的說明可以在命令列中鍵入以下命令檢視:

C
1 manmalloc

2 預備知識

在實現malloc之前,需要先解釋一些Linux系統記憶體相關的知識。

2.1 Linux記憶體管理

2.1.1 虛擬記憶體地址與實體記憶體地址

為了簡單,現代作業系統在處理記憶體地址時,普遍採用虛擬記憶體地址技術。即在彙編程式(或機器語言)層面,當涉及記憶體地址時,都是使用虛擬記憶體地址。採用這種技術時,每個程序彷彿自己獨享一片2N位元組的記憶體,其中N是機器位數。例如在64位CPU和64位作業系統下,每個程序的虛擬地址空間為264Byte。

這種虛擬地址空間的作用主要是簡化程式的編寫及方便作業系統對程序間記憶體的隔離管理,真實中的程序不太可能(也用不到)如此大的記憶體空間,實際能用到的記憶體取決於實體記憶體大小。

由於在機器語言層面都是採用虛擬地址,當實際的機器碼程式涉及到記憶體操作時,需要根據當前程序執行的實際上下文將虛擬地址轉換為實體記憶體地址,才能實現對真實記憶體資料的操作。這個轉換一般由一個叫MMU(Memory Management Unit)的硬體完成。

2.1.2 頁與地址構成

在現代作業系統中,不論是虛擬記憶體還是實體記憶體,都不是以位元組為單位進行管理的,而是以頁(Page)為單位。一個記憶體頁是一段固定大小的連續記憶體地址的總稱,具體到Linux中,典型的記憶體頁大小為4096Byte(4K)。

所以記憶體地址可以分為頁號和頁內偏移量。下面以64位機器,4G實體記憶體,4K頁大小為例,虛擬記憶體地址和實體記憶體地址的組成如下:

上面是虛擬記憶體地址,下面是實體記憶體地址。由於頁大小都是4K,所以頁內便宜都是用低12位表示,而剩下的高地址表示頁號。

MMU對映單位並不是位元組,而是頁,這個對映通過查一個常駐記憶體的資料結構頁表來實現。現在計算機具體的記憶體地址對映比較複雜,為了加快速度會引入一系列快取和優化,例如TLB等機制。下面給出一個經過簡化的記憶體地址翻譯示意圖,雖然經過了簡化,但是基本原理與現代計算機真實的情況的一致的。

2.1.3 記憶體頁與磁碟頁

我們知道一般將記憶體看做磁碟的的快取,有時MMU在工作時,會發現頁表表明某個記憶體頁不在實體記憶體中,此時會觸發一個缺頁異常(Page Fault),此時系統會到磁碟中相應的地方將磁碟頁載入到記憶體中,然後重新執行由於缺頁而失敗的機器指令。關於這部分,因為可以看做對malloc實現是透明的,所以不再詳細講述,有興趣的可以參考《深入理解計算機系統》相關章節。

最後附上一張在維基百科找到的更加符合真實地址翻譯的流程供大家參考,這張圖加入了TLB和缺頁異常的流程(圖片來源頁)。

2.2 Linux程序級記憶體管理

2.2.1 記憶體排布

明白了虛擬記憶體和實體記憶體的關係及相關的對映機制,下面看一下具體在一個程序內是如何排布記憶體的。

以Linux 64位系統為例。理論上,64bit記憶體地址可用空間為0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF,這是個相當龐大的空間,Linux實際上只用了其中一小部分(256T)。

根據Linux核心相關文件描述,Linux64位作業系統僅使用低47位,高17位做擴充套件(只能是全0或全1)。所以,實際用到的地址為空間為0x0000000000000000 ~ 0x00007FFFFFFFFFFF和0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF,其中前面為使用者空間(User Space),後者為核心空間(Kernel Space)。圖示如下:

對使用者來說,主要關注的空間是User Space。將User Space放大後,可以看到裡面主要分為如下幾段:

  • Code:這是整個使用者空間的最低地址部分,存放的是指令(也就是程式所編譯成的可執行機器碼)
  • Data:這裡存放的是初始化過的全域性變數
  • BSS:這裡存放的是未初始化的全域性變數
  • Heap:堆,這是我們本文重點關注的地方,堆自低地址向高地址增長,後面要講到的brk相關的系統呼叫就是從這裡分配記憶體
  • Mapping Area:這裡是與mmap系統呼叫相關的區域。大多數實際的malloc實現會考慮通過mmap分配較大塊的記憶體區域,本文不討論這種情況。這個區域自高地址向低地址增長
  • Stack:這是棧區域,自高地址向低地址增長

下面我們主要關注Heap區域的操作。對整個Linux記憶體排布有興趣的同學可以參考其它資料。

2.2.2 Heap記憶體模型

一般來說,malloc所申請的記憶體主要從Heap區域分配(本文不考慮通過mmap申請大塊記憶體的情況)。

由上文知道,程序所面對的虛擬記憶體地址空間,只有按頁對映到實體記憶體地址,才能真正使用。受物理儲存容量限制,整個堆虛擬記憶體空間不可能全部對映到實際的實體記憶體。Linux對堆的管理示意如下:

Linux維護一個break指標,這個指標指向堆空間的某個地址。從堆起始地址到break之間的地址空間為對映好的,可以供程序訪問;而從break往上,是未對映的地址空間,如果訪問這段空間則程式會報錯。

2.2.3 brk與sbrk

由上文知道,要增加一個程序實際的可用堆大小,就需要將break指標向高地址移動。Linux通過brk和sbrk系統呼叫操作break指標。兩個系統呼叫的原型如下:

C
1 2 intbrk(void*addr); void*sbrk(intptr_tincrement);

brk將break指標直接設定為某個地址,而sbrk將break從當前位置移動increment所指定的增量。brk在執行成功時返回0,否則返回-1並設定errno為ENOMEM;sbrk成功時返回break移動之前所指向的地址,否則返回(void *)-1。

一個小技巧是,如果將increment設定為0,則可以獲得當前break的地址。

另外需要注意的是,由於Linux是按頁進行記憶體對映的,所以如果break被設定為沒有按頁大小對齊,則系統實際上會在最後對映一個完整的頁,從而實際已對映的記憶體空間比break指向的地方要大一些。但是使用break之後的地址是很危險的(儘管也許break之後確實有一小塊可用記憶體地址)。

2.2.4 資源限制與rlimit

系統對每一個程序所分配的資源不是無限的,包括可對映的記憶體空間,因此每個程序有一個rlimit表示當前程序可用的資源上限。這個限制可以通過getrlimit系統呼叫得到,下面程式碼獲取當前程序虛擬記憶體空間的rlimit:

C
1 2 3 4 5 intmain(){ structrlimit*limit=(structrlimit*)malloc(sizeof(structrlimit)); getrlimit(RLIMIT_AS,limit); printf("soft limit: %ld, hard limit: %ld\n",limit->rlim_cur,limit->rlim_max); }

其中rlimit是一個結構體:

C
1 2 3 4 structrlimit{ rlim_t rlim_cur;/* Soft limit */ rlim_trlim_max;/* Hard limit (ceiling for rlim_cur) */ };

每種資源有軟限制和硬限制,並且可以通過setrlimit對rlimit進行有條件設定。其中硬限制作為軟限制的上限,非特權程序只能設定軟限制,且不能超過硬限制。

3 實現malloc

3.1 玩具實現

在正式開始討論malloc的實現前,我們可以利用上述知識實現一個簡單但幾乎沒法用於真實的玩具malloc,權當對上面知識的複習:

C
1 2 3 4 5 6 7 8 9 10 11 /* 一個玩具malloc */ #include <sys/types.h> #include <unistd.h> void*malloc(size_tsize) { void*p; p=sbrk(0); if(sbrk(size)==(void*)-1) returnNULL; returnp; }

這個malloc每次都在當前break的基礎上增加size所指定的位元組數,並將之前break的地址返回。這個malloc由於對所分配的記憶體缺乏記錄,不便於記憶體釋放,所以無法用於真實場景。

3.2 正式實現

下面嚴肅點討論malloc的實現方案。

3.2.1 資料結構

首先我們要確定所採用的資料結構。一個簡單可行方案是將堆記憶體空間以塊(Block)的形式組織起來,每個塊由meta區和資料區組成,meta區記錄資料塊的元資訊(資料區大小、空閒標誌位、指標等等),資料區是真實分配的記憶體區域,並且資料區的第一個位元組地址即為malloc返回的地址。

可以用如下結構體定義一個block:

C
1 2 3 4 5 6 7 8 typedefstructs_block*t_block; structs_block{ size_tsize;/* 資料區大小 */ t_block next;/* 指向下個塊的指標 */ intfree;/* 是否是空閒塊 */ intpadding;/* 填充4位元組,保證meta塊長度為8的倍數 */ chardata[1]/* 這是一個虛擬欄位,表示資料塊的第一個位元組,長度不應計入meta */ };

由於我們只考慮64位機器,為了方便,我們在結構體最後填充一個int,使得結構體本身的長度為8的倍數,以便記憶體對齊。示意圖如下:

3.2.2 尋找合適的block

現在考慮如何在block鏈中查詢合適的block。一般來說有兩種查詢演算法:

  • First fit:從頭開始,使用第一個資料區大小大於要求size的塊所謂此次分配的塊
  • Best fit:從頭開始,遍歷所有塊,使用資料區大小大於size且差值最小的塊作為此次分配的塊

兩種方法各有千秋,best fit具有較高的記憶體使用率(payload較高),而first fit具有更好的執行效率。這裡我們採用first fit演算法。

C
1 2 3 4 5 6 7 8 9 /* First fit */ t_block find_block(t_block*last,size_t size){ t_blockb=first_block; while(b&&!(b->free&&b->size>=size)){ *last=b; b=b->next; } returnb; }

find_block從frist_block開始,查詢第一個符合要求的block並返回block起始地址,如果找不到這返回NULL。這裡在遍歷時會更新一個叫last的指標,這個指標始終指向當前遍歷的block。這是為了如果找不到合適的block而開闢新block使用的,具體會在接下來的一節用到。

3.2.3 開闢新的block

如果現有block都不能滿足size的要求,則需要在連結串列最後開闢一個新的block。這裡關鍵是如何只使用sbrk建立一個struct:

C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #define BLOCK_SIZE 24 /* 由於存在虛擬的data欄位,sizeof不能正確計算meta長度,這裡手工設定 */ t_blockextend_heap(t_blocklast,size_ts){ t_blockb; b=sbrk(0); if(sbrk(BLOCK_SIZE+s)==(void*)-1) returnNULL; b->size=s; b->next=NULL; if(last) last->next=b; b->free=0; returnb; }

3.2.4 分裂block

First fit有一個比較致命的缺點,就是可能會讓很小的size佔據很大的一塊block,此時,為了提高payload,應該在剩餘資料區足夠大的情況下,將其分裂為一個新的block,示意如下:

實現程式碼:

C
1 2 3 4 5 6 7 8 9 voidsplit_block(t_blockb,size_ts){ t_block new; new=b->data+s; new->size=b->size-s-BLOCK_SIZE; new->next=b->next; new->free=1; b->size=s; b->next=new; }

3.2.5 malloc的實現

有了上面的程式碼,我們可以利用它們整合成一個簡單但初步可用的malloc。注意首先我們要定義個block連結串列的頭first_block,初始化為NULL;另外,我們需要剩餘空間至少有BLOCK_SIZE + 8才執行分裂操作。

由於我們希望malloc分配的資料區是按8位元組對齊,所以在size不為8的倍數時,我們需要將size調整為大於size的最小的8的倍數:

C
1 2 3 4 5 size_talign8(size_ts){ if(s&0x7==0) returns; return((s>>3)+1)<<3; }
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #define BLOCK_SIZE 24 void*first_block=NULL; /* other functions... */ void*malloc(size_tsize){ t_blockb,last; size_ts; /* 對齊地址 */ s=align8(size); if(first_block){ /* 查詢合適的block */ last=first_block; b=find_block(&last,s); if(b){ /* 如果可以,則分裂 */ if((b->size-s)>=(BLOCK_SIZE+8)) split_block(b,s); b->free=0; }else{ /* 沒有合適的block,開闢一個新的 */ b=extend_heap(last,s); if(!b) returnNULL; } }else{ b=extend_heap(NULL,s); if(!b) returnNULL; first_block=b; } returnb->data; }

3.2.6 calloc的實現

有了malloc,實現calloc只要兩步:

  1. malloc一段記憶體
  2. 將資料區內容置為0

由於我們的資料區是按8位元組對齊的,所以為了提高效率,我們可以每8位元組一組置0,而不是一個一個位元組設定。我們可以通過新建一個size_t指標,將記憶體區域強制看做size_t型別來實現。

C
1 2 3 4 5 6 7 8 9 10 11 void*calloc(size_tnumber,size_t size){ size_t*new; size_ts8,i; new=malloc(number*size); if(new

相關推薦

malloc函式實現原理

任何一個用過或學過C的人對malloc都不會陌生。大家都知道malloc可以分配一段連續的記憶體空間,並且在不再使用時可以通過free釋放掉。但是,許多程式設計師對malloc背後的事情並不熟悉,許多人甚至把malloc當做作業系統所提供的系統呼叫或C的關鍵字。實際上,m

拜託,面試請不要再問我TCC分散式事務的實現原理

往期文章 1、 拜託!面試請不要再問我Spring Cloud底層原理 2、 【雙11狂歡的背後】微服務註冊中心如何承載大型系統的千萬級訪問? 3、 【效能優化之道】每秒上萬併發下的Spring Cloud引數優化實戰 4、 微服務架構如何保障

拜託,面試請不要再問我Redis分散式鎖的實現原理【石杉的架構筆記】

歡迎關注個人公眾號:石杉的架構筆記(ID:shishan100) 週一至五早8點半!精品技術文章準時送上! 目錄 一、寫在前面 二、Redisson實現Redis分散式鎖的底層原理       (1)加鎖機制       (2)鎖互斥機制  

拜託,面試請不要再問我Redis分散式鎖的實現原理

目錄 一、寫在前面 二、Redisson實現Redis分散式鎖的底層原理       (1)加鎖機制       (2)鎖互斥機制       (3)watch dog自動延期機制   &nbs

STL中的sort函式實現原理

STL的sort()演算法,資料量大時採用Quick Sort,分段遞迴排序。一旦分段後的資料量小於某個閾值,為避免Quick Sort的遞迴呼叫帶來過大的額外開銷,就改用Insertion Sort(插入排序)。如果遞迴層次過深,還會改用Heap Sort。 STL中的sort並非只是

簡述malloc實現原理

可以基於夥伴系統實現,也可以使用基於連結串列的實現。 將所有的空閒記憶體塊連成連結串列,每個節點記錄空閒記憶體卡的地址、大小等資訊。 分配記憶體時,找到大小合適的塊,將該記憶體塊一分為二(一塊的大小與使用者請求的大小相等,另一塊的大小就是剩下的位元組) free時,

動態代理之投鞭斷流看一下MyBatis的底層實現原理

作者 l 祖大俊來源:https://my.oschina.net/zudajun一日小區漫步,

C語言編寫log檔案以及printf函式實現原理

C語言編寫log檔案以及printf函式實現原理        在系統除錯中通過在檔案系統裡寫log日誌是一個長期分析系統執行的好方法。做系統除錯應該養成這個習慣,可以方便的監控系統可能出現的各種異常。        今天學習下log日誌的書寫方法。Log一般可以分為以下4種

C++物件模型之虛擬函式實現原理

在C++中,多型(polymorphism)的意思是,用基類的指標或者引用,定址出一個派生類物件。而虛擬函式(virtual member function)是多型的基礎,這也是面向物件程式設計迷人之處。現在剛好有時間,就寫一下自己對C++在單一繼承情況下如何實現虛擬函式的

虛擬函式實現原理

前言 C++中的虛擬函式的作用主要是實現了多型的機制。關於多型,簡而言之就是用父類型別的指標指向其子類的例項,然後通過父類的指標呼叫實際子類的成員函式。這種技術可以讓父類的指標有“多種形態”,這是一種泛型技術。所謂泛型技術,說白了就是試圖使用不變的程式碼來實現可變的演算法。

linux-malloc底層實現原理

本文大致講解一下linux下malloc的底層實現原理。 首先malloc肯定是從堆中分配記憶體,而堆又在使用者空間中佔據什麼位置?通過下面這張圖可以看出來: 很明顯是32位系統,定址空間是4G,linux系統下0-3G是使用者模式,3-4G是核心模式。而在使用者模式下又

手寫執行緒池,對照學習ThreadPoolExecutor執行緒池實現原理

![](https://img-blog.csdnimg.cn/20201210093337790.jpg) 作者:小傅哥 部落格:[https://bugstack.cn](https://bugstack.cn) Github:[https://github.com/fuzhengwei/CodeGuid

設計模式,你相信嗎,只用兩個函式實現事務

大家好,今天給大家介紹一個新的設計模式,叫做memento模式。 memento在英文當中是紀念品的意思,在這裡,指的是物件的深度拷貝。通過對物件深度拷貝的方法來實現事務的功能。有了解過資料庫的小夥伴們應該都知道,在資料庫當中有些操作是繫結的,要麼一起執行成功,要麼一起不執行,絕對不執行某些操作執行了,某些操

malloc()函式實現原理和工作機制

malloc函式的實質體現在,它有一個將可用的記憶體塊連線為一個長長的列表的所謂空閒連結串列。呼叫malloc函式時,它沿連線表尋找一個大到足以滿足使用者請求所需要的記憶體塊。然後,將該記憶體塊一分為二(一塊的大小與使用者請求的大小相等,另一塊的大小就是剩下的位

記憶體動態分配函式malloc的基本實現原理

    malloc是C語言最常用的標準庫函式之一,用於在程式執行中動態地申請記憶體空間。我們都會使用它,其函式原型為: extern void *malloc(unsigned int num_by

C++函式模板及實現原理

    C++為我們提供了函式模板機制。所謂函式模板,實際上是建立一個通用函式,其函式型別和形參型別不具體指定,用一個虛擬的型別來代表。這個通用函式就稱為函式模板。     凡是函式體相同的函式都可以用這個模板來代替,不必定義多個函式,只需在模板中定義

C++多型呼叫實現原理(虛擬函式表詳解)

1.帶有虛擬函式的基類物件模型 我們先看段程式碼: #include<iostream> using namespace std; class B1 { public: void func1() {} int _b; }; class B2 { pub

瞭解MmMapIoSpace以及MmUnmapIoSpace函式實現原理以及實現方法

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

終於有人把“TCC分布式事務”實現原理講明白了

出錯 訂單狀態 關於 重復數據 復數 同時 可能 參數 開啟 之前網上看到很多寫分布式事務的文章,不過大多都是將分布式事務各種技術方案簡單介紹一下。很多朋友看了還是不知道分布式事務到底怎麽回事,在項目裏到底如何使用。 所以這篇文章,就用大白話+手工繪圖,並結合一個電商系統的

阿里P8架構師談:分散式資料庫資料一致性的原理、與技術實現方案

  背景 可用性(Availability)和一致性(Consistency)是分散式系統的基本問題,先有著名的CAP理論定義過分散式環境下二者不可兼得的關係,又有神祕的Paxos協議號稱是史上最簡單的分散式系統一致性演算法並獲得圖靈獎,再有開源產品ZooKeeper實現的Z