1. Page Table Create
Page Table을 본격적으로 생성하는 과정은 2가지로 나누어져있다.
앞서 본 Kernel 분석(v5.14.16) - Page Table (1) 참조
이 중에서 page table을 memblock의 physical memory연결해주는 작업은 다음과 같다.
가장 먼저 page table을 할당하고 physical address에 매핑해주어야 한다. 이 때 Linux Page table의 sequence 중 가장먼저 사용되는 pgd table이 처음으로 할당되는 대상이다.
static void __create_pgd_mapping(pgd_t *pgdir, phys_addr_t phys, unsigned long virt, phys_addr_t size, pgprot_t prot, phys_addr_t (*pgtable_alloc)(int), int flags) { unsigned long addr, end, next; pgd_t *pgdp = pgd_offset_pgd(pgdir, virt); /* * If the virtual and physical address don't have the same offset * within a page, we cannot map the region as the caller expects. */ if (WARN_ON((phys ^ virt) & ~PAGE_MASK)) return; phys &= PAGE_MASK; addr = virt & PAGE_MASK; end = PAGE_ALIGN(virt + size); do { next = pgd_addr_end(addr, end); alloc_init_pud(pgdp, addr, next, phys, prot, pgtable_alloc, flags); phys += next - addr; } while (pgdp++, addr = next, addr != end); } |
여기서 virtual address와 physical address는 아예 같은 offset을 가지며 1:1로 매핑된다.
virtual과 physical offset이 같을 수 있도록 WARN_ON()을 통해서 체크하고 pgd table의 주소 값과 end값을 설정하여 do ~ while문을 통해 pgd table entry를 하나씩 할당한다. 이 때 virtual address(addr)에 pgd table의 entry가 지정되어있지 않다면 이건 2st page table로 연결된 것이기 때문에 alloc_init_pud를 통해서 pud table을 할당한다.
결과적으로 위 작업은 virtual과 physical을 1:1로 pgd table entry단위로 매핑을 진행하면서 만약 virtual address에 2st로 연결된 부분이 존재하면 pud table을 할당해서 연결해주는 역할을 진행하는 것이다.
static void alloc_init_pud(pgd_t *pgdp, unsigned long addr, unsigned long end, phys_addr_t phys, pgprot_t prot, phys_addr_t (*pgtable_alloc)(int), int flags) { unsigned long next; pud_t *pudp; p4d_t *p4dp = p4d_offset(pgdp, addr); p4d_t p4d = READ_ONCE(*p4dp); if (p4d_none(p4d)) { p4dval_t p4dval = P4D_TYPE_TABLE | P4D_TABLE_UXN; phys_addr_t pud_phys; if (flags & NO_EXEC_MAPPINGS) p4dval |= P4D_TABLE_PXN; BUG_ON(!pgtable_alloc); pud_phys = pgtable_alloc(PUD_SHIFT); __p4d_populate(p4dp, pud_phys, p4dval); p4d = READ_ONCE(*p4dp); } BUG_ON(p4d_bad(p4d)); pudp = pud_set_fixmap_offset(p4dp, addr); do { pud_t old_pud = READ_ONCE(*pudp); next = pud_addr_end(addr, end); /* * For 4K granule only, attempt to put down a 1GB block */ if (use_1G_block(addr, next, phys) && (flags & NO_BLOCK_MAPPINGS) == 0) { pud_set_huge(pudp, phys, prot); /* * After the PUD entry has been populated once, we * only allow updates to the permission attributes. */ BUG_ON(!pgattr_change_is_safe(pud_val(old_pud), READ_ONCE(pud_val(*pudp)))); } else { alloc_init_cont_pmd(pudp, addr, next, phys, prot, pgtable_alloc, flags); BUG_ON(pud_val(old_pud) != 0 && pud_val(old_pud) != READ_ONCE(pud_val(*pudp))); } phys += next - addr; } while (pudp++, addr = next, addr != end); pud_clear_fixmap(); } |
alloc_init_pud는 pud table을 pgd와 마찬가지로 virtual address를 pud entry단위로 반복문을 통해 반복하면서 alloc_init_cont_pmd를 통한 pmd 할당을 진행한다. 여기서 만약 1G 이상의 block이라면 pud_set_huge를 통해서 pmd로 연결하지않고 직접 pud에 할당한다.
static void alloc_init_cont_pmd(pud_t *pudp, unsigned long addr, unsigned long end, phys_addr_t phys, pgprot_t prot, phys_addr_t (*pgtable_alloc)(int), int flags) { unsigned long next; pud_t pud = READ_ONCE(*pudp); /* * Check for initial section mappings in the pgd/pud. */ BUG_ON(pud_sect(pud)); if (pud_none(pud)) { pudval_t pudval = PUD_TYPE_TABLE | PUD_TABLE_UXN; phys_addr_t pmd_phys; if (flags & NO_EXEC_MAPPINGS) pudval |= PUD_TABLE_PXN; BUG_ON(!pgtable_alloc); pmd_phys = pgtable_alloc(PMD_SHIFT); __pud_populate(pudp, pmd_phys, pudval); pud = READ_ONCE(*pudp); } BUG_ON(pud_bad(pud)); do { pgprot_t __prot = prot; next = pmd_cont_addr_end(addr, end); /* use a contiguous mapping if the range is suitably aligned */ if ((((addr | next | phys) & ~CONT_PMD_MASK) == 0) && (flags & NO_CONT_MAPPINGS) == 0) __prot = __pgprot(pgprot_val(prot) | PTE_CONT); init_pmd(pudp, addr, next, phys, __prot, pgtable_alloc, flags); phys += next - addr; } while (addr = next, addr != end); } |
alloc_init_pmd()는 pud와마찬가지로 이전 page table 즉 여기서는 pud entry가 NULL인경우 pmd entry를 할당하는 과정을 진행하고 있다.
그리고 똑같이 entry단위로 반복문을 통해서 할당을 진행한다
여기서 다른점은 연속적으로 mapping을 한다는 것이다. 기본적으로 NO_CONT_MAPPINGS flag 등이 설정되어있지 않다면 __prot값을 다시 설정해줌으로써 연속적으로 할당 할 수 있도록 구현되어있다.
이후 init_pmd()를 실행하는데 해당 function은 기존 pmd할당 하든지 alloc_init_cont_pte() 호출하든지 둘 중 하나 pmd entry반복문으로 똑같이 진행한다.
최종적으로 위와 같이 pte에서는 entry단위로 반복문을 진행하면서 1:1 mapping진행 (물론 page size만큼씩할당 예를 들어 4KB)
마지막으로 page table을 다음 단계의 page table로 이관하는 동작을 알아보면 다음과 같이 arch/arm64/include/asm/pgtable.h에서 확인 가능하다.
static inline void set_p4d(p4d_t *p4dp, p4d_t p4d) { if (in_swapper_pgdir(p4dp)) { set_swapper_pgd((pgd_t *)p4dp, __pgd(p4d_val(p4d))); return; } WRITE_ONCE(*p4dp, p4d); dsb(ishst); isb(); } |
swapper_pg_dir 인지 여부로 나눠서 동작하며 pgd to pud 등 다음 page table로 연결해주는 동작을 진행한다.
그리고 CPU가 공유하여 사용중인 커널영역의 수정을 안전하게 적용하기 위해 dsb를 통해서 캐시 작업이 완료될 때까지 배리어를 수행하고 isb를 통해서 instruction pipeline을 clear해준다.
2. TTBR Register Set
TTBR1 Register에 값에는 swapper_pg_dir 즉 pgd table의 주소 값이 set되어야 한다.
해당 과정을 진행하기 전에 현재 커널의 처리방식을 살펴보면 page table을 virtual address로 할당하는 것 까지 완료 즉 모든 커널 동작은 이미 가상주소를 사용해서 동작하고 있기 때문에 TTBR1 register에 바로 pgd table의 physical address를 넣을 수는 없고 idmap 테이블을 사용해서 pgd table의 physical address와 TTBR1 register physical address를 각각 구해서 TTBR1 register내 pgd table의 주소 값을 설정해야 한다.
과정의 시작은 arch/arm64/include/asm/mmu_context.h에서 시작된다.
static inline void __nocfi cpu_replace_ttbr1(pgd_t *pgdp) { typedef void (ttbr_replace_func)(phys_addr_t); extern ttbr_replace_func idmap_cpu_replace_ttbr1; ttbr_replace_func *replace_phys; /* phys_to_ttbr() zeros lower 2 bits of ttbr with 52-bit PA */ phys_addr_t ttbr1 = phys_to_ttbr(virt_to_phys(pgdp)); if (system_supports_cnp() && !WARN_ON(pgdp != lm_alias(swapper_pg_dir))) { /* * cpu_replace_ttbr1() is used when there's a boot CPU * up (i.e. cpufeature framework is not up yet) and * latter only when we enable CNP via cpufeature's * enable() callback. * Also we rely on the cpu_hwcap bit being set before * calling the enable() function. */ ttbr1 |= TTBR_CNP_BIT; } replace_phys = (void *)__pa_symbol(function_nocfi(idmap_cpu_replace_ttbr1)); cpu_install_idmap(); replace_phys(ttbr1); cpu_uninstall_idmap(); } |
가장 먼저 ttbr1 변수에 pgd table의 물리조수를 구한뒤 idmap_cpu_replace_ttbr1 funtion의 주소 값을 구한다.
cpu_install_idmap의 경우 idmap의 매핑영역을 활성화(TTBR0 register에 임시로 idmap table physical address를 할당해서 참조할 수있도로 한다)하고 replcae_phys 즉 idmap_cpu_replase_ttbr1 funtion의 physical addresss를 pgd table physical address를 매개변수로하여 호출한다. 이 후 cpu_uninstall_idmap을 통해 idmap 매핑영역을 비활성화(TTBR0에 저장된 idmap table physical address를 클리어한다)한다.
SYM_FUNC_START(idmap_cpu_replace_ttbr1) save_and_disable_daif flags=x2 __idmap_cpu_set_reserved_ttbr1 x1, x3 offset_ttbr1 x0, x3 msr ttbr1_el1, x0 isb restore_daif x2 ret SYM_FUNC_END(idmap_cpu_replace_ttbr1) |
즉 실제적으로 TTBR1 register에 pgd table physical address를 넣은 과정은 위 idmap_cpu_replace_ttbr1 어셈블리어로 동작이다.
ttbr1_el1, x0를 통해서 매개변수로 입력받은 pgd table의 physical address를 레지스터에 저장한다.
저장 후 isb를 통해 파이프라인을 비운다.
위 과정을 진행할 때 사용하는 idmap table physical address는 cpu_install_idmap(), cpu_uninstall_idmap()을 통해서 TTBR0에 idmap table 주소 값을 할당 및 비할당 해줌으로써 TTBR0를 통해 idmap table에 접근 및 작업을 진행할 수 있도록한다.
(TTBR0 레지스터는 원래 유저에서 TTBR1와 마찬가지로 page table 주소를 저장하는 역할을 하지만 부팅 초반에는 위와 같이 idmap table을 임시로 할당하여 사용하는 용도로 사용한다)
추가로 ARM64에서는 커널 메모리 용도로 사용하는 virtual address area가 매우 크기 때문에 보통 physical 과 virtual area를 1:1로 매핑해서 사용한다. 가상주소와 물리주소간의 변환은 virt_to_phys(), phys_to_virt()를 사용한다.
3. Fixmap
fixmap의 경우 컴파일 시점에 가상주소 공간이 이미결정된다. 즉 기본적으로 다른 메모리 영역은 physical addresss만 존재하고 위의 과정들을 부팅초반에 겪음으로써 virtual address할당 및 연결을 진행하지만 fixmap은 컴파일 시점 즉 부팅 처음부터 virtual address가 존재한다.
때문에 부팅 초반에 fixmap을 통해서 virtual 임시 매핑 등 작업을 진행한다.
fixmap은 여러 API를 가지며 위에서 언급한 바와 같이 다른 프로세스에서 임시 지원 등을 할 때 해당 API를 사용한다.
arch/arm64/include/asm/fixmap.h 파일이 존재한다.
해당 파일에 fixmap API들이 enum으로 나열되어있고
void __init early_fixmap_init(void);
extern void __set_fixmap(enum fixed_addresses idx, phys_addr_t phys, pgprot_t prot);
와 같이 head.S를 통한 fixmap할당도 전에 fixmap 사용을위한 early_fixmap_init() 함수와 가상 주소 공간인 fixmap에 실제적으로 physical address를 fixmap entry 중 하나에 write해두기 위한 _set_fixmap() function이 존재한다.
- ref
https://elixir.bootlin.com/linux/v5.14.16/source/...
코드로 알아보는 ARM 리눅스 커널
https://0xax.gitbooks.io/linux-insides/content/MM/linux-mm-2.html
'OS > Linux' 카테고리의 다른 글
[Linux Kernel] Kernel 분석(v5.14.16) - memblock (0) | 2022.01.23 |
---|---|
[Linux Kernel] Kernel 분석(v5.14.16) - I/O mapping (0) | 2021.12.26 |
[Linux Kernel] Kernel 분석(v5.14.16) - Page Table (1) (0) | 2021.12.06 |
[Linux Kernel] objdump, arm-eabi-addr2line (0) | 2021.12.04 |
[Linux Kernel] Kernel 분석(v5.14.16) - Device tree (0) | 2021.11.25 |