依然把前面描述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();
/*如果该页其实是低端内存页,则会直接做偏移获取其映射的虚拟地址
否则调用函数kmap_high*/
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;
/*注意底下仅仅是获取了自旋锁,是为避免多CPU访问,但没有关中断!!!
因为永久映射是可能睡眠的,所以不能被中断上下文等不可以睡眠的场合应用,
所以就不用关中断了
另外,为什么永久映射可能会睡眠? 因为可能存在所有永久映射条目都已在映射了,
现在要想再加一个永久映射,就得等待里边某一个映射的释放才行*/
/*
* 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;
}
首先查看这个高端物理页是否已经做过高端映射了,通过函数page_address查看,
void *page_address(struct page *page)
{
unsigned long flags;
void *ret;
struct page_address_slot *pas;
/*如果不是高端地址,则直接返回page描述符所映射的虚拟地址*/
/*函数lowmem_page_address:
通过page_to_pfn获取page在mem_map中的位置
然后乘以页大小得到物理地址
最后通过__va计算出虚拟地址*/
if (!PageHighMem(page))
return lowmem_page_address(page);
/*返回该页(page)所在的哈希链表的表头*/
pas = page_slot(page);
ret = NULL;
/*由哈希链表头,遍历该链表查找该page所对应的虚拟地址*/
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:
/*对于我们marvell的arm,LAST_PKMAP值为512(对于普然和broadlight的mips,为1024即4M),即永久映射区可以映射的高端内存页为512个
即可以同时映射的高端内存为512 * 4K = 2M*/
count = LAST_PKMAP;
/* Find an empty entry */
/*不断试图从512个可映射页条目中找到一个空闲的,找到则break*/
for (;;) {
last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;
/*当找不到计数值为0的页表项时,就要调用flush_all_zero_pkmaps(),
将计数值为1的页表项置为0,即撤销已经不用了的映射,并且刷新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;
/*
* Sleep for somebody else to unmap their entries
*/
/*既未break退出也未返回continue,这说明现在512个可以映射的高端内存页,全都被正在映射着,
这时需要等待某个页释放它的映射,所以声明一个等待队列,并让出CPU
这也说明,申请高端内存的映射,是可能会睡眠的!所以不要用在中断上下文等不可睡眠代码的里面*/
{
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;
}
}
/*在512个可映射条目中找到空闲条目了,获取该页表项对应的线性地址并赋给vaddr
永久映射区中,物理页的虚拟地址也是线性的,也是通过偏移运算得出*/
vaddr = PKMAP_ADDR(last_pkmap_nr);
/*将pkmap_page_table中对应的pte设为申请映射的页框的pte,完成永久内核映射区中的页表项条目到物理页框的映射*/
set_pte_at(&init_mm, vaddr,
&(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));
/*内核用一个pkmap_count数组来记录pkmap_page_table中每一个页表项的使用状态,
其实就是为每个页表项分配一个计数器来记录相应的页表是否已经被用来映射。计数值分为以下三种情况:
1、计数值为0:对应的页表项没有映射高端内存,即为空闲可用的
2、计数值为1: 对应的页表项没有映射高端内存,但是不可用,因为上次映射后对应的TLB项还未被淸刷
3、计数值为n(n>1):对应的页表项已经映射了一个高端内存页框,并且有n-1个内核成分正在利用这种映射关系
这里先预设置为1,在返回到kmap_high中会再加1,表示被一个内核成分正在利用该映射*/
pkmap_count[last_pkmap_nr] = 1;
/*实现映射该页的虚拟地址,将该页及其虚拟地址添加到page_address_htable链表中*/
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));
/*找出该页(page)所在的哈希链表的表头*/
pas = page_slot(page);
/*如果是映射操作*/
if (virtual) { /* Add */
/*确保空闲链表page_address_pool不为空*/
BUG_ON(list_empty(&page_address_pool));
/*在临界区执行,取出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);
/*加入page页、所需要映射的虚拟地址*/
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 */
/*和映射操作整好相反,删除该页所在的哈希链表的该页的节点,
然后尾插法恢复空闲链表page_address_pool一个新节点*/
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;
}
临时映射和永久映射差别不是很大,只是说临时映射顾名思义,比较临时,和永久相反,永久映射若需要修改映射关系需要先释放映射再创建新的映射关系,而临时映射就是直接再映射一次把前面的映射关系覆盖掉就行了,确实是比较临时;所以临时映射不会睡眠,它不会像永久映射,可能产生条目满需要等待其他条目被释放的情况,如果条目满了它只需覆盖掉一个就行了;
对于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(同时适配多核)*/
idx = type + KM_TYPE_NR * smp_processor_id();
/*得到对应页表项的虚拟地址,也是通过加减偏移运算*/
vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
#ifdef CONFIG_DEBUG_HIGHMEM
/*
* With debugging enabled, kunmap_atomic forces that entry to 0.
* Make sure it was indeed properly unmapped.
*/
BUG_ON(!pte_none(*(TOP_PTE(vaddr))));
#endif
/*将页表项与page进行关联,用kmap_pte-idx而不是用kmap_pte+idx,因为固定映射区是逆向生长的,
也就是说枚举项越靠前的部分的虚拟地址越靠后*/
set_pte_ext(TOP_PTE(vaddr), mk_pte(page, kmap_prot), 0);
/*
* When debugging is off, kunmap_atomic leaves the previous mapping
* in place, so this TLB flush ensures the TLB is updated with the
* new mapping.
*/
/*更新CPU的TLB*/
local_flush_tlb_kernel_page(vaddr);
return (void *)vaddr;
}
这部分源码的道理和永久映射的kmap非常相似,就不解释了。
高端映射其实还有很多应该理解的东西,后续随着应用不断补充吧。