HAproxy 和Agent的內存管理

今天讀了agent的內存管理,其優勢在於不用頻繁地申請和釋放內存,從而消耗時間,但是也有劣勢在於內存只會增加,不會下降。那麼下面來解讀一下它的實現。

一:
首先從基本架構來說,可以看到,在內存裏面其實是由鏈表連接的pool。
每個pool包含了一個鏈表和size,鏈表內的每個項表示每個內存塊,每個內存塊的大小由size決定,。可以看下面的數據結構。一些重要的數據項已經用註釋說明。

《HAproxy 和Agent的內存管理》

struct pool_head {
    void **free_list;//可以理解爲鏈表,free_list的值表示當前可以分配的地址,*free_list的值表示下一塊可以分配的地址
    CIRCLEQ_ENTRY(pool_head) pool_circqe;// 用來連接pool的數據項
    unsigned int used;//表示當前pool內已經使用的內存塊
    unsigned int allocated;//表示當前pool已經分配的內存塊
    unsigned int limit;
    unsigned int minavail;//
    unsigned int size;//表示每個內存塊的大小
    unsigned int flags;
    unsigned int users;
    char name[12];
};

二:
看完了數據結構,那麼pool創建的時候是怎麼樣的呢?
看下述函數,有3個參數,第一個參數表示當前pool的名字,第二個表示pool內存塊的大小。
其實思想比較簡單,就是遍歷當前所有pool,然後找到跟想要分配size相同的內存,如果找到了, 就公用。不然新創建一個,插入隊列。
具體細節可以看下面的註釋。

struct pool_head *create_pool(char *name, unsigned int size, unsigned int flags) {


    struct pool_head *pool;
    struct pool_head *entry;
    struct pool_head *start;
    unsigned int align;

    align = 16;
    size = (size + align - 1) & -align;// 讓size爲16的倍數

    start = NULL;
    pool = NULL;

    CIRCLEQ_FOREACH(entry,&pools,pool_circqe)//變量當前分配的pool
    {
        if (entry->size == size) {
            if (flags & entry->flags & MEM_F_SHARED) {
            //如果size大小相等且都爲shared,則直接用這個pool就可以了
                pool = entry;
                log_debug("Sharing %s with %s\n", name, pool->name);
                break;
            }
        } else if (entry->size > size) {
            log_debug("no suitable pool for %s", name);
            start = entry;
            break;
        }
    }

    if (!pool) {//如果沒找到
        pool = calloc(1, sizeof(*pool));//分配pool的內存
        if (!pool) {
            log_error("allocate %s error,no more memory!", name);
            return NULL;
        }
        if (name)
            da_strlcpy(pool->name, name, sizeof(pool->name));
        pool->size = size;//讓pool的size等於入參size
        pool->flags = flags;
        if(start==NULL)//判斷是不是空隊列,如果空隊列,插頭,否則插start後面
        {
            CIRCLEQ_INSERT_HEAD(&pools, pool, pool_circqe);
        }
        else
        {
            CIRCLEQ_INSERT_AFTER(&pools, start, pool, pool_circqe);
        }
    }
    pool->users++;
    return pool;
}

三:
那下一步是怎麼分配內存,分配內存的代碼很簡單,如下示例

struct msg *m = pool_alloc(pool2_msg);//pool2_msg 是pool_head類型的

那麼這個pool_alloc函數(其實是個宏定義)是怎麼實現的呢?
它有兩種可能,
1. 當前free_list沒有可用的內存了(即(void *)free_list == NULL),那麼它就調用pool_refill_alloc這個函數來分配內存,直接返回給上述struct msg類型的m。pool_refill_alloc就不再贅述了,就是calloc一下內存,讓pool->used++, pool->allocated++;
2. 當前free_list還有可用內存,那麼就將free_list往後移一格,即((pool)->free_list = (void *)(pool)->free_list), 然後返回(void *)free_list;

這裏面有些指針比較難,列一下,可以反覆思考下,理解下:
(pool)->free_list = (void *)(pool)->free_list;

看到這,我當時蒙了,這樣來說free_list豈不是一直爲NULL,那麼就可以看下面的釋放內存了。

#define pool_alloc(pool) \
({                                                              \
        void *__p;                                              \
        if ((__p = (pool)->free_list) == NULL) \ __p = pool_refill_alloc(pool); \ else { \ (pool)->free_list = *(void **)(pool)->free_list;\
                (pool)->used++;                                 \
        }                                                       \
        __p;                                                    \
})


釋放內存:
代碼比較簡單,分爲了3步走
1: 讓當前(pool)->free_list轉爲(void ), 然後ptr轉爲(void *)之後*ptr = (pool)->free_list。
2. (pool)->free_list = (void *)ptr
3. (pool)->used–; 減少pool內存用的數量
這樣我們等於ptr的後面掛了free_list的當前可用節點,ptr是當前可用節點內存塊。
這裏面有些指針比較難,列一下,可以反覆思考下,理解下:
(void *) ptr
(void *)(ptr)

其實到這,我們不禁思考,一個calloc必定對應free 的呢?這樣豈不是內存泄漏了?
No: 後面還有一個memory gc的操作,這個操作會真正釋放內存,這個操作可以作爲在程序退出的時候執行,或者接收信號量異步釋放

#define pool_free(pool, ptr) \
({                                                              \
        if (likely((ptr) != NULL)) {                            \
                *(void **)(ptr) = (void *)(pool)->free_list;    \
                (pool)->free_list = (void *)(ptr);              \
                (pool)->used--;                                 \
        }                                                       \
})

五:
真正的內存釋放:
很簡單了,看註釋吧

CIRCLEQ_FOREACH(entry,&pools,pool_circqe)//遍歷所有pool
    {
        void *temp, *next;

        next = entry->free_list;//pool中free_list的元素
        while (next && entry->allocated > entry->minavail
                && entry->allocated > entry->used) {
            //變量freelist元素
            temp = next;
            next = *(void **) temp;// next指向free_list下一個元素,temp用來被釋放
            entry->allocated--;
            free(temp);//釋放真正的內存
        }
        entry->free_list = next;//注意這句話哦,要賦值哦,不然思考下後果?
    }

完:
這個內存管理還是比較有意思的, 自己在寫之前也不是特別清晰,寫完自己也清楚了很多,這是一個內存管理方案,下次把Nginx的內存管理方案再摸清楚一點,寫清楚點。

点赞