本文共 11245 字,大约阅读时间需要 37 分钟。
slub 分配器工作原理
相关函数与结构体
进程创建的do_fork中会调用copy_process函数,这个函数会调用 dup_task_struct 函数
\linux-4.13.16\kernel\fork.c
dup_task_struct函数\linux-4.13.16\kernel\fork.c
dup_task_struct 调用函数 alloc_task_struct_node,分配一个 task_struct 对象,用于复制task_structalloc_task_struct_node 会调用 kmem_cache_alloc_node 函数,在 task_struct 的缓存区域 task_struct_cachep 分配了一块内存。有了这个缓存区,每次创建 task_struct 的时候,不用到内存里面去分配,先在缓存里面看看有没有直接可用的。当一个进程结束,task_struct 也不用直接被销毁,而是放回到缓存中,这就是 kmem_cache_free 的作用。
\linux-4.13.16\kernel\fork.c
static inline struct task_struct *alloc_task_struct_node(int node){ return kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node);}static inline void free_task_struct(struct task_struct *tsk){ kmem_cache_free(task_struct_cachep, tsk);}
task_struct_cachep 在系统初始化的时候 被 kmem_cache_create 函数创建
\linux-4.13.16\kernel\fork.c
kmem_cache_create 函数 专门用于分配 task_struct 对象的缓存。这个缓存区的名字就叫 task_struct。缓存区中每一块的大小正好等于 task_struct 的大小,也即 arch_task_struct_size。
kmem_cache_create 函数
\linux-4.13.16\mm\slab_common.c
缓存区 struct kmem_cache 结构
\linux-4.13.16\include\linux\slub_def.h
struct kmem_cache { struct kmem_cache_cpu __percpu *cpu_slab; /* Used for retriving partial slabs etc */ unsigned long flags; unsigned long min_partial; int size; /* The size of an object including meta data */ int object_size; /* The size of an object without meta data */ int offset; /* Free pointer offset. */#ifdef CONFIG_SLUB_CPU_PARTIAL int cpu_partial; /* Number of per cpu partial objects to keep around */#endif struct kmem_cache_order_objects oo; /* Allocation and freeing of slabs */ struct kmem_cache_order_objects max; struct kmem_cache_order_objects min; gfp_t allocflags; /* gfp flags to use on each alloc */ int refcount; /* Refcount for slab cache destroy */ void (*ctor)(void *);...... const char *name; /* Name (only for display!) */ struct list_head list; /* List of slab caches */...... struct kmem_cache_node *node[MAX_NUMNODES];};对于缓存来讲,就是分配了连续几页的大内存块,然后根据缓存对象的大小,切成小内存块。
三个 kmem_cache_order_objects 类型的变量。 order,就是 2 的 order 次方个页面的大内存块,objects 就是能够存放的缓存对象的数量
将大内存块切分成小内存块,就像下面这样
图片来自极客时间趣谈linux操作系统每一项的结构都是缓存对象后面跟一个下一个空闲对象的指针,这样非常方便将所有的空闲对象链成一个链。
三个变量:size 是包含这个指针的大小,object_size 是纯对象的大小,offset 就是把下一个空闲对象的指针存放在这一项里的偏移量。
缓存区 struct kmem_cache 的维护
两个重要的成员变量,kmem_cache_cpu 和 kmem_cache_node,每个 NUMA 节点上分别都有一个
图片来自极客时间趣谈linux操作系统分配缓存块的两种路径,fast path 和 slow path,也就是快速通道和普通通道。kmem_cache_cpu 就是快速通道,kmem_cache_node 是普通通道。
每次分配的时候,要先从** kmem_cache_cpu** 进行分配。如果 kmem_cache_cpu 里面没有空闲的块,那就到 kmem_cache_node 中进行分配;如果还是没有空闲的块,才去伙伴系统分配新的页。
kmem_cache_cpu 结构
\linux-4.13.16\include\linux\slub_def.h
struct kmem_cache_cpu { void **freelist; /* Pointer to next available object */ unsigned long tid; /* Globally unique transaction id */ struct page *page; /* The slab from which we are allocating */#ifdef CONFIG_SLUB_CPU_PARTIAL struct page *partial; /* Partially allocated frozen slabs */#endif#ifdef CONFIG_SLUB_STATS unsigned stat[NR_SLUB_STAT_ITEMS];#endif};page 指向大内存块的第一个页,缓存块就是从里面分配的 freelist 指向大内存块里面第一个空闲的项 partial 指向的也是大内存块的第一个页,之所以名字叫 partial(部分),就是因为它里面部分被分配出去了,部分是空的。备用列表,当 page 满了,就会从这里找
kmem_cache_node 结构
\linux-4.13.16\mm\slab.hstruct kmem_cache_node { spinlock_t list_lock;......#ifdef CONFIG_SLUB unsigned long nr_partial; struct list_head partial;......#endif};partial,是一个链表。这个链表里存放的是部分空闲的内存块,kmem_cache_cpu 里面的 partial 的备用列表,如果那里没有,就到这里来找
分配过程
copy_process函数调用 dup_task_struct函数调用alloc_task_struct_node函数调用 kmem_cache_alloc_node函数调用 slab_alloc_node函数
kmem_cache_alloc_node 函数
\linux-4.13.16\mm\slub.cvoid *kmem_cache_alloc_node(struct kmem_cache *s, gfp_t gfpflags, int node){ void *ret = slab_alloc_node(s, gfpflags, node, _RET_IP_); trace_kmem_cache_alloc_node(_RET_IP_, ret, s->object_size, s->size, gfpflags, node); return ret;}
slab_alloc_node 函数
\linux-4.13.16\mm\slub.c
/* * Inlined fastpath so that allocation functions (kmalloc, kmem_cache_alloc) * have the fastpath folded into their functions. So no function call * overhead for requests that can be satisfied on the fastpath. * * The fastpath works by first checking if the lockless freelist can be used. * If not then __slab_alloc is called for slow processing. * * Otherwise we can simply pick the next object from the lockless free list. */static __always_inline void *slab_alloc_node(struct kmem_cache *s, gfp_t gfpflags, int node, unsigned long addr){ void *object; struct kmem_cache_cpu *c; struct page *page; unsigned long tid;...... tid = this_cpu_read(s->cpu_slab->tid); c = raw_cpu_ptr(s->cpu_slab);...... object = c->freelist; page = c->page; if (unlikely(!object || !node_match(page, node))) { object = __slab_alloc(s, gfpflags, node, addr, c); stat(s, ALLOC_SLOWPATH); } ...... return object;}快速通道取出 cpu_slab 也即 kmem_cache_cpu 的 freelist,这就是第一个空闲的项,可以直接返回。如果没有空闲的了,则只好进入普通通道,调用 __slab_alloc。
__slab_alloc 函数
\linux-4.13.16\mm\slub.cstatic void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node, unsigned long addr, struct kmem_cache_cpu *c){ void *freelist; struct page *page;......redo:...... /* must check again c->freelist in case of cpu migration or IRQ */ freelist = c->freelist; if (freelist) goto load_freelist; freelist = get_freelist(s, page); if (!freelist) { c->page = NULL; stat(s, DEACTIVATE_BYPASS); goto new_slab; }load_freelist: c->freelist = get_freepointer(s, freelist); c->tid = next_tid(c->tid); return freelist;new_slab: if (slub_percpu_partial(c)) { page = c->page = slub_percpu_partial(c); slub_set_percpu_partial(c, page); stat(s, CPU_PARTIAL_ALLOC); goto redo; } freelist = new_slab_objects(s, gfpflags, node, &c);...... return freeli首先再次尝试一下 kmem_cache_cpu 的 freelist,如果有别人已经释放了一些缓存就跳到 load_freelist将 freelist 指向下一个空闲项.
如果 freelist 还是没有,则跳到 new_slab 里面去,先去 kmem_cache_cpu 的 partial 里面看,如果 partial 不是空的,那就将 kmem_cache_cpu 的 page,也就是快速通道的那一大块内存,替换为 partial 里面的大块内存。然后 redo,重新试下。
如果slub_percpu_partial还不行,那就要调用 new_slab_objects函数
new_slab_objects 函数
\linux-4.13.16\mm\slub.c
static inline void *new_slab_objects(struct kmem_cache *s, gfp_t flags, int node, struct kmem_cache_cpu **pc){ void *freelist; struct kmem_cache_cpu *c = *pc; struct page *page; freelist = get_partial(s, flags, node, c); if (freelist) return freelist; page = new_slab(s, flags, node); if (page) { c = raw_cpu_ptr(s->cpu_slab); if (c->page) flush_slab(s, c); freelist = page->freelist; page->freelist = NULL; stat(s, ALLOC_SLAB); c->page = page; *pc = c; } else freelist = NULL; return freelisget_partial 会根据 node id,找到相应的 kmem_cache_node,然后调用 get_partial_node函数,开始在这个节点进行分配
get_partial_node 函数
\linux-4.13.16\mm\slub.c
/* * Try to allocate a partial slab from a specific node. */static void *get_partial_node(struct kmem_cache *s, struct kmem_cache_node *n, struct kmem_cache_cpu *c, gfp_t flags){ struct page *page, *page2; void *object = NULL; int available = 0; int objects; /* * Racy check. If we mistakenly see no partial slabs then we * just allocate an empty slab. If we mistakenly try to get a * partial slab and there is none available then get_partials() * will return NULL. */ if (!n || !n->nr_partial) return NULL; spin_lock(&n->list_lock); list_for_each_entry_safe(page, page2, &n->partial, lru) { void *t; if (!pfmemalloc_match(page, flags)) continue; t = acquire_slab(s, n, page, object == NULL, &objects); if (!t) break; available += objects; if (!object) { c->page = page; stat(s, ALLOC_FROM_PARTIAL); object = t; } else { put_cpu_partial(s, page, 0); stat(s, CPU_PARTIAL_NODE); } if (!kmem_cache_has_cpu_partial(s) || available > slub_cpu_partial(s) / 2) break; } spin_unlock(&n->list_lock); return object;}
如果 kmem_cache_node 里面也没有空闲的内存,在new_slab_objects 函数里面 同通过new_slab 函数调用 allocate_slab
allocate_slab 函数
\linux-4.13.16\mm\slub.c
static struct page *allocate_slab(struct kmem_cache *s, gfp_t flags, int node){ struct page *page; struct kmem_cache_order_objects oo = s->oo; gfp_t alloc_gfp; void *start, *p; int idx, order; bool shuffle; flags &= gfp_allowed_mask;...... page = alloc_slab_page(s, alloc_gfp, node, oo); if (unlikely(!page)) { oo = s->min; alloc_gfp = flags; /* * Allocation may have failed due to fragmentation. * Try a lower order alloc if possible */ page = alloc_slab_page(s, alloc_gfp, node, oo); if (unlikely(!page)) goto out; stat(s, ORDER_FALLBACK); }...... return page;}alloc_slab_page 分配页面,分配的时候按 kmem_cache_order_objects 里面的 order 来
页面换出
触发换出的情况:
1、分配内存时发现没有空闲 2、内存管理主动换出分配内存时发现没有空闲
解析申请一个页面时,调用 get_page_from_freelist->node_reclaim->__node_reclaim->shrink_node
内存管理主动换出
由内核线程 kswapd 实现
- 在系统初始化的时候就被创建,无限循环,直到系统停止
- kswapd 在内存不紧张时休眠, 在内存紧张时检测内存 调用 balance_pgdat->kswapd_shrink_node->shrink_node
- shrink_node 会调用 shrink_node_memcg
shrink_node_memcg 函数
\linux-4.13.16\mm\vmscan.c
/* * This is a basic per-node page freer. Used by both kswapd and direct reclaim. */static void shrink_node_memcg(struct pglist_data *pgdat, struct mem_cgroup *memcg, struct scan_control *sc, unsigned long *lru_pages){ ...... unsigned long nr[NR_LRU_LISTS]; enum lru_list lru;...... while (nr[LRU_INACTIVE_ANON] || nr[LRU_ACTIVE_FILE] || nr[LRU_INACTIVE_FILE]) { unsigned long nr_anon, nr_file, percentage; unsigned long nr_scanned; for_each_evictable_lru(lru) { if (nr[lru]) { nr_to_scan = min(nr[lru], SWAP_CLUSTER_MAX); nr[lru] -= nr_to_scan; nr_reclaimed += shrink_list(lru, nr_to_scan, lruvec, memcg, sc); } }...... }......页面都挂在LRU (LRU 是 Least Recent Use,也就是最近最少使用)链表中, 页面有两种类型: 匿名页; 文件内存映射页
enum lru_list lru 结构
\include\linux\mmzone.henum lru_list { LRU_INACTIVE_ANON = LRU_BASE, LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE, LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE, LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE, LRU_UNEVICTABLE, NR_LRU_LISTS};#define for_each_lru(lru) for (lru = 0; lru < NR_LRU_LISTS; lru++)#define for_each_evictable_lru(lru) for (lru = 0; lru <= LRU_ACTIVE_FILE; lru++)static inline int is_file_lru(enum lru_list lru){ return (lru == LRU_INACTIVE_FILE || lru == LRU_ACTIVE_FILE);}static inline int is_active_lru(enum lru_list lru){ return (lru == LRU_ACTIVE_ANON || lru == LRU_ACTIVE_FILE);}** shrink_list 函数**
\linux-4.13.16\mm\vmscan.c
static unsigned long shrink_list(enum lru_list lru, unsigned long nr_to_scan, struct lruvec *lruvec, struct mem_cgroup *memcg, struct scan_control *sc){ if (is_active_lru(lru)) { if (inactive_list_is_low(lruvec, is_file_lru(lru), memcg, sc, true)) shrink_active_list(nr_to_scan, lruvec, sc, lru); return 0; } return shrink_inactive_list(nr_to_scan, lruvec, sc, lru);}匿名页;和文件内存映射页 每一类有两个列表: active 和 inactive 列表
要换出时, 从 inactive 列表中找到最不活跃的页换出
更新列表, shrink_list 先缩减 active 列表, 再缩减不活跃列表
shrink_inactive_list 缩减不活跃列表时对页面进行回收:
- 匿名页回收: 分配 swap, 将内存也写入文件系统
- 文件内存映射页: 将内存中的文件修改写入文件中
总结
物理内存来讲,从下层到上层的关系及分配模式如下:
- 物理内存分 NUMA 节点,分别进行管理
- 每个 NUMA 节点分成多个内存区域
- 每个内存区域分成多个物理页面
- 伙伴系统将多个连续的页面作为一个大的内存块分配给上层
- kswapd 负责物理页面的换入换出
- Slub Allocator 将从伙伴系统申请的大内存块切成小块,分配给其他系统
参考资料:
趣谈Linux操作系统(极客时间)链接:
欢迎大家来一起交流学习转载地址:https://blog.csdn.net/leacock1991/article/details/107501740 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!