0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

淺析物理內(nèi)存與虛擬內(nèi)存的關(guān)系及其管理機制

Linux愛好者 ? 來源:程序員不是碼農(nóng) ? 作者:程序員不是碼農(nóng) ? 2021-04-12 09:55 ? 次閱讀

本文主要介紹內(nèi)存管理機制:物理內(nèi)存與虛擬內(nèi)存的關(guān)系,Linux內(nèi)存管理機制,Python內(nèi)存管理機制,Nginx內(nèi)存管理機制,環(huán)形緩沖區(qū)機制,以及TC-malloc內(nèi)存分配器的Andriod管理機制的簡單介紹。

一。 物理內(nèi)存與虛擬內(nèi)存

眾所周知,程序需要加載到物理內(nèi)存才能運行,多核時代會出現(xiàn)多個進程同時操作同一物理地址的情況,進而造成混亂和程序崩潰。計算機當(dāng)中很多問題的解決都是通過引入中間層,為解決物理內(nèi)存使用問題,虛擬內(nèi)存作為中間層進入了操作系統(tǒng),從此,程序不在直接操作物理內(nèi)存,只能看到虛擬內(nèi)存,通過虛擬內(nèi)存,非常優(yōu)雅的將進程環(huán)境隔離開來,每個進程都擁有自己獨立的虛擬地址空間,且所有進程地址空間范圍完全一致,也給編程帶來了很大的便利,同時也提高了物理內(nèi)存的使用率,可同時運行更多的進程。

物理內(nèi)存和虛擬內(nèi)存之間的關(guān)系

虛擬內(nèi)存以頁為單位進行劃分,每個頁對應(yīng)物理內(nèi)存上的頁框(通常大小為4KB),內(nèi)存管理單元(MMU)負責(zé)將虛擬地址轉(zhuǎn)換為物理地址,MMU中有一張頁表來存儲這些映射關(guān)系。

并非虛擬內(nèi)存中所有的頁都會分配對應(yīng)的物理內(nèi)存,為充分利用物理內(nèi)存,保證盡可能多的進程運行在操作系統(tǒng)上,因此需要提高物理內(nèi)存利用率,對于很少用到的虛擬內(nèi)存頁不分配對應(yīng)的物理內(nèi)存,只有用到的頁分配物理內(nèi)存。雖然從程序角度來看,虛擬內(nèi)存為連續(xù)地址空間,但其實,它被分隔成多個物理內(nèi)存碎片,甚至還有部分?jǐn)?shù)據(jù)并不在內(nèi)存中,而是在磁盤上。

當(dāng)訪問虛擬內(nèi)存時,通過MMU尋找與之對應(yīng)的物理內(nèi)存,如果沒有找到,操作系統(tǒng)會觸發(fā)缺頁中斷,從磁盤中取得所缺的頁并將其換入物理內(nèi)存,并在頁表中建立虛擬頁與物理頁的映射關(guān)系。

如果物理內(nèi)存滿了,操作系統(tǒng)會根據(jù)某種頁面置換算法(比如LRU算法),將物理內(nèi)存對應(yīng)的頁換出到磁盤,如果被換出的物理內(nèi)存被修改過,則必須將其寫回磁盤以更新對應(yīng)的副本。

當(dāng)進程創(chuàng)建時,內(nèi)核為進程分配4G虛擬內(nèi)存,此時,僅僅只是建立一個映射關(guān)系,程序的數(shù)據(jù)和代碼都還在磁盤中,只有當(dāng)運行時才換回物理內(nèi)存。并且,通過malloc來分配動態(tài)內(nèi)存時,也只分配了虛擬內(nèi)存,并不會直接給物理內(nèi)存,因此,理論上來說malloc可分配的內(nèi)存大小應(yīng)該是無限制的(實際當(dāng)然會有很多算法進行限制)。

多進程使用同一物理內(nèi)存圖如下:

34cbac64-9b2f-11eb-8b86-12bb97331649.png

物理內(nèi)存與虛擬內(nèi)存關(guān)系

二。 Linux內(nèi)存管理機制進程地址空間

進程地址空間分為內(nèi)核空間(3G到4G)和用戶空間(0到3G),如下圖。

34e647c2-9b2f-11eb-8b86-12bb97331649.png

進程內(nèi)存地址空間

內(nèi)核通過brk和mmap來分配(虛擬)內(nèi)存,malloc/free底層實現(xiàn)即為brk, mmap和unmmap

當(dāng)malloc內(nèi)存小于128k時采用brk,其通過將數(shù)據(jù)段(.data)的地址指針_edata往高地址推來分配內(nèi)存,brk分配的內(nèi)存需要高地址內(nèi)存全部釋放后才會釋放,當(dāng)最高地址空間空閑內(nèi)存大于128K時,執(zhí)行內(nèi)存緊縮操作。

當(dāng)malloc內(nèi)存大于128K時采用mmap,其在堆棧中間的文件映射區(qū)域(Memory Mapping Segment)找空閑虛擬內(nèi)存,mmap分配的內(nèi)存可單獨釋放。

每個進程都對應(yīng)一個mm_struct結(jié)構(gòu)體,即唯一的進程地址空間

// include/linux/mm.h

struct vm_area_struct {

struct mm_struct * vm_mm;

};

// include/linux/sched.h

struct mm_struct {

struct vm_area_struct *mmap; // vma鏈表結(jié)構(gòu)

struct rb_root mm_rb; // 紅黑樹指針

struct vm_area_struct *mmap_cache; // 指向最近找到的虛擬區(qū)間

atomic_t mm_users; // 正在使用該地址的進程數(shù)

atomic_t mm_count; // 引用計數(shù),為0時銷毀

struct list_head mmlist; // 所有mm_struct結(jié)構(gòu)體都通過mmlist連接在一個雙向鏈表中

};

linux內(nèi)核用struct page結(jié)構(gòu)體表示物理頁:

// include/linux/mm.h

struct page {

unsigned long flags; // 頁標(biāo)識符

atomic_t count; // 頁引用計數(shù)

struct list_head list; // 頁鏈表

struct address_space *mapping; // 所屬的inode

unsigned long index; // mapping中的偏移

struct list_head lru; // LRU最近最久未使用, struct slab結(jié)構(gòu)指針鏈表頭變量

void *virtual; // 頁虛擬地址

}

內(nèi)存碎片與外存碎片

內(nèi)存碎片

產(chǎn)生原因:分配的內(nèi)存空間大于請求所需的內(nèi)存空間,造成內(nèi)存碎片

解決辦法:伙伴算法,主要包括內(nèi)存分配和釋放兩步:

內(nèi)存分配:需滿足兩個條件,1) 大于請求所需內(nèi)存;2)為最小內(nèi)存塊(如64K為一頁)的倍數(shù)。比如,最小內(nèi)存塊為64K,若分配100K內(nèi)存,則應(yīng)分配64*2=128K內(nèi)存大小。

內(nèi)存釋放:包含兩步,1)釋放內(nèi)存;2)檢查是否可與相鄰塊合并,直到?jīng)]有可合并內(nèi)存塊。

接下來通過一張圖來詳細說明伙伴算法原理(From wiki),如下:

3512638e-9b2f-11eb-8b86-12bb97331649.png

伙伴算法圖解

Step步驟詳解(注意最左側(cè)Step為步驟,ABCD申請者對應(yīng)不同的顏色):

初始化內(nèi)存,最小內(nèi)存塊為64K,分配1024KB(只截取部分進行說明)

A申請34K內(nèi)存,因此需64K內(nèi)存塊,步驟2.1 2.2 2.3 2.4都為對半操作,步驟2.5找到滿足條件的塊(64K),分配給A

B申請66K內(nèi)存,因此需要128K內(nèi)存塊,有現(xiàn)成的直接分配

C申請35K內(nèi)存,需64K內(nèi)存塊,直接分配

D申請67K內(nèi)存,需128K內(nèi)存塊,步驟5.1對半操作,步驟5.2分配

釋放B內(nèi)存塊,沒有相鄰內(nèi)存可合并

釋放D內(nèi)存塊,步驟7.1釋放內(nèi)存,步驟7.2 與相鄰塊進行內(nèi)存合并

A釋放內(nèi)存,不許合并內(nèi)存

C釋放內(nèi)存,步驟9.1釋放內(nèi)存,步驟9.2-9.5進行合并,整塊內(nèi)存恢復(fù)如初

以上為伙伴算法原理,Linux關(guān)鍵代碼在mm/page_alloc.c中,有興趣讀者可在內(nèi)核源碼中閱讀細節(jié),如下:

//mm/page_alloc.c

// 塊分配, removing an element from the buddy allocator

// 再zone中找到一個空閑塊,order(0:單頁,1:雙頁,2:4頁 2 ^ order)

static struct page * __rmqueue(struct zone *zone, unsigned int order)

{

}

// 塊釋放,處理合并邏輯

static int

free_pages_bulk(struct zone *zone, int count, struct list_head *list, unsigned int order) {

}

這里簡單介紹云風(fēng)實現(xiàn)的伙伴算法,實現(xiàn)思路:用數(shù)組實現(xiàn)完全二叉樹來管理內(nèi)存,樹節(jié)點標(biāo)記使用狀態(tài),在分配和釋放中通過節(jié)點的狀態(tài)來進行內(nèi)存塊的分離與合并,如下:

// 數(shù)組實現(xiàn)二叉樹

struct buddy {

int level; // 二叉樹深度

uint8_tree[1]; // 記錄二叉樹用來存儲內(nèi)存塊(節(jié)點)使用情況,柔性數(shù)組,不占內(nèi)存

};

// 分配大小為s的內(nèi)存

int

buddy_alloc(struct buddy * self, int s) {

// 分配大小s的內(nèi)存,返回分配內(nèi)存偏移量地址(首地址)

int size;

if (s == 0) {

size = 1;

} else {

// 獲取大于s的最小2次冪

size = (int)next_pow_of_2(s);

}

int length = 1 《《 self-》level;

if(size 》 length)

return -1;

int index = 0;

int level = 0;

while (index 》= 0) {

//具體分配細節(jié)。..

}

return -1;

}

// 釋放內(nèi)存并嘗試合并

void

buddy_free(struct buddy * self, int offset) {

// 釋放偏移量offset開始的內(nèi)存塊

int left = 0;

int length = 1 《《 self-》level;

int index;

for (;;) {

switch(self-》tree[index]) {

case NODE_USED:

_combine(self, index); // 嘗試合并

return;

case NODE_UNUSED:

return;

default:

// 。..

}

}

}

外存碎片

產(chǎn)生原因:未被分配的內(nèi)存,出現(xiàn)大量零碎不連續(xù)小內(nèi)存,無法滿足較大內(nèi)存申請,造成外部碎片

解決辦法:采用slab分配器,處理小內(nèi)存分配問題,slab分配器分配內(nèi)存以字節(jié)為單位,基于伙伴系統(tǒng)分配的大內(nèi)存進一步細分成小內(nèi)存分配

slab分三種:slabs_full(完全分配的slab),slabs_partial(部分分配的slab),slabs_empty(空slab),一個slab分配滿了之后就從slabs_partial刪除,同時插入到slab_fulls中。

slab兩個作用:1)小對象分配,不必每個小對象分配一個頁,節(jié)省空間;2)內(nèi)核中一些小對象創(chuàng)建析構(gòu)頻繁,slab對小對象緩存,可重復(fù)利用一些相同對象,減少內(nèi)存分配次數(shù)。(應(yīng)用于內(nèi)核對象的緩存)。

slab分配器基于對象(內(nèi)核中數(shù)據(jù)結(jié)構(gòu))進行管理,相同類型對象歸為一類,每當(dāng)申請這樣一個對象,slab分配器就從一個slab列表中分配一個這樣大小的單元,當(dāng)釋放時,將其重新保存到原列表中,而不是直接返還給伙伴系統(tǒng),避免內(nèi)存碎片。slab分配對象時,會使用最近釋放的對象的內(nèi)存塊,因此其駐留在cpu高速緩存中的概率會大大提高

3523d970-9b2f-11eb-8b86-12bb97331649.png

Slab分配器

三。 Python內(nèi)存管理機制內(nèi)存管理層次結(jié)構(gòu)

35634380-9b2f-11eb-8b86-12bb97331649.png

Python內(nèi)存層次結(jié)構(gòu)

Layer 0:操作系統(tǒng)提供的內(nèi)存管理接口,比如malloc,free,python不能干涉這一層

Layer 1:封裝malloc,free等接口PyMem_API,提供統(tǒng)一的raw memory管理接口,為了可移植性。

Layer 2:構(gòu)建了更高抽象層次的內(nèi)存管理策略(GC藏身之處)

Layer 3:對象緩沖池

// 第1層 PyMem_Malloc通過一個宏P(guān)yMem_MALLOC實現(xiàn)

// pymem.h

PyAPI_FUNC(void *) PyMem_Malloc(size_t);

PyAPI_FUNC(void *) PyMem_Realloc(size_t);

PyAPI_FUNC(void *) PyMem_Free(size_t);

#define PyMem_MALLOC(n) ((size_t)(n) 》 (size_t)PY_SSIZE_T_MAX ? NULL

: malloc(((n) != 0) ? (n) : 1))

#define PyMem_MALLOC(n) ((size_t)(n) 》 (size_t)PY_SSIZE_T_MAX ? NULL

: realloc(((n) != 0) ? (n) : 1))

#define PyMem_FREE free

// Type-oriented memory interface 指定類型

#define PyMem_New(type, n)

( ((size_t)(n) 》 PY_SSIZE_T_MAX / sizeof(type)) ? NULL :

( (type*)PyMem_Malloc((n) * sizeof(type))) ) )

#define PyMem_NEW(type, n)

( ((size_t)(n) 》 PY_SSIZE_T_MAX / sizeof(type)) ? NULL :

( (type*)PyMem_MALLOC((n) * sizeof(type))) ) )

小塊空間的內(nèi)存池

Python內(nèi)存池可視為一個層次結(jié)構(gòu),自下而上分為四層:block,pool,arena和內(nèi)存池(概念),其中bock, pool, arena在python中都能找到實體,而內(nèi)存池是由所有這些組織起來的一個概念。

Python針對小對象(小于256字節(jié))的內(nèi)存分配采用內(nèi)存池來進行管理,大對象直接使用標(biāo)準(zhǔn)C的內(nèi)存分配器malloc。

對小對象內(nèi)存的分配器Python進行了3個等級的抽象,從上至下依次為:Arena,Pool和Block。即,Pool由Block組成,Arena由Pool組成。

Block

block內(nèi)存大小值被稱為size class, 大小為:[8, 16, 24, 32, 40, 48 。.. 256],(8*n),內(nèi)存管理器的最小單元,一個Block存儲一個Python對象。

// obmalloc.c

// 8字節(jié)對齊

#define ALIGNMENT 8

#define ALIGNMENT_SHIFT 3

#define ALIGNMENT_MASK (ALIGNMENT - 1)

// block大小上限為256,超過256KB,則交由第一層的內(nèi)存管理機制

#define SMALL_REQUEST_THRESHOLD 256

#define NB_SMALL_SIZZE_CLASSES (SMALL_REQUEST_THREASHOLD / ALIGNMENT)

// size class index 轉(zhuǎn)換到 size class

#define INDEX2SIZE(I) (((unit) (I)) + 1) 《《 ALIGMENT_SHIFT)

// sizes class 轉(zhuǎn)換到size class index

size = (uint )(nbytes - 1) 》》 ALIGMENT_SHIFT;

小于256KB的小塊內(nèi)存分配如下圖。

3671ac80-9b2f-11eb-8b86-12bb97331649.png

Block分配策略

如果申請內(nèi)存大小為28字節(jié),則PyObject_Malloc從內(nèi)存池中分配32字節(jié)的block,size class index為3的pool(參考上圖)。

Pool

Pool為一個雙向鏈表結(jié)構(gòu),一系列Block組成一個Pool,一個Pool中所有Block大小一樣;一個Pool大小通常為4K(一個虛擬/系統(tǒng)內(nèi)存頁的大?。?/p>

一個小對象被銷毀后,其內(nèi)存不會馬上歸還系統(tǒng),而是在Pool中被管理著,用于分配給后面申請的內(nèi)存對象。Pool的三種狀態(tài)

used狀態(tài):Pool中至少有一個Block已被使用,且至少還有一個Block未被使用,存在usedpools數(shù)組中。

full狀態(tài):Pool中所有的block都已經(jīng)被使用,這種狀態(tài)的Pool在Arena中,但不再Arena的freepools鏈表中

empty狀態(tài):Pool中所有的Block都未被使用,處于這個狀態(tài)的Pool的集合通過其pool_head中的nextpool構(gòu)成一個鏈表,表頭為arena_object中的freepools

// obmalloc.c

#define SYSTEM_PAGE_SIZE (4 * 1024)

#define SYSTEM_PAGE_SIZE_MASK (SYSTEM_PAGE_SIZE - 1)

// 一個pool大小

#define POOL_SIZE SYSTEM_PAGE_SIZE

#define POOL_SIZE_MASK SYSTEM_PAGE_SIZE_MASK

/*pool for small blocks*/

struct pool_header {

union {

block *_padding;

uint count; }ref; // 分配的block數(shù)量

block *freeblock; // 指向pool中可用block的單向鏈表

struct pool_header *nextpool; // 指向下一個

struct pool_header *prevpool; // 指向上一個

uint arenaindex;

// 記錄pool保存的block的大小,一個pool中所有block都是szidx大小

// 和size class index聯(lián)系在一起

uint szidx;

uint nextoffset;

uint maxnextoffset;

};

typedef struct pool_header *poolp;

擁有相同block大小的pool通過雙向鏈表連接起來,python使用一個數(shù)組usedpools來管理使用中的pool

36ca69a6-9b2f-11eb-8b86-12bb97331649.png

Userpools結(jié)構(gòu)

以下為Python內(nèi)存分配部分代碼:

// obmalloc.c

typedef uchar block;

void *

PyObject_Malloc(sizes_t nbytes)

{

block *bp; // 指向從pool中取出第一塊block的指針

poolp pool; // 指向一塊4kb內(nèi)存

poolp next;

uint size;

// 小于SMALL_REQUEST_THRESHOLD 使用Python的小塊內(nèi)存的內(nèi)存池,否則走malloc

if ((nbytes - 1) 《 SMALL_REQUEST_THRESHOLD) {

// 根據(jù)申請內(nèi)存的大小獲得對應(yīng)的獲得size class index, 從usedpools中取pool

size = (uint)(nbytes - 1) 》》 ALIGNMENT_SHIFT;

pool = usedpools[size + size];

// 如果usedpools中有可用pool, 使用這個pool來分配block$

if (pool != pool-》nextpool) {

。..

}

}

}

Arena

Arena是Python直接從操作系統(tǒng)分配和申請內(nèi)存的單位,一個Arena為256KB,每個Arena包含64個Pool,Arena管理的內(nèi)存是離散的,Pool管理的內(nèi)存是連續(xù)的。同Pool,Arena也是一個雙向鏈表結(jié)構(gòu)。

3744b2d8-9b2f-11eb-8b86-12bb97331649.png

Arena結(jié)構(gòu)

Python在分配Pool的時候優(yōu)先選擇可用Pool數(shù)量少的Arena進行內(nèi)存分配,這樣做的目的是為了讓Pool更為集中,避免Arena占用大量空閑內(nèi)存空間,因為Python只有在Arena中所有的Pool全為空時才會釋放Arena中的內(nèi)存。

Python中會同時存在多個Arena,由Arenas數(shù)組統(tǒng)一管理。

// obmalloc.c

#define ARENA_SIZE (256 《《 10) // 256kb

// arena包含arena_object及其管理的pool集合,就如同pool和pool_header一樣

struct arena_object {

uintptr_t address; // arena地址

block* pool_address; // 下一個pool地址

uint nfreepools;

uint ntotalpools;

struct pool_header* freepools; // 可用pool通過單鏈表連接

struct arena_object* nextarena;

struct arena_object* prearena;

};

// arenas管理著arena_object的集合

static struct arena_object* arenas = NULL;

// 未使用的arena_object鏈表

static struct arena_object * unused_arena_objects = NULL;

// 可用的arena_object鏈表

static struct arena_object * usable_arenas = NULL;

static struct arena_object * nwe_arena(void)

{

struct arena_object * arenaobj;

uint excess;

// 判斷是否需要擴充“未使用的”arena_object列表

if (unused_arena_objects == NULL) {

// 確定本次需要申請的arena_object的個數(shù),并申請內(nèi)存

numarenas = maxarenas ? maxarenas 《《 1 : INITIAL_ARENA_OBJECTS;

。..

}

// 從unused_arena_objects中取出一個未使用的arena_object

arenaobj = unused_arena_objects;

unused_arena_objects = arenaobj-》nextarena;

// 建立arena_object和pool的聯(lián)系

arenaobj-》address = (uptr)address;

。..

return arenaobj;

}

內(nèi)存池全景圖

375cb0cc-9b2f-11eb-8b86-12bb97331649.png

內(nèi)存池全景圖

四。 Nginx內(nèi)存管理機制

在介紹Nginx內(nèi)存管理之前,先參照Nginx實現(xiàn)一個簡單的內(nèi)存池,結(jié)構(gòu)圖如下:

3779025e-9b2f-11eb-8b86-12bb97331649.png

其中,mp_pool_s為內(nèi)存池的結(jié)構(gòu)體頭,包含內(nèi)存池的一些全局信息,block為小塊內(nèi)存塊,每一個block有一個mp_node_s結(jié)構(gòu)體,也即mp_pool_s通過鏈表將所有的block連接起來進行管理,而大塊內(nèi)存由mp_large_s進行分配。申明的數(shù)據(jù)結(jié)構(gòu)如下:

// 結(jié)構(gòu)體

// 大塊內(nèi)存結(jié)構(gòu)體

struct mp_large_s {

struct mp_large_s *next;

void *alloc;

};

// 小塊內(nèi)存節(jié)點,小塊內(nèi)存構(gòu)成一個鏈表

struct mp_node_s {

unsigned char *last; // 下一次內(nèi)存從此分配

unsigned char *end; // 內(nèi)存池結(jié)束位置

struct mp_node_s *next; // 指向下一個內(nèi)存塊

size_t failed; // 改內(nèi)存塊/node分配失敗的次數(shù)

};

// 內(nèi)存池結(jié)構(gòu)

struct mp_pool_s {

size_t max; // 能直接從內(nèi)存池中申請的最大內(nèi)存,超過需要走大塊內(nèi)存申請邏輯

struct mp_node_s *current; // 當(dāng)前分配的node

struct mp_large_s *large; // 大塊內(nèi)存結(jié)構(gòu)體

struct mp_node_s head[0]; // 柔性數(shù)組不占用大小,其地址為緊挨著結(jié)構(gòu)體的第一個node

};

// 需要實現(xiàn)的接口

struct mp_pool_s *mp_create_pool(size_t size); // 創(chuàng)建內(nèi)存池

void mp_destory_pool(struct mp_pool_s *pool); // 銷毀內(nèi)存池

void *mp_alloc(struct mp_pool_s *pool, size_t size); // 分配內(nèi)存 對齊

void mp_free(struct mp_pool_s *pool, void *p); // 釋放p節(jié)點內(nèi)存

接下來介紹接口實現(xiàn),先介紹一個接口函數(shù)posix_memalign,函數(shù)原型如下:

int posix_memalign(void**memptr, size_t alignment, size_t size);

/* memptr: 分配好的內(nèi)存空間的首地址

alignment: 對齊邊界,Linux中32位系統(tǒng)8字節(jié),64位系統(tǒng)16字節(jié),必須為2的冪

size: 指定分配size字節(jié)大小的內(nèi)存

*/

其功能類似malloc,不過其申請的內(nèi)存都是對齊的。

內(nèi)存池相關(guān)接口實現(xiàn)如下(只貼出部分代碼,完整代碼私信我)

// 創(chuàng)建并初始化內(nèi)存池

struct mp_pool_s *mp_create_pool(size_t size) {

struct mp_pool_s *p;

// 分配內(nèi)存池內(nèi)存:mp_pool_s + mp_node_s + size

int ret = posix_memalign((void**)&p), MP_ALIGNMENT, size + sizeof(struct mp_pool_s) + sizeof(struct mp_node_s));

if (ret) { return NULL; }

// 可從內(nèi)存池申請的最大內(nèi)存

p-》max = (size 《 MP_MAX_ALLOC_FROM_POOL) ? size : MP_MAX_ALLOC_FROM_POOL;

p-》current = p-》head; // 當(dāng)前可分配的第一個節(jié)點mp_node_s

//一些初始化工作

return p;

}

// 銷毀內(nèi)存池

void mp_destroy_pool(struct mp_pool_s *pool) {

struct mp_node_s *h, *n;

struct mp_large_s *l;

// 銷毀大塊內(nèi)存

for (l = pool-》large; l; l = l-》next) { /*.。.*/}

// 銷毀小塊內(nèi)存

h = pool-》head-》next;

while (h) {/*.。.*/}

free(pool);

}

// mp_alloc 分配內(nèi)存

void *mp_alloc(struct mp_pool_s *pool, size_t size) {

if (size 《= pool-》max) { // 小塊內(nèi)存分配

p = pool-》current;

do {

/*.。.不斷尋找下一個可用節(jié)點*/

p = p-》next; // 不夠則找下一個節(jié)點

} while (p);

// 內(nèi)存池中所有節(jié)點內(nèi)存都不以滿足分配size內(nèi)存,需要再次分配一個block

return mp_alloc_block(pool, size);

}

return mp_alloc_large(pool, size); // 大塊內(nèi)存分配

}

// 大塊節(jié)點內(nèi)存釋放

void mp_free(struct mp_pool_s *pool, void *p) {

struct mp_large_s *l;

for (l = pool-》large; l; l = l-》next) {

if (p == l-》alloc) {

free(l-》alloc);

//。..

}

}

}

有了上面簡化版,接下來看Nginx中內(nèi)存管理就比較清晰的,其原理跟上述內(nèi)存池一致,先上一張圖:

38088afa-9b2f-11eb-8b86-12bb97331649.png

Nginx內(nèi)存池結(jié)構(gòu)

以下為Nginx實現(xiàn),源代碼主要在src/core/ngx_palloc.h/c兩個文件中

// 內(nèi)存塊結(jié)構(gòu)體,每個內(nèi)存塊都有,在最開頭的部分,管理本塊內(nèi)存

typedef struct {

u_char *last; // 可用內(nèi)存的起始位置,小塊內(nèi)存每次都從這里分配

u_char *end; // 可用內(nèi)存的結(jié)束位置

ngx_pool_t *next; // 寫一個內(nèi)存池節(jié)點

ngx_unit_t failed; // 本節(jié)點分配失敗次數(shù),超過4次,認(rèn)為本節(jié)點滿,不參與分配,滿的內(nèi)存塊也不會主動回收

}ngx_pool_data_t;

// 大塊內(nèi)存節(jié)點

typedef struct ngx_pool_large_s ngx_pool_large_t;

struct ngx_pool_large_s {

ngx_pool_large_t *next; // 多塊大內(nèi)存串成鏈表,方便回收利用

void *alloc; // 指向malloc分配的大塊內(nèi)存

};

// nginx內(nèi)存池結(jié)構(gòu)體

// 多個節(jié)點串成的單向鏈表,每個節(jié)點分配小塊內(nèi)存

// max,current,大塊內(nèi)存鏈表旨在頭節(jié)點

// 64位系統(tǒng)大小位80字節(jié),結(jié)構(gòu)體沒有保存內(nèi)存塊大小的字段,由d.end - p得到

struct ngx_pool_s {

// 本內(nèi)存節(jié)點信息

ngx_pool_data_t d;

// 下面的字段旨在第一個塊中有意義

size_t max; // 塊最大內(nèi)存

ngx_pool_t *current; // 當(dāng)前使用的內(nèi)存池節(jié)點

ngx_chain_t *chain;

ngx_pool_large_t *large; // 大塊內(nèi)存

ngx_pool_cleanup_t *cleanup; // 清理鏈表頭指針

ngx_log_t *log;

};

// 創(chuàng)建內(nèi)存池

ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log);

// 銷毀內(nèi)存池

// 調(diào)用清理函數(shù)鏈表,檢查大塊內(nèi)存鏈表,直接free,遍歷內(nèi)存池節(jié)點,逐個free

void ngx_destroy_pool(ngx_pool_t *pool);

// 重置內(nèi)存池,釋放內(nèi)存,但不歸還系統(tǒng)

// 之前分配的內(nèi)存塊依舊保留,重置空閑指針位置

void ngx_reset_pool(ngx_pool_t *pool);

// 分配內(nèi)存 8字節(jié)對齊,速度快,少量浪費 》4k則直接malloc分配大塊內(nèi)存

void *ngx_palloc(ngx_pool_t *pool, size_t size);

void *ngx_pnalloc(ngx_pool_t *pool, size_t size); // 不對齊

void *ngx_pcalloc(ngx_pool_t *pool, size_t size); // 對齊分配,且初始化

// 大塊內(nèi)存free

ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);

五。 Ringbuffer環(huán)形緩沖區(qū)機制Ringbuffer的兩個特性:1)先進先出;2)緩沖區(qū)用完,會回卷,丟棄久遠數(shù)據(jù),保存新數(shù)據(jù)。其結(jié)構(gòu)如下圖:

381e728e-9b2f-11eb-8b86-12bb97331649.png

Ringbuffer結(jié)構(gòu)

Ringbuffer的好處:1)減少內(nèi)存分配進而減少系統(tǒng)調(diào)用開銷;2)減少內(nèi)存碎片,利于程序長期穩(wěn)定運行。

應(yīng)用場景:服務(wù)端程序收到多個客戶端網(wǎng)絡(luò)數(shù)據(jù)流時,可先暫存在Ringbuffer,等收到一個完整數(shù)據(jù)包時再讀取。

Linux 5.1合入了一個新的異步IO框架和實現(xiàn):io_uring, io_uring設(shè)計了一對共享的RingBuffer用于應(yīng)用和內(nèi)核之間的通信,其中,針對提交隊列(SQ),應(yīng)用是IO提交的生產(chǎn)者(producer),內(nèi)核是消費者(consumer);反過來,針對完成隊列(CQ),內(nèi)核是完成事件的生產(chǎn)者,應(yīng)用是消費者。

以下為一份簡單Ringbuffer實現(xiàn):

// ringbuffer.c

#define BUFFER_SIZE 16 // 緩沖區(qū)的長度

static u32 validLen; // 已使用的數(shù)據(jù)長度

static u8* pHead = NULL; // 環(huán)形存儲區(qū)的首地址

static u8* pTail = NULL; // 環(huán)形存儲區(qū)的尾地址

static u8* pValid = NULL; // 已使用的緩沖區(qū)首地址

static u8* pValidTail = NULL; // 已使用的緩沖區(qū)尾地址

// 初始化環(huán)形緩沖區(qū)

void init Ringbuffer(void) {

if (pHead == NULL) pHead = (u8*)malloc(BUFFER_SIZE);

pValid = pValidTail = pHead;

pTail = pHead + BUFFER_SIZE;

validLen = 0;

}

// 向緩沖區(qū)寫入數(shù)據(jù),buffer寫入數(shù)據(jù)指針,addLen寫入數(shù)據(jù)長度

int writeRingbuffer(u8* buffer, u32 addLen) {

// 將數(shù)據(jù)copy到pValidTail處

if (pValidTail + addLen 》 pTail) // ringbuffer回卷

{

int len1 = addLen - pValidTail;

int len2 = addLen - len1;

memcpy(pValidTail, buffer, len1);

memcpy(pHead, buffer + len1, len2);

pValidTail = pHead + len2; // 新的有效數(shù)據(jù)區(qū)結(jié)尾指針

} else {

memcpy(pValidTail, buffer, addLen);

pValidTail += addLen; // 新的有效數(shù)據(jù)結(jié)尾指針

}

// 重新計算已使用區(qū)的起始位置

if (validLen + addLen 》 BUFFER_SIZE) {

int moveLen = validLen + addLen - BUFFER_SIZE; // 有效指針將要移動的長度

if (pValid + moveLen 》 pTail) {

int len1 = pTail - pValid;

int len2 = moveLen - len1;

pValid = pHead + len2;

} else {

pValid = pValid + moveLen;

}

validLen = BUFFER_SIZE;

}else {

validLen += addLen;

}

return 0;

}

// 從緩沖區(qū)內(nèi)取出數(shù)據(jù),buffer讀取數(shù)據(jù)的buffer,len長度

int readRingBuffer(u8* buffer, u32 len)

{

if (len 》 validLen) len = validLen;

if (pValid + len 》 pTail) { // 回卷

int len1 = pTail - pValid;

int len2 = len - len1;

memcpy(buffer, pValid, len1);

memcpy(buffer + len1, pHead, len2);

pValid = pHead + len2;

} else {

memcpy(buffer, pValid, len);

pValid = pValid + len;

}

validLen -= len;

return len;

}

六。 TCMalloc(Thread-Caching Malloc)

內(nèi)存分配器以下Tcmalloc和Andriod內(nèi)存管理這兩部分只做簡單介紹。

tcmalloc優(yōu)點:內(nèi)存分配效率高,運行速度快,穩(wěn)定性強,能夠有效降低系統(tǒng)負載;

應(yīng)用場景:多核,高并發(fā),多線程

tcmalloc內(nèi)存申請流程:

ThreadCache對象不夠,就從CentralCache中批量申請

CentralCache不夠,從PageHeap申請Span

PageHeap沒有適合的Page,則向操作系統(tǒng)申請

tcmalloc釋放流程:

ThreadCache釋放對象積累到一定程度,就釋放給CentralCache

CentralCache中一個Span釋放完全了,則把這個Span歸還給PageHeap

PageHeap發(fā)現(xiàn)一批連續(xù)的Page都釋放了,則歸還給操作系統(tǒng)

多個連續(xù)的Page組成Span, Span 中記錄起始 Page 的編號,以及 Page 數(shù)量,大對象(》32k)直接分配Span,小對象(《=32k)在Span中分配Object。以下為上述結(jié)構(gòu)圖解:

3850eb74-9b2f-11eb-8b86-12bb97331649.png

ThreadCache

?。跜entralCache](/Users/zhongrunkang/Library/Application Support/typora-user-images/image-20210228203604225.png)

38b64d48-9b2f-11eb-8b86-12bb97331649.png

PageHeap

七。 Andriod內(nèi)存管理機制

Q:Andriod的Java程序為什么容易出現(xiàn)OOM?

A:因為Andriod系統(tǒng)堆Dalvik的VM HeapSize做了硬性限制,當(dāng)java進程申請的java空間超過閾值時,就會拋出OOM,這樣設(shè)計的目的是為了讓比較多的進程常駐內(nèi)存,這樣程序啟動時就不用每次都重新加載到內(nèi)存,能夠給用戶更快的響應(yīng)。

Andriod系統(tǒng)中的應(yīng)用程序基本都是Java進程。

Andriod內(nèi)存管理機制

分配機制:

為每一個進程分配一個合理大小的內(nèi)存塊,保證每個進程能夠正常運行,同時確保進程不會占用太多的內(nèi)存;Andriod系統(tǒng)需要最大限度的讓更多進程存活在內(nèi)存中,以保證用戶再次打開應(yīng)用時減少應(yīng)用的啟動時間,提高用戶體驗。

回收機制:

當(dāng)系統(tǒng)內(nèi)存不足時,需要一個合理的回收再分配機制,以保證新的進程可以正常運行?;厥諘r殺死那些正在占用內(nèi)存的進程,OS需要提供一個合理的殺死進程機制。
編輯:lyn

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • Linux
    +關(guān)注

    關(guān)注

    87

    文章

    11304

    瀏覽量

    209518
  • 內(nèi)存管理
    +關(guān)注

    關(guān)注

    0

    文章

    168

    瀏覽量

    14141
  • python
    +關(guān)注

    關(guān)注

    56

    文章

    4797

    瀏覽量

    84690
  • nginx
    +關(guān)注

    關(guān)注

    0

    文章

    149

    瀏覽量

    12176

原文標(biāo)題:一文淺析內(nèi)存管理機制

文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    虛擬內(nèi)存和云計算的關(guān)系

    虛擬內(nèi)存是一種計算機系統(tǒng)內(nèi)存管理技術(shù),它通過將物理內(nèi)存與磁盤空間結(jié)合起來,使得應(yīng)用程序可以訪問比物理
    的頭像 發(fā)表于 12-04 09:50 ?124次閱讀

    虛擬內(nèi)存溢出該怎么處理 虛擬內(nèi)存在服務(wù)器中的應(yīng)用

    在現(xiàn)代計算機系統(tǒng)中,虛擬內(nèi)存是一種重要的資源管理技術(shù),它允許系統(tǒng)使用硬盤空間來擴展物理內(nèi)存的容量。然而,當(dāng)系統(tǒng)運行的程序和進程超出了物理
    的頭像 發(fā)表于 12-04 09:49 ?160次閱讀

    Linux下如何管理虛擬內(nèi)存 使用虛擬內(nèi)存時的常見問題

    在Linux系統(tǒng)中,虛擬內(nèi)存管理是操作系統(tǒng)內(nèi)核的一個重要功能,負責(zé)管理物理內(nèi)存和磁盤上的交換空間。以下是對Linux下如何
    的頭像 發(fā)表于 12-04 09:19 ?392次閱讀

    虛擬內(nèi)存對計算機性能的影響

    在現(xiàn)代計算機系統(tǒng)中,內(nèi)存管理是確保系統(tǒng)高效運行的關(guān)鍵因素之一。虛擬內(nèi)存技術(shù)作為內(nèi)存管理的核心組成部分,對于提升計算機性能和用戶體驗起著至關(guān)重
    的頭像 發(fā)表于 12-04 09:17 ?680次閱讀

    什么是虛擬內(nèi)存分頁 Windows系統(tǒng)虛擬內(nèi)存優(yōu)化方法

    虛擬內(nèi)存分頁概述 在Windows操作系統(tǒng)中,虛擬內(nèi)存是通過分頁機制實現(xiàn)的。分頁允許系統(tǒng)將內(nèi)存中的數(shù)據(jù)移動到硬盤上,以便為當(dāng)前運行的程序騰出空間。這個過程對于保持系統(tǒng)的流暢運行至關(guān)重要
    的頭像 發(fā)表于 12-04 09:16 ?324次閱讀

    虛擬內(nèi)存不足如何解決 虛擬內(nèi)存物理內(nèi)存的區(qū)別

    虛擬內(nèi)存不足的解決方案 虛擬內(nèi)存不足是計算機用戶經(jīng)常遇到的問題,尤其是在運行大型軟件或多任務(wù)處理時。以下是一些解決虛擬內(nèi)存不足問題的方法: 增加物理
    的頭像 發(fā)表于 12-04 09:14 ?407次閱讀

    虛擬內(nèi)存的作用和原理 如何調(diào)整虛擬內(nèi)存設(shè)置

    虛擬內(nèi)存,也稱為虛擬內(nèi)存管理或頁面文件,是計算機操作系統(tǒng)中的一種內(nèi)存管理技術(shù)。它允許系統(tǒng)使用硬盤空間作為額外的RAM(隨機存取存儲器),以彌
    的頭像 發(fā)表于 12-04 09:13 ?412次閱讀

    如何優(yōu)化RAM內(nèi)存使用

    :使用任務(wù)管理器查看當(dāng)前運行的程序和服務(wù),關(guān)閉那些不需要的。 禁用啟動程序 :減少開機啟動項,只保留必要的程序。 2. 優(yōu)化操作系統(tǒng)設(shè)置 調(diào)整虛擬內(nèi)存 :合理設(shè)置虛擬內(nèi)存,避免過多占用硬盤空間。 清理磁盤 :定期進行磁盤清理,
    的頭像 發(fā)表于 11-11 09:58 ?351次閱讀

    Windows管理內(nèi)存的三種主要方式

    Windows操作系統(tǒng)提供了多種方式來管理內(nèi)存,以確保系統(tǒng)資源的有效利用和性能的優(yōu)化。以下是關(guān)于Windows管理內(nèi)存的三種主要方式的詳細闡述,包括堆
    的頭像 發(fā)表于 10-12 17:09 ?790次閱讀

    邏輯內(nèi)存物理內(nèi)存的區(qū)別

    邏輯內(nèi)存物理內(nèi)存是計算機系統(tǒng)中兩個重要的概念,它們在計算機的運行和數(shù)據(jù)處理中起著至關(guān)重要的作用。 1. 物理內(nèi)存(Physical Mem
    的頭像 發(fā)表于 09-27 15:38 ?687次閱讀

    內(nèi)存緩沖區(qū)和內(nèi)存關(guān)系

    內(nèi)存緩沖區(qū)和內(nèi)存之間的關(guān)系是計算機體系結(jié)構(gòu)中一個至關(guān)重要的方面,它們共同協(xié)作以提高數(shù)據(jù)處理的效率和系統(tǒng)的整體性能。
    的頭像 發(fā)表于 09-10 14:38 ?584次閱讀

    深入理解Java 8內(nèi)存管理機制及故障排查實戰(zhàn)指南

    Java的自動內(nèi)存管理機制是由 JVM 中的垃圾收集器來實現(xiàn)的,垃圾收集器會定期掃描堆內(nèi)存中的對象,檢測并清除不再使用的對象,以釋放內(nèi)存資源。
    的頭像 發(fā)表于 04-04 08:10 ?1005次閱讀
    深入理解Java 8<b class='flag-5'>內(nèi)存</b><b class='flag-5'>管理機制</b>及故障排查實戰(zhàn)指南

    物理內(nèi)存模型的演變

    內(nèi)存管理概述中,主要是以Linux v2.6.11為例進行分析的,但是計算技術(shù)在不斷發(fā)展,新的存儲架構(gòu)、新的指令集架構(gòu)、新的SoC架構(gòu)等都對物理內(nèi)存模型的抽象提出了更高要求。為此,必須
    的頭像 發(fā)表于 02-25 10:35 ?473次閱讀

    Linux內(nèi)核內(nèi)存管理之內(nèi)核非連續(xù)物理內(nèi)存分配

    我們已經(jīng)知道,最好將虛擬地址映射到連續(xù)頁幀,從而更好地利用緩存并實現(xiàn)更低的平均內(nèi)存訪問時間。然而,如果對內(nèi)存區(qū)域的請求并不頻繁,那么考慮基于通過連續(xù)線性地址訪問非連續(xù)頁幀的分配方案是有意義的。該模式
    的頭像 發(fā)表于 02-23 09:44 ?972次閱讀
    Linux內(nèi)核<b class='flag-5'>內(nèi)存</b><b class='flag-5'>管理</b>之內(nèi)核非連續(xù)<b class='flag-5'>物理</b><b class='flag-5'>內(nèi)存</b>分配

    拆解mmap內(nèi)存映射的本質(zhì)!

    mmap 內(nèi)存映射里所謂的內(nèi)存其實指的是虛擬內(nèi)存,在調(diào)用 mmap 進行匿名映射的時候(比如進行堆內(nèi)存的分配),是將進程虛擬內(nèi)存空間中的某一
    的頭像 發(fā)表于 01-24 14:30 ?1748次閱讀
    拆解mmap<b class='flag-5'>內(nèi)存</b>映射的本質(zhì)!