diff --git a/study/kernel/02-memory/03-initialize/05-build_zonelists/README.md b/study/kernel/02-memory/03-initialize/05-build_zonelists/README.md index 842eee6..7c209c9 100644 --- a/study/kernel/02-memory/03-initialize/05-build_zonelists/README.md +++ b/study/kernel/02-memory/03-initialize/05-build_zonelists/README.md @@ -252,553 +252,20 @@ bootmem_init +至此,bootmem_init已经完成了节点和管理区的关键数据已完成初始化, 内核在后面为内存管理做得一个准备工作就是将所有节点的管理区都链入到zonelist中,便于后面内存分配工作的进行. -**建立内存管理的数据结构** - - -对相关数据结构的初始化是从全局启动函数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 +#2 后备内存域列表zonelists ------- -内核setup_arch的最后通过bootmem_init中完成了内存数据结构的初始化(包括内存结点pg_data_t, 内存管理域zone和页面信息page), 数据结构已经基本准备好了, 在后面为内存管理做得一个准备工作就是将所有节点的管理区都链入到zonelist中,便于后面内存分配工作的进行. +内核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函数(已经完成的工作) +##2.1 回到start_kernel函数(已经完成的工作) ------- @@ -834,10 +301,35 @@ asmlinkage __visible void __init start_kernel(void) 下面内核开始通过start_kernel()->build_all_zonelists来设计内存的组织形式 -##3.2 build_all_zonelists初始化zonelists + +##2.2 后备内存域列表zonelist ------- +内存节点pg_data_t中将内存节点中的内存区域zone按照某种组织层次存储在一个zonelist中, 即pglist_data->node_zonelists成员信息 +```cpp +// http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L626 +typedef struct pglist_data +{ + struct zone node_zones[MAX_NR_ZONES]; + struct zonelist node_zonelists[MAX_ZONELISTS]; +} +``` + + +内核定义了内存的一个层次结构关系, 首先试图分配廉价的内存,如果失败,则根据访问速度和容量,逐渐尝试分配更昂贵的内存. + +高端内存最廉价, 因为内核没有任何部分依赖于从该内存域分配的内存, 如果高端内存用尽, 对内核没有副作用, 所以优先分配高端内存 + +普通内存域的情况有所不同, 许多内核数据结构必须保存在该内存域, 而不能放置到高端内存域, 因此如果普通内存域用尽, 那么内核会面临内存紧张的情况 + +DMA内存域最昂贵,因为它用于外设和系统之间的数据传输。 +举例来讲,如果内核指定想要分配高端内存域。它首先在当前结点的高端内存域寻找适当的空闲内存段,如果失败,则查看该结点的普通内存域,如果还失败,则试图在该结点的DMA内存域分配。如果在3个本地内存域都无法找到空闲内存,则查看其他结点。这种情况下,备选结点应该尽可能靠近主结点,以最小化访问非本地内存引起的性能损失。 + + + +##2.3 build_all_zonelists初始化zonelists +------- 内核在start_kernel中通过build_all_zonelists完成了内存结点及其管理内存域的初始化工作, 调用如下 @@ -849,6 +341,8 @@ asmlinkage __visible void __init start_kernel(void) [build_all_zonelists](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029)建立内存管理结点及其内存域的组织形式, 将描述内存的数据结构(结点, 管理域, 页帧)通过一定的算法组织在一起, 方便以后内存管理工作的进行. 该函数定义在[mm/page_alloc.c?v4.7, line 5029](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029) +##2.4 build_all_zonelists函数 +------- ```cpp /* @@ -904,7 +398,7 @@ void __ref build_all_zonelists(pg_data_t *pgdat, struct zone *zone) ``` -##3.3 设置结点初始化顺序 +#3 设置结点初始化顺序 ------- @@ -919,7 +413,7 @@ void __ref build_all_zonelists(pg_data_t *pgdat, struct zone *zone) ``` -##3.3.1 zonelist +##3.1 zonelist ------- @@ -943,7 +437,7 @@ EXPORT_SYMBOL(zone_table); -##3.3.2 内存域初始化顺序zonelist_order +##3.2 内存域初始化顺序zonelist_order ------- @@ -1023,7 +517,7 @@ static char zonelist_order_name[3][8] = {"Default", "Node", "Zone"}; -##3.3.3 set_zonelist_order设置排列方式 +##3.3 set_zonelist_order设置排列方式 ------- 内核就通过通过set_zonelist_order函数设置当前系统的内存域排列方式current_zonelist_order, 其定义依据系统的NUMA结构还是UMA结构有很大的不同. @@ -1069,7 +563,7 @@ static void set_zonelist_order(void) -##3.3.4 default_zonelist_order函数选择最优的配置 +##3.4 default_zonelist_order函数选择最优的配置 ------- @@ -1109,7 +603,7 @@ static int default_zonelist_order(void) -###3.3.5 user_zonelist_order用户指定排列方式 +###3.5 user_zonelist_order用户指定排列方式 ------- @@ -1188,7 +682,7 @@ static __init int setup_numa_zonelist_order(char *s) early_param("numa_zonelist_order", setup_numa_zonelist_order); ``` -##3.4 build_all_zonelists_init完成内存域zonelists的初始化 +#4 build_all_zonelists_init完成内存域zonelists的初始化 ------- build_all_zonelists函数在通过set_zonelist_order设置了zonelists中结点的组织顺序后, 首先检查了ssytem_state标识. 如果当前系统处于boot阶段(SYSTEM_BOOTING), 就开始通过build_all_zonelists_init函数初始化zonelist @@ -1207,7 +701,7 @@ build_all_zonelists(pg_data_t *pgdat, struct zone *zone) ``` -###3.4.1 system_state系统状态标识 +##4.1 system_state系统状态标识 ------- @@ -1245,7 +739,7 @@ else #endif ``` -##3.4.2 build_all_zonelists_init函数 +##4.2 build_all_zonelists_init函数 build_all_zonelists函数在如果当前系统处于boot阶段(system_state == SYSTEM_BOOTING), 就开始通过build_all_zonelists_init函数初始化zonelist @@ -1295,7 +789,7 @@ static int __build_all_zonelists(void *data) NUMA系统调用该函数的次数等同于结点的数目. 每次调用对一个不同结点生成内存域数据 -##3.4.3 build_zonelists初始化每个内存结点的zonelists +##4.3 build_zonelists初始化每个内存结点的zonelists ------- build_zonelists(pg_data_t *pgdat)完成了节点pgdat上zonelists的初始化工作, 它建立了备用层次结构zonelists. 由于UMA和NUMA架构下结点的层次结构有很大的区别, 因此内核分别提供了两套不同的接口. @@ -1463,7 +957,7 @@ m+2、…、N1、0、1、…、m1。这确保了不过度使用任何结 -##3.4.4 setup_pageset初始化per_cpu缓存 +##4.4 setup_pageset初始化per_cpu缓存 ------- 前面讲解内存管理域zone的时候, 提到了per-CPU缓存, 即冷热页. 在组织每个节点的zonelist的过程中, setup_pageset初始化了per-CPU缓存(冷热页面) @@ -1498,12 +992,12 @@ static __meminit void zone_pcp_init(struct zone *zone) ``` -#4 总结 +#5 总结 ------- -##4.1 start_kernel启动流程 +##5.1 start_kernel启动流程 ------- @@ -1553,7 +1047,7 @@ start_kernel() | ``` -##4.2 pidhash_init配置高端内存 +##5.2 pidhash_init配置高端内存 ------- @@ -1576,7 +1070,7 @@ void pidhash_init(void) | INIT_HLIST_HEAD(&pid_hash[i]); ``` -##4.3 build_all_zonelists初始化每个内存节点的zonelists +##5.3 build_all_zonelists初始化每个内存节点的zonelists ------- 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 21ff471..d2e8725 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 @@ -26,6 +26,9 @@ Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该 ------- +##2.1 伙伴系统数据结构 +------- + 系统内存中的每个物理内存页(页帧),都对应于一个struct page实例, 每个内存域都关联了一个struct zone的实例,其中保存了用于管理伙伴数据的主要数数组 @@ -73,9 +76,10 @@ zone->free_area[MAX_ORDER]数组中阶作为各个元素的索引, 用于指定 ![空闲页快](../images/order_free_list.png) -##MAX_ORDER与FORCE_MAX_ZONEORDER配置选项 +##2.2 最大阶MAX_ORDER与FORCE_MAX_ZONEORDER配置选项 ------- + 一般来说`MAX_ORDER`默认定义为11, 这意味着一次分配可以请求的页数最大是2^11=2048, 参见[include/linux/mmzone.h?v=4.7, line 22](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L22) ```cpp @@ -117,9 +121,10 @@ default "11"` -##内存区是如何连接的 +##2.3 内存区是如何连接的 ------- + 内存区中第1页内的链表元素, 可用于将内存区维持在链表中。因此,也不必引入新的数据结构来管理物理上连续的页,否则这些页不可能在同一内存区中. 如下图所示 @@ -145,9 +150,125 @@ default "11"` 上述输出给出了各个内存域中每个分配阶中空闲项的数目, 从左至右, 阶依次升高. 上面给出的信息取自4 GiB物理内存的AMD64系统. -#3 避免碎片 + + +# 分配器API ------- +## 分配内存的接口 +------- + +就伙伴系统的接口而言, NUMA或UMA体系结构是没有差别的, 二者的调用语法都是相同的. + +所有函数的一个共同点是 : 只能分配2的整数幂个页. + +因此,接口中不像C标准库的malloc函数或bootmem和memblock分配器那样指定了所需内存大小作为参数. 相反, 必须指定的是分配阶, 伙伴系统将在内存中分配$2^order$页. 内核中细粒度的分配只能借助于slab分配器(或者slub、slob分配器), 后者基于伙伴系统 + + +| 内存分配函数 | 功能 | 定义 | +|:-----:|:-----:| +| alloc_pages(mask, order) | 分配$2^order$页并返回一个struct page的实例,表示分配的内存块的起始页 | [NUMA-include/linux/gfp.h, line 466](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L466)
[UMA-include/linux/gfp.h?v=4.7, line 476](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L476) | +| alloc_page(mask) | 是前者在order = 0情况下的简化形式,只分配一页 | [include/linux/gfp.h?v=4.7, line 483](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L483) | +| get_zeroed_page(mask) | 分配一页并返回一个page实例,页对应的内存填充0(所有其他函数,分配之后页的内容是未定义的) | [mm/page_alloc.c?v=4.7, line 3900](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3900)| | +| [__get_free_pages(mask, order)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3883)
[__get_free_page(mask)](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L500) | 工作方式与上述函数相同,但返回分配内存块的虚拟地址,而不是page实例 | +| get_dma_pages(gfp_mask, order) | 用来获得适用于DMA的页. | [include/linux/gfp.h?v=4.7, line 503](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L503) | + + +在空闲内存无法满足请求以至于分配失败的情况下,所有上述函数都返回空指针(比如alloc_pages和alloc_page)或者0(比如get_zeroed_page、__get_free_pages和__get_free_page). + +因此内核在各次分配之后都必须检查返回的结果. 这种惯例与设计得很好的用户层应用程序没什么不同, 但在内核中忽略检查会导致严重得多的故障 + + +内核除了伙伴系统函数之外, 还提供了其他内存管理函数. 它们以伙伴系统为基础, 但并不属于伙伴分配器自身. 这些函数包括vmalloc和vmalloc_32, 使用页表将不连续的内存映射到内核地址空间中, 使之看上去是连续的. + +还有一组kmalloc类型的函数, 用于分配小于一整页的内存区. 其实现 +将在本章后续的几节分别讨论。 + +## 释放函数 +------- + +有4个函数用于释放不再使用的页,与所述函数稍有不同 + + +| 内存释放函数 | 描述 | +|:--------------:|:-----:| +| [free_page(struct page *)](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L520)
[free_pages(struct page *, order)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3918) | 用于将一个或2order页返回给内存管理子系统。内存区的起始地址由指向该内存区的第一个page实例的指针表示 | +| [__free_page(addr)](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L519)
[__free_pages(addr, order)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3906) | 类似于前两个函数,但在表示需要释放的内存区时,使用了虚拟内存地址而不是page实例 | + +##分配掩码 +------- + + +前述所有函数中强制使用的mask参数,到底是什么语义? + +我们知道Linux将内存划分为内存域. 内核提供了所谓的内存域修饰符(zone modifier)(在掩码的最低4个比特位定义), 来指定从哪个内存域分配所需的页. + + +参见[include/linux/gfp.h?v=4.7, line 12~374](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L12) + +```cpp +// http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7 + +// line 12 ~ line 44 +/* Plain integer GFP bitmasks. Do not use this directly. */ +#define ___GFP_DMA 0x01u +#define ___GFP_HIGHMEM 0x02u +#define ___GFP_DMA32 0x04u +#define ___GFP_MOVABLE 0x08u +/* ...... */ + +// line 46 ~ line 192 +#define __GFP_DMA ((__force gfp_t)___GFP_DMA) +#define __GFP_HIGHMEM ((__force gfp_t)___GFP_HIGHMEM) +#define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32) +#define __GFP_MOVABLE ((__force gfp_t)___GFP_MOVABLE) /* ZONE_MOVABLE allowed */ + +// line 194 ~ line 260 +``` +其中GFP缩写的意思为获取空闲页(get free page), __GFP_MOVABLE不表示物理内存域, 但通知内核应在特殊的虚拟内存域ZONE_MOVABLE进行相应的分配. + + +我们从注释中找到这样的信息 +```cpp +bit result +================= +0x0 => NORMAL +0x1 => DMA or NORMAL +0x2 => HIGHMEM or NORMAL +0x3 => BAD (DMA+HIGHMEM) +0x4 => DMA32 or DMA or NORMAL +0x5 => BAD (DMA+DMA32) +0x6 => BAD (HIGHMEM+DMA32) +0x7 => BAD (HIGHMEM+DMA32+DMA) +0x8 => NORMAL (MOVABLE+0) +0x9 => DMA or NORMAL (MOVABLE+DMA) +0xa => MOVABLE (Movable is valid only if HIGHMEM is set too) +0xb => BAD (MOVABLE+HIGHMEM+DMA) +0xc => DMA32 (MOVABLE+DMA32) +0xd => BAD (MOVABLE+DMA32+DMA) +0xe => BAD (MOVABLE+DMA32+HIGHMEM) +0xf => BAD (MOVABLE+DMA32+HIGHMEM+DMA) + +GFP_ZONES_SHIFT must be <= 2 on 32 bit platforms. +``` + +很有趣的一点是,没有\__GFP_NORMAL常数,而内存分配的主要负担却落到ZONE_NORMAL内存域 + +内核考虑到这一点, 提供了一个函数gfp_zone来计算与给定分配标志兼容的最高内存域. 那么内存分配可以从该内存域或更低的内存域进行, 该函数定义在[](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L394) + +```cpp +static inline enum zone_type gfp_zone(gfp_t flags) +{ + enum zone_type z; + int bit = (__force int) (flags & GFP_ZONEMASK); + + z = (GFP_ZONE_TABLE >> (bit * GFP_ZONES_SHIFT)) & + ((1 << GFP_ZONES_SHIFT) - 1); + VM_BUG_ON((GFP_ZONE_BAD >> bit) & 1); + return z; +} +``` + diff --git a/study/kernel/02-memory/sh.exe.stackdump b/study/kernel/02-memory/sh.exe.stackdump new file mode 100644 index 0000000..9f15a41 --- /dev/null +++ b/study/kernel/02-memory/sh.exe.stackdump @@ -0,0 +1,12 @@ +Stack trace: +Frame Function Args +00000000000 0018007208E (00180215F9C, 00180215E46, 00000400000, 000005FEE10) +00000000000 00180046DF2 (000005FFE78, 00000000000, 00000000000, 00000002000) +00000000000 00180046E32 (00000600000, 00000800000, 00000400000, 00000600000) +00000000000 0018004715A (00000000000, 00100401000, 000003C8000, 00000000000) +00000000000 0018004719E (00000000000, 00000000000, 00000000000, 00000000000) +00000000000 001004D24B1 (00000000000, 00000000000, 00000000000, 00000000000) +00000000000 00100401010 (00000000000, 00000000000, 00000000000, 00100401000) +00000000000 7FFB65B18102 (00000000000, 00000000000, 00000000000, 00000000000) +00000000000 7FFB684BC5B4 (00000000000, 00000000000, 00000000000, 00000000000) +End of stack trace