Before you start#
Read NUMA, huge pages, and memory placement. This chapter reads upstream Linux v6.12, a historical teaching baseline. Allocation context and virtual address behavior are architecture and configuration dependent; the source exercise works without building a kernel. The observation uses an unprivileged x86-64 guest, though the same Python program often works on arm64.
Learning goals#
Choose an allocator by object size, physical-contiguity requirement and execution context; trace a fallback; and explain who must free an object and when. Distinguish per-CPU placement from synchronization and userspace memory from kernel allocations.
Source walkthrough#
- kvmalloc request
Needs virtual contiguity; context governs fallback.
- kmalloc attempt
Success returns physically contiguous allocation.
- Fallback gate
Nonblocking flags or small failed request return NULL.
- vmalloc fallback
May use separate physical pages; release via kvfree.
A slab allocator keeps caches of objects, often of a fixed size. A struct kmem_cache describes a cache; a subsystem may create a dedicated cache for many objects of the same type. General-purpose kmalloc selects a size-class cache for an allocation. The returned object is at least the requested size; that does not grant permission to use bytes beyond the requested size. kzalloc requests the same kind of allocation with zero initialization. kfree releases a kmalloc-family allocation, and kmem_cache_free must receive the matching cache for an object obtained from that cache. See the interfaces and comments in include/linux/slab.h and implementation in mm/slab_common.c.
The size-class abstraction is not an unlimited physical allocator. kmalloc memory has a physically contiguous backing allocation, with practical limits and failure possibilities. vmalloc instead assembles pages into a virtually contiguous kernel address range: adjacent virtual addresses need not refer to adjacent physical frames. Mapping those pages costs page tables and other resources. A driver cannot assume a vmalloc address is suitable as one physically contiguous DMA buffer. DMA APIs, not virtual contiguity alone, determine what a device can address. mm/vmalloc.c implements these mappings.
kvmalloc is useful when the caller needs a virtually contiguous buffer but can accept either kind of backing. In mm/util.c, __kvmalloc_node_noprof, the first relevant fragment is (GPL-2.0; intermediate lines omitted):
ret = __kmalloc_node_noprof(PASS_BUCKET_PARAMS(size, b),
kmalloc_gfp_adjust(flags, size),
node);
if (ret || size <= PAGE_SIZE)
return ret;
First it tries the kmalloc path with adjusted flags. A successful result returns immediately; so does a failure for a request no larger than a page. The second fragment, later in the same function, controls fallback:
/* non-sleeping allocations are not supported by vmalloc */
if (!gfpflags_allow_blocking(flags))
return NULL;
Only after those checks and a size bound does this implementation call __vmalloc_node_range_noprof. Thus a large failed allocation with a nonblocking flag does not magically become a vmalloc allocation. Use kvfree for a kvmalloc result because the caller cannot tell which backing was chosen. A NULL result is a normal failure that must be handled before dereferencing the pointer.
- kmalloc try
__kmalloc_node_noprof runs first, with kmalloc_gfp_adjust(flags, size).
- Early return
A success, or a failure for size <= PAGE_SIZE, returns at once.
- Nonblocking stop
Without a blocking flag the function returns NULL before any vmalloc attempt.
- vmalloc fallback
__vmalloc_node_range_noprof gives virtually contiguous backing; release with kvfree.
Allocation context and lifetime#
GFP flags describe permitted allocator behavior. GFP_KERNEL permits blocking and reclaim in appropriate process context. GFP_ATOMIC is intended for contexts that cannot sleep and may access reserves, but allocation can still fail; it is not a general performance setting. Flags interact with filesystem and I/O recursion, reclaim and accounting; consult include/linux/gfp_types.h. Do not hold a spinlock and then invoke a potentially sleeping allocation. For an interrupt-facing object, preallocation or a bounded pool may be preferable to depending on reserves.
A per-CPU allocation gives each possible CPU its own instance, represented using special __percpu pointer annotations. Accessors select the intended CPU instance; the pointer is not simply an ordinary pointer to one shared object. A counter increment local to one CPU avoids much cache-line bouncing, but migration between selection and access, preemption, interrupts, remote readers, aggregation and CPU hotplug still matter. this_cpu operations and explicit CPU pinning have different contracts. alloc_percpu storage must outlive all users and be released by free_percpu only after callbacks and references have stopped; see mm/percpu.c.
For API-level details, distinguish a raw __percpu pointer from a normal dereference: use the documented this_cpu_*, get_cpu_ptr/put_cpu_ptr, or per_cpu_ptr access pattern appropriate to the operation. Pinning a task to a CPU only addresses migration during the protected interval; it does not by itself exclude interrupts or remote access. See the per-CPU access API and per-CPU allocator.
Consider a hypothetical statistics service with 64 CPUs and one 8-byte count per CPU. It needs 64 logical instances, not one shared integer. A local increment on CPU 3 changes that CPU's instance from 7 to 8; it does not update CPU 4's instance. A reader computing a total of 400 while updates continue obtains only the consistency guaranteed by its chosen read/synchronization scheme, not necessarily one atomic system-wide snapshot. At teardown, unregister producers, wait for any in-flight callbacks, then free the storage. Freeing it immediately after unregistering a producer is unsafe if a callback already held the pointer.
For a second hypothetical, ask kvmalloc for a 1 MiB table in sleepable context. If the contiguous allocation fails, the implementation may allocate a virtually contiguous fallback. If that also fails, unwind earlier resources and return an error. Neither successful path establishes a stable physical address or a DMA mapping. These are reasoned cases from the code, not measured outcomes.
Selecting an allocation and a release point#
The first question is what address property the consumer needs. A small control structure used only by CPU code is a plausible kzalloc object. Zeroing initializes bytes, not semantic invariants: a zeroed mutex, list head, reference counter or work item still needs its proper initializer. A dedicated kmem_cache can be useful for many same-type objects and cache-specific facilities, but a cache does not automatically solve lifetime races. The allocation size and object type must match every later access. Check size arithmetic before allocating an array: multiplying count by element size can overflow; kernel helpers such as kmalloc_array and kcalloc account for this risk.
In include/linux/slab.h, v6.12, SPDX GPL-2.0 (surrounding declarations omitted):
/**
* kcalloc - allocate memory for an array. The memory is set to zero.
* @n: number of elements.
* @size: element size.
* @flags: the type of memory to allocate (see kmalloc).
*/
#define kcalloc(n, size, flags) kmalloc_array(n, size, (flags) | __GFP_ZERO)
Notice that kcalloc is a macro over kmalloc_array, so the overflow check and the zeroing come from one shared helper. The excerpt does not show that helper's body or its failure return, and zeroing does not establish an object's invariants; the chapter's earlier point about initializers still applies.
For a large table whose consumer can use virtually contiguous bytes, kvmalloc can avoid insisting on a high-order physically contiguous allocation. Its caller must allow the possible vmalloc backing and use a compatible free. For a page-oriented algorithm, allocating individual pages can avoid pretending that the data is one linear physical region. The correct choice depends on what the consumer actually requires. For instance, a scatter-gather capable device can often use multiple mapped segments, but its DMA API, segment limits, direction and synchronization requirements must be honored.
A failure-safe constructor usually initializes resources in an order that permits the error path to undo only what succeeded. Suppose the object allocation succeeds and a later buffer allocation fails: release the object before returning the error. If a callback can observe the object, publish it only after initialization and remove it from lookup before final release. Unpublishing alone is insufficient when an already-running reader still owns a reference. The exact wait or reference primitive belongs to the subsystem contract. These rules apply equally if the allocation is zeroed: zeroing does not prevent double free or use after free.
NUMA placement is a preference governed by allocation flags, policy, availability and fallback; requesting a node does not mean every byte will always reside there under every possible operation. A large vmalloc range can be backed by multiple pages, potentially with different placement. If measuring locality, record the configuration and use appropriate instrumentation rather than deriving the answer from a pointer value.
Why GFP is part of the call contract#
A function's allocation flag is a statement about what the allocator may do on behalf of that caller. Reclaim may need to wait, invoke filesystem or I/O paths, or contend for resources. In a process-context setup function that owns no conflicting locks, waiting can be appropriate; under a spinlock, in a hard interrupt, or in another non-sleepable context it is not. A function that looks harmless in isolation can therefore be invalid when called from a different context. Read both the caller's locking and the allocator flags. On PREEMPT_RT some lock implementations and contexts differ, so do not infer every execution rule from an x86 non-RT experiment.
Consider a packet handler that must update a short per-CPU statistic and occasionally produce a larger report. A workable design may update a preallocated counter in the time-sensitive path, then schedule report construction in a context allowed to sleep. The design still needs a rule for CPU migration, aggregation, teardown and failed report allocation. It must also decide whether losing a sample is acceptable. Neither GFP_ATOMIC nor a workqueue automatically establishes those rules; they address only parts of the problem.
Zero-size and unusually large allocation requests have interface-specific behavior. Do not use the truthiness of an arbitrary allocation result as proof that it has room for one byte without reading that allocator's documented contract. Integer overflow is another distinct error: if a computed size wraps before it reaches the allocator, even a successful allocation can be too small for the subsequent copy. Kernel array-size helpers make the multiplication check explicit. When resizing, keep track of whether a failure leaves the old object valid under the specific resize API, and do not overwrite the only pointer to it prematurely.
The kernel's debugging facilities can expose some mistakes but are not an ownership protocol. A sanitizer may catch a use-after-free during a tested execution, yet an untested race can remain. The proof obligation is to identify the last possible user, stop or synchronize it, and use the matching deallocator after all references are gone. A memory leak is the opposite failure: ownership was never transferred or released on one path. Reading every goto cleanup label in a constructor is often more useful than reading only its successful return.
SLUB's per-CPU fast path#
Linux v6.12's SLUB implementation keeps a kmem_cache_cpu for a cache on each CPU. It contains a local freelist, a transaction identifier tid, the current slab, and, when CONFIG_SLUB_CPU_PARTIAL is enabled, a partial-slab pointer. A slab itself tracks its freelist, in-use count, object count and frozen state. In this version a frozen CPU slab is exempt from ordinary list management: its owning CPU allocates from it, while other CPUs can free objects back to the slab under the allocator's synchronization. A CPU partial slab is not the same as a frozen CPU slab. The node partial list has its own lock; a kref or an application lock does not substitute for these allocator locks. See the state and locking comments in mm/slub.c.
- kmem_cache_cpu
- Per CPU: a local freelist, the tid, the current slab and, when configured, a partial slab.
- Frozen CPU slab
- Its owning CPU allocates from it; other CPUs can free objects back under allocator synchronization.
- CPU partial slab
- Cached partial slabs under CONFIG_SLUB_CPU_PARTIAL; not the same state as a frozen CPU slab.
- Node partial list
- Guarded by its own lock; a kref or application lock does not protect it.
The v6.12 __slab_alloc_node reads per-CPU state before attempting its conditional fast path (GPL-2.0; surrounding code omitted):
c = raw_cpu_ptr(s->cpu_slab);
tid = READ_ONCE(c->tid);
The pointer selects a CPU's cache state; tid is sampled to detect a conflicting operation or migration before the atomic update. The code reads the freelist and current slab, and if the fast path is unavailable, the list is empty, or the slab does not match the requested node, it calls the slow allocator. When the supported lockless update fails, it retries. The tid check is not a general synchronization primitive for the allocated object's fields. PREEMPT_RT, debug caches, architecture support for the required compare-and-exchange, and configuration can force locking or other paths. Freeing uses do_slab_free or __slab_free according to slab and CPU state; after kfree, the caller must not read the object simply because a freelist still contains its bytes.
A cache constructor prepares objects according to the cache API when slabs are set up; do not assume it zeroes an object on every reuse. Initialize fields for each new logical owner. kcalloc(n, size, flags) uses checked array allocation and zeroing; check count/size overflow and allocation failure rather than multiplying in an unchecked expression. For zero-sized kmalloc requests, v6.12 defines ZERO_SIZE_PTR: it is not a usable one-byte object, can be passed to kfree, and must not be dereferenced. The exact helpers and contracts are in include/linux/slab.h.
vmalloc and per-CPU implementation boundaries#
The vmalloc implementation reserves virtual address ranges represented by vmap_area/vm_struct, obtains backing pages and installs kernel page-table mappings. The vm_struct records an array of pages and its size; virtual contiguity does not make those pages one physical extent. Unmapping and virtual-address reclamation can involve lazy handling and TLB flushes. A caller cannot retain a pointer after vfree just because a virtual area may be reclaimed lazily. Other references can extend the backing pages' lifetime, but they do not make the old vmalloc address valid.
In v6.12, vfree may sleep in ordinary process context and must not be called from NMI. Its interrupt-context branch defers freeing through vfree_atomic; vfree_atomic is documented for atomic contexts except NMI and schedules deferred work. This is more precise than a blanket statement that freeing vmalloc memory always sleeps or is always safe in every atomic context. The caller must still prevent outstanding users before the free and choose the API appropriate to its context. See mm/vmalloc.c.
In the same file, v6.12, SPDX GPL-2.0-only (the NMI comment and the rest of the function omitted), vfree begins with its interrupt-context branch:
void vfree(const void *addr)
{
struct vm_struct *vm;
int i;
if (unlikely(in_interrupt())) {
vfree_atomic(addr);
return;
}
Notice that the deferral depends on the context at the call, not on the allocation's flags. The excerpt does not show the process-context path after the check, where might_sleep() applies, nor what vfree_atomic does with the address later.
The per-CPU allocator has a first chunk established during setup and can allocate dynamic chunks as needed; exact embedding, population and layout depend on architecture and configuration. A per-CPU pointer denotes instances selected using per-CPU accessors, not one normal shared array. A multi-step read/update must prevent or accommodate migration as required by its accessor and locking protocol; local interrupts, remote readers and CPU hotplug can also matter. Even if each CPU updates only its own counter, a sum collected while updates continue is not automatically one atomic snapshot. mm/percpu.c implements the chunk and balancing machinery.
Read-only observation: awk '/^(Slab|SReclaimable|SUnreclaim|VmallocTotal|VmallocUsed|Percpu):/ {print}' /proc/meminfo displays aggregate kernel accounting fields when exposed. Their units are reported by the file, commonly kB; fields and values depend on the running kernel. Slab is not the size of one named cache, SReclaimable is not memory guaranteed to be freed immediately, and VmallocUsed does not reveal whether one particular allocation is physically contiguous. A missing field or inaccessible procfs is an expected limitation. This complements the existing userspace mmap example, which does not exercise kmalloc or prove SLUB behavior.
Practice#
Source reading: with a v6.12 checkout at $KERNEL_SRC, run git -C "$KERNEL_SRC" describe --tags --exact-match and rg -n -A35 'void \*__kvmalloc_node_noprof' "$KERNEL_SRC/mm/util.c". Expect v6.12, then locate the kmalloc attempt, the blocking check and the vmalloc return. Also run rg -n -A65 'static __always_inline void \*__slab_alloc_node' "$KERNEL_SRC/mm/slub.c" and rg -n -A35 'static __always_inline void do_slab_free' "$KERNEL_SRC/mm/slub.c"; locate the per-CPU fast-path check and slow/free fallback. Record which branch makes a nonblocking vmalloc fallback fail. The lines are source evidence, not a prediction that your machine's next allocation will follow that branch.
Guest observation: inside the unprivileged disposable guest, run:
python3 - <<'PY'
import mmap
p = mmap.mmap(-1, 1048576)
p[0:4] = b'LAB!'
print(len(p), p[0:4])
p.close()
PY
Expected: 1048576 b'LAB!'. This demonstrates a userspace virtual mapping and a written value, not kmalloc, kvmalloc, physical contiguity, or DMA. If Python reports MemoryError or OSError, record the failure and available guest memory rather than claiming the mapping exists. The process exits and releases its mapping; no host change is needed. Deliver a table selecting an allocator for a small zeroed object, a large virtually contiguous table, and per-CPU counters, with allocation context and release method for each.
Knowledge check#
-
What does the fallback in
__kvmalloc_node_noprofdo when the initial allocation fails and the flags cannot block? -
Why is
kvfreeappropriate for the returned pointer? -
Why does per-CPU placement not by itself guarantee a coherent total?
-
Can a vmalloc buffer be handed to a device merely because its virtual bytes are adjacent?
-
What does the SLUB transaction identifier protect, and can a constructor replace per-allocation initialization?
Answers#
-
It returns
NULLbefore the vmalloc fallback; the caller must handle failure. -
The result may be backed by either allocator;
kvfreehandles that distinction. -
Writers can update different instances while a reader sums them. Migration and concurrent remote access also require the relevant access discipline.
-
No. Virtual adjacency does not prove physical or device-address adjacency; the driver must use the appropriate DMA mapping contract.
-
The
tidhelps detect a conflicting per-CPU freelist operation or migration before the fast-path update. It does not protect the allocated object's fields, and a cache constructor is not guaranteed to zero an object on every reuse.
Checkpoint#
Keep the allocator table and source-branch note. Pass when all three choices include context, a matching free, a failure response and the physical-contiguity assumption. Revisit the fallback and GFP definitions, correct missing assumptions and retry; this is practice, not certification.