人生像是在跑马拉松,能够完赛的都是不断地坚持向前迈进;人生就是像在跑马拉松,不断调整步伐,把握好分分秒秒;人生还是像在跑马拉松,能力决定了能跑短程、半程还是全程。人生其实就是一场马拉松,坚持不懈,珍惜时间。
分类: LINUX
2015-11-20 23:38:58
前面分析了slub分配算法的初始化,继续分析slub分配算法的slab创建过程。
Slub分配算法创建slab类型,其函数入口为kmem_cache_create(),具体实现:
该函数的入参name表示要创建的slab类型名称,size为该slab每个对象的大小,align则是其内存对齐的标准, flags则表示申请内存的标识,而ctor则是初始化每个对象的构造函数,至于实现则是简单地封装了kmem_cache_create_memcg()。
继而分析kmem_cache_create_memcg()的实现:
函数入口处调用的get_online_cpus()与put_online_cpus()是配对使用的,用于对cpu_online_map的加解锁;接下来的kmem_cache_sanity_check()主要是用于合法性检查,检查指定名称的slab是否已经创建,仅在CONFIG_DEBUG_VM开启的时候起作用;如果memcg不为空指针,表示创建的slab与memcg关联,此外由于每memcg的cache会在初始化分配的时候异步创建,多个线程将会尝试创建同样的cache,但只有一个会创建成功,那么如果代码执行到此处调用cache_from_memcg_idx()检查到cache已经被创建,那么cache_from_memcg_idx()将会返回NULL;再往下的__kmem_cache_alias(),该函数检查已创建的slab是否存在与当前想要创建的slab的对象大小相匹配的,如果有则通过别名合并到一个缓存中进行访问。
看一下__kmem_cache_alias()具体实现:
该函数主要通过find_mergeable()查找可合并slab的kmem_cache结构,如果找到的情况下,将kmem_cache的引用计数作自增,同时更新kmem_cache的对象大小及元数据偏移量,最后调用sysfs_slab_alias()在sysfs中添加别号。
进一步分析find_mergeable()函数的具体实现:
该查找函数先获取将要创建的slab的内存对齐值及创建slab的内存标识。接着经由list_for_each_entry()遍历整个slab_caches链表;通过slab_unmergeable()判断遍历的kmem_cache是否允许合并,主要依据主要是缓冲区属性的标识及slab的对象是否有特定的初始化构造函数,如果不允许合并则跳过;判断当前的kmem_cache的对象大小是否小于要查找的,是则跳过;再接着if ((flags & SLUB_MERGE_SAME) != (s->flags & SLUB_MERGE_SAME)) 判断当前的kmem_cache与查找的标识类型是否一致,不是则跳过;往下就是if ((s->size & ~(align - 1)) != s->size)判断对齐量是否匹配,if (s->size - size >= sizeof(void *))判断大小相差是否超过指针类型大小,if (!cache_match_memcg(s, memcg))判断memcg是否匹配。经由多层判断检验,如果找到可合并的slab,则返回回去,否则返回NULL。
回到kmem_cache_create_memcg(),如果__kmem_cache_alias()找到了可合并的slab,则将其kmem_cache结构返回。否则将会创建新的slab,其将通过kmem_cache_zalloc()申请一个kmem_cache结构对象,然后初始化该结构的对象大小、对齐值及对象的初始化构造函数等数据成员信息;其中slab的名称将通过kstrdup()申请空间并拷贝存储至空间中;接着的memcg_alloc_cache_params()主要是申请kmem_cache的memcg_params成员结构空间并初始化;至于往下的__kmem_cache_create()则主要是申请并创建slub的管理结构及kmem_cache其他数据的初始化,具体后面将进行详细分析。
接着往下的out_unlock标签主要是用于处理slab创建的收尾工作,如果创建失败,将会进入err分支进行失败处理;最后的out_free_cache标签主要是用于初始化kmem_cache失败时将申请的空间进行释放,然后跳转至out_unlock进行失败后处理。
具体看一下__kmem_cache_create()实现:
其中里面调用的kmem_cache_open()主要是初始化slub结构。而后在调用sysfs_slab_add()前会先解锁slab_mutex,这主要是因为sysfs函数会做大量的事情,为了避免调用sysfs函数中持有该锁从而导致阻塞等情况;而sysfs_slab_add()主要是将kmem_cache添加到sysfs。如果出错,将会通过kmem_cache_close()将slub销毁。
深入分析kmem_cache_open()实现:
这里面的kmem_cache_flags()用于获取设置缓存描述的标识,用于区分slub是否开启了调试;继而调用calculate_sizes()计算并初始化kmem_cache结构的各项数据。
具体calculate_sizes()实现:
最前面的ALIGN(size, sizeof(void *))是用于将slab对象的大小舍入对与sizeof(void *)指针大小对齐,其为了能够将空闲指针存放至对象的边界中;如果开启CONFIG_SLUB_DEBUG配置的情况下,接下来的if ((flags & SLAB_POISON) && !(flags & SLAB_DESTROY_BY_RCU) && !s->ctor)判断则为了判断用户是否会在对象释放后或者申请前访问,以设定SLUB的调试功能是否使能,也就是决定了对poison对象是否进行修改操作,其主要是为了通过将对象填充入特定的字符数据以实现对内存写越界进行调测,其填入的字符有:
#define POISON_INUSE 0x5a /* for use-uninitialised poisoning */
#define POISON_FREE 0x6b /* for use-after-free poisoning */
#define POISON_END 0xa5 /* end-byte of poisoning */
再接着的if ((flags & SLAB_RED_ZONE) && size == s->object_size)检验同样用于调测,其主要是在对象前后设置RedZone信息,通过检查该信息以扑捉Buffer溢出的问题;然后设置kmem_cache的inuse成员以表示元数据的偏移量,同时表示对象实际使用的大小,也意味着对象与空闲对象指针之间的可能偏移量;接着往下的if (((flags & (SLAB_DESTROY_BY_RCU | SLAB_POISON)) || s->ctor))判断是否允许对象写越界,如果不允许则重定位空闲对象指针到对象的末尾,并设置kmem_cache结构的offset(即对象指针的偏移),同时调整size为包含空闲对象指针。
同样在开启了CONFIG_SLUB_DEBUG配置的情况下,如果设置了SLAB_STORE_USER标识,将会在对象末尾加上两个track的空间大小,用于记录该对象的使用轨迹信息(分别是申请和释放的信息)。具体会记录什么,可以看一下track的结构定义;此外如果设置了SLAB_RED_ZONE,将会新增空白边界,主要是用于破获内存写越界信息,目的是与其任由其越界破坏了空闲对象指针或者内存申请释放轨迹信息,倒不如捕获内存写越界信息。
再往下则是根据前面统计的size做对齐操作并更新到kmem_cache结构中;然后根据调用时的入参forced_order为-1,其将通过calculate_order()计算单slab的页框阶数,同时得出kmem_cache结构的oo、min、max等相关信息。
着重分析一下calculate_order():
其主要是计算每个slab所需页面的阶数。经判断来自系统参数的最少对象数slub_min_objects是否已经配置,否则将会通过处理器数nr_cpu_ids计算最小对象数;同时通过order_objects()计算最高阶下,slab对象最多个数,最后取得最小值min_objects;接着通过两个while循环,分别对min_objects及fraction进行调整,通过slab_order()计算找出最佳的阶数,其中fraction用来表示slab内存未使用率的指标,值越大表示允许的未使用内存越少,也就是说不断调整单个slab的对象数以及降低碎片指标,由此找到一个最佳值。
如果对象个数及内存未使用率指标都调整到最低了仍得不到最佳阶值时,将尝试一个slab仅放入单个对象,由此计算出的order不大于slub_max_order,则将该值返回;如果order大于slub_max_order,则不得不尝试将阶数值调整至最大值MAX_ORDER,以期得到结果;如果仍未得结果,那么将返回失败。
末尾看一下slab_order()的实现:
该函数入参size表示对象大小,min_objects为最小对象量,max_order为最高阶,fract_leftover表示slab的内存未使用率,而reserved则表示slab的保留空间大小。内存页面存储对象个数使用的objects是u15的长度,故其最多可存储个数为MAX_OBJS_PER_PAGE,即32767。所以如果order_objects()以min_order换算内存大小剔除reserved后,通过size求得的对象个数大于MAX_OBJS_PER_PAGE,则改为MAX_OBJS_PER_PAGE进行求阶。如果对象大小较大时,页面容纳的数量小于MAX_OBJS_PER_PAGE,那么通过for循环,调整阶数以期找到一个能够容纳该大小最少对象数量及其保留空间的并且内存的使用率满足条件的阶数。
末了回到kmem_cache_open()函数中继续查看其剩余的初始化动作。
其会继续初始化slub结构,set_min_partial()是用于设置partial链表的最小值,主要是由于对象的大小越大,则需挂入的partial链表的页面则容易越多,设置最小值是为了避免过度使用页面分配器造成冲击。再往下的多个if-else if判断赋值主要是根据对象的大小以及配置的情况,对cpu_partial进行设置;cpu_partial表示的是每个CPU在partial链表中的最多对象个数,该数据决定了:1)当使用到了极限时,每个CPU的partial slab释放到每个管理节点链表的个数;2)当使用完每个CPU的对象数时,CPU的partial slab来自每个管理节点的对象数。
kmem_cache_open()函数中接着往下的是init_kmem_cache_nodes():
该函数通过for_each_node_state遍历每个管理节点,并向kmem_cache_node全局管理控制块为所遍历的节点申请一个kmem_cache_node结构空间对象,并将kmem_cache的s内的成员node初始化。
值得注意的是slab_state如果是DOWN状态,表示slub分配器还没有初始化完毕,意味着kmem_cache_node结构空间对象的cache还没建立,暂时无法进行对象分配,此时将会通过early_kmem_cache_node_alloc()进行kmem_cache_node对象的slab进行创建。这里补充说明一下:这是是slub分配算法初始化才会进入到的分支,即mm_init()->kmem_cache_init()->create_boot_cache()->create_boot_cache(kmem_cache_node, "kmem_cache_node",sizeof(struct kmem_cache_node), SLAB_HWCACHE_ALIGN)-> __kmem_cache_create()->kmem_cache_open()->init_kmem_cache_nodes()->early_kmem_cache_node_alloc()该流程才会进入到early_kmem_cache_node_alloc()该函数执行,然后执行完了在kmem_cache_init()调用完create_boot_cache()及register_hotmemory_notifier()随即将slab_state设置为PARTIAL表示已经可以分配kmem_cache_node。
此外,如果已经创建了kmem_cache_node的slab,则将会通过kmem_cache_alloc_node()从初始化好的kmem_cache_node申请一空闲对象。
现在对前期的缺失进行补全分析,进入early_kmem_cache_node_alloc(),分析一下其实现:
该函数将先通过new_slab()创建kmem_cache_node结构空间对象的slab,如果创建的slab不在对应的内存节点中,则通过printk输出调试信息;接着向创建的slab取出一个对象,并根据CONFIG_SLUB_DEBUG配置对对象进行初始化(init_object()对数据区和RedZone进行标识,同时init_tracking()记录轨迹信息),然后inc_slabs_node()更新统计信息;最后将slab添加到partial链表中。
而进一步分析new_slab()的实现:
首先通过allocate_slab()申请一个slab块,继而通过compound_order()从该slab的首个page结构中获取其占用页面的order信息,然后inc_slabs_node()更新内存管理节点的slab统计信息,而memcg_bind_pages()则是更新内存cgroup的页面信息;再接下来page_address()获取页面的虚拟地址,然后根据SLAB_POISON标识以确定是否memset()该slab的空间;最后则是for_each_object()遍历每一个对象,通过setup_object()初始化对象信息以及set_freepointer()设置空闲页面指针,最终将slab初始完毕。
结合初始化信息,可以总结出当创建的slab中对象在所有配置启用时的对象结构信息如图:
正如前面文章中对kmem_cache结构的成员解析,object_size是slab对象的实际大小, inuse为元数据的偏移量(也表示对象实际使用大小),而offset为存放空闲对象指针的偏移;inuse和offset在图中显示是相等的,但是并非完全如此,inuse是必然有值的,而offset则是看情况了;至于结构体中的size成员表示的则是整个对象的大小。SLAB_POISON设置项仅是对对象object_size的大小做标识,而SLAB_RED_ZONE则是对象与空闲对象指针相距的空间做标识,至于track信息则是在空闲对象指针之后了。BTW,在track之后,根据SLAB_RED_ZONE的设置,新增了一块sizeof(void *)大小的空间好像并未被使用,可能代码看得不够细,后续再细细斟酌一番。
回到代码往下,继续allocate_slab()函数的实现:
如果申请slab所需页面设置__GFP_WAIT标志,表示运行等待,则将local_irq_enable()将中断使能;接着将尝试使用常规的s->oo配置进行alloc_slab_page()内存页面申请。如果申请失败,则将其调至s->min进行降阶再次尝试申请;如果申请成功,同时kmemcheck调测功能开启(kmemcheck_enabled为true)且kmem_cache的flags未标识SLAB_NOTRACK 或DEBUG_DEFAULT_FLAGS,将会进行kmemcheck内存检测的初始化设置。接着根据flags的__GFP_WAIT标识与否将中断功能禁用。最后通过mod_zone_page_state计算更新内存管理区的状态统计。
其中的alloc_slab_page()则是通过Buddy伙伴算法进行内存分配:
伙伴算法就不再做重复讲解了。
回到kmem_cache_open()函数,在init_kmem_cache_nodes()之后,如果初始化成功,则将会继而调用alloc_kmem_cache_cpus():
该函数主要通过__alloc_percpu()为每个CPU申请空间,然后通过init_kmem_cache_cpus()将申请空间初始化至每个CPU上。
至此,slub算法中的缓冲区创建分析完毕。