Explain the concept of high memory in the context of the Linux kernel

Explain the concept of high memory in the context of the Linux kernel

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:

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:

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

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:

Comparisons to Other Systems

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)

Best Practices (For the Rare Cases This Still Applies)

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:

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

Exit mobile version