天天看點

Linux核心記憶體管理-頁表的映射過程

作者:核心中文社群

Linux下的頁表映射分為兩種,一是Linux自身的頁表映射,另一種是ARM32 MMU硬體的映射。

1. ARM32頁表映射

由于ARM32和Linux核心維護的頁表項有所不同,是以維護了兩套PTE。

PGD存放在swapper_pd_dir中,一個PGD目錄項其實包含了兩份ARM32 PGD。

是以再配置設定PTE的時候,共配置設定了1024個PTE,512個給Linux OS維護用;512個給ARM32 MMU用,對應兩個PGD的頁表數目。

由于Linux OS和ARM32的PTE緊鄰,是以兩者的轉換也友善進行。

1.1 ARM32處理器查詢頁表

32bit的Linux采用三級映射:PGD-->PMD-->PTE,64bit的Linux采用四級映射:PGD-->PUD-->PMD-->PTE,多了個PUD。

縮寫是PGD:Page Global Directory、PUD:Page Upper Directory、PMD:Page Middle Directory、PTE:Page Table Entry。

在ARM32 Linux采用兩層映射,省略了PMD,除非在定義了CONFIG_ARM_LPAE才會使用3級映射。

在ARM32架構中,可以按段(section)來映射,這是采用單層映射模式。

使用頁面映射需要兩層映射結構,頁面可以是64KB或4KB大小。

Linux核心記憶體管理-頁表的映射過程

需要的小夥伴私信回複核心免費領取

1.1.1 ARM32架構MMU4KB頁面映射過程

如果采用頁表映射的方式,段映射表就變成一級映射表(Linux中稱為PGD),其頁表項提供的不再是實體位址,而是二級頁表的基位址。

32位虛拟位址的高12位(bit[31:20])作為通路一級頁表的索引值,找到相應的表項,每個表項指向一個二級頁表。

以虛拟位址的次8位(bit[19:12])作為通路二級頁表的索引值,得到相應的頁表項,從這個頁表項中找到20位的實體頁面位址。

最後将這20位實體頁面位址和虛拟位址的低12位拼湊在一起,得到最終的32位實體位址。

這個過程在ARM32架構中由MMU硬體完成,軟體不需要接入。

Linux核心記憶體管理-頁表的映射過程

1.1.2 ARMv7-AR中關于Short Descriptor映射概覽圖

關于4K頁表的映射過程在ARMv7-AR使用者架構手冊有關介紹。

一個位址映射的概覽圖,32位虛拟位址從TTBR1中找到First-level table位址,然後取虛拟位址VA[31:20]作為序号找到Second-level table位址。

取虛拟位址VA[19:12]作為序号找到Page位址。

Linux核心記憶體管理-頁表的映射過程

規格書中Small Page映射過程

Figure B3-11 Small page address translation是映射的細節:

Linux核心記憶體管理-頁表的映射過程

1.2 Linux頁表映射相關資料結構

我們知道在map_lowmem()使用create_mapping()建立頁表映射,這個函數的參數結構是struct map_desc。

下面來研究它的相關結構,有助于了解核心是如何處理頁表映射的。

arch\arm\include\asm\mach\map.h:

struct map_desc {
    unsigned long virtual;------虛拟位址起始位址
    unsigned long pfn;----------實體位址開始頁幀号
    unsigned long length;-------記憶體空間大小
    unsigned int type;----------mem_types中的序号
};
           

map_desc中的type指向類型為struct mem_type的mem_types數組:

arch\arm\mm\mm.h:
struct mem_type {
    pteval_t prot_pte;------------PTE屬性
    pteval_t prot_pte_s2;---------定義CONFIG_ARM_LPAE才有效
    pmdval_t prot_l1;-------------PMD屬性
    pmdval_t prot_sect;-----------Section類型映射
    unsigned int domain;----------定義ARM中不同的域
};

arch\arm\mm\mmu.c:
static struct mem_type mem_types[] = {
...
    [MT_MEMORY_RWX] = {
        .prot_pte  = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY,----------------------注意這裡都是L_PTE_*類型,需要在寫入MMU對應PTE時進行轉換。
        .prot_l1   = PMD_TYPE_TABLE,
        .prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
        .domain    = DOMAIN_KERNEL,
    },
    [MT_MEMORY_RW] = {
        .prot_pte  = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
                 L_PTE_XN,
        .prot_l1   = PMD_TYPE_TABLE,
        .prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
        .domain    = DOMAIN_KERNEL,
    },
...
}
           

下面重點關注Page Table類型的一級頁表和二級頁表的細節,以及Linux核心中的定義:

ARM32中PGD定義

下面是First-level descriptor詳細說明:

Linux核心記憶體管理-頁表的映射過程
/*
 * Hardware page table definitions.
 *
 * + Level 1 descriptor (PMD)
 *   - common
 */
#define PMD_TYPE_MASK        (_AT(pmdval_t, 3) << 0)---------------------------01對應PageTable
#define PMD_TYPE_FAULT        (_AT(pmdval_t, 0) << 0)
#define PMD_TYPE_TABLE        (_AT(pmdval_t, 1) << 0)
#define PMD_TYPE_SECT        (_AT(pmdval_t, 2) << 0)
#define PMD_PXNTABLE        (_AT(pmdval_t, 1) << 2)     /* v7 */
#define PMD_BIT4        (_AT(pmdval_t, 1) << 4)
#define PMD_DOMAIN(x)        (_AT(pmdval_t, (x)) << 5)
#define PMD_PROTECTION        (_AT(pmdval_t, 1) << 9)        /* v5 */
           

ARM32中PTE定義

下面是Second-level descriptor詳細說明:

Linux核心記憶體管理-頁表的映射過程
/*
 * + Level 2 descriptor (PTE)
 *   - common
 */
#define PTE_TYPE_MASK        (_AT(pteval_t, 3) << 0)
#define PTE_TYPE_FAULT        (_AT(pteval_t, 0) << 0)
#define PTE_TYPE_LARGE        (_AT(pteval_t, 1) << 0)
#define PTE_TYPE_SMALL        (_AT(pteval_t, 2) << 0)
#define PTE_TYPE_EXT        (_AT(pteval_t, 3) << 0)        /* v5 */
#define PTE_BUFFERABLE        (_AT(pteval_t, 1) << 2)
#define PTE_CACHEABLE        (_AT(pteval_t, 1) << 3)

/*
 *   - extended small page/tiny page
 */
#define PTE_EXT_XN        (_AT(pteval_t, 1) << 0)        /* v6 */
#define PTE_EXT_AP_MASK        (_AT(pteval_t, 3) << 4)
#define PTE_EXT_AP0        (_AT(pteval_t, 1) << 4)
#define PTE_EXT_AP1        (_AT(pteval_t, 2) << 4)
#define PTE_EXT_AP_UNO_SRO    (_AT(pteval_t, 0) << 4)
#define PTE_EXT_AP_UNO_SRW    (PTE_EXT_AP0)
#define PTE_EXT_AP_URO_SRW    (PTE_EXT_AP1)
#define PTE_EXT_AP_URW_SRW    (PTE_EXT_AP1|PTE_EXT_AP0)
#define PTE_EXT_TEX(x)        (_AT(pteval_t, (x)) << 6)    /* v5 */
#define PTE_EXT_APX        (_AT(pteval_t, 1) << 9)        /* v6 */
#define PTE_EXT_COHERENT    (_AT(pteval_t, 1) << 9)        /* XScale3 */
#define PTE_EXT_SHARED        (_AT(pteval_t, 1) << 10)    /* v6 */
#define PTE_EXT_NG        (_AT(pteval_t, 1) << 11)    /* v6 */
           

Linux中PTE定義

由于Linux對于PTE的定義和ARM硬體不一緻,下面的L_開頭的定義都是針對Linux的,L_MT開頭的是bit[5:2]表示的記憶體類型。

/*
 * "Linux" PTE definitions.
 *
 * We keep two sets of PTEs - the hardware and the linux version.
 * This allows greater flexibility in the way we map the Linux bits
 * onto the hardware tables, and allows us to have YOUNG and DIRTY
 * bits.
 *
 * The PTE table pointer refers to the hardware entries; the "Linux"
 * entries are stored 1024 bytes below.
 */
#define L_PTE_VALID        (_AT(pteval_t, 1) << 0)        /* Valid */
#define L_PTE_PRESENT        (_AT(pteval_t, 1) << 0)
#define L_PTE_YOUNG        (_AT(pteval_t, 1) << 1)
#define L_PTE_DIRTY        (_AT(pteval_t, 1) << 6)
#define L_PTE_RDONLY        (_AT(pteval_t, 1) << 7)
#define L_PTE_USER        (_AT(pteval_t, 1) << 8)
#define L_PTE_XN        (_AT(pteval_t, 1) << 9)
#define L_PTE_SHARED        (_AT(pteval_t, 1) << 10)    /* shared(v6), coherent(xsc3) */
#define L_PTE_NONE        (_AT(pteval_t, 1) << 11)

/*
 * These are the memory types, defined to be compatible with
 * pre-ARMv6 CPUs cacheable and bufferable bits:   XXCB
 */
#define L_PTE_MT_UNCACHED    (_AT(pteval_t, 0x00) << 2)    /* 0000 */
#define L_PTE_MT_BUFFERABLE    (_AT(pteval_t, 0x01) << 2)    /* 0001 */
#define L_PTE_MT_WRITETHROUGH    (_AT(pteval_t, 0x02) << 2)    /* 0010 */
#define L_PTE_MT_WRITEBACK    (_AT(pteval_t, 0x03) << 2)    /* 0011 */
#define L_PTE_MT_MINICACHE    (_AT(pteval_t, 0x06) << 2)    /* 0110 (sa1100, xscale) */
#define L_PTE_MT_WRITEALLOC    (_AT(pteval_t, 0x07) << 2)    /* 0111 */
#define L_PTE_MT_DEV_SHARED    (_AT(pteval_t, 0x04) << 2)    /* 0100 */
#define L_PTE_MT_DEV_NONSHARED    (_AT(pteval_t, 0x0c) << 2)    /* 1100 */
#define L_PTE_MT_DEV_WC        (_AT(pteval_t, 0x09) << 2)    /* 1001 */
#define L_PTE_MT_DEV_CACHED    (_AT(pteval_t, 0x0b) << 2)    /* 1011 */
#define L_PTE_MT_VECTORS    (_AT(pteval_t, 0x0f) << 2)    /* 1111 */
#define L_PTE_MT_MASK        (_AT(pteval_t, 0x0f) << 2)
           

ARM PMD描述符bit[8:5]用于描述Domain,但ARM Linux隻定義使用三個:

#define DOMAIN_KERNEL    2---------用于核心空間
#define DOMAIN_TABLE    2
#define DOMAIN_USER    1-----------用于使用者空間
#define DOMAIN_IO    0-------------用于I/O位址域
           

1.3 設定PGD頁面目錄

create_mapping的參數是struct map_desc類型,用于描述一個虛拟位址區域線性映射到實體區域。基于這塊區域建立PGD/PTE。

static void __init create_mapping(struct map_desc *md)
{
    unsigned long addr, length, end;
    phys_addr_t phys;
    const struct mem_type *type;
    pgd_t *pgd;
...
    type = &mem_types[md->type];------------------------------找到對應的struct mem_type
...
    addr = md->virtual & PAGE_MASK;---------------------------對齊到頁
    phys = __pfn_to_phys(md->pfn);----------------------------頁到實體位址轉換
    length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK));
...
    pgd = pgd_offset_k(addr);---------------------------------根據addr找到對應虛拟位址對應的pgd位址
    end = addr + length;
    do {
        unsigned long next = pgd_addr_end(addr, end);

        alloc_init_pud(pgd, addr, next, phys, type);----------初始化下一級頁表

        phys += next - addr;
        addr = next;
    } while (pgd++, addr != end);-----------------------------周遊區間位址,步長是PGDIR_SIZE,即2MB大小的空間。
}
           

這裡面有三個地方需要解釋:

pgd_offset_k

将虛拟位址進行轉換得到PMD的指針。

#define PGDIR_SHIFT 21

/* to find an entry in a page-table-directory */
#define pgd_index(addr)    ((addr) >> PGDIR_SHIFT)

#define pgd_offset(mm, addr)    ((mm)->pgd + pgd_index(addr))


#define pgd_offset_k(addr)    pgd_offset(&init_mm, addr)

struct mm_struct init_mm = {
    .mm_rb        = RB_ROOT,
    .pgd        = swapper_pg_dir,
    .mm_users    = ATOMIC_INIT(2),
    .mm_count    = ATOMIC_INIT(1),
    .mmap_sem    = __RWSEM_INITIALIZER(init_mm.mmap_sem),
    .page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
    .mmlist        = LIST_HEAD_INIT(init_mm.mmlist),
    INIT_MM_CONTEXT(init_mm)
};
           

由虛拟記憶體布局圖中swapper_pg_dir可知,大小為16KB,裡面有詳細的解釋。init_mm.pgd指向swapper_pg_dir。

pgd_addr_end

include\asm-generic\pgtable.h:

#define pgd_addr_end(addr, end)                        \
({    unsigned long __boundary = ((addr) + PGDIR_SIZE) & PGDIR_MASK;    \
    (__boundary - 1 < (end) - 1)? __boundary: (end);        \
})

arch\arm\include\asm\pgtable-2level.h:
/*
 * PMD_SHIFT determines the size of the area a second-level page table can map
 * PGDIR_SHIFT determines what a third-level page table entry can map
 */
#define PMD_SHIFT        21
#define PGDIR_SHIFT        21

#define PMD_SIZE        (1UL << PMD_SHIFT)
#define PMD_MASK        (~(PMD_SIZE-1))
#define PGDIR_SIZE        (1UL << PGDIR_SHIFT)
#define PGDIR_MASK        (~(PGDIR_SIZE-1))
           

由于PGDIR_SHIFT為21,是以一個PGD頁表目錄對應2MB大小的空間,即[addr, addr+PGDIR_SIZE)。是以PGD的數目為2^11,2028個。整個PGD頁表占用空間為2048*4B=8KB。

這和ARM硬體的4096 PGD不一緻。這裡涉及到Linux實作技巧,在建立PTE中進行分析。

是以此處按照2MB步長,周遊[virtual, virtual+length)空間建立PDG頁表和PTE。

alloc_init_pte

由于ARM-Linux采用兩級頁表映射,跳過PUD/PMD,直接到alloc_init_pte建立PTE。

alloc_init_pud-->alloc_init_pmd-->alloc_init_pte

arch\arm\mm\mmu.c:
static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,------------這裡的pmd=pud=pgd。
                  unsigned long end, unsigned long pfn,
                  const struct mem_type *type)
{
    pte_t *pte = early_pte_alloc(pmd, addr, type->prot_l1);------------------使用prot_l1作為參數,建立PGD頁表目錄,傳回addr對應的pte位址。
    do {
        set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);---------調用體系結構相關彙編,配置PTE。
        pfn++;
    } while (pte++, addr += PAGE_SIZE, addr != end);-------------------------周遊[addr, end)區間記憶體,以PAGE_SIZE為步長。
}
           

下面看看如何配置設定PGD頁表目錄:

static pte_t * __init early_pte_alloc(pmd_t *pmd, unsigned long addr, unsigned long prot)
{
    if (pmd_none(*pmd)) {---------------------------------------------------如果PGD的内容為空,即PTE還沒有建立,擇取建立頁面。
        pte_t *pte = early_alloc(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE);-------配置設定512+512個PTE頁表項
        __pmd_populate(pmd, __pa(pte), prot);-------------------------------生成pmd頁表目錄,并刷入RAM
    }
    BUG_ON(pmd_bad(*pmd));
    return pte_offset_kernel(pmd, addr);------------------------------------傳回目前addr對應的PTE位址
}

early_alloc-->early_alloc_aligned:
static void __init *early_alloc_aligned(unsigned long sz, unsigned long align)
{
    void *ptr = __va(memblock_alloc(sz, align));-------------------------------------基于memblock進行配置設定,這裡配置設定4096B,剛好是一頁大小。
    memset(ptr, 0, sz);
    return ptr;
}
           

是以存放PGD需要的空間通過memblock進行申請,PTE_HWTABLE_OFF和PTE_HWTABLE_SIZE都為512,是以一個1024個PTE。

下面是early_pte_alloc配置設定的空間示意圖:前面512個表項是給Linux OS使用的,後512個表項是給ARM硬體MMU用的。

Linux核心記憶體管理-頁表的映射過程

Linux核心PGD/PTE映射關系

static inline void __pmd_populate(pmd_t *pmdp, phys_addr_t pte,
                  pmdval_t prot)
{
    pmdval_t pmdval = (pte + PTE_HWTABLE_OFF) | prot;------------------生成pmdp[0]的内容
    pmdp[0] = __pmd(pmdval);
#ifndef CONFIG_ARM_LPAE
    pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t));---------------------生成緊鄰的pmdp[1]的内容
#endif
    flush_pmd_entry(pmdp);---------------------------------------------将pmdp兩個刷入到RAM中
}
           

Linux的PGD頁表目錄和ARM32不同,總數和ARM32是一樣的。

Linux核心記憶體管理-頁表的映射過程

在arm_mm_memblock_reserve中,通過swapper_pg_dir可以知道其大小為16KB。

就來看看SWAPPER_PG_DIR_SIZE,一共2048個PGD,但是每個PGD包含了兩個相鄰的PGD頁面目錄項。

typedef pmdval_t pgd_t[2];---------------------------------------------8位元組

#define SWAPPER_PG_DIR_SIZE (PTRS_PER_PGD * sizeof(pgd_t))-------------2048*8B=16KB


/*
 * Reserve the special regions of memory
 */
void __init arm_mm_memblock_reserve(void)
{
    /*
     * Reserve the page tables.  These are already in use,
     * and can only be in node 0.
     */
    memblock_reserve(__pa(swapper_pg_dir), SWAPPER_PG_DIR_SIZE);
...
}
           

1.4 設定PTE表項

要了解是如何設定PTE表項,就需要參照B3.3.1 Translation table entry formants中關于Second-level descriptors的描述。

Linux核心記憶體管理-頁表的映射過程
Linux核心記憶體管理-頁表的映射過程
Linux核心記憶體管理-頁表的映射過程
Linux核心記憶體管理-頁表的映射過程
arch\arm\include\asm\pgtable-2level.h:
#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)

arch\arm\include\asm\glue-proc.h:
#ifndef MULTI_CPU
...
#define cpu_set_pte_ext            __glue(CPU_NAME,_set_pte_ext)
...
#endif


arch\arm\mm\proc-v7-2level.S:
/*
 *    cpu_v7_set_pte_ext(ptep, pte)
 *
 *    Set a level 2 translation table entry.
 *
 *    - ptep  - pointer to level 2 translation table entry----------放入r0
 *          (hardware version is stored at +2048 bytes)
 *    - pte   - PTE value to store----------------------------------放入r1
 *    - ext    - value for extended PTE bits------------------------放入r2
 */
ENTRY(cpu_v7_set_pte_ext)
#ifdef CONFIG_MMU
    str    r1, [r0]            @ linux version----------将r1的值存入r0位址的記憶體中

    bic    r3, r1, #0x000003f0--------------------------清除r1的bit[9:4],存入r3
    bic    r3, r3, #PTE_TYPE_MASK-----------------------PTE_TYPE_MASK為0x03,記清除低2位
    orr    r3, r3, r2-----------------------------------r3與r2或,存入r3
    orr    r3, r3, #PTE_EXT_AP0 | 2---------------------這裡将bit1和bit4置位,是以是Small page。

    tst    r1, #1 << 4----------------------------------判斷r1的bit4是否為0
    orrne    r3, r3, #PTE_EXT_TEX(1)--------------------設定TEX為1

    eor    r1, r1, #L_PTE_DIRTY
    tst    r1, #L_PTE_RDONLY | L_PTE_DIRTY
    orrne    r3, r3, #PTE_EXT_APX-----------------------設定AP[2]

    tst    r1, #L_PTE_USER
    orrne    r3, r3, #PTE_EXT_AP1-----------------------設定AP[1]

    tst    r1, #L_PTE_XN
    orrne    r3, r3, #PTE_EXT_XN------------------------設定XN位

    tst    r1, #L_PTE_YOUNG
    tstne    r1, #L_PTE_VALID
    eorne    r1, r1, #L_PTE_NONE
    tstne    r1, #L_PTE_NONE
    moveq    r3, #0

 ARM(    str    r3, [r0, #2048]! )---------------------并沒有寫入r0,而是寫入r0+2048Bytes的偏移。
 THUMB(    add    r0, r0, #2048 )
 THUMB(    str    r3, [r0] )
    ALT_SMP(W(nop))
    ALT_UP (mcr    p15, 0, r0, c7, c10, 1)        @ flush_pte
#endif
    bx    lr
ENDPROC(cpu_v7_set_pte_ext)

           

原文作者:ArnoldLu

原文位址:https://www.cnblogs.com/arnoldlu/p/8087022.html(版權歸原文作者所有,侵權聯系删除)

繼續閱讀