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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// 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; // 正在使用該地址的進(jìn)程數(shù)

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

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

};

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

// include/linux/mm.h

struct page {

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

atomic_t count; // 頁引用計(jì)數(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)存空間大于請(qǐng)求所需的內(nèi)存空間,造成內(nèi)存碎片

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

內(nèi)存分配:需滿足兩個(gè)條件,1) 大于請(qǐng)求所需內(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)存塊。

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

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

伙伴算法圖解

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

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

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

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

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

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

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

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

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

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

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

//mm/page_alloc.c

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

// 再zone中找到一個(gè)空閑塊,order(0:?jiǎn)雾摚?:雙頁,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) {

}

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

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

struct buddy {

int level; // 二叉樹深度

uint8_tree[1]; // 記錄二叉樹用來存儲(chǔ)內(nèi)存塊(節(jié)點(diǎn))使用情況,柔性數(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) {

//具體分配細(xì)節(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)存申請(qǐng),造成外部碎片

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

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

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

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

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

Slab分配器

三。 Python內(nèi)存管理機(jī)制內(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:對(duì)象緩沖池

// 第1層 PyMem_Malloc通過一個(gè)宏P(guān)yMem_MALLOC實(shí)現(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)存池可視為一個(gè)層次結(jié)構(gòu),自下而上分為四層:block,pool,arena和內(nèi)存池(概念),其中bock, pool, arena在python中都能找到實(shí)體,而內(nèi)存池是由所有這些組織起來的一個(gè)概念。

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

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

Block

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

// obmalloc.c

// 8字節(jié)對(duì)齊

#define ALIGNMENT 8

#define ALIGNMENT_SHIFT 3

#define ALIGNMENT_MASK (ALIGNMENT - 1)

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

#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分配策略

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

Pool

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

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

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

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

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

// obmalloc.c

#define SYSTEM_PAGE_SIZE (4 * 1024)

#define SYSTEM_PAGE_SIZE_MASK (SYSTEM_PAGE_SIZE - 1)

// 一個(gè)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; // 指向下一個(gè)

struct pool_header *prevpool; // 指向上一個(gè)

uint arenaindex;

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

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

uint szidx;

uint nextoffset;

uint maxnextoffset;

};

typedef struct pool_header *poolp;

擁有相同block大小的pool通過雙向鏈表連接起來,python使用一個(gè)數(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ù)申請(qǐng)內(nèi)存的大小獲得對(duì)應(yīng)的獲得size class index, 從usedpools中取pool

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

pool = usedpools[size + size];

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

if (pool != pool-》nextpool) {

。..

}

}

}

Arena

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

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

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

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

Python中會(huì)同時(shí)存在多個(gè)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; // 下一個(gè)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;

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

if (unused_arena_objects == NULL) {

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

numarenas = maxarenas ? maxarenas 《《 1 : INITIAL_ARENA_OBJECTS;

。..

}

// 從unused_arena_objects中取出一個(gè)未使用的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)存管理機(jī)制

在介紹Nginx內(nèi)存管理之前,先參照Nginx實(shí)現(xiàn)一個(gè)簡(jiǎ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)存塊,每一個(gè)block有一個(gè)mp_node_s結(jié)構(gòu)體,也即mp_pool_s通過鏈表將所有的block連接起來進(jìn)行管理,而大塊內(nèi)存由mp_large_s進(jìn)行分配。申明的數(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é)點(diǎn),小塊內(nèi)存構(gòu)成一個(gè)鏈表

struct mp_node_s {

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

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

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

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

};

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

struct mp_pool_s {

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

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)體的第一個(gè)node

};

// 需要實(shí)現(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)存 對(duì)齊

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

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

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

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

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

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

*/

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

內(nèi)存池相關(guān)接口實(shí)現(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)存池申請(qǐng)的最大內(nèi)存

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

p-》current = p-》head; // 當(dāng)前可分配的第一個(gè)節(jié)點(diǎn)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 {

/*.。.不斷尋找下一個(gè)可用節(jié)點(diǎn)*/

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

} while (p);

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

return mp_alloc_block(pool, size);

}

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

}

// 大塊節(jié)點(diǎn)內(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);

//。..

}

}

}

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

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

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

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

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

typedef struct {

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

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

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

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

}ngx_pool_data_t;

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

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)體

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

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

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

struct ngx_pool_s {

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

ngx_pool_data_t d;

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

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

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

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é)點(diǎn),逐個(gè)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é)對(duì)齊,速度快,少量浪費(fèi) 》4k則直接malloc分配大塊內(nèi)存

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

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

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

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

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

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

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

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

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

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

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

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

// ringbuffer.c

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

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

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

static u8* pTail = NULL; // 環(huán)形存儲(chǔ)區(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ù)長(zhǎng)度

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é)尾指針

}

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

if (validLen + addLen 》 BUFFER_SIZE) {

int moveLen = validLen + addLen - BUFFER_SIZE; // 有效指針將要移動(dòng)的長(zhǎng)度

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長(zhǎng)度

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)存管理這兩部分只做簡(jiǎn)單介紹。

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

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

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

ThreadCache對(duì)象不夠,就從CentralCache中批量申請(qǐng)

CentralCache不夠,從PageHeap申請(qǐng)Span

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

tcmalloc釋放流程:

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

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

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

多個(gè)連續(xù)的Page組成Span, Span 中記錄起始 Page 的編號(hào),以及 Page 數(shù)量,大對(duì)象(》32k)直接分配Span,小對(duì)象(《=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)存管理機(jī)制

Q:Andriod的Java程序?yàn)槭裁慈菀壮霈F(xiàn)OOM?

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

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

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

分配機(jī)制:

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

回收機(jī)制:

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

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

    關(guān)注

    87

    文章

    11207

    瀏覽量

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

    關(guān)注

    0

    文章

    168

    瀏覽量

    14117
  • python
    +關(guān)注

    關(guān)注

    55

    文章

    4767

    瀏覽量

    84375
  • nginx
    +關(guān)注

    關(guān)注

    0

    文章

    142

    瀏覽量

    12154

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

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

收藏 人收藏

    評(píng)論

    相關(guān)推薦

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

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

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

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

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

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

    把ddr內(nèi)存轉(zhuǎn)為固態(tài)硬盤的pcie轉(zhuǎn)接卡

    可以把轉(zhuǎn)接卡里的內(nèi)存識(shí)別為相應(yīng)大小的存儲(chǔ)空間。。就是這東西有大佬會(huì)設(shè)計(jì)嗎。。。個(gè)人想玩一下,玩過虛擬內(nèi)存模擬硬盤軟件了。淘寶想買卻沒有這類轉(zhuǎn)接卡。。。
    發(fā)表于 05-26 20:31

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

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

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

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

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

    我們已經(jīng)知道,最好將虛擬地址映射到連續(xù)頁幀,從而更好地利用緩存并實(shí)現(xiàn)更低的平均內(nèi)存訪問時(shí)間。然而,如果對(duì)內(nèi)存區(qū)域的請(qǐng)求并不頻繁,那么考慮基于通過連續(xù)線性地址訪問非連續(xù)頁幀的分配方案是有意義的。該模式
    的頭像 發(fā)表于 02-23 09:44 ?843次閱讀
    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)存其實(shí)指的是虛擬內(nèi)存,在調(diào)用 mmap 進(jìn)行匿名映射的時(shí)候(比如進(jìn)行堆內(nèi)存的分配),是將進(jìn)程虛擬內(nèi)存空間中的某一
    的頭像 發(fā)表于 01-24 14:30 ?1368次閱讀
    拆解mmap<b class='flag-5'>內(nèi)存</b>映射的本質(zhì)!

    FreeRTOS內(nèi)存機(jī)制詳解

    FreeRTOS是一種實(shí)時(shí)操作系統(tǒng),它提供了多種內(nèi)存分配方式,包括動(dòng)態(tài)內(nèi)存分配和靜態(tài)內(nèi)存分配。
    的頭像 發(fā)表于 12-31 16:49 ?2458次閱讀
    FreeRTOS<b class='flag-5'>內(nèi)存</b><b class='flag-5'>機(jī)制</b>詳解

    Windows服務(wù)器虛擬內(nèi)存的設(shè)置建議

    虛擬內(nèi)存是計(jì)算機(jī)操作系統(tǒng)用于擴(kuò)展物理內(nèi)存的一種機(jī)制。在Windows服務(wù)器上,虛擬內(nèi)存的設(shè)置對(duì)系統(tǒng)性能和穩(wěn)定性至關(guān)重要。以下是關(guān)于Windo
    的頭像 發(fā)表于 12-25 17:03 ?2191次閱讀

    java虛擬機(jī)內(nèi)存包括遠(yuǎn)空間內(nèi)存

    詳細(xì)介紹JVM內(nèi)存的各個(gè)部分及其作用。 Java堆(Heap) Java堆是JVM管理的最大一塊內(nèi)存區(qū)域,用于存放Java對(duì)象實(shí)例。在堆中分配內(nèi)存
    的頭像 發(fā)表于 12-05 14:15 ?366次閱讀

    jvm管理內(nèi)存包括哪幾個(gè)運(yùn)行時(shí)數(shù)據(jù)內(nèi)存

    JVM(Java虛擬機(jī))是Java程序的運(yùn)行環(huán)境,它提供了內(nèi)存管理機(jī)制管理Java程序所需的運(yùn)行時(shí)數(shù)據(jù)內(nèi)存。這些運(yùn)行時(shí)數(shù)據(jù)
    的頭像 發(fā)表于 12-05 14:09 ?511次閱讀

    內(nèi)存管理單元的重要功能是什么

    微觀理解 內(nèi)存管理單元(MMU)的一個(gè)重要功能是使系統(tǒng)能夠運(yùn)行多個(gè)任務(wù),作為獨(dú)立的程序運(yùn)行在他們自己的 私有虛擬內(nèi)存空間。 它們不需要了解系統(tǒng)的物理
    的頭像 發(fā)表于 11-26 15:36 ?615次閱讀
    <b class='flag-5'>內(nèi)存</b><b class='flag-5'>管理</b>單元的重要功能是什么

    MMU內(nèi)存管理單元的宏觀理解

    (Memory Management Unit,內(nèi)存管理單元)是一種硬件模塊,用于在CPU和內(nèi)存之間實(shí)現(xiàn)虛擬內(nèi)存管理。 其主要功能是將
    的頭像 發(fā)表于 11-26 15:21 ?526次閱讀
    MMU<b class='flag-5'>內(nèi)存</b><b class='flag-5'>管理</b>單元的宏觀理解

    java內(nèi)存溢出的幾種原因和解決辦法

    Java是一種使用垃圾回收機(jī)制的編程語言,由于自動(dòng)內(nèi)存管理機(jī)制的存在,Java程序中發(fā)生內(nèi)存溢出(Out of Memory)錯(cuò)誤的情況相對(duì)較少。然而,雖然Java垃圾回收器負(fù)責(zé)釋放無
    的頭像 發(fā)表于 11-23 14:44 ?5959次閱讀