本文目的在于分析Linux内存管理机制的slab分配器。内核版本为2.6.31。
1. SLAB分配器
内核需要经常分配内存,我们在内核中最常用的分配内存的方式就是kmalloc了。前面讲过的伙伴系统只支持按页分配内存,但这个单位太大了,有时候我们并不需要这么大的内存,比如我想申请128字节的空间,如果直接使用伙伴系统则需分配4KB的一整页,这显然是浪费。
slab分配器将页拆分为更小的单位来管理,来满足小于一页的内存需求。它将连续的几个页划分出更小的部分拿来分配相同类型的内存对象,对象的位置尽量做到某种对齐要求(如处理器的L1高速缓存行对齐)以便不对CPU高速缓存和TLB带来显著影响。slab把一类对象统一管理,例如,划出两个页的内存,将其分成n小份用来分配一类对象的实例,同时slab也维护一些通用的对象,用来供kmalloc使用。
提供小块内存并不是slab分配器的唯一任务,由于结构上的特点,它也用作一个缓存,主要针对经常分配内存并释放的对象。slab分配器将释放的内存保存在一个内部列表中,并不马上返还给伙伴系统。在请求为该类对象分配一个实例时,会使用最近释放的内存块,这样就不必触及伙伴系统以缩短分配消耗的时间,另外由于该内存块是“新”的,其驻留在CPU高速缓存的概率也会较高。
在下面的代码分析中,你会看到slab的这些“手段”是如何实现的。
先说一下“slab着色(slab coloring)”机制,是用来防止高速缓存冲突的:相同类型的slab、对象很有可能被保存到相同的CPU cache的缓存行中,经常使用的对象被放到CPU cache中,这当然使我们想要的,但如果两个不同的对象每次都被放到相同的缓存行中,那交替的读取这两个对象,会导致缓存行的内容不断的被更新,也就无法体现缓存的好处了。不过我的内核版本是2.6.31,已经没有slab着色机制了,所以大家看到coloring什么的就不要再纠结了。
内核中还提供了slob和slub两种替代品,因为slab的结构很复杂,并且需要使用太多额外的空间去管理一类对象。关于这两个替代品我就不多说了,slub在性能和缓存占用方面都要优于slab,并且在一些嵌入式设备上会看到内核使用slub。
2. SLAB分配器的实现
2.1 SLAB分配器初始化
系统启动时slab分配器初始化的函数为kmem_cache_init()和kmem_cache_init_late()。函数名中的“cache”是指slab分配器,我们也称作slab缓存,注意,它与CPU中的高速缓存没有关系,但上面讲到slab利用了高速缓存的特性。下面的分析中,我会使用“slab缓存”这种叫法。
kmem_cache_init()函数为分配slab对象准备最基本的环境。在分析这个函数之前,我们先看一个内核中创建slab缓存的例子:
static struct kmem_cache *nf_conntrack_cachep __read_mostly;
nf_conntrack_cachep= kmem_cache_create("nf_conntrack",
sizeof(struct nf_conn),
0, SLAB_DESTROY_BY_RCU, NULL);
上面的代码在内核协议栈的链接跟踪模块中创建struct nf_conn的slab缓存,这个slab缓存用于分配struct nf_conn对象。
当想申请一个struct nf_conn结构的对象时,使用kmem_cache_alloc()函数进行分配。
struct nf_conn *ct;
ct =kmem_cache_alloc(nf_conntrack_cachep, gfp);
可以看到,创建和使用slab缓存是非常方便的。在/proc/slabinfo文件中可以看到内核中所创建的slab缓存。
kmem_cache_create()用于创建一个slab缓存,在哪里创建呢,kmem_cache_init()函数的工作就是初始化用于创建slab缓存的slab缓存。也就是说,一个slab缓存也应该是通过函数kmem_cache_create()来创建的,但是很容易想到,内核中的第一个slab缓存肯定不能通过这个函数来创建,在内核中使用一个编译时生成的静态变量作为第一个slab缓存。
slab缓存用一个struct kmem_cache结构来描述。内核中的第一个slab缓存定义如下:
static struct kmem_cache cache_cache = {
.batchcount = 1,
.limit = BOOT_CPUCACHE_ENTRIES,
.shared = 1,
.buffer_size = sizeof(struct kmem_cache),
.name = "kmem_cache",
};
系统中所有的slab缓存都被放入一个全局链表中:
staticstruct list_head cache_chain;
接下来我们来分析kmem_cache_init()函数,它的实现分为下面几个步骤:
1. 创建cache_cache,它将用于分配系统中除了它自身以外的所有slab缓存的kmem_cache对象。
2. 创建可以分配struct arraycache_init和struct kmem_list3的slab cache。先创建这两个通用cache的原因后面会讲到,他们也供kmalloc使用。这两个cache是通过kmem_cache_create()创建的,因为cache_cache已经可用了。这一步之后,将slab_early_init置为0。
3. 使用kmem_cache_create()创建剩余的通用cache,“剩余”是相对第2步中的两个cache,他们都是可以供kmalloc使用的。这些通用cache的名字和对象大小见下面表格。
4. 为cache_cache.array[]和malloc_sizes[INDEX_AC].cs_cachep->array[]重新分配空间。
5. 为cache_cache.nodelists[]、malloc_sizes[INDEX_AC].cs_cachep-> nodelists[]和malloc_sizes[INDEX_L3].cs_cachep-> nodelists[]重新分配空间。
通用cache的名字和对象大小用两个数组来记录:malloc_sizes[]和cache_names[]。
cache_names[]
malloc_sizes[]
size-32
32
size-64
64
size-96
96
size-128
128
……
……
NULL
ULONG_MAX
对于数据结构的其他细节先不做讨论,在讲到在一个cache上分配对象时会说明数据结构。
这部分需要注意一些静态变量,在初始化cache_cache的时候只用到了静态分配的变量,他们之间的关系如下图,其中全局变量用红色标出。
这时还没有用户自己创建的slab cache,所以这里看到的都是通用cache。这些通用cache主要供kmalloc使用。在这期间,不要使用kmalloc函数。
2.2 创建SLAB缓存
除了cache_cache自身,创建一个slab缓存的方法为kmem_cache_create()。slab缓存分为on-slab和off-slab两种,on-slab就是slab管理信息和它所管理的对象放在一起,off-slab就是slab管理信息和他所管理的对象分开存放,后面会看到为什么会区分这两种类型的slab。
我们先了解一下struct kmem_cache结构体的成员。
struct kmem_cache {
/* 1)per-cpu data, touched during every alloc/free */
struct array_cache *array[NR_CPUS]; /*per-CPU缓存 */
/* 2)Cache tunables. Protected by cache_chain_mutex */
unsigned int batchcount;
unsigned int limit;
unsigned int shared;
/*每次分配的大小,如nf_conn的cache的buffer_size为sizeof(structnf_conn) */
unsigned int buffer_size;
u32 reciprocal_buffer_size;
/* 3)touched by every alloc & free from the backend */
unsigned int flags; /* constant flags */
unsigned int num; /* # of objs per slab */
/* 4)cache_grow/shrink */
/* order of pgs per slab (2^n) */
unsigned int gfporder;
/* force GFP flags, e.g. GFP_DMA */
gfp_t gfpflags;
size_t colour; /* cache colouring range */
unsigned int colour_off; /* colour offset */
/* 为slab管理信息分配空间的cache。 */
struct kmem_cache *slabp_cache;
/* slab管理信息的size,即struct slab和n个kmem_bufctl_t */
unsigned int slab_size;
unsigned int dflags; /* dynamic flags */
/* constructor func */
void (*ctor)(void *obj);
/* 5) cache creation/removal */
const char *name;
struct list_head next;
/*
* We put nodelists[] at the end ofkmem_cache.
* We still use [MAX_NUMNODES] and not [1] or[0] because cache_cache
* is statically defined, so we reserve themax number of nodes.
*/
struct kmem_list3 *nodelists[MAX_NUMNODES];
/*
* Donot add fields after nodelists[]
*/
};
kmem_cache_create()函数的声明如下:
struct kmem_cache *
kmem_cache_create(const char *name, size_t size, size_t align,
unsigned long flags, void (*ctor)(void *));
五个参数分别为:
name:要创建的cache的名字,将赋值给kmem_cache结构的name成员。
size:要创建的cache每次分配对象的大小,将赋值给kmem_cache结构的buffer_size成员。
align:分配对象以及slab管理信息的对齐量,基本上都为0,即使用默认的对齐方式。
flags:标记,kmem_cache结构的flags成员。
ctor:分配新的slab的时候的构造函数,kmem_cache结构的ctor成员。
这个函数的工作如下:
1. 根据flags、CPU的cache line以及传入的align参数,调整slab管理信息的的对齐量。
2. 用kmem_cache_zalloc(&cache_cache, gfp)在cache_cache上分配一个kmem_cache结构实例cachep。
3. 如果对象的size不小于(PAGE_SIZE >> 3),并且全局标记slab_early_init=0,就强制给flags设置CFLGS_OFF_SLAB。
4. 根据align调整buffer_size大小,并调用calculate_slab_order()函数,该函数从order=0寻找最小的order满足2^order个页的大小可用于分配至少一个对象,找到之后给cachep->num和cachep->gfporder赋值,num成员为2^gfporder个页可分配多少个对象,函数返回值left_over为剩余的空间。对于on slab的cache而言,满足cachep->num * cachep->buffer_size+ cachep->slab_size + left_over = (2 ^ cachep->gfporder) * PAGE_SIZE。而对于off slab的cache而言,满足cachep->num * cachep->buffer_size + left_over = (2 ^ cachep->gfporder)* PAGE_SIZE,即没有slab管理信息的空间,因为off slab的cache的管理信息单独放到另一个地方。
5. 如果left_over比slab管理信息空间大,且cachep是off slab的,则把cachep改为on slab的,即清除CFLGS_OFF_SLAB标记。同时将left_over的值减掉slab管理信息的大小。
6. 给cachep的一些成员赋值:
cachep->colour_off = cache_line_size();
/* Offset must be a multiple of thealignment. */
if (cachep->colour_off < align)
cachep->colour_off = align;
cachep->colour = left_over /cachep->colour_off;
cachep->slab_size = slab_size;
cachep->flags = flags;
cachep->gfpflags = 0;
if (CONFIG_ZONE_DMA_FLAG && (flags& SLAB_CACHE_DMA))
cachep->gfpflags |= GFP_DMA;
cachep->buffer_size = size;
cachep->reciprocal_buffer_size =reciprocal_value(size);
7. 如果cachep是off slab的, slab管理信息单独放在其他一个地方。这个地方就是根据slab_size(slab管理信息的大小)在通用cache上选择一个合适的cache,注意这里只是选择cache,没有给slab信息分配空间。选好的cache赋值给cachep->slabp_cache。
8. 对cachep->nodelists[0]和cachep->array[0]赋值。调用的函数为setup_cpu_cache(),这个函数中根据全局变量g_cpucache_up的值给cachep的两个成员分配不同的值。最终结果就是为cachep->array[0]分配sizeof(void *) * cachep->batchcount+ sizeof(struct array_cache)大小的空间,其中cachep->batchcount是struct array_cache中entry的数目。cachep->nodelists[0]中的三个链表都初始为空。
9. 将cachep加入到全局链表:list_add(&cachep->next, &cache_chain);
g_cpucache_up变量:
这个变量记录了在不同阶段,slab缓存初始化的状态,它可取的值有:
static enum {
NONE,
PARTIAL_AC,
PARTIAL_L3,
EARLY,
FULL
} g_cpucache_up;
在kmem_cache_init()和kmem_cache_init_late()之间没有调用过kmem_cache_create(),即在g_cpucache_up等于EARLY和FULL之间没有创建过cache。也就是说,g_cpucache_up除了FULL之外的所有状态都只在kmem_cache_init()中有用到,即只有创建通用cache有用到。
假定INDEX_AC=0,PARTIAL_L3=3。下表显示了随着创建不同的slab缓存,g_cpucache_up记录的状态的变化:
cache名称
g_cpucache_up
cache_cache
NONE
size-32(array cache)
NONE
size-64(kmem_list3 structures)
PARTIAL_AC
其他通用cache
PARTIAL_L3
自定义cache
FULL
2.3 在SLAB缓存分配空间
在slab缓存中分配对象,使用的函数是kmem_cache_alloc()或kmem_cache_zalloc(),函数返回void *类型指针。实际的分配工作由____cache_alloc()完成,它的函数体很简单:
static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
void *objp;
struct array_cache *ac;
ac = cpu_cache_get(cachep); /* ac =cachep->array[0] */
if (likely(ac->avail)) {
ac->touched= 1;
objp = ac->entry[--ac->avail]; /* 最后一个entry */
} else {
objp = cache_alloc_refill(cachep, flags);
}
return objp;
}
如果cachep->array[0]->avail不为0,则直接从cachep->array[0]->entry[]末尾取一个对象返回,并将avail的值减1。
如果cachep->array[0]->avail为0,即没有可用对象可分配,则调用cache_alloc_refill()。
struct kmem_cache的array数组的每个元素都是一个per-CPU缓存,slab分配一个对象,最终都是先填充到这个缓存中,再在这上面分配出去的。
struct kmem_cache {
/* 1) per-cpu data, touched during everyalloc/free */
struct array_cache *array[NR_CPUS]; /*per-CPU缓存 */
……
}
struct array_cache结构体定义如下:
struct array_cache {
unsigned int avail; /* 该缓存中可用对象的数目 */
unsigned int limit; /* 对象数目的限制,在释放缓存时使用 */
/* 如果该缓存中没有对象可分配了,每次需要向slab申请填充对象的数量 */
unsigned int batchcount;
unsigned int touched; /* 该缓存是否是活动的(最近分配过对象) */
spinlock_t lock;
void *entry[]; /* 实际的对象存放在这儿 */ /*
* Must have thisdefinition in here for the proper
* alignment ofarray_cache. Also simplifies accessing
* the entries.
*/
};
在per-CPU缓存上分配对象时,是从后往前分配的,每分配出一个对象,avail减1,所以avail除了表示可用的对象数量,还是一个数组下标,可以通过array.entry[avail]直接获取对象。而在释放对象时,则先放到per-CPU缓存的最后,因为内核假设刚释放的对象仍然处在CPU高速缓存中,会尽快在此分配它。
per-CPU缓存的entry[]是一个指针数组,所以它只是存放对象的指针,真正的对象在slab缓存中。
我们先看一下一个on slab的cache中每个slab的结构:
图中,slab中的对象总数由cachep->num记录。colour为着色区,我们不去关注。slab管理信息部分包括一个struct slab结构,以及cachep->num个kmem_bufctl_t的值,方便来定位某个对象。浅蓝色的对象区域存放实际的对象,每个对象大小由cachep->buffer_size指出。可以看出,一个slab缓存中,有很多空间用作了管理信息。
struct slab结构体定义如下,注意inuse和free两个成员的含义:
struct slab {
struct list_head list;
unsigned long colouroff;
void *s_mem;
/* 已经被使用的对象,最多为cachep->num*/
unsigned int inuse;
/* 标识当前还未被分配的对象的kmem_bufctl_t区域偏移量 */
kmem_bufctl_t free;
unsigned short nodeid;
};
我们使用kmem_cache_alloc(cachep, flags)在cache上申请一个对象时的分配步骤如下:
1. 先试图在array cache即cachep->array[0]->entry[]上获取,它上面可分配的对象的数目由cachep->array[0]->avail记录。如果这一步找到了可用对象,就返回ac->entry[--ac->avail]。注意,在kmem_cache_create的时候,没有分配对象的空间,所以avail肯定是0的。
2. 当avail=0的时候,则array cache已经用完。就试图在kmem_list3即cachep->nodelists[0]上面分配,这就是cache_alloc_refill()函数需要做的事情。structkmem_list3中有三个双向链表,分别指向“已部分使用的slab”、“已用尽的slab”和“完全空闲的slab”。
1) cache_alloc_refill()先查找slabs_partial链表中有没有slab,如果有则在这里分配;如果没有则查找slabs_free链表中有没有slab,如果有则在这里分配。在这两个链表上分配对象的方式为:将slab中的cachep->array[0]->batchcount数量的对象“分配”给cachep->array[0]->entry[],这个“分配”的过程只是指针指向的操作。然后cachep->array[0]的inuse成员的值增加,free成员前移,同时更新cachep->array[0]->avail的值。这样,array cache上又可分配对象了,于是返回ac->entry[--ac->avail]。
注意,在slab分配出去batchcount个对象后,需要判断该slabs链表是否用尽,如果用尽就将其转移到slabs_full链表中。
2) 如果在slabs_partial和slabs_free链表中都没有slab对象了,就需要在内存中重新分配一个slab。这是cache_grow()函数的工作。
3. cache_grow()函数的主要任务就是在伙伴系统中分配2^(cachep->gfporder)个页,并给分配的页都加上PG_slab标记使其为slab使用。该函数具体实现如下:
a) 算出此次分配的slab的colour空间,其实这些结构体的colour相关的成员在内核中并没有用到。下面代码中cachep->colour是在创建cache的时候通过left_over计算的,为颜色数量,l3->colour_next是当前选择cachep->colour中的哪个颜色,cachep->colour_off是每个颜色占用的空间大小。最后算出的offset即是上图中最开始的colour区域的长度。
l3 = cachep->nodelists[nodeid];
spin_lock(&l3->list_lock);
offset = l3->colour_next;/* init as 0 */
l3->colour_next++;
if (l3->colour_next >= cachep->colour)
l3->colour_next = 0; /* 取值为0 ~ colour-1 */
spin_unlock(&l3->list_lock);
offset *= cachep->colour_off;
例如如果一个cache的cachep->colour=3,即有三种颜色,则分配的slab开头的colour区域的长度就可能为0、cachep->colour_off和2 * cachep->colour_off。
b) 分配2^(cachep->gfporder)个页,并给分配的页加上PG_slab标记。分配成功后获得第一个页的起始地址的指针objp。
c) 在objp中分配slab管理信息的空间。这里分为两种情况,如果cache是on slab的,则直接在objp的地址上非slab管理信息分配空间,并给struct slab的成员赋值。而如果是off slab的,即slab管理信息和slab对象不在一起,这时slab管理信息对象是在cachep->slabp_cache上分配的,分配的函数依然是调用kmem_cache_alloc()。
d) 将分配的所有页与所属的slab和cache建立映射关系,具体做法为,将objp开始的2^(cache->gfporder)个页对应的struct page都进行以下赋值:
page->lru.next = (structlist_head *)cache;
page->lru.prev = (structlist_head *)slab;
其中cache和slab是当前的cache和刚分配的slab信息。这样的目的是为了可以方便的找到一个对象所属于的slab和cache。
e) 调用每个对象的ctor方法,并给每个对象对应的kmem_bufctl_t赋个值,这个值从1开始,直到cachep->num-1,最后一个kmem_bufctl_t赋值为BUFCTL_END。
f) 将分配好的slab添加到l3的slabs_free列表中,即cachep->nodelists[0]->slabs_free列表。同时l3->free_objects += cachep->num,注意这个值是l3中的三个列表中可用对象的总数,但不是cachep中可用对象的总数,因为ac->entry[]中还有。
cache_grow()返回后,重新执行cache_alloc_refill()函数,这时便可以在上述的步骤中便可以找到一个对象来返回。
我们再回过头来看一下为cachep分配slab管理信息的函数alloc_slabmgmt()。上面的c)步骤中讲到在off slab的时候要在cachep->slabp_cache上分配slab管理信息,我们知道这个slabp_cache是在kmem_cache_create的时候根据slab_size大小在通用cache上选择的一个合适的cache。而在分配slab管理信息的时候,slab管理信息作为cache的对象slabp_cache又会有它自己的slab管理信息,这样又会重复这一分配动作,必定会导致递归,当然递归的前提是slabp_cache是off slab的,也就说,slabp_cache不能是off slab的。我实际看到的slabp_cache都是size-64或size-32,因此都是on slab的,我人为的将size-64或size-32改为off slab的,不出所料,kernel就起不来了。
如果cache是off slab的,那它的slab结构分为两部分:
2.4 在SLAB缓存释放空间
释放slab对象使用kmem_cache_free()函数,它直接调用了__cache_free()函数。
释放一个对象时,分为两种情况:
1. 如果per-CPU缓存中可用对象数目小于其limit的限制,则直接将对象释放到per-CPU缓存中。
2. 如果per-CPU缓存中可用对象数目达到其limit的限制,则需要先将batchcount数目的对象释放到slab缓存中,这个释放动作顺序为从前往后(即释放下标为0~batchcount-1的对象),因为这时最开始释放的对象很可能已经不在高速缓存中了。然后再将我们要释放的对象释放到per-CPU缓存中,并且将之前下标为batchcount以及之后的对象前移。
static inline void__cache_free(struct kmem_cache *cachep, void *objp)
{
struct array_cache *ac = cpu_cache_get(cachep);
……
if (likely(ac->avail < ac->limit)) {
ac->entry[ac->avail++] = objp;
return;
} else {
cache_flusharray(cachep, ac);
ac->entry[ac->avail++] = objp;
}
}
释放部分对象到slab缓存中的函数为cache_flusharray(),最终通过free_block()完成的,free_block()函数的工作是:
1. 获取对象所在的slab缓存,这是通过virt_to_page()来完成的。前面在分析cache_grow()函数时讲到过slab和page的关系。
2. 将得到的slab从缓存链表中删除。
3. 将对象放回到slab中。
4. 将slab重新添加到缓存链表中,分两种情况:如果这时slab中所有对象都是未使用的,就将其放到slabs_free链表中,否则将其放到slabs_partial链表中。另外,如果将slab放到slabs_free链表,会先检查缓存中空闲对象数目总数是否超过了预定义的free_limit限制,如果超过了,则直接调用slab_destroy()释放掉这个slab。
static voidfree_block(struct kmem_cache *cachep, void **objpp, int nr_objects,
int node)
{
int i;
struct kmem_list3 *l3;
for (i = 0; i < nr_objects; i++) {
void *objp = objpp[i];
struct slab *slabp;
/* 获取对象所在的slab */
slabp = virt_to_slab(objp);
l3 = cachep->nodelists[node];
/* 将slab删除 */
list_del(&slabp->list);
check_spinlock_acquired_node(cachep, node);
check_slabp(cachep, slabp);
/* 将对象放回slab中 */
slab_put_obj(cachep, slabp, objp, node);
STATS_DEC_ACTIVE(cachep);
l3->free_objects++;
check_slabp(cachep, slabp);
/* 将slab重新添加到缓存链表中 */
if (slabp->inuse == 0) {
/* 如果对象总数超出限制,释放整个slab */
if (l3->free_objects > l3->free_limit) {
l3->free_objects -= cachep->num;
/* No need to drop any previously held
* lock here,even if we have a off-slab slab
* descriptor itis guaranteed to come from
* a differentcache, refer to comments before
*alloc_slabmgmt.
*/
slab_destroy(cachep, slabp);
} else {
/* 添加到slabs_free链表中的开头 */
list_add(&slabp->list, &l3->slabs_free);
}
} else {
/* Unconditionally move a slab to the end of the
* partial list onfree - maximum time for the
* other objects tobe freed, too.
*/
/* 添加到slabs_partial链表的末尾 */
list_add_tail(&slabp->list,&l3->slabs_partial);
}
}
}
2.5 销毁SLAB缓存
要销毁一个slab缓存(struct kmem_cache结构的实例),需要调用kmem_cache_destroy()函数,该函数删除缓存的步骤为:
1. 将cachep从cache_cache链表中删除。
2. 将cachep中所有对象释放掉,空间还给伙伴系统。如果该slab缓存是off-slab的,还要将slab管理信息从cachep->slabp_cache中释放。
3. 将cachep的per-CPU缓存和struct kmem_list3结构释放。
4. 由于cachep是cache_cache的一个对象,所以需要将cache_cache中将该对象删除,这一步使用了kmem_cache_free()函数,将slab缓存包括它管理的所有对象都释放。
2. kmalloc
slab是kmalloc的基础,kmalloc使用上面讲到的通用slab缓存来分配空间。
void *kmalloc(size_t size,gfp_t flags);
kmalloc可分配的最大size由KMALLOC_MAX_SIZE定义,这个值在2^25B和buddy的最大分配阶之间取一个小值。
#defineKMALLOC_SHIFT_HIGH ((MAX_ORDER +PAGE_SHIFT - 1) <= 25 ? \
(MAX_ORDER + PAGE_SHIFT - 1) : 25)
#define KMALLOC_MAX_SIZE (1UL<< KMALLOC_SHIFT_HIGH)
#defineKMALLOC_MAX_ORDER (KMALLOC_SHIFT_HIGH -PAGE_SHIFT)
kmalloc的实现很简单:
1. 根据size大小找到最小能装下一个对象的通用cache。
2. 调用kmem_cache_alloc(cachep,flags)进行分配。
由此可知,kmalloc分配的空间是物理上连续的。
原文:https://blog.csdn.net/jasonchen_gbd/article/details/44024009
转载于:https://www.cnblogs.com/longchang/p/10749402.html