Linux内核malloc()背后的实现原理——内存池

内存池的原理。内存池是一种将对象预先分配到一块连续的内存中,用时直接从这块内存中获取对象的高效内存分配方式。内存池的原理基本可以分为以下三点:

  1. 预先分配一段内存空间:内存池在初始化时会一次性分配一段指定大小的内存,保存为一段连续的空间。例如,我们可以一次性分配10个对象的内存空间。
  2. 对象管理:内存池会记录哪些内存块可用(没有被占用),哪些已被使用。内存池保持一个空闲对象的队列,程序可以从队列中获取一个空闲对象,当一个对象不再使用时将其返回到队列中。
  3. 内存管理:内存池采用内存分区的方法管理内存,将内存空间分成若干个小块,每块大小为我们分配时指定的单个对象大小。当需要分配一个对象时,内存池从空闲队列中取出一个可用的空闲对象,返回给程序。当程序不再使用这个对象时,将其返回到空闲队列中,供以后的分配使用。

以上就是内存池的基本原理,其优点有:

  1. 提高内存管理效率:由于内存池是一次性申请一大块内存,所以减少了申请、释放内存的开销,提高了程序的执行效率。
  2. 防止内存碎片:内存池分配内存空间的是固定大小的内存块,避免了因为频繁的内存分配和释放,导致内存碎片的问题。
  3. 提高性能:内存池提供了一种高效的内存分配方式,可以大大提高程序的性能,尤其对于频繁内存分配的程序,可以明显地提升运行效率。

相对于栈而言,堆这片内存面临着一个稍微复杂的行为模式:在任意时刻,程序可能发出请求,要么申请一段内存,要么释放一段已经申请过的内存,而且申请的大小从几个字节到几个GB都有可能,我们不能假设程序一次申请多少堆空间,因此,堆的管理显得较为复杂。

那么,使用 malloc() 在堆上分配内存到底是如何实现的呢?

一种做法是把 malloc() 的内存管理交给系统内核去做,既然内核管理着进程的地址空间,那么如果它提供一个系统调用,可以让 malloc() 使用这个系统调用去申请内存,不就可以了吗?当然这是一种理论上的做法,但实际上这样做的性能比较差,因为每次程序申请或者释放堆空间都要进行系统调用。我们知道系统调用的性能开销是比较大的,当程序对堆的操作比较频繁时,这样做的结果会严重影响程序的性能。

比较好的做法就是 malloc() 向操作系统申请一块适当大小的堆空间,然后由 malloc() 自己管理这块空间。

malloc() 相当于向操作系统“批发”了一块较大的内存空间,然后“零售”给程序用。当全部“售完”或程序有大量的内存需求时,再根据实际需求向操作系统“进货”。当然 malloc() 在向程序零售堆空间时,必须管理它批发来的堆空间,不能把同一块地址出售两次,导致地址的冲突。于是 malloc() 需要一个算法来管理堆空间,这个算法就是堆的分配算法。

malloc()和free()的分配算法

在程序运行过程中,堆内存从低地址向高地址连续分配,随着内存的释放,会出现不连续的空闲区域,如下图所示:

带阴影的方框是已被分配的内存,白色方框是空闲内存或已被释放的内存。程序需要内存时,malloc() 首先遍历空闲区域,看是否有大小合适的内存块,如果有,就分配,如果没有,就向操作系统申请(发生系统调用)。为了保证分配给程序的内存的连续性,malloc() 只会在一个空闲区域中分配,而不能将多个空闲区域联合起来。

内存块(包括已分配和空闲的)的结构类似于链表,它们之间通过指针连接在一起。在实际应用中,一个内存块的结构如下图所示:

Linux内核malloc()背后的实现原理——内存池_第1张图片

next 是指针,指向下一个内存块,used 用来表示当前内存块是否已被使用。这样,整个堆区就会形成如下图所示的链表:

现在假设需要为程序分配100个字节的内存,当搜索到图中第一个空闲区域(大小为200个字节)时,发现满足条件,那么就在这里分配。这时候 malloc() 会把第一个空闲区域拆分成两部分,一部分交给程序使用,剩下的部分任然空闲,如下图所示:

仍然以图3为例,当程序释放掉第三个内存块时,就会形成新的空闲区域,free() 会将第二、三、四个连续的空闲区域合并为一个,如下图所示:

可以看到,malloc() 和 free() 所做的工作主要是对已有内存块的分拆和合并,并没有频繁地向操作系统申请内存,这大大提高了内存分配的效率。

另外,由于单向链表只能向一个方向搜索,在合并或拆分内存块时不方便,所以大部分 malloc() 实现都会在内存块中增加一个 pre 指针指向上一个内存块,构成双向链表,如下图所示:

Linux内核malloc()背后的实现原理——内存池_第2张图片

链表是一种经典的堆内存管理方式,经常被用在教学中,很多C语言教程都会提到“栈内存的分配类似于数据结构中的栈,而堆内存的分配却类似于数据结构中的链表”就是源于此。

链表式内存管理虽然思路简单,容易理解,但存在很多问题,例如:

一旦链表中的 pre 或 next 指针被破坏,整个堆就无法工作,而这些数据恰恰很容易被越界读写所接触到。

小的空闲区域往往不容易再次分配,形成很多内存碎片。

经常分配和释放内存会造成链表过长,增加遍历的时间。

针对链表的缺点,后来人们提出了位图和对象池的管理方式,而现在的 malloc() 往往采用多种方式复合而成,不同大小的内存块往往采用不同的措施,以保证内存分配的安全和效率。

内存池原理

不管具体的分配算法是怎样的,为了减少系统调用,减少物理内存碎片,malloc() 的整体思想是先向操作系统申请一块大小适当的内存,然后自己管理,这就是内存池(Memory Pool)。

内存池的研究重点不是向操作系统申请内存,而是对已申请到的内存的管理,这涉及到非常复杂的算法,是一个永远也研究不完的课题,除了C标准库自带的 malloc(),还有一些第三方的实现,比如 Goolge 的 tcmalloc 和 jemalloc。

我们知道,C/C++是编译型语言,没有内存回收机制,程序员需要自己释放不需要的内存,这在给程序带来了很大灵活性的同时,也带来了不少风险,例如C/C++程序经常会发生内存泄露,程序刚开始运行时占用内存很少,随着时间的推移,内存使用不断增加,导致整个计算机运行缓慢。

内存泄露的问题往往难于调试和发现,或者只有在特定条件下才会复现,这给代码修改带来了不少障碍。为了提高程序的稳定性和健壮性,后来的 Java、Python、C#、JavaScript、PHP 等使用了虚拟机机制的非编译型语言都加入了垃圾内存自动回收机制,这样程序员就不需要管理内存了,系统会自动识别不再使用的内存并把它们释放掉,避免内存泄露。可以说,这些高级语言在底层都实现了自己的内存池,也即有自己的内存管理机制。

池化技术

在计算机中,有很多使用“池”这种技术的地方,除了内存池,还有连接池、线程池、对象池等。以服务器上的线程池为例,它的主要思想是:先启动若干数量的线程,让它们处于睡眠状态,当接收到客户端的请求时,唤醒池中某个睡眠的线程,让它来处理客户端的请求,当处理完这个请求,线程又进入睡眠状态。

所谓“池化技术”,就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。之所以要申请过量的资源,是因为每次申请该资源都有较大的开销,不如提前申请好了,这样使用时就会变得非常快捷,大大提高程序运行效率。

数据结构

typedef struct mempool_s {
	spinlock_t lock;  // 防止多处理器并发引入的一种锁
	int min_nr;		// elements 数组中的成员数量
	int curr_nr;		// 当前elements数组中空闲的成员数量 
	void **elements;   // 用来存放内存成员的二维数组,长度:min_nr 宽度:各个内存对象的长度

	void *pool_data;  // 内存池与内核缓冲区结合使用(这个指针专门用来指向这种内存对象对应的缓存区的指针)
	mempool_alloc_t *alloc; // 用户在创建一个内存池对象时,提供的内存分配函数。
							//(此函数可以是用户自己定义的,也可以是系统提供的API)
	mempool_free_t *free;  // 内存释放函数
	wait_queue_head_t wait; // 任务等待队列 
} mempool_t;

内存池创建函数

Linux内核malloc()背后的实现原理——内存池_第3张图片

Linux内核malloc()背后的实现原理——内存池_第4张图片

内存池分配内存

Linux内核malloc()背后的实现原理——内存池_第5张图片

内存对象重新放到内存池

Linux内核malloc()背后的实现原理——内存池_第6张图片

内存池设计实现(简单的内存池程序)

设计思路

  • 获取内存映射表的位置
  • 获取内存所在位置
  • 获取内存分配表的位置
  • 内存池初始化
  • 内存分配
  • 内存释放
  • 内存池销毁

代码实现

#include 
#include 
#include 

#define ALLOC_SIZE 8
#define LOOP 5
#define MAX_POOL_SIZE 1024 * 1024 
#define BLOCK_SIZE 64

typedef struct memory_map_table {
    char *p_block;
    int index;
    int used;
}Memory_Map_Table;

typedef struct memory_alloc_table {
    char *p_start;
    int used;
    int block_start_index;
    int block_cnt;
}Memory_Alloc_Table;

typedef struct memory_pool {
    char *memory_start;
    Memory_Alloc_Table *alloc_table;
    Memory_Map_Table *map_table;
    int total_size;
    int internal_total_size;
    int increment;
    int used_size;
    int block_size;
    int block_cnt;
    int alloc_cnt;
}Memory_Pool;

// 获取内存映射表所对应的位置
Memory_Map_Table *map_table_pos(Memory_Pool *pool) {
    Memory_Map_Table *pm = (Memory_Map_Table *)(pool->memory_start + sizeof(Memory_Pool));
    return pm;
}

// 获取内存分配表
Memory_Alloc_Table *alloc_table_pos(Memory_Pool *pool) {
    Memory_Alloc_Table *pm = (Memory_Alloc_Table *)(pool->memory_start + sizeof(Memory_Pool) \
        + sizeof(Memory_Map_Table) * (pool->block_cnt));
    return pm;
}

// 获取内存所在位置
char * memory_pos(Memory_Pool *pool) {
    char *pm = (char *)(pool->memory_start + sizeof(Memory_Pool) \
        + (sizeof(Memory_Map_Table) + sizeof(Memory_Alloc_Table)) * pool->block_cnt);
    
    return pm;
}

// 初始化内存池
Memory_Pool *Memory_pool_init(int size, int increment) {
    char *p = NULL;
    char *p_memory = NULL;
    Memory_Pool *pool = NULL;
    Memory_Alloc_Table *alloc_table = NULL;
    Memory_Alloc_Table *p_alloc_table = NULL;
    Memory_Map_Table *map_table = NULL;
    Memory_Map_Table *p_map_table = NULL;

    int block_cnt = 0;
    int all_size = 0;
    int i = 0;

    if (size < 0 || size > MAX_POOL_SIZE) {
        printf("Memory_pool_init(): Invalid size(%d). \n", size);
        return pool;
    }

    block_cnt = (size + BLOCK_SIZE - 1) / BLOCK_SIZE;
    all_size = sizeof(Memory_Pool) + (sizeof(Memory_Map_Table) + sizeof(Memory_Alloc_Table)) * block_cnt + size;

    p = (char *)malloc(all_size);

    if (p == NULL) {
        printf("Malloc Failed.\n");
        return pool;
    }

    memset(p, 0, all_size);
    pool = (Memory_Pool *)p;
    pool->block_cnt = block_cnt;
    pool->block_size = BLOCK_SIZE;
    pool->increment = increment;
    pool->internal_total_size = BLOCK_SIZE * block_cnt;
    pool->total_size = size;
    pool->used_size = 0;
    pool->alloc_cnt = 0;
    pool->memory_start = p;

    p_memory = memory_pos(pool);
    map_table = map_table_pos(pool);
    for (i = 0; i < block_cnt; i ++) {
        p_map_table = (Memory_Map_Table *)((char *)map_table + i * sizeof(Memory_Map_Table));
        p_map_table->index = 0;
        p_map_table->p_block = p_memory + i * BLOCK_SIZE;
        p_map_table->used = 0;
    }

    alloc_table = alloc_table_pos(pool);
    for (i = 0; i < block_cnt; i ++) {
        p_alloc_table = (Memory_Alloc_Table *)((char *)alloc_table + i * sizeof(Memory_Alloc_Table));
        p_alloc_table->block_cnt = 0;
        p_alloc_table->block_start_index = 1;
        p_alloc_table->p_start = NULL;
        p_alloc_table->used = 0;
    }
    
    printf("Memory_pool_init: total size: %d, block cnt: %d, block size: %d\n", \
        pool->total_size, pool->block_cnt, BLOCK_SIZE);

    return pool;
}

// 分配内存
void *Memory_alloc(Memory_Pool *pool, int size) {
    char *p_start = NULL;
    int need_block_cnt = 0;
    Memory_Alloc_Table *alloc_table = NULL;
    Memory_Alloc_Table *p_alloc_table = NULL;
    Memory_Map_Table *map_table = NULL;
    Memory_Map_Table *p_map_table = NULL;

    int block_cnt = 0;
    int start_index = -1;
    int i = 0;

    if (size <= 0) {
        printf("Memory_alloc(): Invalid size(%d) \n", size);
        return p_start;
    }
    
    if (size > pool->total_size) {
        printf("Memory_alloc(): %d is more than total size.\n", size);
        return p_start;
    }

    if (size > pool->total_size - pool->used_size) {
        printf("Memory_alloc(): free memory(%d) is less than allocated(%d).\n ", pool->total_size - pool->used_size, size);
        return NULL;
    }

    need_block_cnt = (size + BLOCK_SIZE - 1) / BLOCK_SIZE;
    map_table = map_table_pos(pool);
    
    start_index = -1;
    for (i = 0; i < pool->block_cnt; i ++) {
        p_map_table = (Memory_Map_Table *)((char *)map_table + i * sizeof(Memory_Map_Table));
        if (p_map_table->used) {
            block_cnt = 0;
            start_index = -1;
            continue;
        }

        if (start_index == -1) 
        start_index = i;
        block_cnt ++;

        if (block_cnt == need_block_cnt) 
            break;
    }

    if (start_index == -1) {
        printf("No available memory to used.\n");
        return NULL;
    }

    alloc_table = alloc_table_pos(pool);

    for (i = 0; i < pool->block_cnt; i ++) {
        p_alloc_table = (Memory_Alloc_Table *)((char *)alloc_table + i * sizeof(Memory_Alloc_Table));
        if (p_alloc_table->used == 0) 
            break;
        p_alloc_table = NULL;
    }

    if (p_alloc_table == NULL) {
        return NULL;
    }
    
    p_map_table = (Memory_Map_Table *)((char *)map_table + sizeof(Memory_Map_Table) * start_index);
    p_alloc_table->p_start = p_map_table->p_block;
    p_alloc_table->block_start_index = p_map_table->index;
    p_alloc_table->block_cnt = block_cnt;
    p_alloc_table->used = 1;

    for (i = start_index; i < start_index + block_cnt; i ++) {
        p_map_table = (Memory_Map_Table *)((char *)map_table + i * sizeof(Memory_Map_Table));
        p_map_table->used = 1;
    }

    printf("Alloc size: %d, Block: (start: %d, end: %d, cnt: %d) \n", \
        size, start_index, start_index + block_cnt - 1, block_cnt);
    pool->block_cnt ++;
    pool->used_size += size;

    return p_alloc_table->p_start;    
}

// 内存释放
void Memory_free(Memory_Pool *pool, void *memory) {
    Memory_Alloc_Table *alloc_table = NULL;
    Memory_Alloc_Table *p_alloc_table = NULL;
    Memory_Map_Table *map_table = NULL;
    Memory_Map_Table *p_map_table = NULL;

    int i = 0;
    int block_start_index = 0;
    int block_cnt = 0;

    if (memory == NULL) {
        printf("Memory_free(): memory is NULL.\n");
        return;
    }

    if (pool == NULL) {
        printf("Pool is NULL.\n");
        return;
    }

    alloc_table = alloc_table_pos(pool);
    for (i = 0; i < pool->alloc_cnt; i ++) {
        p_alloc_table = (Memory_Alloc_Table *)((char *)alloc_table + i * sizeof(Memory_Alloc_Table));
        if (p_alloc_table->p_start == memory) {
            block_start_index = p_alloc_table->block_start_index;
            block_cnt = p_alloc_table->block_cnt;
        }
    }

    if (block_cnt == 0) {
        return;
    }

    map_table = map_table_pos(pool);
    printf("Block_Free: start: %d, end: %d, cnt: %d\n", block_start_index,\
        block_start_index + block_cnt - 1, block_cnt);
    
    for (i = block_start_index; i < block_start_index + block_cnt; i ++) {
        p_map_table = (Memory_Map_Table *)((char *)map_table + i * sizeof(Memory_Map_Table));
        p_map_table->used = 0;
    }

    p_alloc_table->used = 0;
    pool->used_size = block_cnt *BLOCK_SIZE;

    return;
}

// 销毁内存池
void Memory_pool_destroy(Memory_Pool *pool) {
    if (pool == NULL) {
        printf("Memory_pool_destroy: pool is NULL.\n");
        return;
    }

    free(pool);
    pool = NULL;

    return;
}

int main() {
    Memory_Pool *pool = NULL;
    char *p1 = NULL;
    int i = 0;

    pool = Memory_pool_init(1024, 512);
    if (pool == NULL) {
        printf("Memory_pool_init error.\n");
    }

    for (i = 0; i < 2; i ++) {
        p1 = (char *)Memory_alloc(pool, ALLOC_SIZE);
        if (p1 == NULL) {
            printf("Malloc failed.\n");
        } else {
            printf("Malloc success.\n");
        }
        Memory_free(pool, p1);
    }
    Memory_pool_destroy(pool);
    return 0;
}

Linux内核malloc()背后的实现原理——内存池_第7张图片

你可能感兴趣的:(Linux,linux)