版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/rikeyone/article/details/85223458
我们知道系统中的高端内存是不能作为直接映射区映射到内核空间的,那么我们想要使用它怎么办呢?前面的文章我们已经有过相关的介绍,可以使用三种方法,分别是pkmap(永久映射)/fixmap(临时固定映射)/vmlloc,本文主要介绍pkmap,也就是永久映射。
入口函数
首先我们来介绍pkmap的入口函数,它就是kmap:
void *kmap(struct page *page)
{
might_sleep();
if (!PageHighMem(page))
return page_address(page);
return kmap_high(page);
}
EXPORT_SYMBOL(kmap);
这个函数会根据该page是否存在与高端内存来区分处理,对于高端内存来说如下:
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);
进一步查看map_new_virtual函数,我们知道pkmap是在虚拟地址空间已经预留出来的一部分地址,我们现在需要申请一个地址用于对特定page的映射。
这个函数的目的就是找到未用的空闲pkmap地址,然后创建读应物理page和该虚拟地址之间的pte页表项。
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;
}
这个函数实现了如下功能:
- 循环查找pkmap区域,判断是否有空闲未用的虚拟地址页
- 如果找到未用的虚拟地址页,那么就中断循环,进行PTE页表的创建
- 如果未找到空闲虚拟地址页,说明所有的pkmap区域都已经被内核其他路径申请完了,则要进行释放zero空的pkmap映射。
- 释放后依然未找到空闲映射区,则申请映射的进程需要进行等待。
- 等待一段时候,系统被唤醒后,重新执行这一系列申请动作。
- 找到空闲映射区后,进行最后的PTE页表创建。
- set_page_address设置对应page的描述符,把申请的映射虚拟地址设置进去。
内核中定义了如下几个关键变量和宏用于pkmap的处理:
static int pkmap_count[LAST_PKMAP];
#define PKMAP_BASE (PAGE_OFFSET - PMD_SIZE)
#define LAST_PKMAP PTRS_PER_PTE
#define LAST_PKMAP_MASK (LAST_PKMAP - 1)
#define PKMAP_NR(virt) (((virt) - PKMAP_BASE) >> PAGE_SHIFT)
#define PKMAP_ADDR(nr) (PKMAP_BASE + ((nr) << PAGE_SHIFT))
可以看到这个PKMAP地址是从PKMAP_BASE这个内核定义开始的。是不是和前文的介绍对应起来了。
最后一个关键点
这个函数的实现可以看到,当pkmap区域被申请完后,下一次的kmap会引起当前进程的休眠,这个细节就导致该kmap的调用不能被用于中断中或者不可延迟的函数中。