vmap()과 유사하게 ioremap() API를 통해서 디바이스가 제공하는 메모리 또는 I/O 포트 주소를 vmalloc영역에 매핑한다.
디바이스 접근 및 사용을위한 가상주소에 대한 매핑은 early I/O memory mapping과 I/O memory mapping 크게 2가지로 나뉜다.
1. early I/O memory mapping
특정 BIOS에 접근하여 각종 메모리 및 디바이스 정보 가져와서 매핑 처리하는 등의 경우를 위한 부팅 극초반을 위해 제공되는 기능이다.
Ex) ACPI 테이블 접근을 통한 각종 디바이스 설정, EFI 테이블에 접근하여 각종 디바이스 설정, 일부 디바이스 특정 설정 정보를 읽어오는 경우
1) early_ioremap_init()
early I/O memory mapping의 경우 극초반에 이루어지기 때문에 fix_map을 통해서 가상메모리 할당이 이루어진다. 이를 위해서 가장 먼저 fixmap 7개 slot을 slot_virt[] 배열로 가상주소에 할당할 수 있도록 준비과정을 진행한다.
slot_virt[] 배열에 7개의 fixmap entry를 할당하고 각각의 entry는 256KB의 크기를 가진다. 해당 배열이 할당되는 공간은 fixmap area 중 FIX_BITMAP_END 부분이다.
이렇게 할당된 slot_virt[] 배열에는 prev_map[] 배열과 prev_size[] 배열을 추가로 사용함으로써 fixmap영역에 할당을 진행한다.
prev_map[]은 할당할 차레의 FIX_BITMAP_END의 entry를 가리키고 있어 해당 주소로 접근하여 할당을진행하고 할당한 size는 prev_size[]에서 관리한다. 위에서 언급한바와 같이 7개의 entry가 생성된 상황이라면 early I/O memory mapping이 가능한 device갯수는 7개로 생각하면 된다.
2) __early_ioremap()
위에서 early_ioremap_init()을 통해서 할당할 fixmap 영역을 만들었다면 해당 함수는 실제적으로 device early I/O memory mapping을 진행하는 함수이다.
static void __init __iomem * __early_ioremap(resource_size_t phys_addr, unsigned long size, pgprot_t prot) { unsigned long offset; resource_size_t last_addr; unsigned int nrpages; enum fixed_addresses idx; int i, slot; WARN_ON(system_state >= SYSTEM_RUNNING); slot = -1; for (i = 0; i < FIX_BTMAPS_SLOTS; i++) { if (!prev_map[i]) { slot = i; break; } } if (WARN(slot < 0, "%s(%pa, %08lx) not found slot\n", __func__, &phys_addr, size)) return NULL; /* Don't allow wraparound or zero size */ last_addr = phys_addr + size - 1; if (WARN_ON(!size || last_addr < phys_addr)) return NULL; prev_size[slot] = size; /* * Mappings have to be page-aligned */ offset = offset_in_page(phys_addr); phys_addr &= PAGE_MASK; size = PAGE_ALIGN(last_addr + 1) - phys_addr; /* * Mappings have to fit in the FIX_BTMAP area. */ nrpages = size >> PAGE_SHIFT; if (WARN_ON(nrpages > NR_FIX_BTMAPS)) return NULL; /* * Ok, go for it.. */ idx = FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*slot; while (nrpages > 0) { if (after_paging_init) __late_set_fixmap(idx, phys_addr, prot); else __early_set_fixmap(idx, phys_addr, prot); phys_addr += PAGE_SIZE; --idx; --nrpages; } WARN(early_ioremap_debug, "%s(%pa, %08lx) [%d] => %08lx + %08lx\n", __func__, &phys_addr, size, slot, offset, slot_virt[slot]); prev_map[slot] = (void __iomem *)(offset + slot_virt[slot]); return prev_map[slot]; } |
실제적으로 fixmap에 할당하는 역할을 진행하는 함수이다. slot배열을 통해서 할당할 entry를 반복문으로 찾고 할당할 주소 값과 사이즈를 앞서 언급한 prev배열에 저장한다.
이후에 할당할 데이터가 entry 크기인 256KB보다 더 큰지 확인하여 안정성을 체크하고 이후 _late_set_fixmap과 _early_set_fixmap 둘 중 하나의 function을 통해서 실제적으로 fixmap에 할당을 진행한다. 할당하는 실제 함수는 __set_fixmap() function이다.
마지막으로 __early_ioremap() function임에도 _late_set_fixmap fucntion을 분기를 통해 지원하는 이유는 타이밍상 page_init()이 진행되어 early가 필요하지 않은경우 early가 아닌 정상적인 ioremap() function과 같은 방식으로 할당을 진행하기 위해 지원한다.
<page_init() fucntion이후 early가아닌 ioremap()을 사용하며 이 때 기준이 되는 flag가 after_paging_init flag이다.>
2. I/O memory mapping
ioremap()은 빈 공간의 vmalloc영역을 찾아서 device관련 가상주소 메모리를 할당한다. 단, 할당을 진행할 때 5개의 함수가 존재하고 함수에 따라서 캐시 지원 및 특정 일반적인 메모리타입, device타입등의 구분이 되어있다. 즉 device상황과 성격에 따라 필요한 ioremap()함수를 호출해서 할당을 진행한다.
ioremap() | 캐시 사용 x, PROP_DEVICE_nGnRE |
ioremap_nocache() | 캐시 사용 x, PROP_DEVICE_nGnRE |
ioremap_wc() | 캐시 사용 x, PROT_NORMAL_NC |
ioremap_wt() | 캐시 사용 x, PROT_DEVICE_nGnRE |
ioremap_cache() | 캐시 사용 가능, PROT_NORMAL |
위 매크로 함수 중 ioremap(), ioremap_nocache(), ioremap_wc(), ioremap_wt()의 경우 실제적으로 __ioremap()을 호출해서 할당을 진행하고 ioremap_cache()의 경우 ioremap_cache() function을 사용해서 할당한다.
실제적으로 할당을 진행하는 각 함수의 역할은 동일하다 physical address size만큼을 vmalloc영역에 캐시 가능한 일반 메모리 타입으로 매핑하고 이에 대한 가상주소 값을 리턴해준다. 이 공통의 역할을 수행하는 실질적인 함수는 ioremap_caller()이다. 즉 위 두함수는 공통적으로 해당 function을 호출한다. 그리고 해당 함수는 아래 경로에 존재하는 __arm_ioremap_pfn_caller()를 호출한다.
- arch/arm/mm/ioremap.c
static void __iomem * __arm_ioremap_pfn_caller(unsigned long pfn, unsigned long offset, size_t size, unsigned int mtype, void *caller) { const struct mem_type *type; int err; unsigned long addr; struct vm_struct *area; phys_addr_t paddr = __pfn_to_phys(pfn); #ifndef CONFIG_ARM_LPAE /* * High mappings must be supersection aligned */ if (pfn >= 0x100000 && (paddr & ~SUPERSECTION_MASK)) return NULL; #endif type = get_mem_type(mtype); if (!type) return NULL; /* * Page align the mapping size, taking account of any offset. */ size = PAGE_ALIGN(offset + size); /* * Try to reuse one of the static mapping whenever possible. */ if (size && !(sizeof(phys_addr_t) == 4 && pfn >= 0x100000)) { struct static_vm *svm; svm = find_static_vm_paddr(paddr, size, mtype); if (svm) { addr = (unsigned long)svm->vm.addr; addr += paddr - svm->vm.phys_addr; return (void __iomem *) (offset + addr); } } /* * Don't allow RAM to be mapped with mismatched attributes - this * causes problems with ARMv6+ */ if (WARN_ON(memblock_is_map_memory(PFN_PHYS(pfn)) && mtype != MT_MEMORY_RW)) return NULL; area = get_vm_area_caller(size, VM_IOREMAP, caller); if (!area) return NULL; addr = (unsigned long)area->addr; area->phys_addr = paddr; #if !defined(CONFIG_SMP) && !defined(CONFIG_ARM_LPAE) if (DOMAIN_IO == 0 && (((cpu_architecture() >= CPU_ARCH_ARMv6) && (get_cr() & CR_XP)) || cpu_is_xsc3()) && pfn >= 0x100000 && !((paddr | size | addr) & ~SUPERSECTION_MASK)) { area->flags |= VM_ARM_SECTION_MAPPING; err = remap_area_supersections(addr, pfn, size, type); } else if (!((paddr | size | addr) & ~PMD_MASK)) { area->flags |= VM_ARM_SECTION_MAPPING; err = remap_area_sections(addr, pfn, size, type); } else #endif err = ioremap_page_range(addr, addr + size, paddr, __pgprot(type->prot_pte)); if (err) { vunmap((void *)addr); return NULL; } flush_cache_vmap(addr, addr + size); return (void __iomem *) (offset + addr); } |
할당을 진행할 때는 page size별로 진행을 하되 만약 재사용가능한 page table영역이 존재한다면 해당 영역을 이용한다. page 재활용을 높여서 page fault가 발생할 위험을 줄이는것으로 보인다.
그리고 page할당을 위한 offset과 page마지막 주소 값(page단위이기 때문에 실제 할당을 원하는 데이터 크기와 할당하는 page의 사이즈는 다르다. 때문에 page 할당기준으로 마지막 addr값을 last_addr로 구해서 사용한다)
위와 같이 page 단위로 addr, size등을 모두 계산하고 실제 물리주소 할당을 진행한다. 할당까지 마친 이후에는 현재 가상주소를 사용하는 상황이기 때문에 가상 주소와 물리주소를 매핑해주는 작업을 진행해야한다. vmalloc에 prot속성으로 매핑을하는데 해당 역할을 진행하는 함수로는 ioremap_page_range() funtion을 이용한다. 해당 함수를 타고타고 들어가면 실제로는 아래 vmap_range_noflush()로가게되고 page table 매핑을 진행한다.
static int vmap_range_noflush(unsigned long addr, unsigned long end, phys_addr_t phys_addr, pgprot_t prot, unsigned int max_page_shift) { pgd_t *pgd; unsigned long start; unsigned long next; int err; pgtbl_mod_mask mask = 0; might_sleep(); BUG_ON(addr >= end); start = addr; pgd = pgd_offset_k(addr); do { next = pgd_addr_end(addr, end); err = vmap_p4d_range(pgd, addr, next, phys_addr, prot, max_page_shift, &mask); if (err) break; } while (pgd++, phys_addr += (next - addr), addr = next, addr != end); if (mask & ARCH_PAGE_TABLE_SYNC_MASK) arch_sync_kernel_mappings(start, end); return err; } |
기본적으로 addr을 바탕으로 시작 주소와, page table offset값을 가져온다 그리고 pgd table entry를 가져와서 마지막 뒤에 붙여서 연결 해주는 작업을 진행한다. page단위로 할당을 진행하기 때문에 반복문을 통해 하나의 page보다 size가 큰 데이터의 경우 여러 page를 할당한다.
vmap_p4d_range는 다음 page table에 같은 방식으로 할당 하기 위한 function이다.
pgd -> pud -> pmd -> pte 순으로 진행이되면 결국 pte에서 물리주소로 매핑을 진행할 것이다.
/*** Page table manipulation functions ***/ static int vmap_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end, phys_addr_t phys_addr, pgprot_t prot, unsigned int max_page_shift, pgtbl_mod_mask *mask) { pte_t *pte; u64 pfn; unsigned long size = PAGE_SIZE; pfn = phys_addr >> PAGE_SHIFT; pte = pte_alloc_kernel_track(pmd, addr, mask); if (!pte) return -ENOMEM; do { BUG_ON(!pte_none(*pte)); #ifdef CONFIG_HUGETLB_PAGE size = arch_vmap_pte_range_map_size(addr, end, pfn, max_page_shift); if (size != PAGE_SIZE) { pte_t entry = pfn_pte(pfn, prot); entry = pte_mkhuge(entry); entry = arch_make_huge_pte(entry, ilog2(size), 0); set_huge_pte_at(&init_mm, addr, pte, entry); pfn += PFN_DOWN(size); continue; } #endif set_pte_at(&init_mm, addr, pte, pfn_pte(pfn, prot)); pfn++; } while (pte += PFN_DOWN(size), addr += size, addr != end); *mask |= PGTBL_PTE_MODIFIED; return 0; } |
위와 같이 pte에서 결국에는 할당을 진행한다. 물론 PAGE_SIZE가아니라면 PAGE_SIZE를 맞춰서 매핑을 진행하는 것으로 보인다.
실제적으로 매핑을 진행하고 pte page table에 할당을 진행하는 함수는 set_pte_at() function이다.
3. vmemmap
메모리 맵을 관리하는 것은 결국에 페이지 속성을 관리하는 배열을 만들어서 페이지 디스크립터 주소들을 할당하고 연결해서 사용하는 방법이다.
방법은 크게 두 가지로 연속적으로 할당을 진행하는 방법과 비연속적이더라도 부분부분 빈 공간을 사용해서 할당하는 방법이 있다.
두 가지 방법은 장단점을 가지고 있으며 연속적인 경우 메모리 할당에 많은 메모리 공간이 필요하고 비연속적인 방법은 속도문제가 있을 것이다.
flat 모델 : 연속으로 할당하는 방법으로 빠른속도의 장점이 있으나 할당에 많은 메모리 필요 sparse 모델 : 공간 효율을 높일 수 있으나 찾아가는데 시간이 더 많이 필요한 성능상 불리한 점이 있다. |
이러한 문제를 해결하기 위해 고안된 메모리 모델이 vmemmap이다. vmemmap은 sparse 메모리 모델 즉 분리 메모리 모델에서 속도를 개선하기 위해 page size단위로 할당하는 것을 section단위로 4K -> 2M으로 높이는 방법이다.
- ref
코드로 알아보는 ARM 리눅스 커널
'OS > Linux' 카테고리의 다른 글
[Linux Kernel] NUMA 메모리 관리 기법 (0) | 2022.01.31 |
---|---|
[Linux Kernel] Kernel 분석(v5.14.16) - memblock (0) | 2022.01.23 |
[Linux Kernel] Kernel 분석(v5.14.16) - Page Table (2) (0) | 2021.12.12 |
[Linux Kernel] Kernel 분석(v5.14.16) - Page Table (1) (0) | 2021.12.06 |
[Linux Kernel] objdump, arm-eabi-addr2line (0) | 2021.12.04 |