内存分配学习(一)-- 实现一个malloc

内存管理


虚拟内存地址与物理内存地址

Linux 操作系统在处理内存地址时,普遍采用虚拟内存地址技术。
机器语言层面都是采用虚拟地址,当实际的机器码程序涉及到内存操作时,需要根据当前进程运行的实际上下文将虚拟地址转换为物理内存地址,才能实现对真实内存数据的操作。
虚拟内存和物理内存的转换是由页表这个数据结构来实现的。
有时MMU在工作时,会发现页表表明某个内存页不在物理内存中,此时会触发一个缺页异常(Page Fault),此时系统会到磁盘中相应的地方将磁盘页载入到内存中,然后重新执行由于缺页而失败的机器指令。

Heap

概念

理论上,64bit内存地址可用空间为0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF,这是个相当庞大的空间,Linux实际上只用了其中一小部分(256T)。
根据Linux内核相关文档描述,Linux64位操作系统仅使用低47位,高17位做扩展(只能是全0或全1)。所以,实际用到的地址为空间为0x0000000000000000 ~ 0x00007FFFFFFFFFFF和0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF,其中前面为用户空间(User Space),后者为内核空间(Kernel Space)。
图示如下:
内存分配

User Space。将User Space放大后,可以看到里面主要分为如下几段:
Code代码段:这是整个用户空间的最低地址部分,存放的是指令(也就是程序所编译成的可执行机器码)
Data:这里存放的是初始化过的全局变量
BSS:这里存放的是未初始化的全局变量
Heap:堆,这是我们本文重点关注的地方,堆自低地址向高地址增长,后面要讲到的brk相关的系统调用就是从这里分配内存
Mapping Area:这里是与mmap系统调用相关的区域。大多数实际的malloc实现会考虑通过mmap分配较大块的内存区域,本文不讨论这种情况。这个区域自高地址向低地址增长
Stack:这是栈区域,就是函数内局部变量存放的区域。自高地址向低地址增长
堆的分配如下图:

heap

mapped region 是由一个break指针来区分的。Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。
<————————–For Use ————–| —————-Unmapped Region—————>

break

brk 和sbrk函数


要增加一个进程实际的可用堆大小,就需要将break指针向高地址移动(堆是低位向高位增长)。Linux通过brk和sbrk系统调用操作break指针。两个系统调用的原型如下:

int brk(void *addr);
void *sbrk(intptr_t increment);

brk将break指针直接设置为某个地址,而sbrk将break从当前位置移动increment所指定的增量。brk在执行成功时返回0,否则返回-1并设置errno为ENOMEM;sbrk成功时返回break移动之前所指向的地址,否则返回(void *)-1,如果increment 是0,则会返回当前指针地址。
另外需要注意的是,由于Linux是按页进行内存映射的,所以如果break被设置为没有按页大小对齐,则系统实际上会在最后映射一个完整的页,从而实际已映射的内存空间比break指向的地方要大一些,。但是使用break之后的地址是很危险的(break指针和Unmapped region之前也许确实有一小块可用内存地址)。

sbrk 实现malloc大概实现是这样的:

void *my_malloc(size_t size)
{

    void *result = sbrk(0);//result初始化为当前指针地址
    if (sbrk(size) == (void*)-1) // 继续分配可用的size--mapping region
        return NULL;
    return result;
}

经过测试,这个malloc可用简单的使用:

int Test4()
{
    char *pDst = (char *)my_malloc(100);
    char testSrc[] = "hello world";
    strcpy(pDst,testSrc);
    printf("the result is %s\n", pDst);
    printf("\n");
    return 0;
}

但是他没有对已经分配的内存进行有效的记录,而且没有进行内存管理,所以还需要更多的处理。

Malloc实现

1. 函数原型

(void *)malloc(int size)

malloc是一个标准库函数,并不是系统调用。函数的返回值是一个void类型的指针,参数为int类型数据,即申请分配的内存大小,单位是byte。内存分配成功之后,malloc函数返回这块内存的首地址。需要一个指针来接收这个地址。但是由于函数的返回值是void *类型的,所以必须强制转换成你所接收的类型。也就是说,这块内存将要用来存储什么类型的数据。比如:

char *p = (char *)malloc(100);

在堆上分配了100个字节内存,返回这块内存的首地址,把地址强制转换成char 类型后赋给char 类型的指针变量p。同时告诉我们这块内存将用来存储char类型的数据。也就是说你只能通过指针变量p来操作这块内存。这块内存本身并没有名字,对它的访问是匿名访问。
同样要注意:如果所申请的内存块大于目前堆上剩余内存块(整块),则内存分配会失败,函数返回NULL。注意这里说的“堆上剩余内存块”不是所有剩余内存块之和,因为malloc函数申请的是连续的一块内存。既然malloc函数申请内存有不成功的可能,那我们在使用指向这块内存的指针时,必须用if(NULL!=p)语句来验证内存确实分配成功了。

2.确定数据结构

将堆内存空间以块(Block)的形式组织起来,每个块由meta区和数据区组成,meta区记录数据块的元信息(数据区大小、空闲标志位、指针等等),数据区是真实分配的内存区域,并且数据区的第一个字节地址即为malloc返回的地址。

struct s_block
{
    size_t size;//data region size,The sizeof unsigned long is 8
    struct s_block *next;//next block 指针,The sizeof pointer is 8
    int free; // free block or not,The sizeof int is 4
    int padding;//填充4字节 来保证meta长度为8字节,The sizeof int is 4
    char data[1];//malloc返回的地址。The sizeof char is 1
};

typedef struct s_block* t_block;

struct block 描述如下图

block_struct

其中前四个域是meta 区,padding是为了字节对齐填充的,没有实际用途, data数据区的第一个字节,存malloc返回的地址。
内存分布如下:
这里写图片描述

3.寻找合适的block

3.1考虑如何在block链中查找合适的block。一般来说有两种查找算法:
1.> First fit:从头开始,使用第一个数据区大小大于要求size的块为此次分配的块

    t_block find_block_from_first(t_block *last, size_t size)
{
    t_block res = (t_block)first_block;
    while(res && !(res->free && res->size >= size))
    {
        *last = res;
        res = res->next;
    }
    return res;
}

2>Best fit:从头开始,遍历所有块,使用数据区大小大于size且差值最小的块作为此次分配的块

#define MIN_SIZE(size1, size2) (((size1) > (size2)) ? ((size1)-(size2)) : -1 )

t_block find_block_best(t_block *last, size_t size)
{
    t_block res = (t_block)first_block;
    size_t min_size = MIN_SIZE(res->size, size);

    while(res && && !res->free && res->next)
    {
        if ((min_size > 0)
            && (min_size < MIN_SIZE(res->next->size, size))) {
            min_size = MIN_SIZE(res->next->size, size);
        }
        *last = res;
        res = res->next;
    }

    return (*last)->next;
}

3.2 开辟新的block,如果现在所有block都不满足要求,则需要在链表最后开辟一个新的block,这里关键是如何只用sbrk创建一个struct,BLOCK_SIZE is the sizeof struct. 因为存在虚拟字段,所以手工设置

t_block extend_heap(t_block last, size_t size)
{
    t_block res;
    res = (t_block)sbrk(0);
    if(sbrk(BLOCK_SIZE+size) == (void*)-1)
    {
        return NULL;
    }
    res->size = size;
    res->free = 0;
    res->next = NULL;
    if(last)
        last->next = res;

    return res;
}

3.3 分裂block. First fit有一个比较致命的缺点,就是可能会让很小的size占据很大的一块block,此时,为了提高payload,应该在剩余数据区足够大的情况下,将其分裂为一个新的block,示意如下:
这里写图片描述

/**split the block_size into size and new size
   block: the block need to split
   size: the left block's new size
   before:
   ------Block1---------------------Block2--------------
            |------------->next
   After:
   ------Block1-----Block2-----------Block3-----------
            |--->next |------>next
            ---size---|---original size-size-BLOCK_SIZE
*/
void split_block(t_block block, size_t size)
{
    if (block->size < (BLOCK_SIZE + size))
    {
        //No need to split
        return;
    }

    t_block new_block = (t_block)(block->data + size);
    new_block->size = block->size - size - BLOCK_SIZE;
    new_block->next = block->next;
    new_block->free = 1;
    block->size = size;
    block->next = new_block;
}

4.实现一个简单的molloc

我们可以利用上面的代码整合成一个简单但初步可用的malloc。注意首先我们要定义个block链表的头first_block,初始化为NULL;另外,我们需要剩余空间至少有BLOCK_SIZE + 8才执行分裂操作。
其次,由于我们希望malloc分配的数据区是按8字节对齐,所以在size不为8的倍数时,我们需要将size调整为大于size的最小的8的倍数。

#define align8(size) (((size&0x7) == 0) ? size : ((size>>3)+1)<<3)
void *my_malloc(size_t size)
{
    t_block result, last;
    size_t block_size;//final size
    //alian
    block_size = align8(size);

    printf("The size is %d, %s, %d\n", block_size, __func__, __LINE__);
    if(first_block) {
        last = (t_block)first_block;
        result = find_block_best(&last,size);
        if(result) {
            //if unused block > avaliable size + 8, split it
            if((result->size-size) > (block_size+8)) {
                split_block(result,size);
            }
            result->free = 0;//not free anymore
        } else {
            // No avaible block, extend new block
            result = extend_heap(last,size);
            if(!result)
                return NULL;
        }
    } else {
        if((result = extend_heap(NULL,size)) == NULL)
            return NULL;
        first_block = result;
    }
    return result->data;
}

5. calloc的实现
1. calloc 相当于初始化一段size为0的内存,由于我们的数据区是按8字节对齐的,所以为了提高效率,我们可以每8字节一组置0,而不是一个一个字节设置。我们可以通过新建一个size_t指针,将内存区域强制看做size_t类型来实现。

/**
    zero with 8 byte
**/
void *calloc(size_t num, size_t size)
{
    size_t *new_block;
    size_t size8, i;
    /*continued memeory*/
    new_block = (size_t*)my_malloc(num*size);
    if (new_block) {
        size8 = align8(num*size) >> 3;
    }
    for (i=0; i<size8; i++) {
        new_block[i] = 0;
    }
    return new_block;

6. free的实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值