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

一、小内存的分配基础

1、kmem_cache_alloc_node的作用

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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到底是什么样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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里面是如何存放缓存块的

1
2
3
4
5
6
7
8
9
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这的定义

1
2
3
4
5
6
7
8
9
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。你还是先重点看这里面的注释,这里面说的就是快速通道和普通通道的概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
 * 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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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,开始在这个节点进行分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
 * 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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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、页面换出是以内存节点为单位的吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
 * 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算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
 * 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、内存页的分类

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 将从伙伴系统申请的大内存块切成小内存,分配给其他系统

 

posted @   活的潇洒80  阅读(4145)  评论(1编辑  收藏  举报
编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示