특징
- 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가 소요된다.
- 2가지 간접 매핑
- 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)를 교체하는 것으로 매핑한다.
- ARM에서의 pkmap area:
- 아키텍처마다 pkmap area와 fixmap area의 위치가 다르며 영역 크기 또한 다르다.
- 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을 보여준다.
아래 그림은 HIGHMEM 영역의 어느 한 페이지를 매핑한 경우의 연관된 흐름을 보여준다.
- pkmap_page_table[]은 실제 매핑용 L2 페이지 테이블이다.
- page_address_htable[]을 사용하여 128개의 해쉬 방법을 사용한다.
- 해시 슬롯에는 리스트 구조로 페이지를 추가 삭제하여 관리한다.
매핑과 해제
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 전역 구조체 변수를 리턴한다.
- #define NODE_DATA(nid) (&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
- #define SECTIONS_PGOFF ((sizeof(unsigned long)*8) – SECTIONS_WIDTH)
- ZONES_WIDTH
- 존 수에 따라 달라지는데 1개 존은 0, 2개 존은 1, 3~4개 존은 2, 그외는 에러
- rpi2: 2개 존을 사용하므로 1
- rpi2의 ZONES_PGOFF=31
- #define NODES_PGOFF (SECTIONS_PGOFF – NODES_WIDTH)
- rpi2의 ZONES_PGSHIFT=31
- #define ZONES_PGOFF (NODES_PGOFF – ZONES_WIDTH)
- page->flags 값에서 zone 비트만 우측으로 쉬프트하고 마스크하여 추출한다.
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)
- ARM에서는 highmem을 위한 최대 매핑 엔트리 수를 리턴한다.
- last_pkmap_nr = get_next_pkmap_nr(color);
- 마지막에 사용된 엔트리 번호에 1을 더한 번호를 리턴한다.
- 0~511 까지를 반복한다.
- 마지막에 사용된 엔트리 번호에 1을 더한 번호를 리턴한다.
- 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를 리턴한다.
참고
- page_address_init() | 문c
- Fixmap | 문c
- kmap(): Mapping Arbitrary Pages Into Kernel VM
- High Memory Management
- What does the Virtual kernel Memory Layout in dmesg imply?
- Understanding The Linux Virtual Memory Manager – 다운로드
- HIGH MEMORY HANDLING | kernel.org
- High Memory | Merculy