On arm64, the second half of bootmem_init turns the physical memory map into zone boundaries and prepares the data structures that the buddy allocator will later consume. The first half dealt with sparse memory; this part is organized around zone_sizes_init. The call path is:
start_kernel() // init/main.c
---->setup_arch() // arch/arm64/kernel/setup.c
---->bootmem_init() // arch/arm64/mm/init.c
---->memblocks_present() // mm/sparse.c
---->sparse_init() // mm/sparse.c
---->zone_sizes_init()
bootmem_init first derives two PFNs from the memblock memory map: min from PFN_UP(memblock_start_of_DRAM()) and max from PFN_DOWN(memblock_end_of_DRAM()). These are the lowest and highest PFNs of known usable DRAM.
void __init bootmem_init(void)
{
unsigned long min, max;
min = PFN_UP(memblock_start_of_DRAM());
max = PFN_DOWN(memblock_end_of_DRAM());
...
zone_sizes_init(min, max);
...
}
zone_sizes_init fills max_zone_pfns for each zone type and then calls free_area_init. For ZONE_DMA under CONFIG_ZONE_DMA, the code combines ACPI IORT and device-tree DMA limits: acpi_iort_dma_get_max_cpu_address() and of_dma_get_max_cpu_address(NULL) are converted with fls64, then min3(32U, dt_zone_dma_bits, acpi_zone_dma_bits) chooses zone_dma_bits. max_zone_phys(zone_dma_bits) produces arm64_dma_phys_limit, and max_zone_pfns[ZONE_DMA] is that limit in PFNs. For ZONE_DMA32, dma32_phys_limit = max_zone_phys(32) supplies the PFN. If no DMA limit has been set, arm64_dma_phys_limit falls back to PHYS_MASK + 1. ZONE_NORMAL always receives max. In a UMA configuration the path is simple because there is only one node, but the same max-PFN array feeds the generic zone setup.
static void __init zone_sizes_init(unsigned long min, unsigned long max)
{
unsigned long max_zone_pfns[MAX_NR_ZONES] = {0};
unsigned int __maybe_unused acpi_zone_dma_bits;
unsigned int __maybe_unused dt_zone_dma_bits;
phys_addr_t __maybe_unused dma32_phys_limit = max_zone_phys(32);
#ifdef CONFIG_ZONE_DMA
acpi_zone_dma_bits = fls64(acpi_iort_dma_get_max_cpu_address());
dt_zone_dma_bits = fls64(of_dma_get_max_cpu_address(NULL));
zone_dma_bits = min3(32U, dt_zone_dma_bits, acpi_zone_dma_bits);
/// Calculate the zone area size; here it defaults to the full DRAM size
arm64_dma_phys_limit = max_zone_phys(zone_dma_bits);
max_zone_pfns[ZONE_DMA] = PFN_DOWN(arm64_dma_phys_limit);
#endif
#ifdef CONFIG_ZONE_DMA32
max_zone_pfns[ZONE_DMA32] = PFN_DOWN(dma32_phys_limit);
if (!arm64_dma_phys_limit)
arm64_dma_phys_limit = dma32_phys_limit;
#endif
if (!arm64_dma_phys_limit)
arm64_dma_phys_limit = PHYS_MASK + 1;
max_zone_pfns[ZONE_NORMAL] = max;
/// Initialize zone structures according to each zone's maximum PFN
free_area_init(max_zone_pfns);
}
free_area_init is where the kernel records zone boundaries and begins per-node initialization. It clears arch_zone_lowest_possible_pfn and arch_zone_highest_possible_pfn, takes the first active PFN from find_min_pfn_with_active_regions, and checks arch_has_descending_max_zone_pfns so architectures with a descending zone layout are handled correctly.
The main boundary loop walks MAX_NR_ZONES. It skips ZONE_MOVABLE, because that zone is calculated separately. For every other zone it sets end_pfn = max(max_zone_pfn[zone], start_pfn), stores the lower and upper possible PFNs, and advances start_pfn to end_pfn. Empty zones naturally end up with identical lower and upper PFNs. After the loop, zone_movable_pfn is cleared and find_zone_movable_pfns_for_nodes determines where ZONE_MOVABLE starts on each node.
The function then prints the zone ranges, the movable-zone start PFNs per node, and the early memory node ranges. While walking the early memory ranges with for_each_mem_pfn_range, it also calls subsection_map_init(start_pfn, end_pfn - start_pfn) to prepare the subsection map for the active online ranges. Node setup follows: mminit_verify_pageflags_layout and setup_nr_node_ids run once, then every online node is passed to free_area_init_node. If a node has present pages, it is marked with N_MEMORY, and check_for_memory updates the node state. The last major action in free_area_init is memmap_init, which initializes the struct page array.
free_area_init_node is the per-node bridge between the global PFN ranges and the buddy allocator. It obtains pgdat through NODE_DATA(nid), warns if pgdat->nr_zones or pgdat->kswapd_highest_zoneidx is already set, and retrieves the node range with get_pfn_range_for_nid. It then records node_id, node_start_pfn, and clears per_cpu_nodestats. The node range is printed, calculate_node_totalpages fills the node and zone page counts, alloc_node_mem_map allocates the flat mem_map when CONFIG_FLAT_NODE_MEM_MAP is in use, pgdat_set_deferred_range configures deferred initialization, and free_area_init_core completes the core zone and free-list setup.
calculate_node_totalpages walks each zone in the node. zone_spanned_pages_in_node reports the spanned pages and the zone start and end PFNs. zone_absent_pages_in_node reports holes. The zone receives spanned_pages = size and present_pages = size - absent; under CONFIG_MEMORY_HOTPLUG, present_early_pages is also set. If the zone has no pages, zone_start_pfn is zeroed. The function sums those values into pgdat->node_spanned_pages and pgdat->node_present_pages.
free_area_init_core first calls pgdat_init_internals and points pgdat->per_cpu_nodestats at the early boot_nodestats. For each zone it takes size = zone->spanned_pages and freesize = zone->present_pages, then estimates how many pages the zone needs for its own memmap. On non-highmem zones, that memmap_pages count is subtracted from freesize when possible; otherwise a warning is emitted. The first zone also subtracts dma_reserve. The remaining pages are accumulated into nr_kernel_pages and nr_all_pages, with a highmem adjustment when enough kernel pages exist. Then zone_init_internals is called. If the zone has no spanned pages, the loop continues. Otherwise the code sets the pageblock order, calls setup_usemap, and finishes with init_currently_empty_zone.
pgdat_init_internals initializes the node resize lock, split queues, kcompactd support, the kswapd_wait and pfmemalloc_wait wait queues, page extension data, and the node lruvec. Split queue setup is tied to transparent hugepage support. The lruvec and lock initialization here are part of the node's early state, before the full page allocator is active.
calc_memmap_size estimates the number of pages needed to hold struct page objects for a zone. It normally starts from spanned_pages. If there are many holes and CONFIG_SPARSEMEM is enabled, using present_pages gives a better estimate, because populated regions inside a zone may not align cleanly to page boundaries; the present_pages >> 4 heuristic accounts for that tradeoff. The final result is PAGE_ALIGN(pages * sizeof(struct page)) >> PAGE_SHIFT.
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;
}
Those page counts show up in boot logs. For example, a DMA32 zone may report 484 pages used for memmap, 0 pages reserved, and 30976 pages with a LIFO batch of 7. A Normal zone may report 15116 pages used for memmap and 952064 pages with a LIFO batch of 63. The memmap pages are treated as unavailable for general allocation, and the remaining pages feed the early counters.
zone_init_internals stores the remaining page count in zone->managed_pages, records the node id, assigns zone->name from zone_names, links the zone back to its pgdat, initializes the zone lock and seqcount, and sets up pcp state. For UMA, zone_set_nid changes little because there is only one node, and zone->zone_pgdat points at the single node structure. The managed_pages value here is only an initial number; memblock_free_all and later buddy initialization adjust it by removing reserved memory, leaving the final count used by the buddy system.
static void __meminit zone_init_internals(struct zone *zone, enum zone_type idx, int nid,
unsigned long remaining_pages)
{
atomic_long_set(&zone->managed_pages, remaining_pages);
zone_set_nid(zone, nid);
zone->name = zone_names[idx];
zone->zone_pgdat = NODE_DATA(nid);
spin_lock_init(&zone->lock);
zone_seqlock_init(zone);
zone_pcp_init(zone);
}
zone_pcp_init points zone->per_cpu_pageset at boot_pageset and zone->per_cpu_zonestats at boot_zonestats, then sets the boot high and batch values. At this stage per-CPU allocation is not fully online, so the code relies on linker-provided offsets for static per-CPU variables. If the zone is populated, the function can print its page count and LIFO batch. The batch value itself is calculated by zone_batchsize: for MMU systems it starts with min(zone_managed_pages(zone) >> 10, (1024 * 1024) / PAGE_SIZE), divides by 4, clamps to at least 1, and rounds down to a power-of-two value minus one. NOMMU systems return 0 so large contiguous frees are not deferred into small batches.
static int zone_batchsize(struct zone *zone)
{
#ifdef CONFIG_MMU
int batch;
/*
* The number of pages to batch allocate is either ~0.1%
* of the zone or 1MB, whichever is smaller. The batch
* size is striking a balance between allocation latency
* and zone lock contention.
*/
batch = min(zone_managed_pages(zone) >> 10, (1024 * 1024) / PAGE_SIZE);
batch /= 4; /* We effectively *= 4 below */
if (batch < 1)
batch = 1;
/*
* Clamp the batch to a 2^n - 1 value. Having a power
* of 2 value was found to be more likely to have
* suboptimal cache aliasing properties in some cases.
*
* For example if 2 tasks are alternately allocating
* batches of pages, one task can end up with a lot
* of pages of one half of the possible page colors
* and the other with pages of the other colors.
*/
batch = rounddown_pow_of_two(batch + batch/2) - 1;
return batch;
#else
/* The deferral and batching of frees should be suppressed under NOMMU
* conditions.
*
* The problem is that NOMMU needs to be able to allocate large chunks
* of contiguous memory as there's no hardware page translation to
* assemble apparent contiguous memory from discontiguous pages.
*
* Queueing large contiguous runs of pages for batching, however,
* causes the pages to actually be freed in smaller chunks. As there
* can be a significant delay between the individual batches being
* recycled, this leads to the once large chunks of space being
* fragmented and becoming unavailable for high-order allocations.
*/
return 0;
#endif
}
The batch printed during this early phase is informational; the value is not stored into the zone here. The real per-CPU pageset setup happens later through setup_per_cpu_pageset during start_kernel.
set_pageblock_order sets the global pageblock_order once. If HPAGE_SHIFT > PAGE_SHIFT, it uses HUGETLB_PAGE_ORDER; otherwise it uses MAX_ORDER - 1. Architectures without variable hugepage sizes keep this default. init_currently_empty_zone then updates pgdat->nr_zones, stores the zone start PFN, initializes the zone free lists, and marks the zone initialized. The free-list initialization is the first appearance of the zone's free_area array, the core data structure that the buddy allocator will use.
static void __meminit zone_init_free_lists(struct zone *zone)
{
unsigned int order, t;
for_each_migratetype_order(order, t) {
INIT_LIST_HEAD(&zone->free_area[order].free_list[t]);
zone->free_area[order].nr_free = 0;
}
}
After zone_init_free_lists, zone->initialized is set to 1. That flag matters because later buddy allocator initialization expects the zone to have reached this state.
Finally, memmap_init initializes the struct page objects. It walks each memory PFN range with for_each_mem_pfn_range, and for every populated zone in the corresponding node it calls memmap_init_zone_range(zone, start_pfn, end_pfn, &hole_pfn). Under CONFIG_SPARSEMEM, the range end is rounded up to PAGES_PER_SECTION, and init_unavailable_range handles the hole up to the section end. Each page is initialized through __init_single_page.
static void __meminit __init_single_page(struct page *page, unsigned long pfn,
unsigned long zone, int nid)
{
mm_zero_struct_page(page);
set_page_links(page, zone, nid, pfn);
init_page_count(page);
page_mapcount_reset(page);
page_cpupid_reset_last(page);
page_kasan_tag_reset(page);
INIT_LIST_HEAD(&page->lru);
#ifdef WANT_PAGE_VIRTUAL
/* The shift won't overflow because ZONE_NORMAL is below 4G. */
if (!is_highmem_idx(zone))
set_page_address(page, __va(pfn << PAGE_SHIFT));
#endif
}
The page is first zeroed. set_page_links writes the zone and node id into page->flags. init_page_count sets _refcount to 1, page_mapcount_reset sets _mapcount to -1, and page_cpupid_reset_last plus page_kasan_tag_reset clear the remaining early tag state. The lru list head is initialized here, which is the first initialization point for that important struct page member. On configurations that keep a virtual page address, non-highmem pages also receive their __va address.

Once __init_single_page finishes, set_pageblock_migratetype marks the pageblock as MIGRATE_MOVABLE. If another migratetype is requested later, the allocator can convert it from that initial movable state. At this point the zone structures, free lists, node counters, and page metadata are ready for the buddy allocator to take over.
