linux slub分配器浅析

在《linux内存管理浅析》中提到内核管理自己使用的内存时,使用了SLAB对象池。SLAB确实是比较复杂,所以一直以来都没有深入看一看。
不过现在,linux内核中,SLAB已经被它的简化版--SLUB所代替。最近抽时间看了一下SLUB的代码,略记一些自己的理解。
尽管SLUB是在内核里面实现的,用户态的对象池其实也可以借鉴这样的做法。

SLUB的总体思想还是跟SLAB类似,对象池里面的内存都是以“大块”为单位来进行分配与回收的。然后每个“大块”又按对象的大小被分割成“小块”,使用者对于对象的分配与回收都是以“小块”为单位来进行的。

SLUB的结构如下图:


另外,kmem_cache还有以下一些参数(后面会解释到):
int size;         /* 每个对象占用的空间 */
int objsize;      /* 对象的大小 */
int offset;       /* 对象所占用的空间中,存放next指针的偏移 */
int refcount;     /* 引用计数 */
int inuse;        /* 对象除next指针外所占用的大小 */
int align;        /* 对齐字节数 */
void (*ctor)(void *); /* 构造函数 */
unsigned long min_partial; /* kmem_cache_node中保存的最小page数 */
struct kmem_cache_order_objects oo; /* 首选的page分配策略(分配多少个连续页面,能划分成多少个对象) */
struct kmem_cache_order_objects min; /* 次选的page分配策略 */
(另外还有一些成员,或支持了一些选项、或支持了DEBUG、或是为周期性的内存回收服务。这里就不列举了。)

 

大体结构

kmem_cache是对象池管理器,每一个kmem_cache管理一种类型的对象。所有的管理器通过双向链表由表头slab_caches串连起来。
这一点跟之前的SLAB是一样的。

kmem_cache的内存管理工作是通过成员kmem_cache_node来完成的。在NUMA环境下(非均质存储结构),一个kmem_cache维护了一组kmem_cache_node,分别对应每一个内存节点。kmem_cache_node只负责管理对应内存节点下的内存。(如果不是 NUMA环境,那么kmem_cache_node只有一个。)

而实际的内存还是靠page结构来管理的,kmem_cache_node通过partial指针串连起一组page(nr_partial代表链表长度),它们就代表了对象池里面的内存。page结构不仅代表了内存,结构里面还有一些union变量用来记录其对应内存的对象分配情况(仅当page被加入到SLUB分配器后有效,其他情况下这些变量有另外的解释)。
原先的SLAB则要复杂一些,SLAB里面的page仅仅是管理内存,不维护“对象”的概念。而是由额外的SLAB控制结构(slab)来管理对象,并通过slab结构的一些指针数组来划定对象的边界。

前面说过,对象池里面的内存是以“大块”为单位来进行分配与回收的,page就是这样的大块。page内部被划分成若干个小块,每一块用于容纳一个对象。这些对象是以链表的形式来存储的,page->freelist就是链表头,只有未被分配的对象才会放在链表中。对象的next指针存放在其偏移量为kmem_cache->offset的位置。(见上面的图)
而在SLAB中,“大块”则是提供控制信息的slab结构。page结构只表示内存,它仅是slab所引用的资源。

每一个page并不只代表一个页面,而是2^order个连续的页面,这里的order值是由kmem_cache里面的oo或min来确定的。分配页面时,首先尝试使用oo里面的order值,分配较合适大小的连续页面(这个值是在kmem_cache创建的时候计算出来的,使用这个值时需要分配一定的连续页面,以使得内存分割成“小块”后剩余的边角废料较少)。如果分配不成功(运行时间长了,内存碎片多了,分配大量连续页面就不容易了),则使用min里面的order值,分配满足对象大小的最少量的连续页面(这个值也是创建kmem_cache时计算出来的)。

kmem_cache_node通过partial指针串连的一组page,这些page必须是没被占满的(一个page被划分成page->objects个对象大小的空间,这些空间中有page->inuse个已经被使用。如果page->objects==page->inuse,则page为full)。如果一个page为full,则它会被从链表中移除。而如果page是free的(page->inuse==0),一般情况下它也会被释放,除非这里的nr_partial(链表长度)小于kmem_cache里面的min_partial。(既然是池,就应该有一定的存量,min_partial就代表最低存量。这个值也是在创建kmem_cache时计算出来的,对象的size较大时,会得到较大的min_partial值。因为较大的size值在分配page时会需要更多连续页面,而分配连续页面不如单个的页面容易,所以应该多缓存一些。)
而原先的SLAB则有三个链表,分别维护“full”、“partial”、“free”的slab。“free”和“partial”在SLUB里面合而为一,成了前面的partial链表。而“full”的page就不维护了。其实也不需要维护,因为page已经full了,不能再满足对象的分配,只能响应对象的回收。而在对象回收时,通过对象的地址就能得到对应的page结构(page结构的地址是与内存地址相对应的,见《linux内存管理浅析》)。维护full的page可以便于查看分配器的状态,所以在DEBUG模式下,kmem_cache_node里面还是会提供一个full链表。

分配与释放

对象的分配与释放并不是直接在kmem_cache_node上面操作的,而是在kmem_cache_cpu上。一个kmem_cache维护了一组kmem_cache_cpu,分别对应系统中的每一个CPU。kmem_cache_cpu相当于为每一个CPU提供了一个分配缓存,以避免CPU总是去kmem_cache_node上面做操作,而产生竞争。并且kmem_cache_cpu能让被它缓存的对象固定在一个CPU上,从而提高CPU的cache命中率。kmem_cache_cpu只提供了一个page的缓存。

原先的SLAB是为每个CPU提供了一个array_cache结构来缓存对象。对象在array_cache结构中的组织形式跟它在slab中的组织形式是不一样的,这也就增加了复杂性。而SLUB则都是通过page结构来组织对象的,组织形式都一样。

进行对象分配的时候,首先尝试在kmem_cache_cpu上去分配。如果分配不成功,再去kmem_cache_node上move一个page到kmem_cache_cpu上面来。分配不成功的原因有两个:kmem_cache_cpu上的page已经full了、或者现在需要分配的node跟kmem_cache_cpu上缓存page对应的node不相同。对于page已full的情况,page被从kmem_cache_cpu上移除掉(或者DEBUG模式下,被移动到对应kmem_cache_node的full链表上);而如果是node不匹配的情况,则kmem_cache_cpu上缓存page会先被move回到其对应kmem_cache_node的partial链表上(再进一步,如果page是free的,且partial链表的长度已经不小于min_partial了,则page被释放)。
反过来,释放对象的时候,通过对象的地址能找到它所对应的page的地址,将对象放归该page即可。但是里面也有一些特殊逻辑,如果page正被kmem_cache_cpu缓存,就没有什么需要额外处理的了;否则,在将对象放归page时,需要对page加锁(因为其他CPU也可能正在该page上分配或释放对象)。另外,如果对象在回收之前该page是full的,则对象释放后该page就成partial的了,它还应该被添加到对应的kmem_cache_node的partial链表中。而如果对象回收之后该page成了free的,则它应该被释放掉。
对象的释放还有一个细节,既然对象会放回到对应的page上去,那如果这个page正在被其他的CPU cache呢(其他CPU的kmem_cache_cpu正指使用这个page)?其实没关系,kmem_cache_cpu和page各自有一个freelist指针,当page被一个CPU cache时,page的freelist上的所有对象全部移动到kmem_cache_cpu的freelist上面去(其实就是一个指针赋值),page的freelist变成NULL。而释放的时候是释放到page的freelist上去。两个freelist互不影响。但是这个地方貌似有个问题,如果一个被cache的page的freelist由于对象的释放而变成非NULL,那么这个page就可能再被cache到其他CPU的kmem_cache_cpu上面去,于是多个kmem_cache_cpu可能cache同一个page。这将导致一个CPU内部的缓存可能cache到其他CPU上的对象(因为CPU缓存跟对象并不是对齐的),从而一个CPU上的对象写操作可能引起另一个CPU的缓存失效。

在kmem_cache被创建的时候,SLUB会根据各种各样的信息,计算出对象池的合理布局(见上面的图)。objsize是对象本身的大小;这个大小经过对齐处理以后就成了inuse;紧贴inuse的后面可能会存放对象的next指针(由offset来标记),从而将对象实际占用的大小扩大到size。
其实,这里的offset并不总是指向inuse后面的位置(否则offset就可以用inuse来代替了)。offset有两种可能的取值,一是 inuse、一是0。这里的offset指定了next指针的位置,而next是为了将对象串连在空闲链表中。那么,需要用到next指针的时候,对象必定是空闲的,对象里面的空间是未被使用的。于是正好,对象里的第一个字长的空间就拿来当next指针好了,此时offset就是0。但是在一些特殊情况下,对象里面的空间不能被复用作next指针,比如对象提供了构造函数ctor,那么对象的空间是被构造过的。此时,offset就等于inuse,next指针只好存放在对象的空间之后。

关于kmem_cache_cpu

前面在讲对象分配与释放的时候,着重讲的是过程。下面再细细分析一下kmem_cache_cpu的作用。

如果没有kmem_cache_cpu,那么分配对象的过程就应该是:
1、从对应的kmem_cache_node的partial链表里面选择一个page;
2、从选中的page的freelist链表里面选择一个对象;
这就是最基本的分配过程。

但是这个过程有个不好的地方,kmem_cache_node的partial链表是全局的、page的freelist链表也是全局的:
1、第一步访问partial链表的时候,需要上锁;
2、第二步访问page->freelist链表的时候,也需要上锁;
3、对象可能刚在CPU1上被释放,又马上被CPU2分配走。不利于CPU cache;

引入kmem_cache_cpu就是对这一问题的优化。每个CPU各自对应一个kmem_cache_cpu实例,用于缓存第一步中选中的那个page。这样一来,第一步就不需要上锁了;而page中的对象在一段时间内也将趋于在同一个CPU上使用,有利于CPU cache。
而kmem_cache_cpu中的freelist则是为了避免第二步的上锁。

假设没有kmem_cache_cpu->freelist,而page->freelist初始时有1、2、3、4,四个对象。考虑如下事件序列:
1、page被CPU1所cache,然后1、2被分配;
2、由于在CPU1上请求的node id与该page不匹配,page被放回kmem_cache_node的partial链表,那么此时page->freelist还剩3和4两个对象;
3、page又被CPU2所cache(page在上一步已经被放回partial链表了)。
此时,page->freelist就有可能被CPU1和CPU2两个CPU所访问,当对象1或2被释放时(这两个对象已经分配给了CPU1),CPU1会访问page->freelist;而显然CPU2分配对象时也要会访问page->freelist。

所以为了避免上锁,kmem_cache_cpu要维护自己的freelist,把page->freelist下的对象都接管过来。
这样一来,CPU1就只跟page->freelist打交道,CPU2跟kmem_cache_cpu的freelist打交道,就不需要上锁了。

 

关于page同时被多CPU使用

前面还提到,从属于同一个page的对象可能cache到不同CPU上,从而可能对CPU的缓存造成一定的影响。不过这似乎也是没办法的事情。

首先,初始状态下,page加入到slub之后,从属于该page的对象都是空闲的,都存在于page->freelist中;
然后,这个page可能被某个CPU的kmem_cache_cpu所cache,假设是CPU-0,那么这个kmem_cache_cpu将得到属于该page的所有对象。 page->freelist将为空;
接下来,这个page下的一部分对象可能在CPU-0上被分配出去;
再接着,可能由于NUMA的node不匹配,这个page从CPU-0的kmem_cache_cpu上面脱离下来。这时page->freelist将保存着那些未被分配出去的对象(而其他的对象已经在CPU-0上被分配出去了);

这时,从属于该page的一部分对象正在CPU-0上被使用着,另一部分对象存在于page->freelist中。
那么,现在就有两个选择:
1、不将这个page放回partial list,阻止其他CPU使用这个page;
2、将这个page放回partial list,允许其他CPU使用这个page;

对于第一种做法,可以避免属于同一个page的对象被cache到不同CPU。但是这个page必须等到CPU-0再次cache它以后才能被继续使用;或者等待CPU-0所使用的从属于这个page的对象全都被释放,然后这个page才能被放回partial list或者直接被释放掉。
这样一来,一个page尽管拥有空闲的对象,却可能在一定时间内处于不可用状态(极端情况是永远不可用)。这样实现的系统似乎不太可控……

而现在的slub选择了第二种做法,将page放回partial list,于是page马上就能被其他CPU使用起来。那么,由此引发的从属于同一个page的对象被cache到不同CPU的问题,也就是没办法的事情……

 

vs slab

相比SLAB,SLUB还有一个比较有意思的特性。当创建新的对象池时,如果发现原先已经创建的某个kmem_cache的size刚好等于或略大于新的size,则新的kmem_cache不会被创建,而是复用这个大小差不多kmem_cache。所以kmem_cache里面还维护了一个refcount(引用计数),表示它被复用的次数。

另外,SLUB也去掉了SLAB中很有意思的一个特性,Coloring(着色)。
什么是着色呢?一个内存“大块”,在按对象大小划分成“小块”的时候,可能并不是那么刚好,还会空余一些边边角角。着色就是利用这些边边角角来做文章,使得“小块”的起始地址并不总是等于“大块”内的0地址,而是在0地址与空余大小之间浮动。这样就使得同一种类型的各个对象,其地址的低几位存在更多的变化。
为什么要这样做呢?这是考虑到了CPU的cache。在学习操作系统原理的时候我们都听说过,为提高CPU对内存的访存效率,CPU提供了cache。于是就有了从内存到cache之间的映射。当CPU指令要求访问一个内存地址的时候,CPU会先看看这个地址是否已经被缓存了。
内存到cache的映射是怎么实现的呢?或者说CPU怎么知道某个内存地址有没有被缓存呢?
一种极端的设计是“全相连映射”,任何内存地址都可以映射到任何的cache位置上。那么CPU拿到一个地址时,它可能被缓存的cache位置就太多了,需要维护一个庞大的映射关系表,并且花费大量的查询时间,才能确定一个地址是否被缓存。这是不太可取的。
于是,cache的映射总是会有这样的限制,一个内存地址只可以被映射到某些个cache位置上。而一般情况下,内存地址的低几位又决定了内存被cache的位置(如:cache_location = address % cache_size)。
好了,回到SLAB的着色,着色可以使同一类型的对象其低几位地址相同的概率减小,从而使得这些对象在cache中映射冲突的概率降低。
这有什么用呢?其实同一种类型的很多对象被放在一起使用的情况是很多的,比如数组、链表、vector、等等情况。当我们在遍历这些对象集合的时候,如果每一个对象都能被CPU缓存住,那么这段遍历代码的处理效率势必会得到提升。这就是着色的意义所在。
SLUB把着色给去掉了,是因为对内存使用更加抠门了,尽可能的把边边角角减少到最小,也就干脆不要着色了。还有就是,既然kmem_cache可以被size差不多的多种对象所复用,复用得越多,着色也就越没意义了。

转载于:https://www.cnblogs.com/wangfengju/archive/2013/05/11/6173088.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/295525.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

openfire 插件开发例子

2019独角兽企业重金招聘Python工程师标准>>> 好久都没有写东西了。今天总结一下之前开发的一些openfire插件。 这次的插件需要提供一个HTTP的接口。通过HTTP来对openfire做一些操作。 插件的目录结构:项目名称“exampleplugin" src/main/javaorg/ji…

WPF实现一个彩虹按钮

WPF开发者QQ群: 340500857 | 微信群 -> 进入公众号主页 加入组织玩玩彩虹文字,这次用 LinearGradientBrush 并且制作成按钮,虽然没技术含量反而有些实用,这就是返璞归真吗。首先来回忆下 LinearGradientBrush 的用法。LinearG…

设计模式的分类和六大设计原则

学习设计模式我是大学研究《java与模式这本书》1024页,很多没有看懂,并且没有总结起来,这次一定要把设计原则和设计模式总结清楚。 设计模式的分类 设计模式分为三大类:创建型模式,共五种:工厂方法模式、…

Java IO的RandomAccessFile的使用(转)

现有如下的一个需求,向已存在1G数据的txt文本里末尾追加一行文字,内容如下“Lucene是一款非常优秀的全文检索库”。可能大多数朋友会觉得这个需求很easy,说实话,确实easy,然后XXX君开始实现了,直接使用Java…

nvidia控制面板点了没反应win7_为什么没有nvidia控制面板_win7没有nvidia控制面板怎么找回-系统城...

2016-10-31 16:15:46  浏览量:30668如果电脑显卡出现问题会导致屏幕画面不清楚,这时候win7系统自带nvidia控制面板就派上用场了。它能够对显卡进行设置,提升显卡功能,但有用户说win7怎么没有nvidia控制面板?找很久也…

公交车座椅上有个洞,竟是为了…很多人都不知道

全世界只有3.14 % 的人关注了爆炸吧知识坐公交车的时候你有没有发现公交车的座椅上通常来说中间都会有个洞洞的大小基本上刚好够一个手指头穿过那么这个洞到底有什么用呢?小编特意问了一圈同事们的回答真的脑洞大开有的说洞口刚好可以穿过手指是不是乘客无聊的时候可…

C# 如何判断某个 tcp 端口是否被占用?

咨询区 Ali:在 C# 中使用 TcpClient 或者其他通用的方式建立的 Socket,请问我如何判断这个端口是否被占用?比如下面的代码:TcpClient c; //I want to check here if port is free. c new TcpClient(ip, port);回答区 jro&#xf…

C#正则表达式编程(四)转致周公

正则表达式提供了功能强大、灵活而又高效的方法来处理文本。正则表达式的全面模式匹配表示法使您可以快速分析大量文本以找到特定的字符模式;提取、编辑、替换或删除文本子字符串;或将提取的字符串添加到集合以生成报告。对于处理字符串(例如…

Quartz 的SB问题 GetNextValidTimeAfter 输出和输出 时区 不同步,好傻的方法?

测试代码如下DateTime kk new DateTime(2012, 6, 4, 15, 0, 0);Quartz.CronExpression cron new Quartz.CronExpression("0 14 15 ? * *");var dt cron.GetNextValidTimeAfter(kk);好傻好伤。dt的时候是 {2012/6/5 7:14:00} 跑出了一个7点来了。正确的期待值应该…

Android之switch控件的用法

在做一个蓝牙开关时候,用到了switch,记一下用法,其实跟Button是几乎一样的. 布局中: <Switch android:id="@+id/open" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textOff="蓝牙关闭中&q…

pythonresponse对象的属性_Scrapy中response属性以及内容提取

PythonPython开发Python语言Scrapy中response属性以及内容提取一.属性url &#xff1a;HTTP响应的url地址,str类型status&#xff1a;HTTP响应的状态码, int类型headers &#xff1a;HTTP响应的头部, 类字典类型, 可以调用get或者getlist方法对其进行访问body&#xff1a;HTTP响…

【转】学习apicloud和IOS之间的模块化使用

最近公司有使用APICloud发开的需求&#xff0c;需要我这边提供一些模块包得封装。因为没有也是刚接触APICloud&#xff0c;所以也就在看官方文档 。下面讲一讲我再使用过程中得一点点东西。 首先&#xff0c;下载官方SDK,下载最新版本的模块开发SDK&#xff0c;找到里面的Modul…

豪横!学术圈“造假之王”,200余篇论文有183篇论文被撤稿

全世界只有3.14 % 的人关注了爆炸吧知识导读&#xff1a;学术造假已经不再是什么新鲜话题&#xff0c;历史上的学术骗子也是数不胜数。其中骗子之王更是达到前无古人的地步&#xff0c;发表论文数212篇&#xff0c;因涉嫌造假而遭到撤稿的就达到了183篇之多。然而打假的过程并不…

在CISCO ASA 防火墙上配置Web ×××

目前市场上产品很多&#xff0c;而且技术各异&#xff0c;就比如传统的 IPSec 来讲&#xff0c; SSL 能让公司实现更多远程用户在不同地点接入&#xff0c;实现更多网络资源访问&#xff0c;且对客户端设备要求低&#xff0c;因而降低了配置和运行支撑成本。很多企业用户采纳 …

学Dapr Actors 看这篇就够了

介绍Actor模式将Actor描述为最低级别的“计算单元”。换句话说&#xff0c;您在一个独立的单元&#xff08;称为actor&#xff09;中编写代码&#xff0c;该单元接收消息并一次处理一个消息&#xff0c;没有任何并发或线程。再换句话说&#xff0c;根据ActorId划分独立计算单元…

博客园2013年5月份第1周源码发布详情

媒体互动学习社区(课程设计)源码 2013-5-10 [VS2010]源码描述&#xff1a;该源码使用VS210SQL08开发&#xff0c;主要分为前台和后台&#xff0c;所有提交使用JQFORM提交&#xff0c;实现无刷新提高用户的体验&#xff0c;前台功能有学科的介绍&#xff0c;课件跟视频下载&…

git之Pushing to the remote branch is not fast-forward错误解决

今天推送代码的时候报错了这个Pushing to the remote branch is not fast-forward,so the push has to be forced.The commits in the remote branch will be lost 错误&#xff0c;然后就出现这个效果&#xff0c;下面是图片。 问题&#xff08;Non-fast-forward&#xff09;的…

tp mysql索引_mysql索引

1 查看表中已存在哪些索引&#xff1a;show index from 表名&#xff1b;在添加索引之前最好先查看一下该表中已存在哪些索引&#xff1a;show index from 表名&#xff1b;1、主键索引注意&#xff1a; 主键索引一张表中只能有一个&#xff0c;但是可以添加多个索引 比如&…

CSS Id 和 Class

2019独角兽企业重金招聘Python工程师标准>>> id 和 class 选择器 如果你要在HTML元素中设置CSS样式&#xff0c;你需要在元素中设置"id" 和 "class"选择器。 id 选择器 id 选择器可以为标有特定 id 的 HTML 元素指定特定的样式。 HTML元素以id属…

这4部有生之年必看的“教材级”纪录片,免费领取!

全世界只有3.14 % 的人关注了爆炸吧知识纪录片是以真实生活为创作素材&#xff0c;以真人真事为表现对象&#xff0c;并对其进行艺术的加工与展现的&#xff0c;以展现真实为本质&#xff0c;并用真实引发人们思考的电影或电视艺术形式。好的纪录片就像打开了一扇新世界的大门&…