chipset: MSM8X25Q
Codebase: Android4.1
Kernel: 3.4.0
当你需要将高端页面长期映射到内核空间的时候,就要使用Kmap函数来实现,即高端内存永久映射。这样避免页表和TLB的更新而导致资源的占用。
使用的时候一般先通过alloc_page(__GFP_HIGHMEM)申请一个page,然后将这个page传给kmap,kmap会建立这个page的页表项,并返回一个虚拟地址供操作。
Pkmap_count是一个长度为LAST_PKMAP的数组,长度为512,,每个元素对应一个永久映射的页,所以可以映射512*4k = 2M页大小。
static int pkmap_count[LAST_PKMAP]; #define LAST_PKMAP PTRS_PER_PTE #define PTRS_PER_PTE 512
Pkmap_count元素的值不同时的意义对应如下:
0:相关也还没使用
1:表示页已经被映射,但是由于TLB没被更新而无法使用。
>=2:为2时,表示内核有一处使用该映射页。为n时,表示有n-1处使用该页。
另外,内核使用struct page_address_map来保存页和虚拟地址之前的关系。struct page_address_map { struct page *page; void *virtual; struct list_head list; };
另外,高端内存永久映射是通过page_address_htable这个变量来管理的,结构为:
static struct page_address_slot { struct list_head lh; /* List of page_address_maps */ spinlock_t lock; /* Protect this bucket's list */ }
它的管理机制使用了哈希表,对应的函数是page_slot(),对于哈希原理,可自行查资料学习。
static struct page_address_slot *page_slot(const struct page *page) { return &page_address_htable[hash_ptr(page, PA_HASH_ORDER)]; }
系统开机启动的时候有如下调用:
Start_kernel() –> setup_arch() -> paging_init() -> kmap_init() static void __init kmap_init(void) { #ifdef CONFIG_HIGHMEM /*该变量保存了PKMAP_BASE对应的页表项地址,PKMAP_BASE为永久映射的起始地址。*/ pkmap_page_table = early_pte_alloc_and_install(pmd_off_k(PKMAP_BASE), PKMAP_BASE, _PAGE_KERNEL_TABLE); #endif }
调用是kmap().
void *kmap(struct page *page) { /*会sleep,所以不能用于中断上下文*/ might_sleep(); /*如果是低端内存,那么返回page对应虚拟地址*/ if (!PageHighMem(page)) return page_address(page); /*否则执行高端内存映射*/ return kmap_high(page); } EXPORT_SYMBOL(kmap);
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; /*页使用lh链表来管理。*/ 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; }
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); /*当前页对应的元素值加1. PKMAP_NR表示相对于PKMAP_BASE的偏移。 #define PKMAP_NR(virt) (((virt) - PKMAP_BASE) >> PAGE_SHIFT) */ pkmap_count[PKMAP_NR(vaddr)]++; /*<2肯定不正常了,因为1表示创建完成,如果跑到这里,2就表示有模块使用此映射了。*/ BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2); unlock_kmap(); /*返回映射之后的虚拟地址*/ return (void*) vaddr; }
static inline unsigned long map_new_virtual(struct page *page) { unsigned long vaddr; int count; start: count = LAST_PKMAP; /* Find an empty entry */ for (;;) { /* last_pkmap_nr 记录当前映射的页数,也可以认为是最后使用的位置。*/ last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK; /*为0表示查找完一轮了,这时它会去刷新pkmap_count的值为1的TLB。*/ if (!last_pkmap_nr) { flush_all_zero_pkmaps(); count = LAST_PKMAP; } /*找到空闲区了*/ if (!pkmap_count[last_pkmap_nr]) break; /* Found a usable entry */ /*未找到,继续找下一个*/ if (--count) continue; /*找了LAST_PKMAP 个之后就睡眠,等待其他模块unmap*/ { DECLARE_WAITQUEUE(wait, current); __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; } } /*从这里看到,永久映射的虚拟地址是继续PKMAP_BASE加上一个offset实现的。*/ vaddr = PKMAP_ADDR(last_pkmap_nr); /*修改内核页表,将该页与页表进行关联,但还未更新TLB。*/ set_pte_at(&init_mm, vaddr, &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot)); /*表示创建映射页表完成*/ pkmap_count[last_pkmap_nr] = 1; /*将vaddr加入到pkmap_page_table 哈希表和struct page_address_map中管理以供后面调用page_address.*/ set_page_address(page, (void *)vaddr); return vaddr; }
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; /*0的时候还没创建映射,不用管。 >=2的时候表示还有模块在使用,也不处理。 1表示已经没有模块使用了,即表示已经unmap了,但是页表还没释放, 这里就是针对页进行释放。*/ if (pkmap_count[i] != 1) continue; pkmap_count[i] = 0; page = pte_page(pkmap_page_table[i]); pte_clear(&init_mm, (unsigned long)page_address(page), &pkmap_page_table[i]); set_page_address(page, NULL); need_flush = 1; } /*有改变,刷新永久映射区全部TLB。*/ if (need_flush) flush_tlb_kernel_range(PKMAP_ADDR(0), PKMAP_ADDR(LAST_PKMAP)); }