Linux 内核内存管理(1)

简介

内存管理是操作系统内核最复杂的部分之一( 我认为它是最复杂的 )。在 内核入口前最后的准备 文中, 我们刚好讲到了start_kernel函数调用之前内核发生的行为。start_kernel函数在内核运行第一个init进程之前会初始化所有的内核( 包括架构相关的特征 )。你可能还记得,在系统启动期间我们构造了 early 页表,identity 页表和 fixmap 页表,此时还没涉及复杂的内存管理。当start_kernel函数被调用时,我们将过渡到更复杂的与内存管理有关的数据结构和技巧。为了能够很好的理解 linux 内核的初始化过程,我们需要清晰地了解这些技术。本章将综述 linux 内核内存管理框架的各个不同部分和相应的 API, 首先介绍memblock

Memblock

Memblock 是在早期引导过程中管理内存的方法之一,此时内核内存分配器还没运行。Memblock 以前被定义为Logical Memory Block( 逻辑内存块 ), 但根据 Yinghai Lu 的 补丁, 它被重命名为memblockx86_64架构的 linux 内核就采用了这种方法。我们在 内核入口前最后的准备 文中已经提到了memblock。现在我们将进一步了解memblock是如何实现的。

我们首先从数据结构着手来了解memblock。所有有关数据结构的定义都可以在 include/linux/memblock.h 头文件中找到。

第一个数据结构的名字如本节标题,如下所示,

该结构体包含五个域。如果bottom_uptrue, 则允许由下而上地分配内存。current_limit指出了内存块的大小限制。接下来的三个域描述了内存块的类型,即预留型,内存型和物理内存型( 如果宏CONFIG_HAVE_MEMBLOCK_PHYS_MAP被定义了 )。我们现在又接触到了一个数据结构memblock_type, 它的定义如下:

该结构体存储的是内存类型信息。它包含的域分别描述了当前内存块含有的内存区域数量,所有内存区域的总共大小,已经分配的内存区域大小和一个指向memblock_region结构体的数组指针。memblock_region结构体描述了内存区域,它的定义如下:

memblock_region结构体提供了内存区域的基地址和大小,标志可以是:

如果宏CONFIG_HAVE_MEMBLOCK_NODE_MAP被定义了,memblock_region还提供一个整数域 —— numa 节点选择器。

图示法可以用来展示以上结构体之间的关系:

Memblock主要包含三个结构体:memblock, memblock_typememblock_region。现在我们已了解了Memblock, 接下来我们将看到Memblock的初始化过程。

Memblock 初始化

memblock的所有 API 描述在 include/linux/memblock.h 头文件中,所有这些 API 的实现在 mm/memblock.c 源文件中。在源文件的开头我们可以看到memblock结构体的初始化:

结构体memblock的初始化变量名和结构体名相同 —— memblock。首先注意__initdata_memblock宏,它的定义如下:

如果启用CONFIG_ARCH_DISCARD_MEMBLOCK宏配置选项,memblock 代码会被放到.init代码段,在内核启动完成后 memblock 代码会从.init代码段释放。

接下来的是memblock结构体中memblock_type memory,memblock_type reservedmemblock_type physmem的初始化。本文中我们只研究memblock_type.regions的初始化过程。需要注意的是每一个memblock_type域都是通过memblock_region数组初始化的:

每一个数组包含128个内存区域,可以查看INIT_MEMBLOCK_REGIONS的宏定义:

需要注意的是所有的数组定义都带有__initdata_memblock宏,该宏定义已在memblock结构体初始化时提到过( 如果忘了请回顾上文 )

memblock结构体中最后两个域:bottom_up内存分配模式被禁用;当前 Memblock 的大小限制是:

0xffffffffffffffff

一旦memblock结构体完成初始化,我们接下来就研究 Memblock API。

Memblock API

我们已经完成了memblock结构体的初始化,接下来我们将研究 Memblock API 和它的实现。在上文中我提到过所有关于memblock的实现都在 mm/memblock.c 源文件中。要理解memblock是如何工作和实现的,我们首先看一下它的用法。在 linux 内核中有几处用到了 memblock,例如 arch/x86/kernel/e820.c 中的函数memblock_x86_fill。该函数遍历由 e820 提供的内存映射表并且通过memblock_add函数把内核预留的内存区域添加到memblock。既然我们首先遇到了memblock_add函数,那就从它开始吧。

memblock_add函数有两个参数:物理基址和内存区域大小,并且把该内存区域添加到memblockmemblock_add函数本身并没有什么,它只是调用了

函数。我们传递的参数依次是:内存块类型(memory),物理基址,内存区域大小,最大节点数( 0 如果CONFIG_NODES_SHIFT没有在配置文件中设置,不然就是CONFIG_NODES_SHIFT )和标志。memblock_add_range函数添加新的内存区域到内存块中。首先,该函数检查给定的内存区域大小,如果是 0 就返回。在这之后,memblock_add_range用给定的memblock_type检查memblock结构体中是否存在内存区域。如果没有,我们就用给定的值填充新的memory_region然后返回 (我们已经在 linux 内核内存管理框架的第一次接触 一文中看到过实现)。如果memblock_type不为空,我们就把新的内存区域添加到memblock_type类型的memblock中。

首先,我们用如下代码获得内存区域的结束位置:

memblock_cap_size函数会设置size大小确保base + size不会溢出。该函数实现相当简单:

memblock_cap_size返回sizeULLONG_MAX - base中的最小值。

在那之后我们得到了新的内存区域的结束地址,memblock_add_range函数检查内存区域是否重叠并和已经添加到memblock中的内存区域合并。把新的内存区域插入到memblock中包含两步:

  • 把新的内存区域中非重叠的部分作为独立的区域加入到 memblock
  • 合并所有相邻的内存区域

接下来遍历所有已经存储的内存区域并检查有没有和新的内存区域重叠:

如果新内存区域没有和已经存储在memblock的内存区域重叠,把该新内存区域插入到memblock中。这是第一次循环,我们需要检查新内存区域是否可以放入内存块中并调用memblock_double_array:

memblock_double_array函数加倍给定的内存区域大小,然后把insert设为true再转到repeat标签。第二次循环,从repeat标签开始经过同样的循环然后用memblock_insert_region函数把当前内存区域插入到内存块:

由于我们在第一次循环中把insert设为true, 现在memblock_insert_region函数将会被调用。memblock_insert_region函数几乎和把新内存区域插入到空的memblock_type代码块有同样的实现( 见上文 ),该函数获得最后一个内存区域:

然后调用memmove函数移动该内存区域:

紧接着填充新内存区域memblock_regionbase域,size域等等, 然后增大memblock_type的大小。最后memblock_add_range函数调用memblock_merge_regions合并所有相邻且兼容的内存区域。

在第二种情况下,新内存区域可能和已存储的内存区域重叠。例如,在memblock中已经有了region

现在我们想把region2加到memblock中,region2含有以下基址和大小:

本例中,把新内存区域的基址设为重叠内存区域的结束地址:

base0x1000。和第二次循环做法一样,我们用以下代码把它添加到 memblock:

本例中,我们先插入overlapping portion( 重叠部分 ) ( 只插入地址更高的部分,因为低地址部分已经在重叠内存区域 ),然后插入剩余部分,最后调用memblock_merge_regions合并这些部分内存区域。memblock_merge_regions函数合并相邻且兼容的内存区域。该函数遍历所有memblock_type类型的内存区域,每次取出两个邻近的内存区域 —— type->regions[i] 和type->regions[i + 1], 然后检查它们是否有相同的标志,属于相同的节点,第一个内存区域的结束地址不等于第二个内存区域的基地址:

如果这些条件一个都不满足,更新第一个内存区域的大小:

因为我们把第二个内存区域大小加到第一个内存区域大小,所以我们要调用memmove函数把当前(this)内存区域后的每一个( 靠每次循环 )内存区域移动到其前一个内存区域:

然后把memblock_type类型的内存区域数量减一:

此后我们就会把两个内存区域合并为一个:

以上就是memblock_add_range函数的全部工作原理。

还有memblock_reserve函数,除了一处不同外其余均与memblock_add函数一致。memblock_reserve函数把memblock_type.reserved类型的内存区域存到 memblock 中,而不是memblck_type.memory

Memblock 不仅提供了添加memoryreserved类型的内存区域的 API,还包括:

  • memblock_remove —— 从 memblock 中移除内存区域
  • memblock_find_in_range —— 在给定的范围内找到未使用的内存
  • memblock_free —— 释放 memblcok 中的内存区域
  • for_each_mem_range —— 反复迭代 memblock

还有更多。。。

获取内存区域信息

Memblock 也提供了 API 来获取memblock中已分配内存区域的信息,分为两个部分:

  • get_allocated_memblock_memory_regions_info —— 获取内存区域信息
  • get_allocated_memblock_reserved_regions_info —— 获取预留内存区域信息

这两个函数的实现很简单。以get_allocated_memblock_reserved_regions_info函数为例:

该函数首先检查memblock是否包含预留内存区域。如果memblock不包含则返回 0,否则我们把预留内存区域的物理地址赋给addr然后返回已分配的数组经对齐过后的大小。对齐用的是PAGE_ALIGN宏,它依赖于页的大小:

函数get_allocated_memblock_memory_regions_info的实现和上面一样,唯一不同的是用到了memblock_type.memory而不是memblock_type.reserved

Memblock 调试

在 memblock 的实现中多次调用了memblock_dbg函数。如果在内核命令行传入memblock=debug选项,就会调用memblock_dbg函数。其实memblock_dbg仅仅是个宏定义,它的展开包含printk函数:

例如,memblock_reserve函数调用了该宏:

结果如下图:

Memblock

Memblock 还支持 debugfs。如果你的内核不是运行在X86架构上,你可以访问:

  • /sys/kernel/debug/memblock/memory
  • /sys/kernel/debug/memblock/reserved
  • /sys/kernel/debug/memblock/physmem

来获得memblock内容的转储。

总结

关于 linux 内核内存管理第一部分到此结束。如果有任何疑问或建议,在 twitter 0xAX 上联系我,或给我发 邮件,或 提交一个 issue

超链接

1 7 收藏 评论

关于作者:ldd

一个想变强大的蚂农 个人主页 · 我的文章 · 10

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部