diff --git a/study/kernel/02-memory/01-description/01-memory/README.md b/study/kernel/02-memory/01-description/01-memory/README.md index f1611b8..3997c26 100644 --- a/study/kernel/02-memory/01-description/01-memory/README.md +++ b/study/kernel/02-memory/01-description/01-memory/README.md @@ -1,251 +1,251 @@ -服务器体系与共享存储器架构 -======= - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - - - - - -##参照 -------- - -| 链接 | -|:-------:| -| [内存管理(一)内存模型之Node](http://biancheng.dnbcw.info/linux/387391.html) | -| [Linux 内存管理 重要结构体](http://blog.chinaunix.net/uid-26009500-id-3078986.html) | -| [Bootmem机制](http://blog.csdn.net/samssm/article/details/25064897) | -| [Linux-2.6.32 NUMA架构之内存和调度](http://www.cnblogs.com/zhenjing/archive/2012/03/21/linux_numa.html) | -| [Linux 用户空间与内核空间——高端内存详解](http://blog.csdn.net/tommy_wxie/article/details/17122923) | -| [探索 Linux 内存模型](http://www.ibm.com/developerworks/cn/linux/l-memmod/) | -| [Linux内存管理](http://blog.chinaunix.net/uid/21718047/cid-151509-list-2.html) | -| [内存管理-之内核内存管理-基于linux3.10](http://blog.csdn.net/shichaog/article/details/45509917) | -| [内存管理(一)](http://www.cnblogs.com/openix/p/3334026.html) | -| [Linux内存管理原理](http://www.cnblogs.com/zhaoyl/p/3695517.html) | -| [第 15 章 内存映射和 DMA](http://www.embeddedlinux.org.cn/ldd3/ch15.html) | -| [ 内存管理(二)struct page ](http://blog.chinaunix.net/uid-30282771-id-5176971.html) | -| [进程页表页和内核页表](http://guojing.me/linux-kernel-architecture/posts/thread-page-table-and-kernel-page-table/) - - - -#1 前景回顾 -------- - -前面我们讲到[服务器体系(SMP, NUMA, MPP)与共享存储器架构(UMA和NUMA)](http://blog.csdn.net/gatieme/article/details/52098615) - -#1.1 UMA和NUMA两种模型 -------- - -共享存储型多处理机有两种模型 - -* 均匀存储器存取(Uniform-Memory-Access,简称UMA)模型 - - 将可用内存以连续方式组织起来, -* 非均匀存储器存取(Nonuniform-Memory-Access,简称NUMA)模型 - -##1.2 UMA模型 -------- - -传统的多核运算是使用SMP(Symmetric Multi-Processor )模式:将多个处理器与一个集中的存储器和I/O总线相连。所有处理器只能访问同一个物理存储器,因此SMP系统有时也被称为一致存储器访问(UMA)结构体系,一致性意指无论在什么时候,处理器只能为内存的每个数据保持或共享唯一一个数值。 - - - ->物理存储器被所有处理机均匀共享。所有处理机对所有存储字具有相同的存取时间,这就是为什么称它为均匀存储器存取的原因。每台处理机可以有私用高速缓存,外围设备也以一定形式共享。 - -很显然,SMP的缺点是可伸缩性有限,因为在存储器和I/O接口达到饱和的时候,增加处理器并不能获得更高的性能,与之相对应的有AMP架构,不同核之间有主从关系,如一个核控制另外一个核的业务,可以理解为多核系统中控制平面和数据平面。 - -##1.3 NUMA模型 -------- - - -NUMA模式是一种分布式存储器访问方式,处理器可以同时访问不同的存储器地址,大幅度提高并行性。 NUMA总是多处理器计算机,系统的哪个CPU都有本地内存, 可支持快速的访问, 各个处理器之前通过总线链接起来, 以支持堆其他CPU的本地内存的访问, 当然访问要比本地内存慢. - - -NUMA模式下,处理器被划分成多个"节点"(node), 每个节点被分配有的本地存储器空间。 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多。 - ->其访问时间随存储字的位置不同而变化。其共享存储器物理上是分布在所有处理机的本地存储器上。所有本地存储器的集合组成了全局地址空间,可被所有的处理机访问。处理机访问本地存储器是比较快的,但访问属于另一台处理机的远程存储器则比较慢,因为通过互连网络会产生附加时延 - -NUMA 的主要优点是伸缩性。NUMA 体系结构在设计上已超越了 SMP 体系结构在伸缩性上的限制。通过 SMP,所有的内存访问都传递到相同的共享内存总线。这种方式非常适用于 CPU 数量相对较少的情况,但不适用于具有几十个甚至几百个 CPU 的情况,因为这些 CPU 会相互竞争对共享内存总线的访问。NUMA 通过限制任何一条内存总线上的 CPU 数量并依靠高速互连来连接各个节点,从而缓解了这些瓶颈状况。 - - - -#2 (N)UMA模型中linux内存的机构 -------- - - -Linux适用于各种不同的体系结构, 而不同体系结构在内存管理方面的差别很大. 因此linux内核需要用一种体系结构无关的方式来表示内存. - -Linux内核通过插入一些兼容层, 使得不同体系结构的差异很好的被隐藏起来, 内核对一致和非一致内存访问使用相同的数据结构 - - -##2.1 (N)UMA模型中linux内存的机构 -------- - - - -非一致存储器访问(NUMA)模式下 - -* 处理器被划分成多个"节点"(node), 每个节点被分配有的本地存储器空间. 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多 - - -* 内存被分割成多个区域(BANK,也叫"簇"),依据簇与处理器的"距离"不同, 访问不同簇的代码也会不同. 比如,可能把内存的一个簇指派给每个处理器,或则某个簇和设备卡很近,很适合DMA,那么就指派给该设备。因此当前的多数系统会把内存系统分割成2块区域,一块是专门给CPU去访问,一块是给外围设备板卡的DMA去访问 - - ->在UMA系统中, 内存就相当于一个只使用一个NUMA节点来管理整个系统的内存. 而内存管理的其他地方则认为他们就是在处理一个(伪)NUMA系统. - - - -#2.2 Linux物理内存的组织形式 -------- - -Linux把物理内存划分为三个层次来管理 - -| 层次 | 描述 | -|:----:|:----:| -| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | -| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | -| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | - -为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 - -* 首先, 内存被划分为结点. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. - -* 接着各个节点又被划分为内存管理区域, 一个管理区域通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. - - -* 最后页帧(page frame)代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号. - - -在一个单独的节点内,任一给定CPU访问页面所需的时间都是相同的。然而,对不同的CPU,这个时间可能就不同。对每个CPU而言,内核都试图把耗时节点的访问次数减到最少这就要小心地选择CPU最常引用的内核数据结构的存放位置. - - - - - -##2.3 内存节点node -------- - - - ->CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 -> ->系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 - -在LINUX中引入一个数据结构`struct pglist_data` ,来描述一个node,定义在[`include/linux/mmzone.h`](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L630) 文件中。(这个结构被typedef pg_data_t)。 - - -* 对于NUMA系统来讲, 整个系统的内存由一个[node_data](http://lxr.free-electrons.com/source/arch/s390/numa/numa.c?v=4.7#L23)的pg_data_t指针数组来管理, - -* 对于PC这样的UMA系统,使用struct pglist_data contig_page_data ,作为系统唯一的node管理所有的内存区域。(UMA系统中中只有一个node) - - -可以使用NODE_DATA(node_id)来查找系统中编号为node_id的结点, 参见[NODE_DATA的定义](http://lxr.free-electrons.com/ident?v=4.7;i=NODE_DATA) - -UMA结构下由于只有一个结点, 因此该宏总是返回全局的contig_page_data, 而与参数node_id无关. - -```cpp -extern struct pglist_data *node_data[]; -#define NODE_DATA(nid) (node_data[(nid)]) -``` - - -在UMA结构的机器中, 只有一个node结点即contig_page_data, 此时NODE_DATA直接指向了全局的contig_page_data, 而与node的编号nid无关, 参照[include/linux/mmzone.h?v=4.7, line 858](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L858), 其中全局唯一的内存node结点contig_page_data定义在[mm/nobootmem.c?v=4.7, line 27](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L27), [linux-2.4.37](http://lxr.free-electrons.com/source/mm/numa.c?v=2.4.37#L15) - - - -```cpp -#ifndef CONFIG_NEED_MULTIPLE_NODES -extern struct pglist_data contig_page_data; -#define NODE_DATA(nid) (&contig_page_data) -#define NODE_MEM_MAP(nid) mem_map -else -/* ...... */ -#endif -``` - -在分配一个页面时, Linux采用节点局部分配的策略, 从最靠近运行中的CPU的节点分配内存, 由于进程往往是在同一个CPU上运行, 因此从当前节点得到的内存很可能被用到 - - - - -##2.4 物理内存区域zone -------- - -因为实际的计算机体系结构有硬件的诸多限制, 这限制了页框可以使用的方式. 尤其是, Linux内核必须处理80x86体系结构的两种硬件约束. - -* ISA总线的直接内存存储DMA处理器有一个严格的限制 : 他们只能对RAM的前16MB进行寻址 - -* 在具有大容量RAM的现代32位计算机中, CPU不能直接访问所有的物理地址, 因为线性地址空间太小, 内核不可能直接映射所有物理内存到线性地址空间, 我们会在后面典型架构(x86)上内存区域划分详细讲解x86_32上的内存区域划分 - - -因此Linux内核对不同区域的内存需要采用不同的管理方式和映射方式, - -为了解决这些制约条件,Linux使用了三种区: - -1. ZONE_DMA : 这个区包含的页用来执行DMA操作。 - -2. ZONE_NOMAL : 这个区包含的都是能正常映射的页。 - -3. ZONE_HIGHEM : 这个区包"高端内存",其中的页能不永久地映射到内核地址空间 - -而为了兼容一些设备的热插拔支持以及内存碎片化的处理, 内核也引入一些逻辑上的内存区. - -1. ZONE_MOVABLE : 内核定义了一个伪内存域ZONE_MOVABLE, 在防止物理内存碎片的机制memory migration中需要使用该内存域. 供防止物理内存碎片的极致使用 - -2. ZONE_DEVICE : 为支持热插拔设备而分配的Non Volatile Memory非易失性内存 - -内核将每个簇所对应的node又被分成的称为管理区(zone)的块,它们各自描述在内存中的范围。一个管理区(zone)由[struct zone](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L326)结构体来描述,在linux-2.4.37之前的内核中是用[`typedef struct zone_struct zone_t `](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=2.4.37#L47)数据结构来描述) - - - -对于x86_32的机器,管理区(内存区域)类型如下分布 - - -| 类型 | 区域 | -| :------- | ----: | -| ZONE_DMA | 0~15MB | -| ZONE_NORMAL | 16MB~895MB | -| ZONE_HIGHMEM | 896MB~物理内存结束 | - - -内核在初始化内存管理区时, 首先建立管理区表zone_table. 参见[mm/page_alloc.c?v=2.4.37, line 38](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=2.4.37#L38) - - -```cpp -/* - * - * The zone_table array is used to look up the address of the - * struct zone corresponding to a given zone number (ZONE_DMA, - * ZONE_NORMAL, or ZONE_HIGHMEM). - */ -zone_t *zone_table[MAX_NR_ZONES*MAX_NR_NODES]; -EXPORT_SYMBOL(zone_table); -``` - -该表处理起来就像一个多维数组, - -* MAX_NR_ZONES是一个节点中所能包容纳的管理区的最大数, 如3个, 定义在[include/linux/mmzone.h?v=2.4.37, line 25](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=2.4.37#L25), 与zone区域的类型(ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM)定义在一起. 当然这时候我们这些标识都是通过宏的方式来实现的, 而不是如今的枚举类型 - - -* MAX_NR_NODES是可以存在的节点的最大数. - -* 函数EXPORT_SYMBOL使得内核的变量或者函数可以被载入的模块(比如我们的驱动模块)所访问. - - -##2.5 内存页page -------- - -大多数内核(kernel)的操作只使用ZONE_NORMAL区域,系统内存由很多固定大小的内存块组成的,这样的内存块称作为“页”(PAGE), - -x86体系结构中,page的大小为4096个字节。 - -每个物理的页由一个`struct page`的数据结构对象来描述。页的数据结构对象都保存在`mem_map`全局数组中,该数组通常被存放在ZONE_NORMAL的首部,或者就在小内存系统中为装入内核映像而预留的区域之后。从载入内核的低地址内存区域的后面内存区域,也就是ZONE_NORMAL开始的地方的内存的页的数据结构对象,都保存在这个全局数组中。 - - - -##2.6 高端内存 -------- - -由于能够被Linux内核直接访问的ZONE_NORMAL区域的内存空间也是有限的,所以LINUX提出了高端内存(High memory)的概念,并且允许对高端内存的访问 +服务器体系与共享存储器架构 +======= + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + + + + + +##参照 +------- + +| 链接 | +|:-------:| +| [内存管理(一)内存模型之Node](http://biancheng.dnbcw.info/linux/387391.html) | +| [Linux 内存管理 重要结构体](http://blog.chinaunix.net/uid-26009500-id-3078986.html) | +| [Bootmem机制](http://blog.csdn.net/samssm/article/details/25064897) | +| [Linux-2.6.32 NUMA架构之内存和调度](http://www.cnblogs.com/zhenjing/archive/2012/03/21/linux_numa.html) | +| [Linux 用户空间与内核空间——高端内存详解](http://blog.csdn.net/tommy_wxie/article/details/17122923) | +| [探索 Linux 内存模型](http://www.ibm.com/developerworks/cn/linux/l-memmod/) | +| [Linux内存管理](http://blog.chinaunix.net/uid/21718047/cid-151509-list-2.html) | +| [内存管理-之内核内存管理-基于linux3.10](http://blog.csdn.net/shichaog/article/details/45509917) | +| [内存管理(一)](http://www.cnblogs.com/openix/p/3334026.html) | +| [Linux内存管理原理](http://www.cnblogs.com/zhaoyl/p/3695517.html) | +| [第 15 章 内存映射和 DMA](http://www.embeddedlinux.org.cn/ldd3/ch15.html) | +| [ 内存管理(二)struct page ](http://blog.chinaunix.net/uid-30282771-id-5176971.html) | +| [进程页表页和内核页表](http://guojing.me/linux-kernel-architecture/posts/thread-page-table-and-kernel-page-table/) + + + +#1 前景回顾 +------- + +前面我们讲到[服务器体系(SMP, NUMA, MPP)与共享存储器架构(UMA和NUMA)](http://blog.csdn.net/gatieme/article/details/52098615) + +#1.1 UMA和NUMA两种模型 +------- + +共享存储型多处理机有两种模型 + +* 均匀存储器存取(Uniform-Memory-Access,简称UMA)模型 + + 将可用内存以连续方式组织起来, +* 非均匀存储器存取(Nonuniform-Memory-Access,简称NUMA)模型 + +##1.2 UMA模型 +------- + +传统的多核运算是使用SMP(Symmetric Multi-Processor )模式:将多个处理器与一个集中的存储器和I/O总线相连。所有处理器只能访问同一个物理存储器,因此SMP系统有时也被称为一致存储器访问(UMA)结构体系,一致性意指无论在什么时候,处理器只能为内存的每个数据保持或共享唯一一个数值。 + + + +>物理存储器被所有处理机均匀共享。所有处理机对所有存储字具有相同的存取时间,这就是为什么称它为均匀存储器存取的原因。每台处理机可以有私用高速缓存,外围设备也以一定形式共享。 + +很显然,SMP的缺点是可伸缩性有限,因为在存储器和I/O接口达到饱和的时候,增加处理器并不能获得更高的性能,与之相对应的有AMP架构,不同核之间有主从关系,如一个核控制另外一个核的业务,可以理解为多核系统中控制平面和数据平面。 + +##1.3 NUMA模型 +------- + + +NUMA模式是一种分布式存储器访问方式,处理器可以同时访问不同的存储器地址,大幅度提高并行性。 NUMA总是多处理器计算机,系统的哪个CPU都有本地内存, 可支持快速的访问, 各个处理器之前通过总线链接起来, 以支持堆其他CPU的本地内存的访问, 当然访问要比本地内存慢. + + +NUMA模式下,处理器被划分成多个"节点"(node), 每个节点被分配有的本地存储器空间。 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多。 + +>其访问时间随存储字的位置不同而变化。其共享存储器物理上是分布在所有处理机的本地存储器上。所有本地存储器的集合组成了全局地址空间,可被所有的处理机访问。处理机访问本地存储器是比较快的,但访问属于另一台处理机的远程存储器则比较慢,因为通过互连网络会产生附加时延 + +NUMA 的主要优点是伸缩性。NUMA 体系结构在设计上已超越了 SMP 体系结构在伸缩性上的限制。通过 SMP,所有的内存访问都传递到相同的共享内存总线。这种方式非常适用于 CPU 数量相对较少的情况,但不适用于具有几十个甚至几百个 CPU 的情况,因为这些 CPU 会相互竞争对共享内存总线的访问。NUMA 通过限制任何一条内存总线上的 CPU 数量并依靠高速互连来连接各个节点,从而缓解了这些瓶颈状况。 + + + +#2 (N)UMA模型中linux内存的机构 +------- + + +Linux适用于各种不同的体系结构, 而不同体系结构在内存管理方面的差别很大. 因此linux内核需要用一种体系结构无关的方式来表示内存. + +Linux内核通过插入一些兼容层, 使得不同体系结构的差异很好的被隐藏起来, 内核对一致和非一致内存访问使用相同的数据结构 + + +##2.1 (N)UMA模型中linux内存的机构 +------- + + + +非一致存储器访问(NUMA)模式下 + +* 处理器被划分成多个"节点"(node), 每个节点被分配有的本地存储器空间. 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多 + + +* 内存被分割成多个区域(BANK,也叫"簇"),依据簇与处理器的"距离"不同, 访问不同簇的代码也会不同. 比如,可能把内存的一个簇指派给每个处理器,或则某个簇和设备卡很近,很适合DMA,那么就指派给该设备。因此当前的多数系统会把内存系统分割成2块区域,一块是专门给CPU去访问,一块是给外围设备板卡的DMA去访问 + + +>在UMA系统中, 内存就相当于一个只使用一个NUMA节点来管理整个系统的内存. 而内存管理的其他地方则认为他们就是在处理一个(伪)NUMA系统. + + + +#2.2 Linux物理内存的组织形式 +------- + +Linux把物理内存划分为三个层次来管理 + +| 层次 | 描述 | +|:----:|:----:| +| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | +| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | +| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | + +为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 + +* 首先, 内存被划分为结点. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. + +* 接着各个节点又被划分为内存管理区域, 一个管理区域通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. + + +* 最后页帧(page frame)代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号. + + +在一个单独的节点内,任一给定CPU访问页面所需的时间都是相同的。然而,对不同的CPU,这个时间可能就不同。对每个CPU而言,内核都试图把耗时节点的访问次数减到最少这就要小心地选择CPU最常引用的内核数据结构的存放位置. + + + + + +##2.3 内存节点node +------- + + + +>CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 +> +>系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 + +在LINUX中引入一个数据结构`struct pglist_data` ,来描述一个node,定义在[`include/linux/mmzone.h`](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L630) 文件中。(这个结构被typedef pg_data_t)。 + + +* 对于NUMA系统来讲, 整个系统的内存由一个[node_data](http://lxr.free-electrons.com/source/arch/s390/numa/numa.c?v=4.7#L23)的pg_data_t指针数组来管理, + +* 对于PC这样的UMA系统,使用struct pglist_data contig_page_data ,作为系统唯一的node管理所有的内存区域。(UMA系统中中只有一个node) + + +可以使用NODE_DATA(node_id)来查找系统中编号为node_id的结点, 参见[NODE_DATA的定义](http://lxr.free-electrons.com/ident?v=4.7;i=NODE_DATA) + +UMA结构下由于只有一个结点, 因此该宏总是返回全局的contig_page_data, 而与参数node_id无关. + +```cpp +extern struct pglist_data *node_data[]; +#define NODE_DATA(nid) (node_data[(nid)]) +``` + + +在UMA结构的机器中, 只有一个node结点即contig_page_data, 此时NODE_DATA直接指向了全局的contig_page_data, 而与node的编号nid无关, 参照[include/linux/mmzone.h?v=4.7, line 858](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L858), 其中全局唯一的内存node结点contig_page_data定义在[mm/nobootmem.c?v=4.7, line 27](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L27), [linux-2.4.37](http://lxr.free-electrons.com/source/mm/numa.c?v=2.4.37#L15) + + + +```cpp +#ifndef CONFIG_NEED_MULTIPLE_NODES +extern struct pglist_data contig_page_data; +#define NODE_DATA(nid) (&contig_page_data) +#define NODE_MEM_MAP(nid) mem_map +else +/* ...... */ +#endif +``` + +在分配一个页面时, Linux采用节点局部分配的策略, 从最靠近运行中的CPU的节点分配内存, 由于进程往往是在同一个CPU上运行, 因此从当前节点得到的内存很可能被用到 + + + + +##2.4 物理内存区域zone +------- + +因为实际的计算机体系结构有硬件的诸多限制, 这限制了页框可以使用的方式. 尤其是, Linux内核必须处理80x86体系结构的两种硬件约束. + +* ISA总线的直接内存存储DMA处理器有一个严格的限制 : 他们只能对RAM的前16MB进行寻址 + +* 在具有大容量RAM的现代32位计算机中, CPU不能直接访问所有的物理地址, 因为线性地址空间太小, 内核不可能直接映射所有物理内存到线性地址空间, 我们会在后面典型架构(x86)上内存区域划分详细讲解x86_32上的内存区域划分 + + +因此Linux内核对不同区域的内存需要采用不同的管理方式和映射方式, + +为了解决这些制约条件,Linux使用了三种区: + +1. ZONE_DMA : 这个区包含的页用来执行DMA操作。 + +2. ZONE_NOMAL : 这个区包含的都是能正常映射的页。 + +3. ZONE_HIGHEM : 这个区包"高端内存",其中的页能不永久地映射到内核地址空间 + +而为了兼容一些设备的热插拔支持以及内存碎片化的处理, 内核也引入一些逻辑上的内存区. + +1. ZONE_MOVABLE : 内核定义了一个伪内存域ZONE_MOVABLE, 在防止物理内存碎片的机制memory migration中需要使用该内存域. 供防止物理内存碎片的极致使用 + +2. ZONE_DEVICE : 为支持热插拔设备而分配的Non Volatile Memory非易失性内存 + +内核将每个簇所对应的node又被分成的称为管理区(zone)的块,它们各自描述在内存中的范围。一个管理区(zone)由[struct zone](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L326)结构体来描述,在linux-2.4.37之前的内核中是用[`typedef struct zone_struct zone_t `](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=2.4.37#L47)数据结构来描述) + + + +对于x86_32的机器,管理区(内存区域)类型如下分布 + + +| 类型 | 区域 | +| :------- | ----: | +| ZONE_DMA | 0~15MB | +| ZONE_NORMAL | 16MB~895MB | +| ZONE_HIGHMEM | 896MB~物理内存结束 | + + +内核在初始化内存管理区时, 首先建立管理区表zone_table. 参见[mm/page_alloc.c?v=2.4.37, line 38](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=2.4.37#L38) + + +```cpp +/* + * + * The zone_table array is used to look up the address of the + * struct zone corresponding to a given zone number (ZONE_DMA, + * ZONE_NORMAL, or ZONE_HIGHMEM). + */ +zone_t *zone_table[MAX_NR_ZONES*MAX_NR_NODES]; +EXPORT_SYMBOL(zone_table); +``` + +该表处理起来就像一个多维数组, + +* MAX_NR_ZONES是一个节点中所能包容纳的管理区的最大数, 如3个, 定义在[include/linux/mmzone.h?v=2.4.37, line 25](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=2.4.37#L25), 与zone区域的类型(ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM)定义在一起. 当然这时候我们这些标识都是通过宏的方式来实现的, 而不是如今的枚举类型 + + +* MAX_NR_NODES是可以存在的节点的最大数. + +* 函数EXPORT_SYMBOL使得内核的变量或者函数可以被载入的模块(比如我们的驱动模块)所访问. + + +##2.5 内存页page +------- + +大多数内核(kernel)的操作只使用ZONE_NORMAL区域,系统内存由很多固定大小的内存块组成的,这样的内存块称作为“页”(PAGE), + +x86体系结构中,page的大小为4096个字节。 + +每个物理的页由一个`struct page`的数据结构对象来描述。页的数据结构对象都保存在`mem_map`全局数组中,该数组通常被存放在ZONE_NORMAL的首部,或者就在小内存系统中为装入内核映像而预留的区域之后。从载入内核的低地址内存区域的后面内存区域,也就是ZONE_NORMAL开始的地方的内存的页的数据结构对象,都保存在这个全局数组中。 + + + +##2.6 高端内存 +------- + +由于能够被Linux内核直接访问的ZONE_NORMAL区域的内存空间也是有限的,所以LINUX提出了高端内存(High memory)的概念,并且允许对高端内存的访问 diff --git a/study/kernel/02-memory/01-description/04-page/README.md b/study/kernel/02-memory/01-description/04-page/README.md index 23c0c61..1ea5938 100644 --- a/study/kernel/02-memory/01-description/04-page/README.md +++ b/study/kernel/02-memory/01-description/04-page/README.md @@ -1,695 +1,695 @@ - 服务器体系与共享存储器架构 -======= - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - - - - -#1 前景回顾 -------- - -#1.1 UMA和NUMA两种模型 -------- - -共享存储型多处理机有两种模型 - -* 均匀存储器存取(Uniform-Memory-Access,简称UMA)模型 - - -* 非均匀存储器存取(Nonuniform-Memory-Access,简称NUMA)模型 - -#1.2 (N)UMA模型中linux内存的机构 -------- - -非一致存储器访问(NUMA)模式下 - -* 处理器被划分成多个"节点"(node), 每个节点被分配有的本地存储器空间. 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多 - - -* 内存被分割成多个区域(BANK,也叫"簇"),依据簇与处理器的"距离"不同, 访问不同簇的代码也会不同. - - -##1.3 Linux如何描述物理内存 -------- - -Linux把物理内存划分为三个层次来管理 - -| 层次 | 描述 | -|:----:|:----:| -| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | -| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | -| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | - - -* 首先内存被划分为结点. 内存中的每个节点都是由pg_data_t描述,而pg_data_t由struct pglist_data定义而来, 该数据结构定义在[include/linux/mmzone.h, line 615](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L615), 每个结点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. - -* 接着各个节点又被划分为内存管理区域, 一个管理区域通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. - -* 最后页帧(page frame)代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号. - - -##1.4 今日内容(页帧struct page) -------- - -分页单元可以实现把线性地址转换为物理地址, 为了效率起见, 线性地址被分为固定长度为单位的组, 称为"页", 页内部的线性地址被映射到连续的物理地址. 这样内核可以指定一个页的物理地址和其存储权限, 而不用指定页所包含的全部线性地址的存储权限. -分页单元把所有RAM分为固定长度的页帧(也叫页框, 物理页, 英文page frame). 每一个页帧包含一个页(page). 也就是说一个页帧的长度与一个页的长度一致. 页框是主存的一部分, 因此也是一个存储区域. 简单来说, 页是一个数据块, 可以存放在任何页框(内存中)或者磁盘(被交换至交换分区)中 - -我们今天就来详细讲解一下linux下物理页帧的描述 - -#2 页帧 -------- - -内核把物理页作为内存管理的基本单位. 尽管处理器的最小可寻址单位通常是字, 但是, 内存管理单元MMU通常以页为单位进行处理. 因此,从虚拟内存的上来看,页就是最小单位. - -页帧代表了系统内存的最小单位, 对内存中的每个页都会创建struct page的一个实例. 内核必须要保证page结构体足够的小,否则仅struct page就要占用大量的内存. - -因为即使在中等程序的内存配置下, 系统的内存同样会分解为大量的页. 例如, IA-32系统中标准页长度为4KB, 在内存大小为384MB时, 大约有100000页. 就当今的标准而言, 这个容量算不上很大, 但页的数目已经非常可观了 - -因而出于节省内存的考虑,内核要尽力保持struct page尽可能的小. 在典型的系统中, 由于页的数目巨大, 因此对page结构的小改动, 也可能导致保存所有page实例所需的物理内存暴涨. - -页的广泛使用, 增加了保持结构长度的难度 : 内存管理的许多部分都使用页, 用于各种不同的用途. 内核的一部分可能完全依赖于struct page提供的特定信息, 而这部分信息堆内核的其他部分页可能是完全无用的. 等等. - - -##2.1 struct page结构 -------- - - 内核用[struct page(include/linux/mm_types.h?v=4.7, line 45)](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v4.7#L45)结构表示系统中的每个物理页. - -出于节省内存的考虑,struct page中使用了大量的联合体union. - -```cpp -/* - * Each physical page in the system has a struct page associated with - * it to keep track of whatever it is we are using the page for at the - * moment. Note that we have no way to track which tasks are using - * a page, though if it is a pagecache page, rmap structures can tell us - * who is mapping it. - * - * The objects in struct page are organized in double word blocks in - * order to allows us to use atomic double word operations on portions - * of struct page. That is currently only used by slub but the arrangement - * allows the use of atomic double word operations on the flags/mapping - * and lru list pointers also. - */ -struct page { - /* First double word block */ - unsigned long flags; /* Atomic flags, some possibly updated asynchronously - 描述page的状态和其他信息 */ - union - { - struct address_space *mapping; /* If low bit clear, points to - * inode address_space, or NULL. - * If page mapped as anonymous - * memory, low bit is set, and - * it points to anon_vma object: - * see PAGE_MAPPING_ANON below. - */ - void *s_mem; /* slab first object */ - atomic_t compound_mapcount; /* first tail page */ - /* page_deferred_list().next -- second tail page */ - }; - - /* Second double word */ - struct { - union { - pgoff_t index; /* Our offset within mapping. - 在映射的虚拟空间(vma_area)内的偏移; - 一个文件可能只映射一部分,假设映射了1M的空间, - index指的是在1M空间内的偏移,而不是在整个文件内的偏移。 */ - void *freelist; /* sl[aou]b first free object */ - /* page_deferred_list().prev -- second tail page */ - }; - - union { -#if defined(CONFIG_HAVE_CMPXCHG_DOUBLE) && \ - defined(CONFIG_HAVE_ALIGNED_STRUCT_PAGE) - /* Used for cmpxchg_double in slub */ - unsigned long counters; -#else - /* - * Keep _refcount separate from slub cmpxchg_double - * data. As the rest of the double word is protected by - * slab_lock but _refcount is not. - */ - unsigned counters; -#endif - - struct { - - union { - /* - * Count of ptes mapped in mms, to show - * when page is mapped & limit reverse - * map searches. - * 页映射计数器 - */ - atomic_t _mapcount; - - struct { /* SLUB */ - unsigned inuse:16; - unsigned objects:15; - unsigned frozen:1; - }; - int units; /* SLOB */ - }; - /* - * Usage count, *USE WRAPPER FUNCTION* - * when manual accounting. See page_ref.h - * 页引用计数器 - */ - atomic_t _refcount; - }; - unsigned int active; /* SLAB */ - }; - }; - - /* - * Third double word block - * - * WARNING: bit 0 of the first word encode PageTail(). That means - * the rest users of the storage space MUST NOT use the bit to - * avoid collision and false-positive PageTail(). - */ - union { - struct list_head lru; /* Pageout list, eg. active_list - * protected by zone->lru_lock ! - * Can be used as a generic list - * by the page owner. - */ - struct dev_pagemap *pgmap; /* ZONE_DEVICE pages are never on an - * lru or handled by a slab - * allocator, this points to the - * hosting device page map. - */ - struct { /* slub per cpu partial pages */ - struct page *next; /* Next partial slab */ -#ifdef CONFIG_64BIT - int pages; /* Nr of partial slabs left */ - int pobjects; /* Approximate # of objects */ -#else - short int pages; - short int pobjects; -#endif - }; - - struct rcu_head rcu_head; /* Used by SLAB - * when destroying via RCU - */ - /* Tail pages of compound page */ - struct { - unsigned long compound_head; /* If bit zero is set */ - - /* First tail page only */ -#ifdef CONFIG_64BIT - /* - * On 64 bit system we have enough space in struct page - * to encode compound_dtor and compound_order with - * unsigned int. It can help compiler generate better or - * smaller code on some archtectures. - */ - unsigned int compound_dtor; - unsigned int compound_order; -#else - unsigned short int compound_dtor; - unsigned short int compound_order; -#endif - }; - -#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && USE_SPLIT_PMD_PTLOCKS - struct { - unsigned long __pad; /* do not overlay pmd_huge_pte - * with compound_head to avoid - * possible bit 0 collision. - */ - pgtable_t pmd_huge_pte; /* protected by page->ptl */ - }; -#endif - }; - - /* Remainder is not double word aligned */ - union { - unsigned long private; /* Mapping-private opaque data: - * usually used for buffer_heads - * if PagePrivate set; used for - * swp_entry_t if PageSwapCache; - * indicates order in the buddy - * system if PG_buddy is set. - * 私有数据指针,由应用场景确定其具体的含义 - */ -#if USE_SPLIT_PTE_PTLOCKS -#if ALLOC_SPLIT_PTLOCKS - spinlock_t *ptl; -#else - spinlock_t ptl; -#endif -#endif - struct kmem_cache *slab_cache; /* SL[AU]B: Pointer to slab */ - }; - -#ifdef CONFIG_MEMCG - struct mem_cgroup *mem_cgroup; -#endif - - /* - * On machines where all RAM is mapped into kernel address space, - * we can simply calculate the virtual address. On machines with - * highmem some memory is mapped into kernel virtual memory - * dynamically, so we need a place to store that address. - * Note that this field could be 16 bits on x86 ... ;) - * - * Architectures with slow multiplication can define - * WANT_PAGE_VIRTUAL in asm/page.h - */ -#if defined(WANT_PAGE_VIRTUAL) - void *virtual; /* Kernel virtual address (NULL if - not kmapped, ie. highmem) */ -#endif /* WANT_PAGE_VIRTUAL */ - -#ifdef CONFIG_KMEMCHECK - /* - * kmemcheck wants to track the status of each byte in a page; this - * is a pointer to such a status block. NULL if not tracked. - */ - void *shadow; -#endif - -#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS - int _last_cpupid; -#endif -} -/* - * The struct page can be forced to be double word aligned so that atomic ops - * on double words work. The SLUB allocator can make use of such a feature. - */ -#ifdef CONFIG_HAVE_ALIGNED_STRUCT_PAGE - __aligned(2 * sizeof(unsigned long)) -#endif -; -``` - -| 字段 | 描述 | -|:---:|:----:| -| flag | 用来存放页的状态,每一位代表一种状态,所以至少可以同时表示出32中不同的状态,这些状态定义在linux/page-flags.h中 | -| virtual | 对于如果物理内存可以直接映射内核的系统, 我们可以之间映射出虚拟地址与物理地址的管理, 但是对于需要使用高端内存区域的页, 即无法直接映射到内核的虚拟地址空间, 因此需要用virtual保存该页的虚拟地址 | -| _refcount | 引用计数,表示内核中引用该page的次数, 如果要操作该page, 引用计数会+1, 操作完成-1. 当该值为0时, 表示没有引用该page的位置,所以该page可以被解除映射,这往往在内存回收时是有用的 | -| _mapcount | 被页表映射的次数,也就是说该page同时被多少个进程共享。初始值为-1,如果只被一个进程的页表映射了,该值为0. 如果该page处于伙伴系统中,该值为PAGE_BUDDY_MAPCOUNT_VALUE(-128),内核通过判断该值是否为PAGE_BUDDY_MAPCOUNT_VALUE来确定该page是否属于伙伴系统 | -| index | 在映射的虚拟空间(vma_area)内的偏移;一个文件可能只映射一部分,假设映射了1M的空间,index指的是在1M空间内的偏移,而不是在整个文件内的偏移 | -| private | 私有数据指针,由应用场景确定其具体的含义 | -| lru |链表头,用于在各种链表上维护该页, 以便于按页将不同类别分组, 主要有3个用途: 伙伴算法, slab分配器, 被用户态使用或被当做页缓存使用 | -| mapping | 指向与该页相关的address_space对象 | -| index | 页帧在映射内部的偏移量 - ->注意区分_count和_mapcount,_mapcount表示的是映射次数,而_count表示的是使用次数;被映射了不一定在使用,但要使用必须先映射。 - - - - - -##2.2 mapping & index -------- - - -mapping指定了页帧所在的地址空间, index是页帧在映射内部的偏移量. 地址空间是一个非常一般的概念. 例如, 可以用在向内存读取文件时. 地址空间用于将文件的内容与装载数据的内存区关联起来. mapping不仅能够保存一个指针, 而且还能包含一些额外的信息, 用于判断页是否属于未关联到地址空间的某个匿名内存区. - - -1. 如果mapping = 0,说明该page属于交换高速缓存页(swap cache);当需要使用地址空间时会指定交换分区的地址空间swapper_space。 - -2. 如果mapping != 0,第0位bit[0] = 0,说明该page属于页缓存或文件映射,mapping指向文件的地址空间address_space。 - -3. 如果mapping != 0,第0位bit[0] != 0,说明该page为匿名映射,mapping指向struct anon_vma对象。 - - -通过mapping恢复anon_vma的方法:anon_vma = (struct anon_vma *)(mapping - PAGE_MAPPING_ANON)。 - - - -pgoff_t index是该页描述结构在地址空间radix树page_tree中的对象索引号即页号, 表示该页在vm_file中的偏移页数, 其类型pgoff_t被定义为unsigned long即一个机器字长. - - -```cpp -/* - * The type of an index into the pagecache. - */ -#define pgoff_t unsigned long -``` - -##2.3 private私有数据指针 -------- - -private私有数据指针, 由应用场景确定其具体的含义: - - -1. 如果设置了PG_private标志,则private字段指向struct buffer_head - -2. 如果设置了PG_compound,则指向struct page - - -3. 如果设置了PG_swapcache标志,private存储了该page在交换分区中对应的位置信息swp_entry_t。 - -4. 如果_mapcount = PAGE_BUDDY_MAPCOUNT_VALUE,说明该page位于伙伴系统,private存储该伙伴的阶 - - - -##2.4 lru链表头 -------- - -最近、最久未使用struct slab结构指针变量 - -lru:链表头,主要有3个用途: - -1. 则page处于伙伴系统中时,用于链接相同阶的伙伴(只使用伙伴中的第一个page的lru即可达到目的)。 - -2. 设置PG_slab, 则page属于slab,page->lru.next指向page驻留的的缓存的管理结构,page->lru.prec指向保存该page的slab的管理结构。 - -3. page被用户态使用或被当做页缓存使用时,用于将该page连入zone中相应的lru链表,供内存回收时使用。 - - - - -#3 体系结构无关的页面的状态flags -------- - -页的不同属性通过一系列页标志描述, 存储在struct page的flag成员中的各个比特位. - -```cpp -struct page { - /* First double word block */ - unsigned long flags; /* Atomic flags, - some possibly updated asynchronously, 描述page的状态和其他信息 */ -``` - - - -这些标识是独立于体系结构的, 因而无法通过特定于CPU或计算机的信息(该信息保存在页表中) - - - - -##3.1 页面到管理区和节点的映射 -------- - - -在**早期的linux-2.4.18的内核**中, [struct page存储有一个指向对应管理区的指针page->zone](http://lxr.linux.no/linux-old+v2.4.18/include/linux/mm.h#L167), 但是该这hi真在吼吼被认为是一种浪费, 因为如果有成千上万的这样的struct page存在, 那么即使是很小的指针也会消耗大量的内存空间. - -因此在**后来linux-2.4.x的更新**中, 删除了这个字段, 取而代之的是page->flags的最高[ZONE_SHIFT位](http://lxr.free-electrons.com/source/include/linux/mm.h?v=2.4.37#L340)和NODE_SHIFT位, 存储了其所在zone和node在内存区域表zone_table的编号索引. - - - -那么内核在初始化内存管理区时, 首先建立管理区表zone_table. 参见[mm/page_alloc.c?v=2.4.37, line 38](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=2.4.37#L38) - -```cpp -/* - * - * The zone_table array is used to look up the address of the - * struct zone corresponding to a given zone number (ZONE_DMA, - * ZONE_NORMAL, or ZONE_HIGHMEM). - */ -zone_t *zone_table[MAX_NR_ZONES*MAX_NR_NODES]; -EXPORT_SYMBOL(zone_table); -``` - - -MAX_NR_ZONES是一个节点中所能包容纳的管理区的最大数, 如3个, 定义在[include/linux/mmzone.h?v=2.4.37, line 25](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=2.4.37#L25), 与zone区域的类型(ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM)定义在一起. 当然这时候我们这些标识都是通过宏的方式来实现的, 而不是如今的枚举类型 - - -MAX_NR_NODES是可以存在的节点的最大数. - -函数EXPORT_SYMBOL使得内核的变量或者函数可以被载入的模块(比如我们的驱动模块)所访问. - -该表处理起来就像一个多维数组, 在函数free_area_init_core中, 一个节点的所有页面都会被初始化. - -内核提供了page_zone通过页面查找其对应的内存区域zone_t, 页提供了set_page_zone接口, 而查找到了zone后, 可以通过 其`struct pglist_data *zone_pgdat`直接获取其所在node信息 - -```cpp -/* - * The zone field is never updated after free_area_init_core() - * sets it, so none of the operations on it need to be atomic. - */ -#define NODE_SHIFT 4 -#define ZONE_SHIFT (BITS_PER_LONG - 8) - -struct zone_struct; -extern struct zone_struct *zone_table[]; - -static inline zone_t *page_zone(struct page *page) -{ - return zone_table[page->flags >> ZONE_SHIFT]; -} - -static inline void set_page_zone(struct page *page, unsigned long zone_num) -{ - page->flags &= ~(~0UL << ZONE_SHIFT); - page->flags |= zone_num << ZONE_SHIFT; -} -``` - -而**后来的内核(至今linux-4.7)**中, 这些必要的标识(ZONE_DMA等)都是通过枚举类型实现的(ZONE_DMA等用enum zone_type定义), 然后zone_table也被移除, 参照[[PATCH] zone table removal miss merge](https://lkml.org/lkml/2006/9/27/112) - -因此内核提供了新的思路, 参见[include/linux/mm.h?v4.7, line 907](http://lxr.free-electrons.com/source/include/linux/mm.h?v4.7#L907) - - -```cpp -static inline struct zone *page_zone(const struct page *page) -{ - return &NODE_DATA(page_to_nid(page))->node_zones[page_zonenum(page)]; -} - -static inline void set_page_zone(struct page *page, enum zone_type zone) -{ - page->flags &= ~(ZONES_MASK << ZONES_PGSHIFT); - page->flags |= (zone & ZONES_MASK) << ZONES_PGSHIFT; -} - -static inline void set_page_node(struct page *page, unsigned long node) -{ - page->flags &= ~(NODES_MASK << NODES_PGSHIFT); - page->flags |= (node & NODES_MASK) << NODES_PGSHIFT; -} -``` - - -其中NODE_DATA使用了全局的node表进行索引. - -在UMA结构的机器中, 只有一个node结点即contig_page_data, 此时NODE_DATA直接指向了全局的contig_page_data, 而与node的编号nid无关, 参照[include/linux/mmzone.h?v=4.7, line 858](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L858), 其中全局唯一的cnode结点ontig_page_data定义在[mm/nobootmem.c?v=4.7, line 27](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L27) - - -```cpp -#ifndef CONFIG_NEED_MULTIPLE_NODES -extern struct pglist_data contig_page_data; -#define NODE_DATA(nid) (&contig_page_data) -#define NODE_MEM_MAP(nid) mem_map -else -/* ...... */ -#endif -``` - -而对于NUMA结构的系统中, 所有的node都存储在node_data数组中, -NODE_DATA直接通过node编号索引即可, 参见[NODE_DATA的定义](http://lxr.free-electrons.com/ident?v=4.7;i=NODE_DATA) - -```cpp -extern struct pglist_data *node_data[]; -#define NODE_DATA(nid) (node_data[(nid)]) -``` - - -那么page的flags标识主要分为4部分,其中标志位flag向高位增长, 其余位字段向低位增长,中间存在空闲位 - -| 字段 | 描述 | -|:----:|:---:| -| section | 主要用于稀疏内存模型SPARSEMEM,可忽略 | -| node | NUMA节点号, 标识该page属于哪一个节点 | -| zone | 内存域标志,标识该page属于哪一个zone | -| flag | page的状态标识 | - - -如下图所示 - -![page的flags标识](./images/flags.png) - - -##3.2 内存页标识pageflags -------- - - -其中最后一个flag用于标识page的状态, 这些状态由枚举常量[`enum pageflags`](http://lxr.free-electrons.com/source/include/linux/page-flags.h?v=4.7#L74)定义, 定义在[include/linux/page-flags.h?v=4.7, line 74](http://lxr.free-electrons.com/source/include/linux/page-flags.h?v=4.7#L74). 常用的有如下状态 - - - - - - - -```cpp -enum pageflags { - PG_locked, /* Page is locked. Don't touch. */ - PG_error, - PG_referenced, - PG_uptodate, - PG_dirty, - PG_lru, - PG_active, - PG_slab, - PG_owner_priv_1, /* Owner use. If pagecache, fs may use*/ - PG_arch_1, - PG_reserved, - PG_private, /* If pagecache, has fs-private data */ - PG_private_2, /* If pagecache, has fs aux data */ - PG_writeback, /* Page is under writeback */ - PG_head, /* A head page */ - PG_swapcache, /* Swap page: swp_entry_t in private */ - PG_mappedtodisk, /* Has blocks allocated on-disk */ - PG_reclaim, /* To be reclaimed asap */ - PG_swapbacked, /* Page is backed by RAM/swap */ - PG_unevictable, /* Page is "unevictable" */ -#ifdef CONFIG_MMU - PG_mlocked, /* Page is vma mlocked */ -#endif -#ifdef CONFIG_ARCH_USES_PG_UNCACHED - PG_uncached, /* Page has been mapped as uncached */ -#endif -#ifdef CONFIG_MEMORY_FAILURE - PG_hwpoison, /* hardware poisoned page. Don't touch */ -#endif -#if defined(CONFIG_IDLE_PAGE_TRACKING) && defined(CONFIG_64BIT) - PG_young, - PG_idle, -#endif - __NR_PAGEFLAGS, - - /* Filesystems */ - PG_checked = PG_owner_priv_1, - - /* Two page bits are conscripted by FS-Cache to maintain local caching - * state. These bits are set on pages belonging to the netfs's inodes - * when those inodes are being locally cached. - */ - PG_fscache = PG_private_2, /* page backed by cache */ - - /* XEN */ - /* Pinned in Xen as a read-only pagetable page. */ - PG_pinned = PG_owner_priv_1, - /* Pinned as part of domain save (see xen_mm_pin_all()). */ - PG_savepinned = PG_dirty, - /* Has a grant mapping of another (foreign) domain's page. */ - PG_foreign = PG_owner_priv_1, - - /* SLOB */ - PG_slob_free = PG_private, - - /* Compound pages. Stored in first tail page's flags */ - PG_double_map = PG_private_2, -}; -``` - - -| 页面状态 | 描述 | -|:-------:|:----:| -| PG_locked | 指定了页是否被锁定, 如果该比特未被置位, 说明有使用者正在操作该page, 则内核的其他部分不允许访问该页, 这可以防止内存管理出现竞态条件 | -| PG_error | 如果涉及该page的I/O操作发生了错误, 则该位被设置 | -| PG_referenced | 表示page刚刚被访问过 | -| PG_uptodate | 表示page的数据已经与后备存储器是同步的, 即页的数据已经从块设备读取,且没有出错,数据是最新的 | -| PG_dirty | 与后备存储器中的数据相比,该page的内容已经被修改. 出于性能能的考虑,页并不在每次改变后立即回写, 因此内核需要使用该标识来表明页面中的数据已经改变, 应该在稍后刷出 | -| PG_lru | 表示该page处于LRU链表上, 这有助于实现页面的回收和切换. 内核使用两个最近最少使用(least recently used-LRU)链表来区别活动和不活动页. 如果页在其中一个链表中, 则该位被设置 | -| PG_active | page处于inactive LRU链表, PG_active和PG_referenced一起控制该page的活跃程度,这在内存回收时将会非常有用
当位于LRU active_list链表上的页面该位被设置, 并在页面移除时清除该位, 它标记了页面是否处于活动状态 | -| PG_slab | 该page属于slab分配器 | -| PG_onwer_priv_1 | | -| PG_arch_1 | 直接从代码中引用, PG_arch_1是一个体系结构相关的页面状态位, 一般的代码保证了在第一次禁图页面高速缓存时, 该位被清除. 这使得体系结构可以延迟到页面被某个进程映射后, 才可以D-Cache刷盘 | -| PG_reserved | 设置该标志,防止该page被交换到swap | -| PG_private | 如果page中的private成员非空,则需要设置该标志, 用于I/O的页可使用该字段将页细分为多核缓冲区 | -| PG_private_2 | | -| PG_writeback | page中的数据正在被回写到后备存储器 | -| PG_head | | -| PG_swapcache | 表示该page处于swap cache中 | -| PG_mappedtodisk | 表示page中的数据在后备存储器中有对应 | -| PG_reclaim | 表示该page要被回收。当PFRA决定要回收某个page后,需要设置该标志 | -| PG_swapbacked | 该page的后备存储器是swap | -| PG_unevictable | 该page被锁住,不能交换,并会出现在LRU_UNEVICTABLE链表中,它包括的几种page:ramdisk或ramfs使用的页, shm_locked、mlock锁定的页 | -| PG_mlocked | 该page在vma中被锁定,一般是通过系统调用mlock()锁定了一段内存 | -| PG_uncached | | -| PG_hwpoison | | -| PG_young | | -| PG_idle | | - - - -内核中提供了一些标准宏,用来检查、操作某些特定的比特位,这些宏定义在[include/linux/page-flags.h?v=4.7, line 183](http://lxr.free-electrons.com/source/include/linux/page-flags.h?v=4.7#L183) - - -```c -#define TESTPAGEFLAG(uname, lname, policy) -#define SETPAGEFLAG(uname, lname, policy) -#define CLEARPAGEFLAG(uname, lname, policy) -``` - -**关于page flags的早期实现** - - -* linux-2.6以后的内核中, 很少出现直接用宏定义的标识, 这些标识大多通过enum枚举常量来定义, 然后__NR_XXXX的形式结束, 正好可以标记出宏参数的个数, 但是在早期的实现中, 这些变量都通过宏来标识 - -例如我们的page->flags用enum pageflags来定义, 内存管理区类型通过zone_type来定义, 但是这些内容在早期的内核中都是通过宏定义来实现的. - -* 其次标识的函数接口也变了, 早期的内核中, 针对每个宏标识都设置了一组test/set/clear, 参见[/include/linux/mm.h?v=2.4.37, line 324](http://lxr.free-electrons.com/source/include/linux/mm.h?v=2.4.37#L324) - -形式如下 - -```cpp -PageXXX(page):检查page是否设置了PG_XXX位 -SetPageXXX(page):设置page的PG_XXX位 -ClearPageXXX(page):清除page的PG_XXX位 -TestSetPageXXX(page):设置page的PG_XXX位,并返回原值 -TestClearPageXXX(page):清除page的PG_XXX位,并返回原值 -``` - - -很多情况下, 需要等待页的状态改变, 然后才能恢复工作. 因此内核提供了两个辅助函数 - -```cpp -http://lxr.free-electrons.com/source/include/linux/pagemap.h?v=4.7#L495 -/* - * Wait for a page to be unlocked. - * - * This must be called with the caller "holding" the page, - * ie with increased "page->count" so that the page won't - * go away during the wait.. - */ -static inline void wait_on_page_locked(struct page *page) - -// http://lxr.free-electrons.com/source/include/linux/pagemap.h?v=4.7#L504 -/* - * Wait for a page to complete writeback - */ -static inline void wait_on_page_writeback(struct page *page) -``` - -假定内核的一部分在等待一个被锁定的页面, 直至页面被解锁. wait_on_page_locked提供了该功能. 在页面被锁定的情况下, 调用该函数, 内核将进入睡眠. 而在页面解锁后, 睡眠进程会被自动唤醒并继续工作 - -wait_on_page_writeback的工作方式类似, 该函数会等待与页面相关的所有待决回写操作结束, 将页面包含的数据同步到块设备为止. - - - -#4 全局页面数组mem_map -------- - -`mem_map`是一个struct page的数组,管理着系统中所有的物理内存页面。在系统启动的过程中,创建和分配mem_map的内存区域, mem_map定义在[mm/page_alloc.c?v=4.7, line 6691](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6691) - - - -```cpp -#ifndef CONFIG_NEED_MULTIPLE_NODES -/* use the per-pgdat data instead for discontigmem - mbligh */ -unsigned long max_mapnr; -struct page *mem_map; - -EXPORT_SYMBOL(max_mapnr); -EXPORT_SYMBOL(mem_map); -#endif -``` - - -UMA体系结构中,free_area_init函数在系统唯一的struct node对象contig_page_data中node_mem_map成员赋值给全局的mem_map变量 - - - + 服务器体系与共享存储器架构 +======= + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + + + + +#1 前景回顾 +------- + +#1.1 UMA和NUMA两种模型 +------- + +共享存储型多处理机有两种模型 + +* 均匀存储器存取(Uniform-Memory-Access,简称UMA)模型 + + +* 非均匀存储器存取(Nonuniform-Memory-Access,简称NUMA)模型 + +#1.2 (N)UMA模型中linux内存的机构 +------- + +非一致存储器访问(NUMA)模式下 + +* 处理器被划分成多个"节点"(node), 每个节点被分配有的本地存储器空间. 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多 + + +* 内存被分割成多个区域(BANK,也叫"簇"),依据簇与处理器的"距离"不同, 访问不同簇的代码也会不同. + + +##1.3 Linux如何描述物理内存 +------- + +Linux把物理内存划分为三个层次来管理 + +| 层次 | 描述 | +|:----:|:----:| +| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | +| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | +| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | + + +* 首先内存被划分为结点. 内存中的每个节点都是由pg_data_t描述,而pg_data_t由struct pglist_data定义而来, 该数据结构定义在[include/linux/mmzone.h, line 615](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L615), 每个结点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. + +* 接着各个节点又被划分为内存管理区域, 一个管理区域通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. + +* 最后页帧(page frame)代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号. + + +##1.4 今日内容(页帧struct page) +------- + +分页单元可以实现把线性地址转换为物理地址, 为了效率起见, 线性地址被分为固定长度为单位的组, 称为"页", 页内部的线性地址被映射到连续的物理地址. 这样内核可以指定一个页的物理地址和其存储权限, 而不用指定页所包含的全部线性地址的存储权限. +分页单元把所有RAM分为固定长度的页帧(也叫页框, 物理页, 英文page frame). 每一个页帧包含一个页(page). 也就是说一个页帧的长度与一个页的长度一致. 页框是主存的一部分, 因此也是一个存储区域. 简单来说, 页是一个数据块, 可以存放在任何页框(内存中)或者磁盘(被交换至交换分区)中 + +我们今天就来详细讲解一下linux下物理页帧的描述 + +#2 页帧 +------- + +内核把物理页作为内存管理的基本单位. 尽管处理器的最小可寻址单位通常是字, 但是, 内存管理单元MMU通常以页为单位进行处理. 因此,从虚拟内存的上来看,页就是最小单位. + +页帧代表了系统内存的最小单位, 对内存中的每个页都会创建struct page的一个实例. 内核必须要保证page结构体足够的小,否则仅struct page就要占用大量的内存. + +因为即使在中等程序的内存配置下, 系统的内存同样会分解为大量的页. 例如, IA-32系统中标准页长度为4KB, 在内存大小为384MB时, 大约有100000页. 就当今的标准而言, 这个容量算不上很大, 但页的数目已经非常可观了 + +因而出于节省内存的考虑,内核要尽力保持struct page尽可能的小. 在典型的系统中, 由于页的数目巨大, 因此对page结构的小改动, 也可能导致保存所有page实例所需的物理内存暴涨. + +页的广泛使用, 增加了保持结构长度的难度 : 内存管理的许多部分都使用页, 用于各种不同的用途. 内核的一部分可能完全依赖于struct page提供的特定信息, 而这部分信息堆内核的其他部分页可能是完全无用的. 等等. + + +##2.1 struct page结构 +------- + + 内核用[struct page(include/linux/mm_types.h?v=4.7, line 45)](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v4.7#L45)结构表示系统中的每个物理页. + +出于节省内存的考虑,struct page中使用了大量的联合体union. + +```cpp +/* + * Each physical page in the system has a struct page associated with + * it to keep track of whatever it is we are using the page for at the + * moment. Note that we have no way to track which tasks are using + * a page, though if it is a pagecache page, rmap structures can tell us + * who is mapping it. + * + * The objects in struct page are organized in double word blocks in + * order to allows us to use atomic double word operations on portions + * of struct page. That is currently only used by slub but the arrangement + * allows the use of atomic double word operations on the flags/mapping + * and lru list pointers also. + */ +struct page { + /* First double word block */ + unsigned long flags; /* Atomic flags, some possibly updated asynchronously + 描述page的状态和其他信息 */ + union + { + struct address_space *mapping; /* If low bit clear, points to + * inode address_space, or NULL. + * If page mapped as anonymous + * memory, low bit is set, and + * it points to anon_vma object: + * see PAGE_MAPPING_ANON below. + */ + void *s_mem; /* slab first object */ + atomic_t compound_mapcount; /* first tail page */ + /* page_deferred_list().next -- second tail page */ + }; + + /* Second double word */ + struct { + union { + pgoff_t index; /* Our offset within mapping. + 在映射的虚拟空间(vma_area)内的偏移; + 一个文件可能只映射一部分,假设映射了1M的空间, + index指的是在1M空间内的偏移,而不是在整个文件内的偏移。 */ + void *freelist; /* sl[aou]b first free object */ + /* page_deferred_list().prev -- second tail page */ + }; + + union { +#if defined(CONFIG_HAVE_CMPXCHG_DOUBLE) && \ + defined(CONFIG_HAVE_ALIGNED_STRUCT_PAGE) + /* Used for cmpxchg_double in slub */ + unsigned long counters; +#else + /* + * Keep _refcount separate from slub cmpxchg_double + * data. As the rest of the double word is protected by + * slab_lock but _refcount is not. + */ + unsigned counters; +#endif + + struct { + + union { + /* + * Count of ptes mapped in mms, to show + * when page is mapped & limit reverse + * map searches. + * 页映射计数器 + */ + atomic_t _mapcount; + + struct { /* SLUB */ + unsigned inuse:16; + unsigned objects:15; + unsigned frozen:1; + }; + int units; /* SLOB */ + }; + /* + * Usage count, *USE WRAPPER FUNCTION* + * when manual accounting. See page_ref.h + * 页引用计数器 + */ + atomic_t _refcount; + }; + unsigned int active; /* SLAB */ + }; + }; + + /* + * Third double word block + * + * WARNING: bit 0 of the first word encode PageTail(). That means + * the rest users of the storage space MUST NOT use the bit to + * avoid collision and false-positive PageTail(). + */ + union { + struct list_head lru; /* Pageout list, eg. active_list + * protected by zone->lru_lock ! + * Can be used as a generic list + * by the page owner. + */ + struct dev_pagemap *pgmap; /* ZONE_DEVICE pages are never on an + * lru or handled by a slab + * allocator, this points to the + * hosting device page map. + */ + struct { /* slub per cpu partial pages */ + struct page *next; /* Next partial slab */ +#ifdef CONFIG_64BIT + int pages; /* Nr of partial slabs left */ + int pobjects; /* Approximate # of objects */ +#else + short int pages; + short int pobjects; +#endif + }; + + struct rcu_head rcu_head; /* Used by SLAB + * when destroying via RCU + */ + /* Tail pages of compound page */ + struct { + unsigned long compound_head; /* If bit zero is set */ + + /* First tail page only */ +#ifdef CONFIG_64BIT + /* + * On 64 bit system we have enough space in struct page + * to encode compound_dtor and compound_order with + * unsigned int. It can help compiler generate better or + * smaller code on some archtectures. + */ + unsigned int compound_dtor; + unsigned int compound_order; +#else + unsigned short int compound_dtor; + unsigned short int compound_order; +#endif + }; + +#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && USE_SPLIT_PMD_PTLOCKS + struct { + unsigned long __pad; /* do not overlay pmd_huge_pte + * with compound_head to avoid + * possible bit 0 collision. + */ + pgtable_t pmd_huge_pte; /* protected by page->ptl */ + }; +#endif + }; + + /* Remainder is not double word aligned */ + union { + unsigned long private; /* Mapping-private opaque data: + * usually used for buffer_heads + * if PagePrivate set; used for + * swp_entry_t if PageSwapCache; + * indicates order in the buddy + * system if PG_buddy is set. + * 私有数据指针,由应用场景确定其具体的含义 + */ +#if USE_SPLIT_PTE_PTLOCKS +#if ALLOC_SPLIT_PTLOCKS + spinlock_t *ptl; +#else + spinlock_t ptl; +#endif +#endif + struct kmem_cache *slab_cache; /* SL[AU]B: Pointer to slab */ + }; + +#ifdef CONFIG_MEMCG + struct mem_cgroup *mem_cgroup; +#endif + + /* + * On machines where all RAM is mapped into kernel address space, + * we can simply calculate the virtual address. On machines with + * highmem some memory is mapped into kernel virtual memory + * dynamically, so we need a place to store that address. + * Note that this field could be 16 bits on x86 ... ;) + * + * Architectures with slow multiplication can define + * WANT_PAGE_VIRTUAL in asm/page.h + */ +#if defined(WANT_PAGE_VIRTUAL) + void *virtual; /* Kernel virtual address (NULL if + not kmapped, ie. highmem) */ +#endif /* WANT_PAGE_VIRTUAL */ + +#ifdef CONFIG_KMEMCHECK + /* + * kmemcheck wants to track the status of each byte in a page; this + * is a pointer to such a status block. NULL if not tracked. + */ + void *shadow; +#endif + +#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS + int _last_cpupid; +#endif +} +/* + * The struct page can be forced to be double word aligned so that atomic ops + * on double words work. The SLUB allocator can make use of such a feature. + */ +#ifdef CONFIG_HAVE_ALIGNED_STRUCT_PAGE + __aligned(2 * sizeof(unsigned long)) +#endif +; +``` + +| 字段 | 描述 | +|:---:|:----:| +| flag | 用来存放页的状态,每一位代表一种状态,所以至少可以同时表示出32中不同的状态,这些状态定义在linux/page-flags.h中 | +| virtual | 对于如果物理内存可以直接映射内核的系统, 我们可以之间映射出虚拟地址与物理地址的管理, 但是对于需要使用高端内存区域的页, 即无法直接映射到内核的虚拟地址空间, 因此需要用virtual保存该页的虚拟地址 | +| _refcount | 引用计数,表示内核中引用该page的次数, 如果要操作该page, 引用计数会+1, 操作完成-1. 当该值为0时, 表示没有引用该page的位置,所以该page可以被解除映射,这往往在内存回收时是有用的 | +| _mapcount | 被页表映射的次数,也就是说该page同时被多少个进程共享。初始值为-1,如果只被一个进程的页表映射了,该值为0. 如果该page处于伙伴系统中,该值为PAGE_BUDDY_MAPCOUNT_VALUE(-128),内核通过判断该值是否为PAGE_BUDDY_MAPCOUNT_VALUE来确定该page是否属于伙伴系统 | +| index | 在映射的虚拟空间(vma_area)内的偏移;一个文件可能只映射一部分,假设映射了1M的空间,index指的是在1M空间内的偏移,而不是在整个文件内的偏移 | +| private | 私有数据指针,由应用场景确定其具体的含义 | +| lru |链表头,用于在各种链表上维护该页, 以便于按页将不同类别分组, 主要有3个用途: 伙伴算法, slab分配器, 被用户态使用或被当做页缓存使用 | +| mapping | 指向与该页相关的address_space对象 | +| index | 页帧在映射内部的偏移量 + +>注意区分_count和_mapcount,_mapcount表示的是映射次数,而_count表示的是使用次数;被映射了不一定在使用,但要使用必须先映射。 + + + + + +##2.2 mapping & index +------- + + +mapping指定了页帧所在的地址空间, index是页帧在映射内部的偏移量. 地址空间是一个非常一般的概念. 例如, 可以用在向内存读取文件时. 地址空间用于将文件的内容与装载数据的内存区关联起来. mapping不仅能够保存一个指针, 而且还能包含一些额外的信息, 用于判断页是否属于未关联到地址空间的某个匿名内存区. + + +1. 如果mapping = 0,说明该page属于交换高速缓存页(swap cache);当需要使用地址空间时会指定交换分区的地址空间swapper_space。 + +2. 如果mapping != 0,第0位bit[0] = 0,说明该page属于页缓存或文件映射,mapping指向文件的地址空间address_space。 + +3. 如果mapping != 0,第0位bit[0] != 0,说明该page为匿名映射,mapping指向struct anon_vma对象。 + + +通过mapping恢复anon_vma的方法:anon_vma = (struct anon_vma *)(mapping - PAGE_MAPPING_ANON)。 + + + +pgoff_t index是该页描述结构在地址空间radix树page_tree中的对象索引号即页号, 表示该页在vm_file中的偏移页数, 其类型pgoff_t被定义为unsigned long即一个机器字长. + + +```cpp +/* + * The type of an index into the pagecache. + */ +#define pgoff_t unsigned long +``` + +##2.3 private私有数据指针 +------- + +private私有数据指针, 由应用场景确定其具体的含义: + + +1. 如果设置了PG_private标志,则private字段指向struct buffer_head + +2. 如果设置了PG_compound,则指向struct page + + +3. 如果设置了PG_swapcache标志,private存储了该page在交换分区中对应的位置信息swp_entry_t。 + +4. 如果_mapcount = PAGE_BUDDY_MAPCOUNT_VALUE,说明该page位于伙伴系统,private存储该伙伴的阶 + + + +##2.4 lru链表头 +------- + +最近、最久未使用struct slab结构指针变量 + +lru:链表头,主要有3个用途: + +1. 则page处于伙伴系统中时,用于链接相同阶的伙伴(只使用伙伴中的第一个page的lru即可达到目的)。 + +2. 设置PG_slab, 则page属于slab,page->lru.next指向page驻留的的缓存的管理结构,page->lru.prec指向保存该page的slab的管理结构。 + +3. page被用户态使用或被当做页缓存使用时,用于将该page连入zone中相应的lru链表,供内存回收时使用。 + + + + +#3 体系结构无关的页面的状态flags +------- + +页的不同属性通过一系列页标志描述, 存储在struct page的flag成员中的各个比特位. + +```cpp +struct page { + /* First double word block */ + unsigned long flags; /* Atomic flags, + some possibly updated asynchronously, 描述page的状态和其他信息 */ +``` + + + +这些标识是独立于体系结构的, 因而无法通过特定于CPU或计算机的信息(该信息保存在页表中) + + + + +##3.1 页面到管理区和节点的映射 +------- + + +在**早期的linux-2.4.18的内核**中, [struct page存储有一个指向对应管理区的指针page->zone](http://lxr.linux.no/linux-old+v2.4.18/include/linux/mm.h#L167), 但是该这hi真在吼吼被认为是一种浪费, 因为如果有成千上万的这样的struct page存在, 那么即使是很小的指针也会消耗大量的内存空间. + +因此在**后来linux-2.4.x的更新**中, 删除了这个字段, 取而代之的是page->flags的最高[ZONE_SHIFT位](http://lxr.free-electrons.com/source/include/linux/mm.h?v=2.4.37#L340)和NODE_SHIFT位, 存储了其所在zone和node在内存区域表zone_table的编号索引. + + + +那么内核在初始化内存管理区时, 首先建立管理区表zone_table. 参见[mm/page_alloc.c?v=2.4.37, line 38](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=2.4.37#L38) + +```cpp +/* + * + * The zone_table array is used to look up the address of the + * struct zone corresponding to a given zone number (ZONE_DMA, + * ZONE_NORMAL, or ZONE_HIGHMEM). + */ +zone_t *zone_table[MAX_NR_ZONES*MAX_NR_NODES]; +EXPORT_SYMBOL(zone_table); +``` + + +MAX_NR_ZONES是一个节点中所能包容纳的管理区的最大数, 如3个, 定义在[include/linux/mmzone.h?v=2.4.37, line 25](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=2.4.37#L25), 与zone区域的类型(ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM)定义在一起. 当然这时候我们这些标识都是通过宏的方式来实现的, 而不是如今的枚举类型 + + +MAX_NR_NODES是可以存在的节点的最大数. + +函数EXPORT_SYMBOL使得内核的变量或者函数可以被载入的模块(比如我们的驱动模块)所访问. + +该表处理起来就像一个多维数组, 在函数free_area_init_core中, 一个节点的所有页面都会被初始化. + +内核提供了page_zone通过页面查找其对应的内存区域zone_t, 页提供了set_page_zone接口, 而查找到了zone后, 可以通过 其`struct pglist_data *zone_pgdat`直接获取其所在node信息 + +```cpp +/* + * The zone field is never updated after free_area_init_core() + * sets it, so none of the operations on it need to be atomic. + */ +#define NODE_SHIFT 4 +#define ZONE_SHIFT (BITS_PER_LONG - 8) + +struct zone_struct; +extern struct zone_struct *zone_table[]; + +static inline zone_t *page_zone(struct page *page) +{ + return zone_table[page->flags >> ZONE_SHIFT]; +} + +static inline void set_page_zone(struct page *page, unsigned long zone_num) +{ + page->flags &= ~(~0UL << ZONE_SHIFT); + page->flags |= zone_num << ZONE_SHIFT; +} +``` + +而**后来的内核(至今linux-4.7)**中, 这些必要的标识(ZONE_DMA等)都是通过枚举类型实现的(ZONE_DMA等用enum zone_type定义), 然后zone_table也被移除, 参照[[PATCH] zone table removal miss merge](https://lkml.org/lkml/2006/9/27/112) + +因此内核提供了新的思路, 参见[include/linux/mm.h?v4.7, line 907](http://lxr.free-electrons.com/source/include/linux/mm.h?v4.7#L907) + + +```cpp +static inline struct zone *page_zone(const struct page *page) +{ + return &NODE_DATA(page_to_nid(page))->node_zones[page_zonenum(page)]; +} + +static inline void set_page_zone(struct page *page, enum zone_type zone) +{ + page->flags &= ~(ZONES_MASK << ZONES_PGSHIFT); + page->flags |= (zone & ZONES_MASK) << ZONES_PGSHIFT; +} + +static inline void set_page_node(struct page *page, unsigned long node) +{ + page->flags &= ~(NODES_MASK << NODES_PGSHIFT); + page->flags |= (node & NODES_MASK) << NODES_PGSHIFT; +} +``` + + +其中NODE_DATA使用了全局的node表进行索引. + +在UMA结构的机器中, 只有一个node结点即contig_page_data, 此时NODE_DATA直接指向了全局的contig_page_data, 而与node的编号nid无关, 参照[include/linux/mmzone.h?v=4.7, line 858](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L858), 其中全局唯一的cnode结点ontig_page_data定义在[mm/nobootmem.c?v=4.7, line 27](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L27) + + +```cpp +#ifndef CONFIG_NEED_MULTIPLE_NODES +extern struct pglist_data contig_page_data; +#define NODE_DATA(nid) (&contig_page_data) +#define NODE_MEM_MAP(nid) mem_map +else +/* ...... */ +#endif +``` + +而对于NUMA结构的系统中, 所有的node都存储在node_data数组中, +NODE_DATA直接通过node编号索引即可, 参见[NODE_DATA的定义](http://lxr.free-electrons.com/ident?v=4.7;i=NODE_DATA) + +```cpp +extern struct pglist_data *node_data[]; +#define NODE_DATA(nid) (node_data[(nid)]) +``` + + +那么page的flags标识主要分为4部分,其中标志位flag向高位增长, 其余位字段向低位增长,中间存在空闲位 + +| 字段 | 描述 | +|:----:|:---:| +| section | 主要用于稀疏内存模型SPARSEMEM,可忽略 | +| node | NUMA节点号, 标识该page属于哪一个节点 | +| zone | 内存域标志,标识该page属于哪一个zone | +| flag | page的状态标识 | + + +如下图所示 + +![page的flags标识](./images/flags.png) + + +##3.2 内存页标识pageflags +------- + + +其中最后一个flag用于标识page的状态, 这些状态由枚举常量[`enum pageflags`](http://lxr.free-electrons.com/source/include/linux/page-flags.h?v=4.7#L74)定义, 定义在[include/linux/page-flags.h?v=4.7, line 74](http://lxr.free-electrons.com/source/include/linux/page-flags.h?v=4.7#L74). 常用的有如下状态 + + + + + + + +```cpp +enum pageflags { + PG_locked, /* Page is locked. Don't touch. */ + PG_error, + PG_referenced, + PG_uptodate, + PG_dirty, + PG_lru, + PG_active, + PG_slab, + PG_owner_priv_1, /* Owner use. If pagecache, fs may use*/ + PG_arch_1, + PG_reserved, + PG_private, /* If pagecache, has fs-private data */ + PG_private_2, /* If pagecache, has fs aux data */ + PG_writeback, /* Page is under writeback */ + PG_head, /* A head page */ + PG_swapcache, /* Swap page: swp_entry_t in private */ + PG_mappedtodisk, /* Has blocks allocated on-disk */ + PG_reclaim, /* To be reclaimed asap */ + PG_swapbacked, /* Page is backed by RAM/swap */ + PG_unevictable, /* Page is "unevictable" */ +#ifdef CONFIG_MMU + PG_mlocked, /* Page is vma mlocked */ +#endif +#ifdef CONFIG_ARCH_USES_PG_UNCACHED + PG_uncached, /* Page has been mapped as uncached */ +#endif +#ifdef CONFIG_MEMORY_FAILURE + PG_hwpoison, /* hardware poisoned page. Don't touch */ +#endif +#if defined(CONFIG_IDLE_PAGE_TRACKING) && defined(CONFIG_64BIT) + PG_young, + PG_idle, +#endif + __NR_PAGEFLAGS, + + /* Filesystems */ + PG_checked = PG_owner_priv_1, + + /* Two page bits are conscripted by FS-Cache to maintain local caching + * state. These bits are set on pages belonging to the netfs's inodes + * when those inodes are being locally cached. + */ + PG_fscache = PG_private_2, /* page backed by cache */ + + /* XEN */ + /* Pinned in Xen as a read-only pagetable page. */ + PG_pinned = PG_owner_priv_1, + /* Pinned as part of domain save (see xen_mm_pin_all()). */ + PG_savepinned = PG_dirty, + /* Has a grant mapping of another (foreign) domain's page. */ + PG_foreign = PG_owner_priv_1, + + /* SLOB */ + PG_slob_free = PG_private, + + /* Compound pages. Stored in first tail page's flags */ + PG_double_map = PG_private_2, +}; +``` + + +| 页面状态 | 描述 | +|:-------:|:----:| +| PG_locked | 指定了页是否被锁定, 如果该比特未被置位, 说明有使用者正在操作该page, 则内核的其他部分不允许访问该页, 这可以防止内存管理出现竞态条件 | +| PG_error | 如果涉及该page的I/O操作发生了错误, 则该位被设置 | +| PG_referenced | 表示page刚刚被访问过 | +| PG_uptodate | 表示page的数据已经与后备存储器是同步的, 即页的数据已经从块设备读取,且没有出错,数据是最新的 | +| PG_dirty | 与后备存储器中的数据相比,该page的内容已经被修改. 出于性能能的考虑,页并不在每次改变后立即回写, 因此内核需要使用该标识来表明页面中的数据已经改变, 应该在稍后刷出 | +| PG_lru | 表示该page处于LRU链表上, 这有助于实现页面的回收和切换. 内核使用两个最近最少使用(least recently used-LRU)链表来区别活动和不活动页. 如果页在其中一个链表中, 则该位被设置 | +| PG_active | page处于inactive LRU链表, PG_active和PG_referenced一起控制该page的活跃程度,这在内存回收时将会非常有用
当位于LRU active_list链表上的页面该位被设置, 并在页面移除时清除该位, 它标记了页面是否处于活动状态 | +| PG_slab | 该page属于slab分配器 | +| PG_onwer_priv_1 | | +| PG_arch_1 | 直接从代码中引用, PG_arch_1是一个体系结构相关的页面状态位, 一般的代码保证了在第一次禁图页面高速缓存时, 该位被清除. 这使得体系结构可以延迟到页面被某个进程映射后, 才可以D-Cache刷盘 | +| PG_reserved | 设置该标志,防止该page被交换到swap | +| PG_private | 如果page中的private成员非空,则需要设置该标志, 用于I/O的页可使用该字段将页细分为多核缓冲区 | +| PG_private_2 | | +| PG_writeback | page中的数据正在被回写到后备存储器 | +| PG_head | | +| PG_swapcache | 表示该page处于swap cache中 | +| PG_mappedtodisk | 表示page中的数据在后备存储器中有对应 | +| PG_reclaim | 表示该page要被回收。当PFRA决定要回收某个page后,需要设置该标志 | +| PG_swapbacked | 该page的后备存储器是swap | +| PG_unevictable | 该page被锁住,不能交换,并会出现在LRU_UNEVICTABLE链表中,它包括的几种page:ramdisk或ramfs使用的页, shm_locked、mlock锁定的页 | +| PG_mlocked | 该page在vma中被锁定,一般是通过系统调用mlock()锁定了一段内存 | +| PG_uncached | | +| PG_hwpoison | | +| PG_young | | +| PG_idle | | + + + +内核中提供了一些标准宏,用来检查、操作某些特定的比特位,这些宏定义在[include/linux/page-flags.h?v=4.7, line 183](http://lxr.free-electrons.com/source/include/linux/page-flags.h?v=4.7#L183) + + +```c +#define TESTPAGEFLAG(uname, lname, policy) +#define SETPAGEFLAG(uname, lname, policy) +#define CLEARPAGEFLAG(uname, lname, policy) +``` + +**关于page flags的早期实现** + + +* linux-2.6以后的内核中, 很少出现直接用宏定义的标识, 这些标识大多通过enum枚举常量来定义, 然后__NR_XXXX的形式结束, 正好可以标记出宏参数的个数, 但是在早期的实现中, 这些变量都通过宏来标识 + +例如我们的page->flags用enum pageflags来定义, 内存管理区类型通过zone_type来定义, 但是这些内容在早期的内核中都是通过宏定义来实现的. + +* 其次标识的函数接口也变了, 早期的内核中, 针对每个宏标识都设置了一组test/set/clear, 参见[/include/linux/mm.h?v=2.4.37, line 324](http://lxr.free-electrons.com/source/include/linux/mm.h?v=2.4.37#L324) + +形式如下 + +```cpp +PageXXX(page):检查page是否设置了PG_XXX位 +SetPageXXX(page):设置page的PG_XXX位 +ClearPageXXX(page):清除page的PG_XXX位 +TestSetPageXXX(page):设置page的PG_XXX位,并返回原值 +TestClearPageXXX(page):清除page的PG_XXX位,并返回原值 +``` + + +很多情况下, 需要等待页的状态改变, 然后才能恢复工作. 因此内核提供了两个辅助函数 + +```cpp +http://lxr.free-electrons.com/source/include/linux/pagemap.h?v=4.7#L495 +/* + * Wait for a page to be unlocked. + * + * This must be called with the caller "holding" the page, + * ie with increased "page->count" so that the page won't + * go away during the wait.. + */ +static inline void wait_on_page_locked(struct page *page) + +// http://lxr.free-electrons.com/source/include/linux/pagemap.h?v=4.7#L504 +/* + * Wait for a page to complete writeback + */ +static inline void wait_on_page_writeback(struct page *page) +``` + +假定内核的一部分在等待一个被锁定的页面, 直至页面被解锁. wait_on_page_locked提供了该功能. 在页面被锁定的情况下, 调用该函数, 内核将进入睡眠. 而在页面解锁后, 睡眠进程会被自动唤醒并继续工作 + +wait_on_page_writeback的工作方式类似, 该函数会等待与页面相关的所有待决回写操作结束, 将页面包含的数据同步到块设备为止. + + + +#4 全局页面数组mem_map +------- + +`mem_map`是一个struct page的数组,管理着系统中所有的物理内存页面。在系统启动的过程中,创建和分配mem_map的内存区域, mem_map定义在[mm/page_alloc.c?v=4.7, line 6691](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6691) + + + +```cpp +#ifndef CONFIG_NEED_MULTIPLE_NODES +/* use the per-pgdat data instead for discontigmem - mbligh */ +unsigned long max_mapnr; +struct page *mem_map; + +EXPORT_SYMBOL(max_mapnr); +EXPORT_SYMBOL(mem_map); +#endif +``` + + +UMA体系结构中,free_area_init函数在系统唯一的struct node对象contig_page_data中node_mem_map成员赋值给全局的mem_map变量 + + + diff --git a/study/kernel/02-memory/03-initialize/01-initialize/README.md b/study/kernel/02-memory/03-initialize/01-initialize/README.md index 7216bef..043c2e4 100644 --- a/study/kernel/02-memory/03-initialize/01-initialize/README.md +++ b/study/kernel/02-memory/03-initialize/01-initialize/README.md @@ -1,674 +1,674 @@ -启动过程期间的内存管理--bootmem分配器 -======= - - - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - - - -在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后内核才能检测到可用内存和寄存器. - - -而我们今天要讲的boot阶段就是系统初始化阶段使用的内存分配器. - - - - -#1 前景回顾 -------- - - -##1.1 Linux内存管理的层次结构 -------- - - -Linux把物理内存划分为三个层次来管理 - -| 层次 | 描述 | -|:----:|:----:| -| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | -| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | -| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | - -为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 - - -* 首先, 内存被划分为**结点**. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. - -* 接着各个节点又被划分为内存管理区域, 一个**管理区域**通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. - - -* 最后**页帧(page frame)**代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号. - - -##1.2 内存结点pg_data_t -------- - - -在LINUX中引入一个数据结构`struct pglist_data` ,来描述一个node,定义在[`include/linux/mmzone.h`](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L630) 文件中。(这个结构被typedef pg_data_t)。 - - -* 对于NUMA系统来讲, 整个系统的内存由一个[node_data](http://lxr.free-electrons.com/source/arch/s390/numa/numa.c?v=4.7#L23)的pg_data_t指针数组来管理 - -* 对于PC这样的UMA系统,使用struct pglist_data contig_page_data ,作为系统唯一的node管理所有的内存区域。(UMA系统中中只有一个node) - -可以使用NODE_DATA(node_id)来查找系统中编号为node_id的结点, 而UMA结构下由于只有一个结点, 因此该宏总是返回全局的contig_page_data, 而与参数node_id无关. - -**NODE_DATA(node_id)查找编号node_id的结点pg_data_t信息** 参见[NODE_DATA的定义](http://lxr.free-electrons.com/ident?v=4.7;i=NODE_DATA) - -```cpp -extern struct pglist_data *node_data[]; -#define NODE_DATA(nid) (node_data[(nid)]) -``` - - -在UMA结构的机器中, 只有一个node结点即contig_page_data, 此时NODE_DATA直接指向了全局的contig_page_data, 而与node的编号nid无关, 参照[include/linux/mmzone.h?v=4.7, line 858](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L858) - - -```cpp -extern struct pglist_data contig_page_data; -#define NODE_DATA(nid) (&contig_page_data) - -``` - -##1.2 物理内存区域 -------- - -因为实际的计算机体系结构有硬件的诸多限制, 这限制了页框可以使用的方式. 尤其是, Linux内核必须处理80x86体系结构的两种硬件约束. - -* ISA总线的直接内存存储DMA处理器有一个严格的限制 : 他们只能对RAM的前16MB进行寻址 - -* 在具有大容量RAM的现代32位计算机中, CPU不能直接访问所有的物理地址, 因为线性地址空间太小, 内核不可能直接映射所有物理内存到线性地址空间, 我们会在后面典型架构(x86)上内存区域划分详细讲解x86_32上的内存区域划分 - - -因此Linux内核对不同区域的内存需要采用不同的管理方式和映射方式, 因此内核将物理地址或者成用zone_t表示的不同地址区域 - -对于x86_32的机器,管理区(内存区域)类型如下分布 - - -| 类型 | 区域 | -| :------- | ----: | -| ZONE_DMA | 0~15MB | -| ZONE_NORMAL | 16MB~895MB | -| ZONE_HIGHMEM | 896MB~物理内存结束 | - - -##1.3 物理页帧 -------- - -内核把物理页作为内存管理的基本单位. 尽管处理器的最小可寻址单位通常是字, 但是, 内存管理单元MMU通常以页为单位进行处理. 因此,从虚拟内存的上来看,页就是最小单位. - -页帧代表了系统内存的最小单位, 对内存中的每个页都会创建struct page的一个实例. 内核必须要保证page结构体足够的小,否则仅struct page就要占用大量的内存. - - - 内核用[struct page(include/linux/mm_types.h?v=4.7, line 45)](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v4.7#L45)结构表示系统中的每个物理页. - -出于节省内存的考虑,struct page中使用了大量的联合体union. - - -`mem_map`是一个struct page的数组,管理着系统中所有的物理内存页面。在系统启动的过程中,创建和分配mem_map的内存区域, mem_map定义在[mm/page_alloc.c?v=4.7, line 6691](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6691) - - -UMA体系结构中,free_area_init函数在系统唯一的struct node对象contig_page_data中node_mem_map成员赋值给全局的mem_map变量 - - -#1.6 今日内容(启动过程中的内存初始化) -------- - - - - -在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. - - -因此我们可以把linux内核的内存管理分三个阶段。 - -| 阶段 | 起点 | 终点 | 描述 | -|:-----:|:-----:|:-----:| -| 第一阶段 | 系统启动 | bootmem或者memblock初始化完成 | 此阶段只能使用memblock_reserve函数分配内存, 早期内核中使用init_bootmem_done = 1标识此阶段结束 | -| 第二阶段 | bootmem或者memblock初始化完 | buddy完成前 | 引导内存分配器bootmem或者memblock接受内存的管理工作, 早期内核中使用mem_init_done = 1标记此阶段的结束 | -| 第三阶段 | buddy初始化完成 | 系统停止运行 | 可以用cache和buddy分配内存 | - - - -**系统启动过程中的内存管理** - - -首先我们来看看start_kernel是如何初始化系统的, start_kerne定义在[init/main.c?v=4.7, line 479](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L479) - -其代码很复杂, 我们只截取出其中与内存管理初始化相关的部分, 如下所示 - - -```cpp -asmlinkage __visible void __init start_kernel(void) -{ - - /* 设置特定架构的信息 - * 同时初始化memblock */ - setup_arch(&command_line); - mm_init_cpumask(&init_mm); - - setup_per_cpu_areas(); - - /* 初始化内存结点和内段区域 */ - build_all_zonelists(NULL, NULL); - page_alloc_init(); - - - /* - * These use large bootmem allocations and must precede - * mem_init(); - * kmem_cache_init(); - */ - mm_init(); - - kmem_cache_init_late(); - - kmemleak_init(); - setup_per_cpu_pageset(); - - rest_init(); -} -``` - - -| 函数 | 功能 | -|:----:|:----:| -| [setup_arch](http://lxr.free-electrons.com/ident?v=4.7;i=setup_arch) | 是一个特定于体系结构的设置函数, 其中一项任务是负责初始化自举分配器 | -| [mm_init_cpumask](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v=4.7#L522) | 初始化CPU屏蔽字 | -| [setup_per_cpu_areas](http://lxr.free-electrons.com/ident?v=4.7;i=setup_per_cpu_areas) | 函数[(查看定义)](http://lxr.free-electrons.com/source/mm/percpu.c?v4.7#L2205])给每个CPU分配内存,并拷贝.data.percpu段的数据. 为系统中的每个CPU的per_cpu变量申请空间.
在SMP系统中, setup_per_cpu_areas初始化源代码中(使用[per_cpu宏](http://lxr.free-electrons.com/source/include/linux/percpu-defs.h#L256))定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本.
此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本
在非SMP系统中这是一个空操作 | -| [build_all_zonelists](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029) | 建立并初始化结点和内存域的数据结构 | -| [mm_init](http://lxr.free-electrons.com/source/init/main.c?v4.7#L464) | 建立了内核的内存分配器,
其中通过[mem_init](http://lxr.free-electrons.com/ident?v=4.7&i=mem_init)停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)
然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器 | -| [kmem_cache_init_late](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378) | 在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器[slab](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378), [slob](http://lxr.free-electrons.com/source/mm/slob.c?v4.7#L655), [slub](http://lxr.free-electrons.com/source/mm/slub.c?v=4.7#L3960)都会定义此函数 | -| [kmemleak_init](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1857) | Kmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 [/sys/kernel/debug/kmemleak](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1467)中, Kmemcheck能够帮助定位大多数内存错误的上下文 | -| [setup_per_cpu_pageset](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5392) | 初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配
由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. | - - - -#2 第一阶段(启动过程中的内存管理) -------- - - -内存管理是操作系统资源管理的重点, 但是在操作系统初始化的初期, 操作系统只是获取到了内存的基本信息, 但是内存管理的数据结构都没有建立, 而我们这些数据结构创建的过程本身就是一个内存分配的过程, 那么就出现一个问题 - - -我们还没有一个内存管理器去负责分配和回收内存, 而我们又不可能将所有的内存信息都静态创建并初始化, 那么我们怎么分配内存管理器所需要的内存呢? 现在我们进入了一个先有鸡还是先有蛋的怪圈, 这种问题的一般解决方法是, 我们先实现一个满足要求的但是可能效率不高的笨家伙(内存管理器), 用它来负责系统初始化初期的内存管理, 最重要的, 用它来初始化我们内存的数据结构, 直到我们真正的内存管理器被初始化完成并能投入使用, 我们将旧的内存管理器丢掉 - -即因此在系统启动过程期间, 内核使用了一个额外的简化形式的内存管理模块早期的**引导内存分配器(boot memory allocator--bootmem分配器)**或者**memblock**, 用于在启动阶段早期分配内存, 而在系统初始化完成后, 该分配器被内核抛弃, 然后初始化了一套新的更加完善的内存分配器. - - - -##2.1 引导内存分配器bootmem -------- - -在启动过程期间, 尽管内存管理尚未初始化, 但是内核仍然需要分配内存以创建各种数据结构, 早期的内核中负责初始化阶段的内存分配器称为**引导内存分配器(boot memory allocator--bootmem分配器)**, 在耳熟能详的伙伴系统建立前内存都是利用分配器来分配的,伙伴系统框架建立起来后,bootmem会过度到伙伴系统. 显然, 对该内存分配器的需求集中于简单性方面, 而不是性能和通用性, 它仅用于初始化阶段. 因此内核开发者决定实现一个最先适配(first-first)分配器用于在启动阶段管理内存. 这是可能想到的最简单的方式. - - -**引导内存分配器(boot memory allocator--bootmem分配器)**基于最先适配(first-first)分配器的原理(这儿是很多系统的内存分配所使用的原理), 使用一个位图来管理页, 以位图代替原来的空闲链表结构来表示存储空间, 位图的比特位的数目与系统中物理内存页面数目相同. 若位图中某一位是1, 则标识该页面已经被分配(已用页), 否则表示未被占有(未用页). - -在需要分配内存时, 分配器逐位的扫描位图, 直至找到一个能提供足够连续页的位置, 即所谓的最先最佳(first-best)或最先适配位置.该分配机制通过记录上一次分配的页面帧号(PFN)结束时的偏移量来实现分配大小小于一页的空间, 连续的小的空闲空间将被合并存储在一页上. - - - -即使是初始化用的最先适配分配器也必须使用一些数据结构存, 内核为系统中每一个结点都提供了一个struct bootmem_data结构的实例, 用于bootmem的内存管理. 它含有引导内存分配器给结点分配内存时所需的信息. 当然, 这时候内存管理还没有初始化, 因而该结构所需的内存是无法动态分配的, 必须在编译时分配给内核. - -在UMA系统上该分配的实现与CPU无关, 而NUMA系统内存结点与CPU相关联, 因此采用了特定体系结构的解决方法. - -bootmem_data的结构定义在[include/linux/bootmem.h?v=4.7, line 28](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L28), 其定义如下所示 - - -关于引导内存分配器的具体内容, 请参见另外一篇博文 - -| CSDN | GitHub | -|:-----:|:------:| -| [引导内存分配器bootmem](带添加链接) | [study/kernel/02-memory/03-initialize/02-bootmem](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/02-memory/03-initialize/02-bootmem) | - - -##2.2 memblock内存分配器 -------- - - -但是bootmem也有很多问题. 最明显的就是外碎片的问题, 因此内核维护了**memblock内存分配器**, 同时用memblock实现了一份bootmem相同的兼容API, 即nobootmem, Memblock以前被定义为Logical Memory Block( 逻辑内存块), 但根据[Yinghai Lu的补丁](https://lkml.org/lkml/2010/7/13/68), 它被重命名为memblock. 并最终替代bootmem成为初始化阶段的内存管理器 - -关于引导内存分配器的具体内容, 请参见另外一篇博文 - - -| CSDN | GitHub | -|:-----:|:------:| -| [memblock内存分配器](带添加链接) | [study/kernel/02-memory/03-initialize/03-memblock](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/02-memory/03-initialize/03-memblock) | - - - - -##2.3 两者的区别与联系 - -bootmem是通过位图来管理,位图存在地地址段, 而memblock是在高地址管理内存, 维护两个链表, 即memory和reserved - -memory链表维护系统的内存信息(在初始化阶段通过bios获取的), 对于任何内存分配, 先去查找memory链表, 然后在reserve链表上记录(新增一个节点,或者合并) - - -1. 两者都可以分配小于一页的内存; - -2. 两者都是就近查找可用的内存, bootmem是从低到高找, memblock是从高往低找; - - -在boot传递给kernel memory bank相关信息后,kernel这边会以memblcok的方式保存这些信息,当buddy system 没有起来之前,在kernel中也是要有一套机制来管理memory的申请和释放. - - -Kernel可以选择nobootmem 或者bootmem 来在buddy system起来之前管理memory. -这两种机制对提供的API是一致的,因此对用户是透明的 - -参见[mm/Makefile](http://lxr.free-electrons.com/source/mm/Makefile#L44) - - -```cpp -ifdef CONFIG_NO_BOOTMEM - obj-y += nobootmem.o -else - obj-y += bootmem.o -endif -``` - - -由于接口是一致的, 那么他们共同使用一份 - -| 头文件 | bootmem接口 | nobootmem接口 | -|:-----:|:-----------:|:------------:| -| [include/linux/bootmem.h](http://lxr.free-electrons.com/source/include/linux/bootmem.h) | [mm/bootmem.c](http://lxr.free-electrons.com/source/mm/bootmem.c) | [mm/nobootmem.c](http://lxr.free-electrons.com/source/mm/nobootmem.c) | - -##2.4 memblock的初始化(arm64架构) -------- - - -前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作. - -```cpp -void __init setup_arch(char **cmdline_p) -{ - /* 初始化memblock */ - arm64_memblock_init( ); - - /* 分页机制初始化 */ - paging_init(); - - bootmem_init(); -} -``` - -| 流程 | 描述 | -|:---:|:----:| -| [arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) | 初始化memblock内存分配器 | -| [paging_init](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538) | 初始化分页机制 | -| [bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | 初始化内存管理 | - - - - -其中arm64_memblock_init就完成了arm64架构下的memblock的初始化 - - - - -#3 第二阶段(初始化buddy内存管理) -------- - - -在arm64架构下, 内核在start_kernel()->setup_arch()函数中依次完成了如下工作 - - -前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作. - -```cpp -void __init setup_arch(char **cmdline_p) -{ - /* 初始化memblock */ - arm64_memblock_init( ); - - /* 分页机制初始化 */ - paging_init(); - - bootmem_init(); -} -``` - -| 流程 | 描述 | -|:---:|:----:| -| [arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) | 初始化memblock内存分配器 | -| [paging_init](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538) | 初始化分页机制 | -| [bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | 初始化内存管理 | - - -其中arm64_memblock_init就完成了arm64架构下的memblock的初始化. - - -而setup_arch则主要完成如下工作 - - -* 调用arm64_memblock_init来完成了memblock的初始化 - -* paging_init初始化内存的分页机制 - -* bootmem_init初始化内存管理 - - -##3.1 初始化流程 -------- - -下面我们就以arm64架构来分析bootmem初始化内存结点和内存域的过程, 在讲解的过程中我们会兼顾的考虑arm64架构下的异同 - -* 首先内核从[start_kernel](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L505)开始启动 - - -* 然后进入体系结构相关的设置部分[setup_arch](http://lxr.free-electrons.com/source/arch/arm/kernel/setup.c?v=4.7#L1073), 开始获取并设置指定体系结构的一些物理信息, 而arm64架构下则对应着[rch/arm64/kernel/setup.c](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) - - -* 在setup_arch函数内, 通过paging_init函数初始化了分页机制和页表的信息 - - -* 接着paging_init函数通过[bootmem_init](http://lxr.free-electrons.com/source/arch/arm/mm/mmu.c#L1642)开始进行初始化工作 - - -arm64在整个初始化的流程上并没有什么不同, 但是有细微的差别 - - -* 由于arm是在后期才开始加入了MMU内存管理单元的, 因此内核必须实现mmu和nonmmu两套不同的代码, 这主要是提现在分页机制的不同上, 因而paging_init分别定义了[arch/arm/mm/nommu.c](http://lxr.free-electrons.com/source/arch/arm/mm/nommu.c?v=4.7#L311)和[arch/arm/mm/mmu.c](http://lxr.free-electrons.com/source/arch/arm/mm/mmu.c?v=4.7#L1623)两个版本, 但是它们均调用了bootmem_init来完成初始化 - - -* 也是因为上面的原因, arm上paging_init有两份代码([mmu](http://lxr.free-electrons.com/source/arch/arm/mm/mmu.c?v=4.7#L162)和[nonmmu](http://lxr.free-electrons.com/source/arch/arm/mm/nommu.c?v=4.7#L311)), 为了降低代码的耦合性, arm通过setup_arch调用paging_init函数, 后者进一步调用了bootmem_init来完成, 而arm64上不存在这样的问题, 则在[setup_arch中顺序的先用paging_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L266)初始化了页表, 然后[setup_arch又调用bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v4.7#L271)来完成了bootmem的初始化 - - - -##3.2 paging_init初始化分页机制 -------- - - -paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响 - -因此在仔细考察其实现之前,很重要的一点是解释该函数的目的。 - -在x86_32系统上内核通常将总的4GB可用虚拟地址空间按3:1的比例划分给用户空间和内核空间, 虚拟地址空间的低端3GB -用于用户状态应用程序, 而高端的1GB则专用于内核. 尽管在分配内核的虚拟地址空间时, 当前系统上下文是不相干的, 但每个进程都有自身特定的地址空间. - -这些划分主要的动机如下所示 - -* 在用户应用程序的执行切换到核心态时(这总是会发生,例如在使用系统调用或发生周期性的时钟中断时),内核必须装载在一个可靠的环境中。因此有必要将地址空间的一部分分配给内核专用. - -* 物理内存页则映射到内核地址空间的起始处,以便内核直接访问,而无需复杂的页表操作. - -如果所有物理内存页都映射到用户空间进程能访问的地址空间中, 如果在系统上有几个应用程序在运行, 将导致严重的安全问题. 每个应用程序都能够读取和修改其他进程在物理内存中的内存区. 显然必须不惜任何代价防止这种情况出现. - -虽然用于用户层进程的虚拟地址部分随进程切换而改变,但是内核部分总是相同的 - - -##3.3 虚拟地址空间(以x86_32位系统为例) -------- - - - -出于内存保护等一系列的考虑, 内核将整个进程的虚拟运行空间划分为内核虚拟运行空间和内核虚拟运行空间 - - - -![虚拟地址空间](../images/vmarea_space.jpg) - -按3:1的比例划分地址空间, 只是约略反映了内核中的情况,内核地址空间作为内核的常驻虚拟地址空间, 自身又分为各个段 - -![内核空间](../images/kernel_space.jpg) - -地址空间的第一段用于将系统的所有物理内存页映射到内核的虚拟地址空间中。由于内核地址空间从偏移量0xC0000000开始,即经常提到的3 GiB,每个虚拟地址x都对应于物理地址x—0xC0000000,因此这是一个简单的线性平移。 - -直接映射区域从0xC0000000到high_memory地址,high_memory准确的数值稍后讨论。第1章提到过,这种方案有一问题。由于内核的虚拟地址空间只有1 GiB,最多只能映射1 GiB物理内存。IA-32系统(没有PAE)最大的内存配置可以达到4 GiB,引出的一个问题是,如何处理剩下的内存? - - -这里有个坏消息。如果物理内存超过896 MiB,则内核无法直接映射全部物理内存。该值甚至比此前提到的最大限制1 GiB还小,因为内核必须保留地址空间最后的128 MiB用于其他目的,我会稍后解释。将这128 MiB加上直接映射的896 MiB内存,则得到内核虚拟地址空间的总数为1 024 MiB = 1GiB。内核使用两个经常使用的缩写normal和highmem,来区分是否可以直接映射的页帧。 - -内核地址空间的最后128 MiB用于何种用途呢?如图3-15所示,该部分有3个用途。 - -* 虚拟内存中连续、但物理内存中不连续的内存区,可以在vmalloc区域分配。该机制通常用于用户过程,内核自身会试图尽力避免非连续的物理地址。内核通常会成功,因为大部分大的内存块都在启动时分配给内核,那时内存的碎片尚不严重。但在已经运行了很长时间的系统上,在内核需要物理内存时,就可能出现可用空间不连续的情况。此类情况,主要出现在动态加载模块时 - -* 持久映射用于将高端内存域中的非持久页映射到内核中 - -* 固定映射是与物理地址空间中的固定页关联的虚拟地址空间项,但具体关联的页帧可以自由 -选择。它与通过固定公式与物理内存关联的直接映射页相反,虚拟固定映射地址与物理内存位置之间 -的关联可以自行定义,关联建立后内核总是会注意到的 - - -同样我们的[用户空间](http://www.360doc.com/content/14/1020/21/19947352_418512226.shtml), 也被划分为几个段, 包括从高地址到低地址分别为 : - - - -![进程的虚拟地址空间](../images/user_space.jpg) - -
- -| 区域 | 存储内容 | -|:---:|:------:| -| 栈 | 局部变量, 函数参数, 返回地址等 | -| 堆 | 动态分配的内存 | -| BSS段 | 未初始化或初值为0的全局变量和静态局部变量| -| 数据段 | 一初始化且初值非0的全局变量和静态局部变量| -| 代码段 | 可执行代码, 字符串面值, 只读变量 | - - -##3.4 bootmem_init初始化内存的基础数据结构(结点pg_data, 内存域zone, 页面page) -------- - -在paging_init之后, 系统的页帧已经建立起来, 然后通过bootmem_init中, 系统开始完成bootmem的初始化工作. - - -不同的体系结构bootmem_init的实现, 没有很大的区别, 但是在初始化的过程中, 其中的很多函数, 依据系统是NUMA还是UMA结构则有不同的定义 - - -bootmem_init函数的实现如下 - -| 函数实现 | arm | arm64 | -|:---:|:---:|:-----:| -| bootmem_init | [arch/arm/mm/init.c, line 282](http://lxr.free-electrons.com/source/arch/arm/mm/init.c?v=4.7#L282) | [arch/arm64/mm/init.c, line 306](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | - - - - -##3.5 build_all_zonelists初始化每个内存节点的zonelists -------- - -内核setup_arch的最后通过bootmem_init中完成了内存数据结构的初始化(包括内存结点pg_data_t, 内存管理域zone和页面信息page), 数据结构已经基本准备好了, 在后面为内存管理做得一个准备工作就是将所有节点的管理区都链入到zonelist中,便于后面内存分配工作的进行. - - -内存节点pg_data_t中将内存节点中的内存区域zone按照某种组织层次存储在一个zonelist中, 即pglist_data->node_zonelists成员信息 - -```cpp -// http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L626 -typedef struct pglist_data -{ - struct zone node_zones[MAX_NR_ZONES]; - struct zonelist node_zonelists[MAX_ZONELISTS]; -} -``` - - -内核定义了内存的一个层次结构关系, 首先试图分配廉价的内存,如果失败,则根据访问速度和容量,逐渐尝试分配更昂贵的内存. - -高端内存最廉价, 因为内核没有任何部分依赖于从该内存域分配的内存, 如果高端内存用尽, 对内核没有副作用, 所以优先分配高端内存 - -普通内存域的情况有所不同, 许多内核数据结构必须保存在该内存域, 而不能放置到高端内存域, 因此如果普通内存域用尽, 那么内核会面临内存紧张的情况 - -DMA内存域最昂贵,因为它用于外设和系统之间的数据传输。 -举例来讲,如果内核指定想要分配高端内存域。它首先在当前结点的高端内存域寻找适当的空闲内存段,如果失败,则查看该结点的普通内存域,如果还失败,则试图在该结点的DMA内存域分配。如果在3个本地内存域都无法找到空闲内存,则查看其他结点。这种情况下,备选结点应该尽可能靠近主结点,以最小化访问非本地内存引起的性能损失。 - - - -#4 总结 -------- - - - -##4.1 start_kernel启动流程 -------- - - -```cpp -start_kernel() - |---->page_address_init() - | 考虑支持高端内存 - | 业务:初始化page_address_pool链表; - | 将page_address_maps数组元素按索引降序插入 - | page_address_pool链表; - | 初始化page_address_htable数组. - | - |---->setup_arch(&command_line); - | 初始化特定体系结构的内容 - |---->arm64_memblock_init( ); [参见memblock和bootmem] - | 初始化引导阶段的内存分配器memblock - | - |---->paging_init(); [参见分页机制初始化paging_init] - | 分页机制初始化 - | - |---->bootmem_init(); [与build_all_zonelist共同完成内存数据结构的初始化] - | 初始化内存数据结构包括内存节点和内存域 - | - |---->setup_per_cpu_areas(); - | 为per-CPU变量分配空间 - | - |---->build_all_zonelist() [bootmem_init初始化数据结构, 该函数初始化zonelists] - | 为系统中的zone建立后备zone的列表. - | 所有zone的后备列表都在 - | pglist_data->node_zonelists[0]中; - | - | 期间也对per-CPU变量boot_pageset做了初始化. - | - |---->page_alloc_init() - |---->hotcpu_notifier(page_alloc_cpu_notifier, 0); - | 不考虑热插拔CPU - | - |---->pidhash_init() - | 详见下文. - | 根据低端内存页数和散列度,分配hash空间,并赋予pid_hash - | - |---->vfs_caches_init_early() - |---->dcache_init_early() - | dentry_hashtable空间,d_hash_shift, h_hash_mask赋值; - | 同pidhash_init(); - | 区别: - | 散列度变化了(13 - PAGE_SHIFT); - | 传入alloc_large_system_hash的最后参数值为0; - | - |---->inode_init_early() - | inode_hashtable空间,i_hash_shift, i_hash_mask赋值; - | 同pidhash_init(); - | 区别: - | 散列度变化了(14 - PAGE_SHIFT); - | 传入alloc_large_system_hash的最后参数值为0; - | -``` - - -##4.2 体系结构相关的初始化工作setup_arch -------- - - -```cpp -setup_arch(char **cmdline_p) - |---->arm64_memblock_init( ); - | 初始化引导阶段的内存分配器memblock - | - | - |---->paging_init(); - | 分页机制初始化 - | - | - |---->bootmem_init(); - | 初始化内存数据结构包括内存节点和内存域 -} -``` - -##4.3 bootmem_init初始化内存的基础数据结构(结点pg_data, 内存域zone, 页面page) -------- - -```cpp -bootmem_init(void) - |---->min = PFN_UP(memblock_start_of_DRAM()); - |---->max = PFN_DOWN(memblock_end_of_DRAM()); - | - | - |---->arm64_numa_init(); - | 支持numa架构 - |---->arm64_numa_init(); - | 支持numa架构 - | - | - |---->zone_sizes_init(min, max); - 来初始化节点和管理区的一些数据项 - | - |---->free_area_init_node - | 初始化内存节点 - | - | - |---->free_area_init_core初始化zone - | - | - |---->memmap_init初始化page页面 - | - | - | - |---->memblock_dump_all(); - | 初始化完成, 显示memblock的保留的所有内存信息 -``` - - -##4.4 build_all_zonelists初始化每个内存节点的zonelists -------- - - -```cpp -void build_all_zonelists(void) - |---->set_zonelist_order() - |---->current_zonelist_order = ZONELIST_ORDER_ZONE; - | - |---->__build_all_zonelists(NULL); - | Memory不支持热插拔, 为每个zone建立后备的zone, - | 每个zone及自己后备的zone,形成zonelist - | - |---->pg_data_t *pgdat = NULL; - | pgdat = &contig_page_data;(单node) - | - |---->build_zonelists(pgdat); - | 为每个zone建立后备zone的列表 - | - |---->struct zonelist *zonelist = NULL; - | enum zone_type j; - | zonelist = &pgdat->node_zonelists[0]; - | - |---->j = build_zonelists_node(pddat, zonelist, 0, MAX_NR_ZONES - 1); - | 为pgdat->node_zones[0]建立后备的zone,node_zones[0]后备的zone - | 存储在node_zonelist[0]内,对于node_zone[0]的后备zone,其后备的zone - | 链表如下(只考虑UMA体系,而且不考虑ZONE_DMA): - | node_zonelist[0]._zonerefs[0].zone = &node_zones[2]; - | node_zonelist[0]._zonerefs[0].zone_idx = 2; - | node_zonelist[0]._zonerefs[1].zone = &node_zones[1]; - | node_zonelist[0]._zonerefs[1].zone_idx = 1; - | node_zonelist[0]._zonerefs[2].zone = &node_zones[0]; - | node_zonelist[0]._zonerefs[2].zone_idx = 0; - | - | zonelist->_zonerefs[3].zone = NULL; - | zonelist->_zonerefs[3].zone_idx = 0; - | - |---->build_zonelist_cache(pgdat); - |---->pdat->node_zonelists[0].zlcache_ptr = NULL; - | UMA体系结构 - | - |---->for_each_possible_cpu(cpu) - | setup_pageset(&per_cpu(boot_pageset, cpu), 0); - |详见下文 - |---->vm_total_pages = nr_free_pagecache_pages(); - | 业务:获得所有zone中的present_pages总和. - | - |---->page_group_by_mobility_disabled = 0; - | 对于代码中的判断条件一般不会成立,因为页数会最够多(内存较大) -``` - - -版权声明知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 - +启动过程期间的内存管理--bootmem分配器 +======= + + + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + + + +在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后内核才能检测到可用内存和寄存器. + + +而我们今天要讲的boot阶段就是系统初始化阶段使用的内存分配器. + + + + +#1 前景回顾 +------- + + +##1.1 Linux内存管理的层次结构 +------- + + +Linux把物理内存划分为三个层次来管理 + +| 层次 | 描述 | +|:----:|:----:| +| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | +| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | +| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | + +为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 + + +* 首先, 内存被划分为**结点**. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. + +* 接着各个节点又被划分为内存管理区域, 一个**管理区域**通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. + + +* 最后**页帧(page frame)**代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号. + + +##1.2 内存结点pg_data_t +------- + + +在LINUX中引入一个数据结构`struct pglist_data` ,来描述一个node,定义在[`include/linux/mmzone.h`](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L630) 文件中。(这个结构被typedef pg_data_t)。 + + +* 对于NUMA系统来讲, 整个系统的内存由一个[node_data](http://lxr.free-electrons.com/source/arch/s390/numa/numa.c?v=4.7#L23)的pg_data_t指针数组来管理 + +* 对于PC这样的UMA系统,使用struct pglist_data contig_page_data ,作为系统唯一的node管理所有的内存区域。(UMA系统中中只有一个node) + +可以使用NODE_DATA(node_id)来查找系统中编号为node_id的结点, 而UMA结构下由于只有一个结点, 因此该宏总是返回全局的contig_page_data, 而与参数node_id无关. + +**NODE_DATA(node_id)查找编号node_id的结点pg_data_t信息** 参见[NODE_DATA的定义](http://lxr.free-electrons.com/ident?v=4.7;i=NODE_DATA) + +```cpp +extern struct pglist_data *node_data[]; +#define NODE_DATA(nid) (node_data[(nid)]) +``` + + +在UMA结构的机器中, 只有一个node结点即contig_page_data, 此时NODE_DATA直接指向了全局的contig_page_data, 而与node的编号nid无关, 参照[include/linux/mmzone.h?v=4.7, line 858](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L858) + + +```cpp +extern struct pglist_data contig_page_data; +#define NODE_DATA(nid) (&contig_page_data) + +``` + +##1.2 物理内存区域 +------- + +因为实际的计算机体系结构有硬件的诸多限制, 这限制了页框可以使用的方式. 尤其是, Linux内核必须处理80x86体系结构的两种硬件约束. + +* ISA总线的直接内存存储DMA处理器有一个严格的限制 : 他们只能对RAM的前16MB进行寻址 + +* 在具有大容量RAM的现代32位计算机中, CPU不能直接访问所有的物理地址, 因为线性地址空间太小, 内核不可能直接映射所有物理内存到线性地址空间, 我们会在后面典型架构(x86)上内存区域划分详细讲解x86_32上的内存区域划分 + + +因此Linux内核对不同区域的内存需要采用不同的管理方式和映射方式, 因此内核将物理地址或者成用zone_t表示的不同地址区域 + +对于x86_32的机器,管理区(内存区域)类型如下分布 + + +| 类型 | 区域 | +| :------- | ----: | +| ZONE_DMA | 0~15MB | +| ZONE_NORMAL | 16MB~895MB | +| ZONE_HIGHMEM | 896MB~物理内存结束 | + + +##1.3 物理页帧 +------- + +内核把物理页作为内存管理的基本单位. 尽管处理器的最小可寻址单位通常是字, 但是, 内存管理单元MMU通常以页为单位进行处理. 因此,从虚拟内存的上来看,页就是最小单位. + +页帧代表了系统内存的最小单位, 对内存中的每个页都会创建struct page的一个实例. 内核必须要保证page结构体足够的小,否则仅struct page就要占用大量的内存. + + + 内核用[struct page(include/linux/mm_types.h?v=4.7, line 45)](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v4.7#L45)结构表示系统中的每个物理页. + +出于节省内存的考虑,struct page中使用了大量的联合体union. + + +`mem_map`是一个struct page的数组,管理着系统中所有的物理内存页面。在系统启动的过程中,创建和分配mem_map的内存区域, mem_map定义在[mm/page_alloc.c?v=4.7, line 6691](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6691) + + +UMA体系结构中,free_area_init函数在系统唯一的struct node对象contig_page_data中node_mem_map成员赋值给全局的mem_map变量 + + +#1.6 今日内容(启动过程中的内存初始化) +------- + + + + +在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. + + +因此我们可以把linux内核的内存管理分三个阶段。 + +| 阶段 | 起点 | 终点 | 描述 | +|:-----:|:-----:|:-----:| +| 第一阶段 | 系统启动 | bootmem或者memblock初始化完成 | 此阶段只能使用memblock_reserve函数分配内存, 早期内核中使用init_bootmem_done = 1标识此阶段结束 | +| 第二阶段 | bootmem或者memblock初始化完 | buddy完成前 | 引导内存分配器bootmem或者memblock接受内存的管理工作, 早期内核中使用mem_init_done = 1标记此阶段的结束 | +| 第三阶段 | buddy初始化完成 | 系统停止运行 | 可以用cache和buddy分配内存 | + + + +**系统启动过程中的内存管理** + + +首先我们来看看start_kernel是如何初始化系统的, start_kerne定义在[init/main.c?v=4.7, line 479](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L479) + +其代码很复杂, 我们只截取出其中与内存管理初始化相关的部分, 如下所示 + + +```cpp +asmlinkage __visible void __init start_kernel(void) +{ + + /* 设置特定架构的信息 + * 同时初始化memblock */ + setup_arch(&command_line); + mm_init_cpumask(&init_mm); + + setup_per_cpu_areas(); + + /* 初始化内存结点和内段区域 */ + build_all_zonelists(NULL, NULL); + page_alloc_init(); + + + /* + * These use large bootmem allocations and must precede + * mem_init(); + * kmem_cache_init(); + */ + mm_init(); + + kmem_cache_init_late(); + + kmemleak_init(); + setup_per_cpu_pageset(); + + rest_init(); +} +``` + + +| 函数 | 功能 | +|:----:|:----:| +| [setup_arch](http://lxr.free-electrons.com/ident?v=4.7;i=setup_arch) | 是一个特定于体系结构的设置函数, 其中一项任务是负责初始化自举分配器 | +| [mm_init_cpumask](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v=4.7#L522) | 初始化CPU屏蔽字 | +| [setup_per_cpu_areas](http://lxr.free-electrons.com/ident?v=4.7;i=setup_per_cpu_areas) | 函数[(查看定义)](http://lxr.free-electrons.com/source/mm/percpu.c?v4.7#L2205])给每个CPU分配内存,并拷贝.data.percpu段的数据. 为系统中的每个CPU的per_cpu变量申请空间.
在SMP系统中, setup_per_cpu_areas初始化源代码中(使用[per_cpu宏](http://lxr.free-electrons.com/source/include/linux/percpu-defs.h#L256))定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本.
此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本
在非SMP系统中这是一个空操作 | +| [build_all_zonelists](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029) | 建立并初始化结点和内存域的数据结构 | +| [mm_init](http://lxr.free-electrons.com/source/init/main.c?v4.7#L464) | 建立了内核的内存分配器,
其中通过[mem_init](http://lxr.free-electrons.com/ident?v=4.7&i=mem_init)停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)
然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器 | +| [kmem_cache_init_late](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378) | 在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器[slab](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378), [slob](http://lxr.free-electrons.com/source/mm/slob.c?v4.7#L655), [slub](http://lxr.free-electrons.com/source/mm/slub.c?v=4.7#L3960)都会定义此函数 | +| [kmemleak_init](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1857) | Kmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 [/sys/kernel/debug/kmemleak](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1467)中, Kmemcheck能够帮助定位大多数内存错误的上下文 | +| [setup_per_cpu_pageset](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5392) | 初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配
由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. | + + + +#2 第一阶段(启动过程中的内存管理) +------- + + +内存管理是操作系统资源管理的重点, 但是在操作系统初始化的初期, 操作系统只是获取到了内存的基本信息, 但是内存管理的数据结构都没有建立, 而我们这些数据结构创建的过程本身就是一个内存分配的过程, 那么就出现一个问题 + + +我们还没有一个内存管理器去负责分配和回收内存, 而我们又不可能将所有的内存信息都静态创建并初始化, 那么我们怎么分配内存管理器所需要的内存呢? 现在我们进入了一个先有鸡还是先有蛋的怪圈, 这种问题的一般解决方法是, 我们先实现一个满足要求的但是可能效率不高的笨家伙(内存管理器), 用它来负责系统初始化初期的内存管理, 最重要的, 用它来初始化我们内存的数据结构, 直到我们真正的内存管理器被初始化完成并能投入使用, 我们将旧的内存管理器丢掉 + +即因此在系统启动过程期间, 内核使用了一个额外的简化形式的内存管理模块早期的**引导内存分配器(boot memory allocator--bootmem分配器)**或者**memblock**, 用于在启动阶段早期分配内存, 而在系统初始化完成后, 该分配器被内核抛弃, 然后初始化了一套新的更加完善的内存分配器. + + + +##2.1 引导内存分配器bootmem +------- + +在启动过程期间, 尽管内存管理尚未初始化, 但是内核仍然需要分配内存以创建各种数据结构, 早期的内核中负责初始化阶段的内存分配器称为**引导内存分配器(boot memory allocator--bootmem分配器)**, 在耳熟能详的伙伴系统建立前内存都是利用分配器来分配的,伙伴系统框架建立起来后,bootmem会过度到伙伴系统. 显然, 对该内存分配器的需求集中于简单性方面, 而不是性能和通用性, 它仅用于初始化阶段. 因此内核开发者决定实现一个最先适配(first-first)分配器用于在启动阶段管理内存. 这是可能想到的最简单的方式. + + +**引导内存分配器(boot memory allocator--bootmem分配器)**基于最先适配(first-first)分配器的原理(这儿是很多系统的内存分配所使用的原理), 使用一个位图来管理页, 以位图代替原来的空闲链表结构来表示存储空间, 位图的比特位的数目与系统中物理内存页面数目相同. 若位图中某一位是1, 则标识该页面已经被分配(已用页), 否则表示未被占有(未用页). + +在需要分配内存时, 分配器逐位的扫描位图, 直至找到一个能提供足够连续页的位置, 即所谓的最先最佳(first-best)或最先适配位置.该分配机制通过记录上一次分配的页面帧号(PFN)结束时的偏移量来实现分配大小小于一页的空间, 连续的小的空闲空间将被合并存储在一页上. + + + +即使是初始化用的最先适配分配器也必须使用一些数据结构存, 内核为系统中每一个结点都提供了一个struct bootmem_data结构的实例, 用于bootmem的内存管理. 它含有引导内存分配器给结点分配内存时所需的信息. 当然, 这时候内存管理还没有初始化, 因而该结构所需的内存是无法动态分配的, 必须在编译时分配给内核. + +在UMA系统上该分配的实现与CPU无关, 而NUMA系统内存结点与CPU相关联, 因此采用了特定体系结构的解决方法. + +bootmem_data的结构定义在[include/linux/bootmem.h?v=4.7, line 28](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L28), 其定义如下所示 + + +关于引导内存分配器的具体内容, 请参见另外一篇博文 + +| CSDN | GitHub | +|:-----:|:------:| +| [引导内存分配器bootmem](带添加链接) | [study/kernel/02-memory/03-initialize/02-bootmem](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/02-memory/03-initialize/02-bootmem) | + + +##2.2 memblock内存分配器 +------- + + +但是bootmem也有很多问题. 最明显的就是外碎片的问题, 因此内核维护了**memblock内存分配器**, 同时用memblock实现了一份bootmem相同的兼容API, 即nobootmem, Memblock以前被定义为Logical Memory Block( 逻辑内存块), 但根据[Yinghai Lu的补丁](https://lkml.org/lkml/2010/7/13/68), 它被重命名为memblock. 并最终替代bootmem成为初始化阶段的内存管理器 + +关于引导内存分配器的具体内容, 请参见另外一篇博文 + + +| CSDN | GitHub | +|:-----:|:------:| +| [memblock内存分配器](带添加链接) | [study/kernel/02-memory/03-initialize/03-memblock](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/02-memory/03-initialize/03-memblock) | + + + + +##2.3 两者的区别与联系 + +bootmem是通过位图来管理,位图存在地地址段, 而memblock是在高地址管理内存, 维护两个链表, 即memory和reserved + +memory链表维护系统的内存信息(在初始化阶段通过bios获取的), 对于任何内存分配, 先去查找memory链表, 然后在reserve链表上记录(新增一个节点,或者合并) + + +1. 两者都可以分配小于一页的内存; + +2. 两者都是就近查找可用的内存, bootmem是从低到高找, memblock是从高往低找; + + +在boot传递给kernel memory bank相关信息后,kernel这边会以memblcok的方式保存这些信息,当buddy system 没有起来之前,在kernel中也是要有一套机制来管理memory的申请和释放. + + +Kernel可以选择nobootmem 或者bootmem 来在buddy system起来之前管理memory. +这两种机制对提供的API是一致的,因此对用户是透明的 + +参见[mm/Makefile](http://lxr.free-electrons.com/source/mm/Makefile#L44) + + +```cpp +ifdef CONFIG_NO_BOOTMEM + obj-y += nobootmem.o +else + obj-y += bootmem.o +endif +``` + + +由于接口是一致的, 那么他们共同使用一份 + +| 头文件 | bootmem接口 | nobootmem接口 | +|:-----:|:-----------:|:------------:| +| [include/linux/bootmem.h](http://lxr.free-electrons.com/source/include/linux/bootmem.h) | [mm/bootmem.c](http://lxr.free-electrons.com/source/mm/bootmem.c) | [mm/nobootmem.c](http://lxr.free-electrons.com/source/mm/nobootmem.c) | + +##2.4 memblock的初始化(arm64架构) +------- + + +前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作. + +```cpp +void __init setup_arch(char **cmdline_p) +{ + /* 初始化memblock */ + arm64_memblock_init( ); + + /* 分页机制初始化 */ + paging_init(); + + bootmem_init(); +} +``` + +| 流程 | 描述 | +|:---:|:----:| +| [arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) | 初始化memblock内存分配器 | +| [paging_init](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538) | 初始化分页机制 | +| [bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | 初始化内存管理 | + + + + +其中arm64_memblock_init就完成了arm64架构下的memblock的初始化 + + + + +#3 第二阶段(初始化buddy内存管理) +------- + + +在arm64架构下, 内核在start_kernel()->setup_arch()函数中依次完成了如下工作 + + +前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作. + +```cpp +void __init setup_arch(char **cmdline_p) +{ + /* 初始化memblock */ + arm64_memblock_init( ); + + /* 分页机制初始化 */ + paging_init(); + + bootmem_init(); +} +``` + +| 流程 | 描述 | +|:---:|:----:| +| [arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) | 初始化memblock内存分配器 | +| [paging_init](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538) | 初始化分页机制 | +| [bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | 初始化内存管理 | + + +其中arm64_memblock_init就完成了arm64架构下的memblock的初始化. + + +而setup_arch则主要完成如下工作 + + +* 调用arm64_memblock_init来完成了memblock的初始化 + +* paging_init初始化内存的分页机制 + +* bootmem_init初始化内存管理 + + +##3.1 初始化流程 +------- + +下面我们就以arm64架构来分析bootmem初始化内存结点和内存域的过程, 在讲解的过程中我们会兼顾的考虑arm64架构下的异同 + +* 首先内核从[start_kernel](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L505)开始启动 + + +* 然后进入体系结构相关的设置部分[setup_arch](http://lxr.free-electrons.com/source/arch/arm/kernel/setup.c?v=4.7#L1073), 开始获取并设置指定体系结构的一些物理信息, 而arm64架构下则对应着[rch/arm64/kernel/setup.c](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) + + +* 在setup_arch函数内, 通过paging_init函数初始化了分页机制和页表的信息 + + +* 接着paging_init函数通过[bootmem_init](http://lxr.free-electrons.com/source/arch/arm/mm/mmu.c#L1642)开始进行初始化工作 + + +arm64在整个初始化的流程上并没有什么不同, 但是有细微的差别 + + +* 由于arm是在后期才开始加入了MMU内存管理单元的, 因此内核必须实现mmu和nonmmu两套不同的代码, 这主要是提现在分页机制的不同上, 因而paging_init分别定义了[arch/arm/mm/nommu.c](http://lxr.free-electrons.com/source/arch/arm/mm/nommu.c?v=4.7#L311)和[arch/arm/mm/mmu.c](http://lxr.free-electrons.com/source/arch/arm/mm/mmu.c?v=4.7#L1623)两个版本, 但是它们均调用了bootmem_init来完成初始化 + + +* 也是因为上面的原因, arm上paging_init有两份代码([mmu](http://lxr.free-electrons.com/source/arch/arm/mm/mmu.c?v=4.7#L162)和[nonmmu](http://lxr.free-electrons.com/source/arch/arm/mm/nommu.c?v=4.7#L311)), 为了降低代码的耦合性, arm通过setup_arch调用paging_init函数, 后者进一步调用了bootmem_init来完成, 而arm64上不存在这样的问题, 则在[setup_arch中顺序的先用paging_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L266)初始化了页表, 然后[setup_arch又调用bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v4.7#L271)来完成了bootmem的初始化 + + + +##3.2 paging_init初始化分页机制 +------- + + +paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响 + +因此在仔细考察其实现之前,很重要的一点是解释该函数的目的。 + +在x86_32系统上内核通常将总的4GB可用虚拟地址空间按3:1的比例划分给用户空间和内核空间, 虚拟地址空间的低端3GB +用于用户状态应用程序, 而高端的1GB则专用于内核. 尽管在分配内核的虚拟地址空间时, 当前系统上下文是不相干的, 但每个进程都有自身特定的地址空间. + +这些划分主要的动机如下所示 + +* 在用户应用程序的执行切换到核心态时(这总是会发生,例如在使用系统调用或发生周期性的时钟中断时),内核必须装载在一个可靠的环境中。因此有必要将地址空间的一部分分配给内核专用. + +* 物理内存页则映射到内核地址空间的起始处,以便内核直接访问,而无需复杂的页表操作. + +如果所有物理内存页都映射到用户空间进程能访问的地址空间中, 如果在系统上有几个应用程序在运行, 将导致严重的安全问题. 每个应用程序都能够读取和修改其他进程在物理内存中的内存区. 显然必须不惜任何代价防止这种情况出现. + +虽然用于用户层进程的虚拟地址部分随进程切换而改变,但是内核部分总是相同的 + + +##3.3 虚拟地址空间(以x86_32位系统为例) +------- + + + +出于内存保护等一系列的考虑, 内核将整个进程的虚拟运行空间划分为内核虚拟运行空间和内核虚拟运行空间 + + + +![虚拟地址空间](../images/vmarea_space.jpg) + +按3:1的比例划分地址空间, 只是约略反映了内核中的情况,内核地址空间作为内核的常驻虚拟地址空间, 自身又分为各个段 + +![内核空间](../images/kernel_space.jpg) + +地址空间的第一段用于将系统的所有物理内存页映射到内核的虚拟地址空间中。由于内核地址空间从偏移量0xC0000000开始,即经常提到的3 GiB,每个虚拟地址x都对应于物理地址x—0xC0000000,因此这是一个简单的线性平移。 + +直接映射区域从0xC0000000到high_memory地址,high_memory准确的数值稍后讨论。第1章提到过,这种方案有一问题。由于内核的虚拟地址空间只有1 GiB,最多只能映射1 GiB物理内存。IA-32系统(没有PAE)最大的内存配置可以达到4 GiB,引出的一个问题是,如何处理剩下的内存? + + +这里有个坏消息。如果物理内存超过896 MiB,则内核无法直接映射全部物理内存。该值甚至比此前提到的最大限制1 GiB还小,因为内核必须保留地址空间最后的128 MiB用于其他目的,我会稍后解释。将这128 MiB加上直接映射的896 MiB内存,则得到内核虚拟地址空间的总数为1 024 MiB = 1GiB。内核使用两个经常使用的缩写normal和highmem,来区分是否可以直接映射的页帧。 + +内核地址空间的最后128 MiB用于何种用途呢?如图3-15所示,该部分有3个用途。 + +* 虚拟内存中连续、但物理内存中不连续的内存区,可以在vmalloc区域分配。该机制通常用于用户过程,内核自身会试图尽力避免非连续的物理地址。内核通常会成功,因为大部分大的内存块都在启动时分配给内核,那时内存的碎片尚不严重。但在已经运行了很长时间的系统上,在内核需要物理内存时,就可能出现可用空间不连续的情况。此类情况,主要出现在动态加载模块时 + +* 持久映射用于将高端内存域中的非持久页映射到内核中 + +* 固定映射是与物理地址空间中的固定页关联的虚拟地址空间项,但具体关联的页帧可以自由 +选择。它与通过固定公式与物理内存关联的直接映射页相反,虚拟固定映射地址与物理内存位置之间 +的关联可以自行定义,关联建立后内核总是会注意到的 + + +同样我们的[用户空间](http://www.360doc.com/content/14/1020/21/19947352_418512226.shtml), 也被划分为几个段, 包括从高地址到低地址分别为 : + + + +![进程的虚拟地址空间](../images/user_space.jpg) + +
+ +| 区域 | 存储内容 | +|:---:|:------:| +| 栈 | 局部变量, 函数参数, 返回地址等 | +| 堆 | 动态分配的内存 | +| BSS段 | 未初始化或初值为0的全局变量和静态局部变量| +| 数据段 | 一初始化且初值非0的全局变量和静态局部变量| +| 代码段 | 可执行代码, 字符串面值, 只读变量 | + + +##3.4 bootmem_init初始化内存的基础数据结构(结点pg_data, 内存域zone, 页面page) +------- + +在paging_init之后, 系统的页帧已经建立起来, 然后通过bootmem_init中, 系统开始完成bootmem的初始化工作. + + +不同的体系结构bootmem_init的实现, 没有很大的区别, 但是在初始化的过程中, 其中的很多函数, 依据系统是NUMA还是UMA结构则有不同的定义 + + +bootmem_init函数的实现如下 + +| 函数实现 | arm | arm64 | +|:---:|:---:|:-----:| +| bootmem_init | [arch/arm/mm/init.c, line 282](http://lxr.free-electrons.com/source/arch/arm/mm/init.c?v=4.7#L282) | [arch/arm64/mm/init.c, line 306](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | + + + + +##3.5 build_all_zonelists初始化每个内存节点的zonelists +------- + +内核setup_arch的最后通过bootmem_init中完成了内存数据结构的初始化(包括内存结点pg_data_t, 内存管理域zone和页面信息page), 数据结构已经基本准备好了, 在后面为内存管理做得一个准备工作就是将所有节点的管理区都链入到zonelist中,便于后面内存分配工作的进行. + + +内存节点pg_data_t中将内存节点中的内存区域zone按照某种组织层次存储在一个zonelist中, 即pglist_data->node_zonelists成员信息 + +```cpp +// http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L626 +typedef struct pglist_data +{ + struct zone node_zones[MAX_NR_ZONES]; + struct zonelist node_zonelists[MAX_ZONELISTS]; +} +``` + + +内核定义了内存的一个层次结构关系, 首先试图分配廉价的内存,如果失败,则根据访问速度和容量,逐渐尝试分配更昂贵的内存. + +高端内存最廉价, 因为内核没有任何部分依赖于从该内存域分配的内存, 如果高端内存用尽, 对内核没有副作用, 所以优先分配高端内存 + +普通内存域的情况有所不同, 许多内核数据结构必须保存在该内存域, 而不能放置到高端内存域, 因此如果普通内存域用尽, 那么内核会面临内存紧张的情况 + +DMA内存域最昂贵,因为它用于外设和系统之间的数据传输。 +举例来讲,如果内核指定想要分配高端内存域。它首先在当前结点的高端内存域寻找适当的空闲内存段,如果失败,则查看该结点的普通内存域,如果还失败,则试图在该结点的DMA内存域分配。如果在3个本地内存域都无法找到空闲内存,则查看其他结点。这种情况下,备选结点应该尽可能靠近主结点,以最小化访问非本地内存引起的性能损失。 + + + +#4 总结 +------- + + + +##4.1 start_kernel启动流程 +------- + + +```cpp +start_kernel() + |---->page_address_init() + | 考虑支持高端内存 + | 业务:初始化page_address_pool链表; + | 将page_address_maps数组元素按索引降序插入 + | page_address_pool链表; + | 初始化page_address_htable数组. + | + |---->setup_arch(&command_line); + | 初始化特定体系结构的内容 + |---->arm64_memblock_init( ); [参见memblock和bootmem] + | 初始化引导阶段的内存分配器memblock + | + |---->paging_init(); [参见分页机制初始化paging_init] + | 分页机制初始化 + | + |---->bootmem_init(); [与build_all_zonelist共同完成内存数据结构的初始化] + | 初始化内存数据结构包括内存节点和内存域 + | + |---->setup_per_cpu_areas(); + | 为per-CPU变量分配空间 + | + |---->build_all_zonelist() [bootmem_init初始化数据结构, 该函数初始化zonelists] + | 为系统中的zone建立后备zone的列表. + | 所有zone的后备列表都在 + | pglist_data->node_zonelists[0]中; + | + | 期间也对per-CPU变量boot_pageset做了初始化. + | + |---->page_alloc_init() + |---->hotcpu_notifier(page_alloc_cpu_notifier, 0); + | 不考虑热插拔CPU + | + |---->pidhash_init() + | 详见下文. + | 根据低端内存页数和散列度,分配hash空间,并赋予pid_hash + | + |---->vfs_caches_init_early() + |---->dcache_init_early() + | dentry_hashtable空间,d_hash_shift, h_hash_mask赋值; + | 同pidhash_init(); + | 区别: + | 散列度变化了(13 - PAGE_SHIFT); + | 传入alloc_large_system_hash的最后参数值为0; + | + |---->inode_init_early() + | inode_hashtable空间,i_hash_shift, i_hash_mask赋值; + | 同pidhash_init(); + | 区别: + | 散列度变化了(14 - PAGE_SHIFT); + | 传入alloc_large_system_hash的最后参数值为0; + | +``` + + +##4.2 体系结构相关的初始化工作setup_arch +------- + + +```cpp +setup_arch(char **cmdline_p) + |---->arm64_memblock_init( ); + | 初始化引导阶段的内存分配器memblock + | + | + |---->paging_init(); + | 分页机制初始化 + | + | + |---->bootmem_init(); + | 初始化内存数据结构包括内存节点和内存域 +} +``` + +##4.3 bootmem_init初始化内存的基础数据结构(结点pg_data, 内存域zone, 页面page) +------- + +```cpp +bootmem_init(void) + |---->min = PFN_UP(memblock_start_of_DRAM()); + |---->max = PFN_DOWN(memblock_end_of_DRAM()); + | + | + |---->arm64_numa_init(); + | 支持numa架构 + |---->arm64_numa_init(); + | 支持numa架构 + | + | + |---->zone_sizes_init(min, max); + 来初始化节点和管理区的一些数据项 + | + |---->free_area_init_node + | 初始化内存节点 + | + | + |---->free_area_init_core初始化zone + | + | + |---->memmap_init初始化page页面 + | + | + | + |---->memblock_dump_all(); + | 初始化完成, 显示memblock的保留的所有内存信息 +``` + + +##4.4 build_all_zonelists初始化每个内存节点的zonelists +------- + + +```cpp +void build_all_zonelists(void) + |---->set_zonelist_order() + |---->current_zonelist_order = ZONELIST_ORDER_ZONE; + | + |---->__build_all_zonelists(NULL); + | Memory不支持热插拔, 为每个zone建立后备的zone, + | 每个zone及自己后备的zone,形成zonelist + | + |---->pg_data_t *pgdat = NULL; + | pgdat = &contig_page_data;(单node) + | + |---->build_zonelists(pgdat); + | 为每个zone建立后备zone的列表 + | + |---->struct zonelist *zonelist = NULL; + | enum zone_type j; + | zonelist = &pgdat->node_zonelists[0]; + | + |---->j = build_zonelists_node(pddat, zonelist, 0, MAX_NR_ZONES - 1); + | 为pgdat->node_zones[0]建立后备的zone,node_zones[0]后备的zone + | 存储在node_zonelist[0]内,对于node_zone[0]的后备zone,其后备的zone + | 链表如下(只考虑UMA体系,而且不考虑ZONE_DMA): + | node_zonelist[0]._zonerefs[0].zone = &node_zones[2]; + | node_zonelist[0]._zonerefs[0].zone_idx = 2; + | node_zonelist[0]._zonerefs[1].zone = &node_zones[1]; + | node_zonelist[0]._zonerefs[1].zone_idx = 1; + | node_zonelist[0]._zonerefs[2].zone = &node_zones[0]; + | node_zonelist[0]._zonerefs[2].zone_idx = 0; + | + | zonelist->_zonerefs[3].zone = NULL; + | zonelist->_zonerefs[3].zone_idx = 0; + | + |---->build_zonelist_cache(pgdat); + |---->pdat->node_zonelists[0].zlcache_ptr = NULL; + | UMA体系结构 + | + |---->for_each_possible_cpu(cpu) + | setup_pageset(&per_cpu(boot_pageset, cpu), 0); + |详见下文 + |---->vm_total_pages = nr_free_pagecache_pages(); + | 业务:获得所有zone中的present_pages总和. + | + |---->page_group_by_mobility_disabled = 0; + | 对于代码中的判断条件一般不会成立,因为页数会最够多(内存较大) +``` + + +版权声明知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 + diff --git a/study/kernel/02-memory/03-initialize/03-memblock/README.md b/study/kernel/02-memory/03-initialize/03-memblock/README.md index 97a0445..e7ec4db 100644 --- a/study/kernel/02-memory/03-initialize/03-memblock/README.md +++ b/study/kernel/02-memory/03-initialize/03-memblock/README.md @@ -1,1348 +1,1348 @@ -的启动过程期间的内存管理--memblock分配器 -======= - - - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - -在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后内核才能检测到可用内存和寄存器. - - - - -#1 前景回顾 -------- - - -##1.1 Linux内存管理的层次结构 -------- - - -Linux把物理内存划分为三个层次来管理 - -| 层次 | 描述 | -|:----:|:----:| -| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | -| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | -| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | - -为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 - - -* 首先, 内存被划分为**结点**. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. - -* 接着各个节点又被划分为内存管理区域, 一个**管理区域**通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. - - -* 最后**页帧(page frame)**代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号. - - - -##1.2 启动过程中的内存初始化 - -在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. - -**系统启动** - -首先我们来看看start_kernel是如何初始化系统的, start_kerne定义在[init/main.c?v=4.7, line 479](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L479) - -其代码很复杂, 我们只截取出其中与内存管理初始化相关的部分, 如下所示 - - -```cpp -asmlinkage __visible void __init start_kernel(void) -{ - - setup_arch(&command_line); - mm_init_cpumask(&init_mm); - - setup_per_cpu_areas(); - - - build_all_zonelists(NULL, NULL); - page_alloc_init(); - - - /* - * These use large bootmem allocations and must precede - * mem_init(); - * kmem_cache_init(); - */ - mm_init(); - - kmem_cache_init_late(); - - kmemleak_init(); - setup_per_cpu_pageset(); - - rest_init(); -} -``` - - -| 函数 | 功能 | -|:----:|:----:| -| [setup_arch](http://lxr.free-electrons.com/ident?v=4.7;i=setup_arch) | 是一个特定于体系结构的设置函数, 其中一项任务是负责初始化自举分配器 | -| [mm_init_cpumask](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v=4.7#L522) | 初始化CPU屏蔽字 | -| [setup_per_cpu_areas](http://lxr.free-electrons.com/ident?v=4.7;i=setup_per_cpu_areas) | 函数[(查看定义)](http://lxr.free-electrons.com/source/mm/percpu.c?v4.7#L2205])给每个CPU分配内存,并拷贝.data.percpu段的数据. 为系统中的每个CPU的per_cpu变量申请空间.
在SMP系统中, setup_per_cpu_areas初始化源代码中(使用[per_cpu宏](http://lxr.free-electrons.com/source/include/linux/percpu-defs.h#L256))定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本.
此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本
在非SMP系统中这是一个空操作 | -| [build_all_zonelists](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029) | 建立并初始化结点和内存域的数据结构 | -| [mm_init](http://lxr.free-electrons.com/source/init/main.c?v4.7#L464) | 建立了内核的内存分配器,
其中通过[mem_init](http://lxr.free-electrons.com/ident?v=4.7&i=mem_init)停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)
然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器 | -| [kmem_cache_init_late](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378) | 在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器[slab](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378), [slob](http://lxr.free-electrons.com/source/mm/slob.c?v4.7#L655), [slub](http://lxr.free-electrons.com/source/mm/slub.c?v=4.7#L3960)都会定义此函数 | -| [kmemleak_init](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1857) | Kmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 [/sys/kernel/debug/kmemleak](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1467)中, Kmemcheck能够帮助定位大多数内存错误的上下文 | -| [setup_per_cpu_pageset](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5392) | 初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配
由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. | - - -##1.3 今日内容(非bootmem下的memblock内存管理) -------- - - -在引导内核的过程中, 需要使用内存, 而这个时候内核的内存管理并没有被创建, 因此也就需要一种精简的内存管理系统先接受这个工作, 而在初始化完成后, 再将旧的接口废弃, 转而使用强大的buddy系统来进行内存管理. - - -前面我们讲解了引导内存管理bootmem机制, 它基于最先适配算法, 早期的Linux内核在引导阶段都是通过bootmem来完成初期的内存管理的. 但是后来的版本(笔者分析的是3.19)开始把bootmem弃用了,`__alloc_memory_core_aarly()`取代了bootmem的`__alloc_memory_core()`来完成内存分配, 而后者其实就是调用的memblock来分配内的. - - - -memblock算法是linux内核初始化阶段的一个内存分配器,本质上是取代了原来的bootmem算法. memblock实现比较简单,而它的作用就是在page allocator初始化之前来管理内存,完成分配和释放请求. - - -为了保证系统的兼容性, 内核为bootmem和memblock提供了相同的API接口. - -这样在编译Kernel的时候可以选择nobootmem或者bootmem 来在buddy system起来之前管理memory. -这两种机制对提供的API是一致的,因此对用户是透明的 - -参见[mm/Makefile](http://lxr.free-electrons.com/source/mm/Makefile#L44) - - -```cpp -ifdef CONFIG_NO_BOOTMEM - obj-y += nobootmem.o -else - obj-y += bootmem.o -endif -``` - - -由于接口是一致的, 那么他们共同使用一份 - -| 头文件 | bootmem接口 | nobootmem接口 | -|:-------:|:----------------:|:-------------------:| -| [include/linux/bootmem.h](http://lxr.free-electrons.com/source/include/linux/bootmem.h) | [mm/bootmem.c](http://lxr.free-electrons.com/source/mm/bootmem.c) | [mm/nobootmem.c](http://lxr.free-electrons.com/source/mm/nobootmem.c) | - - -Memblock是在早期引导过程中管理内存的方法之一,此时内核内存分配器还没运行. Memblock以前被定义为Logical Memory Block( 逻辑内存块), 但根据[Yinghai Lu的补丁](https://lkml.org/lkml/2010/7/13/68), 它被重命名为memblock. - - - ->参考 -> ->[Linux 内核内存管理(1)](http://blog.jobbole.com/88452) -> ->[Linux内核初期内存管理---memblock](http://www.maxwellxxx.com/linuxmemblock) -> ->[Linux kernel memory management Part 1.](https://0xax.gitbooks.io/linux-insides/content/mm/linux-mm-1.html) - -#2 memblock的数据结构 -------- - -##2.1 struct memblock结构 -------- - - -首先来看下memblock结构的定义,文件是[include/linux/memblock.h](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7) - -```cpp -struct memblock { - bool bottom_up; /* is bottom up direction? - 如果true, 则允许由下而上地分配内存*/ - phys_addr_t current_limit; /*指出了内存块的大小限制*/ - /* 接下来的三个域描述了内存块的类型,即预留型,内存型和物理内存*/ - struct memblock_type memory; - struct memblock_type reserved; -#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP - struct memblock_type physmem; -#endif -}; -``` - -该结构体包含五个域。 - -| 字段 | 描述 | -|:---:|:----:| -| bottom_up | 表示分配器分配内存的方式
true:从低地址(内核映像的尾部)向高地址分配
false:也就是top-down,从高地址向地址分配内存. | -| current_limit | 指出了内存块的大小限制, 用于限制通过memblock_alloc的内存申请 | -| memory | 是可用内存的集合 | -| reserved | 已分配内存的集合 | -| physmem | 物理内存的集合(需要配置CONFIG_HAVE_MEMBLOCK_PHYS_MAP参数) | - -接下来的三个域描述了内存块的类型 - -* 预留型 - -* 内存型 - -* 物理内存型(需要配置宏CONFIG_HAVE_MEMBLOCK_PHYS_MAP) - - -##2.2 struct memblock_type -------- - -我们现在又接触到了一个数据结构[memblock_type](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L40), 它的定义在[include/linux/memblock.h?v=4.7, line 40](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L40) - -```cpp -struct memblock_type -{ - unsigned long cnt; /* number of regions */ - unsigned long max; /* size of the allocated array */ - phys_addr_t total_size; /* size of all regions */ - struct memblock_region *regions; -}; -``` - - -该结构体存储的是内存类型信息 - - - -| 字段 | 描述 | -|:---:|:----:| -| cnt | 当前集合(memory或者reserved)中记录的内存区域个数 | -| max | 当前集合(memory或者reserved)中可记录的内存区域的最大个数 | -| total_size | 集合记录区域信息大小 | -| regions | 内存区域结构指针 | - -它包含的域分别描述了当前内存块含有的内存区域数量, - -所有内存区域的总共大小,已经分配的内存区域大小和一个指向memblock_region结构体的数组指针 - - -##2.3 内存区域memblock_region -------- - - -memblock_region结构体描述了内存区域,它的定义在它的定义在[include/linux/memblock.h?v=4.7, line 31](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L31) - - -```cpp -struct memblock_region -{ - phys_addr_t base; - phys_addr_t size; - unsigned long flags; -#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP - int nid; -#endif -}; -``` -| 字段 | 描述 | -|:---:|:----:| -| base | 内存区域起始地址 | -| size | 内存区域大小 | -| flags | 标记 | -| nid | node号 | - - -##2.4 内存区域标识 -------- - - -memblock_region的flags字段存储了当期那内存域的标识信息, 标识用enum变量来定义, 参见[include/linux/memblock.h?v=4.7, line 23](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L23) - -```cpp - -/* Definition of memblock flags. */ -enum { - 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 */ -}; -``` - -##2.5 结构总体布局 -------- - - -图示法可以用来展示以上结构体之间的关系: - -```cpp -+---------------------------+ +---------------------------+ -| memblock | | | -| _______________________ | | | -| | memory | | | Array of the | -| | memblock_type |-|-->| membock_region | -| |_______________________| | | | -| | +---------------------------+ -| _______________________ | +---------------------------+ -| | reserved | | | | -| | memblock_type |-|-->| Array of the | -| |_______________________| | | memblock_region | -| | | | -+---------------------------+ +---------------------------+ - - - -``` -Memblock主要包含三个结构体:memblock, memblock_type和memblock_region。现在我们已了解了Memblock, 接下来我们将看到Memblock的初始化过程。 - - -##2.6 初始化memblock静态变量 -------- - - -在编译时,会分配好memblock结构所需要的内存空间, 文件是[mm/memblock.c](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L28) - -结构体memblock的初始化变量名和结构体名相同memblock - - -```cpp - -static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock; -static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_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, - - .reserved.regions = memblock_reserved_init_regions, - .reserved.cnt = 1, /* empty dummy entry */ - .reserved.max = INIT_MEMBLOCK_REGIONS, - -#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP - .physmem.regions = memblock_physmem_init_regions, - .physmem.cnt = 1, /* empty dummy entry */ - .physmem.max = INIT_PHYSMEM_REGIONS, -#endif - - .bottom_up = false, - .current_limit = MEMBLOCK_ALLOC_ANYWHERE, -}; -``` - - - -**__initdata_memblock宏指定存储位置** - -我们可以注意到初始化使用了__initdata_memblock宏,它的定义在[include/linux/memblock.h?v=4.7, line 64](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L64), 如下所示 - -```cpp -#ifdef CONFIG_ARCH_DISCARD_MEMBLOCK -#define __init_memblock __meminit -#define __initdata_memblock __meminitdata -#else -#define __init_memblock -#define __initdata_memblock -#endif -``` - -如果启用`CONFIG_ARCH_DISCARD_MEMBLOCK`宏配置选项,memblock代码会被放到.init代码段, 在内核启动完成后 memblock代码会从.init代码段释放。 - - -**3个memblock_type的初始化** - - -接下来的是memblock结构体中3个memblock_type类型数据 **memory**, **reserved**和**physmem**的初始化 - - - -它们的**memblock_typecnt域**(当前集合中区域个数)被初始化为1. -**memblock_typemax域**(当前集合中最大区域个数)被初始化为`INIT_MEMBLOCK_REGIONS`和`INIT_PHYSMEM_REGIONS` - - - -其中`INIT_MEMBLOCK_REGIONS`为128, 参见[include/linux/memblock.h?v=4.7, line 20](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L20) - -```cpp -#define INIT_MEMBLOCK_REGIONS 128 -#define INIT_PHYSMEM_REGIONS 4 -``` - -而**memblock_type.regions**域都是通过memblock_region数组初始化的, 所有的数组定义都带有__initdata_memblock宏 - - -memblock结构体中最后两个域**bottom_up**内存分配模式被禁用(bottom_up = false, 因此内存分配方式为top-down.), 当前 Memblock的大小限制是`MEMBLOCK_ALLOC_ANYWHERE`为~(phys_addr_t)0即为0xffffffff. - -```cpp -/* Flags for memblock_alloc_base() amd __memblock_alloc_base() */ -#define MEMBLOCK_ALLOC_ANYWHERE (~(phys_addr_t)0) -#define MEMBLOCK_ALLOC_ACCESSIBLE 0 -``` - - - - - - -#3 Memblock-API函数接口 -------- - - -##3.1 Memblock-API函数接口 -------- - - -既然内核静态创建并初始化了__initdata_memblock这个变量, 那么memblock又是怎么运作的呢? - - -在上文中我提到过所有关于memblock的实现都在[mm/memblock.c](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7)源文件中 - - -抛开其他的先不谈,如果要使用memblock,最上层函数一共就4个 - -```cpp -///////////////////////////////////////// -// 基本接口 -///////////////////////////////////////// -// 向memory区中添加内存区域. -memblock_add(phys_addr_t base, phys_addr_t size) - -// 向memory区中删除区域. -memblock_remove(phys_addr_t base, phys_addr_t size) - -// 申请内存 -memblock_alloc(phys_addr_t size, phys_addr_t align) - - -// 释放内存 -memblock_free(phys_addr_t base, phys_addr_t size) - - -///////////////////////////////////////// -// 查找 & 遍历 -///////////////////////////////////////// -// 在给定的范围内找到未使用的内存 -phys_addr_t memblock_find_in_range(phys_addr_t start, phys_addr_t end, phys_addr_t size, phys_addr_t align) - -// 反复迭代 memblock -for_each_mem_range(i, type_a, type_b, nid, flags, p_start, p_end, p_nid) - - - - -///////////////////////////////////////// -// 获取信息 -///////////////////////////////////////// -// 获取内存区域信息 -phys_addr_t get_allocated_memblock_memory_regions_info(phys_addr_t *addr); -// 获取预留内存区域信息 -phys_addr_t get_allocated_memblock_reserved_regions_info(phys_addr_t *addr); - -///////////////////////////////////////// -// 获取信息 -///////////////////////////////////////// -#define memblock_dbg(fmt, ...) \ - if (memblock_debug) printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__) - -``` - - -大致翻看了一下内核代码, 发现很少使用memblock_free(),因为很多地方都是申请了内存做永久使用的. 再者,其实在内核中通过memblock_alloc来分配内存其实比较少,一般都是在调用memblock底层的一些函数来简单粗暴的分配的. - - -##3.2 memblock_add将内存区域加入到memblock中 -------- - -###3.2.1 memblock_add函数 -------- - - -memblock_add函数负责向memory区中添加内存区域, 有两个参数:物理基址和内存区域大小,并且把该内存区域添加到memblock。 - -memblock_add函数本身并没有什么, 它只是调用了[memblock_add_range函数](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L609)来完成工作, 定义在[mm/memblock.c?v=4.7, line 609](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L609) - -```cpp -int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size) -{ - memblock_dbg("memblock_add: [%#016llx-%#016llx] flags %#02lx %pF\n", - (unsigned long long)base, - (unsigned long long)base + size - 1, - 0UL, (void *)_RET_IP_); - return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0); -``` - - -memblock_add传递的参数依次是 : 内存块类型(memory), 物理基址, 内存区域大小, 最大节点数(0如果CONFIG_NODES_SHIFT没有在配置文件中设置,不然就是CONFIG_NODES_SHIFT)和标志 - - - -###3.2.2 memblock_add_range函数代码 -------- - - - -memblock_add_range函数添加新的内存区域到内存块中, 定义在[mm/memblock.c?v=4.7, line 504](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L504) - - - - -* 首先,该函数检查给定的内存区域大小, 如果是0就返回. - -* 在这之后, memblock_add_range用给定的memblock_type检查memblock结构体中是否存在内存区域 - -* 如果没有,我们就用给定的值填充新的memory_region然后返回 - -* 如果memblock_type不为空,我们就把新的内存区域添加到memblock_type类型的memblock中。 - -```cpp -/** - * memblock_add_range - add new memblock region - * @type: memblock type to add new region into - * @base: base address of the new region - * @size: size of the new region - * @nid: nid of the new region - * @flags: flags of the new region - * - * Add new memblock region [@base,@base+@size) into @type. The new region - * is allowed to overlap with existing ones - overlaps don't affect already - * existing regions. @type is guaranteed to be minimal (all neighbouring - * compatible regions are merged) after the addition. - * - * RETURNS: - * 0 on success, -errno on failure. - */ -int __init_memblock memblock_add_range(struct memblock_type *type, - phys_addr_t base, phys_addr_t size, - int nid, unsigned long flags) -{ - bool insert = false; - phys_addr_t obase = base; - /* 获取内存区域的结束位置, - * memblock_cap_size函数会设置size大小确保base + size不会溢出 */ - phys_addr_t end = base + memblock_cap_size(base, &size); - int idx, nr_new; - struct memblock_region *rgn; - - if (!size) - return 0; - - /* special case for empty array */ - if (type->regions[0].size == 0) { - WARN_ON(type->cnt != 1 || type->total_size); - type->regions[0].base = base; - type->regions[0].size = size; - type->regions[0].flags = flags; - memblock_set_region_node(&type->regions[0], nid); - type->total_size = size; - return 0; - } -repeat: - /* - * The following is executed twice. Once with %false @insert and - * then with %true. The first counts the number of regions needed - * to accomodate the new area. The second actually inserts them. - */ - base = obase; - nr_new = 0; - - for_each_memblock_type(type, rgn) { - phys_addr_t rbase = rgn->base; - phys_addr_t rend = rbase + rgn->size; - - if (rbase >= end) - break; - if (rend <= base) - continue; - /* - * @rgn overlaps. If it separates the lower part of new - * area, insert that portion. - */ - if (rbase > base) { -#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP - WARN_ON(nid != memblock_get_region_node(rgn)); -#endif - WARN_ON(flags != rgn->flags); - nr_new++; - if (insert) - memblock_insert_region(type, idx++, base, - rbase - base, nid, - flags); - } - /* area below @rend is dealt with, forget about it */ - base = min(rend, end); - } - - /* insert the remaining portion */ - if (base < end) { - nr_new++; - if (insert) - memblock_insert_region(type, idx, base, end - base, - nid, flags); - } - - /* - * If this was the first round, resize array and repeat for actual - * insertions; otherwise, merge and return. - */ - if (!insert) { - while (type->cnt + nr_new > type->max) - if (memblock_double_array(type, obase, size) < 0) - return -ENOMEM; - insert = true; - goto repeat; - } else { - memblock_merge_regions(type); - return 0; - } -} -``` - -###3.2.3 memblock_add_range函数流程解析 -------- - - -首先,我们用如下代码获得内存区域的结束位置: - -```cpp -phys_addr_t end = base + memblock_cap_size(base, &size); -``` - -memblock_cap_size函数会设置size大小确保base + size不会溢出。该函数实现相当简单, 参见[mm/memblock.c?v=4.7, line 79](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L79) - - -```cpp -/* adjust *@size so that (@base + *@size) doesn't overflow, return new size */ -static inline phys_addr_t memblock_cap_size(phys_addr_t base, phys_addr_t *size) -{ - return *size = min(*size, (phys_addr_t)ULLONG_MAX - base); -} -``` - - -memblock_cap_size返回size和ULLONG_MAX - base中的最小值 - -在那之后我们得到了新的内存区域的结束地址, 然后 - -* 查内存区域是否重叠 - -* 将新的添加到memblock, 并且看是否能和已经添加到memblock中的内存区域进行合并 - -首先遍历所有已经存储的内存区域并检查有没有和新的内存区域重叠 - - -```cpp -for_each_memblock_type(type, rgn) { - phys_addr_t rbase = rgn->base; - phys_addr_t rend = rbase + rgn->size; - - if (rbase >= end) - break; - if (rend <= base) - continue; - /* ...... */ - /* area below @rend is dealt with, forget about it */ - base = min(rend, end); -} -``` - -如果新内存区域没有和已经存储在memblock的内存区域重叠, 把该新内存区域插入到memblock中. 如果有重叠通通过一个小巧的来完成冲突处理 - -```cpp -base = min(rend, end); -``` - - - -重叠检查完毕后, 新的内存区域已经是一块干净的不包含重叠区域的内存, 把新的内存区域插入到memblock中包含两步: - -* 把新的内存区域中非重叠的部分作为独立的区域加入到memblock - -* 合并所有相邻的内存区域 - -这个过程分为两次循环来完成, 由一个[标识变量insert](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L582)和[report代码跳转标签](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L543)控制 - -* 第一次循环的时候, 检查新内存区域是否可以放入内存块中并调用memblock_double_array, 而由于[insert = false](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L524), 则执行[!insert条件语句标记的代码块](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L591), 并设置[insert = true](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L595), 然后[goto 跳转到report标签](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L596)继续开始第二次循环 - -* 第二次循环中, insert = true, 则执行相应的insert == true的代码块, 并且执行[memblock_insert_region将新内存区域插入](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L583), 最后执行[memblock_merge_regions(type)](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L598)合并内存区域 - -这是第一次循环, 我们需要检查新内存区域是否可以放入内存块中并调用memblock_double_array: - -```cpp -/* - * If this was the first round, resize array and repeat for actual - * insertions; otherwise, merge and return. - */ -if (!insert) { /* 第一次执行的的时候insert == false */ - while (type->cnt + nr_new > type->max) - if (memblock_double_array(type, obase, size) < 0) - return -ENOMEM; - insert = true; - goto repeat; -} else { - /* ...... */ -} -``` -memblock_double_array函数加倍给定的内存区域大小,然后把insert设为true再转到repeat标签. - -第二次循环,从repeat标签开始经过同样的循环然后用memblock_insert_region函数把当前内存区域插入到内存块: - -```cpp -/* insert the remaining portion */ -if (base < end) { - nr_new++; - if (insert) - memblock_insert_region(type, idx, base, end - base, - nid, flags); -} -``` - -由于我们在第一次循环中把insert设为true, 现在memblock_insert_region函数将会被调用 - - -[memblock_insert_region函数](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L476)几乎和把新内存区域插入到空的memblock_type代码块有同样的实现, 定义在[mm/memblock.c?v=4.7, line 476](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L476)该函数获得最后一个内存区域: - -```cpp -struct memblock_region *rgn = &type->regions[idx]; -``` - -然后调用memmove函数移动该内存区域: - -```cpp -memmove(rgn + 1, rgn, (type->cnt - idx) * sizeof(*rgn)); -``` - -紧接着填充新内存区域memblock_region的base域,size域等等, 然后增大memblock_type的大小。 - -最后memblock_add_range函数调用[memblock_merge_regions](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L444)合并所有相邻且兼容的内存区域, 定义在[mm/memblock.c?v=4.7, line 444](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L444) - - -```cpp -/* - * If this was the first round, resize array and repeat for actual - * insertions; otherwise, merge and return. - */ -if (!insert) { - /* ...... */ -} else { - memblock_merge_regions(type); - return 0; -} -``` - -##3.3 memblock_remove删除内存区域 -------- - - - -memblock_remove用来完成删除内存区域的工作, 该函数定义在[mm/memblock.c?v=4.7, line 710](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L710) - -```cpp -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L710 -int __init_memblock memblock_remove(phys_addr_t base, phys_addr_t size) -{ - return memblock_remove_range(&memblock.memory, base, size); -} -``` - - -##3.4 memblock_alloc申请内存 -------- - - -而相比来说, 申请内存的函数memblock_alloc实现方式就比较麻烦了, 如下所示 - -`emblock_alloc(phys_addr_t size, phys_addr_t align`)其实就是在当前NODE在内存范围0-MEMBLOCK_ALLOC_ACCESSIBLE(其实是current_limit)中分配一个大小为size的内存区域. - - -###3.4.1 memblock_alloc函数代码 -------- - - -```cpp -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L727 -phys_addr_t __init memblock_alloc(phys_addr_t size, phys_addr_t align) -{ - return memblock_alloc_base(size, align, MEMBLOCK_ALLOC_ACCESSIBLE); -} - - -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1192 -phys_addr_t __init memblock_alloc_base(phys_addr_t size, phys_addr_t align, phys_addr_t max_addr) -{ - phys_addr_t alloc; - - alloc = __memblock_alloc_base(size, align, max_addr); - - if (alloc == 0) - panic("ERROR: Failed to allocate 0x%llx bytes below 0x%llx.\n", - (unsigned long long) size, (unsigned long long) max_addr); - - return alloc; -} - -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1186 -phys_addr_t __init __memblock_alloc_base(phys_addr_t size, phys_addr_t align, phys_addr_t max_addr) -{ - return memblock_alloc_base_nid(size, align, max_addr, NUMA_NO_NODE, - MEMBLOCK_NONE); -} - -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1163 -phys_addr_t __init __memblock_alloc_base(phys_addr_t size, phys_addr_t align, phys_addr_t max_addr) -{ - return memblock_alloc_base_nid(size, align, max_addr, NUMA_NO_NODE, - MEMBLOCK_NONE); -} -``` -memblock_alloc()很粗暴的从能用的内存里分配, 而有些情况下需要从特定的内存范围内分配内存. 解决方法就是通过memblock_alloc_range_nid函数或者实现类似机制的函数 - -最终memblock_alloc的也是通过memblock_alloc_range_nid函数来完成内存分配的 - - - -###3.4.2 memblock_alloc_range_nid函数 -------- - - -下面我们就来看看memblock_alloc_range_nid函数的实现, 该函数定义在[mm/memblock.c?v=4.7, line 1133](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1133) - -```cpp -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1133 -static phys_addr_t __init memblock_alloc_range_nid(phys_addr_t size, - phys_addr_t align, phys_addr_t start, - phys_addr_t end, int nid, ulong flags) -{ - phys_addr_t found; - - if (!align) - align = SMP_CACHE_BYTES; - - found = memblock_find_in_range_node(size, align, start, end, nid, - flags); - if (found && !memblock_reserve(found, size)) { - /* - * The min_count is set to 0 so that memblock allocations are - * never reported as leaks. - */ - kmemleak_alloc(__va(found), size, 0, 0); - return found; - } - return 0; -} -``` - - -memblock_alloc_range_nid函数的主要工作如下 - -* 首先使用memblock_find_in_range_node指定内存区域和大小查找内存区域 - -* memblock_reserve后将其标为已经分配 - - -###3.4.3 memblock_find_in_range_node函数 -------- - -该函数定义在[mm/memblock.c?v=4.7, lien 178](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L178) - - -```cpp -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L178 -phys_addr_t __init_memblock memblock_find_in_range_node(phys_addr_t size, - phys_addr_t align, phys_addr_t start, - phys_addr_t end, int nid, ulong flags) -{ - phys_addr_t kernel_end, ret; - - /* pump up @end */ - if (end == MEMBLOCK_ALLOC_ACCESSIBLE) - end = memblock.current_limit; - - /* avoid allocating the first page */ - start = max_t(phys_addr_t, start, PAGE_SIZE); - end = max(start, end); - kernel_end = __pa_symbol(_end); - - /* - * try bottom-up allocation only when bottom-up mode - * is set and @end is above the kernel image. - */ - if (memblock_bottom_up() && end > kernel_end) { - phys_addr_t bottom_up_start; - - /* make sure we will allocate above the kernel */ - bottom_up_start = max(start, kernel_end); - - /* ok, try bottom-up allocation first */ - ret = __memblock_find_range_bottom_up(bottom_up_start, end, - size, align, nid, flags); - if (ret) - return ret; - - /* - * we always limit bottom-up allocation above the kernel, - * but top-down allocation doesn't have the limit, so - * retrying top-down allocation may succeed when bottom-up - * allocation failed. - * - * bottom-up allocation is expected to be fail very rarely, - * so we use WARN_ONCE() here to see the stack trace if - * fail happens. - */ - WARN_ONCE(1, "memblock: bottom-up allocation failed, memory hotunplug may be affected\n"); - } - - return __memblock_find_range_top_down(start, end, size, align, nid, - flags); -} -``` -* 如果从memblock_alloc过来, end就是MEMBLOCK_ALLOC_ACCESSIBLE,这个时候会设置为current_limit. - -* 如果不通过memblock_alloc分配, 内存范围就是指定的范围. 紧接着对start做调整,为的是避免申请到第一个页面 - -memblock_bottom_up返回的是memblock.bottom_up,前面初始化的时候也知道这个值是false(在numa初始化时会设置为true),所以初始化前期应该调用的是__memblock_find_range_top_down函数去查找内存: - - - - -###3.4.4 __memblock_find_range_top_down查找内存区域 -------- - -最后通过[__memblock_find_range_top_down](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L140)函数去查找内存 - - -```cpp -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L140 -static phys_addr_t __init_memblock -__memblock_find_range_top_down(phys_addr_t start, phys_addr_t end, - phys_addr_t size, phys_addr_t align, int nid, - ulong flags) -{ - phys_addr_t this_start, this_end, cand; - u64 i; - - for_each_free_mem_range_reverse(i, nid, flags, &this_start, &this_end, - NULL) { - this_start = clamp(this_start, start, end); - this_end = clamp(this_end, start, end); - - if (this_end < size) - continue; - - cand = round_down(this_end - size, align); - if (cand >= this_start) - return cand; - } - - return 0; -} -``` - - -* 函数通过使用for_each_free_mem_range_reverse宏封装调用__next_free_mem_range_rev()函数,此函数逐一将memblock.memory里面的内存块信息提取出来与memblock.reserved的各项信息进行检验,确保返回的this_start和this_end不会是分配过的内存块。 - -* 然后通过clamp取中间值,判断大小是否满足,满足的情况下,将自末端向前(因为这是top-down申请方式)的size大小的空间的起始地址(前提该地址不会超出this_start)返回回去 - -至此满足要求的内存块算是找到了。 - - -###3.4.5 memblock_reserve标记申请的内存 -------- - - -现在我们回到[memblock_alloc_range_nid函数](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1133), 我们说该函数完成了两项工作 - -* 首先通过memblock_find_in_range_node指定内存区域和大小查找内存区域 - -* 找到内存区域后, 调用memblock_reserve后将其标为已经分配 - -现在我们已经找到了内存区域了, 那么我们继续看看memblock_reserve函数是如何堆内存进行标记的, 该函数定义在[mm/memblock.c?v=4.7, line 727](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L727) - -```cpp -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L727 -int __init_memblock memblock_reserve(phys_addr_t base, phys_addr_t size) -{ - memblock_dbg("memblock_reserve: [%#016llx-%#016llx] flags %#02lx %pF\n", - (unsigned long long)base, - (unsigned long long)base + size - 1, - 0UL, (void *)_RET_IP_); - - return memblock_add_range(&memblock.reserved, base, size, MAX_NUMNODES, 0); -} - -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L609 -int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size) -{ - memblock_dbg("memblock_add: [%#016llx-%#016llx] flags %#02lx %pF\n", - (unsigned long long)base, - (unsigned long long)base + size - 1, - 0UL, (void *)_RET_IP_); - - return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0); -} -``` - -我们会发现首先memblock_reserve函数也是通过memblock_add_range来实现的, 我们把memblock_add的实现贴出来进行对比, 我们会发现他们就第一个参数不一样 - -* memblock_reserve使用全局变量memblock的reserved域, 最终将分配到的内存块信息添加到reserved区域中 - -* emblock_add则使用了全局变量的memory域, 最终将内存块添加到了memory区域 - - -memblock_add_range函数的流程我们前面已经将的很详细了, 这里只简单的叙述一下子 - -* 如果memblock算法管理内存为空的时候,则将当前空间添加进去 - -* 不为空的情况下,则先检查是否存在内存重叠的情况,如果有的话,则剔除重叠部分,然后将其余非重叠的部分添加进去 - -* 如果出现region[]数组空间不够的情况,则通过memblock_double_array()添加新的region[]空间 - -* 最后通过memblock_merge_regions()把紧挨着的内存合并了 - - - - - -##2.5 memblock_free释放内存区域 -------- - - -```cpp -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L695 -static int __init_memblock memblock_remove_range(struct memblock_type *type, - phys_addr_t base, phys_addr_t size) -{ - int start_rgn, end_rgn; - int i, ret; - - ret = memblock_isolate_range(type, base, size, &start_rgn, &end_rgn); - if (ret) - return ret; - - for (i = end_rgn - 1; i >= start_rgn; i--) - memblock_remove_region(type, i); -} - -// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L716 -int __init_memblock memblock_free(phys_addr_t base, phys_addr_t size) -{ - memblock_dbg(" memblock_free: [%#016llx-%#016llx] %pF\n", - (unsigned long long)base, - (unsigned long long)base + size - 1, - (void *)_RET_IP_); - - kmemleak_free_part(__va(base), size); - return memblock_remove_range(&memblock.reserved, base, size); -} -``` -#3 兼容bootmem的接口API -------- - - - -##3.1 memblock与bootmem -------- - -我们了解到memblock是作为bootmem的替代品而引入内核的 - -在编译Kernel的时候可以选择nobootmem或者bootmem 来在buddy system起来之前管理memory. -这两种机制对提供的API是一致的,因此对用户是透明的 - -参见[mm/Makefile](http://lxr.free-electrons.com/source/mm/Makefile#L44) - - -```cpp -ifdef CONFIG_NO_BOOTMEM - obj-y += nobootmem.o -else - obj-y += bootmem.o -endif -``` - - -为了保证系统的兼容性, 内核为bootmem和memblock提供了相同的API接口. - - - -##3.2 memblock与bootmem接口对比 -------- - - -由于接口是一致的, 那么他们共同使用一份 - -| 头文件 | bootmem接口 | nobootmem接口 | -|:-------:|:----------------:|:-------------------:| -| [include/linux/bootmem.h](http://lxr.free-electrons.com/source/include/linux/bootmem.h) | [mm/bootmem.c](http://lxr.free-electrons.com/source/mm/bootmem.c) | [mm/nobootmem.c](http://lxr.free-electrons.com/source/mm/nobootmem.c) | - - - -
-我们知道memblock自己的接口都在自己的头文件和源文件中 - -| memblock 头文件 | memblock接口 | -|:--------------:|:------------:| -| [include/linux/memblock.h](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7) | [mm/memblock.c](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7) - - -为了实现接口兼容, 内核用[mm/memblock.c](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7)定义的memblock接口, 实现了一套bootmem的接口机制, 而bootmem的接口我们在上一篇引导内存管理bootmem机制中已经讲过了, 这些实现的bootmem函数接口API, 就定义在[mm/nobootmem.c](http://lxr.free-electrons.com/source/mm/nobootmem.c)文件中, 然后内核把他们进行了封装, 然后提供了与bootmem相同功能和函数的接口, 这些接口都在[include/linux/memblock.h](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7). - - - -在NUMA系统上, 基本的API是相同的, 但是函数增加了_node后缀, 与UMA系统的函数相比, 还需要一些额外的参数, 用于指定内存分配的结点. - - -| 函数 | bootmem定义 | nobootmem定义 | -|:-----:|:------------------:|:----------------------:| -| ZONE_NORMAL的分配函数 | 按照指定大小在ZONE_NORMAL内存域分配函数. 数据是对齐的, 这使得内存或者从可适用于L1高速缓存的理想位置开始| | -| alloc_bootmem(size) | [alloc_bootmem](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L122)
[__alloc_bootmem](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L700)
[___alloc_bootmem](http://lxr.free-electrons.com/source/mm/bootmem.c?=4.7#L672) | [alloc_bootmem](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L122)
[__alloc_bootmem](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L309)
[___alloc_bootmem](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L281) | -| alloc_bootmem_align(size) | [alloc_bootmem_align](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L124)
基于__alloc_bootmem实现 | [alloc_bootmem_align](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L124)
[__alloc_bootmem](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L309)
[___alloc_bootmem](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L281) | -| alloc_bootmem_pages(size)) | [alloc_bootmem_pages](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L128)
基于__alloc_bootmem实现 | [alloc_bootmem_pages](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L128)
基于__alloc_bootmem实现 | -| alloc_bootmem_nopanic(size) | [alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L126)
[__alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L664)
[___alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L632) | [alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L126)
[__alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L261)
[___alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L235) | -| ZONE_DMA区域的分配函数 | | -| alloc_bootmem_low(size) | [alloc_bootmem_low](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L141)
[__alloc_bootmem_low](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L415)底层基于___alloc_bootmem | [alloc_bootmem_low](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L141)
底层基于___alloc_bootmem | -| alloc_bootmem_low_pages_nopanic(size) | [alloc_bootmem_low_pages_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L143)
底层基于[__alloc_bootmem_low_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L838)
[___alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L632) | [alloc_bootmem_low_pages_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L143)
底层基于[__alloc_bootmem_low_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L421)
[___alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L235) | -| alloc_bootmem_low_pages(size) | [alloc_bootmem_low_pages](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L832)
底层基于[__alloc_bootmem_low_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L838) | [alloc_bootmem_low_pages](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L832)
底层基于[__alloc_bootmem_low_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L421) | -| NUMA结构的分配函数 | | | -| alloc_bootmem_node(pgdat, size) | [alloc_bootmem_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L132)
[__alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L777)
[ ___alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L708) | [alloc_bootmem_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L132)
[__alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L383)
[ ___alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | -| alloc_bootmem_node_nopanic(pgdat, size) | [alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L134)
[__alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L738)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L708) | [alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L134)
[__alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L344)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | -| alloc_bootmem_pages_node(pgdat, size) | [alloc_bootmem_pages_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L136)
[__alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L747)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L708) | [alloc_bootmem_pages_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L136)
[__alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L344)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | -| alloc_bootmem_pages_node_nopanic(pgdat, size) | [alloc_bootmem_pages_node_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L138)
[__alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L344)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | -| alloc_bootmem_low_pages_node(pgdat, size) | [alloc_bootmem_low_pages_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L14)
[__alloc_bootmem_low_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L861)[___alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L747)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L707) | [alloc_bootmem_low_pages_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L14)
[__alloc_bootmem_low_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L444)[___alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L353)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | - - -我们可以看到最基本的实现思路都是一样的, 只是最底层的实现函数有细微的区别. - - -##3.3 实现差异 -------- - - -* UMA结构下这些函数最终都是通过___alloc_bootmem_nopanic函数来实现的 - -* NUMA架构下, 最终这些函数都是简介的调用___alloc_bootmem_node_nopanic函数来实现的, - - - - -| 函数 | bootmem | memblock | -|:-----:|:-----------:|:------------:| -| ___alloc_bootmem_nopanic | [mm/bootmem.c?v=4.7, line 632](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L632), 通过[alloc_bootmem_core](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L607)函数来实现 | [mm/nobootmem.c?v=4.7, line 235](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L235), 通过[__alloc_memory_core_early](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L36) | -| ___alloc_bootmem_node_nopanic | [mm/bootmem.c?v=4.7, line 708](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L708), 通过调用[alloc_bootmem_core](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L607)和[alloc_bootmem_bdata](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L500)来实现 | [mm/nobootmem.c?v=4.7, line 317](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317), 通过调用[__alloc_memory_core_early](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L36)来实现 | - - -bootmem的核心函数__alloc_memory_core()的实现机制我们前一篇博文[引导分配器bootmem](待添加链接)已经讲过了, 那么memblock下nobootmem的核心函数__alloc_memory_core_early是怎么实现的呢? - -前面[2.4.1节memblock_alloc函数代码](待添加链接)我们分析memblock_alloc函数的时候提到, 该函数最终通过memblock_alloc_range_nid函数粗暴粗暴的进行内存分配, 而有些情况下需要从特定的内存范围内分配内存. 解决方法就是通过memblock_alloc_range_nid函数或者实现类似机制的函数, 这里的__alloc_memory_core_early函数就是基于memblock_alloc_range_nid同样的思路实现的函数 - - -* 首先使用memblock_find_in_range_node指定内存区域和大小查找内存区域 - -* memblock_reserve后将其标为已经分配 - - -我们列出 memblock_alloc_range_nid函数与__alloc_memory_core_early函数的实现对比 - -| memblock_alloc_range_nid | __alloc_memory_core_early | -|:------------------------------:|:------------------------------:| -| [mm/memblock.c?v=4.7, line 1133](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1133) | [mm/nobootmem.c?v=4.7, line 36](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L36) | - - -#4 memblock初始化 -------- - -如果从整个linux生命周期来讲,涉及到各种初始化等,这里来详细分析,因为还没有分析完内核,所以这里是分析到哪里就记录到哪里了. - -##4.1 x86架构下的memblock初始化 -------- - -要理解memblock是如何工作和实现的, 我们首先看一下它的用法. - - - -在Linux内核中有几处用到了memblock, 例如 arch/x86/kernel/e820.c中的函数`memblock_x86_fill`. 该函数遍历由`e820`提供的内存映射表并且通过`memblock_add`函数把内核预留的内存区域添加到memblock。既然我们首先遇到了`memblock_add`函数,那就从它开始吧。 - - -在内核初始化初期,物理内存会通过`Int 0x15`来被探测和整理, 存放到`e820`中.而初始化就发生在这个以后. 参见[arch/x86/kernel/setup.c?v=4.7, line 1096](http://lxr.free-electrons.com/source/arch/x86/kernel/setup.c?v=4.7#L1096) - -```cpp -void __init setup_arch(char **cmdline_p) -{ - /* - * Need to conclude brk, before memblock_x86_fill() - * it could use memblock_find_in_range, could overlap with - * brk area. - */ - reserve_brk(); - - cleanup_highmap(); - - memblock_set_current_limit(ISA_END_ADDRESS); - memblock_x86_fill(); -} -``` - -首先内核建立内核页表需要扩展__brk, 而扩展后的brk就立即被声明为已分配. 这项工作是由reserve_brk通过调用memblock_reserve完成的, 而其实并不是正真通过memblock分配的, 因为此时memblock还没有完成初始化 - -[reserve_brk](http://lxr.free-electrons.com/source/arch/x86/kernel/setup.c?v=4.9#L209)函数定义在[arch/x86/kernel/setup.c?v=4.7, line 209](http://lxr.free-electrons.com/source/arch/x86/kernel/setup.c?v=4.9#L209), 此时memblock还没有初始化, 只能通过[memblock_reserve](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L727)来完成内存的分配 - -```cpp -static void __init reserve_brk(void) -{ - if (_brk_end > _brk_start) - memblock_reserve(__pa_symbol(_brk_start), - _brk_end - _brk_start); - - /* Mark brk area as locked down and no longer taking any - new allocations */ - _brk_start = 0; -} -``` - -设置完__brk后, 可以看到,setup_arch()函数通过memblock_x86_fill(),依据e820中的信息来初始化memblock. - -```cpp -void __init memblock_x86_fill(void) -{ - int i; - u64 end; - - /* - * EFI may have more than 128 entries - * We are safe to enable resizing, beause memblock_x86_fill() - * is rather later for x86 - */ - memblock_allow_resize(); - - for (i = 0; i < e820.nr_map; i++) { - struct e820entry *ei = &e820.map[i]; - - end = ei->addr + ei->size; - if (end != (resource_size_t)end) - continue; - - if (ei->type != E820_RAM && ei->type != E820_RESERVED_KERN) - continue; - - memblock_add(ei->addr, ei->size); - } - - /* throw away partial pages */ - memblock_trim_memory(PAGE_SIZE); - - memblock_dump_all(); -} -``` - -比较简单,通过e820中的信息memblock_add(),将内存添加到memblock中的memory中,当做可分配内存.后两个函数主要是修剪内存使之对齐和输出信息. - -至此, 我们的memblock就初始化好了, 简单而且粗暴 - - - -##4.2 arm架构下的memblock初始化 -------- - - -arm下的memblock初始化也是从start_kernel()->[setup_arch()](http://lxr.free-electrons.com/source/arch/arm/kernel/setup.c#L1034)开始的, 在setup_arch()中arm架构通过arm_memblock_init完成了memblock的初始化工作. - -```cpp -void __init setup_arch(char **cmdline_p) -{ - arm_memblock_init(mdesc); -} -``` - -arm_memblock_init定义在[arch/arm/mm/init.c](http://lxr.free-electrons.com/source/arch/arm/mm/init.c?v=4.7#L230), 如下所示 - - -```cpp -void __init arm_memblock_init(const struct machine_desc *mdesc) -{ - /* Register the kernel text, kernel data and initrd with memblock. */ -#ifdef CONFIG_XIP_KERNEL - memblock_reserve(__pa(_sdata), _end - _sdata); -#else - memblock_reserve(__pa(_stext), _end - _stext); -#endif -#ifdef CONFIG_BLK_DEV_INITRD - /* FDT scan will populate initrd_start */ - if (initrd_start && !phys_initrd_size) { - phys_initrd_start = __virt_to_phys(initrd_start); - phys_initrd_size = initrd_end - initrd_start; - } - initrd_start = initrd_end = 0; - if (phys_initrd_size && - !memblock_is_region_memory(phys_initrd_start, phys_initrd_size)) { - pr_err("INITRD: 0x%08llx+0x%08lx is not a memory region - disabling initrd\n", - (u64)phys_initrd_start, phys_initrd_size); - phys_initrd_start = phys_initrd_size = 0; - } - if (phys_initrd_size && - memblock_is_region_reserved(phys_initrd_start, phys_initrd_size)) { - pr_err("INITRD: 0x%08llx+0x%08lx overlaps in-use memory region - disabling initrd\n", - (u64)phys_initrd_start, phys_initrd_size); - phys_initrd_start = phys_initrd_size = 0; - } - if (phys_initrd_size) { - memblock_reserve(phys_initrd_start, phys_initrd_size); - - /* Now convert initrd to virtual addresses */ - initrd_start = __phys_to_virt(phys_initrd_start); - initrd_end = initrd_start + phys_initrd_size; - } -#endif - - arm_mm_memblock_reserve(); - - /* reserve any platform specific memblock areas */ - if (mdesc->reserve) - mdesc->reserve(); - - early_init_fdt_reserve_self(); - early_init_fdt_scan_reserved_mem(); - - /* reserve memory for DMA contiguous allocations */ - dma_contiguous_reserve(arm_dma_limit); - - arm_memblock_steal_permitted = false; - memblock_dump_all(); -} -``` - -##4.3 arm64下的memblock初始化 -------- - -前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作. - -```cpp -void __init setup_arch(char **cmdline_p) -{ - /* 初始化memblock */ - arm64_memblock_init( ); - - /* 分页机制初始化 */ - paging_init(); - - bootmem_init(); -} -``` - -| 流程 | 描述 | -|:---:|:----:| -| [arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) | 初始化memblock内存分配器 | -| [paging_init](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538) | 初始化分页机制 | -| [bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | 初始化内存管理 | - - - - -其中arm64_memblock_init就完成了arm64架构下的memblock的初始化 - - -与arm架构类似, arm64的memblock初始化没有意外, 只是初始化函数成为[arm64_memblock_init()](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L261), 该函数定义在[arch/arm64/mm/init.c?v=4.7, line 192](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L192) - - - -#5 总结 -------- - - -memblock内存管理是将所有的物理内存放到`memblock.memory`中作为可用内存来管理, 分配过的内存只加入到`memblock.reserved`中, 并不从`memory`中移出. - -同理释放内存也会加入到`memory`中. 也就是说, `memory`在`fill`过后基本就是不动的了. 申请和分配内存仅仅修改`reserved`就达到目的. 在初始化阶段没有那么多复杂的内存操作场景, 甚至很多地方都是申请了内存做永久使用的, 所以这样的内存管理方式已经足够凑合着用了, 毕竟内核也不指望用它一辈子. 在系统完成初始化之后所有的工作会移交给强大的`buddy`系统来进行内存管理 - +的启动过程期间的内存管理--memblock分配器 +======= + + + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + +在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后内核才能检测到可用内存和寄存器. + + + + +#1 前景回顾 +------- + + +##1.1 Linux内存管理的层次结构 +------- + + +Linux把物理内存划分为三个层次来管理 + +| 层次 | 描述 | +|:----:|:----:| +| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | +| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | +| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | + +为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 + + +* 首先, 内存被划分为**结点**. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. + +* 接着各个节点又被划分为内存管理区域, 一个**管理区域**通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. + + +* 最后**页帧(page frame)**代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号. + + + +##1.2 启动过程中的内存初始化 + +在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. + +**系统启动** + +首先我们来看看start_kernel是如何初始化系统的, start_kerne定义在[init/main.c?v=4.7, line 479](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L479) + +其代码很复杂, 我们只截取出其中与内存管理初始化相关的部分, 如下所示 + + +```cpp +asmlinkage __visible void __init start_kernel(void) +{ + + setup_arch(&command_line); + mm_init_cpumask(&init_mm); + + setup_per_cpu_areas(); + + + build_all_zonelists(NULL, NULL); + page_alloc_init(); + + + /* + * These use large bootmem allocations and must precede + * mem_init(); + * kmem_cache_init(); + */ + mm_init(); + + kmem_cache_init_late(); + + kmemleak_init(); + setup_per_cpu_pageset(); + + rest_init(); +} +``` + + +| 函数 | 功能 | +|:----:|:----:| +| [setup_arch](http://lxr.free-electrons.com/ident?v=4.7;i=setup_arch) | 是一个特定于体系结构的设置函数, 其中一项任务是负责初始化自举分配器 | +| [mm_init_cpumask](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v=4.7#L522) | 初始化CPU屏蔽字 | +| [setup_per_cpu_areas](http://lxr.free-electrons.com/ident?v=4.7;i=setup_per_cpu_areas) | 函数[(查看定义)](http://lxr.free-electrons.com/source/mm/percpu.c?v4.7#L2205])给每个CPU分配内存,并拷贝.data.percpu段的数据. 为系统中的每个CPU的per_cpu变量申请空间.
在SMP系统中, setup_per_cpu_areas初始化源代码中(使用[per_cpu宏](http://lxr.free-electrons.com/source/include/linux/percpu-defs.h#L256))定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本.
此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本
在非SMP系统中这是一个空操作 | +| [build_all_zonelists](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029) | 建立并初始化结点和内存域的数据结构 | +| [mm_init](http://lxr.free-electrons.com/source/init/main.c?v4.7#L464) | 建立了内核的内存分配器,
其中通过[mem_init](http://lxr.free-electrons.com/ident?v=4.7&i=mem_init)停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)
然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器 | +| [kmem_cache_init_late](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378) | 在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器[slab](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378), [slob](http://lxr.free-electrons.com/source/mm/slob.c?v4.7#L655), [slub](http://lxr.free-electrons.com/source/mm/slub.c?v=4.7#L3960)都会定义此函数 | +| [kmemleak_init](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1857) | Kmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 [/sys/kernel/debug/kmemleak](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1467)中, Kmemcheck能够帮助定位大多数内存错误的上下文 | +| [setup_per_cpu_pageset](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5392) | 初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配
由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. | + + +##1.3 今日内容(非bootmem下的memblock内存管理) +------- + + +在引导内核的过程中, 需要使用内存, 而这个时候内核的内存管理并没有被创建, 因此也就需要一种精简的内存管理系统先接受这个工作, 而在初始化完成后, 再将旧的接口废弃, 转而使用强大的buddy系统来进行内存管理. + + +前面我们讲解了引导内存管理bootmem机制, 它基于最先适配算法, 早期的Linux内核在引导阶段都是通过bootmem来完成初期的内存管理的. 但是后来的版本(笔者分析的是3.19)开始把bootmem弃用了,`__alloc_memory_core_aarly()`取代了bootmem的`__alloc_memory_core()`来完成内存分配, 而后者其实就是调用的memblock来分配内的. + + + +memblock算法是linux内核初始化阶段的一个内存分配器,本质上是取代了原来的bootmem算法. memblock实现比较简单,而它的作用就是在page allocator初始化之前来管理内存,完成分配和释放请求. + + +为了保证系统的兼容性, 内核为bootmem和memblock提供了相同的API接口. + +这样在编译Kernel的时候可以选择nobootmem或者bootmem 来在buddy system起来之前管理memory. +这两种机制对提供的API是一致的,因此对用户是透明的 + +参见[mm/Makefile](http://lxr.free-electrons.com/source/mm/Makefile#L44) + + +```cpp +ifdef CONFIG_NO_BOOTMEM + obj-y += nobootmem.o +else + obj-y += bootmem.o +endif +``` + + +由于接口是一致的, 那么他们共同使用一份 + +| 头文件 | bootmem接口 | nobootmem接口 | +|:-------:|:----------------:|:-------------------:| +| [include/linux/bootmem.h](http://lxr.free-electrons.com/source/include/linux/bootmem.h) | [mm/bootmem.c](http://lxr.free-electrons.com/source/mm/bootmem.c) | [mm/nobootmem.c](http://lxr.free-electrons.com/source/mm/nobootmem.c) | + + +Memblock是在早期引导过程中管理内存的方法之一,此时内核内存分配器还没运行. Memblock以前被定义为Logical Memory Block( 逻辑内存块), 但根据[Yinghai Lu的补丁](https://lkml.org/lkml/2010/7/13/68), 它被重命名为memblock. + + + +>参考 +> +>[Linux 内核内存管理(1)](http://blog.jobbole.com/88452) +> +>[Linux内核初期内存管理---memblock](http://www.maxwellxxx.com/linuxmemblock) +> +>[Linux kernel memory management Part 1.](https://0xax.gitbooks.io/linux-insides/content/mm/linux-mm-1.html) + +#2 memblock的数据结构 +------- + +##2.1 struct memblock结构 +------- + + +首先来看下memblock结构的定义,文件是[include/linux/memblock.h](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7) + +```cpp +struct memblock { + bool bottom_up; /* is bottom up direction? + 如果true, 则允许由下而上地分配内存*/ + phys_addr_t current_limit; /*指出了内存块的大小限制*/ + /* 接下来的三个域描述了内存块的类型,即预留型,内存型和物理内存*/ + struct memblock_type memory; + struct memblock_type reserved; +#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP + struct memblock_type physmem; +#endif +}; +``` + +该结构体包含五个域。 + +| 字段 | 描述 | +|:---:|:----:| +| bottom_up | 表示分配器分配内存的方式
true:从低地址(内核映像的尾部)向高地址分配
false:也就是top-down,从高地址向地址分配内存. | +| current_limit | 指出了内存块的大小限制, 用于限制通过memblock_alloc的内存申请 | +| memory | 是可用内存的集合 | +| reserved | 已分配内存的集合 | +| physmem | 物理内存的集合(需要配置CONFIG_HAVE_MEMBLOCK_PHYS_MAP参数) | + +接下来的三个域描述了内存块的类型 + +* 预留型 + +* 内存型 + +* 物理内存型(需要配置宏CONFIG_HAVE_MEMBLOCK_PHYS_MAP) + + +##2.2 struct memblock_type +------- + +我们现在又接触到了一个数据结构[memblock_type](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L40), 它的定义在[include/linux/memblock.h?v=4.7, line 40](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L40) + +```cpp +struct memblock_type +{ + unsigned long cnt; /* number of regions */ + unsigned long max; /* size of the allocated array */ + phys_addr_t total_size; /* size of all regions */ + struct memblock_region *regions; +}; +``` + + +该结构体存储的是内存类型信息 + + + +| 字段 | 描述 | +|:---:|:----:| +| cnt | 当前集合(memory或者reserved)中记录的内存区域个数 | +| max | 当前集合(memory或者reserved)中可记录的内存区域的最大个数 | +| total_size | 集合记录区域信息大小 | +| regions | 内存区域结构指针 | + +它包含的域分别描述了当前内存块含有的内存区域数量, + +所有内存区域的总共大小,已经分配的内存区域大小和一个指向memblock_region结构体的数组指针 + + +##2.3 内存区域memblock_region +------- + + +memblock_region结构体描述了内存区域,它的定义在它的定义在[include/linux/memblock.h?v=4.7, line 31](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L31) + + +```cpp +struct memblock_region +{ + phys_addr_t base; + phys_addr_t size; + unsigned long flags; +#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP + int nid; +#endif +}; +``` +| 字段 | 描述 | +|:---:|:----:| +| base | 内存区域起始地址 | +| size | 内存区域大小 | +| flags | 标记 | +| nid | node号 | + + +##2.4 内存区域标识 +------- + + +memblock_region的flags字段存储了当期那内存域的标识信息, 标识用enum变量来定义, 参见[include/linux/memblock.h?v=4.7, line 23](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L23) + +```cpp + +/* Definition of memblock flags. */ +enum { + 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 */ +}; +``` + +##2.5 结构总体布局 +------- + + +图示法可以用来展示以上结构体之间的关系: + +```cpp ++---------------------------+ +---------------------------+ +| memblock | | | +| _______________________ | | | +| | memory | | | Array of the | +| | memblock_type |-|-->| membock_region | +| |_______________________| | | | +| | +---------------------------+ +| _______________________ | +---------------------------+ +| | reserved | | | | +| | memblock_type |-|-->| Array of the | +| |_______________________| | | memblock_region | +| | | | ++---------------------------+ +---------------------------+ + + + +``` +Memblock主要包含三个结构体:memblock, memblock_type和memblock_region。现在我们已了解了Memblock, 接下来我们将看到Memblock的初始化过程。 + + +##2.6 初始化memblock静态变量 +------- + + +在编译时,会分配好memblock结构所需要的内存空间, 文件是[mm/memblock.c](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L28) + +结构体memblock的初始化变量名和结构体名相同memblock + + +```cpp + +static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock; +static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_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, + + .reserved.regions = memblock_reserved_init_regions, + .reserved.cnt = 1, /* empty dummy entry */ + .reserved.max = INIT_MEMBLOCK_REGIONS, + +#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP + .physmem.regions = memblock_physmem_init_regions, + .physmem.cnt = 1, /* empty dummy entry */ + .physmem.max = INIT_PHYSMEM_REGIONS, +#endif + + .bottom_up = false, + .current_limit = MEMBLOCK_ALLOC_ANYWHERE, +}; +``` + + + +**__initdata_memblock宏指定存储位置** + +我们可以注意到初始化使用了__initdata_memblock宏,它的定义在[include/linux/memblock.h?v=4.7, line 64](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L64), 如下所示 + +```cpp +#ifdef CONFIG_ARCH_DISCARD_MEMBLOCK +#define __init_memblock __meminit +#define __initdata_memblock __meminitdata +#else +#define __init_memblock +#define __initdata_memblock +#endif +``` + +如果启用`CONFIG_ARCH_DISCARD_MEMBLOCK`宏配置选项,memblock代码会被放到.init代码段, 在内核启动完成后 memblock代码会从.init代码段释放。 + + +**3个memblock_type的初始化** + + +接下来的是memblock结构体中3个memblock_type类型数据 **memory**, **reserved**和**physmem**的初始化 + + + +它们的**memblock_typecnt域**(当前集合中区域个数)被初始化为1. +**memblock_typemax域**(当前集合中最大区域个数)被初始化为`INIT_MEMBLOCK_REGIONS`和`INIT_PHYSMEM_REGIONS` + + + +其中`INIT_MEMBLOCK_REGIONS`为128, 参见[include/linux/memblock.h?v=4.7, line 20](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7#L20) + +```cpp +#define INIT_MEMBLOCK_REGIONS 128 +#define INIT_PHYSMEM_REGIONS 4 +``` + +而**memblock_type.regions**域都是通过memblock_region数组初始化的, 所有的数组定义都带有__initdata_memblock宏 + + +memblock结构体中最后两个域**bottom_up**内存分配模式被禁用(bottom_up = false, 因此内存分配方式为top-down.), 当前 Memblock的大小限制是`MEMBLOCK_ALLOC_ANYWHERE`为~(phys_addr_t)0即为0xffffffff. + +```cpp +/* Flags for memblock_alloc_base() amd __memblock_alloc_base() */ +#define MEMBLOCK_ALLOC_ANYWHERE (~(phys_addr_t)0) +#define MEMBLOCK_ALLOC_ACCESSIBLE 0 +``` + + + + + + +#3 Memblock-API函数接口 +------- + + +##3.1 Memblock-API函数接口 +------- + + +既然内核静态创建并初始化了__initdata_memblock这个变量, 那么memblock又是怎么运作的呢? + + +在上文中我提到过所有关于memblock的实现都在[mm/memblock.c](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7)源文件中 + + +抛开其他的先不谈,如果要使用memblock,最上层函数一共就4个 + +```cpp +///////////////////////////////////////// +// 基本接口 +///////////////////////////////////////// +// 向memory区中添加内存区域. +memblock_add(phys_addr_t base, phys_addr_t size) + +// 向memory区中删除区域. +memblock_remove(phys_addr_t base, phys_addr_t size) + +// 申请内存 +memblock_alloc(phys_addr_t size, phys_addr_t align) + + +// 释放内存 +memblock_free(phys_addr_t base, phys_addr_t size) + + +///////////////////////////////////////// +// 查找 & 遍历 +///////////////////////////////////////// +// 在给定的范围内找到未使用的内存 +phys_addr_t memblock_find_in_range(phys_addr_t start, phys_addr_t end, phys_addr_t size, phys_addr_t align) + +// 反复迭代 memblock +for_each_mem_range(i, type_a, type_b, nid, flags, p_start, p_end, p_nid) + + + + +///////////////////////////////////////// +// 获取信息 +///////////////////////////////////////// +// 获取内存区域信息 +phys_addr_t get_allocated_memblock_memory_regions_info(phys_addr_t *addr); +// 获取预留内存区域信息 +phys_addr_t get_allocated_memblock_reserved_regions_info(phys_addr_t *addr); + +///////////////////////////////////////// +// 获取信息 +///////////////////////////////////////// +#define memblock_dbg(fmt, ...) \ + if (memblock_debug) printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__) + +``` + + +大致翻看了一下内核代码, 发现很少使用memblock_free(),因为很多地方都是申请了内存做永久使用的. 再者,其实在内核中通过memblock_alloc来分配内存其实比较少,一般都是在调用memblock底层的一些函数来简单粗暴的分配的. + + +##3.2 memblock_add将内存区域加入到memblock中 +------- + +###3.2.1 memblock_add函数 +------- + + +memblock_add函数负责向memory区中添加内存区域, 有两个参数:物理基址和内存区域大小,并且把该内存区域添加到memblock。 + +memblock_add函数本身并没有什么, 它只是调用了[memblock_add_range函数](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L609)来完成工作, 定义在[mm/memblock.c?v=4.7, line 609](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L609) + +```cpp +int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size) +{ + memblock_dbg("memblock_add: [%#016llx-%#016llx] flags %#02lx %pF\n", + (unsigned long long)base, + (unsigned long long)base + size - 1, + 0UL, (void *)_RET_IP_); + return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0); +``` + + +memblock_add传递的参数依次是 : 内存块类型(memory), 物理基址, 内存区域大小, 最大节点数(0如果CONFIG_NODES_SHIFT没有在配置文件中设置,不然就是CONFIG_NODES_SHIFT)和标志 + + + +###3.2.2 memblock_add_range函数代码 +------- + + + +memblock_add_range函数添加新的内存区域到内存块中, 定义在[mm/memblock.c?v=4.7, line 504](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L504) + + + + +* 首先,该函数检查给定的内存区域大小, 如果是0就返回. + +* 在这之后, memblock_add_range用给定的memblock_type检查memblock结构体中是否存在内存区域 + +* 如果没有,我们就用给定的值填充新的memory_region然后返回 + +* 如果memblock_type不为空,我们就把新的内存区域添加到memblock_type类型的memblock中。 + +```cpp +/** + * memblock_add_range - add new memblock region + * @type: memblock type to add new region into + * @base: base address of the new region + * @size: size of the new region + * @nid: nid of the new region + * @flags: flags of the new region + * + * Add new memblock region [@base,@base+@size) into @type. The new region + * is allowed to overlap with existing ones - overlaps don't affect already + * existing regions. @type is guaranteed to be minimal (all neighbouring + * compatible regions are merged) after the addition. + * + * RETURNS: + * 0 on success, -errno on failure. + */ +int __init_memblock memblock_add_range(struct memblock_type *type, + phys_addr_t base, phys_addr_t size, + int nid, unsigned long flags) +{ + bool insert = false; + phys_addr_t obase = base; + /* 获取内存区域的结束位置, + * memblock_cap_size函数会设置size大小确保base + size不会溢出 */ + phys_addr_t end = base + memblock_cap_size(base, &size); + int idx, nr_new; + struct memblock_region *rgn; + + if (!size) + return 0; + + /* special case for empty array */ + if (type->regions[0].size == 0) { + WARN_ON(type->cnt != 1 || type->total_size); + type->regions[0].base = base; + type->regions[0].size = size; + type->regions[0].flags = flags; + memblock_set_region_node(&type->regions[0], nid); + type->total_size = size; + return 0; + } +repeat: + /* + * The following is executed twice. Once with %false @insert and + * then with %true. The first counts the number of regions needed + * to accomodate the new area. The second actually inserts them. + */ + base = obase; + nr_new = 0; + + for_each_memblock_type(type, rgn) { + phys_addr_t rbase = rgn->base; + phys_addr_t rend = rbase + rgn->size; + + if (rbase >= end) + break; + if (rend <= base) + continue; + /* + * @rgn overlaps. If it separates the lower part of new + * area, insert that portion. + */ + if (rbase > base) { +#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP + WARN_ON(nid != memblock_get_region_node(rgn)); +#endif + WARN_ON(flags != rgn->flags); + nr_new++; + if (insert) + memblock_insert_region(type, idx++, base, + rbase - base, nid, + flags); + } + /* area below @rend is dealt with, forget about it */ + base = min(rend, end); + } + + /* insert the remaining portion */ + if (base < end) { + nr_new++; + if (insert) + memblock_insert_region(type, idx, base, end - base, + nid, flags); + } + + /* + * If this was the first round, resize array and repeat for actual + * insertions; otherwise, merge and return. + */ + if (!insert) { + while (type->cnt + nr_new > type->max) + if (memblock_double_array(type, obase, size) < 0) + return -ENOMEM; + insert = true; + goto repeat; + } else { + memblock_merge_regions(type); + return 0; + } +} +``` + +###3.2.3 memblock_add_range函数流程解析 +------- + + +首先,我们用如下代码获得内存区域的结束位置: + +```cpp +phys_addr_t end = base + memblock_cap_size(base, &size); +``` + +memblock_cap_size函数会设置size大小确保base + size不会溢出。该函数实现相当简单, 参见[mm/memblock.c?v=4.7, line 79](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L79) + + +```cpp +/* adjust *@size so that (@base + *@size) doesn't overflow, return new size */ +static inline phys_addr_t memblock_cap_size(phys_addr_t base, phys_addr_t *size) +{ + return *size = min(*size, (phys_addr_t)ULLONG_MAX - base); +} +``` + + +memblock_cap_size返回size和ULLONG_MAX - base中的最小值 + +在那之后我们得到了新的内存区域的结束地址, 然后 + +* 查内存区域是否重叠 + +* 将新的添加到memblock, 并且看是否能和已经添加到memblock中的内存区域进行合并 + +首先遍历所有已经存储的内存区域并检查有没有和新的内存区域重叠 + + +```cpp +for_each_memblock_type(type, rgn) { + phys_addr_t rbase = rgn->base; + phys_addr_t rend = rbase + rgn->size; + + if (rbase >= end) + break; + if (rend <= base) + continue; + /* ...... */ + /* area below @rend is dealt with, forget about it */ + base = min(rend, end); +} +``` + +如果新内存区域没有和已经存储在memblock的内存区域重叠, 把该新内存区域插入到memblock中. 如果有重叠通通过一个小巧的来完成冲突处理 + +```cpp +base = min(rend, end); +``` + + + +重叠检查完毕后, 新的内存区域已经是一块干净的不包含重叠区域的内存, 把新的内存区域插入到memblock中包含两步: + +* 把新的内存区域中非重叠的部分作为独立的区域加入到memblock + +* 合并所有相邻的内存区域 + +这个过程分为两次循环来完成, 由一个[标识变量insert](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L582)和[report代码跳转标签](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L543)控制 + +* 第一次循环的时候, 检查新内存区域是否可以放入内存块中并调用memblock_double_array, 而由于[insert = false](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L524), 则执行[!insert条件语句标记的代码块](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L591), 并设置[insert = true](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L595), 然后[goto 跳转到report标签](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L596)继续开始第二次循环 + +* 第二次循环中, insert = true, 则执行相应的insert == true的代码块, 并且执行[memblock_insert_region将新内存区域插入](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L583), 最后执行[memblock_merge_regions(type)](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L598)合并内存区域 + +这是第一次循环, 我们需要检查新内存区域是否可以放入内存块中并调用memblock_double_array: + +```cpp +/* + * If this was the first round, resize array and repeat for actual + * insertions; otherwise, merge and return. + */ +if (!insert) { /* 第一次执行的的时候insert == false */ + while (type->cnt + nr_new > type->max) + if (memblock_double_array(type, obase, size) < 0) + return -ENOMEM; + insert = true; + goto repeat; +} else { + /* ...... */ +} +``` +memblock_double_array函数加倍给定的内存区域大小,然后把insert设为true再转到repeat标签. + +第二次循环,从repeat标签开始经过同样的循环然后用memblock_insert_region函数把当前内存区域插入到内存块: + +```cpp +/* insert the remaining portion */ +if (base < end) { + nr_new++; + if (insert) + memblock_insert_region(type, idx, base, end - base, + nid, flags); +} +``` + +由于我们在第一次循环中把insert设为true, 现在memblock_insert_region函数将会被调用 + + +[memblock_insert_region函数](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L476)几乎和把新内存区域插入到空的memblock_type代码块有同样的实现, 定义在[mm/memblock.c?v=4.7, line 476](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L476)该函数获得最后一个内存区域: + +```cpp +struct memblock_region *rgn = &type->regions[idx]; +``` + +然后调用memmove函数移动该内存区域: + +```cpp +memmove(rgn + 1, rgn, (type->cnt - idx) * sizeof(*rgn)); +``` + +紧接着填充新内存区域memblock_region的base域,size域等等, 然后增大memblock_type的大小。 + +最后memblock_add_range函数调用[memblock_merge_regions](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L444)合并所有相邻且兼容的内存区域, 定义在[mm/memblock.c?v=4.7, line 444](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L444) + + +```cpp +/* + * If this was the first round, resize array and repeat for actual + * insertions; otherwise, merge and return. + */ +if (!insert) { + /* ...... */ +} else { + memblock_merge_regions(type); + return 0; +} +``` + +##3.3 memblock_remove删除内存区域 +------- + + + +memblock_remove用来完成删除内存区域的工作, 该函数定义在[mm/memblock.c?v=4.7, line 710](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L710) + +```cpp +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L710 +int __init_memblock memblock_remove(phys_addr_t base, phys_addr_t size) +{ + return memblock_remove_range(&memblock.memory, base, size); +} +``` + + +##3.4 memblock_alloc申请内存 +------- + + +而相比来说, 申请内存的函数memblock_alloc实现方式就比较麻烦了, 如下所示 + +`emblock_alloc(phys_addr_t size, phys_addr_t align`)其实就是在当前NODE在内存范围0-MEMBLOCK_ALLOC_ACCESSIBLE(其实是current_limit)中分配一个大小为size的内存区域. + + +###3.4.1 memblock_alloc函数代码 +------- + + +```cpp +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L727 +phys_addr_t __init memblock_alloc(phys_addr_t size, phys_addr_t align) +{ + return memblock_alloc_base(size, align, MEMBLOCK_ALLOC_ACCESSIBLE); +} + + +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1192 +phys_addr_t __init memblock_alloc_base(phys_addr_t size, phys_addr_t align, phys_addr_t max_addr) +{ + phys_addr_t alloc; + + alloc = __memblock_alloc_base(size, align, max_addr); + + if (alloc == 0) + panic("ERROR: Failed to allocate 0x%llx bytes below 0x%llx.\n", + (unsigned long long) size, (unsigned long long) max_addr); + + return alloc; +} + +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1186 +phys_addr_t __init __memblock_alloc_base(phys_addr_t size, phys_addr_t align, phys_addr_t max_addr) +{ + return memblock_alloc_base_nid(size, align, max_addr, NUMA_NO_NODE, + MEMBLOCK_NONE); +} + +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1163 +phys_addr_t __init __memblock_alloc_base(phys_addr_t size, phys_addr_t align, phys_addr_t max_addr) +{ + return memblock_alloc_base_nid(size, align, max_addr, NUMA_NO_NODE, + MEMBLOCK_NONE); +} +``` +memblock_alloc()很粗暴的从能用的内存里分配, 而有些情况下需要从特定的内存范围内分配内存. 解决方法就是通过memblock_alloc_range_nid函数或者实现类似机制的函数 + +最终memblock_alloc的也是通过memblock_alloc_range_nid函数来完成内存分配的 + + + +###3.4.2 memblock_alloc_range_nid函数 +------- + + +下面我们就来看看memblock_alloc_range_nid函数的实现, 该函数定义在[mm/memblock.c?v=4.7, line 1133](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1133) + +```cpp +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1133 +static phys_addr_t __init memblock_alloc_range_nid(phys_addr_t size, + phys_addr_t align, phys_addr_t start, + phys_addr_t end, int nid, ulong flags) +{ + phys_addr_t found; + + if (!align) + align = SMP_CACHE_BYTES; + + found = memblock_find_in_range_node(size, align, start, end, nid, + flags); + if (found && !memblock_reserve(found, size)) { + /* + * The min_count is set to 0 so that memblock allocations are + * never reported as leaks. + */ + kmemleak_alloc(__va(found), size, 0, 0); + return found; + } + return 0; +} +``` + + +memblock_alloc_range_nid函数的主要工作如下 + +* 首先使用memblock_find_in_range_node指定内存区域和大小查找内存区域 + +* memblock_reserve后将其标为已经分配 + + +###3.4.3 memblock_find_in_range_node函数 +------- + +该函数定义在[mm/memblock.c?v=4.7, lien 178](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L178) + + +```cpp +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L178 +phys_addr_t __init_memblock memblock_find_in_range_node(phys_addr_t size, + phys_addr_t align, phys_addr_t start, + phys_addr_t end, int nid, ulong flags) +{ + phys_addr_t kernel_end, ret; + + /* pump up @end */ + if (end == MEMBLOCK_ALLOC_ACCESSIBLE) + end = memblock.current_limit; + + /* avoid allocating the first page */ + start = max_t(phys_addr_t, start, PAGE_SIZE); + end = max(start, end); + kernel_end = __pa_symbol(_end); + + /* + * try bottom-up allocation only when bottom-up mode + * is set and @end is above the kernel image. + */ + if (memblock_bottom_up() && end > kernel_end) { + phys_addr_t bottom_up_start; + + /* make sure we will allocate above the kernel */ + bottom_up_start = max(start, kernel_end); + + /* ok, try bottom-up allocation first */ + ret = __memblock_find_range_bottom_up(bottom_up_start, end, + size, align, nid, flags); + if (ret) + return ret; + + /* + * we always limit bottom-up allocation above the kernel, + * but top-down allocation doesn't have the limit, so + * retrying top-down allocation may succeed when bottom-up + * allocation failed. + * + * bottom-up allocation is expected to be fail very rarely, + * so we use WARN_ONCE() here to see the stack trace if + * fail happens. + */ + WARN_ONCE(1, "memblock: bottom-up allocation failed, memory hotunplug may be affected\n"); + } + + return __memblock_find_range_top_down(start, end, size, align, nid, + flags); +} +``` +* 如果从memblock_alloc过来, end就是MEMBLOCK_ALLOC_ACCESSIBLE,这个时候会设置为current_limit. + +* 如果不通过memblock_alloc分配, 内存范围就是指定的范围. 紧接着对start做调整,为的是避免申请到第一个页面 + +memblock_bottom_up返回的是memblock.bottom_up,前面初始化的时候也知道这个值是false(在numa初始化时会设置为true),所以初始化前期应该调用的是__memblock_find_range_top_down函数去查找内存: + + + + +###3.4.4 __memblock_find_range_top_down查找内存区域 +------- + +最后通过[__memblock_find_range_top_down](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L140)函数去查找内存 + + +```cpp +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L140 +static phys_addr_t __init_memblock +__memblock_find_range_top_down(phys_addr_t start, phys_addr_t end, + phys_addr_t size, phys_addr_t align, int nid, + ulong flags) +{ + phys_addr_t this_start, this_end, cand; + u64 i; + + for_each_free_mem_range_reverse(i, nid, flags, &this_start, &this_end, + NULL) { + this_start = clamp(this_start, start, end); + this_end = clamp(this_end, start, end); + + if (this_end < size) + continue; + + cand = round_down(this_end - size, align); + if (cand >= this_start) + return cand; + } + + return 0; +} +``` + + +* 函数通过使用for_each_free_mem_range_reverse宏封装调用__next_free_mem_range_rev()函数,此函数逐一将memblock.memory里面的内存块信息提取出来与memblock.reserved的各项信息进行检验,确保返回的this_start和this_end不会是分配过的内存块。 + +* 然后通过clamp取中间值,判断大小是否满足,满足的情况下,将自末端向前(因为这是top-down申请方式)的size大小的空间的起始地址(前提该地址不会超出this_start)返回回去 + +至此满足要求的内存块算是找到了。 + + +###3.4.5 memblock_reserve标记申请的内存 +------- + + +现在我们回到[memblock_alloc_range_nid函数](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1133), 我们说该函数完成了两项工作 + +* 首先通过memblock_find_in_range_node指定内存区域和大小查找内存区域 + +* 找到内存区域后, 调用memblock_reserve后将其标为已经分配 + +现在我们已经找到了内存区域了, 那么我们继续看看memblock_reserve函数是如何堆内存进行标记的, 该函数定义在[mm/memblock.c?v=4.7, line 727](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L727) + +```cpp +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L727 +int __init_memblock memblock_reserve(phys_addr_t base, phys_addr_t size) +{ + memblock_dbg("memblock_reserve: [%#016llx-%#016llx] flags %#02lx %pF\n", + (unsigned long long)base, + (unsigned long long)base + size - 1, + 0UL, (void *)_RET_IP_); + + return memblock_add_range(&memblock.reserved, base, size, MAX_NUMNODES, 0); +} + +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L609 +int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size) +{ + memblock_dbg("memblock_add: [%#016llx-%#016llx] flags %#02lx %pF\n", + (unsigned long long)base, + (unsigned long long)base + size - 1, + 0UL, (void *)_RET_IP_); + + return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0); +} +``` + +我们会发现首先memblock_reserve函数也是通过memblock_add_range来实现的, 我们把memblock_add的实现贴出来进行对比, 我们会发现他们就第一个参数不一样 + +* memblock_reserve使用全局变量memblock的reserved域, 最终将分配到的内存块信息添加到reserved区域中 + +* emblock_add则使用了全局变量的memory域, 最终将内存块添加到了memory区域 + + +memblock_add_range函数的流程我们前面已经将的很详细了, 这里只简单的叙述一下子 + +* 如果memblock算法管理内存为空的时候,则将当前空间添加进去 + +* 不为空的情况下,则先检查是否存在内存重叠的情况,如果有的话,则剔除重叠部分,然后将其余非重叠的部分添加进去 + +* 如果出现region[]数组空间不够的情况,则通过memblock_double_array()添加新的region[]空间 + +* 最后通过memblock_merge_regions()把紧挨着的内存合并了 + + + + + +##2.5 memblock_free释放内存区域 +------- + + +```cpp +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L695 +static int __init_memblock memblock_remove_range(struct memblock_type *type, + phys_addr_t base, phys_addr_t size) +{ + int start_rgn, end_rgn; + int i, ret; + + ret = memblock_isolate_range(type, base, size, &start_rgn, &end_rgn); + if (ret) + return ret; + + for (i = end_rgn - 1; i >= start_rgn; i--) + memblock_remove_region(type, i); +} + +// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L716 +int __init_memblock memblock_free(phys_addr_t base, phys_addr_t size) +{ + memblock_dbg(" memblock_free: [%#016llx-%#016llx] %pF\n", + (unsigned long long)base, + (unsigned long long)base + size - 1, + (void *)_RET_IP_); + + kmemleak_free_part(__va(base), size); + return memblock_remove_range(&memblock.reserved, base, size); +} +``` +#3 兼容bootmem的接口API +------- + + + +##3.1 memblock与bootmem +------- + +我们了解到memblock是作为bootmem的替代品而引入内核的 + +在编译Kernel的时候可以选择nobootmem或者bootmem 来在buddy system起来之前管理memory. +这两种机制对提供的API是一致的,因此对用户是透明的 + +参见[mm/Makefile](http://lxr.free-electrons.com/source/mm/Makefile#L44) + + +```cpp +ifdef CONFIG_NO_BOOTMEM + obj-y += nobootmem.o +else + obj-y += bootmem.o +endif +``` + + +为了保证系统的兼容性, 内核为bootmem和memblock提供了相同的API接口. + + + +##3.2 memblock与bootmem接口对比 +------- + + +由于接口是一致的, 那么他们共同使用一份 + +| 头文件 | bootmem接口 | nobootmem接口 | +|:-------:|:----------------:|:-------------------:| +| [include/linux/bootmem.h](http://lxr.free-electrons.com/source/include/linux/bootmem.h) | [mm/bootmem.c](http://lxr.free-electrons.com/source/mm/bootmem.c) | [mm/nobootmem.c](http://lxr.free-electrons.com/source/mm/nobootmem.c) | + + + +
+我们知道memblock自己的接口都在自己的头文件和源文件中 + +| memblock 头文件 | memblock接口 | +|:--------------:|:------------:| +| [include/linux/memblock.h](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7) | [mm/memblock.c](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7) + + +为了实现接口兼容, 内核用[mm/memblock.c](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7)定义的memblock接口, 实现了一套bootmem的接口机制, 而bootmem的接口我们在上一篇引导内存管理bootmem机制中已经讲过了, 这些实现的bootmem函数接口API, 就定义在[mm/nobootmem.c](http://lxr.free-electrons.com/source/mm/nobootmem.c)文件中, 然后内核把他们进行了封装, 然后提供了与bootmem相同功能和函数的接口, 这些接口都在[include/linux/memblock.h](http://lxr.free-electrons.com/source/include/linux/memblock.h?v=4.7). + + + +在NUMA系统上, 基本的API是相同的, 但是函数增加了_node后缀, 与UMA系统的函数相比, 还需要一些额外的参数, 用于指定内存分配的结点. + + +| 函数 | bootmem定义 | nobootmem定义 | +|:-----:|:------------------:|:----------------------:| +| ZONE_NORMAL的分配函数 | 按照指定大小在ZONE_NORMAL内存域分配函数. 数据是对齐的, 这使得内存或者从可适用于L1高速缓存的理想位置开始| | +| alloc_bootmem(size) | [alloc_bootmem](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L122)
[__alloc_bootmem](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L700)
[___alloc_bootmem](http://lxr.free-electrons.com/source/mm/bootmem.c?=4.7#L672) | [alloc_bootmem](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L122)
[__alloc_bootmem](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L309)
[___alloc_bootmem](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L281) | +| alloc_bootmem_align(size) | [alloc_bootmem_align](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L124)
基于__alloc_bootmem实现 | [alloc_bootmem_align](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L124)
[__alloc_bootmem](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L309)
[___alloc_bootmem](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L281) | +| alloc_bootmem_pages(size)) | [alloc_bootmem_pages](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L128)
基于__alloc_bootmem实现 | [alloc_bootmem_pages](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L128)
基于__alloc_bootmem实现 | +| alloc_bootmem_nopanic(size) | [alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L126)
[__alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L664)
[___alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L632) | [alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L126)
[__alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L261)
[___alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L235) | +| ZONE_DMA区域的分配函数 | | +| alloc_bootmem_low(size) | [alloc_bootmem_low](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L141)
[__alloc_bootmem_low](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L415)底层基于___alloc_bootmem | [alloc_bootmem_low](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L141)
底层基于___alloc_bootmem | +| alloc_bootmem_low_pages_nopanic(size) | [alloc_bootmem_low_pages_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L143)
底层基于[__alloc_bootmem_low_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L838)
[___alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L632) | [alloc_bootmem_low_pages_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L143)
底层基于[__alloc_bootmem_low_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L421)
[___alloc_bootmem_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L235) | +| alloc_bootmem_low_pages(size) | [alloc_bootmem_low_pages](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L832)
底层基于[__alloc_bootmem_low_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L838) | [alloc_bootmem_low_pages](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L832)
底层基于[__alloc_bootmem_low_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L421) | +| NUMA结构的分配函数 | | | +| alloc_bootmem_node(pgdat, size) | [alloc_bootmem_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L132)
[__alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L777)
[ ___alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L708) | [alloc_bootmem_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L132)
[__alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L383)
[ ___alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | +| alloc_bootmem_node_nopanic(pgdat, size) | [alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L134)
[__alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L738)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L708) | [alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L134)
[__alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L344)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | +| alloc_bootmem_pages_node(pgdat, size) | [alloc_bootmem_pages_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L136)
[__alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L747)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L708) | [alloc_bootmem_pages_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L136)
[__alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L344)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | +| alloc_bootmem_pages_node_nopanic(pgdat, size) | [alloc_bootmem_pages_node_nopanic](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L138)
[__alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L344)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | +| alloc_bootmem_low_pages_node(pgdat, size) | [alloc_bootmem_low_pages_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L14)
[__alloc_bootmem_low_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L861)[___alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L747)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L707) | [alloc_bootmem_low_pages_node](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L14)
[__alloc_bootmem_low_node](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L444)[___alloc_bootmem_node](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L353)
[___alloc_bootmem_node_nopanic](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317) | + + +我们可以看到最基本的实现思路都是一样的, 只是最底层的实现函数有细微的区别. + + +##3.3 实现差异 +------- + + +* UMA结构下这些函数最终都是通过___alloc_bootmem_nopanic函数来实现的 + +* NUMA架构下, 最终这些函数都是简介的调用___alloc_bootmem_node_nopanic函数来实现的, + + + + +| 函数 | bootmem | memblock | +|:-----:|:-----------:|:------------:| +| ___alloc_bootmem_nopanic | [mm/bootmem.c?v=4.7, line 632](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L632), 通过[alloc_bootmem_core](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L607)函数来实现 | [mm/nobootmem.c?v=4.7, line 235](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L235), 通过[__alloc_memory_core_early](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L36) | +| ___alloc_bootmem_node_nopanic | [mm/bootmem.c?v=4.7, line 708](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L708), 通过调用[alloc_bootmem_core](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L607)和[alloc_bootmem_bdata](http://lxr.free-electrons.com/source/mm/bootmem.c?v=4.7#L500)来实现 | [mm/nobootmem.c?v=4.7, line 317](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L317), 通过调用[__alloc_memory_core_early](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L36)来实现 | + + +bootmem的核心函数__alloc_memory_core()的实现机制我们前一篇博文[引导分配器bootmem](待添加链接)已经讲过了, 那么memblock下nobootmem的核心函数__alloc_memory_core_early是怎么实现的呢? + +前面[2.4.1节memblock_alloc函数代码](待添加链接)我们分析memblock_alloc函数的时候提到, 该函数最终通过memblock_alloc_range_nid函数粗暴粗暴的进行内存分配, 而有些情况下需要从特定的内存范围内分配内存. 解决方法就是通过memblock_alloc_range_nid函数或者实现类似机制的函数, 这里的__alloc_memory_core_early函数就是基于memblock_alloc_range_nid同样的思路实现的函数 + + +* 首先使用memblock_find_in_range_node指定内存区域和大小查找内存区域 + +* memblock_reserve后将其标为已经分配 + + +我们列出 memblock_alloc_range_nid函数与__alloc_memory_core_early函数的实现对比 + +| memblock_alloc_range_nid | __alloc_memory_core_early | +|:------------------------------:|:------------------------------:| +| [mm/memblock.c?v=4.7, line 1133](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1133) | [mm/nobootmem.c?v=4.7, line 36](http://lxr.free-electrons.com/source/mm/nobootmem.c?v=4.7#L36) | + + +#4 memblock初始化 +------- + +如果从整个linux生命周期来讲,涉及到各种初始化等,这里来详细分析,因为还没有分析完内核,所以这里是分析到哪里就记录到哪里了. + +##4.1 x86架构下的memblock初始化 +------- + +要理解memblock是如何工作和实现的, 我们首先看一下它的用法. + + + +在Linux内核中有几处用到了memblock, 例如 arch/x86/kernel/e820.c中的函数`memblock_x86_fill`. 该函数遍历由`e820`提供的内存映射表并且通过`memblock_add`函数把内核预留的内存区域添加到memblock。既然我们首先遇到了`memblock_add`函数,那就从它开始吧。 + + +在内核初始化初期,物理内存会通过`Int 0x15`来被探测和整理, 存放到`e820`中.而初始化就发生在这个以后. 参见[arch/x86/kernel/setup.c?v=4.7, line 1096](http://lxr.free-electrons.com/source/arch/x86/kernel/setup.c?v=4.7#L1096) + +```cpp +void __init setup_arch(char **cmdline_p) +{ + /* + * Need to conclude brk, before memblock_x86_fill() + * it could use memblock_find_in_range, could overlap with + * brk area. + */ + reserve_brk(); + + cleanup_highmap(); + + memblock_set_current_limit(ISA_END_ADDRESS); + memblock_x86_fill(); +} +``` + +首先内核建立内核页表需要扩展__brk, 而扩展后的brk就立即被声明为已分配. 这项工作是由reserve_brk通过调用memblock_reserve完成的, 而其实并不是正真通过memblock分配的, 因为此时memblock还没有完成初始化 + +[reserve_brk](http://lxr.free-electrons.com/source/arch/x86/kernel/setup.c?v=4.9#L209)函数定义在[arch/x86/kernel/setup.c?v=4.7, line 209](http://lxr.free-electrons.com/source/arch/x86/kernel/setup.c?v=4.9#L209), 此时memblock还没有初始化, 只能通过[memblock_reserve](http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L727)来完成内存的分配 + +```cpp +static void __init reserve_brk(void) +{ + if (_brk_end > _brk_start) + memblock_reserve(__pa_symbol(_brk_start), + _brk_end - _brk_start); + + /* Mark brk area as locked down and no longer taking any + new allocations */ + _brk_start = 0; +} +``` + +设置完__brk后, 可以看到,setup_arch()函数通过memblock_x86_fill(),依据e820中的信息来初始化memblock. + +```cpp +void __init memblock_x86_fill(void) +{ + int i; + u64 end; + + /* + * EFI may have more than 128 entries + * We are safe to enable resizing, beause memblock_x86_fill() + * is rather later for x86 + */ + memblock_allow_resize(); + + for (i = 0; i < e820.nr_map; i++) { + struct e820entry *ei = &e820.map[i]; + + end = ei->addr + ei->size; + if (end != (resource_size_t)end) + continue; + + if (ei->type != E820_RAM && ei->type != E820_RESERVED_KERN) + continue; + + memblock_add(ei->addr, ei->size); + } + + /* throw away partial pages */ + memblock_trim_memory(PAGE_SIZE); + + memblock_dump_all(); +} +``` + +比较简单,通过e820中的信息memblock_add(),将内存添加到memblock中的memory中,当做可分配内存.后两个函数主要是修剪内存使之对齐和输出信息. + +至此, 我们的memblock就初始化好了, 简单而且粗暴 + + + +##4.2 arm架构下的memblock初始化 +------- + + +arm下的memblock初始化也是从start_kernel()->[setup_arch()](http://lxr.free-electrons.com/source/arch/arm/kernel/setup.c#L1034)开始的, 在setup_arch()中arm架构通过arm_memblock_init完成了memblock的初始化工作. + +```cpp +void __init setup_arch(char **cmdline_p) +{ + arm_memblock_init(mdesc); +} +``` + +arm_memblock_init定义在[arch/arm/mm/init.c](http://lxr.free-electrons.com/source/arch/arm/mm/init.c?v=4.7#L230), 如下所示 + + +```cpp +void __init arm_memblock_init(const struct machine_desc *mdesc) +{ + /* Register the kernel text, kernel data and initrd with memblock. */ +#ifdef CONFIG_XIP_KERNEL + memblock_reserve(__pa(_sdata), _end - _sdata); +#else + memblock_reserve(__pa(_stext), _end - _stext); +#endif +#ifdef CONFIG_BLK_DEV_INITRD + /* FDT scan will populate initrd_start */ + if (initrd_start && !phys_initrd_size) { + phys_initrd_start = __virt_to_phys(initrd_start); + phys_initrd_size = initrd_end - initrd_start; + } + initrd_start = initrd_end = 0; + if (phys_initrd_size && + !memblock_is_region_memory(phys_initrd_start, phys_initrd_size)) { + pr_err("INITRD: 0x%08llx+0x%08lx is not a memory region - disabling initrd\n", + (u64)phys_initrd_start, phys_initrd_size); + phys_initrd_start = phys_initrd_size = 0; + } + if (phys_initrd_size && + memblock_is_region_reserved(phys_initrd_start, phys_initrd_size)) { + pr_err("INITRD: 0x%08llx+0x%08lx overlaps in-use memory region - disabling initrd\n", + (u64)phys_initrd_start, phys_initrd_size); + phys_initrd_start = phys_initrd_size = 0; + } + if (phys_initrd_size) { + memblock_reserve(phys_initrd_start, phys_initrd_size); + + /* Now convert initrd to virtual addresses */ + initrd_start = __phys_to_virt(phys_initrd_start); + initrd_end = initrd_start + phys_initrd_size; + } +#endif + + arm_mm_memblock_reserve(); + + /* reserve any platform specific memblock areas */ + if (mdesc->reserve) + mdesc->reserve(); + + early_init_fdt_reserve_self(); + early_init_fdt_scan_reserved_mem(); + + /* reserve memory for DMA contiguous allocations */ + dma_contiguous_reserve(arm_dma_limit); + + arm_memblock_steal_permitted = false; + memblock_dump_all(); +} +``` + +##4.3 arm64下的memblock初始化 +------- + +前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作. + +```cpp +void __init setup_arch(char **cmdline_p) +{ + /* 初始化memblock */ + arm64_memblock_init( ); + + /* 分页机制初始化 */ + paging_init(); + + bootmem_init(); +} +``` + +| 流程 | 描述 | +|:---:|:----:| +| [arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) | 初始化memblock内存分配器 | +| [paging_init](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538) | 初始化分页机制 | +| [bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | 初始化内存管理 | + + + + +其中arm64_memblock_init就完成了arm64架构下的memblock的初始化 + + +与arm架构类似, arm64的memblock初始化没有意外, 只是初始化函数成为[arm64_memblock_init()](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L261), 该函数定义在[arch/arm64/mm/init.c?v=4.7, line 192](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L192) + + + +#5 总结 +------- + + +memblock内存管理是将所有的物理内存放到`memblock.memory`中作为可用内存来管理, 分配过的内存只加入到`memblock.reserved`中, 并不从`memory`中移出. + +同理释放内存也会加入到`memory`中. 也就是说, `memory`在`fill`过后基本就是不动的了. 申请和分配内存仅仅修改`reserved`就达到目的. 在初始化阶段没有那么多复杂的内存操作场景, 甚至很多地方都是申请了内存做永久使用的, 所以这样的内存管理方式已经足够凑合着用了, 毕竟内核也不指望用它一辈子. 在系统完成初始化之后所有的工作会移交给强大的`buddy`系统来进行内存管理 + diff --git a/study/kernel/02-memory/03-initialize/04-bootmem_init/README.md b/study/kernel/02-memory/03-initialize/04-bootmem_init/README.md index 1c66c60..cdcc4d9 100644 --- a/study/kernel/02-memory/03-initialize/04-bootmem_init/README.md +++ b/study/kernel/02-memory/03-initialize/04-bootmem_init/README.md @@ -1,1245 +1,1245 @@ -初始化内存管理 -======= - - - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - - -在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后奇偶内核才能检测到可用内存和寄存器. - - - -#1 前景回顾 -------- - - -##1.1 Linux内存管理的层次结构 -------- - - -Linux把物理内存划分为三个层次来管理 - -| 层次 | 描述 | -|:----:|:----:| -| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | -| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | -| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | - -为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 - - -* 首先, 内存被划分为**结点**. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. - -* 接着各个节点又被划分为内存管理区域, 一个**管理区域**通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. - - -* 最后**页帧(page frame)**代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号. - - -##1.2 今日内容(启动过程中的内存初始化) -------- - - -在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. - - -因此我们可以把linux内核的内存管理分三个阶段。 - -| 阶段 | 起点 | 终点 | 描述 | -|:-----:|:-----:|:-----:| -| 第一阶段 | 系统启动 | bootmem或者memblock初始化完成 | 此阶段只能使用memblock_reserve函数分配内存, 早期内核中使用init_bootmem_done = 1标识此阶段结束 | -| 第二阶段 | bootmem或者memblock初始化完 | buddy完成前 | 引导内存分配器bootmem或者memblock接受内存的管理工作, 早期内核中使用mem_init_done = 1标记此阶段的结束 | -| 第三阶段 | buddy初始化完成 | 系统停止运行 | 可以用cache和buddy分配内存 | - - - -##1.3 start_kernel系统启动阶段的内存初始化过程 -------- - -首先我们来看看start_kernel是如何初始化系统的, start_kerne定义在[init/main.c?v=4.7, line 479](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L479) - -其代码很复杂, 我们只截取出其中与内存管理初始化相关的部分, 如下所示 - - -```cpp -asmlinkage __visible void __init start_kernel(void) -{ - - setup_arch(&command_line); - mm_init_cpumask(&init_mm); - - setup_per_cpu_areas(); - - - build_all_zonelists(NULL, NULL); - page_alloc_init(); - - - /* - * These use large bootmem allocations and must precede - * mem_init(); - * kmem_cache_init(); - */ - mm_init(); - - kmem_cache_init_late(); - - kmemleak_init(); - setup_per_cpu_pageset(); - - rest_init(); -} -``` - - -| 函数 | 功能 | -|:----:|:----:| -| [setup_arch](http://lxr.free-electrons.com/ident?v=4.7;i=setup_arch) | 是一个特定于体系结构的设置函数, 其中一项任务是负责初始化自举分配器 | -| [mm_init_cpumask](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v=4.7#L522) | 初始化CPU屏蔽字 | -| [setup_per_cpu_areas](http://lxr.free-electrons.com/ident?v=4.7;i=setup_per_cpu_areas) | 函数[(查看定义)](http://lxr.free-electrons.com/source/mm/percpu.c?v4.7#L2205])给每个CPU分配内存,并拷贝.data.percpu段的数据. 为系统中的每个CPU的per_cpu变量申请空间.
在SMP系统中, setup_per_cpu_areas初始化源代码中(使用[per_cpu宏](http://lxr.free-electrons.com/source/include/linux/percpu-defs.h#L256))定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本.
此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本
在非SMP系统中这是一个空操作 | -| [build_all_zonelists](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029) | 建立并初始化结点和内存域的数据结构 | -| [mm_init](http://lxr.free-electrons.com/source/init/main.c?v4.7#L464) | 建立了内核的内存分配器,
其中通过[mem_init](http://lxr.free-electrons.com/ident?v=4.7&i=mem_init)停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)
然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器 | -| [kmem_cache_init_late](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378) | 在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器[slab](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378), [slob](http://lxr.free-electrons.com/source/mm/slob.c?v4.7#L655), [slub](http://lxr.free-electrons.com/source/mm/slub.c?v=4.7#L3960)都会定义此函数 | -| [kmemleak_init](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1857) | Kmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 [/sys/kernel/debug/kmemleak](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1467)中, Kmemcheck能够帮助定位大多数内存错误的上下文 | -| [setup_per_cpu_pageset](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5392) | 初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配
由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. | - - - - -##1.4 setup_arch函数初始化内存流程 -------- - - -前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作. - -```cpp -void __init setup_arch(char **cmdline_p) -{ - /* 初始化memblock */ - arm64_memblock_init( ); - - /* 分页机制初始化 */ - paging_init(); - - bootmem_init(); -} -``` - -| 流程 | 描述 | -|:---:|:----:| -| [arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) | 初始化memblock内存分配器 | -| [paging_init](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538) | 初始化分页机制 | -| [bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | 初始化内存管理 | - - -该函数主要执行了如下操作 - - -1. 使用arm64_memblock_init来完成memblock机制的初始化工作, 至此memblock分配器接受系统中系统中内存的分配工作 - -2. 调用paging_init来完成系统分页机制的初始化工作, 建立页表, 从而内核可以完成虚拟内存的映射和转换工作 - -3. 最后调用bootmem_init来完成实现buddy内存管理所需要的工作 - - - -##1.5 (第一阶段)启动过程中的内存分配器 -------- - - -在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. - -这个阶段的内存分配其实很简单, 因此我们往往称之为内存分配器(而不是内存管理器), 早期的内核中内存分配器使用的**bootmem引导分配器**, 它基于一个内存位图bitmap, 使用最优适配算法来查找内存, 但是这个分配器有很大的缺陷, 最严重的就是内存碎片的问题, 因此在后来的内核中将其舍弃《而使用了**新的memblock机制**. memblock机制的初始化在arm64上是通过[arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229)函数来实现的 - -```cpp -```cpp -start_kernel() - |---->page_address_init() - | 考虑支持高端内存 - | 业务:初始化page_address_pool链表; - | 将page_address_maps数组元素按索引降序插入 - | page_address_pool链表; - | 初始化page_address_htable数组. - | - |---->setup_arch(&command_line); - | 初始化特定体系结构的内容 - | - |---->arm64_memblock_init( ); - | 初始化引导阶段的内存分配器memblock - | - |---->paging_init(); - | 分页机制初始化 - | - |---->bootmem_init(); [当前位置] - | 始化内存数据结构包括内存节点, 内存域和页帧page - | - |---->arm64_numa_init(); - | 支持numa架构 - | - |---->zone_sizes_init(min, max); - 来初始化节点和管理区的一些数据项 - | - |---->free_area_init_node - | 初始化内存节点 - | - |---->free_area_init_core - | 初始化zone - | - |---->memmap_init - | 初始化page页面 - | - |---->memblock_dump_all(); - | 初始化完成, 显示memblock的保留的所有内存信息 - | - |---->build_all_zonelist() - | 为系统中的zone建立后备zone的列表. - | 所有zone的后备列表都在 - | pglist_data->node_zonelists[0]中; - | - | 期间也对per-CPU变量boot_pageset做了初始化. - | -``` - - -##1.6 今日内容(第二阶段(一)--初始化内存管理数据结构) -------- - - -我们之前讲了在memblock完成之后, 内存初始化开始进入第二阶段, 第二阶段是一个漫长的过程, 它执行了一系列复杂的操作, 从体系结构相关信息的初始化慢慢向上层展开, 其主要执行了如下操作 - - - -**特定于体系结构的设置** - - -在完成了基础的内存结点和内存域的初始化工作以后, 我们必须克服一些硬件的特殊设置 - -* 在初始化内存的结点和内存区域之前, 内核先通过pagging_init初始化了内核的分页机制, 这样我们的虚拟运行空间就初步建立, 并可以完成物理地址到虚拟地址空间的映射工作. - -在arm64架构下, 内核在start_kernel()->setup_arch()中通过arm64_memblock_init( )完成了memblock的初始化之后, 接着通过setup_arch()->paging_init()开始初始化分页机制 - - -paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响 - - -* 在分页机制完成后, 内核通过setup_arch()->bootmem_init开始进行内存基本数据结构(内存结点pg_data_t, 内存域zone和页帧)的初始化工作, 就是在这个函数中, 内核开始从体系结构相关的部分逐渐展开到体系结构无关的部分, 在zone_sizes_init->free_area_init_node中开始, 内核开始进行内存基本数据结构的初始化, 也不再依赖于特定体系结构无关的层次 - -```cpp -bootmem_init() -始化内存数据结构包括内存节点, 内存域和页帧page -| -|---->arm64_numa_init(); -| 支持numa架构 -| -|---->zone_sizes_init(min, max); - 来初始化节点和管理区的一些数据项 - | - |---->free_area_init_node - | 初始化内存节点 - | - |---->free_area_init_core - | 初始化zone - | - |---->memmap_init - | 初始化page页面 -| -|---->memblock_dump_all(); -| 初始化完成, 显示memblock的保留的所有内存信息 -``` - - - - -**建立内存管理的数据结构** - - -对相关数据结构的初始化是从全局启动函数start_kernel中开始的, 该函数在加载内核并激活各个子系统之后执行. 由于内存管理是内核一个非常重要的部分, 因此在特定体系结构的设置步骤中检测并确定系统中内存的分配情况后, 会立即执行内存管理的初始化. - - - -**移交早期的分配器到内存管理器** - - - -最后我们的内存管理器已经初始化并设置完成, 可以投入运行了, 因此内核将内存管理的工作从早期的内存分配器(bootmem或者memblock)移交到我们的buddy伙伴系统. - - - - -#2 初始化前的准备工作 -------- - - -##2.1 回到setup_arch函数(当前已经完成的工作) -------- - - -现在我们回到start_kernel()->setup_arch()函数 - - -```cpp -void __init setup_arch(char **cmdline_p) -{ - /* 初始化memblock */ - arm64_memblock_init( ); - - /* 分页机制初始化 */ - paging_init(); - - bootmem_init(); -} -``` - - -到目前位置我们已经完成了如下工作 - -* memblock已经通过arm64_memblock_init完成了初始化, 至此系统中的内存可以通过memblock分配了 - -* paging_init完成了分页机制的初始化, 至此内核已经布局了一套完整的虚拟内存空间 - - -至此我们所有的内存都可以通过memblock机制来分配和释放, 尽管它实现的笨拙而简易, 但是已经足够我们初始化阶段使用了, 反正内核页不可能指着它过一辈子, 而我们也通过pagging_init创建了页表, 为内核提供了一套可供内核和进程运行的虚拟运行空间, 我们可以安全的进行内存的分配了 - -因此该是时候初始化我们强大的buddy系统了. - -内核接着setup_arch()->bootmem_init()函数开始执行 - -体系结构相关的代码需要在启动期间建立如下信息 - -* 系统中各个内存域的页帧边界,保存在max_zone_pfn数组 - -早期的内核还需记录各结点页帧的分配情况,保存在全局变量early_node_map中 - -![zone_sizes_init函数](../images/arch_do_somethig.png) - -内核提供了一个通用的框架, 用于将上述信息转换为伙伴系统预期的节点和内存域数据结构, 但是在此之前各个体系结构必须自行建立相关结构. - - -##2.2 bootmem_init函数初始化内存结点和管理域 -------- - - -arm64架构下, 在setup_arch中通过paging_init函数初始化内核分页机制之后, 内核通过`bootmem_init()`开始完成内存结点和内存区域的初始化工作, 该函数定义在[arch/arm64/mm/init.c, line 306](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) - -```cpp -void __init bootmem_init(void) -{ - unsigned long min, max; - - min = PFN_UP(memblock_start_of_DRAM()); - max = PFN_DOWN(memblock_end_of_DRAM()); - - early_memtest(min << PAGE_SHIFT, max << PAGE_SHIFT); - - max_pfn = max_low_pfn = max; - - arm64_numa_init(); - /* - * Sparsemem tries to allocate bootmem in memory_present(), so must be - * done after the fixed reservations. - */ - arm64_memory_present(); - - sparse_init(); - zone_sizes_init(min, max); - - high_memory = __va((max << PAGE_SHIFT) - 1) + 1; - memblock_dump_all(); -} -``` - - -##2.3 zone_sizes_init函数 -------- - - -在初始化内存结点和内存域之前, 内核首先通过setup_arch()-->bootmem_init()-->zone_sizes_init()来初始化节点和管理区的一些数据项, 其中关键的是初始化了系统中各个内存域的页帧边界,保存在max_zone_pfn数组. - - - -[zone_sizes_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L92)函数定义在[arch/arm64/mm/init.c?v=4.7, line 92](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L92), 由于arm64支持NUMA和UMA两种存储器架构, 因此该函数依照NUMA和UMA, 有两种不同的实现. - -```cpp -#ifdef CONFIG_NUMA - -static void __init zone_sizes_init(unsigned long min, unsigned long max) -{ - unsigned long max_zone_pfns[MAX_NR_ZONES] = {0}; - - if (IS_ENABLED(CONFIG_ZONE_DMA)) - max_zone_pfns[ZONE_DMA] = PFN_DOWN(max_zone_dma_phys()); - max_zone_pfns[ZONE_NORMAL] = max; - - free_area_init_nodes(max_zone_pfns); -} - -#else - -static void __init zone_sizes_init(unsigned long min, unsigned long max) -{ - struct memblock_region *reg; - unsigned long zone_size[MAX_NR_ZONES], zhole_size[MAX_NR_ZONES]; - unsigned long max_dma = min; - - memset(zone_size, 0, sizeof(zone_size)); - - /* 4GB maximum for 32-bit only capable devices */ -#ifdef CONFIG_ZONE_DMA - max_dma = PFN_DOWN(arm64_dma_phys_limit); - zone_size[ZONE_DMA] = max_dma - min; -#endif - zone_size[ZONE_NORMAL] = max - max_dma; - - memcpy(zhole_size, zone_size, sizeof(zhole_size)); - - for_each_memblock(memory, reg) { - unsigned long start = memblock_region_memory_base_pfn(reg); - unsigned long end = memblock_region_memory_end_pfn(reg); - - if (start >= max) - continue; - -#ifdef CONFIG_ZONE_DMA - if (start < max_dma) { - unsigned long dma_end = min(end, max_dma); - zhole_size[ZONE_DMA] -= dma_end - start; - } -#endif - if (end > max_dma) { - unsigned long normal_end = min(end, max); - unsigned long normal_start = max(start, max_dma); - zhole_size[ZONE_NORMAL] -= normal_end - normal_start; - } - } - - free_area_init_node(0, zone_size, min, zhole_size); -} - -#endif /* CONFIG_NUMA */ -``` - -在获取了三个管理区的页面数后, NUMA架构下通过free_area_init_nodes()来完成后续工作, 其中核心函数为free_area_init_node(),用来针对特定的节点进行初始化, 由于UMA架构下只有一个内存结点, 因此直接通过free_area_init_node来完成内存结点的初始化 - - -截至到目前为止, 体系结构相关的部分已经结束了, 各个体系结构已经自行建立了自己所需的一些底层数据结构, 这些结构建立好以后, 内核将繁重的内存数据结构创建和初始化的工作交给free_area_init_node(s)函数来完成, - -#3 free_area_init_nodes初始化NUMA管理数据结构 -------- - ->注意 -> ->此部分内容参照 -> ->[Linux内存管理伙伴算法](http://www.linuxidc.com/Linux/2012-09/70711p3.htm) -> ->[linux 内存管理 - paging_init 函数](http://blog.csdn.net/decload/article/details/8080126) - - -[free_area_init_nodes](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6460)初始化了NUMA系统中所有结点的pg_data_t和zone、page的数据, 并打印了管理区信息, 该函数定义在[mm/page_alloc.c?v=4.7, line 6460](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6460) - -##3.1 代码注释 -------- - -```cpp -// 初始化各个节点的所有pg_data_t和zone、page的数据 -void __init free_area_init_nodes(unsigned long *max_zone_pfn) -{ - unsigned long start_pfn, end_pfn; - int i, nid; - - /* Record where the zone boundaries are - * 全局数组arch_zone_lowest_possible_pfn - * 用来存储各个内存域可使用的最低内存页帧编号 */ - memset(arch_zone_lowest_possible_pfn, 0, - sizeof(arch_zone_lowest_possible_pfn)); - - /* 全局数组arch_zone_highest_possible_pfn - * 用来存储各个内存域可使用的最高内存页帧编号 */ - memset(arch_zone_highest_possible_pfn, 0, - sizeof(arch_zone_highest_possible_pfn)); - - /* 辅助函数find_min_pfn_with_active_regions - * 用于找到注册的最低内存域中可用的编号最小的页帧 */ - arch_zone_lowest_possible_pfn[0] = find_min_pfn_with_active_regions(); - - /* max_zone_pfn记录了各个内存域包含的最大页帧号 */ - arch_zone_highest_possible_pfn[0] = max_zone_pfn[0]; - - /* 依次遍历,确定各个内存域的边界 */ - for (i = 1; i < MAX_NR_ZONES; i++) { - /* 由于ZONE_MOVABLE是一个虚拟内存域 - * 不与真正的硬件内存域关联 - * 该内存域的边界总是设置为0 */ - if (i == ZONE_MOVABLE) - continue; - /* 第n个内存域的最小页帧 - * 即前一个(第n-1个)内存域的最大页帧 */ - arch_zone_lowest_possible_pfn[i] = - arch_zone_highest_possible_pfn[i-1]; - /* 不出意外,当前内存域的最大页帧 - * 由max_zone_pfn给出 */ - arch_zone_highest_possible_pfn[i] = - max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]); - } - arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0; - arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0; - - /* Find the PFNs that ZONE_MOVABLE begins at in each node */ - memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn)); - /* 用于计算进入ZONE_MOVABLE的内存数量 */ - find_zone_movable_pfns_for_nodes(); - - /* Print out the zone ranges - * 将各个内存域的最大、最小页帧号显示出来 */ - pr_info("Zone ranges:\n"); - for (i = 0; i < MAX_NR_ZONES; i++) { - if (i == ZONE_MOVABLE) - continue; - pr_info(" %-8s ", zone_names[i]); - if (arch_zone_lowest_possible_pfn[i] == - arch_zone_highest_possible_pfn[i]) - pr_cont("empty\n"); - else - pr_cont("[mem %#018Lx-%#018Lx]\n", - (u64)arch_zone_lowest_possible_pfn[i] - << PAGE_SHIFT, - ((u64)arch_zone_highest_possible_pfn[i] - << PAGE_SHIFT) - 1); - } - - /* Print out the PFNs ZONE_MOVABLE begins at in each node */ - pr_info("Movable zone start for each node\n"); - for (i = 0; i < MAX_NUMNODES; i++) { - /* 对每个结点来说,zone_movable_pfn[node_id] - * 表示ZONE_MOVABLE在movable_zone内存域中所取得内存的起始地址 - * 内核确保这些页将用于满足符合ZONE_MOVABLE职责的内存分配 */ - if (zone_movable_pfn[i]) - { - /* 显示各个内存域的分配情况 */ - pr_info(" Node %d: %#018Lx\n", i, - (u64)zone_movable_pfn[i] << PAGE_SHIFT); - } - } - - /* Print out the early node map */ - pr_info("Early memory node ranges\n"); - for_each_mem_pfn_range(i, MAX_NUMNODES, &start_pfn, &end_pfn, &nid) - pr_info(" node %3d: [mem %#018Lx-%#018Lx]\n", nid, - (u64)start_pfn << PAGE_SHIFT, - ((u64)end_pfn << PAGE_SHIFT) - 1); - - /* Initialise every node */ - mminit_verify_pageflags_layout(); - setup_nr_node_ids(); - - /* 代码遍历所有的活动结点, - * 并分别对各个结点调用free_area_init_node建立数据结构, - * 该函数需要结点第一个可用的页帧作为一个参数, - * 而find_min_pfn_for_node则从early_node_map数组提取该信息 */ - for_each_online_node(nid) { - pg_data_t *pgdat = NODE_DATA(nid); - free_area_init_node(nid, NULL, - find_min_pfn_for_node(nid), NULL); - - /* Any memory on that node - * 根据node_present_pages字段判断结点具有内存 - * 则在结点位图中设置N_HIGH_MEMORY标志 - * 该标志只表示结点上存在普通或高端内存 - * 因此check_for_regular_memory - * 进一步检查低于ZONE_HIGHMEM的内存域中是否有内存 - * 并据此在结点位图中相应地设置N_NORMAL_MEMORY */ - if (pgdat->node_present_pages) - node_set_state(nid, N_MEMORY); - check_for_memory(pgdat, nid); - } -} -``` - -free_area_init_nodes函数中通过循环遍历各个节点,循环中调用了free_area_init_node函数初始化该节点对应的pg_data_t和zone、page的数据. - - -##3.2 设置可使用的页帧编号 -------- - - -free_area_init_nodes首先必须分析并改写特定于体系结构的代码提供的信息。其中,需要对照在zone_max_pfn和zone_min_pfn中指定的内存域的边界,计算各个内存域可使用的最低和最高的页帧编号。使用了两个全局数组来存储这些信息: - -参见[mm/page_alloc.c?v=4.7, line 259)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L259) - -```cpp -static unsigned long __meminitdata arch_zone_lowest_possible_pfn[MAX_NR_ZONES]; - -static unsigned long __meminitdata arch_zone_highest_possible_pfn[MAX_NR_ZONES]; -``` - -通过max_zone_pfn传递给free_area_init_nodes的信息记录了各个内存域包含的最大页帧号。 -free_area_init_nodes将该信息转换为一种更方便的表示形式,即以[low, high]形式描述各个内 -存域的页帧区间,存储在前述的全局变量中(我省去了对这些变量填充字节0的初始化过程): - - -```cpp -void __init free_area_init_nodes(unsigned long *max_zone_pfn) -{ - /* ...... */ - arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0; - arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0; - - /* Find the PFNs that ZONE_MOVABLE begins at in each node */ - memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn)); - /* 用于计算进入ZONE_MOVABLE的内存数量 */ - find_zone_movable_pfns_for_nodes(); - /* 依次遍历,确定各个内存域的边界 */ - for (i = 1; i < MAX_NR_ZONES; i++) { - /* 由于ZONE_MOVABLE是一个虚拟内存域 - * 不与真正的硬件内存域关联 - * 该内存域的边界总是设置为0 */ - if (i == ZONE_MOVABLE) - continue; - /* 第n个内存域的最小页帧 - * 即前一个(第n-1个)内存域的最大页帧 */ - arch_zone_lowest_possible_pfn[i] = - arch_zone_highest_possible_pfn[i-1]; - /* 不出意外,当前内存域的最大页帧 - * 由max_zone_pfn给出 */ - arch_zone_highest_possible_pfn[i] = - max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]); - } - - /* ...... */ -} -``` -辅助函数find_min_pfn_with_active_regions用于找到注册的最低内存域中可用的编号最小的页帧。该内存域不必一定是ZONE_DMA,例如,在计算机不需要DMA内存的情况下也可以是ZONE_NORMAL。最低内存域的最大页帧号可以从max_zone_pfn提供的信息直接获得。 - -##3.3 构建其他内存域的页帧区间 -------- - -接下来构建其他内存域的页帧区间,方法很直接:第n个内存域的最小页帧,即前一个(第n-1个)内存域的最大页帧。当前内存域的最大页帧由max_zone_pfn给出 - -```cpp -void __init free_area_init_nodes(unsigned long *max_zone_pfn) -{ - /* ...... */ - - arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0; - arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0; - - /* Find the PFNs that ZONE_MOVABLE begins at in each node */ - memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn)); - /* 用于计算进入ZONE_MOVABLE的内存数量 */ - find_zone_movable_pfns_for_nodes(); - - /* ...... */ -} -``` -由于ZONE_MOVABLE是一个虚拟内存域,不与真正的硬件内存域关联,该内存域的边界总是设置为0。回忆前文,可知只有在指定了内核命令行参数kernelcore或movablecore之一时,该内存域才会存在. -该内存域一般开始于各个结点的某个特定内存域的某一页帧号。相应的编号在find_zone_movable_pfns_for_nodes里计算。 - -现在可以向用户提供一些有关已确定的页帧区间的信息。举例来说,其中可能包括下列内容(输出取自AMD64系统,有4 GiB物理内存): - - -```cpp -> dmesg - -Zone PFN ranges: -DMA 0 0 -> 4096 -DMA32 4096 -> 1048576 -Normal 1048576 -> 1245184 -``` - - -##3.4 建立结点数据结构 -------- - - -free_area_init_nodes剩余的部分遍历所有结点,分别建立其数据结构 - -```cpp -void __init free_area_init_nodes(unsigned long *max_zone_pfn) -{ - /* 输出有关内存域的信息 */ - /* ...... */ - - /* 代码遍历所有的活动结点, - * 并分别对各个结点调用free_area_init_node建立数据结构, - * 该函数需要结点第一个可用的页帧作为一个参数, - * 而find_min_pfn_for_node则从early_node_map数组提取该信息 */ - for_each_online_node(nid) { - pg_data_t *pgdat = NODE_DATA(nid); - free_area_init_node(nid, NULL, - find_min_pfn_for_node(nid), NULL); - - /* Any memory on that node - * 根据node_present_pages字段判断结点具有内存 - * 则在结点位图中设置N_HIGH_MEMORY标志 - * 该标志只表示结点上存在普通或高端内存 - * 因此check_for_regular_memory - * 进一步检查低于ZONE_HIGHMEM的内存域中是否有内存 - * 并据此在结点位图中相应地设置N_NORMAL_MEMORY */ - if (pgdat->node_present_pages) - node_set_state(nid, N_MEMORY); - check_for_memory(pgdat, nid); - } - - /* ...... */ -} -``` - - -代码遍历所有活动结点,并分别对各个结点调用free_area_init_node建立数据结构。该函数需要结点第一个可用的页帧作为一个参数,而find_min_pfn_for_node则从early_node_map数组提取该信息。 - -如果根据node_present_pages字段判断结点具有内存,则在结点位图中设置N_HIGH_MEMORY标志。我们知道该标志只表示结点上存在普通或高端内存,因此check_for_regular_memory进一步检查低于ZONE_HIGHMEM的内存域中是否有内存,并据此在结点位图中相应地设置N_NORMAL_MEMORY标志 - - -#4 free_area_init_node初始化UMA内存结点 -------- - -[free_area_init_nodes](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6076)函数初始化所有结点的pg_data_t和zone、page的数据,并打印了管理区信息. - - -##4.1 free_area_init_node函数注释 -------- - -该函数定义在[mm/page_alloc.c?v=4.7, line 6076](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6076) - -```cpp -void __paginginit free_area_init_node(int nid, unsigned long *zones_size, - unsigned long node_start_pfn, unsigned long *zholes_size) -{ - pg_data_t *pgdat = NODE_DATA(nid); - unsigned long start_pfn = 0; - unsigned long end_pfn = 0; - - /* pg_data_t should be reset to zero when it's allocated */ - WARN_ON(pgdat->nr_zones || pgdat->classzone_idx); - - reset_deferred_meminit(pgdat); - pgdat->node_id = nid; - pgdat->node_start_pfn = node_start_pfn; -#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP - get_pfn_range_for_nid(nid, &start_pfn, &end_pfn); - pr_info("Initmem setup node %d [mem %#018Lx-%#018Lx]\n", nid, - (u64)start_pfn << PAGE_SHIFT, - end_pfn ? ((u64)end_pfn << PAGE_SHIFT) - 1 : 0); -#else - start_pfn = node_start_pfn; -#endif - /* 首先累计各个内存域的页数 - * 计算结点中页的总数 - * 对连续内存模型而言 - * 这可以通过zone_sizes_init完成 - * 但calculate_node_totalpages还考虑了内存空洞 */ - calculate_node_totalpages(pgdat, start_pfn, end_pfn, - zones_size, zholes_size); - /* 分配了该节点的页面描述符数组 - * [pgdat->node_mem_map数组的内存分配 */ - alloc_node_mem_map(pgdat); -#ifdef CONFIG_FLAT_NODE_MEM_MAP - printk(KERN_DEBUG "free_area_init_node: node %d, pgdat %08lx, node_mem_map %08lx\n", - nid, (unsigned long)pgdat, - (unsigned long)pgdat->node_mem_map); -#endif - - /* 对该节点的每个区[DMA,NORMAL,HIGH]的的结构进行初始化 */ - free_area_init_core(pgdat); -} -``` -##4.2 流程分析 -------- - -* [calculate_node_totalpages](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5789)函数累计各个内存域的页数,计算结点中页的总数。对连续内存模型而言,这可以通过zone_sizes_init完成,但calculate_node_totalpages还考虑了内存空洞,该函数定义在[mm/page_alloc.c, line 5789](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5789) - - 以下例子取自一个UMA系统, 具有512 MiB物理内存。 -```cpp -> dmesg -... -On node 0 totalpages: 131056 -``` - - -* [alloc_node_mem_map(pgdat)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6030)函数分配了该节点的页面描述符数组[pgdat->node_mem_map数组的内存分配. - - -* 继续调用[free_area_init_core](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5932)函数,继续初始化该节点的pg_data_t结构,初始化zone以及page结构 ,##2.6 free_area_init_core函数是初始化zone的核心 - - - -##4.3 alloc_node_mem_map函数 -------- - - -alloc_node_mem_map负责初始化一个简单但非常重要的数据结构。如上所述,系统中的各个物理内存页,都对应着一个struct page实例。该结构的初始化由alloc_node_mem_map执行 - - -```cpp -static void __init_refok alloc_node_mem_map(struct pglist_data *pgdat) -{ - unsigned long __maybe_unused start = 0; - unsigned long __maybe_unused offset = 0; - - /* Skip empty nodes */ - if (!pgdat->node_spanned_pages) - return; - -#ifdef CONFIG_FLAT_NODE_MEM_MAP - start = pgdat->node_start_pfn & ~(MAX_ORDER_NR_PAGES - 1); - offset = pgdat->node_start_pfn - start; - /* ia64 gets its own node_mem_map, before this, without bootmem */ - if (!pgdat->node_mem_map) { - unsigned long size, end; - struct page *map; - - /* - * The zone's endpoints aren't required to be MAX_ORDER - * aligned but the node_mem_map endpoints must be in order - * for the buddy allocator to function correctly. - */ - end = pgdat_end_pfn(pgdat); - end = ALIGN(end, MAX_ORDER_NR_PAGES); - size = (end - start) * sizeof(struct page); - map = alloc_remap(pgdat->node_id, size); - if (!map) - map = memblock_virt_alloc_node_nopanic(size, - pgdat->node_id); - pgdat->node_mem_map = map + offset; - } -#ifndef CONFIG_NEED_MULTIPLE_NODES - /* - * With no DISCONTIG, the global mem_map is just set as node 0's - */ - if (pgdat == NODE_DATA(0)) { - mem_map = NODE_DATA(0)->node_mem_map; -#if defined(CONFIG_HAVE_MEMBLOCK_NODE_MAP) || defined(CONFIG_FLATMEM) - if (page_to_pfn(mem_map) != pgdat->node_start_pfn) - mem_map -= offset; -#endif /* CONFIG_HAVE_MEMBLOCK_NODE_MAP */ - } -#endif -#endif /* CONFIG_FLAT_NODE_MEM_MAP */ -} -``` -没有页的空结点显然可以跳过。如果特定于体系结构的代码尚未建立内存映射(这是可能的,例如,在IA-64系统上),则必须分配与该结点关联的所有struct page实例所需的内存。各个体系结构可以为此提供一个特定的函数。但目前只有在IA-32系统上使用不连续内存配置时是这样。在所有其他的配置上,则使用普通的自举内存分配器进行分配。请注意,代码将内存映射对齐到伙伴系统的最大分配阶,因为要使所有的计算都工作正常,这是必需的。 - - -指向该空间的指针不仅保存在pglist_data实例中,还保存在全局变量mem_map中,前提是当前考察的结点是系统的第0个结点(如果系统只有一个内存结点,则总是这样)。mem_map是一个全局数组,在讲解内存管理时,我们会经常遇到, 定义在[mm/memory.c?v=4.7, line 85](http://lxr.free-electrons.com/source/mm/memory.c?v=4.7#L85) - -```cpp -struct page *mem_map; -``` - - - -然后在free_area_init_node函数的最后, 通过free_area_init_core来完成内存域zone的初始化 - - - -#5 free_area_init_core初始化内存域zone -------- - -初始化内存域数据结构涉及的繁重工作由free_area_init_core执行,它会依次遍历结点的所有内存域, 该函数定义在[mm/page_alloc.c?v=4.7, line 5932](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5932) - - -##5.1 free_area_init_core函数代码注释 -------- - -```cpp -/* - * Set up the zone data structures: - * - mark all pages reserved - * - mark all memory queues empty - * - clear the memory bitmaps - * - * NOTE: pgdat should get zeroed by caller. - */ -static void __paginginit free_area_init_core(struct pglist_data *pgdat) -{ - enum zone_type j; - int nid = pgdat->node_id; - int ret; - - /* 初始化pgdat->node_size_lock自旋锁 */ - pgdat_resize_init(pgdat); -#ifdef CONFIG_NUMA_BALANCING - spin_lock_init(&pgdat->numabalancing_migrate_lock); - pgdat->numabalancing_migrate_nr_pages = 0; - pgdat->numabalancing_migrate_next_window = jiffies; -#endif -#ifdef CONFIG_TRANSPARENT_HUGEPAGE - spin_lock_init(&pgdat->split_queue_lock); - INIT_LIST_HEAD(&pgdat->split_queue); - pgdat->split_queue_len = 0; -#endif - - /* 初始化pgdat->kswapd_wait等待队列 */ - init_waitqueue_head(&pgdat->kswapd_wait); - /* 初始化页换出守护进程创建空闲块的大小 - * 为2^kswapd_max_order */ - init_waitqueue_head(&pgdat->pfmemalloc_wait); -#ifdef CONFIG_COMPACTION - init_waitqueue_head(&pgdat->kcompactd_wait); -#endif - pgdat_page_ext_init(pgdat); - - /* 遍历每个管理区 */ - for (j = 0; j < MAX_NR_ZONES; j++) { - struct zone *zone = pgdat->node_zones + j; - unsigned long size, realsize, freesize, memmap_pages; - unsigned long zone_start_pfn = zone->zone_start_pfn; - - /* size为该管理区中的页框数,包括洞 */ - size = zone->spanned_pages; - /* realsize为管理区中的页框数,不包括洞 / - realsize = freesize = zone->present_pages; - - /* - * Adjust freesize so that it accounts for how much memory - * is used by this zone for memmap. This affects the watermark - * and per-cpu initialisations - * 调整realsize的大小,即减去page结构体占用的内存大小 */ - /* memmap_pags为包括洞的所有页框的page结构体所占的大小 */ - memmap_pages = calc_memmap_size(size, realsize); - if (!is_highmem_idx(j)) { - if (freesize >= memmap_pages) { - freesize -= memmap_pages; - if (memmap_pages) - printk(KERN_DEBUG - " %s zone: %lu pages used for memmap\n", - zone_names[j], memmap_pages); - } else /* 内存不够存放page结构体 */ - pr_warn(" %s zone: %lu pages exceeds freesize %lu\n", - zone_names[j], memmap_pages, freesize); - } - - /* Account for reserved pages - * 调整realsize的大小,即减去DMA保留页的大小 */ - if (j == 0 && freesize > dma_reserve) { - freesize -= dma_reserve; - printk(KERN_DEBUG " %s zone: %lu pages reserved\n", - zone_names[0], dma_reserve); - } - - if (!is_highmem_idx(j)) - nr_kernel_pages += freesize; - /* Charge for highmem memmap if there are enough kernel pages */ - else if (nr_kernel_pages > memmap_pages * 2) - nr_kernel_pages -= memmap_pages; - nr_all_pages += freesize; - - /* - * Set an approximate value for lowmem here, it will be adjusted - * when the bootmem allocator frees pages into the buddy system. - * And all highmem pages will be managed by the buddy system. - */ - /* 设置zone->spanned_pages为包括洞的页框数 */ - zone->managed_pages = is_highmem_idx(j) ? realsize : freesize; -#ifdef CONFIG_NUMA - /* 设置zone中的节点标识符 */ - zone->node = nid; - /* 设置可回收页面比率 */ - zone->min_unmapped_pages = (freesize*sysctl_min_unmapped_ratio) - / 100; - /* 设置slab回收缓存页的比率 */ - zone->min_slab_pages = (freesize * sysctl_min_slab_ratio) / 100; -#endif - /* 设置zone的名称 */ - zone->name = zone_names[j]; - - /* 初始化各种锁 */ - spin_lock_init(&zone->lock); - spin_lock_init(&zone->lru_lock); - zone_seqlock_init(zone); - /* 设置管理区属于的节点对应的pg_data_t结构 */ - zone->zone_pgdat = pgdat; - /* 初始化cpu的页面缓存 */ - zone_pcp_init(zone); - - /* For bootup, initialized properly in watermark setup */ - mod_zone_page_state(zone, NR_ALLOC_BATCH, zone->managed_pages); - - /* 初始化lru相关成员 */ - lruvec_init(&zone->lruvec); - if (!size) - continue; - - set_pageblock_order(); - /* 定义了CONFIG_SPARSEMEM该函数为空 */ - setup_usemap(pgdat, zone, zone_start_pfn, size); - /* 设置pgdat->nr_zones和zone->zone_start_pfn成员 - * 初始化zone->free_area成员 - * 初始化zone->wait_table相关成员 - */ - ret = init_currently_empty_zone(zone, zone_start_pfn, size); - BUG_ON(ret); - /* 初始化该zone对应的page结构 */ - memmap_init(size, nid, j, zone_start_pfn); - } - /* ...... */ -} -``` - - -##5.2 流程讲解 -------- - - -初始化内存域数据结构涉及的繁重工作由free_area_init_core执行,它会依次遍历结点的所有内存域 - - -```cpp -static void __paginginit free_area_init_core(struct pglist_data *pgdat) -{ - enum zone_type j; - int nid = pgdat->node_id; - int ret; - - /* ...... */ - /* 遍历每个管理区 */ - for (j = 0; j < MAX_NR_ZONES; j++) { - struct zone *zone = pgdat->node_zones + j; - unsigned long size, realsize, freesize, memmap_pages; - unsigned long zone_start_pfn = zone->zone_start_pfn; - - /* size为该管理区中的页框数,包括洞 */ - size = zone->spanned_pages; - /* realsize为管理区中的页框数,不包括洞 / - realsize = freesize = zone->present_pages; - - /* ...... */ -} -``` - - -内存域的真实长度,可通过跨越的页数减去空洞覆盖的页数而得到。这两个值是通过两个辅助函数计算的,我不会更详细地讨论了。其复杂性实质上取决于内存模型和所选定的配置选项,但所有变体最终都没有什么意外之处 - - -```cpp -static void __paginginit free_area_init_core(struct pglist_data *pgdat) -{ - /* ...... */ - if (!is_highmem_idx(j)) - nr_kernel_pages += freesize; - /* Charge for highmem memmap if there are enough kernel pages */ - else if (nr_kernel_pages > memmap_pages * 2) - nr_kernel_pages -= memmap_pages; - nr_all_pages += freesize; - - /* - * Set an approximate value for lowmem here, it will be adjusted - * when the bootmem allocator frees pages into the buddy system. - * And all highmem pages will be managed by the buddy system. - */ - /* 设置zone->spanned_pages为包括洞的页框数 */ - zone->managed_pages = is_highmem_idx(j) ? realsize : freesize; -#ifdef CONFIG_NUMA - /* 设置zone中的节点标识符 */ - zone->node = nid; - /* 设置可回收页面比率 */ - zone->min_unmapped_pages = (freesize*sysctl_min_unmapped_ratio) - / 100; - /* 设置slab回收缓存页的比率 */ - zone->min_slab_pages = (freesize * sysctl_min_slab_ratio) / 100; -#endif - /* 设置zone的名称 */ - zone->name = zone_names[j]; - - /* 初始化各种锁 */ - spin_lock_init(&zone->lock); - spin_lock_init(&zone->lru_lock); - zone_seqlock_init(zone); - /* 设置管理区属于的节点对应的pg_data_t结构 */ - zone->zone_pgdat = pgdat; - /* ...... */ -} -``` - - -内核使用两个全局变量跟踪系统中的页数。nr_kernel_pages统计所有一致映射的页,而nr_all_pages还包括高端内存页在内free_area_init_core始化为0 - -我们比较感兴趣的是调用的两个辅助函数 - -* zone_pcp_init尝试初始化该内存域的per-CPU缓存, 定义在[mm/page_alloc.c?v=4.7, line 5443](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5443) - -* init_currently_empty_zone初始化free_area列表,并将属于该内存域的所有page实例都设置为初始默认值。正如前文的讨论,调用了memmap_init_zone来初始化内存域的页, 定义在[mm/page_alloc.c?v=4.7, line 5458](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5458) - -我们还可以回想前文提到的,所有页属性起初都设置MIGRATE_MOVABLE。 -此外,空闲列表是在zone_init_free_lists中初始化的 - - -```cpp -static void __paginginit free_area_init_core(struct pglist_data *pgdat) -{ - /* ...... */ - { - /* 初始化cpu的页面缓存 */ - zone_pcp_init(zone); - - /* 设置pgdat->nr_zones和zone->zone_start_pfn成员 - * 初始化zone->free_area成员 - * 初始化zone->wait_table相关成员 - */ - ret = init_currently_empty_zone(zone, zone_start_pfn, size); - BUG_ON(ret); - /* 初始化该zone对应的page结构 */ - memmap_init(size, nid, j, zone_start_pfn); - } - /* ...... */ -} -``` - - - -#6 memmap_init初始化page页面 -------- - -在free_area_init_core初始化内存管理区zone的过程中, 通过memmap_init函数对每个内存管理区zone的page内存进行了初始化 - - -memmap_init函数定义在[mm/page_alloc.c?v=4.7, line ](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5241) - -```cpp -#ifndef __HAVE_ARCH_MEMMAP_INIT -#define memmap_init(size, nid, zone, start_pfn) \ - memmap_init_zone((size), (nid), (zone), (start_pfn), MEMMAP_EARLY) -#endif -``` -memmap_init_zone函数完成了page的初始化工作, 该函数定义在[mm/page_alloc.c?v=4.7, line 5139](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5139) - - -至此,节点和管理区的关键数据已完成初始化,内核在后面为内存管理做得一个准备工作就是将所有节点的管理区都链入到zonelist中,便于后面内存分配工作的进行 - -内核在start_kernel()-->build_all_zonelist()中完成zonelist的初始化 - - - - -#7 总结 -------- - - - -##7.1 start_kernel启动流程 -------- - - -```cpp -start_kernel() - |---->page_address_init() - | 考虑支持高端内存 - | 业务:初始化page_address_pool链表; - | 将page_address_maps数组元素按索引降序插入 - | page_address_pool链表; - | 初始化page_address_htable数组. - | - |---->setup_arch(&command_line); - | - |---->setup_per_cpu_areas(); - | 为per-CPU变量分配空间 - | - |---->build_all_zonelist() - | 为系统中的zone建立后备zone的列表. - | 所有zone的后备列表都在 - | pglist_data->node_zonelists[0]中; - | - | 期间也对per-CPU变量boot_pageset做了初始化. - | - |---->page_alloc_init() - |---->hotcpu_notifier(page_alloc_cpu_notifier, 0); - | 不考虑热插拔CPU - | - |---->pidhash_init() - | 详见下文. - | 根据低端内存页数和散列度,分配hash空间,并赋予pid_hash - | - |---->vfs_caches_init_early() - |---->dcache_init_early() - | dentry_hashtable空间,d_hash_shift, h_hash_mask赋值; - | 同pidhash_init(); - | 区别: - | 散列度变化了(13 - PAGE_SHIFT); - | 传入alloc_large_system_hash的最后参数值为0; - | - |---->inode_init_early() - | inode_hashtable空间,i_hash_shift, i_hash_mask赋值; - | 同pidhash_init(); - | 区别: - | 散列度变化了(14 - PAGE_SHIFT); - | 传入alloc_large_system_hash的最后参数值为0; - | -``` - -##7.2 pidhash_init配置高端内存 -------- - - -```cpp -void pidhash_init(void) - |---->pid_hash = alloc_large_system_hash("PID", sizeof(*pid_hash), - | 0, 18, HASH_EARLY|HASH_SMALL, &pidhash_shift, NULL, 4096); - | 根据nr_kernel_pages(低端内存的页数),分配哈希数组,以及各个哈希 - | 数组元素下的哈希链表的空间,原理如下: - | number = nr_kernel_pages; - | number >= (18 - PAGE_SHIFT) 根据散列度获得数组元素个数 - | number = roundup_pow_of_two(number); - | pidhash_shift = max{x | 2**x <= number} - | size = number * sizeof(*pid_hash); - | 使用位图分配器分配size空间,将返回值付给pid_hash; - | - |---->pidhash_size = 1 << pidhash_shift; - | - |---->for(i = 0; i < pidhash_size; i++) - | INIT_HLIST_HEAD(&pid_hash[i]); -``` - -##7.3 build_all_zonelists初始化每个内存节点的zonelists -------- - - - -```cpp -void build_all_zonelists(void) - |---->set_zonelist_order() - |---->current_zonelist_order = ZONELIST_ORDER_ZONE; - | - |---->__build_all_zonelists(NULL); - | Memory不支持热插拔, 为每个zone建立后备的zone, - | 每个zone及自己后备的zone,形成zonelist - | - |---->pg_data_t *pgdat = NULL; - | pgdat = &contig_page_data;(单node) - | - |---->build_zonelists(pgdat); - | 为每个zone建立后备zone的列表 - | - |---->struct zonelist *zonelist = NULL; - | enum zone_type j; - | zonelist = &pgdat->node_zonelists[0]; - | - |---->j = build_zonelists_node(pddat, zonelist, 0, MAX_NR_ZONES - 1); - | 为pgdat->node_zones[0]建立后备的zone,node_zones[0]后备的zone - | 存储在node_zonelist[0]内,对于node_zone[0]的后备zone,其后备的zone - | 链表如下(只考虑UMA体系,而且不考虑ZONE_DMA): - | node_zonelist[0]._zonerefs[0].zone = &node_zones[2]; - | node_zonelist[0]._zonerefs[0].zone_idx = 2; - | node_zonelist[0]._zonerefs[1].zone = &node_zones[1]; - | node_zonelist[0]._zonerefs[1].zone_idx = 1; - | node_zonelist[0]._zonerefs[2].zone = &node_zones[0]; - | node_zonelist[0]._zonerefs[2].zone_idx = 0; - | - | zonelist->_zonerefs[3].zone = NULL; - | zonelist->_zonerefs[3].zone_idx = 0; - | - |---->build_zonelist_cache(pgdat); - |---->pdat->node_zonelists[0].zlcache_ptr = NULL; - | UMA体系结构 - | - |---->for_each_possible_cpu(cpu) - | setup_pageset(&per_cpu(boot_pageset, cpu), 0); - |详见下文 - |---->vm_total_pages = nr_free_pagecache_pages(); - | 业务:获得所有zone中的present_pages总和. - | - |---->page_group_by_mobility_disabled = 0; - | 对于代码中的判断条件一般不会成立,因为页数会最够多(内存较大) +初始化内存管理 +======= + + + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + + +在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后奇偶内核才能检测到可用内存和寄存器. + + + +#1 前景回顾 +------- + + +##1.1 Linux内存管理的层次结构 +------- + + +Linux把物理内存划分为三个层次来管理 + +| 层次 | 描述 | +|:----:|:----:| +| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | +| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | +| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | + +为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 + + +* 首先, 内存被划分为**结点**. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. + +* 接着各个节点又被划分为内存管理区域, 一个**管理区域**通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. + + +* 最后**页帧(page frame)**代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号. + + +##1.2 今日内容(启动过程中的内存初始化) +------- + + +在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. + + +因此我们可以把linux内核的内存管理分三个阶段。 + +| 阶段 | 起点 | 终点 | 描述 | +|:-----:|:-----:|:-----:| +| 第一阶段 | 系统启动 | bootmem或者memblock初始化完成 | 此阶段只能使用memblock_reserve函数分配内存, 早期内核中使用init_bootmem_done = 1标识此阶段结束 | +| 第二阶段 | bootmem或者memblock初始化完 | buddy完成前 | 引导内存分配器bootmem或者memblock接受内存的管理工作, 早期内核中使用mem_init_done = 1标记此阶段的结束 | +| 第三阶段 | buddy初始化完成 | 系统停止运行 | 可以用cache和buddy分配内存 | + + + +##1.3 start_kernel系统启动阶段的内存初始化过程 +------- + +首先我们来看看start_kernel是如何初始化系统的, start_kerne定义在[init/main.c?v=4.7, line 479](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L479) + +其代码很复杂, 我们只截取出其中与内存管理初始化相关的部分, 如下所示 + + +```cpp +asmlinkage __visible void __init start_kernel(void) +{ + + setup_arch(&command_line); + mm_init_cpumask(&init_mm); + + setup_per_cpu_areas(); + + + build_all_zonelists(NULL, NULL); + page_alloc_init(); + + + /* + * These use large bootmem allocations and must precede + * mem_init(); + * kmem_cache_init(); + */ + mm_init(); + + kmem_cache_init_late(); + + kmemleak_init(); + setup_per_cpu_pageset(); + + rest_init(); +} +``` + + +| 函数 | 功能 | +|:----:|:----:| +| [setup_arch](http://lxr.free-electrons.com/ident?v=4.7;i=setup_arch) | 是一个特定于体系结构的设置函数, 其中一项任务是负责初始化自举分配器 | +| [mm_init_cpumask](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v=4.7#L522) | 初始化CPU屏蔽字 | +| [setup_per_cpu_areas](http://lxr.free-electrons.com/ident?v=4.7;i=setup_per_cpu_areas) | 函数[(查看定义)](http://lxr.free-electrons.com/source/mm/percpu.c?v4.7#L2205])给每个CPU分配内存,并拷贝.data.percpu段的数据. 为系统中的每个CPU的per_cpu变量申请空间.
在SMP系统中, setup_per_cpu_areas初始化源代码中(使用[per_cpu宏](http://lxr.free-electrons.com/source/include/linux/percpu-defs.h#L256))定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本.
此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本
在非SMP系统中这是一个空操作 | +| [build_all_zonelists](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029) | 建立并初始化结点和内存域的数据结构 | +| [mm_init](http://lxr.free-electrons.com/source/init/main.c?v4.7#L464) | 建立了内核的内存分配器,
其中通过[mem_init](http://lxr.free-electrons.com/ident?v=4.7&i=mem_init)停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)
然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器 | +| [kmem_cache_init_late](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378) | 在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器[slab](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378), [slob](http://lxr.free-electrons.com/source/mm/slob.c?v4.7#L655), [slub](http://lxr.free-electrons.com/source/mm/slub.c?v=4.7#L3960)都会定义此函数 | +| [kmemleak_init](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1857) | Kmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 [/sys/kernel/debug/kmemleak](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1467)中, Kmemcheck能够帮助定位大多数内存错误的上下文 | +| [setup_per_cpu_pageset](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5392) | 初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配
由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. | + + + + +##1.4 setup_arch函数初始化内存流程 +------- + + +前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作. + +```cpp +void __init setup_arch(char **cmdline_p) +{ + /* 初始化memblock */ + arm64_memblock_init( ); + + /* 分页机制初始化 */ + paging_init(); + + bootmem_init(); +} +``` + +| 流程 | 描述 | +|:---:|:----:| +| [arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) | 初始化memblock内存分配器 | +| [paging_init](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538) | 初始化分页机制 | +| [bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | 初始化内存管理 | + + +该函数主要执行了如下操作 + + +1. 使用arm64_memblock_init来完成memblock机制的初始化工作, 至此memblock分配器接受系统中系统中内存的分配工作 + +2. 调用paging_init来完成系统分页机制的初始化工作, 建立页表, 从而内核可以完成虚拟内存的映射和转换工作 + +3. 最后调用bootmem_init来完成实现buddy内存管理所需要的工作 + + + +##1.5 (第一阶段)启动过程中的内存分配器 +------- + + +在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. + +这个阶段的内存分配其实很简单, 因此我们往往称之为内存分配器(而不是内存管理器), 早期的内核中内存分配器使用的**bootmem引导分配器**, 它基于一个内存位图bitmap, 使用最优适配算法来查找内存, 但是这个分配器有很大的缺陷, 最严重的就是内存碎片的问题, 因此在后来的内核中将其舍弃《而使用了**新的memblock机制**. memblock机制的初始化在arm64上是通过[arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229)函数来实现的 + +```cpp +```cpp +start_kernel() + |---->page_address_init() + | 考虑支持高端内存 + | 业务:初始化page_address_pool链表; + | 将page_address_maps数组元素按索引降序插入 + | page_address_pool链表; + | 初始化page_address_htable数组. + | + |---->setup_arch(&command_line); + | 初始化特定体系结构的内容 + | + |---->arm64_memblock_init( ); + | 初始化引导阶段的内存分配器memblock + | + |---->paging_init(); + | 分页机制初始化 + | + |---->bootmem_init(); [当前位置] + | 始化内存数据结构包括内存节点, 内存域和页帧page + | + |---->arm64_numa_init(); + | 支持numa架构 + | + |---->zone_sizes_init(min, max); + 来初始化节点和管理区的一些数据项 + | + |---->free_area_init_node + | 初始化内存节点 + | + |---->free_area_init_core + | 初始化zone + | + |---->memmap_init + | 初始化page页面 + | + |---->memblock_dump_all(); + | 初始化完成, 显示memblock的保留的所有内存信息 + | + |---->build_all_zonelist() + | 为系统中的zone建立后备zone的列表. + | 所有zone的后备列表都在 + | pglist_data->node_zonelists[0]中; + | + | 期间也对per-CPU变量boot_pageset做了初始化. + | +``` + + +##1.6 今日内容(第二阶段(一)--初始化内存管理数据结构) +------- + + +我们之前讲了在memblock完成之后, 内存初始化开始进入第二阶段, 第二阶段是一个漫长的过程, 它执行了一系列复杂的操作, 从体系结构相关信息的初始化慢慢向上层展开, 其主要执行了如下操作 + + + +**特定于体系结构的设置** + + +在完成了基础的内存结点和内存域的初始化工作以后, 我们必须克服一些硬件的特殊设置 + +* 在初始化内存的结点和内存区域之前, 内核先通过pagging_init初始化了内核的分页机制, 这样我们的虚拟运行空间就初步建立, 并可以完成物理地址到虚拟地址空间的映射工作. + +在arm64架构下, 内核在start_kernel()->setup_arch()中通过arm64_memblock_init( )完成了memblock的初始化之后, 接着通过setup_arch()->paging_init()开始初始化分页机制 + + +paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响 + + +* 在分页机制完成后, 内核通过setup_arch()->bootmem_init开始进行内存基本数据结构(内存结点pg_data_t, 内存域zone和页帧)的初始化工作, 就是在这个函数中, 内核开始从体系结构相关的部分逐渐展开到体系结构无关的部分, 在zone_sizes_init->free_area_init_node中开始, 内核开始进行内存基本数据结构的初始化, 也不再依赖于特定体系结构无关的层次 + +```cpp +bootmem_init() +始化内存数据结构包括内存节点, 内存域和页帧page +| +|---->arm64_numa_init(); +| 支持numa架构 +| +|---->zone_sizes_init(min, max); + 来初始化节点和管理区的一些数据项 + | + |---->free_area_init_node + | 初始化内存节点 + | + |---->free_area_init_core + | 初始化zone + | + |---->memmap_init + | 初始化page页面 +| +|---->memblock_dump_all(); +| 初始化完成, 显示memblock的保留的所有内存信息 +``` + + + + +**建立内存管理的数据结构** + + +对相关数据结构的初始化是从全局启动函数start_kernel中开始的, 该函数在加载内核并激活各个子系统之后执行. 由于内存管理是内核一个非常重要的部分, 因此在特定体系结构的设置步骤中检测并确定系统中内存的分配情况后, 会立即执行内存管理的初始化. + + + +**移交早期的分配器到内存管理器** + + + +最后我们的内存管理器已经初始化并设置完成, 可以投入运行了, 因此内核将内存管理的工作从早期的内存分配器(bootmem或者memblock)移交到我们的buddy伙伴系统. + + + + +#2 初始化前的准备工作 +------- + + +##2.1 回到setup_arch函数(当前已经完成的工作) +------- + + +现在我们回到start_kernel()->setup_arch()函数 + + +```cpp +void __init setup_arch(char **cmdline_p) +{ + /* 初始化memblock */ + arm64_memblock_init( ); + + /* 分页机制初始化 */ + paging_init(); + + bootmem_init(); +} +``` + + +到目前位置我们已经完成了如下工作 + +* memblock已经通过arm64_memblock_init完成了初始化, 至此系统中的内存可以通过memblock分配了 + +* paging_init完成了分页机制的初始化, 至此内核已经布局了一套完整的虚拟内存空间 + + +至此我们所有的内存都可以通过memblock机制来分配和释放, 尽管它实现的笨拙而简易, 但是已经足够我们初始化阶段使用了, 反正内核页不可能指着它过一辈子, 而我们也通过pagging_init创建了页表, 为内核提供了一套可供内核和进程运行的虚拟运行空间, 我们可以安全的进行内存的分配了 + +因此该是时候初始化我们强大的buddy系统了. + +内核接着setup_arch()->bootmem_init()函数开始执行 + +体系结构相关的代码需要在启动期间建立如下信息 + +* 系统中各个内存域的页帧边界,保存在max_zone_pfn数组 + +早期的内核还需记录各结点页帧的分配情况,保存在全局变量early_node_map中 + +![zone_sizes_init函数](../images/arch_do_somethig.png) + +内核提供了一个通用的框架, 用于将上述信息转换为伙伴系统预期的节点和内存域数据结构, 但是在此之前各个体系结构必须自行建立相关结构. + + +##2.2 bootmem_init函数初始化内存结点和管理域 +------- + + +arm64架构下, 在setup_arch中通过paging_init函数初始化内核分页机制之后, 内核通过`bootmem_init()`开始完成内存结点和内存区域的初始化工作, 该函数定义在[arch/arm64/mm/init.c, line 306](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) + +```cpp +void __init bootmem_init(void) +{ + unsigned long min, max; + + min = PFN_UP(memblock_start_of_DRAM()); + max = PFN_DOWN(memblock_end_of_DRAM()); + + early_memtest(min << PAGE_SHIFT, max << PAGE_SHIFT); + + max_pfn = max_low_pfn = max; + + arm64_numa_init(); + /* + * Sparsemem tries to allocate bootmem in memory_present(), so must be + * done after the fixed reservations. + */ + arm64_memory_present(); + + sparse_init(); + zone_sizes_init(min, max); + + high_memory = __va((max << PAGE_SHIFT) - 1) + 1; + memblock_dump_all(); +} +``` + + +##2.3 zone_sizes_init函数 +------- + + +在初始化内存结点和内存域之前, 内核首先通过setup_arch()-->bootmem_init()-->zone_sizes_init()来初始化节点和管理区的一些数据项, 其中关键的是初始化了系统中各个内存域的页帧边界,保存在max_zone_pfn数组. + + + +[zone_sizes_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L92)函数定义在[arch/arm64/mm/init.c?v=4.7, line 92](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L92), 由于arm64支持NUMA和UMA两种存储器架构, 因此该函数依照NUMA和UMA, 有两种不同的实现. + +```cpp +#ifdef CONFIG_NUMA + +static void __init zone_sizes_init(unsigned long min, unsigned long max) +{ + unsigned long max_zone_pfns[MAX_NR_ZONES] = {0}; + + if (IS_ENABLED(CONFIG_ZONE_DMA)) + max_zone_pfns[ZONE_DMA] = PFN_DOWN(max_zone_dma_phys()); + max_zone_pfns[ZONE_NORMAL] = max; + + free_area_init_nodes(max_zone_pfns); +} + +#else + +static void __init zone_sizes_init(unsigned long min, unsigned long max) +{ + struct memblock_region *reg; + unsigned long zone_size[MAX_NR_ZONES], zhole_size[MAX_NR_ZONES]; + unsigned long max_dma = min; + + memset(zone_size, 0, sizeof(zone_size)); + + /* 4GB maximum for 32-bit only capable devices */ +#ifdef CONFIG_ZONE_DMA + max_dma = PFN_DOWN(arm64_dma_phys_limit); + zone_size[ZONE_DMA] = max_dma - min; +#endif + zone_size[ZONE_NORMAL] = max - max_dma; + + memcpy(zhole_size, zone_size, sizeof(zhole_size)); + + for_each_memblock(memory, reg) { + unsigned long start = memblock_region_memory_base_pfn(reg); + unsigned long end = memblock_region_memory_end_pfn(reg); + + if (start >= max) + continue; + +#ifdef CONFIG_ZONE_DMA + if (start < max_dma) { + unsigned long dma_end = min(end, max_dma); + zhole_size[ZONE_DMA] -= dma_end - start; + } +#endif + if (end > max_dma) { + unsigned long normal_end = min(end, max); + unsigned long normal_start = max(start, max_dma); + zhole_size[ZONE_NORMAL] -= normal_end - normal_start; + } + } + + free_area_init_node(0, zone_size, min, zhole_size); +} + +#endif /* CONFIG_NUMA */ +``` + +在获取了三个管理区的页面数后, NUMA架构下通过free_area_init_nodes()来完成后续工作, 其中核心函数为free_area_init_node(),用来针对特定的节点进行初始化, 由于UMA架构下只有一个内存结点, 因此直接通过free_area_init_node来完成内存结点的初始化 + + +截至到目前为止, 体系结构相关的部分已经结束了, 各个体系结构已经自行建立了自己所需的一些底层数据结构, 这些结构建立好以后, 内核将繁重的内存数据结构创建和初始化的工作交给free_area_init_node(s)函数来完成, + +#3 free_area_init_nodes初始化NUMA管理数据结构 +------- + +>注意 +> +>此部分内容参照 +> +>[Linux内存管理伙伴算法](http://www.linuxidc.com/Linux/2012-09/70711p3.htm) +> +>[linux 内存管理 - paging_init 函数](http://blog.csdn.net/decload/article/details/8080126) + + +[free_area_init_nodes](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6460)初始化了NUMA系统中所有结点的pg_data_t和zone、page的数据, 并打印了管理区信息, 该函数定义在[mm/page_alloc.c?v=4.7, line 6460](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6460) + +##3.1 代码注释 +------- + +```cpp +// 初始化各个节点的所有pg_data_t和zone、page的数据 +void __init free_area_init_nodes(unsigned long *max_zone_pfn) +{ + unsigned long start_pfn, end_pfn; + int i, nid; + + /* Record where the zone boundaries are + * 全局数组arch_zone_lowest_possible_pfn + * 用来存储各个内存域可使用的最低内存页帧编号 */ + memset(arch_zone_lowest_possible_pfn, 0, + sizeof(arch_zone_lowest_possible_pfn)); + + /* 全局数组arch_zone_highest_possible_pfn + * 用来存储各个内存域可使用的最高内存页帧编号 */ + memset(arch_zone_highest_possible_pfn, 0, + sizeof(arch_zone_highest_possible_pfn)); + + /* 辅助函数find_min_pfn_with_active_regions + * 用于找到注册的最低内存域中可用的编号最小的页帧 */ + arch_zone_lowest_possible_pfn[0] = find_min_pfn_with_active_regions(); + + /* max_zone_pfn记录了各个内存域包含的最大页帧号 */ + arch_zone_highest_possible_pfn[0] = max_zone_pfn[0]; + + /* 依次遍历,确定各个内存域的边界 */ + for (i = 1; i < MAX_NR_ZONES; i++) { + /* 由于ZONE_MOVABLE是一个虚拟内存域 + * 不与真正的硬件内存域关联 + * 该内存域的边界总是设置为0 */ + if (i == ZONE_MOVABLE) + continue; + /* 第n个内存域的最小页帧 + * 即前一个(第n-1个)内存域的最大页帧 */ + arch_zone_lowest_possible_pfn[i] = + arch_zone_highest_possible_pfn[i-1]; + /* 不出意外,当前内存域的最大页帧 + * 由max_zone_pfn给出 */ + arch_zone_highest_possible_pfn[i] = + max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]); + } + arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0; + arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0; + + /* Find the PFNs that ZONE_MOVABLE begins at in each node */ + memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn)); + /* 用于计算进入ZONE_MOVABLE的内存数量 */ + find_zone_movable_pfns_for_nodes(); + + /* Print out the zone ranges + * 将各个内存域的最大、最小页帧号显示出来 */ + pr_info("Zone ranges:\n"); + for (i = 0; i < MAX_NR_ZONES; i++) { + if (i == ZONE_MOVABLE) + continue; + pr_info(" %-8s ", zone_names[i]); + if (arch_zone_lowest_possible_pfn[i] == + arch_zone_highest_possible_pfn[i]) + pr_cont("empty\n"); + else + pr_cont("[mem %#018Lx-%#018Lx]\n", + (u64)arch_zone_lowest_possible_pfn[i] + << PAGE_SHIFT, + ((u64)arch_zone_highest_possible_pfn[i] + << PAGE_SHIFT) - 1); + } + + /* Print out the PFNs ZONE_MOVABLE begins at in each node */ + pr_info("Movable zone start for each node\n"); + for (i = 0; i < MAX_NUMNODES; i++) { + /* 对每个结点来说,zone_movable_pfn[node_id] + * 表示ZONE_MOVABLE在movable_zone内存域中所取得内存的起始地址 + * 内核确保这些页将用于满足符合ZONE_MOVABLE职责的内存分配 */ + if (zone_movable_pfn[i]) + { + /* 显示各个内存域的分配情况 */ + pr_info(" Node %d: %#018Lx\n", i, + (u64)zone_movable_pfn[i] << PAGE_SHIFT); + } + } + + /* Print out the early node map */ + pr_info("Early memory node ranges\n"); + for_each_mem_pfn_range(i, MAX_NUMNODES, &start_pfn, &end_pfn, &nid) + pr_info(" node %3d: [mem %#018Lx-%#018Lx]\n", nid, + (u64)start_pfn << PAGE_SHIFT, + ((u64)end_pfn << PAGE_SHIFT) - 1); + + /* Initialise every node */ + mminit_verify_pageflags_layout(); + setup_nr_node_ids(); + + /* 代码遍历所有的活动结点, + * 并分别对各个结点调用free_area_init_node建立数据结构, + * 该函数需要结点第一个可用的页帧作为一个参数, + * 而find_min_pfn_for_node则从early_node_map数组提取该信息 */ + for_each_online_node(nid) { + pg_data_t *pgdat = NODE_DATA(nid); + free_area_init_node(nid, NULL, + find_min_pfn_for_node(nid), NULL); + + /* Any memory on that node + * 根据node_present_pages字段判断结点具有内存 + * 则在结点位图中设置N_HIGH_MEMORY标志 + * 该标志只表示结点上存在普通或高端内存 + * 因此check_for_regular_memory + * 进一步检查低于ZONE_HIGHMEM的内存域中是否有内存 + * 并据此在结点位图中相应地设置N_NORMAL_MEMORY */ + if (pgdat->node_present_pages) + node_set_state(nid, N_MEMORY); + check_for_memory(pgdat, nid); + } +} +``` + +free_area_init_nodes函数中通过循环遍历各个节点,循环中调用了free_area_init_node函数初始化该节点对应的pg_data_t和zone、page的数据. + + +##3.2 设置可使用的页帧编号 +------- + + +free_area_init_nodes首先必须分析并改写特定于体系结构的代码提供的信息。其中,需要对照在zone_max_pfn和zone_min_pfn中指定的内存域的边界,计算各个内存域可使用的最低和最高的页帧编号。使用了两个全局数组来存储这些信息: + +参见[mm/page_alloc.c?v=4.7, line 259)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L259) + +```cpp +static unsigned long __meminitdata arch_zone_lowest_possible_pfn[MAX_NR_ZONES]; + +static unsigned long __meminitdata arch_zone_highest_possible_pfn[MAX_NR_ZONES]; +``` + +通过max_zone_pfn传递给free_area_init_nodes的信息记录了各个内存域包含的最大页帧号。 +free_area_init_nodes将该信息转换为一种更方便的表示形式,即以[low, high]形式描述各个内 +存域的页帧区间,存储在前述的全局变量中(我省去了对这些变量填充字节0的初始化过程): + + +```cpp +void __init free_area_init_nodes(unsigned long *max_zone_pfn) +{ + /* ...... */ + arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0; + arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0; + + /* Find the PFNs that ZONE_MOVABLE begins at in each node */ + memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn)); + /* 用于计算进入ZONE_MOVABLE的内存数量 */ + find_zone_movable_pfns_for_nodes(); + /* 依次遍历,确定各个内存域的边界 */ + for (i = 1; i < MAX_NR_ZONES; i++) { + /* 由于ZONE_MOVABLE是一个虚拟内存域 + * 不与真正的硬件内存域关联 + * 该内存域的边界总是设置为0 */ + if (i == ZONE_MOVABLE) + continue; + /* 第n个内存域的最小页帧 + * 即前一个(第n-1个)内存域的最大页帧 */ + arch_zone_lowest_possible_pfn[i] = + arch_zone_highest_possible_pfn[i-1]; + /* 不出意外,当前内存域的最大页帧 + * 由max_zone_pfn给出 */ + arch_zone_highest_possible_pfn[i] = + max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]); + } + + /* ...... */ +} +``` +辅助函数find_min_pfn_with_active_regions用于找到注册的最低内存域中可用的编号最小的页帧。该内存域不必一定是ZONE_DMA,例如,在计算机不需要DMA内存的情况下也可以是ZONE_NORMAL。最低内存域的最大页帧号可以从max_zone_pfn提供的信息直接获得。 + +##3.3 构建其他内存域的页帧区间 +------- + +接下来构建其他内存域的页帧区间,方法很直接:第n个内存域的最小页帧,即前一个(第n-1个)内存域的最大页帧。当前内存域的最大页帧由max_zone_pfn给出 + +```cpp +void __init free_area_init_nodes(unsigned long *max_zone_pfn) +{ + /* ...... */ + + arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0; + arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0; + + /* Find the PFNs that ZONE_MOVABLE begins at in each node */ + memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn)); + /* 用于计算进入ZONE_MOVABLE的内存数量 */ + find_zone_movable_pfns_for_nodes(); + + /* ...... */ +} +``` +由于ZONE_MOVABLE是一个虚拟内存域,不与真正的硬件内存域关联,该内存域的边界总是设置为0。回忆前文,可知只有在指定了内核命令行参数kernelcore或movablecore之一时,该内存域才会存在. +该内存域一般开始于各个结点的某个特定内存域的某一页帧号。相应的编号在find_zone_movable_pfns_for_nodes里计算。 + +现在可以向用户提供一些有关已确定的页帧区间的信息。举例来说,其中可能包括下列内容(输出取自AMD64系统,有4 GiB物理内存): + + +```cpp +> dmesg + +Zone PFN ranges: +DMA 0 0 -> 4096 +DMA32 4096 -> 1048576 +Normal 1048576 -> 1245184 +``` + + +##3.4 建立结点数据结构 +------- + + +free_area_init_nodes剩余的部分遍历所有结点,分别建立其数据结构 + +```cpp +void __init free_area_init_nodes(unsigned long *max_zone_pfn) +{ + /* 输出有关内存域的信息 */ + /* ...... */ + + /* 代码遍历所有的活动结点, + * 并分别对各个结点调用free_area_init_node建立数据结构, + * 该函数需要结点第一个可用的页帧作为一个参数, + * 而find_min_pfn_for_node则从early_node_map数组提取该信息 */ + for_each_online_node(nid) { + pg_data_t *pgdat = NODE_DATA(nid); + free_area_init_node(nid, NULL, + find_min_pfn_for_node(nid), NULL); + + /* Any memory on that node + * 根据node_present_pages字段判断结点具有内存 + * 则在结点位图中设置N_HIGH_MEMORY标志 + * 该标志只表示结点上存在普通或高端内存 + * 因此check_for_regular_memory + * 进一步检查低于ZONE_HIGHMEM的内存域中是否有内存 + * 并据此在结点位图中相应地设置N_NORMAL_MEMORY */ + if (pgdat->node_present_pages) + node_set_state(nid, N_MEMORY); + check_for_memory(pgdat, nid); + } + + /* ...... */ +} +``` + + +代码遍历所有活动结点,并分别对各个结点调用free_area_init_node建立数据结构。该函数需要结点第一个可用的页帧作为一个参数,而find_min_pfn_for_node则从early_node_map数组提取该信息。 + +如果根据node_present_pages字段判断结点具有内存,则在结点位图中设置N_HIGH_MEMORY标志。我们知道该标志只表示结点上存在普通或高端内存,因此check_for_regular_memory进一步检查低于ZONE_HIGHMEM的内存域中是否有内存,并据此在结点位图中相应地设置N_NORMAL_MEMORY标志 + + +#4 free_area_init_node初始化UMA内存结点 +------- + +[free_area_init_nodes](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6076)函数初始化所有结点的pg_data_t和zone、page的数据,并打印了管理区信息. + + +##4.1 free_area_init_node函数注释 +------- + +该函数定义在[mm/page_alloc.c?v=4.7, line 6076](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6076) + +```cpp +void __paginginit free_area_init_node(int nid, unsigned long *zones_size, + unsigned long node_start_pfn, unsigned long *zholes_size) +{ + pg_data_t *pgdat = NODE_DATA(nid); + unsigned long start_pfn = 0; + unsigned long end_pfn = 0; + + /* pg_data_t should be reset to zero when it's allocated */ + WARN_ON(pgdat->nr_zones || pgdat->classzone_idx); + + reset_deferred_meminit(pgdat); + pgdat->node_id = nid; + pgdat->node_start_pfn = node_start_pfn; +#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP + get_pfn_range_for_nid(nid, &start_pfn, &end_pfn); + pr_info("Initmem setup node %d [mem %#018Lx-%#018Lx]\n", nid, + (u64)start_pfn << PAGE_SHIFT, + end_pfn ? ((u64)end_pfn << PAGE_SHIFT) - 1 : 0); +#else + start_pfn = node_start_pfn; +#endif + /* 首先累计各个内存域的页数 + * 计算结点中页的总数 + * 对连续内存模型而言 + * 这可以通过zone_sizes_init完成 + * 但calculate_node_totalpages还考虑了内存空洞 */ + calculate_node_totalpages(pgdat, start_pfn, end_pfn, + zones_size, zholes_size); + /* 分配了该节点的页面描述符数组 + * [pgdat->node_mem_map数组的内存分配 */ + alloc_node_mem_map(pgdat); +#ifdef CONFIG_FLAT_NODE_MEM_MAP + printk(KERN_DEBUG "free_area_init_node: node %d, pgdat %08lx, node_mem_map %08lx\n", + nid, (unsigned long)pgdat, + (unsigned long)pgdat->node_mem_map); +#endif + + /* 对该节点的每个区[DMA,NORMAL,HIGH]的的结构进行初始化 */ + free_area_init_core(pgdat); +} +``` +##4.2 流程分析 +------- + +* [calculate_node_totalpages](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5789)函数累计各个内存域的页数,计算结点中页的总数。对连续内存模型而言,这可以通过zone_sizes_init完成,但calculate_node_totalpages还考虑了内存空洞,该函数定义在[mm/page_alloc.c, line 5789](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5789) + + 以下例子取自一个UMA系统, 具有512 MiB物理内存。 +```cpp +> dmesg +... +On node 0 totalpages: 131056 +``` + + +* [alloc_node_mem_map(pgdat)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6030)函数分配了该节点的页面描述符数组[pgdat->node_mem_map数组的内存分配. + + +* 继续调用[free_area_init_core](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5932)函数,继续初始化该节点的pg_data_t结构,初始化zone以及page结构 ,##2.6 free_area_init_core函数是初始化zone的核心 + + + +##4.3 alloc_node_mem_map函数 +------- + + +alloc_node_mem_map负责初始化一个简单但非常重要的数据结构。如上所述,系统中的各个物理内存页,都对应着一个struct page实例。该结构的初始化由alloc_node_mem_map执行 + + +```cpp +static void __init_refok alloc_node_mem_map(struct pglist_data *pgdat) +{ + unsigned long __maybe_unused start = 0; + unsigned long __maybe_unused offset = 0; + + /* Skip empty nodes */ + if (!pgdat->node_spanned_pages) + return; + +#ifdef CONFIG_FLAT_NODE_MEM_MAP + start = pgdat->node_start_pfn & ~(MAX_ORDER_NR_PAGES - 1); + offset = pgdat->node_start_pfn - start; + /* ia64 gets its own node_mem_map, before this, without bootmem */ + if (!pgdat->node_mem_map) { + unsigned long size, end; + struct page *map; + + /* + * The zone's endpoints aren't required to be MAX_ORDER + * aligned but the node_mem_map endpoints must be in order + * for the buddy allocator to function correctly. + */ + end = pgdat_end_pfn(pgdat); + end = ALIGN(end, MAX_ORDER_NR_PAGES); + size = (end - start) * sizeof(struct page); + map = alloc_remap(pgdat->node_id, size); + if (!map) + map = memblock_virt_alloc_node_nopanic(size, + pgdat->node_id); + pgdat->node_mem_map = map + offset; + } +#ifndef CONFIG_NEED_MULTIPLE_NODES + /* + * With no DISCONTIG, the global mem_map is just set as node 0's + */ + if (pgdat == NODE_DATA(0)) { + mem_map = NODE_DATA(0)->node_mem_map; +#if defined(CONFIG_HAVE_MEMBLOCK_NODE_MAP) || defined(CONFIG_FLATMEM) + if (page_to_pfn(mem_map) != pgdat->node_start_pfn) + mem_map -= offset; +#endif /* CONFIG_HAVE_MEMBLOCK_NODE_MAP */ + } +#endif +#endif /* CONFIG_FLAT_NODE_MEM_MAP */ +} +``` +没有页的空结点显然可以跳过。如果特定于体系结构的代码尚未建立内存映射(这是可能的,例如,在IA-64系统上),则必须分配与该结点关联的所有struct page实例所需的内存。各个体系结构可以为此提供一个特定的函数。但目前只有在IA-32系统上使用不连续内存配置时是这样。在所有其他的配置上,则使用普通的自举内存分配器进行分配。请注意,代码将内存映射对齐到伙伴系统的最大分配阶,因为要使所有的计算都工作正常,这是必需的。 + + +指向该空间的指针不仅保存在pglist_data实例中,还保存在全局变量mem_map中,前提是当前考察的结点是系统的第0个结点(如果系统只有一个内存结点,则总是这样)。mem_map是一个全局数组,在讲解内存管理时,我们会经常遇到, 定义在[mm/memory.c?v=4.7, line 85](http://lxr.free-electrons.com/source/mm/memory.c?v=4.7#L85) + +```cpp +struct page *mem_map; +``` + + + +然后在free_area_init_node函数的最后, 通过free_area_init_core来完成内存域zone的初始化 + + + +#5 free_area_init_core初始化内存域zone +------- + +初始化内存域数据结构涉及的繁重工作由free_area_init_core执行,它会依次遍历结点的所有内存域, 该函数定义在[mm/page_alloc.c?v=4.7, line 5932](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5932) + + +##5.1 free_area_init_core函数代码注释 +------- + +```cpp +/* + * Set up the zone data structures: + * - mark all pages reserved + * - mark all memory queues empty + * - clear the memory bitmaps + * + * NOTE: pgdat should get zeroed by caller. + */ +static void __paginginit free_area_init_core(struct pglist_data *pgdat) +{ + enum zone_type j; + int nid = pgdat->node_id; + int ret; + + /* 初始化pgdat->node_size_lock自旋锁 */ + pgdat_resize_init(pgdat); +#ifdef CONFIG_NUMA_BALANCING + spin_lock_init(&pgdat->numabalancing_migrate_lock); + pgdat->numabalancing_migrate_nr_pages = 0; + pgdat->numabalancing_migrate_next_window = jiffies; +#endif +#ifdef CONFIG_TRANSPARENT_HUGEPAGE + spin_lock_init(&pgdat->split_queue_lock); + INIT_LIST_HEAD(&pgdat->split_queue); + pgdat->split_queue_len = 0; +#endif + + /* 初始化pgdat->kswapd_wait等待队列 */ + init_waitqueue_head(&pgdat->kswapd_wait); + /* 初始化页换出守护进程创建空闲块的大小 + * 为2^kswapd_max_order */ + init_waitqueue_head(&pgdat->pfmemalloc_wait); +#ifdef CONFIG_COMPACTION + init_waitqueue_head(&pgdat->kcompactd_wait); +#endif + pgdat_page_ext_init(pgdat); + + /* 遍历每个管理区 */ + for (j = 0; j < MAX_NR_ZONES; j++) { + struct zone *zone = pgdat->node_zones + j; + unsigned long size, realsize, freesize, memmap_pages; + unsigned long zone_start_pfn = zone->zone_start_pfn; + + /* size为该管理区中的页框数,包括洞 */ + size = zone->spanned_pages; + /* realsize为管理区中的页框数,不包括洞 / + realsize = freesize = zone->present_pages; + + /* + * Adjust freesize so that it accounts for how much memory + * is used by this zone for memmap. This affects the watermark + * and per-cpu initialisations + * 调整realsize的大小,即减去page结构体占用的内存大小 */ + /* memmap_pags为包括洞的所有页框的page结构体所占的大小 */ + memmap_pages = calc_memmap_size(size, realsize); + if (!is_highmem_idx(j)) { + if (freesize >= memmap_pages) { + freesize -= memmap_pages; + if (memmap_pages) + printk(KERN_DEBUG + " %s zone: %lu pages used for memmap\n", + zone_names[j], memmap_pages); + } else /* 内存不够存放page结构体 */ + pr_warn(" %s zone: %lu pages exceeds freesize %lu\n", + zone_names[j], memmap_pages, freesize); + } + + /* Account for reserved pages + * 调整realsize的大小,即减去DMA保留页的大小 */ + if (j == 0 && freesize > dma_reserve) { + freesize -= dma_reserve; + printk(KERN_DEBUG " %s zone: %lu pages reserved\n", + zone_names[0], dma_reserve); + } + + if (!is_highmem_idx(j)) + nr_kernel_pages += freesize; + /* Charge for highmem memmap if there are enough kernel pages */ + else if (nr_kernel_pages > memmap_pages * 2) + nr_kernel_pages -= memmap_pages; + nr_all_pages += freesize; + + /* + * Set an approximate value for lowmem here, it will be adjusted + * when the bootmem allocator frees pages into the buddy system. + * And all highmem pages will be managed by the buddy system. + */ + /* 设置zone->spanned_pages为包括洞的页框数 */ + zone->managed_pages = is_highmem_idx(j) ? realsize : freesize; +#ifdef CONFIG_NUMA + /* 设置zone中的节点标识符 */ + zone->node = nid; + /* 设置可回收页面比率 */ + zone->min_unmapped_pages = (freesize*sysctl_min_unmapped_ratio) + / 100; + /* 设置slab回收缓存页的比率 */ + zone->min_slab_pages = (freesize * sysctl_min_slab_ratio) / 100; +#endif + /* 设置zone的名称 */ + zone->name = zone_names[j]; + + /* 初始化各种锁 */ + spin_lock_init(&zone->lock); + spin_lock_init(&zone->lru_lock); + zone_seqlock_init(zone); + /* 设置管理区属于的节点对应的pg_data_t结构 */ + zone->zone_pgdat = pgdat; + /* 初始化cpu的页面缓存 */ + zone_pcp_init(zone); + + /* For bootup, initialized properly in watermark setup */ + mod_zone_page_state(zone, NR_ALLOC_BATCH, zone->managed_pages); + + /* 初始化lru相关成员 */ + lruvec_init(&zone->lruvec); + if (!size) + continue; + + set_pageblock_order(); + /* 定义了CONFIG_SPARSEMEM该函数为空 */ + setup_usemap(pgdat, zone, zone_start_pfn, size); + /* 设置pgdat->nr_zones和zone->zone_start_pfn成员 + * 初始化zone->free_area成员 + * 初始化zone->wait_table相关成员 + */ + ret = init_currently_empty_zone(zone, zone_start_pfn, size); + BUG_ON(ret); + /* 初始化该zone对应的page结构 */ + memmap_init(size, nid, j, zone_start_pfn); + } + /* ...... */ +} +``` + + +##5.2 流程讲解 +------- + + +初始化内存域数据结构涉及的繁重工作由free_area_init_core执行,它会依次遍历结点的所有内存域 + + +```cpp +static void __paginginit free_area_init_core(struct pglist_data *pgdat) +{ + enum zone_type j; + int nid = pgdat->node_id; + int ret; + + /* ...... */ + /* 遍历每个管理区 */ + for (j = 0; j < MAX_NR_ZONES; j++) { + struct zone *zone = pgdat->node_zones + j; + unsigned long size, realsize, freesize, memmap_pages; + unsigned long zone_start_pfn = zone->zone_start_pfn; + + /* size为该管理区中的页框数,包括洞 */ + size = zone->spanned_pages; + /* realsize为管理区中的页框数,不包括洞 / + realsize = freesize = zone->present_pages; + + /* ...... */ +} +``` + + +内存域的真实长度,可通过跨越的页数减去空洞覆盖的页数而得到。这两个值是通过两个辅助函数计算的,我不会更详细地讨论了。其复杂性实质上取决于内存模型和所选定的配置选项,但所有变体最终都没有什么意外之处 + + +```cpp +static void __paginginit free_area_init_core(struct pglist_data *pgdat) +{ + /* ...... */ + if (!is_highmem_idx(j)) + nr_kernel_pages += freesize; + /* Charge for highmem memmap if there are enough kernel pages */ + else if (nr_kernel_pages > memmap_pages * 2) + nr_kernel_pages -= memmap_pages; + nr_all_pages += freesize; + + /* + * Set an approximate value for lowmem here, it will be adjusted + * when the bootmem allocator frees pages into the buddy system. + * And all highmem pages will be managed by the buddy system. + */ + /* 设置zone->spanned_pages为包括洞的页框数 */ + zone->managed_pages = is_highmem_idx(j) ? realsize : freesize; +#ifdef CONFIG_NUMA + /* 设置zone中的节点标识符 */ + zone->node = nid; + /* 设置可回收页面比率 */ + zone->min_unmapped_pages = (freesize*sysctl_min_unmapped_ratio) + / 100; + /* 设置slab回收缓存页的比率 */ + zone->min_slab_pages = (freesize * sysctl_min_slab_ratio) / 100; +#endif + /* 设置zone的名称 */ + zone->name = zone_names[j]; + + /* 初始化各种锁 */ + spin_lock_init(&zone->lock); + spin_lock_init(&zone->lru_lock); + zone_seqlock_init(zone); + /* 设置管理区属于的节点对应的pg_data_t结构 */ + zone->zone_pgdat = pgdat; + /* ...... */ +} +``` + + +内核使用两个全局变量跟踪系统中的页数。nr_kernel_pages统计所有一致映射的页,而nr_all_pages还包括高端内存页在内free_area_init_core始化为0 + +我们比较感兴趣的是调用的两个辅助函数 + +* zone_pcp_init尝试初始化该内存域的per-CPU缓存, 定义在[mm/page_alloc.c?v=4.7, line 5443](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5443) + +* init_currently_empty_zone初始化free_area列表,并将属于该内存域的所有page实例都设置为初始默认值。正如前文的讨论,调用了memmap_init_zone来初始化内存域的页, 定义在[mm/page_alloc.c?v=4.7, line 5458](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5458) + +我们还可以回想前文提到的,所有页属性起初都设置MIGRATE_MOVABLE。 +此外,空闲列表是在zone_init_free_lists中初始化的 + + +```cpp +static void __paginginit free_area_init_core(struct pglist_data *pgdat) +{ + /* ...... */ + { + /* 初始化cpu的页面缓存 */ + zone_pcp_init(zone); + + /* 设置pgdat->nr_zones和zone->zone_start_pfn成员 + * 初始化zone->free_area成员 + * 初始化zone->wait_table相关成员 + */ + ret = init_currently_empty_zone(zone, zone_start_pfn, size); + BUG_ON(ret); + /* 初始化该zone对应的page结构 */ + memmap_init(size, nid, j, zone_start_pfn); + } + /* ...... */ +} +``` + + + +#6 memmap_init初始化page页面 +------- + +在free_area_init_core初始化内存管理区zone的过程中, 通过memmap_init函数对每个内存管理区zone的page内存进行了初始化 + + +memmap_init函数定义在[mm/page_alloc.c?v=4.7, line ](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5241) + +```cpp +#ifndef __HAVE_ARCH_MEMMAP_INIT +#define memmap_init(size, nid, zone, start_pfn) \ + memmap_init_zone((size), (nid), (zone), (start_pfn), MEMMAP_EARLY) +#endif +``` +memmap_init_zone函数完成了page的初始化工作, 该函数定义在[mm/page_alloc.c?v=4.7, line 5139](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5139) + + +至此,节点和管理区的关键数据已完成初始化,内核在后面为内存管理做得一个准备工作就是将所有节点的管理区都链入到zonelist中,便于后面内存分配工作的进行 + +内核在start_kernel()-->build_all_zonelist()中完成zonelist的初始化 + + + + +#7 总结 +------- + + + +##7.1 start_kernel启动流程 +------- + + +```cpp +start_kernel() + |---->page_address_init() + | 考虑支持高端内存 + | 业务:初始化page_address_pool链表; + | 将page_address_maps数组元素按索引降序插入 + | page_address_pool链表; + | 初始化page_address_htable数组. + | + |---->setup_arch(&command_line); + | + |---->setup_per_cpu_areas(); + | 为per-CPU变量分配空间 + | + |---->build_all_zonelist() + | 为系统中的zone建立后备zone的列表. + | 所有zone的后备列表都在 + | pglist_data->node_zonelists[0]中; + | + | 期间也对per-CPU变量boot_pageset做了初始化. + | + |---->page_alloc_init() + |---->hotcpu_notifier(page_alloc_cpu_notifier, 0); + | 不考虑热插拔CPU + | + |---->pidhash_init() + | 详见下文. + | 根据低端内存页数和散列度,分配hash空间,并赋予pid_hash + | + |---->vfs_caches_init_early() + |---->dcache_init_early() + | dentry_hashtable空间,d_hash_shift, h_hash_mask赋值; + | 同pidhash_init(); + | 区别: + | 散列度变化了(13 - PAGE_SHIFT); + | 传入alloc_large_system_hash的最后参数值为0; + | + |---->inode_init_early() + | inode_hashtable空间,i_hash_shift, i_hash_mask赋值; + | 同pidhash_init(); + | 区别: + | 散列度变化了(14 - PAGE_SHIFT); + | 传入alloc_large_system_hash的最后参数值为0; + | +``` + +##7.2 pidhash_init配置高端内存 +------- + + +```cpp +void pidhash_init(void) + |---->pid_hash = alloc_large_system_hash("PID", sizeof(*pid_hash), + | 0, 18, HASH_EARLY|HASH_SMALL, &pidhash_shift, NULL, 4096); + | 根据nr_kernel_pages(低端内存的页数),分配哈希数组,以及各个哈希 + | 数组元素下的哈希链表的空间,原理如下: + | number = nr_kernel_pages; + | number >= (18 - PAGE_SHIFT) 根据散列度获得数组元素个数 + | number = roundup_pow_of_two(number); + | pidhash_shift = max{x | 2**x <= number} + | size = number * sizeof(*pid_hash); + | 使用位图分配器分配size空间,将返回值付给pid_hash; + | + |---->pidhash_size = 1 << pidhash_shift; + | + |---->for(i = 0; i < pidhash_size; i++) + | INIT_HLIST_HEAD(&pid_hash[i]); +``` + +##7.3 build_all_zonelists初始化每个内存节点的zonelists +------- + + + +```cpp +void build_all_zonelists(void) + |---->set_zonelist_order() + |---->current_zonelist_order = ZONELIST_ORDER_ZONE; + | + |---->__build_all_zonelists(NULL); + | Memory不支持热插拔, 为每个zone建立后备的zone, + | 每个zone及自己后备的zone,形成zonelist + | + |---->pg_data_t *pgdat = NULL; + | pgdat = &contig_page_data;(单node) + | + |---->build_zonelists(pgdat); + | 为每个zone建立后备zone的列表 + | + |---->struct zonelist *zonelist = NULL; + | enum zone_type j; + | zonelist = &pgdat->node_zonelists[0]; + | + |---->j = build_zonelists_node(pddat, zonelist, 0, MAX_NR_ZONES - 1); + | 为pgdat->node_zones[0]建立后备的zone,node_zones[0]后备的zone + | 存储在node_zonelist[0]内,对于node_zone[0]的后备zone,其后备的zone + | 链表如下(只考虑UMA体系,而且不考虑ZONE_DMA): + | node_zonelist[0]._zonerefs[0].zone = &node_zones[2]; + | node_zonelist[0]._zonerefs[0].zone_idx = 2; + | node_zonelist[0]._zonerefs[1].zone = &node_zones[1]; + | node_zonelist[0]._zonerefs[1].zone_idx = 1; + | node_zonelist[0]._zonerefs[2].zone = &node_zones[0]; + | node_zonelist[0]._zonerefs[2].zone_idx = 0; + | + | zonelist->_zonerefs[3].zone = NULL; + | zonelist->_zonerefs[3].zone_idx = 0; + | + |---->build_zonelist_cache(pgdat); + |---->pdat->node_zonelists[0].zlcache_ptr = NULL; + | UMA体系结构 + | + |---->for_each_possible_cpu(cpu) + | setup_pageset(&per_cpu(boot_pageset, cpu), 0); + |详见下文 + |---->vm_total_pages = nr_free_pagecache_pages(); + | 业务:获得所有zone中的present_pages总和. + | + |---->page_group_by_mobility_disabled = 0; + | 对于代码中的判断条件一般不会成立,因为页数会最够多(内存较大) ``` \ No newline at end of file diff --git a/study/kernel/02-memory/03-initialize/05-build_zonelists/README.md b/study/kernel/02-memory/03-initialize/05-build_zonelists/README.md index 7c209c9..995b718 100644 --- a/study/kernel/02-memory/03-initialize/05-build_zonelists/README.md +++ b/study/kernel/02-memory/03-initialize/05-build_zonelists/README.md @@ -1,1123 +1,1123 @@ -初始化内存管理 -======= - - - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - - -在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后奇偶内核才能检测到可用内存和寄存器. - - - -#1 前景回顾 -------- - - -##1.1 Linux内存管理的层次结构 -------- - - -Linux把物理内存划分为三个层次来管理 - -| 层次 | 描述 | -|:----:|:----:| -| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | -| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | -| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | - -为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 - - -* 首先, 内存被划分为**结点**. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. - -* 接着各个节点又被划分为内存管理区域, 一个**管理区域**通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. - - -* 最后**页帧(page frame)**代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号. - - -##1.2 今日内容(启动过程中的内存初始化) -------- - - -在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. - - -因此我们可以把linux内核的内存管理分三个阶段。 - -| 阶段 | 起点 | 终点 | 描述 | -|:-----:|:-----:|:-----:| -| 第一阶段 | 系统启动 | bootmem或者memblock初始化完成 | 此阶段只能使用memblock_reserve函数分配内存, 早期内核中使用init_bootmem_done = 1标识此阶段结束 | -| 第二阶段 | bootmem或者memblock初始化完 | buddy完成前 | 引导内存分配器bootmem或者memblock接受内存的管理工作, 早期内核中使用mem_init_done = 1标记此阶段的结束 | -| 第三阶段 | buddy初始化完成 | 系统停止运行 | 可以用cache和buddy分配内存 | - - - -##1.3 start_kernel系统启动阶段的内存初始化过程 -------- - -首先我们来看看start_kernel是如何初始化系统的, start_kerne定义在[init/main.c?v=4.7, line 479](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L479) - -其代码很复杂, 我们只截取出其中与内存管理初始化相关的部分, 如下所示 - - -```cpp -asmlinkage __visible void __init start_kernel(void) -{ - - setup_arch(&command_line); - mm_init_cpumask(&init_mm); - - setup_per_cpu_areas(); - - - build_all_zonelists(NULL, NULL); - page_alloc_init(); - - - /* - * These use large bootmem allocations and must precede - * mem_init(); - * kmem_cache_init(); - */ - mm_init(); - - kmem_cache_init_late(); - - kmemleak_init(); - setup_per_cpu_pageset(); - - rest_init(); -} -``` - - -| 函数 | 功能 | -|:----:|:----:| -| [setup_arch](http://lxr.free-electrons.com/ident?v=4.7;i=setup_arch) | 是一个特定于体系结构的设置函数, 其中一项任务是负责初始化自举分配器 | -| [mm_init_cpumask](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v=4.7#L522) | 初始化CPU屏蔽字 | -| [setup_per_cpu_areas](http://lxr.free-electrons.com/ident?v=4.7;i=setup_per_cpu_areas) | 函数[(查看定义)](http://lxr.free-electrons.com/source/mm/percpu.c?v4.7#L2205])给每个CPU分配内存,并拷贝.data.percpu段的数据. 为系统中的每个CPU的per_cpu变量申请空间.
在SMP系统中, setup_per_cpu_areas初始化源代码中(使用[per_cpu宏](http://lxr.free-electrons.com/source/include/linux/percpu-defs.h#L256))定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本.
此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本
在非SMP系统中这是一个空操作 | -| [build_all_zonelists](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029) | 建立并初始化结点和内存域的数据结构 | -| [mm_init](http://lxr.free-electrons.com/source/init/main.c?v4.7#L464) | 建立了内核的内存分配器,
其中通过[mem_init](http://lxr.free-electrons.com/ident?v=4.7&i=mem_init)停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)
然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器 | -| [kmem_cache_init_late](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378) | 在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器[slab](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378), [slob](http://lxr.free-electrons.com/source/mm/slob.c?v4.7#L655), [slub](http://lxr.free-electrons.com/source/mm/slub.c?v=4.7#L3960)都会定义此函数 | -| [kmemleak_init](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1857) | Kmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 [/sys/kernel/debug/kmemleak](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1467)中, Kmemcheck能够帮助定位大多数内存错误的上下文 | -| [setup_per_cpu_pageset](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5392) | 初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配
由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. | - - - - -##1.4 setup_arch函数初始化内存流程 -------- - - -前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作. - -```cpp -void __init setup_arch(char **cmdline_p) -{ - /* 初始化memblock */ - arm64_memblock_init( ); - - /* 分页机制初始化 */ - paging_init(); - - bootmem_init(); -} -``` - -| 流程 | 描述 | -|:---:|:----:| -| [arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) | 初始化memblock内存分配器 | -| [paging_init](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538) | 初始化分页机制 | -| [bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | 初始化内存管理 | - - -该函数主要执行了如下操作 - - -1. 使用arm64_memblock_init来完成memblock机制的初始化工作, 至此memblock分配器接受系统中系统中内存的分配工作 - -2. 调用paging_init来完成系统分页机制的初始化工作, 建立页表, 从而内核可以完成虚拟内存的映射和转换工作 - -3. 最后调用bootmem_init来完成实现buddy内存管理所需要的工作 - - - -##1.5 (第一阶段)启动过程中的内存分配器 -------- - - -在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. - -这个阶段的内存分配其实很简单, 因此我们往往称之为内存分配器(而不是内存管理器), 早期的内核中内存分配器使用的**bootmem引导分配器**, 它基于一个内存位图bitmap, 使用最优适配算法来查找内存, 但是这个分配器有很大的缺陷, 最严重的就是内存碎片的问题, 因此在后来的内核中将其舍弃《而使用了**新的memblock机制**. memblock机制的初始化在arm64上是通过[arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229)函数来实现的 - -```cpp -```cpp -start_kernel() - |---->page_address_init() - | 考虑支持高端内存 - | 业务:初始化page_address_pool链表; - | 将page_address_maps数组元素按索引降序插入 - | page_address_pool链表; - | 初始化page_address_htable数组. - | - |---->setup_arch(&command_line); - | 初始化特定体系结构的内容 - | - |---->arm64_memblock_init( ); - | 初始化引导阶段的内存分配器memblock - | - |---->paging_init(); - | 分页机制初始化 - | - |---->bootmem_init(); - | 始化内存数据结构包括内存节点, 内存域和页帧page - | - |---->arm64_numa_init(); - | 支持numa架构 - | - |---->zone_sizes_init(min, max); - 来初始化节点和管理区的一些数据项 - | - |---->free_area_init_node - | 初始化内存节点 - | - |---->free_area_init_core - | 初始化zone - | - |---->memmap_init - | 初始化page页面 - | - |---->memblock_dump_all(); - | 初始化完成, 显示memblock的保留的所有内存信息 - | - |---->build_all_zonelist() [当前位置] - | 为系统中的zone建立后备zone的列表. - | 所有zone的后备列表都在 - | pglist_data->node_zonelists[0]中; - | - | 期间也对per-CPU变量boot_pageset做了初始化. - | -``` - - -##1.6 今日内容(第二阶段(二)--初始化备用内存域列表zonelists) -------- - - -我们之前讲了在memblock完成之后, 内存初始化开始进入第二阶段, 第二阶段是一个漫长的过程, 它执行了一系列复杂的操作, 从体系结构相关信息的初始化慢慢向上层展开, 其主要执行了如下操作 - - - -**特定于体系结构的设置** - - -在完成了基础的内存结点和内存域的初始化工作以后, 我们必须克服一些硬件的特殊设置 - -* 在初始化内存的结点和内存区域之前, 内核先通过pagging_init初始化了内核的分页机制, 这样我们的虚拟运行空间就初步建立, 并可以完成物理地址到虚拟地址空间的映射工作. - -在arm64架构下, 内核在start_kernel()->setup_arch()中通过arm64_memblock_init( )完成了memblock的初始化之后, 接着通过setup_arch()->paging_init()开始初始化分页机制 - - -paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响 - - -* 在分页机制完成后, 内核通过setup_arch()->bootmem_init开始进行内存基本数据结构(内存结点pg_data_t, 内存域zone和页帧)的初始化工作, 就是在这个函数中, 内核开始从体系结构相关的部分逐渐展开到体系结构无关的部分, 在zone_sizes_init->free_area_init_node中开始, 内核开始进行内存基本数据结构的初始化, 也不再依赖于特定体系结构无关的层次 - -```cpp -bootmem_init -始化内存数据结构包括内存节点, 内存域和页帧page -| -|---->arm64_numa_init(); -| 支持numa架构 -| -|---->zone_sizes_init(min, max); - 来初始化节点和管理区的一些数据项 - | - |---->free_area_init_node - | 初始化内存节点 - | - |---->free_area_init_core - | 初始化zone - | - |---->memmap_init - | 初始化page页面 -| -|---->memblock_dump_all(); -| 初始化完成, 显示memblock的保留的所有内存信息 -``` - - - -至此,bootmem_init已经完成了节点和管理区的关键数据已完成初始化, 内核在后面为内存管理做得一个准备工作就是将所有节点的管理区都链入到zonelist中,便于后面内存分配工作的进行. - - -内核在start_kernel()-->build_all_zonelist()中完成zonelist的初始化 - - -#2 后备内存域列表zonelists -------- - - -内核setup_arch的最后通过bootmem_init中完成了内存数据结构的初始化(包括内存结点pg_data_t, 内存管理域zone和页面信息page), 数据结构已经基本准备好了, 在后面为内存管理做得一个准备工作就是将所有节点的管理区都链入到zonelist中, 便于后面内存分配工作的进行. - - -##2.1 回到start_kernel函数(已经完成的工作) -------- - - -前面我们分析了start_kernel()->setup_arch()函数, 已经完成了memblock内存分配器的创建和初始化工作, 然后paging_init也完成分页机制的初始化, 然后bootmem_init也完成了内存结点和内存管理域的初始化工作. setup_arch函数已经执行完了, 现在我们回到start_kernel - - -```cpp -asmlinkage __visible void __init start_kernel(void) -{ - - setup_arch(&command_line); - - - build_all_zonelists(NULL, NULL); - page_alloc_init(); - - - /* - * These use large bootmem allocations and must precede - * mem_init(); - * kmem_cache_init(); - */ - mm_init(); - - kmem_cache_init_late(); - - kmemleak_init(); - setup_per_cpu_pageset(); - - rest_init(); -} -``` - -下面内核开始通过start_kernel()->build_all_zonelists来设计内存的组织形式 - - -##2.2 后备内存域列表zonelist -------- - -内存节点pg_data_t中将内存节点中的内存区域zone按照某种组织层次存储在一个zonelist中, 即pglist_data->node_zonelists成员信息 - -```cpp -// http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L626 -typedef struct pglist_data -{ - struct zone node_zones[MAX_NR_ZONES]; - struct zonelist node_zonelists[MAX_ZONELISTS]; -} -``` - - -内核定义了内存的一个层次结构关系, 首先试图分配廉价的内存,如果失败,则根据访问速度和容量,逐渐尝试分配更昂贵的内存. - -高端内存最廉价, 因为内核没有任何部分依赖于从该内存域分配的内存, 如果高端内存用尽, 对内核没有副作用, 所以优先分配高端内存 - -普通内存域的情况有所不同, 许多内核数据结构必须保存在该内存域, 而不能放置到高端内存域, 因此如果普通内存域用尽, 那么内核会面临内存紧张的情况 - -DMA内存域最昂贵,因为它用于外设和系统之间的数据传输。 -举例来讲,如果内核指定想要分配高端内存域。它首先在当前结点的高端内存域寻找适当的空闲内存段,如果失败,则查看该结点的普通内存域,如果还失败,则试图在该结点的DMA内存域分配。如果在3个本地内存域都无法找到空闲内存,则查看其他结点。这种情况下,备选结点应该尽可能靠近主结点,以最小化访问非本地内存引起的性能损失。 - - - -##2.3 build_all_zonelists初始化zonelists -------- - -内核在start_kernel中通过build_all_zonelists完成了内存结点及其管理内存域的初始化工作, 调用如下 - - -```cpp - build_all_zonelists(NULL, NULL); -``` - -[build_all_zonelists](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029)建立内存管理结点及其内存域的组织形式, 将描述内存的数据结构(结点, 管理域, 页帧)通过一定的算法组织在一起, 方便以后内存管理工作的进行. 该函数定义在[mm/page_alloc.c?v4.7, line 5029](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029) - - -##2.4 build_all_zonelists函数 -------- - -```cpp -/* - * Called with zonelists_mutex held always - * unless system_state == SYSTEM_BOOTING. - * - * __ref due to (1) call of __meminit annotated setup_zone_pageset - * [we're only called with non-NULL zone through __meminit paths] and - * (2) call of __init annotated helper build_all_zonelists_init - * [protected by SYSTEM_BOOTING]. - */ -void __ref build_all_zonelists(pg_data_t *pgdat, struct zone *zone) -{ - /* 设置zonelist中节点和内存域的组织形式 - * current_zonelist_order变量标识了当前系统的内存组织形式 - * zonelist_order_name以字符串存储了系统中内存组织形式的名称 */ - set_zonelist_order(); - - if (system_state == SYSTEM_BOOTING) { - build_all_zonelists_init(); - } else { -#ifdef CONFIG_MEMORY_HOTPLUG - if (zone) - setup_zone_pageset(zone); -#endif - /* we have to stop all cpus to guarantee there is no user - of zonelist */ - stop_machine(__build_all_zonelists, pgdat, NULL); - /* cpuset refresh routine should be here */ - } - vm_total_pages = nr_free_pagecache_pages(); - /* - * Disable grouping by mobility if the number of pages in the - * system is too low to allow the mechanism to work. It would be - * more accurate, but expensive to check per-zone. This check is - * made on memory-hotadd so a system can start with mobility - * disabled and enable it later - */ - if (vm_total_pages < (pageblock_nr_pages * MIGRATE_TYPES)) - page_group_by_mobility_disabled = 1; - else - page_group_by_mobility_disabled = 0; - - pr_info("Built %i zonelists in %s order, mobility grouping %s. Total pages: %ld\n", - nr_online_nodes, - zonelist_order_name[current_zonelist_order], - page_group_by_mobility_disabled ? "off" : "on", - vm_total_pages); -#ifdef CONFIG_NUMA - pr_info("Policy zone: %s\n", zone_names[policy_zone]); -#endif -} -``` - - -#3 设置结点初始化顺序 -------- - - -在build_all_zonelists开始, 首先内核通过set_zonelist_order函数设置了`zonelist_order`,如下所示, 参见[mm/page_alloc.c?v=4.7, line 5031](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5031) - -```cpp -void __ref build_all_zonelists(pg_data_t *pgdat, struct zone *zone) -{ - set_zonelist_order(); - /* ....... */ -} -``` - - -##3.1 zonelist -------- - - -前面我们讲解内存管理域时候讲解到, 系统中的所有管理域都存储在一个多维的数组zone_table. 内核在初始化内存管理区时, 必须要建立管理区表zone_table. 参见[mm/page_alloc.c?v=2.4.37, line 38](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=2.4.37#L38) - - -```cpp -/* - * - * The zone_table array is used to look up the address of the - * struct zone corresponding to a given zone number (ZONE_DMA, - * ZONE_NORMAL, or ZONE_HIGHMEM). - */ -zone_t *zone_table[MAX_NR_ZONES*MAX_NR_NODES]; -EXPORT_SYMBOL(zone_table); -``` - -* MAX_NR_NODES为系统中内存结点的数目 - -* MAX_NR_ZONES为系统中单个内存结点所拥有的最大内存区域数目 - - - -##3.2 内存域初始化顺序zonelist_order -------- - - -NUMA系统中存在多个节点, 每个节点对应一个`struct pglist_data`结构, 每个结点中可以包含多个zone, 如: ZONE_DMA, ZONE_NORMAL, 这样就产生几种排列顺序, 以2个节点2个zone为例(zone从高到低排列, ZONE_DMA0表示节点0的ZONE_DMA,其它类似). - -* Legacy方式, 每个节点只排列自己的zone; - -![Legacy方式](../images/legacy-order.jpg) - -* Node方式, 按节点顺序依次排列,先排列本地节点的所有zone,再排列其它节点的所有zone。 - - -![Node方式](../images/node-order.jpg) - - -* Zone方式, 按zone类型从高到低依次排列各节点的同相类型zone - - - -![Zone方式](../images/zone-order.jpg) - - - -可通过启动参数"numa_zonelist_order"来配置zonelist order,内核定义了3种配置, 这些顺序定义在[mm/page_alloc.c?v=4.7, line 4551](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4551) - -```cpp -// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4551 -/* - * zonelist_order: - * 0 = automatic detection of better ordering. - * 1 = order by ([node] distance, -zonetype) - * 2 = order by (-zonetype, [node] distance) - * - * If not NUMA, ZONELIST_ORDER_ZONE and ZONELIST_ORDER_NODE will create - * the same zonelist. So only NUMA can configure this param. - */ -#define ZONELIST_ORDER_DEFAULT 0 /* 智能选择Node或Zone方式 */ - -#define ZONELIST_ORDER_NODE 1 /* 对应Node方式 */ - -#define ZONELIST_ORDER_ZONE 2 /* 对应Zone方式 */ -``` - ->注意 -> ->在非NUMA系统中(比如UMA), 由于只有一个内存结点, 因此ZONELIST_ORDER_ZONE和ZONELIST_ORDER_NODE选项会配置相同的内存域排列方式, 因此, 只有NUMA可以配置这几个参数 - - - - - - -全局的current_zonelist_order变量标识了系统中的当前使用的内存域排列方式, 默认配置为ZONELIST_ORDER_DEFAULT, 参见[mm/page_alloc.c?v=4.7, line 4564](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4564) - - -```cpp -// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4564 -/* zonelist order in the kernel. - * set_zonelist_order() will set this to NODE or ZONE. - */ -static int current_zonelist_order = ZONELIST_ORDER_DEFAULT; -static char zonelist_order_name[3][8] = {"Default", "Node", "Zone"}; -``` - - - - - -而zonelist_order_name方式分别对应了Legacy方式, Node方式和Zone方式. 其zonelist_order_name[current_zonelist_order]就标识了当前系统中所使用的内存域排列方式的名称"Default", "Node", "Zone". - - -| 宏 | zonelist_order_name[宏](排列名称) | 排列方式 | 描述 | -|:--:|:-------------------:|:------:|:----:| -| ZONELIST_ORDER_DEFAULT | Default | | 由系统智能选择Node或Zone方式 | -| ZONELIST_ORDER_NODE | Node | Node方式 | 按节点顺序依次排列,先排列本地节点的所有zone,再排列其它节点的所有zone | -| ZONELIST_ORDER_ZONE | Zone | Zone方式 | 按zone类型从高到低依次排列各节点的同相类型zone | - - - -##3.3 set_zonelist_order设置排列方式 -------- - -内核就通过通过set_zonelist_order函数设置当前系统的内存域排列方式current_zonelist_order, 其定义依据系统的NUMA结构还是UMA结构有很大的不同. - -```cpp -// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4571 -#ifdef CONFIG_NUMA -/* The value user specified ....changed by config */ -static int user_zonelist_order = ZONELIST_ORDER_DEFAULT; -/* string for sysctl */ -#define NUMA_ZONELIST_ORDER_LEN 16 -char numa_zonelist_order[16] = "default"; - - -// http://lxr.free-electrons.com/source/mm/page_alloc.c#L4571 -static void set_zonelist_order(void) -{ - if (user_zonelist_order == ZONELIST_ORDER_DEFAULT) - current_zonelist_order = default_zonelist_order(); - else - current_zonelist_order = user_zonelist_order; -} - - -#else /* CONFIG_NUMA */ - -// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4892 -static void set_zonelist_order(void) -{ - current_zonelist_order = ZONELIST_ORDER_ZONE; -} -``` - - -其设置的基本流程如下 - -* 如果系统当前系统是非NUMA结构的, 则系统中只有一个结点, 配置ZONELIST_ORDER_NODE和ZONELIST_ORDER_ZONE结果相同. 那么set_zonelist_order函数被定义为直接配置当前系统的内存域排列方式`current_zonelist_order`为ZONE方式(与NODE效果相同) - -* 如果系统是NUMA结构, 则设置为系统指定的方式即可 - 1. 当前的排列方式为ZONELIST_ORDER_DEFAULT, 即系统默认方式, 则current_zonelist_order则由内核交给default_zonelist_order采用一定的算法选择一个最优的分配策略, 目前的系统中如果是32位则配置为ZONE方式, 而如果是64位系统则设置为NODE方式 - - 2. 当前的排列方式不是默认方式, 则设置为user_zonelist_order指定的内存域排列方式 - - - -##3.4 default_zonelist_order函数选择最优的配置 -------- - - - -在UMA结构下, 内存域使用NODE和ZONE两个排列方式会产生相同的效果, 因此系统不用特殊指定, 直接通过set_zonelist_order函数, 将当前系统的内存域排列方式`current_zonelist_order`配置为为ZONE方式(与NODE效果相同)即可 - - -但是NUMA结构下, 默认情况下(当配置了ZONELIST_ORDER_DEFAULT), 系统需要根据系统自身的环境信息选择一个最优的配置(NODE或者ZONE方式), 这个工作就由**default_zonelist_order函数**了来完成. 其定义在[mm/page_alloc.c?v=4.7, line 4789](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4789) - - -```cpp -#if defined(CONFIG_64BIT) -/* - * Devices that require DMA32/DMA are relatively rare and do not justify a - * penalty to every machine in case the specialised case applies. Default - * to Node-ordering on 64-bit NUMA machines - */ -static int default_zonelist_order(void) -{ - return ZONELIST_ORDER_NODE; -} -#else -/* - * On 32-bit, the Normal zone needs to be preserved for allocations accessible - * by the kernel. If processes running on node 0 deplete the low memory zone - * then reclaim will occur more frequency increasing stalls and potentially - * be easier to OOM if a large percentage of the zone is under writeback or - * dirty. The problem is significantly worse if CONFIG_HIGHPTE is not set. - * Hence, default to zone ordering on 32-bit. - */ -static int default_zonelist_order(void) -{ - return ZONELIST_ORDER_ZONE; -} -#endif /* CONFIG_64BIT */ -``` - - - -###3.5 user_zonelist_order用户指定排列方式 -------- - - -在NUMA结构下, 系统支持用户指定内存域的排列方式, 用户以字符串的形式操作numa_zonelist_order(default, node和zone), 最终被内核转换为user_zonelist_order, 这个变量被指定为字符串numa_zonelist_order指定的排列方式, 他们定义在[mm/page_alloc.c?v4.7, line 4573](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L4573), 注意只有在NUMA结构中才需要这个配置信息. - - -```cpp -#ifdef CONFIG_NUMA -/* The value user specified ....changed by config */ -static int user_zonelist_order = ZONELIST_ORDER_DEFAULT; -/* string for sysctl */ -#define NUMA_ZONELIST_ORDER_LEN 16 -char numa_zonelist_order[16] = "default"; - -#else -/* ......*/ -#endif -``` - -而接受和处理用户配置的工作, 自然是交给我们强大的proc文件系统来完成的, 可以通过/proc/sys/vm/numa_zonelist_order动态改变zonelist order的分配方式。 - - - - -![/proc/sys/vm/numa_zonelist_order`](../images/proc-numa_zonelist_order.png) - - - -内核通过setup_numa_zonelist_order读取并处理用户写入的配置信息 - -* 接收到用户的信息后用__parse_numa_zonelist_order处理接收的参数 - -* 如果前面用__parse_numa_zonelist_order处理的信息串成功, 则将对用的设置信息写入到字符串numa_zonelist_order中 - - -参见[mm/page_alloc.c?v=4.7, line 4578](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4578) - - -```cpp -/* - * interface for configure zonelist ordering. - * command line option "numa_zonelist_order" - * = "[dD]efault - default, automatic configuration. - * = "[nN]ode - order by node locality, then by zone within node - * = "[zZ]one - order by zone, then by locality within zone - */ - -static int __parse_numa_zonelist_order(char *s) -{ - if (*s == 'd' || *s == 'D') { - user_zonelist_order = ZONELIST_ORDER_DEFAULT; - } else if (*s == 'n' || *s == 'N') { - user_zonelist_order = ZONELIST_ORDER_NODE; - } else if (*s == 'z' || *s == 'Z') { - user_zonelist_order = ZONELIST_ORDER_ZONE; - } else { - pr_warn("Ignoring invalid numa_zonelist_order value: %s\n", s); - return -EINVAL; - } - return 0; -} - -static __init int setup_numa_zonelist_order(char *s) -{ - int ret; - - if (!s) - return 0; - - ret = __parse_numa_zonelist_order(s); - if (ret == 0) - strlcpy(numa_zonelist_order, s, NUMA_ZONELIST_ORDER_LEN); - - return ret; -} -early_param("numa_zonelist_order", setup_numa_zonelist_order); -``` - -#4 build_all_zonelists_init完成内存域zonelists的初始化 -------- - -build_all_zonelists函数在通过set_zonelist_order设置了zonelists中结点的组织顺序后, 首先检查了ssytem_state标识. 如果当前系统处于boot阶段(SYSTEM_BOOTING), 就开始通过build_all_zonelists_init函数初始化zonelist - - -```cpp -build_all_zonelists(pg_data_t *pgdat, struct zone *zone) -{ - /* 设置zonelist中节点和内存域的组织形式 - * current_zonelist_order变量标识了当前系统的内存组织形式 - * zonelist_order_name以字符串存储了系统中内存组织形式的名称 */ - set_zonelist_order(); - - if (system_state == SYSTEM_BOOTING) { - build_all_zonelists_init(); -``` - - -##4.1 system_state系统状态标识 -------- - - -其中`system_state`变量是一个系统全局定义的用来表示系统当前运行状态的枚举变量, 其定义在[include/linux/kernel.h?v=4.7, line 487](http://lxr.free-electrons.com/source/include/linux/kernel.h?v=4.7#L487) - - -```cpp -/* Values used for system_state */ -extern enum system_states -{ - SYSTEM_BOOTING, - SYSTEM_RUNNING, - SYSTEM_HALT, - SYSTEM_POWER_OFF, - SYSTEM_RESTART, -} system_state; -``` - -* 如果系统system_state是SYSTEM_BOOTING, 则调用`build_all_zonelists_init`初始化所有的内存结点 - -* 否则的话如果定义了冷热页`CONFIG_MEMORY_HOTPLUG`且参数zone(待初始化的内存管理域zone)不为NULL, 则调用setup_zone_pageset设置冷热页 - - - -```cpp -if (system_state == SYSTEM_BOOTING) -{ - build_all_zonelists_init(); -} -else -{ -#ifdef CONFIG_MEMORY_HOTPLUG - if (zone) - setup_zone_pageset(zone); -#endif -``` - -##4.2 build_all_zonelists_init函数 - -build_all_zonelists函数在如果当前系统处于boot阶段(system_state == SYSTEM_BOOTING), 就开始通过build_all_zonelists_init函数初始化zonelist - - -build_all_zonelists_init函数定义在[mm/page_alloc.c?v=4.7, line 5013](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5013) - -```cpp -static noinline void __init -build_all_zonelists_init(void) -{ - __build_all_zonelists(NULL); - mminit_verify_zonelist(); - cpuset_init_current_mems_allowed(); -} -``` - - -build_all_zonelists_init将将所有工作都委托给__build_all_zonelists完成了zonelists的初始化工作, 后者又对系统中的各个NUMA结点分别调用build_zonelists. - - -函数__build_all_zonelists定义在[mm/page_alloc.c?v=4.7, line 4959](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4959) - - -```cpp -/* return values int ....just for stop_machine() */ -static int __build_all_zonelists(void *data) -{ - int nid; - int cpu; - pg_data_t *self = data; - - /* ...... */ - - for_each_online_node(nid) { - pg_data_t *pgdat = NODE_DATA(nid); - - build_zonelists(pgdat); - } - /* ...... */ -} -``` - -`for_each_online_node`遍历了系统中所有的活动结点. - -由于UMA系统只有一个结点,build_zonelists只调用了一次, 就对所有的内存创建了内存域列表. - -NUMA系统调用该函数的次数等同于结点的数目. 每次调用对一个不同结点生成内存域数据 - - -##4.3 build_zonelists初始化每个内存结点的zonelists -------- - -build_zonelists(pg_data_t *pgdat)完成了节点pgdat上zonelists的初始化工作, 它建立了备用层次结构zonelists. 由于UMA和NUMA架构下结点的层次结构有很大的区别, 因此内核分别提供了两套不同的接口. - -如下所示 - -```cpp -// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7 -4571 #ifdef CONFIG_NUMA - -4586 static int __parse_numa_zonelist_order(char *s) - -4601 static __init int setup_numa_zonelist_order(char *s) - -4619 int numa_zonelist_order_handler(struct ctl_table *table, int write, -4620 void __user *buffer, size_t *length, - -4678 static int find_next_best_node(int node, nodemask_t *used_node_mask) - -4730 static void build_zonelists_in_node_order(pg_data_t *pgdat, int node) - -4746 static void build_thisnode_zonelists(pg_data_t *pgdat) - -4765 static void build_zonelists_in_zone_order(pg_data_t *pgdat, int nr_nodes) - -4789 #if defined(CONFIG_64BIT) - -4795 static int default_zonelist_order(void) -4799 #else -4808 static int default_zonelist_order(void) -4812 #endif /* CONFIG_64BIT */ - -4822 static void build_zonelists(pg_data_t *pgdat) - -4872 #ifdef CONFIG_HAVE_MEMORYLESS_NODES -4879 int local_memory_node(int node) -4888 #endif - -4890 #else /* CONFIG_NUMA */ - -4897 static void build_zonelists(pg_data_t *pgdat) - -4892 static void set_zonelist_order(void) - -4931 #endif /* CONFIG_NUMA */ -``` - -| 函数 | NUMA | UMA | -|:------:|:--------:| -| build_zonelists | [build_zonelists -=> mm/page_alloc.c?v=4.7, line 4822](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4822) | [build_zonelists -=> mm/page_alloc.c?v=4.7, line 4897](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4897)

[build_zonelists_node -=> mm/page_alloc.c?v=4.7, line 4531](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4531) | - -我们以UMA结构下的build_zonelists为例, 来讲讲内核是怎么初始化备用内存域层次结构的, UMA结构下的build_zonelists函数定义在[mm/page_alloc.c?v=4.7, line 4897](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4897), 如下所示 - - -node_zonelists的数组元素通过指针操作寻址, 这在C语言中是完全合法的惯例。实际工作则委托给build_zonelist_node。在调用时,它首先生成本地结点内分配内存时的备用次 - - -内核在build_zonelists中按分配代价从昂贵到低廉的次序, 迭代了结点中所有的内存域. 而在build_zonelists_node中, 则按照分配代价从低廉到昂贵的次序, 迭代了分配代价不低于当前内存域的内存域. - - -首先我们来看看build_zonelists_node函数, 该函数定义在[mm/page_alloc.c?v=4.7, line 4531](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4531) - -```cpp -/* - * Builds allocation fallback zone lists. - * - * Add all populated zones of a node to the zonelist. - */ -static int build_zonelists_node(pg_data_t *pgdat, struct zonelist *zonelist, int nr_zones) -{ - struct zone *zone; - enum zone_type zone_type = MAX_NR_ZONES; - - do { - zone_type--; - zone = pgdat->node_zones + zone_type; - if (populated_zone(zone)) { - zoneref_set_zone(zone, - &zonelist->_zonerefs[nr_zones++]); - check_highest_zone(zone_type); - } - } while (zone_type); - - return nr_zones; -} -``` - -备用列表zonelists的各项是借助于zone_type参数排序的, 该参数指定了最优先选择哪个内存域, 该参数的初始值是外层循环的控制变量i. - -我们知道其值可能是ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA或ZONE_DMA32之一. - -nr_zones表示从备用列表中的哪个位置开始填充新项. 由于列表中尚没有项, 因此调用者传递了0. - -内核在build_zonelists中按分配代价从昂贵到低廉的次序, 迭代了结点中所有的内存域. 而在build_zonelists_node中, 则按照分配代价从低廉到昂贵的次序, 迭代了分配代价不低于当前内存域的内存域. - -在build_zonelists_node的每一步中, 都对所选的内存域调用populated_zone, 确认zone->present_pages大于0, 即确认内存域中确实有页存在. 倘若如此, 则将指向zone实例的指针添加到zonelist->zones中的当前位置. 后备列表的当前位置保存在nr_zones. - -在每一步结束时, 都将内存域类型zone_type减1.换句话说, 设置为一个更昂贵的内存域类型. 例如, 如果开始的内存域是ZONE_HIGHMEM, 减1后下一个内存域类型是ZONE_NORMAL. - -考虑一个系统, 有内存域ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA。在第一次运行build_zonelists_node时, 实际上会执行下列赋值 - -```cpp -zonelist->zones[0] = ZONE_HIGHMEM; -zonelist->zones[1] = ZONE_NORMAL; -zonelist->zones[2] = ZONE_DMA; -``` - -我们以某个系统为例, 图中示范了一个备用列表在多次循环中不断填充的过程. 系统中共有四个结点 - -![连续填充备用列表](../images/build_zonelists_node.png) - -```cpp -其中 -A=(NUMA)结点0 0=DMA内存域 -B=(NUMA)结点1 1=普通内存域 -C=(NUMA)结点2 2=高端内存域 -D=(NUMA)结点3 -``` - - -第一步之后, 列表中的分配目标是高端内存, 接下来是第二个结点的普通和DMA内存域. - -内核接下来必须确立次序, 以便将系统中其他结点的内存域按照次序加入到备用列表. - -现在我们回到build_zonelists函数, UMA架构下该函数定义在[mm/page_alloc.c?v=4.7, line 4897](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4897), 如下所示 - - - -```cpp -static void build_zonelists(pg_data_t *pgdat) -{ - int node, local_node; - enum zone_type j; - struct zonelist *zonelist; - - /* ...... */ - - for (node = local_node + 1; node < MAX_NUMNODES; node++) { - if (!node_online(node)) - continue; - j = build_zonelists_node(NODE_DATA(node), zonelist, j); - } - for (node = 0; node < local_node; node++) { - if (!node_online(node)) - continue; - j = build_zonelists_node(NODE_DATA(node), zonelist, j); - } - - zonelist->_zonerefs[j].zone = NULL; - zonelist->_zonerefs[j].zone_idx = 0; -} -``` - -第一个循环依次迭代大于当前结点编号的所有结点. 在我们的例子中,有4个结点编号副本为0、1、2、3,此时只剩下结点3。新的项通过build_zonelists_node被加到备用列表。此时j的作用就体现出来了。在本地结点的备用目标找到之后,该变量的值是3。该值用作新项的起始位置。如果结点3也由3个内存域组成,备用列表在第二个循环之后的情况如图3-9的第二步所示 - -第二个for循环接下来对所有编号小于当前结点的结点生成备用列表项。在我们的例子中,这些结点的编号为0和1。 如果这些结点也有3个内存域,则循环完毕之后备用列表的情况如下图下半部分所示 - -![完成的备用列表](../images/build_zonelists.png) - -备用列表中项的数目一般无法准确知道,因为系统中不同结点的内存域配置可能并不相同。因此 -列表的最后一项赋值为空指针,显式标记列表结束。 -对总数N个结点中的结点m来说,内核生成备用列表时,选择备用结点的顺序总是:m、m+1、 -m+2、…、N1、0、1、…、m1。这确保了不过度使用任何结点。例如,对照情况是:使用一个独立 -于m、不变的备用列表。 - - - -##4.4 setup_pageset初始化per_cpu缓存 -------- - -前面讲解内存管理域zone的时候, 提到了per-CPU缓存, 即冷热页. 在组织每个节点的zonelist的过程中, setup_pageset初始化了per-CPU缓存(冷热页面) - -```cpp -static void setup_pageset(struct per_cpu_pageset *p, unsigned long batch) -{ - pageset_init(p); - pageset_set_batch(p, batch); -} -``` - - - -在此之前free_area_init_node初始化内存结点的时候, 内核就输出了冷热页的一些信息, 该工作由zone_pcp_init完成, 该函数定义在[mm/page_alloc.c?v=4.7, line 5029](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5029) - -```cpp -static __meminit void zone_pcp_init(struct zone *zone) -{ - /* - * per cpu subsystem is not up at this point. The following code - * relies on the ability of the linker to provide the - * offset of a (static) per cpu variable into the per cpu area. - */ - zone->pageset = &boot_pageset; - - if (populated_zone(zone)) - printk(KERN_DEBUG " %s zone: %lu pages, LIFO batch:%u\n", - zone->name, zone->present_pages, - zone_batchsize(zone)); -} -``` - - -#5 总结 -------- - - - -##5.1 start_kernel启动流程 -------- - - -```cpp -start_kernel() - |---->page_address_init() - | 考虑支持高端内存 - | 业务:初始化page_address_pool链表; - | 将page_address_maps数组元素按索引降序插入 - | page_address_pool链表; - | 初始化page_address_htable数组. - | - |---->setup_arch(&command_line); - | - |---->setup_per_cpu_areas(); - | 为per-CPU变量分配空间 - | - |---->build_all_zonelist() - | 为系统中的zone建立后备zone的列表. - | 所有zone的后备列表都在 - | pglist_data->node_zonelists[0]中; - | - | 期间也对per-CPU变量boot_pageset做了初始化. - | - |---->page_alloc_init() - |---->hotcpu_notifier(page_alloc_cpu_notifier, 0); - | 不考虑热插拔CPU - | - |---->pidhash_init() - | 详见下文. - | 根据低端内存页数和散列度,分配hash空间,并赋予pid_hash - | - |---->vfs_caches_init_early() - |---->dcache_init_early() - | dentry_hashtable空间,d_hash_shift, h_hash_mask赋值; - | 同pidhash_init(); - | 区别: - | 散列度变化了(13 - PAGE_SHIFT); - | 传入alloc_large_system_hash的最后参数值为0; - | - |---->inode_init_early() - | inode_hashtable空间,i_hash_shift, i_hash_mask赋值; - | 同pidhash_init(); - | 区别: - | 散列度变化了(14 - PAGE_SHIFT); - | 传入alloc_large_system_hash的最后参数值为0; - | -``` - -##5.2 pidhash_init配置高端内存 -------- - - -```cpp -void pidhash_init(void) - |---->pid_hash = alloc_large_system_hash("PID", sizeof(*pid_hash), - | 0, 18, HASH_EARLY|HASH_SMALL, &pidhash_shift, NULL, 4096); - | 根据nr_kernel_pages(低端内存的页数),分配哈希数组,以及各个哈希 - | 数组元素下的哈希链表的空间,原理如下: - | number = nr_kernel_pages; - | number >= (18 - PAGE_SHIFT) 根据散列度获得数组元素个数 - | number = roundup_pow_of_two(number); - | pidhash_shift = max{x | 2**x <= number} - | size = number * sizeof(*pid_hash); - | 使用位图分配器分配size空间,将返回值付给pid_hash; - | - |---->pidhash_size = 1 << pidhash_shift; - | - |---->for(i = 0; i < pidhash_size; i++) - | INIT_HLIST_HEAD(&pid_hash[i]); -``` - -##5.3 build_all_zonelists初始化每个内存节点的zonelists -------- - - - -```cpp -void build_all_zonelists(void) - |---->set_zonelist_order() - |---->current_zonelist_order = ZONELIST_ORDER_ZONE; - | - |---->__build_all_zonelists(NULL); - | Memory不支持热插拔, 为每个zone建立后备的zone, - | 每个zone及自己后备的zone,形成zonelist - | - |---->pg_data_t *pgdat = NULL; - | pgdat = &contig_page_data;(单node) - | - |---->build_zonelists(pgdat); - | 为每个zone建立后备zone的列表 - | - |---->struct zonelist *zonelist = NULL; - | enum zone_type j; - | zonelist = &pgdat->node_zonelists[0]; - | - |---->j = build_zonelists_node(pddat, zonelist, 0, MAX_NR_ZONES - 1); - | 为pgdat->node_zones[0]建立后备的zone,node_zones[0]后备的zone - | 存储在node_zonelist[0]内,对于node_zone[0]的后备zone,其后备的zone - | 链表如下(只考虑UMA体系,而且不考虑ZONE_DMA): - | node_zonelist[0]._zonerefs[0].zone = &node_zones[2]; - | node_zonelist[0]._zonerefs[0].zone_idx = 2; - | node_zonelist[0]._zonerefs[1].zone = &node_zones[1]; - | node_zonelist[0]._zonerefs[1].zone_idx = 1; - | node_zonelist[0]._zonerefs[2].zone = &node_zones[0]; - | node_zonelist[0]._zonerefs[2].zone_idx = 0; - | - | zonelist->_zonerefs[3].zone = NULL; - | zonelist->_zonerefs[3].zone_idx = 0; - | - |---->build_zonelist_cache(pgdat); - |---->pdat->node_zonelists[0].zlcache_ptr = NULL; - | UMA体系结构 - | - |---->for_each_possible_cpu(cpu) - | setup_pageset(&per_cpu(boot_pageset, cpu), 0); - |详见下文 - |---->vm_total_pages = nr_free_pagecache_pages(); - | 业务:获得所有zone中的present_pages总和. - | - |---->page_group_by_mobility_disabled = 0; - | 对于代码中的判断条件一般不会成立,因为页数会最够多(内存较大) +初始化内存管理 +======= + + + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + + +在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后奇偶内核才能检测到可用内存和寄存器. + + + +#1 前景回顾 +------- + + +##1.1 Linux内存管理的层次结构 +------- + + +Linux把物理内存划分为三个层次来管理 + +| 层次 | 描述 | +|:----:|:----:| +| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | +| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | +| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | + +为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 + + +* 首先, 内存被划分为**结点**. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. + +* 接着各个节点又被划分为内存管理区域, 一个**管理区域**通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. + + +* 最后**页帧(page frame)**代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号. + + +##1.2 今日内容(启动过程中的内存初始化) +------- + + +在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. + + +因此我们可以把linux内核的内存管理分三个阶段。 + +| 阶段 | 起点 | 终点 | 描述 | +|:-----:|:-----:|:-----:| +| 第一阶段 | 系统启动 | bootmem或者memblock初始化完成 | 此阶段只能使用memblock_reserve函数分配内存, 早期内核中使用init_bootmem_done = 1标识此阶段结束 | +| 第二阶段 | bootmem或者memblock初始化完 | buddy完成前 | 引导内存分配器bootmem或者memblock接受内存的管理工作, 早期内核中使用mem_init_done = 1标记此阶段的结束 | +| 第三阶段 | buddy初始化完成 | 系统停止运行 | 可以用cache和buddy分配内存 | + + + +##1.3 start_kernel系统启动阶段的内存初始化过程 +------- + +首先我们来看看start_kernel是如何初始化系统的, start_kerne定义在[init/main.c?v=4.7, line 479](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L479) + +其代码很复杂, 我们只截取出其中与内存管理初始化相关的部分, 如下所示 + + +```cpp +asmlinkage __visible void __init start_kernel(void) +{ + + setup_arch(&command_line); + mm_init_cpumask(&init_mm); + + setup_per_cpu_areas(); + + + build_all_zonelists(NULL, NULL); + page_alloc_init(); + + + /* + * These use large bootmem allocations and must precede + * mem_init(); + * kmem_cache_init(); + */ + mm_init(); + + kmem_cache_init_late(); + + kmemleak_init(); + setup_per_cpu_pageset(); + + rest_init(); +} +``` + + +| 函数 | 功能 | +|:----:|:----:| +| [setup_arch](http://lxr.free-electrons.com/ident?v=4.7;i=setup_arch) | 是一个特定于体系结构的设置函数, 其中一项任务是负责初始化自举分配器 | +| [mm_init_cpumask](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v=4.7#L522) | 初始化CPU屏蔽字 | +| [setup_per_cpu_areas](http://lxr.free-electrons.com/ident?v=4.7;i=setup_per_cpu_areas) | 函数[(查看定义)](http://lxr.free-electrons.com/source/mm/percpu.c?v4.7#L2205])给每个CPU分配内存,并拷贝.data.percpu段的数据. 为系统中的每个CPU的per_cpu变量申请空间.
在SMP系统中, setup_per_cpu_areas初始化源代码中(使用[per_cpu宏](http://lxr.free-electrons.com/source/include/linux/percpu-defs.h#L256))定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本.
此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本
在非SMP系统中这是一个空操作 | +| [build_all_zonelists](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029) | 建立并初始化结点和内存域的数据结构 | +| [mm_init](http://lxr.free-electrons.com/source/init/main.c?v4.7#L464) | 建立了内核的内存分配器,
其中通过[mem_init](http://lxr.free-electrons.com/ident?v=4.7&i=mem_init)停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)
然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器 | +| [kmem_cache_init_late](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378) | 在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器[slab](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378), [slob](http://lxr.free-electrons.com/source/mm/slob.c?v4.7#L655), [slub](http://lxr.free-electrons.com/source/mm/slub.c?v=4.7#L3960)都会定义此函数 | +| [kmemleak_init](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1857) | Kmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 [/sys/kernel/debug/kmemleak](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1467)中, Kmemcheck能够帮助定位大多数内存错误的上下文 | +| [setup_per_cpu_pageset](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5392) | 初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配
由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. | + + + + +##1.4 setup_arch函数初始化内存流程 +------- + + +前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作. + +```cpp +void __init setup_arch(char **cmdline_p) +{ + /* 初始化memblock */ + arm64_memblock_init( ); + + /* 分页机制初始化 */ + paging_init(); + + bootmem_init(); +} +``` + +| 流程 | 描述 | +|:---:|:----:| +| [arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) | 初始化memblock内存分配器 | +| [paging_init](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538) | 初始化分页机制 | +| [bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | 初始化内存管理 | + + +该函数主要执行了如下操作 + + +1. 使用arm64_memblock_init来完成memblock机制的初始化工作, 至此memblock分配器接受系统中系统中内存的分配工作 + +2. 调用paging_init来完成系统分页机制的初始化工作, 建立页表, 从而内核可以完成虚拟内存的映射和转换工作 + +3. 最后调用bootmem_init来完成实现buddy内存管理所需要的工作 + + + +##1.5 (第一阶段)启动过程中的内存分配器 +------- + + +在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. + +这个阶段的内存分配其实很简单, 因此我们往往称之为内存分配器(而不是内存管理器), 早期的内核中内存分配器使用的**bootmem引导分配器**, 它基于一个内存位图bitmap, 使用最优适配算法来查找内存, 但是这个分配器有很大的缺陷, 最严重的就是内存碎片的问题, 因此在后来的内核中将其舍弃《而使用了**新的memblock机制**. memblock机制的初始化在arm64上是通过[arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229)函数来实现的 + +```cpp +```cpp +start_kernel() + |---->page_address_init() + | 考虑支持高端内存 + | 业务:初始化page_address_pool链表; + | 将page_address_maps数组元素按索引降序插入 + | page_address_pool链表; + | 初始化page_address_htable数组. + | + |---->setup_arch(&command_line); + | 初始化特定体系结构的内容 + | + |---->arm64_memblock_init( ); + | 初始化引导阶段的内存分配器memblock + | + |---->paging_init(); + | 分页机制初始化 + | + |---->bootmem_init(); + | 始化内存数据结构包括内存节点, 内存域和页帧page + | + |---->arm64_numa_init(); + | 支持numa架构 + | + |---->zone_sizes_init(min, max); + 来初始化节点和管理区的一些数据项 + | + |---->free_area_init_node + | 初始化内存节点 + | + |---->free_area_init_core + | 初始化zone + | + |---->memmap_init + | 初始化page页面 + | + |---->memblock_dump_all(); + | 初始化完成, 显示memblock的保留的所有内存信息 + | + |---->build_all_zonelist() [当前位置] + | 为系统中的zone建立后备zone的列表. + | 所有zone的后备列表都在 + | pglist_data->node_zonelists[0]中; + | + | 期间也对per-CPU变量boot_pageset做了初始化. + | +``` + + +##1.6 今日内容(第二阶段(二)--初始化备用内存域列表zonelists) +------- + + +我们之前讲了在memblock完成之后, 内存初始化开始进入第二阶段, 第二阶段是一个漫长的过程, 它执行了一系列复杂的操作, 从体系结构相关信息的初始化慢慢向上层展开, 其主要执行了如下操作 + + + +**特定于体系结构的设置** + + +在完成了基础的内存结点和内存域的初始化工作以后, 我们必须克服一些硬件的特殊设置 + +* 在初始化内存的结点和内存区域之前, 内核先通过pagging_init初始化了内核的分页机制, 这样我们的虚拟运行空间就初步建立, 并可以完成物理地址到虚拟地址空间的映射工作. + +在arm64架构下, 内核在start_kernel()->setup_arch()中通过arm64_memblock_init( )完成了memblock的初始化之后, 接着通过setup_arch()->paging_init()开始初始化分页机制 + + +paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响 + + +* 在分页机制完成后, 内核通过setup_arch()->bootmem_init开始进行内存基本数据结构(内存结点pg_data_t, 内存域zone和页帧)的初始化工作, 就是在这个函数中, 内核开始从体系结构相关的部分逐渐展开到体系结构无关的部分, 在zone_sizes_init->free_area_init_node中开始, 内核开始进行内存基本数据结构的初始化, 也不再依赖于特定体系结构无关的层次 + +```cpp +bootmem_init +始化内存数据结构包括内存节点, 内存域和页帧page +| +|---->arm64_numa_init(); +| 支持numa架构 +| +|---->zone_sizes_init(min, max); + 来初始化节点和管理区的一些数据项 + | + |---->free_area_init_node + | 初始化内存节点 + | + |---->free_area_init_core + | 初始化zone + | + |---->memmap_init + | 初始化page页面 +| +|---->memblock_dump_all(); +| 初始化完成, 显示memblock的保留的所有内存信息 +``` + + + +至此,bootmem_init已经完成了节点和管理区的关键数据已完成初始化, 内核在后面为内存管理做得一个准备工作就是将所有节点的管理区都链入到zonelist中,便于后面内存分配工作的进行. + + +内核在start_kernel()-->build_all_zonelist()中完成zonelist的初始化 + + +#2 后备内存域列表zonelists +------- + + +内核setup_arch的最后通过bootmem_init中完成了内存数据结构的初始化(包括内存结点pg_data_t, 内存管理域zone和页面信息page), 数据结构已经基本准备好了, 在后面为内存管理做得一个准备工作就是将所有节点的管理区都链入到zonelist中, 便于后面内存分配工作的进行. + + +##2.1 回到start_kernel函数(已经完成的工作) +------- + + +前面我们分析了start_kernel()->setup_arch()函数, 已经完成了memblock内存分配器的创建和初始化工作, 然后paging_init也完成分页机制的初始化, 然后bootmem_init也完成了内存结点和内存管理域的初始化工作. setup_arch函数已经执行完了, 现在我们回到start_kernel + + +```cpp +asmlinkage __visible void __init start_kernel(void) +{ + + setup_arch(&command_line); + + + build_all_zonelists(NULL, NULL); + page_alloc_init(); + + + /* + * These use large bootmem allocations and must precede + * mem_init(); + * kmem_cache_init(); + */ + mm_init(); + + kmem_cache_init_late(); + + kmemleak_init(); + setup_per_cpu_pageset(); + + rest_init(); +} +``` + +下面内核开始通过start_kernel()->build_all_zonelists来设计内存的组织形式 + + +##2.2 后备内存域列表zonelist +------- + +内存节点pg_data_t中将内存节点中的内存区域zone按照某种组织层次存储在一个zonelist中, 即pglist_data->node_zonelists成员信息 + +```cpp +// http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L626 +typedef struct pglist_data +{ + struct zone node_zones[MAX_NR_ZONES]; + struct zonelist node_zonelists[MAX_ZONELISTS]; +} +``` + + +内核定义了内存的一个层次结构关系, 首先试图分配廉价的内存,如果失败,则根据访问速度和容量,逐渐尝试分配更昂贵的内存. + +高端内存最廉价, 因为内核没有任何部分依赖于从该内存域分配的内存, 如果高端内存用尽, 对内核没有副作用, 所以优先分配高端内存 + +普通内存域的情况有所不同, 许多内核数据结构必须保存在该内存域, 而不能放置到高端内存域, 因此如果普通内存域用尽, 那么内核会面临内存紧张的情况 + +DMA内存域最昂贵,因为它用于外设和系统之间的数据传输。 +举例来讲,如果内核指定想要分配高端内存域。它首先在当前结点的高端内存域寻找适当的空闲内存段,如果失败,则查看该结点的普通内存域,如果还失败,则试图在该结点的DMA内存域分配。如果在3个本地内存域都无法找到空闲内存,则查看其他结点。这种情况下,备选结点应该尽可能靠近主结点,以最小化访问非本地内存引起的性能损失。 + + + +##2.3 build_all_zonelists初始化zonelists +------- + +内核在start_kernel中通过build_all_zonelists完成了内存结点及其管理内存域的初始化工作, 调用如下 + + +```cpp + build_all_zonelists(NULL, NULL); +``` + +[build_all_zonelists](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029)建立内存管理结点及其内存域的组织形式, 将描述内存的数据结构(结点, 管理域, 页帧)通过一定的算法组织在一起, 方便以后内存管理工作的进行. 该函数定义在[mm/page_alloc.c?v4.7, line 5029](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029) + + +##2.4 build_all_zonelists函数 +------- + +```cpp +/* + * Called with zonelists_mutex held always + * unless system_state == SYSTEM_BOOTING. + * + * __ref due to (1) call of __meminit annotated setup_zone_pageset + * [we're only called with non-NULL zone through __meminit paths] and + * (2) call of __init annotated helper build_all_zonelists_init + * [protected by SYSTEM_BOOTING]. + */ +void __ref build_all_zonelists(pg_data_t *pgdat, struct zone *zone) +{ + /* 设置zonelist中节点和内存域的组织形式 + * current_zonelist_order变量标识了当前系统的内存组织形式 + * zonelist_order_name以字符串存储了系统中内存组织形式的名称 */ + set_zonelist_order(); + + if (system_state == SYSTEM_BOOTING) { + build_all_zonelists_init(); + } else { +#ifdef CONFIG_MEMORY_HOTPLUG + if (zone) + setup_zone_pageset(zone); +#endif + /* we have to stop all cpus to guarantee there is no user + of zonelist */ + stop_machine(__build_all_zonelists, pgdat, NULL); + /* cpuset refresh routine should be here */ + } + vm_total_pages = nr_free_pagecache_pages(); + /* + * Disable grouping by mobility if the number of pages in the + * system is too low to allow the mechanism to work. It would be + * more accurate, but expensive to check per-zone. This check is + * made on memory-hotadd so a system can start with mobility + * disabled and enable it later + */ + if (vm_total_pages < (pageblock_nr_pages * MIGRATE_TYPES)) + page_group_by_mobility_disabled = 1; + else + page_group_by_mobility_disabled = 0; + + pr_info("Built %i zonelists in %s order, mobility grouping %s. Total pages: %ld\n", + nr_online_nodes, + zonelist_order_name[current_zonelist_order], + page_group_by_mobility_disabled ? "off" : "on", + vm_total_pages); +#ifdef CONFIG_NUMA + pr_info("Policy zone: %s\n", zone_names[policy_zone]); +#endif +} +``` + + +#3 设置结点初始化顺序 +------- + + +在build_all_zonelists开始, 首先内核通过set_zonelist_order函数设置了`zonelist_order`,如下所示, 参见[mm/page_alloc.c?v=4.7, line 5031](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5031) + +```cpp +void __ref build_all_zonelists(pg_data_t *pgdat, struct zone *zone) +{ + set_zonelist_order(); + /* ....... */ +} +``` + + +##3.1 zonelist +------- + + +前面我们讲解内存管理域时候讲解到, 系统中的所有管理域都存储在一个多维的数组zone_table. 内核在初始化内存管理区时, 必须要建立管理区表zone_table. 参见[mm/page_alloc.c?v=2.4.37, line 38](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=2.4.37#L38) + + +```cpp +/* + * + * The zone_table array is used to look up the address of the + * struct zone corresponding to a given zone number (ZONE_DMA, + * ZONE_NORMAL, or ZONE_HIGHMEM). + */ +zone_t *zone_table[MAX_NR_ZONES*MAX_NR_NODES]; +EXPORT_SYMBOL(zone_table); +``` + +* MAX_NR_NODES为系统中内存结点的数目 + +* MAX_NR_ZONES为系统中单个内存结点所拥有的最大内存区域数目 + + + +##3.2 内存域初始化顺序zonelist_order +------- + + +NUMA系统中存在多个节点, 每个节点对应一个`struct pglist_data`结构, 每个结点中可以包含多个zone, 如: ZONE_DMA, ZONE_NORMAL, 这样就产生几种排列顺序, 以2个节点2个zone为例(zone从高到低排列, ZONE_DMA0表示节点0的ZONE_DMA,其它类似). + +* Legacy方式, 每个节点只排列自己的zone; + +![Legacy方式](../images/legacy-order.jpg) + +* Node方式, 按节点顺序依次排列,先排列本地节点的所有zone,再排列其它节点的所有zone。 + + +![Node方式](../images/node-order.jpg) + + +* Zone方式, 按zone类型从高到低依次排列各节点的同相类型zone + + + +![Zone方式](../images/zone-order.jpg) + + + +可通过启动参数"numa_zonelist_order"来配置zonelist order,内核定义了3种配置, 这些顺序定义在[mm/page_alloc.c?v=4.7, line 4551](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4551) + +```cpp +// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4551 +/* + * zonelist_order: + * 0 = automatic detection of better ordering. + * 1 = order by ([node] distance, -zonetype) + * 2 = order by (-zonetype, [node] distance) + * + * If not NUMA, ZONELIST_ORDER_ZONE and ZONELIST_ORDER_NODE will create + * the same zonelist. So only NUMA can configure this param. + */ +#define ZONELIST_ORDER_DEFAULT 0 /* 智能选择Node或Zone方式 */ + +#define ZONELIST_ORDER_NODE 1 /* 对应Node方式 */ + +#define ZONELIST_ORDER_ZONE 2 /* 对应Zone方式 */ +``` + +>注意 +> +>在非NUMA系统中(比如UMA), 由于只有一个内存结点, 因此ZONELIST_ORDER_ZONE和ZONELIST_ORDER_NODE选项会配置相同的内存域排列方式, 因此, 只有NUMA可以配置这几个参数 + + + + + + +全局的current_zonelist_order变量标识了系统中的当前使用的内存域排列方式, 默认配置为ZONELIST_ORDER_DEFAULT, 参见[mm/page_alloc.c?v=4.7, line 4564](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4564) + + +```cpp +// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4564 +/* zonelist order in the kernel. + * set_zonelist_order() will set this to NODE or ZONE. + */ +static int current_zonelist_order = ZONELIST_ORDER_DEFAULT; +static char zonelist_order_name[3][8] = {"Default", "Node", "Zone"}; +``` + + + + + +而zonelist_order_name方式分别对应了Legacy方式, Node方式和Zone方式. 其zonelist_order_name[current_zonelist_order]就标识了当前系统中所使用的内存域排列方式的名称"Default", "Node", "Zone". + + +| 宏 | zonelist_order_name[宏](排列名称) | 排列方式 | 描述 | +|:--:|:-------------------:|:------:|:----:| +| ZONELIST_ORDER_DEFAULT | Default | | 由系统智能选择Node或Zone方式 | +| ZONELIST_ORDER_NODE | Node | Node方式 | 按节点顺序依次排列,先排列本地节点的所有zone,再排列其它节点的所有zone | +| ZONELIST_ORDER_ZONE | Zone | Zone方式 | 按zone类型从高到低依次排列各节点的同相类型zone | + + + +##3.3 set_zonelist_order设置排列方式 +------- + +内核就通过通过set_zonelist_order函数设置当前系统的内存域排列方式current_zonelist_order, 其定义依据系统的NUMA结构还是UMA结构有很大的不同. + +```cpp +// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4571 +#ifdef CONFIG_NUMA +/* The value user specified ....changed by config */ +static int user_zonelist_order = ZONELIST_ORDER_DEFAULT; +/* string for sysctl */ +#define NUMA_ZONELIST_ORDER_LEN 16 +char numa_zonelist_order[16] = "default"; + + +// http://lxr.free-electrons.com/source/mm/page_alloc.c#L4571 +static void set_zonelist_order(void) +{ + if (user_zonelist_order == ZONELIST_ORDER_DEFAULT) + current_zonelist_order = default_zonelist_order(); + else + current_zonelist_order = user_zonelist_order; +} + + +#else /* CONFIG_NUMA */ + +// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4892 +static void set_zonelist_order(void) +{ + current_zonelist_order = ZONELIST_ORDER_ZONE; +} +``` + + +其设置的基本流程如下 + +* 如果系统当前系统是非NUMA结构的, 则系统中只有一个结点, 配置ZONELIST_ORDER_NODE和ZONELIST_ORDER_ZONE结果相同. 那么set_zonelist_order函数被定义为直接配置当前系统的内存域排列方式`current_zonelist_order`为ZONE方式(与NODE效果相同) + +* 如果系统是NUMA结构, 则设置为系统指定的方式即可 + 1. 当前的排列方式为ZONELIST_ORDER_DEFAULT, 即系统默认方式, 则current_zonelist_order则由内核交给default_zonelist_order采用一定的算法选择一个最优的分配策略, 目前的系统中如果是32位则配置为ZONE方式, 而如果是64位系统则设置为NODE方式 + + 2. 当前的排列方式不是默认方式, 则设置为user_zonelist_order指定的内存域排列方式 + + + +##3.4 default_zonelist_order函数选择最优的配置 +------- + + + +在UMA结构下, 内存域使用NODE和ZONE两个排列方式会产生相同的效果, 因此系统不用特殊指定, 直接通过set_zonelist_order函数, 将当前系统的内存域排列方式`current_zonelist_order`配置为为ZONE方式(与NODE效果相同)即可 + + +但是NUMA结构下, 默认情况下(当配置了ZONELIST_ORDER_DEFAULT), 系统需要根据系统自身的环境信息选择一个最优的配置(NODE或者ZONE方式), 这个工作就由**default_zonelist_order函数**了来完成. 其定义在[mm/page_alloc.c?v=4.7, line 4789](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4789) + + +```cpp +#if defined(CONFIG_64BIT) +/* + * Devices that require DMA32/DMA are relatively rare and do not justify a + * penalty to every machine in case the specialised case applies. Default + * to Node-ordering on 64-bit NUMA machines + */ +static int default_zonelist_order(void) +{ + return ZONELIST_ORDER_NODE; +} +#else +/* + * On 32-bit, the Normal zone needs to be preserved for allocations accessible + * by the kernel. If processes running on node 0 deplete the low memory zone + * then reclaim will occur more frequency increasing stalls and potentially + * be easier to OOM if a large percentage of the zone is under writeback or + * dirty. The problem is significantly worse if CONFIG_HIGHPTE is not set. + * Hence, default to zone ordering on 32-bit. + */ +static int default_zonelist_order(void) +{ + return ZONELIST_ORDER_ZONE; +} +#endif /* CONFIG_64BIT */ +``` + + + +###3.5 user_zonelist_order用户指定排列方式 +------- + + +在NUMA结构下, 系统支持用户指定内存域的排列方式, 用户以字符串的形式操作numa_zonelist_order(default, node和zone), 最终被内核转换为user_zonelist_order, 这个变量被指定为字符串numa_zonelist_order指定的排列方式, 他们定义在[mm/page_alloc.c?v4.7, line 4573](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L4573), 注意只有在NUMA结构中才需要这个配置信息. + + +```cpp +#ifdef CONFIG_NUMA +/* The value user specified ....changed by config */ +static int user_zonelist_order = ZONELIST_ORDER_DEFAULT; +/* string for sysctl */ +#define NUMA_ZONELIST_ORDER_LEN 16 +char numa_zonelist_order[16] = "default"; + +#else +/* ......*/ +#endif +``` + +而接受和处理用户配置的工作, 自然是交给我们强大的proc文件系统来完成的, 可以通过/proc/sys/vm/numa_zonelist_order动态改变zonelist order的分配方式。 + + + + +![/proc/sys/vm/numa_zonelist_order`](../images/proc-numa_zonelist_order.png) + + + +内核通过setup_numa_zonelist_order读取并处理用户写入的配置信息 + +* 接收到用户的信息后用__parse_numa_zonelist_order处理接收的参数 + +* 如果前面用__parse_numa_zonelist_order处理的信息串成功, 则将对用的设置信息写入到字符串numa_zonelist_order中 + + +参见[mm/page_alloc.c?v=4.7, line 4578](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4578) + + +```cpp +/* + * interface for configure zonelist ordering. + * command line option "numa_zonelist_order" + * = "[dD]efault - default, automatic configuration. + * = "[nN]ode - order by node locality, then by zone within node + * = "[zZ]one - order by zone, then by locality within zone + */ + +static int __parse_numa_zonelist_order(char *s) +{ + if (*s == 'd' || *s == 'D') { + user_zonelist_order = ZONELIST_ORDER_DEFAULT; + } else if (*s == 'n' || *s == 'N') { + user_zonelist_order = ZONELIST_ORDER_NODE; + } else if (*s == 'z' || *s == 'Z') { + user_zonelist_order = ZONELIST_ORDER_ZONE; + } else { + pr_warn("Ignoring invalid numa_zonelist_order value: %s\n", s); + return -EINVAL; + } + return 0; +} + +static __init int setup_numa_zonelist_order(char *s) +{ + int ret; + + if (!s) + return 0; + + ret = __parse_numa_zonelist_order(s); + if (ret == 0) + strlcpy(numa_zonelist_order, s, NUMA_ZONELIST_ORDER_LEN); + + return ret; +} +early_param("numa_zonelist_order", setup_numa_zonelist_order); +``` + +#4 build_all_zonelists_init完成内存域zonelists的初始化 +------- + +build_all_zonelists函数在通过set_zonelist_order设置了zonelists中结点的组织顺序后, 首先检查了ssytem_state标识. 如果当前系统处于boot阶段(SYSTEM_BOOTING), 就开始通过build_all_zonelists_init函数初始化zonelist + + +```cpp +build_all_zonelists(pg_data_t *pgdat, struct zone *zone) +{ + /* 设置zonelist中节点和内存域的组织形式 + * current_zonelist_order变量标识了当前系统的内存组织形式 + * zonelist_order_name以字符串存储了系统中内存组织形式的名称 */ + set_zonelist_order(); + + if (system_state == SYSTEM_BOOTING) { + build_all_zonelists_init(); +``` + + +##4.1 system_state系统状态标识 +------- + + +其中`system_state`变量是一个系统全局定义的用来表示系统当前运行状态的枚举变量, 其定义在[include/linux/kernel.h?v=4.7, line 487](http://lxr.free-electrons.com/source/include/linux/kernel.h?v=4.7#L487) + + +```cpp +/* Values used for system_state */ +extern enum system_states +{ + SYSTEM_BOOTING, + SYSTEM_RUNNING, + SYSTEM_HALT, + SYSTEM_POWER_OFF, + SYSTEM_RESTART, +} system_state; +``` + +* 如果系统system_state是SYSTEM_BOOTING, 则调用`build_all_zonelists_init`初始化所有的内存结点 + +* 否则的话如果定义了冷热页`CONFIG_MEMORY_HOTPLUG`且参数zone(待初始化的内存管理域zone)不为NULL, 则调用setup_zone_pageset设置冷热页 + + + +```cpp +if (system_state == SYSTEM_BOOTING) +{ + build_all_zonelists_init(); +} +else +{ +#ifdef CONFIG_MEMORY_HOTPLUG + if (zone) + setup_zone_pageset(zone); +#endif +``` + +##4.2 build_all_zonelists_init函数 + +build_all_zonelists函数在如果当前系统处于boot阶段(system_state == SYSTEM_BOOTING), 就开始通过build_all_zonelists_init函数初始化zonelist + + +build_all_zonelists_init函数定义在[mm/page_alloc.c?v=4.7, line 5013](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5013) + +```cpp +static noinline void __init +build_all_zonelists_init(void) +{ + __build_all_zonelists(NULL); + mminit_verify_zonelist(); + cpuset_init_current_mems_allowed(); +} +``` + + +build_all_zonelists_init将将所有工作都委托给__build_all_zonelists完成了zonelists的初始化工作, 后者又对系统中的各个NUMA结点分别调用build_zonelists. + + +函数__build_all_zonelists定义在[mm/page_alloc.c?v=4.7, line 4959](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4959) + + +```cpp +/* return values int ....just for stop_machine() */ +static int __build_all_zonelists(void *data) +{ + int nid; + int cpu; + pg_data_t *self = data; + + /* ...... */ + + for_each_online_node(nid) { + pg_data_t *pgdat = NODE_DATA(nid); + + build_zonelists(pgdat); + } + /* ...... */ +} +``` + +`for_each_online_node`遍历了系统中所有的活动结点. + +由于UMA系统只有一个结点,build_zonelists只调用了一次, 就对所有的内存创建了内存域列表. + +NUMA系统调用该函数的次数等同于结点的数目. 每次调用对一个不同结点生成内存域数据 + + +##4.3 build_zonelists初始化每个内存结点的zonelists +------- + +build_zonelists(pg_data_t *pgdat)完成了节点pgdat上zonelists的初始化工作, 它建立了备用层次结构zonelists. 由于UMA和NUMA架构下结点的层次结构有很大的区别, 因此内核分别提供了两套不同的接口. + +如下所示 + +```cpp +// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7 +4571 #ifdef CONFIG_NUMA + +4586 static int __parse_numa_zonelist_order(char *s) + +4601 static __init int setup_numa_zonelist_order(char *s) + +4619 int numa_zonelist_order_handler(struct ctl_table *table, int write, +4620 void __user *buffer, size_t *length, + +4678 static int find_next_best_node(int node, nodemask_t *used_node_mask) + +4730 static void build_zonelists_in_node_order(pg_data_t *pgdat, int node) + +4746 static void build_thisnode_zonelists(pg_data_t *pgdat) + +4765 static void build_zonelists_in_zone_order(pg_data_t *pgdat, int nr_nodes) + +4789 #if defined(CONFIG_64BIT) + +4795 static int default_zonelist_order(void) +4799 #else +4808 static int default_zonelist_order(void) +4812 #endif /* CONFIG_64BIT */ + +4822 static void build_zonelists(pg_data_t *pgdat) + +4872 #ifdef CONFIG_HAVE_MEMORYLESS_NODES +4879 int local_memory_node(int node) +4888 #endif + +4890 #else /* CONFIG_NUMA */ + +4897 static void build_zonelists(pg_data_t *pgdat) + +4892 static void set_zonelist_order(void) + +4931 #endif /* CONFIG_NUMA */ +``` + +| 函数 | NUMA | UMA | +|:------:|:--------:| +| build_zonelists | [build_zonelists -=> mm/page_alloc.c?v=4.7, line 4822](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4822) | [build_zonelists -=> mm/page_alloc.c?v=4.7, line 4897](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4897)

[build_zonelists_node -=> mm/page_alloc.c?v=4.7, line 4531](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4531) | + +我们以UMA结构下的build_zonelists为例, 来讲讲内核是怎么初始化备用内存域层次结构的, UMA结构下的build_zonelists函数定义在[mm/page_alloc.c?v=4.7, line 4897](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4897), 如下所示 + + +node_zonelists的数组元素通过指针操作寻址, 这在C语言中是完全合法的惯例。实际工作则委托给build_zonelist_node。在调用时,它首先生成本地结点内分配内存时的备用次 + + +内核在build_zonelists中按分配代价从昂贵到低廉的次序, 迭代了结点中所有的内存域. 而在build_zonelists_node中, 则按照分配代价从低廉到昂贵的次序, 迭代了分配代价不低于当前内存域的内存域. + + +首先我们来看看build_zonelists_node函数, 该函数定义在[mm/page_alloc.c?v=4.7, line 4531](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4531) + +```cpp +/* + * Builds allocation fallback zone lists. + * + * Add all populated zones of a node to the zonelist. + */ +static int build_zonelists_node(pg_data_t *pgdat, struct zonelist *zonelist, int nr_zones) +{ + struct zone *zone; + enum zone_type zone_type = MAX_NR_ZONES; + + do { + zone_type--; + zone = pgdat->node_zones + zone_type; + if (populated_zone(zone)) { + zoneref_set_zone(zone, + &zonelist->_zonerefs[nr_zones++]); + check_highest_zone(zone_type); + } + } while (zone_type); + + return nr_zones; +} +``` + +备用列表zonelists的各项是借助于zone_type参数排序的, 该参数指定了最优先选择哪个内存域, 该参数的初始值是外层循环的控制变量i. + +我们知道其值可能是ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA或ZONE_DMA32之一. + +nr_zones表示从备用列表中的哪个位置开始填充新项. 由于列表中尚没有项, 因此调用者传递了0. + +内核在build_zonelists中按分配代价从昂贵到低廉的次序, 迭代了结点中所有的内存域. 而在build_zonelists_node中, 则按照分配代价从低廉到昂贵的次序, 迭代了分配代价不低于当前内存域的内存域. + +在build_zonelists_node的每一步中, 都对所选的内存域调用populated_zone, 确认zone->present_pages大于0, 即确认内存域中确实有页存在. 倘若如此, 则将指向zone实例的指针添加到zonelist->zones中的当前位置. 后备列表的当前位置保存在nr_zones. + +在每一步结束时, 都将内存域类型zone_type减1.换句话说, 设置为一个更昂贵的内存域类型. 例如, 如果开始的内存域是ZONE_HIGHMEM, 减1后下一个内存域类型是ZONE_NORMAL. + +考虑一个系统, 有内存域ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA。在第一次运行build_zonelists_node时, 实际上会执行下列赋值 + +```cpp +zonelist->zones[0] = ZONE_HIGHMEM; +zonelist->zones[1] = ZONE_NORMAL; +zonelist->zones[2] = ZONE_DMA; +``` + +我们以某个系统为例, 图中示范了一个备用列表在多次循环中不断填充的过程. 系统中共有四个结点 + +![连续填充备用列表](../images/build_zonelists_node.png) + +```cpp +其中 +A=(NUMA)结点0 0=DMA内存域 +B=(NUMA)结点1 1=普通内存域 +C=(NUMA)结点2 2=高端内存域 +D=(NUMA)结点3 +``` + + +第一步之后, 列表中的分配目标是高端内存, 接下来是第二个结点的普通和DMA内存域. + +内核接下来必须确立次序, 以便将系统中其他结点的内存域按照次序加入到备用列表. + +现在我们回到build_zonelists函数, UMA架构下该函数定义在[mm/page_alloc.c?v=4.7, line 4897](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4897), 如下所示 + + + +```cpp +static void build_zonelists(pg_data_t *pgdat) +{ + int node, local_node; + enum zone_type j; + struct zonelist *zonelist; + + /* ...... */ + + for (node = local_node + 1; node < MAX_NUMNODES; node++) { + if (!node_online(node)) + continue; + j = build_zonelists_node(NODE_DATA(node), zonelist, j); + } + for (node = 0; node < local_node; node++) { + if (!node_online(node)) + continue; + j = build_zonelists_node(NODE_DATA(node), zonelist, j); + } + + zonelist->_zonerefs[j].zone = NULL; + zonelist->_zonerefs[j].zone_idx = 0; +} +``` + +第一个循环依次迭代大于当前结点编号的所有结点. 在我们的例子中,有4个结点编号副本为0、1、2、3,此时只剩下结点3。新的项通过build_zonelists_node被加到备用列表。此时j的作用就体现出来了。在本地结点的备用目标找到之后,该变量的值是3。该值用作新项的起始位置。如果结点3也由3个内存域组成,备用列表在第二个循环之后的情况如图3-9的第二步所示 + +第二个for循环接下来对所有编号小于当前结点的结点生成备用列表项。在我们的例子中,这些结点的编号为0和1。 如果这些结点也有3个内存域,则循环完毕之后备用列表的情况如下图下半部分所示 + +![完成的备用列表](../images/build_zonelists.png) + +备用列表中项的数目一般无法准确知道,因为系统中不同结点的内存域配置可能并不相同。因此 +列表的最后一项赋值为空指针,显式标记列表结束。 +对总数N个结点中的结点m来说,内核生成备用列表时,选择备用结点的顺序总是:m、m+1、 +m+2、…、N1、0、1、…、m1。这确保了不过度使用任何结点。例如,对照情况是:使用一个独立 +于m、不变的备用列表。 + + + +##4.4 setup_pageset初始化per_cpu缓存 +------- + +前面讲解内存管理域zone的时候, 提到了per-CPU缓存, 即冷热页. 在组织每个节点的zonelist的过程中, setup_pageset初始化了per-CPU缓存(冷热页面) + +```cpp +static void setup_pageset(struct per_cpu_pageset *p, unsigned long batch) +{ + pageset_init(p); + pageset_set_batch(p, batch); +} +``` + + + +在此之前free_area_init_node初始化内存结点的时候, 内核就输出了冷热页的一些信息, 该工作由zone_pcp_init完成, 该函数定义在[mm/page_alloc.c?v=4.7, line 5029](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5029) + +```cpp +static __meminit void zone_pcp_init(struct zone *zone) +{ + /* + * per cpu subsystem is not up at this point. The following code + * relies on the ability of the linker to provide the + * offset of a (static) per cpu variable into the per cpu area. + */ + zone->pageset = &boot_pageset; + + if (populated_zone(zone)) + printk(KERN_DEBUG " %s zone: %lu pages, LIFO batch:%u\n", + zone->name, zone->present_pages, + zone_batchsize(zone)); +} +``` + + +#5 总结 +------- + + + +##5.1 start_kernel启动流程 +------- + + +```cpp +start_kernel() + |---->page_address_init() + | 考虑支持高端内存 + | 业务:初始化page_address_pool链表; + | 将page_address_maps数组元素按索引降序插入 + | page_address_pool链表; + | 初始化page_address_htable数组. + | + |---->setup_arch(&command_line); + | + |---->setup_per_cpu_areas(); + | 为per-CPU变量分配空间 + | + |---->build_all_zonelist() + | 为系统中的zone建立后备zone的列表. + | 所有zone的后备列表都在 + | pglist_data->node_zonelists[0]中; + | + | 期间也对per-CPU变量boot_pageset做了初始化. + | + |---->page_alloc_init() + |---->hotcpu_notifier(page_alloc_cpu_notifier, 0); + | 不考虑热插拔CPU + | + |---->pidhash_init() + | 详见下文. + | 根据低端内存页数和散列度,分配hash空间,并赋予pid_hash + | + |---->vfs_caches_init_early() + |---->dcache_init_early() + | dentry_hashtable空间,d_hash_shift, h_hash_mask赋值; + | 同pidhash_init(); + | 区别: + | 散列度变化了(13 - PAGE_SHIFT); + | 传入alloc_large_system_hash的最后参数值为0; + | + |---->inode_init_early() + | inode_hashtable空间,i_hash_shift, i_hash_mask赋值; + | 同pidhash_init(); + | 区别: + | 散列度变化了(14 - PAGE_SHIFT); + | 传入alloc_large_system_hash的最后参数值为0; + | +``` + +##5.2 pidhash_init配置高端内存 +------- + + +```cpp +void pidhash_init(void) + |---->pid_hash = alloc_large_system_hash("PID", sizeof(*pid_hash), + | 0, 18, HASH_EARLY|HASH_SMALL, &pidhash_shift, NULL, 4096); + | 根据nr_kernel_pages(低端内存的页数),分配哈希数组,以及各个哈希 + | 数组元素下的哈希链表的空间,原理如下: + | number = nr_kernel_pages; + | number >= (18 - PAGE_SHIFT) 根据散列度获得数组元素个数 + | number = roundup_pow_of_two(number); + | pidhash_shift = max{x | 2**x <= number} + | size = number * sizeof(*pid_hash); + | 使用位图分配器分配size空间,将返回值付给pid_hash; + | + |---->pidhash_size = 1 << pidhash_shift; + | + |---->for(i = 0; i < pidhash_size; i++) + | INIT_HLIST_HEAD(&pid_hash[i]); +``` + +##5.3 build_all_zonelists初始化每个内存节点的zonelists +------- + + + +```cpp +void build_all_zonelists(void) + |---->set_zonelist_order() + |---->current_zonelist_order = ZONELIST_ORDER_ZONE; + | + |---->__build_all_zonelists(NULL); + | Memory不支持热插拔, 为每个zone建立后备的zone, + | 每个zone及自己后备的zone,形成zonelist + | + |---->pg_data_t *pgdat = NULL; + | pgdat = &contig_page_data;(单node) + | + |---->build_zonelists(pgdat); + | 为每个zone建立后备zone的列表 + | + |---->struct zonelist *zonelist = NULL; + | enum zone_type j; + | zonelist = &pgdat->node_zonelists[0]; + | + |---->j = build_zonelists_node(pddat, zonelist, 0, MAX_NR_ZONES - 1); + | 为pgdat->node_zones[0]建立后备的zone,node_zones[0]后备的zone + | 存储在node_zonelist[0]内,对于node_zone[0]的后备zone,其后备的zone + | 链表如下(只考虑UMA体系,而且不考虑ZONE_DMA): + | node_zonelist[0]._zonerefs[0].zone = &node_zones[2]; + | node_zonelist[0]._zonerefs[0].zone_idx = 2; + | node_zonelist[0]._zonerefs[1].zone = &node_zones[1]; + | node_zonelist[0]._zonerefs[1].zone_idx = 1; + | node_zonelist[0]._zonerefs[2].zone = &node_zones[0]; + | node_zonelist[0]._zonerefs[2].zone_idx = 0; + | + | zonelist->_zonerefs[3].zone = NULL; + | zonelist->_zonerefs[3].zone_idx = 0; + | + |---->build_zonelist_cache(pgdat); + |---->pdat->node_zonelists[0].zlcache_ptr = NULL; + | UMA体系结构 + | + |---->for_each_possible_cpu(cpu) + | setup_pageset(&per_cpu(boot_pageset, cpu), 0); + |详见下文 + |---->vm_total_pages = nr_free_pagecache_pages(); + | 业务:获得所有zone中的present_pages总和. + | + |---->page_group_by_mobility_disabled = 0; + | 对于代码中的判断条件一般不会成立,因为页数会最够多(内存较大) ``` \ No newline at end of file diff --git a/study/kernel/02-memory/04-buddy/03-fragmentation/README.md b/study/kernel/02-memory/04-buddy/03-fragmentation/README.md index 79ccfab..14d4339 100644 --- a/study/kernel/02-memory/04-buddy/03-fragmentation/README.md +++ b/study/kernel/02-memory/04-buddy/03-fragmentation/README.md @@ -1,602 +1,602 @@ -伙伴系统之避免碎片 -======= - - - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - - - -#1 前景提要 -------- - - -##1.1 Linux内存层次结构 -------- - - - -##1.2 碎片化问题 - - - -**分页与分段** - -页是信息的物理单位, 分页是为了实现非连续分配, 以便解决内存碎片问题, 或者说分页是由于系统管理的需要. 段是信息的逻辑单位,它含有一组意义相对完整的信息, 分段的目的是为了更好地实现共享, 满足用户的需要. - -页的大小固定且由系统确定, 将逻辑地址划分为页号和页内地址是由机器硬件实现的. 而段的长度却不固定, 决定于用户所编写的程序, 通常由编译程序在对源程序进行编译时根据信息的性质来划分. - -分页的作业地址空间是一维的. 分段的地址空间是二维的. - - - -**内部碎片与外部碎片** - - -在页式虚拟存储系统中, 用户作业的地址空间被划分成若干大小相等的页面, 存储空间也分成也页大小相等的物理块, 但一般情况下, 作业的大小不可能都是物理块大小的整数倍, 因此作业的最后一页中仍有部分空间被浪费掉了. 由此可知, 页式虚拟存储系统中存在**内碎片**. - - -在段式虚拟存储系统中, 作业的地址空间由若干个逻辑分段组成, 每段分配一个连续的内存区, 但各段之间不要求连续, 其内存的分配方式类似于动态分区分配.由此可知, 段式虚拟存储系统中存在**外碎片**. - - - - - -在内存管理中, "内零头"和"外零头"个指的是什么? - -在固定式分区分配, 可变式分区分配, 页式虚拟存储系统, 段式虚拟存储系统中, 各会存在何种碎片? 为什么? - - -解答: - -在存储管理中 - -* **内碎片**是指分配给作业的存储空间中未被利用的部分 - - 在固定式分区分配中, 为将一个用户作业装入内存, 内存分配程序从系统分区表中找出一个能满足作业要求的空闲分区分配给作业, 由于一个作业的大小并不一定与分区大小相等, 因此, 分区中有一部分存储空间浪费掉了. 由此可知, 固定式分区分配中存在**内碎片**. - -* **外碎片**是指系统中无法利用的小存储块. - - 在可变式分区分配中, 为把一个作业装入内存, 应按照一定的分配算法从系统中找出一个能满足作业需求的空闲分区分配给作业, 如果这个空闲分区的容量比作业申请的空间容量要大, 则将该分区一分为二, 一部分分配给作业, 剩下的部分仍然留作系统的空闲分区。由此可知,可变式分区分配中存在**外碎片**. - - - -简言之 - -随着存储区的分配和释放过程的进行, 在各个被分配出去的分区之间会存在很多的小空闲区, 暂时不能被利用, 这就是"外部碎片". - - -在固定分区管理算法中, 分给程序的内存空间往往大于程序所需的空间, 这剩余部分的空间不能被其他程序所用, 这就是"内部碎片" - - -##1.3 buddy伙伴系统 -------- - - - -##1.4 今日内容(buddy伙伴系统如何避免碎片) -------- - -Linux伙伴系统分配内存的大小要求2的幂指数页, 这也会产生严重的**内部碎片**. - -伙伴系统的基本原理已经在前面已经讨论过, 一个双链表即可满足伙伴系统的所有需求, 其方案在最近几年间确实工作得非常好。但在Linux内存管理方面,有一个长期存在的问题 : 在系统启动并长期运行后,物理内存会产生很多碎片。该情形如下图所示 - -![物理内存的碎片](../images/physical_memory_fragmentation.png) - -假定内存由60页组成,这显然不是超级计算机,但用于示例却足够了。左侧的地址空间中散布着空闲页。尽管大约25%的物理内存仍然未分配,但最大的连续空闲区只有一页. 这对用户空间应用程序没有问题:其内存是通过页表映射的,无论空闲页在物理内存中的分布如何,应用程序看到的内存 -似乎总是连续的。右图给出的情形中,空闲页和使用页的数目与左图相同,但所有空闲页都位于一个连续区中。 - -但对内核来说,碎片是一个问题. 由于(大多数)物理内存一致映射到地址空间的内核部分, 那么在左图的场景中, 无法映射比一页更大的内存区. 尽管许多时候内核都分配的是比较小的内存, 但也有时候需要分配多于一页的内存. 显而易见, 在分配较大内存的情况下, 右图中所有已分配页和空闲页都处于连续内存区的情形,是更为可取的. - -很有趣的一点是, 在大部分内存仍然未分配时, 就也可能发生碎片问题. 考虑下图所示的情形. - -只分配了4页,但可分配的最大连续区只有8页,因为伙伴系统所能工作的分配范围只能是2的幂次. - - -![物理内存的碎片](../images/some_memory_fragmentation.png) - - -我提到内存碎片只涉及内核,这只是部分正确的。大多数现代CPU都提供了使用巨型页的可能性,比普通页大得多。这对内存使用密集的应用程序有好处。在使用更大的页时,地址转换后备缓冲器只需处理较少的项,降低了TLB缓存失效的可能性。但分配巨型页需要连续的空闲物理内存! - -很长时间以来,物理内存的碎片确实是Linux的弱点之一。尽管已经提出了许多方法,但没有哪个方法能够既满足Linux需要处理的各种类型工作负荷提出的苛刻需求,同时又对其他事务影响不大。 - - -目前Linux内核为解决内存碎片的方案提供了两类解决方案 - -* 依据可移动性组织页避免内存碎片 - -* 虚拟可移动内存域避免内存碎片 - -#2 依据可移动性组织页避免内存碎片 -------- - -依据可移动性组织页是方式物理内存碎片的一种可能方法. - -##2.1 依据可移动性组织页 -------- - - -在内核2.6.24开发期间,防止碎片的方法最终加入内核。在我讨论具体策略之前,有一点需要澄清。 - -文件系统也有碎片,该领域的碎片问题主要通过碎片合并工具解决。它们分析文件系统,重新排序已分配存储块,从而建立较大的连续存储区. 理论上,该方法对物理内存也是可能的,但由于许多物理内存页不能移动到任意位置,阻碍了该方法的实施。因此,内核的方法是反碎片(anti-fragmentation), 即试图从最初开始尽可能防止碎片. - - -反碎片的工作原理如何? - - - -为理解该方法,我们必须知道内核将已分配页划分为下面3种不同类型。 - - -| 页面类型 | 描述 | 举例 | -|:---------:|:-----:|:-----:| -| 不可移动页 | 在内存中有固定位置, **不能移动**到其他地方. | 核心内核分配的大多数内存属于该类别 | -| 可移动页 | **可以随意地移动**. | 属于用户空间应用程序的页属于该类别. 它们是通过页表映射的
如果它们复制到新位置,页表项可以相应地更新,应用程序不会注意到任何事 | -| 可回收页 | **不能直接移动, 但可以删除, 其内容可以从某些源重新生成**. | 例如,映射自文件的数据属于该类别
kswapd守护进程会根据可回收页访问的频繁程度,周期性释放此类内存. , 页面回收本身就是一个复杂的过程. 内核会在可回收页占据了太多内存时进行回收, 在内存短缺(即分配失败)时也可以发起页面回收. | - - - -页的可移动性,依赖该页属于3种类别的哪一种. 内核使用的**反碎片技术**, 即基于将具有相同可移动性的页分组的思想. - - -为什么这种方法有助于减少碎片? - - -由于页无法移动, 导致在原本几乎全空的内存区中无法进行连续分配. 根据页的可移动性, 将其分配到不同的列表中, 即可防止这种情形. 例如, 不可移动的页不能位于可移动内存区的中间, 否则就无法从该内存区分配较大的连续内存块. - - -想一下, 上图中大多数空闲页都属于可回收的类别, 而分配的页则是不可移动的. 如果这些页聚集到两个不同的列表中, 如下图所示. 在不可移动页中仍然难以找到较大的连续空闲空间, 但对可回收的页, 就容易多了. - - -![减少内存碎片](../images/little_memory_fragmentation.png) - - -但要注意, 从最初开始, 内存并未划分为可移动性不同的区. 这些是在运行时形成的. 内核的另一种方法确实将内存分区, 分别用于可移动页和不可移动页的分配, 我会下文讨论其工作原理. 但这种划分对这里描述的方法是不必要的 - - - - -##2.2 迁移类型 -------- - - -尽管内核使用的反碎片技术卓有成效,它对伙伴分配器的代码和数据结构几乎没有影响。内核定义了一些枚举常量(早期用宏来实现)来表示不同的迁移类型, 参见[include/linux/mmzone.h?v=4.7, line 38](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L38) - -```cpp -enum { - MIGRATE_UNMOVABLE, - MIGRATE_MOVABLE, - MIGRATE_RECLAIMABLE, - MIGRATE_PCPTYPES, /* the number of types on the pcp lists */ - MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES, -#ifdef CONFIG_CMA - /* - * MIGRATE_CMA migration type is designed to mimic the way - * ZONE_MOVABLE works. Only movable pages can be allocated - * from MIGRATE_CMA pageblocks and page allocator never - * implicitly change migration type of MIGRATE_CMA pageblock. - * - * The way to use it is to change migratetype of a range of - * pageblocks to MIGRATE_CMA which can be done by - * __free_pageblock_cma() function. What is important though - * is that a range of pageblocks must be aligned to - * MAX_ORDER_NR_PAGES should biggest page be bigger then - * a single pageblock. - */ - MIGRATE_CMA, -#endif -#ifdef CONFIG_MEMORY_ISOLATION - MIGRATE_ISOLATE, /* can't allocate from here */ -#endif - MIGRATE_TYPES -}; -``` - - -| 宏 | 类型 | -|:----:|:-----:| -| MIGRATE_UNMOVABLE | 不可移动页 | -| MIGRATE_MOVABLE | 可移动页 | -| MIGRATE_RECLAIMABLE | 可回收页 | -| MIGRATE_PCPTYPES | 是per_cpu_pageset, 即用来表示每CPU页框高速缓存的数据结构中的链表的迁移类型数目 | -| MIGRATE_HIGHATOMIC | = MIGRATE_PCPTYPES, 在罕见的情况下,内核需要分配一个高阶的页面块而不能休眠.如果向具有特定可移动性的列表请求分配内存失败,这种紧急情况下可从MIGRATE_HIGHATOMIC中分配内存 | -| MIGRATE_CMA | Linux内核最新的连续内存分配器(CMA), 用于避免预留大块内存 | -| MIGRATE_ISOLATE | 是一个特殊的虚拟区域, 用于跨越NUMA结点移动物理内存页. 在大型系统上, 它有益于将物理内存页移动到接近于使用该页最频繁的CPU. | -| MIGRATE_TYPES | 只是表示迁移类型的数目, 也不代表具体的区域 | - -对于MIGRATE_CMA类型, 其中在我们使用ARM等嵌入式Linux系统的时候, 一个头疼的问题是GPU, Camera, HDMI等都需要预留大量连续内存,这部分内存平时不用,但是一般的做法又必须先预留着. 目前, Marek Szyprowski和Michal Nazarewicz实现了一套全新的Contiguous Memory Allocator. 通过这套机制, 我们可以做到不预留内存,这些内存平时是可用的,只有当需要的时候才被分配给Camera,HDMI等设备. 参照[宋宝华--Linux内核最新的连续内存分配器(CMA)——避免预留大块内存](http://21cnbao.blog.51cto.com/109393/898846/), 内核为此提供了函数is_migrate_cma来检测当前类型是否为MIGRATE_CMA, 该函数定义在[include/linux/mmzone.h?v=4.7, line 69](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L69) - -```cpp -/* In mm/page_alloc.c; keep in sync also with show_migration_types() there */ -extern char * const migratetype_names[MIGRATE_TYPES]; - -#ifdef CONFIG_CMA -# define is_migrate_cma(migratetype) unlikely((migratetype) == MIGRATE_CMA) -#else -# define is_migrate_cma(migratetype) false -#endif -``` - -##2.3 free_area的改进 -------- - - -对伙伴系统数据结构的主要调整, 是将空闲列表分解为MIGRATE_TYPE个列表, 可以参见free_area的定义[include/linux/mmzone.h?v=4.7, line 88](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L88) - -```cpp -struct free_area -{ - struct list_head free_list[MIGRATE_TYPES]; -unsigned long nr_free; -}; -``` - -* nr_free统计了所有列表上空闲页的数目,而每种迁移类型都对应于一个空闲列表 - - - -这样我们的伙伴系统的内存框架就如下所示 - -![依据可移动性组织页](../images/migrate_buddy.png) - - -宏for_each_migratetype_order(order, type)可用于迭代指定迁移类型的所有分配阶 - -```cpp -#define for_each_migratetype_order(order, type) \ - for (order = 0; order < MAX_ORDER; order++) \ - for (type = 0; type < MIGRATE_TYPES; type++) -``` - - -##2.4 迁移备用列表fallbacks -------- - - - -如果内核无法满足针对某一给定迁移类型的分配请求, 会怎么样? - - - -此前已经出现过一个类似的问题, 即特定的NUMA内存域无法满足分配请求时. 我们需要从其他内存域中选择一个代价最低的内存域完成内存的分配, 因此内核在内存的结点pg_data_t中提供了一个备用内存域列表zonelists. - -内核在内存迁移的过程中处理这种情况下的做法是类似的. 提供了一个备用列表fallbacks, 规定了在指定列表中无法满足分配请求时. 接下来应使用哪一种迁移类型, 定义在[mm/page_alloc.c?v=4.7, line 1799](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L1799) - - -```cpp -/* - * This array describes the order lists are fallen back to when - * the free lists for the desirable migrate type are depleted - * 该数组描述了指定迁移类型的空闲列表耗尽时 - * 其他空闲列表在备用列表中的次序 - */ -static int fallbacks[MIGRATE_TYPES][4] = { - // 分配不可移动页失败的备用列表 - [MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_TYPES }, - // 分配可回收页失败时的备用列表 - [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_TYPES }, - // 分配可移动页失败时的备用列表 - [MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_TYPES }, -#ifdef CONFIG_CMA - [MIGRATE_CMA] = { MIGRATE_TYPES }, /* Never used */ -#endif -#ifdef CONFIG_MEMORY_ISOLATION - [MIGRATE_ISOLATE] = { MIGRATE_TYPES }, /* Never used */ -#endif -}; -``` - ->该数据结构大体上是自明的 : -> ->每一行对应一个类型的备用搜索域的顺序, 在内核想要分配不可移动页`MIGRATE_UNMOVABLE`时, 如果对应链表为空, 则遍历fallbacks[MIGRATE_UNMOVABLE], 首先后退到可回收页链表`MIGRATE_RECLAIMABLE`, 接下来到可移动页链表`MIGRATE_MOVABLE`, 最后到紧急分配链表`MIGRATE_TYPES`. - - -##2.5 全局pageblock_order变量 -------- - - -全局变量和辅助函数尽管页可移动性分组特性总是编译到内核中,但只有在系统中有足够内存可以分配到多个迁移类型对应的链表时,才是有意义的。由于每个迁移链表都应该有适当数量的内存,内核需要定义"适当"的概念. 这是通过两个全局变量pageblock_order和pageblock_nr_pages提供的. 第一个表示内核认为是"大"的一个分配阶, pageblock_nr_pages则表示该分配阶对应的页数。如果体系结构提供了巨型页机制, 则pageblock_order通常定义为巨型页对应的分配阶. 定义在[include/linux/pageblock-flags.h?v=4.7, line 44](http://lxr.free-electrons.com/source/include/linux/pageblock-flags.h?v=4.7#L42) - - -```cpp -#ifdef CONFIG_HUGETLB_PAGE - - #ifdef CONFIG_HUGETLB_PAGE_SIZE_VARIABLE - - /* Huge page sizes are variable */ - extern unsigned int pageblock_order; - - #else /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */ - - /* Huge pages are a constant size */ - #define pageblock_order HUGETLB_PAGE_ORDER - - #endif /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */ - -#else /* CONFIG_HUGETLB_PAGE */ - - /* If huge pages are not used, group by MAX_ORDER_NR_PAGES */ - #define pageblock_order (MAX_ORDER-1) - -#endif /* CONFIG_HUGETLB_PAGE */ - -#define pageblock_nr_pages (1UL << pageblock_order) -``` - -在IA-32体系结构上, 巨型页长度是4MB, 因此每个巨型页由1024个普通页组成, 而HUGETLB_PAGE_ORDER则定义为10. 相比之下, IA-64体系结构允许设置可变的普通和巨型页长度, 因此HUGETLB_PAGE_ORDER的值取决于内核配置. - -如果体系结构不支持巨型页, 则将其定义为第二高的分配阶, 即`MAX_ORDER - 1` - -```cpp -/* If huge pages are not used, group by MAX_ORDER_NR_PAGES */ -#define pageblock_order (MAX_ORDER-1) -``` - -如果各迁移类型的链表中没有一块较大的连续内存, 那么页面迁移不会提供任何好处, 因此在可用内存太少时内核会关闭该特性. 这是在build_all_zonelists函数中检查的, 该函数用于初始化内存域列表. 如果没有足够的内存可用, 则全局变量[`page_group_by_mobility_disabled`](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L79)设置为0, 否则设置为1. - -内核如何知道给定的分配内存属于何种迁移类型? - -我们将在以后讲解, 有关各个内存分配的细节都通过分配掩码指定. - -内核提供了两个标志,分别用于表示分配的内存是可移动的(\__GFP_MOVABLE)或可回收的(\__GFP_RECLAIMABLE). - - - -##2.6 gfpflags_to_migratetype转换分配标识到迁移类型 -------- - - -如果这些标志都没有设置, 则分配的内存假定为不可移动的. 辅助函数gfpflags_to_migratetype可用于转换分配标志及对应的迁移类型, 该函数定义在[include/linux/gfp.h?v=4.7, line 266](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L266) - -```cpp -static inline int gfpflags_to_migratetype(const gfp_t gfp_flags) -{ - VM_WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK); - BUILD_BUG_ON((1UL << GFP_MOVABLE_SHIFT) != ___GFP_MOVABLE); - BUILD_BUG_ON((___GFP_MOVABLE >> GFP_MOVABLE_SHIFT) != MIGRATE_MOVABLE); - - if (unlikely(page_group_by_mobility_disabled)) - return MIGRATE_UNMOVABLE; - - /* Group based on mobility */ - return (gfp_flags & GFP_MOVABLE_MASK) >> GFP_MOVABLE_SHIFT; -} -``` - ->linux-2.6.x的内核中转换分配标志及对应的迁移类型的辅助函数为allocflags_to_migratetype, 这个名字会有歧义的, 让我们误以为参数的标识中有alloc flags, 但是其实并不然, 因此后来的内核中将该函数更名为gfpflags_to_migratetype, 参见[Rename it to gfpflags_to_migratetype()](https://patchwork.kernel.org/patch/4291831) - -在2.6.25中为如下接口 - -```cpp -/* Convert GFP flags to their corresponding migrate type */ -static inline int allocflags_to_migratetype(gfp_t gfp_flags) -{ - WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK); - - if (unlikely(page_group_by_mobility_disabled)) - return MIGRATE_UNMOVABLE; - - /* Group based on mobility */ - return (((gfp_flags & __GFP_MOVABLE) != 0) << 1) | - ((gfp_flags & __GFP_RECLAIMABLE) != 0); -} -```` - -如果停用了页面迁移特性, 则所有的页都是不可移动的. 否则. 该函数的返回值可以直接用作free_area.free_list的数组索引. - -##2.7 pageblock_flags变量与其函数接口 - -最后要注意, 每个内存域都提供了一个特殊的字段, 可以跟踪包含pageblock_nr_pages个页的内存区的属性. 即zone->pageblock_flags字段, 当前只有与页可移动性相关的代码使用, 参见[include/linux/mmzone.h?v=4.7, line 367](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L367) - - -```cpp -struct zone -{ -#ifndef CONFIG_SPARSEMEM - /* - * Flags for a pageblock_nr_pages block. See pageblock-flags.h. - * In SPARSEMEM, this map is stored in struct mem_section - */ - unsigned long *pageblock_flags; -#endif /* CONFIG_SPARSEMEM */ -}; -``` - -在初始化期间, 内核自动确保对内存域中的每个不同的迁移类型分组, 在pageblock_flags中都分配了足够存储NR_PAGEBLOCK_BITS个比特位的空间。当前,表示一个连续内存区的迁移类型需要3个比特位, 参见[include/linux/pageblock-flags.h?v=4.7, line 28](http://lxr.free-electrons.com/source/include/linux/pageblock-flags.h?v=4.7#L28) - -```cpp -/* Bit indices that affect a whole block of pages */ -enum pageblock_bits { - PB_migrate, - PB_migrate_end = PB_migrate + 3 - 1, - /* 3 bits required for migrate types */ - PB_migrate_skip,/* If set the block is skipped by compaction */ - - /* - * Assume the bits will always align on a word. If this assumption - * changes then get/set pageblock needs updating. - */ - NR_PAGEBLOCK_BITS -}; -``` - - -内核提供`set_pageblock_migratetype`负责设置以page为首的一个内存区的迁移类型, 该函数定义在[mm/page_alloc.c?v=4.7, line 458](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L458), 如下所示 - - -```cpp -void set_pageblock_migratetype(struct page *page, int migratetype) -{ - if (unlikely(page_group_by_mobility_disabled && - migratetype < MIGRATE_PCPTYPES)) - migratetype = MIGRATE_UNMOVABLE; - - set_pageblock_flags_group(page, (unsigned long)migratetype, - PB_migrate, PB_migrate_end); -} -``` - - - -`migratetype`参数可以通过上文介绍的`gfpflags_to_migratetype`辅助函数构建. 请注意很重要的一点, 页的迁移类型是预先分配好的, 对应的比特位总是可用, 与页是否由伙伴系统管理无关. 在释放内存时,页必须返回到正确的迁移链表。这之所以可行,是因为能够从`get_pageblock_migratetype`获得所需的信息. 参见[include/linux/mmzone.h?v=4.7, line 84](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L84) - - -```cpp -#define get_pageblock_migratetype(page) \ - get_pfnblock_flags_mask(page, page_to_pfn(page), \ - PB_migrate_end, MIGRATETYPE_MASK) -``` - -##2.8 /proc/pagetypeinfo获取页面分配状态 -------- - -最后请注意, 在各个迁移链表之间, 当前的页面分配状态可以从`/proc/pagetypeinfo`获得. - - -![proc/pagetypeinfo]() - - - - -##2.9 可移动性的分组的初始化 -------- - - - - -在内存子系统初始化期间, memmap_init_zone负责处理内存域的page实例. 该函数定义在[mm/page_alloc.c?v=4.7, line 5139](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5139), 该函数完成了一些不怎么有趣的标准初始化工作,但其中有一件是实质性的,即所有的页最初都标记为可移动的. 参见[mm/page_alloc.c?v=4.7, line 5224](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5224) - - -```cpp -/* - * Initially all pages are reserved - free ones are freed - * up by free_all_bootmem() once the early boot process is - * done. Non-atomic initialization, single-pass. - */ -void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone, - unsigned long start_pfn, enum memmap_context context) -{ - /* ...... */ - - for (pfn = start_pfn; pfn < end_pfn; pfn++) { - /* ...... */ -not_early: - if (!(pfn & (pageblock_nr_pages - 1))) { - struct page *page = pfn_to_page(pfn); - - __init_single_page(page, pfn, zone, nid); - set_pageblock_migratetype(page, MIGRATE_MOVABLE); - } else { - __init_single_pfn(pfn, zone, nid); - } - } -} -``` - - -在分配内存时, 如果必须"盗取"不同于预定迁移类型的内存区, 内核在策略上倾向于"盗取"更大的内存区. 由于所有页最初都是可移动的, 那么在内核分配不可移动的内存区时, 则必须"盗取". - -实际上, 在启动期间分配可移动内存区的情况较少, 那么分配器有很高的几率分配长度最大的内存区, 并将其从可移动列表转换到不可移动列表. 由于分配的内存区长度是最大的, 因此不会向可移动内存中引入碎片. - -总而言之, 这种做法避免了启动期间内核分配的内存(经常在系统的整个运行时间都不释放)散布到物理内存各处, 从而使其他类型的内存分配免受碎片的干扰,这也是页可移动性分组框架的最重要的目标之一. - - - -#3 虚拟可移动内存域避免内存碎片 -------- - -##3.1 虚拟可移动内存域 - -依据可移动性组织页是防止物理内存碎片的一种可能方法,内核还提供了另一种阻止该问题的手段 : 虚拟内存域`ZONE_MOVABLE`. - -该机制在内核2.6.23开发期间已经并入内核, 比可移动性分组框架加入内核早一个版本. 与可移动性分组相反, `ZONE_MOVABLE`特性必须由管理员显式激活. - -基本思想很简单 : 可用的物理内存划分为两个内存域, 一个用于可移动分配, 一个用于不可移动分配. 这会自动防止不可移动页向可移动内存域引入碎片. - - -这马上引出了另一个问题 : 内核如何在两个竞争的内存域之间分配可用的内存? - -这显然对内核要求太高,因此系统管理员必须作出决定。毕竟,人可以更好地预测计算机需要处理的场景,以及各种类型内存分配的预期分布. - -##3.2 数据结构 -------- - - -`kernelcore`参数用来指定用于不可移动分配的内存数量, 即用于既不能回收也不能迁移的内存数量。剩余的内存用于可移动分配。在分析该参数之后,结果保存在全局变量required_kernelcore中. - -还可以使用参数`movablecore`控制用于可移动内存分配的内存数量。required_kernelcore的大小将会据此计算。如果有些聪明人同时指定两个参数,内核会按前述方法计算出`required_kernelcore`的值,并取指定值和计算值中较大的一个. - -全局变量required_kernelcore和required_movablecore的定义在[mm/page_alloc.c?v=4.7, line 261](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L261), 如下所示 - -```cpp -static unsigned long __initdata required_kernelcore; -static unsigned long __initdata required_movablecore; -``` - - -取决于体系结构和内核配置,ZONE_MOVABLE内存域可能位于高端或普通内存域, 参见[include/linux/mmzone.h?v=4.7, line 267](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L267) - -```cpp -enum zone_type { -#ifdef CONFIG_ZONE_DMA - ZONE_DMA, -#endif -#ifdef CONFIG_ZONE_DMA32 - ZONE_DMA32, -#endif - ZONE_NORMAL, -#ifdef CONFIG_HIGHMEM - ZONE_HIGHMEM, -#endif - ZONE_MOVABLE, -#ifdef CONFIG_ZONE_DEVICE - ZONE_DEVICE, -#endif - __MAX_NR_ZONES -}; -``` - -与系统中所有其他的内存域相反, ZONE_MOVABLE并不关联到任何硬件上有意义的内存范围. 实际上, 该内存域中的内存取自高端内存域或普通内存域, 因此我们在下文中称ZONE_MOVABLE是一个虚拟内存域. - -辅助函数[find_zone_movable_pfns_for_nodes](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6231)用于计算进入ZONE_MOVABLE的内存数量. - -如果kernelcore和movablecore参数都没有指定find_zone_movable_pfns_for_nodes会使ZONE_MOVABLE保持为空,该机制处于无效状态. - -谈到从物理内存域提取多少内存用于ZONE_MOVABLE的问题, 必须考虑下面两种情况 - -* 用于不可移动分配的内存会平均地分布到所有内存结点上 - -* 只使用来自最高内存域的内存。在内存较多的32位系统上, 这通常会是ZONE_HIGHMEM, 但是对于64位系统,将使用ZONE_NORMAL或ZONE_DMA32. - -实际计算相当冗长,也不怎么有趣,因此我不详细讨论了。实际上起作用的是结果 - -* 用于为虚拟内存域ZONE_MOVABLE提取内存页的物理内存域,保存在全局变量movable_zone中 - -* 对每个结点来说, zone_movable_pfn[node_id]表示ZONE_MOVABLE在movable_zone内存域中所取得内存的起始地址. - - - zone_movable_pfn定义在[mm/page_alloc.c?v=4.7, line 263](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L263) - -```cpp -static unsigned long __meminitdata zone_movable_pfn[MAX_NUMNODES]; -static bool mirrored_kernelcore; -``` - -内核确保这些页将用于满足符合ZONE_MOVABLE职责的内存分配。 - -##3.3 实现 -------- - - -到现在为止描述的数据结构如何应用? - - -类似于页面迁移方法, 分配标志在此扮演了关键角色. - - -具体的实现将在3.5.4节更详细地讨论. 目前只要知道所有可移动分配都必须指定\__GFP_HIGHMEM和\__GFP_MOVABLE即可. - - +伙伴系统之避免碎片 +======= + + + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + + + +#1 前景提要 +------- + + +##1.1 Linux内存层次结构 +------- + + + +##1.2 碎片化问题 + + + +**分页与分段** + +页是信息的物理单位, 分页是为了实现非连续分配, 以便解决内存碎片问题, 或者说分页是由于系统管理的需要. 段是信息的逻辑单位,它含有一组意义相对完整的信息, 分段的目的是为了更好地实现共享, 满足用户的需要. + +页的大小固定且由系统确定, 将逻辑地址划分为页号和页内地址是由机器硬件实现的. 而段的长度却不固定, 决定于用户所编写的程序, 通常由编译程序在对源程序进行编译时根据信息的性质来划分. + +分页的作业地址空间是一维的. 分段的地址空间是二维的. + + + +**内部碎片与外部碎片** + + +在页式虚拟存储系统中, 用户作业的地址空间被划分成若干大小相等的页面, 存储空间也分成也页大小相等的物理块, 但一般情况下, 作业的大小不可能都是物理块大小的整数倍, 因此作业的最后一页中仍有部分空间被浪费掉了. 由此可知, 页式虚拟存储系统中存在**内碎片**. + + +在段式虚拟存储系统中, 作业的地址空间由若干个逻辑分段组成, 每段分配一个连续的内存区, 但各段之间不要求连续, 其内存的分配方式类似于动态分区分配.由此可知, 段式虚拟存储系统中存在**外碎片**. + + + + + +在内存管理中, "内零头"和"外零头"个指的是什么? + +在固定式分区分配, 可变式分区分配, 页式虚拟存储系统, 段式虚拟存储系统中, 各会存在何种碎片? 为什么? + + +解答: + +在存储管理中 + +* **内碎片**是指分配给作业的存储空间中未被利用的部分 + + 在固定式分区分配中, 为将一个用户作业装入内存, 内存分配程序从系统分区表中找出一个能满足作业要求的空闲分区分配给作业, 由于一个作业的大小并不一定与分区大小相等, 因此, 分区中有一部分存储空间浪费掉了. 由此可知, 固定式分区分配中存在**内碎片**. + +* **外碎片**是指系统中无法利用的小存储块. + + 在可变式分区分配中, 为把一个作业装入内存, 应按照一定的分配算法从系统中找出一个能满足作业需求的空闲分区分配给作业, 如果这个空闲分区的容量比作业申请的空间容量要大, 则将该分区一分为二, 一部分分配给作业, 剩下的部分仍然留作系统的空闲分区。由此可知,可变式分区分配中存在**外碎片**. + + + +简言之 + +随着存储区的分配和释放过程的进行, 在各个被分配出去的分区之间会存在很多的小空闲区, 暂时不能被利用, 这就是"外部碎片". + + +在固定分区管理算法中, 分给程序的内存空间往往大于程序所需的空间, 这剩余部分的空间不能被其他程序所用, 这就是"内部碎片" + + +##1.3 buddy伙伴系统 +------- + + + +##1.4 今日内容(buddy伙伴系统如何避免碎片) +------- + +Linux伙伴系统分配内存的大小要求2的幂指数页, 这也会产生严重的**内部碎片**. + +伙伴系统的基本原理已经在前面已经讨论过, 一个双链表即可满足伙伴系统的所有需求, 其方案在最近几年间确实工作得非常好。但在Linux内存管理方面,有一个长期存在的问题 : 在系统启动并长期运行后,物理内存会产生很多碎片。该情形如下图所示 + +![物理内存的碎片](../images/physical_memory_fragmentation.png) + +假定内存由60页组成,这显然不是超级计算机,但用于示例却足够了。左侧的地址空间中散布着空闲页。尽管大约25%的物理内存仍然未分配,但最大的连续空闲区只有一页. 这对用户空间应用程序没有问题:其内存是通过页表映射的,无论空闲页在物理内存中的分布如何,应用程序看到的内存 +似乎总是连续的。右图给出的情形中,空闲页和使用页的数目与左图相同,但所有空闲页都位于一个连续区中。 + +但对内核来说,碎片是一个问题. 由于(大多数)物理内存一致映射到地址空间的内核部分, 那么在左图的场景中, 无法映射比一页更大的内存区. 尽管许多时候内核都分配的是比较小的内存, 但也有时候需要分配多于一页的内存. 显而易见, 在分配较大内存的情况下, 右图中所有已分配页和空闲页都处于连续内存区的情形,是更为可取的. + +很有趣的一点是, 在大部分内存仍然未分配时, 就也可能发生碎片问题. 考虑下图所示的情形. + +只分配了4页,但可分配的最大连续区只有8页,因为伙伴系统所能工作的分配范围只能是2的幂次. + + +![物理内存的碎片](../images/some_memory_fragmentation.png) + + +我提到内存碎片只涉及内核,这只是部分正确的。大多数现代CPU都提供了使用巨型页的可能性,比普通页大得多。这对内存使用密集的应用程序有好处。在使用更大的页时,地址转换后备缓冲器只需处理较少的项,降低了TLB缓存失效的可能性。但分配巨型页需要连续的空闲物理内存! + +很长时间以来,物理内存的碎片确实是Linux的弱点之一。尽管已经提出了许多方法,但没有哪个方法能够既满足Linux需要处理的各种类型工作负荷提出的苛刻需求,同时又对其他事务影响不大。 + + +目前Linux内核为解决内存碎片的方案提供了两类解决方案 + +* 依据可移动性组织页避免内存碎片 + +* 虚拟可移动内存域避免内存碎片 + +#2 依据可移动性组织页避免内存碎片 +------- + +依据可移动性组织页是方式物理内存碎片的一种可能方法. + +##2.1 依据可移动性组织页 +------- + + +在内核2.6.24开发期间,防止碎片的方法最终加入内核。在我讨论具体策略之前,有一点需要澄清。 + +文件系统也有碎片,该领域的碎片问题主要通过碎片合并工具解决。它们分析文件系统,重新排序已分配存储块,从而建立较大的连续存储区. 理论上,该方法对物理内存也是可能的,但由于许多物理内存页不能移动到任意位置,阻碍了该方法的实施。因此,内核的方法是反碎片(anti-fragmentation), 即试图从最初开始尽可能防止碎片. + + +反碎片的工作原理如何? + + + +为理解该方法,我们必须知道内核将已分配页划分为下面3种不同类型。 + + +| 页面类型 | 描述 | 举例 | +|:---------:|:-----:|:-----:| +| 不可移动页 | 在内存中有固定位置, **不能移动**到其他地方. | 核心内核分配的大多数内存属于该类别 | +| 可移动页 | **可以随意地移动**. | 属于用户空间应用程序的页属于该类别. 它们是通过页表映射的
如果它们复制到新位置,页表项可以相应地更新,应用程序不会注意到任何事 | +| 可回收页 | **不能直接移动, 但可以删除, 其内容可以从某些源重新生成**. | 例如,映射自文件的数据属于该类别
kswapd守护进程会根据可回收页访问的频繁程度,周期性释放此类内存. , 页面回收本身就是一个复杂的过程. 内核会在可回收页占据了太多内存时进行回收, 在内存短缺(即分配失败)时也可以发起页面回收. | + + + +页的可移动性,依赖该页属于3种类别的哪一种. 内核使用的**反碎片技术**, 即基于将具有相同可移动性的页分组的思想. + + +为什么这种方法有助于减少碎片? + + +由于页无法移动, 导致在原本几乎全空的内存区中无法进行连续分配. 根据页的可移动性, 将其分配到不同的列表中, 即可防止这种情形. 例如, 不可移动的页不能位于可移动内存区的中间, 否则就无法从该内存区分配较大的连续内存块. + + +想一下, 上图中大多数空闲页都属于可回收的类别, 而分配的页则是不可移动的. 如果这些页聚集到两个不同的列表中, 如下图所示. 在不可移动页中仍然难以找到较大的连续空闲空间, 但对可回收的页, 就容易多了. + + +![减少内存碎片](../images/little_memory_fragmentation.png) + + +但要注意, 从最初开始, 内存并未划分为可移动性不同的区. 这些是在运行时形成的. 内核的另一种方法确实将内存分区, 分别用于可移动页和不可移动页的分配, 我会下文讨论其工作原理. 但这种划分对这里描述的方法是不必要的 + + + + +##2.2 迁移类型 +------- + + +尽管内核使用的反碎片技术卓有成效,它对伙伴分配器的代码和数据结构几乎没有影响。内核定义了一些枚举常量(早期用宏来实现)来表示不同的迁移类型, 参见[include/linux/mmzone.h?v=4.7, line 38](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L38) + +```cpp +enum { + MIGRATE_UNMOVABLE, + MIGRATE_MOVABLE, + MIGRATE_RECLAIMABLE, + MIGRATE_PCPTYPES, /* the number of types on the pcp lists */ + MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES, +#ifdef CONFIG_CMA + /* + * MIGRATE_CMA migration type is designed to mimic the way + * ZONE_MOVABLE works. Only movable pages can be allocated + * from MIGRATE_CMA pageblocks and page allocator never + * implicitly change migration type of MIGRATE_CMA pageblock. + * + * The way to use it is to change migratetype of a range of + * pageblocks to MIGRATE_CMA which can be done by + * __free_pageblock_cma() function. What is important though + * is that a range of pageblocks must be aligned to + * MAX_ORDER_NR_PAGES should biggest page be bigger then + * a single pageblock. + */ + MIGRATE_CMA, +#endif +#ifdef CONFIG_MEMORY_ISOLATION + MIGRATE_ISOLATE, /* can't allocate from here */ +#endif + MIGRATE_TYPES +}; +``` + + +| 宏 | 类型 | +|:----:|:-----:| +| MIGRATE_UNMOVABLE | 不可移动页 | +| MIGRATE_MOVABLE | 可移动页 | +| MIGRATE_RECLAIMABLE | 可回收页 | +| MIGRATE_PCPTYPES | 是per_cpu_pageset, 即用来表示每CPU页框高速缓存的数据结构中的链表的迁移类型数目 | +| MIGRATE_HIGHATOMIC | = MIGRATE_PCPTYPES, 在罕见的情况下,内核需要分配一个高阶的页面块而不能休眠.如果向具有特定可移动性的列表请求分配内存失败,这种紧急情况下可从MIGRATE_HIGHATOMIC中分配内存 | +| MIGRATE_CMA | Linux内核最新的连续内存分配器(CMA), 用于避免预留大块内存 | +| MIGRATE_ISOLATE | 是一个特殊的虚拟区域, 用于跨越NUMA结点移动物理内存页. 在大型系统上, 它有益于将物理内存页移动到接近于使用该页最频繁的CPU. | +| MIGRATE_TYPES | 只是表示迁移类型的数目, 也不代表具体的区域 | + +对于MIGRATE_CMA类型, 其中在我们使用ARM等嵌入式Linux系统的时候, 一个头疼的问题是GPU, Camera, HDMI等都需要预留大量连续内存,这部分内存平时不用,但是一般的做法又必须先预留着. 目前, Marek Szyprowski和Michal Nazarewicz实现了一套全新的Contiguous Memory Allocator. 通过这套机制, 我们可以做到不预留内存,这些内存平时是可用的,只有当需要的时候才被分配给Camera,HDMI等设备. 参照[宋宝华--Linux内核最新的连续内存分配器(CMA)——避免预留大块内存](http://21cnbao.blog.51cto.com/109393/898846/), 内核为此提供了函数is_migrate_cma来检测当前类型是否为MIGRATE_CMA, 该函数定义在[include/linux/mmzone.h?v=4.7, line 69](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L69) + +```cpp +/* In mm/page_alloc.c; keep in sync also with show_migration_types() there */ +extern char * const migratetype_names[MIGRATE_TYPES]; + +#ifdef CONFIG_CMA +# define is_migrate_cma(migratetype) unlikely((migratetype) == MIGRATE_CMA) +#else +# define is_migrate_cma(migratetype) false +#endif +``` + +##2.3 free_area的改进 +------- + + +对伙伴系统数据结构的主要调整, 是将空闲列表分解为MIGRATE_TYPE个列表, 可以参见free_area的定义[include/linux/mmzone.h?v=4.7, line 88](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L88) + +```cpp +struct free_area +{ + struct list_head free_list[MIGRATE_TYPES]; +unsigned long nr_free; +}; +``` + +* nr_free统计了所有列表上空闲页的数目,而每种迁移类型都对应于一个空闲列表 + + + +这样我们的伙伴系统的内存框架就如下所示 + +![依据可移动性组织页](../images/migrate_buddy.png) + + +宏for_each_migratetype_order(order, type)可用于迭代指定迁移类型的所有分配阶 + +```cpp +#define for_each_migratetype_order(order, type) \ + for (order = 0; order < MAX_ORDER; order++) \ + for (type = 0; type < MIGRATE_TYPES; type++) +``` + + +##2.4 迁移备用列表fallbacks +------- + + + +如果内核无法满足针对某一给定迁移类型的分配请求, 会怎么样? + + + +此前已经出现过一个类似的问题, 即特定的NUMA内存域无法满足分配请求时. 我们需要从其他内存域中选择一个代价最低的内存域完成内存的分配, 因此内核在内存的结点pg_data_t中提供了一个备用内存域列表zonelists. + +内核在内存迁移的过程中处理这种情况下的做法是类似的. 提供了一个备用列表fallbacks, 规定了在指定列表中无法满足分配请求时. 接下来应使用哪一种迁移类型, 定义在[mm/page_alloc.c?v=4.7, line 1799](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L1799) + + +```cpp +/* + * This array describes the order lists are fallen back to when + * the free lists for the desirable migrate type are depleted + * 该数组描述了指定迁移类型的空闲列表耗尽时 + * 其他空闲列表在备用列表中的次序 + */ +static int fallbacks[MIGRATE_TYPES][4] = { + // 分配不可移动页失败的备用列表 + [MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_TYPES }, + // 分配可回收页失败时的备用列表 + [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_TYPES }, + // 分配可移动页失败时的备用列表 + [MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_TYPES }, +#ifdef CONFIG_CMA + [MIGRATE_CMA] = { MIGRATE_TYPES }, /* Never used */ +#endif +#ifdef CONFIG_MEMORY_ISOLATION + [MIGRATE_ISOLATE] = { MIGRATE_TYPES }, /* Never used */ +#endif +}; +``` + +>该数据结构大体上是自明的 : +> +>每一行对应一个类型的备用搜索域的顺序, 在内核想要分配不可移动页`MIGRATE_UNMOVABLE`时, 如果对应链表为空, 则遍历fallbacks[MIGRATE_UNMOVABLE], 首先后退到可回收页链表`MIGRATE_RECLAIMABLE`, 接下来到可移动页链表`MIGRATE_MOVABLE`, 最后到紧急分配链表`MIGRATE_TYPES`. + + +##2.5 全局pageblock_order变量 +------- + + +全局变量和辅助函数尽管页可移动性分组特性总是编译到内核中,但只有在系统中有足够内存可以分配到多个迁移类型对应的链表时,才是有意义的。由于每个迁移链表都应该有适当数量的内存,内核需要定义"适当"的概念. 这是通过两个全局变量pageblock_order和pageblock_nr_pages提供的. 第一个表示内核认为是"大"的一个分配阶, pageblock_nr_pages则表示该分配阶对应的页数。如果体系结构提供了巨型页机制, 则pageblock_order通常定义为巨型页对应的分配阶. 定义在[include/linux/pageblock-flags.h?v=4.7, line 44](http://lxr.free-electrons.com/source/include/linux/pageblock-flags.h?v=4.7#L42) + + +```cpp +#ifdef CONFIG_HUGETLB_PAGE + + #ifdef CONFIG_HUGETLB_PAGE_SIZE_VARIABLE + + /* Huge page sizes are variable */ + extern unsigned int pageblock_order; + + #else /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */ + + /* Huge pages are a constant size */ + #define pageblock_order HUGETLB_PAGE_ORDER + + #endif /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */ + +#else /* CONFIG_HUGETLB_PAGE */ + + /* If huge pages are not used, group by MAX_ORDER_NR_PAGES */ + #define pageblock_order (MAX_ORDER-1) + +#endif /* CONFIG_HUGETLB_PAGE */ + +#define pageblock_nr_pages (1UL << pageblock_order) +``` + +在IA-32体系结构上, 巨型页长度是4MB, 因此每个巨型页由1024个普通页组成, 而HUGETLB_PAGE_ORDER则定义为10. 相比之下, IA-64体系结构允许设置可变的普通和巨型页长度, 因此HUGETLB_PAGE_ORDER的值取决于内核配置. + +如果体系结构不支持巨型页, 则将其定义为第二高的分配阶, 即`MAX_ORDER - 1` + +```cpp +/* If huge pages are not used, group by MAX_ORDER_NR_PAGES */ +#define pageblock_order (MAX_ORDER-1) +``` + +如果各迁移类型的链表中没有一块较大的连续内存, 那么页面迁移不会提供任何好处, 因此在可用内存太少时内核会关闭该特性. 这是在build_all_zonelists函数中检查的, 该函数用于初始化内存域列表. 如果没有足够的内存可用, 则全局变量[`page_group_by_mobility_disabled`](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L79)设置为0, 否则设置为1. + +内核如何知道给定的分配内存属于何种迁移类型? + +我们将在以后讲解, 有关各个内存分配的细节都通过分配掩码指定. + +内核提供了两个标志,分别用于表示分配的内存是可移动的(\__GFP_MOVABLE)或可回收的(\__GFP_RECLAIMABLE). + + + +##2.6 gfpflags_to_migratetype转换分配标识到迁移类型 +------- + + +如果这些标志都没有设置, 则分配的内存假定为不可移动的. 辅助函数gfpflags_to_migratetype可用于转换分配标志及对应的迁移类型, 该函数定义在[include/linux/gfp.h?v=4.7, line 266](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L266) + +```cpp +static inline int gfpflags_to_migratetype(const gfp_t gfp_flags) +{ + VM_WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK); + BUILD_BUG_ON((1UL << GFP_MOVABLE_SHIFT) != ___GFP_MOVABLE); + BUILD_BUG_ON((___GFP_MOVABLE >> GFP_MOVABLE_SHIFT) != MIGRATE_MOVABLE); + + if (unlikely(page_group_by_mobility_disabled)) + return MIGRATE_UNMOVABLE; + + /* Group based on mobility */ + return (gfp_flags & GFP_MOVABLE_MASK) >> GFP_MOVABLE_SHIFT; +} +``` + +>linux-2.6.x的内核中转换分配标志及对应的迁移类型的辅助函数为allocflags_to_migratetype, 这个名字会有歧义的, 让我们误以为参数的标识中有alloc flags, 但是其实并不然, 因此后来的内核中将该函数更名为gfpflags_to_migratetype, 参见[Rename it to gfpflags_to_migratetype()](https://patchwork.kernel.org/patch/4291831) + +在2.6.25中为如下接口 + +```cpp +/* Convert GFP flags to their corresponding migrate type */ +static inline int allocflags_to_migratetype(gfp_t gfp_flags) +{ + WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK); + + if (unlikely(page_group_by_mobility_disabled)) + return MIGRATE_UNMOVABLE; + + /* Group based on mobility */ + return (((gfp_flags & __GFP_MOVABLE) != 0) << 1) | + ((gfp_flags & __GFP_RECLAIMABLE) != 0); +} +```` + +如果停用了页面迁移特性, 则所有的页都是不可移动的. 否则. 该函数的返回值可以直接用作free_area.free_list的数组索引. + +##2.7 pageblock_flags变量与其函数接口 + +最后要注意, 每个内存域都提供了一个特殊的字段, 可以跟踪包含pageblock_nr_pages个页的内存区的属性. 即zone->pageblock_flags字段, 当前只有与页可移动性相关的代码使用, 参见[include/linux/mmzone.h?v=4.7, line 367](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L367) + + +```cpp +struct zone +{ +#ifndef CONFIG_SPARSEMEM + /* + * Flags for a pageblock_nr_pages block. See pageblock-flags.h. + * In SPARSEMEM, this map is stored in struct mem_section + */ + unsigned long *pageblock_flags; +#endif /* CONFIG_SPARSEMEM */ +}; +``` + +在初始化期间, 内核自动确保对内存域中的每个不同的迁移类型分组, 在pageblock_flags中都分配了足够存储NR_PAGEBLOCK_BITS个比特位的空间。当前,表示一个连续内存区的迁移类型需要3个比特位, 参见[include/linux/pageblock-flags.h?v=4.7, line 28](http://lxr.free-electrons.com/source/include/linux/pageblock-flags.h?v=4.7#L28) + +```cpp +/* Bit indices that affect a whole block of pages */ +enum pageblock_bits { + PB_migrate, + PB_migrate_end = PB_migrate + 3 - 1, + /* 3 bits required for migrate types */ + PB_migrate_skip,/* If set the block is skipped by compaction */ + + /* + * Assume the bits will always align on a word. If this assumption + * changes then get/set pageblock needs updating. + */ + NR_PAGEBLOCK_BITS +}; +``` + + +内核提供`set_pageblock_migratetype`负责设置以page为首的一个内存区的迁移类型, 该函数定义在[mm/page_alloc.c?v=4.7, line 458](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L458), 如下所示 + + +```cpp +void set_pageblock_migratetype(struct page *page, int migratetype) +{ + if (unlikely(page_group_by_mobility_disabled && + migratetype < MIGRATE_PCPTYPES)) + migratetype = MIGRATE_UNMOVABLE; + + set_pageblock_flags_group(page, (unsigned long)migratetype, + PB_migrate, PB_migrate_end); +} +``` + + + +`migratetype`参数可以通过上文介绍的`gfpflags_to_migratetype`辅助函数构建. 请注意很重要的一点, 页的迁移类型是预先分配好的, 对应的比特位总是可用, 与页是否由伙伴系统管理无关. 在释放内存时,页必须返回到正确的迁移链表。这之所以可行,是因为能够从`get_pageblock_migratetype`获得所需的信息. 参见[include/linux/mmzone.h?v=4.7, line 84](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L84) + + +```cpp +#define get_pageblock_migratetype(page) \ + get_pfnblock_flags_mask(page, page_to_pfn(page), \ + PB_migrate_end, MIGRATETYPE_MASK) +``` + +##2.8 /proc/pagetypeinfo获取页面分配状态 +------- + +最后请注意, 在各个迁移链表之间, 当前的页面分配状态可以从`/proc/pagetypeinfo`获得. + + +![proc/pagetypeinfo]() + + + + +##2.9 可移动性的分组的初始化 +------- + + + + +在内存子系统初始化期间, memmap_init_zone负责处理内存域的page实例. 该函数定义在[mm/page_alloc.c?v=4.7, line 5139](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5139), 该函数完成了一些不怎么有趣的标准初始化工作,但其中有一件是实质性的,即所有的页最初都标记为可移动的. 参见[mm/page_alloc.c?v=4.7, line 5224](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5224) + + +```cpp +/* + * Initially all pages are reserved - free ones are freed + * up by free_all_bootmem() once the early boot process is + * done. Non-atomic initialization, single-pass. + */ +void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone, + unsigned long start_pfn, enum memmap_context context) +{ + /* ...... */ + + for (pfn = start_pfn; pfn < end_pfn; pfn++) { + /* ...... */ +not_early: + if (!(pfn & (pageblock_nr_pages - 1))) { + struct page *page = pfn_to_page(pfn); + + __init_single_page(page, pfn, zone, nid); + set_pageblock_migratetype(page, MIGRATE_MOVABLE); + } else { + __init_single_pfn(pfn, zone, nid); + } + } +} +``` + + +在分配内存时, 如果必须"盗取"不同于预定迁移类型的内存区, 内核在策略上倾向于"盗取"更大的内存区. 由于所有页最初都是可移动的, 那么在内核分配不可移动的内存区时, 则必须"盗取". + +实际上, 在启动期间分配可移动内存区的情况较少, 那么分配器有很高的几率分配长度最大的内存区, 并将其从可移动列表转换到不可移动列表. 由于分配的内存区长度是最大的, 因此不会向可移动内存中引入碎片. + +总而言之, 这种做法避免了启动期间内核分配的内存(经常在系统的整个运行时间都不释放)散布到物理内存各处, 从而使其他类型的内存分配免受碎片的干扰,这也是页可移动性分组框架的最重要的目标之一. + + + +#3 虚拟可移动内存域避免内存碎片 +------- + +##3.1 虚拟可移动内存域 + +依据可移动性组织页是防止物理内存碎片的一种可能方法,内核还提供了另一种阻止该问题的手段 : 虚拟内存域`ZONE_MOVABLE`. + +该机制在内核2.6.23开发期间已经并入内核, 比可移动性分组框架加入内核早一个版本. 与可移动性分组相反, `ZONE_MOVABLE`特性必须由管理员显式激活. + +基本思想很简单 : 可用的物理内存划分为两个内存域, 一个用于可移动分配, 一个用于不可移动分配. 这会自动防止不可移动页向可移动内存域引入碎片. + + +这马上引出了另一个问题 : 内核如何在两个竞争的内存域之间分配可用的内存? + +这显然对内核要求太高,因此系统管理员必须作出决定。毕竟,人可以更好地预测计算机需要处理的场景,以及各种类型内存分配的预期分布. + +##3.2 数据结构 +------- + + +`kernelcore`参数用来指定用于不可移动分配的内存数量, 即用于既不能回收也不能迁移的内存数量。剩余的内存用于可移动分配。在分析该参数之后,结果保存在全局变量required_kernelcore中. + +还可以使用参数`movablecore`控制用于可移动内存分配的内存数量。required_kernelcore的大小将会据此计算。如果有些聪明人同时指定两个参数,内核会按前述方法计算出`required_kernelcore`的值,并取指定值和计算值中较大的一个. + +全局变量required_kernelcore和required_movablecore的定义在[mm/page_alloc.c?v=4.7, line 261](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L261), 如下所示 + +```cpp +static unsigned long __initdata required_kernelcore; +static unsigned long __initdata required_movablecore; +``` + + +取决于体系结构和内核配置,ZONE_MOVABLE内存域可能位于高端或普通内存域, 参见[include/linux/mmzone.h?v=4.7, line 267](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L267) + +```cpp +enum zone_type { +#ifdef CONFIG_ZONE_DMA + ZONE_DMA, +#endif +#ifdef CONFIG_ZONE_DMA32 + ZONE_DMA32, +#endif + ZONE_NORMAL, +#ifdef CONFIG_HIGHMEM + ZONE_HIGHMEM, +#endif + ZONE_MOVABLE, +#ifdef CONFIG_ZONE_DEVICE + ZONE_DEVICE, +#endif + __MAX_NR_ZONES +}; +``` + +与系统中所有其他的内存域相反, ZONE_MOVABLE并不关联到任何硬件上有意义的内存范围. 实际上, 该内存域中的内存取自高端内存域或普通内存域, 因此我们在下文中称ZONE_MOVABLE是一个虚拟内存域. + +辅助函数[find_zone_movable_pfns_for_nodes](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6231)用于计算进入ZONE_MOVABLE的内存数量. + +如果kernelcore和movablecore参数都没有指定find_zone_movable_pfns_for_nodes会使ZONE_MOVABLE保持为空,该机制处于无效状态. + +谈到从物理内存域提取多少内存用于ZONE_MOVABLE的问题, 必须考虑下面两种情况 + +* 用于不可移动分配的内存会平均地分布到所有内存结点上 + +* 只使用来自最高内存域的内存。在内存较多的32位系统上, 这通常会是ZONE_HIGHMEM, 但是对于64位系统,将使用ZONE_NORMAL或ZONE_DMA32. + +实际计算相当冗长,也不怎么有趣,因此我不详细讨论了。实际上起作用的是结果 + +* 用于为虚拟内存域ZONE_MOVABLE提取内存页的物理内存域,保存在全局变量movable_zone中 + +* 对每个结点来说, zone_movable_pfn[node_id]表示ZONE_MOVABLE在movable_zone内存域中所取得内存的起始地址. + + + zone_movable_pfn定义在[mm/page_alloc.c?v=4.7, line 263](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L263) + +```cpp +static unsigned long __meminitdata zone_movable_pfn[MAX_NUMNODES]; +static bool mirrored_kernelcore; +``` + +内核确保这些页将用于满足符合ZONE_MOVABLE职责的内存分配。 + +##3.3 实现 +------- + + +到现在为止描述的数据结构如何应用? + + +类似于页面迁移方法, 分配标志在此扮演了关键角色. + + +具体的实现将在3.5.4节更详细地讨论. 目前只要知道所有可移动分配都必须指定\__GFP_HIGHMEM和\__GFP_MOVABLE即可. + + 由于内核依据分配标志确定进行内存分配的内存域, 在设置了上述的标志时, 可以选择ZONE_MOVABLE内存域. 这是将ZONE_MOVABLE集成到伙伴系统中所需的唯一改变!其余的可以通过适用于所有内存域的通用例程处理, 我们将在下文讨论 \ No newline at end of file diff --git a/study/kernel/02-memory/04-buddy/04-alloc_page/README.md b/study/kernel/02-memory/04-buddy/04-alloc_page/README.md index 571b330..1d34840 100644 --- a/study/kernel/02-memory/04-buddy/04-alloc_page/README.md +++ b/study/kernel/02-memory/04-buddy/04-alloc_page/README.md @@ -1,793 +1,793 @@ -服务器体系与共享存储器架构 -======= - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - - - - -在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法. - -Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述. - -伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者. - -* 内核如何记住哪些内存块是空闲的 - -* 分配空闲页面的方法 - -* 影响分配器行为的众多标识位 - -* 内存碎片的问题和分配器如何处理碎片 - - - -#内存分配API -------- - -##2.1 内存分配器API -------- - - - -就伙伴系统的接口而言, NUMA或UMA体系结构是没有差别的, 二者的调用语法都是相同的. - -所有函数的一个共同点是 : 只能分配2的整数幂个页. - -因此,接口中不像C标准库的malloc函数或bootmem和memblock分配器那样指定了所需内存大小作为参数. 相反, 必须指定的是分配阶, 伙伴系统将在内存中分配$2^order$页. 内核中细粒度的分配只能借助于slab分配器(或者slub、slob分配器), 后者基于伙伴系统 - - -| 内存分配函数 | 功能 | 定义 | -|:-----:|:-----:| -| alloc_pages(mask, order) | 分配$2^order$页并返回一个struct page的实例,表示分配的内存块的起始页 | [NUMA-include/linux/gfp.h, line 466](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L466)
[UMA-include/linux/gfp.h?v=4.7, line 476](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L476) | -| alloc_page(mask) | 是前者在order = 0情况下的简化形式,只分配一页 | [include/linux/gfp.h?v=4.7, line 483](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L483) | -| get_zeroed_page(mask) | 分配一页并返回一个page实例,页对应的内存填充0(所有其他函数,分配之后页的内容是未定义的) | [mm/page_alloc.c?v=4.7, line 3900](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3900)| | -| [__get_free_pages(mask, order)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883)
[__get_free_page(mask)](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L500) | 工作方式与上述函数相同,但返回分配内存块的虚拟地址,而不是page实例 | -| get_dma_pages(gfp_mask, order) | 用来获得适用于DMA的页. | [include/linux/gfp.h?v=4.7, line 503](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L503) | - - -在空闲内存无法满足请求以至于分配失败的情况下,所有上述函数都返回空指针(比如alloc_pages和alloc_page)或者0(比如get_zeroed_page、__get_free_pages和__get_free_page). - -因此内核在各次分配之后都必须检查返回的结果. 这种惯例与设计得很好的用户层应用程序没什么不同, 但在内核中忽略检查会导致严重得多的故障 - - -内核除了伙伴系统函数之外, 还提供了其他内存管理函数. 它们以伙伴系统为基础, 但并不属于伙伴分配器自身. 这些函数包括vmalloc和vmalloc_32, 使用页表将不连续的内存映射到内核地址空间中, 使之看上去是连续的. - -还有一组kmalloc类型的函数, 用于分配小于一整页的内存区. 其实现将在以后分别讨论。 - - - -##2.2 内存分配API统一到alloc_pages接口 -------- - - -通过使用标志、内存域修饰符和各个分配函数,内核提供了一种非常灵活的内存分配体系.尽管如此, 所有接口函数都可以追溯到一个简单的基本函数(alloc_pages_node) - -分配单页的函数[`alloc_page`](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L483)和[`__get_free_page`](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L500), 还有[`__get_dma_pages`](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L503)是借助于宏定义的. - -```cpp -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L483 -#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0) - -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L500 -#define __get_free_page(gfp_mask) \ - __get_free_pages((gfp_mask), 0)` - -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L503 -#define __get_dma_pages(gfp_mask, order) \ - __get_free_pages((gfp_mask) | GFP_DMA, (order)) -``` - -[`get_zeroed_page`](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3900)的实现也没什么困难, 对`__get_free_pages`使用`__GFP_ZERO`标志,即可分配填充字节0的页. 再返回与页关联的内存区地址即可. - - -```cpp -// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3900 -unsigned long get_zeroed_page(gfp_t gfp_mask) -{ - return __get_free_pages(gfp_mask | __GFP_ZERO, 0); -} -EXPORT_SYMBOL(get_zeroed_page); -``` - - -[`__get_free_pages`](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883)调用`alloc_pages`完成内存分配, 而alloc_pages又借助于alloc_pages_node - -[`__get_free_pages`](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883)函数的定义在[mm/page_alloc.c?v=4.7, line 3883](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883) - -```cpp -// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883 -unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) -{ - struct page *page; - - /* - * __get_free_pages() returns a 32-bit address, which cannot represent - * a highmem page - */ - VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0); - - page = alloc_pages(gfp_mask, order); - if (!page) - return 0; - return (unsigned long) page_address(page); -} -EXPORT_SYMBOL(__get_free_pages); -``` - -在这种情况下, 使用了一个普通函数而不是宏, 因为`alloc_pages`返回的`page`实例需要使用辅助 - -函数`page_address`转换为内存地址. 在这里,只要知道该函数可根据`page`实例计算相关页的线性内存地址即可. 对高端内存页这是有问题的 - - - -这样, 就完成了所有分配内存的API函数到公共的基础函数`alloc_pages`的统一 - - -![伙伴系统中各个分配函数之间的关系](../images/alloc_pages.png) - - -另外所有体系结构都必须实现的标准函数`clear_page`, 可帮助alloc_pages对页填充字节0, 实现如下表所示 - -| x86 | arm | -|:----:|:-----:| -| [arch/x86/include/asm/page_32.h?v=4.7, line 24](http://lxr.free-electrons.com/source/arch/x86/include/asm/page_32.h?v=4.7#L24) | [arch/arm/include/asm/page.h?v=4.7#L14](http://lxr.free-electrons.com/source/arch/arm/include/asm/page.h?v=4.7#L142)
[arch/arm/include/asm/page-nommu.h](http://lxr.free-electrons.com/source/arch/arm/include/asm/page-nommu.h?v=4.7#L20) | - - -##2.2 alloc_pages函数分配页 -------- - - -既然所有的内存分配API函数都可以追溯掉`alloc_page`函数, 从某种意义上说,该函数是伙伴系统主要实现的"发射台". - - -`alloc_pages`函数的定义是依赖于NUMA或者UMA架构的, 定义如下 - - -```cpp -#ifdef CONFIG_NUMA - -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L465 -static inline struct page * -alloc_pages(gfp_t gfp_mask, unsigned int order) -{ - return alloc_pages_current(gfp_mask, order); -} - -#else - -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L476 -#define alloc_pages(gfp_mask, order) \ - alloc_pages_node(numa_node_id(), gfp_mask, order) -#endif -``` - - -UMA结构下的`alloc_pages`是通过`alloc_pages_node`函数实现的, 下面我们看看`alloc_pages_node`函数的定义, 在[include/linux/gfp.h?v=4.7, line 448](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L448) - - -```cpp -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L448 -/* - * Allocate pages, preferring the node given as nid. When nid == NUMA_NO_NODE, - * prefer the current CPU's closest node. Otherwise node must be valid and - * online. - */ -static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask, - unsigned int order) -{ - if (nid == NUMA_NO_NODE) - nid = numa_mem_id(); - - return __alloc_pages_node(nid, gfp_mask, order); -} -``` - -它只是执行了一个简单的检查, 如果指定负的结点ID(不存在, 即[NUMA_NO_NODE = -1](http://lxr.free-electrons.com/source/include/linux/numa.h?v=4.7#L13)), 内核自动地使用当前执行CPU对应的结点nid = [numa_mem_id();](http://lxr.free-electrons.com/source/include/linux/topology.h?v=4.7#L137), 然后调用`__alloc_pages_node`函数进行了内存分配 - - - -`__alloc_pages_node`函数定义在[include/linux/gfp.h?v=4.7, line 435)](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L435), 如下所示 - -```cpp -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L435 -/* - * Allocate pages, preferring the node given as nid. The node must be valid and - * online. For more general interface, see alloc_pages_node(). - */ -static inline struct page * -__alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order) -{ - VM_BUG_ON(nid < 0 || nid >= MAX_NUMNODES); - VM_WARN_ON(!node_online(nid)); - - return __alloc_pages(gfp_mask, order, node_zonelist(nid, gfp_mask)); -} -``` - -内核假定传递给改alloc_pages_node函数的结点nid是被激活, 即online的.但是为了安全它还是检查并警告内存结点不存在的情况. 接下来的工作委托给__alloc_pages, 只需传递一组适当的参数, 其中包括节点nid的备用内存域列表zonelist. - - -现在`__alloc_pages`函数没什么特别的, 它直接将自己的所有信息传递给`__alloc_pages_nodemask`来完成内存的分配 - -```cpp -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L428 -static inline struct page * -__alloc_pages(gfp_t gfp_mask, unsigned int order, - struct zonelist *zonelist) -{ - return __alloc_pages_nodemask(gfp_mask, order, zonelist, NULL); -} -``` - -##2.3 伙伴系统的心脏__alloc_pages_nodemask -------- - -内核源代码将`__alloc_pages_nodemask`称之为"伙伴系统的心脏"(`the 'heart' of the zoned buddy allocator``), 因为它处理的是实质性的内存分配. - -由于"心脏"的重要性, 我将在下文详细介绍该函数. - - - -`__alloc_pages_nodemask`函数定义在[include/linux/gfp.h?v=4.7#L428](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L428) - - - -#3 选择页 -------- - -我们先把注意力转向页面选择是如何工作的。 - -##3.1 内存水印标志 -------- - -还记得之前讲过的内存水印么 - -```cpp -enum zone_watermarks { - WMARK_MIN, - WMARK_LOW, - WMARK_HIGH, - NR_WMARK -}; - -#define min_wmark_pages(z) (z->watermark[WMARK_MIN]) -#define low_wmark_pages(z) (z->watermark[WMARK_LOW]) -#define high_wmark_pages(z) (z->watermark[WMARK_HIGH]) -```` - - - -内核需要定义一些函数使用的标志,用于控制到达各个水印指定的临界状态时的行为, 这些标志用宏来定义, 定义在[mm/internal.h?v=4.7, line 453](http://lxr.free-electrons.com/source/mm/internal.h?v=4.7#L453) - -```cpp -/* The ALLOC_WMARK bits are used as an index to zone->watermark */ -#define ALLOC_WMARK_MIN WMARK_MIN /* 1 = 0x01, 使用pages_min水印 */ -#define ALLOC_WMARK_LOW WMARK_LOW /* 2 = 0x02, 使用pages_low水印 */ -#define ALLOC_WMARK_HIGH WMARK_HIGH /* 3 = 0x03, 使用pages_high水印 */ -#define ALLOC_NO_WATERMARKS 0x04 /* don't check watermarks at all 完全不检查水印 */ - -/* Mask to get the watermark bits */ -#define ALLOC_WMARK_MASK (ALLOC_NO_WATERMARKS-1) - -#define ALLOC_HARDER 0x10 /* try to alloc harder, 试图更努力地分配, 即放宽限制 */ -#define ALLOC_HIGH 0x20 /* __GFP_HIGH set, 设置了__GFP_HIGH */ -#define ALLOC_CPUSET 0x40 /* check for correct cpuset, 检查内存结点是否对应着指定的CPU集合 */ -#define ALLOC_CMA 0x80 /* allow allocations from CMA areas */ -#define ALLOC_FAIR 0x100 /* fair zone allocation */ -``` - -前几个标志(`ALLOC_WMARK_MIN`, `ALLOC_WMARK_LOW`, `ALLOC_WMARK_HIGH`, `ALLOC_NO_WATERMARKS`)表示在判断页是否可分配时, 需要考虑哪些水印. 默认情况下(即没有因其他因素带来的压力而需要更多的内存), 只有内存域包含页的数目至少为zone->pages_high时, 才能分配页.这对应于`ALLOC_WMARK_HIGH`标志. 如果要使用较低(zone->pages_low)或最低(zone->pages_min)设置, 则必须相应地设置`ALLOC_WMARK_MIN`或`ALLOC_WMARK_LOW`. 而`ALLOC_NO_WATERMARKS`则通知内核在进行内存分配时不要考虑内存水印. - - -`ALLOC_HARDER`通知伙伴系统在急需内存时放宽分配规则. 在分配高端内存域的内存时, `ALLOC_HIGH`进一步放宽限制. - -`ALLOC_CPUSET`告知内核, 内存只能从当前进程允许运行的CPU相关联的内存结点分配, 当然该选项只对NUMA系统有意义. - -`ALLOC_CMA`通知伙伴系统从CMD区域中分配内存 - -最后, `ALLOC_FAIR`则希望内核公平(均匀)的从内存域zone中进行内存分配 - - - -##3.2 `zone_watermark_ok`函数检查标志 -------- - -设置的标志在`zone_watermark_ok`函数中检查, 该函数根据设置的标志判断是否能从给定的内存域分配内存. 该函数定义在[mm/page_alloc.c?v=4.7, line 2820](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L2820) - -```cpp -// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L2820 -bool zone_watermark_ok(struct zone *z, unsigned int order, unsigned long mark, - int classzone_idx, unsigned int alloc_flags) -{ - return __zone_watermark_ok(z, order, mark, classzone_idx, alloc_flags, - zone_page_state(z, NR_FREE_PAGES)); -} -``` - -而`__zone_watermark_ok`函数则完成了检查的工作, 该函数定义在[mm/page_alloc.c?v=4.7, line 2752](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L2752) - -```cpp -// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L2752 -/* - * Return true if free base pages are above 'mark'. For high-order checks it - * will return true of the order-0 watermark is reached and there is at least - * one free page of a suitable size. Checking now avoids taking the zone lock - * to check in the allocation paths if no pages are free. - */ -bool __zone_watermark_ok(struct zone *z, unsigned int order, unsigned long mark, - int classzone_idx, unsigned int alloc_flags, - long free_pages) -{ - long min = mark; - int o; - const bool alloc_harder = (alloc_flags & ALLOC_HARDER); - - /* free_pages may go negative - that's OK - * free_pages可能变为负值, 没有关系 */ - free_pages -= (1 << order) - 1; - - if (alloc_flags & ALLOC_HIGH) - min -= min / 2; - - /* - * If the caller does not have rights to ALLOC_HARDER then subtract - * the high-atomic reserves. This will over-estimate the size of the - * atomic reserve but it avoids a search. - */ - if (likely(!alloc_harder)) - free_pages -= z->nr_reserved_highatomic; - else - min -= min / 4; - -#ifdef CONFIG_CMA - /* If allocation can't use CMA areas don't use free CMA pages */ - if (!(alloc_flags & ALLOC_CMA)) - free_pages -= zone_page_state(z, NR_FREE_CMA_PAGES); -#endif - - /* - * Check watermarks for an order-0 allocation request. If these - * are not met, then a high-order request also cannot go ahead - * even if a suitable page happened to be free. - */ - if (free_pages <= min + z->lowmem_reserve[classzone_idx]) - return false; - - /* If this is an order-0 request then the watermark is fine */ - if (!order) - return true; - - /* For a high-order request, check at least one suitable page is free - * 在下一阶,当前阶的页是不可用的 */ - for (o = order; o < MAX_ORDER; o++) { - struct free_area *area = &z->free_area[o]; - int mt; - - if (!area->nr_free) - continue; - - if (alloc_harder) - return true; - - /* 所需高阶空闲页的数目相对较少 */ - for (mt = 0; mt < MIGRATE_PCPTYPES; mt++) { - if (!list_empty(&area->free_list[mt])) - return true; - } - -#ifdef CONFIG_CMA - if ((alloc_flags & ALLOC_CMA) && - !list_empty(&area->free_list[MIGRATE_CMA])) { - return true; - } -#endif - } - return false; -} -``` - - -我们知道[`zone_per_state`](http://lxr.free-electrons.com/source/include/linux/vmstat.h?v=4.7#L130)用来访问每个内存域的统计量. 在上述代码中, 得到的是空闲页的数目. - -```cpp -free_pages -= zone_page_state(z, NR_FREE_CMA_PAGES); -``` - -在解释了`ALLOC_HIGH`和`ALLOC_HARDER`标志之后(将最小值标记降低到当前值的一半或四分之一,使得分配过程努力或更加努力), -```cpp -if (alloc_flags & ALLOC_HIGH) - min -= min / 2; - -if (likely(!alloc_harder)) - free_pages -= z->nr_reserved_highatomic; -else - min -= min / 4; -``` - - -该函数会检查空闲页的数目`free_pages`是否小于最小值与[`lowmem_reserve`](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L341)中指定的紧急分配值`min`之和. - -```cpp -if (free_pages <= min + z->lowmem_reserve[classzone_idx]) - return false; -``` - -如果不小于, 则代码遍历所有小于当前阶的分配阶, 其中nr_free记载的是当前分配阶的空闲页块数目. - -```cpp -/* For a high-order request, check at least one suitable page is free */ -for (o = order; o < MAX_ORDER; o++) { - struct free_area *area = &z->free_area[o]; - int mt; - - if (!area->nr_free) - continue; - - if (alloc_harder) - return true; - - for (mt = 0; mt < MIGRATE_PCPTYPES; mt++) { - if (!list_empty(&area->free_list[mt])) - return true; - } - -#ifdef CONFIG_CMA - if ((alloc_flags & ALLOC_CMA) && - !list_empty(&area->free_list[MIGRATE_CMA])) { - return true; - } -#endif -} -``` - -如果内核遍历所有的低端内存域之后,发现内存不足, 则不进行内存分配. - - - - -##3.3 get_page_from_freelist函数 -------- - -http://blog.csdn.net/yuzhihui_no1/article/details/50776826 -http://bbs.chinaunix.net/thread-3769001-1-1.html - -`get_page_from_freelist`是伙伴系统使用的另一个重要的辅助函数. 它通过标志集和分配阶来判断是否能进行分配。如果可以,则发起实际的分配操作. 该函数定义在[mm/page_alloc.c?v=4.7, line 2905](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L2905) - -这个函数的参数很有意思, 之前的时候这个函数的参数只能用复杂来形容 - - -```cpp -static struct page * -get_page_from_freelist(gfp_t gfp_mask, nodemask_t *nodemask, unsigned int order, - struct zonelist *zonelist, int high_zoneidx, int alloc_flags, - struct zone *preferred_zone, int migratetype) -``` - -但是这仍然不够, 随着内核的不段改进, 所支持的特性也越多, 分配内存时需要参照的标识也越来越多, 那难道看着这个函数的参数不断膨胀么, 这个不是内核黑客们所能容忍的, 于是大家想出了一个解决方案, 把那些相关联的参数封装成一个结构 - -```cpp -static struct page * -get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags, const struct alloc_context *ac) -``` - - -这个封装好的结构就是[`struct alloc_context`](http://lxr.free-electrons.com/source/mm/internal.h?v=4.7#L103), 定义在[mm/internal.h?v=4.7, line 103](http://lxr.free-electrons.com/source/mm/internal.h?v=4.7#L103) - -```cpp -/* - * Structure for holding the mostly immutable allocation parameters passed - * between functions involved in allocations, including the alloc_pages* - * family of functions. - * - * nodemask, migratetype and high_zoneidx are initialized only once in - * __alloc_pages_nodemask() and then never change. - * - * zonelist, preferred_zone and classzone_idx are set first in - * __alloc_pages_nodemask() for the fast path, and might be later changed - * in __alloc_pages_slowpath(). All other functions pass the whole strucure - * by a const pointer. - */ -struct alloc_context { - struct zonelist *zonelist; - nodemask_t *nodemask; - struct zoneref *preferred_zoneref; - int migratetype; - enum zone_type high_zoneidx; - bool spread_dirty_pages; -}; -``` - - - -| 字段 | 描述 | -|:-----:|:-----:| -| zonelist | 当perferred_zone上没有合适的页可以分配时,就要按zonelist中的顺序扫描该zonelist中备用zone列表,一个个的试用 | -| nodemask | 表示节点的mask,就是是否能在该节点上分配内存,这是个bit位数组 | -| preferred_zone | 表示从high_zoneidx后找到的合适的zone,一般会从该zone分配;分配失败的话,就会在zonelist再找一个preferred_zone = 合适的zone | -| migratetype | 迁移类型,在zone->free_area.free_list[XXX] 作为分配下标使用,这个是用来反碎片化的,修改了以前的free_area结构体,在该结构体中再添加了一个数组,该数组以迁移类型为下标,每个数组元素都挂了对应迁移类型的页链表 | -| high_zoneidx | 是表示该分配时,所能分配的最高zone,一般从high-->normal-->dma 内存越来越昂贵,所以一般从high到dma分配依次分配 | -| spread_dirty_pages | | - - -zonelist是指向备用列表的指针. 在预期内存域没有空闲空间的情况下, 该列表确定了扫描系统其他内存域(和结点)的顺序. - -随后的for循环所作的基本上与直觉一致, 遍历备用列表的所有内存域,用最简单的方式查找一个适当的空闲内存块 - -* 首先,解释ALLOC_*标志(\__cpuset_zone_allowed_softwall是另一个辅助函数, 用于检查给定内存域是否属于该进程允许运行的CPU). - -* zone_watermark_ok接下来检查所遍历到的内存域是否有足够的空闲页,并试图分配一个连续内存块。如果两个条件之一不能满足,即或者没有足够的空闲页,或者没有连续内存块可满足分配请求,则循环进行到备用列表中的下一个内存域,作同样的检查. 直到找到一个合适的页面, 在进行try_this_node进行内存分配 - -* 如果内存域适用于当前的分配请求, 那么buffered_rmqueue试图从中分配所需数目的页 - -```cpp -/* - * get_page_from_freelist goes through the zonelist trying to allocate - * a page. - */ -static struct page * -get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags, const struct alloc_context *ac) -{ - struct zoneref *z = ac->preferred_zoneref; - struct zone *zone; - bool fair_skipped = false; - bool apply_fair = (alloc_flags & ALLOC_FAIR); - -zonelist_scan: - /* - * Scan zonelist, looking for a zone with enough free. - * See also __cpuset_node_allowed() comment in kernel/cpuset.c. - */ - for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx, - ac->nodemask) { - struct page *page; - unsigned long mark; - - if (cpusets_enabled() && - (alloc_flags & ALLOC_CPUSET) && - !__cpuset_zone_allowed(zone, gfp_mask)) - continue; - /* - * Distribute pages in proportion to the individual - * zone size to ensure fair page aging. The zone a - * page was allocated in should have no effect on the - * time the page has in memory before being reclaimed. - */ - if (apply_fair) { - if (test_bit(ZONE_FAIR_DEPLETED, &zone->flags)) { - fair_skipped = true; - continue; - } - if (!zone_local(ac->preferred_zoneref->zone, zone)) { - if (fair_skipped) - goto reset_fair; - apply_fair = false; - } - } - /* - * When allocating a page cache page for writing, we - * want to get it from a zone that is within its dirty - * limit, such that no single zone holds more than its - * proportional share of globally allowed dirty pages. - * The dirty limits take into account the zone's - * lowmem reserves and high watermark so that kswapd - * should be able to balance it without having to - * write pages from its LRU list. - * - * This may look like it could increase pressure on - * lower zones by failing allocations in higher zones - * before they are full. But the pages that do spill - * over are limited as the lower zones are protected - * by this very same mechanism. It should not become - * a practical burden to them. - * - * XXX: For now, allow allocations to potentially - * exceed the per-zone dirty limit in the slowpath - * (spread_dirty_pages unset) before going into reclaim, - * which is important when on a NUMA setup the allowed - * zones are together not big enough to reach the - * global limit. The proper fix for these situations - * will require awareness of zones in the - * dirty-throttling and the flusher threads. - */ - if (ac->spread_dirty_pages && !zone_dirty_ok(zone)) - continue; - - mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK]; - if (!zone_watermark_fast(zone, order, mark, - ac_classzone_idx(ac), alloc_flags)) { - int ret; - - /* Checked here to keep the fast path fast */ - BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK); - if (alloc_flags & ALLOC_NO_WATERMARKS) - goto try_this_zone; - - if (zone_reclaim_mode == 0 || - !zone_allows_reclaim(ac->preferred_zoneref->zone, zone)) - continue; - - ret = zone_reclaim(zone, gfp_mask, order); - switch (ret) { - case ZONE_RECLAIM_NOSCAN: - /* did not scan */ - continue; - case ZONE_RECLAIM_FULL: - /* scanned but unreclaimable */ - continue; - default: - /* did we reclaim enough */ - if (zone_watermark_ok(zone, order, mark, - ac_classzone_idx(ac), alloc_flags)) - goto try_this_zone; - - continue; - } - } - -try_this_zone: - page = buffered_rmqueue(ac->preferred_zoneref->zone, zone, order, - gfp_mask, alloc_flags, ac->migratetype); - if (page) { - prep_new_page(page, order, gfp_mask, alloc_flags); - - /* - * If this is a high-order atomic allocation then check - * if the pageblock should be reserved for the future - */ - if (unlikely(order && (alloc_flags & ALLOC_HARDER))) - reserve_highatomic_pageblock(page, zone, order); - - return page; - } - } - - /* - * The first pass makes sure allocations are spread fairly within the - * local node. However, the local node might have free pages left - * after the fairness batches are exhausted, and remote zones haven't - * even been considered yet. Try once more without fairness, and - * include remote zones now, before entering the slowpath and waking - * kswapd: prefer spilling to a remote zone over swapping locally. - */ - if (fair_skipped) { -reset_fair: - apply_fair = false; - fair_skipped = false; - reset_alloc_batches(ac->preferred_zoneref->zone); - z = ac->preferred_zoneref; - goto zonelist_scan; - } - - return NULL; -} -``` - - -#4 分配控制 -------- - -如前所述, `__alloc_pages_nodemask`是伙伴系统的心脏. 我们已经处理了所有的准备工作并描述了所有可能的标志, 现在我们把注意力转向相对复杂的部分 : 函数`__alloc_pages_nodemask`的实现, 这也是内核中比较冗长的部分 -之一. 特别是在可用内存太少或逐渐用完时, 函数就会比较复杂. 如果可用内存足够,则必要的工作会很快完成,就像下述代码 - -##4.1 函数源代码注释 -------- - -`__alloc_pages_nodemask`函数定义在[include/linux/gfp.h?v=4.7#L428](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L428) - - - - -```cpp -// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3779 -/* - * This is the 'heart' of the zoned buddy allocator. - */ -struct page * -__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, - struct zonelist *zonelist, nodemask_t *nodemask) -{ - struct page *page; - unsigned int cpuset_mems_cookie; - unsigned int alloc_flags = ALLOC_WMARK_LOW|ALLOC_FAIR; - gfp_t alloc_mask = gfp_mask; /* The gfp_t that was actually used for allocation */ - struct alloc_context ac = { - .high_zoneidx = gfp_zone(gfp_mask), - .zonelist = zonelist, - .nodemask = nodemask, - .migratetype = gfpflags_to_migratetype(gfp_mask), - }; - - if (cpusets_enabled()) { - alloc_mask |= __GFP_HARDWALL; - alloc_flags |= ALLOC_CPUSET; - if (!ac.nodemask) - ac.nodemask = &cpuset_current_mems_allowed; - } - - gfp_mask &= gfp_allowed_mask; - - lockdep_trace_alloc(gfp_mask); - - might_sleep_if(gfp_mask & __GFP_DIRECT_RECLAIM); - - if (should_fail_alloc_page(gfp_mask, order)) - return NULL; - - /* - * Check the zones suitable for the gfp_mask contain at least one - * valid zone. It's possible to have an empty zonelist as a result - * of __GFP_THISNODE and a memoryless node - */ - if (unlikely(!zonelist->_zonerefs->zone)) - return NULL; - - if (IS_ENABLED(CONFIG_CMA) && ac.migratetype == MIGRATE_MOVABLE) - alloc_flags |= ALLOC_CMA; - -retry_cpuset: - cpuset_mems_cookie = read_mems_allowed_begin(); - - /* Dirty zone balancing only done in the fast path */ - ac.spread_dirty_pages = (gfp_mask & __GFP_WRITE); - - /* - * The preferred zone is used for statistics but crucially it is - * also used as the starting point for the zonelist iterator. It - * may get reset for allocations that ignore memory policies. - */ - ac.preferred_zoneref = first_zones_zonelist(ac.zonelist, - ac.high_zoneidx, ac.nodemask); - if (!ac.preferred_zoneref) { - page = NULL; - goto no_zone; - } - - /* First allocation attempt */ - page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac); - if (likely(page)) - goto out; - - /* - * Runtime PM, block IO and its error handling path can deadlock - * because I/O on the device might not complete. - */ - alloc_mask = memalloc_noio_flags(gfp_mask); - ac.spread_dirty_pages = false; - - /* - * Restore the original nodemask if it was potentially replaced with - * &cpuset_current_mems_allowed to optimize the fast-path attempt. - */ - if (cpusets_enabled()) - ac.nodemask = nodemask; - page = __alloc_pages_slowpath(alloc_mask, order, &ac); - -no_zone: - /* - * When updating a task's mems_allowed, it is possible to race with - * parallel threads in such a way that an allocation can fail while - * the mask is being updated. If a page allocation is about to fail, - * check if the cpuset changed during allocation and if so, retry. - */ - if (unlikely(!page && read_mems_allowed_retry(cpuset_mems_cookie))) { - alloc_mask = gfp_mask; - goto retry_cpuset; - } - -out: - if (kmemcheck_enabled && page) - kmemcheck_pagealloc_alloc(page, order, gfp_mask); - - trace_mm_page_alloc(page, order, alloc_mask, ac.migratetype); - - return page; -} -EXPORT_SYMBOL(__alloc_pages_nodemask); -``` - -最简单的情形中, 分配空闲内存区只涉及调用一次`get_page_from_freelist`, 然后返回所需数目的页(由标号got_pg处的代码处理). - +服务器体系与共享存储器架构 +======= + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + + + + +在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法. + +Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述. + +伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者. + +* 内核如何记住哪些内存块是空闲的 + +* 分配空闲页面的方法 + +* 影响分配器行为的众多标识位 + +* 内存碎片的问题和分配器如何处理碎片 + + + +#内存分配API +------- + +##2.1 内存分配器API +------- + + + +就伙伴系统的接口而言, NUMA或UMA体系结构是没有差别的, 二者的调用语法都是相同的. + +所有函数的一个共同点是 : 只能分配2的整数幂个页. + +因此,接口中不像C标准库的malloc函数或bootmem和memblock分配器那样指定了所需内存大小作为参数. 相反, 必须指定的是分配阶, 伙伴系统将在内存中分配$2^order$页. 内核中细粒度的分配只能借助于slab分配器(或者slub、slob分配器), 后者基于伙伴系统 + + +| 内存分配函数 | 功能 | 定义 | +|:-----:|:-----:| +| alloc_pages(mask, order) | 分配$2^order$页并返回一个struct page的实例,表示分配的内存块的起始页 | [NUMA-include/linux/gfp.h, line 466](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L466)
[UMA-include/linux/gfp.h?v=4.7, line 476](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L476) | +| alloc_page(mask) | 是前者在order = 0情况下的简化形式,只分配一页 | [include/linux/gfp.h?v=4.7, line 483](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L483) | +| get_zeroed_page(mask) | 分配一页并返回一个page实例,页对应的内存填充0(所有其他函数,分配之后页的内容是未定义的) | [mm/page_alloc.c?v=4.7, line 3900](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3900)| | +| [__get_free_pages(mask, order)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883)
[__get_free_page(mask)](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L500) | 工作方式与上述函数相同,但返回分配内存块的虚拟地址,而不是page实例 | +| get_dma_pages(gfp_mask, order) | 用来获得适用于DMA的页. | [include/linux/gfp.h?v=4.7, line 503](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L503) | + + +在空闲内存无法满足请求以至于分配失败的情况下,所有上述函数都返回空指针(比如alloc_pages和alloc_page)或者0(比如get_zeroed_page、__get_free_pages和__get_free_page). + +因此内核在各次分配之后都必须检查返回的结果. 这种惯例与设计得很好的用户层应用程序没什么不同, 但在内核中忽略检查会导致严重得多的故障 + + +内核除了伙伴系统函数之外, 还提供了其他内存管理函数. 它们以伙伴系统为基础, 但并不属于伙伴分配器自身. 这些函数包括vmalloc和vmalloc_32, 使用页表将不连续的内存映射到内核地址空间中, 使之看上去是连续的. + +还有一组kmalloc类型的函数, 用于分配小于一整页的内存区. 其实现将在以后分别讨论。 + + + +##2.2 内存分配API统一到alloc_pages接口 +------- + + +通过使用标志、内存域修饰符和各个分配函数,内核提供了一种非常灵活的内存分配体系.尽管如此, 所有接口函数都可以追溯到一个简单的基本函数(alloc_pages_node) + +分配单页的函数[`alloc_page`](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L483)和[`__get_free_page`](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L500), 还有[`__get_dma_pages`](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L503)是借助于宏定义的. + +```cpp +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L483 +#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0) + +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L500 +#define __get_free_page(gfp_mask) \ + __get_free_pages((gfp_mask), 0)` + +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L503 +#define __get_dma_pages(gfp_mask, order) \ + __get_free_pages((gfp_mask) | GFP_DMA, (order)) +``` + +[`get_zeroed_page`](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3900)的实现也没什么困难, 对`__get_free_pages`使用`__GFP_ZERO`标志,即可分配填充字节0的页. 再返回与页关联的内存区地址即可. + + +```cpp +// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3900 +unsigned long get_zeroed_page(gfp_t gfp_mask) +{ + return __get_free_pages(gfp_mask | __GFP_ZERO, 0); +} +EXPORT_SYMBOL(get_zeroed_page); +``` + + +[`__get_free_pages`](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883)调用`alloc_pages`完成内存分配, 而alloc_pages又借助于alloc_pages_node + +[`__get_free_pages`](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883)函数的定义在[mm/page_alloc.c?v=4.7, line 3883](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883) + +```cpp +// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883 +unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) +{ + struct page *page; + + /* + * __get_free_pages() returns a 32-bit address, which cannot represent + * a highmem page + */ + VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0); + + page = alloc_pages(gfp_mask, order); + if (!page) + return 0; + return (unsigned long) page_address(page); +} +EXPORT_SYMBOL(__get_free_pages); +``` + +在这种情况下, 使用了一个普通函数而不是宏, 因为`alloc_pages`返回的`page`实例需要使用辅助 + +函数`page_address`转换为内存地址. 在这里,只要知道该函数可根据`page`实例计算相关页的线性内存地址即可. 对高端内存页这是有问题的 + + + +这样, 就完成了所有分配内存的API函数到公共的基础函数`alloc_pages`的统一 + + +![伙伴系统中各个分配函数之间的关系](../images/alloc_pages.png) + + +另外所有体系结构都必须实现的标准函数`clear_page`, 可帮助alloc_pages对页填充字节0, 实现如下表所示 + +| x86 | arm | +|:----:|:-----:| +| [arch/x86/include/asm/page_32.h?v=4.7, line 24](http://lxr.free-electrons.com/source/arch/x86/include/asm/page_32.h?v=4.7#L24) | [arch/arm/include/asm/page.h?v=4.7#L14](http://lxr.free-electrons.com/source/arch/arm/include/asm/page.h?v=4.7#L142)
[arch/arm/include/asm/page-nommu.h](http://lxr.free-electrons.com/source/arch/arm/include/asm/page-nommu.h?v=4.7#L20) | + + +##2.2 alloc_pages函数分配页 +------- + + +既然所有的内存分配API函数都可以追溯掉`alloc_page`函数, 从某种意义上说,该函数是伙伴系统主要实现的"发射台". + + +`alloc_pages`函数的定义是依赖于NUMA或者UMA架构的, 定义如下 + + +```cpp +#ifdef CONFIG_NUMA + +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L465 +static inline struct page * +alloc_pages(gfp_t gfp_mask, unsigned int order) +{ + return alloc_pages_current(gfp_mask, order); +} + +#else + +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L476 +#define alloc_pages(gfp_mask, order) \ + alloc_pages_node(numa_node_id(), gfp_mask, order) +#endif +``` + + +UMA结构下的`alloc_pages`是通过`alloc_pages_node`函数实现的, 下面我们看看`alloc_pages_node`函数的定义, 在[include/linux/gfp.h?v=4.7, line 448](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L448) + + +```cpp +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L448 +/* + * Allocate pages, preferring the node given as nid. When nid == NUMA_NO_NODE, + * prefer the current CPU's closest node. Otherwise node must be valid and + * online. + */ +static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask, + unsigned int order) +{ + if (nid == NUMA_NO_NODE) + nid = numa_mem_id(); + + return __alloc_pages_node(nid, gfp_mask, order); +} +``` + +它只是执行了一个简单的检查, 如果指定负的结点ID(不存在, 即[NUMA_NO_NODE = -1](http://lxr.free-electrons.com/source/include/linux/numa.h?v=4.7#L13)), 内核自动地使用当前执行CPU对应的结点nid = [numa_mem_id();](http://lxr.free-electrons.com/source/include/linux/topology.h?v=4.7#L137), 然后调用`__alloc_pages_node`函数进行了内存分配 + + + +`__alloc_pages_node`函数定义在[include/linux/gfp.h?v=4.7, line 435)](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L435), 如下所示 + +```cpp +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L435 +/* + * Allocate pages, preferring the node given as nid. The node must be valid and + * online. For more general interface, see alloc_pages_node(). + */ +static inline struct page * +__alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order) +{ + VM_BUG_ON(nid < 0 || nid >= MAX_NUMNODES); + VM_WARN_ON(!node_online(nid)); + + return __alloc_pages(gfp_mask, order, node_zonelist(nid, gfp_mask)); +} +``` + +内核假定传递给改alloc_pages_node函数的结点nid是被激活, 即online的.但是为了安全它还是检查并警告内存结点不存在的情况. 接下来的工作委托给__alloc_pages, 只需传递一组适当的参数, 其中包括节点nid的备用内存域列表zonelist. + + +现在`__alloc_pages`函数没什么特别的, 它直接将自己的所有信息传递给`__alloc_pages_nodemask`来完成内存的分配 + +```cpp +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L428 +static inline struct page * +__alloc_pages(gfp_t gfp_mask, unsigned int order, + struct zonelist *zonelist) +{ + return __alloc_pages_nodemask(gfp_mask, order, zonelist, NULL); +} +``` + +##2.3 伙伴系统的心脏__alloc_pages_nodemask +------- + +内核源代码将`__alloc_pages_nodemask`称之为"伙伴系统的心脏"(`the 'heart' of the zoned buddy allocator``), 因为它处理的是实质性的内存分配. + +由于"心脏"的重要性, 我将在下文详细介绍该函数. + + + +`__alloc_pages_nodemask`函数定义在[include/linux/gfp.h?v=4.7#L428](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L428) + + + +#3 选择页 +------- + +我们先把注意力转向页面选择是如何工作的。 + +##3.1 内存水印标志 +------- + +还记得之前讲过的内存水印么 + +```cpp +enum zone_watermarks { + WMARK_MIN, + WMARK_LOW, + WMARK_HIGH, + NR_WMARK +}; + +#define min_wmark_pages(z) (z->watermark[WMARK_MIN]) +#define low_wmark_pages(z) (z->watermark[WMARK_LOW]) +#define high_wmark_pages(z) (z->watermark[WMARK_HIGH]) +```` + + + +内核需要定义一些函数使用的标志,用于控制到达各个水印指定的临界状态时的行为, 这些标志用宏来定义, 定义在[mm/internal.h?v=4.7, line 453](http://lxr.free-electrons.com/source/mm/internal.h?v=4.7#L453) + +```cpp +/* The ALLOC_WMARK bits are used as an index to zone->watermark */ +#define ALLOC_WMARK_MIN WMARK_MIN /* 1 = 0x01, 使用pages_min水印 */ +#define ALLOC_WMARK_LOW WMARK_LOW /* 2 = 0x02, 使用pages_low水印 */ +#define ALLOC_WMARK_HIGH WMARK_HIGH /* 3 = 0x03, 使用pages_high水印 */ +#define ALLOC_NO_WATERMARKS 0x04 /* don't check watermarks at all 完全不检查水印 */ + +/* Mask to get the watermark bits */ +#define ALLOC_WMARK_MASK (ALLOC_NO_WATERMARKS-1) + +#define ALLOC_HARDER 0x10 /* try to alloc harder, 试图更努力地分配, 即放宽限制 */ +#define ALLOC_HIGH 0x20 /* __GFP_HIGH set, 设置了__GFP_HIGH */ +#define ALLOC_CPUSET 0x40 /* check for correct cpuset, 检查内存结点是否对应着指定的CPU集合 */ +#define ALLOC_CMA 0x80 /* allow allocations from CMA areas */ +#define ALLOC_FAIR 0x100 /* fair zone allocation */ +``` + +前几个标志(`ALLOC_WMARK_MIN`, `ALLOC_WMARK_LOW`, `ALLOC_WMARK_HIGH`, `ALLOC_NO_WATERMARKS`)表示在判断页是否可分配时, 需要考虑哪些水印. 默认情况下(即没有因其他因素带来的压力而需要更多的内存), 只有内存域包含页的数目至少为zone->pages_high时, 才能分配页.这对应于`ALLOC_WMARK_HIGH`标志. 如果要使用较低(zone->pages_low)或最低(zone->pages_min)设置, 则必须相应地设置`ALLOC_WMARK_MIN`或`ALLOC_WMARK_LOW`. 而`ALLOC_NO_WATERMARKS`则通知内核在进行内存分配时不要考虑内存水印. + + +`ALLOC_HARDER`通知伙伴系统在急需内存时放宽分配规则. 在分配高端内存域的内存时, `ALLOC_HIGH`进一步放宽限制. + +`ALLOC_CPUSET`告知内核, 内存只能从当前进程允许运行的CPU相关联的内存结点分配, 当然该选项只对NUMA系统有意义. + +`ALLOC_CMA`通知伙伴系统从CMD区域中分配内存 + +最后, `ALLOC_FAIR`则希望内核公平(均匀)的从内存域zone中进行内存分配 + + + +##3.2 `zone_watermark_ok`函数检查标志 +------- + +设置的标志在`zone_watermark_ok`函数中检查, 该函数根据设置的标志判断是否能从给定的内存域分配内存. 该函数定义在[mm/page_alloc.c?v=4.7, line 2820](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L2820) + +```cpp +// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L2820 +bool zone_watermark_ok(struct zone *z, unsigned int order, unsigned long mark, + int classzone_idx, unsigned int alloc_flags) +{ + return __zone_watermark_ok(z, order, mark, classzone_idx, alloc_flags, + zone_page_state(z, NR_FREE_PAGES)); +} +``` + +而`__zone_watermark_ok`函数则完成了检查的工作, 该函数定义在[mm/page_alloc.c?v=4.7, line 2752](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L2752) + +```cpp +// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L2752 +/* + * Return true if free base pages are above 'mark'. For high-order checks it + * will return true of the order-0 watermark is reached and there is at least + * one free page of a suitable size. Checking now avoids taking the zone lock + * to check in the allocation paths if no pages are free. + */ +bool __zone_watermark_ok(struct zone *z, unsigned int order, unsigned long mark, + int classzone_idx, unsigned int alloc_flags, + long free_pages) +{ + long min = mark; + int o; + const bool alloc_harder = (alloc_flags & ALLOC_HARDER); + + /* free_pages may go negative - that's OK + * free_pages可能变为负值, 没有关系 */ + free_pages -= (1 << order) - 1; + + if (alloc_flags & ALLOC_HIGH) + min -= min / 2; + + /* + * If the caller does not have rights to ALLOC_HARDER then subtract + * the high-atomic reserves. This will over-estimate the size of the + * atomic reserve but it avoids a search. + */ + if (likely(!alloc_harder)) + free_pages -= z->nr_reserved_highatomic; + else + min -= min / 4; + +#ifdef CONFIG_CMA + /* If allocation can't use CMA areas don't use free CMA pages */ + if (!(alloc_flags & ALLOC_CMA)) + free_pages -= zone_page_state(z, NR_FREE_CMA_PAGES); +#endif + + /* + * Check watermarks for an order-0 allocation request. If these + * are not met, then a high-order request also cannot go ahead + * even if a suitable page happened to be free. + */ + if (free_pages <= min + z->lowmem_reserve[classzone_idx]) + return false; + + /* If this is an order-0 request then the watermark is fine */ + if (!order) + return true; + + /* For a high-order request, check at least one suitable page is free + * 在下一阶,当前阶的页是不可用的 */ + for (o = order; o < MAX_ORDER; o++) { + struct free_area *area = &z->free_area[o]; + int mt; + + if (!area->nr_free) + continue; + + if (alloc_harder) + return true; + + /* 所需高阶空闲页的数目相对较少 */ + for (mt = 0; mt < MIGRATE_PCPTYPES; mt++) { + if (!list_empty(&area->free_list[mt])) + return true; + } + +#ifdef CONFIG_CMA + if ((alloc_flags & ALLOC_CMA) && + !list_empty(&area->free_list[MIGRATE_CMA])) { + return true; + } +#endif + } + return false; +} +``` + + +我们知道[`zone_per_state`](http://lxr.free-electrons.com/source/include/linux/vmstat.h?v=4.7#L130)用来访问每个内存域的统计量. 在上述代码中, 得到的是空闲页的数目. + +```cpp +free_pages -= zone_page_state(z, NR_FREE_CMA_PAGES); +``` + +在解释了`ALLOC_HIGH`和`ALLOC_HARDER`标志之后(将最小值标记降低到当前值的一半或四分之一,使得分配过程努力或更加努力), +```cpp +if (alloc_flags & ALLOC_HIGH) + min -= min / 2; + +if (likely(!alloc_harder)) + free_pages -= z->nr_reserved_highatomic; +else + min -= min / 4; +``` + + +该函数会检查空闲页的数目`free_pages`是否小于最小值与[`lowmem_reserve`](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L341)中指定的紧急分配值`min`之和. + +```cpp +if (free_pages <= min + z->lowmem_reserve[classzone_idx]) + return false; +``` + +如果不小于, 则代码遍历所有小于当前阶的分配阶, 其中nr_free记载的是当前分配阶的空闲页块数目. + +```cpp +/* For a high-order request, check at least one suitable page is free */ +for (o = order; o < MAX_ORDER; o++) { + struct free_area *area = &z->free_area[o]; + int mt; + + if (!area->nr_free) + continue; + + if (alloc_harder) + return true; + + for (mt = 0; mt < MIGRATE_PCPTYPES; mt++) { + if (!list_empty(&area->free_list[mt])) + return true; + } + +#ifdef CONFIG_CMA + if ((alloc_flags & ALLOC_CMA) && + !list_empty(&area->free_list[MIGRATE_CMA])) { + return true; + } +#endif +} +``` + +如果内核遍历所有的低端内存域之后,发现内存不足, 则不进行内存分配. + + + + +##3.3 get_page_from_freelist函数 +------- + +http://blog.csdn.net/yuzhihui_no1/article/details/50776826 +http://bbs.chinaunix.net/thread-3769001-1-1.html + +`get_page_from_freelist`是伙伴系统使用的另一个重要的辅助函数. 它通过标志集和分配阶来判断是否能进行分配。如果可以,则发起实际的分配操作. 该函数定义在[mm/page_alloc.c?v=4.7, line 2905](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L2905) + +这个函数的参数很有意思, 之前的时候这个函数的参数只能用复杂来形容 + + +```cpp +static struct page * +get_page_from_freelist(gfp_t gfp_mask, nodemask_t *nodemask, unsigned int order, + struct zonelist *zonelist, int high_zoneidx, int alloc_flags, + struct zone *preferred_zone, int migratetype) +``` + +但是这仍然不够, 随着内核的不段改进, 所支持的特性也越多, 分配内存时需要参照的标识也越来越多, 那难道看着这个函数的参数不断膨胀么, 这个不是内核黑客们所能容忍的, 于是大家想出了一个解决方案, 把那些相关联的参数封装成一个结构 + +```cpp +static struct page * +get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags, const struct alloc_context *ac) +``` + + +这个封装好的结构就是[`struct alloc_context`](http://lxr.free-electrons.com/source/mm/internal.h?v=4.7#L103), 定义在[mm/internal.h?v=4.7, line 103](http://lxr.free-electrons.com/source/mm/internal.h?v=4.7#L103) + +```cpp +/* + * Structure for holding the mostly immutable allocation parameters passed + * between functions involved in allocations, including the alloc_pages* + * family of functions. + * + * nodemask, migratetype and high_zoneidx are initialized only once in + * __alloc_pages_nodemask() and then never change. + * + * zonelist, preferred_zone and classzone_idx are set first in + * __alloc_pages_nodemask() for the fast path, and might be later changed + * in __alloc_pages_slowpath(). All other functions pass the whole strucure + * by a const pointer. + */ +struct alloc_context { + struct zonelist *zonelist; + nodemask_t *nodemask; + struct zoneref *preferred_zoneref; + int migratetype; + enum zone_type high_zoneidx; + bool spread_dirty_pages; +}; +``` + + + +| 字段 | 描述 | +|:-----:|:-----:| +| zonelist | 当perferred_zone上没有合适的页可以分配时,就要按zonelist中的顺序扫描该zonelist中备用zone列表,一个个的试用 | +| nodemask | 表示节点的mask,就是是否能在该节点上分配内存,这是个bit位数组 | +| preferred_zone | 表示从high_zoneidx后找到的合适的zone,一般会从该zone分配;分配失败的话,就会在zonelist再找一个preferred_zone = 合适的zone | +| migratetype | 迁移类型,在zone->free_area.free_list[XXX] 作为分配下标使用,这个是用来反碎片化的,修改了以前的free_area结构体,在该结构体中再添加了一个数组,该数组以迁移类型为下标,每个数组元素都挂了对应迁移类型的页链表 | +| high_zoneidx | 是表示该分配时,所能分配的最高zone,一般从high-->normal-->dma 内存越来越昂贵,所以一般从high到dma分配依次分配 | +| spread_dirty_pages | | + + +zonelist是指向备用列表的指针. 在预期内存域没有空闲空间的情况下, 该列表确定了扫描系统其他内存域(和结点)的顺序. + +随后的for循环所作的基本上与直觉一致, 遍历备用列表的所有内存域,用最简单的方式查找一个适当的空闲内存块 + +* 首先,解释ALLOC_*标志(\__cpuset_zone_allowed_softwall是另一个辅助函数, 用于检查给定内存域是否属于该进程允许运行的CPU). + +* zone_watermark_ok接下来检查所遍历到的内存域是否有足够的空闲页,并试图分配一个连续内存块。如果两个条件之一不能满足,即或者没有足够的空闲页,或者没有连续内存块可满足分配请求,则循环进行到备用列表中的下一个内存域,作同样的检查. 直到找到一个合适的页面, 在进行try_this_node进行内存分配 + +* 如果内存域适用于当前的分配请求, 那么buffered_rmqueue试图从中分配所需数目的页 + +```cpp +/* + * get_page_from_freelist goes through the zonelist trying to allocate + * a page. + */ +static struct page * +get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags, const struct alloc_context *ac) +{ + struct zoneref *z = ac->preferred_zoneref; + struct zone *zone; + bool fair_skipped = false; + bool apply_fair = (alloc_flags & ALLOC_FAIR); + +zonelist_scan: + /* + * Scan zonelist, looking for a zone with enough free. + * See also __cpuset_node_allowed() comment in kernel/cpuset.c. + */ + for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx, + ac->nodemask) { + struct page *page; + unsigned long mark; + + if (cpusets_enabled() && + (alloc_flags & ALLOC_CPUSET) && + !__cpuset_zone_allowed(zone, gfp_mask)) + continue; + /* + * Distribute pages in proportion to the individual + * zone size to ensure fair page aging. The zone a + * page was allocated in should have no effect on the + * time the page has in memory before being reclaimed. + */ + if (apply_fair) { + if (test_bit(ZONE_FAIR_DEPLETED, &zone->flags)) { + fair_skipped = true; + continue; + } + if (!zone_local(ac->preferred_zoneref->zone, zone)) { + if (fair_skipped) + goto reset_fair; + apply_fair = false; + } + } + /* + * When allocating a page cache page for writing, we + * want to get it from a zone that is within its dirty + * limit, such that no single zone holds more than its + * proportional share of globally allowed dirty pages. + * The dirty limits take into account the zone's + * lowmem reserves and high watermark so that kswapd + * should be able to balance it without having to + * write pages from its LRU list. + * + * This may look like it could increase pressure on + * lower zones by failing allocations in higher zones + * before they are full. But the pages that do spill + * over are limited as the lower zones are protected + * by this very same mechanism. It should not become + * a practical burden to them. + * + * XXX: For now, allow allocations to potentially + * exceed the per-zone dirty limit in the slowpath + * (spread_dirty_pages unset) before going into reclaim, + * which is important when on a NUMA setup the allowed + * zones are together not big enough to reach the + * global limit. The proper fix for these situations + * will require awareness of zones in the + * dirty-throttling and the flusher threads. + */ + if (ac->spread_dirty_pages && !zone_dirty_ok(zone)) + continue; + + mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK]; + if (!zone_watermark_fast(zone, order, mark, + ac_classzone_idx(ac), alloc_flags)) { + int ret; + + /* Checked here to keep the fast path fast */ + BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK); + if (alloc_flags & ALLOC_NO_WATERMARKS) + goto try_this_zone; + + if (zone_reclaim_mode == 0 || + !zone_allows_reclaim(ac->preferred_zoneref->zone, zone)) + continue; + + ret = zone_reclaim(zone, gfp_mask, order); + switch (ret) { + case ZONE_RECLAIM_NOSCAN: + /* did not scan */ + continue; + case ZONE_RECLAIM_FULL: + /* scanned but unreclaimable */ + continue; + default: + /* did we reclaim enough */ + if (zone_watermark_ok(zone, order, mark, + ac_classzone_idx(ac), alloc_flags)) + goto try_this_zone; + + continue; + } + } + +try_this_zone: + page = buffered_rmqueue(ac->preferred_zoneref->zone, zone, order, + gfp_mask, alloc_flags, ac->migratetype); + if (page) { + prep_new_page(page, order, gfp_mask, alloc_flags); + + /* + * If this is a high-order atomic allocation then check + * if the pageblock should be reserved for the future + */ + if (unlikely(order && (alloc_flags & ALLOC_HARDER))) + reserve_highatomic_pageblock(page, zone, order); + + return page; + } + } + + /* + * The first pass makes sure allocations are spread fairly within the + * local node. However, the local node might have free pages left + * after the fairness batches are exhausted, and remote zones haven't + * even been considered yet. Try once more without fairness, and + * include remote zones now, before entering the slowpath and waking + * kswapd: prefer spilling to a remote zone over swapping locally. + */ + if (fair_skipped) { +reset_fair: + apply_fair = false; + fair_skipped = false; + reset_alloc_batches(ac->preferred_zoneref->zone); + z = ac->preferred_zoneref; + goto zonelist_scan; + } + + return NULL; +} +``` + + +#4 分配控制 +------- + +如前所述, `__alloc_pages_nodemask`是伙伴系统的心脏. 我们已经处理了所有的准备工作并描述了所有可能的标志, 现在我们把注意力转向相对复杂的部分 : 函数`__alloc_pages_nodemask`的实现, 这也是内核中比较冗长的部分 +之一. 特别是在可用内存太少或逐渐用完时, 函数就会比较复杂. 如果可用内存足够,则必要的工作会很快完成,就像下述代码 + +##4.1 函数源代码注释 +------- + +`__alloc_pages_nodemask`函数定义在[include/linux/gfp.h?v=4.7#L428](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L428) + + + + +```cpp +// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3779 +/* + * This is the 'heart' of the zoned buddy allocator. + */ +struct page * +__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, + struct zonelist *zonelist, nodemask_t *nodemask) +{ + struct page *page; + unsigned int cpuset_mems_cookie; + unsigned int alloc_flags = ALLOC_WMARK_LOW|ALLOC_FAIR; + gfp_t alloc_mask = gfp_mask; /* The gfp_t that was actually used for allocation */ + struct alloc_context ac = { + .high_zoneidx = gfp_zone(gfp_mask), + .zonelist = zonelist, + .nodemask = nodemask, + .migratetype = gfpflags_to_migratetype(gfp_mask), + }; + + if (cpusets_enabled()) { + alloc_mask |= __GFP_HARDWALL; + alloc_flags |= ALLOC_CPUSET; + if (!ac.nodemask) + ac.nodemask = &cpuset_current_mems_allowed; + } + + gfp_mask &= gfp_allowed_mask; + + lockdep_trace_alloc(gfp_mask); + + might_sleep_if(gfp_mask & __GFP_DIRECT_RECLAIM); + + if (should_fail_alloc_page(gfp_mask, order)) + return NULL; + + /* + * Check the zones suitable for the gfp_mask contain at least one + * valid zone. It's possible to have an empty zonelist as a result + * of __GFP_THISNODE and a memoryless node + */ + if (unlikely(!zonelist->_zonerefs->zone)) + return NULL; + + if (IS_ENABLED(CONFIG_CMA) && ac.migratetype == MIGRATE_MOVABLE) + alloc_flags |= ALLOC_CMA; + +retry_cpuset: + cpuset_mems_cookie = read_mems_allowed_begin(); + + /* Dirty zone balancing only done in the fast path */ + ac.spread_dirty_pages = (gfp_mask & __GFP_WRITE); + + /* + * The preferred zone is used for statistics but crucially it is + * also used as the starting point for the zonelist iterator. It + * may get reset for allocations that ignore memory policies. + */ + ac.preferred_zoneref = first_zones_zonelist(ac.zonelist, + ac.high_zoneidx, ac.nodemask); + if (!ac.preferred_zoneref) { + page = NULL; + goto no_zone; + } + + /* First allocation attempt */ + page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac); + if (likely(page)) + goto out; + + /* + * Runtime PM, block IO and its error handling path can deadlock + * because I/O on the device might not complete. + */ + alloc_mask = memalloc_noio_flags(gfp_mask); + ac.spread_dirty_pages = false; + + /* + * Restore the original nodemask if it was potentially replaced with + * &cpuset_current_mems_allowed to optimize the fast-path attempt. + */ + if (cpusets_enabled()) + ac.nodemask = nodemask; + page = __alloc_pages_slowpath(alloc_mask, order, &ac); + +no_zone: + /* + * When updating a task's mems_allowed, it is possible to race with + * parallel threads in such a way that an allocation can fail while + * the mask is being updated. If a page allocation is about to fail, + * check if the cpuset changed during allocation and if so, retry. + */ + if (unlikely(!page && read_mems_allowed_retry(cpuset_mems_cookie))) { + alloc_mask = gfp_mask; + goto retry_cpuset; + } + +out: + if (kmemcheck_enabled && page) + kmemcheck_pagealloc_alloc(page, order, gfp_mask); + + trace_mm_page_alloc(page, order, alloc_mask, ac.migratetype); + + return page; +} +EXPORT_SYMBOL(__alloc_pages_nodemask); +``` + +最简单的情形中, 分配空闲内存区只涉及调用一次`get_page_from_freelist`, 然后返回所需数目的页(由标号got_pg处的代码处理). + 第一次内存分配尝试不会特别积极. 如果在某个内存域中无法找到空闲内存, 则意味着内存没剩下多少了, 内核需要增加较多的工作量才能找到更多内存("重型武器"稍后才会出现). \ No newline at end of file diff --git a/study/kernel/02-memory/04-buddy/05-free_page/README.md b/study/kernel/02-memory/04-buddy/05-free_page/README.md index 88dd91f..7290703 100644 --- a/study/kernel/02-memory/04-buddy/05-free_page/README.md +++ b/study/kernel/02-memory/04-buddy/05-free_page/README.md @@ -1,1394 +1,1394 @@ -服务器体系与共享存储器架构 -======= - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - - - - -在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法. - -Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述. - -伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者. - -* 内核如何记住哪些内存块是空闲的 - -* 分配空闲页面的方法 - -* 影响分配器行为的众多标识位 - -* 内存碎片的问题和分配器如何处理碎片 - - - - -#2 伙伴系统的结构 -------- - - -##2.1 伙伴系统数据结构 -------- - -系统内存中的每个物理内存页(页帧),都对应于一个struct page实例, 每个内存域都关联了一个struct zone的实例,其中保存了用于管理伙伴数据的主要数数组 - - -```cpp -// http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L324 -struct zone -{ - /* free areas of different sizes */ - struct free_area free_area[MAX_ORDER]; -}; -``` - -struct free_area是一个伙伴系统的辅助数据结构, 它定义在[include/linux/mmzone.h?v=4.7, line 88](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L88) - -```cpp -struct free_area { - struct list_head free_list[MIGRATE_TYPES]; -unsigned long nr_free; -}; -``` - -| 字段 | 描述 | -|:-----:|:-----:| -| free_list | 是用于连接空闲页的链表. 页链表包含大小相同的连续内存区 | -| nr_free | 指定了当前内存区中空闲页块的数目(对0阶内存区逐页计算,对1阶内存区计算页对的数目,对2阶内存区计算4页集合的数目,依次类推 | - - - -伙伴系统的分配器维护空闲页面所组成的块, 这里每一块都是2的方幂个页面, 方幂的指数称为**阶**. - - -阶是伙伴系统中一个非常重要的术语. 它描述了内存分配的数量单位. 内存块的长度是$2^order$, 其中order的范围从0到MAX_ORDER - - -zone->free_area[MAX_ORDER]数组中阶作为各个元素的索引, 用于指定对应链表中的连续内存区包含多少个页帧. - -* 数组中第0个元素的阶为0, 它的free_list链表域指向具有包含区为单页($2^0=1$)的内存页面链表 - -* 数组中第1个元素的free_list域管理的内存区为两页($2^1=2$) - -* 第3个管理的内存区为4页, 依次类推. - -* 直到$2^{MAX_ORDER-1}$个页面大小的块 - -![空闲页快](../images/order_free_list.png) - - -##2.2 最大阶MAX_ORDER与FORCE_MAX_ZONEORDER配置选项 -------- - - -一般来说`MAX_ORDER`默认定义为11, 这意味着一次分配可以请求的页数最大是2^11=2048, 参见[include/linux/mmzone.h?v=4.7, line 22](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L22) - -```cpp -/* Free memory management - zoned buddy allocator. */ -#ifndef CONFIG_FORCE_MAX_ZONEORDER -#define MAX_ORDER 11 -#else -#define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER -#endif -#define MAX_ORDER_NR_PAGES (1 << (MAX_ORDER - 1)) -``` - - - -但如果特定于体系结构的代码设置了`FORCE_MAX_ZONEORDER`配置选项, 该值也可以手工改变 - -例如,IA-64系统上巨大的地址空间可以处理`MAX_ORDER = 18`的情形,而ARM或v850系统则使用更小的值(如8或9). 但这不一定是由计算机支持的内存数量比较小引起的,也可能是内存对齐方式的要求所导致 - - -可以参考一些架构的Kconfig文件如下 - -| arm | arm64 | -|:----:|:-----:|:-------:| -| [arch/arm/Kconfig?v=4.7, line 1696](http://lxr.free-electrons.com/source/arch/arm/Kconfig?v=4.7#L1696) | [arch/arm64/Kconfig?v=4.7, line 679](http://lxr.free-electrons.com/source/arch/arm64/Kconfig?v=4.7#L679) - - -比如[arm64体系结构的Kconfig配置文件的描述](http://lxr.free-electrons.com/source/arch/arm64/Kconfig?v=4.7#L679) | - -```cpp -config FORCE_MAX_ZONEORDER -int -default "14" if (ARM64_64K_PAGES && TRANSPARENT_HUGEPAGE) -default "12" if (ARM64_16K_PAGES && TRANSPARENT_HUGEPAGE) -default "11"` -``` - - - - - - -##2.3 内存区是如何连接的 -------- - - -内存区中第1页内的链表元素, 可用于将内存区维持在链表中。因此,也不必引入新的数据结构来管理物理上连续的页,否则这些页不可能在同一内存区中. 如下图所示 - - -![伙伴系统中相互连接的内存区](../images/buddy_node_connect.png) - - -伙伴不必是彼此连接的. 如果一个内存区在分配其间分解为两半, 内核会自动将未用的一半加入到对应的链表中. - -如果在未来的某个时刻, 由于内存释放的缘故, 两个内存区都处于空闲状态, 可通过其地址判断其是否为伙伴. 管理工作较少, 是伙伴系统的一个主要优点. - -基于伙伴系统的内存管理专注于某个结点的某个内存域, 例如, DMA或高端内存域. 但所有内存域和结点的伙伴系统都通过备用分配列表连接起来. - -下图说明了这种关系. - -![伙伴系统和内存域/结点之间的关系](../images/buddy_and_node_zone.png) - - -最后要注意, 有关伙伴系统和当前状态的信息可以在/proc/buddyinfo中获取 - -![伙伴系统和当前状态的信息](../images/buddy_info.png) - - -上述输出给出了各个内存域中每个分配阶中空闲项的数目, 从左至右, 阶依次升高. 上面给出的信息取自4 GiB物理内存的AMD64系统. - -#传统伙伴系统算法 -------- - -在内核分配内存时, 必须记录页帧的已分配或空闲状态, 以免两个进程使用同样的内存区域. 由于内存分配和释放非常频繁, 内核还必须保证相关操作尽快完成. 内核可以只分配完整的页帧. 将内存划分为更小的部分的工作, 则委托给用户空间中的标准库. 标准库将来源于内核的页帧拆分为小的区域, 并为进程分配内存. - - -内核中很多时候要求分配连续页. 为快速检测内存中的连续区域, 内核采用了一种古老而历经检验的技术: **伙伴系统** - -系统中的空闲内存块总是两两分组, 每组中的两个内存块称作伙伴. 伙伴的分配可以是彼此独立的. 但如果两个伙伴都是空闲的, 内核会将其合并为一个更大的内存块, 作为下一层次上某个内存块的伙伴. - -下图示范了该系统, 图中给出了一对伙伴, 初始大小均为8页. 即系统中所有的页面都是8页的. - -![伙伴系统](../images/buddy_system.png) - -内核对所有大小相同的伙伴(1、2、4、8、16或其他数目的页),都放置到同一个列表中管理. 各有8页的一对伙伴也在相应的列表中. - -如果系统现在需要8个页帧, 则将16个页帧组成的块拆分为两个伙伴. 其中一块用于满足应用程序的请求, 而剩余的8个页帧则放置到对应8页大小内存块的列表中. - -如果下一个请求只需要2个连续页帧, 则由8页组成的块会分裂成2个伙伴, 每个包含4个页帧. 其中一块放置回伙伴列表中,而另一个再次分裂成2个伙伴, 每个包含2页。其中一个回到伙伴系统,另一个则传递给应用程序. - -在应用程序释放内存时, 内核可以直接检查地址, 来判断是否能够创建一组伙伴, 并合并为一个更大的内存块放回到伙伴列表中, 这刚好是内存块分裂的逆过程。这提高了较大内存块可用的可能性. - -在系统长期运行时,服务器运行几个星期乃至几个月是很正常的,许多桌面系统也趋向于长期开机运行,那么会发生称为碎片的内存管理问题。频繁的分配和释放页帧可能导致一种情况:系统中有若干页帧是空闲的,但却散布在物理地址空间的各处。换句话说,系统中缺乏连续页帧组成的较大的内存块,而从性能上考虑,却又很需要使用较大的连续内存块。通过伙伴系统可以在某种程度上减少这种效应,但无法完全消除。如果在大块的连续内存中间刚好有一个页帧分配出去,很显然这两块空闲的内存是无法合并的. - -在内核版本2.6.24之后, 增加了一些有效措施来防止内存碎片. - - -#3 避免碎片 -------- - -在第1章给出的简化说明中, 一个双链表即可满足伙伴系统的所有需求. 在内核版本2.6.23之前, 的确是这样. 但在内核2.6.24开发期间, 内核开发者对伙伴系统的争论持续了相当长时间. 这是因为伙伴系统是内核最值得尊敬的一部分,对它的改动不会被大家轻易接受 - - -##3.1 内存碎片 - - -伙伴系统的基本原理已经在第1章中讨论过,其方案在最近几年间确实工作得非常好。但在Linux内存管理方面,有一个长期存在的问题:在系统启动并长期运行后,物理内存会产生很多碎片。该情形如下图所示 - -![物理内存的碎片](../images/physical_memory_fragmentation.png) - -假定内存由60页组成,这显然不是超级计算机,但用于示例却足够了。左侧的地址空间中散布着空闲页。尽管大约25%的物理内存仍然未分配,但最大的连续空闲区只有一页. 这对用户空间应用程序没有问题:其内存是通过页表映射的,无论空闲页在物理内存中的分布如何,应用程序看到的内存 -似乎总是连续的。右图给出的情形中,空闲页和使用页的数目与左图相同,但所有空闲页都位于一个连续区中。 - -但对内核来说,碎片是一个问题. 由于(大多数)物理内存一致映射到地址空间的内核部分, 那么在左图的场景中, 无法映射比一页更大的内存区. 尽管许多时候内核都分配的是比较小的内存, 但也有时候需要分配多于一页的内存. 显而易见, 在分配较大内存的情况下, 右图中所有已分配页和空闲页都处于连续内存区的情形,是更为可取的. - -很有趣的一点是, 在大部分内存仍然未分配时, 就也可能发生碎片问题. 考虑图3-25的情形. - -只分配了4页,但可分配的最大连续区只有8页,因为伙伴系统所能工作的分配范围只能是2的幂次. - - -![物理内存的碎片](../images/some_memory_fragmentation.png) - -我提到内存碎片只涉及内核,这只是部分正确的。大多数现代CPU都提供了使用巨型页的可能性,比普通页大得多。这对内存使用密集的应用程序有好处。在使用更大的页时,地址转换后备缓冲器只需处理较少的项,降低了TLB缓存失效的可能性。但分配巨型页需要连续的空闲物理内存! - -很长时间以来,物理内存的碎片确实是Linux的弱点之一。尽管已经提出了许多方法,但没有哪个方法能够既满足Linux需要处理的各种类型工作负荷提出的苛刻需求,同时又对其他事务影响不大。 - - - - -##3.2 依据可移动性组织页 -------- - - -在内核2.6.24开发期间,防止碎片的方法最终加入内核。在我讨论具体策略之前,有一点需要澄清。 - -文件系统也有碎片,该领域的碎片问题主要通过碎片合并工具解决。它们分析文件系统,重新排序已分配存储块,从而建立较大的连续存储区. 理论上,该方法对物理内存也是可能的,但由于许多物理内存页不能移动到任意位置,阻碍了该方法的实施。因此,内核的方法是反碎片(anti-fragmentation), 即试图从最初开始尽可能防止碎片. - - -反碎片的工作原理如何? - - - -为理解该方法,我们必须知道内核将已分配页划分为下面3种不同类型。 - - -| 页面类型 | 描述 | 举例 | -|:---------:|:-----:|:-----:| -| 不可移动页 | 在内存中有固定位置, **不能移动**到其他地方. | 核心内核分配的大多数内存属于该类别 | -| 可移动页 | **可以随意地移动**. | 属于用户空间应用程序的页属于该类别. 它们是通过页表映射的
如果它们复制到新位置,页表项可以相应地更新,应用程序不会注意到任何事 | -| 可回收页 | **不能直接移动, 但可以删除, 其内容可以从某些源重新生成**. | 例如,映射自文件的数据属于该类别
kswapd守护进程会根据可回收页访问的频繁程度,周期性释放此类内存. , 页面回收本身就是一个复杂的过程. 内核会在可回收页占据了太多内存时进行回收, 在内存短缺(即分配失败)时也可以发起页面回收. | - - - -页的可移动性,依赖该页属于3种类别的哪一种. 内核使用的**反碎片技术**, 即基于将具有相同可移动性的页分组的思想. - - -为什么这种方法有助于减少碎片? - - -由于页无法移动, 导致在原本几乎全空的内存区中无法进行连续分配. 根据页的可移动性, 将其分配到不同的列表中, 即可防止这种情形. 例如, 不可移动的页不能位于可移动内存区的中间, 否则就无法从该内存区分配较大的连续内存块. - - -想一下, 上图中大多数空闲页都属于可回收的类别, 而分配的页则是不可移动的. 如果这些页聚集到两个不同的列表中, 如下图所示. 在不可移动页中仍然难以找到较大的连续空闲空间, 但对可回收的页, 就容易多了. - - -![减少内存碎片](../images/little_memory_fragmentation.png) - - -但要注意, 从最初开始, 内存并未划分为可移动性不同的区. 这些是在运行时形成的. 内核的另一种方法确实将内存分区, 分别用于可移动页和不可移动页的分配, 我会下文讨论其工作原理. 但这种划分对这里描述的方法是不必要的 - - - -##3.3 避免碎片数据结构 -------- - - - -###3.3.1 迁移类型 -------- - - -尽管内核使用的反碎片技术卓有成效,它对伙伴分配器的代码和数据结构几乎没有影响。内核定义了一些枚举常量(早期用宏来实现)来表示不同的迁移类型, 参见[include/linux/mmzone.h?v=4.7, line 38](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L38) - -```cpp -enum { - MIGRATE_UNMOVABLE, - MIGRATE_MOVABLE, - MIGRATE_RECLAIMABLE, - MIGRATE_PCPTYPES, /* the number of types on the pcp lists */ - MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES, -#ifdef CONFIG_CMA - /* - * MIGRATE_CMA migration type is designed to mimic the way - * ZONE_MOVABLE works. Only movable pages can be allocated - * from MIGRATE_CMA pageblocks and page allocator never - * implicitly change migration type of MIGRATE_CMA pageblock. - * - * The way to use it is to change migratetype of a range of - * pageblocks to MIGRATE_CMA which can be done by - * __free_pageblock_cma() function. What is important though - * is that a range of pageblocks must be aligned to - * MAX_ORDER_NR_PAGES should biggest page be bigger then - * a single pageblock. - */ - MIGRATE_CMA, -#endif -#ifdef CONFIG_MEMORY_ISOLATION - MIGRATE_ISOLATE, /* can't allocate from here */ -#endif - MIGRATE_TYPES -}; -``` - - -| 宏 | 类型 | -|:----:|:-----:| -| MIGRATE_UNMOVABLE | 不可移动页 | -| MIGRATE_MOVABLE | 可移动页 | -| MIGRATE_RECLAIMABLE | 可回收页 | -| MIGRATE_PCPTYPES | 是per_cpu_pageset, 即用来表示每CPU页框高速缓存的数据结构中的链表的迁移类型数目 | -| MIGRATE_HIGHATOMIC | = MIGRATE_PCPTYPES, 在罕见的情况下,内核需要分配一个高阶的页面块而不能休眠.如果向具有特定可移动性的列表请求分配内存失败,这种紧急情况下可从MIGRATE_HIGHATOMIC中分配内存 | -| MIGRATE_CMA | Linux内核最新的连续内存分配器(CMA), 用于避免预留大块内存 | -| MIGRATE_ISOLATE | 是一个特殊的虚拟区域, 用于跨越NUMA结点移动物理内存页. 在大型系统上, 它有益于将物理内存页移动到接近于使用该页最频繁的CPU. | -| MIGRATE_TYPES | 只是表示迁移类型的数目, 也不代表具体的区域 | - -对于MIGRATE_CMA类型, 其中在我们使用ARM等嵌入式Linux系统的时候, 一个头疼的问题是GPU, Camera, HDMI等都需要预留大量连续内存,这部分内存平时不用,但是一般的做法又必须先预留着. 目前, Marek Szyprowski和Michal Nazarewicz实现了一套全新的Contiguous Memory Allocator. 通过这套机制, 我们可以做到不预留内存,这些内存平时是可用的,只有当需要的时候才被分配给Camera,HDMI等设备. 参照[宋宝华--Linux内核最新的连续内存分配器(CMA)——避免预留大块内存](http://21cnbao.blog.51cto.com/109393/898846/), 内核为此提供了函数is_migrate_cma来检测当前类型是否为MIGRATE_CMA, 该函数定义在[include/linux/mmzone.h?v=4.7, line 69](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L69) - -```cpp -/* In mm/page_alloc.c; keep in sync also with show_migration_types() there */ -extern char * const migratetype_names[MIGRATE_TYPES]; - -#ifdef CONFIG_CMA -# define is_migrate_cma(migratetype) unlikely((migratetype) == MIGRATE_CMA) -#else -# define is_migrate_cma(migratetype) false -#endif -``` - - - - -对伙伴系统数据结构的主要调整, 是将空闲列表分解为MIGRATE_TYPE个列表, 可以参见free_area的定义[include/linux/mmzone.h?v=4.7, line 88](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L88) - -```cpp -struct free_area -{ - struct list_head free_list[MIGRATE_TYPES]; -unsigned long nr_free; -}; -``` - -* nr_free统计了所有列表上空闲页的数目,而每种迁移类型都对应于一个空闲列表 - - -宏for_each_migratetype_order(order, type)可用于迭代指定迁移类型的所有分配阶 - -```cpp -#define for_each_migratetype_order(order, type) \ - for (order = 0; order < MAX_ORDER; order++) \ - for (type = 0; type < MIGRATE_TYPES; type++) -``` - - -###3.3.2 迁移备用列表fallbacks -------- - - - -如果内核无法满足针对某一给定迁移类型的分配请求, 会怎么样? - - - -此前已经出现过一个类似的问题, 即特定的NUMA内存域无法满足分配请求时. 我们需要从其他内存域中选择一个代价最低的内存域完成内存的分配, 因此内核在内存的结点pg_data_t中提供了一个备用内存域列表zonelists. - -内核在内存迁移的过程中处理这种情况下的做法是类似的. 提供了一个备用列表fallbacks, 规定了在指定列表中无法满足分配请求时. 接下来应使用哪一种迁移类型, 定义在[mm/page_alloc.c?v=4.7, line 1799](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L1799) - - -```cpp -/* - * This array describes the order lists are fallen back to when - * the free lists for the desirable migrate type are depleted - * 该数组描述了指定迁移类型的空闲列表耗尽时 - * 其他空闲列表在备用列表中的次序 - */ -static int fallbacks[MIGRATE_TYPES][4] = { - // 分配不可移动页失败的备用列表 - [MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_TYPES }, - // 分配可回收页失败时的备用列表 - [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_TYPES }, - // 分配可移动页失败时的备用列表 - [MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_TYPES }, -#ifdef CONFIG_CMA - [MIGRATE_CMA] = { MIGRATE_TYPES }, /* Never used */ -#endif -#ifdef CONFIG_MEMORY_ISOLATION - [MIGRATE_ISOLATE] = { MIGRATE_TYPES }, /* Never used */ -#endif -}; -``` - ->该数据结构大体上是自明的 : -> ->每一行对应一个类型的备用搜索域的顺序, 在内核想要分配不可移动页`MIGRATE_UNMOVABLE`时, 如果对应链表为空, 则遍历fallbacks[MIGRATE_UNMOVABLE], 首先后退到可回收页链表`MIGRATE_RECLAIMABLE`, 接下来到可移动页链表`MIGRATE_MOVABLE`, 最后到紧急分配链表`MIGRATE_TYPES`. - - -##3.3.3 pageblock_order变量 -------- - - -全局变量和辅助函数尽管页可移动性分组特性总是编译到内核中,但只有在系统中有足够内存可以分配到多个迁移类型对应的链表时,才是有意义的。由于每个迁移链表都应该有适当数量的内存,内核需要定义"适当"的概念. 这是通过两个全局变量pageblock_order和pageblock_nr_pages提供的. 第一个表示内核认为是"大"的一个分配阶, pageblock_nr_pages则表示该分配阶对应的页数。如果体系结构提供了巨型页机制, 则pageblock_order通常定义为巨型页对应的分配阶. 定义在[include/linux/pageblock-flags.h?v=4.7, line 44](http://lxr.free-electrons.com/source/include/linux/pageblock-flags.h?v=4.7#L42) - - -```cpp -#ifdef CONFIG_HUGETLB_PAGE - - #ifdef CONFIG_HUGETLB_PAGE_SIZE_VARIABLE - - /* Huge page sizes are variable */ - extern unsigned int pageblock_order; - - #else /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */ - - /* Huge pages are a constant size */ - #define pageblock_order HUGETLB_PAGE_ORDER - - #endif /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */ - -#else /* CONFIG_HUGETLB_PAGE */ - - /* If huge pages are not used, group by MAX_ORDER_NR_PAGES */ - #define pageblock_order (MAX_ORDER-1) - -#endif /* CONFIG_HUGETLB_PAGE */ - -#define pageblock_nr_pages (1UL << pageblock_order) -``` - -在IA-32体系结构上, 巨型页长度是4MB, 因此每个巨型页由1024个普通页组成, 而HUGETLB_PAGE_ORDER则定义为10. 相比之下, IA-64体系结构允许设置可变的普通和巨型页长度, 因此HUGETLB_PAGE_ORDER的值取决于内核配置. - -如果体系结构不支持巨型页, 则将其定义为第二高的分配阶, 即`MAX_ORDER - 1` - -```cpp -/* If huge pages are not used, group by MAX_ORDER_NR_PAGES */ -#define pageblock_order (MAX_ORDER-1) -``` - -如果各迁移类型的链表中没有一块较大的连续内存, 那么页面迁移不会提供任何好处, 因此在可用内存太少时内核会关闭该特性. 这是在build_all_zonelists函数中检查的, 该函数用于初始化内存域列表. 如果没有足够的内存可用, 则全局变量[`page_group_by_mobility_disabled`](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L79)设置为0, 否则设置为1. - -内核如何知道给定的分配内存属于何种迁移类型? - -我们将在以后讲解, 有关各个内存分配的细节都通过分配掩码指定. - -内核提供了两个标志,分别用于表示分配的内存是可移动的(\__GFP_MOVABLE)或可回收的(\__GFP_RECLAIMABLE). - - - -###3.3.4 gfpflags_to_migratetype函数 -------- - - -如果这些标志都没有设置, 则分配的内存假定为不可移动的. 辅助函数gfpflags_to_migratetype可用于转换分配标志及对应的迁移类型, 该函数定义在[include/linux/gfp.h?v=4.7, line 266](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L266) - -```cpp -static inline int gfpflags_to_migratetype(const gfp_t gfp_flags) -{ - VM_WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK); - BUILD_BUG_ON((1UL << GFP_MOVABLE_SHIFT) != ___GFP_MOVABLE); - BUILD_BUG_ON((___GFP_MOVABLE >> GFP_MOVABLE_SHIFT) != MIGRATE_MOVABLE); - - if (unlikely(page_group_by_mobility_disabled)) - return MIGRATE_UNMOVABLE; - - /* Group based on mobility */ - return (gfp_flags & GFP_MOVABLE_MASK) >> GFP_MOVABLE_SHIFT; -} -``` - ->linux-2.6.x的内核中转换分配标志及对应的迁移类型的辅助函数为allocflags_to_migratetype, 这个名字会有歧义的, 让我们误以为参数的标识中有alloc flags, 但是其实并不然, 因此后来的内核中将该函数更名为gfpflags_to_migratetype, 参见[Rename it to gfpflags_to_migratetype()](https://patchwork.kernel.org/patch/4291831) - -在2.6.25中为如下接口 - -```cpp -/* Convert GFP flags to their corresponding migrate type */ -static inline int allocflags_to_migratetype(gfp_t gfp_flags) -{ - WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK); - - if (unlikely(page_group_by_mobility_disabled)) - return MIGRATE_UNMOVABLE; - - /* Group based on mobility */ - return (((gfp_flags & __GFP_MOVABLE) != 0) << 1) | - ((gfp_flags & __GFP_RECLAIMABLE) != 0); -} -```` - -如果停用了页面迁移特性, 则所有的页都是不可移动的. 否则. 该函数的返回值可以直接用作free_area.free_list的数组索引. - -###3.3.5 pageblock_flags变量与其函数接口 - -最后要注意, 每个内存域都提供了一个特殊的字段, 可以跟踪包含pageblock_nr_pages个页的内存区的属性. 即zone->pageblock_flags字段, 当前只有与页可移动性相关的代码使用, 参见[include/linux/mmzone.h?v=4.7, line 367](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L367) - - -```cpp -struct zone -{ -#ifndef CONFIG_SPARSEMEM - /* - * Flags for a pageblock_nr_pages block. See pageblock-flags.h. - * In SPARSEMEM, this map is stored in struct mem_section - */ - unsigned long *pageblock_flags; -#endif /* CONFIG_SPARSEMEM */ -}; -``` - -在初始化期间, 内核自动确保对内存域中的每个不同的迁移类型分组, 在pageblock_flags中都分配了足够存储NR_PAGEBLOCK_BITS个比特位的空间。当前,表示一个连续内存区的迁移类型需要3个比特位, 参见[include/linux/pageblock-flags.h?v=4.7, line 28](http://lxr.free-electrons.com/source/include/linux/pageblock-flags.h?v=4.7#L28) - -```cpp -/* Bit indices that affect a whole block of pages */ -enum pageblock_bits { - PB_migrate, - PB_migrate_end = PB_migrate + 3 - 1, - /* 3 bits required for migrate types */ - PB_migrate_skip,/* If set the block is skipped by compaction */ - - /* - * Assume the bits will always align on a word. If this assumption - * changes then get/set pageblock needs updating. - */ - NR_PAGEBLOCK_BITS -}; -``` - - -内核提供`set_pageblock_migratetype`负责设置以page为首的一个内存区的迁移类型, 该函数定义在[mm/page_alloc.c?v=4.7, line 458](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L458), 如下所示 - - -```cpp -void set_pageblock_migratetype(struct page *page, int migratetype) -{ - if (unlikely(page_group_by_mobility_disabled && - migratetype < MIGRATE_PCPTYPES)) - migratetype = MIGRATE_UNMOVABLE; - - set_pageblock_flags_group(page, (unsigned long)migratetype, - PB_migrate, PB_migrate_end); -} -``` - - - -`migratetype`参数可以通过上文介绍的`gfpflags_to_migratetype`辅助函数构建. 请注意很重要的一点, 页的迁移类型是预先分配好的, 对应的比特位总是可用, 与页是否由伙伴系统管理无关. 在释放内存时,页必须返回到正确的迁移链表。这之所以可行,是因为能够从`get_pageblock_migratetype`获得所需的信息. 参见[include/linux/mmzone.h?v=4.7, line 84](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L84) - - -```cpp -#define get_pageblock_migratetype(page) \ - get_pfnblock_flags_mask(page, page_to_pfn(page), \ - PB_migrate_end, MIGRATETYPE_MASK) -``` - - -最后请注意, 在各个迁移链表之间, 当前的页面分配状态可以从`/proc/pagetypeinfo`获得. - - -![proc/pagetypeinfo]() - - - - -##初始化基于可移动性的分组 -------- - - - - -在内存子系统初始化期间, memmap_init_zone负责处理内存域的page实例. 该函数定义在[mm/page_alloc.c?v=4.7, line 5139](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5139), 该函数完成了一些不怎么有趣的标准初始化工作,但其中有一件是实质性的,即所有的页最初都标记为可移动的. 参见[mm/page_alloc.c?v=4.7, line 5224](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5224) - - -```cpp -/* - * Initially all pages are reserved - free ones are freed - * up by free_all_bootmem() once the early boot process is - * done. Non-atomic initialization, single-pass. - */ -void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone, - unsigned long start_pfn, enum memmap_context context) -{ - /* ...... */ - - for (pfn = start_pfn; pfn < end_pfn; pfn++) { - /* ...... */ -not_early: - if (!(pfn & (pageblock_nr_pages - 1))) { - struct page *page = pfn_to_page(pfn); - - __init_single_page(page, pfn, zone, nid); - set_pageblock_migratetype(page, MIGRATE_MOVABLE); - } else { - __init_single_pfn(pfn, zone, nid); - } - } -} -``` - - -在分配内存时, 如果必须"盗取"不同于预定迁移类型的内存区, 内核在策略上倾向于"盗取"更大的内存区. 由于所有页最初都是可移动的, 那么在内核分配不可移动的内存区时, 则必须"盗取". - -实际上, 在启动期间分配可移动内存区的情况较少, 那么分配器有很高的几率分配长度最大的内存区, 并将其从可移动列表转换到不可移动列表. 由于分配的内存区长度是最大的, 因此不会向可移动内存中引入碎片. - -总而言之, 这种做法避免了启动期间内核分配的内存(经常在系统的整个运行时间都不释放)散布到物理内存各处, 从而使其他类型的内存分配免受碎片的干扰,这也是页可移动性分组框架的最重要的目标之一. - - -#4 分配器API -------- - - -##4.1 分配内存的接口 -------- - -就伙伴系统的接口而言, NUMA或UMA体系结构是没有差别的, 二者的调用语法都是相同的. - -所有函数的一个共同点是 : 只能分配2的整数幂个页. - -因此,接口中不像C标准库的malloc函数或bootmem和memblock分配器那样指定了所需内存大小作为参数. 相反, 必须指定的是分配阶, 伙伴系统将在内存中分配$2^order$页. 内核中细粒度的分配只能借助于slab分配器(或者slub、slob分配器), 后者基于伙伴系统 - - -| 内存分配函数 | 功能 | 定义 | -|:-----:|:-----:| -| alloc_pages(mask, order) | 分配$2^order$页并返回一个struct page的实例,表示分配的内存块的起始页 | [NUMA-include/linux/gfp.h, line 466](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L466)
[UMA-include/linux/gfp.h?v=4.7, line 476](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L476) | -| alloc_page(mask) | 是前者在order = 0情况下的简化形式,只分配一页 | [include/linux/gfp.h?v=4.7, line 483](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L483) | -| get_zeroed_page(mask) | 分配一页并返回一个page实例,页对应的内存填充0(所有其他函数,分配之后页的内容是未定义的) | [mm/page_alloc.c?v=4.7, line 3900](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3900)| | -| [__get_free_pages(mask, order)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883)
[__get_free_page(mask)](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L500) | 工作方式与上述函数相同,但返回分配内存块的虚拟地址,而不是page实例 | -| get_dma_pages(gfp_mask, order) | 用来获得适用于DMA的页. | [include/linux/gfp.h?v=4.7, line 503](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L503) | - - -在空闲内存无法满足请求以至于分配失败的情况下,所有上述函数都返回空指针(比如alloc_pages和alloc_page)或者0(比如get_zeroed_page、__get_free_pages和__get_free_page). - -因此内核在各次分配之后都必须检查返回的结果. 这种惯例与设计得很好的用户层应用程序没什么不同, 但在内核中忽略检查会导致严重得多的故障 - - -内核除了伙伴系统函数之外, 还提供了其他内存管理函数. 它们以伙伴系统为基础, 但并不属于伙伴分配器自身. 这些函数包括vmalloc和vmalloc_32, 使用页表将不连续的内存映射到内核地址空间中, 使之看上去是连续的. - -还有一组kmalloc类型的函数, 用于分配小于一整页的内存区. 其实现 -将在本章后续的几节分别讨论。 - - - -## 释放函数 -------- - -有4个函数用于释放不再使用的页,与所述函数稍有不同 - - -| 内存释放函数 | 描述 | -|:--------------:|:-----:| -| [free_page(struct page *)](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L520)
[free_pages(struct page *, order)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3918) | 用于将一个或2order页返回给内存管理子系统。内存区的起始地址由指向该内存区的第一个page实例的指针表示 | -| [__free_page(addr)](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L519)
[__free_pages(addr, order)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3906) | 类似于前两个函数,但在表示需要释放的内存区时,使用了虚拟内存地址而不是page实例 | - - - -##4.2 分配掩码 -------- - - -前述所有函数中强制使用的mask参数,到底是什么语义? - -我们知道Linux将内存划分为内存域. 内核提供了所谓的内存域修饰符(zone modifier)(在掩码的最低4个比特位定义), 来指定从哪个内存域分配所需的页. - - -内核使用宏的方式定义了这些掩码, 一个掩码的定义被划分为3个部分进行定义, 我们会逐步展开来讲解, 参见[include/linux/gfp.h?v=4.7, line 12~374](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L12), 共计26个掩码信息, 因此后面__GFP_BITS_SHIFT = 26. - -```cpp -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7 - -/* line 12 ~ line 44 第一部分 - * 定义可掩码所在位的信息, 每个掩码对应一位为1 - * 定义形式为 #define ___GFP_XXX 0x01u - */ -/* Plain integer GFP bitmasks. Do not use this directly. */ -#define ___GFP_DMA 0x01u -#define ___GFP_HIGHMEM 0x02u -#define ___GFP_DMA32 0x04u -#define ___GFP_MOVABLE 0x08u -/* ...... */ - -/* line 46 ~ line 192 第二部分 - * 定义掩码和MASK信息, 第二部分的某些宏可能是第一部分一个或者几个的组合 - * 定义形式为 #define __GFP_XXX ((__force gfp_t)___GFP_XXX) - */ -#define __GFP_DMA ((__force gfp_t)___GFP_DMA) -#define __GFP_HIGHMEM ((__force gfp_t)___GFP_HIGHMEM) -#define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32) -#define __GFP_MOVABLE ((__force gfp_t)___GFP_MOVABLE) /* ZONE_MOVABLE allowed */ -#define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE) - -/* line 194 ~ line 260 第三部分 - * 定义掩码 - * 定义形式为 #define GFP_XXX __GFP_XXX - */ -#define GFP_DMA __GFP_DMA -#define GFP_DMA32 __GFP_DMA32 -``` - - -其中GFP缩写的意思为获取空闲页(get free page), __GFP_MOVABLE不表示物理内存域, 但通知内核应在特殊的虚拟内存域ZONE_MOVABLE进行相应的分配. - -##4.2.1 定义掩码位 -------- - - -我们首先来看**第一部分**, 内核源代码中定义在[include/linux/gfp.h?v=4.7, line 18 ~ line 44](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L17), 共计26个掩码信息, - - -```cpp -/* Plain integer GFP bitmasks. Do not use this directly. */ -#define ___GFP_DMA 0x01u -#define ___GFP_HIGHMEM 0x02u -#define ___GFP_DMA32 0x04u -#define ___GFP_MOVABLE 0x08u /* 页是可移动的 */ -#define ___GFP_RECLAIMABLE 0x10u /* 页是可回收的 */ -#define ___GFP_HIGH 0x20u /* 应该访问紧急分配池? */ -#define ___GFP_IO 0x40u /* 可以启动物理IO? */ -#define ___GFP_FS 0x80u /* 可以调用底层文件系统? */ -#define ___GFP_COLD 0x100u /* 需要非缓存的冷页 */ -#define ___GFP_NOWARN 0x200u /* 禁止分配失败警告 */ -#define ___GFP_REPEAT 0x400u /* 重试分配,可能失败 */ -#define ___GFP_NOFAIL 0x800u /* 一直重试,不会失败 */ -#define ___GFP_NORETRY 0x1000u /* 不重试,可能失败 */ -#define ___GFP_MEMALLOC 0x2000u /* 使用紧急分配链表 */ -#define ___GFP_COMP 0x4000u /* 增加复合页元数据 */ -#define ___GFP_ZERO 0x8000u /* 成功则返回填充字节0的页 */ -#define ___GFP_NOMEMALLOC 0x10000u /* 不使用紧急分配链表 */ -#define ___GFP_HARDWALL 0x20000u /* 只允许在进程允许运行的CPU所关联的结点分配内存 */ -#define ___GFP_THISNODE 0x40000u /* 没有备用结点,没有策略 */ -#define ___GFP_ATOMIC 0x80000u /* 用于原子分配,在任何情况下都不能中断 */ -#define ___GFP_ACCOUNT 0x100000u -#define ___GFP_NOTRACK 0x200000u -#define ___GFP_DIRECT_RECLAIM 0x400000u -#define ___GFP_OTHER_NODE 0x800000u -#define ___GFP_WRITE 0x1000000u -#define ___GFP_KSWAPD_RECLAIM 0x2000000u -``` - -###4.2.2 定义掩码 -------- - -然后**第二部分**, 相对而言每一个宏又被重新定义如下, 参见[include/linux/gfp.h?v=4.7, line 46 ~ line 192](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L46) - -```cpp -/* -* Physical address zone modifiers (see linux/mmzone.h - low four bits) -* -* Do not put any conditional on these. If necessary modify the definitions -* without the underscores and use them consistently. The definitions here may -* be used in bit comparisons. -*/ -#define __GFP_DMA ((__force gfp_t)___GFP_DMA) -#define __GFP_HIGHMEM ((__force gfp_t)___GFP_HIGHMEM) -#define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32) -#define __GFP_MOVABLE ((__force gfp_t)___GFP_MOVABLE) /* ZONE_MOVABLE allowed */ -#define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE) - -/* -* Page mobility and placement hints -* -* These flags provide hints about how mobile the page is. Pages with similar -* mobility are placed within the same pageblocks to minimise problems due -* to external fragmentation. -* -* __GFP_MOVABLE (also a zone modifier) indicates that the page can be -* moved by page migration during memory compaction or can be reclaimed. -* -* __GFP_RECLAIMABLE is used for slab allocations that specify -* SLAB_RECLAIM_ACCOUNT and whose pages can be freed via shrinkers. -* -* __GFP_WRITE indicates the caller intends to dirty the page. Where possible, -* these pages will be spread between local zones to avoid all the dirty -* pages being in one zone (fair zone allocation policy). -* -* __GFP_HARDWALL enforces the cpuset memory allocation policy. -* -* __GFP_THISNODE forces the allocation to be satisified from the requested -* node with no fallbacks or placement policy enforcements. -* -* __GFP_ACCOUNT causes the allocation to be accounted to kmemcg (only relevant -* to kmem allocations). -*/ -#define __GFP_RECLAIMABLE ((__force gfp_t)___GFP_RECLAIMABLE) -#define __GFP_WRITE ((__force gfp_t)___GFP_WRITE) -#define __GFP_HARDWALL ((__force gfp_t)___GFP_HARDWALL) -#define __GFP_THISNODE ((__force gfp_t)___GFP_THISNODE) -#define __GFP_ACCOUNT ((__force gfp_t)___GFP_ACCOUNT) - -/* -* Watermark modifiers -- controls access to emergency reserves -* -* __GFP_HIGH indicates that the caller is high-priority and that granting -* the request is necessary before the system can make forward progress. -* For example, creating an IO context to clean pages. -* -* __GFP_ATOMIC indicates that the caller cannot reclaim or sleep and is -* high priority. Users are typically interrupt handlers. This may be -* used in conjunction with __GFP_HIGH - * - * __GFP_MEMALLOC allows access to all memory. This should only be used when - * the caller guarantees the allocation will allow more memory to be freed - * very shortly e.g. process exiting or swapping. Users either should - * be the MM or co-ordinating closely with the VM (e.g. swap over NFS). - * - * __GFP_NOMEMALLOC is used to explicitly forbid access to emergency reserves. - * This takes precedence over the __GFP_MEMALLOC flag if both are set. - */ -#define __GFP_ATOMIC ((__force gfp_t)___GFP_ATOMIC) -#define __GFP_HIGH ((__force gfp_t)___GFP_HIGH) -#define __GFP_MEMALLOC ((__force gfp_t)___GFP_MEMALLOC) -#define __GFP_NOMEMALLOC ((__force gfp_t)___GFP_NOMEMALLOC) - -/* - * Reclaim modifiers - * - * __GFP_IO can start physical IO. - * - * __GFP_FS can call down to the low-level FS. Clearing the flag avoids the - * allocator recursing into the filesystem which might already be holding - * locks. - * - * __GFP_DIRECT_RECLAIM indicates that the caller may enter direct reclaim. - * This flag can be cleared to avoid unnecessary delays when a fallback - * option is available. - * - * __GFP_KSWAPD_RECLAIM indicates that the caller wants to wake kswapd when - * the low watermark is reached and have it reclaim pages until the high - * watermark is reached. A caller may wish to clear this flag when fallback - * options are available and the reclaim is likely to disrupt the system. The - * canonical example is THP allocation where a fallback is cheap but - * reclaim/compaction may cause indirect stalls. - * - * __GFP_RECLAIM is shorthand to allow/forbid both direct and kswapd reclaim. - * - * __GFP_REPEAT: Try hard to allocate the memory, but the allocation attempt - * _might_ fail. This depends upon the particular VM implementation. - * - * __GFP_NOFAIL: The VM implementation _must_ retry infinitely: the caller - * cannot handle allocation failures. New users should be evaluated carefully - * (and the flag should be used only when there is no reasonable failure - * policy) but it is definitely preferable to use the flag rather than - * opencode endless loop around allocator. - * - * __GFP_NORETRY: The VM implementation must not retry indefinitely and will - * return NULL when direct reclaim and memory compaction have failed to allow - * the allocation to succeed. The OOM killer is not called with the current - * implementation. - */ -#define __GFP_IO ((__force gfp_t)___GFP_IO) -#define __GFP_FS ((__force gfp_t)___GFP_FS) -#define __GFP_DIRECT_RECLAIM ((__force gfp_t)___GFP_DIRECT_RECLAIM) /* Caller can reclaim */ -#define __GFP_KSWAPD_RECLAIM ((__force gfp_t)___GFP_KSWAPD_RECLAIM) /* kswapd can wake */ -#define __GFP_RECLAIM ((__force gfp_t)(___GFP_DIRECT_RECLAIM|___GFP_KSWAPD_RECLAIM)) -#define __GFP_REPEAT ((__force gfp_t)___GFP_REPEAT) -#define __GFP_NOFAIL ((__force gfp_t)___GFP_NOFAIL) -#define __GFP_NORETRY ((__force gfp_t)___GFP_NORETRY) - -/* - * Action modifiers - * - * __GFP_COLD indicates that the caller does not expect to be used in the near - * future. Where possible, a cache-cold page will be returned. - * - * __GFP_NOWARN suppresses allocation failure reports. - * - * __GFP_COMP address compound page metadata. - * - * __GFP_ZERO returns a zeroed page on success. - * - * __GFP_NOTRACK avoids tracking with kmemcheck. - * - * __GFP_NOTRACK_FALSE_POSITIVE is an alias of __GFP_NOTRACK. It's a means of - * distinguishing in the source between false positives and allocations that - * cannot be supported (e.g. page tables). - * - * __GFP_OTHER_NODE is for allocations that are on a remote node but that - * should not be accounted for as a remote allocation in vmstat. A - * typical user would be khugepaged collapsing a huge page on a remote - * node. - */ -#define __GFP_COLD ((__force gfp_t)___GFP_COLD) -#define __GFP_NOWARN ((__force gfp_t)___GFP_NOWARN) -#define __GFP_COMP ((__force gfp_t)___GFP_COMP) -#define __GFP_ZERO ((__force gfp_t)___GFP_ZERO) -#define __GFP_NOTRACK ((__force gfp_t)___GFP_NOTRACK) -#define __GFP_NOTRACK_FALSE_POSITIVE (__GFP_NOTRACK) -#define __GFP_OTHER_NODE ((__force gfp_t)___GFP_OTHER_NODE) - -/* Room for N __GFP_FOO bits */ -#define __GFP_BITS_SHIFT 26 -#define __GFP_BITS_MASK ((__force gfp_t)((1 << __GFP_BITS_SHIFT) - 1)) -``` -给出的常数,其中一些很少使用,因此我不会讨论。其中最重要的一些常数语义如下所示 - - -/* \__GFP_WAIT表示分配内存的请求可以中断。也就是说,调度器在该请求期间可随意选择另一个过程执行,或者该请求可以被另一个更重要的事件中断. 分配器还可以在返回内存之前, 在队列上等待一个事件(相关进程会进入睡眠状态). - ->虽然名字相似,但__GFP_HIGH与__GFP_HIGHMEM毫无关系,请不要弄混这两者\ - -| 宏 | 描述 | -|:---:|:----:| -| \__GFP_RECLAIMABLE
\__GFP_MOVABLE | 是页迁移机制所需的标志. 顾名思义,它们分别将分配的内存标记为可回收的或可移动的。这影响从空闲列表的哪个子表获取内存 | -| \___GFP_HIGH | 如果请求非常重要, 则设置\__GFP_HIGH,即内核急切地需要内存时。在分配内存失败可能给内核带来严重后果时(比如威胁到系统稳定性或系统崩溃), 总是会使用该标志 | -| \___GFP_IO |说明在查找空闲内存期间内核可以进行I/O操作. 实际上, 这意味着如果内核在内存分配期间换出页, 那么仅当设置该标志时, 才能将选择的页写入硬盘 | -| \___GFP_FS |允许内核执行VFS操作. 在与VFS层有联系的内核子系统中必须禁用, 因为这可能引起循环递归调用. | -| \___GFP_COLD | 如果需要分配不在CPU高速缓存中的“冷”页时,则设置\__GFP_COLD | -| \___GFP_NOWARN | 在分配失败时禁止内核故障警告。在极少数场合该标志有用 | -| \___GFP_REPEAT | 在分配失败后自动重试,但在尝试若干次之后会停止 | -| \___GFP_NOFAIL | 在分配失败后一直重试,直至成功 | -| \___GFP_NORETRY | 在分配失败后不重试,因此可能分配失败 | -| \___GFP_ZERO | 在分配成功时,将返回填充字节0的页 | -| \__GFP_HARDWALL | 只在NUMA系统上有意义. 它限制只在分配到当前进程的各个CPU所关联的结点分配内存。如果进程允许在所有CPU上运行(默认情况),该标志是无意义的。只有进程可以运行的CPU受限时,该标志才有效果 | -| \__GFP_THISNODE | 也只在NUMA系统上有意义。如果设置该比特位,则内存分配失败的情况下不允许使用其他结点作为备用,需要保证在当前结点或者明确指定的结点上成功分配内存 | - - -其次还定义了我们程序和函数中所需要的掩码MASK的信息, 由于其中__GFP_DMA, __GFP_DMA32, __GFP_HIGHMEM, __GFP_MOVABLE是在内存中分别有对应的内存域信息, 因此我们定义了内存域的掩码GFP_ZONEMASK, 参见[include/linux/gfp.h?v=4.7, line 57](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L57) - -```cpp -#define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE) -``` - -那自然还有__GFP_BITS_SHIFT来表示我们所有的掩码位, 由于我们共计26个掩码位 - -```cpp -/* Room for N __GFP_FOO bits */ -#define __GFP_BITS_SHIFT 26 -#define __GFP_BITS_MASK ((__force gfp_t)((1 << __GFP_BITS_SHIFT) - 1)) -``` - -###4.2.3 掩码分组 -------- - - -最后来看**第三部分**, 由于这些标志几乎总是组合使用,内核作了一些分组,包含了用于各种标准情形的适当的标志. - -如果有可能的话,在内存管理子系统之外,总是把下列分组之一用于内存分配. 在内核源代码中,双下划线通常用于内部数据和定义。而这些预定义的分组名没有双下划线前缀,这一点从侧面验证了上述说法. - -```cpp -#define GFP_ATOMIC (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM) -#define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS) -#define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT) -#define GFP_NOWAIT (__GFP_KSWAPD_RECLAIM) -#define GFP_NOIO (__GFP_RECLAIM) -#define GFP_NOFS (__GFP_RECLAIM | __GFP_IO) -#define GFP_TEMPORARY (__GFP_RECLAIM | __GFP_IO | __GFP_FS | \ - __GFP_RECLAIMABLE) -#define GFP_USER (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL) -#define GFP_DMA __GFP_DMA -#define GFP_DMA32 __GFP_DMA32 -#define GFP_HIGHUSER (GFP_USER | __GFP_HIGHMEM) -#define GFP_HIGHUSER_MOVABLE (GFP_HIGHUSER | __GFP_MOVABLE) -#define GFP_TRANSHUGE ((GFP_HIGHUSER_MOVABLE | __GFP_COMP | \ - __GFP_NOMEMALLOC | __GFP_NORETRY | __GFP_NOWARN) & \ - ~__GFP_RECLAIM) - -/* Convert GFP flags to their corresponding migrate type */ -#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE) -#define GFP_MOVABLE_SHIFT 3 -``` - -| 掩码组 | 描述 | -|:-------:|:-----:| -| GFP_ATOMIC | 用于原子分配,在任何情况下都不能中断, 可能使用紧急分配链表中的内存 | -| GFP_NOIO
GFP_NOFS | 分别明确禁止I/O操作和访问VFS层, 但同时设置了\__GFP_RECLAIM,因此可以被回收 | -| GFP_KERNEL
GFP_USER | 分别是内核和用户分配的默认设置。二者的失败不会立即威胁系统稳定性, GFP_KERNEL绝对是内核源代码中最常使用的标志 | -| GFP_HIGHUSER | 是GFP_USER的一个扩展, 也用于用户空间. 它允许分配无法直接映射的高端内存. 使用高端内存页是没有坏处的,因为用户过程的地址空间总是通过非线性页表组织的 | -| GFP_HIGHUSER_MOVABLE |用途类似于GFP_HIGHUSER,但分配将从虚拟内存域ZONE_MOVABLE进行 | -| GFP_DMA
GFP_DMA32 | 用于分配适用于DMA的内存, 当前是\__GFP_DMA的同义词, GFP_DMA32也是\__GFP_GMA32的同义词 | - - -最后内核设置了碎片管理的可移动依据组织页的MASK信息GFP_MOVABLE_MASK, 参见[include/linux/gfp.h?v=4.7, line 262](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L262) - -```cpp -/* Convert GFP flags to their corresponding migrate type */ -#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE) -#define GFP_MOVABLE_SHIFT 3 -``` - - - -###4.2.4 总结 -------- - -我们从注释中找到这样的信息, 可以作为参考[]() - -```cpp -bit result -================= -0x0 => NORMAL -0x1 => DMA or NORMAL -0x2 => HIGHMEM or NORMAL -0x3 => BAD (DMA+HIGHMEM) -0x4 => DMA32 or DMA or NORMAL -0x5 => BAD (DMA+DMA32) -0x6 => BAD (HIGHMEM+DMA32) -0x7 => BAD (HIGHMEM+DMA32+DMA) -0x8 => NORMAL (MOVABLE+0) -0x9 => DMA or NORMAL (MOVABLE+DMA) -0xa => MOVABLE (Movable is valid only if HIGHMEM is set too) -0xb => BAD (MOVABLE+HIGHMEM+DMA) -0xc => DMA32 (MOVABLE+DMA32) -0xd => BAD (MOVABLE+DMA32+DMA) -0xe => BAD (MOVABLE+DMA32+HIGHMEM) -0xf => BAD (MOVABLE+DMA32+HIGHMEM+DMA) - -GFP_ZONES_SHIFT must be <= 2 on 32 bit platforms. -``` - - - -很有趣的一点是,没有\__GFP_NORMAL常数,而内存分配的主要负担却落到ZONE_NORMAL内存域 - -内核考虑到这一点, 提供了一个函数gfp_zone来计算与给定分配标志兼容的最高内存域. 那么内存分配可以从该内存域或更低的内存域进行, 该函数定义在[include/linux/gfp.h?v=4.7, line 394](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L394) - -```cpp -static inline enum zone_type gfp_zone(gfp_t flags) -{ - enum zone_type z; - int bit = (__force int) (flags & GFP_ZONEMASK); - - z = (GFP_ZONE_TABLE >> (bit * GFP_ZONES_SHIFT)) & - ((1 << GFP_ZONES_SHIFT) - 1); - VM_BUG_ON((GFP_ZONE_BAD >> bit) & 1); - return z; -} -``` - -其中GFP_ZONES_SHIFT的定义如下, 在[include/linux/gfp.h?v=4.7, line 337](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L337) - - -```cpp -#if defined(CONFIG_ZONE_DEVICE) && (MAX_NR_ZONES-1) <= 4 -/* ZONE_DEVICE is not a valid GFP zone specifier */ -#define GFP_ZONES_SHIFT 2 -#else -#define GFP_ZONES_SHIFT ZONES_SHIFT -#endif - -#if 16 * GFP_ZONES_SHIFT > BITS_PER_LONG -#error GFP_ZONES_SHIFT too large to create GFP_ZONE_TABLE integer -#endif -``` - - -由于内存域修饰符的解释方式不是那么直观, 表3-7给出了该函数结果的一个例子, 其中DMA和DMA32内存域相同. 假定在下文中没有设置\__GFP_MOVABLE修饰符. - -| 修饰符 | 扫描的内存域 | -|:-------:|:--------------:| -| 无 | ZONE_NORMAL、ZONE_DMA | -| \__GFP_DMA | ZONE_DMA | -| \__GFP_DMA & \__GFP_HIGHMEM | ZONE_DMA | -| \__GFP_HIGHMEM | ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA | - -* 如果\__GFP_DMA和\__GFP_HIGHMEM都没有设置, 则首先扫描ZONE_NORMAL, 后面是ZONE_DMA - -* 如果设置了\__GFP_HIGHMEM没有设置__GFP_DMA,则结果是从ZONE_HIGHMEM开始扫描所有3个内存域。= - -* 如果设置了__GFP_DMA,那么\__GFP_HIGHMEM设置与否没有关系. 只有ZONE_DMA用于3种情形. 这是合理的, 因为同时使用\__GFP_HIGHMEM和__GFP_DMA没有意义. 高端内存从来都不适用于DMA - - -设置\__GFP_MOVABLE不会影响内核的决策,除非它与\__GFP_HIGHMEM同时指定. 在这种情况下, 会使用特殊的虚拟内存域ZONE_MOVABLE满足内存分配请求. 对前文描述的内核的反碎片策略而言, 这种行为是必要的. - -除了内存域修饰符之外, 掩码中还可以设置一些标志. - -下图中给出了掩码的布局,以及与各个比特位置关联的常数. \__GFP_DMA32出现了几次,因为它可能位于不同的地方. - - -![GFP掩码的布局](../images/gfp_flag_mask.png) - - - -与内存域修饰符相反, 这些额外的标志并不限制从哪个物理内存段分配内存, 但确实可以改变分配器的行为. 例如, 它们可以修改查找空闲内存时的积极程度. - - -##4.3 分配页 -------- - - -###4.3.1 内存分配统一到alloc_pages接口 -------- - -通过使用标志、内存域修饰符和各个分配函数,内核提供了一种非常灵活的内存分配体系.尽管如此, 所有接口函数都可以追溯到一个简单的基本函数(alloc_pages_node) - -分配单页的函数[`alloc_page`](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L483)和[`__get_free_page`](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L500), 还有[`__get_dma_pages`](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L503)是借助于宏定义的. - -```cpp -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L483 -#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0) - -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L500 -#define __get_free_page(gfp_mask) \ - __get_free_pages((gfp_mask), 0)` - -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L503 -#define __get_dma_pages(gfp_mask, order) \ - __get_free_pages((gfp_mask) | GFP_DMA, (order)) -``` - -[`get_zeroed_page`](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3900)的实现也没什么困难, 对`__get_free_pages`使用`__GFP_ZERO`标志,即可分配填充字节0的页. 再返回与页关联的内存区地址即可. - - -```cpp -// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3900 -unsigned long get_zeroed_page(gfp_t gfp_mask) -{ - return __get_free_pages(gfp_mask | __GFP_ZERO, 0); -} -EXPORT_SYMBOL(get_zeroed_page); -``` - - -[`__get_free_pages`](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883)调用`alloc_pages`完成内存分配, 而alloc_pages又借助于alloc_pages_node - -[`__get_free_pages`](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883)函数的定义在[mm/page_alloc.c?v=4.7, line 3883](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883) - -```cpp -// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883 -unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) -{ - struct page *page; - - /* - * __get_free_pages() returns a 32-bit address, which cannot represent - * a highmem page - */ - VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0); - - page = alloc_pages(gfp_mask, order); - if (!page) - return 0; - return (unsigned long) page_address(page); -} -EXPORT_SYMBOL(__get_free_pages); -``` - -在这种情况下, 使用了一个普通函数而不是宏, 因为`alloc_pages`返回的`page`实例需要使用辅助 - -函数`page_address`转换为内存地址. 在这里,只要知道该函数可根据`page`实例计算相关页的线性内存地址即可. 对高端内存页这是有问题的 - - - -这样, 就完成了所有分配内存的API函数到公共的基础函数`alloc_pages`的统一 - - -![伙伴系统中各个分配函数之间的关系](../images/alloc_pages.png) - - -所有体系结构都必须实现的标准函数`clear_page`, 可帮助alloc_pages对页填充字节0, 实现如下表所示 - -| x86 | arm | -|:----:|:-----:| -| [arch/x86/include/asm/page_32.h?v=4.7, line 24](http://lxr.free-electrons.com/source/arch/x86/include/asm/page_32.h?v=4.7#L24) | [arch/arm/include/asm/page.h?v=4.7#L14](http://lxr.free-electrons.com/source/arch/arm/include/asm/page.h?v=4.7#L142)
[arch/arm/include/asm/page-nommu.h](http://lxr.free-electrons.com/source/arch/arm/include/asm/page-nommu.h?v=4.7#L20) | - - -###4.3.2 alloc_pages函数分配页 -------- - - -既然所有的内存分配API函数都可以追溯掉`alloc_page`函数, 从某种意义上说,该函数是伙伴系统主要实现的"发射台". - - -`alloc_pages`函数的定义是依赖于NUMA或者UMA架构的, 定义如下 - - -```cpp -#ifdef CONFIG_NUMA - -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L465 -static inline struct page * -alloc_pages(gfp_t gfp_mask, unsigned int order) -{ - return alloc_pages_current(gfp_mask, order); -} - -#else - -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L476 -#define alloc_pages(gfp_mask, order) \ - alloc_pages_node(numa_node_id(), gfp_mask, order) -#endif -``` - - -UMA结构下的`alloc_pages`是通过`alloc_pages_node`函数实现的, 下面我们看看`alloc_pages_node`函数的定义, 在[include/linux/gfp.h?v=4.7, line 448](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L448) - - -```cpp -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L448 -/* - * Allocate pages, preferring the node given as nid. When nid == NUMA_NO_NODE, - * prefer the current CPU's closest node. Otherwise node must be valid and - * online. - */ -static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask, - unsigned int order) -{ - if (nid == NUMA_NO_NODE) - nid = numa_mem_id(); - - return __alloc_pages_node(nid, gfp_mask, order); -} -```` - -它只是执行了一个简单的检查, 如果指定负的结点ID(不存在, 即[NUMA_NO_NODE = -1](http://lxr.free-electrons.com/source/include/linux/numa.h?v=4.7#L13)), 内核自动地使用当前执行CPU对应的结点nid = [numa_mem_id();](http://lxr.free-electrons.com/source/include/linux/topology.h?v=4.7#L137), 然后调用`__alloc_pages_node`函数进行了内存分配 - - - -`__alloc_pages_node`函数定义在[include/linux/gfp.h?v=4.7, line 435)](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L435), 如下所示 - -```cpp -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L435 -/* - * Allocate pages, preferring the node given as nid. The node must be valid and - * online. For more general interface, see alloc_pages_node(). - */ -static inline struct page * -__alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order) -{ - VM_BUG_ON(nid < 0 || nid >= MAX_NUMNODES); - VM_WARN_ON(!node_online(nid)); - - return __alloc_pages(gfp_mask, order, node_zonelist(nid, gfp_mask)); -} -``` - -内核假定传递给改alloc_pages_node函数的结点nid是被激活, 即online的.但是为了安全它还是检查并警告内存结点不存在的情况. 接下来的工作委托给__alloc_pages, 只需传递一组适当的参数, 其中包括节点nid的备用内存域列表zonelist. - - -现在`__alloc_pages`函数没什么特别的, 它直接将自己的所有信息传递给`__alloc_pages_nodemask`来完成内存的分配 - -```cpp -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L428 -static inline struct page * -__alloc_pages(gfp_t gfp_mask, unsigned int order, - struct zonelist *zonelist) -{ - return __alloc_pages_nodemask(gfp_mask, order, zonelist, NULL); -} -``` - -###4.3.3 伙伴系统的心脏__alloc_pages_nodemask -------- - -内核源代码将`__alloc_pages`称之为"伙伴系统的心脏"(`the 'heart' of the zoned buddy allocator``), 因为它处理的是实质性的内存分配. - -由于"心脏"的重要性, 我将在下文详细介绍该函数. - -`__alloc_pages`函数定义在[include/linux/gfp.h?v=4.7#L428](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L428) - - - -```cpp -// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3779 -/* - * This is the 'heart' of the zoned buddy allocator. - */ -struct page * -__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, - struct zonelist *zonelist, nodemask_t *nodemask) -{ - struct page *page; - unsigned int cpuset_mems_cookie; - unsigned int alloc_flags = ALLOC_WMARK_LOW|ALLOC_FAIR; - gfp_t alloc_mask = gfp_mask; /* The gfp_t that was actually used for allocation */ - struct alloc_context ac = { - .high_zoneidx = gfp_zone(gfp_mask), - .zonelist = zonelist, - .nodemask = nodemask, - .migratetype = gfpflags_to_migratetype(gfp_mask), - }; - - if (cpusets_enabled()) { - alloc_mask |= __GFP_HARDWALL; - alloc_flags |= ALLOC_CPUSET; - if (!ac.nodemask) - ac.nodemask = &cpuset_current_mems_allowed; - } - - gfp_mask &= gfp_allowed_mask; - - lockdep_trace_alloc(gfp_mask); - - might_sleep_if(gfp_mask & __GFP_DIRECT_RECLAIM); - - if (should_fail_alloc_page(gfp_mask, order)) - return NULL; - - /* - * Check the zones suitable for the gfp_mask contain at least one - * valid zone. It's possible to have an empty zonelist as a result - * of __GFP_THISNODE and a memoryless node - */ - if (unlikely(!zonelist->_zonerefs->zone)) - return NULL; - - if (IS_ENABLED(CONFIG_CMA) && ac.migratetype == MIGRATE_MOVABLE) - alloc_flags |= ALLOC_CMA; - -retry_cpuset: - cpuset_mems_cookie = read_mems_allowed_begin(); - - /* Dirty zone balancing only done in the fast path */ - ac.spread_dirty_pages = (gfp_mask & __GFP_WRITE); - - /* - * The preferred zone is used for statistics but crucially it is - * also used as the starting point for the zonelist iterator. It - * may get reset for allocations that ignore memory policies. - */ - ac.preferred_zoneref = first_zones_zonelist(ac.zonelist, - ac.high_zoneidx, ac.nodemask); - if (!ac.preferred_zoneref) { - page = NULL; - goto no_zone; - } - - /* First allocation attempt */ - page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac); - if (likely(page)) - goto out; - - /* - * Runtime PM, block IO and its error handling path can deadlock - * because I/O on the device might not complete. - */ - alloc_mask = memalloc_noio_flags(gfp_mask); - ac.spread_dirty_pages = false; - - /* - * Restore the original nodemask if it was potentially replaced with - * &cpuset_current_mems_allowed to optimize the fast-path attempt. - */ - if (cpusets_enabled()) - ac.nodemask = nodemask; - page = __alloc_pages_slowpath(alloc_mask, order, &ac); - -no_zone: - /* - * When updating a task's mems_allowed, it is possible to race with - * parallel threads in such a way that an allocation can fail while - * the mask is being updated. If a page allocation is about to fail, - * check if the cpuset changed during allocation and if so, retry. - */ - if (unlikely(!page && read_mems_allowed_retry(cpuset_mems_cookie))) { - alloc_mask = gfp_mask; - goto retry_cpuset; - } - -out: - if (kmemcheck_enabled && page) - kmemcheck_pagealloc_alloc(page, order, gfp_mask); - - trace_mm_page_alloc(page, order, alloc_mask, ac.migratetype); - - return page; -} -EXPORT_SYMBOL(__alloc_pages_nodemask); -``` - -##4.4 __free_pages - - -类似地,内存释放函数也可以归约到一个主要的函数(\__free_pages), 只是用不同的参数调用而已 - -前面我们讲过内核释放的两个主要函数有\__free_page和free_page, 它们的定义在[include/linux/gfp.h?v=4.7#L519](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L519) - - - -```cpp -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L519 -#define __free_page(page) __free_pages((page), 0) -#define free_page(addr) free_pages((addr), 0) -``` - -而free_pages是通过__free_pages来完成内存释放的, 参见[mm/page_alloc.c?v=4.7#L3918](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3918) - - - -```cpp -void free_pages(unsigned long addr, unsigned int order) -{ - if (addr != 0) { - VM_BUG_ON(!virt_addr_valid((void *)addr)); - __free_pages(virt_to_page((void *)addr), order); - } -} -``` - - -`free_pages`和`__free_pages`之间的关系通过函数而不是宏建立, 因为首先必须将虚拟地址转换为指向`struct page`的指针 - - - -`virt_to_page`将虚拟内存地址转换为指向page实例的指针. 基本上, 这是讲解内存分配函数时介绍的page_address辅助函数的逆过程. - - -下图以图形化方式综述了各个内存释放函数之间的关系 - - -![伙伴系统各个内存释放函数之间的关系](../images/__free_pages.png) - - - - +服务器体系与共享存储器架构 +======= + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + + + + +在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法. + +Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述. + +伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者. + +* 内核如何记住哪些内存块是空闲的 + +* 分配空闲页面的方法 + +* 影响分配器行为的众多标识位 + +* 内存碎片的问题和分配器如何处理碎片 + + + + +#2 伙伴系统的结构 +------- + + +##2.1 伙伴系统数据结构 +------- + +系统内存中的每个物理内存页(页帧),都对应于一个struct page实例, 每个内存域都关联了一个struct zone的实例,其中保存了用于管理伙伴数据的主要数数组 + + +```cpp +// http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L324 +struct zone +{ + /* free areas of different sizes */ + struct free_area free_area[MAX_ORDER]; +}; +``` + +struct free_area是一个伙伴系统的辅助数据结构, 它定义在[include/linux/mmzone.h?v=4.7, line 88](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L88) + +```cpp +struct free_area { + struct list_head free_list[MIGRATE_TYPES]; +unsigned long nr_free; +}; +``` + +| 字段 | 描述 | +|:-----:|:-----:| +| free_list | 是用于连接空闲页的链表. 页链表包含大小相同的连续内存区 | +| nr_free | 指定了当前内存区中空闲页块的数目(对0阶内存区逐页计算,对1阶内存区计算页对的数目,对2阶内存区计算4页集合的数目,依次类推 | + + + +伙伴系统的分配器维护空闲页面所组成的块, 这里每一块都是2的方幂个页面, 方幂的指数称为**阶**. + + +阶是伙伴系统中一个非常重要的术语. 它描述了内存分配的数量单位. 内存块的长度是$2^order$, 其中order的范围从0到MAX_ORDER + + +zone->free_area[MAX_ORDER]数组中阶作为各个元素的索引, 用于指定对应链表中的连续内存区包含多少个页帧. + +* 数组中第0个元素的阶为0, 它的free_list链表域指向具有包含区为单页($2^0=1$)的内存页面链表 + +* 数组中第1个元素的free_list域管理的内存区为两页($2^1=2$) + +* 第3个管理的内存区为4页, 依次类推. + +* 直到$2^{MAX_ORDER-1}$个页面大小的块 + +![空闲页快](../images/order_free_list.png) + + +##2.2 最大阶MAX_ORDER与FORCE_MAX_ZONEORDER配置选项 +------- + + +一般来说`MAX_ORDER`默认定义为11, 这意味着一次分配可以请求的页数最大是2^11=2048, 参见[include/linux/mmzone.h?v=4.7, line 22](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L22) + +```cpp +/* Free memory management - zoned buddy allocator. */ +#ifndef CONFIG_FORCE_MAX_ZONEORDER +#define MAX_ORDER 11 +#else +#define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER +#endif +#define MAX_ORDER_NR_PAGES (1 << (MAX_ORDER - 1)) +``` + + + +但如果特定于体系结构的代码设置了`FORCE_MAX_ZONEORDER`配置选项, 该值也可以手工改变 + +例如,IA-64系统上巨大的地址空间可以处理`MAX_ORDER = 18`的情形,而ARM或v850系统则使用更小的值(如8或9). 但这不一定是由计算机支持的内存数量比较小引起的,也可能是内存对齐方式的要求所导致 + + +可以参考一些架构的Kconfig文件如下 + +| arm | arm64 | +|:----:|:-----:|:-------:| +| [arch/arm/Kconfig?v=4.7, line 1696](http://lxr.free-electrons.com/source/arch/arm/Kconfig?v=4.7#L1696) | [arch/arm64/Kconfig?v=4.7, line 679](http://lxr.free-electrons.com/source/arch/arm64/Kconfig?v=4.7#L679) + + +比如[arm64体系结构的Kconfig配置文件的描述](http://lxr.free-electrons.com/source/arch/arm64/Kconfig?v=4.7#L679) | + +```cpp +config FORCE_MAX_ZONEORDER +int +default "14" if (ARM64_64K_PAGES && TRANSPARENT_HUGEPAGE) +default "12" if (ARM64_16K_PAGES && TRANSPARENT_HUGEPAGE) +default "11"` +``` + + + + + + +##2.3 内存区是如何连接的 +------- + + +内存区中第1页内的链表元素, 可用于将内存区维持在链表中。因此,也不必引入新的数据结构来管理物理上连续的页,否则这些页不可能在同一内存区中. 如下图所示 + + +![伙伴系统中相互连接的内存区](../images/buddy_node_connect.png) + + +伙伴不必是彼此连接的. 如果一个内存区在分配其间分解为两半, 内核会自动将未用的一半加入到对应的链表中. + +如果在未来的某个时刻, 由于内存释放的缘故, 两个内存区都处于空闲状态, 可通过其地址判断其是否为伙伴. 管理工作较少, 是伙伴系统的一个主要优点. + +基于伙伴系统的内存管理专注于某个结点的某个内存域, 例如, DMA或高端内存域. 但所有内存域和结点的伙伴系统都通过备用分配列表连接起来. + +下图说明了这种关系. + +![伙伴系统和内存域/结点之间的关系](../images/buddy_and_node_zone.png) + + +最后要注意, 有关伙伴系统和当前状态的信息可以在/proc/buddyinfo中获取 + +![伙伴系统和当前状态的信息](../images/buddy_info.png) + + +上述输出给出了各个内存域中每个分配阶中空闲项的数目, 从左至右, 阶依次升高. 上面给出的信息取自4 GiB物理内存的AMD64系统. + +#传统伙伴系统算法 +------- + +在内核分配内存时, 必须记录页帧的已分配或空闲状态, 以免两个进程使用同样的内存区域. 由于内存分配和释放非常频繁, 内核还必须保证相关操作尽快完成. 内核可以只分配完整的页帧. 将内存划分为更小的部分的工作, 则委托给用户空间中的标准库. 标准库将来源于内核的页帧拆分为小的区域, 并为进程分配内存. + + +内核中很多时候要求分配连续页. 为快速检测内存中的连续区域, 内核采用了一种古老而历经检验的技术: **伙伴系统** + +系统中的空闲内存块总是两两分组, 每组中的两个内存块称作伙伴. 伙伴的分配可以是彼此独立的. 但如果两个伙伴都是空闲的, 内核会将其合并为一个更大的内存块, 作为下一层次上某个内存块的伙伴. + +下图示范了该系统, 图中给出了一对伙伴, 初始大小均为8页. 即系统中所有的页面都是8页的. + +![伙伴系统](../images/buddy_system.png) + +内核对所有大小相同的伙伴(1、2、4、8、16或其他数目的页),都放置到同一个列表中管理. 各有8页的一对伙伴也在相应的列表中. + +如果系统现在需要8个页帧, 则将16个页帧组成的块拆分为两个伙伴. 其中一块用于满足应用程序的请求, 而剩余的8个页帧则放置到对应8页大小内存块的列表中. + +如果下一个请求只需要2个连续页帧, 则由8页组成的块会分裂成2个伙伴, 每个包含4个页帧. 其中一块放置回伙伴列表中,而另一个再次分裂成2个伙伴, 每个包含2页。其中一个回到伙伴系统,另一个则传递给应用程序. + +在应用程序释放内存时, 内核可以直接检查地址, 来判断是否能够创建一组伙伴, 并合并为一个更大的内存块放回到伙伴列表中, 这刚好是内存块分裂的逆过程。这提高了较大内存块可用的可能性. + +在系统长期运行时,服务器运行几个星期乃至几个月是很正常的,许多桌面系统也趋向于长期开机运行,那么会发生称为碎片的内存管理问题。频繁的分配和释放页帧可能导致一种情况:系统中有若干页帧是空闲的,但却散布在物理地址空间的各处。换句话说,系统中缺乏连续页帧组成的较大的内存块,而从性能上考虑,却又很需要使用较大的连续内存块。通过伙伴系统可以在某种程度上减少这种效应,但无法完全消除。如果在大块的连续内存中间刚好有一个页帧分配出去,很显然这两块空闲的内存是无法合并的. + +在内核版本2.6.24之后, 增加了一些有效措施来防止内存碎片. + + +#3 避免碎片 +------- + +在第1章给出的简化说明中, 一个双链表即可满足伙伴系统的所有需求. 在内核版本2.6.23之前, 的确是这样. 但在内核2.6.24开发期间, 内核开发者对伙伴系统的争论持续了相当长时间. 这是因为伙伴系统是内核最值得尊敬的一部分,对它的改动不会被大家轻易接受 + + +##3.1 内存碎片 + + +伙伴系统的基本原理已经在第1章中讨论过,其方案在最近几年间确实工作得非常好。但在Linux内存管理方面,有一个长期存在的问题:在系统启动并长期运行后,物理内存会产生很多碎片。该情形如下图所示 + +![物理内存的碎片](../images/physical_memory_fragmentation.png) + +假定内存由60页组成,这显然不是超级计算机,但用于示例却足够了。左侧的地址空间中散布着空闲页。尽管大约25%的物理内存仍然未分配,但最大的连续空闲区只有一页. 这对用户空间应用程序没有问题:其内存是通过页表映射的,无论空闲页在物理内存中的分布如何,应用程序看到的内存 +似乎总是连续的。右图给出的情形中,空闲页和使用页的数目与左图相同,但所有空闲页都位于一个连续区中。 + +但对内核来说,碎片是一个问题. 由于(大多数)物理内存一致映射到地址空间的内核部分, 那么在左图的场景中, 无法映射比一页更大的内存区. 尽管许多时候内核都分配的是比较小的内存, 但也有时候需要分配多于一页的内存. 显而易见, 在分配较大内存的情况下, 右图中所有已分配页和空闲页都处于连续内存区的情形,是更为可取的. + +很有趣的一点是, 在大部分内存仍然未分配时, 就也可能发生碎片问题. 考虑图3-25的情形. + +只分配了4页,但可分配的最大连续区只有8页,因为伙伴系统所能工作的分配范围只能是2的幂次. + + +![物理内存的碎片](../images/some_memory_fragmentation.png) + +我提到内存碎片只涉及内核,这只是部分正确的。大多数现代CPU都提供了使用巨型页的可能性,比普通页大得多。这对内存使用密集的应用程序有好处。在使用更大的页时,地址转换后备缓冲器只需处理较少的项,降低了TLB缓存失效的可能性。但分配巨型页需要连续的空闲物理内存! + +很长时间以来,物理内存的碎片确实是Linux的弱点之一。尽管已经提出了许多方法,但没有哪个方法能够既满足Linux需要处理的各种类型工作负荷提出的苛刻需求,同时又对其他事务影响不大。 + + + + +##3.2 依据可移动性组织页 +------- + + +在内核2.6.24开发期间,防止碎片的方法最终加入内核。在我讨论具体策略之前,有一点需要澄清。 + +文件系统也有碎片,该领域的碎片问题主要通过碎片合并工具解决。它们分析文件系统,重新排序已分配存储块,从而建立较大的连续存储区. 理论上,该方法对物理内存也是可能的,但由于许多物理内存页不能移动到任意位置,阻碍了该方法的实施。因此,内核的方法是反碎片(anti-fragmentation), 即试图从最初开始尽可能防止碎片. + + +反碎片的工作原理如何? + + + +为理解该方法,我们必须知道内核将已分配页划分为下面3种不同类型。 + + +| 页面类型 | 描述 | 举例 | +|:---------:|:-----:|:-----:| +| 不可移动页 | 在内存中有固定位置, **不能移动**到其他地方. | 核心内核分配的大多数内存属于该类别 | +| 可移动页 | **可以随意地移动**. | 属于用户空间应用程序的页属于该类别. 它们是通过页表映射的
如果它们复制到新位置,页表项可以相应地更新,应用程序不会注意到任何事 | +| 可回收页 | **不能直接移动, 但可以删除, 其内容可以从某些源重新生成**. | 例如,映射自文件的数据属于该类别
kswapd守护进程会根据可回收页访问的频繁程度,周期性释放此类内存. , 页面回收本身就是一个复杂的过程. 内核会在可回收页占据了太多内存时进行回收, 在内存短缺(即分配失败)时也可以发起页面回收. | + + + +页的可移动性,依赖该页属于3种类别的哪一种. 内核使用的**反碎片技术**, 即基于将具有相同可移动性的页分组的思想. + + +为什么这种方法有助于减少碎片? + + +由于页无法移动, 导致在原本几乎全空的内存区中无法进行连续分配. 根据页的可移动性, 将其分配到不同的列表中, 即可防止这种情形. 例如, 不可移动的页不能位于可移动内存区的中间, 否则就无法从该内存区分配较大的连续内存块. + + +想一下, 上图中大多数空闲页都属于可回收的类别, 而分配的页则是不可移动的. 如果这些页聚集到两个不同的列表中, 如下图所示. 在不可移动页中仍然难以找到较大的连续空闲空间, 但对可回收的页, 就容易多了. + + +![减少内存碎片](../images/little_memory_fragmentation.png) + + +但要注意, 从最初开始, 内存并未划分为可移动性不同的区. 这些是在运行时形成的. 内核的另一种方法确实将内存分区, 分别用于可移动页和不可移动页的分配, 我会下文讨论其工作原理. 但这种划分对这里描述的方法是不必要的 + + + +##3.3 避免碎片数据结构 +------- + + + +###3.3.1 迁移类型 +------- + + +尽管内核使用的反碎片技术卓有成效,它对伙伴分配器的代码和数据结构几乎没有影响。内核定义了一些枚举常量(早期用宏来实现)来表示不同的迁移类型, 参见[include/linux/mmzone.h?v=4.7, line 38](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L38) + +```cpp +enum { + MIGRATE_UNMOVABLE, + MIGRATE_MOVABLE, + MIGRATE_RECLAIMABLE, + MIGRATE_PCPTYPES, /* the number of types on the pcp lists */ + MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES, +#ifdef CONFIG_CMA + /* + * MIGRATE_CMA migration type is designed to mimic the way + * ZONE_MOVABLE works. Only movable pages can be allocated + * from MIGRATE_CMA pageblocks and page allocator never + * implicitly change migration type of MIGRATE_CMA pageblock. + * + * The way to use it is to change migratetype of a range of + * pageblocks to MIGRATE_CMA which can be done by + * __free_pageblock_cma() function. What is important though + * is that a range of pageblocks must be aligned to + * MAX_ORDER_NR_PAGES should biggest page be bigger then + * a single pageblock. + */ + MIGRATE_CMA, +#endif +#ifdef CONFIG_MEMORY_ISOLATION + MIGRATE_ISOLATE, /* can't allocate from here */ +#endif + MIGRATE_TYPES +}; +``` + + +| 宏 | 类型 | +|:----:|:-----:| +| MIGRATE_UNMOVABLE | 不可移动页 | +| MIGRATE_MOVABLE | 可移动页 | +| MIGRATE_RECLAIMABLE | 可回收页 | +| MIGRATE_PCPTYPES | 是per_cpu_pageset, 即用来表示每CPU页框高速缓存的数据结构中的链表的迁移类型数目 | +| MIGRATE_HIGHATOMIC | = MIGRATE_PCPTYPES, 在罕见的情况下,内核需要分配一个高阶的页面块而不能休眠.如果向具有特定可移动性的列表请求分配内存失败,这种紧急情况下可从MIGRATE_HIGHATOMIC中分配内存 | +| MIGRATE_CMA | Linux内核最新的连续内存分配器(CMA), 用于避免预留大块内存 | +| MIGRATE_ISOLATE | 是一个特殊的虚拟区域, 用于跨越NUMA结点移动物理内存页. 在大型系统上, 它有益于将物理内存页移动到接近于使用该页最频繁的CPU. | +| MIGRATE_TYPES | 只是表示迁移类型的数目, 也不代表具体的区域 | + +对于MIGRATE_CMA类型, 其中在我们使用ARM等嵌入式Linux系统的时候, 一个头疼的问题是GPU, Camera, HDMI等都需要预留大量连续内存,这部分内存平时不用,但是一般的做法又必须先预留着. 目前, Marek Szyprowski和Michal Nazarewicz实现了一套全新的Contiguous Memory Allocator. 通过这套机制, 我们可以做到不预留内存,这些内存平时是可用的,只有当需要的时候才被分配给Camera,HDMI等设备. 参照[宋宝华--Linux内核最新的连续内存分配器(CMA)——避免预留大块内存](http://21cnbao.blog.51cto.com/109393/898846/), 内核为此提供了函数is_migrate_cma来检测当前类型是否为MIGRATE_CMA, 该函数定义在[include/linux/mmzone.h?v=4.7, line 69](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L69) + +```cpp +/* In mm/page_alloc.c; keep in sync also with show_migration_types() there */ +extern char * const migratetype_names[MIGRATE_TYPES]; + +#ifdef CONFIG_CMA +# define is_migrate_cma(migratetype) unlikely((migratetype) == MIGRATE_CMA) +#else +# define is_migrate_cma(migratetype) false +#endif +``` + + + + +对伙伴系统数据结构的主要调整, 是将空闲列表分解为MIGRATE_TYPE个列表, 可以参见free_area的定义[include/linux/mmzone.h?v=4.7, line 88](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L88) + +```cpp +struct free_area +{ + struct list_head free_list[MIGRATE_TYPES]; +unsigned long nr_free; +}; +``` + +* nr_free统计了所有列表上空闲页的数目,而每种迁移类型都对应于一个空闲列表 + + +宏for_each_migratetype_order(order, type)可用于迭代指定迁移类型的所有分配阶 + +```cpp +#define for_each_migratetype_order(order, type) \ + for (order = 0; order < MAX_ORDER; order++) \ + for (type = 0; type < MIGRATE_TYPES; type++) +``` + + +###3.3.2 迁移备用列表fallbacks +------- + + + +如果内核无法满足针对某一给定迁移类型的分配请求, 会怎么样? + + + +此前已经出现过一个类似的问题, 即特定的NUMA内存域无法满足分配请求时. 我们需要从其他内存域中选择一个代价最低的内存域完成内存的分配, 因此内核在内存的结点pg_data_t中提供了一个备用内存域列表zonelists. + +内核在内存迁移的过程中处理这种情况下的做法是类似的. 提供了一个备用列表fallbacks, 规定了在指定列表中无法满足分配请求时. 接下来应使用哪一种迁移类型, 定义在[mm/page_alloc.c?v=4.7, line 1799](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L1799) + + +```cpp +/* + * This array describes the order lists are fallen back to when + * the free lists for the desirable migrate type are depleted + * 该数组描述了指定迁移类型的空闲列表耗尽时 + * 其他空闲列表在备用列表中的次序 + */ +static int fallbacks[MIGRATE_TYPES][4] = { + // 分配不可移动页失败的备用列表 + [MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_TYPES }, + // 分配可回收页失败时的备用列表 + [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_TYPES }, + // 分配可移动页失败时的备用列表 + [MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_TYPES }, +#ifdef CONFIG_CMA + [MIGRATE_CMA] = { MIGRATE_TYPES }, /* Never used */ +#endif +#ifdef CONFIG_MEMORY_ISOLATION + [MIGRATE_ISOLATE] = { MIGRATE_TYPES }, /* Never used */ +#endif +}; +``` + +>该数据结构大体上是自明的 : +> +>每一行对应一个类型的备用搜索域的顺序, 在内核想要分配不可移动页`MIGRATE_UNMOVABLE`时, 如果对应链表为空, 则遍历fallbacks[MIGRATE_UNMOVABLE], 首先后退到可回收页链表`MIGRATE_RECLAIMABLE`, 接下来到可移动页链表`MIGRATE_MOVABLE`, 最后到紧急分配链表`MIGRATE_TYPES`. + + +##3.3.3 pageblock_order变量 +------- + + +全局变量和辅助函数尽管页可移动性分组特性总是编译到内核中,但只有在系统中有足够内存可以分配到多个迁移类型对应的链表时,才是有意义的。由于每个迁移链表都应该有适当数量的内存,内核需要定义"适当"的概念. 这是通过两个全局变量pageblock_order和pageblock_nr_pages提供的. 第一个表示内核认为是"大"的一个分配阶, pageblock_nr_pages则表示该分配阶对应的页数。如果体系结构提供了巨型页机制, 则pageblock_order通常定义为巨型页对应的分配阶. 定义在[include/linux/pageblock-flags.h?v=4.7, line 44](http://lxr.free-electrons.com/source/include/linux/pageblock-flags.h?v=4.7#L42) + + +```cpp +#ifdef CONFIG_HUGETLB_PAGE + + #ifdef CONFIG_HUGETLB_PAGE_SIZE_VARIABLE + + /* Huge page sizes are variable */ + extern unsigned int pageblock_order; + + #else /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */ + + /* Huge pages are a constant size */ + #define pageblock_order HUGETLB_PAGE_ORDER + + #endif /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */ + +#else /* CONFIG_HUGETLB_PAGE */ + + /* If huge pages are not used, group by MAX_ORDER_NR_PAGES */ + #define pageblock_order (MAX_ORDER-1) + +#endif /* CONFIG_HUGETLB_PAGE */ + +#define pageblock_nr_pages (1UL << pageblock_order) +``` + +在IA-32体系结构上, 巨型页长度是4MB, 因此每个巨型页由1024个普通页组成, 而HUGETLB_PAGE_ORDER则定义为10. 相比之下, IA-64体系结构允许设置可变的普通和巨型页长度, 因此HUGETLB_PAGE_ORDER的值取决于内核配置. + +如果体系结构不支持巨型页, 则将其定义为第二高的分配阶, 即`MAX_ORDER - 1` + +```cpp +/* If huge pages are not used, group by MAX_ORDER_NR_PAGES */ +#define pageblock_order (MAX_ORDER-1) +``` + +如果各迁移类型的链表中没有一块较大的连续内存, 那么页面迁移不会提供任何好处, 因此在可用内存太少时内核会关闭该特性. 这是在build_all_zonelists函数中检查的, 该函数用于初始化内存域列表. 如果没有足够的内存可用, 则全局变量[`page_group_by_mobility_disabled`](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L79)设置为0, 否则设置为1. + +内核如何知道给定的分配内存属于何种迁移类型? + +我们将在以后讲解, 有关各个内存分配的细节都通过分配掩码指定. + +内核提供了两个标志,分别用于表示分配的内存是可移动的(\__GFP_MOVABLE)或可回收的(\__GFP_RECLAIMABLE). + + + +###3.3.4 gfpflags_to_migratetype函数 +------- + + +如果这些标志都没有设置, 则分配的内存假定为不可移动的. 辅助函数gfpflags_to_migratetype可用于转换分配标志及对应的迁移类型, 该函数定义在[include/linux/gfp.h?v=4.7, line 266](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L266) + +```cpp +static inline int gfpflags_to_migratetype(const gfp_t gfp_flags) +{ + VM_WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK); + BUILD_BUG_ON((1UL << GFP_MOVABLE_SHIFT) != ___GFP_MOVABLE); + BUILD_BUG_ON((___GFP_MOVABLE >> GFP_MOVABLE_SHIFT) != MIGRATE_MOVABLE); + + if (unlikely(page_group_by_mobility_disabled)) + return MIGRATE_UNMOVABLE; + + /* Group based on mobility */ + return (gfp_flags & GFP_MOVABLE_MASK) >> GFP_MOVABLE_SHIFT; +} +``` + +>linux-2.6.x的内核中转换分配标志及对应的迁移类型的辅助函数为allocflags_to_migratetype, 这个名字会有歧义的, 让我们误以为参数的标识中有alloc flags, 但是其实并不然, 因此后来的内核中将该函数更名为gfpflags_to_migratetype, 参见[Rename it to gfpflags_to_migratetype()](https://patchwork.kernel.org/patch/4291831) + +在2.6.25中为如下接口 + +```cpp +/* Convert GFP flags to their corresponding migrate type */ +static inline int allocflags_to_migratetype(gfp_t gfp_flags) +{ + WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK); + + if (unlikely(page_group_by_mobility_disabled)) + return MIGRATE_UNMOVABLE; + + /* Group based on mobility */ + return (((gfp_flags & __GFP_MOVABLE) != 0) << 1) | + ((gfp_flags & __GFP_RECLAIMABLE) != 0); +} +```` + +如果停用了页面迁移特性, 则所有的页都是不可移动的. 否则. 该函数的返回值可以直接用作free_area.free_list的数组索引. + +###3.3.5 pageblock_flags变量与其函数接口 + +最后要注意, 每个内存域都提供了一个特殊的字段, 可以跟踪包含pageblock_nr_pages个页的内存区的属性. 即zone->pageblock_flags字段, 当前只有与页可移动性相关的代码使用, 参见[include/linux/mmzone.h?v=4.7, line 367](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L367) + + +```cpp +struct zone +{ +#ifndef CONFIG_SPARSEMEM + /* + * Flags for a pageblock_nr_pages block. See pageblock-flags.h. + * In SPARSEMEM, this map is stored in struct mem_section + */ + unsigned long *pageblock_flags; +#endif /* CONFIG_SPARSEMEM */ +}; +``` + +在初始化期间, 内核自动确保对内存域中的每个不同的迁移类型分组, 在pageblock_flags中都分配了足够存储NR_PAGEBLOCK_BITS个比特位的空间。当前,表示一个连续内存区的迁移类型需要3个比特位, 参见[include/linux/pageblock-flags.h?v=4.7, line 28](http://lxr.free-electrons.com/source/include/linux/pageblock-flags.h?v=4.7#L28) + +```cpp +/* Bit indices that affect a whole block of pages */ +enum pageblock_bits { + PB_migrate, + PB_migrate_end = PB_migrate + 3 - 1, + /* 3 bits required for migrate types */ + PB_migrate_skip,/* If set the block is skipped by compaction */ + + /* + * Assume the bits will always align on a word. If this assumption + * changes then get/set pageblock needs updating. + */ + NR_PAGEBLOCK_BITS +}; +``` + + +内核提供`set_pageblock_migratetype`负责设置以page为首的一个内存区的迁移类型, 该函数定义在[mm/page_alloc.c?v=4.7, line 458](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L458), 如下所示 + + +```cpp +void set_pageblock_migratetype(struct page *page, int migratetype) +{ + if (unlikely(page_group_by_mobility_disabled && + migratetype < MIGRATE_PCPTYPES)) + migratetype = MIGRATE_UNMOVABLE; + + set_pageblock_flags_group(page, (unsigned long)migratetype, + PB_migrate, PB_migrate_end); +} +``` + + + +`migratetype`参数可以通过上文介绍的`gfpflags_to_migratetype`辅助函数构建. 请注意很重要的一点, 页的迁移类型是预先分配好的, 对应的比特位总是可用, 与页是否由伙伴系统管理无关. 在释放内存时,页必须返回到正确的迁移链表。这之所以可行,是因为能够从`get_pageblock_migratetype`获得所需的信息. 参见[include/linux/mmzone.h?v=4.7, line 84](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L84) + + +```cpp +#define get_pageblock_migratetype(page) \ + get_pfnblock_flags_mask(page, page_to_pfn(page), \ + PB_migrate_end, MIGRATETYPE_MASK) +``` + + +最后请注意, 在各个迁移链表之间, 当前的页面分配状态可以从`/proc/pagetypeinfo`获得. + + +![proc/pagetypeinfo]() + + + + +##初始化基于可移动性的分组 +------- + + + + +在内存子系统初始化期间, memmap_init_zone负责处理内存域的page实例. 该函数定义在[mm/page_alloc.c?v=4.7, line 5139](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5139), 该函数完成了一些不怎么有趣的标准初始化工作,但其中有一件是实质性的,即所有的页最初都标记为可移动的. 参见[mm/page_alloc.c?v=4.7, line 5224](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5224) + + +```cpp +/* + * Initially all pages are reserved - free ones are freed + * up by free_all_bootmem() once the early boot process is + * done. Non-atomic initialization, single-pass. + */ +void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone, + unsigned long start_pfn, enum memmap_context context) +{ + /* ...... */ + + for (pfn = start_pfn; pfn < end_pfn; pfn++) { + /* ...... */ +not_early: + if (!(pfn & (pageblock_nr_pages - 1))) { + struct page *page = pfn_to_page(pfn); + + __init_single_page(page, pfn, zone, nid); + set_pageblock_migratetype(page, MIGRATE_MOVABLE); + } else { + __init_single_pfn(pfn, zone, nid); + } + } +} +``` + + +在分配内存时, 如果必须"盗取"不同于预定迁移类型的内存区, 内核在策略上倾向于"盗取"更大的内存区. 由于所有页最初都是可移动的, 那么在内核分配不可移动的内存区时, 则必须"盗取". + +实际上, 在启动期间分配可移动内存区的情况较少, 那么分配器有很高的几率分配长度最大的内存区, 并将其从可移动列表转换到不可移动列表. 由于分配的内存区长度是最大的, 因此不会向可移动内存中引入碎片. + +总而言之, 这种做法避免了启动期间内核分配的内存(经常在系统的整个运行时间都不释放)散布到物理内存各处, 从而使其他类型的内存分配免受碎片的干扰,这也是页可移动性分组框架的最重要的目标之一. + + +#4 分配器API +------- + + +##4.1 分配内存的接口 +------- + +就伙伴系统的接口而言, NUMA或UMA体系结构是没有差别的, 二者的调用语法都是相同的. + +所有函数的一个共同点是 : 只能分配2的整数幂个页. + +因此,接口中不像C标准库的malloc函数或bootmem和memblock分配器那样指定了所需内存大小作为参数. 相反, 必须指定的是分配阶, 伙伴系统将在内存中分配$2^order$页. 内核中细粒度的分配只能借助于slab分配器(或者slub、slob分配器), 后者基于伙伴系统 + + +| 内存分配函数 | 功能 | 定义 | +|:-----:|:-----:| +| alloc_pages(mask, order) | 分配$2^order$页并返回一个struct page的实例,表示分配的内存块的起始页 | [NUMA-include/linux/gfp.h, line 466](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L466)
[UMA-include/linux/gfp.h?v=4.7, line 476](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L476) | +| alloc_page(mask) | 是前者在order = 0情况下的简化形式,只分配一页 | [include/linux/gfp.h?v=4.7, line 483](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L483) | +| get_zeroed_page(mask) | 分配一页并返回一个page实例,页对应的内存填充0(所有其他函数,分配之后页的内容是未定义的) | [mm/page_alloc.c?v=4.7, line 3900](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3900)| | +| [__get_free_pages(mask, order)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883)
[__get_free_page(mask)](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L500) | 工作方式与上述函数相同,但返回分配内存块的虚拟地址,而不是page实例 | +| get_dma_pages(gfp_mask, order) | 用来获得适用于DMA的页. | [include/linux/gfp.h?v=4.7, line 503](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L503) | + + +在空闲内存无法满足请求以至于分配失败的情况下,所有上述函数都返回空指针(比如alloc_pages和alloc_page)或者0(比如get_zeroed_page、__get_free_pages和__get_free_page). + +因此内核在各次分配之后都必须检查返回的结果. 这种惯例与设计得很好的用户层应用程序没什么不同, 但在内核中忽略检查会导致严重得多的故障 + + +内核除了伙伴系统函数之外, 还提供了其他内存管理函数. 它们以伙伴系统为基础, 但并不属于伙伴分配器自身. 这些函数包括vmalloc和vmalloc_32, 使用页表将不连续的内存映射到内核地址空间中, 使之看上去是连续的. + +还有一组kmalloc类型的函数, 用于分配小于一整页的内存区. 其实现 +将在本章后续的几节分别讨论。 + + + +## 释放函数 +------- + +有4个函数用于释放不再使用的页,与所述函数稍有不同 + + +| 内存释放函数 | 描述 | +|:--------------:|:-----:| +| [free_page(struct page *)](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L520)
[free_pages(struct page *, order)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3918) | 用于将一个或2order页返回给内存管理子系统。内存区的起始地址由指向该内存区的第一个page实例的指针表示 | +| [__free_page(addr)](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L519)
[__free_pages(addr, order)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3906) | 类似于前两个函数,但在表示需要释放的内存区时,使用了虚拟内存地址而不是page实例 | + + + +##4.2 分配掩码 +------- + + +前述所有函数中强制使用的mask参数,到底是什么语义? + +我们知道Linux将内存划分为内存域. 内核提供了所谓的内存域修饰符(zone modifier)(在掩码的最低4个比特位定义), 来指定从哪个内存域分配所需的页. + + +内核使用宏的方式定义了这些掩码, 一个掩码的定义被划分为3个部分进行定义, 我们会逐步展开来讲解, 参见[include/linux/gfp.h?v=4.7, line 12~374](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L12), 共计26个掩码信息, 因此后面__GFP_BITS_SHIFT = 26. + +```cpp +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7 + +/* line 12 ~ line 44 第一部分 + * 定义可掩码所在位的信息, 每个掩码对应一位为1 + * 定义形式为 #define ___GFP_XXX 0x01u + */ +/* Plain integer GFP bitmasks. Do not use this directly. */ +#define ___GFP_DMA 0x01u +#define ___GFP_HIGHMEM 0x02u +#define ___GFP_DMA32 0x04u +#define ___GFP_MOVABLE 0x08u +/* ...... */ + +/* line 46 ~ line 192 第二部分 + * 定义掩码和MASK信息, 第二部分的某些宏可能是第一部分一个或者几个的组合 + * 定义形式为 #define __GFP_XXX ((__force gfp_t)___GFP_XXX) + */ +#define __GFP_DMA ((__force gfp_t)___GFP_DMA) +#define __GFP_HIGHMEM ((__force gfp_t)___GFP_HIGHMEM) +#define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32) +#define __GFP_MOVABLE ((__force gfp_t)___GFP_MOVABLE) /* ZONE_MOVABLE allowed */ +#define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE) + +/* line 194 ~ line 260 第三部分 + * 定义掩码 + * 定义形式为 #define GFP_XXX __GFP_XXX + */ +#define GFP_DMA __GFP_DMA +#define GFP_DMA32 __GFP_DMA32 +``` + + +其中GFP缩写的意思为获取空闲页(get free page), __GFP_MOVABLE不表示物理内存域, 但通知内核应在特殊的虚拟内存域ZONE_MOVABLE进行相应的分配. + +##4.2.1 定义掩码位 +------- + + +我们首先来看**第一部分**, 内核源代码中定义在[include/linux/gfp.h?v=4.7, line 18 ~ line 44](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L17), 共计26个掩码信息, + + +```cpp +/* Plain integer GFP bitmasks. Do not use this directly. */ +#define ___GFP_DMA 0x01u +#define ___GFP_HIGHMEM 0x02u +#define ___GFP_DMA32 0x04u +#define ___GFP_MOVABLE 0x08u /* 页是可移动的 */ +#define ___GFP_RECLAIMABLE 0x10u /* 页是可回收的 */ +#define ___GFP_HIGH 0x20u /* 应该访问紧急分配池? */ +#define ___GFP_IO 0x40u /* 可以启动物理IO? */ +#define ___GFP_FS 0x80u /* 可以调用底层文件系统? */ +#define ___GFP_COLD 0x100u /* 需要非缓存的冷页 */ +#define ___GFP_NOWARN 0x200u /* 禁止分配失败警告 */ +#define ___GFP_REPEAT 0x400u /* 重试分配,可能失败 */ +#define ___GFP_NOFAIL 0x800u /* 一直重试,不会失败 */ +#define ___GFP_NORETRY 0x1000u /* 不重试,可能失败 */ +#define ___GFP_MEMALLOC 0x2000u /* 使用紧急分配链表 */ +#define ___GFP_COMP 0x4000u /* 增加复合页元数据 */ +#define ___GFP_ZERO 0x8000u /* 成功则返回填充字节0的页 */ +#define ___GFP_NOMEMALLOC 0x10000u /* 不使用紧急分配链表 */ +#define ___GFP_HARDWALL 0x20000u /* 只允许在进程允许运行的CPU所关联的结点分配内存 */ +#define ___GFP_THISNODE 0x40000u /* 没有备用结点,没有策略 */ +#define ___GFP_ATOMIC 0x80000u /* 用于原子分配,在任何情况下都不能中断 */ +#define ___GFP_ACCOUNT 0x100000u +#define ___GFP_NOTRACK 0x200000u +#define ___GFP_DIRECT_RECLAIM 0x400000u +#define ___GFP_OTHER_NODE 0x800000u +#define ___GFP_WRITE 0x1000000u +#define ___GFP_KSWAPD_RECLAIM 0x2000000u +``` + +###4.2.2 定义掩码 +------- + +然后**第二部分**, 相对而言每一个宏又被重新定义如下, 参见[include/linux/gfp.h?v=4.7, line 46 ~ line 192](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L46) + +```cpp +/* +* Physical address zone modifiers (see linux/mmzone.h - low four bits) +* +* Do not put any conditional on these. If necessary modify the definitions +* without the underscores and use them consistently. The definitions here may +* be used in bit comparisons. +*/ +#define __GFP_DMA ((__force gfp_t)___GFP_DMA) +#define __GFP_HIGHMEM ((__force gfp_t)___GFP_HIGHMEM) +#define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32) +#define __GFP_MOVABLE ((__force gfp_t)___GFP_MOVABLE) /* ZONE_MOVABLE allowed */ +#define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE) + +/* +* Page mobility and placement hints +* +* These flags provide hints about how mobile the page is. Pages with similar +* mobility are placed within the same pageblocks to minimise problems due +* to external fragmentation. +* +* __GFP_MOVABLE (also a zone modifier) indicates that the page can be +* moved by page migration during memory compaction or can be reclaimed. +* +* __GFP_RECLAIMABLE is used for slab allocations that specify +* SLAB_RECLAIM_ACCOUNT and whose pages can be freed via shrinkers. +* +* __GFP_WRITE indicates the caller intends to dirty the page. Where possible, +* these pages will be spread between local zones to avoid all the dirty +* pages being in one zone (fair zone allocation policy). +* +* __GFP_HARDWALL enforces the cpuset memory allocation policy. +* +* __GFP_THISNODE forces the allocation to be satisified from the requested +* node with no fallbacks or placement policy enforcements. +* +* __GFP_ACCOUNT causes the allocation to be accounted to kmemcg (only relevant +* to kmem allocations). +*/ +#define __GFP_RECLAIMABLE ((__force gfp_t)___GFP_RECLAIMABLE) +#define __GFP_WRITE ((__force gfp_t)___GFP_WRITE) +#define __GFP_HARDWALL ((__force gfp_t)___GFP_HARDWALL) +#define __GFP_THISNODE ((__force gfp_t)___GFP_THISNODE) +#define __GFP_ACCOUNT ((__force gfp_t)___GFP_ACCOUNT) + +/* +* Watermark modifiers -- controls access to emergency reserves +* +* __GFP_HIGH indicates that the caller is high-priority and that granting +* the request is necessary before the system can make forward progress. +* For example, creating an IO context to clean pages. +* +* __GFP_ATOMIC indicates that the caller cannot reclaim or sleep and is +* high priority. Users are typically interrupt handlers. This may be +* used in conjunction with __GFP_HIGH + * + * __GFP_MEMALLOC allows access to all memory. This should only be used when + * the caller guarantees the allocation will allow more memory to be freed + * very shortly e.g. process exiting or swapping. Users either should + * be the MM or co-ordinating closely with the VM (e.g. swap over NFS). + * + * __GFP_NOMEMALLOC is used to explicitly forbid access to emergency reserves. + * This takes precedence over the __GFP_MEMALLOC flag if both are set. + */ +#define __GFP_ATOMIC ((__force gfp_t)___GFP_ATOMIC) +#define __GFP_HIGH ((__force gfp_t)___GFP_HIGH) +#define __GFP_MEMALLOC ((__force gfp_t)___GFP_MEMALLOC) +#define __GFP_NOMEMALLOC ((__force gfp_t)___GFP_NOMEMALLOC) + +/* + * Reclaim modifiers + * + * __GFP_IO can start physical IO. + * + * __GFP_FS can call down to the low-level FS. Clearing the flag avoids the + * allocator recursing into the filesystem which might already be holding + * locks. + * + * __GFP_DIRECT_RECLAIM indicates that the caller may enter direct reclaim. + * This flag can be cleared to avoid unnecessary delays when a fallback + * option is available. + * + * __GFP_KSWAPD_RECLAIM indicates that the caller wants to wake kswapd when + * the low watermark is reached and have it reclaim pages until the high + * watermark is reached. A caller may wish to clear this flag when fallback + * options are available and the reclaim is likely to disrupt the system. The + * canonical example is THP allocation where a fallback is cheap but + * reclaim/compaction may cause indirect stalls. + * + * __GFP_RECLAIM is shorthand to allow/forbid both direct and kswapd reclaim. + * + * __GFP_REPEAT: Try hard to allocate the memory, but the allocation attempt + * _might_ fail. This depends upon the particular VM implementation. + * + * __GFP_NOFAIL: The VM implementation _must_ retry infinitely: the caller + * cannot handle allocation failures. New users should be evaluated carefully + * (and the flag should be used only when there is no reasonable failure + * policy) but it is definitely preferable to use the flag rather than + * opencode endless loop around allocator. + * + * __GFP_NORETRY: The VM implementation must not retry indefinitely and will + * return NULL when direct reclaim and memory compaction have failed to allow + * the allocation to succeed. The OOM killer is not called with the current + * implementation. + */ +#define __GFP_IO ((__force gfp_t)___GFP_IO) +#define __GFP_FS ((__force gfp_t)___GFP_FS) +#define __GFP_DIRECT_RECLAIM ((__force gfp_t)___GFP_DIRECT_RECLAIM) /* Caller can reclaim */ +#define __GFP_KSWAPD_RECLAIM ((__force gfp_t)___GFP_KSWAPD_RECLAIM) /* kswapd can wake */ +#define __GFP_RECLAIM ((__force gfp_t)(___GFP_DIRECT_RECLAIM|___GFP_KSWAPD_RECLAIM)) +#define __GFP_REPEAT ((__force gfp_t)___GFP_REPEAT) +#define __GFP_NOFAIL ((__force gfp_t)___GFP_NOFAIL) +#define __GFP_NORETRY ((__force gfp_t)___GFP_NORETRY) + +/* + * Action modifiers + * + * __GFP_COLD indicates that the caller does not expect to be used in the near + * future. Where possible, a cache-cold page will be returned. + * + * __GFP_NOWARN suppresses allocation failure reports. + * + * __GFP_COMP address compound page metadata. + * + * __GFP_ZERO returns a zeroed page on success. + * + * __GFP_NOTRACK avoids tracking with kmemcheck. + * + * __GFP_NOTRACK_FALSE_POSITIVE is an alias of __GFP_NOTRACK. It's a means of + * distinguishing in the source between false positives and allocations that + * cannot be supported (e.g. page tables). + * + * __GFP_OTHER_NODE is for allocations that are on a remote node but that + * should not be accounted for as a remote allocation in vmstat. A + * typical user would be khugepaged collapsing a huge page on a remote + * node. + */ +#define __GFP_COLD ((__force gfp_t)___GFP_COLD) +#define __GFP_NOWARN ((__force gfp_t)___GFP_NOWARN) +#define __GFP_COMP ((__force gfp_t)___GFP_COMP) +#define __GFP_ZERO ((__force gfp_t)___GFP_ZERO) +#define __GFP_NOTRACK ((__force gfp_t)___GFP_NOTRACK) +#define __GFP_NOTRACK_FALSE_POSITIVE (__GFP_NOTRACK) +#define __GFP_OTHER_NODE ((__force gfp_t)___GFP_OTHER_NODE) + +/* Room for N __GFP_FOO bits */ +#define __GFP_BITS_SHIFT 26 +#define __GFP_BITS_MASK ((__force gfp_t)((1 << __GFP_BITS_SHIFT) - 1)) +``` +给出的常数,其中一些很少使用,因此我不会讨论。其中最重要的一些常数语义如下所示 + + +/* \__GFP_WAIT表示分配内存的请求可以中断。也就是说,调度器在该请求期间可随意选择另一个过程执行,或者该请求可以被另一个更重要的事件中断. 分配器还可以在返回内存之前, 在队列上等待一个事件(相关进程会进入睡眠状态). + +>虽然名字相似,但__GFP_HIGH与__GFP_HIGHMEM毫无关系,请不要弄混这两者\ + +| 宏 | 描述 | +|:---:|:----:| +| \__GFP_RECLAIMABLE
\__GFP_MOVABLE | 是页迁移机制所需的标志. 顾名思义,它们分别将分配的内存标记为可回收的或可移动的。这影响从空闲列表的哪个子表获取内存 | +| \___GFP_HIGH | 如果请求非常重要, 则设置\__GFP_HIGH,即内核急切地需要内存时。在分配内存失败可能给内核带来严重后果时(比如威胁到系统稳定性或系统崩溃), 总是会使用该标志 | +| \___GFP_IO |说明在查找空闲内存期间内核可以进行I/O操作. 实际上, 这意味着如果内核在内存分配期间换出页, 那么仅当设置该标志时, 才能将选择的页写入硬盘 | +| \___GFP_FS |允许内核执行VFS操作. 在与VFS层有联系的内核子系统中必须禁用, 因为这可能引起循环递归调用. | +| \___GFP_COLD | 如果需要分配不在CPU高速缓存中的“冷”页时,则设置\__GFP_COLD | +| \___GFP_NOWARN | 在分配失败时禁止内核故障警告。在极少数场合该标志有用 | +| \___GFP_REPEAT | 在分配失败后自动重试,但在尝试若干次之后会停止 | +| \___GFP_NOFAIL | 在分配失败后一直重试,直至成功 | +| \___GFP_NORETRY | 在分配失败后不重试,因此可能分配失败 | +| \___GFP_ZERO | 在分配成功时,将返回填充字节0的页 | +| \__GFP_HARDWALL | 只在NUMA系统上有意义. 它限制只在分配到当前进程的各个CPU所关联的结点分配内存。如果进程允许在所有CPU上运行(默认情况),该标志是无意义的。只有进程可以运行的CPU受限时,该标志才有效果 | +| \__GFP_THISNODE | 也只在NUMA系统上有意义。如果设置该比特位,则内存分配失败的情况下不允许使用其他结点作为备用,需要保证在当前结点或者明确指定的结点上成功分配内存 | + + +其次还定义了我们程序和函数中所需要的掩码MASK的信息, 由于其中__GFP_DMA, __GFP_DMA32, __GFP_HIGHMEM, __GFP_MOVABLE是在内存中分别有对应的内存域信息, 因此我们定义了内存域的掩码GFP_ZONEMASK, 参见[include/linux/gfp.h?v=4.7, line 57](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L57) + +```cpp +#define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE) +``` + +那自然还有__GFP_BITS_SHIFT来表示我们所有的掩码位, 由于我们共计26个掩码位 + +```cpp +/* Room for N __GFP_FOO bits */ +#define __GFP_BITS_SHIFT 26 +#define __GFP_BITS_MASK ((__force gfp_t)((1 << __GFP_BITS_SHIFT) - 1)) +``` + +###4.2.3 掩码分组 +------- + + +最后来看**第三部分**, 由于这些标志几乎总是组合使用,内核作了一些分组,包含了用于各种标准情形的适当的标志. + +如果有可能的话,在内存管理子系统之外,总是把下列分组之一用于内存分配. 在内核源代码中,双下划线通常用于内部数据和定义。而这些预定义的分组名没有双下划线前缀,这一点从侧面验证了上述说法. + +```cpp +#define GFP_ATOMIC (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM) +#define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS) +#define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT) +#define GFP_NOWAIT (__GFP_KSWAPD_RECLAIM) +#define GFP_NOIO (__GFP_RECLAIM) +#define GFP_NOFS (__GFP_RECLAIM | __GFP_IO) +#define GFP_TEMPORARY (__GFP_RECLAIM | __GFP_IO | __GFP_FS | \ + __GFP_RECLAIMABLE) +#define GFP_USER (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL) +#define GFP_DMA __GFP_DMA +#define GFP_DMA32 __GFP_DMA32 +#define GFP_HIGHUSER (GFP_USER | __GFP_HIGHMEM) +#define GFP_HIGHUSER_MOVABLE (GFP_HIGHUSER | __GFP_MOVABLE) +#define GFP_TRANSHUGE ((GFP_HIGHUSER_MOVABLE | __GFP_COMP | \ + __GFP_NOMEMALLOC | __GFP_NORETRY | __GFP_NOWARN) & \ + ~__GFP_RECLAIM) + +/* Convert GFP flags to their corresponding migrate type */ +#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE) +#define GFP_MOVABLE_SHIFT 3 +``` + +| 掩码组 | 描述 | +|:-------:|:-----:| +| GFP_ATOMIC | 用于原子分配,在任何情况下都不能中断, 可能使用紧急分配链表中的内存 | +| GFP_NOIO
GFP_NOFS | 分别明确禁止I/O操作和访问VFS层, 但同时设置了\__GFP_RECLAIM,因此可以被回收 | +| GFP_KERNEL
GFP_USER | 分别是内核和用户分配的默认设置。二者的失败不会立即威胁系统稳定性, GFP_KERNEL绝对是内核源代码中最常使用的标志 | +| GFP_HIGHUSER | 是GFP_USER的一个扩展, 也用于用户空间. 它允许分配无法直接映射的高端内存. 使用高端内存页是没有坏处的,因为用户过程的地址空间总是通过非线性页表组织的 | +| GFP_HIGHUSER_MOVABLE |用途类似于GFP_HIGHUSER,但分配将从虚拟内存域ZONE_MOVABLE进行 | +| GFP_DMA
GFP_DMA32 | 用于分配适用于DMA的内存, 当前是\__GFP_DMA的同义词, GFP_DMA32也是\__GFP_GMA32的同义词 | + + +最后内核设置了碎片管理的可移动依据组织页的MASK信息GFP_MOVABLE_MASK, 参见[include/linux/gfp.h?v=4.7, line 262](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L262) + +```cpp +/* Convert GFP flags to their corresponding migrate type */ +#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE) +#define GFP_MOVABLE_SHIFT 3 +``` + + + +###4.2.4 总结 +------- + +我们从注释中找到这样的信息, 可以作为参考[]() + +```cpp +bit result +================= +0x0 => NORMAL +0x1 => DMA or NORMAL +0x2 => HIGHMEM or NORMAL +0x3 => BAD (DMA+HIGHMEM) +0x4 => DMA32 or DMA or NORMAL +0x5 => BAD (DMA+DMA32) +0x6 => BAD (HIGHMEM+DMA32) +0x7 => BAD (HIGHMEM+DMA32+DMA) +0x8 => NORMAL (MOVABLE+0) +0x9 => DMA or NORMAL (MOVABLE+DMA) +0xa => MOVABLE (Movable is valid only if HIGHMEM is set too) +0xb => BAD (MOVABLE+HIGHMEM+DMA) +0xc => DMA32 (MOVABLE+DMA32) +0xd => BAD (MOVABLE+DMA32+DMA) +0xe => BAD (MOVABLE+DMA32+HIGHMEM) +0xf => BAD (MOVABLE+DMA32+HIGHMEM+DMA) + +GFP_ZONES_SHIFT must be <= 2 on 32 bit platforms. +``` + + + +很有趣的一点是,没有\__GFP_NORMAL常数,而内存分配的主要负担却落到ZONE_NORMAL内存域 + +内核考虑到这一点, 提供了一个函数gfp_zone来计算与给定分配标志兼容的最高内存域. 那么内存分配可以从该内存域或更低的内存域进行, 该函数定义在[include/linux/gfp.h?v=4.7, line 394](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L394) + +```cpp +static inline enum zone_type gfp_zone(gfp_t flags) +{ + enum zone_type z; + int bit = (__force int) (flags & GFP_ZONEMASK); + + z = (GFP_ZONE_TABLE >> (bit * GFP_ZONES_SHIFT)) & + ((1 << GFP_ZONES_SHIFT) - 1); + VM_BUG_ON((GFP_ZONE_BAD >> bit) & 1); + return z; +} +``` + +其中GFP_ZONES_SHIFT的定义如下, 在[include/linux/gfp.h?v=4.7, line 337](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L337) + + +```cpp +#if defined(CONFIG_ZONE_DEVICE) && (MAX_NR_ZONES-1) <= 4 +/* ZONE_DEVICE is not a valid GFP zone specifier */ +#define GFP_ZONES_SHIFT 2 +#else +#define GFP_ZONES_SHIFT ZONES_SHIFT +#endif + +#if 16 * GFP_ZONES_SHIFT > BITS_PER_LONG +#error GFP_ZONES_SHIFT too large to create GFP_ZONE_TABLE integer +#endif +``` + + +由于内存域修饰符的解释方式不是那么直观, 表3-7给出了该函数结果的一个例子, 其中DMA和DMA32内存域相同. 假定在下文中没有设置\__GFP_MOVABLE修饰符. + +| 修饰符 | 扫描的内存域 | +|:-------:|:--------------:| +| 无 | ZONE_NORMAL、ZONE_DMA | +| \__GFP_DMA | ZONE_DMA | +| \__GFP_DMA & \__GFP_HIGHMEM | ZONE_DMA | +| \__GFP_HIGHMEM | ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA | + +* 如果\__GFP_DMA和\__GFP_HIGHMEM都没有设置, 则首先扫描ZONE_NORMAL, 后面是ZONE_DMA + +* 如果设置了\__GFP_HIGHMEM没有设置__GFP_DMA,则结果是从ZONE_HIGHMEM开始扫描所有3个内存域。= + +* 如果设置了__GFP_DMA,那么\__GFP_HIGHMEM设置与否没有关系. 只有ZONE_DMA用于3种情形. 这是合理的, 因为同时使用\__GFP_HIGHMEM和__GFP_DMA没有意义. 高端内存从来都不适用于DMA + + +设置\__GFP_MOVABLE不会影响内核的决策,除非它与\__GFP_HIGHMEM同时指定. 在这种情况下, 会使用特殊的虚拟内存域ZONE_MOVABLE满足内存分配请求. 对前文描述的内核的反碎片策略而言, 这种行为是必要的. + +除了内存域修饰符之外, 掩码中还可以设置一些标志. + +下图中给出了掩码的布局,以及与各个比特位置关联的常数. \__GFP_DMA32出现了几次,因为它可能位于不同的地方. + + +![GFP掩码的布局](../images/gfp_flag_mask.png) + + + +与内存域修饰符相反, 这些额外的标志并不限制从哪个物理内存段分配内存, 但确实可以改变分配器的行为. 例如, 它们可以修改查找空闲内存时的积极程度. + + +##4.3 分配页 +------- + + +###4.3.1 内存分配统一到alloc_pages接口 +------- + +通过使用标志、内存域修饰符和各个分配函数,内核提供了一种非常灵活的内存分配体系.尽管如此, 所有接口函数都可以追溯到一个简单的基本函数(alloc_pages_node) + +分配单页的函数[`alloc_page`](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L483)和[`__get_free_page`](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L500), 还有[`__get_dma_pages`](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L503)是借助于宏定义的. + +```cpp +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L483 +#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0) + +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L500 +#define __get_free_page(gfp_mask) \ + __get_free_pages((gfp_mask), 0)` + +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L503 +#define __get_dma_pages(gfp_mask, order) \ + __get_free_pages((gfp_mask) | GFP_DMA, (order)) +``` + +[`get_zeroed_page`](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3900)的实现也没什么困难, 对`__get_free_pages`使用`__GFP_ZERO`标志,即可分配填充字节0的页. 再返回与页关联的内存区地址即可. + + +```cpp +// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3900 +unsigned long get_zeroed_page(gfp_t gfp_mask) +{ + return __get_free_pages(gfp_mask | __GFP_ZERO, 0); +} +EXPORT_SYMBOL(get_zeroed_page); +``` + + +[`__get_free_pages`](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883)调用`alloc_pages`完成内存分配, 而alloc_pages又借助于alloc_pages_node + +[`__get_free_pages`](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883)函数的定义在[mm/page_alloc.c?v=4.7, line 3883](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883) + +```cpp +// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883 +unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) +{ + struct page *page; + + /* + * __get_free_pages() returns a 32-bit address, which cannot represent + * a highmem page + */ + VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0); + + page = alloc_pages(gfp_mask, order); + if (!page) + return 0; + return (unsigned long) page_address(page); +} +EXPORT_SYMBOL(__get_free_pages); +``` + +在这种情况下, 使用了一个普通函数而不是宏, 因为`alloc_pages`返回的`page`实例需要使用辅助 + +函数`page_address`转换为内存地址. 在这里,只要知道该函数可根据`page`实例计算相关页的线性内存地址即可. 对高端内存页这是有问题的 + + + +这样, 就完成了所有分配内存的API函数到公共的基础函数`alloc_pages`的统一 + + +![伙伴系统中各个分配函数之间的关系](../images/alloc_pages.png) + + +所有体系结构都必须实现的标准函数`clear_page`, 可帮助alloc_pages对页填充字节0, 实现如下表所示 + +| x86 | arm | +|:----:|:-----:| +| [arch/x86/include/asm/page_32.h?v=4.7, line 24](http://lxr.free-electrons.com/source/arch/x86/include/asm/page_32.h?v=4.7#L24) | [arch/arm/include/asm/page.h?v=4.7#L14](http://lxr.free-electrons.com/source/arch/arm/include/asm/page.h?v=4.7#L142)
[arch/arm/include/asm/page-nommu.h](http://lxr.free-electrons.com/source/arch/arm/include/asm/page-nommu.h?v=4.7#L20) | + + +###4.3.2 alloc_pages函数分配页 +------- + + +既然所有的内存分配API函数都可以追溯掉`alloc_page`函数, 从某种意义上说,该函数是伙伴系统主要实现的"发射台". + + +`alloc_pages`函数的定义是依赖于NUMA或者UMA架构的, 定义如下 + + +```cpp +#ifdef CONFIG_NUMA + +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L465 +static inline struct page * +alloc_pages(gfp_t gfp_mask, unsigned int order) +{ + return alloc_pages_current(gfp_mask, order); +} + +#else + +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L476 +#define alloc_pages(gfp_mask, order) \ + alloc_pages_node(numa_node_id(), gfp_mask, order) +#endif +``` + + +UMA结构下的`alloc_pages`是通过`alloc_pages_node`函数实现的, 下面我们看看`alloc_pages_node`函数的定义, 在[include/linux/gfp.h?v=4.7, line 448](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L448) + + +```cpp +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L448 +/* + * Allocate pages, preferring the node given as nid. When nid == NUMA_NO_NODE, + * prefer the current CPU's closest node. Otherwise node must be valid and + * online. + */ +static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask, + unsigned int order) +{ + if (nid == NUMA_NO_NODE) + nid = numa_mem_id(); + + return __alloc_pages_node(nid, gfp_mask, order); +} +```` + +它只是执行了一个简单的检查, 如果指定负的结点ID(不存在, 即[NUMA_NO_NODE = -1](http://lxr.free-electrons.com/source/include/linux/numa.h?v=4.7#L13)), 内核自动地使用当前执行CPU对应的结点nid = [numa_mem_id();](http://lxr.free-electrons.com/source/include/linux/topology.h?v=4.7#L137), 然后调用`__alloc_pages_node`函数进行了内存分配 + + + +`__alloc_pages_node`函数定义在[include/linux/gfp.h?v=4.7, line 435)](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L435), 如下所示 + +```cpp +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L435 +/* + * Allocate pages, preferring the node given as nid. The node must be valid and + * online. For more general interface, see alloc_pages_node(). + */ +static inline struct page * +__alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order) +{ + VM_BUG_ON(nid < 0 || nid >= MAX_NUMNODES); + VM_WARN_ON(!node_online(nid)); + + return __alloc_pages(gfp_mask, order, node_zonelist(nid, gfp_mask)); +} +``` + +内核假定传递给改alloc_pages_node函数的结点nid是被激活, 即online的.但是为了安全它还是检查并警告内存结点不存在的情况. 接下来的工作委托给__alloc_pages, 只需传递一组适当的参数, 其中包括节点nid的备用内存域列表zonelist. + + +现在`__alloc_pages`函数没什么特别的, 它直接将自己的所有信息传递给`__alloc_pages_nodemask`来完成内存的分配 + +```cpp +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L428 +static inline struct page * +__alloc_pages(gfp_t gfp_mask, unsigned int order, + struct zonelist *zonelist) +{ + return __alloc_pages_nodemask(gfp_mask, order, zonelist, NULL); +} +``` + +###4.3.3 伙伴系统的心脏__alloc_pages_nodemask +------- + +内核源代码将`__alloc_pages`称之为"伙伴系统的心脏"(`the 'heart' of the zoned buddy allocator``), 因为它处理的是实质性的内存分配. + +由于"心脏"的重要性, 我将在下文详细介绍该函数. + +`__alloc_pages`函数定义在[include/linux/gfp.h?v=4.7#L428](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L428) + + + +```cpp +// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3779 +/* + * This is the 'heart' of the zoned buddy allocator. + */ +struct page * +__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, + struct zonelist *zonelist, nodemask_t *nodemask) +{ + struct page *page; + unsigned int cpuset_mems_cookie; + unsigned int alloc_flags = ALLOC_WMARK_LOW|ALLOC_FAIR; + gfp_t alloc_mask = gfp_mask; /* The gfp_t that was actually used for allocation */ + struct alloc_context ac = { + .high_zoneidx = gfp_zone(gfp_mask), + .zonelist = zonelist, + .nodemask = nodemask, + .migratetype = gfpflags_to_migratetype(gfp_mask), + }; + + if (cpusets_enabled()) { + alloc_mask |= __GFP_HARDWALL; + alloc_flags |= ALLOC_CPUSET; + if (!ac.nodemask) + ac.nodemask = &cpuset_current_mems_allowed; + } + + gfp_mask &= gfp_allowed_mask; + + lockdep_trace_alloc(gfp_mask); + + might_sleep_if(gfp_mask & __GFP_DIRECT_RECLAIM); + + if (should_fail_alloc_page(gfp_mask, order)) + return NULL; + + /* + * Check the zones suitable for the gfp_mask contain at least one + * valid zone. It's possible to have an empty zonelist as a result + * of __GFP_THISNODE and a memoryless node + */ + if (unlikely(!zonelist->_zonerefs->zone)) + return NULL; + + if (IS_ENABLED(CONFIG_CMA) && ac.migratetype == MIGRATE_MOVABLE) + alloc_flags |= ALLOC_CMA; + +retry_cpuset: + cpuset_mems_cookie = read_mems_allowed_begin(); + + /* Dirty zone balancing only done in the fast path */ + ac.spread_dirty_pages = (gfp_mask & __GFP_WRITE); + + /* + * The preferred zone is used for statistics but crucially it is + * also used as the starting point for the zonelist iterator. It + * may get reset for allocations that ignore memory policies. + */ + ac.preferred_zoneref = first_zones_zonelist(ac.zonelist, + ac.high_zoneidx, ac.nodemask); + if (!ac.preferred_zoneref) { + page = NULL; + goto no_zone; + } + + /* First allocation attempt */ + page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac); + if (likely(page)) + goto out; + + /* + * Runtime PM, block IO and its error handling path can deadlock + * because I/O on the device might not complete. + */ + alloc_mask = memalloc_noio_flags(gfp_mask); + ac.spread_dirty_pages = false; + + /* + * Restore the original nodemask if it was potentially replaced with + * &cpuset_current_mems_allowed to optimize the fast-path attempt. + */ + if (cpusets_enabled()) + ac.nodemask = nodemask; + page = __alloc_pages_slowpath(alloc_mask, order, &ac); + +no_zone: + /* + * When updating a task's mems_allowed, it is possible to race with + * parallel threads in such a way that an allocation can fail while + * the mask is being updated. If a page allocation is about to fail, + * check if the cpuset changed during allocation and if so, retry. + */ + if (unlikely(!page && read_mems_allowed_retry(cpuset_mems_cookie))) { + alloc_mask = gfp_mask; + goto retry_cpuset; + } + +out: + if (kmemcheck_enabled && page) + kmemcheck_pagealloc_alloc(page, order, gfp_mask); + + trace_mm_page_alloc(page, order, alloc_mask, ac.migratetype); + + return page; +} +EXPORT_SYMBOL(__alloc_pages_nodemask); +``` + +##4.4 __free_pages + + +类似地,内存释放函数也可以归约到一个主要的函数(\__free_pages), 只是用不同的参数调用而已 + +前面我们讲过内核释放的两个主要函数有\__free_page和free_page, 它们的定义在[include/linux/gfp.h?v=4.7#L519](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L519) + + + +```cpp +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L519 +#define __free_page(page) __free_pages((page), 0) +#define free_page(addr) free_pages((addr), 0) +``` + +而free_pages是通过__free_pages来完成内存释放的, 参见[mm/page_alloc.c?v=4.7#L3918](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3918) + + + +```cpp +void free_pages(unsigned long addr, unsigned int order) +{ + if (addr != 0) { + VM_BUG_ON(!virt_addr_valid((void *)addr)); + __free_pages(virt_to_page((void *)addr), order); + } +} +``` + + +`free_pages`和`__free_pages`之间的关系通过函数而不是宏建立, 因为首先必须将虚拟地址转换为指向`struct page`的指针 + + + +`virt_to_page`将虚拟内存地址转换为指向page实例的指针. 基本上, 这是讲解内存分配函数时介绍的page_address辅助函数的逆过程. + + +下图以图形化方式综述了各个内存释放函数之间的关系 + + +![伙伴系统各个内存释放函数之间的关系](../images/__free_pages.png) + + + + diff --git a/study/kernel/02-memory/04-buddy/06-kmalloc/README.md b/study/kernel/02-memory/04-buddy/06-kmalloc/README.md index 3ba4e0c..eae8549 100644 --- a/study/kernel/02-memory/04-buddy/06-kmalloc/README.md +++ b/study/kernel/02-memory/04-buddy/06-kmalloc/README.md @@ -1,618 +1,618 @@ -free_page释放内存空间--Linux内存管理(十八) -======= - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - - - - - -#1 前景回顾 -------- - - -#1.1 内核映射区 -------- - - -尽管`vmalloc`函数族可用于从高端内存域向内核映射页帧(这些在内核空间中通常是无法直接看到的), 但这并不是这些函数的实际用途. - -重要的是强调以下事实 : 内核提供了其他函数用于将`ZONE_HIGHMEM`页帧显式映射到内核空间, 这些函数与vmalloc机制无关. 因此, 这就造成了混乱. - - -而在高端内存的页不能永久地映射到内核地址空间. 因此, 通过alloc_pages()函数以\__GFP_HIGHMEM标志获得的内存页就不可能有逻辑地址. - -在x86_32体系结构总, 高于896MB的所有物理内存的范围大都是高端内存, 它并不会永久地或自动映射到内核地址空间, 尽管X86处理器能够寻址物理RAM的范围达到4GB(启用PAE可以寻址64GB), 一旦这些页被分配, 就必须映射到内核的逻辑地址空间上. 在x86_32上, 高端地址的页被映射到内核地址空间(即虚拟地址空间的3GB~4GB) - -内核地址空间的最后128 MiB用于何种用途呢? - -该部分有3个用途。 - -1. 虚拟内存中连续、但物理内存中不连续的内存区,可以在vmalloc区域分配. 该机制通常用于用户过程, 内核自身会试图尽力避免非连续的物理地址。内核通常会成功,因为大部分大的内存块都在启动时分配给内核,那时内存的碎片尚不严重。但在已经运行了很长时间的系统上, 在内核需要物理内存时, 就可能出现可用空间不连续的情况. 此类情况, 主要出现在动态加载模块时. - -2. 持久映射用于将高端内存域中的非持久页映射到内核中 - -3. 固定映射是与物理地址空间中的固定页关联的虚拟地址空间项,但具体关联的页帧可以自由选择. 它与通过固定公式与物理内存关联的直接映射页相反,虚拟固定映射地址与物理内存位置之间的关联可以自行定义,关联建立后内核总是会注意到的. - -![x86_32上的地址划分](../images/x86_32_mapping.png) - -在这里有两个预处理器符号很重要 \__VMALLOC_RESERVE设置了`vmalloc`区域的长度, 而`MAXMEM`则表示内核可以直接寻址的物理内存的最大可能数量. - -![内核虚拟地址空间](../images/kernel_space.jpg) - - -内核中, 将内存划分为各个区域是通过图3-15所示的各个常数控制的。根据内核和系统配置, 这些常数可能有不同的值。直接映射的边界由high_memory指定。 - - - - -1. 直接映射区 - 线性空间中从3G开始最大896M的区间, 为直接内存映射区,该区域的线性地址和物理地址存在线性转换关系:线性地址=3G+物理地址。 - -2. 动态内存映射区 - 该区域由内核函数`vmalloc`来分配, 特点是 : 线性空间连续, 但是对应的物理空间不一定连续. `vmalloc`分配的线性地址所对应的物理页可能处于低端内存, 也可能处于高端内存. - -3. 永久内存映射区 - 该区域可访问高端内存. 访问方法是使用`alloc_page(_GFP_HIGHMEM)`分配高端内存页或者使用`kmap`函数将分配到的高端内存映射到该区域. - -4. 固定映射区 - 该区域和4G的顶端只有4k的隔离带,其每个地址项都服务于特定的用途,如ACPI_BASE等。 - ->说明 -> ->注意用户空间当然可以使用高端内存,而且是正常的使用,内核在分配那些不经常使用的内存时,都用高端内存空间(如果有),所谓不经常使用是相对来说的,比如内核的一些数据结构就属于经常使用的,而用户的一些数据就属于不经常使用的。用户在启动一个应用程序时,是需要内存的,而每个应用程序都有3G的线性地址,给这些地址映射页表时就可以直接使用高端内存。 -> ->而且还要纠正一点的是:那128M线性地址不仅仅是用在这些地方的,如果你要加载一个设备,而这个设备需要映射其内存到内核中,它也需要使用这段线性地址空间来完成,否则内核就不能访问设备上的内存空间了. -> ->总之,内核的高端线性地址是为了访问内核固定映射以外的内存资源。进程在使用内存时,触发缺页异常,具体将哪些物理页映射给用户进程是内核考虑的事情. 在用户空间中没有高端内存这个概念. - - -#2 kmallc & kfree分配释放连续的物理内存 -------- - -**kmalloc和kzalloc** - - -kmalloc函数与用户空间的malloc一族函数非常类似, 只不过它多了一个flags参数, kmalloc函数是一个简单的接口, 用它可以获取以字节为单位的一块内核内存. - -如果你需要整个页, 那么前面讨论的页分配接口是更好的选择. 但是, 对大多数内核分配来说, kmalloc接口用的更多, 同时内核也提供了kzalloc该接口在kmalloc的基础上会将分配的内存清0. 他们定义在[tools/virtio/linux/kernel.h?v=4.7, line 46](http://lxr.free-electrons.com/source/tools/virtio/linux/kernel.h?v=4.7#L46) - - - - -这两个函数返回一个指向内存块的指针, 其内存块至少要有size大小. 所分配的内存区在物理上是连续的. 在出错时, 它返回NULL. 除非没有足够的内存可用, 否则内核总能分配成功. 在对kmalloc调用之后, 你必须检查返回的是不是NULL, 如果是, 要适当处理错误. - -**kfree释放内存** - -kmalloc的另一端就是kfree, 用于释放分配的内存, kfree声明与 - - -| kmalloc定义 | kzalloc定义 | kfree定义 | -|:--------------:|:--------------:|-----------:| -| [tools/virtio/linux/kernel.h?v=4.7, line 46](http://lxr.free-electrons.com/source/tools/virtio/linux/kernel.h?v=4.7#L46) | [tools/virtio/linux/kernel.h?v=4.7, line 52](http://lxr.free-electrons.com/source/tools/virtio/linux/kernel.h?v=4.7#L52) | [tools/virtio/linux/kernel.h?v=4.7, line 60](http://lxr.free-electrons.com/source/tools/virtio/linux/kernel.h?v=4.7#L60) | -| [include/linux/slab.h, line 466](http://lxr.free-electrons.com/source/include/linux/slab.h?v=4.7#L466) | [include/linux/slab.h?v=4.7, line 620](http://lxr.free-electrons.com/source/include/linux/slab.h?v=4.7#L620) | [mm/slob.c?v=4.7, line 484](http://lxr.free-electrons.com/source/mm/slob.c?v=4.7#L484)
[mm/slub.c?v=4.7, line 3645](http://lxr.free-electrons.com/source/mm/slub.c?v=4.7#L3645)
[mm/slab.c?v=4.7, line 3853](http://lxr.free-electrons.com/source/mm/slab.c?v=4.7#L3853) | - - -#3 分配掩码(gfp_mask标志) -------- - - -##3.1 分配掩码 -------- - -前述所有函数中强制使用的mask参数,到底是什么语义? - -我们知道Linux将内存划分为内存域. 内核提供了所谓的内存域修饰符(zone modifier)(在掩码的最低4个比特位定义), 来指定从哪个内存域分配所需的页. - - -内核使用宏的方式定义了这些掩码, 一个掩码的定义被划分为3个部分进行定义, 我们会逐步展开来讲解, 参见[include/linux/gfp.h?v=4.7, line 12~374](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L12), 共计26个掩码信息, 因此后面__GFP_BITS_SHIFT = 26. - - -##3.2 掩码分类 -------- - - -Linux中这些掩码标志`gfp_mask`分为3种类型 : - - -| 类型 | 描述 | -|:-----:|:-----:| -| 区描述都符 | 内核把物理内存分为多个区, 每个区用于不同的目的, 区描述符指明到底从这些区中的哪一区进行分配 | -| 行为修饰符 | 表示内核应该如何分配所需的内存. 在某些特定情况下, 只能使用某些特定的方法分配内存 | -| 类型标志 | 组合了行为修饰符和区描述符, 将这些可能用到的组合归纳为不同类型 | - - - - - -##3.3 内核中掩码的定义 -------- - - - -###3.3.1 **内核中的定义方式** -------- - - -```cpp -// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7 - -/* line 12 ~ line 44 第一部分 - * 定义可掩码所在位的信息, 每个掩码对应一位为1 - * 定义形式为 #define ___GFP_XXX 0x01u - */ -/* Plain integer GFP bitmasks. Do not use this directly. */ -#define ___GFP_DMA 0x01u -#define ___GFP_HIGHMEM 0x02u -#define ___GFP_DMA32 0x04u -#define ___GFP_MOVABLE 0x08u -/* ...... */ - -/* line 46 ~ line 192 第二部分 - * 定义掩码和MASK信息, 第二部分的某些宏可能是第一部分一个或者几个的组合 - * 定义形式为 #define __GFP_XXX ((__force gfp_t)___GFP_XXX) - */ -#define __GFP_DMA ((__force gfp_t)___GFP_DMA) -#define __GFP_HIGHMEM ((__force gfp_t)___GFP_HIGHMEM) -#define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32) -#define __GFP_MOVABLE ((__force gfp_t)___GFP_MOVABLE) /* ZONE_MOVABLE allowed */ -#define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE) - -/* line 194 ~ line 260 第三部分 - * 定义掩码 - * 定义形式为 #define GFP_XXX __GFP_XXX - */ -#define GFP_DMA __GFP_DMA -#define GFP_DMA32 __GFP_DMA32 -``` - - -其中GFP缩写的意思为获取空闲页(get free page), __GFP_MOVABLE不表示物理内存域, 但通知内核应在特殊的虚拟内存域ZONE_MOVABLE进行相应的分配. - - -###3.3.2 定义掩码位 -------- - - -我们首先来看**第一部分**, 内核源代码中定义在[include/linux/gfp.h?v=4.7, line 18 ~ line 44](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L17), 共计26个掩码信息. - - -```cpp -/* Plain integer GFP bitmasks. Do not use this directly. */ -// 区域修饰符 -#define ___GFP_DMA 0x01u -#define ___GFP_HIGHMEM 0x02u -#define ___GFP_DMA32 0x04u - -// 行为修饰符 -#define ___GFP_MOVABLE 0x08u /* 页是可移动的 */ -#define ___GFP_RECLAIMABLE 0x10u /* 页是可回收的 */ -#define ___GFP_HIGH 0x20u /* 应该访问紧急分配池? */ -#define ___GFP_IO 0x40u /* 可以启动物理IO? */ -#define ___GFP_FS 0x80u /* 可以调用底层文件系统? */ -#define ___GFP_COLD 0x100u /* 需要非缓存的冷页 */ -#define ___GFP_NOWARN 0x200u /* 禁止分配失败警告 */ -#define ___GFP_REPEAT 0x400u /* 重试分配,可能失败 */ -#define ___GFP_NOFAIL 0x800u /* 一直重试,不会失败 */ -#define ___GFP_NORETRY 0x1000u /* 不重试,可能失败 */ -#define ___GFP_MEMALLOC 0x2000u /* 使用紧急分配链表 */ -#define ___GFP_COMP 0x4000u /* 增加复合页元数据 */ -#define ___GFP_ZERO 0x8000u /* 成功则返回填充字节0的页 */ -// 类型修饰符 -#define ___GFP_NOMEMALLOC 0x10000u /* 不使用紧急分配链表 */ -#define ___GFP_HARDWALL 0x20000u /* 只允许在进程允许运行的CPU所关联的结点分配内存 */ -#define ___GFP_THISNODE 0x40000u /* 没有备用结点,没有策略 */ -#define ___GFP_ATOMIC 0x80000u /* 用于原子分配,在任何情况下都不能中断 */ -#define ___GFP_ACCOUNT 0x100000u -#define ___GFP_NOTRACK 0x200000u -#define ___GFP_DIRECT_RECLAIM 0x400000u -#define ___GFP_OTHER_NODE 0x800000u -#define ___GFP_WRITE 0x1000000u -#define ___GFP_KSWAPD_RECLAIM 0x2000000u -``` - -###3.3.3 定义掩码 -------- - -然后**第二部分**, 相对而言每一个宏又被重新定义如下, 参见[include/linux/gfp.h?v=4.7, line 46 ~ line 192](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L46) - -```cpp -/* -* Physical address zone modifiers (see linux/mmzone.h - low four bits) -* -* Do not put any conditional on these. If necessary modify the definitions -* without the underscores and use them consistently. The definitions here may -* be used in bit comparisons. -* 定义区描述符 -*/ -#define __GFP_DMA ((__force gfp_t)___GFP_DMA) -#define __GFP_HIGHMEM ((__force gfp_t)___GFP_HIGHMEM) -#define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32) -#define __GFP_MOVABLE ((__force gfp_t)___GFP_MOVABLE) /* ZONE_MOVABLE allowed */ -#define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE) - -/* -* Page mobility and placement hints -* -* These flags provide hints about how mobile the page is. Pages with similar -* mobility are placed within the same pageblocks to minimise problems due -* to external fragmentation. -* -* __GFP_MOVABLE (also a zone modifier) indicates that the page can be -* moved by page migration during memory compaction or can be reclaimed. -* -* __GFP_RECLAIMABLE is used for slab allocations that specify -* SLAB_RECLAIM_ACCOUNT and whose pages can be freed via shrinkers. -* -* __GFP_WRITE indicates the caller intends to dirty the page. Where possible, -* these pages will be spread between local zones to avoid all the dirty -* pages being in one zone (fair zone allocation policy). -* -* __GFP_HARDWALL enforces the cpuset memory allocation policy. -* -* __GFP_THISNODE forces the allocation to be satisified from the requested -* node with no fallbacks or placement policy enforcements. -* -* __GFP_ACCOUNT causes the allocation to be accounted to kmemcg (only relevant -* to kmem allocations). -*/ -#define __GFP_RECLAIMABLE ((__force gfp_t)___GFP_RECLAIMABLE) -#define __GFP_WRITE ((__force gfp_t)___GFP_WRITE) -#define __GFP_HARDWALL ((__force gfp_t)___GFP_HARDWALL) -#define __GFP_THISNODE ((__force gfp_t)___GFP_THISNODE) -#define __GFP_ACCOUNT ((__force gfp_t)___GFP_ACCOUNT) - -/* -* Watermark modifiers -- controls access to emergency reserves -* -* __GFP_HIGH indicates that the caller is high-priority and that granting -* the request is necessary before the system can make forward progress. -* For example, creating an IO context to clean pages. -* -* __GFP_ATOMIC indicates that the caller cannot reclaim or sleep and is -* high priority. Users are typically interrupt handlers. This may be -* used in conjunction with __GFP_HIGH - * - * __GFP_MEMALLOC allows access to all memory. This should only be used when - * the caller guarantees the allocation will allow more memory to be freed - * very shortly e.g. process exiting or swapping. Users either should - * be the MM or co-ordinating closely with the VM (e.g. swap over NFS). - * - * __GFP_NOMEMALLOC is used to explicitly forbid access to emergency reserves. - * This takes precedence over the __GFP_MEMALLOC flag if both are set. - */ -#define __GFP_ATOMIC ((__force gfp_t)___GFP_ATOMIC) -#define __GFP_HIGH ((__force gfp_t)___GFP_HIGH) -#define __GFP_MEMALLOC ((__force gfp_t)___GFP_MEMALLOC) -#define __GFP_NOMEMALLOC ((__force gfp_t)___GFP_NOMEMALLOC) - -/* - * Reclaim modifiers - * - * __GFP_IO can start physical IO. - * - * __GFP_FS can call down to the low-level FS. Clearing the flag avoids the - * allocator recursing into the filesystem which might already be holding - * locks. - * - * __GFP_DIRECT_RECLAIM indicates that the caller may enter direct reclaim. - * This flag can be cleared to avoid unnecessary delays when a fallback - * option is available. - * - * __GFP_KSWAPD_RECLAIM indicates that the caller wants to wake kswapd when - * the low watermark is reached and have it reclaim pages until the high - * watermark is reached. A caller may wish to clear this flag when fallback - * options are available and the reclaim is likely to disrupt the system. The - * canonical example is THP allocation where a fallback is cheap but - * reclaim/compaction may cause indirect stalls. - * - * __GFP_RECLAIM is shorthand to allow/forbid both direct and kswapd reclaim. - * - * __GFP_REPEAT: Try hard to allocate the memory, but the allocation attempt - * _might_ fail. This depends upon the particular VM implementation. - * - * __GFP_NOFAIL: The VM implementation _must_ retry infinitely: the caller - * cannot handle allocation failures. New users should be evaluated carefully - * (and the flag should be used only when there is no reasonable failure - * policy) but it is definitely preferable to use the flag rather than - * opencode endless loop around allocator. - * - * __GFP_NORETRY: The VM implementation must not retry indefinitely and will - * return NULL when direct reclaim and memory compaction have failed to allow - * the allocation to succeed. The OOM killer is not called with the current - * implementation. - */ -#define __GFP_IO ((__force gfp_t)___GFP_IO) -#define __GFP_FS ((__force gfp_t)___GFP_FS) -#define __GFP_DIRECT_RECLAIM ((__force gfp_t)___GFP_DIRECT_RECLAIM) /* Caller can reclaim */ -#define __GFP_KSWAPD_RECLAIM ((__force gfp_t)___GFP_KSWAPD_RECLAIM) /* kswapd can wake */ -#define __GFP_RECLAIM ((__force gfp_t)(___GFP_DIRECT_RECLAIM|___GFP_KSWAPD_RECLAIM)) -#define __GFP_REPEAT ((__force gfp_t)___GFP_REPEAT) -#define __GFP_NOFAIL ((__force gfp_t)___GFP_NOFAIL) -#define __GFP_NORETRY ((__force gfp_t)___GFP_NORETRY) - -/* - * Action modifiers - * - * __GFP_COLD indicates that the caller does not expect to be used in the near - * future. Where possible, a cache-cold page will be returned. - * - * __GFP_NOWARN suppresses allocation failure reports. - * - * __GFP_COMP address compound page metadata. - * - * __GFP_ZERO returns a zeroed page on success. - * - * __GFP_NOTRACK avoids tracking with kmemcheck. - * - * __GFP_NOTRACK_FALSE_POSITIVE is an alias of __GFP_NOTRACK. It's a means of - * distinguishing in the source between false positives and allocations that - * cannot be supported (e.g. page tables). - * - * __GFP_OTHER_NODE is for allocations that are on a remote node but that - * should not be accounted for as a remote allocation in vmstat. A - * typical user would be khugepaged collapsing a huge page on a remote - * node. - */ -#define __GFP_COLD ((__force gfp_t)___GFP_COLD) -#define __GFP_NOWARN ((__force gfp_t)___GFP_NOWARN) -#define __GFP_COMP ((__force gfp_t)___GFP_COMP) -#define __GFP_ZERO ((__force gfp_t)___GFP_ZERO) -#define __GFP_NOTRACK ((__force gfp_t)___GFP_NOTRACK) -#define __GFP_NOTRACK_FALSE_POSITIVE (__GFP_NOTRACK) -#define __GFP_OTHER_NODE ((__force gfp_t)___GFP_OTHER_NODE) - -/* Room for N __GFP_FOO bits */ -#define __GFP_BITS_SHIFT 26 -#define __GFP_BITS_MASK ((__force gfp_t)((1 << __GFP_BITS_SHIFT) - 1)) -``` - - -给出的常数,其中一些很少使用,因此我不会讨论。其中最重要的一些常数语义如下所示 - - -其中在开始的位置定义了对应的**区修饰符**, 定义在[include/linux/gfp.h?v=4.7, line 46 ~ line 57](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L46) - - -| 区修饰符标志 | 描述 | -|:--------------:|:-----:| -| \__GFP_DMA | 从ZONE_DMA中分配内存 | -| \__GFP_HIGHMEM | 从ZONE_HIGHMEM活ZONE_NORMAL中分配内存 | -| \__GFP_DMA32 | 从ZONE_DMA32中分配内存 | -| \__GFP_MOVABLE | 从__GFP_MOVABLE中分配内存 | - -其次还定义了我们程序和函数中所需要的掩码MASK的信息, 由于其中__GFP_DMA, __GFP_DMA32, __GFP_HIGHMEM, __GFP_MOVABLE是在内存中分别有对应的内存域信息, 因此我们定义了内存域的掩码GFP_ZONEMASK, 参见[include/linux/gfp.h?v=4.7, line 57](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L57) - -```cpp -#define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE) -``` - - -接着内核定义了**行为修饰符** - -/* \__GFP_WAIT表示分配内存的请求可以中断。也就是说,调度器在该请求期间可随意选择另一个过程执行,或者该请求可以被另一个更重要的事件中断. 分配器还可以在返回内存之前, 在队列上等待一个事件(相关进程会进入睡眠状态). - ->虽然名字相似,但__GFP_HIGH与__GFP_HIGHMEM毫无关系,请不要弄混这两者\ - -| 行为修饰符 | 描述 | -|:---:|:----:| -| \__GFP_RECLAIMABLE
\__GFP_MOVABLE | 是页迁移机制所需的标志. 顾名思义,它们分别将分配的内存标记为可回收的或可移动的。这影响从空闲列表的哪个子表获取内存 | -| \__GFP_WRITE | | -| \__GFP_HARDWALL | 只在NUMA系统上有意义. 它限制只在分配到当前进程的各个CPU所关联的结点分配内存。如果进程允许在所有CPU上运行(默认情况),该标志是无意义的。只有进程可以运行的CPU受限时,该标志才有效果 | -| \__GFP_THISNODE | 也只在NUMA系统上有意义。如果设置该比特位,则内存分配失败的情况下不允许使用其他结点作为备用,需要保证在当前结点或者明确指定的结点上成功分配内存 | -| \__GFP_ACCOUNT | | -|--------|--------| -| \__GFP_ATOMIC | | -| \__GFP_HIGH | 如果请求非常重要, 则设置\__GFP_HIGH,即内核急切地需要内存时。在分配内存失败可能给内核带来严重后果时(比如威胁到系统稳定性或系统崩溃), 总是会使用该标志 | -| \__GFP_MEMALLOC | | -| \__GFP_NOMEMALLOC | | -|--------|--------| -| \__GFP_IO |说明在查找空闲内存期间内核可以进行I/O操作. 实际上, 这意味着如果内核在内存分配期间换出页, 那么仅当设置该标志时, 才能将选择的页写入硬盘 | -| \__GFP_FS |允许内核执行VFS操作. 在与VFS层有联系的内核子系统中必须禁用, 因为这可能引起循环递归调用. | -| \__GFP_DIRECT_RECLAIM | | -| \__GFP_KSWAPD_RECLAIM | | -| \__GFP_RECLAIM | | -| \__GFP_REPEAT | 在分配失败后自动重试,但在尝试若干次之后会停止 | -| \__GFP_NOFAIL | 在分配失败后一直重试,直至成功 | -| \__GFP_NORETRY | 在分配失败后不重试,因此可能分配失败 | -|--------|--------| -| \__GFP_COLD | 如果需要分配不在CPU高速缓存中的“冷”页时,则设置\__GFP_COLD | -| \__GFP_NOWARN | 在分配失败时禁止内核故障警告。在极少数场合该标志有用 | -| \__GFP_COMP | 添加混合页元素, 在hugetlb的代码内部使用 | -| \__GFP_ZERO | 在分配成功时,将返回填充字节0的页 | -| \__GFP_NOTRACK | | -| \__GFP_NOTRACK_FALSE_POSITIVE
\__GFP_NOTRACK | | -| \__GFP_OTHER_NODE | | - - -那自然还有__GFP_BITS_SHIFT来表示我们所有的掩码位, 由于我们共计26个掩码位 - -```cpp -/* Room for N __GFP_FOO bits */ -#define __GFP_BITS_SHIFT 26 -#define __GFP_BITS_MASK ((__force gfp_t)((1 << __GFP_BITS_SHIFT) - 1)) -``` - -可以同时指定这些分配标志, 例如 - -```cpp -ptr = kmalloc(size, __GFP_IO | __GFP_FS); -``` - -说明页分配器(最终会调用alloc_page)在分配时可以执行I/O, 在必要时还可以执行文件系统操作. 这就让内核有很大的自由度, 以便它尽可能找到空闲的内存来满足分配请求. 大多数分配器都会执行这些修饰符, 但一般不是这样直接指定, 而是将这些行为描述符标志进行分组, 即**类型标志** - - - -###3.3.4 掩码分组 -------- - - - -最后来看**第三部分**, 由于这些标志几乎总是组合使用,内核作了一些分组,包含了用于各种标准情形的适当的标志. 称之为**类型标志**, 定义在[include/linux/gfp.h?v=4.7, lien 194 ~ line 258](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L245) - -类型标志指定所需的行为和区描述符以安城特殊类型的处理, 正因为这一点, 内核总是趋于使用正确的类型标志, 而不是一味地指定它可能用到的多种描述符. 这么做既简单又不容易出错误. - -如果有可能的话, 在内存管理子系统之外, 总是把下列分组之一用于内存分配. 在内核源代码中, 双下划线通常用于内部数据和定义. 而这些预定义的分组名没有双下划线前缀, 点从侧面验证了上述说法. - -```cpp -#define GFP_ATOMIC (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM) -#define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS) -#define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT) -#define GFP_NOWAIT (__GFP_KSWAPD_RECLAIM) -#define GFP_NOIO (__GFP_RECLAIM) -#define GFP_NOFS (__GFP_RECLAIM | __GFP_IO) -#define GFP_TEMPORARY (__GFP_RECLAIM | __GFP_IO | __GFP_FS | \ - __GFP_RECLAIMABLE) -#define GFP_USER (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL) -#define GFP_DMA __GFP_DMA -#define GFP_DMA32 __GFP_DMA32 -#define GFP_HIGHUSER (GFP_USER | __GFP_HIGHMEM) -#define GFP_HIGHUSER_MOVABLE (GFP_HIGHUSER | __GFP_MOVABLE) -#define GFP_TRANSHUGE ((GFP_HIGHUSER_MOVABLE | __GFP_COMP | \ - __GFP_NOMEMALLOC | __GFP_NORETRY | __GFP_NOWARN) & \ - ~__GFP_RECLAIM) - -/* Convert GFP flags to their corresponding migrate type */ -#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE) -#define GFP_MOVABLE_SHIFT 3 -``` - -| 掩码组 | 描述 | -|:-------:|:-----:| -| GFP_ATOMIC | 用于原子分配,在任何情况下都不能中断, 可能使用紧急分配链表中的内存, 这个标志用在中断处理程序, 下半部, 持有自旋锁以及其他不能睡眠的地方 | -| GFP_KERNEL | 这是一种常规的分配方式, 可能会阻塞. 这个标志在睡眠安全时用在进程的长下文代码中. 为了获取调用者所需的内存, 内核会尽力而为. 这个标志应该是首选标志 | -| GFP_KERNEL_ACCOUNT | | -| GFP_NOWAIT | 与GFP_ATOMIC类似, 不同之处在于, 调用不会退给紧急内存池, 这就增加了内存分配失败的可能性 | -| GFP_NOIO | 这种分配可以阻塞, 但不会启动磁盘I/O, 这个标志在不能引发更多的磁盘I/O时阻塞I/O代码, 这可能导致令人不愉快的递归 | -| GFP_NOFS | 这种分配在必要时可以阻塞, 但是也可能启动磁盘, 但是不会启动文件系统操作, 这个标志在你不鞥在启动另一个文件系统操作时, 用在文件系统部分的代码中 | -| GFP_TEMPORARY | | -| GFP_USER | 这是一种常规的分配方式, 可能会阻塞. 这个标志用于为用户空间进程分配内存时使用 | -| GFP_DMA
GFP_DMA32 | 用于分配适用于DMA的内存, 当前是\__GFP_DMA的同义词, GFP_DMA32也是\__GFP_GMA32的同义词 | -| GFP_HIGHUSER | 是GFP_USER的一个扩展, 也用于用户空间. 它允许分配无法直接映射的高端内存. 使用高端内存页是没有坏处的,因为用户过程的地址空间总是通过非线性页表组织的 | -| GFP_HIGHUSER_MOVABLE |用途类似于GFP_HIGHUSER,但分配将从虚拟内存域ZONE_MOVABLE进行 | -| GFP_TRANSHUGE | | - - -* 其中GFP_NOIO和GFP_NOFS, 分别明确禁止I/O操作和访问VFS层, 但同时设置了\__GFP_RECLAIM,因此可以被回收 - -* 而GFP_KERNEL和GFP_USER. 分别是内核和用户分配的默认设置。二者的失败不会立即威胁系统稳定性, GFP_KERNEL绝对是内核源代码中最常使用的标志 | - -最后内核设置了碎片管理的可移动依据组织页的MASK信息GFP_MOVABLE_MASK, 参见[include/linux/gfp.h?v=4.7, line 262](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L262) - -```cpp -/* Convert GFP flags to their corresponding migrate type */ -#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE) -#define GFP_MOVABLE_SHIFT 3 -``` - -在你编写的绝大多数代码中, 用么用到的是GFP_KERNEL, 要么是GFP_ATOMIC, 当然各个类型标志也均有其应用场景 - -| 情形 | 相应标志 | -|:-----:|:----------:| -| 进程上下文, 可以睡眠 | 使用GFP_KERNEL | -| 进程上下文, 不可以睡眠 | 使用GFP_KERNEL, 在你睡眠之前或之后以GFP_KERNEL执行内存分配 | -| 中断处理程序 | 使用GFP_ATMOIC | -| 软中断 | 使用GFP_ATMOIC | -| tasklet | 使用GFP_ATMOIC | -| 需要用于DMA的内存, 可以睡眠 | 使用(GFP_DMA GFP_KERNEL) | -| 需要用于DMA的内存, 不可以睡眠 | 使用(GFP_DMA GFP_ATOMIC), 或在你睡眠之前执行内存分配 | - - -###3.3.5 掩码总结 -------- - -我们从注释中找到这样的信息, 可以作为参考[]() - -```cpp -bit result -================= -0x0 => NORMAL -0x1 => DMA or NORMAL -0x2 => HIGHMEM or NORMAL -0x3 => BAD (DMA+HIGHMEM) -0x4 => DMA32 or DMA or NORMAL -0x5 => BAD (DMA+DMA32) -0x6 => BAD (HIGHMEM+DMA32) -0x7 => BAD (HIGHMEM+DMA32+DMA) -0x8 => NORMAL (MOVABLE+0) -0x9 => DMA or NORMAL (MOVABLE+DMA) -0xa => MOVABLE (Movable is valid only if HIGHMEM is set too) -0xb => BAD (MOVABLE+HIGHMEM+DMA) -0xc => DMA32 (MOVABLE+DMA32) -0xd => BAD (MOVABLE+DMA32+DMA) -0xe => BAD (MOVABLE+DMA32+HIGHMEM) -0xf => BAD (MOVABLE+DMA32+HIGHMEM+DMA) - -GFP_ZONES_SHIFT must be <= 2 on 32 bit platforms. -``` - -##3.4 掩码函数接口 -------- - -很有趣的一点是,没有\__GFP_NORMAL常数,而内存分配的主要负担却落到ZONE_NORMAL内存域 - -内核考虑到这一点, 提供了一个函数gfp_zone来计算与给定分配标志兼容的最高内存域. 那么内存分配可以从该内存域或更低的内存域进行, 该函数定义在[include/linux/gfp.h?v=4.7, line 394](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L394) - -```cpp -static inline enum zone_type gfp_zone(gfp_t flags) -{ - enum zone_type z; - int bit = (__force int) (flags & GFP_ZONEMASK); - - z = (GFP_ZONE_TABLE >> (bit * GFP_ZONES_SHIFT)) & - ((1 << GFP_ZONES_SHIFT) - 1); - VM_BUG_ON((GFP_ZONE_BAD >> bit) & 1); - return z; -} -``` - -其中GFP_ZONES_SHIFT的定义如下, 在[include/linux/gfp.h?v=4.7, line 337](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L337) - - -```cpp -#if defined(CONFIG_ZONE_DEVICE) && (MAX_NR_ZONES-1) <= 4 -/* ZONE_DEVICE is not a valid GFP zone specifier */ -#define GFP_ZONES_SHIFT 2 -#else -#define GFP_ZONES_SHIFT ZONES_SHIFT -#endif - -#if 16 * GFP_ZONES_SHIFT > BITS_PER_LONG -#error GFP_ZONES_SHIFT too large to create GFP_ZONE_TABLE integer -#endif -``` - - -由于内存域修饰符的解释方式不是那么直观, 表3-7给出了该函数结果的一个例子, 其中DMA和DMA32内存域相同. 假定在下文中没有设置\__GFP_MOVABLE修饰符. - -| 修饰符 | 扫描的内存域 | -|:-------:|:--------------:| -| 无 | ZONE_NORMAL、ZONE_DMA | -| \__GFP_DMA | ZONE_DMA | -| \__GFP_DMA & \__GFP_HIGHMEM | ZONE_DMA | -| \__GFP_HIGHMEM | ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA | - -* 如果\__GFP_DMA和\__GFP_HIGHMEM都没有设置, 则首先扫描ZONE_NORMAL, 后面是ZONE_DMA - -* 如果设置了\__GFP_HIGHMEM没有设置__GFP_DMA,则结果是从ZONE_HIGHMEM开始扫描所有3个内存域。= - -* 如果设置了__GFP_DMA,那么\__GFP_HIGHMEM设置与否没有关系. 只有ZONE_DMA用于3种情形. 这是合理的, 因为同时使用\__GFP_HIGHMEM和__GFP_DMA没有意义. 高端内存从来都不适用于DMA - - -设置\__GFP_MOVABLE不会影响内核的决策,除非它与\__GFP_HIGHMEM同时指定. 在这种情况下, 会使用特殊的虚拟内存域ZONE_MOVABLE满足内存分配请求. 对前文描述的内核的反碎片策略而言, 这种行为是必要的. - -除了内存域修饰符之外, 掩码中还可以设置一些标志. - -下图中给出了掩码的布局,以及与各个比特位置关联的常数. \__GFP_DMA32出现了几次,因为它可能位于不同的地方. - - -![GFP掩码的布局](../images/gfp_flag_mask.png) - - - -与内存域修饰符相反, 这些额外的标志并不限制从哪个物理内存段分配内存, 但确实可以改变分配器的行为. 例如, 它们可以修改查找空闲内存时的积极程度. - - - +free_page释放内存空间--Linux内存管理(十八) +======= + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + + + + + +#1 前景回顾 +------- + + +#1.1 内核映射区 +------- + + +尽管`vmalloc`函数族可用于从高端内存域向内核映射页帧(这些在内核空间中通常是无法直接看到的), 但这并不是这些函数的实际用途. + +重要的是强调以下事实 : 内核提供了其他函数用于将`ZONE_HIGHMEM`页帧显式映射到内核空间, 这些函数与vmalloc机制无关. 因此, 这就造成了混乱. + + +而在高端内存的页不能永久地映射到内核地址空间. 因此, 通过alloc_pages()函数以\__GFP_HIGHMEM标志获得的内存页就不可能有逻辑地址. + +在x86_32体系结构总, 高于896MB的所有物理内存的范围大都是高端内存, 它并不会永久地或自动映射到内核地址空间, 尽管X86处理器能够寻址物理RAM的范围达到4GB(启用PAE可以寻址64GB), 一旦这些页被分配, 就必须映射到内核的逻辑地址空间上. 在x86_32上, 高端地址的页被映射到内核地址空间(即虚拟地址空间的3GB~4GB) + +内核地址空间的最后128 MiB用于何种用途呢? + +该部分有3个用途。 + +1. 虚拟内存中连续、但物理内存中不连续的内存区,可以在vmalloc区域分配. 该机制通常用于用户过程, 内核自身会试图尽力避免非连续的物理地址。内核通常会成功,因为大部分大的内存块都在启动时分配给内核,那时内存的碎片尚不严重。但在已经运行了很长时间的系统上, 在内核需要物理内存时, 就可能出现可用空间不连续的情况. 此类情况, 主要出现在动态加载模块时. + +2. 持久映射用于将高端内存域中的非持久页映射到内核中 + +3. 固定映射是与物理地址空间中的固定页关联的虚拟地址空间项,但具体关联的页帧可以自由选择. 它与通过固定公式与物理内存关联的直接映射页相反,虚拟固定映射地址与物理内存位置之间的关联可以自行定义,关联建立后内核总是会注意到的. + +![x86_32上的地址划分](../images/x86_32_mapping.png) + +在这里有两个预处理器符号很重要 \__VMALLOC_RESERVE设置了`vmalloc`区域的长度, 而`MAXMEM`则表示内核可以直接寻址的物理内存的最大可能数量. + +![内核虚拟地址空间](../images/kernel_space.jpg) + + +内核中, 将内存划分为各个区域是通过图3-15所示的各个常数控制的。根据内核和系统配置, 这些常数可能有不同的值。直接映射的边界由high_memory指定。 + + + + +1. 直接映射区 + 线性空间中从3G开始最大896M的区间, 为直接内存映射区,该区域的线性地址和物理地址存在线性转换关系:线性地址=3G+物理地址。 + +2. 动态内存映射区 + 该区域由内核函数`vmalloc`来分配, 特点是 : 线性空间连续, 但是对应的物理空间不一定连续. `vmalloc`分配的线性地址所对应的物理页可能处于低端内存, 也可能处于高端内存. + +3. 永久内存映射区 + 该区域可访问高端内存. 访问方法是使用`alloc_page(_GFP_HIGHMEM)`分配高端内存页或者使用`kmap`函数将分配到的高端内存映射到该区域. + +4. 固定映射区 + 该区域和4G的顶端只有4k的隔离带,其每个地址项都服务于特定的用途,如ACPI_BASE等。 + +>说明 +> +>注意用户空间当然可以使用高端内存,而且是正常的使用,内核在分配那些不经常使用的内存时,都用高端内存空间(如果有),所谓不经常使用是相对来说的,比如内核的一些数据结构就属于经常使用的,而用户的一些数据就属于不经常使用的。用户在启动一个应用程序时,是需要内存的,而每个应用程序都有3G的线性地址,给这些地址映射页表时就可以直接使用高端内存。 +> +>而且还要纠正一点的是:那128M线性地址不仅仅是用在这些地方的,如果你要加载一个设备,而这个设备需要映射其内存到内核中,它也需要使用这段线性地址空间来完成,否则内核就不能访问设备上的内存空间了. +> +>总之,内核的高端线性地址是为了访问内核固定映射以外的内存资源。进程在使用内存时,触发缺页异常,具体将哪些物理页映射给用户进程是内核考虑的事情. 在用户空间中没有高端内存这个概念. + + +#2 kmallc & kfree分配释放连续的物理内存 +------- + +**kmalloc和kzalloc** + + +kmalloc函数与用户空间的malloc一族函数非常类似, 只不过它多了一个flags参数, kmalloc函数是一个简单的接口, 用它可以获取以字节为单位的一块内核内存. + +如果你需要整个页, 那么前面讨论的页分配接口是更好的选择. 但是, 对大多数内核分配来说, kmalloc接口用的更多, 同时内核也提供了kzalloc该接口在kmalloc的基础上会将分配的内存清0. 他们定义在[tools/virtio/linux/kernel.h?v=4.7, line 46](http://lxr.free-electrons.com/source/tools/virtio/linux/kernel.h?v=4.7#L46) + + + + +这两个函数返回一个指向内存块的指针, 其内存块至少要有size大小. 所分配的内存区在物理上是连续的. 在出错时, 它返回NULL. 除非没有足够的内存可用, 否则内核总能分配成功. 在对kmalloc调用之后, 你必须检查返回的是不是NULL, 如果是, 要适当处理错误. + +**kfree释放内存** + +kmalloc的另一端就是kfree, 用于释放分配的内存, kfree声明与 + + +| kmalloc定义 | kzalloc定义 | kfree定义 | +|:--------------:|:--------------:|-----------:| +| [tools/virtio/linux/kernel.h?v=4.7, line 46](http://lxr.free-electrons.com/source/tools/virtio/linux/kernel.h?v=4.7#L46) | [tools/virtio/linux/kernel.h?v=4.7, line 52](http://lxr.free-electrons.com/source/tools/virtio/linux/kernel.h?v=4.7#L52) | [tools/virtio/linux/kernel.h?v=4.7, line 60](http://lxr.free-electrons.com/source/tools/virtio/linux/kernel.h?v=4.7#L60) | +| [include/linux/slab.h, line 466](http://lxr.free-electrons.com/source/include/linux/slab.h?v=4.7#L466) | [include/linux/slab.h?v=4.7, line 620](http://lxr.free-electrons.com/source/include/linux/slab.h?v=4.7#L620) | [mm/slob.c?v=4.7, line 484](http://lxr.free-electrons.com/source/mm/slob.c?v=4.7#L484)
[mm/slub.c?v=4.7, line 3645](http://lxr.free-electrons.com/source/mm/slub.c?v=4.7#L3645)
[mm/slab.c?v=4.7, line 3853](http://lxr.free-electrons.com/source/mm/slab.c?v=4.7#L3853) | + + +#3 分配掩码(gfp_mask标志) +------- + + +##3.1 分配掩码 +------- + +前述所有函数中强制使用的mask参数,到底是什么语义? + +我们知道Linux将内存划分为内存域. 内核提供了所谓的内存域修饰符(zone modifier)(在掩码的最低4个比特位定义), 来指定从哪个内存域分配所需的页. + + +内核使用宏的方式定义了这些掩码, 一个掩码的定义被划分为3个部分进行定义, 我们会逐步展开来讲解, 参见[include/linux/gfp.h?v=4.7, line 12~374](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L12), 共计26个掩码信息, 因此后面__GFP_BITS_SHIFT = 26. + + +##3.2 掩码分类 +------- + + +Linux中这些掩码标志`gfp_mask`分为3种类型 : + + +| 类型 | 描述 | +|:-----:|:-----:| +| 区描述都符 | 内核把物理内存分为多个区, 每个区用于不同的目的, 区描述符指明到底从这些区中的哪一区进行分配 | +| 行为修饰符 | 表示内核应该如何分配所需的内存. 在某些特定情况下, 只能使用某些特定的方法分配内存 | +| 类型标志 | 组合了行为修饰符和区描述符, 将这些可能用到的组合归纳为不同类型 | + + + + + +##3.3 内核中掩码的定义 +------- + + + +###3.3.1 **内核中的定义方式** +------- + + +```cpp +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7 + +/* line 12 ~ line 44 第一部分 + * 定义可掩码所在位的信息, 每个掩码对应一位为1 + * 定义形式为 #define ___GFP_XXX 0x01u + */ +/* Plain integer GFP bitmasks. Do not use this directly. */ +#define ___GFP_DMA 0x01u +#define ___GFP_HIGHMEM 0x02u +#define ___GFP_DMA32 0x04u +#define ___GFP_MOVABLE 0x08u +/* ...... */ + +/* line 46 ~ line 192 第二部分 + * 定义掩码和MASK信息, 第二部分的某些宏可能是第一部分一个或者几个的组合 + * 定义形式为 #define __GFP_XXX ((__force gfp_t)___GFP_XXX) + */ +#define __GFP_DMA ((__force gfp_t)___GFP_DMA) +#define __GFP_HIGHMEM ((__force gfp_t)___GFP_HIGHMEM) +#define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32) +#define __GFP_MOVABLE ((__force gfp_t)___GFP_MOVABLE) /* ZONE_MOVABLE allowed */ +#define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE) + +/* line 194 ~ line 260 第三部分 + * 定义掩码 + * 定义形式为 #define GFP_XXX __GFP_XXX + */ +#define GFP_DMA __GFP_DMA +#define GFP_DMA32 __GFP_DMA32 +``` + + +其中GFP缩写的意思为获取空闲页(get free page), __GFP_MOVABLE不表示物理内存域, 但通知内核应在特殊的虚拟内存域ZONE_MOVABLE进行相应的分配. + + +###3.3.2 定义掩码位 +------- + + +我们首先来看**第一部分**, 内核源代码中定义在[include/linux/gfp.h?v=4.7, line 18 ~ line 44](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L17), 共计26个掩码信息. + + +```cpp +/* Plain integer GFP bitmasks. Do not use this directly. */ +// 区域修饰符 +#define ___GFP_DMA 0x01u +#define ___GFP_HIGHMEM 0x02u +#define ___GFP_DMA32 0x04u + +// 行为修饰符 +#define ___GFP_MOVABLE 0x08u /* 页是可移动的 */ +#define ___GFP_RECLAIMABLE 0x10u /* 页是可回收的 */ +#define ___GFP_HIGH 0x20u /* 应该访问紧急分配池? */ +#define ___GFP_IO 0x40u /* 可以启动物理IO? */ +#define ___GFP_FS 0x80u /* 可以调用底层文件系统? */ +#define ___GFP_COLD 0x100u /* 需要非缓存的冷页 */ +#define ___GFP_NOWARN 0x200u /* 禁止分配失败警告 */ +#define ___GFP_REPEAT 0x400u /* 重试分配,可能失败 */ +#define ___GFP_NOFAIL 0x800u /* 一直重试,不会失败 */ +#define ___GFP_NORETRY 0x1000u /* 不重试,可能失败 */ +#define ___GFP_MEMALLOC 0x2000u /* 使用紧急分配链表 */ +#define ___GFP_COMP 0x4000u /* 增加复合页元数据 */ +#define ___GFP_ZERO 0x8000u /* 成功则返回填充字节0的页 */ +// 类型修饰符 +#define ___GFP_NOMEMALLOC 0x10000u /* 不使用紧急分配链表 */ +#define ___GFP_HARDWALL 0x20000u /* 只允许在进程允许运行的CPU所关联的结点分配内存 */ +#define ___GFP_THISNODE 0x40000u /* 没有备用结点,没有策略 */ +#define ___GFP_ATOMIC 0x80000u /* 用于原子分配,在任何情况下都不能中断 */ +#define ___GFP_ACCOUNT 0x100000u +#define ___GFP_NOTRACK 0x200000u +#define ___GFP_DIRECT_RECLAIM 0x400000u +#define ___GFP_OTHER_NODE 0x800000u +#define ___GFP_WRITE 0x1000000u +#define ___GFP_KSWAPD_RECLAIM 0x2000000u +``` + +###3.3.3 定义掩码 +------- + +然后**第二部分**, 相对而言每一个宏又被重新定义如下, 参见[include/linux/gfp.h?v=4.7, line 46 ~ line 192](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L46) + +```cpp +/* +* Physical address zone modifiers (see linux/mmzone.h - low four bits) +* +* Do not put any conditional on these. If necessary modify the definitions +* without the underscores and use them consistently. The definitions here may +* be used in bit comparisons. +* 定义区描述符 +*/ +#define __GFP_DMA ((__force gfp_t)___GFP_DMA) +#define __GFP_HIGHMEM ((__force gfp_t)___GFP_HIGHMEM) +#define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32) +#define __GFP_MOVABLE ((__force gfp_t)___GFP_MOVABLE) /* ZONE_MOVABLE allowed */ +#define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE) + +/* +* Page mobility and placement hints +* +* These flags provide hints about how mobile the page is. Pages with similar +* mobility are placed within the same pageblocks to minimise problems due +* to external fragmentation. +* +* __GFP_MOVABLE (also a zone modifier) indicates that the page can be +* moved by page migration during memory compaction or can be reclaimed. +* +* __GFP_RECLAIMABLE is used for slab allocations that specify +* SLAB_RECLAIM_ACCOUNT and whose pages can be freed via shrinkers. +* +* __GFP_WRITE indicates the caller intends to dirty the page. Where possible, +* these pages will be spread between local zones to avoid all the dirty +* pages being in one zone (fair zone allocation policy). +* +* __GFP_HARDWALL enforces the cpuset memory allocation policy. +* +* __GFP_THISNODE forces the allocation to be satisified from the requested +* node with no fallbacks or placement policy enforcements. +* +* __GFP_ACCOUNT causes the allocation to be accounted to kmemcg (only relevant +* to kmem allocations). +*/ +#define __GFP_RECLAIMABLE ((__force gfp_t)___GFP_RECLAIMABLE) +#define __GFP_WRITE ((__force gfp_t)___GFP_WRITE) +#define __GFP_HARDWALL ((__force gfp_t)___GFP_HARDWALL) +#define __GFP_THISNODE ((__force gfp_t)___GFP_THISNODE) +#define __GFP_ACCOUNT ((__force gfp_t)___GFP_ACCOUNT) + +/* +* Watermark modifiers -- controls access to emergency reserves +* +* __GFP_HIGH indicates that the caller is high-priority and that granting +* the request is necessary before the system can make forward progress. +* For example, creating an IO context to clean pages. +* +* __GFP_ATOMIC indicates that the caller cannot reclaim or sleep and is +* high priority. Users are typically interrupt handlers. This may be +* used in conjunction with __GFP_HIGH + * + * __GFP_MEMALLOC allows access to all memory. This should only be used when + * the caller guarantees the allocation will allow more memory to be freed + * very shortly e.g. process exiting or swapping. Users either should + * be the MM or co-ordinating closely with the VM (e.g. swap over NFS). + * + * __GFP_NOMEMALLOC is used to explicitly forbid access to emergency reserves. + * This takes precedence over the __GFP_MEMALLOC flag if both are set. + */ +#define __GFP_ATOMIC ((__force gfp_t)___GFP_ATOMIC) +#define __GFP_HIGH ((__force gfp_t)___GFP_HIGH) +#define __GFP_MEMALLOC ((__force gfp_t)___GFP_MEMALLOC) +#define __GFP_NOMEMALLOC ((__force gfp_t)___GFP_NOMEMALLOC) + +/* + * Reclaim modifiers + * + * __GFP_IO can start physical IO. + * + * __GFP_FS can call down to the low-level FS. Clearing the flag avoids the + * allocator recursing into the filesystem which might already be holding + * locks. + * + * __GFP_DIRECT_RECLAIM indicates that the caller may enter direct reclaim. + * This flag can be cleared to avoid unnecessary delays when a fallback + * option is available. + * + * __GFP_KSWAPD_RECLAIM indicates that the caller wants to wake kswapd when + * the low watermark is reached and have it reclaim pages until the high + * watermark is reached. A caller may wish to clear this flag when fallback + * options are available and the reclaim is likely to disrupt the system. The + * canonical example is THP allocation where a fallback is cheap but + * reclaim/compaction may cause indirect stalls. + * + * __GFP_RECLAIM is shorthand to allow/forbid both direct and kswapd reclaim. + * + * __GFP_REPEAT: Try hard to allocate the memory, but the allocation attempt + * _might_ fail. This depends upon the particular VM implementation. + * + * __GFP_NOFAIL: The VM implementation _must_ retry infinitely: the caller + * cannot handle allocation failures. New users should be evaluated carefully + * (and the flag should be used only when there is no reasonable failure + * policy) but it is definitely preferable to use the flag rather than + * opencode endless loop around allocator. + * + * __GFP_NORETRY: The VM implementation must not retry indefinitely and will + * return NULL when direct reclaim and memory compaction have failed to allow + * the allocation to succeed. The OOM killer is not called with the current + * implementation. + */ +#define __GFP_IO ((__force gfp_t)___GFP_IO) +#define __GFP_FS ((__force gfp_t)___GFP_FS) +#define __GFP_DIRECT_RECLAIM ((__force gfp_t)___GFP_DIRECT_RECLAIM) /* Caller can reclaim */ +#define __GFP_KSWAPD_RECLAIM ((__force gfp_t)___GFP_KSWAPD_RECLAIM) /* kswapd can wake */ +#define __GFP_RECLAIM ((__force gfp_t)(___GFP_DIRECT_RECLAIM|___GFP_KSWAPD_RECLAIM)) +#define __GFP_REPEAT ((__force gfp_t)___GFP_REPEAT) +#define __GFP_NOFAIL ((__force gfp_t)___GFP_NOFAIL) +#define __GFP_NORETRY ((__force gfp_t)___GFP_NORETRY) + +/* + * Action modifiers + * + * __GFP_COLD indicates that the caller does not expect to be used in the near + * future. Where possible, a cache-cold page will be returned. + * + * __GFP_NOWARN suppresses allocation failure reports. + * + * __GFP_COMP address compound page metadata. + * + * __GFP_ZERO returns a zeroed page on success. + * + * __GFP_NOTRACK avoids tracking with kmemcheck. + * + * __GFP_NOTRACK_FALSE_POSITIVE is an alias of __GFP_NOTRACK. It's a means of + * distinguishing in the source between false positives and allocations that + * cannot be supported (e.g. page tables). + * + * __GFP_OTHER_NODE is for allocations that are on a remote node but that + * should not be accounted for as a remote allocation in vmstat. A + * typical user would be khugepaged collapsing a huge page on a remote + * node. + */ +#define __GFP_COLD ((__force gfp_t)___GFP_COLD) +#define __GFP_NOWARN ((__force gfp_t)___GFP_NOWARN) +#define __GFP_COMP ((__force gfp_t)___GFP_COMP) +#define __GFP_ZERO ((__force gfp_t)___GFP_ZERO) +#define __GFP_NOTRACK ((__force gfp_t)___GFP_NOTRACK) +#define __GFP_NOTRACK_FALSE_POSITIVE (__GFP_NOTRACK) +#define __GFP_OTHER_NODE ((__force gfp_t)___GFP_OTHER_NODE) + +/* Room for N __GFP_FOO bits */ +#define __GFP_BITS_SHIFT 26 +#define __GFP_BITS_MASK ((__force gfp_t)((1 << __GFP_BITS_SHIFT) - 1)) +``` + + +给出的常数,其中一些很少使用,因此我不会讨论。其中最重要的一些常数语义如下所示 + + +其中在开始的位置定义了对应的**区修饰符**, 定义在[include/linux/gfp.h?v=4.7, line 46 ~ line 57](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L46) + + +| 区修饰符标志 | 描述 | +|:--------------:|:-----:| +| \__GFP_DMA | 从ZONE_DMA中分配内存 | +| \__GFP_HIGHMEM | 从ZONE_HIGHMEM活ZONE_NORMAL中分配内存 | +| \__GFP_DMA32 | 从ZONE_DMA32中分配内存 | +| \__GFP_MOVABLE | 从__GFP_MOVABLE中分配内存 | + +其次还定义了我们程序和函数中所需要的掩码MASK的信息, 由于其中__GFP_DMA, __GFP_DMA32, __GFP_HIGHMEM, __GFP_MOVABLE是在内存中分别有对应的内存域信息, 因此我们定义了内存域的掩码GFP_ZONEMASK, 参见[include/linux/gfp.h?v=4.7, line 57](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L57) + +```cpp +#define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE) +``` + + +接着内核定义了**行为修饰符** + +/* \__GFP_WAIT表示分配内存的请求可以中断。也就是说,调度器在该请求期间可随意选择另一个过程执行,或者该请求可以被另一个更重要的事件中断. 分配器还可以在返回内存之前, 在队列上等待一个事件(相关进程会进入睡眠状态). + +>虽然名字相似,但__GFP_HIGH与__GFP_HIGHMEM毫无关系,请不要弄混这两者\ + +| 行为修饰符 | 描述 | +|:---:|:----:| +| \__GFP_RECLAIMABLE
\__GFP_MOVABLE | 是页迁移机制所需的标志. 顾名思义,它们分别将分配的内存标记为可回收的或可移动的。这影响从空闲列表的哪个子表获取内存 | +| \__GFP_WRITE | | +| \__GFP_HARDWALL | 只在NUMA系统上有意义. 它限制只在分配到当前进程的各个CPU所关联的结点分配内存。如果进程允许在所有CPU上运行(默认情况),该标志是无意义的。只有进程可以运行的CPU受限时,该标志才有效果 | +| \__GFP_THISNODE | 也只在NUMA系统上有意义。如果设置该比特位,则内存分配失败的情况下不允许使用其他结点作为备用,需要保证在当前结点或者明确指定的结点上成功分配内存 | +| \__GFP_ACCOUNT | | +|--------|--------| +| \__GFP_ATOMIC | | +| \__GFP_HIGH | 如果请求非常重要, 则设置\__GFP_HIGH,即内核急切地需要内存时。在分配内存失败可能给内核带来严重后果时(比如威胁到系统稳定性或系统崩溃), 总是会使用该标志 | +| \__GFP_MEMALLOC | | +| \__GFP_NOMEMALLOC | | +|--------|--------| +| \__GFP_IO |说明在查找空闲内存期间内核可以进行I/O操作. 实际上, 这意味着如果内核在内存分配期间换出页, 那么仅当设置该标志时, 才能将选择的页写入硬盘 | +| \__GFP_FS |允许内核执行VFS操作. 在与VFS层有联系的内核子系统中必须禁用, 因为这可能引起循环递归调用. | +| \__GFP_DIRECT_RECLAIM | | +| \__GFP_KSWAPD_RECLAIM | | +| \__GFP_RECLAIM | | +| \__GFP_REPEAT | 在分配失败后自动重试,但在尝试若干次之后会停止 | +| \__GFP_NOFAIL | 在分配失败后一直重试,直至成功 | +| \__GFP_NORETRY | 在分配失败后不重试,因此可能分配失败 | +|--------|--------| +| \__GFP_COLD | 如果需要分配不在CPU高速缓存中的“冷”页时,则设置\__GFP_COLD | +| \__GFP_NOWARN | 在分配失败时禁止内核故障警告。在极少数场合该标志有用 | +| \__GFP_COMP | 添加混合页元素, 在hugetlb的代码内部使用 | +| \__GFP_ZERO | 在分配成功时,将返回填充字节0的页 | +| \__GFP_NOTRACK | | +| \__GFP_NOTRACK_FALSE_POSITIVE
\__GFP_NOTRACK | | +| \__GFP_OTHER_NODE | | + + +那自然还有__GFP_BITS_SHIFT来表示我们所有的掩码位, 由于我们共计26个掩码位 + +```cpp +/* Room for N __GFP_FOO bits */ +#define __GFP_BITS_SHIFT 26 +#define __GFP_BITS_MASK ((__force gfp_t)((1 << __GFP_BITS_SHIFT) - 1)) +``` + +可以同时指定这些分配标志, 例如 + +```cpp +ptr = kmalloc(size, __GFP_IO | __GFP_FS); +``` + +说明页分配器(最终会调用alloc_page)在分配时可以执行I/O, 在必要时还可以执行文件系统操作. 这就让内核有很大的自由度, 以便它尽可能找到空闲的内存来满足分配请求. 大多数分配器都会执行这些修饰符, 但一般不是这样直接指定, 而是将这些行为描述符标志进行分组, 即**类型标志** + + + +###3.3.4 掩码分组 +------- + + + +最后来看**第三部分**, 由于这些标志几乎总是组合使用,内核作了一些分组,包含了用于各种标准情形的适当的标志. 称之为**类型标志**, 定义在[include/linux/gfp.h?v=4.7, lien 194 ~ line 258](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L245) + +类型标志指定所需的行为和区描述符以安城特殊类型的处理, 正因为这一点, 内核总是趋于使用正确的类型标志, 而不是一味地指定它可能用到的多种描述符. 这么做既简单又不容易出错误. + +如果有可能的话, 在内存管理子系统之外, 总是把下列分组之一用于内存分配. 在内核源代码中, 双下划线通常用于内部数据和定义. 而这些预定义的分组名没有双下划线前缀, 点从侧面验证了上述说法. + +```cpp +#define GFP_ATOMIC (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM) +#define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS) +#define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT) +#define GFP_NOWAIT (__GFP_KSWAPD_RECLAIM) +#define GFP_NOIO (__GFP_RECLAIM) +#define GFP_NOFS (__GFP_RECLAIM | __GFP_IO) +#define GFP_TEMPORARY (__GFP_RECLAIM | __GFP_IO | __GFP_FS | \ + __GFP_RECLAIMABLE) +#define GFP_USER (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL) +#define GFP_DMA __GFP_DMA +#define GFP_DMA32 __GFP_DMA32 +#define GFP_HIGHUSER (GFP_USER | __GFP_HIGHMEM) +#define GFP_HIGHUSER_MOVABLE (GFP_HIGHUSER | __GFP_MOVABLE) +#define GFP_TRANSHUGE ((GFP_HIGHUSER_MOVABLE | __GFP_COMP | \ + __GFP_NOMEMALLOC | __GFP_NORETRY | __GFP_NOWARN) & \ + ~__GFP_RECLAIM) + +/* Convert GFP flags to their corresponding migrate type */ +#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE) +#define GFP_MOVABLE_SHIFT 3 +``` + +| 掩码组 | 描述 | +|:-------:|:-----:| +| GFP_ATOMIC | 用于原子分配,在任何情况下都不能中断, 可能使用紧急分配链表中的内存, 这个标志用在中断处理程序, 下半部, 持有自旋锁以及其他不能睡眠的地方 | +| GFP_KERNEL | 这是一种常规的分配方式, 可能会阻塞. 这个标志在睡眠安全时用在进程的长下文代码中. 为了获取调用者所需的内存, 内核会尽力而为. 这个标志应该是首选标志 | +| GFP_KERNEL_ACCOUNT | | +| GFP_NOWAIT | 与GFP_ATOMIC类似, 不同之处在于, 调用不会退给紧急内存池, 这就增加了内存分配失败的可能性 | +| GFP_NOIO | 这种分配可以阻塞, 但不会启动磁盘I/O, 这个标志在不能引发更多的磁盘I/O时阻塞I/O代码, 这可能导致令人不愉快的递归 | +| GFP_NOFS | 这种分配在必要时可以阻塞, 但是也可能启动磁盘, 但是不会启动文件系统操作, 这个标志在你不鞥在启动另一个文件系统操作时, 用在文件系统部分的代码中 | +| GFP_TEMPORARY | | +| GFP_USER | 这是一种常规的分配方式, 可能会阻塞. 这个标志用于为用户空间进程分配内存时使用 | +| GFP_DMA
GFP_DMA32 | 用于分配适用于DMA的内存, 当前是\__GFP_DMA的同义词, GFP_DMA32也是\__GFP_GMA32的同义词 | +| GFP_HIGHUSER | 是GFP_USER的一个扩展, 也用于用户空间. 它允许分配无法直接映射的高端内存. 使用高端内存页是没有坏处的,因为用户过程的地址空间总是通过非线性页表组织的 | +| GFP_HIGHUSER_MOVABLE |用途类似于GFP_HIGHUSER,但分配将从虚拟内存域ZONE_MOVABLE进行 | +| GFP_TRANSHUGE | | + + +* 其中GFP_NOIO和GFP_NOFS, 分别明确禁止I/O操作和访问VFS层, 但同时设置了\__GFP_RECLAIM,因此可以被回收 + +* 而GFP_KERNEL和GFP_USER. 分别是内核和用户分配的默认设置。二者的失败不会立即威胁系统稳定性, GFP_KERNEL绝对是内核源代码中最常使用的标志 | + +最后内核设置了碎片管理的可移动依据组织页的MASK信息GFP_MOVABLE_MASK, 参见[include/linux/gfp.h?v=4.7, line 262](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L262) + +```cpp +/* Convert GFP flags to their corresponding migrate type */ +#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE) +#define GFP_MOVABLE_SHIFT 3 +``` + +在你编写的绝大多数代码中, 用么用到的是GFP_KERNEL, 要么是GFP_ATOMIC, 当然各个类型标志也均有其应用场景 + +| 情形 | 相应标志 | +|:-----:|:----------:| +| 进程上下文, 可以睡眠 | 使用GFP_KERNEL | +| 进程上下文, 不可以睡眠 | 使用GFP_KERNEL, 在你睡眠之前或之后以GFP_KERNEL执行内存分配 | +| 中断处理程序 | 使用GFP_ATMOIC | +| 软中断 | 使用GFP_ATMOIC | +| tasklet | 使用GFP_ATMOIC | +| 需要用于DMA的内存, 可以睡眠 | 使用(GFP_DMA GFP_KERNEL) | +| 需要用于DMA的内存, 不可以睡眠 | 使用(GFP_DMA GFP_ATOMIC), 或在你睡眠之前执行内存分配 | + + +###3.3.5 掩码总结 +------- + +我们从注释中找到这样的信息, 可以作为参考[]() + +```cpp +bit result +================= +0x0 => NORMAL +0x1 => DMA or NORMAL +0x2 => HIGHMEM or NORMAL +0x3 => BAD (DMA+HIGHMEM) +0x4 => DMA32 or DMA or NORMAL +0x5 => BAD (DMA+DMA32) +0x6 => BAD (HIGHMEM+DMA32) +0x7 => BAD (HIGHMEM+DMA32+DMA) +0x8 => NORMAL (MOVABLE+0) +0x9 => DMA or NORMAL (MOVABLE+DMA) +0xa => MOVABLE (Movable is valid only if HIGHMEM is set too) +0xb => BAD (MOVABLE+HIGHMEM+DMA) +0xc => DMA32 (MOVABLE+DMA32) +0xd => BAD (MOVABLE+DMA32+DMA) +0xe => BAD (MOVABLE+DMA32+HIGHMEM) +0xf => BAD (MOVABLE+DMA32+HIGHMEM+DMA) + +GFP_ZONES_SHIFT must be <= 2 on 32 bit platforms. +``` + +##3.4 掩码函数接口 +------- + +很有趣的一点是,没有\__GFP_NORMAL常数,而内存分配的主要负担却落到ZONE_NORMAL内存域 + +内核考虑到这一点, 提供了一个函数gfp_zone来计算与给定分配标志兼容的最高内存域. 那么内存分配可以从该内存域或更低的内存域进行, 该函数定义在[include/linux/gfp.h?v=4.7, line 394](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L394) + +```cpp +static inline enum zone_type gfp_zone(gfp_t flags) +{ + enum zone_type z; + int bit = (__force int) (flags & GFP_ZONEMASK); + + z = (GFP_ZONE_TABLE >> (bit * GFP_ZONES_SHIFT)) & + ((1 << GFP_ZONES_SHIFT) - 1); + VM_BUG_ON((GFP_ZONE_BAD >> bit) & 1); + return z; +} +``` + +其中GFP_ZONES_SHIFT的定义如下, 在[include/linux/gfp.h?v=4.7, line 337](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L337) + + +```cpp +#if defined(CONFIG_ZONE_DEVICE) && (MAX_NR_ZONES-1) <= 4 +/* ZONE_DEVICE is not a valid GFP zone specifier */ +#define GFP_ZONES_SHIFT 2 +#else +#define GFP_ZONES_SHIFT ZONES_SHIFT +#endif + +#if 16 * GFP_ZONES_SHIFT > BITS_PER_LONG +#error GFP_ZONES_SHIFT too large to create GFP_ZONE_TABLE integer +#endif +``` + + +由于内存域修饰符的解释方式不是那么直观, 表3-7给出了该函数结果的一个例子, 其中DMA和DMA32内存域相同. 假定在下文中没有设置\__GFP_MOVABLE修饰符. + +| 修饰符 | 扫描的内存域 | +|:-------:|:--------------:| +| 无 | ZONE_NORMAL、ZONE_DMA | +| \__GFP_DMA | ZONE_DMA | +| \__GFP_DMA & \__GFP_HIGHMEM | ZONE_DMA | +| \__GFP_HIGHMEM | ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA | + +* 如果\__GFP_DMA和\__GFP_HIGHMEM都没有设置, 则首先扫描ZONE_NORMAL, 后面是ZONE_DMA + +* 如果设置了\__GFP_HIGHMEM没有设置__GFP_DMA,则结果是从ZONE_HIGHMEM开始扫描所有3个内存域。= + +* 如果设置了__GFP_DMA,那么\__GFP_HIGHMEM设置与否没有关系. 只有ZONE_DMA用于3种情形. 这是合理的, 因为同时使用\__GFP_HIGHMEM和__GFP_DMA没有意义. 高端内存从来都不适用于DMA + + +设置\__GFP_MOVABLE不会影响内核的决策,除非它与\__GFP_HIGHMEM同时指定. 在这种情况下, 会使用特殊的虚拟内存域ZONE_MOVABLE满足内存分配请求. 对前文描述的内核的反碎片策略而言, 这种行为是必要的. + +除了内存域修饰符之外, 掩码中还可以设置一些标志. + +下图中给出了掩码的布局,以及与各个比特位置关联的常数. \__GFP_DMA32出现了几次,因为它可能位于不同的地方. + + +![GFP掩码的布局](../images/gfp_flag_mask.png) + + + +与内存域修饰符相反, 这些额外的标志并不限制从哪个物理内存段分配内存, 但确实可以改变分配器的行为. 例如, 它们可以修改查找空闲内存时的积极程度. + + + diff --git a/study/kernel/02-memory/04-buddy/07-highmem_mapping/06-vmalloc-(non_contiguous_pages)/README.md b/study/kernel/02-memory/04-buddy/07-highmem_mapping/06-vmalloc-(non_contiguous_pages)/README.md index 224eb10..d267bf3 100644 --- a/study/kernel/02-memory/04-buddy/07-highmem_mapping/06-vmalloc-(non_contiguous_pages)/README.md +++ b/study/kernel/02-memory/04-buddy/07-highmem_mapping/06-vmalloc-(non_contiguous_pages)/README.md @@ -1,506 +1,506 @@ -内存中不连续的页的分配 -======= - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - - - - -在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法. - -Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述. - -伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者. - -* 内核如何记住哪些内存块是空闲的 - -* 分配空闲页面的方法 - -* 影响分配器行为的众多标识位 - -* 内存碎片的问题和分配器如何处理碎片 - - -#1 内存中不连续的页的分配 -------- - - -根据上文的讲述, 我们知道物理上连续的映射对内核是最好的, 但并不总能成功地使用. 在分配一大块内存时, 可能竭尽全力也无法找到连续的内存块. - -在用户空间中这不是问题,因为普通进程设计为使用处理器的分页机制, 当然这会降低速度并占用TLB. - -在内核中也可以使用同样的技术. 内核分配了其内核虚拟地址空间的一部分, 用于建立连续映射. - - -在IA-32系统中, 前16M划分给DMA区域, 后面一直到第896M作为NORMAL直接映射区, 紧随直接映射的前896MB物理内存,在插入的8MB安全隙之后, 是一个用于管理不连续内存的区域. 这一段具有线性地址空间的所有性质. 分配到其中的页可能位于物理内存中的任何地方. 通过修改负责该区域的内核页表, 即可做到这一点. - -![内核虚拟地址空间的划分](../images/kernel_space.jpg) - -![内核虚拟地址空间的划分](../images/vmalloc_space.png) - -Persistent mappings和Fixmaps地址空间都比较小, 这里我们忽略它们, 这样只剩下直接地址映射和VMALLOC区, 这个划分应该是平衡两个需求的结果 - -1. 尽量增加DMA和Normal区大小,也就是直接映射地址空间大小,当前主流平台的内存,基本上都超过了512MB,很多都是标配1GB内存,因此注定有一部分内存无法进行线性映射。 - -2. 保留一定数量的VMALLOC大小,这个值是应用平台特定的,如果应用平台某个驱动需要用vmalloc分配很大的地址空间,那么最好通过在kernel参数中指定vmalloc大小的方法,预留较多的vmalloc地址空间。 - -3. 并不是Highmem没有或者越少越好,这个是我的个人理解,理由如下:高端内存就像个垃圾桶和缓冲区,防止来自用户空间或者vmalloc的映射破坏Normal zone和DMA zone的连续性,使得它们碎片化。当这个垃圾桶较大时,那么污染Normal 和DMA的机会自然就小了。 - -通过这种方式, 将内核的内核虚拟地址空间划分为几个不同的区域 - -下面的图是VMALLOC地址空间内部划分情况 - - - -![内核虚拟地址空间的划分](../images/vmalloc.jpg) - - -#2 用vmalloc分配内存 -------- - -vmalloc是一个接口函数, 内核代码使用它来分配在虚拟内存中连续但在物理内存中不一定连续的内存 - -```cpp -// http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L70 -void *vmalloc(unsigned long size); -``` - -该函数只需要一个参数, 用于指定所需内存区的长度, 与此前讨论的函数不同, 其长度单位不是页而是字节, 这在用户空间程序设计中是很普遍的. - -使用vmalloc的最著名的实例是内核对模块的实现. 因为模块可能在任何时候加载, 如果模块数据比较多, 那么无法保证有足够的连续内存可用, 特别是在系统已经运行了比较长时间的情况下. - -如果能够用小块内存拼接出足够的内存, 那么使用vmalloc可以规避该问题 - -内核中还有大约[400处地方调用了vmalloc](http://lxr.free-electrons.com/ident?v=4.7;i=vmalloc), 特别是在设备和声音驱动程序中. - -因为用于vmalloc的内存页总是必须映射在内核地址空间中, 因此使用`ZONE_HIGHMEM`内存域的页要优于其他内存域. 这使得内核可以节省更宝贵的较低端内存域, 而又不会带来额外的坏处. 因此, `vmalloc`等映射函数是内核出于自身的目的(并非因为用户空间应用程序)使用高端内存页的少数情形之一. - -所有有关vmalloc的数据结构和API结构声明在[`include/linux/vmalloc.h`](http://lxr.free-electrons.com/source/include/linux/vmalloc.h)和[``]() - -| 声明头文件 | `NON-MMU`实现 | `MMU`实现 | -|:------------:|:------------------:|:-----------:| -| [`include/linux/vmalloc.h`](http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7) | [`mm/nommu.c`](http://lxr.free-electrons.com/source/mm/nommu.c?v=4.7) | [`mm/vmalloc.c`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7) | - -##2.1 数据结构 -------- - - -内核在管理虚拟内存中的`vmalloc`区域时, 内核必须跟踪哪些子区域被使用、哪些是空闲的. 为此定义了一个数据结构`vm_struct`, 将所有使用的部分保存在一个链表中. 该结构提的定义在[include/linux/vmalloc.h?v=4.7, line 32](http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L32) - - -```cpp -// http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L32 -struct vm_struct { - struct vm_struct *next; - void *addr; - unsigned long size; - unsigned long flags; - struct page **pages; - unsigned int nr_pages; - phys_addr_t phys_addr; - const void *caller; -}; -``` - ->注意, 内核使用了一个重要的数据结构称之为`vm_area_struct`, 以管理用户空间进程的虚拟地址空间内容. 尽管名称和目的都是类似的, 虽然二者都是做虚拟地址空间映射的, 但不能混淆这两个结构。 -> -> -> ->1. 前者是内核虚拟地址空间映射,而后者则是应用进程虚拟地址空间映射。 -> ->2. 前者不会产生page fault,而后者一般不会提前分配页面,只有当访问的时候,产生page fault来分配页面。 - - - - -对于每个用vmalloc分配的子区域, 都对应于内核内存中的一个该结构实例. 该结构各个成员的语义如下 - -| 字段 | 描述 | -|:-----:|:-----:| -| next | 使得内核可以将vmalloc区域中的所有子区域保存在一个单链表上 | -| addr |定义了分配的子区域在虚拟地址空间中的起始地址。size表示该子区域的长度. 可以根据该信息来勾画出vmalloc区域的完整分配方案 | -| flags | 存储了与该内存区关联的标志集合, 这几乎是不可避免的. 它只用于指定内存区类型 | -| pages | 是一个指针,指向page指针的数组。每个数组成员都表示一个映射到虚拟地址空间中的物理内存页的page实例 | -| nr_pages | 指定pages中数组项的数目,即涉及的内存页数目 | -| phys_addr | 仅当用ioremap映射了由物理地址描述的物理内存区域时才需要。该信息保存在phys_addr中 | -| caller | | - -其中`flags`只用于指定内存区类型, 所有可能的flag标识以宏的形式定义在[include/linux/vmalloc.h?v=4.7, line 14](http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L14) - -```cpp -// http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L14 -/* bits in flags of vmalloc's vm_struct below */ -#define VM_IOREMAP 0x00000001 /* ioremap() and friends */ -#define VM_ALLOC 0x00000002 /* vmalloc() */ -#define VM_MAP 0x00000004 /* vmap()ed pages */ -#define VM_USERMAP 0x00000008 /* suitable for remap_vmalloc_range */ -#define VM_UNINITIALIZED 0x00000020 /* vm_struct is not fully initialized */ -#define VM_NO_GUARD 0x00000040 /* don't add guard page */ -#define VM_KASAN 0x00000080 /* has allocated kasan shadow memory */ -/* bits [20..32] reserved for arch specific ioremap internals */ -``` - -| flag标识 | 描述 | -|:-----:|:-----:| -| VM_IOREMAP | 表示将几乎随机的物理内存区域映射到vmalloc区域中. 这是一个特定于体系结构的操作 | -| VM_ALLOC | 指定由vmalloc产生的子区域 | -| VM_MAP | 用于表示将现存pages集合映射到连续的虚拟地址空间中 | -| VM_USERMAP | | -| VM_UNINITIALIZED | | -| VM_NO_GUARD | | -| VM_KASAN | | - -下图给出了该结构使用方式的一个实例. 其中依次映射了3个(假想的)物理内存页, 在物理内存中的位置分别是1 023、725和7 311. 在虚拟的vmalloc区域中, 内核将其看作起始于VMALLOC_START + 100的一个连续内存区, 大小为3*PAGE_SIZE的内核地址空间,被映射到物理页面725, 1023和7311 - -![将物理内存页映射到vmalloc区域](../images/vmlloc_map.jpg) - - -##2.2创建vm_area -------- - -因为大部分体系结构都支持`mmu,` 这里我们只考虑有`mmu的`情况. 实际上没有`mmu`支持时, `vmalloc`就无法实现非连续物理地址到连续内核地址空间的映射, `vmalloc`退化为`kmalloc`实现. - -###2.2.1 vmlist全局链表 -------- - -在创建一个新的虚拟内存区之前, 必须找到一个适当的位置. `vm_area`实例组成的一个链表, 管理着`vmalloc`区域中已经建立的各个子区域. 定义在[`mm/vmalloc`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1170)的全局变量[`vmlist`]( http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1170)是表头. 定义在[mm/vmalloc.c?v=4.7, line 1170]( http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1170) - - -```cpp -// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1170 -static struct vm_struct *vmlist __initdata; -``` - - - -###2.2.2 分配函数 -------- - -内核在mm/vmalloc中提供了辅助函数`get_vm_area`和`__get_vm_area`, 它们负责参数准备工作, 而实际的分配工作交给底层函数`__get_vm_area_node`来完成, 这些函数定义在[mm/vmalloc.c?v=4.7, line 1388](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1388) - -```cpp -struct vm_struct *__get_vm_area(unsigned long size, unsigned long flags, - unsigned long start, unsigned long end) -{ - return __get_vm_area_node(size, 1, flags, start, end, NUMA_NO_NODE, - GFP_KERNEL, __builtin_return_address(0)); -} -EXPORT_SYMBOL_GPL(__get_vm_area); - -struct vm_struct *__get_vm_area_caller(unsigned long size, unsigned long flags, - unsigned long start, unsigned long end, - const void *caller) -{ - return __get_vm_area_node(size, 1, flags, start, end, NUMA_NO_NODE, - GFP_KERNEL, caller); -} - -/** - * get_vm_area - reserve a contiguous kernel virtual area - * @size: size of the area - * @flags: %VM_IOREMAP for I/O mappings or VM_ALLOC - * - * Search an area of @size in the kernel virtual mapping area, - * and reserved it for out purposes. Returns the area descriptor - * on success or %NULL on failure. - */ -struct vm_struct *get_vm_area(unsigned long size, unsigned long flags) -{ - return __get_vm_area_node(size, 1, flags, VMALLOC_START, VMALLOC_END, - NUMA_NO_NODE, GFP_KERNEL, - __builtin_return_address(0)); -} - -struct vm_struct *get_vm_area_caller(unsigned long size, unsigned long flags, - const void *caller) -{ - return __get_vm_area_node(size, 1, flags, VMALLOC_START, VMALLOC_END, - NUMA_NO_NODE, GFP_KERNEL, caller); -} -``` - -这些函数是负责实际工作的`__get_vm_area_node`函数的前端. 根据子区域的长度信息, `__get_vm_area_node`函数试图在虚拟的`vmalloc`空间中找到一个适当的位置. 该函数定义在[mm/vmalloc.c?v=4.7, line 1354](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1354) - - - 由于各个`vmalloc`子区域之间需要插入1页(警戒页)作为安全隙, 内核首先适当提高需要分配的内存长度. - -```cpp -static struct vm_struct *__get_vm_area_node(unsigned long size, - unsigned long align, unsigned long flags, unsigned long start, - unsigned long end, int node, gfp_t gfp_mask, const void *caller) -{ - struct vmap_area *va; - struct vm_struct *area; - - BUG_ON(in_interrupt()); - if (flags & VM_IOREMAP) - align = 1ul << clamp_t(int, fls_long(size), - PAGE_SHIFT, IOREMAP_MAX_ORDER); - - size = PAGE_ALIGN(size); - if (unlikely(!size)) - return NULL; - - area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node); - if (unlikely(!area)) - return NULL; - - if (!(flags & VM_NO_GUARD)) - size += PAGE_SIZE; - - va = alloc_vmap_area(size, align, start, end, node, gfp_mask); - if (IS_ERR(va)) { - kfree(area); - return NULL; - } - - setup_vmalloc_vm(area, va, flags, caller); - - return area; -} -``` - -`start`和`end`参数分别由调用者设置, 比如`get_vm_area`函数和`get_vm_area_caller`函数传入`VMALLOC_START`和`VMALLOC_END`. 接下来循环遍历vmlist的所有表元素,直至找到一个适当的项 - - -###2.2.3 释放函数 -------- - -[`remove_vm_area`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1454)函数将一个现存的子区域从vmalloc地址空间删除. - - -函数声明如下, [`include/linux/vmalloc.h?v=4.7, line 121`](http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L121) -```cpp -// http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L121 -struct vm_struct *remove_vm_area(void *addr); -``` - -函数定义在[`mm/vmalloc.c?v=4.7, line 1454`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1454) -```cpp -// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1446 -/** - * remove_vm_area - find and remove a continuous kernel virtual area - * @addr: base address - * - * Search for the kernel VM area starting at @addr, and remove it. - * This function returns the found VM area, but using it is NOT safe - * on SMP machines, except for its size or flags. - */ -struct vm_struct *remove_vm_area(const void *addr) -{ - struct vmap_area *va; - - va = find_vmap_area((unsigned long)addr); - if (va && va->flags & VM_VM_AREA) { - struct vm_struct *vm = va->vm; - - spin_lock(&vmap_area_lock); - va->vm = NULL; - va->flags &= ~VM_VM_AREA; - spin_unlock(&vmap_area_lock); - - vmap_debug_free_range(va->va_start, va->va_end); - kasan_free_shadow(vm); - free_unmap_vmap_area(va); - - return vm; - } - return NULL; -} -``` - - - -##2.3 vmalloc分配内存区 -------- - -`vmalloc`发起对不连续的内存区的分配操作. 该函数只是一个前端, 为`__vmalloc`提供适当的参数, 后者直接调用`__vmalloc_node`. - - -`vmalloc`只是`__vmalloc_node_flags`的前端接口, 复杂向`__vmalloc_node_flags`传递数据, 而`__vmalloc_node_flags`又是`__vmalloc_node`的前端接口, 而后者又将实际的工作交给`__vmalloc_node_range`函数来完成 - - - -[`vmalloc`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1754)函数定义在[`mm/vmalloc.c?v=4.7, line 1754`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1754), 将实际的工作交给`__vmalloc_node_flags`函数来完成. - -```cpp -// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1754 -/** - * vmalloc - allocate virtually contiguous memory - * @size: allocation size - * Allocate enough pages to cover @size from the page level - * allocator and map them into contiguous kernel virtual space. - * - * For tight control over page level allocator and protection flags - * use __vmalloc() instead. - */ -void *vmalloc(unsigned long size) -{ - return __vmalloc_node_flags(size, NUMA_NO_NODE, - GFP_KERNEL | __GFP_HIGHMEM); -} -EXPORT_SYMBOL(vmalloc); -``` - -[`__vmalloc_node_flags`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1747)函数定义在[`mm/vmalloc.c?v=4.7, line 1747`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1747), 通过`__vmalloc_node`来完成实际的工作. - -```cpp -// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1747 -static inline void *__vmalloc_node_flags(unsigned long size, - int node, gfp_t flags) -{ - return __vmalloc_node(size, 1, flags, PAGE_KERNEL, - node, __builtin_return_address(0)); -} -``` -[`__vmalloc_node`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1719)函数定义在[`mm/vmalloc.c?v=4.7, line 1719`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1719), 通过`__vmalloc_node_range`来完成实际的工作. - - -```cpp -// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1719 -/** - * __vmalloc_node - allocate virtually contiguous memory - * @size: allocation size - * @align: desired alignment - * @gfp_mask: flags for the page level allocator - * @prot: protection mask for the allocated pages - * @node: node to use for allocation or NUMA_NO_NODE - * @caller: caller's return address - * - * Allocate enough pages to cover @size from the page level - * allocator with @gfp_mask flags. Map them into contiguous - * kernel virtual space, using a pagetable protection of @prot. - */ -static void *__vmalloc_node(unsigned long size, unsigned long align, - gfp_t gfp_mask, pgprot_t prot, - int node, const void *caller) -{ - return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END, - gfp_mask, prot, 0, node, caller); -} -``` - -[`__vmalloc_node_range`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1658)最终完成了内存区的分配工作 - - -```cpp -// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1658 -/** - * __vmalloc_node_range - allocate virtually contiguous memory - * @size: allocation size - * @align: desired alignment - * @start: vm area range start - * @end: vm area range end - * @gfp_mask: flags for the page level allocator - * @prot: protection mask for the allocated pages - * @vm_flags: additional vm area flags (e.g. %VM_NO_GUARD) - * @node: node to use for allocation or NUMA_NO_NODE - * @caller: caller's return address - * - * Allocate enough pages to cover @size from the page level - * allocator with @gfp_mask flags. Map them into contiguous - * kernel virtual space, using a pagetable protection of @prot. - */ -void *__vmalloc_node_range(unsigned long size, unsigned long align, - unsigned long start, unsigned long end, gfp_t gfp_mask, - pgprot_t prot, unsigned long vm_flags, int node, - const void *caller) -{ - struct vm_struct *area; - void *addr; - unsigned long real_size = size; - - size = PAGE_ALIGN(size); - if (!size || (size >> PAGE_SHIFT) > totalram_pages) - goto fail; - - area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED | - vm_flags, start, end, node, gfp_mask, caller); - if (!area) - goto fail; - - addr = __vmalloc_area_node(area, gfp_mask, prot, node); - if (!addr) - return NULL; - - /* - * In this function, newly allocated vm_struct has VM_UNINITIALIZED - * flag. It means that vm_struct is not fully initialized. - * Now, it is fully initialized, so remove this flag here. - */ - clear_vm_uninitialized_flag(area); - - /* - * A ref_count = 2 is needed because vm_struct allocated in - * __get_vm_area_node() contains a reference to the virtual address of - * the vmalloc'ed block. - */ - kmemleak_alloc(addr, real_size, 2, gfp_mask); - - return addr; - -fail: - warn_alloc_failed(gfp_mask, 0, - "vmalloc: allocation failure: %lu bytes\n", - real_size); - return NULL; -} -``` - -实现分为3部分 - -* 首先, `get_vm_area`在`vmalloc`地址空间中找到一个适当的区域. - -* 接下来从物理内存分配各个页 - -* 最后将这些页连续地映射到`vmalloc`区域中, 分配虚拟内存的工作就完成了. - -如果显式指定了分配页帧的结点, 则内核调用`alloc_pages_node`, 否则,使用`alloc_page`从当前结点分配页帧. - -分配的页从相关结点的伙伴系统移除. 在调用时, `vmalloc`将`gfp_mask`设置为`GFP_KERNEL` | `__GFP_HIGHMEM`,内核通过该参数指示内存管理子系统尽可能从`ZONE_HIGHMEM`内存域分配页帧. 理由已经在上文给出:低端内存域的页帧更为宝贵,因此不应该浪费到vmalloc的分配中,在此使用高 -端内存域的页帧完全可以满足要求。 -内存取自伙伴系统,而gfp_mask设置为`GFP_KERNEL` | `__GFP_HIGHMEM`,因此内核指示内存管 -理子系统尽可能从`ZONE_HIGHMEM`分配页帧。其原因我们已经知道 - - - -#3 备选映射方法 -------- - -除了`vmalloc`之外,还有其他方法可以创建虚拟连续映射。这些都基于上文讨论的`__vmalloc`函数或使用非常类似的机制 - - -* `vmalloc_32`的工作方式与vmalloc相同,但会确保所使用的物理内存总是可以用普通32位指针寻址。如果某种体系结构的寻址能力超出基于字长计算的范围, 那么这种保证就很重要。例如,在启用了`PAE`的`IA-32`系统上,就是如此. - -* `vmap`使用一个`page`数组作为起点,来创建虚拟连续内存区。与vmalloc相比,该函数所用的物理内存位置不是隐式分配的,而需要先行分配好,作为参数传递。此类映射可通过`vm_map`实例中的`VM_MAP`标志辨别。 - -* 不同于上述的所有映射方法, `ioremap`是一个特定于处理器的函数, 必须在所有体系结构上实现. 它可以将取自物理地址空间、由系统总线用于I/O操作的一个内存块,映射到内核的地址空间中. - -该函数在设备驱动程序中使用很多, 可将用于与外设通信的地址区域暴露给内核的其他部分使用(当然也包括其本身). - - - -#4 释放内存 -------- - - -有两个函数用于向内核释放内存, `vfree`用于释放`vmalloc`和`vmalloc_32`分配的区域,而`vunmap`用于释放由`vmap`或`ioremap`创建的映射。这两个函数都会归结到`__vunmap` - -```cpp -void __vunmap(void *addr, int deallocate_pages) -``` - - -`addr`表示要释放的区域的起始地址, `deallocate_pages`指定了是否将与该区域相关的物理内存页返回给伙伴系统. `vfree`将后一个参数设置为1, 而`vunmap`设置为0, 因为在这种情况下只删除映射, 而不将相关的物理内存页返回给伙伴系统. 图3-40给出了`__vunmap`的代码流程图 - - -不必明确给出需要释放的区域长度, 长度可以从`vmlist`中的信息导出. 因此`__vunmap`的第一个任务是在`__remove_vm_area`(由`remove_vm_area`在完成锁定之后调用)中扫描该链表, 以找到 -相关项。 -unmap_vm_area使用找到的vm_area实例,从页表删除不再需要的项。与分配内存时类似,该函 -数需要操作各级页表,但这一次需要删除涉及的项。它还会更新CPU高速缓存。 -如果__vunmap的参数deallocate_pages设置为1(在vfree中),内核会遍历area->pages的所 -有元素,即指向所涉及的物理内存页的page实例的指针。然后对每一项调用__free_page,将页释放 -到伙伴系统。 +内存中不连续的页的分配 +======= + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + + + + +在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法. + +Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述. + +伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者. + +* 内核如何记住哪些内存块是空闲的 + +* 分配空闲页面的方法 + +* 影响分配器行为的众多标识位 + +* 内存碎片的问题和分配器如何处理碎片 + + +#1 内存中不连续的页的分配 +------- + + +根据上文的讲述, 我们知道物理上连续的映射对内核是最好的, 但并不总能成功地使用. 在分配一大块内存时, 可能竭尽全力也无法找到连续的内存块. + +在用户空间中这不是问题,因为普通进程设计为使用处理器的分页机制, 当然这会降低速度并占用TLB. + +在内核中也可以使用同样的技术. 内核分配了其内核虚拟地址空间的一部分, 用于建立连续映射. + + +在IA-32系统中, 前16M划分给DMA区域, 后面一直到第896M作为NORMAL直接映射区, 紧随直接映射的前896MB物理内存,在插入的8MB安全隙之后, 是一个用于管理不连续内存的区域. 这一段具有线性地址空间的所有性质. 分配到其中的页可能位于物理内存中的任何地方. 通过修改负责该区域的内核页表, 即可做到这一点. + +![内核虚拟地址空间的划分](../images/kernel_space.jpg) + +![内核虚拟地址空间的划分](../images/vmalloc_space.png) + +Persistent mappings和Fixmaps地址空间都比较小, 这里我们忽略它们, 这样只剩下直接地址映射和VMALLOC区, 这个划分应该是平衡两个需求的结果 + +1. 尽量增加DMA和Normal区大小,也就是直接映射地址空间大小,当前主流平台的内存,基本上都超过了512MB,很多都是标配1GB内存,因此注定有一部分内存无法进行线性映射。 + +2. 保留一定数量的VMALLOC大小,这个值是应用平台特定的,如果应用平台某个驱动需要用vmalloc分配很大的地址空间,那么最好通过在kernel参数中指定vmalloc大小的方法,预留较多的vmalloc地址空间。 + +3. 并不是Highmem没有或者越少越好,这个是我的个人理解,理由如下:高端内存就像个垃圾桶和缓冲区,防止来自用户空间或者vmalloc的映射破坏Normal zone和DMA zone的连续性,使得它们碎片化。当这个垃圾桶较大时,那么污染Normal 和DMA的机会自然就小了。 + +通过这种方式, 将内核的内核虚拟地址空间划分为几个不同的区域 + +下面的图是VMALLOC地址空间内部划分情况 + + + +![内核虚拟地址空间的划分](../images/vmalloc.jpg) + + +#2 用vmalloc分配内存 +------- + +vmalloc是一个接口函数, 内核代码使用它来分配在虚拟内存中连续但在物理内存中不一定连续的内存 + +```cpp +// http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L70 +void *vmalloc(unsigned long size); +``` + +该函数只需要一个参数, 用于指定所需内存区的长度, 与此前讨论的函数不同, 其长度单位不是页而是字节, 这在用户空间程序设计中是很普遍的. + +使用vmalloc的最著名的实例是内核对模块的实现. 因为模块可能在任何时候加载, 如果模块数据比较多, 那么无法保证有足够的连续内存可用, 特别是在系统已经运行了比较长时间的情况下. + +如果能够用小块内存拼接出足够的内存, 那么使用vmalloc可以规避该问题 + +内核中还有大约[400处地方调用了vmalloc](http://lxr.free-electrons.com/ident?v=4.7;i=vmalloc), 特别是在设备和声音驱动程序中. + +因为用于vmalloc的内存页总是必须映射在内核地址空间中, 因此使用`ZONE_HIGHMEM`内存域的页要优于其他内存域. 这使得内核可以节省更宝贵的较低端内存域, 而又不会带来额外的坏处. 因此, `vmalloc`等映射函数是内核出于自身的目的(并非因为用户空间应用程序)使用高端内存页的少数情形之一. + +所有有关vmalloc的数据结构和API结构声明在[`include/linux/vmalloc.h`](http://lxr.free-electrons.com/source/include/linux/vmalloc.h)和[``]() + +| 声明头文件 | `NON-MMU`实现 | `MMU`实现 | +|:------------:|:------------------:|:-----------:| +| [`include/linux/vmalloc.h`](http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7) | [`mm/nommu.c`](http://lxr.free-electrons.com/source/mm/nommu.c?v=4.7) | [`mm/vmalloc.c`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7) | + +##2.1 数据结构 +------- + + +内核在管理虚拟内存中的`vmalloc`区域时, 内核必须跟踪哪些子区域被使用、哪些是空闲的. 为此定义了一个数据结构`vm_struct`, 将所有使用的部分保存在一个链表中. 该结构提的定义在[include/linux/vmalloc.h?v=4.7, line 32](http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L32) + + +```cpp +// http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L32 +struct vm_struct { + struct vm_struct *next; + void *addr; + unsigned long size; + unsigned long flags; + struct page **pages; + unsigned int nr_pages; + phys_addr_t phys_addr; + const void *caller; +}; +``` + +>注意, 内核使用了一个重要的数据结构称之为`vm_area_struct`, 以管理用户空间进程的虚拟地址空间内容. 尽管名称和目的都是类似的, 虽然二者都是做虚拟地址空间映射的, 但不能混淆这两个结构。 +> +> +> +>1. 前者是内核虚拟地址空间映射,而后者则是应用进程虚拟地址空间映射。 +> +>2. 前者不会产生page fault,而后者一般不会提前分配页面,只有当访问的时候,产生page fault来分配页面。 + + + + +对于每个用vmalloc分配的子区域, 都对应于内核内存中的一个该结构实例. 该结构各个成员的语义如下 + +| 字段 | 描述 | +|:-----:|:-----:| +| next | 使得内核可以将vmalloc区域中的所有子区域保存在一个单链表上 | +| addr |定义了分配的子区域在虚拟地址空间中的起始地址。size表示该子区域的长度. 可以根据该信息来勾画出vmalloc区域的完整分配方案 | +| flags | 存储了与该内存区关联的标志集合, 这几乎是不可避免的. 它只用于指定内存区类型 | +| pages | 是一个指针,指向page指针的数组。每个数组成员都表示一个映射到虚拟地址空间中的物理内存页的page实例 | +| nr_pages | 指定pages中数组项的数目,即涉及的内存页数目 | +| phys_addr | 仅当用ioremap映射了由物理地址描述的物理内存区域时才需要。该信息保存在phys_addr中 | +| caller | | + +其中`flags`只用于指定内存区类型, 所有可能的flag标识以宏的形式定义在[include/linux/vmalloc.h?v=4.7, line 14](http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L14) + +```cpp +// http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L14 +/* bits in flags of vmalloc's vm_struct below */ +#define VM_IOREMAP 0x00000001 /* ioremap() and friends */ +#define VM_ALLOC 0x00000002 /* vmalloc() */ +#define VM_MAP 0x00000004 /* vmap()ed pages */ +#define VM_USERMAP 0x00000008 /* suitable for remap_vmalloc_range */ +#define VM_UNINITIALIZED 0x00000020 /* vm_struct is not fully initialized */ +#define VM_NO_GUARD 0x00000040 /* don't add guard page */ +#define VM_KASAN 0x00000080 /* has allocated kasan shadow memory */ +/* bits [20..32] reserved for arch specific ioremap internals */ +``` + +| flag标识 | 描述 | +|:-----:|:-----:| +| VM_IOREMAP | 表示将几乎随机的物理内存区域映射到vmalloc区域中. 这是一个特定于体系结构的操作 | +| VM_ALLOC | 指定由vmalloc产生的子区域 | +| VM_MAP | 用于表示将现存pages集合映射到连续的虚拟地址空间中 | +| VM_USERMAP | | +| VM_UNINITIALIZED | | +| VM_NO_GUARD | | +| VM_KASAN | | + +下图给出了该结构使用方式的一个实例. 其中依次映射了3个(假想的)物理内存页, 在物理内存中的位置分别是1 023、725和7 311. 在虚拟的vmalloc区域中, 内核将其看作起始于VMALLOC_START + 100的一个连续内存区, 大小为3*PAGE_SIZE的内核地址空间,被映射到物理页面725, 1023和7311 + +![将物理内存页映射到vmalloc区域](../images/vmlloc_map.jpg) + + +##2.2创建vm_area +------- + +因为大部分体系结构都支持`mmu,` 这里我们只考虑有`mmu的`情况. 实际上没有`mmu`支持时, `vmalloc`就无法实现非连续物理地址到连续内核地址空间的映射, `vmalloc`退化为`kmalloc`实现. + +###2.2.1 vmlist全局链表 +------- + +在创建一个新的虚拟内存区之前, 必须找到一个适当的位置. `vm_area`实例组成的一个链表, 管理着`vmalloc`区域中已经建立的各个子区域. 定义在[`mm/vmalloc`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1170)的全局变量[`vmlist`]( http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1170)是表头. 定义在[mm/vmalloc.c?v=4.7, line 1170]( http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1170) + + +```cpp +// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1170 +static struct vm_struct *vmlist __initdata; +``` + + + +###2.2.2 分配函数 +------- + +内核在mm/vmalloc中提供了辅助函数`get_vm_area`和`__get_vm_area`, 它们负责参数准备工作, 而实际的分配工作交给底层函数`__get_vm_area_node`来完成, 这些函数定义在[mm/vmalloc.c?v=4.7, line 1388](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1388) + +```cpp +struct vm_struct *__get_vm_area(unsigned long size, unsigned long flags, + unsigned long start, unsigned long end) +{ + return __get_vm_area_node(size, 1, flags, start, end, NUMA_NO_NODE, + GFP_KERNEL, __builtin_return_address(0)); +} +EXPORT_SYMBOL_GPL(__get_vm_area); + +struct vm_struct *__get_vm_area_caller(unsigned long size, unsigned long flags, + unsigned long start, unsigned long end, + const void *caller) +{ + return __get_vm_area_node(size, 1, flags, start, end, NUMA_NO_NODE, + GFP_KERNEL, caller); +} + +/** + * get_vm_area - reserve a contiguous kernel virtual area + * @size: size of the area + * @flags: %VM_IOREMAP for I/O mappings or VM_ALLOC + * + * Search an area of @size in the kernel virtual mapping area, + * and reserved it for out purposes. Returns the area descriptor + * on success or %NULL on failure. + */ +struct vm_struct *get_vm_area(unsigned long size, unsigned long flags) +{ + return __get_vm_area_node(size, 1, flags, VMALLOC_START, VMALLOC_END, + NUMA_NO_NODE, GFP_KERNEL, + __builtin_return_address(0)); +} + +struct vm_struct *get_vm_area_caller(unsigned long size, unsigned long flags, + const void *caller) +{ + return __get_vm_area_node(size, 1, flags, VMALLOC_START, VMALLOC_END, + NUMA_NO_NODE, GFP_KERNEL, caller); +} +``` + +这些函数是负责实际工作的`__get_vm_area_node`函数的前端. 根据子区域的长度信息, `__get_vm_area_node`函数试图在虚拟的`vmalloc`空间中找到一个适当的位置. 该函数定义在[mm/vmalloc.c?v=4.7, line 1354](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1354) + + + 由于各个`vmalloc`子区域之间需要插入1页(警戒页)作为安全隙, 内核首先适当提高需要分配的内存长度. + +```cpp +static struct vm_struct *__get_vm_area_node(unsigned long size, + unsigned long align, unsigned long flags, unsigned long start, + unsigned long end, int node, gfp_t gfp_mask, const void *caller) +{ + struct vmap_area *va; + struct vm_struct *area; + + BUG_ON(in_interrupt()); + if (flags & VM_IOREMAP) + align = 1ul << clamp_t(int, fls_long(size), + PAGE_SHIFT, IOREMAP_MAX_ORDER); + + size = PAGE_ALIGN(size); + if (unlikely(!size)) + return NULL; + + area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node); + if (unlikely(!area)) + return NULL; + + if (!(flags & VM_NO_GUARD)) + size += PAGE_SIZE; + + va = alloc_vmap_area(size, align, start, end, node, gfp_mask); + if (IS_ERR(va)) { + kfree(area); + return NULL; + } + + setup_vmalloc_vm(area, va, flags, caller); + + return area; +} +``` + +`start`和`end`参数分别由调用者设置, 比如`get_vm_area`函数和`get_vm_area_caller`函数传入`VMALLOC_START`和`VMALLOC_END`. 接下来循环遍历vmlist的所有表元素,直至找到一个适当的项 + + +###2.2.3 释放函数 +------- + +[`remove_vm_area`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1454)函数将一个现存的子区域从vmalloc地址空间删除. + + +函数声明如下, [`include/linux/vmalloc.h?v=4.7, line 121`](http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L121) +```cpp +// http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L121 +struct vm_struct *remove_vm_area(void *addr); +``` + +函数定义在[`mm/vmalloc.c?v=4.7, line 1454`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1454) +```cpp +// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1446 +/** + * remove_vm_area - find and remove a continuous kernel virtual area + * @addr: base address + * + * Search for the kernel VM area starting at @addr, and remove it. + * This function returns the found VM area, but using it is NOT safe + * on SMP machines, except for its size or flags. + */ +struct vm_struct *remove_vm_area(const void *addr) +{ + struct vmap_area *va; + + va = find_vmap_area((unsigned long)addr); + if (va && va->flags & VM_VM_AREA) { + struct vm_struct *vm = va->vm; + + spin_lock(&vmap_area_lock); + va->vm = NULL; + va->flags &= ~VM_VM_AREA; + spin_unlock(&vmap_area_lock); + + vmap_debug_free_range(va->va_start, va->va_end); + kasan_free_shadow(vm); + free_unmap_vmap_area(va); + + return vm; + } + return NULL; +} +``` + + + +##2.3 vmalloc分配内存区 +------- + +`vmalloc`发起对不连续的内存区的分配操作. 该函数只是一个前端, 为`__vmalloc`提供适当的参数, 后者直接调用`__vmalloc_node`. + + +`vmalloc`只是`__vmalloc_node_flags`的前端接口, 复杂向`__vmalloc_node_flags`传递数据, 而`__vmalloc_node_flags`又是`__vmalloc_node`的前端接口, 而后者又将实际的工作交给`__vmalloc_node_range`函数来完成 + + + +[`vmalloc`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1754)函数定义在[`mm/vmalloc.c?v=4.7, line 1754`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1754), 将实际的工作交给`__vmalloc_node_flags`函数来完成. + +```cpp +// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1754 +/** + * vmalloc - allocate virtually contiguous memory + * @size: allocation size + * Allocate enough pages to cover @size from the page level + * allocator and map them into contiguous kernel virtual space. + * + * For tight control over page level allocator and protection flags + * use __vmalloc() instead. + */ +void *vmalloc(unsigned long size) +{ + return __vmalloc_node_flags(size, NUMA_NO_NODE, + GFP_KERNEL | __GFP_HIGHMEM); +} +EXPORT_SYMBOL(vmalloc); +``` + +[`__vmalloc_node_flags`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1747)函数定义在[`mm/vmalloc.c?v=4.7, line 1747`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1747), 通过`__vmalloc_node`来完成实际的工作. + +```cpp +// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1747 +static inline void *__vmalloc_node_flags(unsigned long size, + int node, gfp_t flags) +{ + return __vmalloc_node(size, 1, flags, PAGE_KERNEL, + node, __builtin_return_address(0)); +} +``` +[`__vmalloc_node`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1719)函数定义在[`mm/vmalloc.c?v=4.7, line 1719`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1719), 通过`__vmalloc_node_range`来完成实际的工作. + + +```cpp +// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1719 +/** + * __vmalloc_node - allocate virtually contiguous memory + * @size: allocation size + * @align: desired alignment + * @gfp_mask: flags for the page level allocator + * @prot: protection mask for the allocated pages + * @node: node to use for allocation or NUMA_NO_NODE + * @caller: caller's return address + * + * Allocate enough pages to cover @size from the page level + * allocator with @gfp_mask flags. Map them into contiguous + * kernel virtual space, using a pagetable protection of @prot. + */ +static void *__vmalloc_node(unsigned long size, unsigned long align, + gfp_t gfp_mask, pgprot_t prot, + int node, const void *caller) +{ + return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END, + gfp_mask, prot, 0, node, caller); +} +``` + +[`__vmalloc_node_range`](http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1658)最终完成了内存区的分配工作 + + +```cpp +// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1658 +/** + * __vmalloc_node_range - allocate virtually contiguous memory + * @size: allocation size + * @align: desired alignment + * @start: vm area range start + * @end: vm area range end + * @gfp_mask: flags for the page level allocator + * @prot: protection mask for the allocated pages + * @vm_flags: additional vm area flags (e.g. %VM_NO_GUARD) + * @node: node to use for allocation or NUMA_NO_NODE + * @caller: caller's return address + * + * Allocate enough pages to cover @size from the page level + * allocator with @gfp_mask flags. Map them into contiguous + * kernel virtual space, using a pagetable protection of @prot. + */ +void *__vmalloc_node_range(unsigned long size, unsigned long align, + unsigned long start, unsigned long end, gfp_t gfp_mask, + pgprot_t prot, unsigned long vm_flags, int node, + const void *caller) +{ + struct vm_struct *area; + void *addr; + unsigned long real_size = size; + + size = PAGE_ALIGN(size); + if (!size || (size >> PAGE_SHIFT) > totalram_pages) + goto fail; + + area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED | + vm_flags, start, end, node, gfp_mask, caller); + if (!area) + goto fail; + + addr = __vmalloc_area_node(area, gfp_mask, prot, node); + if (!addr) + return NULL; + + /* + * In this function, newly allocated vm_struct has VM_UNINITIALIZED + * flag. It means that vm_struct is not fully initialized. + * Now, it is fully initialized, so remove this flag here. + */ + clear_vm_uninitialized_flag(area); + + /* + * A ref_count = 2 is needed because vm_struct allocated in + * __get_vm_area_node() contains a reference to the virtual address of + * the vmalloc'ed block. + */ + kmemleak_alloc(addr, real_size, 2, gfp_mask); + + return addr; + +fail: + warn_alloc_failed(gfp_mask, 0, + "vmalloc: allocation failure: %lu bytes\n", + real_size); + return NULL; +} +``` + +实现分为3部分 + +* 首先, `get_vm_area`在`vmalloc`地址空间中找到一个适当的区域. + +* 接下来从物理内存分配各个页 + +* 最后将这些页连续地映射到`vmalloc`区域中, 分配虚拟内存的工作就完成了. + +如果显式指定了分配页帧的结点, 则内核调用`alloc_pages_node`, 否则,使用`alloc_page`从当前结点分配页帧. + +分配的页从相关结点的伙伴系统移除. 在调用时, `vmalloc`将`gfp_mask`设置为`GFP_KERNEL` | `__GFP_HIGHMEM`,内核通过该参数指示内存管理子系统尽可能从`ZONE_HIGHMEM`内存域分配页帧. 理由已经在上文给出:低端内存域的页帧更为宝贵,因此不应该浪费到vmalloc的分配中,在此使用高 +端内存域的页帧完全可以满足要求。 +内存取自伙伴系统,而gfp_mask设置为`GFP_KERNEL` | `__GFP_HIGHMEM`,因此内核指示内存管 +理子系统尽可能从`ZONE_HIGHMEM`分配页帧。其原因我们已经知道 + + + +#3 备选映射方法 +------- + +除了`vmalloc`之外,还有其他方法可以创建虚拟连续映射。这些都基于上文讨论的`__vmalloc`函数或使用非常类似的机制 + + +* `vmalloc_32`的工作方式与vmalloc相同,但会确保所使用的物理内存总是可以用普通32位指针寻址。如果某种体系结构的寻址能力超出基于字长计算的范围, 那么这种保证就很重要。例如,在启用了`PAE`的`IA-32`系统上,就是如此. + +* `vmap`使用一个`page`数组作为起点,来创建虚拟连续内存区。与vmalloc相比,该函数所用的物理内存位置不是隐式分配的,而需要先行分配好,作为参数传递。此类映射可通过`vm_map`实例中的`VM_MAP`标志辨别。 + +* 不同于上述的所有映射方法, `ioremap`是一个特定于处理器的函数, 必须在所有体系结构上实现. 它可以将取自物理地址空间、由系统总线用于I/O操作的一个内存块,映射到内核的地址空间中. + +该函数在设备驱动程序中使用很多, 可将用于与外设通信的地址区域暴露给内核的其他部分使用(当然也包括其本身). + + + +#4 释放内存 +------- + + +有两个函数用于向内核释放内存, `vfree`用于释放`vmalloc`和`vmalloc_32`分配的区域,而`vunmap`用于释放由`vmap`或`ioremap`创建的映射。这两个函数都会归结到`__vunmap` + +```cpp +void __vunmap(void *addr, int deallocate_pages) +``` + + +`addr`表示要释放的区域的起始地址, `deallocate_pages`指定了是否将与该区域相关的物理内存页返回给伙伴系统. `vfree`将后一个参数设置为1, 而`vunmap`设置为0, 因为在这种情况下只删除映射, 而不将相关的物理内存页返回给伙伴系统. 图3-40给出了`__vunmap`的代码流程图 + + +不必明确给出需要释放的区域长度, 长度可以从`vmlist`中的信息导出. 因此`__vunmap`的第一个任务是在`__remove_vm_area`(由`remove_vm_area`在完成锁定之后调用)中扫描该链表, 以找到 +相关项。 +unmap_vm_area使用找到的vm_area实例,从页表删除不再需要的项。与分配内存时类似,该函 +数需要操作各级页表,但这一次需要删除涉及的项。它还会更新CPU高速缓存。 +如果__vunmap的参数deallocate_pages设置为1(在vfree中),内核会遍历area->pages的所 +有元素,即指向所涉及的物理内存页的page实例的指针。然后对每一项调用__free_page,将页释放 +到伙伴系统。 最后,必须释放用于管理该内存区的内核数据结构。 \ No newline at end of file diff --git a/study/kernel/02-memory/04-buddy/07-highmem_mapping/07-kmap-(kernel_mapping)/README.md b/study/kernel/02-memory/04-buddy/07-highmem_mapping/07-kmap-(kernel_mapping)/README.md index 470df04..8719c97 100644 --- a/study/kernel/02-memory/04-buddy/07-highmem_mapping/07-kmap-(kernel_mapping)/README.md +++ b/study/kernel/02-memory/04-buddy/07-highmem_mapping/07-kmap-(kernel_mapping)/README.md @@ -1,633 +1,633 @@ -服务器体系与共享存储器架构 -======= - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - - - - -在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法. - -Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述. - -伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者. - -* 内核如何记住哪些内存块是空闲的 - -* 分配空闲页面的方法 - -* 影响分配器行为的众多标识位 - -* 内存碎片的问题和分配器如何处理碎片 - - -#1 高端内存与内核映射 -------- - - -尽管`vmalloc`函数族可用于从高端内存域向内核映射页帧(这些在内核空间中通常是无法直接看到的), 但这并不是这些函数的实际用途. - -重要的是强调以下事实 : 内核提供了其他函数用于将`ZONE_HIGHMEM`页帧显式映射到内核空间, 这些函数与vmalloc机制无关. 因此, 这就造成了混乱. - - -而在高端内存的页不能永久地映射到内核地址空间. 因此, 通过alloc_pages()函数以\__GFP_HIGHMEM标志获得的内存页就不可能有逻辑地址. - -在x86_32体系结构总, 高于896MB的所有物理内存的范围大都是高端内存, 它并不会永久地或自动映射到内核地址空间, 尽管X86处理器能够寻址物理RAM的范围达到4GB(启用PAE可以寻址64GB), 一旦这些页被分配, 就必须映射到内核的逻辑地址空间上. 在x86_32上, 高端地址的页被映射到内核地址空间(即虚拟地址空间的3GB~4GB) - -内核地址空间的最后128 MiB用于何种用途呢? - -该部分有3个用途。 - -1. 虚拟内存中连续、但物理内存中不连续的内存区,可以在vmalloc区域分配. 该机制通常用于用户过程, 内核自身会试图尽力避免非连续的物理地址。内核通常会成功,因为大部分大的内存块都在启动时分配给内核,那时内存的碎片尚不严重。但在已经运行了很长时间的系统上, 在内核需要物理内存时, 就可能出现可用空间不连续的情况. 此类情况, 主要出现在动态加载模块时. - -2. 持久映射用于将高端内存域中的非持久页映射到内核中 - -3. 固定映射是与物理地址空间中的固定页关联的虚拟地址空间项,但具体关联的页帧可以自由选择. 它与通过固定公式与物理内存关联的直接映射页相反,虚拟固定映射地址与物理内存位置之间的关联可以自行定义,关联建立后内核总是会注意到的. - -![x86_32上的地址划分](../../images/x86_32_mapping.png) - -在这里有两个预处理器符号很重要 \__VMALLOC_RESERVE设置了`vmalloc`区域的长度, 而`MAXMEM`则表示内核可以直接寻址的物理内存的最大可能数量. - -![内核虚拟地址空间](../../images/kernel_space.jpg) - - -内核中, 将内存划分为各个区域是通过图3-15所示的各个常数控制的。根据内核和系统配置, 这些常数可能有不同的值。直接映射的边界由high_memory指定。 - - - - -1. 直接映射区 - 线性空间中从3G开始最大896M的区间, 为直接内存映射区,该区域的线性地址和物理地址存在线性转换关系:线性地址=3G+物理地址。 - -2. 动态内存映射区 - 该区域由内核函数`vmalloc`来分配, 特点是 : 线性空间连续, 但是对应的物理空间不一定连续. `vmalloc`分配的线性地址所对应的物理页可能处于低端内存, 也可能处于高端内存. - -3. 永久内存映射区 - 该区域可访问高端内存. 访问方法是使用`alloc_page(_GFP_HIGHMEM)`分配高端内存页或者使用`kmap`函数将分配到的高端内存映射到该区域. - -4. 固定映射区 - 该区域和4G的顶端只有4k的隔离带,其每个地址项都服务于特定的用途,如ACPI_BASE等。 - - ->说明 -> ->注意用户空间当然可以使用高端内存,而且是正常的使用,内核在分配那些不经常使用的内存时,都用高端内存空间(如果有),所谓不经常使用是相对来说的,比如内核的一些数据结构就属于经常使用的,而用户的一些数据就属于不经常使用的。用户在启动一个应用程序时,是需要内存的,而每个应用程序都有3G的线性地址,给这些地址映射页表时就可以直接使用高端内存。 -> ->而且还要纠正一点的是:那128M线性地址不仅仅是用在这些地方的,如果你要加载一个设备,而这个设备需要映射其内存到内核中,它也需要使用这段线性地址空间来完成,否则内核就不能访问设备上的内存空间了. -> ->总之,内核的高端线性地址是为了访问内核固定映射以外的内存资源。进程在使用内存时,触发缺页异常,具体将哪些物理页映射给用户进程是内核考虑的事情. 在用户空间中没有高端内存这个概念. - -即内核对于低端内存, 不需要特殊的映射机制, 使用**直接映射**即可以访问普通内存区域, 而对于高端内存区域, 内核可以采用三种不同的机制将页框映射到高端内存 : 分别叫做**永久内核映射**、**临时内核映射**以及**非连续内存分配** - - - -#2 持久内核映射 -------- - - -如果需要将高端页帧长期映射(作为持久映射)到内核地址空间中, 必须使用kmap函数. 需要映射的页用指向page的指针指定,作为该函数的参数。该函数在有必要时创建一个映射(即,如果该页确实是高端页), 并返回数据的地址. - -如果没有启用高端支持, 该函数的任务就比较简单. 在这种情况下, 所有页都可以直接访问, 因此只需要返回页的地址, 无需显式创建一个映射. - -如果确实存在高端页, 情况会比较复杂. 类似于vmalloc, 内核首先必须建立高端页和所映射到的地址之间的关联. 还必须在虚拟地址空间中分配一个区域以映射页帧, 最后, 内核必须记录该虚拟区域的哪些部分在使用中, 哪些仍然是空闲的. - - -##2.1 数据结构 -------- - -内核在IA-32平台上在`vmalloc`区域之后分配了一个区域, 从`PKMAP_BASE`到`FIXADDR_START`. 该区域用于持久映射. 不同体系结构使用的方案是类似的. - -永久内核映射允许内核建立高端页框到内核地址空间的长期映射。 他们使用着内核页表中一个专门的页表, 其地址存放在变量`pkmap_page_table`中, 页表中的表项数由[LAST_PKMAP宏](http://lxr.free-electrons.com/source/arch/arm/include/asm/highmem.h?v=4.7#L7)产生. 因此,内核一次最多访问2MB或4MB的高端内存. - -```cpp -#define PKMAP_BASE (PAGE_OFFSET - PMD_SIZE) -``` - -页表映射的线性地址从`PKMAP_BASE`开始. `pkmap_count`数组包含`LAST_PKMAP`个计数器,`pkmap_page_table`页表中的每一项都有一个。 - -```cpp -// http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L126 -static int pkmap_count[LAST_PKMAP]; -static __cacheline_aligned_in_smp DEFINE_SPINLOCK(kmap_lock); - -pte_t * pkmap_page_table; -``` - -高端映射区逻辑页面的分配结构用分配表(`pkmap_count`)来描述,它有1024项,对应于映射区内不同的逻辑页面。当分配项的值等于0时为自由项,等于1时为缓冲项,大于1时为映射项。映射页面的分配基于分配表的扫描,当所有的自由项都用完时,系统将清除所有的缓冲项,如果连缓冲项都用完时,系统将进入等待状态。 - - - -```cpp -// http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L126 -/* -高端映射区逻辑页面的分配结构用分配表(pkmap_count)来描述,它有1024项, -对应于映射区内不同的逻辑页面。当分配项的值等于零时为自由项,等于1时为 -缓冲项,大于1时为映射项。映射页面的分配基于分配表的扫描,当所有的自由 -项都用完时,系统将清除所有的缓冲项,如果连缓冲项都用完时,系 -统将进入等待状态。 -*/ -static int pkmap_count[LAST_PKMAP]; -``` -`pkmap_count`(在[mm/highmem.c?v=4.7, line 126](http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L126)定义)是一容量为`LAST_PKMAP`的整数数组, 其中每个元素都对应于一个持久映射页。它实际上是被映射页的一个使用计数器,语义不太常见. - -内核可以通过get_next_pkmap_nr获取到pkmap_count数组中元素的个数, 该函数定义在[mm/highmem.c?v=4.7, line 66](http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L66) - -```cpp -/* - * Get next index for mapping inside PKMAP region for page with given color. - */ -static inline unsigned int get_next_pkmap_nr(unsigned int color) -{ - static unsigned int last_pkmap_nr; - - last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK; - return last_pkmap_nr; -} -``` - - -该计数器计算了内核使用该页的次数加1. 如果计数器值为2, 则内核中只有一处使用该映射页. 计数器值为5表示有4处使用. 一般地说,计数器值为n代表内核中有n-1处使用该页. 和通常的使用计数器一样, 0意味着相关的页没有使用.计数器值1有特殊语义. 这表示该位置关联的页已经映射, 但由于CPU的TLB没有更新而无法使用, 此时访问该页, 或者失败, 或者会访问到一个不正确的地址 - - -为了记录高端内存页框与永久内核映射包含的线性地址之间的联系,内核使用了`page_address_htable`散列表. 该表包含一个`page_address_map`数据结构,用于为高端内存中的每一个页框进行当前映射。而该数据结构还包含一个指向页描述符的指针和分配给该页框的线性地址。 - - -内核利用`page_address_map`数据结构, 来建立物理内存页的page实例与其在虚似内存区中位置之间的关联. - -```cpp -/* - * Describes one page->virtual association - */ -struct page_address_map -{ - struct page *page; - void *virtual; - struct list_head list; -}; -``` -该结构用于建立`page-->virtual`的映射(该结构由此得名). - - - -| 字段 | 描述 | -|:-----:|:-----:| -| page | 是一个指向全局`mem_map`数组中的`page`实例的指针 | -| virtual | 指定了该页在内核虚拟地址空间中分配的位置 | - - -为便于组织, 映射保存在散列表中, 结构中的链表元素用于建立溢出链表,以处理散列碰撞. 该散列表通过`page_address_htable`数组实现, 定义在[mm/highmem.c?v=4.7, line 392](http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L392) - - - -```cpp -/* - * Hash table bucket - */ -static struct page_address_slot { - struct list_head lh; /* List of page_address_maps */ - spinlock_t lock; /* Protect this bucket's list */ -} ____cacheline_aligned_in_smp page_address_htable[1<lock, flags); - if (!list_empty(&pas->lh)) {{/*如果对应的链表不空, - 该链表中存放的是page_address_map结构*/ - struct page_address_map *pam; - /*对每个链表中的元素*/ - list_for_each_entry(pam, &pas->lh, list) { - if (pam->page == page) { - /*返回线性地址*/ - ret = pam->virtual; - goto done; - } - } - } -done: - spin_unlock_irqrestore(&pas->lock, flags); - return ret; -} - -EXPORT_SYMBOL(page_address); -``` - - - -`page_address`首先检查传递进来的`page`实例在普通内存还是在高端内存. - -* 如果是前者(普通内存区域), 页地址可以根据`page`在`mem_map`数组中的位置计算. 这个工作可以通过[lowmem_page_address](http://lxr.free-electrons.com/source/include/linux/mm.h?v=4.7#L964)调用[page_to_virt(page)](http://lxr.free-electrons.com/ident?v=4.7;i=page_to_virt)来完成 - - -* 对于后者, 可通过上述散列表查找虚拟地址. - - -##2.3 kmap创建映射 -------- - - -###2.3.1 kmap函数 -------- - -为通过`page`指针建立映射, 必须使用`kmap`函数. - -不同体系结构的定义可能不同, 但是大多数体系结构的定义都如下所示, 比如arm上该函数定义在[arch/arm/mm/highmem.c?v=4.7, line 37](http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=4.7#L37), 如下所示 - -```cpp -/*高端内存映射,运用数组进行操作分配情况 -分配好后需要加入哈希表中;*/ -void *kmap(struct page *page) -{ - might_sleep(); - if (!PageHighMem(page)) /*如果页框不属于高端内存*/ - return page_address(page); - return kmap_high(page); /*页框确实属于高端内存*/ -} -EXPORT_SYMBOL(kmap); -``` - - -`kmap`函数只是一个`page_address`的前端,用于确认指定的页是否确实在高端内存域中. 否则, 结果返回`page_address`得到的地址. 如果确实在高端内存中, 则内核将工作委托给`kmap_high` - - -`kmap_high`的实现在函数[mm/highmem.c?v=4.7, line 275](http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L275)中, 定义如下 - - -###2.3.2 kmap_high函数 -------- - - -```cpp -/** - * kmap_high - map a highmem page into memory - * @page: &struct page to map - * - * Returns the page's virtual memory address. - * - * We cannot call this from interrupts, as it may block. - */ -void *kmap_high(struct page *page) -{ - unsigned long vaddr; - - /* - * For highmem pages, we can't trust "virtual" until - * after we have the lock. - */ - lock_kmap(); /*保护页表免受多处理器系统上的并发访问*/ - - /*检查是否已经被映射*/ - vaddr = (unsigned long)page_address(page); - if (!vaddr) )/* 如果没有被映射 */ - /*把页框的物理地址插入到pkmap_page_table的 - 一个项中并在page_address_htable散列表中加入一个 - 元素*/ - vaddr = map_new_virtual(page); - /*分配计数加一,此时流程都正确应该是2了*/ - pkmap_count[PKMAP_NR(vaddr)]++; - BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2); - unlock_kmap(); - return (void*) vaddr; ;/*返回地址*/ -} - -EXPORT_SYMBOL(kmap_high); -``` - - - -###2.3.3 map_new_virtual函数 -------- - -上文讨论的`page_address`函数首先检查该页是否已经映射. 如果它不对应到有效地址, 则必须使用`map_new_virtual`映射该页. - -该函数定义在[mm/highmem.c?v=4.7, line 213](http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L213), 将执行下列主要的步骤. - -```cpp -static inline unsigned long map_new_virtual(struct page *page) -{ - unsigned long vaddr; - int count; - unsigned int last_pkmap_nr; - unsigned int color = get_pkmap_color(page); - -start: - count = get_pkmap_entries_count(color); - /* Find an empty entry */ - for (;;) { - last_pkmap_nr = get_next_pkmap_nr(color); /*加1,防止越界*/ - /* 接下来判断什么时候last_pkmap_nr等于0,等于0就表示1023(LAST_PKMAP(1024)-1)个页表项已经被分配了 - ,这时候就需要调用flush_all_zero_pkmaps()函数,把所有pkmap_count[] 计数为1的页表项在TLB里面的entry给flush掉 - ,并重置为0,这就表示该页表项又可以用了,可能会有疑惑为什么不在把pkmap_count置为1的时候也 - 就是解除映射的同时把TLB也flush呢? - 个人感觉有可能是为了效率的问题吧,毕竟等到不够的时候再刷新,效率要好点吧。*/ - if (no_more_pkmaps(last_pkmap_nr, color)) { - flush_all_zero_pkmaps(); - count = get_pkmap_entries_count(color); - } - - if (!pkmap_count[last_pkmap_nr]) - break; /* Found a usable entry */ - if (--count) - continue; - - /* - * Sleep for somebody else to unmap their entries - */ - { - DECLARE_WAITQUEUE(wait, current); - wait_queue_head_t *pkmap_map_wait = - get_pkmap_wait_queue_head(color); - - __set_current_state(TASK_UNINTERRUPTIBLE); - add_wait_queue(pkmap_map_wait, &wait); - unlock_kmap(); - schedule(); - remove_wait_queue(pkmap_map_wait, &wait); - lock_kmap(); - - /* Somebody else might have mapped it while we slept */ - if (page_address(page)) - return (unsigned long)page_address(page); - - /* Re-start */ - goto start; - } - } - /*返回这个页表项对应的线性地址vaddr.*/ - vaddr = PKMAP_ADDR(last_pkmap_nr); - /*设置页表项*/ - set_pte_at(&init_mm, vaddr, - &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot)); - /*接下来把pkmap_count[last_pkmap_nr]置为1,1不是表示不可用吗, - 既然映射已经建立好了,应该赋值为2呀,其实这个操作 - 是在他的上层函数kmap_high里面完成的(pkmap_count[PKMAP_NR(vaddr)]++).*/ - pkmap_count[last_pkmap_nr] = 1; - /*到此为止,整个映射就完成了,再把page和对应的线性地址 - 加入到page_address_htable哈希链表里面就可以了*/ - set_page_address(page, (void *)vaddr); - - return vaddr; -} -``` - -1. 从最后使用的位置(保存在全局变量last_pkmap_nr中)开始,反向扫描pkmap_count数组, 直至找到一个空闲位置. 如果没有空闲位置,该函数进入睡眠状态,直至内核的另一部分执行解除映射操作腾出空位. 在到达`pkmap_count`的最大索引值时, 搜索从位置0开始. 在这种情况下, 还调用 -`flush_all_zero_pkmaps`函数刷出CPU高速缓存(读者稍后会看到这一点)。 - -2. 修改内核的页表,将该页映射在指定位置。但尚未更新TLB. - -3. 新位置的使用计数器设置为1。如上所述,这意味着该页已分配但无法使用,因为TLB项未更新. - -4. set_page_address将该页添加到持久内核映射的数据结构。 -该函数返回新映射页的虚拟地址. 在不需要高端内存页的体系结构上(或没有设置CONFIG_HIGHMEM),则使用通用版本的kmap返回页的地址,且不修改虚拟内存 - - -##2.4 kunmap解除映射 -------- - - -用`kmap`映射的页, 如果不再需要, 必须用`kunmap`解除映射. 照例, 该函数首先检查相关的页(由`page`实例标识)是否确实在高端内存中. 倘若如此, 则实际工作委托给`mm/highmem.c`中的`kunmap_high`, 该函数的主要任务是将`pkmap_count`数组中对应位置在计数器减1 - - -该机制永远不能将计数器值降低到小于1. 这意味着相关的页没有释放。因为对使用计数器进行了额外的加1操作, 正如前文的讨论, 这是为确保CPU高速缓存的正确处理. - -也在上文提到的`flush_all_zero_pkmaps`是最终释放映射的关键. 在`map_new_virtual`从头开始搜索空闲位置时, 总是调用该函数. - -它负责以下3个操作。 - -1. `flush_cache_kmaps`在内核映射上执行刷出(在需要显式刷出的大多数体系结构上,将使用`flush_cache_all`刷出CPU的全部的高速缓存), 因为内核的全局页表已经修改. - -2. 扫描整个`pkmap_count`数组. 计数器值为1的项设置为0,从页表删除相关的项, 最后删除该映射。 - -3. 最后, 使用`flush_tlb_kernel_range`函数刷出所有与`PKMAP`区域相关的`TLB`项. - - -###2.4.1 kunmap函数 -------- - - -同kmap类似, 每个体系结构都应该实现自己的kmap函数, 大多数体系结构的定义都如下所示, 参见[arch/arm/mm/highmem.c?v=4.7, line 46](http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=4.7#L46) - -```cpp -void kunmap(struct page *page) -{ - BUG_ON(in_interrupt()); - if (!PageHighMem(page)) - return; - kunmap_high(page); -} -EXPORT_SYMBOL(kunmap); -``` - - -内核首先检查待释放内存区域是不是在高端内存区域 - -* 如果内存区域在普通内存区, 则内核并没有通过kmap_high对其建立持久的内核映射, 当然也无需用kunmap_high释放 - -* 如果内存区域在高端内存区, 则内核通过kunmap_high释放该内存空间 - - - -###2.4.2 kunmap_high函数 -------- - -kunmap_high函数定义在[mm/highmem.c?v=4.7, line 328](http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L328) - -```cpp -#ifdef CONFIG_HIGHMEM -/** - * kunmap_high - unmap a highmem page into memory - * @page: &struct page to unmap - * - * If ARCH_NEEDS_KMAP_HIGH_GET is not defined then this may be called - * only from user context. - */ -void kunmap_high(struct page *page) -{ - unsigned long vaddr; - unsigned long nr; - unsigned long flags; - int need_wakeup; - unsigned int color = get_pkmap_color(page); - wait_queue_head_t *pkmap_map_wait; - - lock_kmap_any(flags); - vaddr = (unsigned long)page_address(page); - BUG_ON(!vaddr); - nr = PKMAP_NR(vaddr); /*永久内存区域开始的第几个页面*/ - - /* - * A count must never go down to zero - * without a TLB flush! - */ - need_wakeup = 0; - switch (--pkmap_count[nr]) { /*减小这个值,因为在映射的时候对其进行了加2*/ - case 0: - BUG(); - case 1: - /* - * Avoid an unnecessary wake_up() function call. - * The common case is pkmap_count[] == 1, but - * no waiters. - * The tasks queued in the wait-queue are guarded - * by both the lock in the wait-queue-head and by - * the kmap_lock. As the kmap_lock is held here, - * no need for the wait-queue-head's lock. Simply - * test if the queue is empty. - */ - pkmap_map_wait = get_pkmap_wait_queue_head(color); - need_wakeup = waitqueue_active(pkmap_map_wait); - } - unlock_kmap_any(flags); - - /* do wake-up, if needed, race-free outside of the spin lock */ - if (need_wakeup) - wake_up(pkmap_map_wait); -} - -EXPORT_SYMBOL(kunmap_high); -#endif -``` - - - -#3 临时内核映射 -------- - - -刚才描述的`kmap`函数不能用于中断处理程序, 因为它可能进入睡眠状态. 如果`pkmap`数组中没有空闲位置, 该函数会进入睡眠状态, 直至情形有所改善. - -因此内核提供了一个备选的映射函数, 其执行是原子的, 逻辑上称为`kmap_atomic`. 该函数的一个主要优点是它比普通的`kmap`快速. 但它不能用 -于可能进入睡眠的代码. 因此, 它对于很快就需要一个临时页的简短代码,是非常理想的. - -`kmap_atomic`的定义在IA-32, PPC, Sparc32上是[特定于体系结构的](http://lxr.free-electrons.com/ident?v=4.7;i=kmap_atomic), 但这3种实现只有非常细微的差别. 其原型是相同的. - - -##3.1 kmap_atomic函数 -------- - - -```cpp -// http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=4.7#L55 -void *kmap_atomic(struct page *page) -``` -page是一个指向高端内存页的管理结构的指针, 而早期的内核中, 增加了一个类型为[enum km_type](http://lxr.free-electrons.com/ident?v=2.6.32;i=km_type)的[type参数](http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=2.6.32#L39), 用于指定所需的映射类型 - -```cpp -// http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=2.6.32#L39 -void *kmap_atomic(struct page *page, enum km_type type) -``` - -而在新的内核中, 删除了这个标识, 但是保留了`km_type`的最大值`KM_TYPE_NR` - - -```cpp -void *kmap_atomic(struct page *page) -{ - unsigned int idx; - unsigned long vaddr; - void *kmap; - int type; - - preempt_disable(); - pagefault_disable(); - if (!PageHighMem(page)) - return page_address(page); - -#ifdef CONFIG_DEBUG_HIGHMEM - /* - * There is no cache coherency issue when non VIVT, so force the - * dedicated kmap usage for better debugging purposes in that case. - */ - if (!cache_is_vivt()) - kmap = NULL; - else -#endif - kmap = kmap_high_get(page); - if (kmap) - return kmap; - - type = kmap_atomic_idx_push(); - - idx = FIX_KMAP_BEGIN + type + KM_TYPE_NR * smp_processor_id(); - vaddr = __fix_to_virt(idx); -#ifdef CONFIG_DEBUG_HIGHMEM - /* - * With debugging enabled, kunmap_atomic forces that entry to 0. - * Make sure it was indeed properly unmapped. - */ - BUG_ON(!pte_none(get_fixmap_pte(vaddr))); -#endif - /* - * When debugging is off, kunmap_atomic leaves the previous mapping - * in place, so the contained TLB flush ensures the TLB is updated - * with the new mapping. - */ - set_fixmap_pte(idx, mk_pte(page, kmap_prot)); - - return (void *)vaddr; -} -EXPORT_SYMBOL(kmap_atomic); -``` - - -这个函数不会被阻塞, 因此可以用在中断上下文和起亚不能重新调度的地方. 它也禁止内核抢占, 这是有必要的, 因此映射对每个处理器都是唯一的(调度可能对哪个处理器执行哪个进程做变动). - - -##3.2 kunmap_atomic函数 -------- - -可以通过函数kunmap_atomic取消映射 - -```cpp -/* - * Prevent people trying to call kunmap_atomic() as if it were kunmap() - * kunmap_atomic() should get the return value of kmap_atomic, not the page. - */ -#define kunmap_atomic(addr) \ -do { \ - BUILD_BUG_ON(__same_type((addr), struct page *)); \ - __kunmap_atomic(addr); \ -} while (0) -``` - -这个函数也不会阻塞. 在很多体系结构中, 除非激活了内核抢占, 否则kunmap_atomic根本无事可做, 因为只有在下一个临时映射到来前上一个临时映射才有效. 因此, 内核完全可以"忘掉"kmap_atomic映射, kunmap_atomic也无需做什么实际的事情. 下一个原子映射将自动覆盖前一个映射. - +服务器体系与共享存储器架构 +======= + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + + + + +在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法. + +Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述. + +伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者. + +* 内核如何记住哪些内存块是空闲的 + +* 分配空闲页面的方法 + +* 影响分配器行为的众多标识位 + +* 内存碎片的问题和分配器如何处理碎片 + + +#1 高端内存与内核映射 +------- + + +尽管`vmalloc`函数族可用于从高端内存域向内核映射页帧(这些在内核空间中通常是无法直接看到的), 但这并不是这些函数的实际用途. + +重要的是强调以下事实 : 内核提供了其他函数用于将`ZONE_HIGHMEM`页帧显式映射到内核空间, 这些函数与vmalloc机制无关. 因此, 这就造成了混乱. + + +而在高端内存的页不能永久地映射到内核地址空间. 因此, 通过alloc_pages()函数以\__GFP_HIGHMEM标志获得的内存页就不可能有逻辑地址. + +在x86_32体系结构总, 高于896MB的所有物理内存的范围大都是高端内存, 它并不会永久地或自动映射到内核地址空间, 尽管X86处理器能够寻址物理RAM的范围达到4GB(启用PAE可以寻址64GB), 一旦这些页被分配, 就必须映射到内核的逻辑地址空间上. 在x86_32上, 高端地址的页被映射到内核地址空间(即虚拟地址空间的3GB~4GB) + +内核地址空间的最后128 MiB用于何种用途呢? + +该部分有3个用途。 + +1. 虚拟内存中连续、但物理内存中不连续的内存区,可以在vmalloc区域分配. 该机制通常用于用户过程, 内核自身会试图尽力避免非连续的物理地址。内核通常会成功,因为大部分大的内存块都在启动时分配给内核,那时内存的碎片尚不严重。但在已经运行了很长时间的系统上, 在内核需要物理内存时, 就可能出现可用空间不连续的情况. 此类情况, 主要出现在动态加载模块时. + +2. 持久映射用于将高端内存域中的非持久页映射到内核中 + +3. 固定映射是与物理地址空间中的固定页关联的虚拟地址空间项,但具体关联的页帧可以自由选择. 它与通过固定公式与物理内存关联的直接映射页相反,虚拟固定映射地址与物理内存位置之间的关联可以自行定义,关联建立后内核总是会注意到的. + +![x86_32上的地址划分](../../images/x86_32_mapping.png) + +在这里有两个预处理器符号很重要 \__VMALLOC_RESERVE设置了`vmalloc`区域的长度, 而`MAXMEM`则表示内核可以直接寻址的物理内存的最大可能数量. + +![内核虚拟地址空间](../../images/kernel_space.jpg) + + +内核中, 将内存划分为各个区域是通过图3-15所示的各个常数控制的。根据内核和系统配置, 这些常数可能有不同的值。直接映射的边界由high_memory指定。 + + + + +1. 直接映射区 + 线性空间中从3G开始最大896M的区间, 为直接内存映射区,该区域的线性地址和物理地址存在线性转换关系:线性地址=3G+物理地址。 + +2. 动态内存映射区 + 该区域由内核函数`vmalloc`来分配, 特点是 : 线性空间连续, 但是对应的物理空间不一定连续. `vmalloc`分配的线性地址所对应的物理页可能处于低端内存, 也可能处于高端内存. + +3. 永久内存映射区 + 该区域可访问高端内存. 访问方法是使用`alloc_page(_GFP_HIGHMEM)`分配高端内存页或者使用`kmap`函数将分配到的高端内存映射到该区域. + +4. 固定映射区 + 该区域和4G的顶端只有4k的隔离带,其每个地址项都服务于特定的用途,如ACPI_BASE等。 + + +>说明 +> +>注意用户空间当然可以使用高端内存,而且是正常的使用,内核在分配那些不经常使用的内存时,都用高端内存空间(如果有),所谓不经常使用是相对来说的,比如内核的一些数据结构就属于经常使用的,而用户的一些数据就属于不经常使用的。用户在启动一个应用程序时,是需要内存的,而每个应用程序都有3G的线性地址,给这些地址映射页表时就可以直接使用高端内存。 +> +>而且还要纠正一点的是:那128M线性地址不仅仅是用在这些地方的,如果你要加载一个设备,而这个设备需要映射其内存到内核中,它也需要使用这段线性地址空间来完成,否则内核就不能访问设备上的内存空间了. +> +>总之,内核的高端线性地址是为了访问内核固定映射以外的内存资源。进程在使用内存时,触发缺页异常,具体将哪些物理页映射给用户进程是内核考虑的事情. 在用户空间中没有高端内存这个概念. + +即内核对于低端内存, 不需要特殊的映射机制, 使用**直接映射**即可以访问普通内存区域, 而对于高端内存区域, 内核可以采用三种不同的机制将页框映射到高端内存 : 分别叫做**永久内核映射**、**临时内核映射**以及**非连续内存分配** + + + +#2 持久内核映射 +------- + + +如果需要将高端页帧长期映射(作为持久映射)到内核地址空间中, 必须使用kmap函数. 需要映射的页用指向page的指针指定,作为该函数的参数。该函数在有必要时创建一个映射(即,如果该页确实是高端页), 并返回数据的地址. + +如果没有启用高端支持, 该函数的任务就比较简单. 在这种情况下, 所有页都可以直接访问, 因此只需要返回页的地址, 无需显式创建一个映射. + +如果确实存在高端页, 情况会比较复杂. 类似于vmalloc, 内核首先必须建立高端页和所映射到的地址之间的关联. 还必须在虚拟地址空间中分配一个区域以映射页帧, 最后, 内核必须记录该虚拟区域的哪些部分在使用中, 哪些仍然是空闲的. + + +##2.1 数据结构 +------- + +内核在IA-32平台上在`vmalloc`区域之后分配了一个区域, 从`PKMAP_BASE`到`FIXADDR_START`. 该区域用于持久映射. 不同体系结构使用的方案是类似的. + +永久内核映射允许内核建立高端页框到内核地址空间的长期映射。 他们使用着内核页表中一个专门的页表, 其地址存放在变量`pkmap_page_table`中, 页表中的表项数由[LAST_PKMAP宏](http://lxr.free-electrons.com/source/arch/arm/include/asm/highmem.h?v=4.7#L7)产生. 因此,内核一次最多访问2MB或4MB的高端内存. + +```cpp +#define PKMAP_BASE (PAGE_OFFSET - PMD_SIZE) +``` + +页表映射的线性地址从`PKMAP_BASE`开始. `pkmap_count`数组包含`LAST_PKMAP`个计数器,`pkmap_page_table`页表中的每一项都有一个。 + +```cpp +// http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L126 +static int pkmap_count[LAST_PKMAP]; +static __cacheline_aligned_in_smp DEFINE_SPINLOCK(kmap_lock); + +pte_t * pkmap_page_table; +``` + +高端映射区逻辑页面的分配结构用分配表(`pkmap_count`)来描述,它有1024项,对应于映射区内不同的逻辑页面。当分配项的值等于0时为自由项,等于1时为缓冲项,大于1时为映射项。映射页面的分配基于分配表的扫描,当所有的自由项都用完时,系统将清除所有的缓冲项,如果连缓冲项都用完时,系统将进入等待状态。 + + + +```cpp +// http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L126 +/* +高端映射区逻辑页面的分配结构用分配表(pkmap_count)来描述,它有1024项, +对应于映射区内不同的逻辑页面。当分配项的值等于零时为自由项,等于1时为 +缓冲项,大于1时为映射项。映射页面的分配基于分配表的扫描,当所有的自由 +项都用完时,系统将清除所有的缓冲项,如果连缓冲项都用完时,系 +统将进入等待状态。 +*/ +static int pkmap_count[LAST_PKMAP]; +``` +`pkmap_count`(在[mm/highmem.c?v=4.7, line 126](http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L126)定义)是一容量为`LAST_PKMAP`的整数数组, 其中每个元素都对应于一个持久映射页。它实际上是被映射页的一个使用计数器,语义不太常见. + +内核可以通过get_next_pkmap_nr获取到pkmap_count数组中元素的个数, 该函数定义在[mm/highmem.c?v=4.7, line 66](http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L66) + +```cpp +/* + * Get next index for mapping inside PKMAP region for page with given color. + */ +static inline unsigned int get_next_pkmap_nr(unsigned int color) +{ + static unsigned int last_pkmap_nr; + + last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK; + return last_pkmap_nr; +} +``` + + +该计数器计算了内核使用该页的次数加1. 如果计数器值为2, 则内核中只有一处使用该映射页. 计数器值为5表示有4处使用. 一般地说,计数器值为n代表内核中有n-1处使用该页. 和通常的使用计数器一样, 0意味着相关的页没有使用.计数器值1有特殊语义. 这表示该位置关联的页已经映射, 但由于CPU的TLB没有更新而无法使用, 此时访问该页, 或者失败, 或者会访问到一个不正确的地址 + + +为了记录高端内存页框与永久内核映射包含的线性地址之间的联系,内核使用了`page_address_htable`散列表. 该表包含一个`page_address_map`数据结构,用于为高端内存中的每一个页框进行当前映射。而该数据结构还包含一个指向页描述符的指针和分配给该页框的线性地址。 + + +内核利用`page_address_map`数据结构, 来建立物理内存页的page实例与其在虚似内存区中位置之间的关联. + +```cpp +/* + * Describes one page->virtual association + */ +struct page_address_map +{ + struct page *page; + void *virtual; + struct list_head list; +}; +``` +该结构用于建立`page-->virtual`的映射(该结构由此得名). + + + +| 字段 | 描述 | +|:-----:|:-----:| +| page | 是一个指向全局`mem_map`数组中的`page`实例的指针 | +| virtual | 指定了该页在内核虚拟地址空间中分配的位置 | + + +为便于组织, 映射保存在散列表中, 结构中的链表元素用于建立溢出链表,以处理散列碰撞. 该散列表通过`page_address_htable`数组实现, 定义在[mm/highmem.c?v=4.7, line 392](http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L392) + + + +```cpp +/* + * Hash table bucket + */ +static struct page_address_slot { + struct list_head lh; /* List of page_address_maps */ + spinlock_t lock; /* Protect this bucket's list */ +} ____cacheline_aligned_in_smp page_address_htable[1<lock, flags); + if (!list_empty(&pas->lh)) {{/*如果对应的链表不空, + 该链表中存放的是page_address_map结构*/ + struct page_address_map *pam; + /*对每个链表中的元素*/ + list_for_each_entry(pam, &pas->lh, list) { + if (pam->page == page) { + /*返回线性地址*/ + ret = pam->virtual; + goto done; + } + } + } +done: + spin_unlock_irqrestore(&pas->lock, flags); + return ret; +} + +EXPORT_SYMBOL(page_address); +``` + + + +`page_address`首先检查传递进来的`page`实例在普通内存还是在高端内存. + +* 如果是前者(普通内存区域), 页地址可以根据`page`在`mem_map`数组中的位置计算. 这个工作可以通过[lowmem_page_address](http://lxr.free-electrons.com/source/include/linux/mm.h?v=4.7#L964)调用[page_to_virt(page)](http://lxr.free-electrons.com/ident?v=4.7;i=page_to_virt)来完成 + + +* 对于后者, 可通过上述散列表查找虚拟地址. + + +##2.3 kmap创建映射 +------- + + +###2.3.1 kmap函数 +------- + +为通过`page`指针建立映射, 必须使用`kmap`函数. + +不同体系结构的定义可能不同, 但是大多数体系结构的定义都如下所示, 比如arm上该函数定义在[arch/arm/mm/highmem.c?v=4.7, line 37](http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=4.7#L37), 如下所示 + +```cpp +/*高端内存映射,运用数组进行操作分配情况 +分配好后需要加入哈希表中;*/ +void *kmap(struct page *page) +{ + might_sleep(); + if (!PageHighMem(page)) /*如果页框不属于高端内存*/ + return page_address(page); + return kmap_high(page); /*页框确实属于高端内存*/ +} +EXPORT_SYMBOL(kmap); +``` + + +`kmap`函数只是一个`page_address`的前端,用于确认指定的页是否确实在高端内存域中. 否则, 结果返回`page_address`得到的地址. 如果确实在高端内存中, 则内核将工作委托给`kmap_high` + + +`kmap_high`的实现在函数[mm/highmem.c?v=4.7, line 275](http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L275)中, 定义如下 + + +###2.3.2 kmap_high函数 +------- + + +```cpp +/** + * kmap_high - map a highmem page into memory + * @page: &struct page to map + * + * Returns the page's virtual memory address. + * + * We cannot call this from interrupts, as it may block. + */ +void *kmap_high(struct page *page) +{ + unsigned long vaddr; + + /* + * For highmem pages, we can't trust "virtual" until + * after we have the lock. + */ + lock_kmap(); /*保护页表免受多处理器系统上的并发访问*/ + + /*检查是否已经被映射*/ + vaddr = (unsigned long)page_address(page); + if (!vaddr) )/* 如果没有被映射 */ + /*把页框的物理地址插入到pkmap_page_table的 + 一个项中并在page_address_htable散列表中加入一个 + 元素*/ + vaddr = map_new_virtual(page); + /*分配计数加一,此时流程都正确应该是2了*/ + pkmap_count[PKMAP_NR(vaddr)]++; + BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2); + unlock_kmap(); + return (void*) vaddr; ;/*返回地址*/ +} + +EXPORT_SYMBOL(kmap_high); +``` + + + +###2.3.3 map_new_virtual函数 +------- + +上文讨论的`page_address`函数首先检查该页是否已经映射. 如果它不对应到有效地址, 则必须使用`map_new_virtual`映射该页. + +该函数定义在[mm/highmem.c?v=4.7, line 213](http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L213), 将执行下列主要的步骤. + +```cpp +static inline unsigned long map_new_virtual(struct page *page) +{ + unsigned long vaddr; + int count; + unsigned int last_pkmap_nr; + unsigned int color = get_pkmap_color(page); + +start: + count = get_pkmap_entries_count(color); + /* Find an empty entry */ + for (;;) { + last_pkmap_nr = get_next_pkmap_nr(color); /*加1,防止越界*/ + /* 接下来判断什么时候last_pkmap_nr等于0,等于0就表示1023(LAST_PKMAP(1024)-1)个页表项已经被分配了 + ,这时候就需要调用flush_all_zero_pkmaps()函数,把所有pkmap_count[] 计数为1的页表项在TLB里面的entry给flush掉 + ,并重置为0,这就表示该页表项又可以用了,可能会有疑惑为什么不在把pkmap_count置为1的时候也 + 就是解除映射的同时把TLB也flush呢? + 个人感觉有可能是为了效率的问题吧,毕竟等到不够的时候再刷新,效率要好点吧。*/ + if (no_more_pkmaps(last_pkmap_nr, color)) { + flush_all_zero_pkmaps(); + count = get_pkmap_entries_count(color); + } + + if (!pkmap_count[last_pkmap_nr]) + break; /* Found a usable entry */ + if (--count) + continue; + + /* + * Sleep for somebody else to unmap their entries + */ + { + DECLARE_WAITQUEUE(wait, current); + wait_queue_head_t *pkmap_map_wait = + get_pkmap_wait_queue_head(color); + + __set_current_state(TASK_UNINTERRUPTIBLE); + add_wait_queue(pkmap_map_wait, &wait); + unlock_kmap(); + schedule(); + remove_wait_queue(pkmap_map_wait, &wait); + lock_kmap(); + + /* Somebody else might have mapped it while we slept */ + if (page_address(page)) + return (unsigned long)page_address(page); + + /* Re-start */ + goto start; + } + } + /*返回这个页表项对应的线性地址vaddr.*/ + vaddr = PKMAP_ADDR(last_pkmap_nr); + /*设置页表项*/ + set_pte_at(&init_mm, vaddr, + &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot)); + /*接下来把pkmap_count[last_pkmap_nr]置为1,1不是表示不可用吗, + 既然映射已经建立好了,应该赋值为2呀,其实这个操作 + 是在他的上层函数kmap_high里面完成的(pkmap_count[PKMAP_NR(vaddr)]++).*/ + pkmap_count[last_pkmap_nr] = 1; + /*到此为止,整个映射就完成了,再把page和对应的线性地址 + 加入到page_address_htable哈希链表里面就可以了*/ + set_page_address(page, (void *)vaddr); + + return vaddr; +} +``` + +1. 从最后使用的位置(保存在全局变量last_pkmap_nr中)开始,反向扫描pkmap_count数组, 直至找到一个空闲位置. 如果没有空闲位置,该函数进入睡眠状态,直至内核的另一部分执行解除映射操作腾出空位. 在到达`pkmap_count`的最大索引值时, 搜索从位置0开始. 在这种情况下, 还调用 +`flush_all_zero_pkmaps`函数刷出CPU高速缓存(读者稍后会看到这一点)。 + +2. 修改内核的页表,将该页映射在指定位置。但尚未更新TLB. + +3. 新位置的使用计数器设置为1。如上所述,这意味着该页已分配但无法使用,因为TLB项未更新. + +4. set_page_address将该页添加到持久内核映射的数据结构。 +该函数返回新映射页的虚拟地址. 在不需要高端内存页的体系结构上(或没有设置CONFIG_HIGHMEM),则使用通用版本的kmap返回页的地址,且不修改虚拟内存 + + +##2.4 kunmap解除映射 +------- + + +用`kmap`映射的页, 如果不再需要, 必须用`kunmap`解除映射. 照例, 该函数首先检查相关的页(由`page`实例标识)是否确实在高端内存中. 倘若如此, 则实际工作委托给`mm/highmem.c`中的`kunmap_high`, 该函数的主要任务是将`pkmap_count`数组中对应位置在计数器减1 + + +该机制永远不能将计数器值降低到小于1. 这意味着相关的页没有释放。因为对使用计数器进行了额外的加1操作, 正如前文的讨论, 这是为确保CPU高速缓存的正确处理. + +也在上文提到的`flush_all_zero_pkmaps`是最终释放映射的关键. 在`map_new_virtual`从头开始搜索空闲位置时, 总是调用该函数. + +它负责以下3个操作。 + +1. `flush_cache_kmaps`在内核映射上执行刷出(在需要显式刷出的大多数体系结构上,将使用`flush_cache_all`刷出CPU的全部的高速缓存), 因为内核的全局页表已经修改. + +2. 扫描整个`pkmap_count`数组. 计数器值为1的项设置为0,从页表删除相关的项, 最后删除该映射。 + +3. 最后, 使用`flush_tlb_kernel_range`函数刷出所有与`PKMAP`区域相关的`TLB`项. + + +###2.4.1 kunmap函数 +------- + + +同kmap类似, 每个体系结构都应该实现自己的kmap函数, 大多数体系结构的定义都如下所示, 参见[arch/arm/mm/highmem.c?v=4.7, line 46](http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=4.7#L46) + +```cpp +void kunmap(struct page *page) +{ + BUG_ON(in_interrupt()); + if (!PageHighMem(page)) + return; + kunmap_high(page); +} +EXPORT_SYMBOL(kunmap); +``` + + +内核首先检查待释放内存区域是不是在高端内存区域 + +* 如果内存区域在普通内存区, 则内核并没有通过kmap_high对其建立持久的内核映射, 当然也无需用kunmap_high释放 + +* 如果内存区域在高端内存区, 则内核通过kunmap_high释放该内存空间 + + + +###2.4.2 kunmap_high函数 +------- + +kunmap_high函数定义在[mm/highmem.c?v=4.7, line 328](http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L328) + +```cpp +#ifdef CONFIG_HIGHMEM +/** + * kunmap_high - unmap a highmem page into memory + * @page: &struct page to unmap + * + * If ARCH_NEEDS_KMAP_HIGH_GET is not defined then this may be called + * only from user context. + */ +void kunmap_high(struct page *page) +{ + unsigned long vaddr; + unsigned long nr; + unsigned long flags; + int need_wakeup; + unsigned int color = get_pkmap_color(page); + wait_queue_head_t *pkmap_map_wait; + + lock_kmap_any(flags); + vaddr = (unsigned long)page_address(page); + BUG_ON(!vaddr); + nr = PKMAP_NR(vaddr); /*永久内存区域开始的第几个页面*/ + + /* + * A count must never go down to zero + * without a TLB flush! + */ + need_wakeup = 0; + switch (--pkmap_count[nr]) { /*减小这个值,因为在映射的时候对其进行了加2*/ + case 0: + BUG(); + case 1: + /* + * Avoid an unnecessary wake_up() function call. + * The common case is pkmap_count[] == 1, but + * no waiters. + * The tasks queued in the wait-queue are guarded + * by both the lock in the wait-queue-head and by + * the kmap_lock. As the kmap_lock is held here, + * no need for the wait-queue-head's lock. Simply + * test if the queue is empty. + */ + pkmap_map_wait = get_pkmap_wait_queue_head(color); + need_wakeup = waitqueue_active(pkmap_map_wait); + } + unlock_kmap_any(flags); + + /* do wake-up, if needed, race-free outside of the spin lock */ + if (need_wakeup) + wake_up(pkmap_map_wait); +} + +EXPORT_SYMBOL(kunmap_high); +#endif +``` + + + +#3 临时内核映射 +------- + + +刚才描述的`kmap`函数不能用于中断处理程序, 因为它可能进入睡眠状态. 如果`pkmap`数组中没有空闲位置, 该函数会进入睡眠状态, 直至情形有所改善. + +因此内核提供了一个备选的映射函数, 其执行是原子的, 逻辑上称为`kmap_atomic`. 该函数的一个主要优点是它比普通的`kmap`快速. 但它不能用 +于可能进入睡眠的代码. 因此, 它对于很快就需要一个临时页的简短代码,是非常理想的. + +`kmap_atomic`的定义在IA-32, PPC, Sparc32上是[特定于体系结构的](http://lxr.free-electrons.com/ident?v=4.7;i=kmap_atomic), 但这3种实现只有非常细微的差别. 其原型是相同的. + + +##3.1 kmap_atomic函数 +------- + + +```cpp +// http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=4.7#L55 +void *kmap_atomic(struct page *page) +``` +page是一个指向高端内存页的管理结构的指针, 而早期的内核中, 增加了一个类型为[enum km_type](http://lxr.free-electrons.com/ident?v=2.6.32;i=km_type)的[type参数](http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=2.6.32#L39), 用于指定所需的映射类型 + +```cpp +// http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=2.6.32#L39 +void *kmap_atomic(struct page *page, enum km_type type) +``` + +而在新的内核中, 删除了这个标识, 但是保留了`km_type`的最大值`KM_TYPE_NR` + + +```cpp +void *kmap_atomic(struct page *page) +{ + unsigned int idx; + unsigned long vaddr; + void *kmap; + int type; + + preempt_disable(); + pagefault_disable(); + if (!PageHighMem(page)) + return page_address(page); + +#ifdef CONFIG_DEBUG_HIGHMEM + /* + * There is no cache coherency issue when non VIVT, so force the + * dedicated kmap usage for better debugging purposes in that case. + */ + if (!cache_is_vivt()) + kmap = NULL; + else +#endif + kmap = kmap_high_get(page); + if (kmap) + return kmap; + + type = kmap_atomic_idx_push(); + + idx = FIX_KMAP_BEGIN + type + KM_TYPE_NR * smp_processor_id(); + vaddr = __fix_to_virt(idx); +#ifdef CONFIG_DEBUG_HIGHMEM + /* + * With debugging enabled, kunmap_atomic forces that entry to 0. + * Make sure it was indeed properly unmapped. + */ + BUG_ON(!pte_none(get_fixmap_pte(vaddr))); +#endif + /* + * When debugging is off, kunmap_atomic leaves the previous mapping + * in place, so the contained TLB flush ensures the TLB is updated + * with the new mapping. + */ + set_fixmap_pte(idx, mk_pte(page, kmap_prot)); + + return (void *)vaddr; +} +EXPORT_SYMBOL(kmap_atomic); +``` + + +这个函数不会被阻塞, 因此可以用在中断上下文和起亚不能重新调度的地方. 它也禁止内核抢占, 这是有必要的, 因此映射对每个处理器都是唯一的(调度可能对哪个处理器执行哪个进程做变动). + + +##3.2 kunmap_atomic函数 +------- + +可以通过函数kunmap_atomic取消映射 + +```cpp +/* + * Prevent people trying to call kunmap_atomic() as if it were kunmap() + * kunmap_atomic() should get the return value of kmap_atomic, not the page. + */ +#define kunmap_atomic(addr) \ +do { \ + BUILD_BUG_ON(__same_type((addr), struct page *)); \ + __kunmap_atomic(addr); \ +} while (0) +``` + +这个函数也不会阻塞. 在很多体系结构中, 除非激活了内核抢占, 否则kunmap_atomic根本无事可做, 因为只有在下一个临时映射到来前上一个临时映射才有效. 因此, 内核完全可以"忘掉"kmap_atomic映射, kunmap_atomic也无需做什么实际的事情. 下一个原子映射将自动覆盖前一个映射. + diff --git a/study/kernel/02-memory/04-buddy/07-highmem_mapping/08-kmap_atomic-(kernel_mapping)/README.md b/study/kernel/02-memory/04-buddy/07-highmem_mapping/08-kmap_atomic-(kernel_mapping)/README.md index 4dad287..d431dab 100644 --- a/study/kernel/02-memory/04-buddy/07-highmem_mapping/08-kmap_atomic-(kernel_mapping)/README.md +++ b/study/kernel/02-memory/04-buddy/07-highmem_mapping/08-kmap_atomic-(kernel_mapping)/README.md @@ -1,365 +1,365 @@ -服务器体系与共享存储器架构 -======= - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - - - - -在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法. - -Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述. - -伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者. - -* 内核如何记住哪些内存块是空闲的 - -* 分配空闲页面的方法 - -* 影响分配器行为的众多标识位 - -* 内存碎片的问题和分配器如何处理碎片 - - -#1 高端内存与内核映射 -------- - - -尽管`vmalloc`函数族可用于从高端内存域向内核映射页帧(这些在内核空间中通常是无法直接看到的), 但这并不是这些函数的实际用途. - -重要的是强调以下事实 : 内核提供了其他函数用于将`ZONE_HIGHMEM`页帧显式映射到内核空间, 这些函数与vmalloc机制无关. 因此, 这就造成了混乱. - - -而在高端内存的页不能永久地映射到内核地址空间. 因此, 通过alloc_pages()函数以\__GFP_HIGHMEM标志获得的内存页就不可能有逻辑地址. - -在x86_32体系结构总, 高于896MB的所有物理内存的范围大都是高端内存, 它并不会永久地或自动映射到内核地址空间, 尽管X86处理器能够寻址物理RAM的范围达到4GB(启用PAE可以寻址64GB), 一旦这些页被分配, 就必须映射到内核的逻辑地址空间上. 在x86_32上, 高端地址的页被映射到内核地址空间(即虚拟地址空间的3GB~4GB) - -内核地址空间的最后128 MiB用于何种用途呢? - -该部分有3个用途。 - -1. 虚拟内存中连续、但物理内存中不连续的内存区,可以在vmalloc区域分配. 该机制通常用于用户过程, 内核自身会试图尽力避免非连续的物理地址。内核通常会成功,因为大部分大的内存块都在启动时分配给内核,那时内存的碎片尚不严重。但在已经运行了很长时间的系统上, 在内核需要物理内存时, 就可能出现可用空间不连续的情况. 此类情况, 主要出现在动态加载模块时. - -2. 持久映射用于将高端内存域中的非持久页映射到内核中 - -3. 固定映射是与物理地址空间中的固定页关联的虚拟地址空间项,但具体关联的页帧可以自由选择. 它与通过固定公式与物理内存关联的直接映射页相反,虚拟固定映射地址与物理内存位置之间的关联可以自行定义,关联建立后内核总是会注意到的. - -![x86_32上的地址划分](../../images/x86_32_mapping.png) - -在这里有两个预处理器符号很重要 \__VMALLOC_RESERVE设置了`vmalloc`区域的长度, 而`MAXMEM`则表示内核可以直接寻址的物理内存的最大可能数量. - -![内核虚拟地址空间](../../images/kernel_space.jpg) - - -内核中, 将内存划分为各个区域是通过图3-15所示的各个常数控制的。根据内核和系统配置, 这些常数可能有不同的值。直接映射的边界由high_memory指定。 - - - - -1. 直接映射区 - 线性空间中从3G开始最大896M的区间, 为直接内存映射区,该区域的线性地址和物理地址存在线性转换关系:线性地址=3G+物理地址。 - -2. 动态内存映射区 - 该区域由内核函数`vmalloc`来分配, 特点是 : 线性空间连续, 但是对应的物理空间不一定连续. `vmalloc`分配的线性地址所对应的物理页可能处于低端内存, 也可能处于高端内存. - -3. 永久内存映射区 - 该区域可访问高端内存. 访问方法是使用`alloc_page(_GFP_HIGHMEM)`分配高端内存页或者使用`kmap`函数将分配到的高端内存映射到该区域. - -4. 固定映射区 - 该区域和4G的顶端只有4k的隔离带,其每个地址项都服务于特定的用途,如ACPI_BASE等。 - - ->说明 -> ->注意用户空间当然可以使用高端内存,而且是正常的使用,内核在分配那些不经常使用的内存时,都用高端内存空间(如果有),所谓不经常使用是相对来说的,比如内核的一些数据结构就属于经常使用的,而用户的一些数据就属于不经常使用的。用户在启动一个应用程序时,是需要内存的,而每个应用程序都有3G的线性地址,给这些地址映射页表时就可以直接使用高端内存。 -> ->而且还要纠正一点的是:那128M线性地址不仅仅是用在这些地方的,如果你要加载一个设备,而这个设备需要映射其内存到内核中,它也需要使用这段线性地址空间来完成,否则内核就不能访问设备上的内存空间了. -> ->总之,内核的高端线性地址是为了访问内核固定映射以外的内存资源。进程在使用内存时,触发缺页异常,具体将哪些物理页映射给用户进程是内核考虑的事情. 在用户空间中没有高端内存这个概念. - -即内核对于低端内存, 不需要特殊的映射机制, 使用**直接映射**即可以访问普通内存区域, 而对于高端内存区域, 内核可以采用三种不同的机制将页框映射到高端内存 : 分别叫做**永久内核映射**、**临时内核映射**以及**非连续内存分配** - - - -#2 固定映射 -------- - - - -##2.1 数据结构 -------- - -linux高端内存中的临时内存区为固定内存区的一部分, 对于固定内存在linux内核中有下面描述 - -| x86 | arm | arm64 | -|:----:|:-----:|:-------:| -| [arch/x86/include/asm/fixmap.h?v=4.7, line 67](http://lxr.free-electrons.com/source/arch/x86/include/asm/fixmap.h?v=4.7#L67) | [arch/arm/include/asm/fixmap.h?v=4.7, line 11](http://lxr.free-electrons.com/source/arch/arm/include/asm/fixmap.h?v=4.7#L11) | [arch/arm64/include/asm/fixmap.h?v=4.7, line 36](http://lxr.free-electrons.com/source/arch/arm64/include/asm/fixmap.h?v=4.7#L36) | - -```cpp -/* - * Here we define all the compile-time 'special' virtual - * addresses. The point is to have a constant address at - * compile time, but to set the physical address only - * in the boot process. - * - * These 'compile-time allocated' memory buffers are - * page-sized. Use set_fixmap(idx,phys) to associate - * physical memory with fixmap indices. - * - */ -enum fixed_addresses { - FIX_HOLE, - - /* - * Reserve a virtual window for the FDT that is 2 MB larger than the - * maximum supported size, and put it at the top of the fixmap region. - * The additional space ensures that any FDT that does not exceed - * MAX_FDT_SIZE can be mapped regardless of whether it crosses any - * 2 MB alignment boundaries. - * - * Keep this at the top so it remains 2 MB aligned. - */ -#define FIX_FDT_SIZE (MAX_FDT_SIZE + SZ_2M) - FIX_FDT_END, - FIX_FDT = FIX_FDT_END + FIX_FDT_SIZE / PAGE_SIZE - 1, - - FIX_EARLYCON_MEM_BASE, - FIX_TEXT_POKE0, - __end_of_permanent_fixed_addresses, - - /* - * Temporary boot-time mappings, used by early_ioremap(), - * before ioremap() is functional. - */ -#define NR_FIX_BTMAPS (SZ_256K / PAGE_SIZE) -#define FIX_BTMAPS_SLOTS 7 -#define TOTAL_FIX_BTMAPS (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS) - - FIX_BTMAP_END = __end_of_permanent_fixed_addresses, - FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1, - - /* - * Used for kernel page table creation, so unmapped memory may be used - * for tables. - */ - FIX_PTE, - FIX_PMD, - FIX_PUD, - FIX_PGD, - - __end_of_fixed_addresses -}; -``` - - -##2.2 固定映射 -------- - - -`ioremap`的作用是将`IO`和`BIOS`以及物理地址空间映射到在896M至1G的128M的地址空间内, 使得kernel能够访问该空间并进行相应的读写操作。 - - ->start_kernel()->setup_arch()->early_ioremap_init() - -然后arm和arm64上`early_ioremap_init`又是`early_ioremap_setup`的前端 - -| 函数 | x86 |arm | arm64 | -|:-----:|:----:|:----:|:--------:| -| early_ioremap_init | [arch/x86/mm/ioremap.c?v=4.7, line 445](http://lxr.free-electrons.com/source/arch/x86/mm/ioremap.c?v=4.7#L445) | [arch/arm/mm/ioremap.c?v=4.7, line 489](http://lxr.free-electrons.com/source/arch/arm/mm/ioremap.c?v=4.7#L489) | [arch/arm64/mm/ioremap.c?v=4.7, line 110](http://lxr.free-electrons.com/source/arch/arm64/mm/ioremap.c?v=4.7#L110) | -| early_ioremap_setup | [mm/early_ioremap.c?v=4.7, line 67](http://lxr.free-electrons.com/source/mm/early_ioremap.c?v=4.7#L67) | 体系结构无关 | 体系结构无关 | - -其中arm和arm64架构下的`early_ioremap_init`函数实现比较简单, 都是直接的`early_ioremap_setup`函数的前端 - - -```cpp -/* - * Must be called after early_fixmap_init - */ -void __init early_ioremap_init(void) -{ - early_ioremap_setup(); -} -``` - -但是arm和arm64下的setup_arch函数则会先调用`early_fixmap_init`函数来填充`fixmap`. 参见[arch/arm/kernel/setup.c?v=4.7, line 1058](http://lxr.free-electrons.com/source/arch/arm/kernel/setup.c?v=4.7#L1058)和[arch/arm64/kernel/setup.c?v=4.7, line 229](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229). - -```cpp -void __init setup_arch(char **cmdline_p) -{ - early_fixmap_init(); - early_ioremap_init(); -} -``` - -`early_fixmap_init`函数的定义在 - -| arm | arm64 | -|:------:|:------:| -| [arch/arm/mm/mmu.c?v=4.7, line 385](http://lxr.free-electrons.com/source/arch/arm/mm/mmu.c?v=4.7#L385) | [arch/arm64/mm/mmu.c?v=4.7, line 676](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L676) | - - -其中arm架构的定义如下所示, 在[arch/arm/mm/mmu.c?v=4.7, line 385](http://lxr.free-electrons.com/source/arch/arm/mm/mmu.c?v=4.7#L385) - -```cpp -void __init early_fixmap_init(void) -{ - pmd_t *pmd; - - /* - * The early fixmap range spans multiple pmds, for which - * we are not prepared: - */ - BUILD_BUG_ON((__fix_to_virt(__end_of_early_ioremap_region) >> PMD_SHIFT) - != FIXADDR_TOP >> PMD_SHIFT); - - /*得到固定映射区的pmd - ,此pmd为虚拟地址转换为物理地址的pmd*/ - pmd = fixmap_pmd(FIXADDR_TOP); - /*将bm_pte页表设置为固定映射区开始地址的pmd的第一个页表;*/ - pmd_populate_kernel(&init_mm, pmd, bm_pte); - - pte_offset_fixmap = pte_offset_early_fixmap; -} -``` - -随后`setup_arch`中调用`early_ioremap_setup`函数将`fixed_address`里的索引的虚拟地址放入`slot_virt`, 参见[mm/early_ioremap.c?v=4.7, line 63](http://lxr.free-electrons.com/source/mm/early_ioremap.c?v=4.7#L63) - -```cpp -static void __iomem *prev_map[FIX_BTMAPS_SLOTS] __initdata; -static unsigned long prev_size[FIX_BTMAPS_SLOTS] __initdata; -static unsigned long slot_virt[FIX_BTMAPS_SLOTS] __initdata; - -void __init early_ioremap_setup(void) -{ - int i; - - for (i = 0; i < FIX_BTMAPS_SLOTS; i++) - if (WARN_ON(prev_map[i])) - break; - /* 将fixed_address里的索引的虚拟地址放入slot_virt - 从代码里面可以看出,放入slot_virt中得虚拟地址为1M */ - for (i = 0; i < FIX_BTMAPS_SLOTS; i++) - slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i); -} -``` - -而x86下的没有定义early_fixmap_init函数, 因此在`early_ioremap_init`函数中完成了fixmap的初始化工作, 定义在[arch/x86/mm/ioremap.c?v=4.7, line 445](http://lxr.free-electrons.com/source/arch/x86/mm/ioremap.c?v=4.7#L445) - - - -##2.3 ioremap函数 -------- - - -对于`ioremap`的使用需要通过`early_memremap`和`early_iounmap`进行. - -由于对应于`ioremap`的内存空间是有限的, 所以对于`ioremap`空间的使用遵照使用结束马上释放的原则. 这就是说`early_memremap`和`early_iounmap`必须配对使用并且访问结束必须马上执行`unmap` - - - - - -#3 临时内核映射 -------- - - -刚才描述的`kmap`函数不能用于中断处理程序, 因为它可能进入睡眠状态. 如果`pkmap`数组中没有空闲位置, 该函数会进入睡眠状态, 直至情形有所改善. - -因此内核提供了一个备选的映射函数, 其执行是原子的, 逻辑上称为`kmap_atomic`. 该函数的一个主要优点是它比普通的`kmap`快速. 但它不能用 -于可能进入睡眠的代码. 因此, 它对于很快就需要一个临时页的简短代码,是非常理想的. - -`kmap_atomic`的定义在IA-32, PPC, Sparc32上是[特定于体系结构的](http://lxr.free-electrons.com/ident?v=4.7;i=kmap_atomic), 但这3种实现只有非常细微的差别. 其原型是相同的. - - -##3.1 kmap_atomic函数 -------- - - -```cpp -// http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=4.7#L55 -void *kmap_atomic(struct page *page) -``` -page是一个指向高端内存页的管理结构的指针, 而早期的内核中, 增加了一个类型为[enum km_type](http://lxr.free-electrons.com/ident?v=2.6.32;i=km_type)的[type参数](http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=2.6.32#L39), 用于指定所需的映射类型 - -```cpp -// http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=2.6.32#L39 -void *kmap_atomic(struct page *page, enum km_type type) -``` - -而在新的内核中, 删除了这个标识, 但是保留了`km_type`的最大值`KM_TYPE_NR` - - -```cpp -void *kmap_atomic(struct page *page) -{ - unsigned int idx; - unsigned long vaddr; - void *kmap; - int type; - - preempt_disable(); - pagefault_disable(); - if (!PageHighMem(page)) - return page_address(page); - -#ifdef CONFIG_DEBUG_HIGHMEM - /* - * There is no cache coherency issue when non VIVT, so force the - * dedicated kmap usage for better debugging purposes in that case. - */ - if (!cache_is_vivt()) - kmap = NULL; - else -#endif - kmap = kmap_high_get(page); - if (kmap) - return kmap; - - type = kmap_atomic_idx_push(); - - idx = FIX_KMAP_BEGIN + type + KM_TYPE_NR * smp_processor_id(); - vaddr = __fix_to_virt(idx); -#ifdef CONFIG_DEBUG_HIGHMEM - /* - * With debugging enabled, kunmap_atomic forces that entry to 0. - * Make sure it was indeed properly unmapped. - */ - BUG_ON(!pte_none(get_fixmap_pte(vaddr))); -#endif - /* - * When debugging is off, kunmap_atomic leaves the previous mapping - * in place, so the contained TLB flush ensures the TLB is updated - * with the new mapping. - */ - set_fixmap_pte(idx, mk_pte(page, kmap_prot)); - - return (void *)vaddr; -} -EXPORT_SYMBOL(kmap_atomic); -``` - - -这个函数不会被阻塞, 因此可以用在中断上下文和起亚不能重新调度的地方. 它也禁止内核抢占, 这是有必要的, 因此映射对每个处理器都是唯一的(调度可能对哪个处理器执行哪个进程做变动). - - -##3.2 kunmap_atomic函数 -------- - -可以通过函数kunmap_atomic取消映射 - -```cpp -/* - * Prevent people trying to call kunmap_atomic() as if it were kunmap() - * kunmap_atomic() should get the return value of kmap_atomic, not the page. - */ -#define kunmap_atomic(addr) \ -do { \ - BUILD_BUG_ON(__same_type((addr), struct page *)); \ - __kunmap_atomic(addr); \ -} while (0) -``` - -这个函数也不会阻塞. 在很多体系结构中, 除非激活了内核抢占, 否则`kunmap_atomic`根本无事可做, 因为只有在下一个临时映射到来前上一个临时映射才有效. 因此, 内核完全可以"忘掉"kmap_atomic映射, kunmap_atomic也无需做什么实际的事情. 下一个原子映射将自动覆盖前一个映射. - +服务器体系与共享存储器架构 +======= + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + + + + +在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法. + +Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述. + +伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者. + +* 内核如何记住哪些内存块是空闲的 + +* 分配空闲页面的方法 + +* 影响分配器行为的众多标识位 + +* 内存碎片的问题和分配器如何处理碎片 + + +#1 高端内存与内核映射 +------- + + +尽管`vmalloc`函数族可用于从高端内存域向内核映射页帧(这些在内核空间中通常是无法直接看到的), 但这并不是这些函数的实际用途. + +重要的是强调以下事实 : 内核提供了其他函数用于将`ZONE_HIGHMEM`页帧显式映射到内核空间, 这些函数与vmalloc机制无关. 因此, 这就造成了混乱. + + +而在高端内存的页不能永久地映射到内核地址空间. 因此, 通过alloc_pages()函数以\__GFP_HIGHMEM标志获得的内存页就不可能有逻辑地址. + +在x86_32体系结构总, 高于896MB的所有物理内存的范围大都是高端内存, 它并不会永久地或自动映射到内核地址空间, 尽管X86处理器能够寻址物理RAM的范围达到4GB(启用PAE可以寻址64GB), 一旦这些页被分配, 就必须映射到内核的逻辑地址空间上. 在x86_32上, 高端地址的页被映射到内核地址空间(即虚拟地址空间的3GB~4GB) + +内核地址空间的最后128 MiB用于何种用途呢? + +该部分有3个用途。 + +1. 虚拟内存中连续、但物理内存中不连续的内存区,可以在vmalloc区域分配. 该机制通常用于用户过程, 内核自身会试图尽力避免非连续的物理地址。内核通常会成功,因为大部分大的内存块都在启动时分配给内核,那时内存的碎片尚不严重。但在已经运行了很长时间的系统上, 在内核需要物理内存时, 就可能出现可用空间不连续的情况. 此类情况, 主要出现在动态加载模块时. + +2. 持久映射用于将高端内存域中的非持久页映射到内核中 + +3. 固定映射是与物理地址空间中的固定页关联的虚拟地址空间项,但具体关联的页帧可以自由选择. 它与通过固定公式与物理内存关联的直接映射页相反,虚拟固定映射地址与物理内存位置之间的关联可以自行定义,关联建立后内核总是会注意到的. + +![x86_32上的地址划分](../../images/x86_32_mapping.png) + +在这里有两个预处理器符号很重要 \__VMALLOC_RESERVE设置了`vmalloc`区域的长度, 而`MAXMEM`则表示内核可以直接寻址的物理内存的最大可能数量. + +![内核虚拟地址空间](../../images/kernel_space.jpg) + + +内核中, 将内存划分为各个区域是通过图3-15所示的各个常数控制的。根据内核和系统配置, 这些常数可能有不同的值。直接映射的边界由high_memory指定。 + + + + +1. 直接映射区 + 线性空间中从3G开始最大896M的区间, 为直接内存映射区,该区域的线性地址和物理地址存在线性转换关系:线性地址=3G+物理地址。 + +2. 动态内存映射区 + 该区域由内核函数`vmalloc`来分配, 特点是 : 线性空间连续, 但是对应的物理空间不一定连续. `vmalloc`分配的线性地址所对应的物理页可能处于低端内存, 也可能处于高端内存. + +3. 永久内存映射区 + 该区域可访问高端内存. 访问方法是使用`alloc_page(_GFP_HIGHMEM)`分配高端内存页或者使用`kmap`函数将分配到的高端内存映射到该区域. + +4. 固定映射区 + 该区域和4G的顶端只有4k的隔离带,其每个地址项都服务于特定的用途,如ACPI_BASE等。 + + +>说明 +> +>注意用户空间当然可以使用高端内存,而且是正常的使用,内核在分配那些不经常使用的内存时,都用高端内存空间(如果有),所谓不经常使用是相对来说的,比如内核的一些数据结构就属于经常使用的,而用户的一些数据就属于不经常使用的。用户在启动一个应用程序时,是需要内存的,而每个应用程序都有3G的线性地址,给这些地址映射页表时就可以直接使用高端内存。 +> +>而且还要纠正一点的是:那128M线性地址不仅仅是用在这些地方的,如果你要加载一个设备,而这个设备需要映射其内存到内核中,它也需要使用这段线性地址空间来完成,否则内核就不能访问设备上的内存空间了. +> +>总之,内核的高端线性地址是为了访问内核固定映射以外的内存资源。进程在使用内存时,触发缺页异常,具体将哪些物理页映射给用户进程是内核考虑的事情. 在用户空间中没有高端内存这个概念. + +即内核对于低端内存, 不需要特殊的映射机制, 使用**直接映射**即可以访问普通内存区域, 而对于高端内存区域, 内核可以采用三种不同的机制将页框映射到高端内存 : 分别叫做**永久内核映射**、**临时内核映射**以及**非连续内存分配** + + + +#2 固定映射 +------- + + + +##2.1 数据结构 +------- + +linux高端内存中的临时内存区为固定内存区的一部分, 对于固定内存在linux内核中有下面描述 + +| x86 | arm | arm64 | +|:----:|:-----:|:-------:| +| [arch/x86/include/asm/fixmap.h?v=4.7, line 67](http://lxr.free-electrons.com/source/arch/x86/include/asm/fixmap.h?v=4.7#L67) | [arch/arm/include/asm/fixmap.h?v=4.7, line 11](http://lxr.free-electrons.com/source/arch/arm/include/asm/fixmap.h?v=4.7#L11) | [arch/arm64/include/asm/fixmap.h?v=4.7, line 36](http://lxr.free-electrons.com/source/arch/arm64/include/asm/fixmap.h?v=4.7#L36) | + +```cpp +/* + * Here we define all the compile-time 'special' virtual + * addresses. The point is to have a constant address at + * compile time, but to set the physical address only + * in the boot process. + * + * These 'compile-time allocated' memory buffers are + * page-sized. Use set_fixmap(idx,phys) to associate + * physical memory with fixmap indices. + * + */ +enum fixed_addresses { + FIX_HOLE, + + /* + * Reserve a virtual window for the FDT that is 2 MB larger than the + * maximum supported size, and put it at the top of the fixmap region. + * The additional space ensures that any FDT that does not exceed + * MAX_FDT_SIZE can be mapped regardless of whether it crosses any + * 2 MB alignment boundaries. + * + * Keep this at the top so it remains 2 MB aligned. + */ +#define FIX_FDT_SIZE (MAX_FDT_SIZE + SZ_2M) + FIX_FDT_END, + FIX_FDT = FIX_FDT_END + FIX_FDT_SIZE / PAGE_SIZE - 1, + + FIX_EARLYCON_MEM_BASE, + FIX_TEXT_POKE0, + __end_of_permanent_fixed_addresses, + + /* + * Temporary boot-time mappings, used by early_ioremap(), + * before ioremap() is functional. + */ +#define NR_FIX_BTMAPS (SZ_256K / PAGE_SIZE) +#define FIX_BTMAPS_SLOTS 7 +#define TOTAL_FIX_BTMAPS (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS) + + FIX_BTMAP_END = __end_of_permanent_fixed_addresses, + FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1, + + /* + * Used for kernel page table creation, so unmapped memory may be used + * for tables. + */ + FIX_PTE, + FIX_PMD, + FIX_PUD, + FIX_PGD, + + __end_of_fixed_addresses +}; +``` + + +##2.2 固定映射 +------- + + +`ioremap`的作用是将`IO`和`BIOS`以及物理地址空间映射到在896M至1G的128M的地址空间内, 使得kernel能够访问该空间并进行相应的读写操作。 + + +>start_kernel()->setup_arch()->early_ioremap_init() + +然后arm和arm64上`early_ioremap_init`又是`early_ioremap_setup`的前端 + +| 函数 | x86 |arm | arm64 | +|:-----:|:----:|:----:|:--------:| +| early_ioremap_init | [arch/x86/mm/ioremap.c?v=4.7, line 445](http://lxr.free-electrons.com/source/arch/x86/mm/ioremap.c?v=4.7#L445) | [arch/arm/mm/ioremap.c?v=4.7, line 489](http://lxr.free-electrons.com/source/arch/arm/mm/ioremap.c?v=4.7#L489) | [arch/arm64/mm/ioremap.c?v=4.7, line 110](http://lxr.free-electrons.com/source/arch/arm64/mm/ioremap.c?v=4.7#L110) | +| early_ioremap_setup | [mm/early_ioremap.c?v=4.7, line 67](http://lxr.free-electrons.com/source/mm/early_ioremap.c?v=4.7#L67) | 体系结构无关 | 体系结构无关 | + +其中arm和arm64架构下的`early_ioremap_init`函数实现比较简单, 都是直接的`early_ioremap_setup`函数的前端 + + +```cpp +/* + * Must be called after early_fixmap_init + */ +void __init early_ioremap_init(void) +{ + early_ioremap_setup(); +} +``` + +但是arm和arm64下的setup_arch函数则会先调用`early_fixmap_init`函数来填充`fixmap`. 参见[arch/arm/kernel/setup.c?v=4.7, line 1058](http://lxr.free-electrons.com/source/arch/arm/kernel/setup.c?v=4.7#L1058)和[arch/arm64/kernel/setup.c?v=4.7, line 229](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229). + +```cpp +void __init setup_arch(char **cmdline_p) +{ + early_fixmap_init(); + early_ioremap_init(); +} +``` + +`early_fixmap_init`函数的定义在 + +| arm | arm64 | +|:------:|:------:| +| [arch/arm/mm/mmu.c?v=4.7, line 385](http://lxr.free-electrons.com/source/arch/arm/mm/mmu.c?v=4.7#L385) | [arch/arm64/mm/mmu.c?v=4.7, line 676](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L676) | + + +其中arm架构的定义如下所示, 在[arch/arm/mm/mmu.c?v=4.7, line 385](http://lxr.free-electrons.com/source/arch/arm/mm/mmu.c?v=4.7#L385) + +```cpp +void __init early_fixmap_init(void) +{ + pmd_t *pmd; + + /* + * The early fixmap range spans multiple pmds, for which + * we are not prepared: + */ + BUILD_BUG_ON((__fix_to_virt(__end_of_early_ioremap_region) >> PMD_SHIFT) + != FIXADDR_TOP >> PMD_SHIFT); + + /*得到固定映射区的pmd + ,此pmd为虚拟地址转换为物理地址的pmd*/ + pmd = fixmap_pmd(FIXADDR_TOP); + /*将bm_pte页表设置为固定映射区开始地址的pmd的第一个页表;*/ + pmd_populate_kernel(&init_mm, pmd, bm_pte); + + pte_offset_fixmap = pte_offset_early_fixmap; +} +``` + +随后`setup_arch`中调用`early_ioremap_setup`函数将`fixed_address`里的索引的虚拟地址放入`slot_virt`, 参见[mm/early_ioremap.c?v=4.7, line 63](http://lxr.free-electrons.com/source/mm/early_ioremap.c?v=4.7#L63) + +```cpp +static void __iomem *prev_map[FIX_BTMAPS_SLOTS] __initdata; +static unsigned long prev_size[FIX_BTMAPS_SLOTS] __initdata; +static unsigned long slot_virt[FIX_BTMAPS_SLOTS] __initdata; + +void __init early_ioremap_setup(void) +{ + int i; + + for (i = 0; i < FIX_BTMAPS_SLOTS; i++) + if (WARN_ON(prev_map[i])) + break; + /* 将fixed_address里的索引的虚拟地址放入slot_virt + 从代码里面可以看出,放入slot_virt中得虚拟地址为1M */ + for (i = 0; i < FIX_BTMAPS_SLOTS; i++) + slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i); +} +``` + +而x86下的没有定义early_fixmap_init函数, 因此在`early_ioremap_init`函数中完成了fixmap的初始化工作, 定义在[arch/x86/mm/ioremap.c?v=4.7, line 445](http://lxr.free-electrons.com/source/arch/x86/mm/ioremap.c?v=4.7#L445) + + + +##2.3 ioremap函数 +------- + + +对于`ioremap`的使用需要通过`early_memremap`和`early_iounmap`进行. + +由于对应于`ioremap`的内存空间是有限的, 所以对于`ioremap`空间的使用遵照使用结束马上释放的原则. 这就是说`early_memremap`和`early_iounmap`必须配对使用并且访问结束必须马上执行`unmap` + + + + + +#3 临时内核映射 +------- + + +刚才描述的`kmap`函数不能用于中断处理程序, 因为它可能进入睡眠状态. 如果`pkmap`数组中没有空闲位置, 该函数会进入睡眠状态, 直至情形有所改善. + +因此内核提供了一个备选的映射函数, 其执行是原子的, 逻辑上称为`kmap_atomic`. 该函数的一个主要优点是它比普通的`kmap`快速. 但它不能用 +于可能进入睡眠的代码. 因此, 它对于很快就需要一个临时页的简短代码,是非常理想的. + +`kmap_atomic`的定义在IA-32, PPC, Sparc32上是[特定于体系结构的](http://lxr.free-electrons.com/ident?v=4.7;i=kmap_atomic), 但这3种实现只有非常细微的差别. 其原型是相同的. + + +##3.1 kmap_atomic函数 +------- + + +```cpp +// http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=4.7#L55 +void *kmap_atomic(struct page *page) +``` +page是一个指向高端内存页的管理结构的指针, 而早期的内核中, 增加了一个类型为[enum km_type](http://lxr.free-electrons.com/ident?v=2.6.32;i=km_type)的[type参数](http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=2.6.32#L39), 用于指定所需的映射类型 + +```cpp +// http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=2.6.32#L39 +void *kmap_atomic(struct page *page, enum km_type type) +``` + +而在新的内核中, 删除了这个标识, 但是保留了`km_type`的最大值`KM_TYPE_NR` + + +```cpp +void *kmap_atomic(struct page *page) +{ + unsigned int idx; + unsigned long vaddr; + void *kmap; + int type; + + preempt_disable(); + pagefault_disable(); + if (!PageHighMem(page)) + return page_address(page); + +#ifdef CONFIG_DEBUG_HIGHMEM + /* + * There is no cache coherency issue when non VIVT, so force the + * dedicated kmap usage for better debugging purposes in that case. + */ + if (!cache_is_vivt()) + kmap = NULL; + else +#endif + kmap = kmap_high_get(page); + if (kmap) + return kmap; + + type = kmap_atomic_idx_push(); + + idx = FIX_KMAP_BEGIN + type + KM_TYPE_NR * smp_processor_id(); + vaddr = __fix_to_virt(idx); +#ifdef CONFIG_DEBUG_HIGHMEM + /* + * With debugging enabled, kunmap_atomic forces that entry to 0. + * Make sure it was indeed properly unmapped. + */ + BUG_ON(!pte_none(get_fixmap_pte(vaddr))); +#endif + /* + * When debugging is off, kunmap_atomic leaves the previous mapping + * in place, so the contained TLB flush ensures the TLB is updated + * with the new mapping. + */ + set_fixmap_pte(idx, mk_pte(page, kmap_prot)); + + return (void *)vaddr; +} +EXPORT_SYMBOL(kmap_atomic); +``` + + +这个函数不会被阻塞, 因此可以用在中断上下文和起亚不能重新调度的地方. 它也禁止内核抢占, 这是有必要的, 因此映射对每个处理器都是唯一的(调度可能对哪个处理器执行哪个进程做变动). + + +##3.2 kunmap_atomic函数 +------- + +可以通过函数kunmap_atomic取消映射 + +```cpp +/* + * Prevent people trying to call kunmap_atomic() as if it were kunmap() + * kunmap_atomic() should get the return value of kmap_atomic, not the page. + */ +#define kunmap_atomic(addr) \ +do { \ + BUILD_BUG_ON(__same_type((addr), struct page *)); \ + __kunmap_atomic(addr); \ +} while (0) +``` + +这个函数也不会阻塞. 在很多体系结构中, 除非激活了内核抢占, 否则`kunmap_atomic`根本无事可做, 因为只有在下一个临时映射到来前上一个临时映射才有效. 因此, 内核完全可以"忘掉"kmap_atomic映射, kunmap_atomic也无需做什么实际的事情. 下一个原子映射将自动覆盖前一个映射. + diff --git a/study/kernel/02-memory/README.md b/study/kernel/02-memory/README.md index 280580d..d780241 100644 --- a/study/kernel/02-memory/README.md +++ b/study/kernel/02-memory/README.md @@ -1,49 +1,49 @@ -服务器体系与共享存储器架构 -======= - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - - -#1 目录 -------- - -| 目录 | 描述 | -|:-------:|:-------:| -| 描述物理内存 | -| 页表管理 | -| 初始化内存管理 | -| 伙伴系统物理内存管理 | -| slab分配器 | -| 非连续内存分配 | -| 高端内存管理 | -| --页面帧回收-- | -| --交换管理-- | -| --进程虚拟地址空间-- | -| 共享内存虚拟文件系统 | -| 内存溢出管理 | -| 进程虚拟内存 | -| 页缓存和块缓存 | -| 页面回收和页交换 | - -#2 参考内容 -------- - - - -| 链接 | -|:-------:| -| [内存管理(一)内存模型之Node](http://biancheng.dnbcw.info/linux/387391.html) | -| [Linux 内存管理 重要结构体](http://blog.chinaunix.net/uid-26009500-id-3078986.html) | -| [Bootmem机制](http://blog.csdn.net/samssm/article/details/25064897) | -| [Linux-2.6.32 NUMA架构之内存和调度](http://www.cnblogs.com/zhenjing/archive/2012/03/21/linux_numa.html) | -| [Linux 用户空间与内核空间——高端内存详解](http://blog.csdn.net/tommy_wxie/article/details/17122923) | -| [探索 Linux 内存模型](http://www.ibm.com/developerworks/cn/linux/l-memmod/) | -| [Linux内存管理](http://blog.chinaunix.net/uid/21718047/cid-151509-list-2.html) | -| [内存管理-之内核内存管理-基于linux3.10](http://blog.csdn.net/shichaog/article/details/45509917) | -| [内存管理(一)](http://www.cnblogs.com/openix/p/3334026.html) | -| [Linux内存管理原理](http://www.cnblogs.com/zhaoyl/p/3695517.html) | -| [第 15 章 内存映射和 DMA](http://www.embeddedlinux.org.cn/ldd3/ch15.html) | -| [ 内存管理(二)struct page ](http://blog.chinaunix.net/uid-30282771-id-5176971.html) | -| [Linux内存管理](http://blog.csdn.net/column/details/linux--mm.html) | +服务器体系与共享存储器架构 +======= + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + + +#1 目录 +------- + +| 目录 | 描述 | +|:-------:|:-------:| +| 描述物理内存 | +| 页表管理 | +| 初始化内存管理 | +| 伙伴系统物理内存管理 | +| slab分配器 | +| 非连续内存分配 | +| 高端内存管理 | +| --页面帧回收-- | +| --交换管理-- | +| --进程虚拟地址空间-- | +| 共享内存虚拟文件系统 | +| 内存溢出管理 | +| 进程虚拟内存 | +| 页缓存和块缓存 | +| 页面回收和页交换 | + +#2 参考内容 +------- + + + +| 链接 | +|:-------:| +| [内存管理(一)内存模型之Node](http://biancheng.dnbcw.info/linux/387391.html) | +| [Linux 内存管理 重要结构体](http://blog.chinaunix.net/uid-26009500-id-3078986.html) | +| [Bootmem机制](http://blog.csdn.net/samssm/article/details/25064897) | +| [Linux-2.6.32 NUMA架构之内存和调度](http://www.cnblogs.com/zhenjing/archive/2012/03/21/linux_numa.html) | +| [Linux 用户空间与内核空间——高端内存详解](http://blog.csdn.net/tommy_wxie/article/details/17122923) | +| [探索 Linux 内存模型](http://www.ibm.com/developerworks/cn/linux/l-memmod/) | +| [Linux内存管理](http://blog.chinaunix.net/uid/21718047/cid-151509-list-2.html) | +| [内存管理-之内核内存管理-基于linux3.10](http://blog.csdn.net/shichaog/article/details/45509917) | +| [内存管理(一)](http://www.cnblogs.com/openix/p/3334026.html) | +| [Linux内存管理原理](http://www.cnblogs.com/zhaoyl/p/3695517.html) | +| [第 15 章 内存映射和 DMA](http://www.embeddedlinux.org.cn/ldd3/ch15.html) | +| [ 内存管理(二)struct page ](http://blog.chinaunix.net/uid-30282771-id-5176971.html) | +| [Linux内存管理](http://blog.csdn.net/column/details/linux--mm.html) |