【Linux内存管理专题】如何统计memblock阶段的内存使用?

一、背景简介

        在平时工作中,因为项目或者客户需求,经常会遇到内存裁剪或者优化的需求,内存裁剪和优化的前提是需要对目前系统的内存使用有个清晰的认识,知道内存都用在哪里,这样我们才可以评估这些内存是否有裁剪和优化的可能性。

        这里先介绍下,Linux的内存管理分成两个阶段管理,一个是在伙伴系统还未初始化完成前的 memblock 阶段,另一个就是伙伴系统接管后的伙伴系统阶段,所以,我们需要对这两个阶段的内存使用有清晰的认识后才能更好的处理内存裁剪和优化的需求,本文主要关注上述的第一个阶段 memblock 阶段的内存使用的分析方法,第二阶段的内存使用分析会在后续章节展现,下图展示了 Linux 内存管理的两个阶段。

二、内核memblock内存管理实现

2.1、memblock内存管理架构

        Linux 内核启动引导初期,此时伙伴系统还没有初始化,该阶段的内存管理由 memblock 模块负责,下图是内核中 memblock 相关结构体之间的关系图,它是 memblock 管理内存的核心,从下图可以看出,memblock 中主要是由 memory 和 reserved 两个数组来管理系统可用内存和保留内存的信息,physmem 描述实际的物理内存,它只在某些特定的架构上有效,当前暂不关注。

2.2、memblock核心结构体

        2.2.1、struct memblock

                该结构体定义在Kernel/include/linux/memblock.h中,它用于管理memblock分配器最基本的元数据,具体定义如下:

/**
 * struct memblock - memblock allocator metadata
 * @bottom_up: is bottom up direction?
 * @current_limit: physical address of the current allocation limit
 * @memory: usabe memory regions
 * @reserved: reserved memory regions
 * @physmem: all physical memory
 */
struct memblock {
	bool bottom_up;  					// 用于描述分配器管理内存的方向,其中,true为自底向上,false为自上向底
	phys_addr_t current_limit;          // 用于描述分配器管理物理内存的最大值
	struct memblock_type memory;        // 用于描述分配器管理的可用内存集合
	struct memblock_type reserved;      // 用于描述分配器管理的预留内存集合
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
	struct memblock_type physmem;       // 用于描述分配器管理的所有物理内存集合
#endif
};

   

        2.2.2、struct memblock_type

                该结构体用于维护特定内存类型集合,具体定义如下,各参数的解释如下所示:

/**
 * struct memblock_type - collection of memory regions of certain type
 * @cnt: number of regions
 * @max: size of the allocated array
 * @total_size: size of all regions
 * @regions: array of regions
 * @name: the memory type symbolic name
 */
struct memblock_type {
	unsigned long cnt;                 // 用于记录结构体中含有的内存区域数量
	unsigned long max;                 // 用于记录为regions内存块数组分配的数量,当需要维护的内存区域数目超过max后,则会倍增regions的内存空间
	phys_addr_t total_size;            // 用于记录该类型内存集合中包含的物理内存数目
	struct memblock_region *regions;   // 表示内存块数组,描述了该集合下管理的所有内存块,每个数组元素代表一块内存区域,可通过索引获取对应区块
	char *name;                        // 用于记录该内存类型结合名字,如memory表示可用内存集合,reserved表示预留内存集合
};

        2.2.3、struct memblock_region

                该结构体表示被管理的内存块,具体定义如下,各参数的解释如下所示:

/**
 * enum memblock_flags - definition of memory region attributes
 * @MEMBLOCK_NONE: no special request
 * @MEMBLOCK_HOTPLUG: hotpluggable region
 * @MEMBLOCK_MIRROR: mirrored region
 * @MEMBLOCK_NOMAP: don't add to kernel direct mapping
 */
enum memblock_flags {
	MEMBLOCK_NONE		= 0x0,	/* No special request */
	MEMBLOCK_HOTPLUG	= 0x1,	/* hotpluggable region */
	MEMBLOCK_MIRROR		= 0x2,	/* mirrored region */
	MEMBLOCK_NOMAP		= 0x4,	/* don't add to kernel direct mapping */
};

/**
 * struct memblock_region - represents a memory region
 * @base: physical address of the region
 * @size: size of the region
 * @flags: memory region attributes
 * @nid: NUMA node id
 */
struct memblock_region {
	phys_addr_t base;             // 用于记录内存块的起始地址
	phys_addr_t size;			  // 用于记录内存块的大小
	enum memblock_flags flags;	  // 用于记录内存类型标志,如普通型、热插拔型等
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
	int nid;					  // 用于记录在NUMA内存系统中标识节点号
#endif
};

         上面就是memblock中三个最核心的结构体,内核定义了一个全局变量memblock用于memblock分配器管理载体,其类型为struct memblock,具体定义如下:

                 1)bottom_up == false 表示分配器管理内存方向是自底向上

                 2)可用内存块数组大小为128,即默认管理128个内存块,超过可扩展

                 3)预留内存块数组大小为128,即默认管理128个内存块,超过可扩展

                 4)物理内存块数组大小为4,即默认管理4个内存块

#define INIT_MEMBLOCK_REGIONS			128
#define INIT_PHYSMEM_REGIONS			4

#ifndef INIT_MEMBLOCK_RESERVED_REGIONS
#define INIT_MEMBLOCK_RESERVED_REGIONS		INIT_MEMBLOCK_REGIONS
#endif

static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_RESERVED_REGIONS] __initdata_memblock;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
static struct memblock_region memblock_physmem_init_regions[INIT_PHYSMEM_REGIONS] __initdata_memblock;
#endif

struct memblock memblock __initdata_memblock = {
    .memory.regions		= memblock_memory_init_regions,
    .memory.cnt		    = 1,	/* empty dummy entry */
    .memory.max		    = INIT_MEMBLOCK_REGIONS,
    .memory.name		= "memory",

    .reserved.regions	= memblock_reserved_init_regions,
    .reserved.cnt		= 1,	/* empty dummy entry */
    .reserved.max		= INIT_MEMBLOCK_REGIONS,
    .reserved.name		= "reserved",

    #ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
    .physmem.regions	= memblock_physmem_init_regions,
    .physmem.cnt		= 1,	/* empty dummy entry */
    .physmem.max		= INIT_PHYSMEM_REGIONS,
    .physmem.name		= "physmem",
    #endif

    .bottom_up		    = false,
    .current_limit		= MEMBLOCK_ALLOC_ANYWHERE,
};

 2.3、memblock相关API

        memblock系统提供相关接口供内核使用,包括内存块的添加、预留、移除和内存申请等功能。

int memblock_add(phys_addr_t base, phys_addr_t size);

int memblock_add_node(phys_addr_t base, phys_addr_t size);

int memblock_remove(phys_addr_t base, phys_addr_t size);

int memblock_free(phys_addr_t base, phys_addr_t size);

int memblock_reserve(phys_addr_t base, phys_addr_t size);

void memblock_trim_memory(phys_addr_t align);

bool memblock_overlaps_region(struct memblock_type *type, phys_addr_t base, phys_addr_t size);

phys_addr_t memblock_find_in_range_node(phys_addr_t size,
                                        phys_addr_t align,
										phys_addr_t start,
                                        phys_addr_t end,
										int nid,
                                        enum memblock_flags flags);

phys_addr_t memblock_find_in_range(phys_addr_t start,
                                   phys_addr_t end,
				   				   phys_addr_t size,
                                   phys_addr_t align);

        2.3.1、memblock_add

                该函数用于将目标内存块添加到可用内存块集合中。

        2.3.2、memblock_remove

                该函数用于将内存块从可用内存块集合中移除。一般在分配内存时从可用内存集合中标记内存由可用到不可用。

        2.3.3、memblock_reserve

                该函数用于将目标内存块添加到预留内存块集合中,这里并不会从可用内存块集合中删除,只是标记为了reserved状态。

        2.3.4、memblock_free

                该函数用于将内存块从预留内存块集合中移除。

2.4、内核memblock基本操作流程

        从上面的介绍,我们可以得到内核memblock的操作流程,大概流程如下:

                1)通过memblock_add将当前系统物理总内存加入到可用内存

                2)通过memblock_reserve将指定的内存区域加入到保留内存

                3)通过memblock_free释放指定的保留内存区域,并将入加入可用内存中

                4)通过memblock_remove将指定内存从可用内存中删除,也就是我们说的不可见内存

                5)在伙伴系统初始化完成后,memblock阶段的实际可用内存(去除保留内存和不可见内存)会交给伙伴系统来管理。

2.5、内核memblock调试方法

        为了进一步了解memblock的详细分配过程,可以在bootargs中加入 "memblock=debug" 来打开相关日志输出,比如有些内存的用途可以通过这些日志来确认。

2.6、memblock内存使用情况梳理

        2.6.1、不可见内存梳理统计

                可用内存之外的内存就是我们常说的不可见内存,这些不可见内存区域信息可以在设备树中的 reserved-memory 节点中提前预置,也可以在 bootloader 阶段动态添加到设备树中,这样在进入内核 memblock 初始化阶段就可以从设备树中拿到这些信息进行对应的操作,比如下图展示了在 bootloader 阶段新增了一些内存区域到设备树中的 reserved_memory 节点,这些内存区域也不一定都是不可见内存,只有 map 段为 0 的才是不可见内存,为 1 的是保留内存。

                下图是在设备树定义部分,这里预置了部分内存区域信息,其中,含有 no-map 参数的内存区域是不可见内存,否则就是保留内存。

                这里对上面的节点进行解读,reserved_memory节点中指定的内存区域分为两种,

                        1)静态分配

                                需要reg参数来指定内存区域的起始地址和大小。

                        2)动态分配

                                需要size参数指定内存区域的大小,内存起始地址是根据系统内存情况动态分配。

                需要注意的是,如果reg和size都存在,reg参数将会生效,size参数会被忽略掉。

kernel/Documentation/devicetree/bindings/reserved-memory/reserved-memory.txt

                设备树中reserved_memory节点下全部的内存区域信息可以通过 ls -alh /sys/firmware/devicetree/base/reserved-memory/ 来查询,这里不仅包含在设备树中预先设置的内存区域还包含在 bootloader阶段动态添加的内存区域信息,如下图所示:

                其中不可见内存的内存区域节点下会有no-map的属性,我们可以通过hexdump -C来查看对应内存块的起始地址和大小(reg参数)或者大小(size参数),对于动态分配的内存区域的起始地址需要打开memblock debug日志后进行查看。

        2.6.2、可用内存和保留内存统计

                总内存大小减去不可见内存后就是可用内存和保留内存的大小了,这些信息被在Linux中被记录在/sys/kernel/debug/memblock/memory/sys/kernel/debug/memblock/reserved文件里,可以通过cat命令来查询。

        2.6.3、怎么理解不可见?

                设置为 no-map 属性的内存区域被称为不可见内存,这里的不可见指的是内核不会主动的为这块内存建立虚拟地址到物理地址的映射,而是需要使用者在使用前主动建立页表映射,通常直接使用 ioremap 来重新建立映射后再访问。

                一般来说,为保留内存添加 no-map 属性可以提供更高的安全性和灵活性,毕竟只有在特定设备建立映射后才能访问。

2.7、豁然开朗

        尽管上面介绍了memblock的基本操作方法以及如何统计memblock内存使用的方法,但是,总觉得还不够直观,尤其是各种内存区域的关系并不能很清晰的看到,不用担心,下面的这张图会让我们对memblock阶段的内存管理有更深刻的认识,下图是根据可用内存、保留内存和设备树中reserved_memory节点中的内存信息绘制的,从左到右依次是物理可用内存、memblock的可用内存、memblock的保留内存和不可见内存,看到这张图后是不是有种豁然开朗的感觉。^v^,大家在平时的内存梳理时也可以按照将其通过画图的方式展现出来,效果很好。

        从下图我们可以很清楚的得到以下信息:

                1)保留内存是可用内存的子集。

                2)设备树中reserved-memory节点中分为保留内存和不可见内存。

三、小结

        本文介绍了Linux中memblock阶段的内存管理架构和原理,这个阶段结束后会将可用内存中除保留内存外的其他内存都交给伙伴系统来进行管理,该阶段的内存使用的复杂度不高,所以,相对来说这部分理解还算比较简单,进入到伙伴系统后可以看到复杂度呈指数上升,需要理解的概念也很多,虽然前路曲折,但是我们还是要勇于前进,敬请期待下一篇章吧!

### Linux 内存管理机制教程 Linux 系统的内存管理机制是操作系统中最为复杂且重要的部分之一,它涉及到物理内存和虚拟内存的分配、回收、映射等操作。以下是关于 Linux 内存管理机制的核心内容及其实现方式: #### 1. 分段机制与分页机制 Linux内存管理以分段机制和分页机制为基础。分段机制将逻辑地址划分为多个段,每个段对应一个线性地址范围[^1]。而分页机制则进一步将线性地址划分为固定大小的页面(通常为 4KB),并通过页表实现虚拟地址到物理地址的映射[^4]。 分段机制主要用于保护进程的地址空间,确保不同进程之间的隔离;分页机制则负责具体的内存分配和回收,支持内存交换(Swap)以及高效的内存利用。 #### 2. 物理内存分区 Linux 2.6 内核将每个内存节点的物理内存划分为三个主要区域(Zone): - **ZONE_DMA**:用于支持 DMA 操作的低地址内存,通常小于 16MB。 - **ZONE_NORMAL**:常规内存区域,适用于大多数内存分配需求,通常在 16MB 到 896MB 之间。 - **ZONE_HIGHMEM**:高地址内存区域,不永久映射到内核地址空间,通常大于 896MB[^3]。 这种分区设计使得系统能够更灵活地应对不同类型的内存访问需求。 #### 3. 内存管理模型 Linux 提供了多种内存管理模型,分别用于不同的阶段和场景: - **Bootmem 和 Memblock**:在系统启动阶段使用,负责初始化内存分配[^2]。 - **Buddy System**:运行时动态分配连续内存块的核心算法,通过将空闲内存按大小分类来快速找到合适的内存块[^2]。 - **Slab Allocator**:用于管理小型对象的内存分配器,减少内存碎片并提高缓存命中率。 #### 4. 页面管理 页面管理是 Linux 内存管理的核心功能之一,主要包括以下几个方面: - **页面分配与回收**:通过 Buddy System 实现页面的动态分配和释放,确保内存利用率最大化。 - **页面交换(Swap)**:当物理内存不足时,将不常用的页面写入磁盘,释放物理内存供其他进程使用[^4]。 - **冷热页面分离**:根据页面的访问频率将其分类为热页面和冷页面,优化内存访问性能。 - **页面缓存**:通过缓存已打开的文件数据,减少磁盘 I/O 操作,提升系统性能[^5]。 #### 5. 内存清理与统计 Linux 提供了丰富的工具和命令来监控和清理内存资源。例如,`free` 命令可以显示系统的总内存、已用内存、空闲内存以及缓存和缓冲区的使用情况[^5]。 - `total`:表示系统的总物理内存。 - `used`:表示已使用的内存,包括应用程序占用的内存和缓存/缓冲区。 - `free`:表示当前未被使用的内存。 - `buffers` 和 `cached`:分别表示目录缓存和文件缓存的使用情况。 #### 代码示例:查看内存状态 以下是一个简单的 Bash 脚本,用于定期监控内存使用情况: ```bash #!/bin/bash while true; do echo "-----------------------------" free -h sleep 5 done ``` ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值