【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内核源码系统性学习

>>>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/694829.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

xxl-job架构原理讲解

1、调度中心 调度中心是一个单独的Web服务&#xff0c;主要是用来触发定时任务的执行 它提供了一些页面操作&#xff0c;我们可以很方便地去管理这些定时任务的触发逻辑 调度中心依赖数据库&#xff0c;所以数据都是存在数据库中的 调度中心也支持集群模式&#xff0c;但是…

mybatis数据操作语句

//基于注解 Mapper public interface reboudapt {Select("select * from dept")List<dept> huoqudept();//删除语句Delete("delete from dept where id #{id}")void deletesc(Integer id);//增加语句Insert("insert into dept(name, create_t…

Docker镜像加速

前言 众所周知&#xff0c;我们常用的一些工具或系统的下载源都是国外的&#xff0c;这就会导致我们在下载一些东西时&#xff0c;会导致下载巨慢或者下载失败的情况&#xff0c;下面便是docker换下载源的教程 镜像加速 下面是几个常用的国内的镜像 科大镜像&#xff1a;ht…

LeetCode每日一题 同构字符串(哈希表)

题目描述 给定两个字符串 s 和 t &#xff0c;判断它们是否是同构的。如果 s 中的字符可以按某种映射关系替换得到 t &#xff0c;那么这两个字符串是同构的。每个出现的字符都应当映射到另一个字符&#xff0c;同时不改变字符的顺序。不同字符不能映射到同一个字符上&#xf…

transformer,视觉模型改进论文的讨论

1、efficientVIT efficientformer 模型 快48.9倍的新SAM&#xff01;清华&MIT&英伟达开源EfficientViT-SAM&#xff1a;精度不变&#xff0c;原地起飞 YOLOv8改进 | 2023主干篇 | EfficientViT替换Backbone&#xff08;高效的视觉变换网络&#xff09; - Snu77的文章 -…

BTC系列-系统学习铭文(一)-比特币上的NFT

Ordinals协议概况 开源项目: https://github.com/ordinals/ord铭文浏览器: https://Ordinals.com关于Ordinals的BIP: https://github.com/ordinals/ord/blob/master/bip.mediawiki序数理论手册: https://docs.ordinals.com/overview.html 所需的技术积累 Ordinals NFTs 是基…

容器库(10)-std::unordered_set

unordered_set是以key为元素无序的关联容器&#xff0c;搜索、移除和插入操作是平均常数的时间复杂度。unordered_set在内部没有按任何顺序排列&#xff0c;而是放在桶当中的&#xff0c;放进哪个桶是通过计算key的hash值来决定的。 template<class Key,class Hash std::h…

Wireshark过滤DNS协议包语法实战

背景 现网DNS服务器发现CPU突增&#xff0c;发现有可能是客户恶意发起的随机子域名扫描&#xff0c;对服务器进行抓包分析&#xff0c;记录下当时的操作。 抓包 执行命令 tcpdump -iany port 53 and host $ip -nnv -w $ip.pcap进行抓包导出到本地&#xff0c;使用Wireshark进…

人工智能|深度学习——基于数字图像处理和深度学习的车牌定位

1.研究背景及研究目的和意义 车牌识别Vehicle License Plate Recognition VLPR) 是从一张或一系列数字图片中自动定位车牌区域并提取车牌信息的图像识别技术。车牌识别 以数字图像处理、模式识别、计算机视觉等技术为基础&#xff0c;是现代智能交通系统的重要组成部分&#xf…

前端项目docker部署

以react项目为例&#xff0c;理论上vue项目也支持。 打包。最好在package.json 里加上"homepage": "."&#xff0c;打包好后&#xff0c;会生成build目录将build目录下的所有文件上传到服务器&#xff0c;假设放在/root/service/shop-h5/public 目录下编写…

C#知识点-16(计算器插件开发、事件、递归、XML)

计算器插件开发 1、Calculator.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace Calculator_DLL {//用来明确所有插件开发人员的开发规范public abstract class Calculator{public int N…

2024-02-21 算法: 测试链表是否有环

点击 <C 语言编程核心突破> 快速C语言入门 算法: 测试链表是否有环 前言一、双指针 ( 快慢指针 )二、代码总结 前言 要解决问题: 一道简单的算法题, 测试链表是否含有环. 想到的思路: 哈希表, 将链表指针强制转换为整型, 利用求余法建立哈希函数. 太复杂, 内存效率不高…

MyBatis的注解式开发

MyBatis的注解式开发 一、准备开发环境1.添加依赖2.jdbc.properties3.mybatis-config.xml4.Article实体类5.SqlSessionUtil工具类 二、Insert三、Delete四、Update五、Select mybatis 中也提供了注解式开发⽅式&#xff0c;采⽤注解可以减少 Sql 映射⽂件的配置。使⽤注解式开发…

设计模式浅析(六) ·命令模式

设计模式浅析(六) 命令模式 日常叨逼叨 java设计模式浅析&#xff0c;如果觉得对你有帮助&#xff0c;记得一键三连&#xff0c;谢谢各位观众老爷&#x1f601;&#x1f601; 命令模式 概念 命令模式&#xff08;Command Pattern&#xff09;是一种行为设计模式&#xff0c…

YOLOv5代码解读[02] models/yolov5l.yaml文件解析

文章目录 YOLOv5代码解读[02] models/yolov5l.yaml文件解析yolov5l.yaml文件检测头1--->耦合头检测头2--->解耦头检测头3--->ASFF检测头Model类解析parse_model函数 YOLOv5代码解读[02] models/yolov5l.yaml文件解析 yolov5l.yaml文件 # YOLOv5 &#x1f680; by Ult…

FPGA SERDESE2 (SDR收发仿真)

高速 Serdes 环路测试 高速串行通信优势非常巨大,只需要很少的IO引脚就可以实现高速通信,这也是当今FPGA高速接口的核心 技术。比如XILINX的7代FPGA,GTX可以达到10.3125Gbps,ultrascale FPGA的GTH可以达到16Gbps。目前国产FPGA还难以达到这么高的接口速度。 高速串行通信经…

社区分享|中华保险基于MeterSphere开展接口自动化测试

中华联合保险集团股份有限公司&#xff08;以下简称为“中华保险”&#xff09;始创于1986年&#xff0c;是全国唯一一家以“中华”冠名的国有控股保险公司。截至2022年12月底&#xff0c;中华保险总资产为1006.06亿元&#xff0c;在全国拥有超过2900个营业网点&#xff0c;员工…

Servlet中的请求与响应

Request和Response 1.Request和Response的概述2.Request对象2.1 Request继承体系2.2 Request获取请求数据2.3 解决post请求乱码问题 *2.4 Request请求转发(-&#xff0c;*)2.5 request的生命周期 3.HTTP响应详解(理解)1.使用抓包查看响应报文协议内容2.HTTP响应报文协议介绍 4.…

汇编英文全称

mov move mvn Mov Negative ldr LoaD Register str Store Register lsl Logic Shift Left lsr Logic Shift Right asr Arithmetic Shift Right 算数右移 ror Rotate right 循环右移…

人工智能讲师AI讲师大模型讲师叶梓介绍及大语言模型技术原理与实践提纲

叶梓&#xff0c;上海交通大学计算机专业博士毕业&#xff0c;高级工程师。主研方向&#xff1a;数据挖掘、机器学习、人工智能。历任国内知名上市IT企业的AI技术总监、资深技术专家&#xff0c;市级行业大数据平台技术负责人。 长期负责城市信息化智能平台的建设工作&#xff…