3.2.1 数据结构

3.2.1.1 页表

分页功能需要页表的来支持,页表的级数与OS版本与硬件架构有关,Linux当前版本最高支持5级页表,可以通过编译配置参数 CONFIG_PGTABLE_LEVELS 来控制开启页表的级数。各级页表分别是:

  • Page Global Directory(PGD)

  • Page 4 Directory(P4D)

  • Page Upper Directory(PUD)

  • Page Middle Directory(PMD)

  • Page Table Entry Directory(PTE)

内核为每级页表的表项都专门定义了一个数据类型:

/* file: arch/x86/include/asm/pgtable_64_types.h */

typedef unsigned long   pteval_t;
typedef unsigned long   pmdval_t;
typedef unsigned long   pudval_t;
typedef unsigned long   p4dval_t;
typedef unsigned long   pgdval_t;
typedef unsigned long   pgprotval_t;

typedef struct { pteval_t pte; } pte_t;

本质上这些类型都是无符号长整型,特意声明成单独的类型是为了让编译器做类型检查。各页表项的类型定义如下:

各个 pgtable-nop[4um]d.h 文件内包含着未开启对应级别页表时的一些声明,这里不展开。

各级页表中的表项(entry)的内容都是一样的,都是关于目标页的一些标记(Flags),典型的标记包括:

  • Present flag 目标页是否在主存中

  • Dirty flag 目标页面的内容是否发生了改变

  • Read/Write flag 标记目标页面的读写权限

  • User/Supervisor flag 目标页面的权限,例如用户进程就无权访问内核的虚拟地址空间

其他的各种标记不在这里一一列出,总之,无符号整型的位数足够表示所有的状态。

3.2.1.2 Page

页是OS管理物理内存的基本单元,每个物理内存页被称为一个页帧(Page Frame),每个页帧都会有一个编号,被称为PFN(Page Frame Number). 对于每个物理页帧,内核都会创建一个叫 page 的数据结构来追踪该页的各种信息与状态, page 是内存管理的核心,我们这里将其完整的代码贴出:

page 与物理页帧是一一对应关系,OS在初始化时会根据物理内存创建出所有的 page 实例,因此我们必须要控制该结构体的大小,以避免过多的消耗内存。但一个内存页可能被用于各种目的, page 中就需要封装各种状态信息,因此内核工程师们精心设计了该数据结构,可以看到很多属性之间都是 union 关系,可以理解为虽然系统在方方面面都需要使用内存,但一个物理内存页一次只能用于一种目的,用于不同目的的字段之间可以是“或”的关系,这样就显著地降低了整个数据结构的大小。

我们在这里先不关心每个字段的意义,后续章节讲到具体的应用场合时会针对性地做详细介绍,不过我们可以先看一下比较重要的字段:

  • flags 该字段是一个无符号长整型,用来存放各种类型的页标记,内核有个专门的文件来定义各种标记位,叫着 include/linux/page-flags.h, 例如标记 PG_locked 表示当前页被加锁了。

    flags字段是一个寸土寸金的地方,只有最重要的标记才有资格在该字段中占有一席之地。

  • _refcount 引用计数,如果是 -1 的话说明没有该页没有被使用,可以重新分配给需要的进程。

3.2.1.3 Zone

理想情况下,内存中的所有页面从功能上讲都是等价的,都可以用于任何目的,但现实却并非如此,例如一些DMA处理器只能访问固定范围内的地址空间(见这里)。因此内核将整个内存地址空间划分成了不同的区,每个区叫着一个 Zone, 每个 Zone 都有自己的用途。

Zone 的划分与系统架构与位数相关,典型x86的32位系统拥有如下分区:

  • ZONEDMA: 用于DMA, 包含16M以内的内存页

  • ZONENORMAL: 包含16M到896M的内存页,这部分内存直接映射到了内核的虚拟地址空间

  • ZONEHIGHMEM: 包含大于896M的内存页,内核通过这部分地址空间来动态映射额外的内存或者IO

什么是ZONEHIGHMEM及为什么需要它?

我们知道32位系统的虚拟地址空间为4G, 并且内核与用户使用相同的虚拟地址空间(详见上一节),内核使用的地址范围是 0xC00000000xFFFFFFFF, 即高位的1G. 也就是说,能够映射到内核地址空间的物理内存只有1G, 如果系统有更大的物理内存,内核也无法使用。为了能够灵活地映射到更多的内存,Linux 将内核的地址空间划分成两块,小于896M 的范围称为低端内存(Low Memory),高于该部分的称为高端内存(High Memory)。

我们知道虚拟地址到物理地址的转化需要页表的支持与MMU的参与,但内核为了效率,将低端内存与物理内存直接关联了起来,具体方法是将 0 到896M 的物理地址直接加上偏移量 0xC0000000 映射到低端内存的地址空间,这样内核在使用低端内存时,物理地址与虚拟地址的转换就非常简单。而128M 的高端内存地址则通过页表动态映射其它的物理内存,操作系统有多种方式来进行这种映射,这里不展开。

除了协助访问更大的物理地址,内核还需要预留一部分地址空间用于其他用途,例如映射IO地址等。

在64位系统中,由于寻址范围的增加,用于内核的虚拟地址空间已经足够大了,因此就不再需要ZONEHIGHMEM了。

内核对 Zone 的划分是可配置的,申明代码如下:

可以看到一定存在的分区是 ZONE_NORMALZONE_MOVABLE, 其他的Zone都是可选项。分区 ZONE_MOVABLE 是个特殊的分区,该分区内的内存页都必须是可迁移的,主要用途包括实现内存的热插拔与内存规整(以减少内存碎片,这里指页这个粒度的内存碎片)。

在Linux 中,可以通过 proc 文件系统来查看当前系统的内存分区情况,具体命令是: cat /proc/zoneinfo

内核中用来封装 Zone 的数据结构定义如下:

其中重要的字段如下:

  • _watermark 水位线用来表示 Zone 中内存的使用情况,用来触发内存回收或 swap 等行为。定义如下:

  • struct pglist_data *zone_pgdat 本Zone所在的Node, Node的概念在下一节介绍

  • struct per_cpu_pageset __percpu *pageset Zone 是一个全局性的变量,多个CPU在对 Zone 中的内存进行分配和释放的过程中会面临严重的竞争问题,于是系统在 Zone 中为每个 CPU 设置了一个本地缓存,该字段就是用来管理每个CPU的缓存页面的。

  • zone_start_pfn Zone 的起始页帧号

  • managed_pages, spanned_pages, present_pages 记录被 Zone 管理的内存页数量,每个字段的含义及计算方式见注释

  • name Zone 的名称,例如 “DMA”, “NORMAL”等

  • struct free_area free_area[MAX_ORDER]; 用于 Buddy System, 后续讲 Buddy System 时会详细介绍

  • flags 用来标记 Zone 的各种信息

因为 Zone 被CPU访问地非常频繁,为了提升访问效率,整个数据结构要求对齐CPU的L1缓存。同时整个数据结构被 ZONE_PADDING 分割成了几部分,目的是为了将用于同一目的的字段聚合在同一个缓存 Line 中。

由于 Zone 是根据内存的用途进行划分的,所以 Zone 也是系统进行内存分配的直接来源,内核最底层的内存管理系统Buddy System就是基于Zone来对内存进行分配与释放的。

3.2.1.4 Node

在介绍调度器的负载均衡时我们讨论过不同的CPU拓扑结构,其主要区别体现在不同CPU对内存的访问方式上,在NUMA(Non-uniform Memory Access) 架构下,每个CPU集群都有一个自己的本地内存,每个这样的本地内存在内核中被叫着一个Node, 使用数据结构 pglist_data 来表示,该数据结构定义如下:

重要的字段包括:

  • struct zone node_zones[MAX_NR_ZONES]; 该 node 划分出来的分区

  • struct zonelist node_zonelists[MAX_ZONELISTS]; 用来串起所有 node 中所有 zone, 由于 Zone 是内存分配时直接打交道的对象,当当前node没有足够的内存时,系统需要从其他 zone 中去请求内存,这个列表就是用来方便遍历用的

  • struct page *node_mem_map; 用来记录本 node 的所有 page, 后续介绍物理内存模型时会涉及

  • node_start_pfn 本 node 的起始 PFN(Page Frame Number)

  • node_present_pages, node_spanned_pages 该 node 中对应的 presentpages 与 spannedpages, 具体意义参见 Zone 中的对应字段

  • flags 记录 node 的各种标记

在 UMA(Uniform Memory Access)架构下,系统所有的内存都使用一个 node 来表示。node, zone, page 是内存管理模块最核心的三个数据结构,三者的基本机构如下:

Last updated

Was this helpful?