Android線性內存分配器LinearAlloc分析

因爲畢業設計做Dalvik內存管理方面的優化。

這些天仔細閱讀了下線性分配器LinearAlloc的代碼

線性分配器代碼爲於android_src/dalvik/vm/目錄下的

只有兩個文件LinearAlloc.h和LinearAlloc.c代碼很少,約800多行而已

線性分配器的目的在於簡單、快速地分配只寫一次(write-once)的內存(即分配並完成初始化寫入後一般不會再改變,保持只讀性質)

它主要用來管理Dalvik中類加載時的內存,因爲類加載後通常是隻讀屬性,而不需要去改變

且在程序的整個運行週期都是有效的,同時它還有共享的特性,一個應用加載後其它進程可以共享使用這些已加載的類從而加快程序的啓動和運行速度

另外,在Java中動態分配內存是由堆來管理的,需要一個垃圾收集器來管理垃圾,對於永久存在的內存區不需要垃圾收集器的掃描清除,

所以將這些永久存在的內存塊放到線性分配器中管理能很好地減少堆混亂和垃圾掃描,加快系性能

好吧,進入正題:

線性內存分配器用shmem從系統中申請一塊大小爲5M的內存,然後用自己的接口來管理它,提供分配和釋放內存的API

它的線性在於分配內存從低地址到高地址,先分配的在前,後分配的在後

每個Dalvik虛擬機實例有個全局的LinearAllocHdr結構體來描述當前虛擬機的線性分配器

gDvm.pBootLoaderAlloc

以下是它的唯一一個數據結構

/*
 * Linear allocation state.  We could tuck this into the start of the
 * allocated region, but that would prevent us from sharing the rest of
 * that first page.
 線性分配狀態,可以將它放到分配區域的前段,但是那會防礙共享它的第一頁之外的頁
 */
typedef struct LinearAllocHdr {
    int     curOffset;          /* offset where next data goes *///下一次分配的地址
    pthread_mutex_t lock;       /* controls updates to this struct *///用來多線程同步的鎖

    char*   mapAddr;            /* start of mmap()ed region *///分配器管理的整塊內存的起始地址
    int     mapLength;          /* length of region *///整塊內存長或大小
    int     firstOffset;        /* for chasing through *///第一次分配的位置
	/* 描述內存中這些頁的讀寫權限 它指向一個位圖,位圖中的每位爲16bit,用來存放對應頁寫的次數*/
    short*  writeRefCount;      /* for ENFORCE_READ_ONLY */
} LinearAllocHdr;


它的主要接口如下
/*
 * 創建一個線性分配器
 */
LinearAllocHdr* dvmLinearAllocCreate(Object* classLoader);

/*
 * 銷燬線性分配器
 */
void dvmLinearAllocDestroy(Object* classLoader);

/*
 * 從線性分配器中分配內存
 */
void* dvmLinearAlloc(Object* classLoader, size_t size);
/*
 * 釋放線性分配器中的內存,注意它不會增加可用的線性內存,只是用來協助其它程序調試用
 */
void dvmLinearFree(Object* classLoader, void* mem);
還有一些其它的API,如
//重新分配大小來存儲原來的內容
void* dvmLinearRealloc(Object* classLoader, void* mem, size_t newSize);

//使這塊區域只讀
INLINE void dvmLinearReadOnly(Object* classLoader, void* mem)

//全可讀寫
INLINE void dvmLinearReadWrite(Object* classLoader, void* mem)

//同C中的strup,用來申請內存存放str中的內容,使用完後由用戶負責調用釋放
char* dvmLinearStrdup(Object* classLoader, const char* str);

//調試用,用來打印內存中的內容
void dvmLinearAllocDump(Object* classLoader);

//檢查從[start,start+length)這個區域是否在線性分配的已分配區間中
bool dvmLinearAllocContains(const void* start, size_t length);



主要關注兩點初始化和分配




A、線性分配器的初始化

1.動態分配一個LinearAllocHdr結構體
pHdr = (LinearAllocHdr*) malloc(sizeof(*pHdr));

2.調用Android API申請一塊匿名區域
fd = ashmem_create_region("dalvik-LinearAlloc", DEFAULT_MAX_LENGTH);//在這個地方申請了一塊內存區域來存放只讀數據,初始默認爲5M

3.計算首次分配內存的地址
pHdr->curOffset = pHdr->firstOffset =//8-4 +4K相當於4K+4的偏移量
        (BLOCK_ALIGN-HEADER_EXTRA) + SYSTEM_PAGE_SIZE;
    pHdr->mapLength = DEFAULT_MAX_LENGTH;//5M
最關鍵的三點
a.每一次內存分配會形成一個內存塊Block它要求64位對齊即8byte對齊
b.每個內存塊前的一個word 32bit 4byte存放描述該塊的信息,包含是否空閒、讀寫權限、長度即free|r/w|length31:30:[29-0]
c.每一個頁可由linux的mprotect函數來設定其讀寫權限,所以跨頁分配時需要特殊處理
所以首次分配時最前面的一個頁空着,並且把第二個頁首地址+塊對齊-頭部長即4KB+8B-4B作爲firstOffset和curOffset值

4.將第二頁設爲讀寫(注:ENFORCE_READ_ONLY宏爲false)
if (mprotect(pHdr->mapAddr + SYSTEM_PAGE_SIZE, SYSTEM_PAGE_SIZE,
            ENFORCE_READ_ONLY ? PROT_READ : PROT_READ|PROT_WRITE) != 0)
    {

5.計算頁數量,併爲位圖申請空間
int numPages = (pHdr->mapLength+SYSTEM_PAGE_SIZE-1) / SYSTEM_PAGE_SIZE;
        pHdr->writeRefCount = calloc(numPages, sizeof(short));//每一個頁用16b來表示總共爲numPages*16bit的位圖空間

6.初始化互斥量
dvmInitMutex(&pHdr->lock);

7.返回描述符
return pHdr;


初始狀態如下圖


頭部結構





B、線性分配器的分配過程

+++++++++++線程同步加鎖
1.獲得全局的分配器狀態描述結構體
LinearAllocHdr* pHdr = getHeader(classLoader);
原型如下
static inline LinearAllocHdr* getHeader(Object* classLoader)//輸入參數無用
{
    return gDvm.pBootLoaderAlloc;//這個是一個全局的,總的線性分配器狀態
}

2.計算下一次分配的地址,即curOffset值
startOffset = pHdr->curOffset;//即當前分配地址的起始
nextOffset = ((startOffset + HEADER_EXTRA*2 + size + (BLOCK_ALIGN-1))
                    & ~(BLOCK_ALIGN-1)) - HEADER_EXTRA;
這個計算式很重要
由於上面三點的原因中的
a.每一次內存分配會形成一個內存塊Block它要求64位對齊即8byte對齊
b.每個內存塊前的一個word 32bit 4byte存放描述該塊的信息
相當於拆成下面兩句
nextOffset = ((startOffset + HEADER_EXTRA*2 + size + (BLOCK_ALIGN-1))& ~(BLOCK_ALIGN-1))//這個處理對齊,兩個HEAD是爲了給下一個塊頭預留空間
nextOffset -= HEADER_EXTRA;//分配位置對齊後向前減去一個頭部的長度(亦即用掉上面預留的頭部空間)

3.計算跨頁問題
    size = nextOffset - (startOffset + HEADER_EXTRA);//少4B,因爲頭部4字節
    lastGoodOff = (startOffset-1) & ~(SYSTEM_PAGE_SIZE-1);//用公式(n,n+1]->n計算它的頁序即第幾個頁
    firstWriteOff = startOffset & ~(SYSTEM_PAGE_SIZE-1);//用公式[n,n+1)->n計算頁序
    /*上果上述兩者不相等,則表示它跨頁了?,但是也僅僅表示恰好在頁對齊處, 而且此時lastGoodOff爲上一個頁數*/
    lastWriteOff = (nextOffset-1) & ~(SYSTEM_PAGE_SIZE-1);//-1限定了只能在頁邊界處才能出現,lastGoodOff!=lastWriteOff只出現在lastGoodOff恰好是頁邊界時
if (lastGoodOff != lastWriteOff || ENFORCE_READ_ONLY) {
    //代碼爲跨頁處理權限問題
}
跨頁的判定
只能出現在兩情況,
一、兩個Block在同一個頁中,上一次分配恰好在頁對齊的地址,而本次分配小於或等於下一個頁對齊地址
二、兩個Block在不同的頁中
即:區間(n,n+1] 爲第n頁


4.更改頁的權限
由於需要初始化,所以一開始要可讀寫
f (lastGoodOff != lastWriteOff || ENFORCE_READ_ONLY) {
        int cc, start, len;
        start = firstWriteOff;
        assert(start <= nextOffset);
        len = (lastWriteOff - firstWriteOff) + SYSTEM_PAGE_SIZE;
        cc = mprotect(pHdr->mapAddr + start, len, PROT_READ | PROT_WRITE);//設爲可讀寫
    }



5.寫入塊的長度,並更新下一次分配地址curOffset的值
*(u4*)(pHdr->mapAddr + startOffset) = size;

pHdr->curOffset = nextOffset;//寫入計算得到的分配地址

6.返回此次分配的地址
return pHdr->mapAddr + startOffset + HEADER_EXTRA;

-------------線程同步解鎖



分配過程中調節如下圖




疑問
1.第一個頁太浪費了
2.5M整個虛擬機實例來使用似乎太小了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章