一、永久映射和临时映射:
2.1、永久映射:
依然把前面描述vmalloc的文章的图搬上来:
high_memory |
VMALLOC_START |
Vmalloc 1 |
4KB隔离带 |
Vmalloc 2 |
4KB隔离带 |
Vmalloc 3 |
4KB隔离带 |
…….. |
VMALLOC_END |
PKMAP_BASE |
8MB隔离带 |
Vmalloc N |
8KB隔离带 |
低端(物理)内存映射 |
FIXADDR_START |
永久映射区 |
临时映射区 |
FIXADDR_TOP |
事实上这个图有些误导,永久映射区始于PKMAP_BASE,但并非结束于FIXADDR_START,在文件arch/arm/include/asm/highmem.h中描述了永久映射区到底是在哪里:
#define PKMAP_BASE (PAGE_OFFSET - PMD_SIZE)
PKMAP_BASE是永久映射区的起始,在这里的arm设备中,PKMAP_BASE是在内核用户分界点之下2MB处,下面这个宏LAST_PKMAP标识永久映射区有多少个条目,注意每个条目是可以映射1页:
#define LAST_PKMAP PTRS_PER_PTE
在这里的arm设备中,LAST_PKMAP值为512,即512个条目,每个条目映射一页,即可最多映射4KB * 512 = 2MB大小,所以永久映射区的范围是[PAGE_OFFSET – 2MB,PAGE_OFFSET],对这里的arm设备就是[0xBFE00000,0xC0000000];这也证明了前面文章说的,高端内存空间不一定就是在high_memory之上的地址空间;
要清楚一个问题就是,一般来说,永久映射包括临时映射是干什么用的,可以说在物理内存不大时(如这里的arm设备是256MB)并且物理地址起始偏移(PHYS_OFFSET)不很大基本上没有意义,因为所有物理内存都可以映射在内核低端内存地址空间,使用简单的偏移即可实现物理地址和虚拟地址的映射,根本用不着什么永久映射,永久映射包括临时映射事实上是在内核空间无法完全容纳物理内存时(比如超过1G的物理内存)才会显出作用;
不论是永久映射还是临时映射,本质上都是建立一个二级映射,把物理地址和虚拟地址对应起来,需要注意的是两者的区别,永久映射是有可能睡眠的,原因在后面描述时会很明显发现,而临时映射不会睡眠,另外永久映射并非一经映射无法解除,临时映射的特点顾名思义,可以不断的覆盖之前的映射关系;
下面看看永久映射的情况:
void *kmap(struct page *page)
{
might_sleep();
if (!PageHighMem(page))
return page_address(page);
return kmap_high(page);
}
kmap函数是体系结构自己实现,这里的arm设备就是在arch/arm/mm/highmem.c文件中,kmap函数的作用是:建立这个属于高端内存的物理页与永久内核映射区的映;
kmap的参数page是要映射的高端物理页地址,如果它是低端物理页,则直接返回它对应的虚拟地址;如果它确实是高端物理页地址,则调用函数kmap_high建立一个高端内存物理页的虚拟映射即永久映射:
void *kmap_high(struct page *page)
{
unsigned long vaddr;
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;
}
首先查看这个高端物理页是否已经做过高端映射了,通过函数page_address查看,
void *page_address(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;
}
注意高端地址是通过哈希链表管理的,这里找出该物理页所在的哈希链表的表头,然后通过遍历找到page描述符所映射的虚拟地址来判断是否映射过,若映射过则返回其映射的虚拟地址,否则返回NULL;
回到函数kmap_high,如果从page_address返回NULL,说明这个高端页不存在高端映射,应该新建一个映射,即为这个高端页分配一个虚拟地址,调用函数map_new_virtual:
static inline unsigned long map_new_virtual(struct page *page)
{
unsigned long vaddr;
int count;
start:
count = LAST_PKMAP;
for (;;) {
last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;
if (!last_pkmap_nr) {
flush_all_zero_pkmaps();
count = LAST_PKMAP;
}
if (!pkmap_count[last_pkmap_nr])
break;
if (--count)
continue;
{
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();
if (page_address(page))
return (unsigned long)page_address(page);
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;
}
全局变量pkmap_count和last_pkmap_nr用于管理全部512个永久映射条目,这里先在for循环中遍历查找一个空白条目,如果找不到说明全部512个条目都正在映射着,如再需映射需要等其中某一个释放才行,这就需要等这个事件的发生,所以会睡眠,这就是为什么在中断上下文等不能睡眠的地方不能使用永久映射的具体原因;
在找到空白条目后会退出for循环创建映射,通过函数PKMAP_ADDR在永久映射区找一个位置,其实全局变量last_pkmap_nr保存当前永久映射的个数,所以下面的源码就很好解释了,就是移位:
#define PKMAP_ADDR(nr) (PKMAP_BASE + ((nr) << PAGE_SHIFT))
接下来创建这个映射关系,创建的依然是二级映射,只是二级页表不用动态申请了,全局变量pkmap_page_table是二级页表条目的数组。
最后把这个物理页地址和虚拟地址组成的映射节点(结构类型struct page_address_map)插入到管理永久映射的哈希链表中,调用函数set_page_address,注意不论是创建映射还是释放映射都会调用这个函数:
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) {
BUG_ON(list_empty(&page_address_pool));
spin_lock_irqsave(&pool_lock, flags);
pam = list_entry(page_address_pool.next,
struct page_address_map, list);
list_del(&pam->list);
spin_unlock_irqrestore(&pool_lock, flags);
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 {
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);
spin_lock_irqsave(&pool_lock, flags);
list_add_tail(&pam->list, &page_address_pool);
spin_unlock_irqrestore(&pool_lock, flags);
goto done;
}
}
spin_unlock_irqrestore(&pas->lock, flags);
}
done:
return;
}
2.2、临时映射:
临时映射和永久映射差别不是很大,只是说临时映射顾名思义,比较临时,和永久相反,永久映射若需要修改映射关系需要先释放映射再创建新的映射关系,而临时映射就是直接再映射一次把前面的映射关系覆盖掉就行了,确实是比较临时;所以临时映射不会睡眠,它不会像永久映射,可能产生条目满需要等待其他条目被释放的情况,如果条目满了它只需覆盖掉一个就行了;
对于arm体系结构,在文件arch/arm/include/asm/fixmap.h文件中定义了临时映射的地址范围:
#define FIXADDR_START 0xfff00000UL
#define FIXADDR_TOP 0xfffe0000UL
#define FIXADDR_SIZE (FIXADDR_TOP - FIXADDR_START)
#define FIX_KMAP_BEGIN 0
#define FIX_KMAP_END (FIXADDR_SIZE >> PAGE_SHIFT)
临时映射地址范围为[0xfff00000,0xfffe0000],长度为896KB即224页的范围,显然FIX_KMAP_BEGIN和FIX_KMAP_END是临时映射条目个数的下标。
通过函数kmap_atomic创建临时映射:
void *kmap_atomic(struct page *page, enum km_type type)
{
unsigned int idx;
unsigned long vaddr;
void *kmap;
pagefault_disable();
if (!PageHighMem(page))
return page_address(page);
debug_kmap_atomic(type);
kmap = kmap_high_get(page);
if (kmap)
return kmap;
idx = type + KM_TYPE_NR * smp_processor_id();
vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
#ifdef CONFIG_DEBUG_HIGHMEM
BUG_ON(!pte_none(*(TOP_PTE(vaddr))));
#endif
set_pte_ext(TOP_PTE(vaddr), mk_pte(page, kmap_prot), 0);
local_flush_tlb_kernel_page(vaddr);
return (void *)vaddr;
}
这部分源码的道理和永久映射的kmap非常相似,就不解释了。
高端映射其实还有很多应该理解的东西,后续随着应用不断补充吧。