做者: 順風車運營研發團隊 李樂php
程序是代碼和數據的集合,進程是運行着的程序;操做系統須要爲進程分配內存;進程運行完畢須要釋放內存;內存管理就是內存的分配和釋放;linux
分段最先出如今8086系統中,當時只有16位地址總線,其能訪問的最大地址是64k;當時的內存大小爲1M;如何利用16位地址訪問1M的內存空間呢?程序員
因而提出了分段式內存管理;
將內存地址分爲段地址與段偏移,段地址會存儲在寄存器中,段偏移即程序實際使用的地址;當CPU須要訪問內存時,會將段地址左移4位,再加上段偏移,便可獲得物理內存地址;
即內存地址=段地址*16+段偏移地址。算法
後來的IA-32在內存中使用一張段表來記錄各個段映射的物理內存地址,CPU只須要爲這個段表提供一個記錄其首地址的寄存器就能夠了;以下圖所示:數組
進程包含多個段:代碼段,數據段,連接庫等;系統須要爲每一個段分配內存;
一種很天然地想法是,根據每一個段實際須要的大小進行分配,並記錄已經佔用的空間和剩餘空間:
當一個段請求內存時,若是有內存中有不少大小不一的空閒位置,那麼選擇哪一個最合理?緩存
a)首先適配:空閒鏈表中選擇第一個位置(優勢:查錶速度快) b)最差適配:選擇一個最大的空閒區域 c)最佳適配:選擇一個空閒位置大小和申請內存大小最接近的位置,好比申請一個40k內存,而恰巧內存中有一個50k的空閒位置;
內存分段管理具備如下優勢:安全
a)內存共享: 對內存分段,能夠很容易把其中的代碼段或數據段共享給其餘程序; b)安全性: 將內存分爲不一樣的段以後,由於不一樣段的內容類型不一樣,因此他們能進行的操做也不一樣,好比代碼段的內容被加載後就不該該容許寫的操做,由於這樣會改變程序的行爲 c)動態連接: 動態連接是指在做業運行以前,並不把幾個目標程序段連接起來。要運行時,先將主程序所對應的目標程序裝入內存並啓動運行,當運行過程當中又須要調用某段時,纔將該段(目標程序)調入內存並進行連接。
儘管分段管理的方式解決了內存的分配與釋放,可是會帶來大量的內存碎片;即儘管咱們內存中仍然存在很大空間,但所有都是一些零散的空間,當申請大塊內存時會出現申請失敗;爲了避免使這些零散的空間浪費,操做系統會作內存緊縮,即將內存中的段移動到另外一位置。但明顯移動進程是一個低效的操做。微信
先說說虛擬內存的概念。CPU訪問物理內存的速度要比磁盤快的多,物理內存能夠認爲是磁盤的緩存,但物理內存是有限的,因而人們想到利用磁盤空間虛擬出的一塊邏輯內存
(這部分磁盤空間Windows下稱之爲虛擬內存,Linux下被稱爲交換空間(Swap Space));數據結構
虛擬內存和真實的物理內存存在着映射關係;函數
爲了解決分段管理帶來的碎片問題,操做系統將虛擬內存分割爲虛擬頁,相應的物理內存被分割爲物理頁;而虛擬頁和物理頁的大小默認都是4K字節;
操做系統以頁爲單位分配內存:假設須要3k字節的內存,操做系統會直接分配一個4K頁給進程
,這就產生了內部碎片(浪費率優於分段管理)
前面說過,物理內存能夠認爲是磁盤的緩存;虛擬頁首先須要分配給進程並建立與物理頁的映射關係,而後才能將磁盤數據載入內存供CPU使用;因而可知,虛擬內存系統必須可以記錄一個虛擬頁是否已經分配給進程;是否已經將磁盤數據載入內存,對應哪一個物理頁;假如沒有載入內存,這個虛擬頁存放在磁盤的哪一個位置;
因而虛擬頁能夠分爲三種類型:已分配,未緩存,已緩存;
當訪問沒有緩存的虛擬頁時,系統會在物理內存中選擇一個犧牲頁,並將虛擬頁從磁盤賦值到物理內存,替換這個犧牲頁;而若是這個犧牲頁已經被修改,則還須要寫回磁盤;這個過程就是所謂的缺頁中斷;
虛擬頁的集合就稱爲頁表(pageTable),頁表就是一個頁表條目(page table entry)的數組;每一個頁表條目都包含有效位標誌,記錄當前虛擬頁是否分配,當前虛擬頁的訪問控制權限;同時包含物理頁號或磁盤地址;
進程所看到的地址都是虛擬地址;在訪問虛擬地址時,操做系統須要將虛擬地址轉化爲實際的物理地址;而虛擬地址到物理地址的映射是存儲在頁表的;
將虛擬地址分爲兩部分:虛擬頁號,記錄虛擬頁在頁表中的偏移量(至關於數組索引);頁內偏移量;而頁表的首地址是存儲在寄存器中;
對於32位系統,內存爲4G,頁大小爲4K,假設每一個頁表項4字節;則頁表包含1M個頁表項,佔用4M的存儲空間,頁表自己就須要分配1K個物理頁;
頁表條目太大時,頁表自己須要佔用更多的物理內存,並且其內存還必須是連續的;
目前有三種優化技術:
1)多級頁表
一級頁表中的每一個PTE負責映射虛擬地址空間中一個4M的片(chunk),每個片由1024個連續的頁面組成;二級頁表的每一個PTE都映射一個4K的虛擬內存頁面;
優勢:節約內存(假如一級頁表中的PTE爲null,則其指向的二級頁表就不存在了,而大多數進程4G的虛擬地址空間大部分都是未分配的;只有一級頁表才老是須要在主存中,系統能夠在須要的時候建立、調入、調出二級頁表)
缺點:虛擬地址到物理地址的翻譯更復雜了
2)TLB
多級頁表能夠節約內存,可是對於一次地址翻譯,增長了內存訪問次數,k級頁表,須要訪問k次內存才能完成地址的翻譯;
由此出現了TLB:他是一個更小,訪問速度更快的虛擬地址的緩存;當須要翻譯虛擬地址時,先在TLB查找,命中的話就能夠直接完成地址的翻譯;沒命中再頁表中查找;
3)hugePage
由於內存大小是固定的,爲了減小映射表的條目,可採起的辦法只有增長頁的尺寸。hugePage便所以而來,使用大頁面2m,4m,16m等等。如此一來映射條目則明顯減小。
linux爲每一個進程維護一個單獨的虛擬地址空間,進程都覺得本身獨佔了整個內存空間,如圖所示:
linux將內存組織爲一些區域(段)的集合,如代碼段,數據段,堆,共享庫段,以及用戶棧都是不一樣的區域。每一個存在的虛擬頁面都保存在某個區域中,不屬於任何一個區域的虛擬頁是不存在的,不能被進程使用;
內核爲系統中的每一個進程維護一個單獨的任務結構task_struct,任務中的一個字段指向mm_struct,他描述了虛擬內存的當前狀態。其中包含兩個字段:pgd指向第一級頁表的基址(當內核運行這個進程時,就將pgd的內容存儲在cr3控制寄存器中);mmap指向一個vm_area_struct區域結構的鏈表;區域結構主要包括如下字段:
vm_start:區域的起始地址;
vm_end:區域的結束地址;
vm_port:指向這個區域所包含頁的讀寫許可權限;
vm_flags:描述這個區域是與其餘進程共享的,仍是私有的等信息;
當咱們訪問虛擬地址時,內核會遍歷vm_area_struct鏈表,根據vm_start和vm_end可以判斷地址合法性;根據vm_por可以判斷地址訪問的合法性;
遍歷鏈表時間性能較差,內核會將vm_area_struct區域組織成一棵樹;
說到這裏就不得不提一下系統調用mmap,其函數聲明爲
void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
函數mmap要求內核建立一個新的虛擬內存區域(注意是新的區域,和堆是平級關係,即mmap函數並非在堆上分配內存的,);最好是從地址addr開始(通常傳null),並將文件描述fd符指定的對象的一個連續的chunk(大小爲len,從文件偏移offset開始)映射到這個新的區域;當fd傳-1時,可用於申請分配內存;
參數port描述這個區域的訪問控制權限,能夠取如下值:
PROT_EXEC //頁內容能夠被執行 PROT_READ //頁內容能夠被讀取 PROT_WRITE //頁能夠被寫入 PROT_NONE //頁不可訪問
參數flags由描述被映射對象類型的位組成,如MAP_SHARED 表示與其它全部映射這個對象的進程共享映射空間;MAP_PRIVATE 表示創建一個寫入時拷貝的私有映射,內存區域的寫入不會影響到原文件。
php在分配2M以上大內存時,就是直接使用mmap申請的;
malloc是c庫函數,用於在堆上分配內存;操做系統給進程分配的堆空間是若干個頁,咱們再調用malloc向進程請求分配若干字節大小的內存;
malloc就是一種內存分配器,負責堆內存的分配與回收;
一樣咱們可使用mmap和munmap來建立和刪除虛擬內存區域,以達到內存的申請與釋放;
觀察第一章第三小節中的虛擬地址空間描述圖,每一個進程都有一個稱爲運行時堆的虛擬內存區域,操做系統內核維護着一個變量brk,指向了堆的頂部;並提供系統調用brk(void* addr)和sbrk(incr)來修改變量brk的值,從而實現堆內存的擴張與收縮;
brk函數將brk指針直接設置爲某個地址,而sbrk函數將brk從當前位置移動incr所指定的增量;(若是將incr設置爲0,則能夠得到當前brk指向的地址)
所以咱們也可使用brk()或sbrk()來動態分配/釋放內存塊;
須要注意的一點是:系統爲每個進程所分配的資源不是無限的,包括可映射的內存空間,即堆內存並非無限大的;因此當調用malloc將堆內存都分配完時,malloc會使用mmap函數額外再申請一個虛擬內存區域(由此發現,使用malloc申請的內存也並不必定是在堆上)
內存分配器用於處理堆上的內存分配或釋放請求;
要實現分配器必須考慮如下幾個問題:
1.空閒塊組織:如何記錄空閒塊;如何標記內存塊是否空閒; 2.分配:如何選擇一個合適的空閒塊來處理分配請求; 3.分割:空閒塊通常狀況會大於實際的分配請求,咱們如何處理這個空閒塊中的剩餘部分; 4.回收:如何處理一個剛剛被釋放的塊;
思考1:空閒塊組織
內存分配與釋放請求時徹底隨機的,最終會形成堆內存被分割爲若干個內存小塊,其中有些處於已分配狀態,有些處於空閒狀態;咱們須要額外的空間來標記內存狀態以及內存塊大小;
下圖爲malloc設計思路:
注:圖中顯示額外使用4字節記錄當前內存塊屬性,其中3比特記錄是否空閒,29比特記錄內存塊大小;實際malloc頭部格式可能會根據版本等調整;不論咱們使用malloc分配多少字節的內存,實際malloc分配的內存都會多幾個字節;
注:空閒內存塊可能會被組織爲一個鏈表結構,由此能夠遍歷全部空閒內存塊,直到查找到一個知足條件的爲止;
思考2:如何選擇合適的空閒塊
在處理內存分配請求時,須要查找空閒內存鏈表,找到一個知足申請條件的空閒內存塊,選擇什麼查找算法;並且頗有可能存在多個符合條件的空閒內存塊,此時如何選擇?
目前有不少比較成熟的算法,如首次適配,最佳適配,最差適配等;
思考3:如何分配
在查找到知足條件的空閒內存塊時,此內存通常狀況會比實際請求分配的內存空間要大;所有分配給用戶,浪費空間;所以通常會將此空閒內存塊切割爲兩個小塊內存,一塊分配給用戶,一塊標記爲新的空閒內存
思考4:如何回收:
當用戶調用free()函數釋放內存時,須要將此塊內存從新標記爲空閒內存,而且插入空閒鏈表;然而須要注意的是,此塊內存可能可以與其餘空閒內存拼接爲更大的空閒內存;此時還須要算法來處理空閒內存的合併;
思考5:內存分配效率問題:
用戶請求分配內存時,須要遍歷空閒內存鏈表,直到查找到一個知足申請條件的空閒內存;因而可知,算法複雜度與鏈表長度成正比;
咱們能夠將空閒內存按照空間大小組織爲多個空閒鏈表,內存大小相近的造成一個鏈表;此時只須要根據申請內存大小查找相應空閒鏈表便可;
更進一步的,空閒內存只會被切割爲固定大小,如2^n字節,每種字節大小的空閒內存造成一個鏈表;(用戶實際分配的內存是2^n字節,大於用戶實際請求)
總結:任何內存分配器都須要額外的空間(數據結構)記錄每一個內存塊大小及其分配狀態;
C/C++下內存管理是讓幾乎每個程序員頭疼的問題,分配足夠的內存、追蹤內存的分配、在不須要的時候釋放內存——這個任務至關複雜。而直接使用系統調用malloc/free、new/delete進行內存分配和釋放,有如下弊端:
調用malloc/new,系統須要根據「最早匹配」、「最優匹配」或其餘算法在內存空閒塊表中查找一塊空閒內存,調用free/delete,系統可能須要合併空閒內存塊,這些都會產生額外的開銷;
頻繁使用時會產生大量內存碎片,從而下降程序運行效率;
容易形成內存泄漏;
內存池(memory pool)是代替直接調用malloc/free、new/delete進行內存管理的經常使用方法,當咱們申請內存空間時,首先到咱們的內存池中查找合適的內存塊,而不是直接向操做系統申請,優點在於:
比malloc/free進行內存申請/釋放的方式快
不會產生或不多產生堆碎片
可避免內存泄漏
內存池通常會組織成以下結構:
結構中主要包含block、list 和pool這三個結構體,block結構包含指向實際內存空間的指針,前向和後向指針讓block可以組成雙向鏈表;list結構中free指針指向空閒 內存塊組成的鏈表,used指針指向程序使用中的內存塊組成的鏈表,size值爲內存塊的大小,list之間組成單向鏈表;pool結構記錄list鏈表的頭和尾。
當用戶申請內存時,只須要根據所申請內存的大小,遍歷list鏈表,查看是否存在相匹配的size;
PHP並無直接使用現有的malloc/free來管理內存的分配和釋放,而是從新實現了一套內存管理方案;
PHP採起「預分配方案」,提早向操做系統申請一個chunk(2M,利用到hugepage特性),而且將這2M內存切割爲不一樣規格(大小)的若干內存塊,當程序申請內存時,直接查找現有的空閒內存塊便可;
PHP將內存分配請求分爲3種狀況:
huge內存:針對大於2M-4K的分配請求,直接調用mmap分配;
large內存:針對小於2M-4K,大於3K的分配請求,在chunk上查找知足條件的若干個連續page;
small內存:針對小於3K的分配請求;PHP拿出若干個頁切割爲8字節大小的內存塊,拿出若干個頁切割爲16字節大小的內存塊,24字節,32字節等等,將其組織成若干個空閒鏈表;每當有分配請求時,只在對應的空閒鏈表獲取一個內存塊便可;
1.1結構體
PHP須要記錄申請的全部chunk,須要記錄chunk中page的使用狀況,要記錄每種規格內存的空閒鏈表,要記錄使用mmap分配的huge內存,等等…………
因而有了如下兩個結構體:
_zend_mm_heap記錄着內存管理器所需的全部數據:
//省略告終構體中不少字段 struct _zend_mm_heap { //統計 size_t size; /* current memory usage */ size_t peak; /* peak memory usage */ //因爲「預分配」方案,實際使用內存和向操做系統申請的內存大小是不同的; size_t real_size; /* current size of allocated pages */ size_t real_peak; /* peak size of allocated pages */ //small內存分爲30種;free_slot數組長度爲30;數組索引上掛着內存空閒鏈表 zend_mm_free_slot *free_slot[ZEND_MM_BINS]; /* free lists for small sizes */ //內存限制 size_t limit; /* memory limit */ int overflow; /* memory overflow flag */ //記錄已分配的huge內存 zend_mm_huge_list *huge_list; /* list of huge allocated blocks */ //PHP會分配若干chunk,記錄當前主chunk首地址 zend_mm_chunk *main_chunk; //統計chunk數目 int chunks_count; /* number of alocated chunks */ int peak_chunks_count; /* peak number of allocated chunks for current request */ }
_zend_mm_chunk記錄着當前chunk的全部數據
struct _zend_mm_chunk { //指向heap zend_mm_heap *heap; //chunk組織爲雙向鏈表 zend_mm_chunk *next; zend_mm_chunk *prev; //當前chunk空閒page數目 uint32_t free_pages; /* number of free pages */ //當前chunk最後一個空閒的page位置 uint32_t free_tail; /* number of free pages at the end of chunk */ //每當申請一個新的chunk時,這個chunk的num會遞增 uint32_t num; //預留 char reserve[64 - (sizeof(void*) * 3 + sizeof(uint32_t) * 3)]; //指向heap,只有main_chunk使用 zend_mm_heap heap_slot; /* used only in main chunk */ //記錄512個page的分配狀況;0表明空閒,1表明已分配 zend_mm_page_map free_map; /* 512 bits or 64 bytes */ //記錄每一個page的詳細信息, zend_mm_page_info map[ZEND_MM_PAGES]; /* 2 KB = 512 * 4 */ };
1.2small內存
前面講過small內存分爲30種規格,每種規格的空閒內存都掛在_zend_mm_heap結構體的free_slot數組上;
30種規格內存以下:
//宏定義:第一列表示序號(稱之爲bin_num),第二列表示每一個small內存的大小(字節數); //第四列表示每次獲取多少個page;第三列表示將page分割爲多少個大小爲第一列的small內存; #define ZEND_MM_BINS_INFO(_, x, y) \ _( 0, 8, 512, 1, x, y) \ _( 1, 16, 256, 1, x, y) \ _( 2, 24, 170, 1, x, y) \ _( 3, 32, 128, 1, x, y) \ _( 4, 40, 102, 1, x, y) \ _( 5, 48, 85, 1, x, y) \ _( 6, 56, 73, 1, x, y) \ _( 7, 64, 64, 1, x, y) \ _( 8, 80, 51, 1, x, y) \ _( 9, 96, 42, 1, x, y) \ _(10, 112, 36, 1, x, y) \ _(11, 128, 32, 1, x, y) \ _(12, 160, 25, 1, x, y) \ _(13, 192, 21, 1, x, y) \ _(14, 224, 18, 1, x, y) \ _(15, 256, 16, 1, x, y) \ _(16, 320, 64, 5, x, y) \ _(17, 384, 32, 3, x, y) \ _(18, 448, 9, 1, x, y) \ _(19, 512, 8, 1, x, y) \ _(20, 640, 32, 5, x, y) \ _(21, 768, 16, 3, x, y) \ _(22, 896, 9, 2, x, y) \ _(23, 1024, 8, 2, x, y) \ _(24, 1280, 16, 5, x, y) \ _(25, 1536, 8, 3, x, y) \ _(26, 1792, 16, 7, x, y) \ _(27, 2048, 8, 4, x, y) \ _(28, 2560, 8, 5, x, y) \ _(29, 3072, 4, 3, x, y) #endif /* ZEND_ALLOC_SIZES_H */
只有這個宏定義有些功能很差用程序實現,好比bin_num=15時,得到此種small內存的字節數?分配此種small內存時須要多少page呢?
因而有了如下3個數組的定義:
//bin_pages是一維數組,數組大小爲30,數組索引爲bin_num, //數組元素爲ZEND_MM_BINS_INFO宏中的第四列 #define _BIN_DATA_PAGES(num, size, elements, pages, x, y) pages, static const uint32_t bin_pages[] = { ZEND_MM_BINS_INFO(_BIN_DATA_PAGES, x, y) };
//bin_elements是一維數組,數組大小爲30,數組索引爲bin_num, //數組元素爲ZEND_MM_BINS_INFO宏中的第三列 #define _BIN_DATA_ELEMENTS(num, size, elements, pages, x, y) elements, static const uint32_t bin_elements[] = { ZEND_MM_BINS_INFO(_BIN_DATA_ELEMENTS, x, y) };
//bin_data_size是一維數組,數組大小爲30,數組索引爲bin_num, //數組元素爲ZEND_MM_BINS_INFO宏中的第二列 #define _BIN_DATA_SIZE(num, size, elements, pages, x, y) size, static const uint32_t bin_data_size[] = { ZEND_MM_BINS_INFO(_BIN_DATA_SIZE, x, y) };
2.1設計思路
上一節提到PHP將small內存分爲30種不一樣大小的規格;
每種大小規格的空閒內存會組織爲鏈表,掛在數組_zend_mm_heap結構體的free_slot[bin_num]索引上;
回顧下free_slot字段的定義:
zend_mm_free_slot *free_slot[ZEND_MM_BINS]; struct zend_mm_free_slot { zend_mm_free_slot *next_free_slot; };
能夠看出空閒內存鏈表的每一個節點都是一個zend_mm_free_slot結構體,其只有一個next指針字段;
思考:對於8字節大小的內存塊,其next指針就須要佔8字節的空間,那用戶的數據存儲在哪裏呢?
答案:free_slot是small內存的空閒鏈表,空閒指的是未分配內存,此時是不須要存儲其餘數據的;當分配給用戶時,此節點會從空閒鏈表刪除,也就不須要維護next指針了;用戶能夠在8字節裏存儲任何數據;
思考:假設調用 void*ptr=emalloc(8)分配了一塊內存;調用efree(ptr)釋放內存時,PHP如何知道這塊內存的字節數呢?如何知道這塊內存應該插入哪一個空閒鏈表呢?
思考1:第二章指出,任何內存分配器都須要額外的數據結構來標誌其管理的每一塊內存:空閒/已分配,內存大小等;PHP也不例外;但是咱們發現使用emalloc(8)分配內存時,其分配的就只是8字節的內存,並無額外的空間來存儲這塊內存的任何屬性;
思考2:觀察small內存宏定義ZEND_MM_BINS_INFO;咱們發現對於每個page,其只可能被分配爲同一種規格;不可能存在一部分分割爲8字節大小,一部分分割爲16字節大小;也就是說每個page的全部small內存塊屬性是相同的;那麼只須要記錄每個page的屬性便可;
思考3:large內存是一樣的思路;申請large內存時,可能須要佔若干個page的空間;可是同一個page只會屬於一個large內存,不可能將一個page的一部分分給某個large內存;
答案:無論page用於small內存仍是large內存分配,只須要記錄每個page的屬性便可,PHP將其記錄在zend_mm_chunk結構體的zend_mm_page_info map[ZEND_MM_PAGES]字段;長度爲512的int數組;對任一塊內存,只要能計算出屬於哪個頁,就能獲得其屬性(內存大小);
2.2入口API
//內存分配對外統一入口API爲_emalloc;函數內部直接調用zend_mm_alloc_heap, //其第一個參數就是zend_mm_heap結構體(全局只有一個),第二個參數就是請求分配內存大小 void* _emalloc(size_t size) { return zend_mm_alloc_heap(AG(mm_heap), size); }
//能夠看出其根據請求內存大小size判斷分配small內存仍是large內存,仍是huge內存 static void *zend_mm_alloc_heap(zend_mm_heap *heap, size_t size) { void *ptr; if (size <= ZEND_MM_MAX_SMALL_SIZE) { ptr = zend_mm_alloc_small(heap, size, ZEND_MM_SMALL_SIZE_TO_BIN(size)); //注意ZEND_MM_SMALL_SIZE_TO_BIN這個宏定義 return ptr; } else if (size <= ZEND_MM_MAX_LARGE_SIZE) { ptr = zend_mm_alloc_large(heap, size); return ptr; } else { return zend_mm_alloc_huge(heap, size); } } //使用到的宏定義以下 #define ZEND_MM_CHUNK_SIZE (2 * 1024 * 1024) /* 2 MB */ #define ZEND_MM_PAGE_SIZE (4 * 1024) /* 4 KB */ #define ZEND_MM_PAGES (ZEND_MM_CHUNK_SIZE / ZEND_MM_PAGE_SIZE) /* 512 */ #define ZEND_MM_FIRST_PAGE (1) #define ZEND_MM_MAX_SMALL_SIZE 3072 #define ZEND_MM_MAX_LARGE_SIZE (ZEND_MM_CHUNK_SIZE - (ZEND_MM_PAGE_SIZE * ZEND_MM_FIRST_PAGE))
2.3計算規格(bin_num)
咱們發如今調用zend_mm_alloc_small時,使用到了ZEND_MM_SMALL_SIZE_TO_BIN,其定義了一個函數,用於將size轉換爲bin_num;即請求7字節時,實際須要分配8字節,bin_num=1;請求37字節時,實際須要分配40字節,bin_num=4;即根據請求的size計算知足條件的最小small內存規格的bin_num;
#define ZEND_MM_SMALL_SIZE_TO_BIN(size) zend_mm_small_size_to_bin(size) static zend_always_inline int zend_mm_small_size_to_bin(size_t size) { unsigned int t1, t2; if (size <= 64) { /* we need to support size == 0 ... */ return (size - !!size) >> 3; } else { t1 = size - 1; t2 = zend_mm_small_size_to_bit(t1) - 3; t1 = t1 >> t2; t2 = t2 - 3; t2 = t2 << 2; return (int)(t1 + t2); //看到這一堆t1,t2,腦子裏只有一個問題:我是誰,我在哪,這是啥; } }
1)先分析size小於64狀況:看看small內存前8組大小定義,8,16,24,32,48,56,64;很簡單,就是等差數列,遞增8;因此對於每一個size只要除以8就能夠了(右移3位);可是對於size=8,16,24,32,40,48,56,64這些值,須要size-1而後除以8才知足;考慮到size=0的狀況,因而有了(size - !!size) >> 3這個表達式;
2)當size大於64時,狀況就複雜了:small內存的字節數變化爲,64,80,96,112,128,160,192,224,256,320,384,448,512……;遞增16,遞增32,遞增64……;
仍是先看看二進制吧:
咱們將size每4個分爲一組,第一組比特序列長度爲7,第二組比特序列長度爲8,……;(即咱們能夠根據比特序列長度得到sise屬於哪一組;思考一下,遞增16,32時,爲何只會加四次呢?)
那咱們能夠這麼算:1)計算出size屬於第幾組;2)計算size在組內的偏移量;3)計算組開始位置。思路就是這樣,可是計算方法並不統一,只要找規律計算出來便可。
//計算當前size屬於哪一組;也就是計算比特序列長度;也就是計算最高位是1的位置; //從低到高位查找也行,O(n)複雜度;使用二分查號,複雜度log(n) //size最大爲3072(不知道的回去看small內存宏定義);將size的二進制當作16比特的序列; //先按照8二分,再按照4或12二分,再按照2/6/10/16二分…… //思路:size與255比較(0xff)比較,若是小於,說明高8位全是0,只須要在低8位查找便可; //………… /* higher set bit number (0->N/A, 1->1, 2->2, 4->3, 8->4, 127->7, 128->8 etc) */ static zend_always_inline int zend_mm_small_size_to_bit(int size) { int n = 16; if (size <= 0x00ff) {n -= 8; size = size << 8;} if (size <= 0x0fff) {n -= 4; size = size << 4;} if (size <= 0x3fff) {n -= 2; size = size << 2;} if (size <= 0x7fff) {n -= 1;} return n; }
2.4開始分配了
前面說過small空閒內存會造成鏈表,掛在zen_mm_heap字段free_slot[bin_num]上;
最初請求分配時,free_slot[bin_num]可能尚未初始化,指向null;此時須要向chunk分配若干頁,將頁分割爲大小相同的內存塊,造成鏈表,掛在free_slot[bin_num]
static zend_always_inline void *zend_mm_alloc_small(zend_mm_heap *heap, size_t size, int bin_num) { //空閒鏈表不爲null,直接分配 if (EXPECTED(heap->free_slot[bin_num] != NULL)) { zend_mm_free_slot *p = heap->free_slot[bin_num]; heap->free_slot[bin_num] = p->next_free_slot; return (void*)p; } else { //先分配頁 return zend_mm_alloc_small_slow(heap, bin_num; } }
//分配頁;切割;造成鏈表 static zend_never_inline void *zend_mm_alloc_small_slow(zend_mm_heap *heap, uint32_t bin_num) { zend_mm_chunk *chunk; int page_num; zend_mm_bin *bin; zend_mm_free_slot *p, *end; //分配頁(頁數目是small內存宏定義第四列);放在下一節large內存分配講解 bin = (zend_mm_bin*)zend_mm_alloc_pages(heap, bin_pages[bin_num]); if (UNEXPECTED(bin == NULL)) { /* insufficient memory */ return NULL; } //以前提過任何內存分配器都須要額外的數據結構記錄每塊內存的屬性;分析發現PHP每一個page的全部內存塊屬性都是相同的;且存儲在zend_mm_chunk結構體的map字段(512個int) //bin即頁的首地址;須要計算bin是當前chunk的第幾頁:1)獲得chunk首地址;2)獲得bin相對chunk首地址偏移量;3)除以頁大小 chunk = (zend_mm_chunk*)ZEND_MM_ALIGNED_BASE(bin, ZEND_MM_CHUNK_SIZE); page_num = ZEND_MM_ALIGNED_OFFSET(bin, ZEND_MM_CHUNK_SIZE) / ZEND_MM_PAGE_SIZE; //記錄頁屬性;後面分析(對於分配的每一個頁都要記錄屬性) chunk->map[page_num] = ZEND_MM_SRUN(bin_num); if (bin_pages[bin_num] > 1) { uint32_t i = 1; do { chunk->map[page_num+i] = ZEND_MM_NRUN(bin_num, i); i++; } while (i < bin_pages[bin_num]); } //切割內存;造成鏈表(bin_data_size,bin_elements是上面介紹過的small內存相關數組) end = (zend_mm_free_slot*)((char*)bin + (bin_data_size[bin_num] * (bin_elements[bin_num] - 1))); heap->free_slot[bin_num] = p = (zend_mm_free_slot*)((char*)bin + bin_data_size[bin_num]); do { p->next_free_slot = (zend_mm_free_slot*)((char*)p + bin_data_size[bin_num]); p = (zend_mm_free_slot*)((char*)p + bin_data_size[bin_num]); } while (p != end); /* terminate list using NULL */ p->next_free_slot = NULL; /* return first element */ return (char*)bin; }
2.5說說記錄頁屬性的map
1)對任意地址p,如何計算頁號?
地址p減去chunk首地址得到偏移量;偏移量除4K便可;問題是如何得到chunk首地址?咱們看看源碼:
chunk = (zend_mm_chunk*)ZEND_MM_ALIGNED_BASE(bin, ZEND_MM_CHUNK_SIZE); page_num = ZEND_MM_ALIGNED_OFFSET(bin, ZEND_MM_CHUNK_SIZE) / ZEND_MM_PAGE_SIZE; #define ZEND_MM_ALIGNED_OFFSET(size, alignment) \ (((size_t)(size)) & ((alignment) - 1)) #define ZEND_MM_ALIGNED_BASE(size, alignment) \ (((size_t)(size)) & ~((alignment) - 1)) #define ZEND_MM_SIZE_TO_NUM(size, alignment) \ (((size_t)(size) + ((alignment) - 1)) / (alignment))
咱們發現計算偏移量或chunk首地址時,須要兩個參數:size,地址p;alignment,調用時傳的是ZEND_MM_CHUNK_SIZE(2M);
其實PHP在申請chunk時,額外添加了一個條件:chunk首地址2M字節對齊;
如圖,2M字節對齊時,給定任意地址p,p的低21位即地址p相對於chunk首地址的偏移量;
那如何保證chunk首地址2M字節對齊呢?分析源碼:
//chunk大小爲size 2M;chunk首地址對齊方式 2M static void *zend_mm_chunk_alloc_int(size_t size, size_t alignment) { void *ptr = zend_mm_mmap(size); if (ptr == NULL) { return NULL; } else if (ZEND_MM_ALIGNED_OFFSET(ptr, alignment) == 0) { //2M對齊,直接返回 return ptr; } else { size_t offset; //沒有2M對齊,先釋放,再從新分配2M+2M-4K空間 //從新分配大小爲2M+2M也是能夠的(減4K是由於操做系統分配內存按頁分配的,頁大小4k) //此時總能定位一段2M的內存空間,且首地址2M對齊 zend_mm_munmap(ptr, size); ptr = zend_mm_mmap(size + alignment - REAL_PAGE_SIZE); //分配了2M+2M-4K空間,須要釋放前面、後面部分空間。只保留中間按2M字節對齊的chunk便可 offset = ZEND_MM_ALIGNED_OFFSET(ptr, alignment); if (offset != 0) { offset = alignment - offset; zend_mm_munmap(ptr, offset); ptr = (char*)ptr + offset; alignment -= offset; } if (alignment > REAL_PAGE_SIZE) { zend_mm_munmap((char*)ptr + size, alignment - REAL_PAGE_SIZE); } return ptr; } } //理論分析,申請2M空間,能直接2M字節對齊的機率很低;可是實驗發現,機率仍是蠻高的,這可能與內核分配內存有關;
2)每一個頁都須要記錄哪些屬性?
chunk裏的某個頁,能夠分配爲large內存,large內存連續佔多少個頁;能夠分配爲small內存,對應的是哪一種規格的small內存(bin_num)
//29-31比特表示當前頁分配爲small仍是large //當前頁用於large內存分配 #define ZEND_MM_IS_LRUN 0x40000000 //當前頁用於small內存分配 #define ZEND_MM_IS_SRUN 0x80000000 //對於large內存,0-9比特表示分配的頁數目 #define ZEND_MM_LRUN_PAGES_MASK 0x000003ff #define ZEND_MM_LRUN_PAGES_OFFSET 0 //對於small內存,0-4比特表示bin_num #define ZEND_MM_SRUN_BIN_NUM_MASK 0x0000001f #define ZEND_MM_SRUN_BIN_NUM_OFFSET 0 //count即large內存佔了多少個頁 #define ZEND_MM_LRUN(count) (ZEND_MM_IS_LRUN | ((count) << ZEND_MM_LRUN_PAGES_OFFSET)) #define ZEND_MM_SRUN(bin_num) (ZEND_MM_IS_SRUN | ((bin_num) << ZEND_MM_SRUN_BIN_NUM_OFFSET))
再回顧一下small內存30種規格的宏定義,bin_num=1六、1七、20-29時,須要分配大於1個頁;此時不只須要記錄bin_num,還須要記錄其對應的頁數目
#define ZEND_MM_SRUN_BIN_NUM_MASK 0x0000001f #define ZEND_MM_SRUN_BIN_NUM_OFFSET 0 #define ZEND_MM_SRUN_FREE_COUNTER_MASK 0x01ff0000 #define ZEND_MM_SRUN_FREE_COUNTER_OFFSET 16 #define ZEND_MM_NRUN_OFFSET_MASK 0x01ff0000 #define ZEND_MM_NRUN_OFFSET_OFFSET 16 //當前頁分配爲small內存;0-4比特存儲bin_num;16-25存儲當前規格須要分配的頁數目; #define ZEND_MM_SRUN_EX(bin_num, count) (ZEND_MM_IS_SRUN | ((bin_num) << ZEND_MM_SRUN_BIN_NUM_OFFSET) | ((count) << ZEND_MM_SRUN_FREE_COUNTER_OFFSET)) //29-31比特表示同時屬於small內存和large內存;0-4比特存儲bin_num;16-25存儲偏移量 //對於bin_num=29,須要分配3個頁,假設爲10,11,12號頁 //map[10]=ZEND_MM_SRUN_EX(29,3);map[11]=ZEND_MM_NRUN(29,1);map[12]=ZEND_MM_NRUN(29,2); #define ZEND_MM_NRUN(bin_num, offset) (ZEND_MM_IS_SRUN | ZEND_MM_IS_LRUN | ((bin_num) << ZEND_MM_SRUN_BIN_NUM_OFFSET) | ((offset) << ZEND_MM_NRUN_OFFSET_OFFSET))
須要從chunk中查找連續pages_count個空閒的頁;zend_mm_chunk結構體的free_map爲512個比特,記錄着每一個頁空閒仍是已分配;
以64位機器爲例,free_map又被分爲8組;每組64比特,看做uint32_t類型;
#define ZEND_MM_CHUNK_SIZE (2 * 1024 * 1024) /* 2 MB */ #define ZEND_MM_PAGE_SIZE (4 * 1024) /* 4 KB */ #define ZEND_MM_PAGES (ZEND_MM_CHUNK_SIZE / ZEND_MM_PAGE_SIZE) /* 512 */ typedef zend_ulong zend_mm_bitset; /* 4-byte or 8-byte integer */ #define ZEND_MM_BITSET_LEN (sizeof(zend_mm_bitset) * 8) /* 32 or 64 */ #define ZEND_MM_PAGE_MAP_LEN (ZEND_MM_PAGES / ZEND_MM_BITSET_LEN) /* 16 or 8 */
static void *zend_mm_alloc_pages(zend_mm_heap *heap, uint32_t pages_count) { //獲取main_chunk zend_mm_chunk *chunk = heap->main_chunk; uint32_t page_num, len; int steps = 0; //其實就是最佳適配算法 while (1) { //free_pages記錄當前chunk的空閒頁數目 if (UNEXPECTED(chunk->free_pages < pages_count)) { goto not_found; } else { /* Best-Fit Search */ int best = -1; uint32_t best_len = ZEND_MM_PAGES; //從free_tail位置開始,後面得頁都是空閒的 uint32_t free_tail = chunk->free_tail; zend_mm_bitset *bitset = chunk->free_map; zend_mm_bitset tmp = *(bitset++); uint32_t i = 0; //從第一組開始遍歷;查找若干連續空閒頁;i實際每次遞增64; //最佳適配算法;查找到知足條件的間隙,空閒頁數目大於pages_count; //best記錄間隙首位置;best_len記錄間隙空閒頁數目 while (1) { //注意:(zend_mm_bitset)-1,表示將-1強制類型轉換爲64位無符號整數,即64位全1(表示當前組的頁全被分配了) while (tmp == (zend_mm_bitset)-1) { i += ZEND_MM_BITSET_LEN; if (i == ZEND_MM_PAGES) { if (best > 0) { page_num = best; goto found; } else { goto not_found; } } tmp = *(bitset++); //當前組的全部頁都分配了,遞增到下一組 } //每個空閒間隙,確定有若干個比特0,查找第一個比特0的位置: //假設當前tmp=01111111(低7位全1,高位全0);則zend_mm_bitset_nts函數返回8 page_num = i + zend_mm_bitset_nts(tmp); 函數實現後面分析 //tmp+1->10000000; tmp&(tmp+1) 其實就是把tmp的低8位所有置0,只保留高位 tmp &= tmp + 1; //若是此時tmp == 0,說明從第個頁page_num到當前組最後一個頁,都是未分配的; //不然,須要找出這個空閒間隙另一個0的位置,相減才能夠得出空閒間隙頁數目 while (tmp == 0) { i += ZEND_MM_BITSET_LEN; //i+64,若是超出free_tail或者512,說明從page_num開始後面全部頁都是空閒的;不然遍歷下一組 if (i >= free_tail || i == ZEND_MM_PAGES) { len = ZEND_MM_PAGES - page_num; if (len >= pages_count && len < best_len) { //從page_num處開始後面頁都空閒,且剩餘頁數目小於已經查找到的連續空閒頁數目,直接分配 chunk->free_tail = page_num + pages_count; goto found; } else { //當前空閒間隙頁不知足條件 chunk->free_tail = page_num; if (best > 0) { //以前有查找到空閒間隙符合分配條件 page_num = best; goto found; } else { //以前沒有查找到空閒頁知足條件,說明失敗 goto not_found; } } } tmp = *(bitset++); //遍歷下一組 } //假設最初tmp=1111000001111000111111,tmp&=tmp+1後,tmp=1111000001111000 000000 //上面while循環進不去;且page_num=7+i; //此時需從低到高位查找第一個1比特位置,爲11,11+i-(7+i)=4,便是連續空閒頁數目 len = i + zend_ulong_ntz(tmp) - page_num; if (len >= pages_count) { //知足分配條件,記錄 if (len == pages_count) { goto found; } else if (len < best_len) { best_len = len; best = page_num; } } //上面計算後tmp=1111000001111000 000000;發現這一組還有一個空閒間隙,擁有5個空閒頁,下一個循環確定須要查找出來; //而目前低10比特其實已經查找過了,那麼須要將低10比特所有置1,以防再次查找到; //tmp-1:1111000001110111 111111; tmp |= tmp - 1:1111000001111111 111111 tmp |= tmp - 1; } } not_found: ……………… found: //查找到知足條件的連續頁,設置從page_num開始pages_count個頁爲已分配 chunk->free_pages -= pages_count; zend_mm_bitset_set_range(chunk->free_map, page_num, pages_count); //標誌當前頁用於large內存分配,分配數目爲pages_count chunk->map[page_num] = ZEND_MM_LRUN(pages_count); //更新free_tail if (page_num == chunk->free_tail) { chunk->free_tail = page_num + pages_count; } //返回當前第一個page的首地址 return ZEND_MM_PAGE_ADDR(chunk, page_num); } //4K大小的字節數組 struct zend_mm_page { char bytes[ZEND_MM_PAGE_SIZE]; }; //偏移page_num*4K #define ZEND_MM_PAGE_ADDR(chunk, page_num) \ ((void*)(((zend_mm_page*)(chunk)) + (page_num)))
看看PHP是如何高效查找0比特位置的:依然是二分查找
static zend_always_inline int zend_mm_bitset_nts(zend_mm_bitset bitset) { int n=0; //64位機器纔會執行 #if SIZEOF_ZEND_LONG == 8 if (sizeof(zend_mm_bitset) == 8) { if ((bitset & 0xffffffff) == 0xffffffff) {n += 32; bitset = bitset >> Z_UL(32);} } #endif if ((bitset & 0x0000ffff) == 0x0000ffff) {n += 16; bitset = bitset >> 16;} if ((bitset & 0x000000ff) == 0x000000ff) {n += 8; bitset = bitset >> 8;} if ((bitset & 0x0000000f) == 0x0000000f) {n += 4; bitset = bitset >> 4;} if ((bitset & 0x00000003) == 0x00000003) {n += 2; bitset = bitset >> 2;} return n + (bitset & 1); }
// #define ZEND_MM_ALIGNED_SIZE_EX(size, alignment) \ (((size) + ((alignment) - Z_L(1))) & ~((alignment) - Z_L(1))) //會將size擴展爲2M字節的整數倍;直接調用分配chunk的函數申請內存 //huge內存以n*2M字節對齊的 static void *zend_mm_alloc_huge(zend_mm_heap *heap, size_t size) { size_t new_size = ZEND_MM_ALIGNED_SIZE_EX(size, MAX(REAL_PAGE_SIZE, ZEND_MM_CHUNK_SIZE)); void *ptr = zend_mm_chunk_alloc(heap, new_size, ZEND_MM_CHUNK_SIZE); return ptr; }
ZEND_API void ZEND_FASTCALL _efree(void *ptr) { zend_mm_free_heap(AG(mm_heap), ptr); } static zend_always_inline void zend_mm_free_heap(zend_mm_heap *heap, void *ptr) { //計算當前地址ptr相對於chunk的偏移 size_t page_offset = ZEND_MM_ALIGNED_OFFSET(ptr, ZEND_MM_CHUNK_SIZE); //偏移爲0,說明是huge內存,直接釋放 if (UNEXPECTED(page_offset == 0)) { if (ptr != NULL) { zend_mm_free_huge(heap, ptr); } } else { //計算chunk首地址 zend_mm_chunk *chunk = (zend_mm_chunk*)ZEND_MM_ALIGNED_BASE(ptr, ZEND_MM_CHUNK_SIZE); //計算頁號 int page_num = (int)(page_offset / ZEND_MM_PAGE_SIZE); //得到頁屬性信息 zend_mm_page_info info = chunk->map[page_num]; //small內存 if (EXPECTED(info & ZEND_MM_IS_SRUN)) { zend_mm_free_small(heap, ptr, ZEND_MM_SRUN_BIN_NUM(info)); } //large內存 else /* if (info & ZEND_MM_IS_LRUN) */ { int pages_count = ZEND_MM_LRUN_PAGES(info); //將頁標記爲空閒 zend_mm_free_large(heap, chunk, page_num, pages_count); } } } static zend_always_inline void zend_mm_free_small(zend_mm_heap *heap, void *ptr, int bin_num) { zend_mm_free_slot *p; //插入空閒鏈表頭部便可 p = (zend_mm_free_slot*)ptr; p->next_free_slot = heap->free_slot[bin_num]; heap->free_slot[bin_num] = p; }
PHP有一個全局惟一的zend_mm_heap,其是zend_mm_chunk一個字段;
zend_mm_chunk至少須要空間2k+;和zend_mm_chunk存儲在哪裏?
這兩個結構體實際上是存儲在chunk的第一個頁,即chunk的第一個頁始終是分配的,且用戶不能申請的;
申請的多個chunk之間是造成雙向鏈表的;以下圖所示:
static zend_mm_heap *zend_mm_init(void) { //將分配的2M空間,強制轉換爲zend_mm_chunk*;並初始化zend_mm_chunk結構體 zend_mm_chunk *chunk = (zend_mm_chunk*)zend_mm_chunk_alloc_int(ZEND_MM_CHUNK_SIZE, ZEND_MM_CHUNK_SIZE); zend_mm_heap *heap; heap = &chunk->heap_slot; chunk->heap = heap; chunk->next = chunk; chunk->prev = chunk; chunk->free_pages = ZEND_MM_PAGES - ZEND_MM_FIRST_PAGE; chunk->free_tail = ZEND_MM_FIRST_PAGE; chunk->num = 0; chunk->free_map[0] = (Z_L(1) << ZEND_MM_FIRST_PAGE) - 1; chunk->map[0] = ZEND_MM_LRUN(ZEND_MM_FIRST_PAGE); heap->main_chunk = chunk; heap->cached_chunks = NULL; heap->chunks_count = 1; heap->peak_chunks_count = 1; heap->cached_chunks_count = 0; heap->avg_chunks_count = 1.0; heap->last_chunks_delete_boundary = 0; heap->last_chunks_delete_count = 0; heap->huge_list = NULL; return heap; }
PHP虛擬機何時初始化內管理器呢?heap與chunk又是何時初始化呢?
下圖爲PHP內存管理器初始化流程;
有興趣同窗能夠在相關函數處加斷點,跟蹤內存管理器初始化流程;
1)須要明白一點:任何內存分配器都須要額外的數據結構來記錄內存的分配狀況;
2)內存池是代替直接調用malloc/free、new/delete進行內存管理的經常使用方法;內存池中空閒內存塊組織爲鏈表結果,申請內存只須要查找空閒鏈表便可,釋放內存須要將內存塊從新插入空閒鏈表;
3)PHP採用預分配內存策略,提早向操做系統分配2M字節大小內存,稱爲chunk;同時將內存分配請求根據字節大小分爲small、huge、large三種;
4)small內存,採用「分離存儲」思想;將空閒內存塊按照字節大小組織爲多個空閒鏈表;
5)large內存每次回分配連續若干個頁,採用最佳適配算法;
6)huge內存直接使用mmap函數向操做系統申請內存(申請大小是2M字節整數倍);
7)chunk中的每一個頁只會被切割爲相同規格的內存塊;因此不須要再每一個內存塊添加頭部,只須要記錄每一個頁的屬性便可;
8)如何方便根據地址計算當前內存塊屬於chunk中的哪個頁?PHP分配的chunk都是2M字節對齊的,任意地址的低21位便是相對chunk首地址,除以頁大小則可得到頁號;
本文首先簡單介紹了計算機操做系統內存相關知識,而後描述了malloc內存分配器設計思路,以及內存池的簡單理論;最後從源碼層面詳細分析了PHP內存管理器的實現;相信經過這篇文章,你們對內存管理頁有了必定的瞭解;
對於PHP源碼有興趣的同窗,歡迎加入咱們的微信羣,咱們能夠一塊兒探討與學習;
同時歡迎關注微博: