Sparse Memory

<kernel v5.0>

Sparse memory 모델은 섹션 메모리 단위로 mem_section 구조체를 통해 다음과 같은 자료를 관리한다.

  • mem_map
  • usemap (페이지블럭)

 

섹션

Sparse memory 모델에서 섹션은 메모리의 online/offlline(hotplug memory)을 관리하는 최소 메모리 크기 단위이다. 전체 메모리를 섹션들로 나누어 사용하는데 적절한 섹션 사이즈로 나누어 사용하는데 아키텍처 마다 다르다.

  • 보통 섹션 크기는 수십MB~수GB를 사용한다.
    • arm64: default 값으로 1G
  • 매핑 테이블에서 사용하는 섹션(1M, 16M(수퍼섹션)이 아니므로 유의한다.

 

섹션 배열 2 가지 관리 방법

섹션에서 mem_map을 관리하는 다음 두 가지 방법을 알아본다.

  • static
    • 컴파일 타임에 1단계 mem_section[] 배열을 결정하여 사용하는 방법이다.
    • 주로 32bit 시스템에서 섹션 수가 적을 때 사용한다.
  • extream
    • 런타임에 1단계 **mem_section[] 포인터 배열을 할당하고, 2 단계는 필요 시마다 mem_section[] 배열을 할당하여 사용하는 방법으로 메모리 낭비를 막는 방법이다.
    • 주로 64bit 시스템에서 섹션 수가 많을 때 사용한다.

 

CONFIG_SPARSEMEM_STATIC

32bit ARM에서 Sparse Memory를 사용하는 Realview-PBX 보드가 섹션당 256MB 크기로 구성된 사례를 사용한다.

  • Realview-PBX
    • 3개의 메모리
      • 256MB @ 0x00000000 -> PAGE_OFFSET
      • 512MB @ 0x20000000 -> PAGE_OFFSET + 0x10000000
      • 256MB @ 0x80000000 -> PAGE_OFFSET + 0x30000000
    • MAX_PHYSMEM_BITS=32 (4G 메모리 크기)
    • SECTION_SIZE_BITS=28 (256MB 섹션 크기)
    • PFN_SECTION_SHIFT=(SECTION_SIZE_BITS – PAGE_SHIFT)=16
    • SECTIONS_PER_ROOT=1
    • SECTIONS_SHIFT=(MAX_PHYSMEM_BITS – SECTION_SIZE_BITS)=4
    • NR_MEM_SECTIONS=2^SECTIONS_SHIFT=16
    • PAGES_PER_SECTION=2^PFN_SECTION_SHIFT=64K
    • PAGE_SECTION_MASK=(~(PAGES_PER_SECTION-1))=0xffff_0000

 

CONFIG_SPARSEMEM_EXTREME

64bit ARM에서 Sparse Memory를 사용하는 경우의 사례로 1GB 크기로 구성된 사례를 사용한다.

  • arm64
    • 2개의 메모리
      • 2GB @ 0x0_8000_0000
      • 2GB @ 0x8_0000_0000
  • MAX_PHYSMEM_BITS=48 (256 TB 메모리 크기)
  • SECTION_SIZE_BITS=30 (1GB 섹션 크기)
  • PFN_SECTION_SHIFT=(SECTION_SIZE_BITS – PAGE_SHIFT)=18
  • SECTIONS_PER_ROOT=(PAGE_SIZE / sizeof (struct mem_section))=256
  • SECTIONS_SHIFT=(MAX_PHYSMEM_BITS – SECTION_SIZE_BITS)=18
  • NR_MEM_SECTIONS=2^SECTIONS_SHIFT=256K
  • PAGES_PER_SECTION=2^PFN_SECTION_SHIFT=256K
  • PAGE_SECTION_MASK=(~(PAGES_PER_SECTION-1))=0xffff_ffff_fffc_0000

 

usemap

usemap은 메모리 회수 매커니즘 중 하나인 compaction에서 전체 메모리를 스캔할 때 사용되는 페이지 블럭 당 4비트로 구성된다.

  • compaction에서 빠른 스캔을 위해 블럭 단위(arm64 디폴트=2M)로 묶어 사용한다.
  • 페이지 블럭 당 사용되는 4비트는 3비트의 mobility(migratype) 속성과 1 비트의 skip 비트로 구성된다.

 

VMEMMAP

32bit arm 에서는 사용하지 않는다. CONFIG_SPARSEMEM_VMEMMAP 커널 옵션을 사용할 수 있는 arm64를 포함하는 일부 64비트 아키텍처에서 Flat Memory 모델처럼 빠르게 운용할 수 있다.

  • 64비트 시스템에서 vmemmap 매핑 공간이 별도로 구성되어 있어 그 영역에 section_mem_map으로 구성된 mem_map들을 매핑한다.
  • vmemmap을 사용하는 경우 노드별 메모리에 분산되어 있는 페이지 descriptor로 serial하게 접근할 수 있어 page_to_pfn() 또는 pfn_to_page() 함수의 성능을 빠르게 얻을 수 있다.

 

다음 그림은 4K 페이지 x 3 레벨의 페이지 변환을 사용하는 arm64 시스템에서 vmemmap이 구성되는 사례를 보여준다.

vmemmap-1b

 


메모리 활성화

arm_memory_present() – ARM32

arch/arm/mm/init.c

static void __init arm_memory_present(void)
{
        struct memblock_region *reg;

        for_each_memblock(memory, reg)
                memory_present(0, memblock_region_memory_base_pfn(reg),
                               memblock_region_memory_end_pfn(reg));
}

memory memblock 들에 대해 memory_present() 함수를 호출하여 mem_section[] 매핑 배열을 초기화한다.

 

arm64_memory_present() – ARM64

arch/arm64/mm/init.c

static void __init arm64_memory_present(void)
{
        struct memblock_region *reg;

        for_each_memblock(memory, reg) {
                int nid = memblock_get_region_node(reg);

                memory_present(nid, memblock_region_memory_base_pfn(reg),
                                memblock_region_memory_end_pfn(reg));
        }
}

sparse 메모리 모델에서 memory memblock 들에 대해 memory_present() 함수를 호출하여 mem_section[] 배열을 초기화한다.

 

memory_present()

mm/sparse.c

/* Record a memory area against a node. */
void __init memory_present(int nid, unsigned long start, unsigned long end)
{
        unsigned long pfn;

#ifdef CONFIG_SPARSEMEM_EXTREME
        if (unlikely(!mem_section)) {
                unsigned long size, align;

                size = sizeof(struct mem_section*) * NR_SECTION_ROOTS;
                align = 1 << (INTERNODE_CACHE_SHIFT);
                mem_section = memblock_alloc(size, align);
        }
#endif

        start &= PAGE_SECTION_MASK;
        mminit_validate_memmodel_limits(&start, &end);
        for (pfn = start; pfn < end; pfn += PAGES_PER_SECTION) {
                unsigned long section = pfn_to_section_nr(pfn);
                struct mem_section *ms;

                sparse_index_init(section, nid);
                set_section_nid(section, nid);

                ms = __nr_to_section(section);
                if (!ms->section_mem_map) {
                        ms->section_mem_map = sparse_encode_early_nid(nid) |
                                                        SECTION_IS_ONLINE;
                        section_mark_present(ms);
                }
        }
}

memory_present() 함수는 CONFIG_HAVE_MEMORY_PRESENT 커널 옵션을 사용하는 경우에만 동작하며 각 섹션에 노드 id를 기록하는데, 자세한 내용은 코드를 보고 알아보기로 한다.

  • 코드 라인 6~14에서 CONFIG_SPARSEMEM_EXTREME 커널 옵션을 사용하는 경우 처음 mem_section이 초기화되지 않은 경우 mem_section[] 배열을 생성한다.
    • arm64 예) 4K 페이지, sizeof(struct mem_section)=16
      • NR_SECTION_ROOTS=1024
  • 코드 라인 16~17에서 요청한 범위의 시작 pfn을 섹션 단위로 내림 정렬한 주소로 변환한 후 해당 범위의 pfn이 실제로 지정할 수 있는 물리 메모리 범위에 포함되는지 검증하고 초과 시 그 주소를 강제로 제한한다.
  • 코드 라인 18~19에서 시작 pfn부터 섹션 단위로 증가시키며 이 값으로 section 번호를 구한다.
  • 코드 라인 22에서 CONFIG_SPARSEMEM_EXTREME 커널 옵션을 사용하는 경우에만 동작하며, 이 함수에서 해당 섹션에 대한 mem_section[]을 동적으로(dynamic) 할당받는다. CONFIG_SPARSEMEM_STATIC 커널 옵션을 사용하는 경우에는 이미 정적(static) 배열이 준비되어 있으므로 아무런 동작도 요구되지 않는다.
  • 코드 라인 23에서 NODE_NOT_IN_PAGE_FLAGS가 정의된 경우 별도의 전역 section_to_node_table[] 배열에 해당 섹션을 인덱스로 해당 노드 id를 가리키게 한다.
    • NODE_NOT_IN_PAGE_FLAGS 커널 옵션은 page 구조체의 flags 필드에 노드 번호를 저장할 비트가 부족한 32비트 아키텍처에서 사용되는 옵션이다.
  • 코드 라인 25~30에서 해당 섹션의 mem_section 구조체내의 section_mem_map에 노드 id와 online 및 present 플래그를 설정한다.

 

아래 그림은 memory_present() 함수가 호출되는 과정에서 mem_section[] 배열이 할당되는 것을 나타낸다.

  • 붉은 색 박스는 sparse_index_alloc() 함수에 의해 dynamic 하게 메모리 할당을 받는 것을 의미한다.
  • 푸른 색 박스는 컴파일 타임에 static하게 배열이 할당됨을 의미한다.

memory_present-1

 

mminit_validate_memmodel_limits()

mm/sparse.c

void __meminit mminit_validate_memmodel_limits(unsigned long *start_pfn,
                                                unsigned long *end_pfn)
{
        unsigned long max_sparsemem_pfn = 1UL << (MAX_PHYSMEM_BITS-PAGE_SHIFT);

        /*
         * Sanity checks - do not allow an architecture to pass
         * in larger pfns than the maximum scope of sparsemem:
         */
        if (*start_pfn > max_sparsemem_pfn) {
                mminit_dprintk(MMINIT_WARNING, "pfnvalidation",
                        "Start of range %lu -> %lu exceeds SPARSEMEM max %lu\n",
                        *start_pfn, *end_pfn, max_sparsemem_pfn);
                WARN_ON_ONCE(1);
                *start_pfn = max_sparsemem_pfn;
                *end_pfn = max_sparsemem_pfn;
        } else if (*end_pfn > max_sparsemem_pfn) {
                mminit_dprintk(MMINIT_WARNING, "pfnvalidation",
                        "End of range %lu -> %lu exceeds SPARSEMEM max %lu\n",
                        *start_pfn, *end_pfn, max_sparsemem_pfn);
                WARN_ON_ONCE(1);
                *end_pfn = max_sparsemem_pfn;
        }   
}

인자로 사용된 시작 pfn, 끝 pfn 값이 물리 메모리 주소 최대 pfn 값을 초과하지 않도록 제한한다.

  • 코드 라인 10~16에서 시작 pfn이 max_sparsemem_pfn보다 크면 경고를 출력하고, start_pfn과 end_pfn에 max_sparsemem_pfn을 설정한다.
  • 코드 라인 17~23에서 끝 pfn이 max_sparsemem_pfn보다 크면 경고를 출력하고, end_pfn에 max_sparsemem_pfn을 설정한다.

 

섹션 인덱스 초기화

sparse_index_init()

mm/sparse.c

#ifdef CONFIG_SPARSEMEM_EXTREME
static int __meminit sparse_index_init(unsigned long section_nr, int nid)
{
        unsigned long root = SECTION_NR_TO_ROOT(section_nr);
        struct mem_section *section;

        if (mem_section[root])
                return -EEXIST;

        section = sparse_index_alloc(nid);
        if (!section)
                return -ENOMEM;

        mem_section[root] = section;

        return 0;
}
#endif

CONFIG_SPARSEMEM_EXTREME 커널 옵션을 사용하는 경우 dynamic하게 mem_section 테이블을 할당 받아 구성한다.

  • 코드 라인 4에서 섹션 번호로 루트 번호를 구한다.
  • 코드 라인 7~8에서 해당 루트 인덱스의 루트 섹션에 값이 존재하는 경우 이미 mem_section[] 테이블이 구성되었으므로 함수를 빠져나간다.
  • 코드 라인 10~12에서 해당 노드에서 2 단계용 섹션 테이블을 할당받아 구성한다. 핫플러그 메모리를 위해 각 mem_section[] 테이블은 해당 노드에 위치해야 한다.
  • 코드 라인 14에서 루트 번호에 해당하는 1단계 mem_section[] 포인터 배열에 새로 할당받은 2 단계 mem_section[] 테이블의 시작 주소를 설정한다.

 

sparse_index_alloc()

mm/sparse.c

#ifdef CONFIG_SPARSEMEM_EXTREME
static struct mem_section noinline __init_refok *sparse_index_alloc(int nid)
{
        struct mem_section *section = NULL;
        unsigned long array_size = SECTIONS_PER_ROOT *
                                   sizeof(struct mem_section);

        if (slab_is_available()) {
                if (node_state(nid, N_HIGH_MEMORY))
                        section = kzalloc_node(array_size, GFP_KERNEL, nid);
                else
                        section = kzalloc(array_size, GFP_KERNEL);
        } else {
                section = memblock_virt_alloc_node(array_size, nid);
        }

        return section;
}
#endif

CONFIG_SPARSEMEM_EXTREME 커널 옵션을 사용하는 경우 mem_section[] 테이블용 메모리를 할당 받는다.

  • 코드 라인 5~6에서 루트 엔트리는 1개의 페이지로 구성되며, 가득 구성될 mem_section 구조체 배열의 크기를 구한다. SECTIONS_PER_ROOT는 루트 엔트리당 mem_section 수를 의미한다. 이 상수는 다음과 같이 정의되어 있다.
    • #define SECTIONS_PER_ROOT (PAGE_SIZE / sizeof(struct mem_section))
        • 루트 엔트리 당 mem_section[] 배열의 크기
        • 예) arm64: 256개
      • sizeof(struct mem_section)
        • 예) arm64: 페이지 확장 정보(CONFIG_PAGE_EXTENSION)없이 16 bytes
  • 코드 라인 8~12에서 슬랩 메모리 할당자가 동작하는 경우 노드가 highmem인 경우에는 kzalloc_node( ) 함수를 통해 메모리를 할당한다. 그렇지 않다면 kzalloc( ) 함수를 통해 lowmem에서 메모리를 할당한다.
  • 코드 라인 13~15에서 슬랩 메모리 할당자가 동작하지 않는 경우 해당 노드의 memblock에 할당한다

 

set_section_nid()

mm/sparse.c

#ifdef NODE_NOT_IN_PAGE_FLAGS
static void set_section_nid(unsigned long section_nr, int nid)
{
        section_to_node_table[section_nr] = nid;
}
#else /* !NODE_NOT_IN_PAGE_FLAGS */
static inline void set_section_nid(unsigned long section_nr, int nid)
{
}
#endif

NODE_NOT_IN_PAGE_FLAGS  옵션을 사용하는 경우 section_to_node_table[]에 섹션 번호에 1:1로 대응하는 노드 번호를 저장한다.

  • page 구조체 멤버 변수 flags에 노드 번호를 저장할 비트가 부족한 32비트 아키텍처에서 사용되는 옵션이다.

 

아래 그림은 set_section_nid() 함수를 통해 주어진 섹션 번호에 노드 번호를 저장한다.

set_section_nid-1

 

__nr_to_section()

mm/sparse.c

static inline struct mem_section *__nr_to_section(unsigned long nr)
{
#ifdef CONFIG_SPARSEMEM_EXTREME
        if (!mem_section)
                return NULL;
#endif
        if (!mem_section[SECTION_NR_TO_ROOT(nr)])
                return NULL;
        return &mem_section[SECTION_NR_TO_ROOT(nr)][nr & SECTION_ROOT_MASK];
}

섹션 번호에 해당하는 mem_section 구조체 정보를 알아온다.

 

아래 그림은 __nr_to_section() 함수를 사용하여 섹션 번호로 mem_section 구조체 정보를 알아오는 단계를 2 가지 예로 나타내었다.

__nr_to_section-1

 

sparse_encode_early_nid()

mm/sparse.c

/*
 * During early boot, before section_mem_map is used for an actual
 * mem_map, we use section_mem_map to store the section's NUMA
 * node.  This keeps us from having to use another data structure.  The
 * node information is cleared just before we store the real mem_map.
 */
static inline unsigned long sparse_encode_early_nid(int nid)
{
        return (nid << SECTION_NID_SHIFT);
}

노드 번호로 SECTION_NID_SHIFT(2) 만큼 좌측으로 쉬프트한다.

 


Sparse 메모리 모델 초기화

sparse_init()

다음 순서도는 Sparse 메모리 초기화에 대한 로직으로 다음과 같은 일들을 한다.

  • 노드별 usemap을 할당하고 임시로 사용되는 usemap_map에 연결한다.
  • CONFIG_SPARSEMEM_VMEMMAP 커널 옵션을 사용하면 mem_map을 vmemmap에 매핑하여 사용한다.
  • CONFIG_SPARSEMEM_VMEMMAP 커널 옵션을 사용하지 않는 경우 노드마다 활성화된 섹션들의 mem_map을 가능하면 한꺼번에 할당하다. (이전 버전의 x86 커널에 적용했었던 CONFIG_SPARSEMEM_ALLOC_MEM_MAP_TOGETHER 커널 옵션과 유사)

 

다음 그림은 sparse_init() 에서 만들어지는 mem_map, usemap, mem_section[] 들을 보여준다.

 

mm/sparse.c

/*
 * Allocate the accumulated non-linear sections, allocate a mem_map
 * for each and record the physical to section mapping.
 */
void __init sparse_init(void)
{
        unsigned long pnum_begin = first_present_section_nr();
        int nid_begin = sparse_early_nid(__nr_to_section(pnum_begin));
        unsigned long pnum_end, map_count = 1;

        /* Setup pageblock_order for HUGETLB_PAGE_SIZE_VARIABLE */
        set_pageblock_order();

        for_each_present_section_nr(pnum_begin + 1, pnum_end) {
                int nid = sparse_early_nid(__nr_to_section(pnum_end));

                if (nid == nid_begin) {
                        map_count++;
                        continue;
                }
                /* Init node with sections in range [pnum_begin, pnum_end) */
                sparse_init_nid(nid_begin, pnum_begin, pnum_end, map_count);
                nid_begin = nid;
                pnum_begin = pnum_end;
                map_count = 1;
        }
        /* cover the last node */
        sparse_init_nid(nid_begin, pnum_begin, pnum_end, map_count);
        vmemmap_populate_print_last();
}

sparse 메모리 모델을 사용하기 위해 페이지 디스크립터들이 집합되어 있는 mem_map들을 섹션 단위로 할당하고 관리하기 위해 초기화하는 과정을 알아본다.
분산된 메모리를 섹션 단위로 관리하기 위해 각 섹션이 mem_map과 usemap을 할당하여 관리할 수 있도록 한다. usemap은 페이지 블록 단위를 결정한 후 페이지 블록당 4비트의 비트맵으로 mobility 특성을 관리한다.

  • 코드 라인 8에서 전체 메모리에 대해 페이지 블록 크기 단위로 4비트의 mobility 특성을 기록하고 관리하기 위해 먼저 전역 변수 pageblock_order를 설정한다.
    • ARM64 시스템의 디폴트 설정은 pageblock_order 값에 9을 사용한다.
  • 코드 라인 10~16에서 present 섹션을 순회하며 동일 노드인 경우 map_count를 증가시켜 한꺼번에 할당 받을 개수를 정한다.
    • present 섹션을 순회 중일 때 pnum_end에는 순회중인 present 섹션 번호가 담긴다.
  • 코드 라인 18~21에서 바뀐 노드인 경우 해당 present  범위의 섹션들을 초기화한다.
  • 코드 라인 24에서 마지막 노드의 경우 해당 present 범위의 섹션들을 초기화한다.
  • 코드 라인 25에서 arm과 arm64에서는 구현되지 않아 출력하는 메시지가 없다.

 

pageblock_order

include/linux/pageblock-flags.h

#ifdef CONFIG_HUGETLB_PAGE

#ifdef CONFIG_HUGETLB_PAGE_SIZE_VARIABLE

/* Huge page sizes are variable */
extern unsigned int pageblock_order;

#else /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */

/* Huge pages are a constant size */
#define pageblock_order         HUGETLB_PAGE_ORDER

#endif /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */

#else /* CONFIG_HUGETLB_PAGE */

/* If huge pages are not used, group by MAX_ORDER_NR_PAGES */
#define pageblock_order         (MAX_ORDER-1)

#endif /* CONFIG_HUGETLB_PAGE */

페이지 블럭 오더 크기는 CONFIG_HUGETLB_PAGE 및 CONFIG_HUGETLB_PAGE_SIZE_VARIABLE 커널 옵션에 사용 유무에 따라 결정된다.

  • CONFIG_HUGETLB_PAGE 사용 시
    • CONFIG_HUGETLB_PAGE_SIZE_VARIABLE  커널 옵션 사용 시
      • pageblock_order를 런타임에 set_pageblock_order() 함수에서 결정한다.
    • CONFIG_HUGETLB_PAGE_SIZE_VARIABLE  커널 옵션 미 사용 시
      • 컴파일 타임에 HUGETLB_PAGE_ORDER 값으로 결정된다.
        • =HPAGE_SHIFT(21) – PAGE_SHIFT(12)=9 (arm64 디폴트 설정 사용 시)
  • CONFIG_HUGETLB_PAGE 커널 옵션을 사용하지 않을 경우
    • 컴파일 타임에 MAX_ORDER-1 값으로 결정된다.

 

for_each_present_section_nr()

mm/sparse.c

#define for_each_present_section_nr(start, section_nr)          \
        for (section_nr = next_present_section_nr(start-1);     \
             ((section_nr >= 0) &&                              \
              (section_nr <= __highest_present_section_nr));    \
             section_nr = next_present_section_nr(section_nr))

@start 섹션을 포함하여 마지막 present 섹션까지 present 섹션마다 순회한다. @section_nr는 출력 인자로 present 섹션 번호이다.

 

next_present_section_nr()

mm/sparse.c

static inline int next_present_section_nr(int section_nr)
{
        do {
                section_nr++;
                if (present_section_nr(section_nr))
                        return section_nr;
        } while ((section_nr <= __highest_present_section_nr));

        return -1;
}

요청한 @section_nr 다음 섹션번호부터 마지막 섹션까지 순회하며 present 섹션을 발견하면 해당 섹션 번호를 반환한다.

 


노드별 sparse 초기화

sparse_init_nid()

mm/sparse.c

/*
 * Initialize sparse on a specific node. The node spans [pnum_begin, pnum_end)
 * And number of present sections in this node is map_count.
 */
static void __init sparse_init_nid(int nid, unsigned long pnum_begin,
                                   unsigned long pnum_end,
                                   unsigned long map_count)
{
        unsigned long pnum, usemap_longs, *usemap;
        struct page *map;

        usemap_longs = BITS_TO_LONGS(SECTION_BLOCKFLAGS_BITS);
        usemap = sparse_early_usemaps_alloc_pgdat_section(NODE_DATA(nid),
                                                          usemap_size() *
                                                          map_count);
        if (!usemap) {
                pr_err("%s: node[%d] usemap allocation failed", __func__, nid);
                goto failed;
        }
        sparse_buffer_init(map_count * section_map_size(), nid);
        for_each_present_section_nr(pnum_begin, pnum) {
                if (pnum >= pnum_end)
                        break;

                map = sparse_mem_map_populate(pnum, nid, NULL);
                if (!map) {
                        pr_err("%s: node[%d] memory map backing failed. Some memory will not be avaii
lable.",
                               __func__, nid);
                        pnum_begin = pnum;
                        goto failed;
                }
                check_usemap_section_nr(nid, usemap);
                sparse_init_one_section(__nr_to_section(pnum), pnum, map, usemap);
                usemap += usemap_longs;
        }
        sparse_buffer_fini();
        return;
failed:
        /* We failed to allocate, mark all the following pnums as not present */
        for_each_present_section_nr(pnum_begin, pnum) {
                struct mem_section *ms;

                if (pnum >= pnum_end)
                        break;
                ms = __nr_to_section(pnum);
                ms->section_mem_map = 0;
        }
}

@nid 노드에 해당하는 @pnum_begin ~ @pnum_end 미만 까지의 섹션 범위들에 대해 @map_count 만큼의 present 섹션과 관련된 mem_map 및 usemap을 할당하고 초기화한다.

  • 코드 라인 8~15에서 @map_count 섹션 수 만큼의 usemap[] 배열을 노드 @nid에서 한꺼번에 할당한다.
  • 코드 라인 16에서 @map_count 섹션 수 만큼의 sparse 버퍼를 노드 @nid에서 한꺼번에 할당한다.
  • 코드 라인 17~28에서 @pnum_begin 섹션 부터 끝 present 섹션까지 순회하며 해당 섹션의 mem_map을 할당받는다.
    • 가능하면 sparse 버퍼에서 할당받는다.
  • 코드 라인 29에서 할당받은 usemap과 노드 정보(pgdat)가 기록된 섹션과 같지 않으면 경고 메시지를 출력한다. usemap 정보가 같은 노드 공간에 구성되지 않으면 메모리 핫리무브(hot remove) 동작 시 노드별로 메모리를 제거해야 할 때 circular dependancy 문제가 발생할 수 있다.
  • 코드 라인 30에서 해당 섹션 하나를 초기화한다.
  • 코드 라인 33에서 할당하고 남은 sparse 버퍼를 할당 해제한다.

 

브라켓 표현법

begin이 1이고 end가 5라고 할 때

  • [begin, end]
    • begin 이상 ~ end 이하
    • 1~5
  • (begin, end)
    • bigin 초과 ~ end 미만
    • 2~4
  • [begin, end)
    • begin 이상 ~ end 미만
    • 1~4
  • (begin, end]
    • begin 초과, end 이하
    • 2~5

 

usemap 할당

sparse_early_usemaps_alloc_pgdat_section()

mm/sparse.c

#ifdef CONFIG_MEMORY_HOTREMOVE
static unsigned long * __init
sparse_early_usemaps_alloc_pgdat_section(struct pglist_data *pgdat,
                                         unsigned long size)
{
        unsigned long goal, limit;
        unsigned long *p;
        int nid;
        /*
         * A page may contain usemaps for other sections preventing the
         * page being freed and making a section unremovable while
         * other sections referencing the usemap remain active. Similarly,
         * a pgdat can prevent a section being removed. If section A
         * contains a pgdat and section B contains the usemap, both
         * sections become inter-dependent. This allocates usemaps
         * from the same section as the pgdat where possible to avoid
         * this problem.
         */
        goal = __pa(pgdat) & (PAGE_SECTION_MASK << PAGE_SHIFT);
        limit = goal + (1UL << PA_SECTION_SHIFT);
        nid = early_pfn_to_nid(goal >> PAGE_SHIFT);
again:
        p = memblock_alloc_try_nid_nopanic(size,
                                                SMP_CACHE_BYTES, goal, limit,
                                                nid);
        if (!p && limit) {
                limit = 0;
                goto again;
        }
        return p;
}
#endif

CONFIG_MEMORY_HOTREMOVE 커널 옵션을 사용하는 경우 use_map을 할당한다. 가능하면 노드 정보(pgdat)가 담겨 있는 섹션에 usemap이 들어갈 수 있도록 메모리 할당을 시도한다. 실패한 경우 위치에 상관없이 다시 한번 할당을 시도한다.

  • 코드 라인 16~17에서 goal에는 노드에서의 섹션 시작 물리 주소를 구하고 limit를 1개의 섹션 크기로 제한한다.
  • 코드 라인 18에서 기존에 mem_map에 저장해놓은 섹션별 노드 정보를 가져온다.
  • 코드 라인 20~22에서 지정된 노드의 goal~limit 범위, 즉 노드 정보가 담겨 있는 섹션 영역 내에서 SMP_CACHE_BYTES align으로 size만큼의 memblock 공간 할당을 요청한다.
  • 코드 라인 23~29에서 한 번 시도해서 할당이 안 되면 limit를 0으로 만들어 다시 한번 시도한다.

 

mem_map용 Sparse 버퍼 할당/해제

노드에 해당하는 전체 present 섹션 메모리에 대한 mem_map[] 배열을 한꺼번에 할당을 시도한다. 이렇게 시도하여 할당이 된 경우 이 sparse 버퍼 메모리를 mem_map[] 배열로 사용한다. 만일 할당이 실패하는 경우 fallback되어 그냥 섹션별로 할당한다.

 

sparse_buffer_init()

mm/sparse.c

static void __init sparse_buffer_init(unsigned long size, int nid)
{
        WARN_ON(sparsemap_buf); /* forgot to call sparse_buffer_fini()? */
        sparsemap_buf =
                memblock_alloc_try_nid_raw(size, PAGE_SIZE,
                                                __pa(MAX_DMA_ADDRESS),
                                                MEMBLOCK_ALLOC_ACCESSIBLE, nid);
        sparsemap_buf_end = sparsemap_buf + size;
}

요청한 @size만큼 노드 @nid에서 페이지 단위로 sparse 버퍼를 할당한다.

 

sparse_buffer_alloc()

mm/sparse.c

void * __meminit sparse_buffer_alloc(unsigned long size)
{
        void *ptr = NULL;

        if (sparsemap_buf) {
                ptr = PTR_ALIGN(sparsemap_buf, size);
                if (ptr + size > sparsemap_buf_end)
                        ptr = NULL;
                else
                        sparsemap_buf = ptr + size;
        }
        return ptr;
}

Sparse 버퍼에서 @size 만큼의 메모리를 할당한다. (for mem_map)

 

sparse_buffer_fini()

mm/sparse.c

static void __init sparse_buffer_fini(void)
{
        unsigned long size = sparsemap_buf_end - sparsemap_buf;

        if (sparsemap_buf && size > 0)
                memblock_free_early(__pa(sparsemap_buf), size);
        sparsemap_buf = NULL;
}

사용하고 남은 sparse 버퍼를 할당 해제한다.

 

usemap 할당 섹션 또는 노드 체크

check_usemap_section_nr()

mm/sparse.c

#ifdef CONFIG_MEMORY_HOTREMOVE
static void __init check_usemap_section_nr(int nid, unsigned long *usemap)
{
        unsigned long usemap_snr, pgdat_snr;
        static unsigned long old_usemap_snr;
        static unsigned long old_pgdat_snr;
        struct pglist_data *pgdat = NODE_DATA(nid);
        int usemap_nid;

        /* First call */
        if (!old_usemap_snr) {
                old_usemap_snr = NR_MEM_SECTIONS;
                old_pgdat_snr = NR_MEM_SECTIONS;
        }

        usemap_snr = pfn_to_section_nr(__pa(usemap) >> PAGE_SHIFT);
        pgdat_snr = pfn_to_section_nr(__pa(pgdat) >> PAGE_SHIFT);
        if (usemap_snr == pgdat_snr)
                return;

        if (old_usemap_snr == usemap_snr && old_pgdat_snr == pgdat_snr)
                /* skip redundant message */
                return;

        old_usemap_snr = usemap_snr;
        old_pgdat_snr = pgdat_snr;

        usemap_nid = sparse_early_nid(__nr_to_section(usemap_snr));
        if (usemap_nid != nid) {
                pr_info("node %d must be removed before remove section %ld\n",
                        nid, usemap_snr);
                return;
        }
        /*
         * There is a circular dependency.
         * Some platforms allow un-removable section because they will just
         * gather other removable sections for dynamic partitioning.
         * Just notify un-removable section's number here.
         */
        pr_info("Section %ld and %ld (node %d) have a circular dependency on usemap and pgdat allocaa
tions\n",
                usemap_snr, pgdat_snr, nid);
}
#endif

CONFIG_MEMORY_HOTREMOVE 커널 옵션을 사용하는 경우 usemap 섹션은 pgdat가 위치한 섹션에 있거나 그렇지 않다면 다른 섹션이 모두 삭제될 때까지 usemap이 위치한 섹션은 삭제되면 안 된다. 따라서 이에 대한 관계를 메시지로 알아보기 위한 루틴이다. 할당받은 usemap과 노드 정보(pgdat)가 기록된 섹션과 같지 않으면 정보를 출력한다. usemap 정보가 같은 노드 공간에 구성되지 않으면 메모리 핫리무브(hot remove) 동작 시 노드별로 메모리를 제거해야 할 때 circular dependancy 문제가 발생할 수 있다. 참고: memory hotplug: allocate usemap on the section with pgdat

  • 코드 라인 11~14에서 처음 호출 시 초깃값으로 진행 중인 섹션 번호를 담는다.
  • 코드 라인 16~19에서 usemap이 할당된 섹션과 노드 정보가 기록된 섹션이 같은 경우 정상이므로 함수를 빠져나간다.
  • 코드 라인 21~23에서 이미 한 번 진행하였던 섹션인 경우 skip 하기 위해 함수를 빠져나간다.
  • 코드 라인 25~26에서 같은 섹션 번호로 다시 한 번 진행하는 경우 skip 하기 위해 섹션 번호들을 기억해둔다.
  • 코드 라인 28~33에서 요청한 @nid에 usemap 노드가 없는 경우 use_map이 있는 섹션 정보를 먼저 hot remove 하여야 한다는 정보를 출력한다.
  • 코드 라인 40~41에서 요청한 @nid에 usemap이 있는 경우 메모리 핫리무브(hot remove) 동작 시 노드별로 메모리를 제거해야 할 때 circular dependancy 문제가 발생할 수 있다는 정보를 출력한다.

 

sparse_early_nid()

mm/sparse.c

static inline int sparse_early_nid(struct mem_section *section)
{
        return (section->section_mem_map >> SECTION_NID_SHIFT);
}

mem_section 구조체 멤버 변수인 section_mem_map에서 노드 정보를 추출하여 리턴한다.

 

usemap_size()

mm/sparse.c

unsigned long usemap_size(void)
{
        return BITS_TO_LONGS(SECTION_BLOCKFLAGS_BITS) * sizeof(unsigned long);
}

usemap 사이즈를 리턴한다.

  •  SECTION_BLOCKFLAGS_BITS
    • 섹션당 pageblock 비트 수 (pageblock_order=9일 때)
      • arm64=2048
  • 예) arm64
    • 2048 / 8=256(byte)

 

sparse_mem_map_populate()

vmemmap을 사용하지 않을 때의 함수이다. arm64 디폴트 설정에서는 vmemmap을 사용하므로 이 함수를 사용하지 않는다.

mm/sparse.c

#ifndef CONFIG_SPARSEMEM_VMEMMAP
struct page __init *sparse_mem_map_populate(unsigned long pnum, int nid,
                struct vmem_altmap *altmap)
{
        unsigned long size = section_map_size();
        struct page *map = sparse_buffer_alloc(size);

        if (map)
                return map;

        map = memblock_alloc_try_nid(size,
                                          PAGE_SIZE, __pa(MAX_DMA_ADDRESS),
                                          MEMBLOCK_ALLOC_ACCESSIBLE, nid);
        return map;
}
#endif /* !CONFIG_SPARSEMEM_VMEMMAP */

CONFIG_SPARSEMEM_VMEMMAP 커널 옵션을 사용하지 않는 시스템에서 미리 할당한 sparse 버퍼를 사용하여 mem_map[]을 할당해준다. 만일 실패하는 경우 별도로 섹션마다 mem_map[] 배열을 memblock 할당 요청한다.

 

sparse_init_one_section()

mm/sparse.c

static void __meminit sparse_init_one_section(struct mem_section *ms,
                unsigned long pnum, struct page *mem_map,
                unsigned long *pageblock_bitmap)
{
        ms->section_mem_map &= ~SECTION_MAP_MASK;
        ms->section_mem_map |= sparse_encode_mem_map(mem_map, pnum) |
                                                        SECTION_HAS_MEM_MAP;
        ms->pageblock_flags = pageblock_bitmap;
}

mem_map 영역을 엔코딩하여 mem_section에 연결하고 usemap 영역도 연결한다.

 

mem_map 주소의 엔코딩/디코딩

섹션별로 mem_map 주소를 지정할 때 하위 비트에는 몇 개의 플래그를 추가하는 것 이외에도, 해당 섹션의 mem_map에 대한 base 주소의 pfn 값을 뺀(-) 값으로 엔코딩하여 저장한다.이렇게 엔코딩하여 저장하는 경우 vmemmap을 사용하지 않는 시스템에서 pfn_to_page() 함수를 사용할 때 한 번의 산술연산이 절감되는 효과가 있다.

  • 예) 0x8000_0000 ~ 0xC000_0000 (1G)을 관리하는 섹션에 대한 mem_map 주소 – 0x80000(base pfn) + 플래그 값으로 엔코딩한다.

 

sparse_encode_mem_map()

mm/sparse.c

/*
 * Subtle, we encode the real pfn into the mem_map such that
 * the identity pfn - section_mem_map will return the actual
 * physical page frame number.
 */
static unsigned long sparse_encode_mem_map(struct page *mem_map, unsigned long pnum)
{
        unsigned long coded_mem_map =
                (unsigned long)(mem_map - (section_nr_to_pfn(pnum)));
        BUILD_BUG_ON(SECTION_MAP_LAST_BIT > (1UL<<PFN_SECTION_SHIFT));
        BUG_ON(coded_mem_map & ~SECTION_MAP_MASK);
        return coded_mem_map;
}

할당 받은 mem_map의 주소 – 섹션에 해당하는 base pfn 값을 엔코딩 값으로 반환한다.

  • vmemmap을 사용하지 않는 경우 pfn_to_page() 함수에서 산술 연산을 한 번 제거하는 효과가 있다.

 

sparse_decode_mem_map()

mm/sparse.c

/*
 * Decode mem_map from the coded memmap
 */
struct page *sparse_decode_mem_map(unsigned long coded_mem_map, unsigned long pnum)
{
        /* mask off the extra low bits of information */
        coded_mem_map &= SECTION_MAP_MASK;
        return ((struct page *)coded_mem_map) + section_nr_to_pfn(pnum);
}

엔코딩된 mem_map 주소에서 하위 3비트를 제거한 후 디코딩하여 mem_map 주소만 반환한다.

 


VMEMMAP 관련

  • arm64에서는 CONFIG_SPARSEMEM_VMEMMAP을 기본적으로 사용한다.
  • 시스템 리소스가 충분하여 vmemmap을 사용하는 경우 pfn_to_page() 및 page_to_pfn() 함수의 동작이 가장 효과적으로 빨라진다.

 

sparse_mem_map_populate()

mm/sparse-vmemmap.c

#ifdef CONFIG_SPARSEMEM_VMEMMAP
struct page * __meminit sparse_mem_map_populate(unsigned long pnum, int nid)
{
        unsigned long start;
        unsigned long end;
        struct page *map;

        map = pfn_to_page(pnum * PAGES_PER_SECTION);
        start = (unsigned long)map;
        end = (unsigned long)(map + PAGES_PER_SECTION);

        if (vmemmap_populate(start, end, nid))
                return NULL;

        return map;
}
#endif

CONFIG_SPARSEMEM_VMEMMAP 커널 옵션을 사용하는 경우 mem_map 영역을 vmemmap 영역에 매핑한다.요청한 섹션으로 주소 범위를 구한 후 해당 주소 범위와 관련한 pgd, pud 및 pmd 테이블들에 구성되지 않은 테이블이 있는 경우 노드에서 페이지를 할당하여 구성하고 매핑한다.

  • sparse 버퍼에 할당된 mem_map 용 공간들을 pte 엔트리에 연결하여 매핑한다.

 

vmemmap_populate()

arch/arm64/mm/mmu.c

#ifdef CONFIG_SPARSEMEM_VMEMMAP
#if !ARM64_SWAPPER_USES_SECTION_MAPS
int __meminit vmemmap_populate(unsigned long start, unsigned long end, int node,
                struct vmem_altmap *altmap)
{
        return vmemmap_populate_basepages(start, end, node);
}
#else   /* !ARM64_SWAPPER_USES_SECTION_MAPS */
int __meminit vmemmap_populate(unsigned long start, unsigned long end, int node,
                struct vmem_altmap *altmap)
{
        unsigned long addr = start;
        unsigned long next;
        pgd_t *pgdp;
        pud_t *pudp;
        pmd_t *pmdp;

        do {
                next = pmd_addr_end(addr, end);

                pgdp = vmemmap_pgd_populate(addr, node);
                if (!pgdp)
                        return -ENOMEM;

                pudp = vmemmap_pud_populate(pgdp, addr, node);
                if (!pudp)
                        return -ENOMEM;

                pmdp = pmd_offset(pudp, addr);
                if (pmd_none(READ_ONCE(*pmdp))) {
                        void *p = NULL;

                        p = vmemmap_alloc_block_buf(PMD_SIZE, node);
                        if (!p)
                                return -ENOMEM;

                        pmd_set_huge(pmdp, __pa(p), __pgprot(PROT_SECT_NORMAL));
                } else
                        vmemmap_verify((pte_t *)pmdp, node, addr, next);
        } while (addr = next, addr != end);

        return 0;
}
#endif  /* CONFIG_ARM64_64K_PAGES */

요청 주소 범위와 관련한 pgd, pud 및 pmd 테이블들에 구성되지 않은 테이블이 있는 경우 노드에서 페이지를 할당하여 구성하고 매핑한다.

  • 4K 페이지를 사용하는 경우 ARM64_SWAPPER_USES_SECTION_MAPS 값은 1이다.

 

구조체 및 주요 변수

mem_section[]

mm/sparse.c

/*
 * Permanent SPARSEMEM data:
 *
 * 1) mem_section       - memory sections, mem_map's for valid memory
 */     
#ifdef CONFIG_SPARSEMEM_EXTREME
struct mem_section *mem_section[NR_SECTION_ROOTS]
        ____cacheline_internodealigned_in_smp;
#else
struct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT]
        ____cacheline_internodealigned_in_smp;
#endif
EXPORT_SYMBOL(mem_section);

mem_section은 섹션 별로 mem_map[] 배열과 usemap[] 배열 정보와 연결된다.

 

mem_section 구조체

include/linux/mmzone.h

struct mem_section {
        /*
         * This is, logically, a pointer to an array of struct
         * pages.  However, it is stored with some other magic.
         * (see sparse.c::sparse_init_one_section())
         *
         * Additionally during early boot we encode node id of
         * the location of the section here to guide allocation.
         * (see sparse.c::memory_present())
         *
         * Making it a UL at least makes someone do a cast
         * before using it wrong.
         */
        unsigned long section_mem_map;

        /* See declaration of similar field in struct zone */
        unsigned long *pageblock_flags;
#ifdef CONFIG_PAGE_EXTENSION
        /*
         * If !SPARSEMEM, pgdat doesn't have page_ext pointer. We use
         * section. (see page_ext.h about this.)
         */
        struct page_ext *page_ext;
        unsigned long pad;
#endif
        /*
         * WARNING: mem_section must be a power-of-2 in size for the
         * calculation and use of SECTION_ROOT_MASK to make sense.
         */
};
  • section_mem_map
    • mem_map을 가리키는 주소로 엔코딩하여 사용된다.
      • page 단위로 정렬되어있다.
    • 다음의 하위 비트들을 추가 정보로 사용한다.
      • bit0: SECTION_MARKED_PRESENT
        • 섹션에 메모리가 있는지 여부를 표현한다.
      • bit1: SECTION_HAS_MEM_MAP
        • 섹션에 mem_map이 연결되었는지 여부를 표현한다.
        • 연결된 mem_map은 섹션 base pfn 값을 빼고 저장된다(엔코딩)
      • bit2: SECTION_IS_ONLINE
        • online 섹션 여부를 표현한다.
      • bits[3~]: 노드 번호
        • 노드 번호는 부트업 중에만 잠깐 사용된다.
  • pageblock_flags
    • usemap을 가리키는 주소

 

기타

  • SECTION_SIZE_BITS, MAX_PHYSMEM_BITS
    • arm
      • 28,  32 (256M, 4G, Realview-PBX)
      • 26,  29 (64M, 512M, RPC)
      • 27, 32 (128M, 4G, SA1100)
    • 32bit arm with LPAE
      • 34, 36 (16G, 64G, Keystone)
    • arm64
      • 30, 48 (1G, 256T)
    • x86
      • 26, 32 (64M, 4G)
    • x86_32 with PAE
      • 29, 36 (512M, 64G)
    • x86_64
      • 27, 46 (128M, 64T)

 

참고

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다