1. 程式人生 > >Linux啟動時的頁表對映

Linux啟動時的頁表對映

核心啟動時進行記憶體對映, map_mem()->create_mapping()

核心支援4級對映(PGD->PUD->PMD->PTE) ,支援的level由巨集CONFIG_PGTABLE_LEVELS定義,目前為3級對映,也即PGD->PMD->PTE

從下圖可以知道,每級頁表分別使用虛擬地址的9位作為索引,也即每級頁表大小為512.  虛擬地址最後12bit作為頁內索引.從而表示一個具體的實體地址

頁表,虛擬地址,實體地址關係

一、前言

經過記憶體初始化程式碼分析(一)記憶體初始化程式碼分析(二)的過渡,我們終於來到了記憶體初始化的核心部分: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開始,低於這個地址就不對了,驗證完虛擬地址,底層是呼叫__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。