【Linux 内核源码分析】内存管理——Slab 分配器

这篇具有很好参考价值的文章主要介绍了【Linux 内核源码分析】内存管理——Slab 分配器。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Slab 分配器

在Linux内核中,伙伴分配器是一种内存管理方式,以页为单位进行内存的管理和分配。但是在内核中,经常会面临结构体内存分配问题,而这些结构体的大小通常是小于一页的。如果使用伙伴分配器来分配这些小内存,将造成很大的内存浪费。因此,为了解决这个问题,Sun公司的Jeff Bonwick在Solaris 2.4中设计并实现了一种新的内存分配器——slab分配器。

与伙伴分配器不同的是,slab 分配器以字节为单位来分配内存,并基于伙伴分配器的大内存进一步细分成小内存进行分配。也就是说,slab 分配器仍然从 Buddy 分配器中申请内存,但是会针对小块内存做出更优化的处理。

除了提供小内存外,slab 分配器的另一个任务是维护常用对象的缓存。在内核中有很多常用结构,它们的初始化对象所需的时间可能等于或超过为其分配空间的成本。当创建一个新的slab时,许多对象将被打包到其中并使用构造函数(如果有)进行初始化。释放对象后,它会保持其初始化状态,这样可以快速分配对象。这样可以提高内存的利用率,并加快内核的运行速度。

SLAB分配器的另一个重要任务是提高CPU硬件缓存的利用率。当将对象包装到SLAB中后,如果还有剩余空间,SLAB会使用这些剩余空间来进行着色。SLAB着色是一种策略,旨在使不同SLAB中的对象使用CPU硬件缓存中的不同行,以减少相互刷新的可能性。通过在SLAB中放置对象的不同起始偏移位置,可以让对象在CPU缓存中使用不同的行,从而提高缓存的利用率。

通过这种方式,原本被浪费的空间可以得到有效利用,进一步优化了内存管理的效率。

通过运行命令sudo cat /proc/slabinfo,可以查看系统当前的SLAB使用情况。以vm_area_struct结构体为例,系统已经分配了13014个vm_area_struct缓存,每个大小为216字节,其中有12392个是活跃的。这些信息可以帮助我们了解系统中SLAB的分配情况,以及对内存的使用情况进行监控和调优。

其中,通过cat /proc/slabinfo命令输出的内容包括了slab的名称、活跃对象数、总对象数、对象大小、每slab对象数、每页slab数等信息。比如,vm_area_structmm_struct等是专用slab,而kmalloc-xxx是通用slab。

在Linux内核中,区分通用和专用slab的一个重要原因是为了避免内存浪费。通用slab由于为了适应不同大小的对象而采用了固定大小的管理方式,当需要分配介于两个固定大小之间的对象时,就会导致内存浪费。

Slab API

Slab分配器提供的API包括kmalloc()kfree(),类似于libc提供的mallocfree函数。在Linux内核中,kmalloc()函数定义在include/linux/slab.h文件中,接收两个参数:size表示申请的内存大小,flags用于指定内存分配的具体内存域,如GFP_DMA用于指定适合DMA的内存区域。

kmalloc()函数实际上会调用__kmalloc()函数,而最终的内存分配实现则是在__do_kmalloc()函数中完成。__kmalloc()函数接受size和flags两个参数,并将它们传递给__do_kmalloc()函数进行内存分配操作。

__do_kmalloc()函数中,根据传入的size和flags参数执行具体的内存分配操作。通过这一系列函数调用,Slab分配器可以根据指定的大小和内存域进行内存分配,以满足不同场景下对内存的需求。

实现原理

Slab分配器是Linux内核中用于管理对象分配与释放的内存分配器,它通过使用内存缓存来提高性能。内存缓存是从伙伴系统(buddy system)中获取的物理内存,用于存储slab分配器管理的对象。

在Slab分配器中,使用结构体struct kmem_cache来描述内存缓存的属性和状态。这个结构体定义在include/linux/slab_def.h文件中,包含了管理slab分配器所需的各种信息,如缓存大小、对象大小、分配算法等。通过这个结构体,Slab分配器可以更有效地管理内存缓存,提高内存分配与释放的效率,并减少碎片化的问题。

struct kmem_cache {
    slab_flags_t flags;             /* 常量标志 */
    unsigned int num;               /* 每个 slab 中的对象数量 */
    unsigned int gfporder;          /* 每个 slab 所使用的页框数量的阶数 */
    gfp_t allocflags;               /* 强制指定的 GFP 标志,例如 GFP_DMA */
    size_t colour;                  /* 缓存着色范围 */
    unsigned int colour_off;        /* 缓存颜色的偏移量 */
    struct kmem_cache *freelist_cache;   /* 管理空闲对象列表的 kmem_cache 缓存 */
    unsigned int freelist_size;     /* 空闲列表的大小,表示当前空闲对象的数量 */
    const char *name;               /* 内存缓存的名称 */
    struct list_head list;          /* 将不同的内存缓存连接成链表 */
    int refcount;                   /* 引用计数,记录当前内存缓存的引用次数 */
    int object_size;                /* 对象的大小 */
    int align;                      /* 对象在内存中的对齐方式 */
    struct kmem_cache_node *node[MAX_NUMNODES];    /* 指向 kmem_cache_node 结构的数组 */
};

SLAB分配器由多个缓存组成,这些缓存通过双向循环链表(即缓存链)链接在一起。在SLAB分配器的上下文中,缓存是用于管理特定类型多个对象的数据结构,例如mm_struct或fs_cache缓存,在kmem_cache->name中保存了它们的名称。在Linux中,单个最大的SLAB缓存大小为32MB。在一个kmem_cache中,所有对象的大小都相同(object_size),而且所有SLAB的大小也相同(gfpordernum)。

每个缓存节点在内存中维护一组称为slab的连续页块,这些页块被切割成小块,用于存储数据结构和对象。kmem_cache结构体中的kmem_cache_node成员记录了该kmem_cache下的所有slabs列表。

kmem_cache_node 结构体定义如下,位于 mm/slab.h 文件中:

struct kmem_cache_node {
    spinlock_t list_lock;  /* 用于保护 slabs_partial、slabs_full、slabs_free 等链表的自旋锁 */

#ifdef CONFIG_SLAB
    struct list_head slabs_partial;  /* 部分使用的 slab 列表,放在第一个位置以获得更好的汇编代码 */
    struct list_head slabs_full;     /* 完全使用的 slab 列表 */
    struct list_head slabs_free;     /* 空闲的 slab 列表 */
    unsigned long total_slabs;       /* 所有 slab 列表的总长度 */
    unsigned long free_slabs;        /* 空闲 slab 列表的长度 */
    unsigned long free_objects;      /* 空闲对象的数量 */
    ...
#endif
};

kmem_cache_node 结构体中包含了用于保护不同链表的自旋锁以及用于管理不同类型的 slab 的链表和计数信息。

kmem_cache_node 结构体记录了三种类型的 slab:

  1. slabs_full: 已完全分配的 slab
  2. slabs_partial: 部分分配的 slab
  3. slabs_free: 空闲的 slab,或者没有对象被分配的 slab

这三个链表保存的是 slab 的描述符,在Linux内核中使用struct page来描述一个 slab。单个 slab 可以在这些 slab 链表之间移动,例如当一个半满的 slab 被分配了对象后变满了,它将从slabs_partial中被移除,并插入到slabs_full中去。

struct page

struct page 结构体定义在 include/linux/mm_types.h 文件中,其中与 slab 相关的结构成员如下所示:

struct page {
    union {
        struct {  /* 页面缓存和匿名页面 */
            ...
        };

        struct {  /* slab、slob 和 slub */
            union {
                struct list_head slab_list;   /* 用于连接属于同一个 slab 的所有页 */
                struct {                      /* 部分页面 */
                    struct page *next;        /* 指向下一个部分页面 */

                    int pages;                /* 剩余页面数量 */
                    int pobjects;             /* 大约对象数量 */
                };
            };
            struct kmem_cache *slab_cache;   /* 当前 slab 所属的 kmem_cache 结构 */
            /* 双字边界 */
            void *freelist;                  /* 第一个空闲对象 */
            union {
                void *s_mem;                /* slab: 第一个对象 */
                unsigned long counters;     /* SLUB */
                ...
            };
        };
        ...
    };
};

struct page 中有一个匿名联合体,其中有一个与 slab 相关的结构体成员。在这个成员中,slab_cache 指向当前 slab 所属的 kmem_cache 结构体。freelist 指向第一个空闲对象,而 s_mem 则指向 slab 中第一个对象的地址。

对于部分页面,pages 表示剩余页面的数量,pobjects 则表示大约对象的数量。对于 slab 页面,slab_list 用于连接属于同一个 slab 的所有页。

void *s_mem: 指向该页框中第一个对象的地址。

struct kmem_cache *slab_cache: 用于追踪所有页面的链表,指向当前slab所属的kmem_cache结构体。

struct list_head slab_list: 用于标识此页框属于哪个slab链表(full、free、partial),即使用此成员将list串联起来。

void *freelist: 指向页框中空闲对象链表。空闲对象链表包含页框中每个空闲对象的索引。

空闲对象链表是一个由数组制成的简单链表,它可以保存在两个地方:

  1. 保存在外部,会从SLAB中分配一个对象用于保存新的SLAB的空闲对象链表。
  2. 保存在内部,即保存在这个SLAB所代表的连续页框的头部。

Slab缓存的建立过程可以分为以下几个步骤:

  1. 首先定义全局变量kmem_cache_boot作为第一个kmem_cache结构体实例,其中包含了一些基本的属性设置,例如batchcountlimitsharedsizename等。

  2. 在系统初始化过程中,通过kmem_cache_init()函数对kmem_cache进行初始化。在这个过程中,首先将kmem_cache指向kmem_cache_boot,并初始化 NUM_INIT_LISTS 个 kmem_cache_node 结构体。

  3. 调用create_boot_cache()函数对kmem_cache_boot进行进一步初始化,计算并申请内存空间以存储kmem_cache结构体中的node数组。然后将kmem_cache加入slab_caches链表,并将slab_state设置为PARTIAL。

  4. 创建kmalloc_caches数组,该数组是一个struct kmem_cache *的二维数组指针,用于存储不同大小的kmem_cache。根据kmalloc_info中保存的信息,调用create_kmalloc_cache()函数生成记录kmem_cache_nodekmem_cache

  5. create_kmalloc_cache()函数首先从kmem_cache中申请一个slab用来保存记录struct kmem_cache_node实例的kmem_cache,然后再调用create_boot_cache()函数对该kmem_cache进行初始化,最后将其添加到slab_caches链表中。

Slab缓存的建立过程主要包括初始化全局的kmem_cache_boot变量、对kmem_cache进行初始化、创建kmalloc_caches数组并生成不同大小的kmem_cache,以及将这些kmem_cache添加到slab_caches链表中。这个过程保证了系统在运行时能够高效地管理内存分配和释放。

Slab缓存建立过程

Slab缓存是一种用于管理内核内存分配的技术。它通过将内存分成大小相等的块,并将这些块组织成不同的缓存,以便更高效地分配和释放内存。建立Slab缓存的过程可以分为几个步骤。第一个步骤是定义全局变量kmem_cache_boot,这是一个kmem_cache结构体实例,它在mm/slab.c中被定义。这个全局变量的定义标志着Slab缓存的建立过程的开始。

/*
 * 定义一个静态的kmem_cache结构体变量kmem_cache_boot,并初始化其字段
 */

static struct kmem_cache kmem_cache_boot = {
    .batchcount = 1,                                // 每次分配的块数为1
    .limit = BOOT_CPUCACHE_ENTRIES,                  // 限制每个CPU缓存池中的块数量
    .shared = 1,                                     // 允许不同CPU核心共享该缓存池
    .size = sizeof(struct kmem_cache),              // 存储的块大小为kmem_cache结构体的大小
    .name = "kmem_cache",                            // 缓存的名称为"kmem_cache"
};

在初始化过程中,kmem_cache_init()函数通过调用create_boot_cache()函数进一步初始化了kmem_cache_boot结构体。第一个kmem cache实例的作用是为创建其他kmem cache实例分配空间,其大小与系统内的node数量有关。通过使用offsetof运算符来求出kmem_cache结构体中node成员的偏移地址,然后再加上系统内node的个数乘以sizeof(struct kmem_cache_node *),从而实现最大程度地节省内存。这样做的原因是,kmem_cache结构体中的node成员是一个数组,其大小用最大的node数定义,但实际上只会根据系统内实际的node数量来申请内存,以避免浪费内存资源。

void __init kmem_cache_init(void)
{
    int i;

    kmem_cache = &kmem_cache_boot;

    // NUM_INIT_LISTS 为 2 * 系统内的节点数量
    // 初始化 kmem_cache_node 结构体
    for (i = 0; i < NUM_INIT_LISTS; i++)
        kmem_cache_node_init(&init_kmem_cache_node[i]);
    // 完成第一个kmem cache实例kmem_cache的初始化
    // 第一个kmem cache实例用于为创建其他kmem cache实例分配空间
    create_boot_cache(kmem_cache, "kmem_cache",
        offsetof(struct kmem_cache, node) +
                  nr_node_ids * sizeof(struct kmem_cache_node *),
                  SLAB_HWCACHE_ALIGN, 0, 0);
    // kmem cache实例加入slab_caches链表
    list_add(&kmem_cache->list, &slab_caches);
    slab_state = PARTIAL;

    kmalloc_caches[KMALLOC_NORMAL][INDEX_NODE] = create_kmalloc_cache(
                kmalloc_info[INDEX_NODE].name[KMALLOC_NORMAL],
                kmalloc_info[INDEX_NODE].size,
                ARCH_KMALLOC_FLAGS, 0,
                kmalloc_info[INDEX_NODE].size);
    slab_state = PARTIAL_NODE;
    setup_kmalloc_cache_index_table();

    slab_early_init = 0;

    /* 5) 替换引导程序 kmem_cache_node */
    {
        int nid;

        for_each_online_node(nid) {
            init_list(kmem_cache, &init_kmem_cache_node[CACHE_CACHE + nid], nid);

            init_list(kmalloc_caches[KMALLOC_NORMAL][INDEX_NODE],
                      &init_kmem_cache_node[SIZE_NODE + nid], nid);
        }
    }

    create_kmalloc_caches(ARCH_KMALLOC_FLAGS);
}

首先我们看到了一个名为slab_caches的链表头,在文件mm/slab_common.c中通过宏LIST_HEAD(slab_caches)进行定义。在kmem_cache初始化后,每个kmem_cache都会通过list_add函数添加到这个链表中。

接下来,我们看到了kmalloc_caches,它是一个指向struct kmem_cache *类型的二维数组指针,在mm/slab_common.c中定义。它使用kmalloc_info中保存的信息来生成不同大小的kmem_cachekmalloc_info的定义类似于一个映射,比如其中的一个元素是{“kmalloc-96”, 96},其中"kmalloc-96"是kmem_cache的名称,96是对象的大小。在这里调用create_kmalloc_cache函数是为了创建一个记录kmem_cache_nodekmem_cache(对INDEX_NODE展开为INDEX_NODE kmalloc_index(sizeof(struct kmem_cache_node)))。

主要是在初始化阶段处理了kmem_cachekmalloc_caches的初始化工作,以及它们与链表slab_caches的关联。

#define INIT_KMALLOC_INFO(__size, __short_size)            \
{                                \
    .name[KMALLOC_NORMAL]  = "kmalloc-" #__short_size,    \   // 设置 KMALLOC_NORMAL 下的 name 字段
    .name[KMALLOC_RECLAIM] = "kmalloc-rcl-" #__short_size,    \   // 设置 KMALLOC_RECLAIM 下的 name 字段
    .size = __size,                        \   // 设置 size 字段
}

const struct kmalloc_info_struct kmalloc_info[] __initconst = {
    INIT_KMALLOC_INFO(0, 0),        // 初始化对象大小为0的kmalloc信息
    INIT_KMALLOC_INFO(96, 96),      // 初始化对象大小为96的kmalloc信息
    INIT_KMALLOC_INFO(192, 192),    // 初始化对象大小为192的kmalloc信息
    INIT_KMALLOC_INFO(8, 8),        // 初始化对象大小为8的kmalloc信息
    ...
    INIT_KMALLOC_INFO(67108864, 64M)   // 初始化对象大小为64M的kmalloc信息
};

create_kmalloc_cache()函数首先调用函数kmem_cache_zalloc(kmem_cache, GFP_NOWAIT)从kmem_cache中申请一个slab,用来保存记录struct kmem_cache_node实例的kmem_cache。然后再调用create_boot_cache()函数对从kmem_cache slab中申请来的kmem_cache进行初始化,并将其添加到slab_caches链表中。

// 创建一个用于管理特定大小对象的kmem_cache,并将其添加到slab_caches链表中
struct kmem_cache *__init create_kmalloc_cache(const char *name,
        unsigned int size, slab_flags_t flags,
        unsigned int useroffset, unsigned int usersize)
{
    // 从kmem_cache中申请一个slab,用来保存记录struct kmem_cache_node实例的kmem_cache
    struct kmem_cache *s = kmem_cache_zalloc(kmem_cache, GFP_NOWAIT);
    // 使用从kmem_cache slab中申请来的内存初始化kmem_cache
    create_boot_cache(s, name, size, flags, useroffset, usersize);
    // 将初始化后的kmem_cache添加到slab_caches链表中
    list_add(&s->list, &slab_caches);
    // 设置kmem_cache的引用计数为1
    s->refcount = 1;
    return s; // 返回创建好的kmem_cache
}

在文件kernel/fork.c中的__init proc_caches_init(void)函数中,进行了vm_area_struct结构体的初始化。以下是相应代码片段:

// 初始化用于管理不同结构体对象的专用slab缓存
void __init proc_caches_init(void)
{
    unsigned int mm_size;

    // 创建用于管理struct sighand_struct对象的kmem_cache
    sighand_cachep = kmem_cache_create("sighand_cache",
            sizeof(struct sighand_struct), 0,
            SLAB_HWCACHE_ALIGN | SLAB_PANIC | SLAB_TYPESAFE_BY_RCU | SLAB_ACCOUNT, sighand_ctor);

    // 创建用于管理struct signal_struct对象的kmem_cache
    signal_cachep = kmem_cache_create("signal_cache",
            sizeof(struct signal_struct), 0,
            SLAB_HWCACHE_ALIGN | SLAB_PANIC | SLAB_ACCOUNT, NULL);

    // 创建用于管理struct files_struct对象的kmem_cache
    files_cachep = kmem_cache_create("files_cache",
            sizeof(struct files_struct), 0,
            SLAB_HWCACHE_ALIGN | SLAB_PANIC | SLAB_ACCOUNT, NULL);

    // 创建用于管理struct fs_struct对象的kmem_cache
    fs_cachep = kmem_cache_create("fs_cache",
            sizeof(struct fs_struct), 0,
            SLAB_HWCACHE_ALIGN | SLAB_PANIC | SLAB_ACCOUNT, NULL);

    // 创建用于管理vm_area_struct对象的kmem_cache
    vm_area_cachep = KMEM_CACHE(vm_area_struct, SLAB_PANIC | SLAB_ACCOUNT);
}

在该函数中,调用了kmem_cache_create()函数创建了一个名为"VM area"的kmem_cache,用于管理vm_area_struct结构体对象。kmem_cache_create()函数的参数依次为:缓存名称、对象大小、对齐方式、分配标志和构造函数。

这样,通过调用proc_caches_init()函数,就可以初始化和创建用于管理vm_area_struct结构体对象的专用slab缓存。

在初始化其他通用的kmem_cache(名称为kmalloc-xxx)时,会调用函数new_kmalloc_cache(),该函数定义在mm/slab_common.c文件中。这个函数的作用是专门用于创建和初始化以"kmalloc-"开头命名的kmem_cache实例。在这个函数中,会根据传入的参数动态地生成对应的kmem_cache实例,并进行相应的初始化工作,包括设置适当的缓存大小、块数量等属性,以便后续可以更高效地管理内存分配。

/*
 * 创建和初始化以"kmalloc-"开头命名的kmem_cache实例
 */
static void __init new_kmalloc_cache(int idx, enum kmalloc_cache_type type, slab_flags_t flags)
{
    // 如果类型为KMALLOC_RECLAIM,则设置标志位SLAB_RECLAIM_ACCOUNT
    if (type == KMALLOC_RECLAIM)
        flags |= SLAB_RECLAIM_ACCOUNT;

    // 调用create_kmalloc_cache函数创建kmem_cache实例,并进行初始化
    kmalloc_caches[type][idx] = create_kmalloc_cache(
                    kmalloc_info[idx].name[type],      // 实例的名称
                    kmalloc_info[idx].size,            // 实例中存储块的大小
                    flags,                             // 实例的标志位
                    0,                                 // 批量分配的块数
                    kmalloc_info[idx].size);           // 实例的大小
}

Slab缓存分配过程

kmem_cache_zalloc()函数实现了从slab中分配内存的功能。其调用流程大致如下:

// 申请并清零一个内存缓存对象
void *kmem_cache_zalloc(struct kmem_cache *s)
{
    // 调用kmem_cache_alloc函数分配一个内存缓存对象
    void *obj = kmem_cache_alloc(s, GFP_KERNEL);

    if (obj)
        memset(obj, 0, s->size); // 清零分配到的内存

    return obj;
}

// 分配一个内存缓存对象
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t flags)
{
    // 调用slab_alloc函数进行内存分配
    return slab_alloc(s, flags);
}

// 执行具体的内存分配操作
static void *__do_cache_alloc(struct kmem_cache *cachep,
                            gfp_t flags)
{
    struct page *page;
    void *object;

    // 获取当前CPU的缓存
    struct kmem_cache_cpu *c = cpu_cache_get(cachep);

    // 检查当前CPU的缓存是否为空,如果为空则重新填充该缓存
    if (!c->freelist) {
        cache_alloc_refill(cachep, flags); // 填充CPU缓存
        if (!c->freelist)
            return NULL; // 如果依然没有可用空闲对象,则返回NULL表示失败
    }

    // 从当前CPU的缓存中获取一个空闲对象
    object = c->freelist;
    
	// 更新当前CPU的缓存状态信息
	c->freelist = *(void **)object;
	c->tid = current->tid;

    // 返回分配到的内存对象
    return object;
}

// 获取当前CPU的缓存
static struct kmem_cache_cpu *cpu_cache_get(struct kmem_cache *cachep)
{
    int cpu = get_cpu();
    struct kmem_cache_node *n = get_node(cachep, cpu);
    
	// 返回当前CPU对应的缓存
	return per_cpu_ptr(n->cpu_slab, cpu);
}

// 填充CPU缓存
static void cache_alloc_refill(struct kmem_cache *cachep,
                            gfp_t flags)
{
	// 具体的填充操作省略...
}

____cache_alloc() 函数中,会先搜索当前 CPU 对应的 array_cache 链表,如果有可用的空闲对象,则直接返回该对象。否则,函数将调用 cache_alloc_refill() 函数尝试从三个不同状态的 slab(即 slabs_freeslabs_partialslabs_full)中寻找可用的内存,并把它们填充到 array_cache 中,再次进行分配。

// 分配一个内存缓存对象
static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
    void *objp;
    struct array_cache *ac;
    
    // 获取当前CPU的缓存
    ac = cpu_cache_get(cachep);

    // 检查CPU缓存中是否有可用的对象
    if (likely(ac->avail)) {
        ac->touched = 1; // 标记缓存已使用过
        objp = ac->entry[--ac->avail]; // 从缓存中获取一个对象

        STATS_INC_ALLOCHIT(cachep); // 增加分配命中统计次数
        goto out; // 跳转到out标签处,表示分配成功
    }

    STATS_INC_ALLOCMISS(cachep); // 增加分配未命中统计次数
    
	// 重新填充CPU缓存,并返回分配到的内存对象
	objp = cache_alloc_refill(cachep, flags);

	// 再次获取CPU缓存,可能已被重新填充
	ac = cpu_cache_get(cachep);

out:
    if (objp)
        kmemleak_erase(&ac->entry[ac->avail]); // 清除kmemleak相关信息

    return objp; // 返回分配到的内存对象
}

struct array_cache 本地 CPU 空闲对象链表

为了提高性能,kmem_cache 实现了一种名为 array_cache 的本地 CPU 空闲对象链表机制。具体来说,对于每个 CPU,都会在其本地空间维护一个针对 kmem_cachearray_cache 链表,用于缓存常用的对象。

kmem_cache 结构体中,有一个成员 struct array_cache __percpu *cpu_cache,用来为每个 CPU 维护本地的空闲对象链表。这样,当需要申请内存时,可以优先尝试从当前 CPU 的本地 array_cache 中获取相应大小的对象,从而提高硬件缓存的命中率,减少锁的竞争,进一步提高系统的性能。

在Linux内核的mm/slab.c文件中定义了一个名为struct array_cache的结构体。该结构体用于表示一个数组缓存,用于高效地管理对象的分配和释放。

struct array_cache的定义如下:

struct array_cache {
  unsigned int avail;
  unsigned int limit;
  unsigned int batchcount;
  unsigned int touched;
  void *entry[];
};
  • avail:表示当前可用的对象数量。
  • limit:表示数组缓存的容量上限,即最大可以容纳的对象数量。
  • batchcount:表示每次从数组缓存中获取对象的数量。
  • touched:表示数组缓存被访问的次数,用于性能统计和优化。
  • entry[]:这是一个柔性数组成员,用于存储实际的对象数据。

通过使用struct array_cache,可以方便地维护对象的数量、容量和访问统计等信息,从而在内存分配过程中提高效率和性能。

系统初始化后,本地CPU空闲对象链表是一个空链表,只有在释放对象时才会将对象加入其中。该链表有一个限制,即最大容量为limit。当链表中的对象数量超过这个限制时,会将batchcount个对象移动到所有CPU共享的空闲对象链表中。

所有 CPU 共享的空闲对象链表

另外,所有CPU共享的空闲对象链表是通过struct kmem_cache_node结构体中的一个array_cache成员struct array_cache shared;实现的。这个共享的缓存用于存储所有CPU共享的空闲对象。

在常规的对象申请流程中,内核首先会从本地CPU空闲对象链表中尝试获取一个对象进行分配。如果失败,则会检查所有CPU共享的空闲对象链表中是否存在对象,并且检查链表中是否有空闲对象。如果有,就会将batchcount个空闲对象转移回本地CPU空闲对象链表中。

如果上述步骤仍然失败,内核会尝试从SLAB中进行分配。如果仍然失败,kmem_cache会尝试从页框分配器中获取一组连续的页框,并建立一个新的SLAB,然后从新的SLAB中获取一个对象。

当对象需要释放时,首先会将对象释放到本地CPU空闲对象链表中。如果本地CPU空闲对象链表中的对象数量过多,kmem_cache会将本地CPU空闲对象链表中的batchcount个对象移动到所有CPU共享的空闲对象链表中。如果所有CPU共享的空闲对象链表中的对象数量也超过了限制,kmem_cache会将batchcount个对象移回它们所属的SLAB中。

如果SLAB中的空闲对象数量太多,kmem_cache会整理出一些空闲的SLAB,并将这些SLAB所占用的页框释放回页框分配器中。

array_cache 填充

cache_alloc_refill()函数实现如下:

// 重新填充缓存中的对象
static void *cache_alloc_refill(struct kmem_cache *cachep, gfp_t flags)
{
    int batchcount;  // 批量分配的数量
    struct kmem_cache_node *n;  // 内存缓存节点
    struct array_cache *ac, *shared;  // CPU缓存、共享缓存
    int node;  // 节点索引
    void *list = NULL;  // 空闲对象链表
    struct page *page;  // 内存页

    ac = cpu_cache_get(cachep);  // 获取当前CPU缓存
    batchcount = ac->batchcount;  // 获取批量分配数量

	// 如果CPU缓存没有被访问过且批量分配数量超过阈值,则将批量分配数量限制为阈值
	if (!ac->touched && batchcount > BATCHREFILL_LIMIT) {
        batchcount = BATCHREFILL_LIMIT;
    }

	// 根据节点索引获取内存缓存节点
	n = get_node(cachep, node);

	BUG_ON(ac->avail > 0 || !n);  // 检查CPU缓存是否有可用对象,以及内存缓存节点是否存在

	shared = READ_ONCE(n->shared);  // 获取共享缓存

	// 如果没有空闲对象,并且没有共享空闲对象可分配,则跳转到direct_grow标签处执行直接增长逻辑
	if (!n->free_objects && (!shared || !shared->avail))
        goto direct_grow;

	spin_lock(&n->list_lock);

	shared = READ_ONCE(n->shared);  // 再次获取共享缓存

	// 尝试从共享缓存中重新填充CPU缓存
	if (shared && transfer_objects(ac, shared, batchcount)) {
        shared->touched = 1;
        goto alloc_done;
    }

	while (batchcount > 0) {
        page = get_first_slab(n, false);  // 获取第一个内存页

		// 如果没有可用的内存页,则跳转到must_grow标签处执行增长逻辑
        if (!page)
            goto must_grow;

        check_spinlock_acquired(cachep);  // 检查自旋锁是否被占用

		// 分配一块连续的对象,并将其加入空闲对象链表
        batchcount = alloc_block(cachep, ac, page, batchcount);
        fixup_slab_list(cachep, n, page, &list);
    }

must_grow:
	n->free_objects -= ac->avail;  // 更新空闲对象数量

alloc_done:
    spin_unlock(&n->list_lock);  // 解锁内存缓存节点链表
    fixup_objfreelist_debug(cachep, &list);  // 调整空闲对象链表的调试信息

direct_grow:
    if (unlikely(!ac->avail)) {
        // 检查是否可以使用pfmemalloc分配器中的对象
        if (sk_memalloc_socks()) {
            void *obj = cache_alloc_pfmemalloc(cachep, n, flags);

            if (obj)
                return obj;
        }

		// 开始进行直接增长操作
        page = cache_grow_begin(cachep, gfp_exact_node(flags), node);

        ac = cpu_cache_get(cachep);  // 再次获取CPU缓存
        if (!ac->avail && page)
            alloc_block(cachep, ac, page, batchcount);
        cache_grow_end(cachep, page);

		// 如果CPU缓存仍然没有可用对象,则返回NULL
        if (!ac->avail)
            return NULL;
    }
    ac->touched = 1;  // 标记CPU缓存已使用过

    return ac->entry[--ac->avail];  // 返回分配到的内存对象
}

kmalloc API

为了提高内存分配的效率和灵活性,通常会使用专用的 slab 分配器进行内存分配。其中,通过调用 kmem_cache_alloc() 函数可以从专门的 slab 中分配内存。举例来说,在 kernel/fork.c 文件中,有一个名为 vm_area_alloc() 的函数,其功能是申请一个 vm_area_struct 结构体实例。这个过程可以通过以下代码完成:vma = kmem_cache_alloc(vm_area_cachep, GFP_KERNEL);

与之相对应的是 kmalloc() 函数,它主要用于分配通用的 slab。在内部实现中,kmalloc() 函数会调用 __do_kmalloc() 函数来完成具体的内存分配操作。

下面是 __do_kmalloc() 函数的实现代码:

static __always_inline void *__do_kmalloc(size_t size, gfp_t flags,
                      unsigned long caller)
{
    struct kmem_cache *cachep;  // 内存缓存结构体指针
    void *ret;  // 分配的内存块指针

    if (unlikely(size > KMALLOC_MAX_CACHE_SIZE))  // 如果请求的大小超过了最大缓存大小,则返回NULL
        return NULL;
    cachep = kmalloc_slab(size, flags);  // 调用kmalloc_slab函数从slab分配器中获取合适的缓存对象
    if (unlikely(ZERO_OR_NULL_PTR(cachep)))  // 如果无法获得合适的缓存对象,则返回错误码
        return cachep;
    ret = slab_alloc(cachep, flags, caller);  // 调用slab_alloc函数从缓存对象中分配一块内存

    ret = kasan_kmalloc(cachep, ret, size, flags);  // 调用kasan_kmalloc函数进行内核地址静态分析工具(KASAN)相关操作,确保内存分配和访问的安全性
    trace_kmalloc(caller, ret,
              size, cachep->size, flags);  // 记录跟踪信息,记录内存分配相关信息

    return ret;  // 返回分配到的内存块指针
}

kmalloc_slab()根据申请的 size 获取到对应的 kmem_cache

kmalloc_slab()函数实现如下:

struct kmem_cache *kmalloc_slab(size_t size, gfp_t flags)
{
    unsigned int index;  // 内存块大小对应的索引

    if (size <= 192) {  // 如果内存块大小小于等于192字节
        if (!size)
            return ZERO_SIZE_PTR;  // 如果请求分配的大小为0,则返回一个特殊指针ZERO_SIZE_PTR

        index = size_index[size_index_elem(size)];  // 根据请求大小选择合适的内存块缓存索引
    } else {  // 如果内存块大小大于192字节
        if (WARN_ON_ONCE(size > KMALLOC_MAX_CACHE_SIZE))
            return NULL;  // 如果请求分配的大小超过了最大缓存大小,则返回NULL

        index = fls(size - 1);  // 使用fls函数计算内存块大小对应的2的幂次方,作为缓存索引
    }

    return kmalloc_caches[kmalloc_type(flags)][index];  // 返回根据索引从kmalloc_caches数组中选择相应的kmem_cache对象
}

kfree

释放通用型 slab 对象时,需要根据一个指针找到待释放的对象,并对其进行释放操作。这一过程可以通过 kfree() 函数来完成,其实现如下所示。在具体的实现过程中,根据一个指针找到待释放的对象是由函数 virt_to_cache() 来实现的。

void kfree(const void *objp)
{
    struct kmem_cache *c;  // 内存块所属的缓存对象指针
    unsigned long flags;

    trace_kfree(_RET_IP_, objp);  // 跟踪内存释放操作

    if (unlikely(ZERO_OR_NULL_PTR(objp)))
        return;  // 如果传入的指针为空或者为零,则直接返回

    local_irq_save(flags);  // 关闭本地中断,并保存标志寄存器值
    kfree_debugcheck(objp);  // 检查内存是否符合释放要求
    c = virt_to_cache(objp);  // 根据内存块地址获取对应的缓存对象指针
    if (!c) {
        local_irq_restore(flags);
        return;  // 如果找不到对应的缓存对象,则直接返回
    }
    debug_check_no_locks_freed(objp, c->object_size);  // 检查是否存在被锁定的内存块被释放

    debug_check_no_obj_freed(objp, c->object_size);  // 检查是否存在重复释放同一个对象

    __cache_free(c, (void *)objp, _RET_IP_);  // 使用__cache_free函数释放内存块,将控制权交还给缓存对象
    local_irq_restore(flags);  // 恢复中断状态
}

virt_to_cache() 函数定义在 mm/slab.h 文件中,它通过调用 virt_to_head_page() 函数来获取该虚拟地址对应的 page 描述符,然后从该描述符中的 slab_cache 字段反向获取到对应的 kmem_cache

static inline struct kmem_cache *virt_to_cache(const void *obj)
{
    struct page *page;  // 内存页对象指针

    page = virt_to_head_page(obj);  // 根据内存块地址获取内存页对象指针
    if (WARN_ONCE(!PageSlab(page), "%s: Object is not a Slab page!\n", __func__))
        return NULL;  // 如果该内存块不是一个Slab页,则打印警告信息,并返回NULL

    return page->slab_cache;  // 返回该Slab页所属的缓存对象指针
}

虚拟地址到 page 描述符的相关实现如下(include/linux/mm.h)。

static inline struct page *virt_to_head_page(const void *x)
{
    struct page *page = virt_to_page(x);  // 将给定虚拟地址转换为对应的页对象指针

    return compound_head(page);  // 返回该页所属的复合页的头部页对象指针
}

// arch/arm64/include/asm/memory.h
#define virt_to_page(x)        pfn_to_page(virt_to_pfn(x))  // 将虚拟地址转换为页对象指针
#define virt_to_pfn(x)        __phys_to_pfn(__virt_to_phys((unsigned long)(x)))  // 将虚拟地址转换为物理页帧号(PFN)
#define __virt_to_phys(x)    __virt_to_phys_nodebug(x)  // 调用无调试信息版本的__virt_to_phys函数

// __is_lm_address 检查是否为线性映射(linear map)
// 0xffff_8000_0000_0000 - 0xffff_8008_0000_0000:线性映射区域映射内存
// 0xffff_8000_0000_0000向下的 kernel space virtual addr 是给 kernel image 使用的[8]
// 这对应 __lm_to_phys 和 __kimg_to_phys 两个宏。
#define __virt_to_phys_nodebug(x) ({                    \
    phys_addr_t __x = (phys_addr_t)(__tag_reset(x));        \
    __is_lm_address(__x) ? __lm_to_phys(__x) : __kimg_to_phys(__x);    \
})

#define __is_lm_address(addr)    (((u64)(addr) ^ PAGE_OFFSET) < (PAGE_END - PAGE_OFFSET))  // 检查地址是否为线性映射地址

#define __lm_to_phys(addr)    (((addr) & ~PAGE_OFFSET) + PHYS_OFFSET)  // 线性映射地址转换为物理地址
#define __kimg_to_phys(addr)    ((addr) - kimage_voffset)  // kernel image 地址转换为物理地址

virt_to_cache()的函数,它可以获取到一个虚拟地址所对应的kmem_cache。接着,内核可以使用__cache_free()函数来释放该虚拟地址对应的内存。

static __always_inline void __cache_free(struct kmem_cache *cachep, void *objp,
                     unsigned long caller)
{
    ...
    ___cache_free(cachep, objp, caller);
}

void ___cache_free(struct kmem_cache *cachep, void *objp,
        unsigned long caller)
{
    struct array_cache *ac = cpu_cache_get(cachep);

    check_irq_off();
    if (unlikely(slab_want_init_on_free(cachep)))
        // 释放时初始化对象
        memset(objp, 0, cachep->object_size);
    ...
    if (nr_online_nodes > 1 && cache_free_alien(cachep, objp))
        return;

    if (ac->avail < ac->limit) {
        STATS_INC_FREEHIT(cachep);
    } else {
        STATS_INC_FREEMISS(cachep);
        cache_flusharray(cachep, ac);
    }

    if (sk_memalloc_socks()) {
        struct page *page = virt_to_head_page(objp);

        if (unlikely(PageSlabPfmemalloc(page))) {
            cache_free_pfmemalloc(cachep, page, objp);
            return;
        }
    }

    __free_one(ac, objp);
}

__cache_free___cache_free__cache_free 是一个静态内联函数,它调用了 ___cache_free 函数。

___cache_free 函数中,首先通过 cpu_cache_get() 获取当前 CPU 的缓存对象,并将其赋值给 ac 变量。然后检查中断是否关闭,如果没有关闭,则会执行警告。

接下来,根据 cachep 所指向的缓存对象的配置,判断是否需要在释放时初始化对象。如果需要,则通过 memset() 将对象清零。

然后判断在线节点数量是否大于 1,并且调用 cache_free_alien() 函数处理非本地节点的情况。

接着,判断当前 CPU 缓存中可用对象数目是否小于限制值。如果是,则增加缓存命中统计量;否则,增加缓存未命中统计量,并调用 cache_flusharray() 函数将当前 CPU 缓存的对象写入共享池。

之后,判断是否存在使用页分配器进行内存分配的套接字。如果存在,并且当前释放的内存页面被标记为 Slab PFMEMALLOC 类型,则调用 cache_free_pfmemalloc() 处理这种情况。

最后,调用 __free_one() 函数实际释放缓存对象。

参考:Linux内核源码分析(内存调优/文件系统/进程管理/设备驱动/网络协议栈)教程

Linux内核源码系统性学习文章来源地址https://www.toymoban.com/news/detail-836869.html

>>>

到了这里,关于【Linux 内核源码分析】内存管理——Slab 分配器的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 架构师成长之路Redis第一篇|Redis 安装介绍以及内存分配器jemalloc

    Redis官网:https://redis.io/download/ 下载安装二进制文件 可下载安装最新版Redis7.2.0,或者可选版本6.x 我这里下载6.2.13和7.2最新版本,后面我们都是安装6.2.13版本的信息进行讲解 二进制文件安装步骤 安装前期准备: 安装gcc yum install gcc 压缩文件 tar -xzf redis6.2.13.tar.gz 编译 cd redis-x

    2024年02月11日
    浏览(47)
  • 【Linux 内核源码分析】物理内存组织结构

    多处理器系统两种体系结构: 非一致内存访问(Non-Uniform Memory Access,NUMA):这种体系结构下,内存被划分成多个内存节点,每个节点由不同的处理器访问。访问一个内存节点所需的时间取决于处理器和内存节点之间的距离,因此处理器与内存节点之间的距离会影响内存访问

    2024年02月22日
    浏览(53)
  • 深入理解 slab cache 内存分配全链路实现

    本文源码部分基于内核 5.4 版本讨论 在经过上篇文章 《从内核源码看 slab 内存池的创建初始化流程》 的介绍之后,我们最终得到下面这幅 slab cache 的完整架构图: 本文笔者将带大家继续从内核源码的角度继续拆解 slab cache 的实现细节,接下来笔者会基于上面这幅 slab cache 完

    2024年02月02日
    浏览(44)
  • Jtti:Linux内存管理中的slab缓存怎么实现

    在Linux内存管理中,slab缓存是一种高效的内存分配机制,用于管理小型对象的内存分配。slab缓存的实现是通过SLAB分配器来完成的,它在Linux内核中对内存分配进行优化。 SLAB分配器将内存分为三个区域:slab、partial、和empty。 Slab区域: Slab区域用于保存完整的内存对象。当有

    2024年02月15日
    浏览(46)
  • Linux内核源码分析 (6)RCU机制及内存优化屏障

    问题: RCU 英文全称为 Read-Copy-Update ,顾名思义就是 读-拷贝-更新 ,是 Linux 内核中重要的同步机制。 Linux 内核已有原子操作、读写信号量等锁机制,为什么要单独设计一个比较复杂的新机制? RCU的原理 RCU记录所有指向共享数据的指针的使用者,当要修改该共享数据时,首先

    2024年02月10日
    浏览(55)
  • 遍历slub分配器申请的object(linux3.16)

    我只是把active_obj的数量基本凑齐了,细节还没有去研究,可能有些地方是错的 之前遇到了一个设备跑了一段直接之后内存降低无法回收的问题。当时通过slabinfo看到某些slab池子里面active_objs和num_objs差距很大。slub分配器无法自己回收 (无法回收的原因有种这个说法,如果你

    2024年01月20日
    浏览(39)
  • Linux内核内存分配函数kmalloc、kzalloc和vmalloc

    在内核环境中,常用的内存分配函数主要有kmalloc、kzalloc和vmalloc这三个。既然这三函数都能在内核申请空间,那么这三个函数有什么区别呢?如何选用呢? 首先是kmalloc,其函数原型为 函数的特点: 申请的内存 虚拟地址和物理地址都是连续 的,允许申请的内存大小较小,具

    2024年02月16日
    浏览(37)
  • linux内核内存分配函数kmalloc()、kzalloc()、vmalloc()与__get_free_page()

    目录 1、值得注意的点 2、函数原型 2.1 kmalloc()与kfree() 2.2 kzalloc与kfree() 2.3 vmalloc与vfree() 2.4 __get_free_page()与free_pages() 2.5 __get_free_pages()与free_pages() 2.6 get_zeroed_page() 1、内核把物理 页 作为内存管理的基本单位,尽管处理器的最小寻址单位通常为字(或者为字节),但是MMU(内存

    2024年02月12日
    浏览(42)
  • 从 malloc 分配大块内存失败 来简看 linux 内存管理

    应用进程 malloc 返回了null,但是观察到的os 的free内存还有较大的余量 ,很奇怪为什么会这样? 不可能是oom导致的(当然也没有 os 的oom 日志),free还有余量,系统也没有cgroup的应用隔离。 我们linux上使用的库函数 malloc 基本都是用glibc库实现的malloc函数(当然如果binary 链接

    2024年02月08日
    浏览(50)
  • 【Linux内核】内存管理——内存回收机制

    转载请注明: https://www.cnblogs.com/Ethan-Code/p/16626560.html 前文提到malloc的内存分配方式,malloc申请的是虚拟内存,只有在程序去访问时,才会触发缺页异常进入内核态,在缺页中断函数中建立物理内存映射。 如果物理内存充足,则直接建立页框与页的映射。当物理内存不足时,内

    2023年04月09日
    浏览(49)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包