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()andkmap_atomic()are deprecated, and howkmap_local_page()replaces them - How
struct mm_structconnects 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
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.
| 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 build — CONFIG_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.
0 – 16 MB
16 MB – 4 GB
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()andkmap_atomic()are both deprecated. Modern kernel code should usekmap_local_page()paired withkunmap_local(). Local mappings are thread-local, CPU-local, allow page faults and preemption in the common case, and work correctly even whenCONFIG_HIGHMEMis 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_ptescounter and anmmap_semreader/writer semaphore insidestruct mm_struct. Current kernels usepgtables_bytes(anatomic_long_t) and a dedicatedmmap_locktype instead — the field names changed, but the underlying purpose is the same.
(current process)
struct mm_struct
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_KERNELfor ordinary allocations and only requestGFP_DMA/GFP_DMA32when your hardware genuinely needs the restricted range. - Prefer the
devm_-managed andkmap_local_page()-based APIs so cleanup happens automatically or predictably. - Check
/proc/zoneinfoon 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.
