关于slub分配器的一些细节

很早以前写过一篇《linux的最新slab分配器---slub分配器》,那篇文章从源代码的角度分析了slub 分配器的大致流程,但是一些细节没有注意到,因此本文就谈一下slub的一些细节,其实还是要从轮廓上考虑细节,真正的细微之处和单纯的源代码一样很容易 让人迷失方向。关于slub的所谓的细节就是:
1.slub的对象何时加入半满链表。按照一般意义上理解,slub对象开始存在于一系列page 上,然后在一个时间点将这个page加入到半满链表,那么这个时间是何时呢?其实是在第一个对象被释放的时候。看看slab_free的慢释放函数 _slab_free就可以知道:
prior = object[offset] = page->freelist;//看alloc的代码,如果一个slab直到用完也没有一个释放,那么,它将保持page->freelist为空的状态。
page->freelist = object;//执行万这里,前面的prior就没有机会为空了。因为现在释放了一个,因此下次再执行到这里的时候,prior就不为空了。
page->inuse--;            //正常流程,使用计数递减,这个和半满链表没有关系。
if (unlikely(SlabFrozen(page))) {  //只要一个基页正在被使用,那么它肯定是frozen的,具体下面分析。
    goto out_unlock;
}
if (unlikely(!page->inuse))
    goto slab_empty;
if (unlikely(!prior)) {   //在没有对象释放时,基页的freelist肯定是NULL,但是在这个释放函数中肯定释放了对象,因此下次就不为空了,但是上次的freelist为 空表明这个基页刚刚因为我们的释放动作有了空闲对象,因此就在这个时候将这个基页加入半满链表。
    add_partial(get_node(s, page_to_nid(page)), page, 1);
    stat(c, FREE_ADD_PARTIAL);
}

上面的注释说“只要一个基页正在被使用,那么它肯定是frozen的”这是什么意思呢?我们看看代码中都在哪里调用了SetSlabFrozen将一个基页 设置为冻结以及在哪里解除冻结,看完发现就在两个地方冻结了基页,一个是__slab_alloc中调用new_slab重新分配新页面之后并且使用这个 新页面分配的slub对象时,另一个就是在get_partial取出一个半满链表使用时。我们称这两种方式为“线下使用”,联系一下cfs的 rbtree,当进程从rbtree的左下角被选中开始运行时,被选中的进程也要“线下”运行,这个线下策略使管理更加简单,解除了被选的元素和落选的元 素之间的错综复杂的关系,可以单独设计出两个模块管理二者,而不躲开了使用一个模块管理这两个本来就矛盾的元素,要知道兼顾选中的和落选的是很麻烦的。解 除冻结就是在基页回到半满链表的时候或者在基页存在但是没有空闲对象可分配从而分配了一个新的页面要替代这个老的基页的时候。
2.在因为得不到基页导致的重新分配基页之后,还可能得到老基页从而导致新分配的页面被废弃。这个怎么讲呢?看看代码就明白了:
page = new_slab(s, gfpflags, node);
if (page) {
    cpu = smp_processor_id();  //先得到我们所在的cpu,这里的意义下面解释
    if (s->cpu_slab[cpu]) {    //如果该cpu对应该缓存的基页存在
        if (node == -1 || page_to_nid(s->cpu_slab[cpu]) == node) { //如果node信息也对口
            discard_slab(s, page);      //那么就不用新分配的页面了,因为考虑到硬件缓存,老的页面被用过,缓存比较热,因此优先使用那个老的基页。
            page = s->cpu_slab[cpu];
            slab_lock(page);
            goto load_freelist;
        }
        flush_slab(s, s->cpu_slab[cpu], cpu);  //node信息不对的话,我们还是要用新的页面,但是因为在这个cpu上存在老的基页,那么一定将老的基页刷新,其实就是解除其冻结状态,因为已经不再 使用了,然后在老基页存在空闲对象的情况下加入到半满链表,如果没有空闲对象,那么将延迟到在老基页释放第一个对象时再将它加入半满链表。
    }
    slab_lock(page);  //使用新的基页
    SetSlabFrozen(page);   //下线
    s->cpu_slab[cpu] = page;  //配置
    goto load_freelist;
}

最开始处有一个smp_processor_id的调用,我们来看看这是为什么,前面的代码指示在没有基页存在时才会到达new_slab分配新页面,可是 要知道在判断得到没有基页这个信息执行绪所在的cpu和得到了新的页面之后所在的cpu并不一定是一个,因为在分配新页面的时候当前上下文可能会睡眠,那 么在分配完毕以后就有可能被调度到别的cpu上了,因此一定要判断当前cpu上的情形再往下走,到哪里都要先探路,即使是我们自己的地盘,一觉醒来也可能 发生天翻地覆的变化,更何况分配新页面的情况很复杂,很可能天上一日地上一百年了。

3.slub消除内碎片的策略。这个细节很艺术啊,只看代码就可以看出个究竟,但是你的c语言功底一定要好,否则肯定晕,很简单:
分配:

object = c->freelist;
c->freelist = object[c->offset];
释放:
object[c->offset] = c->freelist;
c->freelist = object;
看 看这两个操作是多么对称,看到些什么了吗?其实这就是一个栈,分配就是一个pop操作而释放就是一个push操作,freelist的头就是栈顶,这个操 作保证被分配的slub对象永远是最近释放的那个,因此保持了硬件高速缓存的热度,另外保证一个slub基页中存在最少的“空洞”,空洞的存在对紧凑性不 利,而且也会影响局部性优势的发挥,其实也是和硬件缓存相关的。前些天有人问object为何定义成void**类型的而理应是void*类型的啊,这里 主要就是linux中惯用的小技巧,这里的void**中的第一个*表示就是一个对象的指针,而第二个*的意义实际不是什么指针,而仅仅是一个数组的下 标,其实就是c->offset了,这第二个*指针符号并没有“指向”的意思,而只是“+”的意思,其实就是在第一个指针的字面上加上一个 c->offset,以下的例子可以会更明白:
char str1[5];

char * str2 = str1;

这里的数组元素就是字符,如果是指针类型的话,就要将char *改为char **了,于是看下面的程序:
int main(int argv,char *argc[])
{
char ** str2 = argc[1];
        printf("%s/n",str2);
}
这个程序就可以把程序的命令行参数打印出来,注意,用的是char **类型而不是理应的char *,这里定义中的第二个*其实就是这个[1]而已。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值