Kernel源码分析-内存管理

整理破碎的知识体系……

根据这个大佬的公众号👉#聊聊Linux内核

内核版本5.19(buddy system前),5.4(buddy system即之后)

虚拟内存管理

先放一张结构图

  • task_struct的mm指向mm_struct结构体
  • mm_struct的mmap成员指向vm_area_struct双向链表的头节点
  • vm_area_struct通过双向链表串联
  • vm_area_struct的vm_mm指向所属的mm_struct
  • mm_struct的mm_rb通过红黑树组织所有vm_area_struct
  • vm_area_struct的vm_rb指向所属红黑树

新版本的vm_area_struct改用maple_tree组织了

mm_struct结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct mm_struct {
struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb;
unsigned long task_size; /* size of task vm space */

unsigned long total_vm; /* Total pages mapped */
unsigned long locked_vm; /* Pages that have PG_mlocked set */
atomic64_t pinned_vm; /* Refcount permanently increased */
unsigned long data_vm; /* VM_WRITE & ~VM_SHARED & ~VM_STACK */
unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE & ~VM_STACK */
unsigned long stack_vm; /* VM_STACK */

unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
};
  • task_size:用户空间虚拟地址大小,等于TASK_SIZE宏,64位是0x00007ffffffff000
  • 定义内存区域的成员:
    • start_code,end_code:代码段
    • start_data,end_data:数据段
    • start_brk,brk:堆地址起始地址,结束地址
    • mmap_base:内存映射区起始地址
    • start_stack:栈起始位置(栈底)
    • arg_start,arg_end:参数列表
    • env_start,env_end:环境变量
  • 物理内存映射内容相关统计变量:
    • total_vm:进程虚拟内存空间中映射物理内存页的总数
    • locked_vm:锁定不能换出的内存页总数
    • pinned_vm:既不能换出,也不能移动的内存页总数
    • data_vm:数据段中映射的内存页数目
    • exec_vm:代码段中存放可执行文件的内存页数目
    • stack_vm:栈中所映射的内存页数目

vm_area_struct结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct vm_area_struct {
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end;

struct vm_area_struct *vm_next, *vm_prev;

struct rb_node vm_rb;

struct mm_struct *vm_mm; /* The address space we belong to. */

pgprot_t vm_page_prot;
unsigned long vm_flags; /* Flags, see mm.h. */

struct anon_vma *anon_vma; /* Serialized by page_table_lock */

const struct vm_operations_struct *vm_ops;

unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units */
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */
} __randomize_layout;
  • vm_start,vm_end:虚拟地址范围(左闭右开)

    • vm_page_prot:页级别访问控制
    • vm_flags:虚拟内存访问控制
    1
    vma->vm_page_prot = vm_get_page_prot(vma->vm_flags) // 转换
  • vm_file:关联被映射的文件

  • vm_pgoff:映射进虚拟内存中的文件内容,在文件中的偏移

  • anon_vma:匿名映射

  • vm_private_data:用于存储VMA中的私有数据

  • vm_ops:针对虚拟内存区域VMA的相关操作的函数指针

    1
    2
    3
    4
    5
    struct vm_operations_struct {
    void (*open)(struct vm_area_struct * area);
    void (*close)(struct vm_area_struct * area);
    vm_fault_t (*fault)(struct vm_fault *vmf);
    };
    • open:指定的虚拟内存区域被加入到进程虚拟内存空间中时调用
    • close:虚拟内存区域VMA从进程虚拟内存空间中被删除时调用
    • fault:发生缺页异常时调用(未分配物理页或被换出)
    • page_mkwrite:当一个只读的页面将要变为可写时调用

虚拟内存空间布局

32位

还是先放一张图

  • 前896M物理内存直接映射到3G—3G+896M

    • 前1M被系统启动占用(BIOS什么的)
    • 后面是内核代码段,数据段,BSS段
    • 进程相关的数据结构,内核栈也会存放在物理内存前896M的这段区域中

    X86 体系结构下,ISA总线的DMA(直接内存存取)控制器,只能对内存的前16M 进行寻址,所以直接映射区又分为DMA映射区和NORMAL映射区

  • 剩下的物理内存动态映射到vmalloc动态映射区,使用vmalloc进行分配,分配的物理页是不连续的

  • 永久映射区允许建立与物理高端内存(896M以上)的长期映射关系,比如内核通过 alloc_pages() 函数在物理内存的高端内存中申请获取到的物理内存页,这些物理内存页可以通过调用 kmap 映射到永久映射区中

  • 固定映射区类似永久映射区,但映射的物理页是固定的,在编译期间就已经确定

  • 临时映射区用于拷贝物理页时临时将物理内存页映射到虚拟内存

64位

依然先放一张图

大部分在32位都有

  • 虚拟内存映射区用于存放page结构体
  • 代码段用于映射内核代码

物理内存管理

物理内存模型

FLATMEM平坦内存模型

一个page全局数组mem_map管全部

pfn和page的转换

1
2
3
4
5
6
#if defined(CONFIG_FLATMEM)

#define __pfn_to_page(pfn) (mem_map + ((pfn) - ARCH_PFN_OFFSET))
#define __page_to_pfn(page) ((unsigned long)((page) - mem_map) + \
ARCH_PFN_OFFSET)
#elif defined(CONFIG_DISCONTIGMEM)

DISCONTIGMEM非连续内存模型

pglist_data表示node

1
2
3
4
5
typedef struct pglist_data {
#ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */
struct page *node_mem_map;
#ifdef CONFIG_PAGE_EXTENSION
}

node_mem_map数组管理page

pfn和page的转换多了一步

  • arch_pfn_to_nid根据物理页的pfn定位到所在node
  • page_to_nid根据page定位到所在node

之后一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#elif defined(CONFIG_DISCONTIGMEM)

#define __pfn_to_page(pfn) \
({ unsigned long __pfn = (pfn); \
unsigned long __nid = arch_pfn_to_nid(__pfn); \
NODE_DATA(__nid)->node_mem_map + arch_local_page_offset(__pfn, __nid);\
})

#define __page_to_pfn(pg) \
({ const struct page *__pg = (pg); \
struct pglist_data *__pgdat = NODE_DATA(page_to_nid(__pg)); \
(unsigned long)(__pg - __pgdat->node_mem_map) + \
__pgdat->node_start_pfn; \
})

SPARSEMEM稀疏内存模型

mem_section管理

1
2
3
struct mem_section {
unsigned long section_mem_map;
}

section_mem_map其实是个指针,指向管理的page数组

用一个全局数组管理所有section

1
extern struct mem_section **mem_section;

pfn和page的转换更复杂了

  • 如果有vmemmap直接使用vmemap(64位空间多随意挥霍( •̀ ω •́ )✧)
  • page_to_pfn先根据page定位到section,再通过section_mem_map定位到pfn
  • pfn_to_page先根据pfn定位到section,再通过pfn从section_mem_map指向的数组定位到page
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#elif defined(CONFIG_SPARSEMEM_VMEMMAP)

/* memmap is virtually contiguous. */
#define __pfn_to_page(pfn) (vmemmap + (pfn))
#define __page_to_pfn(page) (unsigned long)((page) - vmemmap)

#elif defined(CONFIG_SPARSEMEM)
/*
* Note: section's mem_map is encoded to reflect its start_pfn.
* section[i].section_mem_map == mem_map's address - start_pfn;
*/
#define __page_to_pfn(pg) \
({ const struct page *__pg = (pg); \
int __sec = page_to_section(__pg); \
(unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec))); \
})

#define __pfn_to_page(pfn) \
({ unsigned long __pfn = (pfn); \
struct mem_section *__sec = __pfn_to_section(__pfn); \
__section_mem_map_addr(__sec) + __pfn; \
})
#endif /* CONFIG_FLATMEM/DISCONTIGMEM/SPARSEMEM */

物理内存架构

  • UMA架构(一致性内存访问)

  • NUMA架构(非一致性内存访问)

    访问本地内存节点很快,访问其他内存节点很慢

NUMA的分配策略

  • MPOL_BIND:必须在绑定的节点进行分配,内存不足则进行swap
  • MPOL_INTERLEAVE:本地节点和远程节点都可以进行分配
  • MPOL_PREFERRED:优先在指定节点分配内存,当指定节点内存不足时,选择离指定节点最近的节点分配内存
  • MPOL_LOCAL(默认):优先在本地节点分配,当本地节点内存不足时,可以在远程节点分配内存

NUMA节点管理

物理内存在内核中管理的层级关系为:Node→Zone→page

一个node表示一个NUMA节点

先放一张总体结构图

  • 使用一个pglist_data的全局数组node_data管理所有node

    1
    2
    struct pglist_data *node_data[MAX_NUMNODES] __read_mostly;
    EXPORT_SYMBOL(node_data);
  • NUMA节点描述结构体pglist_data

    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
    typedef struct pglist_data {
    struct zone node_zones[MAX_NR_ZONES];

    struct zonelist node_zonelists[MAX_ZONELISTS];

    int nr_zones; /* number of populated zones in this node */

    unsigned long node_start_pfn;
    unsigned long node_present_pages; /* total number of physical pages */
    unsigned long node_spanned_pages; /* total size of physical page
    range, including holes */
    int node_id;
    wait_queue_head_t kswapd_wait;
    wait_queue_head_t pfmemalloc_wait;
    struct task_struct *kswapd; /* Protected by
    mem_hotplug_begin/end() */
    int kswapd_order;
    enum zone_type kswapd_highest_zoneidx;

    int kswapd_failures; /* Number of 'reclaimed == 0' runs */

    #ifdef CONFIG_COMPACTION
    int kcompactd_max_order;
    enum zone_type kcompactd_highest_zoneidx;
    wait_queue_head_t kcompactd_wait;
    struct task_struct *kcompactd;
    #endif
    } pg_data_t;
    • node_id:NUMA节点id

    • node_mem_map:page类型数组,包含NUMA中的所有物理页

    • node_start_pfn:指向NUMA节点内第一个物理页的PFN(PFN全局唯一)

    • node_present_pages:可用的物理页面数量(不包含空洞)

    • node_spanned_pages:所有物理页面数量(包含空洞)

    • nr_zones:用于统计NUMA节点内包含的物理内存区域个数

      注:只有第一个NUMA节点可以包含所有种类的zone,比如DMA必须从物理内存起点开始

    • node_zones:zone数组,包含NUMA节点中的所有物理内存区域

    • node_zonelists:zonelist数组,包含了备用NUMA节点和这些备用节点中的物理内存区域(备用节点按访问距离远近排列)

    • kswapd:一个用于回收不经常使用的页面的进程

    • kcompactd:一个用于内存的规整避免内存碎片的进程

  • NUMA节点物理内存区域的划分

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    enum zone_type {
    #ifdef CONFIG_ZONE_DMA
    ZONE_DMA,
    #endif
    #ifdef CONFIG_ZONE_DMA32
    ZONE_DMA32,
    #endif
    ZONE_NORMAL,
    #ifdef CONFIG_HIGHMEM
    ZONE_HIGHMEM,
    #endif
    ZONE_MOVABLE,
    #ifdef CONFIG_ZONE_DEVICE
    ZONE_DEVICE,
    #endif
    __MAX_NR_ZONES

    };
    • ZONE_DMA:用于那些无法对全部物理内存进行寻址的硬件设备,进行 DMA 时的内存分配(如ISA只能寻址前16M)
    • ZONE_DMA32:提供给32位设备(只能寻址4G物理内存)执行DMA操作时使用的(只在64位系统中起作用)
    • ZONE_NORMAL:直接映射的896M剩下的部分
    • ZONE_HIGHMEM:剩下的高端内存
    • ZONE_DEVICE:为支持热插拔设备而分配的非易失性内存(也可用于内核崩溃时保存相关的调试信息)
    • ZONE_MOVABLE:内核定义的一个虚拟内存区域,该zone中的物理页可以来自于上边介绍的几种真实的物理区域,页都是可以迁移的,主要是为了防止内存碎片和支持内存的热插拔
  • node_states:NUMA节点不止一个的时候使用,位图,用于维护各个NUMA节点的状态信息(只有一个NUMA节点时没有)

    1
    2
    typedef struct { DECLARE_BITMAP(bits, MAX_NUMNODES); } nodemask_t;
    extern nodemask_t _unused_nodemask_arg_;

    节点状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    enum node_states {
    N_POSSIBLE, /* The node could become online at some point */
    N_ONLINE, /* The node is online */
    N_NORMAL_MEMORY, /* The node has regular memory */
    #ifdef CONFIG_HIGHMEM
    N_HIGH_MEMORY, /* The node has regular or high memory */
    #else
    N_HIGH_MEMORY = N_NORMAL_MEMORY,
    #endif
    N_MEMORY, /* The node has memory(regular, high, movable) */
    N_CPU, /* The node has one or more cpus */
    N_GENERIC_INITIATOR, /* The node has one or more Generic Initiators */
    NR_NODE_STATES
    };
    • N_POSSIBLE:节点随时会上线
    • N_ONLINE:节点已上线
    • N_NORMAL_MEMORY:节点没有高端内存,只有ZONE_NORMAL
    • N_HIGH_MEMORY:节点有ZONE_NORMAL或者有ZONE_HIGHMEM
    • N_MEMORY:节点有ZONE_NORMAL,ZONE_HIGHMEM,ZONE_MOVABLE
    • N_CPU:表示节点包含一个或多个 CPU

NUMA节点中的物理内存管理

zone和node的关系

zone结构体

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
struct zone {
unsigned long _watermark[NR_WMARK];
unsigned long watermark_boost;

unsigned long nr_reserved_highatomic;

long lowmem_reserve[MAX_NR_ZONES];

struct pglist_data *zone_pgdat;
struct per_cpu_pageset __percpu *pageset;

unsigned long zone_start_pfn;

atomic_long_t managed_pages;
unsigned long spanned_pages;
unsigned long present_pages;

const char *name;

ZONE_PADDING(_pad1_)

struct free_area free_area[MAX_ORDER];

spinlock_t lock;

ZONE_PADDING(_pad3_)
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;
  • lock:防止并发访问

  • name:内存区域名称,Normal / DMA / HighMem

  • zone_pgdat:指向所属的NUMA节点

  • zone_start_pfn:属于该zone中的第一个物理页PFN

  • spanned_pages:zone中所有物理页个数(包括空洞)

  • present_pages:zone中可用物理页个数(不包括空洞)

  • managed_pages:buddy system管理的物理页个数

  • free_area:buddy system的核心数据结构

  • vm_stat:该zone使用的统计信息

  • nr_reserved_highatomic:该zone预留内存的大小[128KB, 65536KB]

  • lowmem_reserve:规定每个内存区域必须为自己保留的物理页数量,防止更高位的内存区域对自己的内存空间进行过多的侵占挤压

  • _watermark:物理内存区域中的水位线

    • 物理内存剩余容量大于_watermark[WMARK_HIGH] → 内存充足,分配无压力
    • 大于_watermark[WMARK_LOW],小于_watermark[WMARK_HIGH] → 分配有压力但可接受
    • 大于_watermark[WMARK_MIN],小于_watermark[WMARK_LOW] → 唤醒kswapd进程开始异步回收
    • 小于_watermark[WMARK_MIN] → 阻塞请求分配的进程,唤醒kswapd进程进行回收,回收完毕唤醒阻塞的进程
  • watermark_boost:优化内存碎片对内存分配的影响,可以动态改变内存区域的基准水位线

  • pageset:per_cpu_pageset,管理冷热页(__percpu变量)

    1
    2
    3
    struct per_cpu_pageset {
    struct per_cpu_pages pcp;
    };

    用一个双向列表管理per_cpu_pages,热页在前,冷页在后

    per_cpu_pages结构体

    1
    2
    3
    4
    5
    6
    7
    8
    struct per_cpu_pages {
    int count; /* number of pages in the list */
    int high; /* high watermark, emptying needed */
    int batch; /* chunk size for buddy add/remove */

    /* Lists of pages, one per migrate type stored on the pcp-lists */
    struct list_head lists[MIGRATE_PCPTYPES];
    };
    • lists:双向列表
    • count:物理页数量
    • high:count超过了high,那么表示list中的页面太多了,内核会从高速缓存中释放batch个页面到物理内存区域中的伙伴系统中
    • batch:见上

物理内存页描述

page结构体

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
struct page {
// 存储 page 的定位信息以及相关标志位
unsigned long flags;

union {
struct { /* Page cache and anonymous pages */
// 用来指向物理页 page 被放置在了哪个 lru 链表上
struct list_head lru;
// 如果 page 为文件页的话,低位为0,指向 page 所在的 page cache
// 如果 page 为匿名页的话,低位为1,指向其对应虚拟地址空间的匿名映射区 anon_vma
struct address_space *mapping;
// 如果 page 为文件页的话,index 为 page 在 page cache 中的索引
// 如果 page 为匿名页的话,表示匿名页在对应进程虚拟内存区域 VMA 中的偏移
pgoff_t index;
// 在不同场景下,private 指向的场景信息不同
unsigned long private;
};

struct { /* slab, slob and slub */
union {
// 用于指定当前 page 位于 slab 中的哪个具体管理链表上。
struct list_head slab_list;
struct {
// 当 page 位于 slab 结构中的某个管理链表上时,next 指针用于指向链表中的下一个 page
struct page *next;
#ifdef CONFIG_64BIT
// 表示 slab 中总共拥有的 page 个数
int pages;
// 表示 slab 中拥有的特定类型的对象个数
int pobjects;
#else
short int pages;
short int pobjects;
#endif
};
};
// 用于指向当前 page 所属的 slab 管理结构
struct kmem_cache *slab_cache;

// 指向 page 中的第一个未分配出去的空闲对象
void *freelist;
union {
// 指向 page 中的第一个对象
void *s_mem;
struct { /* SLUB */
// 表示 slab 中已经被分配出去的对象个数
unsigned inuse:16;
// slab 中所有的对象个数
unsigned objects:15;
// 当前内存页 page 被 slab 放置在 CPU 本地缓存列表中,frozen = 1,否则 frozen = 0
unsigned frozen:1;
};
};
};
struct { /* 复合页 compound page 相关*/
// 复合页的尾页指向首页
unsigned long compound_head;
// 用于释放复合页的析构函数,保存在首页中
unsigned char compound_dtor;
// 该复合页有多少个 page 组成
unsigned char compound_order;
// 该复合页被多少个进程使用,内存页反向映射的概念,首页中保存
atomic_t compound_mapcount;
};

// 表示 slab 中需要释放回收的对象链表
struct rcu_head rcu_head;
};

union { /* This union is 4 bytes in size. */
// 表示该 page 映射了多少个进程的虚拟内存空间,一个 page 可以被多个进程映射
atomic_t _mapcount;

};

// 内核中引用该物理页的次数,表示该物理页的活跃程度。
atomic_t _refcount;

#if defined(WANT_PAGE_VIRTUAL)
void *virtual; // 内存页对应的虚拟内存地址
#endif /* WANT_PAGE_VIRTUAL */

} _struct_page_alignment;
  • mapping:page cache(高速页缓存)结构体address_space,被文件的inode持有

    • page为文件页:mapping最低位为0,指向页关联文件的address_space
    • page为匿名页:mapping最低位为1,指向匿名页在进程虚拟内存空间中的匿名映射区域anon_vma结构,用于物理内存到虚拟内存的反向映射
  • index:pgoff_t

    • page为文件页:表示该内存页中的文件数据在文件内部的偏移offset,偏移单位为页大小
    • page为匿名页:表示匿名页在对应进程虚拟内存区域VMA中的偏移
  • _mapcount:表示该page映射了多少个进程的虚拟内存空间,一个page可以被多个进程映射

  • lru:指向物理页被放置在了哪个链表上(active,inactive)

  • _refcount:用来记录内核中引用该物理页的次数,表示该物理页的活跃程度

  • flags:

    • 高8位储存定位信息(section,zone,node)
    • 储存访问相关,换入换出相关等标志
  • 复合页相关

    • flags:如果是首页会设置PG_head
    • compound_head:复合页的尾页用来指向首页
    • compound_dtor:用于释放复合页的析构函数
    • compound_order:复合页的order
    • compound_mapcount:使用复合页的进程个数,内存页反向映射的概念,首页中保存
    • compound_pincount:复合页使用计数,首页中保存

匿名页的反向映射

在物理页需要被迁移或者回收时使用,此时需要找到物理页映射的虚拟地址,并断开连接

  • page:结构体表示一个物理页

  • anon_vma:表示一个匿名页(仅用于反向映射)

  • anon_vma_chain:表示一个匿名页和一段虚拟内存的关系

    匿名页和虚拟内存是一对多的关系,因为一个匿名页可能映射到很多进程的虚拟空间 → anon_vma和anon_vma_chain也是一对多的关系

  • vm_area_struct:表示一段虚拟内存

  • page中的mapping指向anon_vma结构体

  • anon_vma结构体中的rb_root红黑树储存了所有与匿名页相关的anon_vma_chain

    1
    2
    3
    4
    5
    6
    7
    8
    struct anon_vma {
    struct anon_vma *root; /* Root of this anon_vma tree */
    struct rw_semaphore rwsem; /* W: modification, R: walking the list */
    atomic_t refcount;
    unsigned degree;
    struct anon_vma *parent; /* Parent of this anon_vma */
    struct rb_root_cached rb_root;
    };
  • anon_vma_chain结构体串联vm_area_struct和anon_vma

    • vma指向相关的vm_area_struct
    • anon_vma指向相关的虚拟页
    • same_vma双向链表串联了vma的所有匿名页
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct anon_vma_chain {
    struct vm_area_struct *vma;
    struct anon_vma *anon_vma;
    struct list_head same_vma; /* locked by mmap_lock & page_table_lock */
    struct rb_node rb; /* locked by anon_vma->rwsem */
    unsigned long rb_subtree_last;
    #ifdef CONFIG_DEBUG_VM_RB
    unsigned long cached_vma_start, cached_vma_last;
    #endif
    };
  • vm_area_struct表示VMA

    1
    2
    3
    4
    struct vm_area_struct {
    struct list_head anon_vma_chain;
    struct anon_vma *anon_vma;
    };
    • anon_vma_chain双向链表串连了VMA的所有匿名页
    • anon_vma用于快速判断 VMA 有没有对应的匿名 page

物理内存分配

物理内存分配接口

alloc_pages函数用于请求2的order次幂个page

1
struct page *alloc_pages(gfp_t gfp, unsigned int order);
  • order:分配阶
  • gfp:用于规范物理内存分配行为的修饰符

返回的是page结构体,需要转换成虚拟地址使用,__get_free_pages函数可以返回虚拟地址

1
2
3
4
5
6
7
8
9
10
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
struct page *page;

page = alloc_pages(gfp_mask & ~__GFP_HIGHMEM, order);
if (!page)
return 0;
return (unsigned long) page_address(page);
}
EXPORT_SYMBOL(__get_free_pages);

内部也是调用了alloc_pages,进行了地址转换

  • 不能分配高端内存,高端内存不适用于直接地址转换
  • 返回地址位于直接映射区

get_zeroed_page会把page内容清零

1
2
3
4
5
unsigned long get_zeroed_page(gfp_t gfp_mask)
{
return __get_free_pages(gfp_mask | __GFP_ZERO, 0);
}
EXPORT_SYMBOL(get_zeroed_page);

__get_dma_pages只分配DMA内存页

1
2
#define __get_dma_pages(gfp_mask, order) \
__get_free_pages((gfp_mask) | GFP_DMA, (order))

两个内存释放函数

1
2
void __free_pages(struct page *page, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);
  • __free_pages对应alloc_pages,使用page结构体
  • free_pages对应__get_free_pages,使用虚拟内存地址

物理内存分配源码实现

大佬写的很详细了所以就放两张图doge

函数调用图

__alloc_pages函数的逻辑

buddy system

zone中buddy system相关成员

free_area的下标表示order

1
2
3
4
5
6
struct zone {
// 被伙伴系统所管理的物理内存页个数(present_pages-reserved_pages)
atomic_long_t managed_pages;
// 伙伴系统的核心数据结构
struct free_area free_area[MAX_ORDER];
}

free_area,不同迁移类型的page用不同的链表管理

1
2
3
4
5
6
7
8
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};

struct list_head {
struct list_head *next, *prev;
};

迁移类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum migratetype {
MIGRATE_UNMOVABLE, // 不可迁移
MIGRATE_MOVABLE, // 可迁移
MIGRATE_RECLAIMABLE, // 可回收
MIGRATE_PCPTYPES, // 属于高速缓存(per_cpu_pageset)
MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES, // 紧急内存
#ifdef CONFIG_CMA
MIGRATE_CMA, // 预留的连续内存CMA
#endif
#ifdef CONFIG_MEMORY_ISOLATION
MIGRATE_ISOLATE, // can't allocate from here
#endif
MIGRATE_TYPES // 迁移类型数量
};
  • UNMOVABLE:一般位于直接映射区,内核所需要的核心内存一般从这分配
  • MOVABLE:一般用于在进程用户空间中分配,因为在用户空间中虚拟内存与物理内存都是通过页表来动态映射的,物理页移动之后,只需要改变页表中的映射关系即可,而虚拟内存地址并不需要改变
  • PCPTYPES:里面包含了高速缓存中的冷页和热页
  • RECLAIMABLE:不能移动但可以回收,比如文件缓存页,之后再从文件中读取就行
  • CMA:contiguous memory allocator,是一个分配连续物理内存页面的分配器,用于分配连续的物理内存
  • ISOLATE:一个虚拟区域,用于跨越NUMA节点移动物理内存页,内核可以将物理内存页移动到使用该页最频繁的CPU 所在的NUMA节点中

长这样

如果对应order,migratetype的page不够,从其他migratetype中申请的顺序,注意fallbacks分配的顺序和正常分配的顺序是反的

1
2
3
4
5
static int fallbacks[MIGRATE_TYPES][3] = {
[MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_TYPES },
[MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_TYPES },
[MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_TYPES },
};

放一张get_page_from_freelist函数的逻辑

  • for_next_zone_zonelist_nodemask遍历当前NUMA节点以及备用节点的所有zone(zonelist)
  • zone_watermark_fast检查zone中的剩余内存是否在指定水位线上,是则跳转至try_this_zone调用rmqueue进入buddy system进行内存分配,分配成功后调用prep_new_page初始化分配好的page,否则继续
  • node_reclaim触发内存回收,回收后通过zone_watermark_ok检查回收的内存是否满足本次分配需要,是则跳转至try_this_zone在zone中分配内存

rmqueue包括了buddy system的主逻辑

  • __rmqueue_smallest封装buddy system的核心流程
  • __rmqueue底层调用__rmqueue_smallest,还包括fallback的过程

一次分配的流程图

内存释放源码逻辑

页物理视图

一次释放的过程

slab

内核版本5.4,slub

slab数据结构体

slab结构图

  • 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
    25
    26
    struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;
    /* Used for retrieving partial slabs, etc. */
    slab_flags_t flags;
    unsigned int size; /* The size of an object including metadata */
    unsigned int object_size;/* The size of an object without metadata */
    unsigned int offset; /* Free pointer offset */
    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 *);
    unsigned int inuse; /* Offset to metadata */
    unsigned int align; /* Alignment */
    unsigned int red_left_pad; /* Left redzone padding size */
    const char *name; /* Name (only for display!) */
    struct list_head list; /* List of slab caches */

    unsigned int useroffset; /* Usercopy region offset */
    unsigned int usersize; /* Usercopy region size */

    struct kmem_cache_node *node[MAX_NUMNODES];
    };
    • flags:管理标志位

      • SLAB_HWCACHE_ALIGN:需要进行cache line(64位)对齐

      • SLAB_POISON:需要填充特殊字节表示状态

      • SLAB_RED_ZONE:需要插入red zone防止越界读写

      • SLAB_CACHE_DMA:slab中内存来自哪个内存区域

      • SLAB_STORE_USER:追踪对象的分配释放信息,追加两个track块

    • size:slab对象真实大小

    • object_size:使用的内存大小(上图object size蓝色部分)

    • offset:freepointer的偏移

    • oo:储存slab需要的page个数

    • max:oo的最大值,初始化为oo

    • min:oo的最小值,初始化为1

    • allocflags:分配时所用的标志位

    • inuse:word size对齐后的大小

    • align:综合word size,cache line,align计算一个合理的对齐尺寸

    • name:slab cache的名称

    • refcount:引用计数

    • list:kmem_cache用双向链表串联

    • cpu_slab:每个cpu有一个slab本地缓存

      1
      2
      3
      4
      5
      6
      7
      8
      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
      };
      • freelist:该slab第一个空闲对象
      • tid:cpu编号
      • page:slab所在的page
      • partial:备用的slab
    • node:备用slab(每个NUMA节点一个)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      struct kmem_cache_node {
      spinlock_t list_lock;
      #ifdef CONFIG_SLUB
      unsigned long nr_partial;
      struct list_head partial;
      #ifdef CONFIG_SLUB_DEBUG
      atomic_long_t nr_slabs;
      atomic_long_t total_objects;
      struct list_head full;
      #endif
      #endif

      };
      • nr_patial:该NUMA节点备用slab个数
      • patial:备用slab用一个patial双链表串联起来
      • full:串联所有分配完的slab
  • page结构体中slab相关成员(高版本另有slab结构体),一个page是一个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
    29
    struct page {
    struct { /* slab, slob and slub */
    union {
    struct list_head slab_list;
    struct { /* Partial pages */
    struct page *next;
    #ifdef CONFIG_64BIT
    int pages; /* Nr of pages left */
    int pobjects; /* Approximate count */
    #else
    short int pages;
    short int pobjects;
    #endif
    };
    };
    struct kmem_cache *slab_cache; /* not slob */
    /* Double-word boundary */
    void *freelist; /* first free object */
    union {
    void *s_mem; /* slab: first object */
    unsigned long counters; /* SLUB */
    struct { /* SLUB */
    unsigned inuse:16;
    unsigned objects:15;
    unsigned frozen:1;
    };
    };
    };
    }
    • slab_list:slab所在管理链表
    • next:partial使用
    • pages:slab所在管理链表中的包含的slab总数
    • pobjects:slab所在管理链表中包含的对象总数
    • slab_cache:指向kmem_cache
    • freelist:指向slab中第一个空闲对象,slab放进cpu_slab后将这个指针赋给cpu_slab,然后置空
    • inuse:使用的空间
    • objects:包含的对象个数
    • frozen:1表示在本地cpu缓存中

分配逻辑

  • 从本地cpu缓存中分配(fast path):查看freelist是否有空闲对象

  • 从本地cpu缓存partial列表中分配:本地cpu缓存的slab(page)中没有空闲对象

    • 遍历partial列表,找一个满足分配的slab
    • 将这个slab从partial中摘下,提升为本地cpu缓存
  • 从NUMA节点缓存中分配:遍历kmem_cache_node->partial列表,将链表中的slab摘下来填充到本地cpu缓存的partial链表中(最多cpu_partial / 2个)

  • 从buddy system中重新申请slab:最开始slab是空的

    初始化新申请的slab,提升为cpu本地缓存的page

函数调用关系图

释放逻辑

  • 释放对象所属slab在cpu本地缓存中(fast path):直接放回cpu本地缓存的slab
  • 释放对象所属slab在cpu本地缓存partial链表中:直接释放
  • 释放对象所属slab从full变成partial(slab不在cpu本地缓存中)
    • 对象放回slab并将slab放入本地cpu缓存的partial链表中(partial中slab个数未超标)
    • 将partial链表中的所有slab转移至对应NUMA节点的node->partial链表的尾部,再将slab插入本地缓存的partial链表(partial中slab个数超标)
  • 释放对象所属slab从partial变成empty
    • 不是活跃slab,放回kmem_cache_node->partial链表(partial中slab个数未超标,min_partial)
    • 放回buddy system(partial中slab个数超标)

函数调用关系图

slab内存池的创建初始化流程

就放一张函数调用关系图

slab内存池的销毁

kmalloc体系

kmalloc体系基于slab allocator体系,本质是不同尺寸的通用slab cache

kmalloc内存块尺寸

slab cache信息储存在kmalloc_info数组中,由kmalloc_info_struct结构体表示

1
2
3
4
extern const struct kmalloc_info_struct {
const char *name;
unsigned int size;
} kmalloc_info[];
  • name:slab cache名字
  • size:内存块大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const struct kmalloc_info_struct kmalloc_info[] __initconst = {
{NULL, 0}, {"kmalloc-96", 96},
{"kmalloc-192", 192}, {"kmalloc-8", 8},
{"kmalloc-16", 16}, {"kmalloc-32", 32},
{"kmalloc-64", 64}, {"kmalloc-128", 128},
{"kmalloc-256", 256}, {"kmalloc-512", 512},
{"kmalloc-1k", 1024}, {"kmalloc-2k", 2048},
{"kmalloc-4k", 4096}, {"kmalloc-8k", 8192},
{"kmalloc-16k", 16384}, {"kmalloc-32k", 32768},
{"kmalloc-64k", 65536}, {"kmalloc-128k", 131072},
{"kmalloc-256k", 262144}, {"kmalloc-512k", 524288},
{"kmalloc-1M", 1048576}, {"kmalloc-2M", 2097152},
{"kmalloc-4M", 4194304}, {"kmalloc-8M", 8388608},
{"kmalloc-16M", 16777216}, {"kmalloc-32M", 33554432},
{"kmalloc-64M", 67108864}
};
  • index > 2 时size为2 ^ index
  • 由于内核大部分申请大小都在192和96左右所以单独提供这两种大小的slab cache

size_index[24]数组用于定义小于192大小的内存块的大小选取规则

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
static u8 size_index[24] __ro_after_init = {
3, /* 8 */
4, /* 16 */
5, /* 24 */
5, /* 32 */
6, /* 40 */
6, /* 48 */
6, /* 56 */
6, /* 64 */
1, /* 72 */
1, /* 80 */
1, /* 88 */
1, /* 96 */
7, /* 104 */
7, /* 112 */
7, /* 120 */
7, /* 128 */
2, /* 136 */
2, /* 144 */
2, /* 152 */
2, /* 160 */
2, /* 168 */
2, /* 176 */
2, /* 184 */
2 /* 192 */
};

注释内是申请大小,数组元素是slab cache对应下标

申请尺寸大于192时使用fls函数计算,fls可以获取参数的最高有效bit的位数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* fls = Find Last Set in word
* @result: [1-32]
* fls(1) = 1, fls(0x80000000) = 32, fls(0) = 0
*/
static inline __attribute__ ((const)) int fls(unsigned long x)
{
int n;

asm volatile(
" fls.f %0, %1 \n" /* 0:31; 0(Z) if src 0 */
" add.nz %0, %0, 1 \n" /* 0:31 -> 1:32 */
: "=r"(n) /* Early clobber not needed */
: "r"(x)
: "cc");

return n;
}

kmalloc架构

所有的kmem_cache储存在一个全局数组中

1
2
3
4
struct kmem_cache *
kmalloc_caches[NR_KMALLOC_TYPES][KMALLOC_SHIFT_HIGH + 1] __ro_after_init =
{ /* initialization for https://bugs.llvm.org/show_bug.cgi?id=42570 */ };
EXPORT_SYMBOL(kmalloc_caches);
  • 第一个下标表示物理内存区域类型

    1
    2
    3
    4
    5
    6
    7
    8
    enum kmalloc_cache_type {
    KMALLOC_NORMAL = 0,
    KMALLOC_RECLAIM,
    #ifdef CONFIG_ZONE_DMA
    KMALLOC_DMA,
    #endif
    NR_KMALLOC_TYPES
    };
  • 第二个下标表示slab cache

kmalloc体系的创建

函数调用流程

1
start_kernel -> mm_init -> kmem_cache_init
1
2
3
4
5
6
7
void __init kmem_cache_init(void)
{
/* slab allocator体系的创建初始化 */
/* Now we can use the kmem_cache to allocate kmalloc slabs */
setup_kmalloc_cache_index_table();
create_kmalloc_caches(0);
}

主要就是两个函数

  • setup_kmalloc_cache_index_table:初始化size_index数组
  • create_kmalloc_caches:创建初始化kmalloc_caches二维数组

kmalloc内存池的分配和回收

kmalloc分配

kfree回收(过于简单甚至不配拥有一张图)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void kfree(const void *x)
{
struct page *page;
void *object = (void *)x;

page = virt_to_head_page(x);
if (unlikely(!PageSlab(page))) {
unsigned int order = compound_order(page);

BUG_ON(!PageCompound(page));
kfree_hook(object);
mod_node_page_state(page_pgdat(page), NR_SLAB_UNRECLAIMABLE,
-(1 << order));
__free_pages(page, order);
return;
}
slab_free(page->slab_cache, page, object, NULL, 1, _RET_IP_);
}
EXPORT_SYMBOL(kfree);
  • 使用virt_to_head_page函数将虚拟地址转化为page结构体
  • 通过PageSlab查看page的是否设置PG_slab标识
    • 没有则是从buddy system中分配的,使用__free_pages放回buddy system
    • 否则调用slab_free放回对应slab

页表体系

就放两张图,调试启动流程的时候对这玩意有深刻的认识了(ˉ▽ˉ;)…


Kernel源码分析-内存管理
http://akaieurus.github.io/2023/11/13/Kernel源码分析-内存管理/
作者
Eurus
发布于
2023年11月13日
许可协议