페이지 테이블 API – ARM32

페이지 테이블 관련 명령(pgd, pud, pmd, pte)은 32bit ARM 리눅스용에 대해 제한하여 설명한다.

  • ARM 리눅스에서는 3단계 페이지 테이블 관리를 사용
    • pgd -> pmd -> pte
  • ARM h/w에서는 2단계 페이지 테이블 관리를 사용한다.
    • pgd(pmd) -> pte

 

엔트리 타입

  • pgd_t
    • typedef struct { pmdval_t pgd[2]; } pgd_t;
    • pmdval_t는 u32형이다.
  • pmd_t
    • typedef struct { pmdval_t pmd; } pmd_t;
  • pte_t
    • typedef struct { pteval_t pte; } pte_t;
    • pteval_t는 u32형이다.

 

pgd 관련

pgd_val()

arch/arm/include/asm/pgtable-2level-types.h

#define pgd_val(x)      ((x).pgd[0])
  • 두 개의 pgd 엔트리 중 첫 번째 pgd 엔트리 값을 리턴한다.

 

pgd_index()

arch/arm/include/asm/pgtable.h

/* to find an entry in a page-table-directory */
#define pgd_index(addr)         ((addr) >> PGDIR_SHIFT)
  • 주어진 주소의 pgd 인덱스 값(0~2047)을 리턴한다.
  • PGDIR_SHIFT=21 (=2M round down)

addr[31:21] 값을 우측으로 PGDIR_SHIFT(21) 비트 만큼 쉬프트하여 인덱스로 활용 (0~2047(0x7ff))

  • rpi2 예) pgd_index(0x8a00_0000) = 0x450

pgd_index-1

 

pgd_offset()

arch/arm/include/asm/pgtable.h

#define pgd_offset(mm, addr)    ((mm)->pgd + pgd_index(addr))
  • mm_struct 구조체가 가리키는 pgd 주소 + pgd 인덱스를 하여 pgd의 offset 주소를 알아올 수 있다.

pgd_offset-1

 

pgd_offset_k()

arch/arm/include/asm/pgtable.h

/* to find an entry in a kernel page-table-directory */
#define pgd_offset_k(addr)      pgd_offset(&init_mm, addr)
  • “_k” suffix(접미) 는 커널이라는 뜻이다. 즉 커널이 사용하는 level-1 PGD 에서 pgd 엔트리 가상 주소를 알아온다.

 

pgd_offset_k-1

 

pgd_addr_end()

include/asm-generic/pgtable.h

/*
 * When walking page tables, get the address of the next boundary,
 * or the end address of the range if that comes earlier.  Although no
 * vma end wraps to 0, rounded up __boundary may wrap to 0 throughout.
 */

#define pgd_addr_end(addr, end)                                         \
({      unsigned long __boundary = ((addr) + PGDIR_SIZE) & PGDIR_MASK;  \
        (__boundary - 1 < (end) - 1)? __boundary: (end);                \
})
  • addr 값에 2M를 더한 값을 round down 하고 end 주소 보다 작은 경우 리턴한다. 그렇지 않은 경우 end를 리턴한다.
  • 예) addr=0x1000_0000, end=0x1040_0000
    • 호출1=0x1020_0000, 호출2=0x1040_0000
  • 예) addr=0x1000_0004, end=0x1040_0004
    • 호출1=0x1020_0000, 호출2=0x1040_0000, 호출3=0x1040_0004

 

pud 관련

pud_val()

include/asm-generic/pgtable-nopud.h

#define pud_val(x)                              (pgd_val((x).pgd))
  • pud 엔트리 값을 리턴한다.
  • 실제 pud 테이블이 없으므로 pgd 값을 사용한다.

 

pud_offset()

include/asm-generic/pgtable-nopud.h

static inline pud_t * pud_offset(pgd_t * pgd, unsigned long address)
{
        return (pud_t *)pgd;
}
  • pud의 offset은 pud 테이블이 없으므로 그냥 첫 번째 인수인 pgd만 리턴한다.

 

pmd 관련

pmd_val()

arch/arm/include/asm/pgtable-2level-types.h

#define pmd_val(x)      ((x).pmd)
  • pmd 엔트리 값을 리턴한다.

 

pmd_index()

  • 사용하지 않음.

 

pmd_offset()

arch/arm/include/asm/pgtable-2level.h

static inline pmd_t *pmd_offset(pud_t *pud, unsigned long addr)
{
        return (pmd_t *)pud;
}
  • pmd의 offset은 pmd 테이블이 없으므로 그냥 첫 번째 인수인 pud만 리턴한다.

 

pmd_off_k()

arch/arm/mm/mm.h

static inline pmd_t *pmd_off_k(unsigned long virt)
{
        return pmd_offset(pud_offset(pgd_offset_k(virt), virt), virt);
}

가상 주소에 대응하는 pmd 엔트리 가상 주소를 알아온다.

  •  pgd_offset_k()
    • 커널이 사용하는 level-1 PGD 에서 가상 주소에 대응하는 pgd 엔트리 가상 주소를 알아온다
  • pud_offset()
    • 첫 번째 인수인 pgd 엔트리 가상 주소를 리턴한다.
  • pmd_offset()
    • 첫 번째 인수가 가리키는 pmd 테이블에서 가상 주소에 대응하는 pmd 엔트리 가상 주소를 알아온다.

pmd_off_k-1

 

pmd_page_vaddr()

arch/arm/include/asm/pgtable.h

arch/arm/include/asm/pgtable.h

static inline pte_t *pmd_page_vaddr(pmd_t pmd)
{
        return __va(pmd_val(pmd) & PHYS_MASK & (s32)PAGE_MASK);
}
  • pmd 테이블의 가상 주소값을 알아온다.
  • pmd 값을 4K round down 한 후 가상 주소로 변환한다.

pmd[31:12] 값을 가상주소로 변환하여 pte 테이블을 가리키는 가상 주소를 알아낸다.

  • 예) rpi2: pmd_page_vaddr(0x0234_5678) = 0x8234_5000

pmd_page_vaddr-1

 

pmd_clear()

arch/arm/include/asm/pgtable-2level.h

#define pmd_clear(pmdp)                 \
        do {                            \
                pmdp[0] = __pmd(0);     \
                pmdp[1] = __pmd(0);     \
                clean_pmd_entry(pmdp);  \
        } while (0)
  • pmd 엔트리 2개를 삭제한다.
    • pgd 엔트리 하나가 pmd 엔트리 2개 이므로 결국 pgd 엔트리를 지우는 것과 동일하다.
  • clean_pmd_entry(pmdp);
    • TLB를 삭제한다.

 

clean_pmd_entry()

arch/arm/include/asm/tlbflush.h

static inline void clean_pmd_entry(void *pmd)
{
        const unsigned int __tlb_flag = __cpu_tlb_flags;
        
        tlb_op(TLB_DCLEAN, "c7, c10, 1  @ flush_pmd", pmd);
        tlb_l2_op(TLB_L2CLEAN_FR, "c15, c9, 1  @ L2 flush_pmd", pmd);
}
  • L1-TLB 엔트리와 L2-TLB 엔트리를 flush(여기서는 invalidate) 한다.

 

pte 관련

pte_val()

arch/arm/include/asm/pgtable-2level-types.h

#define pte_val(x)      ((x).pte)

  • pte 엔트리 값을 리턴한다.

 

pte_index()

arch/arm/include/asm/pgtable.h

#define pte_index(addr)         (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
  • 주어진 주소의 pte 인덱스 값(0~511)을 리턴한다.
  • PTRS_PER_PTE=512
  • PAGE_SHIFT=12 (=4K round down)

addr[12:20] 값을 우측으로 12bit 쉬프트하여 인덱스로 활용 (0~511(0x1ff))

  • rpi2 예) pte_index(0x8a12_0000) = 0x120

pte_index-1

 

pte_offset_kernel()

arch/arm/include/asm/pgtable.h

#define pte_offset_kernel(pmd,addr)     (pmd_page_vaddr(*(pmd)) + pte_index(addr))

level-1 테이블에서 pmd 포인터가 가리키는 pmd_t 값에 연동된 level-2 테이블에서 addr 값으로 pte index를 알아내어 해당 pte_t의 주소를 알아낸다.

  • 예) rpi2: *pmd=0x0a00_0000
    • pte_offset_kernel(0x8000_6008, 0x8234_5000) = 0x8a00_0514

pte_offset_kernel-1

 

__pte_map() & __pte_unmap()

CONFIG_HIGHPTE

  • pte 테이블을 highmem에서 운용하고자 할 때 사용하는 커널 옵션이다.
  • highmem에 있는 페이지 테이블을 access  할 때 kmap_atomic()을 사용하여 highmem에 위치한 페이지 테이블을 fixmap에 매핑 후 그 페이지 access 할 수 있다. 사용한 후에는 kunmap_atomic()을 사용하여 페이지 테이블을 fixmap에서 언매핑하여야 한다.

pte 페이지 테이블을 lowmem에서 운영하는 경우에는 별도의 매핑을 할 필요가 없으므로 곧바로 해당 영역의 주소를 알아와서 access할 수 있다.

arch/arm/include/asm/pgtable.h

#ifndef CONFIG_HIGHPTE
#define __pte_map(pmd)          pmd_page_vaddr(*(pmd))
#define __pte_unmap(pte)        do { } while (0)
#else
#define __pte_map(pmd)          (pte_t *)kmap_atomic(pmd_page(*(pmd)))
#define __pte_unmap(pte)        kunmap_atomic(pte)
#endif

level-1 테이블에서 pmd 포인터가 가리키는 pmd_t 값에 연동된 pte 테이블의 가상 주소 값을 알아낸다.

  • 예) rpi2: *pmd = 0x0a10_0000
    • __pte_map(0x8000_6004) = 0x8a10_0000

__pte_map-1

 

pte_offset_map()

arch/arm/include/asm/pgtable.h

#define pte_offset_map(pmd,addr)        (__pte_map(pmd) + pte_index(addr))

level-1 테이블에서 pmd 포인터가 가리키는 pmd_t 값에 연동된 level-2 테이블에서 addr 값으로 pte index를 알아내어 해당 pte_t의 가상 주소를 알아낸다.

  • 예) rpi2: *pmd=0x0a00_0000
    • pte_offset_kernel(0x8000_6008, 0x8234_5000) = 0x8a00_0514

pte_offset_map-1

 

set_pte_ext()

arch/arm/include/asm/pgtable-2level.h

#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)

 

pte_clear()

arch/arm/include/asm/pgtable.h

#define pte_clear(mm,addr,ptep) set_pte_ext(ptep, __pte(0), 0)

 

cpu_set_pte_ext()

arch/arm/include/asm/proc-fns.h

#define cpu_set_pte_ext                 processor.set_pte_ext
cpu_v7_set_pte_ext()

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
 *                (hardware version is stored at +2048 bytes)
 *      - pte   - PTE value to store
 *      - ext   - value for extended PTE bits
 */
ENTRY(cpu_v7_set_pte_ext)
#ifdef CONFIG_MMU
        str     r1, [r0]                        @ linux version

        bic     r3, r1, #0x000003f0
        bic     r3, r3, #PTE_TYPE_MASK
        orr     r3, r3, r2
        orr     r3, r3, #PTE_EXT_AP0 | 2

        tst     r1, #1 << 4
        orrne   r3, r3, #PTE_EXT_TEX(1)

        eor     r1, r1, #L_PTE_DIRTY
        tst     r1, #L_PTE_RDONLY | L_PTE_DIRTY
        orrne   r3, r3, #PTE_EXT_APX

        tst     r1, #L_PTE_USER
        orrne   r3, r3, #PTE_EXT_AP1

        tst     r1, #L_PTE_XN
        orrne   r3, r3, #PTE_EXT_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]! )
 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)

ptep 주소에 위치한 pte 엔트리를 설정하는 경우 ptep로부터 2K byte 윗 쪽에 위치한 hw pte 엔트리 역시 linux pte 속성 값을 h/w pte 속성 값으로 변환하여 설정한다.

set_pte_ext-1

r1(linux pte 엔트리 값)

set_pte_ext-2

  • MT(Memory Type)
    • 0: MT_UNCACHED
    • 1: MT_BUFFERABLE
    • 2: MT_WRITETHROUGH
    • 3: MT_WRITEBACK
    • 4: MT_DEVICE_SHARED
    • 6: MT_MINICACHE
    • 9: MT_DEV_WC
    • B: MT_DEV_CACHED
    • C: MT_DEV_NONSHARED
    • F: MT_VECTORS

r3(hw pte 엔트리 값), r2(ext, hw용 확장 pte 플래그)

  • r3 값의 h/w pte AP, TEX, Table, XN 비트는 linux pte 값을 사용하여 재 설정하기 위해 일단 clear 한다.
    • r3 → 비트클리어(0x3f3)

set_pte_ext-3

  • r3에 인수로 받은 h/w용 pte 플래그를 합치고 privileged access 권한에서 read 또는 read/write 권한을 주기위해 EXT_AP0을 설정하고 2차 테이블을 지원하기 위해 TABLE 비트를 설정한다.
    • r3 | ext(hw용 확장 pte 플래그) | PTE_EXT_AP0(0x10) | 2

set_pte_ext-4

  • pte의 bit4가 1인 경우 TRE를 설정한다.
    • r3 | PTE_EXT_TEX(0x40)

set_pte_ext-5

  • pte의 L_PTE_DIRTY가 없거나 L_PTE_RDONLY가 있으면 EXT_APX를 설정하여 read only로 만든다.
    • r3 | PTX_EXT_APX(0x200)

set_pte_ext-6

  • pte의 L_PTE_USER가 있으면 unprivileged access 권한에서도 read 또는 read/write 권한을 주기 위해 EXT_AP1을 설정한다.
    • r3 | PTE_EXT_AP1(0x20)

set_pte_ext-7

  • pte의 L_PTE_XN가 있으면 페이지에서 실행코드가 동작하지 않도록 XN을 설정한다.
    • r3 | PTEX_EXT_XN(0x1)

set_pte_ext-8

  • pte의 L_PTE_YOUNG이 없거나 L_PTE_VALID(L_PTE_PRESENT)가 없거나 L_PTE_NONE이 있으면 r3을 모두 0으로 변경
    • 리눅스 PTE 엔트리에만 있는 L_PTE_YOUNG이 있으면서 L_PTE_PRESENT가 없는 경우 pte 엔트리가 아니라 swap 엔트리를 의미한다. (H/W PTE 값은 0을 대입)
    • 리눅스 PTE 엔트리에만 있는 L_PTE_NONE은 PAGE_NONE 매핑으로 automatic NUMA balancing을 위해 일부러 유저 페이지에 대한 fault를 발생시켜 사용하기 위해 이용한다. (H/W PTE 값은 0을 대입)

 

  • 리눅스용 pte 엔트리 속성 -> h/w pte 엔트리 속성 변환 예)
    • linux pte 엔트리=0x1234_565f(0b0001_0010_0011_0100_0101_0110_0101_1111)
      • pfn=0x12345, prot=MT_MEMORY_RW = SHARE | XN | DIRTY | WA(0b0111) | YOUNG | PRESENT
    • h/w pte 엔트리=0x1234_545f(0b0001_0010_0011_0100_0101_0100_0101_1111)
      • pfn=0x12345, prot=SHARE | AP(0b001=RW,NA) | TEX(0b001) | C | B | TABLE | XN

 

실제 연결된 페이지 테이블 예)

pgd-1

 

xxx_page() 관련

pgd_page()

include/asm-generic/pgtable-nopud.h

#define pgd_page(pgd)                           (pud_page((pud_t){ pgd }))
  • pud page()를 호출 -> pmd_page() 호출하여 pmd 테이블에 대응하는 page를 리턴한다.

 

pud_page()

  • 사용하지 않음.

 

pmd_page()

arch/arm/include/asm/pgtable.h

#define pmd_page(pmd)           pfn_to_page(__phys_to_pfn(pmd_val(pmd) & PHYS_MASK))
  • pmd 테이블에 대응하는 page를 리턴한다

 

pte_page()

arch/arm/include/asm/pgtable.h

#define pte_page(pte)           pfn_to_page(pte_pfn(pte))
  • pte 값에 대응하는 pfn 값을 알아온 후 해당 page를 리턴한다.

 

pte_pfn()

arch/arm/include/asm/pgtable.h

#define pte_pfn(pte)            ((pte_val(pte) & PHYS_MASK) >> PAGE_SHIFT)
  • pte 엔트리 값을 12 bit 우측 쉬프트하여 pfn(물리 주소 frame 번호) 값으로 변환한다.

 

pfn_to_page()

include/asm-generic/memory_model.h

#define pfn_to_page __pfn_to_page
#define __pfn_to_page(pfn)      (mem_map + ((pfn) - ARCH_PFN_OFFSET))
  • pfn 번호에 대응하는 page 구조체 주소를 알아온다.
  • __pfn_to_page
    • 메모리 모델에 따라 구현이 다르다.
    • CONFIG_FLATMEM
      • ARCH_PFN_OFFSET = 물리주소 PFN
      • rpi2: mem_map + pfn
    • mem_map은 PFN0부터 시작되는 물리메모리 페이지의 정보인 page[] 구조체 배열을 가리킨다.

 

참고

Mem_map

<kernel v5.0>

Mem_map

특징

  • mem_map은 모든 물리 메모리 페이지 프레임에 대한 정보를 담아둔 page 구조체 배열이다.
    • 처음 리눅스에서는 unsigned short 타입의 참조 카운터 배열로 시작하다가 점점 필요 멤버가 늘어나 오늘날의 page 구조체가 되었다.
  • NUMA 시스템과 UMA 시스템에서의 접근 방법이 다르고 메모리 모델별로도 접근 방법이 다르다.
  • mem_map의 초기화 함수 경로는 아키텍처 및 커널 설정마다 다르다.
    • arm with flatmem
    • arm with sparsemem
      • setup_arch() → paging_init() → bootmem_init()  → sparse_init()
    • arm64 with sparsemem
      • setup_arch() → bootmem_init() → sparse_init()

mem_map-1b

 

Flat Memory with mem_map

  • NEED_MULTIPLE_NODES 커널 옵션이 사용되지 않을 때 *mem_map 포인터 변수는 하나의 page[] 구조체 배열을 가리킨다.
  • FLAT_NODE_MEM_MAP 커널 옵션을 사용하는 경우 contig_page_data.node_mem_map를 통해서 page[] 구조체 배열을 가리킨다.

mm-8b

 

Discontiguous Memory with mem_map

  • node_data[].node_mem_map를 통해서 page[] 구조체 배열을 가리킨다.
  • x86_32에서는 섹션 to 노드 매핑을 하는 테이블을 별도로 구현하여 사용한다.
  • 실제 사용하는 메모리에 대해서만 관리하고 hole 영역에 대해서는 전혀 관리하지 않는다.

mm-6

 

Sparse Memory with mem_map

  • 하나의 mem_section 구조체는 PAGES_PER_SECTION 갯 수 만큼의 page[] 배열을 가리키고 관리한다.
  • hole을 포함한 메모리 전체를 mem_section을 통해 관리한다.
  • hole 영역에 대해서도 mem_section 테이블에 엔트리가 존재하고 멤버 변수 node_mem_map 값을 null로 하는 것으로 미사용 섹션을 의미한다. 그 외 사용되는 메모리의 관리는 mem_section[]과 page[] 구조체를 통해 관리된다..
  • 섹션의 크기는 페이지 테이블에서 사용하는 섹션 크기가 아니라 Sparsemem에서 사용하는 크기로 수십MB ~ 수GB 크기로 online/offline(pluglable memory)을 관리하기 위한 최소 크기 단위이므로 혼동되지 않도록 한다.
  • 메모리 할당 관리를 위해 mem_section 구조체의 구현이 두 개로 나뉘어 있다.
    •  SPARSEMEM_STATIC
      • mem_section[][] 이중 배열로 만들어 컴파일 타임에 각각의 page[] 배열을 가리키고 관리한다.
      • mem_section[][] 이중 배열은 hole을 포함한 모든 메모리를 관리하므로 hole의 size가 적절한 경우 별도의 dynamic 할당 없이 사용하므로 간편하나 hole size가 매우 큰 경우에는 mem_section 구조체 x hole 섹션 수 만큼의 메모리 낭비를 하므로 적합하지 않다.
    •  SPARSEMEM_EXTREME
      • *mem_section[] 포인터 배열만 컴파일 타임에 만들어지고 실제 mem_section[] 배열은 별도로 필요한 만큼 메모리를 할당 받아 page[] 배열을 가리키고 관리한다.
      • * mem_section[] 포인터 배열을 사용하여 hole size가 매우 큰 경우를 대비하여 메모리 낭비를 줄일 수 있도록 설계되었다.
    • NODE_NOT_IN_PAGE_FLAGS
      • page 구조체의 flags 멤버변수에 node 번호를 기록하지 않는 경우 node 번호를 별도로 섹션에 대해 1:1로 매핑하여 저장한다.
      • 32비트 시스템에서 비트 자리가 부족하여 노드 번호를 기록하지 못하는 경우 사용

 

다음 그림은 SPARSEMEM_EXTREME을 사용하지 않는 것으로 32bit arm – Realview-PBX 보드의 사용예이다.

 

다음 그림은 SPARSEMEM_EXTREME을 사용하는 것으로 arm64 아키텍처에서 4G DRAM을 사용한 예이다.

 


page 디스크립터

 

모든 물리 메모리 페이지마다 하나의 페이지 디스크립터가 할당된다. 모든 메모리 페이지마다 생성되므로 사이즈에 민감하다. 따라서 사이즈를 최대한 줄이기위해 페이지 디스크립터내에서 관리되는 멤버들을 유니온 타입으로 묶어 사용하도록 설계되었다.

 

다음은 32bit 시스템에서 동작하는 page 디스크립터를 보여준다. (디폴트 옵션을 사용 시 36바이트이다.)

 

다음은 64bit 시스템에서 동작하는 page 디스크립터를 보여준다. (디폴트 옵션을 사용 시 64바이트이다.)

 

struct page

include/linux/mm_types.h -1/3-

/*
 * Each physical page in the system has a struct page associated with
 * it to keep track of whatever it is we are using the page for at the
 * moment. Note that we have no way to track which tasks are using
 * a page, though if it is a pagecache page, rmap structures can tell us
 * who is mapping it.
 *
 * If you allocate the page using alloc_pages(), you can use some of the
 * space in struct page for your own purposes.  The five words in the main
 * union are available, except for bit 0 of the first word which must be
 * kept clear.  Many users use this word to store a pointer to an object
 * which is guaranteed to be aligned.  If you use the same storage as
 * page->mapping, you must restore it to NULL before freeing the page.
 *
 * If your page will not be mapped to userspace, you can also use the four
 * bytes in the mapcount union, but you must call page_mapcount_reset()
 * before freeing it.
 *
 * If you want to use the refcount field, it must be used in such a way
 * that other CPUs temporarily incrementing and then decrementing the
 * refcount does not cause problems.  On receiving the page from
 * alloc_pages(), the refcount will be positive.
 *
 * If you allocate pages of order > 0, you can use some of the fields
 * in each subpage, but you may need to restore some of their values
 * afterwards.
 *
 * SLUB uses cmpxchg_double() to atomically update its freelist and
 * counters.  That requires that freelist & counters be adjacent and
 * double-word aligned.  We align all struct pages to double-word
 * boundaries, and ensure that 'freelist' is aligned within the
 * struct.
 */
struct page {
        unsigned long flags;            /* Atomic flags, some possibly
                                         * updated asynchronously */
        /*
         * Five words (20/40 bytes) are available in this union.
         * WARNING: bit 0 of the first word is used for PageTail(). That
         * means the other users of this union MUST NOT use the bit to
         * avoid collision and false-positive PageTail().
         */
        union {
                struct {        /* Page cache and anonymous pages */
                        /**
                         * @lru: Pageout list, eg. active_list protected by
                         * zone_lru_lock.  Sometimes used as a generic list
                         * by the page owner.
                         */
                        struct list_head lru;
                        /* See page-flags.h for PAGE_MAPPING_FLAGS */
                        struct address_space *mapping;
                        pgoff_t index;          /* Our offset within mapping. */
                        /**
                         * @private: Mapping-private opaque data.
                         * Usually used for buffer_heads if PagePrivate.
                         * Used for swp_entry_t if PageSwapCache.
                         * Indicates order in the buddy system if PageBuddy.
                         */
                        unsigned long private;
                };
                struct {        /* slab, slob and slub */
                        union {
                                struct list_head slab_list;     /* uses lru */
                                struct {        /* Partial pages */
                                        struct page *next;
#ifdef CONFIG_64BIT
                                        int pages;      /* Nr of pages left */
                                        int pobjects;   /* Approximate count */
#else
                                        short int pages;
                                        short int pobjects;
#endif
                                };
                        };
                        struct kmem_cache *slab_cache; /* not slob */
                        /* Double-word boundary */
                        void *freelist;         /* first free object */
                        union {
                                void *s_mem;    /* slab: first object */
                                unsigned long counters;         /* SLUB */
                                struct {                        /* SLUB */
                                        unsigned inuse:16;
                                        unsigned objects:15;
                                        unsigned frozen:1;
                                };
                        };
                };

 

First 워드
  • flags
    • 페이지 플래그

 

2nd ~ 6th 워드 -1/2-

페이지 캐시 또는 anonymous 페이지

  • _lru
    • LRU 리스트에 연결될 때 사용한다.
  • *mapping
    • 유저 매핑 관련 포인터가 담기며 하위 2비트는 이의 용도를 구분하는 플래그로 사용한다.
      • 페이지 캐시로 사용되는 경우 address_space 구조체를 가리킨다.
        • non-lru movable 페이지들은 address_space 구조체 포인터에 PAGE_MAPPING_MOVABLE 플래그를 추가하여 관리한다.
          • 예) zram, balloon 드라이버
      • 유저용 가상 메모리 페이지(anonymous page)인 경우 CONFIG_KSM 커널 옵션 사용 여부에 따라 달라진다.
        • KSM 커널 옵션 사용하지 않을 때에는 PAGE_MAPPING_ANON(1) 플래그만 추가되고 anon 매핑 영역인 anon_vma 구조체 포인터를 가리킨다.
        • KSM 커널 옵션을 사용하는 경우에는 PAGE_MAPPING_ANON(1) 및 PAGE_MAPPING_MOVABLE(2) 플래그를 추가하고, KSM용 private 구조체 포인터를 가리킨다.
  • index
    • 매핑 영역안의 offset 값이 담긴다.
  • private
    • 매핑에 사용하는 private 데이터가 담긴다.
      • Private 페이지에서 buffer_heads를 위해 사용된다.
      • 스웝 페이지 캐시의 swp_entry_t를 위해 사용된다.
      • 버디 페이지의 order가 담긴다.

슬랩(slab, slob, slub) 페이지

  • slab_list
    • LRU 리스트에 연결될 때 사용한다.
  • *next
    • partial 페이지를 관리한다.
  • pages
    • partial 페이지 수가 담긴다.
      • 자신을 포함하여 뒤(next)로 연결되어 있는 slub page들의 수가 담긴다.
  • pobjects
    • 대략적인 object 수
      • 정확하지는 않지만 대략적으로 내 slub page를 포함한 다음(next) slub page들의 총 free object 수가 담긴다.
      • 이 카운터는 종종 free 되는 object들로 인해 정확히 산출되지 않는다
  • *slab_cache
    • 연결된 슬랩 캐시를 가리킨다.
  • *freelist
    • free 오브젝트들이 대기하는 리스트이다.
  • *s_mem
    • slab:의 first object를 가리킨다.
  • counters
    • 아래 32바이트를 한꺼번에 access할 때 사용한다.
    • inuse:16
      • 사용 중인 object 수.
    • objects:15
      • 슬랩이 관리하는 object 수
    • frozen:1
      • per-cpu로 관리하고 있는 슬랩 페이지 여부를 가리킨다.

 

include/linux/mm_types.h -2/3-

                struct {        /* Tail pages of compound page */
                        unsigned long compound_head;    /* Bit zero is set */

                        /* First tail page only */
                        unsigned char compound_dtor;
                        unsigned char compound_order;
                        atomic_t compound_mapcount;
                };
                struct {        /* Second tail page of compound page */
                        unsigned long _compound_pad_1;  /* compound_head */
                        unsigned long _compound_pad_2;
                        struct list_head deferred_list;
                };
                struct {        /* Page table pages */
                        unsigned long _pt_pad_1;        /* compound_head */
                        pgtable_t pmd_huge_pte; /* protected by page->ptl */
                        unsigned long _pt_pad_2;        /* mapping */
                        union {
                                struct mm_struct *pt_mm; /* x86 pgds only */
                                atomic_t pt_frag_refcount; /* powerpc */
                        };
#if ALLOC_SPLIT_PTLOCKS
                        spinlock_t *ptl;
#else
                        spinlock_t ptl;
#endif
                };
                struct {        /* ZONE_DEVICE pages */
                        /** @pgmap: Points to the hosting device page map. */
                        struct dev_pagemap *pgmap;
                        unsigned long hmm_data;
                        unsigned long _zd_pad_1;        /* uses mapping */
                };

                /** @rcu_head: You can use this to free a page by RCU. */
                struct rcu_head rcu_head;
        };
2nd ~ 6th 워드 -2/2-

Compound  tail 페이지들

참고로 Compound 페이지의 헤드 페이지(page[0])에는 PG_head 플래그가 설정된다.

  • compound_head
    • compound 페이지의 헤더가 아닌 모든 tail 페이지에서 compound 헤더 페이지  디스크립터 포인터를 담고, bit0를 1로 설정한다.
  • compound_dtor
    • 첫 번째 tail 페이지(page[1])에 compound 페이지 소멸자 구분 id를 담는다.
    • 다음과 같은 compound 페이지 소멸자 id들 중 하나를 담는다.
      • NULL_COMPOUND_DTOR
      • COMPOUND_PAGE_DTOR
      • HUGETLB_PAGE_DTOR
      • TRANSHUGE_PAGE_DTOR
  • compound_order
    • 첫 번째 tail 페이지(page[1])에 compound 페이지의 order가 담긴다.
  • compound_mapcount
    • 첫 번째 tail 페이지(page[1])에 매핑 카운트 수가 담긴다.
  • _compound_pad_1
    • 사용하지 않는다.
  • _compound_pad_2
    • 사용하지 않는다.
  • deferred_list

페이지 테이블(pgd, pud, pmd, pte)용 페이지

  • _pt_pad_1
    • 사용하지 않는다.
  • pmd_huge_pte
    • huge용 pte 테이블을 가리키는 page 디스크립터 포인터가 담긴다.
  • _pt_pad_2
    • 사용하지 않는다.
  • *pt_mm
    • x86 pgds only
  • *pt_frag_refcount
    • powerpc only
  • *ptl or ptl
    • 페이지 테이블 spinlock이다.
    • spinlock_t 사이즈가 unsigned long 단위에 포함되는 경우 ptl을 사용하고 그렇지 않은 경우 spinlock_t를 할당하고 이를 가리킬 때 *ptl을 사용한다.

존 디바이스 페이지

  • *pgmap
    • 존 디바이스를 위한 dev_pagemap 구조체 포인터가 담긴다.
  • hmm_data
    • hmm 디바이스 메모리용 드라이버 데이터가 담긴다.
  • _zd_pad_1
    • 사용하지 않는다.

 

include/linux/mm_types.h -3/3-

        union {         /* This union is 4 bytes in size. */
                /*
                 * If the page can be mapped to userspace, encodes the number
                 * of times this page is referenced by a page table.
                 */
                atomic_t _mapcount;

                /*
                 * If the page is neither PageSlab nor mappable to userspace,
                 * the value stored here may help determine what this page
                 * is used for.  See page-flags.h for a list of page types
                 * which are currently stored here.
                 */
                unsigned int page_type;

                unsigned int active;            /* SLAB */
                int units;                      /* SLOB */
        };

        /* Usage count. *DO NOT USE DIRECTLY*. See page_ref.h */
        atomic_t _refcount;

#ifdef CONFIG_MEMCG
        struct mem_cgroup *mem_cgroup;
#endif

        /*
         * On machines where all RAM is mapped into kernel address space,
         * we can simply calculate the virtual address. On machines with
         * highmem some memory is mapped into kernel virtual memory
         * dynamically, so we need a place to store that address.
         * Note that this field could be 16 bits on x86 ... 😉
         *
         * Architectures with slow multiplication can define
         * WANT_PAGE_VIRTUAL in asm/page.h
         */
#if defined(WANT_PAGE_VIRTUAL)
        void *virtual;                  /* Kernel virtual address (NULL if
                                           not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */

#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
        int _last_cpupid;
#endif
} _struct_page_alignment;
기타
  • _mapcount
    • 매핑 카운트
    • 여러 개의 페이지 테이블에서 이 페이지를 매핑하는 경우 매핑 카운터가 증가된다.
      • 공유 페이지를 여러 유저 프로세스에서 공유하는 경우 다중 매핑이 된다.
  • page_type
    • 유저 매핑된 페이지가 아니고 슬랩 페이지를 제외한 커널 페이지 타입을 지정한다.
    • 다음과 같은 페이지 타입이저장된다.
      • PG_buddy
      • PG_ballon
      • PG_kmemcg
      • PG_table
  • active
    • slab: 할당자에서 사용된다.
  • units
    • slob: 할당자에서 사용된다.
  • _refcount
    • 참조 카운터로 사용자가 직접 액세스하면 안된다.
  • *mem_cgroup
    • 메모리 cgroup을 사용할 때 해당 mem_cgroup 구조체 포인터를 지정한다.
  • *virtual
    • highmem을 사용하는 32비트 시스템의 특정 아키텍처에서 highmem 페이지가 커널 주소 공간에 매핑된 경우 커널 주소 공간의 가상 주소가 담긴다.
    • ARM32의 경우 HASHED_PAGE_VIRTUAL을 사용한다.
      • highmem 페이지를 매핑 시 page_address_map 구조체를 별도로 할당하여 사용하므로 이 멤버를 사용하지 않는다.
  • _last_cpupid
    • NUMA 밸런싱을 위해 사용되며 페이지의 플래그에 cpu 및 pid를 담지 못하는 경우 이 멤버를 사용하여 마지막 사용한 cpu 및 pid를 담는다.

 


페이지 플래그

/*
 * Various page->flags bits:
 *
 * PG_reserved is set for special pages, which can never be swapped out. Some
 * of them might not even exist...
 *
 * The PG_private bitflag is set on pagecache pages if they contain filesystem
 * specific data (which is normally at page->private). It can be used by
 * private allocations for its own usage.
 *
 * During initiation of disk I/O, PG_locked is set. This bit is set before I/O
 * and cleared when writeback _starts_ or when read _completes_. PG_writeback
 * is set before writeback starts and cleared when it finishes.
 *
 * PG_locked also pins a page in pagecache, and blocks truncation of the file
 * while it is held.
 *
 * page_waitqueue(page) is a wait queue of all tasks waiting for the page
 * to become unlocked.
 *
 * PG_uptodate tells whether the page's contents is valid.  When a read
 * completes, the page becomes uptodate, unless a disk I/O error happened.
 *
 * PG_referenced, PG_reclaim are used for page reclaim for anonymous and
 * file-backed pagecache (see mm/vmscan.c).
 *
 * PG_error is set to indicate that an I/O error occurred on this page.
 *
 * PG_arch_1 is an architecture specific page state bit.  The generic code
 * guarantees that this bit is cleared for a page when it first is entered into
 * the page cache.
 *
 * PG_hwpoison indicates that a page got corrupted in hardware and contains
 * data with incorrect ECC bits that triggered a machine check. Accessing is
 * not safe since it may cause another machine check. Don't touch!
 */

/*
 * Don't use the *_dontuse flags.  Use the macros.  Otherwise you'll break
 * locked- and dirty-page accounting.
 *
 * The page flags field is split into two parts, the main flags area
 * which extends from the low bits upwards, and the fields area which
 * extends from the high bits downwards.
 *
 *  | FIELD | ... | FLAGS |
 *  N-1           ^       0
 *               (NR_PAGEFLAGS)
 *
 * The fields area is reserved for fields mapping zone, node (for NUMA) and
 * SPARSEMEM section (for variants of SPARSEMEM that require section ids like
 * SPARSEMEM_EXTREME with !SPARSEMEM_VMEMMAP).
 */
enum pageflags {
        PG_locked,              /* Page is locked. Don't touch. */
        PG_referenced,
        PG_uptodate,
        PG_dirty,
        PG_lru,
        PG_active,
        PG_workingset,
        PG_waiters,             /* Page has waiters, check its waitqueue. Must be bit #7 and in the
same byte as "PG_locked" */
        PG_error,
        PG_slab,
        PG_owner_priv_1,        /* Owner use. If pagecache, fs may use*/
        PG_arch_1,
        PG_reserved,
        PG_private,             /* If pagecache, has fs-private data */
        PG_private_2,           /* If pagecache, has fs aux data */
        PG_writeback,           /* Page is under writeback */
        PG_head,                /* A head page */
        PG_mappedtodisk,        /* Has blocks allocated on-disk */
        PG_reclaim,             /* To be reclaimed asap */
        PG_swapbacked,          /* Page is backed by RAM/swap */
        PG_unevictable,         /* Page is "unevictable"  */
#ifdef CONFIG_MMU
        PG_mlocked,             /* Page is vma mlocked */
#endif
#ifdef CONFIG_ARCH_USES_PG_UNCACHED
        PG_uncached,            /* Page has been mapped as uncached */
#endif
#ifdef CONFIG_MEMORY_FAILURE
        PG_hwpoison,            /* hardware poisoned page. Don't touch */
#endif
#if defined(CONFIG_IDLE_PAGE_TRACKING) && defined(CONFIG_64BIT)
        PG_young,
        PG_idle,
#endif
        __NR_PAGEFLAGS,

        /* Filesystems */
        PG_checked = PG_owner_priv_1,

        /* SwapBacked */
        PG_swapcache = PG_owner_priv_1, /* Swap page: swp_entry_t in private */

        /* Two page bits are conscripted by FS-Cache to maintain local caching
         * state.  These bits are set on pages belonging to the netfs's inodes
         * when those inodes are being locally cached.
         */
        PG_fscache = PG_private_2,      /* page backed by cache */

        /* XEN */
        /* Pinned in Xen as a read-only pagetable page. */
        PG_pinned = PG_owner_priv_1,
        /* Pinned as part of domain save (see xen_mm_pin_all()). */
        PG_savepinned = PG_dirty,
        /* Has a grant mapping of another (foreign) domain's page. */
        PG_foreign = PG_owner_priv_1,

        /* SLOB */
        PG_slob_free = PG_private,

        /* Compound pages. Stored in first tail page's flags */
        PG_double_map = PG_private_2,

        /* non-lru isolated movable page */
        PG_isolated = PG_reclaim,
};

 

참고

Fixmap

<kernel v5.0>

Fixmap 주요 용도

고정 매핑 영역(이하 fixmap 영역)은 컴파일 타임에 가상 주소가 결정(fix)되는 공간이다. 따라서 주로 매핑 서브시스템(vmap 등)이 활성화되기 전에 매핑이 필요할 때 fixmap을 사용한다. 예를 들어, 콘솔 디바이스를 정식 초기화하기 전에 우선 사용하기 위해 이 공간에 임시 매핑한다. 런타임 시에도 커널 코드를 변경하거나 페이지 테이블의 갱신이 필요할 때 등의 용도로 사용한다.
fixmap은 용도별로 여러 블록으로 나뉜다. 가상 주소는 컴파일 타임에 결정되고, 물리 주소는 fixmap용 API를 사용하여 런타임 시에(부팅 타임 포함) 매핑하여 사용한다. 가상 주소 매핑(vmap)이 활성화되기 전에 소규모의 특정 자원들이 이러한 고정 매핑 주소 공간을 사용한다.

 

아키텍처 및 커널 버전에 따라 용도가 조금씩 다르지만 주로 사용하는 용도는 다음과 같다. 그 외의 용도는 Fixmap 슬롯 분류에서 추가 설명한다.

1) io 디바이스 early 매핑
  • ioremap() 함수를 사용할 수 없는 이른 부트업 타임에 디바이스를 매핑할 수 있도록 아래와 같이 두 가지 방법으로 지원한다.
    • 컴파일 타임에 static하게 설정한 디바이스를 고정 매핑 시 사용한다. (early console 등)
    • 정규 디바이스 매핑(ioremap)을 사용할 수 없는 부트업 초반 러닝 타임에 early_ioremap() 함수를 통해 디바이스를 fixmap 가상 주소 영역에 임시로 매핑하여 사용할 수 있다.
      • 총 7회, 각각 256K 매핑 가능
2) highmem 물리 메모리의 커널 매핑
  • ARM32 커널의 경우 물리 영역의 highmem 페이지를 cpu id 별로 할당된 fixmap 가상 주소 영역에 매핑하여 사용한다.
    • stack based kmap_atomic
      • 초기 매핑할 수 있는 index slot은 기존에는 cpu id와 사용 타입에 따라 고정되었는데 현재는 cpu id에 KM_TYPE_NR(ARM=20) 수 만큼 스택과 같은 방법으로 push/pop을 이용하여 운영된다.
        • 즉 cpu 마다 20개의 매핑 슬롯이 배정된다.
      • 참고: mm: stack based kmap_atomic()
    • kmap_atomic() 함수를 사용하여 highmem 페이지를 현재 cpu에 해당하는 fixmap 영역을 사용하여 매핑을 한다. 이미 kmap 영역에 매핑이 된 경우 해당 가상 주소를 리턴한다.
    • ZONE_HIGHMEM은 커널에서 몇 가지 매핑 방법을 사용하여 항상 매핑과 언매핑을 반복 사용하며 사용하기 때문에 access 속도가 매핑을 위한 overhead가 발생하여 항상 pre 매핑된 ZONE_NORMAL에 비해  느린 성능을 보여준다. 물론 kernel과 다르게 user level 에서는 각 task 마다 매우 방대한 user address space(설정에 따라 1G, 2G 또는 3G)의 공간에 매핑하여 사용한다.
  • 64비트 시스템은 가상 주소 공간이 무척 크므로 시스템 내의 모든 물리 메모리를 모두 매핑할 수 있다. 따라서 이러한 경우에는 highmem을 운용할 필요가 없다.
3) 커널 코드 변경
  • Read only 설정된 커널 코드를 변경할 때 fixmap 가상 주소 영역을 임시로 사용한다.

 

다른 매핑  방법과 간단한 비교

  • vmap
    • 장시간 여러 페이지를 매핑하여 사용할 수 있고 꽤 큰 vmalloc address space에 매핑을 한다.
      • ARM32
        • 240M 공간
      • ARM64
        • CONFIG_VM_BITS 크기 마다 다르며 VM 공간의 절반에서 일부 vmemmap, pci io, fixmap, kimage, module image 영역등을 제외한 공간으로 거의 VM 공간의 절반이라고 생가하면 된다.
        • 예를 들어 CONFIG_VM_BITS=39를 사용하는 경우 VM 크기는 512G이다. 이 중 vmalloc 공간은 약 246G 정도된다.
  • kmap
    • kmap address space에 일정 시간 동안 매핑을 하여 사용한다. 매핑이 완료되면 스케쥴되어 다른 태스크로 바뀌더라도 매핑을 유지한다.
      • ARM32
        • 2M 공간
  • fixmap
    • highmem 페이지에 대해 아주 극히 짧은 시간 매핑하여 사용할 수 있고 fixmap address space에 매핑을 한다. sleep 되지 않아 interrupt context에서 사용될 수 있다.
      • 스케쥴되어 다른 태스크로 바뀌기 전에 unmap 되어야 한다.
    • 다른 io 영역 등을 부트업 타임에 고정매핑하여 사용한다.
    • ARM32
      • 기존 2M -> 3M 공간으로 커졌다.
    • ARM64
      • 커널 버전과 커널 옵션마다 다르지만 현재 약 6M 공간

 

Fixmap 가상 주소 영역

  • fixmap 가상 주소 영역은 아키텍처마다 위치가 다르고 크기도 다르다.
    • ARM32
      • 현재 3M의 가상 주소 공간을 사용하여 768개의 페이지에 높은 주소부터 아래로 인덱스 슬롯(0 ~ 767)을 붙여 사용한다.
      • FIXADDR_START(0xffc0_0000) ~ FIXADDR_END(0xfff0_0000) 까지 3M 영역을 사용한다.
      • FIXADDR_TOP 영역은 FIXADDR_END – PAGE_SIZE(4K ) 이다.
        • 인덱스 지정은 FIXADDR_TOP(0xffef_f000)이 0번 인덱스로 아래 방향으로 인덱스 번호가 증가된다.
        • 인덱스 번호는 0부터 최대 0x2ff (767)을 지정할 수 있다.
    • ARM64
      • 약 6M의 가상 주소 공간을 사용하고 높은 주소부터 아래로 인덱스 슬롯을 붙여 사용한다.

 

다음 그림은 ARM64 시스템에서 fixmap 가상 주소 영역을 보여준다. (VA_BITS=48)

  • vmemmap 공간은 더 커진다.
    • 64 바이트 이하인 경우 2T 영역을 사용한다.
    • 64 바이트를 초과하는 경우 4T 영역을 사용한다.

 

다음 그림은 ARM64 시스템에서 fixmap 가상 주소 영역을 보여준다. (VA_BITS=39)

  • vmemmap 공간은 컴파일 옵션에 따라 달라지는 page 디스크립터(struct page) 사이즈에 따라 크기가 달라진다.
    • 64 바이트 이하인 경우 4G 영역을 사용한다.
    • 64 바이트를 초과하는 경우 8G 영역을 사용한다.

 

다음과 같이 시스템 부팅 시에 fixmap의 가상 주소 위치를 확인할 수 있다. (ARM64, VA_BITS=39, 커널 v4.4)

Virtual kernel memory layout:
    modules : 0xffffff8000000000 - 0xffffff8008000000   (   128 MB)
     malloc : 0xffffff8008000000 - 0xffffffbdbfff0000   (   246 GB)
      .init : 0xffffff8009040000 - 0xffffff8009160000   (  1152 KB)
      .text : 0xffffff8008080000 - 0xffffff8008bf0000   ( 11712 KB)
    .rodata : 0xffffff8008bf0000 - 0xffffff8009040000   (  4416 KB)
      .data : 0xffffff8009160000 - 0xffffff80092f6008   (  1625 KB)
    vmemmap : 0xffffffbdc0000000 - 0xffffffbfc0000000   (     8 GB maximum)
              0xffffffbdc0008000 - 0xffffffbdc3e00000   (    61 MB actual)
    fixed   : 0xffffffbffe7fd000 - 0xffffffbffec00000   (  4108 KB)
    PCI I/O : 0xffffffbffee00000 - 0xffffffbfffe00000   (    16 MB)
    memory  : 0xffffffc000200000 - 0xffffffc0f8000000   (  3966 MB)

 

다음은 시스템에서 vmemmap을 사용하지 않는 경우의 모습이다. (ARM64, VA_BITS=39, 커널 v4.4)

  • vmemmap을 사용하지 않아도 그 공간은 비워둔다.
Virtual kernel memory layout:
    modules : 0xffffff8000000000 - 0xffffff8008000000   (   128 MB)
    vmalloc : 0xffffff8008000000 - 0xffffffbdbfff0000   (   246 GB)
      .init : 0xffffff8008880000 - 0xffffff80088e0000   (   384 KB)
      .text : 0xffffff8008080000 - 0xffffff8008640000   (  5888 KB)
    .rodata : 0xffffff8008640000 - 0xffffff8008880000   (  2304 KB)
      .data : 0xffffff80088e0000 - 0xffffff800895c5e0   (   498 KB)
    fixed   : 0xffffffbffe7fd000 - 0xffffffbffec00000   (  4108 KB)
    PCI I/O : 0xffffffbffee00000 - 0xffffffbfffe00000   (    16 MB)
    memory  : 0xffffffc000000000 - 0xffffffc080000000   (  2048 MB)

 

커널 VA 영역 변경(new)

 

다음 그림과 같이 커널 v5.4에서 커널 영역의 절반이 flip된 것을 확인할 수 있다.

 

커널 v5.4에서 바뀐 주요 매크로 상수 값은 다음과 같다. (예: VA_BITS=48)

VMEMMAP_START=           0xfffffdffffe00000
PCI_IO_END=              0xfffffdffffc00000
PCI_IO_START=            0xfffffdfffec00000
FIXADDR_TOP=             0xfffffdfffea00000
FIXADDR_START=           0xfffffdfffe5f9000
VMALLOC_END=             0xfffffdffbfff0000
VMALLOC_START=           0xffff800010000000
KIMAGE_VADDR=            0xffff800010000000
MODULES_END=             0xffff800010000000
MODULES_VADDR=           0xffff800008000000
BPF_JIT_REGION_END=      0xffff800008000000
BPF_JIT_REGION_START=    0xffff800000000000
PAGE_END=                0xffff800000000000
PAGE_OFFSET=             0xffff000000000000

 

Fixmap 인덱스 슬롯

  • fixmap 인덱스 슬롯 번호에 따라 가상 주소가 고정되어 있다.
    • 예) ARM32
      • index=0 -> vaddr=0xffef_f000 (FIXADDR_TOP)
      • index=1 -> vaddr=0xffef_e000
      • index=767 -> vaddr=0xffc0_0000 (FIXADDR_START)

 

특정 페이지 매핑 예)

물리 주소 0x4000_0000로 시작하는 1 페이지를 fixmap 1번 슬롯 인덱스에 매핑한다.

  • set_fixmap(1, 0x4000_0000)

 

Fixmap 슬롯 분류

Fixmap은 아키텍처 및 커널 버전에 따라 여러 가지 용도로 슬롯을 나누어 제공된다.

  • HOLE
    • ARM32의 경우 사용하지 않는다.
    • ARM64의 경우 디버깅 목적으로 1 개의 슬롯이 제공되며, 예비 엔트리 페이지로 현재 사용하지 않는다.
    • 커널 v3.19-rc1에서 추가되었다.
  • FDT
    • ARM32의 경우 사용하지 않는다.
    • ARM64의 경우 4M의 디바이스 트리(FDT)를 커버하는 슬롯을 제공한다.
      • FDT는 최대 2M이지만 2M 단위의 align을 사용하기 때문에 최대 4M의 영역이 필요하다.
    • 커널 v4.2-rc1에서 추가되었다.
  •  EARLYCON
    • 시리얼 디바이스를 콘솔로 사용할 목적으로 정규 매핑하기 전 입출력을 위해 1개의 인덱스 슬롯을 사용한다. (early console)
    • 이 영역은 2015년 8월 kernel v4.3-rc1에 추가되었다.
  • KMAP
    • 32비트 시스템에서만 사용되는 highmem 물리 메모리 영역을 매핑할 때 사용되는 공간으로 cpu 수(NR_CPUS)에 따라 사용되는 인덱스 슬롯이 다르다.
      • Fixmap이 처음 소개되었을 때 사용하였다.
    • ARM32의 경우 cpu 수에 따라 20개씩 인덱스 슬롯이 주어진다.
      • 기존 16개에서 20개로 증가하였다.
    • x86_32의 경우 cpu 수에 따라 41개씩 인덱스 슬롯이 주어진다.
  • TEXT_POKE
    • 커널 코드, kprobes, static key 등을 사용 시 read only 커널 코드가 변경되는데 이러한 코드를 이곳 한 두개의 슬롯을 사용하여 잠시 매핑 후 변경한다.
    • ARM32의 경우 2개의 슬롯을 사용한다.
    • ARM64의 경우 1개의 슬롯을 사용한다.
  • APEI_GHES
  • ENTRY_TRAMP
  • BTMAPS
    • 정규 ioremap()을 사용할 수 없는 이른 부트업 타임에 early_ioremap()을 통해 디바이스들을 임시 매핑하여 사용하는 곳이다.
    • 최대 7 번의 매핑을 할 수 있으며, 각각은 256K를 사용할 수 있다.
    • 커널 v3.15-rc1에서 추가되었다.
  • FIX_PTE, FIX_PMD, FIX_PUD, FIX_PGD
    • 런타임에 커널 페이지 테이블을 생성 시 TLB에 문제 없이 적용할 목적으로 atomic하게 처리하기 위해 사용되며 각각 1개씩 총 4개의 인덱스 슬롯을 사용한다.
    • 커널 v.4.6-rc1에서 추가되었다.

 

fixed_address – ARM32

arch/arm/include/asm/fixmap.h

enum fixed_addresses {
        FIX_EARLYCON_MEM_BASE,
        __end_of_permanent_fixed_addresses,

        FIX_KMAP_BEGIN = __end_of_permanent_fixed_addresses,
        FIX_KMAP_END = FIX_KMAP_BEGIN + (KM_TYPE_NR * NR_CPUS) - 1,

        /* Support writing RO kernel text via kprobes, jump labels, etc. */
        FIX_TEXT_POKE0,
        FIX_TEXT_POKE1,

        __end_of_fixmap_region,

        /*
         * Share the kmap() region with early_ioremap(): this is guaranteed
         * not to clash since early_ioremap() is only available before
         * paging_init(), and kmap() only after.
         */
#define NR_FIX_BTMAPS           32
#define FIX_BTMAPS_SLOTS        7
#define TOTAL_FIX_BTMAPS        (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)

        FIX_BTMAP_END = __end_of_permanent_fixed_addresses,
        FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1,
        __end_of_early_ioremap_region
};

 

다음 그림은 ARM32 fixmap 블럭들을 보여준다.

 

fixed_address – ARM64

arch/arm64/include/asm/fixmap.h

/*
 * Here we define all the compile-time 'special' virtual
 * addresses. The point is to have a constant address at
 * compile time, but to set the physical address only
 * in the boot process.
 *
 * These 'compile-time allocated' memory buffers are
 * page-sized. Use set_fixmap(idx,phys) to associate
 * physical memory with fixmap indices.
 *
 */
enum fixed_addresses {
        FIX_HOLE,

        /*
         * Reserve a virtual window for the FDT that is 2 MB larger than the
         * maximum supported size, and put it at the top of the fixmap region.
         * The additional space ensures that any FDT that does not exceed
         * MAX_FDT_SIZE can be mapped regardless of whether it crosses any
         * 2 MB alignment boundaries.
         *
         * Keep this at the top so it remains 2 MB aligned.
         */
#define FIX_FDT_SIZE            (MAX_FDT_SIZE + SZ_2M)
        FIX_FDT_END,
        FIX_FDT = FIX_FDT_END + FIX_FDT_SIZE / PAGE_SIZE - 1,

        FIX_EARLYCON_MEM_BASE,
        FIX_TEXT_POKE0,

#ifdef CONFIG_ACPI_APEI_GHES
        /* Used for GHES mapping from assorted contexts */
        FIX_APEI_GHES_IRQ,
        FIX_APEI_GHES_NMI,
#endif /* CONFIG_ACPI_APEI_GHES */

#ifdef CONFIG_UNMAP_KERNEL_AT_EL0
        FIX_ENTRY_TRAMP_DATA,
        FIX_ENTRY_TRAMP_TEXT,
#define TRAMP_VALIAS            (__fix_to_virt(FIX_ENTRY_TRAMP_TEXT))
#endif /* CONFIG_UNMAP_KERNEL_AT_EL0 */
        __end_of_permanent_fixed_addresses,

        /*
         * Temporary boot-time mappings, used by early_ioremap(),
         * before ioremap() is functional.
         */
#define NR_FIX_BTMAPS           (SZ_256K / PAGE_SIZE)
#define FIX_BTMAPS_SLOTS        7
#define TOTAL_FIX_BTMAPS        (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)

        FIX_BTMAP_END = __end_of_permanent_fixed_addresses,
        FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1,

        /*
         * Used for kernel page table creation, so unmapped memory may be used
         * for tables.
         */
        FIX_PTE,
        FIX_PMD,
        FIX_PUD,
        FIX_PGD,

        __end_of_fixed_addresses
};

 

다음 그림은 ARM64 fixmap 블럭들을 보여준다.

 

초기화

early_fixmap_init() – ARM32

arch/arm/mm/mmu.c

void __init early_fixmap_init(void)
{
        pmd_t *pmd;

        /*
         * The early fixmap range spans multiple pmds, for which
         * we are not prepared:
         */
        BUILD_BUG_ON((__fix_to_virt(__end_of_early_ioremap_region) >> PMD_SHIFT)
                     != FIXADDR_TOP >> PMD_SHIFT);

        pmd = fixmap_pmd(FIXADDR_TOP);
        pmd_populate_kernel(&init_mm, pmd, bm_pte);

        pte_offset_fixmap = pte_offset_early_fixmap;
}

fixmap 영역을 활성화한다.

  • 코드 라인 12에서 fixmap 가상 주소의 top 영역에 해당하는 pmd 엔트리를 알아온다.
  • 코드 라인 13에서 컴파일 타임에 준비된 static bm_pte 페이지를 사용하여 fixmap을 운용할 pte 테이블을 연결하여 활성화한다.
    • pte 테이블 하나당 2M를 커버한다. pte 영역이 4M라 두 개의 pte 테이블이 필요하지만 처음에는 최상위 영역만 활성화한다.

 

early_fixmap_init() – ARM64

dynamic 매핑이 활성화되기 이전에 일부 고정된 가상 주소 영역에 특정 물리 주소를 매핑 시켜 사용할 수 있는 fixmap 가상 주소 영역을 먼저(early) 사용하려 한다. 이러한 fixmap 가상 주소 영역을 지원하기 위해 pgd 테이블의 fixmap 가상 주소에 해당하는 엔트리에 3개의 static 페이지 테이블 bm_pud[], bm_pmd[] 및 bm_pte[]를 연결 구성하여 fixmap 가상 주소 영역을 사용할 수 있는 완전한 매핑 테이블을 준비한다. 정규 매핑 함수 및 정규 메모리 할당자를 사용할 수 없으므로 3개의 페이지 테이블은 컴파일 타임에 static하게 생성하여 활용한다. 참고로 early_fixmap_init() 함수에서는 전체 fixmap 영역 중 FDT를 제외한 아랫 부분 2M를 먼저 커버하기 위해 매핑합니다.

 

arch/arm64/mm/mmu.c

/*
 * The p*d_populate functions call virt_to_phys implicitly so they can't be used
 * directly on kernel symbols (bm_p*d). This function is called too early to use
 * lm_alias so __p*d_populate functions must be used to populate with the
 * physical address from __pa_symbol.
 */
void __init early_fixmap_init(void)
{
        pgd_t *pgdp, pgd;
        pud_t *pudp;
        pmd_t *pmdp;
        unsigned long addr = FIXADDR_START;

        pgdp = pgd_offset_k(addr);
        pgd = READ_ONCE(*pgdp);
        if (CONFIG_PGTABLE_LEVELS > 3 &&
            !(pgd_none(pgd) || pgd_page_paddr(pgd) == __pa_symbol(bm_pud))) {
                /*
                 * We only end up here if the kernel mapping and the fixmap
                 * share the top level pgd entry, which should only happen on
                 * 16k/4 levels configurations.
                 */
                BUG_ON(!IS_ENABLED(CONFIG_ARM64_16K_PAGES));
                pudp = pud_offset_kimg(pgdp, addr);
        } else {
                if (pgd_none(pgd))
                        __pgd_populate(pgdp, __pa_symbol(bm_pud), PUD_TYPE_TABLE);
                pudp = fixmap_pud(addr);
        }
        if (pud_none(READ_ONCE(*pudp)))
                __pud_populate(pudp, __pa_symbol(bm_pmd), PMD_TYPE_TABLE);
        pmdp = fixmap_pmd(addr);
        __pmd_populate(pmdp, __pa_symbol(bm_pte), PMD_TYPE_TABLE);

        /*
         * The boot-ioremap range spans multiple pmds, for which
         * we are not prepared:
         */
        BUILD_BUG_ON((__fix_to_virt(FIX_BTMAP_BEGIN) >> PMD_SHIFT)
                     != (__fix_to_virt(FIX_BTMAP_END) >> PMD_SHIFT));

        if ((pmdp != fixmap_pmd(fix_to_virt(FIX_BTMAP_BEGIN)))
             || pmdp != fixmap_pmd(fix_to_virt(FIX_BTMAP_END))) {
                WARN_ON(1);
                pr_warn("pmdp %p != %p, %p\n",
                        pmdp, fixmap_pmd(fix_to_virt(FIX_BTMAP_BEGIN)),
                        fixmap_pmd(fix_to_virt(FIX_BTMAP_END)));
                pr_warn("fix_to_virt(FIX_BTMAP_BEGIN): %08lx\n",
                        fix_to_virt(FIX_BTMAP_BEGIN));
                pr_warn("fix_to_virt(FIX_BTMAP_END):   %08lx\n",
                        fix_to_virt(FIX_BTMAP_END));

                pr_warn("FIX_BTMAP_END:       %d\n", FIX_BTMAP_END);
                pr_warn("FIX_BTMAP_BEGIN:     %d\n", FIX_BTMAP_BEGIN);
        }
}

fixmap 가상 주소 영역을 활성화한다.

  • p*d_populate() 함수 사용
    • 페이지 테이블들을 레벨 별로 연결하기 위해 각 페이지 테이블의 물리 주소가 필요하다. 컴파일 타임에 커널 심볼로 결정된 bm_pud[] 등의 테이블 주소를 물리 주소로 변환하여야 한다. p*g_populate() 함수는 내부에서 virt_to_phys() 및 __pa() 함수를 직접적으로 사용하는데, 아직은 부트 업 초반 시점이라 페이지 디스크립터 배열로 구성된 memmap 등이 활성화되지 않은 상태이다. 따라서 memmap을 사용하여 가상 주소를 물리 주소로 변경하는 정규 API인 virt_to_phys() 및 __pa() 함수를 사용할 수는 없다. 이러한 경우를 대비해서 커널 심볼에 한하여 물리 주소로 바꿔주는 별도의 함수가 있다. __pa_symbol() 함수를 사용하여 가상 주소로된 커널 심볼 주소로 물리 주소를 구한다. 그런 후 구한 물리 주소를 인자로 사용하여 __p*g_populate() 함수를 사용한다.
  • 코드 라인 6에서 fixmap 영역의 가장 낮은 가상 주소를 addr에 대입한다.
    • arm32와 달리 arm64에서는 fixmap의 상위 주소에 4M에 해당하는 DTB가 위치하고 처음에 사용하려고 하는 대부분의 항목들은 fixmap 영역의 아래에 위치하므로 주소가 낮은 부분을 먼저 활성화할 계획이다.
  • 코드 라인 8~9에서 가상 주소 addr에 해당하는 pgd 엔트리 값을 읽어온다.
    • pgd 테이블에서 pgd 엔트리 포인터인 pgdp를 알아온 후, 이 포인터를 통해 pgd 엔트리 값을 읽어 pgd에 대입한다.
  • 코드 라인 10~18에서 페이지 테이블 변환 레벨이 4단계이고 페이지 크기로 16K를 사용하는 커널인 경우 pgd 엔트리가 최대 2개밖에 존재하지 않는다. 그중 하나는 커널 메모리용 가상 주소 공간이고, 나머지 하나는 커널에서 여러 용도로 사용되는 몇 가지 공간 주소를 모두 포함하여 사용되며 그중에는 커널 이미지 영역이나 fixmap 영역도 포함된다. 즉, 커널 이미지와 fixmap 영역은 1개의 bm_pud[ ] 테이블에 존재하게 된다. 이러한 경우 bm_pud[ ] 페이지 테이블이 커널 이미지 용도로 이미 활성화되어 사용 중이므로 fixmap을 위해 다시 활성화할 필요가 없어진다. 따라서 곧바로 fixmap 시작 주소에 해당하는 pud 엔트리 포인터를 구한다.
    • 이 조건은 4단계 + 16K 페이지를 사용하는 것만 허용하므로 그 외의 경우 버그 메시지를 출력한다.
  • 코드 라인 19~23에서 그 외의 경우 bm_pud[ ] 테이블은 fixmap 영역 및 커널 이미지 영역과 같이 공유하지 않고 fixmap 영역 위주로 사용한다. 그래서 fixmap 영역을 사용하기 위해 bm_pud[ ] 테이블을 pgd 엔트리와 연결하여 활성화한 후에 fixmap 시작 주소에 해당하는 pud 엔트리 주소를 구한다.
    • 4 단계 + 4K 페이지 및 3단계의 모든 페이지(4K, 16K, 64K)를 포함한다.
  • 코드 라인 24~25에서 pud에 연결된 pmd 테이블이 없는 경우 bm_pmd[] 테이블을 사용하여 연결한다.
  • 코드 라인 26에서 addr 주소에 해당하는 pmd 엔트리 포인터를 알아온다.
  • 코드 라인 27에서 pmd에 연결된 pte 테이블이 없는 경우 bm_pte[] 테이블을 사용하여 연결한다.
    • 전체 fixmap 영역 중 FDT를 제외한 아랫부분 2M에 해당하는 부분을 매핑하여 커버합니다.
  • 코드 라인 36~49에서 early_ioremap() 함수에서 사용하는 btmap 영역의 시작과 끝에 해당하는 pud 테이블의 pmd 엔트리 주소 값들이 위에서 읽어온 pmd 엔트리 주소 값과 다른 경우 경고 메시지를 출력한다.

 

bm_pte, bm_pmd, bm_pud는 fixmap에 대한 페이지 테이블로 별도의 페이지 할당 없이 커널 빌드 시 생성되는 static 배열을 이용한다

arch/arm64/mm/mmu.c

static pte_t bm_pte[PTRS_PER_PTE] __page_aligned_bss;
static pmd_t bm_pmd[PTRS_PER_PMD] __page_aligned_bss __maybe_unused;
static pud_t bm_pud[PTRS_PER_PUD] __page_aligned_bss __maybe_unused;

 

다음 그림은 fixmap을 위해 각 단계의 페이지 테이블이 활성화되는 모습을 보여준다.

  • 4K 페이지, VA_BITS=39를 사용하면 3단계의 페이지 테이블을 사용하므로 중간 pud 테이블이 생략된다.

 

주요 Fixmap API

set_fixmap()

include/asm-generic/fixmap.h

#define set_fixmap(idx, phys)                           \
        __set_fixmap(idx, phys, FIXMAP_PAGE_NORMAL)

fixmap의 요청 인덱스 @idx 에 물리 주소@phys에 해당하는 페이지 하나를 매핑하는데 normal 커널 페이지 매핑 속성으로 한다.

  • ARM32
    • L_PTE_YOUNG | L_PTE_PRESENT | L_PTE_XN | L_PTE_DIRTY | L_PTE_MT_WRITEBACK
  • ARM64
    • PROT_NORMAL 속성 사용
      • PTE_TYPE_PAGE | PTE_AF | PTE_SHARED | [PTE_NG] | PTE_PXN | PTE_UXN | PTE_DIRTY | PTE_WRITE | PTE_ATTRINDX(MT_NORMAL)

 

clear_fixmap()

include/asm-generic/fixmap.h

#define clear_fixmap(idx)                       \
        __set_fixmap(idx, 0, FIXMAP_PAGE_CLEAR)

fixmap의 요청 인덱스 @idx 영역에 매핑된 물리 주소 1개 페이지를 언매핑하는데 속성은 CLEAR(0)로 설정한다.

 

set_fixmap_nocache()

include/asm-generic/fixmap.h

#define set_fixmap_nocache(idx, phys) \
        __set_fixmap(idx, phys, FIXMAP_PAGE_NOCACHE)

fixmap의 요청 인덱스 @idx 영역에 물리 주소 @phys에 해당하는 1개 페이지를 캐시 없이 매핑하는데 속성은 FIXMAP_PAGE_NOCACHE로 설정한다.

  • ARM32
    • L_PTE_YOUNG | L_PTE_PRESENT | L_PTE_XN | L_PTE_DIRTY | L_PTE_MT_DEV_SHARED | L_PTE_SHARE
  • ARM64
    • PROT_NORMAL_NC 속성 사용
      • PTE_TYPE_PAGE | PTE_AF | PTE_SHARED | [PTE_NG] | PTE_PXN | PTE_UXN | PTE_DIRTY | PTE_WRITE | PTE_ATTRINDX(MT_NORMAL_NC)

 

set_fixmap_io()

include/asm-generic/fixmap.h

#define set_fixmap_io(idx, phys) \
        __set_fixmap(idx, phys, FIXMAP_PAGE_IO)

fixmap의 요청 인덱스 @idx 영역에 물리 주소 @phys에 해당하는 1개 페이지를 매핑하는데 속성은 FIXMAP_PAGE_IO로 설정한다.

  • ARM32
    • nocache와 동일한 속성 사용
  • ARM64
    • PROT_DEVICE_nGnRE 속성 사용
      • PTE_TYPE_PAGE | PTE_AF | PTE_SHARED | [PTE_NG] | PTE_PXN | PTE_UXN | PTE_DIRTY | PTE_WRITE | PTE_ATTRINDX(MT_DEVICE_nGnRE)

 

__set_fixmap() – ARM32

arch/arm/mm/mmu.c

/*
 * To avoid TLB flush broadcasts, this uses local_flush_tlb_kernel_range().
 * As a result, this can only be called with preemption disabled, as under
 * stop_machine().
 */
void __set_fixmap(enum fixed_addresses idx, phys_addr_t phys, pgprot_t prot)
{
        unsigned long vaddr = __fix_to_virt(idx);
        pte_t *pte = pte_offset_kernel(pmd_off_k(vaddr), vaddr);

        /* Make sure fixmap region does not exceed available allocation. */
        BUILD_BUG_ON(FIXADDR_START + (__end_of_fixed_addresses * PAGE_SIZE) >
                     FIXADDR_END);
        BUG_ON(idx >= __end_of_fixed_addresses);

        if (pgprot_val(prot))
                set_pte_at(NULL, vaddr, pte,
                        pfn_pte(phys >> PAGE_SHIFT, prot));
        else
                pte_clear(NULL, vaddr, pte);
        local_flush_tlb_kernel_range(vaddr, vaddr + PAGE_SIZE);
}

물리 주소 @phys에 해당하는 페이지 1개를 fixmap 인덱스 @idx 번호 영역에 @prot 속성으로 매핑한다. 이 함수를 언매핑 용도로 사용할 수도 있는데, 이러한 경우에는 phys 물리 주소 및 flags 속성을 0으로 사용한다.

  • 코드 라인 3에서 인덱스 번호 @idx에 매치되는 fixmap 영역의 가상 주소를 알아온다.
  • 코드 라인 4에서 가상 주소에 해당하는 페이지 테이블의 엔트리 포인터를 알아온다.
  • 코드 라인 11~13에서 매핑을 요청한 경우 플래그를 사용하여 fixmap 엔트리를 물리 주소 @phys에 매핑한다.
  • 코드 라인 14~15에서 언매핑을 요청한 경우 fixmap 슬롯 인덱스에 해당하는 pte 엔트리를 클리어한다.
  • 코드 라인 16에서 해당 가상 주소 페이지 영역에 대한 tlb 플러시를 수행한다.

 

__set_fixmap() – ARM64

arch/arm64/mm/mmu.c

/*
 * Unusually, this is also called in IRQ context (ghes_iounmap_irq) so if we
 * ever need to use IPIs for TLB broadcasting, then we're in trouble here.
 */
void __set_fixmap(enum fixed_addresses idx,
                               phys_addr_t phys, pgprot_t flags)
{
        unsigned long addr = __fix_to_virt(idx);
        pte_t *ptep;

        BUG_ON(idx <= FIX_HOLE || idx >= __end_of_fixed_addresses);

        ptep = fixmap_pte(addr);

        if (pgprot_val(flags)) {
                set_pte(ptep, pfn_pte(phys >> PAGE_SHIFT, flags));
        } else {
                pte_clear(&init_mm, addr, ptep);
                flush_tlb_kernel_range(addr, addr+PAGE_SIZE);
        }
}

@idx에 해당하는 슬롯 위치의 fixmap 가상 주소 공간에 @phys 물리 주소를 @flags 속성으로 매핑한다. 이 함수를 언매핑 용도로 사용할 수도 있는데, 이러한 경우에는 phys 물리 주소 및 flags 속성을 0으로 사용한다.

  • 코드 라인 4에서 인덱스 번호 @idx에 매치되는 fixmap 영역의 가상 주소를 알아온다.
  • 코드 라인 9에서 가상 주소에 해당하는 페이지 테이블의 엔트리 포인터를 알아온다.
  • 코드 라인 11~12에서 매핑을 요청한 경우 플래그를 사용하여 fixmap 엔트리를 물리 주소 @phys에 매핑한다.
  • 코드 라인 13~16에서 언매핑을 요청한 경우 fixmap 슬롯 인덱스에 해당하는 pte 엔트리를 클리어한 후 해당 가상 주소 1개 페이지 영역에 대한 tlb 플러시를 수행한다.

 

fix_to_virt()

include/asm-generic/fixmap.h

/*
 * 'index to address' translation. If anyone tries to use the idx
 * directly without translation, we catch the bug with a NULL-deference
 * kernel oops. Illegal ranges of incoming indices are caught too.
 */
static __always_inline unsigned long fix_to_virt(const unsigned int idx)
{
        BUILD_BUG_ON(idx >= __end_of_fixed_addresses);
        return __fix_to_virt(idx);
}

fixmap 영역에 대한 인덱스로 가상 주소를 알아온다.

#define __fix_to_virt(x)        (FIXADDR_TOP - ((x) << PAGE_SHIFT))

 

virt_to_fix()

include/asm-generic/fixmap.h

static inline unsigned long virt_to_fix(const unsigned long vaddr)
{
        BUG_ON(vaddr >= FIXADDR_TOP || vaddr < FIXADDR_START);
        return __virt_to_fix(vaddr);
}

fixmap area에 매치되는 fixmap 인덱스를 리턴한다.

 

__virt_to_fix()

include/asm-generic/fixmap.h

#define __virt_to_fix(x)        ((FIXADDR_TOP - ((x)&PAGE_MASK)) >> PAGE_SHIFT)

 

Highmem 페이지 매핑

다음 그림은 fixmap 영역에서 highmem 페이지 매핑에 사용되는 FIX_KMAP 블럭을 보여준다.

 

highmem 매핑 예)

highmem 페이지를 fixmap의 kmap 블럭에 매핑한다. cpu id별로 할당된 수 만큼의 영역에서 스택처럼 push/pull로 운영하여 사용한다.

  • kmap_atomic(page)

 

다음 그림은 highmem 물리 메모리 페이지를 fixmap 공간의 kmap 블럭에 매핑하여 사용하는 모습을 보여준다.

 

다음 그림은 ARM32에서 highmem 물리 페이지를 fixmap 영역에 매핑할 때 페이지 디스크립터와 페이지 테이블간의 관계를 보여준다.

  • mem_map에 포함된 page 디스크립터들, pgd 및 pte 페이지 테이블 등이 어우러져 복잡한 모습이다.

fixmap-1b

 

highmem 페이지 할당 & 해제

kmap_atomic() – ARM32

arch/arm/mm/highmem.c

void *kmap_atomic(struct page *page)
{
        unsigned int idx;
        unsigned long vaddr;
        void *kmap;
        int type;

        preempt_disable();
        pagefault_disable();
        if (!PageHighMem(page))
                return page_address(page);

#ifdef CONFIG_DEBUG_HIGHMEM
        /*
         * There is no cache coherency issue when non VIVT, so force the
         * dedicated kmap usage for better debugging purposes in that case.
         */
        if (!cache_is_vivt())
                kmap = NULL;
        else
#endif
                kmap = kmap_high_get(page);
        if (kmap)
                return kmap;

        type = kmap_atomic_idx_push();

        idx = type + KM_TYPE_NR * smp_processor_id();
        vaddr = __fix_to_virt(idx);
#ifdef CONFIG_DEBUG_HIGHMEM
        /*
         * With debugging enabled, kunmap_atomic forces that entry to 0.
         * Make sure it was indeed properly unmapped.
         */
        BUG_ON(!pte_none(get_fixmap_pte(vaddr)));
#endif
        /*
         * When debugging is off, kunmap_atomic leaves the previous mapping
         * in place, so the contained TLB flush ensures the TLB is updated
         * with the new mapping.
         */
        set_fixmap_pte(idx, mk_pte(page, kmap_prot));

        return (void *)vaddr;
}
EXPORT_SYMBOL(kmap_atomic);

highmem page를 atomic하게 fixmap 영역에 매핑한다. 이미 kmap 영역에 매핑된 경우 매핑된 가상 주소를 리턴한다.

  • pagefault_disable()
    • preemption 카운터를 증가시켜 preemption을 disable하고 barrier()를 수행한다.
  • if (!PageHighMem(page))
    • page 주소가 highmem 영역이 아니면
  • return page_address(page);
    • kmap에 이미 매핑되어 있는 경우 page에 해당하는 가상 주소를 리턴한다.
  • kmap = kmap_high_get(page);
    • pkmap 참조 카운터를 증가시키고 highmem page에 해당하는 가상 주소를 리턴한다.
  •  if (kmap)
    • 만일 kmap이 이미 발견된 경우 리턴한다.
  • type = kmap_atomic_idx_push();
    • __kmap_atomic_idx 증가 시키고 이전 값을 알아온다.
  • idx = type + KM_TYPE_NR * smp_processor_id();
    • type 값 + KM_TYPE_NR(20) * cpu id
  • vaddr = __fix_to_virt(idx);
    • fixmap에서 해당 idx 번호로 가상 주소를 알아온다.
    • fixmap은 idx 0번이 FIXADDR_TOP을 가리킨다.
      • FIXADDR_TOP
        • 0xffef_f000= (FIXADDR_END(0xfff00000UL) – PAGE_SIZE)
    • 인덱스 번호는 0부터 최대 0x2ff (767)번 까지 가능하며 매핑 되는 가상 주소는 0xffc0_0000 ~ 0xffef_ffff까지 총 3M이다.
      • 실제 허용 가능한 슬롯 인덱스 번호는 해당 cpu별로 KM_TYPE_NR(ARM=20) 개로 제한된다.
  • set_fixmap_pte(idx, mk_pte(page, kmap_prot));
    • mk_pte()
      • page 주소와 kmap_prot 속성 값을 합쳐서 pte 엔트리를 만든다.
    • idx 번호에 해당하는 fixmap 영역에 pte 엔트리를 매핑한다.

 

__fix_to_virt()

include/asm-generic/fixmap.h

#define __fix_to_virt(x)        (FIXADDR_TOP - ((x) << PAGE_SHIFT))

 

mk_pte() – ARM & ARM64

arch/arm64/include/asm/pgtable.h

#define mk_pte(page,prot)       pfn_pte(page_to_pfn(page), prot)

 

set_fixmap_pte() – ARM32

arch/arm/mm/highmem.c

static inline void set_fixmap_pte(int idx, pte_t pte)
{
        unsigned long vaddr = __fix_to_virt(idx);
        pte_t *ptep = pte_offset_kernel(pmd_off_k(vaddr), vaddr);

        set_pte_ext(ptep, pte, 0); 
        local_flush_tlb_kernel_page(vaddr);
}

idx 번호에 해당하는 fixmap 영역에 pte 엔트리를 매핑한다.

  • unsigned long vaddr = __fix_to_virt(idx);
    • idx 번호에 해당하는 fixmap 영역의 가상 주소를 알아온다.
  • pte_t *ptep = pte_offset_kernel(pmd_off_k(vaddr), vaddr);
    • pmd_off_k()
      • 가상 주소값으로 pmd 엔트리 주소 값을 알아온다.
    • pte_offset_kernel()
      • pmd 엔트리 주소 값과 vaddr 값을 사용하여 pte 엔트리 주소를 알아온다.
  • set_pte_ext(ptep, pte, 0);
    • pte 엔트리 주소에 pte 값을 저장한다.
      • rpi2: cpu_v7_set_pte_ext() 호출
  • local_flush_tlb_kernel_page(vaddr);
    • vaddr에 해당하는 tlb 캐시를 flush한다.

 

local_flush_tlb_kernel_page() – ARM32

arch/arm/include/asm/tlbflush.h

static inline void local_flush_tlb_kernel_page(unsigned long kaddr)
{
        const unsigned int __tlb_flag = __cpu_tlb_flags;

        kaddr &= PAGE_MASK;

        if (tlb_flag(TLB_WB))
                dsb(nshst);

        __local_flush_tlb_kernel_page(kaddr);
        tlb_op(TLB_V7_UIS_PAGE, "c8, c7, 1", kaddr);

        if (tlb_flag(TLB_BARRIER)) {
                dsb(nsh);
                isb();
        }   
}

 

__local_flush_tlb_kernel_page() – ARM32

arch/arm/include/asm/tlbflush.h

static inline void __local_flush_tlb_kernel_page(unsigned long kaddr)
{
        const int zero = 0;
        const unsigned int __tlb_flag = __cpu_tlb_flags;

        tlb_op(TLB_V4_U_PAGE, "c8, c7, 1", kaddr);
        tlb_op(TLB_V4_D_PAGE, "c8, c6, 1", kaddr);
        tlb_op(TLB_V4_I_PAGE, "c8, c5, 1", kaddr);
        if (!tlb_flag(TLB_V4_I_PAGE) && tlb_flag(TLB_V4_I_FULL))
                asm("mcr p15, 0, %0, c8, c5, 0" : : "r" (zero) : "cc");

        tlb_op(TLB_V6_U_PAGE, "c8, c7, 1", kaddr);
        tlb_op(TLB_V6_D_PAGE, "c8, c6, 1", kaddr);
        tlb_op(TLB_V6_I_PAGE, "c8, c5, 1", kaddr);
}

 

tlb_op() – ARM32

arch/arm/include/asm/tlbflush.h

#define tlb_op(f, regs, arg)    __tlb_op(f, "p15, 0, %0, " regs, arg)

 

__tlb_op() – ARM32

arch/arm/include/asm/tlbflush.h

#define __tlb_op(f, insnarg, arg)                                       \
        do {                                                            \
                if (always_tlb_flags & (f))                             \
                        asm("mcr " insnarg                              \
                            : : "r" (arg) : "cc");                      \
                else if (possible_tlb_flags & (f))                      \
                        asm("tst %1, %2\n\t"                            \
                            "mcrne " insnarg                            \
                            : : "r" (arg), "r" (__tlb_flag), "Ir" (f)   \
                            : "cc");                                    \
        } while (0)

 

 

kmap_atomic_idx_push() – 32bit

include/linux/highmem.h

static inline int kmap_atomic_idx_push(void)
{
        int idx = __this_cpu_inc_return(__kmap_atomic_idx) - 1;

#ifdef CONFIG_DEBUG_HIGHMEM
        WARN_ON_ONCE(in_irq() && !irqs_disabled());
        BUG_ON(idx >= KM_TYPE_NR);
#endif
        return idx;
}

해당 cpu에서 사용할 fixmap용 슬롯 인덱스를 리턴하고 그 값은 증가 시킨다.

  • CONFIG_DEBUG_HIGHMEM 옵션을 사용하는 경우 fixmap 인덱스 슬롯이 KM_TYPE_NR(ARM=20)을 초과하는 경우 버그에 대한 메시지를 출력하고 멈춘다.
  • int idx = __this_cpu_inc_return(__kmap_atomic_idx);
    • __kmap_atomic_idx per-cpu 데이터를 1 증가
    • idx 에는 증가전 값을 담아 리턴한다.

 

__this_cpu_inc_return()
#define __this_cpu_inc_return(pcp)      __this_cpu_add_return(pcp, 1)
  • pcp에 1을 더한 값을 리턴

 

__this_cpu_add_return()
#define __this_cpu_add_return(pcp, val)                                 \
({                                                                      \
        __this_cpu_preempt_check("add_return");                         \
        raw_cpu_add_return(pcp, val);                                   \
})
  • preemption이 disable되어 있지  않거나 irq가 disable되어 있지 않으면 stack dump
  • __kmap_atomic_idx per-cpu 데이터를 1 증가

 

raw_cpu_add_return()
#define raw_cpu_add_return(pcp, val)    __pcpu_size_call_return2(raw_cpu_add_return_, pcp, val)
  • scalar 데이터 타입 pcp의 사이즈에 따른 최적화된 덧셈 함수를 분류하기 위해 매크로 함수 호출

 

__pcpu_size_call_return2()
#define __pcpu_size_call_return2(stem, variable, ...)                   \
({                                                                      \
        typeof(variable) pscr2_ret__;                                   \
        __verify_pcpu_ptr(&(variable));                                 \
        switch(sizeof(variable)) {                                      \
        case 1: pscr2_ret__ = stem##1(variable, __VA_ARGS__); break;    \
        case 2: pscr2_ret__ = stem##2(variable, __VA_ARGS__); break;    \
        case 4: pscr2_ret__ = stem##4(variable, __VA_ARGS__); break;    \
        case 8: pscr2_ret__ = stem##8(variable, __VA_ARGS__); break;    \
        default:                                                        \
                __bad_size_call_parameter(); break;                     \
        }                                                               \
        pscr2_ret__;                                                    \
})
  • variable의 사이즈에 따라 stem(함수명 인수)+1/2/4/8 숫자를 붙여 호출

 

this_cpu_add_return_4()
#define this_cpu_add_return_4(pcp, val) this_cpu_generic_add_return(pcp, val)
  • ARM 아키텍처는 사이즈와 관계 없이 generic 코드를 호출한다.

 

this_cpu_generic_add_return()
#define this_cpu_generic_add_return(pcp, val)                           \
({                                                                      \
        typeof(pcp) __ret;                                              \
        unsigned long __flags;                                          \
        raw_local_irq_save(__flags);                                    \
        raw_cpu_add(pcp, val);                                          \
        __ret = raw_cpu_read(pcp);                                      \
        raw_local_irq_restore(__flags);                                 \
        __ret;                                                          \
})
  • per-cpu 변수에 val 값을 더하고 다시 읽어 리턴한다.

 

raw_cpu_add()
#define raw_cpu_add(pcp, val)           __pcpu_size_call(raw_cpu_add_, pcp, val)
  • scalar 데이터 타입 pcp의 사이즈에 따른 최적화된 덧셈 함수를 분류하기 위해 매크로 함수 호출

 

__pcpu_size_call()
#define __pcpu_size_call(stem, variable, ...)                           \
do {                                                                    \
        __verify_pcpu_ptr(&(variable));                                 \
        switch(sizeof(variable)) {                                      \
                case 1: stem##1(variable, __VA_ARGS__);break;           \
                case 2: stem##2(variable, __VA_ARGS__);break;           \
                case 4: stem##4(variable, __VA_ARGS__);break;           \
                case 8: stem##8(variable, __VA_ARGS__);break;           \
                default:                                                \
                        __bad_size_call_parameter();break;              \
        }                                                               \
} while (0)
  • variable의 사이즈에 따라 stem(함수명 인수)+1/2/4/8 숫자를 붙여 호출

 

raw_cpu_add_4()
#define raw_cpu_add_4(pcp, val)         raw_cpu_generic_to_op(pcp, val, +=)
  • ARM 아키텍처는 사이즈와 관계 없이 generic 코드를 호출한다.

 

raw_cpu_generic_to_op()
#define raw_cpu_generic_to_op(pcp, val, op)                             \
do {                                                                    \
        *raw_cpu_ptr(&(pcp)) op val;                                    \
} while (0)
  • 예) pcp=__kmap_atomic_idx, val=1, op= +=
    • per-cpu int 데이터인 __kmap_atomic_idx += 1

 

kunmap_atomic() – 32bit

include/linux/highmem.h

/*
 * Prevent people trying to call kunmap_atomic() as if it were kunmap()
 * kunmap_atomic() should get the return value of kmap_atomic, not the page.
 */
#define kunmap_atomic(addr)                                     \
do {                                                            \
        BUILD_BUG_ON(__same_type((addr), struct page *));       \
        __kunmap_atomic(addr);                                  \
} while (0)

kmap_atomic()을 사용하여 fixmap 영역에 매핑되어 있는 highmem 영역을 해제한다.

 

__kunmap_atomic() – 32bit

arch/arm/mm/highmem.c

void __kunmap_atomic(void *kvaddr)
{
        unsigned long vaddr = (unsigned long) kvaddr & PAGE_MASK;
        int idx, type;

        if (kvaddr >= (void *)FIXADDR_START) {
                type = kmap_atomic_idx();
                idx = type + KM_TYPE_NR * smp_processor_id();

                if (cache_is_vivt())
                        __cpuc_flush_dcache_area((void *)vaddr, PAGE_SIZE);
#ifdef CONFIG_DEBUG_HIGHMEM
                BUG_ON(vaddr != __fix_to_virt(idx));
                set_fixmap_pte(idx, __pte(0));
#else
                (void) idx;  /* to kill a warning */
#endif
                kmap_atomic_idx_pop();
        } else if (vaddr >= PKMAP_ADDR(0) && vaddr < PKMAP_ADDR(LAST_PKMAP)) {
                /* this address was obtained through kmap_high_get() */
                kunmap_high(pte_page(pkmap_page_table[PKMAP_NR(vaddr)]));
        }
        pagefault_enable();
}
EXPORT_SYMBOL(__kunmap_atomic);

kmap_atomic()을 사용하여 fixmap 또는 kmap 영역에 매핑되어 있는 highmem 영역을 해제한다.

  • if (kvaddr >= (void *)FIXADDR_START) {
    • 주소가 fixmap 영역인 경우
  •  type = kmap_atomic_idx();
    • __kmap_atomic_idx per-cpu 데이터값에서 1을 뺀 인덱스 값
  • idx = type + KM_TYPE_NR * smp_processor_id();
    • KM_TYPE_NR=20
  • if (cache_is_vivt())
    • L1 d-cache 타입이 VIVT인 경우
    • rpi2:
      • L1 d-cache는 CACHEID_VIPT_NONALIASING
      • L1 i-cache는 CACHEID_VIPT_I_ALIASING
  • __cpuc_flush_dcache_area((void *)vaddr, PAGE_SIZE);
    • 해당 주소의 1 페이지 영역의 d-cache를 flush 한다.
  • kmap_atomic_idx_pop();
    • __kmap_atomic_idx per-cpu 데이터를 1 감소시킨다.
  • } else if (vaddr >= PKMAP_ADDR(0) && vaddr < PKMAP_ADDR(LAST_PKMAP)) {
    • 가상 주소가 pkmap 매핑 영역인 경우
  • kunmap_high(pte_page(pkmap_page_table[PKMAP_NR(vaddr)]));
    • kmap 영역에 매핑된 highmem page 주소를 매핑 해제한다.
  • pagefault_enable();
    • preemption을 enable 한다.

 

참고

Kmap(Pkmap)

특징

  • HIGHMEM 영역을 lowmem 영역 중 kmap address space에 매핑하여 사용 하기 위한 Kernel Mapping 방법
    • 아키텍처마다 kmap address space 위치와 크기가 다름
    • ARM: PAGE_OFFSET – 2M ~ PAGE_OFFSET 까지 2M 크기
    • HIGHMEM 영역을 user에서 접근할 때에는 당연히 항상 매핑을 하여 사용한다.
    • HIGHMEM 영역은 kernel에서 접근할 때 NORMAL이나 DMA(DMA32) 영역과 달리 kernel에서 1:1 direct mapping된 채로 운영되지 않는다. 즉 HIGHMEM 영역을 kernel에서 접근하려면 별도의 매핑을 통해서만 접근이 가능하다.
      • HIGHMEM 영역을 kernel에서 접근하려면 kmap, vmap, 또는 fixmap 등의 방식으로 매핑하여 사용할 수 있다.
  • Pkmap(Persistence Kernel Map)으로 명명됨
  • x86 32비트 시스템에서는 896M 이상의 메모리의 경우 direct access를 할 수 없어서 그 이상의 메모리를 access하기 위해서는 간접 매핑을 하여 사용할 수 있다.
    • 2가지 간접 매핑
      • 첫 번째 매핑은 2~4G 영역까지 간접 매핑
      • 두 번째 매핑은 64G 영역까지 간접 매핑 (PAE)
        • 실제 리눅스는 관련 매핑 데이터에대한 overhead가 크고 반드시 매핑 데이터는 직접 access가 가능한 ZONE_NORMAL 영역에 있어야 하므로 64G까지 매핑을 해야 하는 경우 ZONE_NORMAL 영역이 너무 작아지므로 최대 매핑을 리눅스 스스로 16G 까지로 제한을 할 수 밖에 없었다.
        • 매핑 데이터는 mem_map, bitmap, pte, … 등등이 있다.
        • 참고로 16G 매핑 시 mem_map의 크기는 16G / 4K * 44byte = 176M가 소요되고, 작은 pte의 경우에도 case에 따라 다르지만 worst case에서 16M가 소요된다.
  • ARM 32비트 시스템에서도 일정 크기 이상의 물리 메모리의 access에는 간접 매핑이 필요하다.
    • 4G를 초과하는 영역에 대해서는 하드웨어적인 제약으로 인해 LPAE를 지원하여 매핑한다.
  • HIGHMEM을 필요한 시간 만큼 매핑하여 사용하게 되면 제한된 매핑 영역과 그 제어로 인해 성능이 떨어진다. 따라서 최대한 HIGHMEM 영역을 적게 사용하는 것이 메모리 매핑으로 인한 overhead를 피할 수 있으며 수 기가 메모리가 필요한 경우 virtual address space가 큰 64 비트 아키텍처를 사용하는 것이 훨씬 유리하다.
  • kmap()과 kunmap()은 한쌍으로 동작하고 highmem 페이지를 매우 잠시간만 lowmem 영역에 매핑하여 사용했다가 다시 해제한다.
  • kmap() 함수를 사용 시 페이지를 체크하여 이미 lowmem 영역에 매핑되어 있는 경우 그냥 사용한다.
  • kmap() 함수는 sleep될 수 있으므로 인터럽트 context에서는 kmap_atomic() 함수를 사용해야 한다.
    • kmap_atomic() 함수는fixmap 매핑 영역을 사용한다.

 

ZONE_HIGHMEM 매핑

물리 메모리는 ZONE 별로 매핑하는 방법이 다른데 다음과 같다.

  • ZONE_DMA
    • x86 등에서 초창기 디바이스 장치들이 ISA 버스를 사용하면서 access할 수 있는 1M 이하의 물리 주소 영역으로 제한되었었다.  그 후 16M까지 확장되어 사용해 왔는데 이를 호환시키기 위해 사용하는 영역이다.
    • ARM에서는 아키텍처 마다 다른 사이즈를 지원한다.
      • rpi2: ZONE_DMA를 사용하지 않는다.
  • ZONE_DMA32
    • x86_64에서 32비트 디바이스 장치들이 access할 수 있는 영역이다.
  •  ZONE_NORMAL
    • 아래 그림과 같이 커널의 lowmem 영역(PAGE_OFFSET 부터 시작)에 1:1 영구 매핑하여 사용한다.
  • ZONE_HIGHMEM
    • 1:1 매핑이 불가능한 메모리 영역을 ZONE_HIGHMEM이라 하는데 이 영역은 lowmem 영역 중 pkmap area 또는 fixmap area를 사용해 HIGHMEM 영역의 4K 단위의 페이지를 이 영역에 필요한 시간 만큼 매핑하여 사용한다.
    • 영역이 제한되어 있기 때문에 필요한 만큼 사용한 후에는 매핑을 해제해야 한다.
      • 아키텍처마다 pkmap area와 fixmap area의 위치가 다르며 영역 크기 또한 다르다.
        • ARM에서의 pkmap area:
          • PAGE_OFFSET 바로 아래에 위치하며 2M 크기를 사용한다.
          • 2M 영역을 사용하므로 PTE(L2) 페이지 하나를 할당 받아 512개의 엔트리(pte_t)를 교체하는 것으로 매핑한다.
    • rpi2에서는 기본 설정에서 ZONE_HIGHMEM을 사용하지 않는다.
      • rpi에서는 PAGE_OFFSET가 0xC000_0000(user space=3G, kernel space=1G) 이었는데 rpi2에서는 HIGHMEM을 사용하지 않으려 PAGE_OFFSET를 0x8000_0000(user space=2G, kernel space=2G)으로 하였다.
      • rpi 같은 임베디드 application이 큰 가상주소를 요구하는 경우가 많지 않아 메모리가 1G 이상의 시스템에서 PAGE_OFFSET를 0x8000_0000으로 사용하는 경우가 있다. 만일 rpi2의 경우도 rpi같이 최대 메모리가 512M 였으면 PAGE_OFFSET를 0xC000_0000으로 설정하였을 것이다.
  • ZONE_MOVEABLE
    • 이 영역은 NUMA 시스템에서 페이지 단편화의 효율을 위해 특별히 디자인된 영역이고 아울러 hotplug 메모리 용도로 사용할 수 있는 영역이다.

아래 그림과 같이 좌측 물리 메모리 주소가 우측 가상 메모리에 매핑된 사례 ZONE_NORMAL과 ZONE_HIGHMEM을 보여준다.

pkmap-2

아래 그림은 HIGHMEM 영역의 어느 한 페이지를 매핑한 경우의 연관된 흐름을 보여준다.

  • pkmap_page_table[]은 실제 매핑용 L2 페이지 테이블이다.
  • page_address_htable[]을 사용하여 128개의 해쉬 방법을 사용한다.
    • 해시 슬롯에는 리스트 구조로 페이지를 추가 삭제하여 관리한다.

kmap-1a

 

매핑과 해제

 

kmap()

arch/arm/mm/highmem.c

void *kmap(struct page *page)
{
        might_sleep();
        if (!PageHighMem(page))
                return page_address(page);
        return kmap_high(page);
}
EXPORT_SYMBOL(kmap);

page 주소를 가상 주소에 매핑하고 해당 가상 주소를 리턴한다. 이미 매핑된 경우는 해당 가상 주소를 리턴한다.

  • might_sleep();
    • Preemption Point가 요구되는 커널에서 필요 시 task를 양보한다.
  • if (!PageHighMem(page))
    • page 주소가 HIGHMEM 영역이 아니면
  • return page_address(page);
    • page에 해당하는 가상 주소를 리턴한다.
  • return kmap_high(page);
    • page 정보를 사용하여 HIGHMEM 영역의 물리주소를 kmap 매핑 주소 영역 중 하나의 페이지에 매핑한 후 그 가상 주소를 리턴한다.

 

 

PageHighMem()
#define PageHighMem(__p) is_highmem(page_zone(__p))

page가 highmem 영역에 있는지 여부를 알아낸다.

  • is_highmeme()
    • 해당 zone이 highmem 영역인지 여부를 알아낸다.

 

page_zone()
static inline struct zone *page_zone(const struct page *page)
{
        return &NODE_DATA(page_to_nid(page))->node_zones[page_zonenum(page)];
}

page에 해당하는 zone 구조체 포인터를 리턴한다.

  •  NODE_DATA()
    • #define NODE_DATA(nid) (&contig_page_data)
      • UMA 시스템에서는 1개의 노드를 사용하며 이 때에는 배열로 관리할 필요가 없어서 &contig_page_data 전역 구조체 변수를 리턴한다.
  • page_to_nid()
    • 페이지 번호로 node id를 알아온다.
  • page_zonenum()
    • page 멤버변수 flags에서 zone값을 알아온다.

 

page_zonenum()
static inline enum zone_type page_zonenum(const struct page *page)
{
        return (page->flags >> ZONES_PGSHIFT) & ZONES_MASK;
}

page 멤버변수 flags에서 zone 값을 알아온다.

  • #define ZONES_PGSHIFT (ZONES_PGOFF * (ZONES_WIDTH != 0))
    • #define ZONES_PGOFF             (NODES_PGOFF – ZONES_WIDTH)
      • #define NODES_PGOFF             (SECTIONS_PGOFF – NODES_WIDTH)
        •  #define SECTIONS_PGOFF          ((sizeof(unsigned long)*8) – SECTIONS_WIDTH)
          • SECTIONS_WIDTH는 sparse 메모리가 아닌 경우 항상 0
          • rpi2는 SPARSEMEM을 사용하지 않아 섹션 비트가 필요 없다 따라서 SECTIONS_PGOFF=32
        • NODES_WIDTH
          • 32비트 정수로 22개의 페이지용 플래그 + 섹션비트, 노드비트, 존비트를 표현할 수 있으면 CONFIG_NODES_SHIFT를 리턴
          • rpi2: 단일 노드이므로 0
        • rpi2는 단일 노드를 사용하므로 NODES_PGOFF=32
      • ZONES_WIDTH
        • 존 수에 따라 달라지는데 1개 존은 0, 2개 존은 1, 3~4개 존은 2, 그외는 에러
        • rpi2: 2개 존을 사용하므로 1
      • rpi2의 ZONES_PGOFF=31
    • rpi2의 ZONES_PGSHIFT=31
  • page->flags 값에서 zone 비트만 우측으로 쉬프트하고 마스크하여 추출한다.

page-1

 

is_highmem()
/**
 * is_highmem - helper function to quickly check if a struct zone is a 
 *              highmem zone or not.  This is an attempt to keep references
 *              to ZONE_{DMA/NORMAL/HIGHMEM/etc} in general code to a minimum.
 * @zone - pointer to struct zone variable
 */
static inline int is_highmem(struct zone *zone)
{
#ifdef CONFIG_HIGHMEM
        int zone_off = (char *)zone - (char *)zone->zone_pgdat->node_zones;
        return zone_off == ZONE_HIGHMEM * sizeof(*zone) ||
               (zone_off == ZONE_MOVABLE * sizeof(*zone) &&
                zone_movable_is_highmem());
#else
        return 0;
#endif
}

해당 zone이 highmem 영역인지 여부를 알아낸다.

  • zone_off
    • 현재 요청한 zone 구조체 주소에서 첫 zone 구조체 주소를 뺀 offset 주소이다.
  • zone_off 사이즈로 HIGHMEM 영역에 있는지 판단하여 리턴한다.

 

page_address()

mm/highmem.c

/**
 * page_address - get the mapped virtual address of a page
 * @page: &struct page to get the virtual address of
 *
 * Returns the page's virtual address.
 */
void *page_address(const struct page *page)
{
        unsigned long flags;
        void *ret;
        struct page_address_slot *pas;

        if (!PageHighMem(page))
                return lowmem_page_address(page);

        pas = page_slot(page);
        ret = NULL;
        spin_lock_irqsave(&pas->lock, flags);
        if (!list_empty(&pas->lh)) {
                struct page_address_map *pam;

                list_for_each_entry(pam, &pas->lh, list) {
                        if (pam->page == page) {
                                ret = pam->virtual;
                                goto done;
                        }
                }   
        }   
done:
        spin_unlock_irqrestore(&pas->lock, flags);
        return ret;
}

EXPORT_SYMBOL(page_address);

인수로 지정된 page 구조체 주소로 기존 해시 테이블을 검색하여 이미 매핑이 되어 있는 가상 주소 값을 찾아서 리턴한다.

  • return lowmem_page_address(page);
    • page에 인수로 조회하여 lowmem 영역에 대한 가상 주소를 리턴한다.
  • pas = page_slot(page);
    • 해시 테이블에서 해당 hash(slot)로 page_address_slot 구조체 포인터를 알아온다.
  • spin_lock_irqsave(&pas->lock, flags);
    • 리스트 엔트리를 검색하기 위해 spin lock을 한다.
  • if (!list_empty(&pas->lh)) {
    • 리스트가 비어있지 않으면
  • list_for_each_entry(pam, &pas->lh, list) {
    • 리스트 엔트리를 모두 조회하여 하나씩 pam(page_address_map 구조체 포인터)에 대입한다.
    • if (pam->page == page) {
      • 엔트리의 페이지와 인수로 요청한 페이지가 동일하면
    • ret = pam->virtual;
      • 해당 엔트리의 virtual 값을 리턴한다.
  • spin_unlock_irqrestore(&pas->lock, flags);
    • 리스트 엔트리의 조회가 모두 끝났으므로 spin unlock을 수행한다.

 

lowmem_page_address()

include/linux/mm.h

static __always_inline void *lowmem_page_address(const struct page *page)
{
        return __va(PFN_PHYS(page_to_pfn(page)));
}

lowmem 영역의 page 주소로 가상 주소값을 구한다.

  •  page_to_pfn()
    • page 주소로 pfn 값을 얻어온다.
    • 구현 루틴은 메모리 모델에 따라 다음 4가지 종류 중 하나를 사용한다.
      • CONFIG_FLATMEM, CONFIG_DISCONTIGMEM, CONFIG_SPARSEMEM_VMEMMAP, CONFIG_SPARSEMEM
      • CONFIG_FLATMEM을 사용하는 매크로 함수이다.
#define __page_to_pfn(page)     ((unsigned long)((page) - mem_map) + ARCH_PFN_OFFSET
  • PFN_PHYS()
    • pfn  값으로 물리주소를 구한다.
#define PFN_PHYS(x)     ((phys_addr_t)(x) << PAGE_SHIFT)
  • __va()
    • lowmem영역의 물리주소를 가상주소 값으로 변환한다.
#define __va(x)                 ((void *)__phys_to_virt((phys_addr_t)(x)))

 

 

kmap_high()

mm/highmem.c

/**
 * kmap_high - map a highmem page into memory
 * @page: &struct page to map
 *
 * Returns the page's virtual memory address.
 *
 * We cannot call this from interrupts, as it may block.
 */
void *kmap_high(struct page *page)
{
        unsigned long vaddr;

        /*
         * For highmem pages, we can't trust "virtual" until
         * after we have the lock.
         */
        lock_kmap();
        vaddr = (unsigned long)page_address(page);
        if (!vaddr)
                vaddr = map_new_virtual(page);
        pkmap_count[PKMAP_NR(vaddr)]++;
        BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
        unlock_kmap();
        return (void*) vaddr;
}

EXPORT_SYMBOL(kmap_high);

page 구조체 정보를 사용하여 해당 페이지 프레임(4K)을 가상 주소에 매핑하고 그 주소를 리턴한다.

  • lock_kmap()
    • 전역 kmap_lock에 대해 spin lock을 사용하여 동시에 kmap() 및 kunmap() 함수를 이용하는 경우 순서대로 sirialization 한다.
  • vaddr = (unsigned long)page_address(page);
    • page 주소로 이미 매핑된 가상 주소를 알아온다.
  • if (!vaddr)
    • 매핑된 주소가 없는 새로운 페이지인 경우
  • vaddr = map_new_virtual(page);
    • 새로운 virtual 주소를 할당한다.
  • pkmap_count[PKMAP_NR(vaddr)]++
    • 해당 주소의 pkmap 엔트리에 대한 pkmap 카운터를 증가시킨다.
  • unlock_kmap();
    • 전역 kmap_lock에 대해 spin unlock을 수행한다.

 

map_new_virtual()

mm/highmem.c

static inline unsigned long map_new_virtual(struct page *page)
{
        unsigned long vaddr;
        int count;
        unsigned int last_pkmap_nr;
        unsigned int color = get_pkmap_color(page);

start:
        count = get_pkmap_entries_count(color);
        /* Find an empty entry */
        for (;;) {
                last_pkmap_nr = get_next_pkmap_nr(color);
                if (no_more_pkmaps(last_pkmap_nr, color)) {
                        flush_all_zero_pkmaps();
                        count = get_pkmap_entries_count(color);
                }
                if (!pkmap_count[last_pkmap_nr])
                        break;  /* Found a usable entry */
                if (--count)
                        continue;

                /*
                 * Sleep for somebody else to unmap their entries
                 */
                {
                        DECLARE_WAITQUEUE(wait, current);
                        wait_queue_head_t *pkmap_map_wait =
                                get_pkmap_wait_queue_head(color);

                        __set_current_state(TASK_UNINTERRUPTIBLE);
                        add_wait_queue(pkmap_map_wait, &wait);
                        unlock_kmap();
                        schedule();
                        remove_wait_queue(pkmap_map_wait, &wait);
                        lock_kmap();

                        /* Somebody else might have mapped it while we slept */
                        if (page_address(page))
                                return (unsigned long)page_address(page);

                        /* Re-start */
                        goto start;
                }
        }
        vaddr = PKMAP_ADDR(last_pkmap_nr);
        set_pte_at(&init_mm, vaddr,
                   &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));

        pkmap_count[last_pkmap_nr] = 1;
        set_page_address(page, (void *)vaddr);

        return vaddr;
}
  • unsigned int color = get_pkmap_color(page);
    • highmem 영역의 메모리에 data cache aliasing이 필요할 때 리턴되는 color 값인데 특정 아키텍처(mips & xtensa)에서만 사용하고 ARM에서는 아직 사용하지 않아 0으로 리턴한다.
    • 참고: mm/highmem: make kmap cache coloring aware
  • count = get_pkmap_entries_count(color);
    • ARM에서는 highmem을 위한 최대 매핑 엔트리 수를 리턴한다.
      • LAST_PKMAP(512)
  • last_pkmap_nr = get_next_pkmap_nr(color);
    • 마지막에 사용된 엔트리 번호에 1을 더한 번호를 리턴한다.
      • 0~511 까지를 반복한다.
  • if (no_more_pkmaps(last_pkmap_nr, color)) {
    • last_pkmap_nr = 0이면?
  • flush_all_zero_pkmaps();
    • 사용하지 않는 pkmap 엔트리를 해제한다.
  • count = get_pkmap_entries_count(color);
    • ARM에서는 항상 LAST_PKMAP(512)을 리턴한다.
  • if (!pkmap_count[last_pkmap_nr])
    • 빈 엔트리를 찾은 경우(엔트리 카운터가 0)
  • DECLARE_WAITQUEUE(wait, current);
    • 현재 태스크로 wait 항목을 만든다.
  • wait_queue_head_t *pkmap_map_wait = get_pkmap_wait_queue_head(color);
    • wait_queue를 알아온다.
  • __set_current_state(TASK_UNINTERRUPTIBLE);
    • 현재 태스크를 uninterruptible로 바꾼다.
  • add_wait_queue(pkmap_map_wait, &wait);
    • pkmap_map_wait 큐에 wait 엔트리를 추가한다.
  • unlock_kmap();
    • sleep() 하기 위해 spin unlock 한다.
  • schedule();
    • 리스케쥴 한다.
  • remove_wait_queue(pkmap_map_wait, &wait);
    • pkmap_map_wait 큐에서 현재 wait 항목을 제거한다.
  • lock_kmap();
    • 다시 spin lock 한다.
  • if (page_address(page))
    • 누군가(다른 태스크) 매핑을 한 경우 빠져나간다.
  • goto start;
    • 다시 처음 부터 시도한다.
  • vaddr = PKMAP_ADDR(last_pkmap_nr);
    • 마지막 사용한 엔트리 번호로 가상 주소를 알아온다.
#define PKMAP_ADDR(nr)          (PKMAP_BASE + ((nr) << PAGE_SHIFT))

 

  • set_pte_at(&init_mm, vaddr, &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));
    • pte 엔트리를 설정한다.
  • pkmap_count[last_pkmap_nr] = 1;
    • 사용 카운터를 1로 만든다.
  • set_page_address(page, (void *)vaddr);
    • page의 가상 주소를 대입한다.

 

flush_all_zero_pkmaps()

mm/highmem.c

static void flush_all_zero_pkmaps(void)
{
        int i;
        int need_flush = 0;

        flush_cache_kmaps();

        for (i = 0; i < LAST_PKMAP; i++) {
                struct page *page;

                /*
                 * zero means we don't have anything to do,
                 * >1 means that it is still in use. Only
                 * a count of 1 means that it is free but
                 * needs to be unmapped
                 */
                if (pkmap_count[i] != 1)
                        continue;
                pkmap_count[i] = 0;

                /* sanity check */
                BUG_ON(pte_none(pkmap_page_table[i]));

                /*
                 * Don't need an atomic fetch-and-clear op here;
                 * no-one has the page mapped, and cannot get at
                 * its virtual address (and hence PTE) without first
                 * getting the kmap_lock (which is held here).
                 * So no dangers, even with speculative execution.
                 */
                page = pte_page(pkmap_page_table[i]);
                pte_clear(&init_mm, PKMAP_ADDR(i), &pkmap_page_table[i]);

                set_page_address(page, NULL);
                need_flush = 1;
        }
        if (need_flush)
                flush_tlb_kernel_range(PKMAP_ADDR(0), PKMAP_ADDR(LAST_PKMAP));
}
  • flush_cache_maps()
#define flush_cache_kmaps() \
        do { \
                if (cache_is_vivt()) \
                        flush_cache_all(); \
        } while (0)
  • for (i = 0; i < LAST_PKMAP; i++) {
    • 전체 PKMAP 엔트리(512) 만큼 루프
  • if (pkmap_count[i] != 1)
    • 사용 카운터가 1을 초과한 경우는 이미 사용중이라는 것을 의미한다.
    • 1인 경우는 free 한 상태인 경우이다.
  • pkmap_count[i] = 0;
    • 일단 카운터를 0으로 설정한다.
  • page = pte_page(pkmap_page_table[i])
    • 해당 루프 카운터에 매치된 pte 엔트리 값으로 page를 알아온다.
  • pte_clear(&init_mm, PKMAP_ADDR(i), &pkmap_page_table[i]);
    • pte 엔트리 클리어
  • set_page_address(page, NULL);
    • PKMAP 엔트리에 매핑된 항목의 virtual 값에 null을 설정
  • need_flush = 1;
    • 한 번이라도 pte 엔트리가 바뀌면 1로 설정한다.
  • flush_tlb_kernel_range(PKMAP_ADDR(0), PKMAP_ADDR(LAST_PKMAP));
    • PKMAP 2M 영역에 대해 tlb flush를 수행한다.

 

set_page_address()

mm/highmem.c

/**
 * set_page_address - set a page's virtual address
 * @page: &struct page to set
 * @virtual: virtual address to use
 */
void set_page_address(struct page *page, void *virtual)
{
        unsigned long flags;
        struct page_address_slot *pas;
        struct page_address_map *pam;

        BUG_ON(!PageHighMem(page));

        pas = page_slot(page);
        if (virtual) {          /* Add */
                pam = &page_address_maps[PKMAP_NR((unsigned long)virtual)];
                pam->page = page;
                pam->virtual = virtual;

                spin_lock_irqsave(&pas->lock, flags);
                list_add_tail(&pam->list, &pas->lh);
                spin_unlock_irqrestore(&pas->lock, flags);
        } else {                /* Remove */
                spin_lock_irqsave(&pas->lock, flags);
                list_for_each_entry(pam, &pas->lh, list) {
                        if (pam->page == page) {
                                list_del(&pam->list);
                                spin_unlock_irqrestore(&pas->lock, flags);
                                goto done;
                        }
                }
                spin_unlock_irqrestore(&pas->lock, flags);
        }
done:
        return;
}

PKMAP 매핑 hash slot에서 page를 검색하여 발견되면 page->virtual 값을 설정한다.

  • pas = page_slot(page);
    • page 주소로 hash slot 정보를 알아온다.
  • if (virtual) {          /* Add */
    • 매핑 추가 명령이 요청된 경우
  • pam = &page_address_maps[PKMAP_NR((unsigned long)virtual)];
    • PKMAP 매핑 엔트리 번호로 pam을 알아온다.
  • pam->page = page;
    • page 구조체 주소를 기록한다.
  • pam->virtual = virtual;
    • 가상 주소를 기록한다.
  • list_add_tail(&pam->list, &pas->lh);
    • pas->lh에 pam->list를 추가한다.
    • 즉 해당 hash slot 관리 배열의 리스트에 page 매핑 구조체를 등록한다.
  • } else { /* Remove */
    • 매핑 삭제 명령이 요청된 경우
  • list_for_each_entry(pam, &pas->lh, list) {
    • 해당 hash slot 관리 배열의 리스트를 모두 조회한다.
  • if (pam->page == page) {
    • 해당 페이지가 발견되면
  • list_del(&pam->list);
    • 해당 매핑을 삭제한다.

 

kunmap()

kmap 영역에 매핑된 highmem page 주소를 매핑 해제한다. highmem page 주소가 아닌 경우는 그냥 리턴한다.

arch/arm/mm/highmem.c

void kunmap(struct page *page)
{
        BUG_ON(in_interrupt());
        if (!PageHighMem(page))
                return;
        kunmap_high(page);
}
EXPORT_SYMBOL(kunmap);
  • if (!PageHighMem(page))
    • page 주소가 HIGHMEM 영역이 아니면 그냥 리턴한다.
  • kunmap_high(page);
    • 해당 page를 kmap 매핑 영역에서 제거한다.

 

kunmap_high()

kmap 영역에 매핑된 highmem page 주소를 매핑 해제한다.

mm/highmem.c

/**
 * kunmap_high - unmap a highmem page into memory
 * @page: &struct page to unmap
 *
 * If ARCH_NEEDS_KMAP_HIGH_GET is not defined then this may be called
 * only from user context.
 */
void kunmap_high(struct page *page)
{
        unsigned long vaddr;
        unsigned long nr;
        unsigned long flags;
        int need_wakeup;
        unsigned int color = get_pkmap_color(page);
        wait_queue_head_t *pkmap_map_wait;

        lock_kmap_any(flags);
        vaddr = (unsigned long)page_address(page);
        BUG_ON(!vaddr);
        nr = PKMAP_NR(vaddr);

        /*
         * A count must never go down to zero
         * without a TLB flush!
         */
        need_wakeup = 0;
        switch (--pkmap_count[nr]) {
        case 0:
                BUG();
        case 1:
                /*
                 * Avoid an unnecessary wake_up() function call.
                 * The common case is pkmap_count[] == 1, but
                 * no waiters.
                 * The tasks queued in the wait-queue are guarded
                 * by both the lock in the wait-queue-head and by
                 * the kmap_lock.  As the kmap_lock is held here,
                 * no need for the wait-queue-head's lock.  Simply
                 * test if the queue is empty.
                 */
                pkmap_map_wait = get_pkmap_wait_queue_head(color);
                need_wakeup = waitqueue_active(pkmap_map_wait);
        }
        unlock_kmap_any(flags);

        /* do wake-up, if needed, race-free outside of the spin lock */
        if (need_wakeup)
                wake_up(pkmap_map_wait);
}

EXPORT_SYMBOL(kunmap_high);
  • unsigned int color = get_pkmap_color(page);
    • ARM에서는 아직 highmem 영역에 d-cache aliasing을 사용하지 않아 0으로 리턴한다.
  • lock_kmap_any(flags);
    • kmap 전역 spin lock을 사용하여 동시에 kmap() 및 kunmap() 함수를 이용하는 것을 순서대로 sirialization 한다.
  • vaddr = (unsigned long)page_address(page);
    • page 주소로 이미 매핑된 가상 주소를 알아온다.
  • nr = PKMAP_NR(vaddr);
    • 가상 주소에 해당하는 슬롯(0~127) 번호를 알아온다.
  • switch (–pkmap_count[nr]) {
    • 사용 카운터를 감소시킨다.
  • case 1:
    • 감소시킨 값이 1인 경우
  • pkmap_map_wait = get_pkmap_wait_queue_head(color);
    • wait_queue를 준비한다.
  • need_wakeup = waitqueue_active(pkmap_map_wait);
    • wait_queue에 태스크가 있으면 true
  • unlock_kmap_any(flags);
    • kmap 전역 spin unlock을 수행한다.
  • wake_up(pkmap_map_wait);
    • 대기 큐에 존재하는 하나의 태스크를 wake up 하게 한다.

 

 

get_pkmap_wait_queue_head()

mm/highmem.c

/*
 * Get head of a wait queue for PKMAP entries of the given color.
 * Wait queues for different mapping colors should be independent to avoid
 * unnecessary wakeups caused by freeing of slots of other colors.
 */
static inline wait_queue_head_t *get_pkmap_wait_queue_head(unsigned int color)
{
        static DECLARE_WAIT_QUEUE_HEAD(pkmap_map_wait);

        return &pkmap_map_wait;
}
  • ARM 아키텍처는 아직 HIGHMEM 영역에 대해 d-cache에 대한 color 값을 사용하지 않아 인수로 사용되는 color 값을 사용하지 않는다.
  • wait_queue를 static inline 할당하고 리턴한다.

 

 

waitqueue_active()

include/linux/wait.h

static inline int waitqueue_active(wait_queue_head_t *q)
{
        return !list_empty(&q->task_list);
}
  • wait_queue에 태스크가 있는 경우 true, 없으면 false를 리턴한다.

 

참고