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

2024-02-22 14:12

本文主要是介绍【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_SLABstruct 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_cachestruct kmem_cache *s = kmem_cache_zalloc(kmem_cache, GFP_NOWAIT);// 使用从kmem_cache slab中申请来的内存初始化kmem_cachecreate_boot_cache(s, name, size, flags, useroffset, usersize);// 将初始化后的kmem_cache添加到slab_caches链表中list_add(&s->list, &slab_caches);// 设置kmem_cache的引用计数为1s->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_cachesighand_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_cachesignal_cachep = kmem_cache_create("signal_cache",sizeof(struct signal_struct), 0,SLAB_HWCACHE_ALIGN | SLAB_PANIC | SLAB_ACCOUNT, NULL);// 创建用于管理struct files_struct对象的kmem_cachefiles_cachep = kmem_cache_create("files_cache",sizeof(struct files_struct), 0,SLAB_HWCACHE_ALIGN | SLAB_PANIC | SLAB_ACCOUNT, NULL);// 创建用于管理struct fs_struct对象的kmem_cachefs_cachep = kmem_cache_create("fs_cache",sizeof(struct fs_struct), 0,SLAB_HWCACHE_ALIGN | SLAB_PANIC | SLAB_ACCOUNT, NULL);// 创建用于管理vm_area_struct对象的kmem_cachevm_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_ACCOUNTif (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缓存仍然没有可用对象,则返回NULLif (!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))  // 如果请求的大小超过了最大缓存大小,则返回NULLreturn 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_PTRindex = size_index[size_index_elem(size)];  // 根据请求大小选择合适的内存块缓存索引} else {  // 如果内存块大小大于192字节if (WARN_ON_ONCE(size > KMALLOC_MAX_CACHE_SIZE))return NULL;  // 如果请求分配的大小超过了最大缓存大小,则返回NULLindex = 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页,则打印警告信息,并返回NULLreturn 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内核源码系统性学习

>>>

这篇关于【Linux 内核源码分析】内存管理——Slab 分配器的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/735496

相关文章

Linux使用fdisk进行磁盘的相关操作

《Linux使用fdisk进行磁盘的相关操作》fdisk命令是Linux中用于管理磁盘分区的强大文本实用程序,这篇文章主要为大家详细介绍了如何使用fdisk进行磁盘的相关操作,需要的可以了解下... 目录简介基本语法示例用法列出所有分区查看指定磁盘的区分管理指定的磁盘进入交互式模式创建一个新的分区删除一个存

Java内存泄漏问题的排查、优化与最佳实践

《Java内存泄漏问题的排查、优化与最佳实践》在Java开发中,内存泄漏是一个常见且令人头疼的问题,内存泄漏指的是程序在运行过程中,已经不再使用的对象没有被及时释放,从而导致内存占用不断增加,最终... 目录引言1. 什么是内存泄漏?常见的内存泄漏情况2. 如何排查 Java 中的内存泄漏?2.1 使用 J

Linux使用dd命令来复制和转换数据的操作方法

《Linux使用dd命令来复制和转换数据的操作方法》Linux中的dd命令是一个功能强大的数据复制和转换实用程序,它以较低级别运行,通常用于创建可启动的USB驱动器、克隆磁盘和生成随机数据等任务,本文... 目录简介功能和能力语法常用选项示例用法基础用法创建可启动www.chinasem.cn的 USB 驱动

高效管理你的Linux系统: Debian操作系统常用命令指南

《高效管理你的Linux系统:Debian操作系统常用命令指南》在Debian操作系统中,了解和掌握常用命令对于提高工作效率和系统管理至关重要,本文将详细介绍Debian的常用命令,帮助读者更好地使... Debian是一个流行的linux发行版,它以其稳定性、强大的软件包管理和丰富的社区资源而闻名。在使用

Redis主从/哨兵机制原理分析

《Redis主从/哨兵机制原理分析》本文介绍了Redis的主从复制和哨兵机制,主从复制实现了数据的热备份和负载均衡,而哨兵机制可以监控Redis集群,实现自动故障转移,哨兵机制通过监控、下线、选举和故... 目录一、主从复制1.1 什么是主从复制1.2 主从复制的作用1.3 主从复制原理1.3.1 全量复制

Linux Mint Xia 22.1重磅发布: 重要更新一览

《LinuxMintXia22.1重磅发布:重要更新一览》Beta版LinuxMint“Xia”22.1发布,新版本基于Ubuntu24.04,内核版本为Linux6.8,这... linux Mint 22.1「Xia」正式发布啦!这次更新带来了诸多优化和改进,进一步巩固了 Mint 在 Linux 桌面

LinuxMint怎么安装? Linux Mint22下载安装图文教程

《LinuxMint怎么安装?LinuxMint22下载安装图文教程》LinuxMint22发布以后,有很多新功能,很多朋友想要下载并安装,该怎么操作呢?下面我们就来看看详细安装指南... linux Mint 是一款基于 Ubuntu 的流行发行版,凭借其现代、精致、易于使用的特性,深受小伙伴们所喜爱。对

Redis主从复制的原理分析

《Redis主从复制的原理分析》Redis主从复制通过将数据镜像到多个从节点,实现高可用性和扩展性,主从复制包括初次全量同步和增量同步两个阶段,为优化复制性能,可以采用AOF持久化、调整复制超时时间、... 目录Redis主从复制的原理主从复制概述配置主从复制数据同步过程复制一致性与延迟故障转移机制监控与维

什么是 Linux Mint? 适合初学者体验的桌面操作系统

《什么是LinuxMint?适合初学者体验的桌面操作系统》今天带你全面了解LinuxMint,包括它的历史、功能、版本以及独特亮点,话不多说,马上开始吧... linux Mint 是一款基于 Ubuntu 和 Debian 的知名发行版,它的用户体验非常友好,深受广大 Linux 爱好者和日常用户的青睐,

Linux(Centos7)安装Mysql/Redis/MinIO方式

《Linux(Centos7)安装Mysql/Redis/MinIO方式》文章总结:介绍了如何安装MySQL和Redis,以及如何配置它们为开机自启,还详细讲解了如何安装MinIO,包括配置Syste... 目录安装mysql安装Redis安装MinIO总结安装Mysql安装Redis搜索Red