Rust語言實現的Slab內存管理器源碼分析

Linux 所使用的 slab 分配器的基礎是 Jeff Bonwick 爲 SunOS 操做系統首次引入的一種算法。Jeff 的分配器是圍繞對象緩存進行的。在內核中,會爲有限的對象集(例如文件描述符和其餘常見結構)分配大量內存。Jeff 發現對內核中普通對象進行初始化所需的時間超過了對其進行分配和釋放所需的時間。所以他的結論是不該該將內存釋放回一個全局的內存池,而是將內存保持爲針對特定目而初始化的狀態。git

咱們來看看rust中如何實現這一算法,也藉此學習rust的應用技術。github

源碼: carllerche/slab:Preallocate memory for values of a given type.算法

Slot結構(抽屜)

先來看一個基礎數據結構Slot,這個數據結構對後面理解Slab相當重要。數組

enum Slot<T> {
    Empty(usize),
    Filled(T),
    Invalid,
}

能夠把Slot想象爲一個抽屜,有三種狀態:緩存

  • Empty 空抽屜,什麼都沒有裝。Empty(usize) 一個帶有編號的空抽屜。
  • Filled 裝滿的抽屜,Filled(T) 一個裝了一個T類型對象的抽屜。
  • Invalid 無效的抽屜,這個抽屜當前處於無效狀態。

一個Slot能夠爲空,能夠無效,能夠裝一個T類型對象且只能裝一個。數據結構

Slab結構(大櫃子)

接着來看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就至關於一長排抽屜。包含四個成員:學習

  • entries: 是一個列表,每一個元素爲Slot類型,就至關因而一長排抽屜。
  • len:這一排抽屜的個數。
  • next:從註釋上看,這是下一個可用的抽屜。但什麼狀況才叫可用,咱們如今不知道,先留着,後面瞭解。
  • _marker:這一項能夠先忽略,它是用來表達一種全部關係的,目前不重要,後面再來詳說。

Slab有兩個類型參數:操作系統

  • T:表示預申請的內存用來存儲什麼類型的對象,即抽屜裏能夠裝什麼類型的對象。
  • I: 表示用來索引對象的類型。咱們能夠把Slab看是一個T的數組,能夠經過 slab[2] 相似的語法來訪問某個對象,這裏的I表示2的類型。I = usize這樣的寫法表示,默認是usize作索引類型,但也能夠爲其它類型。也就是說使用者能夠根據某種索引找到對應的抽屜。

經過這兩個數據結構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中,只要不越界,就直接返回。

未完待續……

相關文章
相關標籤/搜索