Linux Kernel Memory Zones Explained-Free Linux Device Drivers Tutorial

Linux Kernel Memory Zones Explained
Low memory, high memory, and the zone allocator — free Linux kernel development course
Lecture 11.2
Kernel 6.x
Beginner Friendly

Understanding Linux kernel memory zones is one of the most important steps in learning how the kernel manages physical RAM. Every page of memory in a running Linux system belongs to a zone, and the zone decides which driver, allocator, or subsystem is allowed to use that page. In this lecture — part of our free Linux kernel development course — we break down low memory, high memory, and the modern zone layout used on kernel 6.x, using original examples instead of copying any textbook content.

This lecture continues our free embedded Linux course on kernel memory management. If you are following along from the previous lecture on virtual memory and page frames, this is the natural next step: how the kernel organizes physical pages into zones so it can hand them out correctly to drivers, the page cache, and user processes.

What You Will Learn

  • The difference between low memory and high memory in the kernel address space
  • Why logical addresses exist for low memory but not for high memory
  • The complete list of Linux kernel memory zones used on kernel 6.x (ZONE_DMA, ZONE_DMA32, ZONE_NORMAL, ZONE_HIGHMEM, ZONE_MOVABLE)
  • How to check zone information on a running system using /proc/zoneinfo
  • Why kmap() and kmap_atomic() are deprecated, and how kmap_local_page() replaces them
  • How struct mm_struct connects a process to its page tables
  • A hands-on kernel module that allocates pages from different zones and prints their properties

Prerequisites

  • Completion of the previous lecture on virtual memory, pages, and page frames
  • Basic familiarity with writing and loading a Linux kernel module
  • A Linux machine or VM running kernel 6.x with build headers installed
This Lecture Is Part Of
Free Embedded Systems Course
Free Linux Development Course
Free Linux Device Drivers Course
Free Linux Kernel Development Course
Free Embedded Linux Course

What Is Low Memory in the Linux Kernel?

When the kernel boots, it permanently maps a portion of physical RAM into its own virtual address space. Any address produced by this permanent mapping is called a logical address. Because the mapping never changes, the kernel can convert a logical address to a physical address (and back) using a simple offset, through the __pa() and __va() macros defined in arch/x86/include/asm/page.h (and the equivalent per-architecture headers).

Memory that has a logical address is called low memory. On the older 32-bit x86 kernel with the classic 3G/1G split, low memory was capped at roughly 896 MB because only 1 GB of virtual address space was reserved for the kernel. On modern 64-bit systems this limitation is gone — the kernel’s virtual address space is enormous (128 PiB or more, depending on 4-level or 5-level paging), so effectively all physical RAM can be permanently mapped and treated as low memory.

Low Memory vs High Memory (32-bit Reference)
4 GB — Top of address space
HIGH MEMORY — top 128 MB — no permanent logical address, mapped on demand
LOW MEMORY — first 896 MB — permanently mapped, has logical addresses
0 GB — Start of kernel virtual address space
Address Type Where It Applies Conversion
Logical address Low memory only __pa(addr) / __va(addr) — simple offset
Virtual address (general) Any mapped memory Requires a full page table walk
Physical address Hardware / DMA What the CPU’s memory bus actually sees

Linux Kernel Memory Zones Explained

Not all physical pages are equal from the kernel’s point of view. Some hardware devices can only perform DMA to a limited range of physical addresses, and 32-bit systems cannot permanently map all of physical RAM. To handle these constraints, the kernel divides physical memory into zones, declared in include/linux/mmzone.h as the enum zone_type.

Zone Typical Range Purpose Common GFP Flag
ZONE_DMA First 16 MB Legacy ISA-class devices with a 24-bit DMA address limit GFP_DMA
ZONE_DMA32 First 4 GB (64-bit only) Devices that can only issue 32-bit DMA addresses GFP_DMA32
ZONE_NORMAL Rest of directly mapped RAM General-purpose kernel allocations GFP_KERNEL
ZONE_HIGHMEM Above ~896 MB (32-bit only) Memory with no permanent logical address; mapped on demand GFP_HIGHUSER
ZONE_MOVABLE Configurable Pages that can be migrated or hot-unplugged, used to fight fragmentation

Two things worth calling out for anyone learning this today rather than from an older textbook: first, ZONE_HIGHMEM does not exist at all on a normal 64-bit x86_64 buildCONFIG_HIGHMEM is only enabled on 32-bit kernels, since 64-bit address space is large enough to map every physical page permanently. Second, ZONE_DMA32 exists only on 64-bit architectures; it did not exist in the original 32-bit zone design at all.

Zone Layout On a Modern x86_64 System
ZONE_DMA
0 – 16 MB
ZONE_DMA32
16 MB – 4 GB
ZONE_NORMAL
4 GB – end of RAM

ZONE_HIGHMEM does not appear here — it is compiled out on 64-bit kernels.

You can inspect the live zone layout of any running Linux machine directly from user space:

$ cat /proc/zoneinfo | grep -A1 "Node 0, zone"
Node 0, zone      DMA
Node 0, zone    DMA32
Node 0, zone   Normal

Expected output: a 64-bit machine with more than 4 GB of RAM typically shows exactly these three zones — DMA, DMA32, and Normal — with no HighMem entry, confirming that high memory is a 32-bit-only concept on modern hardware.

High Memory and Temporary Kernel Mappings

On a 32-bit kernel where ZONE_HIGHMEM is active, a page in high memory has no permanent kernel virtual address. Before the kernel can read or write such a page, it must create a temporary mapping, use it, and then tear it down. Older documentation (and older textbooks) describe this using kmap() for a sleepable mapping and kmap_atomic() for a fast, non-preemptible one.

Modernization note (verified against current kernel documentation): kmap() and kmap_atomic() are both deprecated. Modern kernel code should use kmap_local_page() paired with kunmap_local(). Local mappings are thread-local, CPU-local, allow page faults and preemption in the common case, and work correctly even when CONFIG_HIGHMEM is disabled — which makes your driver code portable between 32-bit and 64-bit kernels without any #ifdef.

Hands-On: ep_zone_demo Kernel Module

The following original driver allocates one page from ZONE_DMA and one page from ZONE_NORMAL, prints which zone each page landed in, converts its address with __pa(), and demonstrates a temporary mapping using kmap_local_page().

// ep_zone_demo.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/gfp.h>
#include <linux/mm.h>
#include <linux/highmem.h>

static struct page *ep_dma_page;
static struct page *ep_normal_page;

static int __init ep_zone_demo_init(void)
{
	void *vaddr;

	/* Page from ZONE_DMA (first 16 MB, legacy DMA-safe memory) */
	ep_dma_page = alloc_page(GFP_DMA);
	if (!ep_dma_page)
		return -ENOMEM;

	/* Page from ZONE_NORMAL (ordinary kernel memory) */
	ep_normal_page = alloc_page(GFP_KERNEL);
	if (!ep_normal_page) {
		__free_page(ep_dma_page);
		return -ENOMEM;
	}

	pr_info("ep_zone_demo: DMA page zone   = %s\n",
		page_zone(ep_dma_page)->name);
	pr_info("ep_zone_demo: DMA page phys   = 0x%llx\n",
		(unsigned long long)__pa(page_address(ep_dma_page)));

	pr_info("ep_zone_demo: Normal page zone = %s\n",
		page_zone(ep_normal_page)->name);
	pr_info("ep_zone_demo: Normal page phys = 0x%llx\n",
		(unsigned long long)__pa(page_address(ep_normal_page)));

	/* Temporary mapping — works on both 32-bit HIGHMEM and 64-bit kernels */
	vaddr = kmap_local_page(ep_normal_page);
	memset(vaddr, 0xAB, 16);
	pr_info("ep_zone_demo: first byte after write = 0x%x\n",
		*(unsigned char *)vaddr);
	kunmap_local(vaddr);

	return 0;
}

static void __exit ep_zone_demo_exit(void)
{
	__free_page(ep_dma_page);
	__free_page(ep_normal_page);
	pr_info("ep_zone_demo: module unloaded, pages freed\n");
}

module_init(ep_zone_demo_init);
module_exit(ep_zone_demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala zone allocation demo");

Build and load it, then check the kernel log:

$ make
$ sudo insmod ep_zone_demo.ko
$ dmesg | tail -6

Expected output (values will differ per machine):

ep_zone_demo: DMA page zone   = DMA
ep_zone_demo: DMA page phys   = 0x1342000
ep_zone_demo: Normal page zone = Normal
ep_zone_demo: Normal page phys = 0x18a2f1000
ep_zone_demo: first byte after write = 0xab

Notice that the ZONE_DMA page’s physical address always stays well under 16 MB (0x1000000), while the ZONE_NORMAL page can land anywhere in the rest of RAM. Unload with sudo rmmod ep_zone_demo.

Process Address Space and struct mm_struct

Zones describe physical memory; struct mm_struct describes how a process’s virtual address space maps onto that physical memory. Every process is represented by a struct task_struct, and every task_struct with its own address space carries a pointer mm to a struct mm_struct. The current running process’s mapping table is always reachable through current->mm.

Field Purpose
pgd Pointer to the process’s top-level page table (Page Global Directory)
mmap_base, task_size Layout boundaries of the process’s virtual address space
mm_users, mm_count Reference counts — how many threads and holders reference this mm
pgtables_bytes Bytes consumed by this process’s page tables (modern replacement for the older nr_ptes field)
mmap_lock Serializes access to the VMA list (renamed from the older mmap_sem)
total_vm, stack_vm Bookkeeping counters for total and stack virtual memory usage

Modernization note (verified against current kernel source): older references describe an nr_ptes counter and an mmap_sem reader/writer semaphore inside struct mm_struct. Current kernels use pgtables_bytes (an atomic_long_t) and a dedicated mmap_lock type instead — the field names changed, but the underlying purpose is the same.

task_struct → mm_struct → Page Tables
struct task_struct
(current process)
mm field →
struct mm_struct
pgd field →
Page Global Directory

Common Mistakes and Troubleshooting Tips

  • Assuming ZONE_HIGHMEM always exists — it is compiled out entirely on most 64-bit kernels; code that assumes it will silently do the wrong thing.
  • Using deprecated kmap()/kmap_atomic() in new code — always reach for kmap_local_page() instead.
  • Forgetting that GFP_DMA memory is scarce — repeatedly allocating from ZONE_DMA in a loop can exhaust that zone quickly on systems with only 16 MB reserved for it.
  • Mixing up logical and virtual addresses — only low-memory pages have a logical address; don’t assume __pa() works on every pointer.

Best Practices

  • Use GFP_KERNEL for ordinary allocations and only request GFP_DMA/GFP_DMA32 when your hardware genuinely needs the restricted range.
  • Prefer the devm_-managed and kmap_local_page()-based APIs so cleanup happens automatically or predictably.
  • Check /proc/zoneinfo on your target board during driver bring-up to confirm which zones actually exist.

Performance Considerations

Allocations from ZONE_NORMAL are the fastest path since no bounce buffering or special mapping is required. Repeated kmap_atomic()-style code paths on 32-bit HIGHMEM kernels used to disable preemption, which the kmap_local_page() migration specifically fixes.

Security Considerations

Never leave a kmap_local_page() mapping unmapped-but-referenced across a context switch or interrupt boundary — the mapping is local to the calling thread and unmapping it out of order corrupts the internal mapping stack.

Real-World Use Cases

  • Network and storage drivers using ZONE_DMA32 buffers for legacy 32-bit-addressing hardware
  • Embedded boards with constrained RAM relying on precise zone sizing during kernel configuration
  • Memory hot-plug systems using ZONE_MOVABLE to safely remove RAM modules at runtime

Summary / Key Takeaways

Concept Key Point
Low memory Permanently mapped, has logical addresses, convertible with __pa()/__va()
High memory 32-bit-only concept, mapped on demand, does not exist on typical 64-bit kernels
Zones ZONE_DMA, ZONE_DMA32, ZONE_NORMAL, ZONE_HIGHMEM, ZONE_MOVABLE — declared in mmzone.h
Temporary mapping Use kmap_local_page()/kunmap_local(), not the deprecated kmap APIs
mm_struct Connects a process to its page tables via pgd, reachable through current->mm

Conclusion

Linux kernel memory zones exist because physical hardware has addressing limits that software must respect. By separating DMA-safe memory, 32-bit-addressable memory, ordinary memory, and on 32-bit systems, high memory, the kernel can hand the right page to the right consumer every time. Combined with the struct mm_struct that ties a process to its page tables, this forms the foundation for everything else we cover later in this free Linux kernel development course — including page table walks, memory mapping (mmap), and the page allocator internals.

Frequently Asked Questions

What are Linux kernel memory zones used for?

They group physical pages by hardware addressing constraints so the kernel’s allocator can satisfy requests like DMA-safe memory or ordinary kernel memory correctly.

Does ZONE_HIGHMEM exist on 64-bit Linux?

No. ZONE_HIGHMEM is only compiled in when CONFIG_HIGHMEM is enabled, which is normally a 32-bit-only kernel configuration.

What is the difference between a logical address and a virtual address?

Every logical address is a virtual address, but only low-memory pages have one, since they use a fixed offset mapping. All mapped memory has a virtual address, but converting it to a physical address in general requires a page table walk.

Why is kmap() deprecated?

kmap() and kmap_atomic() are deprecated in favor of kmap_local_page(), which is thread-local, CPU-local, and avoids unnecessarily disabling preemption or page faults.

What replaced the mm_struct field nr_ptes?

Current kernels track page table memory usage using the atomic_long_t field pgtables_bytes instead of the older nr_ptes counter.

How can I check which memory zones exist on my system?

Run cat /proc/zoneinfo and look for the “Node X, zone” lines; each one names a zone that is active on your hardware.

What is ZONE_MOVABLE used for?

It holds pages that the kernel can migrate or offline on demand, which helps reduce physical memory fragmentation and supports memory hot-plug.

 

Continue The Free Linux Kernel Development Course

Next up: page table structure — PGD, P4D, PUD, PMD, and PTE — on this free embedded Linux course.

Leave a Reply

Your email address will not be published. Required fields are marked *