리눅스는 페이지 테이블 관리에 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()
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(짝 수)
- LPAE를 사용하지 않는 경우 pmd 엔트리 하나는 두 개로 이루어져 있는데 각각 1M를 담당한다. 만일 요청 가상 주소를 1M 단위의 인덱스만을 남겼을 때 홀 수인 경우 pmd 엔트리를 증가시켜 pmd+1 번째를 지정해야 한다.
- *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 테이블을 가리키는 주소
- 다음 pmdp 엔트리에 pmdval + 1K한 값
페이지 테이블 매핑
아래 그림과 같이 16K의 영역을 매핑하는 경우 LPAE를 사용하지 않는 32bit ARM 아키텍처에서의 호출되는 함수의 흐름을 나타내 보았다.
아래는 위의 함수 호출을 통하여 완성된 페이지 테이블 형태를 나타내었다.
참고
- Memblock – (1) | 문c
- Memblock – (2) | 문c
- 페이지 테이블 관련 명령(pgd, pud, pmd, pte) | 문c
안녕하세요. 이파란입니다.
오늘도 열심히 복습하려고 찾아왔습니다.
첫번째 줄에
리눅스는 페이지 테이블 관리에 4단계 테이블(pgd -> pud -> pmd -> pte)을 사용하는데 32bit ARM은 3단계 테이블(pgd -> pud(x) -> pmd -> pte)을 사용한다.
// 4단계 테이블(PGD -> P4D -> PUD -> PMD -> PTE) ARM64의 경우 최대 4 단계
P4D 가 맞나요?
안녕하세요? 이파란님.
이 자료를 작성하던 당시에 리눅스는 페이지 테이블 관리에 4단계 테이블을 사용하였지만,
지금은 5단계를 사용하고 있습니다.
ARM64의 경우 P4D를 제외하고 2~4단계를 사용합니다. (최대 4단계: PGD->PUD->PMD->PTE)
ARM64 페이지 테이블은 다음 자료를 참고하세요.
http://jake.dothome.co.kr/pt64/
감사합니다.
감사합니다!
앗 제가 자꾸 햇갈리네요. ^^;
PTE 까지 포함한 단계네요.
최근의 P4D 를 포함하면 5단계가 되네요.