Memory Model -1- (Basic)

<kernel v5.15>

물리 메모리 모델

물리 메모리의 0번 주소부터 시작하여 마지막 끝 주소까지 연속된 주소를 사용하는 시스템의 경우 물리 메모리를 관리하는 방법이 가장 간단하다. 그런데 그러한 물리 메모리의 중간에 약간의 홀(hole)이 있어 cpu로 부터 접근할 수 없는 경우가 있다. 어떠한 아키텍처들은 물리 메모리를 배치할 때 중간에 홀(hole)이 생성되는 경우가 있는 경우가 있다. 또한 NUMA 시스템을 보면 메모리 뱅크 사이에 커다란 홀이 발생할 수도 있다.

 

리눅스 커널은 물리 메모리를 페이지 프레임 단위(디폴트 4K)로 1:1로 대응하여 관리를 하는 page 구조체를 사용한다. 예를 들어 1GB 메모리를 가진 ARM64 시스템의 경우 에서 의 경우 1G 물리 메모리는 페이지 프레임 단위로 1M(0x100000)개를 사용하고, 이에 대응하는 page 구조체는 1M * 64바이트(디폴트) = 64MB 의 용량을 차지한다. 그러니깐 전체 물리 메모리의 약 1.5%를 page 구조체들이 사용을 하며, 이들을 mem_map이라고 부른다.

 

물리 메모리는 물리 메모리 주소 공간 0부터 시작하는 PFN(Page Frame Number) 숫자 개념을 사용하며, 이 PFN과 page 구조체는 1:1로 대응하므로 서로 변환을 많이하며 변환이 쉽고 빨라야 한다. 이 때 사용하는 API는 pfn_to_page()와 page_to_pfn() 매크로 함수를 사용한다. 전체 물리 메모리가 hole 없이 구성된 경우 page 구조체들로 구성된 mem_map은 일괄적으로 생성하고 이에 대해 pfn을 통한 접근이 단순 산술 연산으로 쉽게 해당 page 구조체를 가리킬 수 있게되는데, 전체 물리 메모리에 홀(hole)이 있는 경우 mem_map 구성에 대한 방법이 달라져아함을 알 수 있고, 이에 대해 리눅스 커널은 3가지의 물리 메모리 모델을 제공하고 있다.

 

3가지 물리 메모리 모델

리눅스는 물리 메모리를 관리하기 위해 FLATMEM, DISCONTIGMEM 및 SPARSEMEM의 3가지 모델을 제공하다, 최근에 DISCONTIGMEM을 제외하였고, 아키텍처들은 이 중 하나 이상을 지원하며 커널 빌드 시 지원되는 메모리 모델 중 하나를 선택하여 빌드한다. (32비트 시스템의 대부분은 FLATMEM을 사용하고, 64비트 시스템의 경우 SPARSEMEM을 사용한다)

mm-1b

 

물리 메모리 모델별 특징

각 물리 메모리 모델은 물리 메모리 사이에 hole이 존재하는 경우 이를 처리하는 방법이 다르다. 물리 메모리의 각 프레임 관리를 위해 page 구조체가 필요하고, 이들의 집합형태가 mem_map이다. 이들을 관리하는 방법에 따라 아래 3가지의 물리 메모리 모델을 알아본다.

 

FLATMEM

  • 설치된 메모리 뱅크들의 주소가 연속된다. 이러한 경우 가장 심플하게 이 모델을 선택할 수 있다.
  • 설치된 메모리 뱅크들의 주소 사이에 작은 hole이 발생하는 경우에도 이 모델을 사용할 수 있다. 대신 해당 hole에 대응하는 mem_map 만큼의 메모리를 낭비한다.

 

DISCONTIGMEM

  • 2009년 NUMA 시스템을 위해 소개되었다.
  • 설치된 메모리 뱅크들의 주소 사이에 큰 hole이 발생하는 경우에 이 모델을 사용했었다.
  • ARM은 2010년 핫 플러그에 대응을 못하는 DISCONTIGMEM을 대체하는 SPARSEMEM이 도입되면서 이 모델의 지원을 종료하였다.
  • 커널 메인라인에서 조차 이 모델을 완전히 제거하였다.

 

SPARSEMEM

  • 설치된 메모리 뱅크들의 주소 사이에 큰 hole이 발생하는 경우(NUMA 시스템 포함) 또는 hotplug memory 기능이 필요한 경우 이모델을 선택할 수 있다.
  • 섹션 단위로 online/offline(hotplug memory) 관리가 되며 섹션 사이즈는 수십MB~수G로 커널 옵션 또는 아키텍처별로 다르다.
    • arm32의 경우 설정된 커널 옵션마다 다르다.
    • arm64의 경우 디폴트 값으로 1G를 사용다가 최근 128M로 변경하였다.

 

ARM with NUMA

mm-2a

메모리 모델 주요 커널 옵션

mm-4
  • ARM64의 경우 다음과 같은 옵션이 사용되어 SPARSEMEM 메모리 모델 1개만을 사용하고 있다.
    • CONFIG_ARCH_SELECT_MEMORY_MODEL=y
    • CONFIG_SELECT_MEMORY_MODEL=y
    • CONFIG_ARCH_SPARSEMEM_ENABLE=y
    • CONFIG_ARCH_SPARSEMEM_DEFAULT=y

 

메모리 모델 부가 커널 옵션

mm-5
  • NEED_MULTIPLE_NODES
    • NUMA 시스템 또는 DISCONTIGMEM 메모리 모델에서 사용할 수 있다.
    • NODE_DATA() 매크로를 사용하여 각 노드의 mem_map을 가리킬 수 있게 한다.
    • 전역 node_data[] 배열을 사용하여 노드별로 mem_map에 접근한다.
  •  FLAT_NODE_MEM_MAP
    • FLATMEM 또는 DISCONTIGMEM 메모리 모델에서 사용할 수 있다.
    • 노드 정보를 기술해 놓은 pglist_data 구조체의 멤버 변수 node_mem_map을 통해 mem_map에 접근한다.
  • HAVE_MEMORY_PRESENT
    • SPARSEMEM 메모리 모델을 위해 요청한 범위에 hole이 발생할 수 있으므로 각 섹션 별로 메모리 존재 여부를 관리할 수 있도록 한다.
  • SPARSEMEM_EXTREME
    • sparse 메모리 모델을 사용할 때 사용되는 두 가지 선택 중 하나이다.
    • mem_section[] 배열을 dynamic 하게 할당 받아 사용하려고 할 때 사용
    • 주로 생성해야 할 섹션 수가 많은 64비트 시스템에서 사용한다.
  • SPARSEMEM_STATIC
    • sparse 메모리 모델을 사용할 때 사용되는 두 가지 선택 중 하나이다.
    • 컴파일 시 static하게 할당해둔 mem_section[]을 사용한다.
    • 주로 생성해야 할 섹션 수가 적은 32비트 시스템에서 사용한다.
  • SPARSEMEM_VMEMMAP
    • vmemmap을 사용하여 빠르게 pfn과 page descriptor 주소를 변환할 수 있다.
    • vmalloc 주소 범위가 큰 64비트 시스템에서 사용한다.
  • MEMORY_HOTPLUG
    • 메모리를 시스템 동작 중에 추가할 수 있다.
  • MEMORY_HOTREMOVE
    • 메모리를 시스템 동작 중에 제거할 수 있다.
    • MEMORY_HOTPLUG, ARCH_ENABLE_MEMORY_HOTREMOVE, MIGRATION 옵션이 있을 때 선택할 수 있다.
  • HIGHMEM
    • 32bit에서 커널에 미리 1:1 매핑되지 않아 커널이 직접 access를 해야 할 때마다 매핑하여 사용해야 하는 메모리 범위이다.
    • 32bit 시스템에서 시스템 메모리가 커널 공간보다 큰 경우 HIGHMEM 옵션을 사용하여 더 많은 메모리를 활용할 수 있게 한다.
  • HIGHPTE
    • 32bit 시스템에서 2차 PTE를 highmem에 할당한다.
    • 32bit 시스템에 수 기가 이상의 메모리를 사용하는 경우 2차 PTE를 highmem에 로드하게 하여 lowmem 메모리를 소모하지 않게 유도한다.
  • NODE_NOT_IN_PAGE_FLAGS
    • page 구조체의 flags 멤버변수에 노드 번호를 기록하지 못하는 경우 노드 번호를 별도로 섹션에 대해 1:1로 매핑하여 저장한다
    • 32비트 시스템에서 비트 자리가 부족하여 노드 번호를 기록하지 못하는 경우 사용

 

메모리 모델과 페이지 관리맵(mem_map)

전체 물리 메모리의 페이지 프레임 만큼 page 구조체 배열이 mem_map이라하는데 다음과 같이 각 메모리 모델별로 mem_map을 관리한다.
  • FLATMEM
    • *mem_map 포인터가 page[] 구조체 배열을 가리킨다.
    • 노드가 하나 이므로 전역 구조체 contig_page_data를 사용하는데 이 구조체의 멤버 변수 node_mem_map 역시 page[] 구조체 배열을 가리킨다.
  • DISCONTIGMEM
    • 노드가 두 개 이상이므로 node_data[] 배열을 사용하는데 이 구조체의 멤버 변수 node_mem_map은 각 노드와 관련된 page[] 구조체 배열을 가리킨다.
  • SPARSEMEM
    • 두 가지 방법을 사용한다.
      • SPARSEMEM_STATIC
        • 섹션 수 만큼 mem_section[] 배열을 컴파일 타임에 정적 메모리에 두고 각 섹션 엔트리의 멤버 변수 section_mem_map이 해당 섹션 크기 만큼의 페이지를 관리하는 page[] 구조체 배열을 가리킨다.
      • SPARSEMEM_EXTREAM
        • 섹션 수 만큼 *mem_section[] 포인터 배열을 정적 메모리에 두고 실제 각 mem_section[] 배열은 커널 초기화를 진행할 때 mem_section[] 배열을 dynamic하게 할당받아 메모리를 사용하고 mem_section[]의 사용은 SPARSEMEM_STATIC과 같다.
mm-3b

참고

 

 

bootmem_init()

memory memblock으로 부터 lowmem 영역의 경계를 파악하여 각각의 zone으로 경계를 나누어 설정하고 0번 노드의 빈 페이지들을 초기화한다. 또한 Sparse 메모리 모델을 지원하는 경우에는 sparse memory의 초기화도 수행한다.

bootmem_init-2a

 

bootmem_init()

arch/arm/mm/init.c

void __init bootmem_init(void)
{
        unsigned long min, max_low, max_high;

        memblock_allow_resize();
        max_low = max_high = 0;

        find_limits(&min, &max_low, &max_high);

        /*
         * Sparsemem tries to allocate bootmem in memory_present(),
         * so must be done after the fixed reservations
         */
        arm_memory_present();

        /*
         * sparse_init() needs the bootmem allocator up and running.
         */
        sparse_init();

        /*
         * Now free the memory - free_area_init_node needs
         * the sparse mem_map arrays initialized by sparse_init()
         * for memmap_init_zone(), otherwise all PFNs are invalid.
         */
        zone_sizes_init(min, max_low, max_high);

        /*
         * This doesn't seem to be used by the Linux memory manager any
         * more, but is used by ll_rw_block.  If we can get rid of it, we
         * also get rid of some of the stuff above as well.
         */
        min_low_pfn = min;
        max_low_pfn = max_low;
        max_pfn = max_high;
}
  • memblock_allow_resize();
    • memblock_can_resize = 1로 만들어 향후 memblock 관리 영역이 모자랄 때 2배 단위로 커질 수 있도록 한다.
  • find_limits(&min, &max_low, &max_high);
    • memblock 정보로 max_pfn, max_low_pfn, min_low_pfn 값을 얻어온다.
  • arm_memory_present();
    • Sparsemem의 경우 내부에서 mem_section[] 매핑을 위한 allocation이 수행되기 때문에 반드시 fixed reservation이 끝난 후에 이 함수가 호출되어야 한다.
    • 몇 개 32bit ARM 머신을 제외하고 대부분의 32bit ARM에서는 CONFIG_SPARSEMEM 옵션을 사용하지 않는다.
    • 참고: Sparse Memory | 문c
  • sparse_init();
    • Sparse memory 모델을 사용하는 시스템을 위해 관리 영역을 할당받고 매핑 초기화한다.
    • 참고: Sparse Memory | 문c
  • zone_sizes_init(min, max_low, max_high);
    • zone  영역을 나누고 초기화한다.

아래와 같이 memblock 설정 값을 읽어 lowmem 영역의 경계를 나눈다. 이 값은 zone을 나누는 경계로도 사용된다.

bootmem_init-1b

 

memblock_allow_resize()

mm/memblock.c

void __init memblock_allow_resize(void)
{
        memblock_can_resize = 1;
}
  • memblock_can_resize 플래그를 enable 시켜 향후 memblock 관리 영역이 모자랄 때 2배 단위로 커질 수 있도록 한다.

 

find_limits()

arch/arm/mm/init.c

static void __init find_limits(unsigned long *min, unsigned long *max_low,
                               unsigned long *max_high)
{
        *max_low = PFN_DOWN(memblock_get_current_limit());
        *min = PFN_UP(memblock_start_of_DRAM());
        *max_high = PFN_DOWN(memblock_end_of_DRAM());
}
  • memblock 정보를 읽어 다음을 설정한다.
  • max_low
    • lowmem/highmem 영역의 경계 pfn
  • min
    • 메모리 영역의 최하 pfn
  • max_high
    • 메모리 영역의 최상 pfn

 

zone_sizes_init()

arch/arm/mm/init.c

static void __init zone_sizes_init(unsigned long min, unsigned long max_low,
        unsigned long max_high)
{
        unsigned long zone_size[MAX_NR_ZONES], zhole_size[MAX_NR_ZONES];
        struct memblock_region *reg;

        /*  
         * initialise the zones.
         */
        memset(zone_size, 0, sizeof(zone_size));

        /*  
         * The memory size has already been determined.  If we need
         * to do anything fancy with the allocation of this memory
         * to the zones, now is the time to do it.
         */
        zone_size[0] = max_low - min;
#ifdef CONFIG_HIGHMEM
        zone_size[ZONE_HIGHMEM] = max_high - max_low;
#endif

        /*  
         * Calculate the size of the holes.
         *  holes = node_size - sum(bank_sizes)
         */
        memcpy(zhole_size, zone_size, sizeof(zhole_size));
        for_each_memblock(memory, reg) {
                unsigned long start = memblock_region_memory_base_pfn(reg);
                unsigned long end = memblock_region_memory_end_pfn(reg);

                if (start < max_low) {
                        unsigned long low_end = min(end, max_low);
                        zhole_size[0] -= low_end - start;
                }   
#ifdef CONFIG_HIGHMEM
                if (end > max_low) {
                        unsigned long high_start = max(start, max_low);
                        zhole_size[ZONE_HIGHMEM] -= end - high_start;
                }
#endif
        }

#ifdef CONFIG_ZONE_DMA
        /*  
         * Adjust the sizes according to any special requirements for
         * this machine type.
         */
        if (arm_dma_zone_size)
                arm_adjust_dma_zone(zone_size, zhole_size,
                        arm_dma_zone_size >> PAGE_SHIFT);
#endif

        free_area_init_node(0, zone_size, min, zhole_size);
}
  • zone_size[0] = max_low – min;
    • lowmem 영역 사이즈 설정
  • zone_size[ZONE_HIGHMEM] = max_high – max_low;
    • highmem이 동작하는 경우 highmem 영역 사이즈 설정
  • memcpy(zhole_size, zone_size, sizeof(zhole_size));
    • zone_size[]를 zhole_size[]에 복사
  • for_each_memblock(memory, reg) {
    • memory memblock을 루프를 돌며 하나씩 얻어온다.
  • if (start < max_low) {
    • memory memblock이 lowmem 영역에 들어있거나 일부 겹치는 경우
  • unsigned long low_end = min(end, max_low);
    • 끝 주소가 lowmem을 초과하지 않게 한다.
  • zhole_size[0] -= low_end – start;
    • hole 사이즈를 감소시킨다. 이 값이 0이되면 hole이 없는 것이다.
  • highmem 영역에 대해서도 위와 똑같은 방법으로 hole 사이즈를 감소시킨다.
  • if (arm_dma_zone_size)
    • DMA zone 사이즈가 존재하는 경우 dma zone 사이즈만큼 ZONE_DMA에 사이즈를 구성하고 ZONE_NORMAL 영역에서 그 사이즈만큼 뺀다.
  • free_area_init_node(0, zone_size, min, zhole_size);
    • 0번 노드에 zone_size[]와, zhole_size[] 정보를 사용하여 빈 페이지들을 초기화한다.
    • 참고: free_area_init_node() | 문c

아래 그림과 같이 highmem 영역을 사용하는 경우 ZONE_HIGHMEM에 대한 사이즈가 지정되고, 아키텍처가 dma 영역을 별도로 지정하여 사용하는 경우 ZONE_NORMAL에 대한 size를 dma 사이즈 만큼 감소시키고 ZONE_DMA에 대한 사이즈를 dma 사이즈로 지정한다.

  • rpi2: ZONE_NORMAL만 사용한다.

bootmem_init-3b

참고

tcm_init()

TCM(Tightly Coupled Memory)이 내장된 SoC에서 DTCM 및 ITCM 메모리 영역의 갯 수와 크기를 알아내서 다음과 같이 매핑하고 enable 한다.

  • DTCM: 0xfffe_8000 가상 주소에 전체 DTCM 영역을 MT_MEMORY_RW_DTCM으로 매핑 예약(static_vmlist에 추가)
  • ITCM: 0xfffe_0000 가상 주소에 전체 ITCM 영역을 MT_MEMORY_RWX_ITCM으로 매핑 예약(static_vmlist에 추가)
  • rpi2: TCM이 없음.

32bit ARM 리눅스에서는 TCM 메모리의 최대 처리 용량은 각각 32K이다.

tcm_init-1

 

tcm_init()

arch/arm/kernel/tcm.c

/*
 * This initializes the TCM memory
 */
void __init tcm_init(void)
{
        u32 tcm_status;
        u8 dtcm_banks;
        u8 itcm_banks;
        size_t dtcm_code_sz = &__edtcm_data - &__sdtcm_data;
        size_t itcm_code_sz = &__eitcm_text - &__sitcm_text;
        char *start;
        char *end;
        char *ram;
        int ret;
        int i;

        /*
         * Prior to ARMv5 there is no TCM, and trying to read the status
         * register will hang the processor.
         */
        if (cpu_architecture() < CPU_ARCH_ARMv5) {
                if (dtcm_code_sz || itcm_code_sz)
                        pr_info("CPU TCM: %u bytes of DTCM and %u bytes of "
                                "ITCM code compiled in, but no TCM present "
                                "in pre-v5 CPU\n", dtcm_code_sz, itcm_code_sz);
                return;
        }

        tcm_status = read_cpuid_tcmstatus();
        dtcm_banks = (tcm_status >> 16) & 0x03;
        itcm_banks = (tcm_status & 0x03);

        /* Values greater than 2 for D/ITCM banks are "reserved" */
        if (dtcm_banks > 2)
                dtcm_banks = 0;
        if (itcm_banks > 2)
                itcm_banks = 0;
  • if (cpu_architecture() < CPU_ARCH_ARMv5) {
    • ARMv5 미만 아키텍처는 TCM이 없으므로 루틴을 빠져나간다.
  • tcm_status = read_cpuid_tcmstatus();
    • DTCM 뱅크 수와 ITCM 뱅크 수를 알아온다.
      • ARMv6 format
        • DTCM: CPUID_TCM[bit17:16]
        • ITCM: CPUID_TCM[bit1:0]
      • ARMv7 format
        • 현재 리눅스 코드가 ARMv7 format을 지원하지 않는다.
  • if (dtcm_banks > 2)
    • 뱅크 수는 0~2까지 인데 3이 나오는 경우는 PB11MPCore가 TCM이 없는데도 잘못 보고를 하는 case가 발견되어 이를 수정한 것이다.
    • 참고: ARM: 6984/1: enhance TCM robustness
        /* Setup DTCM if present */
        if (dtcm_banks > 0) {
                for (i = 0; i < dtcm_banks; i++) {
                        ret = setup_tcm_bank(0, i, dtcm_banks, &dtcm_end);
                        if (ret)
                                return;
                }
                /* This means you compiled more code than fits into DTCM */
                if (dtcm_code_sz > (dtcm_end - DTCM_OFFSET)) {
                        pr_info("CPU DTCM: %u bytes of code compiled to "
                                "DTCM but only %lu bytes of DTCM present\n",
                                dtcm_code_sz, (dtcm_end - DTCM_OFFSET));
                        goto no_dtcm;
                }
                dtcm_res.end = dtcm_end - 1;
                request_resource(&iomem_resource, &dtcm_res);
                dtcm_iomap[0].length = dtcm_end - DTCM_OFFSET;
                iotable_init(dtcm_iomap, 1);
                /* Copy data from RAM to DTCM */
                start = &__sdtcm_data;
                end   = &__edtcm_data;
                ram   = &__dtcm_start;
                memcpy(start, ram, dtcm_code_sz);
                pr_debug("CPU DTCM: copied data from %p - %p\n",
                         start, end);
                dtcm_present = true;
        } else if (dtcm_code_sz) {
                pr_info("CPU DTCM: %u bytes of code compiled to DTCM but no "
                        "DTCM banks present in CPU\n", dtcm_code_sz);
        }
  • if (dtcm_banks > 0) {
    • DTCM 뱅크가 발견되면
  • for (i = 0; i < dtcm_banks; i++) {
    • DTCM 뱅크 수 만큼 루프를 돈다.
  • ret = setup_tcm_bank(0, i, dtcm_banks, &dtcm_end);
    • TCM 관련 레지스터를 설정한다.
    • dtcm_end는 초기에 TCM의 base 주소가 담겨서 함수에 진입하고 함수를 빠져 나올때엔  TCM 사이즈만큼 증가되어 다음 TCM 메모리의 주소가 될 위치를 가리킨다.
  • if (dtcm_code_sz > (dtcm_end – DTCM_OFFSET)) {
    • 커널에 있는 DTCM용 데이터 사이즈가 실제 DTCM 영역 크기보다 더 큰 경우 에러를 출력하고 루틴을 빠져 나온다.
  • request_resource(&iomem_resource, &dtcm_res)
    • iomem_resource에 TCM 영역을 추가한다.
  • iotable_init(dtcm_iomap, 1);
    • static_vmlist에 영역을 추가하여 매핑 예약을 한다.
  • 커널에 있는 DTCM 데이터 영역을 TCM 영역에 복사한다.
  • dtcm_present = true;
    • DTCM이 준비되었음을 알리는 전역 변수이다.
no_dtcm:
        /* Setup ITCM if present */
        if (itcm_banks > 0) {
                for (i = 0; i < itcm_banks; i++) {
                        ret = setup_tcm_bank(1, i, itcm_banks, &itcm_end);
                        if (ret)
                                return;
                }
                /* This means you compiled more code than fits into ITCM */
                if (itcm_code_sz > (itcm_end - ITCM_OFFSET)) {
                        pr_info("CPU ITCM: %u bytes of code compiled to "
                                "ITCM but only %lu bytes of ITCM present\n",
                                itcm_code_sz, (itcm_end - ITCM_OFFSET));
                        return;
                }
                itcm_res.end = itcm_end - 1;
                request_resource(&iomem_resource, &itcm_res);
                itcm_iomap[0].length = itcm_end - ITCM_OFFSET;
                iotable_init(itcm_iomap, 1);
                /* Copy code from RAM to ITCM */
                start = &__sitcm_text;
                end   = &__eitcm_text;
                ram   = &__itcm_start;
                memcpy(start, ram, itcm_code_sz);
                pr_debug("CPU ITCM: copied code from %p - %p\n",
                         start, end);
                itcm_present = true;
        } else if (itcm_code_sz) {
                pr_info("CPU ITCM: %u bytes of code compiled to ITCM but no "
                        "ITCM banks present in CPU\n", itcm_code_sz);
        }
}
  • ITCM의 코드는 DTCM의 코드와 거의 동일하다.

 

다음 그림은 DTCM 및 ITCM 메모리가 각각 2개씩 내장된 ARM SoC를 대상으로 가상 주소에 매핑되는 예를 들었다.

  • DTCM 및 ITCM이 각각 2개씩인 상황에 대해 메모리를 매핑하였지만 실제 SoC에는 DTCM 및 ITCM 메모리가 없거나 있는 경우 각각 1개가 내장되어 있다.

tcm_init-2a

 

read_cpuid_tcmstatus()

arch/arm/include/asm/cputype.h

static inline unsigned int __attribute_const__ read_cpuid_tcmstatus(void)
{
        return read_cpuid(CPUID_TCM);
}
  • TCMTR(TCM Type Register) 레지스터를 통해 DTCM 뱅크 수와 ITCM 뱅크 수를 알아낼 수 있다.
    • ARMv6 format
      • DTCM: CPUID_TCM[bit17:16]
      • ITCM: CPUID_TCM[bit1:0]
    • CPUID_TCM=2
    • 두 개의 비트를 사용하여 0=no TCM, 1=1개 TCM, 2=2개 TCM, 3=undefined

 

setup_tcm_bank()

arch/arm/kernel/tcm.c

static int __init setup_tcm_bank(u8 type, u8 bank, u8 banks,
                                  u32 *offset)
{
        const int tcm_sizes[16] = { 0, -1, -1, 4, 8, 16, 32, 64, 128,
                                    256, 512, 1024, -1, -1, -1, -1 };
        u32 tcm_region;
        int tcm_size;

        /*
         * If there are more than one TCM bank of this type,
         * select the TCM bank to operate on in the TCM selection
         * register.
         */
        if (banks > 1)
                asm("mcr        p15, 0, %0, c9, c2, 0"
                    : /* No output operands */
                    : "r" (bank));

        /* Read the special TCM region register c9, 0 */
        if (!type)
                asm("mrc        p15, 0, %0, c9, c1, 0"
                    : "=r" (tcm_region));
        else
                asm("mrc        p15, 0, %0, c9, c1, 1"
                    : "=r" (tcm_region));
        tcm_size = tcm_sizes[(tcm_region >> 2) & 0x0f];
        if (tcm_size < 0) {
                pr_err("CPU: %sTCM%d of unknown size\n",
                       type ? "I" : "D", bank);
                return -EINVAL;
        } else if (tcm_size > 32) {
                pr_err("CPU: %sTCM%d larger than 32k found\n",
                       type ? "I" : "D", bank);
                return -EINVAL;
        } else {
                pr_info("CPU: found %sTCM%d %dk @ %08x, %senabled\n",
                        type ? "I" : "D",
                        bank,
                        tcm_size,
                        (tcm_region & 0xfffff000U),
                        (tcm_region & 1) ? "" : "not ");
        }

        /* Not much fun you can do with a size 0 bank */
        if (tcm_size == 0)
                return 0;

        /* Force move the TCM bank to where we want it, enable */
        tcm_region = *offset | (tcm_region & 0x00000ffeU) | 1;

        if (!type)
                asm("mcr        p15, 0, %0, c9, c1, 0"
                    : /* No output operands */
                    : "r" (tcm_region));
        else
                asm("mcr        p15, 0, %0, c9, c1, 1"
                    : /* No output operands */
                    : "r" (tcm_region));

        /* Increase offset */
        *offset += (tcm_size << 10);

        pr_info("CPU: moved %sTCM%d %dk to %08x, enabled\n",
                type ? "I" : "D",
                bank,
                tcm_size,
                (tcm_region & 0xfffff000U));
        return 0;
}
  • if (banks > 1)
    • 뱅크 수가 1개를 넘어가는 경우
  • asm(“mcr        p15, 0, %0, c9, c2, 0”
    • TCMSR(TCM Selection Register)을 사용하여 TCM 선택을 한다.
  • if (!type)
    • type=0(DTCM)이면
  • asm(“mrc        p15, 0, %0, c9, c1, 0”
    • DTCMRR(Data TCM Region Register)을 통해 DTCM 정보를 읽어온다.
  •  asm(“mrc        p15, 0, %0, c9, c1, 1”
    • ITCMRR(Instruction TCM Region Register)을 통해 ITCM 정보를 읽어온다.
  • tcm_size = tcm_sizes[(tcm_region >> 2) & 0x0f];
    • 알아온 TCM 정보에서 사이즈를 지정하는 5비트 중 4비트만 읽어낸다.
  • tcm_size가 0보다 작거나 32보다 큰 경우 에럴를 출력하고 루틴을 빠져나간다.
  • tcm_size가 0인 경우 TCM이 없는 것으로 인식하고 루틴을 빠져나간다.
  • tcm_region = *offset | (tcm_region & 0x00000ffeU) | 1;
    • tcm_region 속성 값에 TCM base 주소를 가리키는 offset을 갱신하고 TCM enable 비트를 설정한다.
  • asm(“mcr        p15, 0, %0, c9, c1, 0”
    • DTCMRR에 tcm_region 속성값을 기록한다.
  • asm(“mcr        p15, 0, %0, c9, c1, 1”
    • ITCMRR에 tcm_region 속성값을 기록한다
  • *offset += (tcm_size << 10);
    • offset에 tcm_size * 1K 만큼을 추가하여 다음 TCM base 주소를 리턴한다.

 

request_resource()

kernel/resource.c

/**
 * request_resource - request and reserve an I/O or memory resource
 * @root: root resource descriptor
 * @new: resource descriptor desired by caller
 *
 * Returns 0 for success, negative error code on error.
 */
int request_resource(struct resource *root, struct resource *new)
{
        struct resource *conflict;

        conflict = request_resource_conflict(root, new);
        return conflict ? -EBUSY : 0;
}
  • new 리소스를 root에 등록할 때 리소스 영역들끼리 겹치는 영역이 있으면 에러가 리턴된다.

 

request_resource_conflict()

kernel/resource.c

/**
 * request_resource_conflict - request and reserve an I/O or memory resource
 * @root: root resource descriptor
 * @new: resource descriptor desired by caller
 *
 * Returns 0 for success, conflict resource on error.
 */
struct resource *request_resource_conflict(struct resource *root, struct resource *new)
{
        struct resource *conflict;

        write_lock(&resource_lock);
        conflict = __request_resource(root, new);
        write_unlock(&resource_lock);
        return conflict;
}
  • write_lock(&resource_lock);
    • resource들간에 연결을 위해 write lock을 수행
  • conflict = __request_resource(root, new);
    • root가 관리하는 영역에 new 영역을 추가한다.
      • root 영역의 범위에 포함되지 않는 경우 root를 리턴한다.
      • 기존 리소스들과 영역이 겹치는 경우 해당 리소스를 리턴한다.
      • 기존 리소스들과 영역이 겹치지 않는 경우 new 영역을 추가하고 null을 리턴한다.
  • write_unlock(&resource_lock);
    • write unlock을 수행

 

__request_resource()

kernel/resource.c

/* Return the conflict entry if you can't request it */
static struct resource * __request_resource(struct resource *root, struct resource *new)
{
        resource_size_t start = new->start;
        resource_size_t end = new->end;
        struct resource *tmp, **p;

        if (end < start)
                return root;
        if (start < root->start)
                return root;
        if (end > root->end)
                return root;
        p = &root->child;
        for (;;) {
                tmp = *p;
                if (!tmp || tmp->start > end) {
                        new->sibling = tmp;
                        *p = new;
                        new->parent = root;
                        return NULL;
                }
                p = &tmp->sibling;
                if (tmp->end < start)
                        continue;
                return tmp;
        }
}
  • if (end < start) return root
    • new 영역의 끝 주소가 시작 주소보다 큰 경우는 root를 리턴
  • if (start < root->start)
    • new 영역이 root의 하단부를 벗어난 경우 root를 리턴
  • if (end > root->end) return root;
    • new 영역이 root의 상단부를 벗어난 경우 root를 리턴

resource-1

  • if (!tmp || tmp->start > end) {
    • new 리소스의 영역이 기존 리소스 영역과 겹치지 않으면서 하단에 위치하는 경우 앞쪽에 추가한다.

resource-3

  • p = &tmp->sibling;
    • 비교할 노드를 다음 옆 리소스로 선택한다.
  • if (tmp->end < start) continue;
    • new 리소스의 영역이 현재 리소스 영역보다 크면 루프를 계속 진행한다.

resource-4

  • new 리소스의 영역이 현재 리소스 영역과 겹치면 현재 리소스를 리턴한다.

resource-2

TCMSR, DTCMRR, ITCMRR 레지스터

  • TCMSR
    • 0~3 번 TCM 메모리 영역(뱅크)를 선택한다.
  • DTCMRR and ITCMRR
    • 현재 선택된 TCM 메모리의 물리 주소, 사이즈를 알아오거나 설정할 수 있다.
      • 물리주소를 바꾸거나 사용 가능 사이즈도 줄일 수 있다.
    • Enable bit를 사용하여 사용 여부를 결정할 수 있다.
    • BaseAddress
      • TCM 메모리의 물리 주소로 size에 align되어 사용된다.
      • Size는 아래 테이블 표를 참고한다.
        • ARM 리눅스는 현재 32K까지 지원한다.

tcm_init-3

 

구조체 및 전역 변수

 

resource 구조체

include/linux/ioport.h

struct resource {
        resource_size_t start;
        resource_size_t end;
        const char *name;
        unsigned long flags;
        struct resource *parent, *sibling, *child;
};
  • start
    • 리소스의 시작 물리 주소
  • end
    • 리소스의 끝 물리 주소
  • name
    • 리소스 명
  • flags
    • TCM 메모리의 플래그는 IORESOURCE_MEM(0x00000200) 이다.
  • parent
    • 리소스의 상위와 연결
  • sibling
    • 다음 리소스와 연결(ascending order)
  • child
    • 하위 리소스와 연결

 

map_desc 구조체

arch/arm/include/asm/mach/map.h

struct map_desc {
        unsigned long virtual;
        unsigned long pfn;
        unsigned long length;
        unsigned int type;
};
  • virtual
    • 가상 시작 주소
  • pfn
    • 물리 프레임 번호
  • length
    • 길이
  • type
    • 매핑 타입

 

dtcm_res & itcm_res

arch/arm/kernel/tcm.c

/*
 * TCM memory resources
 */
static struct resource dtcm_res = {
        .name = "DTCM RAM",
        .start = DTCM_OFFSET,
        .end = DTCM_OFFSET,
        .flags = IORESOURCE_MEM
};

static struct resource itcm_res = {
        .name = "ITCM RAM",
        .start = ITCM_OFFSET,
        .end = ITCM_OFFSET,
        .flags = IORESOURCE_MEM
};

 

dtcm_iomap[] & itcm_iomap[]

arch/arm/kernel/tcm.c

static struct map_desc dtcm_iomap[] __initdata = {
        {
                .virtual        = DTCM_OFFSET,
                .pfn            = __phys_to_pfn(DTCM_OFFSET),
                .length         = 0,
                .type           = MT_MEMORY_RW_DTCM
        }
};      
        
static struct map_desc itcm_iomap[] __initdata = {
        {
                .virtual        = ITCM_OFFSET,
                .pfn            = __phys_to_pfn(ITCM_OFFSET),
                .length         = 0,
                .type           = MT_MEMORY_RWX_ITCM,
        }
};

 

기타 전역 변수

static struct gen_pool *tcm_pool;
static bool dtcm_present;
static bool itcm_present;

/* TCM section definitions from the linker */
extern char __itcm_start, __sitcm_text, __eitcm_text;
extern char __dtcm_start, __sdtcm_data, __edtcm_data;

/* These will be increased as we run */
u32 dtcm_end = DTCM_OFFSET;
u32 itcm_end = ITCM_OFFSET;

 

참고

kmap_init()

pkmap과 fixmap을 사용한 매핑을 하기 위해 각각의 PTE 테이블을 할당 받아 초기화한다.

kmap_init-1

kmap_init()

arch/arm/mm/mmu.c

static void __init kmap_init(void)
{
#ifdef CONFIG_HIGHMEM
        pkmap_page_table = early_pte_alloc(pmd_off_k(PKMAP_BASE),
                PKMAP_BASE, _PAGE_KERNEL_TABLE);
#endif

        early_pte_alloc(pmd_off_k(FIXADDR_START), FIXADDR_START,
                        _PAGE_KERNEL_TABLE);
}
  • pkmap_page_table = early_pte_alloc(pmd_off_k(PKMAP_BASE), PKMAP_BASE, _PAGE_KERNEL_TABLE);
    • CONFIG_HIGHMEM 커널 옵션을 사용할 수 있으면 pkmap 사용을 위해 pkmap_page_table 전역 변수에 pkmap 시작 주소에 해당하는 pmd 엔트리 주소를 알아온다.
      • 만일 pmd 엔트리 값이 없는 경우 pte 테이블을 할당 받는다.
    • _PAGE_KERNEL_TABLE
      • (PMD_TYPE_TABLE | PMD_BIT4 | PMD_DOMAIN(DOMAIN_KERNEL))
    • PKMAP_BASE(PAGE_OFFSET-2M)
  •   early_pte_alloc(pmd_off_k(FIXADDR_START), FIXADDR_START, _PAGE_KERNEL_TABLE);
    • fixmap 사용을 위해 pte 테이블을 준비한다.
      • FIXADDR_START(0xffc0_0000)

pkmap용 L2 테이블이 없는 경우 할당 받아 초기화 한다. pkmap_page_table은 pkmap용 L2테이블을 가리킨다.

kmap_init-2

fixmap용 L2 테이블이 없는 경우 할당 받아 초기화한다.

kmap_init-3

 

참고

devicemaps_init()

아래와 같이 IO 디바이스 영역에 대한 매핑을 한다.

  • 벡터 페이지 8K를 할당받고 초기 데이터를 copy한다.
  • XIP 커널 영역을 MT_ROM으로 매핑
  • Cache Flushing Region이 필요한 아키텍처의 경우 해당 주소부터 1M를 MT_CACHECLEAN으로 매핑하고 더 필요 시 1M를 MT_MINICLEAN으로 매핑
  • 벡터테이블을 MT_LOW_VECTORS/MT_HIGH_VECTORS로 매핑
  • 머신 아키텍처가 map_io 함수를 지원하는 경우 해당 함수를 호출하여 디바이스 영역을 정적으로 매핑하게 한다. 아키텍처가 별도로 요구하지 않는 경우 CONFIG_DEBUG_LL 커널 옵션이 설정된 경우 콘솔 디바이스 영역을 MT_DEVICE로 static_vmlist에 추가하여  매핑 예약
    • UART, I2C, GPIO, …. 등
  • 매핑된 영역에 대해 짝이 맞지 않는 pmd 엔트리를 1M 추가 매핑하여 설정한다.
  • PCI 영역 0xFEE0_0000 부터 2M를 static_vmlist에 추가하여 매핑 예약.
  • TLB 등을 포함한 i-cache, d-cache를 flush 한다.

 

devicemaps_init()

devicemaps_init-1

 

vmalloc 영역에 대한 매핑용 페이지 테이블 엔트리를 삭제하므로 모든 디버그용 디바이스 매핑이 삭제될 것이다. 따라서 이 함수를 수정하거나 디버깅 하는 경우 매우 조심해야 한다는 의미이다.

arch/arm/mm/mmu.c

/*
 * Set up the device mappings.  Since we clear out the page tables for all
 * mappings above VMALLOC_START, we will remove any debug device mappings.
 * This means you have to be careful how you debug this function, or any
 * called function.  This means you can't use any function or debugging
 * method which may touch any device, otherwise the kernel _will_ crash.
 */
static void __init devicemaps_init(const struct machine_desc *mdesc)
{
        struct map_desc map; 
        unsigned long addr;
        void *vectors;

        /*
         * Allocate the vector page early.
         */
        vectors = early_alloc(PAGE_SIZE * 2);

        early_trap_init(vectors);

        for (addr = VMALLOC_START; addr; addr += PMD_SIZE)
                pmd_clear(pmd_off_k(addr));

        /*
         * Map the kernel if it is XIP.
         * It is always first in the modulearea.
         */
#ifdef CONFIG_XIP_KERNEL
        map.pfn = __phys_to_pfn(CONFIG_XIP_PHYS_ADDR & SECTION_MASK);
        map.virtual = MODULES_VADDR;
        map.length = ((unsigned long)_etext - map.virtual + ~SECTION_MASK) & SECTION_MASK;
        map.type = MT_ROM;
        create_mapping(&map);
#endif

        /*
         * Map the cache flushing regions.
         */
#ifdef FLUSH_BASE
        map.pfn = __phys_to_pfn(FLUSH_BASE_PHYS);
        map.virtual = FLUSH_BASE;
        map.length = SZ_1M;
        map.type = MT_CACHECLEAN;
        create_mapping(&map);
#endif
#ifdef FLUSH_BASE_MINICACHE
        map.pfn = __phys_to_pfn(FLUSH_BASE_PHYS + SZ_1M);
        map.virtual = FLUSH_BASE_MINICACHE;
        map.length = SZ_1M;
        map.type = MT_MINICLEAN;
        create_mapping(&map);
#endif
  • vectors = early_alloc(PAGE_SIZE * 2);
    • 벡터 테이블 용도로 사용하기 위해 4K  페이지 두 개를 memblock을 통해 할당 받는다.
  • early_trap_init(vectors);
    • 할당된 벡터 영역에 커널에 존재하는 벡터 데이터를 복사하여 준비한다.
  • for (addr = VMALLOC_START; addr; addr += PMD_SIZE) pmd_clear(pmd_off_k(addr));
    • vmalloc 공간에 대한 1차 페이지 테이블 매핑을 모두 지운다.

XIP 커널 매핑

  • XIP 커널 옵션을 사용하는 경우 XIP 커널의 시작 물리 주소로 pfn 값을 구한다.
  • 매핑할 가상 주소 값은  MODULES_ADDR(PAGE_OFFSET)이고 길이는 커널의 코드 영역 끝에서 커널 시작 주소를 뺸 후 섹션 단위로 round up한다.
  • 매핑 메모리 타입을 MT_ROM으로 하여 매핑을 한다.
    • MT_ROM 타입의 특징
      • 섹션 매핑만 허용
      • 메모리 접근 권한은 privileged level에서 read만 가능
      • non-share
      • WBWA 캐시 속성
      • Normal Memory 속성

Cache Flushing Regions – CACHECLEAN

  • 시스템이 FLUSH_BASE 옵션을 지원하면 사용할 수 있다.
  • 지원되는 시스템에서 FLUSH_BASE_PHYS가 매핑될 시작 물리 주소가 지정되어 있고 길이는 1M로 고정되어 있다.
    • rpi2: 사용하지 않음.
  • 매핑 메모리 타입을 MT_CACHECLEAN으로 하여 매핑을 한다.
    • MT_CACHECLEAN 타입의 특징
      • 섹션 매핑만 허용
      • 메모리 접근 권한은 privileged level에서 read만 가능
      • share
      • 캐시는 사용하지 못하고 버퍼만 사용 가능
      • Device 속성
      • 코드 실행 금지(XN)

Cache Flushing Regions – MINICACHE

  • 시스템이 FLUSH_BASE_MINICACHE 옵션을 지원하면 사용할 수 있다.
  • 지원되는 시스템에서 FLUSH_BASE_PHYS + 1M가 매핑될 시작 물리 주소가 지정되어 있고 길이는 1M로 고정되어 있다.
    • rpi2: 사용하지 않음.
  • 매핑 메모리 타입을 MT_MINICLEAN으로 하여 매핑을 한다.
    • MT_MINICLEAN 타입의 특징
      • 섹션 매핑만 허용
      • 메모리 접근 권한은 privileged level에서 read만 가능
      • Non-share
      • 캐시와 버퍼를 사용하지 않음
      • Strong Ordered Memory 속성
      • 코드 실행 금지(XN)

 

        /*
         * Create a mapping for the machine vectors at the high-vectors
         * location (0xffff0000).  If we aren't using high-vectors, also
         * create a mapping at the low-vectors virtual address.
         */
        map.pfn = __phys_to_pfn(virt_to_phys(vectors));
        map.virtual = 0xffff0000;
        map.length = PAGE_SIZE;
#ifdef CONFIG_KUSER_HELPERS
        map.type = MT_HIGH_VECTORS;
#else
        map.type = MT_LOW_VECTORS;
#endif
        create_mapping(&map);

        if (!vectors_high()) {
                map.virtual = 0;
                map.length = PAGE_SIZE * 2;
                map.type = MT_LOW_VECTORS;
                create_mapping(&map);
        }

        /* Now create a kernel read-only mapping */
        map.pfn += 1;
        map.virtual = 0xffff0000 + PAGE_SIZE;
        map.length = PAGE_SIZE;
        map.type = MT_LOW_VECTORS;
        create_mapping(&map);

        /*
         * Ask the machine support to map in the statically mapped devices.
         */
        if (mdesc->map_io)
                mdesc->map_io();
        else
                debug_ll_io_init();
        fill_pmd_gaps();

        /* Reserve fixed i/o space in VMALLOC region */
        pci_reserve_io();

        /*
         * Finally flush the caches and tlb to ensure that we're in a
         * consistent state wrt the writebuffer.  This also ensures that
         * any write-allocated cache lines in the vector page are written
         * back.  After this point, we can start to touch devices again.
         */
        local_flush_tlb_all();
        flush_cache_all();
}

할당 받은 벡터 페이지를 CPU가 사용하는 가상 주소에 맞게 매핑하는데 CPU는 SCTLR에서 벡터의 주소를 0x0(low vector address) 또는 0xffff_0000(high vector) address의 둘 중 하나로 설정할 수 있다.

  • 벡터 영역의 첫 페이지를 0xffff_0000 가상 주소에 MT_HIGH_VECTORS 메모리 타입으로 매핑한다.
    • MT_HIGH_VECTORS 메모리 타입 속성
      • 섹션을 사용하지 못하고 small page 매핑만 가능
      • 메모리 접근 권한은 privileged level(kernel space)과 unprivileged level(user space)에서 read만 가능
      • Share
      • WBWA 캐시 속성
  • if (!vectors_high()) {
    • SCTLR의 벡터 모드가 high가 아닌 경우 벡터 영역의 두 개 페이지를 0x0 가상 주소에 MT_LOW_VECTORS 메모리 타입으로 매핑한다.
  • 벡터 영역의 두 번째 페이지를 0xffff_1000 가상 주소에 MT_LOW_VECTORS 메모리 타입으로 매핑한다.
  • if (mdesc->map_io) mdesc->map_io();
    • 머신 아키텍처가 map_io 함수를 지원하는 경우 해당 함수를 호출하여 디바이스 영역을 정적으로 매핑하게 한다.
    • rpi2 예) bcm2709_map_io()
  •  debug_ll_io_init();
    • 디버그 출력용 디바이스 장치가 사용하는 주소 1 페이지를 static_vmlist에 추가하여 매핑 예약한다.
  • fill_pmd_gaps();
    • 매핑된 영역에 대해 짝이 맞지 않는 pmd 엔트리를 1M 추가 매핑하여 설정한다.
  • pci_reserve_io();
    • PCI에서 사용하는 주소 영역을 static_vmlist에 추가하여 매핑 예약한다.
  • 마지막으로 모든 TLB 캐시 엔트리를 flush하고 모든 캐시를 flush 한다.

 

early_trap_init()

arch/arm/kernel/traps.c

void __init early_trap_init(void *vectors_base)
{
#ifndef CONFIG_CPU_V7M
        unsigned long vectors = (unsigned long)vectors_base;
        extern char __stubs_start[], __stubs_end[];
        extern char __vectors_start[], __vectors_end[];
        unsigned i;

        vectors_page = vectors_base;
             
        /*
         * Poison the vectors page with an undefined instruction.  This
         * instruction is chosen to be undefined for both ARM and Thumb
         * ISAs.  The Thumb version is an undefined instruction with a
         * branch back to the undefined instruction.
         */ 
        for (i = 0; i < PAGE_SIZE / sizeof(u32); i++)
                ((u32 *)vectors_base)[i] = 0xe7fddef1;

        /*
         * Copy the vectors, stubs and kuser helpers (in entry-armv.S)
         * into the vector page, mapped at 0xffff0000, and ensure these
         * are visible to the instruction stream.
         */
        memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
        memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end - __stubs_start);

        kuser_init(vectors_base);

        flush_icache_range(vectors, vectors + PAGE_SIZE * 2);
        modify_domain(DOMAIN_USER, DOMAIN_CLIENT);
#else /* ifndef CONFIG_CPU_V7M */
        /*
         * on V7-M there is no need to copy the vector table to a dedicated
         * memory area. The address is configurable and so a table in the kernel
         * image can be used.
         */
#endif
}
  • for (i = 0; i < PAGE_SIZE / sizeof(u32); i++) ((u32 *)vectors_base)[i] = 0xe7fddef1;
    • 할당 받은 벡터 페이지를 0xe7fd_def1으로 설정한다.
    • 이 값은 ARM 또는 THUMB instruction에 없는 코드를 사용하여 이 영역의 코드로 jump되는 경우 exception이 발생하게 하였다.
  • memcpy((void *)vectors, __vectors_start, __vectors_end – __vectors_start);
    • 할당 받은 벡터 페이지에 커널의 vectors_start ~ vectors_end 까지의 영역을 복사한다.
  • memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end – __stubs_start);
    • 할당 받은 벡터 페이지 + 4K에 커널의 stubs_start ~ stubs_end 까지의 영역을 복사한다
  • kuser_init(vectors_base);
    • CONFIG_KUSER_HELPERS 커널 옵션을 사용하는 경우 할당 받은 벡터 영역 + 4K 바로 밑에 kuser_helper 코드를 복사하고 필요 시 vectors + 0xfe8(hardware TLS instruction)의 4바이트를 vectors + 0xfe0(kuser_get_tls) 주소에 복사한다.
  • flush_icache_range(vectors, vectors + PAGE_SIZE * 2);
    • 벡터 2개 페이지를 i-cache flush 한다.
  • modify_domain(DOMAIN_USER, DOMAIN_CLIENT);
    • DOMAIN_USER 영역을 DOMAIN_CLIENT(0b01)로 설정한다.

 

kuser_init()

arch/arm/kernel/traps.c

static void __init kuser_init(void *vectors)
{
        extern char __kuser_helper_start[], __kuser_helper_end[];
        int kuser_sz = __kuser_helper_end - __kuser_helper_start;

        memcpy(vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz);

        /*  
         * vectors + 0xfe0 = __kuser_get_tls
         * vectors + 0xfe8 = hardware TLS instruction at 0xffff0fe8
         */
        if (tls_emu || has_tls_reg)
                memcpy(vectors + 0xfe0, vectors + 0xfe8, 4); 
}
  •  memcpy(vectors + 0x1000 – kuser_sz, __kuser_helper_start, kuser_sz);
    • 할당 받은 벡터 영역 + 4K 바로 밑에 kuser_helper 코드를 복사한다.
  • if (tls_emu || has_tls_reg) memcpy(vectors + 0xfe0, vectors + 0xfe8, 4);
    • tls_emu와 has_tls_reg가 하나라도 설정된 경우 vectors + 0xfe8(hardware TLS instruction)의 4바이트를 vectors + 0xfe0(kuser_get_tls) 주소에 복사한다.
      • user space에서 TLS(Thread Local Storage) 값의 access를 하기 위해 사용
        • HWCAP_TLS 기능이 있는 일부 ARMv6  이상의 아키텍처에서 TPIDRURO 및 TPIDRURW(TLS 레지스터로 이용)를 사용
        • HWCAP_TLS 기능이 없는 아키텍처는 vectors + 0xff0(kuser_get_tls) 주소를 사용한다.
      • TPIDRPRW, TPIDRURO, TPIDRURW 레지스터는 ARM에서 계획했던 TPID의 access 용도로 사용하지 않고 다음과 같이 per-cpu나 TLS base 주소를 access 하는데에 사용한다.
        • TPIDRPRW는 per-cpu offset 용도로 사용
        • TPIDRURO는 user space에서의 TLS 값을access
        • TPIDRURW는 kernel space에서의 TLS 값을 access
      • 참고: Kernel-provided User Helpers | kernel.org

 

bcm2709_map_io()

arch/arm/mach-bcm2709/bcm2709.c

void __init bcm2709_map_io(void)
{
        iotable_init(bcm2709_io_desc, ARRAY_SIZE(bcm2709_io_desc));
}

rpi2에서 사용하는 bcm2709 칩에서 기본적으로 사용되는 IO device의 매핑을 한다.

 

bcm2709_io_desc[] 배열

arch/arm/mach-bcm2709/bcm2709.c

static struct map_desc bcm2709_io_desc[] __initdata = {
        {
         .virtual = IO_ADDRESS(ARMCTRL_BASE),
         .pfn = __phys_to_pfn(ARMCTRL_BASE),
         .length = SZ_4K,
         .type = MT_DEVICE},
        {
         .virtual = IO_ADDRESS(UART0_BASE),
         .pfn = __phys_to_pfn(UART0_BASE),
         .length = SZ_4K,
         .type = MT_DEVICE},
        {
         .virtual = IO_ADDRESS(UART1_BASE),
         .pfn = __phys_to_pfn(UART1_BASE),
         .length = SZ_4K,
         .type = MT_DEVICE},
        {
         .virtual = IO_ADDRESS(DMA_BASE),
         .pfn = __phys_to_pfn(DMA_BASE),
         .length = SZ_4K,
         .type = MT_DEVICE},
        {
         .virtual = IO_ADDRESS(MCORE_BASE),
         .pfn = __phys_to_pfn(MCORE_BASE),
         .length = SZ_4K,
         .type = MT_DEVICE},
        {
         .virtual = IO_ADDRESS(ST_BASE),
         .pfn = __phys_to_pfn(ST_BASE),
         .length = SZ_4K,
         .type = MT_DEVICE},
        {
         .virtual = IO_ADDRESS(USB_BASE),
         .pfn = __phys_to_pfn(USB_BASE),
         .length = SZ_128K,
         .type = MT_DEVICE},
        {
         .virtual = IO_ADDRESS(PM_BASE),
         .pfn = __phys_to_pfn(PM_BASE),
         .length = SZ_4K,
         .type = MT_DEVICE},
        {
         .virtual = IO_ADDRESS(GPIO_BASE),
         .pfn = __phys_to_pfn(GPIO_BASE),
         .length = SZ_4K,
         .type = MT_DEVICE},
        {
         .virtual = IO_ADDRESS(ARM_LOCAL_BASE),
         .pfn = __phys_to_pfn(ARM_LOCAL_BASE),
         .length = SZ_4K,
         .type = MT_DEVICE},
};

 

  • rpi2에서 사용하는 각 device 장치의 매핑을 위해 가상 주소, 물리주소, 길이, 매핑할 메모리 타입이 배열로 제공된다.

 

IO_ADDRESS()

arch/arm/mach-bcm2709/include/mach/platform.c

/* macros to get at IO space when running virtually */
#define IO_ADDRESS(x)   (((x) & 0x00ffffff) + (((x) >> 4) & 0x0f000000) + 0xf0000000)

IO 디바이스의 물리 주소를 가상 주소로 변경한다.

  • IO 장치의 0xABxx_xxxx 물리 주소를 가상 주소로 변경하면 A를 B의 자리로 옮기고 그 자리에 F를 사용한다.
    • 0xFAxx_xxxx

 

#define BCM2708_PERI_BASE        0x3F000000
  • bcm2708 및 bcm2709 둘 다 장치의 base 물리 주소를 0x3f00_0000을 사용한다.

 

#define UART0_BASE               (BCM2708_PERI_BASE + 0x201000) /* Uart 0 */
  • UART0의 물리주소는
    • 0x3f20_1000
  • 이 주소를 가상주소로 변경하면
    • 0xf320_1000
  • 참고로 이 주소를 rpi2의 main core인 VC(Video Core)가 사용하는 bus 주소로 변경하면 다(rpi2의 device의 버스 주소는 DTB에서 사용)
    • 0x7e20_1000

 

debug_ll_addr()

arch/arm/mm/mmu.c

#ifdef CONFIG_DEBUG_LL
void __init debug_ll_io_init(void)
{
        struct map_desc map;

        debug_ll_addr(&map.pfn, &map.virtual);
        if (!map.pfn || !map.virtual)
                return;
        map.pfn = __phys_to_pfn(map.pfn);
        map.virtual &= PAGE_MASK;
        map.length = PAGE_SIZE;
        map.type = MT_DEVICE;
        iotable_init(&map, 1);
}
#endif
  • debug_ll_addr(&map.pfn, &map.virtual);
    • 디버그용 디바이스 장치의 물리 주소와 가상 주소를 알아온다.
  • map
    • 1개 페이지를 MT_DEVICE 타입으로 매핑할 구조체를 준비하였다.
  • iotable_init(&map, 1);
    • 1개 맵 구조체 정보대로 static_vmlist에 추가하여 매핑 예약을 한다.

 

debug_ll_addr()

arch/arm/kernel/debug.S

ENTRY(debug_ll_addr)
                addruart r2, r3, ip
                str     r2, [r0]
                str     r3, [r1]
                ret     lr  
ENDPROC(debug_ll_addr)
  • uart 포트의 물리주소를 r0 레지스터가 가리키는 주소에 저장한다.
  • uart 포트의 가상 주소를 r1 레지스터가 가기키는 주소에 저장한다.

 

addruart()

mach-bcm2709/include/mach/debug-macro.S

                .macro  addruart, rp, rv, tmp 
                ldr     \rp, =UART0_BASE
                ldr     \rv, =IO_ADDRESS(UART0_BASE)
                .endm

rpi2의 uart0 포트를 디버그 출력용으로 사용할 때를 예로 들었다.

  • UART0_BASE=0x3F00_0000 + 0x20_1000
  • IO_ADDRESS()
    • #define IO_ADDRESS(x)   (((x) & 0x00ffffff) + (((x) >> 4) & 0x0f000000) + 0xf0000000)
    • IO_ADDRESS(0x3f20_1000)=0xf320_1000
  • rp
    • uart0의 물리주소가 담긴다.
    • rpi2: 0x3f20_1000
  • rv
    • uart0의 가상주소가 담긴다.
    • rpi2: f320_1000

 

fill_pmd_gaps()

arch/arm/mm/mmu.c

/*
 * The Linux PMD is made of two consecutive section entries covering 2MB
 * (see definition in include/asm/pgtable-2level.h).  However a call to
 * create_mapping() may optimize static mappings by using individual
 * 1MB section mappings.  This leaves the actual PMD potentially half
 * initialized if the top or bottom section entry isn't used, leaving it
 * open to problems if a subsequent ioremap() or vmalloc() tries to use
 * the virtual space left free by that unused section entry.
 *
 * Let's avoid the issue by inserting dummy vm entries covering the unused
 * PMD halves once the static mappings are in place.
 */

static void __init fill_pmd_gaps(void)
{
        struct static_vm *svm;
        struct vm_struct *vm;
        unsigned long addr, next = 0;
        pmd_t *pmd;

        list_for_each_entry(svm, &static_vmlist, list) {
                vm = &svm->vm;
                addr = (unsigned long)vm->addr;
                if (addr < next)
                        continue;

                /*
                 * Check if this vm starts on an odd section boundary.
                 * If so and the first section entry for this PMD is free
                 * then we block the corresponding virtual address.
                 */
                if ((addr & ~PMD_MASK) == SECTION_SIZE) {
                        pmd = pmd_off_k(addr);
                        if (pmd_none(*pmd))
                                pmd_empty_section_gap(addr & PMD_MASK);
                }

                /*
                 * Then check if this vm ends on an odd section boundary.
                 * If so and the second section entry for this PMD is empty
                 * then we block the corresponding virtual address.
                 */
                addr += vm->size;
                if ((addr & ~PMD_MASK) == SECTION_SIZE) {
                        pmd = pmd_off_k(addr) + 1;
                        if (pmd_none(*pmd))
                                pmd_empty_section_gap(addr);
                }

                /* no need to look at any vm entry until we hit the next PMD */
                next = (addr + PMD_SIZE - 1) & PMD_MASK;
        }
}
  • list_for_each_entry(svm, &static_vmlist, list) {
    • static_vmlist를 루프를 돌며 수행
  • if ((addr & ~PMD_MASK) == SECTION_SIZE) {
    • 요청 가상 주소가 홀수 섹션인 경우 즉 pmd가 홀수 엔트리인경우
  • pmd = pmd_off_k(addr);
    • 가상 주소로 pmd 엔트리 가상 주소를 알아온다.
  • if (pmd_none(*pmd)) pmd_empty_section_gap(addr & PMD_MASK);
    • 만일 pmd 엔트리가 없는 경우 해당 1M를 그 전 섹션 주소로 매핑 요청(예약)을 한다.
    • 예) 0x3f10_0000 (홀수 섹션)의 pmd 엔트리가 없는 경우 0x3f00_0000(pair 구성된 짝수 섹션 주소)로 매핑 요청을 한다.
  • addr += vm->size;
    • 이 번에는 사이즈를 더하여 끝 가상 주소를 알아온다.
  •  if ((addr & ~PMD_MASK) == SECTION_SIZE) {
    • 역시 끝  가상 주소가 홀수 섹션인 경우 즉 pmd가 홀수 엔트리인 경우
  • pmd = pmd_off_k(addr) + 1;
    • 가상 주소로 pmd 엔트리 주소를 알아온 후 1을 더해 다음 pmd 엔트리 주소를 알아온다.
  • if (pmd_none(*pmd)) pmd_empty_section_gap(addr);
    • 해당 주소의 pmd 엔트리가 비어 있는 경우 해당 주소로 매핑 요청(예약)을 한다.
  • next = (addr + PMD_SIZE – 1) & PMD_MASK;
    • 다음 찾을 주소는 현재 매핑한 주소를 pmd 크기(2M) round up 한 주소 이상을 찾게 설정한다.
    • 예) addr=0x3f10_5000
      • next=3f20_0000이 되고 다음 엔트리의 주소는 3f20_0000 이상 부터 매핑확인을 한다.

아래 그림과 같이 LPAE를 사용하지 않는 32bit ARM에서는 pmd 엔트리들이 항상 페어를 이루어야 하기 때문에 영역의 시작 주소와 끝 주소를 체크하여 pmd 엔트리가 짝을 이루지 못한 엔트리들에 대해서도 나중에 매핑을 할 수 있도록 static_vmlist에 추가한다.

fill_pmd_gaps-1

 

pmd_empty_section_gap()

arch/arm/mm/mmu.c

static void __init pmd_empty_section_gap(unsigned long addr)
{
        vm_reserve_area_early(addr, SECTION_SIZE, pmd_empty_section_gap);
}
  • 요청 영역을 static_vmlist에 추가한다.

 

pci_reserve_io()

arch/arm/mm/mmu.c

#if defined(CONFIG_PCI) && !defined(CONFIG_NEED_MACH_IO_H)
static void __init pci_reserve_io(void)
{
        struct static_vm *svm;

        svm = find_static_vm_vaddr((void *)PCI_IO_VIRT_BASE);
        if (svm)
                return;

        vm_reserve_area_early(PCI_IO_VIRT_BASE, SZ_2M, pci_reserve_io);
}
#else
#define pci_reserve_io() do { } while (0)
#endif
  • svm = find_static_vm_vaddr((void *)PCI_IO_VIRT_BASE);
    • static_vmlist에 PCI IO 주소가 이미 등록이 되어 있는지 확인하여 등록 되어 있는 경우 함수를 빠져나간다.
  • vm_reserve_area_early(PCI_IO_VIRT_BASE, SZ_2M, pci_reserve_io);
    • 등록되어 있지 않은 경우 PCI IO 주소로 2M를 매핑 요청(예약)을 한다.
    • PCI_IO_VIRT_BASE=0xfee0_0000

 

벡터 매핑

devicemaps_init-2

  • MT_LOW_VECTORS는 read only, MT_HIGH_VECTORS는 read/write access 권한을 갖는 메모리 속성
  • CONFIG_KUSER_HELPERS 커널 옵션을 사용하지는 않는 경우 MT_HIGH_VECTORS로 매핑하지 않는다.

 

find_static_vm_vaddr()

arch/arm/mm/ioremap.c

struct static_vm *find_static_vm_vaddr(void *vaddr)
{
        struct static_vm *svm;
        struct vm_struct *vm;

        list_for_each_entry(svm, &static_vmlist, list) {
                vm = &svm->vm;

                /* static_vmlist is ascending order */
                if (vm->addr > vaddr)
                        break;

                if (vm->addr <= vaddr && vm->addr + vm->size > vaddr)
                        return svm;
        }

        return NULL;
}

static_vmlist의 등록된 엔트리들의 영역 중 요청 주소가 포함되는 경우 해당 엔트리를 리턴한다.

  • list_for_each_entry(svm, &static_vmlist, list) {
    • static_vmlist 엔트리를 루프를 돌며 하나씩 가져온다.
    • if (vm->addr > vaddr)
      • 엔트리의 영역 시작 주소가 요청 주소보다 큰 경우 null을 리턴
    •  if (vm->addr <= vaddr && vm->addr + vm->size > vaddr)
      • 엔트리의 영역 범위에 요청 주소가 있는 경우 현재 엔트리를 리턴한다.

 

BCM2709 IO 테이블 매핑

bcm2709_map_io()

arch/arm/mm/mmu.c

void __init bcm2709_map_io(void)
{
        iotable_init(bcm2709_io_desc, ARRAY_SIZE(bcm2709_io_desc));
}

bcm2709용 io 주소 페이지들을 static 매핑하도록 요청한다.

 

iotable_init()

arch/arm/mm/mmu.c

/*
 * Create the architecture specific mappings
 */
void __init iotable_init(struct map_desc *io_desc, int nr)
{
        struct map_desc *md;
        struct vm_struct *vm;
        struct static_vm *svm;

        if (!nr)
                return;
                                  
        svm = early_alloc_aligned(sizeof(*svm) * nr, __alignof__(*svm));

        for (md = io_desc; nr; md++, nr--) {
                create_mapping(md);

                vm = &svm->vm;
                vm->addr = (void *)(md->virtual & PAGE_MASK);
                vm->size = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK));
                vm->phys_addr = __pfn_to_phys(md->pfn);
                vm->flags = VM_IOREMAP | VM_ARM_STATIC_MAPPING;
                vm->flags |= VM_ARM_MTYPE(md->type);
                vm->caller = iotable_init;
                add_static_vm_early(svm++);
        }
}

bcm2709용 io 주소 페이지들을 static 매핑하도록 요청한다.

  • 코드 라인 13에서 요청한 갯 수만큼 static_vm 구조체를 할당해온다.
  • 코드 라인 15~16에서 요청한 갯 수 만큼 io 디스크립터들에 대해  루프를 돌며 고정 매핑을 수행한다.
    • 매핑 시 vmalloc 공간 중 상위 부분에 고정 매핑 한다.
  • 코드 라인 18~21에서 주소, 사이즈, 물리 주소 등을 svm에 대입한다.
  • 코드 라인 22에서 vm_struct 구성 시 VM_IOREMAP, VM_ARM_STATIC_MAPPING을 포함시켜 io용 static 매핑을 하였음을 알린다.
  • 코드 라인 23에서 매핑 디스크립터의 타입을 20bit 좌측으로 쉬프트하여 플래그에 대입한다.
  • 코드 라인 24에서 caller로 iotable_init() 함수에서 추가하였음을 알린다.
  • 코드 라인 25에서 &static_vmlist에서 가상 주소순으로 static_vm을 해당 위치에 insert 한다.
    • 가장 높은 가상 주소가 리스트의 선두이다.

 

bcm2709_io_desc IO 맵 디스크립터

arch/arm/mm/mmu.c

static struct map_desc bcm2709_io_desc[] __initdata = {
        {
         .virtual = IO_ADDRESS(ARMCTRL_BASE),
         .pfn = __phys_to_pfn(ARMCTRL_BASE),
         .length = SZ_4K,
         .type = MT_DEVICE},
        {
         .virtual = IO_ADDRESS(UART0_BASE),
         .pfn = __phys_to_pfn(UART0_BASE),
         .length = SZ_4K,
         .type = MT_DEVICE},
        {
         .virtual = IO_ADDRESS(UART1_BASE),
         .pfn = __phys_to_pfn(UART1_BASE),
         .length = SZ_4K,
         .type = MT_DEVICE},
        {
         .virtual = IO_ADDRESS(DMA_BASE),
         .pfn = __phys_to_pfn(DMA_BASE),
         .length = SZ_4K,
         .type = MT_DEVICE},
        {
         .virtual = IO_ADDRESS(MCORE_BASE),
         .pfn = __phys_to_pfn(MCORE_BASE),
         .length = SZ_4K,
         .type = MT_DEVICE},
        {
         .virtual = IO_ADDRESS(ST_BASE),
         .pfn = __phys_to_pfn(ST_BASE),
         .length = SZ_4K,
         .type = MT_DEVICE}, 
        {
         .virtual = IO_ADDRESS(USB_BASE),
         .pfn = __phys_to_pfn(USB_BASE),
         .length = SZ_128K,
         .type = MT_DEVICE},
        {        
         .virtual = IO_ADDRESS(PM_BASE),
         .pfn = __phys_to_pfn(PM_BASE),
         .length = SZ_4K,
         .type = MT_DEVICE},
        {
         .virtual = IO_ADDRESS(GPIO_BASE),
         .pfn = __phys_to_pfn(GPIO_BASE),
         .length = SZ_4K,
         .type = MT_DEVICE},
        {
         .virtual = IO_ADDRESS(ARM_LOCAL_BASE),
         .pfn = __phys_to_pfn(ARM_LOCAL_BASE),
         .length = SZ_4K,
         .type = MT_DEVICE},
};

BCM2709에서 사용할 IO에 대한 고정 매핑 항목들이다.

  • 예) ARMCTL_BASE
    • Video Core 물리 주소
      • 0x7e00_0000
    • ARM 물리 주소
      • 0x3F00_0000
        • 주의) rpi: 0x2000_0000 -> rpi2: 0x3f00_0000
    • ARM 고정 매핑할 가상 주소
      • 0xf300_0000

 

구조체 및 전역 변수

map_desc 구조체

arch/arm/include/asm/mach/map.h

struct map_desc {
        unsigned long virtual;
        unsigned long pfn;
        unsigned long length;
        unsigned int type;
};

 

참고