페이지 테이블 매핑 – ARM32

리눅스는 페이지 테이블 관리에 4단계 테이블(pgd -> pud -> pmd -> pte)을 사용하는데 32bit ARM은 3단계 테이블(pgd -> pud(x) -> pmd -> pte)을 사용한다.

  • LPAE를 사용하지 않을 경우에는 pmd 테이블이 실제로 없기 때문에 pgd 엔트리 하나를 pmd 엔트리 포인터 2개에 대응하여 사용한다.
  • 실제 LPAE를 사용하지 않는 32bit ARM h/w는 2단계 테이블매핑만 사용한다.

 

create_mapping()

 

create_mapping-3

 

arch/arm/mm/mmu.c

/*
 * Create the page directory entries and any necessary
 * page tables for the mapping specified by `md'.  We
 * are able to cope here with varying sizes and address
 * offsets, and we take full advantage of sections and
 * supersections.
 */
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;

        if (md->virtual != vectors_base() && md->virtual < TASK_SIZE) {
                pr_warn("BUG: not creating mapping for 0x%08llx at 0x%08lx in user region\n",
                        (long long)__pfn_to_phys((u64)md->pfn), md->virtual);
                return;
        }

        if ((md->type == MT_DEVICE || md->type == MT_ROM) &&
            md->virtual >= PAGE_OFFSET &&
            (md->virtual < VMALLOC_START || md->virtual >= VMALLOC_END)) {
                pr_warn("BUG: mapping for 0x%08llx at 0x%08lx out of vmalloc space\n",
                        (long long)__pfn_to_phys((u64)md->pfn), md->virtual);
        }

        type = &mem_types[md->type];

#ifndef CONFIG_ARM_LPAE
        /*
         * Catch 36-bit addresses
         */
        if (md->pfn >= 0x100000) {
                create_36bit_mapping(md, type);
                return;
        }
#endif

        addr = md->virtual & PAGE_MASK;
        phys = __pfn_to_phys(md->pfn);
        length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK));

        if (type->prot_l1 == 0 && ((addr | phys | length) & ~SECTION_MASK)) {
                pr_warn("BUG: map for 0x%08llx at 0x%08lx can not be mapped using pages, ignoring.\n",
                        (long long)__pfn_to_phys(md->pfn), addr);
                return;
        }
  • if (md->virtual != vectors_base() && md->virtual < TASK_SIZE) {
    • 가상 주소가 벡터 주소가 아니면서 user space 영역이면 경고를 출력 후 매핑을 포기하고 그냥 리턴한다.
  • if ((md->type == MT_DEVICE || md->type == MT_ROM) && md->virtual >= PAGE_OFFSET && (md->virtual < VMALLOC_START || md->virtual >= VMALLOC_END)) {
    • 메모리 타입이 디바이스나 ROM 이면서 가상 주소가 kernel space 이고 vmalloc 영역을 벗어난 경우 경고를 출력 후 매핑을 포기하고 그냥 리턴한다.
  • if (md->pfn >= 0x100000) { create_36bit_mapping(md, type);
    • LPAE를 사용하지 않으면서 pfn이 0x100000 이상이면 즉, 4G를 초과하는 경우 36비트 매핑을 사용한다.
    • LPAE 없이 xscale 아키텍처 등에서 large-section(16M) 페이지를 사용하여 36비트 매핑을 사용할 수 있다.
  • addr = md->virtual & PAGE_MASK;
    • 가상 주소를 page 단위로 round down 한다.
  • phys = __pfn_to_phys(md->pfn);
    • pfn 값으로 물리 주소를 구한다.
  • length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK));
    • 가상 주소를 4K 단위로 round down할 때 남는 주소 영역에 요청 길이만큼을 4K round up 하여 길이를 산출한다.
    • 예) rpi2: md->virtual=0x8050_1b00, md->pfn=0x501, md->length=0x1500
      • addr=0x8050_1000, phys=0x0050_1000,  length=0x2000
    • 예) 위의 조건에서 md->length=0x1504인 경우
      • addr=0x8050_1000, phys=0x0050_1000,  length=0x3000
  • if (type->prot_l1 == 0 && ((addr | phys | length) & ~SECTION_MASK)) {
    • 요청 메모리 타입이 1차 테이블을 사용하지 않는 경우 즉 섹션 타입을 사용해야 하는 하는 경우 addr, phys, length가 섹션 align이 안된 경우 경고를 출력 후 매핑을 포기하고 그냥 리턴한다.

 

        pgd = pgd_offset_k(addr);
        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);
}
  • pgd = pgd_offset_k(addr);
    • 가상 주소 값으로 pgd 엔트리 가상 주소를 알아온다.
  • end = addr + length;
    • 매핑할 가상 주소의 끝 주소
  • unsigned long next = pgd_addr_end(addr, end);
    • addr 값에 2M를 더한 값을 구해오는데 초과하는 경우 end 주소를 리턴한다.
  • alloc_init_pud(pgd, addr, next, phys, type);
    • pgd 엔트리 주소에 addr 주소부터 next 주소까지를 물리주소(phys)와 타입 정보로 매핑한다.
  • phys += next – addr;
    • 매핑한 사이즈만큼 추가
  • addr = next;
    • 다음 매핑을 위해 addr 준비
  • } while (pgd++, addr != end);
    • pgd 인덱스를 증가(8 bytes)시키고 addr 주소가 끝날 때까지 루프를 돈다.

 

alloc_init_pud()

arch/arm/mm/mmu.c

static void __init alloc_init_pud(pgd_t *pgd, unsigned long addr,
                                  unsigned long end, phys_addr_t phys,
                                  const struct mem_type *type)
{
        pud_t *pud = pud_offset(pgd, addr);
        unsigned long next;

        do {
                next = pud_addr_end(addr, end);
                alloc_init_pmd(pud, addr, next, phys, type);
                phys += next - addr;
        } while (pud++, addr = next, addr != end);
}

addr 주소부터 end 주소까지를 pud 엔트리가 커버하는 크기만큼 증가시키며 pud 엔트리를 매핑한다. 단 LPAE를 사용하지 않는 경우 pud 테이블이 없으므로 요청된 인수 그대로 alloc_init_pmd() 함수를 1회 호출하고 리턴한다.

  • pud_t *pud = pud_offset(pgd, addr);
    • pud 테이블이 없기 때문에 pgd를 리턴한다.
  • next = pud_addr_end(addr, end);
    • pud 테이블이 없기 때문에 end를 리턴한다.
  • alloc_init_pmd(pud, addr, next, phys, type);
    • pud 엔트리 주소에 addr 주소부터 next 주소까지를 물리주소(phys)와 타입 정보로 매핑한다.
  • phys += next – addr;
    • 매핑한 사이즈만큼  추가
  • } while (pud++, addr = next, addr != end);
    • pud 인덱스를 증가(4 bytes)시키고 addr 주소가 끝날 때까지 루프를 돌게 되어 있지만 ARM 32bit에서는 pud 테이블이 없기 때문에 조건이 자동으로 만료되어 루프를 탈출하게 된다.

 

alloc_init_pmd()

arch/arm/mm/mmu.c

static void __init alloc_init_pmd(pud_t *pud, unsigned long addr,
                                      unsigned long end, phys_addr_t phys,
                                      const struct mem_type *type)
{
        pmd_t *pmd = pmd_offset(pud, addr);
        unsigned long next;

        do {
                /*
                 * With LPAE, we must loop over to map
                 * all the pmds for the given range.
                 */
                next = pmd_addr_end(addr, end);
                
                /*
                 * Try a section mapping - addr, next and phys must all be
                 * aligned to a section boundary.
                 */
                if (type->prot_sect && 
                                ((addr | next | phys) & ~SECTION_MASK) == 0) {
                        __map_init_section(pmd, addr, next, phys, type);
                } else {
                        alloc_init_pte(pmd, addr, next,
                                                __phys_to_pfn(phys), type);
                }

                phys += next - addr;

        } while (pmd++, addr = next, addr != end);
}

addr 주소부터 end 주소까지를 2M씩 증가시키며 pmd 엔트리를 매핑한다. 단 LPAE를 사용하지 않는 경우 pmd 테이블이 없으므로 요청된 인수 그대로 __map_init_section() 또는 alloc_init_pte() 함수를 1회 호출하고 리턴한다.

  • pmd_t *pmd = pmd_offset(pud, addr);
    • LPAE를 사용하지 않는 경우 pud(실제 pgd) 인수가 그대로 리턴된다.
    • LPAE를 사용하는 경우 pud(실제 pgd) 테이블에서 가상주소로 pmd 인덱스를 계산하여 pmd 엔트리 주소를 알아온다.
  • next = pmd_addr_end(addr, end);
    • LPAE를 사용하지 않는 경우 end를 그대로 리턴한다.
    • LPAE를 사용하는 경우 다음 2M 단위로 align 된 주소를 리턴하는데 end를 넘어가는 경우 end를 리턴한다.
  • if (type->prot_sect && ((addr | next | phys) & ~SECTION_MASK) == 0) {
    • 요청 메모리 타입이 섹션용 매핑이 가능하고 요청 인수들이 섹션 크기로 align된 경우
  • __map_init_section(pmd, addr, next, phys, type);
    • pmd 엔트리를 섹션으로 매핑한다.
  • alloc_init_pte(pmd, addr, next, __phys_to_pfn(phys), type);
    • 그 외에는 2차 PTE 테이블을 만든다.

 

__map_init_section()

arch/arm/mm/mmu.c

static void __init __map_init_section(pmd_t *pmd, unsigned long addr,
                        unsigned long end, phys_addr_t phys,
                        const struct mem_type *type)
{
        pmd_t *p = pmd;

#ifndef CONFIG_ARM_LPAE
        /*
         * In classic MMU format, puds and pmds are folded in to
         * the pgds. pmd_offset gives the PGD entry. PGDs refer to a
         * group of L1 entries making up one logical pointer to
         * an L2 table (2MB), where as PMDs refer to the individual
         * L1 entries (1MB). Hence increment to get the correct
         * offset for odd 1MB sections.
         * (See arch/arm/include/asm/pgtable-2level.h)
         */
        if (addr & SECTION_SIZE)
                pmd++;
#endif
        do {
                *pmd = __pmd(phys | type->prot_sect);
                phys += SECTION_SIZE;
        } while (pmd++, addr += SECTION_SIZE, addr != end);

        flush_pmd_entry(p);
}

섹션을 매핑하기 위한 함수이다.

  • if (addr & SECTION_SIZE) pmd++
    • LPAE를 사용하지 않는 경우 pmd 엔트리 하나는 두 개로 이루어져 있는데 각각 1M를 담당한다. 만일 요청 가상 주소를 1M 단위의 인덱스만을 남겼을 때 홀 수인 경우 pmd 엔트리를 증가시켜 pmd+1 번째를 지정해야 한다.
      • 예) 0x0030_0000(홀 수), 0x0040_0000(짝 수)
  • *pmd = __pmd(phys | type->prot_sect);
    • pmd 엔트리에 물리 주소와 요청 메모리 타입의 섹션 속성을 설정한다.
  • phys += SECTION_SIZE;
    • 1M 증가
  • } while (pmd++, addr += SECTION_SIZE, addr != end);
    • pmd 인덱스를 증가(4 bytes)시키고 addr 주소가 끝날 때까지 루프를 돈다.
  • flush_pmd_entry(p);
    • 1차, 2차 TLB 테이블에서 pmd 엔트리를 삭제한다.

 

alloc_init_pte()

arch/arm/mm/mmu.c

static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,
                                  unsigned long end, unsigned long pfn,
                                  const struct mem_type *type)
{
        pte_t *pte = early_pte_alloc(pmd, addr, type->prot_l1);
        do {
                set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);
                pfn++;
        } while (pte++, addr += PAGE_SIZE, addr != end);
}
  • pte_t *pte = early_pte_alloc(pmd, addr, type->prot_l1);
    • pte 테이블을 할당한다.
  • set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);
    • pte 엔트리를 설정한다.
  • } while (pte++, addr += PAGE_SIZE, addr != end);
    • pte 엔트리를 증가 , 주소를 4K 증가하고 end 주소까지 루프를 돈다.

 

early_pte_alloc()

arch/arm/mm/mmu.c

static pte_t * __init early_pte_alloc(pmd_t *pmd, unsigned long addr, unsigned long prot)
{
        if (pmd_none(*pmd)) {
                pte_t *pte = early_alloc(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE);
                __pmd_populate(pmd, __pa(pte), prot);
        }
        BUG_ON(pmd_bad(*pmd));
        return pte_offset_kernel(pmd, addr);
}
  • if (pmd_none(*pmd)) {
    • pmd 엔트리에 값이 있어 pte 테이블이 필요한 경우
  • pte_t *pte = early_alloc(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE);
    • memblock에서 free 4K 공간을 할당 받고 해당 페이지의 가상 주소를 알아온다.
  • __pmd_populate(pmd, __pa(pte), prot);
    • 해당 pte 테이블을 populate로 기록 한다.
  • pte_offset_kernel(pmd, addr);
    • pte 테이블에서 가상주소로 pte 엔트리 주소를 구한다.

 

__pmd_populate()

arch/arm/include/asm/pgalloc.h

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] = __pmd(pmdval); 
#ifndef CONFIG_ARM_LPAE
        pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t));
#endif
        flush_pmd_entry(pmdp);
}
  • pmdval_t pmdval = (pte + PTE_HWTABLE_OFF) | prot;
    • pte 테이블 주소 + 2K한 후 속성값을 추가한 값
  • pmdp[0] = __pmd(pmdval);
    • pmdp 엔트리에 pmdval 엔트리 값을 기록한다.
    • ARM h/w pt #0 테이블을 가리키는 주소
  • pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t));
    • 다음 pmdp 엔트리에 pmdval + 1K한 값
      • pte 테이블 주소 + 3K한 후 속성값을 추가한 값과 동일
    • ARM h/w pt #1 테이블을 가리키는 주소

 

페이지 테이블 매핑

아래 그림과 같이 16K의 영역을 매핑하는 경우 LPAE를 사용하지 않는 32bit ARM 아키텍처에서의 호출되는 함수의 흐름을 나타내 보았다.

create_mapping-1

 

아래는 위의 함수 호출을 통하여 완성된 페이지 테이블 형태를 나타내었다.

create_mapping-2

 

참고

 

 

3 thoughts to “페이지 테이블 매핑 – ARM32”

  1. 안녕하세요. 이파란입니다.

    오늘도 열심히 복습하려고 찾아왔습니다.

    첫번째 줄에

    리눅스는 페이지 테이블 관리에 4단계 테이블(pgd -> pud -> pmd -> pte)을 사용하는데 32bit ARM은 3단계 테이블(pgd -> pud(x) -> pmd -> pte)을 사용한다.

    // 4단계 테이블(PGD -> P4D -> PUD -> PMD -> PTE) ARM64의 경우 최대 4 단계

    P4D 가 맞나요?

    1. 안녕하세요? 이파란님.

      이 자료를 작성하던 당시에 리눅스는 페이지 테이블 관리에 4단계 테이블을 사용하였지만,
      지금은 5단계를 사용하고 있습니다.

      ARM64의 경우 P4D를 제외하고 2~4단계를 사용합니다. (최대 4단계: PGD->PUD->PMD->PTE)
      ARM64 페이지 테이블은 다음 자료를 참고하세요.
      http://jake.dothome.co.kr/pt64/

      감사합니다.

댓글 남기기