diff --git a/study/kernel/02-memory/01-description/03-zone/README.md b/study/kernel/02-memory/01-description/03-zone/README.md index b1a7750..96d14dc 100644 --- a/study/kernel/02-memory/01-description/03-zone/README.md +++ b/study/kernel/02-memory/01-description/03-zone/README.md @@ -1,915 +1,1104 @@ - 服务器体系与共享存储器架构 -======= - -| 日期 | 内核版本 | 架构| 作者 | 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 前景回顾 -------- - - -前面我们讲到[服务器体系(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)模型 - -**UMA模型** - -物理存储器被所有处理机均匀共享。所有处理机对所有存储字具有相同的存取时间,这就是为什么称它为均匀存储器存取的原因。每台处理机可以有私用高速缓存,外围设备也以一定形式共享。 - -**NUMA模型** - -NUMA模式下,处理器被划分成多个"节点"(node), 每个节点被分配有的本地存储器空间。 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多。 - - -#1.2 (N)UMA模型中linux内存的机构 -------- - - - -非一致存储器访问(NUMA)模式下 - -* 处理器被划分成多个"节点"(node), 每个节点被分配有的本地存储器空间. 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多 - - -* 内存被分割成多个区域(BANK,也叫"簇"),依据簇与处理器的"距离"不同, 访问不同簇的代码也会不同. 比如,可能把内存的一个簇指派给每个处理器,或则某个簇和设备卡很近,很适合DMA,那么就指派给该设备。因此当前的多数系统会把内存系统分割成2块区域,一块是专门给CPU去访问,一块是给外围设备板卡的DMA去访问 - ->在UMA系统中, 内存就相当于一个只使用一个NUMA节点来管理整个系统的内存. 而内存管理的其他地方则认为他们就是在处理一个(伪)NUMA系统. - - -##1.3 Linux如何描述物理内存 -------- - -Linux把物理内存划分为三个层次来管理 - -| 层次 | 描述 | -|:----:|:----:| -| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | -| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | -| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | - - -##1.4 用pd_data_t描述内存节点node -------- - - ->CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 -> ->系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 - -* 首先, 内存被划分为结点. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. - - -内存中的每个节点都是由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) - - -在分配一个页面时, Linux采用节点局部分配的策略, 从最靠近运行中的CPU的节点分配内存, 由于进程往往是在同一个CPU上运行, 因此从当前节点得到的内存很可能被用到 - - -##1.5 今日内容(内存管理域zone) -------- - -为了支持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, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. - - -下面我们就来详解讲讲内存管理域的内容zone - - -#2 为什么要将内存node分成不同的区域zone -------- - - -NUMA结构下, 每个处理器CPU与一个本地内存直接相连, 而不同处理器之前则通过总线进行进一步的连接, 因此相对于任何一个CPU访问本地内存的速度比访问远程内存的速度要快, 而Linux为了兼容NUMAJ结构, 把物理内存相依照CPU的不同node分成簇, 一个CPU-node对应一个本地内存pgdata_t. - - - -这样已经很好的表示物理内存了, 在一个理想的计算机系统中, 一个页框就是一个内存的分配单元, 可用于任何事情:存放内核数据, 用户数据和缓冲磁盘数据等等. 任何种类的数据页都可以存放在任页框中, 没有任何限制. - - -但是Linux内核又把各个物理内存节点分成个不同的管理区域zone, 这是为什么呢? - - -因为实际的计算机体系结构有硬件的诸多限制, 这限制了页框可以使用的方式. 尤其是, Linux内核必须处理80x86体系结构的两种硬件约束. - -* ISA总线的直接内存存储DMA处理器有一个严格的限制 : 他们只能对RAM的前16MB进行寻址 - -* 在具有大容量RAM的现代32位计算机中, CPU不能直接访问所有的物理地址, 因为线性地址空间太小, 内核不可能直接映射所有物理内存到线性地址空间, 我们会在后面典型架构(x86)上内存区域划分详细讲解x86_32上的内存区域划分 - - -因此Linux内核对不同区域的内存需要采用不同的管理方式和映射方式, 因此内核将物理地址或者成用zone_t表示的不同地址区域 - - - -#3 内存管理区类型zone_type -------- - -前面我们说了由于硬件的一些约束, 低端的一些地址被用于DMA, 而在实际内存大小超过了内核所能使用的现行地址的时候, 一些高地址处的物理地址不能简单持久的直接映射到内核空间. 因此内核将内存的节点node分成了不同的内存区域方便管理和映射. - -Linux使用enum zone_type来标记内核所支持的所有内存区域 - - -##3.1 内存区域类型zone_type -------- - -zone_type结构定义在[include/linux/mmzone.h](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L267), 其基本信息如下所示 - -```c -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_DMA类型的内存区域在物理内存的低端,主要是ISA设备只能用低端的地址做DMA操作。ZONE_NORMAL类型的内存区域直接被内核映射到线性地址空间上面的区域(line address space),ZONE_HIGHMEM将保留给系统使用,是系统中预留的可用内存空间,不能被内核直接映射。 - - -##3.2 不同的内存区域的作用 -------- - -在内存中,每个簇所对应的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)数据结构来描述) - -管理区的类型用zone_type表示, 有如下几种 - -| 管理内存域 | 描述 | -|:---------:|:---:| -| ZONE_DMA | 标记了适合DMA的内存域. 该区域的长度依赖于处理器类型. 这是由于古老的ISA设备强加的边界. 但是为了兼容性, 现代的计算机也可能受此影响 | -| ZONE_DMA32 | 标记了使用32位地址字可寻址, 适合DMA的内存域. 显然, 只有在53位系统中ZONE_DMA32才和ZONE_DMA有区别, 在32位系统中, 本区域是空的, 即长度为0MB, 在Alpha和AMD64系统上, 该内存的长度可能是从0到4GB | -| ZONE_NORMAL | 标记了可直接映射到内存段的普通内存域. 这是在所有体系结构上保证会存在的唯一内存区域, 但无法保证该地址范围对应了实际的物理地址. 例如, 如果AMD64系统只有两2G内存, 那么所有的内存都属于ZONE_DMA32范围, 而ZONE_NORMAL则为空 | -| ZONE_HIGHMEM | 标记了超出内核虚拟地址空间的物理内存段, 因此这段地址不能被内核直接映射 | -| ZONE_MOVABLE | 内核定义了一个伪内存域ZONE_MOVABLE, 在防止物理内存碎片的机制memory migration中需要使用该内存域. 供防止物理内存碎片的极致使用 | -| ZONE_DEVICE | 为支持热插拔设备而分配的Non Volatile Memory非易失性内存 | -| MAX_NR_ZONES | 充当结束标记, 在内核中想要迭代系统中所有内存域, 会用到该常亮 | - -根据编译时候的配置, 可能无需考虑某些内存域. 例如在64位系统中, 并不需要高端内存, 因为AM64的linux采用4级页表,支持的最大物理内存为64TB, 对于虚拟地址空间的划分,将0x0000,0000,0000,0000 – 0x0000,7fff,ffff,f000这128T地址用于用户空间;而0xffff,8000,0000,0000以上的128T为系统空间地址, 这远大于当前我们系统中的内存空间, 因此所有的物理地址都可以直接映射到内核中, 不需要高端内存的特殊映射. 可以参见[Documentation/x86/x86_64/mm.txt](https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt) - ` - - -ZONE_MOVABLE和ZONE_DEVICE其实是和其他的ZONE的用途有异, - -* ZONE_MOVABLE在防止物理内存碎片的机制中需要使用该内存区域, - -* ZONE_DEVICE笔者也第一次知道了,理解有错的话欢迎大家批评指正, 这个应该是为支持热插拔设备而分配的Non Volatile Memory非易失性内存, - - ->关于ZONE_DEVICE, 具体的信息可以参见[ATCH v2 3/9] mm: ZONE_DEVICE for "device memory"](https://lkml.org/lkml/2015/8/25/844) -> ->While pmem is usable as a block device or via DAX mappings to userspace -there are several usage scenarios that can not target pmem due to its -lack of struct page coverage. In preparation for "hot plugging" pmem -into the vmemmap add ZONE_DEVICE as a new zone to tag these pages -separately from the ones that are subject to standard page allocations. -Importantly "device memory" can be removed at will by userspace -unbinding the driver of the device. - - -##3.3 典型架构(x86)上内存区域划分 -------- - - -对于x86机器,管理区(内存区域)类型如下分布 - -| 类型 | 区域 | -| :------- | ----: | -| ZONE_DMA | 0~15MB | -| ZONE_NORMAL | 16MB~895MB | -| ZONE_HIGHMEM | 896MB~物理内存结束 | - -而由于32位系统中, Linux内核虚拟地址空间只有1G, 而0~895M这个986MB被用于DMA和直接映射, 剩余的物理内存被成为高端内存. 那内核是如何借助剩余128MB高端内存地址空间是如何实现访问可以所有物理内存? - -当内核想访问高于896MB物理地址内存时,从0xF8000000 ~ 0xFFFFFFFF地址空间范围内找一段相应大小空闲的逻辑地址空间,借用一会。借用这段逻辑地址空间,建立映射到想访问的那段物理内存(即填充内核PTE页面表),临时用一会,用完后归还。这样别人也可以借用这段地址空间访问其他物理内存,实现了使用有限的地址空间,访问所有所有物理内存 - ->关于高端内存的内容, 我们后面会专门抽出一章进行讲解 - -因此, 传统和X86_32位系统中, 前16M划分给ZONE_DMA, 该区域包含的页框可以由老式的基于ISAS的设备通过DMA使用"直接内存访问(DMA)", ZONE_DMA和ZONE_NORMAL区域包含了内存的常规页框, 通过把他们线性的映射到现行地址的第4个GB, 内核就可以直接进行访问, 相反ZONE_HIGHME包含的内存页不能由内核直接访问, 尽管他们也线性地映射到了现行地址空间的第4个GB. 在64位体系结构中, 线性地址空间的大小远远好过了系统的实际物理地址, 内核可知直接将所有的物理内存映射到线性地址空间, 因此64位体系结构上ZONE_HIGHMEM区域总是空的. - - -#4 管理区结构zone_t -------- - ->一个管理区(zone)由[`struct zone`](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L326)结构体来描述(linux-3.8~目前linux4.5),而在linux-2.4.37之前的内核中是用[`struct zone_struct `](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=2.4.37#L47)数据结构来描述), 他们都通过typedef被重定义为zone_t类型 - -zone对象用于跟踪诸如页面使用情况的统计数, 空闲区域信息和锁信息 - ->里面保存着内存使用状态信息,如page使用统计, 未使用的内存区域,互斥访问的锁(LOCKS)等. - - -##4.1 struct zone管理域数据结构 -------- - -`struct zone`在`linux/mmzone.h`中定义, 在linux-4.7的内核中可以使用[include/linux/mmzone.h](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L324)来查看其定义 - -```cpp -struct zone -{ - /* Read-mostly fields */ - - /* zone watermarks, access with *_wmark_pages(zone) macros */ - unsigned long watermark[NR_WMARK]; - - unsigned long nr_reserved_highatomic; - - /* - * We don't know if the memory that we're going to allocate will be - * freeable or/and it will be released eventually, so to avoid totally - * wasting several GB of ram we must reserve some of the lower zone - * memory (otherwise we risk to run OOM on the lower zones despite - * there being tons of freeable ram on the higher zones). This array is - * recalculated at runtime if the sysctl_lowmem_reserve_ratio sysctl - * changes. - * 分别为各种内存域指定了若干页 - * 用于一些无论如何都不能失败的关键性内存分配。 - */ - long lowmem_reserve[MAX_NR_ZONES]; - -#ifdef CONFIG_NUMA - int node; -#endif - - /* - * The target ratio of ACTIVE_ANON to INACTIVE_ANON pages on - * this zone's LRU. Maintained by the pageout code. - * 不活动页的比例, - * 接着是一些很少使用或者大部分情况下是只读的字段: - * wait_table wait_table_hash_nr_entries wait_table_bits - * 形成等待列队,可以等待某一页可供进程使用 */ - unsigned int inactive_ratio; - - /* 指向这个zone所在的pglist_data对象 */ - struct pglist_data *zone_pgdat; - /*/这个数组用于实现每个CPU的热/冷页帧列表。内核使用这些列表来保存可用于满足实现的“新鲜”页。但冷热页帧对应的高速缓存状态不同:有些页帧很可能在高速缓存中,因此可以快速访问,故称之为热的;未缓存的页帧与此相对,称之为冷的。*/ - struct per_cpu_pageset __percpu *pageset; - - /* - * This is a per-zone reserve of pages that are not available - * to userspace allocations. - * 每个区域保留的不能被用户空间分配的页面数目 - */ - unsigned long totalreserve_pages; - -#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 */ - -#ifdef CONFIG_NUMA - /* - * zone reclaim becomes active if more unmapped pages exist. - */ - unsigned long min_unmapped_pages; - unsigned long min_slab_pages; -#endif /* CONFIG_NUMA */ - - /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT - * 只内存域的第一个页帧 */ - unsigned long zone_start_pfn; - - /* - * spanned_pages is the total pages spanned by the zone, including - * holes, which is calculated as: - * spanned_pages = zone_end_pfn - zone_start_pfn; - * - * present_pages is physical pages existing within the zone, which - * is calculated as: - * present_pages = spanned_pages - absent_pages(pages in holes); - * - * managed_pages is present pages managed by the buddy system, which - * is calculated as (reserved_pages includes pages allocated by the - * bootmem allocator): - * managed_pages = present_pages - reserved_pages; - * - * So present_pages may be used by memory hotplug or memory power - * management logic to figure out unmanaged pages by checking - * (present_pages - managed_pages). And managed_pages should be used - * by page allocator and vm scanner to calculate all kinds of watermarks - * and thresholds. - * - * Locking rules: - * - * zone_start_pfn and spanned_pages are protected by span_seqlock. - * It is a seqlock because it has to be read outside of zone->lock, - * and it is done in the main allocator path. But, it is written - * quite infrequently. - * - * The span_seq lock is declared along with zone->lock because it is - * frequently read in proximity to zone->lock. It's good to - * give them a chance of being in the same cacheline. - * - * Write access to present_pages at runtime should be protected by - * mem_hotplug_begin/end(). Any reader who can't tolerant drift of - * present_pages should get_online_mems() to get a stable value. - * - * Read access to managed_pages should be safe because it's unsigned - * long. Write access to zone->managed_pages and totalram_pages are - * protected by managed_page_count_lock at runtime. Idealy only - * adjust_managed_page_count() should be used instead of directly - * touching zone->managed_pages and totalram_pages. - */ - unsigned long managed_pages; - unsigned long spanned_pages; /* 总页数,包含空洞 */ - unsigned long present_pages; /* 可用页数,不包哈空洞 */ - - /* 指向管理区的传统名字, "DMA", "NROMAL"或"HIGHMEM" */ - const char *name; - -#ifdef CONFIG_MEMORY_ISOLATION - /* - * Number of isolated pageblock. It is used to solve incorrect - * freepage counting problem due to racy retrieving migratetype - * of pageblock. Protected by zone->lock. - */ - unsigned long nr_isolate_pageblock; -#endif - -#ifdef CONFIG_MEMORY_HOTPLUG - /* see spanned/present_pages for more description */ - seqlock_t span_seqlock; -#endif - - /* - * wait_table -- the array holding the hash table - * wait_table_hash_nr_entries -- the size of the hash table array - * wait_table_bits -- wait_table_size == (1 << wait_table_bits) - * - * The purpose of all these is to keep track of the people - * waiting for a page to become available and make them - * runnable again when possible. The trouble is that this - * consumes a lot of space, especially when so few things - * wait on pages at a given time. So instead of using - * per-page waitqueues, we use a waitqueue hash table. - * - * The bucket discipline is to sleep on the same queue when - * colliding and wake all in that wait queue when removing. - * When something wakes, it must check to be sure its page is - * truly available, a la thundering herd. The cost of a - * collision is great, but given the expected load of the - * table, they should be so rare as to be outweighed by the - * benefits from the saved space. - * - * __wait_on_page_locked() and unlock_page() in mm/filemap.c, are the - * primary users of these fields, and in mm/page_alloc.c - * free_area_init_core() performs the initialization of them. - */ - /* 进程等待队列的散列表, 这些进程正在等待管理区中的某页 */ - wait_queue_head_t *wait_table; - /* 等待队列散列表中的调度实体数目 */ - unsigned long wait_table_hash_nr_entries; - /* 等待队列散列表数组大小, 值为2^order */ - unsigned long wait_table_bits; - - ZONE_PADDING(_pad1_) - - /* free areas of different sizes - 页面使用状态的信息,以每个bit标识对应的page是否可以分配 - 是用于伙伴系统的,每个数组元素指向对应阶也表的数组开头 - 以下是供页帧回收扫描器(page reclaim scanner)访问的字段 - scanner会跟据页帧的活动情况对内存域中使用的页进行编目 - 如果页帧被频繁访问,则是活动的,相反则是不活动的, - 在需要换出页帧时,这样的信息是很重要的: */ - struct free_area free_area[MAX_ORDER]; - - /* zone flags, see below 描述当前内存的状态, 参见下面的enum zone_flags结构 */ - unsigned long flags; - - /* Write-intensive fields used from the page allocator, 保存该描述符的自旋锁 */ - spinlock_t lock; - - ZONE_PADDING(_pad2_) - - /* Write-intensive fields used by page reclaim */ - - /* Fields commonly accessed by the page reclaim scanner */ - spinlock_t lru_lock; /* LRU(最近最少使用算法)活动以及非活动链表使用的自旋锁 */ - struct lruvec lruvec; - - /* - * When free pages are below this point, additional steps are taken - * when reading the number of free pages to avoid per-cpu counter - * drift allowing watermarks to be breached - * 在空闲页的数目少于这个点percpu_drift_mark的时候 - * 当读取和空闲页数一样的内存页时,系统会采取额外的工作, - * 防止单CPU页数漂移,从而导致水印被破坏。 - */ - unsigned long percpu_drift_mark; - -#if defined CONFIG_COMPACTION || defined CONFIG_CMA - /* pfn where compaction free scanner should start */ - unsigned long compact_cached_free_pfn; - /* pfn where async and sync compaction migration scanner should start */ - unsigned long compact_cached_migrate_pfn[2]; -#endif - -#ifdef CONFIG_COMPACTION - /* - * On compaction failure, 1<lock,zone->lru_lock,zone->pageset这些成员使用不同的cache line. | - - - -##4.2 ZONE_PADDING将数据保存在高速缓冲行 -------- - -该结构比较特殊的地方是它由ZONE_PADDING分隔的几个部分. 这是因为堆zone结构的访问非常频繁. 在多处理器系统中, 通常会有不同的CPU试图同时访问结构成员. 因此使用锁可以防止他们彼此干扰, 避免错误和不一致的问题. 由于内核堆该结构的访问非常频繁, 因此会经常性地获取该结构的两个自旋锁zone->lock和zone->lru_lock - - - -那么数据保存在CPU高速缓存中, 那么会处理得更快速. 高速缓冲分为行, 每一行负责不同的内存区. 内核使用ZONE_PADDING宏生成"填充"字段添加到结构中, 以确保每个自旋锁处于自身的缓存行中 - -ZONE_PADDING宏定义在[nclude/linux/mmzone.h?v4.7, line 105](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v4.7#L105) - -```cpp -/* - * zone->lock and zone->lru_lock are two of the hottest locks in the kernel. - * So add a wild amount of padding here to ensure that they fall into separate - * cachelines. There are very few zone structures in the machine, so space - * consumption is not a concern here. - */ -#if defined(CONFIG_SMP) - struct zone_padding - { - char x[0]; - } ____cacheline_internodealigned_in_smp; - #define ZONE_PADDING(name) struct zone_padding name; - -#else - #define ZONE_PADDING(name) - #endif -``` - - -内核还用了____cacheline_internodealigned_in_smp,来实现最优的高速缓存行对其方式. - -该宏定义在[include/linux/cache.h](http://lxr.free-electrons.com/source/include/linux/cache.h?v=4.7#L68) - -```cpp -#if !defined(____cacheline_internodealigned_in_smp) - #if defined(CONFIG_SMP) - #define ____cacheline_internodealigned_in_smp \ - __attribute__((__aligned__(1 << (INTERNODE_CACHE_SHIFT)))) - #else - #define ____cacheline_internodealigned_in_smp - #endif -#endif -``` - -##4.3 水印watermark[NR_WMARK]与kswapd内核线程 -------- - -Zone的管理调度的一些参数watermarks水印, 水存量很小(MIN)进水量,水存量达到一个标准(LOW)减小进水量,当快要满(HIGH)的时候,可能就关闭了进水口 - -WMARK_LOW, WMARK_LOW, WMARK_HIGH就是这个标准 - - -```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]) -``` - -在linux-2.4中, zone结构中使用如下方式表示水印, 参照[include/linux/mmzone.h?v=2.4.37, line 171](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=2.4.37#L171) - -``` c -typedef struct zone_watermarks_s -{ - unsigned long min, low, high; -} zone_watermarks_t; - - -typedef struct zone_struct { - zone_watermarks_t watermarks[MAX_NR_ZONES]; -``` - - -在Linux-2.6.x中标准是直接通过成员pages_min, pages_low and pages_high定义在zone结构体中的, 参照[include/linux/mmzone.h?v=2.6.24, line 214](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=2.6.24#L214) - - -当系统中可用内存很少的时候,系统进程kswapd被唤醒, 开始回收释放page, 水印这些参数(WMARK_MIN, WMARK_LOW, WMARK_HIGH)影响着这个代码的行为 - -每个zone有三个水平标准:watermark[WMARK_MIN], watermark[WMARK_LOW], watermark[WMARK_HIGH],帮助确定zone中内存分配使用的压力状态 - -| 标准 | 描述 | -|:----:|:---:| -| watermark[WMARK_MIN] | 当空闲页面的数量达到page_min所标定的数量的时候, 说明页面数非常紧张, 分配页面的动作和kswapd线程同步运行.
WMARK_MIN所表示的page的数量值,是在内存初始化的过程中调用[free_area_init_core](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5932)中计算的。这个数值是根据zone中的page的数量除以一个>1的系数来确定的。通常是这样初始化的ZoneSizeInPages/12 | -| watermark[WMARK_LOW] | 当空闲页面的数量达到WMARK_LOW所标定的数量的时候,说明页面刚开始紧张, 则kswapd线程将被唤醒,并开始释放回收页面 | -| watermark[WMARK_HIGH] | 当空闲页面的数量达到page_high所标定的数量的时候, 说明内存页面数充足, 不需要回收, kswapd线程将重新休眠,通常这个数值是page_min的3倍 | - -* 如果空闲页多于pages_high = watermark[WMARK_HIGH], 则说明内存页面充足, 内存域的状态是理想的. - -* 如果空闲页的数目低于pages_low = watermark[WMARK_LOW], 则说明内存页面开始紧张, 内核开始将页患处到硬盘. - -* 如果空闲页的数目低于pages_min = watermark[WMARK_MIN], 则内存页面非常紧张, 页回收工作的压力就比较大 - - -##4.3 内存域标志 -------- - -[内存管理域zone_t结构中的flags字段](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v4.7#L475)描述了内存域的当前状态 - - -```cpp -// http://lxr.free-electrons.com/source/include/linux/mmzone.h#L475 -struct zone -{ - /* zone flags, see below */ - unsigned long flags; -} -``` - -它允许使用的标识用[`enum zone_flags`](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v4.7#L525)标识, 该枚举标识定义在[include/linux/mmzone.h?v4.7, line 525](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v4.7#L525), 如下所示 - -```cpp -enum zone_flags -{ - ZONE_RECLAIM_LOCKED, /* prevents concurrent reclaim */ - ZONE_OOM_LOCKED, /* zone is in OOM killer zonelist 内存域可被回收*/ - ZONE_CONGESTED, /* zone has many dirty pages backed by - * a congested BDI - */ - ZONE_DIRTY, /* reclaim scanning has recently found - * many dirty file pages at the tail - * of the LRU. - */ - ZONE_WRITEBACK, /* reclaim scanning has recently found - * many pages under writeback - */ - ZONE_FAIR_DEPLETED, /* fair zone policy batch depleted */ -}; -``` - - -| flag标识 | 描述 | -|:-------:|:-------:| -| ZONE_RECLAIM_LOCKED | 防止并发回收, 在SMP上系统, 多个CPU可能试图并发的回收亿i个内存域. ZONE_RECLAIM_LCOKED标志可防止这种情况: 如果一个CPU在回收某个内存域, 则设置该标识. 这防止了其他CPU的尝试 | -| ZONE_OOM_LOCKED | 用于某种不走运的情况: 如果进程消耗了大量的内存, 致使必要的操作都无法完成, 那么内核会使徒杀死消耗内存最多的进程, 以获取更多的空闲页, 该标志可以放置多个CPU同时进行这种操作 | -| ZONE_CONGESTED | 标识当前区域中有很多脏页 | -| ZONE_DIRTY | 用于标识最近的一次页面扫描中, LRU算法发现了很多脏的页面 | -| ZONE_WRITEBACK | 最近的回收扫描发现有很多页在写回 | -| ZONE_FAIR_DEPLETED | 公平区策略耗尽(没懂) | - - -##4.4 内存域统计信息vm_stat -------- - - -内存域struct zone的vm_stat维护了大量有关该内存域的统计信息. 由于其中维护的大部分信息曲面没有多大意义 - -```cpp -// http://lxr.free-electrons.com/source/include/linux/mmzone.h#L522 -struct zone -{ - atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS]; -} -``` - -vm_stat的统计信息由`enum zone_stat_item`枚举变量标识, 定义在[include/linux/mmzone.h?v=4.7, line 110](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L110) - -```cpp -enum zone_stat_item -{ - /* First 128 byte cacheline (assuming 64 bit words) */ - NR_FREE_PAGES, - NR_ALLOC_BATCH, - NR_LRU_BASE, - NR_INACTIVE_ANON = NR_LRU_BASE, /* must match order of LRU_[IN]ACTIVE */ - NR_ACTIVE_ANON, /* " " " " " */ - NR_INACTIVE_FILE, /* " " " " " */ - NR_ACTIVE_FILE, /* " " " " " */ - NR_UNEVICTABLE, /* " " " " " */ - NR_MLOCK, /* mlock()ed pages found and moved off LRU */ - NR_ANON_PAGES, /* Mapped anonymous pages */ - NR_FILE_MAPPED, /* pagecache pages mapped into pagetables. - only modified from process context */ - NR_FILE_PAGES, - NR_FILE_DIRTY, - NR_WRITEBACK, - NR_SLAB_RECLAIMABLE, - NR_SLAB_UNRECLAIMABLE, - NR_PAGETABLE, /* used for pagetables */ - NR_KERNEL_STACK, - /* Second 128 byte cacheline */ - NR_UNSTABLE_NFS, /* NFS unstable pages */ - NR_BOUNCE, - NR_VMSCAN_WRITE, - NR_VMSCAN_IMMEDIATE, /* Prioritise for reclaim when writeback ends */ - NR_WRITEBACK_TEMP, /* Writeback using temporary buffers */ - NR_ISOLATED_ANON, /* Temporary isolated pages from anon lru */ - NR_ISOLATED_FILE, /* Temporary isolated pages from file lru */ - NR_SHMEM, /* shmem pages (included tmpfs/GEM pages) */ - NR_DIRTIED, /* page dirtyings since bootup */ - NR_WRITTEN, /* page writings since bootup */ - NR_PAGES_SCANNED, /* pages scanned since last reclaim */ -#ifdef CONFIG_NUMA - NUMA_HIT, /* allocated in intended node */ - NUMA_MISS, /* allocated in non intended node */ - NUMA_FOREIGN, /* was intended here, hit elsewhere */ - NUMA_INTERLEAVE_HIT, /* interleaver preferred this zone */ - NUMA_LOCAL, /* allocation from local node */ - NUMA_OTHER, /* allocation from other node */ -#endif - WORKINGSET_REFAULT, - WORKINGSET_ACTIVATE, - WORKINGSET_NODERECLAIM, - NR_ANON_TRANSPARENT_HUGEPAGES, - NR_FREE_CMA_PAGES, - NR_VM_ZONE_STAT_ITEMS -}; -``` - -内核提供了很多方式来获取当前内存域的状态信息, 这些函数大多定义在[include/linux/vmstat.h?v=4.7](http://lxr.free-electrons.com/source/include/linux/vmstat.h?v=4.7) - - - -##4.5 Zone等待队列表(zone wait queue table) -------- - - -struct zone中实现了一个等待队列, 可用于等待某一页的进程, 内核将进程排成一个列队, 等待某些条件. 在条件变成真时, 内核会通知进程恢复工作. - -```c -struct zone -{ - wait_queue_head_t *wait_table; - unsigned long wait_table_hash_nr_entries; - unsigned long wait_table_bits; -} -``` - -| 字段 | 描述 | -|:-----:|:-----:| -| wait_table | 待一个page释放的等待队列哈希表。它会被wait_on_page(),unlock_page()函数使用. 用哈希表,而不用一个等待队列的原因,防止进程长期等待资源 | -| wait_table_hash_nr_entries | 哈希表中的等待队列的数量 | -| wait_table_bits | 等待队列散列表数组大小, wait_table_size == (1 << wait_table_bits) | - - - - -当对一个page做I/O操作的时候,I/O操作需要被锁住,防止不正确的数据被访问。进程在访问page前,wait_on_page_locked函数,使进程加入一个等待队列 - -访问完成后,UnlockPage函数解锁其他进程对page的访问。其他正在等待队列中的进程被唤醒。每个page都可以有一个等待队列,但是太多的分离的等待队列使得花费太多的内存访问周期。替代的解决方法,就是将所有的队列放在struct zone数据结构中 - - -也可以有一种可能,就是struct zone中只有一个队列,但是这就意味着,当一个page unlock的时候,访问这个zone里内存page的所有休眠的进程将都被唤醒,这样就会出现拥堵(thundering herd)的问题。建立一个哈希表管理多个等待队列,能解决这个问题,zone->wait_table就是这个哈希表。哈希表的方法可能还是会造成一些进程不必要的唤醒。但是这种事情发生的机率不是很频繁的。下面这个图就是进程及等待队列的运行关系: - - -等待队列的哈希表的分配和建立在free_area_init_core()函数中进行。哈希表的表项的数量在wait_table_size() 函数中计算,并且保持在zone->wait_table_size成员中。最大4096个等待队列。最小是NoPages / PAGES_PER_WAITQUEUE的2次方,NoPages是zone管理的page的数量,PAGES_PER_WAITQUEUE被定义256。(原文:For smaller tables, the size of the table is the minimum power of 2 required to store NoPages / PAGES PER WAITQUEUE number of queues, where NoPages is the number of pages in the zone and PAGE PER WAITQUEUE is defined to be 256.) -下面这个公式可以用于计算这个值: - -zone->wait_table_bits用于计算:根据page 地址得到需要使用的等待队列在哈希表中的索引的算法因子。page_waitqueue()函数负责返回zone中page所对应等待队列。它用一个基于struct page虚拟地址的简单的乘法哈希算法来确定等待队列的。 -page_waitqueue()函数用GOLDEN_RATIO_PRIME的地址和“右移zone→wait_table_bits一个索引值”的一个乘积来确定等待队列在哈希表中的索引的。 -Zone的初始化 -在kernel page table通过paging_init()函数完全建立起z来以后,zone被初始化。下面章节将描述这个。当然不同的体系结构这个过程肯定也是不一样的,但它们的目的却是相同的:确定什么参数需要传递给free_area_init()函数(对于UMA体系结构)或者free_area_init_node()函数(对于NUMA体系结构)。这里省略掉NUMA体系结构的说明。 -free_area_init()函数的参数: -unsigned long *zones_sizes: 系统中每个zone所管理的page的数量的数组。这个时候,还没能确定zone中那些page是可以分配使用的(free)。这个信息知道boot memory allocator完成之前还无法知道。 -来源: http://www.uml.org.cn/embeded/201208071.asp - - -##4.6 冷热页与Per-CPU上的页面高速缓存 -------- - - -内核经常请求和释放单个页框. 为了提升性能, 每个内存管理区都定义了一个每CPU(Per-CPU)的页面高速缓存. 所有"每CPU高速缓存"包含一些预先分配的页框, 他们被定义满足本地CPU发出的单一内存请求. - -`struct zone`的pageset成员用于实现冷热分配器(hot-n-cold allocator) - -```cpp -struct zone -{ - struct per_cpu_pageset __percpu *pageset; -}; -``` -内核说页面是热的, 意味着页面已经加载到CPU的高速缓存, 与在内存中的页相比, 其数据访问速度更快. 相反, 冷页则不再高速缓存中. 在多处理器系统上每个CPU都有一个或者多个告诉缓存. 各个CPU的管理必须是独立的. - ->尽管内存域可能属于一个特定的NUMA结点, 因而关联到某个特定的CPU。 但其他CPU的告诉缓存仍然可以包含该内存域中的页面. 最终的效果是, 每个处理器都可以访问系统中的所有页, 尽管速度不同. 因而, 特定于内存域的数据结构不仅要考虑到所属NUMA结点相关的CPU, 还必须照顾到系统中其他的CPU. - - -pageset是一个指针, 其容量与系统能够容纳的CPU的数目的最大值相同. - - -数组元素类型为per_cpu_pageset, 定义在[include/linux/mmzone.h?v4.7, line 254](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v4.7#L254), 如下所示 - -```cpp -struct per_cpu_pageset { - struct per_cpu_pages pcp; -#ifdef CONFIG_NUMA - s8 expire; -#endif -#ifdef CONFIG_SMP - s8 stat_threshold; - s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS]; -#endif -}; -``` - -该结构由一个per_cpu_pages pcp变量组成, 该数据结构定义如下, 位于[include/linux/mmzone.h?v4.7, line 245](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v4.7#L245) - - - -```cpp -struct per_cpu_pages { - int count; /* number of pages in the list 列表中的页数 */ - int high; /* high watermark, emptying needed 页数上限水印, 在需要的情况清空列表 */ - int batch; /* chunk size for buddy add/remove, 添加/删除多页块的时候, 块的大小 */ - - /* Lists of pages, one per migrate type stored on the pcp-lists 页的链表*/ - struct list_head lists[MIGRATE_PCPTYPES]; -}; -``` - -| 字段 | 描述 | -|:---:|:----:| -| count | 记录了与该列表相关的页的数目 | -| high | 是一个水印. 如果count的值超过了high, 则表明列表中的页太多了 | -| batch | 如果可能, CPU的高速缓存不是用单个页来填充的, 而是欧诺个多个页组成的块, batch作为每次添加/删除页时多个页组成的块大小的一个参考值 | -| list | 一个双链表, 保存了当前CPU的冷页或热页, 可使用内核的标准方法处理 | - - -在内核中只有一个子系统会积极的尝试为任何对象维护per-cpu上的list链表, 这个子系统就是slab分配器. - -* struct per_cpu_pageset具有一个字段, 该字段 - -* struct per_cpu_pages则维护了链表中目前已有的一系列页面, 高极值和低极值决定了何时填充该集合或者释放一批页面, 变量决定了一个块中应该分配多少个页面, 并最后决定在页面前的实际链表中分配多少各页面 - - - - -#4.7 内存域的第一个页帧zone_start_pfn -------- - -struct zone中通过zone_start_pfn成员标记了内存管理区的页面地址. - - -然后内核也通过一些全局变量标记了物理内存所在页面的偏移, 这些变量定义在[mm/nobootmem.c?v4.7, line 31](http://lxr.free-electrons.com/source/mm/nobootmem.c?v4.7#L31) - -```cpp -unsigned long max_low_pfn; -unsigned long min_low_pfn; -unsigned long max_pfn; -unsigned long long max_possible_pfn; -``` - -PFN是物理内存以Page为单位的偏移量 - -| 变量 | 描述 | -|:----:|:---:| -| max_low_pfn | x86中,max_low_pfn变量是由find_max_low_pfn函数计算并且初始化的,它被初始化成ZONE_NORMAL的最后一个page的位置。这个位置是kernel直接访问的物理内存, 也是关系到kernel/userspace通过“PAGE_OFFSET宏”把线性地址内存空间分开的内存地址位置 | -| min_low_pfn | 系统可用的第一个pfn是[min_low_pfn变量](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v4.7#L16), 开始与_end标号的后面, 也就是kernel结束的地方.在文件mm/bootmem.c中对这个变量作初始化 -| max_pfn | 系统可用的最后一个PFN是[max_pfn变量](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v4.7#L21), 这个变量的初始化完全依赖与硬件的体系结构. | -| max_possible_pfn | - - - - - - -x86的系统中, find_max_pfn函数通过读取e820表获得最高的page frame的数值, 同样在文件mm/bootmem.c中对这个变量作初始化。e820表是由BIOS创建的 - ->This is the physical memory directly accessible by the kernel and is related to the kernel/userspace split in the linear address space marked by PAGE OFFSET. - -我理解为这段地址kernel可以直接访问,可以通过PAGE_OFFSET宏直接将kernel所用的虚拟地址转换成物理地址的区段。在文件mm/bootmem.c中对这个变量作初始化。在内存比较小的系统中max_pfn和max_low_pfn的值相同 -min_low_pfn, max_pfn和max_low_pfn这3个值,也要用于对高端内存(high memory)的起止位置的计算。在arch/i386/mm/init.c文件中会对类似的highstart_pfn和highend_pfn变量作初始化。这些变量用于对高端内存页面的分配。后面将描述。 - - - -#5 管理区表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中, 一个节点的所有页面都会被初始化. - - - -#总结 -------- - -在linux中,内核也不是对所有物理内存都一视同仁,内核而是把页分为不同的区, 使用区来对具有相似特性的页进行分组. - -Linux必须处理如下两种硬件存在缺陷而引起的内存寻址问题: - -1. 一些硬件只能用某些特定的内存地址来执行DMA - -2. 一些体系结构其内存的物理寻址范围比虚拟寻址范围大的多。这样,就有一些内存不能永久地映射在内核空间上。 - -为了解决这些制约条件,Linux使用了三种区: - -1. ZONE_DMA : 这个区包含的页用来执行DMA操作。 - -2. ZONE_NOMAL : 这个区包含的都是能正常映射的页。 - -3. ZONE_HIGHEM : 这个区包"高端内存",其中的页能不永久地映射到内核地址空间 - -而为了兼容一些设备的热插拔支持以及内存碎片化的处理, 内核也引入一些逻辑上的内存区. - -1. ZONE_MOVABLE : 内核定义了一个伪内存域ZONE_MOVABLE, 在防止物理内存碎片的机制memory migration中需要使用该内存域. 供防止物理内存碎片的极致使用 - -2. ZONE_DEVICE : 为支持热插拔设备而分配的Non Volatile Memory非易失性内存 - -区的实际使用与体系结构是相关的。linux把系统的内存结点划分区, 一个区包含了若干个内存页面, 形成不同的内存池,这样就可以根据用途进行分配了 - -需要说明的是,区的划分没有任何物理意义, 只不过是内核为了管理页而采取的一种逻辑上的分组. 尽管某些分配可能需要从特定的区中获得页, 但这并不是说, 某种用途的内存一定要从对应的区来获取,如果这种可供分配的资源不够用了,内核就会占用其他可用去的内存. - - -下表给出每个区及其在X86上所占的列表 - - -![每个区及其在X86上所占的列表](./images/zone_x86_32.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) | + + + +#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)模型 + +**UMA模型** + +物理存储器被所有处理机均匀共享。所有处理机对所有存储字具有相同的存取时间,这就是为什么称它为均匀存储器存取的原因。每台处理机可以有私用高速缓存,外围设备也以一定形式共享。 + +**NUMA模型** + +NUMA模式下,处理器被划分成多个"节点"(node), 每个节点被分配有的本地存储器空间。 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多。 + + +#1.2 (N)UMA模型中linux内存的机构 +------- + + + +非一致存储器访问(NUMA)模式下 + +* 处理器被划分成多个"节点"(node), 每个节点被分配有的本地存储器空间. 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多 + + +* 内存被分割成多个区域(BANK,也叫"簇"),依据簇与处理器的"距离"不同, 访问不同簇的代码也会不同. 比如,可能把内存的一个簇指派给每个处理器,或则某个簇和设备卡很近,很适合DMA,那么就指派给该设备。因此当前的多数系统会把内存系统分割成2块区域,一块是专门给CPU去访问,一块是给外围设备板卡的DMA去访问 + +>在UMA系统中, 内存就相当于一个只使用一个NUMA节点来管理整个系统的内存. 而内存管理的其他地方则认为他们就是在处理一个(伪)NUMA系统. + + +##1.3 Linux如何描述物理内存 +------- + +Linux把物理内存划分为三个层次来管理 + +| 层次 | 描述 | +|:----:|:----:| +| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | +| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | +| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | + + +##1.4 用pd_data_t描述内存节点node +------- + + +>CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 +> +>系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 + +* 首先, 内存被划分为结点. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. + + +内存中的每个节点都是由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) + + +在分配一个页面时, Linux采用节点局部分配的策略, 从最靠近运行中的CPU的节点分配内存, 由于进程往往是在同一个CPU上运行, 因此从当前节点得到的内存很可能被用到 + + +##1.5 今日内容(内存管理域zone) +------- + +为了支持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, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. + + +下面我们就来详解讲讲内存管理域的内容zone + + +#2 为什么要将内存node分成不同的区域zone +------- + + +NUMA结构下, 每个处理器CPU与一个本地内存直接相连, 而不同处理器之前则通过总线进行进一步的连接, 因此相对于任何一个CPU访问本地内存的速度比访问远程内存的速度要快, 而Linux为了兼容NUMAJ结构, 把物理内存相依照CPU的不同node分成簇, 一个CPU-node对应一个本地内存pgdata_t. + + + +这样已经很好的表示物理内存了, 在一个理想的计算机系统中, 一个页框就是一个内存的分配单元, 可用于任何事情:存放内核数据, 用户数据和缓冲磁盘数据等等. 任何种类的数据页都可以存放在任页框中, 没有任何限制. + + +但是Linux内核又把各个物理内存节点分成个不同的管理区域zone, 这是为什么呢? + + +因为实际的计算机体系结构有硬件的诸多限制, 这限制了页框可以使用的方式. 尤其是, Linux内核必须处理80x86体系结构的两种硬件约束. + +* ISA总线的直接内存存储DMA处理器有一个严格的限制 : 他们只能对RAM的前16MB进行寻址 + +* 在具有大容量RAM的现代32位计算机中, CPU不能直接访问所有的物理地址, 因为线性地址空间太小, 内核不可能直接映射所有物理内存到线性地址空间, 我们会在后面典型架构(x86)上内存区域划分详细讲解x86_32上的内存区域划分 + + +因此Linux内核对不同区域的内存需要采用不同的管理方式和映射方式, 因此内核将物理地址或者成用zone_t表示的不同地址区域 + + + +#3 内存管理区类型zone_type +------- + +前面我们说了由于硬件的一些约束, 低端的一些地址被用于DMA, 而在实际内存大小超过了内核所能使用的现行地址的时候, 一些高地址处的物理地址不能简单持久的直接映射到内核空间. 因此内核将内存的节点node分成了不同的内存区域方便管理和映射. + +Linux使用enum zone_type来标记内核所支持的所有内存区域 + + +##3.1 内存区域类型zone_type +------- + +zone_type结构定义在[include/linux/mmzone.h](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L267), 其基本信息如下所示 + +```c +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_DMA类型的内存区域在物理内存的低端,主要是ISA设备只能用低端的地址做DMA操作。ZONE_NORMAL类型的内存区域直接被内核映射到线性地址空间上面的区域(line address space),ZONE_HIGHMEM将保留给系统使用,是系统中预留的可用内存空间,不能被内核直接映射。 + + +##3.2 不同的内存区域的作用 +------- + +在内存中,每个簇所对应的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)数据结构来描述) + +管理区的类型用zone_type表示, 有如下几种 + +| 管理内存域 | 描述 | +|:---------:|:---:| +| ZONE_DMA | 标记了适合DMA的内存域. 该区域的长度依赖于处理器类型. 这是由于古老的ISA设备强加的边界. 但是为了兼容性, 现代的计算机也可能受此影响 | +| ZONE_DMA32 | 标记了使用32位地址字可寻址, 适合DMA的内存域. 显然, 只有在53位系统中ZONE_DMA32才和ZONE_DMA有区别, 在32位系统中, 本区域是空的, 即长度为0MB, 在Alpha和AMD64系统上, 该内存的长度可能是从0到4GB | +| ZONE_NORMAL | 标记了可直接映射到内存段的普通内存域. 这是在所有体系结构上保证会存在的唯一内存区域, 但无法保证该地址范围对应了实际的物理地址. 例如, 如果AMD64系统只有两2G内存, 那么所有的内存都属于ZONE_DMA32范围, 而ZONE_NORMAL则为空 | +| ZONE_HIGHMEM | 标记了超出内核虚拟地址空间的物理内存段, 因此这段地址不能被内核直接映射 | +| ZONE_MOVABLE | 内核定义了一个伪内存域ZONE_MOVABLE, 在防止物理内存碎片的机制memory migration中需要使用该内存域. 供防止物理内存碎片的极致使用 | +| ZONE_DEVICE | 为支持热插拔设备而分配的Non Volatile Memory非易失性内存 | +| MAX_NR_ZONES | 充当结束标记, 在内核中想要迭代系统中所有内存域, 会用到该常亮 | + +根据编译时候的配置, 可能无需考虑某些内存域. 例如在64位系统中, 并不需要高端内存, 因为AM64的linux采用4级页表,支持的最大物理内存为64TB, 对于虚拟地址空间的划分,将0x0000,0000,0000,0000 – 0x0000,7fff,ffff,f000这128T地址用于用户空间;而0xffff,8000,0000,0000以上的128T为系统空间地址, 这远大于当前我们系统中的内存空间, 因此所有的物理地址都可以直接映射到内核中, 不需要高端内存的特殊映射. 可以参见[Documentation/x86/x86_64/mm.txt](https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt) + ` + + +ZONE_MOVABLE和ZONE_DEVICE其实是和其他的ZONE的用途有异, + +* ZONE_MOVABLE在防止物理内存碎片的机制中需要使用该内存区域, + +* ZONE_DEVICE笔者也第一次知道了,理解有错的话欢迎大家批评指正, 这个应该是为支持热插拔设备而分配的Non Volatile Memory非易失性内存, + + +>关于ZONE_DEVICE, 具体的信息可以参见[ATCH v2 3/9] mm: ZONE_DEVICE for "device memory"](https://lkml.org/lkml/2015/8/25/844) +> +>While pmem is usable as a block device or via DAX mappings to userspace +there are several usage scenarios that can not target pmem due to its +lack of struct page coverage. In preparation for "hot plugging" pmem +into the vmemmap add ZONE_DEVICE as a new zone to tag these pages +separately from the ones that are subject to standard page allocations. +Importantly "device memory" can be removed at will by userspace +unbinding the driver of the device. + + +##3.3 典型架构(x86)上内存区域划分 +------- + + +对于x86机器,管理区(内存区域)类型如下分布 + +| 类型 | 区域 | +| :------- | ----: | +| ZONE_DMA | 0~15MB | +| ZONE_NORMAL | 16MB~895MB | +| ZONE_HIGHMEM | 896MB~物理内存结束 | + +而由于32位系统中, Linux内核虚拟地址空间只有1G, 而0~895M这个986MB被用于DMA和直接映射, 剩余的物理内存被成为高端内存. 那内核是如何借助剩余128MB高端内存地址空间是如何实现访问可以所有物理内存? + +当内核想访问高于896MB物理地址内存时,从0xF8000000 ~ 0xFFFFFFFF地址空间范围内找一段相应大小空闲的逻辑地址空间,借用一会。借用这段逻辑地址空间,建立映射到想访问的那段物理内存(即填充内核PTE页面表),临时用一会,用完后归还。这样别人也可以借用这段地址空间访问其他物理内存,实现了使用有限的地址空间,访问所有所有物理内存 + +>关于高端内存的内容, 我们后面会专门抽出一章进行讲解 + +因此, 传统和X86_32位系统中, 前16M划分给ZONE_DMA, 该区域包含的页框可以由老式的基于ISAS的设备通过DMA使用"直接内存访问(DMA)", ZONE_DMA和ZONE_NORMAL区域包含了内存的常规页框, 通过把他们线性的映射到现行地址的第4个GB, 内核就可以直接进行访问, 相反ZONE_HIGHME包含的内存页不能由内核直接访问, 尽管他们也线性地映射到了现行地址空间的第4个GB. 在64位体系结构中, 线性地址空间的大小远远好过了系统的实际物理地址, 内核可知直接将所有的物理内存映射到线性地址空间, 因此64位体系结构上ZONE_HIGHMEM区域总是空的. + + +#4 管理区结构zone_t +------- + +>一个管理区(zone)由[`struct zone`](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L326)结构体来描述(linux-3.8~目前linux4.5),而在linux-2.4.37之前的内核中是用[`struct zone_struct `](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=2.4.37#L47)数据结构来描述), 他们都通过typedef被重定义为zone_t类型 + +zone对象用于跟踪诸如页面使用情况的统计数, 空闲区域信息和锁信息 + +>里面保存着内存使用状态信息,如page使用统计, 未使用的内存区域,互斥访问的锁(LOCKS)等. + + +##4.1 struct zone管理域数据结构 +------- + +`struct zone`在`linux/mmzone.h`中定义, 在linux-4.7的内核中可以使用[include/linux/mmzone.h](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L324)来查看其定义 + +```cpp +struct zone +{ + /* Read-mostly fields */ + + /* zone watermarks, access with *_wmark_pages(zone) macros */ + unsigned long watermark[NR_WMARK]; + + unsigned long nr_reserved_highatomic; + + /* + * We don't know if the memory that we're going to allocate will be + * freeable or/and it will be released eventually, so to avoid totally + * wasting several GB of ram we must reserve some of the lower zone + * memory (otherwise we risk to run OOM on the lower zones despite + * there being tons of freeable ram on the higher zones). This array is + * recalculated at runtime if the sysctl_lowmem_reserve_ratio sysctl + * changes. + * 分别为各种内存域指定了若干页 + * 用于一些无论如何都不能失败的关键性内存分配。 + */ + long lowmem_reserve[MAX_NR_ZONES]; + +#ifdef CONFIG_NUMA + int node; +#endif + + /* + * The target ratio of ACTIVE_ANON to INACTIVE_ANON pages on + * this zone's LRU. Maintained by the pageout code. + * 不活动页的比例, + * 接着是一些很少使用或者大部分情况下是只读的字段: + * wait_table wait_table_hash_nr_entries wait_table_bits + * 形成等待列队,可以等待某一页可供进程使用 */ + unsigned int inactive_ratio; + + /* 指向这个zone所在的pglist_data对象 */ + struct pglist_data *zone_pgdat; + /*/这个数组用于实现每个CPU的热/冷页帧列表。内核使用这些列表来保存可用于满足实现的“新鲜”页。但冷热页帧对应的高速缓存状态不同:有些页帧很可能在高速缓存中,因此可以快速访问,故称之为热的;未缓存的页帧与此相对,称之为冷的。*/ + struct per_cpu_pageset __percpu *pageset; + + /* + * This is a per-zone reserve of pages that are not available + * to userspace allocations. + * 每个区域保留的不能被用户空间分配的页面数目 + */ + unsigned long totalreserve_pages; + +#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 */ + +#ifdef CONFIG_NUMA + /* + * zone reclaim becomes active if more unmapped pages exist. + */ + unsigned long min_unmapped_pages; + unsigned long min_slab_pages; +#endif /* CONFIG_NUMA */ + + /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT + * 只内存域的第一个页帧 */ + unsigned long zone_start_pfn; + + /* + * spanned_pages is the total pages spanned by the zone, including + * holes, which is calculated as: + * spanned_pages = zone_end_pfn - zone_start_pfn; + * + * present_pages is physical pages existing within the zone, which + * is calculated as: + * present_pages = spanned_pages - absent_pages(pages in holes); + * + * managed_pages is present pages managed by the buddy system, which + * is calculated as (reserved_pages includes pages allocated by the + * bootmem allocator): + * managed_pages = present_pages - reserved_pages; + * + * So present_pages may be used by memory hotplug or memory power + * management logic to figure out unmanaged pages by checking + * (present_pages - managed_pages). And managed_pages should be used + * by page allocator and vm scanner to calculate all kinds of watermarks + * and thresholds. + * + * Locking rules: + * + * zone_start_pfn and spanned_pages are protected by span_seqlock. + * It is a seqlock because it has to be read outside of zone->lock, + * and it is done in the main allocator path. But, it is written + * quite infrequently. + * + * The span_seq lock is declared along with zone->lock because it is + * frequently read in proximity to zone->lock. It's good to + * give them a chance of being in the same cacheline. + * + * Write access to present_pages at runtime should be protected by + * mem_hotplug_begin/end(). Any reader who can't tolerant drift of + * present_pages should get_online_mems() to get a stable value. + * + * Read access to managed_pages should be safe because it's unsigned + * long. Write access to zone->managed_pages and totalram_pages are + * protected by managed_page_count_lock at runtime. Idealy only + * adjust_managed_page_count() should be used instead of directly + * touching zone->managed_pages and totalram_pages. + */ + unsigned long managed_pages; + unsigned long spanned_pages; /* 总页数,包含空洞 */ + unsigned long present_pages; /* 可用页数,不包哈空洞 */ + + /* 指向管理区的传统名字, "DMA", "NROMAL"或"HIGHMEM" */ + const char *name; + +#ifdef CONFIG_MEMORY_ISOLATION + /* + * Number of isolated pageblock. It is used to solve incorrect + * freepage counting problem due to racy retrieving migratetype + * of pageblock. Protected by zone->lock. + */ + unsigned long nr_isolate_pageblock; +#endif + +#ifdef CONFIG_MEMORY_HOTPLUG + /* see spanned/present_pages for more description */ + seqlock_t span_seqlock; +#endif + + /* + * wait_table -- the array holding the hash table + * wait_table_hash_nr_entries -- the size of the hash table array + * wait_table_bits -- wait_table_size == (1 << wait_table_bits) + * + * The purpose of all these is to keep track of the people + * waiting for a page to become available and make them + * runnable again when possible. The trouble is that this + * consumes a lot of space, especially when so few things + * wait on pages at a given time. So instead of using + * per-page waitqueues, we use a waitqueue hash table. + * + * The bucket discipline is to sleep on the same queue when + * colliding and wake all in that wait queue when removing. + * When something wakes, it must check to be sure its page is + * truly available, a la thundering herd. The cost of a + * collision is great, but given the expected load of the + * table, they should be so rare as to be outweighed by the + * benefits from the saved space. + * + * __wait_on_page_locked() and unlock_page() in mm/filemap.c, are the + * primary users of these fields, and in mm/page_alloc.c + * free_area_init_core() performs the initialization of them. + */ + /* 进程等待队列的散列表, 这些进程正在等待管理区中的某页 */ + wait_queue_head_t *wait_table; + /* 等待队列散列表中的调度实体数目 */ + unsigned long wait_table_hash_nr_entries; + /* 等待队列散列表数组大小, 值为2^order */ + unsigned long wait_table_bits; + + ZONE_PADDING(_pad1_) + + /* free areas of different sizes + 页面使用状态的信息,以每个bit标识对应的page是否可以分配 + 是用于伙伴系统的,每个数组元素指向对应阶也表的数组开头 + 以下是供页帧回收扫描器(page reclaim scanner)访问的字段 + scanner会跟据页帧的活动情况对内存域中使用的页进行编目 + 如果页帧被频繁访问,则是活动的,相反则是不活动的, + 在需要换出页帧时,这样的信息是很重要的: */ + struct free_area free_area[MAX_ORDER]; + + /* zone flags, see below 描述当前内存的状态, 参见下面的enum zone_flags结构 */ + unsigned long flags; + + /* Write-intensive fields used from the page allocator, 保存该描述符的自旋锁 */ + spinlock_t lock; + + ZONE_PADDING(_pad2_) + + /* Write-intensive fields used by page reclaim */ + + /* Fields commonly accessed by the page reclaim scanner */ + spinlock_t lru_lock; /* LRU(最近最少使用算法)活动以及非活动链表使用的自旋锁 */ + struct lruvec lruvec; + + /* + * When free pages are below this point, additional steps are taken + * when reading the number of free pages to avoid per-cpu counter + * drift allowing watermarks to be breached + * 在空闲页的数目少于这个点percpu_drift_mark的时候 + * 当读取和空闲页数一样的内存页时,系统会采取额外的工作, + * 防止单CPU页数漂移,从而导致水印被破坏。 + */ + unsigned long percpu_drift_mark; + +#if defined CONFIG_COMPACTION || defined CONFIG_CMA + /* pfn where compaction free scanner should start */ + unsigned long compact_cached_free_pfn; + /* pfn where async and sync compaction migration scanner should start */ + unsigned long compact_cached_migrate_pfn[2]; +#endif + +#ifdef CONFIG_COMPACTION + /* + * On compaction failure, 1<lock,zone->lru_lock,zone->pageset这些成员使用不同的cache line. | + + + +##4.2 ZONE_PADDING将数据保存在高速缓冲行 +------- + +该结构比较特殊的地方是它由ZONE_PADDING分隔的几个部分. 这是因为堆zone结构的访问非常频繁. 在多处理器系统中, 通常会有不同的CPU试图同时访问结构成员. 因此使用锁可以防止他们彼此干扰, 避免错误和不一致的问题. 由于内核堆该结构的访问非常频繁, 因此会经常性地获取该结构的两个自旋锁zone->lock和zone->lru_lock + + + +那么数据保存在CPU高速缓存中, 那么会处理得更快速. 高速缓冲分为行, 每一行负责不同的内存区. 内核使用ZONE_PADDING宏生成"填充"字段添加到结构中, 以确保每个自旋锁处于自身的缓存行中 + +ZONE_PADDING宏定义在[nclude/linux/mmzone.h?v4.7, line 105](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v4.7#L105) + +```cpp +/* + * zone->lock and zone->lru_lock are two of the hottest locks in the kernel. + * So add a wild amount of padding here to ensure that they fall into separate + * cachelines. There are very few zone structures in the machine, so space + * consumption is not a concern here. + */ +#if defined(CONFIG_SMP) + struct zone_padding + { + char x[0]; + } ____cacheline_internodealigned_in_smp; + #define ZONE_PADDING(name) struct zone_padding name; + +#else + #define ZONE_PADDING(name) + #endif +``` + + +内核还用了____cacheline_internodealigned_in_smp,来实现最优的高速缓存行对其方式. + +该宏定义在[include/linux/cache.h](http://lxr.free-electrons.com/source/include/linux/cache.h?v=4.7#L68) + +```cpp +#if !defined(____cacheline_internodealigned_in_smp) + #if defined(CONFIG_SMP) + #define ____cacheline_internodealigned_in_smp \ + __attribute__((__aligned__(1 << (INTERNODE_CACHE_SHIFT)))) + #else + #define ____cacheline_internodealigned_in_smp + #endif +#endif +``` + +##4.3 水印watermark[NR_WMARK]与kswapd内核线程 +------- + +Zone的管理调度的一些参数watermarks水印, 水存量很小(MIN)进水量,水存量达到一个标准(LOW)减小进水量,当快要满(HIGH)的时候,可能就关闭了进水口 + +WMARK_LOW, WMARK_LOW, WMARK_HIGH就是这个标准 + + +```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]) +``` + +在linux-2.4中, zone结构中使用如下方式表示水印, 参照[include/linux/mmzone.h?v=2.4.37, line 171](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=2.4.37#L171) + +``` c +typedef struct zone_watermarks_s +{ + unsigned long min, low, high; +} zone_watermarks_t; + + +typedef struct zone_struct { + zone_watermarks_t watermarks[MAX_NR_ZONES]; +``` + + +在Linux-2.6.x中标准是直接通过成员pages_min, pages_low and pages_high定义在zone结构体中的, 参照[include/linux/mmzone.h?v=2.6.24, line 214](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=2.6.24#L214) + + +当系统中可用内存很少的时候,系统进程kswapd被唤醒, 开始回收释放page, 水印这些参数(WMARK_MIN, WMARK_LOW, WMARK_HIGH)影响着这个代码的行为 + +每个zone有三个水平标准:watermark[WMARK_MIN], watermark[WMARK_LOW], watermark[WMARK_HIGH],帮助确定zone中内存分配使用的压力状态 + +| 标准 | 描述 | +|:----:|:---:| +| watermark[WMARK_MIN] | 当空闲页面的数量达到page_min所标定的数量的时候, 说明页面数非常紧张, 分配页面的动作和kswapd线程同步运行.
WMARK_MIN所表示的page的数量值,是在内存初始化的过程中调用[free_area_init_core](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5932)中计算的。这个数值是根据zone中的page的数量除以一个>1的系数来确定的。通常是这样初始化的ZoneSizeInPages/12 | +| watermark[WMARK_LOW] | 当空闲页面的数量达到WMARK_LOW所标定的数量的时候,说明页面刚开始紧张, 则kswapd线程将被唤醒,并开始释放回收页面 | +| watermark[WMARK_HIGH] | 当空闲页面的数量达到page_high所标定的数量的时候, 说明内存页面数充足, 不需要回收, kswapd线程将重新休眠,通常这个数值是page_min的3倍 | + +* 如果空闲页多于pages_high = watermark[WMARK_HIGH], 则说明内存页面充足, 内存域的状态是理想的. + +* 如果空闲页的数目低于pages_low = watermark[WMARK_LOW], 则说明内存页面开始紧张, 内核开始将页患处到硬盘. + +* 如果空闲页的数目低于pages_min = watermark[WMARK_MIN], 则内存页面非常紧张, 页回收工作的压力就比较大 + + +##4.3 内存域标志 +------- + +[内存管理域zone_t结构中的flags字段](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v4.7#L475)描述了内存域的当前状态 + + +```cpp +// http://lxr.free-electrons.com/source/include/linux/mmzone.h#L475 +struct zone +{ + /* zone flags, see below */ + unsigned long flags; +} +``` + +它允许使用的标识用[`enum zone_flags`](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v4.7#L525)标识, 该枚举标识定义在[include/linux/mmzone.h?v4.7, line 525](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v4.7#L525), 如下所示 + +```cpp +enum zone_flags +{ + ZONE_RECLAIM_LOCKED, /* prevents concurrent reclaim */ + ZONE_OOM_LOCKED, /* zone is in OOM killer zonelist 内存域可被回收*/ + ZONE_CONGESTED, /* zone has many dirty pages backed by + * a congested BDI + */ + ZONE_DIRTY, /* reclaim scanning has recently found + * many dirty file pages at the tail + * of the LRU. + */ + ZONE_WRITEBACK, /* reclaim scanning has recently found + * many pages under writeback + */ + ZONE_FAIR_DEPLETED, /* fair zone policy batch depleted */ +}; +``` + + +| flag标识 | 描述 | +|:-------:|:-------:| +| ZONE_RECLAIM_LOCKED | 防止并发回收, 在SMP上系统, 多个CPU可能试图并发的回收亿i个内存域. ZONE_RECLAIM_LCOKED标志可防止这种情况: 如果一个CPU在回收某个内存域, 则设置该标识. 这防止了其他CPU的尝试 | +| ZONE_OOM_LOCKED | 用于某种不走运的情况: 如果进程消耗了大量的内存, 致使必要的操作都无法完成, 那么内核会使徒杀死消耗内存最多的进程, 以获取更多的空闲页, 该标志可以放置多个CPU同时进行这种操作 | +| ZONE_CONGESTED | 标识当前区域中有很多脏页 | +| ZONE_DIRTY | 用于标识最近的一次页面扫描中, LRU算法发现了很多脏的页面 | +| ZONE_WRITEBACK | 最近的回收扫描发现有很多页在写回 | +| ZONE_FAIR_DEPLETED | 公平区策略耗尽(没懂) | + + +##4.4 内存域统计信息vm_stat +------- + + +内存域struct zone的vm_stat维护了大量有关该内存域的统计信息. 由于其中维护的大部分信息曲面没有多大意义 + +```cpp +// http://lxr.free-electrons.com/source/include/linux/mmzone.h#L522 +struct zone +{ + atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS]; +} +``` + +vm_stat的统计信息由`enum zone_stat_item`枚举变量标识, 定义在[include/linux/mmzone.h?v=4.7, line 110](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L110) + +```cpp +enum zone_stat_item +{ + /* First 128 byte cacheline (assuming 64 bit words) */ + NR_FREE_PAGES, + NR_ALLOC_BATCH, + NR_LRU_BASE, + NR_INACTIVE_ANON = NR_LRU_BASE, /* must match order of LRU_[IN]ACTIVE */ + NR_ACTIVE_ANON, /* " " " " " */ + NR_INACTIVE_FILE, /* " " " " " */ + NR_ACTIVE_FILE, /* " " " " " */ + NR_UNEVICTABLE, /* " " " " " */ + NR_MLOCK, /* mlock()ed pages found and moved off LRU */ + NR_ANON_PAGES, /* Mapped anonymous pages */ + NR_FILE_MAPPED, /* pagecache pages mapped into pagetables. + only modified from process context */ + NR_FILE_PAGES, + NR_FILE_DIRTY, + NR_WRITEBACK, + NR_SLAB_RECLAIMABLE, + NR_SLAB_UNRECLAIMABLE, + NR_PAGETABLE, /* used for pagetables */ + NR_KERNEL_STACK, + /* Second 128 byte cacheline */ + NR_UNSTABLE_NFS, /* NFS unstable pages */ + NR_BOUNCE, + NR_VMSCAN_WRITE, + NR_VMSCAN_IMMEDIATE, /* Prioritise for reclaim when writeback ends */ + NR_WRITEBACK_TEMP, /* Writeback using temporary buffers */ + NR_ISOLATED_ANON, /* Temporary isolated pages from anon lru */ + NR_ISOLATED_FILE, /* Temporary isolated pages from file lru */ + NR_SHMEM, /* shmem pages (included tmpfs/GEM pages) */ + NR_DIRTIED, /* page dirtyings since bootup */ + NR_WRITTEN, /* page writings since bootup */ + NR_PAGES_SCANNED, /* pages scanned since last reclaim */ +#ifdef CONFIG_NUMA + NUMA_HIT, /* allocated in intended node */ + NUMA_MISS, /* allocated in non intended node */ + NUMA_FOREIGN, /* was intended here, hit elsewhere */ + NUMA_INTERLEAVE_HIT, /* interleaver preferred this zone */ + NUMA_LOCAL, /* allocation from local node */ + NUMA_OTHER, /* allocation from other node */ +#endif + WORKINGSET_REFAULT, + WORKINGSET_ACTIVATE, + WORKINGSET_NODERECLAIM, + NR_ANON_TRANSPARENT_HUGEPAGES, + NR_FREE_CMA_PAGES, + NR_VM_ZONE_STAT_ITEMS +}; +``` + +内核提供了很多方式来获取当前内存域的状态信息, 这些函数大多定义在[include/linux/vmstat.h?v=4.7](http://lxr.free-electrons.com/source/include/linux/vmstat.h?v=4.7) + + + +##4.5 Zone等待队列表(zone wait queue table) +------- + + +struct zone中实现了一个等待队列, 可用于等待某一页的进程, 内核将进程排成一个列队, 等待某些条件. 在条件变成真时, 内核会通知进程恢复工作. + +```c +struct zone +{ + wait_queue_head_t *wait_table; + unsigned long wait_table_hash_nr_entries; + unsigned long wait_table_bits; +} +``` + +| 字段 | 描述 | +|:-----:|:-----:| +| wait_table | 待一个page释放的等待队列哈希表。它会被wait_on_page(),unlock_page()函数使用. 用哈希表,而不用一个等待队列的原因,防止进程长期等待资源 | +| wait_table_hash_nr_entries | 哈希表中的等待队列的数量 | +| wait_table_bits | 等待队列散列表数组大小, wait_table_size == (1 << wait_table_bits) | + + + + +当对一个page做I/O操作的时候,I/O操作需要被锁住,防止不正确的数据被访问。进程在访问page前,wait_on_page_locked函数,使进程加入一个等待队列 + +访问完成后,UnlockPage函数解锁其他进程对page的访问。其他正在等待队列中的进程被唤醒。每个page都可以有一个等待队列,但是太多的分离的等待队列使得花费太多的内存访问周期。替代的解决方法,就是将所有的队列放在struct zone数据结构中 + + +也可以有一种可能,就是struct zone中只有一个队列,但是这就意味着,当一个page unlock的时候,访问这个zone里内存page的所有休眠的进程将都被唤醒,这样就会出现拥堵(thundering herd)的问题。建立一个哈希表管理多个等待队列,能解决这个问题,zone->wait_table就是这个哈希表。哈希表的方法可能还是会造成一些进程不必要的唤醒。但是这种事情发生的机率不是很频繁的。下面这个图就是进程及等待队列的运行关系: + + +等待队列的哈希表的分配和建立在free_area_init_core()函数中进行。哈希表的表项的数量在wait_table_size() 函数中计算,并且保持在zone->wait_table_size成员中。最大4096个等待队列。最小是NoPages / PAGES_PER_WAITQUEUE的2次方,NoPages是zone管理的page的数量,PAGES_PER_WAITQUEUE被定义256。(原文:For smaller tables, the size of the table is the minimum power of 2 required to store NoPages / PAGES PER WAITQUEUE number of queues, where NoPages is the number of pages in the zone and PAGE PER WAITQUEUE is defined to be 256.) +下面这个公式可以用于计算这个值: + +zone->wait_table_bits用于计算:根据page 地址得到需要使用的等待队列在哈希表中的索引的算法因子。page_waitqueue()函数负责返回zone中page所对应等待队列。它用一个基于struct page虚拟地址的简单的乘法哈希算法来确定等待队列的。 +page_waitqueue()函数用GOLDEN_RATIO_PRIME的地址和“右移zone→wait_table_bits一个索引值”的一个乘积来确定等待队列在哈希表中的索引的。 +Zone的初始化 +在kernel page table通过paging_init()函数完全建立起z来以后,zone被初始化。下面章节将描述这个。当然不同的体系结构这个过程肯定也是不一样的,但它们的目的却是相同的:确定什么参数需要传递给free_area_init()函数(对于UMA体系结构)或者free_area_init_node()函数(对于NUMA体系结构)。这里省略掉NUMA体系结构的说明。 +free_area_init()函数的参数: +unsigned long *zones_sizes: 系统中每个zone所管理的page的数量的数组。这个时候,还没能确定zone中那些page是可以分配使用的(free)。这个信息知道boot memory allocator完成之前还无法知道。 +来源: http://www.uml.org.cn/embeded/201208071.asp + + +##4.6 冷热页与Per-CPU上的页面高速缓存 +------- + + +内核经常请求和释放单个页框. 为了提升性能, 每个内存管理区都定义了一个每CPU(Per-CPU)的页面高速缓存. 所有"每CPU高速缓存"包含一些预先分配的页框, 他们被定义满足本地CPU发出的单一内存请求. + +`struct zone`的pageset成员用于实现冷热分配器(hot-n-cold allocator) + +```cpp +struct zone +{ + struct per_cpu_pageset __percpu *pageset; +}; +``` +内核说页面是热的, 意味着页面已经加载到CPU的高速缓存, 与在内存中的页相比, 其数据访问速度更快. 相反, 冷页则不再高速缓存中. 在多处理器系统上每个CPU都有一个或者多个告诉缓存. 各个CPU的管理必须是独立的. + +>尽管内存域可能属于一个特定的NUMA结点, 因而关联到某个特定的CPU。 但其他CPU的告诉缓存仍然可以包含该内存域中的页面. 最终的效果是, 每个处理器都可以访问系统中的所有页, 尽管速度不同. 因而, 特定于内存域的数据结构不仅要考虑到所属NUMA结点相关的CPU, 还必须照顾到系统中其他的CPU. + + +pageset是一个指针, 其容量与系统能够容纳的CPU的数目的最大值相同. + + +数组元素类型为per_cpu_pageset, 定义在[include/linux/mmzone.h?v4.7, line 254](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v4.7#L254), 如下所示 + +```cpp +struct per_cpu_pageset { + struct per_cpu_pages pcp; +#ifdef CONFIG_NUMA + s8 expire; +#endif +#ifdef CONFIG_SMP + s8 stat_threshold; + s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS]; +#endif +}; +``` + +该结构由一个per_cpu_pages pcp变量组成, 该数据结构定义如下, 位于[include/linux/mmzone.h?v4.7, line 245](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v4.7#L245) + + + +```cpp +struct per_cpu_pages { + int count; /* number of pages in the list 列表中的页数 */ + int high; /* high watermark, emptying needed 页数上限水印, 在需要的情况清空列表 */ + int batch; /* chunk size for buddy add/remove, 添加/删除多页块的时候, 块的大小 */ + + /* Lists of pages, one per migrate type stored on the pcp-lists 页的链表*/ + struct list_head lists[MIGRATE_PCPTYPES]; +}; +``` + +| 字段 | 描述 | +|:---:|:----:| +| count | 记录了与该列表相关的页的数目 | +| high | 是一个水印. 如果count的值超过了high, 则表明列表中的页太多了 | +| batch | 如果可能, CPU的高速缓存不是用单个页来填充的, 而是欧诺个多个页组成的块, batch作为每次添加/删除页时多个页组成的块大小的一个参考值 | +| list | 一个双链表, 保存了当前CPU的冷页或热页, 可使用内核的标准方法处理 | + + +在内核中只有一个子系统会积极的尝试为任何对象维护per-cpu上的list链表, 这个子系统就是slab分配器. + +* struct per_cpu_pageset具有一个字段, 该字段 + +* struct per_cpu_pages则维护了链表中目前已有的一系列页面, 高极值和低极值决定了何时填充该集合或者释放一批页面, 变量决定了一个块中应该分配多少个页面, 并最后决定在页面前的实际链表中分配多少各页面 + + + + +#4.7 内存域的第一个页帧zone_start_pfn +------- + +struct zone中通过zone_start_pfn成员标记了内存管理区的页面地址. + + +然后内核也通过一些全局变量标记了物理内存所在页面的偏移, 这些变量定义在[mm/nobootmem.c?v4.7, line 31](http://lxr.free-electrons.com/source/mm/nobootmem.c?v4.7#L31) + +```cpp +unsigned long max_low_pfn; +unsigned long min_low_pfn; +unsigned long max_pfn; +unsigned long long max_possible_pfn; +``` + +PFN是物理内存以Page为单位的偏移量 + +| 变量 | 描述 | +|:----:|:---:| +| max_low_pfn | x86中,max_low_pfn变量是由find_max_low_pfn函数计算并且初始化的,它被初始化成ZONE_NORMAL的最后一个page的位置。这个位置是kernel直接访问的物理内存, 也是关系到kernel/userspace通过“PAGE_OFFSET宏”把线性地址内存空间分开的内存地址位置 | +| min_low_pfn | 系统可用的第一个pfn是[min_low_pfn变量](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v4.7#L16), 开始与_end标号的后面, 也就是kernel结束的地方.在文件mm/bootmem.c中对这个变量作初始化 +| max_pfn | 系统可用的最后一个PFN是[max_pfn变量](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v4.7#L21), 这个变量的初始化完全依赖与硬件的体系结构. | +| max_possible_pfn | + + + + + + +x86的系统中, find_max_pfn函数通过读取e820表获得最高的page frame的数值, 同样在文件mm/bootmem.c中对这个变量作初始化。e820表是由BIOS创建的 + +>This is the physical memory directly accessible by the kernel and is related to the kernel/userspace split in the linear address space marked by PAGE OFFSET. + +我理解为这段地址kernel可以直接访问,可以通过PAGE_OFFSET宏直接将kernel所用的虚拟地址转换成物理地址的区段。在文件mm/bootmem.c中对这个变量作初始化。在内存比较小的系统中max_pfn和max_low_pfn的值相同 +min_low_pfn, max_pfn和max_low_pfn这3个值,也要用于对高端内存(high memory)的起止位置的计算。在arch/i386/mm/init.c文件中会对类似的highstart_pfn和highend_pfn变量作初始化。这些变量用于对高端内存页面的分配。后面将描述。 + + + +#5 管理区表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中, 一个节点的所有页面都会被初始化. + + + + +#6 zonelist内存域存储层次 +------- + + + +##6.1 内存域之间的层级结构 +------- + + +当前结点与系统中其他结点的内存域之前存在一种等级次序 + + +我们考虑一个例子, 其中内核想要分配高端内存. + +1. 它首先企图在当前结点的高端内存域找到一个大小适当的空闲段. 如果失败, 则查看该结点的普通内存域. 如果还失败, 则试图在该结点的DMA内存域执行分配. + +2. 如果在3个本地内存域都无法找到空闲内存, 则查看其他结点. 在这种情况下, 备 +选结点应该尽可能靠近主结点, 以最小化由于访问非本地内存引起的性能损失. + +内核定义了内存的一个层次结构, 首先试图分配"廉价的"内存. 如果失败, 则根据访问速度和容量, 逐渐尝试分配"更昂贵的"内存. + + +**高端内存是最廉价的**, 因为内核没有任何部份依赖于从该内存域分配的内存. 如果高端内存域用尽, 对内核没有任何副作用, 这也是优先分配高端内存的原因. + +**其次是普通内存域**, 这种情况有所不同. 许多内核数据结构必须保存在该内存域, 而不能放置到高端内存域. + +因此如果普通内存完全用尽, 那么内核会面临紧急情况. 所以只要高端内存域的内存没有用尽, 都不会从普通内存域分配内存. + +**最昂贵的是DMA内存域**, 因为它用于外设和系统之间的数据传输. 因此从该内存域分配内存是最后一招. + + + +##6.2 zonelist结构 +------- + + +内核还针对当前内存结点的备选结点, 定义了一个等级次序. 这有助于在当前结点所有内存域的内存都用尽时, 确定一个备选结点 + +内核使用[pg_data_t](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L627)中的[zonelist数组](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L629), 来表示所描述的层次结构. + +```cpp +typedef struct pglist_data { + struct zonelist node_zonelists[MAX_ZONELISTS]; + /* ...... */ +}pg_data_t; +``` + +>关于该结构zonelist的所有相关信息定义[include/linux/mmzone.h?v=4.7, line 568](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L568), 我们下面慢慢来讲. + +node_zonelists数组对每种可能的内存域类型, 都配置了一个独立的数组项. + +该数组项的大小MAX_ZONELISTS用一个匿名的枚举常量定义, 定义在[include/linux/mmzone.h?v=4.7, line 571](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L571) + +```cpp +enum +{ + ZONELIST_FALLBACK, /* zonelist with fallback */ +#ifdef CONFIG_NUMA + /* + * The NUMA zonelists are doubled because we need zonelists that + * restrict the allocations to a single node for __GFP_THISNODE. + */ + ZONELIST_NOFALLBACK, /* zonelist without fallback (__GFP_THISNODE) */ +#endif + MAX_ZONELISTS +}; +``` + + +我们会发现在UMA结构下, 数组大小MAX_ZONELISTS = 1, 因为只有一个内存结点, zonelist中只会存储一个`ZONELIST_FALLBACK`类型的结构, 但是NUMA下需要多余的`ZONELIST_NOFALLBACK`用以表示当前结点的信息 + +pg_data_t->node_zonelists数组项用struct zonelis结构体定义, 该结构包含了类型为`struct zoneref`的一个备用列表由于该备用列表必须包括所有结点的所有内存域,因此由MAX_NUMNODES * MAX_NZ_ZONES项组成,外加一个用于标记列表结束的空指针 + +struct zonelist结构的定义在[include/linux/mmzone.h?v=4.7, line 606](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L606) + + +```cpp +/* + * One allocation request operates on a zonelist. A zonelist + * is a list of zones, the first one is the 'goal' of the + * allocation, the other zones are fallback zones, in decreasing + * priority. + * + * To speed the reading of the zonelist, the zonerefs contain the zone index + * of the entry being read. Helper functions to access information given + * a struct zoneref are + * + * zonelist_zone() - Return the struct zone * for an entry in _zonerefs + * zonelist_zone_idx() - Return the index of the zone for an entry + * zonelist_node_idx() - Return the index of the node for an entry + */ +struct zonelist { + struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1]; +}; +``` + +而struct zoneref结构的定义如下[include/linux/mmzone.h?v=4.7, line 583](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L583) + +/* + * This struct contains information about a zone in a zonelist. It is stored + * here to avoid dereferences into large structures and lookups of tables + */ +struct zoneref { + struct zone *zone; /* Pointer to actual zone */ + int zone_idx; /* zone_idx(zoneref->zone) */ +}; + + +##6.3 内存域的排列方式 +------- + + +那么我们内核是如何组织在zonelist中组织内存域的呢? + +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 +#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 | + + +内核就通过通过set_zonelist_order函数设置当前系统的内存域排列方式current_zonelist_order, 其定义依据系统的NUMA结构还是UMA结构有很大的不同. 该函数定义在[mm/page_alloc.c?v=4.7, line 4571](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4571) + + +##6.4 build_all_zonelists初始化内存节点 +------- + +内核通过build_all_zonelists初始化了内存结点的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) + +* 建立备用层次结构的任务委托给build_zonelists, 该函数为每个NUMA结点都创建了相应的数据结构. 它需要指向相关的pg_data_t实例的指针作为参数 + + + + + +#7 总结 +------- + + +在linux中,内核也不是对所有物理内存都一视同仁,内核而是把页分为不同的区, 使用区来对具有相似特性的页进行分组. + +Linux必须处理如下两种硬件存在缺陷而引起的内存寻址问题: + +1. 一些硬件只能用某些特定的内存地址来执行DMA + +2. 一些体系结构其内存的物理寻址范围比虚拟寻址范围大的多。这样,就有一些内存不能永久地映射在内核空间上。 + +为了解决这些制约条件,Linux使用了三种区: + +1. ZONE_DMA : 这个区包含的页用来执行DMA操作。 + +2. ZONE_NOMAL : 这个区包含的都是能正常映射的页。 + +3. ZONE_HIGHEM : 这个区包"高端内存",其中的页能不永久地映射到内核地址空间 + +而为了兼容一些设备的热插拔支持以及内存碎片化的处理, 内核也引入一些逻辑上的内存区. + +1. ZONE_MOVABLE : 内核定义了一个伪内存域ZONE_MOVABLE, 在防止物理内存碎片的机制memory migration中需要使用该内存域. 供防止物理内存碎片的极致使用 + +2. ZONE_DEVICE : 为支持热插拔设备而分配的Non Volatile Memory非易失性内存 + +区的实际使用与体系结构是相关的。linux把系统的内存结点划分区, 一个区包含了若干个内存页面, 形成不同的内存池,这样就可以根据用途进行分配了 + +需要说明的是,区的划分没有任何物理意义, 只不过是内核为了管理页而采取的一种逻辑上的分组. 尽管某些分配可能需要从特定的区中获得页, 但这并不是说, 某种用途的内存一定要从对应的区来获取,如果这种可供分配的资源不够用了,内核就会占用其他可用去的内存. + + +下表给出每个区及其在X86上所占的列表 + + +![每个区及其在X86上所占的列表](./images/zone_x86_32.png) + diff --git a/study/kernel/02-memory/03-initialize/04-init_buddy/README.md b/study/kernel/02-memory/03-initialize/04-init_buddy/README.md index 0b83267..71ba230 100644 --- a/study/kernel/02-memory/03-initialize/04-init_buddy/README.md +++ b/study/kernel/02-memory/03-initialize/04-init_buddy/README.md @@ -1,1437 +1,1556 @@ -初始化内存管理 -======= - - - -| 日期 | 内核版本 | 架构| 作者 | 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)函数来实现的 - - - - -##1.6 今日内容(第二阶段--初始化buddy内存管理) -------- - - -**初始化内存分页机制** - - -在初始化内存的结点和内存区域之前, 内核先通过pagging_init初始化了内核的分页机制, 这样我们的虚拟运行空间就初步建立, 并可以完成物理地址到虚拟地址空间的映射工作. - - -在arm64架构下, 内核在start_kernel()->setup_arch()中通过arm64_memblock_init( )完成了memblock的初始化之后, 接着通过setup_arch()->paging_init()开始初始化分页机制 - - -paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响 - - - -**特定于体系结构的设置** - - -在完成了基础的内存结点和内存域的初始化工作以后, 我们必须克服一些硬件的特殊设置 - - -**建立内存管理的数据结构** - - -对相关数据结构的初始化是从全局启动函数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()函数开始执行 - - -##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()来初始化节点和管理区的一些数据项 - - - -[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来完成内存结点的初始化 - - -##2.4 free_area_init_node(s)初始化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) - - -```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的数据. - - -##2.5 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的数据,并打印了管理区信息. - -该函数定义在[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); -} -``` - -* [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) - - -* [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的核心 - - - -##2.6 free_area_init_core初始化zone -------- - -```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); - } -} -``` - - - - -##2.7 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的初始化 - - -#3 初始化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个本地内存域都无法找到空闲内存,则查看其他结点。这种情况下,备选结点应该尽可能靠近主结点,以最小化访问非本地内存引起的性能损失。 - - -##3.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来设计内存的组织形式 - -##3.2 build_all_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) - - - -```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.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.3.1 zone table -------- - - -前面我们讲解内存管理域时候讲解到, 系统中的所有管理域都存储在一个多维的数组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.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.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.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.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); -``` - -##3.4 build_all_zonelists_init -------- - -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(); -``` - - -###3.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 -``` - -##3.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完成了zonelists的初始化工作 - -```cpp -/* return values int ....just for stop_machine() */ -static int __build_all_zonelists(void *data) -{ - int nid; - int cpu; - pg_data_t *self = data; - -#ifdef CONFIG_NUMA - memset(node_load, 0, sizeof(node_load)); -#endif - - if (self && !node_online(self->node_id)) { - build_zonelists(self); - } - - for_each_online_node(nid) { - pg_data_t *pgdat = NODE_DATA(nid); - - build_zonelists(pgdat); - } - - /* - * Initialize the boot_pagesets that are going to be used - * for bootstrapping processors. The real pagesets for - * each zone will be allocated later when the per cpu - * allocator is available. - * - * boot_pagesets are used also for bootstrapping offline - * cpus if the system is already booted because the pagesets - * are needed to initialize allocators on a specific cpu too. - * F.e. the percpu allocator needs the page allocator which - * needs the percpu allocator in order to allocate its pagesets - * (a chicken-egg dilemma). - */ - for_each_possible_cpu(cpu) { - setup_pageset(&per_cpu(boot_pageset, cpu), 0); - -#ifdef CONFIG_HAVE_MEMORYLESS_NODES - /* - * We now know the "local memory node" for each node-- - * i.e., the node of the first zone in the generic zonelist. - * Set up numa_mem percpu variable for on-line cpus. During - * boot, only the boot cpu should be on-line; we'll init the - * secondary cpus' numa_mem as they come on-line. During - * node/memory hotplug, we'll fixup all on-line cpus. - */ - if (cpu_online(cpu)) - set_cpu_numa_mem(cpu, local_memory_node(cpu_to_node(cpu))); -#endif - } - - return 0; -} -``` - -##3.4.3 build_zonelists初始化每个内存结点的zonelists -------- - -build_zonelists(pg_data_t *pgdat)完成了节点pgdat上zonelists的初始化工作 - - -* 如果设置了设置了ZONELIST_ORDER_NODE内存区域顺序排列, 就调用build_zonelists_in_node_order(pgdat, node)完 -成节点内区域顺序的排列 - - -该函数的任务是在当前处理的结点和系统中其他结点的内存域之间建立一种等级次序。接下来,依据这种次序分配内存,如果在期望的结点内存域中,没有空闲内存,就去查找相邻结点的内存域 - -内核定义了内存的一个层次结构关系, 首先试图分配廉价的内存,如果失败,则根据访问速度和容量,逐渐尝试分配更昂贵的内存. - -高端内存最廉价, 因为内核没有任何部分依赖于从该内存域分配的内存, 如果高端内存用尽, 对内核没有副作用, 所以优先分配高端内存 - -普通内存域的情况有所不同, 许多内核数据结构必须保存在该内存域, 而不能放置到高端内存域, 因此如果普通内存域用尽, 那么内核会面临内存紧张的情况 - -DMA内存域最昂贵,因为它用于外设和系统之间的数据传输。 -举例来讲,如果内核指定想要分配高端内存域。它首先在当前结点的高端内存域寻找适当的空闲内存段,如果失败,则查看该结点的普通内存域,如果还失败,则试图在该结点的DMA内存域分配。如果在3个本地内存域都无法找到空闲内存,则查看其他结点。这种情况下,备选结点应该尽可能靠近主结点,以最小化访问非本地内存引起的性能损失。 - -##3.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)); -} -``` - - - - -#总结 -------- - - - -## 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; - | -``` - -##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]); -``` - -## 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)函数来实现的 + + + + +##1.6 今日内容(第二阶段--初始化buddy内存管理) +------- + + +**初始化内存分页机制** + + +在初始化内存的结点和内存区域之前, 内核先通过pagging_init初始化了内核的分页机制, 这样我们的虚拟运行空间就初步建立, 并可以完成物理地址到虚拟地址空间的映射工作. + + +在arm64架构下, 内核在start_kernel()->setup_arch()中通过arm64_memblock_init( )完成了memblock的初始化之后, 接着通过setup_arch()->paging_init()开始初始化分页机制 + + +paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响 + + + +**特定于体系结构的设置** + + +在完成了基础的内存结点和内存域的初始化工作以后, 我们必须克服一些硬件的特殊设置 + + +**建立内存管理的数据结构** + + +对相关数据结构的初始化是从全局启动函数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()函数开始执行 + + +##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()来初始化节点和管理区的一些数据项 + + + +[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来完成内存结点的初始化 + + +##2.4 free_area_init_node(s)初始化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) + + +```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的数据. + + +##2.5 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的数据,并打印了管理区信息. + +该函数定义在[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); +} +``` + +* [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) + + +* [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的核心 + + + +##2.6 free_area_init_core初始化zone +------- + +```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); + } +} +``` + + + + +##2.7 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的初始化 + + +#3 初始化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个本地内存域都无法找到空闲内存,则查看其他结点。这种情况下,备选结点应该尽可能靠近主结点,以最小化访问非本地内存引起的性能损失。 + + +##3.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来设计内存的组织形式 + +##3.2 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) + + + +```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.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.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.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.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.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.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); +``` + +##3.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(); +``` + + +###3.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 +``` + +##3.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系统调用该函数的次数等同于结点的数目. 每次调用对一个不同结点生成内存域数据 + + +##3.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、不变的备用列表。 + + + +##3.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)); +} +``` + + +#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); + | + |---->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; + | +``` + +##4.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]); +``` + +##4.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/images/build_zonelists.png b/study/kernel/02-memory/03-initialize/images/build_zonelists.png new file mode 100644 index 0000000..6bb6938 Binary files /dev/null and b/study/kernel/02-memory/03-initialize/images/build_zonelists.png differ diff --git a/study/kernel/02-memory/03-initialize/images/build_zonelists_node.png b/study/kernel/02-memory/03-initialize/images/build_zonelists_node.png new file mode 100644 index 0000000..4ccc99d Binary files /dev/null and b/study/kernel/02-memory/03-initialize/images/build_zonelists_node.png differ diff --git a/study/kernel/02-memory/03-initialize/images/i386-setup_memory.jpg b/study/kernel/02-memory/03-initialize/images/i386-setup_memory.jpg index 935c025..7f528c4 100644 Binary files a/study/kernel/02-memory/03-initialize/images/i386-setup_memory.jpg and b/study/kernel/02-memory/03-initialize/images/i386-setup_memory.jpg differ diff --git a/study/kernel/02-memory/03-initialize/images/kernel_space.jpg b/study/kernel/02-memory/03-initialize/images/kernel_space.jpg index 9b93955..912c6ad 100644 Binary files a/study/kernel/02-memory/03-initialize/images/kernel_space.jpg and b/study/kernel/02-memory/03-initialize/images/kernel_space.jpg differ diff --git a/study/kernel/02-memory/04-buddy/01-buddy_system/README.md b/study/kernel/02-memory/04-buddy/01-buddy_system/README.md index 38c6b1d..06ed87e 100644 --- a/study/kernel/02-memory/04-buddy/01-buddy_system/README.md +++ b/study/kernel/02-memory/04-buddy/01-buddy_system/README.md @@ -1,82 +1,125 @@ -服务器体系与共享存储器架构 -======= - -| 日期 | 内核版本 | 架构| 作者 | 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) | - - - - -在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法. - - - -#2 伙伴系统的结构 -------- - - -系统内存中的每个物理内存页(页帧),都对应于一个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页集合的数目,依次类推 | - - -阶是伙伴系统中一个非常重要的术语. 它描述了内存分配的数量单位. 内存块的长度是2order,其中order的范围从0到MAX_ORDER, 参见[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)) -``` - - -该常数通常设置为11,这意味着一次分配可以请求的页数最大是2^11=2048 - -但如果特定于体系结构的代码设置了FORCE_MAX_ZONEORDER配置选项, 该值也可以手工改变 - -例如,IA-64系统上巨大的地址空间可以处理`MAX_ORDER = 18`的情形,而ARM或v850系统则使用更小的值(如8或9). 但这不一定是由计算机支持的内存数量比较小引起的,也可能是内存对齐方式的要求所导致 - -或者可以参考arm体系结构的Kconfig配置文件的描述 - -```cpp -config FORCE_MAX_ZONEORDER -int -default "14" if (ARM64_64K_PAGES && TRANSPARENT_HUGEPAGE) -default "12" if (ARM64_16K_PAGES && TRANSPARENT_HUGEPAGE) -default "11"` -``` - -free_area[]数组中各个元素的索引也解释为阶,用于指定对应链表中的连续内存区包含多少个 -页帧。第0个链表包含的内存区为单页(20=1),第1个链表管理的内存区为两页(21=2),第3个管理 -的内存区为4页,依次类推。 -内存区是如何连接的?内存区中第1页内的链表元素,可用于将内存区维持在链表中。因此,也 -不必引入新的数据结构来管理物理上连续的页,否则这些页不可能在同一内存区中. - -图3-22对此给出 -了图示。 \ No newline at end of file +服务器体系与共享存储器架构 +======= + +| 日期 | 内核版本 | 架构| 作者 | 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) | + + + + +在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法. + + + +#2 伙伴系统的结构 +------- + + +系统内存中的每个物理内存页(页帧),都对应于一个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页集合的数目,依次类推 | + + +阶是伙伴系统中一个非常重要的术语. 它描述了内存分配的数量单位. 内存块的长度是2order,其中order的范围从0到MAX_ORDER, 参见[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)) +``` + +该常数通常设置为11,这意味着一次分配可以请求的页数最大是2^11=2048 + +但如果特定于体系结构的代码设置了`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"` +``` + + + + +free_area[]数组中各个元素的索引也解释为阶, 用于指定对应链表中的连续内存区包含多少个页帧. + +* 第0个链表包含的内存区为单页($2^0=1$) + +* 第1个链表管理的内存区为两页($2^1=2$) + +* 第3个管理的内存区为4页, 依次类推. + + +内存区是如何连接的? + +内存区中第1页内的链表元素, 可用于将内存区维持在链表中。因此,也不必引入新的数据结构来管理物理上连续的页,否则这些页不可能在同一内存区中. 如下图所示 + + +![伙伴系统中相互连接的内存区](../images/buddy_node_connect.png) + + +伙伴不必是彼此连接的. 如果一个内存区在分配其间分解为两半, 内核会自动将未用的一半加入到对应的链表中. + +如果在未来的某个时刻, 由于内存释放的缘故, 两个内存区都处于空闲状态, 可通过其地址判断其是否为伙伴. 管理工作较少, 是伙伴系统的一个主要优点. + +基于伙伴系统的内存管理专注于某个结点的某个内存域, 例如, DMA或高端内存域. 但所有内存域和结点的伙伴系统都通过备用分配列表连接起来. + +下图说明了这种关系. + +![伙伴系统和内存域/结点之间的关系](../images/buddy_and_node_zone.png) + + +最后要注意, 有关伙伴系统和当前状态的信息可以在/proc/buddyinfo中获取 + +![伙伴系统和当前状态的信息](../imaes) + +上述输出给出了各个内存域中每个分配阶中空闲项的数目, 从左至右, 阶依次升高. 上面给出的信息取自4 GiB物理内存的AMD64系统. + + +#3 避免碎片 +------- + + + + diff --git a/study/kernel/02-memory/04-buddy/images/buddy_and_node_zone.png b/study/kernel/02-memory/04-buddy/images/buddy_and_node_zone.png new file mode 100644 index 0000000..26f4464 Binary files /dev/null and b/study/kernel/02-memory/04-buddy/images/buddy_and_node_zone.png differ diff --git a/study/kernel/02-memory/04-buddy/images/buddy_node_connect.png b/study/kernel/02-memory/04-buddy/images/buddy_node_connect.png new file mode 100644 index 0000000..490a4b7 Binary files /dev/null and b/study/kernel/02-memory/04-buddy/images/buddy_node_connect.png differ