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

这篇具有很好参考价值的文章主要介绍了【Linux 内核源码分析】物理内存组织结构。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

  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内核源码系统性学习文章来源地址https://www.toymoban.com/news/detail-835141.html

>>>

到了这里,关于【Linux 内核源码分析】物理内存组织结构的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 一、课程设计目的与任务《数据结构》课程设计是为训练学生的数据组织能力和提高程序设计能力而设置的增强实践能力的课程。目的:学习数据结构课程,旨在使学生学会分析研究数据对象的特性,学会数据的组织方法,以

    一、课程设计目的与任务 《数据结构》课程设计是为训练学生的数据组织能力和提高程序设计能力而设置的增强实践能力的课程。目的:学习数据结构课程,旨在使学生学会分析研究数据对象的特性,学会数据的组织方法,以便选择合适的数据的逻辑结构和存储结构以及相应

    2024年02月21日
    浏览(65)
  • ORACLE Apex: EBS多组织结构 理解与配置

    ORACLE EBS :多组织结构 理解与配置 Oracle APEX中文社区|Oracle APEX资源|Oracle APEX学习|EBS开发|EBS INTERFACE|Oracle EBS开发|Oracle数据库开发|EBS API| 多组织结构 多组织结构 ORACLE EBS一个很大的卖点是它的多组织结构.ORACLE EBS的文档资料里面解释呈现这样一个树型图:     ​   实际上, OR

    2024年02月08日
    浏览(29)
  • Rust之使用结构体来组织相关的数据

    结构或结构体,是一种自定义数据类型,它允许我们命名多个相关的值并将它们组成一个有机的结合体。 和元组一样,结构体中的数据可以拥有不同的类型,不同的是,结构体需要给每个数据赋予名字以便于清楚地表达它们的意义。 struct 被用来定义并命名结构体。一

    2024年02月15日
    浏览(31)
  • 各类Python项目的项目结构及代码组织最佳实践

    1. 了解Python项目文件组织结构非常重要 为什么要掌握python项目结构? 优秀的程序员都使用规范的项目代码结构,了解这些好的习惯方式,有助于快速读懂代码 如果项目是几个人合作开发,好的代码结构,便于分工,减少混乱,使项目开发过程更顺畅。 采用规范的项目结构,

    2023年04月16日
    浏览(92)
  • rust关于项目结构包,Crate和mod和目录的组织

    最近开始学习rust语言。感觉这门语言相对java确实是难上很多。开几个文章把遇到的问题记录一下 关于包,Crate这块先看看官方书籍怎么说的 crate 是 Rust 在编译时最小的代码单位。如果你用 rustc 而不是 cargo 来编译一个文件(第一章我们这么做过),编译器还是会将那个文件

    2024年02月13日
    浏览(42)
  • 【Rust】Rust学习 第五章使用结构体组织相关联的数据

    定义结构体,需要使用  struct  并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字和类型,我们称为  字段 ( field )。 实例化(不可变变量) 可变变量 字段初始化简写语法 通过已经存着的变

    2024年02月13日
    浏览(46)
  • 驱动开发:内核物理内存寻址读写

    在某些时候我们需要读写的进程可能存在虚拟内存保护机制,在该机制下用户的 CR3 以及 MDL 读写将直接失效,从而导致无法读取到正确的数据,本章我们将继续研究如何实现物理级别的寻址读写。 首先,驱动中的物理页读写是指在驱动中直接读写物理内存页(而不是虚拟内

    2024年02月11日
    浏览(38)
  • postgresql 使用之 存储架构 触摸真实数据的存储结构以及组织形式,存入数据库的数据原来在这里

    ​ 专栏内容 : postgresql内核源码分析 手写数据库toadb 并发编程 个人主页 :我的主页 座右铭:天行健,君子以自强不息;地势坤,君子以厚德载物. postgresql 数据库服务运行时,数据在磁盘上是如何存储的呢?这就涉及到了存储架构。 在文件系统中,我们可以看到以目录和文

    2024年02月14日
    浏览(38)
  • 深入分析linux内核的内存分配函数devm_kzalloc

    在分析驱动代码的时候,经常会遇到使用devm_kzalloc()为一个设备分配一片内存的情况。devm_kzalloc()是内核用来分配内存的函数,同样可以分配内存的内核函数还有devm_kmalloc, kzalloc, kmalloc。它们之间的区别在于devm_XXX分配的内存可以跟设备进行绑定,当设备跟驱动分离时,跟设备

    2024年02月02日
    浏览(41)
  • Linux内核源码分析 1:Linux内核体系架构和学习路线

    好久没有动笔写文章了,这段时间经历了蛮多事情的。这段时间自己写了一两个基于不同指令集的 Linux 内核, x86 和 RISC-V 。期间也去做了一些嵌入式相关的工作,研究了一下 ARM 指令集架构。 虽然今年九月份我就要申请了,具体申请 AI 方向还是机器人、嵌入式、操作系统、

    2024年02月07日
    浏览(51)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包