Linux内存管理系列之三-slab缓存机制
一 CPU三级缓存
为了解决CPU与物理内存速度不匹配的问题,计算机系统引入了缓存,它的读写速度介于CPU与物理内存之间。主要利用程序局部性原理,采用预先读与延迟写的策略,让CPU尽可能少地访问物理内存。
目前一般采用三级缓存机制,其逻辑图如下:
一级缓存的读写速度最快,它是放在CPU内部的,二三级缓存速度比较慢放在主板上。
一般情况下,缓存的命中率能达到90%以上,这样可以大大加快CPU的访问速度。
二 伙伴系统
内存管理的关键就是如何能够分配与释放内存,而且尽量避免内存碎片。通常情况下,对待内存分配有两种方式,大对象(大的连续空间分配)、小对象(小的空间分配)。
伙伴系统主要是针对大的连续空间分配。
在常用的内存管理技术中,往往存在内部碎片与外部碎片两种无法完全利用内存的情况,其总结如下图:
内核采用伙伴系统算法来解决内部碎片的问题,该算法将内存中所有的空闲页框组成11个块链表,每个块链表大小为:1,2,4…..1024个连续页框大小,且每个块的第一个页框的物理地址是该块大小的整数倍。假设以后每次请求的内存区大小为aKB,则分配x个连续页框,x满足4x<=a<=4(x+1),(x属于1,2,4…..1024)。伙伴系统解决了外部碎片的问题,不会存在程序无法加载到内存块的情况。
因为内存存在三种不同的分区,所以对应有三种不同的伙伴系统,在struct zone中存在一个free_area数值,它用来表示该区内所有的空闲块。该数值第k各元素代表该区内大小为2^k的空闲块,同时free_area[k]中同样包含字段nr_free用来指示2^k的空闲块的个数。
伙伴系统中的分配函数_ _rmqueue(),该函数需要zone管理区描述符地址和order块的个数两个参数,如果分配成功则返回第一个描述页框的页描述符,否则返回null。释放块函数_ _free_pages_bulk(),需要3个参数,page块中第一个描述符的地址,zone管理区描述符地址和order块的个数。
内核释放块的过程是分配的逆过程,它要满足:两个块的大小一样计为b,两个块的物理地址连续,第一个块的页框的物理地址是2*b*2^12。
我们访问/proc/pagetypeinfo目录,可以看到伙伴系统链表分配情况:
我们可以看到,一般情况下都是从DMA分区中获取内存块来保证读写性能。
/proc/buddyinfo可以看到伙伴系统和当前状态的信息:
三 slab机制
前面讲到伙伴系统克服了外部碎片的问题,但是对于只有一个字节的内存需求也最少需要分配一个页框大小的块,而且页内的其余内存不能被其他进程使用,这就形成了很大的内部碎片。为了应对内部碎片,内核又提供了slab机制。
在分配与释放内存时需要操作很多数据结构,slab机制利用了CPU三级缓存机制,将这些结构体存放在缓存cache中,分配的时候从缓存获取,释放的时候又交还给缓存。这样就提高了内存分配与释放的速度,同时在cache中使用空闲链表,所以这些缓存会连续存放不会导致碎片。Slab分配器有以下几个基本原则:
1:频繁使用的数据结构应该被缓存起来;
2:频繁使用的数据结构会导致内存碎片,可以使用空闲链表将缓存连续存放不会产生碎片;
3:缓存可以减少CPU访问内存的次数;
4:让部分缓存专属于固定处理器,可以减少smp锁的使用。
Slab将不同的结构体对象划分到不同的高速缓存组,一般情况下一个slab由一页组成,每个高速缓存由多个slab组成,每个slab可以存放多个对象。Slab的结构体如下:
每个高速缓存都使用kmem_cache结构来表示,该结构包含三个链表:slabs_full,slabs_partial,slabs_empty。分别代表slab满队列,slab半满队列,slab空队列。
一般情况下,有结构提的请求都是去slab半满队列中分配,如果没有空的slab就需要创建slab。
Slab队列在维护结构体对象时也会预先创建多余的对象出来,在有对象请求时可以直接从slab中获取。我们可以通过实验观察:
/proc/slabinfo目录存放了slab的全部信息,我观察本机Linux系统:
task_struct 282 291 2656 3 2 : tunables 24 12 8 : slabdata 97 97 0
发现一共有282个task_struct结构体,然后执行top命令:
Tasks: 181 total, 1 running, 180 sleeping, 0 stopped, 0 zombie发现系统上只有181个进程在运行。说明slab提供预分配技术。
Slab的创建函数是kmem_getpages(),里面有3个参数,缓存结构体指针,gfp_t创建标志,nodeid标志用来决定是否从同一个内存节点请求分配。Slab又是高速缓存的组成部分,创建高速缓存的函数kmem_cache_create()。
创建了缓存与slab,接着就是从slab中获取结构体对象,对象的分配函数kmem_cache_alloc(struct kmem_chche *cachep, gfp_t flags),其中两个参数分别用来指定高速缓存的位置和slab分配器的分配类型。如果高速缓存所有的slab中都没有空闲的对象,则调用kmem_getpages()为缓存获取新的页。同时释放对象返回给slab的函数kmem_cache_free(struct kmem_chche *cachep,void *objp)。
关于slab着色,因为在不同 slab 内具有相同偏移量的对象最终很可能映射到同一高速缓存行中。而使用 slab 分配器的对象通常是频繁使用的小对象,高速缓存的硬件可能因此而花费内存周期在同一高速缓存行与 RAM 内存单元之间来来往往的传送两个对象。
假设对象 A,B 均为 32B ,且 A 的地址从 0 开始, B 的地址从 16K 开始,则根据组相联或直接相联映射方式(全相联方式很少使用), A,B 对象很可能映射到 cache 的第 0 行 ,此时,如果 CPU 交替的访问 A,B 各 50 次,每一次访问 cache 第 0 行都失效,从而需要从内存传送数据。而 slab 着色就是为解决该问题产生的,不同的颜色代表了不同的起始对象偏移量,对于 B 对象,如果将其位置偏移向右偏移 32B ,则其可能会被映射到 cache 的第 1 行上,这样交替的访问
A,B 各 50 次,只需要 2 次内存访问即可。
假设对象 A,B 均为 32B ,且 A 的地址从 0 开始, B 的地址从 16K 开始,则根据组相联或直接相联映射方式(全相联方式很少使用), A,B 对象很可能映射到 cache 的第 0 行 ,此时,如果 CPU 交替的访问 A,B 各 50 次,每一次访问 cache 第 0 行都失效,从而需要从内存传送数据。而 slab 着色就是为解决该问题产生的,不同的颜色代表了不同的起始对象偏移量,对于 B 对象,如果将其位置偏移向右偏移 32B ,则其可能会被映射到 cache 的第 1 行上,这样交替的访问
A,B 各 50 次,只需要 2 次内存访问即可。