【Linux 内核源码分析】物理内存组织结构

本文详细解析了非一致性内存访问(NUMA)体系结构下的内存管理,涉及内存模型(平坦、不连续和稀疏)、三级结构(节点、区域和页),以及Linux内核中冷热页的分配策略。同时,讲解了页表设计、页表项操作和体系结构相关的内存管理函数,为理解Linux内核内存管理提供了深入剖析。
摘要由CSDN通过智能技术生成

多处理器系统两种体系结构:

  1. 非一致内存访问(Non-Uniform Memory Access,NUMA):这种体系结构下,内存被划分成多个内存节点,每个节点由不同的处理器访问。访问一个内存节点所需的时间取决于处理器和内存节点之间的距离,因此处理器与内存节点之间的距离会影响内存访问速度。

  2. 对称多处理器(Symmetric Multi-Processor,SMP):这种体系结构是一致内存访问(Uniform Memory Access,UMA)的一种形式,所有处理器对内存的访问时间是相同的,即无论处理器的位置如何,访问内存的开销是相等的。

内存模型

Linux内核内存模型是从处理器的角度看到的物理内存分布,内核管理不同内存模型的方式存在差异。内存管理子系统支持以下三种内存模型:

  1. 平坦内存(Flat Memory):在这种模型下,内存的物理地址空间是连续的,且没有空洞。这是最简单的内存模型,因为对于物理内存的管理而言,只需按顺序分配内存即可。

  2. 不连续内存(Discontiguous Memory):在这种模型下,内存的物理地址空间存在空洞,但是这种模型可以高效地处理空洞。这是因为内存管理子系统可以跟踪哪些物理地址是已经被占用,哪些是空闲的,然后在空闲内存之间分配新的内存。

  3. 稀疏内存(Sparse Memory):在这种模型下,内存的物理地址空间也存在空洞,但是如果要支持内存热插拔,只能选择稀疏内存模型。这是因为在内存热插拔时,可能会出现大量的空洞,如果采用不连续内存模型,那么在进行内存分配时,需要遍历整个物理地址空间,这样会造成不必要的开销。而稀疏内存模型可以维护一个可扩展的物理地址空间列表,只需在该列表中分配内存即可。

三级结构

内存管理子系统使用节点、区域和页三级结构来描述物理内存的管理。

  1. 节点:节点是指物理内存的逻辑分组单元,通常对应于具有特定特性或位置的一组物理内存。每个节点包含一个或多个区域,用于管理一定范围内的物理内存。

  2. 区域:区域是节点内部的一个逻辑划分,用于管理一定范围内的物理内存页。不同的区域可能具有不同的特性,例如可回收内存、不可回收内存等。常见的区域包括高速缓存区、低速缓存区、DMA区等。

  3. 页:页是内存管理的最小单位,通常是以固定大小(如4KB)划分的内存块。操作系统通过页表来映射虚拟内存和物理内存之间的对应关系,实现内存的管理和地址转换。

内存节点(pglist_data)

内存节点分为两种情况:

  • 对于NUMA(非一致性存储访问)体系的内存节点,内存节点根据处理器和内存的距离划分。在NUMA架构中,不同的处理器可能与不同的内存区域相连,因此系统将内存划分为不同的节点,以便更有效地管理和分配内存资源。

  • 在具有不连续内存的NUMA系统中,内存节点表示比区域的级别更高的内存区域,根据物理地址是否连续划分。在这种情况下,每块物理地址连续的内存被视为一个内存节点。这种划分方式可以帮助内核更好地管理非连续内存的分配和使用,确保系统能够有效地利用所有可用的物理内存空间。

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

    unsigned long node_start_pfn; // 该节点的起始物理页号
    unsigned long node_present_pages; // 物理页总数,即该节点上存在的物理页数量
    unsigned long node_spanned_pages; // 物理页范围的总长度,包括空洞,即该节点的物理页范围总大小
    int node_id; // 节点标识符
    ...
}

pglist_data 结构体定义了一个内存节点的数据结构。

  • node_zones:内存区域数组,用于存储该节点内每个内存区域的信息。
  • node_zonelists:备用区域列表,用于存储备用的内存区域的信息。
  • nr_zones:记录该节点包含的内存区域的数量。
  • node_mem_map:内存映射表,用于存储每个物理页的信息。仅在没有使用 SPARSEMEM 的情况下有效。
  • node_page_ext:页的扩展属性,用于存储与页相关的额外属性。仅在启用了 CONFIG_PAGE_EXTENSION 的情况下有效。
  • node_start_pfn:该节点的起始物理页号。
  • node_present_pages:该节点上存在的物理页数量。
  • node_spanned_pages:该节点的物理页范围总大小,包括空洞。
  • node_id:节点标识符。

内存区域(zone)

内存区域是将物理内存按照不同属性进行划分的一种方式。每个内存区域都有一个唯一的类型标识,类型包括 ZONE_DMA、ZONE_DMA32、ZONE_NORMAL、ZONE_HIGHMEM、ZONE_MOVABLE、ZONE_DEVICE 等。

enum zone_type {
    // DMA区域,直接内存访问。如果有些设备不能直接访问所有内存,需要使用DMA区域
#ifdef CONFIG_ZONE_DMA
	ZONE_DMA,
#endif

    // DMA32区域64位系统,如果既要支持只能直接访问16MB以下的内存设备,又要支持只能直接访问4GB以下内存的32设备,必须使用此DMA32区域
#ifdef CONFIG_ZONE_DMA32
	ZONE_DMA32,
#endif

	/*
	 * Normal addressable memory is in ZONE_NORMAL. DMA operations can be
	 * performed on pages in ZONE_NORMAL if the DMA devices support
	 * transfers to all addressable memory.
	 */
    // 普通区域:直接映射到内核虚拟地址空间的内存区域,又称为普通区域
	ZONE_NORMAL,

#ifdef CONFIG_HIGHMEM
	/*
	 * A memory area that is only addressable by the kernel through
	 * mapping portions into its own address space. This is for example
	 * used by i386 to allow the kernel to address the memory beyond
	 * 900MB. The kernel will set up special mappings (page
	 * table entries on i386) for each page that the kernel needs to
	 * access.
	 */
    // 高端内存区域:内核和用户地址空间按1:3划分,内核地址空间只有1GB,不能把1GB以上的内存直接映射到内核地址
	ZONE_HIGHMEM,
#endif

    // 可移动区域:它是一个伪内存区域,用来存放内存碎片
	ZONE_MOVABLE,

#ifdef CONFIG_ZONE_DEVICE
    // 设备区域:支持持久内存热插拔增加的内存区域,每个内存区域有一个zone结构体来描述
	ZONE_DEVICE,
#endif

	__MAX_NR_ZONES
};

这样整理后的代码更加清晰易懂,注释也更容易理解各个内存区域的作用。

  • ZONE_DMA:适用于 DMA 的内存区域,长度受处理器类型的限制。在 IA-32 计算机上,限制为 16 MiB。
  • ZONE_DMA32:适用于可使用 32 位地址字寻址的 DMA 的内存区域。在 64 位系统上,ZONE_DMA 和 ZONE_DMA32 有所不同。在 32 位计算机上,ZONE_DMA32 区域为空,即长度为 0 MiB。
  • ZONE_NORMAL:直接映射区域,标记了可直接映射到内核段的普通内存区域。这是在所有体系结构上保证都会存在的唯一内存区域。但是,该地址范围并不一定对应实际的物理内存,例如在某些系统中,所有内存都属于 ZONE_DMA32 范围,而 ZONE_NORMAL 区域为空。
  • ZONE_HIGHMEM:该内存区域是早期 32 位体系结构的产物,因为内核和用户地址空间是 1:3 划分的,所以不能将内核 1GB 以上的内存直接映射到内核地址空间。在 64 位系统上,由于地址空间非常大,不存在这种问题。
  • ZONE_MOVABLE:可移动区域,是一个伪内存区域,用于防止内存碎片。可以用于分配无法被移动的内存对象的区域,将该区域中的页框移动到另一个区域,并释放原始区域。
  • ZONE_DEVICE:持久内存热插拔增加的区域,用于支持设备驱动程序动态分配内存。

一个内存节点可能包含多个内存区域,这些区域的类型和数量可以根据系统的需求进行配置。每个内存区域都有一组特定的操作函数集合,用于管理该区域中的页框。通过内存区域的划分,可以更加有效地管理和利用物理内存。

冷热页

  • struct zonepageset成员用于实现冷热分配器:在Linux内核中,为了提高内存管理的效率和性能,使用了冷热页(Cold and Hot Pages)的概念。具体来说,在每个内存区域(zone)中,定义了一个pageset结构体,用于管理该区域中的冷热页。

  • 热页指的是已经加载到CPU的高速缓存中的页面,与内存中的其他页相比,其数据结构可以更快地被访问。冷页则指不在CPU高速缓存中的页面,当需要访问它们时,需要从内存中读取数据,这会导致较高的延迟。

  • 在多处理器系统中,每个CPU都有一个或多个高速缓存。由于各个CPU具有独立的高速缓存,因此对冷热页的管理必须是针对每个CPU独立进行的。每个CPU都有自己的冷热分配器(pageset),用于管理该CPU的热页和冷页。这样可以避免多个CPU之间相互竞争同一份冷热页管理的问题,提高了系统的并发性和性能。

// 每CPU页面结构体定义
struct per_cpu_pages {
    int count;      // 列表中页面数量
    int high;       // 高水位标记,需要清空
    int batch;      // 伙伴系统添加/移除的块大小
 
    // 页面列表,每个迁移类型在PCP列表上存储一个
    struct list_head lists[MIGRATE_PCPTYPES];
};
  • count记录了与该列表相关的页面数量。它表示当前列表中的页的数量。

  • high是一个水印(watermark)。当count的值超过了high时,表示列表中的页太多了,需要进行一些处理。这个水印可以用来判断列表是否过载。

  • batch表示每次添加页的参考值。在填充CPU高速缓存时,通常不是一次只填充一个页面,而是以块为单位填充,batch就是指定每次填充的页数。

  • lists是一个数组,用于存储不同迁移类型的页面列表。每个迁移类型对应一个列表,在PCP(per-CPU Page)列表上存储。

物理页(page)

页是内存管理的最小单位:在内存管理中,页是内存的基本单位,页面中的内存物理地址是连续的。在Linux内核中,物理页被视为内存管理的基本单位,即内核中的内存管理单元MMU将物理页作为基本单位进行管理。

不同体系结构支持不同的页大小:不同的计算机体系结构支持不同大小的页。例如,32位体系结构通常支持4KB的页,而64位体系结构通常支持8KB的页。另外,像MIPS64架构体系可能支持更大的页,比如16KB的页。

每个物理页对应一个page结构体:在Linux内核中,每个物理页都对应一个称为页描述符(page structure)的数据结构,用于描述和管理该物理页的相关信息。每个内存节点的pglist_data实例中的成员node_mem_map指向该内存节点包含的所有物理页的页描述符组成的数组。

struct page {
	unsigned long flags;  // 原子标志,有些情况下会异步更新

	union {
		struct {  // 页面缓存和匿名页面
			struct list_head lru;
			// 如果最低位为0,则指向 inode 的 address_space 或为 NULL
			// 如果页映射为匿名地址,最低位置位,而且指针指向 anon_vma 对象
			struct address_space *mapping;
			pgoff_t index;  // 映射中的偏移量

			// 用于映射私有、不透明数据
			// 如果设置了 PagePrivate,则通常用于 buffer_heads
			// 如果设置了 PageSwapCache,则用于 swp_entry_t
			// 如果设置了 PageBuddy,则用于伙伴系统中的阶
			unsigned long private;
		};

		struct {  // slab、slob 和 slub
			union {
				struct list_head slab_list;
				struct {  // 部分页面
					struct page *next;
#ifdef CONFIG_64BIT
					int pages;  // 剩余页面数
					int pobjects;  // 近似计数
#else
					short int pages;
					short int pobjects;
#endif
				};
			};
			struct kmem_cache *slab_cache;  // 非 slob 时的 kmem_cache 指针
			/* 双字边界 */
			void *freelist;  // 第一个空闲对象
			union {
				void *s_mem;  // slab 分配器的第一个对象
				unsigned long counters;  // SLUB 计数器
				struct {  // SLUB
					unsigned inuse:16;
					unsigned objects:15;
					unsigned frozen:1;
				};
			};
		};
		// 其他字段...
	};
};
  • flags:表示页的各种状态和属性的标志位。这些标志位在某些情况下会被异步更新。

  • union:使用联合体来存储不同类型的页的信息。

    • 对于页面缓存和匿名页面(Page cache and anonymous pages):

      • lru:用于将页面链接到 LRU(Least Recently Used)链表,以进行页面置换。
      • mapping:指向 inode 的 address_space 或为 NULL。如果页面映射为匿名地址,则最低位置位且指针指向 anon_vma 对象。
      • index:表示页面在映射中的偏移量。
      • private:用于映射私有、不透明数据。根据不同的标志位,可以用于不同的目的,如 PagePrivate 用于 buffer_headsPageSwapCache 用于 swp_entry_tPageBuddy 用于伙伴系统中的阶。
    • 对于 slab、slob 和 slub:

      • slab_listnext:用于管理页面的链表结构,对于 slab 和 slob 分配器,用于链接已分配和未分配的页面;对于 slub 分配器,用于链接部分页面。
      • slab_cache:对于 slab 分配器,指向相关的 kmem_cache 结构体;对于 slob 分配器和 slub 分配器,该字段不使用。
      • freelists_mem:对于 slab 分配器,指向第一个空闲对象;对于 slob 分配器和 slub 分配器,用于存储其他信息,如计数器、使用中的对象数量等。

页表

页表是操作系统中用于实现虚拟内存到物理内存映射的重要数据结构。层次化的页表结构被设计用来支持对大地址空间的快速、高效管理。

  1. 内存地址的分解:
    根据四级页表结构,虚拟内存地址被分解为5部分,其中4个表项用于选择页,1个索引表示页内位置。每个指针末端的几个比特位用于指定所选页帧内部的位置,具体的比特位数由PAGE_SHIFT指定。PMD_SHIFT指定了页内偏移量和最后一级页表项所需比特位的总数。通过减去PAGE_SHIFT,可以得到最后一级页表项索引所需的比特位数。类似地,PUD_SHIFT由PMD_SHIFT加上中间层页表索引所需的比特位长度,而PGDIR_SHIFT由PUD_SHIFT加上上层页表索引所需的比特位长度。计算全局页目录中一项所能寻址的部分地址空间长度,可以通过以2为底的对数计算得到PGDIR_SHIFT。

  2. 页表的格式:
    内核提供了4个数据结构来表示页表项的结构:

  • pgd_t用于全局页目录项
  • pud_t用于上层页目录项
  • pmd_t用于中间页目录项
  • pte_t用于直接页表项
  1. 特定于PTE的信息:
    最后一级页表中的项不仅包含了指向页的内存位置的指针,还包含了与页面相关的附加信息。每种体系结构都需要提供两个东西,以便内存管理子系统能够修改pte_t项中额外的比特位。这两个东西分别是保存额外比特位的__pgprot数据类型,以及用于修改这些比特位的pte_modify函数。

通过以上分析,我们可以更好地理解页表的设计原理和结构,以及各个级别的页表项在管理地址空间时的作用和关联。如果您有任何进一步的问题或需要更多解释,请随时提出。

查询和设置内存页与体系结构相关状态的函数

  1. 查询函数:

    • pte_present():检查给定页表项是否存在于内存中。
    • pte_write():检查给定页表项是否可写。
    • pte_user():检查给定页表项是否为用户空间可访问。
    • pte_dirty():检查给定页表项是否被修改过。
    • pte_young():检查给定页表项是否被访问过。
  2. 设置函数:

    • set_pte():设置给定页表项的内容。
    • set_pte_at():在指定地址处设置页表项的内容。
    • pte_clear():清除给定页表项的内容。
    • pte_mkwrite():将只读页表项转换为可写。
    • pte_mkdirty():标记页表项已被修改。
  3. 体系结构相关函数:

    • pgd_index():获取全局页目录项的索引。
    • pmd_offset():获取中间页目录项的指针。
    • pud_offset():获取上层页目录项的指针。
    • pte_offset_kernel():获取内核页表项的指针。
    • pfn_to_page():将物理页框号转换为对应的页结构体。

创建和操作页表项的函数

  1. 创建页表项:

    • pte_alloc():分配一个新的页表项。
    • pte_alloc_one():分配一个新的单个页表项。
    • pte_alloc_kernel():分配一个新的内核页表项。
  2. 释放页表项:

    • pte_free():释放一个页表项的内存。
    • pte_free_kernel():释放一个内核页表项的内存。
  3. 操作页表项:

    • pte_clear():清除给定页表项的内容。
    • pte_val():获取页表项的原始值。
    • set_pte():设置指定页表项的内容。
    • pte_mkclean():将页表项标记为干净(未修改)。
    • pte_mkdirty():将页表项标记为脏(已修改)。
    • pte_present():检查给定页表项是否存在于内存中。
    • pte_write():检查给定页表项是否可写。

参考:Linux内核源码分析(内存调优/文件系统/进程管理/设备驱动/网络协议栈)教程

Linux内核源码系统性学习

>>>
  • 16
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值