slub的理念

slub分配器对一个内核开发者来讲是既熟悉又陌生的。

熟悉是因为在开发的过程中大家总会使用到它,什么kmem_cache_alloc(), kmalloc()都是slub分配器的接口。而陌生是因为大部分开发者都不了解slub分配器的工作机制。像页分配器大家至少还听说过伙伴系统,而slub分配器好像真的一点绯闻都没有。

那今天我就尝试用我这粗陋的认知给大家揭开一点点盖头来。

从设计理念开始

假装自己懂得很多的样子,给大家讲讲我对内存模块的理解。

内存模块就是一个大管家,管理着系统中内存的使用。有人要用内存了,它分配一点。有人释放内存了,它小心翼翼收好。

在整个模块中,我们能看到其运用了三个设计思想。

第一个思想就是分层。通过之前的文章分析我们也能看到在x86架构上分成了:e820, memblock, page, slub这么几层。分层设计的思想在计算机业十分普遍,其优势也自然用不着我说。

第二个思想我认为是分类管理

这个思想从页分配器这一层就开始了。为了更好的管理内存,在页分配器这一层,页就依照NUMA和ZONE的属性分类,在不同需求时分配不同的页。而且页还依照他连续空闲的大小分类,这样对不同大小的内存请求也可以快速找到空闲页。slub中也是这种思想,

其做法就是事先分配好指定大小的内存板,当有内存请求时,直接在指定的内存板中分配和释放。

而这个思想在我们生活中也有用武之地,当遇到比较多的物品时,就可以分类存放方便使用。比如忘了在哪个节目中听说有女明星有上百双鞋放满了一个屋子,那这个时候就可以按照运动鞋,高跟鞋,休闲鞋等分类。否则找鞋所花费的检索时间还不如直接网购一双来的快。

第三个思想是预取,也就是事先准备好一些资源而不是每次等有请求时再去分配。

这个思想也很常见,比如说cpu上的cache。当每次读取一段数据的时候,把其后的一段内容也加载进cache。根据概率统计,在读取一部分内容后有很大概率会访问之后的部分内容,所以这样的设计可以提升性能。(PS:这也导致了各种性能优化的小窍门)

slub分配器也使用了这样的思想。在每一个分类的内存板中,都预先保存了空闲的内存以便于快速响应。

好了,吹牛的部分讲完了,接下来就看看slub是究竟如何做到分类管理和预取的。

分类管理

在slub分配器中,用来做分类管理的就是这个kmem_cache结构体了。

kmem_cache                      
+------------------------------+
|name                          |
|    (char *)                  |
+------------------------------+
|object_size                   |   = original object size
|inuse                         |   = ALIGN(object_size, sizeof(void *))
|size                          |   = ALIGN(inuse + padding + debug space, s->align)
|align                         |
|offset                        |
|    (int)                     |
|reserved                      |
+------------------------------+
|oo                            |
|min                           |
|max                           |
|   (kmem_cache_order_objects) |
|   +--------------------------+
|   |order                     |
|   |order_objects             |
+---+--------------------------+

说到分类,我们总是按照某些属性分的,比如说鞋子的用途。那在slub中按照什么属性分类呢?

首先就是按照名字了。所以我们看到在调用kmem_cache_create()的第一个参数就是name,而这个名字就保存在了kmem_cache中的name字段。我们可以通过

cat /proc/slabinfo

来查看系统中slub的分类。比如会有常见的task_struct, inode_cache等。

另一种是按照大小分类。我们可以在上面命令的输出中看到kmalloc-512, kmalloc-256等字样。这就是那些没有特定名字,按照大小来分配时选用的内存板。

其实按照名字分类时隐含了按大小分类的意思,这里单独列出是为了引出slub中对大小的一个计算,也就是按照什么样的标准进行预取。

预取

预取的目的是为了能够提高系统的性能,那它是如何做到这一点的呢?我们来看看生活中的例子。

比如在超市中,我们看到货架上琳琅满目的商品,这都可以算是预取。超市按照估计预先把商品放在货架上,等待顾客的购买。好像没有见过哪个超市是每个商品只摆放一个的吧?如果每种商品货架上只有一个,那么如果有几个人要购买,还不要等很久?

预取首先节省了顾客的平均购买时间。

接下来我们再来看看另一头,服务员的工作。顾客在购买的过程中,服务员也会不断地补充货架。但是从仓库取出商品,到摆放到货架需要一定的时间。如果每当一个商品被拿走就去仓库取出一个商品补充,那是不是太琐碎了呢?如果有哪个超市是这么做的,那么这个超市肯定要倒闭。所以超市中改进的方案是,当货架上的货物基本卖完了后,才去仓库取出一批货物补充货架。

预取其次降低了服务员的工作负担。

道理都是懂的,但是落到操作上就有一个实际问题需要解决--一批是多少?拿少了不能最大化每次取货的劳动,拿多了货架上放不下。那如果是你,你会如何定义“一批”的数量呢?

如果是我的话,我可能会选择补满一货架

生活中如此,内核中也是如此。

内存管理模块是层次化的,slub分配器建立在页分配器上,所以可以牵强的理解为页是slub的货架

超市中货物上架前需要做好两个计算:

  • 货架的大小

  • 货架上能放多少货物

在slub中同样要计算相应的两个值:

  • 用多大的页来作为货架

  • 每个页中可以放多少object

这两个数据都保存在上图kmem_cache结构体中的oo字段。整个计算过程在calculate_sizes()函数中,图表中其余字段在计算过程中各有用途。

PS:上面说到的页不是单个的物理页,而是内核struct page对应页的概念。

来举两个例子说明一下问题:

  • 假如想要申请的结构体(货物)大小是512字节,那么页(货架)可以选择为4K字节大小,每个页(货架)上就可以存放8个结构体(货物)。

  • 假如想要申请的结构体(货物)大小是2050字节,那么页(货架)可以选择为8K字节大小,每个页(货架)上就可以存放3个结构体(货物)。

当然在实际计算的时候第二种情况的值可能不是这样,因为大家可以看到这么选择其实会有较大的浪费,内核很有可能选择更大的页来减少内存浪费。

好了,希望本文对大家理解slub有一点点的帮助。slub依然博大精深,还有很多非常巧(nue)妙(xin)的设计。有兴趣的童鞋做好心理准备~

Last updated