What is Slab: Where do kernel small objects come from?
The previous articles have already mentioneduser address space, VMA, page table, struct page, zone and buddy allocatorStringed together.
The buddy allocator solves the problem of "taking out consecutive page blocks such as 1 page, 2 pages, and 4 pages from physical memory." But many of the things that the kernel allocates daily are not of full page size:
- one
vm_area_struct. - one
task_struct. - A dentry.
- an inode.
- A temporary structure of tens of bytes.
If these small objects directly ask buddy for 4KB pages, there will be two obvious problems:
- Waste: tens of byte objects occupy one page, and the internal fragmentation is too large.
- Slow: Frequent buddy entry, frequent page block splitting, lock contention and cache locality are not good.
So this article answers a question closer to the kernel itself:
Buddy allocates pages. Where do the large number of small objects of tens or hundreds of bytes in the kernel come from?
The answer is slab allocator. More precisely,slab is the collective name for a class of small object allocators; a common implementation in modern Linux is SLUB. You will also see the names SLAB and SLOB in old materials. To understand the main thread mechanism, we can first focus on these three levels of relationships:
buddy allocator
│
│ Allocate one or more pages
▼
slab page / slab folio
│
│ Cut into fixed size objects
▼
kmem_cache
│
│ Allocate the same object to the kernel
▼
task_struct / vm_area_struct / dentry / kmalloc-*
Here is a general judgment:
User data pages usually come from buddy; kernel small objects are usually fetched from the slab cache first. When there are no free objects in the corresponding slab cache, new pages are requested from buddy.
1. Why buddy is not suitable for small objects
The basic unit of buddy allocator is page. Assuming the page size is 4KB,order=0 is 1 page,order=1 is 2 pages,order=2 It's 4 pages.
This is good for allocating user data pages, page table pages, large contiguous pages:
alloc_pages(order=0) -> 1 page -> 4KB
alloc_pages(order=1) -> 2 pages -> 8KB
alloc_pages(order=2) -> 4 pages -> 16KB
But if the kernel only wants a 152-byte or so vm_area_struct, it would be a waste to just take one page:
4KB page
┌──────────────┬──────────────────────────────────────┐
│ 152B object │ The remaining space is wasted if it is not used to store other objects. │
└──────────────┴──────────────────────────────────────┘
A better way is to take one or several pages from buddy at a time, and then cut this page into many objects of equal size.
4KB page
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ obj │ obj │ obj │ obj │ obj │ obj │ ... │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘
This is the basic motivation of slab allocator:Convert page-level allocation to object-level allocation.
2. Slab, SLAB, and SLUB are not the same concepts.
Many articles use slab, SLAB, and SLUB interchangeably. Be careful when reading kernel information:
- slab allocator: Common name, referring to the small object allocation system of the Linux kernel.
- SLAB: An earlier concrete implementation with a more complex structure and retains more queues and metadata.
- SLUB: A common implementation in modern Linux. The design goal is to reduce metadata and lock competition, and the fast path is more direct.
- SLOB: Old small system implementation, oriented to very small memory environments; it should no longer be regarded as a regular path in modern mainline materials.
For this series, it is not necessary to expand every field of SLAB and SLUB. What really matters are these abstractions:
kmem_cache A cache of objects, such as vm_area_struct cache
slab One or more pages that host a set of objects
object The small object actually returned to the kernel caller
per-cpu freelist each CPU Priority local free object list
partial slab Some objects have been allocated and some objects are still free. slab
In modern kernels, the internal implementation of SLUB uses folio, per-node partial list, per-cpu status and other structures. Let’s not get bogged down by field names here, just grasp the responsibilities:
kmem_cache Regardless of a kind of object, slab page carries a batch of objects, and freelist strings together free objects that can be quickly allocated.
three,kmem_cache: One object per cache
There are many objects in the kernel that "appear in large numbers for a long time and have a fixed size". Such as VMA objects, process objects, directory entry objects. Building special caches for these objects has several benefits:
- The object size is fixed and segmentation is simple.
- Similar objects are reused and cache locality is better.
- Construction/debug/alignment information can be preserved.
- After release, it may not be returned to the buddy immediately. It can be kept in the cache for reuse next time.
so kmem_cache It can first be understood as a "kernel object pool". A cache usually serves a fixed-size kernel object, or a fixed-size general purpose object. kmalloc distribute.
Let’s start with this section kmem_cache Let’s break it down into several pieces and look at it: first look at it as a static entrance to the object pool, then look at the several states of the slab, and finally look at the per-CPU and per-node two-tier backup pools.
3.1 kmem_cache static entry
The picture below specifically shows:There is a dedicated management struct vm_area_struct Object kmem_cache. It manages the VMA descriptor itself, which is the metadata object in the kernel; it does not manage the user data page covered by this VMA.
kmem_cache: vm_area_struct
│
├─ object size: sizeof(struct vm_area_struct)
├─ alignment
├─ flags / debug / accounting
├─ CPU0 freelist -> obj -> obj -> obj
├─ CPU1 freelist -> obj -> obj
└─ partial slabs
│
▼
slab page
┌─────┬─────┬─────┬─────┬─────┐
│ obj │ obj │ obj │ obj │ obj │
└─────┴─────┴─────┴─────┴─────┘
Take it apart according to the implementation of Linux 6.12 SLUB.kmem_cache You are the main entrance to this object pool. It contains global properties such as object size, alignment, constructor, cache name, etc., as well as per-CPU and per-node status:
struct kmem_cache
├─ name -> cache name, e.g. "vm_area_struct"
├─ object_size -> The size of the object that the caller really needs
├─ size -> plus alignment/Object slot size after metadata
├─ offset -> free object Li Cun next pointer position
├─ flags / align / ctor / allocflags ...
│
├─ cpu_slab -> per-CPU of struct kmem_cache_cpu
│ │
│ ├─ CPU0: freelist / active slab / partial
│ ├─ CPU1: freelist / active slab / partial
│ └─ ...
│
└─ node[] -> each NUMA node of struct kmem_cache_node
│
├─ partial slab list
└─ list_lock / nr_partial
3.2 partial, full and empty are just slab states
Handy here partial slab Make it clear. Some objects in a slab have been allocated, and some objects are still free. It is a partial slab:
partial slab
┌──────┬──────┬──────┬──────┬──────┐
│ used │ free │ used │ free │ used │
└──────┴──────┴──────┴──────┴──────┘
It is not a new memory area, but a state of slab. SLUB cares about partial slab because this kind of slab can continue to provide objects, so it will be hung on the standby linked list:
kmem_cache_cpu->partial -> per-CPU of partial slab Standby linked list
kmem_cache_node->partial -> per-node of partial slab Standby linked list
Where are the empty and full slabs hung?
full slab
└─ All objects have been allocated. ordinary SLUB The fast path usually does not need to be linked to an allocable linked list;
Because it has no free objects, it will not be found first when allocating.
turn on SLUB debug It may be maintained while waiting for configuration full list Easy to inspect and traverse.
empty slab
└─ All objects are free. It may be retained for reuse, or it may be returned when policy allows. buddy;
In ordinary paths, it usually does not look like partial slab In this way, it will be hung on "Continuously Allocation" for a long time. partial list superior.
So don't put partial/full/empty Think of it as three completely equal resident linked lists. The most critical thing for ordinary SLUB allocation paths is partial list: It saves slabs that "still have free objects and are worthy of continued allocation". The full slab has no allocable objects, and the empty slab may be recycled or reused as the current slab.
3.3 per-CPU and per-node are two different levels of spare pools
Here's kmem_cache_cpu Not another kind of object cache, butsomeone kmem_cache Fast path status on a CPU. it hangs on kmem_cache->cpu_slab Below this per-CPU pointer. Looking at the fields of Linux 6.12, it can be simplified to:
struct kmem_cache_cpu
├─ freelist -> current CPU next directly assignable free object
├─ tid -> Cooperate freelist Do lock-free cmpxchg renew
├─ slab -> current CPU being allocated active slab
├─ partial -> current CPU cache some partial slabs
└─ lock -> Use the slow path when modifying these fields
The problem it solves is: if each CPU grabs the same global lock and the same global freelist, small object allocation will be very slow. So SLUB prepares per-CPU status for each cache, so that the current CPU can directly access it from its own cache most of the time. kmem_cache_cpu->freelist Pop object.
Then why the same kmem_cache already there kmem_cache_cpu, and there are kmem_cache_node? Because they solve problems at two different levels:
kmem_cache
└─ Manage global specifications and entries for "objects of this type"
kmem_cache_cpu
└─ Regardless of "a certain CPU What is the fastest way to distribute currently?”
kmem_cache_node
└─ Regardless of "a certain NUMA node What are the reusable slab”
kmem_cache_cpu It is a CPU local cache layer that is biased toward “current use and immediate use”. inside it slab is the active slab currently being allocated by the CPU.partial Is a small list of spare partial slabs cached next to the current CPU.
kmem_cache_node It is a NUMA node-level shared spare pool, biased towards "reusable on this node". its partial The linked list hangs the partial slabs on this node, which are shared by multiple CPUs on this node in the slow path.
So they both refer to slab, but have different meanings:
kmem_cache_cpu->slab
└─ current CPU being allocated active slab, usually one
kmem_cache_cpu->partial
└─ current CPU Locally cached fallback partial slabs, Can be multiple
kmem_cache_node->partial
└─ current NUMA node public partial slab Pool, give multiple CPU slow path sharing
When will we get to node-level partial list? Usually the CPU local layers are not enough:
1. kmem_cache_cpu->freelist Empty
2. current kmem_cache_cpu->slab Can't find available objects
3. kmem_cache_cpu->partial No suitable backup slab
4. Just went kmem_cache_node->partial look for this NUMA node on partial slab
5. if node partial No, no more buddy allocator To create a new page, create a new slab
kmem_cache_cpu->partial Although it is also a backup pool, it is not infinite and does not necessarily exist. For example, it is not turned on CONFIG_SLUB_CPU_PARTIAL, the current CPU local cache is just empty, the number of caches reaches the limit and is drained back to the node, NUMA node does not match,pfmemalloc If the attributes do not match, the allocation path will continue to fall back to kmem_cache_node->partial.
In short:kmem_cache_cpu->partial Is the small spare pool of the current CPU;kmem_cache_node->partial Is the public standby pool of the current NUMA node. When the CPU local pool is empty, inappropriate, not enabled, or restricted, the node-level slow path will be taken.
3.4 Special object cache and general size cache
There are many similar caches, which can be roughly divided into two categories.
The first type is a dedicated object cache. One cache corresponds to a common kernel structure:
vm_area_struct -> manage struct vm_area_struct, Describes a virtual address range of a process
task_struct -> manage struct task_struct, describe a Linux task / thread
dentry -> Manage directory entry cache objects, connection pathnames and inode
inode_cache -> manage VFS inode Object describing file metadata in the file system
files_cache -> Management process opens file table related structures
filp -> manage struct file, Corresponds to one open file instance
buffer_head -> Manage block device buffer related metadata
skbuff_head_cache -> Manage network packages skb Related objects
The second category is a general size cache, service kmalloc(size) This type of small block allocation is not bound to a specific structure type:
kmalloc-8
kmalloc-16
kmalloc-32
kmalloc-64
kmalloc-128
kmalloc-256
kmalloc-512
kmalloc-1k
kmalloc-2k
The static structure of the universal size cache is very similar to that of the special object cache, except that it is not bound to a specific structure type, but to a size range. by kmalloc-64 For example:
kmalloc-64 cache
│
├─ object size: 64 bytes
├─ alignment / flags / debug / accounting
├─ CPU0 freelist -> 64B obj -> 64B obj -> 64B obj
├─ CPU1 freelist -> 64B obj -> 64B obj
└─ partial slabs
│
▼
slab page
┌────┬────┬────┬────┬────┬────┬────┬────┐
│64B │64B │64B │64B │64B │64B │64B │64B │
├────┼────┼────┼────┼────┼────┼────┼────┤
│64B │64B │64B │64B │64B │64B │64B │64B │
└────┴────┴────┴────┴────┴────┴────┴────┘
If the page size is 4096 bytes, ideally kmalloc-64 A 4KB slab page can be cut out 4096 / 64 = 64 objects. You can also see this in the experiments later in this article:
kmalloc-64 objsize=64 objperslab=64 pagesperslab=1
Let’s look at a slightly larger example. In the experimental output kmalloc-512 yes:
kmalloc-512 objsize=512 objperslab=32 pagesperslab=4
This means that a slab of this cache consists of 4 pages:
4 * 4096 = 16384 bytes
16384 / 512 = 32 objects
That is:A slab consists of 4 physical pages, with a total of 32 512-byte object slots cut out in it.. Expanded by page, there are 8 objects per page:
kmalloc-512 one of slab
page 0 page 1 page 2 page 3
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ 8 indivual 512B obj │ 8 indivual 512B obj │ 8 indivual 512B obj │ 8 indivual 512B obj │
└──────────────┴──────────────┴──────────────┴──────────────┘
total: 4 pages * 8 objects/page = 32 objects
Expand it a little more carefully:
4-page slab for kmalloc-512
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│512B │512B │512B │512B │512B │512B │512B │512B │ page 0
├──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┤
│512B │512B │512B │512B │512B │512B │512B │512B │ page 1
├──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┤
│512B │512B │512B │512B │512B │512B │512B │512B │ page 2
├──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┤
│512B │512B │512B │512B │512B │512B │512B │512B │ page 3
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
There are still limitations to be added here: This is the layout on the x86_64 test kernel this time in this article. Change kernel version, configuration, debugging options or architecture,kmalloc-512 of pagesperslab and objperslab All may be different.
So the essence of a universal size cache can be summarized as:
kmalloc-N cache
│
├─ Each object slot is fixed to N bytes
├─ As long as the caller does not exceed N bytes, Just put it in
└─ What structure is in the slot? It is determined by calling kmalloc() The kernel code determines
For example, a certain kernel code calls kmalloc(80, GFP_KERNEL), it is not necessarily worth building a separate dedicated cache, SLUB usually puts it into a general-sized cache that can accommodate 80 bytes. Specifically fall into kmalloc-96, kmalloc-128 Or something else, depending on the kernel configuration, architecture and cache merge strategy.
What these two types of cache have in common is that they allocateKernel objects in the kernel address space, not user mode malloc(64) Those 64 bytes returned to the user program. User-mode small objects are split in the user virtual address space by libc allocator; the kernel slab manages the kernel's own metadata and temporary objects.
When the kernel code needs a VMA object, it does not hand-write "cut a piece from which page and which offset", but allocates it through the corresponding cache:
kmem_cache_alloc(vm_area_struct_cachep, GFP_KERNEL)
When the kernel requires a small block of memory of a common size, a common entry point is:
kmalloc(size, GFP_KERNEL)
kmalloc(64), kmalloc(512) This type of general allocation usually falls behind kmalloc-64, kmalloc-512 This type of general slab cache. That is to say,kmalloc Not another set that bypasses slab; small size kmalloc It relies on slab/SLUB to provide objects.
Special object:
alloc_vm_area_struct -> kmem_cache_alloc(vm_area_struct cache)
universal nugget:
kmalloc(80) -> Choose the appropriate size level -> kmalloc-96 / kmalloc-128 wait cache
The specific cache name will be affected by the kernel version, configuration, debugging options and cache merge. Do not regard the name on a certain machine as the ABI.
4. How is the special object cache initialized?
Have been using it before vm_area_struct cache, dentry cache This statement. So when and how did these special object caches appear?
Let’s give the conclusion first:
Special object caches are usually called by the corresponding subsystem during the initialization phase. kmem_cache_create() Create it; this call first creates a cache description of "how this kind of object should be managed." The slab page that actually carries the object often has to wait for the first allocation or when the cache needs to supplement the object, then apply for the page from the buddy allocator and cut it into objects.
In other words, the initialization process is divided into two levels:
First level: Create kmem_cache
└─ Establish object specifications: name, object size, alignment, flags, ctor, CPU/node state
Second level: Create slab page
└─ cache When there are no available objects in the buddy To page, initialize the page to slab, Cut into objects
Do not mix these two layers together.kmem_cache_create() What is created is a "management entrance to the object pool"; it does not necessarily pre-allocate all objects immediately.
4.1 The subsystem first declares "I want an object pool"
Taking the VMA as an example, the kernel requires a large number of struct vm_area_struct. This type of object has a fixed size, frequent life cycle, and is likely to be reused after release, so it is suitable for a dedicated cache.
When initializing, the relevant code will do something like this:
vm_area_struct cache =
kmem_cache_create(
name = "vm_area_struct",
object_size = sizeof(struct vm_area_struct),
align = Alignment requirements,
flags = debug/Recycle/RCU/accounting Waiting for signs,
ctor = optional constructor
)
This is not allocating a VMA object, but telling SLUB:
In the future, if anyone wants struct vm_area_struct,
Please manage it by this size, this alignment, these rules.
Similarly, dentry, inode,struct file, task_struct common objects, etc., will also create their own dedicated kmem_cache. The names of these caches usually also appear in /proc/slabinfo inside.
4.2 kmem_cache_create() Calculate object layout first
When creating a dedicated cache, SLUB must first convert the "object size in the eyes of the caller" into the "object slot size actually used within the slab".
These two sizes are not necessarily the same:
object_size
└─ The size of the structure that the caller really needs, for example sizeof(struct vm_area_struct)
size
└─ slab in each object slot The actual span of,
May contain alignment, freelist pointer position, redzone, poison, KASAN Wait for additional expenses
For example, if a structure itself is 152 bytes, SLUB may expand the slot to 160 bytes due to alignment; if the debugging option is turned on, it may be even larger. After the slot size is determined, SLUB can continue to calculate:
one slab how many pages to use?
How many objects can be placed on one or more pages?
free object inside next Where is the pointer placed? offset?
this cache Can it have the same specifications as others? cache merge?
so kmem_cache It’s not just a matter of remembering one sizeof(type). It saves a complete set of object layout and allocation strategies:
struct kmem_cache
├─ name
├─ object_size
├─ size
├─ align
├─ offset -> free object Inside next pointer position
├─ flags
├─ ctor
├─ cpu_slab -> per-CPU fast path status
└─ node[] -> per-node partial slab pool
After this step is completed, the cache already "exists". Even if it does not have any slab page yet, it already knows how to create slabs, how to cut objects, and how to release objects in the future.
4.3 Reinitialize per-CPU and per-node status
A dedicated cache cannot have only global specifications. In order to make allocation fast, it also needs to prepare the CPU and NUMA nodes.
To simplify it, these management structures will be prepared when creating the cache:
kmem_cache: vm_area_struct
│
├─ cpu_slab
│ ├─ CPU0: freelist = NULL, slab = NULL, partial = NULL
│ ├─ CPU1: freelist = NULL, slab = NULL, partial = NULL
│ └─ ...
│
└─ node[]
├─ node0: partial list = empty
└─ node1: partial list = empty
When first created, these linked lists are usually empty:
not yet active slab
not yet per-CPU freelist
not yet per-node partial slab
This is normal. Because only the "warehouse rules" and "shelf locations" have been established at this time, the goods may not come in yet.
4.4 It is possible to ask buddy for a page only when it is allocated for the first time.
When someone actually allocates a VMA object later, it will go to this cache:
kmem_cache_alloc(vm_area_struct cache)
If this is a newly created cache with no objects yet, the path will go all the way to the slow path:
1. current CPU freelist is empty
2. current CPU active slab is empty
3. current CPU partial list is empty
4. current node partial list is empty
5. Towards buddy allocator Request one or more pages
buddy only returns pages, it does not know whether these pages will contain VMA or dentry in the future:
buddy allocator
└─ return 1 page or multiple consecutive physical pages
After SLUB gets these pages, it initializes them into slabs belonging to this cache:
new page / new folio
│
├─ marked as slab
├─ Record slab_cache = vm_area_struct cache
│ That is, "specially allocated struct vm_area_struct the object of kmem_cache”
├─ calculate objects quantity
├─ initialization inuse / frozen Waiting state
└─ String the object slots in the page into freelist
Expanded into a diagram:
kmem_cache_alloc(vm_area_struct cache)
│
▼
cache There are no objects available in
│
▼
alloc_pages()
│
▼
buddy Return a set of pages
│
▼
SLUB Initialize this set of pages to slab
│
▼
according to cache->size cut into object slots
│
▼
Bundle free objects strung together freelist
│
▼
return one of struct vm_area_struct *
So the complete build relationship from top to bottom is:
Subsystem initialization
│
└─ kmem_cache_create("vm_area_struct", sizeof(struct vm_area_struct), ...)
│
├─ create / initialization struct kmem_cache
├─ Calculate object slot layout: size, align, offset, objects per slab
├─ initialization per-CPU state: kmem_cache_cpu
└─ initialization per-node state: kmem_cache_node
first or subsequent allocation
│
└─ kmem_cache_alloc(vm_area_struct cache)
│
├─ Check first per-CPU freelist / partial slab
├─ Check again per-node partial slab
└─ There is no time buddy Page
│
└─ initialization struct slab / slab folio
│
├─ cut into multiple vm_area_struct object slot
├─ string together free object freelist
└─ Return an object to the caller
There is another detail here: some caches or subsystems may actively warm up and retain a certain number of objects after initialization, and some boot stages also have special paths. But to understand the main line, you can first remember it according to this model:
kmem_cache_create()
└─ Create object pool rules and management portals
kmem_cache_alloc()
└─ Only consume existing objects when they are needed freelist, or to buddy To create a new page slab
4.5 Initialization differences between special object cache and general kmalloc cache
Private object caches are explicitly created by a subsystem:
VMA Subsystem -> vm_area_struct cache
VFS -> dentry / inode / filp cache
Process management related code -> task_struct cache
Universal kmalloc-* The cache is the size level created in batches by the slab subsystem itself during the initialization phase:
kmalloc-8
kmalloc-16
kmalloc-32
kmalloc-64
kmalloc-128
...
The creation entrances and uses of the two are different, but they fall into the same core abstraction after creation:
All struct kmem_cache
All object_size / size / offset
All per-CPU and per-node state
When there is a lack of objects, the buddy Page creation slab
Hang the object back after releasing it freelist
The difference is only in the upper semantics:
Special object cache
└─ “What is placed here is struct vm_area_struct”
Universal kmalloc cache
└─ “Here we only guarantee that the slot size is 64B / 128B / 256B, What's inside is up to the caller”
5. What is inside the slab page?
A slab usually consists of one or more pages. For small objects, many objects can be placed on one page; for larger objects, one slab may require multiple pages.
To put it more bluntly, slab is to take one or more physical pages obtained from buddy and press a certain kmem_cache object slot sizeLogical segmentationinto fixed-size object slots; the free object slots are then strung together through freelist, popped up when allocated, and pushed back when released.
In Linux 6.12 SLUB,struct slab is a very critical descriptor. It is not an object that users can see directly, but a management state that the kernel places on "a set of pages that host objects". In implementation, it reuses struct page some fields, since the slab still essentially comes from the page allocator.
This section looks at slab page from two levels: first look struct slab How does this descriptor connect the object page to the cache? Let’s see how the free object borrows its own space to string it into a freelist.
5.1 struct slab describe what
You can first look at the simplified static structure:
struct slab
├─ slab_cache -> this slab Which one does it belong to? kmem_cache
│
├─ slab_list -> hang on node partial/full Used when using linked lists
│ or
├─ next/slabs -> hang on per-cpu partial Used when using linked lists
│
├─ freelist -> this slab first within free object
├─ inuse -> The number of objects currently allocated
├─ objects -> this slab How many object slots are there in total?
└─ frozen -> Whether to temporarily break away from ordinary linked list management
Bundle kmem_cache, kmem_cache_cpu, kmem_cache_node and slab Looking at them together, the most intuitive way is not to memorize the fields first, but to look at their distribution in memory management: on the left is a small batch of management structures, and on the right is a set of slab pages that actually carry objects.
Still with kmalloc-512 For example:
Below is a fully annotated picture to quickly establish a global impression. There are many lines in the picture, so it is only suitable to look at "who is related to whom" first; if you want to see clearly where a certain field points one by one, it is recommended to open the interactive version and press cpu freelist, slab freelist, cpu partial, node partial Click through this sequence:

5.2 Two sets of pointer relationships that are most easily confused
When looking at this picture, the two groups of relationships are most easily confused.
The first group is kmem_cache_cpu->freelist and slab->freelist. They are all object freelist, the chain is the free object, not the slab:
struct kmem_cache_cpu
├─ slab -> current CPU in use active slab
└─ freelist -> current CPU You can take it directly next time free object
struct slab
└─ freelist -> this slab internal free object Linked list header
The node of this chain is the free object itself, and the next pointer is stored inside the object. object + cache->offset Location. The questions it answers are:
Which object can be returned directly from the next allocation??
kmem_cache_cpu->freelist It is the head of the linked list directly consumed by the current CPU fast path. When it is non-null, the allocation can directly pop an object back. Normally, it points to kmem_cache_cpu->slab A free object in this active slab:

kmem_cache_cpu
├─ slab -> active slab
└─ freelist -> active slab inside free object -> free object -> ...
Judging from the real SLUB source code, when a slab is loaded as the active slab of the current CPU, SLUB will slab->freelist This chain is transferred to kmem_cache_cpu->freelist, and put slab->freelist Kiyoshi NULL. So the common ones on active slab are:
kmem_cache_cpu->slab -> slab
kmem_cache_cpu->freelist -> obj1 -> obj2 -> obj3 -> NULL
slab->freelist -> NULL
This does not mean that there are no idle objects in this slab, but that these idle objects have been handed over to the fast path freelist of the current CPU. Wait until this slab is no longer used by a certain kmem_cache_cpu->slab When used as an active slab, if there are free objects inside, SLUB will reintegrate these objects. slab->freelist, let slab re-describe "what other free objects do I have". If this slab is full and there is no free object, then slab->freelist still NULL.
slab->freelist It is the head of the free object linked list recorded by a certain slab. It describes which objects within this slab have not been allocated. It is commonly used in scenarios such as partial slab, remote CPU releasing objects, CPU slab inactivation, slow path migration, etc.

So these two freelists are not two caches, nor are they two objects. They all point to the same type of free object, but with different management levels:
kmem_cache_cpu->freelist
└─ current CPU Fast path object entry
slab->freelist
└─ someone slab Own free object entry
when kmem_cache_cpu->freelist Empty, the slow path may start from the current active slab slab->freelist Supplementary objects; if the current slab is not suitable, go to partial slab or ask buddy for a new page.
The second group is kmem_cache_cpu->slab and kmem_cache_cpu->partial. The levels of these two field chains are different:
struct kmem_cache_cpu
├─ slab -> current CPU Currently using active slab
├─ freelist -> current CPU directly assignable free object
└─ partial -> current CPU cached string partial slabs
kmem_cache_cpu->slab Usually points to a slab. It is the active slab currently being served by the CPU. The fast path mainly revolves around it and kmem_cache_cpu->freelist Work.
kmem_cache_cpu->partial Points to a list of partial slabs. It is not the slab currently being allocated, but the backup slab pool of the current CPU local cache. The linked list node is slab, not object:

kmem_cache_cpu->partial -> slab -> slab -> slab
When the active slab is used up or is no longer suitable for continued allocation, the slow path can be kmem_cache_cpu->partial Take out a partial slab and install it into kmem_cache_cpu->slab, making it a new active slab:
kmem_cache_cpu->partial someone on slab
│
▼
kmem_cache_cpu->slab
Add another status perspective:
active slab
└─ being raped by someone CPU of kmem_cache_cpu->slab Point to, used for the present CPU distribute
per-cpu partial slab
└─ hang on kmem_cache_cpu->partial Come on, give this CPU spare
per-node partial slab
└─ hang on kmem_cache_node->partial Come on, give this node on CPU slow path usage
full slab
└─ All objects have been allocated. Normal non-debug paths generally do not require maintenance full list;
turn on SLUB debug While waiting for configuration, it may hang full list Easy to check.
empty slab
└─ All objects are free. May be retained temporarily, may be returned when policy allows buddy.

So the same struct slab There is usually only one external position at the same time:
by someone CPU as active slab
or hang on a CPU of partial list
or hang on a node of partial list
or already full, No longer ordinary partial list superior
or empty Waiting for reuse later/release
But slab can also have its own object freelist inside:
struct slab
├─ external location: active / per-cpu partial / per-node partial / full / empty
└─ internal freelist: free object -> free object -> NULL
so slab This descriptor answers:
Which pages do these belong to? cache?
How many object slots are there in total??
How many have been used??
Where is the head of the object list that is still free??
Which one is it hanging on now? partial list on, or being raped by someone CPU as active slab?
5.3 How to string free object into freelist
Assuming the page size is 4KB and the object size is 256 bytes, without considering metadata, alignment and debug padding, 16 objects can be placed intuitively:
4KB slab page
┌────┬────┬────┬────┬────┬────┬────┬────┐
│obj0│obj1│obj2│obj3│obj4│obj5│obj6│obj7│
├────┼────┼────┼────┼────┼────┼────┼────┤
│obj8│obj9│... │ │
└────┴────┴────┴─────────────────────────┘
Each object has only two core states:
allocated: Has been returned to a kernel caller for use
free: Still there cache Here, waiting for the next allocation
Free objects will be strung into freelist. The simplified diagram is as follows:
slab page
┌─────┬─────┬─────┬─────┬─────┐
│ used│ free│ used│ free│ free│
└─────┴──┬──┴─────┴──┬──┴──┬──┘
│ │ │
└──────────►└────►┘
freelist
This picture needs to be a little more specific:In the Linux 6.12 SLUB implementation corresponding to this article's experiment, freelist does not allocate an additional set of linked list nodes, but uses the free object's own storage space to store the next pointer..
The key logic in the source code is:
static inline void *get_freepointer(struct kmem_cache *s, void *object)
{
unsigned long ptr_addr;
freeptr_t p;
ptr_addr = (unsigned long)object + s->offset;
p = *(freeptr_t *)(ptr_addr);
return freelist_ptr_decode(s, p, ptr_addr);
}
static inline void set_freepointer(struct kmem_cache *s, void *object, void *fp)
{
unsigned long freeptr_addr = (unsigned long)object + s->offset;
*(freeptr_t *)freeptr_addr = freelist_ptr_encode(s, fp, freeptr_addr);
}
In other words, free object object + s->offset The location stores the address of the "next free object". When freelist hardening is not turned on, it can be understood as a normal next pointer; when it is turned on CONFIG_SLAB_FREELIST_HARDENED , this pointer will be encoded to prevent the freelist from being easily tampered with.
The key points here are:Only free objects will be used as linked list nodes. After the object is allocated, this memory is used by the caller and is no longer maintained by SLUB. next.
Expanding the inside of a free object looks like this:
free object A starting address
│
▼
┌──────────────┬────────────────────────┬──────────────────────┐
│ object bytes │ next pointer slot │ object bytes │
│ │ *(A + cache->offset)=D │ │
└──────────────┴────────────────────────┴──────────────────────┘
▲
│
└─ cache->offset Decide next Where is the pointer placed inside the object?
That cache->offset How did it come about?
It is not determined dynamically each time the object is allocated, but when the object is created kmem_cache At this time, the layout calculation logic of SLUB is uniformly calculated. All object slots in this cache use the same s->offset Find the freelist pointer.
With Linux 6.12 calculate_sizes() To simplify the logic, the rules are roughly:
1. First object_size Align to pointer size
because free pointer can only be placed word-aligned location.
2. if free pointer Cannot overwrite object content
For example: SLAB_TYPESAFE_BY_RCU, The object has ctor, turn on poison,
some redzone/debug scene
Then put free pointer Place it behind the effective area of the object:
s->offset = aligned_object_size
That is, it is not seen by the caller object content, but in this slab
object slot In the additional expanded metadata location.
3. in the case of SLAB_TYPESAFE_BY_RCU and create cache explicitly provided freeptr_offset
Then use the location specified by the caller:
s->offset = args->freeptr_offset
4. Under normal circumstances, the free pointer Place it near the middle of the object:
s->offset = ALIGN_DOWN(object_size / 2, sizeof(void *))
This is done to allow next The pointer moves away from the object boundary, reducing the small range of adjacent objects.
Writing out of bounds directly destroys freelist pointer probability.
so cache->offset Essentially the object layout parameters of this cache. Ordinary cache often puts next Place it near the middle of a free object; caches with RCU, constructor, or debug/poison constraints may place it outside the object's payload.
So the slab graph that is closer to implementation is:
slab page
┌────────┬─────────────────┬────────┬─────────────────┬─────────────────┐
│ used │ free obj A │ used │ free obj D │ free obj E │
│ object │ [next slot]=D │ object │ [next slot]=E │ [next slot]=NULL│
└────────┴────────┬────────┴────────┴────────┬────────┴────────┬────────┘
│ │ │
└─────────────────────────►└────────────────►NULL
Pay attention to the picture [next slot] Not an additional inserted structure field. It is just a small piece of storage space of the free object itself, which is temporarily interpreted by SLUB as "the address of the next free object". If the object is reassigned to the caller, this location will also change back to the normal object content.
5.4 Where is the head of freelist?
So where is the "head" of freelist?
There are two common head pointers in 6.12 SLUB:
struct kmem_cache_cpu
├─ freelist -> current CPU active slab The next quickly allocable object in
├─ tid -> and freelist Do lock-free update transactions together id
└─ slab -> current CPU being allocated active slab
struct slab
├─ freelist -> this slab its own first free object
├─ inuse -> Number of objects used
└─ objects -> this slab total number of objects in
Fast path allocation gives priority to the current CPU kmem_cache_cpu->freelist. The logic can be simplified to:
object = c->freelist
next = get_freepointer(cache, object)
c->freelist = next
return object
That is, popping an object from the head of the linked list:
before:
c->freelist -> A -> D -> E -> NULL
allocate one object:
return A
after:
c->freelist -> D -> E -> NULL
When releasing to the current CPU active slab, the logic is reversed:
old = c->freelist
set_freepointer(cache, object, old)
c->freelist = object
That is, push the newly released object to the head of the linked list:
before:
c->freelist -> D -> E -> NULL
free A:
A->next = D
after:
c->freelist -> A -> D -> E -> NULL
slab->freelist It is used for the idle object chain of the slab itself, especially in scenarios such as partial slab, slow path, remote CPU releasing objects, and CPU slab deactivation. For example, the slow path of 6.12 will start from slab->freelist Get object:
object = slab->freelist
slab->freelist = get_freepointer(cache, object)
slab->inuse++
So it can be summarized as:
Linked list node: free object itself
next pointer: exists free object internal object + cache->offset
Fast path header: current CPU of kmem_cache_cpu->freelist
slab own head: struct slab->freelist
If debugging capabilities such as SLUB debug, KASAN, red zone, poison, etc. are enabled, there may be additional padding and inspection information around the object. this will change objsize, How many objects can be placed in each slab will also affect performance. so read /proc/slabinfo When doing this, remember that it reflects the actual layout under the current kernel configuration.
6. per-cpu freelist: take the lock-free fast path first
Kernel objects are allocated very frequently. If the global linked list is locked for every allocation, lock competition will be heavy if there are too many CPUs.
Taking common SLUB implementations such as Linux 6.12 as an example, the fast path will try to get objects from the freelist of the current CPU:
kmem_cache_alloc(cache)
│
▼
current CPU freelist There are free objects?
│
├─ have
│ └─ Pops an object and returns it to the caller
│
└─ No
└─ Enter the slow path
Only the slow path will look for the partial slab, or even ask the buddy for a new page. The partial here is not just one layer: it usually first looks at the partial slab cached locally by the current CPU, and then returns to the partial list shared by the current NUMA node; if there is no suitable slab on both sides, you need to ask buddy for a new page:
per-cpu freelist is empty
│
▼
try to find per-cpu partial slab
│
├─ turn up
│ ├─ Bundle slab receive current CPU
│ └─ Get objects from inside
│
└─ not found
│
▼
try to find per-node partial slab
│
├─ turn up
│ ├─ Bundle slab receive current CPU
│ └─ Get objects from inside
│
└─ not found
│
▼
Towards buddy Allocate new page
│
▼
Cut into a batch of objects
│
▼
Return one of the objects and hang the rest freelist
This explains why slab is above buddy:
The kernel wants a small object
│
▼
slab cache There are ready-made free objects
│
├─ Yes: Return the object directly
│
└─ No: even partial slab When you can't make up for it, ask buddy If you want a page, then cut the object
buddy doesn't know "this is a VMA object" or "this is a dentry object". Buddy is only responsible for providing pages to slab; slab is responsible for object size, object status, and object reuse.
Seven, once kmem_cache_alloc dynamic process
Putting the above structures together, a dedicated cache allocation can be drawn as:
kmem_cache_alloc(vm_area_struct cache)
│
▼
current CPU freelist
│
├─ Have an object
│ │
│ └─ pop object -> return struct vm_area_struct *
│
└─ no object
│
▼
per-cpu partial slabs
│
├─ have partial slab
│ │
│ └─ Get a free object
│
└─ No partial slab
│
▼
per-node partial slabs
│
├─ have partial slab
│ │
│ └─ Get a free object
│
└─ No partial slab
│
▼
alloc_pages()
│
▼
buddy from zone allocation page
│
▼
initialized to slab, cut into objects
│
▼
return an object
The release path is reversed:
kmem_cache_free(cache, obj)
│
▼
pass obj Address to find where it is slab
│
▼
Confirm this slab Which one does it belong to? kmem_cache
│
▼
Bundle obj of next slot written as old freelist head
│
▼
freelist Change the header to obj
│
▼
slab become partial / full / empty in some state
│
├─ cache Still keep this page, waiting for reuse
└─ When pressure or strategy is triggered, empty slab Maybe give it back buddy
In other words, object "recycling" does not restore the fields in the object to their initial values one by one, but first re-marks this memory as a free object, and then hangs it back to the free object linked list of the corresponding cache.
Simplified into a linked list operation:
before free:
c->freelist -> D -> E -> NULL
free object A:
*(A + cache->offset) = D
c->freelist = A
after free:
c->freelist -> A -> D -> E -> NULL
this A + cache->offset The location is as shown in the previous picture [next slot]. After the object is released, the caller can no longer read or write A, so SLUB can put A The internal space is used to store the freelist pointer.
There is another implicit step when releasing: SLUB to know A Which slab it belongs to. Because the slab itself comes from the page allocator, the kernel can find the object hosting it through the object address. struct slab, and then from slab->slab_cache Retrieve what belongs kmem_cache. In this way, even if you take Universal kfree(obj), the kernel can also return to the correct cache, instead of having the caller manually indicate that this is kmalloc-64 still kmalloc-512.
This leads to a lower-level question:Given an object address, how does the kernel locate the slab where it is located? This question and folio, struct page, struct slab The relationship between them is discussed separately in the next section.
8. How to return the object address to slab:folio and struct slab
Appeared here folio It can first be understood as:Page-level abstraction used by the kernel to manage one or more pages as a whole.
In the past, many kernel codes were transferred directly struct page *,but struct page It may represent an ordinary 4KB page, or it may represent a tail page in a compound page, and the semantics are easily confused.struct folio The intention is to make the code more clearly express "I am operating the head and the entire group of pages", rather than any one page. A folio can contain only one page or multiple pages.
8.1 What is slab folio?
In SLUB, it is very straightforward:Slabs are allocated and described by folio. In other words, a slab may be one page or multiple pages; the kernel treats the page group hosting this batch of objects as a folio, and then hangs slab management information on the page descriptor field of this folio.
Linux 6.12 mm/slab.h This relationship can be seen here:
struct slab and struct page/folio Not three unrelated pieces of memory.
right slab folio Let’s talk, struct slab It’s from this set of pages slab perspective.
folio_slab(folio) -> Bundle slab folio Convert to struct slab
slab_folio(slab) -> Bundle struct slab transfer back folio
The source code comments also clearly say: slab is allocated as folio, which contains the actual objects, and uses the first folio struct page certain fields in SLUB; these fields are passed in SLUB struct slab access.
Let’s take a look at the two transformations separately.
8.2 page_folio(page) How to know which folio a page belongs to
Each physical page frame in the kernel has a struct page. If an allocation is only one page, then the page itself is the head of the folio. If a certain allocation is for multiple pages, that is, compound page / large folio, then the first page is the head page, and the subsequent pages are the tail page. tail page compound_head The field will record the address of the head page and mark "I am the tail page" with the lowest bit.
In other words, in a multi-page folio, only the first page is the head page, and all subsequent pages are tail pages:
4-page folio / compound page
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ page[0] │ page[1] │ page[2] │ page[3] │
│ head page │ tail page │ tail page │ tail page │
│ │ compound_head│ compound_head│ compound_head│
│ │ = page[0]+1│ = page[0]+1│ = page[0]+1│
└──────────────┴──────┬───────┴──────┬───────┴──────┬───────┘
│ │ │
└──────────────┴──────────────┘
│
▼
page[0]
Here's +1 It does not mean that the real address of the head page is increased by 1 byte, but that the kernel borrows the lowest bit of the pointer to mark it: the lower bit is 1, which means "this field stores the head page pointer, and the current page is the tail page." Remove this bit when retrieving the head page.
For slab, if a slab occupies 4 pages, then these 4 pages are a slab folio:
4-page slab folio
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ page[0] │ page[1] │ page[2] │ page[3] │
│ head page │ tail page │ tail page │ tail page │
│ slab metadata│ object bytes │ object bytes │ object bytes │
│ / first objs │ │ │ │
└──────────────┴──────────────┴──────────────┴──────────────┘
▲
│
└─ struct slab / folio perspective from head page start
So the object address no matter where it falls page[0], page[1], page[2] still page[3], pass first virt_to_page(obj) Find the specific page and then pass page_folio(page) can all return to the same head page, that is, the same folio.
Linux 6.12 page_folio() Essentially, take the compound head:
single-page folio:
page
│
└─ compound_head(page) == page
│
▼
folio = (struct folio *)page
multi-page folio:
head page tail page 1 tail page 2
┌────────┐ ┌──────────┐ ┌──────────┐
│ head │◄──────│compound │ │compound │
│ page │ │head=head │ │head=head │
└────────┘ └──────────┘ └──────────┘
│ │
└──────────── page_folio(tail page) ◄──┘
returns head as folio
The source code is simplified as follows:
static __always_inline unsigned long _compound_head(const struct page *page)
{
unsigned long head = READ_ONCE(page->compound_head);
if (head & 1)
return head - 1;
return (unsigned long)page;
}
#define page_folio(p) ((struct folio *)_compound_head(p))
so page_folio(page) Instead of searching "which folio contains this page". The inclusion relationship has been written on the tail page compound_head In metadata; single-page folio returns itself directly.
8.3 folio_slab(folio) How to know which slab the folio corresponds to?
First, it is necessary to determine whether this folio is used as slab by SLUB. There are page type tags in Linux,folio_test_slab(folio) Just check whether the head page of this folio has slab type. Not all folio can be converted to struct slab;Only slab folio can be turned like this.
Once it is confirmed that it is slab folio,folio_slab(folio) Currently it is a type conversion:
#define folio_slab(folio) ((struct slab *)(folio))
This may seem a bit crude, but there is a structural layout behind it.struct slab Reuse the page descriptor field of the head page, Linux 6.12 mm/slab.h There is a group in static_assert Check key field offset:
struct page.flags <-> struct slab.__page_flags
struct page.compound_head <-> struct slab.slab_cache
struct page._refcount <-> struct slab.__page_refcount
That is, the same block of page metadata:
Normal page view: struct page / struct folio
slab Page perspective: struct slab
SLUB is available when the set of pages is marked as slab folio struct slab Perspective reading inside slab_cache, freelist, inuse, objects fields.
This step does not scan all slabs, nor does it check back from the freelist. The object address itself falls within a certain kernel virtual address range. The kernel can first replace the virtual address with the corresponding struct page / folio, and then find the slab descriptor from this folio.
In Linux 6.12 this path can be simplified to:
obj address
│
▼
virt_to_page(obj)
│
▼
page_folio(page)
│
▼
folio_test_slab(folio)
│
├─ no slab folio: Can't press slab object release
│
└─ yes slab folio
│
▼
folio_slab(folio) -> struct slab
│
▼
slab->slab_cache -> struct kmem_cache
The encapsulation in the source code is:
static inline struct slab *virt_to_slab(const void *addr)
{
struct folio *folio = virt_to_folio(addr);
if (!folio_test_slab(folio))
return NULL;
return folio_slab(folio);
}
Why folio can be found struct slab? Because slab is essentially one or more pages separated by buddy, the kernel originally maintains each physical page. struct page metadata. SLUB will mark these pages as slabs and attach slab description information to the page/folio related metadata. So after given the object address, you can first locate "which group of pages this address belongs to", and then "whether this group of pages is a slab and which cache it belongs to".
8.4 Return to release path
If the release occurs on the active slab currently being used by the CPU, a common fast path is to push the object to kmem_cache_cpu->freelist. If the object belongs to the active slab of another CPU, or the current slab status needs to be adjusted, it will take the slow path and update slab->freelist, slab->inuse, put slab back into partial list if necessary.
when slab->inuse When it changes from full to not full, the slab will have free objects again and may enter the partial state; when slab->inuse When it becomes 0, the entire slab is empty. Empty slabs may be retained by the cache for reuse, or may be released back to buddy when shrinkage, memory pressure, or policies allow.
So don't put kfree() Understood as "return the physical page to the system immediately". After a small object is released, it is usually just returned to the corresponding cache. The cache retains some free objects to make the next allocation faster.
Nine,/proc/slabinfo what do you see
User mode can pass /proc/slabinfo Observe slab cache. It is not a per-process view, but rather a global slab usage for the current core. A common manager looks like this:
vm_area_struct 3066 3276 152 26 1 : tunables 0 0 0 : slabdata 126 126 0
The first few fields are the most important:
name active_objs num_objs objsize objperslab pagesperslab
The meaning can be read like this:
name:cache name.active_objs: The number of objects currently in use.num_objs: The total capacity of objects currently owned by this cache.objsize:Single object size.objperslab: How many objects are in each slab.pagesperslab: How many pages each slab consists of.
by vm_area_struct For example:
objsize=152 objperslab=26 pagesperslab=1
On this test kernel, a vm_area_struct The cache object size is 152 bytes, and a 4KB slab page can hold 26 objects.26 * 152 = 3952, the remaining space is used for alignment, metadata, or unusable fragmentation. This number is not a fixed value for all kernels and may vary depending on the kernel version, configuration, architecture, or debugging options.
You can view it directly with the command:
head -5 /proc/slabinfo
grep -E '^(vm_area_struct|task_struct|dentry|kmalloc-64|kmalloc-512) ' /proc/slabinfo
Can also be used slabtop Dynamic observation:
slabtop
But be careful: what you see in the container /proc/slabinfo It is still a view provided by the host kernel, usually not a "slab exclusive to this container". Therefore, it is suitable for observing cache structure and trends, but is not suitable as a strictly isolated container memory ledger.
10. Experiment: Create many VMAs and observe vm_area_struct cache
The following program does three things:
- Prints the current machine architecture, kernel version, and page size.
- read
/proc/slabinfoSeveral typical cache fields. - Create 2000 4KB anonymous maps and use them alternately
PROT_NONEandPROT_READ, try to avoid adjacent VMAs being merged by the kernel.
10.1 Experimental code
The complete code is as follows, also placed in the same directory 06-slabinfo-demo.c:
#define _GNU_SOURCE
#include <errno.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/utsname.h>
#include <unistd.h>
struct slab_row {
char name[96];
unsigned long active_objs;
unsigned long num_objs;
unsigned long objsize;
unsigned long objperslab;
unsigned long pagesperslab;
};
static int read_slab_row(const char *cache, struct slab_row *out)
{
FILE *fp = fopen("/proc/slabinfo", "r");
char line[512];
if (!fp) {
fprintf(stderr, "open /proc/slabinfo failed: %s\n", strerror(errno));
return -1;
}
while (fgets(line, sizeof(line), fp)) {
struct slab_row row;
if (line[0] == '#' || strncmp(line, "slabinfo", 8) == 0)
continue;
memset(&row, 0, sizeof(row));
if (sscanf(line, "%95s %lu %lu %lu %lu %lu",
row.name, &row.active_objs, &row.num_objs,
&row.objsize, &row.objperslab, &row.pagesperslab) != 6)
continue;
if (strcmp(row.name, cache) == 0) {
*out = row;
fclose(fp);
return 0;
}
}
fclose(fp);
return 1;
}
static void print_slab_cache(const char *tag, const char *cache)
{
struct slab_row row;
int ret = read_slab_row(cache, &row);
if (ret < 0)
exit(1);
if (ret > 0) {
printf("[%s] %-24s not found\n", tag, cache);
return;
}
printf("[%s] %-24s active=%lu total=%lu objsize=%lu objperslab=%lu pagesperslab=%lu\n",
tag, row.name, row.active_objs, row.num_objs, row.objsize,
row.objperslab, row.pagesperslab);
}
static unsigned long count_self_maps(void)
{
FILE *fp = fopen("/proc/self/maps", "r");
char line[512];
unsigned long count = 0;
if (!fp) {
perror("open /proc/self/maps");
exit(1);
}
while (fgets(line, sizeof(line), fp))
count++;
fclose(fp);
return count;
}
int main(int argc, char **argv)
{
const long page_size = sysconf(_SC_PAGESIZE);
int mappings = 2000;
void **addr;
struct utsname uts;
if (argc > 1)
mappings = atoi(argv[1]);
if (mappings <= 0) {
fprintf(stderr, "usage: %s [positive_mapping_count]\n", argv[0]);
return 2;
}
if (uname(&uts) != 0) {
perror("uname");
return 1;
}
addr = calloc((size_t)mappings, sizeof(*addr));
if (!addr) {
perror("calloc");
return 1;
}
printf("machine=%s sysname=%s release=%s\n",
uts.machine, uts.sysname, uts.release);
printf("page_size=%ld mappings=%d\n", page_size, mappings);
printf("self VMA count before=%lu\n", count_self_maps());
print_slab_cache("before", "vm_area_struct");
print_slab_cache("before", "task_struct");
print_slab_cache("before", "dentry");
print_slab_cache("before", "kmalloc-64");
print_slab_cache("before", "kmalloc-512");
for (int i = 0; i < mappings; i++) {
int prot = (i % 2 == 0) ? PROT_NONE : PROT_READ;
addr[i] = mmap(NULL, (size_t)page_size, prot,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
if (addr[i] == MAP_FAILED) {
fprintf(stderr, "mmap %d failed: %s\n", i, strerror(errno));
return 1;
}
}
printf("self VMA count after mmap=%lu\n", count_self_maps());
print_slab_cache("after mmap", "vm_area_struct");
print_slab_cache("after mmap", "task_struct");
print_slab_cache("after mmap", "dentry");
print_slab_cache("after mmap", "kmalloc-64");
print_slab_cache("after mmap", "kmalloc-512");
for (int i = 0; i < mappings; i++) {
if (munmap(addr[i], (size_t)page_size) != 0) {
fprintf(stderr, "munmap %d failed: %s\n", i, strerror(errno));
return 1;
}
}
printf("self VMA count after munmap=%lu\n", count_self_maps());
print_slab_cache("after munmap", "vm_area_struct");
print_slab_cache("after munmap", "task_struct");
print_slab_cache("after munmap", "dentry");
print_slab_cache("after munmap", "kmalloc-64");
print_slab_cache("after munmap", "kmalloc-512");
free(addr);
return 0;
}
10.2 Compile and run
Compile and run in x86_64 container:
docker run --rm --platform linux/amd64 \
-v /Users/xyzjiao/article:/work -w /work \
gcc:13 \
bash -lc 'gcc -O2 -Wall -Wextra os/memory/06-slabinfo-demo.c -o /tmp/slabinfo-demo && /tmp/slabinfo-demo 2000'
A real output is as follows:
machine=x86_64 sysname=Linux release=6.12.65-linuxkit
page_size=4096 mappings=2000
self VMA count before=33
[before] vm_area_struct active=3066 total=3276 objsize=152 objperslab=26 pagesperslab=1
[before] task_struct active=464 total=476 objsize=4160 objperslab=7 pagesperslab=8
[before] dentry active=557786 total=583611 objsize=192 objperslab=21 pagesperslab=1
[before] kmalloc-64 active=5096 total=5440 objsize=64 objperslab=64 pagesperslab=1
[before] kmalloc-512 active=11098 total=13664 objsize=512 objperslab=32 pagesperslab=4
self VMA count after mmap=2033
[after mmap] vm_area_struct active=4992 total=4992 objsize=152 objperslab=26 pagesperslab=1
[after mmap] task_struct active=464 total=476 objsize=4160 objperslab=7 pagesperslab=8
[after mmap] dentry active=557786 total=583611 objsize=192 objperslab=21 pagesperslab=1
[after mmap] kmalloc-64 active=5096 total=5440 objsize=64 objperslab=64 pagesperslab=1
[after mmap] kmalloc-512 active=11098 total=13664 objsize=512 objperslab=32 pagesperslab=4
self VMA count after munmap=33
[after munmap] vm_area_struct active=4992 total=4992 objsize=152 objperslab=26 pagesperslab=1
[after munmap] task_struct active=464 total=476 objsize=4160 objperslab=7 pagesperslab=8
[after munmap] dentry active=557785 total=583611 objsize=192 objperslab=21 pagesperslab=1
[after munmap] kmalloc-64 active=5096 total=5440 objsize=64 objperslab=64 pagesperslab=1
[after munmap] kmalloc-512 active=11098 total=13664 objsize=512 objperslab=32 pagesperslab=4
10.3 How to read the results
Several things can be read from this set of results.
First, the test does run in an x86_64 userspace container:
machine=x86_64
Second, after creating 2000 anonymous mappings, the current process's own VMA number increases from 33 to 2033:
self VMA count before=33
self VMA count after mmap=2033
This shows that the program does indeed create a large number of VMAs, rather than being merged into one large VMA.
third,vm_area_struct The number of active objects in the cache has increased significantly:
[before] vm_area_struct active=3066 total=3276
[after mmap] vm_area_struct active=4992 total=4992
The increment is not strictly equal to 2000, which is normal. There are several reasons:
/proc/slabinfoIt is a global view, and other tasks on the system may also allocate or release objects at the same time.- The kernel may expand the cache in advance,
totalIt reflects the current capacity of the cache, not the exact allocation number for a single experiment. - SLUB will retain free objects and slabs and does not require
munmapThen immediately return all object capacity to buddy.
fourth,munmap After the VMA number of the current process returns to 33, but vm_area_struct cache active/total Did not immediately return to before the experiment:
self VMA count after munmap=33
[after munmap] vm_area_struct active=4992 total=4992
This just illustrates an important feature of slab cache:The release of objects does not mean that the slab page hosting them returns to buddy immediately.. The cache can retain the object capacity and reuse it directly when creating a VMA later.
11. How to connect slab with the structures of previous articles
Now we can connect the structures discussed earlier in this series.
When creating a VMA, the kernel requires a metadata object:
mmap()
│
▼
create / Adjustment VMA
│
▼
distribute vm_area_struct
│
▼
kmem_cache_alloc(vm_area_struct cache)
│
▼
slab cache
│
└─ Not enough time buddy Page
The first time an anonymous map is accessed, the kernel requires the user data page:
User writes mmap address
│
▼
page fault
│
▼
find_vma: Address is legal
│
▼
alloc_pages()
│
▼
buddy from zone Allocate physical pages in
│
▼
fill PTE: virtual page -> physical page
Don't mix these two paths:
VMA metadata object: vm_area_struct -> slab -> buddy
User data page: page fault -> buddy -> PTE
Page table page itself: page table allocation -> buddy
slab is not allocated directly to user processes malloc The piece of memory returned. user mode chunks malloc The first is the behavior of libc allocator; when libc needs to expand the address space, it passes brk or mmap Find the kernel. When the kernel processes this request, it may use slab for metadata such as VMA, file objects, page tables, etc.; the physical pages that actually carry user data usually still come from buddy.
12. Several points that are easy to make mistakes
First,slab page is still a physical page.
slab does not bypass buddy. It is to cut out small objects on the page provided by buddy.
buddy tube page
slab tube object
second,kmalloc Small objects usually use slab cache.
kmalloc-64, kmalloc-512 This type of cache is a common landing point for general-purpose small block allocation. Use a dedicated cache for special objects and a kmalloc cache for ordinary small blocks.
third,Releasing the object does not mean releasing the page immediately.
kmem_cache_free() Return the object to cache. The cache may continue to hold slab pages, waiting for subsequent reuse. Only at the right time can the empty slab be returned to buddy.
fourth,/proc/slabinfo It is a global observation port, not a process private ledger..
You can use it to see vm_area_struct, dentry, inode, kmalloc-* Wait for the size of the cache, but you cannot directly regard a certain row of numbers as the exclusive consumption of a certain process.
fifth,Specific object sizes are not ABI.
In the experiment vm_area_struct objsize=152, but this is just 6.12.65-linuxkit Results on this x86_64 test core. The value may change by opening different configurations, changing kernel versions, changing architectures, and enabling debugging capabilities.
13. The core conclusion of this article
Put slab back into the entire memory management map, its location is:
User virtual address
│
▼
VMA: Is the address legal?
│
▼
Page table: whether the virtual page has been mapped
│
▼
buddy: Allocate physical pages
│
├─ User data page
├─ page table page
└─ slab page
│
▼
kernel small object: vm_area_struct / task_struct / dentry / inode / kmalloc-*
Concluding in one sentence:
Buddy is the page frame allocator, and slab is the kernel object allocator built on top of the page frame. The former answers "Which zone to get a few pages from", and the latter answers "Which small object cut out from this page can be used by the kernel".
From an initialization perspective,kmem_cache_create() First create a cache entry for "how this type of object should be managed";kmem_cache_alloc() Consume freelist when an object is needed, or ask buddy to create a new slab.
Next articleagain task_struct, mm_struct, user stack, kernel stack, page table and VMA into a process/thread layout, and see how these structures are organized on a real task.
References
- Linux kernel documentation: Short users guide for the slab allocator
- Linux kernel documentation: The /proc Filesystem
- Linux kernel documentation: Memory Management APIs