一步一步学linux操作系统: 21 内存管理_小内存分配与页面换出
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_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_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 内存管理_小内存分配与页面换出相关推荐
- linux服务器操作系统日志都有哪些,Linux操作系统服务器日志管理详解
Linux操作系统服务器日志管理详解 Linux操作系统服务器日志管理详解 日志对于安全来说,非常重要,他记录了系统每天发生的各种各样的事情,你可以通过他来检查错误发生的原因,或者受到攻击时攻击者留下 ...
- Linux操作系统原理与应用04:内存管理
目录 1. Linux内存管理概述 1.1 内存的层次结构 1.2 虚拟内存概述 1.2.1 虚拟内存基本思想 1.2.2 进程虚拟地址空间 1.3 内核空间到物理空间的映射 1.3.1 内核空间的线 ...
- linux的原理和运用,Linux操作系统原理与应用_内存寻址
原标题:Linux操作系统原理与应用_内存寻址 第五讲今天上线啦. 在本次课程中,陈老师详细的讲解了有关于内存寻址的演变的相关知识. 第一部分中,介绍了关于内存寻址的相关背景知识.内存寻址-操作系统设 ...
- 操作系统内存管理_操作系统6内存管理基础
引言 花了一段时间才把之前的笔记整理了一部分,平时太忙也没啥时间.今天开始整理内存管理部分的,内存管理部分大致分为三部分笔记,第一部分就是本篇内存管理基础,第二部分是虚拟内存,第三部分高速缓存. 一个 ...
- 【Linux 内核 内存管理】优化内存屏障 ③ ( 编译器屏障 | 禁止 / 开启内核抢占 与 方法保护临界区 | preempt_disable 禁止内核抢占源码 | 开启内核抢占源码 )
文章目录 一.禁止 / 开启内核抢占 与 方法保护临界区 二.编译器优化屏障 三.preempt_disable 禁止内核抢占 源码 四.preempt_enable 开启内核抢占 源码 一.禁止 / ...
- 一步一步学linux操作系统: 14 进程调度三完_抢占式调度
抢占式调度 情况1:最常见的现象就是一个进程执行时间太长了,是时候切换到另一个进程 那怎么衡量一个进程的运行时间呢? 在计算机里面有一个时钟,会过一段时间触发一次时钟中断,通知操作系统,时间又过去一个 ...
- 一步一步学linux操作系统: 32 输入与输出系统_ 块设备二_直接 I/O,缓存 I/O 与 块设备数据写入请求
直接 I/O 与 缓存 I/O 可以参见 https://blog.csdn.net/leacock1991/article/details/108035136 对于 ext4 文件系统,最后调用的是 ...
- 为什么学Linux操作系统?
文章目录 Linux的介绍 Linux的发展 Linux的发展现状与趋势 Linux学习方法 分为四大块 Linux可以干嘛 再来回答为什么学习Linux 主要三大块 学习Linux主要是学习什么? ...
- 趣谈Linux操作系统学习笔记:用户态内存映射:如何找到正确的会议室?(第25讲)...
一.mmap原理 在虚拟内存空间那一节,我们知道,每一个进程都有一个列表vm_area_struct,指向虚拟地址空间的不同内存块,这个变量名字叫mmap struct mm_struct {stru ...
最新文章
- 设计模式之装饰模式(Java实现)
- 有钱任性!字节跳动又给员工发钱了!字节程序员:吓一跳,莫名其妙多了几万块!...
- Ask Me Anything #1 我是新晋CNCF TOC张磊,你有什么想问我的?
- 聚类算法之DBScan(Java实现)[转]
- Linux上用户之间对话
- Linux的实际操作:文件目录类实用指令(压缩gzip tar -zcvf和解压缩gunzip tar -zxvf)
- 别再传李笑来的录音了!这才是有关区块链最靠谱最简单易懂的科普
- Javascript实现的倒计时时钟
- php 采集程序 宋正河
- 电子科大820历年真题_【真题实战】电子科技大学2016计算机专业基础820真题
- 异常:Invalid or unexpected token
- python编程入门书籍-关于 Python 的经典入门书籍有哪些?
- Java JSch 远程执行 Shell 命令
- 接口测试神奇APIPOST
- 安卓谷歌地图打开闪退问题解决
- Flink DataStream的wordCount、数据源和Sink、Side Outputs、两阶段提交(two-phase commit, 2PC)
- 小码哥C++:第一课
- 我的世界刷猪人塔java版_我的世界1.11.2自动猪人塔制作指南 猪人塔存档下载 | 我的世界 | MC世界侠...
- androidstudio引用本地maven_android studio 之dependence添加依赖maven仓库中的项目出错
- Web系统常见安全漏洞介绍及解决方案-sql注入