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반복문으로 똑같이 진행한다.

 

#define set_pte(pteptr, pteval)        ((*(pteptr)) = (pteval))

최종적으로 위와 같이 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

+ Recent posts