>

内存处清理计算法,innodb引擎剖判之内部存储器

- 编辑:www.bifa688.com -

内存处清理计算法,innodb引擎剖判之内部存储器

MySQL系列:innodb引擎分析之内存管理

在innodb中实现了自己的内存池系统和内存堆分配系统,在innodb的内存管理系统中,大致分为三个部分:基础的内存块分配管理、内存伙伴分配器和内存堆分配器。innodb定义和实现内存池的主要目的是提供内存的使用率和效率,防止内存碎片和内存分配跟踪和调试。我们先来看看他们的关系和结构。

以下是它的关系结构图:

图片 1

上图中的:

ut_mem_block块是基础内存管理

Buddy allocatZ喎?" target="_blank" class="keylink">vcsrHxNq05rvvsOm31sXkxvc8L3A CjxwPiAgICBtZW1faGVhcMrHxNq05rbRt9bF5Mb3PC9wPgo8aDE MS67 bShxNq05rncwO08L2gxPgppbm5vZGLW0LXExNq05rfWxeS6zcTatObKzbfFysfNqLn9zbPSu7XEveG5ub340NC53MDto6y 38zltcTKtc/W1Np1dDBtZW0uaLrNdXQwbWVtLmO1sdbQo6zG5NbQ1 7W2NKqtcS zcrHttRtYWxsb2O6zWZyZWW1xLfi17Cho82ouf3Su7j2wbSx7b3hubnM5cC0udzA7dLRvq231sXktcTE2rTmo6y94bm5zOXI58/Co7oKPHByZSBjbGFzcz0="brush:sql;"> typedef ut_mem_block_struct { ulint size; /*这个被分配block的内存大小*/ ulint magic_n; /*节点魔法字,用于校验所用*/ UT_LIST_NODE_T(ut_mem_block_t) mem_block_list; /*block list node,指定prev node和next node*/ };关于block的list定义是个全局的变量,UT_LIST_BASE_NODE_T(ut_mem_block_t) ut_mem_block_list;所有分配的block都会加入到这个list当中。在ut_malloc_low函数分配内存的时候会将分配的block加入到list当中,在ut_free的时候会所释放的内存所在的block从list当中删除。除了这两个函数以外,innodb还提供ut_free_all_mem函数来释放所有分配的block和统计分配内存的总数ut_total_allocated_memory功能。
基础内存管理的方法如下:
ut_malloc_low 分配一个n长度的内存块,并将分配的块记录到ut_mem_block_list当中.
ut_malloc 与ut_malloc_low功能相同,但是会用0初始化所分配的内存。
ut_free 释放一个分配的内存块,并将其从ut_mem_block_list当中删除。
ut_free_all_mem 释放ut_mem_block_list所有的内存块并清空ut_mem_block_list
以上函数是支持多线程并发操作的,也就是说是线程安全的。 innodb这样做的目的是保证所有malloc出去的内存都在 ut_mem_block_list当中,以便管理。 基础内存管理的结构如下: 图片 2

转自:

Linux Kernel启动过程中的内存管理

2.伙伴分配器

innodb的伙伴分配器是基于ut_malloc_low函数之上的内存管理器,在创建伙伴分配器时,innodb会一下用ut_malloc_low开辟一个很大的内存块,然后用伙伴分配来分配这个块的内存使用。innodb的伙伴分配器是基于2的基数为基础的管理方式,其buddy alloc pool的定义如下:

   struct mem_pool_struct
    {   
        byte*               buf;                          /*整体内存的句柄*/
        ulint                size;                        /*整体内存大小*/
        ulint                reserved;                    /*当前分配出去的总内存大小*/
        mutex             mutex;                          /*多线程互斥量*/
        UT_LIST_BASE_NODE_T(mem_area_t) free_list[64];    /*area_t链表数组,每个数组单元能管理2的i次方内存块列表,i是数组的下标*/
    };

   struct mem_area_struct
    {
         ulint size_and_free;                                        /*area的内存大小(一定是2的次方),最后一个bit表示是否已经释放*/
         UT_LIST_NODE_T(mem_area_t) free_list;       /*area链表的上下area,因为buddy area是会分裂的,有可能多个*/
     };

mem_area_t是一个buddy的内存区域,也就是mem_area_struct。以下是一个32位机器管理1024字节内存块的buddy list分布: 图片 3
每一个area是有mem_area_t头和可分配的内存(memory_buffer)确定的,memory_buffer的长度不小于mem_area_t头的长度,在32位机器上mem_area_t的头应该是16个字节(8字节对齐)。

Buddy算法的优缺点:

 

2.1mem_area_t的分裂

在内存分配的过程中,有可能会造成mem_area_t的分裂,还是以上面的例子来说,加入我们要分配一个200字节的内存,这时候伙伴分配器的分配流程是这样的: 1.找到一个离200 sizeof(mem_area_t)最近的2的i次方的数(256),确定i = 8, 2.在free_list[i]的列表中查找是否有空闲的node,如果有,将node职位no free.如果没有,对i 1层执行查找是否有可用的内存, 3.在上面的例子中,i 1=9,free_list是空的,继续在i 2层找,一次类推,直到找到有node的层,也就是i = 10; 4.首先对10层进行分裂,分裂成两512大小的第9层节点,并从10删除area,在第9层加入2个512的node. 5.然后在对第9层的第一个节点进行分裂,分裂两个大小为256的第8层节点,并从第九层删除,在第8层加入2个节点。 6.将第一个256大小的area分配给对应的操作者,并置为no free标识。 以下是分配了一个200字节的内存池结构: 图片 4
如果分配出去后的area_t会从free_list[i]链表中删除,也就是说在buddy上将是记录的。

1)尽管伙伴内存算法在内存碎片问题上已经做的相当出色,但是该算法中,一个很小的块往往会阻碍一个大块的合并,一个系统中,对内存块的分配,大小是随机的,一片内存中仅一个小的内存块没有释放,旁边两个大的就不能合并。
图片 5
2)算法中有一定的浪费现象,伙伴算法是按2的幂次方大小进行分配内存块,当然这样做是有原因的,即为了避免把大的内存块拆的太碎,更重要的是使分配和释放过程迅速。但是他也带来了不利的一面,如果所需内存大小不是2的幂次方,就会有部分页面浪费。有时还很严重。比如原来是1024个块,申请了16个块,再申请600个块就申请不到了,因为已经被分割了。

好的操作系统必然要有好的内存管理系统来支持。好的内存管理系统就像一个艺术品,因为在其中我们可以看到空间优化和时间优化的完美平衡(既要省内存又要分配和释放足够快)。Linux为我们提供了这样一个范例,关于它的内存管理在很多讲kernel的书都可以找到。但在这一切还没有建立起来时,系统又是怎么工作的呢?

2.2mem_area_t的合并

如果200字节分配出去后,使用完毕会归还给buddy allocator,还是拿上面的例子来说,就会发生area合并的情况,步骤如下:
1.使用者归还伙伴分配的内存,首先会根据area_t的信息去找到自己的buddy,也就是第8层另外一个没有被分配的area.
2.找到buddy area后,判断buddy area是否是释放状态,如果是,触发合并,将自己和buddy area从第8层删除,合并成一个512大小的第9层area,
3.在重复1 ~ 2步,又会将自己和第九层另外一个buddy area合并成一个1024大小的第10层area.

3)另外拆分和合并涉及到 较多的链表和位图操作,开销还是比较大的。

 

2.3buddy allocator的接口函数:

mem_pool_create 构建一个buddy allocator
mem_area_alloc 用buddy allocator分配一块内存
mem_area_free 将一块内存归还给buddy allocator
mem_pool_get_reserved 获得buddy allocator已经使用的内存大小

Buddy(伙伴的定义):

在系统启动时内存分配大致经历了这样几个阶段(基于kernel 2.6.29):

3内存分配堆(memory heap)

innodb中的内存管理最终的体现形式是mem_heap_t内存分配与管理,所有关于内存分配的操作都会调用mem_heap的API方法,mem_heap_t的结构定义如下:

struct mem_block_info_struct
{
     ulint  magic_n;         /*魔法字*/
     char  file_name[8];    /*分配内存的文件*/
     ulint  line;            /*分配内存的文件所在行*/
     ulint  len;             /*block的长度*/
     ulint  type;            /*依赖的底层分配类型,有DYNAMIC、BUFFER、BTR_SEARCH三种类型*/
     ibool  init_block;  /*是否是外部分配的内存块*/

     ulint  free;           /*被占用的空间大小*/
     ulint  start;          /*可分配内存的起始位置*/
     byte*  free_block;     /*备用block,仅仅在BTR_SEARCH方式可用*/

     UT_LIST_BASE_NODE_T(mem_block_t) base;
     UT_LIST_NODE_T(mem_block_t)  list;
};

备注:mem_block_info_struct/mem_block_info_t/mem_block_t/mem_heap_t是等价
mem_heap_t的内存结构如下:
图片 6系统的malloc和free来作为内存管理。MySQL默认的是系统管理内存方式,一些有经验的DBA会使用系统的管理内存方式 TMalloc来做内存优化,借助TMalloc高效的内存管理方式实现MySQL的性能提升。

在innodb中实现了自己的内存池系统和内存堆分配系统,在innodb的内存管理系统中,大致分为三个部分:...

这里给出伙伴的概念,满足以下三个条件的称为伙伴:
1)两个块大小相同;
2)两个块地址连续;
3)两个块必须是同一个大块中分离出来的;

 

Buddy算法的分配原理:

  1. 静态分配(如果这也算一种的话。。。)

  2. e820表

  3. bootmem allocator

  4. zone allocator(buddy system)

  5. slab allocator

  6. 虚拟空间分配,如用vmalloc, mmap这些函数分配

假如系统需要4(2*2)个页面大小的内存块,该算法就到free_area[2]中查找,如果链表中有空闲块,就直接从中摘下并分配出去。如果没有,算法将顺着数组向上查找free_area[3],如果free_area[3]中有空闲块,则将其从链表中摘下,分成等大小的两部分,前四个页面作为一个块插入free_area[2],后4个页面分配出去,free_area[3]中也没有,就再向上查找,如果free_area[4]中有,就将这16(2*2*2*2)个页面等分成两份,前一半挂如free_area[3]的链表头部,后一半的8个页等分成两等分,前一半挂free_area[2]
的链表中,后一半分配出去。假如free_area[4]也没有,则重复上面的过程,知道到达free_area数组的最后,如果还没有则放弃分配。

 

图片 7

当然以上的几个阶段的时间界限并不总是很明显,有些时候是并存的。以下是初始化代码中几个关键点:

 

start_kernel()  

Buddy算法的释放原理:

    setup_arch()  

内存的释放是分配的逆过程,也可以看作是伙伴的合并过程。当释放一个块时,先在其对应的链表中考查是否有伙伴存在,如果没有伙伴块,就直接把要释放的块挂入链表头;如果有,则从链表中摘下伙伴,合并成一个大块,然后继续考察合并后的块在更大一级链表中是否有伙伴存在,直到不能合并或者已经合并到了最大的块(2*2*2*2*2*2*2*2*2个页面)。

    setup_memory_map() //从boot_params.e820_map读入e820表信息,以后就可以用find_e820_area()分配了。  

图片 8

    init_memory_mapping()  

整个过程中,位图扮演了重要的角色,如图2所示,位图的某一位对应两个互为伙伴的块,为1表示其中一块已经分配出去了,为0表示两块都空闲。伙伴中无论是分配还是释放都只是相对的位图进行异或操作。分配内存时对位图的
是为释放过程服务,释放过程根据位图判断伙伴是否存在,如果对相应位的异或操作得1,则没有伙伴可以合并,如果异或操作得0,就进行合并,并且继续按这种方式合并伙伴,直到不能合并为止。

        kernel_physical_mapping_init() //在虚拟空间映射kernel页表的low mem部分,创建页表过程中需要分配内存就是通过find_e820_area()。  

Buddy内存管理的实现:

    initmem_init()    

提到buddy 就会想起linux 下的物理内存的管理 ,这里的memory pool 上实现的 buddy 系统

        setup_bootmem_allocator() //初始化bootmem allocator,bootmem allocator可用。  

和linux 上按page 实现的buddy系统有所不同的是,他是按照字节的2的n次方来做block的size

            early_res_to_bootmem() //把之前静态分配或者从通过find_e820_area()分配的区域置成保留。  

实现的机制中主要的结构如下:

    paging_init() //完成kernel页表high mem中persistent kernel mapping和temporary kernel mapping部分的初始化,之后可以通过kmap()或kmap_atomic()把物理页映射到high mem区域。  

整个buddy 系统的结构:

        zone_sizes_init() //初始化zone allocator,但只是初始化,还没法用它分配,因为所有的freelist还是被置成空的。  

struct mem_pool_table

vmalloc_init() //初始化分配noncontiguous memory area所需要的结构。vmalloc能分配high mem中从VMALLOC_START到VMALLOC_END的虚拟空间。加上前面paging_init()中提到的两种,针对kernel的high mem的三种映射方式就全了。该函数中通过bootmem allocator分配自身需要的内存。  

{

mem_init() //完成zone allocator,也就是buddy system的初始化,之后alloc_page()就可以用了。这里将bootmem allocator中的未分配空间转到zone allocator中,然后禁用了bootmem allocator。  

#define MEM_POOL_TABLE_INIT_COOKIE (0x62756479)

kmem_cache_init() //初始化slab allocator。它是zone allocator上的一层加强,弥补了zone allocator的一些固有不足,如只能以2的n次幂分配物理页。kmalloc()会从slab allocator上分配,而slab allocator中cache不够又会从zone allocator分配。  

uint32 initialized_cookie; /* Cookie 指示内存已经被初始化后的魔数,  如果已经初始化设置为0x62756479*/

 

uint8 *mem_pool_ptr;/* 指向内存池的地址*/

系统启动刚开始的一些数据是静态分配的,如kernel本身的代码段和数据段,因为这时还没有任何分配器存在。这些都被loader存放在固定的物理地址,并被临时页表映射到固定的虚拟地址。

uint32 mem_pool_size; /* 整个pool 的size,下面是整个max block size 的大小*/

 

uint32 max_block_size; /* 必须是2的n次方,表示池中最大块的大小*/   
boolean assert_on_empty; /* 如果该值被设置成TRUE,内存分配请求没有完成就返回 并输出出错信息*/
 uint32 mem_remaining; /* 当前内存池中剩余内存字节数*/                                              
uint32 max_free_list_index; /* 最大freelist 的下标,*/
struct mem_free_hdr_type     *free_lists[MAX_LEVELS];/* 这个就是伙伴系统的level数组*/

e280表和find_e820_area()可以称得上最早的allocator,尽管它很简单。系统启动早期,detect_memory()函数中,系统通过15h中断从BIOS中读取物理内存信息,将之放到boot_params(也就是zeropage中)。之后set_memory_map()函数将这些信息再读入e820结构体里,之后find_e820_area()就可以从里面分配内存了。分配方式采用简单的线性查找,并把分配出去的空间通过reserve_early()记录到early_res这个结构中,这些信息将会在bootmem allocator的初始化时用来置位那些已分配的物理内存区域。举例来说,当系统要建立kernel页表时,需要申请页表本身所占的内存,于是调用one_page_table_init(),它发现bootmem allocator尚不可用,于是调用alloc_low_page(),这个函数就会到[table_start, start_end]这个区域里去拿内存,而这块内存是在之前find_early_table_space()中通过find_e820_area()申请出来的。当kernel页表建立完后,reserve_early()被调用,它将[table_start, table_end]这块区域以"PGTABLE"为label记录下来。

#ifdef FEATURE_MEM_CHECK
uint32 max_block_requested;
  uint32 min_free_mem; /* 放mem_remaining */
#endif /* FEATURE_ONCRPC_MEM_CHECK*/
};

 

 

然后是bootmem allocator,它和e820直接分配一样,也是一个中间过渡产物。bootmem allocator,顾名思义就是在系统启动时候用的临时内存分配器。它在zone allocator建立好之后就被禁用,而在其中仍然空闲的区域会被回收到zone allocator中。bootmem allocator是一种基于bitmap的分配器,因此速度也很快。从bootmem allocator分配使用函数alloc_bootmem()。

这个结构是包含在free node 或alloc node 中的结构:

 

其中check 和 fill 都被设置为某个pattern
用来检查该node 的合法性
#define MEM_HDR_CHECK_PATTERN ((uint16)0x3CA4)
#define MEM_HDR_FILL_PATTERN ((uint8)0x5C)

再就是zone allocator。我们知道典型的系统上有三个zone:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。系统为每个zone都建立了我们熟悉的buddy system。简单地说,buddy system把物理内存区域按2的n次方(n称为order)挂在zone->free_area上。分配的时候拆分它们直到满足分配申请要求,释放的时候再进行合并。从zone allocator分配和释放分别用函数alloc_page()和free_page()。

typedef struct  tagBuddyMemBlockHeadType

 

{

Buddy system非常高效,但带来了内部碎片问题,因为它只能分配出2的0次方到2的MAX_ORDER次方的页,而且它也不利于硬件cache的利用。而内核中经常会频繁申请固定大小的内存如process descriptor, open file object等。如果每次都从buddy system中申请,既费时间又费空间。出于空间和时间的效率考虑,于是有了slab allocator。slab allocator相当于将内存资源按每种固定大小进行缓存,放在cache中。系统要的时候直接从这个cache里拿,而释放时则不是真的释放,而是放回到cache中。slab allocator维护很多cache,每个cache中包含同一类型的boject。cache又划分为slab,slab通常包含几个连接的物理页,其中存放在被分配的或者尚空闲的object。对于slab allocator中的内存资源,调用kmalloc()或者直接调用kmem_cache_alloc()进行分配,调用kfree()或者直接调用kmem_cache_free()进行释放。当kmem_cache_alloc()被调用且cache中没有object时,会调用cache_grow()来增加cache中的slab,这也是slab的创建过程。cache_grow()继而调用kmem_getpages()为object分配物理内存。而kmem_getpages最终会到buddy system中去分配(kmem_getpages() => alloc_pages_node() => __alloc_pages())。因此我们说slab allocator不是buddy system的替代,还是加强。

    mem_pool_type pool; /*回指向内存池*/

 

    uint16 check;

之后,系统的内存管理系统就初步建立好了。系统中的内存资源有两种-虚拟空间和物理空间。在kernel态,用vmalloc()申请虚拟空间,同时它也调用了alloc_page()申请物理空间,再映射到虚拟空间中。而kmalloc可以直接从slab allocator中申请物理空间,而slab allocator中如果内存不够了再到buddy system中去分配。这样申请来的物理空间在虚拟地址空间中还没有显式映射,当然了,如果是low mem部分则已经在系统初始化时被映射到PAGE_OFFSET处了。而user态中如果app调用malloc这样的函数,malloc会调用mmap,而mmap会申请调用进程的虚拟空间,这里虚拟空间还没有对应的物理页。只有真地访问时发生page fault了,在pagefault handler里才会从buddy system中去分配物理页。

    uint8 state; /* bits 0-3 放该node 属于那1级 bit 7 如果置1,表示已经分配(not free)

Kernel启动过程中的内存管理 好的操作系统必然要有好的内存管理系统来支持。好的内存管理系统就像一个艺术品,因为在其中我们可以...

    uint8 fill;

} BUDDY_MEM_BLOCK_HEAD_TYPE;

这个结构就是包含node 类型结构的 free header 的结构:

typedef struct  tagBuddyMemHeadType

{

    mem_node_hdr_type hdr;

    struct mem_free_hdr_type * pNext;   /* next,prev,用于连接free header的双向 list*/

    struct mem_free_hdr_type * pPrev;

} mem_free_hdr_type;

这个结构就是包含node 类型结构的 alloc header 的结构:
已分配的mem 的node 在内存中就是这样表示的

  1. typedef struct mem_alloc_hdr_type

  2. {

  3.    mem_node_hdr_type hdr;

  4. #ifdef FEATURE_MEM_CHECK_OVERWRITE

  5.    uint32     in_use_size;

  6. #endif

  7. } mem_alloc_hdr_type;

其中用in_use_size 来表示如果请求分配的size 所属的level上实际用了多少
比如申请size=2000bytes, 按size to level 应该是2048,实际in_use_size
为2000,剩下48byte 全部填充为某一数值,然后在以后free 是可以check
是否有overwite 到着48byte 中的数值,一般为了速度,只 检查8到16byte

另外为什么不把这剩下的48byte 放到freelist 中其他level 中呢,这个可能
因为本来buddy 系统的缺点就是容易产生碎片,这样的话就更碎了

关于free or alloc node 的示意图:

假设

最小块为2^4=16,着是由mem_alloc_hdr_type (12byte)决定的, 实际可分配4byte

如果假定最大max_block_size =1024,

如果pool 有mem_free_hdr_type[0]上挂了两个1024的block node

上图是free node, 下图紫色为alloc node

图片 9

接下来主要是buddy 系统的操作主要包括pool init , mem alloc ,mem free

pool init :
 1. 将实际pool 的大小去掉mem_pool_table 结构大小后的size 放到
     mem_pool_size, 并且修改实际mem_pool_ptr指向前进mem_pool_table
     结构大小的地址
 2.  接下来主要将mem_pool_size 大小的内存,按最大块挂到free_lists 上
    level 为0的list 上,然后小于该level block size 部分,继续挂大下一
    级,循环到全部处理完成  (感觉实际用于pool的size ,应该为减去
    mem_pool_table 的大小,然后和最大块的size 对齐,这样比较好,
    但没有实际测试过)
   
   
mem alloc:
    这部分相当简单,先根据请求mem的size ,实际分配时需要加上mem_alloc_hdr_type
这12byte ,然后根据调整后的size,计算实际应该在那个 level上分配,如果有相应级
很简单,直接返回,如果没有,一级一级循环查找,找到后,把省下的部分,在往下一级
一级插入到对应级的freelist 上

mem free:
     其中free 的地址,减去12 就可以获得mem_alloc_hdr_type 结构
     然后确定buddy 在该被free block 前,还是后面, 然后合并buddy,
     循环寻找上一级的buddy ,有就再合并,只到最大block size 那级

关于这个算法,在<<The Art  of Computer Programming>> vol 1,的
动态存储分配中有描述,对于那些只有OSAL 的小系统,该算法相当有用

本文由88bifa必发唯一官网发布,转载请注明来源:内存处清理计算法,innodb引擎剖判之内部存储器