一、小内存的分配基础

1、kmem_cache_alloc_node的作用

通过这段代码可以看出,它调用了kmem_cache_alloc_node函数,在task_struct的缓存区域task_struct分配了一块内存

static struct kmem_cache *task_struct_cachep;

task_struct_cachep = kmem_cache_create("task_struct",
arch_task_struct_size, align,
SLAB_PANIC|SLAB_NOTRACK|SLAB_ACCOUNT, NULL); 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);
}

1、在系统初始化的时候,task_struct_cachep 会被 kmem_cache_create 函数创建。

2、这个函数也比较容易看懂、专门用于分配 task_struct 对象的缓存。这个缓存区的名字就叫 task_struct。

3、缓存区中每一块的大小正好等于 task_struct 的大小,也即 arch_task_struct_size。

1、kmem_cache_alloc_node函数的作用?

1、有了这个缓存区,每次创建task_struct的时候,我们就不用到内存里面去分配,先在缓存里面看看有没有直接可用的,这就是kmem_cache_alloc_node的作用

2、kmem_cache_free的作用

当一个进程结束,task_struct 也不用直接被销毁,而是放回到缓存中,这就是kmem_cache_free的作用,

这样,新进程创建的时候,我们就可以直接用现成的缓存中的task_struct了

2、缓存区struct kmem_cache到底是什么样子

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];
};

3、LIST_HEAD

1、在 struct kemem_cache里面,有个变量struct list_head list,这个结构我们已经看到过多次了

2、我们可以想象一下,对于操作系统来讲,要创建和管理的缓存绝对不止task_struct,难道mm_struct就不需要吗?

3、fs_struct就不需要吗?都需要,因此所有的缓存最后都会放在一个链表里面这就是LIST_HEAD(slab_caches)

对于缓存来来讲,其实就是分配了连续几页的答内存块,然后根据缓存对象的大小,切成小内存块所以我们这里有三个kmem_cache_order_objects 类型的变量:

1、这里面有order,就是2的order次方个页面的答内存块,

2、objects就是能够存放的缓存对象的数量

最终,我们讲答内存块切分成小内存块,样子就像下面这样

每一项的结构都是缓存对象后面跟一个下一个空闲对象的指针,这样非常方便将所有的空闲对象链成一个链,其实这就相当于咱们数据结构

里面学的,用数组实现一个可随机插入和删除的链表

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

那这些缓存对象那些被分配了,那些在空着,什么情况下整个大内存块被分配完,需要向伙伴系统申请几个页形成新的大内存块?这些信息该由谁来维护呢?

接下来就是最重要的两个成员变量出场的时候了kmem_cache_cpu和kmem_cache_node,它们是每个NUMA节点上有一个,我们只需要看一个节点里面的情况

二、小内存分配详解

1、分配总流程图

我们来看一下,kemem_cache_cpu里面是如何存放缓存块的

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
......
};

在这里,page指向的答内存块的第一个页,缓存块就是从里面分配的,freelist指向大内存块里面第一个空闲的项按照上面说的,这一项会有指针指向下一个空闲的项,最终所有空闲的项会形成一个链表

partial指向的页是大内存块的第一个页,之所以明叫partial(部分),就是因为它里面部分被分配出去了,部分是空的,这是一个备用列表当page满了,就会从这里找

我们来看一下,kemem_cache_node这的定义

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的备用列表,如果那里没有,就到这里来找。

2、分配过程源码解析

下面我们就来看看这个分配过程。kmem_cache_alloc_node 会调用 slab_alloc_node。你还是先重点看这里面的注释,这里面说的就是快速通道和普通通道的概念

/*
* 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。

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

如果真的还不行,那就要到 new_slab_objects 了

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,开始在这个节点进行分配

/*
* 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;
......
list_for_each_entry_safe(page, page2, &n->partial, lru) {
void *t; 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;
}
......
return object;

acquire_slab 会从 kmem_cache_node的partial 链表中拿下一大块内存来,并且将 freelist也就是第一块空闲的缓存块,赋值给t

并且当第一轮循环的时候,将kmem_cache_cpu的page指向去下来的这一大块内存,返回的object就是这块内存里面的第一个缓存t

如果kmem_cache_cpu也有一个partial,就会进行第二轮,再次取下一大块内存来,这次调用put_cpu_partial,放到 kmem_cache_cpu的 partial 里面。

如果kmem_cache_node里面也没有空闲的内存,这就说明原来分配的页里面都放满了,就要回到 new_slab_objects 函数,里面new_slab 函数会调用 allocate_slab。

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来。如果第一次分配不成功、说明内存已经很紧张了,那就换成min版本的kmem_cache_order_objects

好了,这个复杂的层层分配机制,我们就讲到这里,你理解到这里也就够用了

三、页面换出

1、页面什么时候放到物理内存中?

虚拟地址空间非常大、物理内存不可能有这么多的空间放得下,所以一般情况下,页面只有在被使用的时候,才会放在物理内存

如果过一段时间不被使用,即便用户进程并没有释放,物理内存管理也有责任做一定的干预,

例如这些物理内存中的页面换出到硬盘上去;将空出的物理内存,交给活跃的进程去使用

2、什么情况下触发页面换出呢?

1、分配内存的时候发现没有地方了,就试图回收一下

2、内存管理系统主动去做,而不是等真的出事再做,这就是内核线程kswapd

3、页面换出是以内存节点为单位的吗?

/*
* The background pageout daemon, started as a kernel thread
* from the init process.
*
* This basically trickles out pages so that we have _some_
* free memory available even if there is no other activity
* that frees anything up. This is needed for things like routing
* etc, where we otherwise might have all activity going on in
* asynchronous contexts that cannot page things out.
*
* If there are applications that are active memory-allocators
* (most normal use), this basically shouldn't matter.
*/
static int kswapd(void *p)
{
unsigned int alloc_order, reclaim_order;
unsigned int classzone_idx = MAX_NR_ZONES - 1;
pg_data_t *pgdat = (pg_data_t*)p;
struct task_struct *tsk = current; for ( ; ; ) {
......
kswapd_try_to_sleep(pgdat, alloc_order, reclaim_order,
classzone_idx);
......
reclaim_order = balance_pgdat(pgdat, alloc_order, classzone_idx);
......
}
}

例如,咱们解析申请一个页面的时候,会调用get_page_from_freelist,接下来的调用链

通过这个调用链,可以看出,页面换出也是以内存节点为单位的

这里的调用链是 balance_pgdat kswapd_shrink_node->shrink_node是以内存节点为单位的,最后也调用shrink_node会调用 shrink_node_memcg。

这里面有一个循环处理页面的列表,看这个函数的注释,其实和上面我们想表达的内存换出是一样的

4、LRU算法

/*
* 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);
}
}
......
}
......

5、内存页的分类

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_evictable_lru(lru) for (lru = 0; lru <= LRU_ACTIVE_FILE; lru++) 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);

从上面的代码可以看出:

1、shrink_list会先缩减毁约页面列表,再压缩不毁约的页面列表,

2、对于不活跃的缩减,shrink_inactive_list就需要对页面进行回收;

3、对于匿名页来讲,需要分配swap,将内存页写入文件系统;

4、对于内存映射关联了文件的,我们需要将在内存中对于文件的修改写回到文件中

四、总结时刻

好了,对于物理内存的管理就讲到这里,我们来总结一下,对于物理内存来讲,从下层到上层的关系及分配模式如何:

1、物理内存分NUMA节点,分别进行管理

2、每个 NUMA 节点分成多个内存区域;

3、每个内存区域分成多个物理页面;

4、伙伴系统将多个连续的页面作为一个大的内存块分配给上层;

5、kswapd 负责物理页面的换入换出;

6、Slub Allocator 将从伙伴系统申请的大内存块切成小内存,分配给其他系统

趣谈Linux操作系统学习笔记:第二十四讲的更多相关文章

  1. 深挖计算机基础:趣谈Linux操作系统学习笔记

    参考极客时间专栏<趣谈Linux操作系统>学习笔记 核心原理篇:内存管理 趣谈Linux操作系统学习笔记:第二十讲 趣谈Linux操作系统学习笔记:第二十一讲 趣谈Linux操作系统学习笔 ...

  2. 趣谈Linux操作系统学习笔记:第二十讲

    一.引子 1.计算两方面的原因 2.内存管理机制 二.独享内存空间的原理 1.会议室和物理内存的关系 和会议室一样,内存都被分成一块块儿的,都编号了号,例如3F-10就是三楼十号会议室.内存页有这样一 ...

  3. 趣谈Linux操作系统学习笔记:第二十五讲

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

  4. 趣谈Linux操作系统学习笔记:第二十六讲

    一.内核页表 和用户态页表不同,在系统初始化的时候,我们就要创建内核页表了 我们从内核页表的根swapper_pg_dir开始找线索,在linux-5.1.3/arch/x86/include/asm ...

  5. 趣谈Linux操作系统学习笔记:第二十八讲

    一.引子 磁盘→盘片→磁道→扇区(每个 512 字节) ext* 定义文件系统的格式 二.inode 与块的存储 1.块 2.不用给他分配一块连续的空间 我们可以分散成一个个小块进行存放 1.优点 2 ...

  6. 趣谈Linux操作系统学习笔记:第二十九讲

    一.引子 在这之前,有一点你需要注意.解析系统调用是了解内核架构最有力力的一把钥匙,这里我们只要重点关注这几个最重要的系统调用就可以了 1.mount 系统调用用于挂载文件系统:2.open 系统调用 ...

  7. 趣谈Linux操作系统学习笔记:第二十七讲

    一.文件系统的功能规划 1.引子 咱们花了这么长的时间,规划了会议室管理系统,这样多个项目执行的时候,隔离性可以得到保证. 但是,会议室里面被回收,会议室里面的资料就丢失了.有一些资料我们希望项目结束 ...

  8. 趣谈Linux操作系统学习笔记:第二十一讲

    一.分段机制 1.分段机制的原理图 2.段选择子 3.段偏移量 例如,我们将上面的虚拟空间分成以下 4 个段,用 0-3 来编号.每个段在段表中有一个项,在物理空间中,段的排列如下图的右边所示. 4. ...

  9. [ExtJS5学习笔记]第二十四节 Extjs5中表格gridpanel或者表单数据后台传输remoteFilter设置

    本文地址:http://blog.csdn.net/sushengmiyan/article/details/39667533 官方文档:http://docs.sencha.com/extjs/5. ...

随机推荐

  1. FCC---CSS Flexbox: Apply the flex-direction Property to Create Rows in the Tweet Embed

    The header and footer in the tweet embed example have child items that could be arranged as rows usi ...

  2. Django 执行 makemigrations 显示 No changes detected in app

    在Django项目配置一下多数据库,但是运行 makemigrations 执行不正常 $ python manage.py makemigrations polls No changes detec ...

  3. Cesium专栏-淹没分析(附源码下载)

    Cesium 是一款面向三维地球和地图的,世界级的JavaScript开源产品.它提供了基于JavaScript语言的开发包,方便用户快速搭建一款零插件的虚拟地球Web应用,并在性能,精度,渲染质量以 ...

  4. 一文解读JSON (转)

    JSON作为目前Web主流的数据交换格式,是每个IT技术人员都必须要了解的一种数据交换格式.尤其是在Ajax和REST技术的大行其道的当今,JSON无疑成为了数据交换格式的首选! 今天我们一起来学习一 ...

  5. Oracle - crfclust.bdb文件太大

    今天在检查oracle rac集群时,突然才发现服务器的根目录下面占用了很多空间,照道理不应该出现这种情况,初步猜想可能是哪个日志或跟踪文件太大导致.切换到跟目录,使用du -sh *来一层一层查看到 ...

  6. Centos8尝鲜

    Centos 8阿里云下载地址https://mirrors.aliyun.com/centos/8.0.1905/isos/x86_64/ Centos8的一些变化 网络服务: 在/etc/sysc ...

  7. 关于spring boot上手的一点介绍

    在spring官网网址 https://spring.io/guides 下,有许多相关介绍,包括可以构建的例子程序. 使用intellij idea,可以通过新建 spring boot initi ...

  8. CALL和RET指令实验

    实验10 1.在屏幕8行3列,用绿色显示data段中的字符串 assume cs:code data segment db data ends code segment start: ;行 ;列 ;颜 ...

  9. Samba安装及配置

    samba 可以实现Windows对Windows . Windows对Linux.Linux对Linux的文件传输 在centos7安装samba yum install samba 启动samba ...

  10. Linux & Go & Vscode & 插件

    Linux Deepin 安装Go 安装Go环境 sudo apt-get install golang 验证一下: 输入 $ go env 输出 GOARCH="amd64" G ...