Operating System Memory Management、Page Fault Exception、Cache Replacement Strategy Learning、LRU Algo

目錄html

0. 引言
1. 頁表
2. 結構化內存管理
3. 物理內存的管理
4. SLAB分配器
5. 處理器高速緩存和TLB控制
6. 內存管理的概念
7. 內存覆蓋與內存交換
8. 內存連續分配管理方式
9. 內存非連續分配管理方式
10. 虛擬內存的概念、特徵及其實現
11. 請求分頁管理方式實現虛擬內存
12. 頁面置換算法
13. 頁面分配策略
14. 頁面抖動和工做集
15. 缺頁異常的處理
16. 堆與內存管理

 

0. 引言前端

有兩種類型的計算機,分別以不一樣的方法管理物理內存node

1. UMA計算機(一致內存訪問 uniform memory access)
將可用內存以連續方式組織起來,SMP系統中的每一個處理器訪問各個內存區都是一樣快

2. NUMA計算機(非一致內存訪問 non-uniform memory access)
多處理器計算機,系統的各個CPU都有本地內存,可支持高速訪問,各個處理器之間經過總線鏈接起來,以支持對其餘CPU的本地內存的訪問,可是跨CPU內存訪問比本地CPU內存訪問要慢
    1) 基於Alpha的WildFire服務器
    2) IMB的NUMA-Q計算機

0x1: (N)UMA模型中的內存組織linux

Linux支持的各類不一樣體系結構在內存管理方面差異很大,因爲Linux內核良好的封裝、以及其中的兼容層,這些差異被很好的隱藏起來了(下層的代碼對上層是透明的),兩個主要的問題是ios

1. 頁表中不一樣數目的間接層(向上透明的多級頁表)
2. NUMA和UMA系統的劃分

內核對一致(UMA)和非一致(NUMA)內存訪問系統使用相同的數據結構,所以針對各類不一樣形式的內存佈局,各個算法幾乎沒有差異。在UMA系統上,只使用一個NUMA節點來管理整個系統內存,而內存管理的其餘部分則認爲它們是在處理一個只有單節點的NUMA系統(這也是Linux內核中常見的兼容思想)程序員

上圖代表了內存劃分的大體狀況web

1. 首先,內存劃分爲"結點",每一個結點關聯到系統中的一個處理器,在內核中表示爲pg_data_t的實例,各個內存節點保存在一個單鏈表中,供內核遍歷
2. 各個結點又劃分爲"內存域",是內存的進一步劃分,各個內存域都關聯了一個數組,用來組織屬於該內存域的物理內存頁(頁幀),對每一個頁幀,都分配一個struct page實例以及所需的管理數據
    1) CONFIG_ZONE_DMA

    2) ZONE_DMA: 
    ZONE_DMA is used when there are devices that are not able to do DMA to all of addressable memory (ZONE_NORMAL). Then we carve out the portion of memory that is needed for these devices. The range is arch specific.
        2.1) parisc、ia6四、sparc: <4G
        2.2) s390: <2G
        2.3) arm: Various
        2.4) alpha: Unlimited or 0-16MB.
        2.5) i38六、x86_6四、multiple other arches: <16M.
    ZONE_DMA標記適合DMA的內存域,該區域的長度依賴於處理器類型,在IA-32計算機上,通常的限制是16MB,這是古老的ISA設備強加的邊界,但更現代的計算機也可能受這個限制的影響

    3) CONFIG_ZONE_DMA32

    4) ZONE_DMA32: 
    x86_64 needs two ZONE_DMAs because it supports devices that are only able to do DMA to the lower 16M but also 32 bit devices that can only do DMA areas below 4G.
    ZONE_DMA32標記了使用32位地址字可尋址、適合DMA的內存域。顯然,只有在64位系統上兩種DMA內存域上纔有差異,在32位計算機上,這個內存域是空的,即長度爲0MB,在Alpha和AMD64系統上,該內存域的長度可能從0到4GB

    5) ZONE_NORMAL
    Normal addressable memory is in ZONE_NORMAL. DMA operations can be performed on pages in ZONE_NORMAL if the DMA devices support transfers to all addressable memory.
    ZONE_NORMAL標記了可直接映射到內核段的普通內存域,這是在全部體系結構上保證都會存在的惟一內存域,但沒法保證該地址範圍對應了實際的物理內存,例如
        1. 若是AMD64系統有2GB內存,那麼全部內存都屬於ZONE_DMA32,而ZONE_NORMAL則爲空

    6) CONFIG_HIGHMEM

    7) ZONE_HIGHMEM
    A memory area that is only addressable by the kernel through mapping portions into its own address space. This is for example used by i386 to allow the kernel to address the memory beyond 900MB.  The kernel will set up special mappings (page table entries on i386) for each page that the kernel needs to access.
    ZONE_HIGHMEM標記了超出內核段的物理內存

    8) ZONE_MOVABLE
    9) __MAX_NR_ZONES
    1) 對可用於(ISA設備的)DMA操做的內存區是有限制的,只有前16MB可用
    2) 通用的"普通"內存區
    3) 高端內存區域沒法直接映射

內核引入了下列常量來枚舉系統中的全部內存域
\linux-2.6.32.63\include\linux\mmzone.h算法

enum zone_type 
{
#ifdef CONFIG_ZONE_DMA
    /*
     * ZONE_DMA is used when there are devices that are not able to do DMA to all of addressable memory (ZONE_NORMAL). Then we carve out the portion of memory that is needed for these devices.
     * The range is arch specific.
     *
     * Some examples
     *
     * Architecture        Limit
     * ---------------------------
     * parisc, ia64, sparc    <4G
     * s390            <2G
     * arm            Various
     * alpha        Unlimited or 0-16MB.
     *
     * i386, x86_64 and multiple other arches <16M.

     ZONE_DMA標記適合DMA的內存域,該區域的長度依賴於處理器類型,在IA-32計算機上,通常的限制是16MB,這是古老的ISA設備強加的邊界,但更現代的計算機也可能受這個限制的影響
     */
    ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
    /*
     * x86_64 needs two ZONE_DMAs because it supports devices that are only able to do DMA to the lower 16M but also 32 bit devices that can only do DMA areas below 4G.

     ZONE_DMA32標記了使用32位地址字可尋址、適合DMA的內存域。顯然,只有在64位系統上兩種DMA內存域上纔有差異,在32位計算機上,這個內存域是空的,即長度爲0MB,在Alpha和AMD64系統上,該內存域的長度可能從0到4GB
     */
    ZONE_DMA32,
#endif
    /*
     * Normal addressable memory is in ZONE_NORMAL. DMA operations can be performed on pages in ZONE_NORMAL if the DMA devices support transfers to all addressable memory.

     ZONE_NORMAL標記了可直接映射到內核段的普通內存域,這是在全部體系結構上保證都會存在的惟一內存域,但沒法保證該地址範圍對應了實際的物理內存,例如
     1. 若是AMD64系統有2GB內存,那麼全部內存都屬於ZONE_DMA32,而ZONE_NORMAL則爲空
     */
    ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
    /*
     * A memory area that is only addressable by the kernel through mapping portions into its own address space. 
     * This is for example used by i386 to allow the kernel to address the memory beyond 900MB. 
     * The kernel will set up special mappings (page table entries on i386) for each page that the kernel needs to access.

     ZONE_HIGHMEM標記了超出內核段的物理內存
     */
    ZONE_HIGHMEM,
#endif
    //內核定義了一個僞內存域ZONE_MOVABLE,在防止物理內存碎片的機制中須要使用該內存域
    ZONE_MOVABLE,
    //__MAX_NR_ZONES充當結束標記,在內核想要迭代系統中的全部內存區域時,會用到該常量
    __MAX_NR_ZONES
};

根據編譯時的配置,可能無須考慮某些內存域,例如數據庫

1. 在64位系統中,並不須要高端內存域
2. 若是支持了只能訪問4GB如下內存的32位外設外,才須要DMA32內存域

處於性能考慮,在爲進程分配內存時,內核老是試圖在當前運行的CPU相關聯的NUMA結點上進行(UMA只有一個結點)。但這並不老是可行的,例如,該結點的內存可能已經用盡,對這個狀況,每一個節點都提供了一個備用列表(藉助struct node_zonelists),該列表包含了其餘結點(和相關的內存域),可用於代替當前結點分配內存,列表項的位置越靠後,就越不適合分配編程

0x2: 數據結構

1. 結點管理

pg_date_t用於表示結點的基本元素

http://www.cnblogs.com/LittleHann/p/3865490.html
//搜索:0x3: struct pg_data_t

2. 結點狀態管理

若是系統中結點多於一個(NUMA),內核會維護一個位圖,用於提供各個結點的狀態信息,狀態是用位掩碼指定的,可以使用下列值
\linux-2.6.32.63\include\linux\nodemask.h

/*
 * Bitmasks that are kept for all the nodes.
 */
enum node_states 
{
    /* 
    The node could become online at some point 
    結點在某個時刻可能變爲聯機
    */
    N_POSSIBLE,        

    /* 
    The node is online 
    結點是聯機的
    */
    N_ONLINE,        

    /* 
    The node has regular memory 
    結點有普通內存域
    */
    N_NORMAL_MEMORY,    
#ifdef CONFIG_HIGHMEM
    /* 
    The node has regular or high memory 
    結點有普通、或高端內存域

    若是結點有普通或高端內存則使用N_HIGH_MEMORY,不然使用N_NORMAL_MEMORY
    */
    N_HIGH_MEMORY,        
#else
    N_HIGH_MEMORY = N_NORMAL_MEMORY,
#endif
    /* 
    The node has one or more cpus 
    結點有一個、或多個CPU
    */
    N_CPU,        
    NR_NODE_STATES
};

狀態N_POSSIBLE、N_ONLINE、N_CPU用於CPU和內存的熱插拔。對內存管理有必要的標誌是N_HIGH_MEMORY、N_NORMAL_MEMORY

兩個輔助函數用來設置或清除位域或特定結點中的一個比特位
\linux-2.6.32.63\include\linux\nodemask.h

static inline void node_set_state(int node, enum node_states state)
{
    __node_set(node, &node_states[state]);
}
static inline void node_clear_state(int node, enum node_states state)
{
    __node_clear(node, &node_states[state]);
}

//宏for_each_node_state用來迭代處於特定狀態的全部結
#define for_each_node_state(__node, __state) \
    for_each_node_mask((__node), node_states[__state])

若是內核編譯爲只支持單個結點(平坦內存模型),則沒有結點位圖,上述操做該位圖的函數則變爲空操做

3. 內存域

內存劃分爲"結點",每一個結點關聯到系統中的一個處理器,各個結點又劃分爲"內存域",是內存的進一步劃分

http://www.cnblogs.com/LittleHann/p/3865490.html
//搜索:0x4: struct zone

4. 冷熱頁

struct zone的pageset成員用於實現冷熱頁分配器(hot-n-cold allocator),在多處理器系統上每一個CPU都有一個或多個高速緩存,各個CPU的管理必須是獨立的

儘管內存域可能屬於一個特定的NUMA結點,於是關聯到某個特定的CPU,但其餘CPU的高速緩存仍然能夠包含該內存域中的頁。實際上,每一個處理器均可以訪問系統中全部的頁,儘管速度不一樣。所以,特定於內存域的數據結構不只要考慮到所屬NUMA結點相關的CPU,還必須考慮到系統中其餘的CPU

pageset是一個數組,其容量與系統可以容納的CPU數目的最大值相同,並非系統中實際存在的CPU數目

struct zone
{
    ..
    struct per_cpu_pageset    pageset[NR_CPUS];
    ..
}
//NR_CPUS是一個能夠在編譯時配置的宏常數,在單處理器系統上其值老是1,針對SMP系統編譯的內核中,其值可能在2~32/64(在64位系統上是64)之間

struct per_cpu_pageset

struct per_cpu_pageset 
{
    /*
    pcp[0]: 熱頁
    pcp[1]: 冷夜
    */
    struct per_cpu_pages pcp;
#ifdef CONFIG_NUMA
    s8 expire;
#endif
#ifdef CONFIG_SMP
    s8 stat_threshold;
    s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS];
#endif
} ____cacheline_aligned_in_smp;


struct per_cpu_pages 
{
    /* 
    number of pages in the list 
    列表中頁數,count記錄與該列表相關的頁的數目
    */
    int count;        

    /* 
    high watermark, emptying needed 
    頁數上限水印,在須要的狀況下清空列表,若是count的值超過了high,則代表列表中的頁太多了,對容量太低的狀態沒有顯式使用水印,若是列表中沒有成員,則從新填充
    */
    int high;    

    /* 
    chunk size for buddy add/remove 
    若是可能,CPU的高速緩存不是用單個頁來填充的,而是用多個頁組成的塊,batch添加/刪除多頁塊的時候,塊的大小(即頁數)的參考值
    */    
    int batch;        

    /* 
    Lists of pages, one per migrate type stored on the pcp-lists 頁的鏈表
    lists是一個雙鏈表,保存了當前CPU的冷頁或熱頁,可以使用內核的標準方法處理
    */
    struct list_head lists[MIGRATE_PCPTYPES];
};

下圖說明了在雙處理器系統上per-CPU緩存的數據結構是如何填充的

5. 頁幀

頁幀表明系統內存的最小單位,對內存中的每一個頁都會建立struct page的一個實例,內核須要保證該結構儘量小,不然可能會出現"內存描述元信息佔用了大量內存"的狀況,在典型的系統中,因爲頁的數目巨大,所以對struct page結構的小的改動,也可能致使保存全部page實例所需的物理內存暴漲

http://www.cnblogs.com/LittleHann/p/3865490.html
//搜索:0x5: struct page

在安全攻防產品的研發中,咱們會大量用到cache的機制。在學習和使用cache緩存的時候,常常會遇到cache的更新和替換的問題,如何有效對cache進行清理、替換,同時要保證cache在清理後還要保持較高的命中率。經過對比咱們發現,操做系統的內存管理調度策略和cache的動態更新策略本質是相似的,經過學習操做系統的內存管理策略,咱們能夠獲得不少關於cache更新的策略思想

Relevant Link:

https://www.google.com.hk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=27&ved=0CDwQFjAGOBQ&url=%68%74%74%70%3a%2f%2f%6f%61%2e%70%61%70%65%72%2e%65%64%75%2e%63%6e%2f%66%69%6c%65%2e%6a%73%70%3f%75%72%6c%74%69%74%6c%65%3d%25%45%36%25%39%36%25%38%37%25%45%34%25%42%42%25%42%36%43%61%63%68%65%25%45%38%25%38%37%25%41%41%25%45%39%25%38%30%25%38%32%25%45%35%25%42%41%25%39%34%25%45%37%25%41%44%25%39%36%25%45%37%25%39%35%25%41%35%25%45%37%25%41%30%25%39%34%25%45%37%25%41%39%25%42%36&ei=veo1VN_RJZfj8AWBnoCACQ&usg=AFQjCNHVjRFlRvV-0O1tYyb4Inv33Pop4A&bvm=bv.76943099,d.dGc&cad=rjt
http://www.cnblogs.com/hanyan225/archive/2011/07/28/2119628.html

 

1. 頁表

頁表尋址和傳統的(DOS時代)的線性尋址的好處在於

1. 頁表用於創建用戶進程的虛擬地址空間和系統物理內存(頁幀)之間的關聯
2. 頁表用於向每一個進程提供一致的虛擬地址空間,應用程序看到的地址空間是一個連續的內存區
3. 頁表也將虛擬內存頁映射到物理內存,於是支持共享內存的實現(同一個物理頁同時映射到不一樣進程的虛擬地址空間)
4. 層次化的頁表用於支持對大地址空間的快速、高效的管理
5. 能夠在不額外增長物理內存的狀況下,將頁換出到塊設備來增長有效的可用內存空間,即將進程中某些不經常使用的虛擬內存進行"解關聯",將對應的頁表映射刪除,從而釋放出這部分物理內存,讓其餘進程能夠用於映射

內核內存管理老是"假定"使用四級頁表,而無論底層處理器是否如此,在IA-32系統中,該體系結構只使用兩級分頁系統(在不使用PAE擴展的狀況下),所以,第3、第四級頁表必須由特定於體系結構的代碼模擬,頁表管理分爲兩個部分

1. 第一部分依賴於體系結構: 全部數據結構和操做數據結構的函數都是定義在特定於體系結構的文件中
2. 第二部分是體系結構無關的
//須要注意的一點是,在Linux內核中,內存管理和體系結構的關聯很密切

0x1: 數據結構

1. 內存地址的分解

根據四級頁表結構的須要,虛擬內存地址分爲5個部分(4個表項用於選擇頁、1個索引表示頁內位置)。各個體系結構不只地址字長度不一樣,並且地址字拆分的方式也不一樣,所以內核定義了宏,用於將地址分解爲各個份量

BITS_PER_LONG定義用於unsigned long變量的比特位數目,所以也適用於用於指向虛擬地址空間的通用指針,須要明白的是,在不一樣的體系結構下,這個"BITS_PER_LONG"長度是不一樣的,以及用於各級頁表的地址分隔長度也是不一樣的

1. PGD
2. PUD: PGDIR_SHIFT由PUD_SHIFT加上上層頁表索引所需的比特位長度,對全局頁目錄中的一項所能尋址的的部分地址空間長度: 2(PGDIR_SHIFT)次方
3. PMD: PUD_SHIFT由PMD_SHIFT加上中間層頁表索引所需的比特位長度
4. PTE: PMD_SHIFT指定了業內偏移量和最後一項頁表項所需比特位的總數,該值減去PAGE_SHIFT,可得最後一項頁表項索引所需比特位的數目。同時PMD_SHIFT代表了一箇中間層頁表項管理的部分地址空間的大小: 2(PMD_SHIFT)次方字節
5. Offset: 每一個指針末端的幾個比特位,用於指定所選頁幀內部的位置,比特位的具體數目由PAGE_SHIFT指定(經過位移+掩碼的形式來進行分段)

在各級頁目錄/頁表中所能存儲的指針數目,也能夠經過宏定義肯定

1. PTRS_PER_PGD: 指定了全局頁目錄中項的數目
2. PTRS_PER_PUD: 對應於上層頁目錄中項的數目
3. PTRS_PER_PMD: 對應於中間頁目錄
4. PTRS_PER_PTE: 頁表中項的數目
/*
咱們知道,Linux的四級頁表實現是向下兼容的,即在兩級頁表的體系結構中,會將PTRS_PER_PMD、PTRS_PER_PTE定義爲1,這使得內核的剩餘部分感受該體系結構也提供了四級頁錶轉換結構
*/

值2(N)次方的計算很容易經過從位置0左移n位計算而獲得,同時Linux的內核的內存頁管理的基本單位都是以2爲底

2. 頁表的格式

內核提供了4中數據結構,用來表示頁表項的結構
\linux-2.6.32.63\include\asm-generic\page.h

/*
These are used to make use of C type-checking..
*/
1. pgd_t: 全局頁目錄項
typedef struct 
{
    unsigned long pgd;
} pgd_t;

3 pmd_t: 中間頁目錄項
typedef struct 
{
    unsigned long pmd[16];
} pmd_t;

4. pte_t: 直接頁表項
typedef struct
{
    unsigned long pte;
} pte_t;

pgprot_t:
typedef struct 
{
    unsigned long pgprot;
} pgprot_t;
typedef struct page *pgtable_t;

內核同時還提供了用於分析頁表項的標準函數,根據不一樣的體系結構,一些函數可能實現爲宏而另外一些則實現爲內聯函數

...
#define pgd_val(x)    ((x).pgd)    //將pte_t等類型的變量轉換爲unsigned long    
#define pmd_val(x)    ((&x)->pmd[0])
#define pte_val(x)    ((x).pte)
#define pgprot_val(x)    ((x).pgprot)

#define __pgd(x)    ((pgd_t) { (x) } )    //pgd_val等函數的逆,將unsigned long整數轉換爲pgd_t等類型的變量
#define __pmd(x)    ((pmd_t) { (x) } )
#define __pte(x)    ((pte_t) { (x) } )
#define __pgprot(x)    ((pgprot_t) { (x) } )
...

PAGE_ALIGN是每種體系結構都必須定義的標準宏,它須要一個地址做爲參數,並將地址"舍入"到下一頁的起始處,即老是返回頁的整倍數。爲了用好處理器的高速緩存資源,將地址對齊到頁邊界是很重要的

從某種程度上來講,四級頁表的索引是體現(基於)對內存地址的分段索引之上的,即將一個內存地址切成幾段,每一段分別表明不一樣層次的索引信息

3. 特定於PTE的信息

最後一級頁表中的項不只包含了指向頁的內存位置的指針,還在多餘比特位包含了與頁有關的附加信息,儘管這些數據是特定於CPU的,可是它們提供了有關"頁訪問控制"的一些信息

1. _PAGE_PRESENT
指定了虛擬內存頁是否存在於內存中,由於頁可能被換出到交換區
    1) 若是頁不在內存中,那麼頁表項的結構一般會有所不一樣,由於不須要描述頁在內存中的位置
    2) 若是頁存在於內存中,就須要信息來標識並找到換出的頁

2. _PAGE_ACCESSED
CPU每次訪問頁時,會自動設置_PAGE_ACCESSED,內核會按期檢查該比特位,以確認頁使用的活躍程序(不常用的頁,比較適合換出)。在讀或寫訪問以後會設置該比特位

3. _PAGE_DIRTY
表示該頁是不是"髒的",即頁的內容是否已經被修改過

4. _PAGE_FILE
數值和_PAGE_DIRTY相同,但用於不一樣的上下文,即頁不在內存中的時候,顯然不存在的頁不多是髒的(由於它不可能被進程修改),所以能夠從新解釋該比特位
    1) 若是沒有設置,則該項指向一個換出頁的位置
    2) 若是該項屬於非線性文件映射,則須要設置_PAGE_FILE

5. _PAGE_USER
若是設置了_PAGE_USER,則容許用戶空間代碼訪問該頁,不然只有內核才能訪問(或CPU處於系統狀態的時候)

6. _PAGE_READ、_PAGE_WRITE、_PAGE_EXECUTE
指定了普通的用戶進程是否容許讀取、寫入、執行該頁中的機器代碼
//內核內存中的頁必須防止用戶進程寫入,對於訪問權限粒度不是很是細的體系結構而言,若是沒有進一步的準則能夠區分讀寫訪問權限,則會定義_PAGE_RW常數,用於同時容許或禁止讀寫訪問

7. _PAGE_BIT_NX
IA-32、AMD64提供了_PAGE_BIT_NX,用於將頁標記爲"不可執行"的(在IA-32系統上,只有啓用了"可尋址64GB內存的頁面地址擴展(page address extension PAE)"時,才能使用該保護位)。它能夠有效防止執行棧頁上的代碼,不然,惡意代碼可能經過緩衝區溢出手段在棧上執行代碼,致使程序的安全漏洞

8. __pgprot、pte_modidy()
每一個體繫結構都必須提供兩個東西,使得內存管理子系統可以修改pte_t項中額外的比特位
    1) 保存額外的比特位的__pgprot數據結構
    2) 以及修改這些比特位的pte_modidy()函數

內核還定義了各類函數,用於查詢和設置內存頁與體系結構相關的狀態,某些處理器可能缺乏對一些給定特性的硬件支持,所以並不是全部的處理器都定義了全部函數
\linux-2.6.32.63\arch\x86\include\asm\pgtable.h

1. pte_present: 檢查頁表項指向的頁是否存在於內存中,該函數能夠用於檢測一頁是否已經換出
2. pte_read: 從用戶空間是否能夠讀取該頁
3. pte_write: 檢查內核是否能夠寫入到該頁
4. pte_exec: 檢查該頁中的數據是否能夠做爲二進制代碼執行
5. pte_dirty: 檢查與頁表項相關的頁是不是髒的,即其內容在上次內核檢查以後是否以已經修改過,須要注意的是,只有在pte_present確認了該頁可用的狀況下(即存在於內存中),才能調用該函數
6. pte_file: 用於非線性映射,經過操做頁表提供了文件內容的一種不一樣視圖,該函數檢查頁表項是否屬於這樣的一個映射,要注意的是,只有在pte_present返回false時,才能調用pte_file,即與該頁表項相關的頁再也不內存中
7. pte_young: 訪問位(一般是_PAGE_ACCESSED)是否設置
8. pte_rdprotect: 清除該頁的讀權限
9. pte_wrprotect: 清除該頁的寫權限 
10. pte_exprotect: 清除執行該頁中的二進制數據的權限
11. pte_mkread: 設置讀權限
12. pte_mkwrite: 設置寫權限
13. pte_mkexec: 容許執行頁的內容
14. pte_mkdirty: 將頁標記爲髒
15. pte_mkclean: "清除"頁,一般指清除_PAGE_DIRTY位
16. pte_mkyoung: 設置訪問位,在大多數體系結構上是_PAGE_ACCESSED
17. pte_mkold: 清除訪問位
//這些函數常常分爲3組,分別用於設置、刪除、查詢某個特定的屬性

0x2: 頁表項的建立和操做

下列爲用於建立新頁表項的全部函數
\linux-2.6.32.63\arch\x86\include\asm\pgtable.h

1. mk_pte
#define mk_pte(page, pgprot)   pfn_pte(page_to_pfn(page), (pgprot))
建立一個頁表項,必須將page實例和所需的頁訪問權限(__pgprot)做爲參數傳遞
2. pte_page
#define pte_page(pte)    pfn_to_page(pte_pfn(pte))
得到頁表項描述的頁對應的page實例地址
3. pte_alloc
4. pte_free
5. set_pte

6. pgd_alloc
\linux-2.6.32.63\arch\x86\mm\pgtable.c
分配並初始化一個可容納完整頁表的內存
7. pgd_free: 釋放頁表佔據的內存
8. set_pgd: 設置頁表中某項的值

9. pud_alloc
10. pud_free
11. set_pud

12. pmd_alloc
13. pmd_free
14. set_pmd

 

2. 結構化內存管理

在內存管理的上下文中,初始化(initiation)能夠有多種含義,在許多CPU上,必須顯示設置適合Linux內核的內存模型(例如在IA-32系統上須要切換到保護模式,而後內核才能檢測可用內存和寄存器)。在初始化過程當中,還必須創建內存管理的數據結構,以及其餘不少事務。由於內核在內存管理徹底初始化以前就須要使用內存,在系統啓動過程期間,使用了一個額外的簡化形式的內存管理模塊,而後又丟棄掉

0x1: 創建數據結構

對相關數據結構的初始化是從全局啓動例程start_kernel中開始的,該例程在加載內核並激活各個子系統以後執行,因爲內存管理是內核一個很是重要的部分,所以在特定於體系結構的設置步驟中檢測內存並肯定系統中內存的分配狀況後,會當即執行內存管理的初始化。此時,已經對各類系統內存模式生成了一個pgdat_t實例,用於保存諸如"結點中內存數量"以及內存在各個"內存域"之間分配狀況的信息。全部平臺都實現了特定於體系結構的NODE_DATA宏,用於經過結點編號查詢與一個NUMA結點相關的pgdat_t實例

1. 先決條件

因爲大部分系統都只有一個"內存結點",爲了確保內存管理代碼是可移植的(一樣能夠適用於UMA和NUMA系統),內核在"\linux-2.6.32.63\mm\page_alloc.c"中定義了一個pg_data_t實例管理全部的物系統內存,這不是特定於CPU的實現,大多數體系結構都採用了該方案

linux-2.6.32.63\arch\x86\include\asm\mmzone_64.h
#define NODE_DATA(nid)        (node_data[nid])

\linux-2.6.32.63\arch\m32r\mm\discontig.c
struct pglist_data *node_data[MAX_NUMNODES];
EXPORT_SYMBOL(node_data);

儘管該宏有一個形式參數用於選擇NUMA結點,但在UMA系統中只有一個僞結點,所以老是返回一樣的數據。內核也能夠依賴於下述事實: 體系結構相關的初始化代碼將"MAX_NUMNODES"設置爲系統中結點的數目,在UMA系統上由於只有一個(形式上的)結點,所以該值老是1,在編譯期間,預處理器會爲特定的配置選擇正確的值

2. 系統啓動

下圖給出的start_kernel的代碼流程圖,其中包括了與內存管理相關的系統初始化函數

1. setup_arch
特定於體系結構的設置函數,其中一項任務是負責初始化自舉分配器

2. setup_per_cpu_areas
    1) 在SMP系統上,setup_per_cpu_areas初始化源代碼中定義的靜態per-cpu變量,這種變量對系統中的每一個CPU都有一個獨立的副本(這類變量保存在內核二進制映像的一個獨立的段中),setup_per_cpu_areas的目的是爲系統的各個CPU分別建立一份這些數據的副本
    2) 在非SMP系統上,該函數是一個空操做

3. build_all_zonelists
創建"結點""內存域"的數據結構

4. mem_init
另外一個特定於體系結構的函數,用於停用bootmem分配器並遷移到實際的內存管理函數

5. setup_per_cpu_pageset
爲"strcut zone->struct per_cpu_pageset pageset[NR_CPUS]"數組的第一個數組元素分配內存。分配第一個數組元素,即意味着爲第一個系統處理器分配,系統的全部內存域都會考慮進來
該函數還負責設置冷熱分配器的限制
//SMP系統上對應於其餘CPU的pageset數組成員,將會在相應的CPU激活時初始化

3. 結點和內存域初始化

build_all_zonelists創建管理結點及其內存域所需的數據結構,該函數能夠經過內核的宏和抽象機制實現,而不用考慮具體的NUMA或UMA系統,由於執行的函數實際上有兩種形式,一種用於NUMA系統,另外一種用於UMA系統。Linux內核常常採用這種技術

#ifdef CONFIG_WORK_HARD
void do_work()
{
    //start
}
#else
void do_work()
{
    //stop
}
#endif

\linux-2.6.32.63\mm\page_alloc.c

void build_all_zonelists(void)
{
    /*
    在當前處理的結點和系統中其餘結點的內存域之間創建一種等級次序,接下來,依據這種次序分配內存
    考慮下面的例子
    內核想要分配高端內存,它首先試圖在當前結點的高端內存域找到一個大小適當的空閒段,若是失敗,則查看該節點的普通域,若是還失敗,則試圖在該結點的DMA內存域執行分配
    若是在3個本地內存域都沒法找到空閒內存,則查看其餘結點,在這種狀況下,備選結點應該儘量靠近主結點,以最小化因爲訪問非本地內存引發的性能損失

    內核將內存域分爲層次結構,首先試圖分配"廉價的"內存,若是失敗,則根據訪問速度和容量,逐漸嘗試分配"更昂貴的"內存
    1. 高端內存是最"廉價的",由於內核沒有部分依賴於從該內存域分配的內存,若是高端內存域用盡,對內核沒有任何反作用,這也是優先分配高端內存的緣由
    2. 普通內存域的狀況有所不一樣,許多內核數據結構必須保存在該內存域,而不能放置到高端內存域。所以若是普通內存徹底用盡,那麼內核會面臨緊急狀況,因此只要高端內存域的內存域沒有用盡,都不會從普通內存域分配內存
    3. 最昂貴的是DMA內存域,由於它用於外設和系統之間的數據傳輸,所以從該內存域分配內存是最後一招(通常狀況下不會從DMA中分配內存)
    */
    set_zonelist_order();

    if (system_state == SYSTEM_BOOTING) 
    {
        __build_all_zonelists(NULL);
        mminit_verify_zonelist();
        cpuset_init_current_mems_allowed();
    } 
    else 
    {
        /* we have to stop all cpus to guarantee there is no user of zonelist */
        stop_machine(__build_all_zonelists, NULL, NULL);
        /* cpuset refresh routine should be here */
    }
    vm_total_pages = nr_free_pagecache_pages();
    /*
     * Disable grouping by mobility if the number of pages in the
     * system is too low to allow the mechanism to work. It would be
     * more accurate, but expensive to check per-zone. This check is
     * made on memory-hotadd so a system can start with mobility
     * disabled and enable it later
     */
    if (vm_total_pages < (pageblock_nr_pages * MIGRATE_TYPES))
        page_group_by_mobility_disabled = 1;
    else
        page_group_by_mobility_disabled = 0;

    printk("Built %i zonelists in %s order, mobility grouping %s.  "
        "Total pages: %ld\n",
            nr_online_nodes,
            zonelist_order_name[current_zonelist_order],
            page_group_by_mobility_disabled ? "off" : "on",
            vm_total_pages);
#ifdef CONFIG_NUMA
    printk("Policy zone: %s\n", zone_names[policy_zone]);
#endif
}
//將全部工做都委託給__build_all_zonelists

/* non-NUMA variant of zonelist performance cache - just NULL zlcache_ptr */
static void build_zonelist_cache(pg_data_t *pgdat)
{
    pgdat->node_zonelists[0].zlcache_ptr = NULL;
}

#endif    /* CONFIG_NUMA */

/* return values int ....just for stop_machine() */
static int __build_all_zonelists(void *dummy)
{
    int nid;

#ifdef CONFIG_NUMA
    memset(node_load, 0, sizeof(node_load));
#endif
    //對系統中的各個NUMA結點分別調用build_zonelists
    for_each_online_node(nid) 
    {
     //pg_data_t *pgdat包含告終點內存配置的全部現存信息,且新建的數據機構也會放置在其中 pg_data_t
*pgdat = NODE_DATA(nid); build_zonelists(pgdat); build_zonelist_cache(pgdat); } return 0; }

build_zonelists(pgdat);

static void build_zonelists(pg_data_t *pgdat)
{
    int node, local_node;
    enum zone_type j;
    struct zonelist *zonelist;

    local_node = pgdat->node_id;

    zonelist = &pgdat->node_zonelists[0];
    j = build_zonelists_node(pgdat, zonelist, 0, MAX_NR_ZONES - 1);

    /*
     * Now we build the zonelist so that it contains the zones
     * of all the other nodes.
     * We don't want to pressure a particular node, so when
     * building the zones for node N, we make sure that the
     * zones coming right after the local ones are those from
     * node N+1 (modulo N)
     */
    //迭代全部的結點內存域,每一個循環在node_zonelist數組中找到第i個zonelist,對第i個內存域計算備用列表
    for (node = local_node + 1; node < MAX_NUMNODES; node++) 
    {
        if (!node_online(node))
            continue;
        //實際工做委託給build_zonelists_node
        j = build_zonelists_node(NODE_DATA(node), zonelist, j, MAX_NR_ZONES - 1);
    }
    for (node = 0; node < local_node; node++) 
    {
        if (!node_online(node))
            continue;
        j = build_zonelists_node(NODE_DATA(node), zonelist, j, MAX_NR_ZONES - 1);
    }

    zonelist->_zonerefs[j].zone = NULL;
    zonelist->_zonerefs[j].zone_idx = 0;
}

build_zonelists_node

/*
 * Builds allocation fallback zone lists.
 *
 * Add all populated zones of a node to the zonelist.
 */
/*
備用列表的各項是根據zone_type參數排序的,該參數指定了最優選擇哪一個內存域,該參數的初始值是外層循環的控制變量i,咱們知道其值多是
1. ZONE_HIGHMEM
2. ZONE_NORMAL
3. ZONE_DMA / ZONE_DMA32

nr_zones表示從備用列表中的哪一個位置開始填充新項,因爲列表中尚沒有項,所以調用者傳遞了0
*/
static int build_zonelists_node(pg_data_t *pgdat, struct zonelist *zonelist, int nr_zones, enum zone_type zone_type)
{
    struct zone *zone;

    BUG_ON(zone_type >= MAX_NR_ZONES);
    zone_type++;

    do 
    {
        //在每一步結束時,都將內存域類型減一,即設置爲一個更"昂貴"的內存域類型(例如從ZONE_HIGHMEM -> ZONE_NORMAL -> ZONE_DMA)
        zone_type--;
        zone = pgdat->node_zones + zone_type;
        if (populated_zone(zone)) 
        {
            zoneref_set_zone(zone,
                &zonelist->_zonerefs[nr_zones++]);
            check_highest_zone(zone_type);
        }

    } while (zone_type);
    return nr_zones;
}

0x2: 特定於體系結構的設置

在IA-32系統上內存管理的初始化在一些細節方面很是微秒,其中必須克服一些與處理器體系結構相關的問題,例如

1. 將處理器從普通模式切換到保護模式
2. 授予CPU訪問32位地址空間的權限
3. 兼容16位8086處理器
4. 分頁在默認狀況下沒有啓用,必須手動激活,這涉及處理器的CR0寄存器

1. 內核在內存中的佈局

在學習各個具體的內存初始化操做以前,咱們先來學習啓動裝載程序將內核複製到內存,而初始化例程的彙編程序部分也已經執行完畢後,此時內存中的具體佈局,內核被裝載到物理內存中的一個固定位置,該位置在編譯時肯定,配置選項PHYSICAL_START用於肯定內核在內存中的位置,會受到配置選項PHYSICAL_ALIGN設置的物理對齊方式的影響

1. 0 ~ 0x1000(4KB): 第一個頁幀,通常會忽略,覺得一般保留給BIOS使用
2. 0x1000 ~ 0x9c80(640KB): 原則上是可用的,但也不用於內核加載,緣由是該區域以後緊鄰的區域由系統保留,用於映射各類ROM(一般是系統BIOS和顯卡ROM),不可能向映射ROM的區域寫入數據。但內核老是會裝載到一個連續的內存區中,若是要從4KB處做爲起始位置來裝載內核映像,則要求內核必須小於640KB
3. 0x9c80 ~ 0x100000: 可用內存區域
4. 0x100000 ~ end: 爲了解決內核加載的問題,IA-32內核使用0x100000做爲起始地址,今後處開始,有足夠的連續內存區,可容納整個內核,內核佔據的內存分爲幾個段,其邊界保存在變量中
    1) _text ~ _etext: 內核代碼段的起始和結束地址,包含了編譯後的內核代碼
    2) _etext ~ _edata: 數據段,保存了大部份內核變量
    3) _edata ~ _end: 初始化數據在內核啓動過程結束後再也不須要(例如,包含初始化爲0的全部靜態全局變量的BSS段),保存在最後一段,在內核初始化完成後,其中的大部分數據均可以從內存刪除,給應用程序留出更多空間
//準確的數值依內核配置而異,由於每種配置的代碼段和數據段長度都不相同,這取決於啓動和禁用了內核的哪些部分,只有起始地址(_text)是相同的

每次編譯內核時,都生成一個文件System.map並保存在源碼目錄下,其中包括了

1. 全局變量
2. 內核定義的導出函數
3. 例程函數地址
4. 內存分段常數的值
    1) _text
    2) _etext
    3) _edata
    4) _end
// /proc/iomem也提供了有關物理內存劃分的各個段的一些信息

2. 初始化步驟

在內核已經載入內存、而初始化的彙編程序部分已經執行完畢後,內核必須執行一些特定於系統的步驟

1. machine_specific_memory_setup
建立一個列表,包括系統佔據的內存區和空閒內存區,BIOS提供的映射給出了在這種狀況下使用的各個內存區,在系統啓動時,找到的內存區由內核函數print_memory_map顯示
dmesg
BIOS-provided physical RAM map:
 BIOS-e820: 0000000000000000 - 000000000009ec00 (usable)
 BIOS-e820: 000000000009ec00 - 00000000000a0000 (reserved)
 BIOS-e820: 00000000000dc000 - 0000000000100000 (reserved)
 BIOS-e820: 0000000000100000 - 000000007fee0000 (usable)
 BIOS-e820: 000000007fee0000 - 000000007feff000 (ACPI data)
 BIOS-e820: 000000007feff000 - 000000007ff00000 (ACPI NVS)
 BIOS-e820: 000000007ff00000 - 0000000080000000 (usable)
 BIOS-e820: 00000000f0000000 - 00000000f8000000 (reserved)
 BIOS-e820: 00000000fec00000 - 00000000fec10000 (reserved)
 BIOS-e820: 00000000fee00000 - 00000000fee01000 (reserved)
 BIOS-e820: 00000000fffe0000 - 0000000100000000 (reserved)

 2. parse_cmdline_early
 分析命令行,從本質上來說,內核也是一個進程,須要被連接進系統中,載入內核映像的時候也有參數傳入,例如
    1) mem=XXX[KkmM]
    2) highmem=XXX[KkmM]
    3) memmap=XXX[KkmM]
若是內核計算的值或BIOS提供的值不正確,管理員能夠修改可用內存的數量或手工劃定內存區

3. setup_memory
    1) 肯定(每一個結點)可用的物理內存頁的數目
    2) 初始化bootmem分配器
    3) 分配各類內存區,例如運行第一個用戶空間過程所需的最初的RAM磁盤

4. paging_init
初始化內核頁表並啓用內存分頁,由於IA-32計算機上默認狀況下分頁是禁用的,若是內核編譯了PAE支持,並且處理器也支持Execute Disabled Protection,則啓用該特性
    1) pagetable_init: 該函數確保了直接映射到內核地址空間的物理內存被初始化,低端內存中的全部頁幀都直接映射到PAGE_OFFSET之上的虛擬內存區,這使得內核無需處理頁表,便可尋址至關一部分可用內存

5. zone_sizes_init
初始化系統中全部結點pgdat_t實例
    1) add_active_range: 對可用的物理內存創建一個相對簡單的列表
    2) free_area_init_nodes: 這是體系結構無關的函數,創建完備的內核數據結構

3. 分頁機制的初始化

咱們知道,paging_init負責創建只能用於內核的頁表,用於空間沒法訪問,這對管理用戶態應用程序和內核訪問內存的方式有深遠的影響
\linux-2.6.32.63\arch\x86\mm\init_32.c

/*
paging_init() sets up the page tables - note that the first 8MB are already mapped by head.S.
This routines also unmaps the page at virtual kernel address 0, so that we can trap those pesky NULL-reference errors in the kernel.
*/
void __init paging_init(void)
{
    //初始化系統的頁表
    pagetable_init();

    //在pagetable_init完成頁表初始化以後,則將CR3寄存器設置爲指向全局頁目錄(swapper_pg_dir)的指針,此時必須激活新的頁表

    //因爲TLB緩存項仍然包含了啓動時分配的一些內存地址數據,此時也必須刷出,與上下文切換期間相反,設置了_PAGE_GLOBAL位的頁也要刷出
    __flush_tlb_all();

    //kmap_init初始化全局變量kmap_pte,在從高端內存內存域將頁映射到內核地址空間時,會使用該變量存入相應內存區的頁表項,此外,用於高端內存內核映射的第一個固定映射內存區的地址保存在全局變量kmem_vstart中
    kmap_init();

    /*
     * NOTE: at this point the bootmem allocator is fully available.
     */
    sparse_init();
    zone_sizes_init();
}

static void __init pagetable_init(void)
{
    //以swapper_pg_dir爲基礎初始化系統的頁表
    pgd_t *pgd_base = swapper_pg_dir;

    //創建固定映射和持久內核,用適當的值填充頁表
    permanent_kmaps_init(pgd_base);
}

咱們以前學習過per-CPU(冷熱)緩存,咱們接下來學習內核如何處理相關數據結構的初始化,以及用於控制緩存填充行爲的"水印"的計算
zone_pcp_init負責初始化該緩存,該函數由free_area_init_nodes
\linux-2.6.32.63\mm\page_alloc.c

static __meminit void zone_pcp_init(struct zone *zone)
{
    int cpu;
    //用zone_batchsize算出批量大小(用於計算最小和最大填充水平的基礎)後,代碼將遍歷系統中的全部CPU
    unsigned long batch = zone_batchsize(zone);

    for (cpu = 0; cpu < NR_CPUS; cpu++) {
#ifdef CONFIG_NUMA
        /* Early boot. Slab allocator not functional yet */
        zone_pcp(zone, cpu) = &boot_pageset[cpu];
        setup_pageset(&boot_pageset[cpu],0);
#else
        //調用setup_pageset填充每一個per_cpu_pageset實例的常量,使用了zone_pcp宏來選擇與當前CPU相關的內存域的pageset實例
        setup_pageset(zone_pcp(zone,cpu), batch);
#endif
    }
    if (zone->present_pages)
        printk(KERN_DEBUG "  %s zone: %lu pages, LIFO batch:%lu\n",
            zone->name, zone->present_pages, batch);
}

static int zone_batchsize(struct zone *zone)
{
#ifdef CONFIG_MMU
    int batch;

    /*
     * The per-cpu-pages pools are set to around 1000th of the
     * size of the zone.  But no more than 1/2 of a meg.
     *
     * OK, so we don't know how big the cache is.  So guess.
     */
    batch = zone->present_pages / 1024;
    if (batch * PAGE_SIZE > 512 * 1024)
        batch = (512 * 1024) / PAGE_SIZE;
    batch /= 4;        /* We effectively *= 4 below */
    if (batch < 1)
        batch = 1;

    /*
     * Clamp the batch to a 2^n - 1 value. Having a power
     * of 2 value was found to be more likely to have
     * suboptimal cache aliasing properties in some cases.
     *
     * For example if 2 tasks are alternately allocating
     * batches of pages, one task can end up with a lot
     * of pages of one half of the possible page colors
     * and the other with pages of the other colors.
     */
    batch = rounddown_pow_of_two(batch + batch/2) - 1;

    return batch;

#else
    /* The deferral and batching of frees should be suppressed under NOMMU
     * conditions.
     *
     * The problem is that NOMMU needs to be able to allocate large chunks
     * of contiguous memory as there's no hardware page translation to
     * assemble apparent contiguous memory from discontiguous pages.
     *
     * Queueing large contiguous runs of pages for batching, however,
     * causes the pages to actually be freed in smaller chunks.  As there
     * can be a significant delay between the individual batches being
     * recycled, this leads to the once large chunks of space being
     * fragmented and becoming unavailable for high-order allocations.
     */
    return 0;
#endif
}

上述代碼計算獲得的batch,大約至關於內存域中頁數的0.25%,根據經驗,緩存大小是主內存的千分之一,考慮到當前系統每一個CPU配備的物理內存大約在1GB ~ 2GB,該規則是有意義的,這樣,計算獲得的批量大小使得冷熱頁緩存中的頁有可能放置到CPU的L2緩存中
在zone_pcp_init結束時,會輸出各個內存域的頁數以及計算出的批量大小

dmesg | grep LIFO
    DMA zone: 56 pages used for memmap
    DMA zone: 101 pages reserved
    DMA zone: 3825 pages, LIFO batch:0
    DMA32 zone: 7112 pages used for memmap
    DMA32 zone: 513048 pages, LIFO batch:31

4. 註冊活動內存區

活動內存區就是不包含空洞的內存區,必須使用add_active_range在全局變量early_node_map中註冊內存區

5. AMD64地址空間的設置

AMD64系統地址空間的設置在某些方面比IA-32要容易,但在另外一些方面要困難,雖然64位地址空間避免了古怪的高端內存區域,但有另外一個因素使狀況複雜化,即64位地址空間跨度太大,基本沒有應用程序須要這個

0x3: 啓動過程期間的內存管理

在內核加載啓動過程當中,儘管內存管理還沒有初始化,但內核仍然須要分配內存以建立各類數據結構,bootmem分配器用於在啓動階段早期分配內存。顯然,對該分配器的需求集中在簡單性方面,而不是性能和通用性,所以Linux內核實現了一個"最早適配(first-fit)"分配器用於在啓動階段管理內存

1. bootmem使用一個"位圖(bitmap)"來管理頁,位圖比特位的數目與系統中物理內存頁的數目相同
    1) 比特位爲1: 表示已經使用該頁
    2) 比特位爲0: 表示空閒頁
2. 在須要分配內存時,分配器逐位掃描位圖,直至找到一個可以提供足夠"連續頁"的位置,即所謂的最早最佳(first-best)或最早適配位置
/*
該過程不是很高效(緣由在於每次都須要遍歷操做),由於每次分配都必須從頭掃描比特鏈,所以在內核徹底初始化以後,不能將該分配器用於內存管理。夥伴系統(連同slab、slub、slob分配器)是一個更好的技術方案
*/

1. 數據結構

最早適配分配器也必須管理一些數據,內核爲系統中每一個結點都提供了一個bootmem_data結構的實例,該結構所需的內存沒法動態分配,必須在編譯時分配給內核。在UMA系統上該分配的實現與CPU無關(NUMA系統採用了特定於體系結構的解決方案)
\linux-2.6.32.63\include\linux\bootmem.h

/*
node_bootmem_map is a map pointer - the bits represent all physical  memory pages (including holes) on the node.
*/
typedef struct bootmem_data 
{
    //node_min_pfn保存了系統中第一個頁的編號,大多數體系結構下都是零
    unsigned long node_min_pfn;
    
    //node_low_pfn是能夠直接管理的物理地址空間中最後一頁的編號(即ZONE_NORMAL的結束頁)
    unsigned long node_low_pfn;

    //node_bootmem_map是指向存儲分配位圖的指針,在IA-32系統上,該內存區(位圖)緊接在內核映像以後,對應的地址保存在_end變量中,該變量在連接期間自動地插入到內核映像中
    void *node_bootmem_map;

    unsigned long last_end_off;
    unsigned long hint_idx;

    /*
    內存不連續的系統可能須要多個bootmem分配器,一個典型的例子是NUMA計算機,其中每一個結點註冊了一個bootmem分配器,但若是物理地址空間中散佈者空洞,也能夠爲每一個連續內存區註冊一個bootmem分配器
    註冊新的自舉分配器(bootmem)能夠使用init_bootmem_core,全部註冊的分配器保存在一個鏈表中,表頭是全局變量bdata_list
    */
    struct list_head list;
} bootmem_data_t;

extern bootmem_data_t bootmem_node_data[];

2. 初始化

bootmem分配器的初始化是一個特定於體系結構的過程,同時還取決於計算機的內存佈局,在IA-32使用setup_memory實現

setup_memory在setup_arch中被調用
/source/arch/m32r/kernel/setup.c 

#ifndef CONFIG_DISCONTIGMEM
static unsigned long __init setup_memory(void)
{
    unsigned long start_pfn, max_low_pfn, bootmap_size;

    //setup_memory分析檢測到的內存區,以找到低端內存區中最大的頁幀號,由於高端內存處理太過麻煩,所以對bootmem分配器無用
    start_pfn = PFN_UP( __pa(_end) );
    
    //全局變量max_low_pfn保存了可映射的最高頁的編號,內核會在啓動日誌中報告找到的內存數量 
    max_low_pfn = PFN_DOWN( __pa(memory_end) );

    /*
     * Initialize the boot-time allocator (with low memory only):
     */
    bootmap_size = init_bootmem_node(NODE_DATA(0), start_pfn, CONFIG_MEMORY_START>>PAGE_SHIFT, max_low_pfn);

    /*
     * Register fully available low RAM pages with the bootmem allocator.
     */
    {
        unsigned long curr_pfn;
        unsigned long last_pfn;
        unsigned long pages;

        /*
         * We are rounding up the start address of usable memory:
         */
        curr_pfn = PFN_UP(__pa(memory_start));

        /*
         * ... and at the end of the usable range downwards:
         */
        last_pfn = PFN_DOWN(__pa(memory_end));

        if (last_pfn > max_low_pfn)
            last_pfn = max_low_pfn;

        pages = last_pfn - curr_pfn;
        free_bootmem(PFN_PHYS(curr_pfn), PFN_PHYS(pages));
    }

    /*
    Reserve the kernel text and Reserve the bootmem bitmap. We do this in two steps 
    first step was init_bootmem()), because this catches the (definitely buggy) case of us accidentally initializing the bootmem allocator with an invalid RAM area.
    
    因爲bootmem分配器須要一些內存頁用於管理分配位圖,必須首先調用reserve_bootmem分配這些內存頁
    但還有一些其餘的內存區已經在使用中,必須相應地標記出來,所以還須要用reserve_bootmem註冊相應的頁,須要註冊的內存區的準確數目,高度依賴於內核配置,例如
    1. 須要保留0頁,由於在許多計算機上該頁是一個特殊的BIOS頁,有些特定於計算機的功能須要該頁才能運做正常
    */
    reserve_bootmem(CONFIG_MEMORY_START + PAGE_SIZE, (PFN_PHYS(start_pfn) + bootmap_size + PAGE_SIZE - 1) - CONFIG_MEMORY_START, BOOTMEM_DEFAULT);

    /*
    reserve physical page 0 - it's a special BIOS page on many boxes, enabling clean reboots, SMP operation, laptop functions.
    */
    reserve_bootmem(CONFIG_MEMORY_START, PAGE_SIZE, BOOTMEM_DEFAULT);

    /*
     * reserve memory hole
     */
#ifdef CONFIG_MEMHOLE
    reserve_bootmem(CONFIG_MEMHOLE_START, CONFIG_MEMHOLE_SIZE,
            BOOTMEM_DEFAULT);
#endif

#ifdef CONFIG_BLK_DEV_INITRD
    if (LOADER_TYPE && INITRD_START) {
        if (INITRD_START + INITRD_SIZE <= (max_low_pfn << PAGE_SHIFT)) {
            reserve_bootmem(INITRD_START, INITRD_SIZE,
                    BOOTMEM_DEFAULT);
            initrd_start = INITRD_START + PAGE_OFFSET;
            initrd_end = initrd_start + INITRD_SIZE;
            printk("initrd:start[%08lx],size[%08lx]\n",
                initrd_start, INITRD_SIZE);
        } else {
            printk("initrd extends beyond end of memory "
                "(0x%08lx > 0x%08lx)\ndisabling initrd\n",
                INITRD_START + INITRD_SIZE,
                max_low_pfn << PAGE_SHIFT);

            initrd_start = 0;
        }
    }
#endif

    return max_low_pfn;
}
#else    /* CONFIG_DISCONTIGMEM */
extern unsigned long setup_memory(void);
#endif    /* CONFIG_DISCONTIGMEM */

3. 對內核的接口

1. 分配內存

內核提供了各類函數,用於在初始化期間分配內存,在UMA系統上有下列函數可用
    1) alloc_bootmem(size)
    2) alloc_bootmem_pages(size)
按指定的大小在ZONE_NORMAL內存域分配內存,數據是對齊的,這使得內存或者從可適用於L1高速緩存的理想位置開始,或者從邊界開始
/*
須要注意的是,用alloc_bootmem/alloc_bootmem_pages函數申請指定大小的內存。若是須要在其餘地方調用這塊內存,能夠將alloc_bootmem返回的內存首地址經過EXPORT_SYMBOL導出,而後就能夠使用這塊內存了,這種內存分配不是否是經過動態分配得到的,而是相似於"內核引導參數"在編譯時就預留出來的內核內存
這種內存分配方式的缺點是,申請內存的代碼必須在連接到內核中的代碼裏才能使用,所以必須從新編譯內核,並且內存管理系統看不到這部份內存,須要用戶自行管理
*/
    3) alloc_bootmem_low
    4) alloc_bootmem_low_pages
alloc_bootmem_low、alloc_bootmem_low_pages和alloc_bootmem、alloc_bootmem_pages相似,區別在於alloc_bootmem_low系列只是從ZONE_DMA內存域分配內存,所以,只有須要DMA內存時,才能使用上述API函數

這些函數都是__alloc_bootmem的前端,__alloc_bootmem將實際工做委託給__alloc_bootmem_nopanic,因爲能夠註冊多個bootmem分配器(這些分配器都保存在一個全局鏈表中),__alloc_bootmem_core會遍歷全部的分配器,直至分配成功爲止
/source/mm/bootmem.c

/*
1. size: 所需內存區的長度
2. align: 數據的對齊方式
3. goal: 開始搜索適當空閒內存區的起始地址
    1) SMP_CACHE_BYTES: 對齊數據,使之在大多數體系結構上可以理想地置於L1高速緩存中
    2) PAGE_SIZE: 將數據對齊到頁邊界,適用於分配一個或多個整頁
*/
static void * __init ___alloc_bootmem(unsigned long size, unsigned long align, unsigned long goal, unsigned long limit)
{
    void *mem = ___alloc_bootmem_nopanic(size, align, goal, limit);

    if (mem)
        return mem;
    /*
     * Whoops, we cannot satisfy the allocation request.
     */
    printk(KERN_ALERT "bootmem alloc of %lu bytes failed!\n", size);
    panic("Out of memory");
    return NULL;
}

static void * __init ___alloc_bootmem_nopanic(unsigned long size, unsigned long align, unsigned long goal, unsigned long limit)
{
    bootmem_data_t *bdata;
    void *region;

restart:
    region = alloc_arch_preferred_bootmem(NULL, size, align, goal, limit);
    if (region)
        return region;

    list_for_each_entry(bdata, &bdata_list, list) 
    {
        if (goal && bdata->node_low_pfn <= PFN_DOWN(goal))
            continue;
        if (limit && bdata->node_min_pfn >= PFN_DOWN(limit))
            break;

        /*
        alloc_bootmem_core韓素的功能相對而言很普遍,在啓動期間不須要過高的效率,該函數主要實現了最早適配算法
        1. 從goal開始,掃描位圖,查找知足分配請求的空閒內存頁
        2. 若是目標頁緊接着上一次分配的頁,內核會判斷所需的內存(包括對齊數據所需的padding空間)是否可以在上一頁分配或從上一頁開始分配
        3. 新分配的頁在位圖對應的比特位設置1
        */
        region = alloc_bootmem_core(bdata, size, align, goal, limit);
        if (region)
            return region;
    }

    if (goal) 
    {
        goal = 0;
        goto restart;
    }

    return NULL;
}

2. 釋放內存

內核提供了free_bootmem函數來釋放內存

/**
 * free_bootmem - mark a page range as usable
 * @addr: starting address of the range
 * @size: size of the range in bytes
 *
 * Partial pages will be considered reserved and left as they are.
 *
 * The range must be contiguous but may span node boundaries.
 */
void __init free_bootmem(unsigned long addr, unsigned long size)
{
    unsigned long start, end;

    kmemleak_free_part(__va(addr), size);

    start = PFN_UP(addr);
    end = PFN_DOWN(addr + size);

    mark_bootmem(start, end, 0, 0);
}

4. 停用bootmem分配器

在系統初始化進行到夥伴分配器可以承擔內存管理的責任後,必須停用bootmem分配器,在UMA和NUMA系統上,停用分別由free_all_bootmem、free_all_bootmem_node完成,在夥伴系統創建以後,特定於體系結構的初始化代碼須要調用這兩個函數

/**
 * free_all_bootmem - release free pages to the buddy allocator
 *
 * Returns the number of pages actually released.
*/
unsigned long __init free_all_bootmem(void)
{
    return free_all_bootmem_core(NODE_DATA(0)->bdata);
}

static unsigned long __init free_all_bootmem_core(bootmem_data_t *bdata)
{
    int aligned;
    struct page *page;
    unsigned long start, end, pages, count = 0;

    if (!bdata->node_bootmem_map)
        return 0;

    start = bdata->node_min_pfn;
    end = bdata->node_low_pfn;

    /*
     * If the start is aligned to the machines wordsize, we might
     * be able to free pages in bulks of that order.
     */
    aligned = !(start & (BITS_PER_LONG - 1));

    bdebug("nid=%td start=%lx end=%lx aligned=%d\n", bdata - bootmem_node_data, start, end, aligned);

    //掃描bootmem分配器的頁位圖,釋放每一個未用的頁
    while (start < end) 
    {
        unsigned long *map, idx, vec;

        map = bdata->node_bootmem_map;
        idx = start - bdata->node_min_pfn;
        vec = ~map[idx / BITS_PER_LONG];

        if (aligned && vec == ~0UL && start + BITS_PER_LONG < end) 
        {
            int order = ilog2(BITS_PER_LONG);

            //到夥伴系統的接口是__free_pages_bootmem函數,該函數對每一個空閒頁調用,該函數內部依賴於標準函數__free_page,它使得這些頁併入夥伴系統的數據結構,在其中做爲空閒頁管理,可用於分配
            __free_pages_bootmem(pfn_to_page(start), order);
            count += BITS_PER_LONG;
        } 
        else 
        {
            unsigned long off = 0;

            while (vec && off < BITS_PER_LONG) 
            {
                if (vec & 1) 
                {
                    page = pfn_to_page(start + off);
                    __free_pages_bootmem(page, 0);
                    count++;
                }
                vec >>= 1;
                off++;
            }
        }
        start += BITS_PER_LONG;
    }

    page = virt_to_page(bdata->node_bootmem_map);
    pages = bdata->node_low_pfn - bdata->node_min_pfn;
    pages = bootmem_bootmap_pages(pages);
    count += pages;
    while (pages--)
        __free_pages_bootmem(page++, 0);

    bdebug("nid=%td released=%lx\n", bdata - bootmem_node_data, count);

    return count;
}

在頁位圖已經徹底掃描後,它佔據的內存空間也必須釋放,此後,只有夥伴系統可用於內存分配

5. 釋放初始化內存

許多內核代碼和數據表只在系統初始化階段須要,例如

1. 對於連接到內核中的驅動程序而言,則沒必要要在內核內存中保持其數據結構的"初始化例程(init函數)",在結構創建後,這些例程就不須要了
2. 驅動程序用於檢測其設備的硬件數據庫,在相關的設備已經識別以後,就再也不須要了

內核提供了兩個屬性(__init、__initcall)用於標記初始化函數和數據,這些必須置於函數或數據的聲明以前,例如

int __init hyper_hopper_probe(struc net_device *dev);
static char stilllocking_msg[] _initdata = "found.\n";

__init、__initdata不能使用普通的C語言實現,所以內核必須再一次藉助於特殊的GNU C編譯器語句。初始化函數實現的背後,其通常性的思想在於,將數據保持在內核映像的一個特定部分,在啓動結束時能夠徹底從內存刪除
\linux-2.6.32.63\include\linux\init.h

/* These are for everybody (although not all archs will actually discard it in modules) */
#define __init        __section(.init.text) __cold notrace
#define __initdata    __section(.init.data)
#define __initconst    __section(.init.rodata)
#define __exitdata    __section(.exit.data)
#define __exit_call    __used __section(.exitcall.exit)

爲從內存中釋放初始化數據,內核沒必要知道數據的性質,惟一相關的信息是這些數據和函數在內存中開始和結束的地址。因爲該信息在編譯時沒法獲得,它是內核在連接時插入的,爲提供該信息,內核定義一對變量__init_begin、__init_end
\linux-2.6.32.63\arch\x86\mm\init.c

void free_initmem(void)
{
    free_init_pages("unused kernel memory", (unsigned long)(&__init_begin), (unsigned long)(&__init_end));
}

free_initmem負責釋放用於初始化的內存區,並將相關的頁返回給夥伴系統,在啓動過程恰好結束時會調用該函數,緊接其後init做爲系統中第一個進程啓動,啓動日誌包含了一條信息,代表釋放了多少內存

dmesg
...
Freeing unused kernel memory: 1292k freed 
Freeing unused kernel memory: 788k freed
Freeing unused kernel memory: 1568k freed
...

 

3. 物理內存的管理

在內核初始化完成後,內存管理的責任由夥伴系統承擔,夥伴系統基於一種相對簡單卻十分強大高效的算法,它結合優秀內存分配器的兩個關鍵特徵: 速度和效率

0x1: 夥伴系統的結構

struct zone
{
    ..
    //不一樣長度的空閒區域
    struct free_area    free_area[MAX_ORDER];
    ..
}

相關數據結構,請參閱另外一篇文章

http://www.cnblogs.com/LittleHann/p/3865490.html
//搜索:0x4: struct zone

struct free_area是一個輔助數據結構
\linux-2.6.32.63\include\linux\mmzone.h

struct free_area 
{
    //用於鏈接空閒頁的鏈表,頁鏈表包含大小相同的連續內存區,其中又定義了多個(MIGRATE_TYPES 個)頁鏈表
    struct list_head    free_list[MIGRATE_TYPES];

    /*
    指定了當前內存區中空閒頁塊的數目
    1. 對0階內存區逐頁計算
    2. 對1階內存區計算頁對的數目
    3. 對2階內存區計算4頁集合的數目
    ..依次類推
    */
    unsigned long        nr_free;
};

階是夥伴系統中一個很是重要的概念,它描述了內存分配的數量單位,內存塊的長度是2(order)次方,其中order的範圍: 0 ~ MAX_ORDER
\linux-2.6.32.63\include\linux\mmzone.h

/* Free memory management - zoned buddy allocator.  */
#ifndef CONFIG_FORCE_MAX_ZONEORDER
#define MAX_ORDER 11
#else
#define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER
#endif
#define MAX_ORDER_NR_PAGES (1 << (MAX_ORDER - 1))

該常數一般設置爲11,這意味着一次分配能夠請求的頁數最大爲2(11)次方 = 2048

struct zone->struct free_area free_area[]數組中各個元素的索引也解釋爲階,用於指定對應鏈表中的連續內存區中包含多少個頁幀

1. 第0個鏈表包含的內存區爲單頁: 2(0)次方 = 1
2. 第1個鏈表管理的內存區爲兩頁: 2(1)次方 = 2
3. 第3個鏈表管理的內存區爲4頁
..

內存區中第一頁的鏈表元素,用於將內存區維持在鏈表中

夥伴之間(指單頁一組、兩頁一組、4頁一組中的每一個內存塊)沒必要是彼此鏈接的,若是一個內存區在分配期間分解成兩半,內核會自動將"未用"的一半加入到對應的鏈表中(加入大小對應的,對應階的鏈表中)。若是在將來的某個時刻,因爲內存釋放等緣故,兩個內存區都處於空閒狀態,可經過其地址判斷其是否爲夥伴,管理成本十分低廉,是夥伴系統的一個主要優勢
基於夥伴系統的內存管理專一於某個結點的某個內存域,例如DMA或高端內存域,但全部內存域和結點的夥伴系統都經過"備用分配列表"鏈接起來

在首選的內存域(或結點)沒法知足內存分配請求時,首先嚐試同一結點的另外一個內存域(從廉價到昂貴),接下來再嘗試另外一個結點,直至知足請求
有關夥伴系統當前狀態信息能夠在/proc/buddyinfo中得到

Node 0, zone      DMA      0      4      3      2      1      1      0      0      1      1      3 
Node 0, zone    DMA32    475    196     63     49     25     28      9      3      5      4    153 
//給出了各個內存域中每一個分配階中空閒項的數目,從左至右,階依次升高

0x2: 避免碎片

1. 根據可移動性組織頁

夥伴系統的性能很是高效,但在Linux內存管理方面,有一個長期存在的問題,在系統啓動並長期運行後,物理內存會產生不少碎片

1. 左側的地址空間中散佈着空閒頁,儘管大約25%的物理內存仍然未分配,但最大的連續空閒頁只有一頁,這對用戶空間程序沒有問題,由於用戶空間的內存是經過頁表映射的,不管空閒頁在物理內存中的分佈如何,應用程序看到的內存都是"連續"2. 右圖給出的情形中,空閒頁和使用頁的數目與左圖相同,但全部空閒頁都位於一個連續內存中

可是對於內核來講,碎片是一個大問題,對於內核映射來講,因爲(大多數)物理內存須要一致映射到地址空間的內核部分,那麼在左圖的場景中,沒法映射比一頁更大的內存區,儘管許多時候都分配的是比較小的內存,但也有時候須要分配多於一頁的內存

須要明白的是,文件系統也有碎片,該領域的碎片問題主要經過碎片合併工具解決,它們分析文件系統,從新排序已分配存儲塊,從而創建較大的連續存儲區。理論上,該方法對物理內存也是可能的,但因爲許多物理內存頁不能移動到任意位置,阻礙了該方法的實施
所以,內核的策略方法是"反碎片(anti-憤然哥們他提on)",即試圖從最初開始儘量防止碎片。在學習反碎片策略的工做原理,咱們須要明白內核將已分配頁劃分爲下面3種不一樣類型

1. 不可移動頁
在內存中有固定位置,不能移動到其餘地方,核心內核分配的大多數內存屬於該類別

2. 可回收頁
不能直接移動,但能夠刪除,其內容能夠從某些源從新生成,例如映射自文件的數據屬於該類別
kswapd守護進程會根據可回收頁訪問的頻繁程度,週期性釋放此類內存,這是一個複雜的過程,屬於頁面回收的範疇,簡單來講內核會在可回收頁佔據了太多內存時進行回收
另外,在內存短缺(即分配失敗)時也能夠發起頁面回收

3. 可移動頁
能夠隨意移動,屬於用戶空間應用程序的頁屬於該類別,它們是經過頁表映射的,若是它們複製到新位置,頁表項能夠相應地更新,應用程序不會注意到任何區別

頁的可移動性,依賴該頁屬於3種類別的哪種,內核使用反碎片技術,即基於將具備相同可移動性的頁分組的思想,根據頁的可移動性,將其分配到不一樣的列表中,這樣,不可移動頁不能位於可移動內存區的中間,避免了碎片的發生(這是一種聚類的思想)
須要注意的是,從最初開始,內存並未劃分爲可移動性不一樣的區,這些是在運行時造成的

數據結構

儘管內核使用的反碎片技術頗有效果,但它對夥伴系統的代碼和數據結構幾乎沒有影響,內核定義了一些宏來表示不一樣的遷移類型
\linux-2.6.32.63\include\linux\mmzone.h

#define MIGRATE_UNMOVABLE     0        //
#define MIGRATE_RECLAIMABLE   1        //
#define MIGRATE_MOVABLE       2
#define MIGRATE_PCPTYPES      3        /* the number of types on the pcp lists */
#define MIGRATE_RESERVE       3        // 若是向具備特定可移動特性的列表請求分配內存失敗,這種緊急情能夠從MIGRATE_RESERVE分配內存
#define MIGRATE_ISOLATE       4        /* can't allocate from here MIGRATE_ISOLATE是一個特殊的虛擬區域,用於跨越NUMA結點移動物理內存頁 */
#define MIGRATE_TYPES         5        // MIGRATE_TYPES用於表示遷移類型的數目,不表明具體的區域

對夥伴系統數據結構的主要調整,是將空閒列表分解爲MIGRATE_TYPES個列表

struct free_area 
{
    //用於鏈接空閒頁的鏈表,頁鏈表包含大小相同的連續內存區,其中又定義了多個(MIGRATE_TYPES 個)頁鏈表
    struct list_head    free_list[MIGRATE_TYPES];

    /*
    指定了當前內存區中空閒頁塊的數目
    1. 對0階內存區逐頁計算
    2. 對1階內存區計算頁對的數目
    3. 對2階內存區計算4頁集合的數目
    ..依次類推
    */
    unsigned long        nr_free;
};

若是內核沒法知足針對某一給定遷移類型的分配請求,內核在這種狀況下會提供一個備用列表,規定了在指定列表中沒法知足分配請求時,接下來使用哪種遷移類型
\linux-2.6.32.63\mm\page_alloc.c

/*
 * This array describes the order lists are fallen back to when the free lists for the desirable migrate type are depleted
 */
static int fallbacks[MIGRATE_TYPES][MIGRATE_TYPES-1] = 
{
    [MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE,   MIGRATE_RESERVE },
    [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,   MIGRATE_RESERVE },
    [MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE },
    [MIGRATE_RESERVE]     = { MIGRATE_RESERVE,     MIGRATE_RESERVE,   MIGRATE_RESERVE }, /* Never used */
};

全局變量和輔助函數

儘管頁可移動性分組特性老是編譯到內核中,但只有在系統中有足夠內存能夠分配到多個遷移類型對應的鏈表時,纔是有意義的。因爲每一個遷移鏈表都應該有適當數量的內存,內核須要定義"適當"的概念,這是經過兩個全局變量提供的

1. pageblock_order: 表示內核認爲是""的一個分配階
2. page_nr_pages: 表示該分配階對應的頁數

若是體系結構提供了巨型頁機制,則pageblock_order一般定義爲巨型頁對應的分配階

1. 在IA-32體系結構上,巨型頁長度是4MB,所以每一個巨型頁由1024個普通頁組成,而HUGETLB_PAGE_ORDER則定義爲10
2. IA-64體系結構容許設置可變的普通和巨型頁長度,所以HUGETLB_PAGE_ORDER的值取決於內核配置
3. 若是體系結構不支持巨型頁,則將其定義爲第二高的分配階

\linux-2.6.32.63\include\linux\pageblock-flags.h

#ifdef CONFIG_HUGETLB_PAGE
#ifdef CONFIG_HUGETLB_PAGE_SIZE_VARIABLE
    /* Huge page sizes are variable */
    extern int pageblock_order;
#else /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */
    /* Huge pages are a constant size */
    #define pageblock_order        HUGETLB_PAGE_ORDER
#endif /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */
#else /* CONFIG_HUGETLB_PAGE */
    /* If huge pages are not used, group by MAX_ORDER_NR_PAGES */
    #define pageblock_order        (MAX_ORDER-1)
#endif /* CONFIG_HUGETLB_PAGE */

內核提供了兩個標誌(分配掩碼),用於代表給定的分配內存屬於何種遷移類型

1. __GFP_MOVALE: 分配的內存是可移動的
2. __GFP_RECLAIMABLE: 分配的內存是可回收的
//若是這些標誌都沒有設置,則分配的內存假定爲不可移動的

在初始化期間,內核自動確保對內存域(struct zone)中的每一個不一樣的遷移類型分組,在各個遷移鏈表之間,當前的頁面分配狀態能夠從/proc/pagetypeinfo中得到

Page block order: 9
Pages per block:  512

Free pages count per migrate type at order       0      1      2      3      4      5      6      7      8      9     10 
Node    0, zone      DMA, type    Unmovable      0      4      3      2      1      1      0      0      1      0      0 
Node    0, zone      DMA, type  Reclaimable      0      0      0      0      0      0      0      0      0      0      0 
Node    0, zone      DMA, type      Movable      0      0      0      0      0      0      0      0      0      0      3 
Node    0, zone      DMA, type      Reserve      0      0      0      0      0      0      0      0      0      1      0 
Node    0, zone      DMA, type      Isolate      0      0      0      0      0      0      0      0      0      0      0 
Node    0, zone    DMA32, type    Unmovable    100     92     31     21     14      5      2      0      1      1      0 
Node    0, zone    DMA32, type  Reclaimable      0      1      0      0      1      1      0      1      1      1      0 
Node    0, zone    DMA32, type      Movable      4      4      3      1      4     19      2      3      1      4    152 
Node    0, zone    DMA32, type      Reserve      1      0      1      1      1      1      1      1      1      1      0 
Node    0, zone    DMA32, type      Isolate      0      0      0      0      0      0      0      0      0      0      0 

Number of blocks type     Unmovable  Reclaimable      Movable      Reserve      Isolate 
Node 0, zone      DMA            1            0            6            1            0 
Node 0, zone    DMA32           74           42          898            2            0 

初始化基於可移動性的分組

在內核子系統初始化期間,memmap_init_zone負責處理內存域的page實例,該函數將全部的頁最初都標記爲可移動的。總而言之,這種作法避免了啓動期間內核分配的內存(常常在系統的整個運行時間都不釋放)散佈到物理內存的各處,從而使其餘類型的內存分配免受碎片的干擾,這也是頁可移動性分組框架的最重要的目標之一

2. 虛擬可移動內存域

根據可移動性組織頁是防止物理內存碎片的一種可能方法,內核還提供了另外一種組織該問題的手段,即虛擬內存域ZONE_MOVABLE,與可移動性分組相反,ZONE_MOVABLE特性必須由管理員顯式激活。基本思想很簡單,可用的物理內存劃分爲兩個內存域: 一個用於可移動分配、另外一個用於不可移動分配。這會自動防止不可移動頁向可移動內存域引入碎片(而且這個碎片仍是沒法清除的)
管理員必須指定用於不可移動分配的內存數量、以及可移動分配的內存數量 

0x3: 初始化內存域和結點數據結構

截至目前,咱們已經在特定於體系結構的代碼中看到了內核如何檢測系統中的可用內存。與高層數據結構(如內存域和結點)的關聯,則須要根據該信息構建,咱們知道,體系結構相關代碼須要在啓動期間創建如下信息

1. 系統中各個內存域的頁幀邊界,保存在max_zone_pfn數組
2. 各結點頁幀的分配狀況,保存在全局變量early_node_map中

1. 管理數據結構的建立

從內核版本2.6.10開始提供了一個通用框架,用於將上述信息轉換爲夥伴系統預期的結點和內存域數據結構,setup_arch->zone_sizes_init->free_area_init_nodes完成主要的工做

在創建結點和內存域內存管理數據結構時,特定於體系結構的代碼和通用內核代碼之間的相關做用

\linux-3.15.5\mm\page_alloc.c

/**
 * free_area_init_nodes - Initialise all pg_data_t and zone data
 * @max_zone_pfn: an array of max PFNs for each zone
 *
 * This will call free_area_init_node() for each active node in the system.
 * Using the page ranges provided by add_active_range(), the size of each
 * zone in each node and their holes is calculated. If the maximum PFN
 * between two adjacent zones match, it is assumed that the zone is empty.
 * For example, if arch_max_dma_pfn == arch_max_dma32_pfn, it is assumed
 * that arch_max_dma32_pfn has no pages. It is also assumed that a zone
 * starts where the previous one ended. For example, ZONE_DMA32 starts
 * at arch_max_dma_pfn.
 */
void __init free_area_init_nodes(unsigned long *max_zone_pfn)
{
    unsigned long nid;
    int i;

    /* 
    Sort early_node_map as initialisation assumes it is sorted 
    根據結點的第一個頁幀start_pfn,對early_node_map中的各項進行排序
    */
    sort_node_map();

    /* 
    Record where the zone boundaries are 
    計算各個內存域可以使用的最低和最高的頁幀編號
    */
    memset(arch_zone_lowest_possible_pfn, 0, sizeof(arch_zone_lowest_possible_pfn));
    memset(arch_zone_highest_possible_pfn, 0, sizeof(arch_zone_highest_possible_pfn));
    //輔助函數find_min_pfn_with_active_regions用於找到註冊的最低內存域中可用的編號最小的頁幀,該內存域不必定是ZONE_DMA
    arch_zone_lowest_possible_pfn[0] = find_min_pfn_with_active_regions();
    arch_zone_highest_possible_pfn[0] = max_zone_pfn[0];
    //接下來構建其餘內存域的頁幀區間,方法很直接,第n個內存域的最小頁幀 = 第n-1個內存域的最大頁幀
    for (i = 1; i < MAX_NR_ZONES; i++) 
    {
        if (i == ZONE_MOVABLE)
            continue;
        arch_zone_lowest_possible_pfn[i] = arch_zone_highest_possible_pfn[i-1];
        arch_zone_highest_possible_pfn[i] = max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]);
    }
    //由於ZONE_MOVABLE是一個虛擬內存域,不與真正的硬件內存域相關聯,該內存域的邊界老是設置爲0
    arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0;
    arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0;

    /* Find the PFNs that ZONE_MOVABLE begins at in each node */
    memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn));
    find_zone_movable_pfns_for_nodes(zone_movable_pfn);

    /* Print out the zone ranges */
    printk("Zone PFN ranges:\n");
    for (i = 0; i < MAX_NR_ZONES; i++) 
    {
        if (i == ZONE_MOVABLE)
            continue;
        printk("  %-8s %0#10lx -> %0#10lx\n", zone_names[i], arch_zone_lowest_possible_pfn[i], arch_zone_highest_possible_pfn[i]);
    }

    /* Print out the PFNs ZONE_MOVABLE begins at in each node */
    printk("Movable zone start PFN for each node\n");
    for (i = 0; i < MAX_NUMNODES; i++) 
    {
        if (zone_movable_pfn[i])
            printk("  Node %d: %lu\n", i, zone_movable_pfn[i]);
    }

    /* Print out the early_node_map[] */
    printk("early_node_map[%d] active PFN ranges\n", nr_nodemap_entries);
    for (i = 0; i < nr_nodemap_entries; i++)
        printk("  %3d: %0#10lx -> %0#10lx\n", early_node_map[i].nid, early_node_map[i].start_pfn, early_node_map[i].end_pfn);

    /* Initialise every node */
    mminit_verify_pageflags_layout();
    setup_nr_node_ids();
    for_each_online_node(nid) 
    {
        pg_data_t *pgdat = NODE_DATA(nid);
        free_area_init_node(nid, NULL, find_min_pfn_for_node(nid), NULL);

        /* Any memory on that node */
        if (pgdat->node_present_pages)
            node_set_state(nid, N_HIGH_MEMORY);
        check_for_regular_memory(pgdat);
    }
}

2. 對各個結點建立數據結構

\linux-3.15.5\mm\page_alloc.c

在內存域邊界已經肯定後,free_area_init_nodes分別對各個內存域調用free_area_init_node建立數據結構

void __paginginit free_area_init_node(int nid, unsigned long *zones_size, unsigned long node_start_pfn, unsigned long *zholes_size)
{
    pg_data_t *pgdat = NODE_DATA(nid);

    pgdat->node_id = nid;
    pgdat->node_start_pfn = node_start_pfn;
    /*
    累積各個內存域的頁數,計算結點中頁的生總數,對連續內存模型而言,這能夠經過zones_size_init完成,但calculate_node_totalpages還考慮了內存域中的空洞
    在系統啓動初始化期間,內核會輸出一段簡短的消息
    dmesg
    On node 0 totalpages: 524142
    */
    calculate_node_totalpages(pgdat, zones_size, zholes_size);

    /*
    alloc_node_mem_map負責初始化一個簡單但很是重要的數據結構,即系統中的各個物理內存頁,都對應着一個strcut page實例,該結構的初始化由alloc_node_mem_map執行
    */
    alloc_node_mem_map(pgdat);
#ifdef CONFIG_FLAT_NODE_MEM_MAP
    printk(KERN_DEBUG "free_area_init_node: node %d, pgdat %08lx, node_mem_map %08lx\n",
        nid, (unsigned long)pgdat,
        (unsigned long)pgdat->node_mem_map);
#endif

    //初始化內存域數據結構涉及的繁重工做由free_area_init_core執行,它會依次遍歷結點的全部內存域
    free_area_init_core(pgdat, zones_size, zholes_size);
}

0x4: 分配器API

就夥伴系統的接口而言,NUMA或UMA體系結構是沒有差異的,兩者的調用語法都是相同的,全部函數的共同點是: 只能分配2的整數冪個頁,所以,接口中不像C標準庫的malloc函數或bootmem分配器那樣任意指定了所需內存大小做爲參數,相反,必須指定的是分配階,夥伴系統將在內存中分配2(order)次方頁。內核中細粒度的分配只能藉助SLAB分配器(或者SLUB、SLOB分配器),它們都基於夥伴系統

1. alloc_pages(mask, order)
分配2(order)次方頁並返回一個struct page的實例,表示分配的內存塊的起始頁

2. get_zeroed_page(mask)
分配一頁並返回一個struct page實例,頁對應的內存填充0(全部其餘函數分配以後頁的內容是未定義的)

3. __get_free_pages(mask, order)
返回分配內存塊的虛擬地址

4. get_dma_pages(gfp_mask, order)
用來得到適用於DMA的頁
/*
在空閒內存沒法知足請求以致於分配失敗的狀況下,全部上述函數都返回空指針(alloc_page)、或者0(get_zeroed_page、__get_free_pages、get_dma_pages)
所以內核在每次分配以後都必須檢查返回的結果,這種慣例與設計良好的用戶層應用程序相似,但在內核中忽略檢查會致使嚴重得多的故障
*/

內核除了夥伴系統函數以外,還提供了其餘內存管理函數,它們以夥伴系統爲基礎,但並不屬於夥伴分配器自身,這些函數包括

1. vmalloc
2. vmalloc_32
//使用頁表將不連續的內存映射到內核地址空間中,使之看上去是連續的

3. kmalloc類型的函數
//用於分配小於一整頁的內存區

有4個函數用於釋放再也不使用的頁

1. free_page(struct page *)
2. free_pages(struct page *, order);
//用於將一個2(order)次方頁返回(釋放)給內存管理子系統,內存區的起始地址由指向該內存區的第一個page實例的的指針表示

3. __free_page(addr)
4. __free_pages(addr, order);
//語義相似於一、2函數,但在表示須要釋放的內存區時,使用了虛擬內存地址而不是page實例

1. 分配掩碼

夥伴系統提供的內存分配接口函數中強制使用的mask參數

內存域修飾符

Linux將內存劃分爲內存域,內核提供了所謂的"內存域修飾符(zone modifier)",在掩碼的最低4個比特位定義來指定從哪一個內存域中分配所需的頁
\linux-2.6.32.63\include\linux\gfp.h

/*
 * GFP bitmasks..
 *
 * Zone modifiers (see linux/mmzone.h - low three bits)
 *
 * Do not put any conditional on these. If necessary modify the definitions without the underscores and use the consistently. The definitions here may be used in bit comparisons.
*/
#define __GFP_DMA    ((__force gfp_t)0x01u)
#define __GFP_HIGHMEM    ((__force gfp_t)0x02u)
#define __GFP_DMA32    ((__force gfp_t)0x04u)
#define GFP_ZONEMASK    (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)

縮寫GFP表明得到空閒頁(get free page),__GFP_MOVABLE不表明物理內存頁,但通知內核應該在特殊的虛擬內存域ZONE_MOVABLE進行相應的分配。值得注意的是,沒有__GFP_NORMAL常數,但內存分配的主要負擔卻落到ZONE_NORMAL內存域,內核考慮到這一點,提供了gfp_zone()函數來計算與給定分配標誌"兼容"的最高(最廉價)內存域,那麼內存分配能夠從該內存域或更低(從最廉價逐漸到最昂貴)的內存域進行
\linux-2.6.32.63\include\linux\gfp.h

/*
1. 若是__GFP_DMA、__GFP_HIGHMEM都沒有設置,則首先掃描ZONE_NORMAL,而後是ZONE_DMA
2. 若是設置了__GFP_HIGHMEM,沒有設置__GFP_DMA,則結果是從__GFP_HIGHMEM開始掃描全部3個內存域
3. 若是設置了__GFP_DMA,那麼無論__GFP_HIGHMEM設置與否都從__GFP_DMA開始掃描

總之遵循的一個基本原則是: 儘可能遵循掩碼參數的指示,同時儘可能從最廉價的內存域開始分配
*/
static inline enum zone_type gfp_zone(gfp_t flags)
{
    enum zone_type z;
    int bit = flags & GFP_ZONEMASK;

    z = (GFP_ZONE_TABLE >> (bit * ZONES_SHIFT)) & ((1 << ZONES_SHIFT) - 1);

    if (__builtin_constant_p(bit))
        MAYBE_BUILD_BUG_ON((GFP_ZONE_BAD >> bit) & 1);
    else {
#ifdef CONFIG_DEBUG_VM
        BUG_ON((GFP_ZONE_BAD >> bit) & 1);
#endif
    }
    return z;
}

操做修飾符

/*
 * Action modifiers - doesn't change the zoning
 */
 /* 
 Can wait and reschedule 能夠等待和重調度 
 __GFP_WAIT表示分配內存的請求能夠中斷,即調度器在該請求期間能夠隨意選擇另外一個進程執行,或者該請求能夠被另外一個更重要的事件中斷
 分配器還能夠在返回內存以前,在隊列上等待一個事件,相關進程會進入睡眠狀態
 */
#define __GFP_WAIT    ((__force gfp_t)0x10u)    

/* 
Should access emergency pools 應該訪問緊急分配池 
若是請求很是重要,則設置__GFP_HIGH,即內核急切地須要內存時,在分配內存失敗可能給內核帶來嚴重後果時(例如威脅到系統穩定性或系統崩潰),老是會使用該標誌
*/
#define __GFP_HIGH    ((__force gfp_t)0x20u)    

/* 
Can start physical IO 能夠啓動物理IO 
__GFP_IO說明在查找空閒內存期間內核能夠進行I/O操做,實際上,這意味着若是內核在內存分配期間換出頁,那麼僅當設置該標誌時,才能將選擇的頁寫入硬盤
*/
#define __GFP_IO    ((__force gfp_t)0x40u)

/* 
Can call down to low-level FS 能夠調用底層文件系統 
__GFP_FS容許內核執行VFS操做,在與VFS層有聯繫的內核子系統中必須禁用,由於這可能引發循環遞歸調用
*/
#define __GFP_FS    ((__force gfp_t)0x80u)            

#define __GFP_COLD    ((__force gfp_t)0x100u)            /* Cache-cold page required 須要分緩存的冷頁(即不在CPU高速緩存中的"冷頁") */
#define __GFP_NOWARN    ((__force gfp_t)0x200u)        /* Suppress page allocation failure warning 禁止分配失敗告警*/
#define __GFP_REPEAT    ((__force gfp_t)0x400u)        /* Try hard to allocate the memory, but the allocation attempt _might_ fail.  This depends upon the particular VM implementation 重試分配,但在重試若干次後會中止,可能失敗 */
#define __GFP_NOFAIL    ((__force gfp_t)0x800u)        /* The VM implementation _must_ retry infinitely: the caller cannot handle allocation failures 一直重試,不會失敗*/
#define __GFP_NORETRY    ((__force gfp_t)0x1000u)    /* The VM implementation must not retry indefinitely 不重試,可能失敗 */
#define __GFP_COMP    ((__force gfp_t)0x4000u)        /* Add compound page metadata 增長複合頁元數據 */
#define __GFP_ZERO    ((__force gfp_t)0x8000u)        /* Return zeroed page on success 成功則返回填充字節0的頁 */
#define __GFP_NOMEMALLOC ((__force gfp_t)0x10000u)     /* Don't use emergency reserves 不使用緊急分配鏈表 */

/* 
Enforce hardwall cpuset memory allocs 只容許在進程容許運行的CPU所關聯的結點分配內存
__GFP_HARDWALL只在NUMA系統上有意義,它限制只在分配到當前進程的各個CPU所關聯的結點分配內存,若是進程容許在全部CPU上運行(默認狀況),該標誌是無心義的,只有進程能夠運行的CPU受限時,該標誌纔有效果
*/
#define __GFP_HARDWALL   ((__force gfp_t)0x20000u)     

/* 
No fallback, no policies 沒有備用結點,沒有策略
__GFP_THISNODE只有在NUMA系統上纔有意義,若是設置該比特位,則內存分配失敗的狀況下不容許使用其餘結點做爲備用,須要保證在當前結點或者明確指定的結點上成功分配內存
*/
#define __GFP_THISNODE    ((__force gfp_t)0x40000u)    
#define __GFP_RECLAIMABLE ((__force gfp_t)0x80000u) /* Page is reclaimable 頁是可回收的*/
#define __GFP_MOVABLE    ((__force gfp_t)0x08u)      /*  Flag that this page will be movable by the page migration mechanism or reclaimed 頁是可移動的 */

#ifdef CONFIG_KMEMCHECK
#define __GFP_NOTRACK    ((__force gfp_t)0x200000u)  /* Don't track with kmemcheck */
#else
#define __GFP_NOTRACK    ((__force gfp_t)0)
#endif

因爲這些標誌幾乎老是組合使用,內核對此做了一些分組,包含了用於各類標準情形的適當的標誌

/* This equals 0, but use constants in case they ever change */
#define GFP_NOWAIT    (GFP_ATOMIC & ~__GFP_HIGH)
/* 
GFP_ATOMIC means both !wait (__GFP_WAIT not set) and use emergency pool 
GFP_ATOMIC用於原子分配,在任何狀況下都不能中斷,可能使用緊急分配鏈表中的內存
*/
#define GFP_ATOMIC    (__GFP_HIGH)

//GFP_NOIO明確禁止I/O操做,但可能被中斷
#define GFP_NOIO    (__GFP_WAIT)
//GFP_NOIO明確禁止I/O操做、以及訪問VFS層,但可能被中斷
#define GFP_NOFS    (__GFP_WAIT | __GFP_IO)

//GFP_KERNEL是內核的默認配置,它的失敗不會當即威脅系統穩定性,這是內核源代碼中最常使用的標誌
#define GFP_KERNEL    (__GFP_WAIT | __GFP_IO | __GFP_FS)
#define GFP_TEMPORARY    (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_RECLAIMABLE)
#define GFP_USER    (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL)

/*
GFP_HIGHUSER是GFP_USER的一個擴展,也用於用戶空間,它容許分配沒法直接映射的高端內存
使用高端內存是沒有壞處的,由於用戶進程的地址空間老是經過非線性頁表組織的
*/
#define GFP_HIGHUSER    (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL |  __GFP_HIGHMEM)
#define GFP_HIGHUSER_MOVABLE    (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL | __GFP_HIGHMEM | __GFP_MOVABLE)

#ifdef CONFIG_NUMA
#define GFP_THISNODE    (__GFP_THISNODE | __GFP_NOWARN | __GFP_NORETRY)
#else
#define GFP_THISNODE    ((__force gfp_t)0)
#endif

/* This mask makes up all the page movable related flags */
#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE)

/* Control page allocator reclaim behavior */
#define GFP_RECLAIM_MASK (__GFP_WAIT|__GFP_HIGH|__GFP_IO|__GFP_FS| __GFP_NOWARN|__GFP_REPEAT|__GFP_NOFAIL| __GFP_NORETRY|__GFP_NOMEMALLOC)

/* Control slab gfp mask during early boot */
#define GFP_BOOT_MASK __GFP_BITS_MASK & ~(__GFP_WAIT|__GFP_IO|__GFP_FS)

/* Control allocation constraints */
#define GFP_CONSTRAINT_MASK (__GFP_HARDWALL|__GFP_THISNODE)

/* Do not use these with a slab allocator */
#define GFP_SLAB_BUG_MASK (__GFP_DMA32|__GFP_HIGHMEM|~__GFP_BITS_MASK)

/* Flag - indicates that the buffer will be suitable for DMA.  Ignored on some
   platforms, used as appropriate on others */

#define GFP_DMA        __GFP_DMA

/* 4GB DMA on some platforms */
#define GFP_DMA32    __GFP_DMA32

2. 內存分配宏

經過使用分配掩碼和各個分配函數,內核提供了一個很是靈活的內存分配體系,儘管如此,全部接口函數均可以追溯到一個簡單的基本函數: alloc_pages_node

1. alloc_page
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
//分配0階的內存頁,即1頁

2. alloc_pages
#define alloc_pages(gfp_mask, order) alloc_pages_node(numa_node_id(), gfp_mask, order)

3. __get_free_page
#define __get_free_page(gfp_mask) __get_free_pages((gfp_mask),0)
//分配0階的內存頁,即1頁

4. __get_free_pages
//
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
    struct page *page;

    /*
     * __get_free_pages() returns a 32-bit address, which cannot represent a highmem page
    */
    VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0);

    page = alloc_pages(gfp_mask, order);
    if (!page)
        return 0;
    return (unsigned long) page_address(page);
}
EXPORT_SYMBOL(__get_free_pages);

5. __get_dma_pages
#define __get_dma_pages(gfp_mask, order) __get_free_pages((gfp_mask) | GFP_DMA,(order))

6. get_zeroed_page
unsigned long get_zeroed_page(gfp_t gfp_mask)
{
    return __get_free_pages(gfp_mask | __GFP_ZERO, 0);
}
EXPORT_SYMBOL(get_zeroed_page);

這樣,就完成了全部API函數到公共基礎函數alloc_pages的統一

相似的,內存釋放函數也能夠規約到一個主要的函數: __free_pages,只是用不一樣的參數調用

1. __free_pages
void __free_pages(struct page *page, unsigned int order)
{
    if (put_page_testzero(page)) 
    {
        trace_mm_page_free_direct(page, order);
        if (order == 0)
            free_hot_page(page);
        else
            __free_pages_ok(page, order);
    }
}

2. __free_page
#define __free_page(page) __free_pages((page), 0)

3. free_page
#define free_page(addr) free_pages((addr),0)

4. free_pages
void free_pages(unsigned long addr, unsigned int order)
{
    if (addr != 0) {
        VM_BUG_ON(!virt_addr_valid((void *)addr));
        //virt_to_page將虛擬內存底子好轉換爲指向page實例的指針
        __free_pages(virt_to_page((void *)addr), order);
    }
}
EXPORT_SYMBOL(free_pages);
//free_pages和__free_pages之間的關係經過函數而不是宏創建,由於首先必須將虛擬地址轉換爲指向struct page的指針

0x5: 分配頁

咱們知道,全部API函數都追溯到alloc_pages_node,該函數是夥伴系統主要實現的"發射臺"
\linux-2.6.32.63\include\linux\gfp.h

static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order)
{
    /* 
    Unknown node is current node 
    若是指定負的結點ID(不存在),內核自動地使用當前執行CPU對應的結點ID
    */
    if (nid < 0)
        nid = numa_node_id();

    //node_zonelist用於選擇分配內存的內存域
    return __alloc_pages(gfp_mask, order, node_zonelist(nid, gfp_mask));
}

在內核源代碼中,__alloc_pages被稱之爲"夥伴系統的心臟",它處理的是實質性的內存分配

1. 選擇頁

輔助函數

首先咱們須要定義一些函數使用的標誌,用於控制到達各個水印指定的臨界狀態時的行爲
\linux-2.6.32.63\mm\page_alloc.c

/* The ALLOC_WMARK bits are used as an index to zone->watermark */
#define ALLOC_WMARK_MIN        WMARK_MIN     /* 使用pages_min水印 */
#define ALLOC_WMARK_LOW        WMARK_LOW     /* 使用pages_low水印 */
#define ALLOC_WMARK_HIGH    WMARK_HIGH     /* 使用pages_high水印 */
#define ALLOC_NO_WATERMARKS    0x04         /* don't check watermarks at all 徹底不檢查水印 */

/* Mask to get the watermark bits */
#define ALLOC_WMARK_MASK    (ALLOC_NO_WATERMARKS-1)

#define ALLOC_HARDER        0x10         /* try to alloc harder 試圖更努力地分配,即放寬限制 */
#define ALLOC_HIGH             0x20         /* __GFP_HIGH set 設置了__GFP_HIGH */
#define ALLOC_CPUSET        0x40         /* check for correct cpuset 檢查內存結點是否對應着指定的CPU集合 */

設置的標誌在zone_watermark_ok函數中檢查,該函數根據設置的標誌判斷是否能從給定的內存域中分配內存
\linux-2.6.32.63\mm\page_alloc.c

/*
 * Return 1 if free pages are above 'mark'. This takes into account the order of the allocation.
*/
int zone_watermark_ok(struct zone *z, int order, unsigned long mark, int classzone_idx, int alloc_flags)
{
    /* free_pages my go negative - that's OK */
    long min = mark;
    long free_pages = zone_nr_free_pages(z) - (1 << order) + 1;
    int o;

    //在解釋了ALLOC_HIGH、ALLOC_HARDER標誌以後(將最小值標記下降到當前值的一半或四分之一,使得分配過程更加努力,即放寬限制)
    if (alloc_flags & ALLOC_HIGH)
        min -= min / 2;
    if (alloc_flags & ALLOC_HARDER)
        min -= min / 4;

    //檢查空閒頁的數目是否小於最小值 + lowmem_reserve中指定的緊急分配值之和
    if (free_pages <= min + z->lowmem_reserve[classzone_idx])
        return 0;
    //若是不小於,則遍歷全部小於當前階的分配階,從free_pages減去當前分配階的全部空閒頁(左移o位是必要的,由於nr_free記載的是當前分配階的空閒頁塊數目)
    for (o = 0; o < order; o++) 
    {
        /* 
        At the next order, this order's pages become unavailable 
        在下一階,當前階的頁是不可用的
        */
        free_pages -= z->free_area[o].nr_free << o;

        /* 
        Require fewer higher order pages to be free 所需高階空閒頁的數目相對較少 
        每升高一階,所需空閒頁的最小值折半
        */
        min >>= 1;

        //若是內核遍歷全部的低端內存域以後,發現內存不足,則不進行內存分配
        if (free_pages <= min)
            return 0;
    }
    return 1;
}

get_page_from_freelist是夥伴系統使用的另外一個重要的輔助函數,它經過標誌集和分配階來判斷是否能進行分配,若是能夠,則發起實際的分配操做

/*
 * get_page_from_freelist goes through the zonelist trying to allocate a page.
*/
static struct page *get_page_from_freelist(gfp_t gfp_mask, nodemask_t *nodemask, unsigned int order, struct zonelist *zonelist, int high_zoneidx, int alloc_flags, struct zone *preferred_zone, int migratetype)
{
    struct zoneref *z;
    struct page *page = NULL;
    int classzone_idx;
    struct zone *zone;
    nodemask_t *allowednodes = NULL;/* zonelist_cache approximation */
    int zlc_active = 0;        /* set if using zonelist_cache */
    int did_zlc_setup = 0;        /* just call zlc_setup() one time */

    classzone_idx = zone_idx(preferred_zone);
zonelist_scan:
    /*
     * Scan zonelist, looking for a zone with enough free.
     * See also cpuset_zone_allowed() comment in kernel/cpuset.c.
     */
    for_each_zone_zonelist_nodemask(zone, z, zonelist, high_zoneidx, nodemask)
    {
        if (NUMA_BUILD && zlc_active && !zlc_zone_worth_trying(zonelist, z, allowednodes))
                continue;
        //cpuset_zone_allowed_softwall用於檢查給定內存域是否屬於該進程容許運行的CPU
        if ((alloc_flags & ALLOC_CPUSET) && !cpuset_zone_allowed_softwall(zone, gfp_mask))
                goto try_next_zone;

        BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK);
        if (!(alloc_flags & ALLOC_NO_WATERMARKS)) 
        {
            unsigned long mark;
            int ret;

            mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];
            //zone_watermark_ok接下來檢查所遍歷到內存域是否有足夠的空閒頁,並試圖分配一個連續的內存塊(足夠空閒頁、連續內存二者必須同時知足纔可繼續,不然循環進行到備用列表中的寫一個內存域)
            if (zone_watermark_ok(zone, order, mark, classzone_idx, alloc_flags))
                goto try_this_zone;

            if (zone_reclaim_mode == 0)
                goto this_zone_full;

            ret = zone_reclaim(zone, gfp_mask, order);
            switch (ret) 
            {
            case ZONE_RECLAIM_NOSCAN:
                /* did not scan */
                goto try_next_zone;
            case ZONE_RECLAIM_FULL:
                /* scanned but unreclaimable */
                goto this_zone_full;
            default:
                /* did we reclaim enough */
                if (!zone_watermark_ok(zone, order, mark, classzone_idx, alloc_flags))
                    goto this_zone_full;
            }
        }

try_this_zone:
        //若是內存域適用於當前的分配請求,則調用buffered_rmqueue試圖從中分配所需數目的頁
        page = buffered_rmqueue(preferred_zone, zone, order, gfp_mask, migratetype);
        if (page)
            break;
this_zone_full:
        if (NUMA_BUILD)
            zlc_mark_zone_full(zonelist, z);
try_next_zone:
        if (NUMA_BUILD && !did_zlc_setup && nr_online_nodes > 1) {
            /*
             * we do zlc_setup after the first zone is tried but only
             * if there are multiple nodes make it worthwhile
             */
            allowednodes = zlc_setup(zonelist, alloc_flags);
            zlc_active = 1;
            did_zlc_setup = 1;
        }
    }

    if (unlikely(NUMA_BUILD && page == NULL && zlc_active)) {
        /* Disable zlc cache for second zonelist scan */
        zlc_active = 0;
        goto zonelist_scan;
    }
    return page;
}

分配控制

咱們知道,alloc_pages_node是內核夥伴系統內存分配最後規約的底層函數,而__alloc_pages又是其中夥伴系統的主函數,該函數的實現比較複雜,尤爲是在可用內存太少或逐漸用完時。若是可用內存足夠,則必要的工做會很快完成
\linux-2.6.32.63\include\linux\gfp.h

static inline struct page *__alloc_pages(gfp_t gfp_mask, unsigned int order, struct zonelist *zonelist)
{
    return __alloc_pages_nodemask(gfp_mask, order, zonelist, NULL);
}

\linux-2.6.32.63\mm\page_alloc.c

/*
 * This is the 'heart' of the zoned buddy allocator.
 */
struct page *__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, struct zonelist *zonelist, nodemask_t *nodemask)
{
    enum zone_type high_zoneidx = gfp_zone(gfp_mask);
    struct zone *preferred_zone;
    struct page *page;
    int migratetype = allocflags_to_migratetype(gfp_mask);

    gfp_mask &= gfp_allowed_mask;

    lockdep_trace_alloc(gfp_mask);

    might_sleep_if(gfp_mask & __GFP_WAIT);

    if (should_fail_alloc_page(gfp_mask, order))
        return NULL;

    /*
    Check the zones suitable for the gfp_mask contain at least one valid zone. 
    It's possible to have an empty zonelist as a result of GFP_THISNODE and a memoryless node
    適合於gfp_mask的內存域列表
    */
    if (unlikely(!zonelist->_zonerefs->zone))
        //若是沒有在內存的結點上使用GFP_THISNODE,致使zonelist爲空,就會發生這種狀況
        return NULL;

    /* The preferred zone is used for statistics later */
    first_zones_zonelist(zonelist, high_zoneidx, nodemask, &preferred_zone);
    if (!preferred_zone)
        return NULL;

    /* 
    First allocation attempt 
    在最簡單的情形中,分配空閒內存區只涉及調用一次get_page_from_freelist,而後返回所需數目的頁
    */
    page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, nodemask, order, zonelist, high_zoneidx, ALLOC_WMARK_LOW|ALLOC_CPUSET, preferred_zone, migratetype);
    if (unlikely(!page))
        //第一次內存分配嘗試不會特別積極,若是在某個內存域中沒法找到空閒內存,則意味着內存相對較緊張,內核須要更多的工做量才能找到更多內存
        page = __alloc_pages_slowpath(gfp_mask, order, zonelist, high_zoneidx, nodemask, preferred_zone, migratetype);

    trace_mm_page_alloc(page, order, gfp_mask, migratetype);
    return page;
}
EXPORT_SYMBOL(__alloc_pages_nodemask);

\linux-2.6.32.63\mm\page_alloc.c

static inline struct page *__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order, struct zonelist *zonelist, enum zone_type high_zoneidx, nodemask_t *nodemask, struct zone *preferred_zone, int migratetype)
{
    const gfp_t wait = gfp_mask & __GFP_WAIT;
    struct page *page = NULL;
    int alloc_flags;
    unsigned long pages_reclaimed = 0;
    unsigned long did_some_progress;
    struct task_struct *p = current;

    /*
     * In the slowpath, we sanity check order to avoid ever trying to
     * reclaim >= MAX_ORDER areas which will never succeed. Callers may
     * be using allocators in order of preference for an area that is
     * too large.
     */
    if (order >= MAX_ORDER) 
    {
        WARN_ON_ONCE(!(gfp_mask & __GFP_NOWARN));
        return NULL;
    }

    /*
     * GFP_THISNODE (meaning __GFP_THISNODE, __GFP_NORETRY and
     * __GFP_NOWARN set) should not cause reclaim since the subsystem
     * (f.e. slab) using GFP_THISNODE may choose to trigger reclaim
     * using a larger set of nodes after it has established that the
     * allowed per node queues are empty and that nodes are
     * over allocated.
     */
    if (NUMA_BUILD && (gfp_mask & GFP_THISNODE) == GFP_THISNODE)
        goto nopage;

restart:
    //喚醒負責換出頁的kswapd守護進程,空閒內存能夠經過縮減內核緩存和非熱點頁面回收來得到,即寫回或換出不多使用的頁,這兩種措施都是由該守護進程發起的
    wake_all_kswapd(order, zonelist, high_zoneidx);

    /*
    OK, we're below the kswapd watermark and have kicked background reclaim. Now things get more complex, so set up alloc_flags according to how we want to proceed.
    在交互守護進程喚醒後,內核開始新的嘗試,這一次進行的搜索更爲積極,對分配標誌進行了調整,修改成一些在當前特定狀況下更有可能分配成功的標誌
    同時,將水印下降到最小值,對實時進程和指定了__GFP_WAIT標誌由於不能睡眠的調用,會設置ALLOC_HARDER
    */
    alloc_flags = gfp_to_alloc_flags(gfp_mask);

rebalance:
    /* 
    This is the last chance, in general, before the goto nopage. 
    使用修改後的標誌集,再一次調用get_page_from_freelist,試圖得到所需的頁
    若是再次失敗,內核會藉助更強有力的措施
    */
    page = get_page_from_freelist(gfp_mask, nodemask, order, zonelist, high_zoneidx, alloc_flags & ~ALLOC_NO_WATERMARKS, preferred_zone, migratetype);
    if (page)
        goto got_pg;

    /* Allocate without watermarks if the context allows */
    if (alloc_flags & ALLOC_NO_WATERMARKS) 
    {
        page = __alloc_pages_high_priority(gfp_mask, order, zonelist, high_zoneidx, nodemask, preferred_zone, migratetype);
        if (page)
            goto got_pg;
    }

    /* Atomic allocations - we can't balance anything */
    if (!wait)
        goto nopage;

    /* Avoid recursion of direct reclaim */
    if (p->flags & PF_MEMALLOC)
        goto nopage;

    /* Avoid allocations with no watermarks from looping endlessly */
    if (test_thread_flag(TIF_MEMDIE) && !(gfp_mask & __GFP_NOFAIL))
        goto nopage;

    /* 
    Try direct reclaim and then allocating 
    嘗試回收頁面,並繼續分配
    */
    page = __alloc_pages_direct_reclaim(gfp_mask, order, zonelist, high_zoneidx, nodemask, alloc_flags, preferred_zone, migratetype, &did_some_progress);
    if (page)
        goto got_pg;

    /*
     * If we failed to make any progress reclaiming, then we are running out of options and have to consider going OOM
     */
    if (!did_some_progress) 
    {
        if ((gfp_mask & __GFP_FS) && !(gfp_mask & __GFP_NORETRY)) 
        {
            if (oom_killer_disabled)
                goto nopage;
            page = __alloc_pages_may_oom(gfp_mask, order, zonelist, high_zoneidx, nodemask, preferred_zone, migratetype);
            if (page)
                goto got_pg;

            /*
            The OOM killer does not trigger for high-order ~__GFP_NOFAIL allocations so if no progress is being made, 
            there are no other options and retrying is unlikely to help.
            */
            if (order > PAGE_ALLOC_COSTLY_ORDER &&
                        !(gfp_mask & __GFP_NOFAIL))
                goto nopage;

            goto restart;
        }
    }

    /* Check if we should retry the allocation */
    pages_reclaimed += did_some_progress;
    if (should_alloc_retry(gfp_mask, order, pages_reclaimed)) 
    {
        /*
        Wait for some write requests to complete then retry 
        等待塊設備層隊列釋放,這樣內核就有機會換出頁
        */
        congestion_wait(BLK_RW_ASYNC, HZ/50);
        goto rebalance;
    }

nopage:
    //不管如何都沒法分配內存了,只能打印OOM事件信息了
    if (!(gfp_mask & __GFP_NOWARN) && printk_ratelimit()) 
    {
        printk(KERN_WARNING "%s: page allocation failure. order:%d, mode:0x%x\n", p->comm, order, gfp_mask);
        dump_stack();
        show_mem();
    }
    return page;
got_pg:
    if (kmemcheck_enabled)
        kmemcheck_pagealloc_alloc(page, order, gfp_mask);
    return page;
}

2. 移除選擇的頁

若是內核找到適當的內存域,且有足夠的空閒頁可供分配,那麼還有兩件事須要完成

1. 必須檢查這些頁是不是"連續"的,由於到目前爲止,只知道有許多空閒頁
2. 必須按夥伴系統的方式從free_lists移除這些頁,這可能須要分解並重排內存區

內核將該工做委託給"get_page_from_freelist->buffered_rmqueue"完成

/*
Really, prep_compound_page() should be called from __rmqueue_bulk(). 
But we cheat by calling it from here, in the order > 0 path.  Saves a branch or two.
*/
static inline struct page *buffered_rmqueue(struct zone *preferred_zone, struct zone *zone, int order, gfp_t gfp_flags, int migratetype)
{
    unsigned long flags;
    struct page *page;
    //若是分配標誌設定了__GFP_COLD,那麼必須從per-CPU緩存取得冷頁
    int cold = !!(gfp_flags & __GFP_COLD);
    int cpu;

again:
    cpu  = get_cpu();
    //若是隻分配一頁,內核會進行優化,即分配階爲0 = 2(1)次方的狀況,該頁不是從夥伴系統直接取得,而是取自per-CPU的頁緩存
    if (likely(order == 0)) 
    {
        struct per_cpu_pages *pcp;
        struct list_head *list;

        //在只請求一頁時,內核試圖藉助於per-CPU緩存加速請求的處理,若是緩存爲空,內核可藉機檢查緩存的填充水平
        pcp = &zone_pcp(zone, cpu)->pcp;
        list = &pcp->lists[migratetype];
        local_irq_save(flags);
        if (list_empty(list)) 
        {
            //在針對當前處理器選擇了適當的per-CPU列表(熱頁或冷頁列表)以後,調用rmqueue_bulk從新填充緩存
            pcp->count += rmqueue_bulk(zone, 0, pcp->batch, list, migratetype, cold);
            if (unlikely(list_empty(list)))
                goto failed;
        }

        if (cold)
            page = list_entry(list->prev, struct page, lru);
        else
            page = list_entry(list->next, struct page, lru);

        list_del(&page->lru);
        pcp->count--;
    } 
    else 
    {
        //須要分配多頁
        if (unlikely(gfp_flags & __GFP_NOFAIL)) 
        {
            /*
             * __GFP_NOFAIL is not to be used in new code.
             *
             * All __GFP_NOFAIL callers should be fixed so that they
             * properly detect and handle allocation failures.
             *
             * We most definitely don't want callers attempting to
             * allocate greater than order-1 page units with
             * __GFP_NOFAIL.
             */
            WARN_ON_ONCE(order > 1);
        }
        spin_lock_irqsave(&zone->lock, flags);
        //__rmqueue會從內存域的夥伴列表中選擇適當的內存塊,若是有必要,該函數會自動分解大塊內存,將未用的部分放回列表中
        page = __rmqueue(zone, order, migratetype);
        spin_unlock(&zone->lock);
        //若是內存域中有足夠的空閒頁知足分配請求,但頁不是連續的,這種狀況,__rmqueue調用失敗並返回NULL指針
        if (!page)
            goto failed;
        __mod_zone_page_state(zone, NR_FREE_PAGES, -(1 << order));
    }

    __count_zone_vm_events(PGALLOC, zone, 1 << order);
    zone_statistics(preferred_zone, zone);
    local_irq_restore(flags);
    put_cpu();

    VM_BUG_ON(bad_range(zone, page));
    /*
    因爲全部失敗情形都跳轉到標號failed處理,這能夠確保內核到達當前點以後,page指向一系列有效的頁
    在返回指針前,prep_new_page須要作一些準備工做,以便內核可以處理這些頁,若是所選擇的頁出了問題,該函數返回正值,在這種狀況下,分配將從頭開始
    */
    if (prep_new_page(page, order, gfp_flags))
        goto again;
    return page;

failed:
    local_irq_restore(flags);
    put_cpu();
    return NULL;
}

__rmqueue輔助函數

內核使用了__rmqueue函數,該函數充當進入夥伴系統核心的"看門人"
\linux-2.6.32.63\mm\page_alloc.c

/*
Do the hard work of removing an element from the buddy allocator. Call me with the zone->lock already held.
*/
static struct page *__rmqueue(struct zone *zone, unsigned int order, int migratetype)
{
    struct page *page;

retry_reserve:
    //根據傳遞進來的分配階、用於獲取頁的內存域、遷移類型,__rmqueue_smallest掃描頁的列表,直至找到適當的連續內存塊
    page = __rmqueue_smallest(zone, order, migratetype);

    if (unlikely(!page) && migratetype != MIGRATE_RESERVE)
    {
        //若是指定的遷移列表不能知足分配請求,則調用__rmqueue_fallback嘗試其餘的遷移列表,做爲應急措施
        page = __rmqueue_fallback(zone, order, migratetype);

        /*
         * Use MIGRATE_RESERVE rather than fail an allocation. goto
         * is used because __rmqueue_smallest is an inline function
         * and we want just one call site
         */
        if (!page) 
        {
            migratetype = MIGRATE_RESERVE;
            goto retry_reserve;
        }
    }

    trace_mm_page_alloc_zone_locked(page, order, migratetype);
    return page;
}

__rmqueue_smallest的實現不是很長,本質上,它由一個循環組成,按遞增順序遍歷內存域的各個特定遷移類型的空閒頁列表,直至找到合適的一項

/*
Go through the free lists for the given migratetype and remove the smallest available page from the freelists
*/
static inline struct page *__rmqueue_smallest(struct zone *zone, unsigned int order, int migratetype)
{
    unsigned int current_order;
    struct free_area * area;
    struct page *page;

    /* 
    Find a page of the appropriate size in the preferred list 在首選的列表中找到適當大小的頁
    搜索從指定分配階對應的項開始,由於要求頁必須是連續的,因此最好的狀況也是當前對應分配階中正好能找到一塊內存區
    1. 若是檢查的列表中有一個元素(非空),那麼它就是可用的,由於其中包含了所需數目的連續頁
    2. 不然,內核將選擇下一個更高分配階,進行相似的搜索 
    */
    for (current_order = order; current_order < MAX_ORDER; ++current_order) 
    {
        area = &(zone->free_area[current_order]);
        if (list_empty(&area->free_list[migratetype]))
            continue;

        page = list_entry(area->free_list[migratetype].next, struct page, lru);
        list_del(&page->lru);
        rmv_page_order(page);
        area->nr_free--;
        //若是須要分配的內存塊長度小於所選擇的連續頁範圍,即若是由於沒有更小的適當的內存塊可用,而從較高的分配階分配了一塊內存,那麼該內存塊必須按照夥伴系統的原理分裂成更小的塊,即調用expand
        expand(zone, page, order, current_order, area, migratetype);
        return page;
    }

    return NULL;
}

0x6: 釋放頁

__free_pages是一個基礎函數,用於實現內核API中全部涉及內存釋放的函數

\linux-2.6.32.63\mm\page_alloc.c

void __free_pages(struct page *page, unsigned int order)
{
    if (put_page_testzero(page)) 
    {
        trace_mm_page_free_direct(page, order);
        /*
        首先判斷所需釋放的內存是單頁仍是較大的內存塊
        1. 若是釋放單頁,則不還給夥伴系統(在申請內存的時候,若是是單頁,也不從夥伴系統中申請,而是從per-CPU中申請),而是置於per-CPU緩存中,對極可能出如今CPU高速緩存的頁,則放置到熱頁的列表中
        free_hot_page簡單的進行參數轉換,隨即調用free_hot_cold_page
        若是free_hot_cold_page判斷per-CPU緩存中頁的數目超出了pcp->count,則將數量爲pcp->batch的一批內存還給夥伴系統,該策略稱之爲"惰性合併(lazy coalescing)"
        這種策略的原理在於: 若是單頁直接返回給夥伴系統,那麼會發生合,爲了知足後來的分配請求又須要進行拆分,惰性合併策略阻止了大量可能白費的合併和拆分操做
        */
        if (order == 0)
            free_hot_page(page);
        else
            /*
            若是釋放多個頁,則將工做委託給__free_pages_ok,即將相關的內存區添加到夥伴系統中適當的free_area列表,即合併到爲一個連續的內存區,放置到高一階的free_area列表中
            若是還能合併一個進一步的夥伴對,那麼繼續進行合併,轉移到更高階的列表,該過程會一直重複下去,直至全部可能的夥伴對都已經合併,並將改變儘量向上傳播
            該策略的核心思想是: 儘可能創造出儘量多的大塊連續內存
            */
            __free_pages_ok(page, order);
    }
}

這裏存在的一個問題是內核如何知道一個夥伴對的兩個部分都位於空閒頁的列表中,爲了將內存塊放回夥伴系統,內核必須計算"潛在夥伴"的地址,以及在有可能合併的狀況下合併後內存塊的索引,爲此,內核提供瞭如下幾個輔助函數
\linux-2.6.32.63\mm\page_alloc.c

/*
 * Locate the struct page for both the matching buddy in our pair (buddy1) and the combined O(n+1) page they form (page).
 *
 * 1) Any buddy B1 will have an order O twin B2 which satisfies the following equation:
 *     B2 = B1 ^ (1 << O)
 * For example, if the starting buddy (buddy2) is #8 its order 1 buddy is #10:
 *     B2 = 8 ^ (1 << 1) = 8 ^ 2 = 10
 *
 * 2) Any buddy B will have an order O+1 parent P which satisfies the following equation:
 *     P = B & ~(1 << O)
 *
 * Assumption: *_mem_map is contiguous at least up to MAX_ORDER
 */
static inline struct page *__page_find_buddy(struct page *page, unsigned long page_idx, unsigned int order)
{
    unsigned long buddy_idx = page_idx ^ (1 << order);

    return page + (buddy_idx - page_idx);
}

static inline unsigned long __find_combined_index(unsigned long page_idx, unsigned int order)
{
    return (page_idx & ~(1 << order));
}

/*
 * This function checks whether a page is free && is the buddy, we can do coalesce a page and its buddy if
 * (a) the buddy is not in a hole &&
 * (b) the buddy is in the buddy system &&
 * (c) a page and its buddy have the same order &&
 * (d) a page and its buddy are in the same zone.
 *
 * For recording whether a page is in the buddy system, we use PG_buddy. Setting, clearing, and testing PG_buddy is serialized by zone->lock.
 * For recording page's order, we use page_private(page).
 */
static inline int page_is_buddy(struct page *page, struct page *buddy, int order)
{
    /*
    夥伴的第一頁若是在夥伴系統中,則對應的struct page實例會設置PG_buddy標誌位,但這不足以做爲合併兩個夥伴的根據
    在釋放2(order)次方頁的內存塊時,內核必須確保第二個夥伴的2(order)次方頁也包含在夥伴系統中
    */
    if (!pfn_valid_within(page_to_pfn(buddy)))
        return 0;

    if (page_zone_id(page) != page_zone_id(buddy))
        return 0;

    if (PageBuddy(buddy) && page_order(buddy) == order) {
        VM_BUG_ON(page_count(buddy) != 0);
        return 1;
    }
    return 0;
}

下面代碼用於肯定一對夥伴是否可以合併(使用到了輔助函數)

/*
 * Freeing function for a buddy system allocator.
 *
 * The concept of a buddy system is to maintain direct-mapped table(containing bit values) for memory blocks of various "orders".
 * The bottom level table contains the map for the smallest allocatable units of memory (here, pages), and each level above it describes pairs of units from the levels below, hence, "buddies".
 * At a high level, all that happens here is marking the table entry at the bottom level available, and propagating the changes upward as necessary, 
 * plus some accounting needed to play nicely with other parts of the VM system.
 * At each level, we keep a list of pages, which are heads of continuous free pages of length of (1 << order) and marked with PG_buddy.  Page's order is recorded in page_private(page) field.
 * So when we are allocating or freeing one, we can derive the state of the other.  
 * That is, if we allocate a small block, and both were free, the remainder of the region must be split into blocks.   
 * If a block is freed, and its buddy is also free, then this triggers coalescing into a block of larger size.            
*/
static inline void __free_one_page(struct page *page, struct zone *zone, unsigned int order, int migratetype)
{
    unsigned long page_idx;

    if (unlikely(PageCompound(page)))
        if (unlikely(destroy_compound_page(page, order)))
            return;

    VM_BUG_ON(migratetype == -1);

    page_idx = page_to_pfn(page) & ((1 << MAX_ORDER) - 1);

    VM_BUG_ON(page_idx & ((1 << order) - 1));
    VM_BUG_ON(bad_range(zone, page));

    //例程試圖釋放分配階爲order的一個內存塊,由於有可能不僅當前內存塊可以與其直接夥伴合併,並且高階的夥伴也能夠合併,所以內核須要找到可能的"最大分配階"
    while (order < MAX_ORDER-1) 
    {
        unsigned long combined_idx;
        struct page *buddy;

        buddy = __page_find_buddy(page, page_idx, order);
        if (!page_is_buddy(page, buddy, order))
            break;

        /* Our buddy is free, merge with it and move up one order. */
        list_del(&buddy->lru);
        zone->free_area[order].nr_free--;
        rmv_page_order(buddy);
        combined_idx = __find_combined_index(page_idx, order);
        page = page + (combined_idx - page_idx);
        page_idx = combined_idx;
        order++;
    }
    set_page_order(page, order);
    list_add(&page->lru, &zone->free_area[order].free_list[migratetype]);
    zone->free_area[order].nr_free++;
}

0x7: 內核中不連續頁的分配

咱們知道,在內存分配中,物理上連續的映射對內核是最好的,但並不總能成功地申請到,在分配一大塊內存時,可能不遺餘力也沒法找到連續的內存塊。在用戶空間這不是問題,由於Ring3的應用程序老是使用內核提供的虛擬內存分頁機制(固然這會下降速度並佔用TLB)
在內核遇到沒法得到一整塊連續的大塊內存的時候用,也使用了一樣的技術,內核分配了其虛擬地址空間的一部分,用於創建連續映射,例以下圖的IA-32系統

vmalloc管理的是一段不連續的內存區域,這一段具備線性地址空間的全部性質,分配到其中的頁可能位於物理內存中的任何地方,經過修改負責該區域的內核頁表,便可作到這一點
每一個vmalloc分配的子區域都是"自包含"的,與其餘vmalloc子區域經過一個內存頁分隔(相似於直接映射和vmalloc區域之間的邊界),不一樣vmalloc子區域之間的分隔也是爲了防止不正確的內存訪問操做(這種狀況只會由於內核故障而出現)

1. 用vmalloc分配內存

vmalloc是一個接口函數,內核代碼使用它來分配在虛擬內存中連續但在物理內存中不必定連續的內存,使用vmalloc的最著名的實例是內核對模塊(LKM)的實現,由於模塊可能在任什麼時候候加載,若是模塊數據比較多,那麼沒法保證有足夠的連續內存可用,特別是系統已經運行了比較長的時間的狀況下,若是可以用小塊內存拼接出足夠的內存,那麼使用vmalloc能夠規避該問題

關於vmalloc函數的相關編程使用方法請參閱另外一篇文章
http://www.cnblogs.com/LittleHann/p/4113830.html

由於用於vmalloc的內存頁老是必須映射在內核地址空間中,所以使用ZONE_HIGHMEM內存域的頁是最優的選擇,這使得內核能夠節省更寶貴的較低端內存域,而又不會帶來額外的好處,所以,vmalloc是內核出於自身的目的使用高端內存頁的少數情形之一

數據結構

內核在管理虛擬內存中的vmalloc區域時,內核必須跟蹤哪些子區域被使用、哪些是空閒的。爲此定義了一個數據結構(strcut vm_struct),將全部使用的部分保存在一個鏈表中,注意和用戶空間進程的虛擬地址存儲結構struct vm_area_struct區分開來
\linux-2.6.32.63\include\linux\vmalloc.h

struct vm_struct 
{
    //next使得內核能夠將vmalloc區域中的全部子區域保存在一個單鏈表上
    struct vm_struct    *next;

    //addr定義了分配的子區域在虛擬地址空間中的起始地址
    void            *addr;
    //size表示該區域的長度,能夠根據該信息獲得vmalloc區域的完成分配方案
    unsigned long        size;
    /*
    flags存儲了與該內存區關聯的標誌聯合,它只用於指定內存區類型
    1. VM_ALLOC: 指定由vmalloc產生的子區域
    2. VM_MAP: 用於表示將現存pages集合映射到連續的虛擬地址空間彙總
    3. VM_TOREMAP: 表示將幾乎隨機的物理內存區域映射到vmalloc區域中,這是一個特定於體系結構的操做
    */
    unsigned long        flags;
    //pages是指向page指針數組的指針,每一個數組成員都表示一個映射到虛擬地址空間中的物理內存頁的page實例
    struct page        **pages;
    //nr_pages指定pages中數組項的數目,即涉及的內存頁數目
    unsigned int        nr_pages;
    //phys_addr僅當用ioremap映射到由物理地址描述的物理內存區域時才須要,該信息保存在phys_addr中
    unsigned long        phys_addr;
    void            *caller;
};

建立vm_area

在建立一個新的虛擬內存區以前,必須找到一個適當的位置,vm_area實例組成的一個鏈表,管理着vmalloc區域中已經創建的各個子區域,內核全局變量vmlist是表頭
\linux-2.6.32.63\mm\vmalloc.c

/*** Old vmalloc interfaces ***/
DEFINE_RWLOCK(vmlist_lock);
struct vm_struct *vmlist;

內核提供了輔助函數get_vm_area

/**
 *    get_vm_area  -  reserve a contiguous kernel virtual area
 *    @size:        size of the area
 *    @flags:        %VM_IOREMAP for I/O mappings or VM_ALLOC
 *
 *    Search an area of @size in the kernel virtual mapping area, and reserved it for out purposes.  Returns the area descriptor on success or %NULL on failure.
 */
struct vm_struct *get_vm_area(unsigned long size, unsigned long flags)
{
    return __get_vm_area_node(size, 1, flags, VMALLOC_START, VMALLOC_END, -1, GFP_KERNEL, __builtin_return_address(0));
}

static struct vm_struct *__get_vm_area_node(unsigned long size, unsigned long align, unsigned long flags, unsigned long start, unsigned long end, int node, gfp_t gfp_mask, void *caller)
{
    static struct vmap_area *va;
    struct vm_struct *area;

    BUG_ON(in_interrupt());
    if (flags & VM_IOREMAP) 
    {
        int bit = fls(size);

        if (bit > IOREMAP_MAX_ORDER)
            bit = IOREMAP_MAX_ORDER;
        else if (bit < PAGE_SHIFT)
            bit = PAGE_SHIFT;

        align = 1ul << bit;
    }

    size = PAGE_ALIGN(size);
    if (unlikely(!size))
        return NULL;

    area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);
    if (unlikely(!area))
        return NULL;

    /*
    We always allocate a guard page.
    老是分配一個警惕頁,做爲安全隙,內核首先適當提升須要分配的內存長度
    */
    size += PAGE_SIZE;

    va = alloc_vmap_area(size, align, start, end, node, gfp_mask);
    if (IS_ERR(va)) 
    {
        kfree(area);
        return NULL;
    }

    /*
     * When this function is called from __vmalloc_node,
     * we do not add vm_struct to vmlist here to avoid
     * accessing uninitialized members of vm_struct such as
     * pages and nr_pages fields. They will be set later.
     * To distinguish it from others, we use a VM_UNLIST flag.
     */
    if (flags & VM_UNLIST)
        setup_vmalloc_vm(area, va, flags, caller);
    else
        insert_vmalloc_vm(area, va, flags, caller);

    return area;
}

remove_vm_area函數將一個現存的子區域從vmlloc地址空間中刪除
\linux-2.6.32.63\mm\vmalloc.c

/**
 *    remove_vm_area  -  find and remove a continuous kernel virtual area
 *    @addr:        base address
 *
 *    Search for the kernel VM area starting at @addr, and remove it. This function returns the found VM area, but using it is NOT safe on SMP machines, except for its size or flags.
 */
struct vm_struct *remove_vm_area(const void *addr)
{
    struct vmap_area *va;

    //依次掃描vmlist的鏈表元素,直至找到匹配者
    va = find_vmap_area((unsigned long)addr);
    if (va && va->flags & VM_VM_AREA) 
    {
        struct vm_struct *vm = va->private;

        if (!(vm->flags & VM_UNLIST)) 
        {
            struct vm_struct *tmp, **p;
            /*
             * remove from list and disallow access to
             * this vm_struct before unmap. (address range
             * confliction is maintained by vmap.)
             */
            write_lock(&vmlist_lock);
            for (p = &vmlist; (tmp = *p) != vm; p = &tmp->next)
                ;
            *p = tmp->next;
            write_unlock(&vmlist_lock);
        }

        vmap_debug_free_range(va->va_start, va->va_end);
        free_unmap_vmap_area(va);
        vm->size -= PAGE_SIZE;

        return vm;
    }
    return NULL;
}

分配內存區

vmalloc發起對不連續的內存區的分配操做

/**
 *    vmalloc  -  allocate virtually contiguous memory
 *    @size:        allocation size
 *    Allocate enough pages to cover @size from the page level
 *    allocator and map them into contiguous kernel virtual space.
 *
 *    For tight control over page level allocator and protection flags
 *    use __vmalloc() instead.
 */
void *vmalloc(unsigned long size)
{
    return __vmalloc_node(size, 1, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL, -1, __builtin_return_address(0));
}
EXPORT_SYMBOL(vmalloc);

void *__vmalloc(unsigned long size, gfp_t gfp_mask, pgprot_t prot)
{
    return __vmalloc_node(size, 1, gfp_mask, prot, -1, __builtin_return_address(0));
}
EXPORT_SYMBOL(__vmalloc);

/**
 *    __vmalloc_node  -  allocate virtually contiguous memory
 *    @size:        allocation size
 *    @align:        desired alignment
 *    @gfp_mask:    flags for the page level allocator
 *    @prot:        protection mask for the allocated pages
 *    @node:        node to use for allocation or -1
 *    @caller:    caller's return address
 *
 *    Allocate enough pages to cover @size from the page level
 *    allocator with @gfp_mask flags.  Map them into contiguous
 *    kernel virtual space, using a pagetable protection of @prot.
 */
static void *__vmalloc_node(unsigned long size, unsigned long align, gfp_t gfp_mask, pgprot_t prot, int node, void *caller)
{
    struct vm_struct *area;
    void *addr;
    unsigned long real_size = size;

    size = PAGE_ALIGN(size);
    if (!size || (size >> PAGE_SHIFT) > totalram_pages)
        return NULL;

    //在vmlloc地址空間中找到一個適當的區域
    area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNLIST, VMALLOC_START, VMALLOC_END, node, gfp_mask, caller);

    if (!area)
        return NULL;

    //從物理內存中分配各個頁
    addr = __vmalloc_area_node(area, gfp_mask, prot, node, caller);

    /*
    In this function, newly allocated vm_struct is not added to vmlist at __get_vm_area_node(). so, it is added here.
    將這些頁連續地映射到vmalloc區域中
    */
    insert_vmalloc_vmlist(area);

    /*
     * A ref_count = 3 is needed because the vm_struct and vmap_area
     * structures allocated in the __get_vm_area_node() function contain
     * references to the virtual address of the vmalloc'ed block.
     */
    kmemleak_alloc(addr, real_size, 3, gfp_mask);

    return addr;
}

對於vmalloc的使用,咱們須要注意的是,要理解它的設計思想和使用場景

使用vmalloc從夥伴系統分配內存時,是逐頁分配的,而不是一次分配一大塊連續內存,這是vmalloc的一個關鍵方面。若是能夠確信可以分配連續內存頁,那麼就沒有必要使用vmalloc,畢竟該函數的全部目的就在於: 即便由於內存碎片的緣故,內存塊中的頁幀可能不是連續的,仍是依舊可以分配大的內存塊。將分配單位拆分得儘量小(即以頁爲單位),能夠確保在物理內存有嚴重碎片的狀況下,vmalloc仍然能夠工做

2. 備選映射方法

除了vmalloc以外,還有其餘方法能夠建立虛擬連續映射,這都基於__vmalloc函數或使用相似的機制

1. vmalloc_32
vmalloc_32的工做方式與vmalloc相同,但會確保所使用的物理內存老是能夠用普通32位指針尋址

2. vmap
使用一個page數組做爲起點,來建立虛擬連續內存區,與vmalloc相比,該函數所用的物理內存位置不是隱式分配的,而須要先行分配好,做爲參數傳遞

3. ioremap
ioremap是一個特定於處理器的函數,必須在全部體系結構上實現,它能夠取自物理地址空間、由系統總線用於I/O操做的一個內存塊,映射到內核的地址空間中
//該函數在設備驅動程序中使用不少,可將用於與外設通訊的地址區域暴露給內核的其餘部分使用

3. 釋放內存

有兩個函數用於向內核釋放內存,這兩個函數都會規約到__vunmap

1. vfree用於釋放vmalloc和vmalloc_32分配的區域
2. vunmap用於釋放由vmap或ioremap建立的映射

\linux-2.6.32.63\mm\vmalloc.c

/*
1. addr: 表示要釋放的區域的起始地址
2. deallocate_pages: 指定了是否將與該區域相關的物理內存頁返回給夥伴系統
    1) vfree: 將這個參數設置爲1
    2) vunmap: 將這個參數設置爲0,即只刪除映射,而不將相關的物理內存頁返回給夥伴系統
*/
static void __vunmap(const void *addr, int deallocate_pages)
{
    struct vm_struct *area;

    if (!addr)
        return;

    if ((PAGE_SIZE-1) & (unsigned long)addr) 
    {
        WARN(1, KERN_ERR "Trying to vfree() bad address (%p)\n", addr);
        return;
    }

    //沒必要明確給出須要釋放的區域長度,長度能夠從vmlist中的信息導出,remove_vm_area掃描該鏈表,以找到相關項
    area = remove_vm_area(addr);
    if (unlikely(!area)) 
    {
        WARN(1, KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n",
                addr);
        return;
    }

    debug_check_no_locks_freed(addr, area->size);
    debug_check_no_obj_freed(addr, area->size);

    if (deallocate_pages) 
    {
        int i;

        for (i = 0; i < area->nr_pages; i++) 
        {
            struct page *page = area->pages[i];

            BUG_ON(!page);
            __free_page(page);
        }

        //釋放用於管理該內存區的內核數據結構
        if (area->flags & VM_VPAGES)
            vfree(area->pages);
        else
            kfree(area->pages);
    }

    kfree(area);
    return;
}

0x8: 內核映射

儘管vmalloc函數族能夠用於從高端內存域向內核映射映射頁幀,但這並非這些函數的實際用途,內核提供了其餘函數用於將ZONE_HIGHMEM頁幀顯式映射到內核空間

1. 持久內核映射

若是須要將高端頁幀長期映射(做爲持久映射)到內核地址空間中,必須使用kmap函數,須要映射的頁用指向page的指針指定,做爲該函數的參數。該函數在有必要時建立一個映射(即若是該頁確實是高端頁),並返回數據的地址
若是沒有啓用高端支持,該函數的任務就比較簡單,在這種狀況下,全部頁均可以直接訪問,所以只須要返回頁的地址,無需顯式建立一個映射
若是確實存在高端頁,狀況會比較複雜,相似於vmalloc,內核首先必須創建高端頁和所映射到的地址之間的關聯,還必須在虛擬地址空間中分配一個區域以映射頁幀,最後,內核必須記錄該虛擬區域的哪些部分在使用中,哪些部分仍然是空閒的

2. 臨時內核映射

kmap函數不能用於中斷處理程序,由於它可能進入睡眠狀態。若是pkmap數組中沒有空閒位置,該函數會進入睡眠狀態,直至情形有所改善。所以內核提供了一個備選的映射函數,其執行是原子的,邏輯上稱kmap_atomic,該函數的一個主要優勢就是它比普通的kmap快速,但它不能用於可能進入睡眠的代碼,所以,他對於很快就須要一個臨時頁的簡短代碼,是很是理想的

3. 沒有高端內存的計算機上的映射函數

在許多體系結構上不支持高端內存,由於不須要該特性,例如64位體系結構,但爲了在使用內核映射函數的時候不須要老是區分高端內存和非高端內存體系結構,內核定義了幾個在普通內存實現兼容函數的宏
\linux-2.6.32.63\include\linux\highmem.h

#ifdef CONFIG_HIGHMEM
#include <asm/highmem.h>

/* declarations for linux/mm/highmem.c */
unsigned int nr_free_highpages(void);
extern unsigned long totalhigh_pages;

void kmap_flush_unused(void);

#else /* CONFIG_HIGHMEM */

static inline unsigned int nr_free_highpages(void) { return 0; }

#define totalhigh_pages 0

#ifndef ARCH_HAS_KMAP
static inline void *kmap(struct page *page)
{
    might_sleep();
    return page_address(page);
}

static inline void kunmap(struct page *page)
{
}

static inline void *kmap_atomic(struct page *page, enum km_type idx)
{
    pagefault_disable();
    return page_address(page);
}
#define kmap_atomic_prot(page, idx, prot)    kmap_atomic(page, idx)

#define kunmap_atomic(addr, idx)    do { pagefault_enable(); } while (0)
#define kmap_atomic_pfn(pfn, idx)    kmap_atomic(pfn_to_page(pfn), (idx))
#define kmap_atomic_to_page(ptr)    virt_to_page(ptr)

#define kmap_flush_unused()    do {} while(0)
#endif

#endif /* CONFIG_HIGHMEM */

 

4. SLAB分配器

內核須要常常分配內存,但沒法藉助於標準庫函數(例如C庫中的malloc),由於標準庫中的函數是基於夥伴系統提供的按頁分配內存,但這個單位太大了(即 只能是2的n次方階的內存塊分配),若是僅僅須要一個10個字符的字符串分配空間,分配一個4KB或者更多空間的完整頁面,這是徹底不可接受的。爲此必須 引入新的管理機制,這會給內核帶來更大的開銷,爲了最小化這個額外負擔對系統性能的影響,該管理層的實現應該儘量緊湊以便不要對處理器的高速緩存和 TLB帶來顯著影響,同時,內核還必須保證內存利用的速度和效率,解決這些問題的一個較好的方案就是SLAB分配,它對許多種類的工做負荷都很是高效
須要明白的是,提供小內存塊不是SLAB分配器的惟一任務,因爲結構上的特色,它也用做一個緩存,主要針對常常分配並釋放的對象,經過創建SLAB緩存,內核可以儲備一些對象,供後續使用,即便在初始化階段也是如此,例如

/*
爲了管理與進程關聯的文件系統數據,內核必須常常生成strcut fs_struct的新實例,此類型實例佔據的內存塊一樣須要常常回收(在進程結束時),換句話說,內核趨向於很是有規律地分配並釋放大小爲sizeof(fs_struct)的內存塊。SLAB分配器將釋放的內存塊保存在一個內部列表裏,並不立刻返回給夥伴系統,在請求爲該類對象分配一個新實例時,會使用最近釋放的內存塊,這帶來2個優勢
*/
1. 因爲內核沒必要使用夥伴系統算法,處理時間會變短(夥伴系統的階次分配和遞歸的階次合併相對較消耗時間)
2. 因爲該內存塊仍讓是""的,所以其仍然駐留在CPU高速緩存的機率較高
3. 調用夥伴系統的操做對系統的數據和指令高速緩存有至關的影響,內核越浪費這些資源,這些資源對用戶空間進程就越不可用,更輕量級的SLAB分配器在可能的狀況下減小了對夥伴系統的調用,有助於防止"緩存污染"
4. 若是數據存儲在夥伴系統直接提供的頁中,那麼其地址老是出如今2的冪次的整數倍附近,這對CPU高速緩存的利用有負面影響,因爲這種地址分佈,使得某些緩存行過分使用,而其餘的則幾乎爲空,多處理器可能會加重這種不利狀況,由於不一樣的內存地址可能在不一樣的總線上傳輸,多處理會致使某些總線擁塞,而其餘的總線幾乎沒有使用

經過"SLAB着色(SLAB coloring)",SLAB分配器可以均勻的分佈對象(讓對象在分配的內存塊區域中均勻分佈,不要老是位於2的冪次起始位置),以實現均勻的緩存使用

常用的內核對象保存在CPU高速緩存中,這是咱們想要的結果。着色這個術語是隱喻性的,它與顏色無關,只是表示SLAB中的對象須要移動的特定偏移量,以便使對象放置到不一樣的緩存行
值得注意的是,之因此稱之爲SLAB分配器,是由於各個緩存管理的對象,會合併爲較大的組,覆蓋一個或多個連續頁幀,這種組稱做SLAB,每一個緩存由這幾個這種SLAB組成

0x1: 備選分配器

儘管SLAB分配器對許多可能的工做負荷都工做良好,但也有一些場景它沒法提供最優性能,若是某些計算機處於當前硬件尺度的邊界上,在此類計算機上使用SLAB分配器會出現一些問題,例如

1. 微小的嵌入式系統: SLAB分配器代碼量和複雜性都過高
2. 配備有大量物理內存的大規模並行系統: SLAB分配器所需的大量元數據可能成爲一個問題

爲了解決這個問題,LINUX KERNEL增長SLAB分配器的2個替代品

1. SLOB分配器(simple linked list of block)進行了特別優化,以便減小代碼量,它圍繞一個簡單的內存塊鏈表展開,在分配內存時,使用了一樣簡單的最早適配算法
SLOB分配器只有大約600行代碼,十分的簡單

2. SLUB分配器經過將頁幀打包爲組,並經過struct page中未使用的字段來管理這些組,試圖最小化所需的內存開銷

因爲SLAB分配器是大多數內核配置的默認選項,但有一點須要強調,內核的其他部分無須關注底層選擇使用了哪一個分配器,全部分配器的前端接口都是相同的,每一個分配器都必須實現一組特定的函數,用於內存分配和緩存

1. kmalloc、_kmalloc、kmalloc_node: 用於通常的(用於特定結點)內存分配函數
2. kmem_cache_alloc、kmem_cache_alloc_node: 提供(特定於結點)特定類型的內核緩存

使用這些標準函數,內核能夠提供更方便的函數,而不涉及內存在內部具體如何管理(例如kcalloc爲數組分配內存、kzalloc分配一個填充字節0的內存區)

普通內核代碼只須要包含slab.h,便可使用內存分配的全部標準內核函數,聯編系統會保證使用編譯時選擇的分配器,來知足程序的內存分配請求

0x2: 內核中的內存管理

內核中通常的內存分配和釋放函數與C標準庫中等價函數的名稱相似,用法也幾乎相同

1. kmalloc(size, flags)
分配長度爲size字節的一個內存區,並返回指向該內存區起始處的一個void指針,若是沒有足夠內存,則結果爲NULL指針
2. kfree(*ptr)
釋放*ptr指向的內存區
3. vmalloc
4. vfree

5. percpu_alloc
6. percpu_free
//用於爲各個系統CPU分配和釋放所需內存區

kmalloc在內核源代碼中的使用數以千計,但模式都是相同的,用kmalloc分配的內存區,首先經過類型轉換爲正確的類型,而後賦值到指針指針變量
info = (struct cdrom_info *) kmalloc( sizeof(struct cdrom_info), GFP_KERNEL );
從程序員的角度來講,創建和使用緩存的任務不是特別困難,必須首先使用kmem_cache_create創建一個適當的緩存,接下來便可使用kmeme_cache_alloc、kmem_cache_free分配和釋放其中包含的對象。SLAB分配器負責完成與夥伴系統的交互,來分配所需的頁,全部活動緩存的列表保存在/proc/slabinfo中

cat /proc/slabinfo
slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
ip_conntrack_expect      0      0    136   28    1 : tunables  120   60    8 : slabdata      0      0      0
ip_conntrack          24     26    304   13    1 : tunables   54   27    8 : slabdata      2      2      0
ip_fib_alias          16     59     64   59    1 : tunables  120   60    8 : slabdata      1      1      0
ip_fib_hash           16     59     64   59    1 : tunables  120   60    8 : slabdata      1      1      0
bio_map_info         100    105   1064    7    2 : tunables   24   12    8 : slabdata     15     15      0
dm_mpath               0      0   1064    7    2 : tunables   24   12    8 : slabdata      0      0      0
jbd_4k                 2      2   4096    1    1 : tunables   24   12    8 : slabdata      2      2      0
dm_uevent              0      0   2608    3    2 : tunables   24   12    8 : slabdata      0      0      0
dm_tio                 0      0     24  144    1 : tunables  120   60    8 : slabdata      0      0      0
dm_io                  0      0     48   77    1 : tunables  120   60    8 : slabdata      0      0      0
sgpool-128            32     32   4096    1    1 : tunables   24   12    8 : slabdata     32     32      0
sgpool-64             32     32   2048    2    1 : tunables   24   12    8 : slabdata     16     16      0
sgpool-32             32     32   1024    4    1 : tunables   54   27    8 : slabdata      8      8      0
sgpool-16             32     32    512    8    1 : tunables   54   27    8 : slabdata      4      4      0
sgpool-8              32     45    256   15    1 : tunables  120   60    8 : slabdata      3      3      0
scsi_io_context        0      0    112   34    1 : tunables  120   60    8 : slabdata      0      0      0
ext3_inode_cache   15169  15185    760    5    1 : tunables   54   27    8 : slabdata   3037   3037      0
ext3_xattr             0      0     88   44    1 : tunables  120   60    8 : slabdata      0      0      0
journal_handle        76    144     24  144    1 : tunables  120   60    8 : slabdata      1      1      0
journal_head         112    160     96   40    1 : tunables  120   60    8 : slabdata      4      4      0
revoke_table           2    202     16  202    1 : tunables  120   60    8 : slabdata      1      1      0
revoke_record          0      0     32  112    1 : tunables  120   60    8 : slabdata      0      0      0
uhci_urb_priv          0      0     56   67    1 : tunables  120   60    8 : slabdata      0      0      0
UNIX                  44     44    704   11    2 : tunables   54   27    8 : slabdata      4      4      0
flow_cache             0      0    128   30    1 : tunables  120   60    8 : slabdata      0      0      0
cfq_ioc_pool          43     90    128   30    1 : tunables  120   60    8 : slabdata      3      3      0
cfq_pool              40     90    216   18    1 : tunables  120   60    8 : slabdata      5      5      0
crq_pool              62     96     80   48    1 : tunables  120   60    8 : slabdata      2      2      0
deadline_drq           0      0     80   48    1 : tunables  120   60    8 : slabdata      0      0      0
as_arq                 0      0     96   40    1 : tunables  120   60    8 : slabdata      0      0      0
mqueue_inode_cache      1      4    896    4    1 : tunables   54   27    8 : slabdata      1      1      0
isofs_inode_cache      0      0    608    6    1 : tunables   54   27    8 : slabdata      0      0      0
hugetlbfs_inode_cache      1      7    576    7    1 : tunables   54   27    8 : slabdata      1      1      0
ext2_inode_cache       0      0    720    5    1 : tunables   54   27    8 : slabdata      0      0      0
ext2_xattr             0      0     88   44    1 : tunables  120   60    8 : slabdata      0      0      0
dnotify_cache          0      0     40   92    1 : tunables  120   60    8 : slabdata      0      0      0
dquot                  0      0    256   15    1 : tunables  120   60    8 : slabdata      0      0      0
eventpoll_pwq          3     53     72   53    1 : tunables  120   60    8 : slabdata      1      1      0
eventpoll_epi          2     20    192   20    1 : tunables  120   60    8 : slabdata      1      1      0
inotify_event_cache     63     92     40   92    1 : tunables  120   60    8 : slabdata      1      1      0
inotify_watch_cache     11     53     72   53    1 : tunables  120   60    8 : slabdata      1      1      0
kioctx                 0      0    320   12    1 : tunables   54   27    8 : slabdata      0      0      0
kiocb                  0      0    256   15    1 : tunables  120   60    8 : slabdata      0      0      0
fasync_cache           0      0     24  144    1 : tunables  120   60    8 : slabdata      0      0      0
shmem_inode_cache    224    250    768    5    1 : tunables   54   27    8 : slabdata     50     50      0
posix_timers_cache      0      0    128   30    1 : tunables  120   60    8 : slabdata      0      0      0
uid_cache              2     30    128   30    1 : tunables  120   60    8 : slabdata      1      1      0
ip_mrt_cache           0      0    128   30    1 : tunables  120   60    8 : slabdata      0      0      0
tcp_bind_bucket       21    112     32  112    1 : tunables  120   60    8 : slabdata      1      1      0
inet_peer_cache        2     30    128   30    1 : tunables  120   60    8 : slabdata      1      1      0
secpath_cache          0      0     64   59    1 : tunables  120   60    8 : slabdata      0      0      0
xfrm_dst_cache         0      0    384   10    1 : tunables   54   27    8 : slabdata      0      0      0
ip_dst_cache          47     60    384   10    1 : tunables   54   27    8 : slabdata      6      6      0
arp_cache              3     15    256   15    1 : tunables  120   60    8 : slabdata      1      1      0
RAW                   19     20    768    5    1 : tunables   54   27    8 : slabdata      4      4      0
UDP                    5      5    768    5    1 : tunables   54   27    8 : slabdata      1      1      0
tw_sock_TCP           19     20    192   20    1 : tunables  120   60    8 : slabdata      1      1      0
request_sock_TCP       0      0    128   30    1 : tunables  120   60    8 : slabdata      0      0      0
TCP                   10     10   1600    5    2 : tunables   24   12    8 : slabdata      2      2      0
blkdev_ioc            35    118     64   59    1 : tunables  120   60    8 : slabdata      2      2      0
blkdev_queue          23     25   1576    5    2 : tunables   24   12    8 : slabdata      5      5      0
blkdev_requests       41     70    272   14    1 : tunables   54   27    8 : slabdata      5      5      0
biovec-256             7      7   4096    1    1 : tunables   24   12    8 : slabdata      7      7      0
biovec-128             7      8   2048    2    1 : tunables   24   12    8 : slabdata      4      4      0
biovec-64              7      8   1024    4    1 : tunables   54   27    8 : slabdata      2      2      0
biovec-16              7     15    256   15    1 : tunables  120   60    8 : slabdata      1      1      0
biovec-4               7     59     64   59    1 : tunables  120   60    8 : slabdata      1      1      0
biovec-1              49    202     16  202    1 : tunables  120   60    8 : slabdata      1      1      0
bio                  300    360    128   30    1 : tunables  120   60    8 : slabdata     12     12      0
utrace_engine_cache      0      0     64   59    1 : tunables  120   60    8 : slabdata      0      0      0
utrace_cache           0      0     64   59    1 : tunables  120   60    8 : slabdata      0      0      0
sock_inode_cache      96     96    640    6    1 : tunables   54   27    8 : slabdata     16     16      0
skbuff_fclone_cache     21     21    512    7    1 : tunables   54   27    8 : slabdata      3      3      0
skbuff_head_cache    498    555    256   15    1 : tunables  120   60    8 : slabdata     37     37      0
file_lock_cache        5     22    176   22    1 : tunables  120   60    8 : slabdata      1      1      0
Acpi-Operand        3248   3304     64   59    1 : tunables  120   60    8 : slabdata     56     56      0
Acpi-ParseExt          0      0     64   59    1 : tunables  120   60    8 : slabdata      0      0      0
Acpi-Parse             0      0     40   92    1 : tunables  120   60    8 : slabdata      0      0      0
Acpi-State             0      0     80   48    1 : tunables  120   60    8 : slabdata      0      0      0
Acpi-Namespace      2199   2240     32  112    1 : tunables  120   60    8 : slabdata     20     20      0
delayacct_cache      225    295     64   59    1 : tunables  120   60    8 : slabdata      5      5      0
taskstats_cache       13     53     72   53    1 : tunables  120   60    8 : slabdata      1      1      0
proc_inode_cache    1188   1212    592    6    1 : tunables   54   27    8 : slabdata    202    202      3
sigqueue              96     96    160   24    1 : tunables  120   60    8 : slabdata      4      4      0
radix_tree_node     6432   6433    536    7    1 : tunables   54   27    8 : slabdata    919    919      0
bdev_cache            24     28    832    4    1 : tunables   54   27    8 : slabdata      7      7      0
sysfs_dir_cache     3468   3520     88   44    1 : tunables  120   60    8 : slabdata     80     80      0
mnt_cache             28     30    256   15    1 : tunables  120   60    8 : slabdata      2      2      0
inode_cache         1267   1281    560    7    1 : tunables   54   27    8 : slabdata    183    183      0
dentry_cache       23148  23148    216   18    1 : tunables  120   60    8 : slabdata   1286   1286      0
filp                 600   1185    256   15    1 : tunables  120   60    8 : slabdata     79     79      0
names_cache           25     25   4096    1    1 : tunables   24   12    8 : slabdata     25     25      0
avc_node              34     53     72   53    1 : tunables  120   60    8 : slabdata      1      1      0
selinux_inode_security  17931  18000     80   48    1 : tunables  120   60    8 : slabdata    375    375      0
key_jar                5     20    192   20    1 : tunables  120   60    8 : slabdata      1      1      0
idr_layer_cache      100    105    528    7    1 : tunables   54   27    8 : slabdata     15     15      0
buffer_head        43856  43880     96   40    1 : tunables  120   60    8 : slabdata   1097   1097      0
mm_struct             76     76    896    4    1 : tunables   54   27    8 : slabdata     19     19      0
vm_area_struct      1632   1914    176   22    1 : tunables  120   60    8 : slabdata     87     87    180
fs_cache             177    177     64   59    1 : tunables  120   60    8 : slabdata      3      3     60
files_cache           75     80    768    5    1 : tunables   54   27    8 : slabdata     16     16      0
signal_cache         122    144    832    9    2 : tunables   54   27    8 : slabdata     16     16      0
sighand_cache         87     87   2112    3    2 : tunables   24   12    8 : slabdata     29     29      0
task_struct          136    136   1888    2    1 : tunables   24   12    8 : slabdata     68     68      0
anon_vma             651   1440     24  144    1 : tunables  120   60    8 : slabdata     10     10     15
pid                  242    295     64   59    1 : tunables  120   60    8 : slabdata      5      5      0
shared_policy_node      0      0     48   77    1 : tunables  120   60    8 : slabdata      0      0      0
numa_policy           38    144     24  144    1 : tunables  120   60    8 : slabdata      1      1      0
size-131072(DMA)       0      0 131072    1   32 : tunables    8    4    0 : slabdata      0      0      0
size-131072            0      0 131072    1   32 : tunables    8    4    0 : slabdata      0      0      0
size-65536(DMA)        0      0  65536    1   16 : tunables    8    4    0 : slabdata      0      0      0
size-65536             0      0  65536    1   16 : tunables    8    4    0 : slabdata      0      0      0
size-32768(DMA)        0      0  32768    1    8 : tunables    8    4    0 : slabdata      0      0      0
size-32768             3      3  32768    1    8 : tunables    8    4    0 : slabdata      3      3      0
size-16384(DMA)        0      0  16384    1    4 : tunables    8    4    0 : slabdata      0      0      0
size-16384             5      5  16384    1    4 : tunables    8    4    0 : slabdata      5      5      0
size-8192(DMA)         0      0   8192    1    2 : tunables    8    4    0 : slabdata      0      0      0
size-8192             13     13   8192    1    2 : tunables    8    4    0 : slabdata     13     13      0
size-4096(DMA)         0      0   4096    1    1 : tunables   24   12    8 : slabdata      0      0      0
size-4096            130    130   4096    1    1 : tunables   24   12    8 : slabdata    130    130      0
size-2048(DMA)         0      0   2048    2    1 : tunables   24   12    8 : slabdata      0      0      0
size-2048            237    244   2048    2    1 : tunables   24   12    8 : slabdata    122    122      0
size-1024(DMA)         0      0   1024    4    1 : tunables   54   27    8 : slabdata      0      0      0
size-1024            752    752   1024    4    1 : tunables   54   27    8 : slabdata    188    188     27
size-512(DMA)          0      0    512    8    1 : tunables   54   27    8 : slabdata      0      0      0
size-512             405    416    512    8    1 : tunables   54   27    8 : slabdata     52     52      0
size-256(DMA)          0      0    256   15    1 : tunables  120   60    8 : slabdata      0      0      0
size-256            1080   1080    256   15    1 : tunables  120   60    8 : slabdata     72     72      0
size-128(DMA)          0      0    128   30    1 : tunables  120   60    8 : slabdata      0      0      0
size-64(DMA)           0      0     64   59    1 : tunables  120   60    8 : slabdata      0      0      0
size-64             2011   2183     64   59    1 : tunables  120   60    8 : slabdata     37     37      0
size-32(DMA)           0      0     32  112    1 : tunables  120   60    8 : slabdata      0      0      0
size-128            1316   1380    128   30    1 : tunables  120   60    8 : slabdata     46     46      0
size-32             2155   2352     32  112    1 : tunables  120   60    8 : slabdata     21     21      0
kmem_cache           134    134   2688    1    1 : tunables   24   12    8 : slabdata    134    134      0

輸出的各列信息包括

1. 用於標識各個緩存的字符串名稱(確保不會建立相同的緩存)
2. 緩存中活動對象的數量
3. 緩存中對象的總數(已用、未用)
4. 所管理對象的長度,按字節計算
5. 一個SLAB中對象的數量
6. 每一個SLAB中頁的數量
7. 活動SLAB的數量
8. 在內核決定向緩存分配更多內存時,所分配對象的數量。每次會分配一個較大的內存塊,以減小與夥伴系統的交互,在縮小緩存時,也使用該值做爲釋放內存塊的大小

0x3: SLAB分配的原理

SLAB分配器由一個緊密地交織的數據和內存結構的網絡組成

1. 保存管理性數據的緩存對象
2. 保存被管理對象的各個SLAB

每一個緩存只負責一種對象類型(例如struct unix_sock實例),或提供通常性的緩衝區。各個緩存中SLAB的數目各有不一樣,這與已經使用的頁的數目、對象長度、被管理對象的數目有關
能夠看到,系統中全部的緩存都保存在一個雙鏈表中,這使得內核有機會依次遍歷全部的緩存,這是有必要的,例如在即將發生內存不足時,內核可能須要縮減分配給緩存的內存數量

1. 緩存的精細結構

咱們更加仔細地研究一下緩存的結構,kmem_cache是Linux內核提供的快速內存緩衝接口,這些內存塊要求是大小相同的,由於分配出的內存在接口釋放時並不真正釋放,而是做爲緩存保留,下一次請求分配時就能夠直接使用,省去了各類內存塊初始化或釋放的操做,所以分配速度很快,一般用於大數量的內存塊分配的狀況,如inode節點、skbuff頭、netfilter的鏈接等,其實kmalloc也是從kmem_cache中分配的,可通  
過/proc/slabinfo文件直接讀取cache分配狀況

 

struct kmem_cache 
{
    /* 
    1) per-cpu data, touched during every alloc/free 
    指向一個數組,每次分配/釋放期間都會訪問,其中包含了與系統CPU數目相同的數組項,每一個元素都是一個指針,指向一個數組緩存(array cache),其中包含了對應於特定系統CPU的管理數據
    struct array_cache 
    {  
        unsigned int avail;  
        unsigned int limit;  
        unsigned int batchcount;  
        unsigned int touched;  
        spinlock_t lock;  
        void *entry[0];  
    };  
    爲最好地利用CPU高速緩存(即儘量地去訪問熱點數據,CPU高速緩存是一種硬件機制,而內核須要作的是儘量遵循正確地使用方式操做內存,最大化地利用這個硬件機制)
    這些per-CPU指針是很重要的,在分配和釋放對象時,採用後進先出原理(LIFO last in first out),內核假定剛釋放的對象仍然處於CPU高速緩存中,會盡快再次分配它(響應下一個分配請求)
    僅當per-CPU緩存爲空時,纔會用SLAB中的空閒對象從新填充它們
    */
    struct array_cache *array[NR_CPUS];
    /* 
    2) Cache tunables. Protected by cache_chain_mutex 
    可調整的緩存參數,由cache_chain_mutex保護
    */
    //batchcount指定了在per-CPU列表爲空的狀況下,從緩存的SLAB中獲取對象的數目,它還表示在緩存增加時分配的對象數目
    unsigned int batchcount;
    //limit指定了per-CPU列表中保存的對象的最大數目,若是超出該值,內核會將batchcount個對象返回到SLAB,若是接下來內核縮減緩存,則釋放的內存從SLAB返回到夥伴系統
    unsigned int limit;
    unsigned int shared;

    //buffer_size指定了緩存中管理的對象的長度
    unsigned int buffer_size;
    u32 reciprocal_buffer_size;
    /* 3) touched by every alloc & free from the backend */

    unsigned int flags;            /* constant flags  常數標誌*/
    unsigned int num;            /* # of objs per slab 每一個SLAB中對象的數量 */

    /* 
    4) cache_grow/shrink 
    緩存的增加/縮減
    */
    /* order of pgs per slab (2^n) 每一個SLAB中頁數,取以2爲底的對數*/
    unsigned int gfporder;

    /* force GFP flags, e.g. GFP_DMA 強制的GFP標誌 */
    gfp_t gfpflags;

    //colour指定了顏色的最大數目
    size_t colour;                
    /* cache colouring range 緩存着色範圍,即基本偏移量乘以顏色值得到的絕對偏移量*/
    unsigned int colour_off;    /* colour offset 着色偏移 */
    struct kmem_cache *slabp_cache;
    unsigned int slab_size;
    /* dynamic flags 動態標誌集合,描述SLAB的"動態性質" */
    unsigned int dflags;        

    /* constructor func 構造函數*/
    void (*ctor)(void *obj);

    /* 5) cache creation/removal 緩存建立/刪除 */
    //name是一個字符串,包含該緩存的名稱
    const char *name;
    //next是一個標準的鏈表元素,用於將kmem_cache的全部實例保存在全局鏈表cache_chain上
    struct list_head next;

    /* 6) statistics 統計量 */
#ifdef CONFIG_DEBUG_SLAB
    unsigned long num_active;
    unsigned long num_allocations;
    unsigned long high_mark;
    unsigned long grown;
    unsigned long reaped;
    unsigned long errors;
    unsigned long max_freeable;
    unsigned long node_allocs;
    unsigned long node_frees;
    unsigned long node_overflow;
    atomic_t allochit;
    atomic_t allocmiss;
    atomic_t freehit;
    atomic_t freemiss;

    /*
     * If debugging is enabled, then the allocator can add additional
     * fields and/or padding to every object. buffer_size contains the total
     * object size including these internal fields, the following two
     * variables contain the offset to the user object and its size.
     */
    int obj_offset;
    int obj_size;
#endif /* CONFIG_DEBUG_SLAB */

    /*
    We put nodelists[] at the end of kmem_cache, because we want to size this array to nr_node_ids slots instead of MAX_NUMNODES 
    We still use [MAX_NUMNODES] and not [1] or [0] because cache_cache is statically defined, so we reserve the max number of nodes.
    每一個內存結點都對應3個表頭,用於組織SLAB的鏈表
    1. 第一個鏈表: 包含徹底用盡的SLAB
    2. 第二個鏈表: 包含部分空閒的SLAB
    3. 第三個鏈表: 空閒的SLAB
    */
    struct kmem_list3 *nodelists[MAX_NUMNODES];
    /*
     * Do not add fields after nodelists[]
     */
};

 

這樣,對象分配的體系結構就造成了一個三級的層次結構,分配成本和操做對CPU高速緩存和TLB的負面影響逐級升高

1. 仍然處於CPU高速緩存中的per-CPU對象
2. 現存SLAB中未使用的對象
3. 剛使用夥伴系統分配的新SLAB中未使用的對象

2. SLAB的精細結構

對象在SLAB中並不是連續排列,而是按照一個至關複雜的方案分佈

/*
 * The slab lists for all objects.
 */
struct kmem_list3 
{
    /*
    管理結構位於每一個SLAB的起始處,保存了全部的管理數據(和用於鏈接緩存鏈表的鏈表元素)
    /*
    struct list_head slabs_partial;    /* partial list first, better asm code */
    struct list_head slabs_full;
    struct list_head slabs_free;

    //free_objects表示slabs_partial和slabs_free的全部SLAB中空閒對象的總數
    unsigned long free_objects;

    //free_limit指定了全部SLAB上允許未使用對象的最大數目
    unsigned int free_limit;
    unsigned int colour_next;    /* Per-node cache coloring 各結點緩存着色 */
    spinlock_t list_lock;
    struct array_cache *shared;    /* shared per node 結點內共享 */
    struct array_cache **alien;    /* on other nodes 在其餘結點上 */

    /* 
    updated without locking 無需鎖定便可更新 
    next_reap定義了內核在兩次嘗試收縮緩存之間,必須通過的時間間隔,其思想是防止因爲頻繁的緩存收縮和增加操做而下降系統性能
    這種操做可能在某些系統共負荷下發生,該技術只能在NUMA系統上使用
    */
    unsigned long next_reap;    

    /* 
    updated without locking 無需鎖定便可更新 
    free_touched表示緩存是不是活動的,在從緩存獲取一個對象時,內核將該變量的值設置爲1,在緩存收縮時,該值重置爲0
    但內核只有在free_touched預先設置爲0時,纔會收縮緩存,由於1表示內核的另外一部分剛從該緩存獲取對象,此時收縮是不合適的
    */
    int free_touched;        
};

用於每一個對象的長度並不反映其確切的大小,相反,長度已經進行了舍入,以知足對齊方式的要求,有兩種可用的備選對齊方案

1. SLAB建立時使用標誌SLAB_HWCACHE_ALIGN,SLAB用戶能夠要求對象按硬件緩存行對齊,那麼會按照cache_line_size的返回值進行對齊,該函數返回特定於處理器的L1緩存大小,若是對象小於緩存行長度的一半,那麼將多個對象放入一個緩存行
2. 若是不要求按硬件緩存行對齊,那麼內核保證對象按BYTES_PER_WORD對齊,該值是表示void指針所需字節的數目
/*
在32位機器上,void指針須要4個字節,所以對有6個字節的對象,則須要8 = 2 * 4個字節,15個字節的對象須要16 = 4 * 4個字節,多餘的字節稱爲填充字節,填充字節能夠加速對SLAB中對象的訪問,若是使用對齊的地址,那麼在幾乎全部的體系結構上,內存訪問都會更快,這彌補了使用填充字節必然致使須要更多內存的不利狀況
*/

大多數狀況下,SLAB內存區的長度(減去了頭部管理數據)是不能被(可能填補過的)對象長度整除的,所以,內核就有了一些多餘的內存,能夠用來以偏移量的形式給SLAB進行"着色",即緩存的各個SLAB成員會指定不一樣的偏移量,以便將數據定位到不一樣的緩存行,於是SLAB開始和結束處的空閒內存是不一樣的,在計算偏移量時,內核必須考慮其餘的對齊因素,例如L1高速緩存中數據的對齊


管理數據能夠放置在SLAB自身,也能夠放置到使用kmalloc分配的不一樣內存區中,內核如何選擇,取決於SLAB的長度和已用對象的數量,管理數據和SLAB內存之間的關聯很容易創建,由於SLAB頭包含一個指針,指向SLAB數據區的起始處(不管管理數據是否在SLAB上)
例以下圖給出了管理數據不在SLAB自身,而位於另外一內存區的情形

最後,內核須要一種方法,經過對象自身便可識別SLAB(以及對象駐留的緩存)(即逆向查找),根據對象的物理內存地址,能夠找到相關的頁,所以能夠在全局mem_map數組中找到對應的page實例

1. struct page -> lru.next: 指向頁駐留的緩存的管理結構
2. struct page -> lru.prev: 指向保存該頁的SLAB的管理結構

設置或讀取SLAB信息、處理緩存信息的任務分別由下列函數完成

//處理緩存信息的設置和讀取
1. static inline void page_set_cache(struct page *page, struct kmem_cache *cache)
2. static inline struct kmem_cache *page_get_cache(struct page *page)

//設置或讀取SLAB信息
1. static inline void page_set_slab(struct page *page, struct slab *slab)
2. static inline struct slab *page_get_slab(struct page *page)

此外,內核還對分配給SLAB分配器的每一個物理內存頁都設置標誌PG_SLAB

Relevant Link:

http://cxw06023273.iteye.com/blog/867312 
http://guojing.me/linux-kernel-architecture/posts/slab-structure/

0x4: 實現

爲了實現SLAB分配器,使用了各類數據結構,SLAB的KERNEL代碼在內核中不多被修改,相關的代碼並不老是很容易閱讀或理解,這是由於許多內存區須要使用指針運算和類型轉換進行操做,這不是C語言中以清晰簡明著稱的領域,因爲SLAB系統帶有大量調試選項,因此代碼中遍及着預處理語句

1. 危險區(red zoning)
在每一個對象的開始和結束處增長一個額外的內存區,其中填充已知的字節模式,若是模式被修改,程序員在分析內核內存時就能夠注意到,多是由於某些代碼訪問了不屬於它們的內存區

2. 對象毒化(object poisoning)
在創建和釋放SLAB時,將對象用預約義的的模式填充,若是在對象分配時注意到該模式已經改變,程序員知道已經發生了未受權的訪問

1. 數據結構

每一個緩存由kmem_cache結構的一個實例表示
linux-2.6.32.63\mm\slab.c

/* 
internal cache of cache description objs 
該結構在內核中其餘地方是不可見的,緩存的用戶無須詳細瞭解緩存是如何實現的,將SLAB緩存視爲經過一組標準函數來高效地建立和釋放特定類型對象的機制,就足夠了
*/
static struct kmem_cache cache_cache = 
{
    .batchcount = 1,
    .limit = BOOT_CPUCACHE_ENTRIES,
    .shared = 1,
    .buffer_size = sizeof(struct kmem_cache),
    .name = "kmem_cache",
};

2. 初始化

爲初始化SLAB數據結構,內核須要若干遠小於一整頁的內存塊,這些最適合由kmalloc分配,這裏關鍵問題是: 只有在SLAB系統已經啓用後,才能使用kmalloc(kmalloc是基於SLAB實現的),更確切地說,該問題涉及kmalloc和per-CPU緩存的初始化,在這些緩存可以初始化以前,kmalloc必須能夠用來分配所需的內存空間,而kmalloc自身也處於初始化的過程當中,即kmalloc只能在kmalloc已經初始化以後初始化,這是覺得僞命題,所以內核必須藉助一些技巧
kmem_cache_init函數用於初始化SLAB分配器,它在內核初始化階段(start_kernel)、夥伴系統啓用以後調用。但在多處理系統上,啓動CPU此時正在運行,而其餘CPU還沒有初始化,kmem_cache_init採用了一個多步驟過程,逐步激活SLAB分配器

1. kmem_cache_init建立系統中的第一個SLAB緩存,以便爲kmem_cache的實例提供內存,爲此,內核使用的主要是在編譯時建立的靜態數據,實際上,一個靜態的數據結構(initarray_cache)用做per-CPU數組,該緩存的名稱是cache_cache
2. kmem_cache_init接下來初始化通常性的緩存,用做kmalloc內存的來源,爲此,針對所需的各個緩存長度,分別調用kmem_cache_create 
3. 在kmem_cache_init最後一步,把到目前爲止一直使用的數據結構的全部靜態實例化成員,用kmalloc動態分配的版本替代

3. 建立緩存

建立新的緩存必須調用kmem_cache_create
\linux-2.6.32.63\mm\slab.c

/*
1. name: 緩存名稱,隨後會出如今/proc/slabinfo中
2. size: 被管理對象以字節記的長度
3. align: 在對齊數據時使用的偏移量
4. flags: 一組標誌
5. ctor: 構造函數
*/
struct kmem_cache *kmem_cache_create (const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *))

建立新緩存是一個漫長的過程

struct kmem_cache *kmem_cache_create (const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *))
{
    size_t left_over, slab_size, ralign;
    struct kmem_cache *cachep = NULL, *pc;
    gfp_t gfp;

    /*
    Sanity checks... these are all serious usage bugs.
    參數有效性檢查
    */
    if (!name || in_interrupt() || (size < BYTES_PER_WORD) || size > KMALLOC_MAX_SIZE) 
    {
        printk(KERN_ERR "%s: Early error in slab %s\n", __func__, name);
        BUG();
    }

    /*
     * We use cache_chain_mutex to ensure a consistent view of
     * cpu_online_mask as well.  Please see cpuup_callback
     */
    if (slab_is_available()) 
    {
        get_online_cpus();
        mutex_lock(&cache_chain_mutex);
    }

    list_for_each_entry(pc, &cache_chain, next) 
    {
        char tmp;
        int res;

        /*
         * This happens when the module gets unloaded and doesn't
         * destroy its slab cache and no-one else reuses the vmalloc
         * area of the module.  Print a warning.
         */
        res = probe_kernel_address(pc->name, tmp);
        if (res) 
        {
            printk(KERN_ERR "SLAB: cache with size %d has lost its name\n", pc->buffer_size);
            continue;
        }

        if (!strcmp(pc->name, name)) 
        {
            printk(KERN_ERR "kmem_cache_create: duplicate cache %s\n", name);
            dump_stack();
            goto oops;
        }
    }

#if DEBUG
    WARN_ON(strchr(name, ' '));    /* It confuses parsers */
#if FORCED_DEBUG
    /*
     * Enable redzoning and last user accounting, except for caches with
     * large objects, if the increased size would increase the object size
     * above the next power of two: caches with object sizes just above a
     * power of two have a significant amount of internal fragmentation.
     */
    if (size < 4096 || fls(size - 1) == fls(size-1 + REDZONE_ALIGN + 2 * sizeof(unsigned long long)))
        flags |= SLAB_RED_ZONE | SLAB_STORE_USER;
    if (!(flags & SLAB_DESTROY_BY_RCU))
        flags |= SLAB_POISON;
#endif
    if (flags & SLAB_DESTROY_BY_RCU)
        BUG_ON(flags & SLAB_POISON);
#endif
    /*
     * Always checks flags, a caller might be expecting debug support which
     * isn't available.
     */
    BUG_ON(flags & ~CREATE_MASK);

    /*
    Check that size is in terms of words.  This is needed to avoid unaligned accesses for some archs when redzoning is used, 
    and makes sure any on-slab bufctl's are also correctly aligned.
    對象對齊
    */
    if (size & (BYTES_PER_WORD - 1)) 
    {
        size += (BYTES_PER_WORD - 1);
        size &= ~(BYTES_PER_WORD - 1);
    }

    /* calculate the final buffer alignment: */

    /* 
    1) arch recommendation: can be overridden for debug
    對象對齊一般也是基於基於處理器的字長,但若是設置了SLAB_HWCACHE_ALIGN標誌,則內核按照特定於體系結構的函數cache_line_size給出的值,來對齊數據
    內核還嘗試將盡量多的對象填充到一個緩存行中,只要對象長度容許,則會一直嘗試將對齊值除以2,所以,會有二、四、6..個對象放入一個緩存行,而不是隻有一個對象
    */
    if (flags & SLAB_HWCACHE_ALIGN) 
    {
        /*
         * Default alignment: as specified by the arch code.  Except if
         * an object is really small, then squeeze multiple objects into
         * one cacheline.
         */
        ralign = cache_line_size();
        while (size <= ralign / 2)
            ralign /= 2;
    } else {
        ralign = BYTES_PER_WORD;
    }

    /*
     * Redzoning and user store require word alignment or possibly larger.
     * Note this will be overridden by architecture or caller mandated
     * alignment if either is greater than BYTES_PER_WORD.
     */
    if (flags & SLAB_STORE_USER)
        ralign = BYTES_PER_WORD;

    if (flags & SLAB_RED_ZONE) {
        ralign = REDZONE_ALIGN;
        /* If redzoning, ensure that the second redzone is suitably
         * aligned, by adjusting the object size accordingly. */
        size += REDZONE_ALIGN - 1;
        size &= ~(REDZONE_ALIGN - 1);
    }

    /* 
    2) arch mandated alignment 
    內核也考慮如下事實,某些體系結構須要一個最小值做爲數據對齊的邊界,由ARCH_SLAB_MINALIGN定義,用於所要求的對齊也能夠接收
    */
    //體系結構強制的最小對齊值
    if (ralign < ARCH_SLAB_MINALIGN) 
    {
        ralign = ARCH_SLAB_MINALIGN;
    }
    /* 
    3) caller mandated alignment 
    使用者強制的對齊值
    */
    if (ralign < align) 
    {
        ralign = align;
    }
    /* disable debug if necessary */
    if (ralign > __alignof__(unsigned long long))
        flags &= ~(SLAB_RED_ZONE | SLAB_STORE_USER);
    /*
    4) Store it.
    存儲最後計算出的對齊值
    */
    align = ralign;

    if (slab_is_available())
        gfp = GFP_KERNEL;
    else
        gfp = GFP_NOWAIT;

    /*
    Get cache's description obj. 
    在數據對齊值計算完畢以後,分配struct kmem_cache的一個新實例
    */
    cachep = kmem_cache_zalloc(&cache_cache, gfp);
    if (!cachep)
        goto oops;

#if DEBUG
    cachep->obj_size = size;

    /*
     * Both debugging options require word-alignment which is calculated
     * into align above.
     */
    if (flags & SLAB_RED_ZONE) {
        /* add space for red zone words */
        cachep->obj_offset += sizeof(unsigned long long);
        size += 2 * sizeof(unsigned long long);
    }
    if (flags & SLAB_STORE_USER) {
        /* user store requires one word storage behind the end of
         * the real object. But if the second red zone needs to be
         * aligned to 64 bits, we must allow that much space.
         */
        if (flags & SLAB_RED_ZONE)
            size += REDZONE_ALIGN;
        else
            size += BYTES_PER_WORD;
    }
#if FORCED_DEBUG && defined(CONFIG_DEBUG_PAGEALLOC)
    if (size >= malloc_sizes[INDEX_L3 + 1].cs_size
        && cachep->obj_size > cache_line_size() && ALIGN(size, align) < PAGE_SIZE) {
        cachep->obj_offset += PAGE_SIZE - ALIGN(size, align);
        size = PAGE_SIZE;
    }
#endif
#endif

    /*
    Determine if the slab management is 'on' or 'off' slab.
    (bootstrapping cannot cope with offslab caches so don't do it too early on.)
    肯定是否將SLAB頭存儲在SLAB之上相對比較簡單,若是對象長度大於頁幀的1/8,則將頭部管理數據存儲在SLAB以外,不然存儲在SLAB之上
    */
    if ((size >= (PAGE_SIZE >> 3)) && !slab_early_init)
        /*
         * Size is large, assume best to place the slab management obj
         * off-slab (should allow better packing of objs).
         */
        flags |= CFLGS_OFF_SLAB;

    //增長對象的長度size,直至對應到以前計算的對齊值
    size = ALIGN(size, align);

    /*
    嘗試找到適當的頁數用做SLAB長度,不過小,也不太大。SLAB中對象太少會增長管理開銷,下降方法的效率,而過大的SLAB內存區則對夥伴系統不利
    針對特定的頁數,來計算對象數目、浪費的空間、着色所需的空間,循環往復,直至內核對結果滿意爲止
    */
    left_over = calculate_slab_order(cachep, size, align, flags);

    if (!cachep->num) 
    {
        printk(KERN_ERR "kmem_cache_create: couldn't create cache %s.\n", name);
        kmem_cache_free(&cache_cache, cachep);
        cachep = NULL;
        goto oops;
    }
    slab_size = ALIGN(cachep->num * sizeof(kmem_bufctl_t)
              + sizeof(struct slab), align);

    /*
     * If the slab has been placed off-slab, and we have enough space then
     * move it on-slab. This is at the expense of any extra colouring.
     */
    if (flags & CFLGS_OFF_SLAB && left_over >= slab_size) {
        flags &= ~CFLGS_OFF_SLAB;
        left_over -= slab_size;
    }

    if (flags & CFLGS_OFF_SLAB) {
        /* really off slab. No need for manual alignment */
        slab_size =
            cachep->num * sizeof(kmem_bufctl_t) + sizeof(struct slab);

#ifdef CONFIG_PAGE_POISONING
        /* If we're going to use the generic kernel_map_pages()
         * poisoning, then it's going to smash the contents of
         * the redzone and userword anyhow, so switch them off.
         */
        if (size % PAGE_SIZE == 0 && flags & SLAB_POISON)
            flags &= ~(SLAB_RED_ZONE | SLAB_STORE_USER);
#endif
    }

    cachep->colour_off = cache_line_size();
    /* Offset must be a multiple of the alignment. */
    if (cachep->colour_off < align)
        cachep->colour_off = align;
    cachep->colour = left_over / cachep->colour_off;
    cachep->slab_size = slab_size;
    cachep->flags = flags;
    cachep->gfpflags = 0;
    if (CONFIG_ZONE_DMA_FLAG && (flags & SLAB_CACHE_DMA))
        cachep->gfpflags |= GFP_DMA;
    cachep->buffer_size = size;
    cachep->reciprocal_buffer_size = reciprocal_value(size);

    if (flags & CFLGS_OFF_SLAB) {
        cachep->slabp_cache = kmem_find_general_cachep(slab_size, 0u);
        /*
         * This is a possibility for one of the malloc_sizes caches.
         * But since we go off slab only for object size greater than
         * PAGE_SIZE/8, and malloc_sizes gets created in ascending order,
         * this should not happen at all.
         * But leave a BUG_ON for some lucky dude.
         */
        BUG_ON(ZERO_OR_NULL_PTR(cachep->slabp_cache));
    }
    cachep->ctor = ctor;
    cachep->name = name;

    if (setup_cpu_cache(cachep, gfp)) {
        __kmem_cache_destroy(cachep);
        cachep = NULL;
        goto oops;
    }

    /* 
    cache setup completed, link it into the list 
    爲完成初始化,將初始化過的kmem_cache實例添加到全局鏈表,表頭爲cache_chain

    */
    list_add(&cachep->next, &cache_chain);
oops:
    if (!cachep && (flags & SLAB_PANIC))
        panic("kmem_cache_create(): failed to create slab `%s'\n",
              name);
    if (slab_is_available()) {
        mutex_unlock(&cache_chain_mutex);
        put_online_cpus();
    }
    return cachep;
}
EXPORT_SYMBOL(kmem_cache_create);

4. 分配對象

kmem_cache_alloc用於從特定的緩存獲取對象,相似於全部的malloc函數,其結果多是指向分配內存區的指針,或者分配失敗返回NULL

/*
1. cachep: 用於獲取對象的緩存
2. flags: 精確描述分配特徵的標誌變量
*/
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)

5. 緩存的增加

若是掃描了全部的SLAB仍然沒有找到空閒的對象,那麼必須使用cache_grow擴大緩存,這是一個代價較高的操做,涉及到對夥伴系統的操做

6. 釋放對象

若是一個分配的對象已經再也不須要,那麼必須使用kmem_cache_free返回給SLAB分配器

7. 銷燬緩存

若是要銷燬只包含未使用對象的一個緩存,則必須調用kmem_cache_destroy函數,該函數主要在刪除模塊時調用,此時須要將分配的內存都釋放

1. 依次掃描slabs_free鏈表上的slab,首先對每一個slab的每一個對象調用析構器函數,而後將slab的內存空間返回給夥伴系統
2. 釋放用於per-CPU緩存的內存空間
3. 從cache_cache鏈表移除相關數據

0x5: 通用緩存

若是不涉及對象緩存,而是傳統意義上的分配/釋放內存,則必須調用kmalloc/kfree函數,這兩個函數至關於用戶空間C庫malloc、free函數的內核等價物。咱們知道,kmalloc、kfree實現爲SLAB分配器的前端,其語義儘量地模仿malloc/free

1. kmalloc的實現

kmalloc的基礎是一個數組,其中是一些分別用於不一樣內存長度的SLAB緩存,數組項是cache_sizes的實例
\linux-2.6.32.63\include\linux\slab_def.h

/* Size description struct for general caches. */
struct cache_sizes 
{
    //cs_size指定了該項負責的內存區的長度,每一個長度對應於兩個SLAB緩存,其中之一提供適合DMA訪問的內存
    size_t             cs_size;
    struct kmem_cache    *cs_cachep;
#ifdef CONFIG_ZONE_DMA
    struct kmem_cache    *cs_dmacachep;
#endif
};

\linux-2.6.32.63\mm\slab.c

/*
 * These are the default caches for kmalloc. Custom caches can have other sizes.
 */
struct cache_sizes malloc_sizes[] = 
{
#define CACHE(x) { .cs_size = (x) },
#include <linux/kmalloc_sizes.h>
    CACHE(ULONG_MAX)
#undef CACHE
};
EXPORT_SYMBOL(malloc_sizes);

2. kfree的實現

\linux-2.6.32.63\mm\slab.c

/**
 * kfree - free previously allocated memory
 * @objp: pointer returned by kmalloc.
 *
 * If @objp is NULL, no operation is performed.
 *
 * Don't free memory not originally allocated by kmalloc()
 * or you will run into trouble.
 */
void kfree(const void *objp)
{
    struct kmem_cache *c;
    unsigned long flags;

    trace_kfree(_RET_IP_, objp);

    if (unlikely(ZERO_OR_NULL_PTR(objp)))
        return;
    local_irq_save(flags);
    kfree_debugcheck(objp);
    c = virt_to_cache(objp);
    debug_check_no_locks_freed(objp, obj_size(c));
    debug_check_no_obj_freed(objp, obj_size(c));
    __cache_free(c, (void *)objp);
    local_irq_restore(flags);
}
EXPORT_SYMBOL(kfree);

 

5. 處理器高速緩存和TLB控制 

高速緩存對系統總性能十分關鍵,這也是內核儘量提供其利用效率的緣由,這主要是經過在內存中巧妙地對齊內核數據,謹慎地混合使用普通函數、內聯定義、宏,也有助於從處理器汲取更高的性能
內核提供了一些命令,能夠直接做用於處理器的高速緩存和TLB,但這些命令並不是用於提供系統的效率,而是用於維護緩存內容的一致性,確保不出現不正確和過期的緩存項,例如在從一個進程的地址空間移除一個映射時,內核負責從TLB中刪除對應項,若是未能這麼作,那麼在先前被映射佔據的虛擬內存地址添加新數據時,對該地址的讀寫操做將被重定向到物理內存中不正確的地址
不一樣體系結構上,高速緩存和TLB的硬件實現千差萬別,所以內核必須創建TLB和高速緩存的一個視圖,在其中考慮到各類不一樣的硬件實現方法,還不能忽略各個體系結構的特定性質

1. TLB的語義抽象是將虛擬地址轉換爲物理地址的一種機制
2. 內核將高速緩存視爲經過虛擬地址快速訪問數據的一種機制,該機制無需訪問物理內存,數據和指令高速緩存並不老是明確區分,若是高速緩存區分數據和指令,那麼特定於體系結構的代碼負責對此進行處理

實際上沒必要要爲每種處理器類型都實現內核定義的每一個控制函數,若是不須要某個函數,其調用能夠替換爲空操做(do{}while(0)),然後由編譯器優化掉,對於高速緩存相關的操做來講,這種狀況很是常見,由於咱們知道,內核假定尋址是基於虛擬地址,那麼對於物理地址組織的高速緩存來講,問題就不存在,一般也沒必要要實現緩存控制函數
內核中各個特定於CPU的部分都必須提供下列函數(即便只是空操做),以便控制TLB和高速緩存

1. flush_tlb_all、flush_cache_all
刷出整個TLB/高速緩存,這隻在操縱內核(而非用戶空間進程的)頁表時須要,由於此類修改不只影響全部進程,並且影響系統中的全部處理器

2. flush_tlb_mm(struct mm_struct * mm)、flush_cache_mm
刷出全部屬於地址空間mm的TLB/高速緩存項

3. flush_tlb_range(struct vm_area_struct * vma, unsigned long start, unsigned long end)、flush_cache_range(vma, start, end)
刷出地址範圍vma->vm_mm中虛擬地址start、end之間的全部TLB/高速緩存項

4. flush_tlb_page(struct vm_area_struct * vma, unsigned long page)、flush_cache_page(vma, page)
刷出虛擬地址在[page, page + PAGE_SIZE]範圍內全部的TLB/高速緩存項

5. update_mmu_cache(struct vm_area_struct * vma, unsigned long address, pte_t pte)
在處理頁失效以後調用,它在處理器的內存管理單元MMU中加入信息,使得虛擬地址address由頁表項pte描述。僅當存在外部MMU時,才須要該函數,一般MMU集成在處理器內部,但有例外狀況,例如MIPS處理器具備外部MMU

內核對數據和指令高速緩存並不做區分,若是須要區分,特定於處理器的代碼可根據vm_area_struct->flags的VM_EXEC標誌位是否設置,來肯定高速緩存包含的是指令仍是數據
flush_cache_、flush_tlb_函數常常成對出現,例如,在使用fork複製進程的地址空間時
\linux-2.6.32.63\kernel\fork.c

//1. 刷出高速緩存
flush_cache_mm(oldmm);
...
//操做頁表(操做內存)
...
//3. 刷出TLB
flush_tlb_mm(oldmm);

遵照先刷出高速緩存、操做內存、刷出TLB這個順序很重要,有如下緣由

1. 若是順序反過來,那麼在TLB刷出以後,正確信息提供以前,多處理器系統中的另外一個CPU可能從進程的頁表取得錯誤的信息
2. 在刷出高速緩存時,某些體系結構須要依賴TLB中的"虛擬->物理"轉換規則(具備該性質的高速緩存稱之爲嚴格的)。flush_tlb_mm必須在flush_cache_mm以後執行,以確保這一點

有些控制函數明確地應用於數據高速緩存(flush_dcache_xxx)、或指令高速緩存(flush_icache_xxx)

1. 若是高速緩存包含幾個虛擬地址不一樣的項指向內存中的同一頁,可能會發生所謂的alias問題,flush_dcache_page(struct page * page)有助於防止該問題。在內核向頁緩存中的一頁寫入數據,或者從映射在用戶空間中的一頁讀出數據時,老是會調用該函數,這個例程使得存在alias問題的各個體系結構有機會防止問題的發生

2. 在內核向內核內存範圍(start、end之間)寫入數據,而該數據將在此後做爲代碼執行,則此時須要調用flush_icache_range(unsigned long start, unsigned long end)。該場景的一個標準示例是向內核載入模塊時,二進制數據首先複製到物理內存中,而後執行。
flush_icache_range確保在數據和指令高速緩存分別實現的狀況下,兩者彼此不發生干擾

3. flush_icache_user_range(*vma, *page, addr, len)是一個特殊的函數,用於ptrace機制
爲了將修改傳送到被調試進程的地址空間,須要使用該函數

小結

1. 在內核進入正常運做以後,內存管理分爲兩個層次處理。夥伴系統負責物理頁幀的管理,而SLAB分配器則處理小塊內存的分配,並提供了用戶層malloc函數族的內核等價物
2. 夥伴系統圍繞着由多頁組成的連續內存塊的拆分和再合併而展開,在連續內存區變爲空閒時,內核會自動注意到這一點,並在相應的分配請求出現時使用它,因爲該機制在系統長時間運行後,沒法以使人滿意的方式防止物理內存碎片發生,所以2.6以後的內核版本引入了反碎片技術,它一方面容許按頁的可移動性將其分組,另外一方面增長了一個新的虛擬內存域。兩者的實質都在於下降在大塊內存中間分配內存的概率(這是致使碎片的根本緣由),以免碎片出現
3. SLAB分配器在夥伴系統之上實現,它不只容許分配任意用途的小塊內存,還用於對常用的數據結構建立特定的緩存

 

6. 內存管理的概念

內存管理(Memory Management)是操做系統設計中最重要和最複雜的內容之一。雖然計算機硬件一直在飛速發展,內存容量也在不斷增加,可是仍然不可能將全部用戶進程和系統所須要的所有程序和數據放入主存中,因此操做系統必須將內存空間進行合理地劃分和有效地動態分配。
操做系統對內存的劃分和動態分配,就是內存管理的概念。有效的內存管理在多道程序設計中很是重要,不只方便用戶使用存儲器、提升內存利用率,還能夠經過虛擬技術從邏輯上擴充存儲器。

內存管理的功能有:

1. 內存空間的分配與回收:
由操做系統完成主存儲器空間的分配和管理,使程序員擺脫存儲分配的麻煩,提升編程效率

2. 地址轉換:
在多道程序環境下,程序中的邏輯地址與內存中的物理地址不可能一致,所以存儲管理必須提供地址變換功能,把邏輯地址轉換成相應的物理地址

3. 內存空間的擴充:
利用虛擬存儲技術或自動覆蓋技術,從邏輯上擴充內存

4. 存儲保護:
保證各道做業在各自的存儲空間內運行,互不干擾

Relevant Link:

http://see.xidian.edu.cn/cpp/html/2608.html


7. 內存覆蓋與內存交換

0x1: 內存覆蓋

早期的計算機系統中,主存容量很小,雖然主存中僅存放一道用戶程序,可是存儲空間放不下用戶進程的現象也常常發生,這一矛盾能夠用覆蓋技術來解決。

覆蓋的基本思想是:因爲程序運行時並不是任什麼時候候都要訪問程序及數據的各個部分(尤爲是大程序),所以能夠把用戶空間分紅一個固定區和若干個覆蓋區。將常常活躍的部分放在固定區,其他部分按調用關係分段。首先將那些即將要訪問的
段放入覆蓋區,其餘段放在外存中,在須要調用前,系統再將其調入覆蓋區,替換覆蓋區中原有的段。

覆蓋技術的特色是打破了必須將一個進程的所有信息裝入主存後才能運行的限制,但當同時運行程序的代碼量大於主存時仍不能運行

將內存覆蓋技術和cache更新技術進行類比,等效於對cache直接進行"簡答替換(簡單刪除)",即不採用任何的排序和選擇策略,粗暴地將cache中的某一段數據清空出去,這種方法不適合在高併發、大流量的場景下

0x2: 內存交換

交換(對換)的基本思想是:
1. 把處於等待狀態(或在CPU調度原則下被剝奪運行權利)的程序從內存移到輔存,把內存空間騰出來,這一過程又叫換出
2. 把準備好競爭CPU運行的程序從輔存移到內存,這一過程又稱爲換入 

有關交換須要注意如下幾個問題:

1. 交換須要備份存儲,一般是快速磁盤。它必須足夠大,而且提供對這些內存映像的直接訪問。
2. 爲了有效使用CPU,須要每一個進程的執行時間比交換時間長,而影響交換時間的主要是轉移時間。轉移時間與所交換的內存空間成正比。
3. 若是換出進程,必須確保該進程是徹底處於空閒狀態。
4. 交換空間一般做爲磁盤的一整塊,且獨立於文件系統,所以使用就可能很快。
5. 交換一般在有許多進程運行且內存空間吃緊時開始啓動,而系統負荷下降就暫停。
6. 普通的交換使用很少,但交換策略的某些變種在許多系統中(如UNIX系統)仍發揮做用。

須要注意的是,覆蓋技術則已成爲歷史;而交換技術在現代操做系統中仍具備較強的生命力。咱們今天在操做系統原理相關書籍上學習的相關調度策略,都是針對"交換技術"的具體實現,而不一樣策略之間的區別就在於2個問題:交換誰?什麼時候交換?

Relevant Link:

http://see.xidian.edu.cn/cpp/html/2609.html


8. 內存連續分配管理方式

Relevant Link:

http://see.xidian.edu.cn/cpp/html/2610.html

 
9. 內存非連續分配管理方式

Relevant Link:

http://see.xidian.edu.cn/cpp/html/2611.html


10. 虛擬內存的概念、特徵及其實現

Relevant Link:

http://see.xidian.edu.cn/cpp/html/2612.html


11. 請求分頁管理方式實現虛擬內存

Relevant Link:

http://see.xidian.edu.cn/cpp/html/2613.html


12. 頁面置換算法

本小節學習頁面置換算法,本質上就是在學習內存的"交換策略"

進程運行時,若其訪問的頁面不在內存而需將其調入,但內存已無空閒空間時,就須要從內存中調出一頁程序或數據,送入磁盤的對換區。(這和cache的動態更新原理是同樣的)
選擇調出頁面的算法就稱爲"頁面置換算法"。好的頁面置換算法應有較低的頁面更換頻率,也就是說,應將之後不會再訪問或者之後較長時間內不會再訪問的頁面先調出

0x1: 最佳置換算法(OPT)

最佳(Optimal, OPT)置換算法所選擇的被淘汰頁面將是之後永不使用的,或者是在最長時間內再也不被訪問的頁面,這樣能夠保證得到最低的缺頁率。但因爲人們目前沒法預知進程在內存下的若千頁面中哪一個是將來最長時間內再也不被訪問的,於是該算法沒法實現。這只是一種判斷算法的最優標準

最佳置換算法能夠用來評價其餘算法。假定系統爲某進程分配了三個物理塊,並考慮有如下頁面號引用串:

7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1

進程運行時

1. 先將7, 0, 1三個頁面依次裝入內存
2. 進程要訪問頁面2時,產生缺頁中斷,根據最佳置換算法,選擇第18次訪問才需調入的頁面7予以淘汰
3. 而後,訪問頁面0時,由於已在內存中因此沒必要產生缺頁中斷
4. 訪問頁面3時又會根據最佳置換算法將頁面1淘汰
5. ……依此類推 

能夠看到,發生缺頁中斷的次數爲9,頁面置換的次數爲6

訪問頁面 7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1
物理塊1 7 7 7 2   2   2     2     2       7    
物理塊2   0 0 0   0   4     0     0       0    
物理塊3     1 1   3   3     3     1       1    
缺頁否  √                      
0x2: 先進先出(FIFO)頁面置換算法
優先淘汰最先進入內存的頁面,亦即在內存中駐留時間最久的頁面。該算法實現簡單,只需把調入內存的頁面根據前後次序連接成隊列,設置一個指針總指向最先的頁面。
但該算法與進程實際運行時的規律不適應,由於有的頁面雖然是最先被調入內存的,可是一直在被進程訪問使用,這是程序的局部性致使的,因此,簡單地根據調入內存時間進行"內存交換",可能會帶來嚴重的"換頁抖動"

訪問頁面 7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1
物理塊1 7 7 7 2   2 2 4 4 4 0     0 0     7 7 7
物理塊2   0 0 0   3 3 3 2 2 2     1 1     1 0 0
物理塊3     1 1   1 0 0 0 3 3     3 2     2 2 1
缺頁否          
這裏仍用上面的實例,釆用FIFO算法進行頁面置換。進程訪問頁面2時,把最先進入內存的頁面7換出。而後訪問頁面3時,再把2, 0, 1中最早進入內存的頁換出。由圖FIFO算法的換頁次數明顯增長了
FIFO算法還會產生當所分配的物理塊數增大而頁故障數不減反增的異常現象,這是由 Belady於1969年發現,故稱爲Belady異常。只有FIFO算法可能出現Belady 異常,而LRU和OPT算法永遠不會出現Belady異常。
訪問頁面 1 2 3 4 1 2 5 1 2 3 4 5
物理塊1 1 1 1 4 4 4 5     ,5' 5  
物理塊2   2 2 2 1 1 1     3 3  
物理塊3     3 3 3 2 2     2 4  
缺頁否      
    1 1 1     5 5 5 5 4 4
物理塊2*   2 2 2     2 1 1 1 1 5
物理塊3*     3 3     3 3 2 2 2 2
物理塊4*       4     4 4 4 3 3 3
缺頁否      
Belady現象的緣由是FIFO算法的置換特徵與進程訪問內存的動態特徵是矛盾的,即被置換的頁面並非進程不會訪問的,於是FIFO並非一個好的置換算法

0x3: 最近最久未使用(LRU)置換算法

LRU算法是一個被普遍使用和接收的cache調度算法,它的調度思想具備較好的合理性

LRU算法的思想是:選擇最近最長時間未訪問過的頁面予以淘汰,它認爲過去一段時間內未訪問過的頁面,在最近的未來可能也不會被訪問。

回到內存交換的總原則:交換誰?什麼時候交換?對於LRU算法來講,這裏的評判標準就是"最近最長時間未使用過",如何計算這個值呢?

1. 用一個鏈表、或者具備鏈表特徵的數據結構來保存數據,取數據時只從頭部取,插入數據的時候只從尾部插入,這樣,每次數據被使用後就會從頭部取出,並插入尾部。這種策略隱含的思想就是越尾部的數據就是越最近被使用的,
頭部的都是不常常被使用的
2. 爲每一個頁面設置一個訪問字段,來記錄頁面自上次被訪問以來所經歷的時間,淘汰頁面時選擇現有頁面中值最大的予以淘汰。

這2種算法都能知足LUR的要求,再對上面的實例釆用LRU算法進行頁面置換

訪問頁面 7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1
物理塊1 7 7 7 2   2   4 4 4 0     1   1   1    
物理塊2   0 0 0   0   0 0 3 3     3   0   0    
物理塊3     1 1   3   3 2 2 2     2   2   7    
缺頁否                
進程第一次對頁面2訪問時,將最近最久未被訪問的頁面7置換出去。而後訪問頁面3時,將最近最久未使用的頁面1換出。
前5次置換的狀況與最佳置換算法相同,但兩種算法並沒有必然聯繫。實際上,LRU算法根據各頁之前的狀況,是"向前看"的,而最佳置換算法則根據各頁之後的使用狀況,是"向後看"的。
LRU性能較好,但須要寄存器和棧的硬件支持。LRU是堆棧類的算法。理論上能夠證實,堆棧類算法不可能出現Belady異常。FIFO算法基於隊列實現,不是堆棧類算法。
LRU算法編程實踐(以int爲key的CRUD操做)
#include <iostream>
#include "lru.hpp"

using namespace std;

struct stHashInfo
{
    int hints;
    time_t tmodify;
};

int main()
{
    // Create a cache
    typedef plb::LRUCacheH4<int, struct stHashInfo> lru_cache;
    lru_cache cache(5000);

    struct stHashInfo a = {5, 19910726};
    struct stHashInfo b = {8, 19910727};
    struct stHashInfo c = {12, 19910728};
    struct stHashInfo d = {22, 19910729};

    /*
    cache[1] = a;
    cache[2] = b;
    cache[3] = c;
    cache[4] = d;
    */
    cache.insert(1, a);
    cache.insert(2, b);
    cache.insert(3, c);
    cache.insert(4, d);


    cache.find(1);
    cache.find(2);
    cache.find(2);
    cache.find(2);
    cache.find(2);
    cache.find(2);
    cache.find(2);
    cache.find(1);
    cache.find(3);

    cache.itemerase(2);

    for (lru_cache::const_iterator it = cache.lru_begin(); it != cache.end(); it++)
    {
        cout << it.key() << " -> hints: " << it.value().hints << " tmodify: " << it.value().tmodify << endl;
    }

    return 0;
}

我在google code的基礎上進行了小幅度的改進,讓這個cache類庫支持增刪改查(CRUD)操做,連接在下面給出

LRU算法編程實踐(以string爲key的CRUD操做)
使用自定義的HASH函數將string的key轉化爲int型後,再插入cache中,在選取HASH函數的時候,須要注意幾點:
1. 運算量不能太大,自己cache存在的意義就是爲了減小CPU運算負載,若是由於插入key致使更多的計算量,則失去了用cache的意義了
2. HASH函數要有足夠的敏感性,對微笑的擾動要能做出較大的值變化,即須要有足夠的抗碰撞性

code

#include <iostream>
#include <string.h>
#include "lru.hpp"

using namespace std;

struct stHashInfo
{
    int hints;
    time_t tmodify;
};

uint32_t  Murmur2( const string &str)
{
    const uint8_t *key = (const uint8_t*)str.c_str();
    uint32_t len = str.length();
    uint32_t seed = 65536;
    const uint32_t m = 0x5bd1e995;
    const uint8_t r = 24;
    uint32_t h = len + seed;
    const uint8_t * data = (const uint8_t *)key;
    for ( ; len >= 4; len -= 4, data += 4 )
    {
        uint32_t k = *(uint32_t *)data * m;
        k ^= k >> r;
        k *= m;
        h = ( h * m ) ^ k;
    }

    switch ( len )
    {
        case 3: h ^= data[2] << 16;
        case 2: h ^= data[1] << 8;
        case 1: h ^= data[0];
                h *= m;
        default:;
    }

    h ^= h >> 13;
    h *= m;
    h ^= h >> 15;

    return h;
}


int main()
{
    // Create a cache
    typedef plb::LRUCacheH4<int, struct stHashInfo> lru_cache;
    lru_cache cache(5000);

    struct stHashInfo a = {5, 19910726};
    struct stHashInfo b = {8, 19910727};
    struct stHashInfo c = {12, 19910728};
    struct stHashInfo d = {22, 19910729};

    /*
    cache[1] = a;
    cache[2] = b;
    cache[3] = c;
    cache[4] = d;
    */

    cache.insert(Murmur2(string("111")), a);
    cache.insert(Murmur2(string("222")), b);
    cache.insert(Murmur2(string("333")), c);
    cache.insert(Murmur2(string("444")), d);

    cache.find(Murmur2(string("444")));
    cache.find(Murmur2(string("111")));
    cache.find(Murmur2(string("111")));
    cache.find(Murmur2(string("222")));

    cache.itemerase(2);

    for (lru_cache::const_iterator it = cache.lru_begin(); it != cache.end(); it++)
    {
        cout << it.key() << " -> hints: " << it.value().hints << " tmodify: " << it.value().tmodify << endl;
    }

    return 0;
}

Relevant Link:
https://code.google.com/p/lru-cache-cpp/
http://files.cnblogs.com/LittleHann/lru.rar
http://floodyberry.com/noncryptohashzoo/Murmur2.html

0x4: 時鐘(CLOCK)置換算法

LRU算法的性能接近於OPT,可是實現起來比較困難,且開銷大;FIFO算法實現簡單,但性能差。爲了得到一個平衡,操做系統的設計者試圖用比較小的開銷接近LRU的性能,這類算法都是CLOCK算法的變體

1. 簡單的CLOCK算法是給每一幀關聯一個附加位,稱爲使用位
2. 當某一頁首次裝入主存時,該幀的使用位設置爲1
3. 當該頁隨後再被訪問到時,它的使用位也被置爲1
4. 對於頁替換算法,用於替換的候選幀集合看作一個循環緩衝區,而且有一個指針與之相關聯
5. 當某一頁被替換時,該指針被設置成指向緩衝區中的下一幀
6. 當須要替換一頁時,操做系統掃描緩衝區,以查找使用位被置爲0的一幀。每當遇到一個使用位爲1的幀時,操做系統就將該位從新置爲0
7. 若是在這個過程開始時,緩衝區中全部幀的使用位均爲0,則選擇遇到的第一個幀替換
8. 若是全部幀的使用位均爲1,則指針在緩衝區中完整地循環一週,把全部使用位都置爲0,而且停留在最初的位置上,替換該幀中的頁(交換是必需要作的)

因爲該算法循環地檢查各頁面的狀況,故稱爲CLOCK算法,又稱爲最近未用(Not Recently Used, NRU)算法

CLOCK算法的性能比較接近LRU,而經過增長使用的位數目,能夠使得CLOCK算法更加高效。在使用位的基礎上再增長一個"修改位",則獲得改進型的CLOCK置換算法。這樣,每一幀都處於如下四種狀況之一:

1. 最近未被訪問,也未被修改(u=0, m=0)。
2. 最近被訪問,但未被修改(u=1, m=0)。
3. 最近未被訪問,但被修改(u=0, m=1)。
4. 最近被訪問,被修改(u=1, m=1)。

算法執行以下操做步驟:

1. 從指針的當前位置開始,掃描幀緩衝區。在此次掃描過程當中,對使用位不作任何修改。選擇遇到的第一個幀(u=0, m=0)用於替換。
2. 若是第1)步失敗,則從新掃描,查找(u=0, m=1)的幀。選擇遇到的第一個這樣的幀用於替換。在這個掃描過程當中,對每一個跳過的幀,把它的使用位設置成0。
3. 若是第2)步失敗,指針將回到它的最初位置,而且集合中全部幀的使用位均爲0。重複第1步,而且若是有必要,重複第2步。這樣將能夠找到供替換的幀。

改進型的CLOCK算法優於簡單CLOCK算法之處在於替換時首選沒有變化的頁。因爲修改過的頁在被替換以前必須寫回,於是這樣作會節省時間

Relevant Link:

http://see.xidian.edu.cn/cpp/html/2614.html
http://blog.csdn.net/ojshilu/article/details/22955741


13. 頁面分配策略

Relevant Link:

http://see.xidian.edu.cn/cpp/html/2615.html


14. 頁面抖動和工做集

Relevant Link:

http://see.xidian.edu.cn/cpp/html/2616.html

 

15. 缺頁異常的處理

在實際須要某個虛擬內存區域的數據以前,虛擬和物理內存之間的關聯不會創建。若是進程訪問的虛擬地址空間部分還沒有與頁幀關聯,處理器會自動觸發一個"缺頁異常",內核必須處理此異常

 

16. 堆與內存管理

相對於棧而言,對內存面臨着更復雜的行爲模式,在任意時刻,程序可能發出請求,要麼申請一段內存,要麼釋放一段已經申請過的內存,並且申請的大小從幾個字節到數GB都是有可能的,所以,堆的管理顯得較爲複雜

0x1: Linux堆簡介

僅僅使用棧對現代程序設計來講是遠遠不夠的,由於棧上的數據在函數返回的時候就會被釋放掉,因此沒法將數據傳遞至函數外部。而全局變量沒有辦法動態地產生,只能在編譯的時候定義,不少狀況下缺少表現力,在這種狀況下,堆(Heap)是惟一的選擇

堆是一塊巨大的內存空間,經常佔據整個虛擬空間的絕大部分,在這片空間裏,程序能夠請求一塊連續內存,並自由的使用,這塊內存在程序主動放棄以前都會一直有效

0x2: 堆操做系統調用

堆內存是操做系統的一種資源,內核負責總管全部進程的地址空間。Linux提供了兩種堆空間分配的方式,即兩個系統調用

1. brk()
#include <unistd.h>
int brk(void *addr);
brk的做用實際上就是設置進程數據段的結束地址,即它能夠擴大或者縮小數據段(Linux下數據段和BSS合併在一塊兒統稱爲數據段)。若是咱們將數據段的結束地址向高地址移動,那麼擴大的那部分空間就能夠被程序使用,把這塊空間做爲堆空間是最多見的作法之一

void *sbrk(intptr_t increment);
Glibc中還有一個函數叫sbrk,它的功能和brk相似,只是參數和返回值不一樣,sbrk以一個增量(increment)做爲參數,即須要增長(負數爲減少)的空間大小,返回值是增長(或減小)後數據段結束地址。sbrk其實是對brk系統調用的包裝(wrapper function),它在內部是基於brk()實現的

2. mmap()
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
/*
參數:
1. addr: 須要申請空間的起始地址,若是起始地址爲0,則Linux系統會自動挑選合適的起始地址
2. prot: 申請空間的權限
    1) 可讀
    2) 可寫
    3) 可執行
3. flags: 映射類型
    1) 文件映射
    2) 匿名空間
4. fd: 在進行文件映射時指定文件描述符
*/
int munmap(void *addr, size_t length);
mmap()的做用和windows系統習的VirtualAlloc()很相似,它的做用就是向操做系統申請一段"虛擬地址空間",固然這塊虛擬地址空間能夠映射到某個文件,當它不將地址映射到某個文件時,咱們稱這塊空間爲匿名(Anonymous)空間,匿名空間就能夠拿來做爲堆空間

值得注意的是,mmap()和windows下的VirtualAlloc()相似,它們都是系統虛擬空間申請函數,它們申請的空間的起始地址(addr)和大小(length)都必須是系統頁的大小的整數倍。對於字節數很小的請求若是也使用mmap的話,會形成大量的空間浪費的

0x3: Glibc對堆內存的分配和管理

對於操做系統來講,全部進程的堆內存都是經過內核來統一管理的,可是對於Ring3的應用程序來講,若是每次進行申請或者釋放堆空間都要進行系統調用,會帶來很大的性能開銷,因此比較好的作法是由程序的運行庫(Glibc庫)向操做系統一次性申請一塊"適當大小"的堆空間,而後由程序運行庫本身管理這塊空間
運行庫至關於向操做系統"批發"了一塊較大的堆空間,而後再"零售"分配給程序用。當所有分配完或程序有大量的內存需求時,再根據實際需求向操做系統再次申請分配。運行庫在向程序"零售"分配堆空間時,必須對這塊堆空間進行有效地管理,這個算法就是"堆的分配算法"

glibc的malloc函數的處理邏輯

\glibc-2.18\malloc\malloc.c

1. 對於小於128KB的請求,直接在現有的堆空間裏,按照堆分配算法爲它分配一塊空間並返回
2. 對於大於128KB的請求,它會使用mmap()系統調用分配一塊匿名空間,而後在這個匿名空間中爲用戶分配空間

code

/* malloc example: random string generator*/
#include <stdio.h>      /* printf, scanf, NULL */
#include <stdlib.h>     /* malloc, free, rand */

int main ()
{
    int i,n;
    char * buffer;

    printf ("How long do you want the string? ");
    scanf ("%d", &i);

    buffer = (char*) malloc (i+1);
    if (buffer==NULL) exit (1);

    for (n=0; n<i; n++)
    {
        buffer[n]=rand()%26+'a';
    } 
    buffer[i]='\0';

    printf ("Random string: %s\n",buffer);
    free (buffer);

    return 0;
}

0x4: C++的運算符對堆內存的分配和管理: new/delete

對於非內部數據類的對象而言,光用maloc/free沒法知足動態對象的要求。對象在建立的同時要自動執行構造函數,對象消亡以前要自動執行析構函數。因爲malloc/free是庫函數而不是運算符,不在編譯器控制權限以內,不可以把執行構造函數和析構函數的任務強加給malloc/free

int *p1 = (int *)malloc(sizeof(int) * length);
==
int *p2 = new int[length];

使用new/delete和使用malloc/free相比,有以下特性

1. new內置了sizeof、類型轉換和類型安全檢查功能

2. 對於非內部數據類型的對象而言,new 在建立動態對象的同時完成了初始化工做。若是對象有多個構造函數,那麼new的語句也能夠有多種形式 

3. 若是用new建立對象數組,那麼只能使用對象的無參數構造函數 
/*
OK: Obj *objects = new Obj[100];    // 建立100 個動態對象
NO: Obj *objects = new Obj[100](1);    // 建立100 個動態對象的同時賦初值1
*/

4. 在用delete釋放對象數組時,留意不要丟了符號'[]' 
/*
OK: delete []objects;  
NO: delete objects;    //至關於delete objects[0],漏掉了另外99個對象。
*/

5. new自動計算須要分配的空間,而malloc須要手工計算字節數

6. new是類型安全的,而malloc不是 
new operator 由兩步構成,分別是 operator new 和 construct
/*
OK: int* p = new float[2];        // 編譯時指出錯誤
NO: int* p = malloc(2*sizeof(float));    // 編譯時沒法指出錯誤
*/
          
7. operator new對應於malloc,但operator new能夠重載,能夠自定義內存分配策略,甚至不作內存分配,甚至分配到非內存設備上。而malloc無能爲力

8. new將調用constructor,而malloc不能;delete將調用destructor,而free不能 

9. malloc/free要庫文件支持,new/delete則不要

Relevant Link:

http://blog.csdn.net/hackbuteer1/article/details/6789164

0x5: 堆分配算法

在大多數狀況下,應用程序使用Glibc庫的malloc/free進行堆內存的申請和釋放(固然,應用程序也能夠使用原始的方法直接使用mmap、brk系統調用進行堆空間的申請),對於Glibc庫來講,如何管理一大塊連續的內存空間,可以按照需求分配、釋放其中的空間,這就是堆分配算法

1. 空閒列表

空閒鏈表(Free List)的方式實際上就是把堆中各個空閒的塊按照鏈表的方式鏈接起來

1. 當用戶請求一塊內存空間時,能夠遍歷整個列表,直到找到合適大小的塊而且將它拆分
2. 當用戶釋放空間時,將它合併到空閒鏈表中

空閒鏈表是這樣一種結構,在堆裏的每一個空閒空間的開頭(或結尾)有一個頭(header),頭結構裏記錄了"上一個"(prev)和"下一個"(next)空閒的地址,也就是說,全部空閒的塊造成了一個鏈表

基於這種結構,在請求分配空間時

1. 首先在空閒鏈表裏查找足夠容納請求大小的一個空閒塊
2. 而後將這個塊分爲2部分
    1) 一部分爲程序請求的空間
    2) 另外一部分爲剩餘下來的空閒空間
3. 將鏈表裏對應原來空閒塊的結構更新爲新的剩下的空閒塊
4. 若是剩下的空閒塊大小爲0,則直接將這個結構從鏈表裏刪除

2. 位圖

針對空閒鏈表的弊端,有一種更加穩健的分配方式,即位圖(bitmap),核心思想是

1. 將整個堆內存劃分爲大量的塊(block),每一個塊的大小相同 
2. 當用戶請求內存的時候,老是分配數個塊的空間給用戶
    1) 第一個塊: 已分配區域的頭(head)
    2) 其他的塊: 已分配區域的主體(body)
3. 咱們能夠使用一個整數數組來記錄塊的使用狀況,因爲每一個塊只有頭/主體/空閒這3種狀態,所以僅僅須要兩位便可表示一個塊,由於稱爲"位圖"
//位圖(bitmap)不位於這全部內存塊中,而是保存其餘地方的一塊獨立的內存區域
4. 位圖(bitmap)的目的是爲實際的堆內存維護一個描述分配狀態的元數據(經常是數組形式),經過對所有目標內存地址創建一一對應的關係,經過bit的方式進行狀態描述

位圖(bitmap)的優缺點以下

1. 速度快
整個堆的空閒信息存儲在一個數組內,所以訪問該數組時cache容易命中

2. 穩定性好
爲了不用戶越界讀寫破壞數據,咱們只須要對佔用空間較小的位圖進行備份便可

3. 塊不須要額外信息,易於管理

4. 分配內存的時候容易產生碎片

5. 若是堆很大,塊很小(這樣可能減少碎片),那麼位圖會很大,這樣可能會致使失去cache命中率高的優點,並且也會浪費必定的空間。針對這種狀況,咱們能夠使用多級的位圖

3. 對象池

對象池的思路很簡單,若是每一次分配的空間大小都同樣,那麼就能夠按照這個每次請求分配的大小做爲一個單位,把整個堆空間劃分爲大小的小塊,每次請求的時候只須要找到一個小塊便可
對象池的管理方法能夠採用空閒鏈表,也能夠採用位圖,與它們的區別僅僅在於對象池假定了每次請求都是一個固定的大小

4. 總結

在真實場景中,堆的分配算法每每是採起多種算法複合而成的,例如glibc

1. 小於64byte的空間申請: 採用相似對象池的算法
2. 大於64byte、小於512byte的空間申請: 採起空閒鏈表或者位圖
3. 大於512byte的空間申請: 最佳適配算法
4. 大於128KB的空間申請: 使用mmap機制直接向操做系統申請空間

 

Copyright (c) 2014 LittleHann All rights reserved

相關文章
相關標籤/搜索