Linux内存管理—Slab分配器
文章目录
Slab分配器的引入
内部碎片
内部碎片就是已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间
内部碎片是处于区域内部或页面内部的存储块。占有这些区域或页面的进程并不使用这个存储块。而在进程占有这块存储块时,系统无法利用它。直到进程释放它,或进程结束时,系统才有可能利用这个存储块。
举例:某进程向系统申请了3K内存空间,系统通过伙伴系统算法可能分配给进程4K(一个标准页面)内存空间,导致剩余1K内存空间无法被系统利用,造成了浪费。这是由于进程请求内存大小与系统分配给它的内存大小不匹配造成的。
如下图所示:
由于伙伴算法采用页框(Page Frame)作为基本内存区,适合于大块内存请求。
但是在很多情况下,进程或者系统往往会以字节为单位请求内存,而非以页为单位请求内存,依然采用伙伴算法必然会造成系统内存的极大浪费。为满足进程或者系统对小片内存的请求(比如调用kmalloc()),对内存管理粒度更小的Slab分配器就产生了。
Slab分配器基础概念
slab
- slab是slab内存分配器从buddy system申请页面的基本单位。
- slab的大小不是固定的,slab从属于某个kmem cache实例,不同的kmem cache实例,其slab的大小是不同的。
- slab的大小必须是2^order个pages,order不能超过buddy system所支持的最大的order。
- slab内存分配器从buddy system分配了slab之后,会将其挂在对应的kmem cache实例的node节点。
object
- object是slab内存分配器对外提供的申请内存的基本单位
- slab内存分配器从buddy system申请了buddy之后,会将其拆分成一个个object,并缓存在kmem cache实例的cpu_cache中,用户申请内存时,其实获取的就是一个个object。
- 一旦object缓存耗尽,就会重新从buddy system申请slab,并将其拆分成object,放入内存池。
cache
slab内存分配器有两种cache,一个是slab的cache,一个是object的cache。
- slab内存分配器从buddy system获取页面后,会将其加入kmem cache的node节点,这个就是slab的cache;
- 将slab拆分成多个object,并将object加入kmem cache的cpu_cache内存池,这个就是object的cache;
这两种cache实际是对共同的物理页面的两种缓存形式。
注意:下文说明的缓存指的并不是真正的缓存,真正的缓存指的是硬件缓存,也就是我们通常所说的L1 cache、L2 cache、L3 cache,硬件缓存是为了解决快速的CPU和速度较慢的内存之间速度不匹配的问题,CPU访问cache的速度要快于内存,如果将常用的数据放到硬件缓存中,使用时CPU直接访问cache而不用再访问内存,从而提升系统速度。
下文中的缓存实际上是用软件在内存中预先开辟一块空间,使用时直接从这一块空间中去取,是Slab分配器为了便于对小块内存的管理而建立的。
Slab配器实际上是建立在伙伴系统算法之上的,Slab分配器使用的内存空间是通过伙伴算法进行分配的,只不过Slab对这些内存空间实现了自己的算法进而对小块内存进行管理。
它们之间的关系如下图所示
我们考虑下面场景:如果一个应用程序经常使用某一种类型的对象,或者说频繁的创建、回收某一种类型的对象,那我们是不是可以尝试将这类对象单独存放起来,当进程不再使用时,我们暂时先不回收,等应用程序再使用时,我们把之前应该回收的对象再拿出来,只不过重新构造一下对象,这样我们就减少了一次释放、申请内存的操作了。
Slab分配器的基本原理
slab分配器中用到了对象这个概念,所谓对象就是内核中的数据结构以及对该数据结构进行创建和撤销的操作。
- 它的基本思想是将内核中经常使用的对象放到高速缓存中,并且由系统保持为初始的可利用状态。
- 根据请求的频率不同,可以创建一组具有适当大小的专用对象来高效处理频繁请求的内存区,例如进程描述符,打开的文件对象等;
- 对于很少遇到的内存区大小,可以基于一系列几何分布大小(如2的幂次方)的对象的分配模式来处理。
- slab分配器把对象分组放进高速缓存,每个高速缓存都是同种类型对象的一种“储备”。
例如,当一个文件被打开时,存放相应“打开文件”对象所需的内存区是从一个叫做filp的slab分配器的高速缓存中得到的。
每个高速缓存所占的内存区又被划分多个slab,每个 slab是由一个或多个连续的页框组成。每个页框中包含若干个对象,既有已经分配的对象,也包含空闲的对象。
它们的关系如下图所示:
如上图,各个高速缓存是通过双向链表连接,高速缓存中的每种slab也是通过双向链表连接,每个高速缓存都有一个空闲slab对象的本地高速缓存,请求内存区时就是通过这个本地高速缓存来分配对象,本地高速缓存从slab中的对象获取,一旦分配给本地高速缓存,对于slab来说,这个对象就是非空闲的(有可能还未被使用)
slab分配器的数据结构
高速缓存描述符kmem_cache_s
每个高速缓存通过kmem_cache_s结构来描述,这个结构中包含了对当前高速缓存各种属性信息的描述。所有的高速缓存通过双向链表组织在一起,形成高速缓存链表cache_chain。
struct kmem_cache_s {
/* 1) per-cpu data, touched during every alloc/free */
/**
* 每CPU指针数组,指向包含空闲对象的本地高速缓存。
*/
struct array_cache *array[NR_CPUS];
/**
* 要转移进本地高速缓存或从本地高速缓存中转移出的大批对象的数量。
*/
unsigned int batchcount;
/**
* 本地高速缓存中空闲对象的最大数目。这个参数可调。
*/
unsigned int limit;
/* 2) touched by every alloc & free from the backend */
/**
* 包含三个链表,为什么要单独放到一个描述符中呢?
*/
struct kmem_list3 lists;
/* NUMA: kmem_3list_t *nodelists[MAX_NUMNODES] */
/**
* 高速缓存中包含的对象的大小。
*/
unsigned int objsize;
/**
* 描述高速缓存永久属性的一组标志。
*/
unsigned int flags; /* constant flags */
/**
* 在一个单独slab中的对象的个数。高速缓存中的所有slab具有相同的大小。
*/
unsigned int num; /* # of objs per slab */
/**
* 整个slab高速缓存中空闲对象的上限。
*/
unsigned int free_limit; /* upper limit of objects in the lists */
/**
* 高速缓存自旋锁。
*/
spinlock_t spinlock;
/* 3) cache_grow/shrink */
/* order of pgs per slab (2^n) */
/**
* 一个单独slab中包含的连续页框数目的对数。
*/
unsigned int gfporder;
/* force GFP flags, e.g. GFP_DMA */
/**
* 分配页框时传递给伙伴系统函数的一组标志。
*/
unsigned int gfpflags;
/**
* slab使用的颜色个数。用于slab着色。
*/
size_t colour; /* cache colouring range */
/**
* slab中的基本对齐偏移。
*/
unsigned int colour_off; /* colour offset */
/**
* 下一个被分配的slab使用的颜色。就是对齐因子。
*/
unsigned int colour_next; /* cache colouring */
/**
* 指向包含slab描述符的普通slab高速缓存。如果使用了内部slab描述符,则这个字段为NULL。
*/
kmem_cache_t *slabp_cache;
/**
* 单个slab的大小。
*/
unsigned int slab_size;
/**
* 高速缓存动态属性标志。
*/
unsigned int dflags; /* dynamic flags */
/* constructor func */
/**
* 高速缓存相关的构造方法的指针。
*/
void (*ctor)(void *, kmem_cache_t *, unsigned long);
/* de-constructor func */
/**
* 高速缓存相关的析构方法的指针。
*/
void (*dtor)(void *, kmem_cache_t *, unsigned long);
/* 4) cache creation/removal */
/**
* 高速缓存名称。
*/
const char *name;
/**
* 高速缓存链表。
*/
struct list_head next;
/* 5) statistics */
#if STATS
/**
* 统计信息
*/
unsigned long num_active;
unsigned long num_allocations;
unsigned long high_mark;
unsigned long grown;
unsigned long reaped;
unsigned long errors;
unsigned long max_freeable;
unsigned long node_allocs;
atomic_t allochit;
atomic_t allocmiss;
atomic_t freehit;
atomic_t freemiss;
#endif
#if DEBUG
/**
* 调试信息
*/
int dbghead;
int reallen;
#endif
};
/**
slab高速缓存描述符内嵌结构kmem_list3
每个kmem_cache_s结构中并不包含对具体slab的描述,而是通过kmem_list3结构组织各个 slab。
struct kmem_list3 {
/**
* 空闲和非空闲对象的slab描述符双向循环链表。
*/
struct list_head slabs_partial; /* partial list first, better asm code */
/**
* 非空闲对象的slab描述符双向循环链表。
*/
struct list_head slabs_full;
/**
* 只包含空闲对象的slab描述符双向循环链表。
*/
struct list_head slabs_free;
unsigned long free_objects;
/**
* slab分配器的页回收算法使用。
*/
int free_touched;
/**
* slab分配器的页回收算法使用。
*/
unsigned long next_reap;
/**
* 所有CPU共享的一个本地高速缓存的指针。它使得将空闲对象从一个本地高速缓存移动到另外一个高速缓存的任务更容易。
* 它的初始大小是batchcount字段的8倍。
*/
struct array_cache *shared;
};
注意 :slabs_free列表中的 slab 是进行回收(reaping)的主要备选对象。正是通过此过程,slab 所使用的内存被返回给操作系统供其他用户使用。
维护以上三个双向链表的目的是为了缩减寻找空闲对象的时间。
- 当请求缓存中的对象时,内核将搜索部分slab链
- 如果部分slab链是空的
- 那么将搜索空闲slab
- 如果空闲slab链表是空的,那么将创建一个新的slab。
slab描述符
struct slab {
// slab高速缓存描述符的三个双向循环链表中的一个。
struct list_head list;
/*
* slab中第一个对象的偏移。
* 同一个高速缓存的不同slab有不同的coloroff值。这样可以避免硬件缓存行的不利影响。
*/
unsigned long colouroff;
/*
* s_mem字段存放的指针指向slab中的第一个对象
*/
void *s_mem; /* including colour offset */
/*
* 当前正在使用的slab中的对象个数。
*/
unsigned int inuse; /* num of objs active in slab */
/*
* slab中下一个空闲对象的下标。如果没有剩下空闲对象则为BUFCT_END
*/
kmem_bufctl_t free;
};
关于SLAB着色
我们知道内存需要处理时要先放入CPU硬件高速缓存中,而CPU硬件高速缓存与内存的映射方式有多种。在同一个kmem_cache中所有SLAB都是相同大小,都是相同连续长度的页框组成,这样的话在不同SLAB中相同对象号对于页框的首地址的偏移量也相同,这样有很可能导致不同SLAB中相同对象号的对象放入CPU硬件高速缓存时会处于同一行,当我们交替操作这两个对象时,CPU的cache就会交替换入换出,效率就非常差。
SLAB着色就是在同一个kmem_cache中对不同的SLAB添加一个偏移量,就让相同对象号的对象不会对齐,也就不会放入硬件高速缓存的同一行中,提高了效率。
着色空间就是前端的空闲区域,这个区有大小都是在分配新的SLAB时计算好的,计算方法很简单,node结点对应的kmem_cache_node中的colour_next乘上kmem_cache中的colour_off就得到了偏移量,然后colour_next++,当colour_next等于kmem_cache中的colour时,colour_next回归到0。
偏移量 = kmem_cache.colour_off * kmem_cache.node[NODE_ID].colour_next;
kmem_cache.node[NODE_ID].colour_next++;
if (kmem_cache.node[NODE_ID].colour_next == kmem_cache.colour)
kmem_cache.node[NODE_ID].colour_next = 0;
高速缓存的分类
普通高速缓存
普通高速缓存并不针对内核中特定的对象,它首先会为kmem_cache结构本身提供高速缓存,这类缓存保存在cache_cache变量中,该变量即代表的是cache_chain链表中的第一个元素;另一方面,它为内核提供了一种通用高速缓存。
/* internal cache of cache description objs */
/**
* 第一个普通高速缓存
*/
static kmem_cache_t cache_cache = {
.lists = LIST3_INIT(cache_cache.lists),
.batchcount = 1,
.limit = BOOT_CPUCACHE_ENTRIES,
.objsize = sizeof(kmem_cache_t),
.flags = SLAB_NO_REAP,
.spinlock = SPIN_LOCK_UNLOCKED,
.name = "kmem_cache",
#if DEBUG
.reallen = sizeof(kmem_cache_t),
#endif
};
slab分配器所提供的小块连续内存的分配是通过通用高速缓存实现的。
通用高速缓存所提供的对象具有几何分布的大小,范围为32到131072字节。内核中提供了kmalloc()和kfree()两个接口分别进行内存的申请和释放。
专用高速缓存
专用高速缓存是根据内核所需,通过指定具体的对象而创建。
高速缓存的申请和释放
创建缓存的步骤
- 描述符的分配和初始化
- 计算slab着色和对象大小
- 在cache_chain链表中添加缓存
创建通用缓存kmem_cache_init()
kmem_cache_init()创建cache_chain和通用缓存,它是在初始化过程中被调用的。注意该函数名前有一个_init前缀,_init修饰的函数载入内存后会在自举和初始化过程结束后被销毁。
void __init kmem_cache_init(void)
{
size_t left_over;
/*
* 变量sizes和names是由kmalloc分配的数组的头
* 此刻这些数组被置于_init数据区。注意kmalloc()此时还并不存在
*到此为止,我们所拥有的只有静态分配的cach_cache描述符
*/
struct cache_sizes *sizes;
struct cache_names *names;
/*
* 用来确定一个slab使用多少页。
* 一个slab中的页面数完全取决于系统中由多少可用内存
* 在x86和PPC体系机构中,变量PAGE_SHIFT的值为12
* 所以我们要确认num_physpages的值是否大于8K.
* 如果超过的话说明我们的机器有超过32M的内存
* 这时每个slab用BREAK_GFP_ORDER_HI个页,否则为每个slab分配1页
*/
if (num_physpages > (32 << 20) >> PAGE_SHIFT)
slab_break_gfp_order = BREAK_GFP_ORDER_HI;
//初始化cache_chain所存放的信号量ache_chain_sem
init_MUTEX(&cache_chain_sem);
//初始化cache_chain链表,所有的缓存描述符都存放在该链表中
INIT_LIST_HEAD(&cache_chain);
//向cache_chain链表中加入cache_cache描述符
list_add(&cache_cache.next, &cache_chain);
cache_cache.colour_off = cache_line_size();
//创建“每个CPU”的缓存
cache_cache.array[smp_processor_id()] = &initarray_cache.cache;
cache_cache.objsize = ALIGN(cache_cache.objsize, cache_line_size());
/* 执行完整性检查,也就是检查在cache_cache中是否至少分配了一个缓存描述符
* 还设置了cache_cache描述符的num字段,并计算还有多少剩余空间
*/
cache_estimate(0, cache_cache.objsize, cache_line_size(), 0,
&left_over, &cache_cache.num);
if (!cache_cache.num)
BUG();
//slab着色是一种内核用于降低缓存对其所带来的性能下降的方法。
cache_cache.colour = left_over/cache_cache.colour_off;
cache_cache.colour_next = 0;
cache_cache.slab_size = ALIGN(cache_cache.num*sizeof(kmem_bufctl_t) +
sizeof(struct slab), cache_line_size());
sizes = malloc_sizes;
names = cache_names;
/* 检查我们是否已经到达sizes数组的末尾,sizes数组的最后一个元素总是被设置成0
* 因此当我们到达数组最后一个元素时,判断条件就为真
*/
while (sizes->cs_size) {
//为常规内存分配请求创建下一个kmalloc缓存,并且验证该缓存是否为空或
sizes->cs_cachep = kmem_cache_create(names->name,
sizes->cs_size, ARCH_KMALLOC_MINALIGN,
(ARCH_KMALLOC_FLAGS | SLAB_PANIC), NULL, NULL);
if (!(OFF_SLAB(sizes->cs_cachep))) {
offslab_limit = sizes->cs_size-sizeof(struct slab);
offslab_limit /= sizeof(kmem_bufctl_t);
}
//为DMA内存分配创建缓存
sizes->cs_dmacachep = kmem_cache_create(names->name_dma,
sizes->cs_size, ARCH_KMALLOC_MINALIGN,
(ARCH_KMALLOC_FLAGS | SLAB_CACHE_DMA | SLAB_PANIC),
NULL, NULL);
//找到sizes和names数组中的下一个元素
sizes++;
names++;
}
创建专用缓存kmem_cache_create()
当通用缓存所提供的内存区不能满足需要时,会调用kmem_cache_create()函数来创建专用缓存。该函数没有_init前缀,因为它被调用后,可以使用永久内存。这通常是在内核初始化时执行的,或者在首次加载内核模块时执行。
创建专用缓存的步骤:
- 创建、分配、初始化缓存描述符
- 对齐对象
- 对齐slab描述符
- 添加缓存到缓存链表中
其原型定义如下:
kmem_cache_t *
kmem_cache_create (const char *name, size_t size, size_t align,
unsigned long flags, void (*ctor)(void*, kmem_cache_t *, unsigned long),
void (*dtor)(void*, kmem_cache_t *, unsigned long))
- name 参数定义了缓存名称
- proc 文件系统(在 /proc/slabinfo 中)使用它标识这个缓存。
- size 参数指定了为这个缓存创建的对象的大小。
- align 参数定义了每个对象必需的对齐。 flags 参数指定了为缓存启用的选项。
撤销一个高速缓存kmem_cache_destory()
缓存与slab都可被销毁。缓存可被缩减或者销毁以返还其占用的内存给空闲内存池。内核在系统内存过低时会调用析构函数。在任意一种情况下,slab也可被销毁且将与之对应的页面返还给伙伴系统,以便重复利用。
内核函数 kmem_cache_destory()用于撤销一个高速缓存, 并将它从cache_chain链表上删除。
这个调用是由内核模块在被卸载时执行的
调用该函数前必须确保存在以下两个条件:
- 高速缓存中的所有slab都必须为空。其实,不管哪个slab中,只要还有一个对象被分配出去并正在使用的话,那怎么可能撤销这个高速缓存呢?
- 在调用kmem_cache_destory()过程中不再访问这个高速缓存。调用者必须确保这种同步
销毁缓存的步骤如下:
- 从缓存链表中删除缓存
- 删除slab描述符
- 删除缓存描述符
定义如下:
int kmem_cache_destroy (kmem_cache_t * cachep)
{
int i;
if (!cachep || in_interrupt())
BUG();
/* Don't let CPUs to come and go */
lock_cpu_hotplug();
/* Find the cache in the chain of caches. */
down(&cache_chain_sem);
/*
* the chain is never empty, cache_cache is never destroyed
*/
list_del(&cachep->next);
up(&cache_chain_sem);
if (__cache_shrink(cachep)) {
slab_error(cachep, "Can't free all objects");
down(&cache_chain_sem);
list_add(&cachep->next,&cache_chain);
up(&cache_chain_sem);
unlock_cpu_hotplug();
return 1;
}
if (unlikely(cachep->flags & SLAB_DESTROY_BY_RCU))
synchronize_kernel();
/* no cpu_online check required here since we clear the percpu
* array on cpu offline and set this to NULL.
*/
for (i = 0; i < NR_CPUS; i++)
kfree(cachep->array[i]);
/* NUMA: free the list3 structures */
kfree(cachep->lists.shared);
cachep->lists.shared = NULL;
kmem_cache_free(&cache_cache, cachep);
unlock_cpu_hotplug();
return 0;
}
创建slab的函数cache_grow()
当缓存刚被创建时,没有slab。事实上,只有当一个对象发出请求的确需要slab时才真正分配slab。这种情况发生在缓存描述符的list.slabs_partial和lists.slabs_free字段都为空时。此时我们并不关心对内存的请求如何转化成对特定缓存中对象的请求,而是假定这种转化已经发生,把关注点转移到slab分配器内的具体实现上。
cache_grow()创建slab并将其放在缓存中。当我们创建slab时,不但分配和初始化其描述符,而且还需要分配物理内存,为此,我们需要与伙伴系统交互以请求页面。这个工作是由kmem_getpages()完成的。
cache_grow()的定义如下:
static int cache_grow (kmem_cache_t * cachep, int flags, int nodeid)
{
struct slab *slabp;
void *objp;
size_t offset;
int local_flags;
unsigned long ctor_flags;
/* Be lazy and only check for valid flags here,
* keeping it out of the critical path in kmem_cache_alloc().
*/
if (flags & ~(SLAB_DMA|SLAB_LEVEL_MASK|SLAB_NO_GROW))
BUG();
if (flags & SLAB_NO_GROW)
return 0;
ctor_flags = SLAB_CTOR_CONSTRUCTOR;
local_flags = (flags & SLAB_LEVEL_MASK);
if (!(local_flags & __GFP_WAIT))
/*
* Not allowed to sleep. Need to tell a constructor about
* this - it might need to know...
*/
ctor_flags |= SLAB_CTOR_ATOMIC;
//禁用中断且锁定描述符,为操作缓存描述符的字段做准备
check_irq_off();
spin_lock(&cachep->spinlock);
/* Get colour for the slab, and cal the next value. */
/**
* 更新高速缓存描述符的当前颜色值。并根据当前颜色值计算数据区的偏移。
* 这样可以平等的在slab间分布颜色值。
*/
offset = cachep->colour_next;
cachep->colour_next++;
if (cachep->colour_next >= cachep->colour)
cachep->colour_next = 0;
offset *= cachep->colour_off;
//解锁缓存描述符且重新启用中断
spin_unlock(&cachep->spinlock);
if (local_flags & __GFP_WAIT)
local_irq_enable();
/*
* The test for missing atomic flag is performed here, rather than
* the more obvious place, simply to reduce the critical path length
* in kmem_cache_alloc(). If a caller is seriously mis-behaving they
* will eventually be caught here (where it matters).
*/
kmem_flagcheck(cachep, flags);
/* Get mem for the objs. */
/**
* 与伙伴系统交互,调用kmem_getpages从分区页框分配器获得一组页框来存放一个单独的slab
*/
if (!(objp = kmem_getpages(cachep, flags, nodeid)))
goto failed;
/* Get slab management. */
/**
* 把slab描述符存放在它该存放的地方
* 可以把slab描述符存放在slab自身或者第一个通用缓存中
*/
if (!(slabp = alloc_slabmgmt(cachep, objp, offset, local_flags)))
goto opps1;
//把页面与缓存和slab描述符相关联
set_slab_attr(cachep, slabp, objp);
/**
* cache_init_objs将构造方法(如果有)应用到新的slab包含的所有对象上。
* 也就是初始化slab中的所有对象
*/
cache_init_objs(cachep, slabp, ctor_flags);
if (local_flags & __GFP_WAIT)
local_irq_disable();
check_irq_off();
spin_lock(&cachep->spinlock);
/* Make slab active. */
/**
* 将新得到的slab描述符slabp添加到高速缓存描述符cachep的全空slab链表的末端。并更新空闲对象计数器
*/
list_add_tail(&slabp->list, &(list3_data(cachep)->slabs_free));
STATS_INC_GROWN(cachep);
list3_data(cachep)->free_objects += cachep->num;
spin_unlock(&cachep->spinlock);//解开自旋锁,并成功返回
return 1;
opps1:
//该段将在页请求出错时被调用,基本上是释放请求的页
kmem_freepages(cachep, objp);
failed:
//启用中断,这样就允许中断传出了
if (local_flags & __GFP_WAIT)
local_irq_disable();
return 0;
}
#if DEBUG
/*
slab的申请和释放
获取slab对象kmem_cache_alloc()
kmem_cache_alloc()在其参数所指定的高速缓存中cachep分配一个slab。
如果高速缓存的所有slab中都没有空闲的对象,那么slab层必须必须通过kmem_getpages()获取新的页。
/**
* 获得新的slab对象
* cachep-指向高速缓存描述符。新空闲对象必须从该高速缓存描述符获得。
* flag-表示传递给分区页框分配器函数的标志。
*/
void * kmem_cache_alloc (kmem_cache_t *cachep, int flags)
{
return __cache_alloc(cachep, flags);
}
释放一个slab对象kmem_cache_free()
kmem_cache_free()在其参数所指定的高速缓存中释放一个slab,并把它返还给原先的slab。这样就能把cachep中的对象objp标记为空闲。
/**
* 释放一个曾经由slab分配器分配给某个内核函数的对象
* cachep-高速缓存描述符的地址。
* objp-要释放的对象的地址。
*/
void kmem_cache_free (kmem_cache_t *cachep, void *objp)
{
unsigned long flags;
local_irq_save(flags);
__cache_free(cachep, objp);
local_irq_restore(flags);
}
总结
推荐阅读
-
【原创】(十三)Linux内存管理之vma/malloc/mmap
-
详解Linux内核内存管理架构
-
【原创】(十)Linux内存管理 - zoned page frame allocator - 5
-
【原创】(十四)Linux内存管理之page fault处理
-
【原创】(八)Linux内存管理 - zoned page frame allocator - 3
-
Linux内核设备驱动之内存管理笔记整理
-
【原创】(十二)Linux内存管理之vmap与vmalloc
-
【原创】(九)Linux内存管理 - zoned page frame allocator - 4
-
MySQL内存管理,内存分配器和操作系统
-
Linux系统基本的内存管理知识讲解