kernel源码分析-3

bootmem分配器(已过时)

    在内核初始化的过程中需要分配内存,内核提供临时的引导内存分配器,在页分配器和块分配器初始化完成之后,把空闲的物理页交给页分配器管理,丢弃引导内存分配器。

1
2
3
4
5
6
7
8
typedef struct bootmem_data {
unsigned long node_min_pfn;//起始物理页号
unsigned long node_low_pfn;//结束物理页号
void *node_bootmem_map;//指向一个位图,即bitmap,与物理页对应,分配置1
unsigned long last_end_off;//上次分配的内存块的结束位置后面一个字节的偏移
unsigned long hint_idx;//是上次分配的内存块的结束位置后面物理页位置中索引,下次优先考虑从这个物理页开始分配
struct list_head list;//
} bootmem_data_t;

分配器算法:

a.只把低端内存添加到bootmem分配器,低端内存是可以直接映射到内核虚拟地址空间的物理内存;

b.使用一个位图记录哪些物理面被分配,如果物理页被分配,把这个物理页对应的位设置为1;
c.采用最先适配算法,扫描位图,找到第一个足够大的空闲内存块;
d.为了支持分配小于一页的内存块,记录上次分配的内存块的结束位置后面一个字节的偏移和后面一页的索引,下次分配时,从上次分配的位置后面开始深度。如果上次分配的最后一个物理页剩余空间足够,可以直接在这个物理页上分配内存。

对外接口:

alloc_bootmem()

free_bootmem()

memblock分配器(替代bootmem)

主要维护两种内存:

    第一种内存是系统可用的物理内存,即系统实际含有的物理内存,其值从DTS中进行配置,通过uboot实际探测之后传入到内核。

    第二种内存是内核预留给操作系统的内存,这部分内存作为特殊功能使用,不能作为共享内存使用。

1
2
3
4
5
6
7
8
9
struct memblock {
bool bottom_up; //值为真表示从低地址向上分配,反之
phys_addr_t current_limit;//可分配内存的最大物理地址
struct memblock_type memory;//内存类型(已分配和未分配)
struct memblock_type reserved;//预留类型(已分配)
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
struct memblock_type physmem;//物理内存类型
#endif
};

物理内存类型和内存类型区别:

    内存类型是物理内存类型的子集,在引导内核时可以使用内核参数”mem=nn[KMG]”,指定可用内存的大小,导致内核不能看见所有的内存;物理内在类型总是包含所有内存范围,内存类型只包含内核参数”mem=”指定的可用内在范围。

1
2
3
4
5
6
7
struct memblock_type {
unsigned long cnt; //当前管理集合中记录的内存区域个数
unsigned long max; //当前管理集合中记录的内存区域的最大个数,最大值是INIT PHYSMEM REGIONS
phys_addr_t total_size; //所有内存块区域的总长度
struct memblock_region *regions;//执行内存区域结构的指针
char *name; //内存块类型的名称
};
1
2
3
4
5
6
7
8
struct memblock_region {
phys_addr_t base;//起始物理地址
phys_addr_t size;//长度
unsigned long flags;//flags标志
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
int nid;//节点编号
#endif
};

初始化过程涉及函数

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
void __init arm64_memblock_init(void)
{
const s64 linear_region_size = -(s64)PAGE_OFFSET;

/* Handle linux,usable-memory-range property */
//解析设备树二进制文件节点
fdt_enforce_memory_region();

/*
* Ensure that the linear region takes up exactly half of the kernel
* virtual address space. This way, we can distinguish a linear address
* from a kernel/module/vmalloc address by testing a single bit.
*/
BUILD_BUG_ON(linear_region_size != BIT(VA_BITS - 1));

/*
* Select a suitable value for the base of physical memory.
*/
//全局变量记录内存的起始物理地址
memstart_addr = round_down(memblock_start_of_DRAM(),
ARM64_MEMSTART_ALIGN);

/*
* Remove the memory that we will not be able to cover with the
* linear mapping. Take care not to clip the kernel which may be
* high in memory.
*/
//把线性映射区域不能覆盖的物理内存范围从memblock中删除
memblock_remove(max_t(u64, memstart_addr + linear_region_size,
__pa_symbol(_end)), ULLONG_MAX);
if (memstart_addr + linear_region_size < memblock_end_of_DRAM()) {
/* ensure that memstart_addr remains sufficiently aligned */
memstart_addr = round_up(memblock_end_of_DRAM() - linear_region_size,
ARM64_MEMSTART_ALIGN);
memblock_remove(0, memstart_addr);
}

/*
* Apply the memory limit if it was set. Since the kernel may be loaded
* high up in memory, add back the kernel region that must be accessible
* via the linear mapping.
*/
//设备树二进制文件中节点指定的命令行中,可以使用mem指定可用内存大小。
//如果指定内存的大小,那么把超过可用长度的物理内存范围从memblock中删除。
if (memory_limit != (phys_addr_t)ULLONG_MAX) {
memblock_mem_limit_remove_map(memory_limit);
memblock_add(__pa_symbol(_text), (u64)(_end - _text));
}

if (IS_ENABLED(CONFIG_BLK_DEV_INITRD) && initrd_start) {
/*
* Add back the memory we just removed if it results in the
* initrd to become inaccessible via the linear mapping.
* Otherwise, this is a no-op
*/
u64 base = initrd_start & PAGE_MASK;
u64 size = PAGE_ALIGN(initrd_end) - base;

/*
* We can only add back the initrd memory if we don't end up
* with more memory than we can address via the linear mapping.
* It is up to the bootloader to position the kernel and the
* initrd reasonably close to each other (i.e., within 32 GB of
* each other) so that all granule/#levels combinations can
* always access both.
*/
if (WARN(base < memblock_start_of_DRAM() ||
base + size > memblock_start_of_DRAM() +
linear_region_size,
"initrd not fully accessible via the linear mapping -- please check your bootloader ...\n")) {
initrd_start = 0;
} else {
memblock_remove(base, size); /* clear MEMBLOCK_ flags */
memblock_add(base, size);
memblock_reserve(base, size);
}
}

if (IS_ENABLED(CONFIG_RANDOMIZE_BASE)) {
extern u16 memstart_offset_seed;
u64 range = linear_region_size -
(memblock_end_of_DRAM() - memblock_start_of_DRAM());

/*
* If the size of the linear region exceeds, by a sufficient
* margin, the size of the region that the available physical
* memory spans, randomize the linear region as well.
*/
if (memstart_offset_seed > 0 && range >= ARM64_MEMSTART_ALIGN) {
range = range / ARM64_MEMSTART_ALIGN + 1;
memstart_addr -= ARM64_MEMSTART_ALIGN *
((range * memstart_offset_seed) >> 16);
}
}

/*
* Register the kernel text, kernel data, initrd, and initial
* pagetables with memblock.
*/
//把内核销像占用物理内存范围添加到memblock中。
memblock_reserve(__pa_symbol(_text), _end - _text);
#ifdef CONFIG_BLK_DEV_INITRD
if (initrd_start) {
memblock_reserve(initrd_start, initrd_end - initrd_start);

/* the generic initrd code expects virtual addresses */
initrd_start = __phys_to_virt(initrd_start);
initrd_end = __phys_to_virt(initrd_end);
}
#endif
//从设备树二进制文件中的内存保留区域,对应设备源文件字段和节点
early_init_fdt_scan_reserved_mem();

/* 4GB maximum for 32-bit only capable devices */
if (IS_ENABLED(CONFIG_ZONE_DMA))
arm64_dma_phys_limit = max_zone_dma_phys();
else
arm64_dma_phys_limit = PHYS_MASK + 1;

reserve_crashkernel();

reserve_elfcorehdr();

dma_contiguous_reserve(arm64_dma_phys_limit);

memblock_allow_resize();
}

memblock暴露接口:

memblock_add:添加新的物理内存块区域到memblock.memory中

memblock_remove:删除物理内存块区域;

memblock_alloc:分配内存;
memblock_free:释放内存。

伙伴分配器

    当系统内核初始化完毕后,使用页分配器管理物理页,当使用的页分配器是伙伴分配
器,伙伴分配器的特点是算法简单且高效。
    连续的物理页称为页块(page block)。阶(order)是伙伴分配器的一个专业术语,是页的数量单位,2n个连续页称为n阶页块。
    满足以下条件的两个n阶页块称为伙伴:

1、两个页块是相邻的,即物理地址是连续的;
2、页块的第一页的物理页号必须是2n的整数倍;
3、如果合并成(n+1)阶页块,第一页的物理页号必须是2n+1的整数倍。

    伙伴分配器分配和释放物理页的数量单位为阶。分配n阶页块的过程如下:
1、查看是否有空闲的n阶页块,如果有直接分配;否则,继续执行下一步;
2、查看是否存在空闲的 (n+1)阶页块,如果有,把(n+1)阶页块分裂为两个n阶页块,一个插入空闲n阶页块链表,另一个分配出去;否则继续执行下一步。
3、查看是否存在空闲的(n+2)阶页块,如果有把(n+2)阶页块分裂为两个(n+1)阶页块,一个插入空闲(n+1)阶页块链表,另一个分裂为两个n阶页块,一个插入空间(n阶页块链表,另一个分配出去;如果没有,继续查看更高阶是否存在空闲页块。

分区伙伴分配器由其改进

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
#define MAX_ORDER 11
struct zone{
//省略
//不同尺寸的空闲区域,最大为10次方
struct free_area free_area[MAX_ORDER];
//省略
}
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
//申请页时,分配标志对应区域类型
#define ___GFP_DMA 0x01u
#define ___GFP_HIGHMEM 0x02u
#define ___GFP_DMA32 0x04u
#define ___GFP_MOVABLE 0x08u
//标志位与之对应的类型
#ifdef CONFIG_HIGHMEM
#define OPT_ZONE_HIGHMEM ZONE_HIGHMEM
#else
#define OPT_ZONE_HIGHMEM ZONE_NORMAL
#endif

#ifdef CONFIG_ZONE_DMA
#define OPT_ZONE_DMA ZONE_DMA
#else
#define OPT_ZONE_DMA ZONE_NORMAL
#endif

#ifdef CONFIG_ZONE_DMA32
#define OPT_ZONE_DMA32 ZONE_DMA32
#else
#define OPT_ZONE_DMA32 ZONE_NORMAL
#endif
//转换表
#define GFP_ZONE_TABLE ( \
(ZONE_NORMAL << 0 * GFP_ZONES_SHIFT) \
| (OPT_ZONE_DMA << ___GFP_DMA * GFP_ZONES_SHIFT) \
| (OPT_ZONE_HIGHMEM << ___GFP_HIGHMEM * GFP_ZONES_SHIFT) \
| (OPT_ZONE_DMA32 << ___GFP_DMA32 * GFP_ZONES_SHIFT) \
| (ZONE_NORMAL << ___GFP_MOVABLE * GFP_ZONES_SHIFT) \
| (OPT_ZONE_DMA << (___GFP_MOVABLE | ___GFP_DMA) * GFP_ZONES_SHIFT) \
| (ZONE_MOVABLE << (___GFP_MOVABLE | ___GFP_HIGHMEM) * GFP_ZONES_SHIFT)\
| (OPT_ZONE_DMA32 << (___GFP_MOVABLE | ___GFP_DMA32) * GFP_ZONES_SHIFT)\
)

备用区域列表
    如果首选的内存节点区域不能满足分配的请求,可以从备用的内存区域借用物理页。借用必须遵守相应的规则:

1、一个内存节点的某个区域类型可以从另一个内存节点的相同区域类型借用物理页,比如节点0的普通区域可以从节点1的普通区域借用物理页。
2、高区域类型可以从低区域类型借用物理页,比如普通区域可以从DMA区域借用物理页;
3、低区域类型不能从高区域类型借用物理页。比如DMA区域不能从普通区域借用物理页。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct bootmem_data;
typedef struct pglist_data {
//内存区域数组
struct zone node_zones[MAX_NR_ZONES];
//备用区域列表
struct zonelist node_zonelists[MAX_ZONELISTS];
int nr_zones;
#ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */
struct page *node_mem_map;
#ifdef CONFIG_PAGE_EXTENSION
struct page_ext *node_page_ext;
#endif
#endif
//省略
}

    UMA系统只有一个备用区域列表,按区域类型从高到低排序。假设UMA系统包含普通区域和DMA区域,那么备用区域列表:{普通区域, DMA区域}。UMA系统每个内存节点有两个备用区域列表:一个包含所有内存节点的区域,另一个只包含当前内存节点的区域。
包含所有内存节点的备用区域列表有两种排序方法:

    节点优先顺序:先根据节点距离从小到大排序,在每个节点里面根据区域类型从高到低排序。优点是优先选择距离近的内存,缺点是在高区域耗尽以前使用的低区域。
    区域优先顺序:先根据区域类型从高到低排序,然后再每个区域类型里面根据节点距离从小到大排序。优点是减少低区域耗尽的概率,缺点是不能保证优先选择距离近的内存。
    默认的排序方法:会自动选择最优的排序方法:比如64位系统,因为需要DMA和DMA32区域的备用相对少,所以选择节点优先顺序;如果是32位系统,选择区域优先顺序。

首选的内存区域什么情况下会从备用区域借用物理页呢?

每个内存区域有3个水线。

    高水线(high): 如果内存区域的空闲页数大于高水线,说明内存区域的内存充足;
    低水线(low): 如果内存区域的空闲页数小于低水线,说明内存区域的内存轻微不足;
    最低水线(min): 如果内存区域的空闲页数小于最低水线,说明内存区域的内存严重不足。最低水线以下的内存称为紧急保留内存,在内存严重不足的紧急情况下,给承诺“分给我们少量的紧急保留内存使用,我可以释放更多的内存”的进程使用。

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
enum zone_watermarks {
WMARK_MIN,
WMARK_LOW,
WMARK_HIGH,
NR_WMARK
};

#define min_wmark_pages(z) (z->watermark[WMARK_MIN])
#define low_wmark_pages(z) (z->watermark[WMARK_LOW])
#define high_wmark_pages(z) (z->watermark[WMARK_HIGH])struct zone {
/* Read-mostly fields */

/* zone watermarks, access with *_wmark_pages(zone) macros */
unsigned long watermark[NR_WMARK];
// 伙伴分配器管理的物理页的数量
// 代表的是zone中的所有页,包含空洞,计算公式:zone_end_pfn-zone_start_pfn
unsigned long managed_pages;
// 当前区域跨越的总页数,包括空洞
// 代表zone中可用的所有物理页,计算公式:spanned_pages-hole_pages
unsigned long spanned_pages;
// 当前区域存在的物理页的数量,不包括空洞
// 代表通过buddy管理所有可用的页,计算公式:present_pages-reserved_+pages
unsigned long present_pages;
//spanned_pages > present_pages > managed_pages
}

核心函数:

__alloc_pages_nodemask()

函数分配物理页流程 :

首先 , 根据 ​​gfp_t gfp_mask​​ 分配标志位 参数 , 得到 “ 内存节点 “ 的 首选 ” 区域类型 “ 和 “ 迁移类型 “ ;

然后 , 执行 “ 快速路径 “ , 第一次分配 尝试使用 低水线分配 ;

如果上述 “ 快速路径 “ 分配失败 , 则执行 “ 慢速路径 “ 分配 ;

__free_pages()