explorer

万丈高楼平地起,勿在浮沙筑高台

0%

[What]Linux 内存管理上的一些细节

参考:

  1. 《Linux 内核设计与实现》
kernel version arch
v5.4.x lts arm32

相对于之前的内存课程稍微深入一点。

虽然在硬件上 CPU 可以寻址的最小单位是字节,但是由于 MMU 是以页为单位来管理物理内存的,所以对于虚拟内存而言,页就是最小单位。

所以内核也将页作为内存管理的基本单位,在 中使用 struct 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
struct page {
//用于存放页的状态,记录页是否为脏,是不是被锁定在内存中等
//flags 用每一位表示一个独立的状态,定义在
unsigned long flags;
//此联合体将会占用 5 个字的大小,对 32 位机来说就是 20 字节
union {
//此结构体记录物理页面对应的有文件背景的页面和匿名页
struct {
struct list_head lru;
struct address_space *mapping;
pgoff_t index;
unsigned long private;
};
//对应的物理地址
struct {
dma_addr_t dma_addr;
};
//……

/** @rcu_head: You can use this to free a page by RCU. */
struct rcu_head rcu_head;
};

//此联合体仅占用 4 字节
union {
atomic_t _mapcount;

unsigned int page_type;

unsigned int active; /* SLAB */
int units; /* SLOB */
};

// 存放页的引用计数
atomic_t _refcount;

#ifdef CONFIG_MEMCG
struct mem_cgroup *mem_cgroup;
#endif

/*
* On machines where all RAM is mapped into kernel address space,
* we can simply calculate the virtual address. On machines with
* highmem some memory is mapped into kernel virtual memory
* dynamically, so we need a place to store that address.
* Note that this field could be 16 bits on x86 ... ;)
*
* Architectures with slow multiplication can define
* WANT_PAGE_VIRTUAL in asm/page.h
*/
#if defined(WANT_PAGE_VIRTUAL)
void *virtual; /* Kernel virtual address (NULL if
not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */

#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
int _last_cpupid;
#endif
};

再次强调: 每个系统中的物理页都有一个 struct page 与之关联 ,内核仅仅用这个数据结构来描述当前时刻再相关的物理页中存放的东西, 这种数据结构的目的在于描述物理内存本身,而不是描述包含在其中的数据。

  • 这就类似于文件系统中的 metadata 一样
  • 既然每个物理页面都有这么一个结构体来描述,那么就要尽量将该结构体的占用做到最小,所以此结构体内部才会用联合来表示

_refcount

_refcount 存放页的引用计数,指有多个虚拟页面指向了该物理页面。

  • 低端内存被线性映射到了内核空间,但被映射不代表就是被使用了

当值为 -1 时,代表当前内核并没有引用这一页,那么新的分配中就可以使用它。

内核编程时,对该值的使用应该调用 中提供的函数,以保证其原子性。

一个页可以被用作:

  • 页缓存:作为硬盘上程序、文件的缓存
    • 这时 mapping 成员指向和这个页关联的 address_space 对象
  • 私有数据:作为进程所申请的内存、栈等
    • 这时 private 指向与之关联的对象
  • 进程页表中的映射:页就是存储该进程虚拟内存到物理内存的映射关系表,就是页表

virtual

virtual 表示该物理页面对应的虚拟地址,有些内存并不是永久地映射到内核地址空间,这种情况下该值为 NULL,在需要的时候再动态的映射。

区(zone)出现的原因:

由于硬件的限制,内核并不能对所有的页一视同仁。有些页位于内存中特定的物理地址上,所以不能将其用于一些特定的任务。

这里的硬件限制(缺陷)是指:

  • 一些硬件的 DMA 寻址范围有限
  • 一些体系结构的内存的物理寻址范围大于虚拟寻址范围

所以便分为了多个区,这在之前的文章中已经提及

每个区都用 struct 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
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
struct zone {
/* Read-mostly fields */

/*
* watermark 表示该区的最小值、最低和最高水位值,以调配内存消耗
*/
unsigned long _watermark[NR_WMARK];
unsigned long watermark_boost;

unsigned long nr_reserved_highatomic;

/*
* 保留的内存区域便于运行特定的程序可以正常的申请到内存
* 并处理特定的任务
*/
long lowmem_reserve[MAX_NR_ZONES];

#ifdef CONFIG_NUMA
int node;
#endif
struct pglist_data *zone_pgdat;
struct per_cpu_pageset __percpu *pageset;

#ifndef CONFIG_SPARSEMEM
/*
* Flags for a pageblock_nr_pages block. See pageblock-flags.h.
* In SPARSEMEM, this map is stored in struct mem_section
*/
unsigned long *pageblock_flags;
#endif /* CONFIG_SPARSEMEM */

/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
//该区的起始页
unsigned long zone_start_pfn;

/*
* spanned_pages is the total pages spanned by the zone, including
* holes, which is calculated as:
* spanned_pages = zone_end_pfn - zone_start_pfn;
*
* present_pages is physical pages existing within the zone, which
* is calculated as:
* present_pages = spanned_pages - absent_pages(pages in holes);
*
* managed_pages is present pages managed by the buddy system, which
* is calculated as (reserved_pages includes pages allocated by the
* bootmem allocator):
* managed_pages = present_pages - reserved_pages;
*
* So present_pages may be used by memory hotplug or memory power
* management logic to figure out unmanaged pages by checking
* (present_pages - managed_pages). And managed_pages should be used
* by page allocator and vm scanner to calculate all kinds of watermarks
* and thresholds.
*
* Locking rules:
*
* zone_start_pfn and spanned_pages are protected by span_seqlock.
* It is a seqlock because it has to be read outside of zone->lock,
* and it is done in the main allocator path. But, it is written
* quite infrequently.
*
* The span_seq lock is declared along with zone->lock because it is
* frequently read in proximity to zone->lock. It's good to
* give them a chance of being in the same cacheline.
*
* Write access to present_pages at runtime should be protected by
* mem_hotplug_begin/end(). Any reader who can't tolerant drift of
* present_pages should get_online_mems() to get a stable value.
*/
atomic_long_t managed_pages;
unsigned long spanned_pages;
unsigned long present_pages;
//该区的名称
const char *name;

#ifdef CONFIG_MEMORY_ISOLATION
/*
* Number of isolated pageblock. It is used to solve incorrect
* freepage counting problem due to racy retrieving migratetype
* of pageblock. Protected by zone->lock.
*/
unsigned long nr_isolate_pageblock;
#endif

#ifdef CONFIG_MEMORY_HOTPLUG
/* see spanned/present_pages for more description */
seqlock_t span_seqlock;
#endif

int initialized;

/* Write-intensive fields used from the page allocator */
ZONE_PADDING(_pad1_)

/* free areas of different sizes */
struct free_area free_area[MAX_ORDER];

/* zone flags, see below */
unsigned long flags;

//对该区的互斥
spinlock_t lock;

/* Write-intensive fields used by compaction and vmstats. */
ZONE_PADDING(_pad2_)

/*
* When free pages are below this point, additional steps are taken
* when reading the number of free pages to avoid per-cpu counter
* drift allowing watermarks to be breached
*/
unsigned long percpu_drift_mark;

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
/* pfn where compaction free scanner should start */
unsigned long compact_cached_free_pfn;
/* pfn where async and sync compaction migration scanner should start */
unsigned long compact_cached_migrate_pfn[2];
unsigned long compact_init_migrate_pfn;
unsigned long compact_init_free_pfn;
#endif

#ifdef CONFIG_COMPACTION
/*
* On compaction failure, 1<
* are skipped before trying again. The number attempted since
* last failure is tracked with compact_considered.
*/
unsigned int compact_considered;
unsigned int compact_defer_shift;
int compact_order_failed;
#endif

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
/* Set to true when the PG_migrate_skip bits should be cleared */
bool compact_blockskip_flush;
#endif

bool contiguous;

ZONE_PADDING(_pad3_)
/* Zone statistics */
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
atomic_long_t vm_numa_stat[NR_VM_NUMA_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;

区的名称在 mm/page_alloc.c 中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

static char * const zone_names[MAX_NR_ZONES] = {
#ifdef CONFIG_ZONE_DMA
"DMA",
#endif
#ifdef CONFIG_ZONE_DMA32
"DMA32",
#endif
"Normal",
#ifdef CONFIG_HIGHMEM
"HighMem",
#endif
"Movable",
#ifdef CONFIG_ZONE_DEVICE
"Device",
#endif
};

可以看出来,只有个 normal zone 和 movable 页面时必须的,其它都是根据硬件情况而可选的。

获得页

现在再回过头来看从 buddy 申请页面的 API:

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
/*** 申请页面 ****/
/*
* @brief : 从 buddy 中获取 2 的 n 次方个页面
* @ret: 返回申请页面的首页面地址
*/
static inline struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)
/*
* @brief : 为了直接操作虚拟地址,可以使用此函数来获取一个物理页面对应的虚拟地址
*/
static inline void *page_address(const struct page *page);
/*
* @brief : 也有更加简单粗暴的函数,直接获取到虚拟地址了
*/
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);

//如果只是想获取一个页面,也有对应的快捷方式
alloc_page(gfp_mask);
__get_free_page(gfp_mask);
//获取一个已经被清零的页面
unsigned long get_zeroed_page(gfp_t gfp_mask);

/*** 释放页面 ****/
void __free_pages(struct page *page, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);
__free_page(page);
free_page(addr);

gfp 标志

之前的课程已经对 gfp_t 进行了介绍,关于它的绝大部分使用场景如下表:

情形 标志
进程上下文,可以睡眠 GFP_KERNEL
进程上下文,不可以睡眠 GFP_ATOMIC
中断、软中断、tasklet GFP_ATOMIC
用于 DMA,可以睡眠 GFP_DMA 或上 GFP_KERNEL
用于 DMA,不可睡眠 GFP_DMA 或上 GFP_ATOMIC

slab 分配器

slab 是基于 buddy 的对内存在内核空间的二次管理

slab 分配器试图在几个基本原则之间寻求一种平衡:

  • 频繁使用的数据结构也会频繁分配和释放,因此应当缓存它们
  • 频繁分配和回收必然会导致内存碎片,为了避免这种现象,空闲链表的缓存会连续的存放,这样不会导致内存碎片
  • 回收的对象可以立即投入下一次分配,因此对于频繁的分配和释放,空闲链表能够提高其性能
  • 如果分配器知道对象大小、页大小和总的高速缓存的大小这样的概念,它会做出更明智的决策
  • 如果让部分缓存专属单个处理器,那么分配和释放就可以在不加 SMP 锁的情况下进行
  • 如果分配器是与 NUMA 相关的,它就可以从相同的内存节点为请求者进行分配
  • 对存放的对象进行着色,以防止多个对象映射到相同的高速缓存行。

slab 分配器的设计

slab 分配器为频繁使用的数据结构划分了对应的高速缓存组,一个组就对应一种特定的数据结构类型。 这些高速缓存组又被划分为多个 slab,slab 由一个或多个物理上连续的页组成。

当内核某部分需要申请一个新对象时,先从部分满的 slab 中分配,如果没有部分满的 slab 就从空的 slab 中进行分配,如果没有空的 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
struct kmem_cache {
struct array_cache __percpu *cpu_cache;

/* 1) Cache tunables. Protected by slab_mutex */
unsigned int batchcount;
unsigned int limit;
unsigned int shared;

unsigned int size;
struct reciprocal_value reciprocal_buffer_size;
/* 2) touched by every alloc & free from the backend */

slab_flags_t flags; /* constant flags */
unsigned int num; /* # of objs per slab */

/* 3) cache_grow/shrink */
/* order of pgs per slab (2^n) */
unsigned int gfporder;

/* force GFP flags, e.g. GFP_DMA */
gfp_t allocflags;

size_t colour; /* cache colouring range */
unsigned int colour_off; /* colour offset */
//这里记录了当前缓存所连接的空闲缓存
struct kmem_cache *freelist_cache;
unsigned int freelist_size;

/* constructor func */
void (*ctor)(void *obj);

/* 4) cache creation/removal */
const char *name;
struct list_head list;
int refcount;
int object_size;
int align;

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

struct kmem_cache_node *node[MAX_NUMNODES];
};

slab 分配器的接口

这在之前的课程已经有过说明了。

当需要频繁创建很多相同类型的对象时,就应该考虑使用 slab 分配器,而不是自己又去做空闲链表。

在栈上静态分配

内核栈不像用户栈那样可以动态的伸缩,内核栈小且固定。

  • 为每个进程分配固定大小的小栈,既可以减少内存的消耗,内核也无须负担太重的栈管理任务。

内核分配给每个进程的栈大小一般是两页大小,在 32 位架构上页一般是 4KB 大小,那么每个进程在内核的栈大小就是 8KB.

比如在 arm32 上就是如此:

1
2
3
#define THREAD_SIZE_ORDER	1
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
#define THREAD_START_SP (THREAD_SIZE - 8)

所以在内核编程时,一定要注意栈不能太大,否则会践踏 thread_info 给整个系统带来灾难。

高端内存的映射

只有低端内存是被内核初始线性映射好了的,高端内存在使用时才来映射,所以对内核而言申请低端内存的效率较高且物理内存还是连续的。 将物理地址映射到虚拟地址可以使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @brief : 将物理地址映射到虚拟地址
* @note : 如果物理地址处于低端内存,由于已经映射过了,所以可以直接返回 page 结构的 virtual 成员
* 如果物理地址处于高端内存,则会更新页表以建立映射
* 注意:该函数可能会睡眠,所以只能用在进程上下文中
*/
void *kmap(struct page *page);

//取消映射
void kunmap(struct page *page);


/**
* @brief : 原子性的映射
* @note : 这种方式不会睡眠,所以是可以用在中断和软中断上下文的
*/
void *kmap_atomic(struct page *page);
kunmap_atomic(addr);

每个 CPU 的数据

对于 SMP 而言,对于给定的处理器其数据是唯一的。

一般将每个 CPU 的数据存放在一个数组中,数组中每项对应一个处理器,对应 index 就是处理器号。

定义与声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @note : 以下两个宏不能在模块内使用,因为链接程序将它们创建在一个唯一的可执行段中(.data.percpu)
*/
//为每个 CPU 创建一个类型为 type,名字为 name 的变量实例
DEFINE_PER_CPU(type, name);
//在别处声明创建的变量(使用此宏以避免编译警告)
DECLARE_PER_CPU(type, name);


/**
* @brief : 动态的为每个处理器创建 type 类型的变量
*/
void *alloc_percpu(type);
//对应的释放
void free_percpu(void __percpu *__pdata);

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @brief : 获取当前处理器 var 变量的值
* @note : 此函数对变量的地址做了解引用,所以可以使用
* get_cpu_var(var)++;
* 来完成变量值的自增
*/
get_cpu_var(var);
//对应的此函数就是获取当前处理器变量的地址
get_cpu_ptr(var);


//由于获取变量会关闭抢占,所以完成操作后,需要调用此函数以打开内核抢占
put_cpu_var(var);
put_cpu_ptr(var);
Last Updated 2020-08-25 二 10:41.
Render by hexo-renderer-org with Emacs 26.3 (Org mode 9.3.7)