系統程式設計師成長計畫 記憶體管理 三

2021-08-23 14:50:00 字數 4203 閱讀 3920

作者****:李先靜

記憶體管理器

在前面學習共享記憶體的時候,我們重新實現了迴圈佇列,兩個實現的不同之處只是在於記憶體分配和釋放上。對比一下 fifo_ring_create的實現:

第一種實現用malloc分配記憶體。

fiforing* fifo_ring_create(size_t length)

return thiz;

}

第二種實現用 shmem_alloc分配記憶體。

fiforing* fifo_ring_create(size_t length)

} return thiz;

}

只是一行**之差,就要把整個佇列重寫一遍,不符合我們前面倡導的dry(don』t repeat yourself)原則。這裡可以看出,記憶體管理器有不同的實現,所以我們引入記憶體管理器這個介面來隔離變化。記憶體管理器的基本功能有:

o 分配記憶體

o 釋放記憶體

o 擴充套件/縮小已經分配的記憶體

o 分配清零的記憶體

據此,我們定義allocator介面如下:

struct  _allocator;

typedef struct _allocator allocator;

typedef void* (*allocatorcallocfunc)(allocator* thiz, size_t nmemb, size_t size);

typedef void* (*allocatorallocfunc)(allocator* thiz, size_t size);

typedef void (*allocatorfreefunc)(allocator* thiz, void *ptr);

typedef void* (*allocatorreallocfunc)(allocator* thiz, void *ptr, size_t size);

typedef void (*allocatordestroyfunc)(allocator* thiz);

struct _allocator

;

基於malloc系統函式的實現:

static void*  allocator_normal_calloc(allocator* thiz, size_t nmemb, size_t size)

...allocator* allocator_normal_create(void)

return thiz;

}

這裡函式與標準c函式一一對應,只需要簡單包裝就行了。

對於共享記憶體,通常的做法是先分配一大塊記憶體,然後進行二次分配,此時需要編寫自己的記憶體管理器。記憶體分配器是很奇特的,任何初學者都可以設計自己 的記憶體分配器,但同時任何高手都不敢說自己能設計出最好的記憶體分配器。為什麼記憶體分配器很難寫好呢?因為設計好的記憶體分配器需要考慮很多因素:

o 最大化相容性

實現記憶體分配器時,先要定義出分配器的介面函式。介面函式沒有必要標新立異,而是要遵循現有標準(如posix或者win32),讓使用者可以平滑的過度到新的記憶體分配器上。

o 最大化可移植性

通常情況下,記憶體分配器要向os申請記憶體,然後進行二次分配,這要呼叫os提供的函式才行。os提供的函式則是因平台而異,盡量抽象出平台相關的**,才能保證記憶體分配器的可移植性。

o 浪費最小的空間

記憶體分配器要管理記憶體,必然要使用一些自己的資料結構,這些資料結構本身也要佔記憶體空間。在使用者眼中,這些記憶體空間毫無疑問是浪費掉了,如果浪費在 記憶體分配器本身的記憶體太多,顯然是不可以接受的。記憶體碎片也是浪費空間的罪魁禍首,記憶體碎片是一些不連續的小塊記憶體,它們總量可能很大,但因為其非連續性 而無法使用,這些空間在某種程度上說也是浪費的。

o 最快的速度

記憶體分配/釋放是常用的操作。按著2/8原則,常用函式的效能對系統的整體效能影響最大,所以記憶體分配器的速度越快越好。遺憾的是,最先匹配演算法,最優匹配演算法和最差匹配演算法,誰也不能說誰更快,因為快與慢要依賴於具體的應用環境。

o 最大化可調性(以適應於不同的情況)

記憶體管理演算法設計的難點就在於要適應不同的情況。事實上,如果缺乏應用的上下文,是無法評估記憶體管理演算法的好壞的,因為專用演算法總是在時/空效能上 有更優的表現。為每種情況都寫一套記憶體管理演算法,顯然是不太合適的。我們不需要追求最優演算法,那樣代價太高,能達到次優就行了。設計一套通用記憶體管理算 法,通過一些引數對它進行配置,可以讓它在特定情況也有相當出色的表現,這就是可調性。

o 最大化除錯功能

作為乙個c/c++程式設計師,記憶體錯誤可以說是我們的噩夢,上一次的記憶體錯誤一定還讓你記憶猶新。記憶體分配器提供的除錯功能,強大易用,特別對於嵌入式環境來說,記憶體錯誤檢測工具缺乏的情況下,記憶體分配器提供的除錯功能就更不可少了。

o 最大化適應性

前面說了最大化可調性,以便讓記憶體分配器適用於不同的情況。但是,對於不同情況都要去調整配置,還是有點麻煩。盡量讓記憶體分配器適用於很廣的情況,只有極少情況下才去調整配置,這就是記憶體分配器的適應性。

設計是乙個多目標優化的過程,而且有些目標之間存在著競爭。如何平衡這些競爭是設計的難點之一。在不同的情況下,這些目標的重要性是不一樣的,所以根本不存在乙個最好的記憶體分配器。換句話說,記憶體分配器的實現是變化的,要根據不同的應用環境而變化。

下面我們實現乙個傻瓜型的記憶體管理器,按上面的標準來看,它沒有什麼實際的意義,但它的優點是簡單,僅僅200多行**,就展示了記憶體管理器的基本原理。

o 分配過程

所有空閒的記憶體塊放在乙個雙向鍊錶中,最初只有一塊。分配時使用首次匹配演算法,在第乙個空閒塊上進行分配。

static void*  allocator_self_manage_alloc(allocator* thiz, size_t size)

} return_val_if_fail(iter != null, null);

/*如果找到的空閒塊剛好滿足需求,就從空閒塊鍊錶中移出它*/

if(iter->length < (length + min_size))

if(iter->prev != null)

if(iter->next != null)

}else

if(iter->next != null)

if(priv->free_list == iter)

iter->length = length;

}/*返回可用的記憶體*/

return (char*)iter + sizeof(size_t);

}

這裡對空閒塊的管理不占用有效記憶體空間,它只是強制的把空閒塊轉換成 freenode結構,這要求任何空閒塊的大小都不小於sizeof(freenode)。

o 釋放記憶體

釋放時把記憶體塊放回空閒鍊錶,然後對相鄰居的記憶體塊進行合併。

static void   allocator_self_manage_free(allocator* thiz, void *ptr)

/*根據記憶體塊位址的大小,把它插入到適當的位置。*/

for(iter = priv->free_list; iter != null; iter = iter->next)

iter->prev = free_iter;

if(priv->free_list == iter)

break;

} if(iter->next == null)

} /*對相鄰居的記憶體進行合併*/

allocator_self_manage_merge(thiz, free_iter);

return;

}

有了allocator介面,我們也可以通過裝飾模式,為記憶體管理器加上越界/洩露檢查等其它輔助功能。下面的allocator是檢查記憶體越界的裝飾。

o 實現函式

static void*  allocator_checkbo_alloc(allocator* thiz, size_t size)

return ptr + 2 * sizeof(void*);

}

分配記憶體時:多分配3 * sizeof(void*)個位元組,分別用來記錄記憶體塊的長度及前後的magic資料。然後呼叫實際的(即被裝飾的)記憶體分配器分配記憶體,記錄長度並填充magic資料,最後返回記憶體指標給呼叫者。

static void   allocator_checkbo_free(allocator* thiz, void *ptr)

return;

}

釋放記憶體時:取得記憶體塊的長度,然後檢查前後的magic資料是否被修改,如果被修改則認為存在寫越界的錯誤。

系統程式設計師成長計畫 記憶體管理 三

作者 李先靜 記憶體管理器 在前面學習共享記憶體的時候,我們重新實現了迴圈佇列,兩個實現的不同之處只是在於記憶體分配和釋放上。對比一下 fifo ring create的實現 第一種實現用malloc分配記憶體。fiforing fifo ring create size t length retu...

系統程式設計師成長計畫005

1.這個變成大寫的函式,就不需要用函式指標來給foreach做引數了。因為他沒有什麼其他變種,不像print那樣,既要print int又要print str。函式指標,或者說 函式,別瞎用!2.書裡的寫法 dlist foreach dlist,str toupper,null 看來還是堅持了 函...

系統程式設計師成長計畫 併發 五

文章出處 作者 李先靜 無鎖 lock free 資料結構 多執行緒併發執行時,雖然有共享資料,如果所有執行緒只是讀取共享資料而不修改它,也是不用加鎖的,比如 段就是共享的 資料 每個執行緒都會讀取,但是不用加鎖。排除所有這些情況,多執行緒之間有共享資料,有的執行緒要修改這些共享資料,有的執行緒要讀...