一步一步学linux操作系统: 21 内存管理_小内存分配与页面换出
发布日期:2021-06-29 17:13:50 浏览次数:2 分类:技术文章

本文共 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_struct

alloc_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操作系统
图片来自极客时间趣谈linux操作系统

每一项的结构都是缓存对象后面跟一个下一个空闲对象的指针,这样非常方便将所有的空闲对象链成一个链。

三个变量:size 是包含这个指针的大小,object_size 是纯对象的大小,offset 就是把下一个空闲对象的指针存放在这一项里的偏移量。

缓存区 struct kmem_cache 的维护

两个重要的成员变量,kmem_cache_cpukmem_cache_node,每个 NUMA 节点上分别都有一个

图片来自极客时间趣谈linux操作系统

图片来自极客时间趣谈linux操作系统

分配缓存块的两种路径,fast pathslow 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.h

struct 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.c

void *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.c

static 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 freelis

在这里插入图片描述

get_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.h

enum 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操作系统

图片来自极客时间趣谈linux操作系统

参考资料:

趣谈Linux操作系统(极客时间)链接:

欢迎大家来一起交流学习

转载地址:https://blog.csdn.net/leacock1991/article/details/107501740 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:一步一步学linux操作系统: 22 内存管理_用户态内存映射
下一篇:一步一步学linux操作系统: 20 内存管理_NUMA 方式物理内存管理_节点、区域、页以及伙伴系统(Buddy System)

发表评论

最新留言

感谢大佬
[***.8.128.20]2024年04月23日 15时59分20秒