学内核之十二:从slab内存管理想到的

接上文:学内核之十二:从slab内存管理想到的----之前言_龙赤子的博客-CSDN博客

开始之前,我们先有一些概念:内存中有内核代码、内核数据。内核在启动时,会建立恒等映射,启动MMU,完成虚拟到物理地址的转换。并在条件成熟时,建立新的线性映射,构建全功能的内存管理空间。在分配器本身构建之前,内核拥有一个简单的分配器,分割出可用内存,完成创世阶段的内存分配需求。之后,同样在条件成熟时,构建全功能的分配器,完成整体基础组件的搭建。

就像人类社会对土地的需求是多种多样的,内核里的各个模块对土地的需求也是多样的。有的需要承包大块土地,做机械化生产;有的需要散量土地,做精细化耕作;也有的需要特定量化的批量地块,做专业生产。也就是说,我们可能需要大块内存,可能需要等大的批量内存,也可能需要随机大小的分散内存。为了满足多样的需求,内核设计了层次化的内存管理模型。最底层提供大量内存,上面基于大量内存提供批量内存,再之上,根据需求,提供散量内存。这里分别对应了内核的伙伴系统、slab分配器和kmalloc分配接口。

上层的管理器基于下一层的管理器获取内存,然后向上一层提供内存。比如,slab从伙伴管理系统获取连续的物理页面,重新管理这些连续的物理页面,将其划分为obj对象,提供给应用kmalloc。这有点像工厂到代理到批发商再到零售商。Slab的obj对象既可能是task、网络skbuff、文件node等这些同等大小大量使用的数据结构(预先分配),也可能是没有固定大小的零碎数据结构,只不过这些数据结构都能对应到适合包装它大小的obj盒子。

上面是对内核内存的分层模型和slab分配器的一个简单描述。继续slab的介绍之前,先看一个小问题。当我们看slab相关资料和代码的时候,都会发现,需要一个Kmem_cache结构。这个结构是slab管理的头,通过这个头,顺藤摸瓜(指针)就能捋清slab的情况。参考下图:

该图片来自蜗窝科技。参见图解slub 

上图比较漂亮,但是跟最新版本的内核有点区别,主要是管理空闲、部分空闲和满链表部分。不过这不影响我们理解slab的思想,也就是意图。理解了意图,对于实现,也就能更好更快的掌握。下面再补充一张图片,来自网络:

 (插一句。画图是项耗时的工作,当我们欣赏这些图片时,应该心怀感激。就像荒野求生的Ed,对自己捕获的食物,总是赞不绝口。其实明眼人都能看出来,绝大部分很难吃。而对于这些又实用又好看的图片,没理由不赞美一下。)

继续。但是我发现,几乎所有的书籍和资料,都未曾讲解这个数据结构(kmem_cache)在内存的哪里。虽然有上述数据结构的分配接口介绍,但实际上,这些分配接口里只做数据项的填充(设置),没做数据结构本身的分配工作(传递的数据结构指针,都是直接使用的)。

其实,这个数据结构是静态分配的。内核将其放在数据区,不需要代码分配,就可以使用。我们可以称其为静态自举。所谓的自举,就是不需要借助别的资源(或工具),自己满足自己的需求。你说是先有蛋还是先有鸡,这个问题就需要自举解决。

list_add(&kmem_cache->list, &slab_caches);

In slab.c

create_boot_cache



In slub.c

bootstrap

创建Kmem_cache,内核只是构建了相关的结构,并没有提供实际的物理内存页面。也就是说采用的承诺制。告诉你有内存可用,实际先不给你,等你真的需要的时候,再想办法给你分配。内核里大量采用这种机制。啥都是告诉你,没问题,想要飞天遁地都没问题,等到有问题时再解决就好了。实在解决不了,大不了就重启得了,还能怎么着。内核就是一个超级销售。

我们来看这个超级销售如何进行Slab的创建和释放过程:

创建时,如上所述,并没有实际分配物理页面。这样,当实际需要分配对象的时候,就会存在没有对象内存可以使用的情况。具体过程是先去查看当前slab kmem cache保存的该cpu本地对象缓冲池。显然,这个缓冲池是空的,也就是说cpu自己的池子里没有对象内存(这里,所谓的当前核心本地的池子,只是指该核心的指针数据池子,具体的slab内存对应的物理页面,并不具有cpu核心相关性。只不过这类页面不是可以直接访问使用的,而是要通过slab系统。那么内核就会让别的cpu看不到不属于其池子里指针指向的对象所属的slab了。其实也不用限制到页面,即使是一小块内存,如果其牵引绳(指针)只有某个cpu可以拿到,那么这小块内存对别的cpu也是不可见的。这本质上从更细的粒度管理了内存。因为别的cpu拿不到牵引绳,自然访问不到内存块,那内存块自然也就到不了其核心专属的cache中)。

那么接着去公共池子看看,这里就是指的共享缓冲池。既然是共享的,虽然池子里也是一些牵引绳,但是这些牵引绳就需要各个cpu核心去争抢了。这里是要强调这个池子的访问需要加锁。当一个cpu核心拿走某个绳子后,其他cpu就不能再看到这个绳子了,除非这个绳子被重新释放回来。显然,此时,共享缓冲池也是空的。共享的池子挂在node上?

两个地方都扑空后,内核就要去看看内存node节点上的有关slab块的链表了。每一个slab块对应了一大块连续的物理页帧,这些页帧会被划分为小的obj对象,供上层实际使用。Node节点应该是先找自己近的吧,这样效率高一些。Node节点中包含三个连接,全、部分以及空,分别挂载了符合特性的slab块。

显然,这次有三个链表可以访问,但是因为之前并未实际分配内存,所以这些链表都是空的。再次扑空。

此时,内核已经走投无路了,唯一的选择就是老老实实跟伙伴系统要连续的物理页面。所需页面数量在创建这块slab kmem cache时已经算清楚了,现在就看伙伴系统了。

我们不考虑伙伴系统运行的情况,不考虑系统内存的其他情况。假设物理内存是有的。

伙伴系统看slab小弟跟自己要土地了,自然要照顾照顾。看自家土地还是足够的,就给slab小兄弟提供其所需的内存。注意,这里是连续的物理页面哗啦啦的交出去了。

Slab系统拿到内存后,自然是十分开心的。但是,这些内存是经历了三起三落(满心找本地一起一落,开心找共享池,二起二落,期待找slab分配器,三起三落)后,从伙伴系统那里收获的,要格外的珍惜。

slab分配器收获页面后,也就收获了对象池。先从中拿出一个对象,给请求者。然后,将slab挂到部分空slab链表上。

下次再次分配对象时,就不需要经历三起三落了,具体如何。。。。似乎第一次分配后,应该给本地cpu对象池子里放点,以备不时之需啊。

俗话说,有借有还,再借不难。Slab从伙伴兄弟哪里借了内存,不用了要还回去,否则,谁还愿意给它借。就这样,系统运转一段时间后,对象用用还还,世界早已不是它当初的样子了。我们可以想象情况首先应是这样的:

Cpu自己的对象池子里有一些空闲的对象了。内核为了提高缓冲的命中率,提高cache的使用率,优先将自己释放的对象放入自己的对象池子,这样下次再用时,省时(就近就有了,走路短)省力(也许cache中还存在呢,另一种就近)。

但是,随着时间的推移,自己的池子可能慢慢的就堆积了大量的空闲对象。如前所述,所谓的对象池子,其实是对象线头的池子。你捏了这么多线头不用,别人也用不了,那不就饿得饿死,撑得撑死了。所以内核下了一个死命令,如果线头多于n个,那就说明你独食吃太多了,需要让别人也雨露均沾点了。此时,内核会从池子里取出batch个对象,批量放到共享池子里。这样,其他cpu分配内存时,也就可以从共享池子里分配内存了。虽然比不上自己的本地池子,但是毕竟也比去slab和伙伴系统哪里少些处理。

继续,随着时间的推移,共享池子里不断迁入一把一把的对象线头,共享池子也受不了了。如果大量的对象线头留在这里,那么内核系统的其他兄弟拿不到内存土地的风险就会增加。为了解决这个问题,内核采用了类似的策略,当共享池子里的线头多到一定程度后,内核选择将共享池子里的线头放回到node中。其实,只需要将对象所在slab的相关状态和计数值调整就可以了。

这个过程进行一段时间后,可能出现很多slab里面的对象全是空闲的。也就是空闲slab链表长的很长。此时,就是将这些内存完整的交给伙伴系统的很好机会。

我们看到,每次的迁移和转移操作,都是积累到一定程度后进行的,这就很好的践行了七分饱的理念,不然,持有这些内存的边际效应就会变小,甚至产生反作用。

总的来看,分配是一个多级跳的过程,释放也是一个多级跳的过程。不激进,不怠慢,有节奏的进行。

现就说这么多吧。有时间再补充补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙赤子

你的小小鼓励助我翻山越岭

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值