hugetlbfs and Reserved Huge Pages
hugetlbfs is the explicit, administrator-managed path to huge pages in Linux. Rather than hoping the kernel opportunistically uses large pages (the job of Transparent Huge Pages), hugetlbfs treats huge pages as a scarce, pre-reserved resource: an administrator allocates a pool of huge pages — physically-contiguous 2 MiB or 1 GiB regions carved out of the buddy allocator — and applications then explicitly request memory backed by them. These pool pages are reserved inside the kernel, cannot be used for other purposes, and cannot be swapped out (kernel admin-guide: HugeTLB Pages). The payoff is a hard guarantee: once reserved, the pages are there at fault time — exactly the determinism databases and hypervisors need. The mechanism is built on
CONFIG_HUGETLBFSand the underlyingCONFIG_HUGETLB_PAGE, exposed through a pseudo-filesystem (hugetlbfs), themmap(MAP_HUGETLB)flag,memfd_create(MFD_HUGETLB), SysV shared memory, and a set of sysctl/sysfs pool tunables.
This note covers the explicit huge-page path end to end. For the automatic alternative and the choice between them, see Transparent Huge Pages and the Huge Pages Overview.
Mental Model — A Pool You Reserve, Then Draw From
hugetlbfs has two halves that are easy to conflate but must be kept distinct: the pool (how many huge pages exist in the kernel, set by the admin) and the consumption (how a process maps some of them). The pool is global (with per-NUMA-node sub-pools); consumption is per-mapping. Between them sits a reservation ledger that promises a mapping its pages before they are actually faulted in.
flowchart TB subgraph POOL["The POOL (admin-controlled, global)"] PERSIST["Persistent pool<br/>nr_hugepages<br/>(pre-allocated, stays)"] SURPLUS["Surplus pool<br/>nr_overcommit_hugepages<br/>(borrowed on demand, freed after)"] end BUDDY["Buddy allocator<br/>(normal page pool)"] BUDDY -->|"carved out at set time"| PERSIST BUDDY -.->|"borrowed when persistent exhausted"| SURPLUS subgraph CONSUME["CONSUMPTION (per process)"] MMAP["mmap(MAP_HUGETLB)<br/>or hugetlbfs file<br/>or MFD_HUGETLB<br/>or SysV shm"] RESV["Reservation made<br/>(HugePages_Rsvd ++)"] FAULT["Fault: page handed out<br/>(HugePages_Free --)"] MMAP --> RESV --> FAULT end PERSIST --> RESV SURPLUS --> RESV
The hugetlbfs pool-and-reservation model. What it shows: an admin pre-carves the persistent pool out of the buddy allocator (nr_hugepages); optionally the kernel may borrow extra “surplus” pages on demand up to nr_overcommit_hugepages and return them when freed. A process maps huge memory through one of four interfaces, which immediately makes a reservation (counted in HugePages_Rsvd) against the pool, and the actual page is handed over only at fault time. The insight: the reservation is what turns “best-effort” into “guaranteed” — by the time your code faults the page, the kernel already promised it exists, so the fault cannot fail for lack of huge pages.
The Pool: Persistent and Surplus Pages
Persistent pages — nr_hugepages
The size of the persistent pool for the default huge-page size is /proc/sys/vm/nr_hugepages. Persistent pages are pre-allocated and returned to the huge-page pool when freed by a task (not back to the general allocator), so they stay reserved for huge-page use across the system’s life (hugetlbpage.rst). A privileged user adjusts the pool at runtime:
echo 1024 > /proc/sys/vm/nr_hugepages # ask for 1024 default-size (2 MiB) huge pagesThis tries to grow or shrink the pool to 1024. Growth can fail — partially — because each huge page needs a physically contiguous run of base pages, and once memory is fragmented the buddy allocator may be unable to assemble enough order-9 (2 MiB) blocks. Always verify what you actually got:
grep Huge /proc/meminfo
# HugePages_Total: 1024
# HugePages_Free: 1024
# HugePages_Rsvd: 0
# HugePages_Surp: 0
# Hugepagesize: 2048 kB
# Hugetlb: 2097152 kBThe five /proc/meminfo fields are the canonical pool dashboard (hugetlbpage.rst):
HugePages_Total— the pool size (matchesnr_hugepagesfor the default size).HugePages_Free— pool pages not yet handed out to any mapping.HugePages_Rsvd— “reserved”: pages a mapping has committed to but not yet faulted in. These are counted as free-but-promised; the count guarantees the mapping can fault without failure. A subtle consequence:HugePages_Freecan be non-zero while every page is reserved, so a new mapping can still fail.HugePages_Surp— “surplus”: pages allocated abovenr_hugepages(see below).Hugepagesize/Hugetlb— the default size in kB and the total bytes consumed across all sizes.
The five counters people confuse
Four numbers describe the pool and one describes the knob that sets it, and almost every hugetlb support question comes from mixing them up. They are not independent — three of them are derived, and the relationships are what make the accounting comprehensible:
| Name | Where | Read/write | Definition | Relationship |
|---|---|---|---|---|
nr_hugepages | /proc/sys/vm/nr_hugepages, /sys/kernel/mm/hugepages/hugepages-<size>kB/nr_hugepages | writable | The persistent pool size the admin asks for. Writing it triggers allocation or freeing immediately and can partially fail. | It is a request, not a fact. Always re-read it: the kernel writes back what it actually achieved. |
HugePages_Total / nr_hugepages (after readback) | /proc/meminfo, sysfs | read | Total huge pages that exist in the pool. | Total = persistent + surplus |
HugePages_Free / free_hugepages | /proc/meminfo, sysfs | read | Pool pages not currently faulted into any mapping — physically idle. | Free = Total − (pages actually mapped) |
HugePages_Rsvd / resv_hugepages | /proc/meminfo, sysfs | read | Pages that are free but already promised to an existing mapping that has not faulted them yet. | Rsvd ≤ Free. Free − Rsvd is the only number that tells you whether a new mmap can succeed. |
HugePages_Surp / surplus_hugepages | /proc/meminfo, sysfs | read | Pages the pool holds above nr_hugepages, borrowed from the buddy allocator on demand. | Surp ≤ nr_overcommit_hugepages (normally; see the HVO caveat below) |
nr_overcommit_hugepages | /proc/sys/vm/nr_overcommit_hugepages, sysfs | writable | Ceiling on HugePages_Surp. | Sets the elastic headroom above the hard floor |
The two writable entries are the policy; the four read-only ones are the consequence. Drawn as one picture:
flowchart LR subgraph POOL["HugePages_Total = nr_hugepages + HugePages_Surp"] direction TB INUSE["IN USE<br/>faulted in, mapped by a process<br/>= Total - Free"] subgraph FREE["HugePages_Free — physically free"] RSVD["HugePages_Rsvd<br/>promised to an existing mapping.<br/>A NEW mmap may NOT use these."] AVAIL["genuinely available<br/>= Free - Rsvd<br/>this is the number that matters"] end end style AVAIL fill:#2d6a4f,color:#fff style RSVD fill:#7f5539,color:#fff
How the four /proc/meminfo counters nest. What it shows: HugePages_Rsvd is a subset of HugePages_Free, not a separate bucket alongside it. A reserved page is physically sitting idle in the free pool — nothing has touched it — but it has already been spoken for. The insight to take: the naive health check HugePages_Free > 0 is wrong, and it is wrong in the direction that produces production incidents. If Total=1024, Free=1024, Rsvd=1024, the pool looks completely idle and the next mmap(MAP_HUGETLB) will still fail with ENOMEM, because every one of those thousand pages is already owed to someone else. Monitor Free − Rsvd.
The kernel computes this relationship explicitly. In gather_surplus_pages() (mm/hugetlb.c L2455–L2472), the arithmetic for “can I reserve delta more pages without borrowing?” is:
needed = (h->resv_huge_pages + delta) - h->free_huge_pages;
if (needed <= 0) {
h->resv_huge_pages += delta; /* fits in the unreserved free pages */
return 0;
}
/* otherwise: try to allocate `needed` surplus pages from the buddy allocator */Reading it symbol by symbol: h is the struct hstate for this huge-page size; delta is how many pages the new mapping wants to reserve; resv_huge_pages and free_huge_pages are the Rsvd and Free counters above. The reservation fits without borrowing exactly when delta ≤ free_huge_pages − resv_huge_pages — the “genuinely available” box in the diagram. Otherwise the kernel tries to make up the shortfall with surplus pages, and if that fails, hugetlb_acct_memory() returns -ENOMEM and the mmap fails.
Uncertain
Verify: the pseudo-code in
Documentation/mm/hugetlbfs_reserv.rststates the check asif (resv_needed <= (resv_huge_pages - free_huge_pages)), which is the subtraction the other way round from the v6.12 code quoted above and would be satisfied only when the pool is over-reserved. Reason: the documentation file says up front that it “attempts to describe how huge page reserve processing is done in the v4.10 kernel”, so it is dated and appears to carry a transcription error; the code was read directly at the v6.12 tag and is authoritative. The prose in that document is otherwise accurate and is the best available description of the reservation design. To resolve: nothing further is needed for correctness here — the code governs — but be aware the doc’s formula should not be quoted. uncertain
Because contiguous memory is most plentiful before fragmentation sets in, the most reliable way to provision a large pool is at boot, on the kernel command line:
hugepagesz=2M hugepages=1024
hugepagesz selects the size (bytes, with kKmMgG suffix), and the following hugepages=N pre-allocates N of them; the pair can be repeated for multiple sizes, and default_hugepagesz=<size> sets which size nr_hugepages and /proc/meminfo refer to (hugetlbpage.rst). A node-targeted form exists too: hugepagesz=2M hugepages=0:1,1:2 allocates 1 page on node 0 and 2 on node 1. The hugepage_alloc_threads=N parameter parallelizes boot-time allocation of (non-gigantic) pages to cut boot time.
Surplus pages — nr_overcommit_hugepages
The persistent pool is a hard floor that wastes memory if oversized and starves applications if undersized. Surplus pages relax this. Writing a non-zero value to /proc/sys/vm/nr_overcommit_hugepages permits the hugetlb subsystem to borrow extra huge pages from the normal buddy pool when the persistent pool is exhausted, up to that limit; as those surplus pages become unused they are freed back to the normal page pool (hugetlbpage.rst). This is “elastic” huge-page capacity: you keep a modest persistent pool for guaranteed baseline performance and let bursts spill into surplus. Two interactions matter. First, growing nr_hugepages promotes existing surplus pages to persistent before allocating fresh ones. Second, shrinking nr_hugepages below the number currently in use converts the in-use balance to surplus — even past the overcommit limit — and no further surplus can be allocated until nr_hugepages + nr_overcommit_hugepages rises or pages are freed (hugetlbpage.rst).
Per-NUMA-Node Pools
On a NUMA machine, which node a huge page lives on determines whether a thread’s accesses are local (fast) or remote (slow). hugetlbfs therefore replicates the pool controls per node under /sys/devices/system/node/node[0-9]*/hugepages/hugepages-<size>kB/, each containing nr_hugepages (writable, this node’s persistent count), and the read-only free_hugepages and surplus_hugepages (hugetlbpage.rst). Writing the per-node nr_hugepages adjusts that node’s pool regardless of the task’s NUMA memory policy or cpuset — it is the direct, deterministic knob.
The global /proc/sys/vm/nr_hugepages instead spreads the pool across nodes as if interleaved. A second global sysctl, /proc/sys/vm/nr_hugepages_mempolicy, honors the writing task’s NUMA memory policy, so you can steer allocation with numactl:
numactl -m 0,1 echo 512 > /proc/sys/vm/nr_hugepages_mempolicyNote the doc’s caveat: surplus and reserve counts remain global quantities, because the kernel cannot know at pool-set time which node a future fault’s mempolicy will choose (hugetlbpage.rst). Per-node sysfs also exposes a richer set including nr_hugepages_mempolicy, resv_hugepages, demote, and demote_size under /sys/kernel/mm/hugepages/hugepages-<size>kB/.
Consuming Huge Pages: the Four Interfaces
A pool by itself does nothing; an application must map memory backed by it. There are four ways, and only one requires mounting the filesystem.
| Interface | Needs a mount? | Shareable across processes? | Backed by | Reservation made at | Typical user |
|---|---|---|---|---|---|
open() a file on a hugetlbfs mount, then mmap it | yes | yes, by path | An inode on the hugetlbfs superblock; reserve map hangs off inode->i_mapping->private_data | mmap, plus min_size= reserves at mount time | QEMU -mem-path, DPDK, libhugetlbfs |
mmap(..., MAP_ANONYMOUS | MAP_HUGETLB, ...) | no | only via MAP_SHARED + fork | An internal hugetlbfs inode the kernel creates for you | mmap | The default modern path; PostgreSQL huge_pages=on |
memfd_create(name, MFD_HUGETLB) → fd → mmap | no | yes, by passing the fd (SCM_RIGHTS) | An anonymous hugetlbfs file | mmap of the fd | VM/sandbox memory backends (memory-backend-memfd); since Linux 4.14 |
shmget(..., SHM_HUGETLB) → shmat() | no | yes, by SysV key | SysV shared-memory segment on hugetlbfs | shmget | Oracle; older PostgreSQL shared_memory_type=sysv setups |
The common thread — and the reason they are listed as one family rather than four features — is that all four end up on hugetlbfs. Even the “no mount needed” paths create an inode on an internal, always-present hugetlbfs superblock; the mount is only about giving those inodes names in the directory tree. That is also why the reservation semantics described below are identical across all four.
1. The hugetlbfs mount (file-backed)
Mount the pseudo-filesystem and create files on it; every file uses huge pages (hugetlbpage.rst):
mount -t hugetlbfs -o pagesize=2M,size=2G,min_size=512M none /mnt/hugepagesize=selects which huge-page size (and thus which pool) this mount draws from.size=caps the bytes (or a percentage of the pool) the filesystem may consume; rounded down to a huge-page boundary.min_size=reserves a floor at mount time: that many pages are reserved immediately, and the mount fails if they are unavailable — useful to guarantee a service its memory up front.uid=/gid=/mode=/nr_inodes=set ownership, permissions (& 01777), and the inode cap.
A quirk worth knowing: hugetlbfs supports read() but not write() system calls, and mmap/munmap lengths must be huge-page aligned or they fail with EINVAL (hugetlbpage.rst). The classic consumer is libhugetlbfs, which can transparently back a program’s heap/BSS/text with hugetlbfs files.
2. mmap(MAP_HUGETLB) (anonymous, no mount)
The most common modern path needs no filesystem mount at all — pass MAP_HUGETLB to an anonymous mmap (mmap(2)):
void *p = mmap(NULL, 2UL * 1024 * 1024, // one 2 MiB page
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
-1, 0);To request a non-default size, OR in the encoded size. MAP_HUGE_2MB and MAP_HUGE_1GB are conveniences; generally you encode log2(size) in six bits at MAP_HUGE_SHIFT (= 26), so the constants are literally #define MAP_HUGE_2MB (21 << MAP_HUGE_SHIFT) and #define MAP_HUGE_1GB (30 << MAP_HUGE_SHIFT) (since Linux 3.8) (mmap(2)):
void *g = mmap(NULL, 1UL << 30, // one 1 GiB gigantic page
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB | MAP_HUGE_1GB,
-1, 0);A value of zero in the size field uses the default huge-page size (the Hugepagesize from /proc/meminfo). Available sizes are exactly the subdirectories of /sys/kernel/mm/hugepages.
The encoding is defined once, in include/uapi/asm-generic/hugetlb_encode.h, and shared verbatim by mmap, memfd_create, shmget and madvise — MAP_HUGE_SHIFT, MFD_HUGE_SHIFT and SHM_HUGE_SHIFT are all #defined to the same HUGETLB_FLAG_ENCODE_SHIFT:
#define HUGETLB_FLAG_ENCODE_SHIFT 26
#define HUGETLB_FLAG_ENCODE_MASK 0x3f /* six bits, so orders 0..63 */
#define HUGETLB_FLAG_ENCODE_2MB (21U << HUGETLB_FLAG_ENCODE_SHIFT)
#define HUGETLB_FLAG_ENCODE_1GB (30U << HUGETLB_FLAG_ENCODE_SHIFT)| Constant | log2(size) stored | Size | Where it applies |
|---|---|---|---|
…_ENCODE_16KB | 14 | 16 KiB | arm64 (16 KiB base pages) |
…_ENCODE_64KB | 16 | 64 KiB | arm64 contiguous-PTE, ppc |
…_ENCODE_2MB | 21 | 2 MiB | x86-64 PMD-level — the default |
…_ENCODE_16MB | 24 | 16 MiB | ppc64 |
…_ENCODE_512MB | 29 | 512 MiB | arm64 with 64 KiB base pages |
…_ENCODE_1GB | 30 | 1 GiB | x86-64 PUD-level — “gigantic” |
…_ENCODE_16GB | 34 | 16 GiB | ppc64 |
Walking the arithmetic: MAP_HUGE_2MB is 21 << 26, because 2 MiB is 2²¹ bytes and the six-bit field starting at bit 26 holds that exponent. MAP_HUGE_SHIFT = 26 and MAP_HUGE_MASK = 0x3f together carve bits 26–31 of the flags argument out for this purpose. Defining a new size therefore needs no new flag — you compute (log2(bytes) << MAP_HUGE_SHIFT) yourself. The constants are conveniences; the field is the interface. Note that the header defines encodings for sizes that no x86-64 kernel supports — the presence of MAP_HUGE_16GB does not mean your machine can allocate one. The authoritative list of sizes this kernel actually supports is, as stated, the set of directories under /sys/kernel/mm/hugepages/.
3. memfd_create(MFD_HUGETLB) (anonymous file)
memfd_create(name, MFD_HUGETLB) creates an anonymous file in hugetlbfs and returns a file descriptor backed by huge pages (since Linux 4.14) (memfd_create(2)). The same size-encoding applies via MFD_HUGE_2MB/MFD_HUGE_1GB. This is the descriptor you pass between processes (e.g. to a hypervisor or a sandbox) to share a huge-page-backed region — note that MFD_HUGETLB and MFD_ALLOW_SEALING could not be combined before Linux 4.16.
4. SysV shared memory (shmget/shmat)
shmget(..., SHM_HUGETLB) allocates a SysV shared segment from the huge-page pool — the oldest interface, used by databases (Oracle, older PostgreSQL setups) that already speak SysV shm.
Permissions
For the non-MAP_ANONYMOUS shared paths, an unprivileged user must either hold CAP_IPC_LOCK or be a member of the group configured in /proc/sys/vm/hugetlb_shm_group, otherwise MAP_HUGETLB/MFD_HUGETLB fail with EPERM (mmap(2), memfd_create(2)).
Reservation — the Concept hugetlbfs Exists For
Everything above is plumbing. Reservation is the idea. Strip it away and hugetlbfs is just “a page allocator that hands out 2 MiB pages”; add it and you get the one property that makes databases and hypervisors willing to depend on it: a fault on a hugetlb mapping cannot fail for lack of huge pages.
The kernel’s own design document is unusually candid about where the idea came from (Documentation/mm/hugetlbfs_reserv.rst):
“These huge pages are instantiated in a task’s address space at page fault time if the VMA indicates huge pages are to be used. If no huge page exists at page fault time, the task is sent a SIGBUS and often dies an unhappy death. Shortly after huge page support was added, it was determined that it would be better to detect a shortage of huge pages at
mmap()time.”
That sentence contains the whole design. There is no way to make a page fault wait for a huge page — huge pages are never swapped, never reclaimed, and assembling a new one needs physically contiguous memory that may simply not exist. So the kernel moves the failure earlier, to the one place where the caller can still do something about it: mmap() returns ENOMEM instead of the process taking a SIGBUS ten minutes later.
The critical distinction: reservation time is not allocation time
This is the single most-misunderstood thing about hugetlbfs, so state it flatly. A reservation is made at mmap() time. A physical page is handed over at fault time. These are different events, and between them the page exists, is idle, is counted in HugePages_Free, and is nevertheless untouchable by anyone else.
sequenceDiagram autonumber participant App as Application participant K as hugetlb core (mm/hugetlb.c) participant L as Global ledger<br/>(per-hstate counters) participant P as Huge page pool Note over L,P: start: Total=1024 Free=1024 Rsvd=0 App->>K: mmap(len=8 pages, MAP_HUGETLB) K->>K: hugetlb_reserve_pages(inode, from, to, vma, vm_flags) alt VM_NORESERVE set K-->>App: return success, NO reservation made Note over L: counters unchanged — fault may SIGBUS later else normal case K->>L: region_chg() then hugetlb_acct_memory(+8) L->>L: needed = (resv + 8) - free<br/>if needed <= 0: resv += 8 alt not enough free pages L-->>K: -ENOMEM K-->>App: mmap fails with ENOMEM else L-->>K: ok K-->>App: mmap succeeds Note over L,P: Total=1024 Free=1024 Rsvd=8<br/>NO physical page touched yet end end App->>K: first store to the mapping -> page fault K->>K: hugetlb_fault() -> alloc_hugetlb_folio() K->>L: vma_needs_reservation() -> 0 (reservation exists) K->>P: dequeue_huge_page_vma() P->>L: free_huge_pages--<br/>resv_huge_pages-- K->>K: folio_set_hugetlb_restore_reserve()<br/>then clear on instantiation K-->>App: fault resolved, cannot fail Note over L,P: Total=1024 Free=1023 Rsvd=7
The full lifetime of one reservation, from mmap to fault. What it shows: the two-phase commit at the heart of hugetlbfs. Step 4–8 happen inside mmap() and touch only counters — HugePages_Free does not move, because no physical page has been consumed. Steps 12–15 happen at the first store and move the page from “free-but-reserved” to “in use”, decrementing both free_huge_pages and resv_huge_pages in the same operation, which is why Rsvd falls as a program touches its memory. The insight to take: watch HugePages_Rsvd immediately after a service starts. If it is large and stays large, the service has reserved memory it is not using — and that memory is invisible to everything else on the box while also not appearing as “in use” in any obvious counter.
The reservation is recorded in a reserve map, a struct resv_map holding a list of struct file_region { long from; long to; } entries indexed in huge pages. Where it lives depends on the mapping type, and so does its polarity — a genuinely confusing inversion the design doc calls out explicitly:
MAP_SHARED (and hugetlbfs files, SysV shm) | MAP_PRIVATE | |
|---|---|---|
| Reserve map lives on | the inode (inode->i_mapping->private_data), created with the inode | the VMA (vma->vm_private_data), created at mmap time |
| An entry in the map means | a reservation exists (or existed) for that range | a reservation has been consumed for that range |
| Consuming a reservation | leaves the map unchanged | adds an entry |
| Who holds the guarantee | every process that maps the file | only the task that called mmap — flagged HPAGE_RESV_OWNER |
Survives fork() for the child | yes | no |
Read the middle row twice: for shared mappings, presence means reserved; for private mappings, absence means reserved. The inversion exists because a private map needs to track which reservations have been used up (so a second fault on the same address does not double-count), while a shared map needs to track which ranges are covered at all.
MAP_NORESERVE, overcommit, and why a process can still SIGBUS
Three distinct escape hatches let a hugetlb fault fail despite everything above, and conflating them is why the topic has a reputation for being confusing.
1. MAP_NORESERVE opts out entirely. hugetlb_reserve_pages() checks this before doing anything else (mm/hugetlb.c L6921–L6926):
/*
* Only apply hugepage reservation if asked. At fault time, an
* attempt will be made for VM_NORESERVE to allocate a page
* without using reserves
*/
if (vm_flags & VM_NORESERVE)
return true;It returns true — success — having made no reservation at all. The mmap succeeds unconditionally, and the fault becomes best-effort: it will try to take an unreserved free page or allocate a surplus page, and SIGBUS if neither is possible. This is the direct analogue of /proc/sys/vm/overcommit_memory for ordinary anonymous memory, and it is why the two get conflated — but note the crucial difference: vm.overcommit_memory and vm.overcommit_ratio have no effect on hugetlb whatsoever. hugetlb has its own, separate overcommit knob (nr_overcommit_hugepages), its own accounting, and MAP_NORESERVE is the only per-mapping opt-out.
2. Hole punching. fallocate(FALLOC_FL_PUNCH_HOLE) on a hugetlbfs file removes pages and the corresponding reserve-map entries. A subsequent fault on that range finds vma_needs_reservation() returning 1 — no reservation — and falls back to best-effort, even though the original mmap reserved the whole file.
3. Copy-on-write in a forked child. This is the subtlest one and the only case where a correctly written, fully reserved program can still die. A MAP_PRIVATE hugetlb mapping’s reservation belongs to one task, the HPAGE_RESV_OWNER. After fork(), parent and child share the pages read-only. When either writes, COW must allocate a fresh huge page — and there is no reservation for it, because mmap reserved only enough for one address space. If that allocation fails, the kernel must choose a victim, and it chooses in favour of the owner: it unmaps the page from the child, sets HPAGE_RESV_UNMAPPED on the child’s VMA, and lets the parent’s fault succeed. The child then dies if it ever touches that address, with a distinctive and searchable message (mm/hugetlb.c L6066–L6076):
if (is_vma_resv_set(vma, HPAGE_RESV_UNMAPPED)) {
pr_warn_ratelimited("PID %d killed due to inadequate hugepage pool\n",
current->pid);
goto out; /* ret was initialised to VM_FAULT_SIGBUS */
}If you ever see killed due to inadequate hugepage pool in dmesg, that is this exact path and nothing else. The defence, as LWN’s huge-pages series notes, is to mark hugetlb regions MADV_DONTFORK in advance, or to exec() immediately after forking (Corbet/Gorman, Huge pages part 1).
flowchart TB F["Huge page fault on a hugetlb VMA"] --> R{"Does a reservation<br/>cover this address?<br/>vma_needs_reservation()"} R -->|"yes (returns 0)"| TAKE["dequeue from pool:<br/>free_huge_pages--<br/>resv_huge_pages--"] TAKE --> OKF(["FAULT SUCCEEDS<br/>guaranteed"]) R -->|"no (returns 1)"| WHY{"why is there<br/>no reservation?"} WHY -->|"MAP_NORESERVE was passed"| BEST WHY -->|"hole punched / truncated<br/>via fallocate()"| BEST WHY -->|"COW in a forked child<br/>of a MAP_PRIVATE mapping"| COW BEST["best-effort: take from the<br/>unreserved surplus of the pool,<br/>or allocate a surplus page"] --> AVAIL{"pool has an<br/>unreserved free page?"} AVAIL -->|yes| OKF2(["fault succeeds"]) AVAIL -->|no| BUS(["SIGBUS — process killed"]) COW --> OWNER{"is the faulting task the<br/>HPAGE_RESV_OWNER?"} OWNER -->|"yes (the parent)"| UNMAP["unmap the page from the CHILD,<br/>set HPAGE_RESV_UNMAPPED on it,<br/>parent's fault succeeds"] UNMAP --> BUSC(["child SIGBUSes later<br/>if it touches that page"]) OWNER -->|"no (the child)"| TRY{"can a fresh huge page<br/>be allocated?"} TRY -->|yes| OKF3(["fault succeeds"]) TRY -->|no| BUS2(["child terminated,<br/>message logged to dmesg"]) style OKF fill:#2d6a4f,color:#fff style BUS fill:#9d0208,color:#fff style BUS2 fill:#9d0208,color:#fff style BUSC fill:#9d0208,color:#fff
Every path by which a hugetlb fault can end, including the three that end in SIGBUS. What it shows: the guarantee has exactly one precondition — that vma_needs_reservation() finds a reservation covering the faulting address. The entire left branch is the guaranteed path and is unconditional once taken. Everything on the right is best-effort, and the reader should be able to name which of the three entry conditions applies in any given incident. The insight to take: “hugetlbfs guarantees the allocation” is true of the reserved path only. A MAP_NORESERVE hugetlb mapping has strictly worse failure behaviour than ordinary anonymous memory, because ordinary memory can fall back to swapping and reclaim while huge pages can do neither.
A historical note that explains why so much folklore about this is wrong: the reservation rules have changed twice. In the earliest implementation, huge pages were faulted eagerly at mmap() time, which guaranteed everything but made mmap extremely expensive and behaved badly on NUMA. Since 2.6.18 faults became lazy, and reservations were made only for shared mappings — a private mapping could fail without fork() ever being involved. Only since 2.6.29 are reservations made for both shared and private mappings, which is the behaviour described throughout this note (Huge pages part 1). Any advice you read that says “private hugetlb mappings are not reserved” is describing a kernel from 2008.
The practical operator takeaway: a successful mmap(MAP_HUGETLB) without MAP_NORESERVE means later faults will not fail for want of huge pages — but it also means a process can reserve the entire pool and starve every other process before touching a single byte, which is exactly why HugePages_Free alone is a misleading indicator of availability and HugePages_Free − HugePages_Rsvd is the number to watch.
Gigantic Pages and CONFIG_CONTIG_ALLOC
A gigantic huge page is one whose page order exceeds the buddy allocator’s maximum. The kernel defines this exactly: hstate_is_gigantic(h) returns huge_page_order(h) > MAX_PAGE_ORDER (include/linux/hugetlb.h, v6.12). On x86-64 a 1 GiB page is order-18, well beyond the buddy allocator’s typical MAX_PAGE_ORDER of 10 (a 4 MiB max block). Because the buddy allocator cannot hand out a block that large, gigantic pages need a different source:
- Boot-time reservation (
hugepagesz=1G hugepages=Non the command line) is always available — at boot, memory is unfragmented and the kernel can simply set aside contiguous ranges. - Runtime allocation requires the contiguous-range allocator.
mm/hugetlb.c’salloc_gigantic_folio()is wrapped in#ifdef CONFIG_CONTIG_ALLOC; when that option is not built in, the function unconditionally returns NULL, and writingnr_hugepagesto grow a gigantic pool returns-EINVALfor the increase (you can still free boot-allocated gigantic pages) — verified directly in v6.12 mm/hugetlb.c (the comment reads: “Gigantic pages runtime allocation depend on the capability for large page range allocation”).gigantic_page_runtime_supported()further gates this per-architecture.
flowchart TB REQ["Admin wants N x 1 GiB pages<br/>huge_page_order = 18"] --> TEST{"hstate_is_gigantic(h)<br/>= huge_page_order(h) > MAX_PAGE_ORDER<br/>= 18 > 10"} TEST -->|"TRUE — buddy allocator<br/>structurally cannot supply it"| ROUTES TEST -->|"FALSE (e.g. 2 MiB = order 9)"| BUDDY["alloc_pages(order 9)<br/>ordinary buddy allocation"] subgraph ROUTES["Three ways to get memory the buddy allocator cannot hand out"] direction TB BOOT["1. BOOT-TIME RESERVATION<br/>hugepagesz=1G hugepages=N<br/>memory is unfragmented; kernel<br/>simply sets ranges aside<br/>ALWAYS WORKS"] CMAR["2. hugetlb_cma=SIZE<br/>CMA area reserved at boot;<br/>gigantic pages carved from it later.<br/>Region stays usable for MOVABLE<br/>pages until needed"] CONTIG["3. RUNTIME via CONFIG_CONTIG_ALLOC<br/>alloc_gigantic_folio()<br/>scans + migrates to assemble a range.<br/>Without CONFIG_CONTIG_ALLOC this<br/>returns NULL and growth gives -EINVAL"] end BOOT --> POOL["1 GiB pages in the pool"] CMAR --> POOL CONTIG --> POOL BUDDY --> POOL style BOOT fill:#2d6a4f,color:#fff style CONTIG fill:#7f5539,color:#fff
The three sources of gigantic pages, and the single predicate that forces the split. What it shows: hstate_is_gigantic() is a one-line test — huge_page_order(h) > MAX_PAGE_ORDER — and everything downstream of it exists because the answer is “true” for 1 GiB on x86-64. A 2 MiB page (order 9 ≤ 10) takes the left branch and is an ordinary buddy allocation like any other; a 1 GiB page (order 18) cannot be, at any level of free memory, on an unfragmented system, ever. The insight to take: “gigantic” is not a size category, it is a sourcing category. It names exactly the huge-page sizes whose order exceeds the buddy allocator’s ceiling, which is why the set of gigantic sizes changes by architecture — on arm64 with 64 KiB base pages, MAX_PAGE_ORDER is 13 and a 512 MiB page is order 13, so it is not gigantic there. Route 1 is the only one that always works, which is why production hosts reserve 1 GiB pools on the kernel command line.
To make runtime gigantic allocation reliable despite fragmentation, the kernel can reserve a Contiguous Memory Allocator (CMA) area for hugetlb at boot via the hugetlb_cma=<size> parameter (parsed by cmdline_parse_hugetlb_cma() in v6.12). Gigantic pages are then carved from this CMA region — and when hugetlb_cma_size is set, boot-time gigantic allocation is skipped in favor of CMA (v6.12 mm/hugetlb.c line ~3450). The demote/demote_size sysfs files let an admin split a gigantic page into the next-smaller huge-page size (e.g. one 1 GiB page → 512 × 2 MiB) without going back through the allocator (hugetlbpage.rst).
That “typical MAX_PAGE_ORDER of 10” is now pinned rather than assumed. include/linux/mmzone.h defines #define MAX_PAGE_ORDER 10 unless CONFIG_ARCH_FORCE_MAX_ORDER overrides it, and arch/x86/Kconfig does not define that symbol at all — verified by fetching the file at both the v6.12 and v6.18 tags and grepping for it (absent in both), with MAX_PAGE_ORDER 10 present in v6.12 mmzone.h and v6.18 mmzone.h alike. So on every x86-64 kernel, both LTS lines, the buddy allocator’s largest block is 2¹⁰ = 1024 pages = 4 MiB, and a 1 GiB page (order 18) is eight orders beyond its reach. The arithmetic is not close: the buddy allocator would need to be 256× more capable to produce one.
The same check on other architectures shows why the bound is not a universal constant: arch/arm64/Kconfig sets ARCH_FORCE_MAX_ORDER to 13 for 64 KiB base pages (a 512 MiB maximum block), and arch/powerpc/Kconfig sets 12 for 4 KiB-page PPC64. On those configurations fewer huge-page sizes count as “gigantic”. The full per-architecture table is in The Buddy Allocator.
HugeTLB Vmemmap Optimization (HVO)
Each huge page is still described by per-base-page struct page (struct page Anatomy) metadata in the vmemmap, which for a 1 GiB page is a large, mostly-redundant array. HVO (HugeTLB Vmemmap Optimization), enabled by CONFIG_HUGETLB_PAGE_OPTIMIZE_VMEMMAP and the hugetlb_free_vmemmap boot param, frees the redundant vmemmap pages, recovering memory (hugetlbpage.rst).
The redundancy is severe and the kernel’s own design note quantifies it exactly (Documentation/mm/vmemmap_dedup.rst). A 2 MiB huge page on x86-64 is 512 base pages, so it needs 512 struct page structures — which at 64 bytes each occupy 8 whole page frames of vmemmap. But “within the HugeTLB subsystem, only the first 4 struct page are used to contain unique information about a HugeTLB page” (the limit __NR_USED_SUBPAGE); the remaining 508 carry nothing but a compound_head pointer, and that pointer is identical in every one of them. So seven of those eight frames are byte-identical copies of each other.
flowchart LR subgraph BEFORE["BEFORE HVO — one 2 MiB huge page"] direction TB B0["vmemmap page 0<br/>holds the 4 struct pages that<br/>carry REAL information"] B1["vmemmap pages 1..7<br/>448 struct page structs whose<br/>only useful field is compound_head<br/>— and it is IDENTICAL in all of them"] end subgraph AFTER["AFTER HVO"] direction TB A0["vmemmap page 0<br/>kept, unchanged"] A1["PTEs for vmemmap pages 1..7<br/>REMAPPED to point at page 0<br/>(read-only)"] A2["the 7 physical page frames<br/>RETURNED to the buddy allocator"] A1 -.->|"all seven virtual pages<br/>now alias one frame"| A0 end BEFORE -->|"hugetlb_vmemmap_optimize()"| AFTER style A2 fill:#2d6a4f,color:#fff
HVO for one PMD-sized (2 MiB) huge page. What it shows: HVO does not delete metadata — it aliases it. The page-table entries covering vmemmap pages 1–7 are rewritten to map the same physical frame as page 0, read-only, and the seven now-unreferenced frames go back to the buddy allocator. Any virt_to_page() on a tail page still resolves; it just lands in the shared copy. The insight to take: the saving scales with huge-page size, and for 1 GiB pages it is dramatic. A 1 GiB page is 262,144 base pages → 4,096 vmemmap frames, of which HVO returns 4,095 — just under 16 MiB of struct page per gigantic page, about 1.6% of the page itself. On a host with a 512 GiB gigantic pool that is roughly 8 GiB of RAM recovered, which is why HVO exists at all.
Two consequences follow from the aliasing, both documented. First, because the tail vmemmap is now read-only and shared, freeing a huge page requires allocating seven fresh pages to un-alias it — so with HVO active, freeing huge pages “can fail… when the system is under memory pressure. Please try again later” (hugetlbpage.rst). Second, and following from the first, HugePages_Surp may briefly exceed its nominal maximum, because a surplus page that should have been returned could not be. On the machine examined in the live section, CONFIG_HUGETLB_PAGE_OPTIMIZE_VMEMMAP=y is built in but CONFIG_HUGETLB_PAGE_OPTIMIZE_VMEMMAP_DEFAULT_ON is not set, so HVO is available but off unless hugetlb_free_vmemmap=on is passed at boot — a distinction worth checking before assuming the saving is being taken.
Production Notes — Databases and Virtual Machines
PostgreSQL. Postgres backs its main shared-memory area (the buffer cache, shared_buffers) with hugetlbfs when huge_pages is set. The three values are precise: try (the default) attempts huge pages and silently falls back to base pages on failure; on requests them and prevents the server from starting if the pool is too small; off disables them. huge_page_size selects the size (0 = system default). It is “only supported when shared_memory_type is set to mmap” (PostgreSQL runtime config). The operational pattern is to size the pool to cover shared_buffers plus overhead, set it at boot, and use huge_pages = on in production so a misconfigured pool fails loudly rather than silently degrading. Notably, the same documentation discourages Transparent Huge Pages for PostgreSQL (“known to cause performance degradation… on some Linux versions”) — a sharp real-world example of why the explicit, reserved path exists.
QEMU / KVM. Hypervisors back guest RAM with hugetlbfs by pointing -mem-path (or memory-backend-file/memory-backend-memfd) at a hugetlbfs mount, frequently with 1 GiB gigantic pages so a multi-hundred-gigabyte guest is mapped by a handful of TLB entries. This is one of the largest wins for huge pages: with nested paging, both the guest and host translations benefit, and 1 GiB pages keep the (small) second-level TLB effective. Because the pool must be reserved before VM launch and 1 GiB pages need boot-time reservation or CMA, hypervisor hosts typically reserve the gigantic pool on the kernel command line.
DPDK and other userspace dataplanes likewise mount hugetlbfs and mmap huge pages to keep packet-buffer pools TLB-resident and to obtain physically-contiguous memory for DMA — the same explicit-reservation pattern.
Reading the hugetlb Surface on a Live System
Everything in this section was captured on Fedora Linux 44, kernel 7.1.13-200.fc44.x86_64, x86-64, 125 GiB RAM, on 2026-09-11, running unprivileged. Source citations remain pinned to v6.12; the sysfs and /proc layout described here is unchanged between them, and the gaps caused by lack of privilege are marked explicitly.
What sizes does this kernel support?
The authoritative list is the set of directories under /sys/kernel/mm/hugepages/ — one per hstate, the kernel’s per-size state structure:
$ ls -1 /sys/kernel/mm/hugepages/
hugepages-1048576kB/
hugepages-2048kB/
Two hstates: 1 GiB and 2 MiB, as expected for an x86-64 CPU with the pdpe1gb feature. Dumping every file in each:
-- /sys/kernel/mm/hugepages/hugepages-2048kB/
free_hugepages = 0
nr_hugepages = 0
nr_hugepages_mempolicy = 0
nr_overcommit_hugepages = 0
resv_hugepages = 0
surplus_hugepages = 0
-- /sys/kernel/mm/hugepages/hugepages-1048576kB/
demote = (Permission denied — mode 0200, root only)
demote_size = 2048kB
free_hugepages = 0
nr_hugepages = 0
nr_hugepages_mempolicy = 0
nr_overcommit_hugepages = 0
resv_hugepages = 0
surplus_hugepages = 0
The asymmetry between the two directories is the interesting part and is not documented as prominently as it deserves: the 2 MiB hstate has no demote or demote_size files at all. Demotion splits a huge page into the next-smaller supported size, so it only exists where there is a next-smaller size; 2 MiB is the smallest huge page on x86-64, so the files are simply not created. The 1 GiB directory’s demote_size = 2048kB says exactly what one 1 GiB page would become: 512 pages of 2 MiB. demote itself is write-only and root-only.
The pool, and the disjointness of the counters
$ grep -i -E 'huge|MemTotal' /proc/meminfo
MemTotal: 131150216 kB
AnonHugePages: 40960 kB
ShmemHugePages: 0 kB
FileHugePages: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 0 kB
This single block demonstrates the counter-disjointness claimed in the comparison table above. The hugetlb pool is completely empty — HugePages_Total: 0, Hugetlb: 0 kB — and yet AnonHugePages: 40960 kB shows 20 huge pages in active use on this machine right now. Those are Transparent Huge Pages, allocated on demand out of the buddy allocator’s order-9 free list, entirely invisible to the HugePages_* family. If you are diagnosing “are huge pages working?”, HugePages_* answers only the hugetlbfs half of the question; AnonHugePages / ShmemHugePages / FileHugePages answer the THP half.
The distribution has already mounted hugetlbfs even though the pool is empty:
$ grep -i huge /proc/filesystems /proc/mounts
/proc/filesystems:nodev hugetlbfs
/proc/mounts:hugetlbfs /dev/hugepages hugetlbfs rw,seclabel,nosuid,nodev,relatime,pagesize=2M 0 0
/dev/hugepages with pagesize=2M is systemd’s standard mount. Its presence proves nothing about pool size — a mount is a namespace for inodes, not an allocation.
The reservation semantics, reproduced
The pool is empty, so every reserved mmap must fail and every unreserved one must succeed. Running that experiment as an ordinary user:
mmap 2MiB MAP_HUGETLB -> MAP_FAILED, errno=12 (Cannot allocate memory)
mmap 2MiB MAP_HUGETLB|MAP_NORESERVE -> OK at 0x7f446c600000
mmap 1GiB MAP_HUGETLB|MAP_HUGE_1GB -> MAP_FAILED, errno=12 (Cannot allocate memory)
mmap 3MiB MAP_HUGETLB (unaligned len) -> MAP_FAILED, errno=12 (Cannot allocate memory)
mmap 2MiB plain anonymous (control) -> OK at 0x7f446c600000
Line by line. Line 1 is the guarantee working exactly as designed: hugetlb_reserve_pages() ran, gather_surplus_pages() computed needed = (0 + 1) - 0 = 1 > 0, could not allocate a surplus page because nr_overcommit_hugepages is 0, and hugetlb_acct_memory() returned -ENOMEM. The failure happened at mmap, before a single byte was touched. Line 2 is the entire MAP_NORESERVE story in one line — the identical call with one extra flag succeeds against a completely empty pool, because hugetlb_reserve_pages() takes its if (vm_flags & VM_NORESERVE) return true; early exit and never consults the ledger. Line 4 shows that an unaligned length does not produce EINVAL here: mmap rounds 3 MiB up to two huge pages and then fails on the pool, so the ENOMEM masks the alignment question entirely (the doc’s EINVAL warning is about munmap and about syscalls operating on existing hugetlb memory, not about mmap length rounding). Line 5 is the control: ordinary anonymous memory at the same address, no problem.
Now the payoff — touching the mapping from line 2, in a child process so the parent survives to report:
char *p = mmap(NULL, 2UL<<20, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB|MAP_NORESERVE, -1, 0);
/* ... mmap succeeded ... */
if (fork() == 0) { p[0] = 'x'; _exit(0); } /* one store */mmap(MAP_HUGETLB|MAP_NORESERVE) SUCCEEDED at 0x7fc4ec200000 with an EMPTY pool
child: killed by signal 7 (Bus error) on first store
That is the SIGBUS the whole reservation machinery exists to prevent, reproduced in eight lines of C on an unmodified system. The kernel had no huge page, could not swap anything out to make one, could not reclaim one, and had no reservation to fall back on — so the only remaining option was to kill the process at the faulting instruction. Compare the two outcomes directly: with reservation the program learns it cannot run at mmap, at startup, with a checkable return value; without reservation it learns at an arbitrary later instant, by dying. Everything in Documentation/mm/hugetlbfs_reserv.rst follows from wanting the first outcome instead of the second.
What could not be run here
Uncertain
Verify: the live reservation accounting in motion —
HugePages_Rsvdrising onmmapand falling as pages are faulted,HugePages_Surpappearing whennr_overcommit_hugepagesis raised, and the per-NUMA-nodefree_hugepagessplit. Reason: this session had no root and no passwordlesssudo(sudo -n true→ “a password is required”). Every write to the pool was refused —echo 1 > /proc/sys/vm/nr_hugepagesandecho 1 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepagesboth returnedpermission denied, andmount -t hugetlbfsreturned “must be superuser to use mount” — so the pool could never be made non-empty and the counters stayed at zero throughout. The empty-pool behaviour above (ENOMEM,MAP_NORESERVEsuccess,SIGBUS) is fully reproduced and verified; the non-empty-pool counter transitions are described from the v6.12 source andhugetlbfs_reserv.rst, not observed. To resolve, on a host with privilege:echo 4 > /proc/sys/vm/nr_hugepages, thengrep Huge /proc/meminfobefore and after anmmap(MAP_HUGETLB)of 2 pages and again after touching them —Rsvdshould go 0 → 2 → 0 whileFreegoes 4 → 4 → 2. uncertain
Failure Modes and Diagnosis
- Pool won’t grow to the requested size.
echo N > nr_hugepagesthengrep Huge /proc/meminfoshowsHugePages_Total < N: physical fragmentation prevented assembling contiguous runs. Fix: reserve at boot, or trigger compaction (echo 1 > /proc/sys/vm/compact_memory), or usehugetlb_cma=for gigantic pages. - App gets
ENOMEM/SIGBUSdespiteHugePages_Free > 0. The free pages are reserved by another mapping (HugePages_Rsvd), or are on the wrong NUMA node. Check per-nodefree_hugepagesandHugePages_Rsvd. EPERMonMAP_HUGETLB/MFD_HUGETLB. MissingCAP_IPC_LOCKand not inhugetlb_shm_groupfor the shared path.EINVALonmunmapof part of a huge page. The doc’s exact wording matters here, because it is weaker than it is usually quoted as: syscalls on hugetlb-backed memory “only have their lengths aligned to the native page size of the processor; they will normally fail with errno set to EINVAL or exclude hugetlb pages that extend beyond the length if not hugepage aligned. For example,munmap(2)will fail if memory is backed by a hugetlb page and the length is smaller than the hugepage size” (hugetlbpage.rst). So it is not a blanket “unaligned ⇒ EINVAL”: an unalignedmmaplength is simply rounded up (confirmed live above, where a 3 MiBMAP_HUGETLBrequest failed withENOMEMfor two huge pages, notEINVAL), whilemunmapof a sub-huge-page range is the case that actually fails.SIGBUSat the first store, withmmaphaving succeeded. Signal 7 on a hugetlb mapping means there was no reservation:MAP_NORESERVE, a punched hole, or a COW fault in a forked child. Checkdmesgforkilled due to inadequate hugepage pool, which pins it specifically to the COW-in-a-child case; its absence points atMAP_NORESERVEinstead. Reproduced live above.- Memory “leaks” / unswappable pressure. Reserved huge pages are pinned and never reclaimed — an oversized pool permanently removes RAM from everything else, which can itself push the system toward the OOM killer for normal allocations.
hugetlbfs versus Transparent Huge Pages, Mechanism by Mechanism
The two mechanisms produce the same hardware outcome — a PMD or PUD entry mapping a large physically contiguous region, saving TLB entries and page-walk levels — and share almost nothing else. This note owns the contrast; Transparent Huge Pages owns THP’s internals (khugepaged, mTHP, the sysfs surface, the split machinery) and Huge Pages Overview owns the decision framework for a given workload. What follows is the mechanism-level diff, which is what you actually need when debugging.
| Dimension | hugetlbfs | Transparent Huge Pages |
|---|---|---|
| Who decides | The administrator sizes a pool; the application explicitly opts in | The kernel, automatically; the application may hint with madvise |
| When the physical memory is allocated | At pool-set time — boot parameter or echo N > nr_hugepages. Pages are carved from the buddy allocator once and never returned (persistent pages) | At fault time, or later by khugepaged collapsing base pages. Every huge page is a fresh order-9 buddy allocation |
| Reservation | Yes — the whole point. mmap reserves; HugePages_Rsvd tracks the debt | None. The kernel doc states it “doesn’t require reservation to prevent hugepage allocation failures to be noticeable from userland” (transhuge.rst) — because there is nothing to fail |
| What happens when a huge page is unavailable | mmap fails with ENOMEM (reserved path), or the fault delivers SIGBUS (MAP_NORESERVE/COW path) | Silent, invisible fallback to 4 KiB base pages. The program never knows |
| Where failure surfaces | Early — at mmap, ideally at process startup | Never as an error; only as slower execution and a lower AnonHugePages in /proc/meminfo |
| Swappable / reclaimable | No. “Huge pages cannot be swapped out under memory pressure” (hugetlbpage.rst). Pool memory is removed from the system’s reclaimable pool entirely | Yes. THP pages are split back into base pages and paged out under pressure; v6.12 additionally splits “underused” THPs off the _deferred_list when shrink_underused is set |
| Memory that is mapped but untouched | Reserved and unusable by anyone else, but not yet physically consumed (Free stays high, Rsvd rises) | Physically consumed — mmap 2 MiB, touch 1 byte, and a full 2 MiB page may be allocated. The kernel doc calls this out as the reason madvise mode exists |
| Interface | MAP_HUGETLB (± MAP_HUGE_1GB), a hugetlbfs mount, MFD_HUGETLB, SHM_HUGETLB | No flags. madvise(MADV_HUGEPAGE) / MADV_NOHUGEPAGE are hints; prctl(PR_SET_THP_DISABLE) disables per-process |
| Permission required | CAP_IPC_LOCK or membership of vm.hugetlb_shm_group for the shared paths; root to size the pool | None |
| Page sizes | Every size in /sys/kernel/mm/hugepages/ — on x86-64, 2 MiB and 1 GiB | PMD-size only (2 MiB on x86-64) plus mTHP sizes below it; never 1 GiB for anonymous memory |
| Applies to | Only memory explicitly mapped through one of the four interfaces | Anonymous memory, tmpfs/shmem, and (increasingly) file-backed page cache |
/proc/meminfo counters | HugePages_Total/Free/Rsvd/Surp, Hugetlb | AnonHugePages, ShmemHugePages, FileHugePages — disjoint sets of counters, which is the fastest way to tell which mechanism a workload is actually using |
| Canonical users | PostgreSQL huge_pages=on, QEMU/KVM guest RAM, DPDK packet pools, Oracle SGA | Everything else: JVMs, Go runtimes, general-purpose servers |
| Failure you will actually debug | mmap: Cannot allocate memory at startup; killed due to inadequate hugepage pool in dmesg | Latency spikes from synchronous compaction at fault time; silently degraded TLB performance with no error anywhere |
Two rows deserve emphasis because they are where reasoning usually goes wrong.
“THP has no reservation” is a feature, not an omission. The kernel’s argument, stated directly in transhuge.rst, is that reservation wastes memory: THP “maximizes the usefulness of free memory if compared to the reservation approach of hugetlbfs by allowing all unused memory to be used as cache or other movable (or even unmovable) entities. … It allows paging and all other advanced VM features to be available on the hugepages.” A hugetlbfs pool of 64 GiB is 64 GiB the page cache can never borrow, even at 3 a.m. when the database is idle. THP gives that memory back. The price is precisely the guarantee.
The failure modes are opposites, and that is the real selection criterion. hugetlbfs fails loudly and early; THP fails silently and forever. If your service’s performance contract depends on huge pages, a silent fallback to base pages is the worst possible outcome — it degrades throughput by tens of percent with no log line, no counter that obviously moved, and no alert. That is why PostgreSQL offers huge_pages = on (refuse to start) as distinct from try (fall back quietly), and why its documentation simultaneously discourages THP (“known to cause performance degradation… on some Linux versions”) while recommending explicit huge pages (PostgreSQL runtime config). They are not alternative implementations of one feature; they are opposite answers to “what should happen when the kernel cannot deliver?”
They also coexist without interacting. A machine can run a 32 GiB hugetlbfs pool for a database and transparent_hugepage=madvise for everything else. The pool memory is simply gone from THP’s perspective — it is not in the buddy allocator any more — so the two never compete for the same pages after boot. The one shared resource is the buddy allocator’s supply of order-9 blocks at the moment you grow the pool, which is exactly why pools are provisioned at boot.
For a comparison of page-table-level representation, see Compound Pages and Large Folios (a huge page is a compound page); for the contiguous-memory machinery gigantic pages depend on, see Memory Compaction.
See Also
- Huge Pages Overview — the conceptual hub: why huge pages exist and how to choose between the two mechanisms.
- Transparent Huge Pages — the automatic, opportunistic counterpart to this explicit, reserved path.
- khugepaged and THP Collapse — THP’s background promotion thread (no hugetlbfs analogue; hugetlbfs is purely admin-driven).
- The Translation Lookaside Buffer and TLB Shootdowns — the finite hardware resource huge pages conserve.
- Memory Management Unit — the hardware that consults the TLB and walks page tables (ISA background).
- Linux Page Table Hierarchy — 2 MiB pages map at the PMD level, 1 GiB (gigantic) at the PUD level.
- NUMA Memory Model / NUMA Memory Policies — why per-node pools and
nr_hugepages_mempolicyexist. - Compound Pages and Large Folios — the
struct page/folio representation of a multi-page huge allocation. - Memory Compaction — assembles the contiguous runs the pool (and gigantic pages) need.
- The Buddy Allocator — the source of persistent/surplus pages, and the
MAX_PAGE_ORDERceiling that makes 1 GiB pages “gigantic.” - MOC: Linux Memory Management MOC — §12, Huge Pages and Transparent Huge Pages.