一步一圖帶你深入理解 Linux 物理內存管理(下)

我們接著上半部分 《一步一圖帶你深入理解 Linux 物理內存管理(上)》 繼續 Linux 物理內存管理的下半部分~~~

5.7 物理內存區域中的冷熱頁

之前筆者在《一文聊透對象在JVM中的內存布局,以及內存對齊和壓縮指針的原理及應用》 一文中為大家介紹 CPU 的高速緩存時曾提到過,根據摩爾定律:芯片中的晶體管數量每隔 18 個月就會翻一番。導致 CPU 的性能和處理速度變得越來越快,而提升 CPU 的運行速度比提升內存的運行速度要容易和便宜的多,所以就導致了 CPU 與內存之間的速度差距越來越大。

CPU 與 內存之間的速度差異到底有多大呢? 我們知道寄存器是離 CPU 最近的,CPU 在訪問寄存器的時候速度近乎于 0 個時鐘周期,訪問速度最快,基本沒有時延。而訪問內存則需要 50 - 200 個時鐘周期。

所以為了彌補 CPU 與內存之間巨大的速度差異,提高CPU的處理效率和吞吐,于是我們引入了 L1 , L2 , L3 高速緩存集成到 CPU 中。CPU 訪問高速緩存僅需要用到 1 - 30 個時鐘周期,CPU 中的高速緩存是對內存熱點數據的一個緩存。

CPU緩存結構.png

CPU 訪問高速緩存的速度比訪問內存的速度快大約10倍,引入高速緩存的目的在于消除CPU與內存之間的速度差距,CPU 用高速緩存來用來存放內存中的熱點數據。

另外我們根據程序的時間局部性原理可以知道,內存的數據一旦被訪問,那么它很有可能在短期內被再次訪問,如果我們把經常訪問的物理內存頁緩存在 CPU 的高速緩存中,那么當進程再次訪問的時候就會直接命中 CPU 的高速緩存,避免了進一步對內存的訪問,極大提升了應用程序的性能。

程序局部性原理表現為:時間局部性和空間局部性。時間局部性是指如果程序中的某條指令一旦執行,則不久之后該指令可能再次被執行;如果某塊數據被訪問,則不久之后該數據可能再次被訪問。空間局部性是指一旦程序訪問了某個存儲單元,則不久之后,其附近的存儲單元也將被訪問。

本文我們的主題是 Linux 物理內存的管理,那么在 NUMA 內存架構下,這些 NUMA 節點中的物理內存區域 zone 管理的這些物理內存頁,哪些是在 CPU 的高速緩存中?哪些又不在 CPU 的高速緩存中呢?內核如何來管理這些加載進 CPU 高速緩存中的物理內存頁呢?

image.png

本小節標題中所謂的熱頁就是已經加載進 CPU 高速緩存中的物理內存頁,所謂的冷頁就是還未加載進 CPU 高速緩存中的物理內存頁,冷頁是熱頁的后備選項。

筆者先以內核版本 2.6.25 之前的冷熱頁相關的管理邏輯為大家講解,因為這個版本的邏輯比較直觀,大家更容易理解。在這個基礎之上,筆者會在介紹內核 5.0 版本對于冷熱頁管理的邏輯,差別不是很大。

struct zone {
    struct per_cpu_pageset  pageset[NR_CPUS];
}

在 2.6.25 版本之前的內核源碼中,物理內存區域 struct zone 包含了一個 struct per_cpu_pageset 類型的數組 pageset。其中內核關于冷熱頁的管理全部封裝在 struct per_cpu_pageset 結構中。

因為每個 CPU 都有自己獨立的高速緩存,所以每個 CPU 對應一個 per_cpu_pageset 結構,pageset 數組容量 NR_CPUS 是一個可以在編譯期間配置的宏常數,表示內核可以支持的最大 CPU個數,注意該值并不是系統實際存在的 CPU 數量。

在 NUMA 內存架構下,每個物理內存區域都是屬于一個特定的 NUMA 節點,NUMA 節點中包含了一個或者多個 CPU,NUMA 節點中的每個內存區域會關聯到一個特定的 CPU 上,但 struct zone 結構中的 pageset 數組包含的是系統中所有 CPU 的高速緩存頁。

因為雖然一個內存區域關聯到了 NUMA 節點中的一個特定 CPU 上,但是其他CPU 依然可以訪問該內存區域中的物理內存頁,因此其他 CPU 上的高速緩存仍然可以包含該內存區域中的物理內存頁。

每個 CPU 都可以訪問系統中的所有物理內存頁,盡管訪問速度不同(這在前邊我們介紹 NUMA 架構的時候已經介紹過),因此特定的物理內存區域 struct zone 不僅要考慮到所屬 NUMA 節點中相關的 CPU,還需要照顧到系統中的其他 CPU。

在表示每個 CPU 高速緩存結構 struct per_cpu_pageset 中有一個 struct per_cpu_pages 類型的數組 pcp,容量為 2。 數組 pcp 索引 0 表示該內存區域加載進 CPU 高速緩存的熱頁集合,索引 1 表示該內存區域中還未加載進 CPU 高速緩存的冷頁集合。

struct per_cpu_pageset {
    struct per_cpu_pages pcp[2];    /* 0: hot.  1: cold */
}

struct per_cpu_pages 結構則是最終用于管理 CPU 高速緩存中的熱頁,冷頁集合的數據結構:

struct per_cpu_pages {
    int count;      /* number of pages in the list */
    int high;       /* high watermark, emptying needed */
    int batch;      /* chunk size for buddy add/remove */
    struct list_head list;  /* the list of pages */
};
  • int count :表示集合中包含的物理頁數量,如果該結構是熱頁集合,則表示加載進 CPU 高速緩存中的物理頁面個數。

  • struct list_head list :該 list 是一個雙向鏈表,保存了當前 CPU 的熱頁或者冷頁。

  • int batch:每次批量向 CPU 高速緩存填充或者釋放的物理頁面個數。

  • int high:如果集合中頁面的數量 count 值超過了 high 的值,那么表示 list 中的頁面太多了,內核會從高速緩存中釋放 batch 個頁面到物理內存區域中的伙伴系統中。

  • int low : 在之前更老的版本中,per_cpu_pages 結構還定義了一個 low 下限值,如果 count 低于 low 的值,那么內核會從伙伴系統中申請 batch 個頁面填充至當前 CPU 的高速緩存中。之后的版本中取消了 low ,內核對容量過低的頁面集合并沒有顯示的使用水位值 low,當列表中沒有其他成員時,內核會重新填充高速緩存。

以上則是內核版本 2.6.25 之前管理 CPU 高速緩存冷熱頁的相關數據結構,我們看到在 2.6.25 之前,內核是使用兩個 per_cpu_pages 結構來分別管理冷頁和熱頁集合的

后來內核開發人員通過測試發現,用兩個列表來管理冷熱頁,并不會比用一個列表集中管理冷熱頁帶來任何的實質性好處,因此在內核版本 2.6.25 之后,將冷頁和熱頁的管理合并在了一個列表中,熱頁放在列表的頭部,冷頁放在列表的尾部。

在內核 5.0 的版本中, struct zone 結構中去掉了原來使用 struct per_cpu_pageset 數,因為 struct per_cpu_pageset 結構中分別管理了冷頁和熱頁。

struct zone {
    struct per_cpu_pages    __percpu *per_cpu_pageset;

    int pageset_high;
    int pageset_batch;

} ____cacheline_internodealigned_in_smp;

直接使用 struct per_cpu_pages 結構的鏈表來集中管理系統中所有 CPU 高速緩存冷熱頁。

struct per_cpu_pages {
    int count;      /* number of pages in the list */
    int high;       /* high watermark, emptying needed */
    int batch;      /* chunk size for buddy add/remove */
        
        .............省略............

    /* Lists of pages, one per migrate type stored on the pcp-lists */
    struct list_head lists[NR_PCP_LISTS];
};

前面我們提到,內核為了最大程度的防止內存碎片,將物理內存頁面按照是否可遷移的特性分為了多種遷移類型:可遷移,可回收,不可遷移。在 struct per_cpu_pages 結構中,每一種遷移類型都會對應一個冷熱頁鏈表。

6. 內核如何描述物理內存頁

image.png

經過前邊幾個小節的介紹,我想大家現在應該對 Linux 內核整個內存管理框架有了一個總體上的認識。

如上圖所示,在 NUMA 架構下內存被劃分成了一個一個的內存節點(NUMA Node),在每個 NUMA 節點中,內核又根據節點內物理內存的功能用途不同,將 NUMA 節點內的物理內存劃分為四個物理內存區域分別是:ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGHMEM。其中 ZONE_MOVABLE 區域是邏輯上的劃分,主要是為了防止內存碎片和支持內存的熱插拔。

物理內存區域中管理的就是物理內存頁( Linux 內存管理的最小單位),前面我們介紹的內核對物理內存的換入,換出,回收,內存映射等操作的單位就是頁。內核為每一個物理內存區域分配了一個伙伴系統,用于管理該物理內存區域下所有物理內存頁面的分配和釋放。

Linux 默認支持的物理內存頁大小為 4KB,在 64 位體系結構中還可以支持 8KB,有的處理器還可以支持 4MB,支持物理地址擴展 PAE 機制的處理器上還可以支持 2MB。

那么 Linux 為什么會默認采用 4KB 作為標準物理內存頁的大小呢

首先關于物理頁面的大小,Linux 規定必須是 2 的整數次冪,因為 2 的整數次冪可以將一些數學運算轉換為移位操作,比如乘除運算可以通過移位操作來實現,這樣效率更高。

那么系統支持 4KB,8KB,2MB,4MB 等大小的物理頁面,它們都是 2 的整數次冪,為啥偏偏要選 4KB 呢?

因為前面提到,在內存緊張的時候,內核會將不經常使用到的物理頁面進行換入換出等操作,還有在內存與文件映射的場景下,都會涉及到與磁盤的交互,數據在磁盤中組織形式也是根據一個磁盤塊一個磁盤塊來管理的,4kB 和 4MB 都是磁盤塊大小的整數倍,但在大多數情況下,內存與磁盤之間傳輸小塊數據時會更加的高效,所以綜上所述內核會采用 4KB 作為默認物理內存頁大小。


假設我們有 4G 大小的物理內存,每個物理內存頁大小為 4K,那么這 4G 的物理內存會被內核劃分為 1M 個物理內存頁,內核使用一個 struct page 的結構體來描述物理內存頁,而每個 struct page 結構體占用內存大小為 40 字節,那么內核就需要用額外的 40 * 1M = 40M 的內存大小來描述物理內存頁。

對于 4G 物理內存而言,這額外的 40M 內存占比相對較小,這個代價勉強可以接受,但是對內存錙銖必較的內核來說,還是會盡最大努力想盡一切辦法來控制 struct page 結構體的大小。

因為對于 4G 的物理內存來說,內核就需要使用 1M 個物理頁面來管理,1M 個物理頁的數量已經是非常龐大的了,因此在后續的內核迭代中,對于 struct page 結構的任何微小改動,都可能導致用于管理物理內存頁的 struct page 實例所需要的內存暴漲。

回想一下我們經歷過的很多復雜業務系統,由于業務邏輯已經非常復雜,在加上業務版本日積月累的迭代,整個業務系統已經變得異常復雜,在這種類型的業務系統中,我們經常會使用一個非常龐大的類來包裝全量的業務響應信息用以應對各種復雜的場景,但是這個類已經包含了太多太多的業務字段了,而且這些業務字段在有的場景中會用到,在有的場景中又不會用到,后面還可能繼續臨時增加很多字段。系統的維護就這樣變得越來越困難。

相比上面業務系統開發中隨意地增加改動類中的字段,在內核中肯定是不會允許這樣的行為發生的。struct page 結構是內核中訪問最為頻繁的一個結構體,就好比是 Linux 世界里最繁華的地段,在這個最繁華的地段租間房子,那租金可謂是相當的高,同樣的道理,內核在 struct page 結構體中增加一個字段的代價也是非常之大,該結構體中每個字段中的每個比特,內核用的都是淋漓盡致。

但是 struct page 結構同樣會面臨很多復雜的場景,結構體中的某些字段在某些場景下有用,而在另外的場景下卻沒有用,而內核又不可能像業務系統開發那樣隨意地為 struct page 結構增加字段,那么內核該如何應對這種情況呢?

下面我們即將會看到 struct page 結構體里包含了大量的 union 結構,而 union 結構在 C 語言中被用于同一塊內存根據不同場景保存不同類型數據的一種方式。內核之所以在 struct page 結構中使用 union,是因為一個物理內存頁面在內核中的使用場景和使用方式是多種多樣的。在這多種場景下,利用 union 盡最大可能使 struct page 的內存占用保持在一個較低的水平。

struct page 結構可謂是內核中最為繁雜的一個結構體,應用在內核中的各種功能場景下,在本小節中一一解釋清楚各個字段的含義是不現實的,下面筆者只會列舉 struct page 中最為常用的幾個字段,剩下的字段筆者會在后續相關文章中專門介紹。


struct page {
    // 存儲 page 的定位信息以及相關標志位
    unsigned long flags;        

    union {
        struct {    /* Page cache and anonymous pages */
            // 用來指向物理頁 page 被放置在了哪個 lru 鏈表上
            struct list_head lru;
            // 如果 page 為文件頁的話,低位為0,指向 page 所在的 page cache
            // 如果 page 為匿名頁的話,低位為1,指向其對應虛擬地址空間的匿名映射區 anon_vma
            struct address_space *mapping;
            // 如果 page 為文件頁的話,index 為 page 在 page cache 中的索引
            // 如果 page 為匿名頁的話,表示匿名頁在對應進程虛擬內存區域 VMA 中的偏移
            pgoff_t index;
            // 在不同場景下,private 指向的場景信息不同
            unsigned long private;
        };
        
        struct {    /* slab, slob and slub */
            union {
                // 用于指定當前 page 位于 slab 中的哪個具體管理鏈表上。
                struct list_head slab_list;
                struct {
                    // 當 page 位于 slab 結構中的某個管理鏈表上時,next 指針用于指向鏈表中的下一個 page
                    struct page *next;
#ifdef CONFIG_64BIT
                    // 表示 slab 中總共擁有的 page 個數
                    int pages;  
                    // 表示 slab 中擁有的特定類型的對象個數
                    int pobjects;   
#else
                    short int pages;
                    short int pobjects;
#endif
                };
            };
            // 用于指向當前 page 所屬的 slab 管理結構
            struct kmem_cache *slab_cache; 
        
            // 指向 page 中的第一個未分配出去的空閑對象
            void *freelist;     
            union {
                // 指向 page 中的第一個對象
                void *s_mem;    
                struct {            /* SLUB */
                    // 表示 slab 中已經被分配出去的對象個數
                    unsigned inuse:16;
                    // slab 中所有的對象個數
                    unsigned objects:15;
                    // 當前內存頁 page 被 slab 放置在 CPU 本地緩存列表中,frozen = 1,否則 frozen = 0
                    unsigned frozen:1;
                };
            };
        };
        struct {    /* 復合頁 compound page 相關*/
            // 復合頁的尾頁指向首頁
            unsigned long compound_head;    
            // 用于釋放復合頁的析構函數,保存在首頁中
            unsigned char compound_dtor;
            // 該復合頁有多少個 page 組成
            unsigned char compound_order;
            // 該復合頁被多少個進程使用,內存頁反向映射的概念,首頁中保存
            atomic_t compound_mapcount;
        };

        // 表示 slab 中需要釋放回收的對象鏈表
        struct rcu_head rcu_head;
    };

    union {     /* This union is 4 bytes in size. */
        // 表示該 page 映射了多少個進程的虛擬內存空間,一個 page 可以被多個進程映射
        atomic_t _mapcount;

    };

    // 內核中引用該物理頁的次數,表示該物理頁的活躍程度。
    atomic_t _refcount;

#if defined(WANT_PAGE_VIRTUAL)
    void *virtual;  // 內存頁對應的虛擬內存地址
#endif /* WANT_PAGE_VIRTUAL */

} _struct_page_alignment;

下面筆者就來為大家介紹下 struct page 結構在不同場景下的使用方式:

第一種使用方式是內核直接分配使用一整頁的物理內存,在《5.2 物理內存區域中的水位線》小節中我們提到,內核中的物理內存頁有兩種類型,分別用于不同的場景:

  1. 一種是匿名頁,匿名頁背后并沒有一個磁盤中的文件作為數據來源,匿名頁中的數據都是通過進程運行過程中產生的,匿名頁直接和進程虛擬地址空間建立映射供進程使用。

  2. 另外一種是文件頁,文件頁中的數據來自于磁盤中的文件,文件頁需要先關聯一個磁盤中的文件,然后再和進程虛擬地址空間建立映射供進程使用,使得進程可以通過操作虛擬內存實現對文件的操作,這就是我們常說的內存文件映射。

struct page {
    // 如果 page 為文件頁的話,低位為0,指向 page 所在的 page cache
    // 如果 page 為匿名頁的話,低位為1,指向其對應虛擬地址空間的匿名映射區 anon_vma
    struct address_space *mapping;
    // 如果 page 為文件頁的話,index 為 page 在 page cache 中的索引
    // 如果 page 為匿名頁的話,表示匿名頁在對應進程虛擬內存區域 VMA 中的偏移
    pgoff_t index; 
}

我們首先來介紹下 struct page 結構中的 struct address_space *mapping 字段。提到 struct address_space 結構,如果大家之前看過筆者 《從 Linux 內核角度探秘 JDK NIO 文件讀寫本質》 這篇文章的話,一定不會對 struct address_space 感到陌生。

image.png

在內核中每個文件都會有一個屬于自己的 page cache(頁高速緩存),頁高速緩存在內核中的結構體就是這個 struct address_space。它被文件的 inode 所持有。

如果當前物理內存頁 struct page 是一個文件頁的話,那么 mapping 指針的最低位會被設置為 0 ,指向該內存頁關聯文件的 struct address_space(頁高速緩存),pgoff_t index 字段表示該內存頁 page 在頁高速緩存 page cache 中的 index 索引。內核會利用這個 index 字段從 page cache 中查找該物理內存頁,

image.png

同時該 pgoff_t index 字段也表示該內存頁中的文件數據在文件內部的偏移 offset。偏移單位為 page size。

對相關查找細節感興趣的同學可以在回看下筆者 《從 Linux 內核角度探秘 JDK NIO 文件讀寫本質》 文章中的《8. page cache 中查找緩存頁》小節。

如果當前物理內存頁 struct page 是一個匿名頁的話,那么 mapping 指針的最低位會被設置為 1 , 指向該匿名頁在進程虛擬內存空間中的匿名映射區域 struct anon_vma 結構(每個匿名頁對應唯一的 anon_vma 結構),用于物理內存到虛擬內存的反向映射。

6.1 匿名頁的反向映射

我們通常所說的內存映射是正向映射,即從虛擬內存到物理內存的映射。而反向映射則是從物理內存到虛擬內存的映射,用于當某個物理內存頁需要進行回收或遷移時,此時需要去找到這個物理頁被映射到了哪些進程的虛擬地址空間中,并斷開它們之間的映射。

在沒有反向映射的機制前,需要去遍歷所有進程的虛擬地址空間中的映射頁表,這個效率顯然是很低下的。有了反向映射機制之后內核就可以直接找到該物理內存頁到所有進程映射的虛擬地址空間 VMA ,并從 VMA 使用的進程頁表中取消映射,

談到 VMA 大家一定不會感到陌生,VMA 相關的內容筆者在 《深入理解 Linux 虛擬內存管理》 這篇文章中詳細的介紹過。

如下圖所示,進程的虛擬內存空間在內核中使用 struct mm_struct 結構表示,進程的虛擬內存空間包含了一段一段的虛擬內存區域 VMA,比如我們經常接觸到的堆,棧。內核中使用 struct vm_area_struct 結構來描述這些虛擬內存區域。

image.png

這里筆者只列舉出 struct vm_area_struct 結構中與匿名頁反向映射相關的字段屬性:

struct vm_area_struct {  

    struct list_head anon_vma_chain;
    struct anon_vma *anon_vma;   
}

這里大家可能會感到好奇,既然內核中有了 struct vm_area_struct 結構來描述虛擬內存區域,那不管是文件頁也好,還是匿名頁也好,都可以使用 struct vm_area_struct 結構體來進行描述,這里為什么有會出現 struct anon_vma 結構和 struct anon_vma_chain 結構?這兩個結構到底是干嘛的?如何利用它倆來完成匿名內存頁的反向映射呢?

根據前幾篇文章的內容我們知道,進程利用 fork 系統調用創建子進程的時候,內核會將父進程的虛擬內存空間相關的內容拷貝到子進程的虛擬內存空間中,此時子進程的虛擬內存空間和父進程的虛擬內存空間是一模一樣的,其中虛擬內存空間中映射的物理內存頁也是一樣的,在內核中都是同一份,在父進程和子進程之間共享(包括 anon_vma 和 anon_vma_chain)。

當進程在向內核申請內存的時候,內核首先會為進程申請的這塊內存創建初始化一段虛擬內存區域 struct vm_area_struct 結構,但是并不會為其分配真正的物理內存。

當進程開始訪問這段虛擬內存時,內核會產生缺頁中斷,在缺頁中斷處理函數中才會去真正的分配物理內存(這時才會為子進程創建自己的 anon_vma 和 anon_vma_chain),并建立虛擬內存與物理內存之間的映射關系(正向映射)。

static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
        .............

    if (!vmf->pte) {
        if (vma_is_anonymous(vmf->vma))
            // 處理匿名頁缺頁
            return do_anonymous_page(vmf);
        else
            // 處理文件頁缺頁
            return do_fault(vmf);
    }

        .............

    if (vmf->flags & (FAULT_FLAG_WRITE|FAULT_FLAG_UNSHARE)) {
        if (!pte_write(entry))
            // 子進程缺頁處理
            return do_wp_page(vmf);
    }

這里我們主要關注 do_anonymous_page 函數,正是在這里內核完成了 struct anon_vma 結構和 struct anon_vma_chain 結構的創建以及相關匿名頁反向映射數據結構的相互關聯。

static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    struct page *page;  

        ........省略虛擬內存到物理內存正向映射相關邏輯.........

    if (unlikely(anon_vma_prepare(vma)))
        goto oom;

    page = alloc_zeroed_user_highpage_movable(vma, vmf->address);

    if (!page)
        goto oom;
  // 建立反向映射關系
    page_add_new_anon_rmap(page, vma, vmf->address);

        ........省略虛擬內存到物理內存正向映射相關邏輯.........
}

在 do_anonymous_page 匿名頁缺頁處理函數中會為 struct vm_area_struct 結構創建匿名頁相關的 struct anon_vma 結構和 struct anon_vma_chain 結構。

并在 anon_vma_prepare 函數中實現 anon_vma 和 anon_vma_chain 之間的關聯 ,隨后調用 alloc_zeroed_user_highpage_movable 從伙伴系統中獲取物理內存頁 struct page,并在 page_add_new_anon_rmap 函數中完成 struct page 與 anon_vma 的關聯(這里正是反向映射關系建立的關鍵)

在介紹匿名頁反向映射源碼實現之前,筆者先來為大家介紹一下相關的兩個重要數據結構 struct anon_vma 和 struct anon_vma_chain,方便大家理解為何 struct page 與 anon_vma 關聯起來就能實現反向映射?

前面我們提到,匿名頁的反向映射關鍵就是建立物理內存頁 struct page 與進程虛擬內存空間 VMA 之間的映射關系。

匿名頁的 struct page 中的 mapping 指針指向的是 struct anon_vma 結構。

struct page {
    struct address_space *mapping; 
    pgoff_t index;  
}

只要我們實現了 anon_vma 與 vm_area_struct 之間的關聯,那么 page 到 vm_area_struct 之間的映射就建立起來了,struct anon_vma_chain 結構做的事情就是建立 anon_vma 與 vm_area_struct 之間的關聯關系。

struct anon_vma_chain {
    // 匿名頁關聯的進程虛擬內存空間(vma屬于一個特定的進程,多個進程多個vma)
    struct vm_area_struct *vma;
    // 匿名頁 page 指向的 anon_vma
    struct anon_vma *anon_vma;
    struct list_head same_vma;   
    struct rb_node rb;         
    unsigned long rb_subtree_last;
#ifdef CONFIG_DEBUG_VM_RB
    unsigned long cached_vma_start, cached_vma_last;
#endif
};

struct anon_vma_chain 結構通過其中的 vma 指針和 anon_vma 指針將相關的匿名頁與其映射的進程虛擬內存空間關聯了起來。

image.png

從目前來看匿名頁 struct page 算是與 anon_vma 建立了關系,又通過 anon_vma_chain 將 anon_vma 與 vm_area_struct 建立了關系。那么就剩下最后一道關系需要打通了,就是如何通過 anon_vma 找到 anon_vma_chain 進而找到 vm_area_struct 呢?這就需要我們將 anon_vma 與 anon_vma_chain 之間的關系也打通。

我們知道每個匿名頁對應唯一的 anon_vma 結構,但是一個匿名物理頁可以映射到不同進程的虛擬內存空間中,每個進程的虛擬內存空間都是獨立的,也就是說不同的進程就會有不同的 VMA。

image.png

不同的 VMA 意味著同一個匿名頁 anon_vma 就會對應多個 anon_vma_chain。那么如何通過一個 anon_vma 找到和他關聯的所有 anon_vma_chain 呢?找到了這些 anon_vma_chain 也就意味著 struct page 找到了與它關聯的所有進程虛擬內存空間 VMA。

我們看看能不能從 struct anon_vma 結構中尋找一下線索:

struct anon_vma {
    struct anon_vma *root;      /* Root of this anon_vma tree */
    struct rw_semaphore rwsem; 
    atomic_t refcount;
    unsigned degree;
    struct anon_vma *parent;    /* Parent of this anon_vma */
    struct rb_root rb_root; /* Interval tree of private "related" vmas */
};

我們重點來看 struct anon_vma 結構中的 rb_root 字段,struct anon_vma 結構中管理了一顆紅黑樹,這顆紅黑樹上管理的全部都是與該 anon_vma 關聯的 anon_vma_chain。我們可以通過 struct page 中的 mapping 指針找到 anon_vma,然后遍歷 anon_vma 中的這顆紅黑樹 rb_root ,從而找到與其關聯的所有 anon_vma_chain。

struct anon_vma_chain {
    // 匿名頁關聯的進程虛擬內存空間(vma屬于一個特定的進程,多個進程多個vma)
    struct vm_area_struct *vma;
    // 匿名頁 page 指向的 anon_vma
    struct anon_vma *anon_vma;
    // 指向 vm_area_struct 中的 anon_vma_chain 列表
    struct list_head same_vma;   
    // anon_vma 管理的紅黑樹中該 anon_vma_chain 對應的紅黑樹節點
    struct rb_node rb;         
};

struct anon_vma_chain 結構中的 rb 字段表示其在對應 anon_vma 管理的紅黑樹中的節點。

image.png

到目前為止,物理內存頁 page 到與其映射的進程虛擬內存空間 VMA,這樣一種一對多的映射關系現在就算建立起來了。

而 vm_area_struct 表示的只是進程虛擬內存空間中的一段虛擬內存區域,這塊虛擬內存區域中可能會包含多個匿名頁,所以 VMA 與物理內存頁 page 也是有一對多的映射關系存在。而這個映射關系在哪里保存呢?

大家注意 struct anon_vma_chain 結構中還有一個列表結構 same_vma,從這個名字上我們很容易就能猜到這個列表 same_vma 中存儲的 anon_vma_chain 對應的 VMA 全都是一樣的,而列表元素 anon_vma_chain 中的 anon_vma 卻是不一樣的。內核用這樣一個鏈表結構 same_vma 存儲了進程相應虛擬內存區域 VMA 中所包含的所有匿名頁。

struct vm_area_struct 結構中的 struct list_head anon_vma_chain 指向的也是這個列表 same_vma。

struct vm_area_struct {  
    // 存儲該 VMA 中所包含的所有匿名頁 anon_vma
    struct list_head anon_vma_chain;
    // 用于快速判斷 VMA 有沒有對應的匿名 page
    // 一個 VMA 可以包含多個 page,但是該區域內的所有 page 只需要一個 anon_vma 來反向映射即可。
    struct anon_vma *anon_vma;   
}
image.png

現在整個匿名頁到進程虛擬內存空間的反向映射鏈路關系,筆者就為大家梳理清楚了,下面我們接著回到 do_anonymous_page 函數中,來一一驗證上述映射邏輯:

static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    struct page *page;  

        ........省略虛擬內存到物理內存正向映射相關邏輯.........

    if (unlikely(anon_vma_prepare(vma)))
        goto oom;

    page = alloc_zeroed_user_highpage_movable(vma, vmf->address);

    if (!page)
        goto oom;

    page_add_new_anon_rmap(page, vma, vmf->address);

        ........省略虛擬內存到物理內存正向映射相關邏輯.........
}

在 do_anonymous_page 中首先會調用 anon_vma_prepare 方法來為匿名頁創建 anon_vma 實例和 anon_vma_chain 實例,并建立它們之間的關聯關系。

int __anon_vma_prepare(struct vm_area_struct *vma)
{
    // 獲取進程虛擬內存空間
    struct mm_struct *mm = vma->vm_mm;
    // 準備為匿名頁分配 anon_vma 以及 anon_vma_chain
    struct anon_vma *anon_vma, *allocated;
    struct anon_vma_chain *avc;
    // 分配 anon_vma_chain 實例
    avc = anon_vma_chain_alloc(GFP_KERNEL);
    if (!avc)
        goto out_enomem;
    // 在相鄰的虛擬內存區域 VMA 中查找可復用的 anon_vma
    anon_vma = find_mergeable_anon_vma(vma);
    allocated = NULL;
    if (!anon_vma) {
        // 沒有可復用的 anon_vma 則創建一個新的實例
        anon_vma = anon_vma_alloc();
        if (unlikely(!anon_vma))
            goto out_enomem_free_avc;
        allocated = anon_vma;
    }

    anon_vma_lock_write(anon_vma);
    /* page_table_lock to protect against threads */
    spin_lock(&mm->page_table_lock);
    if (likely(!vma->anon_vma)) {
        // VMA 中的 anon_vma 屬性就是在這里賦值的
        vma->anon_vma = anon_vma;
        // 建立反向映射關聯
        anon_vma_chain_link(vma, avc, anon_vma);
        /* vma reference or self-parent link for new root */
        anon_vma->degree++;
        allocated = NULL;
        avc = NULL;
    }
        .................
}

anon_vma_prepare 方法中調用 anon_vma_chain_link 方法來建立 anon_vma,anon_vma_chain,vm_area_struct 三者之間的關聯關系:

static void anon_vma_chain_link(struct vm_area_struct *vma,
                struct anon_vma_chain *avc,
                struct anon_vma *anon_vma)
{
    // 通過 anon_vma_chain 關聯 anon_vma 和對應的 vm_area_struct
    avc->vma = vma;
    avc->anon_vma = anon_vma;
    // 將 vm_area_struct 中的 anon_vma_chain 鏈表加入到 anon_vma_chain 中的 same_vma 鏈表中
    list_add(&avc->same_vma, &vma->anon_vma_chain);
    // 將初始化好的 anon_vma_chain 加入到 anon_vma 管理的紅黑樹 rb_root 中
    anon_vma_interval_tree_insert(avc, &anon_vma->rb_root);
}
image.png

到現在為止還缺關鍵的最后一步,就是打通匿名內存頁 page 到 vm_area_struct 之間的關系,首先我們就需要調用 alloc_zeroed_user_highpage_movable 方法從伙伴系統中申請一個匿名頁。當獲取到 page 實例之后,通過 page_add_new_anon_rmap 最終建立起 page 到 vm_area_struct 的整條反向映射鏈路。

static void __page_set_anon_rmap(struct page *page,
    struct vm_area_struct *vma, unsigned long address, int exclusive)
{
    struct anon_vma *anon_vma = vma->anon_vma;
           .........省略..............
    // 低位置 1
    anon_vma = (void *) anon_vma + PAGE_MAPPING_ANON;
    // 轉換為 address_space 指針賦值給 page 結構中的 mapping 字段
    page->mapping = (struct address_space *) anon_vma;
    // page 結構中的 index 表示該匿名頁在虛擬內存區域 vma 中的偏移
    page->index = linear_page_index(vma, address);
}

現在讓我們再次回到本小節 《6.1 匿名頁的反向映射》的開始,再來看這段話,是不是感到非常清晰了呢~~

如果當前物理內存頁 struct page 是一個匿名頁的話,那么 mapping 指針的最低位會被設置為 1 , 指向該匿名頁在進程虛擬內存空間中的匿名映射區域 struct anon_vma 結構(每個匿名頁對應唯一的 anon_vma 結構),用于物理內存到虛擬內存的反向映射。

如果當前物理內存頁 struct page 是一個文件頁的話,那么 mapping 指針的最低位會被設置為 0 ,指向該內存頁關聯文件的 struct address_space(頁高速緩存)。pgoff_t index 字段表示該內存頁 page 在頁高速緩存中的 index 索引,也表示該內存頁中的文件數據在文件內部的偏移 offset。偏移單位為 page size。

struct page 結構中的 struct address_space *mapping 指針的最低位如何置 1 ,又如何置 0 呢?關鍵在下面這條語句:

    struct anon_vma *anon_vma = vma->anon_vma;
    // 低位置 1
    anon_vma = (void *) anon_vma + PAGE_MAPPING_ANON;

anon_vma 指針加上 PAGE_MAPPING_ANON ,并轉換為 address_space 指針,這樣可確保 address_space 指針的低位為 1 表示匿名頁。

address_space 指針在轉換為 anon_vma 指針的時候可通過如下語句實現:

anon_vma = (struct anon_vma *) (mapping - PAGE_MAPPING_ANON)

PAGE_MAPPING_ANON 常量定義在內核 /include/linux/page-flags.h 文件中:

#define PAGE_MAPPING_ANON   0x1

而對于文件頁來說,page 結構的 mapping 指針最低位本來就是 0 ,因為 address_space 類型的指針實現總是對齊至 sizeof(long),因此在 Linux 支持的所有計算機上,指向 address_space 實例的指針最低位總是為 0 。

內核可以通過這個技巧直接檢查 page 結構中的 mapping 指針的最低位來判斷該物理內存頁到底是匿名頁還是文件頁

前面說了文件頁的 page 結構的 index 屬性表示該內存頁 page 在磁盤文件中的偏移 offset ,偏移單位為 page size 。

那匿名頁的 page 結構中的 index 屬性表示什么呢?我們接著來看 linear_page_index 函數:

static inline pgoff_t linear_page_index(struct vm_area_struct *vma,
                    unsigned long address)
{
    pgoff_t pgoff;
    if (unlikely(is_vm_hugetlb_page(vma)))
        return linear_hugepage_index(vma, address);
    pgoff = (address - vma->vm_start) >> PAGE_SHIFT;
    pgoff += vma->vm_pgoff;
    return pgoff;
}

邏輯很簡單,就是表示匿名頁在對應進程虛擬內存區域 VMA 中的偏移。

在本小節最后,還有一個與反向映射相關的重要屬性就是 page 結構中的 _mapcount。

struct page {
    struct address_space *mapping; 
    pgoff_t index;  
    // 表示該 page 映射了多少個進程的虛擬內存空間,一個 page 可以被多個進程映射
    atomic_t _mapcount
}

經過本小節詳細的介紹,我想大家現在已經猜到 _mapcount 字段的含義了,我們知道一個物理內存頁可以映射到多個進程的虛擬內存空間中,比如:共享內存映射,父子進程的創建等。page 與 VMA 是一對多的關系,這里的 _mapcount 就表示該物理頁映射到了多少個進程的虛擬內存空間中。

6.2 內存頁回收相關屬性

我們接著來看 struct page 中剩下的其他屬性,我們知道物理內存頁在內核中分為匿名頁和文件頁,在《5.2 物理內存區域中的水位線》小節中,筆者還提到過兩個重要的鏈表分別為:active 鏈表和 inactive 鏈表。

其中 active 鏈表用來存放訪問非常頻繁的內存頁(熱頁), inactive 鏈表用來存放訪問不怎么頻繁的內存頁(冷頁),當內存緊張的時候,內核就會優先將 inactive 鏈表中的內存頁置換出去。

內核在回收內存的時候,這兩個列表中的回收優先級為:inactive 鏈表尾部 > inactive 鏈表頭部 > active 鏈表尾部 > active 鏈表頭部。

我們可以通過 cat /proc/zoneinfo 命令來查看不同 NUMA 節點中不同內存區域中的 active 鏈表和 inactive 鏈表中物理內存頁的個數:

image.png
  • nr_zone_active_anon 和 nr_zone_inactive_anon 分別是該內存區域內活躍和非活躍的匿名頁數量。

  • nr_zone_active_file 和 nr_zone_inactive_file 分別是該內存區域內活躍和非活躍的文件頁數量。

為什么會有 active 鏈表和 inactive 鏈表

內存回收的關鍵是如何實現一個高效的頁面替換算法 PFRA (Page Frame Replacement Algorithm) ,提到頁面替換算法大家可能立馬會想到 LRU (Least-Recently-Used) 算法。LRU 算法的核心思想就是那些最近最少使用的頁面,在未來的一段時間內可能也不會再次被使用,所以在內存緊張的時候,會優先將這些最近最少使用的頁面置換出去。在這種情況下其實一個 active 鏈表就可以滿足我們的需求。

但是這里會有一個嚴重的問題,LRU 算法更多的是在時間維度上的考量,突出最近最少使用,但是它并沒有考量到使用頻率的影響,假設有這樣一種狀況,就是一個頁面被瘋狂頻繁的使用,毫無疑問它肯定是一個熱頁,但是這個頁面最近的一次訪問時間離現在稍微久了一點點,此時進來大量的頁面,這些頁面的特點是只會使用一兩次,以后將再也不會用到。

在這種情況下,根據 LRU 的語義這個之前頻繁地被瘋狂訪問的頁面就會被置換出去了(本來應該將這些大量一次性訪問的頁面置換出去的),當這個頁面在不久之后要被訪問時,此時已經不在內存中了,還需要在重新置換進來,造成性能的損耗。這種現象也叫 Page Thrashing(頁面顛簸)。

因此,內核為了將頁面使用頻率這個重要的考量因素加入進來,于是就引入了 active 鏈表和 inactive 鏈表。工作原理如下:

  1. 首先 inactive 鏈表的尾部存放的是訪問頻率最低并且最少訪問的頁面,在內存緊張的時候,這些頁面被置換出去的優先級是最大的。

  2. 對于文件頁來說,當它被第一次讀取的時候,內核會將它放置在 inactive 鏈表的頭部,如果它繼續被訪問,則會提升至 active 鏈表的尾部。如果它沒有繼續被訪問,則會隨著新文件頁的進入,內核會將它慢慢的推到 inactive 鏈表的尾部,如果此時再次被訪問則會直接被提升到 active 鏈表的頭部。大家可以看出此時頁面的使用頻率這個因素已經被考量了進來。

  3. 對于匿名頁來說,當它被第一次讀取的時候,內核會直接將它放置在 active 鏈表的尾部,注意不是 inactive 鏈表的頭部,這里和文件頁不同。因為匿名頁的換出 Swap Out 成本會更大,內核會對匿名頁更加優待。當匿名頁再次被訪問的時候就會被被提升到 active 鏈表的頭部。

  4. 當遇到內存緊張的情況需要換頁時,內核會從 active 鏈表的尾部開始掃描,將一定量的頁面降級到 inactive 鏈表頭部,這樣一來原來位于 inactive 鏈表尾部的頁面就會被置換出去。

內核在回收內存的時候,這兩個列表中的回收優先級為:inactive 鏈表尾部 > inactive 鏈表頭部 > active 鏈表尾部 > active 鏈表頭部。

為什么會把 active 鏈表和 inactive 鏈表分成兩類,一類是匿名頁,一類是文件頁

在本文 《5.2 物理內存區域中的水位線》小節中,筆者為大家介紹了一個叫做 swappiness 的內核參數, 我們可以通過 cat /proc/sys/vm/swappiness 命令查看,swappiness 選項的取值范圍為 0 到 100,默認為 60。

swappiness 用于表示 Swap 機制的積極程度,數值越大,Swap 的積極程度,越高越傾向于回收匿名頁。數值越小,Swap 的積極程度越低,越傾向于回收文件頁

因為回收匿名頁和回收文件頁的代價是不一樣的,回收匿名頁代價會更高一點,所以引入 swappiness 來控制內核回收的傾向。

注意: swappiness 只是表示 Swap 積極的程度,當內存非常緊張的時候,即使將 swappiness 設置為 0 ,也還是會發生 Swap 的。

假設我們現在只有 active 鏈表和 inactive 鏈表,不對這兩個鏈表進行匿名頁和文件頁的歸類,在需要頁面置換的時候,內核會先從 active 鏈表尾部開始掃描,當 swappiness 被設置為 0 時,內核只會置換文件頁,不會置換匿名頁。

由于 active 鏈表和 inactive 鏈表沒有進行物理頁面類型的歸類,所以鏈表中既會有匿名頁也會有文件頁,如果鏈表中有大量的匿名頁的話,內核就會不斷的跳過這些匿名頁去尋找文件頁,并將文件頁替換出去,這樣從性能上來說肯定是低效的。

因此內核將 active 鏈表和 inactive 鏈表按照匿名頁和文件頁進行了歸類,當 swappiness 被設置為 0 時,內核只需要去 nr_zone_active_file 和 nr_zone_inactive_file 鏈表中掃描即可,提升了性能。

其實除了以上筆者介紹的四種 LRU 鏈表(匿名頁的 active 鏈表,inactive 鏈表和文件頁的active 鏈表, inactive 鏈表)之外,內核還有一種鏈表,比如進程可以通過 mlock() 等系統調用把內存頁鎖定在內存里,保證該內存頁無論如何不會被置換出去,比如出于安全或者性能的考慮,頁面中可能會包含一些敏感的信息不想被 swap 到磁盤上導致泄密,或者一些頻繁訪問的內存頁必須一直貯存在內存中。

當這些被鎖定在內存中的頁面很多時,內核在掃描 active 鏈表的時候也不得不跳過這些頁面,所以內核又將這些被鎖定的頁面單獨拎出來放在一個獨立的鏈表中。

現在筆者為大家介紹五種用于存放 page 的鏈表,內核會根據不同的情況將一個物理頁放置在這五種鏈表其中一個上。那么對于物理頁的 struct page 結構中就需要有一個屬性用來標識該物理頁究竟被內核放置在哪個鏈表上。

struct page {
   struct list_head lru;
   atomic_t _refcount;
}

struct list_head lru 屬性就是用來指向物理頁被放置在了哪個鏈表上。

atomic_t _refcount 屬性用來記錄內核中引用該物理頁的次數,表示該物理頁的活躍程度。

6.3 物理內存頁屬性和狀態的標志位 flag

struct page {
    unsigned long flags;
} 

在本文 《2.3 SPARSEMEM 稀疏內存模型》小節中,我們提到,內核為了能夠更靈活地管理粒度更小的連續物理內存,于是就此引入了 SPARSEMEM 稀疏內存模型。

image.png

SPARSEMEM 稀疏內存模型的核心思想就是提供對粒度更小的連續內存塊進行精細的管理,用于管理連續內存塊的單元被稱作 section 。內核中用于描述 section 的數據結構是 struct mem_section。

由于 section 被用作管理小粒度的連續內存塊,這些小的連續物理內存在 section 中也是通過數組的方式被組織管理(圖中 struct page 類型的數組)。

每個 struct mem_section 結構體中有一個 section_mem_map 指針用于指向連續內存的 page 數組。而所有的 mem_section 也會被存放在一個全局的數組 mem_section 中。

那么給定一個具體的 struct page,在稀疏內存模型中內核如何定位到這個物理內存頁到底屬于哪個 mem_section 呢 ?這是第一個問題~~

筆者在《5. 內核如何管理 NUMA 節點中的物理內存區域》小節中講到了內存的架構,在 NUMA 架構下,物理內存被劃分成了一個一個的內存節點(NUMA 節點),在每個 NUMA 節點內部又將其所管理的物理內存按照功能不同劃分成了不同的內存區域 zone,每個內存區域管理一片用于特定具體功能的物理內存 page。

物理內存在內核中管理的層級關系為:None -> Zone -> page

image.png

那么在 NUMA 架構下,給定一個具體的 struct page,內核又該如何確定該物理內存頁究竟屬于哪個 NUMA 節點,屬于哪塊內存區域 zone 呢? 這是第二個問題。

關于以上筆者提出的兩個問題所需要的定位信息全部存儲在 struct page 結構中的 flags 字段中。前邊我們提到,struct page 是 Linux 世界里最繁華的地段,這里的地價非常昂貴,所以 page 結構中這些字段里的每一個比特內核都會物盡其用。

struct page {
    unsigned long flags;
} 

因此這個 unsigned long 類型的 flags 字段中不僅包含上面提到的定位信息還會包括物理內存頁的一些屬性和標志位。flags 字段的高 8 位用來表示 struct page 的定位信息,剩余低位表示特定的標志位。

image.png

struct page 與其所屬上層結構轉換的相應函數定義在 /include/linux/mm.h 文件中:

static inline unsigned long page_to_section(const struct page *page)
{
    return (page->flags >> SECTIONS_PGSHIFT) & SECTIONS_MASK;
}

static inline pg_data_t *page_pgdat(const struct page *page)
{
    return NODE_DATA(page_to_nid(page));
}

static inline struct zone *page_zone(const struct page *page)
{
    return &NODE_DATA(page_to_nid(page))->node_zones[page_zonenum(page)];
}

在我們介紹完了 flags 字段中高位存儲的位置定位信息之后,接下來就該來介紹下在低位比特中表示的物理內存頁的那些標志位~~

物理內存頁的這些標志位定義在內核 /include/linux/page-flags.h文件中:

enum pageflags {
    PG_locked,      /* Page is locked. Don't touch. */
    PG_referenced,
    PG_uptodate,
    PG_dirty,
    PG_lru,
    PG_active,
    PG_slab,
    PG_reserved,
    PG_compound,
    PG_private,     
    PG_writeback,       
    PG_reclaim,     
#ifdef CONFIG_MMU
    PG_mlocked,     /* Page is vma mlocked */
    PG_swapcache = PG_owner_priv_1, 

        ................
};
  • PG_locked 表示該物理頁面已經被鎖定,如果該標志位置位,說明有使用者正在操作該 page , 則內核的其他部分不允許訪問該頁, 這可以防止內存管理出現競態條件,例如:在從硬盤讀取數據到 page 時。

  • PG_mlocked 表示該物理內存頁被進程通過 mlock 系統調用鎖定常駐在內存中,不會被置換出去。

  • PG_referenced 表示該物理頁面剛剛被訪問過。

  • PG_active 表示該物理頁位于 active list 鏈表中。PG_referenced 和 PG_active 共同控制了系統使用該內存頁的活躍程度,在內存回收的時候這兩個信息非常重要。

  • PG_uptodate 表示該物理頁的數據已經從塊設備中讀取到內存中,并且期間沒有出錯。

  • PG_readahead 當進程在順序訪問文件的時候,內核會預讀若干相鄰的文件頁數據到 page 中,物理頁 page 結構設置了該標志位,表示它是一個正在被內核預讀的頁。相關詳細內容可回看筆者之前的這篇文章 《從 Linux 內核角度探秘 JDK NIO 文件讀寫本質》

  • PG_dirty 物理內存頁的臟頁標識,表示該物理內存頁中的數據已經被進程修改,但還沒有同步會磁盤中。筆者在 《從 Linux 內核角度探秘 JDK NIO 文件讀寫本質》 一文中也詳細介紹過。

  • PG_lru 表示該物理內存頁現在被放置在哪個 lru 鏈表上,比如:是在 active list 鏈表中 ? 還是在 inactive list 鏈表中 ?

  • PG_highmem 表示該物理內存頁是在高端內存中。

  • PG_writeback 表示該物理內存頁正在被內核的 pdflush 線程回寫到磁盤中。詳情可回看文章《從 Linux 內核角度探秘 JDK NIO 文件讀寫本質》

  • PG_slab 表示該物理內存頁屬于 slab 分配器所管理的一部分。

  • PG_swapcache 表示該物理內存頁處于 swap cache 中。 struct page 中的 private 指針這時指向 swap_entry_t 。

  • PG_reclaim 表示該物理內存頁已經被內核選中即將要進行回收。

  • PG_buddy 表示該物理內存頁是空閑的并且被伙伴系統所管理。

  • PG_compound 表示物理內存頁屬于復合頁的其中一部分。

  • PG_private 標志被置位的時候表示該 struct page 結構中的 private 指針指向了具體的對象。不同場景指向的對象不同。

除此之外內核還定義了一些標準宏,用來檢查某個物理內存頁 page 是否設置了特定的標志位,以及對這些標志位的操作,這些宏在內核中的實現都是原子的,命名格式如下:

  • PageXXX(page):檢查 page 是否設置了 PG_XXX 標志位

  • SetPageXXX(page):設置 page 的 PG_XXX 標志位

  • ClearPageXXX(page):清除 page 的 PG_XXX 標志位

  • TestSetPageXXX(page):設置 page 的 PG_XXX 標志位,并返回原值

另外在很多情況下,內核通常需要等待物理頁 page 的某個狀態改變,才能繼續恢復工作,內核提供了如下兩個輔助函數,來實現在特定狀態的阻塞等待:

static inline void wait_on_page_locked(struct page *page)
static inline void wait_on_page_writeback(struct page *page)

當物理頁面在鎖定的狀態下,進程調用了 wait_on_page_locked 函數,那么進程就會阻塞等待知道頁面解鎖。

當物理頁面正在被內核回寫到磁盤的過程中,進程調用了 wait_on_page_writeback 函數就會進入阻塞狀態直到臟頁數據被回寫到磁盤之后被喚醒。

6.4 復合頁 compound_page 相關屬性

我們都知道 Linux 管理內存的最小單位是 page,每個 page 描述 4K 大小的物理內存,但在一些對于內存敏感的使用場景中,用戶往往期望使用一些巨型大頁。

巨型大頁就是通過兩個或者多個物理上連續的內存頁 page 組裝成的一個比普通內存頁 page 更大的頁,

因為這些巨型頁要比普通的 4K 內存頁要大很多,所以遇到缺頁中斷的情況就會相對減少,由于減少了缺頁中斷所以性能會更高。

另外,由于巨型頁比普通頁要大,所以巨型頁需要的頁表項要比普通頁要少,頁表項里保存了虛擬內存地址與物理內存地址的映射關系,當 CPU 訪問內存的時候需要頻繁通過 MMU 訪問頁表項獲取物理內存地址,由于要頻繁訪問,所以頁表項一般會緩存在 TLB 中,因為巨型頁需要的頁表項較少,所以節約了 TLB 的空間同時降低了 TLB 緩存 MISS 的概率,從而加速了內存訪問。

還有一個使用巨型頁受益場景就是,當一個內存占用很大的進程(比如 Redis)通過 fork 系統調用創建子進程的時候,會拷貝父進程的相關資源,其中就包括父進程的頁表,由于巨型頁使用的頁表項少,所以拷貝的時候性能會提升不少。

以上就是巨型頁存在的原因以及使用的場景,但是在 Linux 內存管理架構中都是統一通過 struct page 來管理內存,而巨型大頁卻是通過兩個或者多個物理上連續的內存頁 page 組裝成的一個比普通內存頁 page 更大的頁,那么巨型頁的管理與普通頁的管理如何統一呢?

這就引出了本小節的主題-----復合頁 compound_page,下面我們就來看下 Linux 如果通過統一的 struct page 結構來描述這些巨型頁(compound_page):

雖然巨型頁(compound_page)是由多個物理上連續的普通 page 組成的,但是在內核的視角里它還是被當做一個特殊內存頁來看待。

下圖所示,是由 4 個連續的普通內存頁 page 組成的一個 compound_page:

image.png

組成復合頁的第一個 page 我們稱之為首頁(Head Page),其余的均稱之為尾頁(Tail Page)。

我們來看一下 struct page 中關于描述 compound_page 的相關字段:

      struct page {      
            // 首頁 page 中的 flags 會被設置為 PG_head 表示復合頁的第一頁
            unsigned long flags;    
            // 其余尾頁會通過該字段指向首頁
            unsigned long compound_head;   
            // 用于釋放復合頁的析構函數,保存在首頁中
            unsigned char compound_dtor;
            // 該復合頁有多少個 page 組成,order 還是分配階的概念,首頁中保存
            // 本例中的 order = 2 表示由 4 個普通頁組成
            unsigned char compound_order;
            // 該復合頁被多少個進程使用,內存頁反向映射的概念,首頁中保存
            atomic_t compound_mapcount;
            // 復合頁使用計數,首頁中保存
            atomic_t compound_pincount;
      }

首頁對應的 struct page 結構里的 flags 會被設置為 PG_head,表示這是復合頁的第一頁。

另外首頁中還保存關于復合頁的一些額外信息,比如用于釋放復合頁的析構函數會保存在首頁 struct page 結構里的 compound_dtor 字段中,復合頁的分配階 order 會保存在首頁中的 compound_order 中,以及用于指示復合頁的引用計數 compound_pincount,以及復合頁的反向映射個數(該復合頁被多少個進程的頁表所映射)compound_mapcount 均在首頁中保存。

復合頁中的所有尾頁都會通過其對應的 struct page 結構中的 compound_head 指向首頁,這樣通過首頁和尾頁就組裝成了一個完整的復合頁 compound_page 。

image.png

6.5 Slab 對象池相關屬性

本小節只是對 slab 的一個簡單介紹,大家有個大概的印象就可以了,后面筆者會有一篇專門的文章為大家詳細介紹 slab 的相關實現細節,到時候還會在重新詳細介紹 struct page 中的相關屬性。

內核中對內存頁的分配使用有兩種方式,一種是一頁一頁的分配使用,這種以頁為單位的分配方式內核會向相應內存區域 zone 里的伙伴系統申請以及釋放。

另一種方式就是只分配小塊的內存,不需要一下分配一頁的內存,比如前邊章節中提到的 struct page ,anon_vma_chain ,anon_vma ,vm_area_struct 結構實例的分配,這些結構通常就是幾十個字節大小,并不需要按頁來分配。

為了滿足類似這種小內存分配的需要,Linux 內核使用 slab allocator 分配器來分配,slab 就好比一個對象池,內核中的數據結構對象都對應于一個 slab 對象池,用于分配這些固定類型對象所需要的內存。

它的基本原理是從伙伴系統中申請一整頁內存,然后劃分成多個大小相等的小塊內存被 slab 所管理。這樣一來 slab 就和物理內存頁 page 發生了關聯,由于 slab 管理的單元是物理內存頁 page 內進一步劃分出來的小塊內存,所以當 page 被分配給相應 slab 結構之后,struct page 里也會存放 slab 相關的一些管理數據。

struct page {

        struct {    /* slab, slob and slub */
            union {
                struct list_head slab_list;
                struct {    /* Partial pages */
                    struct page *next;
#ifdef CONFIG_64BIT
                    int pages;  /* Nr of pages left */
                    int pobjects;   /* Approximate count */
#else
                    short int pages;
                    short int pobjects;
#endif
                };
            };
            struct kmem_cache *slab_cache; /* not slob */
            /* Double-word boundary */
            void *freelist;     /* first free object */
            union {
                void *s_mem;    /* slab: first object */
                struct {            /* SLUB */
                    unsigned inuse:16;
                    unsigned objects:15;
                    unsigned frozen:1;
                };
            };
        };

}
  • struct list_head slab_list :slab 的管理結構中有眾多用于管理 page 的鏈表,比如:完全空閑的 page 鏈表,完全分配的 page 鏈表,部分分配的 page 鏈表,slab_list 用于指定當前 page 位于 slab 中的哪個具體鏈表上。

  • struct page *next : 當 page 位于 slab 結構中的某個管理鏈表上時,next 指針用于指向鏈表中的下一個 page。

  • int pages : 表示 slab 中總共擁有的 page 個數。

  • int pobjects : 表示 slab 中擁有的特定類型的對象個數。

  • struct kmem_cache *slab_cache : 用于指向當前 page 所屬的 slab 管理結構,通過 slab_cache 將 page 和 slab 關聯起來。

  • void *freelist : 指向 page 中的第一個未分配出去的空閑對象,前面介紹過,slab 向伙伴系統申請一個或者多個 page,并將一整頁 page 劃分出多個大小相等的內存塊,用于存儲特定類型的對象。

  • void *s_mem : 指向 page 中的第一個對象。

  • unsigned inuse : 表示 slab 中已經被分配出去的對象個數,當該值為 0 時,表示 slab 中所管理的對象全都是空閑的,當所有的空閑對象達到一定數目,該 slab 就會被伙伴系統回收掉。

  • unsigned objects : slab 中所有的對象個數。

  • unsigned frozen : 當前內存頁 page 被 slab 放置在 CPU 本地緩存列表中,frozen = 1,否則 frozen = 0 。

總結

到這里,關于 Linux 物理內存管理的相關內容筆者就為大家介紹完了,本文的內容比較多,尤其是物理內存頁反向映射相關的內容比較復雜,涉及到的關聯關系比較多,現在筆者在帶大家總結一下本文的主要內容,方便大家復習回顧:

在本文的開始,筆者首先從 CPU 角度為大家介紹了三種物理內存模型:FLATMEM 平坦內存模型,DISCONTIGMEM 非連續內存模型,SPARSEMEM 稀疏內存模型。

隨后筆者又接著介紹了兩種物理內存架構:一致性內存訪問 UMA 架構,非一致性內存訪問 NUMA 架構。

在這個基礎之上,又按照內核對物理內存的組織管理層次,分別介紹了 Node 節點,物理內存區域 zone 等相關內核結構。它們的層次如下圖所示:

image.png

在把握了物理內存的總體架構之后,又引出了眾多細節性的內容,比如:物理內存區域的管理與劃分,物理內存區域中的預留內存,物理內存區域中的水位線及其計算方式,物理內存區域中的冷熱頁。

最后,筆者詳細介紹了內核如何通過 struct page 結構來描述物理內存頁,其中匿名頁反向映射的內容比較復雜,需要大家多多梳理回顧一下。

好了,本文的內容到這里就全部結束了,感謝大家的耐心觀看,我們下篇文章見~~~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容