一文淺析內存管理機制

本文主要介紹內存管理機制:物理內存與虛擬內存的關係,Linux 內存管理機制,Python 內存管理機制,Nginx 內存管理機制,環形緩衝區機制,以及 TC-malloc 內存分配器的 Andriod 管理機制的簡單介紹。

一. 物理內存與虛擬內存


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

物理內存和虛擬內存之間的關係

虛擬內存以頁爲單位進行劃分,每個頁對應物理內存上的頁框(通常大小爲 4KB),內存管理單元(MMU)負責將虛擬地址轉換爲物理地址,MMU 中有一張頁表來存儲這些映射關係。

並非虛擬內存中所有的頁都會分配對應的物理內存,爲充分利用物理內存,保證儘可能多的進程運行在操作系統上,因此需要提高物理內存利用率,對於很少用到的虛擬內存頁不分配對應的物理內存,只有用到的頁分配物理內存。雖然從程序角度來看,虛擬內存爲連續地址空間,但其實,它被分隔成多個物理內存碎片,甚至還有部分數據並不在內存中,而是在磁盤上。

當訪問虛擬內存時,通過 MMU 尋找與之對應的物理內存,如果沒有找到,操作系統會觸發缺頁中斷,從磁盤中取得所缺的頁並將其換入物理內存,並在頁表中建立虛擬頁與物理頁的映射關係。

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

當進程創建時,內核爲進程分配 4G 虛擬內存,此時,僅僅只是建立一個映射關係,程序的數據和代碼都還在磁盤中,只有當運行時才換回物理內存。並且,通過malloc來分配動態內存時,也只分配了虛擬內存,並不會直接給物理內存,因此,理論上來說malloc可分配的內存大小應該是無限制的(實際當然會有很多算法進行限制)。

多進程使用同一物理內存圖如下:

物理內存與虛擬內存關係

二. Linux 內存管理機制

進程地址空間

進程地址空間分爲內核空間 (3G 到 4G) 和用戶空間(0 到 3G),如下圖.

進程內存地址空間

內核通過brkmmap來分配(虛擬)內存,malloc/free底層實現即爲brkmmapunmmap

當 malloc 內存小於 128k 時採用brk,其通過將數據段 (.data) 的地址指針_edata往高地址推來分配內存,brk分配的內存需要高地址內存全部釋放後纔會釋放,當最高地址空間空閒內存大於 128K 時,執行內存緊縮操作。

當 malloc 內存大於 128K 時採用mmap,其在堆棧中間的文件映射區域(Memory Mapping Segment)找空閒虛擬內存,mmap分配的內存可單獨釋放。

每個進程都對應一個mm_struct結構體, 即唯一的進程地址空間

// 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鏈表結構
    struct rb_root mm_rb;   // 紅黑樹指針
    struct vm_area_struct *mmap_cache;  // 指向最近找到的虛擬區間
    atomic_t mm_users;  // 正在使用該地址的進程數
    atomic_t mm_count;  // 引用計數,爲0時銷燬
    struct list_head mmlist;  // 所有mm_struct結構體都通過mmlist連接在一個雙向鏈表中
};

linux 內核用struct page結構體表示物理頁:

// include/linux/mm.h
struct page {
    unsigned long flags;  // 頁標識符
    atomic_t count;  // 頁引用計數
    struct list_head list;  // 頁鏈表
    struct address_space *mapping;  // 所屬的inode
    unsigned long index;  // mapping中的偏移
    struct list_head lru;  // LRU最近最久未使用, struct slab結構指針鏈表頭變量
    void *virtual;  // 頁虛擬地址
}

內存碎片與外存碎片

內存碎片

產生原因:分配的內存空間大於請求所需的內存空間,造成內存碎片

解決辦法:夥伴算法,主要包括內存分配和釋放兩步:

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

夥伴算法圖解

Step 步驟詳解(注意最左側 Step 爲步驟,ABCD 申請者對應不同的顏色):

  1. 初始化內存,最小內存塊爲 64K,分配 1024KB(只截取部分進行說明)

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

  3. B 申請 66K 內存,因此需要 128K 內存塊,有現成的直接分配

  4. C 申請 35K 內存,需 64K 內存塊,直接分配

  5. D 申請 67K 內存,需 128K 內存塊,步驟 5.1 對半操作,步驟 5.2 分配

  6. 釋放 B 內存塊,沒有相鄰內存可合併

  7. 釋放 D 內存塊,步驟 7.1 釋放內存,步驟 7.2 與相鄰塊進行內存合併

  8. A 釋放內存,不許合併內存

  9. C 釋放內存,步驟 9.1 釋放內存,步驟 9.2-9.5 進行合併,整塊內存恢復如初

以上爲夥伴算法原理,Linux 關鍵代碼在mm/page_alloc.c中,有興趣讀者可在內核源碼中閱讀細節,如下:

//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) {
}

這裏簡單介紹雲風實現的夥伴算法,實現思路:用數組實現完全二叉樹來管理內存,樹節點標記使用狀態,在分配和釋放中通過節點的狀態來進行內存塊的分離與合併,如下:

// 數組實現二叉樹
struct buddy {
   int level;  // 二叉樹深度
    uint8_tree[1]; // 記錄二叉樹用來存儲內存塊(節點)使用情況,柔性數組,不佔內存
};

// 分配大小爲s的內存
int
buddy_alloc(struct buddy * self, int s) {
    // 分配大小s的內存,返回分配內存偏移量地址(首地址)
    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) {
        //具體分配細節...
    }
    return -1;
}

// 釋放內存並嘗試合併
void
buddy_free(struct buddy * self, int offset) {
    // 釋放偏移量offset開始的內存塊
    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:
              // ...
        }
    }
}

外存碎片

產生原因:未被分配的內存,出現大量零碎不連續小內存,無法滿足較大內存申請,造成外部碎片

解決辦法:採用 slab 分配器,處理小內存分配問題,slab 分配器分配內存以字節爲單位,基於夥伴系統分配的大內存進一步細分成小內存分配

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

slab 兩個作用:1)小對象分配,不必每個小對象分配一個頁,節省空間;2)內核中一些小對象創建析構頻繁,slab 對小對象緩存,可重複利用一些相同對象,減少內存分配次數。(應用於內核對象的緩存)。

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

Slab 分配器

三. Python 內存管理機制

內存管理層次結構

Python 內存層次結構

// 第1層 PyMem_Malloc通過一個宏PyMem_MALLOC實現
// 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))) ) )

小塊空間的內存池

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

Python 針對小對象(小於 256 字節)的內存分配採用內存池來進行管理,大對象直接使用標準 C 的內存分配器malloc

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

Block

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

// obmalloc.c
// 8字節對齊
#define ALIGNMENT 8
#define ALIGNMENT_SHIFT 3
#define ALIGNMENT_MASK (ALIGNMENT - 1)
// block大小上限爲256,超過256KB,則交由第一層的內存管理機制
#define SMALL_REQUEST_THRESHOLD  256
#define NB_SMALL_SIZZE_CLASSES (SMALL_REQUEST_THREASHOLD / ALIGNMENT)
// size class index 轉換到 size class
#define INDEX2SIZE(I) (((unit) (I)) + 1) << ALIGMENT_SHIFT)
// sizes class 轉換到size class index
size = (uint )(nbytes - 1) >> ALIGMENT_SHIFT;

小於 256KB 的小塊內存分配如下圖。

Block 分配策略

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

Pool

Pool 爲一個雙向鏈表結構,一系列 Block 組成一個 Pool,一個 Pool 中所有 Block 大小一樣;一個 Pool 大小通常爲 4K(一個虛擬 / 系統內存頁的大小)。

一個小對象被銷燬後,其內存不會馬上歸還系統,而是在 Pool 中被管理着,用於分配給後面申請的內存對象。Pool 的三種狀態

// 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數量
    block *freeblock;  // 指向pool中可用block的單向鏈表
    struct pool_header *nextpool;  // 指向下一個
    struct pool_header *prevpool;  // 指向上一個
    uint arenaindex;
    // 記錄pool保存的block的大小,一個pool中所有block都是szidx大小
    // 和size class index聯繫在一起
    uint szidx;
    uint nextoffset;  
    uint maxnextoffset;
};

typedef struct pool_header *poolp;

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

Userpools 結構

以下爲 Python 內存分配部分代碼:

// obmalloc.c
typedef uchar block;
void *
PyObject_Malloc(sizes_t nbytes)
{
    block *bp;  // 指向從pool中取出第一塊block的指針
    poolp pool;  // 指向一塊4kb內存
    poolp next;
    uint size;
 // 小於SMALL_REQUEST_THRESHOLD 使用Python的小塊內存的內存池,否則走malloc
    if ((nbytes - 1) < SMALL_REQUEST_THRESHOLD) {
        // 根據申請內存的大小獲得對應的獲得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 直接從操作系統分配和申請內存的單位,一個 Arena 爲 256KB,每個 Arena 包含 64 個 Pool,Arena 管理的內存是離散的,Pool 管理的內存是連續的。同 Pool,Arena 也是一個雙向鏈表結構。

Arena 結構

Python 在分配 Pool 的時候優先選擇可用 Pool 數量少的 Arena 進行內存分配,這樣做的目的是爲了讓 Pool 更爲集中,避免 Arena 佔用大量空閒內存空間,因爲 Python 只有在 Arena 中所有的 Pool 全爲空時纔會釋放 Arena 中的內存。

Python 中會同時存在多個 Arena,由 Arenas 數組統一管理。

// 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的個數,並申請內存
        numarenas = maxarenas ? maxarenas << 1 : INITIAL_ARENA_OBJECTS;
        ...
    }
    // 從unused_arena_objects中取出一個未使用的arena_object
    arenaobj = unused_arena_objects;
    unused_arena_objects = arenaobj->nextarena;
    // 建立arena_object和pool的聯繫
    arenaobj->address = (uptr)address;
    ...
    return arenaobj;
}

內存池全景圖

內存池全景圖

四. Nginx 內存管理機制

在介紹 Nginx 內存管理之前,先參照 Nginx 實現一個簡單的內存池,結構圖如下:

其中,mp_pool_s爲內存池的結構體頭,包含內存池的一些全局信息,block爲小塊內存塊,每一個block有一個mp_node_s結構體,也即mp_pool_s通過鏈表將所有的block連接起來進行管理,而大塊內存由mp_large_s進行分配。申明的數據結構如下:

// 結構體
// 大塊內存結構體
struct mp_large_s {
    struct mp_large_s *next;
    void *alloc;
};
// 小塊內存節點,小塊內存構成一個鏈表
struct mp_node_s {
  unsigned char *last;  // 下一次內存從此分配
    unsigned char *end;  // 內存池結束位置
    struct mp_node_s *next;  // 指向下一個內存塊
    size_t failed;  // 改內存塊/node分配失敗的次數
};
// 內存池結構
struct mp_pool_s {
   size_t max;  // 能直接從內存池中申請的最大內存,超過需要走大塊內存申請邏輯
    struct mp_node_s *current;  // 當前分配的node
    struct mp_large_s *large;  // 大塊內存結構體
    struct mp_node_s head[0];  // 柔性數組不佔用大小,其地址爲緊挨着結構體的第一個node
};
// 需要實現的接口
struct mp_pool_s *mp_create_pool(size_t size);  // 創建內存池
void mp_destory_pool(struct mp_pool_s *pool); // 銷燬內存池
void *mp_alloc(struct mp_pool_s *pool, size_t size);  // 分配內存 對齊
void mp_free(struct mp_pool_s *pool, void *p); // 釋放p節點內存

接下來介紹接口實現,先介紹一個接口函數posix_memalign,函數原型如下:

int posix_memalign(void**memptr, size_t alignment, size_t size);
/* memptr: 分配好的內存空間的首地址
  alignment: 對齊邊界,Linux中32位系統8字節,64位系統16字節,必須爲2的冪
  size: 指定分配size字節大小的內存
*/

其功能類似malloc,不過其申請的內存都是對齊的。

內存池相關接口實現如下(只貼出部分代碼,完整代碼私信我)

// 創建並初始化內存池
struct mp_pool_s *mp_create_pool(size_t size) {
    struct mp_pool_s *p;
   // 分配內存池內存: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; }
    // 可從內存池申請的最大內存
    p->max = (size < MP_MAX_ALLOC_FROM_POOL) ? size : MP_MAX_ALLOC_FROM_POOL;
    p->current = p->head;  // 當前可分配的第一個節點mp_node_s
    //一些初始化工作
    return p;
}
// 銷燬內存池
void mp_destroy_pool(struct mp_pool_s *pool) {
    struct mp_node_s *h, *n;
    struct mp_large_s *l;
    // 銷燬大塊內存
    for (l = pool->large; l; l = l->next) { /*...*/}
    // 銷燬小塊內存
    h = pool->head->next;
    while (h) {/*...*/}
    free(pool);
}
// mp_alloc 分配內存
void *mp_alloc(struct mp_pool_s *pool, size_t size) {
    if (size <= pool->max) {  // 小塊內存分配
        p = pool->current;
        do {
            /*...不斷尋找下一個可用節點*/
            p = p->next; // 不夠則找下一個節點
        } while (p);
        // 內存池中所有節點內存都不以滿足分配size內存,需要再次分配一個block
        return mp_alloc_block(pool, size);
    }
    return mp_alloc_large(pool, size); // 大塊內存分配
}
// 大塊節點內存釋放
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 中內存管理就比較清晰的,其原理跟上述內存池一致,先上一張圖:

Nginx 內存池結構

以下爲 Nginx 實現,源代碼主要在src/core/ngx_palloc.h/c兩個文件中

// 內存塊結構體,每個內存塊都有,在最開頭的部分,管理本塊內存
typedef struct {
    u_char   *last;  // 可用內存的起始位置,小塊內存每次都從這裏分配
    u_char  *end;  // 可用內存的結束位置
    ngx_pool_t *next; // 寫一個內存池節點
    ngx_unit_t failed;  // 本節點分配失敗次數,超過4次,認爲本節點滿,不參與分配,滿的內存塊也不會主動回收
}ngx_pool_data_t;
// 大塊內存節點
typedef struct ngx_pool_large_s ngx_pool_large_t;
struct ngx_pool_large_s {
    ngx_pool_large_t *next;  // 多塊大內存串成鏈表,方便回收利用
    void      *alloc;  // 指向malloc分配的大塊內存
};
// nginx內存池結構體
// 多個節點串成的單向鏈表,每個節點分配小塊內存
// max,current,大塊內存鏈表旨在頭節點
// 64位系統大小位80字節,結構體沒有保存內存塊大小的字段,由d.end - p得到
struct ngx_pool_s {
    // 本內存節點信息
    ngx_pool_data_t  d;
    // 下面的字段旨在第一個塊中有意義
    size_t      max;  // 塊最大內存
    ngx_pool_t     *current;  // 當前使用的內存池節點
    ngx_chain_t     *chain;
    ngx_pool_large_t    *large;   // 大塊內存
    ngx_pool_cleanup_t  *cleanup;  // 清理鏈表頭指針
    ngx_log_t     *log;
};
// 創建內存池
ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log);
// 銷燬內存池
// 調用清理函數鏈表,檢查大塊內存鏈表,直接free,遍歷內存池節點,逐個free
void ngx_destroy_pool(ngx_pool_t *pool);
// 重置內存池,釋放內存,但不歸還系統
// 之前分配的內存塊依舊保留,重置空閒指針位置
void ngx_reset_pool(ngx_pool_t *pool);
// 分配內存 8字節對齊,速度快,少量浪費 >4k則直接malloc分配大塊內存
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); // 對齊分配,且初始化
// 大塊內存free
ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);

五. Ringbuffer 環形緩衝區機制

Ringbuffer 的兩個特性:1)先進先出;2)緩衝區用完,會回捲,丟棄久遠數據,保存新數據。其結構如下圖:

Ringbuffer 結構

Ringbuffer 的好處:1)減少內存分配進而減少系統調用開銷;2)減少內存碎片,利於程序長期穩定運行。

應用場景:服務端程序收到多個客戶端網絡數據流時,可先暫存在 Ringbuffer,等收到一個完整數據包時再讀取。

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

以下爲一份簡單 Ringbuffer 實現:

// ringbuffer.c
#define BUFFER_SIZE 16  // 緩衝區的長度
static u32 validLen;  // 已使用的數據長度
static u8* pHead = NULL;  // 環形存儲區的首地址
static u8* pTail = NULL;  // 環形存儲區的尾地址
static u8* pValid = NULL;  // 已使用的緩衝區首地址
static u8* pValidTail = NULL;  // 已使用的緩衝區尾地址
// 初始化環形緩衝區
void init Ringbuffer(void) {
    if (pHead == NULL) pHead = (u8*)malloc(BUFFER_SIZE);
    pValid = pValidTail = pHead;
    pTail = pHead + BUFFER_SIZE;
    validLen = 0;
}
// 向緩衝區寫入數據,buffer寫入數據指針,addLen寫入數據長度
int writeRingbuffer(u8* buffer, u32 addLen) {
    // 將數據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;  // 新的有效數據區結尾指針
    } else {
        memcpy(pValidTail, buffer, addLen);
        pValidTail += addLen;  // 新的有效數據結尾指針
    }
    // 重新計算已使用區的起始位置
    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;
}
// 從緩衝區內取出數據,buffer讀取數據的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) 內存分配器

以下 Tcmalloc 和 Andriod 內存管理這兩部分只做簡單介紹。

tcmalloc 優點:內存分配效率高,運行速度快,穩定性強,能夠有效降低系統負載;

應用場景:多核,高併發,多線程

tcmalloc 內存申請流程:

tcmalloc 釋放流程:

多個連續的 Page 組成 Span, Span 中記錄起始 Page 的編號,以及 Page 數量,大對象 (>32k) 直接分配 Span,小對象 (<=32k) 在 Span 中分配 Object。以下爲上述結構圖解:

ThreadCache

![CentralCache](/Users/zhongrunkang/Library/Application Support/typora-user-images/image-20210228203604225.png)

PageHeap

七. Andriod 內存管理機制

Q:Andriod 的 Java 程序爲什麼容易出現 OOM?

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

Andriod 系統中的應用程序基本都是 Java 進程。

Andriod 內存管理機制

分配機制:

爲每一個進程分配一個合理大小的內存塊,保證每個進程能夠正常運行,同時確保進程不會佔用太多的內存;Andriod 系統需要最大限度的讓更多進程存活在內存中,以保證用戶再次打開應用時減少應用的啓動時間,提高用戶體驗。

回收機制:

當系統內存不足時,需要一個合理的回收再分配機制,以保證新的進程可以正常運行。回收時殺死那些正在佔用內存的進程,OS 需要提供一個合理的殺死進程機制。

參考

虛擬內存與物理內存的聯繫與區別

https://blog.csdn.net/lvyibin890/article/details/82217193

雲風夥伴算法實現

https://github.com/cloudwu/buddy.git

Python 源碼分析

https://book.douban.com/subject/3117898/

ringbuffer c code

https://blog.csdn.net/maowentao0416/article/details/81984269

https://zhuanlan.zhihu.com/p/29216091

tcmalloc 圖

tcmalloc 官方文檔

https://zhuanlan.zhihu.com/p/29216091

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/YECe70N5j8t-z3OjvG2kLg