说明
- 裁减内核预留内存占用,在启动log中,发现memmap占用了大块内存(446个pages)。
On node 0 totalpages: 32576
memblock_alloc_try_nid: 1835008 bytes align=0x40 nid=0 from=0x0000000000000000 max_addr=0x0000000000000000 alloc_node_mem_map.constprop.0+0x4c/0xd0
memblock_reserve: [0x000000008357d000-0x000000008373cfff] memblock_alloc_range_nid+0x96/0xc2
DMA32 zone: 446 pages used for memmap
DMA32 zone: 0 pages reserved
DMA32 zone: 32576 pages, LIFO batch:7
作用
- 物理内存管理时,zone按固定大小划分成一个个内存页,每一个物理内存页都需要一个struct page结构来保存其元数据(状态以及使用情况等),这个结构的存储和管理机制就是memmap。
- 每一个zone都需要拿出部分内存页来存储struct page结构体。
// file: include/linux/mm_types.h
...
struct page {
unsigned long flags; /* Atomic flags, some possibly
* updated asynchronously */
/*
* Five words (20/40 bytes) are available in this union.
* WARNING: bit 0 of the first word is used for PageTail(). That
* means the other users of this union MUST NOT use the bit to
* avoid collision and false-positive PageTail().
*/
union {
struct { /* Page cache and anonymous pages */
/**
* @lru: Pageout list, eg. active_list protected by
* pgdat->lru_lock. Sometimes used as a generic list
* by the page owner.
*/
struct list_head lru;
/* See page-flags.h for PAGE_MAPPING_FLAGS */
struct address_space *mapping;
pgoff_t index; /* Our offset within mapping. */
/**
* @private: Mapping-private opaque data.
* Usually used for buffer_heads if PagePrivate.
* Used for swp_entry_t if PageSwapCache.
* Indicates order in the buddy system if PageBuddy.
*/
unsigned long private;
};
....
} _struct_page_alignment;
占用空间大小
// file: mm/page_alloc.c
static unsigned long __init calc_memmap_size(unsigned long spanned_pages, unsigned long present_pages)
{
unsigned long pages = spanned_pages;
/*
* Provide a more accurate estimation if there are holes within
* the zone and SPARSEMEM is in use. If there are holes within the
* zone, each populated memory region may cost us one or two extra
* memmap pages due to alignment because memmap pages for each
* populated regions may not be naturally aligned on page boundary.
* So the (present_pages >> 4) heuristic is a tradeoff for that.
*/
if (spanned_pages > present_pages + (present_pages >> 4) &&
IS_ENABLED(CONFIG_SPARSEMEM))
pages = present_pages;
return PAGE_ALIGN(pages * sizeof(struct page)) >> PAGE_SHIFT;
}
static void __init free_area_init_core(struct pglist_data *pgdat)
{
enum zone_type j;
int nid = pgdat->node_id;
pgdat_init_internals(pgdat);
pgdat->per_cpu_nodestats = &boot_nodestats;
for (j = 0; j < MAX_NR_ZONES; j++) {
struct zone *zone = pgdat->node_zones + j;
unsigned long size, freesize, memmap_pages;
unsigned long zone_start_pfn = zone->zone_start_pfn;
size = zone->spanned_pages;
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
*/
memmap_pages = calc_memmap_size(size, freesize);
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
pr_warn(" %s zone: %lu pages exceeds freesize %lu\n",
zone_names[j], memmap_pages, freesize);
}
....
}
}
- 根据以上代码可知,memmap占用page计算公式为:
内存页对齐(管理的totalpages * struct page的size)/ 4096(页内存大小)。
- 根据条件(spanned_pages > present_pages + (present_pages >> 4) &&
IS_ENABLED(CONFIG_SPARSEMEM)),totapages 可能是 zone的spanned_pages也可能是present_pages。
- 条件(spanned_pages > present_pages + (present_pages >> 4)是考虑到导致(spanned_pages > present_pages)的内存空洞未内存页对齐而设定的折中,经验值。
spanned_pages和present_pages
- spanned_pages 是将物理内存地址当做持续的空间计算出的page个数,没有考虑预留内存以及新技术内存热插拔导致的内存空洞。
- present_pages 是去掉内存空洞后计算出的实际page个数。
三种内存模型
- linux的历史上出现过三种内存模型去管理它们,依次是
- 平坦内存模型(flat memory model)
- 不连续内存模型 (discontiguous memory model)
- 稀疏内存模型(sparse memory model)。
- 提出新的内存模型是因为老的内存模型已不适应计算机硬件的新技术(例如:NUMA技术、内存热插拔等),主要的原因是:老模型未考虑到内存空洞。
- 内存模型的设计则主要是权衡以下两点(空间与时间):
- 尽量少的消耗内存去管理众多的struct page。
- pfn_to_page和page_to_pfn的转换效率。
FLATMEM (flat memory model)
- FLATMEM内存模型是Linux最早使用的内存模型,那时计算机的内存通常不大需求也很单一,不需要考虑热插拔等导致内存空洞的因素。
- Linux会使用一个struct page mem_map[x]的数组根据PFN去依次存放所有的strcut page,且mem_map也位于内核空间的线性映射区,所以根据PFN(页帧号)即可轻松的找到目标页帧的strcut page。
- FLATMEM模型是非常简单可靠的机制,但是最大的问题就是未考虑内存空洞,而导致内存浪费。
DISCONTIGMEM (discontiguous memory model)
- DISCONTIGMEM的意思是不连续内存模型,解决的就是FLATMEM未考虑的内存空洞问题,但是其是非常短命的内存模型,很快被后续的SPARSEMEM完全替代。
- 注意:较新版本的Linux内核配置中没有该选项。
SPARSEMEM (sparse memory model)
- SPARSEMEM(稀疏内存模型)是当前内核默认的选择,从2005年被提出后沿用至今,但中间经过几次优化,包括:CONFIG_SPARSEMEM_VMEMMAP和CONFIG_SPARSEMEM_EXTREME的引入,这两个配置通常是被打开的。
- 主要解决的问题:
- 可以解决内存空洞导致的内存浪费。
- 支持内存的热插拔(memory hotplug)。
- 支持nodes间的overlap。
减小memmap内存占用
- 回到最初的目标:裁减memmap内存占用,思路如下:
- 减少totoal page
- 将预留内存(reserved memory)从total page中排除(no-map)
- 裁减struct page结构体
- 由于对细节的了解不够以及内存占用会做页对齐,裁减较小,对齐后,也看不到太多效果。
减少totoal page
- 使用no-map将预留内存从total page中排除,但是由于嵌入式平台使用的是FLATMEM模型,没有效果。
- 修改内核配置,将内存模型改为Sparse模型:
Memory Management options --->
Memory model (Sparse Memory) --->
[] Flat Memory
[] Sparse Memory
- 实测后发现memmap占用确实降了,只需要440个page,如下:
On node 0 totalpages: 32126
DMA32 zone: 440 pages used for memmap
DMA32 zone: 0 pages reserved
DMA32 zone: 32126 pages, LIFO batch:7
// Flat memory
Memory: 48032K/130304K available (3478K kernel code, 500K rwdata, 1504K rodata, 124K init, 212K bss, 82272K reserved, 0K cma-reserved)
// Sparse memory
Memory: 31636K/128504K available (3474K kernel code, 499K rwdata, 1505K rodata, 128K init, 212K bss, 96868K reserved, 0K cma-reserved)
- 开启memblock debug,log如下,确认Sparse相对于Flat模型会占用大量内存(16~18MB),因此对于嵌入式平台,使用Sparse模型得不偿失。
memblock_alloc_try_nid: 16777216 bytes align=0x40 nid=-1 from=0x0000000000000000 max_addr=0x0000000000000000 sparse_init+0xce/0x1d4
memblock_reserve: [0x0000000081dcd000-0x0000000082dccfff] memblock_alloc_range_nid+0x96/0xc2
memblock_alloc_try_nid: 4096 bytes align=0x40 nid=0 from=0x0000000000000000 max_addr=0x0000000000000000 sparse_index_alloc+0x42/0x64
memblock_reserve: [0x000000008373c000-0x000000008373cfff] memblock_alloc_range_nid+0x96/0xc2
memblock_alloc_try_nid: 40 bytes align=0x40 nid=0 from=0x0000000000000000 max_addr=0x0000000000000000 sparse_init_nid+0x3e/0x1a2
memblock_reserve: [0x000000008373bfc0-0x000000008373bfe7] memblock_alloc_range_nid+0x96/0xc2
memblock_alloc_exact_nid_raw: 2097152 bytes align=0x200000 nid=0 from=0x0000000080000000 max_addr=0x0000000000000000 sparse_init_nid+0xc4/0x1a2
memblock_reserve: [0x0000000083400000-0x00000000835fffff] memblock_alloc_range_nid+0x96/0xc2
memblock_alloc_try_nid_raw: 4096 bytes align=0x1000 nid=0 from=0x0000000080000000 max_addr=0x0000000000000000 vmemmap_pud_populate+0x16/0x4a
memblock_reserve: [0x000000008373a000-0x000000008373afff] memblock_alloc_range_nid+0x96/0xc2
memblock_alloc_try_nid_raw: 4096 bytes align=0x1000 nid=0 from=0x0000000080000000 max_addr=0x0000000000000000 vmemmap_pmd_populate+0x3a/0x6c
memblock_reserve: [0x0000000083739000-0x0000000083739fff] memblock_alloc_range_nid+0x96/0xc2
memblock_free: [0x00000000835c0000-0x00000000835fffff] sparse_init_nid+0x116/0x1a2
- 其它优化思路:将预留内存从total pages中排除,不采用no-map方式,采用其它方式,将排除后的内存段告诉内核(修改dts中memory大小)。