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整个虚拟机实例来使用似乎太小了

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