主要介绍kmalloc和kfree代码流程,侧重kmalloc和kfree流程中锁使用规则,会引用到cpuset,mempolicy(内存策略),numa相关知识。如果读起来比较困难可以参考另一篇随笔《内存管理-slab[原理]》
kmalloc
kmalloc原型如下:
1 // /include/linuxslab_def.h 2 static __always_inline void *kmalloc(size_t size, gfp_t flags)
函数功能:从内核态内存中申请size字节大小的内存段返回给系统,内核态内存指的是DMA区和NORMAL区,即kmalloc不能申请HIGHTMEM区的内存(其实再64位系统中不存在HIGHTMEM区)
结合代码流程将kmalloc流程分成两个阶段,下分会详细介绍这两个阶段的内容
kmalloc第一个阶段
第一个阶段函数调用路径如图(1)
图(1)
这个阶段主要完成的功能如下:
A.根据入参size和gfp_t选择合适的kmam_cache数据结构。具体如下:size的范围是1到2的(MAX_ORDER + PAGE_SHIFT - 1)次方,2的MAX_ORDER-1次方是buddy一次alloc_pages所能申请到的最大连续物理页数(大多数系统是2^10次方),2的PAGE_SHIFT次方是每个物理页框的大小。(宗上:得到一个结论,一个slab描述符最多经过一次alloc_pages获取内存,一次free_pages释放内存,并且slab下管理的最大内存段上限是2的(MAX_ORDER + PAGE_SHIFT - 1)次方.)根据size我们只能选定特定长度的kmem_cache,但《内存管理-slab[原理]》一文中提到每一个长度的对象对应的kmem_cache有两个,一个用来管理NORMAL区或者DMA区内存,两外一个专门管理DMA区内存,因此还需要第二个参数,才能选到唯一的kmem_cache
B.根据入参flags从1中通过size筛选出来的两个kmem_cache中选择一个。具体如下:如果flags中GFP_DMA位被值位,就选择专门用来管理DMA区内存的kmem_cache.否者则选择另一个用来管理NORMAL区或者DMA区内存的kmem_cache.(gfp具体参数可以通过如下命令过滤内核源码看到大概情况:grep -r '[^_]*kmalloc[[:space:]]('|sed -nr 's/.*,[[:space:]]*(GFP.*)$/\1/gp')
C.利用local_irq_save关闭cpu的本地中断.关中断原因:接下来会访问到kmem_cache以及其下的slab描述符等数据结构,而kmalloc是允许在中断上下文调用的,所以必须关中断来对中断上下文同步。这里特意用irq_save和irq_restore来关中断,而不是用irq_disable和irq_enable来关中断原因是kmalloc允许在关中断后调用。
上面代码经过1->4和2->3就功能来说完全相同,就是完成上面的A和B两个功能。但是为什么要分两个路径?原因:1->4这个路径在通过sizeof(XXX)的形式给出入参size的情况下会提高些性能;其实只要size入参是编译器能够计算出来的常量时,都走1->4这个路径,否者走2->3这个路径(注意kmalloc原型时inline)。路径5这里完成功能C,即关中断,调用__do_cache_alloc函数,__do_cache_alloc返回后,开中断,然后根据GFP_ZERO|flags结果来判断是否对返回的对象做memset(0,...)的操作。
到此介绍完了第一阶段的函数流程,宗上:第一阶段主要动作按时序如下:选择唯一一个kmem_cache->关中断->调用__do_cache_alloc来分配对象->开中断->根据GFP_ZERO|flags结果判断是否要对分配到的对象做memset(0,...)的操作。
kmalloc第二个阶段
通过第一个阶段得出结论:实际分配对象的操作是在__do_cache_alloc中来完成的。(注意__cache_alloc和__do_cache_alloc这两个函数的名字不是偶然的,在内核编码中经常出现XXXX和__do_XXXX这种组合,其中XXXX是__do_XXXX的包裹函数,XXXX中一般用来处理同步:比如关中断,关抢占,获取自旋锁,或者检查入参合法性等等,而实际的工作时在__do_XXXX中完成)
slab分配要实现的目标:
1.如果kmalloc入参flags显示有GFP_THISNODE:只能在当前cpu的本地节点分配内存
2.如果kmalloc入参flags没有GFP_THISNODE且在中断上下文:首先在当前cpu的本地节点分配对象,如果本地节点分配失败,则依次在系统中所有的节点上分配
3.如果kmalloc入参flag没有GFP_THISNODE且不在中断上下文且进程设置了cpuset或者mempolicy,限制只能在某个或某些node节点上分配内存:1.首先根据cpuset和mempolicy来选择一个节点,在这个节点上分配对象。2.如果所选择的这个节点分配不到对象则依次fallback到cpuset和mempolicy所允许的节点集合中分配对象,如果上面集合中的所有节点都分配不到对象,则尝试第二次(这里不是完全的重新尝试,类时重新尝试一次),如果第二次分配失败,则返回失败。
4.如果kmalloc参flag没有GFP_THISNODE且不在中断上下文且没有设置cpuset或mempolicy,
如图2是第二个阶段的关键点函数以及调用路径,总的来说需要注意两点:
α:其中1,2,3是并行关系,即__do_cache_alloc先调用1,1成功分配到对象则直接返回对象(成功),否者调用3,如果3成功分配到对象则直接返回(成功),否者调用2,如果2成功分配到对象则直接返回(成功),否者返回NULL(失败)
β:路径1和2最后都汇聚到___cache_alloc_node函数,而调用路径8->9出现了递归调用,找到这个递归调用结束的条件,是理解这部分代码的关键。
图(2)
alternate_node_alloc,___cache_alloc_node, ___cache_alloc的原型如下,结合图(2)1->4和2最后都汇聚到___cache_alloc_node函数,因此这个函数是kmalloc的核心。下面我们分别介绍1,2,3三个路径上函数完成的功能。
1 //:/mm/slab.c 三个函数的flags字段都是由kmalloc透传过来,没有做过任何改变 2 static void *alternate_node_alloc(struct kmem_cache *cachep, gfp_t flags); 3 static void *____cache_alloc_node(struct kmem_cache *cachep, gfp_t flags,int nodeid); 4 static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags);
路径1
由于路径1经过alternate_node_alloc后和路径2汇聚,路径1讨论主要集中在函数__do_cache_alloc调用alternate_node_alloc的时机,以及alternate_node_alloc完成的工作。
路径1主要是为了处理cpuset和mempolicy。首先,cpuset和mempolicy针对内核slab内存分配都是用来限制进程上下文分配的内存,不会限制中断(包括软中断和硬中断)上下文的内存分配。