Linux內存描述之內存區域zone--Linux內存管理(三)

1 內存管理域zone

爲了支持NUMA模型,也即CPU對不同內存單元的訪問時間可能不同,此時系統的物理內存被劃分爲幾個節點(node), 一個node對應一個內存簇bank,即每個內存簇被認爲是一個節點

  • 首先, 內存被劃分爲結點. 每個節點關聯到系統中的一個處理器, 內核中表示爲pg_data_t的實例. 系統中每個節點被鏈接到一個以NULL結尾的pgdat_list鏈表中,而其中的每個節點利用pg_data_tnode_next字段鏈接到下一節.而對於PC這種UMA結構的機器來說, 只使用了一個成爲contig_page_data的靜態pg_data_t結構.
  • 接着各個節點又被劃分爲內存管理區域, 一個管理區域通過struct zone_struct描述, 其被定義爲zone_t, 用以表示內存的某個範圍, 低端範圍的16MB被描述爲ZONE_DMA, 某些工業標準體系結構中的(ISA)設備需要用到它, 然後是可直接映射到內核的普通內存域ZONE_NORMAL,最後是超出了內核段的物理地址域ZONE_HIGHMEM, 被稱爲高端內存. 是系統中預留的可用內存空間, 不能被內核直接映射.

下面我們就來詳解講講內存管理域的內容zone

2 爲什麼要將內存node分成不同的區域zone

NUMA結構下, 每個處理器CPU與一個本地內存直接相連, 而不同處理器之前則通過總線進行進一步的連接, 因此相對於任何一個CPU訪問本地內存的速度比訪問遠程內存的速度要快, 而Linux爲了兼容NUMAJ結構, 把物理內存相依照CPU的不同node分成簇, 一個CPU-node對應一個本地內存pgdata_t.

這樣已經很好的表示物理內存了, 在一個理想的計算機系統中, 一個頁框就是一個內存的分配單元, 可用於任何事情:存放內核數據, 用戶數據和緩衝磁盤數據等等. 任何種類的數據頁都可以存放在任頁框中, 沒有任何限制.

但是Linux內核又把各個物理內存節點分成個不同的管理區域zone, 這是爲什麼呢?

因爲實際的計算機體系結構有硬件的諸多限制, 這限制了頁框可以使用的方式. 尤其是, Linux內核必須處理80x86體系結構的兩種硬件約束.

  • ISA總線的直接內存存儲DMA處理器有一個嚴格的限制 : 他們只能對RAM的前16MB進行尋址
  • 在具有大容量RAM的現代32位計算機中, CPU不能直接訪問所有的物理地址, 因爲線性地址空間太小, 內核不可能直接映射所有物理內存到線性地址空間, 我們會在後面典型架構(x86)上內存區域劃分詳細講解x86_32上的內存區域劃分

因此Linux內核對不同區域的內存需要採用不同的管理方式和映射方式, 因此內核將物理地址或者成用zone_t表示的不同地址區域

3 內存管理區類型zone_type

前面我們說了由於硬件的一些約束, 低端的一些地址被用於DMA, 而在實際內存大小超過了內核所能使用的現行地址的時候, 一些高地址處的物理地址不能簡單持久的直接映射到內核空間. 因此內核將內存的節點node分成了不同的內存區域方便管理和映射.

Linux使用enum zone_type來標記內核所支持的所有內存區域

3.1 內存區域類型zone_type

zone_type結構定義在include/linux/mmzone.h, 其基本信息如下所示

enum zone_type
{
#ifdef CONFIG_ZONE_DMA
    ZONE_DMA,
#endif

#ifdef CONFIG_ZONE_DMA32

    ZONE_DMA32,
#endif

    ZONE_NORMAL,

#ifdef CONFIG_HIGHMEM
    ZONE_HIGHMEM,
#endif
    ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
    ZONE_DEVICE,
#endif
    __MAX_NR_ZONES

};

不同的管理區的用途是不一樣的,ZONE_DMA類型的內存區域在物理內存的低端,主要是ISA設備只能用低端的地址做DMA操作。ZONE_NORMAL類型的內存區域直接被內核映射到線性地址空間上面的區域(line address space),ZONE_HIGHMEM將保留給系統使用,是系統中預留的可用內存空間,不能被內核直接映射。

3.2 不同的內存區域的作用

在內存中,每個簇所對應的node又被分成的稱爲管理區(zone)的塊,它們各自描述在內存中的範圍。一個管理區(zone)由struct zone結構體來描述,在linux-2.4.37之前的內核中是用typedef struct zone_struct zone_t數據結構來描述)

管理區的類型用zone_type表示, 有如下幾種

table th:nth-of-type(1){ width: 20%; }

管理內存域

描述

ZONE_DMA

標記了適合DMA的內存域. 該區域的長度依賴於處理器類型. 這是由於古老的ISA設備強加的邊界. 但是爲了兼容性, 現代的計算機也可能受此影響

ZONE_DMA32

標記了使用32位地址字可尋址, 適合DMA的內存域. 顯然, 只有在53位系統中ZONE_DMA32才和ZONE_DMA有區別, 在32位系統中, 本區域是空的, 即長度爲0MB, 在Alpha和AMD64系統上, 該內存的長度可能是從0到4GB

ZONE_NORMAL

標記了可直接映射到內存段的普通內存域. 這是在所有體系結構上保證會存在的唯一內存區域, 但無法保證該地址範圍對應了實際的物理地址. 例如, 如果AMD64系統只有兩2G內存, 那麼所有的內存都屬於ZONE_DMA32範圍, 而ZONE_NORMAL則爲空

ZONE_HIGHMEM

標記了超出內核虛擬地址空間的物理內存段, 因此這段地址不能被內核直接映射

ZONE_MOVABLE

內核定義了一個僞內存域ZONE_MOVABLE, 在防止物理內存碎片的機制memory migration中需要使用該內存域. 供防止物理內存碎片的極致使用

ZONE_DEVICE

爲支持熱插拔設備而分配的Non Volatile Memory非易失性內存

MAX_NR_ZONES

充當結束標記, 在內核中想要迭代系統中所有內存域, 會用到該常亮

根據編譯時候的配置, 可能無需考慮某些內存域. 如在64位系統中, 並不需要高端內存, 因爲AM64的linux採用4級頁表,支持的最大物理內存爲64TB, 對於虛擬地址空間的劃分,將0x0000,0000,0000,0000 – 0x0000,7fff,ffff,f000這128T地址用於用戶空間;而0xffff,8000,0000,0000以上的128T爲系統空間地址, 這遠大於當前我們系統中的內存空間, 因此所有的物理地址都可以直接映射到內核中, 不需要高端內存的特殊映射. 可以參見Documentation/x86/x86_64/mm.txt

ZONE_MOVABLE和ZONE_DEVICE其實是和其他的ZONE的用途有異,

  • ZONE_MOVABLE在防止物理內存碎片的機制中需要使用該內存區域,
  • ZONE_DEVICE筆者也第一次知道了,理解有錯的話歡迎大家批評指正, 這個應該是爲支持熱插拔設備而分配的Non Volatile Memory非易失性內存,

關於ZONE_DEVICE, 具體的信息可以參見[ATCH v2 3/9mm: ZONE_DEVICE for “device memory” While pmem is usable as a block device or via DAX mappings to userspace there are several usage scenarios that can not target pmem due to its lack of struct page coverage. In preparation for “hot plugging” pmem into the vmemmap add ZONE_DEVICE as a new zone to tag these pages separately from the ones that are subject to standard page allocations. Importantly “device memory” can be removed at will by userspace unbinding the driver of the device.

3.3 典型架構(x86)上內存區域劃分

對於x86機器,管理區(內存區域)類型如下分佈

類型

區域

ZONE_DMA

0~15MB

ZONE_NORMAL

16MB~895MB

ZONE_HIGHMEM

896MB~物理內存結束

而由於32位系統中, Linux內核虛擬地址空間只有1G, 而0~895M這個986MB被用於DMA和直接映射, 剩餘的物理內存被成爲高端內存. 那內核是如何藉助剩餘128MB高端內存地址空間是如何實現訪問可以所有物理內存?

當內核想訪問高於896MB物理地址內存時,從0xF8000000 ~ 0xFFFFFFFF地址空間範圍內找一段相應大小空閒的邏輯地址空間,借用一會。借用這段邏輯地址空間,建立映射到想訪問的那段物理內存(即填充內核PTE頁面表),臨時用一會,用完後歸還。這樣別人也可以借用這段地址空間訪問其他物理內存,實現了使用有限的地址空間,訪問所有所有物理內存

關於高端內存的內容, 我們後面會專門抽出一章進行講解

因此, 傳統和X86_32位系統中, 前16M劃分給ZONE_DMA, 該區域包含的頁框可以由老式的基於ISAS的設備通過DMA使用”直接內存訪問(DMA)”, ZONE_DMA和ZONE_NORMAL區域包含了內存的常規頁框, 通過把他們線性的映射到現行地址的第4個GB, 內核就可以直接進行訪問, 相反ZONE_HIGHME包含的內存頁不能由內核直接訪問, 儘管他們也線性地映射到了現行地址空間的第4個GB. 在64位體系結構中, 線性地址空間的大小遠遠好過了系統的實際物理地址, 內核可知直接將所有的物理內存映射到線性地址空間, 因此64位體系結構上ZONE_HIGHMEM區域總是空的.

4 管理區結構zone_t

一個管理區(zone)由struct zone結構體來描述(linux-3.8~目前linux4.5),而在linux-2.4.37之前的內核中是用struct zone_struct數據結構來描述), 他們都通過typedef被重定義爲zone_t類型

zone對象用於跟蹤諸如頁面使用情況的統計數, 空閒區域信息和鎖信息

裏面保存着內存使用狀態信息,如page使用統計, 未使用的內存區域,互斥訪問的鎖(LOCKS)等.

4.1 struct zone管理域數據結構

struct zone在linux/mmzone.h中定義, 在linux-4.7的內核中可以使用include/linux/mmzone.h來查看其定義

struct zone
{
    /* Read-mostly fields */

    /* zone watermarks, access with *_wmark_pages(zone) macros */
    unsigned long watermark[NR_WMARK];

    unsigned long nr_reserved_highatomic;

    /*
     * We don't know if the memory that we're going to allocate will be
     * freeable or/and it will be released eventually, so to avoid totally
     * wasting several GB of ram we must reserve some of the lower zone
     * memory (otherwise we risk to run OOM on the lower zones despite
     * there being tons of freeable ram on the higher zones).  This array is
     * recalculated at runtime if the sysctl_lowmem_reserve_ratio sysctl
     * changes.
     * 分別爲各種內存域指定了若干頁
     * 用於一些無論如何都不能失敗的關鍵性內存分配。
     */
    long lowmem_reserve[MAX_NR_ZONES];

#ifdef CONFIG_NUMA
    int node;
#endif

    /*
     * The target ratio of ACTIVE_ANON to INACTIVE_ANON pages on
     * this zone's LRU.  Maintained by the pageout code.
     * 不活動頁的比例,
     * 接着是一些很少使用或者大部分情況下是隻讀的字段:
     * wait_table wait_table_hash_nr_entries wait_table_bits
     * 形成等待列隊,可以等待某一頁可供進程使用  */
    unsigned int inactive_ratio;

    /*  指向這個zone所在的pglist_data對象  */
    struct pglist_data      *zone_pgdat;
    /*/這個數組用於實現每個CPU的熱/冷頁幀列表。內核使用這些列表來保存可用於滿足實現的“新鮮”頁。但冷熱頁幀對應的高速緩存狀態不同:有些頁幀很可能在高速緩存中,因此可以快速訪問,故稱之爲熱的;未緩存的頁幀與此相對,稱之爲冷的。*/
    struct per_cpu_pageset __percpu *pageset;

    /*
     * This is a per-zone reserve of pages that are not available
     * to userspace allocations.
     * 每個區域保留的不能被用戶空間分配的頁面數目
     */
    unsigned long       totalreserve_pages;

#ifndef CONFIG_SPARSEMEM
    /*
     * Flags for a pageblock_nr_pages block. See pageblock-flags.h.
     * In SPARSEMEM, this map is stored in struct mem_section
     */
    unsigned long       *pageblock_flags;
#endif /* CONFIG_SPARSEMEM */

#ifdef CONFIG_NUMA
    /*
     * zone reclaim becomes active if more unmapped pages exist.
     */
    unsigned long       min_unmapped_pages;
    unsigned long       min_slab_pages;
#endif /* CONFIG_NUMA */

    /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT
     * 只內存域的第一個頁幀 */
    unsigned long       zone_start_pfn;

    /*
     * spanned_pages is the total pages spanned by the zone, including
     * holes, which is calculated as:
     *      spanned_pages = zone_end_pfn - zone_start_pfn;
     *
     * present_pages is physical pages existing within the zone, which
     * is calculated as:
     *      present_pages = spanned_pages - absent_pages(pages in holes);
     *
     * managed_pages is present pages managed by the buddy system, which
     * is calculated as (reserved_pages includes pages allocated by the
     * bootmem allocator):
     *      managed_pages = present_pages - reserved_pages;
     *
     * So present_pages may be used by memory hotplug or memory power
     * management logic to figure out unmanaged pages by checking
     * (present_pages - managed_pages). And managed_pages should be used
     * by page allocator and vm scanner to calculate all kinds of watermarks
     * and thresholds.
     *
     * Locking rules:
     *
     * zone_start_pfn and spanned_pages are protected by span_seqlock.
     * It is a seqlock because it has to be read outside of zone->lock,
     * and it is done in the main allocator path.  But, it is written
     * quite infrequently.
     *
     * The span_seq lock is declared along with zone->lock because it is
     * frequently read in proximity to zone->lock.  It's good to
     * give them a chance of being in the same cacheline.
     *
     * Write access to present_pages at runtime should be protected by
     * mem_hotplug_begin/end(). Any reader who can't tolerant drift of
     * present_pages should get_online_mems() to get a stable value.
     *
     * Read access to managed_pages should be safe because it's unsigned
     * long. Write access to zone->managed_pages and totalram_pages are
     * protected by managed_page_count_lock at runtime. Idealy only
     * adjust_managed_page_count() should be used instead of directly
     * touching zone->managed_pages and totalram_pages.
     */
    unsigned long       managed_pages;
    unsigned long       spanned_pages;             /*  總頁數,包含空洞  */
    unsigned long       present_pages;              /*  可用頁數,不包哈空洞  */

    /*  指向管理區的傳統名字, "DMA", "NROMAL"或"HIGHMEM" */
    const char          *name;

#ifdef CONFIG_MEMORY_ISOLATION
    /*
     * Number of isolated pageblock. It is used to solve incorrect
     * freepage counting problem due to racy retrieving migratetype
     * of pageblock. Protected by zone->lock.
     */
    unsigned long       nr_isolate_pageblock;
#endif

#ifdef CONFIG_MEMORY_HOTPLUG
    /* see spanned/present_pages for more description */
    seqlock_t           span_seqlock;
#endif

    /*
     * wait_table       -- the array holding the hash table
     * wait_table_hash_nr_entries   -- the size of the hash table array
     * wait_table_bits      -- wait_table_size == (1 << wait_table_bits)
     *
     * The purpose of all these is to keep track of the people
     * waiting for a page to become available and make them
     * runnable again when possible. The trouble is that this
     * consumes a lot of space, especially when so few things
     * wait on pages at a given time. So instead of using
     * per-page waitqueues, we use a waitqueue hash table.
     *
     * The bucket discipline is to sleep on the same queue when
     * colliding and wake all in that wait queue when removing.
     * When something wakes, it must check to be sure its page is
     * truly available, a la thundering herd. The cost of a
     * collision is great, but given the expected load of the
     * table, they should be so rare as to be outweighed by the
     * benefits from the saved space.
     *
     * __wait_on_page_locked() and unlock_page() in mm/filemap.c, are the
     * primary users of these fields, and in mm/page_alloc.c
     * free_area_init_core() performs the initialization of them.
     */
    /*  進程等待隊列的散列表, 這些進程正在等待管理區中的某頁  */
    wait_queue_head_t       *wait_table;
    /*  等待隊列散列表中的調度實體數目  */
    unsigned long       wait_table_hash_nr_entries;
    /*  等待隊列散列表數組大小, 值爲2^order  */
    unsigned long       wait_table_bits;

    ZONE_PADDING(_pad1_)

    /* free areas of different sizes
       頁面使用狀態的信息,以每個bit標識對應的page是否可以分配
       是用於夥伴系統的,每個數組元素指向對應階也表的數組開頭
       以下是供頁幀回收掃描器(page reclaim scanner)訪問的字段
       scanner會跟據頁幀的活動情況對內存域中使用的頁進行編目
       如果頁幀被頻繁訪問,則是活動的,相反則是不活動的,
       在需要換出頁幀時,這樣的信息是很重要的:   */
    struct free_area    free_area[MAX_ORDER];

    /* zone flags, see below 描述當前內存的狀態, 參見下面的enum zone_flags結構 */
    unsigned long       flags;

    /* Write-intensive fields used from the page allocator, 保存該描述符的自旋鎖  */
    spinlock_t          lock;

    ZONE_PADDING(_pad2_)

    /* Write-intensive fields used by page reclaim */

    /* Fields commonly accessed by the page reclaim scanner */
    spinlock_t          lru_lock;   /* LRU(最近最少使用算法)活動以及非活動鏈表使用的自旋鎖  */
    struct lruvec       lruvec;

    /*
     * When free pages are below this point, additional steps are taken
     * when reading the number of free pages to avoid per-cpu counter
     * drift allowing watermarks to be breached
     * 在空閒頁的數目少於這個點percpu_drift_mark的時候
     * 當讀取和空閒頁數一樣的內存頁時,系統會採取額外的工作,
     * 防止單CPU頁數漂移,從而導致水印被破壞。
     */
    unsigned long percpu_drift_mark;

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
    /* pfn where compaction free scanner should start */
    unsigned long       compact_cached_free_pfn;
    /* pfn where async and sync compaction migration scanner should start */
    unsigned long       compact_cached_migrate_pfn[2];
#endif

#ifdef CONFIG_COMPACTION
    /*
     * On compaction failure, 1<<compact_defer_shift compactions
     * are skipped before trying again. The number attempted since
     * last failure is tracked with compact_considered.
     */
    unsigned int        compact_considered;
    unsigned int        compact_defer_shift;
    int                       compact_order_failed;
#endif

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
    /* Set to true when the PG_migrate_skip bits should be cleared */
    bool            compact_blockskip_flush;
#endif

    bool            contiguous;

    ZONE_PADDING(_pad3_)
    /* Zone statistics 內存域的統計信息, 參見後面的enum zone_stat_item結構 */
    atomic_long_t       vm_stat[NR_VM_ZONE_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;

字段

描述

watermark

每個 zone 在系統啓動時會計算出 3 個水位值, 分別爲 WMAKR_MIN, WMARK_LOW, WMARK_HIGH 水位, 這在頁面分配器和 kswapd 頁面回收中會用到

lowmem_reserve[MAX_NR_ZONES]

zone 中預留的內存, 爲了防止一些代碼必須運行在低地址區域,所以事先保留一些低地址區域的內存

pageset

page管理的數據結構對象,內部有一個page的列表(list)來管理。每個CPU維護一個page list,避免自旋鎖的衝突。這個數組的大小和NR_CPUS(CPU的數量)有關,這個值是編譯的時候確定的

lock

對zone併發訪問的保護的自旋鎖

free_area[MAX_ORDER]

頁面使用狀態的信息,以每個bit標識對應的page是否可以分配

lru_lock

LRU(最近最少使用算法)的自旋鎖

wait_table

待一個page釋放的等待隊列哈希表。它會被wait_on_page(),unlock_page()函數使用. 用哈希表,而不用一個等待隊列的原因,防止進程長期等待資源

wait_table_hash_nr_entries

哈希表中的等待隊列的數量

zone_pgdat

指向這個zone所在的pglist_data對象

zone_start_pfn

和node_start_pfn的含義一樣。這個成員是用於表示zone中的開始那個page在物理內存中的位置的present_pages, spanned_pages: 和node中的類似的成員含義一樣

name

zone的名字,字符串表示: “DMA”,”Normal” 和”HighMem”

totalreserve_pages

每個區域保留的不能被用戶空間分配的頁面數目

ZONE_PADDING

由於自旋鎖頻繁的被使用,因此爲了性能上的考慮,將某些成員對齊到cache line中,有助於提高執行的性能。使用這個宏,可以確定zone->lock,zone->lru_lock,zone->pageset這些成員使用不同的cache line.

managed_pages

zone 中被夥伴系統管理的頁面數量

spanned_pages

zone 中包含的頁面數量

present_pages

zone 中實際管理的頁面數量. 對一些體系結構來說, 其值和 spanned_pages 相等

lruvec

LRU 鏈表集合

vm_stat

zone 計數

4.2 ZONE_PADDING將數據保存在高速緩衝行

該結構比較特殊的地方是它由ZONE_PADDING分隔的幾個部分. 這是因爲堆zone結構的訪問非常頻繁.在多處理器系統中, 通常會有不同的CPU試圖同時訪問結構成員. 因此使用鎖可以防止他們彼此干擾, 避免錯誤和不一致的問題. 由於內核堆該結構的訪問非常頻繁, 因此會經常性地獲取該結構的兩個自旋鎖zone->lockzone->lru_lock

由於 struct zone 結構經常被訪問到, 因此這個數據結構要求以 L1 Cache 對齊. 另外, 這裏的 ZONE_PADDING( )zone->lockzone_lru_lock 這兩個很熱門的鎖可以分佈在不同的 Cahe Line 中.一個內存 node 節點最多也就幾個 zone, 因此 zone 數據結構不需要像 struct page 一樣關心數據結構的大小,

那麼數據保存在CPU高速緩存中, 那麼會處理得更快速. 高速緩衝分爲行, 每一行負責不同的內存區.內核使用ZONE_PADDING宏生成”填充”字段添加到結構中, 以確保每個自旋鎖處於自身的緩存行中

ZONE_PADDING宏定義在include/linux/mmzone.h?v4.7, line 105

/*
 * zone->lock and zone->lru_lock are two of the hottest locks in the kernel.
 * So add a wild amount of padding here to ensure that they fall into separate
 * cachelines.  There are very few zone structures in the machine, so space
 * consumption is not a concern here.
     */
#if defined(CONFIG_SMP)
    struct zone_padding
    {
            char x[0];
    } ____cacheline_internodealigned_in_smp;
    #define ZONE_PADDING(name)      struct zone_padding name;

#else
    #define ZONE_PADDING(name)
 #endif

內核還用了____cacheline_internodealigned_in_smp,來實現最優的高速緩存行對其方式.

該宏定義在include/linux/cache.h

#if !defined(____cacheline_internodealigned_in_smp)
    #if defined(CONFIG_SMP)
        #define ____cacheline_internodealigned_in_smp \
        __attribute__((__aligned__(1 << (INTERNODE_CACHE_SHIFT))))
    #else
        #define ____cacheline_internodealigned_in_smp
    #endif
#endif

4.3 水印watermark[NR_WMARK]與kswapd內核線程

Zone的管理調度的一些參數watermarks水印, 水存量很小(MIN)進水量,水存量達到一個標準(LOW)減小進水量,當快要滿(HIGH)的時候,可能就關閉了進水口

WMARK_LOW, WMARK_LOW, WMARK_HIGH就是這個標準

enum zone_watermarks
{
        WMARK_MIN,
        WMARK_LOW,
        WMARK_HIGH,
        NR_WMARK
};


#define min_wmark_pages(z) (z->watermark[WMARK_MIN])
#define low_wmark_pages(z) (z->watermark[WMARK_LOW])
#define high_wmark_pages(z) (z->watermark[WMARK_HIGH])

在linux-2.4中, zone結構中使用如下方式表示水印, 參照include/linux/mmzone.h?v=2.4.37, line 171

typedef struct zone_watermarks_s
{
    unsigned long min, low, high;
} zone_watermarks_t;


typedef struct zone_struct {
    zone_watermarks_t       watermarks[MAX_NR_ZONES];

在Linux-2.6.x中標準是直接通過成員pages_min, pages_low and pages_high定義在zone結構體中的, 參照include/linux/mmzone.h?v=2.6.24, line 214

當系統中可用內存很少的時候,系統進程kswapd被喚醒, 開始回收釋放page, 水印這些參數(WMARK_MIN, WMARK_LOW, WMARK_HIGH)影響着這個代碼的行爲

每個zone有三個水平標準:watermark[WMARK_MIN], watermark[WMARK_LOW], watermark[WMARK_HIGH],幫助確定zone中內存分配使用的壓力狀態

標準

描述

watermark[WMARK_MIN]

當空閒頁面的數量達到page_min所標定的數量的時候, 說明頁面數非常緊張, 分配頁面的動作和kswapd線程同步運行. WMARK_MIN所表示的page的數量值,是在內存初始化的過程中調用free_area_init_core中計算的。這個數值是根據zone中的page的數量除以一個>1的係數來確定的。通常是這樣初始化的ZoneSizeInPages/12

watermark[WMARK_LOW]

當空閒頁面的數量達到WMARK_LOW所標定的數量的時候,說明頁面剛開始緊張, 則kswapd線程將被喚醒,並開始釋放回收頁面

watermark[WMARK_HIGH]

當空閒頁面的數量達到page_high所標定的數量的時候, 說明內存頁面數充足, 不需要回收, kswapd線程將重新休眠,通常這個數值是page_min的3倍

table th:nth-of-type(1){ width: 30%; }

  • 如果空閒頁多於pages_high = watermark[WMARK_HIGH], 則說明內存頁面充足, 內存域的狀態是理想的.
  • 如果空閒頁的數目低於pages_low = watermark[WMARK_LOW], 則說明內存頁面開始緊張, 內核開始將頁回收到硬盤.
  • 如果空閒頁的數目低於pages_min = watermark[WMARK_MIN], 則內存頁面非常緊張, 頁回收工作的壓力就比較大

4.3 內存域標誌

內存管理域zone_t結構中的flags字段描述了內存域的當前狀態

//  http://lxr.free-electrons.com/source/include/linux/mmzone.h#L475
struct zone
{
    /* zone flags, see below */
    unsigned long           flags;
}

它允許使用的標識用enum zone_flags標識, 該枚舉標識定義在include/linux/mmzone.h?v4.7, line 525, 如下所示

enum zone_flags
{
    ZONE_RECLAIM_LOCKED,         /* prevents concurrent reclaim */
    ZONE_OOM_LOCKED,               /* zone is in OOM killer zonelist 內存域可被回收*/
    ZONE_CONGESTED,                 /* zone has many dirty pages backed by
                                                    * a congested BDI
                                                    */
    ZONE_DIRTY,                           /* reclaim scanning has recently found
                                                   * many dirty file pages at the tail
                                                   * of the LRU.
                                                   */
    ZONE_WRITEBACK,                 /* reclaim scanning has recently found
                                                   * many pages under writeback
                                                   */
    ZONE_FAIR_DEPLETED,           /* fair zone policy batch depleted */
};

flag標識

描述

ZONE_RECLAIM_LOCKED

防止併發回收, 在SMP上系統, 多個CPU可能試圖併發的回收億i個內存域. ZONE_RECLAIM_LCOKED標誌可防止這種情況: 如果一個CPU在回收某個內存域, 則設置該標識. 這防止了其他CPU的嘗試

ZONE_OOM_LOCKED

用於某種不走運的情況: 如果進程消耗了大量的內存, 致使必要的操作都無法完成, 那麼內核會使徒殺死消耗內存最多的進程, 以獲取更多的空閒頁, 該標誌可以放置多個CPU同時進行這種操作

ZONE_CONGESTED

標識當前區域中有很多髒頁

ZONE_DIRTY

用於標識最近的一次頁面掃描中, LRU算法發現了很多髒的頁面

ZONE_WRITEBACK

最近的回收掃描發現有很多頁在寫回

ZONE_FAIR_DEPLETED

公平區策略耗盡(沒懂)

4.4 內存域統計信息vm_stat

內存域struct zone的vm_stat維護了大量有關該內存域的統計信息. 由於其中維護的大部分信息曲面沒有多大意義

//  http://lxr.free-electrons.com/source/include/linux/mmzone.h#L522
struct zone
{
      atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
}

vm_stat的統計信息由enum zone_stat_item枚舉變量標識, 定義在include/linux/mmzone.h?v=4.7, line 110

enum zone_stat_item
{
    /* First 128 byte cacheline (assuming 64 bit words) */
    NR_FREE_PAGES,
    NR_ALLOC_BATCH,
    NR_LRU_BASE,
    NR_INACTIVE_ANON = NR_LRU_BASE, /* must match order of LRU_[IN]ACTIVE */
    NR_ACTIVE_ANON,         /*  "     "     "   "       "         */
    NR_INACTIVE_FILE,       /*  "     "     "   "       "         */
    NR_ACTIVE_FILE,         /*  "     "     "   "       "         */
    NR_UNEVICTABLE,         /*  "     "     "   "       "         */
    NR_MLOCK,               /* mlock()ed pages found and moved off LRU */
    NR_ANON_PAGES,  /* Mapped anonymous pages */
    NR_FILE_MAPPED, /* pagecache pages mapped into pagetables.
                       only modified from process context */
    NR_FILE_PAGES,
    NR_FILE_DIRTY,
    NR_WRITEBACK,
    NR_SLAB_RECLAIMABLE,
    NR_SLAB_UNRECLAIMABLE,
    NR_PAGETABLE,           /* used for pagetables */
    NR_KERNEL_STACK,
    /* Second 128 byte cacheline */
    NR_UNSTABLE_NFS,        /* NFS unstable pages */
    NR_BOUNCE,
    NR_VMSCAN_WRITE,
    NR_VMSCAN_IMMEDIATE,    /* Prioritise for reclaim when writeback ends */
    NR_WRITEBACK_TEMP,      /* Writeback using temporary buffers */
    NR_ISOLATED_ANON,       /* Temporary isolated pages from anon lru */
    NR_ISOLATED_FILE,       /* Temporary isolated pages from file lru */
    NR_SHMEM,               /* shmem pages (included tmpfs/GEM pages) */
    NR_DIRTIED,             /* page dirtyings since bootup */
    NR_WRITTEN,             /* page writings since bootup */
    NR_PAGES_SCANNED,       /* pages scanned since last reclaim */
#ifdef CONFIG_NUMA
    NUMA_HIT,               /* allocated in intended node */
    NUMA_MISS,              /* allocated in non intended node */
    NUMA_FOREIGN,           /* was intended here, hit elsewhere */
    NUMA_INTERLEAVE_HIT,    /* interleaver preferred this zone */
    NUMA_LOCAL,             /* allocation from local node */
    NUMA_OTHER,             /* allocation from other node */
#endif
    WORKINGSET_REFAULT,
    WORKINGSET_ACTIVATE,
    WORKINGSET_NODERECLAIM,
    NR_ANON_TRANSPARENT_HUGEPAGES,
    NR_FREE_CMA_PAGES,
    NR_VM_ZONE_STAT_ITEMS
};

內核提供了很多方式來獲取當前內存域的狀態信息, 這些函數大多定義在include/linux/vmstat.h?v=4.7

4.5 Zone等待隊列表(zone wait queue table)

struct zone中實現了一個等待隊列, 可用於等待某一頁的進程, 內核將進程排成一個列隊, 等待某些條件. 在條件變成真時, 內核會通知進程恢復工作.

struct zone
{
    wait_queue_head_t       *wait_table;
    unsigned long               wait_table_hash_nr_entries;
    unsigned long               wait_table_bits;
}

字段

描述

wait_table

待一個page釋放的等待隊列哈希表。它會被wait_on_page(),unlock_page()函數使用. 用哈希表,而不用一個等待隊列的原因,防止進程長期等待資源

wait_table_hash_nr_entries

哈希表中的等待隊列的數量

wait_table_bits

等待隊列散列表數組大小, wait_table_size == (1 << wait_table_bits)

當對一個page做I/O操作的時候,I/O操作需要被鎖住,防止不正確的數據被訪問。進程在訪問page前,wait_on_page_locked函數,使進程加入一個等待隊列

訪問完成後,UnlockPage函數解鎖其他進程對page的訪問。其他正在等待隊列中的進程被喚醒。每個page都可以有一個等待隊列,但是太多的分離的等待隊列使得花費太多的內存訪問週期。替代的解決方法,就是將所有的隊列放在struct zone數據結構中

也可以有一種可能,就是struct zone中只有一個隊列,但是這就意味着,當一個page unlock的時候,訪問這個zone裏內存page的所有休眠的進程將都被喚醒,這樣就會出現擁堵(thundering herd)的問題。建立一個哈希表管理多個等待隊列,能解決這個問題,zone->wait_table就是這個哈希表。哈希表的方法可能還是會造成一些進程不必要的喚醒。但是這種事情發生的機率不是很頻繁的。下面這個圖就是進程及等待隊列的運行關係:

等待隊列的哈希表的分配和建立在free_area_init_core函數中進行。哈希表的表項的數量在wait_table_size() 函數中計算,並且保持在zone->wait_table_size成員中。最大4096個等待隊列。最小是NoPages / PAGES_PER_WAITQUEUE的2次方,NoPages是zone管理的page的數量,PAGES_PER_WAITQUEUE被定義256

zone->wait_table_bits用於計算:根據page 地址得到需要使用的等待隊列在哈希表中的索引的算法因子. page_waitqueue()函數負責返回zone中page所對應等待隊列。它用一個基於struct page虛擬地址的簡單的乘法哈希算法來確定等待隊列的.

page_waitqueue()函數用GOLDEN_RATIO_PRIME的地址和“右移zone→wait_table_bits一個索引值”的一個乘積來確定等待隊列在哈希表中的索引的。

Zone的初始化, 在kernel page table通過paging_init()函數完全建立起z來以後,zone被初始化。下面章節將描述這個。當然不同的體系結構這個過程肯定也是不一樣的,但它們的目的卻是相同的:確定什麼參數需要傳遞給free_area_init()函數(對於UMA體系結構)或者free_area_init_node()函數(對於NUMA體系結構)。這裏省略掉NUMA體系結構的說明。

free_area_init()函數的參數:

unsigned long *zones_sizes: 系統中每個zone所管理的page的數量的數組。這個時候,還沒能確定zone中那些page是可以分配使用的(free)。這個信息知道boot memory allocator完成之前還無法知道。

4.6 冷熱頁與Per-CPU上的頁面高速緩存

內核經常請求和釋放單個頁框. 爲了提升性能, 每個內存管理區都定義了一個每CPU(Per-CPU)的頁面高速緩存. 所有”每CPU高速緩存”包含一些預先分配的頁框, 他們被定義滿足本地CPU發出的單一內存請求.

struct zone的pageset成員用於實現冷熱分配器(hot-n-cold allocator)

struct zone
{
    struct per_cpu_pageset __percpu *pageset;
};

內核說頁面是熱的, 意味着頁面已經加載到CPU的高速緩存, 與在內存中的頁相比, 其數據訪問速度更快. 相反, 冷頁則不再高速緩存中. 在多處理器系統上每個CPU都有一個或者多個告訴緩存. 各個CPU的管理必須是獨立的.

儘管內存域可能屬於一個特定的NUMA結點, 因而關聯到某個特定的CPU。 但其他CPU的告訴緩存仍然可以包含該內存域中的頁面. 最終的效果是, 每個處理器都可以訪問系統中的所有頁, 儘管速度不同. 因而, 特定於內存域的數據結構不僅要考慮到所屬NUMA結點相關的CPU, 還必須照顧到系統中其他的CPU.

pageset是一個指針, 其容量與系統能夠容納的CPU的數目的最大值相同.

數組元素類型爲per_cpu_pageset, 定義在include/linux/mmzone.h?v4.7, line 254 如下所示

struct per_cpu_pageset {
       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
};

該結構由一個per_cpu_pages pcp變量組成, 該數據結構定義如下, 位於include/linux/mmzone.h?v4.7, line 245

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[MIGRATE_PCPTYPES];
};

字段

描述

count

記錄了與該列表相關的頁的數目

high

是一個水印. 如果count的值超過了high, 則表明列表中的頁太多了

batch

如果可能, CPU的高速緩存不是用單個頁來填充的, 而是歐諾個多個頁組成的塊, batch作爲每次添加/刪除頁時多個頁組成的塊大小的一個參考值

list

一個雙鏈表, 保存了當前CPU的冷頁或熱頁, 可使用內核的標準方法處理

在內核中只有一個子系統會積極的嘗試爲任何對象維護per-cpu上的list鏈表, 這個子系統就是slab分配器.

  • struct per_cpu_pageset具有一個字段, 該字段
  • struct per_cpu_pages則維護了鏈表中目前已有的一系列頁面, 高極值和低極值決定了何時填充該集合或者釋放一批頁面, 變量決定了一個塊中應該分配多少個頁面, 並最後決定在頁面前的實際鏈表中分配多少各頁面

4.7 內存域的第一個頁幀zone_start_pfn

struct zone中通過zone_start_pfn成員標記了內存管理區的頁面地址.

然後內核也通過一些全局變量標記了物理內存所在頁面的偏移, 這些變量定義在mm/nobootmem.c?v4.7, line 31

unsigned long max_low_pfn;
unsigned long min_low_pfn;
unsigned long max_pfn;
unsigned long long max_possible_pfn;

PFN是物理內存以Page爲單位的偏移量

變量

描述

max_low_pfn

x86中,max_low_pfn變量是由find_max_low_pfn函數計算並且初始化的,它被初始化成ZONE_NORMAL的最後一個page的位置。這個位置是kernel直接訪問的物理內存, 也是關係到kernel/userspace通過“PAGE_OFFSET宏”把線性地址內存空間分開的內存地址位置

min_low_pfn

系統可用的第一個pfn是min_low_pfn變量, 開始與_end標號的後面, 也就是kernel結束的地方.在文件mm/bootmem.c中對這個變量作初始化

max_pfn

系統可用的最後一個PFN是max_pfn變量, 這個變量的初始化完全依賴與硬件的體系結構.

max_possible_pfn

x86的系統中, find_max_pfn函數通過讀取e820表獲得最高的page frame的數值, 同樣在文件mm/bootmem.c中對這個變量作初始化。e820表是由BIOS創建的

This is the physical memory directly accessible by the kernel and is related to the kernel/userspace split in the linear address space marked by PAGE OFFSET.

我理解爲這段地址kernel可以直接訪問,可以通過PAGE_OFFSET宏直接將kernel所用的虛擬地址轉換成物理地址的區段。在文件mm/bootmem.c中對這個變量作初始化。在內存比較小的系統中max_pfn和max_low_pfn的值相同

min_low_pfn, max_pfn和max_low_pfn這3個值,也要用於對高端內存(high memory)的起止位置的計算。在arch/i386/mm/init.c文件中會對類似的highstart_pfn和highend_pfn變量作初始化。這些變量用於對高端內存頁面的分配。後面將描述。

5 管理區表zone_table與管理區節點的映射

內核在初始化內存管理區時, 首先建立管理區表zone_table. 參見mm/page_alloc.c?v=2.4.37, line 38

/*
 *
 * The zone_table array is used to look up the address of the
 * struct zone corresponding to a given zone number (ZONE_DMA,
 * ZONE_NORMAL, or ZONE_HIGHMEM).
 */
zone_t *zone_table[MAX_NR_ZONES*MAX_NR_NODES];
EXPORT_SYMBOL(zone_table);

MAX_NR_ZONES是一個節點中所能包容納的管理區的最大數, 如3個, 定義在include/linux/mmzone.h?v=2.4.37, line 25, 與zone區域的類型(ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM)定義在一起. 當然這時候我們這些標識都是通過宏的方式來實現的, 而不是如今的枚舉類型

MAX_NR_NODES是可以存在的節點的最大數.

函數EXPORT_SYMBOL使得內核的變量或者函數可以被載入的模塊(比如我們的驅動模塊)所訪問.

該表處理起來就像一個多維數組, 在函數free_area_init_core中, 一個節點的所有頁面都會被初始化.

6 zonelist內存域存儲層次

6.1 內存域之間的層級結構

當前結點與系統中其他結點的內存域之前存在一種等級次序

我們考慮一個例子, 其中內核想要分配高端內存.

  1. 它首先企圖在當前結點的高端內存域找到一個大小適當的空閒段. 如果失敗, 則查看該結點的普通內存域. 如果還失敗, 則試圖在該結點的DMA內存域執行分配.
  2. 如果在3個本地內存域都無法找到空閒內存, 則查看其他結點. 在這種情況下, 備選結點應該儘可能靠近主結點, 以最小化由於訪問非本地內存引起的性能損失.

內核定義了內存的一個層次結構, 首先試圖分配”廉價的”內存. 如果失敗, 則根據訪問速度和容量, 逐漸嘗試分配”更昂貴的”內存.

高端內存是最廉價的, 因爲內核沒有任何部份依賴於從該內存域分配的內存. 如果高端內存域用盡, 對內核沒有任何副作用, 這也是優先分配高端內存的原因.

其次是普通內存域, 這種情況有所不同. 許多內核數據結構必須保存在該內存域, 而不能放置到高端內存域.

因此如果普通內存完全用盡, 那麼內核會面臨緊急情況. 所以只要高端內存域的內存沒有用盡, 都不會從普通內存域分配內存.

最昂貴的是DMA內存域, 因爲它用於外設和系統之間的數據傳輸. 因此從該內存域分配內存是最後一招.

6.2 zonelist結構

內核還針對當前內存結點的備選結點, 定義了一個等級次序. 這有助於在當前結點所有內存域的內存都用盡時, 確定一個備選結點

內核使用pg_data_t中的zonelist數組, 來表示所描述的層次結構.

typedef struct pglist_data {
    struct zonelist node_zonelists[MAX_ZONELISTS];
    /*  ......  */
}pg_data_t;

關於該結構zonelist的所有相關信息定義include/linux/mmzone.h?v=4.7, line 568, 我們下面慢慢來講.

node_zonelists數組對每種可能的內存域類型, 都配置了一個獨立的數組項.

該數組項的大小MAX_ZONELISTS用一個匿名的枚舉常量定義, 定義在include/linux/mmzone.h?v=4.7, line 571

enum
{
    ZONELIST_FALLBACK,      /* zonelist with fallback */
#ifdef CONFIG_NUMA
    /*
     * The NUMA zonelists are doubled because we need zonelists that
     * restrict the allocations to a single node for __GFP_THISNODE.
     */
    ZONELIST_NOFALLBACK,    /* zonelist without fallback (__GFP_THISNODE) */
#endif
    MAX_ZONELISTS
};

我們會發現在UMA結構下, 數組大小MAX_ZONELISTS = 1, 因爲只有一個內存結點, zonelist中只會存儲一個ZONELIST_FALLBACK類型的結構, 但是NUMA下需要多餘的ZONELIST_NOFALLBACK用以表示當前結點的信息

pg_data_t->node_zonelists數組項用struct zonelis結構體定義, 該結構包含了類型爲struct zoneref的一個備用列表由於該備用列表必須包括所有結點的所有內存域,因此由MAX_NUMNODES * MAX_NZ_ZONES項組成,外加一個用於標記列表結束的空指針

struct zonelist結構的定義在include/linux/mmzone.h?v=4.7, line 606

/*
 * One allocation request operates on a zonelist. A zonelist
 * is a list of zones, the first one is the 'goal' of the
 * allocation, the other zones are fallback zones, in decreasing
 * priority.
 *
 * To speed the reading of the zonelist, the zonerefs contain the zone index
 * of the entry being read. Helper functions to access information given
 * a struct zoneref are
 *
 * zonelist_zone()      - Return the struct zone * for an entry in _zonerefs
 * zonelist_zone_idx()  - Return the index of the zone for an entry
 * zonelist_node_idx()  - Return the index of the node for an entry
 */
struct zonelist {
    struct zoneref _zo

而struct zoneref結構的定義如下include/linux/mmzone.h?v=4.7, line 583

/*
 * This struct contains information about a zone in a zonelist. It is stored
 * here to avoid dereferences into large structures and lookups of tables
 */
struct zoneref {
    struct zone *zone;      /* Pointer to actual zone */
    int zone_idx;       /* zone_idx(zoneref->zone) */
};

6.3 內存域的排列方式

那麼我們內核是如何組織在zonelist中組織內存域的呢?

NUMA系統中存在多個節點, 每個節點對應一個struct pglist_data結構, 每個結點中可以包含多個zone, 如: ZONE_DMA, ZONE_NORMAL, 這樣就產生幾種排列順序, 以2個節點2個zone爲例(zone從高到低排列, ZONE_DMA0表示節點0的ZONE_DMA,其它類似).

  • Legacy方式, 每個節點只排列自己的zone;
  • Node方式, 按節點順序依次排列,先排列本地節點的所有zone,再排列其它節點的所有zone。
  • Zone方式, 按zone類型從高到低依次排列各節點的同相類型zone

可通過啓動參數”numa_zonelist_order”來配置zonelist order,內核定義了3種配置, 這些順序定義在mm/page_alloc.c?v=4.7, line 4551

// http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L4551
#define ZONELIST_ORDER_DEFAULT  0 /* 智能選擇Node或Zone方式 */

#define ZONELIST_ORDER_NODE     1 /* 對應Node方式 */

#define ZONELIST_ORDER_ZONE     2 /* 對應Zone方式 */

注意 在非NUMA系統中(比如UMA), 由於只有一個內存結點, 因此ZONELIST_ORDER_ZONE和ZONELIST_ORDER_NODE選項會配置相同的內存域排列方式, 因此, 只有NUMA可以配置這幾個參數

全局的current_zonelist_order變量標識了系統中的當前使用的內存域排列方式, 默認配置爲ZONELIST_ORDER_DEFAULT, 參見mm/page_alloc.c?v=4.7, line 4564

zonelist_order_name宏

排列方式

描述

ZONELIST_ORDER_DEFAULT

Default

由系統智能選擇Node或Zone方式

ZONELIST_ORDER_NODE

Node

Node方式

按節點順序依次排列,先排列本地節點的所有zone,再排列其它節點的所有zone

ZONELIST_ORDER_ZONE

Zone

Zone方式

按zone類型從高到低依次排列各節點的同相類型zone

6.4 build_all_zonelists初始化內存節點

內核通過build_all_zonelists初始化了內存結點的zonelists域

  • 首先內核通過set_zonelist_order函數設置了zonelist_order,如下所示, 參見mm/page_alloc.c?v=4.7, line 5031
  • 建立備用層次結構的任務委託給build_zonelists, 該函數爲每個NUMA結點都創建了相應的數據結構. 它需要指向相關的pg_data_t實例的指針作爲參數

7 總結

在linux中,內核也不是對所有物理內存都一視同仁,內核而是把頁分爲不同的區, 使用區來對具有相似特性的頁進行分組.

Linux必須處理如下兩種硬件存在缺陷而引起的內存尋址問題:

  1. 一些硬件只能用某些特定的內存地址來執行DMA
  2. 一些體系結構其內存的物理尋址範圍比虛擬尋址範圍大的多。這樣,就有一些內存不能永久地映射在內核空間上。

爲了解決這些制約條件,Linux使用了三種區:

  1. ZONE_DMA : 這個區包含的頁用來執行DMA操作。
  2. ZONE_NOMAL : 這個區包含的都是能正常映射的頁。
  3. ZONE_HIGHEM : 這個區包”高端內存”,其中的頁能不永久地映射到內核地址空間

而爲了兼容一些設備的熱插拔支持以及內存碎片化的處理, 內核也引入一些邏輯上的內存區.

  1. ZONE_MOVABLE : 內核定義了一個僞內存域ZONE_MOVABLE, 在防止物理內存碎片的機制memory migration中需要使用該內存域. 供防止物理內存碎片的極致使用
  2. ZONE_DEVICE : 爲支持熱插拔設備而分配的Non Volatile Memory非易失性內存區的實際使用與體系結構是相關的。linux把系統的內存結點劃分區, 一個區包含了若干個內存頁面, 形成不同的內存池,這樣就可以根據用途進行分配了

需要說明的是,區的劃分沒有任何物理意義, 只不過是內核爲了管理頁而採取的一種邏輯上的分組. 儘管某些分配可能需要從特定的區中獲得頁, 但這並不是說, 某種用途的內存一定要從對應的區來獲取,如果這種可供分配的資源不夠用了,內核就會佔用其他可用去的內存.

下表給出每個區及其在X86上所佔的列表

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章