The first time I ran into the term “high memory,” I was debugging a strange performance issue on an old 32-bit Linux server that had 8 GB of RAM installed but seemed to behave, in certain workloads, as if it had far less usable memory than that. Digging into /proc/meminfo and kernel documentation is where I actually learned what “high memory” means, and why it was such a specifically 32-bit problem — one that’s mostly historical now, but still shows up in embedded systems, older documentation, and technical interviews.
The Root Cause: Address Space Is Split Between Kernel and User
On a 32-bit x86 Linux system, the total addressable virtual memory space is 4 GB (2^32 bytes), because that’s all a 32-bit pointer can represent. The kernel has to reserve part of that address space for itself in every process’s address space (so it can handle system calls, interrupts, and context switches efficiently without a full address space swap). The traditional Linux split on 32-bit x86 was 3 GB for user space and 1 GB for the kernel — commonly written as the “3G/1G split.”
That 1 GB kernel portion has to accommodate not just the kernel’s own code and data structures, but also — critically — a way to directly address physical RAM. Here’s where the problem starts: if a machine has more than roughly 1 GB of physical RAM (in practice, given other kernel address space needs, the real threshold sits around 896 MB), the kernel simply doesn’t have enough address space left in its 1 GB slice to map all of physical memory directly, all the time.
What “Low Memory” and “High Memory” Mean
Given that split, Linux divides physical RAM into two conceptual zones from the kernel’s point of view:
- Low memory (
ZONE_NORMAL, roughly the first ~896 MB of physical RAM on 32-bit x86): Directly, permanently mapped into the kernel’s address space. The kernel can access it at any time with a simple, fast pointer dereference — no special setup required. - High memory (
ZONE_HIGHMEM, everything above that boundary): Physical RAM that exists and is usable, but that the kernel cannot keep permanently mapped into its own limited address space, because there simply isn’t enough room in the kernel’s 1 GB (or less) virtual address slice to map it all simultaneously.
To be clear: “high memory” doesn’t describe some special or faster kind of RAM hardware — it describes RAM whose physical address is high enough that the kernel can’t give it a permanent slot in its own address space.
32-bit Address Space (4 GB total)
---------------------------------------------
0 3 GB 4 GB
|--- User Space ---|---- Kernel Space -----|
(1 GB, minus overhead)
Kernel's view of physical RAM:
0 --------- ~896 MB --------------------- (RAM end, e.g. 8 GB)
| ZONE_NORMAL | ZONE_HIGHMEM |
| (always mapped)| (mapped temporarily, on demand) |
How the Kernel Actually Uses High Memory
Since the kernel can’t keep high memory permanently mapped, it uses temporary mappings whenever it actually needs to touch a page of high memory:
kmap()creates a temporary virtual mapping for a high-memory page, valid until explicitly released withkunmap(). This works but has real cost: mapping and unmapping aren’t free, and the pool of addresses reserved for temporary mappings is itself limited, creating contention under heavy use.kmap_atomic()provides a faster, per-CPU temporary mapping meant for short-lived access, usable even in contexts where sleeping isn’t allowed (unlikekmap(), which can block if the temporary mapping pool is exhausted).
This means any kernel code that wants to touch high-memory pages has to go through extra steps that low-memory pages never require — an added layer of complexity and overhead purely as a workaround for the address space shortage.
Why This Was a Real Performance and Complexity Problem
- Overhead of temporary mappings. Every access to high memory pages that requires
kmap()involves extra bookkeeping compared to a direct pointer dereference into permanently-mapped low memory. - Limited number of simultaneous temporary mappings. The pool of virtual addresses set aside for
kmap()is itself constrained, meaning heavy concurrent high-memory access could stall waiting for a mapping slot to free up. - Certain kernel data structures had to specifically avoid high memory. Some kernel structures (page tables among them) historically needed to live in low memory because the kernel needed to reference them without the overhead of temporary mapping — this shaped how memory allocators like the slab allocator had to behave differently for high versus low memory pages.
- The practical ceiling this created. Even with high memory support, 32-bit Linux systems had real, well-known practical limits and inefficiencies once physical RAM grew much beyond a few gigabytes — a major, concrete motivation for the broader industry shift to 64-bit systems.
Why High Memory Is Mostly a Non-Issue Today
On 64-bit systems (x86-64, ARM64), the virtual address space is vastly larger (typically 48-bit or 57-bit addressable today, i.e., terabytes to petabytes of address space), so the kernel can comfortably map essentially all physical RAM directly, permanently, without needing ZONE_HIGHMEM at all. On a modern 64-bit Linux kernel, you’ll typically find ZONE_HIGHMEM simply doesn’t exist, or contains zero pages — /proc/zoneinfo will show Normal and DMA/DMA32 zones, but no meaningful highmem zone, because the address space shortage that created the whole concept no longer applies.
High memory remains genuinely relevant today mainly in:
- 32-bit embedded Linux systems that are still deployed, particularly in industrial and IoT contexts where migrating to 64-bit hardware isn’t economically justified.
- Historical/legacy documentation and codebases, where understanding old kernel behavior (or debugging an ancient system still in production) requires knowing what
ZONE_HIGHMEMandkmap()actually did. - Educational contexts, since the high memory problem is one of the clearest, most concrete illustrations of why address space size is a hard architectural constraint, not just an implementation detail.
Comparisons to Other Systems
- 32-bit Windows had an almost identical problem and an analogous solution: Physical Address Extension (PAE) let 32-bit Windows systems address more than 4 GB of physical RAM in aggregate, but individual processes (and the kernel’s own direct-mapped region) still faced comparable addressing constraints, leading Microsoft to similarly emphasize the shift to 64-bit Windows for any system with substantial RAM.
- 32-bit macOS/Darwin systems faced the same fundamental 4 GB ceiling, and Apple’s own transition to 64-bit kernels (fully completed with OS X Mountain Lion-era kernels defaulting to 64-bit) followed the same underlying motivation.
- 64-bit systems across all platforms — Linux, Windows, macOS — have effectively retired this entire class of problem for general-purpose computing, though address space layout randomization (ASLR) and address space limits remain relevant for security and specific workloads even at 64-bit scale.
Practical Example: Reading /proc/zoneinfo
On an old 32-bit system with several GB of RAM, /proc/zoneinfo would show something like:
Node 0, zone DMA
Node 0, zone Normal
Node 0, zone HighMem
pages free 123456
...
The presence of a populated HighMem zone with real page counts was the direct, visible sign that the system was hitting the 32-bit addressing wall — physical RAM existed that the kernel couldn’t map directly. On a modern 64-bit system, this same file simply won’t show a meaningful highmem zone at all.
Troubleshooting High-Memory-Related Issues (Legacy Systems)
- Unexpectedly poor performance under memory-intensive workloads on 32-bit systems — check for heavy
kmap()/kunmap()activity, which can indicate the kernel is spending significant time managing temporary mappings rather than doing useful work. vmalloc()failures orvmallocaddress space exhaustion — on 32-bit systems, thevmallocarea (used for non-contiguous kernel allocations) competes for the same scarce kernel address space as everything else in the 1 GB kernel split, and can be exhausted under heavy driver or module use.- Consider PAE (
CONFIG_HIGHMEM64G) configuration issues — if a 32-bit kernel wasn’t built with high memory / PAE support enabled, RAM beyond the low-memory boundary may not be usable by the OS at all, appearing as “missing” memory even though it’s physically installed.
Best Practices (For the Rare Cases This Still Applies)
- On any 32-bit system with more than roughly 1 GB of RAM, ensure the kernel is built with appropriate
CONFIG_HIGHMEMsupport, or that RAM beyond the low-memory boundary is being wasted. - For any new deployment where it’s a realistic option, prefer 64-bit kernels entirely — this eliminates the high memory problem categorically rather than managing around it.
- When maintaining legacy 32-bit embedded systems, be mindful that any kernel code touching high-memory pages needs correct, careful use of
kmap()/kunmap()(orkmap_atomic()in non-sleeping contexts) — mismatched or missing unmap calls can exhaust the temporary mapping pool.
Zones Beyond High Memory: The Full Picture
High memory doesn’t exist in isolation — it’s one part of a broader zone system the kernel uses to categorize physical memory based on hardware addressing constraints, and understanding the full picture makes the high-memory concept clearer by contrast. On classic 32-bit x86, the kernel typically recognized three zones:
ZONE_DMA— a small region (historically the first 16 MB of physical RAM) reserved for older ISA-era DMA-capable devices that could only address a limited range of physical memory directly; some modern drivers still request allocations from this zone for hardware with similar legacy addressing constraints.ZONE_NORMAL— the directly, permanently mapped low-memory region discussed above, roughly up to ~896 MB on typical 32-bit configurations.ZONE_HIGHMEM— everything above that, requiring temporary mapping as described.
On 64-bit systems, the picture shifts: ZONE_DMA and ZONE_DMA32 (for devices limited to 32-bit addressing even on a 64-bit system — a surprisingly common constraint for certain older or simpler hardware) still exist, alongside ZONE_NORMAL, but ZONE_HIGHMEM is generally absent because the addressing shortage that necessitated it doesn’t apply. Each zone has its own free-page freelists and its own watermarks for triggering memory reclaim, which is part of why memory allocation requests in the kernel (GFP_DMA, GFP_HIGHUSER, GFP_KERNEL, and similar flags) specify which zone(s) they’re willing to be satisfied from — a low-level detail that only really makes sense once you understand that “physical memory” was never a single undifferentiated pool from the kernel’s point of view, even before high memory entered the picture.
Why the 3G/1G Split Itself Was Configurable
It’s worth noting that the traditional 3 GB user / 1 GB kernel split wasn’t the only option even within 32-bit Linux — it was a build-time configuration choice, and different splits traded off differently against the high-memory problem. A “4G/4G split” patch set (used by some enterprise distributions in the mid-2000s for database servers with heavy RAM needs) gave the kernel its own full 4 GB virtual address range, separate from user space entirely, at the cost of a more expensive context switch between user and kernel mode (since the two could no longer share simple, fast TLB-friendly address space layout tricks). This was a direct, practical illustration of the underlying trade-off: more kernel address space reduced high-memory pressure and its associated kmap() overhead, but increased per-syscall overhead elsewhere — there was no free lunch available purely through address space layout tuning, which is ultimately why the real, lasting fix was the industry-wide move to 64-bit addressing rather than ever-more-clever 32-bit workarounds.
A Note on ARM and Other 32-bit Architectures
While this discussion has centered on x86, the high-memory problem was never x86-specific — any 32-bit architecture with more physical RAM than its kernel/user address space split could directly map faced the same fundamental issue. 32-bit ARM Linux systems (common in earlier-generation smartphones, routers, and embedded devices) implemented their own ZONE_HIGHMEM support with the same kmap()-based approach, following the same general architecture as x86, though the specific address space split conventions differed somewhat based on ARM’s own memory layout conventions. This cross-architecture consistency is a good reminder that high memory was never really an “x86 quirk” — it was an inevitable consequence of 32-bit addressing colliding with growing RAM capacity, something every 32-bit architecture eventually had to confront in more or less the same way.
Highmem’s Effect on the Buffer Cache and I/O Performance
One place the high-memory constraint had genuinely visible, everyday performance consequences was the page/buffer cache — the mechanism Linux uses to keep recently-read or written file data resident in RAM for fast subsequent access. On a 32-bit system with several gigabytes of RAM, the vast majority of physical memory available for caching file data was, by necessity, high memory, simply because low memory was so comparatively small and needed to remain available for kernel data structures that genuinely couldn’t tolerate the overhead of temporary mapping. This meant ordinary file I/O operations — reading a large file, writing to disk — routinely involved kmap()/kunmap() calls as the kernel moved data between its buffer cache (largely in high memory) and the low-memory structures actually managing that I/O, adding measurable per-operation overhead compared to the equivalent operation on a 64-bit system where the entire cache lives in directly-mapped memory with no special handling required at all. Kernel developers at the time specifically optimized bulk I/O paths (like bio and block layer scatter-gather operations) to minimize the number of individual kmap() calls needed per operation, batching work where possible — a good illustration of how a seemingly low-level addressing constraint could ripple upward into shaping the design of much higher-level, performance-sensitive kernel subsystems.
Highmem-Related Kernel Configuration Options in Detail
For anyone actually working with a 32-bit kernel configuration today (legacy embedded work being the most likely reason), it’s worth knowing the specific relevant config options rather than just the general concept: CONFIG_NOHIGHMEM disables high memory support entirely, appropriate only for systems genuinely certain to have less RAM than the low-memory boundary; CONFIG_HIGHMEM4G enables support for physical RAM up to roughly 4 GB total; and CONFIG_HIGHMEM64G extends this further, in conjunction with PAE (Physical Address Extension) support, to address significantly more physical RAM — up to 64 GB in principle, though obviously still constrained by the same fundamental kmap()-based access overhead for anything above the low-memory boundary regardless of how much total physical RAM the system nominally supports. Choosing incorrectly here — building a CONFIG_NOHIGHMEM kernel for a machine with more RAM than the low-memory boundary supports — doesn’t crash the system; it simply means the kernel silently ignores and never uses the RAM above that boundary, which is a specific, well-documented gotcha that has confused more than a few people troubleshooting “missing” memory on older 32-bit installations.
Summary
High memory in the Linux kernel is a concept that exists purely because of a specific, historical constraint: on 32-bit systems, the kernel’s own slice of the 4 GB virtual address space (commonly 1 GB, in the traditional 3G/1G split) wasn’t large enough to permanently map all physical RAM once a machine had more than roughly 1 GB installed. Linux split physical memory into low memory (permanently mapped, directly accessible) and high memory (temporarily mapped on demand via kmap()/kmap_atomic(), at real performance and complexity cost). This entire problem is essentially solved on 64-bit systems, whose vastly larger address spaces mean the kernel can map all physical RAM directly without any special zone — making high memory today mostly a legacy/embedded-systems concern and a genuinely useful case study in how hard address-space limits shape operating system design.
FAQs
Is high memory a special, faster type of RAM? No — it’s ordinary physical RAM whose address happens to fall outside the range the 32-bit kernel could permanently map into its own limited address space.
Does 64-bit Linux have high memory? Generally no — 64-bit address spaces are large enough that the kernel can directly map all physical RAM, so ZONE_HIGHMEM is typically empty or absent entirely.
What’s the difference between kmap() and kmap_atomic()? kmap() can block (sleep) if no temporary mapping slot is immediately available; kmap_atomic() provides a faster, non-blocking, per-CPU mapping suitable for use in contexts where sleeping isn’t allowed.
Why did 32-bit Windows have a similar issue? The same 4 GB address space ceiling applied, and Microsoft addressed it with Physical Address Extension (PAE), analogous in purpose (though different in mechanism) to Linux’s high memory zone.
Should I worry about high memory on a modern server? Almost certainly not, unless you’re specifically working with legacy 32-bit hardware or embedded systems — modern 64-bit deployments don’t encounter this limitation.
References
- Linux Kernel Documentation, “High Memory Handling” — https://www.kernel.org/doc/html/latest/mm/highmem.html
- “Understanding the Linux Kernel, 3rd Edition” (Bovet, Cesati) — O’Reilly Media
- Linux Kernel Source,
include/linux/highmem.h - Microsoft Docs, “Physical Address Extension” — https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/physical-address-extension
- “Linux Device Drivers, 3rd Edition” (Corbet, Rubini, Kroah-Hartman) — https://lwn.net/Kernel/LDD3/