linux无法分配已对其,Linux slab分配器-浅析

本文主要以内核Linux 4.19来简单说明slab分配器的大概流程。slab是操作系统的一种机制,slab分配算法采用cache 存储。slab 缓存、从缓存中分配和释放对象然后销毁缓存的过程必须要定义一个 kmem_cache 对象,然后对其进行初始化这个特定的缓存包含 32 字节的对象(来自百度百科)。为了防止内存碎片, Linux在slab机制中明确的将伙伴子系统中的page 分成了多组内存大小不同的片段,如96, 192, 2^3,2^4...等,不同大小的片段属于不同的kmem_cache, 因此,当用户调用kmalloc申请内存的时候,只会从对应内存大小片段的kmem_cache中去分配。

首先如下是一个kmem_cache结构(Linux中存在多个这样的结构,每个这样的结构可以管理分配对应大小的内存片段),这里只列出了关键的几个成员。

点击(此处)折叠或打开

struct kmem_cache {

struct array_cache __percpu *cpu_cache;  // 用于找到空闲内存的结点信息

unsigned int batchcount;        // 一次分配或者释放的大小为size的数量

unsigned int limit;             //该kmem最大支持的size数量,也就是几个node的总和

unsigned int shared;            // 共享数量

unsigned int size; // 每个内存分配块的大小(这个size包含给用户内存、pad,kasan大小的总和)

unsigned int num;             // 每个order的page能分配的最大size个数

unsigned int gfporder;     // 需要从order为rfporder的伙伴子系统申请page

gfp_t allocflags;    // 申请page的flag

size_t colour;

unsigned int colour_off;

struct kmem_cache *freelist_cache; // 如果管理结构和slag不在一起,就会使用该结构

unsigned int freelist_size;     // 管理结构大小

const char *name;  // 名字

int object_size;  // 用户能在当前kmem_cache申请的最大内存

int align; //对齐

#ifdef CONFIG_KASAN

struct kasan_cache kasan_info;  // 指向kasan结构,主要用于检查内存越界使用的

#endif

unsigned int useroffset;    /* Usercopy region offset */

unsigned int usersize;        /* Usercopy region size */

struct kmem_cache_node *node[MAX_NUMNODES];  // 记录mem node(numa)的具体信息

}

kmem_cache_node结构如下(该结构表示在numa中的一个内存bank),

点击(此处)折叠或打开

struct kmem_cache_node {

spinlock_t list_lock;

struct list_head slabs_partial;  // 用于存放没有完全使用的page

struct list_head slabs_full; // 用于存放完全使用了的page

struct list_head slabs_free; // 用于存放空闲的page,这里的page可能会被释放到伙伴子系统当中

unsigned long total_slabs;    /* length of all slab lists */

unsigned long free_slabs;    /* length of free slab list only */

unsigned long free_objects;  // 空闲的大小为size的个数

unsigned int free_limit; // 空闲个数的最大值

unsigned int colour_next;    /* Per-node cache coloring */

struct array_cache *shared;    // 共享内存结点

struct alien_cache **alien;    /* on other nodes */

unsigned long next_reap;    /* updated without locking */

int free_touched;        /* updated without locking */

}

从上可以了解到,从伙伴子系统中分配出来的page会被挂到对应内存node的partial/full/free等三个链表。当page刚被申请出来时在free链表,如果该page被部分使用了,则在partial链表,如果全部被使用了,则在full链表。对于array_cache结构如下,该结构是用于分配内存的时候,能快速拿到希望的内存结点而使用的,

点击(此处)折叠或打开

struct array_cache {

unsigned int avail;  // cache中可用的内存片段数量

unsigned int limit; // ac中最大可用缓存的片段数量

unsigned int batchcount; // ac一次性申请或者释放的片段数量

unsigned int touched;

void *entry[];// 指向不同内存片段的内存地址

}

上述为kmem_cache的关键结构体,那么用图形表示为,

9df6c3ac5ca42460beee9ff5da3dd3b6.png

因此,之间的关系如图,page的lru会链接到mem_node的slabs_xxx链表上,而page的freelist用来记录该order的page的不同片段的起始地址,page->s_mem为page所代码的地址的起始地址。另外分配有些从kmem_cache的ac中分配,然后再是mem_node(ac只是mem_node的一个缓存而已)。

先看看kmem_cache的初始化,初始化栈如下,本章只会对几个函数做出说明,

点击(此处)折叠或打开

start_kernel

mm_init

kmem_cache_init          // 初始化kmem_cache

create_boot_cache    // 创建启动kmem,用于为创建其他kmem_cache的时候分配结点内存

create_kmalloc_cache // 创建用于分配kmem_cache的结点kmem

create_kmalloc_caches // 创建其他大小的kmem_cache

new_kmalloc_cache // 创建新的一个kmem_cache

create_kmalloc_cache

create_boot_cache

__kmem_cache_create  // 初始化kmem_cache结构成员的默认值

kasan_cache_create // 预留Kasan的空间,即增加size大小

set_objfreelist_slab_cache // 设置管理page片段的管理结构位置

set_off_slab_cache // 设置 page片段管理结构不在slab上

set_on_slab_cache // 设置Page片段管理结构在slab上

setup_cpu_cache // 初始化ac,即array_cache结构

enable_cpucache

从上发现,在kmem_cache_init中,需要先调用create_boot_cache,这是为什么呢?这主要是因为后期在创建kmem_cache的时候也会申请内存,但是由于kmem_cache都还没有,又怎么能申请到内存呢,于是增加了一个boot_kmem_cache,这个结构是一个静态结构,不需要分配空间。这样,当初始化了这个结构后,后面去创建其他kmem_cache的时候,就能正常分配结点内存了。

点击(此处)折叠或打开

void __init kmem_cache_init(void)

{

int i;

kmem_cache = &kmem_cache_boot; // 准备初始化boot kmem cache

if (!IS_ENABLED(CONFIG_NUMA) || num_possible_nodes() == 1)

use_alien_caches = 0;

for (i = 0; i < NUM_INIT_LISTS; i++)

kmem_cache_node_init(&init_kmem_cache_node[i]);  // 初始化kmem node信息

if (!slab_max_order_set && totalram_pages > (32 << 20) >> PAGE_SHIFT)

slab_max_order = SLAB_MAX_ORDER_HI;

// 初始化 boot 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);

list_add(&kmem_cache->list, &slab_caches); // 添加到slag_caches全局链表

memcg_link_cache(kmem_cache);

slab_state = PARTIAL; // slab_stae设置为PARTIAL,这影响array_cache和node初始化

//初始化用于申请node的结点

kmalloc_caches[INDEX_NODE] = create_kmalloc_cache(

kmalloc_info[INDEX_NODE].name,

kmalloc_size(INDEX_NODE), ARCH_KMALLOC_FLAGS,

0, kmalloc_size(INDEX_NODE));

slab_state = PARTIAL_NODE;  // 设置slab_state为PARTAL_NODE,setup_cpu_cache会用到

setup_kmalloc_cache_index_table();

slab_early_init = 0;

{  // 将boot kmem cache的node数据内容和INDEX_NODE的node数据置换

int nid;

for_each_online_node(nid) {

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

init_list(kmalloc_caches[INDEX_NODE],

&init_kmem_cache_node[SIZE_NODE + nid], nid);

}

}

// 创建其他大小的kmem_cache,实现就是一个for循环,调用__kmem_cache_create的过程

create_kmalloc_caches(ARCH_KMALLOC_FLAGS);

}

其他的函数没什么说的, 下面说说__kmem_cache_create函数,该函数用来初始化kmem_cache,实现如下

点击(此处)折叠或打开

int __kmem_cache_create(struct kmem_cache *cachep, slab_flags_t flags)

{

size_t ralign = BYTES_PER_WORD;

gfp_t gfp;

int err;

unsigned int size = cachep->size;

size = ALIGN(size, BYTES_PER_WORD); //对齐

if (flags & SLAB_RED_ZONE) {  // kasan对齐相关

ralign = REDZONE_ALIGN;

size = ALIGN(size, REDZONE_ALIGN);

}

if (ralign < cachep->align) {

ralign = cachep->align;

}

if (ralign > __alignof__(unsigned long long))

flags &= ~(SLAB_RED_ZONE | SLAB_STORE_USER);

cachep->align = ralign; //赋值对齐值

cachep->colour_off = cache_line_size();

if (cachep->colour_off < cachep->align)

cachep->colour_off = cachep->align;

if (slab_is_available()) // 申请标记

gfp = GFP_KERNEL;

else

gfp = GFP_NOWAIT;

kasan_cache_create(cachep, &size, &flags); // 如果支持kasan,则为kasan在用户申请内存中,预留空间

size = ALIGN(size, cachep->align);

if (FREELIST_BYTE_INDEX && size < SLAB_OBJ_MIN_SIZE)

size = ALIGN(SLAB_OBJ_MIN_SIZE, cachep->align);

// 这里需要注意,freelist用于管理对应order的page中分出来的大小为size的每个片段索引号,objfreelist, off slab, on slab三者只用其中一个,优先考虑objfreelist,然后是off slab,最后是on slab

if (set_objfreelist_slab_cache(cachep, size, flags)) {

flags |= CFLGS_OBJFREELIST_SLAB;

goto done;

}

if (set_off_slab_cache(cachep, size, flags)) {

flags |= CFLGS_OFF_SLAB;

goto done;

}

if (set_on_slab_cache(cachep, size, flags))

goto done;

return -E2BIG;

done:

cachep->freelist_size = cachep->num * sizeof(freelist_idx_t); // freelist大小

cachep->flags = flags;

cachep->allocflags = __GFP_COMP;

if (flags & SLAB_CACHE_DMA)

cachep->allocflags |= GFP_DMA;

if (flags & SLAB_CACHE_DMA32)

cachep->allocflags |= GFP_DMA32;

if (flags & SLAB_RECLAIM_ACCOUNT)

cachep->allocflags |= __GFP_RECLAIMABLE;

cachep->size = size;

cachep->reciprocal_buffer_size = reciprocal_value(size);

if (OFF_SLAB(cachep)) {

cachep->freelist_cache =

kmalloc_slab(cachep->freelist_size, 0u);

}

err = setup_cpu_cache(cachep, gfp); // 初始化 cpu cache和mem node

if (err) {

__kmem_cache_release(cachep);

return err;

}

return 0;

}

通过上面函数,我们大概知道用户调用kmalloc申请一个内存的时候,内存的分布

86fc543aa06d5fad8d864fa0eb9cdef3.png

如上图,user_size才是用户申请的时候填写的数据大小,后面都是为了检测内存越界而预留的空间。

点击(此处)折叠或打开

static int __ref setup_cpu_cache(struct kmem_cache *cachep, gfp_t gfp)

{

if (slab_state >= FULL) // 刚开始 slab_state状态是DOWN,只有最后都成功了会是FULL

return enable_cpucache(cachep, gfp);

cachep->cpu_cache = alloc_kmem_cache_cpus(cachep, 1, 1);

if (!cachep->cpu_cache)

return 1;

if (slab_state == DOWN) {  // 安装boot kmem的node信息

/* Creation of first cache (kmem_cache). */

set_up_node(kmem_cache, CACHE_CACHE);

} else if (slab_state == PARTIAL) {

/* For kmem_cache_node */

set_up_node(cachep, SIZE_NODE); // 安装size node的node信息

} else {

int node;

for_each_online_node(node) { // 为每个mem node申请内存结点

cachep->node[node] = kmalloc_node(

sizeof(struct kmem_cache_node), gfp, node);

BUG_ON(!cachep->node[node]);

kmem_cache_node_init(cachep->node[node]);

}

}

// 初始化ac结点

cachep->node[numa_mem_id()]->next_reap =

jiffies + REAPTIMEOUT_NODE +

((unsigned long)cachep) % REAPTIMEOUT_NODE;

cpu_cache_get(cachep)->avail = 0;

cpu_cache_get(cachep)->limit = BOOT_CPUCACHE_ENTRIES;

cpu_cache_get(cachep)->batchcount = 1;

cpu_cache_get(cachep)->touched = 0;

cachep->batchcount = 1;

cachep->limit = BOOT_CPUCACHE_ENTRIES;

return 0;

}

从上面知道,初始化的时候,ac的limit都是1,那么什么时候会改变limit的值呢?栈如下,

点击(此处)折叠或打开

start_kernel

kmem_cache_init_late

enable_cpucache // 这里会调整ac等的limit值

内存申请函数kmalloc,申请规则:kmalloc会先从对应kmem_cache的ac缓存中去获取内存片段,如果无法从ac中获取到内存片段,则尝试从node的shared中分配,如果依然无法分配则考虑从mem node的链表上去获取page,当然如果链表上也没有了,就只有从最慢的的伙伴子系统中去分配了,函数调用栈如下,

点击(此处)折叠或打开

kmalloc

kmalloc_large // 用户传入的size大于KMALLOC_MAX_CACHE_SIZE的时候,用kmalloc_order,实际就是从伙伴子系统中分配内存

kmalloc_order

alloc_pages

__kmalloc

__do_kmalloc

kmalloc_slab // 通过用户传入的size,查找需要从哪个kmem_cache中分配

slab_alloc

__do_cache_alloc

____cache_alloc // 从kmem_cache中分配内存

kasan_kmalloc // 将内存object_size没有用完的部分,填充kasan red pading

下面重点讲解 ___cache_alloc函数,该函数完成内存从kmem_cache中的分配,实现如下

点击(此处)折叠或打开

static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags)

{

void *objp;

struct array_cache *ac;

check_irq_off();

ac = cpu_cache_get(cachep); // 获取到当前kmem_cache的arrary_cache成员

if (likely(ac->avail)) { // 判断ac中是否还有未分配的内存片段,有就直接赋值分配。goto out

ac->touched = 1;

objp = ac->entry[--ac->avail];

STATS_INC_ALLOCHIT(cachep);

goto out;

}

// 因为ac中没有可用于分配的内存片段,则需要从slabs_partial或者slabs_free链表中去要

STATS_INC_ALLOCMISS(cachep);

objp = cache_alloc_refill(cachep, flags);

ac = cpu_cache_get(cachep);

out:

if (objp) // 如果分配内存成功,将ac中对该内存信息,进行清除,以免再次被分配

kmemleak_erase(&ac->entry[ac->avail]);

return objp;

}

从node链表slabs_partial、slabs_free中去获取内存片段的函数实现如下,

点击(此处)折叠或打开

static void *cache_alloc_refill(struct kmem_cache *cachep, gfp_t flags)

{

int batchcount;

struct kmem_cache_node *n;

struct array_cache *ac, *shared;

int node;

void *list = NULL;

struct page *page;

check_irq_off();

node = numa_mem_id();

ac = cpu_cache_get(cachep); // 获取ac

batchcount = ac->batchcount; // 得到一次性要从node中申请的片段个数

if (!ac->touched && batchcount > BATCHREFILL_LIMIT) {

batchcount = BATCHREFILL_LIMIT;

}

n = get_node(cachep, node); // 得到cache对应的Node结构

BUG_ON(ac->avail > 0 || !n);

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);

// 发现有共享内存片段,则优先从共享中申请batchcount个片段

if (shared && transfer_objects(ac, shared, batchcount)) {

shared->touched = 1;

goto alloc_done;

}

while (batchcount > 0) { // 还有片段未申请完,则调用get_first_slab从node的链表上获取

/* Get slab alloc is to come from. */

page = get_first_slab(n, false);

if (!page)

goto must_grow;

check_spinlock_acquired(cachep);

batchcount = alloc_block(cachep, ac, page, batchcount);

fixup_slab_list(cachep, n, page, &list); // 因为page中的片段被使用,则需要调整page所在的链表

}

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)) {

/* Check if we can use obj in pfmemalloc slab */

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); //从伙伴子系统申请一个order的page

/*

* cache_grow_begin() can reenable interrupts,

* then ac could change.

*/

ac = cpu_cache_get(cachep);

if (!ac->avail && page)

alloc_block(cachep, ac, page, batchcount); // 释放到ac中缓存起来

cache_grow_end(cachep, page);

if (!ac->avail)

return NULL;

}

ac->touched = 1;

return ac->entry[--ac->avail]; // 返回第一个片段

}

get_first_slab实现如下,

点击(此处)折叠或打开

static struct page *get_first_slab(struct kmem_cache_node *n, bool pfmemalloc)

{

struct page *page;

assert_spin_locked(&n->list_lock);// 优先从slabs_partial中去获取page

page = list_first_entry_or_null(&n->slabs_partial, struct page, lru);

if (!page) {

n->free_touched = 1;

page = list_first_entry_or_null(&n->slabs_free, struct page,

lru);

if (page)

n->free_slabs--;

}

if (sk_memalloc_socks())

page = get_valid_first_slab(n, page, pfmemalloc);

return page;

}

fixup_slab_list实现如下,

点击(此处)折叠或打开

static inline void fixup_slab_list(struct kmem_cache *cachep,

struct kmem_cache_node *n, struct page *page,

void **list)

{

list_del(&page->lru);

if (page->active == cachep->num) {

list_add(&page->lru, &n->slabs_full);

if (OBJFREELIST_SLAB(cachep)) {

page->freelist = NULL;

}

} else

list_add(&page->lru, &n->slabs_partial);

}

内存释放函数kfree, 释放规则:kfree会优先将内存片段释放到kmem_cache的ac缓存中,如果ac缓存满了,则需要将一定数量的内存片段释放到mem node的链表上,如果mem node的链表也超过了限制,就将page释放到伙伴子系统当中。为什么要设置限制?就是防止slab占用过多的内存,而导致内核系统中其他区间可用内存不足。函数调用栈如下,

点击(此处)折叠或打开

kfree

virt_to_cache // 通过虚拟地址获取对应的kmem_cache结构

__cache_free

kasan_slab_free // 释放kasan

___cache_free // 释放内存片段到kmem_cache

___cache_free实现如下,

点击(此处)折叠或打开

void ___cache_free(struct kmem_cache *cachep, void *objp,

unsigned long caller)

{

struct array_cache *ac = cpu_cache_get(cachep); // 获取到ac

check_irq_off();

kmemleak_free_recursive(objp, cachep->flags);

objp = cache_free_debugcheck(cachep, objp, caller);

if (nr_online_nodes > 1 && cache_free_alien(cachep, objp))

return;

if (ac->avail < ac->limit) { //如果ac中缓存的数量没有超过限制,就释放到ac中缓存起来

STATS_INC_FREEHIT(cachep);

} else { // ac中的缓存超过限制,则释放到node对应的链表上

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;

}

}

ac->entry[ac->avail++] = objp;

}

cache_flusharray释放一个batchcount的内存片段到node的链表上,实现如下

点击(此处)折叠或打开

static void cache_flusharray(struct kmem_cache *cachep, struct array_cache *ac)

{

int batchcount;

struct kmem_cache_node *n;

int node = numa_mem_id();

LIST_HEAD(list);

batchcount = ac->batchcount;

check_irq_off();

n = get_node(cachep, node);

spin_lock(&n->list_lock);

if (n->shared) { // 存在共享,则优先释放到node的共享结点上

struct array_cache *shared_array = n->shared;

int max = shared_array->limit - shared_array->avail;

if (max) {

if (batchcount > max)

batchcount = max;

memcpy(&(shared_array->entry[shared_array->avail]),

ac->entry, sizeof(void *) * batchcount);

shared_array->avail += batchcount;

goto free_done;

}

}

free_block(cachep, ac->entry, batchcount, node, &list); // 释放ac到node的链表上,如果存在node中也超出了限制,则把free链表上的page挂到list指向的链表上

free_done:

spin_unlock(&n->list_lock);

slabs_destroy(cachep, &list); //将list上面挂的page释放到伙伴子系统

ac->avail -= batchcount; // 调整ac缓存

memmove(ac->entry, &(ac->entry[batchcount]), sizeof(void *)*ac->avail);

}

free_block实现,

点击(此处)折叠或打开

static void free_block(struct kmem_cache *cachep, void **objpp,

int nr_objects, int node, struct list_head *list)

{

int i;

struct kmem_cache_node *n = get_node(cachep, node);

struct page *page;

n->free_objects += nr_objects;

// 依次释放ac中的每个内存片段

for (i = 0; i < nr_objects; i++) {

void *objp;

struct page *page;

objp = objpp[i];

page = virt_to_head_page(objp);

list_del(&page->lru);

check_spinlock_acquired_node(cachep, node);

slab_put_obj(cachep, page, objp);

STATS_DEC_ACTIVE(cachep);

if (page->active == 0) { // 如果active为0表示,该page的所有片段都已经被释放,空闲

list_add(&page->lru, &n->slabs_free);

n->free_slabs++;

} else {

list_add_tail(&page->lru, &n->slabs_partial);

}

}

// 这里如果free_objects数量超出了free_limit限制,则释放slabs_free上面的page

while (n->free_objects > n->free_limit && !list_empty(&n->slabs_free)) {

n->free_objects -= cachep->num;

page = list_last_entry(&n->slabs_free, struct page, lru);

list_move(&page->lru, list);

n->free_slabs--;

n->total_slabs--;

}

}

什么是kasan? kasan是Linux内核引入的一种专门检测slab内存越界问题的,通过给内存打上特定的pad来实现,这个操作比较消耗内存。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值