0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Linux内核内存管理之slab分配器

jf_0tjVfeJz 来源:嵌入式ARM和Linux 2024-02-22 09:25 次阅读

本文在行文的过程中,会多次提到cache或缓存的概念。如果没有特殊在前面添加硬件的限定词,就说明cache指的是slab分配器使用的软件缓存的意思。如果添加了硬件限定词,则指的是处理器的硬件缓存,比如L1-DCache、L1-ICache之类的。

本节我们讨论memory area,一段具有连续物理地址和任意长度的内存。

buddy算法将页帧作为最基本的memory area。这对于申请较大内存的请求是非常好的,但是,如果是很小的memory area请求,比如几十或几百字节,我们将如何处理呢?

很明显,申请一个完整页帧存储几十个字节是非常浪费资源的。如果引入新数据结构描述在同一个页帧内如何分配memory area,会引入一个新问题:内部碎片。这是由于请求的内存大小和分配的memory area大小不匹配造成。

早期Linux内核就是采用这种经典方案是,提供大小成几何分布的memory area;换句话说,大小是2的幂次方,而不是要存储数据的实际大小。这样的好处是,无论请求的内存是多少,我们都能保证内存碎片小于50%。基于这种方法,内核创建了13个memory area列表,列表元素的大小从32 → 131072字节。这些列表还是用buddy系统申请内存页帧,或释放不再包含memory area的页帧。使用一个动态列表追踪每个页帧内的自由memory area数量。

1 slab分配器

在buddy系统之上运行前述的memory area分配算法不是特别有效。在Sun Microsystems Solaris 2.4操作系统中首次引入的slab分配器方案给出了一种更好地算法:

存储的数据类型影响memory area的分配方式(类似C++语言中的类的概念,也就是面向对象的编程思想)。例如,给用户态进程申请分配一个页帧,内核会调用get_zeroed_page()函数,将该页填充为0。

slab分配器扩展了该思想,将memory area视为对象,该对象由一组数据结构和一对函数组成,这对函数又称为构造函数和析构函数。前者初始化memory area,而后者负责解除初始化。

为了避免重复初始化对象,slab分配器不会丢弃已经申请但要释放的对象,而是将其保存在内存中。当申请新对象时,直接从内存中获取,而无需重新初始化。

内核往往重复地申请相同类型的memory area(建立cache,重复利用)。比如,当内核创建一个新进程时,它会分配一些固定大小的memory area,每组固定大小的memory area组成一张表,用来保存进程描述符、打开的文件对象等等。当进程结束时,这些memory area以及管理它们的表能够被重复利用。因为进程的创建和销毁是很频繁的,如果没有slab分配器,内核就会浪费时间重复地分配和释放包含相同大小memory area的页帧;而slab分配器,将它们保存在缓存中,可以快速的重复利用。

memory area的申请可以按照它们的使用频率来分类。通过创建一组具有合适大小的专用对象,可以有效处理预期特定大小的内存申请,从而避免内部碎片。同时,对于很少遇见的大小,可以按照几何分布大小(例如,2的幂次方)的对象进行处理,尽管这种方法仍然会导致内部碎片的产生。

引入大小不是几何分布的对象还有一个微妙的好处:内核数据结构的首地址往往不是以2的幂次方大小分布的物理地址上。(通俗地讲,就是数据结构的首地址不太可能正好落在2的幂次方大小的地址上)。因此,辅以硬件cache,可以产生更好的性能。

硬件cache性能是限制尽可能少地调用伙伴关系分配器的另一个原因(频繁调用buddy系统,会降低系统性能)。每次调用伙伴关系系统,都会弄脏硬件cache,也就会增加平均内存访问时间。内核函数对硬件cache的影响被称为函数占用空间;它被定义为当该函数终止后,覆盖的cache百分比。很明显,越大的占用空间比会导致该函数之后的代码执行越慢,因为此时的硬件cache需要重新读取内存,填充自己。

slab分配器将对象分组保存到cache中,每个cache保存了相同类型的对象。比如,打开一个文件,为了保存打开的文件这个对象,就会从slab分配器的filp缓存对象中(文件指针的意思),申请memory area。

包含一个cache的memory area被分成很多个slab;每个slab包含一个或多个连续的页帧,这些页帧用来保存已经分配的对象和自由空闲的对象。如下图所示:

0e2d7826-d0a6-11ee-a297-92fbcf53809c.png

内核会周期性地扫描这些cache,释放掉那些空slab占用的页帧。

2 cache描述符

描述cache的数据类型是kmem_cache_t(等价于struct kmem_cache_s),各字段如下表所示。其中,忽略了收集统计信息和调试的几个字段。

表8-8kmem_cache_t的各个字段

类型 名称 描述
struct array_cache*[] array Per-CPU数组,包含指向本地的那些cache
unsigned int batchcount 与本地cache传送的对象数量
unsigned int limit 本地cache中空闲对象的最大数量。这是可调的
struct kmem_list3 lists 见下表
unsigned int objsize cache中对象的大小
unsigned int flags 描述cache永久属性的一些标志集
unsigned int num 单个slab中的对象数量(同一cache中slab大小相同)
unsigned int free_limit 整个slab cache中自由空闲对象的上限
spinlock_t spinlock 保护cache的自旋锁
unsigned int gfporder 单个slab中连续页帧数量的对数
unsigned int gfpflags 申请分配页帧时传递给伙伴关系函数的标志组合
size_t colour slab颜色数量
unsigned int colour_off slab中基本对齐偏移量
unsigned int colour_next 用于下一个slab的颜色
kmem_cache_t * slabp_cache 指向通用slab cache的指针,该cache包含slab描述符
(如果其内部的slab描述符已经被使用,则为NULL)
unsigned int slab_size 单个slab的大小
unsigned int dflags 描述cache的动态属性的标志集
void * ctor 指向与cache相关联的构造函数方法的指针
void * dtor 指向与cache相关联的析构函数方法的指针
const char * name 保存cache名称的字符数组
struct list_head next cache描述符的双向链表的指针

lists字段参见下表。

表8-9kmem_list3结构体的各个字段

类型 名称 描述
struct list_head slabs_partial slab描述符(包含自由和非自由对象)的双向循环链表
struct list_head slabs_full slab描述符(包含非自由对象)的双向循环链表
struct list_head slabs_free slab描述符(包含自由对象)的双向循环链表
unsigned long free_objects cache中自由对象的数量
int free_touched slab分配器的页回收算法使用
unsigned long next_reap slab分配器的页回收算法使用
struct array_cache * shared 指向所有CPU共享的本地cache

3 slab描述符

cache中的每一个slab都有自己的描述符,其各字段描述,如下表所示:

类型 名称 描述
struct list_head list 指向三个slab描述符的双向链表之一。
也就是cache描述符的kmem_list3中的列表
(slabs_full、slabs_partial、slabs_free)
unsigned long colouroff 该slab中第一个对象的偏移量(跟染色有关)
void * s_mem 该slab中第一个对象的地址
unsigned int inuse 该slab中当前使用的对象数量(非自由)
unsigned int free 该slab中下一个自由对象的索引
如果没有自由对象则等于BUFCTL_END

slab描述符有两个存储的地方:

外部slab描述符

存储在该slab之外,通用缓存之一中(由cache_sizes指向)。

内部slab描述符

存储在该slab之内内,也就是分配给slab的第一个页帧的开头处。

当对象的大小小于512MB时,或者当内部碎片在slab内为slab描述符和对象描述符留出足够的空间时,slab分配器选择第二种解决方案。如果slab描述符存储在slab之外,则cache描述符的flags字段中的CFLGS_OFF_SLAB标志被设置为1;否则它将被设置为0。

下图展示了cache和slab描述符的主要关系。已经使用的slab,部分使用的slab和未使用的slab,它们使用不同链表串联起来。

4 通用和特殊cache

cache可以分为两类:通用和特殊。通用cache仅由slab分配器使用,而特殊cache由内核其它部分使用。

0e49a88e-d0a6-11ee-a297-92fbcf53809c.png

通用cache包含:

第一个cache称为kmem_cache,它的对象都是内核中其余cache的描述符。cache_cache变量保存着这个特殊cache的描述符。

一些包含通用memory area的cache。这些内存区域范围是13个呈几何分布的内存大小。内核中有一个表malloc_sizes(一个数组,数据类型是cache_sizes),它指向26个通用cache描述符,这些cache的大小是32、64、128、256、512、1024、2048、4096、8192、16384、32768、65536、131072字节。对于每种大小,都有两种cache:一种适用于ISA DMA分配,另一种适用于普通内存分配。

系统初始化的时候,调用函数kmem_cache_init()建立通用cache。

特殊cache都是调用kmem_cache_create()函数创建的。依据传参,该函数首先检查处理新cache的最佳方式(例如,slab描述符位于slab内部还是外部)。然后从cache_cache通用缓存中分配新的cache描述符,并将其插入到一个cache描述符的链表cache_chain中(插入操作使用cache_chain_sem信号量进行保护,避免竞态条件发生)。

从cache_chain链表中销毁和移除一个cache,可以调用kmem_cache_destroy()。此函数对于那些在加载时创建cache,卸载时销毁cache的模块非常有用。为了避免浪费内存空间,内核必须在销毁cache本身之前,需要销毁所有的slab。而kmem_cache_shrink()函数正好可以通过调用slab_destroy()迭代销毁所有slab。

不管是通用还是特殊cache,都可以读取/proc/slabinfo获取其名称;该文件还指定了每个cache中自由对象、已分配对象的数量。

# name                 : tunables  
#  : slabdata   
isofs_inode_cache     72     72    656   24    4 : tunables    0    0    0 : slabdata      3      3      0
nf_conntrack         175    175    320   25    2 : tunables    0    0    0 : slabdata      7      7      0
au_finfo               0      0    192   21    1 : tunables    0    0    0 : slabdata      0      0      0
au_icntnr              0      0    832   39    8 : tunables    0    0    0 : slabdata      0      0      0
au_dinfo               0      0    192   21    1 : tunables    0    0    0 : slabdata      0      0      0
ovl_inode             69     69    688   23    4 : tunables    0    0    0 : slabdata      3      3      0
kvm_async_pf           0      0    136   30    1 : tunables    0    0    0 : slabdata      0      0      0
kvm_vcpu               0      0  17152    1    8 : tunables    0    0    0 : slabdata      0      0      0
...省略(内核数据对象使用)
pool_workqueue      1393   1568    256   32    2 : tunables    0    0    0 : slabdata     49     49      0
radix_tree_node    13253  14896    584   28    4 : tunables    0    0    0 : slabdata    532    532      0
task_group           275    275    640   25    4 : tunables    0    0    0 : slabdata     11     11      0
vmap_area           3584   3584     64   64    1 : tunables    0    0    0 : slabdata     56     56      0
dma-kmalloc-8k         0      0   8192    4    8 : tunables    0    0    0 : slabdata      0      0      0
...省略
dma-kmalloc-8          0      0      8  512    1 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-192        0      0    192   21    1 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-96         0      0     96   42    1 : tunables    0    0    0 : slabdata      0      0      0
...省略
kmalloc-rcl-16         0      0     16  256    1 : tunables    0    0    0 : slabdata      0      0      0
kmalloc-rcl-8          0      0      8  512    1 : tunables    0    0    0 : slabdata      0      0      0
kmalloc-8k           168    168   8192    4    8 : tunables    0    0    0 : slabdata     42     42      0
kmalloc-4k          3832   3856   4096    8    8 : tunables    0    0    0 : slabdata    482    482      0
...省略
kmalloc-16          9472   9472     16  256    1 : tunables    0    0    0 : slabdata     37     37      0
kmalloc-8          12288  12288      8  512    1 : tunables    0    0    0 : slabdata     24     24      0
kmem_cache_node     2432   2432     64   64    1 : tunables    0    0    0 : slabdata     38     38      0
kmem_cache          2239   2340    448   36    4 : tunables    0    0    0 : slabdata     65     65      0

5 slab分配器和zone分配器的关系

前面我们知道,cache的页帧分配是在初始化时就完成的。而slab分配器创建新slab时,则需要zone分配器获取一组空闲且连续的物理内存(不会分配高端内存)。因此,需要调用kmem_getpages()函数,在UMA系统上,实现大概如下所示:

void * kmem_getpages(kmem_cache_t *cachep, int flags)
{
    struct page *page;
    int i;

    flags   |= cachep->gfpflags;
    page    = alloc_pages(flags, cachep->gfporder);
    if (!page)
        return NULL;
    i = (1 << cache->gfporder);
    if (cachep->flags & SLAB_RECLAIM_ACCOUNT)
        atomic_add(i, &slab_reclaim_pages);
    while (i--)
        SetPageSlab(page++);
    return page_address(page);
}

参数说明:

cachep

指向需要额外页帧的cache描述符的指针(所需页帧数量由cachep->gfporder字段中的阶数决定)。

flags

指定如何请求页帧(参见Zone分配器)。这个标志与cache描述符中保存的标志组合使用。

内存分配请求的大小由缓存描述符的gfporder字段指定,该字段决定了缓存中slab的大小。如果slab cache设置了SLAB_RECLAIM_ACCOUNT标志,当内核检查是否有足够的内存来满足一些用户请求时,分配给slab的页帧将被视为可回收页。该函数还在分配的页帧的页描述符中设置PG_slab标志。

释放slab页帧时,调用kmem_freepages()函数:

    void kmem_freepages(kmem_cache_t *cachep, void *addr)
    {
        unsigned long i = (1 << cachep->gfporder);
        struct page *page = virt_to_page(addr);

        if (current->reclaim_state)
            current->reclaim_state->reclaimed_slab += i;
        while (i--)
            ClearPageSlab(page++);
        free_pages((unsigned long) addr, cachep->gfporder);
        if (cachep->flags & SLAB_RECLAIM_ACCOUNT)
            atomic_sub(1<gfporder, &slab_reclaim_pages);
    }

该函数释放slab页帧,从线性地址为addr的页帧开始。如果当前进程正在执行内存回收(current->reclaim_state字段不是NULL),reclaim_state->reclaimed_slab加上要释放的页帧数,由页帧回收算法计算在内。此外,如果设置了SLAB_RECLAIM_ACCOUNT标志(见上文),适当的减小slab_reclaim_pages,该变量用来记录在内存不足时,分配给slab的页帧有多少是空闲的。

6 分配slab给cache

刚创建的cache不包含slab,因此也就没有任何空闲的对象。只有在满足下面两个条件时才会将slab分配给cache:

有新对象的分配请求时

cache没有空闲对象时

slab分配器调用cache_grow()分配新的slab。具体的过程如下所示:

首先,调用kmem_getpages()从ZONE页帧分配器获取存储slab所需的页帧;

然后,调用alloc_slabmgmt()来获取新的slab描述符。如果设置了cache描述符的CFLGS_OFF_SLAB标志,则从cache描述符的slabp_cache指向的通用缓存中分配slab描述符;否则,将在slab的第1个页帧中分配slab描述符。

对于给定的页帧,内核必须能够确定它是否被slab分配器使用,如果是,则必须能够快速导出相应cache和slab描述符的地址。因此,cache_grow()扫描分配给新slab的页帧的所有页描述符,并分别使用cache和slab描述符的地址加载page描述符中lru字段的next和prev字段。这是正确的,因为lru字段仅在页帧空闲时由buddy伙伴系统使用,而由slab分配器处理的页帧设置了PG_slab标志,对buddy伙伴系统而言不是空闲的。相反的问题:对于给定的slab,哪些是实现它的页帧?可以通过使用slab描述符的s_mem字段(slab第一个页帧的起始地址)和cache描述符的gfporder字段(slab大小)来确定。

接下来,cache_init_objs(),对slab所有对象调用构造函数;(也就是初始化所有对象)

最后,调用list_add_tail()将获取的slab描述符(*slabp)添加到cache描述符(*cachep)中的空闲slab列表中,并更新空闲对象的计数:

    list_add_tail(&slabp->list, &cachep->lists->slabs_free);
    cachep->lists->free_objects += cachep->num;

7 从cache中释放slab

销毁slab分为两种情况:

slab cache中有太多空闲的对象;

定时器函数周期性地检查是否有完全未使用的slab需要释放

销毁过程的实现函数是slab_destroy(),销毁slab并将对应的页帧释放回ZONE页帧分配器:

    void slab_destroy(kmem_cache_t *cachep, slab_t *slabp)
    {
        /* 检查cache是否具有析构函数:如果有对所有对象调用析构函数 */
        if (cachep->dtor) {
            int i;
            for (i = 0; i < cachep->num; i++) {
                // objp指向当前正在处理的对象
                void* objp = slabp->s_mem+cachep->objsize*i;
                (cachep->dtor)(objp, cachep, 0);
            }
        }

        /* 将slab使用的所有页帧返回给buddy系统 */
        kmem_freepages(cachep, slabp->s_mem - slabp->colouroff);

        /* 如果slab描述符存储在slab之外,需要从slab描述符的缓存中释放 */
        if (cachep->flags & CFLGS_OFF_SLAB)
            kmem_cache_free(cachep->slabp_cache, slabp);

        /* 如果slab cache被设置了`SLAB_DESTROY_BY_RCU`标志,
         * 意味着使用延迟执行的方法释放`slab`,使用call_rcu()注册回调函数
         * 由回调函数调用kmem_freepages()。如果可能,还需要调用kmem_cache_free
         */
        if (unlikely(cachep->flags & SLAB_DESTROY_BY_RCU)) {
            struct slab_rcu *slab_rcu;
            slab_rcu = (struct slab_rcu *) slabp;
            slab_rcu->cachep = cachep;
            slab_rcu->addr = addr;
            call_rcu(&slab_rcu->head, kmem_rcu_free);
        } else {
            kmem_freepages(cachep, addr);
            if (OFF_SLAB(cachep))
                kmem_cache_free(cachep->slabp_cache, slabp);
        }
    }

8 对象描述符

每个对象也有描述符,类型是kmem_bufctl_t,这是一个unsigned short类型。对象描述符数组就存放在slab描述符的后边。所以,对象描述符的存储位置也分为两种情况,如下图所示:

slab外部

存储在由slabp_cache指向的通用cache中。对象描述符和对象所占用的内存大小,有存储在slab中的对象数量决定(cache描述符中的num字段)。

slab内部

存储在slab之内,就在slab描述符后边。

数组中的第一个对象描述符描述第一个对象,依次对应。对象描述符是一个unsigned short整数,只有当对象是空闲时才有意义。它包含指向下一个空闲对象的索引,因此形成了一个空闲对象的列表。该列表中,最后一个空闲对象的索引标记为BUFCTL_END(0xffff)。

0e5fe914-d0a6-11ee-a297-92fbcf53809c.png

9 对象的内存对齐

slab分配器管理的对象在内存中需要对齐,也就是说,存储它们的初始物理地址是给定常数倍数的内存单元中,通常是2的幂。这个常数称为对齐因子。

slab分配器允许的最大对齐因子是4096(页帧大小)。这意味着对象可以通过引用它们的物理地址或线性地址来对齐。在这两种情况下,只有地址的低12位可能会被对齐改变。

通常,如果物理地址按照word(即计算机内部存储器总线的宽度)对齐,计算机访问存储单元的速度会更快。因此,默认情况下,kmem_cache_create()函数根据BYTES_PER_WORD宏指定的字长来对齐对象。

对于×86处理器,宏的值为4,字长为32位。在创建新的slab cache时,可以将对象在硬件L1-cache中对齐。为了实现这一点,内核设置了SLAB_HWCACHE_ALIGNcache描述符标志。kmem_cache_create()会按照如下方式处理请求:

如果对象大于cache line的一半,那么它在内存中的对齐大小就是L1_CACHE_BYTES;换句话说,总是位于cache line的起始处。

否则,对象大小按照obj_size * n = L1_CACHE_BYTES计算出的合理值进行对齐,总之要保证一个对象不能跨越2个cache line。

很显然,slab分配器在这儿就是采用以空间换取时间的思想;通过人为的增加对象的大小获得更好地缓存性能,但也会产生内部碎片。

10 slab染色

我们知道,同一条cache line可以映射许多不同的内存块。在本章中,我们还看到了相同大小的对象最终被存储在硬件cache中相同的偏移位置。在不同slab中具有相同偏移量的对象将以相对较高的概率最终映射到相同的cache line中。因此,频繁访问映射到同一cache line的不同内存位置时,需要来回在硬件cache和内存之间搬运数据,造成访存性能降低。slab分配器通过一种称为slab染色的策略避免这种行为:将不同的值(称为colors)赋给不同的slab。

在分析slab着色之前,我们必须先看一下cache中对象的布局。因为cache在内存中是对齐的,这意味着对象地址必须是给定值(比如aln)的倍数。但即使考虑到对齐约束,也有许多可能的方法将对象存放到slab中。如何选择取决于对以下变量所做的决定:

num

可以存储到slab中的对象数量。

osize

对象大小,包含对齐字节。

dsize

描述符大小(包括slab和所有对象描述符的大小),按照cache line对齐。如果slab和对象描述符存储在slab之外,它的值等于0。

free

slab中未使用的字节(那些没有分配给任何对象的字节)。

所以,slab总长可以用下面的公式计算:

slab length = (num × osize) + dsize + free

free总是小于osize,否则就可以在slab中添加一个对象了。但是,free可能大于aln。

slab分配器利用free未使用字节为slab染色。术语color简单地对slab进行划分,从而允许内存分配器将对象分散到不同的线性地址中。通过这种方式,内核可以从处理器的硬件cache中获得最佳性能。

将slab染色可以将slab的第一个对象存储到不同的内存位置,同时满足对齐约束。可用的color数量是free⁄aln(该值存储在cache描述符的colour字段中)。因此,第1个颜色值是0,最后一个为(free⁄aln)−1。(一种特殊情况是,free < aln,colour设为0,所有的slab使用颜色值0,颜色的数量是一个。)

如果slab被使用颜色值col染色,第一个对象的偏移量(相对于slab初始地址)等于col × aln + dsize个字节。如下图所示,图中阐释了slab内对象的位置如何依赖slab颜色值。本质上,染色就是将slab中未使用的部分字节从结尾处移动到起始处。

0e767170-d0a6-11ee-a297-92fbcf53809c.png

染色只有在free足够大时才起作用。很明显,如果对象没有要求对齐,或者如果slab中未使用的字节数小于对齐因子(free < aln),唯一可能的slab染色就一个,颜色值为0,即第一个对象的偏移量赋值为零。

通过将当前颜色存储在cache描述符中的color_next字段中,cache_grow()函数将color_next指定的颜色分配给新的slab,然后增加该字段的值。到达colour后,它再次绕到“0”。通过这种方式,每个slab都使用与前一个不同的颜色创建,直到最大可用颜色。此外,cache_grow()函数从cache描述符的color_off字段获取值aln,根据slab内部对象的数量计算dsize,最后将值col × aln + dsize存储在slab描述符的coloroff字段中。

11 空闲slab对象的本地缓存

在多核处理器系统中,Linux v2.6版本实现的slab分配器,与最初的Solaris 2.4实现不同。为了减少处理器之间的自旋锁竞争并更好地利用硬件缓存,slab分配器的每个缓存都包含一个CPU核的本地数据结构,该数据结构由指向被释放对象指针组成的数组,称为“slab本地缓存”。大多数slab对象的分配和释放只影响本地缓存;只有当本地缓存下溢或溢出时,才会涉及到slab数据结构。这种技术与前面的“CPU本地页帧缓存”一节中介绍的技术非常相似。

缓存描述符的array字段是指向该指针数组,而指针指向array_cache数据结构,系统中的每个CPU都有一个这样的元素。每个array_cache数据结构都是空闲对象的本地缓存的描述符,其字段如表8-11所示。

表8-11array_cache结构的字段

类型 名称 描述
unsigned int avail 指向本地缓存中可用对象的指针数。该字段可以作为缓存中的第一个空闲slot。
unsigned int limit 本地缓存的大小。也就是说,本地缓存中指针的最大数量。
unsigned int batchcount 本地缓存重填或清空的块大小。
unsigned int touched 如果本地缓存最近被使用,则标志设置为1。

注意,本地缓存描述符不包括本地缓存本身的地址;实际上,本地缓存就放在描述符后面,代码如下所示。当然,本地缓存存储的是指向被释放对象的指针,而不是对象本身,对象总是放在缓存的slab中。

struct arraycache_init {
    struct array_cache cache;
    void * entries[BOOT_CPUCACHE_ENTRIES];
};

当创建新的slab缓存时,kmem_cache_create()函数确定本地缓存的大小(将此值存储在缓存描述符的limit字段中),分配它们,并将它们的指针存储在缓存描述符的array字段中。大小取决于存储在slab缓存中的对象的大小,范围从1表示非常大的对象到120表示较小的对象。此外,“batchcount”字段的初始值,即在块中从本地缓存中添加或删除的对象的数量,最初设置为本地缓存大小的一半。

系统管理员可以为每个cache调节本地缓存的大小,方法是写batchcount字段值,该值保存在/proc/slabinfo文件中。

在多核系统中,存储小内存对象的slab缓存还支持一个额外的本地缓存,它的地址存储在缓存描述符的lists.shared字段中。顾名思义,共享本地缓存是所有CPU共享的,它使空闲对象在本地缓存之间进行迁移变得容易。初始值等于8倍于batchcount的值。

12 分配slab对象

新对象可以通过调用kmem_cache_alloc()函数获得。参数cachep指向必须从中获得新的空闲对象的缓存描述符,而参数flag表示要传递给ZONE页帧分配器函数的标志,如果缓存的所有slab都已满时,使用这些标志创建新的slab。

该函数本质上等价于下面的代码:

    void * kmem_cache_alloc(kmem_cache_t *cachep, int flags)
    {
        unsigned long       save_flags;
        void                *objp;
        struct array_cache  *ac;

        local_irq_save(save_flags);
        ac = cachep->array[smp_processor_id()];
        if (ac->avail) {
            ac->touched = 1;
            objp = ((void **)(ac+1))[--ac->avail];
        } else
            objp = cache_alloc_refill(cachep, flags);
        local_irq_restore(save_flags);
        return objp;
    }

该函数首先尝试从本地缓存中检索一个空闲对象。如果有空闲对象,avail字段包含本地缓存中指向最后一个被释放对象的索引。因为本地缓存数组存储在ac描述符之后,((void**)(ac+1))[——ac->avail]获取该空闲对象的地址并减小ac->avail的值。调用cache_alloc_refill()函数来重新填充本地缓存,并在本地缓存中没有空闲对象时获得一个空闲对象。

cache_alloc_fill()函数基本上执行以下步骤:

static void* cache_alloc_refill(kmem_cache_t* cachep, int flags)
{
    // ...
    check_irq_off();

    /* 1. 获取本地缓存描述符 */
    ac = ac_data(cachep);
retry:
    batchcount = ac->batchcount;
    if (!ac->touched && batchcount > BATCHREFILL_LIMIT) {
        /* 如果最近这个缓存很少有活动,执行部分填充。
         * 否则容易产生refill bouncing。
         */
        batchcount = BATCHREFILL_LIMIT;
    }
    l3 = list3_data(cachep);

    /* 2. 申请自旋锁 */
    spin_lock(&cachep->spinlock);

    /* 3. 如果slab缓存包含本地共享缓存,且其中包含一些空闲对象时。
     * 则将这些空闲对象转移给本地缓存。
     */
    if (l3->shared) {
        struct array_cache *shared_array = l3->shared;
        if (shared_array->avail) {
            if (batchcount > shared_array->avail)
                batchcount = shared_array->avail;
            shared_array->avail -= batchcount;
            ac->avail = batchcount;
            memcpy(ac_entry(ac), &ac_entry(shared_array)[shared_array->avail],
                    sizeof(void*)*batchcount);
            shared_array->touched = 1;
            goto alloc_done;
        }
    }

    /* 4 使用batchcount个指针填充本地缓存,这些指针是slab中的空闲对象的地址 */
    while (batchcount > 0) {
        struct list_head *entry;
        struct slab *slabp;

        /* 4.a 查看cache描述符中的slabs_partial和slabs_free列表,寻找合适的空闲对象:
         * (1) 如果slabs_partial还有空闲对象,则从其中申请空闲对象
         * (2) 否则从slabs_free申请空闲对象
         * (3) 如果slabs_free没有空闲对象,则跳转到must_grow,扩展slab缓存
         */
        entry = l3->slabs_partial.next;
        if (entry == &l3->slabs_partial) {
            l3->free_touched = 1;
            entry = l3->slabs_free.next;
            if (entry == &l3->slabs_free)
                goto must_grow;
        }

        slabp = list_entry(entry, struct slab, list);
        check_slabp(cachep, slabp);
        check_spinlock_acquired(cachep);
        while (slabp->inuse < cachep->num && batchcount--) {
            kmem_bufctl_t next;
            STATS_INC_ALLOCED(cachep);
            STATS_INC_ACTIVE(cachep);
            STATS_SET_HIGH(cachep);

            /* 4.b 获取空闲对象:
             * (1)获取slab中空闲对象的指针;
             * (2)slab描述符的inuse字段+1,表明该空闲对象已经被使用
             * (3)slab描述符的free字段+1,指向下一个空闲对象
             */
            ac_entry(ac)[ac->avail++] = slabp->s_mem + slabp->free*cachep->objsize;
            slabp->inuse++;
            next = slab_bufctl(slabp)[slabp->free];
            slabp->free = next;
        }
        check_slabp(cachep, slabp);

        /* 4.c 将已经消耗的slab插入到cache描述符的正确列表中
         * (1) slab_fulll列表
         * (2) slab_partial列表
         */
        list_del(&slabp->list);
        if (slabp->free == BUFCTL_END)
            list_add(&slabp->list, &l3->slabs_full);
        else
            list_add(&slabp->list, &l3->slabs_partial);
    }

must_grow:
    /* 5. 此时,要添加到本地缓存的指针数量存储在`ac->avail`变量中。
     * 将kmem_list3结构体的字段free_objects减去添加到本地缓存中的指针数量,
     * 即是表明这些对象不再是空闲的了。
     */
    l3->free_objects -= ac->avail;

alloc_done:
    /* 6. 释放自旋锁 */
    spin_unlock(&cachep->spinlock);

    /* 8. 如果没有发生缓存重填,则调用cache_grow申请一个新的slab,
     *    并申请分配新的空闲对象
     */
    if (unlikely(!ac->avail)) {
        int x;
        x = cache_grow(cachep, flags, -1);

        /* 9. 没有申请到新slab缓存,则返回NULL;
         *    否则跳转到第一步,重新分配对象
         */
        ac = ac_data(cachep);
        if (!x && ac->avail == 0)   // 没有可用的空闲对象,放弃
            return NULL;

        if (!ac->avail)     // 对象重填被中断?
            goto retry;
    }

    /* 7 如果ac->avail大于0(说明某些缓存被重填了),设置本地缓存被使用,
     *   并返回插入到本地缓存中的最后一个空闲对象的指针
     */
    ac->touched = 1;
    return ac_entry(ac)[--ac->avail];
}

13 释放slab对象

kmem_cache_free()函数释放先前由slab分配器分配给某个内核函数的对象。它的参数是cachep,缓存描述符的地址,objp,要释放的对象的地址:

    void kmem_cache_free(kmem_cache_t *cachep, void *objp)
    {
        unsigned long flags;
        struct array_cache *ac;

        local_irq_save(flags);
        ac = cachep->array[smp_processor_id()];
        if (ac->avail == ac->limit)
            cache_flusharray(cachep, ac);
        ((void**)(ac+1))[ac->avail++] = objp;
        local_irq_restore(flags);
    }

该函数首先检查本地缓存是否有空间容纳指向空闲对象的额外指针。如果是,指针被添加到本地缓存中,函数返回。否则,它首先调用cache_flusharray()来耗尽本地缓存,然后将指针添加到本地缓存。

cache_flusharray()函数的主要功能如下:

static void cache_flusharray (kmem_cache_t* cachep, struct array_cache *ac)
{
    int batchcount;
    batchcount = ac->batchcount;
    check_irq_off();

    /* 1. 申请自旋锁 */
    spin_lock(&cachep->spinlock);

    /* 2. 判断贡献本地缓存是否还有空间,如果有,
     *    则从CPU本地缓存中拷贝batchcount个指针到共享缓存中;
     *    然后跳转到第4步。
     */
    if (cachep->lists.shared) {
        struct array_cache *shared_array = cachep->lists.shared;
        int max = shared_array->limit-shared_array->avail;
        if (max) {
            if (batchcount > max)
                batchcount = max;
            memcpy(&ac_entry(shared_array)[shared_array->avail],
                    &ac_entry(ac)[0],
                    sizeof(void*)*batchcount);
            shared_array->avail += batchcount;
            goto free_done;
        }
    }

    /* 3 将当前本地缓存中的batchcount对象返还给`slab`分配器 */
    free_block(cachep, &ac_entry(ac)[0], batchcount);
free_done:
    /* 4. 释放自旋锁 */
    spin_unlock(&cachep->spinlock);
    /* 5. 将移动到共享本地缓存或slab分配器中的对象个数从本地缓存描述符中减去 */
    ac->avail -= batchcount;
    /* 6. 将本地缓存中所有合法的指针移动到本地缓存数组的起始处。
     *    因为原起始处的对象指针已经被我们移走了。
     */
    memmove(&ac_entry(ac)[0], &ac_entry(ac)[batchcount],
            sizeof(void*)*ac->avail);
}

free_block()函数的主要功能如下:

static void free_block(kmem_cache_t *cachep, void **objpp, int nr_objects)
{
    int i;

    check_spinlock_acquired(cachep);

    /* NUMA: move add into loop
     * 1. 增加cache描述符的空闲对象计数
     */
    cachep->lists.free_objects += nr_objects;

    for (i = 0; i < nr_objects; i++) {
        void *objp = objpp[i];
        struct slab *slabp;
        unsigned int objnr;

        /* 2. 获取对象所在slab的描述符地址:
         *    (slab所在页的描述符的lru字段指向相应的slab描述符)
         */
        slabp = GET_PAGE_SLAB(virt_to_page(objp));
        // 3. 从slab cache列表中移除slab描述符
        // (cachep->lists.slabs_partial或cachep->lists.slabs_full)
        list_del(&slabp->list);
        // 4. 计算对象在slab中的索引
        objnr = (objp - slabp->s_mem) / cachep->objsize;
        check_slabp(cachep, slabp);

        // 5. 将slab中下一个空闲对象的索引存入对象描述符中
        //    下一个空闲对象是`objnr`。
        //    (最后一个释放的对象,将是第一个待分配的对象)
        slab_bufctl(slabp)[objnr] = slabp->free;
        slabp->free = objnr;
        STATS_DEC_ACTIVE(cachep);
        // 6. 对象已经恢复空闲
        slabp->inuse--;
        check_slabp(cachep, slabp);

        /* */
        if (slabp->inuse == 0) {
            /* 7 如果slab中所有对象都是空闲的(inuse=0),且
             *   整个slab缓存中空闲对象的数量大于cache上限,则
             *   将多余的空闲对象占用的slab页帧释放回`ZONE`页帧分配器>
             *   
             *   cachep->free_limit == cachep->num+ (1+N) × cachep->batchcount
             *   此处的N是系统中CPU核的数量。
             */
            if (cachep->lists.free_objects > cachep->free_limit) {
                cachep->lists.free_objects -= cachep->num;
                slab_destroy(cachep, slabp);
            } else {
                /* 8. 如果slab中所有对象都是空闲的(inuse=0),但整个slab缓存
                 *    中的空闲对线小于等于cachep->free_limit,则将slab描述符
                 *    插入到cachep->lists.slabs_free列表中
                 */
                list_add(&slabp->list,
                &list3_data_ptr(cachep, objp)->slabs_free);
            }
        } else {
            /* 9. 如果inuse大于0,说明slab部分被用,所以将slab描述符插入到
             *    cachep->lists.slabs_partial列表中。
             *    无条件的将一个slab移动到slabs_partial列表的末尾,
             *    这也是释放其它的最大时间。
             */
            list_add_tail(&slabp->list,
                &list3_data_ptr(cachep, objp)->slabs_partial);
        }
    }
}

14 通用对象

在前面通用和特殊缓存一节中,对内存不频繁的请求是通过一组通用缓存实现的,这些通用缓存对象的大小是按照几何大小均匀分布的,从32→131072个字节。

这类对象是通过kmalloc()实现的,约等于下面的代码:

    void * kmalloc(size_t size, int flags)
    {
        struct cache_sizes  *csizep = malloc_sizes;
        kmem_cache_t        *cachep;

        for (; csizep->cs_size; csizep++) {
            if (size > csizep->cs_size)
                continue;
            if (flags & __GFP_DMA)
                cachep = csizep->cs_dmacachep;
            else
                cachep = csizep->cs_cachep;
            return kmem_cache_alloc(cachep, flags);
        }
        return NULL;
    }

该函数借助malloc_sizes表锁定最接近请求内存大小的2的幂次方对应的cache描述符表。然后调用kmem_cache_alloc()分配对象,可以传递给DMA内存的缓存描述符,也可以是普通内存的缓存描述符,取决于调用者是否传递了__GFP_DMA标志。

对应的释放通用缓存对象的函数是kfree():

    void kfree(const void *objp)
    {
        kmem_cache_t    *c;
        unsigned long   flags;

        if (!objp)
            return;
        local_irq_save(flags);
        c = (kmem_cache_t *)(virt_to_page(objp)->lru.next);
        kmem_cache_free(c, (void *)objp);
        local_irq_restore(flags);
    }

正确的缓存描述符是通过读取包含内存区域的第1页帧的描述符的lru.next字段来确定的。通过调用kmem_cache_free()释放内存区域。

15 内存池

内存池是Linux v2.6内核的一个新特性。基本上,内存池允许内核子系统(比如块设备子系统)分配一些动态内存,仅在内存不足的紧急情况下使用。

首先我们不应该将内存池(memory pool)和预留页帧池混淆。

实际上,这些预留页帧只能用于满足中断处理程序或关键代码区发出的原子内存分配请求。相反,内存池是动态内存的储备,只能由特定的内核组件(即内存池的“所有者”)使用。内核组件通常不使用这些预留的动态内存池;但是,如果动态内存变得稀缺时,以至于所有正常的内存分配请求都注定要失败,内核组件可以调用特殊的内存池函数,作为最后的手段,可以从内存池预留获得所需的内存。

通常,内存池也是基于slab分配器申请的,也就是说,内存池就是预留了一些slab对象。但是,内存池可以分配任何类型的动态内存,从整个的页帧到非常小的内存块都可以。因此,我们一般将内存池处理的内存单元称为内存元素。

内存池用mempool_t数据结构表示,各个字段如下表所示:

类型 名称 描述
spinlock_t lock 保护对象的自旋锁
int min_nr 内存池的最大数量
int curr_nr 内存池的当前数量
void ** elements 指向内存池中内存元素指针的数组的指针
void * pool_data 内存池所有者的私有数据
mempool_alloc_t * alloc 分配一个内存池元素的方法
mempool_free_t * free 释放一个内存池元素的方法
wait_queue_head_t wait 内存池为空时使用的等待队列

min_nr存储内存池中元素的初始数量。换句话说,存储在此字段中的值表示内存池的所有者肯定会从内存分配器获得的内存元素的数量。curr_nr总是小于或等于min_nr,它存储当前包含在内存池中的内存元素的数量。内存元素本身由一个指针数组引用,其地址存储在“elements”字段中。

alloc和free方法分别与底层内存分配器交互以获取和释放内存元素。这两种方法都可以是由拥有内存池的内核组件提供的自定义函数。

当内存元素是slab对象时,alloc和free方法通常由mempool_alloc_slab()和mempool_free_slab()函数实现,它们分别调用kmem_cache_alloc()和kmem_cache_free()函数。在这种情况下,mempool_t对象的pool_data字段存储slab缓存描述符的地址。

当内存元素是自定义数据对象时,alloc和free方法通常由kmalloc和kfree函数实现。分配的通用缓存对象。如下所示:

    static void *pkt_rb_alloc(int gfp_mask, void *data)
    {
        return kmalloc(sizeof(struct pkt_rb_node), gfp_mask);
    }

mempool_create()创建一个新的内存池:它接收的参数是min_nr,alloc和free函数的地址,及pool_data。该函数为mempool_t对象和指向内存元素的指针数组分配内存,然后重复调用alloc方法来获取min_nr内存元素。相反,mempool_destroy()函数释放池中的所有内存元素,然后释放元素数组和mempool_t对象本身。

为了从内存池中分配一个元素,内核调用mempool_alloc()函数,传递给它mempool_t对象的地址和内存分配标志。本质上,该函数根据指定的内存分配标志,通过调用alloc方法,尝试从底层内存分配器分配内存元素。如果分配成功,该函数返回获得的内存元素,而不触及内存池。否则,如果分配失败,则从内存池中取一个内存元素。当然,在内存不足的情况下,太多的内存分配请求会耗尽内存池;在这种情况下,如果没有设置__GFP_WAIT标志,mempool_alloc()会阻塞当前进程,直到内存元素被释放到内存池中。

相反,要将元素释放到内存池中,内核调用mempool_free()函数。如果内存池未满(curr_min小于min_nr),则该函数将元素添加到内存池中。否则,mempool_free()调用free方法将元素释放到底层内存分配器。

审核编辑:汤梓红

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 内核
    +关注

    关注

    3

    文章

    1309

    浏览量

    39846
  • Linux
    +关注

    关注

    87

    文章

    10991

    浏览量

    206736
  • 分配器
    +关注

    关注

    0

    文章

    176

    浏览量

    25293
  • 函数
    +关注

    关注

    3

    文章

    3868

    浏览量

    61309

原文标题:Linux内核8.7-内存管理之slab分配器

文章出处:【微信号:嵌入式ARM和Linux,微信公众号:嵌入式ARM和Linux】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Linux内核内存管理之ZONE内存分配器

    内核中使用ZONE分配器满足内存分配请求。该分配器必须具有足够的空闲页帧,以便满足各种内存大小请
    的头像 发表于 02-21 09:29 417次阅读

    Linux内存系统: Linux 内存分配算法

    表项也会相应的更新6、slab 算法——基本原理1) 基本概念· Linux 所使用的 slab 分配器的基础是 Jeff Bonwick 为 SunOS 操作系统首次引入的一种算法·
    发表于 08-24 07:44

    深入细节的详解,嵌入式必懂知识Linux内存管理

    前面说的段页管理机制算是虚拟空间的部分,然而linux内存管理的另外一个重要部分就是物理内存管理
    发表于 08-28 10:34

    内核内存是如何进行分配

    嵌入式LINUX驱动学习12内核内存分配一、头文件、函数及说明:一、头文件、函数及说明://头文件位置 : include/
    发表于 12-17 06:44

    关于RTT支持的内存分配算法

    的融合。 最原始的SLAB算法是Jeff Bonwick为Solaris 操作系统而引入的一种高效内核内存分配算法。 RT-Thread的SLAB
    发表于 04-27 14:40

    关于RTT支持的内存分配算法

    的融合。 最原始的SLAB算法是Jeff Bonwick为Solaris 操作系统而引入的一种高效内核内存分配算法。 RT-Thread的SLAB
    发表于 04-27 14:42

    VGA分配器,VGA分配器是什么意思

    VGA分配器,VGA分配器是什么意思 VGA分配器的概念:   VGA分配器是将计算机或其它VGA输出信号分配至多个VGA显示设备或投影显
    发表于 03-26 09:59 2301次阅读

    分配器,什么是分配器

    分配器,什么是分配器 将一路微波功率按一定比例分成n路输出的功率元件称为功率分配器。按输出功率比例不同, 可分为等功率分配器和不等功率
    发表于 04-02 13:48 2577次阅读
    <b class='flag-5'>分配器</b>,什么是<b class='flag-5'>分配器</b>

    linux内存管理中的SLAB分配器详解

    管理区页框分配器,这里我们简称为页框分配器,在页框分配器中主要是管理物理内存,将物理
    发表于 05-17 15:01 1926次阅读
    <b class='flag-5'>linux</b><b class='flag-5'>内存</b><b class='flag-5'>管理</b>中的<b class='flag-5'>SLAB</b><b class='flag-5'>分配器</b>详解

    深入剖析SLUB分配器SLAB分配器的区别

    首先为什么要说slub分配器内核里小内存分配一共有三种,SLAB/SLUB/SLOB,slub分配器
    发表于 05-17 16:05 872次阅读
    深入剖析SLUB<b class='flag-5'>分配器</b>和<b class='flag-5'>SLAB</b><b class='flag-5'>分配器</b>的区别

    Linux内核深度解析》之内存地址空间

    内核空间提供了把页划分成小内存分配的块分配器,提供分配内存的接口 kmalloc()和释放
    的头像 发表于 07-15 14:22 1920次阅读

    bootmem分配器使用的数据结构

    内核初始化的过程中需要分配内存内核提供了临时的引导内存分配器,在页
    的头像 发表于 07-22 11:18 1147次阅读

    Linux内核之伙伴分配器

    内核初始化完毕后,使用页分配器管理物理页,当前使用的页分配器是伙伴分配器,伙伴分配器的特点是算法
    的头像 发表于 07-25 14:06 1330次阅读

    Linux内核之块分配器

    为了解决小块内存分配问题,Linux 内核提供了块分配器,最早实现的块分配器
    的头像 发表于 07-27 09:35 1250次阅读

    Linux内核引导内存分配器的原理

    Linux内核引导内存分配器使用的是伙伴系统算法。这种算法是一种用于动态内存分配的高效算法,它将
    发表于 04-03 14:52 246次阅读