For the complete documentation index, see llms.txt. This page is also available as Markdown.

Node-Zone-Page

Node, Zone, Page是内核中内存管理模块非常常见的几个概念,但是一直以来都不清楚其究竟是什么含义,之间又有什么联系。我想很多人也有类似我的困惑。今天我就装个大头,帮助大家理理这里面的关系。

一张经典的图

这里写图片描述

此图来自Describing Physical Memory

这张图想必有探索过内存管理的同学可能都会见过,但是估计大部分看不懂,或者知道个一知半解。因为我以前看的时候也看不懂,这讲的是个毛?好像是有点那个意思,但是是如何和实际上的内存对应起来的呢?

希望看完这篇文章后,能对这张图能够有更多一点的了解。

Node和Zone的概念

我想先讲一下这两个概念之间的区别,或许有了这两者之间的概念,才能够更好的理解内存管理。至少我走过了这么多弯路之后,才发现自己之前的概念是错误的。

Node

Zone

两者结合在一起

让我尝试用两句话来定义Node和Zone

  • Node是从内存亲和性出发的定义,看到的表现是地址上的分布,但实际上不是从地址出发的定义。

  • Zone是一个从地址大小出发的定义,不论系统上的内存大小是多少,每个zone的空间是一定的。比如ZONE_DMA一定是16M一下的空间。

根据以上的定义其实可以得出,在某些node上的zone是空的。比如你仔细看上面几个图,node1上就不会存在ZONE_DMA的空间。

我的一张图

在文章的一开始,我们就看到了一张经典的对内存管理结构的图。对node和zone有了一定理解之后,尝试将这两者的关系,画成图。其中node_data对应的是pg_data_t。

沿用之前6G内存系统的例子,这时内核初始化完的结构如下:

图中有几个点值得注意:

  • ZONE_DMA和ZONE_DMA32的大小是固定的

  • Node0上的ZONE_NORMAL是空的

  • Node2上的ZONE_DMA是空的

怎么样,这样看是不是清楚了一点?

验证的源代码

来,给你个源代码,这样你自己回去也可以试试。

注: 输出的数值会和理论值有差别

PS: free_area_init()中也有相关的日志输出。

划分node/zone的边界

既然每个node都由pgdat表示,那么就需要看看这部分信息是如何初始化的。

函数free_area_init是负责初始化的,这里我们主要观察该函数的行为。而该函数在整个系统初始化过程的位置参见memblock中的初始化流程。

相关日志

内核启动过程中,我们可以通过free_area_init()中输出的日志观察每个node/zone所使用的区域。

日志中搜索的关键字是:"Zone ranges:", "Early memory node ranges"。

运行起来后,能在文件/proc/zoneinfo中看到更多的数据。

ZONE_MOVABLE

这次花了点时间看了下ZONE_MOVABLE,本来以为这个zone和其他的没有什么区别。实际不然。

这个zone的大小,在x86上,是计算出来的,而不是像其他的zone有明确的事先边界。这里我把和ZONE_MOVABLE计算相关的代码在下面着重标出来。

为什么要有ZONE_MOVABLE

如果说其他zone的存在是因为物理访问的限制,比如某些设备只能访问32位以内的内存,所以划分了ZONE_DMA32,那ZONE_MOVABLE则更多是软件上的。

可以看ZONE_MOVABLE的注释:

ZONE_MOVABLE is similar to ZONE_NORMAL, except that it contains movable pages with few exceptional cases described below. Main use cases for ZONE_MOVABLE are to make memory offlining/unplug more likely to succeed, and to locally limit unmovable allocations

所以ZONE_MOVABLE没有硬性的边界,其边界是人为可以调整,并且在启动时计算出来的。

计算ZONE_MOVABLE的边界

默认情况下我们看不到这个zone的存在,有下面几种方式可以开启:

  • movablecore

  • kernelcore

这两者是正反两面,我们只看kernelcore。

使用kernelcore有三种参数方式:

  • kernelcore=512M # 512MB

  • kernelcore=20% # 总内存的20%

  • kernelcore=mirror

其大致意思就是希望至少留多少空间给kernel,这部分因为是kernel用,所以我不希望它热插拔。然后剩下的部分呢,拿去做movable就好。

根据这个参数,find_zone_movable_pfns_for_nodes()会计算出对每个numa节点的边界.

  • 如果没有设置参数,或者要求的kernelcore大于所有内存,则不会进行计算

  • movable_zone只会在最后一个zone,find_usable_zone_for_movable

  • 根据kernelcore平均计算出每个节点上预留的大小kernelcore_node.

  • 每个节点上预留kernelcore_node,并计算出zone_movable_pfn[nid]

然后,这个计算出的zone_movable_pfn会在adjust_zone_range_for_zone_movable()时使用。

  • ZONE_MOVABLE: 因为是软件计算出来的,所以zone_start_pfn就是zone_movable_pfn[nid] 而zone_end_pfn就是movable_zone的最大值

  • 非ZONE_MOVABLE且!mirrored_kernelcore: 如果当前的这段区域和zone_movable_pfn[nid] 重叠,则切分

调整ZONE_MOVABLE的边界

很有意思的是,我发现find_zone_movable_pfns_for_nodes()完成后,内核打印Zone ranges和Movable zone start for each node的时候,实际上movable zone的区域还没有落到实处。

因为内核此时输出的日志限制,zone normal还是原来的范围。

真正让zone movalbe出现的,还要依赖函数adjust_zone_range_for_zone_movable()。

到这里,zone movable的区域才算是真的出现了。而被挖掉一块的zone,也剔除了被占用的部分。

mirrored_kernelcore

之前我们看到kernelcore命令行参数有三种方式,其中最后一种mirror和前两者的处理不同。

当我们指定内核命令行参数kernelcore=mirror后,就会设置mirrored_kernelcore为true。这样在find_zone_movable_pfns_for_nodes()中就会特殊处理。

但是有意思的是,这个命令行选项还要配合memblock_has_mirror()使用。

这里计算zone_movable_pfn时,就变成了取节点上非镜像内存的最小值。

但是这样的话,最后通过adjust_zone_range_for_zone_movable()调整,得到实际的zone边界。我觉得这意味着硬件标识的mirror区域也是在低zone范围内的.

思考题

每个page作为链表元素链接在了free_list上,那page数据结构本身放在哪里呢?你猜的到吗?

Last updated

Was this helpful?