Linux 所使用的 slab 分配器的基础是 Jeff Bonwick 为 SunOS 操做系统首次引入的一种算法。Jeff 的分配器是围绕对象缓存进行的。在内核中,会为有限的对象集(例如文件描述符和其余常见结构)分配大量内存。Jeff 发现对内核中普通对象进行初始化所需的时间超过了对其进行分配和释放所需的时间。所以他的结论是不该该将内存释放回一个全局的内存池,而是将内存保持为针对特定目而初始化的状态。git
咱们来看看rust中如何实现这一算法,也借此学习rust的应用技术。github
源码: carllerche/slab:Preallocate memory for values of a given type.算法
先来看一个基础数据结构Slot,这个数据结构对后面理解Slab相当重要。数组
enum Slot<T> { Empty(usize), Filled(T), Invalid, }
能够把Slot想象为一个抽屉,有三种状态:缓存
一个Slot能够为空,能够无效,能够装一个T类型对象且只能装一个。数据结构
接着来看Slab数据结构,这个数据结构表明着从内存中申请的一大块内存,这一大块内存中能够存储许多相同类型的对象,就像一个包含许多抽屉的大柜子。函数
/// A preallocated chunk of memory for storing objects of the same type. pub struct Slab<T, I = usize> { // Chunk of memory entries: Vec<Slot<T>>, // Number of Filled elements currently in the slab len: usize, // Offset of the next available slot in the slab. Set to the slab's // capacity when the slab is full. next: usize, _marker: PhantomData<I>, }
Slab就至关于一长排抽屉。包含四个成员:学习
Slab有两个类型参数:操作系统
经过这两个数据结构Slab内存分配模型就创建完毕了,Slab分配的核心思想是“用完不还”。一次性获取一块大的内存,之后不够了还能够再申请内存,可是申请了我就不还,用完了我也暂时不还,省得下次要用的时候还要申请(主要解决的问题是在频繁的使用过程当中,申请抽屉的时间大于使用抽屉的时间)。code
那使用Slab的方式就是,我预计一下今天我要装20个东西(T),那我就一次性申请一个包含20个抽屉的Slab(大柜子),来一个东西我用一个抽屉,用完了我柜子仍是先留着,等我不用了,再把柜子还回去。
按使用顺序,首先来看申请Slab的方法:
impl<T, I> Slab<T, I> { /// Returns an empty `Slab` with the requested capacity pub fn with_capacity(capacity: usize) -> Slab<T, I> { let entries = (1..capacity + 1) .map(Slot::Empty) .collect::<Vec<_>>(); Slab { entries: entries, next: 0, len: 0, _marker: PhantomData, } } ...... }
根据你要的大小,返回一个大柜子,其中的每一个抽屉都是空的,什么也没有装,利用Vec来申请内存。
这个方法自己没有什么难懂的,只是有一个语法细节须要特别注意:
let entries = (1..capacity + 1) .map(Slot::Empty) .collect::<Vec<_>>();
这一行在建立空抽屉,(1..capacity + 1)
表示建立一个Range,每一个元素为usize,从1到capacity+1。接下来经过map方法建立抽屉,不过这里传给map的参数看起来怪怪的,map的定义以下:
fn map<B, F>(self, f: F) -> Map<Self, F> where F: FnMut(Self::Item) -> B
传给map的参数应该是一个将usize映射为Slot的函数才对,而这里直接传入的是Slot::Empty,是一个枚举类型的变元!
再来看看Slot的定义:
enum Slot<T> { Empty(usize), Filled(T), Invalid, }
Empty是一个tupe struct variant。在Rust中申明这样的变元时,编译会自动为其生成一个构造函数
fn Slot::Empty(u: usize) -> Slot { Slot::Empty(u) }
因此这里将Slot::Empty直接传递给map是合法的。
整个建立函数的意思是申请一个包含指定数量的抽屉的大柜子,为每一个抽屉编一个号(1..n),且初始状态为空。
大柜子申请好了,如今来看看如何往其中放东西
impl<T, I: Into<usize> + From<usize>> Slab<T, I> { ...... /// Insert a value into the slab, returning the associated token pub fn insert(&mut self, val: T) -> Result<I, T> { match self.vacant_entry() { Some(entry) => Ok(entry.insert(val).index()), None => Err(val), } } ...... /// Returns a handle to a vacant entry. /// /// This allows optionally inserting a value that is constructed with the /// index. pub fn vacant_entry(&mut self) -> Option<VacantEntry<T, I>> { let idx = self.next; if idx >= self.entries.len() { return None; } Some(VacantEntry { slab: self, idx: idx, }) } ......
先来看这一句
I: Into<usize> + From<usize>
这一句要求索引类型I是能够与usize进行来回转换的。以前咱们看到每一个抽屉都有一个usize的编号,而I又是用来索引抽屉的,若是I能够与uszie进行映射,那么经过I找抽屉的任务就能够完成。
为何不直接用usize来作索引,还要那么麻烦的接受一个任意类型呢?若是直接使用usize作索引,那么一个给定的索引编号就直接对应到一个抽屉,索引和抽屉之间是一一映射的关系。若是使用I作索引,多个I能够映射到同一个usize就能够作到索引和抽屉之间的多对一关系,带来更多的灵活性,并且能表达更多信息,mio库的做者就使用业务含意丰富的Token索引替换了默认的简单的usize索引。
再来看插入函数
pub fn insert(&mut self, val: T) -> Result<I, T> { ... }
从函数申明看出,调用这个方法会修改Slab内部数据,传入要存储的对象val,若是存储成功,返回索引,不成功返回传入的val。这就比如我请你帮我把东西寄存在抽屉里,若是有合适的抽屉,你帮我把东西放好后告诉我放在第几个抽屉里了,若是没有找到合适的抽屉,你把东西还给我。
第一步就是找空箱子,经过vacant_entry方法完成
pub fn vacant_entry(&mut self) -> Option<VacantEntry<T, I>> { ... }
方法申明中看出,若是找到返回一个VacantEntry对象,没有返回None。
pub struct VacantEntry<'a, T: 'a, I: 'a> { slab: &'a mut Slab<T, I>, idx: usize, }
slab为大柜子的可变引用,idx是找到的抽屉的编号。 根据vacant_entry方法的实现能够知道找箱子的算法。下一个可用的抽屉编号保存在next中,只要不越界,就直接返回。
未完待续……