ARM64頁表映射過程

參考wowotech關於此topic的講解:

http://www.wowotech.net/memory_management/mem_init_3.html

內存初始化代碼分析(三):創建系統內存地址映射

作者:linuxer 發佈於:2016-11-24 12:08 分類:內存管理

一、前言

經過內存初始化代碼分析(一)內存初始化代碼分析(二)的過渡,我們終於來到了內存初始化的核心部分:paging_init。當然本文不能全部解析完該函數(那需要的篇幅太長了),我們只關注創建系統內存地址映射這部分代碼實現,也就是解析paging_init中的map_mem函數。

同樣的,我們選擇的是4.4.6的內核代碼,體系結構相關的代碼來自ARM64。

 

二、準備階段

在進入實際的代碼分析之前,我們首先回頭看看目前內存的狀態。偌大的物理地址空間中,系統內存佔據了一段或者幾段地址空間,這些信息被保存在了memblock模塊中的memory type類型的數組中,數組中每一個memory region描述了一段系統內存信息(base、size以及node id)。OK,系統內存就這麼多,但是也並非所有的memory type類型的數組中區域都是free的,實際上,有些已經被使用或者保留使用的內存區域需要從memory type類型的一段或者幾段地址空間中摘取下來,並定義在reserved type類型的數組中。實際上,在整個系統初始化過程中(更具體的說是內存管理模塊完成初始化之前),我們都需要暫時使用memblock這樣一個booting階段的內存管理模塊來暫時進行內存的管理(收集內存佈局信息只是它的副業),每次分配的內存都是在reserved type數組中增加一個新的item或者擴展其中一個memory region的size而已。

通過memblock模塊,我們已經收集了內存佈局的信息(memory type類型的數組),也知道了free memory資源的信息(memory type類型的數組減去reserved type類型的數組,從集合論的角度看,reserved type類型的數組是memory type類型的數組的真子集),但是,要管理這些珍貴的系統內存,首先要能夠訪問他們啊(順便說一句:memblock中的那些數組定義的地址都是物理地址),通過前面的分析文章,我們知道有兩段內存已經是可見的(完成了地址映射),一個是kernel image段,另外一個是fdt段。而廣大的系統內存區域仍然在黑暗之中,等待我們去拯救(進行地址映射)。

最後,我們思考這樣一個問題:是否memory type類型的數組代表了整個的系統內存的地址空間呢?當然不是,有些驅動可能會保留一段系統內存區域爲自己使用,同時也不希望OS管理這段內存(或者說對OS不可見),而是自己創建該段內存的地址映射。如果你對dts中的memory reserve節點比較熟悉的話,那麼實際上這樣的reserved memory region是有no-map屬性的。這時候,內核初始化過程中,在解析該reserved-memory節點的時候,會將該段地址從memblock模塊中移除。而在map_mem函數中,爲所有memory type類型的數組創建地址映射的時候,有no-map屬性的那段內存地址將不會創建地址映射,也就不在OS的控制範圍內了。

 

三、概覽

創建系統內存地址映射的代碼在map_mem中,如下:

static void __init map_mem(void)  {
    struct memblock_region *reg;
    phys_addr_t limit;

    limit = PHYS_OFFSET + SWAPPER_INIT_MAP_SIZE;---------------(1)
    memblock_set_current_limit(limit);

    for_each_memblock(memory, reg) {------------------------(2)
        phys_addr_t start = reg->base;――確定該region的起始地址
        phys_addr_t end = start + reg->size; ――確定該region的結束地址

        if (start >= end)--參數檢查
            break;

        if (ARM64_SWAPPER_USES_SECTION_MAPS) {----------------(3)
            if (start < limit)
                start = ALIGN(start, SECTION_SIZE);
            if (end < limit) {
                limit = end & SECTION_MASK;
                memblock_set_current_limit(limit);
            }
        }
        __map_memblock(start, end);-------------------------(4)
    }

    memblock_set_current_limit(MEMBLOCK_ALLOC_ANYWHERE);----------(5)
}

(1)首先限制了當前memblock的上限。之所以這麼做是因爲在進行mapping的時候,如果任何一級的Translation table不存在的話都需要進行頁表內存的分配。而在這個時間點上,夥伴系統沒有ready,無法動態分配。當然,這時候memblock已經ready了,但是如果分配的內存都還沒有創建地址映射(整個物理內存佈局已知並且保存在了memblock模塊中的memblock模塊中,但是並非所有系統內存的地址映射都已經建立好的,而我們map_mem函數的本意就是要創建所有系統內存的mapping),內核一旦訪問memblock_alloc分配的物理內存,悲劇就會發生了。怎麼破?這裏採用了限定memblock上限的方法。一旦設定了上限,那麼memblock_alloc分配的物理內存不會高於這個上限。

設定怎樣的上限呢?基本思路就是在map_mem的調用過程中,不需要分配translation table,怎麼做到呢?當然是儘量利用已經靜態定義好的那些頁表了。PHYS_OFFSET是物理內存的起始地址,SWAPPER_INIT_MAP_SIZE 是啓動階段kernel direct mapping的size。也就是說,從PHYS_OFFSET到PHYS_OFFSET + SWAPPER_INIT_MAP_SIZE的區域,所有的頁表(各個level的translation table)都已經OK,不需要分配,只需要把描述符寫入頁表即可。因此,如果將當前memblock分配的上限設定在這裏將不會產生內存分配的動作(因爲頁表都已經ready)。

(2)對系統中所有的memory type的region建立對應的地址映射。由於reserved type的memory region是memory type的region的真子集,因此reserved memory 的地址映射也就一併建立了。

(3)如果不使用section map,那麼我們在kernel direct mapping區域靜態分配了PGD~PTE的頁表,通過起始地址對齊以及對memblock limit的設定就可以保證在create_mapping()的時候不分配頁表內存。但是在下面的情況下:

    (A)Memory block的start或者end地址並沒有對齊在2M上

    (B)使用section map

在這種情況下,調用create_mapping()的時候會分配pte頁表內存(沒有對齊2M,無法進行section mapping)。怎麼破?還好第一個memory block(也就是kernel image所在的block)的start address是必定對齊在2M地址上的,所以只要考慮end地址,這時候需要適當的縮小limit到end & SECTION_MASK就可以保證分配的頁表內存是已經建立地址映射的了。

(4)__map_memblock代碼如下:

static void __init __map_memblock(phys_addr_t start, phys_addr_t end)
{
    create_mapping(start, __phys_to_virt(start), end - start,
            PAGE_KERNEL_EXEC);
}

需要說明的是,在map_mem之後,所有之前通過__create_page_tables創建的描述符都被覆蓋了,取而代之的是新的映射,並且memory attribute如下:

#define PAGE_KERNEL_EXEC    __pgprot(_PAGE_DEFAULT | PTE_UXN | PTE_DIRTY | PTE_WRITE)

大部分memory attribute保持不變(例如MT_NORMAL、PTE_AF 、 PTE_SHARED等),有幾個bit需要說明一下:PTE_UXN,Unprivileged Execute-never bit,也就是限制userspace從這裏取指執行。PTE_DIRTY是一個軟件設定的bit,硬件並不操作這個bit,OS軟件用這個bit標識該entry是clean or dirty,如果是dirty的話,說明該page的數據已經被寫入過,如果該page需要被swapped out,那麼還需要保存dirty的數據才能回收該page。關於PTE_WRITE的解釋todo。

(5)所有的系統內存的地址映射已經建立完畢,取消之前的上限,讓memblock模塊可以自由的分配內存。

 

四、填充PGD中的描述符

create_mapping實際上是調用底層的__create_mapping函數完成地址映射的,具體代碼如下:

static void __init create_mapping(phys_addr_t phys, unsigned long virt,
                  phys_addr_t size, pgprot_t prot)
{
    if (virt < VMALLOC_START) {
        pr_warn("BUG: not creating mapping for %pa at 0x%016lx - outside kernel range\n",
            &phys, virt);
        return;
    }
    __create_mapping(&init_mm, pgd_offset_k(virt & PAGE_MASK), phys, virt,
             size, prot, early_alloc);
}

create_mapping的作用是將起始物理地址等於phys,大小是size的這一段物理內存mapping到起始虛擬地址是virt的虛擬地址空間,映射的memory attribute是prot。內核的虛擬地址空間從VMALLOC_START開始,低於這個地址就不對了,這裏需要補充一下,前面講到arm32的頁面映射時注意到映射的虛擬地址最大不能超過highmem的界限,而highmem的界限爲VMALLOC_START-8MB空間,所以這裏看到這個地方的時候就有了一個小疑惑,爲什麼arm64需要大於這個VMALLOC_START呢?

首先ARM64不存在highmem的概念,然後再看VMALLOC_START定義:

#define VMALLOC_START		(VA_START)
#define VA_START		(UL(0xffffffffffffffff) << VA_BITS)
#define PAGE_OFFSET		(UL(0xffffffffffffffff) << (VA_BITS - 1))
#define __phys_to_virt(x)	((unsigned long)((x) - PHYS_OFFSET + PAGE_OFFSET))

通過以上代碼可得VMALLOC_START=0xFFFFFF8000000000,而PAGE_OFFSET=0xFFFFFFC000000000

memory type的virt值對應的物理內存值除非不屬於物理內存空間範圍,否則肯定大於VMALLOC_START。

驗證完虛擬地址,底層是調用__create_mapping函數,傳遞的參數情況是這樣的,init_mm是內核空間的內存描述符,pgd_offset_k是根據給定的虛擬地址,在kernel space的pgd中找到對應的描述符的位置,early_alloc是在mapping過程中,如果需要分配內存的話(頁表需要內存),調用該函數進行內存的分配。__create_mapping函數具體代碼如下:

static void  __create_mapping(struct mm_struct *mm, pgd_t *pgd,
                    phys_addr_t phys, unsigned long virt,
                    phys_addr_t size, pgprot_t prot,
                    void *(*alloc)(unsigned long size))
{
    unsigned long addr, length, end, next;

    addr = virt & PAGE_MASK;------------------------(1)
    length = PAGE_ALIGN(size + (virt & ~PAGE_MASK));

    end = addr + length;
    do {----------------------------------(2)
        next = pgd_addr_end(addr, end);--------------------(3)
        alloc_init_pud(mm, pgd, addr, next, phys, prot, alloc);----------(4)
        phys += next - addr;
    } while (pgd++, addr = next, addr != end);
}

創建地址映射熟悉要明確地址空間,不同的進程有不同的地址空間,struct mm_struct就是描述一個進程的虛擬地址空間,當然,我們這裏的場景是爲內核虛擬地址空間而創建地址映射,因此傳遞的參數是init_mm。需要創建地址映射的起始虛擬地址是virt,該虛擬地址對應的PUD中的描述符是一個8B的內存,pgd就是指向這個描述符內存的指針。

(1)因爲地址映射的最小單位是page,因此這裏進行mapping的虛擬地址需要對齊到page size,同樣的,長度也需要對齊到page size。經過對齊運算,(addr,length)定義的地址範圍應該是囊括(virt,size)定義的地址範圍,並且是對齊到page的。

(2)(addr,length)這個虛擬地址範圍可能需要佔據多個PGD entry,因此這裏我們需要一個循環,不斷的調用alloc_init_pud函數來完成(addr,length)這個虛擬地址範圍的映射,當然,alloc_init_pud函數其實也會建立下游(例如PUD、PMD、PTE)翻譯表的entry。

(3)pgd中的一個描述符只能mapping有限區域的虛擬地址(PGDIR_SIZE),pgd_addr_end的宏就是計算addr所在區域的end address。如果計算出來的end address小於傳入的end地址參數,那麼返回end參數值。也就是說,如果(addr,length)這個虛擬地址範圍的mapping需要跨越多個pgd entry,那麼next變量保存了下一個pgd entry的起始虛擬地址。

(4)這個函數有兩個作用,一是填充pgd entry,二是創建後續的pud translation table(如果需要的話)並進行下游Translation table的建立。

 

五、分配PUD頁表內存並填充相應的描述符

alloc_init_pud並非只是操作pud,實際上它是操作pgd的一個entry,並分配初始pud以及後續translation table的。填充PGD的entry需要給出對應PUD translation table的內存地址,如果PUD不存在,那麼alloc_init_pud還需要分配PUD translation table(page size),只有得到PUD翻譯表的物理內存地址,我們才能填充PGD entry。具體代碼如下:

static void alloc_init_pud(struct mm_struct *mm, pgd_t *pgd,
                  unsigned long addr, unsigned long end,
                  phys_addr_t phys, pgprot_t prot,
                  void *(*alloc)(unsigned long size))
{
    pud_t *pud;
    unsigned long next;

    if (pgd_none(*pgd)) {--------------------------(1)
        pud = alloc(PTRS_PER_PUD * sizeof(pud_t));
        pgd_populate(mm, pgd, pud);
    } 
    pud = pud_offset(pgd, addr); ---------------------(2)
    do { --------------------------------(3)
        next = pud_addr_end(addr, end);

        if (use_1G_block(addr, next, phys)) { ----------------(4)
            pud_t old_pud = *pud;
            set_pud(pud, __pud(phys | pgprot_val(mk_sect_prot(prot)))); -----(5)

            if (!pud_none(old_pud)) { ---------------------(6)
                flush_tlb_all(); ------------------------(7)
                if (pud_table(old_pud)) {
                    phys_addr_t table = __pa(pmd_offset(&old_pud, 0));
                    if (!WARN_ON_ONCE(slab_is_available()))
                        memblock_free(table, PAGE_SIZE); ------------(8)
                }
            }
        } else {
            alloc_init_pmd(mm, pud, addr, next, phys, prot, alloc);
        }
        phys += next - addr;
    } while (pud++, addr = next, addr != end);
}

(1)如果當前pgd entry是全0的話,說明還沒有對應的下級PUD頁表內存,因此需要進行PUD頁表內存的分配。需要說明的是這時候,夥伴系統沒有ready,分配內存仍然使用memblock模塊,pgd_populate用來建立pgd entry和PUD 頁表內存的關係。

(2)至此,pud的頁表內存已經有了,但是addr對應PUD中的哪一個描述符呢?pud_offset給出了答案,其返回的指針指向傳入參數addr地址對應的pud 描述符內存,而我們隨後的任務就是填充pud entry了。

(3)雖然(addr,end)之間的虛擬地址範圍共享一個pgd entry,但是這個地址範圍對應的pud entry可能有多個,通過循環,逐一填充pud entry,同時分配並初始化下一階頁表。

(4)如果沒有可能存在的1G block地址映射,這裏的代碼邏輯和上一節中的類似,只不過不斷的循環調用alloc_init_pud改成alloc_init_pmd即可。然而,ARM64的MMU硬件提供了灰常強大的功能,系統支持1G size的block mapping,如果能夠應用會獲取非常多的好處:不需要分配下級的translation table節省了內存,更重要的是大大降低了TLB miss,提高了性能。既然這麼好,當然要使用,不過有什麼條件呢?首先系統配置必須是4k的page size,這種配置下,一個PUD entry可以覆蓋1G的memory block。此外,起止虛擬地址以及映射到的物理地址都必須要對齊在1G size上。

(5)填寫一個PUD描述符,一次搞定1G size的address mapping,沒有PMD和PTE的頁表內存,沒有對PMD 和PTE描述符的訪問,多麼簡單,多麼美妙啊。假設系統內存4G,並且物理地址對齊在1G上(虛擬地址PAGE_OFFSET本來就是對齊在1G的),那麼4個PUD的描述符就搞定了內核空間的線性地址映射區間。

(6)如果pud entry是非空的,那麼就說明之前已經有了對該段地址的mapping(也許是隻映射了部分)。一個簡單的例子就是起始階段的kernel image mapping,在__create_page_tables創建pud 以及pmd中entry。如果不能進行section mapping,那麼還建立了PTE中的描述符,現在這些描述符都沒有用了,我們可以丟棄它們了。

(7)雖然建立了新的頁表,但是舊的頁表還殘留在了TLB中,必須將其“趕盡殺絕”,清除出TLB。

(8)如果pud指向了一個table描述符,也就是說明該entry指向一個PMD table,那麼需要釋放其memory。

 

六、分配PMD頁表內存並填充相應的描述符

1G block mapping雖好,不一定適合所有的系統,下面我一起看看PUD entry中填充的是block descriptor的情況(描述符指向PMD translation table):

static void alloc_init_pmd(struct mm_struct *mm, pud_t *pud,
                  unsigned long addr, unsigned long end,
                  phys_addr_t phys, pgprot_t prot,
                  void *(*alloc)(unsigned long size))
{
    pmd_t *pmd;
    unsigned long next;

    if (pud_none(*pud) || pud_sect(*pud)) {-------------------(1)
        pmd = alloc(PTRS_PER_PMD * sizeof(pmd_t));---分配pmd頁表內存
        if (pud_sect(*pud)) {--------------------------(2)
            split_pud(pud, pmd);
        }
        pud_populate(mm, pud, pmd);---------------------(3)
        flush_tlb_all();
    }
    BUG_ON(pud_bad(*pud));

    pmd = pmd_offset(pud, addr);-----------------------(4)
    do {
        next = pmd_addr_end(addr, end);
        if (((addr | next | phys) & ~SECTION_MASK) == 0) {------------(5)
            pmd_t old_pmd =*pmd;
            set_pmd(pmd, __pmd(phys | pgprot_val(mk_sect_prot(prot))));

            if (!pmd_none(old_pmd)) {----------------------(6)
                flush_tlb_all();
                if (pmd_table(old_pmd)) {
                    phys_addr_t table = __pa(pte_offset_map(&old_pmd, 0));
                    if (!WARN_ON_ONCE(slab_is_available()))
                        memblock_free(table, PAGE_SIZE);
                }
            }
        } else {
            alloc_init_pte(pmd, addr, next, __phys_to_pfn(phys),
                       prot, alloc);
        }
        phys += next - addr;
    } while (pmd++, addr = next, addr != end);
}

 

(1)有兩個場景需要分配PMD的頁表內存,一個是該pud entry是空的,我們需要分配後續的PMD頁表內存。另外一個是舊的pud entry是section 描述符,映射了1G的address block。但是現在由於種種原因,我們需要修改它,故需要remove這個1G block的section mapping。

(2)雖然是建立新的mapping,但是原來舊的1G mapping也要保留的,也許這次我們只是想更新部分地址映射呢。在這種情況下,我們先通過split_pud函數調用把一個1G block mapping轉換成通過pmd進行mapping的形式(一個pud的section mapping描述符(1G size)變成了512個pmd中的section mapping描述符(2M size)。形式變了,味道不變,加量不加價,仍然是1G block的地址映射。

(3)修正pud entry,讓其指向新的pmd頁表內存,同時flush tlb的內容。

(4)下面這段代碼的邏輯起始是和alloc_init_pud是類似的。如果不能進行2M的section mapping,那麼就循環調用alloc_init_pte進行地址的mapping,這裏我們就不多說了,重點看看2M section mapping的處理。

(5)如果滿足2M section的要求,那麼就調用set_pmd填充pmd entry。

(6)如果有舊的section mapping,並且指向一個PTE table,那麼還需要釋放這些不需要的PTE頁表描述符佔用的內存。

 

七、分配PTE頁表內存並填充相應的描述符

static void alloc_init_pte(pmd_t *pmd, unsigned long addr,
                  unsigned long end, unsigned long pfn,
                  pgprot_t prot,
                  void *(*alloc)(unsigned long size))
{
    pte_t *pte;

    if (pmd_none(*pmd) || pmd_sect(*pmd)) {----------------(1)
        pte = alloc(PTRS_PER_PTE * sizeof(pte_t));
        if (pmd_sect(*pmd))
            split_pmd(pmd, pte);----------------------(2)
        __pmd_populate(pmd, __pa(pte), PMD_TYPE_TABLE);--------(3)
        flush_tlb_all();
    }
    BUG_ON(pmd_bad(*pmd)); 

    pte = pte_offset_kernel(pmd, addr);
    do {
        set_pte(pte, pfn_pte(pfn, prot));-------------------(4)
        pfn++;
    } while (pte++, addr += PAGE_SIZE, addr != end);
}

 

(1)走到這個函數,說明後續需要建立PTE這一個level的頁表描述符,因此,需要分配PTE頁表內存,場景有兩個,一個是從來沒有進行過映射,另外一個是已經建立映射,但是是section mapping,不符合要求。

(2)如果之前有section mapping,那麼我們需要將其分裂成512個pte中的page descriptor。

(3)讓pmd entry指向新的pte頁表內存。需要說明的是:如果之前pmd entry是空的,那麼新的pte頁表中有512個invalid descriptor,如果之前有section mapping,那麼實際上這個新的PTE頁表已經通過split_pmd填充了512個page descritor。

(4)循環設定(addr,end)這段地址區域的pte中的page descriptor。

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