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_PARTIALint cpu_partial;  /* Number of per cpu partial objects to keep around */
#endifstruct 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_cpukmem_cache_node,每个 NUMA 节点上分别都有一个


图片来自极客时间趣谈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_PARTIALstruct page *partial; /* Partially allocated frozen slabs */
#endif
#ifdef CONFIG_SLUB_STATSunsigned 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_SLUBunsigned 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;} elsefreelist = 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操作系统(极客时间)链接:
http://gk.link/a/10iXZ
欢迎大家来一起交流学习

一步一步学linux操作系统: 21 内存管理_小内存分配与页面换出相关推荐

  1. linux服务器操作系统日志都有哪些,Linux操作系统服务器日志管理详解

    Linux操作系统服务器日志管理详解 Linux操作系统服务器日志管理详解 日志对于安全来说,非常重要,他记录了系统每天发生的各种各样的事情,你可以通过他来检查错误发生的原因,或者受到攻击时攻击者留下 ...

  2. Linux操作系统原理与应用04:内存管理

    目录 1. Linux内存管理概述 1.1 内存的层次结构 1.2 虚拟内存概述 1.2.1 虚拟内存基本思想 1.2.2 进程虚拟地址空间 1.3 内核空间到物理空间的映射 1.3.1 内核空间的线 ...

  3. linux的原理和运用,Linux操作系统原理与应用_内存寻址

    原标题:Linux操作系统原理与应用_内存寻址 第五讲今天上线啦. 在本次课程中,陈老师详细的讲解了有关于内存寻址的演变的相关知识. 第一部分中,介绍了关于内存寻址的相关背景知识.内存寻址-操作系统设 ...

  4. 操作系统内存管理_操作系统6内存管理基础

    引言 花了一段时间才把之前的笔记整理了一部分,平时太忙也没啥时间.今天开始整理内存管理部分的,内存管理部分大致分为三部分笔记,第一部分就是本篇内存管理基础,第二部分是虚拟内存,第三部分高速缓存. 一个 ...

  5. 【Linux 内核 内存管理】优化内存屏障 ③ ( 编译器屏障 | 禁止 / 开启内核抢占 与 方法保护临界区 | preempt_disable 禁止内核抢占源码 | 开启内核抢占源码 )

    文章目录 一.禁止 / 开启内核抢占 与 方法保护临界区 二.编译器优化屏障 三.preempt_disable 禁止内核抢占 源码 四.preempt_enable 开启内核抢占 源码 一.禁止 / ...

  6. 一步一步学linux操作系统: 14 进程调度三完_抢占式调度

    抢占式调度 情况1:最常见的现象就是一个进程执行时间太长了,是时候切换到另一个进程 那怎么衡量一个进程的运行时间呢? 在计算机里面有一个时钟,会过一段时间触发一次时钟中断,通知操作系统,时间又过去一个 ...

  7. 一步一步学linux操作系统: 32 输入与输出系统_ 块设备二_直接 I/O,缓存 I/O 与 块设备数据写入请求

    直接 I/O 与 缓存 I/O 可以参见 https://blog.csdn.net/leacock1991/article/details/108035136 对于 ext4 文件系统,最后调用的是 ...

  8. 为什么学Linux操作系统?

    文章目录 Linux的介绍 Linux的发展 Linux的发展现状与趋势 Linux学习方法 分为四大块 Linux可以干嘛 再来回答为什么学习Linux 主要三大块 学习Linux主要是学习什么? ...

  9. 趣谈Linux操作系统学习笔记:用户态内存映射:如何找到正确的会议室?(第25讲)...

    一.mmap原理 在虚拟内存空间那一节,我们知道,每一个进程都有一个列表vm_area_struct,指向虚拟地址空间的不同内存块,这个变量名字叫mmap struct mm_struct {stru ...

最新文章

  1. 设计模式之装饰模式(Java实现)
  2. 有钱任性!字节跳动又给员工发钱了!字节程序员:吓一跳,莫名其妙多了几万块!...
  3. Ask Me Anything #1 我是新晋CNCF TOC张磊,你有什么想问我的?
  4. 聚类算法之DBScan(Java实现)[转]
  5. Linux上用户之间对话
  6. Linux的实际操作:文件目录类实用指令(压缩gzip tar -zcvf和解压缩gunzip tar -zxvf)
  7. 别再传李笑来的录音了!这才是有关区块链最靠谱最简单易懂的科普
  8. Javascript实现的倒计时时钟
  9. php 采集程序 宋正河
  10. 电子科大820历年真题_【真题实战】电子科技大学2016计算机专业基础820真题
  11. 异常:Invalid or unexpected token
  12. python编程入门书籍-关于 Python 的经典入门书籍有哪些?
  13. Java JSch 远程执行 Shell 命令
  14. 接口测试神奇APIPOST
  15. 安卓谷歌地图打开闪退问题解决
  16. Flink DataStream的wordCount、数据源和Sink、Side Outputs、两阶段提交(two-phase commit, 2PC)
  17. 小码哥C++:第一课
  18. 我的世界刷猪人塔java版_我的世界1.11.2自动猪人塔制作指南 猪人塔存档下载 | 我的世界 | MC世界侠...
  19. androidstudio引用本地maven_android studio 之dependence添加依赖maven仓库中的项目出错
  20. Web系统常见安全漏洞介绍及解决方案-sql注入

热门文章

  1. js商场导航思路--ivx--canvas实现导航动画效果
  2. 哥去面试,每次都被赶出来,没天理
  3. 抱歉,系统网络繁忙,您暂时无法付款,请稍后再试。
  4. AutoCAD 2014 新特性和新API网络视频讲座
  5. 招标文件 服务器参数,深信服超融合平台招标参数.docx
  6. 深度学习推荐系统综述
  7. 数字化+特色小镇解决方案
  8. 新手学习MAYA,需要知道的10个技巧!
  9. php试卷分析,考试过后试卷分析该怎么做
  10. 读《区块链革命》笔记与思考——金融服务领域将如何实现变革?