Linux-3.14.12內存管理筆記【構建內存管理框架(1)】

傳統的計算機結構中,整個物理內存都是一條線上的,CPU訪問整個內存空間所須要的時間都是相同的。這種內存結構被稱之爲UMA(Uniform Memory Architecture,一致存儲結構)。可是隨着計算機的發展,一些新型的服務器結構中,尤爲是多CPU的狀況下,物理內存空間的訪問就難以控制所需的時間相同了。在多CPU的環境下,系統只有一條總線,有多個CPU都連接到上面,並且每一個CPU都有本身本地的物理內存空間,可是也能夠經過總線去訪問別的CPU物理內存空間,同時也存在着一些多CPU均可以共同訪問的公共物理內存空間。因而乎這就出現了一個新的狀況,因爲各類物理內存空間所處的位置不一樣,因而訪問它們的時間長短也就各異,無法保證一致。對於這種狀況的內存結構,被稱之爲NUMA(Non-Uniform Memory Architecture,非一致存儲結構)。事實上也沒有徹底的UMA,好比常見的單CPU電腦,RAM、ROM等物理存儲空間的訪問時間並不是一致的,只是純粹對RAM而言,是UMA的。此外還有一種稱之爲MPP的結構(Massive Parallel Processing,大規模並行處理系統),是由多個SMP服務器經過必定的節點互聯網絡進行鏈接,協同工做,完成相同的任務。從外界使用者看來,它是一個服務器系統。node

迴歸正題,着重看一下NUMA。因爲NUMA存儲結構的引入,這就須要相應的管理機制來支持, linux 2.4版本就已經開始對其支持了。隨着新增管理機制的支持,也隨之引入了Node的概念(存儲節點),把訪問時間相同的存儲空間歸結爲一個存儲節點。因而當前分析的3.14.12版本,linux的物理內存管理機制將物理內存劃分爲三個層次來管理,依次是:Node(存儲節點)、Zone(管理區)和Page(頁面)。linux

image

存儲節點的數據結構爲pg_data_t,每一個NUMA節點都有一個pg_data_t負責記載該節點的內存佈局信息。其中pg_data_t結構體中存儲管理區信息的爲node_zones成員,其數據結構爲zone,每一個pg_data_t都有多個node_zones,一般是三個:ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM。算法

ZONE_DMA區一般是因爲計算機中部分設備沒法直接訪問所有內存空間而特意劃分出來給該部分設備使用的區,x86環境中,該區一般小於16M。服務器

ZONE_NORMAL區位於ZONE_DMA後面,這個區域被內核直接映射到線性地址的高端部分,x86環境中,該區一般爲16M-896M。網絡

ZONE_HIGHMEM區則是系統除了ZONE_DMA和ZONE_NORMAL區後剩下的物理內存,這個區不能直接被內核映射,x86環境中,該區一般爲896M之後的內存。數據結構

爲何要有高端內存的存在?一般都知道內核空間的大小爲1G(線性空間爲:3-4G)。那麼映射這1G內存須要多少頁全局目錄項?很容易能夠算出來是256項,內核有這麼多線程在其中,1G夠嗎?很明顯不夠,若是要使用超出1G的內存空間怎麼辦?若是要使用內存,很明顯必需要作映射,那麼騰出幾個頁全局目錄項出來作映射?Bingo,就是這樣,那麼騰出多少來呢?linux內核的設計就是騰出32個頁全局目錄項,256的1/8。那麼32個頁全局目錄項對應多大的內存空間?算一下能夠知道是128M,也就是說直接映射的內存空間是896M。使用超過896M的內存空間視爲高端內存,一旦使用的時候,就須要作映射轉換,這是一件很耗資源的事情。因此不要常使用高端內存,就是這麼一個由來。多線程

接着看一下內存管理框架的初始化實現,initmem_init():app

【file:/arch/x86/mm/init_32.c】
#ifndef CONFIG_NEED_MULTIPLE_NODES
void __init initmem_init(void)
{
#ifdef CONFIG_HIGHMEM
    highstart_pfn = highend_pfn = max_pfn;
    if (max_pfn > max_low_pfn)
        highstart_pfn = max_low_pfn;
    printk(KERN_NOTICE "%ldMB HIGHMEM available.\n",
        pages_to_mb(highend_pfn - highstart_pfn));
    high_memory = (void *) __va(highstart_pfn * PAGE_SIZE - 1) + 1;
#else
    high_memory = (void *) __va(max_low_pfn * PAGE_SIZE - 1) + 1;
#endif
 
    memblock_set_node(0, (phys_addr_t)ULLONG_MAX, &memblock.memory, 0);
    sparse_memory_present_with_active_regions(0);
 
#ifdef CONFIG_FLATMEM
    max_mapnr = IS_ENABLED(CONFIG_HIGHMEM) ? highend_pfn : max_low_pfn;
#endif
    __vmalloc_start_set = true;
 
    printk(KERN_NOTICE "%ldMB LOWMEM available.\n",
            pages_to_mb(max_low_pfn));
 
    setup_bootmem_allocator();
}
#endif /* !CONFIG_NEED_MULTIPLE_NODES */

將high_memory初始化爲低端內存頁框max_low_pfn對應的地址大小,接着調用memblock_set_node,根據函數命名,能夠推斷出該函數用於給早前創建的memblock算法設置node節點信息。框架

memblock_set_node的實現:ide

【file:/mm/memblock.c】
/**
 * memblock_set_node - set node ID on memblock regions
 * @base: base of area to set node ID for
 * @size: size of area to set node ID for
 * @type: memblock type to set node ID for
 * @nid: node ID to set
 *
 * Set the nid of memblock @type regions in [@base,@base+@size) to @nid.
 * Regions which cross the area boundaries are split as necessary.
 *
 * RETURNS:
 * 0 on success, -errno on failure.
 */
int __init_memblock memblock_set_node(phys_addr_t base, phys_addr_t size,
                      struct memblock_type *type, int nid)
{
    int start_rgn, end_rgn;
    int i, ret;
 
    ret = memblock_isolate_range(type, base, size, &start_rgn, &end_rgn);
    if (ret)
        return ret;
 
    for (i = start_rgn; i < end_rgn; i++)
        memblock_set_region_node(&type->regions[i], nid);
 
    memblock_merge_regions(type);
    return 0;
}

memblock_set_node主要調用了三個函數作相關操做:memblock_isolate_range、memblock_set_region_node和memblock_merge_regions。

其中memblock_isolate_range:

【file:/mm/memblock.c】
/**
 * memblock_isolate_range - isolate given range into disjoint memblocks
 * @type: memblock type to isolate range for
 * @base: base of range to isolate
 * @size: size of range to isolate
 * @start_rgn: out parameter for the start of isolated region
 * @end_rgn: out parameter for the end of isolated region
 *
 * Walk @type and ensure that regions don't cross the boundaries defined by
 * [@base,@base+@size). Crossing regions are split at the boundaries,
 * which may create at most two more regions. The index of the first
 * region inside the range is returned in *@start_rgn and end in *@end_rgn.
 *
 * RETURNS:
 * 0 on success, -errno on failure.
 */
static int __init_memblock memblock_isolate_range(struct memblock_type *type,
                    phys_addr_t base, phys_addr_t size,
                    int *start_rgn, int *end_rgn)
{
    phys_addr_t end = base + memblock_cap_size(base, &size);
    int i;
 
    *start_rgn = *end_rgn = 0;
 
    if (!size)
        return 0;
 
    /* we'll create at most two more regions */
    while (type->cnt + 2 > type->max)
        if (memblock_double_array(type, base, size) < 0)
            return -ENOMEM;
 
    for (i = 0; i < type->cnt; i++) {
        struct memblock_region *rgn = &type->regions[i];
        phys_addr_t rbase = rgn->base;
        phys_addr_t rend = rbase + rgn->size;
 
        if (rbase >= end)
            break;
        if (rend <= base)
            continue;
 
        if (rbase < base) {
            /*
             * @rgn intersects from below. Split and continue
             * to process the next region - the new top half.
             */
            rgn->base = base;
            rgn->size -= base - rbase;
            type->total_size -= base - rbase;
            memblock_insert_region(type, i, rbase, base - rbase,
                           memblock_get_region_node(rgn),
                           rgn->flags);
        } else if (rend > end) {
            /*
             * @rgn intersects from above. Split and redo the
             * current region - the new bottom half.
             */
            rgn->base = end;
            rgn->size -= end - rbase;
            type->total_size -= end - rbase;
            memblock_insert_region(type, i--, rbase, end - rbase,
                           memblock_get_region_node(rgn),
                           rgn->flags);
        } else {
            /* @rgn is fully contained, record it */
            if (!*end_rgn)
                *start_rgn = i;
            *end_rgn = i + 1;
        }
    }
 
    return 0;
}

該函數主要作分割操做,在memblock算法創建時,只是判斷了flags是否相同,而後將連續內存作合併操做,而此時創建node節點,則根據入參base和size標記節點內存範圍將內存劃分開來。若是memblock中的region剛好以該節點內存範圍末尾劃分開來的話,那麼則將region的索引記錄至start_rgn,索引加1記錄至end_rgn返回回去;若是memblock中的region跨越了該節點內存末尾分界,那麼將會把當前的region邊界調整爲node節點內存範圍邊界,另外一部分經過memblock_insert_region()函數插入到memblock管理regions當中,以完成拆分。

順便看一下memblock_insert_region()函數:

【file:/mm/memblock.c】
/**
 * memblock_insert_region - insert new memblock region
 * @type: memblock type to insert into
 * @idx: index for the insertion point
 * @base: base address of the new region
 * @size: size of the new region
 * @nid: node id of the new region
 * @flags: flags of the new region
 *
 * Insert new memblock region [@base,@base+@size) into @type at @idx.
 * @type must already have extra room to accomodate the new region.
 */
static void __init_memblock memblock_insert_region(struct memblock_type *type,
                           int idx, phys_addr_t base,
                           phys_addr_t size,
                           int nid, unsigned long flags)
{
    struct memblock_region *rgn = &type->regions[idx];
 
    BUG_ON(type->cnt >= type->max);
    memmove(rgn + 1, rgn, (type->cnt - idx) * sizeof(*rgn));
    rgn->base = base;
    rgn->size = size;
    rgn->flags = flags;
    memblock_set_region_node(rgn, nid);
    type->cnt++;
    type->total_size += size;
}

這裏一個memmove()將後面的region信息日後移,另外調用memblock_set_region_node()將原region的node節點號保留在被拆分出來的region當中。

而memblock_set_region_node()函數實現僅是賦值而已:

【file:/mm/memblock.h】
static inline void memblock_set_region_node(struct memblock_region *r, int nid)
{
    r->nid = nid;
}

至此,回到memblock_set_node()函數,裏面接着memblock_isolate_range()被調用的memblock_set_region_node()已知是獲取node節點號,而memblock_merge_regions()則前面已經分析過了,是用於將region合併的。

最後回到initmem_init()函數中,memblock_set_node()返回後,接着調用的函數爲sparse_memory_present_with_active_regions()。

這裏sparse memory涉及到linux的一個內存模型概念。linux內核有三種內存模型:Flat memory、Discontiguous memory和Sparse memory。其分別表示:

  • Flat memory:顧名思義,物理內存是平坦連續的,整個系統只有一個node節點。
  • Discontiguous memory:物理內存不連續,內存中存在空洞,也於是系統將物理內存分爲多個節點,可是每一個節點的內部內存是平坦連續的。值得注意的是,該模型不只是對於NUMA環境而言,UMA環境上一樣可能存在多個節點的狀況。
  • Sparse memory:物理內存是不連續的,節點的內部內存也多是不連續的,系統也於是可能會有一個或多個節點。此外,該模型是內存熱插拔的基礎。

看一下sparse_memory_present_with_active_regions()的實現:

【file:/mm/page_alloc.c】
/**
 * sparse_memory_present_with_active_regions - Call memory_present for each active range
 * @nid: The node to call memory_present for. If MAX_NUMNODES, all nodes will be used.
 *
 * If an architecture guarantees that all ranges registered with
 * add_active_ranges() contain no holes and may be freed, this
 * function may be used instead of calling memory_present() manually.
 */
void __init sparse_memory_present_with_active_regions(int nid)
{
    unsigned long start_pfn, end_pfn;
    int i, this_nid;
 
    for_each_mem_pfn_range(i, nid, &start_pfn, &end_pfn, &this_nid)
        memory_present(this_nid, start_pfn, end_pfn);
}

裏面的for_each_mem_pfn_range()是一個旨在循環的宏定義,而memory_present()因爲實驗環境中沒有定義CONFIG_HAVE_MEMORY_PRESENT,因此是個空函數。暫且擱置不作深刻研究。

最後看一下initmem_init()退出前調用的函數setup_bootmem_allocator():

【file:/arch/x86/mm/init_32.c】
void __init setup_bootmem_allocator(void)
{
    printk(KERN_INFO " mapped low ram: 0 - %08lx\n",
         max_pfn_mapped<<PAGE_SHIFT);
    printk(KERN_INFO " low ram: 0 - %08lx\n", max_low_pfn<<PAGE_SHIFT);
}

原來該函數是用來初始化bootmem管理算法的,但如今x86的環境已經使用了memblock管理算法,這裏僅做保留打印部分信息。

相關文章
相關標籤/搜索